完善资料让更多小伙伴认识你,还能领取20积分哦, 立即完善>
1)实验平台:【正点原子】 NANO STM32F103 开发板
2)摘自《正点原子STM32 F1 开发指南(NANO 板-HAL 库版)》关注官方微信号公众号,获取更多资料:正点原子 第九章 串口通信实验 前面几章介绍了 STM32F1 的 IO 口操作。这一章我们将学习 STM32F 的串口,教大家如何使用 STM32F1 的串口来发送和接收数据。本章将实现如下功能:STM32F1 通过串口和上位机的对话,STM32F1 在收到上位机发过来的字符串后,原原本本的返回给上位机。本章分为如下几个小节: 9.1 STM32 串口简介 9.2 硬件设计 9.3 软件设计 9.4 下载验证 9.5 STM32CubeMX 配置串口 9.1 STM32 串口简介 串口作为 MCU 的重要外部接口,同时也是软件开发重要的调试手段,其重要性不言而喻。 现在基本上所有的 MCU 都会带有串口,STM32 自然也不例外。 STM32 的串口资源相当丰富的,功能也相当强劲。ALIENTEK NANO STM32 开发板所使 用的 STM32F10RBT6 最多可提供 3 路串口,有分数波特率发生器、支持同步单线通信和半双 工单线通讯、支持 LIN、支持调制解调器操作、智能卡协议和 IrDA SIR ENDEC 规范、具有 DMA等。 接下来我们先从寄存器层面,告诉你如何设置串口,以达到我们最基本的通信功能。本章,我们将实现利用串口 1 不停的打印信息到电脑上,同时接收从串口发过来的数据,把发送过来的数据直接送回给电脑。NANO STM32 开发板板载了一个 USB 虚拟串口,我们本章介绍通过USB 串口和电脑通信。 在 4.4.1 章节端口复用功能已经讲解过,对于复用功能的 IO,我们首先要使能 GPIO 时钟,然后使能复用功能时钟,同时要把 GPIO 模式设置为复用功能对应的模式(这个可以查看手册《STM32 中文参考手册 V10》P110 的表格“8.1.11 外设的 GPIO 配置”)。这些准备工作做完之后,剩下的当然是串口参数的初始化设置,包括波特率,停止位等等参数。在设置完成只能接下来就是使能串口,这很容易理解。同时,如果我们开启了串口的中断,当然要初始化 NVIC设置中断优先级别,最后编写中断服务函数。串口设置的一般步骤可以总结为如下几个步骤: 1) 串口时钟使能,GPIO 时钟使能 2) 串口复位 3) GPIO 端口模式设置 4) 串口参数初始化 5) 开启中断并且初始化 NVIC(如果需要开启中断才需要这个步骤) 6) 使能串口 7) 编写中断处理函数 下面,我们就简单介绍下这几个与串口基本配置直接相关的寄存器。 1,串口时钟使能。串口作为 STM32F103 的一个外设,其时钟由外设时钟使能寄存器控制,这里我们使用的串口 1 是在 APB2ENR 寄存器的第 14 位。APB2ENR 寄存器在之前已经介绍过了,这里不再介绍。只是说明一点,就是除了串口 1 的时钟使能在 APB2ENR 寄存器,其他串口的时钟使能位都在 APB1ENR 寄存器。 2,串口波特率设置,STM32 的每个串口都一个自己独立的波特率寄存器USART_BRR,通过设置该寄存器就可以达到配置不停波特率的目的。其各位描述如图 9.1.1 所示: 图 9.1.1 寄存器 USART_BRR 各位描述 STM32 采用了分数波特率,其实就是在这个寄存器(USART_BRR)里面体现的。 USART_BRR 的最低 4 位(位[3:0])用来存放小数部分 DIV_Fraction,紧挨着的 12 位(位[15:4])用来存放整数部分 DIV_Mantissa,最高 16 位未使用。 这里,我们简单介绍下波特率的计算,STM32 的串口波特率计算公式如下: 上式中是给串口的时钟(PCLK1 用于 USART2、3、4、5,PCLK2 用于 USART1); USARTDIV 是一个无符号定点数。我们只要得到 USARTDIV 的值,就可以得到串口波特率寄存器 USART1->BRR 的值,反过来,我们得到 USART1->BRR 的值,也可以推导出 USARTDIV的值。但我们更关心的是如何从 USARTDIV 的值得到 USART_BRR 的值,因为一般我们知道的是波特率,和 PCLKx 的时钟,要求的就是 USART_BRR 的值。 下面我们来介绍如何通过 USARTDIV 得到串口 USART_BRR 寄存器的值。假设我们的串口 1要设置为 115200 的波特率,而 PCLK2 的失踪为 72Mhz。这样根据上面的公式有: USARTDIV=72000000/(115200*16)=39.0625 那么得到: DIV_Fraction=16*0.625=1=0X01; DIV_Mantissa=39=0X27; 这样,我们就得到了 USART1->BRR 的值为 0X0271。只要设置串口 1 的 BRR 寄存器值为 0X0271 就可以得到 115200 的波特率。 当然,并不是任何条件下都可以随便设置串口波特率的,在某些波特率和 PCLK2 频率下,还是会存在误差的,具体可以参考《STM32 中文参考手册》第 525 页的表 176。 3,串口控制。STM32F1 的每个串口都有 3 个控制寄存器 USART_CR1~3,串口的很多功能配置都是通过这 3 个寄存器来设置的。这里我们只要用到 USART_CR1 就可以实现我们的功能了,该寄存器的各位描述如图 9.1.2 所示: 图 9.1.2 USART1_CR1 寄存器各位描述 该寄存器的高 16 位没有用到,低 16 位用于串口的功能设置。UE 为串口使能位,通过该位置 1,以使能串口:M 为字节选择位,当该位为 0 的时候设置串口为 8 个字长外加 n 个停止位,停止位的个数(n)是根据 USART_CR2 的[13:12]位设置来决定的,默认为 0;PCE 为校验使能位,设置为 0,则禁止校验,否则使能校验:PS 为校验位选择位,设置为 0 则为偶校验, 否则为奇校验;TXEIE 为发送缓冲区中断使能位,设置该位为 1,当 USART_SR 中的 TXE 位为 1 时,将产生串口中断;TCIE 为发送完成中断使能位,设置该位为 1,当 USART_SR 中的 TXE 位为 1 时,将产生串口中断;RXNEIE 为接收缓冲区非空中断使能,设置该位为 1,当 USART_SR 中的 ORE 或者 RXNE 位为 1 时,将产生串口中断;TE 为发送使能位,设置为 1, 将开启串口的发送功能:RE 为接收使能位,用法同 TE。 其他位的设置,这里就不一一列举出来了,大家可以参考《STM32 中文参考手册》第 542 页有详细介绍,在这里我们就不列出来了。 4,数据发送与接收 STM32F1 的发送与接收是通过数据寄存器 USART_DR 来实现的,这 是一个双寄存器,包含了 TDR 和 RDR。当向 DR 寄存器写数据的时候,实际是写入 TDR,串 口就会自动发送数据:当收到数据,读 DR 寄存器的时候,实际读取的是 RDR。TDR 和 RDR 对外是不可见的,所以我们操作的就只有 DR 寄存器,该寄存器的各位描述如图 9.1.3 所示: 图 9.1.3 USART_DR 寄存器各位描述 可以看到,虽然是 一个 32 位寄存器,但是只用了低 9 位(DR[8:0]),其他都是保留。 DR[8:0]为串口数据,包含了发送或接收的数据。由于它是由两个寄存器(TDR 和 RDR)组成 的,一个给发送(TDR),一个给接收用(RDR),该寄存器兼具读和写的功能。TDR 寄存器 提供了内部总线和输出移位寄存器之间的并行接口。RDR 寄存器提供了输入移位寄存器和内部 总线之间的并行接口。 当使能校验位(USART_CR1 中 PCE 位被置位)进行发送时,写到 MSB 的值(根据数据的长度不同,MSB 是第 7 位或者第 8 位)会被后来的校验位取代。 当使能校验位进行接收时,读到的 MSB 位是接收到的校验位。 5,串口状态。串口的状态可以通过状态寄存器 USART_SR 读取。USART_SR 的各位描述如图 9.1.4 所示: 图 9.1.4 USART_SR 寄存器各位描述 这里我们关注一下两个位,第 5、6 位 RXNE 和 TC。 RXNE(读数据寄存器非空),当该位被置 1 的时候,就是提示已经有数据被接收到了,并且 可以读出来了。这时候我们要做的就是尽快去读取 USART_DR,通过读 USART_DR 可以将该 位清零,也可以向该位写 0,直接清除。 TC(发送完成),当该位被置位的时候,表示 USART_DR 内的数据已经被发送完成了。如果 设置了这个位的中断,则会产生中断。该位也有两种清零方式: 1)读 USART_SR,写 USART_DR。 2)直接向该位写 0。 通过以上一些寄存器的操作外加一下 IO 口的配置,我们就可以达到串口最基本的配置了, 关于串口更详细的介绍,请参考《STM32 中文参考手册》第 516~548 页,通用同步异步收发器 这一章节。 对于怎么直接使用寄存器配置串口收发,请参考我们寄存器版本教程和源码。接下来我们 将着重讲解使用 HAL 库实现串口配置和使用的方法。HAL 库中,串口相关的函数和定义主要 在文件 stm32f1xx_hal_uart.c 和 stm32f1xx_hal_uart.h 中。接下来我们看看 HAL 库提供的串口相 关操作函数。 1)串口参数初始化(波特率/停止位等),并使能串口。 串口作为 STM32 的一个外设,HAL 库为其配置了串口初始化函数。接下来我们看看串口 初始化函数 HAL_UART_Init 相关知识,定义如下: HAL_StatusTypeDef HAL_UART_Init(UART_HandlerTypeDef *huart); 该函数只有一个入口参数 huart,为 UART_HandleTypeDef 结构体指针类型,我们俗称其为 串口句柄,它的使用会贯穿整个串口程序。一般情况下,我们会定义一个 UART_HandleTypeDef 结构体类型全局变量,然后初始化各个成员变量。接下来我们看看结构体 UART_HandleTypeDef 的定义: typedef struct { USART_TypeDef *Instance; UART_InitTypeDef Init; uint8_t *pTxBuffPtr; uint16_t TxXferSize; __IO uint16_t TxXferCount; uint8_t *pRxBuffPtr; uint16_t RxXferSize; __IO uint16_t RxXferCount; DMA_HandleTypeDef *hdmatx; DMA_HandleTypeDef *hdmarx; HAL_LockTypeDef Lock; __IO HAL_UART_StateTypeDef gState; __IO HAL_UART_StateTypeDef RxState; __IO uint32_t ErrorCode; }UART_HandleTypeDef; 该结构体成员变量非常多,一般情况下载调用函数 HAL_UART_Init 对串口进行初始化的 时候,我们只需要先设置 Instance 和 Init 两个成员变量的值。接下来我们依次解释一下各个成 员变量的含义。 Instance 是 USART_TypeDef 结构体指针变量类型变量,它是执行寄存器基地址,实际上这 个基地址 HAL 库已经定义好了,如果是串口 1,取值为 USART1 即可。 Init 是 UART_InitTypeDef 结构体类型变量,它是用来设置串口的各个参数,包括波特率, 停止位等,它的使用方法非常简单。UART_InitTypeDef 结构体定义如下: typedef struct { uint32_t BaudRate; //波特率 uint32_t WordLength; //字长 uint32_t StopBits; //停止位 uint32_t Parity; //奇偶校验 uint32_t Mode; //收/发模式设置 uint32_t HwFlowCtl; //硬件流设置 uint32_t OverSampling; //过采样设置 }UART_InitTypeDef; 该结构第一个参数 BaudRate 为串口波特率,波特率可以说是串口最重要的参数了,它用来 确定串口通信的速率。第二个参数 WordLength 为字长,可以设置为 8 位字长或者 9 位字长, 这里我们设置为 8 位字长数据格式 UART_WORDLENGTH_8B。第三个参数 StopBits 为停止位 设 置 , 可 以 设 置 为 1 个 停 止 位 或 者 2 个 停 止 位 , 这 里 我 们 设 置 为 1 位 停 止 位 UART_STOPBOTS_1。第四个参数 Parity 设定是否需要奇偶校验,我们设定为无奇偶检验位。 第五个参数 Mode 为串口模块,可以设置为只收模式,只发模式,或者收发模式。这里我们设 置为全双工收发模式。第六个参数 HwFlowCtl 为是否支持硬件流控制,我们设置为无硬件流控 制。第七个参数 OverSampling 用来设置过采样为 16 倍还是 8 倍。 pTxBuddPtr,TxXferSize 和 TxXferCout 三个变量分别用来设置串口发送的数据缓存指针, 发送的数据量和还剩余的要发送的数据量。而接下来的三个变量 pRxBuffPtr,RxXferSize 和 RxXferCount 则是用来设置接收的数据缓存指针,接收的最大数据量以及还剩余的要接收的数 据量。这六个变量是 HAL 库处理中间变量,详细使用方法在我们讲解中断服务函数的时候给 大家讲解。 hdmatx 和 hdmarx 是串口 DMA 相关的变量,指向 DMA 句柄,这里我们先不讲解。 其他的三个变量就是一些 HAL 库处理过程状态标志位和串口通信的错误码。 函数 HAL_UART_Init 使用的一般格式为: UART_HandleTypeDef UART1_Handler; //UART 句柄 UART1_Handler.Instance=USART1; //USART1 UART1_Handler.Init.BaudRate=bound; //波特率 UART1_Handler.Init.WordLength=UART_WORDLENGTH_8B; //字长为 8 位数据格式 UART1_Handler.Init.StopBits=UART_STOPBITS_1; //一个停止位 UART1_Handler.Init.Parity=UART_PARITY_NONE; //无奇偶校验位 UART1_Handler.Init.HwFlowCtl=UART_HWCONTROL_NONE; //无硬件流控 UART1_Handler.Init.Mode=UART_MODE_TX_RX; //收发模式 HAL_UART_Init(&UART1_Handler); //HAL_UART_Init()会使能 UART1 这里我们需要说明的是,函数 HAL_UART_Init 内部会调用串口使能函数使能相应串口, 所以调用了该函数之后我我们就不需要重复使能串口了。当然,HAL 库也提供了具体的串口使 能和关闭方法,具体使用方法如下: __HAL_UART_ENABLE(handler);//使能句柄 handler 指定的串口 __HAL_UART_DISABLE(handler);//关闭句柄 hander 指定的串口 这里还需要提醒大家,串口作为一个重要外设,在调用的初始化函数 HAL_UART_Init 内 部,会先调用 MSP 初始化回调函数进行 MCU 相关的初始化,函数为: void HAL_UART_MspInit(UART_HandleTypeDef*huart); 我们在程序中,只需要重写该函数即可。一般情况下,该函数内部用来编写 IO 口初始化, 时钟使能以及 NVIC 配置。 2)使能串口和 GPIO 口时钟 我们要使用串口,所以我们必须使能串口时钟和使用到的 GPIO 口时钟。例如我们要使用 串口 1,我们必须使能串口 1 时钟和 GPIOA 时钟(串口 1 使用的是 PA9 和 PA10)。具体方法 如下: __HAL_RCC_USART1_CLK_ENABLE();//使能 USART1 时钟 __HAL_RCC_GPIOA_CLK_ENABLE();//使能 GPIOA 时钟 使能相关方法我们在时钟系统相关章节有讲解,操作方法也非常简单,这里我们就不重复讲解。 3)GPIO 口初始化设置(速度,上下拉等)以及复用映射配置 我们在跑马灯实验中讲解过,在 HAL 库中 IO 口初始化参数设置和复用映射配置是在函数 HAL_GPIO_Init 中一次性完成的。这里大家只需要注意,我们要复用 PA9 和 PA10 为串口 发送接收相关引脚,我们需要配置 IO 口为复用,同时复用映射到串口 1。配置源码如下: GPIO_InitTypeDef GPIO_Initure; GPIO_Initure.Pin=GPIO_PIN_9; //PA9 GPIO_Initure.Mode=GPIO_MODE_AF_PP; //复用推挽输出 GPIO_Initure.Pull=GPIO_PULLUP; //上拉 GPIO_Initure.Speed=GPIO_SPEED_FREQ_HIGH;//高速 HAL_GPIO_Init(GPIOA,&GPIO_Initure); //初始化 PA9 GPIO_Initure.Pin=GPIO_PIN_10; //PA10 GPIO_Initure.Mode=GPIO_MODE_AF_INPUT;//模式要设置为复用输入模式! HAL_GPIO_Init(GPIOA,&GPIO_Initure); //初始化 PA10 4)开启串口相关中断,配置串口中断优先级 HAL 库中定义了一个使能串口中断的标识符__HAL_UART_ENABLE_IT,大家可以把它当 一个函数来使用,具体定义请参考 HAL 库文件 stm32f1xx_hal_uart.h 中该标识符定义。例如我 们要使能接收完成中断,方法如下: __HAL_UART_ENABLE_IT(huart,UART_IT_RXNE); //开启接收完成中断 第一个参数为我们步骤 1 讲解的串口句柄,类型为 UART_HandleTypeDef 结构体类型。第 二个参数为我们要开启的中断类型值,可选值在头文件 stm32f1xx_hal_uart.h 中有宏定义。 有开启中断就有关闭中断,操作方法为: __HAL_UART_DISABLE_IT(huart,UART_IT_RXNE); //关闭接收完成中断 对于中断优先级配置,方法就非常简单,详细知识请参考 4.5 小节相关知识。参考方法为: HAL_NVIC_EnableIRQ(USART1_IRQn); //使能 USART1 中断通道 HAL_NVIC_SetPriorty(USART1_IRQn,3,3); //抢占优先级 3,子优先级 3 5)编写中断服务函数 串口 1 中断服务函数为: void USART1_IRQHandler(void); 当发生中断的时候,程序就会执行中断服务函数。然后我们在中断服务函数中编写们相应 的逻辑代码即可。HAL 库实际上对中断处理过程进行了完整的封装,具体内容我们在 9.3 小节 通过结合实验源码给大家详细讲解。 6)串口数据接收和发送 STM32F1 的发送与接收是通过数据寄存器 USART_DR 来实现的,这是一个双寄存器,包 含了 TDR 和 RDR。当向该寄存器写数据的时候,串口就会自动发送,当收到数据的时候,也 是存在该寄存器内。HAL 库操作 USART_DR 寄存器发送数据的函数是: HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef*huart uint8_t*pData,uint16_t Size,uint32_t Timeout); 通过该函数向串口寄存器 USART_DR 写入一个数据。 HAL 库操作 USART_DR 寄存器读取串口接收到的数据的函数是: HAL_StatusTypeDef HAL_UART_Receive(UART_Handler TypeDef*huart,uint8_t*pData,uint16_t Size,uint32_t Timeout); 通过该函数可以读取串口接收到的数据。 9.2 硬件设计 本实验需要用到的硬件资源有: 1) 指示灯 DS0 2) 串口 1 串口 1 之前还没有介绍过,本实验用到的串口 1 与 USB 串口并没有在 PCB 上连接在一起, 需要通过跳线帽来连接一下。这里我们把 P5 的 RXD 和 TXD 用跳线帽与 PA9 和 PA10 连接起 来。如图 9.2.1 所示: 图 9.2.1 硬件连接图示意图 连接上这里之后,我们在硬件上就设置完成了,可以开始软件设计了。 9.3 软件设计 本小节,我们首先会讲解使用 HAL 库配置串口的一般步骤。然后我们会具体讲解我们串 口实验程序实现。ALIENTEK 编写的串口相关的源码在 SYSTEM 分组之下的 usart.c 和 usart.h 中。 9.1 小节我们讲解了 HAL 库中串口操作的一般步骤以及操作函数。在使用 HAL 库配置串 口的时候,HAL 库为我们封装了串口配置步骤。接下来我们以串口接收中断为例讲解 HAL 库 串口程序执行流程。 和其他外设一样,HAL 库为串口的使用开放了 MSP 函数。在串口初始化函数 HAL_UART_Init 内部,会调用串口 MSP 函数 HAL_UART_MspInit 来设置与 MCU 相关的配置。 根据前面的讲解,函数 HAL_UART_Init 主要用来初始化与串口相关的函数(这些参数与 MCU 无关),包括波特率,停止位等。而串口 MSP 函数 HAL_UART_MspInit 用来设置 GPIO 初始 化,NVIC 配置等于 MCU 相关的配置。 这里我们定义了一个函数 uart_init 用来调用 HAL_UART_Init 初始化串口参数配置,具体 函数如下: UART_HandleTypeDef UART1_Handler; //UART 句柄 //初始化 IO 串口 1 //bound:波特率 void uart_init(u32 bound) { //UART 初始化设置 UART1_Handler.Instance=USART1; //USART1 UART1_Handler.Init.BaudRate=bound; //波特率 UART1_Handler.Init.WordLength=UART_WORDLENGTH_8B; //字长为 8 位数据格式UART1_Handler.Init.StopBits=UART_STOPBITS_1; //一个停止位 UART1_Handler.Init.Parity=UART_PARITY_NONE; //无奇偶校验位 UART1_Handler.Init.HwFlowCtl=UART_HWCONTROL_NONE; //无硬件流控 UART1_Handler.Init.Mode=UART_MODE_TX_RX; //收发模式 HAL_UART_Init(&UART1_Handler); //HAL_UART_Init()会使能 UART1 HAL_UART_Receive_IT(&UART1_Handler, (u8 *)aRxBuffer, RXBUFFERSIZE); //该函数会开启接收中断:标志位 UART_IT_RXNE,并且设置接收缓冲以及接收缓冲 接收最大数据量 } 该函数实现的是我们 9.1 小节讲解的步骤 1 的内容。同时这里大家需要注意,最后一行代 码调用函数 HAL_UART_Receive_IT,作用是开启接收中断,同时设置接收的缓存区以及接收 的数据量,对于这个缓冲我们在湖面会给大家讲解它的作用。 串口 MSP 函数 HAL_UART_MspInit 函数我们自定义了其内容,代码如下: void HAL_UART_MspInit(UART_HandleTypeDef *huart) { //GPIO 端口设置 GPIO_InitTypeDef GPIO_Initure; if(huart->Instance==USART1)//如果是串口 1,进行串口 1 MSP 初始化 { __HAL_RCC_GPIOA_CLK_ENABLE(); //使能 GPIOA 时钟 __HAL_RCC_USART1_CLK_ENABLE(); //使能 USART1 时钟 __HAL_RCC_AFIO_CLK_ENABLE(); GPIO_Initure.Pin=GPIO_PIN_9; //PA9 GPIO_Initure.Mode=GPIO_MODE_AF_PP; //复用推挽输出 GPIO_Initure.Pull=GPIO_PULLUP; //上拉 GPIO_Initure.Speed=GPIO_SPEED_FREQ_HIGH;//高速 HAL_GPIO_Init(GPIOA,&GPIO_Initure); //初始化 PA9 GPIO_Initure.Pin=GPIO_PIN_10; //PA10 GPIO_Initure.Mode=GPIO_MODE_AF_INPUT;//模式要设置为复用输入模式! HAL_GPIO_Init(GPIOA,&GPIO_Initure); //初始化 PA10 #if EN_USART1_RX HAL_NVIC_EnableIRQ(USART1_IRQn); //使能 USART1 中断通道 HAL_NVIC_SetPriority(USART1_IRQn,3,3); //抢占优先级 3,子优先级 3 #endif } } 该函数代码实现的是我们 9.1 小节讲解的步骤 2 到 4 的内容。这里大家需要注意,在该段代码中,通过判断宏定义标识符 EN_USART1_RX 的值来确定是否开启串口中断通道和设置串 口 1 中断优先级。标识符 EN_USART1_RX 在头文件 usart.h 中有定义,默认情况下我们设置为 1。 #define EN_USART1_RX 1 //使能(1)/禁止(0)串口 1 接收 通过上面两个函数,我们就配置了串口相关设置。接下来就是编写中断服务函数 USART1_IRQHandler。而 HAL 库中,对中断服务函数的编写有非常严格的讲究。 首先 HAL 库定义了一个串口中断处理通用函数 HAL_UART_IRQHandler,该函数声明如 下: void HAL_UART_IRQHandler(UART_HandleTypeDef*huart); 该函数只有一个入口参数就是 UART_HandleTypeDef 结构体指针类型的串口句柄 huart,使 用我们在调用 HAL_UART_Init 函数时需要设置的同一个变量即可。该函数一般在中断服务函 数中调用,作为串口中断处理的通用入口。一般调用方法为: void USART1_IRQHandler(void) { HAL_UART_IRQHandler(&UART1_Handler);//调用 HAL 库中断处理公用函数 ....//中断处理完成后的结束工作 } 也就是说,真正的串口中断处理逻辑我们会最终在函数HAL_UART_IRQHandler内部执行。 而该函数是 HAL 库已经定义好,而且用户一般不能随意修改。这个时候大家会问,那么我们 的 中 断 控 制 逻 辑 编 写 在 哪 里 呢 ? 为 了 把 这 个 问 题 讲 清 楚 , 我 们 要 来 看 看 函 数 HAL_UART_IRQHandler 内部具体实现过程。因为本章实验,我们主要实现的是串口中断接收, 也就是每次接收到一个字符后进入中断服务函数来处理。所以我们就以中断接收为例给大家讲 解。这里为了篇幅考虑,我们仅仅列出串口中断执行流程中与接收相关的源码。 函数 HAL_UART_IRQHandler 关于串口接收相关源码如下: void HAL_UART_IRQHandler(UART_HandleTypeDef *huart) { uint32_t isrflags = READ_REG(huart->Instance->SR); uint32_t cr1its = READ_REG(huart->Instance->CR1); uint32_t cr3its = READ_REG(huart->Instance->CR3); uint32_t errorflags = 0x00U; uint32_t dmarequest = 0x00U; errorflags = (isrflags & (uint32_t)(USART_SR_PE | USA RT_SR_FE | USART_SR_ORE | USART_SR_NE)); if(errorflags == RESET) { if(((isrflags & USART_SR_RXNE) != RESET) && ((cr1its & USART_CR1_RXNEIE) != RESET)) { UART_Receive_IT(huart); return; } } ...//此处省略部分代码 } 从代码逻辑可以看出,在函数 HAL_UART_IRQHandler 内部通过判断中断类型是否为接收 完成中断,确定是否调用 HAL 另外一个函数 UART_Receive_IT()。函数 UART_Receive_IT()的 作用是把每次中断接收到的字符保存在串口句柄的缓存指针 pRxBuffPtr 中,同时每次接收一个 字符,其计数器 RxXferCount 减 1,直到接收完成 RxXferSize 个字符之后 RxXferCount 设置为 0,同时调用接收完成回调函数 HAL_UART_RxCpltCallback 进行处理。为了篇幅考虑,这里我 们仅列出 UART_Receive_IT()函数调用回调函数 HAL_UART_RxCpltCallback 的处理逻辑,代 码如下: static HAL_StatusTypeDef UART_Receive_IT(UART_HandleTypeDef*huart) { ...//此处省略部分代码 if(--huart->RxXferCount == 0U) { ...//此处省略部分代码 HAL_UART_RxCpltCallback(huart); } ...//此处省略部分代码 } 最后我们列出串口接收中断的一般流程,如图 9.3.1 所示: 图 9.3.1 串口接收中断执行流程图 这里,我们再把串口接收中断的一般流程进行概括:当接收到一个字符之后,在函数UART_Receive_IT中会把数据保存在串口句柄的成员变量pRxBuffPtr缓存中,同时RxXferCount 计数器减 1。如果我们设置 RxXferSize=10,那么当接收到 10 个字符之后,RxXferCount 会由 10 减到 0(RxXferCount 初始值等于 RxXferSize),这个时候再调用接收完成回调函数 HAL_UART_RxCpltCallback 进行处理。接下来我们看看我们的配置。 首先,我们回到用户函数 uart_init 定义可以看到,在 uart_init 函数中调用完 HAL_UART_Init 后我们还调用 HAL_UART_Receive_IT 开启接收中断,并且初始化串口句柄的缓存相关参数。 代码如下: HAL_UART_Receive_IT(&UART1_Handler, (u8 *)aRxBuffer, RXBUFFERSIZE); 而 aRXBuffer 是我们定义的一个全局数据变量,RXBUFFERSZIE 是我们定义的一个标识 符: #define RXBUFFERSIZE 1 u8 aRxBuffer[RXBUFFERSIZE]; 所以调用 HAL_UART_Receive_IT 函数后,除了开启接收中断外还确定了每次接收 RXBUFFERSIZE 个字符后标示接收结束从而进入回调函数HAL_UART_RxCpltCallback 进行相 应处理。最后我们看看 HAL_UART_RxCpltCallback 函数定义: void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if(huart->Instance==USART1)//如果是串口 1 { if((USART_RX_STA&0x8000)==0)//接收未完成 { if(USART_RX_STA&0x4000)//接收到了 0x0d { if(aRxBuffer[0]!=0x0a)USART_RX_STA=0;//接收错误,重新开始 else USART_RX_STA|=0x8000; //接收完成了 } else //还没收到 0X0D { if(aRxBuffer[0]==0x0d)USART_RX_STA|=0x4000; else { USART_RX_BUF[USART_RX_STA&0X3FFF]=aRxBuffer[0] ; USART_RX_STA++; if(USART_RX_STA>(USART_REC_LEN-1))USART_RX_STA=0; //接收数据错误,重新开始接收 } } } } } 因为我们设置了串口句柄成员变量 RxXferSize 为 1,也就是每当串口 1 发生了接收完成中 断后(接收到一个字符),就会跳到该函数执行。当串口接收到一个字符后,它会保存在缓存aRxBuffer 中,由于我们设置了缓存大小为 1,而且 RxXferSize=1,所以每次接受一个字符,会 直接保存到 RxXferSize[0]中,我们直接通过读取 RxXferSize[0]的值就是本次接收到的字符。这 里我们设计了一个小小的接收协议:通过这个函数,配合一个数组 USART_RX_BUF[],一个 接收状态寄存器 USART_RX_STA(此寄存器其实就是一个全局变量,由作者自行添加。由于 它起到类似寄存器的功能,这里暂且称之寄存器)实现对串口数据的接收管理。 USART_RX_BUF 的大小由 USART_REC_LEN 定义,也就是一次接收的数据最大不能超过 USART_REC_LEN 个字节。USART_RX_STA 是一个接收状态寄存器其各的定义如表 9.3.2 所 示: 表 9.3.2 接收状态寄存器位定义表 设计思路如下: 当接收到从电脑发过来的数据,把接收到的数据保存在 USART_RX_BUF 中,同时在接收状态 寄存器(USART_RX_STA)中计数接收到的有效数据个数,当收到回车(回车的表示由 2 个 字节组成:0X0D 和 0X0A)的第一个字节 0X0D 时,计数器将不再增加,等待 0X0A 的到来, 而如果 0X0A 没有来到,则认为这次接收失败,重新开始下一次接收。如果顺利接收到 0X0A, 则标记 USART_RX_STA 的第 15 位,这样完成一次接收,并等待该位被其他程序清除,从而 开始下一次的接收,而如果迟迟没有收到 0X0D,那么在接收数据超过 USART_REC_LEN 的时 候,则会丢弃前面的数据,重新接收。 在函数 USART1_IRQHandler 的结尾还有几行行代码,其中部分代码是超时退出逻辑,关 键逻辑代码如下: while (HAL_UART_GetState(&UART1_Handler) != HAL_UART_STATE_READY); while(HAL_UART_Receive_IT(&UART1_Handler, (u8 *)aRxBuffer, RXBUFFERSIZE) != HAL_OK); 这两行代码作用非常简单,第一行代码是判断串口是否就绪,如果没有就绪就等待就绪。 第二行代码是继续调用 HAL_UART_Receive_IT 函数来开启中断和重新设置 RxXferSize 和 RxXferCount 的初始值为 1,也就是开启新的接收中断。 学到这里大家会发现,HAL 库定义的串口中断逻辑确实非常复杂,并且因为处理过程繁 琐所以效率不高。这里我们需要说明的是,在中断服务函数中,大家也可以不用调用 HAL_UART_IRQHandler 函数,而是直接编写自己的中断服务函数。串口实验我们之之所以 遵循 HAL 库写法,是为了让大家对 HAL 库有一个更清晰的理解。 如果我们不用中断处理回调函数,那么就不用初始化串口句柄的中断接收缓存,所以我们 HAL_UART_Receive_IT 函数就不用出现在初始化函数 uart_init 中,而是直接在要开启中断的 地方调用__HAL_UART_ENABLE_IT 单独开启中断即可。如果不用中断回调函数处理,中断服 务函数内容为: //串口 1 中断服务程序 void USART1_IRQHandler(void) { u8 Res; HAL_StatusTypeDef err; #if SYSTEM_SUPPORT_OS //使用 OS OSIntEnter(); #endif if((__HAL_UART_GET_FLAG(&UART1_Handler,UART_FLAG_RXNE)!=RESET)) //接收中断(接收到的数据必须是 0x0d 0x0a 结尾) { Res=USART1->DR; if((USART_RX_STA&0x8000)==0)//接收未完成 { if(USART_RX_STA&0x4000)//接收到了 0x0d { if(Res!=0x0a)USART_RX_STA=0;//接收错误,重新开始 else USART_RX_STA|=0x8000; //接收完成了 } else //还没收到 0X0D { if(Res==0x0d)USART_RX_STA|=0x4000; else { USART_RX_BUF[USART_RX_STA&0X3FFF]=Res ; USART_RX_STA++; if(USART_RX_STA>(USART_REC_LEN-1))USART_RX_STA=0; //接收数据错误,重新开始接收 } } } } HAL_UART_IRQHandler(&UART1_Handler); #if SYSTEM_SUPPORT_OS //使用 OS OSIntExit(); #endif } 这段代码逻辑跟上面的中断回调函数类似,只不过这里还需要通过 HAL 库串口接收函数 HAL_UART_Receive 来获取接收到的字符进行相应的处理,这里我们就不做过多的讲解。在我 们后面很多实验,为了效率和处理逻辑方便,我们会选择将接收控制逻辑直接编写在中断函数 内部。 HAL 库一共提供了 5 个中断处理回调函数: void HAL_UART_TxCpltCallback(UART_HandleTypeDef*huart);//发送完成回调函数 void HAL_USART_TxHalfCpltCallback(USART_HandleTypeDef *husart);//发送完成过半 void HAL_UART_RxCpltCallback(UART_HandleTypeDef*huart);//接收完成回调函数 Void HAL_UART_RxHalfCpltCallbackk(UART_HandleTypeDef*huart);//接收完成过半 Void HAL_UART_ErrorCallback(UART_HandleTypeDef*huart);//错误处理回调函数 有兴趣的同学可以自行测试每个回调函数的使用方法,这里我们就不做过多讲解。最后我们来 看看主函数: int main(void) { u8 len; u16 times=0; HAL_Init(); //初始化 HAL 库 Stm32_Clock_Init(RCC_PLL_MUL9); //设置时钟,72M delay_init(72); //初始化延时函数 uart_init(115200); //初始化串口 115200 LED_Init(); //初始化 LED KEY_Init(); //初始化按键 while(1) { if(USART_RX_STA&0x8000) { len=USART_RX_STA&0x3fff;//得到此次接收到的数据长度 printf("rn 您发送的消息为:rn"); HAL_UART_Transmit(&UART1_Handler,(uint8_t*)USART_RX_BUF,len,1000); //发送接收到的数据 while(__HAL_UART_GET_FLAG(&UART1_Handler, UART_FLAG_TC)!=SET);//等待发送结束 printf("rnrn");//插入换行 USART_RX_STA=0; }else { times++; if(times%5000==0) { printf("rnALIENTEK NANO STM32 开发板 串口实验rn"); printf("正点原子@ALIENTEKrnrnrn"); } if(times%200==0)printf("请输入数据,以回车键结束rn"); if(times%30==0)LED0=!LED0;//闪烁 LED,提示系统正在运行. delay_ms(10); } } } 这段代码逻辑比较简单,首先判断全局变量 USART_RX_STA 的最高位是否为 1,如果为 1 的话,那么代表前一次数据接收已经完成,接下来就是把我们自定义接收缓冲的数据发送到 串口。接下来我们重点以下两句。 HAL_UART_Transmit(&UART1_Handler,(uint8*)USART_RX_BUF,len,1000); While(__HAL_UART_GET_FLAG(&UART1_Handler,UART_FLAG_TC)!=SET); 第一句,其实就是调用 HAL 串口发送函数 HAL_UART_Transmit 来发送一个字符到串口。 第二句呢,就是我们发送一个字节之后,要检测这个数据是否已经被发送完成了。 9.4 下载验证 前面 3 章实例,我们均介绍了软件仿真,仿真的基本技巧也差不多介绍完了,接下来我们 将淡化这部分,因为代码都是经过作者检验,并且全部在 ALIENTEK NANO STM32 开发板上 验证了的,有兴趣的朋友可以自己仿真看看。但是这里要说明几点: 1,IO 口复用的,信号在逻辑分析窗口是不能显示出来的(看不到波形),这一点请大家 注意。比如串口的输出,SPI,USB,CAN 等。你在仿真的时候在该窗口看不到任何信息。遇 到这样的情况,你就不得不准备一个逻辑分析仪,外加一个 ULINK 或者 STLINK 来做在线调 试。但一般情况,这些都是有现成的例子,不用这几个东西一般也能编出来。 2,仿真并不能代表实际情况。只能从某些方面给你一些启示,告诉你大方向,不能尽信仿 真,当然也不能完全没有仿真。比如上面 IO 口的输出,仿真的时候,其翻转速度可以达到很 快,但是实际上 STM32 的 IO 输出就达不到这个速度。 总之,我们要合理的利用仿真,也不能过于依赖仿真。当仿真解决不了了,可以试试在线 调试,在线调试一般都可以知道问题在哪个地方,但是问题要怎么解决还是得各位自己动脑筋、 找资料了。 把程序下载到到 NANO STM32 V1 板子,可以看到板子上的 DS0 开始闪烁,说明程序已经 在跑了。串口调试助手,我们用 XCOM V2.0,该软件在光盘有提供,且无需安装,直接可以运 行,但是需要你的电脑安装有.NET Framework 4.0(WIN7 及以上系统直接自带了)或以上版本的 环境才可以,该软件的详细介绍请看:http://www.openedv.com/posts/list/22994.htm 这个帖子。 接着我们打开 XCOM V2.0,设置串口为开发板的 USB 虚拟串(ST 虚拟串口,得根据你自 己的电脑选择,我的电脑是 COM56,另外,请注意:波特率是 115200),可以看到如图 9.4.1 所示信息: 图 9.4.1 串口调试助手收到的信息 从图 9.4.1 可以看出,STM32F1 的串口数据发送是没问题的了。但是,因为我们在程序上 面设置了必须输入回车,串口才认可接收到的数据,所以必须在发送数据后再发送一个回车符, 这里 XCOM 提供的发送方法是通过勾选发送新行实现,如图 9.4.1,只要勾选了这个选项,每 次发送数据后,XCOM 都会自动多发一个回车(0X0D+0X0A)。设置好了发送新行,我们再在发 送区输入你想要发送的文字,然后单击发送,可以得到如图 9.4.2 所示结果: 图 9.4.2 发送数据后收到的数据 可以看到,我们发送的消息被发送回来了(图中圈圈内)。大家可以试试,如果不发送回 车(取消发送新行),在输入内容之后,直接按发送是什么结果。 9.5 STM32CubeMX 配置串口 前面章节我们详细讲解了使用 STM32CubeMX 配置 IO 口输入输出,本小节我们将讲解使 用 STM32CubeMX 配置串口方法。同样,大家直接复制光盘的 STM32CubeMX 配置的工程模 板,目录为:“4,程序源码标准例程-HAL 库函数版本实验 0-3 Template 工程模板-使用 STM32CubeMX 配置”。然后使用 STM32CubeMX 打开该工程(点击工程目录的 Template.ioc)。 这里我们同样不再讲解 RCC 相关配置,我们仅仅讲解串口相关配置方法。 这里我们要配置串口 1,所以首先我们要使能串口 1 然后设置相应通信模式。打开 Pinout 选项卡界面,左侧依次进入 Configuration->Peripherals->USART1 配置栏,如下图 9.5.1 所示: 图 9.5.1 进入 Configuration->Peripherals->USART1 配置栏 USART1 配置栏有 2 个选项。第一个选项 Mode 用来设置串口 1 的模式或者关闭串口 1。 第二个选项 Hardware Flow Control(RS232)用来开启/关闭串口 1 的硬件流控制,该选项只有在 Mode 选项值为 Asynchronous(异步通信)模式的前提下才有效。这里我们要开启串口 1 的异步模 式,并且不使用硬件流控制,所以这里我们直接选择 Mode 值为 Asynchronous 即可。配置好的 USART1 界面如下图 9.5.2 所示: 图 9.5.2 USART1 配置 配置好串口 1 为异步通信模式后,那么在硬件上会使用到 PA9 和 PA10 作为串口 1 的发送 接收引脚。在 STM32CubeMX 中,当我们选择好外设的工作模式之后,软件会自动配置 GPIO 口的相关模式和参数。在 Pinout 界面我们看看芯片引脚图会发现,PA9 和 PA10 端口的模式会自动复用为发送和接收模式,如下图 9.5.3 所示: 图 9.5.3 PA9/PA10 配置 同时,进入 GPIO 配置详细界面会发现,IO 口的模式等参数都做了相应的修改。参考 6.5 小节方法,依次进入 Configuration->GPIO 界面会发现,Pin Configuration 界面多了一个 USART1 选项卡,该选项卡界面便是用来配置和查看串口引脚 PA9 和 PA10 配置参数的。如下图 9.5.4 所示: 图 9.5.4 USART1 引脚详细配置界面 对于外设的功能引脚,在我们使能相应的外设(比如 USART1)之后,STM32CubeMX 会 自动设置 GPIO 相关配置,一般情况下用户不再需要再修改。所以这里,对于 PA9 和 PA10 的配置我们就保留软件配置即可。 接下来我们需要配置 USART1 外设相关的参数,包括波特率,停止位等。我们直接进入 Configuration 选项卡,如果我们之前使能了 USART1,那么在 Conectivity 栏会出现 USART1 配 置按钮。如下图 9.5.5 所示: 图 9.5.5 Configuration 选项卡 接下来我们点击 USART1 配置按钮,进入 USART1 详细参数配置界面。在弹出的 USART1 Configuration 界面会出现 5 个配置选项卡。 Parameter Settings 选项卡用来配置 USART1 的初始化参数,包括波特率停止位等等。这里 我们将 USART1 配置为:波特率 115200,8 位字节模式,无奇偶校验位,发送/接收均开启。 User Constats 是用来配置用户常量。 NVIC 选项卡用来使能 USART1 中断。这里我们勾上 Enabled 选项。 DMA Setting 是在使用 USART1 DMA 的情况才需要配置,这里我们不配置。 GPIO Setting 便是查看和配置 USART1 参数之后,如果我们使用到串口中断,那么我们还 需要设置中断优先级分组。接下来便是配置 NVIC 相关参数。同样的方法,进入 Conguration 选项卡,点击 NVIC 按钮,如下图 8.5.6 所示: 图 9.5.6 Configuration->NVIC 按钮 点击 NVIC 按钮之后,弹出 NVIC 配置界面 NVIC Configuration,如下图 9.5.7 所示: 图 9.5.7 NVIC Configuration 配置界面 在弹出的 NVIC Configuration 界面,我们首先设置中断优先级分组级别,我们系统初始化设置为分组 2,那么就是 2 位抢占优先级和 2 位响应优先级。所以这里的参数我们选择“2 bits forpre-emption prioriy”,也就是 2 位抢占优先级。 配置完中断优先级分组之后,接下来我们要配置的是 USART1 的抢占优先级和响应优先级值,这里我们设置抢占和响应优先级为 3 即可。 进行完上面的操作之后,接下来我们便是生成工程代码。 打开生成的工程可以看到,在 main.c 文件中生成了如下串口初始化关键代码: static void MX_USART1_UART_Init(void) { 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_RX; huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE; huart1.Init.OverSampling = UART_OVERSAMPLING_16; if (HAL_UART_Init(&huart1) != HAL_OK) { _Error_Handler(__FILE__, __LINE__); } } 同时在 stm32f1xx_hal_msp.c 中,生成了串口 MSP 函数 HAL_UART_MspInit 内容如下: void HAL_UART_MspInit(UART_HandleTypeDef* huart) { GPIO_InitTypeDef GPIO_InitStruct; if(huart->Instance==USART1) { __HAL_RCC_USART1_CLK_ENABLE(); GPIO_InitStruct.Pin = GPIO_PIN_9; GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); GPIO_InitStruct.Pin = GPIO_PIN_10; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_NOPULL; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); HAL_NVIC_SetPriority(USART1_IRQn, 3, 3); HAL_NVIC_EnableIRQ(USART1_IRQn); } } 函数 MX_USART1_UART_Init 的内容和本章串口实验源码中函数 uart_init 中调用HAL_UART_Init 函数作用类似,只不过波特率是通过入口参数动态设置。而生成的 MSP 函数HAL_UART_MspInit 内容和实验中该函数的作用就几乎是一模一样了。关于使用 STM32CubeMX 配置串口的方法就给大家介绍到这里。 |
|
相关推荐
|
|
1111 浏览 2 评论
843 浏览 0 评论
嵌入式开发 Win11安装ST-Link 提示 设备描述符请求失败 怎么处理?
968 浏览 0 评论
1254 浏览 1 评论
求助:STM32F407串口控制外设无效,用电脑串口助手有效
2137 浏览 3 评论
小黑屋| 手机版| Archiver| 电子发烧友 ( 湘ICP备2023018690号 )
GMT+8, 2024-8-6 16:21 , Processed in 0.652183 second(s), Total 68, Slave 50 queries .
Powered by 电子发烧友网
© 2015 bbs.elecfans.com
关注我们的微信
下载发烧友APP
电子发烧友观察
版权所有 © 湖南华秋数字科技有限公司
电子发烧友 (电路图) 湘公网安备 43011202000918 号 电信与信息服务业务经营许可证:合字B2-20210191