单片机如何实现日志等级打印(适用于多线程,多串口)

本文介绍了如何在C语言中利用`__VA_ARGS__`宏实现不同等级的日志打印,从ESP32的ESP_LOGx()扩展到单线程和多线程环境下的日志控制,包括使用互斥锁保证线程安全。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

(1)如果有嵌入式企业需要招聘湖南区域日常实习生,任何区域的暑假Linux驱动实习岗位,可C站直接私聊,或者邮件:zhangyixu02@gmail.com,此消息至2025年1月1日前均有效
(2)如果玩过ESP32的同学,会发现,他们的存在日志打印的内容,可以实现不同等级的日志信息打印,如果我们也想要在其他MCU上实现这一功能,应当如何处理呢?
(3)学习本文之前,建议先学习一下:ESP32S3的ESP_LOGx()控制台输出详细介绍

理论

“__ VA_ARGS __”宏介绍

(1)首先,在了解单片机如何实现日志等级打印之前,我们需要了解## __VA_ARGS__这个宏知识。
(2)在C语言中,很多特殊宏, __VA_ARGS__属于其中之一,它用于表示宏中的可变参数部分。在定义带有可变参数的宏时,可以使用__VA_ARGS__来表示在宏中传递的可变参数列表。
(3)例如下面这个例子中,我想对printf()函数进行宏定义,可以这么做。

#include <stdio.h>

// 简单的可变参数宏,将可变参数打印到控制台
#define PRINT_VARIABLE_ARGS(format, ...)  printf(format, __VA_ARGS__)

int main()
{
    // 使用宏打印不定数量的参数
    PRINT_VARIABLE_ARGS("Hello, %s! The sum is: %d\n", "John", 10+20);

    return 0;
}

“##”作用

(1)不知道各位看到上面的代码,有没有发现一些问题。对于printf()函数,我们可以只输出一个字符串,也就是只传入一个参数。
(2)我们于是尝试只传入一个参数试试,会发现存在报错。为什么呢?我们看看他预处理之后的结果就了解问题所在了,我们看到,预处理之后还有一个逗号,这明显是不符合C语言规范的。

#include <stdio.h>

// 简单的可变参数宏,将可变参数打印到控制台
#define PRINT_VARIABLE_ARGS(format, ...)  printf(format, __VA_ARGS__)

int main()
{
    // 使用宏打印不定数量的参数
    PRINT_VARIABLE_ARGS("Hello");
	//预处理之后  printf("Hello", );
    return 0;
}

(3)为了解决上面的问题,C语言又提供了一种处理办法,“##”。如果可变参数被忽略或为空,’##’操作将使预处理器(preprocessor)去除掉它前面的那个逗号。
(4)因此,我们按照如下方式编写即可。

#include <stdio.h>

// 简单的可变参数宏,将可变参数打印到控制台
#define PRINT_VARIABLE_ARGS(format, ...)  printf(format, ##__VA_ARGS__)

int main()
{
    // 使用宏打印不定数量的参数
    PRINT_VARIABLE_ARGS("Hello");
	//预处理之后  printf("Hello");
    return 0;
}

实操

单线程裸机程序实现

(1)了解了上面的知识之后,我们就可以开始实操了。这里我们将模拟ESP32的函数打印函数实现。
(2)Set_Log_Level用于设置日志等级的,Verbose输出信息最多,Error输出信息最少,NO_output不进行日志打印。

#include <stdio.h>

#define NO_output  0
#define Error      1
#define Warning    2
#define Info       3
#define Debug      4
#define Verbose    5

#define Set_Log_Level Info //设置日志输出等级

#if Set_Log_Level >= Verbose
#define ESP_LOGV(format, ...)  printf("Verbose:"format, ##__VA_ARGS__)
#else
#define ESP_LOGV(format, ...)
#endif /*<Set_Log_Level >= Verbose */

#if Set_Log_Level >= Debug
#define ESP_LOGD(format, ...)  printf("Debug:"format, ##__VA_ARGS__)
#else
#define ESP_LOGD(format, ...)
#endif /*<Set_Log_Level >= Debug */

#if Set_Log_Level >= Info
#define ESP_LOGI(format, ...)  printf("Info:"format, ##__VA_ARGS__)
#else
#define ESP_LOGI(format, ...)
#endif /*<Set_LOG_Level >= Info */

#if Set_Log_Level >= Warning
#define ESP_LOGW(format, ...)  printf("Warning:"format, ##__VA_ARGS__)
#else
#define ESP_LOGW(format, ...)
#endif /*<Set_Log_Level >= Warning */

#if Set_Log_Level >= Error
#define ESP_LOGE(format, ...)  printf("Error:"format, ##__VA_ARGS__)
#else
#define ESP_LOGE(format, ...)
#endif /*<Set_Log_Level >= Error */


int main() 
{
    // 使用宏打印不定数量的参数
    ESP_LOGE("Hello, %s! The sum is: %d\n", "John", 10+20);
    ESP_LOGW("Hello, %s! The sum is: %d\n", "John", 10+20);
    ESP_LOGI("Hello, %s! The sum is: %d\n", "John", 10+20);
    ESP_LOGD("Hello, %s! The sum is: %d\n", "John", 10+20);
    ESP_LOGV("Hello, %s! The sum is: %d\n", "John", 10+20);
    return 0;
}

多串口实现日志打印

(1)此部分详细信息请看C站:如何编写一个可变参数函数?如何让所有单片机的所有串口实现printf函数?
(2)这里的Set_Log_Level用于设置日志等级,Set_Log_Uartx 用于设置多串口日志打印的地址信息。

#include <stdio.h>

#define NO_output  0
#define Error      1
#define Warning    2
#define Info       3
#define Debug      4
#define Verbose    5

#define Set_Log_Level Info  //设置日志输出等级

#define Set_Log_Uart0 UART0 //设置串口地址
#define Set_Log_Uart1 UART1
#define Set_Log_Uart2 UART2
/* 作用 : 用于多路串口重定义
 * 传入参数 : 
     baseAddress : 要打印的串口地址,UARTx_BASE,x可为0,1,2,3,4,5,6,7
     format : 需要打印的东西
     ... : 如果是打印字符,输入%c。有符号数字,%d。用法与printf一样
 * 返回值 : 无
*/
void Uart_Printf(uint32_t baseAddress, const char *format,...)
{
    uint32_t length;
    va_list args;
    uint32_t i;
    char TxBuffer[128] = {0};

    va_start(args, format);
    length = vsnprintf((char*)TxBuffer, sizeof(TxBuffer), (char*)format, args);
    va_end(args);

    for(i = 0; i < length; i++)
	{
		//根据不同单片机的串口发送函数,依次发送TxBuffer[i]数据。
		//以下以MSP430为例
		USCI_A_UART_transmitData(baseAddress, TxBuffer[i]);
		//以下以TM4C123为例
		while(UARTBusy(baseAddress));
		UARTCharPut(baseAddress,TxBuffer[i]);
		//以下以STM32F103为例
		USART_SendByte(baseAddress,TxBuffer[i]);
		while(USART_GetFlagStatus(baseAddress,USART_FLAG_TC) == RESET);
	}
}

#if Set_Log_Level >= Verbose
#define Uart0_LOGV(format, ...)  printf(Set_Log_Uart0,"Verbose:"format, ##__VA_ARGS__)
#define Uart1_LOGV(format, ...)  printf(Set_Log_Uart1,"Verbose:"format, ##__VA_ARGS__)
#define Uart2_LOGV(format, ...)  printf(Set_Log_Uart2,"Verbose:"format, ##__VA_ARGS__)
#else
#define Uart0_LOGV(format, ...)
#define Uart1_LOGV(format, ...)
#define Uart2_LOGV(format, ...)
#endif /*<Set_Log_Level >= Verbose */

/*
 * 下面的就大家自己仿照者写吧
 */

多线程多串口实现日志打印

(1)在多线程的任务处理中,使用printf()函数可能就会需要考虑到线程安全的问题。printf() 在多线程环境下不是线程安全的主要原因是它可能涉及到底层的文件流(stdout),而这些文件流的操作不是原子的。多个线程同时调用 printf()时,可能会导致输出的混乱或错误,因为一个线程的输出可能会与另一个线程的输出交错。
(2)为了避免上述问题,我们可以使用互斥锁进行保护。下面以FreeRTOS为例子。

#include <stdio.h>
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"

#define NO_output  0
#define Error      1
#define Warning    2
#define Info       3
#define Debug      4
#define Verbose    5

#define Set_Log_Level Info  //设置日志输出等级

#define Set_Log_Uart0 UART0 //设置串口地址
#define Set_Log_Uart1 UART1
#define Set_Log_Uart2 UART2

SemaphoreHandle_t mutex;
void Thread_Safe_Printf(uint32_t baseAddress, const char *format,...)
{
    uint32_t length;
    va_list args;
    uint32_t i;
    char TxBuffer[128] = {0};
	// 获取互斥锁
	if (xSemaphoreTake(mutex, portMAX_DELAY)) 
	{
		va_start(args, format);
	    length = vsnprintf((char*)TxBuffer, sizeof(TxBuffer), (char*)format, args);
	    va_end(args);
	
	    for(i = 0; i < length; i++)
		{
			//根据不同单片机的串口发送函数,依次发送TxBuffer[i]数据。
			//以下以MSP430为例
			USCI_A_UART_transmitData(baseAddress, TxBuffer[i]);
			//以下以TM4C123为例
			while(UARTBusy(baseAddress));
			UARTCharPut(baseAddress,TxBuffer[i]);
			//以下以STM32F103为例
			USART_SendByte(baseAddress,TxBuffer[i]);
			while(USART_GetFlagStatus(baseAddress,USART_FLAG_TC) == RESET);
		}
		// 释放互斥锁
		xSemaphoreGive(mutex);
	}
}

#if Set_Log_Level >= Verbose
#define Uart0_LOGV(format, ...)  Thread_Safe_Printf(Set_Log_Uart0,"Verbose:"format, ##__VA_ARGS__)
#define Uart1_LOGV(format, ...)  Thread_Safe_Printf(Set_Log_Uart1,"Verbose:"format, ##__VA_ARGS__)
#define Uart2_LOGV(format, ...)  Thread_Safe_Printf(Set_Log_Uart2,"Verbose:"format, ##__VA_ARGS__)
#else
#define Uart0_LOGV(format, ...)
#define Uart1_LOGV(format, ...)
#define Uart2_LOGV(format, ...)
#endif /*<Set_Log_Level >= Verbose */

/*
 * 下面的就大家自己仿照者写吧
 */

参考

(1)C站:ESP32S3的ESP_LOGx()控制台输出详细介绍
(2)C站:如何编写一个可变参数函数?如何让所有单片机的所有串口实现printf函数?
(3)C站:整理:C/C++可变参数,“## VA_ARGS”宏的介绍和使用
(4)C站:C语言中"#“和”##"的用法

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

风正豪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值