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 设备地址动态化(禁止硬编码)

c 复制代码
// ❌ 错误写法:硬编码地址,驱动无法适配其他地址的同型号设备
msg.addr = 0x48;

// ✅ 正确写法:从 client 结构体动态获取,由设备树注入
msg.addr = pclient->addr;

二、I²C 驱动:LM75 温度传感器

2.1 驱动框架结构

复制代码
lm75_init()
    → i2c_add_driver(&lm75_driver)        // 注册 i2c 驱动

lm75_driver(i2c_driver 结构体)
    → .probe    = probe                   // 设备匹配成功时调用
    → .remove   = remove                  // 设备移除时调用
    → .driver.of_match_table              // 与设备树 compatible 匹配
    → .id_table                           // 与设备名匹配

probe()
    → misc_register(&misc)                // 注册字符设备
    → pclient = client                    // 保存 client 指针(关键!)

2.2 每次新建 I²C 驱动必须改动的地方

① 设备名称(改 3 处,必须一致)
c 复制代码
// 第1处:宏定义
#define DEV_NAME "lm75"

// 第2处:i2c_driver 的 driver.name(驱动内部名,建议与设备名一致)
.driver = {
    .name = DEV_NAME,        // ← 改这里
    ...
}

// 第3处:i2c_device_id 表中的 name
static const struct i2c_device_id lm75_id_table[] = {
    [0] = {.name = "ti,lm75"}    // ← 改这里,必须与设备树 compatible 一致
};

⚠️ name 字段为空是内核 Oops 崩溃的常见原因:内核注册驱动时会用 name 与设备树 compatible 做字符串比较,name 为 NULL 则传入空指针,触发 string_compare 崩溃。

② 设备树 compatible(改 2 处,必须完全一致)
c 复制代码
// of_device_id 匹配表
static const struct of_device_id match_table[] = {
    [0] = {.compatible = "ti,lm75"}    // ← 改这里
};

// 同时设备树 .dts 中对应节点也要写:
// compatible = "ti,lm75";
③ 全局 client 指针变量名(改成对应设备名)
c 复制代码
static struct i2c_client * pclient;    // ← 改变量名,如换传感器改为 padc_client 等
④ probe / remove / init / exit 中的打印和函数名

根据实际设备修改 printk 中的设备名称字符串,便于调试时区分日志来源。


2.3 LM75 的 read 函数详解

LM75 读取温度:直接读 2 字节,无需先写寄存器地址

c 复制代码
static ssize_t read(struct file * file, char __user * buf, size_t len, loff_t * offset)
{
    int ret = 0;
    unsigned char data[2] = {0};          // ① 准备2字节接收缓冲区

    struct i2c_msg msg =                  // ② 构造 i2c_msg
    {
        .addr  = pclient->addr,           //    从机地址(由设备树注入,动态获取)
        .flags = I2C_M_RD,                //    标志:读操作
        .len   = 2,                       //    读取2字节(温度寄存器高字节+低字节)
        .buf   = data                     //    数据存入 data 缓冲区
    };

    // ③ 执行 I²C 传输(1条消息)
    ret = pclient->adapter->algo->master_xfer(pclient->adapter, &msg, 1);
    if(ret < 0)
        return ret;

    // ④ 将数据从内核空间拷贝到用户空间
    ret = copy_to_user(buf, data, sizeof(data));
    printk("lm75  read...\n");
    return ret;
}

LM75 读取流程图:

复制代码
应用层 read()
    ↓
构造 i2c_msg(flags=I2C_M_RD, len=2)
    ↓
master_xfer(adapter, &msg, 1)
    ↓
I²C 总线:START → 从机地址+R → ACK → 读高字节 → ACK → 读低字节 → NAK → STOP
    ↓
data[0] = 高字节,data[1] = 低字节
    ↓
copy_to_user 传给应用层

应用层解析温度(驱动返回原始字节,应用层自行处理):

c 复制代码
// 应用层读取示例
unsigned char raw[2];
read(fd, raw, 2);
// LM75 温度寄存器:高9位有效,bit15为符号位,精度0.5℃
short temp = (raw[0] << 8 | raw[1]) >> 7;
printf("温度 = %d.%d ℃\n", temp / 2, (temp & 1) * 5);

💡 优化建议(课程推荐):可在驱动层完成解析,向应用层直接返回摄氏度数值,减少应用层的解析负担,符合"驱动封装硬件细节"原则。


三、SPI 驱动:ADXL345 三轴加速度计

3.1 驱动框架结构

复制代码
adxl345_driver_init()
    → spi_register_driver(&adxl345_driver)  // 注册 spi 驱动

adxl345_driver(spi_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 处)
c 复制代码
// 第1处:宏定义
#define DEV_NAME "adxl345"               // ← 改这里

// 第2处:spi_driver 的 driver.name
.driver = {
    .name = DEV_NAME,                    // ← 改这里
    ...
}

// 第3处:spi_device_id 表
static const struct spi_device_id adxl345_table[] = {
    {.name = "adxl345"},                 // ← 改这里,与设备树 compatible 一致
    {}
};
② 设备树 compatible(改 2 处)
c 复制代码
// of_device_id 匹配表
static const struct of_device_id of_adxl345_table[] = {
    {.compatible = "adxl345"},           // ← 改这里
    {}
};

// 设备树 .dts 中对应节点:
// compatible = "adxl345";
// reg = <0>;                // 片选号 CS0
// spi-max-frequency = <4000000>;  // 最大时钟4MHz
③ 全局 spi_device 指针(改成对应设备名)
c 复制代码
static struct spi_device * adxl345_device;    // ← 改变量名
④ SPI 模式(根据数据手册修改)
c 复制代码
spi->mode = SPI_MODE_3;    // ← ADXL345 使用 MODE_3,其他设备可能是 MODE_0/1/2
ret = spi_setup(spi);       // 必须调用 spi_setup 才能使配置生效
⑤ 寄存器初始化(adxl345_init)
c 复制代码
static void adxl345_init(void)
{
    adxl345_write(0x2E, 0x08);   // ← 根据目标芯片数据手册修改初始化寄存器
    adxl345_write(0x31, 0x0B);
    adxl345_write(0x2C, 0x08);
    adxl345_write(0x2D, 0x0B);
}

3.3 SPI 底层读写函数详解

adxl345_read(读单个寄存器)
c 复制代码
static unsigned char adxl345_read(unsigned char reg_addr)
{
    unsigned char data = 0;
    int ret = 0;

    // ① ADXL345 读操作:寄存器地址最高位置1(0x80)表示读操作
    unsigned char tx_data = (reg_addr | 0x80);

    struct spi_message msg;
    struct spi_transfer transfer[2] =    // ② 两段传输:先写地址,再读数据
    {
        [0] = {
            .tx_buf = &tx_data,          // 第1段:发送寄存器地址(带读标志位)
            .len    = 1
        },
        [1] = {
            .rx_buf = &data,             // 第2段:接收1字节数据
            .len    = 1
        }
    };

    spi_message_init(&msg);              // ③ 初始化 message
    spi_message_add_tail(&transfer[0], &msg);  // ④ 将 transfer 加入 message 链表
    spi_message_add_tail(&transfer[1], &msg);
    ret = spi_sync(adxl345_device, &msg);      // ⑤ 同步执行传输
    if(ret < 0)
        return ret;

    return data;
}
adxl345_write(写单个寄存器)
c 复制代码
static int adxl345_write(unsigned char reg_addr, unsigned char data)
{
    int ret = 0;
    unsigned char tx_data[2] = {0};
    struct spi_message msg;
    struct spi_transfer transfer =       // ① 写操作只需一段传输(地址+数据合并发送)
    {
        .tx_buf = tx_data,
        .len    = 2
    };
    tx_data[0] = reg_addr;               // ② 第1字节:寄存器地址(写操作,最高位不置1)
    tx_data[1] = data;                   // ③ 第2字节:要写入的数据

    spi_message_init(&msg);
    spi_message_add_tail(&transfer, &msg);
    ret = spi_sync(adxl345_device, &msg);
    if(ret < 0)
        return ret;

    return ret;
}

读 vs 写对比:

读操作(read) 写操作(write)
transfer 数量 2个(发地址 + 收数据) 1个(地址+数据合并发送)
地址处理 `reg_addr 0x80`(置最高位)
tx/rx buf transfer[0] 用 tx_buf,transfer[1] 用 rx_buf 只用 tx_buf

3.4 ADXL345 的 read 函数详解

ADXL345 读取三轴加速度:依次读6个寄存器(X高低、Y高低、Z高低)

c 复制代码
static ssize_t read(struct file * file, char __user * buf, size_t size, loff_t * loff)
{
    int ret = 0;
    short data[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字节)
    ret = copy_to_user(buf, data, sizeof(data));

    return 0;
}

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 设备 ID(probe 中读取验证,ADXL345 固定返回 0xE5)
0x32 X 轴数据低字节
0x33 X 轴数据高字节
0x34 Y 轴数据低字节
0x35 Y 轴数据高字节
0x36 Z 轴数据低字节
0x37 Z 轴数据高字节
0x2C BW_RATE(数据速率和功耗控制)
0x2D POWER_CTL(功耗控制,0x08=测量模式)
0x2E INT_ENABLE(中断使能)
0x31 DATA_FORMAT(数据格式,0x0B=全分辨率±16g)

四、I²C 与 SPI 驱动对比

4.1 框架结构对比

对比项 I²C(LM75) SPI(ADXL345)
驱动结构体 struct i2c_driver struct spi_driver
probe 参数 struct i2c_client *client struct spi_device *spi
全局设备指针 struct i2c_client *pclient struct spi_device *adxl345_device
注册函数 i2c_add_driver() spi_register_driver()
注销函数 i2c_del_driver() spi_unregister_driver()
底层传输 master_xfer() / i2c_transfer() spi_sync()
消息结构体 struct i2c_msg struct spi_message + struct spi_transfer

4.2 read 函数写法对比

I²C(LM75):一次构造一个 msg,直接读

c 复制代码
// 构造1条 msg → 执行1次 master_xfer
struct i2c_msg msg = {
    .addr  = pclient->addr,
    .flags = I2C_M_RD,
    .len   = 2,
    .buf   = data
};
ret = pclient->adapter->algo->master_xfer(pclient->adapter, &msg, 1);

SPI(ADXL345):构造 message + 多个 transfer,通过封装函数分步读

c 复制代码
// 不在 read() 里直接构造 message,而是调用封装好的 adxl345_read()
// adxl345_read() 内部:构造2个 transfer → 加入 message → spi_sync
data[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 设备树节点:

dts 复制代码
&i2c1 {
    lm75@48 {
        compatible = "ti,lm75";
        reg = <0x48>;          /* I²C 从机地址 */
    };
};

SPI 设备树节点:

dts 复制代码
&spi3 {
    adxl345@0 {
        compatible = "adxl345";
        reg = <0>;                       /* 片选号 CS0 */
        spi-max-frequency = <4000000>;   /* 最大时钟 4MHz */
    };
};

关键区别 :I²C 的 reg 是从机地址(如 0x48);SPI 的 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 有效)
    └── ...(可以有多个 transfer,CS 全程保持有效)

使用步骤(固定模板):

c 复制代码
// Step 1: 定义 transfer 数组
struct spi_transfer transfer[N] = { ... };

// Step 2: 初始化 message
spi_message_init(&msg);

// Step 3: 逐一将 transfer 加入 message 链表
spi_message_add_tail(&transfer[0], &msg);
spi_message_add_tail(&transfer[1], &msg);

// Step 4: 同步执行
ret = spi_sync(spi_device, &msg);

5.2 SPI 四种模式

模式 CPOL CPHA 说明
MODE_0 0 0 空闲低,第1个边沿采样
MODE_1 0 1 空闲低,第2个边沿采样
MODE_2 1 0 空闲高,第1个边沿采样
MODE_3 1 1 空闲高,第2个边沿采样(ADXL345使用)

六、调试规范

6.1 printk 等级规范

c 复制代码
printk(KERN_ERR  "adxl345: 错误信息\n");    // 必须打印的错误
printk(KERN_INFO "adxl345: 设备加载成功\n"); // 一般信息
printk(KERN_DEBUG"adxl345: 调试输出\n");     // 调试信息(默认不显示)
bash 复制代码
# 提高日志级别,查看所有输出(调试时使用)
echo 8 > /proc/sys/kernel/printk

# 生产环境只保留 ERR / WARNING,避免日志污染
echo 4 > /proc/sys/kernel/printk

6.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 驱动验证步骤

bash 复制代码
# 1. 加载驱动模块
insmod adxl345.ko
insmod lm75.ko

# 2. 查看是否 probe 成功
dmesg | tail -20

# 3. 查看设备节点是否生成
ls /dev/adxl345
ls /dev/lm75

# 4. SPI 设备:确认在 sysfs 中可见
ls /sys/bus/spi/devices/

# 5. I²C 设备:确认在 sysfs 中可见
ls /sys/bus/i2c/devices/

# 6. 读取数据测试
cat /dev/adxl345
cat /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_client I²C 设备描述符,包含从机地址、adapter 指针等,在 probe 中获取
i2c_msg 一次 I²C 读或写操作的描述,包含地址、方向、长度、缓冲区
I2C_M_RD i2c_msg.flags 置此位表示读操作,不置位表示写操作
master_xfer I²C 总线底层传输函数,通过 adapter→algo→master_xfer 调用
spi_device SPI 设备描述符,在 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 设备树中用于驱动匹配的字符串,必须与驱动代码中完全一致
相关推荐
CinzWS21 小时前
A53电源管理(下):DVFS与热管理的硬件实现——ARM芯片的“冷静艺术“
arm开发·嵌入式·芯片验证·原型验证·a53
誰能久伴不乏1 天前
剥开协议的伪装:用 Wireshark 显微镜级拆解 TCP 握手与挥手
arm开发·tcp/ip·wireshark
somi71 天前
ARM-驱动-10自定义通信协议
linux·arm开发·自用
疏星浅月2 天前
虚拟内存三大核心作用详解
linux·c语言·arm开发·嵌入式硬件
somi72 天前
ARM-驱动-09-LCD FrameBuffer
arm开发·驱动开发·算法·自用
每天进步一点点️2 天前
透视 SOC 内部:APU Cluster 如何驱动 DB15 的 CAN/ETH 信号输出
arm开发·soc·芯片
xiaoyaohou112 天前
032、部署优化(三):OpenVINO与ARM平台(NCNN、TNN)部署
arm开发·人工智能·openvino
路溪非溪2 天前
抓取手机的蓝牙HCI日志并分析
linux·arm开发·驱动开发·智能手机
somi73 天前
ARM-05-Platform + DTS + GPIO子系统 + 中断 + 等待队列 + 错误处理
linux·运维·arm开发