1. 项目概述与核心思路最近在基于瑞芯微RV1126B平台开发一个物联网边缘设备其中一个核心功能就是通过SPI总线连接外部的RFID读卡模块实现身份识别。虽然Linux内核已经提供了完善的SPI驱动框架但真要在用户空间把SPI用起来尤其是结合具体的硬件比如RC522还是有不少细节需要琢磨。网上关于RV1126的SPI资料比较零散官方文档又偏向驱动层对应用开发者不够友好。我花了一周多时间从零开始把EASY EAI Nano-TB开发板上的SPI调通了期间踩了不少坑也总结了一套从环境搭建、代码编译到实际读写、问题排查的完整流程。这篇文章我就把这些实战经验系统地梳理出来目标就是让你拿到这块板子参照我的步骤能在半小时内跑通第一个SPI应用并理解背后的每一个参数和操作。简单来说SPISerial Peripheral Interface是一种高速、全双工、同步的串行通信总线在嵌入式领域用得非常多像Flash、传感器、显示屏、RFID模块等经常通过它和主控通信。在Linux下SPI设备被抽象成了/dev/spidevX.Y这样的字符设备文件应用层通过标准的文件IO接口open,read,write,ioctl就能操作这大大降低了开发难度。我们不需要去深究SPI控制器寄存器怎么配置重点在于理解如何通过ioctl设置正确的通信模式、速率等参数以及如何根据外设如RC522的特定协议组织数据帧。2. SPI基础与RV1126B硬件资源解析2.1 SPI通信核心概念快速回顾在动手写代码之前我们必须搞清楚SPI通信的几个关键参数这些参数直接决定了主设备RV1126B和从设备如RC522能否“对上话”。首先是最重要的通信模式Mode它由时钟极性CPOL和时钟相位CPHA共同决定共有4种模式Mode 0, 1, 2, 3。这个概念很多新手容易混淆我打个比方CPOL决定了时钟线在空闲时是“站着”高电平还是“蹲着”低电平CPHA则决定了数据是在时钟变化的“第一个边沿”还是“第二个边沿”被采样。你的从设备芯片手册里一定会写明它支持哪种模式主设备必须配置成相同的模式。比如RC522通常工作在Mode 0即CPOL0空闲时SCLK为低电平CPHA0数据在SCLK的上升沿被采样下降沿被移出。其次是比特率Speed也就是通信速度。这个值不是越高越好它受限于从设备的最大支持速率和PCB走线质量。RC522的SPI接口最高支持10MHz但在长线或干扰环境下适当降低速率比如1MHz可以提高稳定性。在Linux中我们通过ioctl设置spidev的max_speed_hz参数。还有一个参数是数据位宽Bits Per Word绝大多数SPI设备都是8位传输也就是一次传输1个字节。但有些设备可能支持16位或其它位宽需要根据数据手册确认。RV1126B的SPI控制器通常支持8位和16位。最后是字节序Endianness和片选CS控制。SPI通常是MSB最高有效位先发送。片选信号的控制方式也很重要Linux的spidev驱动默认会在每次传输前后自动拉低和拉高片选线。但对于某些特殊时序要求的设备可能需要通过GPIO手动控制片选这就需要我们绕过spidev的部分自动化功能或者使用spidev提供的“三线”模式等高级配置。2.2 RV1126B SPI硬件接口与设备节点映射EASY EAI Nano-TB开发板基于RV1126B芯片其SPI控制器资源比较丰富。根据板卡设计通常会有多个SPI控制器引出到排针上。理解/dev/spidev(bus).(cs)这个设备文件命名规则至关重要。bus代表SPI总线号对应芯片内部的SPI控制器编号如SPI0, SPI1, SPI3。每条总线包含一组SCLK、MOSI、MISO信号线。cs代表片选号对应控制器的片选信号线如CS0, CS1。同一条总线上可以挂多个设备通过不同的片选信号来区分。以我手头的板子为例启用SPI功能后在/dev目录下能看到/dev/spidev0.0 SPI0总线使用CS0片选。/dev/spidev0.1 SPI0总线使用CS1片选。/dev/spidev3.0 SPI3总线使用CS0片选。/dev/spidev3.1 SPI3总线使用CS1片选。注意具体哪个物理排针对应哪个spidev设备节点必须查阅你所用开发板的原理图或引脚复用表。例如我的RC522模块连接到了SPI3的CS0引脚那么我程序中操作的设备文件就是/dev/spidev3.0。接错了总线或者片选代码再怎么调也没用。2.3 硬件连接实战RC522模块接线RC522模块是一个典型的SPI从设备。与开发板的连接需要接对5根线不算电源SCLK (SPI时钟)- 连接至开发板SPI3的SCLK引脚。MOSI (主出从入)- 连接至开发板SPI3的MOSI引脚。MISO (主入从出)- 连接至开发板SPI3的MISO引脚。CS (片选)- 连接至开发板SPI3的CS0引脚。GND (地)- 连接至开发板GND。此外RC522还需要3.3V供电。务必确认开发板排针的电压是3.3VRV1126B的IO电压通常是3.3V与RC522匹配。如果接成5V可能会损坏模块。接线时的一个小技巧使用杜邦线连接时最好给电源线3.3V和GND使用不同颜色的线并且先接好电源和地再接信号线避免热插拔引起意外。接好后最好用万用表量一下电压是否正确。3. 开发环境搭建与源码获取3.1 编译环境准备基于EASY EAI官方SDKRV1126B是ARM Cortex-A7架构我们需要在x86的PC上进行交叉编译。EASY EAI提供了打包好的Docker编译环境这大大简化了配置过程。获取SDK与Docker镜像首先你需要从EASY EAI官方或提供的网盘链接下载完整的SDK包和Docker镜像文件。这个过程可能比较耗时因为SDK通常有几个GB大小。导入Docker镜像在Ubuntu PC上使用docker load -i命令导入下载的镜像文件。启动编译容器按照文档进入SDK目录执行./run.sh脚本。这个脚本会挂载当前目录到容器内的/mnt目录并进入容器的交互式Shell。关键检查点进入容器后立刻检查两件事执行arm-linux-gnueabihf-gcc -v确认交叉编译工具链已正确安装并能输出版本信息。执行ls /mnt确认你的SDK源码目录已成功挂载进来。后续所有编译操作都必须在容器内进行并且依赖/mnt目录下的文件所以不要退出或卸载它。实操心得官方提供的run.sh脚本有时候会因为Docker版本或系统权限问题执行失败。常见的坑是“Permission denied”。我的解决方法是首先用sudo usermod -aG docker $USER将当前用户加入docker组并重新登录。如果还不行直接使用sudo docker run -it -v $(pwd):/mnt [IMAGE_NAME] /bin/bash命令手动启动容器其中[IMAGE_NAME]用docker images查到的镜像ID或名称替换。3.2 获取与编译SPI示例代码官方示例代码通常存放在网盘。下载后你需要将其复制到Docker容器内的工作目录。创建并进入工作目录cd /opt sudo mkdir -p EASY-EAI-Nano-TB/demo # 可能需要sudo权限 cd EASY-EAI-Nano-TB/demo这里使用/opt目录是官方的习惯你也可以放在/home下但要确保路径有读写权限。拷贝源码假设你在宿主机Windows/Mac的下载目录里有07_SPI.tar.gz。在Docker容器内可以通过共享文件夹或者用docker cp命令传进去。更简单的方法是在启动Docker容器时已经把宿主机的SDK目录挂载到了/mnt那么源码可能已经在/mnt的某个子目录里了。直接找到并解压即可。cp /mnt/path/to/your/download/07_SPI.tar.gz . tar -xzf 07_SPI.tar.gz cd 07_SPI编译源码执行编译脚本。./build.sh如果一切顺利你会在当前目录或指定的输出目录如/userdata下看到生成的可执行文件test-rfid,test-fram,test-spidev。部署到开发板编译脚本通常会自动通过scp或adb将可执行文件推送到开发板的/userdata目录。如果没有自动部署你需要手动操作# 假设开发板IP是192.168.1.100用户是root scp test-rfid root192.168.1.100:/userdata/注意事项编译时最常见的错误是“找不到头文件”或“链接库失败”。这通常是因为交叉编译工具链的sysroot路径设置不对或者必要的库文件如libpthread.so没有包含在工具链里。EASY EAI的Docker环境一般已经配置好。如果遇到问题检查build.sh脚本中的CFLAGS和LDFLAGS确保--sysroot指向了正确的SDK路径通常是/mnt下的某个目录。4. SPI用户空间编程深度解析4.1 打开与配置SPI设备操作SPI设备的第一步是打开设备文件并配置参数。我们来看test-spidev.c或rfid.c中的核心初始化函数。#include stdio.h #include fcntl.h #include unistd.h #include sys/ioctl.h #include linux/spi/spidev.h #include string.h int spi_init(const char *device, uint8_t mode, uint8_t bits, uint32_t speed, uint16_t delay) { int fd; // 1. 打开设备文件 fd open(device, O_RDWR); if (fd 0) { perror(Cant open SPI device); return -1; } // 2. 设置SPI通信模式 if (ioctl(fd, SPI_IOC_WR_MODE, mode) 0) { perror(Cant set SPI mode); close(fd); return -1; } // 读取模式以确认设置成功可选 uint8_t mode_read; if (ioctl(fd, SPI_IOC_RD_MODE, mode_read) 0) { perror(Cant get SPI mode); } else if (mode_read ! mode) { fprintf(stderr, SPI mode set error: expected %d, got %d\n, mode, mode_read); } // 3. 设置每字位数通常是8 if (ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, bits) 0) { perror(Cant set bits per word); close(fd); return -1; } // 4. 设置最大时钟速度Hz if (ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, speed) 0) { perror(Cant set max speed); close(fd); return -1; } // 5. 设置传输延迟us通常为0 // 注意这个delay是片选激活后到第一个时钟沿的延迟以及最后一个时钟沿后到片选失效的延迟。 // 有些设备需要这个时间来准备或稳定数据。 if (delay 0) { // 设置delay的ioctl可能不是标准SPI_IOC_WR_DELAY_US需要查内核驱动支持。 // 更常见的做法是在每次传输的spi_ioc_transfer结构体中设置delay_usecs字段。 } printf(SPI device %s opened successfully. Mode%d, Bits%d, Speed%d Hz\n, device, mode, bits, speed); return fd; }关键点解析open函数以读写方式打开/dev/spidev3.0这样的设备节点。ioctl函数这是配置SPI的核心。SPI_IOC_WR_MODE等是Linux内核定义的SPI控制命令。模式Mode必须与从设备一致。RC522用Mode 0所以这里mode参数传0。速度Speed单位是Hz。1000000表示1MHz。对于RC522初始调试可以用500kHz或1MHz稳定后再尝试提高。位宽Bits传8。延迟Delay大部分简单应用设为0即可。如果需要更精细的控制在后面的spi_ioc_transfer结构中。4.2 执行SPI数据传输理解spi_ioc_transfer配置好设备后真正的数据收发通过ioctl(fd, SPI_IOC_MESSAGE(N), transfer)完成其中transfer是一个或多个struct spi_ioc_transfer结构体。这是SPI编程中最核心的部分。#include linux/spi/spidev.h int spi_transfer(int fd, uint8_t *tx_buf, uint8_t *rx_buf, uint32_t len) { struct spi_ioc_transfer transfer {0}; transfer.tx_buf (unsigned long)tx_buf; transfer.rx_buf (unsigned long)rx_buf; transfer.len len; transfer.speed_hz 1000000; // 本次传输的速度可覆盖全局设置 transfer.delay_usecs 0; // 传输结束后的延迟单位微秒 transfer.bits_per_word 8; // 本次传输的位宽 transfer.cs_change 0; // 重要0表示传输后保持片选有效1表示传输后释放片选 // 对于全双工SPI一次transfer同时完成发送和接收。 // tx_buf和rx_buf可以指向同一个缓冲区原地读写也可以不同。 // 如果只读tx_buf可以为NULL但需要发送全0或特定数据看设备要求。 // 如果只写rx_buf可以为NULL。 int ret ioctl(fd, SPI_IOC_MESSAGE(1), transfer); // 发送1个transfer if (ret 1) { // 返回值是成功传输的字节数小于1表示出错 perror(SPI transfer failed); return -1; } return 0; // 成功 }结构体字段深度解读tx_buf/rx_buf发送和接收数据的缓冲区指针。SPI是全双工意味着主设备在发送tx_buf数据的同时也会从MISO线接收数据到rx_buf。即使你只想读数据也必须提供一个tx_buf通常是全0或命令字因为时钟是由主设备发出的没有发送就没有时钟也就无法接收。len传输的字节数。tx_buf和rx_buf指向的缓冲区大小至少为len。speed_hz和bits_per_word可以针对本次传输单独设置如果不设置或为0则使用设备打开时的全局设置。delay_usecs本次传输结束后片选保持有效的延迟时间微秒。某些设备需要数据稳定时间。cs_change这是极易出错的地方设置为0本次传输结束后不改变片选信号的状态。如果之前片选是有效的低电平那么它继续保持有效。这用于连续传输多个数据帧且不希望中间片选无效的情况。设置为1本次传输结束后释放片选信号拉高然后在下一个传输开始前再次拉低片选。这用于分隔两个独立的命令或数据包。RC522读写实战分析查看rc522.c中的PCD_WriteRegister和PCD_ReadRegister函数你会发现它们通常一次传输2个字节第一个字节是地址和读写命令最高位表示读/写第二个字节是数据。在连续读取多个寄存器时它们可能会将cs_change设为0在一次片选有效期间发送多个地址并读取多个数据以提高效率。4.3 RFID例程核心逻辑剖析让我们聚焦test-rfid.c的main函数理解一个完整的RFID卡读取流程是如何通过SPI实现的。int main() { // ... 变量初始化 ... int fd spi_init(/dev/spidev3.0, 0, 8, 1000000, 0); // 1. 初始化SPI rfid_init(fd); // 2. 初始化RC522芯片通过SPI发送一系列配置寄存器命令 while(1) { // 3. 寻卡发送寻卡命令PICC_REQIDL if(rfid_request(PICC_REQIDL, card_rev_buf) MI_OK) { // 4. 防冲突如果有多张卡获取其中一张的序列号 if(rfid_anticoll(card_serial_num) MI_OK) { // 5. 选卡选择这张卡进行后续操作 if(rfid_select(card_serial_num) MI_OK) { // 6. 认证使用密钥A或B对指定扇区进行认证 status rfid_auth_state(PICC_AUTHENT1A, sector_addr, key, card_serial_num); if(status MI_OK) { // 7. 读数据读取该扇区16字节数据 status rfid_read(sector_addr, data_buffer); if(status MI_OK) { // 打印数据 print_buff(data_buffer, 16); } } // 8. 休眠让卡片进入休眠状态 rfid_halt(); } } } usleep(500000); // 延时500ms再寻卡 } close(fd); return 0; }流程拆解与SPI交互初始化rfid_init(fd)内部通过SPI向RC522写入多个寄存器值设置其工作模式、射频参数、定时器等。这本质上是一系列spi_transfer调用发送写寄存器命令和值。寻卡RequestRC522通过天线发送特定格式的射频信号。如果有卡进入磁场卡片会回复一个ATQAAnswer to Request码。这个“发送命令-等待回复”的过程在驱动层是由RC522芯片自己完成的但MCU需要通过SPI读取RC522的内部FIFO缓冲区来获取ATQA值。所以rfid_request函数内部是先通过SPI向RC522发送“寻卡”命令然后循环通过SPI读取状态寄存器判断是否有数据最后再通过SPI读取FIFO得到结果。防冲突与选卡Anticoll Select如果有多张卡需要通过防冲突算法一般是基于序列号的位帧防冲突选出一张。RC522芯片硬件支持这部分算法MCU只需要通过SPI发送相应命令并读取序列号即可。rfid_select函数则是通过SPI发送“选卡”命令和序列号卡片会返回一个SAKSelect Acknowledge码确认选中。认证AuthenticationM1卡的数据安全是基于扇区的每个扇区有独立的密钥。在读/写某个扇区前必须先用密钥Key A或Key B进行认证。rfid_auth_state函数通过SPI向RC522发送认证命令、扇区地址、密钥和卡片序列号。认证过程是RC522和卡片之间的加密通信MCU只是发起命令。读/写数据认证成功后就可以通过SPI发送读/写命令、地址然后读取数据或写入数据到RC522的FIFO再由RC522通过射频与卡片完成数据交换。休眠Halt操作完成后发送休眠命令让卡片进入低功耗状态避免持续响应。整个过程中MCURV1126B与RC522之间的所有交互都是通过我们编写的spi_transfer函数操作/dev/spidev3.0这个设备文件来完成的。Linux SPI驱动帮我们处理了时钟生成、数据移位等底层硬件操作我们只需要关心“发什么命令”和“收什么数据”。5. 进阶应用与深度优化5.1 多设备管理与片选控制在实际项目中一条SPI总线上可能挂载多个设备比如同时连接RC522和一个SPI Flash。Linux的spidev驱动为每个(bus, cs)对创建一个设备节点例如/dev/spidev3.0和/dev/spidev3.1。在代码中你需要为每个设备分别调用open和配置。但是这里有一个重要的限制spidev驱动默认的片选控制是“自动”的即在每次ioctl(SPI_IOC_MESSAGE)传输前后驱动会自动拉低和拉高对应的片选线GPIO。这适用于大多数情况。然而有些设备可能需要非标准的片选时序比如需要在两次传输之间保持片选有效。需要用同一个片选信号控制多个设备通过额外的GPIO。片选信号需要与其他控制信号如复位、使能有严格的时序关系。解决方案使用cs_change字段如前所述在spi_ioc_transfer结构中将cs_change设为0可以在多个transfer之间保持片选有效。这对于发送一个多字节的命令包非常有用。手动GPIO控制如果自动片选不能满足要求可以考虑将SPI设备配置为“三线”模式无硬件片选并通过另一个GPIO口手动控制片选。但这需要内核驱动支持且可能影响性能。更常见的做法是仍然使用spidev的硬件片选但对于需要复杂协同的设备将其放在不同的SPI总线上或者使用其他通信方式如I2C。内核设备树Device Tree配置对于片选极性高有效还是低有效、时钟相位/极性的默认值等可以在内核设备树中为每个SPI设备节点进行静态配置。这样应用层打开spidev时一些参数就已经是预设好的。修改设备树需要重新编译内核属于驱动层开发范畴。5.2 性能优化与稳定性考量当SPI用于高速或持续数据传输时例如从SPI Flash读取大量数据性能优化就很重要。使用SPI_IOC_MESSAGE(N)进行批量传输ioctl调用本身有开销。与其为每个字节或每个小数据包调用一次ioctl不如将多个spi_ioc_transfer结构体组织成一个数组然后通过ioctl(fd, SPI_IOC_MESSAGE(N), transfer_array)一次提交。这允许驱动进行可能的优化如DMA准备并减少用户态到内核态的切换次数。RC522的例程中单次读写寄存器通常只传输2-3个字节所以没有这样用。但对于连续读多个扇区可以考虑优化。调整缓冲区与传输大小确保tx_buf和rx_buf是内存对齐的例如用malloc分配有时能提升DMA效率。单次传输的长度len也并非越大越好需要结合驱动和硬件的限制。时钟速度与信号完整性在RV1126B上SPI3的最高时钟可能达到50MHz甚至更高。但实际能达到多高取决于从设备极限RC522最高10MHz。PCB走线长线、过孔、靠近干扰源都会降低最高可靠速率。内核驱动配置有些平台驱动或设备树可能限制了分频系数。调试建议从低速如1MHz开始测试逐步提高直到出现通信错误然后留出20%-30%的余量作为工作频率。错误处理与重试机制SPI通信可能受到电磁干扰。在关键操作如认证、写数据后应该增加读取验证。对于读卡操作加入重试逻辑是必要的就像例程中的while(rfid_request(...) ! MI_OK numAtempt-- 0)。5.3 调试技巧与问题排查实录调试SPI问题逻辑分析仪或示波器是终极武器。如果没有可以依靠打印和代码分析。问题1打开设备文件失败 (open返回-1)可能原因设备节点不存在检查/dev/spidev3.0是否存在。不存在可能是内核未配置SPI驱动或设备树未启用该SPI控制器。权限不足默认/dev/spidev*设备文件可能属于root。解决方案a) 使用sudo运行程序b) 修改udev规则让特定用户组有权限推荐c) 使用chmod临时改权限不安全。排查命令ls -l /dev/spidev* # 查看设备节点及权限 dmesg | grep spi # 查看内核启动时SPI驱动的加载信息问题2SPI传输失败 (ioctl返回-1或实际传输字节数为0)可能原因模式、速度等参数设置失败检查ioctl(SPI_IOC_WR_MODE)等调用的返回值。硬件连接错误SCLK, MOSI, MISO, CS接错或虚焊。最容易被忽略的是GND未共地。电源问题用万用表测量RC522的VCC引脚确认是稳定的3.3V。软件排查在spi_transfer函数中加入详细打印输出每次传输的tx_buf和rx_buf内容。对于RC522可以尝试先读写一个已知的寄存器如版本寄存器来测试通信是否正常。问题3能通信但数据不对寻不到卡、认证失败可能原因模式不匹配这是头号杀手确认RC522和程序都使用Mode 0。字节序或位序问题SPI通常是MSB firstRC522也如此。但有些设备是LSB first。检查驱动或设备树配置。在spi_ioc_transfer中可以通过ioctl设置SPI_IOC_WR_LSB_FIRST但标准spidev不一定支持所有控制器。时序问题片选cs_change设置不当。例如RC522的某些命令需要连续发送多个字节中间片选不能释放。检查rfid.c中关键函数如PCD_WriteRegister的transfer.cs_change设置。射频参数问题RC522需要正确配置发射功率、接收增益等寄存器才能有效读卡。rfid_init函数里的初始化序列至关重要不要随意修改。不同批次的天线或卡片可能需要对少数寄存器进行微调。问题4程序运行一次后再次运行失败或需要复位可能原因SPI设备或RC522芯片状态未正确复位。确保在程序退出前或开始前调用了正确的关闭或复位函数。对于RC522rfid_halt()和软复位命令可以使其回到已知状态。对于Linux SPI驱动简单的close(fd)通常就够了但有些底层硬件可能需要更复杂的清理。一个实用的调试函数在代码中添加一个spi_debug_print函数在每次传输前后打印关键信息。void spi_debug_print(const char *tag, struct spi_ioc_transfer *xfer, uint8_t *tx, uint8_t *rx) { printf([%s] len%d, speed%d, cs_change%d, delay%d\n, tag, xfer-len, xfer-speed_hz, xfer-cs_change, xfer-delay_usecs); if (tx) { printf(TX: ); for(int i0; ixfer-len; i) printf(%02x , tx[i]); printf(\n); } if (rx) { printf(RX: ); for(int i0; ixfer-len; i) printf(%02x , rx[i]); printf(\n); } }6. 扩展思考从例程到产品化官方例程test-rfid是一个很好的起点但它是一个阻塞式的、循环寻卡的demo。在产品中我们需要考虑更多多线程与事件驱动不能让读卡循环一直阻塞主线程。应该创建一个专门的读卡线程或者使用select/poll监听某个GPIO中断如果RC522的IRQ引脚接到了开发板当有卡靠近时触发读卡操作。资源管理与异常恢复增加看门狗机制如果SPI通信长时间无响应尝试复位RC522通过GPIO控制其RST引脚并重新初始化。日志与状态上报将读卡成功、失败、认证错误等事件通过日志系统记录并上报给上层业务逻辑。配置化将SPI设备路径/dev/spidev3.0、模式、速度、RC522的射频参数等提取到配置文件中方便不同硬件版本或环境的适配。代码封装与复用将SPI操作和RC522协议操作封装成独立的、线程安全的库提供清晰的API如rfid_card_detect,rfid_read_sector供不同的应用程序调用。通过这个RV1126B SPI从入门到调试的完整过程我们可以看到Linux下操作SPI外设的核心在于理解spidev接口和ioctl的使用并结合具体外设的数据手册实现其通信协议。剩下的就是耐心调试和不断优化了。希望这篇长文能帮你少走弯路快速在RV1126B上玩转SPI。
RV1126B平台SPI驱动开发实战:从Linux spidev到RC522 RFID应用
发布时间:2026/5/23 12:19:05
1. 项目概述与核心思路最近在基于瑞芯微RV1126B平台开发一个物联网边缘设备其中一个核心功能就是通过SPI总线连接外部的RFID读卡模块实现身份识别。虽然Linux内核已经提供了完善的SPI驱动框架但真要在用户空间把SPI用起来尤其是结合具体的硬件比如RC522还是有不少细节需要琢磨。网上关于RV1126的SPI资料比较零散官方文档又偏向驱动层对应用开发者不够友好。我花了一周多时间从零开始把EASY EAI Nano-TB开发板上的SPI调通了期间踩了不少坑也总结了一套从环境搭建、代码编译到实际读写、问题排查的完整流程。这篇文章我就把这些实战经验系统地梳理出来目标就是让你拿到这块板子参照我的步骤能在半小时内跑通第一个SPI应用并理解背后的每一个参数和操作。简单来说SPISerial Peripheral Interface是一种高速、全双工、同步的串行通信总线在嵌入式领域用得非常多像Flash、传感器、显示屏、RFID模块等经常通过它和主控通信。在Linux下SPI设备被抽象成了/dev/spidevX.Y这样的字符设备文件应用层通过标准的文件IO接口open,read,write,ioctl就能操作这大大降低了开发难度。我们不需要去深究SPI控制器寄存器怎么配置重点在于理解如何通过ioctl设置正确的通信模式、速率等参数以及如何根据外设如RC522的特定协议组织数据帧。2. SPI基础与RV1126B硬件资源解析2.1 SPI通信核心概念快速回顾在动手写代码之前我们必须搞清楚SPI通信的几个关键参数这些参数直接决定了主设备RV1126B和从设备如RC522能否“对上话”。首先是最重要的通信模式Mode它由时钟极性CPOL和时钟相位CPHA共同决定共有4种模式Mode 0, 1, 2, 3。这个概念很多新手容易混淆我打个比方CPOL决定了时钟线在空闲时是“站着”高电平还是“蹲着”低电平CPHA则决定了数据是在时钟变化的“第一个边沿”还是“第二个边沿”被采样。你的从设备芯片手册里一定会写明它支持哪种模式主设备必须配置成相同的模式。比如RC522通常工作在Mode 0即CPOL0空闲时SCLK为低电平CPHA0数据在SCLK的上升沿被采样下降沿被移出。其次是比特率Speed也就是通信速度。这个值不是越高越好它受限于从设备的最大支持速率和PCB走线质量。RC522的SPI接口最高支持10MHz但在长线或干扰环境下适当降低速率比如1MHz可以提高稳定性。在Linux中我们通过ioctl设置spidev的max_speed_hz参数。还有一个参数是数据位宽Bits Per Word绝大多数SPI设备都是8位传输也就是一次传输1个字节。但有些设备可能支持16位或其它位宽需要根据数据手册确认。RV1126B的SPI控制器通常支持8位和16位。最后是字节序Endianness和片选CS控制。SPI通常是MSB最高有效位先发送。片选信号的控制方式也很重要Linux的spidev驱动默认会在每次传输前后自动拉低和拉高片选线。但对于某些特殊时序要求的设备可能需要通过GPIO手动控制片选这就需要我们绕过spidev的部分自动化功能或者使用spidev提供的“三线”模式等高级配置。2.2 RV1126B SPI硬件接口与设备节点映射EASY EAI Nano-TB开发板基于RV1126B芯片其SPI控制器资源比较丰富。根据板卡设计通常会有多个SPI控制器引出到排针上。理解/dev/spidev(bus).(cs)这个设备文件命名规则至关重要。bus代表SPI总线号对应芯片内部的SPI控制器编号如SPI0, SPI1, SPI3。每条总线包含一组SCLK、MOSI、MISO信号线。cs代表片选号对应控制器的片选信号线如CS0, CS1。同一条总线上可以挂多个设备通过不同的片选信号来区分。以我手头的板子为例启用SPI功能后在/dev目录下能看到/dev/spidev0.0 SPI0总线使用CS0片选。/dev/spidev0.1 SPI0总线使用CS1片选。/dev/spidev3.0 SPI3总线使用CS0片选。/dev/spidev3.1 SPI3总线使用CS1片选。注意具体哪个物理排针对应哪个spidev设备节点必须查阅你所用开发板的原理图或引脚复用表。例如我的RC522模块连接到了SPI3的CS0引脚那么我程序中操作的设备文件就是/dev/spidev3.0。接错了总线或者片选代码再怎么调也没用。2.3 硬件连接实战RC522模块接线RC522模块是一个典型的SPI从设备。与开发板的连接需要接对5根线不算电源SCLK (SPI时钟)- 连接至开发板SPI3的SCLK引脚。MOSI (主出从入)- 连接至开发板SPI3的MOSI引脚。MISO (主入从出)- 连接至开发板SPI3的MISO引脚。CS (片选)- 连接至开发板SPI3的CS0引脚。GND (地)- 连接至开发板GND。此外RC522还需要3.3V供电。务必确认开发板排针的电压是3.3VRV1126B的IO电压通常是3.3V与RC522匹配。如果接成5V可能会损坏模块。接线时的一个小技巧使用杜邦线连接时最好给电源线3.3V和GND使用不同颜色的线并且先接好电源和地再接信号线避免热插拔引起意外。接好后最好用万用表量一下电压是否正确。3. 开发环境搭建与源码获取3.1 编译环境准备基于EASY EAI官方SDKRV1126B是ARM Cortex-A7架构我们需要在x86的PC上进行交叉编译。EASY EAI提供了打包好的Docker编译环境这大大简化了配置过程。获取SDK与Docker镜像首先你需要从EASY EAI官方或提供的网盘链接下载完整的SDK包和Docker镜像文件。这个过程可能比较耗时因为SDK通常有几个GB大小。导入Docker镜像在Ubuntu PC上使用docker load -i命令导入下载的镜像文件。启动编译容器按照文档进入SDK目录执行./run.sh脚本。这个脚本会挂载当前目录到容器内的/mnt目录并进入容器的交互式Shell。关键检查点进入容器后立刻检查两件事执行arm-linux-gnueabihf-gcc -v确认交叉编译工具链已正确安装并能输出版本信息。执行ls /mnt确认你的SDK源码目录已成功挂载进来。后续所有编译操作都必须在容器内进行并且依赖/mnt目录下的文件所以不要退出或卸载它。实操心得官方提供的run.sh脚本有时候会因为Docker版本或系统权限问题执行失败。常见的坑是“Permission denied”。我的解决方法是首先用sudo usermod -aG docker $USER将当前用户加入docker组并重新登录。如果还不行直接使用sudo docker run -it -v $(pwd):/mnt [IMAGE_NAME] /bin/bash命令手动启动容器其中[IMAGE_NAME]用docker images查到的镜像ID或名称替换。3.2 获取与编译SPI示例代码官方示例代码通常存放在网盘。下载后你需要将其复制到Docker容器内的工作目录。创建并进入工作目录cd /opt sudo mkdir -p EASY-EAI-Nano-TB/demo # 可能需要sudo权限 cd EASY-EAI-Nano-TB/demo这里使用/opt目录是官方的习惯你也可以放在/home下但要确保路径有读写权限。拷贝源码假设你在宿主机Windows/Mac的下载目录里有07_SPI.tar.gz。在Docker容器内可以通过共享文件夹或者用docker cp命令传进去。更简单的方法是在启动Docker容器时已经把宿主机的SDK目录挂载到了/mnt那么源码可能已经在/mnt的某个子目录里了。直接找到并解压即可。cp /mnt/path/to/your/download/07_SPI.tar.gz . tar -xzf 07_SPI.tar.gz cd 07_SPI编译源码执行编译脚本。./build.sh如果一切顺利你会在当前目录或指定的输出目录如/userdata下看到生成的可执行文件test-rfid,test-fram,test-spidev。部署到开发板编译脚本通常会自动通过scp或adb将可执行文件推送到开发板的/userdata目录。如果没有自动部署你需要手动操作# 假设开发板IP是192.168.1.100用户是root scp test-rfid root192.168.1.100:/userdata/注意事项编译时最常见的错误是“找不到头文件”或“链接库失败”。这通常是因为交叉编译工具链的sysroot路径设置不对或者必要的库文件如libpthread.so没有包含在工具链里。EASY EAI的Docker环境一般已经配置好。如果遇到问题检查build.sh脚本中的CFLAGS和LDFLAGS确保--sysroot指向了正确的SDK路径通常是/mnt下的某个目录。4. SPI用户空间编程深度解析4.1 打开与配置SPI设备操作SPI设备的第一步是打开设备文件并配置参数。我们来看test-spidev.c或rfid.c中的核心初始化函数。#include stdio.h #include fcntl.h #include unistd.h #include sys/ioctl.h #include linux/spi/spidev.h #include string.h int spi_init(const char *device, uint8_t mode, uint8_t bits, uint32_t speed, uint16_t delay) { int fd; // 1. 打开设备文件 fd open(device, O_RDWR); if (fd 0) { perror(Cant open SPI device); return -1; } // 2. 设置SPI通信模式 if (ioctl(fd, SPI_IOC_WR_MODE, mode) 0) { perror(Cant set SPI mode); close(fd); return -1; } // 读取模式以确认设置成功可选 uint8_t mode_read; if (ioctl(fd, SPI_IOC_RD_MODE, mode_read) 0) { perror(Cant get SPI mode); } else if (mode_read ! mode) { fprintf(stderr, SPI mode set error: expected %d, got %d\n, mode, mode_read); } // 3. 设置每字位数通常是8 if (ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, bits) 0) { perror(Cant set bits per word); close(fd); return -1; } // 4. 设置最大时钟速度Hz if (ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, speed) 0) { perror(Cant set max speed); close(fd); return -1; } // 5. 设置传输延迟us通常为0 // 注意这个delay是片选激活后到第一个时钟沿的延迟以及最后一个时钟沿后到片选失效的延迟。 // 有些设备需要这个时间来准备或稳定数据。 if (delay 0) { // 设置delay的ioctl可能不是标准SPI_IOC_WR_DELAY_US需要查内核驱动支持。 // 更常见的做法是在每次传输的spi_ioc_transfer结构体中设置delay_usecs字段。 } printf(SPI device %s opened successfully. Mode%d, Bits%d, Speed%d Hz\n, device, mode, bits, speed); return fd; }关键点解析open函数以读写方式打开/dev/spidev3.0这样的设备节点。ioctl函数这是配置SPI的核心。SPI_IOC_WR_MODE等是Linux内核定义的SPI控制命令。模式Mode必须与从设备一致。RC522用Mode 0所以这里mode参数传0。速度Speed单位是Hz。1000000表示1MHz。对于RC522初始调试可以用500kHz或1MHz稳定后再尝试提高。位宽Bits传8。延迟Delay大部分简单应用设为0即可。如果需要更精细的控制在后面的spi_ioc_transfer结构中。4.2 执行SPI数据传输理解spi_ioc_transfer配置好设备后真正的数据收发通过ioctl(fd, SPI_IOC_MESSAGE(N), transfer)完成其中transfer是一个或多个struct spi_ioc_transfer结构体。这是SPI编程中最核心的部分。#include linux/spi/spidev.h int spi_transfer(int fd, uint8_t *tx_buf, uint8_t *rx_buf, uint32_t len) { struct spi_ioc_transfer transfer {0}; transfer.tx_buf (unsigned long)tx_buf; transfer.rx_buf (unsigned long)rx_buf; transfer.len len; transfer.speed_hz 1000000; // 本次传输的速度可覆盖全局设置 transfer.delay_usecs 0; // 传输结束后的延迟单位微秒 transfer.bits_per_word 8; // 本次传输的位宽 transfer.cs_change 0; // 重要0表示传输后保持片选有效1表示传输后释放片选 // 对于全双工SPI一次transfer同时完成发送和接收。 // tx_buf和rx_buf可以指向同一个缓冲区原地读写也可以不同。 // 如果只读tx_buf可以为NULL但需要发送全0或特定数据看设备要求。 // 如果只写rx_buf可以为NULL。 int ret ioctl(fd, SPI_IOC_MESSAGE(1), transfer); // 发送1个transfer if (ret 1) { // 返回值是成功传输的字节数小于1表示出错 perror(SPI transfer failed); return -1; } return 0; // 成功 }结构体字段深度解读tx_buf/rx_buf发送和接收数据的缓冲区指针。SPI是全双工意味着主设备在发送tx_buf数据的同时也会从MISO线接收数据到rx_buf。即使你只想读数据也必须提供一个tx_buf通常是全0或命令字因为时钟是由主设备发出的没有发送就没有时钟也就无法接收。len传输的字节数。tx_buf和rx_buf指向的缓冲区大小至少为len。speed_hz和bits_per_word可以针对本次传输单独设置如果不设置或为0则使用设备打开时的全局设置。delay_usecs本次传输结束后片选保持有效的延迟时间微秒。某些设备需要数据稳定时间。cs_change这是极易出错的地方设置为0本次传输结束后不改变片选信号的状态。如果之前片选是有效的低电平那么它继续保持有效。这用于连续传输多个数据帧且不希望中间片选无效的情况。设置为1本次传输结束后释放片选信号拉高然后在下一个传输开始前再次拉低片选。这用于分隔两个独立的命令或数据包。RC522读写实战分析查看rc522.c中的PCD_WriteRegister和PCD_ReadRegister函数你会发现它们通常一次传输2个字节第一个字节是地址和读写命令最高位表示读/写第二个字节是数据。在连续读取多个寄存器时它们可能会将cs_change设为0在一次片选有效期间发送多个地址并读取多个数据以提高效率。4.3 RFID例程核心逻辑剖析让我们聚焦test-rfid.c的main函数理解一个完整的RFID卡读取流程是如何通过SPI实现的。int main() { // ... 变量初始化 ... int fd spi_init(/dev/spidev3.0, 0, 8, 1000000, 0); // 1. 初始化SPI rfid_init(fd); // 2. 初始化RC522芯片通过SPI发送一系列配置寄存器命令 while(1) { // 3. 寻卡发送寻卡命令PICC_REQIDL if(rfid_request(PICC_REQIDL, card_rev_buf) MI_OK) { // 4. 防冲突如果有多张卡获取其中一张的序列号 if(rfid_anticoll(card_serial_num) MI_OK) { // 5. 选卡选择这张卡进行后续操作 if(rfid_select(card_serial_num) MI_OK) { // 6. 认证使用密钥A或B对指定扇区进行认证 status rfid_auth_state(PICC_AUTHENT1A, sector_addr, key, card_serial_num); if(status MI_OK) { // 7. 读数据读取该扇区16字节数据 status rfid_read(sector_addr, data_buffer); if(status MI_OK) { // 打印数据 print_buff(data_buffer, 16); } } // 8. 休眠让卡片进入休眠状态 rfid_halt(); } } } usleep(500000); // 延时500ms再寻卡 } close(fd); return 0; }流程拆解与SPI交互初始化rfid_init(fd)内部通过SPI向RC522写入多个寄存器值设置其工作模式、射频参数、定时器等。这本质上是一系列spi_transfer调用发送写寄存器命令和值。寻卡RequestRC522通过天线发送特定格式的射频信号。如果有卡进入磁场卡片会回复一个ATQAAnswer to Request码。这个“发送命令-等待回复”的过程在驱动层是由RC522芯片自己完成的但MCU需要通过SPI读取RC522的内部FIFO缓冲区来获取ATQA值。所以rfid_request函数内部是先通过SPI向RC522发送“寻卡”命令然后循环通过SPI读取状态寄存器判断是否有数据最后再通过SPI读取FIFO得到结果。防冲突与选卡Anticoll Select如果有多张卡需要通过防冲突算法一般是基于序列号的位帧防冲突选出一张。RC522芯片硬件支持这部分算法MCU只需要通过SPI发送相应命令并读取序列号即可。rfid_select函数则是通过SPI发送“选卡”命令和序列号卡片会返回一个SAKSelect Acknowledge码确认选中。认证AuthenticationM1卡的数据安全是基于扇区的每个扇区有独立的密钥。在读/写某个扇区前必须先用密钥Key A或Key B进行认证。rfid_auth_state函数通过SPI向RC522发送认证命令、扇区地址、密钥和卡片序列号。认证过程是RC522和卡片之间的加密通信MCU只是发起命令。读/写数据认证成功后就可以通过SPI发送读/写命令、地址然后读取数据或写入数据到RC522的FIFO再由RC522通过射频与卡片完成数据交换。休眠Halt操作完成后发送休眠命令让卡片进入低功耗状态避免持续响应。整个过程中MCURV1126B与RC522之间的所有交互都是通过我们编写的spi_transfer函数操作/dev/spidev3.0这个设备文件来完成的。Linux SPI驱动帮我们处理了时钟生成、数据移位等底层硬件操作我们只需要关心“发什么命令”和“收什么数据”。5. 进阶应用与深度优化5.1 多设备管理与片选控制在实际项目中一条SPI总线上可能挂载多个设备比如同时连接RC522和一个SPI Flash。Linux的spidev驱动为每个(bus, cs)对创建一个设备节点例如/dev/spidev3.0和/dev/spidev3.1。在代码中你需要为每个设备分别调用open和配置。但是这里有一个重要的限制spidev驱动默认的片选控制是“自动”的即在每次ioctl(SPI_IOC_MESSAGE)传输前后驱动会自动拉低和拉高对应的片选线GPIO。这适用于大多数情况。然而有些设备可能需要非标准的片选时序比如需要在两次传输之间保持片选有效。需要用同一个片选信号控制多个设备通过额外的GPIO。片选信号需要与其他控制信号如复位、使能有严格的时序关系。解决方案使用cs_change字段如前所述在spi_ioc_transfer结构中将cs_change设为0可以在多个transfer之间保持片选有效。这对于发送一个多字节的命令包非常有用。手动GPIO控制如果自动片选不能满足要求可以考虑将SPI设备配置为“三线”模式无硬件片选并通过另一个GPIO口手动控制片选。但这需要内核驱动支持且可能影响性能。更常见的做法是仍然使用spidev的硬件片选但对于需要复杂协同的设备将其放在不同的SPI总线上或者使用其他通信方式如I2C。内核设备树Device Tree配置对于片选极性高有效还是低有效、时钟相位/极性的默认值等可以在内核设备树中为每个SPI设备节点进行静态配置。这样应用层打开spidev时一些参数就已经是预设好的。修改设备树需要重新编译内核属于驱动层开发范畴。5.2 性能优化与稳定性考量当SPI用于高速或持续数据传输时例如从SPI Flash读取大量数据性能优化就很重要。使用SPI_IOC_MESSAGE(N)进行批量传输ioctl调用本身有开销。与其为每个字节或每个小数据包调用一次ioctl不如将多个spi_ioc_transfer结构体组织成一个数组然后通过ioctl(fd, SPI_IOC_MESSAGE(N), transfer_array)一次提交。这允许驱动进行可能的优化如DMA准备并减少用户态到内核态的切换次数。RC522的例程中单次读写寄存器通常只传输2-3个字节所以没有这样用。但对于连续读多个扇区可以考虑优化。调整缓冲区与传输大小确保tx_buf和rx_buf是内存对齐的例如用malloc分配有时能提升DMA效率。单次传输的长度len也并非越大越好需要结合驱动和硬件的限制。时钟速度与信号完整性在RV1126B上SPI3的最高时钟可能达到50MHz甚至更高。但实际能达到多高取决于从设备极限RC522最高10MHz。PCB走线长线、过孔、靠近干扰源都会降低最高可靠速率。内核驱动配置有些平台驱动或设备树可能限制了分频系数。调试建议从低速如1MHz开始测试逐步提高直到出现通信错误然后留出20%-30%的余量作为工作频率。错误处理与重试机制SPI通信可能受到电磁干扰。在关键操作如认证、写数据后应该增加读取验证。对于读卡操作加入重试逻辑是必要的就像例程中的while(rfid_request(...) ! MI_OK numAtempt-- 0)。5.3 调试技巧与问题排查实录调试SPI问题逻辑分析仪或示波器是终极武器。如果没有可以依靠打印和代码分析。问题1打开设备文件失败 (open返回-1)可能原因设备节点不存在检查/dev/spidev3.0是否存在。不存在可能是内核未配置SPI驱动或设备树未启用该SPI控制器。权限不足默认/dev/spidev*设备文件可能属于root。解决方案a) 使用sudo运行程序b) 修改udev规则让特定用户组有权限推荐c) 使用chmod临时改权限不安全。排查命令ls -l /dev/spidev* # 查看设备节点及权限 dmesg | grep spi # 查看内核启动时SPI驱动的加载信息问题2SPI传输失败 (ioctl返回-1或实际传输字节数为0)可能原因模式、速度等参数设置失败检查ioctl(SPI_IOC_WR_MODE)等调用的返回值。硬件连接错误SCLK, MOSI, MISO, CS接错或虚焊。最容易被忽略的是GND未共地。电源问题用万用表测量RC522的VCC引脚确认是稳定的3.3V。软件排查在spi_transfer函数中加入详细打印输出每次传输的tx_buf和rx_buf内容。对于RC522可以尝试先读写一个已知的寄存器如版本寄存器来测试通信是否正常。问题3能通信但数据不对寻不到卡、认证失败可能原因模式不匹配这是头号杀手确认RC522和程序都使用Mode 0。字节序或位序问题SPI通常是MSB firstRC522也如此。但有些设备是LSB first。检查驱动或设备树配置。在spi_ioc_transfer中可以通过ioctl设置SPI_IOC_WR_LSB_FIRST但标准spidev不一定支持所有控制器。时序问题片选cs_change设置不当。例如RC522的某些命令需要连续发送多个字节中间片选不能释放。检查rfid.c中关键函数如PCD_WriteRegister的transfer.cs_change设置。射频参数问题RC522需要正确配置发射功率、接收增益等寄存器才能有效读卡。rfid_init函数里的初始化序列至关重要不要随意修改。不同批次的天线或卡片可能需要对少数寄存器进行微调。问题4程序运行一次后再次运行失败或需要复位可能原因SPI设备或RC522芯片状态未正确复位。确保在程序退出前或开始前调用了正确的关闭或复位函数。对于RC522rfid_halt()和软复位命令可以使其回到已知状态。对于Linux SPI驱动简单的close(fd)通常就够了但有些底层硬件可能需要更复杂的清理。一个实用的调试函数在代码中添加一个spi_debug_print函数在每次传输前后打印关键信息。void spi_debug_print(const char *tag, struct spi_ioc_transfer *xfer, uint8_t *tx, uint8_t *rx) { printf([%s] len%d, speed%d, cs_change%d, delay%d\n, tag, xfer-len, xfer-speed_hz, xfer-cs_change, xfer-delay_usecs); if (tx) { printf(TX: ); for(int i0; ixfer-len; i) printf(%02x , tx[i]); printf(\n); } if (rx) { printf(RX: ); for(int i0; ixfer-len; i) printf(%02x , rx[i]); printf(\n); } }6. 扩展思考从例程到产品化官方例程test-rfid是一个很好的起点但它是一个阻塞式的、循环寻卡的demo。在产品中我们需要考虑更多多线程与事件驱动不能让读卡循环一直阻塞主线程。应该创建一个专门的读卡线程或者使用select/poll监听某个GPIO中断如果RC522的IRQ引脚接到了开发板当有卡靠近时触发读卡操作。资源管理与异常恢复增加看门狗机制如果SPI通信长时间无响应尝试复位RC522通过GPIO控制其RST引脚并重新初始化。日志与状态上报将读卡成功、失败、认证错误等事件通过日志系统记录并上报给上层业务逻辑。配置化将SPI设备路径/dev/spidev3.0、模式、速度、RC522的射频参数等提取到配置文件中方便不同硬件版本或环境的适配。代码封装与复用将SPI操作和RC522协议操作封装成独立的、线程安全的库提供清晰的API如rfid_card_detect,rfid_read_sector供不同的应用程序调用。通过这个RV1126B SPI从入门到调试的完整过程我们可以看到Linux下操作SPI外设的核心在于理解spidev接口和ioctl的使用并结合具体外设的数据手册实现其通信协议。剩下的就是耐心调试和不断优化了。希望这篇长文能帮你少走弯路快速在RV1126B上玩转SPI。