3、传感器数据采集:生物电位传感器(ADS1292)驱动开发、I2C/SPI总线时序分析、数据缓冲(环形缓冲区实现)

好,咱们进入第三章。这一章,说实话,是整门课里最「硬核」的部分之一。你想想看,医疗贴片的核心是什么?就是把人体那点微弱的生物电信号,准确、实时地抓出来。而ADS1292这颗芯片,就是干这个活的「尖兵」。我个人习惯,每次拿到新板子,第一件事就是先把传感器驱动调通,否则后面全是空中楼阁。

3.1 ADS1292 芯片初识与关键寄存器配置

ADS1292是TI公司的一款双通道、24位Δ-Σ模数转换器,专门为生物电位测量设计的。它内部集成了可编程增益放大器(PGA)、右腿驱动(RLD)电路,还有威尔逊中心端(WCT)——说白了,就是一颗芯片把前端模拟电路全包了。

我在项目中遇到过一个问题:明明信号线都接对了,读出来的数据却全是乱码。后来排查了半天,发现是芯片的参考电压配置错了。ADS1292的参考电压可以内部生成,也可以外部输入。默认情况下,它用的是内部4V参考。但如果你像我一样,用的是3.3V供电的系统,就得小心了——内部参考电压可能不稳定。

⚠️ 注意: 内部参考电压(VREF = 4V)要求AVDD至少为4.5V。如果系统供电只有3.3V,必须使用外部参考电压,或者将AVDD单独供电到5V。

下面是我常用的初始化寄存器配置表,直接抄作业就行,但每个寄存器的含义你得搞懂:

寄存器地址 寄存器名称 配置值 说明
0x01 CONFIG1 0x96 设置采样率250SPS,内部振荡器
0x02 CONFIG2 0xC0 开启测试信号,用于自检
0x03 CONFIG3 0xEC 开启RLD,内部参考缓冲使能
0x05 CH1SET 0x60 通道1增益12,正常输入
0x06 CH2SET 0x60 通道2增益12,正常输入
0x07 RLD_SENS 0x2C RLD从两个通道的P端取信号

3.2 SPI总线时序分析与驱动开发

ADS1292使用SPI接口通信。SPI这东西,说白了就是四根线:SCLK、DIN、DOUT、CS。但ADS1292有个特殊的地方——它用DOUT线同时做数据输出和「数据就绪」指示。

为什么会这样?因为ADS1292的DOUT引脚在CS拉低后,会先输出一个低电平脉冲,表示「数据准备好了」。这个脉冲叫DRDY(Data Ready)。你必须在检测到这个低电平后,再开始读数据。我刚开始写驱动时,没注意这个细节,直接连续读SPI,结果读到的全是前一帧的旧数据。

💡 关键时序点:
  • CS拉低后,等待DOUT变为低电平(DRDY信号)
  • 然后发送24个SCLK时钟,读取一个通道的24位数据
  • 连续读取两个通道,共48个SCLK
  • 最后拉高CS,结束本次传输

下面是我写的一个SPI读取函数,加了详细的注释:

// SPI读取ADS1292双通道数据
// 返回0表示成功,-1表示超时
int ads1292_read_data(int32_t *ch1, int32_t *ch2) {
    uint8_t rx_buf[6];  // 2通道 * 3字节 = 6字节
    uint32_t timeout = 1000;
    
    // 1. 拉低CS,开始通信
    HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET);
    
    // 2. 等待DRDY信号(DOUT变低)
    while (HAL_GPIO_ReadPin(DOUT_GPIO_Port, DOUT_Pin) != GPIO_PIN_RESET) {
        if (--timeout == 0) {
            HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET);
            return -1;  // 超时,返回错误
        }
    }
    
    // 3. 连续读取6字节(SPI时钟由主机提供)
    //    注意:ADS1292的SPI模式是模式1(CPOL=0, CPHA=1)
    HAL_SPI_Receive(&hspi1, rx_buf, 6, 100);
    
    // 4. 拉高CS,结束传输
    HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET);
    
    // 5. 将3字节拼接成24位有符号数
    *ch1 = (int32_t)((rx_buf[0] << 16) | (rx_buf[1] << 8) | rx_buf[2]);
    *ch2 = (int32_t)((rx_buf[3] << 16) | (rx_buf[4] << 8) | rx_buf[5]);
    
    // 6. 符号扩展(24位转32位)
    if (*ch1 & 0x800000) *ch1 |= 0xFF000000;
    if (*ch2 & 0x800000) *ch2 |= 0xFF000000;
    
    return 0;
}
🔧 避坑指南: 我曾经在SPI时钟极性上栽过跟头。ADS1292要求SPI模式1(CPOL=0, CPHA=1),也就是时钟空闲为低,数据在时钟下降沿采样。如果你用的是STM32的HAL库,记得在初始化时设置正确。否则读出来的数据会错位一位,整条波形都是歪的。

3.3 数据缓冲:环形缓冲区实现

传感器数据采集是个「生产-消费」模型。ADC中断服务函数是生产者,主循环里的算法处理是消费者。如果两者速度不匹配,数据就会丢失。环形缓冲区(Ring Buffer)就是解决这个问题的经典方案。

嗯,这里要注意:环形缓冲区不是万能的。如果生产者速度持续大于消费者,缓冲区满了之后,新数据会覆盖旧数据。所以缓冲区大小要根据采样率和处理时间来估算。

以ADS1292为例,采样率250SPS,每个样本两个通道共6字节。如果主循环处理一个样本需要10ms,那么缓冲区至少需要能存3个样本(30ms的数据量)。我个人习惯留出3倍余量,也就是至少9个样本的缓冲区。

下面是我常用的环形缓冲区实现,支持任意数据类型:

// 环形缓冲区结构体
typedef struct {
    uint8_t *buffer;    // 数据缓冲区
    uint32_t size;      // 缓冲区大小(字节数)
    uint32_t head;      // 写指针
    uint32_t tail;      // 读指针
    uint32_t count;     // 当前数据量
} ring_buffer_t;

// 初始化环形缓冲区
void ring_buffer_init(ring_buffer_t *rb, uint8_t *buf, uint32_t size) {
    rb->buffer = buf;
    rb->size = size;
    rb->head = 0;
    rb->tail = 0;
    rb->count = 0;
}

// 写入数据(生产者调用)
// 返回0成功,-1表示缓冲区满
int ring_buffer_write(ring_buffer_t *rb, uint8_t *data, uint32_t len) {
    // 检查剩余空间
    if (rb->count + len > rb->size) {
        return -1;  // 缓冲区满,数据丢失
    }
    
    // 分两段写入(处理回绕)
    uint32_t space_to_end = rb->size - rb->head;
    if (len <= space_to_end) {
        // 不需要回绕
        memcpy(&rb->buffer[rb->head], data, len);
        rb->head += len;
    } else {
        // 需要回绕
        memcpy(&rb->buffer[rb->head], data, space_to_end);
        memcpy(rb->buffer, data + space_to_end, len - space_to_end);
        rb->head = len - space_to_end;
    }
    
    rb->count += len;
    return 0;
}

// 读取数据(消费者调用)
// 返回实际读取的字节数
int ring_buffer_read(ring_buffer_t *rb, uint8_t *data, uint32_t len) {
    // 检查可用数据
    if (rb->count < len) {
        len = rb->count;  // 有多少读多少
    }
    if (len == 0) return 0;
    
    // 分两段读取
    uint32_t data_to_end = rb->size - rb->tail;
    if (len <= data_to_end) {
        memcpy(data, &rb->buffer[rb->tail], len);
        rb->tail += len;
    } else {
        memcpy(data, &rb->buffer[rb->tail], data_to_end);
        memcpy(data + data_to_end, rb->buffer, len - data_to_end);
        rb->tail = len - data_to_end;
    }
    
    rb->count -= len;
    return len;
}
⚡ 实战要点:
  • 在ADC中断中调用ring_buffer_write,注意中断服务函数要尽量短
  • 在主循环中调用ring_buffer_read,处理完数据后再读取下一批
  • 如果发现缓冲区经常满,说明处理速度跟不上,需要优化算法或降低采样率
  • 我曾经在一个项目里,因为缓冲区太小,导致心电信号的R波峰值被截断,诊断结果完全错误。后来把缓冲区从256字节扩大到1024字节,问题就解决了。

3.4 实战:将ADS1292数据送入环形缓冲区

最后,我们把上面所有知识点串起来。在ADC的数据就绪中断中,读取ADS1292的数据,然后写入环形缓冲区。主循环再从缓冲区取出数据,进行滤波和特征提取。

// 定义环形缓冲区(全局变量)
#define BUFFER_SIZE 1024
static uint8_t buffer_pool[BUFFER_SIZE];
static ring_buffer_t data_ring;

// 初始化
void sensor_init(void) {
    ring_buffer_init(&data_ring, buffer_pool, BUFFER_SIZE);
    ads1292_init();  // 配置寄存器
    ads1292_start(); // 开启连续转换模式
}

// ADC数据就绪中断回调(由DRDY引脚触发的外部中断)
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
    if (GPIO_Pin == DRDY_Pin) {
        int32_t ch1, ch2;
        uint8_t sample_buf[6];
        
        // 读取ADS1292数据
        if (ads1292_read_data(&ch1, &ch2) == 0) {
            // 将24位数据拆成字节,存入缓冲区
            sample_buf[0] = (ch1 >> 16) & 0xFF;
            sample_buf[1] = (ch1 >> 8) & 0xFF;
            sample_buf[2] = ch1 & 0xFF;
            sample_buf[3] = (ch2 >> 16) & 0xFF;
            sample_buf[4] = (ch2 >> 8) & 0xFF;
            sample_buf[5] = ch2 & 0xFF;
            
            // 写入环形缓冲区
            ring_buffer_write(&data_ring, sample_buf, 6);
        }
    }
}

// 主循环中处理数据
void main_loop(void) {
    uint8_t sample_buf[6];
    
    while (1) {
        // 从缓冲区读取一个样本
        if (ring_buffer_read(&data_ring, sample_buf, 6) == 6) {
            // 还原24位数据
            int32_t ch1 = (sample_buf[0] << 16) | (sample_buf[1] << 8) | sample_buf[2];
            int32_t ch2 = (sample_buf[3] << 16) | (sample_buf[4] << 8) | sample_buf[5];
            
            // 符号扩展
            if (ch1 & 0x800000) ch1 |= 0xFF000000;
            if (ch2 & 0x800000) ch2 |= 0xFF000000;
            
            // 这里进行后续处理:滤波、特征提取等
            process_ecg_sample(ch1, ch2);
        }
        
        // 其他任务...
    }
}
💡 个人经验: 在中断里调用ring_buffer_write时,一定要保证这个函数不会被重入。如果主循环也在操作同一个环形缓冲区,记得加临界区保护。我习惯在写操作前关闭全局中断,写完后立即打开。虽然会引入微秒级的延迟,但对于250SPS的采样率来说,完全不影响。

好了,这一章的内容就到这里。ADS1292的驱动开发,说白了就是三件事:配好寄存器、搞定SPI时序、用环形缓冲区把数据接住。下一章我们会讲如何对这些原始数据进行滤波处理,把心电信号从噪声里「捞」出来。