Linux 驱动学习笔记(部分)

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 控制器就是按这些参数配置的,具体数值必须参考屏幕规格书。

六、小结

这几个知识点可以串成一条驱动学习线:

  1. SPI 驱动 :用 spi_write_then_read 做简单寄存器读写,用 spi_message/spi_transfer 做复杂传输;
  2. 内核模块 :module_init/module_exit 是基础,module_platform_driver、module_spi_driver 是封装;
  3. 字符设备接口 :read/write 传数据,ioctl 发控制命令,mmap 做零拷贝内存映射;
  4. LCD 驱动:重点理解 PCLK、HSYNC、VSYNC、DE 以及前后廊参数。
相关推荐
zhikouai1 小时前
AI辅助逆向工程链路:从二进制接入到等价性校验
java·linux·前端
程序员-Benothing2 小时前
Linux磁盘IO性能分析:iostat、iotop、fio实战
linux·运维·服务器
LiaoWL1232 小时前
从零开始学 Linux(一):目录结构、常用命令与 VIM 编辑器上手
linux·运维·服务器
霖霖7142 小时前
【ESP32笔记】SmartConfig一键配网
笔记·单片机·mcu·物联网
我命由我123452 小时前
ReAct 循环
学习·ai·产品运营·学习方法·ai编程·ai写作·产品
霖霖7142 小时前
【ESP32笔记】WIFI连接—STA模式
笔记·单片机·mcu·物联网
for_ever_love__2 小时前
LangGraph Checkpointer:持久化、断点续跑与多轮记忆
服务器·数据库·学习·持久化·langgraph·checkpointer·断点续跑
Lhan.zzZ2 小时前
Linux + Qt/QML:解决 QtQuick.VirtualKeyboard.Plugins 模块加载失败与环境变量配置
linux·数据库·qt
小毛驴8502 小时前
mvn help:effective-settings 与 mvn help:effective-pom
java·linux·前端