Q1. You have one thread reading from an I2C accelerometer and another thread building SPI frames. Without ZBus, what specific failure can occur when both threads access the same global struct imu_data? Describe a concrete scenario where accel_x, accel_y, and accel_z are internally inconsistent in a single read.
Q2. Explain the difference between a ZBus subscriber and a ZBus listener in terms of:
- Which thread their code runs in
- Whether they can call k_msleep()
- Whether they can miss a message
Q3. What does proto3 do with a field that equals its default value during encoding? Give the concrete example of what happens to a SensorFrame message when the GPS loses fix and all GPS position fields become zero.
Q4. The ZBus channel holds only the latest value. Explain what this means for a subscriber publishing at 100Hz. If the packer thread runs at only 50Hz (once every 20ms), does it receive every message published in those 20ms? What does it receive?
Q5. What does ZBUS_OBSERVER_DEFINE (and its siblings ZBUS_SUBSCRIBER_DEFINE, ZBUS_LISTENER_DEFINE) actually do at the linker level? Why does an observer work globally without any explicit “register” call in main()?
Q6. Explain why pb_encode() can return true but the SPI frame still causes the Jetson to decode garbage. What specific check are beginners missing?
// WRONG:
pb_decode(&in_stream, ...) where in_stream was created with sizeof(buffer)
Q7. You have a listener callback that is called 100 times per second. The callback calls LOG_INF("accel_x=%.3f", data.accel_x). What is the likely failure mode, and how would you diagnose it?
Q8. What is the stack depth danger when calling pb_encode() on a SensorFrame that contains both ImuData and WheelData sub-messages? Why is this worse than encoding a flat message?
pb_encode(SensorFrame) ~120 bytes
└─ pb_encode_submessage(ImuData) ~80 bytes
└─ pb_encode(ImuData) ~120 bytes
└─ pb_encode_fixed32() ~16 bytes each (×7 float fields)
Q9. What is the _init_zero idiom in nanopb and why is it mandatory? Write a code snippet showing exactly what happens if you forget it.
void bad_encode_fn(void)
{
SensorFrame frame; // uninitialized — stack garbage!
frame.seq = 42; // set one field
// frame.has_imu is now random stack bytes — might be 1 or might be another value
// frame.imu.* fields contain whatever was on the stack from a previous function call
pb_ostream_t stream = pb_ostream_from_buffer(buf, sizeof(buf));
pb_encode(&stream, SensorFrame_fields, &frame);
// If has_imu was randomly non-zero on the stack, the encoder will attempt to encode
// imu with random float values — looks like valid data on the Jetson,
// but it's complete garbage that your robot's EKF will process.
}
Q10. A colleague proposes: “When GPS loses fix, just send the last known GPS coordinates instead of zeros, so the payload size stays constant and we don’t have the variable-length problem.” Evaluate this proposal. Is it correct? What are its failure modes?
Bug 1. What is wrong with this subscriber thread?
ZBUS_SUBSCRIBER_DEFINE(logger_sub, 4);
void logger_thread_fn(void *a, void *b, void *c)
{
const struct zbus_channel *chan;
struct imu_data data;
zbus_chan_add_obs(&imu_chan, &logger_sub, K_MSEC(5));
while (1) {
zbus_sub_wait(&logger_sub, &chan, K_FOREVER);
// Read data directly from our own copy
zbus_chan_read(&logger_sub, &data, K_NO_WAIT);
LOG_INF("ax=%.2f", data.accel_x);
}
}
K_THREAD_DEFINE(logger_tid, 512, logger_thread_fn, NULL, NULL, NULL, 7, 0, 0);
Bug 2. Find the bug in this listener:
static void data_ready_listener(const struct zbus_channel *chan)
{
struct imu_data data;
zbus_chan_read(chan, &data, K_MSEC(100));
k_sem_give(&imu_data_ready_sem);
}
ZBUS_LISTENER_DEFINE(imu_listener, data_ready_listener);
zbus_chan_read(chan, &data, K_NO_WAIT);
Bug 3. What is wrong with this encoding code?
uint8_t spi_buf[128];
void encode_and_send(const struct imu_data *imu)
{
SensorFrame frame;
frame.seq = g_seq++;
frame.has_imu = true;
frame.imu.accel_x = imu->accel_x;
frame.imu.accel_y = imu->accel_y;
frame.imu.accel_z = imu->accel_z;
pb_ostream_t stream = pb_ostream_from_buffer(spi_buf, sizeof(spi_buf));
pb_encode(&stream, SensorFrame_fields, &frame);
spi_slave_set_tx_buffer(spi_buf, sizeof(spi_buf));
}
Bug 4. This channel definition is in a header file. What breaks?
// channels.h
#ifndef CHANNELS_H
#define CHANNELS_H
#include <zephyr/zbus/zbus.h>
struct imu_data {
float accel_x, accel_y, accel_z;
};
ZBUS_CHAN_DEFINE( // ← in header!
imu_chan,
struct imu_data,
NULL, NULL,
ZBUS_OBSERVERS_EMPTY,
ZBUS_MSG_INIT(0)
);
#endif
Bug 5. What does this decode code do wrong?
// Jetson-side C code reading SPI
uint8_t rx_buf[128];
spi_read(rx_buf, sizeof(rx_buf)); // reads exactly 128 bytes
pb_istream_t stream = pb_istream_from_buffer(rx_buf, sizeof(rx_buf));
SensorFrame frame = SensorFrame_init_zero;
bool ok = pb_decode(&stream, SensorFrame_fields, &frame);
uint16_t payload_len = (rx_buf[2] << 8) | rx_buf[3];
pb_istream_t stream = pb_istream_from_buffer(rx_buf + 4, payload_len);
Bug 6. This code uses a ZBus listener to handle incoming SPI data requests. Find the problem:
static void spi_request_listener_cb(const struct zbus_channel *chan)
{
struct spi_request req;
zbus_chan_read(chan, &req, K_NO_WAIT);
// Prepare the response buffer
uint8_t response[64];
encode_response(&req, response, sizeof(response));
// Write to SPI peripheral register
spi_write_blocking(SPI1, response, sizeof(response));
}
ZBUS_LISTENER_DEFINE(spi_request_handler, spi_request_listener_cb);
static void spi_request_listener_cb(const struct zbus_channel *chan)
{
// Cache the request (fast copy)
zbus_chan_read(chan, &g_pending_request, K_NO_WAIT);
k_sem_give(&spi_work_sem); // fast, non-blocking, ISR-safe
}
// A separate thread waits on spi_work_sem and does the actual SPI write
Bug 7. What is the versioning bug in this .proto change?
// sensor_frame.proto v1
message ImuData {
float accel_x = 1;
float accel_y = 2;
float accel_z = 3;
uint64 timestamp_us = 4;
}
// sensor_frame.proto v2 — new engineer added temperature
message ImuData {
float accel_x = 1;
float accel_y = 2;
float temperature = 3; // ← new field placed at old accel_z's number
float accel_z = 4; // ← accel_z moved to field 4
uint64 timestamp_us = 5; // ← timestamp_us also renumbered
}
message ImuData {
float accel_x = 1;
float accel_y = 2;
float accel_z = 3; // unchanged
uint64 timestamp_us = 4; // unchanged
float temperature = 5; // NEW — gets next available number
}
Bug 8. This code is in the 100Hz publisher thread. What is the race condition?
static struct imu_data g_imu_shared; // global — packer reads this
void imu_thread_fn(void *a, void *b, void *c)
{
struct imu_data local;
while (1) {
read_icm42688(&local);
local.timestamp_us = k_ticks_to_us_near64(k_uptime_ticks());
// Publish to ZBus (safe, atomic copy)
zbus_chan_pub(&imu_chan, &local, K_MSEC(1));
// ALSO update the global for direct access by legacy code
g_imu_shared = local; // struct assignment
k_msleep(10);
}
}
C1. Complete the ZBUS_CHAN_DEFINE call for a GPS channel that uses struct gps_data, has no validator, is initially zeroed, and has no compile-time observers (observers will be added at runtime):
ZBUS_CHAN_DEFINE(
_________, // channel name
_________, // message type
_________, // validator (none)
_________, // user_data (none)
_________, // observers
_________ // initial value
);
ZBUS_CHAN_DEFINE(
gps_chan,
struct gps_data,
NULL,
NULL,
ZBUS_OBSERVERS_EMPTY,
ZBUS_MSG_INIT(0)
);
C2. Complete the subscriber thread pattern. The subscriber should wait on imu_chan notifications with no timeout and read the data when notified:
ZBUS_SUBSCRIBER_DEFINE(analysis_sub, _____); // queue depth for 100Hz with 50ms tolerance
void analysis_thread_fn(void *a, void *b, void *c)
{
const struct zbus_channel *chan;
struct imu_data data;
// Register as observer of imu_chan
_____(_____, _____, K_MSEC(5));
while (1) {
int rc = _____(_____, _____, _____); // block until notification
if (rc != 0) continue;
if (chan == _____) {
_____(_____, _____, K_NO_WAIT); // read latest
run_analysis(&data);
}
}
}
ZBUS_SUBSCRIBER_DEFINE(analysis_sub, 5); // 50ms/10ms = 5 slots
void analysis_thread_fn(void *a, void *b, void *c)
{
const struct zbus_channel *chan;
struct imu_data data;
zbus_chan_add_obs(&imu_chan, &analysis_sub, K_MSEC(5));
while (1) {
int rc = zbus_sub_wait(&analysis_sub, &chan, K_FOREVER);
if (rc != 0) continue;
if (chan == &imu_chan) {
zbus_chan_read(&imu_chan, &data, K_NO_WAIT);
run_analysis(&data);
}
}
}
C3. Fill in the encode_sensor_frame() function body for the encode stream setup and the mandatory checks:
uint16_t encode_sensor_frame(SensorFrame *frame, uint8_t *buf, size_t buf_size)
{
// 1. Set up output stream
pb_ostream_t stream = _________________________;
// 2. Encode
bool ok = _________________________;
// 3. Check for error and log it
if (!ok) {
LOG_ERR("pb_encode failed: %s", _________);
return 0;
}
// 4. Return the ACTUAL bytes written (not sizeof(buf))
return (uint16_t)_____________;
}
uint16_t encode_sensor_frame(SensorFrame *frame, uint8_t *buf, size_t buf_size)
{
pb_ostream_t stream = pb_ostream_from_buffer(buf, buf_size);
bool ok = pb_encode(&stream, SensorFrame_fields, frame);
if (!ok) {
LOG_ERR("pb_encode failed: %s", PB_GET_ERROR(&stream));
return 0;
}
return (uint16_t)stream.bytes_written;
}
C4. Complete the SPI framing header write. The magic bytes are 0xDE 0xAD. payload_len is 16-bit big-endian:
void write_frame_header(uint8_t *frame_buf, uint16_t payload_len)
{
frame_buf[0] = _____;
frame_buf[1] = _____;
frame_buf[2] = _____; // high byte of payload_len
frame_buf[3] = _____; // low byte of payload_len
}
// On the Jetson receiver, extract the header:
bool parse_frame_header(const uint8_t *buf, uint16_t *out_len)
{
if (buf[0] != _____ || buf[1] != _____) {
return false; // desync
}
*out_len = (____) ; // reconstruct uint16_t
return true;
}
void write_frame_header(uint8_t *frame_buf, uint16_t payload_len)
{
frame_buf[0] = 0xDE;
frame_buf[1] = 0xAD;
frame_buf[2] = (uint8_t)(payload_len >> 8);
frame_buf[3] = (uint8_t)(payload_len & 0xFF);
}
bool parse_frame_header(const uint8_t *buf, uint16_t *out_len)
{
if (buf[0] != 0xDE || buf[1] != 0xAD) {
return false;
}
*out_len = ((uint16_t)buf[2] << 8) | buf[3];
return true;
}
C5. Fill in the .options file entry and the .proto message for a struct that includes a robot name (max 32 chars including null) and a status string (max 16 chars including null):
// status.proto
message RobotStatus {
uint32 robot_id = 1;
string _______ = 2; // robot name
string _______ = 3; // status
uint64 uptime_ms = 4;
}
# status.options
RobotStatus._______ max_size:____
RobotStatus._______ max_size:____
message RobotStatus {
uint32 robot_id = 1;
string name = 2;
string status = 3;
uint64 uptime_ms = 4;
}
RobotStatus.name max_size:32
RobotStatus.status max_size:16
C6. A thread for the packer is defined below. Fill in the missing values based on these requirements: must tolerate 20ms of ZBus backlog at 100Hz; must accommodate pb_encode + LOG_ERR on stack; should run at lower priority than the IMU thread (priority 3) but higher than idle:
K_THREAD_DEFINE(
packer_tid,
_____, // stack size
packer_thread_fn,
NULL, NULL, NULL,
_____, // priority
0,
0
);
ZBUS_SUBSCRIBER_DEFINE(packer_sub, _____); // queue depth
K_THREAD_DEFINE(
packer_tid,
2048, // 2048: pb_encode ~750 bytes + LOG_ERR ~400 bytes + margin
packer_thread_fn,
NULL, NULL, NULL,
4, // priority 4: lower than IMU (3), higher than default idle (14)
0,
0
);
ZBUS_SUBSCRIBER_DEFINE(packer_sub, 2); // 20ms / 10ms = 2 notification slots
Lab 1: ZBus Silent Drop Detection
Goal: Reproduce and measure the ZBus silent drop behavior.
Setup:
1. Create an IMU simulator thread that publishes to imu_chan every 10ms, with a seq field that increments by 1 each publish.
2. Create a subscriber thread with queue depth 1 and simulate it being “slow” by adding k_msleep(25) inside the loop.
3. On the Jetson (or via UART log), print delta_seq = current_seq - prev_seq for every received message.
Verification criteria:
- Without the k_msleep(25): delta_seq should always be 1. If you see any values > 1, your test setup is wrong.
- With the k_msleep(25): delta_seq should frequently be 2–3. Log these as WRN.
- Add zbus_obs_get_chan_drop_cnt() to a shell command and verify the drop count matches your delta_seq > 1 count.
- Increase queue depth from 1 to 4 to 10 and observe how the drop number changes.
Expected learning: You’ll see that identical channel values for N consecutive reads (when delta_seq > 1) is the signature of drops — not sensor stiction.
Lab 2: Variable-Length Encoding Edge Case
Goal: Observe the GPS zero-omission problem and verify the payload_length fix.
Setup:
1. Create a SensorFrame with full GPS data (latitude=35.69, longitude=139.69, has_fix=true).
2. Encode it and record stream.bytes_written — call this size_with_fix.
3. Set all GPS fields to zero (has_fix=false, lat=0, lon=0, alt=0).
4. Encode it and record stream.bytes_written — call this size_without_fix.
Verification criteria:
- size_with_fix > size_without_fix (GPS fields were omitted = smaller output). The difference should be ≥10 bytes.
- Take the “with fix” encoded bytes. Put them in a 128-byte buffer (zeros for remainder). Decode using sizeof(128_byte_buffer) — observe what fields come out wrong.
- Now decode using size_with_fix only — verify all fields decode correctly.
- Repeat with the “without fix” payload.
Expected learning: The difference in encoded size confirms proto3 omits zero fields. The importance of payload_length becomes concrete — you can see exactly how many bytes are valid.
Lab 3: Stack High-Water Mark for pb_encode
Goal: Measure the actual stack impact of pb_encode with nested sub-messages.
Setup:
1. Create a thread with a 1024-byte stack that calls pb_encode() on a SensorFrame with both ImuData and WheelData populated.
2. Enable CONFIG_STACK_SENTINEL=y and CONFIG_THREAD_STACK_INFO=y.
3. After 100 encode calls, call k_thread_stack_space_get() and log the result.
Verification criteria:
- With 1024-byte stack + CONFIG_STACK_SENTINEL=y: expect to see a stack sentinel violation panic (confirms the stack is too small).
- With 2048-byte stack: k_thread_stack_space_get() should show ≥200 bytes unused.
- With 4096-byte stack: verify unused space doubles — confirms linear relationship.
- Add LOG_ERR() to the error path (even if never triggered) and measure stack increase.
Expected learning: pb_encode stack usage is real and measurable. Using k_thread_stack_space_get() is the correct way to size threads rather than guessing.
Lab 4: Round-Trip Test on Linux Before Porting
Goal: Validate the full encode→decode pipeline on a Linux host before touching the STM32.
Setup:
1. Write test/round_trip.c (see study notes section 2.7) for your actual SensorFrame schema.
2. Compile with: gcc round_trip.c sensor_frame.pb.c -Inanopb -lnanopb -o round_trip
3. Add test cases for ALL edge cases:
- Normal: all fields populated
- GPS no fix: GPS fields all zero, has_fix=false
- Minimal: only seq set, no sub-messages
- Max values: float FLT_MAX, timestamp_us = UINT64_MAX / 2
Verification criteria:
- All test cases pass assert checks without memory errors (run under valgrind: valgrind ./round_trip).
- Print hex dumps for each test case. Label them and keep them. These are your reference for debugging STM32-side output.
- For the “GPS no fix” case: confirm that encoded size is smaller than “normal” case.
- The round-trip test must pass before you write a single line of firmware packer code.
Expected learning: Bugs found on Linux in 5 minutes would cost 2 hours on STM32 (flash-debug cycle). This is the “test on host first” discipline.
Design 1: Multi-sensor ZBus architecture
You’re adding three new sensors to the robot: a LIDAR (10Hz), a barometer (50Hz), and a battery monitor (1Hz). Currently the packer thread subscribes to a single imu_chan at 100Hz.
Question: Design the ZBus channel + observer architecture for the complete multi-sensor system. Consider: - Should the packer subscribe to all channels individually, or should there be a “data aggregator” thread? - How do you handle the 100× rate mismatch between LIDAR (10Hz) and IMU (100Hz)? - The battery monitor data is critical — what happens if its ZBus notification is dropped? - What queue depths do you choose for each sensor’s subscriber?
Write a table showing: sensor → channel → subscriber/listener → thread priority → queue depth, and justify each choice.
Design 2: Detecting ZBus drops in production
Your robot runs at 100Hz for 8-hour shifts. Occasionally, operators report that the robot “acts weird” for 1–2 seconds, then recovers. Looking at the rosbag, you see 3–4 consecutive identical sensor frames.
Question: Design a monitoring system to catch this in production.
SensorFrame to make drops detectable by the Jetson?Write a 10-line design summary. There is no single correct answer — scoring is based on identifying the right questions and showing the tradeoffs.
Design 3: GPS fix-loss forward compatibility
Future firmware (v3) will add a gps_accuracy_m float field (field number 8) to GpsData. The current firmware on installed robots (v2) does not have this field. Jetson software will be updated before all robot firmwares are.
Question: 1. What happens when updated Jetson v3 software receives a frame from old firmware v2 (without field 8)? 2. What happens when v2 Jetson software receives a frame from new firmware v3 (with field 8)? 3. Is there any code change needed on either side to make this safe? 4. What test must you run in the CI pipeline to verify forward/backward compatibility? Describe the minimal test fixture.