3、传感器接口协议:数字接口(I2C、SPI、UART)、模拟接口(ADC)、脉冲接口

做嵌入式这么多年,我接触过的传感器少说也有上百种。每次拿到一个新传感器,第一件事就是看它的接口类型。为什么?因为接口决定了你怎么接、怎么读、怎么处理数据。说白了,接口就是传感器和MCU之间的「沟通语言」。选对了,事半功倍;选错了,调试到怀疑人生。

今天咱们就把传感器接口这件事彻底聊透。我按三大类来讲:数字接口、模拟接口、脉冲接口。每种接口我都会结合自己的项目经验,把那些坑和技巧都抖出来。

3.1 数字接口:I2C、SPI、UART

数字接口是目前最主流的传感器接口。传感器内部自带ADC和寄存器,MCU直接通过协议读取数字值。好处很明显——抗干扰强、连线少、精度高。

3.1.1 I2C接口

I2C只用两根线:SCL(时钟)和SDA(数据)。支持多主机多从机,每个设备有唯一地址。我最早用I2C是在一个温湿度采集项目上,当时选了SHT30传感器。

典型接线:

MCU (主)          SHT30 (从)
SCL  ————————  SCL
SDA  ————————  SDA
VCC  ————————  VCC (3.3V)
GND  ————————  GND

代码示例(读取温度):

// 伪代码 - I2C读取SHT30温度
i2c_start();
i2c_write(0x44 << 1);  // 设备地址 + 写位
i2c_write(0x2C);        // 命令高位
i2c_write(0x06);        // 命令低位
i2c_stop();

delay_ms(10);           // 等待转换

i2c_start();
i2c_write(0x44 << 1 | 0x01);  // 地址 + 读位
data_high = i2c_read(ACK);
data_low  = i2c_read(NACK);
i2c_stop();

temp = (data_high << 8) | data_low;
temp_c = -45 + 175 * temp / 65535.0;
⚠️ 避坑指南: 我曾经在一个项目里被I2C的「时钟拉伸」坑惨了。有些从机在忙时会拉低SCL,主机必须等待。如果MCU的I2C外设不支持时钟拉伸,通信就会卡死。解决办法:要么换支持时钟拉伸的MCU,要么用软件模拟I2C。
💡 我的习惯: 多传感器共用一个I2C总线时,我会在每根线上加4.7kΩ上拉电阻。如果总线长度超过20cm,上拉电阻要适当减小到2.2kΩ。这是经验值,你试试就知道。

3.1.2 SPI接口

SPI比I2C快得多,全双工通信。四根线:SCLK(时钟)、MOSI(主出从入)、MISO(主入从出)、CS(片选)。适合高速传感器,比如加速度计、陀螺仪、ADC芯片。

典型接线(多从机):

MCU         传感器1    传感器2
SCLK ———— SCLK ———— SCLK
MOSI ———— MOSI ———— MOSI
MISO ———— MISO ———— MISO
CS1  ———— CS
CS2  —————————————— CS

代码示例(读取MPU6050加速度):

// 伪代码 - SPI读取MPU6050
cs_low();  // 拉低片选

spi_transfer(0x80 | 0x3B);  // 发送读命令 + 寄存器地址
accel_x_h = spi_transfer(0x00);
accel_x_l = spi_transfer(0x00);
accel_y_h = spi_transfer(0x00);
accel_y_l = spi_transfer(0x00);
accel_z_h = spi_transfer(0x00);
accel_z_l = spi_transfer(0x00);

cs_high(); // 拉高片选

accel_x = (accel_x_h << 8) | accel_x_l;
🔑 关键点: SPI的时钟极性(CPOL)和时钟相位(CPHA)必须和传感器匹配。我见过太多人在这上面翻车——读出来的数据全是0xFF或者0x00。查手册里的时序图,确认是模式0、1、2还是3。

3.1.3 UART接口

UART就是串口,异步通信。两根线:TX和RX。很多GPS模块、激光雷达、一些工业传感器都用UART。优点是简单,缺点是只能点对点。

典型接线:

MCU         传感器
TX   ————  RX
RX   ————  TX
VCC  ————  VCC
GND  ————  GND

代码示例(解析GPS NMEA语句):

// 伪代码 - 解析$GPGGA语句
char buffer[100];
uart_read_line(buffer, sizeof(buffer));

if (strstr(buffer, "$GPGGA")) {
    char *token = strtok(buffer, ",");
    token = strtok(NULL, ",");  // 时间
    token = strtok(NULL, ",");  // 纬度
    char *lat = token;
    token = strtok(NULL, ",");  // 南北纬
    // ... 继续解析
}
💡 经验之谈: UART通信最容易出问题的是波特率不匹配。我习惯用115200,但如果线太长(超过2米),我会降到9600或19200。另外,记得加校验位,我一般用无校验+1停止位,也就是8N1。

3.2 模拟接口:ADC

模拟接口是最原始的传感器接口。传感器输出的是连续变化的电压信号,MCU通过ADC(模数转换器)把它变成数字值。比如电位器、光敏电阻、热敏电阻、一些老式的压力传感器。

典型电路(分压式):

VCC (3.3V)
   │
   ┌┴┐
   │R│ 10kΩ 固定电阻
   └┬┘
   │
   ├───── MCU ADC引脚
   │
   ┌┴┐
   │ │ 光敏电阻 (随光照变化)
   └┬┘
   │
  GND

代码示例(读取ADC值):

// 伪代码 - 读取ADC通道0
adc_init(ADC_CH0);  // 初始化,设置参考电压3.3V

uint16_t raw = adc_read(ADC_CH0);  // 返回0-4095 (12位ADC)

// 换算成电压
float voltage = raw * 3.3 / 4095.0;

// 如果是光敏电阻,查表或公式换算成光照度
float lux = voltage_to_lux(voltage);
⚠️ 避坑指南: 我曾经在一个环境监测项目里,ADC读数一直跳,波动有几十个LSB。查了半天,发现是参考电压引脚没加滤波电容。后来在VREF引脚对地加了一个100nF电容,读数立马稳了。记住:ADC对电源噪声极其敏感。
🔑 关键点:
  • 分辨率:8位、10位、12位、16位。位数越高,精度越高,但转换速度越慢。
  • 参考电压:内部参考(通常1.1V或2.5V)还是外部参考?外部参考更稳定。
  • 采样率:根据信号变化速度选择。温度变化慢,10Hz就够了;音频信号需要44.1kHz。

3.3 脉冲接口

脉冲接口输出的是方波信号,MCU通过测量脉冲的频率、宽度或个数来获取数据。常见的有三种:频率输出、PWM输出、脉冲计数。

3.3.1 频率输出型传感器

传感器输出频率随被测量变化。比如一些流量计、风速计。MCU用定时器捕获频率。

代码示例(测量频率):

// 伪代码 - 使用输入捕获测量频率
timer_input_capture_init(TIM2, CH1);  // 配置定时器2通道1为输入捕获

uint32_t freq = 0;
while(1) {
    uint32_t period = get_capture_value();  // 获取捕获到的周期值
    freq = timer_clock / period;            // 频率 = 时钟频率 / 周期
    printf("频率: %d Hz\n", freq);
    delay_ms(1000);
}

3.3.2 PWM输出型传感器

传感器输出PWM波,占空比代表测量值。比如一些超声波测距模块、舵机角度传感器。MCU测量高电平时间即可。

代码示例(测量PWM占空比):

// 伪代码 - 测量PWM高电平时间
uint32_t high_time = pulseIn(PIN, HIGH);  // 测量高电平脉宽(us)
uint32_t period = pulseIn(PIN, HIGH) + pulseIn(PIN, LOW);

float duty = (float)high_time / period * 100.0;
float distance = high_time / 58.0;  // 超声波模块: 58us/cm

3.3.3 脉冲计数型传感器

传感器每检测到一个事件就输出一个脉冲。比如旋转编码器、水表、电表。MCU用外部中断或定时器计数。

代码示例(脉冲计数):

// 伪代码 - 外部中断计数
volatile uint32_t pulse_count = 0;

void EXTI0_IRQHandler() {
    if (EXTI_GetITStatus(EXTI_Line0) != RESET) {
        pulse_count++;
        EXTI_ClearITPendingBit(EXTI_Line0);
    }
}

// 主循环中计算转速
while(1) {
    uint32_t count = pulse_count;
    pulse_count = 0;
    float rpm = count * 60.0 / 1.0;  // 假设1秒采样一次
    printf("转速: %.2f RPM\n", rpm);
    delay_ms(1000);
}
💡 我的习惯: 脉冲计数一定要做去抖处理。机械开关或编码器在接触瞬间会产生多个脉冲。我一般用软件延时10-20ms,或者用硬件RC滤波。曾经在一个电机测速项目里,没去抖直接计数,转速读数直接翻了三倍。

3.4 接口选型对比

接口类型 线数 速度 距离 抗干扰 典型传感器
I2C 2 100k-3.4MHz ~1m 温湿度、气压、IMU
SPI 4+ 10MHz-80MHz ~1m 加速度计、ADC、显示屏
UART 2 9600-921600bps ~10m GPS、激光雷达、工业传感器
ADC 1 取决于ADC ~1m 光敏、热敏、电位器
脉冲 1 取决于MCU ~10m 编码器、流量计、超声波
🔑 选型建议:
  • 追求简单、传感器数量多 → I2C
  • 追求速度、数据量大 → SPI
  • 远距离、点对点 → UART(加RS485更远)
  • 成本敏感、老式传感器 → ADC
  • 测转速、流量、位置 → 脉冲接口
传感器接口协议知识体系 传感器接口 数字接口 模拟接口 脉冲接口 I2C SPI UART ADC(模数转换) 分压电路 / 参考电压 频率输出 PWM输出 脉冲计数 选型原则:速度、距离、抗干扰、成本、传感器数量 实战要点:上拉电阻、滤波电容、去抖处理、协议匹配

好了,传感器接口这部分就聊到这儿。每种接口都有自己的脾气,摸透了就好用。我个人的建议是:新手先从I2C和UART入手,这两个最友好。等玩熟了再碰SPI和ADC。脉冲接口看着简单,但去抖和时序处理需要点功夫。

记住一点:没有最好的接口,只有最合适的接口。根据你的项目需求——速度、距离、成本、开发周期——去选,准没错。


专注资料整理