结课作业自选01. 内核空间 MPU6050 体感鼠标驱动程序(二)(完整实现流程)

Source

目录

一. 题目要求-内核空间 MPU6050 体感鼠标驱动程序

二. 伪代码及程序运行流程

三. 主要函数详解(根据代码流程进行详解)

3.1 module_i2c_driver宏(对应“1”)

3.2 mpu_of_match设备树匹配表(对应“2”)

3.3 MODULE_DEVICE_TABLE宏声明驱动支持的设备列表

3.4 mpu_mouse_probe驱动检测函数(初始化设备)(对应“3”)

3.4.1 init_gpio_reg初始化GPIO时钟和寄存器映射

3.5 timer_callback回调函数(对应“4、5”)

3.6 accel_work_handler 工作队列处理函数(定时读取数据)(对应“6、7”)

3.6.1 read_accel加速度读取函数

3.6.1.1 i2c_smbus_read_i2c_block_data函数功能

3.6.1.2 convert_accel函数 转换加速度为位移

3.6.2 lowpass_filter低通滤波函数

四. 完整版代码


一. 题目要求-内核空间 MPU6050 体感鼠标驱动程序

        (1)采用课上练习“设备驱动练习”给出的 i2c 框架程序

        (2)修改 dts 文件时,按照课件中的描述修改。

        (3)实现驱动模型要求的 probe()函数。

注意:

        鼠标功能除了 MPU6050 运动传感器外还需要一个或两个按键(左右键),

        按键可以使用按键中断或者使用 MPU6050 定时器同步读取电平。        

        自己设计滤波程序使鼠标指针稳定。

        (4)实现 mpu6050 的定时服务函数,并向 input 核心层报告事件。可以自己选择实

现鼠标类设备还是触摸屏类设备。

        (5)编写一个应用程序来测试驱动,读出鼠标坐标值和按键事件。

二. 伪代码及程序运行流程

        代码执行流程和前一个博客中,内核使用mpu6050的流程一致,此代码就是在前代码的基础上修改完成的。

        1. 模块加载insmod mpu6050_kernel.ko时module_i2c_driver宏会自动注册

        2. mpu_mouse_driver结构体里的.probe对应的mpu_mouse_probe在I2C总线检测到匹配的设备时执行(根据mpu_mouse_driver结构体中的.of_match_table设备树匹配表进行匹配检测)

        3. mpu_mouse_probe在进行一系列设备初始化之后执行mod_timer函数定时器计时

        4. 定时器到期之后执行定时器回调函数timer_callback

        5. timer_callback里面执行data->work,也就是accel_work_handler工作队列处理函数

        6. accel_work_handler函数读取加速度并传给虚拟鼠标控制鼠标实现体感鼠标功能

        7. accel_work_handler函数最后执行mod_timer函数,重新给定时器计时,然后回到4.一直循环执行,直到rmmod mpu6050_kernel.ko为止

// mpu_mouse_kernel.c

struct mpu_mouse_data {
    // 代码中用到的主要变量
};

/* 加速度转换函数(纯整数运算) */
static void convert_accel(int16_t raw_x, int16_t raw_y, int *dx, int *dy) {
    // 将加速度转换成鼠标的位移
}

/*  低通滤波函数:filtered_val = (new_val + 3*last)/4 */
static void lowpass_filter(int *filtered_val, int new_val) {
    // 低通滤波
}

/* 读取加速度计数据 */
static void read_accel(struct i2c_client *client, int *dx, int *dy) {

    // 从MPU6050寄存器读取原始数据(加速度XYZ)
    i2c_smbus_read_i2c_block_data(client, 0x3B, 6, buf);
    
    // 转换加速度为位移(注意,这里是将raw_y传给了x, raw_x传给了y, 因为mpu和屏幕的xy轴是相反的)
    convert_accel(raw_y, raw_x, dx, dy);
}

/* 新增:初始化GPIO时钟和寄存器映射(基于gpios.c中的myopen函数逻辑) */
static void init_gpio_reg(struct mpu_mouse_data *data) {
    // 映射APER_CLK并启用GPIO时钟(复用gpios.c中的逻辑)
    // 映射GPIO_DATA2寄存器(复用gpios.c中的逻辑)
}

// 6. accel_work_handler函数读取加速度并传给虚拟鼠标控制鼠标实现体感鼠标功能
// 7. accel_work_handler函数最后执行mod_timer函数,重新给定时器计时,然后回到4.一直循环执行,直到rmmod mpu6050_kernel.ko为止
/* 修改:在工作队列处理函数中添加按键检测(新增代码) */
/* 工作队列处理函数(定时读取数据) */
static void accel_work_handler(struct work_struct *work) {
    // 读取加速度及按键事件并上报
    // 重新调度定时器
    mod_timer(&data->timer, jiffies + msecs_to_jiffies(SAMPLE_INTERVAL));
}

// 5. timer_callback里面执行data->work,也就是accel_work_handler工作队列处理函数
/* 定时器回调函数(触发工作队列) */
static void timer_callback(struct timer_list *t) {
    struct mpu_mouse_data *data = from_timer(data, t, timer);
    schedule_work(&data->work);
}

// 3. mpu_mouse_probe在进行一系列设备初始化之后执行mod_timer函数定时器倒计时
// 4. 定时器到期之后执行定时器回调函数timer_callback
/* 修改:在probe函数中初始化GPIO(新增代码) */
/* 驱动探测函数(初始化设备) */
static int mpu_mouse_probe(struct i2c_client *client, const struct i2c_device_id *id) {
    // 设备初始化

    // 初始化定时器和工作队列
    timer_setup(&data->timer, timer_callback, 0);
    INIT_WORK(&data->work, accel_work_handler);
    mod_timer(&data->timer, jiffies + msecs_to_jiffies(SAMPLE_INTERVAL));

    return 0;
}

/* 修改:在remove函数中释放GPIO映射(新增代码) */
/* 驱动移除函数(释放资源) */
static int mpu_mouse_remove(struct i2c_client *client) {
    // +++ 新增:取消GPIO寄存器映射
    // 清理定时器和工作队列
}

/* 设备树匹配表 */
static const struct of_device_id mpu_of_match[] = {
    { .compatible = "inv,mpu6050" },    // 必须与设备树中的compatible字段一致
    { }
};
MODULE_DEVICE_TABLE(of, mpu_of_match);

// 2. mpu_mouse_driver结构体里的.probe对应的mpu_mouse_probe在I2C总线检测到匹配的设备时执行
/* I2C驱动结构体 */
static struct i2c_driver mpu_mouse_driver = {
    .probe = mpu_mouse_probe,
    .remove = mpu_mouse_remove,
    .driver = {
        .name = "mpu6050-mouse",
        .of_match_table = mpu_of_match,     // 启用设备树匹配
    },
};

// 1. 模块加载insmod mpu6050_kernel.ko时module_i2c_driver宏会自动注册
module_i2c_driver(mpu_mouse_driver);
MODULE_DESCRIPTION("MPU6050 I2C Mouse Driver with GPIO Buttons");
MODULE_LICENSE("GPL");

三. 主要函数详解(根据代码流程进行详解)

3.1 module_i2c_driver宏(对应“1”)

        1. 模块加载insmod mpu6050_kernel.ko时module_i2c_driver宏会自动注册

        1. 自动生成模块的加载/卸载函数

        开发者只需定义一个 i2c_driver 结构体,并通过 module_i2c_driver 宏将其绑定,即可自动生成以下代码:

module_init(i2c_driver_probe);  // 模块加载时调用 i2c_add_driver
module_exit(i2c_driver_remove); // 模块卸载时调用 i2c_del_driver

        无需手动编写 module_init 和 module_exit

        2. 封装驱动注册与注销

        宏内部通过调用 i2c_add_driver 和 i2c_del_driver 完成以下操作:

        注册驱动:将 i2c_driver 注册到内核的 I2C 子系统。

        注销驱动:在模块卸载时,安全地移除驱动并释放资源。

        3. 本代码解释

static struct i2c_driver mpu_mouse_driver = {
    .probe = mpu_mouse_probe,
    .remove = mpu_mouse_remove,
    .driver = {
        .name = "mpu6050-mouse",
        .of_match_table = mpu_of_match,
    },
};
module_i2c_driver(mpu_mouse_driver);

        注册流程
        当通过 insmod 加载驱动模块时,module_i2c_driver 会自动调用 i2c_add_driver(&mpu_mouse_driver),触发设备探测(.probe 函数)。

        注销流程
        当通过 rmmod 卸载模块时,自动调用 i2c_del_driver(&mpu_mouse_driver),执行 .remove 函数清理资源。

        driver.name:驱动名称(需唯一)。

        of_match_table(可选):设备树匹配表。

3.2 mpu_of_match设备树匹配表(对应“2”)

        2. mpu_mouse_driver结构体里的.probe对应的mpu_mouse_probe在I2C总线检测到匹配的设备时执行(根据mpu_mouse_driver结构体中的.of_match_table设备树匹配表进行匹配检测)

        设备树文件中i2c连接mpu6050的compatible 字段如下,mpu_of_match中的compatible 字段与设备树中的compatible 字段相同,即为匹配成功,然后执行mpu_mouse_probe函数。

3.3 MODULE_DEVICE_TABLE宏声明驱动支持的设备列表

   MODULE_DEVICE_TABLE 是 Linux 内核中一个关键的宏,用于 声明驱动支持的设备列表,并帮助内核实现模块与设备的动态匹配。以下是其具体作用和实现细节: 

static const struct of_device_id mpu_of_match[] = {
    { .compatible = "inv,mpu6050" }, // 与设备树节点中的 compatible 字段匹配
    { }
};
MODULE_DEVICE_TABLE(of, mpu_of_match); // 关键宏声明

具体流程

        模块加载时

   1. 内核解析模块中的 MODULE_DEVICE_TABLE(of, ...),将兼容性字符串(如 "inv,mpu6050")注册到全局设备树匹配表。

        设备树解析时

        2. 内核启动时,设备树中的节点若包含 compatible = "inv,mpu6050",则会触发匹配逻辑。

        驱动绑定

        3. 内核调用匹配驱动的 .probe 函数(即 mpu_mouse_probe),完成硬件初始化。

3.4 mpu_mouse_probe驱动检测函数(初始化设备)(对应“3”)

        3. mpu_mouse_probe在进行一系列设备初始化之后执行mod_timer函数定时器计时

/* 修改:在probe函数中初始化GPIO(新增代码) */
/* 驱动探测函数(初始化设备) */
static int mpu_mouse_probe(struct i2c_client *client, const struct i2c_device_id *id) {
    struct device *dev = &client->dev;  // 获取与当前 I2C 设备关联的通用设备结构体指针
    struct mpu_mouse_data *data;
    int ret;
    
    // 分配设备数据结构
    data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL);
    if (!data) return -ENOMEM;
    // 初始化I2C客户端
    data->client = client;
    i2c_set_clientdata(client, data);
    // 初始化输入设备
    data->input = devm_input_allocate_device(dev);
    if (!data->input) return -ENOMEM;
    
    data->input->name = "MPU6050 Mouse";
    data->input->id.bustype = BUS_I2C;
    
    // !!! 修改:注册按键事件类型(新增EV_KEY支持)
    __set_bit(EV_REL, data->input->evbit);
    __set_bit(REL_X, data->input->relbit);
    __set_bit(REL_Y, data->input->relbit);
    __set_bit(EV_KEY, data->input->evbit);      // +++ 新增按键事件
    __set_bit(BTN_LEFT, data->input->keybit);   // +++ 左键
    __set_bit(BTN_RIGHT, data->input->keybit);  // +++ 右键
    
    // 注册输入设备
    ret = input_register_device(data->input);
    if (ret) {
        dev_err(dev, "Failed to register input device\n");
        return ret;
    }
    
    // +++ 新增:初始化GPIO寄存器
    init_gpio_reg(data);
    
    // 初始化MPU6050
    i2c_smbus_write_byte_data(client, 0x1C, 0x00);
    i2c_smbus_write_byte_data(client, 0x6B, 0x00);
    msleep(100);
    
    data->filtered_dx = 0;
    data->filtered_dy = 0;
    
    // +++ 新增:初始化按键状态和去抖动计数器
    data->prev_left_state = 1; // 默认未按下
    data->prev_right_state = 1;

    // 初始化定时器和工作队列
    timer_setup(&data->timer, timer_callback, 0);
    INIT_WORK(&data->work, accel_work_handler);
    mod_timer(&data->timer, jiffies + msecs_to_jiffies(SAMPLE_INTERVAL));

    dev_info(dev, "MPU6050 Mouse Driver Initialized\n");
    return 0;
}

3.4.1 init_gpio_reg初始化GPIO时钟和寄存器映射

        仿照gpio内核驱动代码改写

/* 新增:初始化GPIO时钟和寄存器映射(基于gpios.c中的myopen函数逻辑) */
static void init_gpio_reg(struct mpu_mouse_data *data) {
    unsigned int *clk_reg;

    // 映射APER_CLK并启用GPIO时钟(复用gpios.c中的逻辑)
    clk_reg = ioremap(APER_CLK, 4);
    if (clk_reg) {
        iowrite32(ioread32(clk_reg) | 1 << 22, clk_reg); // 设置第22位
        iounmap(clk_reg);
    } else {
        pr_err("Failed to map APER_CLK register\n");
    }
    
    // 映射GPIO_DATA2寄存器(复用gpios.c中的逻辑)
    data->gpio_reg = ioremap(GPIO_DATA2, 4);
    if (!data->gpio_reg) {
        pr_err("Failed to map GPIO_DATA2 register\n");
    }
}

3.5 timer_callback回调函数(对应“4、5”)

        4. 定时器到期之后执行定时器回调函数timer_callback

        5. timer_callback里面执行data->work,也就是accel_work_handler工作队列处理函数

/* 定时器回调函数(触发工作队列) */
static void timer_callback(struct timer_list *t) {
    struct mpu_mouse_data *data = from_timer(data, t, timer);
    schedule_work(&data->work);
}

        schedule_work(&data->work) 的作用是 将工作项 data->work 提交到内核的全局工作队列中,以便在进程上下文中异步执行 accel_work_handler 函数,确保内核的实时性和稳定性,避免中断处理被阻塞。

3.6 accel_work_handler 工作队列处理函数(定时读取数据)(对应“6、7”)

        6. accel_work_handler函数读取加速度并传给虚拟鼠标控制鼠标实现体感鼠标功能

        7. accel_work_handler函数最后执行mod_timer函数,重新给定时器计时,然后回到4.一直循环执行,直到rmmod mpu6050_kernel.ko为止

        按键状态的获取和判断也在此函数中

/* 修改:在工作队列处理函数中添加按键检测(新增代码) */
/* 工作队列处理函数(定时读取数据) */
static void accel_work_handler(struct work_struct *work) {
    struct mpu_mouse_data *data = container_of(work, struct mpu_mouse_data, work);
    int dx, dy;
    uint32_t gpio_state;
    int current_left, current_right;
    
    // 读取加速度并滤波
    read_accel(data->client, &dx, &dy);
    lowpass_filter(&data->filtered_dx, dx);
    lowpass_filter(&data->filtered_dy, dy);
    // 上报相对位移事件
    input_report_rel(data->input, REL_X, data->filtered_dx);
    input_report_rel(data->input, REL_Y, data->filtered_dy);
    
    // +++ 新增:读取GPIO状态并上报按键事件
    if (data->gpio_reg) {
        gpio_state = ioread32(data->gpio_reg);
        current_left = !(gpio_state & 0x04); // bit2=0表示左键按下    current_left=1是按下,=0是未按下
        current_right = !(gpio_state & 0x02); // bit1=0表示右键按下
        /* 无按键按下:37f:0011 0111 0111
        左按键按下:37b:0011 0111 1011
        右按键按下:37d:0011 0111 1101 */

        // +++ 新增:去抖动逻辑
        if (current_left != data->prev_left_state 
        || current_right != data->prev_right_state) {
            
            // 状态稳定后上报按键事件
            input_report_key(data->input, BTN_LEFT, current_left);
            input_report_key(data->input, BTN_RIGHT, current_right);
            // 更新上一次状态
            data->prev_left_state = current_left;
            data->prev_right_state = current_right;
        }
    }
    input_sync(data->input);
    // 重新调度定时器
    mod_timer(&data->timer, jiffies + msecs_to_jiffies(SAMPLE_INTERVAL));
}

3.6.1 read_accel加速度读取函数

/* 读取加速度计数据 */
static void read_accel(struct i2c_client *client, int *dx, int *dy) {
    uint8_t buf[6];
    int16_t raw_x, raw_y;
    
    // 从MPU6050寄存器读取原始数据(加速度XYZ)
    i2c_smbus_read_i2c_block_data(client, 0x3B, 6, buf);
    
    // 合并高8位和低8位数据
    raw_x = (buf[0] << 8) | buf[1];
    raw_y = (buf[2] << 8) | buf[3];
    
    // 转换加速度为位移(注意,这里是将raw_y传给了x, raw_x传给了y, 因为mpu和屏幕的xy轴是相反的)
    convert_accel(raw_y, raw_x, dx, dy);
}
3.6.1.1 i2c_smbus_read_i2c_block_data函数功能

        指定寄存器地址:通过 reg 参数指定要读取的寄存器起始地址。

        封装位置Linux 内核的 i2c-core-smbus.c 文件。

3.6.1.2 convert_accel函数 转换加速度为位移

        因为强制使用浮点运算会导致代码无法运行,纯整数运算确保驱动在不同硬件平台上的通用性。所以这里使用整数运算,通过设置合理的灵敏度、死区等宏定义参数,确保代码稳定运行。

/* 加速度转换函数(纯整数运算) */
static void convert_accel(int16_t raw_x, int16_t raw_y, int *dx, int *dy) {
    // 1. 原始数据转实际加速度(cm/s²)
    int ax = (raw_x * GRAVITY_CM_S2) / ACCEL_SCALE_2G;
    int ay = (raw_y * GRAVITY_CM_S2) / ACCEL_SCALE_2G;
    
    // 2. 应用死区滤波
    ax = (abs(ax) < DEADZONE) ? 0 : ax;
    ay = (abs(ay) < DEADZONE) ? 0 : ay;
    
    // 3. 加速度转位移(灵敏度调整)
    *dx = (ax * SENSITIVITY) / 100;  // 整数运算避免浮点
    *dy = -(ay * SENSITIVITY) / 100; // Y轴方向取反
}

3.6.2 lowpass_filter低通滤波函数

/*  低通滤波函数:filtered_val = (new_val + 3*last)/4 */
static void lowpass_filter(int *filtered_val, int new_val) {
    *filtered_val = (new_val + 3 * (*filtered_val)) / 4;
}

1. 低通滤波函数的作用

        (1)抑制高频噪声
        通过衰减信号中的快速变化部分(如传感器噪声、瞬时干扰),保留低频成分(如真实运动趋势),使数据更平滑稳定。

        (2)平滑信号输出
        减少测量值的突变,提升数据的可读性和后续处理的可靠性(如鼠标移动控制)。

2. 为什么使用这个公式(此公式的优势)

        (1)计算高效

        仅需 一次乘法、一次加法、一次除法(或位运算),适合实时处理。示例代码中,除法为整数运算(/4),可优化为右移操作(>> 2),进一步提升速度。

        (2)内存占用极低

        只需保存 上一次滤波值,无需存储多组历史数据(如移动平均滤波需保存N个样本)。

        (3)参数可调性强

        通过调整权重比例(如 (new_val + 7 * filtered_val) / 8),可灵活控制平滑效果与响应速度的平衡。

        (4)避免浮点运算

        纯整数运算兼容无FPU的嵌入式平台,减少内核上下文切换开销。

        (5)平滑效果显著

        在MPU6050驱动中,能有效抑制加速度计的抖动噪声,使鼠标移动更平滑。

四. 完整版代码

        内核空间MPU6050体感鼠标驱动程序资源-CSDN文库