中颖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工作在模式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 ++;
            }
        }
    }
}

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/560380.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

汇编语言88888

汇编语言安装指南 第一步&#xff1a;在github上下载汇编语言的安装包 网址&#xff1a;GitHub - HaiPenglai/bilibili_assembly: B站-汇编语言-pdf、代码、环境等资料B站-汇编语言-pdf、代码、环境等资料. Contribute to HaiPenglai/bilibili_assembly development by creat…

Flyweight 享元

意图 运用共享技术有效地支持大量细粒度的对象。 结构 其中 Flyweight描述一个接口&#xff0c;通过这个接口Flyweight可以接受并作用于外部状态。ConcreteFlyweight实现Flyweight接口&#xff0c;并作为内部状态&#xff08;如果有&#xff09;增加存储空间。ConcreteFlywe…

数字阅览室解决方案

一、方案概述 “数字阅览室”概念一经提出&#xff0c;就得到了广泛的关注&#xff0c;纷纷组织力量进行探讨、研究和开发&#xff0c;进行各种模型的试验。随着数字地球概念、技术、应用领域的发展&#xff0c;数字阅览室已成为数字地球家庭的成员&#xff0c;为信息高速公路…

vue3:树的默认勾选和全选、取消全选

实现的功能&#xff0c;上面有个选择框&#xff0c;当选中全部时&#xff0c;下方树被全选 代码&#xff1a; <template><div><el-select v-model"selectAll" style"margin-bottom: 10px;" change"handleSelectAllChange">&…

运行transformers报错check_min_version(“4.40.0.dev0“)

在huggingface &#xff08;transformers项目地址&#xff09;下载transformers的项目 并 python /transformers/examples/pytorch/language-modeling/run_clm.py 时报错 报错如下&#xff1a; 安装的 transformers 版本不对&#xff0c;这里安装了 4.39.3&#xff0c;实际想…

网站备案期间怎么关闭首页显示无法访问-文章及其它页面正常访问

自从做了开发者之后才发现每个人博主的需求都是不同的&#xff0c;的的确确颠覆了我的观点&#xff0c;无论是页面布局还是SEO相关的设置&#xff0c;可能是因为站点属性不同所以需求不同&#xff0c;慢慢的就会在主题加入一些自定接口来满足不同人的需求&#xff0c;有人需要P…

双链表的实现

我们知道链表其实有很多种&#xff0c;什么带头&#xff0c;什么双向啊&#xff0c;我们今天来介绍双向带头循环链表&#xff0c;了解了这个其他种类的链表就很简单了。冲冲冲&#xff01;&#xff01;&#xff01; 链表的简单分类 链表有很多种&#xff0c;什么带头循环链表&…

【c基础】文件操作

1.fopen和fclose函数 函数原型 FILE *fopen(const char *path, const char *mode); 参数解释&#xff1a; 返回值&#xff1a;fopen打开成功&#xff0c;则返回有效file的有效地址&#xff0c;失败返回NULL。path是文件路径&#xff0c;可以相对路径&#xff0c;可以绝对路径…

“手撕“三大特性之一的<继承>(上)

目录 一、为什么需要继承 二、什么是继承 三、继承怎么写 四、成员的访问 1.父类与子类的成员变量不同名 2.父类与子类的成员变量同名 3.父类与子类的成员方法不同名 4.父类与子类的成员方法同名 五、super关键字 一、为什么需要继承 先让我们看一段Java代码&#…

Unity地形关联出错的解决办法以及地形深度拷贝

问题 最近发现unity地形系统的一个bug&#xff0c;导入的场景地形数据关联错乱了&#xff0c;关联到别的场景的地形数据了&#xff0c;meta替换了也没用&#xff0c;不清楚它具体是怎么关联的。 看下面的案例&#xff1a; 可以看到正常这个场景的地形数据应该关联的是Scene_E…

力扣HOT100 - 142. 环形链表 II

解题思路&#xff1a; public class Solution {public ListNode detectCycle(ListNode head) {Set<ListNode> set new HashSet<>();while (head ! null) {if (!set.add(head)) {return head;}head head.next;}return null;} }

文心一言 VS 讯飞星火 VS chatgpt (241)-- 算法导论17.3 7题

七、为动态整数多重集 S (允许包含重复值)设计一种数据结构&#xff0c;支持如下两个操作&#xff1a;① INSERT(S,x) 将 x 插入 S 中&#xff1b;② DELETE-LARGER-HALF(S) 将最大的 ⌈|S|/2⌉ 个元素从S中删除。解释如何实现这种数据结构&#xff0c;使得任意 m 个 INSERT 和…

Linux的firewalld防火墙

介绍firewalld&#xff1a; ①、firewalld&#xff08;Dynamic Firewall Manager of Linux systems&#xff0c;Linux系统的动态防火墙管理器&#xff09;服务是默认的防火墙配置管理工具&#xff0c;它拥有基于CLI&#xff08;命令行界面&#xff09;和基于GUI&#xff08;图…

Web Tours系统使用说明书

1.系统简介 产品名称&#xff1a;LoadRunner的Web Tours系统 产品功能&#xff1a;注册和登录用户信息、订票办理、退片办理、查询已定票信息、退出系统 系统默认用户&#xff1a;用户名jojo 密码&#xff1a;bean 2. 功能简介 2.1 注册用户信息 点击 sign up now&#xff…

(自学用)正演理论

基于波动方程如何解决数值频散问题——快速正演方法 NAD方法&#xff1a; 怎样离散/逼近高阶偏导数&#xff08;如何采样&#xff09;&#xff1a; 传统方法是用某一点及其周围点的函数f的线性组合来逼近导数。只有函数值&#xff0c;要想提高精度&#xff0c;压制数值频散就必…

(C语言)fscanf与fprintf函数详解

目录 1 fprintf详解 1.1 向文件流中输入数据 1.2 向标准输入流写数据 2. fscanf函数详解 2.1 从文件中读取格式化数据 2.2 从标准输入流中读取格式化数据 1 fprintf详解 头文件&#xff1a;stdio.h 该函数和printf的参数特别像&#xff0c;只是多了一个参数stream&#…

Unity类银河恶魔城学习记录13-5,6 p146 Delete save file,p147 Encryption of saved data源代码

Alex教程每一P的教程原代码加上我自己的理解初步理解写的注释&#xff0c;可供学习Alex教程的人参考 此代码仅为较上一P有所改变的代码 【Unity教程】从0编程制作类银河恶魔城游戏_哔哩哔哩_bilibili FileDataHandler.cs using System; using System.IO; using UnityEngine; p…

现代农业AI智能化升级之路:机器学习在现代农业领域的现状与未来发展

&#x1f9d1; 作者简介&#xff1a;阿里巴巴嵌入式技术专家&#xff0c;深耕嵌入式人工智能领域&#xff0c;具备多年的嵌入式硬件产品研发管理经验。 &#x1f4d2; 博客介绍&#xff1a;分享嵌入式开发领域的相关知识、经验、思考和感悟&#xff0c;欢迎关注。提供嵌入式方向…

Android AIDL接口

一.AlDI接口简介 AIDL&#xff08;Android Interface Definition Language&#xff09;是一种 IDL 语言&#xff0c;用于生成可以在 Android 设备上两个进程之间进行进程间通信&#xff08;IPC&#xff09;的代码。 通过 AIDL&#xff0c;可以在一个进程中获取另一个进程的数据…

力扣112,路径总和

给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点 的路径&#xff0c;这条路径上所有节点值相加等于目标和 targetSum 。如果存在&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 。 叶子节点 是指没有子节点…
最新文章