
前段时间把一套跑在 STM32F4 上的 EtherCAT 从站代码往新的主控平台迁移原以为协议栈才是最难啃的骨头真正动手才发现卡进度的地方几乎全在 SPI 这一条链路上。EtherCAT-7 是我们内部对这块从站板卡的项目代号跟协议版本没有关系最近这轮改动就是把原来基于 STM32F4 LAN9252 的方案移植到一块以 RK3568 为核心的板子上。EtherCAT 从站代码移植表面上看着是换编译链、换平台库实际上 ESC 芯片没换、SSC 协议栈主体没大动真正需要重写的部分几乎都集中在 SPI 底层寄存器读写时序、片选极性、DMA 搬运、中断接入每一项都会直接影响从站能不能被主站正常扫描到。写这篇文章是想把这次移植中 SPI 部分的完整思路和踩坑过程记录下来。内容不涉及太深的协议原理更多是实操层面的经验比如为什么 ESC 芯片要用 SPI 和主控通信、SSC 生成的代码里哪些文件必须改、RK3568 上 spidev 怎么配、读寄存器全 FF 时应该如何排查。如果你也在做 EtherCAT 从站开发或者准备把一套 MCU 上的从站代码往另一个平台搬这篇应该能帮你少走不少弯路。1. 先搞清楚 SPI 在 EtherCAT 从站里的角色主控与 ESC 的本地总线1.1 ESC 芯片为什么把 SPI 作为标配接口EtherCAT 从站硬件上通常有一块专门的 ESCEtherCAT Slave Controller芯片常见的有 LAN9252、AX58100、ET1100 这些。实时以太网帧的收发、报文解析、FMMU 映射这些重活全部由 ESC 芯片内部的硬件状态机完成主控 CPU 并不直接参与网络数据链路。主控和 ESC 之间需要的只是一条访问 ESC 内部寄存器和过程数据 RAM 的本地总线。这条本地总线有几种选择并行总线、SPI、Microwire。实际从站方案里SPI 用得最多原因是引脚少、实现简单对于从站应用来说带宽也基本够用。LAN9252 这类芯片内部寄存器空间不大过程数据交互通常是几十到几百字节只要 SPI 时钟跑在几 MHz 以上就没有瓶颈。另一个好处是 SPI 从站接口在 FPGA 或 MCU 上很容易实现很多国产 ESC 兼容芯片也沿用了这种接口方式。所以在移植的时候首先要有这个概念EtherCAT 通信协议本身由 ESC 芯片扛着主控通过 SPI 读写的只是 ESC 的控制面板——状态寄存器、AL 事件、过程数据缓冲区。理解了这一点就会明白为什么 SPI 底层写不好整个从站就动不了因为主控和 ESC 之间唯一的通道就是这根线。1.2 移植改动清单协议栈能留SPI 底层必须重写SSCSlave Stack Code生成的从站工程整体上可以分成三层应用层用户自己的 PDO 映射逻辑、协议栈层EtherCAT 状态机处理、邮箱通信、CoE 处理、硬件抽象层SPI 驱动、定时器、中断。平台切换的时候应用层和协议栈层基本上可以原样保留真正要动的是硬件抽象层。我这次移植的实际改动清单大致是这样SPI 驱动原工程用的是 STM32 HAL 库的 SPI 外设驱动要换成 RK3568 Linux 下的 spidev 接口。片选控制原工程用硬件片选加软件拉低处理新平台用的是 GPIO 片选设备树里就要显式配置。中断处理原工程有独立的中断服务函数新平台需要把 LAN9252 的 IRQ 输出接到 RK3568 的 GPIO再在用户态用 poll 或中断线程处理。缓存管理原工程在 MCU 上直接用数组当缓冲区新平台在 Linux 用户态跑DMA 缓冲区的 cache 对齐问题必须处理。其中 SPI 底层改动是绝对大头。协议栈的代码文件基本没动甚至编译完直接链接就能过但 SPI_Transfer 这个函数不重写整个从站就是死的。1.3 新旧平台 SPI 外设差异对照用表格把两个平台的 SPI 差异列出来移植前心里会有底。对比项STM32F4原平台RK3568 Linux新平台SPI 接口访问方式寄存器操作 HAL 库/dev/spidevB.C 文件接口初始化方式HAL_SPI_Init 配置设备树节点 驱动自动配置片选硬件 NSS 或 GPIOcs-gpios 或硬件片选中断单个 IRQHandlerGPIO 中断 用户态 poll/线程缓存一致性无需考虑DMA 缓冲区需要 cacheline 对齐调试手段在线调试直接看寄存器spidev ioctl 慢速验证逻辑分析仪辅助这些差异决定了移植工作不是简单改个函数名而是对整个 SPI 通路重新做一遍设计。我在动手前把这些列出来后面每一项都对着做避免遗漏。2. 最小工程验证在 RK3568 上先把 SPI 物理链路跑通这一步是我强烈建议所有做移植的人都先做的不要一上来就去跑完整 SSC 代码先单独验证 SPI 链路能不能正常读写 ESC 寄存器。把这条链路当成独立模块调通后面协议栈的问题排查范围会小很多。2.1 设备树里 SPI 节点怎么配RK3568 上有多个 SPI 控制器具体用哪一个要查板卡的原理图。我当时接的是 SPI0设备树配置如下spi0 { status okay; pinctrl-names default; pinctrl-0 spi0_pins; cs-gpios gpio2 7 GPIO_ACTIVE_LOW; spidev0 { compatible spidev; reg 0; spi-max-frequency 5000000; spi-cpol 0; spi-cpha 0; }; };几个关键点cs-gpios指定了片选引脚。LAN9252 的片选是低有效所以GPIO_ACTIVE_LOW必须写上否则读写时序完全不对。spi-max-frequency这里先保守一点5MHz。后面稳定了再往上调LAN9252 是可以跑到更高速度的但调试阶段没必要给自己增加不确定因素。spi-cpol和spi-cpha我直接明确写 0对应 SPI Mode 0。LAN9252 默认支持 SPI Mode 0这也是大多数 ESC 芯片最常用的工作模式。按照我的实际经验设备树写完先别急着写用户态程序先看/dev/spidev0.0节点有没有生成。没有的话多半是设备树 compatible 匹配不上或者内核没有打开 spidev 驱动。2.2 用户态 spidev 读写验证节点生成后写一个最简单的 C 程序通过 spidev 的 ioctl 接口发送一组数据。这里注意SPI 是全双工协议读数据的时候主控必须同时发送时钟所以读 ESC 寄存器时发送的 dummy 字节一般填 0xFF 或 0x00具体根据 ESC 芯片手册要求。#include stdio.h #include fcntl.h #include unistd.h #include sys/ioctl.h #include linux/spi/spidev.h int main(void) { int fd open(/dev/spidev0.0, O_RDWR); if (fd 0) { perror(open spidev); return -1; } uint8_t mode SPI_MODE_0; ioctl(fd, SPI_IOC_WR_MODE, mode); uint32_t speed 5000000; ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, speed); // 先读一个 ESC 识别/测试寄存器验证链路 uint8_t tx[4] {0x03, 0x00, 0x00, 0x00}; uint8_t rx[4] {0}; struct spi_ioc_transfer tr {0}; tr.tx_buf (unsigned long)tx; tr.rx_buf (unsigned long)rx; tr.len sizeof(tx); tr.speed_hz speed; tr.bits_per_word 8; int ret ioctl(fd, SPI_IOC_MESSAGE(1), tr); if (ret 0) { perror(SPI_IOC_MESSAGE); return -1; } printf(rx: %02x %02x %02x %02x\n, rx[0], rx[1], rx[2], rx[3]); close(fd); return 0; }第一次跑通的时候如果读回的不是全 FF说明 SPI 物理链路基本通了。我当时读回的第一个字节就是 0x00后面字节能对上预期值心里就踏实了一大半。2.3 片选处理硬件片选还是 GPIO 片选RK3568 的 SPI 控制器本身带硬件片选输出理论上接上就能用。但实际用起来有几个问题硬件片选极性在某些内核版本下控制不够直观多设备共用总线时会麻烦而且硬件片选的时序在高速传输时不一定符合 ESC 芯片对片选建立时间的要求。所以我的建议是直接用 GPIO 片选这也是很多 RK 平台方案的常见做法。GPIO 片选在设备树里配置好后spidev 驱动会在每次传输前自动拉低片选、传输结束后拉高用户态程序不需要手动操作 GPIO。刚开始如果对片选极性没把握可以用一个简单的实验配置好后测量片选引脚在传输瞬间的波形确认是低有效再继续往下调。3. SSC 协议栈中 SPI 层的替换与适配测试程序跑通只是第一步。接下来要把 ESP 芯片的寄存器读写能力接入到 SSC 生成的从站代码里。SSC 代码里 SPI 相关的部分相对集中适配的目标很清楚让协议栈调用 SPI 读写接口时底层走的是新的 spidev 通道。3.1 SSC 代码里与 SPI 直接相关的文件SSC 生成的工程通常有这些和 SPI 直接相关的文件SPI_Driver.c/SPI_Driver.h封装了对 SPI 外设的直接操作比如 SPI 初始化、片选控制、数据收发。esc_hw.c/esc_hw.hESC 芯片的硬件初始化包括复位、读取芯片类型、配置 ESC 寄存器。main.c初始化流程调用 SPI 初始化和 ESC 初始化。原工程里 STM32 的 SPI 初始化用的是 HAL 库比如HAL_SPI_Init、HAL_SPI_TransmitReceive。到 RK3568 Linux 平台这些函数都不存在了需要把它们替换成基于 spidev 的实现。因为 SSC 代码调用 SPI 接口的层次很单一替换起来并不复杂关键是把数据收发函数实现正确。3.2 SPI_Transfer 和寄存器读写函数的适配SSC 代码里底层数据传输最终都会落到一个类似SPI_Transfer的函数上。这个函数的作用是一边发送 tx 缓冲区数据一边接收 rx 缓冲区数据长度由参数指定。在 STM32 上的实现一般是 HAL 库的HAL_SPI_TransmitReceive在 Linux 下就是一次SPI_IOC_MESSAGEioctl。一个简化的新实现如下int spi_transfer(uint8_t *tx, uint8_t *rx, uint16_t len, uint32_t speed_hz) { struct spi_ioc_transfer tr {0}; tr.tx_buf (unsigned long)tx; tr.rx_buf (unsigned long)rx; tr.len len; tr.speed_hz speed_hz; tr.bits_per_word 8; tr.cs_change 0; int ret ioctl(spi_fd, SPI_IOC_MESSAGE(1), tr); if (ret 0) return -1; return 0; }这里面有几个细节要注意tr.tx_buf和tr.rx_buf不能为 0即使只读数据tx 缓冲区也要有一个有效指针否则 ioctl 会报 EINVAL。tr.cs_change在普通传输中保持 0表示一次传输结束后片选恢复默认状态。speed_hz不需要每次传输都设置但显式设置可以避免依赖全局状态。SSC 代码里还有一类典型的操作是读 ESC 寄存器比如读 AL 控制寄存器、DL 状态寄存器等。这些操作在底层会拆成发送命令字节 发送地址字节 读取数据字节几个阶段SPI_Transfer 只是搬运工不需要理解命令语义所以适配时不用改协议层逻辑只要保证 SPI_Transfer 收发数据准确就行。3.3 中断与实时性从 NVIC 到 GPIO 轮询EtherCAT 从站和主站的交互并不是靠主控不断轮询 ESC 寄存器实现的那样太浪费 CPU。ESC 芯片有事件输出引脚通常叫 IRQ 或 INT当有 AL 事件、PDI 数据更新等情况时这个引脚会拉低或拉高通知主控来处理。在 STM32 工程里这个引脚配置成外部中断输入中断服务函数里会调用 SSC 代码的事件处理函数。移到 RK3568 Linux 平台后有两种做法第一种用 GPIO 中断加内核驱动把事件上报到用户态。实现复杂但实时性更好。第二种用户态开一个线程用 poll 或 select 监听 GPIO 事件。简单直接适合验证功能。我这次先用第二种方式跑通功能。实现思路是把 GPIO 导出成用户态可访问的接口用 poll 等待上升沿或下降沿事件来了再调用 SSC 的事件处理函数。示意见int gpio_fd open(/sys/class/gpio/gpioXX/value, O_RDONLY); struct pollfd pfd { .fd gpio_fd, .events POLLPRI, }; // 触发后调用 SSC 事件处理函数需要提醒的是Linux 用户态的调度延迟是真实存在的。如果从站所在的系统对同步性能要求很高比如需要 DC 同步精度在微秒级最好用 PREEMPT_RT 补丁并把处理线程绑定到指定 CPU 核同时把线程优先级调到最高。如果要求再高就直接上 RTOS 或者裸机方案。这是平台选型时就要想清楚的事不要等到移植完才发现实时性不达标。4. 调试过程实录全 FF 读数是 SPI 移植的必修课任何做过 SPI 相关开发的人几乎都遇到过读回来全是 FF这个现象。EtherCAT 从站 SPI 移植里这个现象尤其常见因为 ESC 芯片对时序、复位、片选的要求都卡得很严。我在这次移植中也踩了一圈把完整排查过程写出来。4.1 第一现场读回来全是 FF把第二部分的测试程序交叉编译到 RK3568 上跑第一眼看到的结果是rx: ff ff ff ff全 FF 意味着 MISO 引脚一直处于高电平ESC 芯片没有有效响应。这个现象能排除的问题很少反而说明问题可能出在任何环节只能从头开始排查。我首先怀疑的是 SPI 模式不对。LAN9252 默认要求 SPI Mode 0也就是 CPOL0、CPHA0数据在时钟上升沿采样。如果设备树里无意中加了spi-cpha或spi-cpol模式就变了数据采样点错位读回来的数据自然不对。确认方法很简单用逻辑分析仪抓 CLK、MOSI、MISO、CS 四根线看空闲时 CLK 是低还是高数据是在上升沿还是下降沿变化。4.2 逻辑分析仪下的时序排查逻辑分析仪一挂问题就很清晰了。正常 SPI Mode 0 的时序应该是空闲时 CLK 为低第一个时钟沿是上升沿数据在上升沿被采样片选在传输期间保持低电平。如果抓到的波形是空闲时 CLK 为高数据在下降沿变化那说明 CPOL/CPHA 配置错了实际工作在了 Mode 2 或 Mode 3。还有一种常见的坑是片选极性。我之前提到设备树里GPIO_ACTIVE_LOW必须正确配置如果配反了片选引脚在数据传输期间是高电平ESC 芯片根本没有被选中MISO 一直保持高阻读回来也就是 FF。逻辑分析仪上可以直接看到片选线的电平一眼就能确认。我这次的问题就出在片选极性上。板级原理图里片选引脚默认是高设备树里忘记加GPIO_ACTIVE_LOW导致 CS 电平完全反向。修正过来之后再跑测试程序读回来的数据立刻正常了。4.3 复位时序与电源带来的隐藏问题还有一次遇到的是间歇性全 FF不是每次上电都发生而是十次里有两三次读不到 ID。这种偶发问题最折腾人查 SPI 模式和片选都查不出问题最后发现是 ESC 芯片的复位时序没满足。LAN9252 这类芯片复位引脚释放后内部需要一段时间完成初始化之后 PDI 接口才能正常工作。如果主控在复位释放后立刻去读寄存器芯片还没准备好自然不回数据。原来的 STM32 工程里复位后加了一个几百微秒的延时移植到 Linux 平台后这个延时顺序没注意导致偶发失败。解决方式也简单在初始化流程里确保复位释放后等待足够时间再开始 SPI 通信。我当时把延时从几百微秒加大到 1ms问题就消失了。另外还要确认电源电压稳定如果 ESC 芯片的 3.3V 供电起来太慢同样会导致类似问题。4.4 打通后验证 EtherCAT 状态机SPI 链路通了之后才能开始验证 EtherCAT 从站状态机。方法是用 TwinCAT 作为主站通过网线连接从站板卡看主站能不能扫描到设备能不能把从站从 INIT 状态推到 OP 状态。这里的坑是如果 SPI 链路只是勉强能通读回来的寄存器数据偶尔有错位从站在 INIT 阶段看上去正常但一到 OP 状态就跑飞。原因往往是 SPI 时钟频率太高信号质量不好或者没有启用 CRC 校验。遇到这种情况先把 SPI 时钟降下来比如从 20MHz 降到 5MHz如果现象消失说明问题在信号完整性而不是逻辑。用 TwinCAT 扫描能看到从站的 Vendor ID 和 Product Code看到这些基本可以确定 ESC 芯片的身份和 SPI 读取路径都是对的。接下来再推状态机每一步都观察主站报错信息比在裸机上用调试器看寄存器直观很多。5. 移植收尾阶段的几个重要提醒SPI 链路通了、从站状态机能跑起来不代表移植就彻底结束了。后面还有不少细节会影响长期稳定性和性能简单记录一下我的经验。5.1 几个容易拖慢进度的配置细节spidev的compatible在部分内核版本下匹配不严格节点生成了但 ioctl 报错可以先检查内核配置是否打开CONFIG_SPI_SPIDEV。RK3568 的 SPI 控制器有一些平台相关的时钟配置设备树里没配好会导致 SPI 时钟频率和预期不一致。用SPI_IOC_RD_MAX_SPEED_HZ读一下实际生效的速度。如果从站支持 DCDistributed Clock启用SYNC0/SYNC1中断时要确认中断引脚和 ESC 的事件输出对应正确否则会出现主站激活同步后从站无响应。5.2 缓存、DMA 与对齐问题Linux 用户态使用 spidev 时默认走的是内核的 SPI 传输路径。数据量大的时候比如过程数据有几百字节要启用 DMA 才能降低 CPU 占用。但 DMA 对缓冲区有对齐要求普通的 malloc 缓冲区可能跨 cacheline导致数据不一致。一个实用的做法是用posix_memalign分配 cacheline 对齐的缓冲区比如 64 字节对齐。另外尽量减少每次 ioctl 传输的零碎包数量把多个寄存器读写合并成一次传输既能提高效率也能减少片选切换次数。5.3 我的个人建议先跑通最小链路再碰协议这次移植下来最大的体会是EtherCAT 从站代码移植的难点往往不在协议栈本身而在最底层的通信链路上。SPI 虽然看起来简单但模式、极性、频率、片选、复位、中断任何一个环节出问题都会表现为从站无法工作而且排查起来容易到处乱撞。所以我真心的建议是不管目标平台是什么一定先把 SPI 最小链路单独验证通过再去碰 SSC 的协议栈。先写一个不依赖任何协议栈的测试程序确认能稳定读写 ESC 寄存器然后再把协议栈接进来。这样每一步的问题都在可控范围内不会出现底层没通、上层又在报错的叠加态排查起来会轻松得多。另外整个过程里逻辑分析仪帮了大忙。调 SPI 协议的东西示波器或逻辑分析仪不是可选项而是必需的。没有它光靠猜模式、猜极性效率会低很多。