3、原始数据采集:配置传感器寄存器、读取红外和红光数据、数据缓存与时间戳
好,咱们进入实战环节的第一个硬骨头——原始数据采集。
说实话,很多做可穿戴设备的工程师,最后发现算法不准,问题往往出在这一步。传感器寄存器没配好,数据读出来就是错的,后面再怎么滤波、怎么算心率,都是白搭。我个人习惯,拿到一个新传感器,第一件事就是看数据手册里的寄存器映射表,而不是急着写代码。
3.1 传感器寄存器配置——别小看这一步
以我们常用的MAX30102为例。这个传感器内部有十几个寄存器,但真正需要配置的,其实就几个关键位。
核心寄存器配置清单:
| 寄存器地址 | 寄存器名称 | 配置值 | 说明 |
|---|---|---|---|
| 0x09 | FIFO配置 | 0x4F | 采样率100Hz,FIFO满时停止 |
| 0x0A | 模式配置 | 0x03 | 红光+红外双LED模式 |
| 0x0C | 红光LED电流 | 0x24 | 约6.4mA,根据肤色调整 |
| 0x0D | 红外LED电流 | 0x24 | 同上,保持对称 |
嗯,这里要注意。LED电流不是越大越好。我在项目中遇到过,把电流调到最大,结果信号饱和了,波形直接削顶。你想想看,削顶后的数据,算出来的血氧值能准吗?
3.2 读取红外和红光数据——I2C通信细节
配置好寄存器,接下来就是读数据了。MAX30102的数据存在FIFO里,每次读出来的是3个字节:
- 高字节(MSB):数据的高8位
- 中字节:数据的中间8位
- 低字节(LSB):数据的低8位
说白了,就是18位的数据,分3次读出来。我习惯用I2C连续读取,一次读3个字节,然后拼成一个32位整数。
// 读取FIFO数据,一次读6个字节(红光+红外各3字节)
uint8_t buffer[6];
i2c_read(MAX30102_ADDR, REG_FIFO_DATA, buffer, 6);
// 拼装红光数据
uint32_t red = ((uint32_t)buffer[0] << 16) |
((uint32_t)buffer[1] << 8) |
(uint32_t)buffer[2];
// 拼装红外数据
uint32_t ir = ((uint32_t)buffer[3] << 16) |
((uint32_t)buffer[4] << 8) |
(uint32_t)buffer[5];
为什么先读红光再读红外?因为传感器内部FIFO的存储顺序就是这样的。你如果顺序搞反了,那算出来的血氧值就完全不对了。
3.3 数据缓存与时间戳——别让数据丢了
原始数据读出来了,但你不能直接扔给算法。为什么?因为算法需要连续的数据流,中间不能有断点。我见过有人用环形缓冲区,也有人用链表。我个人习惯用环形缓冲区,简单、高效、不涉及动态内存分配。
环形缓冲区设计要点:
- 缓冲区大小:至少能存2秒的数据。100Hz采样率,就是200个样本点。我一般设512,留点余量。
- 读写指针:写指针由中断服务函数更新,读指针由算法线程更新。注意加互斥锁,防止数据竞争。
- 时间戳:每个数据点都要打时间戳。用微秒级定时器,或者RTOS的系统滴答。
// 环形缓冲区结构体
typedef struct {
uint32_t red_data[BUFFER_SIZE];
uint32_t ir_data[BUFFER_SIZE];
uint64_t timestamp[BUFFER_SIZE]; // 微秒级时间戳
volatile uint16_t write_index;
volatile uint16_t read_index;
} ring_buffer_t;
// 写入数据(在中断中调用)
void buffer_write(uint32_t red, uint32_t ir) {
uint16_t next = (buffer.write_index + 1) % BUFFER_SIZE;
if (next != buffer.read_index) { // 缓冲区未满
buffer.red_data[buffer.write_index] = red;
buffer.ir_data[buffer.write_index] = ir;
buffer.timestamp[buffer.write_index] = get_us_timer();
buffer.write_index = next;
} else {
// 缓冲区溢出,丢弃最旧的数据
buffer.read_index = (buffer.read_index + 1) % BUFFER_SIZE;
buffer.write_index = next;
}
}
你想想看,如果没有时间戳,你只知道数据点A在数据点B前面,但不知道它们之间间隔了多少微秒。算法需要精确的时间间隔来计算心率,这个误差会直接传递到最终结果。
3.4 实战中的常见问题
最后,分享几个我踩过的坑:
- I2C时钟频率:MAX30102最高支持400kHz。但有些开发板的I2C默认是100kHz,读数据会慢。我建议用400kHz,但注意线长不要超过10cm。
- FIFO读取时机:不要用轮询方式读FIFO。用中断,每次FIFO达到半满或者满时触发。这样可以减少CPU占用。
- 数据对齐:红光和红外数据是交替存储在FIFO里的。读的时候要成对读,否则数据会错位。
嗯,原始数据采集这部分,说白了就是三个字:稳、准、快。寄存器配置要稳,数据读取要准,缓存和时间戳要快。这三步做好了,后面的算法才有意义。
下一章,我们会聊怎么对这些原始数据进行预处理——滤波、去基线漂移、去运动伪迹。到时候你会发现,原始数据采集阶段留下的坑,都会在预处理阶段暴露出来。所以,这一章的内容,值得你多花点时间。