I2C和SPI协议

芯片手册PDF可下载免费免登录:https://www.alldatasheetcn.com/

IIC协议

  • I²C(Inter-Integrated Circuit)是一种常用的串行通信协议,用于集成电路之间的短距离数据传输。它由飞利浦公司发明,广泛应用于微控制器和外围设备之间的通信。
  • 电路图结构
    在这里插入图片描述

1.特点

  1. 双线设计
  • SDA(Serial Data Line):串行数据线,用于在设备之间传输数据。
    • SDA 代表“串行数据线”。其中“A”表示 Data 中的 “A”,因为 Data 是一个名词,SDA 体现的是数据传输。
  • SCL(Serial Clock Line):串行时钟线,用于同步数据传输。
    • SCL 代表“串行时钟线”。其中“L”表示 Clock Line 中的 “L”,因为 Line 表示线路,SCL 体现的是时钟信号传输。
  1. 主从架构
  • I²C网络中可以有多个主设备和从设备。主设备负责生成时钟信号,并发起通信。从设备则响应主设备的请求。
  1. 地址识别
  • 每个从设备有一个唯一的7位或10位地址。7位地址更常用,意味着总共有128个可用地址(0-127)。
  1. 双向数据传输
  • 主设备可以向从设备发送数据,也可以从从设备接收数据。通信过程通过起始条件(START)、数据传输、停止条件(STOP)来控制。
  1. 速率
  • 标准模式:100 kbps
  • 快速模式:400 kbps
  • 高速模式:3.4 Mbps(不常用)
  1. 总线仲裁
  • 线仲裁机制确保多个主设备可以同时连接在同一条总线上,而不会产生数据冲突。总线仲裁是通过位仲裁和时钟同步来实现的
  • 当多个主机想要启动通信时,它们都通过生成启动条件开始(将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 位数字数据
配置与使用
  1. 连接:
    将 MPU6050 连接到微控制器的 I²C 接口(SDA 和 SCL)以及电源(VCC 和 GND)。
  2. 初始化:
    配置 MPU6050 的寄存器以设置工作模式、陀螺仪和加速度计的量程。
  3. 读取数据:
    通过 I²C 协议读取加速度计、陀螺仪和温度传感器的数据。
  4. 数据处理:
    根据需要处理原始数据,例如通过滤波算法(如卡尔曼滤波或互补滤波)计算物体的姿态。
具体代码(裸机开发)
  • 标志位的自动置位:当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 = &reg;                   // 设置寄存器地址缓冲区
	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 模式有所不同。这确保了数据的稳定性,使从设备能够在适当的时机可靠地接收数据。
        在这里插入图片描述
模式CPOLCPHA空闲状态数据采样时机数据变化时机
模式 000低电平上升沿下降沿
模式 101低电平下降沿上升沿
模式 210高电平上升沿下降沿
模式 311高电平下降沿上升沿

2.时序图部分

  • 不同的模式,其实就是SCLK空闲时间电平状态和数据采样起点不同

CPOL=0(时钟空闲时为低电平)

CPHA=0:数据采样发生在上升沿(低电平到高电平的跳变),数据在下降沿(高电平到低电平的跳变)更新。
在这里插入图片描述
CPHA=1:数据采样发生在下降沿,数据在上升沿更新。
在这里插入图片描述

CPOL=1(时钟空闲时为高电平)

CPHA=0:数据采样发生在下降沿(高电平到低电平的跳变),数据在上升沿(低电平到高电平的跳变)更新。
在这里插入图片描述
CPHA=1:数据采样发生在上升沿,数据在下降沿更新。
在这里插入图片描述

3.通信原理

  1. 选择从设备
    • 主设备通过将 CS/SS(片选信号)拉低,选择特定的从设备,启动通信。
  2. 发送时钟信号
    • 主设备发送 SCK(时钟信号),控制从设备进行写入或读取操作。数据的采集时机取决于所用的 SPI 模式,它将立即读取数据线上的信号,这样就得到了一位数据(1bit)。
  3. 数据发送与接收
    • 主设备将要发送的数据写入发送数据缓存区(移位寄存器),通过 MOSI(主设备输出,从设备输入)信号线一位一位地发送给从设备
    • 与此同时,从设备通过 MISO(主设备输入,从设备输出)信号线发送其数据,数据同样通过其移位寄存器一位一位地移出
      在这里插入图片描述
  4. 全双工通信
    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) 一般会在完成一次写操作或擦除操作之后自动复位。这是一种保护机制,防止意外的写操作损坏数据。因此,每次进行写入或擦除之前,都需要重新发送写使能命令。
  • 根据芯片手册写东西
    W25Q64BV芯片手册
//写使能
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;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值