ARTICLE DETAIL

资讯详情

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

从STM32到RK3568:EtherCAT从站SPI通信移植实战解析

从STM32到RK3568:EtherCAT从站SPI通信移植实战解析 1. 项目概述一次典型EtherCAT从站代码移植1.1 为什么要做从站代码移植做工业控制的朋友应该都有体会EtherCAT从站的代码移植这件事看着是个纯粹的技术活实际上背后牵着一堆工程决策。我这次要聊的项目就是把一套原本跑在别的平台上的EtherCAT从站代码整个迁到以RK3568为核心的Linux平台上而其中最核心、最容易出问题的就是SPI通信这一环。先说清楚这个项目的背景。EtherCAT从站开发硬件上通常逃不开一个ESC芯片也就是EtherCAT Slave Controller常见的有LAN9252、AX58100之类的。主控通过SPI接口去读写ESC芯片内部的寄存器从而跟主站交换过程数据。这套方案的优点很明显主控只负责应用逻辑实时性要求高的EtherCAT协议栈部分由ESC芯片硬件来处理主控侧的压力小很多。那为什么还要移植因为产品迭代不可避免。芯片涨价、供货交期、主控算力不够、客户要求换国产平台——任何一个原因都可能让你把原来稳定的代码换到新平台上去。我这次的目的地是正点原子的RK3568开发板运行Linux 6.6.119内核这个版本的内核已经带了EtherCAT IGC支持对从站开发来说是个不错的底座。1.2 移植的核心难点在哪里很多第一次做从站移植的朋友容易把注意力放在EtherCAT协议本身这其实是本末倒置。协议是标准的ESC芯片厂家给的驱动代码也是标准的真正需要花心思的是你主控和ESC之间的那根“管子”——SPI。对EtherCAT从站来说SPI通信质量直接决定了整个系统的实时性能和稳定性。EtherCAT的同步周期通常是1ms甚至更短在每个周期内主控要通过SPI从ESC读取输入数据、写入输出数据这个读写操作必须在微秒级别完成同时还不能出错。SPI时序不对、DMA配置不合理、中断响应延迟过大任何一个环节掉链子都会导致从站丢帧、总线抖动甚至整个EtherCAT网络掉线。这次移植遇到的另一个难点是平台差异。原来的代码跑在STM32上用的是HAL库的SPI接口直接裸机编程延时用DWT计数一切都好控制。迁移到RK3568的Linux环境下SPI通信要走内核的SPI子系统用spidev或者自己写内核驱动中断处理要面对Linux的调度延迟这一切都跟原来不一样了。所以在动工之前我建议你先想清楚一个问题你要移植的是“代码”还是“功能”如果只是把代码从一个平台照搬到另一个平台那大概率会失败。真正的移植是重新理解原有代码的每一行逻辑然后用新平台的语言重新实现一遍。1.3 本文适合谁来读这篇博文适合正在做EtherCAT从站开发、准备做平台移植或者对SPI通信在实时场景下的应用感兴趣的工程师。我会从整体设计思路讲起再到具体的代码实现和排错过程尽量把那些文档里不会写的细节和经验都交代清楚。2. 移植前必须搞懂的ESC与SPI通信原理2.1 ESC芯片在从站系统里到底干什么要理解移植工作首先要理解ESC芯片在整个EtherCAT从站里的位置。这里打一个比方EtherCAT主站是一个物流中心的管理系统ESC芯片就是你工厂门口的收发室。收发室帮你接收包裹从站数据帧、分类登记处理寻址和映射、通知你来取件触发中断你把新包裹交给它送出去写邮箱和过程数据。真正干活的工厂车间也就是你的应用程序不需要操心物流链路怎么运作只需要跟收发室对接就行。从硬件的角度ESC芯片内部有不少功能模块主要包含EtherCAT数据链路层控制器负责物理层数据帧的收发包处理支持100Mbps全双工以太网通信。现场总线内存管理单元FMMU把主站发送的过程数据映射到ESC的本地RAM地址。同步管理器SyncManager管理过程数据和邮箱数据的同步读写保证数据一致性。寄存器空间包含状态机寄存器、中断寄存器、AL状态寄存器等通过这些寄存器可以让主控读取从站状态、配置工作模式。分布式时钟DC用于实现各从站之间的时钟同步这是EtherCAT运动控制方案的关键精度能到纳秒级。我的习惯是把ESC芯片看成主控的一张“外设网卡”主控不做协议解析只通过SPI接口间接操作。这也意味着从站的核心代码其实是分成两块的一块是跟ESC打交道的底层驱动另一块是跑在主控上的从站协议栈逻辑。2.2 SPI在EtherCAT从站中的角色与选型SPI是ESC和主控之间最主要的通信通道。它的角色可以通俗理解为收发室和车间之间的“内部专用通道”——其他人走不了这条路走这条路只为了送东西所以这条通道一定要快、要稳。EtherCAT从站的SPI接口一般跑的是从站模式。主控是SPI MasterESC是SPI Slave。通信数据格式通常包含一个访问地址16位寄存器地址或内存地址、一个命令字节读/写标志、以及若干字节的数据负载。不同厂家芯片的SPI数据帧格式略有差异但整体套路一致。在选择SPI工作模式的时候有个容易踩坑的点。很多人默认使用SPI Mode 0CPOL0CPHA0但LAN9252要求的是Mode 0或Mode 3有些ESC芯片对SPI时钟极性和相位的容忍度没那么高不确定的时候一定要查数据手册的SPI时序图。另外一个需要考虑的点是SPI的时钟频率。理论上SPI跑得越高读写ESC的速度越快。实际使用中要考虑线缆长度、主控SPI控制器稳定性、ESC芯片最高支持的时钟频率。比如LAN9252的SPI从站模式最大可以跑到25MHz或更高但我建议一开始先用10MHz跑通然后逐步提高不要一上来就挑战极限频率出了问题你都不知道是硬件问题还是时序问题。还有就是SPI模式设计的选择——你是打算做“同步”模式还是“异步”模式。同步模式比较简单主控发出读命令阻塞等待SPI返回整个过程一气呵成。异步模式则用DMA加中断主控发出命令之后立刻去干别的等SPI传输完成中断再回来处理数据。EtherCAT从站通常对实时性要求很高我建议用DMA加中断的方式尤其是在Linux这种非实时操作系统上阻塞等待会导致CPU被大量占用系统的整体实时性没法保证。2.3 对SPI时序图和寄存器映射的前置理解在动代码之前我强烈建议你把ESC芯片的SPI时序图和寄存器映射表拿到手打印出来贴在电脑旁边。这玩意儿不看清楚后面调试就是抓瞎。SPI时序图重点关注三个地方SPI时钟极性和相位CPOL/CPHA确认主控配置和芯片匹配。片选信号的建立时间和保持时间如果片选拉下去之后立即发时钟有些芯片可能还没准备好。读写命令的格式这是最关键的。一般来说SPI传输的第一个字节是命令字节最高位是读写位剩余位包含寄存器地址。有的芯片是发地址时发两个字节然后才是数据。寄存器映射表则决定了你代码里宏定义怎么写法。ESC芯片的寄存器地址、数据区起始地址、邮箱区地址这些都要在初始化阶段配置好。LAN9252的寄存器地址是16位的SPI数据帧格式是“命令位16位地址8位数据”这种结构实际传输时需要拼接。关于这块内容如果你用的是正点原子RK3568这样的开发板厂商会提供一些SPI回环测试的例程建议先在板子上用一根杜邦线把MOSI和MISO短接跑通SPI回环能收到自己发出去的数据再去接ESC芯片。这样能把“SPI驱动本身的问题”和“ESC通信的问题”分开来排查。3. 从原平台代码解析到新平台架构映射3.1 原平台代码的功能拆解我这次移植的源项目原平台是STM32F4主控实跑的是裸机程序SPI用DMA方式工作。拿到原始代码之后我没有急着动手改而是先把代码里跟平台相关的部分列出来。原始代码大概可以拆成以下几个模块SPI物理层驱动初始化GPIO、SPI外设、DMA通道实现SPI读写接口。ESC驱动层通过SPI读写ESC寄存器、读写过程数据处理IRQ中断。从站协议栈实现EtherCAT状态机的切换INIT→PRE_OP→SAFE_OP→OP处理邮箱通信。应用层从过程数据里解析出IO输入输出根据客户需求做逻辑控制。其中协议栈和应用层通常可以跨平台复用因为这些逻辑不依赖具体的硬件接口。真正要改的就是SPI物理层驱动和ESC驱动层。也就是说我这次移植的“重头戏”其实就落在“SPI驱动适配”上。在做功能拆解的时候我还会把代码里的延时逻辑、中断回调、信号量交互位单独标记出来。比如原来代码里在SPI发送之后用了一个标志位等待DMA传输完成。这个标志位在STM32裸机上没有任何问题但到了Linux下你对DMA完成事件的等待方式完全不同必须要做适配改造。这种隐藏在细小逻辑里的“平台依赖”才是移植工作的真正难点。3.2 目标平台RK3568和Linux SPI子系统架构RK3568的SPI控制器是芯片内置的支持主机模式和从机模式支持DMA传输最高工作频率也能满足EtherCAT从站的需求。跟STM32裸机开发不一样在Linux下操作SPI你需要接触的是内核的SPI子系统。Linux的SPI子系统做了非常好的抽象层次大概是这样的SPI控制器驱动由芯片厂商提供负责操作硬件寄存器处理数据收发。对开发者来说一般不用关心。SPI协议驱动对应具体的SPI设备可以是一个内核驱动也可以直接用spidev设备节点。SPI设备节点通过设备树来声明描述这个SPI设备挂在哪个控制器上、片选是哪个、SPI模式是什么、最大速率多少。在RK3568这么跑Linux的平台上做SPI通信我一般会先决定是走“用户态spidev”还是“内核态驱动”这两条路。用户态spidev的优点是开发方便直接用open、ioctl、read、write操作设备节点就行适合做功能验证和小数据量的通信。缺点是实时性没有保证每次SPI操作都可能被内核调度打断。内核态驱动则复杂一些需要写完整的驱动程序把SPI通信逻辑放到内核态配合DMA、等待队列、中断处理实时性会好很多。对于EtherCAT这种对实时性要求非常高的场景最终一定要走内核态驱动。不过我建议你的移植过程分两步走先用spidev把ESC读写打通验证寄存器访问、中断响应这些基本功能然后再把核心通信逻辑迁到内核驱动里做性能优化。3.3 映射关系和驱动架构重设计在动手写代码之前我先做了映射关系表格把原平台代码和要新写的代码一一对应起来。原平台STM32代码块 | 新平台Linux代码块 | 处理方法 SPI初始化GPIO/外设/DMA | 设备树配置 SPI控制器驱动初始化 | 设备树声明SPI节点配置模式、速率、DMA通道 SPI发送/接收接口 | SPI子系统的transfer接口 | 封装成自己的spi_read_reg/spi_write_reg函数 DMA完成标志位 | Linux的completion或wait_queue | 注册DMA完成中断用wait_event等待 IRQ中断处理 | IRQ线程化中断处理 | 在中断回调中唤醒处理线程把耗时的SPI读写放到线程里 毫秒延时 | msleep / usleep_range | 按需求替换这套映射关系搞清楚了后面写代码就是按图索骥心里有底。4. 实操从零开始移植EtherCAT从站SPI通信代码4.1 设备树配置先把底子打好RK3568上有多个SPI控制器我先把硬件连线的引脚确认好。正点原子RK3568开发板上SPI0的引脚复用在GPIO3_C0到GPIO3_C3我设计用的ESC芯片LAN9252连接在SPI0上片选用CS0另外ESC的IRQ输出接在GPIO的某个中断脚上。设备树配置是第一步也是后面所有工作的基础。我的设备树节点大概长这样spi0 { status okay; pinctrl-names default; pinctrl-0 spi0_pins, spi0_cs0_pins; cs-gpios gpio3 RK_PC1 GPIO_ACTIVE_LOW; lan9252: lan92520 { compatible microchip,lan9252; reg 0; spi-max-frequency 10000000; spi-cpha; spi-cpol; interrupt-parent gpio3; interrupts RK_PC2 IRQ_TYPE_EDGE_FALLING; status okay; }; };这里有几个细节特别提醒一下。LAS9252的SPI模式从晶片手册上是Mode 0或者Mode 3根据实际跟主控的电平时序匹配情况来选择。如果你的主控逻辑和芯片对接不上现象就是读出来的寄存器全是0xFF或者0x00。这时候先查设备树里的spi-cpha、spi-cpol配置别急着改代码。spi-max-frequency一开始我建议设10MHz。100MHz的时钟会把信号边沿变得很陡峭如果PCB布线没做好信号完整性问题会非常头疼。10MHz跑通后再逐步提高。interrupts属性的触发方式要看ESC芯片的中断输出极性。LAN9252的中断输出默认是低电平有效所以设置为IRQ_TYPE_EDGE_FALLING是有道理的。但如果你的硬件电路把IRQ反相了也要对应调整。4.2 底层SPI读写接口封装设备树配置好之后要考虑的就是SPI读写接口怎么实现的问题。在Linux内核态做SPI通信核心是围绕spi_transfer和spi_message来构建请求。在我的代码里我封装了三个核心接口spi_read_reg、spi_write_reg、spi_read_buf。其中spi_read_reg读单个寄存器spi_write_reg写单个寄存器spi_read_buf批量读数据。批量读主要用于周期读取过程数据性能要求高必须用DMA。以下是spi_read_reg的代码示例int lan9252_spi_read_reg(struct lan9252_dev *dev, u16 addr, u8 *val) { struct spi_transfer tr; struct spi_message msg; u8 tx_buf[4]; u8 rx_buf[4]; tx_buf[0] (addr 8) 0xFF; tx_buf[1] addr 0xFF; tx_buf[2] 0x00; /* dummy byte */ tx_buf[3] 0x00; memset(tr, 0, sizeof(tr)); spi_message_init(msg); tr.tx_buf tx_buf; tr.rx_buf rx_buf; tr.len 4; spi_message_add_tail(tr, msg); int ret spi_sync(dev-spi_dev, msg); if (ret) { dev_err(dev-spi_dev-dev, spi read reg failed: addr0x%04x\n, addr); return ret; } *val rx_buf[3]; return 0; }这里请你注意一个细节EtherCAT从站的SPI读操作不是“发地址等数据返回”那种简单的读写。在SPI这种全双工总线上你发数据的同时也在接收数据所以读操作需要多发一个dummy字节来获取数据字节。如果dummy字节数量不对你读回来的数据位置就会错位看起来就是“错位”或者“延时”现象很难排查。我当初第一次调的时候就是没搞清楚dummy字节读出来的寄存器数据全部是上一条命令的残留值折腾了一个下午才发现是这里多了个字节。别问我为什么这么设计——这就是芯片设计时的时序约定你只能按规矩来。4.3 ESC初始化流程的代码适配ESC芯片的初始化流程是理解从站代码的钥匙。这一步我建议你先把原理彻底弄通再去看代码。初始化流程大致有这几个步骤复位ESC芯片等待复位完成。配置同步管理器SyncManager把过程数据的输入输出通道、邮箱通道映射到ESC的RAM区域。配置FMMU把过程数据和地址映射关系建立起来。配置ESC的PHY控制确保物理层正常。使能ESC的中断输出让主控能够接收到事件。在Linux平台上这些初始化逻辑和裸机没有太大区别只是通过平台相关的SPI接口来访问寄存器。所以在移植代码时这些初始化逻辑基本不用改我只需要把原来代码里的HAL_SPI_TransmitReceive接口换成linux的spi_read_reg/spi_write_reg即可。我举一个具体的例子。原来STM32的Hal库代码可能是这样的uint32_t HAL_SPI_TransmitReceive(SPI_HandleTypeDef *hspi, uint8_t *pTxData, uint8_t *pRxData, uint16_t Size, uint32_t Timeout);而移植到Linux之后对应的接口变成了static int spi_transfer(struct lan9252_dev *dev, u8 *tx, u8 *rx, size_t len);这种适配工作是相对机械的纯粹是接口替换。最需要注意的反而是那些“隐式依赖”。比如原来代码里在SPI发送数据之前会把NRST引脚拉低再拉高触发ESC复位。到了Linux平台复位引脚要通过在设备树里把它配置成GPIO输出口来操作。类似这种操纵引脚的部分就是容易漏掉的“隐形代码”。4.4 DMA加持下的批量数据读写EtherCAT从站最核心、最频繁的操作是在每个同步周期里读写过程数据。这个过程对时间的要求极高如果用普通PIO模式一个个字节搬耗费的CPU时间不可接受。内核态里使用DMA配置方法其实非常直接。SPI控制器驱动本身已经结合DMA了你要做的事情只是保证SPI transfer的buf是DMA可控的内存。在Linux内核里使用kmalloc分配的内存通常都可以但如果你用栈上分配的缓冲区局部数组可能不行需要改用devm_kzalloc之类的内核API。批量读的过程数据可以这样实现int lan9252_spi_read_buf(struct lan9252_dev *dev, u16 addr, u8 *buf, size_t len) { struct spi_transfer tr; struct spi_message msg; u8 addr_head[2]; addr_head[0] 0x80 | ((addr 8) 0x7F); /* 读命令 高地址 */ addr_head[1] addr 0xFF; int total_len 2 len; /* 2字节头 数据 */ memset(tr, 0, sizeof(tr)); spi_message_init(msg); tr.tx_buf NULL; tr.rx_buf NULL; /* 实际上我需要一个合并传输方案 */ ... }在批量传输时你可以选择用spi_message带多个spi_transfer链的方式第一个transfer发送2字节地址头第二个transfer读取数据。这样可以避免申请内存再把地址头部和数据拼接在一起。这属于比较细致的性能优化了。4.5 中断处理与实时性设计在EtherCAT从站开发里中断处理的实时性是一个大话题。ESC芯片产生中断的信号通常是以下几种情况同步管理器事件新数据到达/数据已读完、邮箱事件、状态机变化事件等。ESC把IRQ引脚拉低主控收到中断后通过SPI读取中断状态寄存器判断具体是什么事件再做相应处理。在Linux平台上实现中断处理有几种常见方式常规中断处理函数只做寄存器的状态保存把具体工作交给下半部的tasklet或者workqueue。线程化中断threaded IRQ可以避免延迟问题也算比较推荐的做法。结合RT补丁的实时线程如果追求极致实时性可以配合Linux实时补丁PREEMPT_RT把中断处理线程设置成SCHED_FIFO的实时调度策略。在做这一步之前你必须想清楚自己想要什么样的实时性。EtherCAT的标准同步周期是1ms如果只是做IO类从站普通Linux内核加上线程化中断其实完全够用。如果要驱动伺服电机这类对同步精度要求极高的设备就要考虑分布式时钟DC和硬实时方案了。我这次采用的方案是线程化中断加等待队列。中断处理函数里只把标志位置位并唤醒等待队列上的处理线程。实际耗时的SPI读写操作都放在处理线程里执行。static irqreturn_t lan9252_irq_handler(int irq, void *data) { struct lan9252_dev *dev data; dev-irq_flag 1; wake_up_interruptible(dev-wait_queue); return IRQ_HANDLED; }处理线程逻辑static int lan9252_worker_thread(void *data) { struct lan9252_dev *dev data; while (!kthread_should_stop()) { wait_event_interruptible(dev-wait_queue, dev-irq_flag || kthread_should_stop()); if (kthread_should_stop()) break; dev-irq_flag 0; handle_esc_interrupt(dev); } return 0; }这样做的优点是中断处理函数执行时间极短Linux内核调度压力很小。缺点是多了一次线程切换的延迟。但对于大多数应用来说这个延迟是可以接受的。如果你对延迟特别敏感可以把工作线程绑定到某个CPU核心并设置成实时调度策略这样能进一步减少调度延迟。5. 实战一次完整的EtherCAT从站移植排错记录5.1 寄存器读回全为0xFFSPI通信纹丝不动这是发生率最高的现象。我这次也没有幸免新板子第一次上电读取LAN9252的寄存器返回的数据全是0xFF。第一步先排查硬件层面的问题。用示波器抓SPI总线的波形看四根线SCLK、MOSI、MISO、CS的时序。如果SCLK没有时钟输出说明SPI控制器没有初始化或者设备树有问题如果有波形但MISO一直是高电平可能是SPI模式不对或者ESC芯片压根没工作起来。我这次抓到的情况是SCLK有输出、MOSI有数据、MISO始终为高。然后用万用表量LAN9252的供电和复位引脚发现复位引脚一直被拉低——芯片一直处于复位状态自然不理会任何SPI指令。这个问题最终定位到复位引脚在设备树里没有被正确配置为GPIO输出默认被拉低了。在设备树里加上复位引脚的GPIO配置之后问题解决。5.2 读寄存器正常写寄存器没反应这种情况又分两种可能要么SPI写时序和写命令格式不对要么芯片被配置成了写保护状态。排查方法很简单用LAN9252的ID寄存器作为定位锚点。这个寄存器出厂时有一个固定的值例如0x9252如果能正常读出来说明芯片已经在线上了。然后你认为向一个无害的配置寄存器写入测试值再看能不能读回来。如果读回来的值不是写入的值那写操作一定有问题。我发现问题出在SPI帧格式上。EZ9252的写指令第一个字节的高位需要是1读是0但我的代码在操作的时候因为地址的位数拼错把写命令位给顶掉了。修正了命令字节的拼接逻辑之后写寄存器恢复正常。这里有一个建议在你写代码的时候先把SPI数据帧格式用注释的形式写清楚比如/* LAN9252 SPI 帧格式 * 第1字节: bit7 读写标志(0读, 1写), bit6-0 寄存器地址高7位 * 第2字节: bit7-0 寄存器地址低8位 * 第3字节及之后: 数据字节 */这样的注释看着简单但能让你在调试的时候少烧很多脑细胞。5.3 周期通信时偶发丢帧这是最折磨人的一个问题。单独测试读写寄存器都非常正常但把从站接入EtherCAT主站跑起来之后偶尔会出现丢帧导致主站报错。这种偶发问题是最难定位的。我怀疑的切入点有三个SPI时钟频率过高导致信号的完整性问题、DMA传输的缓冲区和缓存一致性问题、中断处理延迟。先解决第一个。把SPI工作频率从20MHz降到10MHz重新跑测试问题依旧。第二个可能性检查了DMA缓冲区用的是devm_kzalloc分配按照DMA API的要求做了缓存一致性处理。第三个可能性用内核trace工具抓了中断处理的时间发现有时候中断处理函数到工作线程开始执行延迟会到几百微秒这在1ms周期下是个不小的占比。最终问题的根源在时钟配置上。RK3568的SPI0控制器时钟我在设备树里没有显式配置内核用了默认的父时钟导致SPI的比特率不是整数分频得到的。这样一来实际SPI时钟频率可能有微妙偏差但偶尔在极端条件下会造成位错误。解决办法是在设备树里显式配置SPI控制器的时钟频率spi0 { assigned-clocks cru SCLK_SPI0; assigned-clock-rates 100000000; };配置完之后SPI的时钟分频变得非常规整再跑压力测试丢帧问题彻底消失。5.4 EtherCAT状态机卡在PRE_OP进入不了SAFE_OP这个现象是在从站代码基本移植完成、接入主站之后才出现的。从站正常进入PRE_OP状态但主站下发指令要求进入SAFE_OP时从站没有任何响应状态机一直卡住。EtherCAT从站的状态机切换是主站通过对从站的控制寄存器写入数据来触发的。从站需要读取状态机的请求执行相应的处理完成后把状态返回给主站。在处理这个问题时我用了一个调试技巧在主站侧打开Wireshark抓包对比从站返回的数据包和正常从站的差异。比对后发现从站的AL状态码寄存器返回了错误代码指向“无效的邮箱配置”。去查代码里的邮箱配置逻辑果然发现邮箱的同步管理器配置有问题。原来代码里用的配置是从上个平台不同的ESC芯片型号传下来的寄存器地址和缓冲区大小设置跟LAN9252不完全匹配。更新了邮箱配置参数之后从站顺利进入了OP状态。5.5 中断风暴IRQ引脚一直被拉低还有一次让我记忆犹新的问题就是现象表现为从站一上电CPU占用率就冲到100%系统几乎卡死。用top命令一看是中断处理进程在疯狂运行。这种“中断风暴”的起因通常有两种一种是ESC芯片长时间没有清除中断状态IRQ引脚一直被拉低另一种是中断处理程序里读取中断状态寄存器的逻辑有问题没有正确清除中断标志。我用示波器抓了IRQ引脚的电平发现它一直是低电平。分析下来是因为我在中断处理函数里读取了中断状态寄存器但没有对相应位做写1清除操作。LAN9252这类芯片通常要求你在读状态之后向中断状态寄存器写1来清除中断标志。少掉了这个“清除”步骤芯片就会认为主机没有处理完中断持续输出低电平脉冲。修复方式很简单在中断处理逻辑末尾向中断状态寄存器写入读取到的值写1清除即可。问题解决之后CPU占用率恢复到了正常水平。6. 性能调优与实践技巧6.1 实时性优化策略代码移植完成、功能跑通之后就该做性能调优了。EtherCAT从站的通信周期是1ms在普通Linux上跑通并不是什么难事但如果你的应用场景对同步精度要求高或者你的系统在通信之外还要做很多别的事情就必须考虑实时性优化。几个常用的优化手段使用PREEMPT_RT补丁。Linux 6.6.119内核可以打上PREEMPT_RT补丁把内核变成完全可抢占的实时内核。SPI的中断线程和通信线程就可以设置成SCHED_FIFO的实时调度策略。CPU隔离和绑核。通过内核启动参数isolcpus把某个核心隔离出来专门跑EtherCAT通信线程避免被其他进程打断。这一点操作起来比听起来麻烦一点但效果立竿见影。减少SPI传输的往返次数。每一次SPI transfer都有协议开销如果你能用一个SPI burst把所有要读的寄存器连续读完性能会好很多。LAN9252支持一次读取连续的地址空间主控可以在一个SPI消息里传大量数据。我当时优化完的效果从最开始的SPI读写一次需要几十微秒降到了批量读过程数据只需要几个微秒效果还是很明显的。6.2 使用内核trace和perf工具定位延迟在调试性能问题的时候靠肉眼和printk是不够的。建议用内核的ftrace工具来测量中断和线程之间的延迟。你可以用trace-cmd来记录和查看调度事件比如sched_wakeup、sched_switch这些事件分析出中断唤醒线程到线程真正运行到底花了多少时间。perf工具也很好用通过perf top可以快速找出CPU占用最高的函数很多问题一眼就能看出来。有一次我发现自己写的SPI读写接口在perf top里占了将近20%的CPU结果发现是spi_sync调用的时候带了一个无意义的循环把它去掉之后CPU占用率直接降下来了。6.3 调试过程中的黄金技巧调试EtherCAT从站有几个手段我认为是必会的。第一个是Wireshark抓包。EtherCAT主站软件通常都支持把网络数据包导出来你用Wireshark的ethercat解析器来分析数据帧可以看到主从站之间完整的通信流程和状态码很多问题在这里一目了然。第二个是示波器加逻辑分析仪。分析SPI时序验证片选信号是否存在毛刺、SPI时钟频率是否准确这套装备不能缺。第三个是ADC和万用表。检查ESC芯片供电是否稳定复位时序是否满足要求。很多“玄学”问题追根溯源都是复位引脚上电太慢或者供电纹波过大。7. 移植清单和避坑指南总结7.1 移植步骤脑图这次EtherCAT从站代码移植的整体流程我总结成了下面这个清单以后再做类似项目时直接对着走第一步分析原代码把协议栈代码和硬件相关代码分开。第二步阅读目标平台的SPI控制器手册和ESC芯片手册确认SPI模式和数据帧格式。第三步在Linux主机上配置设备树声明SPI节点、GPIO引脚、中断引脚。第四步用spidev先打通物理通道验证SPI回环然后读取ESC的ID寄存器。第五步实现内核态SPI读写接口包括单寄存器和批量数据读写。第六步把原代码的ESC初始化逻辑翻译成目标平台的SPI读写调用。第七步实现中断处理线程完成中断事件处理。第八步接入主站测试跑通状态机切换用Wireshark验证通信正确性。第九步DMA调优、中断延迟调优、实时性测试。7.2 避坑指南这次移植过程中踩过的坑我都记下来了整理成表格给你问题类型症状原因解决方案SPI模式不匹配读寄存器全为0xFFCPOL/CPHA配置不对查手册正确配置spi-cpol/spi-cpha复位引脚未配置芯片不工作复位引脚被拉低设备树配置GPIO输出手动控制复位SPI帧格式错误写寄存器无效命令字节拼接错误注释清楚协议格式按位拼接dummy字节数量不对数据错位读操作少发dummy字节对照时序图加dummy字节中断未清除中断风暴中断状态寄存器未写1清除读完状态后写1清除所有中断位DMA缓冲区未正确分配数据不一致缓存一致性问题用devm_kzalloc分配DMA安全内存SPI时钟分频不整偶发丢帧父时钟频率不当设备树显式配置SPI控制器时钟频率状态机卡在PRE_OP无法进入SAFE_OP邮箱同步管理器配置错误对比手册检查邮箱区域配置参数7.3 移植代码的性能实测数据最后分享一组我在RK3568上实测的性能数据供你参考对比。SPI配置10MHz批量读64字节过程数据单次SPI传输时间约7微秒。中断引脚触发到内核工作线程开始执行平均延迟约15微秒最大延迟约80微秒在低负载情况下。EtherCAT周期1ms循环运行连续跑24小时没有出现丢帧和总线错误。如果把这个配置改成SPI 20MHz加上PREEMPT_RT实时内核中断线程的延迟能压到平均10微秒以内完全可以满足绝大多数从站应用的要求。个人实际操作中最大的体会是EtherCAT从站移植的重头戏从来不是EtherCAT协议本身而是你脚下那根SPI管道挖得够不够深、铺得够不够稳。把SPI通信这一层做扎实协议栈和应用层都是水到渠成的事。希望这篇记录能帮你少走一些我走过的弯路。
返回列表