Covers: west, Kconfig, Devicetree, threads, timers, logging
Write your answer before revealing. These test understanding, not memorization.
Q1. You clone the Zephyr repository directly (git clone https://github.com/zephyrproject-rtos/zephyr) and run west build -b nucleo_h743zi2 myapp/. What critical pieces are missing compared to using west init, and what specific error do you expect?
Q2. Your colleague says “I changed CONFIG_BLINK_PERIOD_MS from 500 to 250 in prj.conf but the blink rate didn’t change after I pressed reset.” What’s going on, and what must they do?
Q3. Explain in one sentence what the find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) line in CMakeLists.txt does that a plain cmake_minimum_required() by itself does not.
Q4. A prj.conf contains CONFIG_SHELL=y. You can see in the Zephyr source that the shell module includes #include <zephyr/drivers/uart.h>. Does that mean you need to also add CONFIG_SERIAL=y and CONFIG_UART_CONSOLE=y to your prj.conf? Why or why not?
Q5. Your application overlay file is at myapp/boards/nucleo_h743zi2.overlay. The Zephyr board file at zephyr/boards/arm/nucleo_h743zi2/nucleo_h743zi2.dts also defines a leds node. Both define a node named led_0. What wins, and what keyword tells you that in the overlay syntax?
Q6. Explain the difference between CONFIG_LOG_DEFAULT_LEVEL=3 and CONFIG_LOG_OVERRIDE_LEVEL=3. When would you use each?
Q7. What is the difference between thread priority 0 and thread priority -1 in Zephyr? Could a thread at priority 0 ever starve a thread at priority -1?
Q8. Your 100Hz thread uses k_msleep(10) inside a while(1) loop. The work inside the loop takes exactly 0.5ms every iteration. After 1000 iterations, how many milliseconds have elapsed, and how far off is that from the target 10,000ms?
Q9. You call k_thread_stack_space_get(&my_thread_tid, &unused) and get unused=48 out of a 1024 byte stack. Should you be worried? What action do you take?
Q10. Your overlay file modifies pin PA5 of GPIOA for an LED. You discover the Nucleo-H743ZI2 board DTS already uses PA5 for a different peripheral. What happens at build time, and how do you resolve the conflict without touching the board DTS?
Q11. Why does LOG_DBG("value=%d", x) have zero overhead at runtime when CONFIG_LOG_DEFAULT_LEVEL=3? What makes it different from wrapping it in if (debug_flag) { printf(...); }?
Q12. A teammate says: “I’ll just use printf() instead of LOG_INF() — it’s simpler.” Name two specific reasons this is wrong in a Zephyr application.
Q13. What does K_THREAD_DEFINE do differently from calling k_thread_create() at runtime? When would you use k_thread_create() instead?
Q14. You add CONFIG_STACK_SENTINEL=y to prj.conf and see a kernel panic message: "Stack sentinel for thread worker_tid overflowed". What does the sentinel actually check, and what does this message guarantee about when the overflow happened?
Q15. An IRQ fires at hardware priority 3 while a thread at software priority 0 is running. Which preempts which, and what fundamental rule does this demonstrate about IRQ priorities vs thread priorities?
Each snippet has exactly ONE bug. Identify it and explain why it’s wrong. Provide the fix.
Bug 1. 100Hz timing with k_msleep
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(timer_work, LOG_LEVEL_DBG);
static void worker_thread(void *a, void *b, void *c)
{
uint32_t last = k_uptime_get_32();
while (1) {
k_msleep(10);
uint32_t now = k_uptime_get_32();
uint32_t dt = now - last;
last = now;
LOG_DBG("dt=%u ms", dt);
/* Simulate 2ms of processing work */
k_busy_wait(2000);
}
}
K_THREAD_DEFINE(worker_tid, 2048, worker_thread, NULL, NULL, NULL, 5, 0, 0);
K_SEM_DEFINE(tick_sem, 0, 1);
static void expiry(struct k_timer *t) { k_sem_give(&tick_sem); }
K_TIMER_DEFINE(tick_timer, expiry, NULL);
static void worker_thread(void *a, void *b, void *c)
{
k_timer_start(&tick_timer, K_MSEC(10), K_MSEC(10));
uint32_t last = k_uptime_get_32();
while (1) {
k_sem_take(&tick_sem, K_FOREVER); /* blocks until timer fires */
uint32_t now = k_uptime_get_32();
uint32_t dt = now - last;
last = now;
LOG_DBG("dt=%u ms", dt);
k_busy_wait(2000); /* timer fires at exactly t+10ms regardless */
}
}
Bug 2. Thread logging without enough stack
static void imu_thread(void *a, void *b, void *c)
{
LOG_INF("IMU thread started");
while (1) {
float ax, ay, az;
read_imu_i2c(&ax, &ay, &az);
LOG_INF("ax=%.3f ay=%.3f az=%.3f", ax, ay, az);
k_msleep(10);
}
}
K_THREAD_DEFINE(imu_tid, 512, imu_thread, NULL, NULL, NULL, 3, 0, 0);
K_THREAD_DEFINE(imu_tid, 2048, imu_thread, NULL, NULL, NULL, 3, 0, 0);
CONFIG_STACK_SENTINEL=y
CONFIG_THREAD_STACK_INFO=y
Bug 3. Custom Kconfig symbol not defined
myapp/
├── CMakeLists.txt
├── prj.conf ← contains CONFIG_SAMPLE_RATE_HZ=100
└── src/
└── main.c ← uses CONFIG_SAMPLE_RATE_HZ
prj.conf:
CONFIG_GPIO=y
CONFIG_LOG=y
CONFIG_SAMPLE_RATE_HZ=100
src/main.c:
#include <zephyr/kernel.h>
int main(void)
{
int period_ms = 1000 / CONFIG_SAMPLE_RATE_HZ;
k_msleep(period_ms);
return 0;
}
Build produces: warning: CONFIG_SAMPLE_RATE_HZ' not defined, defaulting to 0 and then a divide-by-zero at runtime.
mainmenu "My Application"
config SAMPLE_RATE_HZ
int "Sample rate in Hz"
default 100
range 1 1000
help
The sampling rate for sensor acquisition in Hertz.
Rebuild required after changing this value.
Bug 4. Timer expiry function doing too much
K_SEM_DEFINE(timer_sem, 0, 1);
static float sensor_reading = 0.0f;
static void timer_expiry_fn(struct k_timer *timer_id)
{
/* Read sensor directly in timer callback */
sensor_reading = read_i2c_sensor_blocking(); /* takes ~2ms */
k_sem_give(&timer_sem);
LOG_DBG("Timer fired, reading=%.3f", sensor_reading);
}
K_TIMER_DEFINE(my_timer, timer_expiry_fn, NULL);
static void timer_expiry_fn(struct k_timer *timer_id)
{
k_sem_give(&timer_sem); /* only this — no blocking, no LOG */
}
static void sensor_thread(void *a, void *b, void *c)
{
k_timer_start(&my_timer, K_MSEC(10), K_MSEC(10));
while (1) {
k_sem_take(&timer_sem, K_FOREVER);
/* Now in thread context — blocking I2C is safe */
float reading = read_i2c_sensor_blocking();
LOG_DBG("reading=%.3f", reading);
}
}
Bug 5. Overlay modifying a node that doesn’t exist
/* boards/nucleo_h743zi2.overlay */
&my_custom_spi {
status = "okay";
cs-gpios = <&gpioa 4 GPIO_ACTIVE_LOW>;
};
Build error: devicetree error: undefined node label 'my_custom_spi'
/* boards/nucleo_h743zi2.overlay */
&spi1 {
status = "okay";
cs-gpios = <&gpioa 4 GPIO_ACTIVE_LOW>;
pinctrl-0 = <&spi1_sck_pa5 &spi1_miso_pa6 &spi1_mosi_pa7>;
pinctrl-names = "default";
};
Bug 6. Shell command registered but never found
/* src/commands.c */
#include <zephyr/shell/shell.h>
static int cmd_status(const struct shell *sh, size_t argc, char **argv)
{
shell_print(sh, "System OK");
return 0;
}
SHELL_CMD_REGISTER(status, NULL, "Print system status", cmd_status);
# CMakeLists.txt
cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(myapp)
target_sources(app PRIVATE src/main.c)
At runtime: typing status in the shell returns "Command not found".
target_sources(app PRIVATE
src/main.c
src/commands.c # ← was missing
)
Bug 7. Misusing k_uptime_get_32() for relative timing
static void measure_work_duration(void)
{
uint32_t start = k_uptime_get_32();
do_expensive_work();
/* Report elapsed time */
uint32_t elapsed = start - k_uptime_get_32();
LOG_INF("Work took %u ms", elapsed);
}
uint32_t start = k_uptime_get_32();
do_expensive_work();
uint32_t elapsed = k_uptime_get_32() - start; /* now - start, not start - now */
LOG_INF("Work took %u ms", elapsed);
Bug 8. Two UART backends fighting
# prj.conf
CONFIG_SHELL=y
CONFIG_LOG=y
CONFIG_LOG_BACKEND_UART=y
CONFIG_UART_CONSOLE=y
CONFIG_SERIAL=y
/* boards/nucleo_h743zi2.overlay — no changes */
Terminal shows interleaved garbage: [00[00:00[INF]:01.23 blin:00:01ky.230 INF] ...]
CONFIG_LOG_BACKEND_UART=n
CONFIG_LOG_BACKEND_SHELL=y
&usart1 {
status = "okay";
current-speed = <115200>;
pinctrl-0 = <&usart1_tx_pa9 &usart1_rx_pa10>;
pinctrl-names = "default";
};
CONFIG_SHELL_BACKEND_SERIAL=y
CONFIG_UART_SHELL_ON_DEV_NAME="USART_1"
Bug 9. Thread silently stops
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(sensor, LOG_LEVEL_INF);
static void sensor_thread(void *a, void *b, void *c)
{
LOG_INF("Sensor thread started");
while (1) {
int ret = read_sensor_data();
if (ret < 0) {
LOG_ERR("Sensor read failed: %d", ret);
return; /* exit thread on error */
}
process_and_log(ret);
k_msleep(10);
}
}
K_THREAD_DEFINE(sensor_tid, 2048, sensor_thread, NULL, NULL, NULL, 5, 0, 0);
The sensor occasionally fails, and after a failure the system appears to keep running (main thread works) but sensor data stops being processed. No crash.
static void sensor_thread(void *a, void *b, void *c)
{
LOG_INF("Sensor thread started");
uint32_t consecutive_errors = 0;
while (1) {
int ret = read_sensor_data();
if (ret < 0) {
consecutive_errors++;
LOG_WRN("Sensor read failed: %d (error #%u)", ret, consecutive_errors);
if (consecutive_errors > 10) {
LOG_ERR("Too many sensor errors — reinitializing");
init_sensor(); /* attempt recovery */
consecutive_errors = 0;
}
k_msleep(100); /* back off before retry */
continue; /* do NOT return — keep thread alive */
}
consecutive_errors = 0;
process_and_log(ret);
k_msleep(10);
}
}
Bug 10. LOG_MODULE_REGISTER called inside a function
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
int main(void)
{
LOG_MODULE_REGISTER(myapp, LOG_LEVEL_INF); /* inside main() */
LOG_INF("System started");
while (1) {
k_msleep(1000);
}
return 0;
}
Build error: error: 'log_myapp' undeclared or linker error about duplicate log module.
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(myapp, LOG_LEVEL_INF); /* file scope — correct */
int main(void)
{
LOG_INF("System started");
while (1) {
k_msleep(1000);
}
return 0;
}
Fill in the blanks (marked _____) to complete working Zephyr code. Show answers in the details block.
Exercise C1. Complete this prj.conf for a project that needs: interactive shell on UART, structured logging at INFO level, GPIO, and a 2KB main stack.
CONFIG_GPIO=_____
CONFIG_SHELL=_____
CONFIG_SHELL_BACKEND_SERIAL=_____
CONFIG_LOG=_____
CONFIG_LOG_DEFAULT_LEVEL=_____
CONFIG_MAIN_STACK_SIZE=_____
CONFIG_UART_CONSOLE=_____
CONFIG_SERIAL=_____
CONFIG_GPIO=y
CONFIG_SHELL=y
CONFIG_SHELL_BACKEND_SERIAL=y
CONFIG_LOG=y
CONFIG_LOG_DEFAULT_LEVEL=3 # 3 = INF
CONFIG_MAIN_STACK_SIZE=2048
CONFIG_UART_CONSOLE=y
CONFIG_SERIAL=y
Exercise C2. Fill in the blanks to define a Kconfig symbol for a configurable sensor threshold:
config SENSOR_THRESHOLD_MG
_____ "Sensor alarm threshold in milligrams"
_____ 500
_____ 10 10000
help
Acceleration threshold above which an alarm is triggered.
Units: milli-g. Rebuild required after changing.
config SENSOR_THRESHOLD_MG
int "Sensor alarm threshold in milligrams"
default 500
range 10 10000
help
Acceleration threshold above which an alarm is triggered.
Units: milli-g. Rebuild required after changing.
Exercise C3. Complete this overlay to map the led0 alias to pin PB14 of GPIOB, active-high:
/ {
_____ {
led0 = &_____custom_led;
};
leds {
compatible = "_____";
custom_led: led_custom_0 {
gpios = <&_____ _____ GPIO_ACTIVE_HIGH>;
label = "Custom LED PB14";
};
};
};
/ {
aliases {
led0 = &custom_led;
};
leds {
compatible = "gpio-leds";
custom_led: led_custom_0 {
gpios = <&gpiob 14 GPIO_ACTIVE_HIGH>;
label = "Custom LED PB14";
};
};
};
Exercise C4. Complete this K_THREAD_DEFINE call for a thread named sensor_tid, stack size 2048, that runs sensor_fn(NULL, NULL, NULL) at priority 7, starting 100ms after boot:
K_THREAD_DEFINE(
_____, /* thread name */
_____, /* stack size */
_____, /* entry function */
_____, _____, _____, /* arg1, arg2, arg3 */
_____, /* priority */
_____, /* options */
_____ /* start delay ms */
);
K_THREAD_DEFINE(
sensor_tid, /* thread name */
2048, /* stack size */
sensor_fn, /* entry function */
NULL, NULL, NULL, /* arg1, arg2, arg3 */
7, /* priority (preemptive, lower priority than main's 0) */
0, /* options (0 = normal thread) */
100 /* start delay: 100ms */
);
Exercise C5. Fill in the blanks to implement the correct 100Hz timer setup:
K_SEM_DEFINE(_____, 0, _____); /* initial=0, max=1 */
static void expiry_fn(struct k_timer *t)
{
k_sem_give(&_____);
}
K_TIMER_DEFINE(my_timer, _____, NULL);
static void timer_thread(void *a, void *b, void *c)
{
k_timer_start(&my_timer, K_MSEC(_____), K_MSEC(_____));
while (1) {
k_sem_take(&_____, K_FOREVER);
/* do 100Hz work */
}
}
K_SEM_DEFINE(tick_sem, 0, 1); /* initial=0 (no ticks yet), max=1 (don't accumulate) */
static void expiry_fn(struct k_timer *t)
{
k_sem_give(&tick_sem);
}
K_TIMER_DEFINE(my_timer, expiry_fn, NULL);
static void timer_thread(void *a, void *b, void *c)
{
k_timer_start(&my_timer, K_MSEC(10), K_MSEC(10)); /* fire in 10ms, repeat every 10ms */
while (1) {
k_sem_take(&tick_sem, K_FOREVER);
/* do 100Hz work */
}
}
Exercise C6. Fill in the blanks to measure and log the jitter of a periodic thread. The dt_ms variable is already computed:
static uint32_t _____ = 0;
static uint32_t _____ = UINT32_MAX;
static uint32_t sample_n = 0;
/* Inside the 100Hz loop, after computing dt_ms: */
if (dt_ms > _____) { _____ = dt_ms; }
if (dt_ms < _____) { _____ = dt_ms; }
_____++;
if (sample_n == _____) { /* every 10 seconds */
LOG_INF("Jitter 10s: min=%u max=%u spread=%u ms",
_____, _____, _____ - _____);
_____ = 0;
_____ = UINT32_MAX;
_____ = 0;
}
static uint32_t max_dt = 0;
static uint32_t min_dt = UINT32_MAX;
static uint32_t sample_n = 0;
if (dt_ms > max_dt) { max_dt = dt_ms; }
if (dt_ms < min_dt) { min_dt = dt_ms; }
sample_n++;
if (sample_n == 1000) { /* every 10 seconds at 100Hz */
LOG_INF("Jitter 10s: min=%u max=%u spread=%u ms",
min_dt, max_dt, max_dt - min_dt);
max_dt = 0;
min_dt = UINT32_MAX;
sample_n = 0;
}
Exercise C7. Complete this CMakeLists.txt for a project with two source files (src/main.c and src/sensors.c) that also needs to include a local header directory src/include/:
cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr _____ HINTS $ENV{ZEPHYR_BASE})
project(sensor_app)
target_sources(app PRIVATE
_____
_____
)
target_include_directories(app PRIVATE
_____
)
cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(sensor_app)
target_sources(app PRIVATE
src/main.c
src/sensors.c
)
target_include_directories(app PRIVATE
src/include/
)
Exercise C8. Fill in the correct prj.conf settings to enable stack overflow detection during development AND allow inspecting the stack high-water mark at runtime:
CONFIG_STACK_SENTINEL=_____ # runtime panic on overflow
CONFIG___________=y # enables k_thread_stack_space_get()
CONFIG_FAULT_DUMP=_____ # emit full register dump on hard fault
CONFIG_STACK_SENTINEL=y
CONFIG_THREAD_STACK_INFO=y
CONFIG_FAULT_DUMP=2
Hands-on tasks requiring hardware (Nucleo-H743ZI2 + CP2102 USB-UART adapter). Each task has a specific, verifiable success criterion.
Lab Task D1. Build and flash your first Blinky from scratch
Steps:
1. Create the directory structure: myapp/, myapp/src/, myapp/boards/.
2. Write CMakeLists.txt with the minimal Zephyr template (section 2.1 of study notes).
3. Write prj.conf with CONFIG_GPIO=y and CONFIG_BLINK_PERIOD_MS=500.
4. Write a Kconfig file defining the BLINK_PERIOD_MS symbol (int, default 500, range 50–5000).
5. Write src/main.c using DT_ALIAS(led0) and gpio_pin_toggle_dt().
6. Run west build -b nucleo_h743zi2 myapp/ and verify it compiles.
7. Run west flash.
8. Change CONFIG_BLINK_PERIOD_MS=100 in prj.conf, rebuild, reflash.
Success criterion: LED blinks at ~1Hz with the 500ms config. After changing to 100ms config and reflashing, LED blinks visibly faster (~5Hz). The rate change persists after pressing the reset button (it doesn’t revert to 500ms).
If it doesn’t work:
LOG_INF("main entered") before the while loop and verify log output. If log appears but no blink, the overlay’s led0 alias is wrong — check it with west build -t guiconfig → “Devicetree” → expand /aliases.Board nucleo_h743zi2 not found: Run source ~/zephyrproject/zephyr/zephyr-env.sh or check ZEPHYR_BASE.west build again before west flash. Check build/zephyr/.config and search for BLINK_PERIOD to verify the new value is present.Lab Task D2. Add UART logging and observe it from minicom
Steps:
1. Add to prj.conf: CONFIG_LOG=y, CONFIG_LOG_DEFAULT_LEVEL=4, CONFIG_LOG_BACKEND_UART=y, CONFIG_UART_CONSOLE=y.
2. Add LOG_MODULE_REGISTER(blinky, LOG_LEVEL_DBG) at file scope in main.c.
3. Add LOG_INF("Blinky started, period=%d ms", CONFIG_BLINK_PERIOD_MS) in main() before the loop.
4. Add LOG_DBG("Toggle complete, cycle=%d", cycle) inside the loop (increment a cycle counter).
5. Build, flash, open minicom at 115200 8N1.
6. Change CONFIG_LOG_DEFAULT_LEVEL=3 (INF), rebuild, reflash.
Success criterion at level 4 (DBG): minicom shows both INF and DBG messages. You should see [INF] blinky: Blinky started, period=500 ms once, then [DBG] blinky: Toggle complete, cycle=1 etc. at 2 messages per second (toggling every 500ms). After changing to level 3 and reflashing: only the INF startup message appears; DBG messages are completely gone.
If it doesn’t work:
/dev/ttyUSB* device.CONFIG_UART_SHELL_ON_DEV_NAME (if set) doesn’t equal CONFIG_UART_CONSOLE_ON_DEV_NAME.LOG_DBG messages missing even at level 4: Check that LOG_MODULE_REGISTER uses LOG_LEVEL_DBG (not LOG_LEVEL_INF) as the second argument.Lab Task D3. Build the UART shell and control blink rate at runtime
CONFIG_SHELL=y and shell UART working.Steps:
1. Add CONFIG_SHELL=y and CONFIG_SHELL_BACKEND_SERIAL=y to prj.conf.
2. Add #include <zephyr/shell/shell.h> and declare a global volatile uint32_t blink_period_ms = CONFIG_BLINK_PERIOD_MS.
3. Implement cmd_blink_set and cmd_blink_get handlers (section 2.4 of study notes).
4. Register with SHELL_CMD_REGISTER.
5. Change main() to use blink_period_ms (the runtime variable) instead of CONFIG_BLINK_PERIOD_MS in k_msleep.
6. Build, flash, connect minicom.
7. Press Enter to get uart:~$ prompt. Type blink get. Then type blink set 100. Observe LED rate change.
Success criterion: blink get returns the current period. blink set 100 causes the LED to visibly speed up within one blink cycle — without reflashing. blink set 5001 returns an error message. The shell prompt reappears after each command.
If it doesn’t work:
uart:~$ prompt: Verify CONFIG_SHELL=y and CONFIG_SHELL_BACKEND_SERIAL=y. Press Enter a few times (shell waits for newline). Check minicom is in “no hardware flow” mode.Command not found for blink: The .c file defining SHELL_CMD_REGISTER may not be in target_sources. Or CONFIG_SHELL_CMDS=y may be required — check with west build -t menuconfig.CONFIG_LOG_BACKEND_SHELL=y instead of CONFIG_LOG_BACKEND_UART=y so log output goes through the shell’s serialized output path.Lab Task D4. Implement and verify 100Hz timer with drift measurement
CONFIG_LOG=y and logging working from D2.Steps:
1. Implement the k_timer + k_sem_take pattern (section 2.7 of study notes) in a separate thread.
2. Compute dt_ms = now - last inside the thread loop.
3. Log with LOG_DBG("tick=%u dt_ms=%u", tick_count, dt_ms) — increment tick_count each iteration.
4. Add LOG_WRN("drift: dt=%u", dt_ms) when dt_ms > 11 || dt_ms < 9.
5. Deliberately add k_busy_wait(500) (0.5ms of fake work) to the thread and verify it does NOT cause drift.
6. Then change to k_busy_wait(15000) (15ms — longer than the 10ms period). Observe the overrun warning.
7. Remove the busy_wait.
Success criterion: With 0.5ms of work: dt_ms values are consistently 10 (no WRN messages). Over 100 ticks (1 second), the cumulative drift measured as (actual_elapsed - 1000ms) is under 5ms. With 15ms of work: WRN messages appear immediately, and dt_ms shows values of 10 or 20 (skipped ticks), not 25ms (which k_msleep would produce).
If it doesn’t work:
CONFIG_STACK_SENTINEL=y to get a useful panic. Increase thread stack to 2048.dt_ms shows 0 every tick: last_tick_ms not being updated. Ensure you do last_tick_ms = now inside the loop after computing dt.dt_ms consistently shows ~10.3ms instead of 10: You’re using k_msleep(10) not k_timer. Double-check the implementation uses k_sem_take blocking on the semaphore, not a sleep.Lab Task D5. Profile stack usage and set appropriate sizes
CONFIG_THREAD_STACK_INFO=y to prj.conf.Steps:
1. Add a shell command stack_info that calls k_thread_stack_space_get() on your timer thread and prints the unused bytes.
2. Run the system for 60 seconds to allow the worst-case stack depth to be reached.
3. Execute stack_info in the shell. Note the unused value.
4. Calculate: high_water = stack_size − unused. Headroom% = (unused / stack_size) × 100.
5. Experiment: deliberately reduce the timer thread stack to unused - 50 (just below the actual high-water + 50 margin). Enable CONFIG_STACK_SENTINEL=y. Expect a kernel panic. Observe the panic message.
6. Restore the correct stack size with ≥20% headroom.
Success criterion: Step 4 should show headroom > 20% with the default 2048-byte stack if only LOG_DBG calls are in the thread. Step 5 should produce a visible kernel panic message containing “Stack sentinel” and the thread name within a few seconds of running (not a silent lockup). After step 6, the system runs stably with the panic gone.
If it doesn’t work:
k_thread_stack_space_get returns error: Ensure CONFIG_THREAD_STACK_INFO=y is set and you are passing &timer_tid (the k_thread struct address), not a raw pointer.LOG_INF with a long format string and several arguments — this forces more stack usage.These questions require synthesizing multiple concepts. No single answer is correct — they’re thinking prompts for self-reflection or pair discussion.
E1. You have three threads: IMU reader (priority 3, 100Hz), UART shell (priority 5, event-driven), and a background data packer (priority 8, continuous). The IMU reader uses k_timer + k_sem. The UART shell calls LOG_INF whenever a command is received.
You observe that every 10th IMU tick has dt_ms=11 instead of 10. The other 9 are exactly 10. No LOG_WRN about overruns in the shell. The pattern repeats exactly every 10 ticks.
Propose one mechanistic hypothesis that explains this exact pattern, and describe what two measurements you would make to confirm or refute it.
E2. Consider the statement: “We don’t need CONFIG_STACK_SENTINEL=y in production because we’ve profiled the stack and have 30% headroom.”
Argue both sides: (a) why this reasoning is sound, and (b) why it could still be wrong. What real-world event could invalidate the 30% headroom measurement after the firmware ships?
E3. Your blinky app works perfectly. Then you add CONFIG_SHELL=y and the LED stops blinking entirely — no crash, no error message. LOG_INF("main entered") appears in the terminal, but nothing after it.
Trace through the most likely failure mode: what did enabling CONFIG_SHELL=y probably change about UART pin assignments or thread scheduling that could cause this?
E4. You are debugging a hard fault. The fault handler prints: CFSR: 0x00000001 MMFAR: 0x00000000. A CFSR value of 0x00000001 means IACCVIOL — instruction access violation. The MMFAR (MemManage Fault Address Register) is 0x00000000, meaning the CPU tried to fetch an instruction from address 0x0.
Given only this information, construct two different hypotheses about what program error could lead to a function call to address 0x0. Which of CONFIG_STACK_SENTINEL=y or CONFIG_FAULT_DUMP=2 would have given you more useful data, and why?
E5. West uses a west.yml manifest to pin every dependency to a specific commit SHA. A colleague proposes: “We should stop using west and just vendor all the Zephyr files into our repo — then we own them and don’t need internet access to build.”
Evaluate this proposal. What does it get right? What problems would it create at month 6 when you need to pull a security patch from Zephyr upstream? Suggest a middle ground that preserves the benefits of both approaches.
E6. In section 1.3 of the study notes, prj.conf is described as controlling “compile-time” behavior. Yet with CONFIG_LOG_RUNTIME_FILTERING=y and log enable dbg my_module in the shell, you can change behavior at runtime without rebuilding. Does this contradict the compile-time model? Explain precisely what is still compile-time vs what becomes runtime when CONFIG_LOG_RUNTIME_FILTERING is enabled.
End of exercises for Section 01. Next: Section 02 — I2C IMU integration and DMA.