Okay, let’s start now with a description of the code. I will begin with the MCU code sketch.ino and its connection to the Python code on the Linux side.
The following graphic shows the overall architecture.
General idea
The main goal of the sketch is to sample the ADC0 input every 5 ms (sampling frequency 200 Hz) and store the samples in a ring buffer (RingBuf[]). The ring buffer has 200 entries, so it can store up to 200 samples, which corresponds to about 1 second of signal data.
Sampling and timing
To generate the 5 ms sampling interval, a Zephyr OS k_timer is used, called SampleTimer. Every 5 ms, the timer callback onSampleTimer() is executed and increments the variable timer_ticks.
The actual ADC read is not done inside the timer callback. Instead, timer_ticks is checked in the Arduino loop() function:
- If timer_ticks is zero, the loop sleeps for 500 µs.
- If timer_ticks is greater than zero (normally one), the function readADC() is called once for each pending tick.
The function readADC() reads ADC0 (analogRead(A0)) and stores the sampled value together with a timestamp (in milliseconds) in the next position of the ring buffer. When the end of the ring buffer is reached, it wraps around to the beginning. This results in a continuously running sampling loop (shown in the lower part of the orange MCU box in the diagram).
Ring buffer and RPC access
The content of RingBuf[] can be accessed via the MsgPack RPC function ecg_get_frame(). This function can be called from the Linux side (MPU) via the RPC bridge using:
Bridge.call("ecg_get_frame")
The function remembers the last read position in the ring buffer and returns only the new samples that have not been sent yet.
On the Python side, Bridge.call() is typically executed every 40 ms. At a sampling rate of 200 Hz, this usually results in about 8 samples per frame. The polling interval can be changed if needed.
Data frame format
ecg_get_frame() builds a compact binary data frame with the following structure:
Number of samples (uint8 count)
Timestamp of the first sample (uint32 t0_ms)
For each sample:
ADC value (uint16)
Time delta to the previous sample (uint8 dt_ms)
CRC16 checksum at the end
Using delta timestamps (dt_ms) significantly reduces the frame size. This is important because the RPC bridge is implemented over a serial connection running only at 115.200 baud.
Python side
On the Linux side, Bridge.call("ecg_get_frame") is executed inside the script arduino_q_bridge_rpc.py, wrapped by the function request_frame(). This function is called periodically from main.py.
The script also contains a parse_frame() function that unpacks and validates the received data frame. More details about the Python side will follow in a later post.
In short:
The MCU samples ADC0 every 5 ms using a Zephyr timer.
Samples are stored continuously in a ring buffer.
The Python side requests new samples on demand via RPC.
Only unsent samples are transmitted, packed efficiently into a small binary frame.
I hope this makes the functionality clear.