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

目录

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

[二. 伪代码及程序运行流程](#二. 伪代码及程序运行流程)

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

[3.1 module_i2c_driver宏(对应"1")](#3.1 module_i2c_driver宏(对应“1”))

[3.2 mpu_of_match设备树匹配表(对应"2")](#3.2 mpu_of_match设备树匹配表(对应“2”))

[3.3 MODULE_DEVICE_TABLE宏声明驱动支持的设备列表](#3.3 MODULE_DEVICE_TABLE宏声明驱动支持的设备列表)

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

[3.4.1 init_gpio_reg初始化GPIO时钟和寄存器映射](#3.4.1 init_gpio_reg初始化GPIO时钟和寄存器映射)

[3.5 timer_callback回调函数(对应"4、5")](#3.5 timer_callback回调函数(对应“4、5”))

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

[3.6.1 read_accel加速度读取函数](#3.6.1 read_accel加速度读取函数)

[3.6.1.1 i2c_smbus_read_i2c_block_data函数功能](#3.6.1.1 i2c_smbus_read_i2c_block_data函数功能)

[3.6.1.2 convert_accel函数 转换加速度为位移](#3.6.1.2 convert_accel函数 转换加速度为位移)

[3.6.2 lowpass_filter低通滤波函数](#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为止

cpp 复制代码
// 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 宏将其绑定,即可自动生成以下代码:

cpp 复制代码
module_init(i2c_driver_probe);  // 模块加载时调用 i2c_add_driver
module_exit(i2c_driver_remove); // 模块卸载时调用 i2c_del_driver

无需手动编写 module_initmodule_exit

2. 封装驱动注册与注销

宏内部通过调用 i2c_add_driveri2c_del_driver 完成以下操作:

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

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

3. 本代码解释

cpp 复制代码
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")

  1. 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 内核中一个关键的宏,用于 声明驱动支持的设备列表,并帮助内核实现模块与设备的动态匹配。以下是其具体作用和实现细节:

cpp 复制代码
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")注册到全局设备树匹配表。

设备树解析时

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

驱动绑定

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

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

  1. mpu_mouse_probe在进行一系列设备初始化之后执行mod_timer函数定时器计时
cpp 复制代码
/* 修改:在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内核驱动代码改写

cpp 复制代码
/* 新增:初始化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")

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

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

cpp 复制代码
/* 定时器回调函数(触发工作队列) */
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")

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

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

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

cpp 复制代码
/* 修改:在工作队列处理函数中添加按键检测(新增代码) */
/* 工作队列处理函数(定时读取数据) */
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加速度读取函数

cpp 复制代码
/* 读取加速度计数据 */
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函数 转换加速度为位移

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

cpp 复制代码
/* 加速度转换函数(纯整数运算) */
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低通滤波函数

cpp 复制代码
/*  低通滤波函数: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文库

相关推荐
coding随想21 分钟前
JavaScript的三大核心组成:ECMAScript、DOM与BOM
开发语言·javascript·ecmascript
0xCC说逆向31 分钟前
Windows逆向工程提升之IMAGE_EXPORT_DIRECTORY
开发语言·数据结构·windows·安全·网络安全·pe结构·逆向工程
带电的小王32 分钟前
C++:动态刷新打印内容
开发语言·c++
不会c嘎嘎32 分钟前
linux初识--基础指令
linux·运维·服务器
贺函不是涵33 分钟前
【沉浸式求职学习day47】【JSP详解】
java·开发语言·学习
网硕互联的小客服36 分钟前
如何排查服务器 CPU 温度过高的问题并解决?
linux·运维·服务器·网络·安全
长流小哥39 分钟前
STM32:深度解析RS-485总线与SP3485芯片
stm32·单片机·嵌入式硬件·keil5
国科安芯39 分钟前
【AS32X601驱动系列教程】GPIO_点亮LED详解
单片机·嵌入式硬件
满怀10151 小时前
【Python正则表达式终极指南】从零到工程级实战
开发语言·python·正则表达式·自动化·文本处理·数据清晰
O。o.尊都假都1 小时前
STM32之温湿度传感器(DHT11)
stm32·单片机·嵌入式硬件