Linux字符设备驱动开发(十):综合实例——I2C传感器 + LED智能控制与进阶指南

前言

在前九篇文章中,我们由浅入深地掌握了字符设备驱动框架、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

交叉编译时设置 ARCHCROSS_COMPILE


五、测试与验证

  1. 确保内核未占用 LM75 :由于设备树中 compatible = "yourname,lm75" 不与内核自带驱动匹配,i2cdetect -y 1 应显示地址 0x4848 而非 UU

  2. 加载驱动insmod smart_led_drv.ko

  3. 检查日志dmesg | tail

  4. 读取状态cat /dev/smart_led

  5. 设置模式与阈值

    bash 复制代码
    echo "th=25000" > /dev/smart_led      # 阈值设为25℃
    echo "mode=2" > /dev/smart_led        # 闪烁模式
    echo "period=300" > /dev/smart_led    # 闪烁周期300ms
  6. 观察 LED:当温度超过 25℃ 时 LED 开始闪烁,低于时熄灭。

  7. 改为常亮模式echo "mode=1" > /dev/smart_led

  8. 卸载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_queuebio,尝试编写 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信息是定位问题的基本功。
  • 善用evtesti2cdetectgpiohexdump等工具快速验证。
  • 遇到Oops不要慌,根据 PC 指针和调用栈反向定位代码,每次都是一种成长。

七、结语

恭喜你完成了本系列全部十篇文章!从点亮一颗 LED 到综合运用 I2C 传感器和智能 LED 控制,你已经掌握了 Linux 字符设备驱动开发的核心知识与实战能力。希望这些内容能为你的嵌入式 Linux 之旅打开一扇门,并在以后的学习与工作中持续发挥作用。

如果觉得这个系列有帮助,欢迎点赞、收藏、关注。有任何疑问或想要探讨的技术方向,请在评论区留言,让我们一起在技术的路上共同进步!


系列完结,感谢阅读!

相关推荐
A小辣椒5 小时前
TShark:Wireshark CLI 功能
linux
A小辣椒9 小时前
TShark:基础知识
linux
AlfredZhao11 小时前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao1 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334661 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪1 天前
linux 拷贝文件或目录到指定的位置
linux
大树882 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠2 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质2 天前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
bush42 天前
嵌入式linux学习记录十四、术语
linux·嵌入式