本专栏由Mculover666创建,主要内容为寻找嵌入式领域内的优质开源项目,一是帮助开发者使用开源项目实现更多的功能,二是通过这些开源项目,学习大佬的代码及背后的实现思想,提升自己的代码水平,和其它专栏相比,本专栏的优势在于:
不会单纯的介绍分享项目,还会包含作者亲自实践的过程分享,甚至还会有对它背后的设计思想解读。
目前本专栏包含的开源项目有:
- SFUD | 一个简洁实用的开源项目,帮你轻松搞定SPI Flash
- cJSON | 一个轻量级C语言JSON解析器
- paho | 支持10种语言编写mqtt客户端,总有一款适合你!
- MultiButton | 一个小巧简单易用的事件驱动型按键驱动模块
- letter-shell | 一个功能强大的嵌入式shell
如果您自己编写或者发现的开源项目不错,欢迎留言或者私信投稿到本专栏,分享获得双倍的快乐!
1. EasyLogger本期给大家带来的开源项目是 EasyLogger,一款轻量级且高性能的日志库,作者armink,目前收获 1.1K 个 star,遵循 MIT 开源许可协议。
EasyLogger 是一款超轻量级、高性能的 C/C++ 日志库,非常适合对资源敏感的软件项目,相比之下, EasyLogger 的功能更加简单,提供给用户的接口更少,上手会更快,更多实用功能支持以插件形式进行动态扩展。
目前EasyLogger支持以下功能:
- 日志输出方式支持串口、Flash、文件等;
- 日志内容可包含级别、时间戳、线程信息、进程信息等;
- 支持多种操作系统,支持裸机;
- 各级别日志支持不同颜色显示;
2. 移植EasyLogger项目地址:https://github.com/armink/EasyLogger
2.1. 移植思路
在移植过程中主要参考两个资料:项目的readme文档和demo工程。
对于这些开源项目,其实移植起来也就两步:
- ① 添加源码到裸机工程中;
- ② 实现需要的接口即可;
2.2. 准备裸机工程
本文中我使用的是小熊派IoT开发套件,主控芯片为STM32L431RCT6:
移植之前需要准备一份裸机工程,我使用STM32CubeMX生成,使用USART1的查询方式发送数据,并将printf重定向到USART1,具体过程请参考:
- STM32CubeMX_06 | 使用USART发送和接收数据(查询模式)
- STM32CubeMX_09 | 重定向printf函数到串口输出的多种方法
串口USART1配置如下:
生成工程后printf重定向代码如下:
#include <stdio.h> int fputc(int ch, FILE *stream) { /* 堵塞判断串口是否发送完成 */ while((USART1->ISR & 0X40) == 0); /* 串口发送完成,将该字符发送 */ USART1->TDR = (uint8_t) ch; return ch; }
裸机工程准备好之后开始移植easylogger。
2.3. 添加elog到工程中
① 复制源码到工程中:
② 在keil中添加easylogge组件的源码文件:
- port/elog_port.c:elog移植接口文件;
- src/elog.c:elog核心功能源码;
- src/elog_utils.c:elog所用到的一些c库工具函数实现;
- src/elog_buf.c(可选添加):elog缓冲输出模式源码;
- src/elog_async.c(可选添加):elog异步输出模式源码;
③ 将easylogger/inc头文件路径添加到keil中:
2.4. 实现elog移植接口
elog的移植接口都已经写好了,在elog_port.c文件中,只需要在函数体中添加代码即可。
① elog初始化接口
ElogErrCode elog_port_init(void);
如果涉及到后续elog使用资源的初始化,比如动态申请分配缓冲区内存,可以放在此接口中,本文中保持默认。
② elog日志输出接口(重点)
//开头添加 #include <stdio.h> …… //接口实现 void elog_port_output(const char *log, size_t size) { //日志使用printf输出,printf已经重定向到串口USART1 printf("%.*s", size, log); }
这儿有个小知识点,%s表示字符串输出,.<十进制数>是精度控制格式符,输出字符时表示输出字符的位数,在精度控制时,小数点后的十进制数可以使用*来占位,在后面提供一个变量作为精度控制的具体值。
③ 日志输出上锁/解锁接口
该接口可以对日志输出接口进行上锁/解锁,以保证日志在并发输出时的正确性,本文中使用的是裸机程序,所以在此使用关闭全局中断来加锁,打开全局中断来解锁:
//开头添加 #include <stm32l4xx_hal.h> …… //接口实现 void elog_port_output_lock(void) { //关闭全局中断 __set_PRIMASK(1); } void elog_port_output_unlock(void) { //开启全局中断 __set_PRIMASK(0); }
STM32开关全局中断的方式很多,本文中直接操作 PRIMASK 寄存器来快速的屏蔽/打开全局中断
④ 系统信息获取接口
elog提供了三个接口用来获取当前时间、获取进程号、获取线程号,因为本文中移植到裸机工程中,并且没有提供时间支持,所以这三个接口都返回空字符串,如下:
const char *elog_port_get_time(void) { return ""; } const char *elog_port_get_p_info(void) { return ""; } const char *elog_port_get_t_info(void) { return ""; }
2.5. 配置elog
elog的核心功能开启宏定义和核心参数宏定义都在配置文件elog_cfg.h中,在本文中只讲述其中重要的宏定义。
日志输出总开关:
/* enable log output. */ #define ELOG_OUTPUT_ENABLE
换行符宏定义修改如下:
/* output newline sign */ #define ELOG_NEWLINE_SIGN "\r\n"
带有颜色的日志输出开关:
/* enable log color */ #define ELOG_COLOR_ENABLE
移植时并没有添加异步输出和缓冲区输出的源码,所以将这两个功能关掉:
至此,移植配置完成,接下来可以开始愉快的使用啦!
3.1. 初始化elog
elog使用之前需要初始化,过程有三步:
① 初始化elog
ElogErrCode elog_init(void);
② 设置日志输出格式
void elog_set_fmt(uint8_t level, size_t set);
其中第一个参数表示设置哪个日志输出级别对应的输出格式,从以下宏定义中选择一个:
/* output log's level */ #define ELOG_LVL_ASSERT 0 #define ELOG_LVL_ERROR 1 #define ELOG_LVL_WARN 2 #define ELOG_LVL_INFO 3 #define ELOG_LVL_DEBUG 4 #define ELOG_LVL_VERBOSE 5
其二个参数是日志输出格式,枚举给出,可以自由组合搭配:
/* all formats index */ 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)
③ 启动elog
void elog_start(void);
接下来在main函数中的usart1初始化函数之后,while(1)之前编写elog初始化代码:
/* USER CODE BEGIN 2 */ /* 初始化elog */ elog_init(); /* 设置每个级别的日志输出格式 */ //输出所有内容 elog_set_fmt(ELOG_LVL_ASSERT, ELOG_FMT_ALL); //输出日志级别信息和日志TAG elog_set_fmt(ELOG_LVL_ERROR, ELOG_FMT_LVL | ELOG_FMT_TAG); elog_set_fmt(ELOG_LVL_WARN, ELOG_FMT_LVL | ELOG_FMT_TAG); elog_set_fmt(ELOG_LVL_INFO, ELOG_FMT_LVL | ELOG_FMT_TAG); //除了时间、进程信息、线程信息之外,其余全部输出 elog_set_fmt(ELOG_LVL_DEBUG, ELOG_FMT_ALL & ~(ELOG_FMT_TIME | ELOG_FMT_P_INFO | ELOG_FMT_T_INFO)); //输出所有内容 elog_set_fmt(ELOG_LVL_VERBOSE, ELOG_FMT_ALL); /* 启动elog */ elog_start(); /* USER CODE END 2 */
3.2. elog日志输出
elog中每种级别都有一种完整方式,两种简化方式,使用时自行选择:
#define elog_assert(tag, ...) #define elog_a(tag, ...) //简化方式1,每次需填写 LOG_TAG #define log_a(...) //简化方式2,LOG_TAG 在文件顶部定义,使用前无需填写 LOG_TAG #define elog_error(tag, ...) #define elog_e(tag, ...) #define log_e(...) #define elog_warn(tag, ...) #define elog_w(tag, ...) #define log_w(...) #define elog_info(tag, ...) #define elog_i(tag, ...) #define log_i(...) #define elog_debug(tag, ...) #define elog_d(tag, ...) #define log_d(...) #define elog_verbose(tag, ...) #define elog_v(tag, ...) #define log_v(...)
前两种在使用的时候只需要包含<elog.h>头文件即可,第三种方式除了包含头文件之外,还需要在文件开始定义TAG宏定义,使用起来和printf相同,所以这里我使用第三种方法演示。
首先在main.c文件开始定义TAG宏,包含头文件:
/* USER CODE BEGIN Includes */ #define LOG_TAG "main" #include <elog.h> /* USER CODE END Includes */
然后在main函数中编写的elog初始化代码之后,继续添加代码,测试elog的使用:
log_a("Hello EasyLogger!"); log_e("Hello EasyLogger!"); log_w("Hello EasyLogger!"); log_i("Hello EasyLogger!"); log_d("Hello EasyLogger!"); log_v("Hello EasyLogger!");
编译,烧写,使用串口终端(Mobaxterm)查看串口输出:
3.3. 五彩缤纷的输出
要想五彩缤纷的日志,仅在elog_cfg.h中使能颜色输出还不够,还需要使用API开启输出:
void elog_set_text_color_enabled(bool enabled);
在初始化elog的时候使能文字颜色输出:
再次编译、下载、查看输出:
每个级别日志的前景色、背景色、字体都可以在elog_cfg.h中修改宏定义,宏定义的值在elog.c中给出,可自行查看,比如这里我将ERROR级别的日志修改为闪烁字体:
编译、下载、查看输出:
3.4. 移植前后内存占用情况
移植前的裸机工程只具有usart1收发功能,移植easylogger之后两者内存对比如下:
3.5. elog的高级功能
elog除了基本的日志功能之外,还提供了一些高级功能,比如:
- 日志输出过滤功能:可以按级别、TAG、关键词过滤日志;
- 缓冲输出模式;
- 异步输出模式;
这些功能如何使用,在项目的readme文档中讲述的很详细,本文限于篇幅,这些高级功能不详细讲述,如有兴趣深入,可以自行研究。
4. 设计思想解读4.1. 数据加工
使用日志打印组件与使用printf最基本的区别在于:输出了更多有利于调试的信息,可以理解为对输出数据进行了一次加工。
打印语句所在文件、函数名、行号这些信息是利用了编译器内置宏的功能:
- __FILE__:文件名
- __FUNCTION__:函数名
- __LINE__:行号
而在终端中输出有颜色的字符则是利用了ANSI escape code,即Escape 序列屏幕控制码,关于这两个知识点详细的解释和示例请阅读:
- 编译器宏详解
- ANSI escape code详解
在elog中对输出内容进行加工处理的函数为:
/** * output the log * * @param level level * @param tag tag * @param file file name * @param func function name * @param line line number * @param format output format * @param ... args * */ void elog_output(uint8_t level, const char *tag, const char *file, const char *func,const long line, const char *format, ...) ;
4.2. 日志输出模式
俗话说,师傅领进门,修行在个人,本文所讲述的只是日志打印组件的基本功能,使用printf直接实现日志输出接口,所以在日志输出模式上和使用printf输出没有区别,只不过多了些信息。
elog支持异步输出模式,开启异步输出模式后,将会提升用户应用程序的执行效率。应用程序在进行日志输出时,无需等待日志彻底输出完成,即可直接返回。
elog也支持缓冲输出模式,开启缓冲输出模式后,如果缓冲区不满,用户线程在进行日志输出时,无需等待日志彻底输出完成,即可直接返回。但当日志缓冲区满以后,将会占用用户线程,自动将缓冲区中的日志全部输出干净。
这两种无需等待,直接返回的日志输出模式,在打印大量日志信息的时候非常重要,打印日志的代码对正常应用程序的影响越小越好,本文不再讲述,还请读者自行研究。
5. 项目工程源码获取和问题交流