简介

实现printf重定向有多种方式,下面一一介绍。

linux环境下

虽然linux系统的默认标准输出设备是显示器,但是我们可以把printf打印输出的内容重定向到其他设备或文件。方法如下:

方法1:

打开一个普通文件,把它的文件描述符指定为标准输出的文件描述符,这样printf打印输出的数据会重定向到这个普通文件。

示例如下:

//实现printf打印输出重定向功能示例1

#include <stdio.h>
#include <unistd.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(void)
{
	
	printf("hello,zzc\n");

	//保存标准输出的文件描述符	
	int stdout_fd = dup(STDOUT_FILENO);

	//打开一个文件,获取文件描述符
	int fd = open("./2.c", O_RDWR|O_APPEND, 0666);
	if(fd < 0)
	{
		printf("open a file fail\n");
	}

	//指定fd为标准输出的文件描述符
	dup2(fd, STDOUT_FILENO);
	
	printf("standard output file descriptor has changed");
	//刷新标准输出的IO缓冲区
	fflush(stdout);
	
#if 1
	//恢复为默认的标准输出,有两种方式
	//方式一,把之前保存的文件描述符重新指定为标准输出的文件描述符
	dup2(stdout_fd, STDOUT_FILENO);
#else
	//方式二,打开当前的控制终端设备文件(文件路径通过tty命令获取),获取文件描述符
	int tty_fd = open("/dev/pts/0", O_RDWR, 0666);
	dup2(tty_fd, STDOUT_FILENO);
#endif
	printf("standard output file descriptor has restored\n");

	return 0;
}

方法2:

使用freopen函数把文件关联到标准输出,这样printf打印输出的数据会重定向到该文件。

示例如下:

//实现printf打印输出重定向功能示例2

#include <stdio.h>
#include <unistd.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(void)
{
	FILE *p = NULL;

	//freopen()函数的主要用途是更改与标准文本流(stderr、stdin或stdout)相关联的文件。
	p = freopen("./2.c", "a", stdout);
	if(NULL == p)
	{
		perror("freopen ./2.c fail");
		return -1;
	}

	printf("standard output has changed\n");
	
	//恢复为默认的标准输出
	p = freopen("/dev/pts/0", "r+", stdout);
	if(NULL == p)
	{
		perror("freopen fail");
		return -1;
	}
	
	printf("standard output has restored\n");

	return 0;
}

另外,如果想刷新标准IO缓冲区,可以在printf 之后调用fflush。

IO重定向

使用重定向符号进行IO重定向。
常见的重定向符号和功能如下图:

输出重定向:

1234


输入重定向:

1


绑定重定向:

1

实例:

显示当前目录文件test test2(test2实际不存在)

2


正确输出与错误输出都显示在屏幕了,现在需要把正确输出写入note.txt

(1> 可以省略,不写,默认输出至标准输出)

1234


把错误输出,不输出到屏幕,输出到err.txt

4


继续追加把输出写入note.txt err.txt, “>>” 是追加操作符

12


将错误输出信息关闭

34


注:

1、&[n] 代表是已经存在的文件描述符,&1 代表输出 &2代表错误输出 &-代表关闭与它绑定的描述符

2、/dev/null 这个设备,是linux 中黑洞设备关闭所有输出:

关闭1,2文件描述符

3


将1,2输出转发给/dev/null设备

1


将错误输出2绑定给正确输出1,然后将正确输出发送给/dev/null设备(&代表标准输出,错误输出;将标准输出与错误输出 重定向到/dev/null)

5


使用标准输入,在a.txt文件中写入"hello world"

注:在shell编程中,“EOF"通常与”<<“结合使用,”<<EOF"表示后续的输入作为子命令或子shell的输入,直到遇到"EOF"

1

MCU环境下(以stm32为例)

在不同的开发环境下,我们有多种手段可以对printf打印输出的数据进行重定向,目的是方便我们调试程序。

首先我们要确定开发环境是什么样的,是软件仿真,是硬件仿真,还是产品功能运行。不同的开发环境,有不同的重定向printf内容的方法。

在实时性上,RTT > SWO >串口 >半主机模式;本文不会讲述RTT、半主机模式,有兴趣的朋友请自行查阅资料。

1、软件仿真

在软件仿真的环境下,以keil mdk工程为例,我们使用微库,然后把printf函数重定向到usart串口。

printf重定向代码如下所示:

#include <stdio.h>
int fputc(int ch, FILE * f)
{
    //等待串口数据发送完毕
    while((USART1->SR & USART_FLAG_TC) == 0);
    
    //发送下一个字符
    USART1->DR = (uint8_t)ch;

    return ch;
}

运行效果如下图所示:

1234567

2、硬件仿真

目标开发板通过仿真器(JLINK、ULINK、STLINK等)连接到PC调试主机,在这种环境下,我们可以把printf输出的数据重定向到串口;也可以重定向到ITM端口,通过SWO线把数据发送给PC。

串口重定向的方法和上述软件仿真一样,这里不再复述,下面着重介绍ITM方式。

重定向到ITM:
在Cortex-M3\M4\M7系列MCU中,内核的调试组件有一个仪器跟踪宏单元(ITM) 。请注意如果你的芯片是 Cortex-M0 或者其他ARM内核,不支持ITM。

下面介绍如何把printf打印输出的数据重定向到ITM端口。

硬件连接:

我们都知道SWD接口正常使用是四根线。而使用ITM机制需要多用SWD的一根线:SWO。
先找到link接口SWO引脚的位置,再找到目标开发板上SWO引脚的位置,然后用杜邦线把两个引脚连接起来。

软件配置

第一步: 添加重定向文件

新建一个文件(文件名自定义),添加到mdk工程,文件的内容如下:

#include <stdio.h>  
  
#define ITM_Port8(n)    (*((volatile unsigned char *)(0xE0000000+4*n)))  
#define ITM_Port16(n)   (*((volatile unsigned short*)(0xE0000000+4*n)))  
#define ITM_Port32(n)   (*((volatile unsigned long *)(0xE0000000+4*n)))  
#define DEMCR           (*((volatile unsigned long *)(0xE000EDFC)))  
#define TRCENA          0x01000000  
  
struct __FILE { int handle; /* Add whatever you need here */ };  
    FILE __stdout;  
    FILE __stdin;  
      
int fputc(int ch, FILE *f)   
{  
    if (DEMCR & TRCENA)   
    {  
        while (ITM_Port32(0) == 0);  
        ITM_Port8(0) = ch;  
    }  
    return(ch);  
}

说明:

1、这个文件用于重定义fputc函数;因为printf函数的底层实现就是fputc,所以需要重定义这个函数,在这个函数里面把printf打印输出的数据重定向到ITM端口
2、上述文件中默认使用ITM的port0,当然可以使用其他的端口。

关于ITM的配置,可以参考以下描述:

123


更多详细描述请参阅stm32有关的参考手册。

第二步:新建一个配置文件(STM32DBG.ini),用于stm32 debugger初始化,把这个文件放在mdk工程下。文件内容如下:

/******************************************************************************/  
/* STM32DBG.INI: STM32 Debugger Initialization File                           */  
/******************************************************************************/  
// <<< Use Configuration Wizard in Context Menu >>>                           //   
/******************************************************************************/  
/* This file is part of the uVision/ARM development tools.                    */  
/* Copyright (c) 2005-2007 Keil Software. All rights reserved.                */  
/* This software may only be used under the terms of a valid, current,        */  
/* end user licence from KEIL for a compatible version of KEIL software       */  
/* development tools. Nothing else gives you the right to use this software.  */  
/******************************************************************************/  
  
  
FUNC void DebugSetup (void) {  
// <h> Debug MCU Configuration  
//   <o1.0>    DBG_SLEEP     <i> Debug Sleep Mode  
//   <o1.1>    DBG_STOP      <i> Debug Stop Mode  
//   <o1.2>    DBG_STANDBY   <i> Debug Standby Mode  
//   <o1.5>    TRACE_IOEN    <i> Trace I/O Enable   
//   <o1.6..7> TRACE_MODE    <i> Trace Mode  
//             <0=> Asynchronous  
//             <1=> Synchronous: TRACEDATA Size 1  
//             <2=> Synchronous: TRACEDATA Size 2  
//             <3=> Synchronous: TRACEDATA Size 4  
//   <o1.8>    DBG_IWDG_STOP <i> Independant Watchdog Stopped when Core is halted  
//   <o1.9>    DBG_WWDG_STOP <i> Window Watchdog Stopped when Core is halted  
//   <o1.10>   DBG_TIM1_STOP <i> Timer 1 Stopped when Core is halted  
//   <o1.11>   DBG_TIM2_STOP <i> Timer 2 Stopped when Core is halted  
//   <o1.12>   DBG_TIM3_STOP <i> Timer 3 Stopped when Core is halted  
//   <o1.13>   DBG_TIM4_STOP <i> Timer 4 Stopped when Core is halted  
//   <o1.14>   DBG_CAN_STOP  <i> CAN Stopped when Core is halted  
// </h>  
_WDWORD(0xE0042004, 0x00000027);  // DBGMCU_CR  
_WDWORD(0xE000ED08, 0x20000000);   // Setup Vector Table Offset Register  
}  
  
DebugSetup();                       // Debugger Setup

第三步:配置mdk工程,如下图:

配置初始化文件

1234


选择SW接口(我这里没有接link,所以有些信息没有显示)

12345

core clock需要设置为你的系统时钟频率,如果你的cpu主频是72MHz,那就设置为72MHz;
跟踪功能需要使能,另外ITM端口默认使用端口0,当然你也可以使用其他端口。

123

第四步: 烧录程序,启动调试

打开debug viewer,你会发现printf打印输出的数据会显示在这个窗口上。那是因为printf重定向到了ITM端口,然后再通过仿真器的SWO线把数据传回PC。

1234


打印出乱码是因为我打印了中文。

注意事项

1、如果使用微库,不需要关闭半主机模式,因为并不会进入半主机模式。
2、如果使用MDK提供的标准库(不勾选微库),就需要关闭半主机模式。方法就是上述重定向文件中添加下面这句话:

#pragma import(__use_no_semihosting_swi)

这句话意思是告知连接器不从C库链接使用半主机的函数。

如果你使用的是AC5编译器,是没有问题的;如果你使用的是AC6编译器,你可能会遇到问题:编译器会报错提示 #pragma import(__use_no_semihosting_swi) 这个命令AC6并不支持。

你可以添加下面这句话来解决这个问题:

__ASM (".global __use_no_semihosting");

3、开发板独立运行(不带仿真器)

不接仿真器,开发板独立运行,这种场景下可以使用串口重定向,这里不再复述。

拓展

一、输入输出重定向(ITM方式)

我们也可以重定向输入,本来是从标准输入设备输入,但是开发板没有这个东西,所以我们可以从PC的标准输入设备输入。

新建一个重定向文件,文件内容如下:(对标准输入和输出都做了重定向)

#pragma import(__use_no_semihosting_swi)  
  
struct __FILE { int handle; /* Add whatever you need here */ };  

FILE __stdout;  
FILE __stdin;  
      
int fputc(int ch, FILE *f)   
{  
    return ITM_SendChar(ch);  
}  
  
volatile int32_t ITM_RxBuffer;  
int fgetc(FILE *f)  
{  
  while (ITM_CheckChar() != 1) __NOP();  
  return (ITM_ReceiveChar());  
}  
  
int ferror(FILE *f)  
{  
    /* Your implementation of ferror */  
    return EOF;  
}  
  
void _ttywrch(int c)  
{  
    fputc(c, 0);  
}  
  
int __backspace()  
{  
    return 0;  
}  
void _sys_exit(int return_code)  
{  
label:  
    goto label;  /* endless loop */  
}

keil工程编译之后运行效果如下:(先从PC键盘输入一个整数,然后打印出这个整数)

123

需要说明以下几点:

1、ITM_SendChar、ITM_CheckChar、ITM_ReceiveChar这几个函数都是core_cm3.h/core_cm4.h/core_cm7.h文件中定义的

2、scanf依赖的函数共有两个,fgetc和__backspace都需要实现,如果缺少__backespace函数,则scanf无法从Debug Viewer Dialog 窗口获取输入

3、如果编译报错,缺少以上某些函数,那就需要添加这几个函数

二、在GCC中使用标准库重定向printf

在Gcc中重定向printf函数时要注意以下两点:

  • 与重定义fputs()函数一样,在使用gcc编译器的时候,需要重新定义_write函数;
  • gcc中没有MicroLib,只能使用标准库;

重定向代码如下所示:

#include <stdio.h>
int _write(int fd, char *ptr, int len)  
{ 
	int ret = len;
	while(len)
	{
		USART_SendData(USART1, *(char *)ptr);
		len--;
  	}
	return ret;
}

总结

本文汇总了printf函数在linux系统下和在mcu环境下重定向的几种方法,比如重定向到串口、重定向到ITM端口、重定向到文件等,其实还可以重定向到RTT。方法还是很多的,需要大家一起探索。

实际上,还是要根据自己的开发环境来选择合适的方法,技术在不断发展,以后肯定会出现更多更好的调试方法,方便我们调试、提高工作效率才是最终的目的。

参考资料