【STM32系列】多路USART串口Printf重定向详解——通用

前言

通常情况下,标准的printf函数只能将输出重定向到一个串口。然而,当我们需要在多个串口上进行输出,而又不希望为每个串口单独封装发送函数时,就可以考虑将printf的输出重定向到多个串口。接下来,本文将详细介绍如何实现这一目标。

注:

  • 本篇文章的代码根据正点原子所提供代码改写。
  • 正点原子所提供代码能在不使用半主机模式下能够正确编译和运行代码,这包括声明一些特殊的汇编指令和函数定义,以避免编译器默认使用半主机模式(不需要勾选 “Use MicroLIB”)
  • 本篇文章代码是基于CubeMX编写的HAL库,但是所涉及HAL库的内容不多,稍微修改,即可兼容标准库。

单个串口的printf重定向

单个串口的重定向,可以参考正点原子所提供代码,直接cope进自己工程中即可。

//usart.c

/******************************************************************************************/
/* 加入以下代码, 支持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 ((USART_UX->ISR & 0X40) == 0);    /* 等待上一个字符发送完成 */

    USART_UX->TDR = (uint8_t)ch;            /* 将要发送的字符 ch 写入到DR寄存器 */
    return ch;
}
#endif

/******************************************************************************************/

多串口的printf重定向

usart.h文件

对正点原子的源码进行了一点小的改动,首先在usart.h文件中添加了一个枚举类型,方便用来索引某个串口,用于printf定向:

其次增加了一个新的串口句柄,存储当前使用的USART句柄(标准库注意修改此处):

最后添加了一个当前串口的索引,用于存储当前使用的USART索引:

以下就是上面所说的在usart.h中所需要增加的代码:

//usart.h

/******************** 以下是多路USART串口printf重定向 ********************/

/* 定义USART索引枚举 */
typedef enum {
	USART_NONE,			/* 无USART */
	USART1_IDX,			/* USART1索引 */
	USART2_IDX,			/* USART2索引 */
	USART3_IDX,			/* USART3索引 */
}Current_USART_Indx;

extern UART_HandleTypeDef* Current_USART_Handle;		/* 当前某个USART的句柄 */
extern Current_USART_Indx Current_USART_Printf_Indx;	/* 当前某个USART的索引 */

void Set_Current_USART(Current_USART_Indx indx);		/* 函数声明,用于设置当前使用的USART */

usart.c文件

Set_Current_USART函数用于设置当前使用的USART。它接受一个Current_USART_Indx类型的参数,并根据该参数更新Current_USART_Handle句柄和Current_USART_Printf_Indx索引

/* 
 * 简介:设置当前使用的USART
 * 参数:indx - 要设置的USART索引
 * 这个参数可以是:USARTx_IDX,其中x可以从1~3
 * 使用举例:(必须要将其放在printf函数前面,指定其中一个串口)
 * 		Set_Current_USART(USART1_IDX);
 * 		printf("我是串口1\r\n");
 */
void Set_Current_USART(Current_USART_Indx indx)
{
	switch(indx)
  {
    case USART1_IDX:
    Current_USART_Handle = &huart1;
    Current_USART_Printf_Indx = USART1_IDX;
    break;
    case USART2_IDX:
    Current_USART_Handle = &huart2;
    Current_USART_Printf_Indx = USART2_IDX;
    break;
    case USART3_IDX:
    Current_USART_Handle = &huart3;
    Current_USART_Printf_Indx = USART3_IDX;
    break;
    default:
    Current_USART_Handle = NULL;
    Current_USART_Printf_Indx = USART_NONE;
    break;
  }
}

fputc函数是printf函数输出字符时调用的底层函数,进行了一些改变,使其可以随时自定义重定向到不同串口:

/* 
 * 简介:重定义fputc函数,用于将字符输出到当前设置的USART
 * 参数:
 * ch - 要发送的字符
 * f  - 文件指针(在此实现中未使用)
 * 返回值:发送的字符(或EOF如果出错)
 */
int fputc(int ch, FILE *f)
{
  if(Current_USART_Handle == NULL){			/* 如果当前没有设置USART句柄,则返回EOF表示错误 */
    return EOF;
  }
  /* 根据当前设置的USART句柄,选择对应的USART外设发送字符 */
  if(Current_USART_Handle == &huart1){		
	while ((USART1->ISR & 0X40) == 0); 		/* 等待USART1发送完成,然后发送字符 */
	USART1->TDR = (uint8_t)ch; 				/* 将要发送的字符 ch 写入到DR寄存器 */
  }
  else if(Current_USART_Handle == &huart2){
	while ((USART2->ISR & 0X40) == 0); 		/* 等待USART2发送完成,然后发送字符 */
	USART2->TDR = (uint8_t)ch; 				/* 将要发送的字符 ch 写入到DR寄存器 */
  }
  else if(Current_USART_Handle == &huart3){
	while ((USART3->ISR & 0X40) == 0); 		/* 等待USART3发送完成,然后发送字符 */
	USART3->TDR = (uint8_t)ch; 				/* 将要发送的字符 ch 写入到DR寄存器 */
  }
  return ch;								/* 返回发送的字符 */
}

此文件中全部的重定向代码:

#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;
}

/************************** 以下是多串口printf重定向函数 **************************/
/* FILE 在 stdio.h里面定义 */
FILE __stdout;

UART_HandleTypeDef* Current_USART_Handle = NULL;
Current_USART_Indx Current_USART_Printf_Indx = USART_NONE;

/* 
 * 简介:重定义fputc函数,用于将字符输出到当前设置的USART
 * 参数:
 * ch - 要发送的字符
 * f  - 文件指针(在此实现中未使用)
 * 返回值:发送的字符(或EOF如果出错)
 */
int fputc(int ch, FILE *f)
{
  if(Current_USART_Handle == NULL){			/* 如果当前没有设置USART句柄,则返回EOF表示错误 */
    return EOF;
  }
  /* 根据当前设置的USART句柄,选择对应的USART外设发送字符 */
  if(Current_USART_Handle == &huart1){		
	while ((USART1->ISR & 0X40) == 0); 		/* 等待USART1发送完成,然后发送字符 */
	USART1->TDR = (uint8_t)ch; 				/* 将要发送的字符 ch 写入到DR寄存器 */
  }
  else if(Current_USART_Handle == &huart2){
	while ((USART2->ISR & 0X40) == 0); 		/* 等待USART2发送完成,然后发送字符 */
	USART2->TDR = (uint8_t)ch; 				/* 将要发送的字符 ch 写入到DR寄存器 */
  }
  else if(Current_USART_Handle == &huart3){
	while ((USART3->ISR & 0X40) == 0); 		/* 等待USART3发送完成,然后发送字符 */
	USART3->TDR = (uint8_t)ch; 				/* 将要发送的字符 ch 写入到DR寄存器 */
  }
  return ch;								/* 返回发送的字符 */
}
/* 
 * 简介:设置当前使用的USART
 * 参数:indx - 要设置的USART索引
 * 这个参数可以是:USARTx_IDX,其中x可以从1~3
 * 使用举例:(必须要将其放在printf函数前面,指定其中一个串口)
 * 		Set_Current_USART(USART1_IDX);
 * 		printf("我是串口1\r\n");
 */
void Set_Current_USART(Current_USART_Indx indx)
{
	switch(indx)
  {
    case USART1_IDX:
    Current_USART_Handle = &huart1;
    Current_USART_Printf_Indx = USART1_IDX;
    break;
    case USART2_IDX:
    Current_USART_Handle = &huart2;
    Current_USART_Printf_Indx = USART2_IDX;
    break;
    case USART3_IDX:
    Current_USART_Handle = &huart3;
    Current_USART_Printf_Indx = USART3_IDX;
    break;
    default:
    Current_USART_Handle = NULL;
    Current_USART_Printf_Indx = USART_NONE;
    break;
  }
}
/************************** 以下是单串口printf重定向函数 **************************/
/**
  * 简介:单个串口printf重定向fputc函数
  * 重定义fputc函数, printf函数最终会通过调用fputc输出字符串到串口 
  *
  *	int fputc(int ch, FILE *f)
  *	{
  *	  while ((USART2->ISR & 0X40) == 0);  等待上一个字符发送完成 
  *	  USART2->TDR = (uint8_t)ch;  将要发送的字符 ch 写入到DR寄存器 
  *	  return ch;
  *	}
  */
#endif

main.c中引用

在使用printf函数之前,需要先调用Set_Current_USART函数设置当前使用的USART。然后,就可以像平常一样使用printf函数了,输出的字符串将会通过指定的USART发送到串口。


Set_Current_USART(USART1_IDX); /* 想要指定不同串口必须在printf前加上此函数 */
printf("我是串口1\r\n");
Set_Current_USART(USART2_IDX); /* 想要指定不同串口必须在printf前加上此函数 */
printf("我是串口2\r\n");
Set_Current_USART(USART3_IDX); /* 想要指定不同串口必须在printf前加上此函数 */
printf("我是串口3\r\n");

通过这种方式,我们可以非常方便地在STM32项目中实现多路USART串口printf重定向,从而大大提高调试的效率和便利性。

<think>嗯,用户问的是“什么是printf重定向,为什么需要printf重定向”。首先,我需要回想一下之前回答过的关于Keil中使用microlib的问题,因为可能有关联。用户之前的问题是关于为什么Keil里要用microlib来支持printf,现在这个问题是关于printf重定向的,应该是在继续深入探讨同一主题。 首先,我需要解释什么是printf重定向。根据之前的知识,printf在标准库中默认输出到标准输出,比如屏幕。但在嵌入式系统中,没有屏幕,所以需要将输出重定向到其他设备,比如串口。这时候就需要重写底层的输出函数,比如fputc或者使用特定的库函数,这就是重定向。 然后,为什么需要重定向呢?用户可能是在开发嵌入式系统时遇到了printf无法正常工作的问题,想知道原因和解决方法。需要解释在嵌入式环境下,默认的输出方式不可用,必须将输出指向可用的外设,比如通过串口发送调试信息。此外,可能还涉及到资源限制,使用microlib等库来减少代码体积,这也和重定向有关。 接下来,我需要考虑用户可能的背景。他们可能在使用Keil或者类似的IDE进行嵌入式开发,比如STM32或者Cortex-M系列。用户可能已经尝试使用printf但发现没有输出,或者遇到了链接错误,因此需要了解如何正确配置。 需要分步骤解释:先定义printf重定向,然后说明为什么需要,可能包括半主机的问题,标准库的依赖,以及嵌入式系统的限制。同时,要提到如何实现重定向,比如重写fputc函数或者使用其他方法,并可能需要启用microlib。 还要注意之前回答的结构,用户可能希望类似的清晰结构,比如分点解释,可能用加粗标题,代码示例等。此外,用户可能需要对比不同情况,比如使用microlib和不使用的区别,但这次的问题主要围绕重定向本身。 需要确保语言简洁,避免技术术语过多,但又要准确。比如解释半主机机制时,可能需要简单说明这是调试机制,依赖调试器,而实际硬件中没有,所以需要重定向。 最后,总结重定向的必要性,可能的应用场景,以及实现方法。确保用户能够理解如何在自己的项目中应用这些步骤,解决实际问题。</think>--- ### **1. 什么是`printf`重定向?** **`printf`重定向**是指将标准C库中的`printf`函数的输出目标**从默认终端(如PC屏幕)更改为其他设备**(如串口、LCD、文件等)的过程。在嵌入式系统中,由于没有屏幕或操作系统支持,开发者需要手动将`printf`的输出重定向到可用外设(如通过UART发送数据到串口调试助手)。 --- ### **2. 为什么需要`printf`重定向?** #### **2.1 嵌入式系统的硬件限制** - **无默认输出设备**:微控制器(如STM32、ARM Cortex-M)通常没有显示屏或控制台,需通过外设(如串口、USB)输出调试信息。 - **依赖底层硬件**:`printf`本身不直接操作硬件,需通过用户代码实现字符传输的底层驱动(如发送字节到串口)。 #### **2.2 标准库的默认行为问题** - **半主机(Semihosting)依赖**: 标准C库的`printf`默认依赖半主机机制,需通过调试器与PC通信输出数据。但此机制: - 仅适用于仿真环境(如Keil的Debug模式),**无法在实际硬件中运行**。 - 会导致程序卡死或崩溃(若未连接调试器)。 - **资源占用大**:标准库的完整实现包含文件操作、宽字符等冗余功能,**占用过多Flash和RAM**,不适合资源有限的嵌入式设备。 #### **2.3 调试与开发需求** - **调试信息输出**:嵌入式开发中,`printf`是调试代码、打印变量值或状态的关键工具。 - **灵活控制输出目标**:可根据需求将输出重定向到不同设备,例如: - 开发阶段:通过串口输出到PC。 - 实际产品:输出到LCD或日志文件。 --- ### **3. 如何实现`printf`重定向?** #### **3.1 核心步骤** 1. **重写底层字符发送函数**: 通常需实现`fputc`(用于字符输出)或`_write`(用于字符串输出),将数据发送到目标设备(如串口): ```c #include <stdio.h> // 重写fputc,将字符发送到串口 int fputc(int ch, FILE *f) { HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 100); // 使用HAL库发送 return ch; } // 若使用标准库且无HAL,可直接操作寄存器: void UART_SendChar(char c) { while (!(USART1->SR & USART_SR_TXE)); // 等待发送缓冲区空 USART1->DR = c; // 写入数据寄存器 } ``` 2. **启用轻量库(如Microlib)**: 在Keil中勾选**Use MicroLIB**(位于`Options for Target -> Target -> Code Generation`),避免半主机依赖并减少代码体积。 3. **关闭半主机(可选)**: 若不使用Microlib,需添加以下代码禁用半主机: ```c #pragma import(__use_no_semihosting) // 声明不依赖半主机 void _sys_exit(int x) { while(1); } // 避免链接半主机相关函数 ``` --- ### **4. 重定向的常见场景** #### **4.1 串口输出** - **应用最广泛**:通过UART将调试信息发送到PC端工具(如SecureCRT、PuTTY)。 - **示例**: ```c // 重定向USART1 int fputc(int ch, FILE *f) { HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 10); return ch; } ``` #### **4.2 LCD或OLED显示** - **显示实时数据**:将`printf`输出到屏幕,用于显示传感器数据或系统状态。 - **示例**: ```c int fputc(int ch, FILE *f) { LCD_WriteChar(ch); // 自定义LCD字符写入函数 return ch; } ``` #### **4.3 文件或日志存储** - **记录运行日志**:将输出保存到SD卡或Flash中。 - **示例**: ```c int fputc(int ch, FILE *f) { write_to_file(&ch, 1); // 自定义文件写入函数 return ch; } ``` --- ### **5. 总结:为什么需要`printf`重定向?** | **场景** | **需求** | |-------------------------|--------------------------------------------------------------------------| | **硬件限制** | 嵌入式设备无默认输出终端,需绑定到物理外设(如串口)。 | | **规避半主机问题** | 避免因依赖调试器导致程序崩溃。 | | **资源优化** | 使用轻量库(如Microlib)减少代码体积,适配有限的Flash/RAM。 | | **调试与维护** | 通过输出关键信息快速定位代码逻辑或硬件问题。 | 通过重定向`printf`,开发者可以在资源受限的嵌入式系统中,灵活、高效地利用标准库功能进行调试和数据处理。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值