1.3 控制流分析
研究 ioctl(输入/输出控制)是掌握 Linux 驱动“运行时配置能力”的最佳切入点。如果说 write() 是数据的“传输通道”,那么 ioctl() 就是设备的“控制面板”。
1.3.1 dev_dbg( ) 介绍
之前我们都是使用 pr_info( ) 进行日志打印,但是使用这个函数需要重新编译内核,接下来介绍另一个函数dev_dbg( )。
步骤一:检查功能是否开启
dev_dbg 默认只有在开启了 CONFIG_DYNAMIC_DEBUG 配置的内核中才有效。如果你的内核编译时没开这个,dev_dbg 会被优化掉,什么都不会输出。
检查方法:在终端输入 grep CONFIG_DYNAMIC_DEBUG /boot/config-$(uname -r)。如果显示 y,说明支持。
book@100ask:~/100ask_imx6ull-sdk/Linux-4.9.88$ grep CONFIG_DYNAMIC_DEBUG /boot/config-$(uname -r)
CONFIG_DYNAMIC_DEBUG=y
步骤二:模块化编译
既然我们要修改的是 spidev.c ,我们先进入 drivers/spi/Makefile
搜索 “spidev.o”,发现
obj-$(CONFIG_SPI_SPIDEV) += spidev.o
在内核源码目录执行
make menuconfig
- 输入 make menuconfig。
- 输入 / (这是搜索快捷键)。
- 输入 SPI_SPIDEV 并回车。
Symbol: SPI_SPIDEV [=m],这说明已经将其配置为模块化(Module)。
当你执行编译时,编译器会将 spidev.c 编译成独立的 spidev.ko 文件,而不是把它编译进内核镜像里。
Type: tristate 的专业解读
三态(Tristate):指该驱动有三种状态:
y (Built-in):强制编译进内核,启动时自动加载。
m (Module):编译为独立的 .ko,按需加载。
n (No):不编译,彻底移除。
你选了 m,这为你提供了“热加载”的能力,不需要每次改代码都烧录整个内核,效率提升了 10 倍不止。
显示名称(Prompt):User mode SPI device driver support
Location为 Device Drivers -> SPI support,这意味着你需要先进入 Device Drivers 这个大菜单,再进入其中的 SPI support 子菜单,才能找到 SPI_SPIDEV。
Depends on: SPI [=y] && SPI_MASTER [=y](依赖链)
这是一个非常重要的知识点。它告诉你:SPI_SPIDEV 不能独立存在。
它依赖于 SPI 和 SPI_MASTER 选项都为 y。
也就是说,如果你的内核连 SPI 控制器驱动都没开启(SPI_MASTER=n),那你即便把 SPI_SPIDEV 设为 m,它也无法工作,因为底层没有“地基”。
根据显示名称(Prompt)和 Location 我们找到Device Drivers -> SPI support -> User mode SPI device driver support,按 M 键将其改为模块。然后重新编译内核 make zImage 并烧录一次,以后就可以一直用 make M=drivers/spi/ modules 来快速调试了。
步骤三:添加打印信息
在 spidev_iotcl ( ) 函数中,在所有变量初始化后添加:
dev_dbg(&spi->dev, “spidev_ioctl: cmd=0x%x\n”, cmd);
dump_stack();
dev_dbg 函数
含义:dev 代表 Device(设备),dbg 代表 Debug(调试)。
功能:这是一个条件性输出函数。它只在两种情况下才会真正向内核日志缓冲区(dmesg)输出信息:
代码中显式定义了 DEBUG 宏。
通过动态调试(Dynamic Debug)机制在运行时动态开启了对该点的调试开关(即我们之前提到的 +p 操作)。
意义:它比 pr_info 更“优雅”,因为它在未开启调试时几乎不产生开销,非常适合留在生产环境的驱动代码中。&spi->dev 参数
作用:提供上下文信息。
解释:spi 是一个 struct spi_device 结构体指针,代表当前的 SPI 从设备。在内核中,每个设备都有一个 struct device 成员。
效果:当你查看日志时,系统会自动在打印信息的前面加上设备名称(例如 spidev spi0.0:),让你一眼看出这条日志是来自哪一个具体的设备。没有这个参数,你就不知道是哪个 SPI 设备触发的日志。“spidev_ioctl: cmd=0x%x\n”, cmd 格式字符串
spidev_ioctl:是一个简单的标签,告诉你这条日志出现在 spidev_ioctl 函数中。
cmd=0x%x:
%x 是十六进制格式化输出。
cmd 是 ioctl 调用的命令码。这个参数非常关键,它代表了用户空间想要对 SPI 设备做什么(比如修改模式、获取频率、或者是发送数据)。
\n:内核打印的惯例,表示换行。
步骤四:编译烧录模块
在你的内核源码根目录下执行以下命令:
# 1. 确保环境变量已设置(针对你的 ARM 开发板) export CROSS_COMPILE=arm-buildroot-linux-gnueabihf- export ARCH=arm # 2. 清理旧缓存(防止干扰) make M=drivers/spi/ clean # 3. 编译模块 make M=drivers/spi/ modulesCC [M] drivers/spi//spidev.o
Building modules, stage 2.
MODPOST 1 modules
CC drivers/spi//spidev.mod.o
LD [M] drivers/spi//spidev.ko
book@100ask:~/100ask_imx6ull-sdk/Linux-4.9.88$ find . | grep “spidev.ko”
./drivers/spi/spidev.ko
# 将文件推送到开发板的/tmp 目录(该目录通常可写) adb push~/100ask_imx6ull-sdk/Linux-4.9.88/drivers/spi/spidev.ko/tmp/进入开发板:
# 进入文件存放目录 cd /tmp # 卸载旧的模块(如果有) rmmod spidev # 加载你的新模块 insmod spidev.ko # 查看是否加载成功 lsmod | grep spidev步骤五:编写程序
#include<stdio.h>#include<fcntl.h>#include<sys/ioctl.h>#include<linux/spi/spidev.h>#include<unistd.h>#include<string.h>#include<stdint.h>intmain(void){intfd;uint32_tmode=SPI_MODE_0;// 1. 打开设备节点fd=open("/dev/spidev0.0",O_RDWR);if(fd<0){perror("无法打开设备 /dev/spidev0.0");return-1;}// 2. 发起一个 ioctl 调用if(ioctl(fd,SPI_IOC_WR_MODE,&mode)<0){perror("ioctl 失败");}else{printf("ioctl 调用成功!\n");}close(fd);return0;}编译烧录执行该程序后,执行
dmesg | tail -n 20
把内核缓冲区里记录的所有日志信息全部读出来,然后通过管道传给 tail 命令,只取最后 20 行。
[ 692.718904] —[ end trace b06cd85feee7dd69 ]—
[ 692.731238] KD_LOG: Created /dev/spidev0.0 successfully
[ 692.738832] ### SPI_DEBUG: spi_register_driver success! ###
[ 933.867307] CPU: 0 PID: 386 Comm: test_spidev Tainted: G W O 4.9.88 #16
[ 933.867335] Hardware name: Freescale i.MX6 UltraLite (Device Tree)
[ 933.867399] [<80112a34>] (unwind_backtrace) from [<8010dc2c>] (show_stack+0x20/0x24)
[ 933.867438] [<8010dc2c>] (show_stack) from [<80469964>] (dump_stack+0x80/0x94)
[ 933.867496] [<80469964>] (dump_stack) from [<7f04d910>] (spidev_ioctl+0x88/0xa58 [spidev])
[ 933.867548] [<7f04d910>] (spidev_ioctl [spidev]) from [<802685ac>] (do_vfs_ioctl+0xb0/0x934)
[ 933.867580] [<802685ac>] (do_vfs_ioctl) from [<80268e74>] (SyS_ioctl+0x44/0x68)
[ 933.867615] [<80268e74>] (SyS_ioctl) from [<80109280>] (ret_fast_syscall+0x0/0x48)
1.3.2 调用链分析
直接来研究这个 spidev_ioctl ( ) 函数
/* spidev_ioctl:处理用户态通过 ioctl 发起的 SPI 操作请求 */spidev_ioctl(structfile*filp,unsignedintcmd,unsignedlongarg){/* 定义错误码变量,用于跟踪函数执行状态 */interr=0;intretval=0;/* 定义设备数据结构体指针 */structspidev_data*spidev;/* 定义 SPI 设备对象指针 */structspi_device*spi;/* 定义临时变量用于存储用户空间传入的配置值 */u32 tmp;/* 定义 SPI 传输描述符数量 */unsignedn_ioc;/* 定义传输描述符指针数组 */structspi_ioc_transfer*ioc;/* 校验命令魔数,确保传入的是合法的 SPI 相关的 ioctl 命令 */if(_IOC_TYPE(cmd)!=SPI_IOC_MAGIC)return-ENOTTY;/* 检查内存访问权限:如果是读取操作,校验用户地址 arg 是否可写 */if(_IOC_DIR(cmd)&_IOC_READ)err=!access_ok(VERIFY_WRITE,(void__user*)arg,_IOC_SIZE(cmd));/* 检查内存访问权限:如果是写入操作,校验用户地址 arg 是否可读 */if(err==0&&_IOC_DIR(cmd)&_IOC_WRITE)err=!access_ok(VERIFY_READ,(void__user*)arg,_IOC_SIZE(cmd));/* 如果地址非法,返回错误码 */if(err)return-EFAULT;/* 从文件私有数据中获取 spidev 设备上下文 */spidev=filp->private_data;/* 加自旋锁:保护设备引用计数操作,防止并发冲突 */spin_lock_irq(&spidev->spi_lock);/* 获取 SPI 设备引用,防止设备在操作过程中被移除 */spi=spi_dev_get(spidev->spi);/* 解锁 */spin_unlock_irq(&spidev->spi_lock);/* 记录调试信息:显示当前的 ioctl 命令码 */dev_dbg(&spi->dev,"spidev_ioctl: cmd=0x%x\n",cmd);/* 打印内核堆栈:用于调试分析系统调用路径 */dump_stack();/* 如果设备对象已不存在,返回关闭状态错误 */if(spi==NULL)return-ESHUTDOWN;/* 获取互斥锁:保护缓冲区和 SPI 设置过程,防止并发修改导致数据竞争 */mutex_lock(&spidev->buf_lock);/* 开始对命令码进行分发处理 */switch(cmd){/* 处理 SPI 配置读取请求 */caseSPI_IOC_RD_MODE:/* 将内核中的 SPI 模式写入用户空间的内存 arg 中 */retval=__put_user(spi->mode&SPI_MODE_MASK,(__u8 __user*)arg);break;caseSPI_IOC_RD_MODE32:/* 同上,支持 32 位模式读取 */retval=__put_user(spi->mode&SPI_MODE_MASK,(__u32 __user*)arg);break;caseSPI_IOC_RD_LSB_FIRST:/* 读取大小端设置位 */retval=__put_user((spi->mode&SPI_LSB_FIRST)?1:0,(__u8 __user*)arg);break;caseSPI_IOC_RD_BITS_PER_WORD:/* 读取每个字的位数 */retval=__put_user(spi->bits_per_word,(__u8 __user*)arg);break;caseSPI_IOC_RD_MAX_SPEED_HZ:/* 读取最高传输速度 */retval=__put_user(spidev->speed_hz,(__u32 __user*)arg);break;/* 处理 SPI 配置写入请求 */caseSPI_IOC_WR_MODE:caseSPI_IOC_WR_MODE32:/* 根据命令版本从用户空间获取模式值 */if(cmd==SPI_IOC_WR_MODE)retval=__get_user(tmp,(u8 __user*)arg);elseretval=__get_user(tmp,(u32 __user*)arg);/* 如果读取成功,进行后续设置 */if(retval==0){u32 save=spi->mode;/* 检查模式值是否合法 */if(tmp&~SPI_MODE_MASK){retval=-EINVAL;break;}/* 合并新的模式位 */tmp|=spi->mode&~SPI_MODE_MASK;spi->mode=(u16)tmp;/* 调用底层驱动应用新的 SPI 设置 */retval=spi_setup(spi);/* 如果设置失败,恢复备份的旧模式 */if(retval<0)spi->mode=save;elsedev_dbg(&spi->dev,"spi mode %x\n",tmp);}break;caseSPI_IOC_WR_LSB_FIRST:/* 设置大小端模式 */retval=__get_user(tmp,(__u8 __user*)arg);if(retval==0){u32 save=spi->mode;if(tmp)spi->mode|=SPI_LSB_FIRST;elsespi->mode&=~SPI_LSB_FIRST;retval=spi_setup(spi);if(retval<0)spi->mode=save;elsedev_dbg(&spi->dev,"%csb first\n",tmp?'l':'m');}break;caseSPI_IOC_WR_BITS_PER_WORD:/* 设置字长 */retval=__get_user(tmp,(__u8 __user*)arg);if(retval==0){u8 save=spi->bits_per_word;spi->bits_per_word=tmp;retval=spi_setup(spi);if(retval<0)spi->bits_per_word=save;elsedev_dbg(&spi->dev,"%d bits per word\n",tmp);}break;caseSPI_IOC_WR_MAX_SPEED_HZ:/* 设置最高速度 */retval=__get_user(tmp,(__u32 __user*)arg);if(retval==0){u32 save=spi->max_speed_hz;spi->max_speed_hz=tmp;retval=spi_setup(spi);if(retval>=0)spidev->speed_hz=tmp;elsedev_dbg(&spi->dev,"%d Hz (max)\n",tmp);spi->max_speed_hz=save;}break;/* 处理数据传输请求 (SPI_IOC_MESSAGE) */default:/* 获取用户传输请求的参数并拷贝到内核空间的缓存中 */ioc=spidev_get_ioc_message(cmd,(structspi_ioc_transfer__user*)arg,&n_ioc);/* 检查传输描述符拷贝是否出错 */if(IS_ERR(ioc)){retval=PTR_ERR(ioc);break;}/* 如果没有需要传输的内容则退出 */if(!ioc)break;/* 执行 SPI 消息传输,这是真正的 SPI 数据收发逻辑 */retval=spidev_message(spidev,ioc,n_ioc);/* 释放临时分配的传输描述符内存 */kfree(ioc);break;}/* 解锁互斥锁 */mutex_unlock(&spidev->buf_lock);/* 释放对 SPI 设备的引用 */spi_dev_put(spi);/* 返回最终操作结果 */returnretval;}spidev_ioctl:控制流(策略制定者)
在这个阶段,驱动层做的是参数化工作,它并不产生实质的物理波形,而是在为接下来的传输做“环境配置”:
确定对象:通过 filp->private_data 拿到属于当前文件的 spidev 实例。
修改状态:修改 spi->mode, spi->bits_per_word 或 spi->max_speed_hz。
锁定资源:通过 mutex_lock 确保配置过程的原子性。
配置底层:通过 spi_setup 真正通知底层硬件去调整寄存器(如修改分频系数以适配 speed_hz)。
1.4 同步与异步的映射
接口层的该功能把用户态结构体映射到 spi_transfe
spidev_message:数据流(执行实施者)
在这个阶段,驱动层做的是物理搬运工作,它是将用户定义的“愿景”转化为“实际电流波动”的过程:
格式转换(映射):将用户空间那套“零碎的、不可直接访问的”地址,转换为内核空间的“统一的、可 DMA 的”spi_transfer 结构体链表。
数据搬运:通过 copy_from_user 实现内存隔离下的数据安全传输(Bounce Buffer 机制)。
同步阻塞(spi_sync):它是执行的最终触发点,将封装好的 spi_message 塞进控制器驱动的队列中,并等待硬件完成任务。
从上面的 spidev_ioctl 可以顺着往下研究 spidev_message
/* spidev_message:将用户态的 SPI 传输请求映射并提交给底层内核总线 */staticintspidev_message(structspidev_data*spidev,structspi_ioc_transfer*u_xfers,unsignedn_xfers){/* 初始化一个内核 SPI 消息队列对象 */structspi_messagemsg;/* 定义指向内核态传输描述符的指针数组 */structspi_transfer*k_xfers;/* 定义遍历描述符时的临时指针 */structspi_transfer*k_tmp;structspi_ioc_transfer*u_tmp;/* 定义传输计数器、总量和缓冲区总大小 */unsignedn,total,tx_total,rx_total;/* 定义指向内核态发送和接收缓冲区的指针 */u8*tx_buf,*rx_buf;/* 默认状态设为 EFAULT,用于处理拷贝失败情况 */intstatus=-EFAULT;/* 初始化 SPI 消息对象,准备挂载传输项 */spi_message_init(&msg);/* 为 n_xfers 个内核传输描述符分配内存,防止内存泄漏需在 done 处释放 */k_xfers=kcalloc(n_xfers,sizeof(*k_tmp),GFP_KERNEL);/* 如果内存分配失败,直接返回内存不足错误 */if(k_xfers==NULL)return-ENOMEM;/* 初始化缓冲区指针,使用预分配的弹跳缓冲区(Bounce Buffer) */tx_buf=spidev->tx_buffer;rx_buf=spidev->rx_buffer;total=0;tx_total=0;rx_total=0;/* 开始遍历用户提供的每一个传输段,并将其转换为内核态描述符 */for(n=n_xfers,k_tmp=k_xfers,u_tmp=u_xfers;n;n--,k_tmp++,u_tmp++){/* 复制传输长度信息 */k_tmp->len=u_tmp->len;/* 累计传输总长度,用于最后作为返回值返回 */total+=k_tmp->len;/* 检查传输长度是否越界,防止整数溢出 */if(total>INT_MAX||k_tmp->len>INT_MAX){status=-EMSGSIZE;gotodone;}/* 如果用户态有接收缓冲区,将其映射到内核接收缓冲区 */if(u_tmp->rx_buf){rx_total+=k_tmp->len;/* 检查是否超过内核预分配的最大缓冲区空间 */if(rx_total>bufsiz){status=-EMSGSIZE;gotodone;}/* 将内核弹跳缓冲区指针赋值给描述符 */k_tmp->rx_buf=rx_buf;/* 安全校验:检查用户态内存地址是否可写 */if(!access_ok(VERIFY_WRITE,(u8 __user*)(uintptr_t)u_tmp->rx_buf,u_tmp->len))gotodone;/* 指针偏移,准备处理下一段 */rx_buf+=k_tmp->len;}/* 如果用户态有发送缓冲区,执行数据拷贝 */if(u_tmp->tx_buf){tx_total+=k_tmp->len;if(tx_total>bufsiz){status=-EMSGSIZE;gotodone;}/* 映射内核发送缓冲区 */k_tmp->tx_buf=tx_buf;/* 使用 copy_from_user 将数据安全地从用户态搬运到内核态 */if(copy_from_user(tx_buf,(constu8 __user*)(uintptr_t)u_tmp->tx_buf,u_tmp->len))gotodone;/* 指针偏移 */tx_buf+=k_tmp->len;}/* 映射其他控制参数:片选、位宽、延时、速度等 */k_tmp->cs_change=!!u_tmp->cs_change;k_tmp->tx_nbits=u_tmp->tx_nbits;k_tmp->rx_nbits=u_tmp->rx_nbits;k_tmp->bits_per_word=u_tmp->bits_per_word;k_tmp->delay_usecs=u_tmp->delay_usecs;k_tmp->speed_hz=u_tmp->speed_hz;/* 如果用户未指定频率,则使用默认的设备频率 */if(!k_tmp->speed_hz)k_tmp->speed_hz=spidev->speed_hz;#ifdefVERBOSE/* 调试模式下打印传输信息 */dev_dbg(&spidev->spi->dev," xfer len %u ...\n",u_tmp->len);#endif/* 将处理好的内核传输项挂载到消息队列链表中 */spi_message_add_tail(k_tmp,&msg);}/* 同步调用底层的 SPI 核心驱动来执行传输 */status=spidev_sync(spidev,&msg);/* 如果传输失败,跳转到清理阶段 */if(status<0)gotodone;/* 传输完成后,将接收到的数据从内核缓冲区拷贝回用户态缓冲区 */rx_buf=spidev->rx_buffer;for(n=n_xfers,u_tmp=u_xfers;n;n--,u_tmp++){if(u_tmp->rx_buf){/* 使用 copy_to_user 完成数据回传 */if(__copy_to_user((u8 __user*)(uintptr_t)u_tmp->rx_buf,rx_buf,u_tmp->len)){status=-EFAULT;gotodone;}rx_buf+=u_tmp->len;}}/* 如果一切顺利,更新返回状态为传输总字节数 */status=total;done:/* 统一清理:释放之前分配的内核传输描述符内存 */kfree(k_xfers);returnstatus;}