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);
}
我的小技巧: 采样值突变超过阈值时,直接丢弃。比如光照值突然从500跳到5000,大概率是干扰。我管这叫“野值剔除”,配合滑动平均效果很好。

2.2 I2C通信:两根线搞定多个传感器

I2C是智能灯具里最常用的协议。一根SDA(数据线),一根SCL(时钟线),就能挂载几十个传感器。每个传感器有唯一地址,主机通过地址寻址。

我常用的光照传感器BH1750、温湿度传感器SHT30,都是I2C接口。接线简单,代码也不复杂。

2.2.1 I2C时序与读写流程

I2C的时序其实不复杂。起始条件:SCL高电平时,SDA从高变低。停止条件:SCL高电平时,SDA从低变高。每个字节后跟一个ACK应答位。

读传感器数据的一般流程:

  1. 主机发送起始条件
  2. 发送设备地址+写位(0)
  3. 等待从机ACK
  4. 发送寄存器地址
  5. 等待从机ACK
  6. 发送重复起始条件
  7. 发送设备地址+读位(1)
  8. 等待从机ACK
  9. 读取数据字节,主机发送ACK
  10. 读取最后一个字节,主机发送NACK
  11. 发送停止条件

嗯,这里要注意:很多新手会忘记重复起始条件。没有它,从机不知道你要切换读写方向。

// 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];
}
我曾经踩过的坑: I2C上拉电阻没选对。电阻太大,信号上升沿太慢;电阻太小,功耗高。4.7kΩ是通用值,但总线电容大时要用2.2kΩ。我建议你根据总线长度和从机数量算一下,别偷懒。

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];
}
我的经验: SPI时钟频率别拉满。标称20MHz的传感器,我一般用10MHz。留点余量,信号完整性更好。特别是线比较长的时候,降频能省很多麻烦。

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);
}
注意: UART没有总线仲裁机制,只能点对点通信。如果你想挂多个设备,得用RS485或者加多路复用器。别想着把三个UART设备的TX都接在一起,会烧端口的。

2.5 协议选择:什么时候用哪个?

我总结了一个简单的选择原则:

  • ADC: 传感器输出模拟电压时用。比如光敏电阻、模拟输出的红外传感器。
  • I2C: 传感器数量多、数据量小、布线紧张时用。两根线挂一堆传感器,省IO口。
  • SPI: 传感器数据量大、要求高速时用。比如陀螺仪、摄像头、高精度ADC芯片。
  • UART: 和无线模块通信、或者传感器本身只支持UART时用。简单直接。

实际项目中,我经常混合使用。比如一个智能灯具里:光照传感器用I2C,距离传感器用SPI,蓝牙模块用UART,按键用ADC(分压读取多个按键)。各取所长,才是工程之道。

核心要点: 数据采集是传感器融合的“地基”。ADC采样要稳,通信协议要准。别急着写融合算法,先把数据读准了再说。数据不准,算法再花哨也是白搭。

好,这一章就到这里。下一章咱们开始讲传感器校准——怎么把原始数据变成真正可用的物理量。到时候我会分享一个我调试了整整一周的校准算法,保证让你少走弯路。