3. 微控制器与传感器接口:GPIO、I2C、SPI、UART

各位同学,今天我们来聊聊嵌入式系统里最核心的话题——微控制器怎么跟传感器“说话”。说白了,就是接口协议。我做了十几年嵌入式,见过太多项目栽在接口选型上。选对了,事半功倍;选错了,调试到怀疑人生。

微控制器和传感器之间,本质上就是数字信号的传递。但不同的传感器,对速度、距离、引脚数量要求都不一样。所以就有了这四种主流接口:GPIO、I2C、SPI、UART。咱们一个一个来拆解。

核心观点:没有最好的接口,只有最合适的接口。选型时先看传感器手册,再看你的MCU资源。

MCU ↔ 传感器接口 GPIO 接口 I2C 总线协议 SPI 总线协议 UART 串口通信 简单、低速、引脚多 2线、多设备、中速 4线、高速、全双工 2线、异步、远距离

3.1 GPIO接口——最基础的数字握手

GPIO,全称通用输入输出口。你想想看,它其实就是一根引脚,要么输出高电平(3.3V或5V),要么输出低电平(0V)。或者反过来,读外部信号是高还是低。

很多简单的传感器,比如按键、LED、蜂鸣器、红外避障模块,都是用GPIO来通信的。我早期做智能小车项目时,四个电机驱动就是靠6个GPIO引脚控制的。那时候觉得真简单,后来才发现——简单的东西最容易出问题。

我的经验:GPIO驱动传感器时,一定要确认是推挽输出还是开漏输出。推挽输出能力强,适合驱动LED;开漏输出适合电平转换和多设备共享总线。搞混了,传感器就是不工作。

GPIO的典型用法:

  • 输出模式:控制LED亮灭、继电器通断、蜂鸣器发声
  • 输入模式:读取按键状态、检测传感器数字输出(如人体红外模块)
  • 中断模式:当引脚电平变化时,立即触发中断响应
// 以STM32为例,GPIO配置代码
GPIO_InitTypeDef GPIO_InitStruct = {0};

// 使能GPIO时钟
__HAL_RCC_GPIOA_CLK_ENABLE();

// 配置PA0为推挽输出,驱动LED
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

// 输出高电平,点亮LED
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET);

注意:GPIO直接驱动传感器时,要注意电流限制。大多数MCU的GPIO最大输出电流在20mA左右。如果传感器需要更大电流,必须加三极管或MOS管驱动。我曾经有个学生直接拿GPIO驱动一个5V继电器,结果烧了引脚——教训深刻。

3.2 I2C总线协议——两线制的大管家

I2C总线,只用两根线:SCL(时钟线)和SDA(数据线)。但它可以挂载几十个设备。怎么做到的?每个设备都有唯一的地址。主机发起通信,从机响应地址匹配的才参与。

我个人习惯把I2C比作一个班级:老师(主机)点名,学生(从机)听到自己学号就答到。老师控制节奏(时钟),学生轮流发言(数据)。

I2C的典型应用场景:

  • 温湿度传感器(如SHT30、AHT20)
  • 加速度计/陀螺仪(如MPU6050)
  • OLED显示屏(0.96寸SSD1306)
  • EEPROM存储器(如AT24C02)

I2C通信的基本流程:

  1. 主机发送起始信号(SCL高电平时,SDA从高变低)
  2. 主机发送7位从机地址 + 1位读写位
  3. 从机应答(ACK)
  4. 数据传输(每字节后跟ACK)
  5. 主机发送停止信号(SCL高电平时,SDA从低变高)
// I2C读取传感器数据示例(伪代码)
uint8_t i2c_read_byte(uint8_t dev_addr, uint8_t reg_addr) {
    uint8_t data = 0;
    
    // 发送起始信号
    i2c_start();
    
    // 发送设备地址 + 写位
    i2c_send_byte(dev_addr << 1 | 0);
    i2c_wait_ack();
    
    // 发送寄存器地址
    i2c_send_byte(reg_addr);
    i2c_wait_ack();
    
    // 重新发送起始信号(重复起始)
    i2c_start();
    
    // 发送设备地址 + 读位
    i2c_send_byte(dev_addr << 1 | 1);
    i2c_wait_ack();
    
    // 读取数据,发送NACK表示结束
    data = i2c_read_byte();
    i2c_send_nack();
    
    // 发送停止信号
    i2c_stop();
    
    return data;
}

避坑指南:我曾经在调试MPU6050时,死活读不到数据。折腾了两天,最后发现是上拉电阻没焊。I2C总线必须要有上拉电阻(通常4.7kΩ),否则信号拉不上去。另外,总线电容太大时,可以适当降低I2C时钟频率,比如从400kHz降到100kHz。

3.3 SPI总线协议——速度之王

SPI,全称串行外设接口。它比I2C快得多,最高能跑到几十MHz。但代价是引脚多:SCK(时钟)、MOSI(主机输出从机输入)、MISO(主机输入从机输出)、CS(片选)。

你想想看,SPI是全双工的——主机发数据的同时,从机也在发数据。这跟I2C的半双工不一样。所以SPI特别适合高速数据传输的场景,比如SD卡读写、LCD屏幕刷新、ADC采样。

SPI的四种模式(CPOL和CPHA组合):

模式 CPOL(时钟极性) CPHA(时钟相位) 说明
模式0 0 0 空闲时低电平,第一个边沿采样
模式1 0 1 空闲时低电平,第二个边沿采样
模式2 1 0 空闲时高电平,第一个边沿采样
模式3 1 1 空闲时高电平,第二个边沿采样

重要提醒:SPI的从机必须通过CS片选信号选中才能通信。如果CS没拉低,从机就“装死”。我见过有人把CS悬空,结果数据发出去石沉大海。另外,不同传感器的SPI模式可能不同,一定要看数据手册确认CPOL和CPHA。

// SPI发送接收示例(基于HAL库)
uint8_t spi_transfer(uint8_t data) {
    uint8_t rx_data = 0;
    HAL_SPI_TransmitReceive(&hspi1, &data, &rx_data, 1, HAL_MAX_DELAY);
    return rx_data;
}

// 读取SPI ADC数据
uint16_t read_adc_spi(uint8_t channel) {
    uint8_t tx_buf[2];
    uint8_t rx_buf[2];
    uint16_t result = 0;
    
    // 构造命令:起始位+通道号
    tx_buf[0] = 0x06 | (channel << 4);
    tx_buf[1] = 0x00;
    
    // CS拉低,选中从机
    HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET);
    
    // 传输数据
    HAL_SPI_TransmitReceive(&hspi1, tx_buf, rx_buf, 2, HAL_MAX_DELAY);
    
    // CS拉高,释放从机
    HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET);
    
    // 组合结果
    result = ((rx_buf[0] & 0x0F) << 8) | rx_buf[1];
    return result;
}

3.4 UART串口通信——最通用的异步接口

UART,通用异步收发器。它只需要两根线:TX(发送)和RX(接收)。没有时钟线,全靠双方约定好波特率来同步。比如9600、115200这些数字,就是每秒传输的位数。

UART的帧格式:起始位(1位)+ 数据位(5~9位,常用8位)+ 校验位(可选)+ 停止位(1~2位)。

我最早接触嵌入式就是从UART开始的。那时候用串口调试,printf打印数据,看着串口助手里的输出,感觉特别有成就感。直到后来做工业项目,才发现UART在长距离传输时抗干扰能力是个大问题。

UART的典型应用:

  • GPS模块(NMEA协议)
  • 蓝牙模块(HC-05、HC-06)
  • RS232/RS485转换(工业现场)
  • 调试日志输出(printf重定向)
// UART初始化配置(STM32)
UART_HandleTypeDef huart2;

void uart_init(void) {
    huart2.Instance = USART2;
    huart2.Init.BaudRate = 115200;
    huart2.Init.WordLength = UART_WORDLENGTH_8B;
    huart2.Init.StopBits = UART_STOPBITS_1;
    huart2.Init.Parity = UART_PARITY_NONE;
    huart2.Init.Mode = UART_MODE_TX_RX;
    huart2.Init.HwFlowCtl = UART_HWCONTROL_NONE;
    huart2.Init.OverSampling = UART_OVERSAMPLING_16;
    HAL_UART_Init(&huart2);
}

// 发送字符串
void uart_send_string(char *str) {
    HAL_UART_Transmit(&huart2, (uint8_t*)str, strlen(str), HAL_MAX_DELAY);
}

// 接收单字节(阻塞)
uint8_t uart_receive_byte(void) {
    uint8_t data;
    HAL_UART_Receive(&huart2, &data, 1, HAL_MAX_DELAY);
    return data;
}

我的经验:UART通信最容易出问题的是波特率不匹配。两边必须设置成一样的,差一点就会乱码。另外,如果通信距离超过1米,建议用RS232电平(±12V)或者RS485差分信号。直接拿TTL电平(3.3V/5V)拉长线,数据会错得一塌糊涂。

3.5 四种接口对比与选型建议

好了,四种接口都讲完了。咱们做个对比总结,方便你以后选型:

特性 GPIO I2C SPI UART
引脚数 1根/信号 2根 4根 2根
速度 取决于软件 100kHz~3.4MHz 最高几十MHz 最高几Mbps
通信方式 单线 半双工 全双工 全双工
多设备支持 差(每设备需独立引脚) 好(地址区分) 好(片选区分) 差(点对点)
距离 板级 板级(<1m) 板级(<1m) 可远(加驱动)
典型传感器 按键、LED 温湿度、IMU SD卡、LCD GPS、蓝牙

选型时,我一般这么考虑:

  • 传感器数量少、速度要求低 → GPIO,简单粗暴
  • 多个传感器、引脚有限 → I2C,两线制搞定一切
  • 需要高速传输大量数据 → SPI,速度就是王道
  • 远距离或与PC通信 → UART,成熟稳定

最后说一句:接口协议只是工具,真正重要的是理解传感器的工作方式。拿到一个新传感器,先看数据手册,搞清楚它用什么接口、什么时序、什么电压。然后对着手册写代码,一次不行就两次。我当年也是这样过来的,没什么捷径。

可可精品资料库