简介:本资源是一套基于nRF52832微控制器实现生物电信号采集与心率同步监测的嵌入式开发实践项目,面向嵌入式初学者及医疗电子方向开发者,解决低功耗可穿戴设备中ECG信号采集、ADS1292芯片驱动、PPS心率事件对齐等关键技术问题。压缩包为ZIP格式,大小8.67MB,包含固件源码、SDK配置文件及硬件接口说明等核心内容,主要文件类型为C源文件、头文件与工程配置文件,支撑从芯片初始化、SPI通信读取ADS1292 24位高精度ECG数据,到PPS脉冲触发时间戳标记的完整链路。已有90人学习下载,资源结构清晰,代码注释详实,覆盖nRF5 SDK底层驱动调用、ADS1292寄存器配置逻辑、数字滤波参数设定及BLE数据透传设计,有助于读者深入理解生物信号前端采集系统软硬协同开发全流程。
1. 用 nRF52832 实时采集 ADS1292 心电信号并输出 PPS 脉冲,不是“蓝牙透传”而是嵌入式闭环控制
很多开发者拿到“nRF52832 + ADS1292”组合的第一反应是:接上蓝牙,把原始 ECG 数据发到手机 App 做心率计算。但标题里那个醒目的PPS(Pulse Per Second)揭示了真实需求——这不是数据上报,而是本地实时闭环处理:ADS1292 持续采样心电信号 → nRF52832 在片上完成 R 波检测与心率计算 → 精确生成与心跳同步的硬件级脉冲信号(PPS),供外部设备(如光电传感器同步模块、ECG 校准仪、嵌入式触发器)直接使用。这种设计常见于医疗级便携设备、多模态生理信号同步采集系统、或需脱离手机依赖的独立监护终端。它绕开了 BLE 协议栈延迟、手机端算法抖动和无线丢包风险,把心率结果以纳秒级精度转化为物理电平跳变。对从事心率血氧测量硬件开发、Android BLE 开发实战中需对接高精度外设、或构建低功耗长期监测节点的工程师而言,这个 ZIP 包本质是一套可裁剪的嵌入式信号处理流水线,而非 demo 工程。
2. 为什么选 nRF52832 驱动 ADS1292?SPI 配置、时序约束与电源噪声隔离是成败关键
ADS1292 是 TI 推出的双通道、24 位 Δ-Σ 心电/生物电专用 ADC,支持内置 PGA、右腿驱动(RLD)、参考电压缓冲等关键医疗功能。而 nRF52832 是 Nordic 的低功耗 Bluetooth 5.0 SoC,其 Cortex-M4F 内核、可编程外设互连(PPI)、以及丰富的定时器资源,恰好满足 ADS1292 的严苛时序要求。二者组合并非简单“MCU 接 ADC”,而是围绕确定性采样、低噪声供电、抗干扰通信三重目标构建硬件链路。
2.1 硬件连接必须满足 ADS1292 的电气约束
ADS1292 对模拟电源(AVDD)、数字电源(DVDD)、参考地(AGND/DGND)分离有明确要求。常见错误是将 nRF52832 的 VDD 和 AVDD 直接共用——这会导致数字开关噪声耦合进模拟链路,使 ECG 基线漂移或工频干扰加剧。正确做法是:
- 使用独立 LDO(如 TPS7A20)为 ADS1292 的 AVDD(2.7–3.6V)供电,纹波 < 10μV;
- nRF52832 的 VDD(1.7–3.6V)通过另一路 LDO 供电;
- AGND 与 DGND 在单点(通常靠近 ADS1292 的 GND 引脚)连接,并铺大面积铜皮散热;
- SPI 信号线(SCLK、DIN、DOUT、CS)需 50Ω 阻抗匹配,CS 线加 100pF 电容滤除毛刺。
提示:ADS1292 的 SCLK 最高支持 2MHz,但实际推荐 1.2MHz。过高的时钟会增大数字噪声辐射,且 nRF52832 的 SPI 外设在高频下易受内部总线竞争影响,导致读取 DRDY 中断丢失。
2.2 SPI 初始化必须启用 DMA 并禁用缓存
nRF52832 的 SPI 外设支持 EasyDMA,这是保证连续采样不丢点的核心。若用轮询或中断方式读取 ADS1292 的 24 位转换结果(含状态字节),CPU 在每次传输后需处理寄存器、判断 DRDY、再启动下次传输——在 250Hz 采样率(4ms 周期)下,中断响应抖动可能超过 50μs,导致采样间隔不稳。正确配置如下(基于 Nordic SDK v17.1):
// 初始化 SPI 主机(对应 nRF52832) nrfx_spim_t spi_instance = NRFX_SPIM_INSTANCE(0); nrfx_spim_config_t spi_config = { .sck_pin = SPIM_SCK_PIN, .mosi_pin = SPIM_MOSI_PIN, // ADS1292 DIN .miso_pin = SPIM_MISO_PIN, // ADS1292 DOUT .ss_pin = SPIM_CS_PIN, // ADS1292 CS (active low) .irq_priority = APP_IRQ_PRIORITY_LOW, .orc = 0xFF, // dummy byte for read .frequency = NRF_SPIM_FREQ_1M, // 1MHz, 平衡噪声与速率 .mode = NRF_SPIM_MODE_0, // CPOL=0, CPHA=0 .bit_order = NRF_SPIM_BIT_ORDER_MSB_FIRST }; // 启用 EasyDMA 缓冲区(双缓冲防溢出) static uint8_t spi_tx_buf[3] = {0x40, 0x00, 0x00}; // ADS1292 读取命令:0x40 + 2字节地址 static uint8_t spi_rx_buf[3]; nrfx_spim_xfer_desc_t xfer_desc = { .p_tx_buffer = spi_tx_buf, .tx_length = sizeof(spi_tx_buf), .p_rx_buffer = spi_rx_buf, .rx_length = sizeof(spi_rx_buf) }; // 启动传输(非阻塞) nrfx_spim_xfer(&spi_instance, &xfer_desc, 0);此代码中spi_tx_buf[0] = 0x40是 ADS1292 的“读取多字节”指令(0x40 | 地址),后续两字节为 dummy;spi_rx_buf将接收 3 字节:1 字节状态 + 2 字节 16 位数据(ADS1292 默认输出 16 位,24 位需配置寄存器)。EasyDMA 确保传输由硬件自动完成,CPU 仅在NRFX_SPIM_EVT_DONE事件中解析结果,释放出足够时间运行 R 波检测算法。
2.3 DRDY 中断必须绑定到 PPI 通道实现零延迟响应
ADS1292 的 DRDY(Data Ready)引脚在每次转换完成时拉低,这是采样同步的唯一可靠依据。若用 GPIO 中断处理 DRDY,从电平变化到 ISR 执行存在数微秒延迟,且中断优先级冲突可能导致漏判。nRF52832 的 PPI(Programmable Peripheral Interconnect)可将 DRDY 电平变化直接触发 SPI 启动,全程无需 CPU 参与:
// 绑定 DRDY 引脚(假设为 P0.12)到 PPI 通道 nrf_gpio_cfg_sense_input(DRDY_PIN, NRF_GPIO_PIN_PULLUP, NRF_GPIO_PIN_SENSE_LOW); uint32_t drdy_event_addr = nrf_gpiote_event_addr_get(NRF_GPIOTE_EVENTS_IN[0]); uint32_t spim_task_addr = nrf_spim_task_addr_get(NRF_SPIM_TASK_START); // 配置 PPI 通道:DRDY 下降沿 → 启动 SPI 传输 nrf_ppi_channel_endpoint_setup( NRF_PPI_CHANNEL0, drdy_event_addr, spim_task_addr ); nrf_ppi_channels_enable(NRF_PPI_CHANNELS_CH0_MASK);此配置下,DRDY 下降沿瞬间触发 SPI 传输,整个链路延迟 < 200ns,彻底消除软件引入的采样抖动。这是实现 PPS 输出时间精度(±1μs)的前提。
3. 从原始 ECG 到 PPS:R 波检测算法、定时器同步与 PPS 电平生成全流程
PPS(Pulse Per Second)在此场景中并非指“每秒一个脉冲”,而是“每个心跳周期生成一个精确宽度的方波脉冲”,其上升沿严格对齐 R 波峰值时刻。这意味着算法必须在单次采样周期内完成:数据接收 → 滤波 → R 波定位 → 定时器预装 → GPIO 翻转。nRF52832 的 64MHz 主频和硬件加速器(如 QDEC、SWI)为此提供支撑。
3.1 片上实时 R 波检测:三阶 IIR 滤波 + 自适应阈值法
ADS1292 输出的是原始 16 位 ECG 数据(范围 -32768 ~ +32767),需先抑制基线漂移和工频干扰。我们采用分步处理:
- 基线校正:用一阶 IIR 高通滤波器(fc=0.5Hz),系数
a1 = 0.9992,b0 = 1,b1 = -1,避免相位失真; - 陷波滤波:50Hz 或 60Hz 二阶 IIR 陷波器(Q=30),系数由 MATLAB
iirnotch生成后固化; - R 波增强:对滤波后信号求导 + 平方 + 滑动窗积分(窗长 150ms),突出 R 波能量峰。
核心检测逻辑(C 语言伪码):
// 假设 ecg_sample 为当前滤波后采样值(int16_t) static int16_t ecg_buffer[500]; // 2s 环形缓冲区(250Hz × 2s) static uint16_t buffer_idx = 0; ecg_buffer[buffer_idx++] = ecg_sample; if (buffer_idx >= 500) buffer_idx = 0; // 计算当前 R 波候选:导数平方积分 int32_t energy_sum = 0; for (int i = 0; i < 150; i++) { // 150ms = 37.5 个点 int16_t idx = (buffer_idx - i + 500) % 500; int32_t diff = ecg_buffer[idx] - ecg_buffer[(idx+1)%500]; energy_sum += (diff * diff) >> 8; // 缩放防溢出 } // 自适应阈值:均值 + 3×标准差(滚动更新) static int32_t avg_energy = 0, std_energy = 0; avg_energy = (avg_energy * 0.99) + (energy_sum * 0.01); std_energy = (std_energy * 0.99) + ((energy_sum - avg_energy) * (energy_sum - avg_energy) * 0.01); int32_t threshold = avg_energy + (int32_t)sqrtf(std_energy) * 3; if (energy_sum > threshold && !r_peak_flag) { // 确认 R 波:检查前 100ms 无更高能量点(防误触发) bool is_true_peak = true; for (int i = 1; i <= 25; i++) { // 100ms int16_t prev_idx = (buffer_idx - i + 500) % 500; if (ecg_buffer[prev_idx] > ecg_buffer[buffer_idx]) { is_true_peak = false; break; } } if (is_true_peak) { r_peak_flag = true; r_peak_time = nrf_timer_us_get(NRF_TIMER0); // 记录当前微秒时间戳 } }该算法在 nRF52832 上单次执行耗时 < 80μs,满足 250Hz 实时性。
3.2 PPS 生成:利用 TIMER0 和 PPI 实现亚微秒级脉冲输出
PPS 脉冲宽度通常设为 100μs~1ms,上升沿需精确对齐 R 波峰值。若用软件延时(nrf_delay_us())生成,误差可达 ±5μs。正确方案是:
- 将 R 波检测结果(
r_peak_time)写入 TIMER0 的 CC[0] 寄存器; - 配置 TIMER0 为 1MHz 计数模式(1μs 分辨率);
- 用 PPI 将 TIMER0 的 COMPARE[0] 事件绑定到 GPIO OUTSET/OUTCLR 任务,控制 PPS 引脚翻转。
// 初始化 TIMER0(1MHz,自由运行) nrf_timer_frequency_set(NRF_TIMER0, NRF_TIMER_FREQ_1MHz); nrf_timer_mode_set(NRF_TIMER0, NRF_TIMER_MODE_TIMER); nrf_timer_bit_width_set(NRF_TIMER0, NRF_TIMER_BIT_WIDTH_32); // PPS 引脚:P0.15(假设) nrf_gpio_cfg_output(PPS_PIN); // PPI 通道 1:TIMER0 COMPARE[0] → GPIO OUTSET(上升沿) uint32_t timer_event_addr = nrf_timer_event_addr_get(NRF_TIMER0, NRF_TIMER_EVENT_COMPARE0); uint32_t gpio_task_addr = nrf_gpio_task_addr_get(NRF_GPIO_TASK_OUTSET); nrf_ppi_channel_endpoint_setup(NRF_PPI_CHANNEL1, timer_event_addr, gpio_task_addr); // PPI 通道 2:TIMER0 COMPARE[1] → GPIO OUTCLR(下降沿) uint32_t timer_event2_addr = nrf_timer_event_addr_get(NRF_TIMER0, NRF_TIMER_EVENT_COMPARE1); uint32_t gpio_task2_addr = nrf_gpio_task_addr_get(NRF_GPIO_TASK_OUTCLR); nrf_ppi_channel_endpoint_setup(NRF_PPI_CHANNEL2, timer_event2_addr, gpio_task2_addr); // 启用通道 nrf_ppi_channels_enable(NRF_PPI_CHANNELS_CH1_MASK | NRF_PPI_CHANNELS_CH2_MASK);当检测到 R 波时,执行:
// 设置 COMPARE[0] = 当前时间(立即触发上升沿) nrf_timer_cc_write(NRF_TIMER0, 0, r_peak_time); // 设置 COMPARE[1] = r_peak_time + 100(100μs 后下降沿) nrf_timer_cc_write(NRF_TIMER0, 1, r_peak_time + 100); nrf_timer_shorts_enable(NRF_TIMER0, NRF_TIMER_SHORT_COMPARE0_CLEAR | NRF_TIMER_SHORT_COMPARE1_CLEAR); nrf_timer_task_trigger(NRF_TIMER0, NRF_TIMER_TASK_START);此方案下,PPS 上升沿与 R 波峰值的时间偏差 < 1μs,完全满足医疗设备同步要求。
3.3 心率值同步广播:BLE 服务设计兼顾 Android 兼容性
PPS 是硬件输出,但心率数值仍需通过 BLE 上传至 Android 设备。为适配“Android BLE 开发实战 心率监测 App”,必须遵循 SIG Heart Rate Service (0x180D) 规范:
- Characteristic UUID
0x2A37(Heart Rate Measurement)需包含:Flags(8-bit)、HR Value(uint8_t)、RR-Interval(可选 uint16_t 数组); - Flags 字节必须设置 bit0(HR Value Format)为 0(uint8_t),bit2(RR-Interval Present)为 1(若发送 RR 间期);
- 通知(Notify)需启用,且 Android 端需注册
android.bluetooth.le.BluetoothLeScanner扫描。
关键代码片段(Nordic SDK):
// 定义 HR Measurement 特征 ble_hrs_init_t hrs_init; memset(&hrs_init, 0, sizeof(hrs_init)); hrs_init.evt_handler = hrs_evt_handler; hrs_init.is_sensor_supported = true; hrs_init.rr_interval_enabled = true; // 启用 RR 间期 hrs_init.battery_level_support = false; hrs_init.body_sensor_location_support = false; // 初始化服务 err_code = ble_hrs_init(&m_hrs, &hrs_init); APP_ERROR_CHECK(err_code); // 发送心率(假设 heart_rate = 72 bpm) uint8_t hr_data[3] = {0x04, 72}; // Flags: 0x04 = uint8_t + RR present uint16_t len = sizeof(hr_data); err_code = ble_hrs_heart_rate_measurement_send(&m_hrs, hr_data, len);Android 端 App 只需监听0x2A37特征即可解析,无需定制协议,降低开发门槛。
4. 调试与验证:用逻辑分析仪抓 PPS 时序、用 Python 解析 BLE 报文、排查 ADS1292 噪声源
PPS 输出是否真正“精确”?BLE 心率值是否与 PPS 严格对应?这些不能靠代码逻辑推演,必须实测验证。以下是工程师现场必做的三项验证动作。
4.1 逻辑分析仪捕获 PPS 与 DRDY 时序关系
使用 Saleae Logic Pro 8 或类似设备,同时接入:
- ADS1292 的 DRDY 引脚(通道 0);
- nRF52832 的 PPS 输出引脚(通道 1);
- nRF52832 的 SCLK(通道 2,确认 SPI 时序)。
典型合格波形应显示:
- DRDY 下降沿到 PPS 上升沿 ≤ 2μs(证明 PPI 链路有效);
- 相邻 PPS 上升沿间隔 = 60000 / HR(如 HR=75,则间隔=800ms),标准差 < 1ms;
- SCLK 在 DRDY 下降后立即启动,无空闲周期。
若发现 PPS 延迟 > 5μs,检查 PPI 通道是否被其他外设抢占(如 UART DMA);若 PPS 间隔抖动大,检查 ADS1292 的 REFOUT 是否稳定(用万用表测 REFOUT 引脚电压,应为 2.4V ± 0.01V)。
4.2 Python 脚本解析 BLE 广播包,比对心率一致性
在 PC 端运行以下脚本(需安装bluepy),持续抓取设备广播的 HR 值,并与 PPS 实际周期比对:
from bluepy.btle import Scanner, DefaultDelegate import time class ScanDelegate(DefaultDelegate): def __init__(self): DefaultDelegate.__init__(self) self.last_hr = 0 self.start_time = time.time() def handleDiscovery(self, dev, isNewDev, isNewData): if dev.addr == "XX:XX:XX:XX:XX:XX": # 替换为目标设备 MAC for (adtype, desc, value) in dev.getScanData(): if adtype == 22: # 0x16 = 128-bit UUID if value.startswith("0000180d"): # HR Service UUID # 解析 HR Measurement (0x2A37) if len(value) >= 12: flags = int(value[8:10], 16) hr_val = int(value[10:12], 16) print(f"[{time.time()-self.start_time:.3f}s] HR={hr_val} bpm") self.last_hr = hr_val scanner = Scanner().withDelegate(ScanDelegate()) scanner.scan(10.0) # 扫描 10 秒运行时,用秒表计数 PPS 脉冲数,计算实际心率。若 Python 读出 HR=72,而 10 秒内 PPS 脉冲数为 718(即 71.8 bpm),误差在 ±0.3 bpm 内即为合格。超出则需检查 R 波检测算法的灵敏度阈值。
4.3 ADS1292 噪声根因排查表:从电源到布局的 5 个关键点
| 现象 | 可能原因 | 验证方法 | 解决方案 |
|---|---|---|---|
| ECG 基线大幅漂移 | AVDD 电源纹波 > 50μV | 用示波器 AC 耦合测 AVDD 对地 | 更换 LDO,增加 10μF 陶瓷电容+100nF 钽电容并联 |
| 50Hz 工频干扰强烈 | PCB 地平面分割不当,RLD 回路未闭合 | 查看 RLD 引脚是否接至人体电极并返回 ADS1292 RLDIN | 确保 RLD 走线短直,RLDIN 与 REFOUT 共用去耦电容 |
| R 波漏检率 > 5% | DRDY 引脚上拉电阻过大(> 10kΩ) | 用逻辑分析仪看 DRDY 低电平宽度是否 < 100ns | 改用 2.2kΩ 上拉,DRDY 线长 < 2cm |
| PPS 脉冲宽度不稳定 | TIMER0 时钟源未锁定为 LFRC | 读取NRF_CLOCK->LFCLKSTAT寄存器 | 在clock_init()中调用nrf_clock_lf_src_set(NRF_CLOCK_LF_SRC_RC)并等待就绪 |
| BLE 连接后 PPS 停止 | SoftDevice 占用 IRQ 优先级过高,阻塞 PPI | 检查NRF_SDH_CLOCK_CONFIG_LF_SRC配置 | 将 SoftDevice IRQ 优先级设为NVIC_SetPriority(SD_IRQn, 3),PPI 保持最高 |
注意:ADS1292 的 REFOUT 引脚必须接 1μF 电容到 AGND,否则内部基准不稳,导致所有通道增益漂移。这是新手最常忽略的硬件陷阱。
5. 进阶技巧:用 nRF52832 的 QDEC 外设实现 PPS 宽度动态调节,适配不同临床场景
PPS 脉冲宽度并非固定值——在临床研究中,需根据设备用途动态调整:
- 同步触发模式:宽度 10μs,用于触发高速摄像机或 EEG 采集卡;
- 指示灯模式:宽度 200ms,驱动 LED 显示实时心率;
- 校准模式:宽度 1s,作为设备自检信号。
nRF52832 的 QDEC(Quadrature Decoder)外设本用于编码器,但可巧妙复用为低功耗脉冲宽度测量器,实现 PPS 宽度的实时反馈调节。原理是:将 PPS 输出同时接入 QDEC 的 A 相输入,用另一个 GPIO 模拟 B 相产生固定延迟,QDEC 计数即反映 PPS 宽度。
5.1 QDEC 测量 PPS 宽度的电路与配置
接线方式:
- PPS 引脚 → QDEC A 相(P0.01);
- 用定时器比较事件在 PPS 上升沿后延迟 X μs,触发 GPIO 翻转 → QDEC B 相(P0.02);
- QDEC 计数器值 = X μs 对应的计数值(1MHz 时钟下,1 count = 1μs)。
初始化代码:
// QDEC 初始化(仅用 A 相,B 相作延迟触发) nrf_qdec_pins_set(NRF_QDEC, QDEC_A_PIN, QDEC_B_PIN, QDEC_LED_PIN); nrf_qdec_decoder_set(NRF_QDEC, NRF_QDEC_RESOLUTION_8X, NRF_QDEC_REPORTPERIOD_10, NRF_QDEC_SAMPLEPERIOD_10); nrf_qdec_enable(NRF_QDEC); // 启动 QDEC nrf_qdec_task_trigger(NRF_QDEC, NRF_QDEC_TASK_START);当 PPS 上升沿到来,QDEC 开始计数;B 相延迟信号到达,QDEC 停止计数。读取NRF_QDEC->SAMPLE.COUNTER即得实际宽度(单位:μs)。
5.2 动态宽度调节策略:基于心率变异性的自适应算法
临床研究表明,心率变异性(HRV)高的人群,PPS 宽度宜设窄(防脉冲重叠);HRV 低者(如运动员静息态),可设宽以增强外部设备识别。算法如下:
// 计算最近 10 个 RR 间期的标准差(ms) static uint16_t rr_buffer[10]; static uint8_t rr_idx = 0; rr_buffer[rr_idx++] = current_rr_ms; if (rr_idx >= 10) rr_idx = 0; uint32_t sum = 0, sum_sq = 0; for (int i = 0; i < 10; i++) { sum += rr_buffer[i]; sum_sq += rr_buffer[i] * rr_buffer[i]; } uint16_t hrv_std = sqrtf((sum_sq - sum*sum/10) / 9); // 动态设置 PPS 宽度 uint16_t pps_width_us; if (hrv_std < 20) { // 低 HRV:宽脉冲 pps_width_us = 500; } else if (hrv_std < 50) { // 中等 HRV pps_width_us = 100; } else { // 高 HRV:窄脉冲 pps_width_us = 20; } // 更新 TIMER0 COMPARE[1] nrf_timer_cc_write(NRF_TIMER0, 1, r_peak_time + pps_width_us);此技巧让同一硬件平台适配心率血氧测量、运动康复评估、睡眠分期研究等多种场景,无需改硬件,只需更新固件参数。
5.3 PPS 输出的电气增强:驱动长线缆与工业级设备
当 PPS 需驱动 > 1m 的屏蔽双绞线(如连接至 PLC 或示波器),GPIO 直接输出可能因容性负载导致边沿变缓(上升时间 > 100ns)。此时应外接 74LVC1G07 缓冲器(开漏输出,支持 5V 电平),并将 PPS 引脚配置为开漏模式:
nrf_gpio_cfg_output(PPS_PIN); nrf_gpio_cfg_default(PPS_PIN); // 恢复默认 nrf_gpio_cfg(PPS_PIN, NRF_GPIO_PIN_DIR_OUTPUT, NRF_GPIO_PIN_INPUT_DISCONNECT, NRF_GPIO_PIN_NOPULL, NRF_GPIO_PIN_H0D, // 开漏模式 NRF_GPIO_PIN_NOSENSE);缓冲器输出端接 1kΩ 上拉至 5V,即可驱动 100pF 负载下上升时间 < 5ns,满足工业设备同步要求。
本文还有配套的精品资源,点击获取