A deterministic, hard real-time medical monitoring system that streams live ECG + EMG biosignals, triggers priority-scheduled alarms within 100 ms, and logs clinical data β all running on a microkernel RTOS.
================================================
VITAL SIGN MONITOR (QNX 8.0)
================================================
ECG |βββββββββββββββββββββββββββββββββββββββ| 487 BPM
EMG |βββββββββββββββββββββββββββββββββββββββ| 342 mV
------------------------------------------------
STATUS: β
SYSTEM NOMINAL
------------------------------------------------
This isn't a hobbyist Arduino sketch β it's a full RTOS architecture project. The Raspberry Pi runs QNX Neutrino, a hard real-time microkernel OS used in medical devices, cars (BlackBerry QNX), and aerospace. The Arduino is just the ADC front-end.
Key RTOS concepts demonstrated:
- Preemptive priority scheduling (SCHED_FIFO) with 4 threads at distinct priorities
- Microkernel fault isolation β a USB driver crash cannot affect the alarm engine
- procmgr_ability() β non-root process acquiring real-time scheduling rights
- Deterministic alarm response β worst-case latency bounded to 100 ms by the scheduler
- Priority inheritance via QNX IPC to prevent priority inversion
.
βββ arduino/
β βββ vital_signs.ino # EMG + ECG acquisition firmware (500 Hz)
βββ qnx/
β βββ medical_monitor.cpp # Multi-threaded QNX C++ application
βββ scripts/
β βββ setup_arduino.sh # QNX target: USB driver + serial setup
βββ README.md
| Component | Model | Role |
|---|---|---|
| SBC | Raspberry Pi 4B (8 GB) | Runs QNX Neutrino 8.0 |
| MCU | Arduino Uno R4 WiFi | 14-bit ADC; EMG filter + ECG sampling |
| EMG sensor | Muscle BioAmp Shield | Surface electrode + instrumentation amp |
| ECG sensor | BioAmp EXG Pill | 3-lead wet electrode (RA, LA, RL) |
| Link | USB Serial (CDC-ACM) | 115,200 baud β /dev/serusb1 on QNX |
Why Arduino R4? The classic Uno has a 10-bit ADC. The R4 gives 14-bit (16,384 levels), significantly reducing quantisation noise in the EMG signal.
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β QNX Neutrino RTOS 8.0 β
β ββββββββββββββββ ββββββββββββββββ β
β β devc-serusb β β med_monitor β β
β β (USB driver β β process β β
β β resource β β β β
β β manager) β β Pri 20 ββββΊ Alarm thread β
β β /dev/serusb1β β Pri 15 ββββΊ Data thread β
β ββββββββ¬ββββββββ β Pri 10 ββββΊ UI thread β
β β read() β Pri 5 ββββΊ Log thread β
β ββββββββΌβββββββββββββββββββββββββββββββββββ β
β β QNX Microkernel β β
β β Scheduling Β· IPC Β· Memory Β· Timers β β
β βββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββ¬ββββββββββββββββββββββββββββββ
β USB CDC-ACM 115200 baud
ββββββββββββΌβββββββββββββββ
β Arduino Uno R4 WiFi β
β EMG: A0 β filter β β
β envelope β CSV β
β ECG: A2 β raw β CSV β
β Output: "342,487\n" β
βββββββββββββββββββββββββββ
| Thread | Priority | Policy | Responsibility |
|---|---|---|---|
| Alarm engine | 20 | SCHED_FIFO |
ECG spike detection β alarm in β€100 ms |
| Data acquisition | 15 | SCHED_FIFO |
Drain USB buffer; parse CSV into globals |
| UI / HMI | 10 | SCHED_RR |
Terminal dashboard at 20 fps |
| Logger | 5 | SCHED_RR |
Background CSV β /tmp/patient_summary.csv |
SCHED_FIFO = no time-slicing; thread runs until it blocks or is preempted by higher priority. Used for the two critical threads so timer jitter never delays them.
// Accumulator pattern: carries forward overshoot deficit
// maintains exact 500 Hz long-term even with loop jitter
timer += 1000000 / SAMPLE_RATE; // reload 2000 Β΅sanalogRead(A0)
βββΊ EMGFilter() 4-stage IIR biquad bandpass (20β450 Hz)
βββΊ abs() full-wave rectification
βββΊ getEnvelop() 128-sample running average
βββΊ Serial.print() CSV output
| Stage | Type | Purpose |
|---|---|---|
| 1 | Lowpass | Attenuate noise above ~450 Hz |
| 2 | Highpass | Remove DC + motion artifact < 20 Hz |
| 3 | Highpass | Steepen roll-off skirt |
| 4 | Bandstop | Notch 50/60 Hz power line interference |
Each stage is Direct Form II Transposed β most numerically stable IIR form for single-precision float. State vars are static, preserving filter memory between loop() calls.
EMG_ENVELOPE,ECG_RAW\n
342,487 β nominal
0,512 β no muscle activity
891,803 β ECG > 800 β QNX alarm fires
# Cross-compile for AArch64 QNX target
qcc -Vgcc_ntoaarch64le_cxx -o med_monitor medical_monitor.cpp# Copy binary
scp -c aes128-ctr -o MACs=hmac-sha2-256 med_monitor qnxuser@<TARGET_IP>:/tmp/
# SSH to target
ssh -c aes128-ctr -o MACs=hmac-sha2-256 qnxuser@<TARGET_IP># 1. Run Arduino setup script first
chmod +x setup_arduino.sh && sudo ./setup_arduino.sh
# 2. Launch monitor
sudo ./med_monitor| Key | Action |
|---|---|
A |
Acknowledge alarm β silences buzzer, keeps visual warning until ECG normalises |
Ctrl+C |
Exit monitor (restores terminal settings) |
# Find Arduino USB IDs
usb -vv
# Kill any existing driver
sudo slay devc-serusb devc-sercdc
# Start USB serial resource manager
sudo /usr/sbin/devc-serusb -v -d vid=0x2341,did=0x1002 &
# Verify device node
ls -l /dev/ser* # expect /dev/serusb1
# Configure baud rate
stty baud=115200 < /dev/serusb1
# Run
sudo ./med_monitorMicrokernel architecture
QNX's kernel is < 80 KB and handles only scheduling, IPC, timers, and memory. Every driver (including devc-serusb) runs as a user-space resource manager process. If the USB driver crashes, the kernel detects it β the alarm and UI threads keep running unaffected.
procmgr_ability() β capability-based privileges
procmgr_ability(0,
PROCMGR_ADN_ROOT | PROCMGR_AOP_ALLOW | PROCMGR_AID_PRIORITY,
PROCMGR_AID_EOL);Grants this process the ability to set SCHED_FIFO priorities above the default ceiling β without running as root. Principle of least privilege.
Priority inversion prevention
QNX's microkernel IPC automatically applies priority inheritance: when a high-priority thread blocks on MsgSend() to a lower-priority server, the server inherits the caller's priority. This is guaranteed by the kernel β the fix that would have prevented the 1997 Mars Pathfinder reset.
volatile shared globals
volatile int g_ecg = 0;
volatile int g_emg = 0;volatile prevents the compiler from caching these in registers across loop iterations β without it, an optimising compiler might never see cross-thread updates. Note: volatile is not atomic; production systems should use pthread_mutex_t or QNX's atomic_* functions.
Non-canonical terminal input
newt.c_lflag &= ~(ICANON | ECHO);
tcsetattr(STDIN_FILENO, TCSANOW, &newt);Switches terminal to raw mode so 'A' is detected without pressing Enter. check_ack() uses select() with zero timeout β non-blocking poll that doesn't stall the 20 fps render loop.
ECG > 800
NOMINAL ββββββββββββββββββΊ CRITICAL (red, beeping)
β² β
β ECG β€ 800 β press 'A'
β (auto-reset) βΌ
βββββββββββββββββ ACKNOWLEDGED (yellow, silent)
| File | Content |
|---|---|
/tmp/patient_summary.csv |
timestamp, EMG, ECG, status β logged every 5 s by background thread |
Host (development)
- QNX SDP 8.0 (includes
qcccross-compiler) - VS Code + QNX SDP Extension (optional, for profiling)
- Windows 11 or Linux
Target
- Raspberry Pi 4B with QNX Neutrino 8.0 Quick Start Image
- Network access (for SCP/SSH deployment)
Hardware
- Arduino Uno R4 WiFi
- Muscle BioAmp Shield (EMG)
- BioAmp EXG Pill (ECG)
- Electrodes + leads
MIT β see LICENSE