Linux 驱动开发学习计划与实战项目(求职导向版)


求职竞争力分析

市场需求与技能权重

技能领域 面试频率 重要程度 简历亮点权重
字符设备驱动 ★★★★★ ★★★★★
设备树(Device Tree) ★★★★★ ★★★★☆
平台驱动模型 ★★★★☆ ★★★★☆
I2C/SPI 总线 ★★★★☆ ★★★★☆
中断处理 ★★★★☆ ★★★★☆
并发控制(锁机制) ★★★★★ 极高 ★★★★★
GPIO/Pinctrl ★★★☆☆ ★★★☆☆
内存管理 ★★★★☆ ★★★★☆
电源管理 ★★☆☆☆ ★★★☆☆
DMA/定时器 ★★★☆☆ ★★★☆☆

面试高频问题清单

必背核心问题:

  1. 用户空间与内核空间的区别?如何进行数据交互?
  2. spinlock 和 mutex 的区别及适用场景?
  3. 中断上下半部的处理方式有哪些?各有什么特点?
  4. 设备树的作用是什么?compatible 属性如何匹配驱动?
  5. platform_driver 的 probe 函数什么时候被调用?
  6. copy_to_user/copy_from_user 的作用和必要性?
  7. kmalloc 和 vmalloc 的区别?
  8. request_irq 的参数含义及标志位作用?
  9. cdev 注册的完整流程是什么?
  10. I2C 协议的基本时序是怎样的?

第一部分:学习计划(求职导向版)

总体规划(共12周)

阶段 周期 核心内容 求职目标 面试重点
基础筑基 第1-2周 内核基础 + 字符设备 掌握驱动开发框架 模块机制、file_operations
核心机制 第3-5周 并发控制 + 设备树 + 中断 独立编写平台驱动 锁机制、设备树解析、中断处理
总线驱动 第6-8周 I2C + SPI + GPIO/Pinctrl 驱动常见外设 I2C/SPI协议、设备树匹配
综合实战 第9-12周 完整项目开发 + 面试准备 简历项目亮点 架构设计、问题排查

每周学习计划与面试准备

第1周:内核基础与开发环境

理论学习:

  • Linux 内核架构(用户空间 vs 内核空间、系统调用)
  • 内核模块机制(insmod、rmmod、modprobe、depmod)
  • 内核编译与交叉编译(.config、make menuconfig)
  • 内核源码目录结构

重点文件:

include/linux/module.h    # 模块API
include/linux/kernel.h    # printk级别
include/linux/init.h      # __init __exit
scripts/Makefile.lib      # 内核编译系统

练习:

  • 编写 hello world 内核模块
  • 掌握 dmesg 日志查看技巧
  • 理解 obj-m 和 obj-y 的区别

面试准备:

  • 背诵:用户空间与内核空间的区别
  • 背诵:内核模块的加载过程
  • 背诵:printk 与 printf 的区别

第2周:字符设备驱动

理论学习:

  • 主设备号与次设备号(静态分配 vs 动态分配)
  • struct file_operations 结构体详解
  • cdev 注册流程(register_chrdev_region → cdev_init → cdev_add)
  • 用户态/内核态数据交互(copy_to_user / copy_from_user)
  • ioctl 机制与命令定义

重点文件:

include/linux/fs.h          # file_operations
include/linux/cdev.h        # cdev 结构体
include/linux/uaccess.h     # copy_to_user
drivers/char/mem.c          # 经典参考

练习:

  • 实现完整字符设备(open/read/write/ioctl/close)
  • 通过 ioctl 实现用户空间控制内核数据

面试准备:

  • 能画出 cdev 注册流程图
  • 背诵:为什么需要 copy_to_user?
  • 背诵:file_operations 各成员的作用

第3周:并发控制与内存管理

理论学习:

  • 自旋锁(spinlock):适用中断上下文
  • 互斥锁(mutex):适用进程上下文
  • 原子变量(atomic_t)
  • 等待队列(wait_queue)实现阻塞操作
  • 内存分配:kmalloc、kzalloc、vmalloc、kfree
  • IO 内存映射:ioremap、iounmap、devm_ioremap
  • 寄存器读写:readl、writel、readb、writeb

重点文件:

include/linux/spinlock.h    # 自旋锁
include/linux/mutex.h       # 互斥锁
include/linux/wait.h        # 等待队列
include/linux/slab.h        # 内存分配
include/linux/io.h          # IO操作

练习:

  • 在字符设备中加入互斥锁保护共享数据
  • 使用等待队列实现阻塞读
  • 练习 ioremap 映射物理地址

面试准备:

  • 背诵:spinlock vs mutex 的区别
  • 背诵:为什么中断上下文不能使用 mutex?
  • 背诵:kmalloc 和 vmalloc 的区别及适用场景

第4周:设备树(Device Tree)

理论学习:

  • 设备树的起源与作用(替代板级文件)
  • DTS/DTB/DTSI 文件关系
  • 设备树节点、属性语法
  • compatible 匹配机制
  • 常用属性:reg、interrupts、clocks、pinctrl、gpio
  • 设备树编译(dtc)与反编译

重点文件:

arch/arm64/boot/dts/rockchip/rk3568.dtsi
arch/arm64/boot/dts/rockchip/rk3568-evb*.dts
include/dt-bindings/
Documentation/devicetree/bindings/

练习:

  • 阅读 RK3568 设备树
  • 编写自定义设备树节点
  • 使用 of_property_read_xxx 系列函数读取属性

面试准备:

  • 背诵:设备树的作用是什么?
  • 背诵:compatible 属性如何匹配驱动?
  • 能解释 reg 属性的含义

第5周:平台驱动模型与中断

理论学习:

  • 总线-设备-驱动模型(bus-device-driver)
  • platform_device 与 platform_driver
  • 设备树匹配机制(of_device_id)
  • probe 和 remove 函数调用时机
  • 资源获取:platform_get_resource、platform_get_irq
  • 中断注册:request_irq / devm_request_irq
  • 中断上下半部:tasklet、workqueue、threaded_irq

重点文件:

drivers/base/platform.c
include/linux/platform_device.h
include/linux/interrupt.h
drivers/gpio/gpio-rockchip.c

练习:

  • 基于设备树 + 平台驱动重写 LED 驱动
  • 实现按键中断驱动(按键触发中断,LED翻转)
  • 使用 workqueue 处理中断耗时操作

面试准备:

  • 背诵:platform_driver 的 probe 何时被调用?
  • 背诵:中断上下半部的区别及适用场景
  • 背诵:tasklet 和 workqueue 的区别

第6周:I2C 子系统

理论学习:

  • I2C 协议基础(SDA/SCL、主从、地址、ACK/NACK)
  • I2C 总线驱动 vs 设备驱动
  • struct i2c_driver、struct i2c_client
  • i2c_transfer / i2c_smbus_read/write 系列函数
  • 设备树中 I2C 设备描述

重点文件:

drivers/i2c/i2c-core.c
include/linux/i2c.h
drivers/i2c/busses/i2c-rk3x.c
drivers/i2c/i2c-dev.c

练习:

  • 编写 I2C 设备驱动(如 SHT30 温湿度传感器)
  • 使用 i2c-tools 调试(i2cdetect、i2cget、i2cset)

面试准备:

  • 背诵:I2C 协议的基本时序
  • 背诵:i2c_transfer 的工作原理
  • 能解释 I2C 设备地址的概念

第7周:SPI 子系统

理论学习:

  • SPI 协议基础(MOSI/MISO/CLK/CS、四种模式)
  • struct spi_driver、struct spi_device
  • spi_sync / spi_async 传输函数
  • SPI 设备树描述

重点文件:

drivers/spi/spi.c
include/linux/spi/spi.h
drivers/spi/spi-rockchip.c

练习:

  • 编写 SPI 设备驱动(如 OLED 显示屏)
  • 使用 spidev 在用户空间测试

面试准备:

  • 背诵:SPI 与 I2C 的区别
  • 背诵:SPI 的四种模式
  • 理解 spi_sync 和 spi_async 的区别

第8周:GPIO 子系统与 Pinctrl

理论学习:

  • GPIO 子系统 API(gpio_request、devm_gpio_request)
  • gpio_direction_input/output、gpio_get/set_value
  • Pinctrl 子系统(引脚复用、电气属性配置)
  • 设备树中 GPIO 和 Pinctrl 的描述

重点文件:

drivers/gpio/gpiolib.c
include/linux/gpio/consumer.h
drivers/pinctrl/pinctrl-rockchip.c

练习:

  • 使用 GPIO 子系统控制 LED 和按键
  • 配置设备树中的 pinctrl 节点

面试准备:

  • 背诵:devm_xxx 系列函数的作用
  • 理解 Pinctrl 的作用

第9-12周:综合实战项目

详见第二部分"实战项目"。


第二部分:实战项目 —— 智能环境监测系统

项目概述

基于 RK3568 开发板,开发完整的智能环境监测系统。此项目可作为简历亮点,展示以下能力:

能力维度 项目体现
驱动架构设计 字符设备框架 + 多子系统整合
设备树配置 完整的设备树节点定义
I2C 总线驱动 SHT30 温湿度传感器
GPIO 控制 LED 指示灯驱动
中断处理 按键中断驱动
并发控制 mutex 保护共享资源
用户态交互 read/ioctl 接口

项目架构

+--------------------------------------------------+
|                  用户空间应用程序                    |
|    cat /dev/env_monitor                           |
|    ioctl(fd, GET_TEMPERATURE, &temp)              |
+--------------------------------------------------+
                        |
                    系统调用
                        |
+--------------------------------------------------+
|               字符设备驱动框架层                      |
|    file_operations: open/read/write/ioctl/release |
+--------------------------------------------------+
                        |
        +---------------+---------------+
        |               |               |
+-------+------+ +------+------+ +------+------+
| 温湿度传感器  | | LED指示灯  | | 按键中断     |
| (I2C驱动)    | | (GPIO驱动) | | (GPIO+IRQ)  |
+--------------+ +------------+ +-------------+
        |               |               |
        +---------------+---------------+
                        |
               RK3568 GPIO/I2C 控制器

硬件接线说明

I2C 温湿度传感器(SHT30):
  - SDA  → RK3568 I2C3_SDA (GPIO1_A4)
  - SCL  → RK3568 I2C3_SCL (GPIO1_A3)
  - VCC  → 3.3V
  - GND  → GND

LED 指示灯:
  - LED1 (绿色/正常) → GPIO3_B5
  - LED2 (红色/告警) → GPIO3_B6

按键:
  - KEY1 (模式切换)  → GPIO3_A4
  - KEY2 (数据清除)  → GPIO3_A5

项目文件结构

env_monitor/
├── dts/
│   └── rk3568-env-monitor.dtsi    # 设备树片段
├── driver/
│   ├── Makefile
│   ├── env_monitor_main.c         # 主驱动(字符设备框架)
│   ├── sht30_i2c.c                # SHT30 I2C 驱动
│   ├── sht30_i2c.h
│   ├── led_gpio.c                 # LED GPIO 驱动
│   ├── led_gpio.h
│   ├── key_irq.c                  # 按键中断驱动
│   └── key_irq.h
└── app/
    ├── Makefile
    └── env_monitor_app.c          # 用户空间测试程序

模块一:设备树配置

文件:rk3568-env-monitor.dtsi

/ {
    env_monitor {
        compatible = "mycompany,env-monitor";
        status = "okay";

        led-green {
            label = "status-green";
            gpios = <&gpio3 RK_PB5 GPIO_ACTIVE_HIGH>;
            default-state = "off";
        };
        led-red {
            label = "alarm-red";
            gpios = <&gpio3 RK_PB6 GPIO_ACTIVE_HIGH>;
            default-state = "off";
        };

        key-mode {
            label = "key-mode";
            gpios = <&gpio3 RK_PA4 GPIO_ACTIVE_LOW>;
            interrupt-parent = <&gpio3>;
            interrupts = <RK_PA4 IRQ_TYPE_EDGE_FALLING>;
            linux,code = <1>;
        };
        key-clear {
            label = "key-clear";
            gpios = <&gpio3 RK_PA5 GPIO_ACTIVE_LOW>;
            interrupt-parent = <&gpio3>;
            interrupts = <RK_PA5 IRQ_TYPE_EDGE_FALLING>;
            linux,code = <2>;
        };
    };
};

&i2c3 {
    status = "okay";
    pinctrl-names = "default";
    pinctrl-0 = <&i2c3m1_xfer>;

    sht30@44 {
        compatible = "sensirion,sht30";
        reg = <0x44>;
        measurement-frequency = <1>;
    };
};

模块二:SHT30 I2C 驱动

文件:sht30_i2c.h

#ifndef _SHT30_I2C_H_
#define _SHT30_I2C_H_

#include <linux/i2c.h>

#define SHT30_ADDR           0x44
#define SHT30_CMD_MEASURE_H  0x2400
#define SHT30_CMD_MEASURE_L  0x240B
#define SHT30_CMD_SOFT_RST   0x30A2

struct sht30_data {
    struct i2c_client *client;
    struct mutex       lock;
    int                temperature;
    int                humidity;
};

int  sht30_init(struct i2c_client *client, struct sht30_data *data);
void sht30_deinit(struct sht30_data *data);
int  sht30_read_temp_humi(struct sht30_data *data);

#endif

文件:sht30_i2c.c

#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/delay.h>
#include "sht30_i2c.h"

static u8 sht30_crc8(const u8 *data, int len)
{
    u8 crc = 0xFF;
    int i, j;
    for (i = 0; i < len; i++) {
        crc ^= data[i];
        for (j = 0; j < 8; j++) {
            crc = (crc & 0x80) ? ((crc << 1) ^ 0x31) : (crc << 1);
        }
    }
    return crc;
}

static int sht30_send_cmd(struct i2c_client *client, u16 cmd)
{
    u8 buf[2] = { (cmd >> 8) & 0xFF, cmd & 0xFF };
    return i2c_master_send(client, buf, 2);
}

int sht30_read_temp_humi(struct sht30_data *data)
{
    u8 buf[6];
    u16 raw_temp, raw_humi;
    int ret;

    mutex_lock(&data->lock);
    ret = sht30_send_cmd(data->client, SHT30_CMD_MEASURE_H);
    if (ret < 0) {
        dev_err(&data->client->dev, "send measure cmd failed\n");
        goto out;
    }

    msleep(20);
    ret = i2c_master_recv(data->client, buf, 6);
    if (ret < 0) {
        dev_err(&data->client->dev, "read data failed\n");
        goto out;
    }

    if (sht30_crc8(buf, 2) != buf[2] || sht30_crc8(buf + 3, 2) != buf[5]) {
        dev_err(&data->client->dev, "CRC error\n");
        ret = -EIO;
        goto out;
    }

    raw_temp = (buf[0] << 8) | buf[1];
    raw_humi = (buf[3] << 8) | buf[4];

    data->temperature = ((int)raw_temp * 1750) / 65535 - 450;
    data->humidity    = ((int)raw_humi * 1000) / 65535;

    ret = 0;
out:
    mutex_unlock(&data->lock);
    return ret;
}

int sht30_init(struct i2c_client *client, struct sht30_data *data)
{
    int ret;
    data->client = client;
    mutex_init(&data->lock);

    ret = sht30_send_cmd(client, SHT30_CMD_SOFT_RST);
    if (ret < 0) {
        dev_err(&client->dev, "soft reset failed\n");
        return ret;
    }
    msleep(10);

    dev_info(&client->dev, "SHT30 initialized, addr=0x%02x\n", client->addr);
    return 0;
}

void sht30_deinit(struct sht30_data *data)
{
    mutex_destroy(&data->lock);
}

模块三:LED GPIO 驱动

文件:led_gpio.h

#ifndef _LED_GPIO_H_
#define _LED_GPIO_H_

#include <linux/gpio.h>

#define LED_GREEN  0
#define LED_RED    1
#define LED_MAX    2

struct led_dev {
    int          gpio;
    const char  *label;
    bool         state;
};

int  led_init(struct led_dev *leds, int count);
void led_deinit(struct led_dev *leds, int count);
void led_set(struct led_dev *led, bool on);
void led_toggle(struct led_dev *led);

#endif

文件:led_gpio.c

#include <linux/module.h>
#include <linux/gpio.h>
#include "led_gpio.h"

int led_init(struct led_dev *leds, int count)
{
    int i, ret;
    for (i = 0; i < count; i++) {
        ret = devm_gpio_request(NULL, leds[i].gpio, leds[i].label);
        if (ret) {
            pr_err("led: request gpio %d failed: %d\n", leds[i].gpio, ret);
            return ret;
        }
        gpio_direction_output(leds[i].gpio, 0);
        leds[i].state = false;
    }
    return 0;
}

void led_deinit(struct led_dev *leds, int count)
{
    int i;
    for (i = 0; i < count; i++) {
        gpio_set_value(leds[i].gpio, 0);
    }
}

void led_set(struct led_dev *led, bool on)
{
    gpio_set_value(led->gpio, on);
    led->state = on;
}

void led_toggle(struct led_dev *led)
{
    led->state = !led->state;
    gpio_set_value(led->gpio, led->state);
}

模块四:按键中断驱动

文件:key_irq.h

#ifndef _KEY_IRQ_H_
#define _KEY_IRQ_H_

#include <linux/interrupt.h>

typedef void (*key_callback_t)(int key_code, void *priv);

struct key_dev {
    int              gpio;
    int              irq;
    int              code;
    const char      *label;
    key_callback_t   callback;
    void            *priv;
};

int  key_init(struct key_dev *keys, int count);
void key_deinit(struct key_dev *keys, int count);

#endif

文件:key_irq.c

#include <linux/module.h>
#include <linux/gpio.h>
#include <linux/interrupt.h>
#include <linux/of.h>
#include "key_irq.h"

static irqreturn_t key_irq_handler(int irq, void *dev_id)
{
    struct key_dev *key = dev_id;
    if (key->callback)
        key->callback(key->code, key->priv);
    return IRQ_HANDLED;
}

int key_init(struct key_dev *keys, int count)
{
    int i, ret;
    for (i = 0; i < count; i++) {
        ret = devm_gpio_request(NULL, keys[i].gpio, keys[i].label);
        if (ret) {
            pr_err("key: request gpio %d failed: %d\n", keys[i].gpio, ret);
            return ret;
        }
        gpio_direction_input(keys[i].gpio);

        ret = devm_request_irq(NULL, keys[i].irq, key_irq_handler,
                               IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
                               keys[i].label, &keys[i]);
        if (ret) {
            pr_err("key: request irq %d failed: %d\n", keys[i].irq, ret);
            return ret;
        }
    }
    return 0;
}

void key_deinit(struct key_dev *keys, int count)
{
    /* devm_* 资源自动释放 */
}

模块五:主驱动(字符设备框架)

文件:env_monitor_main.c

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/of.h>
#include <linux/of_gpio.h>
#include <linux/platform_device.h>
#include <linux/i2c.h>

#include "sht30_i2c.h"
#include "led_gpio.h"
#include "key_irq.h"

#define DEVICE_NAME  "env_monitor"
#define CLASS_NAME   "env_monitor_class"

#define ENV_IOC_MAGIC  'E'
#define ENV_IOC_GET_TEMP   _IOR(ENV_IOC_MAGIC, 1, int)
#define ENV_IOC_GET_HUMI   _IOR(ENV_IOC_MAGIC, 2, int)
#define ENV_IOC_GET_ALARM  _IOR(ENV_IOC_MAGIC, 3, int)
#define ENV_IOC_CLR_ALARM  _IO(ENV_IOC_MAGIC, 4)

#define TEMP_HIGH_THRESHOLD  400
#define HUMI_HIGH_THRESHOLD  800

struct env_monitor_dev {
    struct cdev         cdev;
    dev_t               devno;
    struct class        *cls;
    struct sht30_data   sht30;
    struct led_dev      leds[LED_MAX];
    struct key_dev      keys[2];
    bool                alarm;
    int                 mode;
};

static struct env_monitor_dev *g_env_dev;

static void key_event_callback(int key_code, void *priv)
{
    struct env_monitor_dev *env = priv;

    if (key_code == 1) {
        env->mode = (env->mode + 1) % 3;
        pr_info("env_monitor: mode switch to %d\n", env->mode);
    } else if (key_code == 2) {
        env->alarm = false;
        led_set(&env->leds[LED_RED], false);
        pr_info("env_monitor: alarm cleared\n");
    }
}

static int env_monitor_open(struct inode *inode, struct file *filp)
{
    filp->private_data = g_env_dev;
    return 0;
}

static ssize_t env_monitor_read(struct file *filp, char __user *buf,
                                size_t count, loff_t *ppos)
{
    struct env_monitor_dev *env = filp->private_data;
    char kbuf[128];
    int len, ret;

    ret = sht30_read_temp_humi(&env->sht30);
    if (ret)
        return ret;

    if (env->sht30.temperature > TEMP_HIGH_THRESHOLD ||
        env->sht30.humidity > HUMI_HIGH_THRESHOLD) {
        env->alarm = true;
        led_set(&env->leds[LED_RED], true);
    }

    len = snprintf(kbuf, sizeof(kbuf),
                   "Temperature: %d.%d C\nHumidity: %d.%d %%\nAlarm: %s\n",
                   env->sht30.temperature / 10, env->sht30.temperature % 10,
                   env->sht30.humidity / 10, env->sht30.humidity % 10,
                   env->alarm ? "YES" : "NO");

    if (count < len)
        return -EINVAL;

    if (copy_to_user(buf, kbuf, len))
        return -EFAULT;

    return len;
}

static long env_monitor_ioctl(struct file *filp, unsigned int cmd,
                              unsigned long arg)
{
    struct env_monitor_dev *env = filp->private_data;
    int val;

    switch (cmd) {
    case ENV_IOC_GET_TEMP:
        sht30_read_temp_humi(&env->sht30);
        val = env->sht30.temperature;
        return copy_to_user((int __user *)arg, &val, sizeof(val)) ? -EFAULT : 0;
    case ENV_IOC_GET_HUMI:
        sht30_read_temp_humi(&env->sht30);
        val = env->sht30.humidity;
        return copy_to_user((int __user *)arg, &val, sizeof(val)) ? -EFAULT : 0;
    case ENV_IOC_GET_ALARM:
        val = env->alarm;
        return copy_to_user((int __user *)arg, &val, sizeof(val)) ? -EFAULT : 0;
    case ENV_IOC_CLR_ALARM:
        env->alarm = false;
        led_set(&env->leds[LED_RED], false);
        return 0;
    default:
        return -ENOTTY;
    }
}

static const struct file_operations env_monitor_fops = {
    .owner          = THIS_MODULE,
    .open           = env_monitor_open,
    .read           = env_monitor_read,
    .unlocked_ioctl = env_monitor_ioctl,
};

static int env_monitor_probe(struct platform_device *pdev)
{
    struct env_monitor_dev *env;
    struct device_node *node = pdev->dev.of_node;
    struct device_node *child;
    int ret;

    env = devm_kzalloc(&pdev->dev, sizeof(*env), GFP_KERNEL);
    if (!env)
        return -ENOMEM;

    g_env_dev = env;
    platform_set_drvdata(pdev, env);

    child = of_get_child_by_name(node, "led-green");
    if (child) {
        env->leds[LED_GREEN].gpio = of_get_gpio(child, 0);
        env->leds[LED_GREEN].label = "status-green";
    }
    child = of_get_child_by_name(node, "led-red");
    if (child) {
        env->leds[LED_RED].gpio = of_get_gpio(child, 0);
        env->leds[LED_RED].label = "alarm-red";
    }

    ret = led_init(env->leds, LED_MAX);
    if (ret) {
        dev_err(&pdev->dev, "led init failed\n");
        return ret;
    }
    led_set(&env->leds[LED_GREEN], true);

    child = of_get_child_by_name(node, "key-mode");
    if (child) {
        env->keys[0].gpio = of_get_gpio(child, 0);
        env->keys[0].irq  = gpio_to_irq(env->keys[0].gpio);
        env->keys[0].code = 1;
        env->keys[0].label = "key-mode";
        env->keys[0].callback = key_event_callback;
        env->keys[0].priv = env;
    }
    child = of_get_child_by_name(node, "key-clear");
    if (child) {
        env->keys[1].gpio = of_get_gpio(child, 0);
        env->keys[1].irq  = gpio_to_irq(env->keys[1].gpio);
        env->keys[1].code = 2;
        env->keys[1].label = "key-clear";
        env->keys[1].callback = key_event_callback;
        env->keys[1].priv = env;
    }

    ret = key_init(env->keys, 2);
    if (ret) {
        dev_err(&pdev->dev, "key init failed\n");
        return ret;
    }

    ret = alloc_chrdev_region(&env->devno, 0, 1, DEVICE_NAME);
    if (ret) {
        dev_err(&pdev->dev, "alloc chrdev region failed\n");
        return ret;
    }

    cdev_init(&env->cdev, &env_monitor_fops);
    env->cdev.owner = THIS_MODULE;
    ret = cdev_add(&env->cdev, env->devno, 1);
    if (ret) {
        dev_err(&pdev->dev, "cdev add failed\n");
        unregister_chrdev_region(env->devno, 1);
        return ret;
    }

    env->cls = devm_class_create(THIS_MODULE, CLASS_NAME);
    if (IS_ERR(env->cls)) {
        dev_err(&pdev->dev, "class create failed\n");
        cdev_del(&env->cdev);
        unregister_chrdev_region(env->devno, 1);
        return PTR_ERR(env->cls);
    }

    devm_device_create(env->cls, &pdev->dev, env->devno, NULL, DEVICE_NAME);

    dev_info(&pdev->dev, "env_monitor probe success\n");
    return 0;
}

static int env_monitor_remove(struct platform_device *pdev)
{
    struct env_monitor_dev *env = platform_get_drvdata(pdev);

    cdev_del(&env->cdev);
    unregister_chrdev_region(env->devno, 1);
    led_deinit(env->leds, LED_MAX);
    sht30_deinit(&env->sht30);

    dev_info(&pdev->dev, "env_monitor removed\n");
    return 0;
}

static int sht30_i2c_probe(struct i2c_client *client,
                            const struct i2c_device_id *id)
{
    if (!g_env_dev)
        return -EPROBE_DEFER;

    return sht30_init(client, &g_env_dev->sht30);
}

static int sht30_i2c_remove(struct i2c_client *client)
{
    if (g_env_dev)
        sht30_deinit(&g_env_dev->sht30);
    return 0;
}

static const struct of_device_id sht30_of_match[] = {
    { .compatible = "sensirion,sht30" },
    { }
};

static const struct of_device_id env_monitor_of_match[] = {
    { .compatible = "mycompany,env-monitor" },
    { }
};

static struct i2c_driver sht30_i2c_driver = {
    .driver = { .name = "sht30", .of_match_table = sht30_of_match },
    .probe  = sht30_i2c_probe,
    .remove = sht30_i2c_remove,
};

static struct platform_driver env_monitor_platform_driver = {
    .driver = { .name = "env_monitor", .of_match_table = env_monitor_of_match },
    .probe  = env_monitor_probe,
    .remove = env_monitor_remove,
};

static int __init env_monitor_init(void)
{
    int ret;

    ret = platform_driver_register(&env_monitor_platform_driver);
    if (ret)
        return ret;

    ret = i2c_add_driver(&sht30_i2c_driver);
    if (ret)
        platform_driver_unregister(&env_monitor_platform_driver);

    return ret;
}

static void __exit env_monitor_exit(void)
{
    i2c_del_driver(&sht30_i2c_driver);
    platform_driver_unregister(&env_monitor_platform_driver);
}

module_init(env_monitor_init);
module_exit(env_monitor_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("dufu");
MODULE_DESCRIPTION("RK3568 Environment Monitor Driver");
MODULE_VERSION("1.0");

模块六:用户空间测试程序

文件:env_monitor_app.c

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <sys/ioctl.h>
#include <signal.h>

#define DEVICE_PATH  "/dev/env_monitor"

#define ENV_IOC_MAGIC  'E'
#define ENV_IOC_GET_TEMP   _IOR(ENV_IOC_MAGIC, 1, int)
#define ENV_IOC_GET_HUMI   _IOR(ENV_IOC_MAGIC, 2, int)
#define ENV_IOC_GET_ALARM  _IOR(ENV_IOC_MAGIC, 3, int)
#define ENV_IOC_CLR_ALARM  _IO(ENV_IOC_MAGIC, 4)

static volatile int running = 1;

void sig_handler(int sig) { running = 0; }

int main(int argc, char *argv[])
{
    int fd, temp, humi, alarm;
    char buf[256];

    signal(SIGINT, sig_handler);

    fd = open(DEVICE_PATH, O_RDONLY);
    if (fd < 0) { perror("open device failed"); return -1; }

    printf("Environment Monitor started. Press Ctrl+C to exit.\n\n");

    while (running) {
        memset(buf, 0, sizeof(buf));
        if (read(fd, buf, sizeof(buf)) > 0)
            printf("=== Read Interface ===\n%s\n", buf);

        if (ioctl(fd, ENV_IOC_GET_TEMP, &temp) == 0)
            printf("[ioctl] Temperature: %d.%d C\n", temp / 10, abs(temp % 10));
        if (ioctl(fd, ENV_IOC_GET_HUMI, &humi) == 0)
            printf("[ioctl] Humidity: %d.%d %%\n", humi / 10, humi % 10);
        if (ioctl(fd, ENV_IOC_GET_ALARM, &alarm) == 0)
            printf("[ioctl] Alarm: %s\n", alarm ? "YES" : "NO");

        if (alarm) {
            printf("High threshold detected! Clearing alarm...\n");
            ioctl(fd, ENV_IOC_CLR_ALARM);
        }

        printf("\n");
        sleep(2);
    }

    close(fd);
    printf("Monitor stopped.\n");
    return 0;
}

Makefile

驱动 Makefile:

KERN_DIR := /path/to/rk356x_linux/kernel
PWD := $(shell pwd)

obj-m := env_monitor.o
env_monitor-objs := env_monitor_main.o sht30_i2c.o led_gpio.o key_irq.o

all:
	$(MAKE) -C $(KERN_DIR) M=$(PWD) modules

clean:
	$(MAKE) -C $(KERN_DIR) M=$(PWD) clean

应用程序 Makefile:

CC := aarch64-linux-gnu-gcc

all:
	$(CC) -o env_monitor_app env_monitor_app.c -static

clean:
	rm -f env_monitor_app

编译与运行步骤

# 1. 编译驱动
cd env_monitor/driver && make

# 2. 编译应用程序
cd ../app && make

# 3. 拷贝到开发板
scp env_monitor.ko env_monitor_app root@<板卡IP>:/data/local/tmp/

# 4. 加载驱动
insmod env_monitor.ko

# 5. 运行测试
./env_monitor_app

项目知识点覆盖与面试价值

模块 覆盖知识点 面试价值
env_monitor_main.c 字符设备框架、ioctl、platform驱动 ★★★★★
sht30_i2c.c I2C协议、CRC校验、mutex保护 ★★★★★
led_gpio.c GPIO子系统、devm资源管理 ★★★☆☆
key_irq.c 中断处理、tasklet/workqueue ★★★★☆
设备树 DTS语法、属性读取、compatible匹配 ★★★★★

简历项目描述模板

智能环境监测系统驱动 (RK3568)
- 基于 Linux 4.19 内核,开发完整的环境监测系统驱动
- 实现 I2C 温湿度传感器(SHT30)驱动,包含 CRC 校验机制
- 设计字符设备框架,提供 read/ioctl 接口供用户空间访问
- 实现按键中断驱动,使用 workqueue 处理中断下半部
- 使用设备树描述硬件资源,支持 platform_driver 自动匹配
- 技术栈:Linux 驱动、I2C子系统、GPIO子系统、中断处理、设备树

第三部分:面试冲刺指南

简历建议

  1. 突出项目经验:将实战项目放在显眼位置,描述清楚职责和成果
  2. 强调技术栈:Linux 内核版本、开发板型号、使用的子系统
  3. 量化成果:如"独立完成XX驱动开发"、“优化XX性能XX%”

面试准备清单

必背知识点:

  • 用户态/内核态区别与数据交互
  • 锁机制(spinlock、mutex、semaphore)
  • 中断处理流程(上下半部)
  • 设备树机制
  • 平台驱动模型
  • I2C/SPI 协议基础

必写代码:

  • hello world 内核模块
  • 字符设备驱动框架
  • platform_driver 模板
  • 中断注册与处理

常见面试题:

  1. 解释 Linux 内核模块的加载过程
  2. spinlock 和 mutex 的区别
  3. 为什么不能在中断上下文使用 mutex?
  4. 设备树的作用是什么?
  5. platform_driver 的 probe 何时被调用?
  6. copy_to_user 和 __copy_to_user 的区别?
  7. kmalloc 和 vmalloc 的区别?
  8. 中断上下半部的处理方式有哪些?
  9. I2C 协议的基本时序?
  10. cdev 注册的完整流程?

扩展学习(进阶)

完成基础项目后,可尝试以下扩展以提升竞争力:

  1. 定时器驱动:使用 hrtimer 实现定时采集
  2. proc/sysfs 接口:通过文件系统暴露设备状态
  3. PWM 驱动:控制蜂鸣器实现声音告警
  4. 电源管理:实现设备低功耗模式
  5. DMA 传输:优化大数据量传输性能
  6. 网络上报:通过 socket 上报数据到云端
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐