news 2026/9/14 20:32:17

Linux驱动开发全路径:从内核模块到设备树再到I2C/CAN总线

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux驱动开发全路径:从内核模块到设备树再到I2C/CAN总线

我自己做了七八年Linux嵌入式驱动,属于那种被中断上下文和内核锁折磨过好几轮的人。今天想写一篇东西,把“Linux驱动开发”这条完整路径梳理出来——从最基础的内核模块,到设备树怎么描述硬件,再到I2C/CAN这两种工业场景里非常典型的总线设备驱动怎么上手。这个题目是根据我最近带新人的经验总结的,很多人一上来就刷“Linux设备驱动开发详解pdf”,或者对着瑞芯微rk3568的设备树文件发呆,结果卡在某个环节就动不了。问题的根源,往往不是不会写代码,而是对整个“从模块到总线再到设备节点”的协作机制缺少系统认知。这篇文章就按我实际跑项目时的顺序来讲,尽量把每一步的原理、步骤和坑都交代清楚,适合正在学习嵌入式Linux驱动开发、或者即将要在一款新板子上从零适配外设的朋友。

1. 从内核模块开始:驱动的血肉和骨架

1.1 为什么内核模块是起点

驱动开发的“起步动作”,永远是写一个能加载、能卸载的内核模块。内核模块(LKM)这个东西,说白了就是一段能在Linux内核运行期间动态插进去执行的代码,不用为了加一个驱动就把整个内核重新编译一遍。对于设备驱动来说,模块就是驱动程序的“壳”,你的probe函数、read/write回调、中断处理全都在这个壳里运行。

很多初学者会觉得模块开发太“玩具”,不如直接改内核源码“正规”。我的看法正好相反:日常调试用模块是最合适的,迭代速度快,出了问题不会把内核搞挂(最多Oops掉当前进程,重启板子就好),而且能逼你搞明白MODULE_LICENSE、module_init/module_exit、printk这些基本机制。等代码稳定了再考虑编译进内核,这属于正确的工作流,不是偷懒。

1.2 模块的加载流程和生命周期

一个最朴素的模块代码大概是这样的:

#include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> static int __init mydrv_init(void) { printk(KERN_INFO "mydrv: module loaded\n"); return 0; } static void __exit mydrv_exit(void) { printk(KERN_INFO "mydrv: module unloaded\n"); } module_init(mydrv_init); module_exit(mydrv_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Your Name"); MODULE_DESCRIPTION("A minimal driver example");

编译用的是内核提供的kbuild机制,Makefile长这样:

obj-m += mydrv.o KERN_DIR ?= /lib/modules/$(shell uname -r)/build all: $(MAKE) -C $(KERN_DIR) M=$(PWD) modules clean: $(MAKE) -C $(KERN_DIR) M=$(PWD) clean

这里有个很多人忽略的点:obj-m把mydrv.o认成外部模块,如果改成obj-y则会编译进内核镜像。对于驱动开发阶段,obj-m加insmod/rmmod就能完成“加载-测试-卸载”闭环,日志直接看dmesg输出。搞明白这一点,你对内核构建系统的理解就会比只会敲make命令的人深一层。

1.3 从模块到字符设备:file_operations的意义

光能打印日志的模块没有实际使用价值。真正的驱动模块,是要向用户空间暴露操作接口的。最常见的形态就是字符设备,通过注册file_operations结构体,把open/read/write/ioctl等系统调用和内核里的实现函数绑起来。

static ssize_t mydrv_read(struct file *file, char __user *buf, size_t count, loff_t *offset) { // 把内核缓冲区的数据拷贝到用户空间 if (copy_to_user(buf, kernel_buf, count) != 0) return -EFAULT; return count; } static const struct file_operations mydrv_fops = { .owner = THIS_MODULE, .open = mydrv_open, .read = mydrv_read, .write = mydrv_write, .release = mydrv_release, };

我用内核模块动态加载去拦截read/write的路径,就是在这个层面上做文章。mydrv_fops里除了标准的读写,你完全可以插入自己的逻辑,比如加密解密、统计计数、权限校验。这里的技术要点是:file_operations里的回调全部运行在进程上下文,可以睡眠;但如果是中断处理函数里触发的逻辑,就得用工作队列或tasklet推后执行。新手最容易犯的错误就是在中断上下文里调用kmalloc(..., GFP_KERNEL),直接导致睡眠。这类问题基本只能靠长期积累经验才能形成条件反射式的警觉。

2. 设备树:驱动和硬件之间的“对接说明书”

2.1 设备树到底解决什么问题

嵌入式平台发展早期,各种外设的信息(地址、中断号、时钟频率)都是直接写死在驱动源码里的。板子一换,就得改代码重编内核。设备树(Device Tree)就是把这些“硬件参数”从代码里抽出来,变成一份独立的数据文件。驱动代码变成“通用逻辑”,具体板子的差异全部体现在设备树二进制(DTB)里。

打个比方:驱动像是“豆浆机的通用电机”,设备树就是“说明书上的安装孔位”。同一台电机,装在不同的机箱上,不用改电机本身,改说明书就行。这个思想贯穿整个Linux驱动开发体系,尤其是I2C/CAN这类总线设备。

2.2 dts、dtsi、dtb:设备树的三种形态

设备树文件有三种形态,很多人傻傻分不清:

  • dts(device tree source):文本格式,描述一块特定板子(board-level)的硬件;
  • dtsi(device tree source include):也是文本格式,但描述的是SoC级公共内容,比如rk3568.dtsi里定义CPU、内存、UART等所有芯片级外设,可以被多个dts文件include复用;
  • dtb(device tree blob):dts编译后生成的二进制文件,内核启动时实际在用的对象。

实际操作时,dts文件之间的include关系是树状的。以rk3568平台为例,你要新加一个外部设备,大概率是在某个板级dts上添加节点,或者通过&i2c1这样的引用往SoC级节点里追加内容。很多新手直接把整个外设节点写在最顶层的dts里,语法看着对,但会被dtsi里的同名节点覆盖或合并,导致神秘故障。遇到这种情况,务必弄清节点的“叠加规则”:同一路径的节点,属性会合并,同名字节数组/compatible会被覆盖。

2.3 设备树里的compatible匹配机制

设备树和驱动的衔接点,是compatible属性。驱动注册时需要定义一个of_device_id匹配表,里面的compatible字符串和设备树节点里的compatible字符串一致时,内核才会调用驱动的probe函数。举个经典例子:

static const struct of_device_id my_i2c_driver_of_match[] = { { .compatible = "solomon,ssd1306" }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, my_i2c_driver_of_match); static struct i2c_driver my_i2c_driver = { .driver = { .name = "my_i2c_driver", .of_match_table = my_i2c_driver_of_match, }, .probe = my_i2c_driver_probe, .remove = my_i2c_driver_remove, };

设备树里对应节点:

&i2c1 { status = "okay"; my_oled: ssd1306@3c { compatible = "solomon,ssd1306"; reg = <0x3c>; reset-gpios = <&gpio4 RK_PB5 GPIO_ACTIVE_LOW>; }; };

而驱动侧初始化的时候,做的是i2c_add_driver(&my_i2c_driver)。内核的I2C核心会在总线扫描时,把设备树里的client节点和驱动匹配上,然后调用probe。注意,这里的地址0x3c是设备在I2C总线上的从机地址,必须和硬件实际地址一致。很多人忽略了一点:ssd1306的I2C地址由硬件引脚决定,常见是0x3c或0x3d。你配错了dts,probe再正确也读不到数据。

2.4 设备树节点的复位引脚与GPIO配置技巧

搜索热词里有一条“linux 设备树设置复位信号时间”,这在实际项目中经常遇到。很多外设(比如OLED、触摸屏、CAN收发器)都有复位引脚,需要驱动在probe或初始化时控制时序。设备树里面可以通过reset-gpios描述复位引脚,然后驱动里用devm_gpiod_get获取GPIO描述符,再用gpiod_set_value控制电平。关键点在于“复位信号时间”,也就是拉低复位引脚后必须等待一段时间再释放,不同芯片要求不一样,有的要几毫秒,有的要几十毫秒。

我通常的做法是:

reset-gpios = <&gpio4 RK_PB5 GPIO_ACTIVE_LOW>;

驱动里:

reset_gpio = devm_gpiod_get(&client->dev, "reset", GPIOD_OUT_HIGH); if (IS_ERR(reset_gpio)) { dev_err(&client->dev, "failed to get reset gpio\n"); return PTR_ERR(reset_gpio); } gpiod_set_value(reset_gpio, 0); // 拉低复位 msleep(20); // 等待20ms gpiod_set_value(reset_gpio, 1); // 释放复位 msleep(100); // 等待芯片完成上电初始化

这里的msleep是真正的“休眠等待”,不是忙等,调用时进程会进入睡眠,所以千万不能用在自旋锁保护的临界区里。复位时间为什么必须看datasheet而不是随便写,因为太短芯片可能还没有彻底复位,太长又会拖慢启动流程。我实测过一些OLED模组,复位时间低于5ms会出现初始化过程中随机花屏的现象,这种事真的只能靠示波器或反复测试去验证。

3. I2C子系统:驱动为主、工具为辅的开发路径

3.1 I2C总线的核心概念

I2C(Inter-Integrated Circuit)是嵌入式系统里最常用的低速板级总线之一。它只靠两根线(SCL时钟线、SDA数据线)就能挂载多个设备,每个设备有一个7位或10位地址。通信时由主机(一般是SoC的I2C控制器)发起时序,从机响应地址和数据。Linux内核把这种“一主多从”的拓扑抽象成I2C adapter(主机控制器)和I2C client(从设备)两个角色。

架构上分三层:最上层是你的设备驱动(比如ssd1306 OLED驱动),中间是I2C核心(提供i2c_transfer等API),最底层是I2C adapter驱动(具体硬件控制器,比如Rockchip的I2C控制器驱动)。我们做设备驱动时,通常只跟client层打交道,通过i2c_transfer或i2c_smbus_read/write系列接口完成寄存器读写。

3.2 手写一个ssd1306 OLED驱动

为了把I2C client驱动的套路讲透,我拿SSD1306 OLED屏为例。这是一个经典到不能再经典的I2C设备,接口协议简单、时序直观,很适合第一次接触I2C驱动的人练手。ssd1306的I2C通信流程是:主机发送从机地址(0x3C写方向),然后发送控制字节(0x00表示后续是命令,0x40表示后续是显示数据),再发送Command/Data内容。

驱动初始化流程如下:

static int my_ssd1306_probe(struct i2c_client *client, const struct i2c_device_id *id) { if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C)) { dev_err(&client->dev, "I2C functionality not supported\n"); return -ENODEV; } ssd1306_oled = devm_kzalloc(&client->dev, sizeof(struct ssd1306_dev), GFP_KERNEL); if (!ssd1306_oled) return -ENOMEM; // 发送初始化命令序列 ssd1306_write_cmd(client, 0xAE); // display off ssd1306_write_cmd(client, 0xD5); // clock div ssd1306_write_cmd(client, 0x80); ssd1306_write_cmd(client, 0xA8); // multiplex ratio ssd1306_write_cmd(client, 0x3F); ssd1306_write_cmd(client, 0xD3); // display offset ssd1306_write_cmd(client, 0x00); ssd1306_write_cmd(client, 0x40); // start line ssd1306_write_cmd(client, 0x8D); // charge pump ssd1306_write_cmd(client, 0x14); ssd1306_write_cmd(client, 0x20); // memory mode ssd1306_write_cmd(client, 0x00); ssd1306_write_cmd(client, 0xA1); // segment remap ssd1306_write_cmd(client, 0xC8); // com scan direction ssd1306_write_cmd(client, 0xDA); // com pins ssd1306_write_cmd(client, 0x12); ssd1306_write_cmd(client, 0xAF); // display on return 0; }

而写命令的具体实现,核心是用i2c_master_send或i2c_transfer组装一帧数据:

static int ssd1306_write_cmd(struct i2c_client *client, u8 cmd) { u8 buf[2]; buf[0] = 0x00; // control byte: command buf[1] = cmd; return i2c_master_send(client, buf, 2); } static int ssd1306_write_data(struct i2c_client *client, u8 *data, int len) { u8 *buf; int ret; buf = devm_kzalloc(&client->dev, len + 1, GFP_KERNEL); if (!buf) return -ENOMEM; buf[0] = 0x40; // control byte: data memcpy(buf + 1, data, len); ret = i2c_master_send(client, buf, len + 1); devm_kfree(&client->dev, buf); return ret; }

这里有一个细节值得多说一句:devm_kzalloc是设备资源管理的分配函数,好处是如果probe失败或设备移除,内核会自动释放内存,不用手动配对kfree。这能省掉很多潜在的内存泄漏问题,我写驱动时能不用手动管理就不手动管理。

3.3 用户态快速验证:i2c-get和i2c-tools

不是所有场景都要立刻写内核驱动。很多时候我先在用户态用i2c-tools把硬件疏通,调通协议逻辑,再写内核驱动。这个“先用户态、后内核态”的流程对调试效率提升非常大。

# 枚举总线上所有I2C设备 i2cdetect -y 0 # 0号总线上扫描到的设备地址,如果看到0x3c就说明SSD1306已经在线 # 读取设备寄存器 i2cget -y 0 0x3c 0x00 # 如果返回0x00,表明芯片正在响应 # 写入设备寄存器 i2cset -y 0 0x3c 0x00 0xAE # 发送display off命令

i2c-tools的价值在于,它让你在写第一行驱动代码之前,就能确认硬件连接、设备地址和寄存器读写逻辑。如果i2cdetect都扫不到设备,说明硬件有问题,写内核驱动再完美也无济于事。我自己调试时,如果客户报“屏幕没点亮”,第一句话就是先让他跑i2cdetect,八成是没接上拉电阻或者地址焊错。

3.4 I2C驱动开发里的常见坑

I2C驱动开发踩过的坑,很多可以提炼成经验:

第一,设备树里地址和实际硬件不匹配。I2C地址是7位,但有些datasheet写的是8位地址(左移了一位)。比如显示0x78,实际设备树节点应该写reg = <0x3c>。搞混的话,i2cdetect也找不出问题,但驱动就是probe不了。

第二,时序匹配问题。I2C有标准模式(100kHz)、快速模式(400kHz)和高速模式(3.4MHz)。芯片规格不同,能支持的速率也不同。对于慢速设备,一定要减小I2C时钟频率,或者看dts里adapter的clock-frequency配置。

第三,没有处理NACK。当从设备不响应时,I2C控制器会返回NACK。驱动里如果没有对i2c_transfer的返回值做充分检查,后续可能会基于无效数据继续执行,导致莫名其妙的逻辑错误。我习惯的做法是每次读写都检查返回值和传输长度。

4. CAN总线:SocketCAN架构下的设备接入

4.1 CAN总线在工业与车载场景的位置

CAN(Controller Area Network)总线是工业控制、汽车电子领域最常用的现场总线之一。它采用差分信号传输,抗干扰能力强,支持多主通信,错误检测机制完善。与I2C/SPI这类板内总线不同,CAN总线通常用于设备之间的远距离通信,比如汽车里的ECU、电池管理(BMS)、仪表盘,以及工业设备里的传感器、PLC。

Linux对CAN的支持方式很独特,它不是提供一套用户态API给驱动直接调用,而是把CAN接口抽象成网络接口(如can0),走的是SocketCAN这个内核子系统。也就是说,你在Linux下操作CAN,跟操作以太网卡是同一套思路:先配置链路层参数(比特率),再用socket收发数据帧。这种设计的最大好处是应用层开发非常方便,不需要关心具体硬件驱动。

4.2 SocketCAN架构与驱动接入点

SocketCAN的驱动层次分为:CAN设备驱动(net_device)、CAN核心协议层(can subsystem)、应用层(socket)。驱动要做的最核心工作是:注册一个net_device,实现ndo_openndo_stopndo_start_xmit这几个回调函数,以及接收数据的软中断处理路径(通常通过netif_rx把收到的帧交给协议栈)。

从设备树的角度来看,CAN控制器的节点一般长这样:

&can1 { status = "okay"; pinctrl-names = "default"; pinctrl-0 = <&can1m0_pins>; phys = <&can_phy0>; // 如果有外部收发器 };

很多国产SoC的CAN控制器是基于Bosch M_CAN或D_CAN内核实现的,内核里已经有现成的驱动,设备树里只需要打开节点、配好引脚mux,再用ip命令设置比特率并启动接口,就能跑起来。

4.3 CAN的设备树节点与收发器配置

下载一份RK3568的dts,你会看到类似这样的CAN节点配置:

&can0 { status = "okay"; compatible = "rockchip,can-2.0"; reg = <0x0 0xfe5c0000 0x0 0x1000>; interrupts = <GIC_SPI 138 IRQ_TYPE_LEVEL_HIGH>; clocks = <&cru CLK_CAN0>, <&cru PCLK_CAN0>; clock-names = "baudclk", "apb_pclk"; pinctrl-names = "default"; pinctrl-0 = <&can0m0_pins>; };

这里reg控制器的寄存器地址段,interrupts中断号,clocksclock-names是时钟信息。如果外接了收发器(比如SJA1000、TJA1050),有些设计还需要在设备树里控制收发器的STBY引脚或使能引脚,这类带反馈控制的收发器,就需要在驱动里做额外的GPIO处理。

我总是建议新手把CAN节点的时钟频率确认清楚。CAN的波特率计算依赖时钟频率,配错了会导致传输异常,而且这种异常往往不会立刻暴露,而是体现在丢帧或帧错误上。用ip -details link show can0可以看到控制器实际配置,如果显示bitrate和你预期不一致,就去检查pclk时钟。

4.4 CAN驱动的注册流程核心

如果SoC的CAN控制器是M_CAN,内核已经有完善的m_can驱动框架,通常不需要从零写CAN控制器驱动。但偶尔会遇到自定义控制器或者旧平台,需要自己实现一个简版驱动。下面这个骨架展示了CAN驱动的注册入口:

static int my_can_probe(struct platform_device *pdev) { struct net_device *dev; struct my_can_priv *priv; dev = alloc_candev(sizeof(struct my_can_priv), my_can_tx_ring_size); if (!dev) return -ENOMEM; dev->netdev_ops = &my_can_netdev_ops; dev->flags |= IFF_ECHO; // CAN设备需要回显功能 priv = netdev_priv(dev); priv->dev = dev; priv->base = devm_platform_ioremap_resource(pdev, 0); if (IS_ERR(priv->base)) { free_candev(dev); return PTR_ERR(priv->base); } priv->irq = platform_get_irq(pdev, 0); if (priv->irq < 0) { free_candev(dev); return priv->irq; } ret = register_candev(dev); if (ret) { free_candev(dev); return ret; } platform_set_drvdata(pdev, dev); return 0; } static struct platform_driver my_can_driver = { .driver = { .name = "my_can", .of_match_table = my_can_of_match, }, .probe = my_can_probe, .remove = my_can_remove, }; module_platform_driver(my_can_driver);

这里面最值得注意的是alloc_candevregister_candev这两个函数,它们跟一般net_device处理不一样,是CAN子系统特有的。alloc_candev会额外分配CAN协议栈需要的私有数据空间。如果你漏了这一步,直接用alloc_netdev去注册CAN网络接口,接口会起不来,或者收发数据不对。这种“看起来像网卡、实际上不是普通网卡”的区别,正是SocketCAN框架需要专门适配的原因。

4.5 用户态工具链与CAN调试流程

CAN接口驱动的测试,离不开can-utils这套用户态工具。经典操作流程是:

# 1. 查看CAN是否识别 ip link show # 2. 配置比特率并启动接口 sudo ip link set can0 up type can bitrate 500000 # 如果该命令失败,先确认dts节点是否打开,或者检查clock时钟 # 3. 监听总线上所有报文 candump can0 # 4. 发送一帧扩展帧 cansend can0 123#DEADBEEF # 5. 发送时加时间戳并输出十六进制 candump -L can0 # 6. 查看接口统计 ip -details -statistics link show can0

实际项目里,CAN调试最常用的组合是:一台设备跑canbuscandump,另一台不断发周期报文,观察收发的正确性和实时性。CAN总线的波特率务必两边一致,不然可以看到total_tx正常,但接收端总是报错帧,严重时接口会进入bus-off状态。在总线调试中最容易忽略的是终端电阻——CAN总线两端必须各接一个120Ω终端电阻,不然信号反射会让通信极不稳定,时好时坏。这个不是驱动问题,但如果你在实验室调板卡调了半天,板子之间的距离超过1米就开始丢帧,可以先检查终端电阻。

4.6 CAN驱动的实时性与性能调优

很多人以为CAN驱动只要收发数据正常就行了,但在实际工业场景里,实时性也是硬指标。我在做车载项目时,遇到过由于CAN中断处理太长,导致高优先级报文延迟的情况。内核里CAN帧的接收路径本来就在中断上下文,你把can_rx_offload这类机制配置不好,就会出现大量的receive error。

常见的性能优化手段有以下几种:

第一,开启NAPI接收。NAPI可以让中断处理变得更轻量,把报文处理放到软中断里批量进行,减少中断触发频率。对CAN这种中低速总线来说效果很好。

第二,增大tx_ring_sizerx_ring_size。分配CAN设备时通过alloc_candev的size参数控制缓冲数量。如果发送压力大,默认的10帧缓冲可能不够用,可以把报文缓冲加大到64或128。

第三,关注时钟频率对波特率误差的影响。CAN波特率误差超过1.5%就会不稳定,当外设时钟不是标准波特率整数倍时,误差可能非常接近临界值。这时候要给时钟适配器或时钟源提供更加粗偏的时钟,或者选择最接近的波特率组合。

不过说实话,对刚接触CAN的开发者,以上调优可以做但不必强求。先把基础收发路径跑通,再考虑性能优化,这个顺序是对的。

5. 常见问题与调试心法实录

5.1 probe函数不执行

这是驱动开发中遇得最多的问题。当insmod成功,但probe就是不被调用,而我按以下顺序排查,往往很快定位:

第一步,检查设备树节点是否真的存在。ls /proc/device-tree/查看对应节点路径,确认status不是disabled。很多SoC开发板默认把不需要的外设节点设成disabled,需要手动改成okay。

第二步,检查compatible字符串是否一致。设备树里的compatible和驱动里的of_device_id完全一致的时候,内核才会匹配。这里有一个隐藏细节:MODULE_DEVICE_TABLE(of, ...)of_match_table必须同时存在,尤其是模块化编译时,缺少MODULE_DEVICE_TABLE会直接导致自动加载功能失效。

第三步,看内核日志。驱动probe被调用时会有注册消息,没被调用时,可以看看dmesg | grep -i i2c之类,确认子系统扫描是否到达节点。有些平台的I2C adapter扫描顺序和dts里的节点顺序不一致,也会让人困惑半天。

5.2 内核打印看不见

内核的printk默认输出等级是4,也就是KERN_WARNING及以上的消息才打。如果驱动里用的是printk(KERN_INFO ...),终端上通常看不到。解决办法两个:一是dmesg -n 8把控制台日志级别调高,二是干脆用dev_info(&client->dev, "...")这样的动态打印,然后通过echo 'file xxx.c +p' > /sys/kernel/debug/dynamic_debug/control打开调试开关。

动态调试(dynamic debug)是排查驱动问题的利器。我现在的习惯是:代码里多用dev_dbg,发布版本用make关闭,调试版本通过sysfs按需开启。这比printk频繁加删注掉强太多,而且性能开销几乎为零。

5.3 I2C/CAN设备树修改后不生效

设备树的修改跟普通C代码不一样,不是重新编译内核就完事了。需要重新编译dtb,并确保bootloader加载的是新的dtb。编译时通常在kernel/arch/arm64/boot/dts/rockchip/目录下执行:

make dtbs

然后把生成的dtb拷贝到BOOT分区,再重启。很多板子支持fastboot或rekernel的方式单独更新dtb,不需要烧整个内核。

如果你改了dts没重启,或者重启后dtb还是旧的,那问题基本出在eMMC/SD卡上的BOOT分区。确认分区挂载路径,对比一下dtb的修改时间,这种问题就能快速定位。

5.4 模块加载时提示“Invalid module format”

这个常见于跨内核版本编译。模块的内核版本和当前运行内核版本不一致,就会出现这种报错。解决办法是:确保用当前内核版本的源码进行编译,并且KERN_DIR指向正确的版本目录。交叉编译时,还要确保ARCHCROSS_COMPILE环境变量设置正确。这里有个容易踩坑的点:你用的内核源码树和实际运行的内核不一定是同一份,最好在板子上执行uname -r,然后严格使用相同版本的源码,避免头文件不匹配。

5.5 I2C地址冲突与总线上其他设备干扰

当某个I2C总线上挂了多个设备时,如果地址不唯一,总线会混乱。排查技巧是用i2cdetect扫描整条总线,看是否有重复地址。另外一些设备支持地址引脚配置,比如ssd1306的SA0引脚决定地址最低位,把两个SSD1306接在同一条总线上时,必须用不同电平让它们拥有不同地址。

6. 写在最后的经验

上面这些内容,基本就是一个人从“只会编译内核模块”到“能独立调通I2C/CAN设备”需要经历的主要路程。模块是驱动的框架,设备树是驱动的说明书,I2C和CAN则代表了两种典型的设备挂载方式。把这几个环节串起来,你就具备了在任意ARM Linux平台上接入新外设的基本能力。

最后再分享一个实际操作中我踩过很多次坑的体会:写驱动代码本身往往不是最耗时间的,真正麻烦的是调试环境不顺畅。所以建议你在动手前,先把三件事准备好:交叉编译工具链能出可执行文件、设备树能单独编译并下载、串口和dmesg日志能清晰输出。这三件基础设施一旦通畅,后续的驱动开发就是按部就班地照着datasheet填寄存器、调时序。做驱动开发,耐心比聪明值钱。很多问题看起来玄学,其实都是硬件连接、时钟频率和地址匹配这些基础因素在作祟,一层层排查下去,总能找到答案。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 20:31:32

Go 语言与 Cgo 混合编程性能剖析:在 C++ 推理库调用中的避坑指南

Go 语言与 Cgo 混合编程性能剖析&#xff1a;在 C 推理库调用中的避坑指南在构建高性能 AI 推理网关与向量检索底座时&#xff0c;Go 语言因其卓越的高并发网络调度能力&#xff08;Goroutine&#xff09;成为了构建网关的首选&#xff1b;而底层的高性能大模型推理引擎&#x…

作者头像 李华
网站建设 2026/9/14 20:29:17

2026年企业级BI平台选型指南与实战对比

1. 企业级BI平台选型的关键挑战2026年的企业数据分析环境正在经历一场深刻变革。传统BI工具与AI能力的深度融合&#xff0c;让企业决策者面临前所未有的选择困境。最近我为三家不同规模的客户完成了BI平台迁移方案&#xff0c;发现选型失误导致的隐性成本往往超出预算30%以上—…

作者头像 李华
网站建设 2026/9/14 20:28:53

嵌入式行业十大高含金量认证解析与备考指南

1. 嵌入式行业认证的价值与现状在嵌入式系统开发领域&#xff0c;专业认证一直是个备受争议的话题。我见过不少同行对考证嗤之以鼻&#xff0c;认为"代码能力才是硬道理"&#xff1b;也遇到过许多工程师&#xff0c;在考取认证后获得了意想不到的职业突破。经过十多年…

作者头像 李华
网站建设 2026/9/14 20:28:07

轻量开源版IDEA:社区版调优与免费替代方案

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

作者头像 李华