“悟已往之不谏,知来者之可追”

引言:在正式开课之前,我想先和大家分享一下我对STM32 HAL库的理解。我们为什么要选择学习HAL库?主要原因如下:首先,它能显著提升程序开发的效率;其次,相比标准库,它在代码移植方面具有更高的效率和准确性;最重要的是,HAL库通过函数封装大大降低了底层硬件的理解门槛,我们只需要掌握函数功能就能快速上手,这为项目开发节省了大量时间。因此在我的STM32F407 HAL库教学中,我们将直接从函数调用入手,重点讲解关键原理,帮助大家快速掌握实际应用技能。

今天我们要学习的是串口通信这一重要内容。作为后续STM32开发的核心工具,串口在MaixCam摄像头参数传输、调试信息接收和视觉反馈等场景中都扮演着关键角色。考虑到这是HAL库基础篇,我们主要聚焦于串口基本功能和基础应用练习。关于更高级的不定长数据收发等进阶内容,将在掌握HAL库基础知识后再进行深入讲解。

另外有一些同学反馈说自己的CubeMX无法正常安装STM32F4的固件库,我已经将STM32F1和F4的固件库打包到网盘里了,无法正常安装的同学可以自行安装:

通过网盘分享的文件:STM32F4学习资料
链接: https://pan.baidu.com/s/1SHrhxexShiv2NIGoTw0upg?pwd=j5t3 提取码: j5t3 
--来自百度网盘超级会员v3的分享

(另外我还将串口助手也附在了我们的学习资料里,大家自行下载哈)

从网盘安装固件包步骤如下:

首先先查看固件库安装位置是否配置正确

检查路径没有问题后,我们再使用快捷键Alt+U或者点击Hlep->Manage embedded software pacages打开固件包管理界面。

接下来我将F4的固件库卸载一下,带大家重新安装一下,下图是卸载后对应位置的显示情况

首先,打开我分享的固件库的链接,将两个安装包下载到自己选取的文件夹中(注意,不要下载到Repository这个文件夹里)。

下载完成后,记住压缩包安装的位置,然后我们再打开固件库管理界面,点击“From Local...”选择已下载的F4固件库安装包,点击打开,会弹出协议同意,我们点击同意,等待进度条加载完成,F4的固件库便安装完成了。具体的步骤如下图所示:

我们可以查看保存固件库的文件夹,会发现F4的固件库已经存在于我们的文件夹里了!如果有疑问,可以在评论区或者私信我,我会尽力为大家解决。

一、 USART串口通信基础

1.认识USART

USART (Universal Synchronous Asynchronous Receiver Transmitter),全称通用同步异步收发器。是一种全双工串行通信协议,支持:

异步模式(UART模式):无需时钟线,依靠预定义的波特率通信(最常见)。

同步模式需额外时钟线(如USART_CK)同步数据传输。

支持单线半双工多处理器通信等扩展模式。

接下来让我和大家介绍一下串行通信与并行通信,全双工与半双工以及同步和异步:

1.1 串行通信和并行通信

串行通信:数据通过单根信号线,按时间顺序一位一位传输的通信方式。我们以过马路为例,假设有8个人(代表8位数据) 需要从马路一侧(发送端)到另一侧(接收端)。如果采用串行通信的方式过马路,仅一条狭窄的人行道,8人排成单列依次通过

从图中我们可以看出来,串行通信的优势是只需要一条通道(节省空间与成本),不受步伐差异的影响(时序简单),卡车干扰只影响当前通过的人(抗干扰强)。但是串行通信所付出的代价也是显而易见的——传输时间过长(需要顺序通过)。

并行通信:数据通过多根信号线,在同一时间进行多位数据传输的通信方式。同样以过马路为例,若采用并行通信的方式过马路,马路可视作被划分成8条平行的人行道,所有的人同时通过。

并行通信相对于串行通信的优势是它可以瞬时完成传输(速度快),但是它需要8倍宽度的马路(硬件成本高),若有人步伐不一致(信号偏移),队伍会混乱(数据错误),一辆轿车经过(电磁干扰)可能会影响多条人行道,这些物理干扰使得其难以提速,串行通过提升单通道速度反而更高效可靠。

总结:

串行:牺牲瞬时速度,换取远距离、高可靠、低成本的通信,主导现代数据传输(USB/PCIe/以太网)。

并行:追求理论高带宽,但受限于物理干扰,逐渐被高速串行技术取代。

我们STM32对程序进行调试输出,就是采用串行通信的方式。

1.2 全双工和半双工

全双工:指的是通信双方可以同时发送和接收数据。常见的全双工通信就是手机,我们通过手机电话方式进行通信,双方可以同时说话,也可以同时听到对方说话。

半双工:指的是通信双方虽然可以发送和接收数据,但是在同一时间,只能有一方发送数据,另一方只能接收数据。常见的半双工通信是对讲机,一方进行信息传达时,另一方只能等汇报结束才能回复。若双方同时进行信息传达,则会造成数据碰撞。

另外还有一种叫做单工通信,所谓单工通信,就是通信双方一方只负责发送数据,另一方只负责接收数据。收音机便是这种通信方式的代表。

1.3 同步和异步

同步模式如同交响乐团的精准合奏:指挥家(USART_CK时钟线)持续挥动指挥棒,乐手(数据位)严格按每个节拍演奏。其核心功能是通过物理时钟信号实时对齐收发双方时序,实现高速连续传输。数据流无需起始/停止位,如同不间断的乐章,在时钟上升/下降沿被采样。例如,在智能卡读卡器中,STM32通过同步USART输出时钟脉冲(如3.5MHz),智能卡芯片严格跟随该时钟逐位传输加密数据——如同乐手紧盯指挥棒,确保每比特数据在精确时刻被读取,避免异步通信的时序误差,特别适合金融交易等高速高可靠性场景。同步模式主要接线如下所示
 

异步模式如同两个带手表的陌生人约定碰头:双方无实时联系,仅靠预对时(波特率)和暗号(帧结构)协作。其功能是在无时钟线下,通过起始位(举手示意)、数据位(传递信息)、停止位(结束动作)自主同步。例如,STM32向PC发送传感器数据时,TX线突然拉低(起始位“举手”)唤醒接收端,随后以115200bps速率发送8位数据(如"25.6℃"的ASCII码),最后拉高停止位(“点头结束”)。接收端检测到起始位后,按本地时钟采样后续数据——如同两人靠各自手表在约定时刻收发信息,虽可能因手表误差(波特率偏差>3%)错认字符,但仅需两根导线,成本低廉且抗干扰强,成为调试、传感器通信的首选。异步模式主要通信如下图所示:
 

2.UART与USART的区别

对于UART和USART的区别,我们可以笼统将它认为是否具备同步通信的能力。对于UART,它仅具备异步通信的能力,而USART既具有异步通信能力,又具备同步通信的能力。对于CubeMX上对串口的配置,是以4个USART的形式和2个UART的形式进行配置的,如下图:

功能上USART是兼容UART的,因此在使用串口时,我们可以先对USART进行配置,一般情况下设置成异步(Asynchronous)通信模式即可。下为UART与USART区别表格,仅供参考。

3. STM32F407的USART外设简介

3.1 支持的USART接口数量及引脚分配

STM32F407VGT6支持的串行通信接口共有6个,其中包括4个USART(同步和异步接口)和2个UART(异步接口),下图为串口默认引脚接口对应表格,当然,串口也有复用外设引脚,以后再做讨论。

3.2 时钟配置对串口通信的影响

时钟配置对串口通信的影响至关重要,它直接决定了通信是否能够成功建立以及数据传输的可靠性。串口通信的核心是发送端和接收端必须使用完全相同的波特率(位速率),而这个波特率的精度和稳定性完全依赖于双方的时钟配置。

3.3 HAL库USART驱动配置

接下来就是对我们的CubeMX进行配置,首先不要忘记我们的最基础的配置——Serial Wire模式和RCC时钟的配置。

基础配置完成后,我们对串口进行相关配置(以USART1为例),其中波特率、字长、奇偶校验和停止位在没有特定需求的情况下都保持默认即可。

最后选择右上角的“GENERATE CODE”生成代码,进入Keil5,进行全局编译

首先我们查看CubeMX对usart在代码上的相关配置

向下翻找还会发现一些引脚的配置,都是CubeMX直接配置好的,我们放心使用。接下来便开始本次课程的核心教学。

    二、串口相关代码例程学习

    1.打印数据前必要的准备——重定向

    重定向(Redirection) 是将标准输出函数(如 printf)从默认的终端转向自定义硬件(如USART串口)的关键技术。重定向代码如下:

    int fputc(int ch,FILE *p)
    {
        while(!(USART1->SR &(1<<7)));
        USART1->DR = ch;
        return ch;
    }

    我们在main.c文件中插入代码(插入到/* USER CODE BEGIN XXX */和/* USER CODE END XXX */之间,这样通过CubeMX更新工程代码时不会被清除掉)

    另外,我们需要检查微库(MicroLIB)是否启用,这个不启用的话是无法在串口调试助手这里打印数据的(曾经踩过的雷)。

    2. USART阻塞式发送接收

    阻塞式(Blocking Mode)是USART通信中最基础、最简单的数据传输方式,其核心特征是 “发送/接收完成前,CPU持续等待,不执行其他任务”

    阻塞式发送接收函数定义如下,不过请记住,这些都不需要记,这是HAL库已经给我们封装好的函数,不需要记下来!!!只是想让大家了解一下HAL库给我们做了哪些工作

    /**阻塞式发送函数
      * @brief  Sends an amount of data in blocking mode.
      * @note   When UART parity is not enabled (PCE = 0), and Word Length is configured to 9 bits (M1-M0 = 01),
      *         the sent data is handled as a set of u16. In this case, Size must indicate the number
      *         of u16 provided through pData.
      * @param  huart Pointer to a UART_HandleTypeDef structure that contains
      *               the configuration information for the specified UART module.
      * @param  pData Pointer to data buffer (u8 or u16 data elements).
      * @param  Size  Amount of data elements (u8 or u16) to be sent
      * @param  Timeout Timeout duration
      * @retval HAL status
      */
    HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size, uint32_t Timeout)
    {
      const uint8_t  *pdata8bits;
      const uint16_t *pdata16bits;
      uint32_t tickstart = 0U;
    
      /* Check that a Tx process is not already ongoing */
      if (huart->gState == HAL_UART_STATE_READY)
      {
        if ((pData == NULL) || (Size == 0U))
        {
          return  HAL_ERROR;
        }
    
        huart->ErrorCode = HAL_UART_ERROR_NONE;
        huart->gState = HAL_UART_STATE_BUSY_TX;
    
        /* Init tickstart for timeout management */
        tickstart = HAL_GetTick();
    
        huart->TxXferSize = Size;
        huart->TxXferCount = Size;
    
        /* In case of 9bits/No Parity transfer, pData needs to be handled as a uint16_t pointer */
        if ((huart->Init.WordLength == UART_WORDLENGTH_9B) && (huart->Init.Parity == UART_PARITY_NONE))
        {
          pdata8bits  = NULL;
          pdata16bits = (const uint16_t *) pData;
        }
        else
        {
          pdata8bits  = pData;
          pdata16bits = NULL;
        }
    
        while (huart->TxXferCount > 0U)
        {
          if (UART_WaitOnFlagUntilTimeout(huart, UART_FLAG_TXE, RESET, tickstart, Timeout) != HAL_OK)
          {
            huart->gState = HAL_UART_STATE_READY;
    
            return HAL_TIMEOUT;
          }
          if (pdata8bits == NULL)
          {
            huart->Instance->DR = (uint16_t)(*pdata16bits & 0x01FFU);
            pdata16bits++;
          }
          else
          {
            huart->Instance->DR = (uint8_t)(*pdata8bits & 0xFFU);
            pdata8bits++;
          }
          huart->TxXferCount--;
        }
    
        if (UART_WaitOnFlagUntilTimeout(huart, UART_FLAG_TC, RESET, tickstart, Timeout) != HAL_OK)
        {
          huart->gState = HAL_UART_STATE_READY;
    
          return HAL_TIMEOUT;
        }
    
        /* At end of Tx process, restore huart->gState to Ready */
        huart->gState = HAL_UART_STATE_READY;
    
        return HAL_OK;
      }
      else
      {
        return HAL_BUSY;
      }
    }
    
    /**阻塞式接收
      * @brief  Receives an amount of data in blocking mode.
      * @note   When UART parity is not enabled (PCE = 0), and Word Length is configured to 9 bits (M1-M0 = 01),
      *         the received data is handled as a set of u16. In this case, Size must indicate the number
      *         of u16 available through pData.
      * @param  huart Pointer to a UART_HandleTypeDef structure that contains
      *               the configuration information for the specified UART module.
      * @param  pData Pointer to data buffer (u8 or u16 data elements).
      * @param  Size  Amount of data elements (u8 or u16) to be received.
      * @param  Timeout Timeout duration
      * @retval HAL status
      */
    HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)
    {
      uint8_t  *pdata8bits;
      uint16_t *pdata16bits;
      uint32_t tickstart = 0U;
    
      /* Check that a Rx process is not already ongoing */
      if (huart->RxState == HAL_UART_STATE_READY)
      {
        if ((pData == NULL) || (Size == 0U))
        {
          return  HAL_ERROR;
        }
    
        huart->ErrorCode = HAL_UART_ERROR_NONE;
        huart->RxState = HAL_UART_STATE_BUSY_RX;
        huart->ReceptionType = HAL_UART_RECEPTION_STANDARD;
    
        /* Init tickstart for timeout management */
        tickstart = HAL_GetTick();
    
        huart->RxXferSize = Size;
        huart->RxXferCount = Size;
    
        /* In case of 9bits/No Parity transfer, pRxData needs to be handled as a uint16_t pointer */
        if ((huart->Init.WordLength == UART_WORDLENGTH_9B) && (huart->Init.Parity == UART_PARITY_NONE))
        {
          pdata8bits  = NULL;
          pdata16bits = (uint16_t *) pData;
        }
        else
        {
          pdata8bits  = pData;
          pdata16bits = NULL;
        }
    
        /* Check the remain data to be received */
        while (huart->RxXferCount > 0U)
        {
          if (UART_WaitOnFlagUntilTimeout(huart, UART_FLAG_RXNE, RESET, tickstart, Timeout) != HAL_OK)
          {
            huart->RxState = HAL_UART_STATE_READY;
    
            return HAL_TIMEOUT;
          }
          if (pdata8bits == NULL)
          {
            *pdata16bits = (uint16_t)(huart->Instance->DR & 0x01FF);
            pdata16bits++;
          }
          else
          {
            if ((huart->Init.WordLength == UART_WORDLENGTH_9B) || ((huart->Init.WordLength == UART_WORDLENGTH_8B) && (huart->Init.Parity == UART_PARITY_NONE)))
            {
              *pdata8bits = (uint8_t)(huart->Instance->DR & (uint8_t)0x00FF);
            }
            else
            {
              *pdata8bits = (uint8_t)(huart->Instance->DR & (uint8_t)0x007F);
            }
            pdata8bits++;
          }
          huart->RxXferCount--;
        }
    
        /* At end of Rx process, restore huart->RxState to Ready */
        huart->RxState = HAL_UART_STATE_READY;
    
        return HAL_OK;
      }
      else
      {
        return HAL_BUSY;
      }
    }

    最后,我们只需要记住下面两行函数调用的指令即可:

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

    在简单的了解了这两个函数之后,我们就要开始实战应用啦!

    实战1:使用USART阻塞式循环发送“Hello,world!”

    主要相关代码如下:

    /* USER CODE BEGIN Includes */
    #include "stdio.h"
    #include "string.h"// 提供strlen()函数
    /* USER CODE END Includes */
    
    /* USER CODE BEGIN PTD */
    int fputc(int ch,FILE *p)
    {
        while(!(USART1->SR &(1<<7)));
        USART1->DR = ch;
        return ch;
    }
    
    char str[] = "Hello,world!\n";//定义字符数组并存入"Hello,world"!
    /* USER CODE END PTD */
    
    int main(void)
    {
    /* USER CODE BEGIN WHILE */
      while (1)
      {
    		HAL_UART_Transmit(&huart1,(uint8_t *)str,sizeof(str),1000);
    		HAL_UART_Receive(&huart1,(uint8_t *)str,sizeof(str),1000);
            HAL_Delay(1000);
        /* USER CODE END WHILE */
    
        /* USER CODE BEGIN 3 */
      }
      /* USER CODE END 3 */
    }

    对于HAL_UART_Transmit来说:
    第一个参数是串口句柄,写为&huart1,则意为我们使用串口1进行数据的发送。
    第二个参数是要发送的数据,(uint8_t *)是强转成uint8_t类型,必须要有, str是我们定义的字符串数组,里面存放着我们要发送的数据"Hello,world!"
    第三个参数是发送数据的大小,使用sizeof(str)可以完美将其大小得出。
    第四个参数是等待时间(ms),在这个时间里系统会阻塞等待数据发送超时后便不再继续等待发送数据,而是执行下一条指令。

    HAL_UART_Receive则与HAL_UART_Transmit参数相同,只是第二个参数为接收数据的一个数组,而不能是某些字符串。而第三个参数是接收数据的大小,这便显示出我们在HAL_UART_Transmit中发送数组中数据的优势,即填写sizeof(str)即可得出需要接收数据的大小。由于我们目的是要实现Hello,world!在串口调试助手中进行循环显示,因此存放接收数据的数组不需要重新定义。

    为我们的开发板接上ST-Link和TTL转串口,接线要这样接!

    J-Link的连接与ST-Link和TTL转串口的连接方式是一样的,编译完成后通过ST-Link或者J-Link下载程序到开发板上。

    打开我们的串口调试助手,若没有串口调试助手,则可以在我所分享的学习资料中进行安装。

    通过网盘分享的文件:STM32F4学习资料
    链接: https://pan.baidu.com/s/1SHrhxexShiv2NIGoTw0upg?pwd=j5t3 提取码: j5t3 
    --来自百度网盘超级会员v3的分享

    串口驱动还未下载的同学请参考下面文章进行驱动安装

    CH340驱动与串口助手下载安装_ch340串口驱动-CSDN博客

    我们通过串口调试助手,观察现象:


     

     实战2:通过串口调试助手,实现电脑发送一个字符,MCU收到后立即发回同样的字符,最终显示在串口调试助手上。

    代码如下:其主要原理就是串口调试助手接收到我们PC发送的数据,将数据存放到一个数组里,然后再通过串口发送函数,将数组中的数据发送到串口调试助手上,就形成了回显效果。

    /* USER CODE END Header */
    /* Includes ------------------------------------------------------------------*/
    #include "main.h"
    #include "usart.h"
    #include "gpio.h"
    
    /* Private includes ----------------------------------------------------------*/
    /* USER CODE BEGIN Includes */
    #include "stdio.h"	//如果重定向的话,必须要有标准输入输出库
    /* USER CODE END Includes */
    
    /* Private typedef -----------------------------------------------------------*/
    /* USER CODE BEGIN PTD */
    int fputc(int ch,FILE *p)
    {
        while(!(USART1->SR &(1<<7)));
        USART1->DR = ch;
        return ch;
    }
    int main(void)
    {
    
      /* USER CODE BEGIN 1 */
    	uint8_t rxData[50]={0};  //接收数据的变量
      /* USER CODE END 1 */
    
      /* MCU Configuration--------------------------------------------------------*/
    
      /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
      HAL_Init();
    
      /* USER CODE BEGIN Init */
    
      /* USER CODE END Init */
    
      /* Configure the system clock */
      SystemClock_Config();
    
      /* USER CODE BEGIN SysInit */
    
      /* USER CODE END SysInit */
    
      /* Initialize all configured peripherals */
      MX_GPIO_Init();
      MX_USART1_UART_Init();
      /* USER CODE BEGIN 2 */
    
    	printf("This is a usart test\n");//打印调试信息,证明串口调试助手可用
      /* USER CODE END 2 */
    
      /* Infinite loop */
      /* USER CODE BEGIN WHILE */
      while (1)
      {
    		HAL_UART_Receive(&huart1,rxData,20,1000);
    		
    		HAL_UART_Transmit(&huart1,rxData,20,1000);
    		
        /* USER CODE END WHILE */
    
        /* USER CODE BEGIN 3 */
      }
      /* USER CODE END 3 */
    }

    将代码下载到板子上,确保接线正确,打开串口调试助手,开始发送数据进行检验,效果如下:

    我们可以发送任意数据,但是不要超长哦!

    找到了一篇非常优秀的文章,中断的知识讲的非常全面,这里特地给各位同学推荐一下,希望和大家一起共同进步!欢迎各位同学在评论区留言哦!

    STM32--中断使用(超详细!)_stm32中断-CSDN博客

    资源分享总结(不定时更新):
    通过网盘分享的文件:STM32F4学习资料
    链接: https://pan.baidu.com/s/1SHrhxexShiv2NIGoTw0upg?pwd=j5t3 提取码: j5t3 
    --来自百度网盘超级会员v3的分享

    Logo

    智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

    更多推荐