ARM-驱动-08-LM75(I2C)和 ADXL345(SPI)

张开发
2026/4/16 21:28:20 15 分钟阅读

分享文章

ARM-驱动-08-LM75(I2C)和 ADXL345(SPI)
一、驱动开发核心原则1.1 封装与分层最重要的设计思想应用层 open() / read() / write() ↓ ↑ 只做字符设备框架相关逻辑 驱动层 封装函数adxl345_read() / adxl345_write() lm75 通过 i2c_transfer 读取 ↓ ↑ 底层通信细节全部封装在这里 总线层 spi_sync() / i2c_transfer()⚠️open / read / write 函数中禁止直接调用寄存器操作或收发指令底层通信必须封装为独立函数。这是内核分层抽象的核心要求。1.2 设备地址动态化禁止硬编码// ❌ 错误写法硬编码地址驱动无法适配其他地址的同型号设备msg.addr0x48;// ✅ 正确写法从 client 结构体动态获取由设备树注入msg.addrpclient-addr;二、I²C 驱动LM75 温度传感器2.1 驱动框架结构lm75_init() → i2c_add_driver(lm75_driver) // 注册 i2c 驱动 lm75_driveri2c_driver 结构体 → .probe probe // 设备匹配成功时调用 → .remove remove // 设备移除时调用 → .driver.of_match_table // 与设备树 compatible 匹配 → .id_table // 与设备名匹配 probe() → misc_register(misc) // 注册字符设备 → pclient client // 保存 client 指针关键2.2 每次新建 I²C 驱动必须改动的地方① 设备名称改 3 处必须一致// 第1处宏定义#defineDEV_NAMElm75// 第2处i2c_driver 的 driver.name驱动内部名建议与设备名一致.driver{.nameDEV_NAME,// ← 改这里...}// 第3处i2c_device_id 表中的 namestaticconststructi2c_device_idlm75_id_table[]{[0]{.nameti,lm75}// ← 改这里必须与设备树 compatible 一致};⚠️name 字段为空是内核 Oops 崩溃的常见原因内核注册驱动时会用 name 与设备树 compatible 做字符串比较name 为 NULL 则传入空指针触发 string_compare 崩溃。② 设备树 compatible改 2 处必须完全一致// of_device_id 匹配表staticconststructof_device_idmatch_table[]{[0]{.compatibleti,lm75}// ← 改这里};// 同时设备树 .dts 中对应节点也要写// compatible ti,lm75;③ 全局 client 指针变量名改成对应设备名staticstructi2c_client*pclient;// ← 改变量名如换传感器改为 padc_client 等④ probe / remove / init / exit 中的打印和函数名根据实际设备修改 printk 中的设备名称字符串便于调试时区分日志来源。2.3 LM75 的 read 函数详解LM75 读取温度直接读 2 字节无需先写寄存器地址staticssize_tread(structfile*file,char__user*buf,size_tlen,loff_t*offset){intret0;unsignedchardata[2]{0};// ① 准备2字节接收缓冲区structi2c_msgmsg// ② 构造 i2c_msg{.addrpclient-addr,// 从机地址由设备树注入动态获取.flagsI2C_M_RD,// 标志读操作.len2,// 读取2字节温度寄存器高字节低字节.bufdata// 数据存入 data 缓冲区};// ③ 执行 I²C 传输1条消息retpclient-adapter-algo-master_xfer(pclient-adapter,msg,1);if(ret0)returnret;// ④ 将数据从内核空间拷贝到用户空间retcopy_to_user(buf,data,sizeof(data));printk(lm75 read...\n);returnret;}LM75 读取流程图应用层 read() ↓ 构造 i2c_msgflagsI2C_M_RD, len2 ↓ master_xfer(adapter, msg, 1) ↓ I²C 总线START → 从机地址R → ACK → 读高字节 → ACK → 读低字节 → NAK → STOP ↓ data[0] 高字节data[1] 低字节 ↓ copy_to_user 传给应用层应用层解析温度驱动返回原始字节应用层自行处理// 应用层读取示例unsignedcharraw[2];read(fd,raw,2);// LM75 温度寄存器高9位有效bit15为符号位精度0.5℃shorttemp(raw[0]8|raw[1])7;printf(温度 %d.%d ℃\n,temp/2,(temp1)*5);优化建议课程推荐可在驱动层完成解析向应用层直接返回摄氏度数值减少应用层的解析负担符合驱动封装硬件细节原则。三、SPI 驱动ADXL345 三轴加速度计3.1 驱动框架结构adxl345_driver_init() → spi_register_driver(adxl345_driver) // 注册 spi 驱动 adxl345_driverspi_driver 结构体 → .probe probe → .remove remove → .driver.of_match_table → .id_table probe() → misc_register(misc_dev) // 注册字符设备 → spi-mode SPI_MODE_3 // 设置 SPI 模式 → spi_setup(spi) // 应用 SPI 配置 → adxl345_device spi // 保存 spi 指针关键 → adxl345_read(0) // 读设备 ID 验证3.2 每次新建 SPI 驱动必须改动的地方① 设备名称改 3 处// 第1处宏定义#defineDEV_NAMEadxl345// ← 改这里// 第2处spi_driver 的 driver.name.driver{.nameDEV_NAME,// ← 改这里...}// 第3处spi_device_id 表staticconststructspi_device_idadxl345_table[]{{.nameadxl345},// ← 改这里与设备树 compatible 一致{}};② 设备树 compatible改 2 处// of_device_id 匹配表staticconststructof_device_idof_adxl345_table[]{{.compatibleadxl345},// ← 改这里{}};// 设备树 .dts 中对应节点// compatible adxl345;// reg 0; // 片选号 CS0// spi-max-frequency 4000000; // 最大时钟4MHz③ 全局 spi_device 指针改成对应设备名staticstructspi_device*adxl345_device;// ← 改变量名④ SPI 模式根据数据手册修改spi-modeSPI_MODE_3;// ← ADXL345 使用 MODE_3其他设备可能是 MODE_0/1/2retspi_setup(spi);// 必须调用 spi_setup 才能使配置生效⑤ 寄存器初始化adxl345_initstaticvoidadxl345_init(void){adxl345_write(0x2E,0x08);// ← 根据目标芯片数据手册修改初始化寄存器adxl345_write(0x31,0x0B);adxl345_write(0x2C,0x08);adxl345_write(0x2D,0x0B);}3.3 SPI 底层读写函数详解adxl345_read读单个寄存器staticunsignedcharadxl345_read(unsignedcharreg_addr){unsignedchardata0;intret0;// ① ADXL345 读操作寄存器地址最高位置10x80表示读操作unsignedchartx_data(reg_addr|0x80);structspi_messagemsg;structspi_transfertransfer[2]// ② 两段传输先写地址再读数据{[0]{.tx_buftx_data,// 第1段发送寄存器地址带读标志位.len1},[1]{.rx_bufdata,// 第2段接收1字节数据.len1}};spi_message_init(msg);// ③ 初始化 messagespi_message_add_tail(transfer[0],msg);// ④ 将 transfer 加入 message 链表spi_message_add_tail(transfer[1],msg);retspi_sync(adxl345_device,msg);// ⑤ 同步执行传输if(ret0)returnret;returndata;}adxl345_write写单个寄存器staticintadxl345_write(unsignedcharreg_addr,unsignedchardata){intret0;unsignedchartx_data[2]{0};structspi_messagemsg;structspi_transfertransfer// ① 写操作只需一段传输地址数据合并发送{.tx_buftx_data,.len2};tx_data[0]reg_addr;// ② 第1字节寄存器地址写操作最高位不置1tx_data[1]data;// ③ 第2字节要写入的数据spi_message_init(msg);spi_message_add_tail(transfer,msg);retspi_sync(adxl345_device,msg);if(ret0)returnret;returnret;}读 vs 写对比读操作read写操作writetransfer 数量2个发地址 收数据1个地址数据合并发送地址处理reg_addr | 0x80置最高位直接用reg_addrtx/rx buftransfer[0] 用 tx_buftransfer[1] 用 rx_buf只用 tx_buf3.4 ADXL345 的 read 函数详解ADXL345 读取三轴加速度依次读6个寄存器X高低、Y高低、Z高低staticssize_tread(structfile*file,char__user*buf,size_tsize,loff_t*loff){intret0;shortdata[3];// ① 3轴每轴2字节用 short 存储有符号16位// ② 读 X 轴0x32低字节| 0x33高字节合成16位有符号数data[0]adxl345_read(0x32)|(adxl345_read(0x33)8);// ③ 读 Y 轴0x34低字节| 0x35高字节data[1]adxl345_read(0x34)|(adxl345_read(0x35)8);// ④ 读 Z 轴0x36低字节| 0x37高字节data[2]adxl345_read(0x36)|(adxl345_read(0x37)8);// ⑤ 拷贝到用户空间6字节3轴 × 2字节retcopy_to_user(buf,data,sizeof(data));return0;}ADXL345 读取流程图应用层 read() ↓ 调用 adxl345_read(0x32) 调用 adxl345_read(0x33) ↓ ↓ SPI: 发送(0x32|0x80) → 接收1字节 SPI: 发送(0x33|0x80) → 接收1字节 ↓ ↓ 低字节 高字节 └──────────── | 合成 ──────────────┘ ↓ data[0] 低字节 | (高字节 8) ← X轴原始值有符号 data[1] Y轴原始值 data[2] Z轴原始值 ↓ copy_to_user(buf, data, 6字节) ↓ 应用层获得3个 short 值寄存器地址对应关系寄存器含义0x00设备 IDprobe 中读取验证ADXL345 固定返回 0xE50x32X 轴数据低字节0x33X 轴数据高字节0x34Y 轴数据低字节0x35Y 轴数据高字节0x36Z 轴数据低字节0x37Z 轴数据高字节0x2CBW_RATE数据速率和功耗控制0x2DPOWER_CTL功耗控制0x08测量模式0x2EINT_ENABLE中断使能0x31DATA_FORMAT数据格式0x0B全分辨率±16g四、I²C 与 SPI 驱动对比4.1 框架结构对比对比项I²CLM75SPIADXL345驱动结构体struct i2c_driverstruct spi_driverprobe 参数struct i2c_client *clientstruct spi_device *spi全局设备指针struct i2c_client *pclientstruct spi_device *adxl345_device注册函数i2c_add_driver()spi_register_driver()注销函数i2c_del_driver()spi_unregister_driver()底层传输master_xfer()/i2c_transfer()spi_sync()消息结构体struct i2c_msgstruct spi_messagestruct spi_transfer4.2 read 函数写法对比I²CLM75一次构造一个 msg直接读// 构造1条 msg → 执行1次 master_xferstructi2c_msgmsg{.addrpclient-addr,.flagsI2C_M_RD,.len2,.bufdata};retpclient-adapter-algo-master_xfer(pclient-adapter,msg,1);SPIADXL345构造 message 多个 transfer通过封装函数分步读// 不在 read() 里直接构造 message而是调用封装好的 adxl345_read()// adxl345_read() 内部构造2个 transfer → 加入 message → spi_syncdata[0]adxl345_read(0x32)|(adxl345_read(0x33)8);data[1]adxl345_read(0x34)|(adxl345_read(0x35)8);data[2]adxl345_read(0x36)|(adxl345_read(0x37)8);4.3 设备树配置对比I²C 设备树节点i2c1 { lm7548 { compatible ti,lm75; reg 0x48; /* I²C 从机地址 */ }; };SPI 设备树节点spi3 { adxl3450 { compatible adxl345; reg 0; /* 片选号 CS0 */ spi-max-frequency 4000000; /* 最大时钟 4MHz */ }; };关键区别I²C 的reg是从机地址如0x48SPI 的reg是片选号如0表示 CS0二者含义完全不同。五、SPI 通信机制详解5.1 spi_message 与 spi_transfer 的关系spi_message一次完整的 SPI 事务 ├── spi_transfer[0] 发送阶段tx_buf 有效rx_buf 为空 ├── spi_transfer[1] 接收阶段tx_buf 为空rx_buf 有效 └── ...可以有多个 transferCS 全程保持有效使用步骤固定模板// Step 1: 定义 transfer 数组structspi_transfertransfer[N]{...};// Step 2: 初始化 messagespi_message_init(msg);// Step 3: 逐一将 transfer 加入 message 链表spi_message_add_tail(transfer[0],msg);spi_message_add_tail(transfer[1],msg);// Step 4: 同步执行retspi_sync(spi_device,msg);5.2 SPI 四种模式模式CPOLCPHA说明MODE_000空闲低第1个边沿采样MODE_101空闲低第2个边沿采样MODE_210空闲高第1个边沿采样MODE_311空闲高第2个边沿采样ADXL345使用六、调试规范6.1 printk 等级规范printk(KERN_ERRadxl345: 错误信息\n);// 必须打印的错误printk(KERN_INFOadxl345: 设备加载成功\n);// 一般信息printk(KERN_DEBUGadxl345: 调试输出\n);// 调试信息默认不显示# 提高日志级别查看所有输出调试时使用echo8/proc/sys/kernel/printk# 生产环境只保留 ERR / WARNING避免日志污染echo4/proc/sys/kernel/printk6.2 内核崩溃Oops定位方法1. 查看崩溃日志中的 PC 地址和 call trace 2. 用 addr2line 或 objdump 定位崩溃在哪一行代码 3. 常见原因 - i2c_driver.id_table 中 .name 为空 → string_compare 收到空指针崩溃 - 全局指针pclient / adxl345_device未在 probe 中赋值就被 read 使用 - copy_to_user / copy_from_user 传入内核地址6.3 驱动验证步骤# 1. 加载驱动模块insmod adxl345.ko insmod lm75.ko# 2. 查看是否 probe 成功dmesg|tail-20# 3. 查看设备节点是否生成ls/dev/adxl345ls/dev/lm75# 4. SPI 设备确认在 sysfs 中可见ls/sys/bus/spi/devices/# 5. I²C 设备确认在 sysfs 中可见ls/sys/bus/i2c/devices/# 6. 读取数据测试cat/dev/adxl345cat/dev/lm75七、新建驱动完整改动清单新建 I²C 驱动以 LM75 为模板改动位置改什么注意事项#define DEV_NAME改为新设备名影响/dev/下节点名称i2c_device_id表中.name改为与设备树 compatible 一致的字符串不匹配则 probe 不触发of_device_id表中.compatible改为与设备树完全一致大小写敏感struct i2c_client * pclient改为新变量名语义清晰read()中 i2c_msg 的.len根据读取字节数修改LM75 读2字节probe()中的 printk改为新设备名方便调试时区分adxl345_init()寄存器表根据数据手册改不同芯片寄存器地址不同新建 SPI 驱动以 ADXL345 为模板改动位置改什么注意事项#define DEV_NAME改为新设备名影响/dev/下节点名称spi_device_id表中.name改为与设备树 compatible 一致不匹配则 probe 不触发of_device_id表中.compatible改为与设备树完全一致大小写敏感struct spi_device * adxl345_device改为新变量名语义清晰spi-mode根据数据手册选 MODE_0/1/2/3模式不对则数据全错adxl345_read()中地址位操作ADXL345 读操作置 bit7其他芯片可能不同查芯片手册adxl345_init()寄存器表根据数据手册改必改read()中读取的寄存器地址和轴数根据新芯片寄存器图修改必改八、关键概念速查概念说明i2c_clientI²C 设备描述符包含从机地址、adapter 指针等在 probe 中获取i2c_msg一次 I²C 读或写操作的描述包含地址、方向、长度、缓冲区I2C_M_RDi2c_msg.flags 置此位表示读操作不置位表示写操作master_xferI²C 总线底层传输函数通过 adapter→algo→master_xfer 调用spi_deviceSPI 设备描述符在 probe 中获取用于所有 SPI 操作spi_transfer一段 SPI 传输指定发送/接收缓冲区和长度spi_message一次完整 SPI 事务包含一个或多个 spi_transfer 的链表spi_sync同步执行 SPI message阻塞直到传输完成spi_setup应用 SPI 设备配置模式、时钟等probe 中必须调用misc_register注册杂项字符设备自动分配次设备号在/dev/下创建节点copy_to_user将内核空间数据拷贝到用户空间read 函数中必须使用probe设备与驱动匹配成功时由内核自动调用相当于驱动的初始化入口compatible设备树中用于驱动匹配的字符串必须与驱动代码中完全一致

更多文章