写在最前面
- 去年在项目中有用easylogger来作为日志输出,但因为是裸机实现项目,在项目发展后期选择了更轻量的log实现方式摒弃了easylogger,如今想把做过的项目用freertos实现以下,由此便需要能够适配RTOS的日志库,从而,用回了easylogger。
- 基于zynq7020,学习和移植easylogger,并写下笔记作为记录
参考链接
网上有很多关于freertos移植easylogger的教程,在我实现的过程中,主要通过以下链接进行学习和移植:
- https://blog.csdn.net/yaq_30401/article/details/124279322
- 代码库中的readme文档有很详细很详细的使用说明,https://gitee.com/Armink/EasyLogger/blob/master/docs/zh/api/kernel.md
一 移植过程
- 从gitee获取源码 : https://gitee.com/Armink/EasyLogger?_from=gitee_search 同时我们可以看到很详细的说明文档,可以进行初步学习
- 复制整个easylogger目录下的easylogger目录至工程中
- 若不将日志写入flash以及文件,可以删除plugin目录,在工程中添加include路径
- 根据参考链接修改elog_cfg.h中的宏定义
- 最重要的是,elog_port.c中互斥量的建立和使用,从而实现了freertos中日志输出的完整性,究其根本其实应该是对串口使用的互斥,因此,当使用了easylogger后,一定不要使用其他的打印方式,如xil_printf,printf等,因xilinx提供的xil_printf以及c标准库的printf均未实现串口的互斥使用机制
二 使用和测试
-
创建空的freertos工程,移植好easylogger后,创建两个任务进行日志打印,测试easylogger打印结果和xil_printf打印结果:
-
使用easylogger打印代码与结果:
#include <stdio.h> #include <stdlib.h> #include "xparameters.h" #include "xil_printf.h" #include "xil_cache.h" #include "xtime_l.h" #include "FreeRTOS.h" #include "task.h" #include "elog.h" #define THREAD_STACKSIZE 1024 #define DEFAULT_THREAD_PRIO 2 TaskHandle_t task1_handler = NULL; TaskHandle_t task2_handler = NULL; /** * init easylogger,这个函数是我们自己添加的,便于用户直接调用,需要在elog.h中添加声明 */ void easylogger_init(void) { /* init Easylogger */ elog_init(); /* set EasyLogger log format */ elog_set_fmt(ELOG_LVL_ASSERT, ELOG_FMT_ALL); elog_set_fmt(ELOG_LVL_ERROR, ELOG_FMT_LVL | ELOG_FMT_TIME | ELOG_FMT_T_INFO); elog_set_fmt(ELOG_LVL_WARN, ELOG_FMT_LVL | ELOG_FMT_T_INFO); elog_set_fmt(ELOG_LVL_INFO, ELOG_FMT_LVL); elog_set_fmt(ELOG_LVL_DEBUG, ELOG_FMT_ALL & ~ELOG_FMT_FUNC); /*Eenbale color*/ elog_set_text_color_enabled(true); /* start EasyLogger */ elog_start(); } void test(void *parameter) { //xil_printf("enter test\r\n"); int *hh = (int *)parameter; while(1) { //xil_printf("hh is %d\r\n",*hh); log_a("%d: Hello EasyLogger!", *hh); log_e("%d: Hello EasyLogger!", *hh); log_w("%d: Hello EasyLogger!", *hh); log_i("%d: Hello EasyLogger!", *hh); log_d("%d: Hello EasyLogger!", *hh); log_v("%d: Hello EasyLogger!", *hh); /* xil_printf("%d: Hello EasyLogger!\r\n", *hh); xil_printf("%d: Hello EasyLogger!\r\n", *hh); xil_printf("%d: Hello EasyLogger!\r\n", *hh); xil_printf("%d: Hello EasyLogger!\r\n", *hh); xil_printf("%d: Hello EasyLogger!\r\n", *hh); xil_printf("%d: Hello EasyLogger!\r\n", *hh);*/ vTaskDelay(1/portTICK_PERIOD_MS); } } void main_task(void *p) { int testNum = 1; xTaskCreate(test, ( const char * const)"task1", THREAD_STACKSIZE, (void *)&testNum, DEFAULT_THREAD_PRIO, &task1_handler); int testNum1 = 2; xTaskCreate(test, ( const char * const)"task2", THREAD_STACKSIZE, (void *)&testNum1, DEFAULT_THREAD_PRIO, &task2_handler); //sys_thread_new("system_init_thread", system_init_thread, &i, THREAD_STACKSIZE, DEFAULT_THREAD_PRIO); vTaskDelete(NULL); } int main(void) { char * pcName = "main_task"; easylogger_init(); TaskHandle_t xHandle = NULL; xTaskCreate(main_task, ( const char * const) pcName, THREAD_STACKSIZE, NULL, DEFAULT_THREAD_PRIO, &xHandle); vTaskStartScheduler(); while (1); return 0; }
结果:如下图,任务间打印实现互斥,无乱序出现
-
使用xil_printf打印代码与结果:
#include <stdio.h> #include <stdlib.h> #include "xparameters.h" #include "xil_printf.h" #include "xil_cache.h" #include "xtime_l.h" #include "FreeRTOS.h" #include "task.h" #include "elog.h" #define THREAD_STACKSIZE 1024 #define DEFAULT_THREAD_PRIO 2 TaskHandle_t task1_handler = NULL; TaskHandle_t task2_handler = NULL; /** * init easylogger,这个函数是我们自己添加的,便于用户直接调用,需要在elog.h中添加声明 */ void easylogger_init(void) { /* init Easylogger */ elog_init(); /* set EasyLogger log format */ elog_set_fmt(ELOG_LVL_ASSERT, ELOG_FMT_ALL); elog_set_fmt(ELOG_LVL_ERROR, ELOG_FMT_LVL | ELOG_FMT_TIME | ELOG_FMT_T_INFO); elog_set_fmt(ELOG_LVL_WARN, ELOG_FMT_LVL | ELOG_FMT_T_INFO); elog_set_fmt(ELOG_LVL_INFO, ELOG_FMT_LVL); elog_set_fmt(ELOG_LVL_DEBUG, ELOG_FMT_ALL & ~ELOG_FMT_FUNC); /*Eenbale color*/ elog_set_text_color_enabled(true); /* start EasyLogger */ elog_start(); } void test(void *parameter) { //xil_printf("enter test\r\n"); int *hh = (int *)parameter; while(1) { //xil_printf("hh is %d\r\n",*hh); /*log_a("%d: Hello EasyLogger!", *hh); log_e("%d: Hello EasyLogger!", *hh); log_w("%d: Hello EasyLogger!", *hh); log_i("%d: Hello EasyLogger!", *hh); log_d("%d: Hello EasyLogger!", *hh); log_v("%d: Hello EasyLogger!", *hh);*/ xil_printf("%d: Hello EasyLogger!\r\n", *hh); xil_printf("%d: Hello EasyLogger!\r\n", *hh); xil_printf("%d: Hello EasyLogger!\r\n", *hh); xil_printf("%d: Hello EasyLogger!\r\n", *hh); xil_printf("%d: Hello EasyLogger!\r\n", *hh); xil_printf("%d: Hello EasyLogger!\r\n", *hh); vTaskDelay(1/portTICK_PERIOD_MS); } } void main_task(void *p) { int testNum = 1; xTaskCreate(test, ( const char * const)"task1", THREAD_STACKSIZE, (void *)&testNum, DEFAULT_THREAD_PRIO, &task1_handler); int testNum1 = 2; xTaskCreate(test, ( const char * const)"task2", THREAD_STACKSIZE, (void *)&testNum1, DEFAULT_THREAD_PRIO, &task2_handler); //sys_thread_new("system_init_thread", system_init_thread, &i, THREAD_STACKSIZE, DEFAULT_THREAD_PRIO); vTaskDelete(NULL); } int main(void) { char * pcName = "main_task"; easylogger_init(); TaskHandle_t xHandle = NULL; xTaskCreate(main_task, ( const char * const) pcName, THREAD_STACKSIZE, NULL, DEFAULT_THREAD_PRIO, &xHandle); vTaskStartScheduler(); while (1); return 0; }
结果如下图:任务间打印存在乱序
三 easylogger 源码学习
能够移植和测试使用仅仅是第一步,还可以进一步学习源码,了解其实现原理和思路
-
目录结构分析如下图,在easylogger的readme.md中有更详细的说明介绍:
- elog_cfg.h文件:
- 打开文件可以看到注释十分清晰,基本每个宏定义的含义都说的很清楚
- 分为四大块:第一块为核心配置,包括输出使能,每行日志的长度等;第二块为颜色区域,给不同的日志等级分配不同的显示颜色;第三块为异步输出相关的配置,因需要pthread的支持,在freertos中需要注释掉异步输出使能?第四块为缓冲输出,即日志输出不会立刻输出,而是等待缓冲区满之后再一起输出。
- 以上四大块的具体实现逐步再看
- elog.h文件:
-
宏定义:6个日志等级;过滤日志的等级;软件版本;日志格式定义(ELOG_FMT_ALL等);
-
数据结构:EasyLogger结构体,ElogErrCode 枚举类型;ElogFilter结构体;以及ElogTagLvlFilter结构体
- 查看EasyLogger结构体,
/* easy logger */ typedef struct { ElogFilter filter; //日志过滤及饿哦固体 size_t enabled_fmt_set[ELOG_LVL_TOTAL_NUM]; //日志等级个数 bool init_ok; //初始化OK标志 bool output_enabled; bool output_lock_enabled; bool output_is_locked_before_enable; bool output_is_locked_before_disable; #ifdef ELOG_COLOR_ENABLE bool text_color_enabled; #endif }EasyLogger, *EasyLogger_t;
-
查看日志输出过滤器结构体
/* output log's filter */ typedef struct { uint8_t level; char tag[ELOG_FILTER_TAG_MAX_LEN + 1]; //过滤标记 char keyword[ELOG_FILTER_KW_MAX_LEN + 1]; //过滤关键字 ElogTagLvlFilter tag_lvl[ELOG_FILTER_TAG_LVL_MAX_NUM]; } ElogFilter, *ElogFilter_t;
-
函数声明:easylogger将所有需要的接口都集中在elog.h中进行定义,从而在使用的时候仅需要引用elog.h即可;其核心代码实现了日志结构体的初始化,开始,停止以及过滤有关的功能,
-
重定义:通过宏定义elog_i,elog_d,elog_e等为log_i,log_d,log_e等
- elog.c文件:
-
全局变量:
- static EasyLogger elog; //静态变量 elog,作为整个easylogger的核心变量,仅elog.c文件可见,对外采用接口形式
- static char log_buf[ELOG_LINE_BUF_SIZE] = { 0 }; //每行可发送的数据长度缓冲区,在输出中,通过对该缓冲区进行填充,实现了日志的格式化,最终将该缓冲区内数据发出
-
初始化函数:ElogErrCode elog_init(void),对elog中字段进行填充,调用elog_port_init()对接口函数进行初始化;设置过滤等级和日志颜色使能
-
开始与结束函数:elog_start(void) < ----- > elog_stop(void) 使能和失能输出标志,即设置elog.output_enabled 标志为enable或disable;
-
设置输出格式:elog_set_fmt(uint8_t level, size_t set),其中level为日志的等级,set则为需要使能的格式位说明
void elog_set_fmt(uint8_t level, size_t set) { ELOG_ASSERT(level <= ELOG_LVL_VERBOSE); elog.enabled_fmt_set[level] = set; } //格式定义如下:可以输出,日志等级,标记,当前时间,进程信息,现成信息,文件目录与文件名,函数名以及行号,每个输出格式使能占一位,使能该日志信息,则将该为置1,进行并运算即可 typedef enum { ELOG_FMT_LVL = 1 << 0, /**< level */ ELOG_FMT_TAG = 1 << 1, /**< tag */ ELOG_FMT_TIME = 1 << 2, /**< current time */ ELOG_FMT_P_INFO = 1 << 3, /**< process info */ ELOG_FMT_T_INFO = 1 << 4, /**< thread info */ ELOG_FMT_DIR = 1 << 5, /**< file directory and name */ ELOG_FMT_FUNC = 1 << 6, /**< function name */ ELOG_FMT_LINE = 1 << 7, /**< line number */ } ElogFmtIndex; //对于全部输出的情况 /* macro definition for all formats */ #define ELOG_FMT_ALL (ELOG_FMT_LVL|ELOG_FMT_TAG|ELOG_FMT_TIME|ELOG_FMT_P_INFO|ELOG_FMT_T_INFO| \ ELOG_FMT_DIR|ELOG_FMT_FUNC|ELOG_FMT_LINE)
-
设置日志输出过滤器:void elog_set_filter(uint8_t level, const char *tag, const char *keyword),可以对某等级的tag或关键字进行过滤
-
日志输出函数:其参数列表为可变参数列表,包含很多信息;整个实现过程为通过elog_strcpy函数,判断各个标记位是否打开,填充log_buf发送缓冲区,最终通过elog_port_output(log_buf, log_len)函数发送
/** *level:日志等级 * tag: 日志输出标记 * file : 若使能了输出到文件,则输出到文件的文件名 */ void elog_output(uint8_t level, const char *tag, const char *file, const char *func, const long line, const char *format, ...)
-
日志颜色输出原理
-
实现代码如下:
#define CSI_START "\033[" #define CSI_END "\033[0m" #ifdef ELOG_COLOR_ENABLE /* color output info */ static const char *color_output_info[] = { [ELOG_LVL_ASSERT] = ELOG_COLOR_ASSERT, // ("35;" "22m") 分别为:字体颜色,背景颜色,字体类型 [ELOG_LVL_ERROR] = ELOG_COLOR_ERROR, [ELOG_LVL_WARN] = ELOG_COLOR_WARN, [ELOG_LVL_INFO] = ELOG_COLOR_INFO, [ELOG_LVL_DEBUG] = ELOG_COLOR_DEBUG, [ELOG_LVL_VERBOSE] = ELOG_COLOR_VERBOSE, }; #endif /* ELOG_COLOR_ENABLE */ //日志头: if (elog.text_color_enabled) { log_len += elog_strcpy(log_len, log_buf + log_len, CSI_START); log_len += elog_strcpy(log_len, log_buf + log_len, color_output_info[level]); } //日志尾: #ifdef ELOG_COLOR_ENABLE /* add CSI end sign */ if (elog.text_color_enabled) { log_len += elog_strcpy(log_len, log_buf + log_len, CSI_END); } #endif
-
实现原理:涉及到了ANSI转义序列(ANSI escape sequences)的知识内容,可通过参考链接学习了解:完整的Ansi 转义序列为:0x1B + “[” + <zero or more numbers, separated by “;”> +
<a letter>
,当如xshell或MobaXterm等工具接收到该串口信息后,会根据转义序列提供的信息显示串口数据- https://zhuanlan.zhihu.com/p/451658181
- https://blog.csdn.net/q1003675852/article/details/134999871
- https://blog.csdn.net/u013391094/article/details/127143727
-
四 测试说明(颜色&过滤)
-
整体输出情况如下,2个线程交替输出所有等级的日志信息,每个等级的日志格式与颜色均不相同
-
设置过滤条件为,低于info,且不含有hhh关键字的日志不输出,效果如下:
elog_set_filter(ELOG_LVL_INFO, "", "hhh");
可以设置和使用的功能还有不少,可以参考API说明多多尝试!