第一次接触嵌入式开发时,我被各种通信协议搞得晕头转向。USART、RS232、RS485、IIC、SPI...这些名词就像天书一样。直到有一次做工业传感器项目,因为选错通信协议导致整个系统不稳定,我才真正明白协议选型的重要性。
通信协议本质上是设备间的"语言规则"。就像人类交流需要共同语言一样,电子设备之间传输数据也需要约定好怎么传、传多快、遇到干扰怎么办。不同协议就像不同方言,各有适用的场景。
串行通信是当前的主流选择,它只用1-2根数据线逐位传输。你可能好奇:并行通信不是更快吗?确实,8根线同时传输理论上比单根线快8倍。但实际项目中,我遇到太多并行通信的坑——线缆成本高、布线复杂、长距离信号不同步。有次用并行接口传视频信号,超过3米就出现雪花纹,换成串行HDMI线立刻解决问题。
同步和异步是另一个关键区分。同步通信像军训走正步,所有动作跟着指挥员(时钟信号)的哨声来;异步通信则像自由讨论,每个人按照自己的节奏发言,但需要明确的开始和结束标志。我在做智能家居网关时,传感器节点用异步通信可以省去时钟线,大幅降低布线复杂度。
全双工、半双工的区别就像电话和对讲机。全双工的USART可以同时收发,适合需要实时交互的调试终端;半双工的RS485虽然要轮流说话,但在工业现场抗干扰能力更强。记得有次用错全双工协议连接PLC,数据冲突导致设备异常停机,损失半天产能。
通信速率的选择也很有讲究。不是越快越好——高速意味着更大的干扰和功耗。我给农业大棚做监测系统时,开始追求115200bps的波特率,结果发现9600bps完全够用,还更省电稳定。关键是要算清楚:温度数据每分钟更新一次,每个读数才几个字节,根本不需要高速传输。
在我的工具箱里,USART就像多功能螺丝刀——不是最专业的,但几乎什么活都能干。它支持全双工异步通信,还能切换同步模式,这种灵活性让它成为调试和设备交互的首选。
USART最实用的功能是打印调试信息。通过简单的串口转USB模块,就能在电脑上看到单片机内部的运行状态。我习惯在每个项目都保留一个USART接口,遇到问题时printf打印变量值,比调试器还方便。记得有次排查电机控制bug,就是靠USART输出的实时PWM占空比发现了问题。
配置USART要注意几个关键参数:
STM32的USART外设用起来很顺手。以STM32F103为例,初始化流程如下:
c复制// 1. 使能时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
// 2. 配置GPIO
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; // TX
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; // RX
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 3. 配置USART
USART_InitStructure.USART_BaudRate = 115200;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_Init(USART1, &USART_InitStructure);
// 4. 使能USART
USART_Cmd(USART1, ENABLE);
小数波特率发生器是USART的亮点功能。传统波特率设置会有舍入误差,而STM32的USART_BRR寄存器支持小数分频。计算波特率的公式是:
code复制USARTDIV = fCK / (16 * BaudRate)
其中fCK是USART时钟频率,把计算结果整数部分写入BRR[15:4],小数部分乘以16后写入BRR[3:0]。
USART的中断处理也很实用。除了常规的发送完成(TXE)、接收完成(RXNE)中断,还有帧错误(FE)、噪声错误(NE)等异常中断。我在产品中会启用错误中断,当通信异常时能快速恢复:
c复制USART_ITConfig(USART1, USART_IT_RXNE | USART_IT_ERR, ENABLE);
void USART1_IRQHandler(void)
{
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
uint8_t data = USART_ReceiveData(USART1);
// 处理接收数据
}
if(USART_GetITStatus(USART1, USART_IT_FE) != RESET)
{
USART_ClearITPendingBit(USART1, USART_IT_FE);
// 帧错误处理
}
}
刚入行时,我总把RS232和RS485搞混。直到有次在工厂调试,老工程师告诉我:"232是点对点,485可以组网"。这句话让我茅塞顿开,后来在多个工业项目中验证了这个经验。
RS232虽然老了,但在设备调试中依然不可替代。它的-3V~-15V/+3V~+15V电平标准比TTL抗干扰强很多。有次用TTL串口连接数控机床,电机一启动数据就乱,换成RS232立刻稳定。不过要注意,RS232的传输距离通常不超过15米,速率建议在19200bps以下。
RS232的硬件连接很简单,只需要三条线:TXD、RXD和GND。但实际布线时我吃过亏——没有将两端GND相连,导致共模干扰。正确的接法应该是:
code复制设备1 TXD —— 设备2 RXD
设备1 RXD —— 设备2 TXD
设备1 GND —— 设备2 GND
RS485才是工业现场的主力。它的差分传输方式抗干扰能力极强,我在变频器旁边测试,RS485在115200bps速率下传输100米毫无压力。RS485支持总线拓扑,最多可以挂接32个设备,特别适合分布式采集系统。
RS485硬件设计要注意终端电阻匹配。总线两端的设备需要接120Ω终端电阻,消除信号反射。我曾遇到一个诡异问题:白天通信正常,晚上就丢数据。后来发现是温度变化导致信号反射,加上终端电阻后解决。典型RS485电路如下:
code复制 120Ω
A ────────/\/\/───────┐
│
B ────────/\/\/───────┘
120Ω
RS485的软件实现需要处理半双工切换。发送前要使能驱动器,发送完成后切回接收状态。我常用的控制流程是:
c复制void RS485_Send(uint8_t *data, uint16_t len)
{
DE_GPIO_Port->BSRR = DE_Pin; // 使能发送
HAL_UART_Transmit(&huart1, data, len, 100);
while(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_TC) == RESET); // 等待发送完成
DE_GPIO_Port->BRR = DE_Pin; // 切回接收
}
在Modbus RTU等工业协议中,RS485的稳定性优势更加明显。我参与过的污水处理厂项目,所有传感器和执行器都通过RS485组网,中央控制器轮询各节点,系统运行五年几乎没出过通信故障。
IIC总线让我又爱又恨。爱它的简洁——两根线搞定多个设备;恨它的脆弱——线稍长就通信失败。经过多次教训,我现在严格遵循IIC的最佳实践:传输距离不超过30cm,速率控制在100kHz以内。
IIC的地址机制很巧妙。7位地址格式可以支持112个设备(16个保留地址),10位地址扩展更多。但实际项目中,我发现挂接超过5个设备就容易出问题。有次做智能家居中控,接了8个IIC设备,结果频繁丢数据。后来改用IIC多路复用器TCA9548A才解决。
IIC的起始/停止条件很有特点。SCL高电平时SDA的跳变表示起始/停止,这个设计让IIC可以与其他总线共存。我在混合使用IIC和SPI时,就利用这个特性实现了总线共享。典型IIC起始停止信号代码:
c复制void I2C_Start(void)
{
SDA_HIGH();
SCL_HIGH();
Delay_us(5);
SDA_LOW(); // 起始条件
Delay_us(5);
SCL_LOW();
}
void I2C_Stop(void)
{
SDA_LOW();
SCL_HIGH();
Delay_us(5);
SDA_HIGH(); // 停止条件
Delay_us(5);
}
软件模拟IIC虽然灵活,但实际项目我推荐用硬件IIC。STM32的硬件IIC支持时钟拉伸、多主机仲裁等高级功能。初始化时要注意配置正确的时序参数:
c复制I2C_InitTypeDef I2C_InitStruct;
I2C_InitStruct.I2C_ClockSpeed = 100000; // 100kHz
I2C_InitStruct.I2C_Mode = I2C_Mode_I2C;
I2C_InitStruct.I2C_DutyCycle = I2C_DutyCycle_2;
I2C_InitStruct.I2C_OwnAddress1 = 0x00; // 主机模式设为0
I2C_InitStruct.I2C_Ack = I2C_Ack_Enable;
I2C_InitStruct.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
I2C_Init(I2C1, &I2C_InitStruct);
IIC的应答机制是保证可靠性的关键。每次传输8位数据后,接收方必须发送ACK信号。我在调试OLED屏幕时,就因为忽略ACK检查导致显示异常。正确的数据收发流程应该严格检查ACK:
c复制uint8_t I2C_WriteByte(uint8_t data)
{
for(int i=0; i<8; i++) {
if(data & 0x80) SDA_HIGH();
else SDA_LOW();
SCL_HIGH();
Delay_us(5);
SCL_LOW();
data <<= 1;
}
SDA_HIGH(); // 释放SDA线
SCL_HIGH();
if(SDA_READ()) { // 检查ACK
SCL_LOW();
return 1; // NACK
}
SCL_LOW();
return 0; // ACK
}
当项目需要高速数据传输时,SPI总是我的首选。它的全双工同步特性让传输速率轻松突破10Mbps。去年做工业相机项目,CMOS传感器通过SPI输出图像数据,速率达到18MHz依然稳定。
SPI的四种模式常让人困惑。简单记法:模式0和3是上升沿采样,区别是时钟空闲状态;模式1和2是下降沿采样。大多数SPI设备支持模式0,但有些特殊器件(如某些ADC)需要特定模式。有次用错模式读取温度传感器,得到的数据全是乱的,排查半天才发现模式不匹配。
SPI的硬件连接要注意片选信号管理。每个从设备需要独立的CS线,主控IO口不够时可以配合译码器使用。我常用的SPI初始化配置如下(STM32标准外设库):
c复制SPI_InitTypeDef SPI_InitStruct;
SPI_InitStruct.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
SPI_InitStruct.SPI_Mode = SPI_Mode_Master;
SPI_InitStruct.SPI_DataSize = SPI_DataSize_8b;
SPI_InitStruct.SPI_CPOL = SPI_CPOL_Low; // 模式0
SPI_InitStruct.SPI_CPHA = SPI_CPHA_1Edge;
SPI_InitStruct.SPI_NSS = SPI_NSS_Soft; // 软件控制NSS
SPI_InitStruct.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4; // fPCLK/4
SPI_InitStruct.SPI_FirstBit = SPI_FirstBit_MSB;
SPI_Init(SPI1, &SPI_InitStruct);
SPI_Cmd(SPI1, ENABLE);
SPI的DMA传输能大幅提升效率。在处理大容量Flash或LCD屏时,DMA可以解放CPU资源。我在开发TFT驱动时,使用DMA传输图像数据,帧率提升3倍:
c复制// 配置SPI TX DMA
DMA_InitTypeDef DMA_InitStruct;
DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&(SPI1->DR);
DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)image_buffer;
DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralDST;
DMA_InitStruct.DMA_BufferSize = sizeof(image_buffer);
DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStruct.DMA_Mode = DMA_Mode_Normal;
DMA_Init(DMA1_Channel3, &DMA_InitStruct);
// 启动DMA传输
SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Tx, ENABLE);
DMA_Cmd(DMA1_Channel3, ENABLE);
while(DMA_GetFlagStatus(DMA1_FLAG_TC3) == RESET); // 等待传输完成
SPI的菊花链连接方式可以节省片选线。这种方式下数据从一个设备传递到下一个设备,适合ADC多通道采集等场景。但要注意时序延迟会累积,链路过长会影响速度。我最多成功串联过4个ADC芯片,速率保持在1MHz以上。
软件模拟SPI在引脚受限时很有用。通过任意IO口模拟时钟和数据线,可以实现SPI通信。我在STM8等资源有限的MCU上常用这种方法:
c复制void SoftSPI_WriteByte(uint8_t data)
{
for(int i=0; i<8; i++) {
MOSI_PIN = (data & 0x80) ? 1 : 0;
SCK_PIN = 1;
data <<= 1;
SCK_PIN = 0;
}
}
uint8_t SoftSPI_ReadByte(void)
{
uint8_t data = 0;
for(int i=0; i<8; i++) {
data <<= 1;
SCK_PIN = 1;
if(MISO_PIN) data |= 0x01;
SCK_PIN = 0;
}
return data;
}