嵌入式软件架构中抽象层设计方法

  大家好,今天分享一篇嵌入式软件架构设计相关的文章。

软件架构这东西,众说纷纭,各有观点。什么是软件架构,我们能在网上找到无数种定义。

比如,我们可以这样定义:软件架构是软件系统的基本结构,体现在其组件、组件之间的关系、组件设计与演进的规则,以及体现这些规则的基础设施。怎么定义一般来说,基本上不重要,我们不是在写学术书籍,工程人员嘛,只关心软件架构能解决什么问题。

软件架构不是制定出来的,而是产品和业务需求所决定的,架构师所做的,只是忠于需求,并合理的表达了需求。软件架构也从来都不是一成不变的。在产品或者产品线的整个生命周期中,随着业务和需求的变化,软件架构不断发展和变化,以适应新的需要。

软件架构,也不是一个简单的项目问题,而是产品或产品线的技术战略问题。一个良好设计并推广的软件架构,能带来如下好处。

  • 最大限度地减少不必要的返工

  • 使嵌入式软件在宏观层面建立规划

  • 增强复用性,降低开发成本

  • 便于团队内部的技术培训

  • 使技术积累更加容易

我经常看到的一个常见问题是,新手工程师,由于经历与知识不足,往往看不到项目全貌,很难深刻理解软件架构,他们往往要经过多年的专业训练,才能逐渐建立架构意识。

但软件架构真的只是资深工程师和架构师的专利吗?这个也不见得。古人作文,讲究立意为先。

今天工程师做项目和产品,也应该先立意。这个意,就是指要有高度。工程师入门能从软件架构的高度出发,看待软件问题,相信对软件的理解,会更加深刻一些。因此,我总结了软件架构的六个步骤,供嵌入式工程师参考。

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的设备),嵌入式软件架构师应该将硬件相关的代码独立出去,并压缩在一个最小的范围内。否则,一旦使用耦合架构,不对硬件相关代码进行剥离,屎山式的代码,几乎是注定的结局。

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

但在嵌入式领域,无论搞什么产品,搞什么复杂的软件架构,剥离硬件相关,是第一步,也是最为关键的一步。连硬件相关代码都剥不干净,软件架构就犹如浮沙筑高台,无从谈起。

合抱之木,生于毫末,有志于提升技术水平的工程师们,先从隔离硬件开始吧。我在此先预祝成功!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/94537.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

g(x)=abx形式的函数最小二乘法计算方法

设函数,利用最小二乘法求解系数a和b: 设,,有 用最小二乘法求解和后,可得和: ,

【网络安全---ICMP报文分析】Wireshark教程----Wireshark 分析ICMP报文数据试验

一,试验环境搭建 1-1 试验环境示例图 1-2 环境准备 两台kali主机(虚拟机) kali2022 192.168.220.129/24 kali2022 192.168.220.3/27 1-2-1 网关配置: 编辑-------- 虚拟网路编辑器 更改设置进来以后 ,先选择N…

用pyinstaller打包LGBM模型为ELF/EXE可执行文件

1. 引入 写好的python代码和模型,如果需要做到离线部署、运行,就必须要将代码和模型打包为可独立运行的可执行文件。 使用pyinstaller就能做到这个,相同的代码,在windows上运行就能打包为exe,在linux上运行就能打包为…

android studio导入android源码模块开发总结

一、aidegen自动生成并导入android模块 1.源码下载后,键入 . build/envsetup.sh lunch sdk_car_x86_64-userdebug 以上命令执行后,tools/asuite/aidegen的源码会被编译为aidegen可执行文件 2.使用aidegen生成并自动导入模块 aidegen Settings -i j -…

AJAX和JSON

1、AJAX: AJAX(Asynchronous JavaScript and XML)是一种用于创建交互式、动态网页的技术。它允许网页在不重新加载整个页面的情况下与服务器进行异步通信,从而改善用户体验。以下是关于AJAX的一些重要信息: 异步通信&a…

银行业务队列简单模拟(队列应用)

设某银行有A、B两个业务窗口,且处理业务的速度不一样,其中A窗口处理速度是B窗口的2倍 —— 即当A窗口每处理完2个顾客时,B窗口处理完1个顾客。给定到达银行的顾客序列,请按业务完成的顺序输出顾客序列。假定不考虑顾客先后到达的时…

虚拟货币(也称为加密货币或数字货币)的运作

虚拟币发展史 虚拟币的发展史可以追溯到20世纪末和21世纪初,以下是虚拟币的重要发展节点: 1998年:比特币白皮书的发布 比特币的概念最早由中本聪(Satoshi Nakamoto)在1998年提出,随后在2008年发布了一份名…

(Note)机器学习面试题

机器学习 1.两位同事从上海出发前往深圳出差,他们在不同时间出发,搭乘的交通工具也不同,能准确描述两者“上海到深圳”距离差别的是: A.欧式距离 B.余弦距离 C.曼哈顿距离 D.切比雪夫距离 S:D 1. 欧几里得距离 计算公式&#x…

JavaScript事件之拖拽事件(详解)

在网页开发的过程中我们经常会接触到拖拽事件,虽然每个网页和每个网页的拖拽的效果大相径庭,但是从根本来讲,代码是几乎一模一样的。   简而言之,拖拽效果就是鼠标按下,被拖拽的元素随着鼠标而移动,鼠标松…

【单片机】13-实时时钟DS1302

1.RTC的简介 1.什么是实时时钟(RTC) (rtc for real time clock) (1)时间点和时间段的概念区分 (2)单片机为什么需要时间点【一定的时间点干什么事情】 (3)RTC如何存在于…

Trie字符串统计(c++题解)

维护一个字符串集合,支持两种操作: I x 向集合中插入一个字符串 x;Q x 询问一个字符串在集合中出现了多少次。 共有 N 个操作,所有输入的字符串总长度不超过 105,字符串仅包含小写英文字母。 输入格式 第一行包含整…

Mydb数据库问题

1、请简要介绍一下这个基于 Java 的简易数据库管理系统。它的主要功能是什么? TM(Transaction Manager):事务管理器,用于维护事务的状态,并提供接口供其他模块查询某个事务的状态。DM(Data Man…

国庆假期day5

作业:请写出七层模型及每一层的功能,请绘制三次握手四次挥手的流程图 1.OSI七层模型: 应用层--------提供函 表示层--------表密缩 会话层--------会话 传输层--------进程的接收和发送 网络层--------寻主机 数据链路层----相邻节点的可靠传…

CAcUiDockControlBar初始位置 2023/8/19 下午3:51:18

2023/8/19 下午3:51:18 CAcUiDockControlBar初始位置 2023/8/19 下午3:52:00 CAcUiDockControlBar的初始位置是根据其在程序代码中的设置而确定的。通常情况下,它的初始位置可以通过以下几种方式进行设置: 使用Create函数:在创建CAcUiDockControlBar对象时,可以调用Cre…

2023/8/12 下午8:41:46 树状控件guilite

2023/8/12 下午8:41:46 树状控件guilite 2023/8/12 下午8:42:08 树状控件(Tree View)是一种常见的图形用户界面(GUI)元素,它通常用于显示层次结构数据或文件系统的目录结构。Guilite 是一个轻量级的跨平台 GUI 库,支持多种控件,包括树状控件。 在 Guilite 中使用树状…

Ubuntu22.04 交叉编译gcc9.5 for arm

一、准备 环境:ubuntu22.04为刚刚安装,未安装gcc等包 vi ~/.bashrc输入 export PATH$PATH:/opt/gcc-arm-8.3-2019.03-x86_64-arm-linux-gnueabihf/bin 保存,reboot 安装: sudo apt install cmake sudo apt install gawk sudo apt instal…

[BJDCTF2020]Mark loves cat

先用dirsearch扫一下,访问一下没有什么 需要设置线程 dirsearch -u http://8996e81f-a75c-4180-b0ad-226d97ba61b2.node4.buuoj.cn:81/ --timeout2 -t 1 -x 400,403,404,500,503,429使用githack python2 GitHack.py http://8996e81f-a75c-4180-b0ad-226d97ba61b2.…

详解Linux的系统调用fork()函数

在Linux系统中,fork()是一个非常重要的系统调用,它的作用是创建一个新的进程。具体来说,fork()函数会在当前进程的地址空间中复制一份子进程,并且这个子进程几乎完全与父进程相同,包括进程代码、数据、堆栈以及打开的文…

【Java 进阶篇】JDBC 数据库连接池详解

数据库连接池是数据库连接的管理和复用工具,它可以有效地降低数据库连接和断开连接的开销,提高了数据库访问的性能和效率。在 Java 中,JDBC 数据库连接池是一个常见的实现方式,本文将详细介绍 JDBC 数据库连接池的使用和原理。 1…

算法强训:第三十四天

文章目录 收件人列表养兔子一、收件人列表OJ链接 本题思路:先接收到一个数字,代表接下来是多少组数据 ,逐个接收每个名字,如果名字中没有,或者 则直接输出,否则在改名字前后拼接"\""再输出,除最后一个名字外,每个名字之后都有一个", " ,该组用例…