一、引言
通过重定向printf 串口,我们可以把调试信息发送在串口上,方便观察参数信息,对程序进行调试
二、原理
printf函数在C语言标准库中是基于fputc函数实现的。fputc函数用于将一个字符输出到指定的文件流中。在嵌入式系统中,我们可以通过重写fputc函数,将字符输出到串口,从而实现printf函数的重定向。
三、实现
1. 标准库实现(Keil5)
这里我们以stm32f103c8t6 实现printf 向串口发送一个字节为例:
这里不实现通过中断达到连续发送字节的效果,只实现通过重定向printf主动发送一个字节的效果,如果需要前者,可以私信我获取
首先进行USART1和GPIO的初始化:
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); //开启USART1的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA9引脚初始化为复用推挽输出
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA10引脚初始化为上拉输入
/*USART初始化*/
USART_InitTypeDef USART_InitStructure; //定义结构体变量
USART_InitStructure.USART_BaudRate = 9600; //波特率
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //硬件流控制,不需要
USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx; //模式,发送模式和接收模式均选择
USART_InitStructure.USART_Parity = USART_Parity_No; //奇偶校验,不需要
USART_InitStructure.USART_StopBits = USART_StopBits_1; //停止位,选择1位
USART_InitStructure.USART_WordLength = USART_WordLength_8b; //字长,选择8位
USART_Init(USART1, &USART_InitStructure); //将结构体变量交给USART_Init,配置USART1
定义一个通过串口发送一个字节的函数:
/**
* 函 数:串口发送一个字节
* 参 数:Byte 要发送的一个字节
* 返 回 值:无
*/
void Serial_SendByte(uint8_t Byte)
{
USART_SendData(USART1, Byte); //将字节数据写入数据寄存器,写入后USART自动生成时序波形
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); //等待发送完成
/*下次写入数据寄存器会自动清除发送完成标志位,故此循环后,无需清除标志位*/
}
然后将printf重定向到该函数:
/**
* 函 数:使用printf需要重定向的底层函数
* 参 数:保持原始格式即可,无需变动
* 返 回 值:保持原始格式即可,无需变动
*/
int fputc(int ch, FILE *f)
{
Serial_SendByte(ch); //将printf的底层重定向到自己的发送字节函数
return ch;
}
之后调用printf 以把想要显示的内容发送到串口
2. hal库实现(STM32CubeIDE)
这里实现向串口发送字符串的效果
- 通过芯片图对引脚进行配置:
- USART1的中断:
- 要修改RCC的时钟来源为HSE让系统时钟频率为72MHZ
- SYS的Debug,要不然下载不进去代码:
- Connectivity的USART1中设置Mode为:Asynchronous
- 然后ctrl+S生成代码,进入代码编辑页面
- 打开USART.c文件,在Includes处添加stdio.h头文件,在/* USER CODE BEGIN 0 /和/ USER CODE END 0 */添加重定向代码:
/* Includes ------------------------------------------------------------------*/
#include "usart.h"
#include "stdio.h"
/* USER CODE BEGIN 0 */
#ifdef __GNUC__
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif
PUTCHAR_PROTOTYPE
{
HAL_UART_Transmit(&huart1, (uint8_t*)&ch,1,HAL_MAX_DELAY);
return ch;
}
/* USER CODE END 0 */
这里解释条件编译的作用:stm32cubuIDE使用的编译器为GCC,GCC的printf的底层是__io_putchar函数,其他编译器的printf的底层是fputc函数,这样做能保证PUTCHAR_PROTOTYPE能始终指向当前编译器printf的底层,并重定向它。
- 通过以上操作,printf函数就能正确向串口发送字符和字符串。
3. hal库实现(重定向系统调用)
与前面不同的是1和2是通过重定向fputc和__io_putchar函数到HAL_UART_Transmit函数来实现逐字符串口发送 ,而3直接重定向系统调用_write()函数
- _write():系统调用层(更底层、更通用)
- fputc():标准库层(较高层、依赖库实现)
- __io_putchar():ST/Keil 模板层(不是标准 C)
- 进行串口的初始化(uart.c文件)
#include "uart.h"
UART_HandleTypeDef huart1;
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)
{
while (1)
{
}
}
}
int uart_write(const uint8_t *data, size_t len)
{
if (data == NULL || len == 0)
{
return 0;
}
if (HAL_UART_Transmit(&huart1, (uint8_t *)data, (uint16_t)len, 1000) != HAL_OK)
{
return -1;
}
return (int)len;
}
HAL驱动方式的初始化流程就是:
MX_USART1_UART_Init()------>HAL_USART_Init()------>HAL_USART_MSP_Init(),先调用MX_USART1_UART_Init函数初始化协议,再通过函数内的HAL_UART_Init()函数调用HAL_USART_MSP_Init()函数(需要我们自己在stm32f4xx_hal_msp.c文件中实现)初始化GPIO引脚
这里的uart_write()函数是我们重定向_write函数的目标函数,重定向的逻辑在retarget.c文件中
#include "uart.h"
#include <errno.h>
#include <stdio.h>
#include <sys/unistd.h>
void retarget_stdio_init(void) // 关闭缓冲,即使没换行也直接输出
{
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
}
int _write(int file, char *ptr, int len) // 以下为重定性的逻辑
{
if (ptr == NULL || len <= 0) // 如果指针为空或长度不合法,直接返回 0(表示写了 0 字节)
{
return 0;
}
if (file == STDOUT_FILENO || file == STDERR_FILENO) // 这里只把 标准输出/标准错误 重定向到 UART,其他描述符(比如文件、socket)不支持
{
int written = 0;
for (int i = 0; i < len; i++)
{
if (ptr[i] == '\n')
{
if (uart_write((const uint8_t *)"\r", 1) < 0)
{
return -1;
}
written++;
}
if (uart_write((const uint8_t *)&ptr[i], 1) < 0)
{
return -1;
}
written++;
}
return written;
}
errno = EBADF;
return -1;
}
for循环中做了两件事:
- 把
\n转成\r\n
很多串口终端(尤其一些 Windows 终端)需要 CRLF 才能正确换行。
所以遇到\n,先发一个\r,再发送原本的\n。- 每个字符调用一次
uart_write(...,1)
优点:实现简单。
缺点:效率较低(每个字符一次调用/可能一次等待发送)。
如果 uart_write 返回负数,认为写失败,直接返回 -1。
最后返回 written:实际写入的字节数(注意:因为插入了 \r,写入字节数可能 大于 len)。
EBADF= Bad file descriptor。- 表示你传进来的
file不合法/不支持。
以上做完后,串口输出的逻辑就为:
printf("hello\n"); → puts/printf → libc → _write(1, ...) → UART 输出到串口。
- 初始化阶段调用:
retarget_stdio_init();让输出不缓存。
四、总结
本文讲解了如何在标准库中实现printf向串口发送字节以及如何在hal库中发送字符串两种操作,第一次发文章,目的是记录自己在学习过程中解决问题的过程,如果能帮助到一些人,当然更好,如果有读者认为该文章有错误的地方或者需要改善的地方,欢迎指正,感谢大家!