约定:

  • 文中串口通信,均为UART异步通信。
  • 函数命名,不使用USART,统一使用:UART1、UART2、UART3、UART4、UART5、UART6。

文章目录:

一、前言

二、文件移植

三、初始化、引脚使用

四、发送

五、接收

六、冲突、错误的解决

七、F103系列的源代码

八、F407系列的源代码


一、前言

STM32的UART串口通信,在使用CubeMX进行配置时,已非常地方便。

配置后,只需编写少量的代码,即可实现收发。

但是,为了更省工夫:

  • 移植更方便 (省掉CubeMX的配置,不用编写回调函数,  复制一下文件就能直接使用)
  • 使用更方便 (函数简单清晰,能收发不同类型的数据,能通信大部分的UART模块)

同时也为了照顾一些刚玩的兄弟:不想费时间学习UART理论、费时间调试收发机制。

为此,对UART的常用初始化、通信,重写了两个文件:bsp_UART.c  和 bsp_UART.h

文章末处,粘贴了完整的代码,需要的自行复制去。

常用的F103、F407,UART1~UART6都已编写、封装好了需要的函数。

每个 UART的操作,共有7个函数可调用。以UART1为例 , 下面将详细描述使用方法:

// 初始化
void      UART1_Init(uint32_t ulBaudrate);                                      // 初始化串口1; GPIO引脚PA9+PA10、中断优先级、通信参数:波特率可设、8位数据、无校验、1个停止位
// 发送
void      UART1_SendString(const char *pcString, ...);                          // 发送字符串;   参数:字符串地址; 使用方法如同printf
uint8_t   UART1_SendAT(char *pcAT, char *pcAckString, uint16_t usTimeOutMs);    // 本函数,针对ESP8266、蓝牙模块等AT固件,用于等待返回期待的信息; 参数:AT指令字符串、期待返回信息字符串、ms等待超时; 返回:0-执行失败、1-执行成功 
void      UART1_SendData(uint8_t *puData, uint16_t usNum);                      // 发送指定数据; 参数:数据地址、字节数
// 接收
uint16_t  UART1_GetRxNum(void);                                                 // 获取接收到的最新一帧字节数
uint8_t*  UART1_GetRxData(void);                                                // 获取接收到的数据 (缓存的地址)
void      UART1_ClearRx(void);                                                  // 清理接收到的数据 (清理最后一帧字节数,因为它是判断接收的标志)


二、文件移植

1、新建 bsp_UART.c 和 bsp_UART.h这两个文件

  • 如果已有魔女开发板的示例网盘,打开随便一个示例的 BSP 文件夹,把其下UART文件夹,复制到你的目标工程中即可,不用另行新建文件。
  • 如果没有,就到文章末处,复制 bsp_UART.c 和 bsp_UART.h 的代码,在目标工程的文件夹中,新建bsp_UART.c 和 bsp_UART.h 这两个文件,对应粘贴进去即可。

2、工程添加 bsp_UART.c 文件

  • 双击keil左侧的目标文件夹,添加bsp_UART.c到工程中。
  • 也可以点击工具栏的 组管理 按钮进行添加; 操作如下图:

 3、添加头文件路径;  

刚才添加了 c文件到工程,还需要添加头文件的路径,编译器才能知道 h 文件的存放位置  。

操作如下:

       

4、添加文件引用

在需要UART功能的文件中,添加:#include "bsp_UART.h"

如,main.c 中想使用UART,就在它的头部位置添加。

操作如下:

好了,移植就是这么简单,至此,你的工程中已经可以正常使用UART功能了。


三、初始化、引脚使用

以使用UART1为例,其它几个UART的初始化方法相同。

调用:UART1_Init (115200) ;    //  参数:波特率     

操作如下:     

                 

此函数将会执行:引脚PA9+PA10的初始化、UART1初始化、中断初始化、进入接收状态。

初始化仅需调用这个函数即可。不用修改代码,不用管其它的。

各个UART在初始化时,将默认使用以下引脚:

这些都是STM32不同系列通用的 UART引脚,不管F1还是F4,都可以使用以下引脚,无需修改。

串口端口 发送引脚 (TX) 接收引脚 (RX)
UART1 PA9 PA10
UART2 PA2 PA3
UART3 PB10 PB11
UART4 PC10 PC11
UART5 PC12 PD2
UART6 PC6 PC7

      


四、发送

各个UART的发送,都提供了 3个函数。

灵活选择,能实现不同场景数据的发送,  如:float、int、数组、字符串、AT指令、结构体,等等。

发送机制:发送中断+环形队列。当然,这些底层不用管、不用修改,只管调用函数。

void      UART1_SendString(const char *pcString, ...);                          // 发送字符串;   参数:字符串地址; 使用方法如同printf
void      UART1_SendData(uint8_t *puData, uint16_t usNum);                      // 发送指定数据; 参数:数据地址、字节数
uint8_t   UART1_SendAT(char *pcAT, char *pcAckString, uint16_t usTimeOutMs);    // 本函数,针对ESP8266、蓝牙模块等AT固件,用于等待返回期待的信息; 参数:AT指令字符串、期待返回信息字符串、ms等待超时; 返回:0-执行失败、1-执行成功 

函数解释:

  发送字符串,用:UART1_SendString (char *string,...) 

使用方法如同 printf,相当灵活,具体可以百度一下printf的格式化如何使用,操作是一样的。

使用示范:

UART1_SendString("\r 你好吗? \r");   
UART1_SendString("字节数:%d 字节\r", rxNum);            
UART1_SendString("数据 (ASCII): %s\r", (char *)rxData);  

②  发送指定长度的数据,用:UART1_SendData (*data, num)   

参数:缓存地址、字节数

可用于发送数组、结构体、字符串 等任意数据。     

使用示范:                

// 发送字符串
UART1_SendData((uint8_t*)"你好吗", 6);             
// 发送数组、16进制数制
uint8_t myArray[50] = {0xAA, 0xBB, 0xCC, 0xDD};    
UART1_SendData(myArray, 8);   
// 发送浮点类型数据
float myFloat = 520.1314;                          
UART1_SendData(&myFloat, sizeof(float));       

③  发送AT指令:UART1_SendAT(char *pcAT, char *pcAckString, uint16_t usTimeOutMs);  

参数:AT指令字符串、期待返回信息字符串、等待超时ms;

返回:0-执行失败、1-执行成功 

本函数能很方便地与ESP8266、蓝牙模块等进行通过。

用于发送AT指令后,等待执行反馈; 

使用示范:

UART1_SendAT("AT+RESTORE\r\n", "ready", 3000)  ? printf("恢复出厂设置:  成功\r\n") : printf("恢复出厂设置:  失败\r\n");   // 恢复模块的出厂设置
UART1_SendAT("AT+CWMODE=1\r\n",   "OK", 3000)  ? printf("配置 STA模式:  成功\r\n") : printf("配置 STA模式:  失败\r\n");   // 工作模式:1_STA, 2_AP, 3_STA+AP
UART1_SendAT("AT+RST\r\n",     "ready", 3000)  ? printf("重启 ESP8266:  成功\r\n") : printf("重启 ESP8266:  失败\r\n");   // 重启模块: 设置工作模式后,需重启才生效
    
char strTemp[100] = {0};
sprintf(strTemp, "AT+CWJAP=\"%s\",\"%s\"\r\n", "wifiName", "wifiPassWord");
if(UART1_SendAT(strTemp, "OK\r\n", 6000) )
    printf("连接WiFi网络 成功\r\n");
else
    printf("连接WiFi网络 失败\r\n");  

printf

        很多人习惯在调试时使用printf。

        在bsp_UART.c里,也做好了它的重定向输出。

        printf已重定向输出到UART1,在初始化 UART1后,直接可用printf。


五、接收

接收处理,共3个函数:        

uint16_t UART1_GetRxNum(void);                       // 获取接收到的最新一帧字节数  
uint8_t* UART1_GetRxdData(void);                     // 获取接收到的数据(缓存的地址)
void     UART1_ClearRx(void);                        // 清理接收到的数据(清理最后一帧字节数,因为它是判断接收的标志)

接收机制上,使用:接收中断 + 空闲中断 + 双缓冲区。这些底层工作都已封装处理好,不用管。使用上,只管调用上面这仨函数。

      

  ①   获取字节数:UART1_GetRxNum ( )    

作用:获取最后一帧接收到的 字节数;

  • 参数:无
  • 返回:字节数 ( uint16_t) 
  • 串口在调用 初始化函数 后,再调用这个函数判断返回值,即可知道接收状态:
  • 当返回值 (接收字节数 ) ==0 时:没有接收到数据; 
  • 当返回值 (接收字节数 )  > 0时:接收到新一帧数据 ;

②  获取数据:UART1_GetRxData ( )

作用:获取最后一帧接收到的 数据地址;

  • 参数:无
  • 返回:数据地址 (uint8_t* ,注意:是指针)
  • 如果不会使用指针,可以把结果当数组使用,用符号 [ ] 对数据进行访问。

③  清0本次接收:UART1_ClearRx ( ) ;      

作用:在处理完最后一帧的数据后,清0本帧数据的缓存

  • 参数:无
  • 返回:无
  • 其实就是清0 最后一帧的接收字节数。
  • 重要:每次接收到一帧数据,处理完了,就调用这函数,清0,以方便下一轮的判断!

下面代码,示范如何接收、处理数据(以串口1为例,其它串口同理):


六、常见冲突、错误的解决

本文所提供的 bsp_UART.c 和 bsp_UART.h ,支持标准库、HAL库,复制过去即可。

1、添加到原子哥的标准库工程中可能的冲突

原子哥的示例,很多人喜欢使用。

这些示例中,一般已带有uart.c和printf的重定向处理。

如果想添加到这些示例中,建议先备份工程!删除工程中UART文件和代码,再移植此文件。

2、添加到CubeMX、CubeIDE工程中可能的冲突

明确地,无需在CubeMX、CubeIDE中做任何UART的设置。

如果工程中已配置UART,建议先备份工程!关闭CubeMX、CubeIDE中对UART使能,再移植。

如果,使用CubeMX、CubeIDEE配置工程时,想使用此代码,又想标记哪些串口和相关的UART引脚已使用,可以的,在CubeMX、CubeIDE上使能相关的UART即可,这样就能方便地标记哪些引脚被使用了。

但是,记得使用上述表格中的引脚,和,不能打勾串口配置选项的中断、DMA,否则冲突。

使用CubeMX时重点注意的冲突:

  • 使用上述表格的所用的串口引脚
  • 在CubeMX或CubeIDE中,可以使能UART,但不要打勾串口的中断、配置DMA,否则冲突。

       

当在CubeMX、CubeIDE中,如上使能串口后,也能使用HAL库的发送函数、接收函数,不冲突:

  • HAL_UART_Transmit ( ) 
  • HAL_UART_Receive( )

只是,不建议使用了。因为bsp_USART.c的函数,使用起来更方便。

        

3、不同串口,使用不同的方式

如果,某个串口,想使用CubeMX进行中断、DMA配置; 而某些串口,想使用文件中的函数:

可以的.

在bsp_UART.h文件中,修改下面的宏定义,默认是1,即开启,修改为0值关闭即可。

宏定义 值解释:

  • 0:自己另行配置、编写,例如使用CubeMX的中断、DMA配置等
  • 1:使用文件的配置、函数

4、发现通信中漏数据

这种情况,是缓存过小。

在bsp_UART.h里,把发送缓存或接收缓存改大一些即可

5、接收时,发现漏帧

不是漏了一帧的某些内容,是整帧漏了。

这种情况,一般是接收处理的不及时。

如对方每100ms发送一次,但我们的代码while里,每间隔500ms才执行一次接收处理。

把接收处理的间隔,调小一些即可。或者,while里的接收处理,完全不用时间间隔。


七、STM32F103 源代码

可复制以下代码到文件,也可以直接下载:bsp_UART.c + bsp_UART.h

1、bsp_UART.h

#ifndef __BSP__UART_H
#define __BSP__UART_H
/***********************************************************************************************************************************
 ** 【代码编写】  魔女开发板团队
 ** 【淘    宝】  魔女开发板       https://demoboard.taobao.com
 ***********************************************************************************************************************************
 ** 【文件名称】  bsp_UART.h
 **
 ** 【 功  能 】  串口通信底层驱动(UART1、UART2、UART3、UART4、UART5、UART6)
 **               波特率-None-8-1
 **               调用全局声明中的串口函数,即可完成初始化、发送、接收.
 **
 ** 【 约  定 】  本文件所用串口通信,均为异步通信。
 **               2024年起更新的示例,串口通信函数命名时,统一使用"UART",而不使用旧文件中的"USART".
 **
 ** 【适用平台】  STM32F103 + keil5 + HAL库/标准库
 **
 ** 【串口引脚】  各个串口的初始化函数UARTx_Init(),将按以下默认引脚进行初始化:
 **               1- UART1  TX_PA9   RX_PA10       特别说明:魔女开发板系列,均板载USB转TTL,PCB已布线连接好UART1, 使用和烧录用的同一USB接口,即可通过UART1和电脑进行通信。具体查看资料文件夹中的说明文件。
 **               2- UART2  TX_PA2   RX_PA3
 **               3- UART3  TX_PB10  RX_PB11
 **               4- UART4  TX_PC10  RX_PC11
 **               5- UART5  TX_PC12  RX_PD2
 **
 ** 【移植说明】  1- 如果使用CubeMX配置的工程,无须对UART进行任何配置;
 **               2- 适用于HAL库、标准库工程,移植后,均可使用;
 **               3- 复制本工程的UART文件夹,到目标工程文件夹中;
 **               4- 添加头文件路径: Keil > Option > C/C++ > Include Paths;
 **               5- 添加C文件到工程: Keil > 左侧工程管理器中双击目标文件夹 > 选择 bsp_UART.c;
 **               6- 添加文件引用:    #include "bsp_uart.h",即哪个文件要用串口功能,就在其代码文件顶部添加引用;
 **
 ** 【代码使用】  每组串口,已封装好7个函数 (初始化1个、发送3个、接收3个)。下面以UART1作示范说明:               
 **               1、初始化:             void      UART1_Init(uint32_t ulBaudrate);                                      // 初始化串口; 配置GPIO引脚PA9+PA10、配置通信参数:波特率-None-8-1、配置中断
 **               2、发送指定长度的数据  void      UART1_SendData(uint8_t *puData, uint16_t usNum);                      // 发送指定长度的数据; 参数:数据地址、字节数
 **               3、发送字符串          void      UART1_SendString(const char *pcString,...);                           // 发送字符串;   参数:字符串地址; 使用方法如同printf
 **               4、发送AT指令          uint8_t   UART1_SendAT(char *pcAT, char *pcAckString, uint16_t usTimeOutMs);    // 本函数,针对ESP8266、蓝牙模块等AT固件,用于等待返回期待的信息; 参数:AT指令字符串、期待返回信息字符串、ms等待超时; 返回:0-执行失败、1-执行成功 
 **               5、获取接收的字节数:   uint16_t  UART1_GetRxNum(void);                                                 // 获取接收到的字节数; 如果返回值大于0,即为接收到新一帧数据
 **               6、获取接收的数据:    uint8_t*  UART1_GetRxData(void);                                                // 获取接收到的数据(缓存的地址)
 **               7、清零接收标志:      void      UART1_ClearRx(void);                                                  // 清0接收标志,即清0接收到的字节数; 每次处理完成数据,就要调用这个函数清0,方可进入下轮接收判断
 **
 ** 【更新记录】  2024-10-22  中断服务函数:使用宏定义代替位操作,以提高可读情; 优化注释;
 **               2024-09-26  增加MD、HD的判断
 **               2024-07-02  增加Modbus_CRC16校验的追加、判断函数
 **               2024-06-30  优化UART初始化:增加奇偶校验3个寄存器位的代码配置行
 **               2024-06-20  优化AT指令函数, 完善反馈等待机制; 取消:UARTx_WaitACK();
 **               2024-06-07  增加AT指令等待反馈处理函数:UARTx_WaitACK();
 **               2024-06-07  优化函数:UARTx_SendString();
 **               2024-06-06  优化代码适配,使文件能适用于标准库、HAL库工程
 **               2024-04-04  UARTx_SendString(),取消临时缓存的static,以减少RAM占用,适配LVGL移植;
 **               2024-03-27  修改xUSATR_TypeDef结构体,优化缓冲的存储方式
 **               2024-02-04  简化接收函数命名,函数名中使用Rx字符,代替旧函数名称中的Reveived字符;
 **               2024-01-09  取消部分静态变量,并入结构体中,使用调用更直观
 **               2024-01-03  完善函数、注释
 **               2023-12-25  增加接收函数的封装,取消接收变量(全局)的使用
 **               2023-12-23  完成对HAL库的支持
 **               2023-01-27  增加宏定义、完善注释
 **               2021-12-16  完善接收机制:取消接收标志,判断接收字节数>0即为接收到新数据
 **               2021-11-03  完善接收函数返回值处理
 **               2021-08-14  增加宏定义:接收缓存区大小设定值,使空间占用更可控;
 **               2021-08-14  修改发送函数名称:UARTx_Printf(),修改为:UARTx_SendString();
 **               2020-09-02  文件建立、UART1接收中断、空闲中断、发送中断、DMA收发
 **               2021-06-04  UART1、2、3及UART4、5 的收发完善代码
 **               2021-06-09  完善文件格式、注释
 **               2021-06-22  完善注释说明
 **
************************************************************************************************************************************/
#include "stdio.h"        // 引用C语言标准库,它定义了标准输入输出函数; 如:printf、scanf、sprintf、fopen 等; 
#include "stdarg.h"       // 引用C语言标准库,它定义了处理可变参数的宏; 如:va_start、va_arg、va_end、va_list 等;
#include "string.h"       // 引用C语言标准库,它定义了操作字符串的函数; 如:strcpy、strcmp、strlen、memset、memcpy 等;



#ifdef USE_STDPERIPH_DRIVER
    #include "stm32f10x.h"                        // 引用 标准库的底层支持文件
#endif                                            
                                                  
#ifdef USE_HAL_DRIVER                             
    #include "stm32f1xx_hal.h"                    // 引用 HAL库的底层支持文件
#endif


/*****************************************************************************
 ** 移植配置修改区
 ** 备注:除非有特殊要求,否则,下面参数已通用如蓝牙通信、ESP8266通信、串口屏等
****************************************************************************/
// 串口开关
#define UART1_EN                       1          // 串口1,0=关、1=启用;  倘若没用到USART1, 置0,就不会开辟USART1发送缓存、接收缓存,省一点资源;
#define UART2_EN                       1          // 串口2,0=关、1=启用;  同上;
#define UART3_EN                       1          // 串口3,0=关、1=启用;  同上;
#define UART4_EN                       1          // 串口4,0=关、1=启用;  同上;
#define UART5_EN                       1          // 串口5,0=关、1=启用;  同上;
// 发送缓冲区大小                                 
#define UART1_TX_BUF_SIZE           2048          // 配置每组USART发送环形缓冲区数组的大小,单位:字节数; 
#define UART2_TX_BUF_SIZE            512          // -- 只有在前面串口开关在打开状态,才会定义具体的缓冲区数组
#define UART3_TX_BUF_SIZE            512          // -- 默认值:512,此值已能适配大部场景的通信; 如果与ESP8266之类的设备通信,可适当增大此值。
#define UART4_TX_BUF_SIZE            512          // -- 值范围:1~65535; 注意初始化后,不要超过芯片最大RAM值。
#define UART5_TX_BUF_SIZE            512          // -- 注意此值是一个环形缓冲区大小,决定每一帧或多帧数据进入发送前的总缓存字节数,先进先出。
// 接收缓冲区大小                                 
#define UART1_RX_BUF_SIZE           2048          // 配置每组USART接收缓冲区的大小,单位:字节; 此值影响范围:中断里的接收缓存大小,接收后数据缓存的大小
#define UART2_RX_BUF_SIZE            512          // --- 当接收到的一帧数据字节数,小于此值时,数据正常;
#define UART3_RX_BUF_SIZE            512          // --- 当接收到的一帧数据字节数,超过此值时,超出部分,将在中断中直接弃舍,直到此帧接收结束(发生空闲中断); 
#define UART4_RX_BUF_SIZE            512          // 
#define UART5_RX_BUF_SIZE            512          // 
// 结束-配置修改



/*****************************************************************************
 ** 声明全局函数
 **
 ** 每个串口的函数:
 ** 初始化    1个   波特率-None-8-1
 ** 发送      3个   发送指定长度数据、字符串、AT指令 
 ** 接收      3个   获取字节数、获取数据、清0
****************************************************************************/
// UART1
void      UART1_Init(uint32_t ulBaudrate);                                      // 初始化串口1; GPIO引脚PA9+PA10、中断优先级、通信参数:波特率可设、8位数据、无校验、1个停止位
void      UART1_SendData(uint8_t *puData, uint16_t usNum);                      // 发送指定数据; 参数:数据地址、字节数
void      UART1_SendString(const char *pcString, ...);                          // 发送字符串;   参数:字符串地址; 使用方法如同printf
uint8_t   UART1_SendAT(char *pcAT, char *pcAckString, uint16_t usTimeOutMs);    // 本函数,针对ESP8266、蓝牙模块等AT固件,用于等待返回期待的信息; 参数:AT指令字符串、期待返回信息字符串、ms等待超时; 返回:0-执行失败、1-执行成功 
uint16_t  UART1_GetRxNum(void);                                                 // 获取接收到的最新一帧字节数
uint8_t * UART1_GetRxData(void);                                                // 获取接收到的数据 (缓存的地址)
void      UART1_ClearRx(void);                                                  // 清理接收到的数据 (清理最后一帧字节数,因为它是判断接收的标志)
// UART2                                                                        
void      UART2_Init(uint32_t ulBaudrate);                                      // 初始化串口2; GPIO引脚PA2+PA3、中断优先级、通信参数:波特率可设、8位数据、无校验、1个停止位
void      UART2_SendData(uint8_t *puData, uint16_t usNum);                      // 发送指定数据; 参数:数据地址、字节数
void      UART2_SendString(const char *pcString, ...);                          // 发送字符串;   参数:字符串地址; 使用方法如同printf
uint8_t   UART2_SendAT(char *pcAT, char *pcAckString, uint16_t usTimeOutMs);    // 本函数,针对ESP8266、蓝牙模块等AT固件,用于等待返回期待的信息; 参数:AT指令字符串、期待返回信息字符串、ms等待超时; 返回:0-执行失败、1-执行成功 
uint16_t  UART2_GetRxNum(void);                                                 // 获取接收到的最新一帧字节数
uint8_t * UART2_GetRxData(void);                                                // 获取接收到的数据 (缓存的地址)
void      UART2_ClearRx(void);                                                  // 清理接收到的数据 (清理最后一帧字节数,因为它是判断接收的标志)
// UART3                                                                        
void      UART3_Init(uint32_t ulBaudrate);                                      // 初始化串口3; GPIO引脚PB10+PB11、中断优先级、通信参数:波特率可设、8位数据、无校验、1个停止位
void      UART3_SendData(uint8_t *puData, uint16_t usNum);                      // 发送指定数据; 参数:数据地址、字节数
void      UART3_SendString(const char *pcString, ...);                          // 发送字符串;   参数:字符串地址; 使用方法如同printf
uint8_t   UART3_SendAT(char *pcAT, char *pcAckString, uint16_t usTimeOutMs);    // 本函数,针对ESP8266、蓝牙模块等AT固件,用于等待返回期待的信息; 参数:AT指令字符串、期待返回信息字符串、ms等待超时; 返回:0-执行失败、1-执行成功 
uint16_t  UART3_GetRxNum(void);                                                 // 获取接收到的最新一帧字节数
uint8_t * UART3_GetRxData(void);                                                // 获取接收到的数据 (缓存的地址)
void      UART3_ClearRx(void);                                                  // 清理接收到的数据 (清理最后一帧字节数,因为它是判断接收的标志)

#ifdef STM32F10X_HD
// UART4                                                            
void      UART4_Init(uint32_t ulBaudrate);                                      // 初始化串口4; GPIO引脚PC10+PC11、中断优先级、通信参数:波特率可设、8位数据、无校验、1个停止位
void      UART4_SendData(uint8_t *puData, uint16_t usNum);                      // 发送指定数据; 参数:数据地址、字节数
void      UART4_SendString(const char *pcString, ...);                          // 发送字符串;   参数:字符串地址; 使用方法如同printf
uint8_t   UART4_SendAT(char *pcAT, char *pcAckString, uint16_t usTimeOutMs);    // 本函数,针对ESP8266、蓝牙模块等AT固件,用于等待返回期待的信息; 参数:AT指令字符串、期待返回信息字符串、ms等待超时; 返回:0-执行失败、1-执行成功 
uint16_t  UART4_GetRxNum(void);                                                 // 获取接收到的最新一帧字节数
uint8_t * UART4_GetRxData(void);                                                // 获取接收到的数据 (缓存的地址)
void      UART4_ClearRx(void);                                                  // 清理接收到的数据 (清理最后一帧字节数,因为它是判断接收的标志)
// UART5                                                                        
void      UART5_Init(uint32_t ulBaudrate);                                      // 初始化串口5; GPIO引脚PC12+PD2、中断优先级、通信参数:波特率可设、8位数据、无校验、1个停止位
void      UART5_SendData(uint8_t *puData, uint16_t usNum);                      // 发送指定数据; 参数:数据地址、字节数
void      UART5_SendString(const char *pcString, ...);                          // 发送字符串;   参数:字符串地址; 使用方法如同printf
uint8_t   UART5_SendAT(char *pcAT, char *pcAckString, uint16_t usTimeOutMs);    // 本函数,针对ESP8266、蓝牙模块等AT固件,用于等待返回期待的信息; 参数:AT指令字符串、期待返回信息字符串、ms等待超时; 返回:0-执行失败、1-执行成功 
uint16_t  UART5_GetRxNum(void);                                                 // 获取接收到的最新一帧字节数
uint8_t * UART5_GetRxData(void);                                                // 获取接收到的数据 (缓存的地址)
void      UART5_ClearRx(void);                                                  // 清理接收到的数据 (清理最后一帧字节数,因为它是判断接收的标志)
#endif      

// 辅助函数:Modbus_CRC16校验
void      Modbus_AddCRC16(uint8_t *_pcData, uint16_t _usLen);                   // 对数据追加2字节的ModbusCRC16校验值到末尾; 参数:原始数据、原始数据字节数; 注意:调用函数后,原始数据会增加2字节
uint8_t   Modbus_CheckCRC16(uint8_t *_pcData, uint16_t _usLen);                 // 对带ModbusCRC16校验的数据段进行校验; 返回:0-错误、1-匹配正确;





#endif

// 文件结尾,需要保留至少1空行

2、bsp_UART.c

/***********************************************************************************************************************************
 ** 【代码编写】  魔女开发板团队
 ** 【代码更新】  2024-10-23-1
 ** 【淘    宝】  魔女开发板      https://demoboard.taobao.com
 ***********************************************************************************************************************************
 ** 【文件名称】  bsp_UART.c
 **
 ** 【文件功能】  各USART的GPIO配置、通信协议配置、中断配置,及功能函数实现
 **
 ** 【适用平台】  STM32F103 + keil5 + HAL库/标准库
 **
 ** 【特别说明】  1、为什么这个文件中,同时有标准库、HAL库的代码?
 **                  是为了方便移植!已做好预编译处理,可适用于标准库、HAL库的工程移植。
 **               2、为什么UART初始化、中断,用寄存器操作,而不是用更有可读性的HAL库?
 **                  因为CubeMX配置工程时,如果不对UART进行配置,且打勾只生成需要的文件,工程中将没有UART的HAL支持文件的。
 **                  另外, 中断里用寄存器操作,与HAL库的重重封装相比,明显地更高效!
************************************************************************************************************************************/
#include "bsp_UART.h"             // 引用头文件






/*****************************************************************************
 ** 声明本地变量
****************************************************************************/
typedef struct
{
    uint16_t  usRxNum;            // 新一帧数据,接收到多少个字节数据
    uint8_t  *puRxData;           // 新一帧数据,数据缓存; 存放的是空闲中断后,从临时接收缓存复制过来的完整数据,并非接收过程中的不完整数据;

    uint8_t  *puTxFiFoData;       // 发送缓冲区,环形队列; 为了方便理解阅读,没有封装成队列函数
    uint16_t  usTxFiFoData ;      // 环形缓冲区的队头
    uint16_t  usTxFiFoTail ;      // 环形缓冲区的队尾
} xUSATR_TypeDef;



#if 0
/******************************************************************************
 * 函  数: delay_us
 * 功  能: ms 延时函数
 * 备  注: 1、系统时钟72MHz
 *          2、打勾:Options/ c++ / One ELF Section per Function
            3、编译优化级别:Level 3(-O3)
 * 参  数: uint32_t  us  微秒值
 * 返回值: 无
 ******************************************************************************/
static volatile uint32_t ulTimesUS;    // 使用volatile声明,防止变量被编译器优化
static void delay_us(uint16_t us)
{
    ulTimesUS = us ;
    while (ulTimesUS)
        ulTimesUS--;                   // 操作外部变量,防止空循环被编译器优化掉
}
#endif



/******************************************************************************
 * 函  数: delay_ms
 * 功  能: ms 延时函数
 * 备  注: 1、系统时钟72MHz
 *          2、打勾:Options/ c++ / One ELF Section per Function
            3、编译优化级别:Level 3(-O3)
 * 参  数: uint32_t  ms  毫秒值
 * 返回值: 无
 ******************************************************************************/
static volatile uint32_t ulTimesMS;    // 使用volatile声明,防止变量被编译器优化
static void delay_ms(uint16_t ms)
{
    ulTimesMS = ms * 5500;
    while (ulTimesMS)
        ulTimesMS--;                   // 操作外部变量,防止空循环被编译器优化掉
}



//   USART-1   //
/
#if UART1_EN

static xUSATR_TypeDef  xUART1 = { 0 };                      // 定义 UART1 的收发结构体
static uint8_t uaUART1RxData[UART1_RX_BUF_SIZE];            // 定义 UART1 的接收缓存
static uint8_t uaUART1TxFiFoData[UART1_TX_BUF_SIZE];        // 定义 UART1 的发送缓存

/******************************************************************************
 * 函  数: UART1_Init
 * 功  能: 初始化USART1的通信引脚、协议参数、中断优先级
 *          引脚:TX-PA10、RX-PA11
 *          协议:波特率-None-8-1
 *          发送:发送中断
 *          接收:接收+空闲中断
 *
 * 参  数: uint32_t  ulBaudrate  通信波特率
 * 返回值: 无
 ******************************************************************************/
void UART1_Init(uint32_t ulBaudrate)
{
#ifdef USE_STDPERIPH_DRIVER
    // 使能 时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);                            // 使能GPIOA
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);                           // 使能USART1
    // 重置 UART
    RCC_APB2PeriphResetCmd(RCC_APB2Periph_USART1, ENABLE);                           // 使能重置
    RCC_APB2PeriphResetCmd(RCC_APB2Periph_USART1, DISABLE);                          // 取消重置
    // 配置 TX引脚 
    GPIO_InitTypeDef  GPIO_InitStructure = {0};                                      // GPIO 初始化结构体
    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_9;                                      // TX 引脚
    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_AF_PP;                                 // 引脚功能
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;                                // 输出速度
    GPIO_Init(GPIOA, &GPIO_InitStructure);                                           // 初始化引脚
    // 配置 RX引脚 
    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_10;                                     // RX 引脚
    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_IPU;                                   // RX引脚工作模式:上拉输入; 如果使用浮空输入,引脚空置时可能产生误输入; 当电路上为一主多从电路时,可以使用复用开漏模式
    GPIO_Init(GPIOA, &GPIO_InitStructure);                                           // 初始化引脚
    // 配置 USART 
    USART_DeInit(USART1);                                                            // 复位串口
    USART_InitTypeDef USART_InitStructure = {0};                                     // UART配置结构体
    USART_InitStructure.USART_BaudRate   = ulBaudrate;                               // 波特率
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;                      // 位长
    USART_InitStructure.USART_StopBits   = USART_StopBits_1;                         // 停止位
    USART_InitStructure.USART_Parity     = USART_Parity_No;                          // 奇偶校验
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;  // 硬件流控制
    USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;                  // 使能工作模式
    USART_Init(USART1, &USART_InitStructure);                                        // 初始化串口
    // 配置 中断
    USART_ITConfig(USART1, USART_IT_TXE, DISABLE);                                   // 发送中断 使能
    USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);                                   // 接收中断 使能
    USART_ITConfig(USART1, USART_IT_IDLE, ENABLE);                                   // 空闲中断 使能
    // 配置 中断优先级
    NVIC_InitTypeDef  NVIC_InitStructure = {0};                                      // 优先级配置结构体
    NVIC_InitStructure .NVIC_IRQChannel = USART1_IRQn;                               // 中断编号
    NVIC_InitStructure .NVIC_IRQChannelPreemptionPriority = 0 ;                      // 抢占优先级
    NVIC_InitStructure .NVIC_IRQChannelSubPriority = 0;                              // 子优先级
    NVIC_InitStructure .NVIC_IRQChannelCmd = ENABLE;                                 // IRQ通道使能
    NVIC_Init(&NVIC_InitStructure);                                                  // 初始化中断优先级
    // 配置完成,打开串口
    USART_Cmd(USART1, ENABLE);                                                       // 配置完成,打开串口, 开始工作
#endif

#ifdef USE_HAL_DRIVER
    // 使能 时钟
    __HAL_RCC_GPIOA_CLK_ENABLE();                               // 使能GPIOA
    __HAL_RCC_USART1_CLK_ENABLE();                              // 使能USART1
    // 重置 UART
    __HAL_RCC_USART1_FORCE_RESET();                             // 使能重置
    __HAL_RCC_USART1_RELEASE_RESET();                           // 取消重置
    // 配置 TX引脚
    GPIO_InitTypeDef    GPIO_InitStruct = {0};                  // 声明初始化要用到的结构体
    GPIO_InitStruct.Pin   = GPIO_PIN_9 ;                        // 引脚 TX-PA9
    GPIO_InitStruct.Mode  = GPIO_MODE_AF_PP;                    // 工作模式
    GPIO_InitStruct.Pull  = GPIO_NOPULL;                        // 上下拉
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;               // 引脚速率
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);                     // 初始化引脚工作模式
    // 配置 RX引脚
    GPIO_InitStruct.Pin   = GPIO_PIN_10;                        // 引脚 RX-PA10
    GPIO_InitStruct.Mode  = GPIO_MODE_INPUT;                    // 工作模式
    GPIO_InitStruct.Pull  = GPIO_PULLUP;                        // 上下拉
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;               // 引脚速率
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);                     // 初始化引脚工作模式
    // 计算波特率参数
    float    temp;
    uint16_t mantissa, fraction;
    SystemCoreClockUpdate();                                    // 更新系统运行频率全局值; 函数SystemCoreClock( ),在标准库、HAL库通用
    temp = (float)SystemCoreClock  / (ulBaudrate * 16);         // 波特率公式计算; USART1挂载在APB2, 时钟为系统时钟的1分频; 全局变量SystemCoreClock,在标准库、HAL库通用;
    mantissa = temp;				                            // 整数部分
    fraction = (temp - mantissa) * 16;                          // 小数部分
    USART1 -> BRR  = mantissa << 4 | fraction;                  // 设置波特率
    // 配置 UART
    USART1 -> CR1  =   0;                                       // 清0
    USART1 -> CR1 |=   0x01 << 2;                               // 接收使能[02]: 0=失能、1=使能
    USART1 -> CR1 |=   0x01 << 3;                               // 发送使能[03]:0=失能、1=使能
    USART1 -> CR1 |=   0x00 << 9;                               // 奇偶校验[09]:0=偶Even、1=奇Odd;  注意:使用奇偶校验,下面两项要置1
    USART1 -> CR1 |=   0x00 << 10;                              // 校验位  [10]:0=禁用、1=使能;     注意,使用奇偶校验,此位要置1 (否则无效、数据错乱)
    USART1 -> CR1 |=   0x00 << 12;                              // 数据位  [12]:0=8位、 1=9位;      注意:使用奇偶校验,此位要置1 (否则数据会发生错乱)
    USART1 -> CR2  =   0;                                       // 数据清0
    USART1 -> CR2 &= ~(0x03 << 12);                             // 停止位[13:12]:00b=1个停止位、01b=0.5个停止位、10b=2个停止位、11b=1.5个停止位
    USART1 -> CR3  =   0;                                       // 数据清0
    USART1 -> CR3 &= ~(0x01 << 6);                              // DMA接收[6]: 0=禁止、1=使能
    USART1 -> CR3 &= ~(0x01 << 7);                              // DMA发送[7]: 0=禁止、1=使能
    // 配置 中断
    USART1 -> CR1 &= ~(0x01 << 7);                              // 关闭发送中断
    USART1 -> CR1 |=   0x01 << 5;                               // 使能接收中断: 接收缓冲区非空
    USART1 -> CR1 |=   0x01 << 4;                               // 使能空闲中断:超过1字节时间没收到新数据
    USART1 -> SR   = ~(0x00F0);                                 // 清理中断标志
    // 配置 中断优先级
    HAL_NVIC_SetPriority(USART1_IRQn, 0, 0);                    // 设置指定中断的优先级; 参数:中断请求编号、抢占级、子优先级
    HAL_NVIC_EnableIRQ(USART1_IRQn);                            // 使能、启用指定的中断
    // 配置完成,打开串口
    USART1 -> CR1 |=   0x01 << 13;                              // 使能UART开始工作
#endif

    // 关联缓冲区
    xUART1.puRxData = uaUART1RxData;                            // 关联接收缓冲区的地址
    xUART1.puTxFiFoData = uaUART1TxFiFoData;                    // 关联发送缓冲区的地址

    // 输出提示
    printf("\r\r\r===========  STM32F103 外设 初始化报告 ===========\r");                     // 输出到串口助手
    SystemCoreClockUpdate();                                                                  // 更新一下系统运行频率变量
    printf("系统时钟频率             %d MHz\r", SystemCoreClock / 1000000);                   // 输出到串口助手
    printf("UART1 初始化配置         %d-None-8-1; 已完成初始化配置、收发配置\r", ulBaudrate); // 输出到串口助手
}

/******************************************************************************
 * 函  数: USART1_IRQHandler
 * 功  能: USART1的接收中断、空闲中断、发送中断
 * 参  数: 无
 * 返回值: 无
 * 备  注: 本函数,当产生中断事件时,由硬件调用。
 *          如果使用本文件代码,在工程文件的其它地方,要注释同名函数,否则冲突。
******************************************************************************/
void USART1_IRQHandler(void)
{
    static uint16_t cnt = 0;                                             // 接收字节数累计:每一帧数据已接收到的字节数
    static uint8_t  rxTemp[UART1_RX_BUF_SIZE];                           // 接收数据缓存数组:每新接收1个字节,先顺序存放到这里,当一帧接收完(发生空闲中断), 再转存到外部缓存:xUARTx.puRxData[ ]
                                                                         
    // 发送中断:用于把环形缓冲的数据,逐字节发出                        
    if ((USART1->SR & USART_SR_TXE) && (USART1->CR1 & USART_CR1_TXEIE))  // 检查发送寄存器空中断使能,且发送寄存器为空; TXE(发送数据寄存器空)、TXEIE(发送缓冲区空中断使能)
    {                                                                    
        USART1->DR = xUART1.puTxFiFoData[xUART1.usTxFiFoTail++];         // 从FIFO队列中取出一个数据,放入USART的发送寄存器(硬件会自动发出),然后将FIFO的尾指针递增,指向下一个要发送的数据
        if (xUART1.usTxFiFoTail == UART1_TX_BUF_SIZE)                    // 检查FIFO尾指针是否到达了FIFO队列的末尾
            xUART1.usTxFiFoTail = 0;                                     // 将尾指针重置为0,实现环形队列的功能
        if (xUART1.usTxFiFoTail == xUART1.usTxFiFoData)                  // 检查FIFO尾指针是否追上了头指针,即所有数据是否都已发送完毕
            USART1->CR1 &= ~USART_CR1_TXEIE;                             // 关闭发送寄存器空中断,防止中断服务程序被不必要地调用
        return;                                                          
    }                                                                    
                                                                         
    // 接收中断:用于逐个字节接收,存放到临时缓存                        
    if (USART1->SR & USART_SR_RXNE)                                      // 检查RXNE(读数据寄存器非空标志位); RXNE中断清理方法:读DR时自动清理;
    {                                                                    
        if (cnt == UART1_RX_BUF_SIZE)                                    // 当前帧已接收的字节量,已满缓存区的大小; 为避免溢出,本包后面接收到的数据直接舍弃;
        {
            printf("警告:UART1单帧接收量,已超出接收缓存大小!\r修改文件bsp_UART.h的UART1_RX_BUF_SIZE值,可解决此问题!\r");
            USART1->DR;                                                  // 读取数据寄存器的数据,但不保存.主要作用:读DR时自动清理接收中断标志;
            return;
        }
        rxTemp[cnt++] = USART1->DR ;                                     // 把新收到的字节数据,顺序存放到RXTemp数组中;注意:读取DR时自动清零中断位;
        return;
    }

    // 空闲中断:用于判断一帧数据结束,整理数据到外部备读
    if (USART1->SR & USART_SR_IDLE)                                      // 检查IDLE(空闲中断标志位); IDLE中断标志清理方法:序列清零,USART1 ->SR;  USART1 ->DR;
    {
        memcpy(xUART1.puRxData, rxTemp, UART1_RX_BUF_SIZE);              // 把本帧接收到的数据,存入到结构体的数组成员xUARTx.puRxData中, 等待处理; 注意:复制的是整个数组,包括0值,以方便字符串输出时尾部以0作字符串结束符
        xUART1.usRxNum  = cnt;                                           // 把接收到的字节数,存入到结构体变量xUARTx.usRxNum中;
        cnt = 0;                                                         // 接收字节数累计器,清零; 准备下一次的接收
        memset(rxTemp, 0, UART1_RX_BUF_SIZE);                            // 接收数据缓存数组,清零; 准备下一次的接收
        USART1 ->SR;
        USART1 ->DR;                                                     // 清零IDLE中断标志位!! 序列清零,顺序不能错!!
        return;
    }

    return;
}

/******************************************************************************
 * 函  数: UART1_SendData
 * 功  能: UART通过中断发送数据
 *         【适合场景】本函数可发送各种数据,而不限于字符串,如int,char
 *         【不 适 合】注意h文件中所定义的发缓冲区大小、注意数据压入缓冲区的速度与串口发送速度的冲突
 * 参  数: uint8_t*  puData   需发送数据的地址
 *          uint16_t  usNum    发送的字节数 ,数量受限于h文件中设置的发送缓存区大小宏定义
 * 返回值: 无
 ******************************************************************************/
void UART1_SendData(uint8_t *puData, uint16_t usNum)
{
    for (uint16_t i = 0; i < usNum; i++)                         // 把数据放入环形缓冲区
    {
        xUART1.puTxFiFoData[xUART1.usTxFiFoData++] = puData[i];  // 把字节放到缓冲区最后的位置,然后指针后移
        if (xUART1.usTxFiFoData == UART1_TX_BUF_SIZE)            // 如果指针位置到达缓冲区的最大值,则归0
            xUART1.usTxFiFoData = 0;
    }                                                            // 为了方便阅读理解,这里没有把此部分封装成队列函数,可以自行封装

    if ((USART1->CR1 & USART_CR1_TXEIE) == 0)                    // 检查USART寄存器的发送缓冲区空置中断(TXEIE)是否已打开
        USART1->CR1 |= USART_CR1_TXEIE;                          // 打开TXEIE中断
}

/******************************************************************************
 * 函  数: UART1_SendString
 * 功  能: 发送字符串
 *          用法请参考printf,及示例中的展示
 *          注意,最大发送字节数为512-1个字符,可在函数中修改上限
 * 参  数: const char *pcString, ...   (如同printf的用法)
 * 返回值: 无
 ******************************************************************************/
void UART1_SendString(const char *pcString, ...)
{
    char mBuffer[512] = {0};;                                     // 开辟一个缓存, 并把里面的数据置0
    va_list ap;                                                   // 新建一个可变参数列表
    va_start(ap, pcString);                                       // 列表指向第一个可变参数
    vsnprintf(mBuffer, 512, pcString, ap);                        // 把所有参数,按格式,输出到缓存; 参数2用于限制发送的最大字节数,如果达到上限,则只发送上限值-1; 最后1字节自动置'\0'
    va_end(ap);                                                   // 清空可变参数列表
    UART1_SendData((uint8_t *)mBuffer, strlen(mBuffer));          // 把字节存放环形缓冲,排队准备发送
}

/******************************************************************************
 * 函    数: UART1_SendAT
 * 功    能: 发送AT命令, 并等待返回信息
 * 参    数: char     *pcString      AT指令字符串
 *            char     *pcAckString   期待的指令返回信息字符串
 *            uint16_t  usTimeOut     发送命令后等待的时间,毫秒
 *
 * 返 回 值: 0-执行失败、1-执行正常
 ******************************************************************************/
uint8_t UART1_SendAT(char *pcAT, char *pcAckString, uint16_t usTimeOutMs)
{
    UART1_ClearRx();                                              // 清0
    UART1_SendString(pcAT);                                       // 发送AT指令字符串

    while (usTimeOutMs--)                                         // 判断是否起时(这里只作简单的循环判断次数处理)
    {
        if (UART1_GetRxNum())                                     // 判断是否接收到数据
        {
            UART1_ClearRx();                                      // 清0接收字节数; 注意:接收到的数据内容 ,是没有被清0的
            if (strstr((char *)UART1_GetRxData(), pcAckString))   // 判断返回数据中是否有期待的字符
                return 1;                                         // 返回:0-超时没有返回、1-正常返回期待值
        }
        delay_ms(1);                                              // 延时; 用于超时退出处理,避免死等
    }
    return 0;                                                     // 返回:0-超时、返回异常,1-正常返回期待值
}

/******************************************************************************
 * 函  数: UART1_SendStringForDMA
 * 功  能: UART通过DMA发送数据,省了占用中断的时间
 *         【适合场景】字符串,字节数非常多,
 *         【不 适 合】1:只适合发送字符串,不适合发送可能含0的数值类数据; 2-时间间隔要足够
 * 参  数: char strintTemp  要发送的字符串首地址
 * 返回值: 无
 * 备  注:  本函数为保留函数,留作用户参考。为了方便移植,本文件对外不再使用本函数。
 ******************************************************************************/
#if 0
void UART1_SendStringForDMA(char *stringTemp)
{
    static uint8_t Flag_DmaTxInit = 0;                // 用于标记是否已配置DMA发送
    uint32_t   num = 0;                               // 发送的数量,注意发送的单位不是必须8位的
    char *t = stringTemp ;                            // 用于配合计算发送的数量

    while (*t++ != 0)  num++;                         // 计算要发送的数目,这步比较耗时,测试发现每多6个字节,增加1us,单位:8位

    while (DMA1_Channel4->CNDTR > 0);                 // 重要:如果DMA还在进行上次发送,就等待; 得进完成中断清标志,F4不用这么麻烦,发送完后EN自动清零
    if (Flag_DmaTxInit == 0)                          // 是否已进行过USAART_TX的DMA传输配置
    {
        Flag_DmaTxInit  = 1;                          // 设置标记,下次调用本函数就不再进行配置了
        USART1 ->CR3   |= 1 << 7;                     // 使能DMA发送
        RCC->AHBENR    |= 1 << 0;                     // 开启DMA1时钟  [0]DMA1   [1]DMA2

        DMA1_Channel4->CCR   = 0;                     // 失能, 清0整个寄存器, DMA必须失能才能配置
        DMA1_Channel4->CNDTR = num;                   // 传输数据量
        DMA1_Channel4->CMAR  = (uint32_t)stringTemp;  // 存储器地址
        DMA1_Channel4->CPAR  = (uint32_t)&USART1->DR; // 外设地址

        DMA1_Channel4->CCR |= 1 << 4;                 // 数据传输方向   0:从外设读   1:从存储器读
        DMA1_Channel4->CCR |= 0 << 5;                 // 循环模式       0:不循环     1:循环
        DMA1_Channel4->CCR |= 0 << 6;                 // 外设地址非增量模式
        DMA1_Channel4->CCR |= 1 << 7;                 // 存储器增量模式
        DMA1_Channel4->CCR |= 0 << 8;                 // 外设数据宽度为8位
        DMA1_Channel4->CCR |= 0 << 10;                // 存储器数据宽度8位
        DMA1_Channel4->CCR |= 0 << 12;                // 中等优先级
        DMA1_Channel4->CCR |= 0 << 14;                // 非存储器到存储器模式
    }
    DMA1_Channel4->CCR  &= ~((uint32_t)(1 << 0));     // 失能,DMA必须失能才能配置
    DMA1_Channel4->CNDTR = num;                       // 传输数据量
    DMA1_Channel4->CMAR  = (uint32_t)stringTemp;      // 存储器地址
    DMA1_Channel4->CCR  |= 1 << 0;                    // 开启DMA传输
}
#endif

/******************************************************************************
 * 函  数: UART1_GetRxNum
 * 功  能: 获取最新一帧数据的字节数
 * 参  数: 无
 * 返回值: 0=没有接收到数据,非0=新一帧数据的字节数
 ******************************************************************************/
uint16_t UART1_GetRxNum(void)
{
    return xUART1.usRxNum ;
}

/******************************************************************************
 * 函  数: UART1_GetRxData
 * 功  能: 获取最新一帧数据 (数据的地址)
 * 参  数: 无
 * 返回值: 缓存地址(uint8_t*)
 ******************************************************************************/
uint8_t *UART1_GetRxData(void)
{
    return xUART1.puRxData ;
}

/******************************************************************************
 * 函  数: UART1_ClearRx
 * 功  能: 清理最后一帧数据的缓存
 *          主要是清0字节数,因为它是用来判断接收的标准
 * 参  数: 无
 * 返回值: 无
 ******************************************************************************/
void UART1_ClearRx(void)
{
    xUART1.usRxNum = 0 ;
}
#endif  // endif UART1_EN





//   USART-2   //
/
#if UART2_EN

static xUSATR_TypeDef  xUART2 = { 0 };                      // 定义 UART2 的收发结构体
static uint8_t uaUART2RxData[UART2_RX_BUF_SIZE];            // 定义 UART2 的接收缓存
static uint8_t uaUART2TxFiFoData[UART2_TX_BUF_SIZE];        // 定义 UART2 的发送缓存

/******************************************************************************
 * 函  数: UART2_Init
 * 功  能: 初始化USART2的通信引脚、协议参数、中断优先级
 *          引脚:TX-PA2、RX-PA3
 *          协议:波特率-None-8-1
 *          发送:发送中断
 *          接收:接收+空闲中断
 *
 * 参  数: uint32_t  ulBaudrate  通信波特率
 * 返回值: 无
 ******************************************************************************/
void UART2_Init(uint32_t ulBaudrate)
{
#ifdef USE_STDPERIPH_DRIVER
    // 使能 时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);                            // 使能GPIOA
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE);                           // 使能USART2
    // 重置 UART
    RCC_APB1PeriphResetCmd(RCC_APB1Periph_USART2, ENABLE);                           // 使能重置
    RCC_APB1PeriphResetCmd(RCC_APB1Periph_USART2, DISABLE);                          // 取消重置
    // 配置 TX引脚 
    GPIO_InitTypeDef  GPIO_InitStructure = {0};                                      // GPIO 初始化结构体
    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_2;                                      // TX 引脚
    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_AF_PP;                                 // 引脚功能
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;                                // 输出速度
    GPIO_Init(GPIOA, &GPIO_InitStructure);                                           // 初始化引脚
    // 配置 RX引脚 
    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_3;                                      // RX 引脚
    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_IPU;                                   // RX引脚工作模式:上拉输入; 如果使用浮空输入,引脚空置时可能产生误输入; 当电路上为一主多从电路时,可以使用复用开漏模式
    GPIO_Init(GPIOA, &GPIO_InitStructure);                                           // 初始化引脚
    // 配置 USART 
    USART_DeInit(USART2);                                                            // 复位串口
    USART_InitTypeDef USART_InitStructure = {0};                                     // UART配置结构体
    USART_InitStructure.USART_BaudRate   = ulBaudrate;                               // 波特率
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;                      // 位长
    USART_InitStructure.USART_StopBits   = USART_StopBits_1;                         // 停止位
    USART_InitStructure.USART_Parity     = USART_Parity_No;                          // 奇偶校验
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;  // 硬件流控制
    USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;                  // 使能工作模式
    USART_Init(USART2, &USART_InitStructure);                                        // 初始化串口
    // 配置 中断
    USART_ITConfig(USART2, USART_IT_TXE, DISABLE);                                   // 发送中断 使能
    USART_ITConfig(USART2, USART_IT_RXNE, ENABLE);                                   // 接收中断 使能
    USART_ITConfig(USART2, USART_IT_IDLE, ENABLE);                                   // 空闲中断 使能
    // 配置 中断优先级
    NVIC_InitTypeDef  NVIC_InitStructure = {0};                                      // 优先级配置结构体
    NVIC_InitStructure .NVIC_IRQChannel = USART2_IRQn;                               // 中断编号
    NVIC_InitStructure .NVIC_IRQChannelPreemptionPriority = 0 ;                      // 抢占优先级
    NVIC_InitStructure .NVIC_IRQChannelSubPriority = 0;                              // 子优先级
    NVIC_InitStructure .NVIC_IRQChannelCmd = ENABLE;                                 // IRQ通道使能
    NVIC_Init(&NVIC_InitStructure);                                                  // 初始化中断优先级
    // 配置完成,打开串口
    USART_Cmd(USART2, ENABLE);                                                       // 配置完成,打开串口, 开始工作
#endif

#ifdef USE_HAL_DRIVER                                           // HAL库 配置    
    // 使能 时钟
    __HAL_RCC_GPIOA_CLK_ENABLE();                               // 使能GPIOA
    __HAL_RCC_USART2_CLK_ENABLE();                              // 使能USART2
    // 重置 UART
    __HAL_RCC_USART2_FORCE_RESET();                             // 使能重置
    __HAL_RCC_USART2_RELEASE_RESET();                           // 取消重置
    // 配置 TX引脚
    GPIO_InitTypeDef    GPIO_InitStruct = {0};                  // 声明初始化要用到的结构体
    GPIO_InitStruct.Pin   = GPIO_PIN_2 ;                        // 引脚 TX-PA2
    GPIO_InitStruct.Mode  = GPIO_MODE_AF_PP;                    // 工作模式
    GPIO_InitStruct.Pull  = GPIO_NOPULL;                        // 上下拉
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;               // 引脚速率
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);                     // 初始化引脚工作模式
    // RX引脚配置
    GPIO_InitStruct.Pin   =  GPIO_PIN_3;                        // 引脚 RX-PA3
    GPIO_InitStruct.Mode  = GPIO_MODE_INPUT;                    // 工作模式
    GPIO_InitStruct.Pull  = GPIO_PULLUP;                        // 上下拉
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;               // 引脚速率
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);                     // 初始化引脚工作模式
    // 计算波特率参数
    float    temp;
    uint16_t mantissa, fraction;
    SystemCoreClockUpdate();                                    // 更新系统运行频率全局值; 函数SystemCoreClock( ),在标准库、HAL库通用
    temp = (float)(SystemCoreClock / 2) / (ulBaudrate * 16);    // 波特率公式计算; USART2挂载在APB1, 时钟为系统时钟的2分频; 全局变量SystemCoreClock,在标准库、HAL库通用;
    mantissa = temp;				                            // 整数部分
    fraction = (temp - mantissa) * 16;                          // 小数部分
    USART2 -> BRR  = mantissa << 4 | fraction;                  // 设置波特率
    // 配置 UART
    USART2 -> CR1  =   0;                                       // 清0
    USART2 -> CR1 |=   0x01 << 2;                               // 接收使能[02]: 0=失能、1=使能
    USART2 -> CR1 |=   0x01 << 3;                               // 发送使能[03]:0=失能、1=使能
    USART2 -> CR1 |=   0x00 << 9;                               // 奇偶校验[09]:0=偶Even、1=奇Odd;  注意:使用奇偶校验,下面两项要置1
    USART2 -> CR1 |=   0x00 << 10;                              // 校验位  [10]:0=禁用、1=使能;     注意,使用奇偶校验,此位要置1 (否则无效、数据错乱)
    USART2 -> CR1 |=   0x00 << 12;                              // 数据位  [12]:0=8位、 1=9位;      注意:使用奇偶校验,此位要置1 (否则数据会发生错乱)
    USART2 -> CR2  =   0;                                       // 数据清0
    USART2 -> CR2 &= ~(0x03 << 12);                             // 停止位[13:12]:00b=1个停止位、01b=0.5个停止位、10b=2个停止位、11b=1.5个停止位
    USART2 -> CR3  =   0;                                       // 数据清0
    USART2 -> CR3 &= ~(0x01 << 6);                              // DMA接收[6]: 0=禁止、1=使能
    USART2 -> CR3 &= ~(0x01 << 7);                              // DMA发送[7]: 0=禁止、1=使能
    // 配置 中断
    USART2 -> CR1 &= ~(0x01 << 7);                              // 关闭发送中断
    USART2 -> CR1 |=   0x01 << 5;                               // 使能接收中断: 接收缓冲区非空
    USART2 -> CR1 |=   0x01 << 4;                               // 使能空闲中断:超过1字节时间没收到新数据
    USART2 -> SR   = ~(0x00F0);                                 // 清理中断标志
    // 配置 中断优先级
    HAL_NVIC_SetPriority(USART2_IRQn, 0, 0);                    // 设置指定中断的优先级; 参数:中断请求编号、抢占级、子优先级
    HAL_NVIC_EnableIRQ(USART2_IRQn);                            // 使能、启用指定的中断
    // 开启USART2
    USART2 -> CR1 |=   0x01 << 13;                              // 使能UART开始工作
#endif

    // 关联缓冲区
    xUART2.puRxData = uaUART2RxData;                            // 获取接收缓冲区的地址
    xUART2.puTxFiFoData = uaUART2TxFiFoData;                    // 获取发送缓冲区的地址
    // 输出提示
    printf("UART2 初始化配置         %d-None-8-1; 已完成初始化配置、收发配置\r", ulBaudrate);
}

/******************************************************************************
 * 函  数: USART2_IRQHandler
 * 功  能: USART2的接收中断、空闲中断、发送中断
 * 参  数: 无
 * 返回值: 无
 * 备  注: 本函数,当产生中断事件时,由硬件调用。
 *          如果使用本文件代码,在工程文件的其它地方,要注释同名函数,否则冲突。
 ******************************************************************************/
void USART2_IRQHandler(void)
{
    static uint16_t cnt = 0;                                             // 接收字节数累计:每一帧数据已接收到的字节数
    static uint8_t  rxTemp[UART2_RX_BUF_SIZE];                           // 接收数据缓存数组:每新接收1个字节,先顺序存放到这里,当一帧接收完(发生空闲中断), 再转存到外部缓存:xUARTx.puRxData[ ]
                                                                         
    // 发送中断:用于把环形缓冲的数据,逐字节发出                        
    if ((USART2->SR & USART_SR_TXE) && (USART2->CR1 & USART_CR1_TXEIE))  // 检查发送寄存器空中断使能,且发送寄存器为空; TXE(发送数据寄存器空)、TXEIE(发送缓冲区空中断使能)
    {                                                                    
        USART2->DR = xUART2.puTxFiFoData[xUART2.usTxFiFoTail++];         // 从FIFO队列中取出一个数据,放入USART的发送寄存器(硬件会自动发出),然后将FIFO的尾指针递增,指向下一个要发送的数据
        if (xUART2.usTxFiFoTail == UART2_TX_BUF_SIZE)                    // 检查FIFO尾指针是否到达了FIFO队列的末尾
            xUART2.usTxFiFoTail = 0;                                     // 将尾指针重置为0,实现环形队列的功能
        if (xUART2.usTxFiFoTail == xUART2.usTxFiFoData)                  // 检查FIFO尾指针是否追上了头指针,即所有数据是否都已发送完毕
            USART2->CR1 &= ~USART_CR1_TXEIE;                             // 关闭发送寄存器空中断,防止中断服务程序被不必要地调用
        return;                                                          
    }                                                                    
                                                                         
    // 接收中断:用于逐个字节接收,存放到临时缓存                        
    if (USART2->SR & USART_SR_RXNE)                                      // 检查RXNE(读数据寄存器非空标志位); RXNE中断清理方法:读DR时自动清理;
    {                                                                    
        if (cnt == UART2_RX_BUF_SIZE)                                    // 当前帧已接收的字节量,已满缓存区的大小; 为避免溢出,本包后面接收到的数据直接舍弃;
        {
            printf("警告:UART2单帧接收量,已超出接收缓存大小!\r修改文件bsp_UART.h的UART2_RX_BUF_SIZE值,可解决此问题!\r");
            USART2->DR;                                                  // 读取数据寄存器的数据,但不保存.主要作用:读DR时自动清理接收中断标志;
            return;
        }
        rxTemp[cnt++] = USART2->DR ;                                     // 把新收到的字节数据,顺序存放到RXTemp数组中;注意:读取DR时自动清零中断位;
        return;
    }

    // 空闲中断:用于判断一帧数据结束,整理数据到外部备读
    if (USART2->SR & USART_SR_IDLE)                                      // 检查IDLE(空闲中断标志位); IDLE中断标志清理方法:序列清零,USART1 ->SR;  USART1 ->DR;
    {
        memcpy(xUART2.puRxData, rxTemp, UART2_RX_BUF_SIZE);              // 把本帧接收到的数据,存入到结构体的数组成员xUARTx.puRxData中, 等待处理; 注意:复制的是整个数组,包括0值,以方便字符串输出时尾部以0作字符串结束符
        xUART2.usRxNum  = cnt;                                           // 把接收到的字节数,存入到结构体变量xUARTx.usRxNum中;
        cnt = 0;                                                         // 接收字节数累计器,清零; 准备下一次的接收
        memset(rxTemp, 0, UART2_RX_BUF_SIZE);                            // 接收数据缓存数组,清零; 准备下一次的接收
        USART2 ->SR;
        USART2 ->DR;                                                     // 清零IDLE中断标志位!! 序列清零,顺序不能错!!
        return;
    }

    return;
}

/******************************************************************************
 * 函  数: UART2_SendData
 * 功  能: UART通过中断发送数据
 *         【适合场景】本函数可发送各种数据,而不限于字符串,如int,char
 *         【不 适 合】注意h文件中所定义的发缓冲区大小、注意数据压入缓冲区的速度与串口发送速度的冲突
 * 参  数: uint8_t* puData     需发送数据的地址
 *          uint8_t  usNum      发送的字节数 ,数量受限于h文件中设置的发送缓存区大小宏定义
 * 返回值: 无
 ******************************************************************************/
void UART2_SendData(uint8_t *puData, uint16_t usNum)
{
    for (uint16_t i = 0; i < usNum; i++)                         // 把数据放入环形缓冲区
    {
        xUART2.puTxFiFoData[xUART2.usTxFiFoData++] = puData[i];  // 把字节放到缓冲区最后的位置,然后指针后移
        if (xUART2.usTxFiFoData == UART2_TX_BUF_SIZE)            // 如果指针位置到达缓冲区的最大值,则归0
            xUART2.usTxFiFoData = 0;
    }

    if ((USART2->CR1 & USART_CR1_TXEIE) == 0)                    // 检查USART寄存器的发送缓冲区空置中断(TXEIE)是否已打开
        USART2->CR1 |= USART_CR1_TXEIE;                          // 打开TXEIE中断
}

/******************************************************************************
 * 函  数: UART2_SendString
 * 功  能: 发送字符串
 *          用法请参考printf,及示例中的展示
 *          注意,最大发送字节数为512-1个字符,可在函数中修改上限
 * 参  数: const char *pcString, ...   (如同printf的用法)
 * 返回值: 无
 ******************************************************************************/
void UART2_SendString(const char *pcString, ...)
{
    char mBuffer[512] = {0};;                               // 开辟一个缓存, 并把里面的数据置0
    va_list ap;                                             // 新建一个可变参数列表
    va_start(ap, pcString);                                 // 列表指向第一个可变参数
    vsnprintf(mBuffer, 512, pcString, ap);                  // 把所有参数,按格式,输出到缓存; 参数2用于限制发送的最大字节数,如果达到上限,则只发送上限值-1; 最后1字节自动置'\0'
    va_end(ap);                                             // 清空可变参数列表
    UART2_SendData((uint8_t *)mBuffer, strlen(mBuffer));    // 把字节存放环形缓冲,排队准备发送
}

/******************************************************************************
 * 函    数: UART2_SendAT
 * 功    能: 发送AT命令, 并等待返回信息
 * 参    数: char     *pcString      AT指令字符串
 *            char     *pcAckString   期待的指令返回信息字符串
 *            uint16_t  usTimeOut     发送命令后等待的时间,毫秒
 *
 * 返 回 值: 0-执行失败、1-执行正常
 ******************************************************************************/
uint8_t UART2_SendAT(char *pcAT, char *pcAckString, uint16_t usTimeOutMs)
{
    UART2_ClearRx();                                              // 清0
    UART2_SendString(pcAT);                                       // 发送AT指令字符串

    while (usTimeOutMs--)                                         // 判断是否起时(这里只作简单的循环判断次数处理)
    {
        if (UART2_GetRxNum())                                     // 判断是否接收到数据
        {
            UART2_ClearRx();                                      // 清0接收字节数; 注意:接收到的数据内容 ,是没有被清0的
            if (strstr((char *)UART2_GetRxData(), pcAckString))   // 判断返回数据中是否有期待的字符
                return 1;                                         // 返回:0-超时没有返回、1-正常返回期待值
        }
        delay_ms(1);                                              // 延时; 用于超时退出处理,避免死等
    }
    return 0;                                                     // 返回:0-超时、返回异常,1-正常返回期待值
}

/******************************************************************************
 * 函  数: UART2_GetRxNum
 * 功  能: 获取最新一帧数据的字节数
 * 参  数: 无
 * 返回值: 0=没有接收到数据,非0=新一帧数据的字节数
 ******************************************************************************/
uint16_t UART2_GetRxNum(void)
{
    return xUART2.usRxNum ;
}

/******************************************************************************
 * 函  数: UART2_GetRxData
 * 功  能: 获取最新一帧数据 (数据的地址)
 * 参  数: 无
 * 返回值: 数据的地址(uint8_t*)
 ******************************************************************************/
uint8_t *UART2_GetRxData(void)
{
    return xUART2.puRxData ;
}

/******************************************************************************
 * 函  数: UART2_ClearRx
 * 功  能: 清理最后一帧数据的缓存
 *          主要是清0字节数,因为它是用来判断接收的标准
 * 参  数: 无
 * 返回值: 无
 ******************************************************************************/
void UART2_ClearRx(void)
{
    xUART2.usRxNum = 0 ;
}
#endif  // endif UART2_EN





//   USART-3   //
/
#if UART3_EN

static xUSATR_TypeDef  xUART3 = { 0 };                      // 定义 UART3 的收发结构体
static uint8_t uaUart3RxData[UART3_RX_BUF_SIZE];            // 定义 UART3 的接收缓存
static uint8_t uaUart3TxFiFoData[UART3_TX_BUF_SIZE];        // 定义 UART3 的发送缓存

/******************************************************************************
 * 函  数: UART3_Init
 * 功  能: 初始化USART3的通信引脚、协议参数、中断优先级
 *          引脚:TX-PB10、RX-PB11
 *          协议:波特率-None-8-1
 *          发送:发送中断
 *          接收:接收+空闲中断
 *
 * 参  数: uint32_t ulBaudrate  通信波特率
 * 返回值: 无
 ******************************************************************************/
void UART3_Init(uint32_t ulBaudrate)
{
#ifdef USE_STDPERIPH_DRIVER
    // 使能 时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);                            // 使能GPIOB
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3, ENABLE);                           // 使能UART3
    // 重置 UART
    RCC_APB1PeriphResetCmd(RCC_APB1Periph_USART3, ENABLE);                           // 使能重置
    RCC_APB1PeriphResetCmd(RCC_APB1Periph_USART3, DISABLE);                          // 取消重置
    // 配置 TX引脚 
    GPIO_InitTypeDef  GPIO_InitStructure = {0};                                      // GPIO 初始化结构体
    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_10;                                     // TX 引脚
    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_AF_PP;                                 // 引脚功能
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;                                // 输出速度
    GPIO_Init(GPIOB, &GPIO_InitStructure);                                           // 初始化引脚
    // 配置 RX引脚 
    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_11;                                     // RX 引脚
    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_IPU;                                   // RX引脚工作模式:上拉输入; 如果使用浮空输入,引脚空置时可能产生误输入; 当电路上为一主多从电路时,可以使用复用开漏模式
    GPIO_Init(GPIOB, &GPIO_InitStructure);                                           // 初始化引脚
    // 配置 USART 
    USART_DeInit(USART3);                                                            // 复位串口
    USART_InitTypeDef USART_InitStructure = {0};                                     // UART配置结构体
    USART_InitStructure.USART_BaudRate   = ulBaudrate;                               // 波特率
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;                      // 位长
    USART_InitStructure.USART_StopBits   = USART_StopBits_1;                         // 停止位
    USART_InitStructure.USART_Parity     = USART_Parity_No;                          // 奇偶校验
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;  // 硬件流控制
    USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;                  // 使能工作模式
    USART_Init(USART3, &USART_InitStructure);                                        // 初始化串口
    // 配置 中断
    USART_ITConfig(USART3, USART_IT_TXE, DISABLE);                                   // 发送中断 使能
    USART_ITConfig(USART3, USART_IT_RXNE, ENABLE);                                   // 接收中断 使能
    USART_ITConfig(USART3, USART_IT_IDLE, ENABLE);                                   // 空闲中断 使能
    // 配置 中断优先级
    NVIC_InitTypeDef  NVIC_InitStructure = {0};                                      // 优先级配置结构体
    NVIC_InitStructure .NVIC_IRQChannel = USART3_IRQn;                               // 中断编号
    NVIC_InitStructure .NVIC_IRQChannelPreemptionPriority = 0 ;                      // 抢占优先级
    NVIC_InitStructure .NVIC_IRQChannelSubPriority = 0;                              // 子优先级
    NVIC_InitStructure .NVIC_IRQChannelCmd = ENABLE;                                 // IRQ通道使能
    NVIC_Init(&NVIC_InitStructure);                                                  // 初始化中断优先级
    // 配置完成,打开串口
    USART_Cmd(USART3, ENABLE);                                                       // 配置完成,打开串口, 开始工作
#endif

#ifdef USE_HAL_DRIVER
    // 使能 时钟
    __HAL_RCC_GPIOB_CLK_ENABLE();                               // 使能GPIOB
    __HAL_RCC_USART3_CLK_ENABLE();                              // 使能USART3
    // 重置 UART
    __HAL_RCC_USART3_FORCE_RESET();                             // 使能重置
    __HAL_RCC_USART3_RELEASE_RESET();                           // 取消重置
    // 配置 TX引脚
    GPIO_InitTypeDef    GPIO_InitStruct = {0};                  // 声明初始化要用到的结构体
    GPIO_InitStruct.Pin   = GPIO_PIN_10 ;                       // 引脚 TX-PB10
    GPIO_InitStruct.Mode  = GPIO_MODE_AF_PP;                    // 工作模式
    GPIO_InitStruct.Pull  = GPIO_NOPULL;                        // 上下拉
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;               // 引脚速率
    HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);                     // 初始化引脚工作模式
    // 配置 RX引脚
    GPIO_InitStruct.Pin   =  GPIO_PIN_11;                       // 引脚 RX-PB11
    GPIO_InitStruct.Mode  = GPIO_MODE_INPUT;                    // 工作模式
    GPIO_InitStruct.Pull  = GPIO_PULLUP;                        // 上下拉
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;               // 引脚速率
    HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);                     // 初始化引脚工作模式
    // 计算波特率参数
    float    temp;
    uint16_t mantissa, fraction;
    SystemCoreClockUpdate();                                    // 更新系统运行频率全局值; 函数SystemCoreClock( ),在标准库、HAL库通用
    temp = (float)(SystemCoreClock / 2) / (ulBaudrate * 16);    // 波特率公式计算; USART3挂载在APB1, 时钟为系统时钟的2分频; 全局变量SystemCoreClock,在标准库、HAL库通用;
    mantissa = temp;				                            // 整数部分
    fraction = (temp - mantissa) * 16;                          // 小数部分
    USART3 -> BRR  = mantissa << 4 | fraction;                  // 设置波特率
    // 配置 UART
    USART3 -> CR1  =   0;                                       // 清0
    USART3 -> CR1 |=   0x01 << 2;                               // 接收使能[02]: 0=失能、1=使能
    USART3 -> CR1 |=   0x01 << 3;                               // 发送使能[03]:0=失能、1=使能
    USART3 -> CR1 |=   0x00 << 9;                               // 奇偶校验[09]:0=偶Even、1=奇Odd;  注意:使用奇偶校验,下面两项要置1
    USART3 -> CR1 |=   0x00 << 10;                              // 校验位  [10]:0=禁用、1=使能;     注意,使用奇偶校验,此位要置1 (否则无效、数据错乱)
    USART3 -> CR1 |=   0x00 << 12;                              // 数据位  [12]:0=8位、 1=9位;      注意:使用奇偶校验,此位要置1 (否则数据会发生错乱)
    USART3 -> CR2  =   0;                                       // 数据清0
    USART3 -> CR2 &= ~(0x03 << 12);                             // 停止位[13:12]:00b=1个停止位、01b=0.5个停止位、10b=2个停止位、11b=1.5个停止位
    USART3 -> CR3  =   0;                                       // 数据清0
    USART3 -> CR3 &= ~(0x01 << 6);                              // DMA接收[6]: 0=禁止、1=使能
    USART3 -> CR3 &= ~(0x01 << 7);                              // DMA发送[7]: 0=禁止、1=使能
    // 配置 中断
    USART3 -> CR1 &= ~(0x01 << 7);                              // 关闭发送中断
    USART3 -> CR1 |=   0x01 << 5;                               // 使能接收中断: 接收缓冲区非空
    USART3 -> CR1 |=   0x01 << 4;                               // 使能空闲中断:超过1字节时间没收到新数据
    USART3 -> SR   = ~(0x00F0);                                 // 清理中断标志
    // 配置 中断优先级
    HAL_NVIC_SetPriority(USART3_IRQn, 0, 0);                    // 设置指定中断的优先级; 参数:中断请求编号、抢占级、子优先级
    HAL_NVIC_EnableIRQ(USART3_IRQn);                            // 使能、启用指定的中断
    // 配置完成,打开串口
    USART3 -> CR1 |=   0x01 << 13;                              // 使能UART开始工作
#endif

    // 关联缓冲区
    xUART3.puRxData = uaUart3RxData;                            // 获取接收缓冲区的地址
    xUART3.puTxFiFoData = uaUart3TxFiFoData;                    // 获取发送缓冲区的地址
    // 输出提示
    printf("UART3 初始化配置         %d-None-8-1; 已完成初始化配置、收发配置\r", ulBaudrate);
}

/******************************************************************************
 * 函  数: USART3_IRQHandler
 * 功  能: USART3的接收中断、空闲中断、发送中断
 * 参  数: 无
 * 返回值: 无
 * 备  注: 本函数,当产生中断事件时,由硬件调用。
 *          如果使用本文件代码,在工程文件的其它地方,要注释同名函数,否则冲突。
 ******************************************************************************/
void USART3_IRQHandler(void)
{
    static uint16_t cnt = 0;                                             // 接收字节数累计:每一帧数据已接收到的字节数
    static uint8_t  rxTemp[UART3_RX_BUF_SIZE];                           // 接收数据缓存数组:每新接收1个字节,先顺序存放到这里,当一帧接收完(发生空闲中断), 再转存到外部缓存:xUARTx.puRxData[ ]
                                                                         
    // 发送中断:用于把环形缓冲的数据,逐字节发出                        
    if ((USART3->SR & USART_SR_TXE) && (USART3->CR1 & USART_CR1_TXEIE))  // 检查发送寄存器空中断使能,且发送寄存器为空; TXE(发送数据寄存器空)、TXEIE(发送缓冲区空中断使能)
    {                                                                    
        USART3->DR = xUART3.puTxFiFoData[xUART3.usTxFiFoTail++];         // 从FIFO队列中取出一个数据,放入USART的发送寄存器(硬件会自动发出),然后将FIFO的尾指针递增,指向下一个要发送的数据
        if (xUART3.usTxFiFoTail == UART3_TX_BUF_SIZE)                    // 检查FIFO尾指针是否到达了FIFO队列的末尾
            xUART3.usTxFiFoTail = 0;                                     // 将尾指针重置为0,实现环形队列的功能
        if (xUART3.usTxFiFoTail == xUART3.usTxFiFoData)                  // 检查FIFO尾指针是否追上了头指针,即所有数据是否都已发送完毕
            USART3->CR1 &= ~USART_CR1_TXEIE;                             // 关闭发送寄存器空中断,防止中断服务程序被不必要地调用
        return;                                                          
    }                                                                    
                                                                         
    // 接收中断:用于逐个字节接收,存放到临时缓存                        
    if (USART3->SR & USART_SR_RXNE)                                      // 检查RXNE(读数据寄存器非空标志位); RXNE中断清理方法:读DR时自动清理;
    {                                                                    
        if (cnt == UART3_RX_BUF_SIZE)                                    // 当前帧已接收的字节量,已满缓存区的大小; 为避免溢出,本包后面接收到的数据直接舍弃;
        {
            printf("警告:UART3单帧接收量,已超出接收缓存大小!\r修改文件bsp_UART.h的UART3_RX_BUF_SIZE值,可解决此问题!\r");
            USART3->DR;                                                  // 读取数据寄存器的数据,但不保存.主要作用:读DR时自动清理接收中断标志;
            return;
        }
        rxTemp[cnt++] = USART3->DR ;                                     // 把新收到的字节数据,顺序存放到RXTemp数组中;注意:读取DR时自动清零中断位
        return;
    }

    // 空闲中断:用于判断一帧数据结束,整理数据到外部备读
    if (USART3->SR & USART_SR_IDLE)                                      // 检查IDLE(空闲中断标志位); IDLE中断标志清理方法:序列清零,USART1 ->SR;  USART1 ->DR;
    {
        memcpy(xUART3.puRxData, rxTemp, UART3_RX_BUF_SIZE);              // 把本帧接收到的数据,存入到结构体的数组成员xUARTx.puRxData中, 等待处理; 注意:复制的是整个数组,包括0值,以方便字符串输出时尾部以0作字符串结束符
        xUART3.usRxNum  = cnt;                                           // 把接收到的字节数,存入到结构体变量xUARTx.usRxNum中;
        cnt = 0;                                                         // 接收字节数累计器,清零; 准备下一次的接收
        memset(rxTemp, 0, UART3_RX_BUF_SIZE);                            // 接收数据缓存数组,清零; 准备下一次的接收
        USART3 ->SR;
        USART3 ->DR;                                                     // 清零IDLE中断标志位!! 序列清零,顺序不能错!!
        return;
    }

    return;
}

/******************************************************************************
 * 函  数: UART3_SendData
 * 功  能: UART通过中断发送数据
 *         【适合场景】本函数可发送各种数据,而不限于字符串,如int,char
 *         【不 适 合】注意h文件中所定义的发缓冲区大小、注意数据压入缓冲区的速度与串口发送速度的冲突
 * 参  数: uint8_t* puData   需发送数据的地址
 *          uint8_t  usNum      发送的字节数 ,数量受限于h文件中设置的发送缓存区大小宏定义
 * 返回值: 无
 ******************************************************************************/
void UART3_SendData(uint8_t *puData, uint16_t usNum)
{
    for (uint16_t i = 0; i < usNum; i++)                           // 把数据放入环形缓冲区
    {
        xUART3.puTxFiFoData[xUART3.usTxFiFoData++] = puData[i];    // 把字节放到缓冲区最后的位置,然后指针后移
        if (xUART3.usTxFiFoData == UART3_TX_BUF_SIZE)              // 如果指针位置到达缓冲区的最大值,则归0
            xUART3.usTxFiFoData = 0;
    }

    if ((USART3->CR1 & USART_CR1_TXEIE) == 0)                      // 检查USART寄存器的发送缓冲区空置中断(TXEIE)是否已打开
        USART3->CR1 |= USART_CR1_TXEIE;                            // 打开TXEIE中断
}

/******************************************************************************
 * 函  数: UART3_SendString
 * 功  能: 发送字符串
 *          用法请参考printf,及示例中的展示
 *          注意,最大发送字节数为512-1个字符,可在函数中修改上限
 * 参  数: const char *pcString, ...   (如同printf的用法)
 * 返回值: 无
 ******************************************************************************/
void UART3_SendString(const char *pcString, ...)
{
    char mBuffer[512] = {0};;                                // 开辟一个缓存, 并把里面的数据置0
    va_list ap;                                              // 新建一个可变参数列表
    va_start(ap, pcString);                                  // 列表指向第一个可变参数
    vsnprintf(mBuffer, 512, pcString, ap);                   // 把所有参数,按格式,输出到缓存; 参数2用于限制发送的最大字节数,如果达到上限,则只发送上限值-1; 最后1字节自动置'\0'
    va_end(ap);                                              // 清空可变参数列表
    UART3_SendData((uint8_t *)mBuffer, strlen(mBuffer));     // 把字节存放环形缓冲,排队准备发送
}

/******************************************************************************
 * 函    数: UART3_SendAT
 * 功    能: 发送AT命令, 并等待返回信息
 * 参    数: char     *pcString      AT指令字符串
 *            char     *pcAckString   期待的指令返回信息字符串
 *            uint16_t  usTimeOut     发送命令后等待的时间,毫秒
 *
 * 返 回 值: 0-执行失败、1-执行正常
 ******************************************************************************/
uint8_t UART3_SendAT(char *pcAT, char *pcAckString, uint16_t usTimeOutMs)
{
    UART3_ClearRx();                                              // 清0
    UART3_SendString(pcAT);                                       // 发送AT指令字符串

    while (usTimeOutMs--)                                         // 判断是否起时(这里只作简单的循环判断次数处理)
    {
        if (UART3_GetRxNum())                                     // 判断是否接收到数据
        {
            UART3_ClearRx();                                      // 清0接收字节数; 注意:接收到的数据内容 ,是没有被清0的
            if (strstr((char *)UART3_GetRxData(), pcAckString))   // 判断返回数据中是否有期待的字符
                return 1;                                         // 返回:0-超时没有返回、1-正常返回期待值
        }
        delay_ms(1);                                              // 延时; 用于超时退出处理,避免死等
    }
    return 0;                                                     // 返回:0-超时、返回异常,1-正常返回期待值
}

/******************************************************************************
 * 函  数: UART3_GetRxNum
 * 功  能: 获取最新一帧数据的字节数
 * 参  数: 无
 * 返回值: 0=没有接收到数据,非0=新一帧数据的字节数
 ******************************************************************************/
uint16_t UART3_GetRxNum(void)
{
    return xUART3.usRxNum ;
}

/******************************************************************************
 * 函  数: UART3_GetRxData
 * 功  能: 获取最新一帧数据 (数据的地址)
 * 参  数: 无
 * 返回值: 数据的地址(uint8_t*)
 ******************************************************************************/
uint8_t *UART3_GetRxData(void)
{
    return xUART3.puRxData ;
}

/******************************************************************************
 * 函  数: UART3_ClearRx
 * 功  能: 清理最后一帧数据的缓存
 *          主要是清0字节数,因为它是用来判断接收的标准
 * 参  数: 无
 * 返回值: 无
 ******************************************************************************/
void UART3_ClearRx(void)
{
    xUART3.usRxNum = 0 ;
}
#endif  // endif UART3_EN




#ifdef STM32F10X_HD
//   UART-4   //
/
#if UART4_EN

static xUSATR_TypeDef  xUART4 = { 0 };                      // 定义 UART4 的收发结构体
static uint8_t uaUart4RxData[UART4_RX_BUF_SIZE];            // 定义 UART4 的接收缓存
static uint8_t uaUart4TxFiFoData[UART4_TX_BUF_SIZE];        // 定义 UART4 的发送缓存

/******************************************************************************
 * 函  数: UART4_Init
 * 功  能: 初始化UART4的通信引脚、协议参数、中断优先级
 *          引脚:TX-PC10、RX-PC11
 *          协议:波特率-None-8-1
 *          发送:发送中断
 *          接收:接收+空闲中断
 *
 * 参  数: uint32_t ulBaudrate  通信波特率
 * 返回值: 无
 ******************************************************************************/
void UART4_Init(uint32_t ulBaudrate)
{
#ifdef USE_STDPERIPH_DRIVER
    // 使能 时钟  
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);                            // 使能GPIOC
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_UART4, ENABLE);                            // 使能UART4
    // 重置 UART
    RCC_APB1PeriphResetCmd(RCC_APB1Periph_UART4, ENABLE);                            // 使能重置
    RCC_APB1PeriphResetCmd(RCC_APB1Periph_UART4, DISABLE);                           // 取消重置
    // 配置 TX引脚 
    GPIO_InitTypeDef  GPIO_InitStructure = {0};                                      // GPIO 初始化结构体
    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_10;                                     // TX 引脚
    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_AF_PP;                                 // 引脚功能
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;                                // 输出速度
    GPIO_Init(GPIOC, &GPIO_InitStructure);                                           // 初始化引脚
    // 配置 RX引脚 
    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_11;                                     // RX 引脚
    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_IPU;                                   // RX引脚工作模式:上拉输入; 如果使用浮空输入,引脚空置时可能产生误输入; 当电路上为一主多从电路时,可以使用复用开漏模式
    GPIO_Init(GPIOC, &GPIO_InitStructure);                                           // 初始化引脚
    // 配置 USART 
    USART_DeInit(UART4);                                                             // 复位串口
    USART_InitTypeDef USART_InitStructure = {0};                                     // UART配置结构体
    USART_InitStructure.USART_BaudRate   = ulBaudrate;                               // 波特率
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;                      // 位长
    USART_InitStructure.USART_StopBits   = USART_StopBits_1;                         // 停止位
    USART_InitStructure.USART_Parity     = USART_Parity_No;                          // 奇偶校验
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;  // 硬件流控制
    USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;                  // 使能工作模式
    USART_Init(UART4, &USART_InitStructure);                                         // 初始化串口
    // 配置 中断
    USART_ITConfig(UART4, USART_IT_TXE, DISABLE);                                    // 发送中断 使能
    USART_ITConfig(UART4, USART_IT_RXNE, ENABLE);                                    // 接收中断 使能
    USART_ITConfig(UART4, USART_IT_IDLE, ENABLE);                                    // 空闲中断 使能
    // 配置 中断优先级
    NVIC_InitTypeDef  NVIC_InitStructure = {0};                                      // 优先级配置结构体
    NVIC_InitStructure .NVIC_IRQChannel = UART4_IRQn;                                // 中断编号
    NVIC_InitStructure .NVIC_IRQChannelPreemptionPriority = 0 ;                      // 抢占优先级
    NVIC_InitStructure .NVIC_IRQChannelSubPriority = 0;                              // 子优先级
    NVIC_InitStructure .NVIC_IRQChannelCmd = ENABLE;                                 // IRQ通道使能
    NVIC_Init(&NVIC_InitStructure);                                                  // 初始化中断优先级
    // 配置完成,打开串口
    USART_Cmd(UART4, ENABLE);                                                        // 配置完成,打开串口, 开始工作
#endif

#ifdef USE_HAL_DRIVER
    // 使能 时钟                                            
    __HAL_RCC_GPIOC_CLK_ENABLE();                               // 使能GPIOC
    __HAL_RCC_UART4_CLK_ENABLE();                               // 使能UART4
    // 重置 UART                                          
    __HAL_RCC_UART4_FORCE_RESET();                              // 使能重置
    __HAL_RCC_UART4_RELEASE_RESET();                            // 取消重置
    // 配置 TX引脚
    GPIO_InitTypeDef    GPIO_InitStruct = {0};                  // 声明初始化要用到的结构体
    GPIO_InitStruct.Pin   = GPIO_PIN_10 ;                       // 引脚 TX-PC10
    GPIO_InitStruct.Mode  = GPIO_MODE_AF_PP;                    // 工作模式
    GPIO_InitStruct.Pull  = GPIO_NOPULL;                        // 上下拉
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;               // 引脚速率
    HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);                     // 初始化引脚工作模式
    // 配置 RX引脚
    GPIO_InitStruct.Pin   =  GPIO_PIN_11;                       // 引脚 RX-PC11
    GPIO_InitStruct.Mode  = GPIO_MODE_INPUT;                    // 工作模式
    GPIO_InitStruct.Pull  = GPIO_PULLUP;                        // 上下拉
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;               // 引脚速率
    HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);                     // 初始化引脚工作模式
    // 计算波特率参数
    float    temp;
    uint16_t mantissa, fraction;
    SystemCoreClockUpdate();                                    // 更新系统运行频率全局值; 函数SystemCoreClock( ),在标准库、HAL库通用
    temp = (float)(SystemCoreClock / 2) / (ulBaudrate * 16);    // 波特率公式计算; UART4挂载在APB1, 时钟为系统时钟的2分频; 全局变量SystemCoreClock,在标准库、HAL库通用;
    mantissa = temp;				                            // 整数部分
    fraction = (temp - mantissa) * 16;                          // 小数部分
    UART4 -> BRR  = mantissa << 4 | fraction;                   // 设置波特率
    // 配置 UART
    UART4 -> CR1  =   0;                                        // 清0
    UART4 -> CR1 |=   0x01 << 2;                                // 接收使能[02]: 0=失能、1=使能
    UART4 -> CR1 |=   0x01 << 3;                                // 发送使能[03]:0=失能、1=使能
    UART4 -> CR1 |=   0x00 << 9;                                // 奇偶校验[09]:0=偶Even、1=奇Odd;  注意:使用奇偶校验,下面两项要置1
    UART4 -> CR1 |=   0x00 << 10;                               // 校验位  [10]:0=禁用、1=使能;     注意,使用奇偶校验,此位要置1 (否则无效、数据错乱)
    UART4 -> CR1 |=   0x00 << 12;                               // 数据位  [12]:0=8位、 1=9位;      注意:使用奇偶校验,此位要置1 (否则数据会发生错乱)
    UART4 -> CR2  =   0;                                        // 数据清0
    UART4 -> CR2 &= ~(0x03 << 12);                              // 停止位[13:12]:00b=1个停止位、01b=0.5个停止位、10b=2个停止位、11b=1.5个停止位
    UART4 -> CR3  =   0;                                        // 数据清0
    UART4 -> CR3 &= ~(0x01 << 6);                               // DMA接收[6]: 0=禁止、1=使能
    UART4 -> CR3 &= ~(0x01 << 7);                               // DMA发送[7]: 0=禁止、1=使能
    // 配置 中断
    UART4 -> CR1 &= ~(0x01 << 7);                               // 关闭发送中断
    UART4 -> CR1 |=   0x01 << 5;                                // 使能接收中断: 接收缓冲区非空
    UART4 -> CR1 |=   0x01 << 4;                                // 使能空闲中断:超过1字节时间没收到新数据
    UART4 -> SR   = ~(0x00F0);                                  // 清理中断标志
    // 配置 中断优先级
    HAL_NVIC_SetPriority(UART4_IRQn, 0, 0);                     // 设置指定中断的优先级; 参数:中断请求编号、抢占级、子优先级
    HAL_NVIC_EnableIRQ(UART4_IRQn);                             // 使能、启用指定的中断
    // 配置完成,打开串口
    UART4 -> CR1 |=   0x01 << 13;                               // 使能UART开始工作
#endif

    // 关联缓冲区
    xUART4.puRxData = uaUart4RxData;                            // 获取接收缓冲区的地址
    xUART4.puTxFiFoData = uaUart4TxFiFoData;                    // 获取发送缓冲区的地址
    // 输出提示
    printf("UART4 初始化配置         %d-None-8-1; 已完成初始化配置、收发配置\r", ulBaudrate);
}

/******************************************************************************
 * 函  数: UART4_IRQHandler
 * 功  能: UART4的中断处理函数
 *          接收中断、空闲中断、发送中断
 * 参  数: 无
 * 返回值: 无
 * 备  注: 本函数,当产生中断事件时,由硬件调用。
 *          如果使用本文件代码,在工程文件的其它地方,要注释同名函数,否则冲突。
 ******************************************************************************/
void UART4_IRQHandler(void)
{
    static uint16_t cnt = 0;                                           // 接收字节数累计:每一帧数据已接收到的字节数
    static uint8_t  rxTemp[UART4_RX_BUF_SIZE];                         // 接收数据缓存数组:每新接收1个字节,先顺序存放到这里,当一帧接收完(发生空闲中断), 再转存到外部缓存:xUARTx.puRxData[ ]
                                                                       
    // 发送中断:用于把环形缓冲的数据,逐字节发出                      
    if ((UART4->SR & USART_SR_TXE) && (UART4->CR1 & USART_CR1_TXEIE))  // 检查发送寄存器空中断使能,且发送寄存器为空; TXE(发送数据寄存器空)、TXEIE(发送缓冲区空中断使能)
    {                                                                  
        UART4->DR = xUART4.puTxFiFoData[xUART4.usTxFiFoTail++];        // 从FIFO队列中取出一个数据,放入USART的发送寄存器(硬件会自动发出),然后将FIFO的尾指针递增,指向下一个要发送的数据
        if (xUART4.usTxFiFoTail == UART4_TX_BUF_SIZE)                  // 检查FIFO尾指针是否到达了FIFO队列的末尾
            xUART4.usTxFiFoTail = 0;                                   // 将尾指针重置为0,实现环形队列的功能
        if (xUART4.usTxFiFoTail == xUART4.usTxFiFoData)                // 检查FIFO尾指针是否追上了头指针,即所有数据是否都已发送完毕
            UART4->CR1 &= ~USART_CR1_TXEIE;                            // 关闭发送寄存器空中断,防止中断服务程序被不必要地调用
        return;                                                        
    }                                                                  
                                                                       
    // 接收中断:用于逐个字节接收,存放到临时缓存                      
    if (UART4->SR & USART_SR_RXNE)                                     // 检查RXNE(读数据寄存器非空标志位); RXNE中断清理方法:读DR时自动清理;
    {                                                                  
        if (cnt == UART4_RX_BUF_SIZE)                                  // 当前帧已接收的字节量,已满缓存区的大小; 为避免溢出,本包后面接收到的数据直接舍弃;
        {           
            printf("警告:UART4单帧接收量,已超出接收缓存大小!\r修改文件bsp_UART.h的UART4_RX_BUF_SIZE值,可解决此问题!\r");
            UART4->DR;                                                 // 读取数据寄存器的数据,但不保存.主要作用:读DR时自动清理接收中断标志;
            return;                                                    
        }                                                              
        rxTemp[cnt++] = UART4->DR ;                                    // 把新收到的字节数据,顺序存放到RXTemp数组中;注意:读取DR时自动清零中断位
        return;                                                        
    }                                                                  
                                                                       
    // 空闲中断:用于判断一帧数据结束,整理数据到外部备读              
    if (UART4->SR & USART_SR_IDLE)                                     // 检查IDLE(空闲中断标志位); IDLE中断标志清理方法:序列清零,USART1 ->SR;  USART1 ->DR;
    {                                                                  
        memcpy(xUART4.puRxData, rxTemp, UART4_RX_BUF_SIZE);            // 把本帧接收到的数据,存入到结构体的数组成员xUARTx.puRxData中, 等待处理; 注意:复制的是整个数组,包括0值,以方便字符串输出时尾部以0作字符串结束符
        xUART4.usRxNum  = cnt;                                         // 把接收到的字节数,存入到结构体变量xUARTx.usRxNum中;
        cnt = 0;                                                       // 接收字节数累计器,清零; 准备下一次的接收
        memset(rxTemp, 0, UART4_RX_BUF_SIZE);                          // 接收数据缓存数组,清零; 准备下一次的接收
        UART4 ->SR;                                                    
        UART4 ->DR;                                                    // 清零IDLE中断标志位!! 序列清零,顺序不能错!!
        return;                                                        
    }

    return;
}

/******************************************************************************
 * 函  数: UART4_SendData
 * 功  能: UART通过中断发送数据
 *         【适合场景】本函数可发送各种数据,而不限于字符串,如int,char
 *         【不 适 合】注意h文件中所定义的发缓冲区大小、注意数据压入缓冲区的速度与串口发送速度的冲突
 * 参  数: uint8_t* puData   需发送数据的地址
 *          uint8_t  usNum    发送的字节数 ,数量受限于h文件中设置的发送缓存区大小宏定义
 * 返回值: 无
 ******************************************************************************/
void UART4_SendData(uint8_t *puData, uint16_t usNum)
{
    for (uint16_t i = 0; i < usNum; i++)                         // 把数据放入环形缓冲区
    {
        xUART4.puTxFiFoData[xUART4.usTxFiFoData++] = puData[i];  // 把字节放到缓冲区最后的位置,然后指针后移
        if (xUART4.usTxFiFoData == UART4_TX_BUF_SIZE)            // 如果指针位置到达缓冲区的最大值,则归0
            xUART4.usTxFiFoData = 0;
    }

    if ((UART4->CR1 & USART_CR1_TXEIE) == 0)                     // 检查USART寄存器的发送缓冲区空置中断(TXEIE)是否已打开
        UART4->CR1 |= USART_CR1_TXEIE;                           // 打开TXEIE中断
}

/******************************************************************************
 * 函  数: UART4_SendString
 * 功  能: 发送字符串
 *          用法请参考printf,及示例中的展示
 *          注意,最大发送字节数为512-1个字符,可在函数中修改上限
 * 参  数: const char *pcString, ...   (如同printf的用法)
 * 返回值: 无
 ******************************************************************************/
void UART4_SendString(const char *pcString, ...)
{
    char mBuffer[512] = {0};;                                     // 开辟一个缓存, 并把里面的数据置0
    va_list ap;                                                   // 新建一个可变参数列表
    va_start(ap, pcString);                                       // 列表指向第一个可变参数
    vsnprintf(mBuffer, 512, pcString, ap);                        // 把所有参数,按格式,输出到缓存; 参数2用于限制发送的最大字节数,如果达到上限,则只发送上限值-1; 最后1字节自动置'\0'
    va_end(ap);                                                   // 清空可变参数列表
    UART4_SendData((uint8_t *)mBuffer, strlen(mBuffer));          // 把字节存放环形缓冲,排队准备发送
}

/******************************************************************************
 * 函    数: UART4_SendAT
 * 功    能: 发送AT命令, 并等待返回信息
 * 参    数: char     *pcString      AT指令字符串
 *            char     *pcAckString   期待的指令返回信息字符串
 *            uint16_t  usTimeOut     发送命令后等待的时间,毫秒
 *
 * 返 回 值: 0-执行失败、1-执行正常
 ******************************************************************************/
uint8_t UART4_SendAT(char *pcAT, char *pcAckString, uint16_t usTimeOutMs)
{
    UART4_ClearRx();                                              // 清0
    UART4_SendString(pcAT);                                       // 发送AT指令字符串

    while (usTimeOutMs--)                                         // 判断是否起时(这里只作简单的循环判断次数处理)
    {
        if (UART4_GetRxNum())                                     // 判断是否接收到数据
        {
            UART4_ClearRx();                                      // 清0接收字节数; 注意:接收到的数据内容 ,是没有被清0的
            if (strstr((char *)UART4_GetRxData(), pcAckString))   // 判断返回数据中是否有期待的字符
                return 1;                                         // 返回:0-超时没有返回、1-正常返回期待值
        }
        delay_ms(1);                                              // 延时; 用于超时退出处理,避免死等
    }
    return 0;                                                     // 返回:0-超时、返回异常,1-正常返回期待值
}

/******************************************************************************
 * 函  数: UART4_GetRxNum
 * 功  能: 获取最新一帧数据的字节数
 * 参  数: 无
 * 返回值: 0=没有接收到数据,非0=新一帧数据的字节数
 ******************************************************************************/
uint16_t UART4_GetRxNum(void)
{
    return xUART4.usRxNum ;
}

/******************************************************************************
 * 函  数: UART4_GetRxData
 * 功  能: 获取最新一帧数据 (数据的地址)
 * 参  数: 无
 * 返回值: 数据的地址(uint8_t*)
 ******************************************************************************/
uint8_t *UART4_GetRxData(void)
{
    return xUART4.puRxData ;
}

/******************************************************************************
 * 函  数: UART4_ClearRx
 * 功  能: 清理最后一帧数据的缓存
 *          主要是清0字节数,因为它是用来判断接收的标准
 * 参  数: 无
 * 返回值: 无
 ******************************************************************************/
void UART4_ClearRx(void)
{
    xUART4.usRxNum = 0 ;
}
#endif  // endif UART4_EN





//   UART-5   //
/
#if UART5_EN

static xUSATR_TypeDef  xUART5 = { 0 };                      // 定义 UART5 的收发结构体
static uint8_t uaUart5RxData[UART5_RX_BUF_SIZE];            // 定义 UART5 的接收缓存
static uint8_t uaUart5TxFiFoData[UART5_TX_BUF_SIZE];        // 定义 UART5 的发送缓存

/******************************************************************************
 * 函  数: UART5_Init
 * 功  能: 初始化UART5的通信引脚、协议参数、中断优先级
 *          引脚:TX-PC12、RX-PD2
 *          协议:波特率-None-8-1
 *          发送:发送中断
 *          接收:接收+空闲中断
 *
 * 参  数: uint32_t ulBaudrate  通信波特率
 * 返回值: 无
 ******************************************************************************/
void UART5_Init(uint32_t ulBaudrate)
{
#ifdef USE_STDPERIPH_DRIVER
    // 使能 时钟  
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);                            // 使能GPIOC
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD, ENABLE);                            // 使能GPIOD
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_UART5, ENABLE);                            // 使能UART5
    // 重置 UART
    RCC_APB1PeriphResetCmd(RCC_APB1Periph_UART5, ENABLE);                            // 使能重置
    RCC_APB1PeriphResetCmd(RCC_APB1Periph_UART5, DISABLE);                           // 取消重置
    // 配置 TX引脚 
    GPIO_InitTypeDef  GPIO_InitStructure = {0};                                      // GPIO 初始化结构体
    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_12;                                     // TX 引脚
    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_AF_PP;                                 // 引脚功能
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;                                // 输出速度
    GPIO_Init(GPIOC, &GPIO_InitStructure);                                           // 初始化引脚
    // 配置 RX引脚 
    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_2;                                      // RX 引脚
    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_IPU;                                   // RX引脚工作模式:上拉输入; 如果使用浮空输入,引脚空置时可能产生误输入; 当电路上为一主多从电路时,可以使用复用开漏模式
    GPIO_Init(GPIOD, &GPIO_InitStructure);                                           // 初始化引脚
    // 配置 USART 
    USART_DeInit(UART5);                                                             // 复位串口
    USART_InitTypeDef USART_InitStructure = {0};                                     // UART配置结构体
    USART_InitStructure.USART_BaudRate   = ulBaudrate;                               // 波特率
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;                      // 位长
    USART_InitStructure.USART_StopBits   = USART_StopBits_1;                         // 停止位
    USART_InitStructure.USART_Parity     = USART_Parity_No;                          // 奇偶校验
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;  // 硬件流控制
    USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;                  // 使能工作模式
    USART_Init(UART5, &USART_InitStructure);                                         // 初始化串口
    // 配置 中断
    USART_ITConfig(UART5, USART_IT_TXE, DISABLE);                                    // 发送中断 使能
    USART_ITConfig(UART5, USART_IT_RXNE, ENABLE);                                    // 接收中断 使能
    USART_ITConfig(UART5, USART_IT_IDLE, ENABLE);                                    // 空闲中断 使能
    // 配置 中断优先级
    NVIC_InitTypeDef  NVIC_InitStructure = {0};                                      // 优先级配置结构体
    NVIC_InitStructure .NVIC_IRQChannel = UART5_IRQn;                                // 中断编号
    NVIC_InitStructure .NVIC_IRQChannelPreemptionPriority = 0 ;                      // 抢占优先级
    NVIC_InitStructure .NVIC_IRQChannelSubPriority = 0;                              // 子优先级
    NVIC_InitStructure .NVIC_IRQChannelCmd = ENABLE;                                 // IRQ通道使能
    NVIC_Init(&NVIC_InitStructure);                                                  // 初始化中断优先级
    // 配置完成,打开串口
    USART_Cmd(UART5, ENABLE);                                                        // 配置完成,打开串口, 开始工作
#endif

#ifdef USE_HAL_DRIVER
    // 使能 时钟                                            
    __HAL_RCC_GPIOC_CLK_ENABLE();                               // 使能GPIOC 
    __HAL_RCC_GPIOD_CLK_ENABLE();                               // 使能GPIOD 
    __HAL_RCC_UART5_CLK_ENABLE();                               // 使能UART5
    // 重置 UART                                        
    __HAL_RCC_UART5_FORCE_RESET();                              // 使能重置
    __HAL_RCC_UART5_RELEASE_RESET();                            // 取消重置
    // 配置 TX引脚
    GPIO_InitTypeDef    GPIO_InitStruct = {0};                  // 声明初始化要用到的结构体
    GPIO_InitStruct.Pin   = GPIO_PIN_12 ;                       // 引脚 TX-PC12
    GPIO_InitStruct.Mode  = GPIO_MODE_AF_PP;                    // 工作模式
    GPIO_InitStruct.Pull  = GPIO_NOPULL;                        // 上下拉
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;               // 引脚速率
    HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);                     // 初始化引脚工作模式
    // 配置 RX引脚
    GPIO_InitStruct.Pin   =  GPIO_PIN_2;                        // 引脚 RX-PD2
    GPIO_InitStruct.Mode  = GPIO_MODE_INPUT;                    // 工作模式
    GPIO_InitStruct.Pull  = GPIO_PULLUP;                        // 上下拉
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;               // 引脚速率
    HAL_GPIO_Init(GPIOD, &GPIO_InitStruct);                     // 初始化引脚工作模式
    // 计算波特率参数
    float    temp;
    uint16_t mantissa, fraction;
    SystemCoreClockUpdate();                                    // 更新系统运行频率全局值; 函数SystemCoreClock( ),在标准库、HAL库通用
    temp = (float)(SystemCoreClock / 2) / (ulBaudrate * 16);    // 波特率公式计算; UART5挂载在APB1, 时钟为系统时钟的2分频; 全局变量SystemCoreClock,在标准库、HAL库通用;
    mantissa = temp;				                            // 整数部分
    fraction = (temp - mantissa) * 16;                          // 小数部分
    UART5 -> BRR  = mantissa << 4 | fraction;                   // 设置波特率
    // 配置 UART
    UART5 -> CR1  =   0;                                        // 清0
    UART5 -> CR1 |=   0x01 << 2;                                // 接收使能[02]: 0=失能、1=使能
    UART5 -> CR1 |=   0x01 << 3;                                // 发送使能[03]:0=失能、1=使能
    UART5 -> CR1 |=   0x00 << 9;                                // 奇偶校验[09]:0=偶Even、1=奇Odd;  注意:使用奇偶校验,下面两项要置1
    UART5 -> CR1 |=   0x00 << 10;                               // 校验位  [10]:0=禁用、1=使能;     注意,使用奇偶校验,此位要置1 (否则无效、数据错乱)
    UART5 -> CR1 |=   0x00 << 12;                               // 数据位  [12]:0=8位、 1=9位;      注意:使用奇偶校验,此位要置1 (否则数据会发生错乱)
    UART5 -> CR2  =   0;                                        // 数据清0
    UART5 -> CR2 &= ~(0x03 << 12);                              // 停止位[13:12]:00b=1个停止位、01b=0.5个停止位、10b=2个停止位、11b=1.5个停止位
    UART5 -> CR3  =   0;                                        // 数据清0
    UART5 -> CR3 &= ~(0x01 << 6);                               // DMA接收[6]: 0=禁止、1=使能
    UART5 -> CR3 &= ~(0x01 << 7);                               // DMA发送[7]: 0=禁止、1=使能
    // 配置 中断
    UART5 -> CR1 &= ~(0x01 << 7);                               // 关闭发送中断
    UART5 -> CR1 |=   0x01 << 5;                                // 使能接收中断: 接收缓冲区非空
    UART5 -> CR1 |=   0x01 << 4;                                // 使能空闲中断:超过1字节时间没收到新数据
    UART5 -> SR   = ~(0x00F0);                                  // 清理中断标志
    // 配置 中断优先级
    HAL_NVIC_SetPriority(UART5_IRQn, 0, 0);                     // 设置指定中断的优先级; 参数:中断请求编号、抢占级、子优先级
    HAL_NVIC_EnableIRQ(UART5_IRQn);                             // 使能、启用指定的中断
    // 配置完成,打开串口
    UART5 -> CR1 |=   0x01 << 13;                               // 使能UART开始工作
#endif

    // 关联缓冲区
    xUART5.puRxData = uaUart5RxData;                            // 获取接收缓冲区的地址
    xUART5.puTxFiFoData = uaUart5TxFiFoData;                    // 获取发送缓冲区的地址
    // 输出提示
    printf("UART5 初始化配置         %d-None-8-1; 已完成初始化配置、收发配置\r", ulBaudrate);
}

/******************************************************************************
 * 函  数: UART5_IRQHandler
 * 功  能: UART5的接收中断、空闲中断、发送中断
 * 参  数: 无
 * 返回值: 无
 * 备  注: 本函数,当产生中断事件时,由硬件调用。
 *          如果使用本文件代码,在工程文件的其它地方,要注释同名函数,否则冲突。
 ******************************************************************************/
void UART5_IRQHandler(void)
{
    static uint16_t cnt = 0;                                           // 接收字节数累计:每一帧数据已接收到的字节数
    static uint8_t  rxTemp[UART5_RX_BUF_SIZE];                         // 接收数据缓存数组:每新接收1个字节,先顺序存放到这里,当一帧接收完(发生空闲中断), 再转存到外部缓存:xUARTx.puRxData[ ]
                                                                       
    // 发送中断:用于把环形缓冲的数据,逐字节发出                      
    if ((UART5->SR & USART_SR_TXE) && (UART5->CR1 & USART_CR1_TXEIE))  // 检查发送寄存器空中断使能,且发送寄存器为空; TXE(发送数据寄存器空)、TXEIE(发送缓冲区空中断使能)
    {                                                                  
        UART5->DR = xUART5.puTxFiFoData[xUART5.usTxFiFoTail++];        // 从FIFO队列中取出一个数据,放入USART的发送寄存器(硬件会自动发出),然后将FIFO的尾指针递增,指向下一个要发送的数据
        if (xUART5.usTxFiFoTail == UART5_TX_BUF_SIZE)                  // 检查FIFO尾指针是否到达了FIFO队列的末尾
            xUART5.usTxFiFoTail = 0;                                   // 将尾指针重置为0,实现环形队列的功能
        if (xUART5.usTxFiFoTail == xUART5.usTxFiFoData)                // 检查FIFO尾指针是否追上了头指针,即所有数据是否都已发送完毕
            UART5->CR1 &= ~USART_CR1_TXEIE;                            // 关闭发送寄存器空中断,防止中断服务程序被不必要地调用
        return;                                                        
    }                                                                  
                                                                       
    // 接收中断:用于逐个字节接收,存放到临时缓存                      
    if (UART5->SR & USART_SR_RXNE)                                     // 检查RXNE(读数据寄存器非空标志位); RXNE中断清理方法:读DR时自动清理;
    {                                                                  
        if (cnt == UART5_RX_BUF_SIZE)                                  // 当前帧已接收的字节量,已满缓存区的大小; 为避免溢出,本包后面接收到的数据直接舍弃;
        {
            printf("警告:UART5单帧接收量,已超出接收缓存大小!\r修改文件bsp_UART.h的UART5_RX_BUF_SIZE值,可解决此问题!\r");
            UART5->DR;                                                 // 读取数据寄存器的数据,但不保存.主要作用:读DR时自动清理接收中断标志;
            return;                                                   
        }                                                             
        rxTemp[cnt++] = UART5->DR ;                                    // 把新收到的字节数据,顺序存放到RXTemp数组中;注意:读取DR时自动清零中断位
        return;                                                       
    }                                                                 
                                                                      
    // 空闲中断:用于判断一帧数据结束,整理数据到外部备读             
    if (UART5->SR & USART_SR_IDLE)                                     // 检查IDLE(空闲中断标志位); IDLE中断标志清理方法:序列清零,USART1 ->SR;  USART1 ->DR;
    {                                                                 
        memcpy(xUART5.puRxData, rxTemp, UART5_RX_BUF_SIZE);            // 把本帧接收到的数据,存入到结构体的数组成员xUARTx.puRxData中, 等待处理; 注意:复制的是整个数组,包括0值,以方便字符串输出时尾部以0作字符串结束符
        xUART5.usRxNum  = cnt;                                         // 把接收到的字节数,存入到结构体变量xUARTx.usRxNum中;
        cnt = 0;                                                       // 接收字节数累计器,清零; 准备下一次的接收
        memset(rxTemp, 0, UART5_RX_BUF_SIZE);                          // 接收数据缓存数组,清零; 准备下一次的接收
        UART5 -> SR;                                                   
        UART5 -> DR;                                                   // 清零IDLE中断标志位!! 序列清零,顺序不能错!!
        return;                                                       
    }

    return;
}

/******************************************************************************
 * 函  数: UART5_SendData
 * 功  能: UART通过中断发送数据
 *         【适合场景】本函数可发送各种数据,而不限于字符串,如int,char
 *         【不 适 合】注意h文件中所定义的发缓冲区大小、注意数据压入缓冲区的速度与串口发送速度的冲突
 * 参  数: uint8_t* pudata     需发送数据的地址
 *          uint8_t  usNum      发送的字节数 ,数量受限于h文件中设置的发送缓存区大小宏定义
 * 返回值: 无
 ******************************************************************************/
void UART5_SendData(uint8_t *pudata, uint16_t usNum)
{
    for (uint16_t i = 0; i < usNum; i++)                         // 把数据放入环形缓冲区
    {
        xUART5.puTxFiFoData[xUART5.usTxFiFoData++] = pudata[i];  // 把字节放到缓冲区最后的位置,然后指针后移
        if (xUART5.usTxFiFoData == UART5_TX_BUF_SIZE)            // 如果指针位置到达缓冲区的最大值,则归0
            xUART5.usTxFiFoData = 0;
    }
                                                                
    if ((UART5->CR1 & USART_CR1_TXEIE) == 0)                     // 检查USART寄存器的发送缓冲区空置中断(TXEIE)是否已打开
        UART5->CR1 |= USART_CR1_TXEIE;                           // 打开TXEIE中断
}

/******************************************************************************
 * 函  数: UART5_SendString
 * 功  能: 发送字符串
 *          用法请参考printf,及示例中的展示
 *          注意,最大发送字节数为512-1个字符,可在函数中修改上限
 * 参  数: const char *pcString, ...   (如同printf的用法)
 * 返回值: 无
 ******************************************************************************/
void UART5_SendString(const char *pcString, ...)
{
    char mBuffer[512] = {0};;                                    // 开辟一个缓存, 并把里面的数据置0
    va_list ap;                                                  // 新建一个可变参数列表
    va_start(ap, pcString);                                      // 列表指向第一个可变参数
    vsnprintf(mBuffer, 512, pcString, ap);                       // 把所有参数,按格式,输出到缓存; 参数2用于限制发送的最大字节数,如果达到上限,则只发送上限值-1; 最后1字节自动置'\0'
    va_end(ap);                                                  // 清空可变参数列表
    UART5_SendData((uint8_t *)mBuffer, strlen(mBuffer));         // 把字节存放环形缓冲,排队准备发送
}

/******************************************************************************
 * 函    数: UART5_SendAT
 * 功    能: 发送AT命令, 并等待返回信息
 * 参    数: char     *pcString      AT指令字符串
 *            char     *pcAckString   期待的指令返回信息字符串
 *            uint16_t  usTimeOut     发送命令后等待的时间,毫秒
 *
 * 返 回 值: 0-执行失败、1-执行正常
 ******************************************************************************/
uint8_t UART5_SendAT(char *pcAT, char *pcAckString, uint16_t usTimeOutMs)
{
    UART5_ClearRx();                                              // 清0
    UART5_SendString(pcAT);                                       // 发送AT指令字符串

    while (usTimeOutMs--)                                         // 判断是否起时(这里只作简单的循环判断次数处理)
    {
        if (UART5_GetRxNum())                                     // 判断是否接收到数据
        {
            UART5_ClearRx();                                      // 清0接收字节数; 注意:接收到的数据内容 ,是没有被清0的
            if (strstr((char *)UART5_GetRxData(), pcAckString))   // 判断返回数据中是否有期待的字符
                return 1;                                         // 返回:0-超时没有返回、1-正常返回期待值
        }
        delay_ms(1);                                              // 延时; 用于超时退出处理,避免死等
    }
    return 0;                                                     // 返回:0-超时、返回异常,1-正常返回期待值
}

/******************************************************************************
 * 函  数: UART5_GetRxNum
 * 功  能: 获取最新一帧数据的字节数
 * 参  数: 无
 * 返回值: 0=没有接收到数据,非0=新一帧数据的字节数
 ******************************************************************************/
uint16_t UART5_GetRxNum(void)
{
    return xUART5.usRxNum ;
}

/******************************************************************************
 * 函  数: UART5_GetRxData
 * 功  能: 获取最新一帧数据 (数据的地址)
 * 参  数: 无
 * 返回值: 数据的地址(uint8_t*)
 ******************************************************************************/
uint8_t *UART5_GetRxData(void)
{
    return xUART5.puRxData ;
}

/******************************************************************************
 * 函  数: UART5_ClearRx
 * 功  能: 清理最后一帧数据的缓存
 *          主要是清0字节数,因为它是用来判断接收的标准
 * 参  数: 无
 * 返回值: 无
 ******************************************************************************/
void UART5_ClearRx(void)
{
    xUART5.usRxNum = 0 ;
}
#endif  // endif UART5_EN



#endif



/  ModbusCRC16 校验   /
///

// CRC16查表法值表数组
// 查表法相比直接计算CRC值,大约可减少一半运算时间
static const uint8_t aModbusCRC16[] =
{
    // 高位
    0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
    0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
    0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
    0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
    0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
    0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
    0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
    0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
    0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
    0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
    0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
    0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
    0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
    0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
    0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
    0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
    // 低位
    0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, 0xC6, 0x06, 0x07, 0xC7, 0x05, 0xC5, 0xC4, 0x04,
    0xCC, 0x0C, 0x0D, 0xCD, 0x0F, 0xCF, 0xCE, 0x0E, 0x0A, 0xCA, 0xCB, 0x0B, 0xC9, 0x09, 0x08, 0xC8,
    0xD8, 0x18, 0x19, 0xD9, 0x1B, 0xDB, 0xDA, 0x1A, 0x1E, 0xDE, 0xDF, 0x1F, 0xDD, 0x1D, 0x1C, 0xDC,
    0x14, 0xD4, 0xD5, 0x15, 0xD7, 0x17, 0x16, 0xD6, 0xD2, 0x12, 0x13, 0xD3, 0x11, 0xD1, 0xD0, 0x10,
    0xF0, 0x30, 0x31, 0xF1, 0x33, 0xF3, 0xF2, 0x32, 0x36, 0xF6, 0xF7, 0x37, 0xF5, 0x35, 0x34, 0xF4,
    0x3C, 0xFC, 0xFD, 0x3D, 0xFF, 0x3F, 0x3E, 0xFE, 0xFA, 0x3A, 0x3B, 0xFB, 0x39, 0xF9, 0xF8, 0x38,
    0x28, 0xE8, 0xE9, 0x29, 0xEB, 0x2B, 0x2A, 0xEA, 0xEE, 0x2E, 0x2F, 0xEF, 0x2D, 0xED, 0xEC, 0x2C,
    0xE4, 0x24, 0x25, 0xE5, 0x27, 0xE7, 0xE6, 0x26, 0x22, 0xE2, 0xE3, 0x23, 0xE1, 0x21, 0x20, 0xE0,
    0xA0, 0x60, 0x61, 0xA1, 0x63, 0xA3, 0xA2, 0x62, 0x66, 0xA6, 0xA7, 0x67, 0xA5, 0x65, 0x64, 0xA4,
    0x6C, 0xAC, 0xAD, 0x6D, 0xAF, 0x6F, 0x6E, 0xAE, 0xAA, 0x6A, 0x6B, 0xAB, 0x69, 0xA9, 0xA8, 0x68,
    0x78, 0xB8, 0xB9, 0x79, 0xBB, 0x7B, 0x7A, 0xBA, 0xBE, 0x7E, 0x7F, 0xBF, 0x7D, 0xBD, 0xBC, 0x7C,
    0xB4, 0x74, 0x75, 0xB5, 0x77, 0xB7, 0xB6, 0x76, 0x72, 0xB2, 0xB3, 0x73, 0xB1, 0x71, 0x70, 0xB0,
    0x50, 0x90, 0x91, 0x51, 0x93, 0x53, 0x52, 0x92, 0x96, 0x56, 0x57, 0x97, 0x55, 0x95, 0x94, 0x54,
    0x9C, 0x5C, 0x5D, 0x9D, 0x5F, 0x9F, 0x9E, 0x5E, 0x5A, 0x9A, 0x9B, 0x5B, 0x99, 0x59, 0x58, 0x98,
    0x88, 0x48, 0x49, 0x89, 0x4B, 0x8B, 0x8A, 0x4A, 0x4E, 0x8E, 0x8F, 0x4F, 0x8D, 0x4D, 0x4C, 0x8C,
    0x44, 0x84, 0x85, 0x45, 0x87, 0x47, 0x46, 0x86, 0x82, 0x42, 0x43, 0x83, 0x41, 0x81, 0x80, 0x40
};


/**********************************************************************************************************
 * 函 数:  Modbus_AddCRC16
 * 功 能:  在数据末尾追加两字节的ModbusCRC16校验值
 * 备 注: 1、函数将根据传入的原数据,计算CRC16值,在数据末尾追加两字节的CRC16校验值;
 *         2、注意传入的数组地址,要至少预留2字节的空间,用于追加CRC16的校验值;
 *
 * 参 数:  uint8_t*  _pcData:参与计算校验值的数据
 *         uint16_t  _usLen :数据字节数; 长度为数据的原字节数即可,不用加2;
 *
 * 返 回:  无
**********************************************************************************************************/
void  Modbus_AddCRC16(uint8_t *_pcData, uint16_t _usLen)
{
    uint8_t  ucCRCHi = 0xFF;                        // CRC值高位
    uint8_t  ucCRCLo = 0xFF;                        // CRC值低位
    uint16_t usIndex;                               // 索引

    while (_usLen--)                                // 开始逐字节查表
    {
        usIndex = ucCRCHi ^ *_pcData++;             // 注意,这里指针地址加1了
        ucCRCHi = ucCRCLo ^ aModbusCRC16[usIndex];
        ucCRCLo = aModbusCRC16[usIndex + 256];
    }
    _pcData[0] = ucCRCHi;                           // 末尾第1字节,追加CRC16的高位
    _pcData[1] = ucCRCLo;                           // 末尾第2字节,追加CRC16的低位
}



/**********************************************************************************************************
 * 函 数:  Modbus_CheckCRC16
 * 功 能:  对带ModbusCRC16校验的数据段进行校验
 *
 * 参 数:  uint8_t*  _pcData:数据段地址
 *         uint16_t  _usLen :数据段的长度(字节数); 长度是整包数据的字节数,即包含ModbusCRC16值的长度,不用减2;
 *
 * 返 回:  0-错误、1-正确
*******************************************************************************************/
uint8_t  Modbus_CheckCRC16(uint8_t *_pcData, uint16_t _usLen)
{
    uint8_t  ucCRCHi = 0xFF;                               // CRC值高位
    uint8_t  ucCRCLo = 0xFF;                               // CRC值低位
    uint16_t usIndex;                                      // 数组的索引

    _usLen -= 2;                                           // 字节数-2,不计算原数据末尾两字节(ModbusCRC16值)

    while (_usLen--)                                       // 开始逐字节查表获得ModbusCRC16值
    {
        usIndex = ucCRCHi ^ *_pcData++;                    // 注意,这里指针地址递增加1
        ucCRCHi = ucCRCLo ^ aModbusCRC16[usIndex];
        ucCRCLo = aModbusCRC16[usIndex + 256];
    }

    if ((ucCRCHi == *_pcData++) && (ucCRCLo == *_pcData))  // 把数据段的CRC16校验值,与原数据末尾的CRC16值相比较
        return 1;                                          // 成功匹配; 返回: 1

    return 0;                                              // 错误; 返回:0
}





/  辅助功能   /
///

/******************************************************************************
 * 函  数: showData
 * 功  能: 把数据,经串口1,发送到串口助手上,方便观察
 * 参  数: uint8_t  *rxData   数据地址
 *          uint16_t  rxNum    字节数
 * 返回值: 无
 ******************************************************************************/
void showData(uint8_t *rxData, uint16_t rxNum)
{
#if 0                                                          // 写法1
    printf("字节数: %d \r", rxNum);                           // 显示字节数
    printf("ASCII 显示数据: %s\r", (char *)rxData);            // 显示数据,以ASCII方式显示,即以字符串的方式显示
    printf("16进制显示数据: ");                                // 显示数据,以16进制方式,显示每一个字节的值
    while (rxNum--)                                            // 逐个字节判断,只要不为'\0', 就继续
        printf("0x%X ", *rxData++);                            // 格式化
    printf("\r\r");                                            // 显示换行
#else                                                          // 写法2
    UART1_SendString("字节数:%d \r", rxNum);                  // 显示字节数
    UART1_SendString("ASCII : %s\r", (char *)rxData);          // 显示数据,以ASCII方式显示,即以字符串的方式显示
    UART1_SendString("16进制: ");                              // 显示数据,以16进制方式,显示每一个字节的值
    while (rxNum--)                                            // 逐个字节判断,只要不为'\0', 就继续
        UART1_SendString("0x%X ", *rxData++);                  // 格式化
    UART1_SendString("\r\r");                                  // 显示换行
#endif
}





//  printf   //
///
/******************************************************************************
 * 功  能: printf函数支持代码
 *         【特别注意】加入以下代码, 使用printf函数时, 不再需要选择use MicroLIB
 * 参  数:
 * 返回值:
 * 备  注: 最后修改_2020年07月15日
 ******************************************************************************/
#pragma import(__use_no_semihosting)

struct __FILE
{
    int handle;
};                                         // 标准库需要的支持函数

FILE __stdout;                             // FILE 在stdio.h文件
void _sys_exit(int x)
{
    x = x;                                 // 定义_sys_exit()以避免使用半主机模式
}

int fputc(int ch, FILE *f)                 // 重定向fputc函数,使printf的输出,由fputc输出到UART,  这里使用串口1(USART1)
{
#if 0                                      // 方式1-使用常用的方式发送数据,比较容易理解,但等待耗时大  
    while ((USART1->SR & 0X40) == 0);      // 等待上一次串口数据发送完成
    USART1->DR = (u8) ch;                  // 写DR,串口1将发送数据
#else
    UART1_SendData((uint8_t *)&ch, 1);     // 方式2-使用队列+中断方式发送数据; 无需像方式1那样等待耗时,但要借助已写好的函数、环形缓冲
#endif
    return ch;
}

// 文件结尾,需要保留至少1空行


八、STM32F407 源代码

 可复制以下代码到文件,也可以直接下载:

bsp_UART.c + bsp_UART.h

1、bsp_UART.h

#ifndef __BSP__UART_H
#define __BSP__UART_H
/***********************************************************************************************************************************
 ** 【代码编写】  魔女开发板团队
 ** 【淘    宝】  https://demoboard.taobao.com
 ***********************************************************************************************************************************
 ** 【文件名称】  bsp_UART.h
 **
 ** 【 功  能 】  串口通信底层驱动(UART1、UART2、UART3、UART4、UART5、UART6)
 **               波特率-None-8-1
 **               调用全局声明中的串口函数,即可完成初始化、发送、接收.
 **
 ** 【 约  定 】  本文件所用串口通信,均为异步通信。
 **               2024年起更新的示例,串口函数命名时统一使用"UART",而不使用旧文件中的"USART".
 **
 ** 【适用平台】  STM32F407 + keil5 + HAL库/标准库
 **
 ** 【串口引脚】  各个串口的初始化函数UARTx_Init(),将按以下默认引脚进行初始化:
 **               1- UART1  TX_PA9   RX_PA10       特别说明:魔女开发板系列,均板载USB转TTL,PCB已布线连接好UART1, 使用和烧录用的同一USB接口,即可通过UART1和电脑进行通信。具体查看资料文件夹中的说明文件。
 **               2- UART2  TX_PA2   RX_PA3
 **               3- UART3  TX_PB10  RX_PB11
 **               4- UART4  TX_PC10  RX_PC11
 **               5- UART5  TX_PC12  RX_PD2
 **               6- UART6  TX_PC6   RX_PC7
 **
 ** 【移植说明】  1- 如果使用CubeMX配置的工程,无须对UART进行任何配置。
 **               2- 适用于HAL库、标准库工程,通过下面操作,均可直接使用。
 **               3- 复制本工程的UART文件夹,到目标工程文件夹中;
 **               4- 添加头文件路径: Keil > Option > C/C++ > Include Paths;
 **               5- 添加C文件到工程: Keil > 左侧工程管理器中双击目标文件夹 > 选择 bsp_UART.c;
 **               6- 添加文件引用:    #include "bsp_uart.h",即哪个文件要用串口功能,就在其代码文件顶部添加引用;
 **
 ** 【代码使用】  每组串口,已封装好7个函数 (初始化1个、发送3个、接收3个)。下面以UART1作示范说明:               
 **               1、初始化:             void      UART1_Init(uint32_t ulBaudrate);                                      // 初始化串口; 配置GPIO引脚PA9+PA10、配置通信参数:波特率-None-8-1、配置中断
 **               2、发送指定长度的数据  void      UART1_SendData(uint8_t *puData, uint16_t usNum);                      // 发送指定长度的数据; 参数:数据地址、字节数
 **               3、发送字符串          void      UART1_SendString(const char *pcString,...);                           // 发送字符串;   参数:字符串地址; 使用方法如同printf
 **               4、发送AT指令          uint8_t   UART1_SendAT(char *pcAT, char *pcAckString, uint16_t usTimeOutMs);    // 本函数,针对ESP8266、蓝牙模块等AT固件,用于等待返回期待的信息; 参数:AT指令字符串、期待返回信息字符串、ms等待超时; 返回:0-执行失败、1-执行成功 
 **               5、获取接收的字节数:   uint16_t  UART1_GetRxNum(void);                                                 // 获取接收到的字节数; 如果返回值大于0,即为接收到新一帧数据
 **               6、获取接收的数据:    uint8_t*  UART1_GetRxData(void);                                                // 获取接收到的数据(缓存的地址)
 **               7、清零接收标志:      void      UART1_ClearRx(void);                                                  // 清0接收标志,即清0接收到的字节数; 每次处理完成数据,就要调用这个函数清0,方可进入下轮接收判断
 **
 ** 【更新记录】  2024-10-22  中断服务函数:使用宏定义代替位操作,以提高可读情; 优化注释;
 **               2024-07-02  增加Modbus_CRC16校验的追加、判断函数
 **               2024-06-30  优化UART初始化:增加奇偶校验3个寄存器位的代码配置行
 **               2024-06-20  优化AT指令函数:名称、机制;
 **               2024-06-07  增加AT指令等待反馈处理函数:UARTx_WaitACK();
 **               2024-06-07  优化函数:UARTx_SendString();
 **               2024-06-06  优化代码适配,使文件能适用于标准库、HAL库工程
 **               2024-04-04  UARTx_SendString(),取消临时缓存的static,以减少RAM占用,适配LVGL移植;
 **               2024-03-27  修改xUSATR_TypeDef结构体,优化缓冲的存储方式
 **               2024-02-04  简化接收函数命名,函数名中使用Rx字符,代替旧函数名称中的Reveived字符;
 **               2024-01-09  取消部分静态变量,并入结构体中,使用调用更直观
 **               2024-01-03  完善函数、注释
 **               2023-12-25  增加接收函数的封装,取消接收变量(全局)的使用
 **               2023-12-23  完成对HAL库的支持
 **               2023-01-27  增加宏定义、完善注释
 **               2021-12-16  完善接收机制:取消接收标志,判断接收字节数>0即为接收到新数据
 **               2021-11-03  完善接收函数返回值处理
 **               2021-08-14  增加宏定义:接收缓存区大小设定值,使空间占用更可控;
 **               2021-08-14  修改发送函数名称:UARTx_Printf(),修改为:UARTx_SendString();
 **               2020-09-02  文件建立、UART1接收中断、空闲中断、发送中断、DMA收发
 **               2021-06-04  UART1、2、3及UART4、5 的收发完善代码
 **               2021-06-09  完善文件格式、注释
 **               2021-06-22  完善注释说明
 **
************************************************************************************************************************************/
#include "stdio.h"        // 引用C语言标准库,它定义了标准输入输出函数; 如:printf、scanf、sprintf、fopen 等; 
#include "stdarg.h"       // 引用C语言标准库,它定义了处理可变参数的宏; 如:va_start、va_arg、va_end、va_list 等;
#include "string.h"       // 引用C语言标准库,它定义了操作字符串的函数; 如:strcpy、strcmp、strlen、memset、memcpy 等;




#ifdef USE_STDPERIPH_DRIVER
    #include "stm32f4xx.h"                        // 引用 标准库的底层支持文件
#endif

#ifdef USE_HAL_DRIVER
    #include "stm32f4xx_hal.h"                    // 引用 HAL库的底层支持文件
#endif



/*****************************************************************************
 ** 移植配置修改区
 ** 备注:除非有特殊要求,否则,下面参数已通用:RS485、蓝牙模块、ESP8266模块、串口屏等
****************************************************************************/
// 串口开关
#define UART1_EN                       1          // 串口1,0=关、1=启用;  倘若没用到UART1, 置0,就不会开辟UART1发送缓存、接收缓存,省一点资源;
#define UART2_EN                       1          // 串口2,0=关、1=启用;  同上;
#define UART3_EN                       1          // 串口3,0=关、1=启用;  同上;
#define UART4_EN                       1          // 串口4,0=关、1=启用;  同上;
#define UART5_EN                       1          // 串口5,0=关、1=启用;  同上;
#define UART6_EN                       1          // 串口5,0=关、1=启用;  同上;
// 发送缓冲区大小
#define UART1_TX_BUF_SIZE           2048          // 配置每组UART发送环形缓冲区数组的大小,单位:字节数; 
#define UART2_TX_BUF_SIZE            512          // -- 只有在前面串口开关在打开状态,才会定义具体的缓冲区数组
#define UART3_TX_BUF_SIZE            512          // -- 默认值:512,此值已能适配大部场景的通信; 如果与ESP8266之类的设备通信,可适当增大此值。
#define UART4_TX_BUF_SIZE            512          // -- 值范围:1~65535; 注意初始化后,不要超过芯片最大RAM值。
#define UART5_TX_BUF_SIZE            512          // -- 注意此值是一个环形缓冲区大小,决定每一帧或多帧数据进入发送前的总缓存字节数,先进先出。
#define UART6_TX_BUF_SIZE            512          //
// 接收缓冲区大小
#define UART1_RX_BUF_SIZE           2048          // 配置每组UART接收缓冲区的大小,单位:字节; 此值影响范围:中断里的接收缓存大小,接收后数据缓存的大小
#define UART2_RX_BUF_SIZE           1024          // --- 当接收到的一帧数据字节数,小于此值时,数据正常;
#define UART3_RX_BUF_SIZE           1024          // --- 当接收到的一帧数据字节数,超过此值时,超出部分,将在中断中直接弃舍,直到此帧接收结束(发生空闲中断); 
#define UART4_RX_BUF_SIZE           1024          // 
#define UART5_RX_BUF_SIZE           1024          // 
#define UART6_RX_BUF_SIZE           1024          // 
// 结束-配置修改



/*****************************************************************************
 ** 声明全局函数
 **
 ** 每个串口的函数:
 ** 初始化    1个   波特率-None-8-1
 ** 发送      3个   发送指定长度数据、字符串、AT指令 
 ** 接收      3个   获取字节数、获取数据、清0
****************************************************************************/
// UART1
void      UART1_Init(uint32_t ulBaudrate);                                      // 初始化串口1; GPIO引脚PA9+PA10、中断优先级、通信参数:波特率可设、8位数据、无校验、1个停止位
void      UART1_SendData(uint8_t *puData, uint16_t usNum);                      // 发送指定数据; 参数:数据地址、字节数
void      UART1_SendString(const char *pcString, ...);                          // 发送字符串;   参数:字符串地址; 使用方法如同printf
uint8_t   UART1_SendAT(char *pcAT, char *pcAckString, uint16_t usTimeOutMs);    // 本函数,针对ESP8266、蓝牙模块等AT固件,用于等待返回期待的信息; 参数:AT指令字符串、期待返回信息字符串、ms等待超时; 返回:0-执行失败、1-执行成功 
uint16_t  UART1_GetRxNum(void);                                                 // 获取接收到的最新一帧字节数
uint8_t * UART1_GetRxData(void);                                                // 获取接收到的数据 (缓存的地址)
void      UART1_ClearRx(void);                                                  // 清理接收到的数据 (清理最后一帧字节数,因为它是判断接收的标志)
// UART2                                                                        
void      UART2_Init(uint32_t ulBaudrate);                                      // 初始化串口2; GPIO引脚PA2+PA3、中断优先级、通信参数:波特率可设、8位数据、无校验、1个停止位
void      UART2_SendData(uint8_t *puData, uint16_t usNum);                      // 发送指定数据; 参数:数据地址、字节数
void      UART2_SendString(const char *pcString, ...);                          // 发送字符串;   参数:字符串地址; 使用方法如同printf
uint8_t   UART2_SendAT(char *pcAT, char *pcAckString, uint16_t usTimeOutMs);    // 本函数,针对ESP8266、蓝牙模块等AT固件,用于等待返回期待的信息; 参数:AT指令字符串、期待返回信息字符串、ms等待超时; 返回:0-执行失败、1-执行成功 
uint16_t  UART2_GetRxNum(void);                                                 // 获取接收到的最新一帧字节数
uint8_t * UART2_GetRxData(void);                                                // 获取接收到的数据 (缓存的地址)
void      UART2_ClearRx(void);                                                  // 清理接收到的数据 (清理最后一帧字节数,因为它是判断接收的标志)
// UART3                                                                        
void      UART3_Init(uint32_t ulBaudrate);                                      // 初始化串口3; GPIO引脚PB10+PB11、中断优先级、通信参数:波特率可设、8位数据、无校验、1个停止位
void      UART3_SendData(uint8_t *puData, uint16_t usNum);                      // 发送指定数据; 参数:数据地址、字节数
void      UART3_SendString(const char *pcString, ...);                          // 发送字符串;   参数:字符串地址; 使用方法如同printf
uint8_t   UART3_SendAT(char *pcAT, char *pcAckString, uint16_t usTimeOutMs);    // 本函数,针对ESP8266、蓝牙模块等AT固件,用于等待返回期待的信息; 参数:AT指令字符串、期待返回信息字符串、ms等待超时; 返回:0-执行失败、1-执行成功 
uint16_t  UART3_GetRxNum(void);                                                 // 获取接收到的最新一帧字节数
uint8_t * UART3_GetRxData(void);                                                // 获取接收到的数据 (缓存的地址)
void      UART3_ClearRx(void);                                                  // 清理接收到的数据 (清理最后一帧字节数,因为它是判断接收的标志)
// UART4                                                            
void      UART4_Init(uint32_t ulBaudrate);                                      // 初始化串口4; GPIO引脚PC10+PC11、中断优先级、通信参数:波特率可设、8位数据、无校验、1个停止位
void      UART4_SendData(uint8_t *puData, uint16_t usNum);                      // 发送指定数据; 参数:数据地址、字节数
void      UART4_SendString(const char *pcString, ...);                          // 发送字符串;   参数:字符串地址; 使用方法如同printf
uint8_t   UART4_SendAT(char *pcAT, char *pcAckString, uint16_t usTimeOutMs);    // 本函数,针对ESP8266、蓝牙模块等AT固件,用于等待返回期待的信息; 参数:AT指令字符串、期待返回信息字符串、ms等待超时; 返回:0-执行失败、1-执行成功 
uint16_t  UART4_GetRxNum(void);                                                 // 获取接收到的最新一帧字节数
uint8_t * UART4_GetRxData(void);                                                // 获取接收到的数据 (缓存的地址)
void      UART4_ClearRx(void);                                                  // 清理接收到的数据 (清理最后一帧字节数,因为它是判断接收的标志)
// UART5                                                                        
void      UART5_Init(uint32_t ulBaudrate);                                      // 初始化串口5; GPIO引脚PC12+PD2、中断优先级、通信参数:波特率可设、8位数据、无校验、1个停止位
void      UART5_SendData(uint8_t *puData, uint16_t usNum);                      // 发送指定数据; 参数:数据地址、字节数
void      UART5_SendString(const char *pcString, ...);                          // 发送字符串;   参数:字符串地址; 使用方法如同printf
uint8_t   UART5_SendAT(char *pcAT, char *pcAckString, uint16_t usTimeOutMs);    // 本函数,针对ESP8266、蓝牙模块等AT固件,用于等待返回期待的信息; 参数:AT指令字符串、期待返回信息字符串、ms等待超时; 返回:0-执行失败、1-执行成功 
uint16_t  UART5_GetRxNum(void);                                                 // 获取接收到的最新一帧字节数
uint8_t * UART5_GetRxData(void);                                                // 获取接收到的数据 (缓存的地址)
void      UART5_ClearRx(void);                                                  // 清理接收到的数据 (清理最后一帧字节数,因为它是判断接收的标志)
// UART6                                                                        
void      UART6_Init(uint32_t ulBaudrate);                                      // 初始化串口6; GPIO引脚PC6+PC7、中断优先级、通信参数:波特率可设、8位数据、无校验、1个停止位
void      UART6_SendData(uint8_t *puData, uint16_t usNum);                      // 发送指定数据; 参数:数据地址、字节数
void      UART6_SendString(const char *pcString, ...);                          // 发送字符串;   参数:字符串地址; 使用方法如同printf
uint8_t   UART6_SendAT(char *pcAT, char *pcAckString, uint16_t usTimeOutMs);    // 本函数,针对ESP8266、蓝牙模块等AT固件,用于等待返回期待的信息; 参数:AT指令字符串、期待返回信息字符串、ms等待超时; 返回:0-执行失败、1-执行成功 
uint16_t  UART6_GetRxNum(void);                                                 // 获取接收到的最新一帧字节数
uint8_t * UART6_GetRxData(void);                                                // 获取接收到的数据 (缓存的地址)
void      UART6_ClearRx(void);                                                  // 清理接收到的数据 (清理最后一帧字节数,因为它是判断接收的标志)     

// 辅助函数:Modbus_CRC16校验
void      Modbus_AddCRC16(uint8_t *_pcData, uint16_t _usLen);                   // 对数据追加2字节的ModbusCRC16校验值到末尾; 参数:原始数据、原始数据字节数; 注意:调用函数后,原始数据会增加2字节
uint8_t   Modbus_CheckCRC16(uint8_t *_pcData, uint16_t _usLen);                 // 对带ModbusCRC16校验的数据段进行校验; 返回:0-错误、1-匹配正确;


#endif

2、bsp_UART.c

/***********************************************************************************************************************************
 ** 【代码编写】  魔女开发板团队
 ** 【最后版本】  2024-07-08-01
 ** 【淘    宝】  https://demoboard.taobao.com
 ***********************************************************************************************************************************
 ** 【文件名称】  bsp_UART.c
 **
 ** 【文件功能】  各UART的GPIO配置、通信协议配置、中断配置,及功能函数实现
 **
 ** 【适用平台】  STM32F407 + keil5 + HAL库/标准库
 **
 ** 【最后更新]   20240708-1
 **         
 ** 【特别说明】  1、为什么这个文件中,同时有标准库、HAL库的代码?
 **                  是为了方便移植!已做好预编译处理,可适用于标准库、HAL库的工程移植。
 **               2、为什么UART初始化、中断,用寄存器操作,而不是用更有可读性的HAL库?
 **                  因为CubeMX配置工程时,如果不对UART进行配置,且打勾只生成需要的文件,工程中将没有UART的HAL支持文件的。
 **                  另外, 中断里用寄存器操作,与HAL库的重重封装相比,明显地更高效!
************************************************************************************************************************************/
#include "bsp_UART.h"             // 头文件




/*****************************************************************************
 ** 声明本地变量
****************************************************************************/
typedef struct
{
    uint16_t  usRxNum;            // 新一帧数据,接收到多少个字节数据
    uint8_t  *puRxData;           // 新一帧数据,数据缓存; 存放的是空闲中断后,从临时接收缓存复制过来的完整数据,并非接收过程中的不完整数据;

    uint8_t  *puTxFiFoData;       // 发送缓冲区,环形队列; 为了方便理解阅读,没有封装成队列函数
    uint16_t  usTxFiFoData ;      // 环形缓冲区的队头
    uint16_t  usTxFiFoTail ;      // 环形缓冲区的队尾
} xUSATR_TypeDef;





/******************************************************************************
 * 函  数: delay_ms
 * 功  能: ms 延时函数
 * 备  注: 1、系统时钟168MHz
 *          2、打勾:Options/ c++ / One ELF Section per Function
            3、编译优化级别:Level 3(-O3)
 * 参  数: uint32_t  ms  毫秒值
 * 返回值: 无
 ******************************************************************************/
static volatile uint32_t ulTimesMS;    // 使用volatile声明,防止变量被编译器优化
static void delay_ms(uint16_t ms)
{
    ulTimesMS = ms * 16500;
    while (ulTimesMS)
        ulTimesMS--;                   // 操作外部变量,防止空循环被编译器优化掉
}





//   UART-1   ///
/
#if UART1_EN

static xUSATR_TypeDef  xUART1 = { 0 };                      // 定义 UART1 的收发结构体
static uint8_t uaUART1RxData[UART1_RX_BUF_SIZE];            // 定义 UART1 的接收缓存
static uint8_t uaUART1TxFiFoData[UART1_TX_BUF_SIZE];        // 定义 UART1 的发送缓存

/******************************************************************************
 * 函  数: UART1_Init
 * 功  能: 初始化USART1的通信引脚、协议参数、中断优先级
 *          引脚:TX-PA10、RX-PA11
 *          协议:波特率-None-8-1
 *          发送:发送中断
 *          接收:接收+空闲中断
 *
 * 参  数: uint32_t  ulBaudrate  通信波特率
 * 返回值: 无
 ******************************************************************************/
void UART1_Init(uint32_t ulBaudrate)
{
#ifdef USE_STDPERIPH_DRIVER
    // 使能 时钟
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);           // 使能 GPIOA  时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);          // 使能 USART1 时钟              
    // 配置 引脚的复用功能                                       
    GPIO_PinAFConfig(GPIOA, GPIO_PinSource9, GPIO_AF_USART1);       // 配置PA9复用功能: USART1
    GPIO_PinAFConfig(GPIOA, GPIO_PinSource10, GPIO_AF_USART1);      // 配置PA10复用功能:USART1
    // 重置 USART                                                  
    RCC_APB2PeriphResetCmd(RCC_APB2Periph_USART1, ENABLE);          // 使能重置
    RCC_APB2PeriphResetCmd(RCC_APB2Periph_USART1, DISABLE);         // 取消重置
    // 配置 TX引脚                                                   
    GPIO_InitTypeDef  GPIO_InitStructure = {0};                     // GPIO 初始化结构体
    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_9;                     // 引脚编号
    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_AF;                   // 引脚方向: 复用功能
    GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;                  // 输出模式:推挽
    GPIO_InitStructure.GPIO_PuPd  = GPIO_PuPd_UP;                   // 上下拉:上拉
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;               // 输出速度:50MHz
    GPIO_Init(GPIOA, &GPIO_InitStructure);                          // 初始化:把上述参数,更新到芯片寄存器
    // 配置R X引脚                                                   
    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_10;                    // 引脚编号
    GPIO_Init(GPIOA, &GPIO_InitStructure);                          // 初始化:把上述参数,更新到芯片寄存器
    // 配置 UART                                                   
    USART_InitTypeDef USART_InitStructure;                          // 声明USART初始化结构体  
    USART_InitStructure.USART_BaudRate = ulBaudrate;                // 设置波特率
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;     // 设置字长为8位数据格式
    USART_InitStructure.USART_StopBits = USART_StopBits_1;          // 设置一个停止位
    USART_InitStructure.USART_Parity = USART_Parity_No;             // 无奇偶校验位
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; // 无硬件数据流控制
    USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; // 收发模式
    USART_Init(USART1, &USART_InitStructure);                       // 初始化USART
    // 配置 中断
    USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);                  // 开启 接收中断
    USART_ITConfig(USART1, USART_IT_IDLE, ENABLE);                  // 开启 空闲中断    
    // 配置中断优先级
    NVIC_InitTypeDef  NVIC_InitStructure = {0};                     // 中断优先级配置结构体
    NVIC_InitStructure .NVIC_IRQChannel = USART1_IRQn;              // 指定中断通道
    NVIC_InitStructure .NVIC_IRQChannelPreemptionPriority = 1;      // 设置抢占优先级
    NVIC_InitStructure .NVIC_IRQChannelSubPriority = 1;             // 设置响应优先级
    NVIC_InitStructure .NVIC_IRQChannelCmd = ENABLE;                // 使能中断通道
    NVIC_Init(&NVIC_InitStructure);                                 // 初始化NVIC
    // 配置完成,开启USART
    USART_Cmd(USART1, ENABLE);                                      // 使能USART1
#endif

#ifdef USE_HAL_DRIVER                                               // HAL库 配置
    // 使能 时钟                                                
    __HAL_RCC_GPIOA_CLK_ENABLE();                                   // 使能GPIOA
    __HAL_RCC_USART1_CLK_ENABLE();                                  // 使能USART1
    // 重置 USART                                                 
    __HAL_RCC_USART1_FORCE_RESET();                                 // 使能重置
    __HAL_RCC_USART1_RELEASE_RESET();                               // 取消重置
    // 配置 引脚                                        
    GPIO_InitTypeDef    GPIO_InitStruct = {0};                      // 声明初始化要用到的结构体
    GPIO_InitStruct.Pin   = GPIO_PIN_9 | GPIO_PIN_10;               // 引脚 TX-PA9、RX-PA10
    GPIO_InitStruct.Mode  = GPIO_MODE_AF_PP;                        // 工作模式
    GPIO_InitStruct.Pull  = GPIO_PULLUP;                            // 上下拉
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;              // 引脚速率
    GPIO_InitStruct.Alternate = GPIO_AF7_USART1;                    // 引脚复用功能
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);                         // 初始化引脚工作模式
    // 计算波特率参数                                              
    float    temp;
    uint16_t mantissa, fraction;
    SystemCoreClockUpdate();                                        // 更新系统运行频率全局值; 函数SystemCoreClock( ),在标准库、HAL库通用
    temp = (float)(SystemCoreClock / 2) / (ulBaudrate * 16);        // 波特率公式计算; USART1挂载在APB2, 时钟为系统时钟的2分频; 全局变量SystemCoreClock,在标准库、HAL库通用;
    mantissa = temp;				                                // 整数部分
    fraction = (temp - mantissa) * 16;                              // 小数部分
    USART1 -> BRR  = mantissa << 4 | fraction;                      // 设置波特率
    // 配置 USART                                            
    USART1 -> CR1  =   0;                                           // 清0
    USART1 -> CR1 |=   0x01 << 2;                                   // 接收使能[02]: 0=失能、1=使能
    USART1 -> CR1 |=   0x01 << 3;                                   // 发送使能[03]:0=失能、1=使能
    USART1 -> CR1 |=   0x00 << 9;                                   // 奇偶校验[09]:0=偶Even、1=奇Odd;  注意:使用奇偶校验,下面两项要置1
    USART1 -> CR1 |=   0x00 << 10;                                  // 校验位  [10]:0=禁用、1=使能;     注意,使用奇偶校验,此位要置1 (否则无效、数据错乱)
    USART1 -> CR1 |=   0x00 << 12;                                  // 数据位  [12]:0=8位、 1=9位;      注意:使用奇偶校验,此位要置1 (否则数据会发生错乱)
    USART1 -> CR2  =   0;                                           // 数据清0
    USART1 -> CR2 &= ~(0x03 << 12);                                 // 停止位[13:12]:00b=1个停止位、01b=0.5个停止位、10b=2个停止位、11b=1.5个停止位
    USART1 -> CR3  =   0;                                           // 数据清0
    USART1 -> CR3 &= ~(0x01 << 6);                                  // DMA接收[6]: 0=禁止、1=使能
    USART1 -> CR3 &= ~(0x01 << 7);                                  // DMA发送[7]: 0=禁止、1=使能
    // 配置 中断                                                 
    USART1 -> CR1 &= ~(0x01 << 7);                                  // 关闭发送中断
    USART1 -> CR1 |=   0x01 << 5;                                   // 使能接收中断: 接收缓冲区非空
    USART1 -> CR1 |=   0x01 << 4;                                   // 使能空闲中断:超过1字节时间没收到新数据
    USART1 -> SR   = ~(0x00F0);                                     // 清理中断
    // 配置 中断优先级                                              
    HAL_NVIC_SetPriority(USART1_IRQn, 1, 1);                        // 设置指定中断的响应优先级;  参数:中断请求编号、抢占级、子优先级
    HAL_NVIC_EnableIRQ(USART1_IRQn);                                // 使能、启用指定的中断
    // 配置完成,开启USART
    USART1 -> CR1 |=   0x01 << 13;                                  // 使能UART开始工作
#endif

    // 关联缓冲区                                                   
    xUART1.puRxData = uaUART1RxData;                                // 关联接收缓冲区的地址
    xUART1.puTxFiFoData = uaUART1TxFiFoData;                        // 关联发送缓冲区的地址   
    
    // 输出提示
    printf("\r\r\r===========  STM32F407VE 外设 初始化报告 ===========\r");                   // 输出到串口助手
    SystemCoreClockUpdate();                                                                  // 更新一下系统运行频率变量
    printf("系统时钟频率             %d MHz\r", SystemCoreClock / 1000000);                   // 输出到串口助手
    printf("UART1 初始化配置         %d-None-8-1; 已完成初始化配置、收发配置\r", ulBaudrate); // 输出到串口助手
}

/******************************************************************************
 * 函  数: USART1_IRQHandler
 * 功  能: USART1的接收中断、空闲中断、发送中断
 * 参  数: 无
 * 返回值: 无
 * 备  注: 本函数,当产生中断事件时,由硬件调用。
 *          如果使用本文件代码,在工程文件的其它地方,要注释同名函数,否则冲突。
******************************************************************************/
void USART1_IRQHandler(void)
{
    static uint16_t cnt = 0;                                             // 接收字节数累计:每一帧数据已接收到的字节数
    static uint8_t  rxTemp[UART1_RX_BUF_SIZE];                           // 接收数据缓存数组:每新接收1个字节,先顺序存放到这里,当一帧接收完(发生空闲中断), 再转存到外部缓存:xUARTx.puRxData[ ]
                                                                         
    // 发送中断:用于把环形缓冲的数据,逐字节发出                        
    if ((USART1->SR & USART_SR_TXE) && (USART1->CR1 & USART_CR1_TXEIE))  // 检查发送寄存器空中断使能,且发送寄存器为空; TXE(发送数据寄存器空)、TXEIE(发送缓冲区空中断使能)
    {                                                                    
        USART1->DR = xUART1.puTxFiFoData[xUART1.usTxFiFoTail++];         // 从FIFO队列中取出一个数据,放入USART的发送寄存器(硬件会自动发出),然后将FIFO的尾指针递增,指向下一个要发送的数据
        if (xUART1.usTxFiFoTail == UART1_TX_BUF_SIZE)                    // 检查FIFO尾指针是否到达了FIFO队列的末尾
            xUART1.usTxFiFoTail = 0;                                     // 将尾指针重置为0,实现环形队列的功能
        if (xUART1.usTxFiFoTail == xUART1.usTxFiFoData)                  // 检查FIFO尾指针是否追上了头指针,即所有数据是否都已发送完毕
            USART1->CR1 &= ~USART_CR1_TXEIE;                             // 关闭发送寄存器空中断,防止中断服务程序被不必要地调用
        return;                                                          
    }                                                                    
                                                                         
    // 接收中断:用于逐个字节接收,存放到临时缓存                        
    if (USART1->SR & USART_SR_RXNE)                                      // 检查RXNE(读数据寄存器非空标志位); RXNE中断清理方法:读DR时自动清理;
    {                                                                    
        if (cnt == UART1_RX_BUF_SIZE)                                    // 当前帧已接收的字节量,已满缓存区的大小; 为避免溢出,本包后面接收到的数据直接舍弃;
        {
            printf("警告:UART1单帧接收量,已超出接收缓存大小!\r修改文件bsp_UART.h的UART1_RX_BUF_SIZE值,可解决此问题!\r");
            USART1->DR;                                                  // 读取数据寄存器的数据,但不保存.主要作用:读DR时自动清理接收中断标志;
            return;
        }
        rxTemp[cnt++] = USART1->DR ;                                     // 把新收到的字节数据,顺序存放到RXTemp数组中;注意:读取DR时自动清零中断位;
        return;
    }

    // 空闲中断:用于判断一帧数据结束,整理数据到外部备读
    if (USART1->SR & USART_SR_IDLE)                                      // 检查IDLE(空闲中断标志位); IDLE中断标志清理方法:序列清零,USART1 ->SR;  USART1 ->DR;
    {
        memcpy(xUART1.puRxData, rxTemp, UART1_RX_BUF_SIZE);              // 把本帧接收到的数据,存入到结构体的数组成员xUARTx.puRxData中, 等待处理; 注意:复制的是整个数组,包括0值,以方便字符串输出时尾部以0作字符串结束符
        xUART1.usRxNum  = cnt;                                           // 把接收到的字节数,存入到结构体变量xUARTx.usRxNum中;
        cnt = 0;                                                         // 接收字节数累计器,清零; 准备下一次的接收
        memset(rxTemp, 0, UART1_RX_BUF_SIZE);                            // 接收数据缓存数组,清零; 准备下一次的接收
        USART1 ->SR;
        USART1 ->DR;                                                     // 清零IDLE中断标志位!! 序列清零,顺序不能错!!
        return;
    }

    return;
}

/******************************************************************************
 * 函  数: UART1_SendData
 * 功  能: UART通过中断发送数据
 *         【适合场景】本函数可发送各种数据,而不限于字符串,如int,char
 *         【不 适 合】注意h文件中所定义的发缓冲区大小、注意数据压入缓冲区的速度与串口发送速度的冲突
 * 参  数: uint8_t*  puData   需发送数据的地址
 *          uint16_t  usNum    发送的字节数 ,数量受限于h文件中设置的发送缓存区大小宏定义
 * 返回值: 无
 ******************************************************************************/
void UART1_SendData(uint8_t *puData, uint16_t usNum)
{
    for (uint16_t i = 0; i < usNum; i++)                         // 把数据放入环形缓冲区
    {
        xUART1.puTxFiFoData[xUART1.usTxFiFoData++] = puData[i];  // 把字节放到缓冲区最后的位置,然后指针后移
        if (xUART1.usTxFiFoData == UART1_TX_BUF_SIZE)            // 如果指针位置到达缓冲区的最大值,则归0
            xUART1.usTxFiFoData = 0;
    }                                                            // 为了方便阅读理解,这里没有把此部分封装成队列函数,可以自行封装

    if ((USART1->CR1 & USART_CR1_TXEIE) == 0)                    // 检查USART寄存器的发送缓冲区空置中断(TXEIE)是否已打开
        USART1->CR1 |= USART_CR1_TXEIE;                          // 打开TXEIE中断
}

/******************************************************************************
 * 函  数: UART1_SendString
 * 功  能: 发送字符串
 *          用法请参考printf,及示例中的展示
 *          注意,最大发送字节数为512-1个字符,可在函数中修改上限
 * 参  数: const char *pcString, ...   (如同printf的用法)
 * 返回值: 无
 ******************************************************************************/
void UART1_SendString(const char *pcString, ...)
{
    char mBuffer[512] = {0};;                               // 开辟一个缓存, 并把里面的数据置0
    va_list ap;                                             // 新建一个可变参数列表
    va_start(ap, pcString);                                 // 列表指向第一个可变参数
    vsnprintf(mBuffer, 512, pcString, ap);                  // 把所有参数,按格式,输出到缓存; 参数2用于限制发送的最大字节数,如果达到上限,则只发送上限值-1; 最后1字节自动置'\0'
    va_end(ap);                                             // 清空可变参数列表
    UART1_SendData((uint8_t *)mBuffer, strlen(mBuffer));    // 把字节存放环形缓冲,排队准备发送
}

/******************************************************************************
 * 函    数: UART1_SendAT
 * 功    能: 发送AT命令, 并等待返回信息
 * 参    数: char     *pcString      AT指令字符串
 *            char     *pcAckString   期待的指令返回信息字符串
 *            uint16_t  usTimeOut     发送命令后等待的时间,毫秒
 *
 * 返 回 值: 0-执行失败、1-执行正常
 ******************************************************************************/
uint8_t UART1_SendAT(char *pcAT, char *pcAckString, uint16_t usTimeOutMs)
{
    UART1_ClearRx();                                              // 清0
    UART1_SendString(pcAT);                                       // 发送AT指令字符串
    while (usTimeOutMs--)                                         // 判断是否起时(这里只作简单的循环判断次数处理)
    {
        if (UART1_GetRxNum())                                     // 判断是否接收到数据
        {
            UART1_ClearRx();                                      // 清0接收字节数; 注意:接收到的数据内容 ,是没有被清0的
            if (strstr((char *)UART1_GetRxData(), pcAckString))   // 判断返回数据中是否有期待的字符
                return 1;                                         // 返回:0-超时没有返回、1-正常返回期待值
        }
        delay_ms(1);                                              // 延时; 用于超时退出处理,避免死等
    }
    return 0;                                                     // 返回:0-超时、返回异常,1-正常返回期待值
}


/******************************************************************************
 * 函  数: UART1_SendStringForDMA
 * 功  能: UART通过DMA发送数据,省了占用中断的时间
 *         【适合场景】字符串,字节数非常多,
 *         【不 适 合】1:只适合发送字符串,不适合发送可能含0的数值类数据; 2-时间间隔要足够
 * 参  数: char strintTemp  要发送的字符串首地址
 * 返回值: 无
 * 备  注:  本函数为保留函数,留作用户参考。为了方便移植,本文件对外不再使用本函数。
 ******************************************************************************/
#if 0
void UART1_SendStringForDMA(char *stringTemp)
{
    static uint8_t Flag_DmaTxInit = 0;                // 用于标记是否已配置DMA发送
    uint32_t   num = 0;                               // 发送的数量,注意发送的单位不是必须8位的
    char *t = stringTemp ;                            // 用于配合计算发送的数量

    while (*t++ != 0)  num++;                         // 计算要发送的数目,这步比较耗时,测试发现每多6个字节,增加1us,单位:8位

    while (DMA1_Channel4->CNDTR > 0);                 // 重要:如果DMA还在进行上次发送,就等待; 得进完成中断清标志,F4不用这么麻烦,发送完后EN自动清零
    if (Flag_DmaTxInit == 0)                          // 是否已进行过USAART_TX的DMA传输配置
    {
        Flag_DmaTxInit  = 1;                          // 设置标记,下次调用本函数就不再进行配置了
        USART1 ->CR3   |= 1 << 7;                     // 使能DMA发送
        RCC->AHBENR    |= 1 << 0;                     // 开启DMA1时钟  [0]DMA1   [1]DMA2

        DMA1_Channel4->CCR   = 0;                     // 失能, 清0整个寄存器, DMA必须失能才能配置
        DMA1_Channel4->CNDTR = num;                   // 传输数据量
        DMA1_Channel4->CMAR  = (uint32_t)stringTemp;  // 存储器地址
        DMA1_Channel4->CPAR  = (uint32_t)&USART1->DR; // 外设地址

        DMA1_Channel4->CCR |= 1 << 4;                 // 数据传输方向   0:从外设读   1:从存储器读
        DMA1_Channel4->CCR |= 0 << 5;                 // 循环模式       0:不循环     1:循环
        DMA1_Channel4->CCR |= 0 << 6;                 // 外设地址非增量模式
        DMA1_Channel4->CCR |= 1 << 7;                 // 存储器增量模式
        DMA1_Channel4->CCR |= 0 << 8;                 // 外设数据宽度为8位
        DMA1_Channel4->CCR |= 0 << 10;                // 存储器数据宽度8位
        DMA1_Channel4->CCR |= 0 << 12;                // 中等优先级
        DMA1_Channel4->CCR |= 0 << 14;                // 非存储器到存储器模式
    }
    DMA1_Channel4->CCR  &= ~((uint32_t)(1 << 0));     // 失能,DMA必须失能才能配置
    DMA1_Channel4->CNDTR = num;                       // 传输数据量
    DMA1_Channel4->CMAR  = (uint32_t)stringTemp;      // 存储器地址
    DMA1_Channel4->CCR  |= 1 << 0;                    // 开启DMA传输
}
#endif

/******************************************************************************
 * 函  数: UART1_GetRxNum
 * 功  能: 获取最新一帧数据的字节数
 * 参  数: 无
 * 返回值: 0=没有接收到数据,非0=新一帧数据的字节数
 ******************************************************************************/
uint16_t UART1_GetRxNum(void)
{
    return xUART1.usRxNum ;
}

/******************************************************************************
 * 函  数: UART1_GetRxData
 * 功  能: 获取最新一帧数据 (数据的地址)
 * 参  数: 无
 * 返回值: 缓存地址(uint8_t*)
 ******************************************************************************/
uint8_t *UART1_GetRxData(void)
{
    return xUART1.puRxData ;
}

/******************************************************************************
 * 函  数: UART1_ClearRx
 * 功  能: 清理最后一帧数据的缓存
 *          主要是清0字节数,因为它是用来判断接收的标准
 * 参  数: 无
 * 返回值: 无
 ******************************************************************************/
void UART1_ClearRx(void)
{
    xUART1.usRxNum = 0 ;
}
#endif  // endif UART1_EN





//   UART-2   ///
/
#if UART2_EN

static xUSATR_TypeDef  xUART2 = { 0 };                      // 定义 UART2 的收发结构体
static uint8_t uaUART2RxData[UART2_RX_BUF_SIZE];            // 定义 UART2 的接收缓存
static uint8_t uaUART2TxFiFoData[UART2_TX_BUF_SIZE];        // 定义 UART2 的发送缓存

/******************************************************************************
 * 函  数: UART2_Init
 * 功  能: 初始化USART2的通信引脚、协议参数、中断优先级
 *          引脚:TX-PA2、RX-PA3
 *          协议:波特率-None-8-1
 *          发送:发送中断
 *          接收:接收+空闲中断
 *
 * 参  数: uint32_t  ulBaudrate  通信波特率
 * 返回值: 无
 ******************************************************************************/
void UART2_Init(uint32_t ulBaudrate)
{
#ifdef USE_STDPERIPH_DRIVER
    // 使能 时钟
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);           // 使能 GPIOA  时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE);          // 使能 USART2 时钟              
    // 配置 引脚的复用功能                                       
    GPIO_PinAFConfig(GPIOA, GPIO_PinSource2, GPIO_AF_USART2);       // 配置PA2复用功能:USART2
    GPIO_PinAFConfig(GPIOA, GPIO_PinSource3, GPIO_AF_USART2);       // 配置PA3复用功能:USART2
    // 重置 USART                                                
    RCC_APB1PeriphResetCmd(RCC_APB1Periph_USART2, ENABLE);          // 使能重置
    RCC_APB1PeriphResetCmd(RCC_APB1Periph_USART2, DISABLE);         // 取消重置
    // 配置 TX引脚                                                   
    GPIO_InitTypeDef  GPIO_InitStructure = {0};                     // GPIO 初始化结构体
    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_2;                     // 引脚编号:TX_PA2
    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_AF;                   // 引脚方向: 复用功能
    GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;                  // 输出模式:推挽
    GPIO_InitStructure.GPIO_PuPd  = GPIO_PuPd_UP;                   // 上下拉:上拉
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;               // 输出速度:50MHz
    GPIO_Init(GPIOA, &GPIO_InitStructure);                          // 初始化:把上述参数,更新到芯片寄存器
    // 配置 RX引脚                                                   
    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_3;                     // 引脚编号:RX_PA3
    GPIO_Init(GPIOA, &GPIO_InitStructure);                          // 初始化:把上述参数,更新到芯片寄存器
    // 配置 UART                                                   
    USART_InitTypeDef USART_InitStructure;                          // 声明USART初始化结构体  
    USART_InitStructure.USART_BaudRate = ulBaudrate;                // 设置波特率
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;     // 设置字长为8位数据格式
    USART_InitStructure.USART_StopBits = USART_StopBits_1;          // 设置一个停止位
    USART_InitStructure.USART_Parity = USART_Parity_No;             // 无奇偶校验位
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; // 无硬件数据流控制
    USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; // 收发模式
    USART_Init(USART2, &USART_InitStructure);                       // 初始化USART
    // 配置 中断
    USART_ITConfig(USART2, USART_IT_RXNE, ENABLE);                  // 开启 接收中断
    USART_ITConfig(USART2, USART_IT_IDLE, ENABLE);                  // 开启 空闲中断  
    // 配置 中断优先级
    NVIC_InitTypeDef  NVIC_InitStructure = {0};                     // 中断优先级配置结构体
    NVIC_InitStructure .NVIC_IRQChannel = USART2_IRQn;              // 指定中断通道
    NVIC_InitStructure .NVIC_IRQChannelPreemptionPriority = 1;      // 设置抢占优先级
    NVIC_InitStructure .NVIC_IRQChannelSubPriority = 1;             // 设置响应优先级
    NVIC_InitStructure .NVIC_IRQChannelCmd = ENABLE;                // 使能中断通道
    NVIC_Init(&NVIC_InitStructure);                                 // 初始化NVIC
    // 配置完成,开启USART
    USART_Cmd(USART2, ENABLE);                                      // 使能USART
#endif

#ifdef USE_HAL_DRIVER                                               // HAL库 配置
    // 使能 时钟                                                   
    __HAL_RCC_GPIOA_CLK_ENABLE();                                   // 使能GPIOA
    __HAL_RCC_USART2_CLK_ENABLE();                                  // 使能USART2
    // 重置 USART                                                  
    __HAL_RCC_USART2_FORCE_RESET();                                 // 使能重置
    __HAL_RCC_USART2_RELEASE_RESET();                               // 取消重置    
    // 配置 GPIO引脚                                               
    GPIO_InitTypeDef    GPIO_InitStruct = {0};                      // 声明初始化要用到的结构体
    GPIO_InitStruct.Pin   = GPIO_PIN_2 | GPIO_PIN_3;                // 引脚 TX-PA2、RX-PA3
    GPIO_InitStruct.Mode  = GPIO_MODE_AF_PP;                        // 工作模式
    GPIO_InitStruct.Pull  = GPIO_PULLUP;                            // 上下拉
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;              // 引脚速率
    GPIO_InitStruct.Alternate = GPIO_AF7_USART2;                    // 引脚复用功能
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);                         // 初始化引脚工作模式
    // 计算波特率参数                                              
    float    temp;                                                 
    uint16_t mantissa, fraction;                                   
    SystemCoreClockUpdate();                                        // 更新系统运行频率全局值; 函数SystemCoreClock( ),在标准库、HAL库通用
    temp = (float)(SystemCoreClock / 4) / (ulBaudrate * 16);        // 波特率公式计算; USART2挂载在APB1, 时钟为系统时钟的4分频; 全局变量SystemCoreClock,在标准库、HAL库通用;
    mantissa = temp;				                                // 整数部分
    fraction = (temp - mantissa) * 16;                              // 小数部分
    USART2 -> BRR  = mantissa << 4 | fraction;                      // 设置波特率
    // 配置 UART
    USART2 -> CR1  =   0;                                           // 清0
    USART2 -> CR1 |=   0x01 << 2;                                   // 接收使能[02]: 0=失能、1=使能
    USART2 -> CR1 |=   0x01 << 3;                                   // 发送使能[03]:0=失能、1=使能
    USART2 -> CR1 |=   0x00 << 9;                                   // 奇偶校验[09]:0=偶Even、1=奇Odd;  注意:使用奇偶校验,下面两项要置1
    USART2 -> CR1 |=   0x00 << 10;                                  // 校验位  [10]:0=禁用、1=使能;     注意,使用奇偶校验,此位要置1 (否则无效、数据错乱)
    USART2 -> CR1 |=   0x00 << 12;                                  // 数据位  [12]:0=8位、 1=9位;      注意:使用奇偶校验,此位要置1 (否则数据会发生错乱)
    USART2 -> CR2  =   0;                                           // 数据清0
    USART2 -> CR2 &= ~(0x03 << 12);                                 // 停止位[13:12]:00b=1个停止位、01b=0.5个停止位、10b=2个停止位、11b=1.5个停止位
    USART2 -> CR3  =   0;                                           // 数据清0
    USART2 -> CR3 &= ~(0x01 << 6);                                  // DMA接收[6]: 0=禁止、1=使能
    USART2 -> CR3 &= ~(0x01 << 7);                                  // DMA发送[7]: 0=禁止、1=使能
    // 配置 中断                                                   
    USART2 -> CR1 &= ~(0x01 << 7);                                  // 关闭发送中断
    USART2 -> CR1 |=   0x01 << 5;                                   // 使能接收中断: 接收缓冲区非空
    USART2 -> CR1 |=   0x01 << 4;                                   // 使能空闲中断:超过1字节时间没收到新数据
    USART2 -> SR   = ~(0x00F0);                                     // 清理中断
    // 配置 中断优先级                                             
    HAL_NVIC_SetPriority(USART2_IRQn, 1, 1);                        // 设置指定中断的响应优先级;  参数:中断请求编号、抢占级、子优先级
    HAL_NVIC_EnableIRQ(USART2_IRQn);                                // 使能、启用指定的中断
    // 配置完成,开启USART                                         
    USART2 -> CR1 |=   0x01 << 13;                                  // 使能UART开始工作
#endif                                                             
    // 关联缓冲区                                                  
    xUART2.puRxData = uaUART2RxData;                                // 获取接收缓冲区的地址
    xUART2.puTxFiFoData = uaUART2TxFiFoData;                        // 获取发送缓冲区的地址
    // 输出提示
    printf("UART2 初始化配置         %d-None-8-1; 已完成初始化配置、收发配置\r", ulBaudrate);
}

/******************************************************************************
 * 函  数: USART2_IRQHandler
 * 功  能: USART2的接收中断、空闲中断、发送中断
 * 参  数: 无
 * 返回值: 无
 * 备  注: 本函数,当产生中断事件时,由硬件调用。
 *          如果使用本文件代码,在工程文件的其它地方,要注释同名函数,否则冲突。
 ******************************************************************************/
void USART2_IRQHandler(void)
{
    static uint16_t cnt = 0;                                             // 接收字节数累计:每一帧数据已接收到的字节数
    static uint8_t  rxTemp[UART2_RX_BUF_SIZE];                           // 接收数据缓存数组:每新接收1个字节,先顺序存放到这里,当一帧接收完(发生空闲中断), 再转存到外部缓存:xUARTx.puRxData[ ]
                                                                         
    // 发送中断:用于把环形缓冲的数据,逐字节发出                        
    if ((USART2->SR & USART_SR_TXE) && (USART2->CR1 & USART_CR1_TXEIE))  // 检查发送寄存器空中断使能,且发送寄存器为空; TXE(发送数据寄存器空)、TXEIE(发送缓冲区空中断使能)
    {                                                                    
        USART2->DR = xUART2.puTxFiFoData[xUART2.usTxFiFoTail++];         // 从FIFO队列中取出一个数据,放入USART的发送寄存器(硬件会自动发出),然后将FIFO的尾指针递增,指向下一个要发送的数据
        if (xUART2.usTxFiFoTail == UART2_TX_BUF_SIZE)                    // 检查FIFO尾指针是否到达了FIFO队列的末尾
            xUART2.usTxFiFoTail = 0;                                     // 将尾指针重置为0,实现环形队列的功能
        if (xUART2.usTxFiFoTail == xUART2.usTxFiFoData)                  // 检查FIFO尾指针是否追上了头指针,即所有数据是否都已发送完毕
            USART2->CR1 &= ~USART_CR1_TXEIE;                             // 关闭发送寄存器空中断,防止中断服务程序被不必要地调用
        return;                                                          
    }                                                                    
                                                                         
    // 接收中断:用于逐个字节接收,存放到临时缓存                        
    if (USART2->SR & USART_SR_RXNE)                                      // 检查RXNE(读数据寄存器非空标志位); RXNE中断清理方法:读DR时自动清理;
    {                                                                    
        if (cnt == UART2_RX_BUF_SIZE)                                    // 当前帧已接收的字节量,已满缓存区的大小; 为避免溢出,本包后面接收到的数据直接舍弃;
        {
            printf("警告:UART2单帧接收量,已超出接收缓存大小!\r修改文件bsp_UART.h的UART2_RX_BUF_SIZE值,可解决此问题!\r");
            USART2->DR;                                                  // 读取数据寄存器的数据,但不保存.主要作用:读DR时自动清理接收中断标志;
            return;
        }
        rxTemp[cnt++] = USART2->DR ;                                     // 把新收到的字节数据,顺序存放到RXTemp数组中;注意:读取DR时自动清零中断位;
        return;
    }

    // 空闲中断:用于判断一帧数据结束,整理数据到外部备读
    if (USART2->SR & USART_SR_IDLE)                                      // 检查IDLE(空闲中断标志位); IDLE中断标志清理方法:序列清零,USART1 ->SR;  USART1 ->DR;
    {
        memcpy(xUART2.puRxData, rxTemp, UART2_RX_BUF_SIZE);              // 把本帧接收到的数据,存入到结构体的数组成员xUARTx.puRxData中, 等待处理; 注意:复制的是整个数组,包括0值,以方便字符串输出时尾部以0作字符串结束符
        xUART2.usRxNum  = cnt;                                           // 把接收到的字节数,存入到结构体变量xUARTx.usRxNum中;
        cnt = 0;                                                         // 接收字节数累计器,清零; 准备下一次的接收
        memset(rxTemp, 0, UART2_RX_BUF_SIZE);                            // 接收数据缓存数组,清零; 准备下一次的接收
        USART2 ->SR;
        USART2 ->DR;                                                     // 清零IDLE中断标志位!! 序列清零,顺序不能错!!
        return;
    }

    return;
}

/******************************************************************************
 * 函  数: UART2_SendData
 * 功  能: UART通过中断发送数据
 *         【适合场景】本函数可发送各种数据,而不限于字符串,如int,char
 *         【不 适 合】注意h文件中所定义的发缓冲区大小、注意数据压入缓冲区的速度与串口发送速度的冲突
 * 参  数: uint8_t* puData     需发送数据的地址
 *          uint8_t  usNum      发送的字节数 ,数量受限于h文件中设置的发送缓存区大小宏定义
 * 返回值: 无
 ******************************************************************************/
void UART2_SendData(uint8_t *puData, uint16_t usNum)
{
    for (uint16_t i = 0; i < usNum; i++)                         // 把数据放入环形缓冲区
    {
        xUART2.puTxFiFoData[xUART2.usTxFiFoData++] = puData[i];  // 把字节放到缓冲区最后的位置,然后指针后移
        if (xUART2.usTxFiFoData == UART2_TX_BUF_SIZE)            // 如果指针位置到达缓冲区的最大值,则归0
            xUART2.usTxFiFoData = 0;
    }

    if ((USART2->CR1 & USART_CR1_TXEIE) == 0)                    // 检查USART寄存器的发送缓冲区空置中断(TXEIE)是否已打开
        USART2->CR1 |= USART_CR1_TXEIE;                          // 打开TXEIE中断
}

/******************************************************************************
 * 函  数: UART2_SendString
 * 功  能: 发送字符串
 *          用法请参考printf,及示例中的展示
 *          注意,最大发送字节数为512-1个字符,可在函数中修改上限
 * 参  数: const char *pcString, ...   (如同printf的用法)
 * 返回值: 无
 ******************************************************************************/
void UART2_SendString(const char *pcString, ...)
{
    char mBuffer[512] = {0};;                               // 开辟一个缓存, 并把里面的数据置0
    va_list ap;                                             // 新建一个可变参数列表
    va_start(ap, pcString);                                 // 列表指向第一个可变参数
    vsnprintf(mBuffer, 512, pcString, ap);                  // 把所有参数,按格式,输出到缓存; 参数2用于限制发送的最大字节数,如果达到上限,则只发送上限值-1; 最后1字节自动置'\0'
    va_end(ap);                                             // 清空可变参数列表
    UART2_SendData((uint8_t *)mBuffer, strlen(mBuffer));    // 把字节存放环形缓冲,排队准备发送
}

/******************************************************************************
 * 函    数: UART2_SendAT
 * 功    能: 发送AT命令, 并等待返回信息
 * 参    数: char     *pcString      AT指令字符串
 *            char     *pcAckString   期待的指令返回信息字符串
 *            uint16_t  usTimeOut     发送命令后等待的时间,毫秒
 *
 * 返 回 值: 0-执行失败、1-执行正常
 ******************************************************************************/
uint8_t UART2_SendAT(char *pcAT, char *pcAckString, uint16_t usTimeOutMs)
{
    UART2_ClearRx();                                              // 清0
    UART2_SendString(pcAT);                                       // 发送AT指令字符串

    while (usTimeOutMs--)                                         // 判断是否起时(这里只作简单的循环判断次数处理)
    {
        if (UART2_GetRxNum())                                     // 判断是否接收到数据
        {
            UART2_ClearRx();                                      // 清0接收字节数; 注意:接收到的数据内容 ,是没有被清0的
            if (strstr((char *)UART2_GetRxData(), pcAckString))   // 判断返回数据中是否有期待的字符
                return 1;                                         // 返回:0-超时没有返回、1-正常返回期待值
        }
        delay_ms(1);                                              // 延时; 用于超时退出处理,避免死等
    }
    return 0;                                                     // 返回:0-超时、返回异常,1-正常返回期待值
}

/******************************************************************************
 * 函  数: UART2_GetRxNum
 * 功  能: 获取最新一帧数据的字节数
 * 参  数: 无
 * 返回值: 0=没有接收到数据,非0=新一帧数据的字节数
 ******************************************************************************/
uint16_t UART2_GetRxNum(void)
{
    return xUART2.usRxNum ;
}

/******************************************************************************
 * 函  数: UART2_GetRxData
 * 功  能: 获取最新一帧数据 (数据的地址)
 * 参  数: 无
 * 返回值: 数据的地址(uint8_t*)
 ******************************************************************************/
uint8_t *UART2_GetRxData(void)
{
    return xUART2.puRxData ;
}

/******************************************************************************
 * 函  数: UART2_ClearRx
 * 功  能: 清理最后一帧数据的缓存
 *          主要是清0字节数,因为它是用来判断接收的标准
 * 参  数: 无
 * 返回值: 无
 ******************************************************************************/
void UART2_ClearRx(void)
{
    xUART2.usRxNum = 0 ;
}
#endif  // endif UART2_EN





//   USART-3   //
/
#if UART3_EN

static xUSATR_TypeDef  xUART3 = { 0 };                      // 定义 UART3 的收发结构体
static uint8_t uaUart3RxData[UART3_RX_BUF_SIZE];            // 定义 UART3 的接收缓存
static uint8_t uaUart3TxFiFoData[UART3_TX_BUF_SIZE];        // 定义 UART3 的发送缓存

/******************************************************************************
 * 函  数: UART3_Init
 * 功  能: 初始化USART3的通信引脚、协议参数、中断优先级
 *          引脚:TX-PB10、RX-PB11
 *          协议:波特率-None-8-1
 *          发送:发送中断
 *          接收:接收+空闲中断
 *
 * 参  数: uint32_t ulBaudrate  通信波特率
 * 返回值: 无
 ******************************************************************************/
void UART3_Init(uint32_t ulBaudrate)
{
#ifdef USE_STDPERIPH_DRIVER
    // 使能 时钟
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);           // 使能 GPIOB  时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3, ENABLE);          // 使能 USART3 时钟              
    // 配置 引脚的复用功能                                       
    GPIO_PinAFConfig(GPIOB, GPIO_PinSource10, GPIO_AF_USART3);      // 配置PB10复用功能:USART3
    GPIO_PinAFConfig(GPIOB, GPIO_PinSource11, GPIO_AF_USART3);      // 配置PB11复用功能:USART3
    // 重置 USART                                                
    RCC_APB1PeriphResetCmd(RCC_APB1Periph_USART3, ENABLE);          // 使能重置
    RCC_APB1PeriphResetCmd(RCC_APB1Periph_USART3, DISABLE);         // 取消重置
    // 配置 TX引脚                                                   
    GPIO_InitTypeDef  GPIO_InitStructure = {0};                     // GPIO 初始化结构体
    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_10;                    // 引脚编号
    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_AF;                   // 引脚方向: 复用功能
    GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;                  // 输出模式:推挽
    GPIO_InitStructure.GPIO_PuPd  = GPIO_PuPd_UP;                   // 上下拉:上拉
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;               // 输出速度:50MHz
    GPIO_Init(GPIOB, &GPIO_InitStructure);                          // 初始化:把上述参数,更新到芯片寄存器
    // 配置 RX引脚                                                   
    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_11;                    // 引脚编号
    GPIO_Init(GPIOB, &GPIO_InitStructure);                          // 初始化:把上述参数,更新到芯片寄存器
    // 配置 UART                                                   
    USART_InitTypeDef USART_InitStructure;                          // 声明USART初始化结构体  
    USART_InitStructure.USART_BaudRate = ulBaudrate;                // 设置波特率
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;     // 设置字长为8位数据格式
    USART_InitStructure.USART_StopBits = USART_StopBits_1;          // 设置一个停止位
    USART_InitStructure.USART_Parity = USART_Parity_No;             // 无奇偶校验位
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; // 无硬件数据流控制
    USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; // 收发模式
    USART_Init(USART3, &USART_InitStructure);                       // 初始化USART
    // 配置 中断
    USART_ITConfig(USART3, USART_IT_RXNE, ENABLE);                  // 开启 接收中断
    USART_ITConfig(USART3, USART_IT_IDLE, ENABLE);                  // 开启 空闲中断  
    // 配置 中断优先级
    NVIC_InitTypeDef  NVIC_InitStructure = {0};                     // 中断优先级配置结构体
    NVIC_InitStructure .NVIC_IRQChannel = USART3_IRQn;              // 指定中断通道
    NVIC_InitStructure .NVIC_IRQChannelPreemptionPriority = 1;      // 设置抢占优先级
    NVIC_InitStructure .NVIC_IRQChannelSubPriority = 1;             // 设置响应优先级
    NVIC_InitStructure .NVIC_IRQChannelCmd = ENABLE;                // 使能中断通道
    NVIC_Init(&NVIC_InitStructure);                                 // 初始化NVIC
    // 配置完成,开启USART
    USART_Cmd(USART3, ENABLE);                                      // 使能USART
#endif

#ifdef USE_HAL_DRIVER                                               // HAL库 配置
    // 使能 时钟                                                    
    __HAL_RCC_GPIOB_CLK_ENABLE();                                   // 使能GPIOB
    __HAL_RCC_USART3_CLK_ENABLE();                                  // 使能USART3
    // 重置 USART                                                   
    __HAL_RCC_USART3_FORCE_RESET();                                 // 使能重置
    __HAL_RCC_USART3_RELEASE_RESET();                               // 取消重置
    // 配置 GPIO引脚                                                
    GPIO_InitTypeDef    GPIO_InitStruct = {0};                      // 声明初始化要用到的结构体
    GPIO_InitStruct.Pin   = GPIO_PIN_10 | GPIO_PIN_11;              // 引脚 TX-PB10、RX-PB11
    GPIO_InitStruct.Mode  = GPIO_MODE_AF_PP;                        // 工作模式
    GPIO_InitStruct.Pull  = GPIO_PULLUP;                            // 上下拉
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;              // 引脚速率
    GPIO_InitStruct.Alternate = GPIO_AF7_USART3;                    // 引脚复用功能
    HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);                         // 初始化引脚工作模式
    // 计算波特率参数                                               
    float    temp;                                                  
    uint16_t mantissa, fraction;                                    
    SystemCoreClockUpdate();                                        // 更新系统运行频率全局值; 函数SystemCoreClock( ),在标准库、HAL库通用
    temp = (float)(SystemCoreClock / 4) / (ulBaudrate * 16);        // 波特率公式计算; USART3挂载在APB1, 时钟为系统时钟的4分频; 全局变量SystemCoreClock,在标准库、HAL库通用;
    mantissa = temp;				                                // 整数部分
    fraction = (temp - mantissa) * 16;                              // 小数部分
    USART3 -> BRR  = mantissa << 4 | fraction;                      // 设置波特率
    // 配置 UART
    USART3 -> CR1  =   0;                                           // 清0
    USART3 -> CR1 |=   0x01 << 2;                                   // 接收使能[02]: 0=失能、1=使能
    USART3 -> CR1 |=   0x01 << 3;                                   // 发送使能[03]:0=失能、1=使能
    USART3 -> CR1 |=   0x00 << 9;                                   // 奇偶校验[09]:0=偶Even、1=奇Odd;  注意:使用奇偶校验,下面两项要置1
    USART3 -> CR1 |=   0x00 << 10;                                  // 校验位  [10]:0=禁用、1=使能;     注意,使用奇偶校验,此位要置1 (否则无效、数据错乱)
    USART3 -> CR1 |=   0x00 << 12;                                  // 数据位  [12]:0=8位、 1=9位;      注意:使用奇偶校验,此位要置1 (否则数据会发生错乱)
    USART3 -> CR2  =   0;                                           // 数据清0
    USART3 -> CR2 &= ~(0x03 << 12);                                 // 停止位[13:12]:00b=1个停止位、01b=0.5个停止位、10b=2个停止位、11b=1.5个停止位
    USART3 -> CR3  =   0;                                           // 数据清0
    USART3 -> CR3 &= ~(0x01 << 6);                                  // DMA接收[6]: 0=禁止、1=使能
    USART3 -> CR3 &= ~(0x01 << 7);                                  // DMA发送[7]: 0=禁止、1=使能
    // 配置中断                                                     
    USART3 -> CR1 &= ~(0x01 << 7);                                  // 关闭发送中断
    USART3 -> CR1 |=   0x01 << 5;                                   // 使能接收中断: 接收缓冲区非空
    USART3 -> CR1 |=   0x01 << 4;                                   // 使能空闲中断:超过1字节时间没收到新数据
    USART3 -> SR   = ~(0x00F0);                                     // 清理一次中断标志
    // 配置 中断优先级                                              
    HAL_NVIC_SetPriority(USART3_IRQn, 1, 1);                        // 设置指定中断的响应优先级;  参数:中断请求编号、抢占级、子优先级
    HAL_NVIC_EnableIRQ(USART3_IRQn);                                // 使能、启用指定的中断
    // 配置完成,开启USART                                          
    USART3 -> CR1 |=   0x01 << 13;                                  // 使能UART开始工作
#endif                                                              
    // 关联缓冲区                                                   
    xUART3.puRxData = uaUart3RxData;                                // 获取接收缓冲区的地址
    xUART3.puTxFiFoData = uaUart3TxFiFoData;                        // 获取发送缓冲区的地址
    // 输出提示
    printf("UART3 初始化配置         %d-None-8-1; 已完成初始化配置、收发配置\r", ulBaudrate);
}

/******************************************************************************
 * 函  数: USART3_IRQHandler
 * 功  能: USART3的接收中断、空闲中断、发送中断
 * 参  数: 无
 * 返回值: 无
 * 备  注: 本函数,当产生中断事件时,由硬件调用。
 *          如果使用本文件代码,在工程文件的其它地方,要注释同名函数,否则冲突。
 ******************************************************************************/
void USART3_IRQHandler(void)
{
    static uint16_t cnt = 0;                                             // 接收字节数累计:每一帧数据已接收到的字节数
    static uint8_t  rxTemp[UART3_RX_BUF_SIZE];                           // 接收数据缓存数组:每新接收1个字节,先顺序存放到这里,当一帧接收完(发生空闲中断), 再转存到外部缓存:xUARTx.puRxData[ ]
                                                                         
    // 发送中断:用于把环形缓冲的数据,逐字节发出                        
    if ((USART3->SR & USART_SR_TXE) && (USART3->CR1 & USART_CR1_TXEIE))  // 检查发送寄存器空中断使能,且发送寄存器为空; TXE(发送数据寄存器空)、TXEIE(发送缓冲区空中断使能)
    {                                                                    
        USART3->DR = xUART3.puTxFiFoData[xUART3.usTxFiFoTail++];         // 从FIFO队列中取出一个数据,放入USART的发送寄存器(硬件会自动发出),然后将FIFO的尾指针递增,指向下一个要发送的数据
        if (xUART3.usTxFiFoTail == UART3_TX_BUF_SIZE)                    // 检查FIFO尾指针是否到达了FIFO队列的末尾
            xUART3.usTxFiFoTail = 0;                                     // 将尾指针重置为0,实现环形队列的功能
        if (xUART3.usTxFiFoTail == xUART3.usTxFiFoData)                  // 检查FIFO尾指针是否追上了头指针,即所有数据是否都已发送完毕
            USART3->CR1 &= ~USART_CR1_TXEIE;                             // 关闭发送寄存器空中断,防止中断服务程序被不必要地调用
        return;                                                          
    }                                                                    
                                                                         
    // 接收中断:用于逐个字节接收,存放到临时缓存                        
    if (USART3->SR & USART_SR_RXNE)                                      // 检查RXNE(读数据寄存器非空标志位); RXNE中断清理方法:读DR时自动清理;
    {                                                                    
        if (cnt == UART3_RX_BUF_SIZE)                                    // 当前帧已接收的字节量,已满缓存区的大小; 为避免溢出,本包后面接收到的数据直接舍弃;
        {
            printf("警告:UART3单帧接收量,已超出接收缓存大小!\r修改文件bsp_UART.h的UART3_RX_BUF_SIZE值,可解决此问题!\r");
            USART3->DR;                                                  // 读取数据寄存器的数据,但不保存.主要作用:读DR时自动清理接收中断标志;
            return;
        }
        rxTemp[cnt++] = USART3->DR ;                                     // 把新收到的字节数据,顺序存放到RXTemp数组中;注意:读取DR时自动清零中断位
        return;
    }

    // 空闲中断:用于判断一帧数据结束,整理数据到外部备读
    if (USART3->SR & USART_SR_IDLE)                                      // 检查IDLE(空闲中断标志位); IDLE中断标志清理方法:序列清零,USART1 ->SR;  USART1 ->DR;
    {
        memcpy(xUART3.puRxData, rxTemp, UART3_RX_BUF_SIZE);              // 把本帧接收到的数据,存入到结构体的数组成员xUARTx.puRxData中, 等待处理; 注意:复制的是整个数组,包括0值,以方便字符串输出时尾部以0作字符串结束符
        xUART3.usRxNum  = cnt;                                           // 把接收到的字节数,存入到结构体变量xUARTx.usRxNum中;
        cnt = 0;                                                         // 接收字节数累计器,清零; 准备下一次的接收
        memset(rxTemp, 0, UART3_RX_BUF_SIZE);                            // 接收数据缓存数组,清零; 准备下一次的接收
        USART3 ->SR;
        USART3 ->DR;                                                     // 清零IDLE中断标志位!! 序列清零,顺序不能错!!
        return;
    }

    return;
}

/******************************************************************************
 * 函  数: UART3_SendData
 * 功  能: UART通过中断发送数据
 *         【适合场景】本函数可发送各种数据,而不限于字符串,如int,char
 *         【不 适 合】注意h文件中所定义的发缓冲区大小、注意数据压入缓冲区的速度与串口发送速度的冲突
 * 参  数: uint8_t* puData   需发送数据的地址
 *          uint8_t  usNum      发送的字节数 ,数量受限于h文件中设置的发送缓存区大小宏定义
 * 返回值: 无
 ******************************************************************************/
void UART3_SendData(uint8_t *puData, uint16_t usNum)
{
    for (uint16_t i = 0; i < usNum; i++)                           // 把数据放入环形缓冲区
    {
        xUART3.puTxFiFoData[xUART3.usTxFiFoData++] = puData[i];    // 把字节放到缓冲区最后的位置,然后指针后移
        if (xUART3.usTxFiFoData == UART3_TX_BUF_SIZE)              // 如果指针位置到达缓冲区的最大值,则归0
            xUART3.usTxFiFoData = 0;
    }

    if ((USART3->CR1 & USART_CR1_TXEIE) == 0)                      // 检查USART寄存器的发送缓冲区空置中断(TXEIE)是否已打开
        USART3->CR1 |= USART_CR1_TXEIE;                            // 打开TXEIE中断
}

/******************************************************************************
 * 函  数: UART3_SendString
 * 功  能: 发送字符串
 *          用法请参考printf,及示例中的展示
 *          注意,最大发送字节数为512-1个字符,可在函数中修改上限
 * 参  数: const char *pcString, ...   (如同printf的用法)
 * 返回值: 无
 ******************************************************************************/
void UART3_SendString(const char *pcString, ...)
{
    char mBuffer[512] = {0};;                                // 开辟一个缓存, 并把里面的数据置0
    va_list ap;                                              // 新建一个可变参数列表
    va_start(ap, pcString);                                  // 列表指向第一个可变参数
    vsnprintf(mBuffer, 512, pcString, ap);                   // 把所有参数,按格式,输出到缓存; 参数2用于限制发送的最大字节数,如果达到上限,则只发送上限值-1; 最后1字节自动置'\0'
    va_end(ap);                                              // 清空可变参数列表
    UART3_SendData((uint8_t *)mBuffer, strlen(mBuffer));     // 把字节存放环形缓冲,排队准备发送
}

/******************************************************************************
 * 函    数: UART3_SendAT
 * 功    能: 发送AT命令, 并等待返回信息
 * 参    数: char     *pcString      AT指令字符串
 *            char     *pcAckString   期待的指令返回信息字符串
 *            uint16_t  usTimeOut     发送命令后等待的时间,毫秒
 *
 * 返 回 值: 0-执行失败、1-执行正常
 ******************************************************************************/
uint8_t UART3_SendAT(char *pcAT, char *pcAckString, uint16_t usTimeOutMs)
{
    UART3_ClearRx();                                              // 清0
    UART3_SendString(pcAT);                                       // 发送AT指令字符串

    while (usTimeOutMs--)                                         // 判断是否起时(这里只作简单的循环判断次数处理)
    {
        if (UART3_GetRxNum())                                     // 判断是否接收到数据
        {
            UART3_ClearRx();                                      // 清0接收字节数; 注意:接收到的数据内容 ,是没有被清0的
            if (strstr((char *)UART3_GetRxData(), pcAckString))   // 判断返回数据中是否有期待的字符
                return 1;                                         // 返回:0-超时没有返回、1-正常返回期待值
        }
        delay_ms(1);                                              // 延时; 用于超时退出处理,避免死等
    }
    return 0;                                                     // 返回:0-超时、返回异常,1-正常返回期待值
}

/******************************************************************************
 * 函  数: UART3_GetRxNum
 * 功  能: 获取最新一帧数据的字节数
 * 参  数: 无
 * 返回值: 0=没有接收到数据,非0=新一帧数据的字节数
 ******************************************************************************/
uint16_t UART3_GetRxNum(void)
{
    return xUART3.usRxNum ;
}

/******************************************************************************
 * 函  数: UART3_GetRxData
 * 功  能: 获取最新一帧数据 (数据的地址)
 * 参  数: 无
 * 返回值: 数据的地址(uint8_t*)
 ******************************************************************************/
uint8_t *UART3_GetRxData(void)
{
    return xUART3.puRxData ;
}

/******************************************************************************
 * 函  数: UART3_ClearRx
 * 功  能: 清理最后一帧数据的缓存
 *          主要是清0字节数,因为它是用来判断接收的标准
 * 参  数: 无
 * 返回值: 无
 ******************************************************************************/
void UART3_ClearRx(void)
{
    xUART3.usRxNum = 0 ;
}
#endif  // endif UART3_EN





//   UART-4   //
/
#if UART4_EN

static xUSATR_TypeDef  xUART4 = { 0 };                      // 定义 UART4 的收发结构体
static uint8_t uaUart4RxData[UART4_RX_BUF_SIZE];            // 定义 UART4 的接收缓存
static uint8_t uaUart4TxFiFoData[UART4_TX_BUF_SIZE];        // 定义 UART4 的发送缓存

/******************************************************************************
 * 函  数: UART4_Init
 * 功  能: 初始化UART4的通信引脚、协议参数、中断优先级
 *          引脚:TX-PC10、RX-PC11
 *          协议:波特率-None-8-1
 *          发送:发送中断
 *          接收:接收+空闲中断
 *
 * 参  数: uint32_t ulBaudrate  通信波特率
 * 返回值: 无
 ******************************************************************************/
void UART4_Init(uint32_t ulBaudrate)
{
#ifdef USE_STDPERIPH_DRIVER
    // 使能 时钟
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOC, ENABLE);           // 使能 GPIOC 时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_UART4, ENABLE);           // 使能 UART4 时钟              
    // 配置 引脚复用功能                                       
    GPIO_PinAFConfig(GPIOC, GPIO_PinSource10, GPIO_AF_UART4);       // 配置PC10复用功能:UART4
    GPIO_PinAFConfig(GPIOC, GPIO_PinSource11, GPIO_AF_UART4);       // 配置PC11复用功能:UART4
    // 重置 USART                                                
    RCC_APB1PeriphResetCmd(RCC_APB1Periph_UART4, ENABLE);           // 使能重置
    RCC_APB1PeriphResetCmd(RCC_APB1Periph_UART4, DISABLE);          // 取消重置
    // 配置 TX引脚                                                   
    GPIO_InitTypeDef  GPIO_InitStructure = {0};                     // GPIO 初始化结构体
    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_10;                    // 引脚编号
    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_AF;                   // 引脚方向: 复用功能
    GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;                  // 输出模式:推挽
    GPIO_InitStructure.GPIO_PuPd  = GPIO_PuPd_UP;                   // 上下拉:上拉
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;               // 输出速度:50MHz
    GPIO_Init(GPIOC, &GPIO_InitStructure);                          // 初始化:把上述参数,更新到芯片寄存器
    // 配置 RX引脚                                                   
    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_11;                    // 引脚编号
    GPIO_Init(GPIOC, &GPIO_InitStructure);                          // 初始化:把上述参数,更新到芯片寄存器
    // 配置 UART                                                   
    USART_InitTypeDef USART_InitStructure;                          // 声明USART初始化结构体  
    USART_InitStructure.USART_BaudRate = ulBaudrate;                // 设置波特率
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;     // 设置字长为8位数据格式
    USART_InitStructure.USART_StopBits = USART_StopBits_1;          // 设置一个停止位
    USART_InitStructure.USART_Parity = USART_Parity_No;             // 无奇偶校验位
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; // 无硬件数据流控制
    USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; // 收发模式
    USART_Init(UART4, &USART_InitStructure);                        // 初始化USART       
    // 配置 中断
    USART_ITConfig(UART4, USART_IT_RXNE, ENABLE);                   // 开启 接收中断
    USART_ITConfig(UART4, USART_IT_IDLE, ENABLE);                   // 开启 空闲中断  
    // 配置 中断优先级
    NVIC_InitTypeDef  NVIC_InitStructure = {0};                     // 中断优先级配置结构体
    NVIC_InitStructure .NVIC_IRQChannel = UART4_IRQn;               // 指定中断通道
    NVIC_InitStructure .NVIC_IRQChannelPreemptionPriority = 1;      // 设置抢占优先级
    NVIC_InitStructure .NVIC_IRQChannelSubPriority = 1;             // 设置响应优先级
    NVIC_InitStructure .NVIC_IRQChannelCmd = ENABLE;                // 使能中断通道
    NVIC_Init(&NVIC_InitStructure);                                 // 初始化NVIC
    // 配置完成,开启USART
    USART_Cmd(UART4, ENABLE);                                       // 使能USART
#endif

#ifdef USE_HAL_DRIVER                                               // HAL库 配置
    // 使能 时钟                                                   
    __HAL_RCC_GPIOC_CLK_ENABLE();                                   // 使能GPIOC
    __HAL_RCC_UART4_CLK_ENABLE();                                   // 使能UART4
    // 重置 USART                                                  
    __HAL_RCC_UART4_FORCE_RESET();                                  // 使能重置
    __HAL_RCC_UART4_RELEASE_RESET();                                // 取消重置
    // 配置 GPIO引脚                                               
    GPIO_InitTypeDef    GPIO_InitStruct = {0};                      // 声明初始化要用到的结构体
    GPIO_InitStruct.Pin   = GPIO_PIN_10 | GPIO_PIN_11;              // 引脚 TX-PC10、RX-PC11
    GPIO_InitStruct.Mode  = GPIO_MODE_AF_PP;                        // 工作模式
    GPIO_InitStruct.Pull  = GPIO_PULLUP;                            // 上下拉
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;              // 引脚速率
    GPIO_InitStruct.Alternate = GPIO_AF8_UART4;                     // 引脚复用功能
    HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);                         // 初始化引脚工作模式
    // 计算波特率参数                                              
    float    temp;
    uint16_t mantissa, fraction;
    SystemCoreClockUpdate();                                        // 更新系统运行频率全局值; 函数SystemCoreClock( ),在标准库、HAL库通用
    temp = (float)(SystemCoreClock / 4) / (ulBaudrate * 16);        // 波特率公式计算; UART4挂载在APB1, 时钟为系统时钟的4分频; 全局变量SystemCoreClock,在标准库、HAL库通用;
    mantissa = temp;				                                // 整数部分
    fraction = (temp - mantissa) * 16;                              // 小数部分
    UART4 -> BRR  = mantissa << 4 | fraction;                       // 设置波特率
    // 配置 UART                                                   
    UART4 -> CR1  =   0;                                            // 清0
    UART4 -> CR1 |=   0x01 << 2;                                    // 接收使能[02]: 0=失能、1=使能
    UART4 -> CR1 |=   0x01 << 3;                                    // 发送使能[03]:0=失能、1=使能
    UART4 -> CR1 |=   0x00 << 9;                                    // 奇偶校验[09]:0=偶Even、1=奇Odd;  注意:使用奇偶校验,下面两项要置1
    UART4 -> CR1 |=   0x00 << 10;                                   // 校验位  [10]:0=禁用、1=使能;     注意,使用奇偶校验,此位要置1 (否则无效、数据错乱)
    UART4 -> CR1 |=   0x00 << 12;                                   // 数据位  [12]:0=8位、 1=9位;      注意:使用奇偶校验,此位要置1 (否则数据会发生错乱)
    UART4 -> CR2  =   0;                                            // 数据清0
    UART4 -> CR2 &= ~(0x03 << 12);                                  // 停止位[13:12]:00b=1个停止位、01b=0.5个停止位、10b=2个停止位、11b=1.5个停止位
    UART4 -> CR3  =   0;                                            // 数据清0
    UART4 -> CR3 &= ~(0x01 << 6);                                   // DMA接收[6]: 0=禁止、1=使能
    UART4 -> CR3 &= ~(0x01 << 7);                                   // DMA发送[7]: 0=禁止、1=使能
    // 配置 中断                                                   
    UART4 -> CR1 &= ~(0x01 << 7);                                   // 关闭发送中断
    UART4 -> CR1 |=   0x01 << 5;                                    // 使能接收中断: 接收缓冲区非空
    UART4 -> CR1 |=   0x01 << 4;                                    // 使能空闲中断:超过1字节时间没收到新数据
    UART4 -> SR   = ~(0x00F0);                                      // 清理中断
    // 配置 中断优选级                                             
    HAL_NVIC_SetPriority(UART4_IRQn, 1, 1);                         // 设置指定中断的响应优先级;  参数:中断请求编号、抢占级、子优先级
    HAL_NVIC_EnableIRQ(UART4_IRQn);                                 // 使能、启用指定的中断
    // 配置完成,打开串口                                                    
    UART4 -> CR1 |=   0x01 << 13;                                   // 使能UART开始工作
#endif                                                             
    // 关联缓冲区                                                  
    xUART4.puRxData = uaUart4RxData;                                // 获取接收缓冲区的地址
    xUART4.puTxFiFoData = uaUart4TxFiFoData;                        // 获取发送缓冲区的地址
    // 输出提示
    printf("UART4 初始化配置         %d-None-8-1; 已完成初始化配置、收发配置\r", ulBaudrate);
}

/******************************************************************************
 * 函  数: UART4_IRQHandler
 * 功  能: UART4的中断处理函数
 *          接收中断、空闲中断、发送中断
 * 参  数: 无
 * 返回值: 无
 * 备  注: 本函数,当产生中断事件时,由硬件调用。
 *          如果使用本文件代码,在工程文件的其它地方,要注释同名函数,否则冲突。
 ******************************************************************************/
void UART4_IRQHandler(void)
{
    static uint16_t cnt = 0;                                           // 接收字节数累计:每一帧数据已接收到的字节数
    static uint8_t  rxTemp[UART4_RX_BUF_SIZE];                         // 接收数据缓存数组:每新接收1个字节,先顺序存放到这里,当一帧接收完(发生空闲中断), 再转存到外部缓存:xUARTx.puRxData[ ]
                                                                       
    // 发送中断:用于把环形缓冲的数据,逐字节发出                      
    if ((UART4->SR & USART_SR_TXE) && (UART4->CR1 & USART_CR1_TXEIE))  // 检查发送寄存器空中断使能,且发送寄存器为空; TXE(发送数据寄存器空)、TXEIE(发送缓冲区空中断使能)
    {                                                                  
        UART4->DR = xUART4.puTxFiFoData[xUART4.usTxFiFoTail++];        // 从FIFO队列中取出一个数据,放入USART的发送寄存器(硬件会自动发出),然后将FIFO的尾指针递增,指向下一个要发送的数据
        if (xUART4.usTxFiFoTail == UART4_TX_BUF_SIZE)                  // 检查FIFO尾指针是否到达了FIFO队列的末尾
            xUART4.usTxFiFoTail = 0;                                   // 将尾指针重置为0,实现环形队列的功能
        if (xUART4.usTxFiFoTail == xUART4.usTxFiFoData)                // 检查FIFO尾指针是否追上了头指针,即所有数据是否都已发送完毕
            UART4->CR1 &= ~USART_CR1_TXEIE;                            // 关闭发送寄存器空中断,防止中断服务程序被不必要地调用
        return;                                                        
    }                                                                  
                                                                       
    // 接收中断:用于逐个字节接收,存放到临时缓存                      
    if (UART4->SR & USART_SR_RXNE)                                     // 检查RXNE(读数据寄存器非空标志位); RXNE中断清理方法:读DR时自动清理;
    {                                                                  
        if (cnt == UART4_RX_BUF_SIZE)                                  // 当前帧已接收的字节量,已满缓存区的大小; 为避免溢出,本包后面接收到的数据直接舍弃;
        {           
            printf("警告:UART4单帧接收量,已超出接收缓存大小!\r修改文件bsp_UART.h的UART4_RX_BUF_SIZE值,可解决此问题!\r");
            UART4->DR;                                                 // 读取数据寄存器的数据,但不保存.主要作用:读DR时自动清理接收中断标志;
            return;                                                    
        }                                                              
        rxTemp[cnt++] = UART4->DR ;                                    // 把新收到的字节数据,顺序存放到RXTemp数组中;注意:读取DR时自动清零中断位
        return;                                                        
    }                                                                  
                                                                       
    // 空闲中断:用于判断一帧数据结束,整理数据到外部备读              
    if (UART4->SR & USART_SR_IDLE)                                     // 检查IDLE(空闲中断标志位); IDLE中断标志清理方法:序列清零,USART1 ->SR;  USART1 ->DR;
    {                                                                  
        memcpy(xUART4.puRxData, rxTemp, UART4_RX_BUF_SIZE);            // 把本帧接收到的数据,存入到结构体的数组成员xUARTx.puRxData中, 等待处理; 注意:复制的是整个数组,包括0值,以方便字符串输出时尾部以0作字符串结束符
        xUART4.usRxNum  = cnt;                                         // 把接收到的字节数,存入到结构体变量xUARTx.usRxNum中;
        cnt = 0;                                                       // 接收字节数累计器,清零; 准备下一次的接收
        memset(rxTemp, 0, UART4_RX_BUF_SIZE);                          // 接收数据缓存数组,清零; 准备下一次的接收
        UART4 ->SR;                                                    
        UART4 ->DR;                                                    // 清零IDLE中断标志位!! 序列清零,顺序不能错!!
        return;                                                        
    }

    return;
}

/******************************************************************************
 * 函  数: UART4_SendData
 * 功  能: UART通过中断发送数据
 *         【适合场景】本函数可发送各种数据,而不限于字符串,如int,char
 *         【不 适 合】注意h文件中所定义的发缓冲区大小、注意数据压入缓冲区的速度与串口发送速度的冲突
 * 参  数: uint8_t* puData   需发送数据的地址
 *          uint8_t  usNum    发送的字节数 ,数量受限于h文件中设置的发送缓存区大小宏定义
 * 返回值: 无
 ******************************************************************************/
void UART4_SendData(uint8_t *puData, uint16_t usNum)
{
    for (uint16_t i = 0; i < usNum; i++)                         // 把数据放入环形缓冲区
    {
        xUART4.puTxFiFoData[xUART4.usTxFiFoData++] = puData[i];  // 把字节放到缓冲区最后的位置,然后指针后移
        if (xUART4.usTxFiFoData == UART4_TX_BUF_SIZE)            // 如果指针位置到达缓冲区的最大值,则归0
            xUART4.usTxFiFoData = 0;
    }

    if ((UART4->CR1 & USART_CR1_TXEIE) == 0)                     // 检查USART寄存器的发送缓冲区空置中断(TXEIE)是否已打开
        UART4->CR1 |= USART_CR1_TXEIE;                           // 打开TXEIE中断
}

/******************************************************************************
 * 函  数: UART4_SendString
 * 功  能: 发送字符串
 *          用法请参考printf,及示例中的展示
 *          注意,最大发送字节数为512-1个字符,可在函数中修改上限
 * 参  数: const char *pcString, ...   (如同printf的用法)
 * 返回值: 无
 ******************************************************************************/
void UART4_SendString(const char *pcString, ...)
{
    char mBuffer[512] = {0};;                                // 开辟一个缓存, 并把里面的数据置0
    va_list ap;                                              // 新建一个可变参数列表
    va_start(ap, pcString);                                  // 列表指向第一个可变参数
    vsnprintf(mBuffer, 512, pcString, ap);                   // 把所有参数,按格式,输出到缓存; 参数2用于限制发送的最大字节数,如果达到上限,则只发送上限值-1; 最后1字节自动置'\0'
    va_end(ap);                                              // 清空可变参数列表
    UART4_SendData((uint8_t *)mBuffer, strlen(mBuffer));     // 把字节存放环形缓冲,排队准备发送
}

/******************************************************************************
 * 函    数: UART4_SendAT
 * 功    能: 发送AT命令, 并等待返回信息
 * 参    数: char     *pcString      AT指令字符串
 *            char     *pcAckString   期待的指令返回信息字符串
 *            uint16_t  usTimeOut     发送命令后等待的时间,毫秒
 *
 * 返 回 值: 0-执行失败、1-执行正常
 ******************************************************************************/
uint8_t UART4_SendAT(char *pcAT, char *pcAckString, uint16_t usTimeOutMs)
{
    UART4_ClearRx();                                              // 清0
    UART4_SendString(pcAT);                                       // 发送AT指令字符串

    while (usTimeOutMs--)                                         // 判断是否起时(这里只作简单的循环判断次数处理)
    {
        if (UART4_GetRxNum())                                     // 判断是否接收到数据
        {
            UART4_ClearRx();                                      // 清0接收字节数; 注意:接收到的数据内容 ,是没有被清0的
            if (strstr((char *)UART4_GetRxData(), pcAckString))   // 判断返回数据中是否有期待的字符
                return 1;                                         // 返回:0-超时没有返回、1-正常返回期待值
        }
        delay_ms(1);                                              // 延时; 用于超时退出处理,避免死等
    }
    return 0;                                                     // 返回:0-超时、返回异常,1-正常返回期待值
}

/******************************************************************************
 * 函  数: UART4_GetRxNum
 * 功  能: 获取最新一帧数据的字节数
 * 参  数: 无
 * 返回值: 0=没有接收到数据,非0=新一帧数据的字节数
 ******************************************************************************/
uint16_t UART4_GetRxNum(void)
{
    return xUART4.usRxNum ;
}

/******************************************************************************
 * 函  数: UART4_GetRxData
 * 功  能: 获取最新一帧数据 (数据的地址)
 * 参  数: 无
 * 返回值: 数据的地址(uint8_t*)
 ******************************************************************************/
uint8_t *UART4_GetRxData(void)
{
    return xUART4.puRxData ;
}

/******************************************************************************
 * 函  数: UART4_ClearRx
 * 功  能: 清理最后一帧数据的缓存
 *          主要是清0字节数,因为它是用来判断接收的标准
 * 参  数: 无
 * 返回值: 无
 ******************************************************************************/
void UART4_ClearRx(void)
{
    xUART4.usRxNum = 0 ;
}
#endif  // endif UART4_EN




//   UART-5   //
/
#if UART5_EN

static xUSATR_TypeDef  xUART5 = { 0 };                      // 定义 UART5 的收发结构体
static uint8_t uaUart5RxData[UART5_RX_BUF_SIZE];            // 定义 UART5 的接收缓存
static uint8_t uaUart5TxFiFoData[UART5_TX_BUF_SIZE];        // 定义 UART5 的发送缓存

/******************************************************************************
 * 函  数: UART5_Init
 * 功  能: 初始化UART5的通信引脚、协议参数、中断优先级
 *          引脚:TX-PC12、RX-PD2
 *          协议:波特率-None-8-1
 *          发送:发送中断
 *          接收:接收+空闲中断
 *
 * 参  数: uint32_t ulBaudrate  通信波特率
 * 返回值: 无
 ******************************************************************************/
void UART5_Init(uint32_t ulBaudrate)
{
#ifdef USE_STDPERIPH_DRIVER
    // 使能 时钟
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOC, ENABLE);           // 使能 GPIOC 时钟
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOD, ENABLE);           // 使能 GPIOD 时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_UART5, ENABLE);           // 使能 UART5 时钟              
    // 配置 引脚复用功能                                       
    GPIO_PinAFConfig(GPIOC, GPIO_PinSource12, GPIO_AF_UART5);       // 配置PC12复用功能:UART5
    GPIO_PinAFConfig(GPIOD, GPIO_PinSource2, GPIO_AF_UART5);        // 配置PD2复用功能 :UART5
    // 重置 USART                                                
    RCC_APB1PeriphResetCmd(RCC_APB1Periph_UART5, ENABLE);           // 使能重置
    RCC_APB1PeriphResetCmd(RCC_APB1Periph_UART5, DISABLE);          // 取消重置
    // 配置 TX引脚                                                   
    GPIO_InitTypeDef  GPIO_InitStructure = {0};                     // GPIO 初始化结构体
    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_12;                    // 引脚编号
    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_AF;                   // 引脚方向: 复用功能
    GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;                  // 输出模式:推挽
    GPIO_InitStructure.GPIO_PuPd  = GPIO_PuPd_UP;                   // 上下拉:上拉
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;               // 输出速度:50MHz
    GPIO_Init(GPIOC, &GPIO_InitStructure);                          // 初始化:把上述参数,更新到芯片寄存器
    // 配置 RX引脚                                                   
    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_2;                     // 引脚编号
    GPIO_Init(GPIOD, &GPIO_InitStructure);                          // 初始化:把上述参数,更新到芯片寄存器
    // 配置UART                                                   
    USART_InitTypeDef USART_InitStructure;                          // 声明USART初始化结构体  
    USART_InitStructure.USART_BaudRate = ulBaudrate;                // 设置波特率
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;     // 设置字长为8位数据格式
    USART_InitStructure.USART_StopBits = USART_StopBits_1;          // 设置一个停止位
    USART_InitStructure.USART_Parity = USART_Parity_No;             // 无奇偶校验位
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; // 无硬件数据流控制
    USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; // 收发模式
    USART_Init(UART5, &USART_InitStructure);                        // 初始化USART
    // 配置 中断
    USART_ITConfig(UART5, USART_IT_RXNE, ENABLE);                   // 开启 接收中断
    USART_ITConfig(UART5, USART_IT_IDLE, ENABLE);                   // 开启 空闲中断  
    // 中断优先级配置
    NVIC_InitTypeDef  NVIC_InitStructure = {0};                     // 中断优先级配置结构体
    NVIC_InitStructure .NVIC_IRQChannel = UART5_IRQn;               // 指定中断通道
    NVIC_InitStructure .NVIC_IRQChannelPreemptionPriority = 1;      // 设置抢占优先级
    NVIC_InitStructure .NVIC_IRQChannelSubPriority = 1;             // 设置响应优先级
    NVIC_InitStructure .NVIC_IRQChannelCmd = ENABLE;                // 使能中断通道
    NVIC_Init(&NVIC_InitStructure);                                 // 初始化NVIC
    // 配置完成,开启USART
    USART_Cmd(UART5, ENABLE);                                       // 使能USART
#endif

#ifdef USE_HAL_DRIVER                                               // HAL库 配置
    // 使能 时钟                                                    
    __HAL_RCC_GPIOC_CLK_ENABLE();                                   // 使能GPIOC 
    __HAL_RCC_GPIOD_CLK_ENABLE();                                   // 使能GPIOD 
    __HAL_RCC_UART5_CLK_ENABLE();                                   // 使能UART5
    // 重置 UART                                                    
    __HAL_RCC_UART5_FORCE_RESET();                                  // 使能重置
    __HAL_RCC_UART5_RELEASE_RESET();                                // 取消重置
    // 配置 TX引脚                                                  
    GPIO_InitTypeDef    GPIO_InitStruct = {0};                      // 声明初始化要用到的结构体
    GPIO_InitStruct.Pin   = GPIO_PIN_12 ;                           // 引脚 TX-PC12
    GPIO_InitStruct.Mode  = GPIO_MODE_AF_PP;                        // 工作模式
    GPIO_InitStruct.Pull  = GPIO_NOPULL;                            // 上下拉
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;              // 引脚速率
    GPIO_InitStruct.Alternate = GPIO_AF8_UART5;                     // 引脚复用功能
    HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);                         // 初始化引脚
    // 配置 RX引脚                                                  
    GPIO_InitStruct.Pin   = GPIO_PIN_2 ;                            // 引脚 RX-PD2
    HAL_GPIO_Init(GPIOD, &GPIO_InitStruct);                         // 初始化引脚工作模式
    // 计算波特率参数                                               
    float    temp;
    uint16_t mantissa, fraction;
    SystemCoreClockUpdate();                                        // 更新系统运行频率全局值; 函数SystemCoreClock( ),在标准库、HAL库通用
    temp = (float)(SystemCoreClock / 4) / (ulBaudrate * 16);        // 波特率公式计算; UART5挂载在APB1, 时钟为系统时钟的4分频; 全局变量SystemCoreClock,在标准库、HAL库通用;
    mantissa = temp;				                                // 整数部分
    fraction = (temp - mantissa) * 16;                              // 小数部分
    UART5 -> BRR  = mantissa << 4 | fraction;                       // 设置波特率
    // 配置 UART                                                   
    UART5 -> CR1  =   0;                                            // 清0
    UART5 -> CR1 |=   0x01 << 2;                                    // 接收使能[02]: 0=失能、1=使能
    UART5 -> CR1 |=   0x01 << 3;                                    // 发送使能[03]:0=失能、1=使能
    UART5 -> CR1 |=   0x00 << 9;                                    // 奇偶校验[09]:0=偶Even、1=奇Odd;  注意:使用奇偶校验,下面两项要置1
    UART5 -> CR1 |=   0x00 << 10;                                   // 校验位  [10]:0=禁用、1=使能;     注意,使用奇偶校验,此位要置1 (否则无效、数据错乱)
    UART5 -> CR1 |=   0x00 << 12;                                   // 数据位  [12]:0=8位、 1=9位;      注意:使用奇偶校验,此位要置1 (否则数据会发生错乱)
    UART5 -> CR2  =   0;                                            // 数据清0
    UART5 -> CR2 &= ~(0x03 << 12);                                  // 停止位[13:12]:00b=1个停止位、01b=0.5个停止位、10b=2个停止位、11b=1.5个停止位
    UART5 -> CR3  =   0;                                            // 数据清0
    UART5 -> CR3 &= ~(0x01 << 6);                                   // DMA接收[6]: 0=禁止、1=使能
    UART5 -> CR3 &= ~(0x01 << 7);                                   // DMA发送[7]: 0=禁止、1=使能
    // 配置 中断                                                   
    UART5 -> CR1 &= ~(0x01 << 7);                                   // 关闭发送中断
    UART5 -> CR1 |=   0x01 << 5;                                    // 使能接收中断: 接收缓冲区非空
    UART5 -> CR1 |=   0x01 << 4;                                    // 使能空闲中断:超过1字节时间没收到新数据
    UART5 -> SR   = ~(0x00F0);                                      // 清理中断
    // 配置 中断优先级                                             
    HAL_NVIC_SetPriority(UART5_IRQn, 0, 0);                         // 设置指定中断的响应优先级;  参数:中断请求编号、抢占级、子优先级
    HAL_NVIC_EnableIRQ(UART5_IRQn);                                 // 使能、启用指定的中断
    // 配置完成,打开串口                                          
    UART5 -> CR1 |=   0x01 << 13;                                   // 使能UART开始工作    
#endif

    // 关联缓冲区
    xUART5.puRxData = uaUart5RxData;                                // 获取接收缓冲区的地址
    xUART5.puTxFiFoData = uaUart5TxFiFoData;                        // 获取发送缓冲区的地址
    // 输出提示
    printf("UART5 初始化配置         %d-None-8-1; 已完成初始化配置、收发配置\r", ulBaudrate);
}

/******************************************************************************
 * 函  数: UART5_IRQHandler
 * 功  能: UART5的接收中断、空闲中断、发送中断
 * 参  数: 无
 * 返回值: 无
 * 备  注: 本函数,当产生中断事件时,由硬件调用。
 *          如果使用本文件代码,在工程文件的其它地方,要注释同名函数,否则冲突。
 ******************************************************************************/
void UART5_IRQHandler(void)
{
    static uint16_t cnt = 0;                                           // 接收字节数累计:每一帧数据已接收到的字节数
    static uint8_t  rxTemp[UART5_RX_BUF_SIZE];                         // 接收数据缓存数组:每新接收1个字节,先顺序存放到这里,当一帧接收完(发生空闲中断), 再转存到外部缓存:xUARTx.puRxData[ ]
                                                                       
    // 发送中断:用于把环形缓冲的数据,逐字节发出                      
    if ((UART5->SR & USART_SR_TXE) && (UART5->CR1 & USART_CR1_TXEIE))  // 检查发送寄存器空中断使能,且发送寄存器为空; TXE(发送数据寄存器空)、TXEIE(发送缓冲区空中断使能)
    {                                                                  
        UART5->DR = xUART5.puTxFiFoData[xUART5.usTxFiFoTail++];        // 从FIFO队列中取出一个数据,放入USART的发送寄存器(硬件会自动发出),然后将FIFO的尾指针递增,指向下一个要发送的数据
        if (xUART5.usTxFiFoTail == UART5_TX_BUF_SIZE)                  // 检查FIFO尾指针是否到达了FIFO队列的末尾
            xUART5.usTxFiFoTail = 0;                                   // 将尾指针重置为0,实现环形队列的功能
        if (xUART5.usTxFiFoTail == xUART5.usTxFiFoData)                // 检查FIFO尾指针是否追上了头指针,即所有数据是否都已发送完毕
            UART5->CR1 &= ~USART_CR1_TXEIE;                            // 关闭发送寄存器空中断,防止中断服务程序被不必要地调用
        return;                                                        
    }                                                                  
                                                                       
    // 接收中断:用于逐个字节接收,存放到临时缓存                      
    if (UART5->SR & USART_SR_RXNE)                                     // 检查RXNE(读数据寄存器非空标志位); RXNE中断清理方法:读DR时自动清理;
    {                                                                  
        if (cnt == UART5_RX_BUF_SIZE)                                  // 当前帧已接收的字节量,已满缓存区的大小; 为避免溢出,本包后面接收到的数据直接舍弃;
        {
            printf("警告:UART5单帧接收量,已超出接收缓存大小!\r修改文件bsp_UART.h的UART5_RX_BUF_SIZE值,可解决此问题!\r");
            UART5->DR;                                                 // 读取数据寄存器的数据,但不保存.主要作用:读DR时自动清理接收中断标志;
            return;                                                   
        }                                                             
        rxTemp[cnt++] = UART5->DR ;                                    // 把新收到的字节数据,顺序存放到RXTemp数组中;注意:读取DR时自动清零中断位
        return;                                                       
    }                                                                 
                                                                      
    // 空闲中断:用于判断一帧数据结束,整理数据到外部备读             
    if (UART5->SR & USART_SR_IDLE)                                     // 检查IDLE(空闲中断标志位); IDLE中断标志清理方法:序列清零,USART1 ->SR;  USART1 ->DR;
    {                                                                 
        memcpy(xUART5.puRxData, rxTemp, UART5_RX_BUF_SIZE);            // 把本帧接收到的数据,存入到结构体的数组成员xUARTx.puRxData中, 等待处理; 注意:复制的是整个数组,包括0值,以方便字符串输出时尾部以0作字符串结束符
        xUART5.usRxNum  = cnt;                                         // 把接收到的字节数,存入到结构体变量xUARTx.usRxNum中;
        cnt = 0;                                                       // 接收字节数累计器,清零; 准备下一次的接收
        memset(rxTemp, 0, UART5_RX_BUF_SIZE);                          // 接收数据缓存数组,清零; 准备下一次的接收
        UART5 -> SR;                                                   
        UART5 -> DR;                                                   // 清零IDLE中断标志位!! 序列清零,顺序不能错!!
        return;                                                       
    }

    return;
}

/******************************************************************************
 * 函  数: UART5_SendData
 * 功  能: UART通过中断发送数据
 *         【适合场景】本函数可发送各种数据,而不限于字符串,如int,char
 *         【不 适 合】注意h文件中所定义的发缓冲区大小、注意数据压入缓冲区的速度与串口发送速度的冲突
 * 参  数: uint8_t* pudata     需发送数据的地址
 *          uint8_t  usNum      发送的字节数 ,数量受限于h文件中设置的发送缓存区大小宏定义
 * 返回值: 无
 ******************************************************************************/
void UART5_SendData(uint8_t *pudata, uint16_t usNum)
{
    for (uint16_t i = 0; i < usNum; i++)                         // 把数据放入环形缓冲区
    {
        xUART5.puTxFiFoData[xUART5.usTxFiFoData++] = pudata[i];  // 把字节放到缓冲区最后的位置,然后指针后移
        if (xUART5.usTxFiFoData == UART5_TX_BUF_SIZE)            // 如果指针位置到达缓冲区的最大值,则归0
            xUART5.usTxFiFoData = 0;
    }
                                                                
    if ((UART5->CR1 & USART_CR1_TXEIE) == 0)                     // 检查USART寄存器的发送缓冲区空置中断(TXEIE)是否已打开
        UART5->CR1 |= USART_CR1_TXEIE;                           // 打开TXEIE中断
}

/******************************************************************************
 * 函  数: UART5_SendString
 * 功  能: 发送字符串
 *          用法请参考printf,及示例中的展示
 *          注意,最大发送字节数为512-1个字符,可在函数中修改上限
 * 参  数: const char *pcString, ...   (如同printf的用法)
 * 返回值: 无
 ******************************************************************************/
void UART5_SendString(const char *pcString, ...)
{
    char mBuffer[512] = {0};;                               // 开辟一个缓存, 并把里面的数据置0
    va_list ap;                                             // 新建一个可变参数列表
    va_start(ap, pcString);                                 // 列表指向第一个可变参数
    vsnprintf(mBuffer, 512, pcString, ap);                  // 把所有参数,按格式,输出到缓存; 参数2用于限制发送的最大字节数,如果达到上限,则只发送上限值-1; 最后1字节自动置'\0'
    va_end(ap);                                             // 清空可变参数列表
    UART5_SendData((uint8_t *)mBuffer, strlen(mBuffer));    // 把字节存放环形缓冲,排队准备发送
}

/******************************************************************************
 * 函    数: UART5_SendAT
 * 功    能: 发送AT命令, 并等待返回信息
 * 参    数: char     *pcString      AT指令字符串
 *            char     *pcAckString   期待的指令返回信息字符串
 *            uint16_t  usTimeOut     发送命令后等待的时间,毫秒
 *
 * 返 回 值: 0-执行失败、1-执行正常
 ******************************************************************************/
uint8_t UART5_SendAT(char *pcAT, char *pcAckString, uint16_t usTimeOutMs)
{
    UART5_ClearRx();                                              // 清0
    UART5_SendString(pcAT);                                       // 发送AT指令字符串

    while (usTimeOutMs--)                                         // 判断是否起时(这里只作简单的循环判断次数处理)
    {
        if (UART5_GetRxNum())                                     // 判断是否接收到数据
        {
            UART5_ClearRx();                                      // 清0接收字节数; 注意:接收到的数据内容 ,是没有被清0的
            if (strstr((char *)UART5_GetRxData(), pcAckString))   // 判断返回数据中是否有期待的字符
                return 1;                                         // 返回:0-超时没有返回、1-正常返回期待值
        }
        delay_ms(1);                                              // 延时; 用于超时退出处理,避免死等
    }
    return 0;                                                     // 返回:0-超时、返回异常,1-正常返回期待值
}

/******************************************************************************
 * 函  数: UART5_GetRxNum
 * 功  能: 获取最新一帧数据的字节数
 * 参  数: 无
 * 返回值: 0=没有接收到数据,非0=新一帧数据的字节数
 ******************************************************************************/
uint16_t UART5_GetRxNum(void)
{
    return xUART5.usRxNum ;
}

/******************************************************************************
 * 函  数: UART5_GetRxData
 * 功  能: 获取最新一帧数据 (数据的地址)
 * 参  数: 无
 * 返回值: 数据的地址(uint8_t*)
 ******************************************************************************/
uint8_t *UART5_GetRxData(void)
{
    return xUART5.puRxData ;
}

/******************************************************************************
 * 函  数: UART5_ClearRx
 * 功  能: 清理最后一帧数据的缓存
 *          主要是清0字节数,因为它是用来判断接收的标准
 * 参  数: 无
 * 返回值: 无
 ******************************************************************************/
void UART5_ClearRx(void)
{
    xUART5.usRxNum = 0 ;
}
#endif  // endif UART5_EN




//   USART-6   //
/
#if UART6_EN

static xUSATR_TypeDef  xUART6 = { 0 };                      // 定义 UART6 的收发结构体
static uint8_t uaUart6RxData[UART6_RX_BUF_SIZE];            // 定义 UART6 的接收缓存
static uint8_t uaUart6TxFiFoData[UART6_TX_BUF_SIZE];        // 定义 UART6 的发送缓存

/******************************************************************************
 * 函  数: UART6_Init
 * 功  能: 初始化USART6的通信引脚、协议参数、中断优先级
 *          引脚:TX-PC6、RX-PC7
 *          协议:波特率-None-8-1
 *          发送:发送中断
 *          接收:接收+空闲中断
 *
 * 参  数: uint32_t ulBaudrate  通信波特率
 * 返回值: 无
 ******************************************************************************/
void UART6_Init(uint32_t ulBaudrate)
{
#ifdef USE_STDPERIPH_DRIVER
    // 使能 时钟
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOC, ENABLE);           // 使能 GPIOC  时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART6, ENABLE);          // 使能 USART6 时钟              
    // 配置 引脚复用功能                                       
    GPIO_PinAFConfig(GPIOC, GPIO_PinSource6, GPIO_AF_USART6);       // 配置PC6复用功能:USART6
    GPIO_PinAFConfig(GPIOC, GPIO_PinSource7, GPIO_AF_USART6);       // 配置PC7复用功能:USART6
    // 重置 USART                                                
    RCC_APB2PeriphResetCmd(RCC_APB2Periph_USART6, ENABLE);          // 使能重置
    RCC_APB2PeriphResetCmd(RCC_APB2Periph_USART6, DISABLE);         // 取消重置
    // 配置 TX引脚                                                   
    GPIO_InitTypeDef  GPIO_InitStructure = {0};                     // GPIO 初始化结构体
    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_6;                     // 引脚编号
    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_AF;                   // 引脚方向: 复用功能
    GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;                  // 输出模式:推挽
    GPIO_InitStructure.GPIO_PuPd  = GPIO_PuPd_UP;                   // 上下拉:上拉
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;               // 输出速度:50MHz
    GPIO_Init(GPIOC, &GPIO_InitStructure);                          // 初始化:把上述参数,更新到芯片寄存器
    // 配置 RX引脚                                                   
    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_7;                     // 引脚编号
    GPIO_Init(GPIOC, &GPIO_InitStructure);                          // 初始化:把上述参数,更新到芯片寄存器
    // 配置UART                                                   
    USART_InitTypeDef USART_InitStructure;                          // 声明USART初始化结构体  
    USART_InitStructure.USART_BaudRate = ulBaudrate;                // 设置波特率
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;     // 设置字长为8位数据格式
    USART_InitStructure.USART_StopBits = USART_StopBits_1;          // 设置一个停止位
    USART_InitStructure.USART_Parity = USART_Parity_No;             // 无奇偶校验位
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; // 无硬件数据流控制
    USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; // 收发模式
    USART_Init(USART6, &USART_InitStructure);                       // 初始化USART
    // 配置 中断
    USART_ITConfig(USART6, USART_IT_RXNE, ENABLE);                  // 开启 接收中断
    USART_ITConfig(USART6, USART_IT_IDLE, ENABLE);                  // 开启 空闲中断  
    // 中断优先级配置
    NVIC_InitTypeDef  NVIC_InitStructure = {0};                     // 中断优先级配置结构体
    NVIC_InitStructure .NVIC_IRQChannel = USART6_IRQn;              // 指定中断通道
    NVIC_InitStructure .NVIC_IRQChannelPreemptionPriority = 1;      // 设置抢占优先级
    NVIC_InitStructure .NVIC_IRQChannelSubPriority = 1;             // 设置响应优先级
    NVIC_InitStructure .NVIC_IRQChannelCmd = ENABLE;                // 使能中断通道
    NVIC_Init(&NVIC_InitStructure);                                 // 初始化NVIC
    // 配置完成,开启USART
    USART_Cmd(USART6, ENABLE);                                      // 使能USART
#endif

#ifdef USE_HAL_DRIVER                                               // HAL库 配置
    // 使能 时钟                                                 
    __HAL_RCC_GPIOC_CLK_ENABLE();                                   // 使能GPIOC 
    __HAL_RCC_USART6_CLK_ENABLE();                                  // 使能UART6
    // 重置 UART5                                                
    __HAL_RCC_USART6_FORCE_RESET();                                 // 使能重置
    __HAL_RCC_USART6_RELEASE_RESET();                               // 取消重置
    // 配置 GPIO引脚                                      
    GPIO_InitTypeDef    GPIO_InitStruct = {0};                      // 声明初始化要用到的结构体
    GPIO_InitStruct.Pin   = GPIO_PIN_6 | GPIO_PIN_7;                // 引脚 TX-PC6、RX-PC7
    GPIO_InitStruct.Mode  = GPIO_MODE_AF_PP;                        // 工作模式
    GPIO_InitStruct.Pull  = GPIO_PULLUP;                            // 上下拉
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;              // 引脚速率
    GPIO_InitStruct.Alternate = GPIO_AF8_USART6;                    // 引脚复用功能
    HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);                         // 初始化引脚工作模式
    // 计算波特率
    float    temp;
    uint16_t mantissa, fraction;
    SystemCoreClockUpdate();                                        // 更新系统运行频率全局值; 函数SystemCoreClock( ),在标准库、HAL库通用
    temp = (float)(SystemCoreClock / 2) / (ulBaudrate * 16);        // 波特率公式计算; USART6挂载在APB2, 时钟为系统时钟的2分频; 全局变量SystemCoreClock,在标准库、HAL库通用;
    mantissa = temp;				                                // 整数部分
    fraction = (temp - mantissa) * 16;                              // 小数部分
    USART6 -> BRR  = mantissa << 4 | fraction;                      // 设置波特率
    // 配置 UART                                                    
    USART6 -> CR1  =   0;                                           // 清0
    USART6 -> CR1 |=   0x01 << 2;                                   // 接收使能[02]: 0=失能、1=使能
    USART6 -> CR1 |=   0x01 << 3;                                   // 发送使能[03]:0=失能、1=使能
    USART6 -> CR1 |=   0x00 << 9;                                   // 奇偶校验[09]:0=偶Even、1=奇Odd;  注意:使用奇偶校验,下面两项要置1
    USART6 -> CR1 |=   0x00 << 10;                                  // 校验位  [10]:0=禁用、1=使能;     注意,使用奇偶校验,此位要置1 (否则无效、数据错乱)
    USART6 -> CR1 |=   0x00 << 12;                                  // 数据位  [12]:0=8位、 1=9位;      注意:使用奇偶校验,此位要置1 (否则数据会发生错乱)
    USART6 -> CR2  =   0;                                           // 数据清0
    USART6 -> CR2 &= ~(0x03 << 12);                                 // 停止位[13:12]:00b=1个停止位、01b=0.5个停止位、10b=2个停止位、11b=1.5个停止位
    USART6 -> CR3  =   0;                                           // 数据清0
    USART6 -> CR3 &= ~(0x01 << 6);                                  // DMA接收[6]: 0=禁止、1=使能
    USART6 -> CR3 &= ~(0x01 << 7);                                  // DMA发送[7]: 0=禁止、1=使能
    // 配置 中断                                                    
    USART6 -> CR1 &= ~(0x01 << 7);                                  // 关闭发送中断
    USART6 -> CR1 |=   0x01 << 5;                                   // 使能接收中断: 接收缓冲区非空
    USART6 -> CR1 |=   0x01 << 4;                                   // 使能空闲中断:超过1字节时间没收到新数据
    USART6 -> SR   = ~(0x00F0);                                     // 清理中断
    // 配置 中断优先级                                                
    HAL_NVIC_SetPriority(USART6_IRQn, 1, 1);                        // 设置指定中断的响应优先级;  参数:中断请求编号、抢占级、子优先级
    HAL_NVIC_EnableIRQ(USART6_IRQn);                                // 使能、启用指定的中断
    // 配置完成,打开串口                                           
    USART6 -> CR1 |=   0x01 << 13;                                  // 使能UART开始工作
#endif

    // 关联缓冲区
    xUART6.puRxData = uaUart6RxData;                                // 获取接收缓冲区的地址
    xUART6.puTxFiFoData = uaUart6TxFiFoData;                        // 获取发送缓冲区的地址
    // 输出提示
    printf("UART6 初始化配置         %d-None-8-1; 已完成初始化配置、收发配置\r", ulBaudrate);
}

/******************************************************************************
 * 函  数: USART6_IRQHandler
 * 功  能: USART6的接收中断、空闲中断、发送中断
 * 参  数: 无
 * 返回值: 无
 * 备  注: 本函数,当产生中断事件时,由硬件调用。
 *          如果使用本文件代码,在工程文件的其它地方,要注释同名函数,否则冲突。
 ******************************************************************************/
void USART6_IRQHandler(void)
{
    static uint16_t cnt = 0;                                             // 接收字节数累计:每一帧数据已接收到的字节数
    static uint8_t  rxTemp[UART6_RX_BUF_SIZE];                           // 接收数据缓存数组:每新接收1个字节,先顺序存放到这里,当一帧接收完(发生空闲中断), 再转存到全局变量:xUARTx.puRxData[xx]中;

    // 发送中断:用于把环形缓冲的数据,逐字节发出
    if ((USART6->SR & USART_SR_TXE) && (USART6->CR1 & USART_CR1_TXEIE))  // 检查发送寄存器空中断使能,且发送寄存器为空; TXE(发送数据寄存器空)、TXEIE(发送缓冲区空中断使能)
    {                                                                    
        USART6->DR = xUART6.puTxFiFoData[xUART6.usTxFiFoTail++];         // 从FIFO队列中取出一个数据,放入USART的发送寄存器(硬件会自动发出),然后将FIFO的尾指针递增,指向下一个要发送的数据
        if (xUART6.usTxFiFoTail == UART6_TX_BUF_SIZE)                    // 检查FIFO尾指针是否到达了FIFO队列的末尾
            xUART6.usTxFiFoTail = 0;                                     // 将尾指针重置为0,实现环形队列的功能
        if (xUART6.usTxFiFoTail == xUART6.usTxFiFoData)                  // 检查FIFO尾指针是否追上了头指针,即所有数据是否都已发送完毕
            USART6->CR1 &= ~USART_CR1_TXEIE;                             // 关闭发送寄存器空中断,防止中断服务程序被不必要地调用
        return;                                                          
    }                                                                    
                                                                         
    // 接收中断:用于逐个字节接收,存放到临时缓存                        
    if (USART6->SR & USART_SR_RXNE)                                      // 检查RXNE(读数据寄存器非空标志位); RXNE中断清理方法:读DR时自动清理;
    {                                                                    
        if (cnt >= UART6_RX_BUF_SIZE)                                    // 当前帧已接收的字节量,已满缓存区的大小; 为避免溢出,本包后面接收到的数据直接舍弃;
        {
            printf("警告:USART6单帧接收量,已超出接收缓存大小!\r修改文件bsp_UART.h的UART6_RX_BUF_SIZE值,可解决此问题!\r");
            USART6->DR;                                                  // 读取数据寄存器的数据,但不保存.主要作用:读DR时自动清理接收中断标志;
            return;                                                      
        }                                                                
        rxTemp[cnt++] = USART6->DR ;                                     // 把新收到的字节数据,顺序存放到RXTemp数组中;注意:读取DR时自动清零中断位
        return;                                                          
    }                                                                    
                                                                         
    // 空闲中断:用于判断一帧数据结束,整理数据到外部备读                
    if (USART6->SR & USART_SR_IDLE)                                      // 检查IDLE(空闲中断标志位); IDLE中断标志清理方法:序列清零,USART1 ->SR;  USART1 ->DR;
    {                                                                    
        memcpy(xUART6.puRxData, rxTemp, UART6_RX_BUF_SIZE);              // 把本帧接收到的数据,存入到结构体的数组成员xUARTx.puRxData中, 等待处理; 注意:复制的是整个数组,包括0值,以方便字符串输出时尾部以0作字符串结束符
        xUART6.usRxNum  = cnt;                                           // 把接收到的字节数,存入到结构体变量xUARTx.usRxNum中;
        cnt = 0;                                                         // 接收字节数累计器,清零; 准备下一次的接收
        memset(rxTemp, 0, UART6_RX_BUF_SIZE);                            // 接收数据缓存数组,清零; 准备下一次的接收
        USART6 ->SR;                                                     
        USART6 ->DR;                                                     // 清零IDLE中断标志位!! 序列清零,顺序不能错!!
        return;
    }

    return;
}


/******************************************************************************
 * 函  数: UART6_SendData
 * 功  能: UART通过中断发送数据
 *         【适合场景】本函数可发送各种数据,而不限于字符串,如int,char
 *         【不 适 合】注意h文件中所定义的发缓冲区大小、注意数据压入缓冲区的速度与串口发送速度的冲突
 * 参  数: uint8_t  *puData   需发送数据的地址
 *          uint8_t   usNum    发送的字节数 ,数量受限于h文件中设置的发送缓存区大小宏定义
 * 返回值: 无
 ******************************************************************************/
void UART6_SendData(uint8_t *puData, uint16_t usNum)
{
    for (uint16_t i = 0; i < usNum; i++)                           // 把数据放入环形缓冲区
    {
        xUART6.puTxFiFoData[xUART6.usTxFiFoData++] = puData[i];    // 把字节放到缓冲区最后的位置,然后指针后移
        if (xUART6.usTxFiFoData == UART6_TX_BUF_SIZE)              // 如果指针位置到达缓冲区的最大值,则归0
            xUART6.usTxFiFoData = 0;
    }

    if ((USART6->CR1 & USART_CR1_TXEIE) == 0)                      // 检查USART寄存器的发送缓冲区空置中断(TXEIE)是否已打开
        USART6->CR1 |= USART_CR1_TXEIE;                            // 打开TXEIE中断
}

/******************************************************************************
 * 函  数: UART6_SendString
 * 功  能: 发送字符串
 *          用法请参考printf,及示例中的展示
 *          注意,最大发送字节数为512-1个字符,可在函数中修改上限
 * 参  数: const char *pcString, ...   (如同printf的用法)
 * 返回值: 无
 ******************************************************************************/
void UART6_SendString(const char *pcString, ...)
{
    char mBuffer[512] = {0};;                               // 开辟一个缓存, 并把里面的数据置0
    va_list ap;                                             // 新建一个可变参数列表
    va_start(ap, pcString);                                 // 列表指向第一个可变参数
    vsnprintf(mBuffer, 512, pcString, ap);                  // 把所有参数,按格式,输出到缓存; 参数2用于限制发送的最大字节数,如果达到上限,则只发送上限值-1; 最后1字节自动置'\0'
    va_end(ap);                                             // 清空可变参数列表
    UART6_SendData((uint8_t *)mBuffer, strlen(mBuffer));    // 把字节存放环形缓冲,排队准备发送
}

/******************************************************************************
 * 函    数: UART6_SendAT
 * 功    能: 发送AT命令, 并等待返回信息
 * 参    数: char     *pcString      AT指令字符串
 *            char     *pcAckString   期待的指令返回信息字符串
 *            uint16_t  usTimeOut     发送命令后等待的时间,毫秒
 *
 * 返 回 值: 0-执行失败、1-执行正常
 ******************************************************************************/
uint8_t UART6_SendAT(char *pcAT, char *pcAckString, uint16_t usTimeOutMs)
{
    UART6_ClearRx();                                              // 清0
    UART6_SendString(pcAT);                                       // 发送AT指令字符串

    while (usTimeOutMs--)                                         // 判断是否起时(这里只作简单的循环判断次数处理)
    {
        if (UART6_GetRxNum())                                     // 判断是否接收到数据
        {
            UART6_ClearRx();                                      // 清0接收字节数; 注意:接收到的数据内容 ,是没有被清0的
            if (strstr((char *)UART6_GetRxData(), pcAckString))   // 判断返回数据中是否有期待的字符
                return 1;                                         // 返回:0-超时没有返回、1-正常返回期待值
        }
        delay_ms(1);                                              // 延时; 用于超时退出处理,避免死等
    }
    return 0;                                                     // 返回:0-超时、返回异常,1-正常返回期待值
}

/******************************************************************************
 * 函  数: UART6_GetRxNum
 * 功  能: 获取最新一帧数据的字节数
 * 参  数: 无
 * 返回值: 0=没有接收到数据,非0=新一帧数据的字节数
 ******************************************************************************/
uint16_t UART6_GetRxNum(void)
{
    return xUART6.usRxNum ;
}

/******************************************************************************
 * 函  数: UART6_GetRxData
 * 功  能: 获取最新一帧数据 (数据的地址)
 * 参  数: 无
 * 返回值: 数据的地址(uint8_t*)
 ******************************************************************************/
uint8_t *UART6_GetRxData(void)
{
    return xUART6.puRxData ;
}

/******************************************************************************
 * 函  数: UART6_ClearRx
 * 功  能: 清理最后一帧数据的缓存
 *          主要是清0字节数,因为它是用来判断接收的标准
 * 参  数: 无
 * 返回值: 无
 ******************************************************************************/
void UART6_ClearRx(void)
{
    xUART6.usRxNum = 0 ;
}
#endif  // endif UART6_EN






/  ModbusCRC16 校验   /
///

// CRC16查表法值表数组
// 查表法相比直接计算CRC值,大约可减少一半运算时间
static const uint8_t aModbusCRC16[] =      
{
    // 高位
    0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 
    0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 
    0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 
    0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 
    0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
    0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 
    0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 
    0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 
    0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 
    0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
    0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 
    0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 
    0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 
    0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 
    0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
    0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 
    // 低位
    0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, 0xC6, 0x06, 0x07, 0xC7, 0x05, 0xC5, 0xC4, 0x04,  
    0xCC, 0x0C, 0x0D, 0xCD, 0x0F, 0xCF, 0xCE, 0x0E, 0x0A, 0xCA, 0xCB, 0x0B, 0xC9, 0x09, 0x08, 0xC8, 
    0xD8, 0x18, 0x19, 0xD9, 0x1B, 0xDB, 0xDA, 0x1A, 0x1E, 0xDE, 0xDF, 0x1F, 0xDD, 0x1D, 0x1C, 0xDC, 
    0x14, 0xD4, 0xD5, 0x15, 0xD7, 0x17, 0x16, 0xD6, 0xD2, 0x12, 0x13, 0xD3, 0x11, 0xD1, 0xD0, 0x10,
    0xF0, 0x30, 0x31, 0xF1, 0x33, 0xF3, 0xF2, 0x32, 0x36, 0xF6, 0xF7, 0x37, 0xF5, 0x35, 0x34, 0xF4, 
    0x3C, 0xFC, 0xFD, 0x3D, 0xFF, 0x3F, 0x3E, 0xFE, 0xFA, 0x3A, 0x3B, 0xFB, 0x39, 0xF9, 0xF8, 0x38, 
    0x28, 0xE8, 0xE9, 0x29, 0xEB, 0x2B, 0x2A, 0xEA, 0xEE, 0x2E, 0x2F, 0xEF, 0x2D, 0xED, 0xEC, 0x2C, 
    0xE4, 0x24, 0x25, 0xE5, 0x27, 0xE7, 0xE6, 0x26, 0x22, 0xE2, 0xE3, 0x23, 0xE1, 0x21, 0x20, 0xE0, 
    0xA0, 0x60, 0x61, 0xA1, 0x63, 0xA3, 0xA2, 0x62, 0x66, 0xA6, 0xA7, 0x67, 0xA5, 0x65, 0x64, 0xA4,
    0x6C, 0xAC, 0xAD, 0x6D, 0xAF, 0x6F, 0x6E, 0xAE, 0xAA, 0x6A, 0x6B, 0xAB, 0x69, 0xA9, 0xA8, 0x68, 
    0x78, 0xB8, 0xB9, 0x79, 0xBB, 0x7B, 0x7A, 0xBA, 0xBE, 0x7E, 0x7F, 0xBF, 0x7D, 0xBD, 0xBC, 0x7C, 
    0xB4, 0x74, 0x75, 0xB5, 0x77, 0xB7, 0xB6, 0x76, 0x72, 0xB2, 0xB3, 0x73, 0xB1, 0x71, 0x70, 0xB0, 
    0x50, 0x90, 0x91, 0x51, 0x93, 0x53, 0x52, 0x92, 0x96, 0x56, 0x57, 0x97, 0x55, 0x95, 0x94, 0x54, 
    0x9C, 0x5C, 0x5D, 0x9D, 0x5F, 0x9F, 0x9E, 0x5E, 0x5A, 0x9A, 0x9B, 0x5B, 0x99, 0x59, 0x58, 0x98,
    0x88, 0x48, 0x49, 0x89, 0x4B, 0x8B, 0x8A, 0x4A, 0x4E, 0x8E, 0x8F, 0x4F, 0x8D, 0x4D, 0x4C, 0x8C, 
    0x44, 0x84, 0x85, 0x45, 0x87, 0x47, 0x46, 0x86, 0x82, 0x42, 0x43, 0x83, 0x41, 0x81, 0x80, 0x40
};


/**********************************************************************************************************
 * 函 数:  Modbus_AddCRC16
 * 功 能:  在数据末尾追加两字节的ModbusCRC16校验值
 * 备 注: 1、函数将根据传入的原数据,计算CRC16值,在数据末尾追加两字节的CRC16校验值;
 *         2、注意传入的数组地址,要至少预留2字节的空间,用于追加CRC16的校验值;
 *         
 * 参 数:  uint8_t*  _pcData:参与计算校验值的数据
 *         uint16_t  _usLen :数据字节数; 长度为数据的原字节数即可,不用加2;
 *
 * 返 回:  无
**********************************************************************************************************/
void  Modbus_AddCRC16(uint8_t *_pcData, uint16_t _usLen)
{
    uint8_t  ucCRCHi = 0xFF;                        // CRC值高位
    uint8_t  ucCRCLo = 0xFF;                        // CRC值低位
    uint16_t usIndex;                               // 索引
                                                    
    while (_usLen--)                                // 开始逐字节查表
    {
        usIndex = ucCRCHi ^ *_pcData++;             // 注意,这里指针地址加1了
        ucCRCHi = ucCRCLo ^ aModbusCRC16[usIndex];
        ucCRCLo = aModbusCRC16[usIndex + 256];
    }
    _pcData[0] = ucCRCHi;                           // 末尾第1字节,追加CRC16的高位
    _pcData[1] = ucCRCLo;                           // 末尾第2字节,追加CRC16的低位
}



/**********************************************************************************************************
 * 函 数:  Modbus_CheckCRC16
 * 功 能:  对带ModbusCRC16校验的数据段进行校验
 *
 * 参 数:  uint8_t*  _pcData:数据段地址
 *         uint16_t  _usLen :数据段的长度(字节数); 长度是整包数据的字节数,即包含ModbusCRC16值的长度,不用减2;
 *
 * 返 回:  0-错误、1-正确
*******************************************************************************************/
uint8_t  Modbus_CheckCRC16(uint8_t *_pcData, uint16_t _usLen)
{
    uint8_t  ucCRCHi = 0xFF;                               // CRC值高位
    uint8_t  ucCRCLo = 0xFF;                               // CRC值低位
    uint16_t usIndex;                                      // 数组的索引
   
    _usLen -= 2;                                           // 字节数-2,不计算原数据末尾两字节(ModbusCRC16值)
    
    while (_usLen--)                                       // 开始逐字节查表获得ModbusCRC16值
    {
        usIndex = ucCRCHi ^ *_pcData++;                    // 注意,这里指针地址递增加1
        ucCRCHi = ucCRCLo ^ aModbusCRC16[usIndex];
        ucCRCLo = aModbusCRC16[usIndex + 256];
    }
    
    if ((ucCRCHi == *_pcData++) && (ucCRCLo == *_pcData))  // 把数据段的CRC16校验值,与原数据末尾的CRC16值相比较
        return 1;                                          // 成功匹配; 返回: 1
    
    return 0;                                              // 错误; 返回:0
}





/  辅助功能   /
///

/******************************************************************************
 * 函  数: debug_showData
 * 功  能: 经printf,发送到串口助手上,方便观察
 * 参  数: uint8_t  *rxData   数据地址
 *          uint16_t  rxNum    字节数
 * 返回值: 无
 ******************************************************************************/
#if 0
void debug_showData(uint8_t *puRxData, uint16_t usRxNum)
{
    printf("字节数: %d \r", usRxNum);                   // 显示字节数
    printf("ASCII 显示数据: %s\r", (char *)puRxData);    // 显示数据,以ASCII方式显示,即以字符串的方式显示
    printf("16进制显示数据: ");                          // 显示数据,以16进制方式,显示每一个字节的值
    while (usRxNum--)                                    // 逐个字节判断,只要不为'\0', 就继续
        printf("0x%X ", *puRxData++);                    // 格式化
    printf("\r\r");                                      // 显示换行
}
#endif




//  printf   //
///
/******************************************************************************
 * 功能说明: printf函数支持代码
 *           【特别注意】加入以下代码, 使用printf函数时, 不再需要打勾use MicroLIB
 * 备    注:
 * 最后更新: 2024年06月07日
 ******************************************************************************/
#pragma import(__use_no_semihosting)

struct __FILE
{
    int handle;
};                                         // 标准库需要的支持函数

FILE __stdout;                             // FILE 在stdio.h文件
void _sys_exit(int x)
{
    x = x;                                 // 定义_sys_exit()以避免使用半主机模式
}

int fputc(int ch, FILE *f)                 // 重定向fputc函数,使printf的输出,由fputc输出到UART
{
    UART1_SendData((uint8_t *)&ch, 1);     // 使用队列+中断方式发送数据; 无需像方式1那样等待耗时,但要借助已写好的函数、环形缓冲
    return ch;
}

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐