打印
[资源共享]

单片机中的日志系统是如何实现的?

[复制链接]
1511|6
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
一、需求和概要
实际项目中经常需要用到通过串口打印一些信息或保存日志信息到文件,从而知道程序的运行状态,这些日志信息有助于故障分析。此日志系统就是对此需求做的一个解决方法。
此日志系统实现了类似printf()的函数用法的两个函数xLogInit() 和xLog() ,在中断中需要打印信息的地方调用函数xLogInit(),在其他任何非中断的地方可以调用xLog()打印需要的信息,这两个函数分别将信息打印到数组和内存的链表中,然后在一个任务中将数组数据和内存的链表中的数据通过串口打印出来,此时随便将这两个数据拷贝到文件的缓冲区,最后在另一个任务将文件缓冲区的数据保存到文件。
二、日志系统函数接口和数据说明
主要有3个函数,分别如下:

  • void xLogInit(void); //日志初始化函数
  • int xLogInt(const char *fmt, ...);//用法同Printf函数,用于中断中打印信息到数组
  • int xLog(const char *fmt, ...); //用法同Printf函数,用于各个任务中打印信息到动态内存。中断中禁止使用此函数,否则会出现错误。


日志系统等级函数
日志系统等级函数主要由xLogInt()和xLog()函数通过宏定义实现,最终用户操作调用的也就是日志系统等级函数,例如:INFO_LOG(("SystemInit/r/n")); 尽量不要直接使用xLogInt()和xLog()函数,日志系统等级函数如下:

  • #define ERROR_LOG(x) if(eLogLevel & _ERROR){xLog x;}else
  • #define WARNING_LOG(x) if(eLogLevel & _WARNING){xLog x;}else
  • #define INFO_LOG(x) if(eLogLevel & _INFO){xLog x;}else
  • #define TRACE_LOG(x) if(eLogLevel & _TRACE){xLog x;}else
  • #define debug_LOG(x) if(eLogLevel & _DEBUG){xLog x;}else
  • #define ERROR_LOG_INT(x) if(eLogLevel & _ERROR){xLogInt x;}else
  • #define WARNING_LOG_INT(x) if(eLogLevel & _WARNING){xLogInt x;}else
  • #define INFO_LOG_INT(x) if(eLogLevel & _INFO){xLogInt x;}else
  • #define TRACE_LOG_INT(x) if(eLogLevel & _TRACE){xLogInt x;}else
  • #define DEBUG_LOG_INT(x) if(eLogLevel & _DEBUG){xLogInt x;}else


日志等级设定和输出设定宏定义

  • #define DE_SetLogLevelInt(x) eLogLevel = x
  • #define DE_ToggleLogLevelInt(x) eLogLevel ^= x
  • #define DE_Set**utint(x) eOutDevice = x
  • #define DE_Toggle**utInt(x) eOutDevice ^= x


日志系统数据结构和数据类型

  • enum LogLevelType{//日志等级枚举类型
  • _ERROR= (unsigned int)0x0001,
  • _WARNING= (unsigned int)0x0002,
  • Set_WARNING= (unsigned int)0x0003,
  • _INFO= (unsigned int)0x0004,
  • Set_INFO= (unsigned int)0x0007,
  • _TRACE= (unsigned int)0x0008,
  • Set_TRACE= (unsigned int)0x000f,
  • _DEBUG= (unsigned int)0x0010,
  • Set_DEBUG= (unsigned int)0x001f
  • };



  • enum OutDeviceType{//日志输出到哪个设备枚举类型
  • _NoOut= (unsigned int)0x00,
  • _ToSD= (unsigned int)0x01,
  • _ToUart= (unsigned int)0x02,
  • _ToFileAndSD= (unsigned int)0x03
  • };



  • struct BufferAccessType{//访问日志缓冲区二维数组的结构体类型
  • unsigned char (* pucBuffer)[DE_BufferWide];//访问DE_BufferWide宽的二维数组指针
  • unsigned short uiWriteIndex;
  • unsigned short uiReadIndex;
  • };



  • struct LogNode{//日志节点类型,用于创建日志链表
  • struct LogNode * pNext;//指向下一个节点
  • unsigned short usLength;//当前节点长度
  • unsigned char ucLevelFlag;//节点信息等级
  • char cStr[1];//指向节点信息,真正的日志信息存在这个地址开始的位置
  • };



  • static unsigned char ucIntLogBuffer[DE_BufferNum][DE_BufferWide] = {0};//用于存储中断里面打印的信息
  • struct BufferAccessType stLogBufAccess;//用于访问日志缓冲区二维数组的结构体类型
  • enum LogLevelType eLogLevel = _INFO;//用于设定日志等级
  • enum OutDeviceType eOutDevice = _ToFileAndSD;//用于设定日志等级
  • struct LogNode *pstStartNode,*pstEndNode;//用于访问日志链表
  • osSemaphoreId FifoChainBinarySemHandle;//用于安全访问日志链表的信号量


三、函数实现原理

  • void xLogInit(void);//日志初始化函数


此函数用于对访问存储日志数组和链表的数据进行初始化,并初始化日志等级和输出设备。具体实现如下:

  • void xLogInit(void){
  • eLogLevel = Set_INFO;
  • eOutDevice = _ToFileAndSD;
  • stLogBufAccess.pucBuffer = ucIntLogBuffer;
  • stLogBufAccess.uiWriteIndex = 0;
  • stLogBufAccess.uiReadIndex = 0;
  • pstStartNode = 0;
  • pstEndNode = 0;}




  • int xLogInt(const char *fmt, ...);//用法同Printf函数,用于中断中打印信息到数组


此函数用于在中断中打印一些信息,用法同printf()函数。
实现原理是:
1.定义一个二维数组

  • static unsigned char ucIntLogBuffer[DE_BufferNum][DE_BufferWide] = {0};//用于存储中断里面打印的信息


2.定义一个访问二维数组类型的变量

  • struct BufferAccessType stLogBufAccess;//用于访问日志缓冲区二维数组的结构体类型


3.调用xLogInit();函数初始化stLogBufAccess变量

  • stLogBufAccess.pucBuffer = ucIntLogBuffer;
  • stLogBufAccess.uiWriteIndex = 0;
  • stLogBufAccess.uiReadIndex = 0;


4.调用此函数时,通过标准C语言库函数vsnprintf()将参数日志信息打印到数组,但在这之前通过对变量stLogBufAccess的访问和逻辑判断,确定接下来要打印的日志信息位于数组的哪一行。变量stLogBufAccess主要用于控制将二维数组当着一个队列来访问。

二维数组

stLogBufAccess.uiWriteIndex 用于指示将要把日志写到哪一行;
stLogBufAccess.uiReadIndex 用于指示将要从哪一行读取日志信息;
stLogBufAccess.pucBuffer 用于访问数组中的具体哪一个元素。
通过对变量stLogBufAccess的控制,将二维数组当成一个环形数组来访问,最后大概是下面这样的情况:

环形二维数组

具体实现代码如下:

  • int xLogInt(const char *fmt, ...){
  • unsigned int uiTmpWriteIndex;
  • int iNum;
  • va_list ap;
  • va_start(ap,fmt);
  • static char i = 0x21;
  • i = (++i == 0x7d) ? 0x21 : i;
  • uiTmpWriteIndex = stLogBufAccess.uiWriteIndex;//临界区需多次读取
  • if(((uiTmpWriteIndex + 1) & (DE_BufferNum - 1)) == stLogBufAccess.uiReadIndex){
  • va_end(ap);
  • return -1;//如果缓存满了就返回 -1
  • }else{
  • stLogBufAccess.pucBuffer[uiTmpWriteIndex][1] = 0;//字符数清零
  • stLogBufAccess.uiWriteIndex = (uiTmpWriteIndex + 1) & (DE_BufferNum - 1);//更新临界区需加保护
  • stLogBufAccess.pucBuffer[uiTmpWriteIndex][0] = i;//将时间加到log缓冲区
  • iNum = vsnprintf((char *)&stLogBufAccess.pucBuffer[uiTmpWriteIndex][2],DE_BufferWide-3,fmt,ap);
  • if((iNum >= 0) && (iNum < DE_BufferWide-3)){
  • ;
  • }else if(iNum >= DE_BufferWide-3){
  • iNum = DE_BufferWide-3;
  • }else{
  • return iNum;//vsnprintf()调用错误
  • }
  • stLogBufAccess.pucBuffer[uiTmpWriteIndex][1] = iNum + 2;//字符数
  • va_end(ap);
  • return 0;
  • }
  • }


其中,
数组的第一个元素stLogBufAccess.pucBuffer[uiTmpWriteIndex][0]保存的是时间标号;
数组的第二个元素stLogBufAccess.pucBuffer[uiTmpWriteIndex][1]保存的是实际日志信息长度加2;
从数组第三个元素stLogBufAccess.pucBuffer[uiTmpWriteIndex][2]开始真正保存有效日志信息。

  • int xLog(const char *fmt, ...);//用法同Printf函数,用于各个任务中打印信息到动态内存。
  • 中断中禁止使用此函数,否则会出现错误。


此函数用于其他任务中调用,打印一些日志信息到链表中,用法同printf()函数。
实现原理是:
1.定义一个日志链表类型,用于存储单条日志

  • struct LogNode{//日志节点类型,用于创建日志链表
  • struct LogNode * pNext;//指向下一个节点
  • unsigned short usLength;//当前节点长度
  • unsigned char ucLevelFlag;//节点信息等级
  • char cStr[1];//指向节点信息,真正的日志信息存在这个地址开始的位置
  • };


2.定义两个日志链表指针,用于访问动态内存中的日志信息,定义一个信号量,用于安全访问链表

  • struct LogNode *pstStartNode,*pstEndNode;//用于访问日志链表
  • osSemaphoreId FifoChainBinarySemHandle;//用于安全访问日志链表的信号量


3.调用xLogInit();函数初始化日志链表指针变量

  • pstStartNode = 0;
  • pstEndNode = 0;


4.调用此函数时,通过标准C语言库函数vsnprintf()将参数日志信息打印到动态申请的内存中,但在这之前需要动态申请需要的内存,确定接下来要打印的日志信息位于内存的何处。这里有个要注意的地方就是,由于这个函数是要被多个其他任务调用,所以不能将动态内存申请完了,要留有一定的动态内存给其他任务使用,所以要在动态申请内存前加一个判断,确保其他程序有足够的动态内存。变量pstEndNode 主要用于新加入日志节点,节点加入链表后需要更新变量pstEndNode 指向新加入的最后一个节点,变量pstStartNode 主要用于读出日志节点,读出节点后需要更新变量pstStartNode 指向下一个需要读出的日志节点。
具体实现代码如下:

  • int xLog(const char *fmt, ...){
  • va_list ap;
  • va_start(ap,fmt);
  • char * p1 = 0;
  • struct LogNode * p2;
  • int i=0;
  • unsigned short usFreeHeapSize;
  • usFreeHeapSize = xPortGetFreeHeapSize();
  • if(((usLostNumber == 0) && (usFreeHeapSize>10240)) || (usLostNumber && (usFreeHeapSize>(10240+1024)))){
  • if(usLostNumber){
  • INFO_LOG(("%d",usLostNumber));
  • usLostNumber = 0;
  • }
  • p1 = (char *)pvPortMalloc(128);
  • if(p1 == 0){
  • va_end(ap);
  • //printf("pvPortMalloc\r\n");
  • return -2;
  • }
  • i = vsnprintf(p1,127,fmt,ap);
  • va_end(ap);
  • if((i >= 0) && (i < 127)){
  • ;
  • }else if(i >= 127){
  • i = 127;
  • }else{
  • return i;//vsnprintf()调用错误
  • }
  • if(i>=0){
  • p2 = (struct LogNode*)pvPortMalloc(i+sizeof(struct LogNode)-1);
  • if(p2 == 0){
  • vPortFree(p1);
  • return -2;
  • }
  • p2->pNext = 0;//这3行是新节点内容填充,包含下一个节点地址、
  • p2->usLength = i;//此节点字符串长度、
  • strcpy(p2->cStr,p1);//此节点字符串
  • vPortFree(p1);
  • //节点加入到链表
  • if((xSemaphoreTake(FifoChainBinarySemHandle,portMAX_DELAY)) == pdTRUE){
  • if(pstStartNode){//已有节点(非空链表)
  • pstEndNode->pNext = p2;//最后一个节点指向新的节点
  • pstEndNode = p2;//更新最后一个节点
  • }else{//没有节点(空链表),加入新节点
  • pstEndNode = p2;
  • pstStartNode = p2;
  • }
  • usListSize += p2->usLength+sizeof(struct LogNode)-1;
  • xSemaphoreGive(FifoChainBinarySemHandle);
  • }
  • return 0;
  • }else{
  • vPortFree(p1);
  • return -1;
  • }
  • }
  • else{
  • usLostNumber++;
  • return -3;
  • }
  • }






使用特权

评论回复
沙发
tpgf| | 2022-6-3 20:44 | 只看该作者
日志文件的作用是什么呢

使用特权

评论回复
板凳
drer| | 2022-6-3 21:20 | 只看该作者
日志文件是自动生成的是吗

使用特权

评论回复
地板
qcliu| | 2022-6-3 21:32 | 只看该作者
有助于故障分析

使用特权

评论回复
5
coshi| | 2022-6-3 21:45 | 只看该作者
日志文件会不会越来越大

使用特权

评论回复
6
kxsi| | 2022-6-3 21:45 | 只看该作者
可以禁止自动生成日志文件吗

使用特权

评论回复
7
wiba| | 2022-6-3 22:00 | 只看该作者
日志文件可以用什么工具查看啊

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

57

主题

344

帖子

0

粉丝