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 之旅打开一扇门,并在以后的学习与工作中持续发挥作用。

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


系列完结,感谢阅读!

相关推荐
2301_809051148 小时前
Linux 网络编程 学习笔记
linux·网络·学习
wanhengidc8 小时前
服务器租用有何优点
运维·服务器·安全·web安全
hai3152475439 小时前
RISC-V核E203核前向旁路的架构性顽疾
驱动开发·架构·硬件架构·硬件工程·risc-v
hai3152475439 小时前
RISC-V CVA6 AXI适配器+DMA桥蜂鸟E203处理器的总线接口单元(BIU)仲裁器
驱动开发·fpga开发·硬件架构·硬件工程·精益工程
ZGi.ai9 小时前
人工审查节点:让自动化工作流多一步人工把关
运维·人工智能·自动化·人机协同·智能体工作流·人工审查
坤昱9 小时前
cfs调度类深入解刨——最新内核细节分析2
linux·服务器·cfs·cfs调度·eevdf调度·eevdf·kernel 7.1
艾莉丝努力练剑9 小时前
【Linux:文件】Ext系列文件系统进阶
linux·运维·服务器·c++·文件系统·文件io·ext
海市公约9 小时前
Linux核心基础命令与权限管理实战指南
linux·运维·服务器·vim·权限管理·系统监控·命令行
eggcode9 小时前
【Qt学习】Linux(ARM架构)在线安装Qt6.x
linux·qt·学习·arm