3、传感器数据采集:生物电位传感器(ADS1292)驱动开发、I2C/SPI总线时序分析、数据缓冲(环形缓冲区实现)
好,咱们进入第三章。这一章,说实话,是整门课里最「硬核」的部分之一。你想想看,医疗贴片的核心是什么?就是把人体那点微弱的生物电信号,准确、实时地抓出来。而ADS1292这颗芯片,就是干这个活的「尖兵」。我个人习惯,每次拿到新板子,第一件事就是先把传感器驱动调通,否则后面全是空中楼阁。
3.1 ADS1292 芯片初识与关键寄存器配置
ADS1292是TI公司的一款双通道、24位Δ-Σ模数转换器,专门为生物电位测量设计的。它内部集成了可编程增益放大器(PGA)、右腿驱动(RLD)电路,还有威尔逊中心端(WCT)——说白了,就是一颗芯片把前端模拟电路全包了。
我在项目中遇到过一个问题:明明信号线都接对了,读出来的数据却全是乱码。后来排查了半天,发现是芯片的参考电压配置错了。ADS1292的参考电压可以内部生成,也可以外部输入。默认情况下,它用的是内部4V参考。但如果你像我一样,用的是3.3V供电的系统,就得小心了——内部参考电压可能不稳定。
下面是我常用的初始化寄存器配置表,直接抄作业就行,但每个寄存器的含义你得搞懂:
| 寄存器地址 | 寄存器名称 | 配置值 | 说明 |
|---|---|---|---|
| 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;
}
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时序、用环形缓冲区把数据接住。下一章我们会讲如何对这些原始数据进行滤波处理,把心电信号从噪声里「捞」出来。