ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

QNX开发之ECAT专用网卡驱动ecpkt · 05-接收

QNX开发之ECAT专用网卡驱动ecpkt · 05-接收 QNX开发之ECAT专用网卡驱动ecpkt · 05-接收上一章结束时驱动的发送路径已经闭环50 条测试报文全部被 PC 端收到write() 一路畅通。但驱动还一帧报文都收不到——接收路径还是空的。这一篇我们来实现接收路径把报文从网线一路送到应用程序的 read() 里。在发送篇里我们已经理解了 SG-DMA 和 bdring接收功能同样是基于这两个模块。它与发送功能不一样的地方在于发送是由驱动主动发起接收则是被动响应的。这种被动响应的处理机制一般是周期轮询或中断两种方案各有优点周期轮询对应用的节奏控制更稳定但数据处理会有一些延迟中断模式的数据处理更及时但中断会打断应用程序的节奏且因为要走中断处理器系统开销也更大。这里我们沿用 devnp 的中断方案在 RX 中断里处理接收事件。注实际上对于 EtherCAT 通信来说发送和接收的节奏都是固定可控的使用周期轮询机制反倒实时性会更好。比如 Ubuntu 上的主站方案 IGH 也会为某些网卡芯片比如 Intel 的 e1000 等提供专用的实时驱动这些实时驱动相比通用驱动的一大改动就是把中断改成了轮询这里有一部分原因是 Linux 的中断处理属于软实时而不是硬实时会引入较大的抖动。我们这里先使用中断模式快速完成 ecpkt 的开发后续如果要进一步调优再考虑改成周期轮询的方式来实现接收功能。1. 同样基于SG-DMA和bdring的接收路径把发送路径倒过来就是接收路径SG-DMA、bdring、frame pool 这套机制全部原样复用只是数据流向反过来。发送是 write() 把用户数据写进槽位驱动把槽位的物理地址和长度填进 BDDMA 沿 bdring 把数据搬给 MAC 发出去接收则是 MAC 收到帧DMA 沿 bdring 找到 BD 指向的槽位把帧写进去驱动再把这一帧交给接收队列最后 read() 从队列里交给应用程序。接收路径和发送路径的主要区别在于槽位的生命周期发送的槽位是“过路的”——write() 时借一个发完就还槽位在 FREE、READY、INFLIGHT 之间流转接收的槽位则必须是“钉死的”——DMA 随时可能把一帧写到任何一个 BD 指向的槽位里如果槽位和 BD 的对应关系像发送那样动态变化DMA 在激活 BD 时 BD 里还没有可用地址DMA 就无法完成数据搬运对应的报文就直接丢了。所以接收路径下必须要保证可用 BD 里的地址是可用的。我们在初始化时一次性地把所有接收槽位和 BD 绑死——BD j 永远指向 slot j槽位的物理地址写进 BD 之后就不再改变BD 的状态切换与槽位的状态绑定在一起bdring 的更新依赖于槽位状态的更新“槽位可用”是 BD 可用的前提。为了区分这两种语义我们给 frame pool 的 slot 加了第五个状态 SLOT_BOUND绑定态发送走借还alloc / release接收走绑定bind / unbind两套生命周期在同一个池里并存。另外RX 环上任何时刻不能有空 BD。DMA 收到帧时是自己找 BD 写数据找不到可用的 buffer 就直接丢包不会等谁——所以初始化时必须把 128 个 BD 全部挂满 buffer 交给硬件同时每处理一帧回收 BD 和补挂新 buffer 必须成对出现。把 BD 从硬件收回来就要立刻重新挂回去少这一步这个槽位从此就收不到包了。2. 中断处理中断挂载与使能我们直接沿用 devnp 里的实现iid InterruptAttach(xzynq-irq, xzynq_isr, arg, 0, _NTO_INTR_FLAGS_PROCESS);devnp里的中断处理包含了中断上半段和中断下半段的处理const struct sigevent *xzynq_isr(void *arg, int iid) { xzynq_dev_t *xzynq arg; struct _iopkt_inter *ient xzynq-inter; /* Disable all interrupts */ out32(xzynq-regbase XZYNQ_EMACPS_IDR_OFFSET, XZYNQ_EMACPS_IXR_ALL_MASK); /* Clear interrupts */ xzynq-isr_status | in32(xzynq-regbase XZYNQ_EMACPS_ISR_OFFSET); out32(xzynq-regbase XZYNQ_EMACPS_ISR_OFFSET, xzynq-isr_status); return interrupt_queue(xzynq-iopkt, ient); }中断处理函数xzynq_isr完成上半段的寄存器操作之后往iopkt的任务队列里添加了一个事件相当于把下半段交给iopkt来处理iopkt的任务队列有点类似于linux下的workqueue属于iopkt特有的机制在ecpkt里面就需要我们自己来实现不过iopkt的任务队列机制是针对多网卡多应用的复杂情况来处理的ecpkt里可以大大简化我们直接起一个线程来处理中断下半段就可以了pthread_create(g_drv.int_thread_t, NULL, int_thread, g_drv.xzynq)补充一下开发接收功能时我想当然地把中断处理和接收数据分成两个模块屏蔽掉接收数据先专注调试中断——比如中断能否触发、中断处理路径是否正常执行等。但 Zynq 的 GEM 控制器上中断触发并不是在 GEM 收到数据时而是在 DMA 把数据完成搬运之后——中断触发的语义不是“有数据到达”而是“有数据可用”。如果没有接收数据逻辑bdring 里的 BD 不会被正常释放BD 很快就会耗尽从而 DMA 无法完成数据搬运进而无法触发“可用数据”中断。所以中断处理和数据接收两个功能是强耦合在一起的两者必须协同起来才能工作。基于上面两点接收路径要做的事情就清楚了初始化时把所有接收槽位预挂给硬件全占满中断里收帧排空接收环、记录长度、交给接收队列、成对补齐read() 把帧从接收队列交给应用程序下面我们具体展开这三件事。1. 预挂接收槽位初始化阶段对每个空闲 BD 做三件事从 bdring 申请一个 BD如果这个 BD 是第一次挂接rx_slot_idx[j] 还是 NONE就把 slot j 和它绑定并把槽位的物理地址写进 BD——这个地址从此不再改动最后把这个 BD 交给硬件。128 个 BD、128 个槽位、每槽 2048 字节一轮循环全部挂满。之后每次收帧后的补挂走的是同一段代码的另一条路径槽位和 BD 地址都不动只是把 BD 重新交给硬件。整个接收数据路径上没有任何 alloc / release——绑定态的槽位永远不进空闲栈也不会被发送路径误用。2. 中断里收帧中断上半段里完成现场保护读 ISR 寄存器并写回清除中断源GEM 的中断是电平触发源不清、中断线就一直拉高、屏蔽中断防止下半段处理期间 ISR 反复重入空转、返回一个 sigevent 唤醒下半段。下半段线程化InterruptWait 被唤醒后进 process_interrupt发现是帧接收完成FRAMERX就调 process_rx 排空整个接收环——from_hw_rx 把所有已完成的 BD 一并取回逐帧校验 SOF / EOF按 rx_slot_idx[j] 找到槽位发送那张是 BD 正持有哪个槽位的反向映射接收这张因为绑定恒成立本质上是一个恒等映射把 BD 报告的真实长度记到槽位上供后面的 read() 查询最后 rx_push 把这一帧交给接收队列。一轮的收尾是成对补齐bdring_free 回收这批 BDsetup_rx_buffers 把它们重新挂回硬件如果这期间又到了新帧继续下一轮循环。3. read() 取数据中断线程把帧塞进接收队列——一个 128 项的环每项记录槽位号和长度用户态的 read() 从环的另一头取。队列满时丢帧计数push 跑在中断线程上下文绝不能阻塞等待。用户 buffer 比帧小时返回 EMSGSIZE帧留在队头不消耗下次 read() 还能取到——“读缓冲太小”不该吃掉一帧数据。数据本身的交付是零拷贝的帧还在 NOCACHE 映射的槽位内存里read() 处理时直接用 _RESMGR_PTR 把槽位内存挂到回复的 iov 上客户端经内核取走全程没有一次 memcpy。这里有一个需要注意的地方read 操作是阻塞的等待数据到来时进程会挂在 read 里等事件。这就会带来一个问题——RM 的消息分发是单线程的等待 read 时 RM 的消息处理 dispatch 是无法被激活的。这种情况下如果一个客户端意外死亡正常情况下 RM 会收到一条消息交给 dispatch 去处理但由于此时 RM 阻塞在 read 里dispatch 不会运行这个客户端死亡的事件就无法被处理RM 就会一直阻塞在 read 里直到有数据到来。在此期间所有需要 dispatch 处理的消息比如新客户端连接的到达都无法被处理。直观来说就是用户程序退出后再次启动请求打开 RM 设备时无法响应。解决这个问题的办法是延迟回复把这次请求的 rcvid 停进一个 pending 数组返回 _RESMGR_NOREPLY——客户端那边看起来还在正常阻塞RM 的 dispatch 线程却已经回到分发循环继续干活新帧到达时驱动给自己投一个脉冲dispatch 线程醒来后由 service_pending 把帧交给停着的那个读请求。对客户端来说这就是一次普普通通的阻塞 read()。完成以上工作后接收功能就可以闭环验证了。我们用下面这套方法测试一下PC 端用小工具发送确定性的广播流量板端运行 gem_test阻塞 read() 收帧每收一帧打印摘要运行日志可以看到PC 发出的报文全都收到了。到这一篇为止驱动的数据收发通路就完整了驱动本身的功能开发也告一段落后续可以使用 SOEM 主站来基于这个驱动进行通信并测量实时性看看专用驱动的效果怎么样。
返回列表