当前位置:首页 > 工业控制 > 工业控制
[导读]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在十几台从站下基本能长期跑稳;剩下就是超时余量和离线退避,决定一台仪表坏了会不会把整条产线数据刷屏拖慢。



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