目錄
一. 題目要求-內核空間 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/s2)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文庫