中颖51芯片学习7. printf重定向到串口与自定义日志输出函数

本文主要是介绍中颖51芯片学习7. printf重定向到串口与自定义日志输出函数,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

中颖51芯片学习7. printf重定向到串口与自定义日志输出函数

  • 一、 printf 重定向
    • 1. 概念
    • 2. 实现方式
    • 3. C51 中printf数值格式化
  • 二、日志函数
    • 1. 实现方案分析
    • 2. 代码
      • (1)log_utils.h
      • (2)main.c
    • 3. 通过预定义宏实现日志分级输出
      • (1)log_utils.h
      • (2)main.c
      • (3)运行效果
  • 三、运行速度问题
    • 1. euart_utils.c
    • 2. main.c

在这里插入图片描述

一、 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十进制有符号整数
%u十进制无符号整数
%f浮点数
%s字符串
%c单个字符
%p指针的值
%e指数形式的浮点数
%x, %X无符号以十六进制表示的整数
%0无符号以八进制表示的整数
%g自动选择合适的表示法

数值的输出是%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工作在模式1select_bank1();// 0110 0111 Tx:P3.3   Rx:P3.4UART0CR = 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 ++;}}}
}

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

这篇关于中颖51芯片学习7. printf重定向到串口与自定义日志输出函数的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/922336

相关文章

C++中实现调试日志输出

《C++中实现调试日志输出》在C++编程中,调试日志对于定位问题和优化代码至关重要,本文将介绍几种常用的调试日志输出方法,并教你如何在日志中添加时间戳,希望对大家有所帮助... 目录1. 使用 #ifdef _DEBUG 宏2. 加入时间戳:精确到毫秒3.Windows 和 MFC 中的调试日志方法MFC

SpringBoot如何使用TraceId日志链路追踪

《SpringBoot如何使用TraceId日志链路追踪》文章介绍了如何使用TraceId进行日志链路追踪,通过在日志中添加TraceId关键字,可以将同一次业务调用链上的日志串起来,本文通过实例代码... 目录项目场景:实现步骤1、pom.XML 依赖2、整合logback,打印日志,logback-sp

Python使用Colorama库美化终端输出的操作示例

《Python使用Colorama库美化终端输出的操作示例》在开发命令行工具或调试程序时,我们可能会希望通过颜色来区分重要信息,比如警告、错误、提示等,而Colorama是一个简单易用的Python库... 目录python Colorama 库详解:终端输出美化的神器1. Colorama 是什么?2.

Oracle的to_date()函数详解

《Oracle的to_date()函数详解》Oracle的to_date()函数用于日期格式转换,需要注意Oracle中不区分大小写的MM和mm格式代码,应使用mi代替分钟,此外,Oracle还支持毫... 目录oracle的to_date()函数一.在使用Oracle的to_date函数来做日期转换二.日

SpringBoot 自定义消息转换器使用详解

《SpringBoot自定义消息转换器使用详解》本文详细介绍了SpringBoot消息转换器的知识,并通过案例操作演示了如何进行自定义消息转换器的定制开发和使用,感兴趣的朋友一起看看吧... 目录一、前言二、SpringBoot 内容协商介绍2.1 什么是内容协商2.2 内容协商机制深入理解2.2.1 内容

C++11的函数包装器std::function使用示例

《C++11的函数包装器std::function使用示例》C++11引入的std::function是最常用的函数包装器,它可以存储任何可调用对象并提供统一的调用接口,以下是关于函数包装器的详细讲解... 目录一、std::function 的基本用法1. 基本语法二、如何使用 std::function

HarmonyOS学习(七)——UI(五)常用布局总结

自适应布局 1.1、线性布局(LinearLayout) 通过线性容器Row和Column实现线性布局。Column容器内的子组件按照垂直方向排列,Row组件中的子组件按照水平方向排列。 属性说明space通过space参数设置主轴上子组件的间距,达到各子组件在排列上的等间距效果alignItems设置子组件在交叉轴上的对齐方式,且在各类尺寸屏幕上表现一致,其中交叉轴为垂直时,取值为Vert

Ilya-AI分享的他在OpenAI学习到的15个提示工程技巧

Ilya(不是本人,claude AI)在社交媒体上分享了他在OpenAI学习到的15个Prompt撰写技巧。 以下是详细的内容: 提示精确化:在编写提示时,力求表达清晰准确。清楚地阐述任务需求和概念定义至关重要。例:不用"分析文本",而用"判断这段话的情感倾向:积极、消极还是中性"。 快速迭代:善于快速连续调整提示。熟练的提示工程师能够灵活地进行多轮优化。例:从"总结文章"到"用

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06

学习hash总结

2014/1/29/   最近刚开始学hash,名字很陌生,但是hash的思想却很熟悉,以前早就做过此类的题,但是不知道这就是hash思想而已,说白了hash就是一个映射,往往灵活利用数组的下标来实现算法,hash的作用:1、判重;2、统计次数;