STM32·HAL库开发(八)USART串口通信——案例:阻塞收发+中断收发+printf重定向+接收数据判断

目录

【通讯的基本知识】

【串行通讯与并行通讯】

【全双工、半双工及单工通信】

【同步通信和异步通信】

【USART—串口通讯】

【物理层】

        不同硬件标准电气特性表

        不同硬件标准接口示意图

        不同硬件标准接线图解

        应用实例

【协议层】

        【USART协议】

        【Modbus协议】

【STM32的串口通信】

【基础知识】

【功能引脚】

【波特率计算】

【状态标记变量USART_RX_STA】

【串口接收中断流程】

【常用函数】

【demo · 串口打印:阻塞收发】

【strlen 和 sizeof的区别】

【memset 和 strcpy 的区别】

【demo · 串口打印:阻塞收发 + printf + LED】

【demo · 串口打印:中断收发 + printf + LED】

【demo · 串口打印:中断收发 + printf + 不使用MicroLIB】

【扩展】

【多串口】

【串口数据本质是二进制】


【通讯的基本知识】

【串行通讯与并行通讯】

        按数据传送的方式,通讯可分为串行通讯并行通讯

        串行通讯是指设备之间通过少量数据信号线(一般是8 根以下),地线以及控制信号线,按数据位形式一位一位地传输数据的通讯方式;
        注意:串行通信未必一定是单根数据线,比如串行差分传输使用两个信号线的差分信号来表示数据信号。差分信号的优点是可以减少共模干扰,抗干扰能力提高,因此数据传输距离更远

        并行通讯一般是指使用8、16、32 及64 根或更多的数据线进行传输的通讯方式;并行通讯使用多个信号线,可以同时传输多个数据位的数据,而串行通讯只有一个信号线,同一时刻只能传输一个数据位的数据。

【全双工、半双工及单工通信】

【同步通信和异步通信】

        根据通讯的数据同步方式,又分为同步和异步两种,可以根据通讯过程中是否有使用到时钟信号进行简单的区分。

        同步通讯:收发设备双方会使用一根信号线表示时钟信号,在时钟信号的驱动下双方进行协调,同步数据。通讯中通常双方会统一规定在时钟信号的上升沿或下降沿对数据线进行采样。
        举例:IIC、SPI、USART 中的同步通讯模式

        异步通讯:不使用时钟信号进行数据同步,它们直接在数据信号中穿插一些同步用的信号位,或者把主体数据进行打包,以数据帧的格式传输数据,某些通讯中还需要双方约定数据的传输速率,以便更好地同步。
        举例:UART、单总线

        同步通讯中,数据信号所传输的内容绝大部分就是有效数据,而异步通讯中会包含有帧的各种标识符,所以同步通讯的效率更高,但是同步通讯双方的时钟允许误差较小,而异步通讯双方的时钟允许误差较大。

【USART—串口通讯】

        串口通讯(Serial Communication) 是一种设备间非常常用的串行通讯方式,因为它简单便捷,因此大部分电子设备都支持该通讯方式。

        对于通讯协议,把它分为物理层协议层
        物理层规定通讯系统中具有机械、电子功能部分的特性,确保原始数据在物理媒体的传输。
        协议层主要规定通讯逻辑,统一收发双方的数据打包、解包标准。

简单来说:物理层规定我们用嘴巴还是用肢体来交流,协议层则规定我们用中文还是英文来交流
常说的串口通讯方式(USART)、ModBus   一般指协议层的
软件协议
常说的TTL、RS485、RS232、RS422   一般指物理层的硬件标准
对于程序的编写,不管采用何种硬件标准,只要使用同一软件协议,程序基本相同

【物理层】

        不同硬件标准电气特性表

        不同硬件标准接口示意图

        不同硬件标准接线图解

不同硬件标准的由来、优缺点、具体介绍

串口、COM、UART、USART、RS232、RS485、RS422、TTL,再也不懵了... (一) - 知乎

USART、RS232、RS485、IIC、SPI通信协议详解_rs485协议-优快云博客

串口通讯接口类型:TTL、RS232和RS485(电平标准)_ttl串口通信-优快云博客

【MCAL_UART】-1.2-图文详解RS232,RS485和MODBUS的关系_modbus和232,485,usb-优快云博客

        应用实例

RS485实例

接线:

        A  9(TX)—— TX (丝印是反的)

        A10(RX)—— RX(丝印是反的)

        VCC——3V3 / 5V   (看模块规格) 

        GND——GND

        A——A

        B——B

        GND——GND(可接不不接,靠A和B压差传输数据)

【协议层】

        【USART协议】

        USART/UART仅规定了协议层通信标准,并没有规定采用什么电气特性,物理层硬件可以根据通讯对象灵活选择,比如,可以是TTL、CMOS、RS232、RS485、RS422。协议层通信标准规定了数据传输的格式:起始位+数据帧+校验位+停止位
        串口通讯的数据包由发送设备通过自身的TXD 接口传输到接收设备的RXD 接口。
        在串口通讯的协议层中,规定了数据包的内容,它由启始位、主体数据、校验位以及停止位组成,通讯双方的数据包格式要约定一致才能正常收发数据。

        波特率
        异步通讯中由于没有时钟信号,所以两个通讯设备之间需要约定好波特率,即每个码元的长度,以便对信号进行解码。常见的波特率为4800、9600、115200 等。

        通讯的起始和停止信号
        串口通讯的一个数据包从起始信号开始,直到停止信号结束。数据包的起始信号由一个逻辑0 的数据位表示,而数据包的停止信号可由0.5、1、1.5 或2 个逻辑1 的数据位表示,只要双方约定一致即可。

        有效数据
        在数据包的起始位之后紧接着的就是要传输的主体数据内容,也称为有效数据,有效数据的长度常被约定为5、6、7 或8 位长。

        数据校验
        在有效数据之后,有一个可选的数据校验位。由于数据通信相对更容易受到外部干扰导致传输数据出现偏差,可以在传输过程加上校验位来解决这个问题。校验方法有奇校验(odd)、偶校验(even)、0 校验(space)、1 校验(mark) 以及无校验(noparity)。

        奇校验要求有效数据和校验位中“1”的个数为奇数,比如一个8 位长的有效数据为:01101001,此时总共有4 个“1”,为达到奇校验效果,校验位为“1”,最后传输的数据将是8 位的有效数据加上1 位的校验位总共9 位。

        偶校验与奇校验要求刚好相反,要求帧数据和校验位中“1”的个数为偶数,比如数据帧:11001010,此时数据帧“1”的个数为4 个,所以偶校验位为“0”。

        0 校验是不管有效数据中的内容是什么,校验位总为“0”,1 校验是校验位总为“1”。

        【Modbus协议】

STM32HAL库RS485-ModBus协议控制伺服电机_stm32 rs485-优快云博客

STM32实现基于RS485的简单的Modbus协议_stm32串口modbus发送-优快云博客

【STM32的串口通信】

【基础知识】

        通用同步异步收发器(Universal Synchronous Asynchronous Receiver and Transmitter) 是一个串行通信设备,可以灵活地与外部设备进行全双工数据交换。

        有别于USART 还有一个UART(Universal Asynchronous Receiver and Transmitter),它在USART 基础上裁剪掉了同步通信功能,只有异步通信;也就是USART比UART多一个时钟信号

        简单区分同步和异步就是看通信时需不需要对外提供时钟输出,我们平时用的串口通信基本都是UART。

        串行通信一般是以帧格式传输数据,即是一帧一帧的传输,每帧包含有起始信号、数据信息、停止信息,可能还有校验信息。

        USART 就是对这些传输参数有具体规定,当然也不是只有唯一一个参数值,很多参数值都可以自定义设置,只是增强它的兼容性。

        USART 满足外部设备对工业标准NRZ 异步串行数据格式的要求,并且使用了小数波特率发生器,可以提供多种波特率,使得它的应用更加广泛。

        USART 支持同步单向通信和半双工单线通信;还支持局域互连网络LIN、智能卡(SmartCard) 协议与lrDA(红外线数据协会) SIR ENDEC 规范。

        USART 支持使用DMA,可实现高速数据通信

        USART 在STM32 应用最多莫过于“打印”程序信息,一般在硬件设计时都会预留一个USART通信接口连接电脑,用于在调试程序是可以把一些调试信息“打印”在电脑端的串口调试助手工具上,从而了解程序运行是否正确、如果出错哪具体哪里出错等等。

【功能引脚】

        TX: 发送数据输出引脚。
        RX: 接收数据输入引脚。
        SW_RX: 数据接收引脚,只用于单线和智能卡模式,属于内部引脚,没有具体外部引脚。
      nRTS: 请求以发送(Request To Send),n 表示低电平有效。如果使能RTS 流控制,当USART 接收器准备好接收新数据时就会将nRTS 变成低电平;当接收寄存器已满时,nRTS 将被设置为高电平。该引脚只适用于硬件流控制。
        nCTS: 清除以发送(Clear To Send),n 表示低电平有效。如果使能CTS 流控制,发送器在发送下一帧数据之前会检测nCTS 引脚,如果为低电平,表示可以发送数据,如果为高电平则在发送完当前数据帧之后停止发送。该引脚只适用于硬件流控制。
        SCLK: 发送器时钟输出引脚。这个引脚仅适用于同步模式。

波特率计算】

        

        DIV_Mantissa = USARTDIV 的整数部分
        DIV_Fraction = (USARTDIV 的小数部分) × 16

        其中,fPLCK 为USART 时钟,USARTDIV 是一个存放在波特率寄存器(USART_BRR) 的一个无符号定点数。其中DIV_Mantissa[11:0] 位定义USARTDIV 的整数部分,DIV_Fraction[3:0] 位定义USARTDIV 的小数部分。

        波特率的常用值有2400、9600、19200、115200。下面以实例讲解如何设定寄存器值得到波特率的值。 USART1 使用APB2 总线时钟,最高可达72MHz,其他USART 的最高频率36MHz。

        以USART1 为例,即fPLCK=72MHz。为得到115200bps 的波特率,此时:
        115200 = 72000000 / (16 ∗ USARTDIV)
        解得USARTDIV=39.0625,可算得DIV_Fraction=0.0625*16=1=0x01,
        DIV_Mantissa=39=0x27,即应该设置USART_BRR 的值为0x271。

        校验控制:
        STM32F103 系列控制器USART 支持奇偶校验。使用校验位时,串口传输的长度将是8 位的数据帧加上1 位的校验位总共9 位,此时USART_CR1 寄存器的M 位需要设置为1,即9 数据位。

        将USART_CR1 寄存器的PCE 位置1 就可以启动奇偶校验控制,奇偶校验由硬件自动完成。启动了奇偶校验控制之后,在发送数据帧时会自动添加校验位,接收数据时自动验证校验位。

        接收数据时如果出现奇偶校验位验证失败,会见USART_SR 寄存器的PE 位置1,并可以产生奇偶校验中断。

        使能了奇偶校验控制后,每个字符帧的格式将变成:起始位+ 数据帧+ 校验位+ 停止位

【状态标记变量USART_RX_STA

        从0 开始,串口中断接收到一个数据(一个字节)就自增 1 。当数据读取全部 OK 时候(回车和换行符号来的时候),那么 USART_RX_STA 的最高位置 1,表示串口数据接收全部完毕。

【串口接收中断流程】

【常用函数】

串口发送/接收函数:
HAL_UART_Transmit();串口发送数据,使用超时管理机制 
HAL_UART_Receive();串口接收数据,使用超时管理机制
HAL_UART_Transmit_IT();串口中断模式发送  
HAL_UART_Receive_IT();串口中断模式接收
HAL_UART_Transmit_DMA();串口DMA模式发送
HAL_UART_Transmit_DMA();串口DMA模式接收

阻塞发送函数:

HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);

功能: 

        串口发送指定长度的数据,若超时未发送完成则不再发送,返回超时标志(HAL_TIMEOUT)

参数:

        huart: 指向uart _ handletypedef结构的huart指针,该结构包含指定uart模块的配置信息
                      UART_HandleTypeDef *huart   UATR的别名
                      如 : UART_HandleTypeDef huart1;       别名就是huart1

        PData:指向数据缓冲区的数据地址(U8或u16数据元素)

        Size:要发送的数据元素( u8或U16 )的大小,以字节为单位

        Timeout: 超时持续时间,单位ms,0就是0ms超时,数据最大发送时间,超过则返回异常

示例:HAL_UART_Transmit(&huart1,(uint8_t*)"zzq \r\n",strlen("zzq \r\n"),0xFFFF); //先串口一 发送一段字符串 时间是0xFFFF,

或者:HAL_UART_Transmit(&huart1,(const unsigned char *)"zzq \r\n",strlen("zzq \r\n"),0xFFFF); //先串口一 发送一段字符串 时间是0xFFFF,

---------------------------------------------------------------------------------------------------------------------------------

阻塞接收函数:

HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);

功能: 

        串口接收指定长度的数据。若超时未接收完成则不再接收,返回超时标志(HAL_TIMEOUT)

参数:

        huart: 指向uart _ handletypedef结构的huart指针,该结构包含指定uart模块的配置信息
                      UART_HandleTypeDef *huart   UATR的别名
                      如 : UART_HandleTypeDef huart1;       别名就是huart1

        PData:指向数据缓冲区的数据地址(U8或u16数据元素)

        Size:要接收的数据元素( u8或U16 )的大小,以字节为单位

        Timeout: 超时持续时间,单位ms,0就是0ms超时,数据最大接收时间,超过则返回异常

示例:if(HAL_UART_Receive(&huart1,rx_buf,sizeof(rx_buf),1000)==HAL_OK){ }

注意:阻塞模式只能接收定长的数据,这个长度是之前设置的数组长度

---------------------------------------------------------------------------------------------------------------------------------
串口中断回调函数:
HAL_UART_IRQHandler(UART_HandleTypeDef *huart);  //串口中断处理函数
HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart);  //串口发送中断回调函数
HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);  //串口接收中断回调函数
HAL_UART_TxHalfCpltCallback(UART_HandleTypeDef *huart);  //串口发送一半中断回调函数(用的较少)
HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart);//串口接收一半回调函数(用的较少)
HAL_UART_ErrorCallback();//串口接收错误函数

---------------------------------------------------------------------------------------------------------------------------------

串口中断发送函数:

HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)

功能: 串口中断发送,以中断方式发送指定长度数据

参数:

        huart: 指向uart _ handletypedef结构的huart指针,该结构包含指定uart模块的配置信息
                      UART_HandleTypeDef *huart   UATR的别名
                      如 : UART_HandleTypeDef huart1;       别名就是huart1

        PData:指向数据缓冲区的数据地址(U8或u16数据元素)

        Size:要发送的数据元素( u8或U16 )的大小,以字节为单位

示例:HAL_UART_Transmit_IT(&huart1,tx_buf,sizeof(tx_buf));

---------------------------------------------------------------------------------------------------------------------------------

串口中断接收函数:

HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)

功能: 串口中断接收,以中断方式接收指定长度数据

过程: 设置数据存放位置、接收数据长度,然后使能串口接收中断。接收到数据时,会触发串口中断。串口中断函数处理,直到接收到指定长度数据,而后关闭中断,进入中断接收回调函数,不再触发接收中断(只触发一次中断)

参数:

        huart: 指向uart _ handletypedef结构的huart指针,该结构包含指定uart模块的配置信息
                      UART_HandleTypeDef *huart   UATR的别名
                      如 : UART_HandleTypeDef huart1;       别名就是huart1

        PData:指向数据缓冲区的数据地址(U8或u16数据元素)

        Size:要接收的数据元素( u8或U16 )的大小,以字节为单位

示例:HAL_UART_Receive_IT(&huart1,rx_buf,sizeof(rx_buf));

---------------------------------------------------------------------------------------------------------------------------------

串口中断处理函数:
HAL_UART_IRQHandler(UART_HandleTypeDef *huart);  

功能: 对接收到的数据进行判断和处理 判断是发送中断还是接收中断,然后进行数据的发送和接收,在中断服务函数中使用;如果接收数据,则会进行接收中断处理函数;如果发送数据,则会进行发送中断处理函数。

---------------------------------------------------------------------------------------------------------------------------------
串口中断回调函数:
HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);  

功能: HAL库的中断进行完之后,并不会直接退出,而是会进入中断回调函数中,用户可以在其中设置代码,串口中断接收完成之后,会进入该函数,该函数为空函数,用户需自行修改,

参数:

UART_HandleTypeDef *huart UATR的别名 如 : UART_HandleTypeDef huart1; 别名就是huart1

举例:   HAL_UART_RxCpltCallback(&huart1){  //自定义处理代码     }

【demo · 串口打印:阻塞收发】

        CubeMX配置如下:
#include "string.h"

int main(void)
{

	uint8_t tx_buf[] = "hello zzq \r\n ";
	uint8_t ch[20] = {0};

    SystemClock_Config();

    MX_GPIO_Init();
    MX_USART1_UART_Init();
	HAL_UART_Transmit(&huart1,tx_buf,strlen((const char*)tx_buf),100);
	HAL_Delay(1);						//等待数据发送完毕
    while (1)
    {
		HAL_UART_Receive(&huart1,ch,sizeof (ch),100);
		HAL_UART_Transmit(&huart1,ch,strlen((const char*)ch),100);
		memset(ch,0,sizeof(ch));
    }
}
【strlen 和 sizeof的区别】

        (1).功能不同:strlen 函数用于计算以 null 结尾的字符串的长度,即字符串中字符的数量,不包括 null 终止符;而 sizeof 运算符用于计算数据类型或变量所占用的内存大小。

        (2).参数类型不同:strlen 函数的参数是 null 结尾的字符串(const char*),而 sizeof 运算符可以接受任意类型的数据作为参数,包括数据类型、变量名、数组名等。

        (3).返回值类型不同:strlen 函数返回的是一个无符号整数类型(size_t),表示字符串的长度;sizeof 运算符返回的是一个编译时常量,表示指定类型或变量所占用的内存字节数。

        (4).计算方式不同:strlen 函数通过逐个查找字符串中的字符,直到遇到 null 终止符来计算字符串的长度;sizeof 运算符在编译时就可以确定其结果,不需要运行时计算。

        需要注意的是,sizeof 运算符计算的是数据类型或变量所占用的内存大小,而不是其中实际存储的数据的长度。对于数组,sizeof 会计算整个数组所占用的内存空间,而不仅仅是数组中的元素个数。

        综上,在 HAL_UART_Transmit 函数中不能使用 sizeof 来设置发送位数,否则会出现单片机始终向串口发送0x00的情况。因为 siezeof(ch) 返回的是数组 ch 的大小,而不是字符串的长度。在 C 语言中,字符数组以 null 终止符 \0 表示字符串的结束。而 siezeof(ch) 返回的是数组 ch 的总大小,包含了数组的所有元素,包括末尾的 null 终止符。因此,当你将 siezeof(ch) 作为参数传递给 HAL_UART_Transmit 函数时,它会发送整个字符数组,包括 null 终止符,而不仅仅是字符串部分。

【memset 和 strcpy 的区别】

        (1).功能不同:memset 用于将一块内存区域填充为指定的值,通常用于初始化数组或清空缓冲区;strcpy 用于将一个字符串复制到另一个字符串中。

        (2).参数不同:memset 函数的参数包括目标内存区域的起始位置、要填充的值以及要填充的字节数;strcpy 函数的参数是目标字符串和源字符串。

        (3).返回值类型不同:memset 函数没有返回值,它会直接修改目标内存区域;strcpy 函数返回目标字符串的地址,即目标字符串的起始位置。

        (4).操作方式不同:memset 使用指定的值填充连续的内存字节,可以灵活地设置目标内存区域的值;strcpy 按顺序将源字符串中的字符复制到目标字符串中,直到遇到 null 终止符或达到目标字符串的末尾。

        需要注意的是,使用 strcpy 函数时必须确保目标字符串有足够的空间来存储源字符串的内容,否则可能会导致缓冲区溢出的问题。为了避免这种情况,可以考虑使用 strncpy 函数,它允许指定要复制的最大字符数。

        综上,在STM32的串口缓冲区清空时,不能使用strcpy。因为strcpy 函数是用于操作字符串的,而串口缓冲区是以字节为单位进行数据传输的。strcpy 函数会按顺序将源字符串中的字符复制到目标字符串中,直到遇到 null 终止符。但是在串口缓冲区中,数据是以字节的形式存储的,并没有 null 终止符来标志数据的结束。当使用 strcpy 函数来清空串口缓冲区时,由于没有 null 终止符,strcpy 会继续复制字节直到遇到内存中的任意一个 null 字节作为结束位置,这可能导致错误的处理或者访问非法内存。

void *memset(void *ptr, int value, size_t num);
//  ptr:要填充的内存区域的起始位置,通常是一个指针
//  value:要填充的值,以整数形式给出(通常是无符号字符或零)
//  num:要填充的字节数

//  函数返回类型为 void *,表示返回一个指向起始位置的指针
//  value 的范围必须在 0-255 之间,因为 memset 函数是按字节填充的
//  使用 memset 函数时,需要确保 ptr 指向的内存区域足够大,并且不会越界

【demo · 串口打印:阻塞收发 + printf + LED】

        
        除了使用HAL_UART_Transmit发送字符串,还有更方便的方法进行串口调试:printf重定向
     
        CubeMX配置和【demo · 串口打印:阻塞收发】配置一致
        在Keil中勾选使用微库

#include "string.h"
#include "stdio.h"



int fputc(int ch,FILE *f)
{	
	//unsigned char temp[1] = {ch};
	HAL_UART_Transmit(&huart1, (uint8_t *)&ch , 1 , 0xffff);
	return ch;
}



int main(void)
{

	uint8_t ch[20] = {0};

    SystemClock_Config();

    MX_GPIO_Init();
    MX_USART1_UART_Init();
    while (1)
    {
		HAL_UART_Receive(&huart1,ch,sizeof (ch),100);
		printf("%s", ch);			
        // printf期望传入的参数类型是 const char *restrict 类型,此处使用 %s 格式说明符打印字符串
		
        
        /*------------------------- 控制LED -------------------------*/
        if(!strcmp((const char *)ch, "on\r\n")) 
		{
			HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET);
			if(HAL_GPIO_ReadPin(LED_GPIO_Port, LED_Pin) == GPIO_PIN_RESET)
				printf("LED已打开\n");
		}
		else if(!strcmp((const char *)ch, "off\r\n"))
		{
			HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET);
			if(HAL_GPIO_ReadPin(LED_GPIO_Port, LED_Pin) == GPIO_PIN_SET)
				printf("LED已打开\n");
		}
		else
		{
			if(ch[0] != '\0')
				printf("指令发送错误:%s", ch);
		}
        /*------------------------- 控制LED -------------------------*/

        memset(ch,0,sizeof(ch));
    }
}

【demo · 串口打印:中断收发 + printf + LED】

        CubeMX配置和【demo · 串口打印:阻塞收发 + printf】配置一致,要勾选微库
        注意:要开启串口中断
/* printf.h */

#ifndef __printf_H__
#define __printf_H__

#include "main.h"
#include <stdio.h>

#define UART1_REC_LEN 200										// 定义最大接收字节数 200,可根据需求调整

extern uint8_t buf;												// 串口接收缓存(1字节)
extern uint8_t UART1_RX_Buffer[UART1_REC_LEN];	// 接收缓冲, 串口接收到的数据放在这个数组里,最大UART1_REC_LEN个字节
extern uint16_t UART1_RX_STA;									// 接收状态: bit15接收完成标志; 		bit14接收到0x0d;	  	bit13~0接收到的有效字节数目

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);
int fputc(int ch, FILE *f);

#endif /*__ printf_H__ */
/* printf.c */

#include "printf.h"
#include "usart.h"

uint8_t buf=0;											// 串口接收缓存(1字节)
uint8_t UART1_RX_Buffer[UART1_REC_LEN];		            // 接收缓冲, 串口接收到的数据放在这个数组里,最大UART1_REC_LEN个字节
uint16_t UART1_RX_STA=0;								// 接收状态: bit15接收完成标志; 		bit14接收到0x0d;	  	bit13~0接收到的有效字节数目

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)	// 接收完成回调函数,收到一个数据后,在这里处理
{
	if(huart->Instance == USART1)				// 判断中断是由哪个串口触发的
	{
		if((UART1_RX_STA & 0x8000) == 0)	    // 判断接收是否完成(UART1_RX_STA bit15 位是否为1),接收未完成(bit15为0)为真,进入接收流程
		{
			if(UART1_RX_STA & 0x4000)			// 如果已经收到了 0x0d (回车)(UART1_RX_STA bit14 位是否为1),接收到回车为真
			{
				if(buf == 0x0a)					// 则接着判断是否收到 0x0a (换行)
					UART1_RX_STA |= 0x8000;		// 如果 0x0a 和 0x0d 都收到,则将 bit15 位置为1
				else
					UART1_RX_STA = 0;			// 否则认为接收错误,重新开始
			}
			else							    // 如果没有收到了 0x0d (回车)
			{
				if(buf == 0x0d)					// 则先判断收到的这个字符是否是 0x0d (回车)
				{
					UART1_RX_STA |= 0x4000;		// 是的话则将 bit14 位置为1
				}
				else
				{
					UART1_RX_Buffer[UART1_RX_STA & 0X3FFF] = buf;	// 否则将接收到的数据保存在缓存数组里
					UART1_RX_STA++;
					if(UART1_RX_STA > UART1_REC_LEN - 1)			// 如果接收数据大于UART1_REC_LEN(200字节),则重新开始接收
						UART1_RX_STA = 0;
				}
			}
		}
		HAL_UART_Receive_IT(&huart1, &buf, 1);						// 重新开启中断
	}
}



int fputc(int ch,FILE *f)
{	
	//unsigned char temp[1] = {ch};
	HAL_UART_Transmit(&huart1, (uint8_t *)&ch , 1 , 0xffff);
	return ch;
}
/* main.c */

#include "main.h"
#include "usart.h"
#include "gpio.h"

#include "printf.h"
#include "string.h"


int main(void)
{    
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_USART1_UART_Init();
	HAL_UART_Receive_IT(&huart1, &buf, 1);

    printf("hello zzq\r\n");
	HAL_Delay(1000);
    
    while (1)
    {
		if(UART1_RX_STA & 0x8000)
		{
			printf("收到数据:");		    // 把“”中的内容打印到串口
			printf("%s",UART1_RX_Buffer);	// 将收到的数据发送到串口
			//HAL_UART_Transmit(&huart1, UART1_RX_Buffer, UART1_RX_STA & 0x3fff, 0xffff);		        // 将收到的数据发送到串口
			while(huart1.gState != HAL_UART_STATE_READY);	// 等待发送完成
			printf("\r\n");



 /*------------------------- 控制LED -------------------------*/
            printf("收到数据:");
			if (!strcmp((const char *)UART1_RX_Buffer, "on")) 
			{
				HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET);
				if(HAL_GPIO_ReadPin(LED_GPIO_Port, LED_Pin) == GPIO_PIN_RESET)
					printf("LED opened\r\n");
			}
			else if(!strcmp((const char *)UART1_RX_Buffer, "off")) 
			{
				HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET);
				if(HAL_GPIO_ReadPin(LED_GPIO_Port, LED_Pin) == GPIO_PIN_SET)
				printf("LED closed\r\n");
			} 
			else 
			{
				if(UART1_RX_Buffer[0] != '\0')
					printf("指令发送错误:%s", UART1_RX_Buffer);
			}
 /*------------------------- 控制LED -------------------------*/



			UART1_RX_STA = 0;				// 重新开始下一次接收
            memset(UART1_RX_Buffer,0,sizeof(UART1_RX_Buffer));	// 清空缓冲区
		}
		
  }
}

【demo · 串口打印:中断收发 + printf + 不使用MicroLIB】

        上一demo中若要使用printf打印信息进行串口调试,需要使用微库,但是微库是精简库,在一些情况下不能使用微库,或者需要使用多个串口时,需要使用其他方法。
/* 以下代码可单独添加到任意.c文件中,如 printf.c 或 usart.c */

#include <stdarg.h>
#include <string.h>
#include <stdio.h>

void UsartPrintf(UART_HandleTypeDef USARTx, char *fmt,...)
{
 
	unsigned char UsartPrintfBuf[296];
	va_list ap;
	unsigned char *pStr = UsartPrintfBuf;
	
	va_start(ap, fmt);
	vsnprintf((char *)UsartPrintfBuf, sizeof(UsartPrintfBuf), fmt, ap);							//格式化
	va_end(ap);
	
	while(*pStr != NULL)
	{
        HAL_UART_Transmit (&USARTx ,(uint8_t *)pStr++,1,HAL_MAX_DELAY );		
	}
 
}
/* 对应的.h文件 */

#define USART_DEBUG		huart1
 
void UsartPrintf(UART_HandleTypeDef USARTx, char *fmt,...);
/* 调用示例 */
UsartPrintf(USART_DEBUG, "The USART1 is OK!\r\n");
/* main.c */

#include "main.h"
#include "usart.h"
#include "gpio.h"

#include "printf.h"


int main(void)
{    
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_USART1_UART_Init();
	HAL_UART_Receive_IT(&huart1, &buf, 1);
    
    while (1)
    {
		if(UART1_RX_STA & 0x8000)
		{
			UsartPrintf(USART_DEBUG, "收到数据:");		        // 把“”中的内容打印到串口
			UsartPrintf(USART_DEBUG, (char *)UART1_RX_Buffer);	// 将收到的数据发送到串口
			//HAL_UART_Transmit(&huart1, UART1_RX_Buffer, UART1_RX_STA & 0x3fff, 0xffff);		        // 将收到的数据发送到串口
			while(huart1.gState != HAL_UART_STATE_READY);	    // 等待发送完成
			UsartPrintf(USART_DEBUG, "\r\n");
			UART1_RX_STA = 0;				                    // 重新开始下一次接收
		}
		UsartPrintf(USART_DEBUG, "hello zzq\r\n");
		HAL_Delay(1000);
  }
}

【扩展】

        上面demo均是通过ch430模块接到电脑上,通过串口助手接收数据
       
        若要扩展,可把ch340替换为蓝牙透传模块或网络模块等任意带TX、RX接口的模块
        LED可替换为继电器
        
        常用蓝牙模块有HC-X、JDY-X,注意重设AD指令后需重启(如波特率设置)

        HC-X设置AT指令时若无反应,可尝试按下模块上的按键

【多串口】

        https://zhuanlan.zhihu.com/p/687455024
        https://zhuanlan.zhihu.com/p/462083002

【串口数据本质是二进制】

参考:

        STM32 通信基本知识 串口通信(USART)_stm32串口通信_Dir_xr的博客-优快云博客
        https://zhuanlan.zhihu.com/p/640347171
        STM32 HAL库 使用printf函数 Use MicroLIB配置_hal库中printf_Dir_xr的博客-优快云博客
        STM32 HAL库 STM32CubeMx -- 串口的使用(USART/UART)_Dir_xr的博客-优快云博客

        HC-05蓝牙模块 教学及问题处理_蓝牙模块hc05进入不了命令响应工作模式-优快云博客
        A_A07_005 JDY-31串口配置与手机端工具使用_jdy31串口助手-优快云博客

<think>好的,用户想了解如何使用STM32 HAL进行UART串口的数据收发,需要教程和示例代码。首先,我需要回忆一下HAL中UART的相关函数和配置步骤。 首先,STM32CubeMX配置是基础。用户需要正确配置UART的引脚、波特率、数据位等参数。然后生成初始化代码。这部分可能需要提到使用CubeMX工具选择UART外设,设置参数,并生成代码。 接下来是发送数据的部分。HAL提供了阻塞发送、中断发送和DMA发送三种方式。用户的问题可能更关注中断方式,因为示例中的引用提到了中断发送。需要分别给出示例,比如HAL_UART_Transmit用于阻塞发送,HAL_UART_Transmit_IT用于中断发送。同时,可能需要提到重定向printf的例子,如引用中的fputc函数,使用中断发送每个字符。 然后是接收数据。同样有阻塞中断和DMA方式。引用2中的例子使用了阻塞接收,在循环中调用HAL_UART_Receive,这可能不够高效,但适合简单应用。中断接收通常需要启用接收中断,并在回调函数中处理数据。需要给出示例代码,比如在main函数初始化后启动接收中断,然后在HAL_UART_RxCpltCallback中处理接收到的数据,并重新启动接收。 此外,需要提醒用户处理接收完成中断回调函数,并重新启动接收以避免丢失后续数据。可能还需要提到DMA方式适合大数据量传输,但用户的问题可能暂时不需要,所以可以简要提及。 还要注意代码的格式和正确性,比如使用正确的句柄名称,比如huart1,以及正确的函数参数。同时,需要确保用户知道在CubeMX中配置UART的正确步骤,比如使能中断等。 最后,生成相关问题,可能涉及不同发送接收方式的区别、如何提高效率、错误处理、DMA使用等,这些问题可以帮助用户进一步学习。</think>[逐步解答] 如何使用STM32 HAL实现UART串口数据收发? 1. **CubeMX基础配置** - 在Pinout视图启用USART外设 - 配置参数:波特率(如115200)、字长(8bit)、校验位、停止位(1bit) - 开启中断(若使用中断模式) - 生成代码时勾选"Generate peripheral initialization as a pair of .c/.h files" 2. **数据发送实现** ```c // 阻塞式发送示例 HAL_UART_Transmit(&huart1, (uint8_t*)"Hello\r\n", 7, 100); // 100ms超时 // 中断发送示例 HAL_UART_Transmit_IT(&huart1, (uint8_t*)txData, sizeof(txData)); // printf重定向(需在代码中添加) int fputc(int ch, FILE *f) { HAL_UART_Transmit_IT(&huart1, (uint8_t *)&ch, 1); osDelay(1); // 确保字符间隔 return 0; } ``` 3. **数据接收实现** ```c // 阻塞式接收(不建议在主循环使用) uint8_t rxData; HAL_UART_Receive(&huart1, &rxData, 1, HAL_MAX_DELAY); // 中断接收(推荐方式) // 在main初始化后启动接收 HAL_UART_Receive_IT(&huart1, &rx_buffer, 1); // 接收完成回调函数 void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if(huart->Instance == USART1) { // 处理接收数据 process_data(rx_buffer); // 重新启动接收 HAL_UART_Receive_IT(&huart1, &rx_buffer, 1); } } ``` 4. **完整工作流程示例** ```c uint8_t rx_data; int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USART1_UART_Init(); HAL_UART_Receive_IT(&huart1, &rx_data, 1); // 启动中断接收 while(1) { // 主循环可执行其他任务 if(new_data_flag) { // 接收完成标志 process_data(rx_data); new_data_flag = 0; } } } ``` [注意事项] 1. 使用中断接收时务必在回调函数中重新启动接收[^2] 2. 波特率误差应控制在2%以内 3. 多字节传输建议使用DMA模式 4. 启用中断后需在CubeMX中配置NVIC优先级
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值