野火+STM32江科大高级篇USART SPI I2C CAN

想深耕嵌入式?这个专辑值得收藏

MCU、FPGA、工控、传感器一站式学习,实战项目直接抄

参考 原文链接:https://blog.csdn.net/weixin_54742551/article/details/134736523

目录

通信协议

串口

RS232

RS485

串口时序

通用同步异步收发器(USART)

USART框图

​编辑

引脚部分

数据寄存器

移位寄存器:

硬件流控

SCLK控制:

​编辑

几个小细节

采样

起始位侦测​编辑

数据采样

波特率发生器

USART 常用库函数

常用

不常用

USART 库函数案例

FILE

Serial_Printf详细解释

串口数据包

I2C

I2C通信的基础

通信协议设计的四个要求

I2C协议介绍

I2C的硬件规定

I2C通信的基本流程

I2C多主机与多从机通信

软件I2C与硬件I2C

硬件I2C

I2C协议的具体实现

软件I2C实现步骤:

硬件I2C实现步骤:

 库函数

软件I2c 

硬件I2c 

SPI

SPI 和 I2C 的比较:

SPI 具体使用时的考虑:

SPI时序基本单元

SPI 时序

SPI 数据传输步骤

W25Q64简介

W25Q64芯片的工作原理

硬件接线

编程和数据存储

常见应用

​编辑库函数 

软件SPI

W25Q64


通信协议

有时钟线(I2C的SCL 和SPI的SCLK)就是双方都按照这个时钟线进行通讯就是同步的,俗称波特率

串口

注意:在串口助手的接收模式中有文本模式和HEX模式两种模式,那么它们有什么区别?
  文本模式和Hex模式是两种不同的文件编辑或浏览模式,不是完全相同的概念。文本模式通常是指以ASCII编码格式表示文本文件的编辑或浏览模式。在文本模式下,文本文件的内容以可读的字符形式显示,包括字母、数字、符号等,这些字符被转换为计算机能够识别和处理的二进制编码。而Hex模式则是指以十六进制编码格式显示文件内容的编辑或浏览模式。在Hex模式下,文件的内容以16进制数值的形式显示,每个字节(byte)用两个十六进制数表示,从0x00到0xFF,可以查看文件的二进制编码,包括数据、指令、标志位等信息。因此,虽然文本模式和Hex模式都是用于文件编辑或浏览的模式,但它们的显示和处理方式不同,用途也不同。

RS232

RS485

波特率:

  本章中主要讲解的是串口异步通讯,异步通讯中由于没有时钟信号(如前面讲解的DB9接口中是没有时钟信号的), 所以两个通讯设备之间需要约定好波特率,即每个码元的长度,以便对信号进行解码, 图 串口数据包的基本组成中用虚线分开的每一格就是代表一个码元。常见的波特率为4800、9600、115200等。
  例如,如果每隔1秒发送一位,那么接收方也必须每隔1秒接收一位。如果接收方过早接收,则可能会重复接收某些位;如果接收方过晚接收,则可能会错过某些位。因此,发送方和接收方必须约定好传输速率,这个速率参数,就是波特率。那反应到波形上,比如我们双方规定波特率为1000bps,那就表示,1s要发1000位,每一位的时间就是1ms,发送方每隔1ms发送一位,接收方每隔1ms接收一位,这就是波特率,它决定了每隔多久发送一位。

串口时序

波特率是9600时间就是1/9600 sX 1000 000  =104us,低位先行

发送一个数据0x55 0101 0101 低位先行就是10101010

通用同步异步收发器(USART)

全双工异步

 这个同步模式,就是多了个时钟CLK的输出;硬件流控制,比如A设备的TX脚向B设备的RX脚发送数据,A设备一直在发,发的太快了,B处理不过来,如果没有硬件流控制,那B就只能抛弃新数据或者覆盖原数据了。如果有硬件流控制,在硬件电路上,会多出一根线,如果B没准备好接收,就置高电平,如果准备好了,就置低电平。A接收到了B反馈的准备信号,就只会在B准备好的时候,才发数据,如果B没准备好,那数据就不会发送出去。这就是硬件流控制,可以防止因为B处理慢而导致数据丢失的问题;之后DMA,是这个串口支持DMA进行数据转运,可以使用DMA转运数据,减轻CPU的负担;最后,智能卡、IrDA、LIN,这些是其他的一些协议。因为这些协议和串口是非常的像,所以STM32就对USART加了一些小改动,就能兼容这么多协议了,不过我们一般不用,像这些协议,Up主也都没用过。

USART框图

引脚部分

TX: 发送数据输出引脚。

RX: 接收数据输入引脚。

SCLK: 发送器时钟输出引脚。这个引脚仅适用于同步模式。

下面这里的SWRX、IRDA_OUT/IN这些是智能卡和IrDA通信的引脚,我们不用这些协议,所以这些引脚就不用管的。
SW_RX: 数据接收引脚,只用于单线和智能卡模式,属于内部引脚,没有具体外部引脚。
nRTS: 请求以发送(Request To Send),n表示低电平有效。如果使能RTS流控制,当USART接收器准备好接收新数据时就会将nRTS变成低电平; 当接收寄存器已满时,nRTS将被设置为高电平。该引脚只适用于硬件流控制。
nCTS: 清除以发送(Clear To Send),n表示低电平有效。如果使能CTS流控制,发送器在发送下一帧数据之前会检测nCTS引脚, 如果为低电平,表示可以发送数据,如果为高电平则在发送完当前数据帧之后停止发送。该引脚只适用于硬件流控制。

数据寄存器

发送移位寄存器接到TX                     RX 接到接收移位寄存器

USART_DR包含了已发送的数据或者接收到的数据。USART_DR实际是包含了两个寄存器,一个专门用于发送的可写TDR, 一个专门用于接收的可读RDR。这两个寄存器占用同一个地址在程序上,只表现为一个寄存器。当进行发送操作时,往USART_DR写入数据会自动存储在TDR内;当进行读取操作时,向USART_DR读取数据会自动提取RDR数据。

USART数据寄存器(USART_DR)只有低9位有效,并且第9位数据是否有效要取决于USART控制寄存器1(USART_CR1)的M位设置, 当M位为0时表示8位数据字长,当M位为1表示9位数据字长,我们一般使用8位数据字长。

  TDR和RDR都是介于系统总线和移位寄存器之间。串行通信是一个位一个位传输的,发送时把TDR内容转移到发送移位寄存器, 然后把移位寄存器数据每一位发送出去,接收时把接收到的每一位顺序保存在接收移位寄存器内然后才转移到RDR。
  USART支持DMA传输,可以实现高速数据传输,具体DMA使用将在DMA章节讲解。

移位寄存器:

注意一下,当TXE标志位置1时,数据其实还没有发送出去,只要数据从TDR转移到发送移位寄存器了,TXE就会置1,我们就可以写入新的数据了【就是发送数据寄存器里一直有数据,而发送移位寄存器里的数据一旦移位完成,那么发送数据寄存器里的数据就会立刻传输进入发送移位寄存器里再次传输】
  看一下接收端这里,也是类似的。数据从RX引脚通向接收移位寄存器,在接收器控制的驱动下,一位一位地读取RX电平,先放在最高位,然后向右移,移位8次之后,就能接收一个字节了。同样,因为串口协议规定是低位先行,所以接收移位寄存器是从高位往低位这个方向移动的。之后,当一个字节移位完成之后,这一个字节的数据就会整体地,一下子转移到接收数据寄存器RDR里来,在转移的过程中,也会置一个标志位叫RXNE (RXNot Empty),接收数据寄存器非空,当我们检测到RXNE置1之后,就可以把数据读走了。同样,这里也是两个寄存器进行缓存,当数据从移位寄存器转移到RDR时,就可以直接移位接收下一帧数据了。
  这就是USART外设整个的工作流程,其实讲到这里,这个外设的主要功能就差不多了。大体上,就是数据寄存器和移位寄存器,发送移位寄存器往TX引脚移位,接收移位寄存器从RX引脚移位。当然发送还需要加上帧头帧尾,接收还需要剔除帧头帧尾,这些操作,它内部有电路会自动执行。我们知道有硬件帮我们做了这些工作就行了

接着我们继续看一下下面的控制部分和一些其他的增强功能

硬件流控

 下面这里是发送器控制,它就是用来控制发送移位寄存器的工作的;接收器控制,用来控制接收移位寄存器的工作;然后左边这里,有一个硬件数据流控,也就是硬件流控制,简称流控。
  这里流控有两个引脚,一个是nRTS,一个是nCTS。nRTS(Request To Send)是请求发送,是输出脚,也就是告诉别人,我当前能不能接收;nCTS (Clear To Send)是清除发送,是输入脚,也就是用于接收别人nRTS的信号的。

这里前面加个n意思是低电平有效,那这两个脚上怎么玩的呢?
  首先,我们需要找到一个支持流控的串口,并将它的TX连接到我们的RX。同时,我们的RTS需要输出一个接收反馈信号,并将其连接到对方的CTS。当我们可以接收数据时,RTS会置为低电平,请求对方发送。对方的CTS接收到信号后,就可以继续发送数据。如果处理不过来,比如接收数据寄存器未及时读取,导致新数据无法接收,此时RTS会置为高电平,对方的CTS接收到信号后,就会暂停发送,直到接收数据寄存器被读取,RTS重新置为低电平,数据才会继续发送。
  当我们的TX向对方发送数据时,对方的RTS会连接到我们的CTS,用于判断对方是否可以接收数据。TX和CTS是一对对应的信号,RX和RTS也是一对对应的信号。此外,CTS和RTS之间也需要交叉连接,这就是流控的工作模式。然而,我们一般不使用流控,因此只需要了解一下即可。(少用原因应该是多消耗两根通信线)

SCLK控制:

接着继续看右边这个模块,这部分电路用于产生同步的时钟信号,它是配合发送移位寄存器输出的,发送寄存器每移位一次,同步时钟电平就跳变一个周期。时钟告诉对方,我移出去一位数据,你看要不要让我这个时钟信号来指导你接收一下?当然这个时钟只支持输出,不支持输入,所以两个USART之间,不能实现同步的串口通信。

那这个时钟信号有什么用呢?
兼容别的协议。比如串口加上时钟之后,就跟SPI协议特别像,所以有了时钟输出的串口,就可以兼容SPI。另外这个时钟也可以做自适应波特率,比如接收设备不确定发送设备给的什么波特率,然后再计算得到波特率,不过这就需要另外写程序来实现这个功能了。这个时钟功能,我们一般不用,所以也是了解一下就行。

这部分的作用是实现串口挂载多设备。我们之前说,串口一般是点对点的通信(只支持两个设备互相通信)。而多设备,在一条总线上,可以接多个从设备,每个设备分配一个地址,我想跟某个设备通信,就先进行寻址,确定通信对象。那回到这里,这个唤醒单元就可以用来实现多设备的功能,在这里可以给串口分配一个地址,当你发送指定地址时,此设备唤醒开始工作,当你发送别的设备地址时,别的设备就唤醒工作,这个设备没收到地址,就会保持沉默。这样就可以实现多设备的串口通信了,这部分功能我们一般不用。

中断申请位,就是状态寄存器这里的各种标志位,状态寄存器这里,有两个标志位比较重要,一个是TXE发送寄存器空,另一个是RXNE接收寄存器非空,这两个是判断发送状态和接收状态的必要标志位,剩下的标志位,了解一下就行。中断输出控制这里,就是配置中断是不是能通向NVIC,这个应该好理解

波特率发生器其实就是分频器,APB时钟进行分频,得到发送和接收移位的时钟。看一下,这里时钟输入是fPCLKx(x=1或2),(USART1挂载在APB2,所以就是PCLK2的时钟,一般是72M;其他的USART都挂载在APB1,所以是PCLK1的时钟,一般是36M)之后这个时钟进行一个分频,除一个USARTDIV的分频系数,并且分为了整数部分和小数部分,因为有些波特率,用72M除一个整数的话,可能除不尽,会有误差。所以这里分频系数是支持小数点后4位的,分频就更加精准,之后分频完之后,还要再除个16,得到发送器时钟和接收器时钟,通向控制部分。然后右边这里,如果TE (TX Enable)为1,就是发送器使能了,发送部分的波特率就有效;如果RE(RX Enable)为1,就是接收器使能了,接收部分的波特率就有效。

几个小细节

数据帧:

这个图,是在程序中配置8位字长和9位字长的波形对比。这里的字长,就是我们前面说的数据位长度。他这里的字长,是包含校验位的,是这种描述方式。

配置停止位:
那最后这些时钟什么的,和上面也都是类似的
接下来我们继续来看这个数据帧,看一下不同停止位的波形变化。STM32的串口,可以配置停止位长度为0.5、1、1.5、2,这四种。

采样

起始位侦测

  那之后,我们继续来看一些细节问题,这两个图展示的是USART电路输入数据的一些策略。对于串口来说,根据我们前面的介绍,可以想到,串口的输出TX应该是比输入RX简单很多,输出你就定时翻转TX引脚高低电平就行了。但是输入,就复杂一些。你不仅要保证,输入的采样频率和波特率一致,还要保证每次输入采样的位置,【要正好处于每一位的正中间,只有在每一位的正中间采样,这样高低电平读进来,才是最可靠的,如果你采样点过于靠前或靠后,那有可能高低电平还正在翻转,电平还不稳定,或者稍有误差,数据就采样错了】。另外,输入最好还要对噪声有一定的判断能力,如果是噪声,最好能置个标志位提醒我一下,这些就是输入数据所面临的问题。
那我们来看一下STM32是如何来设计输入电路的呢?

就是把原来一次采样的频率提高为原来的16倍速就可以采样16次更加精准,16次都是0就没噪声,那就是起始位。

第一个图展示了USART的起始位侦测。当输入电路侦测到数据帧的起始位后,将以波特率的频率连续采样一帧数据。同时,从起始位开始,采样位置要对齐到位的正中间。只要第一位对齐了,后面就都是对齐的。
  为了实现这些功能,输入电路对采样时钟进行了细分,以波特率的16倍频率进行采样。在一位的时间里,可以进行16次采样。比如最开始时,空闲状态为高电平,采样一直是1。在某个位置突然采到0,说明两次采样之间出现了下降沿,如果没有噪声,那之后就应该是起始位了。在起始位,会进行连续16次采样,没有噪声的话,这16次采样肯定都是0。但是实际电路还是会存在一些噪声,所以这里即使出现下降沿了,后续也要再采样几次以防万一。
  根据手册描述,接收电路在下降沿之后的第3次、5次、7次进行一批采样,在第8次、9次、10次再进行一批采样这两批采样都要求每3位里面至少应有2个0。如果没有噪声,那肯定全是0,满足情况;如果有一些轻微的噪声导致3位里面只有两个0,另一个是1,那也算是检测到了起始位(但是在状态寄存器里会置一个NE(Noise Error),提醒你数据收到了但是有噪声,你悠着点用);如果3位里面只有1个0,那就不算检测到了起始位,可能前面那个下降沿是噪声导致的,这时电路就忽略前面的数据重新开始捕捉下降沿。
  这就是STM32的串口在接收过程中对噪声的处理。如果通过了这个起始位侦测那接收状态就由空闲变为接收起始位同时第8、9、10次采样的位置就正好是起始位的正中间。之后接收数据位时就在第8、9、10次进行采样这样就能保证采样位置在位的正中间了。这就是起始位侦测和采样位置对齐的策略。

数据采样

同样采样16次,8 9 10都相同的时候就说明采样到了

波特率发生器

为什么这里公式有个16,因为它内部还有一个16倍波特率的采样时钟,所以这里输入时钟/DV要等于16倍的波特率,最终计算波特率,自然要多除一个16了。
举个例子,比如我要配置USART1为9600的波特率,那如何配置这个BRR寄存器呢?
我们代入公式,就是9600等于 USART1的时钟是72M 除 16倍的DIV,解得,DIV=72M/9600/16,最终等于468.75,则二进制数是11101 0100.11v。所以最终写到这个寄存器就是整数部分为11101 0100,前面多出来的补0,小数部分为11,后面多出来的补0。这就是根据波特率写BRR寄存器的方法,了解一下,不过,我们用库函数配置的话,就非常方便,需要多少波特率,直接写就行了,库函数会自动帮我们算。

USB转串口模块的内部电路图

USART 常用库函数

常用

/**
  * @brief  将指定的USART外设寄存器重置为其默认复位值。
  * @param  USARTx: x 可以是 1, 2, 3, 4 或 5,用于选择USART外设。
  * @retval 无
  */
void USART_DeInit(USART_TypeDef* USARTx);

/**
  * @brief  根据 USART_InitStruct 中指定的参数初始化 USARTx 外设。
  * @param  USARTx: x 可以是 1, 2, 3, 4 或 5,用于选择USART外设。
  * @param  USART_InitStruct: 指向 USART 初始化结构的指针,该结构包含了初始化配置信息。
  * @retval 无
  */
void USART_Init(USART_TypeDef* USARTx, USART_InitTypeDef* USART_InitStruct);

/**
  * @brief  填充每个USART_InitStruct成员的默认值。
  * @param  USART_InitStruct: 指向USART初始化结构的指针。
  * @retval 无
  */
void USART_StructInit(USART_InitTypeDef* USART_InitStruct);

/**
  * @brief  根据 USART_ClockInitStruct 中指定的参数初始化 USARTx 的时钟。
  * @param  USARTx: x 可以是 1, 2, 3, 4 或 5,用于选择USART外设。
  * @param  USART_ClockInitStruct: 指向 USART 时钟初始化结构的指针,该结构包含了时钟配置的信息。
  * @retval 无
  */
void USART_ClockInit(USART_TypeDef* USARTx, USART_ClockInitTypeDef* USART_ClockInitStruct);

/**
  * @brief  填充每个 USART_ClockInitStruct 成员的默认值。
  * @param  USART_ClockInitStruct: 指向USART时钟初始化结构的指针。
  * @retval 无
  */
void USART_ClockStructInit(USART_ClockInitTypeDef* USART_ClockInitStruct);

/**
  * @brief  启用或禁用指定的 USART 外设。
  * @param  USARTx: x 可以是 1, 2, 3, 4 或 5,用于选择USART外设。
  * @param  NewState: USART 外设的新状态。可以是: ENABLE 或 DISABLE。
  * @retval 无
  */
void USART_Cmd(USART_TypeDef* USARTx, FunctionalState NewState);

/**
  * @brief  配置指定的USART中断。
  * @param  USARTx: x 可以是 1, 2, 3, 4 或 5,用于选择USART外设。
  * @param  USART_IT: 指定要启用或禁用的 USART 中断源。
  * @param  NewState: 中断的新状态。可以是: ENABLE 或 DISABLE。
  * @retval 无
  */
void USART_ITConfig(USART_TypeDef* USARTx, uint16_t USART_IT, FunctionalState NewState);

/**
  * @brief  启用或禁用指定的 USART DMA 请求。
  * @param  USARTx: x 可以是 1, 2, 3, 4 或 5,用于选择USART外设。
  * @param  USART_DMAReq: 指定 DMA 请求源。
  * @param  NewState: DMA 请求的新状态。可以是: ENABLE 或 DISABLE。
  * @retval 无
  */
void USART_DMACmd(USART_TypeDef* USARTx, uint16_t USART_DMAReq, FunctionalState NewState);

/**
  * @brief  设置USART节点地址(用于多处理器通信模式)。
  * @param  USARTx: x 可以是 1, 2, 3, 4 或 5,用于选择USART外设。
  * @param  USART_Address: 指定节点地址。
  * @retval 无
  */
void USART_SetAddress(USART_TypeDef* USARTx, uint8_t USART_Address);

/**
  * @brief  选择 USART 多处理器通信模式的唤醒方法。
  * @param  USARTx: x 可以是 1, 2, 3, 4 或 5,用于选择USART外设。
  * @param  USART_WakeUp: 指定唤醒方法。
  * @retval 无
  */
void USART_WakeUpConfig(USART_TypeDef* USARTx, uint16_t USART_WakeUp);

/**
  * @brief  启用或禁用 USART 接收器的空闲检测。
  * @param  USARTx: x 可以是 1, 2, 3, 4 或 5,用于选择USART外设。
  * @param  NewState: 接收器空闲检测的新状态。可以是: ENABLE 或 DISABLE。
  * @retval 无
  */
void USART_ReceiverWakeUpCmd(USART_TypeDef* USARTx, FunctionalState NewState);

/**
  * @brief  配置 LIN 模式的断开检测长度。
  * @param  USARTx: x 可以是 1, 2, 3, 4 或 5,用于选择USART外设。
  * @param  USART_LINBreakDetectLength: 指定 LIN 断开检测长度。
  * @retval 无
  */

void USART_SendData(USART_TypeDef* USARTx, uint16_t Data);

/**
  * @brief  通过 USARTx 接收单个数据。
  * @param  USARTx: x 可以是 1, 2, 3, 4 或 5,用于选择USART外设。
  * @retval 接收到的数据。
  */
uint16_t USART_ReceiveData(USART_TypeDef* USARTx);

/**
  * @brief  检查是否设置了指定的 USART 标志。
  * @param  USARTx: x 可以是 1, 2, 3, 4 或 5,用于选择USART外设。
  * @param  USART_FLAG: 指定要检查的标志。
  * @retval FlagStatus: 标志的状态(SET 或 RESET)。
  */
FlagStatus USART_GetFlagStatus(USART_TypeDef* USARTx, uint16_t USART_FLAG);

/**
  * @brief  清除指定的 USART 标志。
  * @param  USARTx: x 可以是 1, 2, 3, 4 或 5,用于选择USART外设。
  * @param  USART_FLAG: 指定要清除的标志。
  * @retval 无
  */
void USART_ClearFlag(USART_TypeDef* USARTx, uint16_t USART_FLAG);

/**
  * @brief  检查指定的 USART 中断是否发生。
  * @param  USARTx: x 可以是 1, 2, 3, 4 或 5,用于选择USART外设。
  * @param  USART_IT: 指定要检查的 USART 中断源。
  * @retval ITStatus: 中断状态(SET 或 RESET)。
  */
ITStatus USART_GetITStatus(USART_TypeDef* USARTx, uint16_t USART_IT);

/**
  * @brief  清除 USART 的中断挂起位。
  * @param  USARTx: x 可以是 1, 2, 3, 4 或 5,用于选择USART外设。
  * @param  USART_IT: 指定要清除的 USART 中断源。
  * @retval 无
  */
void USART_ClearITPendingBit(USART_TypeDef* USARTx, uint16_t USART_IT);

不常用


/**
  * @brief  启用或禁用指定的USART LIN模式。
  * @param  USARTx: x 可以是 1, 2, 3, 4 或 5,用于选择USART外设。
  * @param  NewState: LIN模式的新状态。可以是: ENABLE 或 DISABLE。
  * @retval 无
  */
void USART_LINCmd(USART_TypeDef* USARTx, FunctionalState NewState);

/**
  * @brief  通过 USARTx 发送单个数据。
  * @param  USARTx: x 可以是 1, 2, 3, 4 或 5,用于选择USART外设。
  * @param  Data: 要发送的数据。
  * @retval 无
  */
/**
  * @brief  通过 USARTx 发送断开信号。
  * @param  USARTx: x 可以是 1, 2, 3, 4 或 5,用于选择USART外设。
  * @retval 无
  */
void USART_SendBreak(USART_TypeDef* USARTx);

/**
  * @brief  设置 SmartCard 模式下的保护时间。
  * @param  USARTx: x 可以是 1, 2, 3, 4 或 5,用于选择USART外设。
  * @param  USART_GuardTime: 保护时间值。
  * @retval 无
  */
void USART_SetGuardTime(USART_TypeDef* USARTx, uint8_t USART_GuardTime);

/**
  * @brief  设置 SmartCard 模式下的预分频器值。
  * @param  USARTx: x 可以是 1, 2, 3, 4 或 5,用于选择USART外设。
  * @param  USART_Prescaler: 预分频器值。
  * @retval 无
  */
void USART_SetPrescaler(USART_TypeDef* USARTx, uint8_t USART_Prescaler);

/**
  * @brief  启用或禁用指定的 SmartCard 模式。
  * @param  USARTx: x 可以是 1, 2, 3, 4 或 5,用于选择USART外设。
  * @param  NewState: SmartCard 模式的新状态。可以是: ENABLE 或 DISABLE。
  * @retval 无
  */
void USART_SmartCardCmd(USART_TypeDef* USARTx, FunctionalState NewState);

/**
  * @brief  启用或禁用 SmartCard 模式下的 NACK 传输。
  * @param  USARTx: x 可以是 1, 2, 3, 4 或 5,用于选择USART外设。
  * @param  NewState: NACK 传输的新状态。可以是: ENABLE 或 DISABLE。
  * @retval 无
  */
void USART_SmartCardNACKCmd(USART_TypeDef* USARTx, FunctionalState NewState);

/**
  * @brief  启用或禁用指定的半双工模式。
  * @param  USARTx: x 可以是 1, 2, 3, 4 或 5,用于选择USART外设。
  * @param  NewState: 半双工模式的新状态。可以是: ENABLE 或 DISABLE。
  * @retval 无
  */
void USART_HalfDuplexCmd(USART_TypeDef* USARTx, FunctionalState NewState);

/**
  * @brief  启用或禁用 8 倍过采样模式。
  * @param  USARTx: x 可以是
/**
  * @brief  启用或禁用 8 倍过采样模式。
  * @param  USARTx: x 可以是 1, 2, 3, 4 或 5,用于选择USART外设。
  * @param  NewState: 8 倍过采样模式的新状态。可以是: ENABLE 或 DISABLE。
  * @retval 无
  */
void USART_OverSampling8Cmd(USART_TypeDef* USARTx, FunctionalState NewState);

/**
  * @brief  启用或禁用一个采样方法。
  * @param  USARTx: x 可以是 1, 2, 3, 4 或 5,用于选择USART外设。
  * @param  NewState: 一个采样方法的新状态。可以是: ENABLE 或 DISABLE。
  * @retval 无
  */
void USART_OneBitMethodCmd(USART_TypeDef* USARTx, FunctionalState NewState);

/**
  * @brief  配置 USART 的 IrDA 通信模式。
  * @param  USARTx: x 可以是 1, 2, 3, 4 或 5,用于选择USART外设。
  * @param  USART_IrDAMode: 指定 IrDA 模式。
  * @retval 无
  */
void USART_IrDAConfig(USART_TypeDef* USARTx, uint16_t USART_IrDAMode);

/**
  * @brief  启用或禁用指定的 IrDA 模式。
  * @param  USARTx: x 可以是 1, 2, 3, 4 或 5,用于选择USART外设。
  * @param  NewState: IrDA 模式的新状态。可以是: ENABLE 或 DISABLE。
  * @retval 无
  */
void USART_IrDACmd(USART_TypeDef* USARTx, FunctionalState NewState);

USART 库函数案例

#include "stm32f10x.h"                  // 设备头文件
#include <stdio.h>
#include <stdarg.h>

/**
  * @brief  初始化串口1。
  * @param  无
  * @retval 无
  *
  * 主要功能:
  * - 启用USART1和GPIOA的时钟。
  * - 配置PA9引脚为复用推挽输出模式,用于USART1的TX功能。
  * - 配置USART1的波特率、模式、奇偶校验位、停止位和字长。
  * - 使能USART1外设。
  */
void Serial_Init(void)
{
	/* 开启时钟 */
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);	// 开启USART1的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);	// 开启GPIOA的时钟
	
	/* GPIO初始化 */
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;				// 设置为复用推挽输出模式
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;					// 选择PA9引脚
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;			// 设置GPIO速度为50MHz
	GPIO_Init(GPIOA, &GPIO_InitStructure);						// 初始化GPIOA引脚
	
	/* USART初始化 */
	USART_InitTypeDef USART_InitStructure;						// 定义USART初始化结构体
	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);					// 应用配置到USART1
	
	/* USART使能 */
	USART_Cmd(USART1, ENABLE);									// 使能USART1
}

/**
  * @brief  发送一个字节的数据到USART1。
  * @param  Byte: 要发送的字节数据。
  * @retval 无
  *
  * 主要功能:
  * - 将一个字节数据发送到USART1。
  * - 等待数据发送完成。
  */
void Serial_SendByte(uint8_t Byte)
{
	USART_SendData(USART1, Byte);								// 发送数据
	while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);	// 等待发送完成
}

/**
  * @brief  发送一个字节数组到USART1。
  * @param  Array: 指向要发送的字节数组的指针。
  * @param  Length: 数组的长度。
  * @retval 无
  *
  * 主要功能:
  * - 遍历数组,并逐个发送数组中的字节数据。
  */
void Serial_SendArray(uint8_t *Array, uint16_t Length)
{
	uint16_t i;
	for (i = 0; i < Length; i ++)	// 遍历数组
	{
		Serial_SendByte(Array[i]);	// 发送每个字节
	}
}

/**
  * @brief  发送一个字符串到USART1。
  * @param  String: 指向要发送的字符串的指针。
  * @retval 无
  *
  * 主要功能:
  * - 遍历字符串并逐个发送字符。
  */
void Serial_SendString(char *String)
{
	uint8_t i;
	for (i = 0; String[i] != '\0'; i ++)	// 遍历字符串,直到遇到结束符
	{
		Serial_SendByte(String[i]);		// 发送每个字符
	}
}

/**
  * @brief  计算 X 的 Y 次方。
  * @param  X: 基数。
  * @param  Y: 指数。
  * @retval 计算结果。
  *
  * 主要功能:
  * - 计算 X 的 Y 次方并返回结果。
  */
uint32_t Serial_Pow(uint32_t X, uint32_t Y)
{
	uint32_t Result = 1;	// 初始化结果为1
	while (Y --)			// 执行 Y 次
	{
		Result *= X;		// 结果乘以 X
	}
	return Result;
}

/**
  * @brief  发送一个数字到USART1。
  * @param  Number: 要发送的数字,范围是 0 到 4294967295。
  * @param  Length: 数字的长度,范围是 0 到 10。
  * @retval 无
  *
  * 主要功能:
  * - 将数字按照指定的长度格式化为字符串并发送。
  */
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  重定向 printf 函数到串口发送函数。
  * @param  ch: 要发送的字符。
  * @param  f: 文件指针,保持原样即可。
  * @retval 发送的字符。
  *
  * 主要功能:
  * - 将 printf 输出重定向到 Serial_SendByte 函数。
  */
int fputc(int ch, FILE *f)
{
	Serial_SendByte(ch);			// 发送字符
	return ch;
}

/**
  * @brief  使用格式化字符串通过串口发送数据。
  * @param  format: 格式化字符串。
  * @param  ...: 可变参数列表。
  * @retval 无
  *
  * 主要功能:
  * - 格式化字符串和参数,并通过串口发送。
  */
void Serial_Printf(char *format, ...)
{
	char String[100];				// 定义字符数组
	va_list arg;					// 定义可变参数列表
	va_start(arg, format);			// 初始化可变参数列表
	vsprintf(String, format, arg);	// 格式化字符串
	va_end(arg);					// 结束可变参数列表
	Serial_SendString(String);		// 发送格式化后的字符串
}

FILE

在函数原型 `int fputc(int ch, FILE *f)` 中,`FILE *f` 是一个指向 `FILE` 结构体的指针,这个结构体在标准库中用于表示文件流。具体来说:

- `FILE` 是一个结构体类型,用于处理文件和流输入输出。
- `FILE *f` 是一个指向 `FILE` 结构体的指针,用于指定要操作的文件流。

在这个函数原型中:

- `ch` 是要写入文件的字符。
- `f` 是一个指向 `FILE` 结构体的指针,指明了要将字符写入哪个文件流。通常在实际使用中,这个参数可以是标准输入流、标准输出流或标准错误流等。

在函数实现中:

- `f` 这个参数在 `fputc` 的实现中通常不被使用,因为在 STM32 的 `fputc` 函数重定向中,它的主要目的是将字符输出到串口,而不是文件。这个参数的存在是为了兼容标准库的函数接口规范。

简而言之,`FILE *f` 在这个上下文中表示文件流的指针,但由于在 STM32 的 `fputc` 实现中它并没有实际用途,所以它只是保留了标准库接口的格式。

`Serial_Printf` 函数是一个自定义的串口打印函数,目的是提供类似于 `printf` 的格式化输出功能,但输出目标是串口而不是标准输出。以下是函数的详细注释:

```c
/**
  * 函数名称:Serial_Printf
  * 函数功能:通过串口发送格式化的字符串
  * 参    数:
  *   - format:格式化字符串,包含文本和格式说明符,例如 "%d"、"%s" 等。
  *   - ...:可变参数列表,根据格式化字符串中的格式说明符提供具体的值。
  * 返 回 值:无
  * 说明:
  *   1. 使用 `vsprintf` 将格式化字符串和可变参数列表处理为一个字符串。
  *   2. 调用 `Serial_SendString` 发送格式化后的字符串到串口。
  */
void Serial_Printf(char *format, ...)
{
    char String[100];                // 定义一个字符数组用于存储格式化后的字符串
    va_list arg;                    // 定义一个 va_list 类型的变量用于存储可变参数列表
    va_start(arg, format);          // 初始化 va_list 变量,将其指向可变参数列表的开始
    vsprintf(String, format, arg); // 使用 vsprintf 处理格式化字符串和参数列表,将结果存储到 String 数组中
    va_end(arg);                    // 结束对可变参数列表的访问
    Serial_SendString(String);      // 将格式化后的字符串发送到串口
}
```

Serial_Printf详细解释

1. **函数定义**:
   - `Serial_Printf` 是一个自定义函数,功能类似于标准库中的 `printf`,但专门用于串口输出。

2. **参数说明**:
   - `format`:格式化字符串,其中可能包含格式说明符,如 `%d`、`%s` 等,用于指定如何格式化后续的参数。
   - `...`:可变参数列表,根据 `format` 中的格式说明符提供具体的值。这些参数的个数和类型在运行时决定。

3. **函数实现**:
   - **字符数组 `String`**:用于存储最终的格式化字符串。
   - **`va_list arg`**:`va_list` 类型用于存储可变参数列表的状态。
   - **`va_start(arg, format)`**:初始化 `arg` 以指向 `format` 参数之后的可变参数。
   - **`vsprintf(String, format, arg)`**:格式化字符串并将结果存储到 `String` 中。
   - **`va_end(arg)`**:结束对可变参数列表的访问。
   - **`Serial_SendString(String)`**:通过串口发送格式化后的字符串。

### 注意事项
- `vsprintf` 函数处理格式化字符串,并将格式化后的结果存储在 `String` 数组中。确保 `String` 数组有足够的空间以容纳格式化后的字符串。
- `Serial_SendString` 是自定义的函数,用于将字符串通过串口发送出去。它会逐个字节发送 `String` 中的内容。

这个函数允许你将格式化的输出通过串口发送到外部设备,适用于调试信息和数据传输等场景。

串口数据包

如果要发送很多个数据的时候比如陀螺仪的X Y Z但接受的哪一方不知道哪一个是X Y Z所以,把这些数据打包就可以很好的规避这个问题 给他们排序第一个X第二个Y第三个Z

接着我们来研究几个问题:

第一个问题:包头包尾和数据载荷重复的问题,这里定义FF为包头,FE为包尾,如果我传输的数据本身就是FF和FE怎么办呢?那这个问题确实存在,如果数据和包头包尾重复,可能会引起误判。对应这个问题我们有如下几种解决方法:第一种,限制载荷数据的范围。如果可以的话,我们可以在发送的时候,对数据进行限幅,比如XYZ,3个数据,变化范围都可以是0~100 那就好办了,我们可以在载荷中只发送0-100的数据,这样就不会和包头包尾重复了;第二种,如果无法避免载荷数据和包头包尾重复,那我们就尽量使用固定长度的数据包。这样由于载荷数据是固定的,只要我们通过包头包尾对齐了数据,我们就可以严格知道,哪个数据应该是包头包尾,哪个数据应该是载荷数据。在接收载荷数据的时候,我们并不会判断它是否是包头包尾,而在接收包头包尾的时候,我们会判断它是不是确实是包头包尾,用于数据对齐。这样,在经过几个数据包的对齐之后,剩下的数据包应该就不会出现问题了;第三种,增加包头包尾的数量,并且尽量让它呈现出载荷数据出现不了的状态。比如我们使用FF、FE作为包头,FD、FC作为包尾,这样也可以避免载荷数据和包头包尾重复的情况发生
第二个问题:这个包头包尾并不是全部都需要的,比如我们可以只要一个包头,把包尾删掉,这样数据包的格式就是,一个包头FF,加4个数据,这样也是可以的。当检测到FF,开始接收,收够4个字节后,置标志位,一个数据包接收完成,这样也可以。不过这样的话,载荷和包头重复的问题会更严重一些,比如最严重的情况下,我载荷全是FF,包头也是FF,那你肯定不知道哪个是包头了,而加上了FE作为包尾,无论数据怎么变化,都是可以分辨出包头包尾的。
第三个问题:固定包长和可变包长的选择问题,对应HEX数据包来说,如果你的载荷会出现和包头包尾重复的情况,那就最好选择固定包长,这样可以避免接收错误,如果你又会重复,又选择可变包长那数据很容易就乱套了;如果载荷不会和包头包尾重复,那可以选择可变包长,数据长度,像这样,4位、3位、等等,1位、10位,来回任意变,肯定都没问题。因为包头包尾是唯一的,只要出现包头,就开始数据包,只要出现包尾,就结束数据包,这样就非常灵活了,这就是固定包长和可变包长选择的问题。
最后一个问题:各种数据转换为字节流的问题。这里数据包都是一个字节一个字节组成的,如果你想发送16位的整型数据、32位的整型数据,float、double,甚至是结构体,其实都没问题,因为它们内部其实都是由一个字节一个字节组成的,只需要用一个uint8_t的指针指向它,把它们当做一个字节数组发送就行了。
好,有关HEX数据包定义的内容,就讲这么多,接下来看一下文本数据包。

 文本数据包和HEX数据包分别对应了文本模式和HEX模式。在HEX数据包中,数据以原始字节形式呈现。而在文本数据包中,每个字节经过了一层编码和译码,最终以文本格式呈现。实际上,每个文本字符背后都有一个字节的HEX数据。
  综上所述,我们需要根据实际场景来选择和设计数据包格式。在需要直接传输和简单解析原始数据的情况下,HEX数据包是更好的选择。而在需要输入指令进行人机交互的场合,文本数据包则更为适用。

  好,数据包格式的定义讲完了,接下来我们就来学一下数据包的收发流程。
  首先,发送数据包的过程相对简单。在发送HEX数据包时,可以通过定义一个数组,填充数据,然后使用之前我们写过的SendArray函数发送即可。在发送文本数据包时,可以通过写一个字符串,然后调用SendString函数发送。因此,发送数据包的过程是可控的,我们可以根据需要发送任何类型的数据包。相比之下,接收数据包的过程较为复杂。
  那接下来,接收一个数据包,这就比较复杂了,我们来学习一下,我这里演示了固定包长HEX数据包的接收方法,和可变包长文本数据包的接收方法,其他的数据包也都可以套用这个形式,等会儿我们写程序就会根据这里面的流程来。

  我们先看一下如何来接收这个固定包长的HEX数据包。要接收固定包长的HEX数据包,我们需要设计一个状态机来处理。根据之前的代码,我们知道每当收到一个字节,程序会进入中断。在中断函数里,我们可以获取这个字节,但获取后需要退出中断。因此,每个收到的数据都是独立的过程,而数据包则具有前后关联性,包括包头、数据和包尾。为了处理这三种状态,我们需要设计一个能够记住不同状态的机制,并在不同状态下执行不同的操作,同时进行状态合理转移。这种程序设计思维就是“状态机”。

 这就是使用状态机接收数据包的思路。这个状态机其实是一种很广泛的编程思路,在很多地方都可以用到,使用的基本步骤是,先根据项目要求定义状态,画几个圈,然后考虑好各个状态在什么情况下会进行转移,如何转移,画好线和转移条件,最后根据这个图来进行编程,这样思维就会非常清晰了。
  那接下来继续,我们来看一下这个可变包长、文本数据包的接收流程。

 好,到这里,我们这个数据包的,定义、分类、优缺点和注意事项,就讲完了,接下来,我们就来写程序,验证一下刚才所学的内容吧。

#include "stm32f10x.h"                  // 设备头文件
#include <stdio.h>
#include <stdarg.h>

uint8_t Serial_TxPacket[4];				// 定义发送数据包数组,数据包格式:FF 01 02 03 04 FE
uint8_t Serial_RxPacket[4];				// 定义接收数据包数组
uint8_t Serial_RxFlag;					// 定义接收数据包标志位

/**
  * 函数名:Serial_Init
  * 功能:初始化串口,包括配置时钟、GPIO、USART及中断
  * 参 数:无
  * 返回值:无
  */
void Serial_Init(void)
{
	/* 开启时钟 */
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);	// 开启USART1的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);	// 开启GPIOA的时钟
	
	/* GPIO初始化 */
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;			// 设置PA9引脚为复用推挽输出模式
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);					// 初始化PA9引脚
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;				// 设置PA10引脚为上拉输入模式
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);					// 初始化PA10引脚
	
	/* 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);				// 配置USART1
	
	/* 中断输出配置 */
	USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);			// 开启串口接收数据中断
	
	/* NVIC中断分组配置 */
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);			// 配置NVIC为分组2
	
	/* NVIC配置 */
	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;		// 选择USART1中断通道
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;			// 使能USART1中断
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;	// 设置抢占优先级为1
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;		// 设置响应优先级为1
	NVIC_Init(&NVIC_InitStructure);							// 配置NVIC
	
	/* 使能USART */
	USART_Cmd(USART1, ENABLE);								// 使能USART1,开始串口通信
}

/**
  * 函数名:Serial_SendByte
  * 功能:通过串口发送一个字节
  * 参 数:Byte 要发送的字节数据
  * 返回值:无
  */
void Serial_SendByte(uint8_t Byte)
{
	USART_SendData(USART1, Byte);		// 将字节数据写入数据寄存器
	while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);	// 等待发送完成
	/* 发送完成标志位会在下一次写入数据寄存器时自动清除 */
}

/**
  * 函数名:Serial_SendArray
  * 功能:通过串口发送一个字节数组
  * 参 数:Array 要发送数组的首地址
  * 参 数:Length 要发送数组的长度
  * 返回值:无
  */
void Serial_SendArray(uint8_t *Array, uint16_t Length)
{
	uint16_t i;
	for (i = 0; i < Length; i ++)		// 遍历数组
	{
		Serial_SendByte(Array[i]);		// 依次发送数组中的每个字节
	}
}

/**
  * 函数名:Serial_SendString
  * 功能:通过串口发送一个字符串
  * 参 数:String 要发送字符串的首地址
  * 返回值:无
  */
void Serial_SendString(char *String)
{
	uint8_t i;
	for (i = 0; String[i] != '\0'; i ++) // 遍历字符串,直到遇到结束符
	{
		Serial_SendByte(String[i]);		// 依次发送字符串中的每个字符
	}
}

/**
  * 函数名:Serial_Pow
  * 功能:计算X的Y次方
  * 参 数:X 底数
  * 参 数:Y 指数
  * 返回值:X的Y次方的结果
  */
uint32_t Serial_Pow(uint32_t X, uint32_t Y)
{
	uint32_t Result = 1;	// 初始结果为1
	while (Y --)			// 执行Y次
	{
		Result *= X;		// 结果乘以X
	}
	return Result;
}

/**
  * 函数名:Serial_SendNumber
  * 功能:通过串口发送一个数字
  * 参 数:Number 要发送的数字,范围:0~4294967295
  * 参 数:Length 要发送数字的长度,范围:0~10
  * 返回值:无
  */
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');	// 发送每一位数字
	}
}

/**
  * 函数名:fputc
  * 功能:重定向printf函数到Serial_SendByte
  * 参 数:ch 要发送的字符
  * 参 数:f 文件指针(保持原样,无需变动)
  * 返回值:发送的字符
  */
int fputc(int ch, FILE *f)
{
	Serial_SendByte(ch);			// 使用Serial_SendByte发送字符
	return ch;
}

/**
  * 函数名:Serial_Printf
  * 功能:封装的printf函数,通过串口发送格式化字符串
  * 参 数:format 格式化字符串
  * 参 数:... 可变参数列表
  * 返回值:无
  */
void Serial_Printf(char *format, ...)
{
	char String[100];				// 定义字符数组用于存储格式化字符串
	va_list arg;					// 定义可变参数列表
	va_start(arg, format);			// 初始化可变参数列表
	vsprintf(String, format, arg);	// 将格式化字符串和参数列表打印到字符数组中
	va_end(arg);					// 结束可变参数列表的访问
	Serial_SendString(String);		// 发送格式化后的字符串
}

/**
  * 函数名:Serial_SendPacket
  * 功能:通过串口发送数据包
  * 参 数:无
  * 返回值:无
  * 说明:将Serial_TxPacket数组内容包裹在包头(FF)和包尾(FE)之间发送
  */
void Serial_SendPacket(void)
{
	Serial_SendByte(0xFF);			// 发送包头
	Serial_SendArray(Serial_TxPacket, 4);	// 发送数据包内容
	Serial_SendByte(0xFE);			// 发送包尾
}

/**
  * 函数名:Serial_GetRxFlag
  * 功能:获取串口接收数据包标志位
  * 参 数:无
  * 返回值:接收数据包标志位,范围:0~1,接收到数据包后,标志位置1,读取后标志位自动清零
  */
uint8_t Serial_GetRxFlag(void)
{
	if (Serial_RxFlag == 1)			// 如果标志位为1
	{
		Serial_RxFlag = 0;			// 清零标志位
		return 1;				
		return 1;					// 返回1,表示数据包已接收
	}
	return 0;						// 返回0,表示没有接收到数据包
}

/**
  * 函数名:USART1_IRQHandler
  * 功能:USART1中断处理函数
  * 参 数:无
  * 返回值:无
  * 说明:当USART1接收到数据时触发此中断,处理数据接收和解析
  */
void USART1_IRQHandler(void)
{
	static uint8_t RxState = 0;		// 定义状态机的状态变量
	static uint8_t pRxPacket = 0;	// 定义接收数据包的当前索引位置
	if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)	// 检查是否接收到数据中断
	{
		uint8_t RxData = USART_ReceiveData(USART1);		// 从数据寄存器读取接收到的数据
		
		/* 使用状态机的思路处理数据包的不同部分 */
		
		/* 状态0:接收数据包的包头 */
		if (RxState == 0)
		{
			if (RxData == 0xFF)		// 如果数据是包头
			{
				RxState = 1;		// 切换到下一个状态
				pRxPacket = 0;		// 数据包索引重置
			}
		}
		/* 状态1:接收数据包的内容 */
		else if (RxState == 1)
		{
			Serial_RxPacket[pRxPacket] = RxData;	// 将接收到的数据存入数据包数组
			pRxPacket ++;		// 数据包索引递增
			if (pRxPacket >= 4)	// 如果接收完4个字节的数据
			{
				RxState = 2;	// 切换到下一个状态
			}
		}
		/* 状态2:接收数据包的包尾 */
		else if (RxState == 2)
		{
			if (RxData == 0xFE)		// 如果数据是包尾
			{
				RxState = 0;		// 状态重置为0
				Serial_RxFlag = 1;	// 设置接收数据包标志位为1,表示成功接收一个数据包
			}
		}
		
		USART_ClearITPendingBit(USART1, USART_IT_RXNE);	// 清除接收中断标志位
	}
}

I2C

本节课程有两个代码,一个是通过软件I2C读写MPU6050,另一个是通过硬件I2C读写MPU6050。这两个代码实现的效果是一样的。首先,我们来看一下软件I2C读写MPU6050的程序现象。

目前,我们通过软件I2C通信,对MPU6050芯片内部的寄存器进行读写。通过写入配置寄存器,可以配置外挂模块;通过读取数据寄存器,可以获取外挂模块的数据。最终,我们读出的数据会显示在OLED屏幕上。

其中,最上面的数据是设备的ID号。MPU6050的ID号固定为0x68,通常我们可以通过读取这个ID号来验证I2C通信是否正常。另外,我之前还测试了不同批次的芯片,发现有些芯片的ID号是0x98。ID号可能会有所不同,但只要数据读取没有问题,这也不影响通信的正常运行。

传感器的输出数据包括加速度传感器和陀螺仪传感器的数值,分别对应X轴、Y轴和Z轴的加速度和角速度。通过改变MPU6050传感器的姿态,这六个数据会相应变化。在介绍该模块时,我会详细解释这些数据。

I2C通信的基础

在学习I2C之前,我们先回顾一下串口通信。串口通信是通过TX引脚向RX引脚发送数据流,数据流以字节为单位,可以组合多个字节。串口通信是异步全双工的协议,一条发送线,一条接收线,没有时钟线。现在,假如有家公司要求你设计一个通信协议,该协议用于与单片机外部模块的通信。这个模块可能是AD转换、温湿度测量、姿态测量等。

这个通信协议至少需要定义两个字节数据,一个字节用于指定要读写的寄存器地址,另一个字节用于指定存储器内容。写入内容可以控制硬件电路,读取内容可以获取电路状态。与单片机内部操作寄存器的原理相同,只是这个模块的寄存器在单片机外部。

通信协议设计的四个要求

假设你设计了一个协议,通过两根通讯线完成数据的传输,并通过约定好的格式来指定寄存器的地址和数据内容。但这家公司对通信协议提出了四个要求:

  1. 资源优化: 由于数据传输过程中不需要同时进行发送和接收,因此可以删除一根通讯线,改为使用一根线进行半双工通信。

  2. 应答机制: 增加应答机制,确保每发送一个字节,对方都能给出回应。

  3. 多设备支持: 支持一根线上挂载多个模块,单片机可以指定与任意一个模块通信,其他模块不能干扰通信。

  4. 同步时序: 改用同步协议,增加一根时钟线,降低对硬件的依赖,同时支持传输暂停和恢复。

I2C协议介绍

I2C总线的全称是“Inter-Integrated Circuit”,简称I2C或I²C。I2C总线由飞利浦公司开发,是一种通用的数据总线,应用非常广泛。许多模块,如MPU6050、OLED显示屏、AT24C02存储器、DS3231实时时钟模块等,都是使用I2C通信协议。

I2C协议的标志性特点是两根通讯线:SCL(Serial Clock,串行时钟线)和SDA(Serial Data,串行数据线)。I2C通信是同步半双工的协议,具有数据应答机制,支持多设备挂载。

I2C的硬件规定

这是一个典型的I2C电路模型。在一组多从模型中,主机完全掌控SCL线,而SDA线则在主机和从机之间切换控制权。所有I2C设备的SCL和SDA都连在一起。在空闲状态下,主机可以主动发起对SDA的控制。只有在从机发送数据和应答时,从机才能短暂获取SDA的控制权。

为了避免总线时序未协调导致电源短路的问题,I2C设计禁止所有设备输出强上拉的高电平,采用外置弱上拉电阻和开漏输出的电路结构。

I2C通信的基本流程

在I2C通信中,主机通过控制SCL和SDA线进行数据传输。每次通信从主机发起开始,过程大致分为以下几步:

1.起始条件 (Start Condition): 主机将SDA线从高电平拉低,然后再将SCL线拉低,表示通信开始。

停止条件 (Stop Condition): 当数据传输完成后,主机将SCL线拉高,并将SDA线从低电平拉高,表示通信结束。

2.发送地址 (Sending Address): 主机在通信开始后发送7位从机地址,紧接着发送读/写位 (R/W bit)。如果是写操作,R/W位为0;如果是读操作,R/W位为1。

发送:低电平主机放数据,高电平从机读数据

接收:低电平从机放数据,高电平主机读数据

3.应答信号 (Acknowledgment): 从机在接收到地址和R/W位后,若该地址匹配,将通过SDA线发送一个低电平信号(ACK),表示应答。若不匹配,则发送高电平(NACK)。

发送应答紧接着接收应答,结束数据传输后,

SCL=0 ,SDA=1  主机释放SDA,从机如果接收到了,将SDA拉下=0 ,说明从机接收到了

主机高电平读取数据SDA=0,从机放弃SDA的控制权

SCL=0 ,SDA=1  主机释放SDA,从机如果没有接收到,SDA=1 ,说明从机没有接收到了

主机高电平读取数据SDA=1,从机放弃SDA的控制权

4.数据传输 (Data Transfer): 主机或从机根据R/W位的状态进行数据传输。传输的数据每个字节都有相应的ACK或NACK应答。

在上述过程中,SCL线提供时钟信号,每次数据传输都是同步进行的。而SDA线在每个时钟周期的高电平时,保持数据的稳定性。

I2C多主机与多从机通信

I2C协议不仅支持单主机、多从机通信,也支持多主机、多从机通信模式。多主机通信时,每个主机都可以主动发起通信。但为了避免多个主机同时控制总线,I2C总线设计了仲裁机制。

  1. 仲裁机制 (Arbitration): 当多个主机同时发起通信时,I2C总线通过比较每个主机发出的SDA信号来决定哪一个主机可以继续占用总线。其他主机则会被强制停止通信。

  2. 时钟同步 (Clock Synchronization): 在多主机环境中,I2C总线允许不同主机具有不同的时钟频率。当多个主机同时控制总线时,I2C会选取最慢的时钟频率作为总线的有效时钟。

软件I2C与硬件I2C

I2C通信可以通过软件模拟或使用硬件外设来实现。

  1. 软件I2C: 通过普通的GPIO引脚来模拟SCL和SDA的时序。这种方式实现灵活,适用于几乎所有单片机,但效率较低,时序也不如硬件I2C精确。

  2. 硬件I2C: 使用单片机内置的I2C外设来处理通信细节,如时序生成、应答信号处理等。硬件I2C效率高,时序精确,适用于对通信要求较高的场合。

硬件I2C

I2C协议的具体实现

在本课程中,我们将通过软件和硬件两种方式实现I2C通信,分别与MPU6050传感器进行交互。

软件I2C实现步骤:
  1. 引脚初始化: 配置SCL和SDA引脚为开漏输出模式,并通过上拉电阻确保空闲状态下为高电平。

  2. 时序控制: 通过编写代码手动控制SCL和SDA引脚的电平,模拟I2C通信时序。

  3. 数据传输: 逐位传输数据,同时监听应答信号。

  4. 错误处理: 在没有收到应答信号时,进行错误处理,如重新发起通信或退出通信。

硬件I2C实现步骤:
  1. 外设初始化: 配置I2C外设的相关寄存器,如时钟频率、工作模式等。

  2. 数据传输: 调用I2C外设的库函数,自动处理通信时序和应答信号。

  3. 中断处理: 根据需要配置I2C中断,实现更高效的数据传输。

  4. 错误处理: 通过中断或状态寄存器监控通信状态,并进行相应的错误处理。

MPU6050

 

 库函数

void I2C_GenerateSTART(I2C_TypeDef* I2Cx, FunctionalState NewState);
void I2C_GenerateSTOP(I2C_TypeDef* I2Cx, FunctionalState NewState);
void I2C_AcknowledgeConfig(I2C_TypeDef* I2Cx, FunctionalState NewState);
void I2C_SendData(I2C_TypeDef* I2Cx, uint8_t Data);

void I2C_Send7bitAddress(I2C_TypeDef* I2Cx, uint8_t Address, uint8_t I2C_Direction);
 

/**
  * @brief  复位I2C外设,将所有寄存器恢复到默认状态。
  * @param  I2Cx: 指定要复位的I2C外设,可以是I2C1或I2C2。
  * @retval 无
  */
void I2C_DeInit(I2C_TypeDef* I2Cx);

/**
  * @brief  初始化I2C外设的指定参数。
  * @param  I2Cx: 指定要初始化的I2C外设,可以是I2C1或I2C2。
  * @param  I2C_InitStruct: 指向I2C_InitTypeDef结构体的指针,该结构体包含指定I2C外设的配置信息。
  * @retval 无
  */
void I2C_Init(I2C_TypeDef* I2Cx, I2C_InitTypeDef* I2C_InitStruct);

/**
  * @brief  用默认值填充每个I2C_InitStruct成员。
  * @param  I2C_InitStruct: 指向将被初始化的I2C_InitTypeDef结构体的指针。
  * @retval 无
  */
void I2C_StructInit(I2C_InitTypeDef* I2C_InitStruct);

/**
  * @brief  启用或禁用指定的I2C外设。
  * @param  I2Cx: 指定要配置的I2C外设,可以是I2C1或I2C2。
  * @param  NewState: 指定I2C外设的新状态,可以是ENABLE或DISABLE。
  * @retval 无
  */
void I2C_Cmd(I2C_TypeDef* I2Cx, FunctionalState NewState);

/**
  * @brief  启用或禁用指定的I2C外设的DMA请求。
  * @param  I2Cx: 指定要配置的I2C外设,可以是I2C1或I2C2。
  * @param  NewState: 指定DMA请求的新状态,可以是ENABLE或DISABLE。
  * @retval 无
  */
void I2C_DMACmd(I2C_TypeDef* I2Cx, FunctionalState NewState);

/**
  * @brief  在DMA传输过程中,指示I2C外设是否发送STOP条件。
  * @param  I2Cx: 指定要配置的I2C外设,可以是I2C1或I2C2。
  * @param  NewState: 指定STOP条件的新状态,可以是ENABLE或DISABLE。
  * @retval 无
  */
void I2C_DMALastTransferCmd(I2C_TypeDef* I2Cx, FunctionalState NewState);

/**
  * @brief  生成I2C起始条件(START)。
  * @param  I2Cx: 指定要配置的I2C外设,可以是I2C1或I2C2。
  * @param  NewState: 启用或禁用I2C起始条件生成,可以是ENABLE或DISABLE。
  * @retval 无
  */
void I2C_GenerateSTART(I2C_TypeDef* I2Cx, FunctionalState NewState);

/**
  * @brief  生成I2C停止条件(STOP)。
  * @param  I2Cx: 指定要配置的I2C外设,可以是I2C1或I2C2。
  * @param  NewState: 启用或禁用I2C停止条件生成,可以是ENABLE或DISABLE。
  * @retval 无
  */
void I2C_GenerateSTOP(I2C_TypeDef* I2Cx, FunctionalState NewState);

/**
  * @brief  配置I2C的ACK应答。
  * @param  I2Cx: 指定要配置的I2C外设,可以是I2C1或I2C2。
  * @param  NewState: 启用或禁用I2C的ACK应答,可以是ENABLE或DISABLE。
  * @retval 无
  */
void I2C_AcknowledgeConfig(I2C_TypeDef* I2Cx, FunctionalState NewState);

/**
  * @brief  配置I2C从设备的第二地址。
  * @param  I2Cx: 指定要配置的I2C外设,可以是I2C1或I2C2。
  * @param  Address: 指定第二地址(7位)的值。
  * @retval 无
  */
void I2C_OwnAddress2Config(I2C_TypeDef* I2Cx, uint8_t Address);

/**
  * @brief  启用或禁用I2C从设备的双地址模式。
  * @param  I2Cx: 指定要配置的I2C外设,可以是I2C1或I2C2。
  * @param  NewState: 启用或禁用双地址模式,可以是ENABLE或DISABLE。
  * @retval 无
  */
void I2C_DualAddressCmd(I2C_TypeDef* I2Cx, FunctionalState NewState);

/**
  * @brief  启用或禁用I2C的通用调用模式。
  * @param  I2Cx: 指定要配置的I2C外设,可以是I2C1或I2C2。
  * @param  NewState: 启用或禁用通用调用模式,可以是ENABLE或DISABLE。
  * @retval 无
  */
void I2C_GeneralCallCmd(I2C_TypeDef* I2Cx, FunctionalState NewState);

/**
  * @brief  启用或禁用指定的I2C中断。
  * @param  I2Cx: 指定要配置的I2C外设,可以是I2C1或I2C2。
  * @param  I2C_IT: 指定要配置的I2C中断源。
  * @param  NewState: 启用或禁用指定的I2C中断,可以是ENABLE或DISABLE。
  * @retval 无
  */
void I2C_ITConfig(I2C_TypeDef* I2Cx, uint16_t I2C_IT, FunctionalState NewState);

/**
  * @brief  向I2C数据寄存器发送数据。
  * @param  I2Cx: 指定要发送数据的I2C外设,可以是I2C1或I2C2。
  * @param  Data: 要发送的8位数据。
  * @retval 无
  */
void I2C_SendData(I2C_TypeDef* I2Cx, uint8_t Data);

/**
  * @brief  从I2C数据寄存器接收数据。
  * @param  I2Cx: 指定要接收数据的I2C外设,可以是I2C1或I2C2。
  * @retval 返回接收到的8位数据。
  */
uint8_t I2C_ReceiveData(I2C_TypeDef* I2Cx);

/**
  * @brief  发送7位从设备地址。
  * @param  I2Cx: 指定要发送地址的I2C外设,可以是I2C1或I2C2。
  * @param  Address: 要发送的7位从设备地址。
  * @param  I2C_Direction: 指定I2C的传输方向,可以是I2C_Direction_Transmitter或I2C_Direction_Receiver。
  * @retval 无
  */
void I2C_Send7bitAddress(I2C_TypeDef* I2Cx, uint8_t Address, uint8_t I2C_Direction);

/**
  * @brief  读取指定的I2C寄存器值。
  * @param  I2Cx: 指定要读取寄存器的I2C外设,可以是I2C1或I2C2。
  * @param  I2C_Register: 指定要读取的寄存器。
  * @retval 返回读取到的16位寄存器值。
  */
uint16_t I2C_ReadRegister(I2C_TypeDef* I2Cx, uint8_t I2C_Register);

/**
  * @brief  启用或禁用I2C软件复位。
  * @param  I2Cx: 指定要配置的I2C外设,可以是I2C1或I2C2。
  * @param  NewState: 启用或禁用I2C软件复位,可以是ENABLE或DISABLE。
  * @retval 无
  */
void I2C_SoftwareResetCmd(I2C_TypeDef* I2Cx, FunctionalState NewState);

/**
  * @brief  配置I2C的NACK位置。
  * @param  I2Cx: 指定要配置的I2C外设,可以是I2C1或I2C2。
  * @param  I2C_NACKPosition: 指定NACK的位置,可以是I2C_NACKPosition_Current或I2C_NACKPosition_Next。
  * @retval 无
  */
void I2C_NACKPositionConfig(I2C_TypeDef* I2Cx, uint16_t I2C_NACKPosition);

/**
  * @brief  配置I2C的SMBus警报(SMBus Alert)状态。
  * @param  I2Cx: 指定要配置的I2C外设,可以是I2C1或I2C2。
  * @param  I2C_SMBusAlert: 指定SMBus警报状态,可以是I2C_SMBusAlert_Low或I2C_SMBusAlert_High。
  * @retval 无
  */
void I2C_SMBusAlertConfig(I2C_TypeDef* I2Cx, uint16_t I2C_SMBusAlert);

/**
  * @brief  启用或禁用I2C的PEC(Packet Error Checking,数据包错误检测)传输。
  * @param  I2Cx: 指定要配置的I2C外设,可以是I2C1或I2C2。
  * @param  NewState: 启用或禁用PEC传输,可以是ENABLE或DISABLE。
  * @retval 无
  */
void I2C_TransmitPEC(I2C_TypeDef* I2Cx, FunctionalState NewState);

/**
  * @brief  配置I2C的PEC位置。
  * @param  I2Cx: 指定要配置的I2C外设,可以是I2C1或I2C2。
  * @param  I2C_PECPosition: 指定PEC位置,可以是I2C_PECPosition_Current或I2C_PECPosition_Next。
  * @retval 无
  */
void I2C_PECPositionConfig(I2C_TypeDef* I2Cx, uint16_t I2C_PECPosition);

/**
  * @brief  启用或禁用I2C的PEC计算功能。
  * @param  I2Cx: 指定要配置的I2C外设,可以是I2C1或I2C2。
  * @param  NewState: 启用或禁用PEC计算功能,可以是ENABLE或DISABLE。
  * @retval 无
  */
void I2C_CalculatePEC(I2C_TypeDef* I2Cx, FunctionalState NewState);

/**
  * @brief  获取I2C当前的PEC值。
  * @param  I2Cx: 指定要读取PEC值的I2C外设,可以是I2C1或I2C2。
  * @retval 返回当前PEC值(8位)。
  */
uint8_t I2C_GetPEC(I2C_TypeDef* I2Cx);

/**
  * @brief  启用或禁用I2C的ARP(Address Resolution Protocol,地址解析协议)。
  * @param  I2Cx: 指定要配置的I2C外设,可以是I2C1或I2C2。
  * @param  NewState: 启用或禁用ARP功能,可以是ENABLE或DISABLE。
  * @retval 无
  */
void I2C_ARPCmd(I2C_TypeDef* I2Cx, FunctionalState NewState);

/**
  * @brief  启用或禁用I2C时钟延展(Clock Stretching)功能。
  * @param  I2Cx: 指定要配置的I2C外设,可以是I2C1或I2C2。
  * @param  NewState: 启用或禁用时钟延展功能,可以是ENABLE或DISABLE。
  * @retval 无
  */
void I2C_StretchClockCmd(I2C_TypeDef* I2Cx, FunctionalState NewState);

/**
  * @brief  配置I2C的快速模式占空比(Fast Mode Duty Cycle)。
  * @param  I2Cx: 指定要配置的I2C外设,可以是I2C1或I2C2。
  * @param  I2C_DutyCycle: 指定快速模式下的占空比,可以是I2C_DutyCycle_2或I2C_DutyCycle_16_9。
  * @retval 无
  */
void I2C_FastModeDutyCycleConfig(I2C_TypeDef* I2Cx, uint16_t I2C_DutyCycle);
/**
 * @brief
 ****************************************************************************************
 *
 *                         I2C 状态监控函数
 *                       
 ****************************************************************************************   
 * 这个 I2C 驱动程序根据应用需求和约束条件提供了三种不同的 I2C 状态监控方式:
 *        
 *  
 * 1) 基本状态监控:
 *    使用 I2C_CheckEvent() 函数:
 *    它将状态寄存器 (SR1 和 SR2) 的内容与给定的事件进行比较
 *    (可以是一个或多个标志位的组合)。
 *    如果当前状态包含给定的标志位,它将返回 SUCCESS;
 *    如果当前状态缺少一个或多个标志位,它将返回 ERROR。
 *    - 何时使用:
 *      - 这个函数适用于大多数应用以及启动阶段
 *        因为事件在产品参考手册 (RM0008) 中有详细描述。
 *      - 它也适合需要定义自己的事件的用户。
 *    - 限制:
 *      - 如果发生错误(即,除了被监控的标志位外,错误标志位也被设置),
 *        I2C_CheckEvent() 函数可能会返回 SUCCESS,尽管通信已挂起或实际状态已损坏。
 *        在这种情况下,建议使用错误中断来监控错误事件,并在中断 IRQ 处理程序中处理它们。
 *        
 *        @note 
 *        对于错误管理,建议使用以下函数:
 *          - I2C_ITConfig() 配置和启用错误中断 (I2C_IT_ERR)。
 *          - I2Cx_ER_IRQHandler() 在错误中断发生时调用。
 *            其中 x 是外设实例 (I2C1, I2C2 ... )。
 *          - 在 I2Cx_ER_IRQHandler() 中调用 I2C_GetFlagStatus() 或 I2C_GetITStatus()
 *            以确定发生了哪个错误。
 *          - 使用 I2C_ClearFlag() 或 I2C_ClearITPendingBit() 和/或 I2C_SoftwareResetCmd()
 *            和/或 I2C_GenerateStop() 清除错误标志和源,
 *            并恢复到正确的通信状态。
 *            
 *
 *  2) 高级状态监控:
 *     使用 I2C_GetLastEvent() 函数,该函数返回一个包含两个状态寄存器图像的单字 (uint32_t)
 *     (状态寄存器 2 的值左移 16 位并与状态寄存器 1 连接)。
 *     - 何时使用:
 *       - 这个函数适用于上述相同的应用,但它可以克服 I2C_GetFlagStatus() 函数的限制 (见下文)。
 *         返回的值可以与库中已定义的事件(stm32f10x_i2c.h)或用户定义的自定义值进行比较。
 *       - 这个函数适合同时监控多个标志位的情况。
 *       - 与 I2C_CheckEvent() 函数相反,这个函数允许用户选择何时接受事件(当所有事件标志都被设置且没有
 *         其他标志被设置时,或仅当需要的标志被设置时,如 I2C_CheckEvent() 函数那样)。
 *     - 限制:
 *       - 用户可能需要定义自己的事件。
 *       - 如果用户决定只检查常规通信标志(而忽略错误标志),则同样的错误管理注意事项也适用于此函数。
 *     
 *
 *  3) 基于标志的状态监控:
 *     使用 I2C_GetFlagStatus() 函数,它只返回一个标志位的状态(例如 I2C_FLAG_RXNE ...)。
 *     - 何时使用:
 *        - 这个函数可以用于特定的应用或调试阶段。
 *        - 它适用于只需要检查一个标志位的情况(大多数 I2C 事件通过多个标志位进行监控)。
 *     - 限制: 
 *        - 调用这个函数时,状态寄存器会被访问。一些标志位在状态寄存器被访问时会被清除。因此,检查一个标志位的状态,可能会清除其他标志位。
 *        - 可能需要调用这个函数两次或更多次才能监控单个事件。
 *            
 */
#include "stm32f10x.h"                  // Device header

/**
 * @brief  检查指定的I2C事件是否发生
 * @param  I2Cx: 指向指定I2C外设的指针,例如 I2C1、I2C2 等
 * @param  I2C_EVENT: 需要检查的I2C事件标志
 * @retval ErrorStatus: 指示检查的结果
 *         - SUCCESS: 指定的事件已发生
 *         - ERROR: 指定的事件未发生
 */
ErrorStatus I2C_CheckEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT)
{
    if ((I2Cx->SR1 & I2C_EVENT) != (I2C_EVENT & 0xFFFF))
    {
        return ERROR;
    }
    return SUCCESS;
}

/**
 * @brief  获取I2C外设的最后一个事件
 * @param  I2Cx: 指向指定I2C外设的指针,例如 I2C1、I2C2 等
 * @retval uint32_t: 返回最后一个发生的事件标志
 */
uint32_t I2C_GetLastEvent(I2C_TypeDef* I2Cx)
{
    return (I2Cx->SR1 & 0xFFFF);
}

/**
 * @brief  获取指定的I2C标志的状态
 * @param  I2Cx: 指向指定I2C外设的指针,例如 I2C1、I2C2 等
 * @param  I2C_FLAG: 需要检查的标志位
 * @retval FlagStatus: 指示标志的状态
 *         - SET: 标志被设置
 *         - RESET: 标志被重置
 */
FlagStatus I2C_GetFlagStatus(I2C_TypeDef* I2Cx, uint32_t I2C_FLAG)
{
    if ((I2Cx->SR1 & I2C_FLAG) != (I2C_FLAG))
    {
        return RESET;
    }
    return SET;
}

/**
 * @brief  清除指定的I2C标志
 * @param  I2Cx: 指向指定I2C外设的指针,例如 I2C1、I2C2 等
 * @param  I2C_FLAG: 需要清除的标志位
 * @retval 无
 */
void I2C_ClearFlag(I2C_TypeDef* I2Cx, uint32_t I2C_FLAG)
{
    I2Cx->SR1 = ~I2C_FLAG;
}

/**
 * @brief  获取指定的I2C中断状态
 * @param  I2Cx: 指向指定I2C外设的指针,例如 I2C1、I2C2 等
 * @param  I2C_IT: 需要检查的中断源
 * @retval ITStatus: 指示中断状态
 *         - SET: 中断已发生
 *         - RESET: 中断未发生
 */
ITStatus I2C_GetITStatus(I2C_TypeDef* I2Cx, uint32_t I2C_IT)
{
    if ((I2Cx->SR1 & I2C_IT) != (I2C_IT))
    {
        return RESET;
    }
    return SET;
}

/**
 * @brief  清除指定的I2C中断挂起位
 * @param  I2Cx: 指向指定I2C外设的指针,例如 I2C1、I2C2 等
 * @param  I2C_IT: 需要清除的中断源
 * @retval 无
 */
void I2C_ClearITPendingBit(I2C_TypeDef* I2Cx, uint32_t I2C_IT)
{
    I2Cx->SR1 &= ~I2C_IT;
}

软件I2c 

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

/* 引脚配置层 */

/**
  * @brief  I2C写SCL引脚电平
  * @param  BitValue: 协议层传入的当前需要写入SCL的电平,范围0~1
  * @retval 无
  */
void MyI2C_W_SCL(uint8_t BitValue)
{
    GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)BitValue);  // 根据BitValue,设置SCL引脚的电平
    Delay_us(10);  // 延时10us,防止时序频率超过要求
}

/**
  * @brief  I2C写SDA引脚电平
  * @param  BitValue: 协议层传入的当前需要写入SDA的电平,范围0~1
  * @retval 无
  */
void MyI2C_W_SDA(uint8_t BitValue)
{
    GPIO_WriteBit(GPIOB, GPIO_Pin_11, (BitAction)BitValue);  // 根据BitValue,设置SDA引脚的电平
    Delay_us(10);  // 延时10us,防止时序频率超过要求
}

/**
  * @brief  I2C读SDA引脚电平
  * @param  无
  * @retval 返回SDA电平,范围0~1
  */
uint8_t MyI2C_R_SDA(void)
{
    Delay_us(10);  // 延时10us,防止时序频率超过要求
    return GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11);  // 读取SDA电平并返回
}

/**
  * @brief  I2C初始化
  * @param  无
  * @retval 无
  */
void MyI2C_Init(void)
{
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);  // 开启GPIOB的时钟

    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);  // 将PB10和PB11引脚初始化为开漏输出

    GPIO_SetBits(GPIOB, GPIO_Pin_10 | GPIO_Pin_11);  // 设置PB10和PB11引脚初始化后默认为高电平
}

/* 协议层 */

/**
  * @brief  I2C起始
  * @param  无
  * @retval 无
  */
void MyI2C_Start(void)
{
    MyI2C_W_SDA(1);  // 释放SDA,确保SDA为高电平
    MyI2C_W_SCL(1);  // 释放SCL,确保SCL为高电平
    MyI2C_W_SDA(0);  // 在SCL高电平期间,拉低SDA,产生起始信号
    MyI2C_W_SCL(0);  // 起始后拉低SCL,占用总线,准备发送数据
}

/**
  * @brief  I2C终止
  * @param  无
  * @retval 无
  */
void MyI2C_Stop(void)
{
    MyI2C_W_SDA(0);  // 拉低SDA,确保SDA为低电平
    MyI2C_W_SCL(1);  // 释放SCL,使SCL呈现高电平
    MyI2C_W_SDA(1);  // 在SCL高电平期间,释放SDA,产生终止信号
}

/**
  * @brief  I2C发送一个字节
  * @param  Byte: 要发送的一个字节数据,范围:0x00~0xFF
  * @retval 无
  */
void MyI2C_SendByte(uint8_t Byte)
{
    for (uint8_t i = 0; i < 8; i++)
    {
        MyI2C_W_SDA(Byte & (0x80 >> i));  // 发送数据的每一位
        MyI2C_W_SCL(1);  // 释放SCL,从机在SCL高电平期间读取SDA
        MyI2C_W_SCL(0);  // 拉低SCL,准备发送下一位
    }
}

/**
  * @brief  I2C接收一个字节
  * @param  无
  * @retval 接收到的一个字节数据,范围:0x00~0xFF
  */
uint8_t MyI2C_ReceiveByte(void)
{
    uint8_t Byte = 0x00;
    MyI2C_W_SDA(1);  // 释放SDA,准备接收数据

    for (uint8_t i = 0; i < 8; i++)
    {
        MyI2C_W_SCL(1);  // 释放SCL,从机在SCL高电平期间发送数据
        if (MyI2C_R_SDA())  // 读取SDA的数据位
        {
            Byte |= (0x80 >> i);  // 如果SDA为高电平,设置相应位为1
        }
        MyI2C_W_SCL(0);  // 拉低SCL,准备接收下一位
    }

    return Byte;
}

/**
  * @brief  I2C发送应答位
  * @param  AckBit: 要发送的应答位,范围:0~1,0表示应答,1表示非应答
  * @retval 无
  */
void MyI2C_SendAck(uint8_t AckBit)
{
    MyI2C_W_SDA(AckBit);  // 发送应答位到SDA
    MyI2C_W_SCL(1);  // 释放SCL,允许从机读取应答位
    MyI2C_W_SCL(0);  // 拉低SCL,准备下一时序
}

/**
  * @brief  I2C接收应答位
  * @param  无
  * @retval 接收到的应答位,范围:0~1,0表示应答,1表示非应答
  */
uint8_t MyI2C_ReceiveAck(void)
{
    MyI2C_W_SDA(1);  // 释放SDA,准备接收应答
    MyI2C_W_SCL(1);  // 释放SCL,从机在SCL高电平期间发送应答位
    uint8_t AckBit = MyI2C_R_SDA();  // 读取应答位
    MyI2C_W_SCL(0);  // 拉低SCL,准备下一时序
    return AckBit;
}

硬件I2c 

#include "stm32f10x.h"

/* I2C初始化函数 */
void I2C_Config(void)
{
    I2C_InitTypeDef I2C_InitStructure;
    GPIO_InitTypeDef GPIO_InitStructure;

    /* 使能I2C1和GPIOB的时钟 */
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);

    /* 配置I2C1的SCL和SDA引脚(PB6为SCL,PB7为SDA) */
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB, &GPIO_InitStructure);

    /* 配置I2C1 */
    I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
    I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
    I2C_InitStructure.I2C_OwnAddress1 = 0x00; // 从设备地址
    I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;
    I2C_InitStructure.I2C_ClockSpeed = 100000; // I2C时钟频率 100kHz
    I2C_Init(I2C1, &I2C_InitStructure);

    /* 使能I2C1 */
    I2C_Cmd(I2C1, ENABLE);
}

/* 向指定I2C从设备发送一个字节 */
void I2C_SendByte(uint8_t addr, uint8_t reg, uint8_t data)
{
    /* 产生开始信号 */
    I2C_GenerateSTART(I2C1, ENABLE);
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));

    /* 发送从设备地址和写命令 */
    I2C_Send7bitAddress(I2C1, addr, I2C_Direction_Transmitter);
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));

    /* 发送寄存器地址 */
    I2C_SendData(I2C1, reg);
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));

    /* 发送数据 */
    I2C_SendData(I2C1, data);
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));

    /* 产生停止信号 */
    I2C_GenerateSTOP(I2C1, ENABLE);
}

/* 从指定I2C从设备读取一个字节 */
uint8_t I2C_ReadByte(uint8_t addr, uint8_t reg)
{
    uint8_t data;

    /* 产生开始信号 */
    I2C_GenerateSTART(I2C1, ENABLE);
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));

    /* 发送从设备地址和写命令 */
    I2C_Send7bitAddress(I2C1, addr, I2C_Direction_Transmitter);
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));

    /* 发送寄存器地址 */
    I2C_SendData(I2C1, reg);
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));

    /* 产生重复开始信号 */
    I2C_GenerateSTART(I2C1, ENABLE);
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));

    /* 发送从设备地址和读命令 */
    I2C_Send7bitAddress(I2C1, addr, I2C_Direction_Receiver);
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED));

    /* 等待接收数据 */
    while (!I2C_GetFlagStatus(I2C1, I2C_FLAG_RXNE));
    data = I2C_ReceiveData(I2C1);

    /* 产生停止信号 */
    I2C_GenerateSTOP(I2C1, ENABLE);

    return data;
}

int main(void)
{
    uint8_t data;

    /* 初始化I2C */
    I2C_Config();

    /* 向从设备写入数据 */
    I2C_SendByte(0xA0, 0x01, 0x55);

    /* 从从设备读取数据 */
    data = I2C_ReadByte(0xA0, 0x01);

    while (1)
    {
        /* 主循环 */
    }
}

SPI

SPI来实现对W25Q64 Flash存储器的读写,然后再学习STM32中的SPI外设,并使用硬件SPI实现同样的功能。接下来,我们先看一下本节课的程序现象。本节课主要有两个代码,一个是软件SPI读写W25Q64,另一个是硬件SPI读写W25Q64。这两个代码实现的效果是一样的。

我们先看一下软件SPI读写W25Q64的代码。这里的W25Q64是一个Flash存储器芯片,它内部可以存储8MB的数据,并且掉电不丢失。如果你之后的项目需要存储大量数据,可以考虑外挂这个芯片来实现。在这里,我们用四根SPI通信线将W25Q64和STM32连接在一起,通过STM32操作引脚电平来实现SPI通信的时序,从而实现读写存储器芯片的目的。

在代码运行的测试中,第一行显示的是ID号,MID是厂商ID,读取的值为0xEF,DID是设备ID,读取的值为0x4017。这些ID号都是固定的数值,在手册中有详细描述。我们通过SPI读写ID号可以进行最简单的测试。如果读取的ID号与手册中的一致,说明SPI通信基本没有问题。既然是存储器芯片,下一步肯定是写入几个数据再读出来看看。第二行写入的内容是四个字节0x01020304,第三行读取的内容显示也是0x01020304,读出来的数据和写入的一样,说明读写存储器芯片没有问题。

当然,更进一步的测试,比如读写更多的数据,或是验证写入的数据是否掉电不丢失,可以在之后的程序中进行验证。程序现象介绍到这里,我们接下来回到PPT,开始研究SPI通信。

SPI的英文全称是Serial Peripheral Interface,字面意思是串行外设接口。它是由摩托罗拉公司开发的一种通用数据总线,用于主控和外挂芯片之间的通信。SPI和I2C两者各有优势和劣势,I2C硬件电路和软件时序的设计相对比较复杂,硬件上需要配置开漏模式并加上拉电阻,而软件上需要处理诸如数据双向传输、应答位、寻址机制等问题。最终,I2C通过这些设计实现了高性价比,在硬件上无论挂载多少设备都只需要两根通信线,而在软件上实现了数据双向通信和多种功能。

相比之下,SPI传输速度更快,并且没有严格规定最大传输速度,最大速度取决于芯片厂商的设计需求。SPI的设计简单粗暴,学习起来也比I2C要简单得多,但硬件开销比较大,通信线的数量更多,且通信过程中经常会有资源浪费的现象。

SPI协议的基本特征是同步全双工通信,使用四根通信线:SCK(串行时钟线)、MOSI(主机输出/从机输入)、MISO(主机输入/从机输出)和SS(从机选择)。在SPI通信中,SCK提供时钟信号,MOSI和MISO分别用于数据发送和接收,而SS用于选择目标从机进行通信。

SPI 和 I2C 的比较:

  1. 速度:

    • SPI: SPI 通信速度更快,没有严格规定的最大传输速度。具体速度由芯片厂商设计。例如,W25Q64 存储器的 SPI 时钟频率可以高达 80 MHz。
    • I2C: I2C 的通信速度相对较慢。标准模式下频率为 100 kHz,快速模式为 400 kHz。尽管高速模式可以达到 3.4 MHz,但普及程度不高。
  2. 设计复杂性:

    • SPI: 相对简单,硬件和软件设计都比较直接。SPI 通信使用了四根通信线,每根线的作用明确。
    • I2C: 硬件和软件设计较为复杂,尤其是单根数据线双向通信的设计,需要处理应答位和寻址机制。
  3. 通信线的数量:

    • SPI: 使用四根通信线(SCK, MOSI, MISO, SS),支持全双工通信,通信速度快但硬件开销大。
    • I2C: 只需两根通信线(SCL, SDA),硬件开销小,但由于使用开漏结构和上拉电阻,导致通信速度受到限制。
  4. 多设备支持:

    • SPI: 支持一主多从模式,通过独立的 SS 线选择从设备。每个从设备需要独立的 SS 线。
    • I2C: 支持多主多从模式,通过设备地址进行寻址,不需要额外的选择线。
  5. 应答机制:

    • SPI: 没有应答机制,数据的发送和接收是独立进行的。
    • I2C: 具有应答机制,可以确认数据是否被成功接收。

SPI 具体使用时的考虑:

  • 硬件电路: SPI 的硬件设计通常会牵涉到更多的通信线,且这些线在主机和从机之间要进行准确的连接和配置。

主机拥有SCK的控制权

跟谁通信就将该从机配置成低电平

配置:推挽输出(只有当SS为低电平的时候才可以配置),上拉输入或上拉

  • 推挽输出: SPI 通信中的输出引脚配置为推挽输出,驱动能力强,能支持更快的通信速度。
  • 资源冲突: 当从机未被选中时,其 MISO 引脚会处于高阻态,以避免多个从设备输出导致的信号冲突。

SPI时序基本单元

SPI 时序

SPI 的时序图帮助理解数据在 SPI 总线上的交换过程。以下是 SPI 的时序关键点:

  1. 时钟信号 (SCK)

    • SPI 是同步协议,因此主机控制时钟信号 (SCK) 的频率和边缘。数据的采样和传输都与时钟信号的边缘相关。
  2. 数据传输

    • 数据的传输在时钟信号的上升沿或下降沿进行(取决于协议设置)。主机和从机在时钟的边缘上进行数据交换。
  3. 数据线 (MOSI 和 MISO)

    • MOSI (主机输出从机输入):主机发送的数据线。主机将数据发送到从机。
    • MISO (主机输入从机输出):从机发送的数据线。从机将数据发送回主机。
  4. 选择线 (SS/CS)

    • 选择线用于选择哪个从机与主机通信。通过将 SS/CS 线拉低来选择对应的从机。如果 SS/CS 线为高电平,则从机处于非选中状态,忽略主机的通信。
  5. 时序操作

    • 主机发送数据:主机通过 MOSI 线将数据发送到从机,同时 SCK 线提供时钟信号。
    • 从机接收数据:从机通过 MISO 线将数据发送回主机,主机的接收器接收这些数据。
    • 数据传输的基本单位是一个字节,每次传输的字节通过时钟信号的边缘进行同步。

SPI 数据传输步骤

  • 初始化

    • 主机和从机都准备好接收和发送数据。主机拉低选择线 (SS),选择要通信的从机。
  • 数据发送

    • 主机通过 MOSI 线将数据发送到从机,同时提供时钟信号 (SCK)。数据在时钟的边缘进行采样。
  • 数据接收

    • 从机通过 MISO 线将数据发送回主机,主机接收这些数据。

交换字节(发送和接收)

第一个边沿的时候,主机和从机同时移出数据

第二个边沿的时候,主机和从机同时移入数据

模式0:开始的下降沿就开始移除

 时钟极性CPOL是指SPI通讯设备处于空闲状态时,SCK信号线的电平信号(即SPI通讯开始前、 NSS线为高电平时SCK的状态)。CPOL=0时, SCK在空闲状态时为低电平,CPOL=1时,则相反。
  时钟相位CPHA是指数据的采样的时刻,当CPHA=0时,MOSI或MISO数据线上的信号将会在SCK时钟线的“奇数边沿”被采样。当CPHA=1时, 数据线在SCK的“偶数边沿”采样。见图 CPHA = 0时的SPI通讯模式 及图 CPHA = 1时的SPI通讯模式 。

模式1:SCK的第一个边沿动作

  • 结束通信

    • 通信完成后,主机将选择线 (SS) 拉高,结束对从机的选择。

2. STM32的SPI架构剖析


1.通讯引脚
  SPI的所有硬件架构都从图 SPI架构图 中左侧MOSI、MISO、SCK及NSS线展开的。STM32芯片有多个SPI外设, 它们的SPI通讯信号引出到不同的GPIO引脚上,使用时必须配置到这些指定的引脚,见表 STM32F10x的SPI引脚 。 关于GPIO引脚的复用功能,可查阅《STM32F10x规格书》,以它为准。

  其中SPI1是APB2上的设备,最高通信速率达36Mbtis/s,SPI2、SPI3是APB1上的设备,最高通信速率为18Mbits/s。除了通讯速率, 在其它功能上没有差异。其中SPI3用到了下载接口的引脚,这几个引脚默认功能是下载,第二功能才是IO口,如果想使用SPI3接口, 则程序上必须先禁用掉这几个IO口的下载功能。一般在资源不是十分紧张的情况下,这几个IO口是专门用于下载和调试程序,不会复用为SPI3。

2. 时钟控制逻辑
  SCK线的时钟信号,由波特率发生器根据“控制寄存器CR1”中的BR[0:2]位控制,该位是对fpclk时钟的分频因子, 对fpclk的分频结果就是SCK引脚的输出时钟频率,计算方法见表 BR位对fpclk的分频 。

其中的fpclk频率是指SPI所在的APB总线频率, APB1为fpclk1,APB2为fpckl2。
通过配置“控制寄存器CR”的“CPOL位”及“CPHA”位可以把SPI设置成前面分析的4种SPI模式。

3. 数据控制逻辑
  SPI的MOSI及MISO都连接到数据移位寄存器上,数据移位寄存器的数据来源及目标接收、发送缓冲区以及MISO、MOSI线。 当向外发送数据的时候,数据移位寄存器以“发送缓冲区”为数据源,把数据一位一位地通过数据线发送出去;当从外部接收数据的时候, 数据移位寄存器把数据线采样到的数据一位一位地存储到“接收缓冲区”中。通过写SPI的“数据寄存器DR”把数据填充到发送缓冲区中, 通讯读“数据寄存器DR”,可以获取接收缓冲区中的内容。其中数据帧长度可以通过“控制寄存器CR1”的“DFF位”配置成8位及16位模式; 配置“LSBFIRST位”可选择MSB先行还是LSB先行。

4. 整体控制逻辑
  整体控制逻辑负责协调整个SPI外设,控制逻辑的工作模式根据我们配置的“控制寄存器(CR1/CR2)”的参数而改变, 基本的控制参数包括前面提到的SPI模式、波特率、LSB先行、主从模式、单双向模式等等。在外设工作时, 控制逻辑会根据外设的工作状态修改“状态寄存器(SR)”,我们只要读取状态寄存器相关的寄存器位, 就可以了解SPI的工作状态了。除此之外,控制逻辑还根据要求,负责控制产生SPI中断信号、DMA请求及控制NSS信号线。
  实际应用中,我们一般不使用STM32 SPI外设的标准NSS信号线,而是更简单地使用普通的GPIO,软件控制它的电平输出,从而产生通讯起始和停止信号。

W25Q64简介

W25Q64芯片的工作原理

  1. 存储器结构

    • 块(Block):整个芯片的存储空间划分为若干块,每块64KB,共128块。块是最基本的擦除单位。
    • 扇区(Sector):每块被进一步划分为多个扇区,每个扇区4KB。扇区是数据读写的基本单位。
    • 页(Page):每个扇区内部再划分为多个页,每页256字节。页是最小的写入单位。
  2. SPI通信

    • SPI接口:芯片通过SPI接口与主控芯片通信,SPI通信包括4个基本引脚:SCK(时钟线)、MOSI(主机输出/从机输入)、MISO(主机输入/从机输出)和CS(片选线)。
    • 双重SPI模式和四重SPI模式:可以在双重SPI模式下同时发送和接收两位数据,在四重SPI模式下可以同时发送和接收四位数据,这种模式下的传输速率更高,但在本课程中我们不涉及这两种模式。
  3. 写保护和数据保持

    • 写保护(WP):通过控制WP引脚可以实现对芯片的写保护,保护状态为低电平有效。
    • 数据保持(HOLD):通过HOLD引脚可以在通信中断时保持芯片的状态,防止时序被中断。HOLD引脚低电平有效。

硬件接线

  1. 电源引脚

    • VCC:芯片电源正极,通常接3.3V电源。
    • GND:芯片电源负极,接地。
  2. SPI通信引脚

    • CS(片选引脚):用于选择芯片,低电平有效。
    • SCK(时钟线):SPI时钟信号。
    • MOSI(主机输出/从机输入):用于数据发送。
    • MISO(主机输入/从机输出):用于数据接收。
  3. 控制引脚

    • WP(写保护引脚):通常接高电平,不启用写保护。
    • HOLD(数据保持引脚):不使用时可以接高电平,启用时接低电平。

3.通讯过程
  STM32使用SPI外设通讯时,在通讯的不同阶段它会对“状态寄存器SR”的不同数据位写入参数,我们通过读取这些寄存器标志来了解通讯状态。

编程和数据存储

  1. 编程命令

    • 需要发给芯片的命令包括读取数据、写入数据、擦除扇区等操作。这些命令通过SPI接口发送。
  2. 数据存储

    • 在写入数据时,需要选择对应的地址和数据页进行写入。写操作通常是按页进行的,即一次写入256字节的数据。

常见应用

  1. 数据存储

    • 可以用于存储程序代码、配置信息或其他需要持久化的数据。比如用于存储显示屏的字库数据,或作为存储固件的外部存储器。
  2. 字库存储

    • 在LCD或OLED显示屏应用中,可以用来存储大量的汉字字库数据,当显示需要汉字时,从芯片中读取对应的数据并显示。
  3. 固件存储

    • 可以用于存储固件代码,主控芯片在启动时从外部芯片中加载执行程序

库函数 

/**
 * @brief  复位SPI或I2S外设的寄存器到默认状态。
 * @param  SPIx: 指向SPI或I2S外设的指针,例如 SPI1、SPI2、SPI3等。
 * @retval 无
 */
void SPI_I2S_DeInit(SPI_TypeDef* SPIx);

/**
 * @brief  初始化SPI外设的配置。
 * @param  SPIx: 指向SPI外设的指针,例如 SPI1、SPI2、SPI3等。
 * @param  SPI_InitStruct: 指向SPI初始化结构体的指针,该结构体包含SPI的配置参数。
 * @retval 无
 */
void SPI_Init(SPI_TypeDef* SPIx, SPI_InitTypeDef* SPI_InitStruct);

/**
 * @brief  初始化I2S外设的配置。
 * @param  SPIx: 指向I2S外设的指针,例如 SPI1、SPI2、SPI3等。
 * @param  I2S_InitStruct: 指向I2S初始化结构体的指针,该结构体包含I2S的配置参数。
 * @retval 无
 */
void I2S_Init(SPI_TypeDef* SPIx, I2S_InitTypeDef* I2S_InitStruct);

/**
 * @brief  初始化SPI初始化结构体的默认值。
 * @param  SPI_InitStruct: 指向SPI初始化结构体的指针。
 * @retval 无
 */
void SPI_StructInit(SPI_InitTypeDef* SPI_InitStruct);

/**
 * @brief  初始化I2S初始化结构体的默认值。
 * @param  I2S_InitStruct: 指向I2S初始化结构体的指针。
 * @retval 无
 */
void I2S_StructInit(I2S_InitTypeDef* I2S_InitStruct);

/**
 * @brief  使能或禁用SPI外设。
 * @param  SPIx: 指向SPI外设的指针,例如 SPI1、SPI2、SPI3等。
 * @param  NewState: 新状态(ENABLE 或 DISABLE)。
 * @retval 无
 */
void SPI_Cmd(SPI_TypeDef* SPIx, FunctionalState NewState);

/**
 * @brief  使能或禁用I2S外设。
 * @param  SPIx: 指向I2S外设的指针,例如 SPI1、SPI2、SPI3等。
 * @param  NewState: 新状态(ENABLE 或 DISABLE)。
 * @retval 无
 */
void I2S_Cmd(SPI_TypeDef* SPIx, FunctionalState NewState);

/**
 * @brief  配置SPI或I2S的中断。
 * @param  SPIx: 指向SPI或I2S外设的指针,例如 SPI1、SPI2、SPI3等。
 * @param  SPI_I2S_IT: 要配置的中断源。
 * @param  NewState: 新状态(ENABLE 或 DISABLE)。
 * @retval 无
 */
void SPI_I2S_ITConfig(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT, FunctionalState NewState);

/**
 * @brief  配置SPI或I2S的DMA请求。
 * @param  SPIx: 指向SPI或I2S外设的指针,例如 SPI1、SPI2、SPI3等。
 * @param  SPI_I2S_DMAReq: 要配置的DMA请求源。
 * @param  NewState: 新状态(ENABLE 或 DISABLE)。
 * @retval 无
 */
void SPI_I2S_DMACmd(SPI_TypeDef* SPIx, uint16_t SPI_I2S_DMAReq, FunctionalState NewState);

/**
 * @brief  发送数据通过SPI外设。
 * @param  SPIx: 指向SPI外设的指针,例如 SPI1、SPI2、SPI3等。
 * @param  Data: 要发送的数据。
 * @retval 无
 */
void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data);

/**
 * @brief  从SPI外设接收数据。
 * @param  SPIx: 指向SPI外设的指针,例如 SPI1、SPI2、SPI3等。
 * @retval 接收到的数据。
 */
uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx);

/**
 * @brief  配置SPI外设的内部NSS信号(软件控制模式)。
 * @param  SPIx: 指向SPI外设的指针,例如 SPI1、SPI2、SPI3等。
 * @param  SPI_NSSInternalSoft: NSS信号配置选项。
 * @retval 无
 */
void SPI_NSSInternalSoftwareConfig(SPI_TypeDef* SPIx, uint16_t SPI_NSSInternalSoft);

/**
 * @brief  使能或禁用SPI外设的SS输出。
 * @param  SPIx: 指向SPI外设的指针,例如 SPI1、SPI2、SPI3等。
 * @param  NewState: 新状态(ENABLE 或 DISABLE)。
 * @retval 无
 */
void SPI_SSOutputCmd(SPI_TypeDef* SPIx, FunctionalState NewState);

/**
 * @brief  配置SPI数据位长度(8位或16位)。
 * @param  SPIx: 指向SPI外设的指针,例如 SPI1、SPI2、SPI3等。
 * @param  SPI_DataSize: 数据位长度配置选项(例如 SPI_DataSize_8b 或 SPI_DataSize_16b)。
 * @retval 无
 */
void SPI_DataSizeConfig(SPI_TypeDef* SPIx, uint16_t SPI_DataSize);

/**
 * @brief  启用SPI外设的CRC计算。
 * @param  SPIx: 指向SPI外设的指针,例如 SPI1、SPI2、SPI3等。
 * @retval 无
 */
void SPI_TransmitCRC(SPI_TypeDef* SPIx);

/**
 * @brief  配置SPI外设的CRC计算功能。
 * @param  SPIx: 指向SPI外设的指针,例如 SPI1、SPI2、SPI3等。
 * @param  NewState: 新状态(ENABLE 或 DISABLE)。
 * @retval 无
 */
void SPI_CalculateCRC(SPI_TypeDef* SPIx, FunctionalState NewState);

/**
 * @brief  获取SPI外设的CRC值。
 * @param  SPIx: 指向SPI外设的指针,例如 SPI1、SPI2、SPI3等。
 * @param  SPI_CRC: CRC配置选项。
 * @retval CRC值。
 */
uint16_t SPI_GetCRC(SPI_TypeDef* SPIx, uint8_t SPI_CRC);

/**
 * @brief  获取SPI外设的CRC多项式。
 * @param  SPIx: 指向SPI外设的指针,例如 SPI1、SPI2、SPI3等。
 * @retval CRC多项式。
 */
uint16_t SPI_GetCRCPolynomial(SPI_TypeDef* SPIx);

/**
 * @brief  配置SPI外设的双向通信模式。
 * @param  SPIx: 指向SPI外设的指针,例如 SPI1、SPI2、SPI3等。
 * @param  SPI_Direction: 双向通信模式配置选项。
 * @retval 无
 */
void SPI_BiDirectionalLineConfig(SPI_TypeDef* SPIx, uint16_t SPI_Direction);

/**
 * @brief  获取SPI或I2S外设的标志位状态。
 * @param  SPIx: 指向SPI或I2S外设的指针,例如 SPI1、SPI2、SPI3等。
 * @param  SPI_I2S_FLAG: 要检查的标志位。
 * @retval 标志位状态(SET 或 RESET)。
 */
FlagStatus SPI_I2S_GetFlagStatus(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);

/**
 * @brief  清除SPI或I2S外设的标志位。
 * @param  SPIx: 指向SPI或I2S外设的指针,例如 SPI1、SPI2、SPI3等。
 * @param  SPI_I2S_FLAG: 要清除的标志位。
 * @retval 无
 */
void SPI_I2S_ClearFlag(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);

/**
 * @brief  获取SPI或I2S外设的中断状态。
 * @param  SPIx: 指向SPI或I2S外设的指针,例如 SPI1、SPI2、SPI3等。
 * @param  SPI_I2S_IT: 要检查的中断源。
 * @retval 中断状态(SET 或 RESET)。
 */
ITStatus SPI_I2S_GetITStatus(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT);

/**
 * @brief  清除SPI或I2S外设的中断挂起位。
 * @param  SPIx: 指向SPI或I2S外设的指针,例如 SPI1、SPI2、SPI3等。
 * @param  SPI_I2S_IT: 要清除的中断源。
 * @retval 无
 */
void SPI_I2S_ClearITPendingBit(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT);
/**
 * @brief  配置SPI外设的内部NSS信号(软件控制模式)。
 * @param  SPIx: 指向SPI外设的指针,例如 SPI1、SPI2、SPI3等。
 * @param  SPI_NSSInternalSoft: NSS信号配置选项,可以是以下值之一:
 *          @arg SPI_NSSInternalSoft_Set: 设置NSS信号(NSS软件控制模式)。
 *          @arg SPI_NSSInternalSoft_Reset: 重置NSS信号。
 * @retval 无
 */
void SPI_NSSInternalSoftwareConfig(SPI_TypeDef* SPIx, uint16_t SPI_NSSInternalSoft);

/**
 * @brief  使能或禁用SPI外设的SS输出功能。
 * @param  SPIx: 指向SPI外设的指针,例如 SPI1、SPI2、SPI3等。
 * @param  NewState: 新状态,可以是以下值之一:
 *          @arg ENABLE: 使能SS输出功能。
 *          @arg DISABLE: 禁用SS输出功能。
 * @retval 无
 */
void SPI_SSOutputCmd(SPI_TypeDef* SPIx, FunctionalState NewState);

/**
 * @brief  配置SPI外设的数据帧长度。
 * @param  SPIx: 指向SPI外设的指针,例如 SPI1、SPI2、SPI3等。
 * @param  SPI_DataSize: 数据帧长度,可以是以下值之一:
 *          @arg SPI_DataSize_8b: 8位数据帧长度。
 *          @arg SPI_DataSize_16b: 16位数据帧长度。
 * @retval 无
 */
void SPI_DataSizeConfig(SPI_TypeDef* SPIx, uint16_t SPI_DataSize);

/**
 * @brief  启用SPI外设的CRC传输。
 * @param  SPIx: 指向SPI外设的指针,例如 SPI1、SPI2、SPI3等。
 * @retval 无
 */
void SPI_TransmitCRC(SPI_TypeDef* SPIx);

/**
 * @brief  使能或禁用SPI外设的CRC计算。
 * @param  SPIx: 指向SPI外设的指针,例如 SPI1、SPI2、SPI3等。
 * @param  NewState: 新状态,可以是以下值之一:
 *          @arg ENABLE: 使能CRC计算。
 *          @arg DISABLE: 禁用CRC计算。
 * @retval 无
 */
void SPI_CalculateCRC(SPI_TypeDef* SPIx, FunctionalState NewState);

/**
 * @brief  获取SPI外设的CRC校验码。
 * @param  SPIx: 指向SPI外设的指针,例如 SPI1、SPI2、SPI3等。
 * @param  SPI_CRC: 指定要获取的CRC校验码,可以是以下值之一:
 *          @arg SPI_CRC_Tx: 获取发送数据的CRC校验码。
 *          @arg SPI_CRC_Rx: 获取接收数据的CRC校验码。
 * @retval 返回指定的CRC校验码值。
 */
uint16_t SPI_GetCRC(SPI_TypeDef* SPIx, uint8_t SPI_CRC);

/**
 * @brief  获取SPI外设的CRC多项式。
 * @param  SPIx: 指向SPI外设的指针,例如 SPI1、SPI2、SPI3等。
 * @retval 返回CRC多项式。
 */
uint16_t SPI_GetCRCPolynomial(SPI_TypeDef* SPIx);

/**
 * @brief  配置SPI外设的双向数据传输模式。
 * @param  SPIx: 指向SPI外设的指针,例如 SPI1、SPI2、SPI3等。
 * @param  SPI_Direction: 指定双向数据传输模式,可以是以下值之一:
 *          @arg SPI_Direction_Tx: 选择发送模式。
 *          @arg SPI_Direction_Rx: 选择接收模式。
 * @retval 无
 */
void SPI_BiDirectionalLineConfig(SPI_TypeDef* SPIx, uint16_t SPI_Direction);

/**
 * @brief  获取SPI或I2S外设的标志位状态。
 * @param  SPIx: 指向SPI或I2S外设的指针,例如 SPI1、SPI2、SPI3等。
 * @param  SPI_I2S_FLAG: 指定要检查的标志位,可以是以下值之一:
 *          @arg SPI_I2S_FLAG_TXE: 发送缓冲区为空标志位。
 *          @arg SPI_I2S_FLAG_RXNE: 接收缓冲区非空标志位。
 *          @arg SPI_I2S_FLAG_BSY: 传输忙标志位。
 *          @arg 其他标志位根据具体芯片参考手册提供。
 * @retval 标志位状态(SET 或 RESET)。
 */
FlagStatus SPI_I2S_GetFlagStatus(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);

/**
 * @brief  清除SPI或I2S外设的指定标志位。
 * @param  SPIx: 指向SPI或I2S外设的指针,例如 SPI1、SPI2、SPI3等。
 * @param  SPI_I2S_FLAG: 指定要清除的标志位。
 * @retval 无
 */
void SPI_I2S_ClearFlag(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);

/**
 * @brief  获取SPI或I2S外设的中断状态。
 * @param  SPIx: 指向SPI或I2S外设的指针,例如 SPI1、SPI2、SPI3等。
 * @param  SPI_I2S_IT: 指定要检查的中断源,可以是以下值之一:
 *          @arg SPI_I2S_IT_TXE: 发送缓冲区为空中断。
 *          @arg SPI_I2S_IT_RXNE: 接收缓冲区非空中断。
 *          @arg SPI_I2S_IT_ERR: 错误中断。
 *          @arg 其他中断源根据具体芯片参考手册提供。
 * @retval 中断状态(SET 或 RESET)。
 */
ITStatus SPI_I2S_GetITStatus(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT);

/**
 * @brief  清除SPI或I2S外设的中断挂起位。
 * @param  SPIx: 指向SPI或I2S外设的指针,例如 SPI1、SPI2、SPI3等。
 * @param  SPI_I2S_IT: 指定要清除的中断源。
 * @retval 无
 */
void SPI_I2S_ClearITPendingBit(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT);

软件SPI

#include "stm32f10x.h"  // Device header

/* 引脚配置层 */

/**
  * @brief  写SS引脚电平。
  * @param  BitValue: 0表示低电平,1表示高电平。
  * @retval 无
  */
inline void MySPI_W_SS(uint8_t BitValue)
{
    GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);
}

/**
  * @brief  写SCK引脚电平。
  * @param  BitValue: 0表示低电平,1表示高电平。
  * @retval 无
  */
inline void MySPI_W_SCK(uint8_t BitValue)
{
    GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)BitValue);
}

/**
  * @brief  写MOSI引脚电平。
  * @param  BitValue: 0表示低电平,1表示高电平。
  * @retval 无
  */
inline void MySPI_W_MOSI(uint8_t BitValue)
{
    GPIO_WriteBit(GPIOA, GPIO_Pin_7, (BitAction)(BitValue ? Bit_SET : Bit_RESET));
}

/**
  * @brief  读MISO引脚电平。
  * @retval 0表示低电平,1表示高电平。
  */
inline uint8_t MySPI_R_MISO(void)
{
    return GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6);
}

/**
  * @brief  SPI引脚初始化。
  * @retval 无
  */
void MySPI_Init(void)
{
    /* 开启GPIOA时钟 */
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

    /* 配置SS、SCK、MOSI引脚为推挽输出 */
    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);

    /* 配置MISO引脚为上拉输入 */
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    /* 设置默认电平 */
    MySPI_W_SS(1);    // SS默认高电平
    MySPI_W_SCK(0);   // SCK默认低电平
}

/* 协议层 */

/**
  * @brief  SPI起始信号。
  * @retval 无
  */
inline void MySPI_Start(void)
{
    MySPI_W_SS(0);  // 拉低SS,开始通信
}

/**
  * @brief  SPI终止信号。
  * @retval 无
  */
inline void MySPI_Stop(void)
{
    MySPI_W_SS(1);  // 拉高SS,终止通信
}

/**
  * @brief  SPI交换传输一个字节,使用SPI模式0。
  * @param  ByteSend: 要发送的字节。
  * @retval 接收到的字节。
  */
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
    uint8_t ByteReceive = 0;

    for (uint8_t i = 0; i < 8; i++)
    {
        MySPI_W_MOSI(ByteSend & 0x80);  // 写入当前位
        ByteSend <<= 1;  // 左移字节,准备下一个位

        MySPI_W_SCK(1);  // 拉高SCK,上升沿发送
        ByteReceive <<= 1;  // 左移接收字节
        ByteReceive |= MySPI_R_MISO();  // 读取MISO位并存储

        MySPI_W_SCK(0);  // 拉低SCK,准备下一个位
    }

    return ByteReceive;
}

W25Q64

#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

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

/* W25Q64 操作层 */

/**
  * @brief  W25Q64 初始化
  * @retval 无
  */
void W25Q64_Init(void)
{
    MySPI_Init();  // 初始化底层SPI接口
}

/**
  * @brief  读取 W25Q64 的 JEDEC ID
  * @param  MID: 输出参数,工厂ID
  * @param  DID: 输出参数,设备ID
  * @retval 无
  */
void W25Q64_ReadID(uint8_t *MID, uint16_t *DID)
{
    MySPI_Start();  // SPI 起始
    MySPI_SwapByte(W25Q64_JEDEC_ID);  // 发送 JEDEC ID 读取指令
    *MID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);  // 接收 MID
    *DID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);  // 接收 DID 高8位
    *DID <<= 8;
    *DID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE);  // 接收 DID 低8位
    MySPI_Stop();  // SPI 终止
}

/**
  * @brief  向 W25Q64 写使能
  * @retval 无
  */
void W25Q64_WriteEnable(void)
{
    MySPI_Start();  // SPI 起始
    MySPI_SwapByte(W25Q64_WRITE_ENABLE);  // 发送写使能指令
    MySPI_Stop();  // SPI 终止
}

/**
  * @brief  等待 W25Q64 结束忙状态
  * @retval 无
  */
void W25Q64_WaitBusy(void)
{
    uint32_t Timeout = 100000;  // 超时计数

    MySPI_Start();  // SPI 起始
    MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1);  // 发送读状态寄存器1指令

    // 等待忙标志位清除
    while ((MySPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01)
    {
        Timeout--;  // 超时计数递减
        if (Timeout == 0)
        {
            // 超时处理
            /* 错误处理代码可以放置在这里 */
            break;  // 跳出等待
        }
    }

    MySPI_Stop();  // SPI 终止
}

/**
  * @brief  执行 W25Q64 的页编程操作
  * @param  Address: 页编程的起始地址
  * @param  DataArray: 用于写入的数据数组
  * @param  Count: 要写入的数据数量,范围为 0~256
  * @retval 无
  */
void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count)
{
    uint16_t i;

    W25Q64_WriteEnable();  // 写使能

    MySPI_Start();  // SPI 起始
    MySPI_SwapByte(W25Q64_PAGE_PROGRAM);  // 发送页编程指令
    MySPI_SwapByte(Address >> 16);  // 发送地址高字节
    MySPI_SwapByte(Address >> 8);  // 发送地址中字节
    MySPI_SwapByte(Address);  // 发送地址低字节

    for (i = 0; i < Count; i++)
    {
        MySPI_SwapByte(DataArray[i]);  // 逐字节写入数据
    }

    MySPI_Stop();  // SPI 终止

    W25Q64_WaitBusy();  // 等待忙状态结束
}

/**
  * @brief  执行 W25Q64 的扇区擦除操作
  * @param  Address: 扇区的起始地址
  * @retval 无
  */
void W25Q64_SectorErase(uint32_t Address)
{
    W25Q64_WriteEnable();  // 写使能

    MySPI_Start();  // SPI 起始
    MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB);  // 发送扇区擦除指令
    MySPI_SwapByte(Address >> 16);  // 发送地址高字节
    MySPI_SwapByte(Address >> 8);  // 发送地址中字节
    MySPI_SwapByte(Address);  // 发送地址低字节
    MySPI_Stop();  // SPI 终止

    W25Q64_WaitBusy();  // 等待忙状态结束
}

/**
  * @brief  从 W25Q64 读取数据
  * @param  Address: 读取的起始地址
  * @param  DataArray: 用于接收数据的数组
  * @param  Count: 要读取的数据数量
  * @retval 无
  */
void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count)
{
    uint32_t i;

    MySPI_Start();  // SPI 起始
    MySPI_SwapByte(W25Q64_READ_DATA);  // 发送读取数据指令
    MySPI_SwapByte(Address >> 16);  // 发送地址高字节
    MySPI_SwapByte(Address >> 8);  // 发送地址中字节
    MySPI_SwapByte(Address);  // 发送地址低字节

    for (i = 0; i < Count; i++)
    {
        DataArray[i] = MySPI_SwapByte(W25Q64_DUMMY_BYTE);  // 逐字节读取数据
    }

    MySPI_Stop();  // SPI 终止
}

#ifndef __W25Q64_INS_H
#define __W25Q64_INS_H

/* W25Q64 指令集定义 */
#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

CAN总线

位时序

数据帧的结构

STM32外设CAN

CAN框图剖析

CAN编程

初始化结构体

CAN发送及接收结构体

CAN筛选器结构体

Unix时间戳

RTC(实时时钟)概述

RTC是一个专门用于产生年月日时分秒信息的定时器,与普通定时器不同,它的主要功能是记录并保持当前时间和日期。RTC在STM32中具有独立性,即使主电源断电,只要连接了备用电源(通过VBAT引脚),RTC仍然可以继续工作。

RTC与BKP、PWR的关系

  • BKP(备份寄存器):用于存储重要的数据,在主电源断电的情况下,数据可以通过备用电池保持不丢失。
  • PWR(电源控制):控制整个STM32的电源管理。虽然这一章主要讨论RTC和BKP,但PWR的相关内容将在下一节中详细讲解。

课程安排

课程分为三个小节:

  1. 时间戳介绍:理解时间戳的概念以及它在RTC中的应用。
  2. BKP与RTC外设结构:详细讲解BKP和RTC的内部结构及其工作机制。
  3. 代码实现与实验现象:通过代码展示如何实现RTC功能,并演示代码的实际运行效果。

实验现象

实验1:读写BKP寄存器
  1. 连接方式:通过ST-Link引出3.3V电源到VBAT引脚,模拟备用电池供电。
  2. 实验目的:在BKP寄存器中写入两个数据,并读取显示在OLED屏幕上。
  3. 实验过程
    • 初始时,读取BKP寄存器,默认值为0。
    • 按下按键后,将数据12345678写入BKP寄存器,再次读取时显示相同的数据。
    • 重复按键操作,验证写入和读取的数据是否一致。
  4. 断电测试:断开主电源(拔掉电源线),通过VBAT维持BKP寄存器的数据,再次上电后验证数据未丢失。
实验2:实时时钟(RTC)显示
  1. 显示内容:OLED屏幕上显示日期、时间、时间戳秒计数器、以及RTC预分频器计数值。
  2. 复位和掉电测试
    • 按下复位键,RTC时间继续运行,证明复位不会影响时间计数。
    • 断开主电源后,RTC通过VBAT供电继续计时,上电后时间数据仍然准确。

时间戳(Timestamp)的详细解释

概念介绍
  • 时间戳:是从UTC(协调世界时)或GMT(格林尼治标准时间)1970年1月1日零时零分零秒开始,经过的总秒数。它不考虑闰秒。
  • 秒计数器:时间戳的核心是一个秒计数器,用来记录从1970年开始的总秒数。这个计数器可以是32位或64位的整型变量。
时间戳的优缺点
  • 优点
    1. 硬件电路简单:直接使用秒计数器,无需处理复杂的年月日时分秒进位。
    2. 时间间隔计算方便:两个时间戳相减即可得出时间间隔,简单高效。
    3. 存储方便:仅需一个整型变量即可表示时间。
  • 缺点
    1. 占用软件资源:时间戳和日期时间之间的转换需要复杂的计算,会占用一定的软件资源。
时间戳的存储格式
  • 32位时间戳:32位有符号整型变量最大能表示的秒数是21亿多,这意味着到2038年1月19日会溢出。
  • 64位时间戳:现代系统大多使用64位时间戳,能够表示的时间范围非常广阔,足以满足人类使用。

时间戳在不同系统中的应用

  • 系统差异:早期的Unix系统使用32位时间戳,而现代的Linux、Windows等系统已采用64位时间戳。
  • STM32的时间戳:STM32的RTC使用32位无符号整数作为时间戳,因此可以支持到2106年。

不同时区的处理

  • 全球统一的秒计数器:全世界所有时区使用相同的秒计数器,具体时区时间通过添加时区偏移来得到。

C语言中的时间戳函数

  • time.h库:C语言提供了time.h库,用于处理时间戳和日期时间之间的转换。通过这些函数,可以很方便地将秒计数器转换为人类可读的日期和时间。

BKP和RTC

BKP(Backup Registers)

简介

  • BKP 的全称是 Backup Registers,翻译为备份寄存器或后备寄存器。
  • 主要用于存储用户应用程序的数据。
  • BKP 寄存器在主电源(VDD)断电时,通过备用电池(VBAT)供电,保持数据不丢失。即使系统在待机模式下唤醒、复位或电源复位时,数据也不会被清除。

引脚定义

  • VDD: 系统主电源,通常接到 3.3V。
  • VBAT: 备用电池供电,用于在主电源断电时继续供电,电池电压范围为 1.8V 到 3.6V。

原理图

  • 如果不接电池,VBAT 引脚是悬空的。
  • 建议如果没有外部电池,应将 VBAT 引脚接到 VDD,并连接一个 100µF 的滤波电容。

功能

  • 存储容量: 中容量设备 BKP 为 20 字节,大容量设备为 84 字节。
  • 数据保持: 当主电源断电时,BKP 数据可以继续保持。如果 VBAT 也没有电,BKP 数据会被清零。
  • 额外功能:
    • TEMPE: 通过 TEMPE 引脚(PC13)产生侵入事件,清除 BKP 中的数据。用于安全设计,防止数据被篡改。
    • RTC 时钟输出: 可以输出 RTC 校准时钟、闹钟脉冲或秒脉冲。RTC 时钟可以通过 PC13 引脚输出,用于其他设备的时间同步。

RTC(Real-Time Clock)

简介

  • RTC 的全称是 Real-Time Clock,即实时钟。
  • 在 STM32 中,RTC 是一个独立的定时器,提供时钟和日历功能,包括年月日时分秒。
  • 与外部 RTC 芯片相比,STM32 内部 RTC 可以减少外部组件,简化设计。

特点

  • 备用电池供电: RTC 在主电源断电后,依靠 VBAT 继续运行,以保持时钟的持续性。
  • 32 位计数器: 用于记录秒数。RTC 通过时间戳的方式提供日期时间信息。
  • 20 位预分频器: 适配不同频率的输入时钟,将时钟频率分为 1Hz,用于驱动 32 位秒计数器。
  • 时钟源: RTC 可选择三种时钟源:
    • HSE: 高速外部晶振,通常为 8MHz 通过 128 分频得到 32.768kHz。
    • LSE: 低速外部晶振,通常为 32.768kHz。
    • LSI: 低速内部 RC 振荡器,固定频率为 40kHz。

框图

 

PRL 设置为32768,32678000/32768=1000=1mhz       1/1000 = 0.001s =1ms  DIV是递减

  • 分频和计时部分: RTC 输入时钟经过预分频器分频到 1Hz,然后驱动 32 位秒计数器。
  • 中断和 NVC 部分: 提供中断功能和时钟输出,唤醒系统或生成时钟信号。
  • 应用总线读写: 用于读写 RTC 寄存器配置和获取时间信息。
  • 与 PW 关联的部分: RTC 的闹钟功能可用于唤醒设备或退出待机模式。

RTC库函数

/**
  * 函数名:RTC_ITConfig
  * 说明:配置RTC中断功能
  * 参数:
  *   - RTC_IT: 需要配置的RTC中断源(例如RTC_IT_ALR, RTC_IT_SEC等)
  *   - NewState: 功能状态,ENABLE或DISABLE
  * 返回值:无
  */
void RTC_ITConfig(uint16_t RTC_IT, FunctionalState NewState);

/**
  * 函数名:RTC_EnterConfigMode
  * 说明:进入RTC配置模式
  * 参数:无
  * 返回值:无
  */
void RTC_EnterConfigMode(void);

/**
  * 函数名:RTC_ExitConfigMode
  * 说明:退出RTC配置模式
  * 参数:无
  * 返回值:无
  */
void RTC_ExitConfigMode(void);

/**
  * 函数名:RTC_GetCounter
  * 说明:获取RTC计数器的当前值
  * 参数:无
  * 返回值:RTC计数器的值(32位)
  */
uint32_t RTC_GetCounter(void);

/**
  * 函数名:RTC_SetCounter
  * 说明:设置RTC计数器的值
  * 参数:
  *   - CounterValue: 要设置的计数器值(32位)
  * 返回值:无
  */
void RTC_SetCounter(uint32_t CounterValue);

/**
  * 函数名:RTC_SetPrescaler
  * 说明:设置RTC预分频器的值
  * 参数:
  *   - PrescalerValue: 要设置的预分频器值(32位)
  * 返回值:无
  */
void RTC_SetPrescaler(uint32_t PrescalerValue);

/**
  * 函数名:RTC_SetAlarm
  * 说明:设置RTC闹钟的值
  * 参数:
  *   - AlarmValue: 要设置的闹钟值(32位)
  * 返回值:无
  */
void RTC_SetAlarm(uint32_t AlarmValue);

/**
  * 函数名:RTC_GetDivider
  * 说明:获取RTC分频器的值
  * 参数:无
  * 返回值:RTC分频器的值(32位)
  */
uint32_t RTC_GetDivider(void);

/**
  * 函数名:RTC_WaitForLastTask
  * 说明:等待RTC完成上一个任务
  * 参数:无
  * 返回值:无
  */
void RTC_WaitForLastTask(void);

/**
  * 函数名:RTC_WaitForSynchro
  * 说明:等待RTC同步完成
  * 参数:无
  * 返回值:无
  */
void RTC_WaitForSynchro(void);

/**
  * 函数名:RTC_GetFlagStatus
  * 说明:获取RTC标志位的状态
  * 参数:
  *   - RTC_FLAG: 要检查的RTC标志位(例如RTC_FLAG_ALR, RTC_FLAG_SEC等)
  * 返回值:标志位状态(SET或RESET)
  */
FlagStatus RTC_GetFlagStatus(uint16_t RTC_FLAG);

/**
  * 函数名:RTC_ClearFlag
  * 说明:清除RTC标志位
  * 参数:
  *   - RTC_FLAG: 要清除的RTC标志位(例如RTC_FLAG_ALR, RTC_FLAG_SEC等)
  * 返回值:无
  */
void RTC_ClearFlag(uint16_t RTC_FLAG);

/**
  * 函数名:RTC_GetITStatus
  * 说明:获取RTC中断状态
  * 参数:
  *   - RTC_IT: 要检查的RTC中断源(例如RTC_IT_ALR, RTC_IT_SEC等)
  * 返回值:中断状态(SET或RESET)
  */
ITStatus RTC_GetITStatus(uint16_t RTC_IT);

/**
  * 函数名:RTC_ClearITPendingBit
  * 说明:清除RTC中断挂起位
  * 参数:
  *   - RTC_IT: 要清除的RTC中断源(例如RTC_IT_ALR, RTC_IT_SEC等)
  * 返回值:无
  */
void RTC_ClearITPendingBit(uint16_t RTC_IT);

实时时钟

#include "stm32f10x.h"                  // Device header
#include <time.h>

uint16_t MyRTC_Time[] = {2023, 1, 1, 23, 59, 55}; // 定义全局时间数组

void MyRTC_SetTime(void);                // 函数声明
void MyRTC_ReadTime(void);               // 函数声明

/**
  * 函数:RTC初始化
  * 说明:初始化RTC,设置时钟源和时间
  */
void MyRTC_Init(void)
{
    /* 开启时钟 */
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);      // 开启PWR时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE);      // 开启BKP时钟
    
    /* 使能备份寄存器访问 */
    PWR_BackupAccessCmd(ENABLE);                            // 启用对备份寄存器的访问
    
    if (BKP_ReadBackupRegister(BKP_DR1) != 0xA5A5)         // 检查是否为第一次配置
    {
        RCC_LSEConfig(RCC_LSE_ON);                          // 开启LSE时钟
        while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) != SET);  // 等待LSE准备就绪
        
        RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);             // 选择LSE作为RTC时钟源
        RCC_RTCCLKCmd(ENABLE);                              // 使能RTC时钟
        
        RTC_WaitForSynchro();                              // 等待RTC同步
        RTC_WaitForLastTask();                              // 等待上一次操作完成
        
        RTC_SetPrescaler(32768 - 1);                       // 设置预分频器为1Hz
        RTC_WaitForLastTask();                              // 等待上一次操作完成
        
        MyRTC_SetTime();                                   // 设置时间
        
        BKP_WriteBackupRegister(BKP_DR1, 0xA5A5);         // 写入标志位,标记RTC已初始化
    }
    else
    {
        RTC_WaitForSynchro();                              // 等待同步
        RTC_WaitForLastTask();                              // 等待上一次操作完成
    }
}

/**
  * 函数:RTC设置时间
  * 说明:将全局时间数组中的时间写入RTC
  */
void MyRTC_SetTime(void)
{
    time_t time_cnt;        // 定义秒计数器
    struct tm time_date;    // 定义日期时间结构体
    
    // 将数组的时间赋值给日期时间结构体
    time_date.tm_year = MyRTC_Time[0] - 1900;  // 年份
    time_date.tm_mon = MyRTC_Time[1] - 1;      // 月份
    time_date.tm_mday = MyRTC_Time[2];         // 日
    time_date.tm_hour = MyRTC_Time[3];         // 时
    time_date.tm_min = MyRTC_Time[4];          // 分
    time_date.tm_sec = MyRTC_Time[5];          // 秒
    
    // 转换为秒计数器,并调整时区
    time_cnt = mktime(&time_date) - 8 * 60 * 60; 
    
    RTC_SetCounter(time_cnt);                   // 设置RTC计数器
    RTC_WaitForLastTask();                      // 等待操作完成
}

/**
  * 函数:RTC读取时间
  * 说明:从RTC读取当前时间,并更新全局时间数组
  */
void MyRTC_ReadTime(void)
{
    time_t time_cnt;        // 定义秒计数器
    struct tm time_date;    // 定义日期时间结构体
    
    // 从RTC读取计数器,并调整时区
    time_cnt = RTC_GetCounter() + 8 * 60 * 60;  
    
    // 转换为日期时间结构体
    time_date = *localtime(&time_cnt);          
    
    // 更新全局时间数组
    MyRTC_Time[0] = time_date.tm_year + 1900;  // 年份
    MyRTC_Time[1] = time_date.tm_mon + 1;      // 月份
    MyRTC_Time[2] = time_date.tm_mday;         // 日
    MyRTC_Time[3] = time_date.tm_hour;         // 时
    MyRTC_Time[4] = time_date.tm_min;          // 分
    MyRTC_Time[5] = time_date.tm_sec;          // 秒
}

BKP库函数

/**
  * @brief  复位备份寄存器到其默认值,并解除对备份区域的锁定。
  * @note   这个函数通常在需要重新配置备份区域或恢复出厂设置时使用。
  */
void BKP_DeInit(void);

/**
  * @brief  配置篡改引脚(Tamper Pin)的检测电平。
  * @param  BKP_TamperPinLevel 检测电平的配置值,通常为低电平或高电平。
  * @note   根据输入参数设置篡改检测引脚的触发电平(高电平或低电平)。
  */
void BKP_TamperPinLevelConfig(uint16_t BKP_TamperPinLevel);

/**
  * @brief  使能或禁用篡改引脚。
  * @param  NewState 使能或禁用状态,值为ENABLE或DISABLE。
  * @note   用于控制是否启用篡改引脚的功能。
  */
void BKP_TamperPinCmd(FunctionalState NewState);

/**
  * @brief  使能或禁用篡改引脚的中断。
  * @param  NewState 使能或禁用状态,值为ENABLE或DISABLE。
  * @note   用于配置是否在篡改事件发生时产生中断。
  */
void BKP_ITConfig(FunctionalState NewState);

/**
  * @brief  配置RTC输出源到备份寄存器。
  * @param  BKP_RTCOutputSource 配置的RTC输出源,可以是RTC时钟或报警。
  * @note   通过此函数可以将RTC的特定输出路由到备份区域。
  */
void BKP_RTCOutputConfig(uint16_t BKP_RTCOutputSource);

/**
  * @brief  设置RTC的校准值。
  * @param  CalibrationValue 要设置的校准值,范围从0到127。
  * @note   此函数用于微调RTC的精度,通过调整校准值来补偿时钟漂移。
  */
void BKP_SetRTCCalibrationValue(uint8_t CalibrationValue);

/**
  * @brief  写入数据到备份寄存器。
  * @param  BKP_DR 备份寄存器的地址。
  * @param  Data 要写入的数据。
  * @note   用于将数据保存到指定的备份寄存器中,在断电后依然能够保存。
  */
void BKP_WriteBackupRegister(uint16_t BKP_DR, uint16_t Data);

/**
  * @brief  从备份寄存器读取数据。
  * @param  BKP_DR 备份寄存器的地址。
  * @retval 读取到的数据。
  * @note   从指定的备份寄存器中读取数据。
  */
uint16_t BKP_ReadBackupRegister(uint16_t BKP_DR);

/**
  * @brief  检查备份域中的标志状态。
  * @retval 返回标志状态,可以是SET或RESET。
  * @note   用于检查备份域中是否存在特定的标志。
  */
FlagStatus BKP_GetFlagStatus(void);

/**
  * @brief  清除备份域中的标志。
  * @note   用于重置备份域中的特定标志位。
  */
void BKP_ClearFlag(void);

/**
  * @brief  检查备份域中的中断状态。
  * @retval 返回中断状态,可以是SET或RESET。
  * @note   用于检查是否发生了备份域相关的中断。
  */
ITStatus BKP_GetITStatus(void);

/**
  * @brief  清除备份域中的中断挂起位。
  * @note   用于清除备份域中断的挂起状态,确保不重复响应同一个中断。
  */
void BKP_ClearITPendingBit(void);

 读写备份寄存器

#include "stm32f10x.h"                  // 设备头文件,包含STM32F10x系列的定义
#include "Delay.h"                      // 延时函数的头文件
#include "OLED.h"                       // OLED显示相关函数的头文件
#include "Key.h"                        // 按键相关函数的头文件

uint8_t KeyNum;                        // 定义用于接收按键键码的变量

uint16_t ArrayWrite[] = {0x1234, 0x5678};  // 定义要写入数据的测试数组
uint16_t ArrayRead[2];                      // 定义要读取数据的测试数组

int main(void)
{
    /* 模块初始化 */
    OLED_Init();                         // 初始化OLED显示模块
    Key_Init();                          // 初始化按键模块
    
    /* 显示静态字符串 */
    OLED_ShowString(1, 1, "W:");        // 在OLED显示的第1行第1列显示字符串"W:"
    OLED_ShowString(2, 1, "R:");        // 在OLED显示的第2行第1列显示字符串"R:"
    
    /* 开启时钟 */
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);  // 开启PWR(备份寄存器)的时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE);  // 开启BKP(备份寄存器)的时钟
    
    /* 备份寄存器访问使能 */
    PWR_BackupAccessCmd(ENABLE);                  // 使能对备份寄存器的访问权限
    
    while (1)
    {
        KeyNum = Key_GetNum();    // 获取按键键码
        
        if (KeyNum == 1)          // 如果按键1被按下
        {
            ArrayWrite[0] ++;    // 测试数据自增
            ArrayWrite[1] ++;
            
            BKP_WriteBackupRegister(BKP_DR1, ArrayWrite[0]);  // 将测试数据写入备份寄存器1
            BKP_WriteBackupRegister(BKP_DR2, ArrayWrite[1]);  // 将测试数据写入备份寄存器2
            
            OLED_ShowHexNum(1, 3, ArrayWrite[0], 4);  // 在OLED上显示写入的测试数据(4位十六进制数)
            OLED_ShowHexNum(1, 8, ArrayWrite[1], 4);  // 在OLED上显示写入的测试数据(4位十六进制数)
        }
        
        ArrayRead[0] = BKP_ReadBackupRegister(BKP_DR1);  // 从备份寄存器1读取数据
        ArrayRead[1] = BKP_ReadBackupRegister(BKP_DR2);  // 从备份寄存器2读取数据
        
        OLED_ShowHexNum(2, 3, ArrayRead[0], 4);        // 在OLED上显示读取的数据(4位十六进制数)
        OLED_ShowHexNum(2, 8, ArrayRead[1], 4);        // 在OLED上显示读取的数据(4位十六进制数)
    }
}

串行FLASH文件系统 FatFs

http://elm-chan.org/fsw/ff/00index_e.html

 FSMC—扩展外部SRAM

 

 

想深耕嵌入式?这个专辑值得收藏

MCU、FPGA、工控、传感器一站式学习,实战项目直接抄

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值