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

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;		//复用推挽输出TXGPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA,&GPIO_InitStructure);//初始化USARTUSART_InitTypeDef USART_InitStructure;USART_InitStructure.USART_BaudRate = 9600;		//波特率9600USART_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);//开启USARTUSART_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;		//复用输出TXGPIO_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);//初始化USARTUSART_InitTypeDef USART_InitStructure;USART_InitStructure.USART_BaudRate = 9600;		//波特率9600USART_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);//开启USARTUSART_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;		//复用输出TXGPIO_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);//初始化USARTUSART_InitTypeDef USART_InitStructure;USART_InitStructure.USART_BaudRate = 9600;		//波特率9600USART_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);//配置NVICNVIC_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);//开启USARTUSART_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;		//复用输出TXGPIO_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);//初始化USARTUSART_InitTypeDef USART_InitStructure;USART_InitStructure.USART_BaudRate = 9600;		//波特率9600USART_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);//配置NVICNVIC_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);//开启USARTUSART_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] ++;//每个数据+1Seriai_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;		//复用输出TXGPIO_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);//初始化USARTUSART_InitTypeDef USART_InitStructure;USART_InitStructure.USART_BaudRate = 9600;		//波特率9600USART_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);//配置NVICNVIC_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);//开启USARTUSART_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_OKOLED_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-PB11GPIO_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);//释放SDAMyI2C_W_SCL(1);//释放SCLMyI2C_W_SDA(0);//拉低SDAMyI2C_W_SCL(0);//拉低SCL
}
/*
@brief:终止条件
*/
void MyI2C_Stop()
{MyI2C_W_SDA(0);//拉低SDAMyI2C_W_SCL(1);//释放SCLMyI2C_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);//主机接受前先释放SDAfor(i=0;i<8;i++){MyI2C_W_SCL(1);//SCL高电平读取SDAif(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);//主机接受前先释放SDAMyI2C_W_SCL(1);//SCL高电平读取SDAAckBit = 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);//分频:采样分频为10MPU6050_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);//恢复默认ACK1return 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);//初始化I2CI2C_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);//分频:采样分频为10MPU6050_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-PA7RCC_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-PA7RCC_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);//初始化SPISPI_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,模式0SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;//软件NSSSPI_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写入到DRwhile(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_RXNE) != SET);//等待RXNE为1,TDR空return SPI_I2S_ReceiveData(SPI1);
}

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

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

相关文章

企业数字化转型好帮手蚓链,超多创新亮点等你来!

家人们&#xff0c;今天必须给大家分享一下蚓链这个超棒的数字化转型好帮手呀&#xff01; 在理念创新上&#xff0c;它做到了以用户为中心&#xff0c;给大家带来精准化、个性化的营销体验呢。 组织创新也超厉害&#xff0c;搭建了开放式创新平台&#xff0c;吸引外部合作伙伴…

gitlab2024最新版安装

系统&#xff1a;redhat9.0 gitlab版本&#xff1a;gitlab-ce-16.10.7-ce.0.el9.x86_64.rpm 安装组件&包依赖&#xff1a;https://packages.gitlab.com/gitlab/gitlab-ce/packages/ol/9/gitlab-ce-16.10.7-ce.0.el9.x86_64.rpm 参考&#xff1a; 前提&#xff1a; 下载gitl…

石英砂酸洗提纯方法和工艺

石英砂酸洗提纯方法和工艺是石英砂加工中至关重要的一个环节&#xff0c;其目的是通过化学手段去除石英砂中的杂质&#xff0c;提升其纯度。以下将详细介绍石英砂酸洗提纯的方法和工艺&#xff0c;以便更好地理解和应用这一技术。 一、概述 石英砂酸洗提纯主要是利用酸液对石英…

MySQL----表级锁行级锁排它锁和共享锁意向锁

MySQL的锁机制 锁&#xff08;Locking&#xff09;是数据库在并发访问时保证数据一致性和完整性的主要机制。在 MySQL 中&#xff0c;不同存储引擎使用不同的加锁方式&#xff1b;我们以 InnoDB 存储引擎为例介绍 MySQL 中的锁机制&#xff0c;其他存储引擎中的锁相对简单一些…

重大利好!亚马逊推出新功能,跨境商家销量或将迎来大爆发

亚马逊美亚站近日推出的一个新功能——帖子曝光。 顾名思义&#xff0c;帖子曝光这个功能可以提高卖家发布帖子的曝光度&#xff0c;吸引潜在用户&#xff0c;从而提升品牌影响力和产品销量。 亚马逊介绍&#xff0c;帖子曝光功能主要通过将品牌卖家的图文或视频帖子【一键】…

Nginx + KeepAlived高可用负载均衡集群

目录 一、Keepealived脑裂现象 1.现象 2.原因 3.解决 4.预防 二、实验部署 1.两台nginx做初始化操作并安装nginx 2.四层反向代理配置 3.配置高可用 4.准备检查nginx运行状态脚本 5.开启keepalived服务并测试 一、Keepealived脑裂现象 1.现象 主服务器和备服务器都同…

表面声波滤波器——叉指换能器(3)

叉指换能器(interdigital transducers&#xff0c;IDT) 是在压电基片表面激励和检测声表面波&#xff0c;从而实现电信号和声信号间的相互转换。 叉指换能器由在压电基片表面上沉积两组互相交错&#xff0c;周期分布的状金属条带(叉指电极)组成&#xff0c;每组电极和一个汇流…

【尚庭公寓SpringBoot + Vue 项目实战】后台岗位管理(十六)

【尚庭公寓SpringBoot Vue 项目实战】后台岗位管理&#xff08;十六&#xff09; 文章目录 【尚庭公寓SpringBoot Vue 项目实战】后台岗位管理&#xff08;十六&#xff09;1、业务说明2、逻辑模型介绍3、接口开发3.1、分页查询岗位信息3.2、保存或更新岗位信息3.3、根据ID删…

Unity2D游戏制作入门 | 14( 之人物实装攻击判定 )

上期链接&#xff1a;Unity2D游戏制作入门 | 13 ( 之人物三段攻击 )-CSDN博客 上期我们聊到给人物添加三段攻击的动画&#xff0c;通过建立新的图层动画当我们按下攻击按键就会自动切换进攻击的动画&#xff0c;如果我们连续按下攻击键&#xff0c;我们还可以进行好几段的攻击…

S32K3通过S32DS实现:S32K3如何将FLASH驱动放到RAM里面、RAM如何实现软件复位数据不丢失操作。

目录 1、概述 2、默认flash存放位置展示 3、通过默认的链接文件将flash放置到RAM 4、通过修改启动与链接文件将flash放在RAM 5、RAM热复位数据不丢失 1、概述 在通过RTD的SDK也好MCAL也好,始终存在一个问题,生成的代码除了看门狗模块,默认都是放在flash里面,按照正常逻…

自动统计一个学科领域中总共有多少研究者

问题&#xff1a;怎么知道一个学科领域中总共有多少研究者&#xff1f; 方法&#xff1a;学者的谷歌学术主页中会有对应的领域&#xff0c;以进化计算为例&#xff0c;进入一位进化计算学者的谷歌主页&#xff0c;然后进入标签“Evolutionary Computation”&#xff0c;只要知…

lombok不起作用排查

1.idea中lombok插件已安装并启用 2.idea中annotation processors已勾选 3.项目中gradle或maven已引入lombok依赖 但提示还是找不到get,set方法。 还需要启用annotationProcessor 重点是annotationProcessor的配置&#xff0c;没有配置这个才是问题出现的关键&#xff01;&…

CSS-0_1 CSS和层叠(样式优先级、内联样式、选择器 用户代理样式)

CSS 的本质就是声明规则 ——《深入解析CSS》 文章目录 CSS层叠和优先级用户代理样式请和用户代理样式和谐相处 选择器单选择器的优先级选择器组的优先级关于选择器的其他源码顺序尽可能的选择优先级低的选择器 内联样式内联样式和JavaScript !important多个 !important 碎碎念…

【计算机毕业设计】230疫苗预约weixin小程序

&#x1f64a;作者简介&#xff1a;拥有多年开发工作经验&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的项目或者毕业设计。 代码可以私聊博主获取。&#x1f339;赠送计算机毕业设计600个选题excel文件&#xff0c;帮助大学选题。赠送开题报告模板&#xff…

【Linux】Jenkins Pipeline流水线详解及基于Jenkins流水线实现自动更新项目(实战)

&#x1f468;‍&#x1f393;博主简介 &#x1f3c5;CSDN博客专家   &#x1f3c5;云计算领域优质创作者   &#x1f3c5;华为云开发者社区专家博主   &#x1f3c5;阿里云开发者社区专家博主 &#x1f48a;交流社区&#xff1a;运维交流社区 欢迎大家的加入&#xff01…

用这个神级提示词插件,能让你的AI绘画工具Stable diffusion提示词直接写中文!

大家好&#xff0c;我是设计师阿威 最近&#xff0c;有同学在使用AI绘画工具 Stable Diffusion的时候和我说&#xff1a;老师&#xff0c;我英文不好&#xff0c;能不能直接让我写中文提示词啊&#xff1f;最好可以直接在SD的输入框就能直接写中文&#xff0c;不用切换网页或者…

Git--Part4--多人协作

theme: nico 在之前的Git博客中&#xff0c;已经把Git本地相关的操作以及远程操作的介绍完了。如下&#xff1a; Git–Part1–基础操作 - 掘金 (juejin.cn)Git–Part2–分支管理 - 掘金 (juejin.cn)Git–Part3–远程操作 & 配置 & 标签管理 - 掘金 (juejin.cn) 这篇文…

vue大作业-端午节主题网站

vue大作业-端午节主题网站介绍 端午节&#xff0c;又称为龙舟节&#xff0c;是中国的传统节日之一&#xff0c;每年农历五月初五庆祝。这个节日不仅是纪念古代爱国诗人屈原的日子&#xff0c;也是家人团聚、共享美食的时刻。今天&#xff0c;我们非常高兴地分享一个以端午节为…

本地运行大语言模型(LLMs)

用例 像PrivateGPT、llama.cpp、Ollama、GPT4All、llamafile 等项目的流行度凸显了本地&#xff08;在您自己的设备上&#xff09;运行大型语言模型&#xff08;LLMs&#xff09;的需求。 这至少有两个重要的好处&#xff1a; 1.隐私&#xff1a;您的数据不会发送给第三方&a…

模拟 IQ 方法的知识

模拟 IQ 调制器&#xff08;用于发射器&#xff09;和 IQ 解调器&#xff08;用于接收器&#xff09;已经使用了数十年&#xff08;[1] 至 [3]&#xff09;。 近推出了新的A/D和D/A转换器&#xff0c;可以直接对1至4 GHz的IF进行采样&#xff1b;在第二、第三和第四奈奎斯特区…