STM32实战总结:HAL之串口

本文详细介绍了STM32中UART和USART的区别,指出USART可支持同步和异步通信,而UART仅用于异步通信。在STM32中,通常将USART视为具有相同异步功能的UART。文章还涵盖了串口的基础知识,如起始位、数据位、校验位、停止位和波特率,并提供了配置串口的步骤。最后,通过实例展示了如何使用HAL库进行串口初始化和发送数据,以及如何重写fputc函数实现串口打印。
摘要由CSDN通过智能技术生成

串口基础参考:嵌入式常见接口协议总结_路溪非溪的博客-CSDN博客

串口内容补充

在STM32中的参考手册中,只讲了USART,并没有提及UART,一开始很疑惑。

难道STM32没有UART?

后面看了数据手册,里面讲了有3个USART和2个UART。

这里讲一下这两个的区别。

UART是通用异步收发器,而USART是通用同步异步收发器。
一般而言,单片机中:

  • 名称为UART的接口一般只能用于异步串行通讯。
  • 名称为USART的接口既可以用于同步串行通讯,也能用于异步串行通讯。

事实上当我们使用USART在异步通信的时候,它与UART没有什么差别。

可是用在同步通信的时候,差别就非常明显了:大家都知道同步通信需要时钟来触发数据传输,也就是说USART相对UART的差别之中的一个就是能提供主动时钟。
如STM32的USART能够提供时钟支持ISO7816的智能卡接口。

在实际使用中,基本不会用到USART的同步功能,又因为USART异步功能和UART一样,所以,基本上可以看做STM32有5个UART。

除了开头链接中讲的基本概念外,以下再补充一些关于串口的知识: 

通信原理

UART用一条传输线将数据一位位地顺序传送,以字符为传输单位。通信中两个字符间的时间间隔多少是不固定的,然而在同一个字符中的两个相邻位间的时间间隔是固定的。

一个字符的信息由起始位、数据位、奇偶校验位和停止位组成。

首先明确,高电平为空闲位;

  • 起始位: 先发出一个逻辑0信号, 表示传输字符的开始,因为串口的起始位都是统一规定的,所以通常不必特意设置。
  • 数据位: 在STM32中,支持8位(包含奇偶校验位)或9位数据位(包含奇偶校验位)
  • 校验位: 数据位加上这一位后, 使得1的位数应为偶数(偶校验)或奇数(奇校验)
  • 停止位: 它是一个字符数据的结束标志. 可以是1位、1.5位、2位的高电平。
  • 空闲位: 处于逻辑1状态, 表示当前线路上没有数据传送。

根据以上可知,每传递一个字符,在不需要奇偶校验的情况下,需要传送10bit。 

常用的波特率有4800、9600、115200,最常用的就是9600。

115200虽然速度快,但是不太稳定,传输距离近。 

查看参考手册

 

32F103xE有5个USART接口。

3个USART,2个UART.

更多功能描述和寄存器操作细节自行查阅参考手册。

老实讲,STM32的参考手册讲的USART比较多,看不太懂,在此记录UART相关的重点内容。

◆PCLK1用于USART2345PCLK2用于USART1

◆全双工的,异步通信。

◆USART利用分数波特率发生器提供宽范围的波特率选择。不要在通信进行中改变波特率寄存器的数值。

◆使用多缓冲器配置的DMA方式,可以实现高速数据通信。

◆总线在发送或接收前应处于空闲状态

◆一个起始位

◆可编程数据字长度(8位或9)

◆校验控制

─ 发送校验位

─ 对接收数据进行校验

◆可配置的停止位-支持12个停止位,由此表明数据帧的结束

◆空闲帧包括了停止位。

◆单独的发送器和接收器使能位

◆检测标志

─ 接收缓冲器满

─ 发送缓冲器空

─ 传输结束标志

◆四个错误检测标志

─ 溢出错误

─ 噪音错误

─ 帧错误

─ 校验错误

◆RX:接收数据串行输。通过过采样技术来区别数据和噪音,从而恢复数据。

◆TX:发送数据输出。当发送器被禁止时,输出引脚恢复到它的I/O端口配置。当发送器被激活, 并且不发送数据时,TX引脚处于高电平。

查看原理图

查看电路设计,如果有使用到相关芯片,再查看相关芯片的数据手册。

这里的CH340是一个USB和TTL的转换芯片。

接到了单片机的USART1接口:

因为需要向PC端打印调试信息,所以,当前只关注发送引脚,即PA9

初始化

打开之前的工程,打开MX文件,接着配置串口。

1、起始位

2、数据位

3、校验位

4、停止位

5、波特率

具体过程如下:

选中USAT1

模式这里选择异步,就相当于是UART

硬件流控不需要。

接着进行基本参数的设置。

注意

字长包含了奇偶校验位

如果不设置奇偶校验位,字长就选8位;

如果设置奇偶校验位,字长就选择9位;

总之,保证数据位是8位

停止位1位即可

采样率默认即可

配置示例:

其他选项可暂时不管。

OK,重新生成代码。

打开新代码,可以看到,多了usart.c文件,以及对应的头文件。

文件里面同样是自动生成的该外设的初始化代码。

看一下其中的一个初始化函数:

void MX_USART1_UART_Init(void)
{

  /* USER CODE BEGIN USART1_Init 0 */

  /* USER CODE END USART1_Init 0 */

  /* USER CODE BEGIN USART1_Init 1 */

  /* USER CODE END USART1_Init 1 */
  huart1.Instance = USART1;
  huart1.Init.BaudRate = 115200;
  huart1.Init.WordLength = UART_WORDLENGTH_8B;
  huart1.Init.StopBits = UART_STOPBITS_1;
  huart1.Init.Parity = UART_PARITY_NONE;
  huart1.Init.Mode = UART_MODE_TX;
  huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
  huart1.Init.OverSampling = UART_OVERSAMPLING_16;
  if (HAL_UART_Init(&huart1) != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN USART1_Init 2 */

  /* USER CODE END USART1_Init 2 */

}

第一行

huart1是一个结构体变量,通过调用Instance指针来设置USART1各寄存器的地址。通常,初始化第一步就是这样,定义好寄存器的各地址,方便后续操作。

第二行及往后,都是在设置串口,分别是波特率、数据位长度、停止位、奇偶校验、发送还是接收、流控、采样等。这里,和MX中设置的是一致的。

电路连接

将USB连接到单片机的串口电路,并在PC端安装CH340驱动。

打开SecureCRT,创建终端。

点击确定连接。

查看HAL串口库函数

前期工作都准备好了,现在开始写代码,通过串口输出信息。

那么,HAL中串口相关的函数有哪些呢?

查看相关驱动层文件stm32f1xx_hal_uart.h和stm32f1xx_hal_uart.c

有很多输出函数:

/* Exported functions --------------------------------------------------------*/
/** @addtogroup UART_Exported_Functions
  * @{
  */

/** @addtogroup UART_Exported_Functions_Group1 Initialization and de-initialization functions
  * @{
  */

/* Initialization/de-initialization functions  **********************************/
HAL_StatusTypeDef HAL_UART_Init(UART_HandleTypeDef *huart);
HAL_StatusTypeDef HAL_HalfDuplex_Init(UART_HandleTypeDef *huart);
HAL_StatusTypeDef HAL_LIN_Init(UART_HandleTypeDef *huart, uint32_t BreakDetectLength);
HAL_StatusTypeDef HAL_MultiProcessor_Init(UART_HandleTypeDef *huart, uint8_t Address, uint32_t WakeUpMethod);
HAL_StatusTypeDef HAL_UART_DeInit(UART_HandleTypeDef *huart);
void HAL_UART_MspInit(UART_HandleTypeDef *huart);
void HAL_UART_MspDeInit(UART_HandleTypeDef *huart);

/* Callbacks Register/UnRegister functions  ***********************************/
#if (USE_HAL_UART_REGISTER_CALLBACKS == 1)
HAL_StatusTypeDef HAL_UART_RegisterCallback(UART_HandleTypeDef *huart, HAL_UART_CallbackIDTypeDef CallbackID, pUART_CallbackTypeDef pCallback);
HAL_StatusTypeDef HAL_UART_UnRegisterCallback(UART_HandleTypeDef *huart, HAL_UART_CallbackIDTypeDef CallbackID);

HAL_StatusTypeDef HAL_UART_RegisterRxEventCallback(UART_HandleTypeDef *huart, pUART_RxEventCallbackTypeDef pCallback);
HAL_StatusTypeDef HAL_UART_UnRegisterRxEventCallback(UART_HandleTypeDef *huart);
#endif /* USE_HAL_UART_REGISTER_CALLBACKS */

/**
  * @}
  */

/** @addtogroup UART_Exported_Functions_Group2 IO operation functions
  * @{
  */

/* IO operation functions *******************************************************/
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);
HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);
HAL_StatusTypeDef HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_UART_DMAPause(UART_HandleTypeDef *huart);
HAL_StatusTypeDef HAL_UART_DMAResume(UART_HandleTypeDef *huart);
HAL_StatusTypeDef HAL_UART_DMAStop(UART_HandleTypeDef *huart);

HAL_StatusTypeDef HAL_UARTEx_ReceiveToIdle(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint16_t *RxLen, uint32_t Timeout);
HAL_StatusTypeDef HAL_UARTEx_ReceiveToIdle_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_UARTEx_ReceiveToIdle_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);

/* Transfer Abort functions */
HAL_StatusTypeDef HAL_UART_Abort(UART_HandleTypeDef *huart);
HAL_StatusTypeDef HAL_UART_AbortTransmit(UART_HandleTypeDef *huart);
HAL_StatusTypeDef HAL_UART_AbortReceive(UART_HandleTypeDef *huart);
HAL_StatusTypeDef HAL_UART_Abort_IT(UART_HandleTypeDef *huart);
HAL_StatusTypeDef HAL_UART_AbortTransmit_IT(UART_HandleTypeDef *huart);
HAL_StatusTypeDef HAL_UART_AbortReceive_IT(UART_HandleTypeDef *huart);

void HAL_UART_IRQHandler(UART_HandleTypeDef *huart);
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart);
void HAL_UART_TxHalfCpltCallback(UART_HandleTypeDef *huart);
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);
void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart);
void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart);
void HAL_UART_AbortCpltCallback(UART_HandleTypeDef *huart);
void HAL_UART_AbortTransmitCpltCallback(UART_HandleTypeDef *huart);
void HAL_UART_AbortReceiveCpltCallback(UART_HandleTypeDef *huart);

void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size);

/**
  * @}
  */

/** @addtogroup UART_Exported_Functions_Group3
  * @{
  */
/* Peripheral Control functions  ************************************************/
HAL_StatusTypeDef HAL_LIN_SendBreak(UART_HandleTypeDef *huart);
HAL_StatusTypeDef HAL_MultiProcessor_EnterMuteMode(UART_HandleTypeDef *huart);
HAL_StatusTypeDef HAL_MultiProcessor_ExitMuteMode(UART_HandleTypeDef *huart);
HAL_StatusTypeDef HAL_HalfDuplex_EnableTransmitter(UART_HandleTypeDef *huart);
HAL_StatusTypeDef HAL_HalfDuplex_EnableReceiver(UART_HandleTypeDef *huart);
/**
  * @}
  */

/** @addtogroup UART_Exported_Functions_Group4
  * @{
  */
/* Peripheral State functions  **************************************************/
HAL_UART_StateTypeDef HAL_UART_GetState(UART_HandleTypeDef *huart);
uint32_t              HAL_UART_GetError(UART_HandleTypeDef *huart);

当前,我们需要向PC端发送信息,所以目前重点关注函数是IO operation functions中的发送函数,发送结束后不用中断提醒,采用常规的轮询模式来发送即可。

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

其返回值是一个状态:

typedef enum
{
  HAL_OK       = 0x00U,
  HAL_ERROR    = 0x01U,
  HAL_BUSY     = 0x02U,
  HAL_TIMEOUT  = 0x03U
} HAL_StatusTypeDef;

第一个参数是指定哪个串口,已经在初始化中定义过了,具体参数就是初始化的参数:

UART_HandleTypeDef huart1;

第二个参数是个char *,也就是要发送的字符/字符串;

第三个参数是要发送的内容的字节大小;

第四个参数是超时时间,也就是说,必须在规定的时间内发送完,因为开串口的时候用的是systick,所以是毫秒为单位(HAL库中只有少量的TimeOut是us级别,大部分都是ms级别;

具体实现,查看定义:

/**
  * @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, uint8_t *pData, uint16_t Size, uint32_t Timeout)
{
  uint8_t  *pdata8bits;
  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;
    }

    /* Process Locked */
    __HAL_LOCK(huart);

    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 = (uint16_t *) pData;
    }
    else
    {
      pdata8bits  = pData;
      pdata16bits = NULL;
    }

    /* Process Unlocked */
    __HAL_UNLOCK(huart);

    while (huart->TxXferCount > 0U)
    {
      if (UART_WaitOnFlagUntilTimeout(huart, UART_FLAG_TXE, RESET, tickstart, Timeout) != HAL_OK)
      {
        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)
    {
      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;
  }
}

代码实现

在myapplication.h中添加usart.h头文件。

本来是可以添加新文件来实现串口的,不过串口算是一个各个地方都会用到的函数,所以直接放到public文件中。

在public.h中的结构体中增加一个函数指针。

#ifndef _PUBLIC_H_
#define _PUBLIC_H_

#include "stdint.h"

typedef struct 
{
    void (*printToScreen)(uint8_t * stringSize);

} public_operator;

extern public_operator publicOperator;

#endif

实现对应的public.c文件

#include "myapplication.h"

static void PrintToScreen(uint8_t *);

public_operator publicOperator =
{
    PrintToScreen
};

static void PrintToScreen(uint8_t *string)
{
    uint16_t stringSize= sizeof(string);
    uint32_t outtime = stringSize * 8 / 115200 * 1000 + 5;
    
    HAL_UART_Transmit(&huart1, string, stringSize, outtime);
}

在设备初始化函数中调用

static void Peripheral_Set(void)
{
    publicOperator.printToScreen((uint8_t *)"program now begin!");
}

以上代码编译烧录后,会发现可以输出到屏幕上,但是只能看到prog这四个字母,为啥?

这里又犯了一个指针传递的问题,使用sizeof时,一定要注意你算出来的是指针的大小还是指针指向的数据的大小。

当我将“program now begin!”传入发送函数时,我其实传入的是一个首字符的首地址

(uint8_t *)"program now begin!" //强转后是个地址

在发送函数中,使用sizeof计算出的,实际上是指针的大小,也就是4个字节。所以,字节和超时时间的计算都错了。这就是为什么只能打印出4个字节的原因。

这里,先将后两个参数写死吧。

static void PrintToScreen(uint8_t *string)
{ 
    HAL_UART_Transmit(&huart1, string, 20, 5);
}

成功输出:

代码优化

printf函数。

我们输出打印信息,通常会使用printf函数,这是标准输入输出头文件stdio.h中定义的函数。为了更加通用,我们能不能去重写printf,让printf的输出被重定位到我们的串口上呢?

为了实现这一目标,我们要先了解一下printf函数的源码。

跳转到printf的定义,只能到这里:

   /*
    * is equivalent to fprintf, but does not support floating-point formats.
    * You can use instead of fprintf to improve code size.
    * Returns: as fprintf.
    */
#pragma __printf_args
extern _ARMABI int printf(const char * __restrict /*format*/, ...) __attribute__((__nonnull__(1)));

再想跳转,跳不过去了。难道底层源码不对外开放?

看不到源码,只能去其他地方找资料了。将收集到的资料进行记录。

printf函数是将输入的内容以某种指定的格式输出到指定的端口,所以,在printf函数中,有两个最关键的函数:

一个是格式化函数vsprintf,主要功能是格式化打印信息,最终得到纯字符串格式的信息等待输出;

另外一个就是输出字符串的函数,要知道,在C中,是没有真正意义上的字符串的,所以,printf中输出字符串,是通过一个循环来一个一个输出字符来实现的。这个字符输出函数是fputc,该函数也是c的一个标准库函数,被printf调用,输出到指定的硬件上:

C 库函数 int fputc(int ch, FILE *stream) 把参数 char 指定的字符(一个无符号字符)写入到指定的流 stream 中,并把位置标识符往前移动。

网上一个课程里写了个大概实现,参考:

移植的时候,格式化不用管。

既然printf是通过调用fputc来输出的,那么,我们就重写fputc,让输出通过串口输出。

_weak

既然要重写fputc,那么,问题来了,我们根本就进不去fputc这个函数,而且,人家写的好好的库函数,肯定不会轻易让你修改,一旦修改,肯定很多地方都会出错。

怎么办呢?

虽然没有在网上找到相关资料。但是,听课程里说,原fputc函数是个弱函数,在该函数前面加了_weak关键字。

函数名称前面加上__weak 修饰符,我们一般称这个函数为“弱函数”。

加上了__weak 修饰符的函数,用户可以在用户文件中重新定义一个同名函数,最终编译器编译的时候,会选择用户定义的函数,如果用户没有重新定义这个函数,那么编译器就会执行__weak 声明的函数,并且编译器不会报错。所以我们可以在别的地方定义一个相同名字的函数,而不必也尽量不要修改之前的函数,。
 

可见,弱函数就是个“备胎”,先用着,有了正式的就自然而然地被替换掉了。

更多详情参考:嵌入式C进阶三 —— weak | 陌路之引

fputc重写

于是,我们就可以直接写个fputc函数,就能实现重写fputc的目的。

这样,也不用再封装结构体来实现串口输出了。

将之前public中串口相关的内容删除。然后重写fputc,fputc中指定输出到串口即可。

int fputc(int ch, FILE *stream)
{
    HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 5);
    return ch;
}

重写了fputc,也在外设初始化代码那里调用了printf

static void Peripheral_Set(void)
{
    printf("2022-09-07");
}

但是很奇怪,不仅没有输出字符串,而且,连状态机的流水灯也没了。

问题在哪里?

仔细想了想,会不会是设置的问题?

记起来之前学习工程设置时,好像有时候需要勾选工程里的这个选项,表示要使用自带的标准库:

勾选之后,再下载,果然就可以了。

也是,printf本来就是自带的库函数,既然用到了,这里肯定要勾选。

至此,只重写了fputc函数,就实现了串口打印。

实现了printf的移植,可以使用其格式化方式,并且,输出到串口中查看输出。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值