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;
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;
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, ","); // 南北纬
// ... 继续解析
}
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);
- 分辨率: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);
}
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和UART入手,这两个最友好。等玩熟了再碰SPI和ADC。脉冲接口看着简单,但去抖和时序处理需要点功夫。
记住一点:没有最好的接口,只有最合适的接口。根据你的项目需求——速度、距离、成本、开发周期——去选,准没错。