1. 嵌入式通信协议概述
在嵌入式系统开发中,通信协议的选择直接影响着系统的稳定性、可靠性和开发效率。作为一名嵌入式工程师,我经常需要在项目中根据不同的应用场景选择合适的通信方式。本文将详细介绍六种最常用的通信协议:UART、RS232、RS485、I2C、SPI和CAN,并基于STM32F103平台提供可直接移植的寄存器级代码实现。
这些协议各有特点:UART简单易用但距离有限;RS232/RS485在UART基础上定义了电气特性,适合工业环境;I2C凭借简单的两线制在传感器领域广泛应用;SPI则以高速全双工特性在存储设备中占据优势;而CAN总线则因其强大的抗干扰能力成为汽车电子的标配。
2. UART协议详解
2.1 基础原理与特性
UART(Universal Asynchronous Receiver Transmitter)是一种通用的异步串行通信协议,采用两条数据线实现全双工通信。其特点包括:
- 异步通信:无需时钟线,依靠预定义的波特率同步
- 全双工:可同时收发数据
- 起始-停止位架构:每个数据帧以起始位开始,停止位结束
在实际项目中,我常用UART实现以下功能:
- 调试信息输出
- 与GPS/蓝牙模块通信
- 简单的主从设备控制
2.2 数据帧格式解析
UART的数据帧包含以下几个部分:
- 起始位:1位低电平
- 数据位:5-9位(通常8位)
- 校验位(可选):奇校验/偶校验
- 停止位:1-2位高电平
波特率计算示例:
对于STM32F103(PCLK=72MHz),要实现115200bps:
BRR = 72000000 / 115200 = 625
实际波特率 = 72000000 / 625 = 115200bps
2.3 硬件流控机制
UART支持硬件流控(RTS/CTS),可防止数据溢出。当接收缓冲区快满时,接收方会拉低CTS信号通知发送方暂停发送。
提示:在长距离或干扰较大环境中,建议启用硬件流控以避免数据丢失。
2.4 STM32寄存器级实现
以下是完整的UART初始化代码,包含发送/接收函数:
c复制// USART1 初始化(115200, 8N1)
void USART1_Init(void) {
// 1. 使能时钟
RCC->APB2ENR |= RCC_APB2ENR_USART1EN | RCC_APB2ENR_IOPAEN;
// 2. 配置GPIO: PA9(TX)复用推挽输出,PA10(RX)浮空输入
GPIOA->CRH &= ~(GPIO_CRH_CNF9 | GPIO_CRH_MODE9 | GPIO_CRH_CNF10 | GPIO_CRH_MODE10);
GPIOA->CRH |= GPIO_CRH_CNF9_1 | GPIO_CRH_MODE9; // TX复用推挽50MHz
GPIOA->CRH |= GPIO_CRH_CNF10_0; // RX浮空输入
// 3. 计算波特率(PCLK2=72MHz)
USART1->BRR = 72000000 / 115200;
// 4. 使能USART,发送和接收
USART1->CR1 = USART_CR1_UE | USART_CR1_TE | USART_CR1_RE;
}
// 发送一个字节(轮询)
void USART1_SendByte(uint8_t data) {
while(!(USART1->SR & USART_SR_TXE));
USART1->DR = data;
}
// 接收一个字节(轮询)
uint8_t USART1_ReceiveByte(void) {
while(!(USART1->SR & USART_SR_RXNE));
return (uint8_t)USART1->DR;
}
2.5 常见问题排查
-
无数据收发:
- 检查时钟使能
- 验证波特率设置
- 确认TX/RX引脚配置正确
-
数据错位:
- 检查双方波特率是否一致
- 验证时钟源频率
- 检查是否有电磁干扰
-
中断不触发:
- 确认NVIC已配置
- 检查中断标志位是否清除
3. RS232与RS485协议
3.1 RS232详解
RS232在UART基础上定义了电气特性:
- 电压电平:+3V~+15V(逻辑0),-3V~-15V(逻辑1)
- 典型接口:DB9
- 传输距离:约15米
硬件连接要点:
- 必须使用电平转换芯片(如MAX3232)
- 基本三线制连接:TX、RX、GND
3.2 RS485详解
RS485采用差分信号传输:
- 电压差:+2V~+6V(逻辑1),-2V~-6V(逻辑0)
- 半双工通信
- 传输距离:可达1200米
硬件设计要点:
- 终端匹配:总线两端需接120Ω电阻
- 偏置电阻:确保空闲时电平确定
- 方向控制:DE/RE引脚控制收发切换
3.3 STM32实现代码
c复制// RS485初始化(使用PA8作为方向控制)
void RS485_Init(void) {
USART1_Init(); // 先初始化UART
// 配置方向控制引脚
GPIOA->CRH |= GPIO_CRH_MODE8; // PA8推挽输出
GPIOA->BRR = GPIO_BRR_BR8; // 初始为接收模式
}
// RS485发送数据
void RS485_SendBytes(uint8_t *buf, uint16_t len) {
GPIOA->BSRR = GPIO_BSRR_BS8; // 切换到发送模式
for(uint16_t i = 0; i < len; i++) {
while(!(USART1->SR & USART_SR_TXE));
USART1->DR = buf[i];
}
while(!(USART1->SR & USART_SR_TC)); // 等待发送完成
delay_us(10); // 延时确保最后一位发送完毕
GPIOA->BRR = GPIO_BRR_BR8; // 切换回接收模式
}
3.4 工程实践要点
-
RS485网络拓扑:
- 采用总线型结构
- 避免星型连接
- 节点间距均匀分布
-
抗干扰设计:
- 使用双绞线
- 添加TVS二极管防护
- 做好接地处理
-
协议设计建议:
- 添加帧头帧尾
- 包含CRC校验
- 实现超时重传机制
4. I2C总线协议
4.1 协议特点
- 两线制:SCL(时钟)、SDA(数据)
- 半双工通信
- 多主机支持
- 7位地址寻址
4.2 通信时序解析
- 起始条件:SCL高时SDA由高变低
- 停止条件:SCL高时SDA由低变高
- 数据有效性:SCL高电平时SDA稳定
- 应答机制:每字节后接收方拉低SDA
4.3 软件模拟实现
c复制// I2C起始信号
void I2C_Start(void) {
SDA_HIGH();
SCL_HIGH();
I2C_Delay();
SDA_LOW();
I2C_Delay();
SCL_LOW();
}
// I2C发送一个字节
uint8_t I2C_WriteByte(uint8_t data) {
for(uint8_t i = 0; i < 8; i++) {
(data & 0x80) ? SDA_HIGH() : SDA_LOW();
data <<= 1;
SCL_HIGH();
I2C_Delay();
SCL_LOW();
I2C_Delay();
}
// 读取应答
SDA_HIGH();
SCL_HIGH();
uint8_t ack = GPIO_ReadInputDataBit(I2C_PORT, SDA_PIN);
SCL_LOW();
return ack;
}
4.4 硬件I2C配置
c复制void I2C1_Init(void) {
// 1. 使能时钟
RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
// 2. 配置引脚为复用开漏
GPIOB->CRL |= GPIO_CRL_CNF6_1 | GPIO_CRL_CNF7_1;
// 3. 配置I2C时钟
I2C1->CR2 = 36; // APB1时钟36MHz
I2C1->CCR = 180; // 100kHz标准模式
I2C1->TRISE = 37; // 最大上升时间
// 4. 使能I2C
I2C1->CR1 |= I2C_CR1_PE;
}
4.5 常见问题解决
-
从机无应答:
- 检查从机地址
- 确认从机电源
- 检查上拉电阻
-
数据错误:
- 调整时序延时
- 检查总线竞争
- 验证时钟频率
-
总线死锁:
- 实现超时恢复机制
- 添加看门狗
- 必要时硬件复位
5. SPI总线协议
5.1 协议特点
- 全双工同步通信
- 主从架构
- 四种工作模式
- 高速传输(可达几十MHz)
5.2 工作模式选择
| 模式 | CPOL | CPHA | 时钟极性 | 采样边沿 |
|---|---|---|---|---|
| 0 | 0 | 0 | 低电平 | 上升沿 |
| 1 | 0 | 1 | 低电平 | 下降沿 |
| 2 | 1 | 0 | 高电平 | 下降沿 |
| 3 | 1 | 1 | 高电平 | 上升沿 |
5.3 STM32硬件SPI实现
c复制void SPI1_Init(void) {
// 1. 使能时钟
RCC->APB2ENR |= RCC_APB2ENR_SPI1EN;
// 2. 配置引脚
GPIOA->CRL |= GPIO_CRL_CNF5_1 | GPIO_CRL_CNF7_1; // SCK,MOSI复用推挽
GPIOA->CRL |= GPIO_CRL_CNF6_0; // MISO浮空输入
// 3. 配置SPI
SPI1->CR1 = SPI_CR1_MSTR | SPI_CR1_BR_0; // 主机模式,fPCLK/2
SPI1->CR1 |= SPI_CR1_SSM | SPI_CR1_SSI; // 软件NSS管理
SPI1->CR1 |= SPI_CR1_SPE; // 使能SPI
}
uint8_t SPI1_Transfer(uint8_t data) {
while(!(SPI1->SR & SPI_SR_TXE));
SPI1->DR = data;
while(!(SPI1->SR & SPI_SR_RXNE));
return (uint8_t)SPI1->DR;
}
5.4 性能优化技巧
- 使用DMA传输大数据
- 合理设置时钟分频
- 优化片选控制时序
- 利用双缓冲特性
6. CAN总线协议
6.1 协议特点
- 多主机架构
- 非破坏性仲裁
- 强大的错误处理
- 差分信号传输
6.2 帧类型与格式
- 数据帧:携带实际数据
- 远程帧:请求数据
- 错误帧:报告错误
- 过载帧:请求延迟
6.3 STM32 CAN配置
c复制void CAN1_Init(void) {
// 1. 使能时钟
RCC->APB1ENR |= RCC_APB1ENR_CAN1EN;
// 2. 进入初始化模式
CAN1->MCR |= CAN_MCR_INRQ;
while(!(CAN1->MSR & CAN_MSR_INAK));
// 3. 配置波特率(250kbps)
CAN1->BTR = (4-1)<<16 | (3-1)<<20 | (8-1);
// 4. 配置过滤器
CAN1->FMR |= CAN_FMR_FINIT;
CAN1->sFilterRegister[0].FR1 = 0;
CAN1->sFilterRegister[0].FR2 = 0;
CAN1->FMR &= ~CAN_FMR_FINIT;
// 5. 退出初始化模式
CAN1->MCR &= ~CAN_MCR_INRQ;
while(CAN1->MSR & CAN_MSR_INAK);
}
6.4 网络设计建议
- 终端电阻匹配
- 合理设置ID优先级
- 实现错误统计与处理
- 考虑总线负载均衡
7. 协议对比与选型指南
| 特性 | UART | RS232 | RS485 | I2C | SPI | CAN |
|---|---|---|---|---|---|---|
| 通信方式 | 异步 | 异步 | 异步 | 同步 | 同步 | 异步 |
| 拓扑结构 | 点对点 | 点对点 | 总线 | 总线 | 主从 | 总线 |
| 最大速率 | 1Mbps | 115k | 10M | 3.4M | 50M | 1M |
| 传输距离 | 几米 | 15m | 1200m | 几米 | 几米 | 1000m |
| 典型应用 | 调试 | 工业 | 工业 | 传感器 | 存储器 | 汽车 |
选型建议:
- 短距离调试:UART
- 工业环境:RS485
- 传感器网络:I2C
- 高速数据传输:SPI
- 复杂工业控制:CAN
在实际项目中,我通常会根据传输距离、速率要求、节点数量和抗干扰需求来综合选择通信协议。例如,在汽车电子项目中,CAN总线是首选;而在消费电子产品中,I2C和SPI更为常见。理解每种协议的特点和适用场景,能够帮助我们在设计初期做出更合理的架构决策。
