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