前言
(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语言中"#“和”##"的用法