前言
在前九篇文章中,我们由浅入深地掌握了字符设备驱动框架、GPIO/PWM/I2C 子系统、中断处理、底半部、输入子系统和内核定时器等核心技术。本系列收官之作将把这些知识点融会贯通,设计一个综合性实例:利用 I2C 温度传感器(LM75)实时监测环境温度,并动态控制一个 LED 的状态------超过阈值时让 LED 闪烁报警,正常时熄灭,同时提供用户空间接口用于查询温度、调整阈值和闪烁周期。
此外,文章后半部分将梳理一条清晰的 Linux 驱动学习路径,并指明后续进阶方向,帮助你建立完整的知识地图。
读完本文你将收获:
- 一个真实的多外设协同驱动设计与实现
- 将前九篇所学知识串联运用的经验
- 一份从入门到精通的 Linux 驱动学习路线图

一、综合实例设计
1.1 硬件平台
- 主控:i.MX6ULL(韦东山课程配套板)
- 温度传感器:LM75B,I2C 地址 0x48,挂接在 I2C1 上
- LED:GPIO5_IO03,低电平有效(与第四篇文章相同)
1.2 功能描述
驱动加载后生成设备节点 /dev/smart_led,提供以下功能:
| 操作 | 说明 |
|---|---|
cat /dev/smart_led |
读取当前温度(毫摄氏度格式)、LED 状态、阈值和闪烁周期 |
echo "th=30000" > /dev/smart_led |
设置温度阈值为 30.0℃(毫摄氏度) |
echo "period=500" > /dev/smart_led |
设置闪烁周期为 500ms |
echo "mode=0/1/2" > /dev/smart_led |
设置报警模式:0-关闭LED, 1-常亮(超阈值亮), 2-闪烁 |
驱动内部使用内核定时器 每 200ms 读取一次 LM75 温度,并根据当前模式与阈值决定 LED 状态。通过自旋锁保护共享数据。
LM75 温度寄存器为 16 位,高 9 位为温度值(补码),每 LSB 代表 0.125℃。本文直接使用 I2C 读操作获取原始数据并转换。
二、设备树修改
在板级设备树中定义 LED 节点和 LM75 传感器节点,并通过 temp-sensor 属性将它们关联。
dts
&i2c1 {
lm75: temperature-sensor@48 {
compatible = "yourname,lm75"; /* 自定义compatible,避免与内核自带驱动冲突 */
reg = <0x48>;
status = "okay";
};
};
/ {
smart_led {
compatible = "yourname,smart-led";
led-gpios = <&gpio5 3 GPIO_ACTIVE_LOW>;
temp-sensor = <&lm75>; /* 引用LM75节点 */
status = "okay";
};
};
说明 :自定义 compatible 可以防止 LM75 被内核自带的 lm75 驱动占用(避免 i2cdetect 显示 UU)。驱动中通过 of_parse_phandle 找到 LM75 节点,再获取其所在 I2C 适配器和地址,动态创建 i2c_client。
三、驱动代码实现
创建文件 smart_led_drv.c,完整代码如下。所有关键函数均附有详细注释。
c
/*
* smart_led_drv.c
* 综合实例:I2C LM75温度传感器 + GPIO LED 智能控制
* 定时读取温度,超过阈值时根据模式控制LED:off、on、blink。
* 设备节点 /dev/smart_led 提供温度查询和参数设置。
* 作者:[你的ID]
* 适配内核:Linux 5.x
*/
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/platform_device.h>
#include <linux/gpio/consumer.h>
#include <linux/i2c.h>
#include <linux/timer.h>
#include <linux/spinlock.h>
#include <linux/jiffies.h>
#include <linux/of.h>
#include <linux/delay.h>
#include <linux/slab.h>
#define DEV_NAME "smart_led"
#define CLASS_NAME "smart_led_class"
#define TEMP_READ_INTERVAL_MS 200 /* 温度读取间隔 200ms */
#define DEFAULT_THRESHOLD 30000 /* 默认阈值 30.0℃(毫摄氏度) */
#define DEFAULT_PERIOD_MS 500 /* 默认闪烁周期 500ms */
static dev_t dev_num;
static struct cdev my_cdev;
static struct class *my_class;
static struct device *my_device;
static struct gpio_desc *led_gpio;
static struct i2c_client *lm75_client; /* LM75 I2C客户端 */
/* 工作参数,使用自旋锁保护 */
static int threshold_mc = DEFAULT_THRESHOLD; /* 阈值,毫摄氏度 */
static int period_ms = DEFAULT_PERIOD_MS; /* 闪烁周期,毫秒 */
static int mode; /* 0: LED off, 1: LED on (over threshold), 2: blink */
static int led_state; /* 当前LED逻辑电平 */
static int current_temp; /* 最新温度,毫摄氏度 */
static struct timer_list read_timer; /* 定时读取温度 */
static struct timer_list blink_timer; /* 闪烁定时器(仅在blink模式且超阈值时使用) */
static DEFINE_SPINLOCK(lock);
/* ---- LM75 温度读取 ---- */
static int lm75_read_temp(struct i2c_client *client, int *temp_mc)
{
int ret;
u8 buf[2];
int raw;
/* LM75 温度寄存器地址为 0x00,使用 I2C 读操作读取 2 字节 */
ret = i2c_master_recv(client, buf, 2);
if (ret < 0) {
pr_err("smart_led: i2c_master_recv failed, err=%d\n", ret);
return ret;
}
raw = (buf[0] << 8) | buf[1];
raw >>= 5; /* 低5位为无效位,右移5位得到11位温度数据 */
if (raw & 0x0400) /* 判断符号位(第10位),负数情况 */
raw -= 2048;
/* 每 LSB 0.125℃,转换为毫摄氏度:乘以 125 */
*temp_mc = raw * 125;
return 0;
}
/* ---- LED 基本控制 ---- */
static void update_led(int on)
{
gpiod_set_value(led_gpio, on ? 1 : 0);
}
/* ---- 闪烁定时器回调:翻转 LED ---- */
static void blink_timer_callback(struct timer_list *t)
{
unsigned long flags;
spin_lock_irqsave(&lock, flags);
led_state = !led_state;
update_led(led_state);
mod_timer(&blink_timer, jiffies + msecs_to_jiffies(period_ms));
spin_unlock_irqrestore(&lock, flags);
}
/* ---- 根据模式和当前温度决定 LED 行为 ---- */
static void led_control(void)
{
int over_threshold = (current_temp >= threshold_mc);
switch (mode) {
case 0: /* off */
del_timer(&blink_timer);
update_led(0);
break;
case 1: /* on when over threshold */
del_timer(&blink_timer);
update_led(over_threshold ? 1 : 0);
break;
case 2: /* blink when over threshold */
if (over_threshold) {
if (!timer_pending(&blink_timer))
mod_timer(&blink_timer, jiffies + msecs_to_jiffies(period_ms));
} else {
del_timer(&blink_timer);
update_led(0);
}
break;
}
}
/* ---- 定时读取温度回调 ---- */
static void read_timer_callback(struct timer_list *t)
{
int temp;
unsigned long flags;
if (lm75_read_temp(lm75_client, &temp) == 0) {
spin_lock_irqsave(&lock, flags);
current_temp = temp;
led_control();
spin_unlock_irqrestore(&lock, flags);
}
mod_timer(&read_timer, jiffies + msecs_to_jiffies(TEMP_READ_INTERVAL_MS));
}
/* ---- 文件操作 ---- */
static int smart_led_open(struct inode *inode, struct file *file)
{
pr_info("smart_led: opened\n");
return 0;
}
static int smart_led_release(struct inode *inode, struct file *file)
{
pr_info("smart_led: closed\n");
return 0;
}
static ssize_t smart_led_read(struct file *file, char __user *buf,
size_t count, loff_t *f_pos)
{
char kbuf[128];
int len;
unsigned long flags;
int temp, th, per, md;
spin_lock_irqsave(&lock, flags);
temp = current_temp;
th = threshold_mc;
per = period_ms;
md = mode;
spin_unlock_irqrestore(&lock, flags);
len = snprintf(kbuf, sizeof(kbuf),
"temp: %d.%03d C\nthreshold: %d.%03d C\nperiod: %d ms\nmode: %s\n",
temp / 1000, (temp < 0 ? -temp : temp) % 1000,
th / 1000, (th < 0 ? -th : th) % 1000,
per,
(md == 0) ? "off" : (md == 1) ? "on" : "blink");
if (*f_pos >= len)
return 0;
if (copy_to_user(buf, kbuf, len))
return -EFAULT;
*f_pos += len;
return len;
}
static int parse_command(char *kbuf, unsigned long *val, char *param)
{
char *p = strchr(kbuf, '=');
if (!p) return -EINVAL;
*p = '\0';
strncpy(param, kbuf, 16);
param[15] = '\0';
return kstrtoul(p + 1, 0, val);
}
static ssize_t smart_led_write(struct file *file, const char __user *buf,
size_t count, loff_t *f_pos)
{
char kbuf[64] = {0};
unsigned long val;
int ret;
char param[16];
unsigned long flags;
if (count > 63) count = 63;
if (copy_from_user(kbuf, buf, count))
return -EFAULT;
ret = parse_command(kbuf, &val, param);
if (ret < 0) return -EINVAL;
spin_lock_irqsave(&lock, flags);
if (strcmp(param, "th") == 0) {
threshold_mc = val;
} else if (strcmp(param, "period") == 0) {
period_ms = val;
if (mode == 2 && timer_pending(&blink_timer))
mod_timer(&blink_timer, jiffies + msecs_to_jiffies(period_ms));
} else if (strcmp(param, "mode") == 0) {
if (val <= 2) {
mode = val;
led_control();
} else {
ret = -EINVAL;
goto out;
}
} else {
ret = -EINVAL;
goto out;
}
ret = count;
out:
spin_unlock_irqrestore(&lock, flags);
return ret;
}
static struct file_operations smart_led_fops = {
.owner = THIS_MODULE,
.open = smart_led_open,
.release = smart_led_release,
.read = smart_led_read,
.write = smart_led_write,
};
/* ---- platform_driver ---- */
static int smart_led_probe(struct platform_device *pdev)
{
int ret;
struct device *dev = &pdev->dev;
struct device_node *np = dev->of_node;
struct device_node *sensor_np;
struct i2c_adapter *adapter;
u32 addr;
pr_info("smart_led: probe\n");
/* 获取 LED GPIO */
led_gpio = gpiod_get(dev, "led", GPIOD_OUT_LOW);
if (IS_ERR(led_gpio)) {
ret = PTR_ERR(led_gpio);
pr_err("smart_led: failed to get led gpio\n");
return ret;
}
/* 通过设备树引用找到 LM75 节点并创建 I2C 客户端 */
sensor_np = of_parse_phandle(np, "temp-sensor", 0);
if (!sensor_np) {
pr_err("smart_led: missing temp-sensor phandle\n");
ret = -ENODEV;
goto err_parse;
}
adapter = of_find_i2c_adapter_by_node(sensor_np->parent);
if (!adapter) {
pr_err("smart_led: cannot find i2c adapter\n");
ret = -ENODEV;
goto err_adapter;
}
if (of_property_read_u32(sensor_np, "reg", &addr)) {
pr_err("smart_led: no reg property\n");
ret = -EINVAL;
goto err_addr;
}
lm75_client = i2c_new_client_device(adapter,
&(struct i2c_board_info){
.type = "yourname,lm75",
.addr = addr,
});
if (IS_ERR(lm75_client)) {
ret = PTR_ERR(lm75_client);
pr_err("smart_led: i2c_new_client_device failed\n");
goto err_i2c;
}
i2c_put_adapter(adapter);
/* 字符设备标准注册流程 */
ret = alloc_chrdev_region(&dev_num, 0, 1, DEV_NAME);
if (ret) goto err_alloc;
cdev_init(&my_cdev, &smart_led_fops);
my_cdev.owner = THIS_MODULE;
ret = cdev_add(&my_cdev, dev_num, 1);
if (ret) goto err_cdev_add;
my_class = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(my_class)) { ret = PTR_ERR(my_class); goto err_class; }
my_device = device_create(my_class, dev, dev_num, NULL, DEV_NAME);
if (IS_ERR(my_device)) { ret = PTR_ERR(my_device); goto err_dev; }
/* 初始化状态 */
threshold_mc = DEFAULT_THRESHOLD;
period_ms = DEFAULT_PERIOD_MS;
mode = 0;
led_state = 0;
current_temp = 0;
/* 启动定时器 */
timer_setup(&read_timer, read_timer_callback, 0);
mod_timer(&read_timer, jiffies + msecs_to_jiffies(TEMP_READ_INTERVAL_MS));
timer_setup(&blink_timer, blink_timer_callback, 0);
pr_info("smart_led: loaded, /dev/%s created\n", DEV_NAME);
return 0;
err_dev: class_destroy(my_class);
err_class: cdev_del(&my_cdev);
err_cdev_add: unregister_chrdev_region(dev_num, 1);
err_alloc: err_i2c: err_addr: i2c_put_adapter(adapter);
err_adapter: err_parse: gpiod_put(led_gpio);
return ret;
}
static int smart_led_remove(struct platform_device *pdev)
{
del_timer_sync(&read_timer);
del_timer_sync(&blink_timer);
device_destroy(my_class, dev_num);
class_destroy(my_class);
cdev_del(&my_cdev);
unregister_chrdev_region(dev_num, 1);
if (lm75_client)
i2c_unregister_device(lm75_client);
update_led(0);
gpiod_put(led_gpio);
pr_info("smart_led: removed\n");
return 0;
}
static const struct of_device_id smart_led_of_match[] = {
{ .compatible = "yourname,smart-led" },
{ }
};
MODULE_DEVICE_TABLE(of, smart_led_of_match);
static struct platform_driver smart_led_driver = {
.probe = smart_led_probe,
.remove = smart_led_remove,
.driver = {
.name = "smart_led",
.owner = THIS_MODULE,
.of_match_table = smart_led_of_match,
},
};
module_platform_driver(smart_led_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Smart LED driver with I2C LM75 and GPIO LED");
MODULE_VERSION("1.0");
四、Makefile
makefile
KERNEL_DIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
obj-m := smart_led_drv.o
all:
make -C $(KERNEL_DIR) M=$(PWD) modules
clean:
make -C $(KERNEL_DIR) M=$(PWD) clean
交叉编译时设置 ARCH 和 CROSS_COMPILE。
五、测试与验证
-
确保内核未占用 LM75 :由于设备树中
compatible = "yourname,lm75"不与内核自带驱动匹配,i2cdetect -y 1应显示地址0x48为48而非UU。 -
加载驱动 :
insmod smart_led_drv.ko -
检查日志 :
dmesg | tail -
读取状态 :
cat /dev/smart_led -
设置模式与阈值 :
bashecho "th=25000" > /dev/smart_led # 阈值设为25℃ echo "mode=2" > /dev/smart_led # 闪烁模式 echo "period=300" > /dev/smart_led # 闪烁周期300ms -
观察 LED:当温度超过 25℃ 时 LED 开始闪烁,低于时熄灭。
-
改为常亮模式 :
echo "mode=1" > /dev/smart_led -
卸载 :
rmmod smart_led_drv
六、Linux 驱动学习路径与进阶方向
6.1 本系列知识体系回顾
通过十篇文章,我们构建了以下技能树:
- 驱动框架:设备号、cdev、class、device_create
- 用户交互:copy_to/from_user
- 并发保护:mutex、spinlock
- 平台模型:platform_driver、设备树匹配
- 硬件接口:GPIO、PWM、I2C(SMBus)
- 输入子系统:input_dev、中断注册与上报
- 中断管理:request_irq、tasklet、workqueue、软件消抖
- 定时驱动:timer_list、周期性任务
- 多设备综合:phandle 关联、i2c_client 动态创建
6.2 进阶方向建议
- 块设备驱动 :了解
request_queue、bio,尝试编写 RAM disk。 - 网络设备驱动 :
net_device、NAPI、socket buffer。 - USB 驱动:URB、gadget、usb_driver。
- 设备树深入:overlay、pinctrl 绑定、中断映射。
- DMA 与内存:DMA API、CMA、IOMMU。
- 实时 Linux:PREEMPT_RT 补丁、cyclictest。
- 调试与性能分析:Ftrace、perf、kgdb、crash dump。
- 内核主线贡献 :阅读
Documentation/process/,使用scripts/checkpatch.pl,参与 mailing list。
推荐参考:
- 《Linux Device Drivers》第三版
- 韦东山《嵌入式 Linux 应用开发完全手册》
- 内核源码
drivers/目录下的实际驱动 kernelnewbies.org社区
6.3 学习建议
- 动手实践是内核学习的第一法则,每个示例都亲手编译、加载、测试。
- 阅读内核日志 (
dmesg)和/proc、/sys信息是定位问题的基本功。 - 善用
evtest、i2cdetect、gpio、hexdump等工具快速验证。 - 遇到
Oops不要慌,根据 PC 指针和调用栈反向定位代码,每次都是一种成长。
七、结语
恭喜你完成了本系列全部十篇文章!从点亮一颗 LED 到综合运用 I2C 传感器和智能 LED 控制,你已经掌握了 Linux 字符设备驱动开发的核心知识与实战能力。希望这些内容能为你的嵌入式 Linux 之旅打开一扇门,并在以后的学习与工作中持续发挥作用。
如果觉得这个系列有帮助,欢迎点赞、收藏、关注。有任何疑问或想要探讨的技术方向,请在评论区留言,让我们一起在技术的路上共同进步!
系列完结,感谢阅读!