当前位置:首页 > 工业控制 > 工业控制
[导读]32台现场设备挂网关,最忌“来一个请求发一个包”。Modbus TCP本身靠MBAP事务号支持流水请求,但若后端是RS485 RTU从站,总线半双工会直接打架;即便后端也是TCP从站,不管理事务ID也会串包。 正确架构是:以太网侧收包解析→请求入队→调度器按从站分发→事务映射表回包。


32台现场设备挂网关,最忌“来一个请求发一个包”。Modbus TCP本身靠MBAP事务号支持流水请求,但若后端是RS485 RTU从站,总线半双工会直接打架;即便后端也是TCP从站,不管理事务ID也会串包。 正确架构是:以太网侧收包解析→请求入队→调度器按从站分发→事务映射表回包。

一、MBAP帧先切干净

Modbus TCP没有CRC,靠TCP可靠传输,帧边界全看MBAP长度域。头7字节:事务ID(2)+协议ID(2)+长度(2)+单元ID(1)。

typedef struct {

   uint16_t trans_id;

   uint16_t proto_id;

   uint16_t len;

   uint8_t  unit_id;

   uint8_t  pdu[256];

   uint8_t  pdu_len;

} mbap_frame_t;


int mbap_parse(uint8_t*buf,int n,mbap_frame_t*f){

   if(n<8) return -1;

   f->trans_id = (buf[0]<<8)|buf[1];

   f->proto_id = (buf[2]<<8)|buf[3];

   if(f->proto_id!=0) return -2;

   f->len = (buf[4]<<8)|buf[5];

   f->unit_id = buf[6];

   f->pdu_len = f->len-1;

   memcpy(f->pdu, buf+7, f->pdu_len);

   return 0;

}

收包必须按长度域凑整帧,TCP会拆包粘包,别recv一次就当一帧。

二、32台设备调度表

每台从站登记IP/端口或RTU站号、功能码白名单、轮询周期、超时:

typedef struct {

   uint8_t unit_id;

   uint8_t mode;        // 0=RTU后端 1=TCP从站

   uint32_t ip;

   uint16_t port;

   uint16_t poll_ms;

   uint16_t timeout_ms;

   uint8_t retry;

   uint8_t online;

} dev_entry_t;


dev_entry_t dev[32];

上位机请求里的Unit ID就是路由键,网关查表知道该转发到哪条总线或哪个socket。

三、请求队列解耦收发

以太网接收任务和总线发送任务不能写在一起,中间加FIFO:

typedef struct {

   uint8_t sock;        // 来源客户端

   uint16_t trans_id;   // 回包匹配

   uint8_t unit_id;

   uint8_t pdu[256];

   uint8_t pdu_len;

   uint32_t tick;

} req_t;


req_t qbuf[16];

uint8_t qhead,qtail,qcnt;


int req_enqueue(req_t*r){

   if(qcnt>=16) return -1;     // 队满丢请求,别积压

   qbuf[qtail]=*r;

   qtail=(qtail+1)%16; qcnt++;

   return 0;

}

队满直接回异常码0x0B(从站无响应)比卡死强。

四、RTU后端:总线互斥单发

若32台实际是RS485上的站号1~32,串口同一时刻只能跑一个事务,调度器串行取队列:

void rtu_schedule_10ms(void){

   static uint8_t busy=0;

   if(busy) return;                 // 上一帧没回完

   if(qcnt==0) return;

   req_t*r=&qbuf[qhead];

   dev_entry_t*d=&dev[r->unit_id-1];


   rtu_build_and_send(d->unit_id,r->pdu,r->pdu_len);

   busy=1;

   r->tick=sys_tick();

   g_pending=r;                     // 等串口ISR回包

}


void rtu_rx_done(uint8_t*pdu,int len){

   req_t*r=g_pending;

   mbap_reply(r->sock,r->trans_id,r->unit_id,pdu,len);

   qhead=(qhead+1)%16; qcnt--;

   g_pending=NULL;

}

超时在定时器里判,超过timeout_ms没回就重发,重发次数耗尽标记online=0并回异常。

五、TCP后端:事务ID映射

若32台本身是Modbus TCP设备,网关给每台维护socket(或同网段多Unit ID复用连接),发之前分配事务号:

uint16_t g_tid;

typedef struct { uint16_t tid; uint8_t sock; } tid_map_t;

tid_map_t tid_map[32];


void fwd_to_tcp(dev_entry_t*d,req_t*r){

   uint16_t tid=++g_tid;

   tid_map[tid&0x1F]=(tid_map_t){tid,r->sock};

   uint8_t out[270];

   out[0]=tid>>8; out[1]=tid&0xff;

   out[2]=0;out[3]=0;

   out[5]=r->pdu_len+1;

   out[6]=d->unit_id;

   memcpy(out+7,r->pdu,r->pdu_len);

   tcp_send(d->sock_fd,out,r->pdu_len+7);

}

收到响应按事务ID查回原客户端,响应顺序不必和请求一致。

六、超时与重传

工业现场别一超时就断连接:

RTU后端:重发1~2次,间隔按波特率

TCP后端:超时重发同事务ID,旧响应迟到直接丢弃

连续N次失败→设备离线,周期拉长为慢轮询

队满、socket断开、MBAP协议ID非0→立即返回异常

七、主循环结构

while(1){

   net_poll_sockets();      // 收Modbus TCP请求→解析→入队

   rtu_schedule_10ms();     // 后端是RTU则串行发

   tcp_schedule();          // 后端是TCP则按连接发

   timeout_check();         // 重传/离线判定

   usleep(1000);

}

Linux下可拆线程:收包线程、RTU状态机线程、各TCP从站轮询线程;RTU总线用互斥锁保护。

八、落坑清单

recv一次当一帧→粘包解析错

Unit ID不映射→32台全发到同从站

RTU后端并发发→总线冲突全丢

事务ID写死0→多请求回包串

队满还硬塞→响应全超时

超时就断开socket→工业网络抖动误杀

MBAP长度域不算单元ID→偏移错

响应按发送顺序匹配→流水线翻车

32台同周期轮询→广播风暴

CRC还加一遍→TCP模式多余且错

九、落地顺序

先通单台Modbus TCP收发+MBAP解析

建32台dev表,Unit ID路由

收包入队,主循环统一调度

RTU后端加总线互斥,单事务在途

TCP后端建事务ID映射表

超时重传+离线慢轮询

关键设备周期短、普通设备周期长

队满返异常、断开连接清理映射

32台接入的本质不是“并发32路”,是请求排队+事务匹配+后端介质互斥。以太网侧可以流水发,RS485侧必须单发;Unit ID负责寻址,Transaction ID负责回包对齐。固件别在socket回调里直接操作串口,也别假设响应按顺序来——队列和映射表一立,32台还是320台,调度逻辑都不用改。



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