当前位置:首页 > 通信技术 > 通信技术
[导读]在工业自动化项目中,嵌入式设备(如数据采集终端、边缘网关)与PLC直连的需求越来越普遍。标准Modbus RTU固然通用,但某些场景下需要更轻量、更安全或更适配私有数据的协议——比如批量读写非连续地址、携带时间戳、加密校验等。此时,用C语言在MCU上实现一套自定义的串口通信协议,是最灵活且可控的方案。本文以STM32与三菱FX系列PLC的RS485通信为例,展示从协议设计到代码实现的全过程。


在工业自动化项目中,嵌入式设备(如数据采集终端、边缘网关)与PLC直连的需求越来越普遍。标准Modbus RTU固然通用,但某些场景下需要更轻量、更安全或更适配私有数据的协议——比如批量读写非连续地址、携带时间戳、加密校验等。此时,用C语言在MCU上实现一套自定义的串口通信协议,是最灵活且可控的方案。本文以STM32与三菱FX系列PLC的RS485通信为例,展示从协议设计到代码实现的全过程。

一、协议帧结构设计

自定义协议的核心是定义明确的帧格式。参考工业现场常用结构,设计如下:

字节偏移 字段 长度 说明

0 帧头 2B 固定0xAA 0x55,用于同步

2 地址 1B PLC站号(1-247)

3 功能码 1B 0x01读寄存器,0x02写寄存器

4 数据长度 2B 数据域字节数(大端)

6 数据域 N B 寄存器地址+值等

6+N CRC16 2B Modbus式CRC校验,低字节在前

帧尾不加额外字节,接收方以超时判断帧结束。这种设计兼顾了检错能力和解析效率。

二、C语言实现:发送与接收

发送函数:组帧后通过串口DMA发送,避免阻塞。

typedef struct {

   uint8_t head[2];   // 0xAA 0x55

   uint8_t addr;

   uint8_t func;

   uint16_t len;      // 大端

   uint8_t data[256];

   uint16_t crc;

} ProtocolFrame;


void plc_send_frame(UART_HandleTypeDef *huart, ProtocolFrame *frame) {

   uint8_t buf[512];

   uint16_t idx = 0;

   buf[idx++] = 0xAA; buf[idx++] = 0x55;

   buf[idx++] = frame->addr;

   buf[idx++] = frame->func;

   buf[idx++] = (frame->len >> 8) & 0xFF;

   buf[idx++] = frame->len & 0xFF;

   memcpy(&buf[idx], frame->data, frame->len);

   idx += frame->len;

   uint16_t crc = calc_crc16(buf, idx);

   buf[idx++] = crc & 0xFF;

   buf[idx++] = (crc >> 8) & 0xFF;

   HAL_UART_Transmit_DMA(huart, buf, idx);

}

接收解析:采用状态机,逐字节处理,避免阻塞。

typedef enum { WAIT_HEAD1, WAIT_HEAD2, WAIT_ADDR, WAIT_FUNC,

              WAIT_LEN_H, WAIT_LEN_L, WAIT_DATA, WAIT_CRC_L, WAIT_CRC_H } RxState;


RxState state = WAIT_HEAD1;

uint8_t rx_buf[512];

uint16_t rx_idx = 0;

uint16_t expected_len = 0;


void plc_rx_byte(uint8_t byte) {

   switch (state) {

       case WAIT_HEAD1:

           if (byte == 0xAA) { state = WAIT_HEAD2; rx_idx = 0; }

           break;

       case WAIT_HEAD2:

           if (byte == 0x55) { state = WAIT_ADDR; rx_buf[rx_idx++] = byte; }

           else state = WAIT_HEAD1;

           break;

       case WAIT_ADDR:

           rx_buf[rx_idx++] = byte; state = WAIT_FUNC; break;

       case WAIT_FUNC:

           rx_buf[rx_idx++] = byte; state = WAIT_LEN_H; break;

       case WAIT_LEN_H:

           rx_buf[rx_idx++] = byte; expected_len = byte << 8; state = WAIT_LEN_L; break;

       case WAIT_LEN_L:

           rx_buf[rx_idx++] = byte; expected_len |= byte;

           if (expected_len == 0) state = WAIT_CRC_L;

           else state = WAIT_DATA; break;

       case WAIT_DATA:

           rx_buf[rx_idx++] = byte;

           if (rx_idx >= 6 + expected_len) state = WAIT_CRC_L;

           break;

       case WAIT_CRC_L:

           rx_buf[rx_idx++] = byte; state = WAIT_CRC_H; break;

       case WAIT_CRC_H:

           rx_buf[rx_idx++] = byte;

           // 校验CRC并处理完整帧

           if (validate_crc(rx_buf, rx_idx)) {

               process_frame(rx_buf, rx_idx);

           }

           state = WAIT_HEAD1;

           break;

   }

}

三、实际应用:读取PLC寄存器

嵌入式设备作为主站,向PLC发送读寄存器请求:

void plc_read_registers(uint8_t station, uint16_t start_addr, uint16_t count) {

   ProtocolFrame frame;

   frame.addr = station;

   frame.func = 0x01;

   frame.len = 4;

   frame.data[0] = (start_addr >> 8) & 0xFF;

   frame.data[1] = start_addr & 0xFF;

   frame.data[2] = (count >> 8) & 0xFF;

   frame.data[3] = count & 0xFF;

   plc_send_frame(&huart1, &frame);

}

PLC返回的帧中,数据域包含寄存器值。解析后即可得到温度、压力等现场数据。

四、工程注意事项

超时处理:接收状态机中,若字节间隔超过50ms,强制复位到WAIT_HEAD1,防止粘帧。

CRC校验:使用查表法计算CRC16,速度比逐位计算快10倍以上。

地址冲突:多设备共线时,每个设备必须分配唯一站号,且在发送后延时20ms以上再切换收发模式(RS485半双工)。

调试手段:先用USB转485模块配合串口助手验证帧格式,再接入PLC。

写在最后

用C语言开发PLC自定义通信协议,本质是“约定帧格式 + 状态机解析 + CRC校验”的工程实践。相比直接使用Modbus库,自定义协议的优势在于:可以灵活嵌入时间戳、批量操作非连续地址、甚至加入加密字段。对于资源受限的MCU,状态机解析方式比中断+队列更节省RAM。当你的嵌入式设备需要与PLC交换特殊格式数据时,这套框架可以作为起点——修改帧头、功能码和解析逻辑,就能适配不同品牌的PLC私有协议。掌握这项技能,意味着你不再受限于标准协议,而是可以根据业务需求“定制”通信语言。



本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除( 邮箱:macysun@21ic.com )。
关闭