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通信的基本流程:

  1. 主机发送起始信号(SCL高电平时,SDA从高变低)
  2. 主机发送7位从机地址 + 1位读写位
  3. 从机应答(拉低SDA表示收到)
  4. 主机发送或接收数据(每8位后跟一个应答位)
  5. 主机发送停止信号(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总线串联起来。每种协议用在最合适的地方,系统才能稳定高效地运行。