2. 数据采集基础:ADC采样、I2C/SPI/UART通信协议在传感器读取中的应用
好,咱们进入第二讲。数据采集,说白了就是让单片机“听”懂传感器的“语言”。
我刚开始做智能灯具项目时,最头疼的就是传感器数据读不准。明明光照传感器放在窗边,读回来的数值却像抽风一样乱跳。后来才发现,不是传感器坏了,而是我压根没搞懂数据采集的底层逻辑。
这一章,咱们就把ADC采样、I2C、SPI、UART这几个基本功彻底讲透。你想想看,传感器要么输出模拟电压,要么输出数字信号。模拟信号靠ADC,数字信号靠通信协议。就这么简单。
2.1 ADC采样:把模拟世界“翻译”成数字
传感器感知的是物理量——光强、温度、距离。这些物理量在电路中表现为连续的电压或电流。但单片机只认0和1。ADC(模数转换器)就是干这个翻译活的。
2.1.1 关键参数:分辨率与采样率
分辨率决定了你能“看”多细。10位ADC能把0-3.3V分成1024份,12位就是4096份。我建议智能灯具至少用12位ADC。为什么?
举个例子:你要检测环境光从300lux到310lux的微弱变化。10位ADC可能只跳1-2个码值,12位ADC能跳4-5个码值。分辨率越高,调光越平滑。我在做一款高端台灯时,用了16位ADC,连月光透过窗帘的细微变化都能捕捉到。
| 分辨率 | 量化等级 | 典型应用 |
|---|---|---|
| 8位 | 256 | 简单开关控制 |
| 10位 | 1024 | 基础光照检测 |
| 12位 | 4096 | 智能灯具调光 |
| 16位 | 65536 | 高精度色温传感 |
采样率呢?灯具传感器变化很慢,几十赫兹就够了。但如果你要做手势识别,那采样率至少得100Hz以上。别盲目追求高采样率,功耗会跟着涨。
2.1.2 硬件滤波与软件滤波
ADC采样最怕噪声。我踩过一个坑:电源纹波直接耦合到ADC参考电压上,导致采样值周期性抖动。后来加了RC低通滤波,问题才解决。
硬件上,我习惯在ADC输入引脚前串联一个100Ω电阻,并联一个0.1μF电容到地。截止频率大概16kHz,够用了。
软件上,我推荐滑动平均滤波。取最近N次采样值求平均,N一般取4-8。代码很简单:
// 滑动平均滤波示例
#define FILTER_N 8
uint16_t adc_buffer[FILTER_N] = {0};
uint8_t adc_index = 0;
uint32_t adc_sum = 0;
uint16_t adc_filter(uint16_t new_value) {
adc_sum -= adc_buffer[adc_index];
adc_buffer[adc_index] = new_value;
adc_sum += new_value;
adc_index = (adc_index + 1) % FILTER_N;
return (uint16_t)(adc_sum / FILTER_N);
}
2.2 I2C通信:两根线搞定多个传感器
I2C是智能灯具里最常用的协议。一根SDA(数据线),一根SCL(时钟线),就能挂载几十个传感器。每个传感器有唯一地址,主机通过地址寻址。
我常用的光照传感器BH1750、温湿度传感器SHT30,都是I2C接口。接线简单,代码也不复杂。
2.2.1 I2C时序与读写流程
I2C的时序其实不复杂。起始条件:SCL高电平时,SDA从高变低。停止条件:SCL高电平时,SDA从低变高。每个字节后跟一个ACK应答位。
读传感器数据的一般流程:
- 主机发送起始条件
- 发送设备地址+写位(0)
- 等待从机ACK
- 发送寄存器地址
- 等待从机ACK
- 发送重复起始条件
- 发送设备地址+读位(1)
- 等待从机ACK
- 读取数据字节,主机发送ACK
- 读取最后一个字节,主机发送NACK
- 发送停止条件
嗯,这里要注意:很多新手会忘记重复起始条件。没有它,从机不知道你要切换读写方向。
// I2C读取BH1750光照值示例
uint16_t bh1750_read_light(void) {
uint8_t data[2] = {0};
// 先写寄存器地址
i2c_start();
i2c_write_byte(0x23 << 1 | 0); // 地址+写
i2c_wait_ack();
i2c_write_byte(0x10); // 连续高分辨率模式
i2c_wait_ack();
i2c_stop();
delay_ms(180); // 等待测量完成
// 再读数据
i2c_start();
i2c_write_byte(0x23 << 1 | 1); // 地址+读
i2c_wait_ack();
data[0] = i2c_read_byte();
i2c_send_ack();
data[1] = i2c_read_byte();
i2c_send_nack();
i2c_stop();
return (data[0] << 8) | data[1];
}
2.3 SPI通信:高速传输的不二之选
SPI比I2C快得多。四根线:SCK(时钟)、MOSI(主出从入)、MISO(主入从出)、CS(片选)。每个从机独占一根CS线,所以接线比I2C多。
什么时候用SPI?当传感器数据量大、更新快的时候。比如一些高精度距离传感器、陀螺仪,我习惯用SPI。智能灯具里,如果你要接摄像头做人体检测,那必须上SPI。
2.3.1 SPI模式与配置
SPI有四种模式,由时钟极性(CPOL)和时钟相位(CPHA)决定。大多数传感器用模式0(CPOL=0,CPHA=0)或模式3(CPOL=1,CPHA=1)。
我建议你养成一个习惯:每次用新传感器,先看数据手册里的时序图。别想当然用默认模式。我曾经因为模式没配对,读回来的数据全是0xFF,折腾了半天。
| 模式 | CPOL | CPHA | 数据采样边沿 |
|---|---|---|---|
| 0 | 0 | 0 | 上升沿采样 |
| 1 | 0 | 1 | 下降沿采样 |
| 2 | 1 | 0 | 下降沿采样 |
| 3 | 1 | 1 | 上升沿采样 |
// SPI读取传感器数据示例
uint16_t spi_read_sensor(uint8_t reg_addr) {
uint8_t tx_data[2] = {reg_addr | 0x80, 0x00}; // 读命令
uint8_t rx_data[2] = {0};
cs_low();
spi_transfer(tx_data, rx_data, 2); // 发送地址,接收数据
cs_high();
return (rx_data[0] << 8) | rx_data[1];
}
2.4 UART通信:简单可靠的“老伙计”
UART是串行通信的老前辈了。一根TX,一根RX,地线共地。不需要时钟线,双方约定好波特率就行。
智能灯具里,UART常用于和WiFi模块、蓝牙模块通信。比如ESP8266、HC-05,都是UART接口。我习惯用115200波特率,兼顾速度和稳定性。
2.4.1 UART数据帧与配置
一个UART数据帧包括:起始位(1位)、数据位(5-8位)、校验位(可选)、停止位(1-2位)。最常用的是8N1:8位数据,无校验,1位停止位。
配置时要注意:波特率误差不能超过2%。晶振频率不标准时,算出来的波特率可能有偏差。我遇到过用12MHz晶振配9600波特率,误差高达3.5%,通信时好时坏。
// UART发送传感器数据示例
void uart_send_sensor_data(uint16_t light, uint16_t temp) {
uint8_t buffer[6];
buffer[0] = 0xAA; // 帧头
buffer[1] = 0x55;
buffer[2] = (light >> 8) & 0xFF;
buffer[3] = light & 0xFF;
buffer[4] = (temp >> 8) & 0xFF;
buffer[5] = temp & 0xFF;
uart_send_bytes(buffer, 6);
}
2.5 协议选择:什么时候用哪个?
我总结了一个简单的选择原则:
- ADC: 传感器输出模拟电压时用。比如光敏电阻、模拟输出的红外传感器。
- I2C: 传感器数量多、数据量小、布线紧张时用。两根线挂一堆传感器,省IO口。
- SPI: 传感器数据量大、要求高速时用。比如陀螺仪、摄像头、高精度ADC芯片。
- UART: 和无线模块通信、或者传感器本身只支持UART时用。简单直接。
实际项目中,我经常混合使用。比如一个智能灯具里:光照传感器用I2C,距离传感器用SPI,蓝牙模块用UART,按键用ADC(分压读取多个按键)。各取所长,才是工程之道。
好,这一章就到这里。下一章咱们开始讲传感器校准——怎么把原始数据变成真正可用的物理量。到时候我会分享一个我调试了整整一周的校准算法,保证让你少走弯路。