良许Linux教程网 干货合集 软件架构——嵌入式软件抽象层

软件架构——嵌入式软件抽象层

软件架构这个概念存在各种不同的定义和观点,我们可以在网络上找到无数种不同的解释。

例如,我们可以这样定义:软件架构是软件系统的基本结构,包括组件以及它们之间的关系,定义了组件的设计和演进规则,并通过基础设施来实现这些规则。然而,具体的定义并不是特别重要,特别是对于工程人员来说,并不是在写学术书籍,我们更关心的是软件架构能够解决什么问题。

软件架构并不是静止不变的,它是由产品和业务需求来决定的,架构师的工作只是忠于需求,并合理地表达这些需求。随着业务和需求的变化,软件架构也会不断发展和变化,以适应新的需求。

软件架构问题不仅仅局限于一个简单的项目,而是产品或产品线的技术战略问题。一个良好设计和推广的软件架构可以带来以下好处:

  • 最小化不必要的重复工作
  • 在宏观层面上规划嵌入式软件
  • 增强可重用性,降低开发成本
  • 方便团队内部的技术培训
  • 使技术积累更加容易

经常发现一个常见问题是,新手工程师由于经验和知识的不足,往往无法全面了解项目,很难深入理解软件架构。他们通常需要经过多年的专业培训才能逐渐形成架构意识。

但是,软件架构真的只是资深工程师和架构师的专利吗?未必。古人在写作文时,注重立意。

今天从事项目和产品的工程师也应该有个明确的立意。这个立意就是要有远大目标。工程师从入门开始就应该从软件架构的高度来看待软件问题,相信这样对软件的理解会更加深刻。因此,我总结了软件架构的六个步骤,供嵌入式工程师参考:

  1. 隔离与硬件相关的代码,建立抽象层
  2. 构建统一的软件基础设施
  3. 适当识别和处理产品数据
  4. 将功能进行分层和分解
  5. 设计组件及其接口
  6. 支持测试、调试和跨平台开发

需要注意的是,光是阅读这六篇文章并不能保证嵌入式工程师就能掌握软件架构。嵌入式软件架构师是无法一蹴而就的。但至少,嵌入式工程师们可以了解到正确的努力方向是什么样的,因为在很多情况下,选择比努力更加重要。

因此,在接下来的几篇文章中,我们将一起探讨设计嵌入式软件架构的六个步骤。

嵌入式软件架构之一 抽象层与硬件隔离

许多新手乃至老手嵌入式工程师,在未了解软件架构之前,把应用层功能和硬件相关的代码,不由自主的搅和在一起写。这种做法非常普遍。比如下面的代码:

void modbus_rtu_write_reply(uint8_t add, uint8_t func_code, uint16_t reg, uint16_t data)
{
    rs485.buff_tx[0] = add;
    rs485.buff_tx[1] = func_code;
    rs485.buff_tx[2] = (uint8_t)(reg >> 8);
    rs485.buff_tx[3] = (uint8_t)(reg);
    rs485.buff_tx[4] = (uint8_t)(data >> 8);
    rs485.buff_tx[5] = (uint8_t)(data);

    uint16_t crc16 = mb_crc16(rs485.buff_tx, 6);

    rs485.buff_tx[6] = (uint8_t)(crc16);
    rs485.buff_tx[7] = (uint8_t)(crc16 >> 8);

    rs485.tx_total = 8;
    rs485.tx_num = 0;

    /* Send data from the uart port. The hardware related program. */
    LL_USART_ClearFlag_TC(USART1);
    LL_USART_EnableIT_TC(USART1);
    USART1->DR = rs485.buff_tx[rs485.tx_num ++];
}

上面的这一段代码,不是一个好例子。从函数LL_USART_ClearFlag_TC开始的一句,也就意味着,这个Modbus的代码,和MCU提供出的固件库耦合在一起写了。

著名的SOLID原则中,有个依赖倒置原则,高层模块不应该依赖于底层模块,它们应该共同依赖于抽象。此处的代码,显然违反了这一原则。Modbus作为高层模块,此处对MCU固件库的API进行了依赖。

对于这种将硬件相关的代码与功能耦合在一起的软件架构,在本文中,我们姑且称之为“耦合架构”;而我们要追求的,是将隔离硬件相关的软件架构,我们称之为“隔离架构”。接下来,我们将详细对比,耦合架构和隔离架构各自的特征。

耦合架构的问题

虽然从原则上来说,耦合架构是不对的,但我个人对这种软件写法,还是能理解的。为什么?万事皆有因,存在即合理。一般而言,大部分嵌入式软件工程师,都出自硬件相关的专业(比如电子、自动化等),来自于软件工程和计算机专业的嵌入式工程师不多(他们都去互联网行业了),因此从他们的知识结构和习惯思维出发,一般从硬件视角看待嵌入式系统,而不是站在软件抽象的视角。

我个人也是电子工程专业毕业的,对此有感受。但理解归理解,道理归道理,既然已经从事嵌入式软件,哪怕是硬件专业出身的,我也建议他一定抛弃既有思维,学会抽象这一强大的软件思维工具,否则他的职业天花板将非常低。

耦合架构带来的问题,也是显而易见的,那就是,实实在在的难以移植。因为一旦硬件发生变化,比如MCU停产,芯片短缺等等(在当前形势下太过常见),嵌入式软件就要大把修改。如果软件规模较大,尝试移植耦合架构的代码到在新MCU上,是一项艰巨的工作,没人愿意干这事。因此产品开发完成,更新架构并推倒重来,几乎是不可能。

别说工程师不愿意,你问问老板答应吗?于是工程师们只能检查所有代码,把与硬件交互的每一行代码改掉,遇到硬件交互方式大不相同的,就更糟心,还要大篇幅的改,边改边骂娘。比如上面的代码,如果换一片芯片,可能要改为以下代码。

void modbus_rtu_write_reply(uint8_t add, uint8_t func_code, uint16_t reg, uint16_t data)
{
    rs485.buff_tx[0] = add;
    rs485.buff_tx[1] = func_code;
    rs485.buff_tx[2] = (uint8_t)(reg >> 8);
    rs485.buff_tx[3] = (uint8_t)(reg);
    rs485.buff_tx[4] = (uint8_t)(data >> 8);
    rs485.buff_tx[5] = (uint8_t)(data);

    uint16_t crc16 = mb_crc16(rs485.buff_tx, 6);

    rs485.buff_tx[6] = (uint8_t)(crc16);
    rs485.buff_tx[7] = (uint8_t)(crc16 >> 8);

    rs485.tx_total = 8;
    rs485.tx_num = 0;

    /* Send data from the uart port. The hardware related program. */
    MCU_NEW_USART_ClearFlag_TC(NEW_USART1);
    MCU_NEW_USART_EnableIT_TC(NEW_USART1);
    NEW_USART1->DR = rs485.buff_tx[rs485.tx_num ++];
}

其次,耦合架构会导致,在开发环境中(如Windows或者Linux,非目标硬件),很难对应用程序进行单元测试。脱离目标硬件,跨平台开发嵌入式程序,是提升开发效率的重要措施。

这一点,我会《嵌入式软件架构六部曲之六 测试、调试与跨平台开发的支持》一文,会有所涉及;在后续的《嵌入式程序的单元测试》系列教程中(暂定)进行详细阐述。这样的开发方式,在很中小型团队的嵌入式开发中,还不多见,但掌握了就真的提升很大效率。

对耦合架构来说,应用程序代码直接调用硬件,如果要进行完整的测试工作,就要花费大量工作,因为测试程序也要去操作硬件,才能验证正确与错误。或者,需要工程师在硬件上完成手动测试(实际上现在大家就这么干的,哈哈)。

手动测试很繁琐,往往让人烦躁,工程师的主观感受,会影响测试质量。很多时候,为了赶进度,或者规避繁琐的测试工作,软件并没有经过很好的测试,整体系统质量受到影响。另外,手动测试,交付软件可能需要更长的时间。而自动测试,往往只需要一瞬间,清楚明了。这个我们会在后续文章《嵌入式软件的测试驱动开发》中进行阐述。

第三,耦合架构将存在不易扩展的问题。耦合架构,往往是共享数据的,也就是所谓的全局变量满天飞。随着软件系统的扩大,每个新功能的添加,变得更加困难,而且是越来越困难,出现BUG的机会急剧增加。屎山就是这么炼成的。

但需要说明的是,数据问题,不是说隔离了硬件,就能完全解决掉。数据问题,是嵌入式软件乃至任何软件的核心问题,它需要在架构六部曲之二和之三中,通过软件基础设施的合理构建,和数据机制的合理制定,共同得到解决。

隔离架构如何解决问题?

到这里,我们架构的第一步,呼之欲出,那就是:将软件架构分离为硬件相关和硬件无关两个部分。这就要引入抽象层这个概念。何为抽象层?抽象层有很多种,比如硬件抽象层(HAL)、设备抽象层(DAL),操作系统抽象层(OSAL),网络抽象层,文件系统抽象层,Flash抽象层(RT-Thread里就有这个)等等。

对谁进行抽象,就会建立这个东西的抽象层,无一定之规。本文中的抽象层,特指硬件抽象层,或者设备抽象层,或者二者兼备。具体是谁,取决于产品特性,可参考后续文章《嵌入式软件中的抽象层》。

在硬件相关代码和硬件独立代码之间创建抽象层,这是软件移植的要求,实际上也是依赖倒置原则需求。在这里,我们有必要对依赖倒置原则进行强调:高层模块不应该依赖于底层模块,它们应该共同依赖于抽象。也就是说,应用层代码(硬件无关),不应该依赖于硬件相关的代码(驱动代码),他们应该依赖于抽象层代码。

抽象层的创建,将允许将应用代码从一个微控制器移动到下一个微控制器,或者一套硬件迁移到另一套硬件,应用层代码不必更换。抽象层打破了硬件依赖关系;换句话说,应用程序根本不必知道,也不必关心,当前运行的是什么硬件,应用程序只需要关心抽象层的API是什么样的。

新的硬件驱动程序要做的,仅仅是满足接口的要求而已。这意味着如果我们更改硬件,则只会更改硬件相关的模块,而不是整个代码库。

void modbus_rtu_write_reply(uint8_t add, uint8_t func_code, uint16_t reg, uint16_t data)
{
    rs485.buff_tx[0] = add;
    rs485.buff_tx[1] = func_code;
    rs485.buff_tx[2] = (uint8_t)(reg >> 8);
    rs485.buff_tx[3] = (uint8_t)(reg);
    rs485.buff_tx[4] = (uint8_t)(data >> 8);
    rs485.buff_tx[5] = (uint8_t)(data);

    uint16_t crc16 = mb_crc16(rs485.buff_tx, 6);

    rs485.buff_tx[6] = (uint8_t)(crc16);
    rs485.buff_tx[7] = (uint8_t)(crc16 >> 8);

    rs485.tx_total = 8;
    rs485.tx_num = 0;

    /* Send data from the uart port. The hardware related program. */
    hal_uart_send(HAL_UART_ID_1, rs485.buff_tx, rs485.tx_total);
}

void hal_uart_send

硬件相关的代码,应该改为如下的样子。这尚且算不上真正的抽象层,只是抽象层最简陋的替代实现方法,实际工程应用中,抽象层还有很多细节需要阐述。限于篇幅,在本文中,我们不进行探讨,请关注后续的《抽象层》系列文章。

void hal_uart_send(uint8_t uart_id, void *buffer, uint32_t size)
{
    /* Start the uart sending process, the remaning data will be send in UART ISR 
       function. */
    MCU_NEW_USART_ClearFlag_TC(NEW_USART1);
    MCU_NEW_USART_EnableIT_TC(NEW_USART1);
    NEW_USART1->DR = rs485.buff_tx[rs485.tx_num ++];
}

抽象层还可以解决单元测试的许多问题。有了抽象层,我们可以在Windows或者Linux上创建硬件的替身程序(mock),也可以称为假硬件。我们可以在假硬件上给出输入数据,并通过检查假硬件给出的输出数据会否符合预期,来对软件进行单元测试。在没有硬件的情况,也可以对应用层程序进行开发。很多嵌入式程序员觉得不可能,但这时很多大公司开发软件的方式。

但是,过去一个月,我在公司的电脑上开发一个软件模块,我所有的开发都是使用主机上的单元测试完成的。最终在目标硬件进行实测的时间,仅仅用了两个小时而已。我们公司是做高端医疗设备的,每台设备价值一两千万,整个公司上上下下,只有八个机器供我们几百人的团队进行测试,非常紧张,需要预约。如果所有人都在目标设备上调试,才能完成编程,那成本是不能承受的。

抽象层的建立,还有一个好处。软件不必等着硬件就绪才开始开发,而在硬件可用之前,就开始专注于开发和交付应用程序。这样做的好处是,可以在项目早期就对客户提供试用服务,并根据客户反馈进行功能调整。如今,太多的团队专注于首先准备好硬件,而核心应用程序是事后才想到的。这样并不利于对嵌入式软件进行良好的设计和实现。

那么如何建立抽象层呢?抽象层的建立,涉及到几个关键的因素:抽象的程度、抽象的手段以及抽象的对象。这些问题,非常复杂,非三言两语就能说清,我们会在后续文章《抽象层》系列文章和视频中,结合实际例程中进行详细阐述。

结论

嵌入式软件与其他软件领域都不一样,因为没有一个软件领域,和嵌入式软件一样,会和硬件进行直接交互(请注意此处直接二字)。为了应对可能出现的硬件变化(无论是MCU,PCBA,还是连接PCBA的设备),嵌入式软件架构师应该将硬件相关的代码独立出去,并压缩在一个最小的范围内。否则,一旦使用耦合架构,不对硬件相关代码进行剥离,屎山式的代码,几乎是注定的结局。

一个成功的软件架构,从来不是一蹴而就,通常是通过迭代和演进创建的。这需要技术负责人,或者架构师,主动去推动软件架构的迭代,不断推动软件的优化重构。这就有点像明星的好身材,从来不是天生,都是后天自律的结果。

但在嵌入式领域,无论搞什么产品,搞什么复杂的软件架构,剥离硬件相关,是第一步,也是最为关键的一步。连硬件相关代码都剥不干净,软件架构就犹如浮沙筑高台,无从谈起。合抱之木,生于毫末,有志于提升技术水平的工程师们,先从隔离硬件开始吧。我在此先预祝成功!

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

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

作者: 良许

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

发表评论

联系我们

联系我们

公众号:良许Linux

在线咨询: QQ交谈

邮箱: yychuyu@163.com

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

微信扫一扫关注我们

关注微博
返回顶部