良许Linux教程网 干货合集 嵌入式软件中如何管理系统数据?

嵌入式软件中如何管理系统数据?

对软件架构的认识有很多不同的观点和争议。在我的观点中,软件架构是指软件系统的基本结构,包括组件、组件之间的关系、组件的设计和演进规则,以及支持这些规则的基础设施。

然而,软件架构并不是一件容易的事情。它贯穿整个软件产品的生命周期,需要所有团队成员的遵守和自律,才能在软件中体现出优秀的架构思想。

对于新手工程师来说,由于项目经验较少,很难从整体上理解软件架构。但是,软件架构并不只是资深工程师的专利。就像古人写作一样,重要的是立意。

当今的工程师在进行项目和产品开发时,也应该先有一个高度的立意。这个立意就是从软件架构的高度出发,看待软件问题,并相信这样可以更深入地理解软件。因此,我总结了嵌入式工程师可以参考的软件架构的六个步骤:

  • 隔离与硬件相关的代码,建立抽象层
  • 建立统一的软件基础设施
  • 识别和管理系统中的数据
  • 进行功能的分层与分解
  • 设计组件及其接口
  • 进行测试、调试和跨平台开发的支持

今天,我们将讨论嵌入式软件架构设计的第三个步骤:识别和管理产品数据。在我的工作中,我发现嵌入式工程师们在思考架构问题时存在两种倾向:

首先,大部分工程师的思考起点是硬件。嵌入式工程师们往往会将整个嵌入式程序变成与硬件交互的底层代码,缺乏对硬件的有效隔离和合理的分层,甚至直接从应用层到寄存器层次进行开发。

《嵌入式软件架构的六个步骤(一)抽象层》中,我们对这一现象做过解释。这种做法,是由大部分嵌入式工程师的知识结构偏硬件决定的,可以理解,但绝不提倡。

因为这种写法,不符合软件发展的主流趋势,不符合层次化和复用性原则,无法支撑大规模的嵌入式软件开发。目前嵌入式开发产品的软件规模大了很多,硬件有关的部分所占的比例,已经降到了非常小的份额。在大多数嵌入式系统中,真正的价值在于应用层代码,即与硬件无关的部分。

image-20230829234534818
image-20230829234534818

一竿子捅到底的架构

其次,工程师总是喜欢围绕中断、任务、总线的原始数据等底层资源,展开对架构的思考。

这依然是面向硬件和面向底层的思维。在硬件资源紧张的年代里(15年前),任务的数量是有限制的(uC/OS-II只支持64个任务,而当前大多支持无限任务,只要RAM和ROM允许),而RAM也是非常紧张的,那时候的工程师,不得不将大量的精力,放在底层上,以便以最小的资源进行数据的处理。

而现在,当资源变得不那么稀缺甚至有些过剩时,我们要面向业务逻辑进行数据结构的设计。而业务逻辑体现到软件里,本质上就是数据抽象与数据结构的设计,其次才是程序的编写。

一旦团队完成了软件架构的第一步和第二步,对硬件相关代码进行了剥离,并建立了统一的软件基础设施(非必须步骤),设计嵌入式软件架构的第三步就是识别和管理产品数据。

数据包含任何类型,只要是系统内部,又利用其功能执行的任何数据,包括一些中间数据和临时数据,都算系统数据。拿我曾经做过的机器人控制器举例,嵌入式系统可能具有以下系统数据:

  • • IO口数据(含开关量输入口、开关量输出口、模拟量输入口、模拟量输入口)
  • • 通信口数据(含串口、CAN接口、RS485等)
  • • 传感器实时数据
  • • 电机实时数据
  • • 车体状态数据
  • • 地图数据
  • • 任务数据
  • • 指令数据
  • • 当前线路数据
  • • 当前位置数据
  • • 错误与报警历史
  • • LOG数据
  • • 配置参数
  • • 脚本数据
  • • 其他数据

这里面,有些数据,为一些功能独享,但也有很多的数据,由不同的器件产生,并被多个功能所共享。系统中数据越多,数据类型越多,数据共享越多时,系统架构就越复杂。当我们设计和构建一个实时嵌入式系统时,我们所做的核心是识别和管理数据。

嵌入式软件设计的第一原则

数据决定设计,是现代嵌入式软件设计的第一原则。Linux的创始人Linus Torvalds在一次演讲中也曾经说过说:”烂程序员关心的是代码,好程序员关心的是数据结构和它们之间的关系。”他在谈到Git时,也曾表达过类似的观点,“好程序员和烂程序员之间的差别,就在于他们认为是代码更重要还是数据结构更重要。”

我们上学时,就学过一个公式:程序 = 数据结构 + 算法。如果从嵌入式程序的宏观视角去理解这个公式:所谓的数据结构,就是数据结构与处理机制;而所谓的算法,就是代码逻辑。好的数据结构,总是会简化代码;而差的数据结构,会导致代码变的比较复杂。

我们可以创建各种漂亮的架构,去开发项目,完成产品。但最有效的架构是围绕系统数据设计的架构。一个只有10个数据和1000个数据的处理方式,是完全不同的。无论工程师琢磨出多么漂亮和优雅的软件架构,只要这个架构不是有效的支撑数据的处理,都是无助于实际开发的。

当我们专注于数据进行架构设计时,需要工程师对数据本身以及数据的转换进行高度关注,关注到数据在软件内部转换的每一个关键环节。实际上,每一个软件(含承载它的硬件),都可以看做一个黑盒子,一端是输入数据,一端是输出数据,而黑盒子是对数据的处理和转换。

比如,我们开发一个恒温壶的项目,其原理大致为从一个热敏电阻获取温度,并经过ADC的采集、转换、滤波等操作,根据采集的温度,决定加热丝是否加热,以保持温度维持在恒定问题。原始温度数据,可以看做输入,加热丝的打开,可以看做输出。

架构可以变得高度关注它应该如何处理数据。事实上,数据的处理仅仅需要少量操作。首先,系统可以输入数据。例如,用户可以通过通信接口按下按钮或接收串行数据。其次,系统可以输出数据。例如,显示像素映射到显示器或驱动电机。第三,系统可以处理数据。

例如,串行数据可能以数据包格式进入系统,然后对其进行解码。进行处理以验证数据包,然后解压缩存储的数据。最后,系统可以将数据存储在易失性或非易失性存储器中。众所周知,在Linux和Unix系统中,五个操作就已经抽象所有的关于文件(可以认为数据的一种):

  • • open
  • • close
  • • read
  • • write
  • • control

识别系统数据,以及可对该数据执行的操作,可以极大地帮助团队设计其嵌入式软件体系结构。分析系统数据,能够很简单的明确设计中的架构需求。不幸的是,太多的团队忽略了数据,要不就是凭感觉和经验在编程,要么就是强行引入了并不适合但光鲜亮丽的架构方案。

在很多时候,合适的架构,都是朴素的。朴素到什么地步呢?当架构对软件质量提升没有帮助时,那就去他的架构!这就是为什么8位单片机和简单的嵌入式产品,不必讲究什么架构,就能完成设计和实现。

但当软件规模增大(个人观点是一万行代码以上)时,引入合理架构就变成了不得不做的事情,这时对系统的核心数据进行识别和分析,就势在必行。

那么什么是系统的核心数据?很多工程师在应用层里直接处理物理通信口(比如UART)来的数据,这依然是硬件思维。资源(RAM)紧张时,我们当然需要在底层,甚至在中断函数中,直接解析数据,以便节省资源。但从系统架构的角度来说,显然物理中断接收到原始数据,与业务层面,并没有直接关系。

在RAM资源允许的情况下,物理中断要经过几次转换,才会到应用层,这几次转换可能包含:外设框架缓冲区、协议栈、设备层、应用层。对于复杂系统,经过的环节可能会更多。

举例来说,我们假定主控制器使用UART(RS232)通过一个电机驱动器,间接控制一个电机,并获取电机实时数据,这在工业控制和机器人等行业是非常常见的场景。外设框架缓冲区,暂存收到的串口数据,然后协议栈解析数据,将有效的数据负载传递给应用层,应用层对数据进行合理转换(滤波、计算等),最后才得到电机的速度、电流、温度等数据。

typedef struct motor_status_data
{
    float speed;                            /* RPM */
    float current;                          /* A */
    float temperature;                      /* °C */
} motor_status_data_t;

void device_motor_get_status(device_motor_t *device, motor_status_data_t *data);

上述代码里,所表达的才是电机这个设备,在系统中的核心数据。它代表电机设备的核心特性。至于这个电机是串口驱动、SPI驱动还是CAN驱动,对应用层来说,并不重要,我们也不应该去关心他,我们将其屏蔽在设备层以下。在设备层里,我们只保留硬件无关的数据。这部分内容,我将在后续文章《设备抽象层》系列中进行详述。

以数据为中心的架构意味着什么?

对很多喜欢搞底层的工程师来说,“数据决定架构”的想法,显得很奇怪。在各种编程领域,面向对象的编程都是被重视的,因为它能将复杂的系统简化。而对象是什么?

本质上,对象就是各种彼此相关数据的集合,以及对该数据进行操作。而面向对象的编程理念已经出现几十年了,有很多人已经在讨论更加先进的编程范式了,但单片机上,面向对象还并未普及。实际上,以数据为核心的软件架构设计,在很多领域不仅是必要,而且是强制的。

在机器人领域,如果不使用数据驱动的架构设计原则,采集数据并脱机复现场景,是不可能实现的。以数据为中心的架构,有如下优势。

首先,可以完美的解决数据安全问题。不同类型的数据有着不同的安全等级。如果能以数据为中心进行软件架构,那么软件架构会和系统安全,完美融合。

工程师可以对数据的安全等级进行设置,不同的安全等级有着不同的保护措施,后续会在《防御式编程》系列文章中详细阐述这个问题。

其次,识别系统数据,可以帮助我们正确将系统拆分到模块这个粒度上。模块(组件),是工程师进行编程工作的最小任务。作为工程师,如果能严格遵循单一职责原则(SRP),每一组数据都会被单独封装在其模块内部,并执行相应的操作。如果模块切分的太粗,会导致很多无关的数据都放在了同一个模块进行处理。

在这种情况下,不符合模块设计的“单一职责原则”,从而导致内聚性差,复用性低。如果模块切分的太细,会导致关联密切的数据,被分到多个不同的模块进行处理。在这种情况下,模块之间会产生数据交换,从而导致耦合性太强,复用性也会降低。

第三,以数据为中心,进行系统架构,就意味着工程师是面对应用层进行系统和软件架构,而不是面向底层。在架构阶段上,工程师所关注到东西,是一组数据及其操作,如何达到简洁的状态,从来都不会关注获取这个数据,所需要的硬件资源是什么?一般而言,硬件机制、中断、缓冲区和DMA等底层细节,都会被直接屏蔽在驱动层以内(参考《嵌入式软件架构的六个步骤之抽象层》)。

那么,围绕数据进行的嵌入式软件架构的原则是什么?这就不得不提到老掉牙的一句话,那就是著名的“高内聚,低耦合”原则。

何为高内聚?将彼此联系紧密的数据,放进同一个模块进行处理;何为低耦合?让模块间的数据交互,尽可能的小。实际上,一旦一个软件模块符合了上述标准,其接口必然变得简洁。简洁的接口,才更有可能是合理的。

对于上述的AGV系统数据来说,我们可以大体分为如下几个域:

  1. \1. 外设与驱动:IO口数据、通信口数据 2. 设备与驱动:传感器实时数据、电机实时数据 3. 运动控制:车体状态数据 4. 业务逻辑:地图数据、任务数据、指令数据、当前线路数据、当前位置数据、错误与报警历史、LOG数据 5. 配置层:配置参数、脚本数据等

每一个域中,可能还会分为几个软件模块,软件模块包含自己的数据。具体的层次与模块分解,我们将在下一步骤中进行详述。

思维稍微脱离底层的工程师,可能会以任务作为架构设计的核心要素。对于多年之前的嵌入式软件设计,这是合适的。对从前资源紧张的MCU而言,任务是一个稀缺资源,每开一个任务,就意味着要耗费不少RAM的开销,这对于RAM资源紧张的MCU来说,不得不从全局上,由架构师小心规划任务的创建。

但在现在的嵌入式系统中,任务也成为了模块中的资源。面向过程式的开发,一般在任务里调用模块。而在合理的架构设计,要不要在模块中启动任务,一般在对模块进行实现的时候,才由负责模块实现的工程师决定。在项目开始时,合格的工程师,想好所有的细节;而优秀的工程师,则会预留最大的空间,推迟细节的决策,提高软件架构的灵活性。

结论

设计嵌入式软件架构的第三步是识别和管理系统数据。对于关注硬件的工程师来说,以数据为中心的软件架构,似乎很奇怪。帮助改变我们思维方式的一种方法是将嵌入式软件的定义修改为:“嵌入式软件,是设计和构建为确定性运行的代码,通畅具备实时性,通过各种形式的输入、处理、输出和存储来管理数据。”

数据体现的是嵌入式系统的本质,体现的是嵌入式系统的抽象特性,只有以数据为核心的嵌入式架构,才可能诞生最合理的软件架构。

识别数据,然后跟踪它如何与系统中的其他数据交互,可以帮助工程师了解架构是如何出现的。当工程师从数据的视角看待嵌入式系统时,一个抽象而非具象的视角已经形成;嵌入式系统架构的蓝图,也已经在他的脑中展开。我们就能进入嵌入式软件架构的第四个步骤,系统层次与模块分解。

以上就是良许教程网为各位朋友分享的Linu系统相关内容。想要了解更多Linux相关知识记得关注公众号“良许Linux”,或扫描下方二维码进行关注,更多干货等着你 !

137e00002230ad9f26e78-265x300
本文由 良许Linux教程网 发布,可自由转载、引用,但需署名作者且注明文章出处。如转载至微信公众号,请在文末添加作者公众号二维码。
良许

作者: 良许

良许,世界500强企业Linux开发工程师,公众号【良许Linux】的作者,全网拥有超30W粉丝。个人标签:创业者,CSDN学院讲师,副业达人,流量玩家,摄影爱好者。
上一篇
下一篇

发表评论

联系我们

联系我们

公众号:良许Linux

在线咨询: QQ交谈

邮箱: yychuyu@163.com

关注微信
微信扫一扫关注我们

微信扫一扫关注我们

关注微博
返回顶部