通过串口USRAT1实现Printf打印
串口调试是开发STM32必不可多少的工具,printf()直接打印到串口,配上超级终端监控软件运行,简单又实用,那么下面就开始搞!!!:
注意: 本教程基于Eclipse+GCC+Eclipse Embedded C/C++插件搭建的STM32工程 ,STM32在Eclipse下的开发环境搭建请移步: 【windows下基于Eclipse和GCC搭建stm32开发环境(1)】
一、串口初始化配置:
要想通过串口USART打印信息,首先得把串口初始化和驱动搞起来:
1、 串口简介:
USART:通用同步/异步串行接收发送器,即串口。一般使用串口都使用异步收发模式。
以USART1为例,需要用到的HAL库函数有:
UART初始化:HAL_UART_Init ( )
异步串口初始化函数,在源文件:stm32f1xx_hal_uart.c
UART对应的GPIO口初始化:HAL_UART_MspInit ( )
串口MSP初始化,其实就是GPIO口模式、速度的初始化,该函数由HAL_UART_Init()函数自动调用,但是原HAL库中是(_weak)声明的空函数,需要我们重写!
串口发送函数
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)
2、启用HAL库UART代码块:
在建立一个新的工程时,HAL库的很多模块代码是没有启动的,包括UART和DMA模块:
所以我们要启用UAST库文件,具体操作如下图:
去掉Exclude resource from build前面的勾子!
stm32f1xx_hal_dma.c的操作和以上类似,不在过多赘述,那为啥要放开dma模块的代码,我们又没用到,主要是因为USART模块可以使用DMA通道传输数据,在HAL库中相互引用,放开DMA模块代码是为了去除编译错误
3、 串口初始化(基于HAL库编程):
基于HAL_UART_Init(),我们创建自己的UART初始化函数:void Usart1Init(int baudRate)
我们先在app路径下新建两个文件:
后续我们所有模块的驱动代码都放到app路径下,然后我们在usart.c中写如下初始化代码:
#include "stm32f1xx.h"
UART_HandleTypeDef UART1_Handle; // UART1串口的初始化句柄,整个工程堆UART1的操作都要用到这个句柄
void Usart1Init(int baudRate) // baudRate 波特率,初始化时指明
{
/*初始化UART1的句柄(参数配置结构体)*/
UART1_Handle.Instance = USART1; //串口句柄1与串口1对应
UART1_Handle.Init.BaudRate = baudRate; //初始化波特率为输入波特率
UART1_Handle.Init.HwFlowCtl = UART_HWCONTROL_NONE; //无硬件控制流
UART1_Handle.Init.Mode = UART_MODE_TX_RX; //串口异步收发模式
UART1_Handle.Init.OverSampling = UART_OVERSAMPLING_16; //默认过采样16倍
UART1_Handle.Init.Parity = UART_PARITY_NONE; //默认无校验位
UART1_Handle.Init.StopBits = UART_STOPBITS_1; //默认1位停止位
UART1_Handle.Init.WordLength = UART_WORDLENGTH_8B; //8bit数据位
if (HAL_UART_Init(&UART1_Handle) != HAL_OK)
{
//Error_Handler();自己定义一个报错,可以是某个异常中断,便于调试
}
}
/重定义HAL_UART_MspInit函数/
void HAL_UART_MspInit(UART_HandleTypeDef* huart)
{
GPIO_InitTypeDef UART1_GPIO = {0};
if(huart->Instance == USART1)
{
__HAL_RCC_USART1_CLK_ENABLE(); //使能串口1时钟
__HAL_RCC_GPIOA_CLK_ENABLE(); //GPIOA时钟使能
UART1_GPIO.Pin = GPIO_PIN_9 | GPIO_PIN_10; //初始化A9 A10
UART1_GPIO.Alternate = GPIO_AF7_USART1; //串口1复用模式
UART1_GPIO.Mode = GPIO_MODE_AF_PP; //推挽复用模式
UART1_GPIO.Pull = GPIO_PULLUP; //上拉
UART1_GPIO.Speed = GPIO_SPEED_FREQ_VERY_HIGH; //高速
HAL_GPIO_Init(GPIOA,&UART1_GPIO); //GPIO初始化函数
HAL_NVIC_SetPriority(USART1_IRQn, 1, 0); //串口中断初始化及使能
HAL_NVIC_EnableIRQ(USART1_IRQn);
}
}
usart.h文件:
#ifndef USART_USART_H_
#define USART_USART_H_
#include "stm32f1xx.h" //因为用到HAL库,所以.h文件也要引用头文件
extern UART_HandleTypeDef UART1_Handle; //.c文件中定义的全局变量要在.h中进行extern声明,以便于在其他文件中调用
void Usart1Init(int baudRate); //函数声明
void HAL_UART_MspInit(UART_HandleTypeDef* huart);
#endif /* USART_USART_H_ */
二、USART1中断函数重定向
虽然进行了UART模块的初始化,但是中断函数也需要我们配置一下,那中断在哪里引用的呢? 答案是:向量表!
首先找到中断向量表定义的位置:
可以看到向量表中USART1串口对应的中断函数是名为USART1_IRQHandler
找一下这个USART1_IRQHandler函数:
鼠标放在这个函数名上面,然后按住Ctrl再按鼠标左键:
找到定义的位置:
可以看到这个中断函数是一个若定义,并且指定了一个别名!也就是说只要执行USART1中断,就运行名为Default_Handler的函数,这是在工程建立时默认的,所有中断都这样,这是需要我们后面重定义的
看一下默认中断中干了啥:
可以看到默中断中啥也没干,就是个死循环,也就是说我们如果不重新指明USART1中断的话,只要触发串口中断,就会进入死循环
那我们怎么重新指明USART1中断呢?
首先改别名:
然后新建这个别名函数:
这样的话只要有中USART1断函数就会进入我们自己的中断函数:
那我们自己的中断函数干嘛呢? 这时候就用到HAL库了,调用HAL库USART1中断函数就好了:
同时我们要把app.h头文件包含进来:
引入app.h主要是为了引入usart.h进而引用extern UART_HandleTypeDef UART1_Handle; 句柄
好了,到这里未知,USART的配置就完成了!
接下来在main函数中应用:
三、串口收发消息调试
1、超级终端连接串口:
超级终端软件推荐:MobaXterm(非常好,开源免费)、Xshell(收费,需破解)
本文以MobaXterm软件为例:
打开串口后,可以看到串口不停的打印出字符串“测试!”,但是好像有一个问题,为啥不是从头开的?
看一下代码:
加上“\r”之后,烧录程序,再次测试:
三、newlib库printf函数重定向
串口已经配置好了,并且能够在超级终端打印信息,那么用printf()打印会怎么样呢?
答案是:看不到打印!
1、newlib库:
这时候我们了解一下newlib库是个啥东西:
由于我们创建工程的时候,选择了使用newlib库,所以我们代码中已经包含了newlib库的代码:
newlib库介绍:
Newlib是一个面向嵌入式系统的与GNU兼容的嵌入式C运行库的其中一种。最初是由Cygnus Solutions收集组装的一个源代码集合,取名为newlib
但是从成熟度来讲,newlib是最优秀的。newlib具有独特的体系结构,使得它能够非常好地满足深度嵌入式系统的要求。newlib可移植性强,具有可重入特性、功能完备等特点,已广泛应用于各种嵌入式系统中。
Newlib的所有库函数都建立在20个桩函数的基础上,这20个桩函数完成一些newlib无法实现的功能:
级I/O和文件系统访问(open、close、read、write、lseek、stat、fstat、fcntl、link、unlink、rename);
扩大内存堆的需求(sbrk);
获得当前系统的日期和时间(gettimeofday、times);
各种类型的任务管理函数(execve、fork、getpid、kill、wait、_exit);
那么newlib库和printf()函数啥关系呢?
printf()是C标准库中的函数,在prinf函数运行时会调用C运行库中的桩函数_write()
这个函数是写的意思,往哪儿写呢?先看一下该函数:
可以看到,原工程是_write函数是调用了DMA串口发送函数,关于DMA的应用,后续文章讲解!
那么我们如果想让printf打印的东西显示在串口连接的超级终端中,我们是不是只需要修改_write函数就行了!
怎么修改呢?接着往下看:
2、write函数重定向:
我们在_write函数中调用串口发送函数,ptr是要发送的字符串指针,len是长度,这个不用管,因为这是printf传给_write函数的,改变_write函数中字符串的写通道,就是所谓的重定向
当然头文件还是要引用进来:
好了,测试一下:
窗口中打印出来printf的内容了!完美!这样我们printf()函数就实现在超级终端的打印了!
结语:
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/a2529280665/article/details/121663539