总结驱动开发中的常用接口内核错误码内核和应用共用一套基础错误码数字但用法有区别。错误号的数值EINVAL22、ENOMEM12 等内核和应用完全一样但返回方式不一样应用层直接返回正整数内核层返回负的错误码-EINVAL、-ENOMEM1. 错误码定义在哪基础错误码都在这include/uapi/asm-generic/errno-base.h include/uapi/linux/errno.h这些是uapi user api意思就是给内核 用户态共同使用的标准定义所以应用#include errno.h内核#include linux/errno.h看到的EINVAL都是22一模一样。2. 关键区别返回时带不带负号应用层用户态 C 语言系统调用失败时库函数会把错误码放到全局变量errno里是正数if (open(...) 0) { printf(%d\n, errno); // 输出 22 这种正数 }内核态驱动、内核函数直接返回负的错误码return -EINVAL; // 实际返回 -22 return -ENOMEM; // 返回 -12内核返回-EINVAL→ 到了应用层 libc 会处理成返回-1errno EINVAL223. 所以可以理解成错误码编号内核 ↔ 应用完全通用内核返回负值应用看到的 errno 是正值4. 你在 WiFi 驱动里常见的对应关系内核里写实际值应用 errno含义-EINVAL-22EINVAL22无效参数-ENOMEM-12ENOMEM12内存不足-EBUSY-16EBUSY16设备忙-ETIMEDOUT-110ETIMEDOUT110超时-EIO-5EIO5I/O 错误-ENODEV-19ENODEV19无此设备一句话记住内核和应用共用同一套错误号内核返回负的应用层 errno 是正的。IS_ERR/PTR_ERR等错误处理Linux 内核指针错误处理全套接口终极总结我给你整理驱动里 100% 会遇到的所有指针错误处理宏 / 函数你截图里的IS_ERR / PTR_ERR只是其中两个。这些是内核通用不是某个子系统专属platform、i2c、spi、gpio、regulator、net 全都用这套一、内核为什么要用这套错误处理内核很多函数返回指针void*/struct xxx*但又想返回错误码负数-ENOMEM、-EINVAL、-EPROBE_DEFER于是内核设计了一套规则正常指针 有效地址错误指针 把错误码编码进指针特殊地址所以必须用专用宏判断、提取。二、全套 5 个核心错误处理宏驱动必背1. IS_ERR(ptr)判断指针是不是错误指针if (IS_ERR(ptr)) { // 出错了 }2. PTR_ERR(ptr)从错误指针里提取错误码负数int err PTR_ERR(ptr);3. ERR_PTR(err)把错误码变成错误指针return ERR_PTR(-ENOMEM);4. IS_ERR_OR_NULL(ptr)指针是 NULL 或者错误指针 → 都算失败if (IS_ERR_OR_NULL(ptr)) return -EINVAL;5. ERR_CAST(ptr)强制类型转换错误指针很少用三、最常用组合驱动 99% 都是这样标准写法 1获取资源失败处理struct gpio_desc *gpiod devm_gpiod_get(dev, reset, GPIOD_OUT_LOW); if (IS_ERR(gpiod)) { // 是不是错误指针 int err PTR_ERR(gpiod); // 拿出错误码 return err; }标准写法 2返回错误指针if (!reg) return ERR_PTR(-ENODEV); // 把错误码变指针标准写法 3判断 NULL 错误if (IS_ERR_OR_NULL(ptr)) return -EINVAL;四、你截图里的代码就是这套if (IS_ERR(power)) { if (PTR_ERR(power) -EPROBE_DEFER) return -EPROBE_DEFER; dev_dbg(...); }IS_ERR→ 判断是不是错误PTR_ERR→ 拿出错误码检查是不是-EPROBE_DEFERprobe 延迟五、常见错误码你会大量看到这些是PTR_ERR拿出来的最常见错误-ENOMEM 内存不足 -EINVAL 参数无效 -ENODEV 无此设备 -EBUSY 设备忙 -EPROBE_DEFER 依赖未就绪最常见于电源、时钟 -ETIMEDOUT 超时 -EIO IO错误六、极简总结背这 4 个就够IS_ERR 是不是错误指针 PTR_ERR 拿出错误码 ERR_PTR 把错误码变指针 IS_ERR_OR_NULL 错误或空内核中的打印接口一、最核心、最常用的 5 个打印接口驱动里 99% 都是这 5 个1. printk —— 最原始、最通用printk(0Hello World\n);内核最基础打印可以指定日志级别任何驱动、任何地方都能用2. pr_info —— 最常用正常信息pr_info(probe ok\n);等价于printk(KERN_INFO ...)驱动正常流程打印3. pr_err —— 最常用错误打印pr_err(failed to request gpio\n);等价于printk(KERN_ERR ...)出错时必用4. pr_warn —— 警告pr_warn(gpio not found\n);5. pr_debug —— 调试打印默认不输出pr_debug(enter %s\n, __func__);需要开 DEBUG 才会输出二、带设备名的打印驱动最推荐比 pr_info 更好会自动显示设备名看 log 非常清晰1. dev_infodev_info(dev, probe success\n);2. dev_errdev_err(dev, reset gpio failed\n);3. dev_warndev_warn(dev, no irq\n);4. dev_dbgdev_dbg(dev, read reg %x\n, val);✔ 优点自动带eth0sdioi2c设备名看 dmesg 一眼知道是谁打的写驱动、看驱动 90% 都用这个三、专门给网络驱动用的打印你会大量看到你看dm9000、WiFi、以太网驱动会遇到1. netdev_errnetdev_err(dev, tx timeout\n);2. netdev_infonetdev_info(dev, link up\n);四、最简单分类记忆背这个1. 通用内核打印printkpr_infopr_errpr_warnpr_debug2. 带设备信息推荐dev_infodev_errdev_warndev_dbg3. 网络驱动专用netdev_errnetdev_info五、驱动里最常用的 3 个你必须记住这 3 个占了驱动打印的90%dev_info→ 正常信息dev_err→ 错误pr_err→ 没有 device 指针时用六、超级实用小技巧驱动里到处都是__func__ // 打印函数名 __LINE__ // 打印行号例子dev_err(dev, %s line %d: error\n, __func__, __LINE__);更多待补充platform平台获取资源的接口一、先记住一句话platform 设备的资源 设备树里的 reg /interrupt/clocks 等硬件信息platform 获取资源接口 驱动从设备树把这些信息读出来二、最核心、最常用的 5 个接口99% 驱动用这些1. platform_get_resource —— 获取任意资源最通用struct resource *res platform_get_resource(pdev, type, index);用途获取reg、内存、IO、总线地址等常用 typeIORESOURCE_MEM→ 设备寄存器地址regIORESOURCE_IRQ→ 中断号IORESOURCE_IO→ IO 端口例子// 获取第0段寄存器地址 res platform_get_resource(pdev, IORESOURCE_MEM, 0); // 获取第1个中断 res platform_get_resource(pdev, IORESOURCE_IRQ, 1);2. platform_get_irq —— 专门获取中断最常用int irq platform_get_irq(pdev, index);最简单、最推荐的获取中断方式3. resource_size —— 获取资源长度reg 大小resource_size_t size resource_size(res);你截图里就有这个4. platform_get_resource_byname —— 按名字获取资源struct resource *res platform_get_resource_byname(pdev, type, name);5. devm_platform_get_and_ioremap_resource —— 获取 映射 二合一现代驱动首选void __iomem *base devm_platform_get_and_ioremap_resource(pdev, index, res);一步完成获取资源检查资源ioremap 映射devm 自动管理释放三、完整列表所有 platform 资源接口1. 获取资源platform_get_resource(pdev, type, index); // 通用 platform_get_resource_byname(pdev, type, name); // 按名字 platform_get_irq(pdev, index); // 中断专用 platform_get_irq_byname(pdev, name); // 按名字获取中断2. 获取 映射现代驱动推荐devm_platform_get_and_ioremap_resource(pdev, index, res); devm_ioremap_resource(dev, res); // 自己获取res后映射3. 辅助函数resource_size(res); // 获取资源长度四、最常用组合你截图里的标准流程// 1. 获取内存资源reg res platform_get_resource(pdev, IORESOURCE_MEM, 0); // 2. 获取长度 size resource_size(res); // 3. 映射老驱动 request_mem_region(res-start, size, name); base ioremap(res-start, size); // 现代驱动直接一步到位 base devm_ioremap_resource(dev, res);五、最简单记忆口诀获取资源用 platform_get_resource 获取中断用 platform_get_irq 获取长度用 resource_size 获取映射用 devm_ioremap_resourceplatform_set_drvdata / platform_get_drvdata一句话讲透platform_set_drvdata / platform_get_drvdata作用把你自己定义的结构体“绑在” platform 设备上方便随时取用这是platform 驱动最核心、最常用、几乎每个驱动都有的一对接口。1. 它到底是干嘛的你写驱动一定会定义一个私有数据结构体比如struct my_drv_data { void __iomem *base; // 寄存器基地址 int irq; // 中断号 struct net_device *ndev; int reset_gpio; };这个结构体在 probe 里创建但open/read/write 等函数里也要用。问题别的函数拿不到这个结构体指针怎么办答案用 platform_set_drvdata 把它存进 pdev 里用 platform_get_drvdata 随时取出来2. 函数原型// 保存存进去 void platform_set_drvdata(struct platform_device *pdev, void *data); // 取出拿回来 void *platform_get_drvdata(struct platform_device *pdev);3. 最经典使用流程所有 platform 驱动都这样① probe 函数里创建 → 保存static int my_probe(struct platform_device *pdev) { // 1. 分配私有数据 struct my_drv_data *drvdata devm_kzalloc(pdev-dev, sizeof(*drvdata), GFP_KERNEL); // 2. 初始化各种资源 drvdata-base devm_ioremap_resource(...); drvdata-irq platform_get_irq(...); // 3. 把私有数据 存进 pdev platform_set_drvdata(pdev, drvdata); // --- 核心 return 0; }② 其他函数里取出使用static int my_remove(struct platform_device *pdev) { // 从 pdev 里取出之前存的结构体 struct my_drv_data *drvdata platform_get_drvdata(pdev); // 直接使用 dev_info(pdev-dev, irq %d\n, drvdata-irq); return 0; }4. 超级通俗比喻platform_set_drvdata 给设备贴一个 “便利贴”便利贴上写着你的所有私有数据地址、中断、GPIO 等任何函数拿到 pdev就能撕下便利贴拿到所有数据5. 为什么必须用它驱动的生命周期函数proberemoveinterruptsuspendresume它们都只给你 pdev不给你私有结构体所以你必须把结构体存在 pdev 身上。6. 最终极简总结platform_set_drvdata(pdev, ptr); // 把我的数据存进设备 platform_get_drvdata(pdev); // 把我的数据取出来这是 platform 驱动的 “数据传递神器”。常见的内存操作接口关于内存分配接口 场景一为CPU分配普通内存这是最常用的情况比如给网络驱动分配一个数据缓冲区来描述数据包。主力函数kmalloc与kzalloc这是分配小块内存的首选分配出的内存在物理地址和虚拟地址上都是连续的。对驱动来说这非常重要因为很多硬件设备都要求物理地址连续。kzalloc是kmalloc的安全版本它会把分配的内存全部清零可以避免读到内核遗留的“脏数据”是强烈推荐的做法。// 分配一个 256 字节的缓冲区并自动清零 void *buffer kzalloc(256, GFP_KERNEL); if (!buffer) { // 处理内存分配失败的情况 return -ENOMEM; } // ... 使用 buffer ... kfree(buffer); // 使用完毕后务必释放关键标志位GFP_KERNELvsGFP_ATOMIC这是新手最容易踩的坑它决定了内存分配的“行为模式”。GFP_KERNEL标准模式。在进程上下文比如驱动的write、read系统调用中使用。如果内存紧张它会主动让出CPU等待内存回收可能会睡眠。因此绝对不能在中断处理函数、自旋锁等原子上下文中使用。GFP_ATOMIC原子模式。用于上述的原子上下文中。分配过程不会睡眠但因此成功率更低应作为紧急情况下的后备选择。大内存分配vmalloc当你需要分配一块很大的、但物理上可以不连续的内存时使用例如加载一个很大的内核模块。它只保证虚拟地址连续。由于内部映射开销较大性能不如kmalloc通常不用于DMA传输。void *large_buf vmalloc(1024 * 1024); // 分配 1MB // ... vfree(large_buf);⚡️ 场景二为DMA设备分配内存当你的网卡如 dm9000需要通过DMA直接往内存里读写数据时就必须使用这类接口。主力函数dma_alloc_coherentDMA操作有一个核心痛点缓存一致性Cache Coherency。CPU的缓存和DMA直接操作的内存内容可能不一致。这个函数分配的内存会确保CPU和DMA设备看到的内容始终一致免去了你手动处理缓存的麻烦。cdma_addr_t dma_handle; // 分配一个DMA缓冲区cpu_addr是CPU使用的虚拟地址 // dma_handle是设备使用的物理总线地址 void *cpu_addr dma_alloc_coherent(dev, size, dma_handle, GFP_KERNEL); // ... 将 dma_handle 告诉硬件然后进行DMA传输 ... dma_free_coherent(dev, size, cpu_addr, dma_handle);️ 场景三访问硬件寄存器 (MMIO)驱动的本质是控制硬件而控制硬件就是读写它的寄存器。在Linux下由于内存管理单元MMU的存在必须先建立“映射”才能访问。主力函数ioremap硬件寄存器有它自己的物理地址比如0x10002000。ioremap的作用就是把这个物理地址映射到内核的虚拟地址空间之后你就可以像操作普通内存一样用指针读写它了。// 假设从数据手册查到某个设备寄存器的物理基址是 0x10002000长度是 4KB #define REG_BASE_PHYS 0x10002000 #define REG_SIZE 0x1000 void __iomem *reg_base; // 在驱动初始化时进行映射 reg_base ioremap(REG_BASE_PHYS, REG_SIZE); if (!reg_base) { return -EIO; } // 使用 readl/writel 等辅助函数来读写寄存器 u32 val readl(reg_base 0x10); // 读偏移 0x10 的寄存器 writel(val | 0x1, reg_base 0x00); // 写偏移 0x00 的寄存器 // 在驱动卸载时解除映射 iounmap(reg_base);注意操作MMIO区域时推荐使用内核提供的readb/writeb、readl/writel等函数而不是直接用*操作符以确保操作的顺序和宽度正确。 内存屏障有时候为了让内存的读写操作严格按照你代码的顺序执行需要使用内存屏障。这在驱动中通常有两个目的保证顺序防止CPU编译器和处理器对I/O操作进行重排序。DMA同步在启动DMA传输前使用dma_wmb()确保所有的描述符和数据都已经真正写入内存。类似地在读取DMA传输完的数据前使用dma_rmb()。// 确保前面的数据写入都完成再通知硬件开始DMA dma_wmb(); writel(CMD_START, reg_base CMD_REG); 小结与最佳实践你的需求是什么应该使用的API一句话提醒分配常规小内存kzalloc/kfree记得使用GFP_KERNEL还是GFP_ATOMIC分配大块虚拟内存vmalloc/vfree性能略低不用于DMA分配DMA一致性内存dma_alloc_coherent/dma_free_coherent解决了让人头疼的缓存一致性问题访问硬件寄存器ioremap/iounmap配合readl/writel使用保证内存操作顺序dma_wmb/dma_rmb启动DMA前和接收数据后特别重要对于新手来说可以记住这几个核心原则尽量使用kzalloc而不是kmalloc。时刻注意自己处于进程上下文还是中断上下文以此来选择GFP_KERNEL或GFP_ATOMIC。所有分配的内存都必须在退出时释放避免内存泄漏。更多补充。一、物理地址 ↔ 虚拟地址 映射操作寄存器专用这是操作硬件寄存器最核心的一套接口你截图里全是这个1.request_mem_region作用申请物理地址段占坑防止冲突老驱动常用2.release_mem_region作用释放物理地址段对应上面3.ioremap作用物理地址 → 内核虚拟地址驱动必须用它才能读写寄存器4.iounmap作用取消映射5. devm_ioremap_resource现代推荐作用三合一申请地址映射devm 自动释放新驱动全部用这个二、内核动态内存分配像用户态 malloc1.kmalloc最常用分配物理连续内存用于小内存一般 128KBbuf kmalloc(size, GFP_KERNEL);2.kfree释放kmalloc3.devm_kmalloc设备托管自动释放驱动最推荐4.vzalloc分配大内存物理地址可以不连续三、IO 内存读写操作寄存器用映射完地址后用这些函数读写寄存器1.readl/readw/readb读 32bit / 16bit / 8bit 寄存器2.writel/writew/writeb写 32bit / 16bit / 8bit 寄存器3.__raw_readl/__raw_writel无内存屏障版本更快但要注意顺序四、Resource 资源获取平台驱动专用你前面看到的platform_get_resource属于这一类1.platform_get_resource获取 reg /irq 资源2.resource_size获取资源长度3.platform_get_irq获取中断号五、内存拷贝内核态常用1.memcpy普通拷贝2.memset清零 / 设置值3.memmove安全拷贝4.dma_memcpyDMA 专用拷贝六、DMA 相关内存网络 / WiFi/SDIO 大量用1.dma_alloc_coherent分配 DMA 可用内存2.dma_free_coherent释放3.dma_map_singleDMA 映射七、最最重要的一张表背这个接口用途场景ioremap物理地址转虚拟操作寄存器devm_ioremap_resource申请 映射 自动释放现代驱动首选request_mem_region申请物理地址老驱动kmalloc/devm_kmalloc内核动态内存申请缓冲区readl/writel寄存器读写操作硬件platform_get_resource获取设备资源平台驱动memcpy/memset内存操作数据处理更多待补充。电源管理子系统Regulator 子系统常用接口你会在驱动里反复看到接口作用场景devm_regulator_get(dev, name)获取 regulator 句柄推荐驱动 probe 阶段regulator_get(dev, name)非托管版本需手动regulator_put老驱动regulator_enable(reg)上电硬件工作前regulator_disable(reg)下电硬件休眠 / 卸载regulator_get_voltage(reg)获取当前电压调试 / 电压检查regulator_set_voltage(reg, min_uV, max_uV)设置电压需调压的硬件IS_ERR/PTR_ERR错误处理所有内核接口通用关键知识点补充1. 为什么用devm_regulator_get自动托管驱动卸载时自动regulator_put彻底避免资源泄漏现代驱动必须用devm_版本老驱动的regulator_get已不推荐2.-EPROBE_DEFER为什么重要嵌入式系统中电源 regulator 可能由其他驱动如 PMIC 驱动提供如果 PMIC 驱动还没加载完网卡驱动先 probe 就会拿不到电源直接失败返回-EPROBE_DEFER让内核稍后重试直到 PMIC 就绪保证驱动正常加载3. 设备树对应写法这段代码对应的设备树节点通常是dm900010000000 { compatible davicom,dm9000; reg 0x10000000 0x100; vcc-supply vcc_3v3; // 对应 vcc };vcc_3v3是板级定义的 regulator比如 PMIC 的 3.3V 电源轨。input子系统一、Input 子系统是干嘛的专门管理按键、触摸屏、鼠标、键盘、遥控器、游戏手柄……它把硬件事件按下、松开、坐标上报给 Linux 内核 → 再给上层应用Android、Linux 桌面上层只需要读/dev/input/eventX就能拿到按键事件。二、标准驱动流程5 步走所有 Input 驱动都一样1. 分配 input_dev 结构体 2. 设置支持哪些事件按键、坐标、力反馈等 3. 注册 Input 设备 4. 硬件触发时上报事件按下/松开 5. 卸载时注销设备三、核心接口大汇总全部在这里1. 分配 / 释放 input_dev// 分配 struct input_dev *input_allocate_device(void); // 释放自动 devm 版本推荐 void input_free_device(struct input_dev *dev);2. 设置事件类型最关键你必须告诉内核你支持哪些事件// 支持按键事件 __set_bit(EV_KEY, input-evbit); // 支持某个按键如 KEY_POWER、KEY_VOLUMEDOWN __set_bit(KEY_POWER, input-keybit);常用事件类型EV_KEY按键EV_ABS绝对值触摸屏 X/YEV_REL相对值鼠标EV_SYN同步内核自动用3. 注册 Input 设备int input_register_device(struct input_dev *dev);4. 卸载注销void input_unregister_device(struct input_dev *dev);5.上报事件最重要硬件触发中断 /poll时调用// 上报按键 input_report_key(struct input_dev *dev, unsigned int code, int value); // 上报绝对坐标触摸屏 input_report_abs(struct input_dev *dev, unsigned int axis, int value); // 上报同步必须表示一帧事件结束 input_sync(struct input_dev *dev);上报必须 sync四、最常用全套 API你 99% 会遇到1. 设备创建input_allocate_device input_register_device input_unregister_device input_free_device2. 事件设置__set_bit(EV_KEY, dev-evbit); __set_bit(KEY_0, dev-keybit); __set_bit(KEY_1, dev-keybit);3. 事件上报input_report_key(dev, code, val); // 1按下 0松开 input_report_abs(dev, ABS_X, x); // 触摸屏X input_report_abs(dev, ABS_Y, y); // 触摸屏Y input_sync(dev); // 提交事件4. 轮询 / 中断专用input_event(dev, type, code, value); //通用上报五、标准 Input 驱动完整模板可直接编译这就是按键驱动最标准写法所有开发板都这么写#include linux/input.h struct input_dev *input; // 1. probe 里初始化 int probe(struct platform_device *pdev) { // 分配 input input_allocate_device(); input-name test_key; input-phys key/input0; // 设置支持的事件按键 __set_bit(EV_KEY, input-evbit); __set_bit(KEY_POWER, input-keybit); // 支持电源键 // 注册 input_register_device(input); platform_set_drvdata(pdev, input); return 0; } // 2. 按键中断里上报 static irqreturn_t key_irq(int irq, void *data) { int val gpio_get_value(gpio); // 上报按键 input_report_key(input, KEY_POWER, !val); input_sync(input); // 必须同步 return IRQ_HANDLED; }六、你必须记住的 3 条铁律上报事件后必须调用 input_sync ()否则上层收不到必须用__set_bit()告诉内核你支持哪些事件Input 驱动只做一件事硬件事件 → 上报给内核input_set_drvdata /input_get_drvdata这对函数专门给 Input 子系统用作用和我们之前学的platform_set_drvdata几乎一模一样我用最简单、最直白的方式给你讲透。一、一句话结论input_set_drvdata 把你的私有数据绑在 input_dev 上input_get_drvdata 从 input_dev 上把私有数据取回来它是Input 子系统专属的数据传递工具。二、函数原型void input_set_drvdata(struct input_dev *dev, void *data); void *input_get_drvdata(struct input_dev *dev);非常简单第一个参数input 设备第二个参数你自己的私有数据指针