新闻详情

新闻详情

首页 / 资讯中心 / 详情

Linux驱动开发实战:从内核模块到设备树与I2C/CAN

发布时间:2026/9/16 3:54:55来源:尧图网络
Linux驱动开发实战:从内核模块到设备树与I2C/CAN
接手一块新板子的时候我习惯先问一句这上面的外设靠什么在Linux里跑起来碰到国产ARM平台、RK3568、全志这类的板子答案几乎是固定的——设备树描述硬件内核驱动匹配设备I2C/CAN这类总线负责搬数据。很多新人一上来就捧着驱动代码啃结果卡在设备树语法、总线框架不熟上越看越乱。这篇文章我就完全按自己实际走过的路径来写先搞懂内核模块怎么存在再看设备树怎么把硬件“说清楚”最后落到I2C和CAN这两条总线的驱动开发与调试全程用实战场景串起来。1. 内核模块是入口一个驱动到底怎么被系统加载起来的1.1 从“Hello World”模块开始先把生命周期跑通很多人以为驱动开发很高深其实第一步是写一个能在内核里跑起来的小模块。所谓内核模块就是一段可以动态加载进内核的代码不需要重新编译整个内核镜像非常适合开发调试。我建议所有新人都先把这个最小模块跑通再谈别的。#include linux/module.h #include linux/init.h #include linux/kernel.h static int __init demo_init(void) { pr_info(demo module loaded\n); return 0; } static void __exit demo_exit(void) { pr_info(demo module unloaded\n); } module_init(demo_init); module_exit(demo_exit); MODULE_LICENSE(GPL); MODULE_DESCRIPTION(Minimal driver module example);配套的Makefile是这样的obj-m : demo.o KERNELDIR : /path/to/kernel-source PWD : $(shell pwd) all: $(MAKE) -C $(KERNELDIR) M$(PWD) modules clean: $(MAKE) -C $(KERNELDIR) M$(PWD) clean编译出来会得到demo.ko。在目标机器上执行insmod demo.ko模块加载时demo_init执行执行rmmod demodemo_exit执行。查看输出用dmesg不要指望它出现在终端里内核日志走的是内核环形缓冲区不是标准输出。这里有几个细节新手非常容易忽略__init和__exit宏的作用是把初始化函数和退出函数放到专门的代码段里模块加载成功后初始化函数占用的内存会被释放掉。这不是可有可无的修饰而是内核模块的常规做法。MODULE_LICENSE(GPL)不只是摆设。很多内核符号对非GPL模块不导出尤其是调试、锁和部分子系统接口。不声明GPL你在一些API上会踩到“未知符号”的坑。insmod只做加载不处理依赖。如果模块依赖其他模块要用modprobe它按依赖关系自动加载这也是后面做驱动时更常用的命令。1.2 模块机制只是“外壳”设备驱动还差关键一步把模块跑通之后你会发现这和“驱动”还差得远。真正的驱动要实现具体的接口让用户态程序能通过文件系统、网络栈或其他子系统访问你的硬件。比如一个字符设备驱动必须实现file_operations结构体再注册到内核的字符设备框架里这样open/read/write/ioctl才会到达你的驱动函数。我个人理解的内核模块到设备驱动的路程是这样的模块机制解决的是“代码怎么塞进内核”的问题。设备模型解决的是“硬件设备怎么被描述、被找到”的问题。总线驱动解决的是“数据怎么在处理器和外设之间流动”的问题。从实践来看不理解设备模型就写驱动很容易写出“一个模块把硬件寄存器全占了”的独享式代码。这种模块在开发板上能跑一进正式项目就崩因为内核的设备模型、电源管理、热插拔机制全都接不上。所以我的建议是模块语法花两三天过一遍就行重点放在理解设备模型上——也就是设备树、总线、驱动三者怎么铆合到一起。1.3 交叉编译与内核源码版本最容易卡住的隐藏门槛做嵌入式Linux驱动几乎不可能在目标板上直接编译都是在宿主机上用交叉编译工具链编好.ko再拷到板子上加载。这里最坑的就是内核源码版本必须和目标板运行的内核版本严格一致同时编译配置也不能改花。具体来说注意三点KERNELDIR指向的内核源码树最好就是板子当前内核同一个版本、同一个配置文件编出来的。否则模块加载时可能出现“版本魔术”不匹配。交叉编译需要指定架构和工具链例如ARCHarm64 CROSS_COMPILEaarch64-linux-gnu-。内核源码目录下得先make modules_prepare之类准备好部署文件否则模块编译会报缺少Module.symvers或include文件的错。这块的坑几乎所有人都会踩解决办法也简单别自己猜测直接从板卡厂商的BSP里拿内核源码和设备树比什么都省事。2. 设备树不是配置文件是驱动与硬件之间的“接线契约”2.1 内核为什么非要设备树不可早年ARM Linux上每换一款板子都要在内核里写一大堆board-xxx.c文件把外设地址、中断号、GPIO复用一个一个硬编码到C结构体里。树莓派、手机和平板这种海量变体一出来这种模式直接失控代码满是平台相关的东西没法维护。设备树的作用就是把这些硬件信息从代码里剥出来变成一份内核启动时读入的结构化数据。你可以把设备树理解成一张“接线表”哪个设备的寄存器在哪、占几号中断、连在哪条I2C总线上、要用哪个引脚都由设备树描述。内核拿到这张表之后把对应设备和驱动匹配上然后调用驱动的probe函数。编写驱动的人不需要知道所有板卡细节只需要处理协议和寄存器逻辑。2.2 dts/dtsi/dtb/dtc的关系先理顺.dts是设备树源文件扩展名是文件后缀实际就是文本。.dtsi是公共片段类似C语言的.h常放SoC公共部分板级的.dts再通过#include引入。.dtb是编译出来的二进制文件。dtc是设备树编译器。反编译工具也很有用dtc -I dtb -O dts -o output.dts input.dtb拿到别人编好的固件反编译看它怎么配置的这在调板子时特别常见。一个基本节点长这样i2c3 { status okay; clock-frequency 400000; touchscreen5d { compatible goodix,gt911; reg 0x5d; interrupt-parent gpio1; interrupts 13 IRQ_TYPE_LEVEL_LOW; reset-gpios gpio3 9 GPIO_ACTIVE_LOW; touchscreen-inverted-x 0; touchscreen-inverted-y 1; }; };这里i2c3是引用SoC基础dtsi里已经定义好的I2C控制器的节点把它“打开”并添加子节点。这里的关键词status决定这个控制器是否启用compatible就是驱动匹配的字符串钥匙reg是该设备在I2C总线上的从机地址interrupts和reset-gpios告诉驱动中断引脚和复位引脚。2.3 compatible匹配规则驱动和设备靠什么“对暗号”设备树里写compatible goodix,gt911驱动的结构体里也要有对应的匹配条目static const struct of_device_id gt911_of_match[] { { .compatible goodix,gt911 }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, gt911_of_match); static struct i2c_driver gt911_driver { .probe gt911_probe, .remove gt911_remove, .driver { .name gt911, .of_match_table of_match_ptr(gt911_of_match), }, };匹配顺序是设备树遍历时拿节点的compatible值和驱动注册到总线上的of_match_table比对匹配成功就调用驱动的probe并把struct device、中断、寄存器地址、引脚资源等通过struct platform_device或struct i2c_client传进驱动。也就是说probe不是你自己调用的是内核框架匹配后自动触发的。很多新手在驱动里写一堆初始化代码却发现根本没执行先查这个匹配关系八成是compatible写错或者驱动根本没注册上。2.4 实战RK3568上给I2C触摸屏配置并修改横竖屏RK3568是不少国产板卡常用的主控它的BSP里设备树路径基本都在arch/arm64/boot/dts/rockchip/下。拿到一块带I2C触摸屏的板子第一步是看核心板的原理图确定触摸屏挂在哪组I2C控制器上、从机地址是多少、复位和中断接了哪个GPIO然后在对应的板级dtsi里打开控制器并添加子节点像上面那段代码那样。如果是竖屏硬件想改成横屏除了显示方向要改触摸坐标轴也要对应翻转。不少电容触摸屏驱动支持设备树属性比如touchscreen-inverted-y、touchscreen-swapped-x-y直接改dts比改驱动代码好维护得多。比如把竖屏变横屏i2c3 { touchscreen5d { compatible goodix,gt911; reg 0x5d; interrupt-parent gpio1; interrupts 13 IRQ_TYPE_LEVEL_LOW; reset-gpios gpio3 9 GPIO_ACTIVE_LOW; touchscreen-inverted-x 1; touchscreen-inverted-y 0; touchscreen-swapped-x-y 1; }; };这几个属性不同驱动实现不同有的用touchscreen-swapped-x-y有的用touchscreen-x-flip之类的名字具体要看驱动源码对设备树属性的解析。我的习惯是改完先不重新编译整个内核只编译设备树并单独替换启动分区里的.dtb板子起不来还能快速回退。这个工作流比每次重刷kernel快太多了。3. I2C子系统把时序细节交给内核驱动只关心设备逻辑3.1 I2C协议的关键点用一句话先过一遍I2C就是两根线SDA数据和SCL时钟。主机发起传输先拉低SCL时机发送起始位再发送7位地址加一位方向位。地址匹配的从机会拉低SDA应答ACK。后面读写字节按位传输每8位数据跟着一个应答位。读完或写完发停止位收尾。关于多字节连续读写本质就是主设备先把要操作的寄存器地址写进去再切换方向连续读多个字节或者连续写多个字节从设备内部寄存器地址自动增加。不理解协议细节能不能写Linux驱动能。因为Linux I2C核心层已经封装好了但你在排查故障的时候必须回来看时序图不然分不清“设备没响应”和“时序不对”到底哪个原因。3.2 Linux的I2C框架adapter、client、driver三个角色Linux对I2C的抽象分两层adapterI2C控制器本身也就是硬件上一组SCL/SDA引脚的对应驱动逻辑负责发送起始位、地址、读写字节、ACK/NACK这些底层动作。client挂在I2C总线上的从设备和一条总线上的一对地址绑定。driver从设备对应的驱动实现具体的寄存器操作逻辑。一个从设备要工作必须先有一个I2C控制器驱动注册了adapter然后设备树里的子节点被解析出来注册成client再通过compatible匹配到i2c_driver触发probe。简单记adapter是高速公路client是入口匝道driver是出口。3.3 手写一个I2C从设备驱动的骨架下面是一个常见的I2C设备驱动的核心框架以读取传感器ID为例static int mydev_probe(struct i2c_client *client) { struct i2c_adapter *adapter client-adapter; u8 reg 0x00; s32 val; if (!i2c_check_functionality(adapter, I2C_FUNC_I2C)) { dev_err(client-dev, adapter does not support I2C\n); return -EOPNOTSUPP; } /* 通过smbus函数读一个寄存器适用于EEPROM、传感器等简单从设备 */ val i2c_smbus_read_byte_data(client, reg); if (val 0) { dev_err(client-dev, read failed: %d\n, val); return val; } dev_info(client-dev, chip id: 0x%02x\n, val); return 0; } static int mydev_remove(struct i2c_client *client) { return 0; } static const struct of_device_id mydev_of_match[] { { .compatible vendor,mydev }, { } }; MODULE_DEVICE_TABLE(of, mydev_of_match); static struct i2c_driver mydev_driver { .probe mydev_probe, .remove mydev_remove, .driver { .name mydev, .of_match_table mydev_of_match, }, }; module_i2c_driver(mydev_driver); MODULE_LICENSE(GPL);注意probe拿到的是struct i2c_client *里面已经包含设备树里的reg地址、中断号、GPIO等信息直接读取就行。需要读一字节以上的寄存器数据时可以用i2c_transfer填充struct i2c_msg数组先写寄存器地址再读数据这个更接近原生协议。很多EEPROM驱动就是用i2c_transfer实现多字节读写的。3.4 排错实战I2C通信失败到底从哪查起I2C调试有一个固定的排查链条按顺序来能省一多半时间先看设备树节点有没有被正确解析启动日志里有i2c相关的报错没有或者去/sys/bus/i2c/devices/下看0-005d这类目录是否存在0-005d对应I2C控制器编号和从机地址。用i2cdetect -y bus号扫描总线上有哪些地址响应。注意有些传感器在初始化前不响应地址这种扫描不出来但总线本身是通的。用逻辑分析仪抓SCL/SDA时序。看起始位、地址、ACK位。地址不对、上拉电阻没装、设备供电没起来都能在时序上看出来。检查引脚复用。I2C引脚经常和别的外设共用Pinctrl设备树里GPIO被其他节点占用或者pinctrl-0没设置正确信号根本到不了控制器。我遇到最多的问题就是设备树里忘了status okay或者clock-frequency配了从机不支持的速率导致偶发通信失败。还有一个隐藏很深的坑I2C总线挂了很多设备时某个设备的SDA线故障可能把整条总线拖死表现为所有设备都响应不了地址。这时候挂个逻辑分析仪最直观比反复改驱动高效多了。4. CAN总线在Linux下的形态从SocketCAN理解驱动对接和排错4.1 CAN和I2C/SPI完全不是一路的协议I2C是典型的“一主多从”CPU永远是主机。CAN不一样总线上所有节点地位平等任意节点都能主动发帧。消息靠帧ID来决定优先级多个节点同时发送时ID小的获胜低优先级的节点自动退避重发。这个“多主机仲裁”的特性决定了它在汽车、工控、机器人里不可替代——任何一个控制器都能随时上报故障而不是等中央控制器来轮询。CAN帧分标准帧CAN 2.0A11位ID和扩展帧CAN 2.0B29位ID数据场最多8字节。CAN FD在后面又扩到最多64字节数据场但对Linux驱动的框架来说差异都被封装了起来不需要改驱动逻辑。4.2 SocketCAN把CAN接口当成普通的网络接口来用Linux对CAN的抽象非常妙——直接用PF_CAN套接字把每个CAN控制器变成一个网络接口叫can0、can1。这么设计的好处是你可以用网络栈那一整套工具和思路来操作CAN配置IP不适用但可以用ip link配置波特率用抓包工具分析报文。典型用法ip link set can0 type can bitrate 500000 ip link set can0 up candump can0 cansend can0 123#DEADBEEF我见过很多老式项目把CAN驱动做成字符设备自己定ioctl收发。一旦车厂需要记录总线日志、模拟多个节点这种方案简直要命。而SocketCAN天然支持candump -l记录pcap文件、canplayer回放历史报文、cansend模拟单帧调试效率不是一个量级。4.3 Linux CAN驱动要实现的几个关键部分在Linux内核里CAN设备驱动主要是实现用alloc_candev分配并初始化struct can_priv。填充struct can_ops包括do_set_mode、do_get_berr_counter等。发送和接收路径都是基于网络的net_device_ops。发送通过ndo_start_xmit接收通过netif_rx将CAN帧上送到网络协议栈。处理总线错误状态、重启等通过内核提供的can_bus_off、can_restart等辅助函数。如果用的是典型SPI接口外挂CAN控制器比如MCP2515设备树里会这样配spi0 { status okay; mcp2515: mcp25150 { compatible microchip,mcp2515; reg 0; clocks mcp2515_clk; interrupt-parent gpio0; interrupts 1 IRQ_TYPE_EDGE_FALLING; spi-max-frequency 10000000; }; };驱动方面MCP2515在主线内核有mcp251x驱动。它本质上是“SPI从设备 中断通知”收到CAN中断后再通过SPI把整个报文读出来。内核框架需要处理的事情很多比如中断下半部、发送队列、错误状态上报但这些都被SocketCAN封装成标准接口了。4.4 波特率、采样点和总线错误处理CAN调试里最容易被忽略的是波特率和采样点。两个参数不光要“一样”还要看具体位时序。ip link set can0 type can bitrate 500000这种方式用的是内核默认的采样点有些场合需要精确控制ip link set can0 type can bitrate 500000 sample-point 0.8采样点决定什么时候去读电平工控链路易受干扰时适当调整能让误码率明显下降。另外CAN节点出现持续错误后会进入bus-off状态驱动要能检测到并恢复。用ip -details link show can0能看到错误状态和统计信息这是排查物理层问题的第一步。5. 把整套路径串起来一块板子的驱动开发与调试流程5.1 先画一张“设备地图”再去看代码拿到一块新板子不要急着翻内核源码先画一张表把外设信息列全。这张表就是驱动开发的起点也是设备树配置的“需求文档”。外设总线从机地址/ID中断引脚复位引脚供电典型速率GT911触摸屏I2C30x5dGPIO1_13GPIO3_093.3V400kHzMCP2515SPI0CS0GPIO0_01无3.3V10MHzOLED屏I2C10x3c无无3.3V100kHzCAN收发外接无无无5V500kbps有了这张表写设备树就只是“翻译”工作不用看一段代码查一下原理图。我在正规项目里都会要求硬件工程师先提供GPIO分配表和外设连接表这些信息不齐驱动开发就是大海捞针。5.2 配置设备树后怎么确定驱动真的“上电开工”了设备树编译打包后启动进入系统在/sys/firmware/devicetree/base/下能看到对应节点在/sys/bus/i2c/devices/或/sys/bus/spi/devices/下能看到设备目录。如果设备目录存在但驱动没有成功probe一般是compatible没匹配上或者驱动内probe执行失败比如中断申请失败、GPIO请求冲突。用dmesg | grep -i i2c或dmesg | grep -i probe能快速看到内核在枚举设备时打印的信息。有的驱动设计不好probe失败不打印任何日志这时候我会在probe入口加dev_info从第一行开始逐段确认卡在哪或者用dev_err把i2c_check_functionality、devm_gpiod_get、request_threaded_irq每一步的返回值都打出来。5.3 重点排查复位信号、引脚复用、中断配置热搜里经常有“设备树设置复位信号时间”的问题这其实是一个典型细节。很多触摸屏、NFC芯片、音频Codec在上电后需要一定的复位时间设备树里一般通过reset-gpios加reset-deassert-us这类属性控制复位释放和延时。reset-gpios gpio3 9 GPIO_ACTIVE_LOW; reset-deassert-us 10000;如果你发现外设时不时“初次访问失败”但重启后又好了很大概率就是这个复位时序不对。另一个高发问题是中断号配置错误。设备树里的interrupts不只是写个GPIO号那么简单还要指定触发类型比如IRQ_TYPE_LEVEL_LOW、IRQ_TYPE_EDGE_FALLING。如果驱动和物理设备触发方式不匹配中断会丢失或者狂触发。我的经验是先用cat /proc/interrupts看中断是否注册再用示波器或逻辑分析仪抓一下引脚电平变化对比触发类型是不是一致。5.4 内核裁剪与部署时的注意事项驱动开发稳定之后第二个任务是系统裁剪优化。嵌入式设备的flash容量有限总不能把整个内核源码和所有驱动都塞进去。用make menuconfig把不用的驱动关掉把不再需要的内核调试选项停掉可以显著缩小内核体积、加快启动速度。裁剪的原则是“最小可用集”先用全功能配置跑通业务再备份一个可启动的内核和设备树然后一版一版往下裁剪。我见过有人一上来就拼命关驱动结果关掉了文件系统或根总线相关的模块板子直接起不来又不知道是哪项配置出问题白白浪费几天。正确的做法是每次只做一组裁剪保留一个能启动的备份把“内核镜像设备树Buildroot/rootfs”作为一个版本整体记录这样回溯问题时效率高得多。最后聊几句实在的驱动开发这条路真正重要的不是背出某个API而是把“硬件 — 设备树 — 内核驱动 — 用户态工具”这条链路上的数据流和错误流理清楚。我最开始写I2C驱动时遇到通信失败就反复改代码后来发现十有八九是设备树没配对或者硬件上拉了错引脚。从那以后我调试驱动一律先查设备树、再抓时序、最后才动驱动代码。如果你正准备入这行建议一定准备一块带逻辑分析仪的开发环境成本不高却能让你少走太多弯路。等这条链路走熟了哪怕换一款主控、换一套外设你也能用同一套方法论快速定位问题这才是“从内核模块到设备树、I2C/CAN系统路径”给你的真正底气。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

AFBR-S50与R7KA8D2KFLCAC:工业级ToF测距硬件协同新范式 2026/9/16 5:28:00

AFBR-S50与R7KA8D2KFLCAC:工业级ToF测距硬件协同新范式

1. 这不是“又一个测距模块”:AFBR-S50 R7KA8D2KFLCAC 组合的真实定位与价值锚点你可能刚在BOM表里看到 AFBR-S50 和 R7KA8D2KFLCAC 这两个型号,第一反应是:“哦,ToF传感器MCU”,然后随手划走。但如果你真这么想&…

阅读更多 →
WebSocket 快速入门:从轮询到长连接的全链路实战 2026/9/16 5:28:00

WebSocket 快速入门:从轮询到长连接的全链路实战

第一次把 WebSocket 跑通的那天,我在浏览器控制台盯着一行connected看了很久。在此之前,我做消息推送用的是轮询:前端setInterval每 3 秒发一次请求,后端告诉你有没有新消息。这套东西能用,但它的本质是寄信——你想知…

阅读更多 →
NVIDIA控制面板消失闪退?从驱动组件到DDU的排查修复指南 2026/9/16 5:28:00

NVIDIA控制面板消失闪退?从驱动组件到DDU的排查修复指南

简介:NVIDIA 控制面板是 NVIDIA 显卡硬件与驱动配套的官方管理工具,主要面向使用 NVIDIA 显卡、需要调整显示设置或更新驱动的普通用户与游戏玩家。这份资源将通用驱动安装包与相关辅助文件打包在一起,解决用户找不到或打不开控制面板的常见问…

阅读更多 →
FPGA QSPI开发必修课:从原理图解读到工程搭建全流程详解 2026/9/16 5:28:00

FPGA QSPI开发必修课:从原理图解读到工程搭建全流程详解

先别急着写代码,原理图都看不明白,工程搭得再好也是白搭。做FPGA开发这些年,我最深的体会就是:QSPI这个接口,说大不大,说小不小,可它牵扯到的东西一点都不少——从原理图上Flash芯片的引脚连接&…

阅读更多 →
LTC4332+R7KA8D2KFLCAC实现百米级SPI远距离通信方案 2026/9/16 5:28:00

LTC4332+R7KA8D2KFLCAC实现百米级SPI远距离通信方案

1. 项目概述:为什么“长距离SPI”是个让人头疼的老大难问题?LTC4332和R7KA8D2KFLCAC这两个型号,乍看像一串随机字符,但只要你做过工业现场数据采集、远程传感器组网,或者调试过几十米外的ADC模块,就会立刻意…

阅读更多 →
LLM应用落地实战:RAG与Agent生产级开发指南 2026/9/16 5:25:00

LLM应用落地实战:RAG与Agent生产级开发指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞