:::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傳輸數據。
底下是晶片和腳位圖
|  |  |
|:----------------------------------------------------:|:----------------------------------------------------:|
:::info
在 `STM32P157Mini` 開發板上並沒有內建 `MPU6050` 這顆晶片,因此,我們需要先確認開發板上尚未被使用的可用引腳。接著,在檢查 `stm32mp15-pinctrl.dtsi` 檔案時,確認是否有適合用於 `I2C2` 的引腳,**最終決定使用 `PH4` 和 `PH5` 作為連接引腳**。
:::
|  |  |
|:--------------------------------------------------------------:|:-------------------------------------------------------:|
# 完整接線圖

| 編號 | 名稱 | 描述 |
|------|---------------|----------------------------------------------------------------------|
| 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`查看設備樹

如果查看`0-0068` 內部的`name` 檔案可以看到`i2c_mpu6050`

:::
# 編寫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)