Linux 驱动学习笔记:SPI 驱动、module、ioctl、mmap 与 LCD 时序
最近在整理嵌入式 Linux 驱动时,把几个容易混淆的点串了一遍:SPI 驱动里的 spi_write_then_read、spi_message/spi_transfer,内核模块里的 module 和 module_platform_driver,字符设备驱动里的 ioctl、mmap,以及 LCD 时序。下面按"从外设驱动到用户空间接口"的顺序整理一下。
一、SPI 驱动:从简单读写到 spi_message
1. spi_write_then_read
spi_write_then_read 是内核提供的 SPI 辅助接口,适合"先发命令、再读数据"的场景。
ret = spi_write_then_read(padxl345_spi, tx, 2, NULL, 0);
它的意思是:
| 参数 | 含义 |
|---|---|
padxl345_spi |
SPI 设备指针 |
tx |
发送缓冲区 |
2 |
发送 2 字节 |
NULL |
不接收数据 |
0 |
接收 0 字节 |
如果 rx 为 NULL、n_rx 为 0,相当于只写不读。这种场景其实也可以直接用 spi_write(),语义更清晰。
2. spi_message + spi_transfer
更通用的 SPI 传输方式是使用 spi_message 和 spi_transfer。
unsigned char tx[7] = {0};
unsigned char rx[7] = {0};
struct spi_message sendmsg;
struct spi_transfer transfer = {
.tx_buf = tx,
.rx_buf = rx,
.len = 7,
.delay_usecs = 20,
};
spi_message_init(&sendmsg);
tx[0] = (0x32 | (0x1 << 7) | (0x1 << 6));
spi_message_add_tail(&transfer, &sendmsg);
ret = spi_sync(padxl345_spi, &sendmsg);
这里:
0x32是 ADXL345 的DATAX0寄存器地址;0x1 << 7表示读操作;0x1 << 6表示多字节连续读;- 所以
tx[0] = 0xF2,表示从DATAX0开始连续读取三轴数据。
spi_sync() 会阻塞等待整个 SPI 事务完成。
二、module 与 module_platform_driver
1. module 是什么
Linux 内核模块是可以动态加载、卸载的内核代码,驱动通常就是以内核模块的形式存在。
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
模块和普通用户程序不同:
| 对比 | 用户程序 | 内核模块 |
|---|---|---|
| 入口 | main() |
module_init() |
| 退出 | return |
module_exit() |
| 打印 | printf() |
printk() |
| 内存分配 | malloc() |
kmalloc() |
2. module_platform_driver
module_platform_driver 是一个简化宏,用来替代手写的 module_init / module_exit。
module_platform_driver(my_driver);
它大致等价于:
static int __init my_driver_init(void)
{
return platform_driver_register(&my_driver);
}
module_init(my_driver_init);
static void __exit my_driver_exit(void)
{
platform_driver_unregister(&my_driver);
}
module_exit(my_driver_exit);
platform_driver 通常用于 SoC 片上外设,比如 LCD、UART、PWM、GPIO 控制器等。
如果是 SPI 从设备驱动,比如 ADXL345、OLED,则通常使用 module_spi_driver。
三、ioctl:用户空间控制设备
read/write 适合传输数据流,ioctl 更适合发送控制命令。
用户空间调用:
ioctl(fd, MY_CMD_SET_RANGE, &range);
驱动中实现:
static long my_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
switch (cmd) {
case MY_CMD_SET_RANGE:
if (copy_from_user(&range, (int __user *)arg, sizeof(range)))
return -EFAULT;
/* 配置设备 */
break;
case MY_CMD_GET_ID:
if (copy_to_user((int __user *)arg, &devid, sizeof(devid)))
return -EFAULT;
break;
default:
return -ENOTTY;
}
return 0;
}
命令码通常使用 <linux/ioctl.h> 中的宏定义:
#define MY_MAGIC 'M'
#define MY_CMD_RESET _IO(MY_MAGIC, 0)
#define MY_CMD_SET _IOW(MY_MAGIC, 1, int)
#define MY_CMD_GET _IOR(MY_MAGIC, 2, int)
#define MY_CMD_RDWR _IOWR(MY_MAGIC, 3, int)
这里的读/写方向是从用户空间视角看的:
| 宏 | 含义 |
|---|---|
_IO |
无参数 |
_IOW |
用户向内核写数据 |
_IOR |
内核向用户读数据 |
_IOWR |
双向传输 |
四、mmap:用户空间直接访问设备内存
mmap 可以把内核物理内存或设备内存映射到用户空间,适合帧缓冲、DMA 缓冲区、寄存器空间等大数据场景。
用户空间:
void *addr = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
驱动中实现 .mmap:
static int my_mmap(struct file *filp, struct vm_area_struct *vma)
{
unsigned long size = vma->vm_end - vma->vm_start;
unsigned long pfn;
if (size > BUFFER_SIZE)
return -EINVAL;
pfn = virt_to_phys(kbuf) >> PAGE_SHIFT;
vma->vm_flags |= VM_IO | VM_DONTEXPAND | VM_DONTDUMP;
vma->vm_page_prot = pgprot_writecombine(vma->vm_page_prot);
return remap_pfn_range(vma, vma->vm_start, pfn, size,
vma->vm_page_prot);
}
remap_pfn_range() 负责把物理页映射到用户 VMA。
相比 read/write,mmap 可以避免反复 copy_from_user/copy_to_user,在 LCD 帧缓冲、摄像头采集、ADC 批量采样等场景下效率更高。
五、LCD 时序:PCLK、HSYNC、VSYNC、DE
LCD 时序本质上是告诉 LCD 控制器什么时候发一帧、什么时候发一行、什么时候发有效像素。
常见信号:
| 信号 | 作用 |
|---|---|
| PCLK | 像素时钟,每个周期传输一个像素 |
| HSYNC | 行同步,表示一行开始 |
| VSYNC | 帧同步,表示一帧开始 |
| DE | 数据使能,高电平时 RGB 数据有效 |
一行完整时序:
HSYNC → HBP → 有效像素 → HFP
一帧完整时序:
VSYNC → VBP → 有效行 → VFP
像素时钟计算:
PCLK = HTotal × VTotal × 刷新率
HTotal = HSW + HBP + HACT + HFP
VTotal = VSW + VBP + VACT + VFP
例如 800×480@60Hz,如果:
HTotal = 800 + 40 + 88 + 128 = 1056
VTotal = 480 + 10 + 33 + 2 = 525
则:
PCLK ≈ 1056 × 525 × 60 ≈ 33.3 MHz
IMX6ULL 的 eLCDIF 控制器就是按这些参数配置的,具体数值必须参考屏幕规格书。
六、小结
这几个知识点可以串成一条驱动学习线:
- SPI 驱动 :用
spi_write_then_read做简单寄存器读写,用spi_message/spi_transfer做复杂传输; - 内核模块 :
module_init/module_exit是基础,module_platform_driver、module_spi_driver是封装; - 字符设备接口 :
read/write传数据,ioctl发控制命令,mmap做零拷贝内存映射; - LCD 驱动:重点理解 PCLK、HSYNC、VSYNC、DE 以及前后廊参数。