用C语言开发PLC自定义通信协议:实现嵌入式设备与工业PLC的直连
在工业自动化项目中,嵌入式设备(如数据采集终端、边缘网关)与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私有协议。掌握这项技能,意味着你不再受限于标准协议,而是可以根据业务需求“定制”通信语言。





