KOEN中HI

Software Architecture · Test Bench

2026-10-11 design record · processor NXP i.MX 95 · ECG AFE159RP8 · SpO2 AFE4490 · component test bench ESP32-S3 + ADS1293

Legend: Confirmed item confirmed as specification · Default value set by the design document (not confirmed) · Unverified value not yet verified against original sources or measurement. Values derived by arithmetic are marked "calculated".

Contents

  1. Overall picture
  2. Core allocation and the M33
  3. Sample rates and signal path
  4. Inter-core communication (shared ring · RPMsg)
  5. Latency budget
  6. Holter storage · trends
  7. Alarm engine
  8. State machine
  9. Power-on self-test
  10. Cloud · security · updates
  11. Software safety-class separation · modules
  12. Development order · measurement list
  13. Test-bench firmware (ESP32-S3 + ADS1293)
  14. Test-bench verification results

1. Overall picture

The processor is the NXP i.MX 95. The measurement section that touches the patient (patient island) is isolated and connects to the processor through digital isolators (ISO7762). The processor chip set (i.MX 95 · AFE159RP8 · AFE4490 · stage-2 RM44L920) is Confirmed specification; the core allocation below is a Default.

 Patient island (isolated, CF)         │ Non-patient side (i.MX 95 SoM)
 ──────────────────────────────────────┼───────────────────────────────────────
 AFE159RP8 (ECG 8ch, 10 electrodes, pace on-chip) ─ISO7762#1─ LPSPI-A ┐
 AFE4490   (SpO2, PRF 500 Hz)            ─ISO7762#2─ LPSPI-B ├─► Cortex-M7 800 MHz (FreeRTOS)
 V1/V5 switch · sim/signal switch state  ─(isolated GPIO)────┘   acquisition·filters·QRS·pace·SpO2
 (+option) NIBP module UART · Resp (AFE built-in)                   ·alarm decision·alarm tone
                                                    │ shared DDR ring (waveforms) + RPMsg (commands·events)
                                                    ▼
                         Cortex-A55 ×6 (Linux, Yocto)  display (Qt)·Holter·trends·cloud·OTA·logs·permissions
                         Cortex-M33 (NXP SM)           power·clocks·reset·PMIC·fault reaction (per-LM reset)
 External: TPS3430 window watchdog (kicked by M7) → WDO → backup buzzer + A55 GPIO   [stage 2] RM44L920 safety MCU (SPI)
 LTC1760 + smart packs A/B (SMBus, owned by M7) · VSYS undervoltage comparator → M7 IRQ

Three core-allocation principles Default

  1. The patient-safety path (acquisition → decision → alarm tone) is closed within the M7 alone. Even if Linux dies, the sound still plays.
  2. Linux only does "display, store, send." If Linux stalls, the M7 notices and sounds a high-priority "display failure."
  3. If the M7 stalls, the System Manager (SM) resets only the M7; meanwhile the TPS3430 sounds the backup buzzer and the A55 turns the waveforms gray.

2. Core allocation and the M33

Changed in design review — monitoring and alarm backup cannot be placed on the M33. On the i.MX 95, the M33 is a core dedicated to the NXP System Manager. Original text of the NXP imx-sm documentation (sm/doc/arch.md):

  • "SM owns the M33 core and does not support customers adding workloads to this core."
  • "the Cortex-M33 is not available to run customer applications. … additional customer functionality may interfere with the real-time response of the SM."

So the Default was set as follows. The M33 is left running NXP SM as-is (no code changes, configuration files only), and the SM's fault reactions (FCCU · watchdog → reset only the affected LM) are used as "primary hardware supervision." The SM configuration already contains the reactions M7_LOCKUP → M7(LM1) reset and WDOG3/WDOG4 → AP(LM2) reset. Monitoring and alarm backup: stage 1: M7 ↔ A55 mutual supervision + external window watchdog TPS3430 + backup buzzer; stage 2: handed over to the RM44L920.

Responsibilities by core Default

CoreOSResponsibilities
M7 (800 MHz, per SoM vendor)FreeRTOS + MCUXpresso SDK + RPMsg-Lite (alternative: Zephyr)AFE159RP8 · AFE4490 acquisition, common time base, pace detection, decimation, dual adaptive notch, HPF/LPF, QRS · HR, SpO2 · PR · PI, (option) respiration · NIBP, alarm condition decision · prioritization · alarm tone synthesis, lead-off · sensor-off, power monitoring (LTC1760 · packs · VSYS), kicking the external watchdog, A55 supervision
M33NXP SM (bare metal, no in-house code)Power · clocks · reset · PMIC, TRDC resource isolation, FCCU fault reaction, reset-reason logging
A55 ×6Linux (NXP vendor kernel)Qt 6 display, Holter recording, trend DB, alarm display · operation · logging, settings · permissions, cloud sync, OTA · USB updates, service mode, security
RM44L920 (stage 2)TI HALCoGen + in-house (TBD)Dual cross-check of alarm decisions, backup alarm tone, external watchdog management, fail-safe decisions

Resource ownership (fixed by SM configuration)

ResourceOwnerNotes
LPSPI ×2 (dedicated ECG · PPG buses)M7AFE4490 cannot tri-state when not selected, so bus sharing is prohibited
DRDY · ADC_RDY interrupt GPIOs, switch-state GPIOsM7
One general-purpose timestamp timerM7Common ECG · PPG time base
SAI or PWM + alarm-tone amplifierM7Sound must play independently of Linux
I²C (SMBus) → LTC1760 · packs A/BM7Because power alarms are decided by the M7
(option) NIBP UARTM7The M7 must know to flag ECG noise while the pump is running
WDOG5M7 (SM fault reaction)WDOG5 → LM_RESET lm 1
PMIC I²C, clocks, resetSM
Display (LVDS) · GPU · eMMC · USB · Wi-Fi/BT · GbE ×2 · backup buzzer GPIOA55
ELE (EdgeLock Secure Enclave)Boot chain · A55

Resource budget (calculated · default)

ItemValueNotes
M7 ITCM / DTCM256 KB / 256 KBPer Zephyr dts. NXP reference manual original Unverified
M7 code placementITCM (DSP · ISR), large tables in DDRDefault
M7 load estimate8 kHz interrupt (≤ 5 µs each → 4 %) + decimation FIR about 0.75 M MAC/s + notch · QRS (500 SPS) → under 10 %Calculated. To be measured with the DWT cycle counter
Shared DDR reserved region32 MiB (rings about 23.2 MiB + margin)Calculated

3. Sample rates and signal path

Why 8 kSPS

Default All ECG channels are acquired at 8 kSPS 24-bit. Pace uses on-chip detection (primary) + M7 software detection (secondary confirmation). If the detailed datasheet confirms that on-chip detection fully meets the requirements, lowering to 2 kSPS will be considered; until then, fixed at 8 kSPS.

Path inside the M7

AFE159RP8 8 kSPS × 8ch × 24bit ── DRDY ISR → LPSPI DMA ─┐
                                                         ├─ ① timestamping (general timer 32bit, sample number u64)
AFE4490 500 SPS × 6 streams ── ADC_RDY ISR → LPSPI ──────┘
   ├─ ② pace: chip flag + 8 kSPS slope · width check → event {sample number, lead, width}
   ├─ ③ pace blanking: fill the pulse interval by linear interpolation at 8 kSPS
   ├─ ④ decimation 8000 → 2000 → 500 SPS (2-stage linear-phase FIR, ÷4·÷4)
   │      group delay estimate: 31 taps (8k) 1.9 ms + 63 taps (2k) 15.5 ms ≈ 17.4 ms (calculated)
   ├─ ⑤ "raw data" branch → Holter storage (500 SPS 24bit, pre-notch · pre-HPF) + calib row
   ├─ ⑥ HPF → dual adaptive notch (60·120 Hz) → LPF   (keep both pre- and post-notch — for comparison view)
   ├─ ⑦ QRS detection (separate 5–15 Hz band, independent of notch state)
   │      pace events excluded from HR · marked only when "Paced=Yes"
   └─ ⑧ PPG: 500 SPS → 5-sample average 100 SPS → band-pass filter → R · SpO2 · PR · PI. 1 s period

Reason for going down to 500 SPS: the previously designed dual-notch coefficients are based on fs = 500 Hz and can be reused as-is. Blanking length and FIR tap counts will be set by measurement in the signal-processing design.

Channel assignment Default

AFE channelBasic 5 electrodes12-lead option
CH1I (LA−RA)I
CH2II (LL−RA)II
CH3III (LL−LA) direct ConfirmedV1
CH4V (V1/V5 switch)V2
CH5~8Self-diagnostic rotation (internal test signal → calib row, supply · temperature)V3~V6
The 12-lead option and "direct III measurement" do not fit into 8 channels at the same time (I · II · III · V1–V6 = 9 channels). Whether to calculate III only in 12-lead mode, or to use two AFEs, is undecided. The software takes the channel map from a configuration table, so either way works without code changes. Whether the AFE159RP8 has the CH5–8 self-diagnostic rotation function is Unverified.

Downsampling for display

4. Inter-core communication — shared memory ring + RPMsg

PathWhatReason
Shared DDR ring (SPSC, M7 writes · A55 reads)Waveforms (raw · processed · PPG) · beat/pace eventsHigh volume (tens of kB per second). Zero copy. The A55 polls the write index every vsync → latency ≤ 1 frame
RPMsgCommands · settings · alarm state · numerics · heartbeatMessage-based, ordering guaranteed. Linux default buffer 512 B

RPMsg common header (24 B, little-endian) Default

FieldTypeDescription
magicu160xEC61
veru8Protocol version (checked in HELLO; device failure if different)
typeu8Message type
lenu16Body length (≤ 472 B = 512 − RPMsg header 16 − 24)
flagsu16bit0 response requested, bit1 retransmission
sequ32Sender sequence number. Detects gaps · reordering
t_idxu64Global sample number at 500 SPS (common time base)
crcu32CRC-32C (over header + body with the crc field set to 0)

Two endpoints: ekg-ctrl (A55 → M7 commands, M7 responses) · ekg-evt (M7 → A55 events · status).

Main messages Default

typeNameDirection · periodKey points
0x01/0x02HELLO / HELLO_ACKAt startupM7 firmware version · protocol version · ring layout table ↔ release number. If different, high-priority "software mismatch"
0x10HB_M7M7→A55 10 HzUptime, device state, ring write index, CPU load, sample loss count, notch state
0x11HB_UIA55→M7 10 HzCounter of frames actually flipped, list of alarm IDs currently shown on screen
0x20~0x22CFG_SET / ACK / SNAPSHOTRequest · response · 1 minSend safety settings and read back the applied value to confirm. Rejected if out of the allowed range
0x30~0x32ALARM_STATE / CMD / CMD_ACKImmediately on change + 1 HzDecisions are made by the M7. The A55 only does pause (default 2 min) · acknowledge · latch release
0x40NUMERICS1 HzHR (source ECG/PPG/SIM), PR, SpO2 ("reference value · not calibrated" flag), PI, valid/invalid per parameter
0x50~0x52DEVICE_STATE · POWER_STATUS · SELFTESTOn change · 1 Hz · POSTDevice state, pack A/B remaining charge · temperature · SOH, self-test results
0x60LEAD_STATUSOn changeLead-off per electrode, V1/V5 switch position, sim/signal position, SpO2 sensor status
0x70 · 0x80/0x81FAULT · SHUTDOWN_REQ/READYOn occurrenceFault code · confirmation that Holter file is closed before shutdown
0x90SERVICE_CMDService mode onlyRejected in measurement mode
0xA0TIME_SYNC1 minUTC ↔ t_idx mapping (for records. Alarm decisions use only t_idx)

Shared memory rings (calculated · default)

All rings have power-of-two lengths; indices are the global sample number u64 masked. The M7 writes all the data, then a memory barrier → write-index update. CRC-32C per block (20 ms = 10 samples).

RingFormatLengthSizeUse
R_RAWint32 × 8ch, 500 SPS (pre-notch · pre-HPF)2¹⁹ = 17.5 min16 MiBHolter · calib. Holds the recording while Linux restarts
R_PRE / R_POSTfloat32 × 8ch × 22¹⁶ = 131 s4 MiBDisplay, notch comparison view, freeze
R_PPG{red, ir int32, pleth float}, 100 SPS2¹⁷ = 21.8 min2.5 MiBRows 5 · 6, Holter
R_EVT32 B events2¹⁴512 KiBHeart icon sync, annotations
Control blockLayout table · write indices · overwrite counters—4 KiB
Totalabout 23.2 MiB → 32 MiB reserved

The reserved region is non-cacheable. Whether this DDR region survives intact when only the A55 is reset is Unverified — measurement item S-01.

5. Latency budget

Electrode → screen (calculated · default)

StageLatency
AFE159RP8 internal digital filterUnverified
2-stage decimation FIR≈ 17.4 ms
M7 block processing (20 ms blocks)≤ 20 ms
Shared ring → A55 polling≤ 16.7 ms
Fixed display delay D (absorbs QRS detection delay, heart icon sync)300 ms initial value
Render + page flip (double buffer, 2 frames)≤ 33 ms
Totalabout 390 ms + AFE latency

Alarms Default

PathBudget
Heart rate high/low limits, asystole (condition delay + signal generation delay)Total ≤ 10 s (condition decision ≤ 8 s + sound start ≤ 1 s + margin) — voluntarily adopting the IEC 60601-2-27 criterion, which does not apply to home use
Lead-off · saturation indication≤ 1 s (standard ≤ 10 s)
SpO2 update stalledLow priority if > 30 s
Battery depletionHigh priority at 20 min remaining (standard: "at least 5 min before")
M7 decision → A55 screen display≤ 50 ms
M7 decision → cloud · caregiver notificationBest effort (no guarantee) — not counted as part of the alarm system

6. Holter storage · trends

Principles

1-second blocks Default

Fixed-size slots: 4 channels 8 KiB, 8 channels 16 KiB (eMMC 4 KiB aligned). Segment files of 1 hour = 3600 slots are preallocated so that file metadata does not change during writing.

PositionFieldsSize
Headermagic 'EKGB', format version, session UUID, block sequence number, first sample number, UTC + monotonic time, sampling (500), channel count · map · scale, copy of filter settings, notch state, flags (SIM · lead-off · Paced · charging)128 B
ECG500 × N channels × 3 B (24-bit packed)6,000 B (4ch) / 12,000 B (8ch)
PPG100 × 2 × 3 B (red · IR)600 B
TrendHR · PR · SpO2 · PI · (RR · NIBP) for this second + validity flags32 B
Annotation TLVBeats (type "for reference"), pace instants (8 kSPS resolution), lead changes, V1/V5 switching, alarm start · end, patient button, time correctionVariable
TrailerCRC-32C + hash chain containing the previous block's SHA-25636 B

Trend DB Default

7. Alarm engine (IEC 60601-1-8 priorities)

Priorities · signals Default

ItemHighMediumLow
Color#FF0000 fillYellow fill + black textCyan fill + black text
BlinkingKeep current 1 Hz (until the standard's original text is checked)SameNone
Sound10-pulse burst3 pulses1–2 pulses

Alarm list (excerpt) Default

TypeAlarmPriority
PhysiologicalHR high/low limits (default 50–120, adjustment step ≤ 5 bpm)Medium
PhysiologicalAsystoleHigh
PhysiologicalSpO2 low limit (default 90 %, default below 85 % prohibited) — shown together with "reference value · not calibrated"Medium
PhysiologicalPR high/low limits (default 50–120)Medium
TechnicalLead-off (electrode name shown) · amplifier saturation · SpO2 sensor offMedium
TechnicalAC lost (entering outage mode) · SOC 10 % · Holter recording failureMedium
TechnicalRemaining time ≤ 20 min · device failure · display failure (A55 stalled)High
TechnicalSpO2 update stalled > 30 s · storage 90 % · cloud connection lostLow
InformationSIMULATION (full screen) · measurement unavailable while charging · AC restored—

Arrhythmia classification results are not used for alarms. They are for records and reference only.

8. State machine

Three axes overlap: device state (S) × per-parameter signal validity × power state (P).

Device states Default

StateEntry conditionScreenSound
BOOT/POSTPower on, M7 startWhite splash + progressOne short self-sounding at end of POST
S0 NormalPOST passed6-row waveforms · numericsOn physiological alarms
S1 Signal invalid (per parameter)Lead-off · sensor-off · saturation · NaN · self-diagnostic deviation · ring data gap of 0.5 sThat row dashed/gray, numeric "---" (previous value not retained)Technical alarm medium/low
S2 Device failurePOST failure, watchdog reset history, AFE communication lost, version mismatch, heartbeat lost"Device failure — measurement unavailable" full screenHigh (M7 speaker or backup buzzer)
S3 Safe stopPatient island power fault, board overheating, remaining time ≤ 5 min"Measurement stopped" + reasonHigh
SERVICEPatient not connected + service authorizationService screen—

Two principles: never show a frozen normal waveform · the alarm tone survives even if the main processor dies.

Power states Default

StateConditionBehavior
P0 ACAC presentCharging. Measurement while charging follows the 2-stage release rule — during stage 1, "measurement unavailable while charging" is shown + hardware interlock
P1 Outage modeAC lostOne medium-priority alarm, backlight 30 %, DVFS, longer upload interval. Measurement · recording · alarms unchanged
P2 LowRemaining ≤ 20 minHigh priority, Holter 1 s period maintained, uploads stopped
P3 Shutdown sequenceRemaining ≤ 5 min or cell thresholdM7 → SHUTDOWN_REQ → A55 closes Holter · fsync · log → SHUTDOWN_READY → island power off → backlight · panel · SoM in order
P4 Sudden outageVSYS comparatorWriting stopped. Loss ≤ 1–2 s

9. Power-on self-test (POST) · periodic tests

OrderItemHandled byOn failure
1AHAB signature verification chain (ROM → container)ELE / ROMDoes not boot
2Read SM reset reason — whether there was a previous watchdog · faultA55Log
3M7 itself: TCM code CRC, RAM pattern, clock cross-check (general timer vs AFE DRDY period)M7S2
4AFE159RP8: chip ID, internal test signal, shorted-input noise, supply · temperature, lead-off comparators (function list Unverified)M7S2
5AFE4490: SPI write/read-back check, ADC_RDY = PRF, LED · PD diagnosticsM7S2
6Battery pack A/B state · SOH, LTC1760 stateM7Per alarm level
7One short alarm toneM7S2
8DDR inline ECC, eMMC life registers, data partition mount · Holter recoveryA55Per alarm level
9Settings CRC, M7 ↔ A55 release number checkA55 · M7S2
10Backup buzzer path (service mode only)——

Periodic tests (during measurement): the calib row doubles as continuous self-diagnosis using the internal test signal; if amplitude is outside ±2 %, "AFE gain abnormal." Supply · temperature are cycled through spare channels every few seconds.

10. Cloud · security · updates

Cloud — store offline, then sync Default

Boot chain (AHAB)

Boot ROM ─(ELE verify)─► boot container [SM(M33) · U-Boot SPL · ATF · OP-TEE · U-Boot]
         U-Boot ─(AHAB auth)─► M7 firmware container (in A/B slot) → M7 start → acquisition start
         U-Boot ─(signed FIT)─► kernel + DTB + initramfs (A/B slot) → rootfs dm-verity

Placing the M7 firmware in the A/B slots means it is updated and rolled back together with Linux as one bundle. The cost is that M7 start is delayed until the U-Boot stage (a few seconds, to be measured). The AHAB signing procedure and rollback counter for the i.MX 95 are Unverified.

OTA (RAUC A/B, signature required) Default

User permissions Default

RoleCan doAuthentication
UserView, event button, sound pause (2 min), brightnessNone
Caregiver+ start · stop recording, link remote notifications, exportPIN
Clinician+ alarm limits · delays, filter mode, Paced setting, enable optionsPIN
Service+ service mode, log export, updates, self-diagnosisPIN + signed service token
ManufacturingWrite serial number · key injection · calibration values → locked after shipmentFactory only

Safety settings (alarm limits · minimum volume · Paced) become "applied" only after CFG_SET to the M7 → read-back confirmation. The M7 keeps running with the last confirmed values.

11. Software safety-class separation (IEC 62304) · modules

Software itemClass (default)Rationale for separation
M7: acquisition · filters · QRS · pace · SpO2 · AlarmCore · alarm tone · power monitoring · A55 supervisionCDecisions leading directly to harm are here
A55: waveform · numerics display (Qt)BM7 monitors the frame counter · displayed alarm list → if it stalls, M7 sounds high priority
A55: AlarmUIBDecisions · sound are on the M7. The M7 enforces the maximum pause time
A55: Holter · trends · cloud · OTA · permissions · logsBFailures surface as technical alarms or are rolled back. Independent of the alarm path
M33: NXP SMSOUPHardware basis for core isolation
RM44L920 (stage 2)C

Basis for the separation: separate cores + SM resource isolation (per LM) + the M7 runs without Linux + external window watchdog. Whether this separation is accepted under the standard is up to the test laboratory, so it is Unverified.

Modules (excerpt)

ModuleLocationInput → output
acq_ecg · acq_ppgM7AFE interrupt · SPI → 8 kSPS frames / 500 SPS 6 streams + timestamps
pace · decim · notch2 · filtM78 kSPS → pace events · 500 SPS · notch state
qrs · spo2M7Beat events · HR / SpO2 · PR · PI · quality
power · alarm_core · supervisorM7Power state · alarm state · alarm tone · watchdog kick · display failure
ipcM7 · A55Rings · RPMsg (C/C++/Python headers auto-generated from a single definition)
ekg-ui · ekg-alarmuiA55Rings · numerics · alarm state → screen, HB_UI, alarm operation
ekg-recorder · ekg-trend · ekg-syncA551-second blocks · SQLite · MQTT · upload
ekg-config · ekg-update · ekg-serviceA55CFG_SET · audit log · RAUC · service commands

Signal-processing code (dsp/) is kept as hardware-independent C so it also builds on a PC, and is unit-tested by replaying public ECG databases. Changing the RTOS would also cost little.

12. Development order · measurement list Default

StageContentCompletion criterion
0 (no board)Protocol definition, signal processing on a PC — decimation · notch · QRS · alarm decisions using recorded and simulator waveformsDetection performance figures output, notch test, alarm step test (80 → 40 · 120 · 0 bpm)
1Start directly with the i.MX 95 evaluation board. Yocto base image, SM configuration, M7 hello + RPMsg + shared ringHELLO/HB round trip, A55 forced-reset measurement S-01
2AFE connection, 8 kSPS acquisition, Qt 6-row screenZero sample loss (24 h), screen latency measured
3AlarmCore + alarm tone + state machine + POST + watchdogFault injection (M7 stall · A55 stall · AFE wire pulled)
4Holter · trends · power integrationRepeated forced power cuts → hash-chain loss ≤ 1–2 s
5Security: AHAB signing, dm-verity, RAUC, USB updatesInvalid signature rejected, rollback after 3 failures
6Cloud (test server in domestic region)Recovery in order after 1 week of lost communication, unconfirmed items not deleted
7Options (12-lead · respiration · NIBP), RM44L920 integrationDual-decision cross-check
Measurement IDWhat
S-01Whether shared DDR and M7 keep running during an A55 reset, Linux reboot time
S-02M7 restart time after reset (length of recording gap meanwhile)
S-03M7 load, 8 kHz interrupt jitter
S-04Electrode → screen latency, AFE159RP8 filter latency
S-05ext4 vs F2FS power-cut test, eMMC Reliable Write behavior
S-06Time from U-Boot to M7 start

Undecided items: lead III handling in the 12-lead option (calculated or 2 AFEs), screen position of the V1/V5 waveform.

13. Test-bench firmware — ESP32-S3 + ADS1293 + heart-rate/oxygen module

This is a test-bench-only configuration, different from the product (AFE159RP8 · AFE4490 · i.MX 95). Its purpose is to check the signal path, frames and receiving screen first using off-the-shelf modules.

Status (as of 2026-10-11): parts not yet arrived. Compilation passed only; zero board uploads and measurements. The "Unverified" items below will be measured and cleared once the parts arrive.

Wiring (ESP32-S3 N16R8)

SignalESP32-S3Notes
ADS1293 SCLK / SDI / SDO / CSBGPIO 12 / 11 / 13 / 10HSPI bus (separate from the 2.0" display bus and pins)
ADS1293 DRDYBGPIO 2Falling-edge interrupt, internal pull-up
ADS1293 ALARMBGPIO 1 (optional)Shown on the status line only
MAX30102 SDA / SCLGPIO 8 / 9400 kHz, INT unused · 10 ms polling
Power3V3 · GNDNo 5 V to the ADS1293 module — VDDIO max 3.6 V

Pins avoided: 4 · 5 · 6 · 7 · 15 (2.0" display) · 19 · 20 (USB) · 43 · 44 (UART0) · 26–32 (flash) · 33–37 (OPI PSRAM) · 0 · 3 · 45 · 46 (boot strapping) · 38 · 48 (RGB LED, depending on board).

ADS1293 — leads I · II · III all measured directly

Channel+−LeadFLEX value
CH1LA (IN2)RA (IN1)I0x11 (same as datasheet example)
CH2LL (IN3)RA (IN1)II0x19 (same as example)
CH3LL (IN3)LA (IN2)III (direct)0x1A — the example turns CH3 off and calculates III = II − I
LA is connected simultaneously to CH1's + and CH3's −. Sharing with the same polarity is what the datasheet example does, but for sharing with opposite polarities there is neither a prohibition nor a permission statement Unverified. So the receiving screen plots the Einthoven residual II − (I + III) for every sample to judge it. The ADS1293 has only 3 channels, so attaching V1/V5 cannot be done at the same time as direct III measurement — a constraint limited to the test bench.

Default electrode assignment: RA→IN1, LA→IN2, LL→IN3, RL←IN4 (RLD output). Since the module jack wiring is unverified, the IN numbers can be changed with command e.

Main registers

AddressValueMeaning
0x06 · 0x07 · 0x080x00 · 0x07 · 0x01DC lead-off on, current injected into IN1–3, 8 nA. Comparison threshold VDD − 0.5 V. Without RL return, all showing "detached" is normal
0x0A · 0x0B · 0x0C0x07 · 0x00 · 0x04Common mode = average of RA · LA · LL → RLD output → IN4 (RL)
0x11 · 0x13 · 0x140x00 · 0x07 · 0x00Internal 2.4 V reference, high resolution on three channels, all on
0x27 · 0x2F0x08 · 0x71DRDYB = CH1 ECG, streaming = status + 3 channels (1+9 bytes)
0x12 · 0x000x04 · 0x01External 4.096 MHz crystal → digital, start conversion after waiting 15 ms. Whether the module has a crystal is Unverified — if not, the firmware warns every second

RL detachment is not caught by lead-off, so whether the RLD output is near the rail (RLDRAIL) is reported separately. Error registers are read every 32 samples, or immediately when the ALARMB bit is set.

Sample rates (datasheet table 8, SDM 102.4 kHz, R1 = 4)

CommandODRBandwidthADCMAXNoise (µV rms, table value)1 LSBCodes per 1 mV
r 533 default533.33 Hz105 Hz0xF300002.070.0861 µV11,612
r 853853.33 Hz175 Hz0xB964F02.670.1129 µV8,859
r 400400 Hz80 Hz0x8000001.810.1635 µV6,117

533 was made the default because it is closest to the notch design based on 500 SPS (the notch coefficients must be recalculated for 533.33 Hz). Only 853 covers the 150 Hz diagnostic bandwidth; all three cover the 40 Hz monitoring bandwidth. Input range is INA differential ±400 mV.

Built-in test signals

CommandSignalExpected code (533 mode)
t ++VTEST = VREF/12 = +200 mV DC10,285,056
t -−200 mV DC5,640,192
t 0Shorted input (noise measurement)7,962,624 (= ADCMAX/2)
t xOff (electrodes)—

Since VTEST and VREF come from the same reference, this is not gain calibration; it is for self-test and checking gain differences between channels. The 1 mV calibration signal (calib) is the job of the external battery simulator.

Heart-rate/oxygen module (presumed MAX30102, I2C 0x57)

Serial protocol

533 SPS × 16 B + 100 × 12 B ≈ 9.7 kB/s (14.9 kB/s at 853 SPS). Binary frames + text lines starting with # (info · 1-second status · warnings); a CSV mode is also available.

ECG frame 16 B : A5 5A 'E' seq(u16 LE) CH1 CH2 CH3(u24, MSB first) flags crc8
PPG frame 12 B : A5 5A 'P' seq(u16 LE) RED IR(u24)                 crc8
crc8 = polynomial 0x07, init 0, from 'E'/'P' through flags

flags: 1 RA detached · 2 LA detached · 4 LL detached · 8 RLD rail · 16 common mode out of range
       32 input out of range · 64 synthetic · 128 test signal

Excerpt of the CRC calculation (the firmware and PC test code use the same header):

static inline uint8_t ekg_crc8(const uint8_t *p, size_t n) {   // polynomial 0x07, initial value 0
  uint8_t c = 0;
  for (size_t i = 0; i < n; i++) {
    c ^= p[i];
    for (int b = 0; b < 8; b++) c = (c & 0x80) ? (uint8_t)((c << 1) ^ 0x07) : (uint8_t)(c << 1);
  }
  return c;
}

Code → voltage conversion uses datasheet equation 13: V = (ADC_OUT/ADCMAX − ½)·2·VREF/3.5.

CommandMeaning
m a / m sADS1293 measured / synthetic (automatically synthetic if no module)
o b / o cBinary / CSV
r 533 · r 853 · r 400Sample rate
t + · t - · t 0 · t xBuilt-in test signals
e 1 2 3 4IN numbers for RA LA LL RL
l 1 · g 1 / g 0Lead-off current ×8 nA (0 = off) · RLD on/off
h 72 · n 50 · w 2 · c 1Synthetic: heart rate · 60 Hz injection µV (RA ×0 · LA ×1 · LL ×2) · white noise µV rms · calib square wave
dADS1293 register dump

The synthetic signal is output in the same ADC code form as the ADS1293, so the receiver does not need to distinguish measured from synthetic (only flags 64 differs). The synthetic model sets limb potentials φRA=0, φLA=I, φLL=II and recalculates the leads as potential differences, so III = II − I holds by definition. The 50° electrical axis is an arbitrary value for synthesis.

Receive · display script (shared by PC · Raspberry Pi)

Safety — before touching the human body

This test bench has no isolation. Module → ESP32 → USB → computer are electrically one piece.

  1. Laptop on battery only. Unplug the charger
  2. Do not plug any other cables into the laptop (HDMI monitor, Ethernet, external disk with power adapter, audio)
  3. Do not use an adapter-powered Raspberry Pi + display for body-electrode tests — only for the synthetic · short · simulator stages
  4. Do not plug directly into a grounded desktop and attach electrodes to the body
  5. While attached, do not touch metal desks · radiators · water pipes
  6. Do not attach over wet skin · wounds. Electrodes are disposable, single use
  7. Do not attach to people with pacemakers · implanted devices
  8. Do not judge health from this device's waveforms · numbers — it is a test bench
  9. Do not apply 5 V to the module (VDDIO max 3.6 V)

Test sequence

StageWhatPass criterion
① SyntheticESP32 only, over USB. Check the entire receiving path without the module (m s · n 50 · c 1)Zero gaps · residual ≤ 2 LSB · heart rate 72 · 60 Hz amplitude equal in I · III and twice that in II
② Shorted noiseModule connected, no electrodes. After confirming REVID=0x01, t 0 for 10 s · t + · t -, then tie the four electrodes to one point at the jack and t xt 0 noise rms ≈ table value (2.07 µV rms in 533 mode) · t ± codes match expected values · no DRDYB warning · lead-off bits 0
③ SimulatorExternal battery simulator (1 V p-p → 1000:1 → 1 mV). Not yet built1 mV reads as 1 mV · residual at noise level (judges whether direct III measurement holds) · raw data saved before/after 60 Hz notch · lead-off response when removing electrodes one by one
④ Human electrodesAfter observing all the safety items above, 4 electrodes RA · LA · LL · RLLead-off 0 · RLD rail 0 · QRS visible · heart rate matches wrist pulse · residual 60 Hz amplitude recorded

Always go through ② · ③ before ④. If a DRDYB warning appears in ② (suspected missing crystal), stop there.

14. Test-bench verification results (2026-10-11)

  • Compilation passed for 2 variants, 0 warnings in test-bench sources (--warnings all, arduino-cli 1.5.2-rc.1 · esp32 core 3.3.12)
    • Basic ESP32-S3 — flash 365,710 B (27 %) · RAM 24,336 B (7 %)
    • Native USB CDC + OPI PSRAM + 16 MB — 366,348 B (27 %) · 25,016 B (7 %)
  • No board upload was done (parts not yet arrived)
  • Frame cross-verification: the same headers as the firmware (frames · synthetic signal) were compiled on a PC to generate a 20-second stream, which was decoded by the Python receiver
    • Of 10,666 ECG frames, the one deliberately CRC-corrupted frame was correctly discarded (counted as 1 gap); 0 loss in 2,000 PPG frames
    • Sync is not lost even when Korean text lines containing 0xA5 0xA5 bytes are mixed in
    • Einthoven residual max 0.086 µV = 1 LSB, rms 0.042 µV → check passed
    • CSV path (line parsing · seq gaps · flags) checked separately
Synthetic-signal replay receive screen: 6 rows of lead I, II, III, residual, pulse wave red, pulse wave IR
Synthetic stream replay screen. The synthetic 60 Hz 50 µV injection is visible in I · II · III, and the residual II−(I+III) row is near 0. The R ratio in the title is a reference value and the SpO2 number is not calibrated. This is a synthetic signal, not a sensor measurement.

Remaining unverified items (to check after parts arrive)

  1. ADS1293 module power configuration (VDD · VDDIO), presence of 4.096 MHz crystal, jack pin assignment, input protection · RLD component values
  2. Whether connecting one input pin simultaneously to CH1 + and CH3 − is allowed (judged by the residual)
  3. Whether the sensor module is a MAX30102, and whether red · IR are swapped
  4. Cross-check against the original MAX30102 datasheet
  5. RGB LED pin and USB port type of the ESP32-S3 board
  6. How many seconds the 8 nA lead-off takes to respond given cable capacitance
  7. Numeric limit for electrode DC current in medical standards