)
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说明支持。book100ask:~/100ask_imx6ull-sdk/Linux-4.9.88$ grep CONFIG_DYNAMIC_DEBUG /boot/config-$(uname -r)CONFIG_DYNAMIC_DEBUGy步骤二模块化编译既然我们要修改的是 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 倍不止。显示名称PromptUser mode SPI device driver supportLocation为 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_MASTERn那你即便把 SPI_SPIDEV 设为 m它也无法工作因为底层没有“地基”。根据显示名称Prompt和 Location 我们找到Device Drivers - SPI support - User mode SPI device driver support按 M 键将其改为模块。然后重新编译内核 make zImage 并烧录一次以后就可以一直用 make Mdrivers/spi/ modules 来快速调试了。步骤三添加打印信息在 spidev_iotcl ( ) 函数中在所有变量初始化后添加dev_dbg(spi-dev, “spidev_ioctl: cmd0x%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: cmd0x%x\n”, cmd 格式字符串spidev_ioctl是一个简单的标签告诉你这条日志出现在 spidev_ioctl 函数中。cmd0x%x%x 是十六进制格式化输出。cmd 是 ioctl 调用的命令码。这个参数非常关键它代表了用户空间想要对 SPI 设备做什么比如修改模式、获取频率、或者是发送数据。\n内核打印的惯例表示换行。步骤四编译烧录模块在你的内核源码根目录下执行以下命令# 1. 确保环境变量已设置针对你的 ARM 开发板 export CROSS_COMPILEarm-buildroot-linux-gnueabihf- export ARCHarm # 2. 清理旧缓存防止干扰 make Mdrivers/spi/ clean # 3. 编译模块 make Mdrivers/spi/ modulesCC [M] drivers/spi//spidev.oBuilding modules, stage 2.MODPOST 1 modulesCC drivers/spi//spidev.mod.oLD [M] drivers/spi//spidev.kobook100ask:~/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步骤五编写程序#includestdio.h#includefcntl.h#includesys/ioctl.h#includelinux/spi/spidev.h#includeunistd.h#includestring.h#includestdint.hintmain(void){intfd;uint32_tmodeSPI_MODE_0;// 1. 打开设备节点fdopen(/dev/spidev0.0,O_RDWR);if(fd0){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_stack0x20/0x24)[ 933.867438] [8010dc2c] (show_stack) from [80469964] (dump_stack0x80/0x94)[ 933.867496] [80469964] (dump_stack) from [7f04d910] (spidev_ioctl0x88/0xa58 [spidev])[ 933.867548] [7f04d910] (spidev_ioctl [spidev]) from [802685ac] (do_vfs_ioctl0xb0/0x934)[ 933.867580] [802685ac] (do_vfs_ioctl) from [80268e74] (SyS_ioctl0x44/0x68)[ 933.867615] [80268e74] (SyS_ioctl) from [80109280] (ret_fast_syscall0x0/0x48)1.3.2 调用链分析直接来研究这个 spidev_ioctl ( ) 函数/* spidev_ioctl处理用户态通过 ioctl 发起的 SPI 操作请求 */spidev_ioctl(structfile*filp,unsignedintcmd,unsignedlongarg){/* 定义错误码变量用于跟踪函数执行状态 */interr0;intretval0;/* 定义设备数据结构体指针 */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(err0_IOC_DIR(cmd)_IOC_WRITE)err!access_ok(VERIFY_READ,(void__user*)arg,_IOC_SIZE(cmd));/* 如果地址非法返回错误码 */if(err)return-EFAULT;/* 从文件私有数据中获取 spidev 设备上下文 */spidevfilp-private_data;/* 加自旋锁保护设备引用计数操作防止并发冲突 */spin_lock_irq(spidev-spi_lock);/* 获取 SPI 设备引用防止设备在操作过程中被移除 */spispi_dev_get(spidev-spi);/* 解锁 */spin_unlock_irq(spidev-spi_lock);/* 记录调试信息显示当前的 ioctl 命令码 */dev_dbg(spi-dev,spidev_ioctl: cmd0x%x\n,cmd);/* 打印内核堆栈用于调试分析系统调用路径 */dump_stack();/* 如果设备对象已不存在返回关闭状态错误 */if(spiNULL)return-ESHUTDOWN;/* 获取互斥锁保护缓冲区和 SPI 设置过程防止并发修改导致数据竞争 */mutex_lock(spidev-buf_lock);/* 开始对命令码进行分发处理 */switch(cmd){/* 处理 SPI 配置读取请求 */caseSPI_IOC_RD_MODE:/* 将内核中的 SPI 模式写入用户空间的内存 arg 中 */retval__put_user(spi-modeSPI_MODE_MASK,(__u8 __user*)arg);break;caseSPI_IOC_RD_MODE32:/* 同上支持 32 位模式读取 */retval__put_user(spi-modeSPI_MODE_MASK,(__u32 __user*)arg);break;caseSPI_IOC_RD_LSB_FIRST:/* 读取大小端设置位 */retval__put_user((spi-modeSPI_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(cmdSPI_IOC_WR_MODE)retval__get_user(tmp,(u8 __user*)arg);elseretval__get_user(tmp,(u32 __user*)arg);/* 如果读取成功进行后续设置 */if(retval0){u32 savespi-mode;/* 检查模式值是否合法 */if(tmp~SPI_MODE_MASK){retval-EINVAL;break;}/* 合并新的模式位 */tmp|spi-mode~SPI_MODE_MASK;spi-mode(u16)tmp;/* 调用底层驱动应用新的 SPI 设置 */retvalspi_setup(spi);/* 如果设置失败恢复备份的旧模式 */if(retval0)spi-modesave;elsedev_dbg(spi-dev,spi mode %x\n,tmp);}break;caseSPI_IOC_WR_LSB_FIRST:/* 设置大小端模式 */retval__get_user(tmp,(__u8 __user*)arg);if(retval0){u32 savespi-mode;if(tmp)spi-mode|SPI_LSB_FIRST;elsespi-mode~SPI_LSB_FIRST;retvalspi_setup(spi);if(retval0)spi-modesave;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(retval0){u8 savespi-bits_per_word;spi-bits_per_wordtmp;retvalspi_setup(spi);if(retval0)spi-bits_per_wordsave;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(retval0){u32 savespi-max_speed_hz;spi-max_speed_hztmp;retvalspi_setup(spi);if(retval0)spidev-speed_hztmp;elsedev_dbg(spi-dev,%d Hz (max)\n,tmp);spi-max_speed_hzsave;}break;/* 处理数据传输请求 (SPI_IOC_MESSAGE) */default:/* 获取用户传输请求的参数并拷贝到内核空间的缓存中 */iocspidev_get_ioc_message(cmd,(structspi_ioc_transfer__user*)arg,n_ioc);/* 检查传输描述符拷贝是否出错 */if(IS_ERR(ioc)){retvalPTR_ERR(ioc);break;}/* 如果没有需要传输的内容则退出 */if(!ioc)break;/* 执行 SPI 消息传输这是真正的 SPI 数据收发逻辑 */retvalspidev_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_transfespidev_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_xferskcalloc(n_xfers,sizeof(*k_tmp),GFP_KERNEL);/* 如果内存分配失败直接返回内存不足错误 */if(k_xfersNULL)return-ENOMEM;/* 初始化缓冲区指针使用预分配的弹跳缓冲区Bounce Buffer */tx_bufspidev-tx_buffer;rx_bufspidev-rx_buffer;total0;tx_total0;rx_total0;/* 开始遍历用户提供的每一个传输段并将其转换为内核态描述符 */for(nn_xfers,k_tmpk_xfers,u_tmpu_xfers;n;n--,k_tmp,u_tmp){/* 复制传输长度信息 */k_tmp-lenu_tmp-len;/* 累计传输总长度用于最后作为返回值返回 */totalk_tmp-len;/* 检查传输长度是否越界防止整数溢出 */if(totalINT_MAX||k_tmp-lenINT_MAX){status-EMSGSIZE;gotodone;}/* 如果用户态有接收缓冲区将其映射到内核接收缓冲区 */if(u_tmp-rx_buf){rx_totalk_tmp-len;/* 检查是否超过内核预分配的最大缓冲区空间 */if(rx_totalbufsiz){status-EMSGSIZE;gotodone;}/* 将内核弹跳缓冲区指针赋值给描述符 */k_tmp-rx_bufrx_buf;/* 安全校验检查用户态内存地址是否可写 */if(!access_ok(VERIFY_WRITE,(u8 __user*)(uintptr_t)u_tmp-rx_buf,u_tmp-len))gotodone;/* 指针偏移准备处理下一段 */rx_bufk_tmp-len;}/* 如果用户态有发送缓冲区执行数据拷贝 */if(u_tmp-tx_buf){tx_totalk_tmp-len;if(tx_totalbufsiz){status-EMSGSIZE;gotodone;}/* 映射内核发送缓冲区 */k_tmp-tx_buftx_buf;/* 使用 copy_from_user 将数据安全地从用户态搬运到内核态 */if(copy_from_user(tx_buf,(constu8 __user*)(uintptr_t)u_tmp-tx_buf,u_tmp-len))gotodone;/* 指针偏移 */tx_bufk_tmp-len;}/* 映射其他控制参数片选、位宽、延时、速度等 */k_tmp-cs_change!!u_tmp-cs_change;k_tmp-tx_nbitsu_tmp-tx_nbits;k_tmp-rx_nbitsu_tmp-rx_nbits;k_tmp-bits_per_wordu_tmp-bits_per_word;k_tmp-delay_usecsu_tmp-delay_usecs;k_tmp-speed_hzu_tmp-speed_hz;/* 如果用户未指定频率则使用默认的设备频率 */if(!k_tmp-speed_hz)k_tmp-speed_hzspidev-speed_hz;#ifdefVERBOSE/* 调试模式下打印传输信息 */dev_dbg(spidev-spi-dev, xfer len %u ...\n,u_tmp-len);#endif/* 将处理好的内核传输项挂载到消息队列链表中 */spi_message_add_tail(k_tmp,msg);}/* 同步调用底层的 SPI 核心驱动来执行传输 */statusspidev_sync(spidev,msg);/* 如果传输失败跳转到清理阶段 */if(status0)gotodone;/* 传输完成后将接收到的数据从内核缓冲区拷贝回用户态缓冲区 */rx_bufspidev-rx_buffer;for(nn_xfers,u_tmpu_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_bufu_tmp-len;}}/* 如果一切顺利更新返回状态为传输总字节数 */statustotal;done:/* 统一清理释放之前分配的内核传输描述符内存 */kfree(k_xfers);returnstatus;}