当前位置:首页>Linux>嵌入式Linux驱动程序数据链路打通实战

嵌入式Linux驱动程序数据链路打通实战

  • 2026-10-30 14:19:56
嵌入式Linux驱动程序数据链路打通实战

一、内核内存分段特性说明

运行Linux系统时,内存通常至少为32MB、64MB,甚至1GB、2GB,因此无需像单片机那样受资源限制。在Linux系统中,整个内核都运行在内存中,而非Flash上;内存中的内核代码会划分成多个段,有单片机开发经验的同学对“分段”应该不陌生。

即便内存容量庞大,内核占用的内存区域仍会进一步细分出多个功能段。如果某个段(如初始化相关的段)在使用后确认不再需要,内核就可以释放该段内存,回收资源。之前提到的module_init、module_exit所指定的段,同样位于内存中,而非Flash。

二、驱动程序两种加载方式的差异

驱动程序有两种部署方式:一是编进内核,二是编译为.ko模块,通过insmod动态加载。两者功能上没有区别,但在调试、升级和系统启动效率上差异显著:

  1. 编进内核:若驱动程序存在bug,需重新编译整个内核并替换,维护成本高;
  2. 动态加载(.ko模块):若驱动存在bug,仅需替换单板上的.ko文件,重启系统即可生效,调试升级更便捷。

此外,为提升系统启动速度,应尽量精简内核,仅保留必要驱动(如显示驱动,确保上电后快速亮屏、启动核心应用);其他非必要驱动可编译为模块,待系统启动完成后再逐一加载。需要注意的是,驱动程序有bug与“污染内核”并非同一概念。

三、完善驱动程序:注册字符设备驱动

步骤1:定义核心变量与结构体

  1. 主设备号:定义静态变量static int major = 0;,设为0表示让系统自动分配主设备号;
  2. file_operations结构体:这是驱动程序的核心结构体,用于封装open、read、write等接口函数。先定义空结构体,后续逐步完善:

staticstructfile_operationsfops = {
    .owner = THIS_MODULE, // 固定赋值为THIS_MODULE,标识驱动所属模块
};

步骤2:实现驱动注册与注销逻辑

  1. 入口函数:在入口函数中调用register_chrdev函数注册字符设备驱动,传入主设备号(0)、驱动名称(如“hello_driver”)和file_operations结构体地址;注册成功后,系统会返回分配的主设备号,需保存该值供卸载时使用;
  2. 出口函数:在出口函数中调用unregister_chrdev函数注销驱动,传入注册时保存的主设备号和驱动名称。

步骤3:添加必要头文件

编译时出现“register_chrdev未声明”的错误,说明缺少对应头文件。register_chrdev函数定义在<linux/fs.h>中,需添加包含语句:#include <linux/fs.h>。

步骤4:编译与测试驱动

  1. 编译驱动:执行make命令,编译生成hello_driver.ko文件;
  2. 装载驱动:sudo insmod hello_driver.ko,通过lsmod命令可查看已装载的驱动程序;
  3. 验证注册结果:执行cat /proc/devices命令,查看字符设备列表,可找到“hello_driver”及系统分配的主设备号(如241);
  4. 卸载驱动:sudo rmmod hello_driver,再次执行cat /proc/devices,会发现hello_driver已从列表中消失。

此时注册的字符设备驱动,file_operations结构体中未实现open、read、write等函数。接下来我们思考:应用程序能否调用open、read、write访问该驱动?

四、内核层逻辑分析:接口函数的容错机制

应用程序调用open、read、write等系统调用时,最终会进入内核的sys_open、sys_read、sys_write函数,进而调用do_sys_open等底层函数。内核会判断驱动程序的file_operations结构体是否实现了对应的接口函数:

  1. 若驱动实现了该接口函数(如open),则调用驱动的对应函数;
  2. 若驱动未实现,则内核不会调用(避免空指针错误),可能直接返回错误。

内核的这一容错机制,确保了即使驱动未实现某些非必需接口,也不会导致内核崩溃。

五、完善驱动:实现read与write函数

为打通数据链路,我们在file_operations结构体中实现read和write函数,仅添加打印功能和固定返回值(验证调用关系)。

步骤1:实现read与write函数


// read函数:应用程序调用read时触发
staticssize_thello_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
    printk("hello_read is called\n");
return1; // 返回1,表示成功读取1个字节(实际未处理数据)
}

// write函数:应用程序调用write时触发
staticssize_thello_write(struct file *file, constchar __user *buf, size_t count, loff_t *ppos)
{
    printk("hello_write is called\n");
return1; // 返回1,表示成功写入1个字节(实际未处理数据)
}

步骤2:关联函数到file_operations结构体


staticstructfile_operationsfops = {
    .owner = THIS_MODULE,
    .read = hello_read,  // 关联read函数
    .write = hello_write, // 关联write函数
};

步骤3:测试验证

  1. 编译驱动:make;
  2. 装载新驱动:sudo insmod hello_driver.ko;
  3. 执行应用程序测试:
  • 读操作:sudo ./hello_driver_test read,打印“open成功,读的返回值是1”,通过dmesg可查看“hello_read is called”;
  • 写操作:sudo ./hello_driver_test write "abcdef",打印“open成功,写的返回值是1”,通过dmesg可查看“hello_write is called”。

测试结论:应用程序调用read、write时,成功触发了驱动程序中对应的hello_read、hello_write函数,“应用程序-驱动程序”的调用关系已打通。当前驱动程序仅实现了调用验证,未实际处理数据(读未返回有效数据,写未接收应用程序数据),后续我们会进一步完善数据处理逻辑。

六、应用程序与驱动程序的数据传递

应用程序调用read/write时,会传入数据缓冲区(buffer)和数据长度(len):read是将驱动数据读入缓冲区,write是将缓冲区数据写入驱动。但驱动程序无法直接访问应用程序的缓冲区——两者处于不同的地址空间(后续详细讲解地址空间概念)。

数据传递需通过专门的内核函数实现:

  1. 驱动→应用程序:驱动程序通过copy_to_user函数,将数据从内核空间拷贝到应用程序的用户空间缓冲区;
  2. 应用程序→驱动程序:驱动程序通过copy_from_user函数,将数据从应用程序的用户空间缓冲区拷贝到内核空间。

最新文章

随机文章