---
# System prepended metadata

title: 【embeded_linux】i2c 實驗
tags: [Embedded_linux]

---


:::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)




