STM32-HAL-串口的printf重定向

一、C语言的格式化输出

C语言的printf是一个标准库函数,用于将格式化的数据输出到标准的输出设备(通常是终端)

  • 基本语法:
int printf(const char *format, ...);

其中的第一个参数const char *format表示输出格式,后面的参数是可变参数,用于填充格式化字符串中的占位符。

  • 字符输出原理:
  1. 格式化字符串处理:printf函数将第一个参数 const char *format 中的格式占位符解析出来,然后根据占位符的类型和顺序依次取可变参数中的值,将这些值转换为字符串,并将其按照格式化字符串中的顺序和样式组合成最终的输出字符串。
  2. 输出字符串存储:printf函数将格式化后的输出字符串存储在内存缓冲区中。
  3. 输出字符串显示:printf函数将内存缓冲区中的输出字符串显示到标准输出设备上,通常是终端。

在学习C语言的时候,在调用头文件#include "stdio.h"的时候,就可以使用printf函数进行格式化打印

#include <stdio.h>

int a = 10;
char str[] = "hello,world!";
int main(void)
{
    printf("%s\n",str);
    printf("a = %d",a);  
    return 0;
}

[result]

hello,world!
a = 10

但是在Keil中,在stm32的使用中是不能直接使用C语言的打印函数的,需要添加支持设置,即调用MDK的微库(MicroLib) 称之为printf的重定向。其实不仅仅可以把打印字符重定向,而且还可以将获取字符重定向。

二、开发准备
  • 基于STM32L431RCT6的小熊派开发板
    在这里插入图片描述

  • windows系统并安装Cubemx和Keil MDK的电脑

三、初始化片上外设

本次开发介绍的主要是串口的重定向,因此就是需要初始化串口外设。

设置串口通信为异步通信,波特率115200
在这里插入图片描述

生成代码,并选择keil-MDK打开该工程

四、设置重定向

4.1 点击魔术棒,然后勾选使用微库(Use MicroLIB)
在这里插入图片描述

针对MicroLIB的介绍:

​ MicroLIB是Keil公司提供的一个C标准库,专为嵌入式系统设计而开发。相对于标准C库,MicroLIB库更加轻量级,代码量更小,适用于嵌入式系统等资源受限的环境。MicroLIB库支持ISO/ANSI C标准的大部分函数,并增加了一些嵌入式系统常用的函数,例如串口通信、GPIO控制等。在MDK的工程中,开发者可以选择使用MicroLIB库来进行开发,以减小程序的代码大小和占用内存的空间。

​ 需要注意的是,MicroLIB库并不是一个完整的C标准库,它只实现了一部分的C标准函数,并且一些函数的实现与标准C库可能存在差异。如果需要使用标准C库的函数或者功能更加完整的C标准库,开发者需要使用其他的C标准库,例如GNU C Library(glibc)等。

4.2 添加串口重定向代码

main.c函数中添加头文件:

#include "stdio.h"

main.c函数的/* USER CODE BEGIN 4 */内添加下面代码即可:

int fputc(int c,FILE *f)
{
    uint8_t ch;  //定义一个无符号8位整型变量ch 并将字符C赋值给它
    ch = c;
    HAL_UART_Transmit(&huart1,&ch,1,1000);
    // 调用HAL库的串口发送函数,将ch发送到USART1串口,等待时间为1000ms
    return c;
}

4.3 在主循环中添加代码进行测试

/* USER CODE BEGIN 2 */
	uint8_t str[] = "Hello GearLong!";
	uint8_t num1 = 10;
	float f = 3.1415926;
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
		printf("%s\r\n",str);
		printf("num = %d\r\n",num1);
		printf("f1 = %f\r\n",f);
		printf("f2 = %.2f\r\n",f);
		printf("f3 = %8.3f\r\n",f);
		HAL_Delay(1000);		
  }

4.4 编译、下载,然后打开串口调试助手查看执行

记得将AT开关拨到AT_MCU

在这里插入图片描述

4.5 不使用微库打印数据

下面的代码来自正点原子的HAL库代码中的串口打印 ,将代码复制到usart.c的代码添加处,并取消勾选微库,并移除上面已经设置的重定向代码。

usart.c函数中添加头文件:

#include "stdio.h"

usart.c函数/* USER CODE BEGIN 1 */内添加下面代码即可:

/* 加入以下代码, 支持printf函数, 而不需要选择use MicroLIB */

#if 1
#if (__ARMCC_VERSION >= 6010050)                    /* 使用AC6编译器时 */
__asm(".global __use_no_semihosting\n\t");          /* 声明不使用半主机模式 */
__asm(".global __ARM_use_no_argv \n\t");            /* AC6下需要声明main函数为无参数格式,否则部分例程可能出现半主机模式 */

#else
/* 使用AC5编译器时, 要在这里定义__FILE 和 不使用半主机模式 */
#pragma import(__use_no_semihosting)

struct __FILE
{
    int handle;
    /* Whatever you require here. If the only file you are using is */
    /* standard output using printf() for debugging, no file handling */
    /* is required. */
};

#endif

/* 不使用半主机模式,至少需要重定义_ttywrch\_sys_exit\_sys_command_string函数,以同时兼容AC6和AC5模式 */
int _ttywrch(int ch)
{
    ch = ch;
    return ch;
}

/* 定义_sys_exit()以避免使用半主机模式 */
void _sys_exit(int x)
{
    x = x;
}

char *_sys_command_string(char *cmd, int len)
{
    return NULL;
}

/* FILE 在 stdio.h里面定义. */
FILE __stdout;

/* 重定义fputc函数, printf函数最终会通过调用fputc输出字符串到串口 */
int fputc(int ch, FILE *f)
{
    while ((USART1->SR & 0X40) == 0);               /* 等待上一个字符发送完成 */
    
    USART1->DR = (uint8_t)ch;                       /* 将要发送的字符 ch 写入到DR寄存器 */	
    return ch;
}
#endif

注意在使用Cubmex生成的HAL库代码中,USART1的相关寄存器可能随着不同型号的MCU会发生变化,如果编译不通过及时修改即可。

4.6 打印输出的结果是一致的
在这里插入图片描述