Linux设备驱动开发实战:设备树、I2C与内核裁剪全解析
一块新板子送到手上厂家BSP能正常开机但把自己外接的I2C传感器接上去dmesg里什么动静都没有。这种场景我遇到太多次了根子常常不在驱动代码的读写逻辑上而在设备树节点、驱动匹配和内核配置这三件事的配合上。今天这篇内容我就围绕Linux设备驱动开发里真正绕不开的那些环节——交叉编译环境、设备树配置、platform驱动框架、I2C驱动实例、内核裁剪和调试性能调优把实际操作中积累的做法和踩过的坑一次性摊开讲。这篇文章适合正在做嵌入式Linux驱动开发的工程师也适合刚转行做Linux驱动、面对内核源码树一脸茫然的学习者。我不会只讲理论每一部分都会给出可以照着抄的步骤和命令同时解释清楚背后的设计逻辑。毕竟驱动开发的大部分问题都出在“不知道这个机制为什么这样工作”上。1. 写驱动前的三道关交叉编译链、内核源码树与config裁剪很多人写驱动上来就打开编辑器敲file_operations结果模块编译不过去或者编译出来了加载不进去。我早期也这么干过后来发现这些问题基本都出在准备工作上。准备工作不到位后面写多少代码都是在给报错调试添素材。1.1 内核源码树为什么是驱动编译的刚需Linux内核模块不是一个完全独立的程序它要引用内核内部的函数和数据结构比如printk、kmalloc、struct file_operations。这些符号的定义都在内核源码树里。模块编译时需要内核源码提供的头文件、生成的头文件比如autoconf.h、utsrelease.h以及编译规则。更关键的一点是模块和内核版本必须严格匹配。内核模块加载时内核会检查模块的vermagic字符串里面记录了内核版本号、是否SMP、是否PREEMPT等信息。如果和你当前运行的内核不一致insmod会直接报invalid module format。这个检查机制防止了模块和内核数据结构不匹配导致的内存破坏。所以第一步就是拿到和开发板运行版本一致的完整内核源码。你可以在开发板上执行uname -r拿到版本号之后去内核官网或者厂商BSP里找对应版本的源码。有些厂商会修改内核并打上自己的补丁这时候用官方原版内核源码编译出来的模块在厂商内核上一样可能加载失败。最稳妥的办法是直接用厂商提供的内核源码包。1.2 交叉编译工具链的选型与版本匹配开发板上的CPU架构和你的PC不同所以需要用交叉编译工具链。ARM 32位平台一般用arm-linux-gnueabihf-ARM 64位平台用aarch64-linux-gnu-。这里有一个容易忽略的坑工具链版本和内核源码版本不能差太多。太新的工具链编译老内核有时会因为编译器行为变化引入一些奇怪的编译错误太老的工具链可能不支持新内核使用的某些语法特性。厂商SDK自带的工具链往往是最保险的因为它就是针对这套BSP验证过的。配置交叉编译环境的常用做法是把工具链的bin目录加到PATH里然后在内核编译时通过参数指定export PATH$PATH:/opt/gcc-arm-9.2-aarch64/bin export ARCHarm64 export CROSS_COMPILEaarch64-linux-gnu-这里我特别提醒一点ARCH和CROSS_COMPILE这两个环境变量在编译内核和编译模块时都要保持一致。有些人编译内核时设置了编译模块时在另一个终端里忘了设置结果模块用宿主机的gcc编译一加载就报格式错误。1.3 模块编译的基本操作与y/m/n的选择拿到源码树后进到内核根目录先执行配置make defconfig # 或者使用厂商提供的配置文件 cp vendor_defconfig .config make menuconfigmenuconfig之后必须执行一次make modules_prepare这个步骤会生成模块编译需要的Module.symvers等文件。如果不执行直接编译模块会报找不到Module.symvers或者各种头文件缺失。编译单个模块的标准命令是这样的make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- M/path/to/your/driver modules这里的M指向你驱动源码所在的目录。目录下需要有一个Makefile里面至少写清楚obj-m : mydriver.o如果你驱动由多个源文件组成写法是obj-m : mydriver.o mydriver-objs : core.o interface.o在menuconfig里驱动有三种编译方式y编译进内核、m编译成独立模块、n不编译。我的习惯是开发调试阶段用m这样可以单独编译、单独加载不用每次改一行代码就重新烧整个内核镜像。等驱动稳定了再考虑是不是要改为y直接编进内核这样启动时就不依赖模块加载顺序和根文件系统里的模块文件了。2. 设备树配置先让内核认识你的外设设备树可能是我见过让新手最头疼的东西。它本质不复杂但牵扯到的知识点特别碎。用一句话概括设备树就是描述板级硬件拓扑的“履历表”。哪个外设挂在哪个总线上、寄存器基地址是多少、中断号是多少、速率多快全部用一颗颗节点描述清楚。内核启动时解析设备树建立起设备和驱动的对应关系。2.1 设备树解决了什么问题在早期的ARM Linux里板级硬件信息都写在arch/arm/mach-xxx目录下的C文件里。换一个板子要改文件、重新编译内核非常麻烦。而且大量硬件配置和驱动代码混在一起维护成本很高。设备树把硬件拓扑从内核代码里抽离出来变成一份独立的数据文件。驱动侧只需要声明自己支持哪些设备剩下的事情交给内核去匹配。你可以把设备树理解成一份“招聘需求表”驱动是“求职者”compatible字段就是求职者简历上的技能标签。招聘需求表和简历对上了双方才能见面触发probe函数。2.2 从原理图到dts节点的完整过程拿到一块板子原理图上画着什么外设、接到哪个I2C控制器、从机地址多少这些信息最终都要翻译成设备树节点。比如一个TI的温度传感器TMP117挂在I2C2控制器上从机地址是0x48设备树里就应该这样写i2c2 { status okay; clock-frequency 100000; tmp11748 { compatible ti,tmp117; reg 0x48; }; };这段话的意思是I2C2控制器使能总线频率设为100kHz总线上有一个地址为0x48的从设备它兼容TI的tmp117驱动。这里有几个新手高频翻车点节点名里的地址和reg必须对应。在设备树里device48表示这个节点在总线上的地址是0x48reg 0x48也必须是同样的值。两者如果不一致有些人会出现设备节点解析正常、但驱动probe拿到的reg地址不对的问题。status okay很关键。有些SoC的默认dtsi里外设控制器默认是disabled的你必须在板级dts里显式打开。少了这一行控制器根本不工作下面挂的设备节点也不会被扫描。中断号的填写。中断号不是随便填的它依赖SoC的中断控制器。比如ARM GIC中断号和Linux的软中断号之间往往存在一个偏移通常是32。到底是填硬件中断号还是软件中断号要看SoC的dtsi里interrupt-cells的约定和现有节点的写法。我一般会先翻看同平台已经验证过的节点照着写踩坑概率最小。2.3 compatible匹配机制驱动被probe的前提设备树写好了驱动侧必须有一张“能力清单”来和它配对。这个清单在驱动代码里表现为of_match_tablestatic const struct of_device_id tmp117_of_match[] { { .compatible ti,tmp117, }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, tmp117_of_match);内核在注册驱动时会遍历总线上已有的设备把设备节点的compatible属性和驱动的of_match_table一一比对。字符串完全一致才调用驱动的probe函数。很多人的驱动加载了、/sys/bus/i2c/devices/下也有设备节点但probe就是不执行最典型的原因就是compatible字符串没对齐。设备树里多写了一个空格、驱动里大小写不一致都会导致匹配失败。排查时可以查看设备树解析结果cat /sys/firmware/devicetree/base/i2c.../tmp11748/compatible看到实际字符串后再去驱动源码里比对问题通常一眼就能看出来。2.4 设备树修改后如何生效修改dts后需要重新编译dtb文件并烧写。最简单的方式是在内核源码目录下make dtbs生成的dtb在arch/arm64/boot/dts/对应厂商目录下。烧写dtb有两种常见方式一是单独烧到dtb分区二是打包进boot镜像。具体取决于平台的启动流程。调试阶段我个人更喜欢用U-Boot的tftp下载dtb改一次下载一次比反复烧sdcard快得多。设备树是否正确生效除了看/sys/firmware/devicetree/base/还可以在U-Boot环境变量里确认fdtfile是否指向正确的dtb文件名。如果U-Boot加载的dtb和内核打包的dtb不是同一份你在dts里改了半天也不会生效这种情况我踩过不止一次。3. platform驱动与字符设备驱动代码的主骨架设备树让内核认识外设了接下来真正干活的就是驱动代码本身。Linux驱动框架很多但嵌入式开发里占大头的是platform驱动模型加上字符设备接口。理解透这一块其他总线类型I2C、SPI、USB都是在这个基础上的变体。3.1 设备和驱动分离的设计逻辑platform驱动模型的核心思想是设备与驱动分离。设备信息用什么地址、占用哪个中断由设备树或ACPI描述设备驱动只关注“怎么操作硬件”。总线负责把两者匹配起来。你可以这样理解设备树是“插头”驱动是“插头对应的电器功能”platform总线就是“插座”。插头插到插座上功能才通电工作。这样做的好处是同一个驱动可以支持多个硬件实例同一个硬件在不同板子上也可以通过改设备树灵活调整资源不用改驱动代码。3.2 一个最小字符设备驱动的骨架字符设备是Linux里最基础、也最常用的设备类型。它给应用层提供一组类似文件的操作接口open、read、write、ioctl、close。下面是最小骨架里init函数的核心动作static int __init mydrv_init(void) { dev_t dev_num; alloc_chrdev_region(dev_num, 0, 1, mydrv); cdev_init(my_cdev, my_fops); cdev_add(my_cdev, dev_num, 1); my_class class_create(mydrv); device_create(my_class, NULL, dev_num, NULL, mydrv%d, 0); return 0; }这段代码做的事情依次是向内核申请一个设备号、初始化字符设备对象并绑定file_operations、把设备加到内核、创建设备类、在/dev下生成设备节点。应用层随后就能通过open(/dev/mydrv0, ...)来访问你的设备。对应的file_operations长这样static const struct file_operations my_fops { .owner THIS_MODULE, .open my_open, .read my_read, .write my_write, .unlocked_ioctl my_ioctl, .release my_close, };注意unlocked_ioctl。旧内核里有个ioctl字段现在推荐用unlocked_ioctl因为内核默认不再为ioctl调大锁。这个细节虽然小但面试和代码审查里经常被问到。3.3 probe函数里到底该干什么platform驱动的probe函数是驱动初始化的主战场所有“准备让设备开始工作”的事情都在这里做。一个典型的probe函数通常包含这几件事读取设备树里的资源of_property_read_u32读参数platform_get_resource拿地址和中断号申请硬件资源ioremap或devm_ioremap_resource映射寄存器地址request_irq注册中断初始化硬件设置GPIO方向、配置外设寄存器、复位芯片注册字符设备或其他子系统接口初始化自旋锁、互斥锁、工作队列等内核同步机制。这里我强烈建议优先使用devm_开头的资源管理API比如devm_ioremap_resource、devm_kzalloc、devm_request_irq。这些API会在设备驱动卸载时自动释放资源省去你在remove函数里逐一手动释放的麻烦。一旦资源泄漏反复加载卸载驱动几次系统可用内存越来越少那种问题最难查。probe函数的一个特殊返回值值得单独说-EPROBE_DEFER。它表示“我依赖的资源现在还没准备好请过一会儿再叫我”。最常见的情形是多个设备之间存在依赖关系比如I2C设备依赖I2C控制器驱动先加载。内核会自动在依赖满足后再次调用你的probe。很多新人不知道这个机制在probe里依赖外部资源时没有返回-EPROBE_DEFER而是直接返回错误导致设备永远初始化不了。3.4 ioctl应用和驱动之间的自定义协议read和write适合传输连续的数据流但驱动开发里更多时候应用层要的是“查状态”“设参数”这类操作。典型例子读传感器的某个寄存器、设置采样频率、使能或关闭某个功能。这时候就该用ioctl。ioctl命令的格式也有讲究通常使用内核提供的宏#define MYDRV_SET_FREQ _IOW(M, 1, int) #define MYDRV_GET_TEMP _IOR(M, 2, int)_IOW和_IOR的意思是往内核写数据、从内核读数据。第一个参数是“魔数”用于区分不同驱动第二个是命令序号第三个是数据类型。这个编码规则保证了应用层和内核层对命令的理解一致。ioctl实现里有一个安全细节必须注意从用户空间传入的指针不能直接访问。内核里要用copy_from_user和copy_to_userif (copy_from_user(val, arg, sizeof(val))) return -EFAULT;直接解引用用户指针后果轻则数据错误重则内核崩溃。这个规矩没有例外哪怕你确信用户程序是自己写的也一定要走拷贝接口。4. 一个I2C驱动从零到能跑的全过程I2C大概是嵌入式外设里最常用的总线了。传感器、EEPROM、RTC、音频编解码器几乎全走I2C。搞懂I2C驱动的完整链路很多外设驱动都能触类旁通。4.1 I2C子系统里的三个角色Linux的I2C子系统分三层adapter控制器、client挂在总线上的从设备、driver从设备的驱动逻辑。adapter是SoC上的I2C控制器它负责在物理总线上产生时序client代表挂在总线上的一颗具体芯片driver就是我们要写的代码。这三者的关系可以类比成一个商店adapter是货架client是放在货架上的商品driver是商品的说明书。商品在货架上说明书和商品匹配上之后商品才能正常“工作”。你可以在用户空间快速查看当前系统有几条I2C总线i2cdetect -l每条总线都是kernel中的一个adapter设备树里I2C控制器节点使能后就会注册对应的adapter。4.2 从设备树节点到i2c_client的注册回到开头那个TMP117的例子。设备树里i2c2下的子节点内核在解析时会自动创建对应的i2c_client结构体挂到i2c2这个adapter上。这个过程对驱动开发者是透明的不需要你手动创建client。但如果你在一个没有设备树的老平台上工作就需要用i2c_new_client_device配合board_info来手动注册clientstatic struct i2c_board_info tmp117_info { I2C_BOARD_INFO(tmp117, 0x48), }; i2c_new_client_device(adapter, tmp117_info);这是设备树普及之前的旧方式现在大部分平台已经不再使用。理解这个机制有助于你明白i2c_client到底是怎么来的排查问题时知道在设备树和设备注册之间发生了什么。4.3 i2c_driver的代码结构与寄存器读写一个标准i2c_driver注册是这样的static const struct i2c_device_id tmp117_id[] { { tmp117, 0 }, { } }; MODULE_DEVICE_TABLE(i2c, tmp117_id); static struct i2c_driver tmp117_driver { .driver { .name tmp117, .of_match_table tmp117_of_match, }, .probe tmp117_probe, .remove tmp117_remove, .id_table tmp117_id, }; module_i2c_driver(tmp117_driver);module_i2c_driver是个简写宏它替你把module_init和module_exit都安排好并且会自动处理依赖关系。只要这个驱动文件被编译模块加载时就会向I2C子系统注册这个driver。probe函数里读写寄存器最常用的两个接口是s32 val i2c_smbus_read_byte_data(client, reg_addr); s32 ret i2c_smbus_write_byte_data(client, reg_addr, val);i2c_smbus_*系列接口会在I2C总线上自动组装读写的时序对大多数8位寄存器设备足够用。如果芯片有更复杂的随机访问需求比如先写16位寄存器地址再读N字节数据可以用i2c_transfer构造两个i2c_msg来完成。这里有一个特别容易坑人的点I2C设备返回的数据字节序。比如TMP117的温度寄存器是16位的有的芯片高字节在前有的低字节在前。读出来之后不处理字节序计算得到的温度值看起来就是乱码、甚至是负的离谱数字。写驱动时一定要查清楚datasheet里的字节序描述。还有ack失败的问题。I2C总线上如果从设备没上电、地址写错、或者上拉电阻没焊读寄存器时dev_err会报acknowledge failed。这不是驱动代码的bug而是硬件链路问题。我一般会用示波器或者逻辑分析仪看SCL/SDA波形确认ACK位电平是否被拉低。软件上排查总线和地址是否正确是调试I2C的第一步。4.4 用户态i2c-tools在调试阶段的妙用在写驱动之前我强烈建议先用用户态工具确认芯片本身能通。这个习惯能帮你把“硬件问题”和“驱动代码问题”快速隔离开。常用命令组合是i2cdetect -y 2 i2cget -y 2 0x48 0x00 i2cset -y 2 0x48 0x01 0x23i2cdetect扫描总线上所有从机地址能看到0x48出现在列表里说明设备和总线连接基本正常。i2cget读寄存器确认寄存器值符合预期。如果这些能通再回头查驱动代码如果用户态都通不了先查硬件。4.5 在FPGA/SoC平台上的I2C差异有些平台上的I2C控制器不是SoC原生的而是在FPGA里用IP核实现的比如Xilinx AXI IIC这种。设备树里的compatible会变成xlnx,xps-iic-2.00.a之类内核加载对应的适配器驱动。这种场景下你在设备树里需要特别注意控制器的寄存器地址范围、中断号是否与FPGA的地址分配一致。还有更常见的做法是GPIO模拟I2C设备树里用i2c-gpio节点描述i2c-gpio0 { compatible i2c-gpio; gpios gpio0 3 0, gpio0 4 0; i2c-gpio,delay-us 5; };这种方案适合低速、不计较CPU占用的场合。但它的时序全靠GPIO翻转模拟遇到总线挂死需要恢复时处理起来比原生I2C控制器麻烦不少。性能敏感的场景还是优先用控制器自带的I2C外设。5. 内核裁剪与系统镜像瘦身从冗余驱动到更快的启动驱动开发做到后面项目要交付生产了内核裁剪就提上日程。厂商默认BSP一般都把什么驱动都编译进去一个内核镜像动辄几十MB启动时间十几秒。裁剪优化的目标就是让镜像只包含当前产品真正需要的东西。5.1 先盘库存再动手术裁剪内核最忌讳拍脑袋。我见过有人为了缩小镜像把内核里所有不认识的功能全关掉结果启动到一半崩溃连串口日志都来不及看。正确的第一步是梳理当前产品的硬件清单。拿一份实际的嵌入式计算平台举例。板子上有CPU、DDR、eMMC、以太网PHY、USB Host、音频Codec、一个UART调试口可能还有几个GPIO控制的LED和按键。裁剪前要做一张映射表外设内核配置项编译方式eMMC存储CONFIG_MMCyFAT文件系统CONFIG_FAT_FSy以太网控制器CONFIG_STMMAC_ETHyUSB HostCONFIG_USB_XHCI_HCDyUARTCONFIG_SERIAL_8250y音频CodecCONFIG_SND_SOC_XXXm这张表是裁剪的路线图。没用到的东西才允许关用到的必须保住。5.2 从内核config到启动路径的逐层裁剪裁剪的层次一般从大功能开始。如果产品不需要无线功能CONFIG_BT、CONFIG_WIRELESS、CONFIG_CFG80211这些直接关掉。不需要显示输出CONFIG_DRM、CONFIG_FB也可以关闭。不需要文件系统多样性只保留ext4和fat16/32剩下的CONFIG_EXT2_FS、CONFIG_XFS_FS、CONFIG_BTRFS_FS都可以关。关注config的同时还要看启动日志。用dmesg按时间戳分析dmesg -T注意看哪些驱动在启动早期被加载哪些在很晚才加载。有些模块其实根本不需要但因为内核里没被裁剪掉启动时还在做类似设备扫描的操作白白浪费几百毫秒。5.3 文件系统层面的瘦身思路内核镜像瘦下来之后还要看rootfs。很多BSP的rootfs是一个完整的发行版里面装着一堆开发工具、文件系统工具、桌面组件。产品上线前这些都要清掉。我的做法是拿busybox重新拼一个最小rootfs。busybox把上百个常用命令合并到一个二进制文件里通过软链接来区分调用哪个命令。配置busybox时只勾选产品需要的applet比如常用的sh、ls、cat、mount、ifconfig、udhcpc这些就够了。大一点的可执行文件可以strip掉符号表aarch64-linux-gnu-strip --strip-unneeded /path/to/binary一个没有strip过的二进制可能有好几MBstrip之后可能只有几百KB。不过要小心如果产品后续需要远程抓取coredump调试符号信息就很有价值这时就不要急着strip或者在交付镜像之外的调试版本里保留符号。5.4 启动时间优化的实操方向裁剪和瘦身做完了接下来就是启动时间优化。这里有几个投产比很高的方向。内核参数精简。U-Boot的bootargs里console保留、root保留其余能省的省掉。quiet参数可以屏蔽大部分内核输出配合loglevel3只打印错误级别的日志减少串口输出对启动时间的拖累。串口在低波特率下打印大段日志非常耗时。异步probe。内核启动时platform设备是一个个串行匹配、串行probe的。如果某个设备的probe里有比较耗时的操作比如等待外部芯片复位、读取EEPROM数据会阻塞后续所有设备。内核提供了driver_async_probe参数可以指定某些驱动异步执行probedriver_async_probetmp117,sdhci需要注意的是异步probe后设备节点创建的顺序不再可控应用层如果依赖设备节点的创建顺序就需要额外处理。rootfs分区等待。rootwait会让内核无限等待root设备出现。如果rootfs在eMMC上eMMC初始化一般很快但有些平台的存储控制器要等固件加载等待时间不稳定。用rootdelay1设为固定1秒延迟或者优化为检测设备节点出现后才继续可以省掉rootwait带来的潜在长时间等待。6. 调驱动必备日志分级、崩溃现场与中断风暴驱动跑不起来或者跑着跑着系统崩了这是驱动开发的家常便饭。和纯应用开发不一样驱动崩溃往往直接让整个系统死掉给你留下的线索只有一串内核日志。学会从日志里快速定位问题是驱动工程师的核心技能。6.1 printk的级别把控与动态调试开关printk是驱动开发最常用的调试工具但它也讲究用法。printk有8个级别从KERN_EMERG到KERN_DEBUG。级别越低越紧急越容易被打印出来。平常开发用dev_info和dev_err就够这两个接口会自动带上设备名日志里能直接看到是哪个设备报错。调试细节时用dev_dbg默认情况下它会被编译器优化掉不产生日志。但内核提供了一套动态调试机制可以在编译时保留dev_dbg运行时按需打开# 挂载debugfs mount -t debugfs none /sys/kernel/debug # 打开某文件下所有动态调试日志 echo file tmp117.c p /sys/kernel/debug/dynamic_debug/control这个机制的好处是正式版本里不用删调试日志保留它们但默认关闭出问题时远程开一个开关就能拿到详细日志非常有用。6.2 oops信息里最关键的三行驱动出错导致系统oops时dmesg最后几行就是现场。很多人一看就慌其实oops信息主要看三处。第一处是PC is at ...它告诉你崩溃时CPU正在执行哪个函数。第二处是LR is at ...它告诉你这个函数是被谁调用的也就是调用者。第三处是Call trace它会列出完整的函数调用链。比如拿到这样一条Call traceCall trace: my_irq_handler0x28/0x48 __handle_irq_event_percpu0x50/0x70 handle_irq_event_percpu0x30/0x50崩溃点在my_irq_handler里。这时候我一般先去源码里看这个函数偏移0x28附近在做什么再用addr2line结合编译出来的vmlinux定位到具体源码行aarch64-linux-gnu-addr2line -e vmlinux -f my_irq_handler0x28如果符号被优化掉了addr2line可能只给出??。这时候System.map文件里的符号表可以作为对照找到函数起始地址手工计算偏移对应的指令再从objdump反汇编里看崩溃指令是什么。6.3 ftrace跟踪函数调用过程有些问题不是崩溃而是驱动行为完全不符合预期。这时候光靠printk打点效率太低我推荐用ftrace。挂载tracefs后设置跟踪器为函数图mount -t tracefs none /sys/kernel/tracing echo function_graph /sys/kernel/tracing/current_tracer echo my_function* /sys/kernel/tracing/set_ftrace_filter echo 1 /sys/kernel/tracing/tracing_on之后/sys/kernel/tracing/trace文件里就会记录每次调用my_function的完整调用链和执行时长。用这个方式你能看到驱动程序里哪些函数耗时异常、哪些函数被反复调用却什么都没干。6.4 一个真实排查案例中断风暴是怎么定位的有一次项目里系统负载很低但整体响应就是慢top里能看到一个ksoftirqd线程占CPU接近100%。我当时第一反应是中断异常直接看cat /proc/interrupts找数字增长极快的那一行发现是某个GPIO中断每秒触发上万次。再对照设备树这是按键的中断按常理人不应该按那么快。用示波器量引脚波形发现是一个浮动引脚在环境干扰下产生大量毛刺每次都触发边沿中断。这种情况如果只在软件里修就是把中断改成双边沿触发加软件去抖或者用request_threaded_irq把中断处理放到线程上下文降低耗时。但真正的根因是硬件上漏了上拉电阻。先让硬件工程师补上拉再在驱动里加去抖双管齐下才算彻底解决。这个案例给我的启发是驱动里的“性能问题”很多时候不是软件逻辑慢而是硬件信号质量差、中断频繁触发导致的无效开销。碰到性能问题先看一眼/proc/interrupts和/proc/softirqs能少走很多弯路。7. 开发机环境的三个小坑磁盘、乱码与DNS驱动开发除了在目标板上调试开发机上的环境问题一样能卡掉你半天时间。这三个问题是评论区里出现频率最高的我在这里一并说清楚。7.1 WSL里删除文件后Windows磁盘空间没释放很多人用WSL做Linux开发环境在WSL里删了大文件Windows的磁盘占用一点没减少。这是因为WSL发行版的文件系统存放在一个动态扩展的vhdx虚拟磁盘文件里删除文件不会自动回收已分配的空间vhdx只会增长不缩水。处理方法分两步。先在Windows命令行执行wsl --shutdown然后以管理员身份打开diskpart对WSL发行版的ext4.vhdx执行压缩select vhdx fileC:\Users\你的用户名\AppData\Local\Packages\...\ext4.vhdx attach vhdx compact vhdx detach vhdx exit做完这一步能节省大量磁盘空间。建议定时做一次别等到磁盘满了才处理。7.2 解压Windows压缩包出现乱码Windows下用中文名压缩文件时文件名编码大多是GBK而Linux发行版默认用UTF-8直接unzip解压就会出现文件名乱码。用支持指定编码的unzip版本可以解决unzip -O CP936 文件名.zip如果系统自带的unzip不支持-O参数可以用7z替代7z x 文件名.zip7z对中文编码的处理比老版本unzip明显好很多。注意不要在乱码已经出现之后再用convmv批量改名那会把正常文件也改乱得不偿失。7.3 配置DNS反复失效开发机上配置DNS很多人直接编辑/etc/resolv.conf但重启网络或者重启系统后配置就没了。这是因为现代Linux发行版大多用systemd-resolved或NetworkManager管理DNS配置/etc/resolv.conf其实是一个符号链接真正的配置源在别处。使用systemd-resolved的话正确做法是编辑/etc/systemd/resolved.conf[Resolve] DNS223.5.5.5然后重启服务systemctl restart systemd-resolved用resolvectl status可以查看当前实际生效的DNS服务器。如果你是在嵌入式板卡上开发没有systemd-resolved直接写/etc/resolv.conf就行但要注意别同时启动多个网络管理服务它们会互相覆盖配置。这几个问题看起来和驱动本身没直接关系但开发环境不通畅真正写驱动的效率会大打折扣。我自己有个习惯把这类开发机上遇到的问题单独记一份笔记什么现象、什么原因、怎么处理每次遇到都记一笔后期基本都能在几分钟内绕开。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →