《嵌入式-STM32开发指南》第二部分 基础篇 - 第6章串口通信(HAL库)

标准库3.5实现:
《嵌入式-STM32开发指南》第二部分 基础篇 - 第6章串口通信

6.1串口简介

通用同步异步收发器(USART)提供了一种灵活的方法与使用工业标准NRZ异步串行数据格式的外部设备之间进行全双工数据交换。USART利用分数波特率发生器提供宽范围的波特率选择。它支持同步单向通信和半双工单线通信,也支持LIN(局部互连网),智能卡协议和IrDA(红外数据组织)SIR ENDEC规范,以及调制解调器(CTS/RTS)操作。它还允许多处理器通信。使用多缓冲器配置的DMA方式,可以实现高速数据通信。图一也就我们熟悉的串口通通信标准。

图1 232通信标准

如图2所示,串口通过三个引脚与其他设备连接在一起。任何USART双向通信至少需要两个脚:接收数据输入(RX)和发送数据输出(TX)。

图2 USB转232

 RX:接收数据串行输入。通过采样技术来区别数据和噪音,从而恢复数据。
 TX :发送数据输出。当发送器被禁止时,输出引脚恢复到它的I/O端口配置。当发送器被激活,并且不发送数据时,TX引脚处于高电平。在单线和智能卡模式里,此I/O 口被同时用于数据的发送和接收。

● 总线在发送或接收前应处于空闲状态 ;
● 一个起始位 ;
● 一个数据字(8或9位),最低有效位在前;
● 1或2个的停止位,由此表明数据帧的结束;
● 使用分数波特率发生器—— 12位整数和4位小数的表示方法。;
● 一个状态寄存器(USART_SR) ;
● 数据寄存器(USART_DR) ;
● 一个波特率寄存器(USART_BRR),12位的整数和4位小数 ;
● 一个智能卡模式下的保护时间寄存器(USART_GTPR) ;
● IrDA_RDI: IrDA模式下的数据输入;
● IrDA_TDO: IrDA模式下的数据输出;
● nCTS: 清除发送,若是高电平,在当前数据传输结束时阻断下一次的数据发送;
● nRTS: 发送请求,若是低电平,表明USART准备好接收数据。
异步串行通信以字符为单位,即一个字符一个字符地传送 。

图3异步串口通信协议

串口外设的架构图(见图 4)看起来十分复杂,实际上对于软件开发人员来说,我们只需要大概了解串口发送的过程即可。从下至上,我们看到串口外设主要由三个部分组成,分别是波特率控制、收发控制和数据存储转移。

 波特率控制
波特率,即每秒传输的二进制位数,用 b/s (bps)表示,通过对时钟的控制可以改变波特率。在配置波特率时,我们向波特比率寄存器 USART_BRR 写入参数,修改了串口时钟 的 分 频值USARTDIV 。 USART_BRR 寄存器包括两部分,分别是 DIV_Mantissa(USARTDIV 的整数部分)和 DIV_Fraction(USARTDIV 的小数)部分,最终,计算公式为 USARTDIV=DIV_Mantissa+(DIV_Fraction/16)。

USARTDIV 是对串口外设的时钟源进行分频的,对于 USART1,由于它挂载在 APB2总线上,所以它的时钟源为 f PCLK2 ;而 USART2、3 挂载在 APB1 上,时钟源则为 fPCLK1,串口的时钟源经过 USARTDIV 分频后分别输出作为发送器时钟及接收器时钟,控制发送和接收的时序。

图4 USART框图

 收发控制
围绕着发送器和接收器控制部分,有好多个寄存器 :CR1、CR2、CR3 和 SR,即USART 的三个控制寄存器(Control Register)及一个状态寄存器(Status Register)。通过向寄存器写入 各种控制参数来控制发送和接收,如奇偶校验位、停止位等,还包括对USART 中断的控制 ;串口的状态在任何时候都可以从状态寄存器中查询得到。其中停止位的配置如图3所示。

发送配置步骤:

1 通过在USART_CR1寄存器上置位UE位来激活USART
2.编程USART_CR1的M位来定义字长。
3.在USART_CR2中编程停止位的位数。
4.如果采用多缓冲器通信,配置USART_CR3中的DMA使能位(DMAT)。按多缓冲器通信中的描述配置DMA寄存器。
5.利用USART_BRR寄存器选择要求的波特率。
6.设置USART_CR1中的TE位,发送一个空闲帧作为第一次数据发送。
7.把要发送的数据写进USART_DR寄存器(此动作清除TXE位)。在只有一个缓冲器的情况下,对每个待发送的数据重复步骤7。
8.在USART_DR寄存器中写入最后一个数据字后,要等待TC=1,它表示最后一个数据帧的传输结束。当需要关闭USART或需要进入停机模式之前,需要确认传输结束,避免破坏最后一次传输。

接收配置步骤:

  1. 将USART_CR1寄存器的UE置1来激活USART。
    2.编程USART_CR1的M位定义字长
    3.在USART_CR2中编写停止位的个数
    4.如果需多缓冲器通信,选择USART_CR3中的DMA使能位(DMAR)。按多缓冲器通信所要求的配置DMA寄存器。
    5.利用波特率寄存器USART_BRR选择希望的波特率。
    6.设置USART_CR1的RE位。激活接收器,使它开始寻找起始位。
图5停止位配置

 数据存储转移

收发控制器根据我们的寄存器配置,对数据存储转移部分的移位寄存器进行控制。当我们需要发送数据时,内核或 DMA 外设(一种数据传输方式,在后面介绍)把数据从内存(变量)写入到发送数据寄存器 TDR 后,发送控制器将适时地自动把数据从 TDR 加载到发送移位寄存器,然后通过串口线 Tx,把数据一位一位地发送出去,当数据从 TDR转移到移位寄存器时,会产生发送寄存器 TDR 已空事件 TXE,当数据从移位寄存器全部发送出去时,会产生数据发送完成事件 TC,这些事件可以在状态寄存器中查询到。而接收数据则是一个逆过程,数据从串口线 Rx 一位一位地输入到接收移位寄存器,然后自动地转移到接收数据寄存器 RDR,最后用内核指令或 DMA 读取到内存(变量)中。

以上对串口通信进行了简单介绍,为了方便各位读者朋友更好的理解,在这里笔者将引入一个新的思想--系统分层思想。既然各位对着有意于嵌入式,那么必须得有对整个系统的架构要有一定的认知。

6.2 STM32Cube生成工程

1.设置RCC
设置高速外部时钟HSE,选择外部时钟源。

图6 RCC配置

2.时钟配置

笔者的板子使用的外部晶振为8MHz,选择外部时钟HSE 8MHz ,PLL锁相环9倍频后为72MHz,系统时钟来源选择为PLL,设置APB2分频器为 /1,这时候定时器的时钟频率为72Mhz。本文笔者使用的定时器是USART1,USART1挂在APB2上,不同的USART挂在不同总线上的。

图7时钟配置

【注】APB1上面连接的是低速外设,包括电源接口、备份接口、CAN、USB、I2C1、I2C2、USART2、USART3、UART4、UART5、SPI2、SP3等;而APB2上面连接的是高速外设,包括UART1、SPI1、Timer1、ADC1、ADC2、ADC3、所有的普通I/O口(PA-PE)、第二功能I/O(AFIO)口等。

3.串口配置
点击USATR1,设置MODE为异步通信(Asynchronous) ,波特率为115200 Bits/s。传输数据长度为8 Bit。奇偶检验无,停止位1 ,接收和发送都使能。

图8基础参数配置

GPIO引脚设置 USART1_RX/USART_TX,默认即可。

图9 USART1的GPIO配置

NVIC Settings 一栏使能接收中断,人默认没打开。

图10 USART1中断使能

6.3串口收发代码讲解

6.3.1串口发送代码实现与讲解(printf重定向)

我们先看如何实现的,再讲解具体的代码。先实现printf重定向函数,在main.c中添加如下函数。

/**
  * @brief 重定向c库函数printf到USARTx
  * @retval None
  */
int fputc(int ch, FILE *f)
{
  HAL_UART_Transmit(&huart1, (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(&huart1, &ch, 1, 0xffff);
  return ch;
}

在main()函数主循环中添加以下代码:

printf("USART1 Test!\n");
HAL_Delay(1000);//这里的延时表示1s。不清楚的请看滴答定时器的内容

好了,这就是串口发送代码实现。另外,笔者在此给出输出格式的说明,请读者朋友参考。

表1 输出格式说明

格式 说明
%d 按照十进制整型数打印
%6d 按照十进制整型数打印,至少6个字符宽
%f 按照浮点数打印
%6f 按照浮点数打印,至少6个字符宽
%.2f 按照浮点数打印,小数点后有2位小数
%6.2f 按照浮点数打印,至少6个字符宽,小数点后有2位小数
%x 按照十六进制打印
%c 打印字符
%s 打印字符串

完整代码请查看配套程序,另外还需添加Use MicroLIB以便支持printf。具体设置参看本节后文的小贴士部分。

好了,我们来总结下串口发送的流程:

1.初始化硬件,时钟;
2.USART 的GPIO初始化,USART参数初始化;
3.重定向printf
4.打印输出

这里只讲解串口的参数初始化,代码如下:

static 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_RX;
  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 */

}

这是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 @ref 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的时候讲解。

HAL_UART_Transmit_IT();串口中断模式发送  
HAL_UART_Transmit_DMA();串口DMA模式发送

6.3.2串口接收代码实现与讲解(中断方式)

和串口发送数据一样,先看如何实现的,然后再进行代码讲解,
在main.c中添加下列定义:

#define RXBUFFERSIZE  256     //最大接收字节数

char TxBuffer[RXBUFFERSIZE];   //发送缓冲
uint8_t RxBuffer;           //接收中断缓冲
uint8_t Uart1_Rx_Cnt = 0;       //接收缓冲计数

在main()主函数中,调用一次接收中断函数

HAL_UART_Receive_IT(&huart1, (uint8_t *)&RxBuffer, 1);

在main.c下方添加中断回调函数

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(&huart1, (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(&huart1, (uint8_t *)&TxBuffer, Uart1_Rx_Cnt,0xFFFF); //将收**加粗样式**到的信息发送出去
      while(HAL_UART_GetState(&huart1) == HAL_UART_STATE_BUSY_TX);//检测UART发送结束
            Uart1_Rx_Cnt = 0;
            memset(TxBuffer,0x00,sizeof(TxBuffer)); //清空数组
        }
    }
    
    HAL_UART_Receive_IT(&huart1, (uint8_t *)&RxBuffer, 1);   //开启接收中断
}

好了,要添加的代码就这些了,下面讲解如何实现串口中断接收的。先看串口接收的编程流程:

1.硬件初始化,时钟初始化;
2.串口GPIO初始化,串口参数配置;
3.在main()函数中使能中断接收;
4.编写HAL_UART_RxCpltCallback中断回调函数,处理接收的数据,

【注】中断接收函数只能触发一次接收中断,所以我们需要在中断回调函数中再次调用中断接收函数。这里可以对比下标准库的流程。

6.3.4实验现象

 串口发送
将程序编译好下载到板子中,打开串口助手,按下图设置相应参数,按下板子的复位按键,在接收区可以看到如下信息。

图11串口发送实验结果

 串口接收
将程序编译好下载到板子中,打开串口助手,按下图设置相应参数,按下板子的复位按键,在接收区可以看到如下信息。

图12 串口接收实验结果

小贴士:printf 函数重定向

要想 printf() 函数工作的话,我们需要把 printf() 重新定向到串口中。重定向是指用户可以自己重写 C 的库函数,当连接器检查到用户编写了与 C 库函数相同名字的函数时,优先采用用户编写的函数,这样用户就可以实现对库的修改了。

为了实现重定向 printf() 函数,我们需要重写 fputc() 这个 C 标准库函数,因为 printf()在 C 标准库函数中实质是一个宏,最终是调用了 fputc() 这个函数。

重定向的这部分工作, 由main.c 文件中的 fputc(int ch, FILE *f) 这个函数来完成。重定向时,我们把 fputc( ) 的形参 ch,作为串口将要发送的数据,也就是说,当使用 printf( ) 时,它先调用这个 fputc( ) 函数,然后使用 ST 库的串口发送函数 USART_SendData(),把数据转移到发送数据寄存器 TDR,触发我们的串口向 PC 发送一个相应的数据。调 用 完 USART_SendData( ) 后 , 要 使 用 while (USART_GetFlagStatus(USART1,USART_FLAG_TC)!= SET) 语句不停地检查串口发送是否完成的标志位TC,一直检测到标志为“完成”,才进入下一步的操作,避免出错。在这段 while 循环检测的延时中,串口外设已经由发送控制器以及根 据我们的配置把数据从移位寄存器一位一位地通过串口线 Tx 发送出去了。

【注意】printf函数在“stdio.h”头文件里,使用该函数必须引用“stdio.h”库, 还
要在编译器中设置一个选项 Use MicroLIB (使用微库)。设置方式如下:
单击Project,选择option选项,再选择Target 勾选Use MicroLIB 即可。

图13设置printf

代码获取方式
1.关注公众号[嵌入式实验楼]
2.在公众号回复关键词[STM32F1]获取资料


欢迎访问我的网站:

BruceOu的哔哩哔哩
BruceOu的主页
BruceOu的博客
CSDN博客
简书

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342