After 40ms 8 samples are available and sent.
The framesize is slightly different to your calculation:
count > 1 Byte, t0 > 4 Bytes, 8*(sample > 2 Bytes + dt > 1 Byte) + crc16 > 2 Bytes = 31 Bytes plus 2-3 bytes for the MsgPack protocol.
If more than 1000 ms have elapsed since the last readout, the oldest data in the ring buffer are overwritten by new data. After 1030 ms (actually 206 samples), the entire ring buffer (=200 samples) would then be transmitted. However, a 0x21 = "!" is also sent, indicating that there has been a loss of data (in this case the oldest 6).
The goal is to keep latency low while still avoiding unnecessary CPU load. If the loop were to sleep for 5000 µs (5 ms), it would indeed guarantee that the next timer tick has occurred. However, in the worst case it would introduce up to 5 ms of additional latency before handling RPC / Bridge requests. This added latency should be avoided.
Sleeping for 500 µs provides a good compromise: It prevents busy-waiting and significantly reduces CPU usage. At the same time, it allows the loop to wake up frequently enough to respond quickly to RPC requests and to process timer ticks with low latency. Between two samples (5 ms apart at 200 Hz), the comparison timer_ticks == 0 is executed at most 10 times, which is negligible overhead. If the loop did not sleep at all, the condition would be evaluated thousands of times per sample interval.
This block prevents unsent data from being silently overwritten without adjusting the buffer pointers. head points to the next write position. last_sent points to the oldest sample that has not yet been sent (the read pointer).
The code computes next = head + 1. If next == last_sent, the ring buffer is full. Writing a new sample would overwrite data that has not yet been sent. In this case: last_sent is advanced by one position, which drops the oldest unsent sample. overflowed is set to true to signal data loss to the host. >> send 0x21
In short: When the buffer is full, new samples are kept and the oldest unsent samples are discarded. This ensures that the buffer always contains the most recent data, while any data loss is explicitly flagged.
(1)
count specifies the number of samples contained in the transmitted frame (e.g. 8 samples after 40 ms, 30 samples after 150 ms). This allows the receiver to correctly parse the variable-length payload.
(2) and (3)
Each sample is stored internally on the MCU in the ring buffer as: value (2 bytes, ADC-value) and t_ms (4 bytes, absolute timestamp in milliseconds)
To reduce the size of the transmitted frame, only one full 32-bit timestamp is sent per frame: t0 is the absolute timestamp (in ms) of the first sample in the frame. For the remaining samples, only the time difference to the previous sample is transmitted
dt is an 8-bit delta timestamp in milliseconds.
dt[0] = 0 for the first sample
Subsequent dt values represent the elapsed time since the previous sample and are saturated at 255 ms. This encoding significantly reduces bandwidth while still preserving timing information. The 8-bit limit (255 ms) is acceptable here because samples are normally spaced at 5 ms (200 Hz), so dt values are typically very small.
(4)
The 0x21 ("!") flag is not yet evaluated by the Python script, but it is intentionally included for future use.
(5)
All graphical illustrations were created using Microsoft PowerPoint.
I really appreciate your effort in reviewing my code — that helps me a lot as well. Thanks!
I want to design compact C++ and data structure exercises for my students and myself, using your Arduino sketch as a basis, because it reflects real engineering practice of Zephyr RTOS.
2.
Since the time interval between successive samples is constant (5 ms), is it necessary for the MCU to transmit this information to the MPU? Can the MPU instead apply the 5-ms interval locally during data processing if required?
3.
If I have understood, then -- (1) For these eight ADC0 samples: 0x01FE(ADC00), 0x0154, 0x0178, 0x0167, 0x01CD, x0155, x0150, 0x01FC(ADC07), the MsgPack frame is:
[1-byte_count][4-byte_timeStamp ADC00H ADC00L][1-byte_dt ADC01H ADC01l], ..., [1-byte_dt ADC07H ADC07L][crc16H crc16L] = 30 bytes. Is it correct?
(2) I have observed in your sketch that you have broken the 32-bit/16-bit data into bytes maually. Does not this library: MsgPack.h support this splitting?
(3) MsgPack frame is formatted into natural binary for transmission over the LUART1 port of the router bridge. Is there any need of sending pre-amble bytes (multi-byte synchronization pattern) before sending the payload?
4.
(1) You have done all the work to make the project functional and have shared it with readers to receive constructive feedback for further improvements. Currently, I am studying your work to enhance mine and my students' knowledge base.
(2) Will you please tell me how many Zephyr threads are there in your Arduino sketch and also their names? This is an academic question as I am currently studying the role of Zephyr RTOS in your sketch.
In fact it is 31 Byte = 1 Byte count + 4 Byte t0 + 8* (2 Byte ADC-value + 1 Byte dt) + 2Byte crc16
(2) MsgPack is not used here to serialize the ECG data structure itself (e.g. as a MsgPack array or map of integers). Instead, MsgPack is only used as a transport container (MsgPack::bin_t<uint8_t>) to carry a raw binary blob. The actual ECG frame format (count, timestamp, samples, delta-times, CRC) is a custom, application-defined binary protocol that is intentionally serialized manually to minimize payload size. If the data were serialized directly as MsgPack integers or arrays, MsgPack would automatically handle byte encoding, but the payload would be significantly larger.
(3) A preamble or synchronization pattern is typically required when transmitting raw UART byte streams, where the receiver must detect frame boundaries. In this setup, however, the ECG frame is not received as a raw UART stream. It is returned as the result of an RPC call (Bridge.call("ecg_get_frame")) via the Arduino RouterBridge.
The RPC/MsgPack layer already provides: message framing, and request/response synchronization. Therefore, the receiver always knows where a message starts and ends, and an additional preamble is not necessary. The CRC included in the custom frame serves data integrity checking, not frame synchronization.
(4) In the sketch itself, no Zephyr threads are explicitly created by the application code. The sketch runs on top of ArduinoCore-zephyr, which internally creates and manages the required Zephyr threads. I assume the following threads:
1 thread for Arduino setup() / loop()
1 thread for Zephyr timer (k_timer) used for periodic sampling
1 thread for RouterBridge / RPC
But I am not really familiar with Zephyr. I let ArduinoCore-zephyr do the magic.
after a while I found the time to rework the code of my project. Since large parts of the code were done automatically by means of AI, readability and structure were limited. Now I put more human thoughts into it and was able to reduce the complexity and number of code lines. One major aprovement was using p5.js for the webbrowser javascript part. It took away a lot of boilerplate code. Probably I will write more about p5.js in my Uno Q Graphics Stack thread. I also discarded crc16 tests between MCU and MPU.
I have had several ECG, echo-cardio-gram. I have also had EEG, electroencephalographs. You are describing an EEG, not an ECG. You have also built an EEG, not an ECG which uses sound waves to get an echo and display a moving picture of the heart and/or other blood vessels.
These are the ECG probes (Fg-1) that I had received as gift from abroad along with a ECG shield to test @jens-bongartz ECG Project presented in this thread using UNO Q.
Yep you are correct from what I remember my Cardiologist testing me. Echo or or Echo Cardiogram is done with ultrasound while and ECG or EKG (same thing) is done via measuring electrical impulses.
It is worth adding that from a circuit design perspective, ECG and EEG are actually closely related. In principle, both are sensitive differential voltage amplifiers designed to measure biopotentials. However, there are a few key differences:
The EEG signal is about an order of magnitude smaller than an ECG signal, mainly because it is measured through the skull. Furthermore the relevant frequencies that need to be filtered out for EEG are lower and typically significantly more measurement channels / electrode placement points are required.
Maybe we can adapt the DIY-ECG circuit for a simple EEG measurement as well, like a single-channel alpha-wave detector (8–12 Hz). E.g. alpha waves rhythm of the brain can changed simply by closing one's eyes while awake. Therefore the electrodes are placed at the back of the head and on one earlobe. The groiund electrode is placed on the opposite earlobe.
I will try it out, when I'll find some time.
I was wondering how I might use the Q and this could be ideal.
It's a really worthwhile project and a good ECG is vital to diagnosing a myriad of heart electrical issues.
I have a personal interest with Complete Heart Block (CHB) or Sick Sinus Syndrome (SSS).
ECGs are a regular event. I'm now pacemaker dependent.
There are a lot of commercial devices for ECG aimed at home users, but none really cater for abnormal rhythm issues and if they use interpretive software as well, can be way off the mark. A visit to the GP and an ECG with software had me as heart attack in progress.
And for the record, UK ECG is Electrocardiogram. Often used hand in hand with Echocardiogram (EC). "Echo" is used for all sorts of studies, an important one being Ejection Fraction (EF) which looks at the pumping efficiency of the heart. Doppler ultrasound looks at flow, especially in peripheral arteries and veins.
Thanks for sharing your experience and background! I'm glad you find the project interesting.
I would like to point out that the DIY-ECG is far from a medical device and was developed purely for educational purposes. Unfortunately, in its current state, it is not capable of performing any clinical analysis aside from heart rate (BPM). The electronics are simply too rudimentary. Even better software cannot circumvent this situation.