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资源:USART1、USART2、USART3
2.1 USART框图和基本结构
下图为USART的框图
左边TX、RX、SW_RX等是USART的引脚部分,TX接到了发送移位寄存器和发送数据寄存器(TDR),RX接到了接收移位寄存器和接收数据寄存器(RDR)。移位寄存器就是把数据一位一位的移出去,对应着串口协议的波形数据位。数据寄存器是存放发送的数据字节或者接收的数据字节,程序上表现为一个寄存器DR,实际上是两个寄存器
当数据从TDR转移到发送移位寄存器,标志位TXE(发送寄存器空)就会置1,发送移位寄存器就会在发送器控制的驱动下,向右移位(低位先行),一位一位的把数据输出到TX,移位完成后,新的数据会再次自动的从TDR转到发送移位寄存器。接收部分同理
硬件流控可以避免数据丢弃或者覆盖数据的现象
SCLK时钟部分,针对发送部分,发送寄存器每移位依次,同步时钟电平就跳变一个周期
唤醒单元是实现串口挂载多设备
中断输出控制中的两个标志位TXE(发送寄存器空)和RXNE(接收寄存器非空)是判断发送状态和接收状态的必要标志位
波特率发生器就是分频器,APB时钟分频,USARTDIV是分频系数,时钟输入是fPCLKx,USART1挂载在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:电源
SCL、SDA:I2C通信线,已内置上拉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资源:I2C1、I2C2
下图为硬件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初始化(复用开漏输出) — 初始化I2C — I2C使能
下面为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的PA4、PA6、PA5和PA7。首先需要开启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资源:SPI1、SPI2
下图为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);
}