打印
[其它应用]

printf重定向到串口与自定义日志输出函数

[复制链接]
1294|5
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
tpgf|  楼主 | 2024-5-7 09:36 | 只看该作者 |只看大图 回帖奖励 |倒序浏览 |阅读模式
一、 printf 重定向
1. 概念
printf重定向是指将标准输出函数printf()的输出流重定向到用户定义的其他输出设备或存储介质,而不是默认的标准输出设备(通常是终端或控制台)。这样做可以将printf()函数输出的内容发送到不同的设备,比如串口、文件、LCD屏幕等,从而实现更灵活的输出方式。

2. 实现方式
通过重写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码。



3. C51 中printf数值格式化
标准的C语言格式化字符格式如下:



数值的输出是%d,如:

printf("My age is %d", age);


但是在C51中,对于单字节变量的格式化输出,需要在%d中加入字母,规则如下:

8位数据格式加字母"b",如 %bd, %bu
16位数据格式加字母"h", 如 %hd
32位数据格式加字母"l",如%ld
上例中的程序:

printf("char(%bd) = %c \n",index, index);


%bd 就是输出 8位数据。

二、日志函数
1. 实现方案分析
上面实现的printf函数,只适合一些比较小的应用场合,比如控制几个灯、开关之类,其原因是:

虽然可以方便地将日志重定向到串口,但是putchar中的WHILE(!TI);会阻塞程序执行。在一些商用场合,MCU的资源、时序都不允许让MCU停止下来等待日志输出。

由于putchar是个单字符发送,重写putchar已经没办法实现中断发送的效果。

为了重定向日志,一种可能的方式是对printf函数进行重写。但printf 使用的是可变长度参数函数,很可惜C51不支持可变数量函数参数的功能,C51的宏也不支持传递可变数量参数,使得重写printf难以实现。如果非要实现,可能要换其它编译器把函数编译成库供C51来调用,我个人觉得过于复杂,所以不再走这条路线。

最终决定的方式是自定义日志函数,使用两层宏参数来实现可长度参数的功能。

2. 代码
(1)log_utils.h
#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




(2)main.c
#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);
    }
}




3. 通过预定义宏实现日志分级输出
下面实现了两个级别的日志等级 :

(1)log_utils.h
#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级别的日志将不会再输出 :



(2)main.c
#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);
    }
}




(3)运行效果



三、运行速度问题
由于串口输出一般较慢,在循环中快速输出日志时,会出现这样情况 : 前面的日志尚未通过串口输出结束、后面的日志又开始调用串口发送函数。

为了最大化利用起串口资源,可将缓冲区做成环形队列,后面要输出的内容直接放入缓冲区,这样可以动态调整缓冲区的大小,以适应快速调用的情况。

但仍需注意的是,单片机的资源有限,缓冲区大小不是可以随心所欲扩大的,另一方面串口速率也限制了发送速度的上限,调用日志的程序还是需要量力而行,避免过于快速输出。

另外,单片机运行在循环执行的程序中,经常有连续输出同样日志的情况,在调用时可以加些限制,防止重复输出相同数据。
下面是输出异常的情况示例:



下面是改写的程序,使用了环形队列,另外提高了波特率:

1. euart_utils.c
#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;
        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 ++;
            }
        }
    }
}



2. main.c
#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
————————————————

                            版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/xundh/article/details/137997471

使用特权

评论回复
沙发
onlycook| | 2024-5-11 10:19 | 只看该作者
如何支持类似printf的格式化字符串,以便在日志中包含变量信息?

使用特权

评论回复
板凳
内政奇才| | 2024-5-11 13:00 | 只看该作者
如果同时写入多个存储介质,如何确保日志的一致性?

使用特权

评论回复
地板
内政奇才| | 2024-5-11 22:00 | 只看该作者
如果在写入串口时发生错误,应该如何处理?

使用特权

评论回复
5
亚瑟| | 2024-5-24 22:12 | 只看该作者
重定向函数一直不知道咋写

使用特权

评论回复
6
yangjiaxu| | 2024-5-27 22:22 | 只看该作者
这个重定向最好是自己写个,要是用keil的microusb的话,忘记勾选就死机

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

1474

主题

14346

帖子

8

粉丝