结课作业自选01. 内核空间 MPU6050 体感鼠标驱动程序(二)(完整实现流程)
目录
一. 题目要求-内核空间 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.cstruct 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);// 初始化MPU6050i2c_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文库