 深度分析)
调用链入口包装层/* ethercatmain.c L1970-1973 */intecx_send_processdata(ecx_contextt*context){returnecx_send_processdata_group(context,0);// 默认用 group 0}intecx_send_processdata_group(ecx_contextt*context,uint8 group)// L1878-1881{returnecx_main_send_processdata(context,group,FALSE);// FALSE 普通IOmap非重叠}应用层调的 ec_send_processdata() 实际是 #ifdef EC_VER1 包装再把全局 ecx_context 传进去。group0大多数单组应用只用 0 组。use_overlap_ioFALSE重叠模式把 IOmap 输入输出放同一块逻辑地址以减少帧长非重叠模式是主站发送帧时输出数据在原地址上发出从站回传时输入数据写在另一块地址上本工程用默认非重叠模式。2.ecx_main_send_processdata()块 1变量初始化 DC 判断uint32 LogAdr;uint16 w1,w2;intlength,sublength;uint8 idx;intwkc;uint8*data;boolean firstFALSE;uint16 currentsegment0;uint32 iomapinputoffset;wkc0;if(context-grouplist[group].hasdc){firstTRUE;// 本群组有DC能力 → 第一帧附加DC同步数据报}块 2计算帧总长if(use_overlap_ioTRUE){length(context-grouplist[group].Obytescontext-grouplist[group].Ibytes)?context-grouplist[group].Obytes:context-grouplist[group].Ibytes;// 取 max(O, I)iomapinputoffsetcontext-grouplist[group].Obytes;// 输入相对输出的偏移}else{lengthcontext-grouplist[group].Obytescontext-grouplist[group].Ibytes;// 总和iomapinputoffset0;}。普通模式一帧里先塞所有输出字节再塞所有输入字节占位符。总长 Obytes Ibytes。。重叠模式LRW 的高端玩法让逻辑地址的输入区和输出区重合输出长输入长 max(O,I)帧更短效率更高。但前提是所有从站的 FMMU 能对齐到同一逻辑区间工程里默认不开。块 3取逻辑起始地址LogAdrcontext-grouplist[group].logstartaddr;if(length){wkc1;logstartaddrec_config_map_group 里分配通常0x00000000。逻辑地址 0 开始第一个字节给第一个输出从站。wkc1标记「本周期有数据报要发送」返回值 1 给调用者表示成功。块 4分岔路口 — blockLRW 判断L1714-1843三个分支分支 AblockLRW ! 0 且 Ibytes ! 0 → 先发 LRD 帧读输入分支 BblockLRW ! 0 且 Obytes ! 0 → 再发 LWR 帧写输出分支 CblockLRW 0 → 一条 LRW 搞定读写默认走这个blockLRW 什么时候为非 0 ethercatconfig.c 里扫描时如果发现某个从站 ESC 芯片不支持 LRW 命令老 ESC就把该从站的 slave-blockLRW聚合到 group 时就取组内最大值。支持 LRW 的从站这个值是 0。分支 ALRD逻辑读帧构造 — blockLRW有输入if(context-grouplist[group].blockLRW){if(context-grouplist[group].Ibytes){currentsegmentcontext-grouplist[group].Isegment;// 输入从第几个segment开始datacontext-grouplist[group].inputs;// data 指向输入区缓冲区lengthcontext-grouplist[group].Ibytes;LogAdrcontext-grouplist[group].Obytes;// 输入的逻辑地址 基址 输出长度Isegmentsegment 数组中输入区的起点下标。因为 IOsegment[] 是 [seg0_len, seg1_len, …]先放输出分段后放输入分段。Isegment 告诉从输出之后哪个段开始是输入。LogAdr Obytes输入的逻辑地址在输出之后。假设输出占 16 字节、输入占 8 字节则输出基址0x0000输入基址0x0010。data 指向 IOmap 输入区——注意这里是发帧前的 data 指针发的是占位符LRD 命令发出时 data 可以是垃圾回来时才填输入值但 SOEM 把它设为 inputs 指针是为了收帧时直接把返回的数据拷到 IOmap 输入区接收侧见后面 memcpy。segment 分片循环do-whiledo{if(currentsegmentcontext-grouplist[group].Isegment)sublengthIOsegment[currentsegment]-Ioffset;// 第一段要跳过 Ioffset 对齐elsesublengthIOsegment[currentsegment];// 后续段是完整长度idxecx_getindex(context-port);// 分配缓冲索引w1LO_WORD(LogAdr);w2HI_WORD(LogAdr);// 32位逻辑地址拆成 1616ecx_setupdatagram(txbuf[idx],EC_CMD_LRD,idx,w1,w2,sublength,data);// 组LRD帧为什么分片 EtherCAT 数据报的 dlength 字段只有 11 位0-2047 字节加上 SM 同步管理器边界不能跨帧切分所以如果 ObytesIbytes 超过一个数据报能装的长度就要切多个。IOsegment[] 是 ec_config_map_group 计算好的不会跨 SM 边界、不会超限的安全切分长度列表nsegments 是段数。if(first){context-DClsublength;// 记住第一帧LRD数据部分有 sublength 字节context-DCtOecx_adddatagram(txbuf[idx],EC_CMD_FRMW,idx,FALSE,slavelist[DCnext].configadr,ECT_REG_DCSYSTIME,sizeof(int64),context-DCtime);firstFALSE;}DC 同步帧追加L1740-1748这是 first 变量的唯一用途EC_CMD_FRMW 配置地址读写对指定从站 configadr 同时读 8 字节 写 8 字节configadr 用组内 DCnext 从站的配置地址DC 主参考时钟从站ec_configdc 里确定ECT_REG_DCSYSTIME0x0910系统时间寄存器 64bitcontext-DCtime 是主站要写入的当前 DC 时间64 位返回值 DCtOecx_adddatagram 返回第二个数据报在以太网帧中的字节偏移量——收帧时要从这个偏移量位置取出 8 字节 DC 时间覆盖主站保存的 DCtime。这个设计太精巧了效果一条以太网帧里串了两个 EtherCAT 数据报LRD FRMW一帧经过从站链既完成了输入采集又完成了主参考时钟从站的时间同步。ecx_outframe_red(context-port,idx);// 发到物理层ecx_pushindex(context,idx,data,sublength);// 压栈length-sublength;LogAdrsublength;datasublength;// 推进游标}while(length(currentsegmentnsegments));ecx_outframe_red单网口场景等价于 ecx_outframe调 bfin_EMAC_send 发出去并把 rxbuf[idx] 状态置 TX。ecx_pushindex把idxdata帧索引, dataIOmap里本小段指针, sublength字节数塞到 idxstack[] 里。后面收帧时按 idx 取回帧内容时用 data 指针直接 memcopy 到正确位置。这是 send 和 receive 之间的契约——栈顶是什么顺序receive 就按什么顺序收。staticvoidecx_pushindex(ecx_contextt*context,uint8 idx,void*data,uint16 length)// L1625{if(context-idxstack-pushedEC_MAXBUF){context-idxstack-idx[context-idxstack-pushed]idx;context-idxstack-data[context-idxstack-pushed]data;context-idxstack-length[context-idxstack-pushed]length;context-idxstack-pushed;}}栈结构ethercatmain.htypedefstruct{uint8 pushed;// 已压入数量uint8 pulled;// 已弹出数量uint8 idx[EC_MAXBUF];// 每段的帧索引void*data[EC_MAXBUF];// 该段在IOmap中的目标指针uint16 length[EC_MAXBUF];// 该段字节长}ec_idxstackT;不是严格的 LIFO 栈其实是「顺序列表」send 按顺序 pushpushedreceive 按顺序 pullpulled。pulledpushed 说明还有没收的。分支 BLWR逻辑写帧构造 — 有输出L1759-1795if(context-grouplist[group].Obytes){datacontext-grouplist[group].outputs;// data指向输出区真正要发给从站的内容lengthcontext-grouplist[group].Obytes;LogAdrcontext-grouplist[group].logstartaddr;// 逻辑地址回到基址输出区在前面currentsegment0;和 LRD 流程基本一致但 dataoutputs、LogAdr 从头开始。一样的 segment 循环分配 idx → setupdatagram(LWR) → 首帧追加 DC(FRMW) → outframe_red → pushindex → 推进游标。分支 CLRW逻辑读写帧构造 — 默认路径L1797-1843else// LRW can be used{if(context-grouplist[group].Obytes)datacontext-grouplist[group].outputs;else{datacontext-grouplist[group].inputs;iomapinputoffset0;// 只有输入就直接用输入偏移0}正常有输出有输入data 取 outputs因为先发输出数据接收回来时输入会覆盖输出区所以 dataoffset 的位置在接收侧做输入修正。如果纯输入从站没有输出则 data 直接是 inputsoffset 重置 0。LRW segment 循环L1814-1842 和前面两段几乎一样do{sublengthIOsegment[currentsegment];idxecx_getindex(context-port);w1LO_WORD(LogAdr);w2HI_WORD(LogAdr);ecx_setupdatagram(txbuf[idx],EC_CMD_LRW,idx,w1,w2,sublength,data);// LRW!if(first){context-DClsublength;context-DCtOecx_adddatagram(txbuf[idx],EC_CMD_FRMW,idx,FALSE,slavelist[DCnext].configadr,ECT_REG_DCSYSTIME,sizeof(int64),context-DCtime);firstFALSE;}ecx_outframe_red(context-port,idx);ecx_pushindex(context,idx,(dataiomapinputoffset),sublength);// ★length-sublength;LogAdrsublength;datasublength;}while(...);唯一差别在 pushindex 的 data 参数(data iomapinputoffset)。这是 LRW 的独特性决定的LRW 帧发送时 data 是 outputs输出数据从站处理时把输出读走、把输入写回同一位置。返回帧时EtherCAT 数据区前半是 outputs 长度后半被 inputs 覆盖——接收侧要用这个 dataObytes 偏移才能定位到 inputs 部分。非 overlap 模式下 iomapinputoffset0实际不偏移这里代码是为 overlap 模式统一写的。