芯片手册PDF可下载免费免登录:https://www.alldatasheetcn.com/
IIC协议
- I²C(Inter-Integrated Circuit)是一种常用的串行通信协议,用于集成电路之间的短距离数据传输。它由飞利浦公司发明,广泛应用于微控制器和外围设备之间的通信。
- 电路图结构
1.特点
- 双线设计
- SDA(Serial Data Line):串行数据线,用于在设备之间传输数据。
- SDA 代表“串行数据线”。其中“A”表示 Data 中的 “A”,因为 Data 是一个名词,SDA 体现的是数据传输。
- SCL(Serial Clock Line):串行时钟线,用于同步数据传输。
- SCL 代表“串行时钟线”。其中“L”表示 Clock Line 中的 “L”,因为 Line 表示线路,SCL 体现的是时钟信号传输。
- 主从架构
- I²C网络中可以有多个主设备和从设备。主设备负责生成时钟信号,并发起通信。从设备则响应主设备的请求。
- 地址识别
- 每个从设备有一个唯一的7位或10位地址。7位地址更常用,意味着总共有128个可用地址(0-127)。
- 双向数据传输
- 主设备可以向从设备发送数据,也可以从从设备接收数据。通信过程通过起始条件(START)、数据传输、停止条件(STOP)来控制。
- 速率
- 标准模式:100 kbps
- 快速模式:400 kbps
- 高速模式:3.4 Mbps(不常用)
- 总线仲裁
- 线仲裁机制确保多个主设备可以同时连接在同一条总线上,而不会产生数据冲突。总线仲裁是通过位仲裁和时钟同步来实现的
- 当多个主机想要启动通信时,它们都通过生成启动条件开始(将SDA线拉低)
- I²C 总线上的设备通过 SDA 线 来进行仲裁。I²C 总线采用 线与逻辑(即 有线与,Wired-AND)的方式,当多个主设备都试图控制总线时,如果有任何一个设备输出低电平(逻辑 0),总线上的 SDA 线就会保持低电平。
- 在 I²C 总线中,主设备在发送每一位数据时,都会同时监听 SDA 线上的状态。如果主设备发送的位是逻辑高(1),但检测到 SDA 线处于低电平(0),则表明总线上有另一个设备在发送低电平,仲裁失败
- 举例
- Master1 和 Master2 都发送了起始信号,它们都认为自己拥有总线的控制权。
- Master1 发送第一个数据位为 1,Master2 发送第一个数据位为 0。
- 由于总线是 线与逻辑(Wired-AND),SDA 线会保持为 0,因为逻辑 0 比 1 有更高的优先级。
- Master1 检测到 SDA 线的实际电平为 0(与自己发送的 1 不符),因此 Master1 知道它已经失去仲裁。
- Master2 检测到 SDA 线的电平为 0(与自己发送的 0 相符),因此 Master2 保留了总线的控制权,继续进行数据传输。
- Master1 将停止发送数据,等待总线空闲,再发起新的通信。
2.线路时序详解
- 初始状态均为高电平,是因为有两个相同的上拉电阻
1. 开始信号和结束信号
开始信号
- 在 I²C 通信中,开始信号(Start Condition) 是由主设备发出的,用于通知从设备即将开始数据传输。
- 当 SCL 线保持高电平时,主设备将 SDA 线从高电平拉至低电平(一个时钟周期)
- SCL和SDA均为高电平,SDA发生下降沿跳变,这个周期内
结束信号
- 结束信号(Stop Condition) 由主设备发出,用于通知从设备数据传输结束
- 在 SCL 线保持高电平时,主设备将 SDA 线从低电平拉至高电平(类似于开始信号,不过方向相反)
- 结束信号之后,I²C 总线被释放,允许其他设备使用
2.发送数据(主发送从)
- 数据有效只能在SCL处于高电平状态
- 发送数据1:SDA处于高电平,SCL处于高电平
- 发送数据1:SDA处于低电平,SCL处于高电平
- 发送数据之后会有返回值确认收到数据
- 每发送完8bit数据后,主设备放开SDA线,由于有上拉电阻的缘故,故SDA默认为低电平,所以此时SDA为低电平(SDA接地)表示确认收到,主设备会读取到这个二进制数。为高电平则表示无应答或发生错误。
- (8位数据+1位应答)
3.接收数据(从发主,也可以理解为主设备读数据)
- 主设备发送起始信号,从设备发送数据,主机应答。不接受了,则发送无应答(这个周期内,SDA处于高电平)。从设备收不到应答信号,便不会再发送数据。主机再发送停止信号。
4.第一次的8位数据
- 前7位:指定从设备的地址。I²C 总线支持多达 127 个从设备,每个设备有唯一的 7 位地址。
- 第 8 位(最低位):读/写位(R/W 位)
- 如果 R/W 位为 0,表示写操作(主设备将数据发送到从设备)。
- 如果 R/W 位为 1,表示读操作(主设备从从设备接收数据)。
5.第二个8bit数据
- 从设备的指针值(子地址或寄存器地址),指向希望访问的寄存器或内存地址。是起始位置,寄存器是相连在一起的,可以按顺序读写多个寄存器。
- 设置的指针值会被保存,下次读写从设置处开始(从设备断电或复位则失效)
- 例如:读写某个从设备的第4个寄存器的值,那么该八位数据位00000100
3.实际应用开发I2C(MPU6050)
- fs4412+MPU6050加速度
- ARM Cortex-A9 双核处理器,主频可达 1.4 GHz。
1.MPU6050简介
- MPU6050 是一款常用的六轴传感器,结合了三轴加速度计和三轴陀螺仪,广泛应用于运动跟踪、姿态控制、无人机、机器人等领域。
- MPU6050 使用 I²C 通信协议,与微控制器(如 Arduino、STM32 等)进行数据交换,支持多设备连接。
- MPU6050 在睡眠模式下功耗极低,适合便携设备。
- 内置温度传感器,可以用于环境温度监测。
MPU6050通信接口
-
寄存器只有8位
-
一般有多个地址,可以用来挂载多个相同的MPU
-
设置工作参数寄存器来控制加速度和陀螺仪传感器(读取范围)。读取加速度和陀螺仪传感器的数据。
主要参数
加速度计
- 量程选择:±2g、±4g、±8g、±16g
- 输出数据速率:可调,最高可达 1 kHz
陀螺仪
- 量程选择:±250°/s、±500°/s、±1000°/s、±2000°/s
- 输出数据速率:可调,最高可达 1 kHz
温度传感器
- 测量范围:-40°C 至 +85°C
- 输出:以°C 为单位的 16 位数字数据
配置与使用
- 连接:
将 MPU6050 连接到微控制器的 I²C 接口(SDA 和 SCL)以及电源(VCC 和 GND)。 - 初始化:
配置 MPU6050 的寄存器以设置工作模式、陀螺仪和加速度计的量程。 - 读取数据:
通过 I²C 协议读取加速度计、陀螺仪和温度传感器的数据。 - 数据处理:
根据需要处理原始数据,例如通过滤波算法(如卡尔曼滤波或互补滤波)计算物体的姿态。
具体代码(裸机开发)
- 标志位的自动置位:当I2C模块完成某个操作(如发送或接收一个字节),I2C硬件通常会自动设置一个中断标志位,表示该操作已经完成。
- 如果不清除中断标志位,硬件会认为当前的操作仍未完成,从而可能导致重复触发中断,影响下一步的操作。
#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 // 电源管理, 典型值: 0x00(正常启用)
#define SlaveAddress 0x68 // MPU6050-I2C地址
/************************延时函数************************/
void mydelay_ms(int time) {
int i, j;
while (time--) {
for (i = 0; i < 165; i++)
for (j = 0; j < 514; j++);
}
}
void iic_write (unsigned char slave_addr, unsigned char addr, unsigned char data) {
/* 对时钟源进行512倍频分频 打开IIC中断(每次完成一个字节的收发后中断标志位会自动置位) */
I2C5.I2CCON = I2C5.I2CCON | (1<<6) | (1<<5);
/* 设置ITC模式为主机发送模式 使能IIC发送和接收 */
I2C5.I2CSTAT = 0xd0;
/* 将第一个字节的数据写入发送寄存器 即从机地址和读/写(MPU6050-I2C地址占7位) */
I2C5.I2CDS = slave_addr << 1;
/* 清除中断标志位 发送起始信号后内部触发 使能IIC发送中断 */
I2C5.I2CSTAT = 0xf0;
/* 等待从机发来一个字节应答信号(应答后中断标志位自动置位) */
while (!(I2C5.I2CCON & (1<<4)));//如果第四位没有变成1则一直等待
/* 将要发送的第二个字节数据(即MPU6050内部寄存器的地址)写入发送寄存器 */
I2C5.I2CDS = addr;
/* 清除中断挂起标志位 开始下一个字节的发送 */
I2C5.I2CCON = I2C5.I2CCON &(~(1<<4));
/*等待从机接收完第一个字节后产生应答信号(应答后中断挂起位自动置位)*/
while (!(I2C5.I2CCON & (1<<4)));
/* 将要发送的第三个字节数据(即要写入MPU6050寄存器的数据)写入发送寄存器 */
I2C5.I2CDS = data;
/* 清除中断挂起标志位,开始下一个字节的发送 */
I2C5.I2CCON = I2C5.I2CCON & (~(1<<4));
/* 等待从机接受完一个字节后产生应答信号(应答后中断标志位自动置位) */
while (!(I2C5.I2CCON & (1<<4)));
/* 发送停止信号 结束本次通信 */
I2C5.I2CSTAT = 0xD0;
/* 清除中断挂起标志位,开始下一个字节的发送 */
I2C5.I2CCON = I2C5.I2CCON & (~(1<<4));
/*延时,等待停止信号发送完成*/
delay_ms(10);
}
unsigned char iic_read(unsigned char slave_addr, unsigned char addr)
{//返回值是读取的数
unsigned char data = 0;
/* 对时钟源进行512倍频分频 打开IIC中断(每次完成一个字节的收发后中断标志位会自动置位) */
I2C5.I2CCON = I2C5.I2CCON | (1<<6) | (1<<5);
/* 设置IIC模式为主机发送模式 使能IIC发送和接收 */
I2C5.I2CSTAT = 0xd0;
/* 将第一个字节的数据写入发送寄存器 即从机地址和读/写(MPU6050-I2C地址占7位) */
I2C5.I2CDS = slave_addr << 1;
/* 清除中断标志位 发送起始信号后内部触发 使能IIC发送中断 */
I2C5.I2CSTAT = 0xf0;
/* 等待从机接受完一个字节后产生应答信号(应答后中断标志位自动置位) */
while (!(I2C5.I2CCON & (1<<4)));
/* 将要发送的第二个字节数据(即要读取的MPU6050内部寄存器的地址)写入发送寄存器 */
I2C5.I2CDS = addr;
/* 清除中断标志位 开始下一个字节的发送 */
I2C5.I2CCON = I2C5.I2CCON & (~(1<<4));
/* 等待从机接受完一个字节后产生应答信号(应答后中断标志位自动置位) */
while (!(I2C5.I2CCON & (1<<4)));
/* 清除中断标志位 重新开始一次通信 改变数据传输方向 */
I2C5.I2CCON = I2C5.I2CCON & (~(1<<4));
/* 将第一个字节的数据写入发送寄存器 即从机地址和读/写(MPU6050-I2C地址+读位1) */
I2C5.I2CDS = slave_addr << 1 | 0x01;
/* 设置IIC为主机接收模式 发送起始信号 使能IIC收发 */
I2C5.I2CSTAT = 0xb0;
/* 等待从机发来数据应答信号 */
while (!(I2C5.I2CCON & (1<<4)));
/* 禁止主机应答信号(即开启非应答,因为只接收一个字节)清除中断标志位 */
I2C5.I2CCON = I2C5.I2CCON & (~(1<<7)) & (~(1<<4));
/* 等待从机发送来的数据 */
while (!(I2C5.I2CCON & (1<<4)));
/* 将从机发来的数据读出 */
data = I2C5.I2CDS;
/* 直接发送停止信号结束本次通信 */
I2C5.I2CSTAT = 0x90;
/* 清除中断挂起标志位 */
I2C5.I2CCON = I2C5.I2CCON & (~(1<<4));
/* 延时等待停止信号稳定 */
mydelay_ms(10);
return data;
}
/* 函数功能:MPU6050初始化 */
void MPU6050_Init() {
iic_write(SlaveAddress, PWR_MGMT_1, 0x00); // 设置使用内部时钟8M
iic_write(SlaveAddress, SMPLRT_DIV, 0x07); // 设置陀螺仪采样率
iic_write(SlaveAddress, CONFIG, 0x06); // 设置数字低通滤波器
//数字低通滤波器(DLPF)用于减少传感器数据中的高频噪声,平滑信号输出,提高测量精度,同时可以根据应用场景在响应速度和数据稳定性之间进行权衡。
iic_write(SlaveAddress, GYRO_CONFIG, 0x18); // 设置陀螺仪量程+-2000度/s
iic_write(SlaveAddress, ACCEL_CONFIG, 0x00); // 设置加速度量程+-2g
}
int main(void) {
unsigned char zvalue_h, zvalue_l; // 存储接收结果
short int zvalue;
/* 设置GPB_2引脚和GPB_3引脚功能为I2C传输引脚 */
GPB.CON = (GPB.CON & ~(0xF<<12)) | (0x3<<12); // 设置GPB_3引脚功能为I2C_SCL
GPB.CON = (GPB.CON & ~(0xF<<8)) | (0x3<<8); // 设置GPB_2引脚功能为I2C_SDA
// uart_init();
MPU6050_Init(); // 初始化I2C接口,初始化MPU6050
printf("\n********** I2C test!! **********\n");
while(1) {
zvalue_h = iic_read(SlaveAddress, GYRO_ZOUT_H); // 获取MPU6050-Z轴角速度高字节
zvalue_l = iic_read(SlaveAddress, GYRO_ZOUT_L); // 获取MPU6050-Z轴角速度低字节
zvalue = (zvalue_h<<8) | zvalue_l; // 获取MPU6050-Z轴角速度
printf("GYRO-Z : Hex: %d\n", zvalue); // 打印MPU6050-Z轴角速度
mydelay_ms(100);
}
return 0;
}
具体代码(驱动开发)
fs4412 MPU6050
//mpu6050.c
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <linux/i2c.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include "mpu6050reg.h" // 引入MPU6050的寄存器定义头文件
#define MPU6050_CNT 1 // 定义设备数量为1
#define MPU6050_NAME "mpu6050" // 定义设备名称
/* 定义MPU6050设备结构体,存储设备信息 */
struct mpu6050_dev {
dev_t devid; /* 设备号,标识字符设备的主设备号和次设备号 */
struct cdev cdev; /* 字符设备结构体,表示Linux内核中的字符设备 */
struct class *class; /* 类,用于在 `/sys/class` 中创建类信息 */
struct device *device; /* 设备,表示实际的设备节点 */
struct device_node *nd; /* 设备树节点,用于从设备树中读取设备信息 */
int major; /* 主设备号,标识设备 */
void *private_data; /* 私有数据,通常用于保存设备相关的信息 */
signed int gyro_x_adc; /* 陀螺仪X轴原始数据 */
signed int gyro_y_adc; /* 陀螺仪Y轴原始数据 */
signed int gyro_z_adc; /* 陀螺仪Z轴原始数据 */
signed int accel_x_adc; /* 加速度计X轴原始数据 */
signed int accel_y_adc; /* 加速度计Y轴原始数据 */
signed int accel_z_adc; /* 加速度计Z轴原始数据 */
signed int temp_adc; /* 温度原始数据 */
};
static struct mpu6050_dev mpu6050dev; // 定义一个MPU6050设备实例
/* 打开设备函数 */
static int mpu6050_open(struct inode *inode, struct file *filp)
{
filp->private_data = &mpu6050dev; /* 设置私有数据,将设备实例关联到文件操作 */
return 0;
}
/* 关闭设备函数 */
static int mpu6050_release(struct inode *inode, struct file *filp)
{
return 0; // 释放设备时无需特别操作,返回0表示成功
}
/* 写入多个寄存器的函数,通过I2C传输 */
static s32 mpu6050_write_regs(struct mpu6050_dev *dev, u8 reg, u8 *buf, u8 len)
{
u8 b[256]; // 定义一个缓冲区,用于存储要写入的寄存器地址和数据
struct i2c_msg msg; // 定义I2C消息结构体
struct i2c_client *client = (struct i2c_client *)dev->private_data; // 获取I2C客户端结构体
b[0] = reg; // 缓冲区的第一个字节是寄存器地址
memcpy(&b[1], buf, len); // 将要写入的数据拷贝到缓冲区
msg.addr = client->addr; // 设置I2C设备地址
msg.flags = 0; // 设置为写操作
msg.buf = b; // 设置消息的缓冲区指针
msg.len = len + 1; // 消息的总长度为寄存器地址+数据长度
return i2c_transfer(client->adapter, &msg, 1); // 发送I2C消息
}
/* 写入单个寄存器的函数 */
static void mpu6050_write_onereg(struct mpu6050_dev *dev, u8 reg, u8 data)
{
u8 buf = data; // 定义缓冲区,将单字节数据存入
mpu6050_write_regs(dev, reg, &buf, 1); // 调用写多个寄存器的函数,但只写一个字节
}
/* 读取多个寄存器的函数 */
static int mpu6050_read_regs(struct mpu6050_dev *dev, u8 reg, void *val, int len)
{
int ret; // 用于存储返回值
struct i2c_msg msg[2]; // 定义两个I2C消息结构体
struct i2c_client *client = (struct i2c_client *)dev->private_data; // 获取I2C客户端结构体
/* 第一个msg是写操作,发送要读取的寄存器地址 */
msg[0].addr = client->addr; // 设置I2C设备地址
msg[0].flags = 0; // 设置为写操作
msg[0].buf = ® // 设置寄存器地址缓冲区
msg[0].len = 1; // 设置寄存器地址长度为1
/* 第二个msg是读操作,读取数据 */
msg[1].addr = client->addr; // 设置I2C设备地址
msg[1].flags = I2C_M_RD; // 设置为读操作
msg[1].buf = val; // 设置读取数据的缓冲区
msg[1].len = len; // 设置要读取的数据长度
ret = i2c_transfer(client->adapter, msg, 2); // 发送I2C读写消息
if(ret == 2) { // 返回2表示成功完成读写
ret = 0; // 成功
} else {
printk("i2c rd failed=%d reg=%06x len=%d\n", ret, reg, len); // 打印错误信息
ret = -EREMOTEIO; // 返回远程I/O错误
}
return ret; // 返回结果
}
/* 读取单个寄存器的函数 */
static unsigned char mpu6050_read_onereg(struct mpu6050_dev *dev, u8 reg)
{
u8 data = 0; // 用于存储读取到的数据
mpu6050_read_regs(dev, reg, &data, 1); // 调用读取多个寄存器的函数,但只读取一个字节
return data; // 返回读取到的单字节数据
}
/* 从MPU6050中读取传感器数据(加速度、陀螺仪、温度) */
void mpu6050_readdata(struct mpu6050_dev *dev)
{
unsigned char data[14]; // 定义缓冲区,用于存储读取的14字节数据
mpu6050_read_regs(dev, MPU6050_RA_ACCEL_XOUT_H, data, 14); // 读取14字节的数据,包含加速度、温度和陀螺仪
/* 将读取到的各个轴的数据转换为有符号16位整数 */
dev->accel_x_adc = (signed short)((data[0] << 8) | data[1]); // 加速度X轴
dev->accel_y_adc = (signed short)((data[2] << 8) | data[3]); // 加速度Y轴
dev->accel_z_adc = (signed short)((data[4] << 8) | data[5]); // 加速度Z轴
dev->temp_adc = (signed short)((data[6] << 8) | data[7]); // 温度
dev->gyro_x_adc = (signed short)((data[8] << 8) | data[9]); // 陀螺仪X轴
dev->gyro_y_adc = (signed short)((data[10] << 8) | data[11]); // 陀螺仪Y轴
dev->gyro_z_adc = (signed short)((data[12] << 8) | data[13]); // 陀螺仪Z轴
}
/* 读取设备文件的函数 */
static ssize_t mpu6050_read(struct file *filp, char __user *buf, size_t cnt, loff_t *off)
{
signed int data[7]; // 定义一个数组,存储陀螺仪、加速度计和温度的数据
long err = 0; // 错误标志
struct mpu6050_dev *dev = (struct mpu6050_dev *)filp->private_data; // 获取设备实例
mpu6050_readdata(dev); // 从MPU6050中读取数据
/* 将各个数据存入数组 */
data[0] = dev->gyro_x_adc;
data[1] = dev->gyro_y_adc;
data[2] = dev->gyro_z_adc;
data[3] = dev->accel_x_adc;
data[4] = dev->accel_y_adc;
data[5] = dev->accel_z_adc;
data[6] = dev->temp_adc;
err = copy_to_user(buf, data, sizeof(data)); // 将数据拷贝到用户空间
return 0; // 返回0表示读取成功
}
/* mpu6050操作函数集 */
static const struct file_operations mpu6050_ops = {
.owner = THIS_MODULE, // 指定模块的拥有者
.open = mpu6050_open, // 打开设备文件时调用的函数
.read = mpu6050_read, // 读取设备文件时调用的函数
.release = mpu6050_release, // 关闭设备文件时调用的函数
};
/* 初始化MPU6050寄存器 */
void mpu6050_reginit(void)
{
u8 value = 0; // 用于存储读取到的设备ID
/* 复位MPU6050 */
mpu6050_write_onereg(&mpu6050dev, MPU6050_RA_PWR_MGMT_1, 0x80); // 写入复位命令
mdelay(50); // 延时50ms等待复位完成
mpu6050_write_onereg(&mpu6050dev, MPU6050_RA_PWR_MGMT_1, 0x01); // 设置时钟源
mdelay(50); // 延时50ms
/* 读取MPU6050的设备ID */
value = mpu6050_read_onereg(&mpu6050dev, MPU6050_RA_WHO_AM_I); // 读取WHO_AM_I寄存器,获取设备ID
printk("mpu6050 ID = %#X\r\n", value); // 打印设备ID
/* 配置传感器参数 */
mpu6050_write_onereg(&mpu6050dev, MPU6050_RA_SMPLRT_DIV, 0x00); // 设置采样率为最高
mpu6050_write_onereg(&mpu6050dev, MPU6050_RA_GYRO_CONFIG, 0x18); // 设置陀螺仪量程为±2000dps
mpu6050_write_onereg(&mpu6050dev, MPU6050_RA_ACCEL_CONFIG, 0x18); // 设置加速度计量程为±16G
mpu6050_write_onereg(&mpu6050dev, MPU6050_RA_CONFIG, 0x04); // 设置陀螺仪低通滤波
mpu6050_write_onereg(&mpu6050dev, MPU6050_RA_PWR_MGMT_2, 0x00); // 启用所有轴
mpu6050_write_onereg(&mpu6050dev, MPU6050_RA_FIFO_EN, 0x00); // 关闭FIFO
}
/* 设备探测函数
设备探测函数(probe 函数)在 Linux 设备驱动程序中起着非常重要的作用。它的主要目的是在设备被检测到时执行初始化操作,并将设备与驱动程序绑定。*/
static int mpu6050_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
int ret = 0; // 用于存储函数返回值
/* 1、构建设备号 */
if (mpu6050dev.major) { // 如果已经指定了主设备号 先判断
mpu6050dev.devid = MKDEV(mpu6050dev.major, 0); // 创建设备号
register_chrdev_region(mpu6050dev.devid, MPU6050_CNT, MPU6050_NAME); // 注册设备号
} else {
alloc_chrdev_region(&mpu6050dev.devid, 0, MPU6050_CNT, MPU6050_NAME); // 分配设备号
mpu6050dev.major = MAJOR(mpu6050dev.devid); // 获取主设备号
}
/* 2、注册设备 */
cdev_init(&mpu6050dev.cdev, &mpu6050_ops); // 初始化字符设备
cdev_add(&mpu6050dev.cdev, mpu6050dev.devid, MPU6050_CNT); // 添加字符设备
/* 3、创建类 */
mpu6050dev.class = class_create(THIS_MODULE, MPU6050_NAME); // 创建类
if (IS_ERR(mpu6050dev.class)) { // 检查类创建是否成功
return PTR_ERR(mpu6050dev.class); // 返回错误
}
/* 4、创建设备 */
mpu6050dev.device = device_create(mpu6050dev.class, NULL, mpu6050dev.devid, NULL, MPU6050_NAME); // 创建设备文件
if (IS_ERR(mpu6050dev.device)) { // 检查设备创建是否成功
return PTR_ERR(mpu6050dev.device); // 返回错误
}
mpu6050dev.private_data = client; /* 保存I2C客户端数据 */
mpu6050_reginit(); /* 初始化MPU6050寄存器 */
return 0; // 返回0表示探测成功
}
/* 设备移除函数 */
static int mpu6050_remove(struct i2c_client *client)
{
/* 注销设备 */
cdev_del(&mpu6050dev.cdev); // 删除字符设备
unregister_chrdev_region(mpu6050dev.devid, MPU6050_CNT); // 注销设备号
/* 注销类和设备 */
device_destroy(mpu6050dev.class, mpu6050dev.devid); // 销毁设备文件
class_destroy(mpu6050dev.class); // 销毁类
return 0;
}
/* 传统的ID匹配表 */
static const struct i2c_device_id mpu6050_id[] = {
{"dar,mpu6050", 0}, // 匹配设备ID
{}
};
/* 设备树匹配表 */
static const struct of_device_id mpu6050_of_match[] = {
{ .compatible = "dar,mpu6050" }, // 匹配设备树的compatible字段
{ /* Sentinel */ }
};
/* I2C驱动结构体 */
static struct i2c_driver mpu6050_driver = {
.probe = mpu6050_probe, // 设备探测函数
.remove = mpu6050_remove, // 设备移除函数
.driver = {
.owner = THIS_MODULE, // 模块拥有者
.name = "mpu6050", // 驱动名称
.of_match_table = mpu6050_of_match, // 设备树匹配表
},
.id_table = mpu6050_id, // 设备ID表
};
/* 驱动初始化 */
static int __init mpu6050_init(void)
{
int ret = 0;
ret = i2c_add_driver(&mpu6050_driver); // 注册I2C驱动
return ret;
}
/* 驱动退出 */
static void __exit mpu6050_exit(void)
{
i2c_del_driver(&mpu6050_driver); // 注销I2C驱动
}
module_init(mpu6050_init); // 注册模块初始化函数
module_exit(mpu6050_exit); // 注册模块退出函数
MODULE_LICENSE("GPL"); // 声明GPL协议
MODULE_AUTHOR("darboy"); // 模块作者
//mpu6050.h 资源文件里面
//mpu6050Demo.c
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "sys/ioctl.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
#include <poll.h>
#include <sys/select.h>
#include <sys/time.h>
#include <signal.h>
#include <fcntl.h>
int main(int argc, char *argv[])
{
int fd;
signed int buffer[7];
signed int gyro_x_adc, gyro_y_adc, gyro_z_adc;
signed int accel_x_adc, accel_y_adc, accel_z_adc;
signed int temp_adc;
float gyro_x_act, gyro_y_act, gyro_z_act;
float accel_x_act, accel_y_act, accel_z_act;
float temp_act;
int ret = 0;
char* filename = "/dev/mpu6050";
fd = open(filename, O_RDWR);
if(fd < 0) {
printf("can't open file %s\r\n", filename);
return -1;
}
while (1) {
ret = read(fd, buffer, sizeof(buffer));
if(ret == 0) {
gyro_x_adc = buffer[0];
gyro_y_adc = buffer[1];
gyro_z_adc = buffer[2];
accel_x_adc = buffer[3];
accel_y_adc = buffer[4];
accel_z_adc = buffer[5];
temp_adc = buffer[6];
gyro_x_act = (float)(gyro_x_adc) / 16.4;
gyro_y_act = (float)(gyro_y_adc) / 16.4;
gyro_z_act = (float)(gyro_z_adc) / 16.4;
accel_x_act = (float)(accel_x_adc) / 2048;
accel_y_act = (float)(accel_y_adc) / 2048;
accel_z_act = (float)(accel_z_adc) / 2048;
temp_act = ((float)(temp_adc) - 25 ) / 326.8 + 25;
printf("\r\nADC value:\r\n");
printf("gx = %d, gy = %d, gz = %d\r\n", gyro_x_adc, gyro_y_adc, gyro_z_adc);
printf("ax = %d, ay = %d, az = %d\r\n", accel_x_adc, accel_y_adc, accel_z_adc);
printf("temp = %d\r\n", temp_adc);
printf("TRUE value:");
printf("act gx = %.2f°/S, act gy = %.2f°/S, act gz = %.2f°/S\r\n", gyro_x_act, gyro_y_act, gyro_z_act);
printf("act ax = %.2fg, act ay = %.2fg, act az = %.2fg\r\n", accel_x_act, accel_y_act, accel_z_act);
printf("act temp = %.2f°C\r\n", temp_act);
}
usleep(100000);
}
close(fd);
return 0;
}
//在对应的开发板上的IIC总线上添加mpu6050,并使用交叉编译工具链
//传入即可。加入驱动
SPI协议
- SPI(Serial Peripheral Interface,串行外设接口)是一种同步串行通信协议,常用于微控制器与外围设备(如传感器、存储器和显示器)之间的数据传输。相比于 I²C,SPI 具有更高的数据传输速度,并且更适合需要快速通信的应用场景。
- 一般用SD卡,FLASH,EEPROM,液晶显示器。传输距离短
1.特点
- 高速传输:SPI 支持较高的数据传输速率,可以达到数十兆赫兹,适合高速数据通信。
- 全双工通信:支持同时发送和接收数据,效率较高。
- 使用SPI通信最少需要3根线
- 主从结构:包含一个主设备和一个或多个从设备,主设备控制时钟信号,并选择要通信的从设备。
- 多从设备支持:可以通过独立的 SS/CS 线连接多个从设备,便于扩展,通常只需要再添加一条新的 SS/CS 线来控制新从设备的选择状态。
- 主要信号线
- MOSI(Master Out, Slave In):主设备输出、从设备输入,用于主设备发送数据到从设备。
- MISO(Master In, Slave Out):主设备输入、从设备输出,用于从设备发送数据到主设备。
- SCK(Serial Clock):串行时钟,由主设备生成,用于同步数据传输。
- SS/CS(Slave Select/Chip Select):从设备选择线,通常由主设备控制,用于选择特定的从设备进行通信。低电平有效,表示选定的从设备处于活动状态。
- 工作模式
- 主要由时钟极性(CPOL)和时钟相位(CPHA)决定
- CPOL(Clock Polarity):决定空闲时钟状态是高电平还是低电平。
- CPHA(Clock Phase):决定数据在时钟的上升沿或下降沿采样。
- CPHA=0,即表示输出(out)端在上一个时钟周期的后沿改变数据,而输入(in)端在时钟周期的前沿(或不久之后)捕获数据。输出端保持数据有效直到当前时钟周期的尾部边缘。对于第一个时钟周期来说,第一位的数据必须在时钟前沿之前出现在MOSI线上。也就是一个CPHA=0的周期包括半个时钟空闲和半个时钟置位的周期
- 在 SPI 通信的 CPHA=1 模式下,主设备在时钟信号的上升沿改变数据,而从设备在时钟的下降沿采集数据。这意味着,主设备必须在时钟的上升沿之前准备好要发送的数据,并在这个时刻将数据在线路上有效。在这种模式下,一个时钟周期同样包括半个时钟空闲时间和半个时钟数据有效时间,但数据变化和采集的时机相对于 CPHA=0 模式有所不同。这确保了数据的稳定性,使从设备能够在适当的时机可靠地接收数据。
模式 | CPOL | CPHA | 空闲状态 | 数据采样时机 | 数据变化时机 |
---|---|---|---|---|---|
模式 0 | 0 | 0 | 低电平 | 上升沿 | 下降沿 |
模式 1 | 0 | 1 | 低电平 | 下降沿 | 上升沿 |
模式 2 | 1 | 0 | 高电平 | 上升沿 | 下降沿 |
模式 3 | 1 | 1 | 高电平 | 下降沿 | 上升沿 |
2.时序图部分
- 不同的模式,其实就是SCLK空闲时间电平状态和数据采样起点不同
CPOL=0(时钟空闲时为低电平)
CPHA=0:数据采样发生在上升沿(低电平到高电平的跳变),数据在下降沿(高电平到低电平的跳变)更新。
CPHA=1:数据采样发生在下降沿,数据在上升沿更新。
CPOL=1(时钟空闲时为高电平)
CPHA=0:数据采样发生在下降沿(高电平到低电平的跳变),数据在上升沿(低电平到高电平的跳变)更新。
CPHA=1:数据采样发生在上升沿,数据在下降沿更新。
3.通信原理
- 选择从设备
- 主设备通过将 CS/SS(片选信号)拉低,选择特定的从设备,启动通信。
- 发送时钟信号
- 主设备发送 SCK(时钟信号),控制从设备进行写入或读取操作。数据的采集时机取决于所用的 SPI 模式,它将立即读取数据线上的信号,这样就得到了一位数据(1bit)。
- 数据发送与接收
- 主设备将要发送的数据写入发送数据缓存区(移位寄存器),通过 MOSI(主设备输出,从设备输入)信号线一位一位地发送给从设备
- 与此同时,从设备通过 MISO(主设备输入,从设备输出)信号线发送其数据,数据同样通过其移位寄存器一位一位地移出
- 全双工通信
SPI 实现全双工通信,主设备和从设备可以同时发送和接收数据。每当主设备发送一个 SCLK(时钟)信号周期时,都会有 1bit 数据通过 MOSI 发送到从设备,同时从设备也通过 MISO 发送 1bit 数据给主设备
4.SPI协议实际应用(STM32f103读写flash(W25Q64BV))
通过HAL_GPIO_WritePin
函数选择芯片
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET);// 选择从设备 1(拉低 CS1 引脚)
HAL库中SPI函数
HAL_SPI_Transmit():通过 SPI 接口发送数据
HAL_StatusTypeDef HAL_SPI_Transmit(SPI_HandleTypeDef *hspi,
uint8_t *pData,
uint16_t Size,
uint32_t Timeout);
参数:
SPI_HandleTypeDef *hspi:指向 SPI 句柄结构体的指针。这个结构体包含了 SPI 外设的配置信息,在初始化时设置。
uint8_t *pData:指向要发送的数据的指针。这个指针指向一个包含要通过 SPI 发送的数据的缓冲区。
uint16_t Size:要发送的数据量,单位为字节。表示缓冲区中的数据大小(即要发送的数据量)。
uint32_t Timeout:操作的超时时间(以毫秒为单位)。如果在指定的时间内发送操作没有完成,函数将返回超时错误。
返回值:
HAL_StatusTypeDef 是一个枚举类型,表示函数执行的状态。它的值可以是以下之一:
HAL_OK:操作成功完成。
HAL_ERROR:发生错误。
HAL_BUSY:SPI 外设忙,无法执行该操作。
HAL_TIMEOUT:操作超时。
举例如下
- SPI 协议中的数据传输单位是“帧”,而每个帧的大小可以是 8 位 或 16 位,甚至其他大小,具体取决于 SPI 外设的配置。
#include "stm32f4xx_hal.h"
SPI_HandleTypeDef hspi1; // 假设你已经初始化了 SPI1
//在调用 HAL_SPI_Transmit() 之前,必须先初始化 SPI 外设。
//通常通过 HAL_SPI_Init() 函数进行初始化。
void SPI_SendData(void)
{
uint8_t data[] = {0xAA, 0xBB, 0xCC}; // 要发送的数据
HAL_StatusTypeDef result;
// 发送数据,通过 SPI1,发送3字节数据,超时时间为100ms
result = HAL_SPI_Transmit(&hspi1, data, sizeof(data), 100);
// 检查发送结果
if (result == HAL_OK) {
// 数据发送成功
} else if (result == HAL_TIMEOUT) {
// 处理超时
} else {
// 处理其他错误
}
}
HAL_SPI_Receive(通过 SPI 接口接受数据)
HAL_StatusTypeDef HAL_SPI_Receive(SPI_HandleTypeDef *hspi,
uint8_t *pData,
uint16_t Size,
uint32_t Timeout);
参数:
SPI_HandleTypeDef *hspi:指向 SPI 句柄的指针。这个句柄结构体包含了 SPI 外设的相关配置信息,通常在初始化 SPI 时设置。
uint8_t *pData:指向用于存放接收数据的缓冲区的指针。数据将被接收到该缓冲区。
uint16_t Size:要接收的数据量,单位是字节。表示你想通过 SPI 接收的字节数。
uint32_t Timeout:操作的超时时间(以毫秒为单位)。如果在指定的时间内未完成接收操作,函数将返回超时错误。
返回值:
HAL_StatusTypeDef 是一个枚举类型,表示函数执行的状态。可能的返回值有:
HAL_OK:操作成功。
HAL_ERROR:发生了错误。
HAL_BUSY:SPI 外设忙,无法执行该操作。
HAL_TIMEOUT:接收操作超时。
举例说明
#include "stm32f4xx_hal.h"
SPI_HandleTypeDef hspi1; // SPI1 句柄
void SPI_ReceiveData(void)
{
uint8_t received_data[3]; // 用于存储接收到的数据
//赋初值有助于在出错时诊断问题
//uint8_t received_data[3] = {0};
HAL_StatusTypeDef result;
// 从从设备接收 3 字节数据,超时时间为1000毫秒
result = HAL_SPI_Receive(&hspi1, received_data, sizeof(received_data), 1000);
// 检查接收结果
if (result == HAL_OK) {
// 数据接收成功,可以处理 received_data 数组
} else if (result == HAL_TIMEOUT) {
// 处理超时错误
} else {
// 处理其他错误
}
}
HAL_SPI_TransmitReceive()(通过 SPI 接口 同时发送和接收数据。)
HAL_StatusTypeDef HAL_SPI_TransmitReceive(SPI_HandleTypeDef *hspi,
uint8_t *pTxData,
uint8_t *pRxData,
uint16_t Size,
uint32_t Timeout);
参数:
SPI_HandleTypeDef *hspi:指向 SPI 句柄的指针。这个句柄结构体包含了 SPI 外设的配置和状态信息,通常在 SPI 初始化时设置。
uint8_t *pTxData:指向发送数据的缓冲区的指针。这个缓冲区包含主设备要通过 SPI 发送到从设备的数据。
uint8_t *pRxData:指向接收数据的缓冲区的指针。SPI 通信期间,从设备发送的数据将存储到这个缓冲区。
uint16_t Size:要发送和接收的数据量,单位是字节。此参数决定发送和接收的数据的长度,发送和接收的数据量必须相同。
uint32_t Timeout:操作的超时时间,单位为毫秒。如果在指定的时间内操作没有完成,函数将返回超时错误。
返回值:
HAL_StatusTypeDef 是一个枚举类型,表示函数执行的状态。可能的返回值包括:
HAL_OK:操作成功完成。
HAL_ERROR:发生错误。
HAL_BUSY:SPI 外设忙,无法执行该操作。
HAL_TIMEOUT:操作超时。
举例说明
#include "stm32f4xx_hal.h"
SPI_HandleTypeDef hspi1; // SPI1 句柄
void SPI_TransmitReceiveData(void)
{
uint8_t tx_data[3] = {0xAA, 0xBB, 0xCC}; // 要发送的数据
uint8_t rx_data[3]; // 接收数据的缓冲区
HAL_StatusTypeDef result;
// 通过 SPI 同时发送和接收 3 字节的数据,超时时间为1000毫秒
result = HAL_SPI_TransmitReceive(&hspi1, tx_data, rx_data, sizeof(tx_data), 1000);
// 检查结果
if (result == HAL_OK) {
// 成功发送和接收,rx_data 现在包含从设备发送的数据
} else if (result == HAL_TIMEOUT) {
// 处理超时错误
} else {
// 处理其他错误
}
}
FLASH芯片写入读出
- 容量(芯片)—>块(通常是 64KB)—>扇区(通常是4kB)—>页(通常是256byte)
- 写入过程
- 写使能(解锁)
- 写入之前要先擦除(最小扇区)
- 延迟100ms
- 写使能
- 写入的最小的单元(最小页)//写入的数据不能跨页。如果你需要写入的数据超过一个页的大小,必须分多个页进行写入。
- 延迟100ms
- 写使能标志(WEL) 一般会在完成一次写操作或擦除操作之后自动复位。这是一种保护机制,防止意外的写操作损坏数据。因此,每次进行写入或擦除之前,都需要重新发送写使能命令。
- 根据芯片手册写东西
//写使能
uint8_t writeEnableCmd[] = {0x06}; // 定义一个字节数组,存储写使能命令 0x06
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); // 拉低片选信号 (CS 引脚)
HAL_SPI_Transmit(&hspi1, writeEnableCmd, 1, HAL_MAX_DELAY); // 通过 SPI 发送写使能命令
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); // 拉高片选信号 (CS 引脚)
//扇区擦除
uint8_t sectorEraseCmd[] = {0x20,0x00,0x00,0x00}; // 定义一个字节数组,存储擦除命令 0x20
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); // 拉低片选信号 (CS 引脚)
HAL_SPI_Transmit(&hspi1, sectorEraseCmd, 4, HAL_MAX_DELAY); // 通过 SPI 发送擦除命令
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); // 拉高片选信号 (CS 引脚)
//页编程
uint8_t pageProgCmd[5]; // 定义一个数组用于存储页编程命令和数据
pageProgCmd[0] = 0x02; // 页编程命令 (0x02)
// 一般用于 Flash 存储器的页写入操作
// 设置 24 位地址,假设这里的地址为 0x000000
pageProgCmd[1] = 0x00; // 地址的高字节(24 位地址的第一个字节)
pageProgCmd[2] = 0x00; // 地址的中间字节
pageProgCmd[3] = 0x00; // 地址的低字节
// 要写入的数据,假设要写入 LED 的状态
pageProgCmd[4] = ledState; // 将 LED 的状态(0 或 1)写入到要写入的数据中
// 选中 SPI 外设的从设备,拉低 CS 引脚
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET);
// 通过 SPI 发送页编程命令和数据,共 5 字节:1 字节命令 + 3 字节地址 + 1 字节数据
HAL_SPI_Transmit(&hspi1, pageProgCmd, 5, HAL_MAX_DELAY);
// 传输完成后,拉高 CS 引脚,结束通信
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);
//读编程
static uint8_t LoadLEDState(void)
{
uint8_t readDataCmd[] = {0x03, 0x00, 0x00, 0x00}; // 读取数据命令 + 24位地址
uint8_t ledState; // 用于存储读取的 LED 状态
// 1. 拉低 CS 引脚,开始通信
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET);
// 2. 发送读取数据命令和地址(共4字节)
HAL_SPI_Transmit(&hspi1, readDataCmd, 4, HAL_MAX_DELAY);
// 3. 接收 1 字节的数据(即 LED 状态)
HAL_SPI_Receive(&hspi1, &ledState, 1, HAL_MAX_DELAY);
// 4. 拉高 CS 引脚,结束通信
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);
// 5. 返回读取到的 LED 状态
return ledState;
}