ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从零手搓Linux GPIO驱动:平台设备与字符设备开发实战

从零手搓Linux GPIO驱动:平台设备与字符设备开发实战 1. 项目概述为什么我们要手搓一套IO口驱动在嵌入式Linux开发里和硬件IO口打交道是家常便饭。你可能用过树莓派的GPIO库或者在STM32上用HAL库点过灯感觉挺方便。但当你真正需要把一个自定义的板子比如搭载了特定传感器或执行器的设备接入到Linux系统中时你会发现那些现成的库突然不灵了。系统根本不认识你的硬件/sys/class/gpio目录下也找不到对应的接口。这时候你就得直面Linux内核从零开始为你的硬件编写驱动并在上层提供一个清晰、安全的访问接口。这个项目要做的就是完整地走一遍这个流程“手搓”一个从用户空间应用程序到底层内核驱动完整控制一个物理IO口的代码。这不仅仅是点亮一个LED那么简单而是理解Linux设备驱动模型、掌握字符设备驱动开发、打通用户态与内核态数据交互的绝佳实践。无论是驱动一块OLED屏幕像热词里提到的hal库驱动oled代码还是与at24c16m、w25q128这类存储芯片通信其底层核心逻辑都是相通的——都是通过控制IO口的高低电平或模拟特定时序来实现的。我遇到过不少开发者能写复杂的应用逻辑但一碰到内核驱动就发怵觉得那是另一个世界的东西。其实驱动开发有它固定的“套路”。通过这个从零开始的IO口驱动项目我希望你能建立起一个清晰的认知框架用户程序如何发起请求 - 请求如何穿越系统调用进入内核 - 内核驱动如何接收并解析请求 - 驱动如何操作硬件寄存器 - 结果又如何返回给用户。这个过程就像为你打开了一扇通往Linux系统底层的大门。2. 核心思路与驱动模型选择在动手写代码之前我们必须先搞清楚在Linux里一个驱动应该以何种形式存在。Linux内核提供了多种设备模型比如平台设备platform device、PCI设备、USB设备等。对于我们这种简单的、映射到内存地址的IO口通常属于SoC芯片的片上外设最常用也最合适的就是平台设备驱动Platform Driver模型。2.1 为什么选择平台设备驱动模型你可以把平台设备理解为那些“板上钉钉”的设备它们的资源比如内存地址、中断号在系统启动时就已经确定并且通常通过设备树Device Tree来描述。我们的GPIO就属于这类。使用平台设备模型的好处是硬件与驱动解耦驱动代码只关心“怎么操作”而硬件资源哪个寄存器、地址是多少则通过设备树来指定。这样同一份驱动代码稍作修改或直接配合不同的设备树就能用在不同的板子上。符合Linux驱动框架能很好地集成到内核的设备模型中便于电源管理、热插拔虽然GPIO通常不支持热插拔等高级功能。资源管理规范内核会帮我们管理设备所需的资源如内存区域ioremap、中断等避免冲突。2.2 驱动形态字符设备确定了模型接下来要决定驱动以什么类型呈现给用户空间。对于GPIO这种可以提供“字节流”式访问的设备我们将其实现为一个字符设备Char Device。这意味着用户程序可以通过标准的文件操作接口open,read,write,ioctl,close来与它交互就像操作一个普通文件一样。这非常直观write可以设置IO口状态read可以读取IO口电平ioctl可以用来配置输入/输出模式、上拉下拉等。2.3 技术路线图我们的项目将严格遵循以下技术路线这也是一个标准Linux内核驱动开发的核心流程定义并注册平台设备在设备树.dts文件中描述我们的IO口硬件资源寄存器基地址、偏移量等。编写平台驱动实现一个platform_driver在其probe函数中完成设备的初始化包括将物理地址映射到内核虚拟地址ioremap以及注册字符设备。实现字符设备操作集定义file_operations结构体实现open、release、read、write、unlocked_ioctl等回调函数这些函数将直接处理来自用户空间的请求。实现硬件操作函数在驱动内部编写具体的函数来读写GPIO控制器寄存器从而控制目标IO口。编写上层测试应用创建一个用户空间的C程序通过打开设备文件、调用ioctl或write来测试驱动功能。3. 环境准备与基础概念澄清在开始编码前确保你有一个可用的Linux开发环境。你需要Linux内核源码最好与你目标运行环境的内核版本一致。可以从 kernel.org 下载。交叉编译工具链如果你的目标板是ARM架构如树莓派、i.MX6UL等就需要arm-linux-gnueabihf-gcc这类工具链。目标开发板一块带有可编程GPIO的嵌入式板子并且已经运行了Linux系统。为了方便我们可以先在QEMU模拟的versatilepb开发板上进行实验它有一个简单的PL061 GPIO控制器非常适合学习。设备树编译器dtc用于编译我们编写的设备树源文件.dts为二进制文件.dtb。3.1 关键概念GPIO与内存映射I/O我们常说的“操作IO口”在硬件层面对于大多数现代微控制器和SoC来说就是读写特定的内存地址。这些地址对应着GPIO控制器的寄存器。例如方向寄存器GPIOx_DIR某一位写1对应的引脚就配置为输出写0则为输入。数据寄存器GPIOx_DATA当引脚为输出时向对应位写1输出高电平写0输出低电平当引脚为输入时读取该位获得引脚当前电平。CPU并不直接与物理地址打交道。在内核驱动中我们需要先用ioremap函数将这段物理地址空间映射到内核的虚拟地址空间得到一个可以像指针一样访问的虚拟地址。之后我们通过读写这个虚拟地址来操作硬件。注意直接使用ioremap映射后应使用iowrite32/ioread32等内核API进行读写而不是直接用C语言的指针解引用。这些API会确保读写顺序和内存屏障在多核系统或某些架构上至关重要。3.2 项目代码结构规划我们的驱动代码将主要包含两个文件my_gpio_driver.c驱动主体包含平台驱动和字符设备驱动的所有实现。my_gpio_app.c用户空间测试应用程序。此外还需要修改或添加设备树文件.dts。4. 从设备树开始向内核声明硬件设备树是连接硬件描述和驱动代码的桥梁。我们首先在目标板的设备树文件中添加一个节点来描述我们的“虚拟”GPIO设备。假设我们使用QEMU的versatilepb其GPIO控制器是pl061我们打算使用其中的GPIO #3。我们不一定需要新增一个独立节点可以直接在已有的pl061节点下通过gpio-line-names属性来标记或者更规范地我们新增一个基于pl061的platform_device。但为了教学清晰我们采用一个更通用的模拟方法我们假设自己有一个独立的“虚拟GPIO模块”其寄存器在物理地址0x101F3000这是versatilepb上pl061的实际地址我们复用。实际上更贴近真实项目的方式是在设备树中定义一个自定义节点并指定其兼容性compatible为我们驱动中定义的字符串。这样驱动就能匹配并管理它。示例设备树片段 (my-gpio-device.dtsi):/dts-v1/; / { compatible arm,versatilepb; my_gpio_device { compatible my-company,my-gpio; // 此字符串必须与驱动中的一致 reg 0x101f3000 0x1000; // GPIO控制器的寄存器基地址和长度 gpio-controller; #gpio-cells 2; status okay; label my_misc_gpio; // 可以定义具体的引脚这里我们假设控制第3号引脚 my-gpio-pin 3; }; };这个节点告诉内核有一个兼容性为”my-company,my-gpio”的设备它的寄存器位于物理地址0x101f3000长度0x1000。gpio-controller和#gpio-cells属性声明了它是一个GPIO控制器。my-gpio-pin是我们自定义的一个属性用于告诉驱动我们具体操作哪个引脚。将这段内容包含到主设备树文件中编译成.dtb并加载到目标板或QEMU内核在启动时就会解析到这个设备节点。5. 内核驱动实现详解现在进入核心部分——编写my_gpio_driver.c。我们将分模块讲解。5.1 头文件与数据结构定义#include linux/module.h #include linux/fs.h #include linux/cdev.h #include linux/device.h #include linux/platform_device.h #include linux/uaccess.h // copy_to_user, copy_from_user #include linux/io.h // ioremap, iowrite32, ioread32 #include linux/slab.h // kmalloc, kfree #define DRIVER_NAME my_gpio_driver #define DEVICE_NAME my_gpio #define CLASS_NAME my_gpio_class // 自定义IOCTL命令 #define MY_GPIO_MAGIC G #define MY_GPIO_SET_DIR_OUT _IOW(MY_GPIO_MAGIC, 1, int) #define MY_GPIO_SET_DIR_IN _IOW(MY_GPIO_MAGIC, 2, int) #define MY_GPIO_WRITE_HIGH _IOW(MY_GPIO_MAGIC, 3, int) #define MY_GPIO_WRITE_LOW _IOW(MY_GPIO_MAGIC, 4, int) #define MY_GPIO_READ _IOR(MY_GPIO_MAGIC, 5, int) // 假设的寄存器偏移量 (基于简化模型真实PL061寄存器不同) #define GPIO_DIR_OFFSET 0x400 // 方向寄存器偏移 #define GPIO_DATA_OFFSET 0x000 // 数据寄存器偏移 struct my_gpio_dev { dev_t devno; // 设备号 struct cdev cdev; // 字符设备结构 struct class *cls; // 设备类 struct device *device; // 设备实例 void __iomem *reg_base; // 映射后的寄存器基地址虚拟指针 int gpio_pin; // 要控制的GPIO引脚号 spinlock_t lock; // 自旋锁用于保护并发访问 };这里定义了驱动名、设备名以及一组ioctl命令。struct my_gpio_dev是我们自定义的设备结构体它保存了驱动运行所需的所有上下文信息这是一个非常重要的设计模式。5.2 平台驱动的探针Probe与移除Removeprobe函数是驱动匹配到设备后内核自动调用的初始化入口。static int my_gpio_probe(struct platform_device *pdev) { int ret 0; struct resource *mem_res; struct my_gpio_dev *my_dev; struct device *dev pdev-dev; // 1. 为设备结构体分配内存 my_dev devm_kzalloc(dev, sizeof(*my_dev), GFP_KERNEL); if (!my_dev) return -ENOMEM; // 2. 从设备树获取资源寄存器内存区域 mem_res platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!mem_res) { dev_err(dev, Failed to get MEM resource\n); return -ENXIO; } // 3. 将物理内存映射到内核虚拟空间 my_dev-reg_base devm_ioremap_resource(dev, mem_res); if (IS_ERR(my_dev-reg_base)) { dev_err(dev, Failed to ioremap memory\n); return PTR_ERR(my_dev-reg_base); } // 4. 从设备树获取自定义属性gpio引脚号 ret of_property_read_u32(dev-of_node, my-gpio-pin, my_dev-gpio_pin); if (ret) { dev_err(dev, Failed to get my-gpio-pin from DT, using default 3\n); my_dev-gpio_pin 3; // 默认值 } dev_info(dev, Probed GPIO device at pin %d\n, my_dev-gpio_pin); // 5. 初始化自旋锁 spin_lock_init(my_dev-lock); // 6. 分配设备号 (动态分配) ret alloc_chrdev_region(my_dev-devno, 0, 1, DEVICE_NAME); if (ret 0) { dev_err(dev, Failed to allocate chrdev region\n); return ret; } // 7. 初始化字符设备结构体关联文件操作函数集 cdev_init(my_dev-cdev, my_gpio_fops); my_dev-cdev.owner THIS_MODULE; // 8. 将字符设备添加到系统 ret cdev_add(my_dev-cdev, my_dev-devno, 1); if (ret 0) { dev_err(dev, Failed to add cdev\n); goto err_cdev_add; } // 9. 创建设备类在/sys/class下可见 my_dev-cls class_create(THIS_MODULE, CLASS_NAME); if (IS_ERR(my_dev-cls)) { ret PTR_ERR(my_dev-cls); dev_err(dev, Failed to create device class\n); goto err_class_create; } // 10. 在/dev下创建设备节点 my_dev-device device_create(my_dev-cls, NULL, my_dev-devno, NULL, DEVICE_NAME); if (IS_ERR(my_dev-device)) { ret PTR_ERR(my_dev-device); dev_err(dev, Failed to create device\n); goto err_device_create; } // 11. 将设备私有数据保存到platform_device中便于其他地方获取 platform_set_drvdata(pdev, my_dev); dev_info(dev, My GPIO driver probed successfully\n); return 0; // 错误处理路径逆向释放资源 err_device_create: class_destroy(my_dev-cls); err_class_create: cdev_del(my_dev-cdev); err_cdev_add: unregister_chrdev_region(my_dev-devno, 1); return ret; }probe函数像是一个总装配线它按顺序获取资源、映射内存、初始化数据结构、注册字符设备、创建sysfs和devfs节点。每个步骤都有错误检查一旦失败就跳转到清理环节确保不会泄露资源。这是驱动稳定性的基石。remove函数是probe的镜像负责清理所有资源static int my_gpio_remove(struct platform_device *pdev) { struct my_gpio_dev *my_dev platform_get_drvdata(pdev); device_destroy(my_dev-cls, my_dev-devno); class_destroy(my_dev-cls); cdev_del(my_dev-cdev); unregister_chrdev_region(my_dev-devno, 1); // devm_ioremap_resource 分配的资源会自动释放无需手动iounmap dev_info(pdev-dev, My GPIO driver removed\n); return 0; }注意我们使用了devm_系列函数如devm_kzalloc,devm_ioremap_resource它们是“设备管理”的API其分配的资源会在设备detach时由内核自动释放大大减少了内存泄漏的风险是现在推荐的写法。5.3 字符设备文件操作集实现这是驱动与用户程序交互的接口。我们定义my_gpio_fopsstatic struct file_operations my_gpio_fops { .owner THIS_MODULE, .open my_gpio_open, .release my_gpio_release, .unlocked_ioctl my_gpio_ioctl, .read my_gpio_read, .write my_gpio_write, };open和release通常用于初始化和清理与文件描述符相关的私有数据。这里我们简单实现static int my_gpio_open(struct inode *inode, struct file *filp) { struct my_gpio_dev *my_dev; // 通过inode-i_cdev获取cdev再通过container_of找到我们的设备结构体 my_dev container_of(inode-i_cdev, struct my_gpio_dev, cdev); filp-private_data my_dev; // 将设备结构体保存到文件私有数据中便于其他函数使用 return 0; } static int my_gpio_release(struct inode *inode, struct file *filp) { // 这里可以进行一些清理工作本例中无特殊操作 return 0; }核心的ioctl函数是控制的中枢static long my_gpio_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { struct my_gpio_dev *my_dev filp-private_data; int ret 0; int pin my_dev-gpio_pin; unsigned long flags; // 用于保存中断状态配合自旋锁 // 检查命令是否是我们定义的 if (_IOC_TYPE(cmd) ! MY_GPIO_MAGIC) return -ENOTTY; if (_IOC_NR(cmd) 5) return -ENOTTY; switch (cmd) { case MY_GPIO_SET_DIR_OUT: spin_lock_irqsave(my_dev-lock, flags); // 假设方向寄存器1为输出0为输入。将对应pin的位设置为1。 // 这里需要先读取修改指定位再写回。为简化假设直接操作。 // iowrite32(ioread32(my_dev-reg_base GPIO_DIR_OFFSET) | (1 pin), // my_dev-reg_base GPIO_DIR_OFFSET); spin_unlock_irqrestore(my_dev-lock, flags); dev_dbg(my_dev-device, Set pin %d as OUTPUT\n, pin); break; case MY_GPIO_SET_DIR_IN: spin_lock_irqsave(my_dev-lock, flags); // iowrite32(ioread32(my_dev-reg_base GPIO_DIR_OFFSET) ~(1 pin), // my_dev-reg_base GPIO_DIR_OFFSET); spin_unlock_irqrestore(my_dev-lock, flags); dev_dbg(my_dev-device, Set pin %d as INPUT\n, pin); break; case MY_GPIO_WRITE_HIGH: spin_lock_irqsave(my_dev-lock, flags); // iowrite32(ioread32(my_dev-reg_base GPIO_DATA_OFFSET) | (1 pin), // my_dev-reg_base GPIO_DATA_OFFSET); spin_unlock_irqrestore(my_dev-lock, flags); dev_dbg(my_dev-device, Set pin %d HIGH\n, pin); break; case MY_GPIO_WRITE_LOW: spin_lock_irqsave(my_dev-lock, flags); // iowrite32(ioread32(my_dev-reg_base GPIO_DATA_OFFSET) ~(1 pin), // my_dev-reg_base GPIO_DATA_OFFSET); spin_unlock_irqrestore(my_dev-lock, flags); dev_dbg(my_dev-device, Set pin %d LOW\n, pin); break; case MY_GPIO_READ: { int value; spin_lock_irqsave(my_dev-lock, flags); // value (ioread32(my_dev-reg_base GPIO_DATA_OFFSET) pin) 0x1; value 0; // 模拟读取 spin_unlock_irqrestore(my_dev-lock, flags); // 将值拷贝回用户空间 if (copy_to_user((int __user *)arg, value, sizeof(value))) ret -EFAULT; dev_dbg(my_dev-device, Read pin %d value: %d\n, pin, value); break; } default: ret -ENOTTY; break; } return ret; }重要提示上面的寄存器操作代码被注释掉了因为真实的PL061 GPIO控制器寄存器布局和操作方式更为复杂涉及GPIO_DIR、GPIO_DATA等多个寄存器并且数据寄存器有“写1置位/写0清除”的机制。这里的代码展示了在ioctl中如何根据命令进行分支处理、如何使用自旋锁保护临界区防止多进程/线程同时操作寄存器导致竞态、以及如何使用copy_to_user将内核数据传回用户空间。在实际项目中你必须查阅芯片数据手册来编写正确的寄存器操作序列。read和write函数提供了流式接口例如write可以接收一个字符串”high”或”low”来控制电平static ssize_t my_gpio_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) { struct my_gpio_dev *my_dev filp-private_data; char kbuf[32]; unsigned long flags; if (count sizeof(kbuf)) count sizeof(kbuf) - 1; if (copy_from_user(kbuf, buf, count)) return -EFAULT; kbuf[count] \0; spin_lock_irqsave(my_dev-lock, flags); if (strncmp(kbuf, high, 4) 0) { // 设置高电平 dev_dbg(my_dev-device, Write HIGH via write()\n); } else if (strncmp(kbuf, low, 3) 0) { // 设置低电平 dev_dbg(my_dev-device, Write LOW via write()\n); } spin_unlock_irqrestore(my_dev-lock, flags); return count; } static ssize_t my_gpio_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) { struct my_gpio_dev *my_dev filp-private_data; char kbuf[32]; int len; unsigned long flags; int value; spin_lock_irqsave(my_dev-lock, flags); // value 读取引脚电平 value 0; spin_unlock_irqrestore(my_dev-lock, flags); len snprintf(kbuf, sizeof(kbuf), GPIO pin %d value: %d\n, my_dev-gpio_pin, value); if (len count) len count; if (copy_to_user(buf, kbuf, len)) return -EFAULT; *f_pos len; return len; }5.4 平台驱动定义与模块入口最后我们将上述所有部分组装起来// 定义平台驱动结构体 static struct platform_driver my_gpio_platform_driver { .probe my_gpio_probe, .remove my_gpio_remove, .driver { .name DRIVER_NAME, .owner THIS_MODULE, .of_match_table my_gpio_of_match, // 设备树匹配表 }, }; // 模块初始化注册平台驱动 static int __init my_gpio_driver_init(void) { pr_info(My GPIO driver init\n); return platform_driver_register(my_gpio_platform_driver); } // 模块退出注销平台驱动 static void __exit my_gpio_driver_exit(void) { pr_info(My GPIO driver exit\n); platform_driver_unregister(my_gpio_platform_driver); } module_init(my_gpio_driver_init); module_exit(my_gpio_driver_exit); MODULE_LICENSE(GPL); MODULE_AUTHOR(Your Name); MODULE_DESCRIPTION(A simple GPIO driver from userspace to kernel); MODULE_VERSION(1.0);6. 用户空间测试应用程序驱动加载后会在/dev下生成设备节点如/dev/my_gpio。我们可以编写一个简单的用户程序来测试它。my_gpio_app.c:#include stdio.h #include stdlib.h #include fcntl.h #include unistd.h #include sys/ioctl.h #include string.h // 必须与驱动中定义的命令完全一致 #define MY_GPIO_MAGIC G #define MY_GPIO_SET_DIR_OUT _IOW(MY_GPIO_MAGIC, 1, int) #define MY_GPIO_SET_DIR_IN _IOW(MY_GPIO_MAGIC, 2, int) #define MY_GPIO_WRITE_HIGH _IOW(MY_GPIO_MAGIC, 3, int) #define MY_GPIO_WRITE_LOW _IOW(MY_GPIO_MAGIC, 4, int) #define MY_GPIO_READ _IOR(MY_GPIO_MAGIC, 5, int) int main(int argc, char **argv) { int fd; int value; fd open(/dev/my_gpio, O_RDWR); if (fd 0) { perror(Failed to open device); return -1; } printf(Testing GPIO driver...\n); // 1. 设置为输出模式 if (ioctl(fd, MY_GPIO_SET_DIR_OUT, 0) 0) { perror(ioctl SET_DIR_OUT failed); close(fd); return -1; } printf(Set as OUTPUT\n); // 2. 输出高电平 if (ioctl(fd, MY_GPIO_WRITE_HIGH, 0) 0) { perror(ioctl WRITE_HIGH failed); close(fd); return -1; } printf(Set HIGH\n); sleep(1); // 3. 输出低电平 if (ioctl(fd, MY_GPIO_WRITE_LOW, 0) 0) { perror(ioctl WRITE_LOW failed); close(fd); return -1; } printf(Set LOW\n); sleep(1); // 4. 设置为输入模式并读取 (注意实际硬件需外部电路配合) if (ioctl(fd, MY_GPIO_SET_DIR_IN, 0) 0) { perror(ioctl SET_DIR_IN failed); close(fd); return -1; } printf(Set as INPUT\n); if (ioctl(fd, MY_GPIO_READ, value) 0) { perror(ioctl READ failed); close(fd); return -1; } printf(Read value: %d\n, value); // 5. 使用write/read接口 write(fd, high, 4); sleep(1); write(fd, low, 3); char buf[64]; int n read(fd, buf, sizeof(buf)-1); if (n 0) { buf[n] \0; printf(Read from driver: %s, buf); } close(fd); return 0; }编译这个应用使用交叉编译工具链拷贝到开发板运行你就能通过驱动控制真实的GPIO电平了。7. 编译、加载与调试实战7.1 驱动编译的Makefile驱动代码需要编译成内核模块.ko文件。一个典型的Makefile如下obj-m my_gpio_driver.o KDIR ? /lib/modules/$(shell uname -r)/build all: make -C $(KDIR) M$(PWD) modules clean: make -C $(KDIR) M$(PWD) clean在驱动源码目录下执行make生成my_gpio_driver.ko。7.2 加载驱动与查看日志将.ko文件拷贝到目标板# 加载模块 sudo insmod my_gpio_driver.ko # 查看内核日志确认probe是否成功 dmesg | tail -20 # 应该能看到 “My GPIO driver probed successfully” 和创建设备节点的信息。 # 检查设备节点是否存在 ls -l /dev/my_gpio # 卸载模块 sudo rmmod my_gpio_driver7.3 调试技巧与常见问题insmod失败提示Unknown symbol这通常是因为驱动引用了未导出的内核函数或者依赖的其他模块未加载。使用modinfo my_gpio_driver.ko查看依赖并用modprobe自动解决依赖。对于自定义函数需要EXPORT_SYMBOL。设备节点未创建检查/sys/class/下是否有my_gpio_class目录。如果没有可能是class_create失败如果有但/dev下没有节点可能是device_create失败或udev规则问题。查看dmesg日志是关键。ioctl返回-1错误码ENOTTY25这表示命令不对。确保用户空间和内核空间定义的ioctl命令类型MY_GPIO_MAGIC和编号完全一致。使用_IOC_TYPE(cmd)和_IOC_NR(cmd)宏在驱动中打印出来对比。操作GPIO无效果这是最可能的问题。寄存器地址错误确认设备树中的reg地址和长度是否正确映射到了真实的GPIO控制器。寄存器操作错误这是重灾区。必须仔细查阅芯片数据手册。GPIO寄存器可能不是简单的“写1置1”。例如有些芯片的数据寄存器是“写1置位写0不变”需要配合“置位/清除寄存器”使用。pl061的GPIO_DATA寄存器访问是带掩码的操作方式比较特殊。引脚复用问题SoC的引脚通常有多种功能GPIO、UART、I2C等。可能默认被配置为其他功能。需要在设备树中通过pinctrl子系统将引脚复用mux为GPIO功能。这是嵌入式Linux驱动开发中一个关键的、容易忽略的步骤。并发访问导致系统不稳定我们的驱动使用了自旋锁保护寄存器操作这是正确的。但在read/write函数中如果操作涉及多个步骤如先读后写也必须用锁保护整个序列防止中间被其他进程打断。使用dev_dbg进行动态调试在驱动中大量使用dev_dbg()代替printk它可以通过动态调试开关CONFIG_DYNAMIC_DEBUG在运行时启用或禁用非常方便。通过echo file my_gpio_driver.c p /sys/kernel/debug/dynamic_debug/control来开启该文件的所有调试信息。8. 进阶思考与扩展这个简单的IO口驱动框架可以扩展到更复杂的设备比如热词中提到的ds1302驱动代码实时时钟需要模拟3线串行时序、tmc2226sa的uart驱动代码步进电机驱动通过UART配置或电机驱动代码可能需要PWM接口。其核心思想不变定义设备在设备树中描述硬件寄存器、中断、时钟、引脚复用等。实现驱动编写平台驱动在probe中获取资源、映射寄存器、注册字符设备或其它类型设备如i2c_driver,spi_driver。实现操作集根据设备功能实现read、write、ioctl在底层通过读写寄存器或使用内核子系统如GPIO子系统、PWM子系统、IIO子系统的API来完成硬件操作。提供用户接口通过设备文件或sysfs属性文件向用户空间提供控制接口。例如要驱动一个OLED屏幕hal库驱动oled代码的Linux版你可能需要将其定义为spi或i2c设备。实现一个spi_driver或i2c_driver。在驱动中实现write函数接收用户空间发来的像素数据或命令通过SPI/I2C总线发送给OLED控制器。可能还需要实现一个framebuffer驱动这样就能直接使用标准的Linux图形显示接口。手搓这个IO口驱动的过程就像在拼一张地图。一开始你只看到几个零散的碎片设备树、平台驱动、字符设备、文件操作但随着每一步的完成碎片逐渐连接起来最终呈现出一条从用户命令到硬件电平的完整路径。这条路就是Linux设备驱动的精髓所在。下次当你再看到linux内核增加网络驱动这样的需求时你会明白它不过是换了一套“地图碎片”网络设备接口net_device但拼接的逻辑是相通的。
返回列表