4、通信协议解析:UART通信基础、I2C总线在货道控制中的应用、Modbus协议详解、CAN总线在大型售货机中的应用
通信协议,说白了就是设备之间沟通的「语言」。你想想看,主控板要告诉货道电机「转几圈」,传感器要把「缺货了」这个信号传回来,没有一套统一的规矩,大家各说各话,系统根本跑不起来。
我做售货机项目这些年,见过不少因为通信问题导致的奇葩故障。有的机器偶尔吐货卡半路,有的干脆死机不响应。查到最后,十有八九是协议没处理好。今天我就把这四种最常见的通信协议掰开揉碎了讲给你听。
4.1 UART通信基础
UART,全称是通用异步收发传输器。这玩意儿历史很久了,但直到今天,它依然是嵌入式系统里最基础、最常用的串行通信方式。
为什么叫「异步」?因为它不需要单独的时钟线。发送方和接收方各自用自己的时钟,只要约定好相同的波特率,就能正常通信。我刚开始学的时候总觉得这不太靠谱,万一两边时钟漂移了怎么办?后来做多了才发现,只要晶振精度够,短距离通信完全没问题。
UART通信的关键参数:
- 波特率:每秒传输的符号数。常见的有9600、115200。我个人习惯用115200,速度快,而且大部分MCU都支持。
- 数据位:通常是8位。早期有5位、6位、7位,现在基本统一用8位了。
- 停止位:1位或2位。我一般用1位,够用。
- 校验位:奇校验、偶校验或无校验。货道控制这种短距离场景,我通常不用校验,省一个bit的传输时间。
在售货机里,UART最常见的用途就是调试输出。比如主控板通过串口打印「货道1电机启动」、「货道3传感器触发」这类信息。你拿一根USB转TTL线连上电脑,打开串口助手就能看到。
// 一个简单的UART初始化代码(STM32平台)
void UART_Init(void) {
// 使能USART1时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
// 配置GPIO:TX为推挽输出,RX为浮空输入
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; // TX
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
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);
// 配置USART参数:115200波特率,8位数据,1位停止,无校验
USART_InitTypeDef USART_InitStructure;
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_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
USART_Init(USART1, &USART_InitStructure);
// 使能USART
USART_Cmd(USART1, ENABLE);
}
小技巧:调试时别用printf直接打印,那玩意儿太占资源。我习惯自己写一个轻量级的串口发送函数,只发关键数据,效率高很多。
4.2 I2C总线在货道控制中的应用
I2C总线,飞利浦公司发明的,两根线搞定一切:一根时钟线SCL,一根数据线SDA。它最大的好处就是节省IO口。你想想看,一个售货机可能有几十个货道,每个货道都要检测电机状态、传感器信号,如果用独立IO,主控芯片的引脚根本不够用。
我在项目中是怎么用的?每个货道模块上放一个I2C从设备,比如IO扩展芯片或专用的电机驱动芯片。主控通过I2C总线轮询每个从设备,读取状态或发送指令。
嗯,这里要注意:I2C总线上每个设备都有一个唯一的地址。7位地址模式最多挂127个设备,10位地址模式更多。但实际使用中,总线电容会限制设备数量。我记得有一次项目,挂了16个从设备,总线波形就开始变形了。后来加了总线缓冲器才解决。
I2C通信的基本流程:
- 主机发送起始信号(SCL高电平时,SDA从高变低)
- 主机发送7位从机地址 + 1位读写位
- 从机应答(拉低SDA表示收到)
- 主机发送或接收数据(每8位后跟一个应答位)
- 主机发送停止信号(SCL高电平时,SDA从低变高)
// I2C读取从设备寄存器的示例代码
uint8_t I2C_ReadRegister(uint8_t devAddr, uint8_t regAddr) {
uint8_t data = 0;
// 发送起始信号
I2C_Start();
// 发送设备地址 + 写操作
I2C_SendByte(devAddr << 1 | 0);
I2C_WaitAck();
// 发送寄存器地址
I2C_SendByte(regAddr);
I2C_WaitAck();
// 重新发送起始信号(重复起始)
I2C_Start();
// 发送设备地址 + 读操作
I2C_SendByte(devAddr << 1 | 1);
I2C_WaitAck();
// 读取数据,发送非应答
data = I2C_ReadByte();
I2C_SendNack();
// 发送停止信号
I2C_Stop();
return data;
}
避坑指南:我曾经遇到过I2C总线锁死的问题。原因是某个从设备异常,一直拉低SDA线不放。后来我在软件里加了超时复位机制,如果总线空闲超过一定时间,就强制复位所有从设备。这个坑踩过一次就记住了。
4.3 Modbus协议详解
Modbus协议,工业自动化领域的「普通话」。它最初是Modicon公司为PLC通信设计的,后来成了事实上的行业标准。在售货机行业,Modbus常用于主控板和各个功能模块之间的通信,比如制冷系统、加热模块、支付模块等。
Modbus有两种传输模式:RTU模式和ASCII模式。RTU模式用二进制传输,效率高;ASCII模式用十六进制字符传输,可读性好但效率低。我一般用RTU模式,毕竟售货机对实时性有要求。
Modbus RTU的报文格式:
| 字段 | 长度 | 说明 |
|---|---|---|
| 地址码 | 1字节 | 从机地址,范围1-247 |
| 功能码 | 1字节 | 指示操作类型,如03读保持寄存器、06写单个寄存器 |
| 数据区 | N字节 | 具体的数据内容 |
| CRC校验 | 2字节 | 循环冗余校验,低字节在前 |
举个例子,主控要读取货道1的温度传感器值。发送报文可能是:01 03 00 00 00 01 84 0A。其中01是从机地址,03是读保持寄存器功能码,00 00是寄存器起始地址,00 01是读取数量,84 0A是CRC校验。
从机回复:01 03 02 00 1E 79 8C。01是从机地址,03是功能码,02是数据字节数,00 1E是温度值(30摄氏度),79 8C是CRC校验。
个人经验:Modbus的CRC校验一定要算对。我见过太多人因为CRC算法写错,导致通信时好时坏。建议直接用查表法,速度快,代码也简洁。网上有现成的CRC-16-Modbus查表,直接拿来用就行。
4.4 CAN总线在大型售货机中的应用
CAN总线,控制器局域网,最早是博世公司为汽车设计的。它最大的特点是可靠性高、实时性好、抗干扰能力强。大型售货机,比如那种能放几百种商品的自动贩卖中心,用CAN总线再合适不过了。
为什么大型售货机要用CAN?你想想看,几十个货道模块分布在机器各个角落,走线距离可能超过10米。如果用UART或I2C,信号衰减和干扰问题会让你头疼死。CAN总线是差分信号传输,两根线(CAN_H和CAN_L)绞在一起,抗干扰能力极强。
CAN总线的另一个好处是多主通信。任何节点都可以主动发送数据,不需要主机轮询。比如某个货道缺货了,它自己就能发一条消息告诉主控。这在实时性要求高的场景下特别有用。
CAN报文帧格式(标准帧):
- 帧起始:1位显性位,表示报文开始
- 仲裁场:11位标识符 + 1位RTR位。标识符越小,优先级越高
- 控制场:6位,包含数据长度码(DLC)
- 数据场:0-8字节数据
- CRC场:15位CRC + 1位CRC界定符
- 应答场:2位,接收节点应答
- 帧结束:7位隐性位
// CAN报文发送示例(基于STM32 bxCAN)
void CAN_SendMessage(uint32_t id, uint8_t *data, uint8_t len) {
CanTxMsg TxMessage;
TxMessage.StdId = id; // 标准标识符
TxMessage.ExtId = 0; // 不使用扩展标识符
TxMessage.IDE = CAN_Id_Standard; // 标准帧
TxMessage.RTR = CAN_RTR_Data; // 数据帧
TxMessage.DLC = len; // 数据长度
for(uint8_t i = 0; i < len; i++) {
TxMessage.Data[i] = data[i];
}
// 发送报文
CAN_Transmit(CAN1, &TxMessage);
}
避坑指南:我曾经在一个项目中,CAN总线莫名其妙丢包。查了两天才发现,是终端电阻没接对。CAN总线两端必须各接一个120欧姆的终端电阻,否则信号反射会导致通信错误。这个细节很多人容易忽略。
总结一下这四种协议的选择思路:
- UART:适合点对点通信,调试和简单控制场景
- I2C:适合板级短距离多设备通信,节省IO口
- Modbus:适合工业级设备间通信,标准化程度高
- CAN:适合远距离、多节点、高可靠性的复杂系统
实际项目中,我经常混合使用这些协议。比如主控板和支付模块用UART通信,和货道驱动板用I2C,和制冷系统用Modbus,整个系统内部再用CAN总线串联起来。每种协议用在最合适的地方,系统才能稳定高效地运行。