用C语言实现工业RS485轮询编程:多设备通信的无冲突调度方案
RS485是半双工总线,同一时刻只能有一方发。多设备通信出问题的根因基本就一条:主站没做好“一问一答”的串行调度,或者从站响应还没收完就切回发送把下一帧怼出去。 正经做法是主站状态机驱动:发完一台再发下一台,超时重试、离线退避,从站永远不主动抢总线。
一、物理层先别埋坑
手拉手拓扑,禁止星型分支
总线首尾各120Ω终端电阻,中间节点不加
DE/RE短接一个GPIO控制:高=发,低=收
发送前拉高DE,发完等TC标志再拉低,否则末字节停止位被截断、CRC失败
帧间静默≥3.5字符时间(9600下约3.6ms),RTU靠静默判帧边界
二、设备表与状态定义
typedef struct {
uint8_t addr;
uint8_t func;
uint16_t reg;
uint16_t cnt;
uint8_t *dst;
uint8_t fail;
uint8_t online;
uint32_t next_ms; // 离线退避用
} slave_t;
slave_t g_sl[8];
typedef enum {
ST_IDLE, ST_TX, ST_WAIT, ST_PARSE
} poll_st;
poll_st st = ST_IDLE;
int cur = 0;
uint32_t tx_t0;
三、DE切换发送
void rs485_dir_tx(uint8_t on){
if(on) HAL_GPIO_WritePin(DE_PORT,DE_PIN,GPIO_PIN_SET);
else HAL_GPIO_WritePin(DE_PORT,DE_PIN,GPIO_PIN_RESET);
}
void rs485_send(uint8_t*frame,int len){
rs485_dir_tx(1);
HAL_UART_Transmit(&huart2, frame, len, 100);
while(__HAL_UART_GET_FLAG(&huart2,UART_FLAG_TC)==RESET);
delay_us(200); // 转向稳定
rs485_dir_tx(0); // 切回接收
}
DMA发送也行,但完成中断里切DE要留字符余量,新手先用阻塞+TC最稳。
四、主站轮询状态机
核心原则:前一台没收完,不下一条指令。
void poll_task(void){
static uint8_t req[16];
uint32_t now = sys_ms();
switch(st){
case ST_IDLE:
if(cur>=8){ cur=0; break; }
if(!g_sl[cur].online && now < g_sl[cur].next_ms) {
cur++; break; // 离线退避中跳过
}
build_req(g_sl[cur], req); // 地址+功能码+寄存器+CRC
rs485_send(req, frame_len(req));
st = ST_RX_WAIT;
tx_t0 = now;
rx_len = 0;
break;
case ST_WAIT:
if(rx_frame_done()){ // 收到完整响应帧
if(crc_ok(rx_buf,rx_len) && match_addr(rx_buf,g_sl[cur].addr)){
parse_resp(g_sl[cur], rx_buf);
g_sl[cur].fail = 0;
g_sl[cur].online = 1;
cur++; st = ST_IDLE;
}else{
st = ST_RETRY;
}
}else if(now - tx_t0 > RESP_TO_MS){ // 超时
st = ST_RETRY;
}
break;
case ST_RETRY:
if(++g_sl[cur].fail >= 3){
g_sl[cur].online = 0;
g_sl[cur].next_ms = now + 10000; // 10s探活
}
cur++; st = ST_IDLE;
break;
}
}
主循环每ms调一次即可,别在UART中断里做协议解析——中断只收字节进环形缓冲。
五、超时怎么定
超时不是越小越灵敏,太小会把正常慢响应判离线,引发重试风暴把总线堵死。
经验值:
9600bps:读2寄存器超时100~150ms
19200:80ms
115200:30ms
网关后挂多从站:超时要叠排队时间
计算口径:发送帧时间 + 从站处理时间 + 响应帧时间 + 余量
六、写命令单独处理
写单寄存器(06)、写多线圈(15)超时重试有副作用——从站可能已执行但响应丢了,重发会变成“阀开两次”。
做法:
写操作进独立写队列,优先级高于读轮询
超时后写后回读确认,不盲目重发
或保证写内容幂等(写目标值而非“+1”)
void write_queue_push(uint8_t addr,uint16_t reg,uint16_t val){
wq[wq_in].addr=addr; wq[wq_in].reg=reg; wq[wq_in].val=val;
wq_in=(wq_in+1)&(WQ_MASK);
}
主循环先排空写队列再跑读轮询。
七、分级轮询(可选)
所有点同周期太浪费:
状态/故障:200ms
模拟量:1s
参数/电能累计:5s
从站表里加period_ms,now>=next_ms才轮询该点。
八、落坑清单
多从站同时发请求→总线冲突乱码
DE提前拉低→末字节截断CRC错
帧间静默不够3.5字符→从站把两帧拼一帧
超时设20ms→USB转485必误判
单站掉线不跳过→整条轮询卡死等超时
中断里解析Modbus→主循环逻辑乱
写命令盲目重试→执行器重复动作
广播地址0当轮询→所有从站回帧撞车
终端电阻每个节点都加→阻抗失配
地址冲突没改→多从站抢答
九、落地顺序
总线手拉手、首尾120Ω、DE/RE单GPIO控
串口参数跟从站对齐(9600/8/N/1起步)
发帧前拉DE,TC后切回接收
状态机:发→等→解析/超时→下一台
超时重试≤3次,失败标离线+退避
写命令单独队列,写后回读
关键数据分级周期轮询
抓包工具验帧间隔≥3.5字符
RS485多设备通信稳不稳,不看你会不会组帧,看轮询状态机清不清楚:总线永远只有主站在点名,从站只答不抢;发完一台等响应,超时别死等,掉线别拖总线。DE切换时机和3.5字符静默是两个最易被忽略的硬指标,这俩对了,Modbus RTU在十几台从站下基本能长期跑稳;剩下就是超时余量和离线退避,决定一台仪表坏了会不会把整条产线数据刷屏拖慢。





