对于软件架构,存在着不同的看法和看法。 在我看来,软件架构是一个软件系统的基本结构,包括它的组件、组件之间的关系、组件设计和演化的规则以及体现这些规则的基础设施。
软件架构从来都不是一件容易的事。 它贯穿于产品的整个生命周期,需要所有团队成员共同遵守和自律,才能在软件中体现架构思想。
新手工程师,由于经历的项目太少,无法看到项目的全貌,很难从全局的角度理解软件架构。 但软件架构真的只是高级工程师的专利吗? 情况不一定如此。 古人在作文上,首先注重的是意念。
如今,工程师在做项目和产品时,也应该首先有一个想法。 这意味着必须有高度。 初级工程师可以从软件架构的高度出发,看待软件问题。 相信对软件的理解会更加深刻。 因此,我总结了软件架构的六步,供嵌入式工程师参考。
今天,我们将探讨设计嵌入式软件架构的第三步:识别和处理产品数据。 在工作中,我发现嵌入式工程师在思考架构问题时有两种倾向:
首先,思考问题的出发点大多是硬件。 嵌入式工程师总是倾向于将整个嵌入式程序变成一直与硬件交互的低级代码,而没有对硬件进行有效隔离,更不用说合理分层,甚至从应用层戳到寄存器。
在 中,我们解释了这种现象。 这种做法是由大多数嵌入式工程师的知识结构决定的,可以理解,但从来不提倡。
因为这种写法不符合软件开发的主流趋势,不符合层次化和复用性的原则,无法支持大规模的嵌入式软件开发。 目前嵌入式开发产品的软件规模要大得多,硬件相关部分的比例已经下降到很小的份额。 在大多数嵌入式系统中,真正的价值在于应用层代码,即与硬件无关的部分。
用一根棍子粘在底部的结构
其次,工程师总是喜欢围绕中断、任务和总线原始数据等底层资源来思考架构。
这仍然是面向硬件、底层的思维。 在硬件资源紧张的时代(15年前),任务数量是有限的(uC/OS-II只支持64个任务,但目前大多数支持无限任务,只要RAM和ROM允许),RAM也非常有限。 紧张的是,当时的工程师必须将大量的精力投入到底层,才能用最少的资源处理数据。
现在,当资源变得不再稀缺甚至有些过剩时,我们需要为业务逻辑设计数据结构。 当业务逻辑体现在软件中时,本质上是数据抽象和数据结构的设计,然后是程序的编写。
一旦团队完成了软件架构的第一步和第二步,剥离了与硬件相关的代码,并建立了统一的软件基础设施(可选步骤),设计嵌入式软件架构的第三步就是识别和管理产品数据。
数据包括任何类型,只要它在系统内部并且其功能执行的任何数据,包括一些中间数据和临时数据,都被视为系统数据。 以我曾经做过的机器人控制器为例,嵌入式系统可能有以下系统数据:
这里,有些数据是某些功能专有的,但也有很多数据是由不同设备生成并由多个功能共享的。 数据越多,数据类型越多,系统中共享的数据越多,系统架构就越复杂。 当我们设计和构建实时嵌入式系统时,我们所做的核心是识别和管理数据。
嵌入式软件设计的首要原则
数据决定设计,这是现代嵌入式软件设计的首要原则。 Linux的创始人Linus Torvalds曾在一次演讲中说过:“糟糕的程序员关心代码,优秀的程序员关心数据结构以及它们之间的关系”。 在谈到 Git 时,他也表达了类似的观点,“优秀程序员和糟糕程序员的区别在于,他们认为代码或数据结构更重要”。
我们上学的时候学过一个公式:程序=数据结构+算法。 如果从嵌入式程序的宏观角度来理解这个公式:所谓数据结构就是数据结构和处理机制; 而所谓算法就是代码逻辑。 一个好的数据结构总是会简化代码; 糟糕的数据结构会导致代码变得更加复杂。
我们可以创造各种美丽的建筑、开发项目、完成产品。 但最有效的架构是围绕系统数据设计的架构。 仅10个数据和1000个数据的处理方式完全不同。 无论软件架构工程师想出多么漂亮、优雅的架构,只要架构不能有效支持数据处理,就无助于实际开发。
当我们围绕数据进行架构设计时,工程师需要关注数据本身和数据转换,关注软件内部数据转换的每一个关键环节。 事实上,每一个软件(包括承载它的硬件)都可以看作是一个黑匣子,一端输入数据,另一端输出数据,黑匣子对数据进行处理和转换。
比如我们开发一个恒温壶项目。 其原理大致是从热敏电阻获取温度,并通过ADC采集、转换、滤波等运算,根据采集到的温度,判断电热丝是否加热以保持温度恒定的问题。 原始温度数据可视为输入,电热丝的开度可视为输出。
架构可以变得高度关注它应该如何处理数据。 事实上,数据的处理只需要很少的操作。 首先,系统可以输入数据。 例如,用户可以按下按钮或通过通信接口接收串行数据。 其次,系统可以输出数据。 例如,显示像素被映射到显示器或驱动电机。 第三,系统可以处理数据。
例如,串行数据可能以数据包格式进入系统,然后进行解码。 进行处理以验证数据包,然后解压缩存储的数据。 最后,系统可以将数据存储在易失性或非易失性存储器中。 众所周知,在Linux和Unix系统中,五个操作已经抽象出了文件的一切(可以认为是一种数据):
识别系统数据以及可以对该数据执行的操作可以极大地帮助团队设计其嵌入式软件架构。 分析系统数据可以轻松明确设计中的架构需求。 不幸的是,太多的团队忽略了数据,要么根据感觉和经验编写代码,要么强行引入不合适的闪亮架构解决方案。
在许多情况下,适当的架构很简单。 有多简单呢? 当架构无助于提高软件质量时,那就毁掉架构吧! 这就是为什么可以设计和实现 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. 外设及驱动:IO 口数据、通讯口数据 2. 设备及驱动:传感器实时数据、电机实时数据 3. 运动控制:车体状态数据 4. 业务逻辑:地图数据、任务数据、命令数据、当前线路数据、当前位置数据、错误和报警历史记录、LOG数据 5、配置层:配置参数、脚本数据等。
每个域可以分为多个软件模块,并且软件模块包含自己的数据。 具体层次和模块分解将在下一步详细介绍。
思维稍微脱离底层的工程师可能会以任务作为架构设计的核心元素。 对于多年前的嵌入式软件设计来说,这是合适的。 对于过去资源紧张的MCU来说,任务是一种稀缺资源。 每打开一个任务,就意味着会消耗大量的RAM。 对于 RAM 资源紧张的 MCU,架构师必须仔细规划任务的创建。
但在当今的嵌入式系统中,任务也成为了模块中的资源。 对于程序开发来说,一般都是在任务中调用模块。 在合理的架构设计中,是否启动模块中的任务一般是由负责模块实现的工程师在模块实现时决定的。 在项目开始时,合格的工程师会考虑所有细节; 优秀的工程师会保留最大的空间来推迟细节的决策,提高软件架构的灵活性。
综上所述
设计嵌入式软件架构的第三步是识别和管理系统数据。 对于专注于硬件的工程师来说,以数据为中心的软件架构可能看起来很奇怪。 帮助改变我们思维方式的一种方法是将嵌入式软件的定义修改为:“嵌入式软件是为了确定性地运行而设计和构建的代码,通常是实时的,通过各种形式的输入、处理、输出和存储来管理数据。”
数据体现了嵌入式系统的本质,体现了嵌入式系统的抽象特征。 只有以数据为核心的嵌入式架构,才能产生最合理的软件架构。
识别数据并跟踪其与系统中其他数据的交互方式可以帮助工程师了解架构是如何出现的。 当工程师从数据的角度看待嵌入式系统时,就形成了抽象而非具体的视角; 嵌入式系统架构的蓝图也已经在他的脑海中展开。 我们可以进入第四步嵌入式软件架构、系统层次和模块分解。