简介
实现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重定向。
常见的重定向符号和功能如下图:
输出重定向:
输入重定向:
绑定重定向:
实例:
显示当前目录文件test test2(test2实际不存在)
正确输出与错误输出都显示在屏幕了,现在需要把正确输出写入note.txt
(1> 可以省略,不写,默认输出至标准输出)
把错误输出,不输出到屏幕,输出到err.txt
继续追加把输出写入note.txt err.txt, “>>” 是追加操作符
将错误输出信息关闭
注:
1、&[n] 代表是已经存在的文件描述符,&1 代表输出 &2代表错误输出 &-代表关闭与它绑定的描述符
2、/dev/null 这个设备,是linux 中黑洞设备关闭所有输出:
关闭1,2文件描述符
将1,2输出转发给/dev/null设备
将错误输出2绑定给正确输出1,然后将正确输出发送给/dev/null设备(&代表标准输出,错误输出;将标准输出与错误输出 重定向到/dev/null)
使用标准输入,在a.txt文件中写入"hello world"
注:在shell编程中,“EOF"通常与”<<“结合使用,”<<EOF"表示后续的输入作为子命令或子shell的输入,直到遇到"EOF"
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;
}
运行效果如下图所示:
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的配置,可以参考以下描述:
更多详细描述请参阅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工程,如下图:
配置初始化文件
选择SW接口(我这里没有接link,所以有些信息没有显示)
core clock需要设置为你的系统时钟频率,如果你的cpu主频是72MHz,那就设置为72MHz;
跟踪功能需要使能,另外ITM端口默认使用端口0,当然你也可以使用其他端口。
第四步: 烧录程序,启动调试
打开debug viewer,你会发现printf打印输出的数据会显示在这个窗口上。那是因为printf重定向到了ITM端口,然后再通过仿真器的SWO线把数据传回PC。
打印出乱码是因为我打印了中文。
注意事项
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键盘输入一个整数,然后打印出这个整数)
需要说明以下几点:
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。方法还是很多的,需要大家一起探索。
实际上,还是要根据自己的开发环境来选择合适的方法,技术在不断发展,以后肯定会出现更多更好的调试方法,方便我们调试、提高工作效率才是最终的目的。
参考资料