
一、驱动工程搭建1、用户应用编程 VS 内核驱动编程差异应用程序运行在用户空间驱动属于内核级别编程代码属于操作系统内核一部分。核心问题如何在已经运行的大型内核代码中插入、执行我们自己的代码驱动本质最终还是操作寄存器原理和裸机一致区别是内核下要做物理地址→虚拟地址映射。2、驱动两种加载方式方式 1静态编译进内核编写驱动.c源码修改Kconfig、Makefilemake menuconfig勾选驱动*代表编译进 zImage驱动代码被打包进 zImage 镜像zImage 体积变大必须重新烧写内核、重启开发板驱动才能生效。方式 2动态模块 ko推荐调试使用编写驱动.c源码修改 Makefileobj‑m xxx.o执行make modules生成.ko模块文件ko 独立于 zImage 镜像不修改内核镜像开发板 Linux 运行后手动加载卸载insmod xxx.ko加载内核模块lsmod查看已经加载的内核模块rmmod xxx卸载内核模块优点不需要重启板子调试效率高。3、内核模块基础宏宏功能说明module_init(func)模块加载入口函数insmod 的时候执行module_exit(func)模块卸载入口函数rmmod 的时候执行MODULE_LICENSE(GPL)许可证声明必须写否则内核报警告部分接口不可用printk()内核打印函数代替应用层 printf不能打印浮点数pr_info(fmt,...)带日志等级的打印普通提示信息pr_err(fmt,...)带日志等级的打印打印错误信息#include linux/init.h #include linux/module.h #include linux/printk.h static int __init mymodule_init(void) { pr_info(mymodule_init ok\n); return 0; } static void __exit mymodule_exit(void) { pr_info(mymodule_exit byebye\n); } module_init(mymodule_init); module_exit(mymodule_exit); MODULE_LICENSE(GPL);4、两种工程组织方式① 放在内核源码树内部开发drivers 目录下新增驱动步骤在drivers/char新建mydevtest.c驱动源码修改同目录Kconfig添加配置项config MYMODULE tristate mymodule inner kernel test default y help driver test kconfig makefile修改同目录Makefileobj‑$(CONFIG_MYMODULE) myhello.omake menuconfig选中对应模块make modules生成 ko将 ko 拷贝到 nfs 的 rootfs 目录开发板 insmod 加载测试。Kconfig控制 menuconfig 图形配置界面Makefile 根据 CONFIG_xxx 变量决定是否编译文件。② 源码树外部独立工程实际项目推荐不改动内核源码驱动工程和内核源码分开放。 目录结构示例├── app # 用户应用程序 │ ├── app.c │ └── Makefile ├── drv # 内核驱动代码 │ ├── .clangd │ ├── compile_commands.json #软链接指向内核源码生成的该文件用于vscode代码补全 │ ├── drv.c │ └── Makefile └── Makefile #顶层总Makefiledrv/Makefile示例MODULE_NAME: drv KER_DIR : /home/linux/imx6ull/linux‑imx‑rel_imx_4.1.15_2.1.0_ga_alientek CUR_DIR : $(shell pwd) obj‑m $(MODULE_NAME).o all: make -C $(KER_DIR) M$(CUR_DIR) modules cp $(MODULE_NAME).ko ~/nfs/rootfs clean: make -C $(KER_DIR) M$(CUR_DIR) clean distclean: rm $(MODULE_NAME).ko make -C $(KER_DIR) M$(CUR_DIR) distcleanapp/Makefile示例SRC:app.c OBJ:app CC:arm‑linux‑gnueabihf‑gcc all: $(CC) $(SRC) -o $(OBJ) clean: rm $(OBJ)顶层 Makefileall: make -C ./app all make -C ./drv all clean: make -C ./app clean make -C ./drv clean前置条件内核源码必须完整编译过一次外部模块编译依赖内核编译产物。5、字符设备驱动基础框架Linux 驱动三大分类1字符设备按字节流顺序读写GPIO、I2C、SPI、UART、LED、按键、LCD、摄像头占驱动 95% 以上2块设备按块读写SD、EMMC、Flash 存储设备随机访问吞吐量大3网络设备wifi、蓝牙、CAN。字符设备开发步骤1alloc_chrdev_region()动态分配设备号推荐register_chrdev_region()静态注册已知设备号2cdev_alloc()/cdev_init()初始化字符设备结构体绑定file_operations3cdev_add()将字符设备注册进内核4file_operations结构体实现open/read/write/release用户态系统调用最终会回调这些内核函数5点两种方创建设备节式手动mknod /dev/led0 c 主设备号 次设备号自动class_create()device_create()sysfs 自动生成 /dev 设备文件6卸载的时候依次执行cdev_del()、unregister_chrdev_region()、device_destroy()、class_destroy()。字符设备 API功能cdev_init初始化 cdev 结构体绑定 file_operationscdev_alloc动态申请 cdev 结构体内存cdev_add把 cdev 注册到内核驱动正式生效cdev_del从内核删除 cdev 设备alloc_chrdev_region动态分配主次设备号unregister_chrdev_region释放已经注册的设备号/proc/devices可以查看系统所有已经注册的字符设备主设备号。file_operations 说明应用层调用 open/read/write/close内核就会调用驱动中file_operations对应的函数指针。6、sysfs 虚拟文件系统sysfs 是内存虚拟文件系统不占用磁盘存储挂载在/sys。sysfs 重要目录目录作用/sys/devices真实物理设备/sys/bus总线devices 子目录存放设备drivers 存放对应驱动/sys/class设备分类按设备类型分组tty、led、input 等/sys/module已经加载的内核模块驱动中使用 sysfsclass_create()创建设备类device_create()创建设备自动生成/dev/xxx节点__ATTR宏定义设备属性device_create_file()创建 sys 属性文件用户空间直接cat /sys/xxx/xxx读echo value /sys/xxx/xxx写用于调试硬件不用写专门应用程序。注意内核属性文件权限不能设置 0666只能使用 0664。show函数返回读取字符串长度store函数必须返回传入数据长度否则应用层会阻塞。相关 API函数功能class_create创建设备类目录class_destroy销毁设备类device_create创建设备自动生成 /dev 节点device_destroy销毁设备__ATTR初始化设备属性结构体device_create_file生成 sys 属性文件device_remove_file删除 sys 属性文件7、misc 杂项设备主设备号固定 10属于字符设备的简化封装主设备号固定为 10自动分配次设备号自动创建设备节点。可以省去alloc_chrdev_region、cdev_init、cdev_add、手动 mknod、class/device 的部分代码。misc API功能misc_register()注册杂项设备misc_deregister()注销杂项设备二、LED 灯驱动知识点1、硬件原理图要点LED低电平点亮高电平熄灭串联 510Ω 限流电阻。 引脚LED0 接到 GPIO1_IO03。2、裸机操作需要的寄存器IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03引脚复用功能寄存器IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03引脚电气特性配置GPIO1_DRGPIO 数据寄存器写 0 输出低电平写 1 输出高电平GPIO1_GDIRGPIO 方向寄存器bit 置 1 配置输出模式。⚠️内核中不能直接操作物理寄存器地址必须做物理地址→虚拟地址映射。3、内存映射相关 API内核访问硬件寄存器头文件#include linux/io.h函数功能说明ioremap(phys, size)把物理地址映射成内核虚拟地址返回虚拟地址指针iounmap(virt_addr)解除映射devm_ioremap(phys,size)资源托管映射设备卸载驱动自动 iounmap 释放推荐优先使用防止内存泄漏devm_ioremap_resource()检查资源冲突设备树驱动优先使用寄存器读写函数32 位 SOCimx6ull 寄存器都是 32bitreadl(addr)读取 32 位寄存器writel(value, addr)写 32 位寄存器。4、用户空间 ↔ 内核空间数据拷贝驱动 read/write 函数中不能直接解引用用户空间指针必须使用专用拷贝函数。copy_from_user(kbuf, ubuf, size)用户空间数据拷贝到内核空间copy_to_user(ubuf, kbuf, size)内核空间数据拷贝到用户空间5、两套 LED 驱动实现思路思路 1直接硬编码寄存器地址不使用设备树ioremap 映射 4 个寄存器物理地址得到虚拟地址writel 配置引脚复用、PAD 电气属性writel 设置 GPIO 方向为输出writel 写 DR 寄存器控制输出高低电平实现 LED 亮灭在 file_operations 的 write 函数接收用户层传进来的亮灭值操作寄存器模块 exit 中 iounmap 释放映射。缺点硬件换板子寄存器地址变了驱动代码必须修改可移植性差。思路 2设备树方式 LED 驱动工程实际推荐设备与驱动分离Linux3.x 之后引入设备树思想硬件资源寄存器、引脚、中断写在 dts 设备树驱动写通用逻辑换硬件只改 dts不用修改驱动 c 代码。设备树相关文件后缀后缀含义.dts板级设备树对应开发板硬件配置.dtsi公共头文件多个 dts 可以 include 引用.dtbDTC 工具编译 dts/dtsi 得到的二进制uboot 加载传递给内核6、编译与测试完整流程修改 dts配置 led 节点pinctrlgpio编译 dtb放到 tftpboot编写驱动 c 代码使用外部 Makefile 编译生成 koko 拷贝到 nfs 共享 rootfs开发板网络启动insmod led_drv.ko加载驱动测试方式二选一应用程序 open /dev/xxxwrite 写 1/0 控制亮灭sysfs 节点echo 1 /sys/devices/virtual/LED/led0/led_statecat /sys/devices/virtual/LED/led0/led_statermmod led_drv卸载驱动。