:::info **實驗目標**: 理解i2c通訊驅動、如何修改i2c設備樹 **實驗目的**:移植i2c晶片到到開發版上 **實驗器材**:正點原子STM32MP157 Mini開發板、筆電、 USB Type-C 轉、RJ45、Gigabit網路卡、mpu6050、杜邦線、麵包版 ::: # mpu6050介紹 參考[MPU6050模塊](https://doc.embedfire.com/module/module_tutorial/zh/latest/Module_Manual/iic_class/mpu6050.html) YH-MPU6050 六軸傳感器模組,採用 InvenSense 的 MPU6050 晶片,可檢測三軸加速度、三軸角速度及溫度數據。透過i2c傳輸數據。 底下是晶片和腳位圖 | ![Image 1](https://hackmd.io/_uploads/S1U9Ac8Vyx.png) | ![Image 2](https://hackmd.io/_uploads/HkuK0cI41l.png) | |:----------------------------------------------------:|:----------------------------------------------------:| :::info 在 `STM32P157Mini` 開發板上並沒有內建 `MPU6050` 這顆晶片,因此,我們需要先確認開發板上尚未被使用的可用引腳。接著,在檢查 `stm32mp15-pinctrl.dtsi` 檔案時,確認是否有適合用於 `I2C2` 的引腳,**最終決定使用 `PH4` 和 `PH5` 作為連接引腳**。 ::: | ![Pin Configuration](https://hackmd.io/_uploads/SJv100vIyl.png) | ![I2C Mapping](https://hackmd.io/_uploads/SyfS3CDUJg.png) | |:--------------------------------------------------------------:|:-------------------------------------------------------:| # 完整接線圖 ![image](https://hackmd.io/_uploads/HkDiw1O8kg.png) | 編號 | 名稱 | 描述 | |------|---------------|----------------------------------------------------------------------| | 1 | MPU6050 | 六軸傳感器模組,用於檢測加速度、角速度及溫度數據。 | | 2 | I2C 接口 | 與主控器通訊的接口,用於傳輸數據。 | | 3 | 電源接口 | 提供模組運行所需的電源,支持多種電壓輸入(如 3.3V、5V)。 | | 4 | 開發版 | stm32p157mini | | 5 | 邏輯分析儀 | 查看訊號 | i2c訊號分析圖 [i2c邏輯分析儀波形圖分析](https://hackmd.io/@a0979552111/r1t_aSx91l) i2c原理 [I2C 基本原理](https://hackmd.io/@a0979552111/HJiaspel1x) # 修改設備樹 ``` &i2c2 { pinctrl-names = "default", "sleep"; pinctrl-0 = <&i2c2_pins_a>; pinctrl-1 = <&i2c2_pins_sleep_a>; status = "okay"; mpu6050:mpu6050@68 { compatible = "fire,i2c_mpu6050"; reg = <0x68>; }; }; ``` 完整版 :::spoiler 打開設備樹代碼1 ```cpp= // SPDX-License-Identifier: (GPL-2.0+ OR BSD-3-Clause) /* * Copyright (C) STMicroelectronics 2019 - All Rights Reserved * Author: Alexandre Torgue <alexandre.torgue@st.com> for STMicroelectronics. */ /dts-v1/; #include "stm32mp157.dtsi" #include "stm32mp15xd.dtsi" #include "stm32mp15-pinctrl.dtsi" #include "stm32mp15xxaa-pinctrl.dtsi" #include "stm32mp157-m4-srm.dtsi" #include "stm32mp157-m4-srm-pinctrl.dtsi" #include "stm32mp157d-atk.dtsi" / { model = "STMicroelectronics STM32MP157D eval daughter"; compatible = "st,stm32mp157d-ed1", "st,stm32mp157"; chosen { stdout-path = "serial0:115200n8"; }; aliases { serial0 = &uart4; }; reserved-memory { gpu_reserved: gpu@f6000000 { reg = <0xf6000000 0x8000000>; no-map; }; optee_memory: optee@fe000000 { reg = <0xfe000000 0x02000000>; no-map; }; }; }; &i2c2 { pinctrl-names = "default", "sleep"; // pinctrl-names = "default"; pinctrl-0 = <&i2c2_pins_a>; pinctrl-1 = <&i2c2_pins_sleep_a>; status = "okay"; mpu6050:mpu6050@68 { compatible = "fire,i2c_mpu6050"; reg = <0x68>; status = "okay"; }; }; &cpu1{ cpu-supply = <&vddcore>; }; &gpu { contiguous-area = <&gpu_reserved>; status = "okay"; }; &optee { status = "okay"; }; ``` ::: :::info ### **Q:為什麼在設備樹中,I2C 節點需要用 `&` 引用並放在根節點外,而不是直接在根節點下定義所有配置?** - **硬體控制器與外設分離** - I2C 節點代表硬體控制器,通常定義在 SoC 的共享文件中(如 `*.dtsi`),便於多處重用和集中管理,而外設則定義在根節點下,描述具體設備功能。 ```cpp #include "stm32mp157.dtsi" #include "stm32mp15xd.dtsi" #include "stm32mp15-pinctrl.dtsi" #include "stm32mp15xxaa-pinctrl.dtsi" #include "stm32mp157-m4-srm.dtsi" #include "stm32mp157-m4-srm-pinctrl.dtsi" #include "stm32mp157d-atk.dtsi" ``` > 這表示 實驗中I2C2 的定義已經在上層的 .dtsi 文件中預先定義。 - **提高靈活性與可維護性** - 使用 `&` 引用可以覆蓋或修改控制器屬性(如狀態和引腳配置),<font color="#f00">而不需要重新定義整個節點,方便日後調整或擴展。</font> ::: :::success 修改過後,從新編譯設備樹 ```shell make dtbs ``` 複製編譯過後的.dtb複製到tftp目錄底下 ```shell cp ./arch/arm/boot/dts/stm32mp157d-atk.dtb ~/linux/tftpboot -f ``` 重啟開發版,進入 /proc/device-tree/,`ls`查看設備樹 ![i2c](https://hackmd.io/_uploads/B14XeydUyg.png) 如果查看`0-0068` 內部的`name` 檔案可以看到`i2c_mpu6050` ![image](https://hackmd.io/_uploads/HJcKeyOUJg.png) ::: # 編寫makefile #### makefile ```makefile! KERNELDIR := /home/alientek/linux/my_linux/linux-5.4.31 CURRENT_PATH := $(shell pwd) obj-m := i2c_mpu6050.o build: kernel_modules kernel_modules: $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules clean: $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean ``` # 驅動代碼 :::spoiler 打開驅動程式碼 ```cpp! //i2c_mpu6050.c #include <linux/init.h> #include <linux/module.h> #include <linux/fs.h> #include <linux/cdev.h> #include <linux/uaccess.h> #include <linux/i2c.h> #include <linux/types.h> #include <linux/kernel.h> #include <linux/delay.h> #include <linux/ide.h> #include <linux/errno.h> #include <linux/gpio.h> #include <linux/of.h> #include <linux/of_address.h> #include <linux/of_gpio.h> #include <asm/io.h> #include <linux/device.h> #include <linux/platform_device.h> #include "i2c_mpu6050.h" /*------------------字元設備內容----------------------*/ #define DEV_NAME "I2C1_mpu6050" #define DEV_CNT (1) /*定義 led 資源結構體,保存獲取得到的節點資訊以及轉換後的虛擬暫存器位址*/ static dev_t mpu6050_devno; //定義字元設備的設備號 static struct cdev mpu6050_chr_dev; //定義字元裝置結構體chr_dev struct class *class_mpu6050; //儲存已建立的class struct device *device_mpu6050; // 儲存已建立的設備 struct device_node *mpu6050_device_node; //rgb_led的設備樹節點結構體 /*------------------IIC設備內容----------------------*/ struct i2c_client *mpu6050_client = NULL; //保存mpu6050裝置對應的i2c_client結構體,匹配成功後由.prob函數return。 /*透過i2c 向mpu6050寫入數據 *mpu6050_client:mpu6050的i2c_client結構體。 *address, 資料要寫入的位址, *data, 要寫入的數據 *返回值,錯誤,-1。成功,0 */ static int i2c_write_mpu6050(struct i2c_client *mpu6050_client, u8 address, u8 data) { int error = 0; u8 write_data[2]; struct i2c_msg send_msg; //要傳送的資料結構體 /*設定要傳送的數據*/ write_data[0] = address; write_data[1] = data; /*發送 iic要寫入的位址 reg*/ send_msg.addr = mpu6050_client->addr; //mpu6050在 iic 總線上的位址 send_msg.flags = 0; //標記為發送數據 send_msg.buf = write_data; //寫入的首位址 send_msg.len = 2; //reg長度 /*執行發送*/ error = i2c_transfer(mpu6050_client->adapter, &send_msg, 1); if (error != 1) { printk(KERN_DEBUG "\n i2c_transfer error \n"); return -1; } return 0; } /*透過i2c 向mpu6050讀取數據 *mpu6050_client:mpu6050的i2c_client結構體。 *address, 要讀取的位址, *data,保存讀取得到的數據 *length,讀取長度 *返回值,錯誤,-1。成功,0 */ static int i2c_read_mpu6050(struct i2c_client *mpu6050_client, u8 address, void *data, u32 length) { int error = 0; u8 address_data = address; struct i2c_msg mpu6050_msg[2]; /*設定讀取位置msg*/ mpu6050_msg[0].addr = mpu6050_client->addr; //mpu6050在 iic 總線上的位址 mpu6050_msg[0].flags = 0; //標記為發送數據 mpu6050_msg[0].buf = &address_data; //寫入的首位址 mpu6050_msg[0].len = 1; //寫入長度 /*設定讀取位置msg*/ mpu6050_msg[1].addr = mpu6050_client->addr; //mpu6050在 iic 總線上的位址 mpu6050_msg[1].flags = I2C_M_RD; //標記為讀取數據 mpu6050_msg[1].buf = data; //讀取得到的數據保存位置 mpu6050_msg[1].len = length; //讀取長度 error = i2c_transfer(mpu6050_client->adapter, mpu6050_msg, 2); if (error != 2) { printk(KERN_DEBUG "\n i2c_read_mpu6050 error \n"); return -1; } return 0; } /*初始化i2c *返回值,成功,返回0。失敗,返回 -1 */ static int mpu6050_init(void) { int error = 0; /*配置mpu6050*/ error += i2c_write_mpu6050(mpu6050_client, PWR_MGMT_1, 0X00); error += i2c_write_mpu6050(mpu6050_client, SMPLRT_DIV, 0X07); error += i2c_write_mpu6050(mpu6050_client, CONFIG, 0X06); error += i2c_write_mpu6050(mpu6050_client, ACCEL_CONFIG, 0X01); if (error < 0) { /*初始化錯誤*/ printk(KERN_DEBUG "\n mpu6050_init error \n"); return -1; } return 0; } /*字元設備操作函數集,open函數實現*/ static int mpu6050_open(struct inode *inode, struct file *filp) { /*向 mpu6050 發送配置數據,讓mpu6050處於正常工作狀態*/ mpu6050_init(); return 0; } /*字元設備操作函數集,.read函數實現*/ static ssize_t mpu6050_read(struct file *filp, char __user *buf, size_t cnt, loff_t *off) { char data_H; char data_L; int error; short mpu6050_result[6]; //保存mpu6050轉換得到的原始數據 i2c_read_mpu6050(mpu6050_client, ACCEL_XOUT_H, &data_H, 1); i2c_read_mpu6050(mpu6050_client, ACCEL_XOUT_L, &data_L, 1); mpu6050_result[0] = data_H << 8; mpu6050_result[0] += data_L; i2c_read_mpu6050(mpu6050_client, ACCEL_YOUT_H, &data_H, 1); i2c_read_mpu6050(mpu6050_client, ACCEL_YOUT_L, &data_L, 1); mpu6050_result[1] = data_H << 8; mpu6050_result[1] += data_L; i2c_read_mpu6050(mpu6050_client, ACCEL_ZOUT_H, &data_H, 1); i2c_read_mpu6050(mpu6050_client, ACCEL_ZOUT_L, &data_L, 1); mpu6050_result[2] = data_H << 8; mpu6050_result[2] += data_L; i2c_read_mpu6050(mpu6050_client, GYRO_XOUT_H, &data_H, 1); i2c_read_mpu6050(mpu6050_client, GYRO_XOUT_L, &data_L, 1); mpu6050_result[3] = data_H << 8; mpu6050_result[3] += data_L; i2c_read_mpu6050(mpu6050_client, GYRO_YOUT_H, &data_H, 1); i2c_read_mpu6050(mpu6050_client, GYRO_YOUT_L, &data_L, 1); mpu6050_result[4] = data_H << 8; mpu6050_result[4] += data_L; i2c_read_mpu6050(mpu6050_client, GYRO_ZOUT_H, &data_H, 1); i2c_read_mpu6050(mpu6050_client, GYRO_ZOUT_L, &data_L, 1); mpu6050_result[5] = data_H << 8; mpu6050_result[5] += data_L; error = copy_to_user(buf, mpu6050_result, cnt); if(error != 0) { printk("copy_to_user error!"); return -1; } return 0; } /*字元設備操作函數集,.release函數實現*/ static int mpu6050_release(struct inode *inode, struct file *filp) { return 0; } /*字元設備操作函數集*/ static struct file_operations mpu6050_chr_dev_fops = { .owner = THIS_MODULE, .open = mpu6050_open, .read = mpu6050_read, .release = mpu6050_release, }; /*----------------平台驅動函數集-----------------*/ static int mpu6050_probe(struct i2c_client *client, const struct i2c_device_id *id) { int ret = -1; //保存錯誤狀態碼 printk(KERN_EMERG "\t match successed \n"); /*---------------------註冊 字元設備部分-----------------*/ //採用動態分配的方式,獲取設備編號,次設備號為0, //設備名稱為rgb-leds,可透過命令cat /proc/devices查看 //DEV_CNT為1,當前只申請一個設備編號 ret = alloc_chrdev_region(&mpu6050_devno, 0, DEV_CNT, DEV_NAME); if (ret < 0) { printk("fail to alloc mpu6050_devno\n"); goto alloc_err; } //關聯字元設備結構體cdev與檔案操作結構體file_operations mpu6050_chr_dev.owner = THIS_MODULE; cdev_init(&mpu6050_chr_dev, &mpu6050_chr_dev_fops); // 添加設備至cdev_map散列表中 ret = cdev_add(&mpu6050_chr_dev, mpu6050_devno, DEV_CNT); if (ret < 0) { printk("fail to add cdev\n"); goto add_err; } /*建立類 */ class_mpu6050 = class_create(THIS_MODULE, DEV_NAME); /*建立設備 DEV_NAME 指定設備名,*/ device_mpu6050 = device_create(class_mpu6050, NULL, mpu6050_devno, NULL, DEV_NAME); mpu6050_client = client; return 0; add_err: // 添加設備失敗時,需要註銷設備號 unregister_chrdev_region(mpu6050_devno, DEV_CNT); printk("\n error! \n"); alloc_err: return -1; } static int mpu6050_remove(struct i2c_client *client) { /*刪除設備*/ device_destroy(class_mpu6050, mpu6050_devno); //清除設備 class_destroy(class_mpu6050); //清除類 cdev_del(&mpu6050_chr_dev); //清除設備號 unregister_chrdev_region(mpu6050_devno, DEV_CNT); //取消註冊字元設備 return 0; } /*定義ID 匹配表*/ static const struct i2c_device_id gtp_device_id[] = { {"fire,i2c_mpu6050", 0}, {}}; /*定義設備樹匹配表*/ static const struct of_device_id mpu6050_of_match_table[] = { {.compatible = "fire,i2c_mpu6050"}, {/* sentinel */}}; /*定義i2c總線設備結構體*/ struct i2c_driver mpu6050_driver = { .probe = mpu6050_probe, .remove = mpu6050_remove, .id_table = gtp_device_id, .driver = { .name = "i2c_mpu6050", .owner = THIS_MODULE, .of_match_table = mpu6050_of_match_table, }, }; /* *驅動初始化函數 */ static int __init mpu6050_driver_init(void) { int ret; pr_info("mpu6050_driver_init\n"); ret = i2c_add_driver(&mpu6050_driver); return ret; } /* *驅動註銷函數 */ static void __exit mpu6050_driver_exit(void) { pr_info("mpu6050_driver_exit\n"); i2c_del_driver(&mpu6050_driver); } module_init(mpu6050_driver_init); module_exit(mpu6050_driver_exit); MODULE_LICENSE("GPL"); ``` ::: :::spoiler 打開驅動程式碼2 ```cpp //i2c_mpu6050.h #ifndef I2C_MPU6050_H #define I2C_MPU6050_H //巨集定義 #define SMPLRT_DIV 0x19 #define CONFIG 0x1A #define GYRO_CONFIG 0x1B #define ACCEL_CONFIG 0x1C #define ACCEL_XOUT_H 0x3B #define ACCEL_XOUT_L 0x3C #define ACCEL_YOUT_H 0x3D #define ACCEL_YOUT_L 0x3E #define ACCEL_ZOUT_H 0x3F #define ACCEL_ZOUT_L 0x40 #define TEMP_OUT_H 0x41 #define TEMP_OUT_L 0x42 #define GYRO_XOUT_H 0x43 #define GYRO_XOUT_L 0x44 #define GYRO_YOUT_H 0x45 #define GYRO_YOUT_L 0x46 #define GYRO_ZOUT_H 0x47 #define GYRO_ZOUT_L 0x48 #define PWR_MGMT_1 0x6B #define WHO_AM_I 0x75 #define SlaveAddress 0xD0 #define Address 0x68 //MPU6050地址 #define I2C_RETRIES 0x0701 #define I2C_TIMEOUT 0x0702 #define I2C_SLAVE 0x0703 //IIC裝置的位址設定 #define I2C_BUS_MODE 0x0780 #endif ``` ::: # 應用代碼 :::spoiler 打開應用程式碼 ```cpp! //test_app_while.c #include <stdio.h> #include <unistd.h> #include <fcntl.h> #include <string.h> #include <stdlib.h> int main(int argc, char *argv[]) { short resive_data[6]; // 儲存收到的 mpu6050 轉換結果數據,依序為 AX(x軸角度), AY, AZ 。 GX(x軸加速度), GY ,GZ /* 開啟文件 */ int fd = open("/dev/I2C1_mpu6050", O_RDWR); if (fd < 0) { printf("open file : %s failed !\n", argv[0]); return -1; } while (1) { /* 讀取數據 */ int error = read(fd, resive_data, 12); if (error < 0) { printf("read file error! \n"); close(fd); return -1; // 讀取失敗直接退出程序 } /* 列印數據*/ printf("AX=%d, AY=%d, AZ=%d ",(int)resive_data[0], (int)resive_data[1], (int)resive_data[2]); printf(" GX=%d, GY=%d, GZ=%d \n \n",(int)resive_data[3], (int)resive_data[4], (int)resive_data[5]); /* 延遲 1 秒*/ sleep(1); } /* 關閉文件 */ int error = close(fd); if (error < 0) { printf("close file error! \n"); } return 0; } ::: # 實驗部分 ```shell #建置 $ make #編譯 test_app_while $ arm-none-linux-gnueabihf-gcc test_app_while.c -o test_app_while #複製mpu6050.ko mpu6050Demo 到指定資料夾 $ cp -f ./i2c_mpu6050.ko test_app_while /home/alientek/linux/nfs/rootfs/lib/modules/5.4.31/ ``` - 加載模組 ```shell 確保在/lib/modules/5.4.31 目錄下 $depmod $modprobe i2c_mpu6050.ko ``` # 實驗結果 讀取訊號 ```shell $ ./test_app_while ``` [實驗影片連結](https://drive.google.com/file/d/1Ox9tSK5X3i-5WPTeUIk45z7dzCXmEwCY/view?usp=sharing) # 邏輯分析儀波形圖分析 [連結](https://hackmd.io/@a0979552111/r1t_aSx91l)