一、驱动开发核心原则
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 |
设备树中用于驱动匹配的字符串,必须与驱动代码中完全一致 |