嵌入式日志系统设计
使用C语言面向对象在嵌入式单片机上设计一套日志系统
嵌入式日志系统设计
1. 背景
-
在单片机嵌入式系统中,往往需要进行日志记录,在生产环境中,串口debug日志往往没有办法获取,而且也需要去查询历史的日志的记录。所以在单板设计时就会考虑使用外部flash来记录日志,但是单片机中没有相应的文件系统,需要手动对日志进行管理。
-
近期我在项目中,就收到了一个需求,实现一个单板的日志系统。需求如下:
- 日志有两个类别,一个是运行日志,负责记录单板在运行时的日志;还有一种是操作日志,用来记录上位机给单板下发的指令。
- 运行日志内容为,字符串信息
- 操作日志内容是两个字节的命令
- 运行日志和操作日志分别存储在外部flash的不同日志区
- 日志要求需要有时间信息,日志等级信息。
- 日志等级分为4种,ERROR,WARNING,INFO,DEBUG
- 时间信息根据单板的RTC时钟获取
- 日志的输出通道有串口UART,网口ETH和外部Flash
- 不同的日志通道的打印日志等级可独立设置,两种日志的打印等级也需要单独设置
- 操作日志和运行日志记录非常频繁,flash空间分别为10M和20M,所以需要尽可能记录更多的日志
- 日志读取至少可以进行全量读取,和查询最近的N条记录,希望实现根据时间信息查询日志
- 记录在Flash的日志,需要在ram中实现双片区缓存,缓存满后记录到flash中
- 要求在板子跑飞复位后,能够保留记录在ram中的日志
-
其实原本单板中是有日志系统的,但是写的非常简陋,在新增的需求中,有两种日志,原本的代码只考虑一种日志的记录,而且各个日志通道等级耦合,添加和删除一个日志通道,会非常麻烦,需要改动多个地方。
-
项目使用C语言,操作系统为Lite OS,在设计过程中,考虑到扩展性和可移植性和兼容性,打算采用面向对象的的思维来设计整套体系,我之前有一篇文章就介绍了在C语言中使用面向对象的分析:OOPC C语言面向对象-CSDN博客。
-
至于,复位后保存RAM中的日志,可以考虑在RAM中划分出一块区域,单独拨给日志缓存区使用,复位后重启检查此片内存区域是否被使用,如果被使用,则恢复ram状态,如果未被使用,则直接初始化,在RAM中划分出一块区域可以参考我这一篇文章:STM32 boot和app之间的数据传递-CSDN博客。
2. 系统设计框架
-
日志记录核心框架
-
框架解析
UML中类名为斜体的为抽象类,可以看出,在此框架中,除了LogManager,都是抽象类。
由于需要尽量多的记录日志,比如操作日志,如果使用字符串的方式去记录时间,则需要二十多个字符,还需要记录操作的指令,一条日志,在25个字节以上,如果使用4字节时间戳,2字节的日志+微秒数,2字节操作指令,那么一共只需要8个字节就可以记录一套日志,那么比原先多出两倍的日志内容,但是有个问题,在日志导出的时候,还需将字节序列的日志转换为字符串给外界。
操作日志内容固定为操作指令,整条日志长度确定,但是运行日志而言,内容为不定长的字符串,如果直接写入到flash中,就会丢失一条日志结束位置,所以对于不定长日志需要使用定界符,来确定一条日志的界限,定界符值就为一条日志的长度,首位各一个字节,这样无论是从前还是从后都可以界定一条日志长度,所以就限定运行日志一条内容在246各字节以内。
下图为不定长日志字节序列的分布
这样日志就有两种形态,一个是字符串形态,还有一个是字节序列,也就是存储体。所以我将日志作为一个类来设计,他可以有正常的构造方式,提供时间,日志等级和日志内容,来构造一条日志,也可以由字节序列来构造一条日志。所以在架构图中,Message作为一个抽象类存在,运行日志和操作日志将继承Message,并且实现他的三个接口,最主要的就是转为字符串和转为字节序列这两个函数。
整个日志体系都是依赖与Message这个抽象类,在读取日志过程中,我们需要将字节序列转换为日志消息对象,这个需要动态调用构造函数,所以也就需要一个工厂来创建消息对象,于是就有了MessageFactory这个抽象类,每个具体日志类都需要由一个日志工厂。而动态创建对象是在读取日志过程中,所以LogReader组合了工厂。
中间的LogManager是唯一的非抽象类,在原本的设计中,也属于抽象类,但是Message和读取流的独立,使得LogManager的变化消失,所以就由抽象类,变为了具体类。LogManager具有输出日志和读取日志的功能,还能够配置各个日志通道的等级。
LogExporter为日志输出基类,由于日志输出的通道会有多个,所以将日志输出通道做成链表式,在LogManager中进行注册。比如注册了Uart通道和Flash通道,LogManager中的输出通道链表为 UART -> Flash,在输出日志时,就会遍历链表,先进行UART日志输出,再进行Flash日志输出。这也就是设计模式中的观察者模式。
LogReader为日志读取基类,由于日志读取是较长的过程,所以这里使用了流的设计,由于读取的策略有多种,所以再LogManager中提供一个读取策略ReadStrategy,返回一个读取流ReadStream。读取流提供read方法,来都一条日志,提供close方法来结束读取释放资源,读取策略由读取方向、读取地址和筛选规则,三个部分组成。能够通过不同的策略实现,全量读取、读取最后N条日志,筛选某个时间范围的日志,筛选某个等级的日志。
3. FLash日志读写设计
-
flash日志读写设计框架
-
设计解析
Flash模块可以作为日志的输出通道,同样也算日志的读取通道,所以FlashLogManager需要为两个读写两个接口服务,只读的情况下,直接依赖FlashIO就可以,写的情况较为复杂,日志首先是纪录到RamBuffer的缓存空间中,而RamBuffer是RamNode组成一个Buffer链表,一般是两个buffer,互为备份,当一个buffer写满时,将buffer挂在到FlushList中,释放信号量,让WriteTask捕捉到信号,拿到Buffer内容写道Flash中,这个过程是异步实现,可能会花费较多的时间,所以在释放信号量后,就直接切换Buffer。注:在原有的设计中也有双缓存区设计,但是不合理的地方在于两个buffer共用一个互斥锁,导致flash在写出时,无法再向buffer缓存中写入日志。buffer的锁应该是独立的,而不能共用一把锁。
由于要支持复位保留日志的特性,所以我修改了链接脚本,将链接脚本里的RAM长度,缩小了20K,而栈顶指针的计算方式是 RAM ORIGIN + LEN,由于LEN减小了20k,所以栈顶指针就减小了20K由于栈的生长方向由高地址向低地址,所以这一片空间就不会被使用,RAM的机制是只要不掉电,数据就不会消失,如果板子跑飞,复位后,并没有发生掉电的情况,所以数据并不会消失,这一片空间,没有其他的使用者,所以日志可以直接恢复。
4. 日志系统部署
-
消息实现
-
flash读写实现
这就是最终的部署图,FlashLogManager需要实现读写两个接口,所以就由了FlashLogReader和FlashLogExpoter,在这一张图中,并没有画出更多的读取测试,只有一个全量读取,另外本系统对互斥锁和信号量做出了了封装,所以这个日志系统适用与所有操作系统,甚至可以用在裸机上。
5. 总结
此项目日志系统使用的是C语言,并且使用的是面向对象的方式来实现,可以明确感受到面向对象的方式增强的对代码架构的组织能力,不在聚焦于某一个函数,某一个数据结构,而是以类和对象为单元进行分析,然后将一系列的类进行组合称为组件,再以组件的方式部署项目,这样就使得我们对于代码的驾驭能力提升了一个级别,面向对象后的一个好处也再可扩展性,多态的特性使得代码的扩展更加方便,还有众多的设计模式可以参考,可以加快我们对于软件架构的思考。但在开发过程中,也感受到了C语言的局限性,C语言并不是为面向对象设计的语言,为了方便我并没有对属性进行进一步的封装,使得所有的类成员都是public,这对于项目复杂后,并不利于扩展,依靠程序员自己的素养来保证不会过分访问。还有就是C语言的错误系统,并没有异常机制来处理错误,只能通过错误码来实现,这就使得,C语言类中的返回值基本都是errno_t类型,代码中充斥着if(ret != 0)这样的语句。从开发角度而言,我更希望使用C++、python和Java这样的语言来进行大型框架编程,当然从设计角度而言,并没有什么区别。
更多推荐
所有评论(0)