当前位置: 首页 > news >正文

Day67 Linux I²C 总线与设备驱动架构、开发流程与调试

day67 Linux I²C 总线与设备驱动架构、开发流程与调试


一、I²C 驱动体系结构总览

Linux I²C 子系统采用 分层 + 分离 的设计思想,整体架构如下:

+---------------------+
|     应用层 (User)   | ← 可直接调用 I²C 接口(如 i2c-tools)
+---------------------+
|   I²C 设备驱动      | ← 如 lm75_driver、at24c08_driver
|   (Client Driver)   |
+---------------------+
|    I²C 核心层       | ← 内核提供,负责匹配 device 与 driver,
|   (I²C Core)        |   管理 adapter 与 client 的绑定
+---------------------+
|   I²C 总线驱动      | ← 如 i2c-s3c2410、i2c-gpio
|   (Adapter Driver)  |
+---------------------+
|     硬件 (HW)       | ← I²C 控制器 + 外设(如 LM75 接在 I²C1 上)
+---------------------+

核心实体说明

实体说明
I²C 总线驱动(Adapter / Bus Driver)实现 I²C 物理总线的读写能力(如 SCL/SDA 控制、时序生成),操作对象是 I²C 总线硬件(如 iic0、iic1、iic2)
I²C 设备驱动(Client Driver)针对具体 I²C 外设(如 lm75、at24c08)实现其功能逻辑,操作对象是 I²C 从设备
platform driver / device基于 platform bus 框架,用于描述和驱动 I²C 控制器硬件(即 I²C adapter)
i2c_adapterI²C 适配器(也称 I²C 控制器或主机控制器),负责管理一条 I²C 总线的通信
i2c_algorithm定义 I²C 总线通信规则(如时序生成方式),是 master_xfer() 的“说明书”
master_xfer()基于 algorithm 实现具体的数据收发,是“执行器”,通过解析 i2c_msg 数组完成传输
i2c_client代表 I²C 从设备,包含设备地址、所属 adapter 等信息
i2c_msg描述一次 I²C 传输的消息结构体

关键理解

  • 总线驱动操作“总线”(如 iic0、iic1)
  • 设备驱动操作“设备”(如 lm75、at24c08)
  • 设备驱动通过 client->adapter->algo->master_xfer() 调用底层总线能力

二、I²C 设备驱动开发流程(以 LM75 为例)

步骤 1:设备树(DTS)中声明设备

&i2c1 {clock-frequency = <100000>;pinctrl-names = "default";pinctrl-0 = <&pinctrl_i2c1>;status = "okay";lm75@48 {compatible = "ti,lm75";  // 必须包含厂商前缀reg = <0x48>;            // I²C 从设备地址};
};

操作命令

vim arch/arm/boot/dts/pt.dts
make pt.dtb
cp arch/arm/boot/dts/pt.dtb ~/tftpboot

⚠️ 注意compatible 字符串必须与驱动中的匹配表一致,否则 probe 不会触发。


步骤 2:编写内核设备驱动(lm75_driver1.c)

#include <linux/init.h>
#include <linux/printk.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <asm/io.h>
#include <asm/string.h>
#include <asm/uaccess.h>
#include <linux/miscdevice.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/i2c.h>
#include <linux/of.h>#define DEV_NAME "lm75"
static struct i2c_client * lm75_client;  // 保存 client 指针,供 read/write 使用// 打开设备文件时调用
static int open(struct inode * node, struct file * file)
{return 0;
}// 读取温度数据
static ssize_t read(struct file * file, char __user * buf, size_t len, loff_t * loff)
{// iic read/writeint ret = 0;unsigned char data[2] = {0};struct i2c_msg msg;// 第一步:写寄存器地址(0x00,温度寄存器)msg.addr = lm75_client->addr;   // 从 client 获取设备地址msg.flags = 0;                  // 写操作msg.len = 1;                    // 写1字节地址msg.buf = data;                 // data[0] 默认为0,即寄存器0ret = lm75_client->adapter->algo->master_xfer(lm75_client->adapter, &msg, 1);if(ret < 0)return ret;// 第二步:读取2字节温度数据msg.addr = lm75_client->addr;msg.flags = I2C_M_RD;           // 读操作标志msg.len = 2;                    // 读2字节msg.buf = data;                 // 接收缓冲区ret = lm75_client->adapter->algo->master_xfer(lm75_client->adapter, &msg, 1);if(ret < 0)return ret;// 将内核数据拷贝到用户空间ret = copy_to_user(buf, data, 2);	return ret;
}// 写操作(本例未实现)
static ssize_t write(struct file * file, const char __user * buf, size_t len, loff_t * loff)
{return 0;
}// 关闭设备文件
static int close(struct inode * node, struct file * file)
{return 0;
}// 文件操作结构体
static  struct file_operations fops = 
{.owner = THIS_MODULE,.open = open,.read = read,.write = write,.release = close
};// misc 设备结构体(自动分配次设备号)
static struct miscdevice misc = 
{.minor = MISC_DYNAMIC_MINOR,.name = DEV_NAME,.fops = &fops
};// 设备匹配成功时调用(probe)
static int probe(struct i2c_client * pclient, const struct i2c_device_id * device_id)
{int ret = misc_register(&misc);if(ret < 0)goto err_misc;lm75_client = pclient;  // 保存 client 指针printk("lm75  probe  slave addr = 0x%x ...\n", lm75_client->addr);return 0;err_misc:printk("misc_register  failed\n");return ret;
}// 设备移除时调用
static int remove(struct i2c_client * pclient)
{misc_deregister(&misc);printk("lm75  remove  ...\n");return 0;
}// 支持的设备列表(用于匹配)
static const struct i2c_device_id lm75_table[] = 
{{.name = "lm75"},{}
};// I²C 驱动结构体
static struct i2c_driver lm75_driver = 
{.probe = probe,.remove = remove,.driver = {.name = DEV_NAME,},.id_table = lm75_table
};// 模块初始化
static int __init lm75_driver_init(void)
{int ret = i2c_add_driver(&lm75_driver);if(ret < 0)goto err_i2c_add;printk("lm75_driver_init    ...\n");return 0;err_i2c_add:return ret;
}// 模块退出
static void __exit lm75_driver_exit(void)
{i2c_del_driver(&lm75_driver);printk("lm75_driver_exit   ...\n");
}module_init(lm75_driver_init);
module_exit(lm75_driver_exit);
MODULE_LICENSE("GPL");

代码功能说明

  • 通过 miscdevice 注册 /dev/lm75 设备节点
  • probe 中保存 i2c_client 指针
  • read 函数实现标准 I²C 读流程:先写寄存器地址 0x00,再读 2 字节温度数据
  • 使用 master_xfer() 直接调用底层总线驱动的传输函数(不推荐,应使用 i2c_transfer(),但此处保留原代码)

编译与加载命令

make modules
cp drivers/char/lm75_driver1.ko ~/nfs/imx6/rootfs
insmod lm75_driver1.ko

理想运行结果

/ # insmod lm75_driver1.ko
lm75  probe  slave addr = 0x48 ...
lm75_driver_init    ...
/ # ./lm75_app1
temp = 27.5
temp = 28.0
temp = 28.5
...
/ # rmmod lm75_driver1
lm75  remove  ...
lm75_driver_exit   ...

步骤 3:编写用户空间测试程序(lm75_app1.c)

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <string.h>
#include <sys/ioctl.h>int main(int argc, const char *argv[])
{int fd = open("/dev/lm75", O_RDWR);  // 打开驱动创建的设备节点if(fd < 0){perror("open lm75 ");return -1;}unsigned char data[2] = {0};while(1){int ret = read(fd, data, 2);  // 读取2字节原始温度数据// 解析:LM75 温度 = (16位值 >> 7) * 0.5°Cfloat temp = (((data[0] << 8) | data[1]) >> 7) * 0.5;printf("temp = %.1f\n", temp);sleep(2);}close(fd);return 0;
}

编译命令

arm-linux-gnueabihf-gcc lm75_app1.c -o lm75_app1

💡 注意:此程序依赖内核驱动创建的 /dev/lm75。若驱动未加载,open 会失败。


三、应用层直接操作 I²C 总线(验证阶段)

在编写驱动前,必须先用应用层程序验证硬件通信

#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <linux/i2c-dev.h>
#include <linux/i2c.h>
#include <sys/ioctl.h>int main(int argc, const char *argv[])
{int fd = open("/dev/i2c-0", O_RDWR);  // 注意:此处应为 i2c-1(若接在 I2C1)if(fd < 0){perror("open i2c-0");return -1;}ioctl(fd, I2C_SLAVE, 0x48);  // 设置从设备地址为 0x48while(1){unsigned char buf[2] = {0};// 第一步:写寄存器地址(0x00)int ret = write(fd, buf, 1);  // buf[0]=0printf("ret = %d\n", ret);// 第二步:读取2字节数据read(fd, buf, sizeof(buf));float temp = (((buf[0] << 8) | buf[1]) >> 7) * 0.5;printf("temp = %.1f\n", temp);sleep(3);}close(fd);return 0;
}

编译命令

arm-linux-gnueabihf-gcc lm75_app1.c -o lm75_app1

⚠️ 常见错误

  • 总线编号错误(如设备接在 i2c-1,却打开 i2c-0)
  • 未先写寄存器地址(直接 read 会读到不确定位置的数据)
  • 硬件焊接错误(如 LM75 引脚反接)

调试工具

i2cdetect -l          # 列出所有 I²C 总线
i2cdetect -y 1        # 扫描 i2c-1 上的设备
dmesg                 # 查看内核日志

四、AT24C08 EEPROM 驱动开发要点

关键通信逻辑

  1. 地址长度

    • AT24C08 有 1024 字节(1K)空间,需 10 位地址
    • 地址 ≤ 255(0xFF):发送 1 字节地址
    • 地址 ≥ 256(如 0x100):必须发送 2 字节地址(高8位 + 低8位)
  2. 写操作流程

    • 发送设备地址 + 写标志
    • 发送内存地址(1 或 2 字节)
    • 发送数据(注意页写限制,通常一页 8/16 字节)
    • 等待 5ms(EEPROM 内部写周期)
  3. 读操作流程(随机读)

    • 先写:设备地址 + 写 → 内存地址
    • 再读:设备地址 + 读 → 读取数据

调试建议

  • 先在应用层验证
    vim i2c1_at24c08.c
    arm-linux-gnueabihf-gcc i2c1_at24c08.c -o at24c08_app
    
  • 闭环测试:写入 0x55 → 读回 → 比对
  • 加入延时:写后 msleep(5)
  • 检查地址字节数:高地址必须发 2 字节

经验总结
“逻辑都没通,你怎么写驱动?写多少条都没用。”
—— 先通逻辑,再写驱动;先验证通信,再封装功能


五、常见问题与排查

问题可能原因解决方法
probe 未触发compatible 不匹配DTS 中写 "ti,lm75",驱动中匹配表写 "lm75"(或完整字符串)
读取数据为 0地址未写、总线错误、硬件问题i2cdetect 扫描;检查焊接;示波器抓波形
模块加载失败已加载、符号冲突rmmod 后重试;必要时重启
第二次通信失败未正确处理 Repeated START确保读操作前完整执行“写地址”流程
写入无效未等待 EEPROM 写完成写后加 msleep(5)

六、工程理念与最佳实践

1. 分层思想

  • 总线驱动(adapter):SoC 厂商提供,无需重写
  • 核心层(i2c-core):内核提供统一 API(如 i2c_transfer()
  • 设备驱动(client):开发者只需实现设备特定逻辑

2. 驱动封装价值

对比项应用层直接操作内核驱动封装
硬件知识要求
复用性
抽象程度
维护成本

🌟 核心理念:驱动的目标是 让不懂硬件的人也能正确使用设备

3. 内核开发注意事项

  • 避免浮点数:内核中不应使用 float(无 FPU 或性能差)
  • 推荐返回原始整型数据(如 16 位有符号整数),由用户空间解析
  • 务必检查 i2c_transfer() 返回值(应等于消息数量)

七、总结

  • I²C 驱动 = 总线驱动 + 设备驱动
  • 开发流程:设备树声明 → 应用层验证 → 编写驱动 → 测试
  • 关键匹配:DTS 中 compatible 必须与驱动 id_table 一致
  • 数据读取:先写寄存器地址,再读数据(LM75);先写内存地址,再读/写(AT24C08)
  • 调试原则:先通逻辑,再写驱动;硬件问题优先排查

“不是重复造轮子,而是把轮子装到车上。”
—— 内核提供 I²C 通信“轮子”,开发者只需将设备逻辑“装上车”

http://www.dtcms.com/a/512469.html

相关文章:

  • 【AI增强质量管理体系结构】AI+自动化测试引擎 与Coze
  • 音频共享耳机专利拆解:碰击惯性数据监测与阈值减速识别机制研究
  • 青岛专业网站设计公司网站后台程序怎么做
  • MySQL创建用户、权限分配以及添加、修改权限
  • 【循环神经网络基础】
  • 郑州网站建设与设计校园网站建设年度总结
  • 中国新冠一共死去的人数网站优化和提升网站排名怎么做
  • 太仓手机网站建设阿里云如何做网站
  • 第二篇:按键交互入门:STM32 GPIO输入与消抖处理
  • JSP九大内置对象
  • 如何选择大良网站建设网站建设插件代码大全
  • 卡码网语言基础课(Python) | 17.判断集合成员
  • 温州专业网站建设成都营销推广公司
  • 淘客做网站还是做app佛山seo网站优化
  • 组合数常见的四种计算方法
  • 美容医疗服务小程序:功能集成,解锁一站式变美新体验
  • 网站建设的展望 视频搭建公司内部网站
  • Mac 从零开始配置 VS Code + Claude/Codex AI 协同开发环境教程
  • 鸿蒙flutter 老项目到新项目的遇到迁移坑点
  • 网站开发z亿玛酷1专注响应式官网设计
  • SD comfy:教程3 - 音频生成
  • 百度网盘登录福建键seo排名
  • Xshell效率实战系列一:内置Xftp快速启动——从1分钟到10秒的传输革命
  • 公路建设网站自己编写网站
  • EN 13986:2004+A1:2015 人造板检测
  • 酒店网站html模板长沙微信网站建设
  • 天河建设网站技术网站备案号密码
  • 步进电机(200 smart DM542)调试文档
  • ubuntu22 docker安装ros1 noetic
  • 惠州网站制作定制和平天津网站建设