1. Linux SPI驱动注册流程深度解析
在嵌入式Linux开发中,SPI总线是最常用的外设接口之一。本文将基于i.MX6ULL平台,详细剖析Linux内核中SPI主设备与从设备的完整注册流程,并结合实际设备树配置和内核日志,揭示每个关键步骤背后的实现机制。
1.1 SPI驱动架构概览
Linux内核的SPI子系统采用分层设计,从上到下主要分为五个层次:
- 用户空间接口层:提供标准的文件操作接口(open/read/write/ioctl)
- 设备驱动层:实现具体SPI设备的业务逻辑(如spidev.c)
- 核心层(spi.c):提供总线注册、设备管理等基础设施
- 控制器抽象层:实现bitbang等传输协议
- 硬件控制层:直接操作寄存器
这种分层设计使得驱动开发者可以专注于设备特定的功能实现,而不必关心底层硬件细节。以i.MX6ULL的ECSPI控制器为例,其驱动代码位于drivers/spi/spi-imx.c中。
1.2 设备树配置解析
在i.MX6ULL的设备树中,ECSPI控制器的典型配置如下:
dts复制ecspi1: ecspi@02008000 {
#address-cells = <1>;
#size-cells = <0>;
compatible = "fsl,imx6ul-ecspi", "fsl,imx51-ecspi";
reg = <0x02008000 0x4000>;
interrupts = <GIC_SPI 31 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clks IMX6UL_CLK_ECSPI1>, <&clks IMX6UL_CLK_ECSPI1>;
clock-names = "ipg", "per";
dmas = <&sdma 3 7 1>, <&sdma 4 7 2>;
dma-names = "rx", "tx";
status = "disabled";
};
关键配置项说明:
compatible:驱动匹配的关键标识reg:控制器寄存器物理地址和范围interrupts:中断号和触发方式clocks:IPG时钟和PER时钟dmas:DMA通道配置
实际使用时需要在板级设备树中启用控制器并配置引脚:
dts复制&ecspi1 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_ecspi1>;
fsl,spi-num-chipselects = <2>;
cs-gpios = <&gpio4 26 GPIO_ACTIVE_LOW>, <&gpio4 24 GPIO_ACTIVE_LOW>;
status = "okay";
};
2. SPI主设备注册流程详解
2.1 驱动与设备匹配
当内核启动时,设备树中的节点会被转换为platform_device,与spi-imx驱动(platform_driver)进行匹配。匹配过程主要依赖compatible属性:
- 内核扫描设备树节点,创建对应的
device_node platform_device_register将节点注册为平台设备- 驱动通过
of_match_table声明兼容的设备 - 总线层调用
platform_match进行匹配
匹配成功后,内核调用驱动的probe函数(spi_imx_probe)进行初始化。
2.2 主设备初始化流程
spi_imx_probe函数的主要工作流程如下:
-
分配主设备结构体:
c复制master = spi_alloc_master(&pdev->dev, sizeof(struct spi_imx_data)); -
初始化硬件相关配置:
- 获取时钟资源:
clk_get获取ipg和per时钟 - 映射寄存器区域:
devm_ioremap_resource - 申请中断:
devm_request_irq - 配置DMA通道
- 获取时钟资源:
-
设置主设备参数:
c复制master->bits_per_word_mask = SPI_BPW_RANGE_MASK(1, 32); master->bus_num = pdev->id; master->num_chipselect = spi_imx->devtype_data->num_chipselect; -
注册传输方法:
c复制
master->prepare_transfer_hardware = spi_imx_prepare_transfer; master->transfer_one = spi_imx_transfer_one; master->unprepare_transfer_hardware = spi_imx_unprepare_transfer;
2.3 主设备注册关键步骤
spi_register_master是注册过程的核心函数,其主要工作包括:
-
分配总线编号:
- 如果
bus_num为负数,动态分配一个未使用的编号 - 静态分配的总线编号需要确保不冲突
- 如果
-
创建设备节点:
c复制dev_set_name(&master->dev, "spi%u", master->bus_num); device_add(&master->dev); -
初始化消息队列:
c复制
spi_init_queue(master); spi_start_queue(master); -
注册从设备:
c复制
of_register_spi_devices(master);
内核日志中可以看到完整的注册过程:
code复制[ 2.551503] spi_imx 2008000.ecspi: spi_imx_probe: entered
[ 2.629267] spi_master (null): is master->dev
[ 2.665008] spi_master spi0: is new name
[ 2.680146] spi_master spi0: spi_master_initialize_queue enter!
3. SPI从设备注册流程
3.1 从设备树到spi_device
对于设备树中定义的SPI从设备,如:
dts复制spidev: icm20608@0 {
compatible = "invensense,icm20608";
spi-max-frequency = <8000000>;
reg = <0>;
};
注册流程如下:
-
解析设备树节点:
- 获取片选号(reg属性)
- 解析SPI模式(spi-cpha/spi-cpol等)
- 获取最大时钟频率
-
创建spi_device:
c复制
spi = spi_alloc_device(master); spi->chip_select = cs; spi->max_speed_hz = max_speed; spi->mode = mode; spi->dev.of_node = nc; -
注册设备:
c复制
spi_add_device(spi);
3.2 从设备注册关键函数
spi_add_device的核心工作:
-
检查片选有效性:
c复制if (spi->chip_select >= master->num_chipselect) return -EINVAL; -
设置设备名称:
c复制dev_set_name(&spi->dev, "%s.%u", dev_name(&master->dev), spi->chip_select); -
添加到设备模型:
c复制
status = device_add(&spi->dev);
内核日志示例:
code复制[ 2.885422] spi_master spi2: of_register_spi_device enter!
[ 2.944544] spi_imx 2010000.ecspi: spi_add_device enter!
[ 2.971088] spi spi2.0: spi_match_device: matching driver 'mc13xxx'
3.3 驱动匹配机制
当从设备注册后,内核会尝试匹配对应的驱动:
-
匹配过程:
- 遍历所有注册的
spi_driver - 检查
id_table或of_match_table - 调用驱动的
probe函数
- 遍历所有注册的
-
典型匹配日志:
code复制[ 7.105351] spi spi2.0: spi_match_device: matching driver 'inv-mpu6000-spi' [ 7.166640] inv-mpu6000-spi spi2.0: mounting matrix not found: using identity... -
匹配失败处理:
- 设备保持注册状态但无驱动绑定
- 后续可以动态加载驱动进行匹配
4. 关键问题与调试技巧
4.1 常见注册问题排查
-
主设备注册失败:
- 检查设备树
compatible是否正确 - 确认寄存器地址范围无冲突
- 验证时钟和DMA配置
- 检查设备树
-
从设备无法识别:
- 确认片选号在有效范围内
- 检查
spi-max-frequency是否合理 - 验证引脚复用配置
-
驱动匹配失败:
- 检查驱动
of_match_table定义 - 确认驱动是否编译进内核或已加载
- 检查驱动
4.2 调试技巧与工具
-
内核日志分析:
bash复制
dmesg | grep spi -
sysfs信息查看:
bash复制ls /sys/bus/spi/devices/ cat /sys/bus/spi/devices/spi2.0/modalias -
设备树调试:
bash复制
dtc -I fs /proc/device-tree | less -
SPI传输测试:
bash复制
spidev_test -D /dev/spidev0.0
4.3 性能优化建议
-
DMA配置优化:
- 确保DMA通道配置正确
- 调整DMA缓冲区大小
-
时钟配置优化:
- 根据外设需求设置合适的时钟频率
- 避免过高的时钟导致信号完整性问题
-
中断优化:
- 使用共享中断减少开销
- 优化中断处理函数
5. 实际案例分析
5.1 ICM-20608陀螺仪驱动注册
以设备树中的ICM-20608为例,完整注册流程如下:
-
设备树节点:
dts复制spidev: icm20608@0 { compatible = "invensense,icm20608"; spi-max-frequency = <8000000>; reg = <0>; }; -
驱动匹配过程:
code复制[ 7.105351] spi spi2.0: spi_match_device: matching driver 'inv-mpu6000-spi' [ 7.112344] spi spi2.0: trying ID table match... -
驱动初始化:
- 配置传感器工作模式
- 初始化寄存器
- 创建输入设备
5.2 多从设备配置
对于支持多个片选的SPI控制器(如ECSPI1的两个片选):
-
设备树配置:
dts复制&ecspi1 { cs-gpios = <&gpio4 26 GPIO_ACTIVE_LOW>, <&gpio4 24 GPIO_ACTIVE_LOW>; status = "okay"; device1@0 { compatible = "vendor,device1"; reg = <0>; }; device2@1 { compatible = "vendor,device2"; reg = <1>; }; }; -
注册日志:
code复制[ 2.713176] spi_imx 2008000.ecspi: CS GPIO 122 ok [ 2.718087] spi_imx 2008000.ecspi: CS GPIO 120 ok
5.3 动态加载驱动
对于未在设备树中声明但需要动态加载的驱动:
-
手动创建设备:
c复制struct spi_board_info dev_info = { .modalias = "spidev", .max_speed_hz = 5000000, .bus_num = 0, .chip_select = 0, .mode = SPI_MODE_0, }; spi_register_board_info(&dev_info, 1); -
加载驱动:
bash复制
modprobe spidev
6. 深入理解SPI核心层
6.1 spi_master结构体解析
struct spi_master是SPI控制器的抽象表示,主要字段包括:
c复制struct spi_master {
struct device dev;
s16 bus_num;
u16 num_chipselect;
u16 min_speed_hz;
u16 max_speed_hz;
int (*setup)(struct spi_device *spi);
int (*transfer)(struct spi_device *spi, struct spi_message *mesg);
/* ... */
};
6.2 spi_message传输机制
SPI数据传输的基本单位是spi_message,其工作流程:
-
创建消息和传输请求:
c复制struct spi_message msg; struct spi_transfer xfer; spi_message_init(&msg); spi_message_add_tail(&xfer, &msg); -
提交传输请求:
c复制
ret = spi_sync(spi, &msg); -
控制器处理:
- 加入传输队列
- 调用
transfer_one_message回调
6.3 bitbang模式实现
对于没有硬件SPI控制器的场景,可以使用bitbang模拟:
-
配置bitbang操作:
c复制struct spi_bitbang bitbang; bitbang->txrx_bufs = spi_imx_buf_rx_swap; bitbang->master->transfer_one_message = spi_bitbang_transfer_one_message; -
实现GPIO操作:
c复制static void spi_imx_chipselect(struct spi_device *spi, int is_active) { gpio_set_value(spi->cs_gpio, !is_active); }
7. 高级话题与扩展
7.1 SPI与DMA协同工作
i.MX6ULL的ECSPI支持DMA传输,配置要点:
-
设备树DMA配置:
dts复制dmas = <&sdma 3 7 1>, <&sdma 4 7 2>; dma-names = "rx", "tx"; -
驱动中DMA初始化:
c复制spi_imx->rx_dma = dma_request_slave_channel(&master->dev, "rx"); spi_imx->tx_dma = dma_request_slave_channel(&master->dev, "tx");
7.2 多SPI控制器管理
在复杂系统中管理多个SPI控制器:
-
设备树定义多个控制器:
dts复制ecspi1: ecspi@02008000 { /* ... */ }; ecspi2: ecspi@0200c000 { /* ... */ }; ecspi3: ecspi@02010000 { /* ... */ }; -
驱动中区分不同实例:
c复制static const struct of_device_id spi_imx_dt_ids[] = { { .compatible = "fsl,imx6ul-ecspi", }, { /* sentinel */ } };
7.3 电源管理集成
实现SPI设备的电源管理:
-
实现PM操作:
c复制static const struct dev_pm_ops spi_imx_pm = { SET_SYSTEM_SLEEP_PM_OPS(spi_imx_suspend, spi_imx_resume) }; -
设备树电源域配置:
dts复制power-domains = <&pd_sdma>;
8. 最佳实践与经验分享
8.1 设备树配置建议
-
引脚复用配置:
- 确保SPI引脚正确复用
- 配置合适的电气特性(驱动强度、上下拉)
-
时钟配置:
- 根据外设需求选择合适时钟源
- 避免过高的时钟频率导致信号问题
-
片选管理:
- 明确指定片选GPIO
- 考虑片选的有效电平
8.2 驱动开发技巧
-
传输优化:
- 使用DMA减少CPU开销
- 合理设置SPI模式
-
错误处理:
- 完善超时处理
- 添加重试机制
-
调试支持:
- 实现sysfs调试接口
- 添加详细的日志输出
8.3 性能调优经验
-
基准测试:
- 测量实际传输速率
- 识别性能瓶颈
-
参数调整:
- 优化SPI时钟分频
- 调整DMA缓冲区大小
-
并发处理:
- 合理使用锁机制
- 考虑多SPI控制器的负载均衡
在实际项目中,我发现i.MX6ULL的ECSPI控制器在DMA模式下可以达到更高的传输效率,但需要特别注意DMA缓冲区的对齐问题。此外,当多个SPI设备共享同一控制器时,片选切换的时序控制尤为关键,不当的配置可能导致设备无法正常响应。