SPI Master驱动开发实战:从设备树到数据传输全解析

张开发
2026/4/16 0:08:03 15 分钟阅读

分享文章

SPI Master驱动开发实战:从设备树到数据传输全解析
1. SPI Master驱动开发概述SPISerial Peripheral Interface是一种常见的同步串行通信协议广泛应用于嵌入式系统和智能硬件设备中。作为开发者理解SPI Master驱动的开发流程对于构建高效稳定的硬件通信系统至关重要。SPI Master驱动负责控制整个SPI总线的通信时序和数据传输是连接主控芯片与外围设备的关键桥梁。在实际项目中我遇到过不少开发者对SPI驱动开发感到困惑的情况。比如有位同事曾经花了整整一周时间调试一个SPI Flash驱动最后发现问题出在设备树配置的时钟极性设置错误。这种经历让我深刻认识到掌握SPI Master驱动的完整开发流程能极大提高开发效率和问题排查能力。SPI协议具有全双工、高速通常1-100MHz、简单的特点使用四线制SCLK、MOSI、MISO、CS进行通信。与I2C相比SPI没有复杂的地址机制和应答信号但需要更多的硬件连线。在Linux系统中SPI驱动框架已经为我们处理了大部分底层细节开发者主要需要关注设备树配置和驱动实现两个核心部分。2. SPI设备树配置详解2.1 Master节点配置设备树是Linux内核中描述硬件配置的重要机制。对于SPI Master驱动开发正确的设备树配置是第一步。在我的项目经验中约30%的SPI通信问题都源于错误的设备树配置。一个典型的SPI Master节点配置如下spi1: spi48030000 { compatible ti,omap4-mcspi; reg 0x48030000 0x400; interrupts 125; #address-cells 1; #size-cells 0; ti,spi-num-cs 4; status disabled; };关键属性说明compatible匹配驱动程序的字符串必须与驱动中的定义一致reg控制器寄存器地址范围interrupts中断号#address-cells和#size-cells必须分别设置为1和0ti,spi-num-cs指定片选信号数量2.2 Slave设备配置在Master节点下我们需要为每个连接的Slave设备创建子节点。这里有个实际案例曾经有个项目需要连接两个SPI设备但由于第二个设备的reg属性设置错误导致系统只能识别第一个设备。正确的Slave设备配置示例spi1 { status okay; pinctrl-names default; pinctrl-0 spi1_pins; flash0 { compatible spidev; reg 0; // 使用CS0 spi-max-frequency 1000000; spi-cpol; // 时钟极性设置 spi-cpha; // 时钟相位设置 }; sensor1 { compatible vendor,spi-sensor; reg 1; // 使用CS1 spi-max-frequency 500000; }; };特别注意reg属性必须唯一且连续spi-max-frequency应根据设备规格设置时钟极性和相位CPOL/CPHA必须与设备要求一致3. SPI驱动框架解析3.1 Linux SPI核心框架Linux内核提供了完善的SPI子系统框架主要由以下几个核心结构体组成spi_controller代表SPI控制器硬件spi_device描述连接的SPI设备spi_driver设备驱动实现spi_message和spi_transfer数据传输的抽象在实际驱动开发中我们主要需要实现spi_controller的传输函数。内核提供了两种实现方式传统方式直接实现transfer函数新式方式使用spi_bitbang框架3.2 数据传输机制SPI数据传输的核心是spi_message和spi_transfer结构体。一个spi_message可以包含多个spi_transfer它们会被顺序执行。这种设计非常灵活可以处理各种复杂的传输场景。数据传输流程示例struct spi_transfer xfer[2]; u8 tx_buf[4], rx_buf[4]; /* 初始化传输结构 */ memset(xfer, 0, sizeof(xfer)); xfer[0].tx_buf tx_buf; xfer[0].len sizeof(tx_buf); xfer[1].rx_buf rx_buf; xfer[1].len sizeof(rx_buf); /* 创建并提交消息 */ struct spi_message msg; spi_message_init(msg); spi_message_add_tail(xfer[0], msg); spi_message_add_tail(xfer[1], msg); /* 执行同步传输 */ int status spi_sync(spi, msg);我曾经在一个传感器项目中需要先发送命令字再读取数据。通过合理使用多个spi_transfer可以很优雅地实现这种需求而无需额外的GPIO控制。4. SPI Master驱动实现4.1 传统实现方式传统方式需要直接实现spi_controller的transfer函数。这种方式灵活性高但实现起来相对复杂。核心实现代码框架static int my_spi_transfer(struct spi_device *spi, struct spi_message *msg) { struct spi_transfer *xfer; /* 遍历所有transfer */ list_for_each_entry(xfer, msg-transfers, transfer_list) { /* 实现具体的硬件传输逻辑 */ if (hardware_transfer(spi, xfer-tx_buf, xfer-rx_buf, xfer-len)) { msg-status -EIO; break; } msg-actual_length xfer-len; } /* 完成通知 */ msg-status 0; spi_finalize_current_message(msg-spi-controller, msg); return 0; } static int my_spi_probe(struct platform_device *pdev) { struct spi_controller *ctlr; /* 分配控制器 */ ctlr spi_alloc_master(pdev-dev, 0); if (!ctlr) return -ENOMEM; /* 设置传输函数 */ ctlr-transfer my_spi_transfer; /* 注册控制器 */ return spi_register_controller(ctlr); }在实际项目中我曾用这种方式实现过一个虚拟SPI控制器用于测试SPI设备驱动。需要注意的是传统方式需要自行处理消息队列和并发访问控制。4.2 使用spi_bitbang框架对于没有专用SPI控制器的平台或者需要软件模拟SPI的情况可以使用spi_bitbang框架。这种方式实现起来更简单但性能较低。实现示例static int bitbang_transfer(struct spi_device *spi, struct spi_transfer *t) { /* 实现位爆破传输逻辑 */ for (int i 0; i t-len; i) { u8 tx t-tx_buf ? t-tx_buf[i] : 0; u8 rx 0; /* 逐位传输 */ for (int j 7; j 0; j--) { set_mosi((tx j) 0x1); set_sck(1); rx | (get_miso() j); set_sck(0); } if (t-rx_buf) t-rx_buf[i] rx; } return 0; } static int bitbang_probe(struct platform_device *pdev) { struct spi_bitbang *bb; struct spi_controller *ctlr; /* 分配控制器 */ ctlr spi_alloc_master(pdev-dev, sizeof(*bb)); /* 初始化bitbang结构 */ bb spi_master_get_devdata(ctlr); bb-master ctlr; bb-txrx_bufs bitbang_transfer; /* 注册控制器 */ return spi_bitbang_start(bb); }在一个低速SPI设备项目中我使用这种方法成功实现了软件SPI Master。需要注意的是软件SPI的时钟频率和稳定性受CPU负载影响较大。5. 调试技巧与常见问题5.1 调试工具与方法调试SPI驱动时以下工具非常有用逻辑分析仪观察实际的SPI波形spidev测试工具验证SPI控制器基本功能devmem2直接读取SPI控制器寄存器内核日志dmesg查看驱动加载和运行信息我常用的调试命令# 查看SPI设备列表 ls /sys/bus/spi/devices/ # 使用spidev测试 spidev_test -D /dev/spidev0.0 -v -p Hello SPI5.2 常见问题与解决方案设备无法识别检查设备树compatible属性是否匹配确认片选信号是否正确配置测量硬件连接是否正常数据传输错误检查时钟极性和相位设置确认SPI模式0/1/2/3与设备要求一致降低时钟频率测试性能问题优化传输缓冲区大小考虑使用DMA传输减少消息分段在一个实际项目中我们遇到SPI Flash读写不稳定的问题。通过逻辑分析仪发现原来是PCB走线过长导致信号质量差。最终通过降低时钟频率和缩短走线解决了问题。6. 性能优化技巧6.1 DMA传输优化对于大数据量传输使用DMA可以显著降低CPU负载。Linux SPI框架支持DMA传输需要在驱动中实现dma_map和dma_unmap操作。DMA配置示例static int spi_dma_setup(struct spi_device *spi) { struct dma_slave_config cfg; memset(cfg, 0, sizeof(cfg)); cfg.direction DMA_MEM_TO_DEV; cfg.dst_addr spi-controller-dma_tx; cfg.dst_addr_width DMA_SLAVE_BUSWIDTH_1_BYTE; return dmaengine_slave_config(spi-controller-dma_tx_ch, cfg); }6.2 消息合并优化减少spi_message的数量可以降低协议开销。我通常会将多个小传输合并为一个大的spi_transfer。优化前// 发送命令 xfer[0].tx_buf cmd; xfer[0].len 1; // 读取数据 xfer[1].rx_buf data; xfer[1].len 4;优化后// 合并为一个传输 xfer[0].tx_buf cmd; xfer[0].rx_buf data; xfer[0].len 5; // 1字节命令4字节数据这种优化在一个传感器项目中将吞吐量提高了约30%。7. 实际案例虚拟SPI Master实现7.1 设备树配置virtual_spi { compatible example,virtual-spi; #address-cells 1; #size-cells 0; virtual_device0 { compatible spidev; reg 0; spi-max-frequency 1000000; }; };7.2 驱动实现关键代码static int virtual_spi_transfer(struct spi_device *spi, struct spi_message *msg) { struct spi_transfer *xfer; list_for_each_entry(xfer, msg-transfers, transfer_list) { printk(Transfer: len%d, tx%p, rx%p\n, xfer-len, xfer-tx_buf, xfer-rx_buf); if (xfer-rx_buf) memset(xfer-rx_buf, 0xAA, xfer-len); msg-actual_length xfer-len; } msg-status 0; spi_finalize_current_message(spi-controller, msg); return 0; } static int virtual_spi_probe(struct platform_device *pdev) { struct spi_controller *ctlr; ctlr spi_alloc_master(pdev-dev, 0); ctlr-transfer_one_message virtual_spi_transfer; return devm_spi_register_controller(pdev-dev, ctlr); }这个虚拟SPI驱动虽然不进行实际硬件操作但对于测试上层SPI设备驱动非常有用。我在多个项目中用它快速验证了SPI设备驱动的逻辑正确性。

更多文章