【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重定向,从而大大提高调试的效率和便利性。

博客导航

博客导航

posted @ 2024-11-27 12:39  膝盖中箭卫兵  阅读(509)  评论(0)    收藏  举报  来源
ORCID iD icon https://orcid.org/0000-0001-5102-772X