第七阶段:spidev.c —— 用户空间接口
2026/7/20 15:47:16 网站建设 项目流程

第七阶段:spidev.c —— 用户空间接口

完整数据流

用户程序 内核 硬件 ──────── ──── ──── open("/dev/spidev0.0") → spidev_open ├── 分配 tx_buffer (4096字节) └── 分配 rx_buffer (4096字节) ioctl(SPI_IOC_MESSAGE(1)) → spidev_ioctl └── spidev_message ├── copy_from_user: 用户数据 → tx_buffer ├── 构造 spi_transfer + spi_message ├── spidev_sync → spi_sync │ └── [第五阶段全部流程] │ → [第六阶段全部流程] │ → 硬件寄存器 → DMA/中断 ├── copy_to_user: rx_buffer → 用户数据 └── 返回

spidev_init —— 模块初始化

staticint__initspidev_init(void){// ① 注册字符设备:主设备号 153,名字 "spi"register_chrdev(SPIDEV_MAJOR,"spi",&spidev_fops);// ② 创建 /sys/class/spidev/spidev_class=class_create(THIS_MODULE,"spidev");// ③ 注册为 SPI 驱动 — 连接第四阶段!spi_register_driver(&spidev_spi_driver);// ↑ 这就是第四阶段的入口!// 设备树里 compatible="rohm,dh2228fv" → probe 被调用}

spidev_probe —— 创建 /dev/spidevX.X

staticintspidev_probe(structspi_device*spi){structspidev_data*spidev;unsignedlongminor;spidev=kzalloc(sizeof(*spidev),GFP_KERNEL);spidev->spi=spi;// 找空闲次设备号minor=find_first_zero_bit(minors,N_SPI_MINORS);// 创建设备节点!spidev->devt=MKDEV(SPIDEV_MAJOR,minor);// 主设备号 153,次设备号动态分配device_create(spidev_class,&spi->dev,spidev->devt,spidev,"spidev%d.%d",spi->master->bus_num,spi->chip_select);// → 创建 /dev/spidev0.0, /dev/spidev0.1 等list_add(&spidev->device_entry,&device_list);// 知识点3spidev->speed_hz=spi->max_speed_hz;// 默认速度 = 设备树里的值}

spidev_open —— 打开设备

staticintspidev_open(structinode*inode,structfile*filp){// ① 在 device_list 中找对应次设备号的 spidev_datalist_for_each_entry(spidev,&device_list,device_entry){if(spidev->devt==inode->i_rdev)// 设备号匹配?break;}// ② 分配 bounce buffer(第一次打开时)if(!spidev->tx_buffer)spidev->tx_buffer=kmalloc(bufsiz,GFP_KERNEL);// 默认 4096 字节if(!spidev->rx_buffer)spidev->rx_buffer=kmalloc(bufsiz,GFP_KERNEL);spidev->users++;filp->private_data=spidev;// 以后 ioctl 通过这个拿 spidev_data}

为什么需要 bounce buffer:用户空间的tx_buf指针指向的内存可能被换出(swap),DMA 不能直接访问。必须先copy_from_user到内核空间的安全缓冲区(tx_buffer),DMA 从这个缓冲区读。


spidev_message —— 核心:用户数据到 SPI 传输

staticintspidev_message(structspidev_data*spidev,structspi_ioc_transfer*u_xfers,unsignedn_xfers){structspi_messagemsg;structspi_transfer*k_xfers;// 内核版 transfer 数组spi_message_init(&msg);k_xfers=kcalloc(n_xfers,sizeof(*k_tmp),GFP_KERNEL);tx_buf=spidev->tx_buffer;// bounce buffer 起点rx_buf=spidev->rx_buffer;// 遍历用户传入的每个 ioc_transferfor(n=n_xfers;n;n--,k_tmp++,u_tmp++){k_tmp->len=u_tmp->len;if(u_tmp->rx_buf){// 有接收 → 预留 bounce buffer 空间k_tmp->rx_buf=rx_buf;rx_buf+=k_tmp->len;}if(u_tmp->tx_buf){// 有发送 → 从用户空间拷贝数据k_tmp->tx_buf=tx_buf;copy_from_user(tx_buf,u_tmp->tx_buf,u_tmp->len);// ← 关键!tx_buf+=k_tmp->len;}k_tmp->cs_change=!!u_tmp->cs_change;k_tmp->bits_per_word=u_tmp->bits_per_word;k_tmp->speed_hz=u_tmp->speed_hz;spi_message_add_tail(k_tmp,&msg);// 加到 message 的 transfer 链表}// 发送!→ 第五阶段 → 第六阶段status=spidev_sync(spidev,&msg);// 传输完成 → 把收到的数据拷回用户空间rx_buf=spidev->rx_buffer;for(n=n_xfers;n;n--,u_tmp++){if(u_tmp->rx_buf)copy_to_user(u_tmp->rx_buf,rx_buf,u_tmp->len);// ← 关键!rx_buf+=u_tmp->len;}}

两次 copy 的原因

  • copy_from_user:用户数据 → 内核安全缓冲区(DMA 能用)
  • copy_to_user:内核安全缓冲区 → 用户空间(用户能读)

spidev_sync —— 调用 spi_sync

staticssize_tspidev_sync(structspidev_data*spidev,structspi_message*message){spi=spidev->spi;// 拿 spi_devicestatus=spi_sync(spi,message);// → 第五阶段!if(status==0)status=message->actual_length;// 返回实际传输字节数returnstatus;}

就一行核心调用。这就是你学了六个阶段的终点——spi_sync


检验

Q1(L1)/dev/spidev0.0这个设备节点是谁创建的?名字里0.0的两部分分别来自哪里?

device_create(spidev_class,&spi->dev,spidev->devt,spidev,"spidev%d.%d",spi->master->bus_num,spi->chip_select);// ↑ 第1个 %d ↑ 第2个 %d

spi0.0=spi0(控制器总线号)+.0(片选号 CS0)。spi0.1 = 控制器 spi0 上的 CS1 设备。

Q2(L2):为什么spidev_message要把用户空间的tx_bufcopy_from_user拷到spidev->tx_buffer,而不是直接把用户指针赋给spi_transfer.tx_buf

DMA 能用的内存缺页

用户空间指针指向的内存可能被 swap 换出。当 DMA 硬件试图访问这块内存时,它不能触发缺页中断(DMA 控制器不是 CPU)。结果就是 DMA 读到错误数据或直接失败。copy_from_user把数据拷到kmalloc分配的内核缓冲区——这块内存永远在物理内存中,DMA 可以安全访问。

一句话:用户空间内存不能做 DMA,必须 copy 到内核安全缓冲区。

Q3(L3)spidev_open中分配的tx_buffer是在第一个用户open时才分配的,而不是在spidev_probe中分配。为什么要延迟到open时?如果 4096 字节bufsiz不够一次传输的 RX 数据总量,会发生什么?

为什么延迟到 open 时分配而不是 probe 时?—— 省内存。一个系统可能有 4 个 spidev 设备(spi0.0、spi0.1、spi1.0、spi1.1),每个 tx_buffer + rx_buffer = 8KB,四个就是 32KB。如果设备从不被打开,这 32KB 就浪费了。

如果 4096 字节不够?—— 不是"分两次传输"。spidev_message 里直接拒绝:

if(rx_total>bufsiz){status=-EMSGSIZE;// ← 直接报错!不拆分gotodone;}

用户看到 ioctl 返回 -EMSGSIZE。解决方法:insmod spidev.ko bufsiz=8192,增大 buffer。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询