开发环境:
IDE:MKD 5.38a
STM32CubeMX: V6.12.0
开发板:STM32H7S78-DK开发板
MCU:STM32H7S7L8H6H
1 串口简介
USART(Universal Synchronous Asynchronous Receiver and Transmitter,通用同步-异步接收发射器)提供了一种灵活的方法与使用工业标准NRZ异步串行数据格式的外部设备之间进行全双工数据交换。USART利用分数波特率发生器提供宽范围的波特率选择。它支持同步单向通信和半双工单线通信,也支持LIN(局部互连网),智能卡协议和IrDA(红外数据组织)SIR ENDEC规范,以及调制解调器(CTS/RTS)操作。它还允许多处理器通信。使用多缓冲器配置的DMA方式,可以实现高速数据通信。
虽然USART既可以同步又可以异步,但是常见的最常用的就是使用功能的异步功能,如果作为异步通信就是UART(Universal Asynchronous Receiver and Transmitter),可以说,UART是USART的子集,但是同步通信相比异步通信多了一根时钟同步信号线。
下面简单介绍下同步和异步。
在同步通讯中,收发设备双方会使用一根信号线表示时钟信号,在时钟信号的驱动下双方进行协调,同步数据,见下图。通讯中通常双方会统一规定在时钟信号的上升沿或下降沿对数据线进行采样。
Figure 1-1 同步通讯
在异步通讯中不使用时钟信号进行数据同步,它们直接在数据信号中穿插一些同步用的信号位,或者把主体数据进行打包,以数据帧的格式传输数据,见下图,某些通讯中还需要双方约定数据的传输速率,以便更好地同步。
Figure 1-2 异步通讯
在同步通讯中,数据信号所传输的内容绝大部分就是有效数据,而异步通讯中会包含有帧的各种标识符,所以同步通讯的效率更高,但是同步通讯双方的时钟允许误差较小,而异步通讯双方的时钟允许误差较大。
从上面的介绍可以看出,USART以同步方式通信需要时钟同步信号,但不需要额外的起始、停止位,可以实现更快的传输速度。但USART控制起来更复杂,因此本文主要讲解以异步通信。
Figure 1-3 异步串口通信协议
异步串行通信以字符为单位,即一个字符一个字符地传送 。
串口外设的架构图看起来十分复杂,实际上对于软件开发人员来说,我们只需要大概了解串口发送的过程即可。从下至上,我们看到串口外设主要由三个部分组成,分别是波特率控制、收发控制和数据存储转移。
Figure 1-4 USART框图
- 波特率控制
波特率,即每秒传输的二进制位数,用 b/s (bps)表示,通过对时钟的控制可以改变波特率。在配置波特率时,我们向波特比率寄存器 USART_BRR 写入参数,修改了串口时钟的分频值USARTDIV。USART_BRR 寄存器包括两部分,分别是 DIV_Mantissa(USARTDIV 的整数部分)和 DIV_Fraction(USARTDIV 的小数)部分,最终,计算公式如下所示。
USARTDIV = DIV_Mantissa+( DIV_Fraction / ( 2- OVER8))。
USARTDIV 是对串口外设的时钟源进行分频的,对于USART1,由于它挂载在 APB2总线上,所以它的时钟源为 PCLK2 ;而 USART2、3 挂载在 APB1上,时钟源则为 PCLK1,串口的时钟源经过 USARTDIV 分频后分别输出作为发送器时钟及接收器时钟,控制发送和接收的时序。
对 USARTDIV 的尾数值和小数值进行编程时,接收器和发送器(Rx和Tx)的波特率均设置为相同值。
公式 1:适用于标准 USART(包括 SPI 模式)的波特率
公式 2:智能卡、 LIN 和 IrDA 模式下的波特率
- 收发控制
围绕着发送器和接收器控制部分,有好多个寄存器 :CR1、CR2、CR3 和 SR,即USART 的三个控制寄存器(Control Register)及一个状态寄存器(Status Register)。通过向寄存器写入 各种控制参数来控制发送和接收,如奇偶校验位、停止位等,还包括对USART 中断的控制 ;串口的状态在任何时候都可以从状态寄存器中查询得到。其中停止位的配置如下图所示。
Figure 1-5 停止位配置
- 通过在USART_CR1寄存器上置位UE位来激活USART
- 编程USART_CR1的M位来定义字长。
- 在USART_CR2中编程停止位的位数。
- 如果采用多缓冲器通信,配置USART_CR3中的DMA使能位(DMAT)。按多缓冲器通信中的描述配置DMA寄存器。
- 利用USART_BRR寄存器选择要求的波特率。
- 设置USART_CR1中的TE位,发送一个空闲帧作为第一次数据发送。
- 把要发送的数据写进USART_DR寄存器(此动作清除TXE位)。在只有一个缓冲器的情况下,对每个待发送的数据重复步骤7。
- 在USART_DR寄存器中写入最后一个数据字后,要等待TC=1,它表示最后一个数据帧的传输结束。当需要关闭USART或需要进入停机模式之前,需要确认传输结束,避免破坏最后一次传输。
- 将USART_CR1寄存器的UE置1来激活USART。
- 编程USART_CR1的M位定义字长
- 在USART_CR2中编写停止位的个数
- 如果需多缓冲器通信,选择USART_CR3中的DMA使能位(DMAR)。按多缓冲器通信所要求的配置DMA寄存器。
- 利用波特率寄存器USART_BRR选择希望的波特率。
- 设置USART_CR1的RE位。激活接收器,使它开始寻找起始位。
收发控制器根据我们的寄存器配置,对数据存储转移部分的移位寄存器进行控制。当我们需要发送数据时,内核或 DMA 外设(一种数据传输方式,在后面介绍)把数据从内存(变量)写入到发送数据寄存器 TDR 后,发送控制器将适时地自动把数据从 TDR 加载到发送移位寄存器,然后通过串口线 Tx,把数据一位一位地发送出去,当数据从 TDR转移到移位寄存器时,会产生发送寄存器 TDR 已空事件 TXE,当数据从移位寄存器全部发送出去时,会产生数据发送完成事件 TC,这些事件可以在状态寄存器中查询到。而接收数据则是一个逆过程,数据从串口线 Rx 一位一位地输入到接收移位寄存器,然后自动地转移到接收数据寄存器 RDR,最后用内核指令或 DMA 读取到内存(变量)中。
以上对串口通信进行了简单介绍,为了方便各位读者朋友更好的理解,在这里笔者将引入一个新的思想--系统分层思想。既然各位对着有意于嵌入式,那么必须得有对整个系统的架构要有一定的认知。对STM32裸机开发,我们可以将分为三层:物理层、协议层和应用层。前文讲了这么多也是对串口协议进行分析,常用的物理层的串口通信标准有232和485。
【注】UART和USART的区别
USART(universal synchronous asynchronous receiver and transmitte): 通用同步异步收发器,USART是一个串行通信设备,可以灵活地与外部设备进行全双工数据交换。
UART(universal asynchronous receiver and transmitter): 通用异步收发器,异步串行通信口(UART)就是我们在嵌入式中常说的串口,它还是一种通用的数据通信议。从名字上可以看出,USART在UART基础上增加了同步功能,即USART是UART的增强型。
当我们使用USART在异步通信的时候,它与UART没有什么区别,但是用在同步通信的时候,区别就很明显了:大家都知道同步通信需要时钟来触发数据传输,也就是说USART相对UART的区别之一就是能提供主动时钟。如STM32的USART可以提供时钟支持ISO7816的智能卡接口。
USART是指单片机的一个端口模块,可以根据需要配置成同步模式(SPI,I2C),也可以将其配置为异步模式,后者就是UART。所以说UART姑且可以称之为一个与SPI,I2C对等的“协议”,而USART则不是一个协议,而是更应该理解为一个实体。相比于同步通讯,UART不需要统一的时钟线,接线更加方便。但是,为了正常的对信号进行解码,使用UART通讯的双方必须事先约定好波特率,即每个码元的长度。
关于串口的深入理解,请参看笔者文章:
https://blog.bruceou.cn/2021/01/detailed-explanation-of-stm32-serial-communication/555/
https://bruceou.blog.csdn.net/article/details/109431870
2 串口通信的寄存器描述
串口常用的寄存器有控制寄存器 (USART_CR)、数据发送寄存器(USART_TDR)、波特比率寄存器(USART_BRR)。
Figure 2-1 控制寄存器1
Figure 2-2 数据发送寄存器
Figure 2-3 波特比率寄存器
3 串口硬件
串口的接口通过三个引脚与其他设备连接在一起。任何USART双向通信至少需要两个脚:接收数据输入(RX)和发送数据输出(TX)。
Figure 3-1 USB转串口
- RX:接收数据串行输入。通过采样技术来区别数据和噪音,从而恢复数据。
- TX :发送数据输出。当发送器被禁止时,输出引脚恢复到它的I/O端口配置。当发送器被激活,并且不发送数据时,TX引脚处于高电平。在单线和智能卡模式里,此I/O 口被同时用于数据的发送和接收。
这里使用USART4,接到ST-LINK的虚拟串口。这样USB转串口不仅可以为开发板供电,还可以与PC串口通信。
4 串口发送(重定向printf)
4.1 STM32Cube生成工程
点击USATR4,设置MODE为异步通信(Asynchronous) ,波特率为115200 Bits/s。传输数据长度为8 Bit。奇偶检验无,停止位1 ,16 倍过采样速度,接收和发送都使能。
Figure 4-1 基础参数配置
GPIO引脚设置 USART_RX->PD0/USART_TX->PD1,默认即可。
Figure 4-2 USART的GPIO配置
然后生成工程即可。
4.2 串口发送代码讲解
下面笔者还要介绍一种常用的串口打印方式I/O重定向,也就是使用printf打印数据到终端,但是我们的裸机系统没有终端,因此如果想让printf / scanf向USART1发送、获取数据,需要通过代码指定C标准库输入/输出函数的控制终端设备,也就是使用功能I/O重定向。
在stdio.h有相应的接口。
/*
* dynamically allocates a buffer of the right size for the
* formatted string, and returns it in (*strp). Formal return value
* is the same as any other printf variant, except that it returns
* -1 if the buffer could not be allocated.
*
* (The functions with __ARM_ prefixed names are identical to the
* ones without, but are available in all compilation modes without
* violating user namespace.)
*/
extern _ARMABI int fgetc(FILE * /*stream*/) __attribute__((__nonnull__(1)));
/*
* reads at most one less than the number of characters specified by n from
* the stream pointed to by stream into the array pointed to by s. No
* additional characters are read after a new-line character (which is
* retained) or after end-of-file. A null character is written immediately
* after the last character read into the array.
* Returns: s if successful. If end-of-file is encountered and no characters
* have been read into the array, the contents of the array remain
* unchanged and a null pointer is returned. If a read error occurs
* during the operation, the array contents are indeterminate and a
* null pointer is returned.
*/
extern _ARMABI int fputc(int /*c*/, FILE * /*stream*/) __attribute__((__nonnull__(2)));
下面我们以实现printf打印数据到USART(即重定义fputc函数)的实现过程。
我们先看如何实现的,再讲解具体的代码。先实现printf重定向函数,在main.c中添加如下函数。
/**
* @retval None
*/
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart4, (uint8_t *)&ch, 1, 0xffff);
return ch;
}
/**
* @brief 重定向c库函数getchar,scanf到USARTx
* @retval None
*/
int fgetc(FILE *f)
{
uint8_t ch = 0;
HAL_UART_Receive(&huart4, &ch, 1, 0xffff);
return ch;
}
另外还需添加微库以便支持printf。具体设置参看本节后文的小贴士部分。
这里只讲解串口的参数初始化,代码如下:
/**
* @brief UART4 Initialization Function
* @param None
* @retval None
*/
static void MX_UART4_Init(void)
{
/* USER CODE BEGIN UART4_Init 0 */
/* USER CODE END UART4_Init 0 */
/* USER CODE BEGIN UART4_Init 1 */
/* USER CODE END UART4_Init 1 */
huart4.Instance = UART4;
huart4.Init.BaudRate = 115200;
huart4.Init.WordLength = UART_WORDLENGTH_8B;
huart4.Init.StopBits = UART_STOPBITS_1;
huart4.Init.Parity = UART_PARITY_NONE;
huart4.Init.Mode = UART_MODE_TX_RX;
huart4.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart4.Init.OverSampling = UART_OVERSAMPLING_16;
huart4.Init.OneBitSampling = UART_ONE_BIT_SAMPLE_DISABLE;
huart4.Init.ClockPrescaler = UART_PRESCALER_DIV1;
huart4.AdvancedInit.AdvFeatureInit = UART_ADVFEATURE_NO_INIT;
if (HAL_UART_Init(&huart4) != HAL_OK)
{
Error_Handler();
}
if (HAL_UARTEx_SetTxFifoThreshold(&huart4, UART_TXFIFO_THRESHOLD_1_8) != HAL_OK)
{
Error_Handler();
}
if (HAL_UARTEx_SetRxFifoThreshold(&huart4, UART_RXFIFO_THRESHOLD_1_8) != HAL_OK)
{
Error_Handler();
}
if (HAL_UARTEx_DisableFifoMode(&huart4) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN UART4_Init 2 */
/* USER CODE END UART4_Init 2 */
}
这是STM32cudeMX自动生成的代码,我们要注意UART_HandleTypeDef结构体,这个结构体就是用来配串口参数的,原型如下:
typedef struct __UART_HandleTypeDef
{
USART_TypeDef *Instance; /*!< UART registers base address */
UART_InitTypeDef Init; /*!< UART communication parameters */
uint8_t *pTxBuffPtr; /*!< Pointer to UART Tx transfer Buffer */
uint16_t TxXferSize; /*!< UART Tx Transfer size */
__IO uint16_t TxXferCount; /*!< UART Tx Transfer Counter */
uint8_t *pRxBuffPtr; /*!< Pointer to UART Rx transfer Buffer */
uint16_t RxXferSize; /*!< UART Rx Transfer size */
__IO uint16_t RxXferCount; /*!< UART Rx Transfer Counter */
DMA_HandleTypeDef *hdmatx; /*!< UART Tx DMA Handle parameters */
DMA_HandleTypeDef *hdmarx; /*!< UART Rx DMA Handle parameters */
HAL_LockTypeDef Lock; /*!< Locking object */
__IO HAL_UART_StateTypeDef gState; /*!< UART state information related to global Handle management
and also related to Tx operations.
This parameter can be a value of [url=home.php?mod=space&uid=1064992]@ref[/url] HAL_UART_StateTypeDef */
__IO HAL_UART_StateTypeDef RxState; /*!< UART state information related to Rx operations.
This parameter can be a value of @ref HAL_UART_StateTypeDef */
__IO uint32_t ErrorCode; /*!< UART Error code */
} UART_HandleTypeDef;
这个结构体很简单,也有英文注释,笔者就不在赘述了。当然啦,除了使用普通方式发送,还可使用中断方式和DMA方式发送数据。这里中断发送数据就不讲了,下面会将中断接收,有兴趣的朋友请自行去看参考手册自行实现,DMA方式会在介绍DMA的时候讲解。
最后在main函数调用printf函数,代码如下:
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
char str[20];
/* USER CODE END 1 */
/* MPU Configuration--------------------------------------------------------*/
MPU_Config();
/* 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_UART4_Init();
/* USER CODE BEGIN 2 */
printf("串口普通方式实验 \r\n");
/* sprintf函数把格式化的数据写入某个字符串 */
sprintf(str,"20%02d-%02d-%02d",24,9,23);
/* 调用格式化输出函数打印输出数据 */
printf("%s\n",str);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
/* Insert delay 200 ms */
HAL_Delay(200);
HAL_GPIO_TogglePin(LED2_GPIO_Port, LED2_Pin);
/* Insert delay 200 ms */
HAL_Delay(200);
HAL_GPIO_TogglePin(LED3_GPIO_Port, LED3_Pin);
/* Insert delay 200 ms */
HAL_Delay(200);
HAL_GPIO_TogglePin(LED4_GPIO_Port, LED4_Pin);
/* Insert delay 200 ms */
HAL_Delay(200);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
好了,这就是串口发送代码实现。
完整代码请查看配套程序,另外还需添加Use MicroLIB以便支持printf。具体设置参看本节后文的小贴士部分。
笔者在此给出输出格式的说明,请读者朋友参考。
Table 4-1 输出格式说明
格式 |
说明 |
%d |
按照十进制整型数打印 |
%6d |
按照十进制整型数打印,至少6个字符宽 |
%f |
按照浮点数打印 |
%6f |
按照浮点数打印,至少6个字符宽 |
%.2f |
按照浮点数打印,小数点后有2位小数 |
%6.2f |
按照浮点数打印,至少6个字符宽,小数点后有2位小数 |
%x |
按照十六进制打印 |
%c |
打印字符 |
%s |
打印字符串 |
我们来总结下串口发送的流程:
1.初始化硬件,时钟;
2.USART 的GPIO初始化,USART参数初始化;
3.重定向printf
4.打印输出
4.3 实验现象
将程序编译好下载到板子中,打开串口助手,按下图设置相应参数,按下板子的复位按键,在接收区可以看到如下信息。
Figure 4-3 串口发送实验结果
5 串口接收数据(中断方式)
5.1 STM32Cube生成工程
和串口发送的大体一致,主要要配置中断。NVIC Settings 一栏使能接收中断,默认没打开。
Figure 5-1 USART中断使能
另外,还需配置中断优先级,默认即可。
Figure 5-2 中断有优先级
5.2 串口接收代码讲解
和串口发送数据一样,先看如何实现的,然后再进行代码讲解,
在main.c中添加下列定义:
#define RXBUFFERSIZE 256 //最大接收字节数
char TxBuffer[RXBUFFERSIZE]; //发送缓冲
uint8_t RxBuffer; //接收中断缓冲
uint8_t Uart1_Rx_Cnt = 0; //接收缓冲计数
在main()主函数中,调用一次接收中断函数
HAL_UART_Receive_IT(&huart4, (uint8_t *)&RxBuffer, 1);
在main.c下方添加中断回调函数
/**
* @brief Rx Transfer completed callbacks.
* @param huart Pointer to a UART_HandleTypeDef structure that contains
* the configuration information for the specified UART module.
* @retval None
*/
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
/* Prevent unused argument(s) compilation warning */
UNUSED(huart);
/* NOTE: This function Should not be modified, when the callback is needed,
the HAL_UART_TxCpltCallback could be implemented in the user file
*/
if(Uart1_Rx_Cnt >= 255) //溢出判断
{
Uart1_Rx_Cnt = 0;
memset(TxBuffer,0x00,sizeof(TxBuffer));
HAL_UART_Transmit(&huart4, (uint8_t *)"数据溢出", 10,0xFFFF);
}
else
{
TxBuffer[Uart1_Rx_Cnt++] = RxBuffer; //接收数据转存
if((TxBuffer[Uart1_Rx_Cnt-1] == 0x0A)&&(TxBuffer[Uart1_Rx_Cnt-2] == 0x0D)) //判断结束位
{
HAL_UART_Transmit(&huart4, (uint8_t *)&TxBuffer, Uart1_Rx_Cnt,0xFFFF); //将收到的信息发送出去
while(HAL_UART_GetState(&huart4) == HAL_UART_STATE_BUSY_TX);//检测UART发送结束
Uart1_Rx_Cnt = 0;
memset(TxBuffer,0x00,sizeof(TxBuffer)); //清空数组
}
}
HAL_UART_Receive_IT(&huart4, (uint8_t *)&RxBuffer, 1); //开启接收中断
}
好了。接下来看看主函数代码:
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MPU Configuration--------------------------------------------------------*/
MPU_Config();
/* 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_UART4_Init();
/* USER CODE BEGIN 2 */
HAL_UART_Receive_IT(&huart4, (uint8_t *)&RxBuffer, 1);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
/* Insert delay 200 ms */
HAL_Delay(200);
HAL_GPIO_TogglePin(LED2_GPIO_Port, LED2_Pin);
/* Insert delay 200 ms */
HAL_Delay(200);
HAL_GPIO_TogglePin(LED3_GPIO_Port, LED3_Pin);
/* Insert delay 200 ms */
HAL_Delay(200);
HAL_GPIO_TogglePin(LED4_GPIO_Port, LED4_Pin);
/* Insert delay 200 ms */
HAL_Delay(200);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
总结下串口接收的编程流程:
1.硬件初始化,时钟初始化;
2.串口GPIO初始化,串口参数配置;
3.在main()函数中使能中断接收;
4.编写HAL_UART_RxCpltCallback中断回调函数,处理接收的数据,
【注】中断接收函数只能触发一次接收中断,所以我们需要在中断回调函数中再次调用中断接收函数。这里可以对比下标准库的流程。
5.3 实验现象
将程序编译好下载到板子中,打开串口助手,按下图设置相应参数,按下板子的复位按键,在接收区可以看到如下信息。
Figure 5-3 串口接收实验结果