STM32单片机-通信协议(下)

news2024/11/20 16:32:00

STM32单片机-通信协议(下)

  • 一、通信协议介绍
  • 二、USART(通用同步/异步收发器)
    • 2.1 USART框图和基本结构
    • 2.2 串口发送
      • 2.2.1 Printf函数移植
      • 2.2.2 串口发送汉字
    • 2.3 串口接收
      • 2.3.1 串口接收+查询
      • 2.3.2 串口接收+中断
    • 2.4 USART串口数据包
      • 2.4.1 数据包格式
      • 2.4.2 数据包接收流程
      • 2.4.3 串口收发Hex数据包
      • 2.4.4 串口收发文本数据包
  • 三、I2C通信
    • 3.1 I2C介绍
    • 3.2 I2C数据帧
    • 3.3 I2C驱动代码
    • 3.4 MPU6050姿态传感器
    • 3.5 软件I2C读取MPU6050
  • 四、I2C外设
    • 4.1 I2C外设介绍
    • 4.2 I2C操作流程
      • 4.2.1 主机发送
      • 4.2.2 主机接收
    • 4.3 硬件I2C读取MPU6050
  • 五、SPI通信
    • 5.1 SPI通信介绍
    • 5.2 SPI时序基本单元
    • 5.3 SPI驱动代码
    • 5.4 W25Q64存储器
    • 5.5 软件SPI读写W25Q64
  • 六、SPI外设
    • 6.1 SPI外设介绍
    • 6.2 SPI基本结构和时序
    • 6.3 硬件SPI读写W25Q64

本篇文章是51系列单片机通信协议的后续,具体内容见如下跳转

一、通信协议介绍

  本节只补充上篇没有的内容
  通信目的:将一个设备数据传送到另一个设备,扩展硬件系统
  通信协议:指定通信的规则,通信双方按照协议规则进行数据收发

在这里插入图片描述

  USART就是51系列单片机的UART,即使用串口的外设
  单端和差分信号是电平的差距,单端信号通信的双方必须要共地,差分信号是靠两个差分信号的电压差来传输信号

二、USART(通用同步/异步收发器)

  • USART是STM32内部集成的硬件外设,可根据数据寄存器的一个字节数据自动生成数据帧时序,从TX引脚发送出去,也可以自动接收RX引脚的数据帧时序,拼接为一个字节数据,存放在数据寄存器
  • 自带波特率发生器(分频器),最高可达4.5Mbits/s
  • 可配置数据位长度(8/9)、停止位长度(0.5/1/1.5/2)
  • 可选校验位(无校验/奇校验/偶校验)
  • 支持同步模式、硬件流控制、DMA、智能卡、IrDA、LIN

  STM32F103C8T6 USART资源:USART1USART2USART3

2.1 USART框图和基本结构

  下图为USART的框图
  左边TX、RX、SW_RX等是USART的引脚部分,TX接到了发送移位寄存器发送数据寄存器(TDR)RX接到了接收移位寄存器接收数据寄存器(RDR)。移位寄存器就是把数据一位一位的移出去,对应着串口协议的波形数据位数据寄存器是存放发送的数据字节或者接收的数据字节,程序上表现为一个寄存器DR,实际上是两个寄存器
  当数据从TDR转移到发送移位寄存器,标志位TXE(发送寄存器空)就会置1,发送移位寄存器就会在发送器控制的驱动下,向右移位(低位先行),一位一位的把数据输出到TX,移位完成后,新的数据会再次自动的从TDR转到发送移位寄存器。接收部分同理
  硬件流控可以避免数据丢弃或者覆盖数据的现象
  SCLK时钟部分,针对发送部分,发送寄存器每移位依次,同步时钟电平就跳变一个周期
  唤醒单元是实现串口挂载多设备
  中断输出控制中的两个标志位TXE(发送寄存器空)和RXNE(接收寄存器非空)是判断发送状态接收状态的必要标志位
  波特率发生器就是分频器,APB时钟分频,USARTDIV分频系数时钟输入fPCLKxUSART1挂载在APB2,所以是PCLK2的时钟,一般是72MHz,其他的USART挂载在APB1,所以是PCLK1的时钟,一般是36MHz,TE使能发送部分波特率,RE使能接收部分波特率
  发送器和接收器的波特率由波特率寄存器里BRR里的DIV确定,计算公式为fPCLK2/1 / (16*DIV)(例需要配置9600的波特率,9600 = 72M/(16*DIV),得DIV = 468.75,转换成二进制111010100.11,整数部分为111010100,小数部分为11,空白部分补0)

在这里插入图片描述
  下图为USART的基本结构
  时钟是PCLK2/1,经过波特率分频后,产生的时钟通向发送控制器接收控制器,用来控制发送/接收移位寄存器和发送/接收数据寄存器,通过GPIO口的复用推挽输出(GPIO控制权交给片上外设),输出到TX引脚,产生串口协议规定的波形RX引脚的波形通过GPIO口的输入,一位一位移入接收寄存器,从接收数据寄存器读,检查RXNE标志位,是否收到数据,同时标志位也可以去申请中断。最后控制cmd开启外设
  虽然看着有4个寄存器的操作,实际在程序中,只有一个DR寄存器配置,写入操作时,数据走上面这条路,进行发送,读取DR时,数据走下面这条路,进行接收

在这里插入图片描述

2.2 串口发送

  STM32通过USART发送数据给电脑端。具体步骤如下:开启RCC时钟(USART和GPIO)GPIO初始化(TX复用推挽输出,RX输入)USART初始化开启USART
  下面给出Serial.c

void Serial_Init()
{
	//开启RCC时钟(USART、GPIO)
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	//GPIO初始化 TX复用推挽输出
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;		//复用推挽输出TX
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStructure);
	//初始化USART
	USART_InitTypeDef USART_InitStructure;
	USART_InitStructure.USART_BaudRate = 9600;		//波特率9600
	USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;		//硬件流控制
	USART_InitStructure.USART_Mode = USART_Mode_Tx;		//发送
	USART_InitStructure.USART_Parity = USART_Parity_No;		//不需要校验
	USART_InitStructure.USART_StopBits = USART_StopBits_1;		//1位停止位
	USART_InitStructure.USART_WordLength = USART_WordLength_8b;		//8位字长
	USART_Init(USART1,&USART_InitStructure);
	//开启USART
	USART_Cmd(USART1,ENABLE);
}

void Serial_SendByte(uint8_t Byte)
{
	USART_SendData(USART1, Byte);
	while(USART_GetFlagStatus(USART1,USART_FLAG_TXE) == RESET);		//TXE标志位为1退出循环
}

  下面函数可以实现发送一个数组字符串、和字符形式的数字

/*
@brief:发送一个数组
@param:数组 数组长度
*/
void Serial_SendArray(uint8_t *Array,uint16_t Length)
{
	uint16_t i;
	for(i = 0;i < Length;i++)
	{
		Serial_SendByte(Array[i]);
	}
}
/*
@brief:发送一个字符串
@param:字符串
*/
void Serial_SendString(char *String)
{
	uint8_t i;
	for(i = 0;String[i] != '\0';i++)
	{
		Serial_SendByte(String[i]);
	}
}
/*
@brief:返回X的Y次方
@param:底数 指数
*/
uint32_t Serial_Pow(uint32_t X, uint32_t Y)
{
	uint32_t Result = 1;
	while (Y--)
	{
		Result *= X;
	}
	return Result;
}
/*
@brief:发送一个字符型数字
@param:字符型数字 数字长度
*/
void Serial_SendNumber(uint32_t Number,uint8_t Length)
{
	uint8_t i;
	for(i = 0; i < Length ;i++)
	{
		Serial_SendByte(Number / Serial_Pow(10,Length - i - 1) %10 + '0');
	}
}

  下面是main.c

#include "stm32f10x.h"                  // Device header
#include "OLED.h"
#include "Serial.h"

int main(void)
{
	OLED_Init();
	Serial_Init();
	Serial_SendByte(0x41);
	Serial_SendByte('A');
	
	uint8_t MyArray[] = {0x42, 0x43, 0x44, 0x45};
	Serial_SendArray(MyArray,4);//数组
	
	Serial_SendString("HelloWorld!\r\n");//字符串
	
	Serial_SendNumber(12345,5);//字符串数字
	
	while(1)
	{
		
	}
}

2.2.1 Printf函数移植

  如下图所示,勾选

在这里插入图片描述

  printf重定向,将printf打印的东西输出到串口。先在Serial文件包含上stdio.h,然后重写fputc函数,在Serial.h中申明stdio.h,主函数printf打印,即可通过串口发送printf内的内容

/*
@brief:重写fputc函数
@brief:fputc是printf的底层,将fputc函数重定向到串口,printf就输出到串口
*/
int fputc(int ch, FILE *f)
{
	Serial_SendByte(ch);
	return ch;
}
	printf("Num = %d \r\n",666);

2.2.2 串口发送汉字

  采用UTF-8的字符编码格式显示汉字时,需要在杂项控制栏里填入–no-multibyte-chars

在这里插入图片描述

  串口助手数据模式选择UTF-8,保持一致,用printf打印汉字,即可发送汉字

	printf("你好,世界");

2.3 串口接收

  STM32通过串口接收数据,步骤和串口发送的部分不同之处:初始化PA9和PA10,分别是复用推挽输出和上拉输入USART模式部分|上RX功能中断和NVIC配置
  串口接收部分,可以使用查询中断两种办法,中断方法还需要配置中断和NVIC

2.3.1 串口接收+查询

  主函数中,不断去判断RXNE标志位,如果置1,说明已经收到数据,那只需要读DR寄存器即可
  下面是Serial.c部分代码

void Serial_Init()
{
	//开启RCC时钟(USART、GPIO)
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	//GPIO初始化 TX复用推挽输出 RX上拉输入
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;		//复用输出TX
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;		//上拉输入模式
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStructure);
	//初始化USART
	USART_InitTypeDef USART_InitStructure;
	USART_InitStructure.USART_BaudRate = 9600;		//波特率9600
	USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;		//硬件流控制
	USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;		//发送和接收
	USART_InitStructure.USART_Parity = USART_Parity_No;		//不需要校验
	USART_InitStructure.USART_StopBits = USART_StopBits_1;		//1位停止位
	USART_InitStructure.USART_WordLength = USART_WordLength_8b;		//8位字长
	USART_Init(USART1,&USART_InitStructure);
	//开启USART
	USART_Cmd(USART1,ENABLE);
}

  主函数循环中判断标志位,读取DR寄存器值即可

while(1)
	{
		if(USART_GetFlagStatus(USART1,USART_FLAG_RXNE) == SET)
		{
			RxData = USART_ReceiveData(USART1);
			OLED_ShowHexNum(1,1,RxData,2);
		}
	}

2.3.2 串口接收+中断

  初始化部分,要加上开启中断NVIC的配置部分

void Serial_Init()
{
	//开启RCC时钟(USART、GPIO)
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	//GPIO初始化 TX复用推挽输出 RX上拉输入
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;		//复用输出TX
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;		//上拉输入模式
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStructure);
	//初始化USART
	USART_InitTypeDef USART_InitStructure;
	USART_InitStructure.USART_BaudRate = 9600;		//波特率9600
	USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;		//硬件流控制
	USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;		//发送和接收
	USART_InitStructure.USART_Parity = USART_Parity_No;		//不需要校验
	USART_InitStructure.USART_StopBits = USART_StopBits_1;		//1位停止位
	USART_InitStructure.USART_WordLength = USART_WordLength_8b;		//8位字长
	USART_Init(USART1,&USART_InitStructure);
	
	//开启中断,RXNE置1进中断
	USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);
	//配置NVIC
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
	NVIC_Init(&NVIC_InitStructure);
	//开启USART
	USART_Cmd(USART1,ENABLE);
}

  中断函数里,读取接收的数据

void USART1_IRQHandler()
{
	if(USART_GetITStatus(USART1,USART_IT_RXNE) == SET)//中断标志位
	{
		Serial_RxData = USART_ReceiveData(USART1);//读取数据
		Serial_RxFlag = 1;
		USART_ClearITPendingBit(USART1,USART_IT_RXNE);//清除标志位
	}
}

  在函数中调用,将STM32接收到的数据显示在OLED上

uint8_t RxData;
int main(void)
{
	OLED_Init();
	Serial_Init();
	
	while(1)
	{
		if(Serial_GetRxFlag() == 1)
		{
			RxData = Serial_GetRxData();
			OLED_ShowHexNum(1,1,RxData,2);
		}
	}
}

2.4 USART串口数据包

  数据包的作用就是把一个个单独的数据给打包起来,方便我们进行多字节的数据通信

2.4.1 数据包格式

  1.HEX数据包格式(以原始的字节数据本身呈现)

  • 固定包长,含包头包尾:这里固定包长字节是4个,数据包前面是包头,后面是包尾

   包头包尾和数据载荷重复的问题:FF为包头,FE为包尾,可以限制载荷数据的范围,避免取到包头包尾的值;或者增加包头包尾的数量等

在这里插入图片描述

  • 可变包长,含包头包尾:包长字节的个数可以变化,数据包前面是包头,后面是包尾

在这里插入图片描述

  2.文本数据包格式(经过编码和译码,以文本形式呈现)

  • 固定包长,含包头包尾:@作为包头,\r \n作为包尾

在这里插入图片描述

  • 可变包长,含包头包尾

在这里插入图片描述

2.4.2 数据包接收流程

  • 固定包长HEX数据包接收

  数据包接收具有关联性,设定状态机参数S,S=0、1、2。
  S=0:判断第一个数据是不是FF,即判断包头
  S=1:开始接收数据,收完4个数据
  S=2:判断最后一个数据是不是FE,即判断包尾

在这里插入图片描述

  • 可变包长文本数据包接收

  数据包接收具有关联性,设定状态机参数S,S=0、1、2。
  S=0:判断第一个数据是不是@,即判断包头
  S=1:开始接收数据,等待包尾 ‘\r’
  S=2:判断最后一个数据是不是 ‘\n’ ,即判断包尾

在这里插入图片描述

2.4.3 串口收发Hex数据包

  STM32通过串口发送数据包时,直接调用串口发送字节函数Serial_SendByte(),依次发送包头,数据,包尾,发送数据存在Serial_TxPacket[]里。
  STM32通过串口接收数据包时,定义状态机变量,每收到一个字节,进入中断函数,实现包头、数据、包尾的接收,接收数据存放在Serial_RxPacket[]里
  下面是Serial.c

#include "stm32f10x.h"                  // Device header
#include "stdio.h"


uint8_t Serial_TxPacket[4];
uint8_t Serial_RxPacket[4];//收发载荷数据
uint8_t Serial_RxFlag;//标志位

void Serial_Init()
{
	//开启RCC时钟(USART、GPIO)
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	//GPIO初始化 TX复用推挽输出 RX上拉输入
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;		//复用输出TX
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;		//上拉输入模式
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStructure);
	//初始化USART
	USART_InitTypeDef USART_InitStructure;
	USART_InitStructure.USART_BaudRate = 9600;		//波特率9600
	USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;		//硬件流控制
	USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;		//发送和接收
	USART_InitStructure.USART_Parity = USART_Parity_No;		//不需要校验
	USART_InitStructure.USART_StopBits = USART_StopBits_1;		//1位停止位
	USART_InitStructure.USART_WordLength = USART_WordLength_8b;		//8位字长
	USART_Init(USART1,&USART_InitStructure);
	
	//开启中断,RXNE置1进中断
	USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);
	//配置NVIC
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
	NVIC_Init(&NVIC_InitStructure);
	//开启USART
	USART_Cmd(USART1,ENABLE);
}

void Serial_SendByte(uint8_t Byte)
{
	USART_SendData(USART1, Byte);
	while(USART_GetFlagStatus(USART1,USART_FLAG_TXE) == RESET);		//数据到了移位寄存器
}
/*
@brief:发送一个数组
@param:数组 数组长度
*/
void Serial_SendArray(uint8_t *Array,uint16_t Length)
{
	uint16_t i;
	for(i = 0;i < Length;i++)
	{
		Serial_SendByte(Array[i]);
	}
}
/*
@brief:发送一个字符串
@param:字符串
*/
void Serial_SendString(char *String)
{
	uint8_t i;
	for(i = 0;String[i] != '\0';i++)
	{
		Serial_SendByte(String[i]);
	}
}
/*
@brief:返回X的Y次方
@param:底数 指数
*/
uint32_t Serial_Pow(uint32_t X, uint32_t Y)
{
	uint32_t Result = 1;
	while (Y--)
	{
		Result *= X;
	}
	return Result;
}
/*
@brief:发送一个字符型数字
@param:字符型数字 数字长度
*/
void Serial_SendNumber(uint32_t Number,uint8_t Length)
{
	uint8_t i;
	for(i = 0; i < Length ;i++)
	{
		Serial_SendByte(Number / Serial_Pow(10,Length - i - 1) %10 + '0');
	}
}

/*
@brief:重写fputc函数
@brief:fputc是printf的底层,将fputc函数重定向到串口,printf就输出到串口
*/
int fputc(int ch, FILE *f)
{
	Serial_SendByte(ch);
	return ch;
}

/*
@brief:发送Serial_TxPacket数据
*/
void Seriai_SendPacket()
{
	Serial_SendByte(0xFF);//发送包头
	Serial_SendArray(Serial_TxPacket,4);//发送载荷数据
	Serial_SendByte(0xFE);//发送包尾
}
/*
@brief:返回Serial_RxFlag
*/
uint8_t Serial_GetRxFlag()
{
	if(Serial_RxFlag == 1)
	{
		Serial_RxFlag = 0;
		return 1;
	}
	return 0;
}


void USART1_IRQHandler()
{
	static uint8_t Rxstate = 0;//状态机变量(全局变量)
	static uint8_t pRxPacket = 0;//指定接收到哪一个,一共4个数
	if(USART_GetITStatus(USART1,USART_IT_RXNE) == SET)//中断标志位
	{
		uint8_t RxData = USART_ReceiveData(USART1);//拿到接收的数据
		if(Rxstate == 0)//判断包头
		{
			if(RxData == 0xFF)//确实是包头
			{
				Rxstate = 1;
				pRxPacket = 0;
			}
		}
		else if(Rxstate == 1)//接收4位数据
		{
			Serial_RxPacket[pRxPacket] = RxData;//接收到的数据存在接收数组中
			pRxPacket++;
			if(pRxPacket >= 4)
			{
				Rxstate = 2;			
			}
		}
		else if(Rxstate == 2)//判断包尾
		{
			if(RxData == 0xFE)
			{
				Rxstate = 0;//回到最初的状态
				Serial_RxFlag = 1;//接收到包尾,给标志位1
			}
		}
		
		USART_ClearITPendingBit(USART1,USART_IT_RXNE);//清除标志位
	}
}

  在OLED显示屏上,显示出STM32发送的数据和接收的数据

uint8_t KeyNum;

int main(void)
{
	OLED_Init();
	Serial_Init();
	Key_Init();
	
	OLED_ShowString(1,1,"TxPacket:");
	OLED_ShowString(3,1,"RxPacket:");

	Serial_TxPacket[0] = 0x01;
	Serial_TxPacket[1] = 0x02;
	Serial_TxPacket[2] = 0x03;
	Serial_TxPacket[3] = 0x04;
	
	Seriai_SendPacket();
	while(1)
	{
		KeyNum = Key_GetNum();
		if(KeyNum == 1)//按下按键
		{
			Serial_TxPacket[0] ++;
			Serial_TxPacket[1] ++;
			Serial_TxPacket[2] ++;
			Serial_TxPacket[3] ++;//每个数据+1
			Seriai_SendPacket();//发送数据包
			OLED_ShowHexNum(2,1,Serial_TxPacket[0],2);
			OLED_ShowHexNum(2,4,Serial_TxPacket[1],2);
			OLED_ShowHexNum(2,7,Serial_TxPacket[2],2);
			OLED_ShowHexNum(2,10,Serial_TxPacket[3],2);//显示发送的数据

		}
		if(Serial_GetRxFlag() == 1)
		{
			OLED_ShowHexNum(4,1,Serial_RxPacket[0],2);
			OLED_ShowHexNum(4,4,Serial_RxPacket[1],2);
			OLED_ShowHexNum(4,7,Serial_RxPacket[2],2);
			OLED_ShowHexNum(4,10,Serial_RxPacket[3],2);
		}
	}
}

2.4.4 串口收发文本数据包

  串口接收包长不固定文本数据包,代码和上述不同的地方有:判断包头包尾需要修改,接收完成后,需要给结束标志位 ‘\0’
  在串口助手中发送文本格式时,包尾是回车,所以需要按下回车,再进行发送
  要求数据接收完整有序,可以将标志位Serial_RxFlag在完成事件时清零,下一个接收数据时,加上判断标志位是否是0

#include "stm32f10x.h"                  // Device header
#include "stdio.h"


char Serial_RxPacket[100];//接收字符
uint8_t Serial_RxFlag;//标志位

void Serial_Init()
{
	//开启RCC时钟(USART、GPIO)
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	//GPIO初始化 TX复用推挽输出 RX上拉输入
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;		//复用输出TX
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;		//上拉输入模式
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStructure);
	//初始化USART
	USART_InitTypeDef USART_InitStructure;
	USART_InitStructure.USART_BaudRate = 9600;		//波特率9600
	USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;		//硬件流控制
	USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;		//发送和接收
	USART_InitStructure.USART_Parity = USART_Parity_No;		//不需要校验
	USART_InitStructure.USART_StopBits = USART_StopBits_1;		//1位停止位
	USART_InitStructure.USART_WordLength = USART_WordLength_8b;		//8位字长
	USART_Init(USART1,&USART_InitStructure);
	
	//开启中断,RXNE置1进中断
	USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);
	//配置NVIC
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
	NVIC_Init(&NVIC_InitStructure);
	//开启USART
	USART_Cmd(USART1,ENABLE);
}

void Serial_SendByte(uint8_t Byte)
{
	USART_SendData(USART1, Byte);
	while(USART_GetFlagStatus(USART1,USART_FLAG_TXE) == RESET);		//数据到了移位寄存器
}
/*
@brief:发送一个数组
@param:数组 数组长度
*/
void Serial_SendArray(uint8_t *Array,uint16_t Length)
{
	uint16_t i;
	for(i = 0;i < Length;i++)
	{
		Serial_SendByte(Array[i]);
	}
}
/*
@brief:发送一个字符串
@param:字符串
*/
void Serial_SendString(char *String)
{
	uint8_t i;
	for(i = 0;String[i] != '\0';i++)
	{
		Serial_SendByte(String[i]);
	}
}
/*
@brief:返回X的Y次方
@param:底数 指数
*/
uint32_t Serial_Pow(uint32_t X, uint32_t Y)
{
	uint32_t Result = 1;
	while (Y--)
	{
		Result *= X;
	}
	return Result;
}
/*
@brief:发送一个字符型数字
@param:字符型数字 数字长度
*/
void Serial_SendNumber(uint32_t Number,uint8_t Length)
{
	uint8_t i;
	for(i = 0; i < Length ;i++)
	{
		Serial_SendByte(Number / Serial_Pow(10,Length - i - 1) %10 + '0');
	}
}

/*
@brief:重写fputc函数
@brief:fputc是printf的底层,将fputc函数重定向到串口,printf就输出到串口
*/
int fputc(int ch, FILE *f)
{
	Serial_SendByte(ch);
	return ch;
}

void USART1_IRQHandler()
{
	static uint8_t Rxstate = 0;//状态机变量(全局变量)
	static uint8_t pRxPacket = 0;//指定接收到哪一个
	if(USART_GetITStatus(USART1,USART_IT_RXNE) == SET)//中断标志位
	{
		uint8_t RxData = USART_ReceiveData(USART1);//拿到接收的数据
		if(Rxstate == 0)//判断包头
		{
			if(RxData == '@' && Serial_RxFlag == 0)//确实是包头,确保事件处理完,再接收
			{
				Rxstate = 1;
				pRxPacket = 0;
			}
		}
		else if(Rxstate == 1)//接收字符数据
		{
			if(RxData == '\r')//包长不确定,需要判断是否是包尾
			{
				Rxstate = 2;
			}
			else
			{
				Serial_RxPacket[pRxPacket] = RxData;//接收到的数据存在接收数组中
				pRxPacket++;
			}
		}
		else if(Rxstate == 2)//判断包尾
		{
			if(RxData == '\n')
			{
				Rxstate = 0;//回到最初的状态
				Serial_RxPacket[pRxPacket] = '\0';//字符串结束标志位
				Serial_RxFlag = 1;//接收到包尾,给标志位1

			}
		}
		USART_ClearITPendingBit(USART1,USART_IT_RXNE);//清除标志位
	}
}

  主函数中调用

int main(void)
{
	LED_Init();
	OLED_Init();
	Serial_Init();
	
	OLED_ShowString(1,1,"TxPacket:");
	OLED_ShowString(3,1,"RxPacket:");


	while(1)
	{
		if(Serial_RxFlag == 1)
		{
			
			OLED_ShowString(4,1,"                ");
			OLED_ShowString(4,1,Serial_RxPacket);
			if(strcmp(Serial_RxPacket,"LED_ON") == 0)//单片机接收到了LED_ON-strcmp函数:如果两个字符串相等,返回0
			{
				LED1_ON();
				Serial_SendString("LED_ON_OK\r\n");//单片机发送LED_ON_OK
				OLED_ShowString(2,1,"                ");
				OLED_ShowString(2,1,"LED_ON_OK");
			}
			else if(strcmp(Serial_RxPacket,"LED_OFF") == 0)
			{
				LED1_OFF();
				Serial_SendString("LED_OFF_OK\r\n");
				OLED_ShowString(2,1,"                ");
				OLED_ShowString(2,1,"LED_OFF_OK");
			}
			else
			{
				Serial_SendString("ERROR\r\n");
				OLED_ShowString(2,1,"                ");
				OLED_ShowString(2,1,"ERROR");
			}
			Serial_RxFlag = 0;
		}
		
	}
}

三、I2C通信

3.1 I2C介绍

  • 具体I2C介绍和时序见51单片机通信协议
  • 主机可以访问I2C总线上的任何一个设备,需要发送指令来确定要访问的是哪个设备,需要把每个从设备都确定一个唯一的设备地址,相当于每个设备的名字

3.2 I2C数据帧

  • 指定地址写:对于指定设备(Slave Address),在指定地址(寄存器地址Reg Address)下,写入指定数据(Data)

  开始 — 主机发送设备地址+写 — 主机接收应答 — 主机发送寄存器地址 — 主机接收应答 — 主机发送数据 — 主机接收应答 — 终止

  • 当前地址读:对于指定设备(Slave Address),在当前地址指针指示的地址(上一个写入数据的地址+1)下,读取从机数据(Data)

  开始 — 主机发送设备地址+读 — 主机接收应答 — 主机接收数据 — 主机发送应答 — 终止

  • 指定地址读:对于指定设备(Slave Address),在指定地址(寄存器地址Reg Address)下,读取从机数据(Data)

  开始 — 主机发送设备地址+写 — 主机接收应答 — 主机发送寄存器地址 — 主机接收应答 — 开始 — 主机发送设备地址+读 — 主机接收应答 — 主机接收数据 — 主机发送应答 — 终止

3.3 I2C驱动代码

  对I2C协议中初始条件终止条件发送一个字节接收一个字节发送应答接收应答
  I2C外设的SCL和SDA分别接在STM32的PB10和PB11口,开启RCC_GPIO时钟,配置成开漏输出模式,在此模式下,GPIO口同样可以输入,只需要释放SDA,接着读SDA数据

void MyI2C_Init()
{
	//开启时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
	
	//GPIO初始化
	//SCL-PB10,SDA-PB11
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;//开漏输出,也可以输入
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10|GPIO_Pin_11;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB,&GPIO_InitStructure);
	
	GPIO_SetBits(GPIOB,GPIO_Pin_10|GPIO_Pin_11);//空闲状态高电平
}
/*
@brief:SCL写高低电平
@param:BitValue高低电平,BitAction类型:一位二进制
*/
void MyI2C_W_SCL(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOB,GPIO_Pin_10,(BitAction)BitValue);
	Delay_us(10);
}
/*
@brief:SDA写高低电平
@param:BitValue高低电平,BitAction类型:一位二进制
*/
void MyI2C_W_SDA(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOB,GPIO_Pin_11,(BitAction)BitValue);
	Delay_us(10);
}
/*
@brief:读PB11的高低电平
@retval:BitValue:PB11的高低电平
*/
uint8_t MyI2C_R_SDA()
{
	uint8_t BitValue;
	BitValue = GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11);
	Delay_us(10);
	return BitValue;
}
/*
@brief:起始条件
*/
void MyI2C_Start()
{
	MyI2C_W_SDA(1);//释放SDA
	MyI2C_W_SCL(1);//释放SCL
	MyI2C_W_SDA(0);//拉低SDA
	MyI2C_W_SCL(0);//拉低SCL
}
/*
@brief:终止条件
*/
void MyI2C_Stop()
{
	MyI2C_W_SDA(0);//拉低SDA
	MyI2C_W_SCL(1);//释放SCL
	MyI2C_W_SDA(1);//释放SDA
}
/*
@brief:发送一个字节
@param:发送的字节数据
*/
void MyI2C_SendByte(uint8_t Byte)
{
	uint8_t i;
	for(i=0;i<8;i++)
	{
		MyI2C_W_SDA(Byte & (0x80>>i));
		MyI2C_W_SCL(1);//释放SCL,从机读取数据
		MyI2C_W_SCL(0);//拉低SCL
	}
}

/*
@brief:接收一个字节
@retval:接收的字节数据
*/
uint8_t MyI2C_ReceiveByte()
{
	uint8_t i;
	uint8_t Byte = 0x00;
	MyI2C_W_SDA(1);//主机接受前先释放SDA
	for(i=0;i<8;i++)
	{
		MyI2C_W_SCL(1);//SCL高电平读取SDA
		if(MyI2C_R_SDA()){Byte |= (0x80>>i);}
		MyI2C_W_SCL(0);//拉低,从机把下一位数据放在SDA上		
	}
	return Byte;
}
/*
@brief:发送应答
@param:AckBit应答位
*/
void MyI2C_SendAck(uint8_t AckBit)
{
		MyI2C_W_SDA(AckBit);
		MyI2C_W_SCL(1);//释放SCL,从机读取数据
		MyI2C_W_SCL(0);//拉低SCL
}
/*
@brief:接收应答
@retval:接收的应答位
*/
uint8_t MyI2C_ReceiveAck()
{
	uint8_t AckBit;
	MyI2C_W_SDA(1);//主机接受前先释放SDA
	MyI2C_W_SCL(1);//SCL高电平读取SDA
	AckBit = MyI2C_R_SDA();
	MyI2C_W_SCL(0);		
	return AckBit;
}

3.4 MPU6050姿态传感器

  • MPU6050是一个6轴姿态传感器,内置3轴加速度计和3轴陀螺仪传感器,可以测量芯片自身X、Y、Z轴的加速度角速度参数,通过数据融合,可进一步得到姿态角,常应用于平衡车、飞行器等需要检测自身姿态的场景

  如果再集成磁场传感器和气压传感器,测量X、Y、Z轴的磁场强度和气压,那就叫做10轴姿态传感器

  • 3轴加速度计:测量X、Y、Z轴的加速度
  • 3轴陀螺仪传感器:测量X、Y、Z轴的角速度

  下图为3轴加速度计和3轴陀螺仪传感器图
  基本原理:设置一种装置,当传感器所感应的参数变化时,装置可以带动电位器滑动或者装置本身的电阻随感应参数变化而变化,外接电源,通过电阻分压,就可以把现实世界的各种参数通过电压来表示,输出一个模拟电压,通过内置的AD转换器对模拟参量量化

在这里插入图片描述

  MPU6050参数

  • 16位ADC采集传感器的模拟信号,量化范围:2^16 = -32768到32767
  • 加速度计满量程选择(类似于ADC的Vref):±2、±4、±8、±16(g)
  • 陀螺仪满量程选择:±250、±500、±1000、±2000(°/sec)

  量程越小,测量分辨率越高,量程越大,测量范围越广
  通过测得的值和满量程得到加速度和角速度:测量值/32768 = X/满量程,解出X

  • 可配置的数字低通滤波器(抖动太厉害的情况下,使得数据平缓)
  • 可配置的时钟源
  • 可配置的采样分频(时钟分频为AD转换和内部电路提供时钟,控制AD转换快慢)
  • I2C从机地址:110 1000 (AD0=0) ,110 1001(AD0=1)

  AD0是引脚,AD0=0,从机地址为0x68,那么在写或者读时,注意读写位或者将从机地址先左移1位,变成0xD0,那么写就是0xD0,读就是0xD1

  下图为MPU6050的硬件电路图
  VCC、GND:电源
  SCLSDAI2C通信线,已内置上拉4.7K电阻
  XCL、XDA:主机I2C通信线,用于与磁场和气压传感器通信,变成10轴
  AD0从机地址最低位
  INT:中断信号输出
  ADO稳压器,可以接5V电压,扩大供电范围

在这里插入图片描述

  下图为MPU6050框图
  左边是传感器部分,包括XYZ轴的加速度计XYZ轴的陀螺仪温度传感器,都相当于是可变电阻,分压后输出模拟电压,然后接到ADC模块,输出数字量,这些数据统一放到数据寄存器中(没有数据覆盖的问题),读取数据寄存器就可以得到传感器测量的值
  右边一大块是寄存器和通信接口部分,INSR中断状态寄存器可以控制内部的事件到中断引脚的输出,SSR数据寄存器存储传感器数据,DMP姿态解算

在这里插入图片描述

3.5 软件I2C读取MPU6050

  MPU6050的SCL和SDA分别接单片机的PB10和PB11
  通过I2C数据帧写入数据给MPU6050和读取MPU6050发送给单片机的数据
  指定地址写开始主机发设备地址+写主机接收应答主机发寄存器地址主机接收应答主机发数据主机接收应答终止
  指定地址读开始主机发设备地址+写主机接收应答主机发寄存器地址主机接收应答开始主机发设备地址+读主机接收应答主机接收数据主机发送非应答终止
  下面是MPU6050.c

#include "stm32f10x.h"                  // Device header
#include "MyI2C.h"
#include "MPU6050_Reg.h"

#define MPU6050_ADDRESS 0xD0//写地址


/*
@brief:指定地址写
@param:RegAddress:寄存器地址 Data:写入的数据
*/
void MPU6050_WriteReg(uint8_t RegAddress,uint8_t Data)
{
	MyI2C_Start();
	MyI2C_SendByte(MPU6050_ADDRESS);
	MyI2C_ReceiveAck();
	MyI2C_SendByte(RegAddress);
	MyI2C_ReceiveAck();
	MyI2C_SendByte(Data);
	MyI2C_ReceiveAck();
	MyI2C_Stop();
}
/*
@brief:指定地址读
@param:RegAddress:寄存器地址
@retval:Data:MPU6050发送的数据
*/
uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{
	uint8_t Data;
	MyI2C_Start();
	MyI2C_SendByte(MPU6050_ADDRESS);
	MyI2C_ReceiveAck();
	MyI2C_SendByte(RegAddress);
	MyI2C_ReceiveAck();
	
	MyI2C_Start();
	MyI2C_SendByte(MPU6050_ADDRESS|0x01);//读地址
	MyI2C_ReceiveAck();
	Data = MyI2C_ReceiveByte();
	MyI2C_SendAck(1);//只读取一个字节,给非应答
	MyI2C_Stop();
	return Data;
}

uint8_t MPU6050_GetID()
{
	return MPU6050_ReadReg(MPU6050_WHO_AM_I);
}

void MPU6050_Init()
{
	MyI2C_Init();
	MPU6050_WriteReg(MPU6050_PWR_MGMT_1,0x01);//电源管理寄存器1:解除睡眠
	MPU6050_WriteReg(MPU6050_PWR_MGMT_2,0x00);//电源管理寄存器2:6个轴均不待机
	MPU6050_WriteReg(MPU6050_SMPLRT_DIV,0x09);//分频:采样分频为10
	MPU6050_WriteReg(MPU6050_CONFIG,0x06);//配置寄存器:滤波参数最大
	MPU6050_WriteReg(MPU6050_GYRO_CONFIG,0x18);//陀螺仪配置寄存器:最大量程
	MPU6050_WriteReg(MPU6050_ACCEL_CONFIG,0x18);//加速度计配置寄存器:最大量程
}
/*
@brief:通过指针变量返回XYZ的加速度值和角速度值
*/
void MPU6050_GetData(int16_t *AccX,int16_t *AccY,int16_t *AccZ,
						int16_t *GyroX,int16_t *GyroY,int16_t *GyroZ)
{
	uint8_t DataH,DataL;
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H);//读取加速度计X轴高八位
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);//读取加速度计X轴低八位
	*AccX = (DataH<<8) | DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H);//读取加速度计Y轴高八位
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);//读取加速度计Y轴低八位
	*AccY = (DataH<<8) | DataL;

	DataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H);//读取加速度计Z轴高八位
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);//读取加速度计Z轴低八位
	*AccZ = (DataH<<8) | DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H);//读取陀螺仪X轴高八位
	DataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);//读取陀螺仪X轴低八位
	*GyroX = (DataH<<8) | DataL;     
                                    
	DataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);//读取陀螺仪Y轴高八位
	DataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);//读取陀螺仪Y轴低八位
	*GyroY = (DataH<<8) | DataL;                    
                                                   
	DataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H);//读取陀螺仪Z轴高八位
	DataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L);//读取陀螺仪Z轴低八位
	*GyroZ = (DataH<<8) | DataL;
}

  下面是main.c

#include "stm32f10x.h"                  // Device header
#include "OLED.h"
#include "MPU6050.h"

int16_t AX,AY,AZ,GX,GY,GZ;

int main(void)
{
	OLED_Init();
	MPU6050_Init();
	OLED_ShowString(1,1,"ID:");
	uint8_t ID = MPU6050_GetID();
	OLED_ShowHexNum(1,4,ID,2);
	while(1)
	{
		MPU6050_GetData(&AX,&AY,&AZ,&GX,&GY,&GZ);//得到指针变量的值
		OLED_ShowSignedNum(2,1,AX,5);
		OLED_ShowSignedNum(3,1,AY,5);
		OLED_ShowSignedNum(4,1,AZ,5);
		OLED_ShowSignedNum(2,8,GX,5);
		OLED_ShowSignedNum(3,8,GY,5);
		OLED_ShowSignedNum(4,8,GZ,5);	
	}
}

四、I2C外设

4.1 I2C外设介绍

  • STM32内部集成了硬件I2C收发电路,类似于USART外设,可以由硬件自动执行时钟生成、起始终止条件生成、应答位收发、数据收发等功能,减轻CPU的负担
  • 支持多主机模型(主机:拥有主线控制总线的权利。从机:只能在主机允许的情况下控制总线)
  • 支持7位/10位地址模式(起始条件后,跟着7位地址+读写位)
  • 支持不同的通讯速度,标准速度(高达100KHz),快速(高达400KHz)
  • 支持DMA
  • 兼容SMBus协议

  STM32F103C8T6硬件I2C资源:I2C1I2C2

  下图为硬件I2C硬件框图
  SDA和SCL接在指定复用GPIO口
  SDA部分,发送时,数据由数据寄存器转到移位寄存器时,状态寄存器的TXE为1,表示发送寄存器为空接收时,输入的数据从SDA一位一位到移位寄存器,数据收齐后,数据整体从移位寄存器到数据寄存器DR,同时置标志位RXNE,表示接收寄存器非空,从数据寄存器读取数据
  SCL部分,时钟控制寄存器写对应的位,电路执行对应的功能,控制寄存器和状态寄存器控制逻辑电路和状态,当逻辑电路产生标志位时,可以触发中断和DMA响应

  串口和I2C类似,串口是全双工,数据收发分开,I2C是半双工,数据收发是同一组寄存器

在这里插入图片描述

  下图是I2C的基本结构图
  GPIO口配置成复用开漏输出,输入仍然有效,GPIO状态来自片上外设
  移位寄存器左移,数据高位先行。时钟控制器提供时钟,开关控制使能

在这里插入图片描述

4.2 I2C操作流程

  基本流程:写入控制寄存器CR数据寄存器DR,就可以控制时序单元的发生,时序单元发生后,检查相应的EV事件,就是状态寄存器SR,来等待时序单元发送完成

4.2.1 主机发送

  7位主发送起始从机地址+写接收应答数据1接收应答数据2接收应答停止
  起始条件后,产生EV5事件,检测起始条件已发送。发送完从机地址,产生EV6事件,代表地址发送结束,然后是EV8_1事件,移位寄存器空数据寄存器空,需要写入DR寄存器进行数据发送。然后到EV8事件开始发数据,其中移位寄存器非空数据寄存器空,写入DR寄存器将清除该事件。然后数据2、3。最后结束时,移位寄存器空数据寄存器空,产生EV8_2事件,字节发送标志位,请求停止位。

  初始条件后,等待EV5事件,发送从机地址+写,等待EV6(发送)事件,发送寄存器地址,等待EV8事件,发送数据,最终终止条件前,等待EV8_2事件,其中硬件I2C自动完成接收应答和发送应答

在这里插入图片描述

4.2.2 主机接收

  7位主接收起始从机地址+读接收应答数据1发送应答数据2非应答停止
  起始条件后,产生EV5事件,检测起始条件已发送。发送完从机地址,产生EV6事件,代表地址发送结束EV6_1事件没有对应的事件标志,只适用于接收1个字节的情况(数据1正在移位,还没有结束)。当移位寄存器成功移入一个字节的数据1,整体数据转入到数据寄存器DR,同时置RxNE标志位,表示数据寄存器非空,即EV7事件。类推产生数据2,产生EV7事件,读走数据2,EV7事件结束。不需要接收数据时,应答位控制寄存器ACK置0,设置终止条件请求,即EV7_1事件,最终到终止条件

  初始条件后,等待EV5事件,发送从机地址+写,等待EV6(发送)事件,发送寄存器地址,等待EV8事件,重复初始条件,等待EV5事件,发送从机地址+读,等待EV6(接收)事件,当只接收一个字节时,需要提前ACK置0申请产生终止条件,等待EV7事件,读取数据,恢复默认ACK,ACK置1

在这里插入图片描述

4.3 硬件I2C读取MPU6050

  硬件I2C需要使用指定的GPIO口,STM32F103C8T6中硬件I2C2接口是SCL-PB10,SDA-PB11
  步骤:开启RCC时钟(I2C和GPIO) — GPIO初始化(复用开漏输出) — 初始化I2CI2C使能
  下面为I2C库函数部分

void I2C_Init(I2C_TypeDef* I2Cx, I2C_InitTypeDef* I2C_InitStruct);//I2C初始化
void I2C_Cmd(I2C_TypeDef* I2Cx, FunctionalState NewState);//使能或失能I2C外设
void I2C_GenerateSTART(I2C_TypeDef* I2Cx, FunctionalState NewState);//生成起始条件
void I2C_GenerateSTOP(I2C_TypeDef* I2Cx, FunctionalState NewState);//生成终止条件
void I2C_AcknowledgeConfig(I2C_TypeDef* I2Cx, FunctionalState NewState);//收到字节以后,是否给从机应答,1应答,0非应答
void I2C_SendData(I2C_TypeDef* I2Cx, uint8_t Data);//发送数据
void I2C_Send7bitAddress(I2C_TypeDef* I2Cx, uint8_t Address, uint8_t I2C_Direction);//发送7位地址
uint8_t I2C_ReceiveData(I2C_TypeDef* I2Cx);//接收数据
ErrorStatus I2C_CheckEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT);//事件状态监控函数

  下面为MPU6050.h
  与软件I2C不同的地方初是初始化函数、指定地址写和读,不需要调用软件I2C的时序代码,使用库函数中I2C的函数,同时还包含死循环超时等待部分
  主函数部分同软件I2C

#include "stm32f10x.h"                  // Device header
#include "MPU6050_Reg.h"

#define MPU6050_ADDRESS 0xD0//写地址

/*
@brief:防止死循环卡死,在此循环里等待和超时退出
*/
void MPU6050_WaitEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT)
{
	uint32_t Timeout;
	Timeout = 10000;
	while (I2C_CheckEvent(I2Cx,I2C_EVENT)!=SUCCESS)
	{
		Timeout --;
		if(Timeout == 0)
		{
			break;
		}
	}
}
/*
@brief:指定地址写
@param:RegAddress:寄存器地址 Data:写入的数据
*/
void MPU6050_WriteReg(uint8_t RegAddress,uint8_t Data)
{	
	I2C_GenerateSTART(I2C2,ENABLE);//初始条件
	MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT);//等待EV5事件和超时退出
	I2C_Send7bitAddress(I2C2,MPU6050_ADDRESS,I2C_Direction_Transmitter);//发送从机地址,写
	//硬件I2C自带发送应答和接收应答
	MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);//等待EV6(发送)事件和超时退出
	I2C_SendData(I2C2,RegAddress);//发送寄存器地址
	MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTING);//等待EV8事件和超时退出
	I2C_SendData(I2C2,Data);//发送数据
	MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTED);//只发送一个数据和超时退出
	I2C_GenerateSTOP(I2C2,ENABLE);//终止条件
}
/*
@brief:指定地址读
@param:RegAddress:寄存器地址
@retval:Data:MPU6050发送的数据
*/
uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{
	uint8_t Data;
	I2C_GenerateSTART(I2C2,ENABLE);//初始条件
	MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT);//等待EV5事件和超时退出
	I2C_Send7bitAddress(I2C2,MPU6050_ADDRESS,I2C_Direction_Transmitter);//发送从机地址,写
	MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);//等待EV6(发送)事件和超时退出
	I2C_SendData(I2C2,RegAddress);//发送寄存器地址
	MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTING);//等待EV8事件和超时退出
	
	I2C_GenerateSTART(I2C2,ENABLE);//重复初始条件
	MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT);//等待EV5事件和超时退出
	I2C_Send7bitAddress(I2C2,MPU6050_ADDRESS,I2C_Direction_Receiver);//发送从机地址,读
	MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED);//等待EV6(接收)事件和超时退出
	
	I2C_AcknowledgeConfig(I2C2,DISABLE);//只接收一个字节,ACK给0,不给应答
	I2C_GenerateSTOP(I2C2,ENABLE);//申请产生终止条件
	
	MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_BYTE_RECEIVED);//等待EV7事件和超时退出
	Data = I2C_ReceiveData(I2C2);//读取数据
	I2C_AcknowledgeConfig(I2C2,ENABLE);//恢复默认ACK1
	
	return Data;
}

uint8_t MPU6050_GetID()
{
	return MPU6050_ReadReg(MPU6050_WHO_AM_I);
}

void MPU6050_Init()
{
	//开启RCC时钟
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
	//GPIO初始化
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;//复用开漏输出
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10|GPIO_Pin_11;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB,&GPIO_InitStructure);
	//初始化I2C
	I2C_InitTypeDef I2C_InitStructure;
	I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;//I2C模式
	I2C_InitStructure.I2C_ClockSpeed = 50000;//SCL时钟频率
	I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;//时钟占空比,频率大于100Khz才有用
	I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;//确定接收一个字节后,是否给从机应答
	I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;//STM32作为从机:7位地址
	I2C_InitStructure.I2C_OwnAddress1 = 0x00;//STM32指定一个自身的地址
	I2C_Init(I2C2,&I2C_InitStructure);
	//I2C初始化
	I2C_Cmd(I2C2,ENABLE);
	
	
	MPU6050_WriteReg(MPU6050_PWR_MGMT_1,0x01);//电源管理寄存器1:解除睡眠
	MPU6050_WriteReg(MPU6050_PWR_MGMT_2,0x00);//电源管理寄存器2:6个轴均不待机
	MPU6050_WriteReg(MPU6050_SMPLRT_DIV,0x09);//分频:采样分频为10
	MPU6050_WriteReg(MPU6050_CONFIG,0x06);//配置寄存器:滤波参数最大
	MPU6050_WriteReg(MPU6050_GYRO_CONFIG,0x18);//陀螺仪配置寄存器:最大量程
	MPU6050_WriteReg(MPU6050_ACCEL_CONFIG,0x18);//加速度计配置寄存器:最大量程
}
/*
@brief:通过指针变量返回XYZ的加速度值和角速度值
*/
void MPU6050_GetData(int16_t *AccX,int16_t *AccY,int16_t *AccZ,
						int16_t *GyroX,int16_t *GyroY,int16_t *GyroZ)
{
	uint8_t DataH,DataL;
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H);//读取加速度计X轴高八位
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);//读取加速度计X轴低八位
	*AccX = (DataH<<8) | DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H);//读取加速度计Y轴高八位
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);//读取加速度计Y轴低八位
	*AccY = (DataH<<8) | DataL;

	DataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H);//读取加速度计Z轴高八位
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);//读取加速度计Z轴低八位
	*AccZ = (DataH<<8) | DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H);//读取陀螺仪X轴高八位
	DataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);//读取陀螺仪X轴低八位
	*GyroX = (DataH<<8) | DataL;     
                                    
	DataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);//读取陀螺仪Y轴高八位
	DataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);//读取陀螺仪Y轴低八位
	*GyroY = (DataH<<8) | DataL;                    
                                                   
	DataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H);//读取陀螺仪Z轴高八位
	DataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L);//读取陀螺仪Z轴低八位
	*GyroZ = (DataH<<8) | DataL;
}

五、SPI通信

5.1 SPI通信介绍

  • SPI(串行外设接口)是由Motorola公司开发的一种通用数据总线
  • 四根通信线:SCK(串行时钟线)、MOSI(主机输出从机输入:主机发送给从机)、MISO(主机输入从机输出:从机发送给主机)、SS(从机选择:指定和谁通信)
  • 同步、全双工
  • 支持总线挂载多设备(一主多从)

  下图为SPI硬件电路

  • 所有SPI设备的SCK、MOSI、MISO分别连在一起
  • 主机另外引出多条SS控制线(低电平有效),分别接到各从机的SS引脚
  • 输出引脚设置为推挽输出输入引脚配置为浮空上拉输入

在这里插入图片描述

  下图为SPI移位示意图
  8位移位寄存器存在一个时钟输入端,时钟源由主机提供,叫做波特率发生器,同时通过SCK引脚进行输出,接到从机的移位寄存器,波特率发生器时钟的上升沿,所有移位寄存器向左移动一位,移出去的位放到MOSI和MISO通信线上。波特率发生器时钟的下降沿,引脚上的位采样输入到移位寄存器的最低位
  SPI高位先行,每来一个时钟,移位寄存器都会向进行移位。主机移位寄存器左边移出去的数据,通过MOSI引脚,输入到从机移位寄存器的右边。从机移位寄存器的左边通过MISO引脚,输入到主机移位寄存器的右边

在这里插入图片描述

5.2 SPI时序基本单元

  • 起始条件SS高电平切换到低电平
  • 终止条件SS低电平切换到高电平

  开始:代表选中了某个从机。结束:结束了从机的选中状态

在这里插入图片描述

  • 交换一个字节(模式0)
  • CPOL = 0:空闲状态时,SCK为低电平
  • CPHA = 0:SCK第个边沿移入数据,第个边沿移出数据

  在第一个边沿之前,就需要移出数据了

  • 交换一个字节(模式1)
  • CPOL = 0(时钟极性):空闲状态时,SCK为低电平
  • CPHA = 1(时钟相位):SCK第个边沿移出数据,第个边沿移入数据(数据采样)

  通信开始时,SS从高到低,通信结束时,SS从低到高。在SS空闲时期,MISO保持高阻态模式,SS下降沿后,从机的MISO被允许开启输出,SS上升沿后,从机的MISO必须置回高阻态
  SCK第一个边沿,即上升沿,主机和从机同时移出数据,主机通过MOSI移出最高位,代表主机要发送B7,从机通过MISO移出最高位,代表从机要发送B7,SCK下降沿时,主机和从机同时移入数据,也就是数据采样,主机移出的B7进入从机移位寄存器的最低位,从机移出的B7进入主机移位寄存器的最低位,循环8次,一个字节的数据交换完成,如果主机只想交换一个字节,置SS高电平MISO置高阻态,结束通信

在这里插入图片描述

  • 交换一个字节(模式2)
  • CPOL = 1:空闲状态时,SCK为高电平
  • CPHA = 0:SCK第个边沿移入数据,第个边沿移出数据

  和模式0相比,SCK的极性取反

  • 交换一个字节(模式3)
  • CPOL = 1:空闲状态时,SCK为高电平
  • CPHA = 1:SCK第个边沿移出数据,第个边沿移入数据(数据采样)

  和模式1相比,SCK的极性取反

  • 发送指令
  • 向SS指定的设备,发送指令(0x06),主机的0x06换了从机的0xFF

在这里插入图片描述

  • 指定地址写
  • 向SS指定的设备,发送写指令(0x02)
  • 随后在指定地址(Address[23:0])下,写入指定数据(Data)

  发指令发地址发数据

在这里插入图片描述

  • 指定地址读
  • 向SS指定的设备,发送读指令(0x03),
  • 随后在指定地址(Address[23:0])下,读取MISO从机数据(Data)

  发指令发地址读数据

在这里插入图片描述

5.3 SPI驱动代码

  软件模拟SPI时序,实现SPI通信
  SPI外设的CS、DO、SLK和DI分别接在了STM32的PA4PA6PA5PA7。首先需要开启RCC,然后初始化GPIO口
  下面给出MySPI.c

#include "stm32f10x.h"                  // Device header

/*
@brief:写SS引脚
*/
void MySPI_W_SS(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOA,GPIO_Pin_4,(BitAction)BitValue);
}
/*
@brief:写SCK引脚
*/
void MySPI_W_SCK(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOA,GPIO_Pin_5,(BitAction)BitValue);
}
/*
@brief:写MOSI引脚
*/
void MySPI_W_MOSI(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOA,GPIO_Pin_7,(BitAction)BitValue);
}
/*
@brief:读MISO引脚
*/
uint8_t MySPI_W_MISO()
{
	return GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_6);
}


void MySPI_Init()
{
	//开启GPIORCC时钟并初始化
	//CS-PA4 DO-PA6 SLK-PA5 DI-PA7
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;//推挽输出
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4|GPIO_Pin_5|GPIO_Pin_7;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;//上拉输入
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStructure);
	
	MySPI_W_SS(1);//SS默认高电平
	MySPI_W_SCK(0);//使用模式0
}
/*
@brief:起始信号
*/
void MySPI_Start()
{
	MySPI_W_SS(0);
}
/*
@brief:终止信号
*/
void MySPI_Stop()
{
	MySPI_W_SS(1);
}
/*
@brief:交换一个字节(模式0)
*/
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
	uint8_t ByteReveive=0x00;
	uint8_t i;
	for(i = 0;i<8;i++)
	{
		MySPI_W_MOSI(ByteSend&(0x80>>i));//移出:第一个边沿之前,主机从机同时移出数据
		MySPI_W_SCK(1);//第一个上升沿
		if(MySPI_W_MISO()){ByteReveive |= (0x80>>i);}//移入:主机读取从机移来(从机的高位开始)的数据
		MySPI_W_SCK(0);//第二位数据的下降沿,然后要移出数据
	}
	
	return ByteReveive;
}
///*
//@brief:交换一个字节(模式0)
//*/
//uint8_t MySPI_SwapByte(uint8_t ByteSend)
//{
//	uint8_t i;
//	for(i = 0;i<8;i++)
//	{
//		MySPI_W_MOSI(ByteSend&0x80);//移出:第一个边沿之前,主机从机的第一位数据移出去
//		ByteSend <<= 1;//左移一位,低位补0
//		MySPI_W_SCK(1);//第一个上升沿
//		if(MySPI_W_MISO()){ByteSend|= 0x01;}//移入:主机读取从机移来(从机的高位开始)的数据
//		MySPI_W_SCK(0);//第二位数据的下降沿,然后要移出数据
//	}
//	return ByteSend;
//}

5.4 W25Q64存储器

  • W25Qxx系列是一种低成本小型化使用简单非易失性存储器,常应用于数据存储字库存储固件程序存储等场景
  • 存储介质:Nor Flash(闪存)
  • 时钟频率:80MHz/160MHz(双重SPI)/320MHz(四重SPI)
  • 存储容量(24位地址-3字节):W25Q64:64Mbit/8MByte

  下图为W25Q64的电路图
  VCC(2.7-3.6V)、GND接地、HOLD(数据保持)、WP(写保护)
  CS(SPI从机选择)、DO(SPI MISO)、DI(SPI MOSI)、CLK(SPI时钟)

在这里插入图片描述

  下图为W25Q64框图
  64MBit以64Kb化成128个Block,块0的起始地址是000000h,结束地址是00FFFFh依次类推。每一个64Kb的Block被划分成了16个4Kb的Sector,起始地址是xx0000h,结束地址是xx0FFFh。在写入数据的时候,是按256字节的Page划分,1个Sector划分成16个Page,Page的地址是00FF00h到00FFFFh
  左下角是SPI的控制逻辑和通信引脚,Page Address是页地址锁存/计数器,Byte Address是字节锁存/计数器,这两个是用来指定地址
  单片机通过SPI发送3个字节地址前两个字节地址发送到页地址锁存/计数器,页地址通过写保护和行解码来选择操作哪一页最低位的字节地址发送到字节锁存/计数器,字节地址通过页解码和256字节页缓存来进行指定地址的读写操作,地址带有计数器,可以实现读写之后,地址自动加1,完成从指定地址开始,连续读写多个字节的目的
%emsp; 数据读写通过256字节RAM缓存区来进行,写入数据会先放到缓存区中,时序结束,芯片将缓存区里的数据复制到对应的Flash里,永久保存

在这里插入图片描述

  Flash操作注意事项

  写入操作时

  • 写入操作前,必须先进行写使能
  • 每个数据位只能由1改写为0,不能由0改写为1
  • 写入数据前必须先擦除(擦除指令),擦除后,所有数据位变为1
  • 擦除必须按最小擦除单元进行
  • 连续写入多字节时,最多写入一页的数据,超过页尾位置的数据,会回到页首覆盖写入
  • 写入操作结束后,芯片进入忙状态不响应新的读写操作

  读取操作时

  • 直接调用读取时序,无需使能无需额外操作没有页的限制,读取操作结束后不会进入忙状态,但不能在忙状态时读取

5.5 软件SPI读写W25Q64

  通过SPI时序驱动,一般流程是先写指令,然后写3字节地址,随后写数据或者读数据
  在写数据擦除数据时,需要事先开启写使能事后等待忙状态
  下图为W25Q64.c和W25Q64芯片的指令集

#include "stm32f10x.h"                  // Device header
#include "MySPI.h"
#include "W25Q64_INS.h"

void W25Q64_Init()
{
	MySPI_Init();
}
/*
@brief:读取ID号
*/
void W25Q64_ReadID(uint8_t *MID,uint16_t *DID)
{
	MySPI_Start();
	MySPI_SwapByte(W25Q64_JEDEC_ID);//主机发送读ID指令9F,按照约定,从机在下一次交换就会把ID号返回给主机
	*MID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);//此时目的是主机接收的数据,所以发送的数据无关紧要:厂商ID
	*DID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);//设备ID高八位
	*DID <<= 8;
	*DID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE);//设备ID低八位
	MySPI_Stop();
}
/*
@brief:写使能
*/
void W25Q64_WriteEnable()
{
	MySPI_Start();
	MySPI_SwapByte(W25Q64_WRITE_ENABLE);
	MySPI_Stop();
}
/*
@brief:读状态寄存器1,判断芯片是不是忙状态
*/
void W25Q64_WaitBusy()
{
	uint32_t Timeout;
	MySPI_Start();
	MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1);
	Timeout = 100000;
	while((MySPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01)//连续读出状态寄存器,实现等待BUSY的功能。BUSY为0,超时,然后退出循环
	{
		Timeout --;
		if(Timeout == 0)
		{
			break;//超时处理
		}
	}
	MySPI_Stop();
}
/*
@brief:在指定地址写数据
@param:Address地址 *DataArray:数据 Count:数据个数
*/
void W25Q64_PageProgram(uint32_t Address,uint8_t *DataArray,uint16_t Count)
{
	uint16_t i;
	W25Q64_WriteEnable();
	MySPI_Start();
	MySPI_SwapByte(W25Q64_PAGE_PROGRAM);//写指令
	MySPI_SwapByte(Address >> 16);//3字节的最高位
	MySPI_SwapByte(Address >> 8);//次高位
	MySPI_SwapByte(Address);//最低位
	for(i=0;i<Count;i++)
	{
		MySPI_SwapByte(DataArray[i]);//写入数据,i个字节
	}
	MySPI_Stop();
	W25Q64_WaitBusy();
}
/*
@brief:擦除指定地址的Sector
*/
void W25Q64_SectorErase(uint32_t Address)
{
	W25Q64_WriteEnable();
	MySPI_Start();
	MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB);//写指令
	MySPI_SwapByte(Address >> 16);//3字节的最高位
	MySPI_SwapByte(Address >> 8);//次高位
	MySPI_SwapByte(Address);//最低位
	MySPI_Stop();
	W25Q64_WaitBusy();
}
/*
@brief:读取指定地址的数据
@param:Address地址 *DataArray:读取的数据  Count:数据个数
*/
void W25Q64_ReadData(uint32_t Address,uint8_t *DataArray,uint32_t Count)
{
	uint32_t i;
	MySPI_Start();
	MySPI_SwapByte(W25Q64_READ_DATA);//写指令:读取数据
	MySPI_SwapByte(Address >> 16);//3字节的最高位
	MySPI_SwapByte(Address >> 8);//次高位
	MySPI_SwapByte(Address);//最低位
	for(i=0;i<Count;i++)
	{
		DataArray[i] = MySPI_SwapByte(W25Q64_DUMMY_BYTE);//读取数据
	}
	MySPI_Stop();
}
#ifndef __W25Q64_INS_H
#define __W25Q64_INS_H

#define W25Q64_WRITE_ENABLE							0x06
#define W25Q64_WRITE_DISABLE						0x04
#define W25Q64_READ_STATUS_REGISTER_1				0x05
#define W25Q64_READ_STATUS_REGISTER_2				0x35
#define W25Q64_WRITE_STATUS_REGISTER				0x01
#define W25Q64_PAGE_PROGRAM							0x02
#define W25Q64_QUAD_PAGE_PROGRAM					0x32
#define W25Q64_BLOCK_ERASE_64KB						0xD8
#define W25Q64_BLOCK_ERASE_32KB						0x52
#define W25Q64_SECTOR_ERASE_4KB						0x20
#define W25Q64_CHIP_ERASE							0xC7
#define W25Q64_ERASE_SUSPEND						0x75
#define W25Q64_ERASE_RESUME							0x7A
#define W25Q64_POWER_DOWN							0xB9
#define W25Q64_HIGH_PERFORMANCE_MODE				0xA3
#define W25Q64_CONTINUOUS_READ_MODE_RESET			0xFF
#define W25Q64_RELEASE_POWER_DOWN_HPM_DEVICE_ID		0xAB
#define W25Q64_MANUFACTURER_DEVICE_ID				0x90
#define W25Q64_READ_UNIQUE_ID						0x4B
#define W25Q64_JEDEC_ID								0x9F
#define W25Q64_READ_DATA							0x03
#define W25Q64_FAST_READ							0x0B
#define W25Q64_FAST_READ_DUAL_OUTPUT				0x3B
#define W25Q64_FAST_READ_DUAL_IO					0xBB
#define W25Q64_FAST_READ_QUAD_OUTPUT				0x6B
#define W25Q64_FAST_READ_QUAD_IO					0xEB
#define W25Q64_OCTAL_WORD_READ_QUAD_IO				0xE3

#define W25Q64_DUMMY_BYTE							0xFF//无用数据


#endif

  下面是main.c

#include "stm32f10x.h"                  // Device header
#include "OLED.h"
#include "W25Q64.h"
#include "W25Q64_INS.h"

uint8_t MID;
uint16_t DID;
uint8_t ArrayWrite[] = {0x01,0x02,0x03,0x04};
uint8_t ArrayRead[4];

int main(void)
{
	OLED_Init();
	W25Q64_Init();
	
	OLED_ShowString(1,1,"MID:   DID:");
	OLED_ShowString(2,1,"W:");
	OLED_ShowString(3,1,"R:");

	W25Q64_ReadID(&MID,&DID);
	OLED_ShowHexNum(1,5,MID,2);
	OLED_ShowHexNum(1,12,DID,4);
	
	W25Q64_SectorErase(0x000000);//擦除0x000000数据
	W25Q64_PageProgram(0x000000,ArrayWrite,4);//0x000000地址里写数据
	W25Q64_ReadData(0x000000,ArrayRead,4);//读0x000000地址里数据
	
	OLED_ShowHexNum(2,3,ArrayWrite[0],2);
	OLED_ShowHexNum(2,6,ArrayWrite[1],2);
	OLED_ShowHexNum(2,9,ArrayWrite[2],2);
	OLED_ShowHexNum(2,12,ArrayWrite[3],2);
	OLED_ShowHexNum(3,3,ArrayRead[0],2);
	OLED_ShowHexNum(3,6,ArrayRead[1],2);
	OLED_ShowHexNum(3,9,ArrayRead[2],2);
	OLED_ShowHexNum(3,12,ArrayRead[3],2);
	while(1)
	{
		
	}
}

六、SPI外设

6.1 SPI外设介绍

  • STM32内部集成了硬件SPI收发电路,可以由硬件自动执行时钟生成数据收发等功能,减轻CPU的负担
  • 可配置8位/16位数据帧、高位先行/低位先行
  • 时钟频(SCK-时钟速度):fpclk/2,4,8,16,32,64,128,256
  • 支持多主机模型、主或从操作
  • 可精简成半双工/单工通信
  • 支持DMA,兼容I2S协议

  STM32F103C8T6硬件SPI资源:SPI1SPI2
  下图为SPI框图
  其中缓冲区(数据寄存器)和移位寄存器部分,实现连续的数据流。图中移位寄存器的低位通过MOSI移出去,MISO的数据移入到移位寄存器的高位,所以此图中是低位先行LSBFIRST控制低位先行还是高位先行
  发送缓冲区(TDR),接收缓冲区(RDR)占用同一个地址(DR),写入数据时,写入到TDR读取数据时,从RDR读出。发送数据时,TDR把数据移动到移位寄存器,同时置TXE为1,表示TDR为空,然后通过MOSI发送。同时,移位寄存器的数据也会转入到接收缓冲区,置RXNE为1,表示RDR非空
  波特率发生器产生SCK时钟,BR寄存器控制分频系数…

在这里插入图片描述

6.2 SPI基本结构和时序

  下图为SPI基本结构
  TDR将发送的数据送到移位寄存器,置TXE标志位,移位寄存器左移,高位先行,通过GPIO-MOSI输出,移入的数据通过MISO到移位寄存器,进入到RDR,置RXNE标志位。波特率发生器产生时钟,输出给SCK引脚
在这里插入图片描述
  下图为主模式全双工连续传输时序图
  CPOL=1,CPHA=1,使用的是模式3,SCK默认高电平,第一个下降沿,MOSI和MISO移出数据,上升沿移入数据,依次进行,低位先行

在这里插入图片描述

  下图为非连续传输发送时序图
  CPOL=1,CPHA=1,模式3,SCK默认高电平。当想要发送数据时,TXE为1,写入发送的数据到TDR等待RXNE为1读取RDR接收的数据,之后只需要重复过程即可

  先发再读

在这里插入图片描述

6.3 硬件SPI读写W25Q64

  硬件SPI接线和软件SPI一样,不需要更改
  步骤:开启RCC时钟(SPI和GPIO) — 初始化GPIO和SPI(SCK、MOSI-复用推挽输出,MISO-上拉输入),SS软件控制的输出信号,配置成推挽输出开关控制
  交换数据步骤:等待TXE=1,TDR空 — 软件写入数据到DR等待RXNE=1,表示收到字节 — 读取DR

  下面给出SPI相关的库函数

void SPI_Init(SPI_TypeDef* SPIx, SPI_InitTypeDef* SPI_InitStruct);//SPI初始化
void SPI_StructInit(SPI_InitTypeDef* SPI_InitStruct);//结构体变量初始化
void SPI_Cmd(SPI_TypeDef* SPIx, FunctionalState NewState);//外设使能
void SPI_I2S_ITConfig(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT, FunctionalState NewState);//中断使能
void SPI_I2S_DMACmd(SPI_TypeDef* SPIx, uint16_t SPI_I2S_DMAReq, FunctionalState NewState);//DMA使能
void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data);//写DR寄存器
uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx);//读DR数据寄存器
FlagStatus SPI_I2S_GetFlagStatus(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);//获取标志位TXE和RXNE

  硬件SPI和软件SPI的区别就在于时序的模拟是由软件还是硬件完成,修改MySPI.c即可,其余部分同软件SPI一样

#include "stm32f10x.h"                  // Device header

/*
@brief:写SS引脚
*/
void MySPI_W_SS(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOA,GPIO_Pin_4,(BitAction)BitValue);
}

void MySPI_Init()
{
	//开启SPI1和GPIORCC时钟并初始化
	//CS-PA4 DO-PA6 SLK-PA5 DI-PA7
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1,ENABLE);
	//CS初始化
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;//推挽输出
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStructure);
	//SLK和MOSI初始化
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//复用推挽输出
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5|GPIO_Pin_7;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStructure);
	//MISO初始化
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;//上拉输入
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStructure);
	//初始化SPI
	SPI_InitTypeDef SPI_InitStructure;
	SPI_InitStructure.SPI_Mode = SPI_Mode_Master;//当前设备为主机
	SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;//双线全双工
	SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;//8位数据帧
	SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;//高位先行
	SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128;//时钟分频
	SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;//低电平有效
	SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;//CPHA=0,模式0
	SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;//软件NSS
	SPI_InitStructure.SPI_CRCPolynomial = 7;//CRC校验
	SPI_Init(SPI1,&SPI_InitStructure);
	
	SPI_Cmd(SPI1,ENABLE);
	MySPI_W_SS(1);
	
}
/*
@brief:起始信号
*/
void MySPI_Start()
{
	MySPI_W_SS(0);
}
/*
@brief:终止信号
*/
void MySPI_Stop()
{
	MySPI_W_SS(1);
}
/*
@brief:交换一个字节(模式0)
*/
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
	while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE) != SET);//等待TXE为1,TDR空
	SPI_I2S_SendData(SPI1,ByteSend);//ByteSend写入到DR
	while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_RXNE) != SET);//等待RXNE为1,TDR空
	return SPI_I2S_ReceiveData(SPI1);
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1838034.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

可平滑替代传统FTP的国产FTP方案,了解一下

企业在处理数据传输时&#xff0c;效率和安全性是关键。尽管传统FTP曾被广泛采用&#xff0c;然而&#xff0c;随着企业业务需求的增长&#xff0c;传统FTP在传输速度、安全性、稳定性以及可控性方面的不足逐渐显现。许多企业正在寻找更为高效、安全且用户体验更好的的国产FTP方…

助力低空经济-eVTOL/无人机ADS-B航管应答机选型指南

一、低空经济概述 “低空经济”在今年全国两会首次写入政府工作报告。近日&#xff0c;工业和信息化部、科学技术部、财政部、中国民用航空局印发《通用航空装备创新应用实施方案&#xff08;2024—2030年&#xff09;》&#xff0c;提出到2030年&#xff0c;推动低空经济形成…

操作系统 大作业

1、现有成绩文件按“姓名 学校 年级 班级 分数”五列组成&#xff0c;编写Shell脚本&#xff0c;将某目录下所有成绩文件&#xff08;≥3个&#xff09;合并为一个&#xff0c;形成“姓名 班级 分数”三列&#xff0c;并按成绩排序&#xff0c;输出年级排名前十。同时输出60以下…

Three.js框架的技术难点

Three.js作为一款功能强大的3D JavaScript框架&#xff0c;在为开发者提供丰富的功能和灵活性的同时&#xff0c;也带来了一些技术难点&#xff0c;需要开发者具备一定的知识和经验才能克服。以下是一些Three.js框架的技术难点。北京木奇移动技术有限公司&#xff0c;专业的软件…

高考志愿选专业,文科生如何分析选择专业?

每到高考时节&#xff0c;学生们最关注的就是专业选择&#xff0c;以及未来职业发展问题&#xff0c;对于文科生来说&#xff0c;面对文科专业的众多选择&#xff0c;很多人都有些不知所措&#xff0c;如何选择适合自己兴趣爱好&#xff0c;又有良好就业前景的工作。从哪些方面…

RTthread+STM32F407ZGTx+烟雾报警检测+蜂鸣器报警+LED闪烁||使用RTthread Studio

目录 实验背景 1.安装环境 2.配置环境 3.先编译下载实例程序2&#xff0c;观察DS0是否闪烁 4.实验方法 5.实例代码 6.硬件连接 7.实验效果 8.关于这次开发遇到的问题 1.反应慢&#xff0c;都熄灭1分钟多了&#xff0c;才报的问题&#xff1f; 2.关于rt_pin_mode(KEY…

Linux 图形化编程GTK3.0 快速入门之布局

GTK3.0 布局之水平布局 核心语法&#xff1a; 水平布局容器&#xff1a; 水平布局容器的创建&#xff1a; GtkWidget *gtk_hbox_new( gboolean homogeneous, gint spacing ); homogeneous&#xff1a;容器内控件是否大小一致( gboolean 取值为TRUE 或 FALSE ) spacing&#…

什么是数字化,什么是数智化?数字化与数智化的区别和联系

什么是数字化&#xff1f;什么是数智化&#xff1f;以及数字化与数智化的区别&#xff0c;下面分为三块跟大家详细讲解。 一、什么是数字化&#xff1f; 1、概念&#xff1a; 数字化&#xff08;Digitalization&#xff09;是将信息转换为数字&#xff08;即计算机可读&#…

操作系统之如何使用C语言完成SFJ和SRTJ,并完成他的甘特图

目录 前言 SFJ SRTJ 结束语 前言 不知不觉已经写博客一个月了&#xff0c;前段时间因为学业上的一些原因咕咕咕了&#xff0c;今天我又回来了。今天我给大家带来的是C语言代码完成的SFJ和SRTJ&#xff0c;并且带大家描述他的甘特图。如果有对SFJ和SRTJ不了解的小伙伴可以翻…

windows anaconda 安装 Labelme

安装 # 创建环境 conda create -n labelme python3.6 #激活环境 conda activate labelme # 安装依赖 conda install pyqt conda install pillow # 安装labelme conda install labelme3.16.2 # 启动labelme labelme右键选择标注类型&#xff0c;从上到下为多边形&#xff08;常…

超声波清洗机哪个牌子好用点?四大卓越臻品不容错过!

选购超声波清洗机能为日常生活提供非常多的便利&#xff01;比如像是戴眼镜或者是有婴儿的家庭&#xff0c;使用超声波清洗机不仅能够帮助清洗需要清洗的物品&#xff0c;同时更能保证物品健康清洁&#xff0c;对清洗的物品进行杀菌灭菌&#xff01;相对于比较费时间并且容易损…

eclipse宝刀未老

Theia 是一个高度可定制的、开源的、基于 Web 的集成开发环境&#xff08;IDE&#xff09;框架。它由 Eclipse Foundation 主导&#xff0c;旨在为云和本地环境提供现代化的、全功能的 IDE 解决方案。Theia 的核心目标是提供一个灵活的平台&#xff0c;开发者可以根据自己的需求…

2005-2022年款福特福克斯维修手册和电路图线路图接线图资料更新

经过整理&#xff0c;2005-2022年款福特福克斯全系列已经更新至汽修帮手资料库内&#xff0c;覆盖市面上99%车型&#xff0c;包括维修手册、电路图、新车特征、车身钣金维修数据、全车拆装、扭力、发动机大修、发动机正时、保养、电路图、针脚定义、模块传感器、保险丝盒图解对…

MaxWell实时监控Mysql并把数据写入到Kafka主题中

配置mysql 启用MySQL Binlog MySQL服务器的Binlog默认是未开启的&#xff0c;如需进行同步&#xff0c;需要先进行开启 修改MySQL配置文件/etc/my.cnf sudo vim/etc/my.cof 增加如下配置 注&#xff1a;MySQL Binlog模式 Statement-based&#xff1a;基于语句&#xff0c;…

定制化物联网设备:开启智能生活新篇章

随着科技的进步&#xff0c;物联网&#xff08;IoT&#xff09;已成为我们日常生活和工作中不可或缺的一部分。从智能家居到工业自动化&#xff0c;物联网设备以其独特的功能和特性&#xff0c;极大地提高了我们的生活质量和工作效率。然而&#xff0c;在众多的物联网设备中&am…

【深度解析】1688货源拿货价比拼多多贵?为何商家仍选1688

对电商卖家来说&#xff0c;首先需要解决的问题就是货源。 虽然知道1688是个大型综合性的货源平台&#xff0c;但很多卖家还是觉得1688上面的货源很贵&#xff0c;甚至在平台严查无货源的背景下&#xff0c;还是坚持从1688拿货。那么为什么有些拼多多的商品会比1688还便宜呢&a…

基于大型语言模型的全双工语音对话方案

摘要解读 我们提出了一种能够以全双工方式运行的生成性对话系统&#xff0c;实现了无缝互动。该系统基于一个精心调整的大型语言模型&#xff08;LLM&#xff09;&#xff0c;使其能够感知模块、运动功能模块以及一个具有两种状态&#xff08;称为神经有限状态机&#xff0c;n…

Linux系统编程——进程信号

目录 一&#xff0c;信号预备 1.1 生活中的信号 1.2 技术应用中的信号 1.3 signal函数捕捉信号 1.3 信号的发送与记录 1.4 信号的常见处理方式 二&#xff0c;信号的产生 2.1 核心转储 2.1.1 环境配置 2.1.2 利用core文件进行调试 2.1.3 core dump标志 2.2 通过系统…

【Python机器学习实战】 | 基于空气质量监测数据,采用多层神经网络预测PM2.5浓度

&#x1f3a9; 欢迎来到技术探索的奇幻世界&#x1f468;‍&#x1f4bb; &#x1f4dc; 个人主页&#xff1a;一伦明悦-CSDN博客 ✍&#x1f3fb; 作者简介&#xff1a; C软件开发、Python机器学习爱好者 &#x1f5e3;️ 互动与支持&#xff1a;&#x1f4ac;评论 &…

【漏洞复现】海康威视 综合安防管理平台软件 center_api_files 任意文件上传漏洞

免责声明&#xff1a; 本文内容旨在提供有关特定漏洞或安全漏洞的信息&#xff0c;以帮助用户更好地了解可能存在的风险。公布此类信息的目的在于促进网络安全意识和技术进步&#xff0c;并非出于任何恶意目的。阅读者应该明白&#xff0c;在利用本文提到的漏洞信息或进行相关测…