嵌入式设备的状态监控与分析怎样才能做到精确?系统日志扮演着至关重要的角色。但在嵌入式设备的开发过程中,与Web后台丰富的框架支持相比,日志的实现方式较为简陋,这让开发者们感到颇为困扰。
系统日志的价值所在
2020-05-31 00:00:36.063 DEBUG [cloud-data-communication-service,a4d26cd44853a39b,a4d26cd44853a39b,false] 5888 --- [http-nio-15050-exec-5] c.s.s.c.c.a.interceptor.UserInterceptor : //TODO 校验token:null
2020-05-31 00:00:36.064 DEBUG [cloud-data-communication-service,a4d26cd44853a39b,a4d26cd44853a39b,false] 5888 --- [http-nio-15050-exec-5] c.s.s.c.c.core.util.UserContextHolder : 设置上下文信息:{}
2020-05-31 00:00:36.192 DEBUG [cloud-data-communication-service,a4d26cd44853a39b,a4d26cd44853a39b,false] 5888 --- [http-nio-15050-exec-5] o.s.w.s.r.ResourceHttpRequestHandler : Resource not found
系统日志对于设备运行至关重要。无论是大型服务器还是普通嵌入式设备,都可能遭遇故障或问题。例如,2019年某大型网络公司的服务器故障,就是通过系统日志找到的,原因竟是某个程序模块的代码漏洞。对于智能家居这类嵌入式设备,系统日志能记录软件运行的每一刻,便于开发者随时了解设备状态。同时,它能迅速捕捉问题发生的瞬间和关键信息,就像为设备健康状况做了详尽的记录。
系统日志让开发者能迅速锁定设备故障原因并加以解决。这在项目开发中极为关键,有助于提高开发速度和确保设备稳定,否则开发者需投入大量时间精力逐一排查。
web与嵌入式在日志框架上的区别
在web后台开发领域,开发者可以选用众多成熟且便捷的日志框架。这些框架显著提高了开发速度和日志记录的便捷性。开发者只需依照框架的指南进行配置,调用相应的方法,就能轻松实现系统日志的全面功能。在此过程中,开发者无需关心日志输出的底层架构等复杂细节。
/********** 禁用半主机模式 **********/
#pragma import(__use_no_semihosting)
struct __FILE
{
int a;
};
FILE __stdout;
void _sys_exit(int x)
{
}
/*****************************************************
*function: 写字符文件函数
*param1: 输出的字符
*param2: 文件指针
*return: 输出字符的ASCII码
******************************************************/
int fputc(int ch, FILE *f)
{
while(USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET); //等待上次发送结束
USART_SendData(USART1, (unsigned char)ch); //发送数据到串口
return ch;
}
嵌入式开发与此不同。嵌入式设备在硬件和软件方面有特定的需求与限制。因此,在嵌入式开发中不能直接采用web的日志框架。这类设备通常资源有限,功能较为单一,比如工业控制领域的嵌入式设备。在这种背景下,开发者必须自行构建日志系统,从基础的日志输出设计到应对各种复杂情况,比如不同存储介质下的日志存储问题,都需要开发者亲自深入研究和解决。
嵌入式常见的日志输出方式
#ifdef ENBALE_DEBUG
#define DBG_ERROR(fmt,args...) printf("[%s,%s,%d] ERROR:"#fmt"rn", __FILE__,__FUNCTION__,__LINE__,##args)
#define DBG_WARN(fmt,args...) printf("[%s,%s,%d] WARN:"#fmt"rn", __FILE__,__FUNCTION__,__LINE__,##args)
#define DBG_DEBUG(fmt,args...) printf("[%s,%s,%d] DEBUG:"#fmt"rn", __FILE__,__FUNCTION__,__LINE__,##args)
#elif ENBALE_WARN
#define DBG_ERROR(fmt,args...) printf("[%s,%s,%d] ERROR:"#fmt"rn", __FILE__,__FUNCTION__,__LINE__,##args)
#define DBG_WARN(fmt,args...) printf("[%s,%s,%d] WARN:"#fmt"rn", __FILE__,__FUNCTION__,__LINE__,##args)
#define DBG_DEBUG(fmt,args...)
#elif ENBALE_ERROR
#define DBG_ERROR(fmt,args...) printf("[%s,%s,%d] ERROR:"#fmt"rn", __FILE__,__FUNCTION__,__LINE__,##args)
#define DBG_WARN(fmt,args...)
#define DBG_DEBUG(fmt,args...)
#else
#define DBG_ERROR(fmt,args...)
#define DBG_WARN(fmt,args...)
#define DBG_DEBUG(fmt,args...)
#endif
嵌入式设备在日志输出方面有多种常见形式。首先便是串口输出,这种在嵌入式开发早期调试阶段极为便利。它对系统配置的要求不高,程序逻辑也相对简单。操作起来只需修改putc函数,并避免使用半主机模式。许多初学者甚至能通过网络找到相应的代码示例。
此外,文件处理方式也很关键。特别在设备初期开发调试完毕后,若要分析设备过往运行情况,排查潜在问题,文件处理方式更为适宜。文件能存储大量日志信息,便于随时查阅不同时期的设备状态。例如,在部分嵌入式医疗设备中,要检查数月甚至数年间的设备运行是否稳定,有无异常数据,文件日志管理就显得尤为重要。
串口打印的优化历程
起初的串口打印程序比较基础简略。程序直接展示大量日志数据,缺乏合理的分类和层次管理,使得输出信息显得杂乱无序。当需要寻找特定问题或查看特定类型的日志时,往往难以在这些纷繁的信息中迅速找到目标。
#include
#include
const unsigned short debugLevel __attribute__((section("ConfigSector"))) = 0; //指定到配置扇区
enum LogLevel
{
ERROR_FILTER = 1,
WARN_FILTER = 2,
DEBUG_FILTER = 3,
};
#define OUPUT_ERROR (debugLevel>= ERROR_FILTER)
#define OUPUT_WARN (debugLevel>= WARN_FILTER)
#define OUPUT_DEBUG (debugLevel>= DEBUG_FILTER)
//strrchr(__FILE__, '\')去除目录路径,只保留文件
#define DBG_ERROR(fmt,args...) error_core(strrchr(__FILE__, '\'),__FUNCTION__,__LINE__,fmt,##args)
#define DBG_WARN(fmt,args...) warn_core(strrchr(__FILE__, '\'),__FUNCTION__,__LINE__,fmt,##args)
#define DBG_DEBUG(fmt,args...) debug_core(strrchr(__FILE__, '\'),__FUNCTION__,__LINE__,fmt,##args)
void change_debug_level(enum LogLevel level)
{
unsigned short temp = level; //枚举sizeof会按子值优化
FLASH_Write(&temp,&debugLevel ,1); //flash写入一个半字两字节至debuglevel的位置
}
void error_core(const char* filename,const char* func, int line,const char* fmt, ...)
{
if(OUPUT_ERROR)
{
va_list valist;/*可变参数的宏*/
printf("[%s,%s,%d] ERROR:",filename,func,line);
va_start(valist,fmt);
vprintf(fmt,valist);
va_end(valist);
printf("rn");
}
}
void debug_core(const char* filename,const char* func, int line,const char* fmt, ...)
{
if(OUPUT_DEBUG)
{
va_list valist;/*可变参数的宏*/
printf("[%s,%s,%d] DEBUG:",filename,func,line);
va_start(valist,fmt);
vprintf(fmt,valist);
va_end(valist);
printf("rn");
}
}
void warn_core(const char* filename,const char* func, int line,const char* fmt, ...)
{
if(OUPUT_WARN)
{
va_list valist;
printf("[%s,%s,%d] WARN:",filename,func,line);
va_start(valist,fmt);
vprintf(fmt,valist);
va_end(valist);
printf("rn");
}
}
后来,我们改进了日志的编码方法,采用了层级式的日志结构。这种方法借鉴了Linux内核的设计,对日志信息进行了细致的分级。不过,在编译过程中,必须正确设置编译选项。但这个方法也有不足之处,比如在调整调试级别时,必须重新编译并更新软件。在嵌入式设备的调试环境中,设备一旦出现异常,即便更新了固件,故障往往难以再次出现。例如,那些在野外工作的物联网传感器,想要再次找到问题点就特别困难。
进一步优化,我们可以在程序运行时调整日志的级别。这通过在闪存中设置调试级别,并使用封装的函数来记录日志来完成。不过,使用时还需留意一些细节,比如,当芯片采用哈弗架构时,全局常量通常由编译器默认分配到闪存。在使用片内闪存时,需确保存储调试级别的闪存区域与代码区不重叠,否则设备可能会出现死机现象。这在设计紧凑的嵌入式设备中尤其重要,一旦出现此类错误,后果可能非常严重。
日志文件存储需要注意的要点
Config_ROM2 0x08010800 FIXED 0x0000800 { ;指定根区,即load address = execution address,
.ANY (ConfigSector)
}
日志文件在嵌入式设备中存放需遵循特定规范。串口主要用于即时信息展示,因此,它在处理长期故障检测和记录历史数据方面显得尤为重要。若使用fatfs管理文件日志,需关注代码逻辑的调整。比如,需将串口输出的日志转换成文件输出的格式。
在文件管理过程中,若能融入RTC模块将更佳。RTC模块能协助对文件进行切割管理,从而让日志文件在时间序列上更有序。但需留意,文件写入过程耗时较长,实际操作时宜独立启动日志进程。该进程专司调用相应程序执行日志文件的写入,其余任务进程则通过消息队列将日志信息发送至该日志进程。这在那些对日志实时性要求不高、却对系统运行效率有较高要求的嵌入式设备中尤为适用。
嵌入式日志的综合考量
开发嵌入式设备时,日志输出的形式和存储方式都需开发者全面考量。这包括设备的使用环境,如是否在野外长时间无人看管或室内短期测试,以及硬件资源状况,如内存和存储空间大小。同时,设备的稳定性需求也会影响日志功能的最终形态。因此,在开发初期就要对日志系统进行规划,确保其在整个设备生命周期中持续有效,以便设备正常运行,并在出现问题时能迅速定位和解决。关于优化嵌入式日志,您有何高见?欢迎分享您的想法,并点赞及转发本文。
#include
#include
const unsigned short debugLevel __attribute__((section("ConfigSector"))) = 0; //指定到配置扇区
enum LogLevel
{
ERROR_FILTER = 1,
WARN_FILTER = 2,
DEBUG_FILTER = 3,
};
u8 outputFile[30];
#define OUPUT_ERROR (debugLevel>= ERROR_FILTER)
#define OUPUT_WARN (debugLevel>= WARN_FILTER)
#define OUPUT_DEBUG (debugLevel>= DEBUG_FILTER)
void change_debug_level(enum LogLevel level)
{
unsigned short temp = level; //枚举sizeof会按子值优化
FLASH_Write(&temp,&debugLevel ,1); //flash写入一个半字两字节至debuglevel的位置
}
void error_core(const char* filename,const char* func, int line,const char* fmt, va_list valist)
{
if(OUPUT_ERROR)
{
FIL fsrc;
time_t time =rtc_get_time(); //按自己的系统获取时间
sprintf(&outputFile[0], "0:/log_d-d-d.txt",
time.Year, time.Month, time.Day);
if(f_open( &fsrc , outputFile,FA_READ|FA_WRITE|FA_OPEN_APPEND) == FR_OK )
{
f_printf(&fsrc,"[d-d-d d:d:d] [%s,%s,%d] ERROR:",time.Year, time.Month, time.Day,time.Hour, time.Minute, time.Second,filename,func,line);
f_vprintf(&fsrc,fmt,valist);//需要自己实现
f_printf(&fsrc,"rn");
f_close(&fsrc);
}
}
}
void debug_core(const char* filename,const char* func, int line,const char* fmt, va_list valist)
{
if(OUPUT_DEBUG)
{
FIL fsrc;
time_t time =rtc_get_time(); //按自己的系统获取时间
sprintf(&outputFile[0], "0:/log_d-d-d.txt",
time.Year, time.Month, time.Day);
if(f_open( &fsrc , outputFile,FA_READ|FA_WRITE|FA_OPEN_APPEND) == FR_OK )
{
f_printf(&fsrc,"[d-d-d d:d:d] [%s,%s,%d] DEBUG:", time.Year, time.Month, time.Day,time.Hour, time.Minute, time.Second,filename,func,line);
f_vprintf(&fsrc,fmt,valist);//需要自己实现
f_printf(&fsrc,"rn");
f_close(&fsrc);
}
}
}
void warn_core(const char* filename,const char* func, int line,const char* fmt, va_list valist)
{
if(OUPUT_WARN)
{
FIL fsrc;
time_t time =rtc_get_time(); //按自己的系统获取时间
sprintf(&outputFile[0], "0:/log_d-d-d.txt",
time.Year, time.Month, time.Day);
if(f_open( &fsrc , outputFile,FA_READ|FA_WRITE|FA_OPEN_APPEND) == FR_OK )
{
f_printf(&fsrc,"[d-d-d d:d:d] [%s,%s,%d] WARN:", time.Year, time.Month, time.Day,time.Hour, time.Minute, time.Second,filename,func,line);
f_vprintf(&fsrc,fmt,valist);//需要自己实现
f_printf(&fsrc,"rn");
f_close(&fsrc);
}
}
}