SPI子系统控制器驱动:基于i.MX6ULL的嵌入式Linux终端系统构建与多子系统控制器驱动开发—SPI控制器驱动开发)
本文是个人项目记录六SPI子系统控制器驱动基于i.MX6ULL的嵌入式Linux终端系统构建与多子系统控制器驱动开发—SPI控制器驱动部分的完整开发记录。基于i.MX6ULL平台编写ECSPI控制器驱动实现PIO轮询和中断驱动两种传输模式通过模块参数运行时切换。配合SPI设备驱动ADXL345传感器完成端到端功能验证使用Ftrace追踪函数调用链编写Shell脚本进行PIO/中断模式性能对比。项目及文档已开源在我的Github会随着逐步做完这个项目的BSP部分和驱动部分一直更新有完整的文档和源码欢迎Star⭐仓库地址 [https://github.com/illusOwo-ff/i.MX6ULL-bsp-driver-project]一、SPI子系统框架SPI协议SPI是全双工同步串行协议Master和Slave各有一个移位寄存器每个SCLK边沿交换1bit。四根信号SCLKMaster产生、MOSIMaster→Slave、MISOSlave→Master、CS片选低有效。四种模式由CPOL空闲时钟电平和CPHA采样边沿组合决定。ECSPI架构IMX6ULL有4个ECSPI控制器每个特性4个通道Channel 0~3对应4个CS各自独立配置CPOL/CPHATX/RX FIFO各64个entry32bit宽两级时钟分频SPI_CLK clk_per / ((PRE1) × 2^POST)SMC立即启动模式写TXDATA自动开始传输一次burst BL1个bit的移位交换中断机制ECSPI到GIC有独立中断线。本项目使用RRENRX Ready Enable中断每当RX FIFO有数据一个burst完成触发ISR。关键结构体spi_master使用填充spi_master的回调函数的方法内核接管消息队列管理驱动只负责硬件相关操作。成员填充值说明bus_numpdev-idSPI总线编号num_chipselect4ECSPI最多4个CSmode_bitsSPI_CPOL | SPI_CPHA | SPI_CS_HIGH支持的SPI模式bits_per_word_maskSPI_BPW_MASK(8) | SPI_BPW_MASK(32)支持的字长dev.of_nodepdev-dev.of_node*框架解析子节点创建spi_devicesetupmy_spi_setup配置SPI设备的模式/频率set_csmy_spi_set_cs选择片选通道transfer_onemy_spi_transfer_one核心传输回调分配方式spi_alloc_master(pdev-dev, sizeof(struct my_spi_data))—— 在master后面附带分配私有数据空间用spi_master_get_devdata(master)获取。注册方式devm_spi_register_master(pdev-dev, master)—— devm版本自动管理注销。transfer_one的同步/异步完成模型transfer_one被框架调用后通过返回值告知框架传输是否完成返回0同步完成框架立即处理下一个transfer返回1异步完成驱动后续调用spi_finalize_current_transfer()通知框架返回负值错误自定义的私有数据结构structmy_spi_data{void__iomem*base;/* 寄存器基地址 */structclk*clk;/* ECSPI时钟 */intirq;/* 中断号 */intbytes_per_word;/* 1/2/4支持多字节宽度 *//* 中断模式专用 */structcompletionxfer_done;constu8*tx_buf;u8*rx_buf;inttx_remain;/* 剩余word数 */intrx_remain;};模块参数PIO/中断模式切换staticbool use_irqfalse;module_param(use_irq,bool,0444);加载时选择insmod my_spi.ko use_irq1。权限0444表示加载后只读不能运行时修改。二、completion机制completion是内核同步机制用于等待事件发生。与锁的区别锁保护数据completion等待事件。核心APIinit_completion首次初始化reinit_completion复用前重置只重置done计数器wait_for_completion_timeout睡眠等待带超时保护complete通知完成可在ISR中安全调用本项目中transfer_irq调reinit_completion→wait_for_completion_timeout等待ISR完成后调complete唤醒。三、NXP手册硬件结构取自NXP官方芯片手册792页寄存器偏移表寄存器偏移用途RXDATA0x00接收数据只读TXDATA0x04发送数据只写CONREG0x08控制EN/SMC/XCH/CS/BL/PRE/POST/CMCONFIGREG0x0C模式配置CPOL/CPHA/SS_CTL/SS_POLINTREG0x10中断使能控制STATREG0x18状态TE/TF/RR/TC等四、函数实现逻辑setup配置SPI设备参数回调读CONFIGREG → 根据spi-mode设置对应channel的CPOL/CPHA/SS_POL位SS_CTL设为0CS在burst间保持低电平→ 写回CONFIGREG读CONREG → 设置对应channel的CM位为1Master模式→ 写回CONREGset_cs片选控制回调读CONREG → 清除CS字段 → 设置CSspi-chip_select → 写回CONREG实际CS引脚电平由GPIO控制使用GPIO CS方案SPI框架自动toggle。configure_xfer公共辅助——时钟和BL配置目标频率 xfer-speed_hz若为0则用spi-max_speed_hz 源时钟 clk_get_rate(data-clk) 遍历post0~15 pre DIV_ROUND_UP(源时钟, 目标频率 × 2^post) - 1 如果pre ≤ 15 → 找到跳出 BL bits_per_word - 18bit模式BL7 读CONREG → 清PRE/POST/BL → 写入新值 → 写回CONREGtransfer_one分发函数回调调用configure_xfer配置时钟和BL计算bytes_per_word bits_per_word / 8根据use_irq调用transfer_pio或transfer_irq辅助函数write_one_word / read_one_word支持8/16/32bit字长根据bytes_per_word用switch-case处理不同宽度的buffer访问和掩码。每次调用处理一个word的发送/接收并移动buffer指针、递减remain计数。transfer_pioPIO轮询传输设SMC1写TXDATA自动开始burst 设置tx_buf/rx_buf/tx_remain/rx_remain 循环 len/bytes_per_word 次 write_one_word写TXDATA自动开始传输 轮询等待STATREG的RR位1带超时保护 read_one_word读RXDATA 等待TC位1 → 写1清除TC 返回0transfer_irq中断模式传输设SMC1 保存tx_buf/rx_buf/tx_remain/rx_remain到私有数据 reinit_completion 先预填充TX FIFO 循环min(tx_remain, 64)次 write_one_word 再使能RREN中断 wait_for_completion_timeout1秒超时 清TC → 返回0先预填再使能中断SMC1下写TXDATA立即开始传输。如果先使能中断再预填ISR可能在预填循环中间被触发ISR和预填循环同时修改tx_buf/tx_remain造成竞争条件。先填完再使能中断避免竞争。ISR中断处理函数批量读RXwhile(RR) → read_one_word 补充TXwhile(tx_remain 0 !TF) → write_one_word 如果rx_remain 0 → 禁止中断 → complete() 返回IRQ_HANDLED两种工作模式len ≤ 64预填充了所有数据ISR只读RX不补TXlen 64ISR读RX并补TX多次ISR调用直到全部完成五、SPI设备驱动ADXL345ADXL345硬件要点三轴加速度传感器SPI Mode 0最大5MHz。SPI通信协议第一字节是地址字节bit7R/Wbit6MB多字节标志bit5~0寄存器地址。Device ID寄存器(0x00)固定返回0xE5。加速度数据在0x32~0x376字节小端序int16_t。SPI设备驱动结构使用SPI总线驱动模型spi_driverprobe里注册字符设备probe spi_setup配置Mode 0/1MHz/8bit → 读Device ID验证(spi_write_then_read发0x80收0xE5) → 初始化ADXL345(写DATA_FORMAT0x00, POWER_CTL0x08) → register_chrdev class_create device_create → /dev/adxl345 read spi_write_then_read(spi, 0xF2, 1, raw, 6) → 组合成int16_t accel[3] → copy_to_user应用程序open(/dev/adxl345)→read(fd,accel,6)→ printf X/Y/Z → close板子平放时Z轴≈2561g在±2g/10bit下的值X/Y接近0。六、设备树配置引脚选择使用ECSPI1引脚选择CSI_DATA组CSI已disabled无冲突信号PadMUX宏ALT值SCLKCSI_DATA04MX6UL_PAD_CSI_DATA04__ECSPI1_SCLK3CS(GPIO)CSI_DATA05MX6UL_PAD_CSI_DATA05__GPIO4_IO265MOSICSI_DATA06MX6UL_PAD_CSI_DATA06__ECSPI1_MOSI3MISOCSI_DATA07MX6UL_PAD_CSI_DATA07__ECSPI1_MISO3我的板级设备树覆盖iomuxc { pinctrl_my_ecspi1: myecspi1grp { fsl,pins MX6UL_PAD_CSI_DATA04__ECSPI1_SCLK 0x100b1 MX6UL_PAD_CSI_DATA05__GPIO4_IO26 0x100b1 MX6UL_PAD_CSI_DATA06__ECSPI1_MOSI 0x100b1 MX6UL_PAD_CSI_DATA07__ECSPI1_MISO 0x100b1 ; }; }; ecspi1 { compatible zxr-my_spi_master; pinctrl-names default; pinctrl-0 pinctrl_my_ecspi1; cs-gpios gpio4 26 GPIO_ACTIVE_LOW; /* GPIO CS */ status okay; adxl3450 { compatible zxr-myadx; reg 0; spi-max-frequency 1000000; }; };使用GPIO CS而非硬件SSECSPI硬件CS的行为依赖多个寄存器配合SS_CTL、burst时序等容易出问题。GPIO CS由SPI框架直接控制更可靠。实际工程中也普遍使用GPIO CS。七、测试验证测试步骤# PIO模式insmod spi_adapter.ko insmod spi_adx1345_drv.ko# dmesg确认: ADXL345 initializedDevice ID验证通过./adxl345_app# 输出: X: 12 Y: -8 Z: 256# 中断模式rmmod spi_adx1345_drvrmmod spi_adapter insmod spi_adapter.kouse_irq1insmod spi_adx1345_drv.ko ./adxl345_app# 读到相同数据cat/proc/interrupts|grepspi# 中断计数增长八、调试分析Ftrace function_graph追踪使用trace-cmd追踪一次2字节SPI传输的完整调用链trace-cmd record-pfunction_graph-gspi_sync ./spi_bench /dev/spidev0.021trace-cmd reportPIO模式调用链关键耗时spi_sync (1817μs) └─ spi_set_cs: 96μs ← GPIO CS assert └─ my_spi_transfer_one: 125μs ← 驱动核心 ├─ clk_get_rate: 34μs ├─ write→poll(12μs)→read ← 第1字节 └─ write→poll(12μs)→read ← 第2字节 └─ spi_set_cs: 97μs ← GPIO CS deassert └─ spi_finalize: 1072μs ← 框架调度开销中断模式调用链关键耗时spi_sync (4806μs) └─ my_spi_transfer_one: 3038μs ├─ 上下文切换: ~1295μs ← 中断开销主要来源 ├─ write_one_word ×2 ← 预填FIFO ├─ ISR第1次(21μs): read write ├─ ISR第2次(66μs): read complete(46μs) └─ wait_for_completion: 31μs ← 已完成直接返回分析结论2字节时中断模式的上下文切换开销~1.3ms远大于PIO的轮询等待24μs中断模式反而更慢。Shell脚本性能对比编写spi_bench测试程序通过spidev发起可配置字节数和迭代次数的SPI传输clock_gettime精确计时配合自动化脚本对比PIO/中断模式# 脚本逻辑对每种数据量# insmod use_irq0 → spi_bench 500次 → rmmod# insmod use_irq1 → spi_bench 500次 → rmmod性能数据数据量PIO延迟PIO吞吐中断延迟中断吞吐结论2B150μs12.9KB/s188μs10.4KB/sPIO快25%32B487μs64.0KB/s498μs62.7KB/s基本持平128B1729μs72.3KB/s1657μs75.4KB/s中断快4.3%结论交叉点在32~128B之间。小数据PIO更优无中断开销大数据中断更优FIFO批量预填充CPU释放。九、遇到的问题与解决问题1SPI传输读到错误数据(0xF2)现象加载设备驱动后Device ID读到0xF2而非0xE5。排查过程在transfer_pio加printk → 确认TX数据正确(0x80)、CONREG配置正确、但RX0xF2SPI回环测试(MOSI短接MISO) → 发0x80收0x80 → 排除控制器和驱动逻辑问题用devmem检查IOMUXC MUX寄存器 → 确认pinctrl正确ALT3ECSPI1用devmem检查INPUT SELECT寄存器 → 全部正确发现原因使用硬件CSECSPI的SS0引脚时CS信号未被正确断言。ECSPI硬件CS的行为依赖SS_CTL、burst时序等多个因素配合。我的解决改用GPIO CS方案。设备树中将SS0引脚复用为GPIO4_IO26cs-gpios指定该GPIOSPI框架直接控制CS电平。结论实际工程中SPI片选普遍使用GPIO而非硬件SS因为GPIO CS更可控可靠。问题2中断模式大数据量传输超时现象32字节以上传输中断模式报-ETIMEDOUT。排查2字节传输正常32字节超时。说明问题与数据量有关。发现原因transfer_irq中先使能RREN中断再预填充FIFO。SMC1下写TXDATA立即开始传输第一个字节8μs后传完触发ISR。ISR和预填充循环同时修改tx_buf/tx_remain产生竞争条件导致数据混乱、rx_remain永远不为0、completion永远不触发。我的解决在控制器驱动代码里交换顺序先预填充FIFO再使能RREN中断。预填充期间中断未使能ISR不会被触发避免竞争。结论在ISR和进程上下文共享数据时必须确保访问时序互斥。使能中断的时机应该在所有共享数据准备好之后。