Hardware implementation of a Pantompkins-style ECG processing pipeline with R-peak detection, RR estimation, and morphological feature extraction. The top-level module is pantompkins.sv.
rtl/SystemVerilog RTL blocks (filters, R-peak detection, morph features/descriptor).tb/Testbench top wrapper.simulation/Simulation scripts and Makefile targets for Questa and Verilator.synth/Quartus synthesis scripts/targets.utils/Helper scripts (e.g., dataset conversion).utils/verif/Verification scripts for comparing R-peak outputs vs MIT-BIH annotations.data/Input datasets (e.g., MIT-BIH).MakefileTop-level unified make targets.requirements.txtPython dependencies for data conversion.
rtl/pantompkins.sv
Inputs:
clkSystem clock.clk_enableSample enable.resetActive-high reset.filter_in[11:0]ECG sample input.
Outputs:
morph_p_q8_23[31:0],morph_q_q8_23[31:0],morph_s_q8_23[31:0],morph_t_q8_23[31:0]Normalized morph descriptor outputs.morph_r_time[31:0]R-peak time associated with the morph descriptor.pre_rr_interval[31:0],post_rr_interval[31:0],local_rr_interval[31:0],global_rr_interval[31:0]RR interval features.feat_validValid strobe when RR + morph outputs are aligned.
Internal pipeline (high level):
- Bandpass/filtering (
filter) - Differentiation (
differentiator) - Squaring
- Moving average (
lowp6) - MWI smoothing (
MWI) - R-peak detection (
r_peak_detector_full) - RR calculation (
rr_hr_block) - R-peak refinement (
r_peak_align_stream) - Morphological features (
morph_features) - Morphological descriptor (
morph_descriptor)
ECG (12b) --> filter (FIR 181) --> differentiator (FIR 5) --> square
--> moving avg (lowp6) --> MWI (FIR 51) --> r_peak_detector_full
|-> rr_hr_block
|-> r_peak_align_stream --> morph_features --> morph_descriptor
rtl/filter.sv: 181-tap FIR bandpass/conditioning filter (MATLAB HDL Coder output).rtl/differentiator.sv: 5-tap FIR differentiator.rtl/lowp6.sv: moving average / smoothing over 23 samples (sum of input + 22 delays).rtl/MWI.sv: 51-tap FIR used as MWI smoothing.rtl/r_peak_detector_full.sv: adaptive thresholding, refractory, T-wave rejection, and search-back.rtl/rr_hr_block.sv: RR interval calculation (assumes FS_HZ=360 by default).rtl/r_peak_align_stream.sv: aligns detected R-peak with raw ECG using a delayed stream and a local search.rtl/morph_features.sv: extracts P/Q/S/T times and amplitudes around each R-peak.rtl/morph_descriptor.sv: normalizes R-peak location and amplitude within P/Q/R/S/T extrema.rtl/int_div.sv: iterative integer divider used bymorph_descriptor.
- Sample rate assumptions in control logic:
r_peak_detector_fulluses constants for ~200 ms and ~2 s windows at 360 Hz.rr_hr_blockdefaults toFS_HZ=360.
morph_featureswindows:PRE_R_SAMPLES=43(~120 ms at 360 Hz)POST_R_SAMPLES=72(~200 ms at 360 Hz)SB_PRE_SAMPLES=PRE_R_SAMPLES+245(extended pre-window for search-back)
r_peak_align_stream:TOTAL_LATENCYcontrols alignment between processed stream and raw ECG.- Top-level sets
TOTAL_LATENCY=220inrtl/pantompkins.sv(sum of filter delays: 181+5+22+51). - Top-level sets
SEARCH_BACK=9andSEARCH_FWD=9(±9 sample refinement window).
- The pipeline is streaming and gated by
clk_enable. - The filters have inherent group delay; the alignment stage uses
TOTAL_LATENCYto compensate before morphological extraction. - R-peak detection operates on the MWI output; refined R-peaks are aligned back to raw ECG by
r_peak_align_stream. - If you change filter lengths or sample rate, re-evaluate:
TOTAL_LATENCYinr_peak_align_streamr_peak_detector_fulltiming constants (refractory, init window)rr_hr_block.FS_HZ
The top-level Makefile delegates to simulation/Makefile and also generates input
data before simulation.
Questa:
make questa
Verilator:
make verilator
Examples:
make verilator TRACE=0
make verilator TRACE=1 RUN_FILE=data/100.txt
make verilator SAMPLES=5000
Notes:
simulation/src.argsenumerates RTL sources.- The Verilator flow builds
Vtband runs it. IfTRACE!=0andRUN_FILEis set, GTKWave opens the matching VCD withsimulation/sig.gtkw. simulation/verilator/main.cppreads all files insimulation/data/unlessRUN_FILEis set.- The Verilator run writes VCDs to
simulation/trace/and R-peak indices tosimulation/rpeaks/. TRACE=0disables VCD dumping;RUN_FILE=<path>runs a single input file.SAMPLES=<n>controls how many samplesutils/cvt.pyextracts intosimulation/data/.
- Ensure the required simulator is installed (Questa or Verilator + GTKWave).
- Run one of the make targets above.
- Inspect waveforms and R-peak outputs in the generated traces.
Input files are generated by utils/cvt.py from MIT-BIH data under data/MIT-BIH.
The simulation make targets run this automatically, but you can run it directly:
python3 utils/cvt.py --samples 20000
Install dependencies for utils/cvt.py and utils/verif/check_ver.py:
pip install -r requirements.txt
utils/verif/check_ver.py compares the simulation r-peak CSVs (simulation/rpeaks/*.csv)
against MIT-BIH annotations and writes summary CSV/XLSX files.
Top-level make:
make verify
Simulation make:
make -C simulation verify
Overrides:
make verify VERIFY_ECG_DB=data/MIT-BIH VERIFY_RPEAKS=simulation/rpeaks VERIFY_RESULTS=simulation/results
Each input file in simulation/data/ should be plain text with one signed integer sample per line.
Values are read as 12-bit signed ECG samples (the testbench drives filter_in[11:0]).
Example (simulation/data/example.txt):
12
15
18
20
17
12
8
5
3
1
-2
-6
-9
-7
-3
0
4
10
15
feat_validpulses once per beat when RR and morph descriptors are aligned.- When
feat_validis high, the top-level outputs (morph_*,morph_r_time, and RR intervals) are valid for that beat. - Internally, you can probe:
r_peak_align_stream.r_peak_refined_valid/r_peak_refinedfor refined peak timing.r_peak_detector_full.candidate_validandcandidate_timefor raw detections.rr_hr_block.rr_feat_validwithpre_rr_interval/post_rr_interval/local_rr_interval.morph_features.validwithp/q/r/s/ttime and amplitude outputs.morph_descriptor.morph_donefor descriptor-valid strobe.
Evaluated against MIT-BIH annotations with ±50 ms tolerance.
| Metric | Value |
|---|---|
| Total beats | 109,494 |
| False Positives (FP) | 780 |
| False Negatives (FN) | 768 |
| Failed detections | 1,548 (1.41%) |
| Sensitivity | 99.30% |
| PPV (Precision) | 99.29% |
| F1 Score | 99.29% |
Notable difficult records (>5% failed detection):
| Record | Beats | Failed | Failed (%) | Notes |
|---|---|---|---|---|
| 217 | 2,208 | 329 | 14.9% | Bigeminy/trigeminy pacing artefacts |
| 208 | 2,955 | 380 | 12.9% | Mixed rhythms, frequent PVCs |
| 207 | 1,860 | 196 | 10.5% | Paced beats, bundle branch block |
| 114 | 1,879 | 119 | 6.3% | LBBB |
| 102 | 2,187 | 138 | 6.3% | Paced rhythm |
Fixed-point hardware SVM (L1-RBF kernel) trained on the DS1/DS2 AAMI split from MIT-BIH.
Features are extracted directly by the RTL pipeline and match the hardware exactly.
RR features are normalised by global_rr (rolling 5-min mean), matching the hardware int_div.
Best configuration — morph_1, morph_2, pre_RR, post_RR (4 features):
| Metric | DS1 (train) | DS2 (test) |
|---|---|---|
| Balanced accuracy | 0.9797 | 0.9676 |
| Recall V | 0.9784 ✅ | 0.9522 ✅ |
| Precision V | 0.8101 ✅ | 0.8028 ✅ |
| F1 V | 0.8863 | 0.8711 |
AAMI targets (Sensitivity ≥ 75%, Positive Predictivity ≥ 75%) — both met on DS2.
SVM: L1-RBF kernel, C=0.5, gamma=0.30, weight_V=6 (threshold calibrated on DS1).
Hardware: 822 support vectors — 3,288 MACs per beat.
To reproduce:
python -m utils.classification.train \
--hw-C 0.5 --hw-gamma 0.30 --hw-weight-v 6 \
--hw-features morph_1,morph_2,pre_RR,post_RR \
--hw-train-n 99999 \
--q-weight-scale 4096 \
--hw-exportAdd your preferred license information here.