4、通信协议基础:UART协议详解与实战、I2C协议详解与实战、SPI协议详解与实战
各位同学,咱们今天聊点硬核的。嵌入式系统里,芯片和芯片之间怎么说话?靠的就是通信协议。UART、I2C、SPI,这三个是嵌入式工程师吃饭的家伙。我做了这么多年生产测试,发现很多产品出问题,根源都在通信协议没搞透。今天咱们就把它们掰开揉碎了讲清楚。
4.1 UART协议:最朴素的“点对点”聊天
UART,全称通用异步收发传输器。说白了,就是两根线(TX和RX)让两个设备互相发数据。它不依赖时钟线,全靠双方约定好“波特率”来同步。
核心要点:
- 异步通信:没有时钟线,靠起始位和停止位来对齐数据。
- 数据帧格式:起始位(1位) + 数据位(5~8位) + 校验位(可选) + 停止位(1~2位)。
- 波特率:每秒传输的比特数。常见的有9600、115200。两边必须一致,否则数据全乱套。
避坑指南: 我曾经在产线上遇到一批POS机,通信时好时坏。查了半天,发现是晶振精度不够,导致实际波特率偏差超过了3%。UART对时钟偏差很敏感,超过3%就容易丢数据。所以,量产时一定要用高精度晶振,或者做波特率自适应校准。
实战代码片段(STM32 HAL库):
// 初始化UART1,波特率115200
UART_HandleTypeDef huart1;
huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
HAL_UART_Init(&huart1);
// 发送数据
uint8_t data[] = "Hello POS!";
HAL_UART_Transmit(&huart1, data, sizeof(data), 1000);
// 接收数据(中断方式)
uint8_t rx_buffer[10];
HAL_UART_Receive_IT(&huart1, rx_buffer, 10);
我的小技巧: 调试UART时,我习惯先用逻辑分析仪抓波形。看一眼起始位和停止位的位置,就能判断波特率对不对。比用示波器方便多了。
4.2 I2C协议:多设备“总线”上的优雅对话
I2C,Inter-Integrated Circuit。它只用两根线(SCL时钟线、SDA数据线),就能挂载多个设备。每个设备有唯一的地址,通过“寻址”来通信。
为什么用I2C? 你想想看,一个主控芯片要跟多个传感器、EEPROM、显示屏通信,如果每个都用UART,那得多少根线?I2C两根线搞定,节省IO口。
协议细节:
- 主从结构:主设备发起通信,从设备响应。
- 起始条件:SCL高电平时,SDA从高变低。
- 停止条件:SCL高电平时,SDA从低变高。
- 应答机制:每发送一个字节,接收方必须拉低SDA表示应答(ACK),否则主设备会认为通信失败。
- 速率模式:标准模式100kHz,快速模式400kHz,高速模式3.4MHz。
注意: I2C总线上的上拉电阻很关键。阻值太小,功耗大;阻值太大,信号上升沿变慢,通信速率上不去。我一般选4.7kΩ,总线电容大时用2.2kΩ。这个在产线测试时一定要测一下波形,确保信号质量。
实战代码片段(读取温度传感器):
// 假设温度传感器地址为0x48
#define TEMP_SENSOR_ADDR 0x48
// 读取温度寄存器
uint8_t reg_addr = 0x00;
uint8_t temp_data[2];
HAL_I2C_Master_Transmit(&hi2c1, TEMP_SENSOR_ADDR, ®_addr, 1, 100);
HAL_I2C_Master_Receive(&hi2c1, TEMP_SENSOR_ADDR, temp_data, 2, 100);
// 计算温度值(假设12位精度)
int16_t raw_temp = (temp_data[0] << 8) | temp_data[1];
float temperature = raw_temp * 0.0625;
避坑指南: 我在项目中遇到过I2C总线“死锁”的问题。就是SDA线一直被拉低,再也拉不起来了。原因是某个从设备异常,没有释放总线。解决办法:在初始化时,强制给SCL发9个时钟脉冲,让从设备复位。这个技巧在产线测试脚本里一定要加上。
4.3 SPI协议:速度之王,全双工通信
SPI,Serial Peripheral Interface。它比I2C快得多,支持全双工(同时收发)。但需要4根线:SCLK(时钟)、MOSI(主出从入)、MISO(主入从出)、CS(片选)。
SPI的特点:
- 全双工:发送和接收同时进行。
- 主从结构:一个主设备可以带多个从设备,每个从设备一个CS片选。
- 无应答机制:SPI没有ACK,主设备只管发,从设备只管收。可靠性靠上层协议保证。
- 四种模式:由时钟极性(CPOL)和时钟相位(CPHA)组合决定。CPOL决定空闲时时钟电平,CPHA决定数据在哪个边沿采样。
| 模式 | CPOL | CPHA | 数据采样边沿 |
|---|---|---|---|
| 模式0 | 0 | 0 | 上升沿 |
| 模式1 | 0 | 1 | 下降沿 |
| 模式2 | 1 | 0 | 下降沿 |
| 模式3 | 1 | 1 | 上升沿 |
我的经验: 调试SPI时,最容易踩的坑就是模式不匹配。主设备和从设备必须工作在同一个模式下。我曾经因为看错数据手册,把模式0和模式1搞混了,结果读出来的数据全是0xFF。后来用逻辑分析仪一看,采样边沿完全反了。所以,我建议你在写驱动前,先用逻辑分析仪确认一下从设备的时序。
实战代码片段(读写Flash存储器):
// SPI初始化(模式0,1MHz)
SPI_HandleTypeDef hspi1;
hspi1.Instance = SPI1;
hspi1.Init.Mode = SPI_MODE_MASTER;
hspi1.Init.Direction = SPI_DIRECTION_2LINES;
hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; // CPOL=0
hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; // CPHA=0
hspi1.Init.NSS = SPI_NSS_SOFT;
hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_16; // 16MHz/16=1MHz
HAL_SPI_Init(&hspi1);
// 读取Flash ID
uint8_t cmd = 0x9F; // 读ID指令
uint8_t rx_data[3];
HAL_GPIO_WritePin(FLASH_CS_GPIO_Port, FLASH_CS_Pin, GPIO_PIN_RESET); // 拉低片选
HAL_SPI_TransmitReceive(&hspi1, &cmd, rx_data, 1, 100); // 发送指令
HAL_SPI_TransmitReceive(&hspi1, dummy, &rx_data[0], 1, 100); // 读第一个字节
HAL_SPI_TransmitReceive(&hspi1, dummy, &rx_data[1], 1, 100); // 读第二个字节
HAL_SPI_TransmitReceive(&hspi1, dummy, &rx_data[2], 1, 100); // 读第三个字节
HAL_GPIO_WritePin(FLASH_CS_GPIO_Port, FLASH_CS_Pin, GPIO_PIN_SET); // 拉高片选
避坑指南: SPI的片选信号一定要干净。如果CS上有毛刺,从设备可能会误触发。我建议在CS引脚上加一个小电容(比如100pF)去耦。另外,如果SPI速率很高(比如10MHz以上),走线要尽量短,避免信号反射。
4.4 三种协议对比与选型建议
好了,三种协议都讲完了。咱们来总结一下,什么时候该用哪个?
| 特性 | UART | I2C | SPI |
|---|---|---|---|
| 线数 | 2根(TX/RX) | 2根(SCL/SDA) | 4根(SCLK/MOSI/MISO/CS) |
| 通信方式 | 异步 | 同步 | 同步 |
| 最大速率 | 几Mbps | 几Mbps | 几十Mbps |
| 多设备支持 | 点对点 | 多设备(地址寻址) | 多设备(片选) |
| 全双工 | 是 | 半双工 | 全双工 |
| 典型应用 | 调试口、GPS模块 | 传感器、EEPROM | Flash、显示屏、SD卡 |
我的选型建议:
- 如果只是两个设备简单通信,比如调试打印,用UART最省事。
- 如果总线上要挂多个低速设备,比如温度、湿度、气压传感器,用I2C最省IO。
- 如果对速度要求高,比如刷屏、读写大容量Flash,用SPI。
最后说一句: 在POS机生产测试中,这三种协议都会用到。UART用来和PC通信打印测试结果,I2C用来读取电池管理芯片和触摸屏控制器,SPI用来读写字库芯片和固件升级。掌握好它们,你的嵌入式之路就稳了。