1. 项目概述与核心挑战折腾Cyclone V SoC的HPSHard Processor System外设尤其是自己手动添加一个IP核并让Linux系统正确识别和驱动这事儿我前前后后搞了得有一个多月。核心目标很明确在一个现成的、运行着Linux的SoC FPGA工程里比如友晶DE1-SoC的参考设计通过Qsys给HPS的轻量级AXI总线h2f_lw_axi_master挂上一个SPI控制器然后让Linux系统在启动时自动加载驱动最终在/dev目录下生成可用的SPI设备节点方便上层应用进行读写操作。听起来流程很标准对吧理论上Qsys连线、Quartus编译、生成设备树、配置内核、更新SD卡一气呵成。但实际操作中我卡在了最后一步系统启动日志明明显示识别到了altera_spi这个控制器但/dev目录下就是找不到对应的spidev设备。这个问题困扰了我很久根本原因在于Qsys自动生成的设备树源文件.dts内容不完整缺少了关键属性导致内核的SPI驱动框架无法成功创建设备节点。本文将详细复盘整个流程重点拆解这个“坑”以及如何填平它希望能帮到同样在SoC FPGA软硬件协同设计中摸索的朋友。2. 硬件系统搭建与Qsys配置2.1 工程基础与IP添加我的起点是一个已经能正常启动Linux的DE1-SoC工程例如其DE1_SOC_Linux_FB工程。这一步很关键它保证了HPS、DDR、时钟、复位等基础子系统是正常工作的我们只需要做“加法”。首先打开工程的Qsys系统。在IP Catalog中搜索并添加SPI (3 Wire Serial)核Altera的Avalon-MM SPI Controller。这里有个细节需要注意Qsys里可能同时存在SPI (3 Wire Serial)和SPI (Avalon-MM)它们协议略有不同。对于挂载到HPS AXI总线上通常选择SPI (Avalon-MM)因为它是一个内存映射的从设备符合AXI总线访问规范。添加后将其重命名为一个有意义的名称比如spi_0。接下来是配置IP核参数。最重要的参数是时钟和寄存器映射宽度。时钟Clock在Clock设置中需要设置SPI控制器模块的工作时钟clk和外部SPI接口的时钟sclk。sclk的频率就是我们最终SPI通信的时钟频率。这里我设定了目标频率为2MHz。但需要注意这个频率会由Qsys系统的主时钟分频得到实际生成的可能不是精确的2M比如我最终实测是1.92MHz这是正常的取决于输入时钟和分频系数。寄存器Registers确保数据宽度Data Width设置为8标准SPI通常按字节传输其他如时钟极性CPOL、时钟相位CPHA可以先保持默认后续在驱动中可以通过ioctl动态配置。配置完成后需要将这个SPI控制器的Avalon-MM Slave端口连接到HPS的h2f_lw_axi_master接口上。h2f_lw_axi_master是HPS通往FPGA逻辑的轻量级AXI总线专门用于连接这类低速外设。连线时注意地址映射Qsys会自动分配一个基地址记下这个地址例如0x00010000后续在设备树中会用到。最后将SPI核的export端口导出。主要是四个信号sclk时钟、mosi主机输出、miso主机输入、ss_n片选低有效。在Qsys中右键点击这些信号选择Export并命名。2.2 Quartus工程集成与“无引脚”测试方案在Qsys中生成系统后回到Quartus II。首先将新生成的.qsys文件或对应的.qip文件添加到工程中。然后在顶层Verilog或VHDL文件中实例化这个新系统模块并将刚才导出的SPI信号连接到顶层端口。这里我采用了一个“取巧”的测试方案由于我这个阶段的目标是验证软硬件链路和驱动加载是否成功而非驱动实际的外设如SPI Flash或传感器所以我并没有在.qsf约束文件中为这四个SPI信号分配具体的物理引脚。也就是说这些信号在FPGA内部是“悬空”的没有接到开发板的任何插针上。那么如何验证SPI控制器是否真的在工作呢我使用了Quartus内置的Signaltap II Logic Analyzer。在Signaltap中添加这四根SPI信号线作为采样信号。编译工程并生成.sof文件后通过JTAG下载到FPGA。当Linux启动后运行测试程序试图访问SPI设备时Signaltap就能实时抓取到这些内部信号线上的波形。如果能抓到符合SPI时序的波形就证明HPS已经成功通过驱动对FPGA里的SPI控制器发出了指令硬件链路是通的。这是一个非常有效的“黑盒”验证方法尤其适合在驱动开发早期、硬件连接尚未最终确定时使用。注意使用Signaltap需要占用额外的FPGA逻辑和存储器资源可能会影响布局布线结果。在最终产品中需要移除Signaltap逻辑。同时确保Signaltap的采样时钟与被测信号如SPI的clk同步或满足奈奎斯特采样定理否则可能抓不到正确波形。3. 设备树Device Tree的生成与关键修改这是整个过程中最核心、也最容易出问题的环节。设备树是Linux内核用来描述硬件拓扑结构的数据结构对于SoC FPGA这种动态可配置的硬件尤为重要。3.1 自动生成与初次编译在Quartus工程全编译并成功生成.sof文件后我们需要为这个新的硬件配置生成设备树文件。Altera/Intel提供了SOC EDS工具链。打开SOCEDS Command Shell切换到你的Quartus工程目录。通常参考工程会自带一个Makefile里面定义了生成dts设备树源文件和dtb设备树二进制 blob的规则。执行命令make dts这个命令会调用bsp-editor或sopc2dts工具根据你的.sopcinfo文件由Qsys生成自动创建soc_system.dts文件。这一步通常比较顺利。接着尝试编译dts为dtbmake dtb这里我遇到了第一个错误Error: soc_system.dts:xxx.xxx: Label or path pll_stream not found这个错误提示pll_stream这个节点不存在。检查我的Qsys系统确实有一个名为pll_stream的PLL IP但它只用于为FPGA内部的某个视频流处理模块提供时钟并没有连接到HPS的AXI总线上理论上Linux不需要管理它。这个错误是工具链自动解析时产生的误报。解决方法我们可以手动使用设备树编译器dtc并添加-fforce参数来忽略这个错误强制生成dtb。dtc -I dts -O dtb -o soc_system.dtb -f soc_system.dts这样soc_system.dtb文件就生成了。将其重命名为SD卡启动分区所要求的名字通常是socfpga.dtb并替换SD卡中的原有文件。3.2 对比分析与手动修补按照上述流程更新SD卡并启动开发板后我在内核启动日志中看到了altera_spi ff200100.spi: Altera SPI Controller probed这令人振奋说明内核已经发现了这个位于地址0xff200100这是我的基地址的SPI控制器并且匹配到了altera_spi这个驱动。然而/dev目录下空空如也没有spidev设备节点。问题出在哪里关键在于内核驱动“探测probe”成功只意味着控制器本身被识别。但要为这个控制器下的每个SPI设备比如一个SPI Flash芯片创建用户空间可访问的节点还需要设备树提供更详细的信息特别是spidev子节点的描述。我对比了网友提供的一个成功案例的dts片段和我自动生成的dts片段发现了天壤之别。自动生成的dts片段不完整spi_0: spi0xff200100 { compatible altr,spi-1.0; reg 0xff200100 0x20; interrupt-parent intc; interrupts 0 43 4; clocks clk_0; };手动修改后的完整dts片段spi_0: spi0xff200100 { compatible altr,spi-16.1, altr,spi-1.0; reg 0xff200100 0x20; interrupt-parent intc; interrupts 0 43 4; clocks clk_0; #address-cells 1; #size-cells 0; bus-num 0; num-chipselect 1; status okay; spidev0: spidev0 { compatible rohm,dh2228fv; reg 0; spi-max-frequency 1000000; }; };关键差异解析compatible属性增加了altr,spi-16.1。这有助于匹配更特定版本的驱动。不同版本的Quartus/IP核可能需要不同的字符串altr,spi-1.0是通用匹配项。#address-cells和#size-cells这是必须添加的它们定义了SPI总线子节点的寻址方式。#address-cells 1表示子节点的reg属性用一个32位数字表示地址即片选索引。#size-cells 0表示没有大小字段。没有这两行内核的SPI子系统无法正确解析子节点。bus-num指定这是系统中的第几个SPI总线。如果只有一个SPI控制器设为0。num-chipselect控制器支持的片选信号数量。我的IP核配置了一个片选所以是1。status “okay”明确启用该设备。spidev子节点这是创建/dev/spidevX.Y设备的关键。compatible “rohm,dh2228fv”这是一个Linux内核中通用的SPI用户空间设备兼容字符串。使用它内核会自动加载spidev驱动并创建设备节点。注意在生产环境中如果连接的是具体器件如Flash应使用该器件的具体兼容字符串。reg 0指定该设备连接在哪个片选上CS0。spi-max-frequency 1000000指定该设备支持的最大SPI时钟频率1MHz。这个值必须设置且不能超过控制器和物理设备的能力。它也是驱动进行时钟分频的依据。3.3 解决节点冲突与最终编译我将上述完整片段手动添加到我的soc_system.dts文件中spi_0节点的大括号内。然后再次执行dtc编译命令却遇到了第二个错误ERROR (duplicate_label): Duplicate label spidev0 on /sopc0/spi0xff200100/spidev0 and /sopc0/bridge0xc0000000/spi0x1000100e0/spidev0错误提示标签spidev0重复了。搜索dts文件发现除了我添加的SPI_0HPS内部还有一个硬核SPI控制器spi0x1000100e0它下面已经定义了一个spidev0节点。设备树中每个节点的标签label必须是全局唯一的。解决方法将我添加的节点标签和节点名修改为唯一的即可。例如将spidev0: spidev0修改为spidev1: spidev0。这样标签spidev1就唯一了而0表示的是在该SPI控制器上的片选索引0不影响。修改后保存重新编译dtb成功。将新的dtb文件更新到SD卡同时别忘了将Quartus编译生成的.sof文件通过quartus_cpf工具转换为.rbf文件Raw Binary File也命名为soc_system.rbf并放入SD卡对应位置。.rbf是HPS在启动阶段配置FPGA逻辑的比特流文件。4. Linux内核驱动配置与编译要让Linux内核支持我们的SPI控制器需要确保相关驱动被编译进内核或作为模块。进入内核配置在Linux内核源码目录下执行make menuconfig。定位SPI驱动使用/键搜索SPI。找到并进入Device Drivers - SPI support。确保* Altera SPI Controller被选中打上*号表示编译进内核。这个驱动对应compatible属性中的“altr,spi-1.0”。同时在SPI support菜单下找到User mode SPI device driver support这个就是spidev驱动也必须选中。它对应spidev子节点中的“rohm,dh2228fv”。保存并编译保存配置通常为.config然后执行内核编译命令如make zImage -j4。编译完成后在arch/arm/boot/目录下得到新的zImage文件。更新启动文件将新编译的zImage和之前准备好的socfpga.dtb、soc_system.rbf一同拷贝到SD卡的FAT32启动分区。至此硬件配置.rbf、硬件描述.dtb、驱动支持zImage都已更新完毕。5. 系统启动验证与用户空间测试将SD卡插入DE1-SoC开发板上电启动。通过串口终端观察启动日志你应该能看到类似以下信息表明SPI控制器和spidev设备都被成功识别和创建altera_spi ff200100.spi: Altera SPI Controller probed ... spidev spi32766.0: spidev spi32766.0 attached to SPI controller spi32766注意这里的spi32766.0其中32766是系统动态分配的总线号0是片选号。登录系统后检查/dev目录ls -l /dev/spi*应该能看到一个名为spidev32766.0的设备节点。5.1 编写简单的C测试程序创建一个简单的C程序如spi_test.c来测试基本的读写功能。这个程序打开设备设置SPI模式、速度和位宽然后发送一字节数据并读取由于MISO未接实际设备读回的数据可能是随机的或0xFF。#include stdio.h #include fcntl.h #include unistd.h #include sys/ioctl.h #include linux/spi/spidev.h #include stdint.h int main() { int fd; const char *device /dev/spidev32766.0; uint8_t mode SPI_MODE_0; // CPOL0, CPHA0 uint8_t bits 8; uint32_t speed 500000; // 500 kHz uint8_t tx_buffer[1] {0xAA}; // 要发送的数据 uint8_t rx_buffer[1] {0}; // 1. 打开设备 fd open(device, O_RDWR); if (fd 0) { perror(Cant open device); return -1; } // 2. 设置SPI模式 if (ioctl(fd, SPI_IOC_WR_MODE, mode) -1) { perror(Cant set SPI mode); close(fd); return -1; } // 3. 设置每字节位数 if (ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, bits) -1) { perror(Cant set bits per word); close(fd); return -1; } // 4. 设置最大时钟速度 if (ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, speed) -1) { perror(Cant set max speed); close(fd); return -1; } // 5. 准备传输结构体 struct spi_ioc_transfer tr { .tx_buf (unsigned long)tx_buffer, .rx_buf (unsigned long)rx_buffer, .len 1, .delay_usecs 0, .speed_hz speed, .bits_per_word bits, }; // 6. 执行SPI传输 if (ioctl(fd, SPI_IOC_MESSAGE(1), tr) -1) { perror(SPI transfer failed); close(fd); return -1; } printf(Sent: 0x%02X, Received: 0x%02X\n, tx_buffer[0], rx_buffer[0]); // 7. 关闭设备 close(fd); return 0; }在开发板Linux系统上使用交叉编译工具链编译该程序arm-linux-gnueabihf-gcc -o spi_test spi_test.c然后运行./spi_test。5.2 使用Signaltap验证硬件波形在运行测试程序的同时Quartus II中的Signaltap II已经设置好并运行。你可以在Signaltap的波形窗口中看到mosi、sclk、ss_n信号上出现清晰的时序波形。ss_n拉低表示传输开始sclk出现脉冲mosi上出现对应0xAA二进制10101010的数据。由于miso没有连接其信号可能为高阻态或固定电平。看到这个波形就铁证如山从Linux用户空间发出的SPI访问指令已经穿过内核驱动、AXI总线成功抵达了FPGA中的SPI控制器并转换成了正确的物理时序。整个软硬件协同链路完全打通。6. 常见问题排查与深度解析6.1 内核启动日志分析要点遇到问题第一现场永远是内核启动日志dmesg。关注以下几点未发现设备如果完全没有altera_spi相关的打印首先检查dtb文件是否正确更新、SPI控制器的compatible属性是否与驱动匹配、寄存器地址reg是否正确。Probe失败如果看到probe failed可能原因是中断号interrupts设置错误、时钟clocks引用错误或未使能、或者寄存器映射长度reg不对。Probe成功但无spidev这就是我遇到的情况。重点检查设备树中SPI控制器节点下是否缺少#address-cells、#size-cells、spidev子节点及其spi-max-frequency属性。6.2 设备树语法与调试工具dtc工具除了编译dts-dtb还可以反编译dtb-dts来检查最终生成的设备树内容dtc -I dtb -O dts -o extracted.dts socfpga.dtb。内核中的设备树系统启动后可以在/proc/device-tree/目录下以文件系统形式查看当前使用的设备树信息。例如cat /proc/device-tree/sopc0/spiff200100/compatible可以查看SPI控制器的兼容字符串。of_API*在驱动开发中内核通过of_*系列函数如of_property_read_u32从设备树节点读取属性。确保dts中的属性名和驱动中查找的名字完全一致。6.3 地址映射与中断号确认寄存器地址reg在Qsys中连接h2f_lw_axi_master时分配的基地址是相对于该总线地址空间的偏移。在设备树中需要的是该外设在HPS视角下的物理地址。对于h2f_lw_axi_master上的设备其物理地址通常是0xff200000 分配的偏移地址。务必在Qsys或生成的map.h文件中确认。中断号interrupts在Qsys中查看SPI控制器的irq输出连接到了哪个中断线并确认该中断线在HPS GIC通用中断控制器中的编号。格式0 43 4通常表示中断类型SPI0 中断号 触发类型4高电平触发。中断号需要与Qsys系统和HPS硬件手册对应。6.4 性能与稳定性考量时钟频率设备树中spi-max-frequency和用户程序ioctl设置的speed最终都会受到IP核中SCLK分频系数的限制。实际通信频率是两者中较小的一个。建议在IP核中设置一个足够高的基准时钟以便在驱动中灵活分频。DMA使用对于大数据量传输可以考虑在Qsys中为SPI控制器启用DMA并在Linux驱动中配置DMA通道以减轻CPU负担。这需要在设备树中补充DMA相关属性。多设备与片选如果SPI总线上有多个从设备需要在设备树中为每个从设备定义一个spidevx子节点x为不同的片选索引并确保num-chipselect设置正确。用户空间程序通过打开不同的/dev/spidevB.CB为总线号C为片选号来访问不同设备。整个流程走通后回头看最关键的突破点就在于对设备树的理解从“自动生成即可”深入到“需要手动完善关键属性”。SoC FPGA的软硬件协同设计要求开发者必须同时具备FPGA硬件描述和Linux内核驱动的知识而设备树正是连接这两端的桥梁。这次踩坑填坑的经历虽然耗时但对理解整个嵌入式Linux系统的硬件抽象层运作机制价值巨大。
SoC FPGA中SPI控制器设备树配置与Linux驱动加载实战
1. 项目概述与核心挑战折腾Cyclone V SoC的HPSHard Processor System外设尤其是自己手动添加一个IP核并让Linux系统正确识别和驱动这事儿我前前后后搞了得有一个多月。核心目标很明确在一个现成的、运行着Linux的SoC FPGA工程里比如友晶DE1-SoC的参考设计通过Qsys给HPS的轻量级AXI总线h2f_lw_axi_master挂上一个SPI控制器然后让Linux系统在启动时自动加载驱动最终在/dev目录下生成可用的SPI设备节点方便上层应用进行读写操作。听起来流程很标准对吧理论上Qsys连线、Quartus编译、生成设备树、配置内核、更新SD卡一气呵成。但实际操作中我卡在了最后一步系统启动日志明明显示识别到了altera_spi这个控制器但/dev目录下就是找不到对应的spidev设备。这个问题困扰了我很久根本原因在于Qsys自动生成的设备树源文件.dts内容不完整缺少了关键属性导致内核的SPI驱动框架无法成功创建设备节点。本文将详细复盘整个流程重点拆解这个“坑”以及如何填平它希望能帮到同样在SoC FPGA软硬件协同设计中摸索的朋友。2. 硬件系统搭建与Qsys配置2.1 工程基础与IP添加我的起点是一个已经能正常启动Linux的DE1-SoC工程例如其DE1_SOC_Linux_FB工程。这一步很关键它保证了HPS、DDR、时钟、复位等基础子系统是正常工作的我们只需要做“加法”。首先打开工程的Qsys系统。在IP Catalog中搜索并添加SPI (3 Wire Serial)核Altera的Avalon-MM SPI Controller。这里有个细节需要注意Qsys里可能同时存在SPI (3 Wire Serial)和SPI (Avalon-MM)它们协议略有不同。对于挂载到HPS AXI总线上通常选择SPI (Avalon-MM)因为它是一个内存映射的从设备符合AXI总线访问规范。添加后将其重命名为一个有意义的名称比如spi_0。接下来是配置IP核参数。最重要的参数是时钟和寄存器映射宽度。时钟Clock在Clock设置中需要设置SPI控制器模块的工作时钟clk和外部SPI接口的时钟sclk。sclk的频率就是我们最终SPI通信的时钟频率。这里我设定了目标频率为2MHz。但需要注意这个频率会由Qsys系统的主时钟分频得到实际生成的可能不是精确的2M比如我最终实测是1.92MHz这是正常的取决于输入时钟和分频系数。寄存器Registers确保数据宽度Data Width设置为8标准SPI通常按字节传输其他如时钟极性CPOL、时钟相位CPHA可以先保持默认后续在驱动中可以通过ioctl动态配置。配置完成后需要将这个SPI控制器的Avalon-MM Slave端口连接到HPS的h2f_lw_axi_master接口上。h2f_lw_axi_master是HPS通往FPGA逻辑的轻量级AXI总线专门用于连接这类低速外设。连线时注意地址映射Qsys会自动分配一个基地址记下这个地址例如0x00010000后续在设备树中会用到。最后将SPI核的export端口导出。主要是四个信号sclk时钟、mosi主机输出、miso主机输入、ss_n片选低有效。在Qsys中右键点击这些信号选择Export并命名。2.2 Quartus工程集成与“无引脚”测试方案在Qsys中生成系统后回到Quartus II。首先将新生成的.qsys文件或对应的.qip文件添加到工程中。然后在顶层Verilog或VHDL文件中实例化这个新系统模块并将刚才导出的SPI信号连接到顶层端口。这里我采用了一个“取巧”的测试方案由于我这个阶段的目标是验证软硬件链路和驱动加载是否成功而非驱动实际的外设如SPI Flash或传感器所以我并没有在.qsf约束文件中为这四个SPI信号分配具体的物理引脚。也就是说这些信号在FPGA内部是“悬空”的没有接到开发板的任何插针上。那么如何验证SPI控制器是否真的在工作呢我使用了Quartus内置的Signaltap II Logic Analyzer。在Signaltap中添加这四根SPI信号线作为采样信号。编译工程并生成.sof文件后通过JTAG下载到FPGA。当Linux启动后运行测试程序试图访问SPI设备时Signaltap就能实时抓取到这些内部信号线上的波形。如果能抓到符合SPI时序的波形就证明HPS已经成功通过驱动对FPGA里的SPI控制器发出了指令硬件链路是通的。这是一个非常有效的“黑盒”验证方法尤其适合在驱动开发早期、硬件连接尚未最终确定时使用。注意使用Signaltap需要占用额外的FPGA逻辑和存储器资源可能会影响布局布线结果。在最终产品中需要移除Signaltap逻辑。同时确保Signaltap的采样时钟与被测信号如SPI的clk同步或满足奈奎斯特采样定理否则可能抓不到正确波形。3. 设备树Device Tree的生成与关键修改这是整个过程中最核心、也最容易出问题的环节。设备树是Linux内核用来描述硬件拓扑结构的数据结构对于SoC FPGA这种动态可配置的硬件尤为重要。3.1 自动生成与初次编译在Quartus工程全编译并成功生成.sof文件后我们需要为这个新的硬件配置生成设备树文件。Altera/Intel提供了SOC EDS工具链。打开SOCEDS Command Shell切换到你的Quartus工程目录。通常参考工程会自带一个Makefile里面定义了生成dts设备树源文件和dtb设备树二进制 blob的规则。执行命令make dts这个命令会调用bsp-editor或sopc2dts工具根据你的.sopcinfo文件由Qsys生成自动创建soc_system.dts文件。这一步通常比较顺利。接着尝试编译dts为dtbmake dtb这里我遇到了第一个错误Error: soc_system.dts:xxx.xxx: Label or path pll_stream not found这个错误提示pll_stream这个节点不存在。检查我的Qsys系统确实有一个名为pll_stream的PLL IP但它只用于为FPGA内部的某个视频流处理模块提供时钟并没有连接到HPS的AXI总线上理论上Linux不需要管理它。这个错误是工具链自动解析时产生的误报。解决方法我们可以手动使用设备树编译器dtc并添加-fforce参数来忽略这个错误强制生成dtb。dtc -I dts -O dtb -o soc_system.dtb -f soc_system.dts这样soc_system.dtb文件就生成了。将其重命名为SD卡启动分区所要求的名字通常是socfpga.dtb并替换SD卡中的原有文件。3.2 对比分析与手动修补按照上述流程更新SD卡并启动开发板后我在内核启动日志中看到了altera_spi ff200100.spi: Altera SPI Controller probed这令人振奋说明内核已经发现了这个位于地址0xff200100这是我的基地址的SPI控制器并且匹配到了altera_spi这个驱动。然而/dev目录下空空如也没有spidev设备节点。问题出在哪里关键在于内核驱动“探测probe”成功只意味着控制器本身被识别。但要为这个控制器下的每个SPI设备比如一个SPI Flash芯片创建用户空间可访问的节点还需要设备树提供更详细的信息特别是spidev子节点的描述。我对比了网友提供的一个成功案例的dts片段和我自动生成的dts片段发现了天壤之别。自动生成的dts片段不完整spi_0: spi0xff200100 { compatible altr,spi-1.0; reg 0xff200100 0x20; interrupt-parent intc; interrupts 0 43 4; clocks clk_0; };手动修改后的完整dts片段spi_0: spi0xff200100 { compatible altr,spi-16.1, altr,spi-1.0; reg 0xff200100 0x20; interrupt-parent intc; interrupts 0 43 4; clocks clk_0; #address-cells 1; #size-cells 0; bus-num 0; num-chipselect 1; status okay; spidev0: spidev0 { compatible rohm,dh2228fv; reg 0; spi-max-frequency 1000000; }; };关键差异解析compatible属性增加了altr,spi-16.1。这有助于匹配更特定版本的驱动。不同版本的Quartus/IP核可能需要不同的字符串altr,spi-1.0是通用匹配项。#address-cells和#size-cells这是必须添加的它们定义了SPI总线子节点的寻址方式。#address-cells 1表示子节点的reg属性用一个32位数字表示地址即片选索引。#size-cells 0表示没有大小字段。没有这两行内核的SPI子系统无法正确解析子节点。bus-num指定这是系统中的第几个SPI总线。如果只有一个SPI控制器设为0。num-chipselect控制器支持的片选信号数量。我的IP核配置了一个片选所以是1。status “okay”明确启用该设备。spidev子节点这是创建/dev/spidevX.Y设备的关键。compatible “rohm,dh2228fv”这是一个Linux内核中通用的SPI用户空间设备兼容字符串。使用它内核会自动加载spidev驱动并创建设备节点。注意在生产环境中如果连接的是具体器件如Flash应使用该器件的具体兼容字符串。reg 0指定该设备连接在哪个片选上CS0。spi-max-frequency 1000000指定该设备支持的最大SPI时钟频率1MHz。这个值必须设置且不能超过控制器和物理设备的能力。它也是驱动进行时钟分频的依据。3.3 解决节点冲突与最终编译我将上述完整片段手动添加到我的soc_system.dts文件中spi_0节点的大括号内。然后再次执行dtc编译命令却遇到了第二个错误ERROR (duplicate_label): Duplicate label spidev0 on /sopc0/spi0xff200100/spidev0 and /sopc0/bridge0xc0000000/spi0x1000100e0/spidev0错误提示标签spidev0重复了。搜索dts文件发现除了我添加的SPI_0HPS内部还有一个硬核SPI控制器spi0x1000100e0它下面已经定义了一个spidev0节点。设备树中每个节点的标签label必须是全局唯一的。解决方法将我添加的节点标签和节点名修改为唯一的即可。例如将spidev0: spidev0修改为spidev1: spidev0。这样标签spidev1就唯一了而0表示的是在该SPI控制器上的片选索引0不影响。修改后保存重新编译dtb成功。将新的dtb文件更新到SD卡同时别忘了将Quartus编译生成的.sof文件通过quartus_cpf工具转换为.rbf文件Raw Binary File也命名为soc_system.rbf并放入SD卡对应位置。.rbf是HPS在启动阶段配置FPGA逻辑的比特流文件。4. Linux内核驱动配置与编译要让Linux内核支持我们的SPI控制器需要确保相关驱动被编译进内核或作为模块。进入内核配置在Linux内核源码目录下执行make menuconfig。定位SPI驱动使用/键搜索SPI。找到并进入Device Drivers - SPI support。确保* Altera SPI Controller被选中打上*号表示编译进内核。这个驱动对应compatible属性中的“altr,spi-1.0”。同时在SPI support菜单下找到User mode SPI device driver support这个就是spidev驱动也必须选中。它对应spidev子节点中的“rohm,dh2228fv”。保存并编译保存配置通常为.config然后执行内核编译命令如make zImage -j4。编译完成后在arch/arm/boot/目录下得到新的zImage文件。更新启动文件将新编译的zImage和之前准备好的socfpga.dtb、soc_system.rbf一同拷贝到SD卡的FAT32启动分区。至此硬件配置.rbf、硬件描述.dtb、驱动支持zImage都已更新完毕。5. 系统启动验证与用户空间测试将SD卡插入DE1-SoC开发板上电启动。通过串口终端观察启动日志你应该能看到类似以下信息表明SPI控制器和spidev设备都被成功识别和创建altera_spi ff200100.spi: Altera SPI Controller probed ... spidev spi32766.0: spidev spi32766.0 attached to SPI controller spi32766注意这里的spi32766.0其中32766是系统动态分配的总线号0是片选号。登录系统后检查/dev目录ls -l /dev/spi*应该能看到一个名为spidev32766.0的设备节点。5.1 编写简单的C测试程序创建一个简单的C程序如spi_test.c来测试基本的读写功能。这个程序打开设备设置SPI模式、速度和位宽然后发送一字节数据并读取由于MISO未接实际设备读回的数据可能是随机的或0xFF。#include stdio.h #include fcntl.h #include unistd.h #include sys/ioctl.h #include linux/spi/spidev.h #include stdint.h int main() { int fd; const char *device /dev/spidev32766.0; uint8_t mode SPI_MODE_0; // CPOL0, CPHA0 uint8_t bits 8; uint32_t speed 500000; // 500 kHz uint8_t tx_buffer[1] {0xAA}; // 要发送的数据 uint8_t rx_buffer[1] {0}; // 1. 打开设备 fd open(device, O_RDWR); if (fd 0) { perror(Cant open device); return -1; } // 2. 设置SPI模式 if (ioctl(fd, SPI_IOC_WR_MODE, mode) -1) { perror(Cant set SPI mode); close(fd); return -1; } // 3. 设置每字节位数 if (ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, bits) -1) { perror(Cant set bits per word); close(fd); return -1; } // 4. 设置最大时钟速度 if (ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, speed) -1) { perror(Cant set max speed); close(fd); return -1; } // 5. 准备传输结构体 struct spi_ioc_transfer tr { .tx_buf (unsigned long)tx_buffer, .rx_buf (unsigned long)rx_buffer, .len 1, .delay_usecs 0, .speed_hz speed, .bits_per_word bits, }; // 6. 执行SPI传输 if (ioctl(fd, SPI_IOC_MESSAGE(1), tr) -1) { perror(SPI transfer failed); close(fd); return -1; } printf(Sent: 0x%02X, Received: 0x%02X\n, tx_buffer[0], rx_buffer[0]); // 7. 关闭设备 close(fd); return 0; }在开发板Linux系统上使用交叉编译工具链编译该程序arm-linux-gnueabihf-gcc -o spi_test spi_test.c然后运行./spi_test。5.2 使用Signaltap验证硬件波形在运行测试程序的同时Quartus II中的Signaltap II已经设置好并运行。你可以在Signaltap的波形窗口中看到mosi、sclk、ss_n信号上出现清晰的时序波形。ss_n拉低表示传输开始sclk出现脉冲mosi上出现对应0xAA二进制10101010的数据。由于miso没有连接其信号可能为高阻态或固定电平。看到这个波形就铁证如山从Linux用户空间发出的SPI访问指令已经穿过内核驱动、AXI总线成功抵达了FPGA中的SPI控制器并转换成了正确的物理时序。整个软硬件协同链路完全打通。6. 常见问题排查与深度解析6.1 内核启动日志分析要点遇到问题第一现场永远是内核启动日志dmesg。关注以下几点未发现设备如果完全没有altera_spi相关的打印首先检查dtb文件是否正确更新、SPI控制器的compatible属性是否与驱动匹配、寄存器地址reg是否正确。Probe失败如果看到probe failed可能原因是中断号interrupts设置错误、时钟clocks引用错误或未使能、或者寄存器映射长度reg不对。Probe成功但无spidev这就是我遇到的情况。重点检查设备树中SPI控制器节点下是否缺少#address-cells、#size-cells、spidev子节点及其spi-max-frequency属性。6.2 设备树语法与调试工具dtc工具除了编译dts-dtb还可以反编译dtb-dts来检查最终生成的设备树内容dtc -I dtb -O dts -o extracted.dts socfpga.dtb。内核中的设备树系统启动后可以在/proc/device-tree/目录下以文件系统形式查看当前使用的设备树信息。例如cat /proc/device-tree/sopc0/spiff200100/compatible可以查看SPI控制器的兼容字符串。of_API*在驱动开发中内核通过of_*系列函数如of_property_read_u32从设备树节点读取属性。确保dts中的属性名和驱动中查找的名字完全一致。6.3 地址映射与中断号确认寄存器地址reg在Qsys中连接h2f_lw_axi_master时分配的基地址是相对于该总线地址空间的偏移。在设备树中需要的是该外设在HPS视角下的物理地址。对于h2f_lw_axi_master上的设备其物理地址通常是0xff200000 分配的偏移地址。务必在Qsys或生成的map.h文件中确认。中断号interrupts在Qsys中查看SPI控制器的irq输出连接到了哪个中断线并确认该中断线在HPS GIC通用中断控制器中的编号。格式0 43 4通常表示中断类型SPI0 中断号 触发类型4高电平触发。中断号需要与Qsys系统和HPS硬件手册对应。6.4 性能与稳定性考量时钟频率设备树中spi-max-frequency和用户程序ioctl设置的speed最终都会受到IP核中SCLK分频系数的限制。实际通信频率是两者中较小的一个。建议在IP核中设置一个足够高的基准时钟以便在驱动中灵活分频。DMA使用对于大数据量传输可以考虑在Qsys中为SPI控制器启用DMA并在Linux驱动中配置DMA通道以减轻CPU负担。这需要在设备树中补充DMA相关属性。多设备与片选如果SPI总线上有多个从设备需要在设备树中为每个从设备定义一个spidevx子节点x为不同的片选索引并确保num-chipselect设置正确。用户空间程序通过打开不同的/dev/spidevB.CB为总线号C为片选号来访问不同设备。整个流程走通后回头看最关键的突破点就在于对设备树的理解从“自动生成即可”深入到“需要手动完善关键属性”。SoC FPGA的软硬件协同设计要求开发者必须同时具备FPGA硬件描述和Linux内核驱动的知识而设备树正是连接这两端的桥梁。这次踩坑填坑的经历虽然耗时但对理解整个嵌入式Linux系统的硬件抽象层运作机制价值巨大。