1. 项目概述
作为一名嵌入式开发工程师,我最近在海思WS63平台上完成了EasyLogger日志库的移植工作。WS63是海思面向IoT连接场景的Wi-Fi/星闪SoC芯片,在开发过程中,可靠的日志系统对于调试和问题排查至关重要。
EasyLogger是一款轻量级、可裁剪的嵌入式日志库,特别适合资源受限的MCU环境。它提供了分级日志、标签过滤、异步输出等实用功能,能够有效平衡日志输出的实时性和系统资源占用。
2. 环境准备
2.1 硬件平台
本次移植基于海思WS63开发板,主要硬件配置如下:
- 主频:160MHz
- RAM:256KB
- Flash:2MB
- 调试串口:UART0,波特率115200
2.2 软件栈
开发环境使用了以下软件组件:
- 操作系统:LiteOS-M
- RTOS接口:CMSIS-RTOS2兼容层
- 编译工具链:gcc-arm-none-eabi
- 构建系统:CMake和GN(OpenHarmony风格)
3. EasyLogger架构解析
3.1 核心组件
EasyLogger采用分层架构设计,主要包含以下组件:
- 核心层(elog.c):负责日志的格式化、过滤和输出调度
- 工具层(elog_utils.c):提供字符串处理和内存操作辅助函数
- 异步层(elog_async.c):实现环形缓冲区和异步输出机制
- 缓冲层(elog_buf.c):提供日志缓冲功能,减少碎片化输出
- 移植层(elog_port.c):平台相关接口的实现
3.2 移植重点
在WS63平台上的移植工作主要集中在以下几个方面:
- 实现移植层接口(锁机制、时间戳获取等)
- 适配CMSIS-RTOS2线程模型
- 配置日志输出参数
- 集成到现有构建系统
4. 移植实现细节
4.1 移植层实现
在elog_port.c中,我们需要实现以下关键接口:
c复制/* 初始化日志系统 */
ElogErrCode elog_port_init(void) {
// 创建互斥信号量
output_lock = osSemaphoreNew(1, 1, NULL);
if (!output_lock) return ELOG_ERR_INIT;
// 创建异步通知信号量
output_notice = osSemaphoreNew(1, 0, NULL);
if (!output_notice) return ELOG_ERR_INIT;
// 创建异步输出线程
osThreadAttr_t attr = {
.name = "elog_async",
.stack_size = ELOG_PORT_ASYNC_STACK_SIZE,
.priority = osPriorityBelowNormal
};
async_thread_id = osThreadNew(async_output, NULL, &attr);
if (!async_thread_id) return ELOG_ERR_INIT;
return ELOG_NO_ERR;
}
/* 日志输出接口 */
void elog_port_output(const char *log, size_t size) {
printf("%.*s", (int)size, log);
}
/* 获取时间戳 */
const char *elog_port_get_time(void) {
static char time_buf[20];
uint32_t ticks = osKernelGetTickCount();
snprintf(time_buf, sizeof(time_buf), "%u.%03u",
ticks / 1000, ticks % 1000);
return time_buf;
}
4.2 CMSIS-RTOS2适配
WS63平台使用CMSIS-RTOS2作为RTOS抽象层,与EasyLogger的异步输出机制需要特别注意:
- 避免使用POSIX线程:不要启用
ELOG_ASYNC_OUTPUT_USING_PTHREAD宏 - 使用CMSIS线程API:通过
osThreadNew创建异步输出线程 - 信号量实现互斥:使用
osSemaphore系列函数实现线程同步
4.3 串口输出配置
确保printf已正确重定向到调试串口。在WS63平台上,通常需要实现_write系统调用:
c复制int _write(int fd, char *ptr, int len) {
if (fd == STDOUT_FILENO || fd == STDERR_FILENO) {
uart_write(UART0, ptr, len);
return len;
}
return -1;
}
5. 配置优化
5.1 内存配置
针对WS63有限的RAM资源,需要合理配置日志缓冲区大小:
c复制// elog_cfg.h
#define ELOG_LINE_BUF_SIZE 128 // 单行日志缓冲区大小
#define ELOG_ASYNC_OUTPUT_ENABLE
#define ELOG_ASYNC_RING_LINE_SLOTS 3 // 异步环形缓冲区槽位数
5.2 输出级别控制
通过以下宏定义控制日志输出级别,在发布版本中可以关闭调试日志:
c复制#define ELOG_OUTPUT_LVL ELOG_LVL_INFO // 发布版本建议设为WARN
#define ELOG_ASYNC_OUTPUT_LVL ELOG_LVL_ASSERT // 所有级别都走异步
5.3 格式配置
根据实际需求配置日志输出格式:
c复制#define ELOG_COLOR_ENABLE // 启用颜色输出
#define ELOG_FMT_USING_DIR // 包含文件名
#define ELOG_FMT_USING_FUNC // 包含函数名
#define ELOG_FMT_USING_LINE // 包含行号
6. 构建系统集成
6.1 CMake集成
在项目的CMakeLists.txt中添加以下配置:
cmake复制# EasyLogger源文件
set(ELOG_SOURCES
src/easylogger/src/elog.c
src/easylogger/src/elog_utils.c
src/easylogger/src/elog_async.c
src/easylogger/src/elog_buf.c
src/easylogger/port/elog_port.c
)
# 包含路径
include_directories(
src/easylogger/inc
)
# 添加到目标
target_sources(${PROJECT_NAME} PRIVATE ${ELOG_SOURCES})
6.2 GN集成
对于OpenHarmony风格的GN构建系统,需要在BUILD.gn中添加:
gn复制static_library("xiaohong") {
sources = [
"src/easylogger/src/elog.c",
"src/easylogger/src/elog_utils.c",
"src/easylogger/src/elog_async.c",
"src/easylogger/src/elog_buf.c",
"src/easylogger/port/elog_port.c",
]
include_dirs = [
"src/easylogger/inc",
"//kernel/osal/cmsis",
]
}
7. 使用指南
7.1 初始化流程
建议在系统启动时按以下顺序初始化日志系统:
c复制#include "elog.h"
void system_init(void) {
// 1. 初始化硬件和RTOS
hardware_init();
osKernelInitialize();
// 2. 初始化串口和printf重定向
uart_init();
// 3. 创建应用线程
osThreadNew(app_main, NULL, NULL);
// 4. 启动内核后初始化日志系统
osKernelStart();
}
void app_main(void *arg) {
// 初始化EasyLogger
elog_init();
elog_start();
// 其他应用初始化
// ...
}
7.2 日志输出示例
EasyLogger提供了多种日志输出方式:
c复制// 定义日志标签和级别
#define LOG_TAG "network"
#define LOG_LVL ELOG_LVL_DEBUG
#include "elog.h"
void network_connect(void) {
log_d("Connecting to AP..."); // 调试日志
int ret = wifi_connect();
if (ret == 0) {
log_i("Connected, RSSI=%d", wifi_get_rssi()); // 信息日志
} else {
log_e("Connection failed: %d", ret); // 错误日志
}
// 原始输出
elog_raw("Raw output without formatting\r\n");
// 十六进制dump
uint8_t packet[] = {0x01, 0x02, 0x03, 0x04};
elog_hexdump("packet", 16, packet, sizeof(packet));
}
7.3 运行时配置
可以在运行时动态调整日志过滤和格式:
c复制// 设置全局日志级别
elog_set_filter_lvl(ELOG_LVL_WARN);
// 按标签设置日志级别
elog_set_filter_tag_lvl("network", ELOG_LVL_DEBUG);
// 设置日志格式
elog_set_fmt(ELOG_LVL_INFO,
ELOG_FMT_LVL | ELOG_FMT_TAG | ELOG_FMT_TIME);
8. 性能优化建议
8.1 内存优化
- 缓冲区大小:根据实际日志量调整
ELOG_LINE_BUF_SIZE和ELOG_ASYNC_RING_LINE_SLOTS - 栈大小:确保异步线程栈足够(默认768字节)
- 静态裁剪:通过
ELOG_OUTPUT_LVL移除不必要级别的日志代码
8.2 实时性优化
- 线程优先级:将异步输出线程设为较低优先级(如
osPriorityBelowNormal) - 同步/异步选择:关键路径日志可使用同步输出,普通日志使用异步
- 日志级别控制:生产环境适当提高输出级别阈值
9. 常见问题排查
9.1 无日志输出
- 检查
printf重定向是否正确实现 - 确认
elog_start()已被调用 - 验证日志级别设置是否过高
9.2 日志输出不完整
- 检查单行缓冲区
ELOG_LINE_BUF_SIZE是否足够 - 确认环形缓冲区槽位
ELOG_ASYNC_RING_LINE_SLOTS是否足够 - 检查异步线程栈是否溢出
9.3 系统卡顿
- 降低异步输出线程优先级
- 减少日志输出频率
- 检查是否有大量同步日志输出
10. 扩展功能
10.1 Flash日志插件
对于需要持久化日志的场景,可以启用Flash日志插件:
- 在
elog_cfg.h中启用ELOG_FLASH_PLUGIN_ENABLE - 实现
elog_flash_port.c中的Flash操作接口 - 配置Flash分区参数
10.2 文件系统日志
如果系统已集成文件系统,可以使用文件日志插件:
- 在
elog_cfg.h中启用ELOG_FILE_PLUGIN_ENABLE - 实现
elog_file_port.c中的文件操作接口 - 配置日志文件路径和轮转策略
11. 实际应用经验
在WS63平台上使用EasyLogger一段时间后,我总结出以下几点经验:
- 启动顺序很重要:确保在RTOS完全启动后再初始化日志系统,避免早期日志丢失
- 内存监控:定期检查日志系统内存使用情况,防止内存泄漏
- 性能平衡:在日志详细度和系统性能之间找到平衡点
- 标签管理:建立统一的日志标签命名规范,便于过滤和查找
通过合理配置和使用,EasyLogger可以成为WS63平台上强大的调试和诊断工具,显著提高开发效率和系统可靠性。