免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

Zynq SPI轮询与中断实战:从寄存器到代码调试全解析

Zynq SPI轮询与中断实战:从寄存器到代码调试全解析 记得第一次在Zedboard上调试SPI接口时踩的坑能写满一页A4纸。Xilinx Zynq平台的SPI控制器说实话不算复杂但正因为不算复杂很多细节在官方文档里往往一笔带过真正出问题时才知道深浅。这篇文章就把我在Zedboard上折腾SPI轮询和中断两种方式的完整经验写出来从寄存器操作到代码骨架从踩坑记录到调试技巧尽量全盘托出希望能让后来者少走点弯路。Zedboard用的是Zynq-7000系列的XC7Z020PS端SPI控制器是基于硬核实现的跟PL端用逻辑搭出来的SPI还不一样——前者的收发逻辑、时钟分频、FIFO管理全部固化在硅片上你只需要处理寄存器即可后者才需要操心状态机轮转和时序生成。很多朋友拿到板子就去找AXI Quad SPI的IP核但如果你用的是PS端MIO引出的SPI接口根本不需要做PL工程直接在SDK里写裸机代码就行。两者最大的区别在于PS端SPI的时钟极性、相位配置在控制器内部完成不涉及FPGA逻辑约束和布局布线调试周期短很多。回到正题。轮询和中断这两者解决的是同一个问题——怎么知道SPI什么时候能把数据发出去、什么时候收到了数据。区别在于轮询是你不停地去看标志位相当于站在厨房门口等水烧开每隔几秒揭一次锅盖费神费力但思路简单中断是你该干嘛干嘛水烧开了壶嘴会吹哨你再跑过去关火CPU利用率高但多了一道中断响应的流程。应用到实际问题里是把哪个方案当默认选择需要具体看业务场景。1. 内容整体设计与思路拆解1.1 为什么Zedboard适合做SPI通信学习Zedboard的PS端集成了两个SPI控制器SPI0和SPI1每个控制器支持两个片选硬件上可以挂多路SPI从设备。MIO的引脚分配里SPI的时钟、MOSI、MISO、片选都可以映射到不同的Bank具体在SDK的引脚配置界面里勾选即可。对于学习SPI轮询和中断来说Zedboard的硬核SPI比PL逻辑实现更合适因为不需要写Verilog也不需要做时序约束寄存器映射和操作流程跟单片机上的SPI驱动非常接近——会写STM32的SPI驱动Zynq PS端的SPI就是换了一套寄存器名字逻辑上是互通的。除了硬核SPI控制器Zedboard上还有一个常用的SPI从设备——板载的QSPI Flash虽然占用了独立的QSPI控制器接口但SPI0接口可以接外部扩展设备。实际调试时我常用杜邦线把SPI的MOSI和MISO短接做回环测试这个操作简单有效能在不接任何外部传感器的情况下验证SPI时序是否正常。回环测试通过后再接真实设备排查问题的范围会小很多。1.2 轮询与中断的核心取舍逻辑轮询模式的核心是一段循环代码——不断读取状态寄存器等待发送FIFO空或被接收FIFO非空。这种方式的优势在于可控性极强每个字节的收发时隙完全由软件决定状态异常时可以在循环里加打印、加延时、做重试写起来非常顺手。缺点也突出SPI时钟在硬件分频后通常工作在MHz级别如果单次传输的数据量大或者通信频繁轮询会占满整个CPU别的事情一概不干。中断模式则是把判断逻辑交给硬件——发送FIFO空、接收FIFO非空、传输完成等事件会触发中断请求CPU在中断服务函数里响应。好处是CPU利用率高主循环可以做其他任务通信在后台进行代价是中断处理有上下文切换开销ISR里需要先读状态寄存器判断中断来源再清标志代码结构比轮询复杂调试时也更容易出隐蔽问题。应用场景的区分其实很明确如果你要接的SPI从设备是单个传感器每次读取就几百字节读写频率不高轮询完全够用且代码更清晰如果你要接的是SPI屏幕刷新、SD卡读写、多设备轮询这类高频高吞吐场景中断或者DMA是必然选择。轮询是基础中断是进阶两者都掌握以后面对实际项目才能灵活选型。2. 核心细节解析与实操要点2.1 Zynq SPI控制器寄存器详解Zynq PS端SPI控制器的寄存器映射我总结成一张速查表开发过程中对照着用非常顺手寄存器偏移地址功能说明CR0x00控制寄存器SPI使能、Master/Slave模式、时钟极性相位、LoopbackSR0x04状态寄存器发送FIFO空/满、接收FIFO空/满、忙标志IER0x08中断使能寄存器RX_FIFO非空、TX_FIFO空、模式错误等中断使能IDR0x0C中断禁止寄存器IMD0x10中断屏蔽寄存器ISR0x14中断状态寄存器读后写1清除TXDR0x1C发送数据寄存器写数据到发送FIFORXDR0x20接收数据寄存器从接收FIFO读数据DRR0x28延时寄存器片选拉高后的延时配置SRR0x30软件复位寄存器这里重点讲一下SR状态寄存器。在Zynq的SPI控制器里SR的第7位是SPI控制器忙标志第4位是发送FIFO空标志第5位是发送FIFO满标志第6位是接收FIFO满标志第3位是接收FIFO空标志。轮询模式里最常盯的就是TX_FIFO_EMPTY和RX_FIFO_EMPTY这两个位。每个SPI控制器内部有128字节的FIFO这意味着你可以一次性写入一定数量的数据不必每写一个字节就等发送完成在发送的同时接收FIFO也在同步累积数据。理解了FIFO的缓冲机制才能明白为什么SPI收发时接收数据必须及时读取——不然接收FIFO满了状态位会置溢出标志后续数据直接丢弃。2.2 轮询模式必须要搞懂的标志位和代码骨架轮询收发的基本流程用一句话说就是写一个字节到发送FIFO等接收FIFO非空读一个字节出来。但实际操作比这句话要复杂因为SPI是全双工——主机发送一位的同时会接收一位所以只要你发了N个字节必然也会收到N个字节。很多新手写代码只发不收或者先发完再收然后发现读到的数据全是垃圾其实是因为时序错了。我的轮询模板是这样写的int spi_poll_transfer(u32 base_addr, u8 *tx_buf, u8 *rx_buf, int len) { int count 0; while (count len) { // 等待发送FIFO不满再写数据 while ((readl(base_addr SR_OFFSET) TX_FIFO_FULL)) ; writel(tx_buf[count], base_addr TXDR_OFFSET); // 关键点发送后必须等待接收FIFO非空再读 while ((readl(base_addr SR_OFFSET) RX_FIFO_EMPTY)) ; rx_buf[count] readl(base_addr RXDR_OFFSET); count; } return len; }这段代码的逻辑顺序是先检查发送FIFO是否已满不满则写入一个字节写入的同时SPI控制器开始移位输出从MISO采样的数据进入接收FIFO接着等待接收FIFO非空读取数据。如此循环直到所有字节传输完成。问题在于这个模板有个隐含前提——每写一个字节就能立刻读回一个字节。这在读寄存器类型的操作里没问题但如果从设备是那种先发命令字、再等待几个时钟周期、然后才有数据返回的类型会有一个“无效接收字节”的空档。此时必须根据从设备的时序要求发完命令后额外发几个哑元字节通常发0x00来产生时钟再读有效数据。2.3 中断模式下的SPI事件处理与ISR写法中断模式需要使能的中断源有两个发送FIFO空和接收FIFO非空。在Zynq的SPI控制器中IER寄存器第1位对应TX_FIFO_EMPTY中断使能第4位对应RX_FIFO_FULL中断使能第5位对应RX_FIFO_NOT_EMPTY中断使能。注意接收方向通常使用RX_FIFO_NOT_EMPTY即接收FIFO里有数据就通知CPU来读而不是等FIFO满了才通知——后者在数据量小于FIFO深度时会导致中断一直不触发数据滞留FIFO里。ISR的写法直接决定了系统是否稳定。我的经验是ISR里不要做复杂逻辑只做数据搬运和状态登记具体业务解析放到主循环或任务里做。另外SPI的中断状态寄存器ISR的清除机制是先读后写读完后对该位写1清除——千万不要写0否则清不掉中断反复触发系统直接卡死。这个坑我见过不止一次。void Spi_IsrHandler(void *CallbackRef) { u32 isr_status readl(SPI0_BASE ISR_OFFSET); // 接收FIFO非空中断 if (isr_status RX_FIFO_NOT_EMPTY_MASK) { while (!(readl(SPI0_BASE SR_OFFSET) RX_FIFO_EMPTY)) { if (rx_count MAX_LEN) { rx_buf[rx_count] readl(SPI0_BASE RXDR_OFFSET); } else break; } // 清除中断标志 writel(RX_FIFO_NOT_EMPTY_MASK, SPI0_BASE ISR_OFFSET); } // 发送FIFO空中断 if (isr_status TX_FIFO_EMPTY_MASK) { if (tx_count tx_len) { writel(tx_buf[tx_count], SPI0_BASE TXDR_OFFSET); } // 发送全部完成时可以禁止发送FIFO空中断 if (tx_count tx_len) { writel(TX_FIFO_EMPTY_MASK, SPI0_BASE IDR_OFFSET); } writel(TX_FIFO_EMPTY_MASK, SPI0_BASE ISR_OFFSET); } }中断模式的时序是主循环里先把第一个字节写入TXDR然后使能TX_FIFO_EMPTY和RX_FIFO_NOT_EMPTY中断后续的发送和接收全部由ISR驱动直到发送完成。常见的问题是发送FIFO在使能中断之前就已经为空了结果使能后中断会立即触发一次——这不是错误设计ISR时要容忍这种“预触发”。另外接收FIFO非空中断的优先级在Zynq GIC里可以单独配置通常建议设置为比其他外设中断略高避免SPI在高速收发时因延迟读取丢数据。2.4 初始化配置常见参数与注意事项初始化配置里最容易出问题的是时钟极性、时钟相位和分频系数。时钟极性CPOL决定SPI时钟空闲时的电平是低还是高时钟相位CPHA决定数据采样是在第一个跳变沿还是第二个跳变沿。这两个参数必须和从设备要求的时序严格匹配一般设备手册里会写“Mode 0”“Mode 3”等对应关系如下SPI模式CPOLCPHA特点Mode 000空闲低电平第一个边沿采样Mode 101空闲低电平第二个边沿采样Mode 210空闲高电平第一个边沿采样Mode 311空闲高电平第二个边沿采样大部分SPI Flash和传感器默认支持Mode 0和Mode 3。实测发现有些从设备宣称支持Mode 0但它在Mode 3下反而更稳定——因为高电平空闲时对信号线和电源线的串扰更不敏感。如果通信时数据偶尔错位试试把模式切换一下可能问题就消失了。Zynq的SPI时钟分频配置在CR寄存器中需要写分频值实际波特率等于PS参考时钟除以2和分频值。例如PS时钟频率为100MHz设置分频值为8则波特率100MHz / (2*8) 6.25MHz。需要注意SPI控制器允许的最高时钟频率Zynq的SPI控制器手册标称最高支持50MHz但实际通信时还要考虑从设备的上限比如某些SPI Flash最高40MHz某些传感器最高1MHz要在设备手册里确认后再配置否则会出现数据采样的建立保持时间不够的隐患。还有硬件片选和软件片选的取舍问题。Zynq的PS端SPI控制器硬件自动管理片选在单次传输开始前拉低、结束后拉高适合单次读写的场景。但如果你要连续读一大块数据比如一次读1KB总时间超过片选自动释放的阈值硬件片选会在中间释放导致从设备认为本次传输结束——这时就得手动控制GPIO拉片选即软件片选。实现方式是周期性地在主循环里不断发起单字节传输或空操作来保持片选有效或者干脆用MIO上的一组GPIO模仿片选信号。软件片选的好处是灵活坏处是时序需要严格设计一不留神就会出现“응답延迟”这类问题。3. 实操过程与核心环节实现3.1 环境准备与最小工程搭建开始实战之前必要的环境是Zedboard开发板、Xilinx SDK我习惯用2018.3版本新版本Vitis流程也类似、一根USB线用于JTAG下载和串口通信。硬件连接上最推荐先做回环测试——用杜邦线把SPI0的MISO和MOSI短接片选和地接好。这个步骤能帮你把SPI控制器自身的收发逻辑和外部设备隔离开后续接任何真实传感器出问题时你能快速定位是控制器配置问题还是外设问题。SDK里建工程的步骤不再赘述重点说下BSP配置。在Board Support Package的配置界面里勾选ps7_spi_0驱动选中spi_polled_example或者spi_intr_example模板SDK会自动生成中断初始化和SPI查找设备的代码。这里有个小坑Xilinx的驱动层XSpips在初始化时会自动设置默认的波特率和模式后续你需要通过XSpips_SetOptions和XSpips_SetClkDivider来覆盖默认值。具体来说XSPIPS_MASTER_OPTION、XSPIPS_MANUAL_CS_OPTION这些选项位控制传输模式的细节用之前务必查一遍头文件定义。3.2 轮询模式完整实现轮询模式我建议先跑一遍Xilinx自带的例子跑通以后再看下面的精简实现。精简版的轮询收发核心代码如下#include xparameters.h #include xspips.h #include sleep.h #define SPI_DEVICE_ID XPAR_XSPIPS_0_DEVICE_ID static XSpips SpiInstance; static u32 SpiConfigStatus; int spi_init(void) { XSpips_Config *config_ptr; config_ptr XSpips_LookupConfig(SPI_DEVICE_ID); if (config_ptr NULL) return -1; SpiConfigStatus XSpips_CfgInitialize(SpiInstance, config_ptr, config_ptr-BaseAddress); if (SpiConfigStatus ! XST_SUCCESS) return -1; // 设置为主模式 XSpips_SetOptions(SpiInstance, XSPIPS_MASTER_OPTION); // 设置SPI模式Mode 0CPOL0 CPHA0 XSpips_SetStatusHandler(SpiInstance, NULL, NULL); // 设置8位数据宽度 XSpips_SetDataWidth(SpiInstance, 8); // 设置分频系数假设PS时钟100MHz目标波特率约6.25MHz XSpips_SetClkDivider(SpiInstance, 8); return 0; } int spi_poll_loopback_test(void) { u8 tx_data[8] {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}; u8 rx_data[8] {0}; int i; // 使能SPI XSpips_Start(SpiInstance); // 片选选中从设备0回环时短接MISO/MOSI即可 XSpips_SetSlaveSelect(SpiInstance, 0x01); for (i 0; i 8; i) { // 写数据到发送FIFO XSpips_Send(SpiInstance, tx_data[i], 1); // 轮询等待接收FIFO非空 while (XSpips_IsRxFull(SpiInstance) 0) ; // 读取接收数据 XSpips_Recv(SpiInstance, rx_data[i], 1); } // 释放片选 XSpips_SetSlaveSelect(SpiInstance, 0x00); // 校验 for (i 0; i 8; i) { if (tx_data[i] ! rx_data[i]) return -1; } return 0; }这里的三个函数是Xilinx驱动库里的标准APIXSpips_Send往FIFO里写数据XSpips_Recv从FIFO里读数据XSpips_IsRxFull查询接收FIFO是否有数据可读。需要说明一点Xilinx驱动的底层会做环形缓冲管理多字节收发时内部逻辑比我前面贴的寄存器级轮询复杂如果你研究驱动源码会发现它其实也是读SR状态位再操作FIFO只是封装了一层友好接口。回环测试跑通后把MISO和MOSI的短接线拆掉接到真实的从设备上对比采集到的数据手册里的设备ID寄存器等参数进一步确认硬件时序和配置完全正确。3.3 中断模式完整实现中断模式在Zynq里比轮询多两步工作初始化GIC中断控制器、注册SPI中断服务函数。GIC的作用相当于总枢纽外设中断先到GICGIC再按优先级分发到CPU核。配置流程如下#include xscugic.h #include xil_exception.h #define SPI0_INTR_ID XPAR_XSPIPS_0_INTR static XScuGic InterruptController; int spi_intr_init(void) { XScuGic_Config *gic_config_ptr; // 初始化GIC gic_config_ptr XScuGic_LookupConfig(XPAR_SCUGIC_SINGLE_DEVICE_ID); XScuGic_CfgInitialize(InterruptController, gic_config_ptr, gic_config_ptr-CpuBaseAddress); // 注册SPI中断 XScuGic_Connect(InterruptController, SPI0_INTR_ID, (Xil_InterruptHandler)XSpips_InterruptHandler, SpiInstance); // 使能GIC中的SPI中断 XScuGic_Enable(InterruptController, SPI0_INTR_ID); // 使能全局异常 Xil_ExceptionInit(); Xil_ExceptionRegisterHandler( XIL_EXCEPTION_ID_IRQ_INT, (Xil_ExceptionHandler)XScuGic_InterruptHandler, InterruptController ); Xil_ExceptionEnable(); // SPI控制器内部中断使能 XSpips_SetStatusHandler(SpiInstance, (void *)SpiInstance, spi_status_handler); XSpips_IntrEnable(SpiInstance, XSPIPS_IXR_RX_OVR_ERR_MASK | XSPIPS_IXR_TX_EMPTY_MASK | XSPIPS_IXR_RX_FULL_MASK); return 0; }ISR里的处理逻辑与前面寄存器级代码一致只不过如果用Xilinx驱动库XSpips_InterruptHandler会先接管中断调度到用户注册的StatusHandler回调函数里。我在status handler里实现数据搬运static void spi_status_handler(XSpips *InstancePtr, u32 IntrEventMask) { u8 rx_byte; // 接收FIFO非空 if (IntrEventMask XSPIPS_IXR_RX_FULL_MASK) { while (XSpips_IsRxEmpty(InstancePtr) 0) { XSpips_Recv(InstancePtr, rx_byte, 1); if (rx_count BUFFER_SIZE) { rx_buffer[rx_count] rx_byte; } } } // 发送FIFO空 if (IntrEventMask XSPIPS_IXR_TX_EMPTY_MASK) { if (tx_count tx_len) { XSpips_Send(InstancePtr, tx_buffer[tx_count], 1); } else { // 发送完成中断禁止 XSpips_IntrDisable(InstancePtr, XSPIPS_IXR_TX_EMPTY_MASK); transfer_done 1; } } }值得注意的细节是Xilinx驱动库里中断状态寄存器是一次性读取后自动处理清除的但在回调函数里判断事件掩码时要注意可能同时出现多个中断标志位所以要用if判断而不是if-else if否则会漏掉同时发生的事件。另外如果接收数据量很大ISR里的while循环会占用较长时间此时从GIC角度看该CPU正忙于中断服务其他低优先级中断会延迟响应——设计时可以把接收循环拆成一字节进一次中断虽然中断次数多了但实时性更均衡。3.4 中断模式之轮询配合的混合策略实际工程中我经常用的一种混合策略是SPI接收用中断发送用轮询。具体来说发送方向由主循环直接写FIFO发送不用等发完而是注册一个RX_FIFO_NOT_EMPTY中断来接收响应数据。这种策略的好处是代码大幅简化避免了发送中断里判断发送完成条件的复杂逻辑同时接收方向因为有中断兜底不会丢数据。缺点是如果发送数据量特别大主循环会被SPI发送阻塞但这在单字节命令多字节响应的场景里完全够用。如果你的从设备通信模式主要是“主发短命令、从回长数据”这套混合策略值得一试。4. 常见问题与排查技巧实录4.1 板级回环测试不通的排查顺序如果回环测试连数据都不对别急着怀疑代码先按顺序排查第一步检查MIO引脚配置是否在SDK里勾选正确——Zynq的MIO是多功能引脚同一引脚可能是SPI也可能是UART或者GPIO配置错误时SPI控制器内部虽然初始化成功但引脚根本没连通第二步检查硬件连接杜邦线是否松动、短接的是不是正确的两个引脚MISO和MOSI在板子丝印上很容易看反第三步检查SPI时钟极性和相位——很多时候数据收发看起来正常但内容随机错位就是CPOL和CPHA跟回环测试的预期不一致第四步检查时钟频率把波特率降下来比如1MHz降低信号反射和干扰的影响确认问题不在传输速率上。按这个顺序排查大部分回环不通的问题能在半小时内解决。我见过最诡异的一个现象是回环测试中前几个字节对后面的字节全部错位——后来发现是SPI控制器的FIFO深度和驱动库的缓冲设置不匹配底层发送时缓冲溢出解决方案是减小单次传输的长度或者增加XSpips驱动库的FIFO深度配置。4.2 中断不触发和中断风暴的典型原因中断模式的常见问题比轮询模式多一个数量级。中断完全不触发的首要检查点是GIC层的配置——XScuGic_Enable是否调用、中断ID是否查对、Xil_ExceptionEnable是否执行这三个环节任何一个出错SPI控制器内部中断配置再正确CPU也收不到中断请求。另外SPI控制器内部的中断使能寄存器需要单独配置驱动库的XSpips_SetupHandler和XSpips_IntrEnable缺一不可——很多人配置了GIC就以为完工了忘了SPI自己还有个中断开关。中断反复触发、系统卡死的问题绝大多数出在中断状态清除环节。Xilinx的SPI控制器ISR清除机制是写1清除有些驱动函数封装后用的是读状态寄存器写状态寄存器的方式如果你直接往ISR写0中断标志永远清不掉硬件逻辑会认为中断条件还在于是继续往GIC发中断请求——这就是中断风暴。排查方法是在ISR入口读ISR寄存器打印出来如果读到的值一直是同一个非零值基本可以确定是清除逻辑写反了。4.3 数据拼接错位和字节丢失的经验教训数据错位是SPI调试里最磨人的问题表现是收到的数据整体移了一位或者混入了杂散数据。最常见的诱因是发送时没有配合从设备的时序——很多从设备要求主机在片选拉低后先发送命令、再延时若干周期、然后读数据如果主机在发完命令后立刻发下一个字节从设备会认为你还在发命令把后面数据当成命令字节处理响应就全错了。解决办法是在命令和读数据之间插入空操作传输发0x00或者用延时函数等待一段时间。具体延时值可以参考从设备手册中的最大响应时间如果手册不给就实测慢慢增加延时直到数据稳定。字节丢失则往往和FIFO溢出有关。接收FIFO深度128字节如果中断响应不及时连续传输超过128字节后FIFO满、数据溢出丢失。处理方案有三个一是缩短单次传输长度超过FIFO深度时分段传输二是改用DMA——Zynq的SPI控制器支持DMA接口PL端的DMA可以搬运大量数据而不经过CPU三是在ISR里加急读循环只要接收FIFO非空就立刻把数据搬到内存数组中让FIFO尽量保持“半空”状态。方案三最常用且代码改动最小。4.4 与其他总线方案对比何时选择DMA中断虽然比轮询高效但Zynq上SPI的极限吞吐还是得靠DMA。DMA的本质是硬件搬运数据——CPU设置好源地址、目的地址、长度后DMA控制器自动把FIFO里的数据搬到内存搬运完成再中断通知CPU。相比中断模式DMA每次传输的CPU介入从“每字节一次”减少到“每批一次”在大量数据快传场景下的CPU占用几乎可以忽略。我的经验是单次传输小于64字节且频率不高时中断模式足够单次传输超过256字节或通信速率超过10MHz时直接上DMA。DMA配置相比中断模式多了两个步骤初始化DMA通道配置通道为SPI的接收或发送方向在DMA控制器的中断服务里处理传输完成事件。Zynq的裸机DMA驱动在SDK里有现成模板复制修改即可但DMA描述符的分配和对齐要求比较严格——描述符地址要求32字节对齐缓冲区地址要求非缓存区或DMA一致性问题要先解决这些细节在文档里都有提但往往要踩过坑才能记得深。4.5 高负载场景下中断延迟的实测数据说一组我在Zedboard上实测的数据供参考。SPI时钟配置为6.25MHz单次传输128字节轮询模式下CPU占用接近100%中断模式下CPU占用约40%DMA模式下CPU占用不到5%。从延迟角度看轮询模式的响应时间是微秒级的——因为CPU一直在检查状态位没有任何延迟中断模式从硬件事件发生到ISR执行大约有1到3微秒的延迟包括GIC仲裁、CPU异常入口跳转、ISR现场保护等开销DMA模式的延迟则取决于DMA描述符链的处理速度通常在几十微秒级别。这个数据说明了一个关键问题轮询模式的实时性其实是最好的因为CPU随时待命中断模式引入了硬件仲裁和软件上下文切换的代价实时性略差但换来了高CPU利用率DMA模式的吞吐量最高、CPU占用最少但延迟最大。所谓“中断比轮询快”是一种误区——快是体现在CPU利用率和系统整体吞吐上单纯比单次响应延迟轮询反而有优势。理解了这一点项目中就知道怎么选了。5. 从Zynq到Linux端SPI驱动的延伸技巧5.1 Linux下SPI设备树配置的要点Zedboard也可以跑Linux而且很多实际产品都是基于Linux系统的这时候SPI驱动就走Linux内核的SPI子系统了。设备树里配置SPI控制器的节点需要在ps7_spi节点下设置status为okay并且配置从设备子节点。spi0 { status okay; compatible xlnx,xps-spi-2.00.a; num-cs 1; spidev0 { compatible rohm,dh2228fv; // 如果是开发用常见的是spidev reg 0; spi-max-frequency 6250000; spi-cpol; spi-cpha; }; };在Linux里把SPI从设备定义为spidev后用户态可以直接通过/dev/spidevX.Y操作总线这是快速验证SPI外接设备功能最方便的方式。但注意spidev在正式产品里不适合直接暴露给用户态推荐在内核里写完整驱动。这个延伸话题很大先记住一个核心概念Linux SPI设备树配置的两层结构——控制器节点描述SPI硬件寄存器地址、中断号和从设备节点描述片选、频率、极性和相位从设备的参数全部写在从设备节点里控制器驱动会从节点信息里自动配置SPI时序。5.2 软件拉片选在Linux下的实现思路前面提到硬件片选可能会在长传输中自动释放Linux下做软件拉片选可以绕开控制器对片选的管理改用GPIO管脚模拟片选信号。需要在设备树里把控制器自带的片选禁用配置为GPIOspi0 { status okay; cs-gpios gpio0 54 0; // 使用GPIO模拟片选 spidev0 { compatible spidev; reg 0; spi-max-frequency 1250000; }; };Linux内核在检测到cs-gpios属性后片选信号的拉低和拉高由GPIO子系统完成不再经过SPI控制器的片选逻辑。好处是片选持续时长完全由软件控制适合那种需要长时间保持片选有效才能读大块数据的传感器。5.3 用户态SPI调试的小工具Linux下调试SPI外设ioctl命令直接与设备交互非常高效推荐一个小工具——spidev_test内核源码的tools/spi目录里有现成代码。编译后向设备发送数据./spidev_test -D /dev/spidev0.0 -p \x01\x02\x03\x04 -s 6250000 -m 0参数说明-D指定设备节点-p指定发送数据-s指定速率-m指定SPI模式。用这个工具做回环测试非常方便前提还是先把MISO和MOSI短接然后看rx数据是否和tx一致。如果一致说明Linux内核的SPI子系统配置没有问题后续的故障范围就能缩小到具体的从设备驱动或硬件接线。6. 最后分享一点我的实用经验折腾完Zedboard的SPI轮询和中断我最深的感受是调试SPI这类同步串行协议靠逻辑分析仪远比靠示波器直观。逻辑分析仪能一次性抓多根线时钟、MISO、MOSI、片选软件里做协议解码后可以直接看到每一比特的含义和时序关系。示波器适合看信号质量但抓协议解析还是逻辑分析仪效率高。现在市面上几百块的逻辑分析仪配合免费软件对SPI这种低速协议绰绰有余实在没有逻辑分析仪时用GPIO翻转配合串口打印也能排查问题只是效率低一些。另外一个有用的习惯是每次测试时先在主程序里打印SPI控制器的当前配置寄存器值包括分频、模式、使能状态再开始收发数据。很多问题的根源是你以为自己配置了实际上某个函数调用失败返回后配置没生效打印一眼就能看出端倪。代码里加一条调试用的读取寄存器函数平时不调用排查问题时打开开关这种“留一手”的习惯能省不少时间。如果在实际调试中遇到了什么问题可以顺着我上面的排查思路走一遍。SPI通信的调试本质上就是“确认每一根线在每一个时刻的电平符合协议要求”轮询和中断只是我们观察和控制这些电平变化的不同手段本质是相通的
返回列表