赞
踩
printf重定向是指将标准输出函数printf()的输出流重定向到用户定义的其他输出设备或存储介质,而不是默认的标准输出设备(通常是终端或控制台)。这样做可以将printf()函数输出的内容发送到不同的设备,比如串口、文件、LCD屏幕等,从而实现更灵活的输出方式。
通过重写putchar函数可以简单地实现printf重定向 。 下面是一个示例:
/**
* @brief printf 重定向
* @param c
*/
void putchar(char c){
SBUF = c;
while(!TI);
TI = 0;
}
调用方法:
main.c
#include "SH79F9476.h" #include "clk_utils.h" #include "cpu.h" #include "euart_utils.h" #include "common_utils.h" #include <stdio.h> void main() { char index=0x31; // 选择高速时钟 highFrequenceClk(); // 初始化串口 Uart0_Init(); while (1) { printf("char(%bd) = %c \n",index, index); index++; if(index>0x7d)index=0x31; // 暂停 delay_ms(500); } }
示例会通过uart0串口输出ascii码。
标准的C语言格式化字符格式如下:
符号 | 作用 |
---|---|
%d | 十进制有符号整数 |
%u | 十进制无符号整数 |
%f | 浮点数 |
%s | 字符串 |
%c | 单个字符 |
%p | 指针的值 |
%e | 指数形式的浮点数 |
%x, %X | 无符号以十六进制表示的整数 |
%0 | 无符号以八进制表示的整数 |
%g | 自动选择合适的表示法 |
数值的输出是%d,如:
printf("My age is %d", age);
但是在C51中,对于单字节变量的格式化输出,需要在%d中加入字母,规则如下:
上例中的程序:
printf("char(%bd) = %c \n",index, index);
%bd
就是输出 8位数据。
上面实现的printf函数,只适合一些比较小的应用场合,比如控制几个灯、开关之类,其原因是:
虽然可以方便地将日志重定向到串口,但是putchar中的WHILE(!TI);
会阻塞程序执行。在一些商用场合,MCU的资源、时序都不允许让MCU停止下来等待日志输出。
由于putchar是个单字符发送,重写putchar已经没办法实现中断发送的效果。
为了重定向日志,一种可能的方式是对printf函数进行重写。但printf 使用的是可变长度参数函数,很可惜C51不支持可变数量函数参数的功能,C51的宏也不支持传递可变数量参数,使得重写printf难以实现。如果非要实现,可能要换其它编译器把函数编译成库供C51来调用,我个人觉得过于复杂,所以不再走这条路线。
最终决定的方式是自定义日志函数,使用两层宏参数来实现可长度参数的功能。
#ifndef __LOG_UTILS_H__ #define __LOG_UTILS_H__ #include "euart_utils.h" #include <stdio.h> // 发送缓冲区 extern U8 gUart0DataTxD[UART0_DATA_BUF_SIZE]; #define TAG gUart0DataTxD+log_len /** * INFO 级别日志 */ #define LOGI(args) \ do { \ U8 log_len; \ log_len = sprintf(gUart0DataTxD, "[I] %s:%bd: ", __FILE__, __LINE__); \ log_len += sprintf args; \ log_len += sprintf(gUart0DataTxD + log_len, "\n"); \ Uart0_Transmit(log_len); \ } while (0) /** * ERROR 级别日志 */ #define LOGE(args) \ do { \ U8 log_len; \ log_len = sprintf(gUart0DataTxD, "[E] %s:%bd: ", __FILE__, __LINE__); \ log_len += sprintf args; \ log_len += sprintf(gUart0DataTxD + log_len, "\n"); \ Uart0_Transmit(log_len); \ } while (0) #endif
#include "SH79F9476.h" #include "clk_utils.h" #include "cpu.h" #include "common_utils.h" #include "isr_utils.h" #include "log_utils.h" #include <stdio.h> // 发送缓冲区 void main() { char index = 0x31; // 选择高速时钟 highFrequenceClk(); enableAllIsr(); // 初始化串口 Uart0_Init(); while (1) { LOGI((TAG, "char(%bd) = %c",index, index)); index++; if (index > 0x7d)index = 0x31; // 暂停 delay_ms(10); } }
下面实现了两个级别的日志等级 :
#ifndef __LOG_UTILS_H__ #define __LOG_UTILS_H__ #include "euart_utils.h" #include <stdio.h> // 发送缓冲区 extern U8 gUart0DataTxD[UART0_DATA_BUF_SIZE]; #define TAG gUart0DataTxD+log_len // 日志等级 #define LOG_LEVEL_NONE 0 #define LOG_LEVEL_ERROR 1 #define LOG_LEVEL_INFO 2 // 判断预定义的宏 LOG_LEVEL #if LOG_LEVEL >= LOG_LEVEL_INFO /** * INFO 级别日志 */ #define LOGI(args) \ do { \ U8 log_len; \ log_len = sprintf(gUart0DataTxD, "[I] %s:%bd: ", __FILE__, __LINE__); \ log_len += sprintf args; \ log_len += sprintf(gUart0DataTxD + log_len, "\n"); \ Uart0_Transmit(log_len); \ } while (0) #else #define LOGI(args) (void)0 #endif #if LOG_LEVEL >= LOG_LEVEL_ERROR /** * ERROR 级别日志 */ #define LOGE(args) \ do { \ U8 log_len; \ log_len = sprintf(gUart0DataTxD, "[E] %s:%bd: ", __FILE__, __LINE__); \ log_len += sprintf args; \ log_len += sprintf(gUart0DataTxD + log_len, "\n"); \ Uart0_Transmit(log_len); \ } while (0) #else #define LOGE(args) (void)0 #endif #endif
当在 Options 里设置 LOG_LEVEL=LOG_LEVEL_ERROR
时,INFO级别的日志将不会再输出 :
#include "SH79F9476.h" #include "clk_utils.h" #include "cpu.h" #include "common_utils.h" #include "isr_utils.h" #include "log_utils.h" #include <stdio.h> // 发送缓冲区 void main() { char index = 0x31; // 选择高速时钟 highFrequenceClk(); enableAllIsr(); // 初始化串口 Uart0_Init(); while (1) { LOGI((TAG, "info(%bd) = %c",index, index)); delay_ms(100); LOGE((TAG, "err(%bd) = %c",index, index)); index++; if (index > 0x7d)index = 0x31; // 暂停 delay_ms(100); } }
由于串口输出一般较慢,在循环中快速输出日志时,会出现这样情况 : 前面的日志尚未通过串口输出结束、后面的日志又开始调用串口发送函数。
为了最大化利用起串口资源,可将缓冲区做成环形队列,后面要输出的内容直接放入缓冲区,这样可以动态调整缓冲区的大小,以适应快速调用的情况。
但仍需注意的是,单片机的资源有限,缓冲区大小不是可以随心所欲扩大的,另一方面串口速率也限制了发送速度的上限,调用日志的程序还是需要量力而行,避免过于快速输出。
另外,单片机运行在循环执行的程序中,经常有连续输出同样日志的情况,在调用时可以加些限制,防止重复输出相同数据。
下面是输出异常的情况示例:
下面是改写的程序,使用了环形队列,另外提高了波特率:
#include "intrins.h" #include "euart_utils.h" #include "api_ext.h" #include "SH79F9476.h" #include "cpu.h" #include <stdio.h> #include "string.h" // 发送缓冲区 static U8 gUart0DataTxD[UART0_DATA_BUF_SIZE]; // 未发送数据长度 static U8 gUart0DataTxDLen; // 内部变量,发送指针 static volatile U8 *ptr_tx0_head; /** * @brief 初始化串口 */ void Uart0_Init() { //=====TX 建议配置为输出H==== P3CR = 0x08; P3 = 0x08; // 配置Uart工作在模式1 select_bank1(); // 0110 0111 Tx:P3.3 Rx:P3.4 UART0CR = 0x67; select_bank0(); SCON = 0x50; /*配置波特率参数,波特率9600*/ /* 计算公式:(int)X=FSY/(16*波特率) ; SBRT=32768-X ; SFINE=(FSY/波特率)-16*X FSY=8M*/ // 波特率发生器高位 SBRTH = 0xFF; // 波特率发生器低位 SBRTL = 0xF3; // 波特率发生器微调 SFINE = 0x0; // 使能串口中断 IEN0 |= 0x10; ptr_tx0_head = &gUart0DataTxD[0]; } /** * @brief 发送缓冲区数据 */ void Uart0_Transmit(U8 len) { gUart0DataTxDLen = len; SBUF = *ptr_tx0_head; if (gUart0DataTxDLen > 0) gUart0DataTxDLen--; if (ptr_tx0_head >= &gUart0DataTxD[UART0_DATA_BUF_SIZE]) { ptr_tx0_head = &gUart0DataTxD[0]; } else { ptr_tx0_head++; } } /** * @brief 向gUart0DataTxD尾部添加数组,注意要考虑到如果添加的过长,就回到队列头部添加剩余部分 */ void Uart0_Append_Bytes(const char *bytes, U8 len) { U8 i ,startIndex; // 如果添加的长度超过缓冲区长度,就只添加缓冲区长度的数据 if (len > UART0_DATA_BUF_SIZE) { len = UART0_DATA_BUF_SIZE; } startIndex = ptr_tx0_head - &gUart0DataTxD[0] + gUart0DataTxDLen; for (i = 0; i < len; i++) { gUart0DataTxD[startIndex] = bytes[i]; startIndex++; // 如果添加的数据长度超过了缓冲区长度,就回到队列头部添加剩余部分 if (startIndex == UART0_DATA_BUF_SIZE) { startIndex = 0; } } Uart0_Transmit(gUart0DataTxDLen+len); } /** * @brief UART0中断 **/ void INT_EUART0(void) interrupt 4{ if(TI){ TI = 0; if(gUart0DataTxDLen >0){ SBUF = *ptr_tx0_head; gUart0DataTxDLen --; // 这里产生了一种情况,如果发送的数据长度超过了缓冲区长度,就会导致ptr_tx0指针超出范围 if(ptr_tx0_head >= &gUart0DataTxD[UART0_DATA_BUF_SIZE]){ ptr_tx0_head = &gUart0DataTxD[0]; }else{ ptr_tx0_head ++; } } } }
#include "SH79F9476.h" #include "clk_utils.h" #include "common_utils.h" #include "isr_utils.h" #include "log_utils.h" // 发送缓冲区 void main() { char index = 0x31; // 选择高速时钟 highFrequenceClk(); enableAllIsr(); // 初始化串口 Uart0_Init(); while (1) { LOGI((TAG, "info(%bd) = %c",index, index)); delay_ms(100); index++; if (index > 0x7d)index = 0x31; // 暂停 delay_ms(20); } }
在主循环20ms暂停情况下可以稳定输出日志:
本文代码开源地址:
https://gitee.com/xundh/learn-sinowealth-51
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。