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

news2024/11/20 1:37:25

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

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

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

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

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

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

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

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

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

  • 使技术积累更加容易

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

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

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

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.coloradmin.cn/o/1059250.html

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

相关文章

并查集详解(原理+代码实现+应用)

文章目录 1. 并查集概念2. 并查集原理2.1 合并2.1 找根 3. 并查集实现3.1 结构定义3.2 FindRoot(找根)3.3 Union(合并)3.4 IsInSet(判断两个值是否在一个集合里)3.5 SetCount(并查集中集合个数&…

Pikachu靶场——文件上传漏洞(Unsafe upfileupload)

文章目录 1. Unsafe upfileupload1.1 客户端检查(client check)1.1.1 源代码分析 1.2 服务端检查(MIME type)1.2.1 源代码分析 1.3 getimagesize()1.3.1 源代码分析 1.4 文件上传漏洞防御 1. Unsafe upfileupload 漏洞描述 文件…

springboot中的@Configuration详解~

作用1:告诉springboot这是一个配置类,等同于之前在spring中的配置文件,而且配置类中可以使用Bean标注在方法上给容器注册组件,该组件默认也是单例 创建pet: package com.springboot.bean;import lombok.AllArgsConstructor; import lombok.…

【进阶C语言】进阶指针--学会所有指针类型

本节内容大致目录: 1.字符指针 2.指针数组(数组) 3.数组指针 (指针)--比较重要 4.函数指针--比较重要 5.函数指针数组--用的较少 6.指向函数指针数组的指针--只需要了解就可以 需要掌握每一种类型的符号和用处。…

Google ProtoBuf介绍

一、背景 前段时间了解到有公司用gRPC、Pulsar、Nacos、SkyWalking、OpenTelemetry、Prometheus、Envoy、Grafana、Sonar、PowerJob、Apollo 这些技术,也是Java路线的,很惭愧,这些我几乎都不了解,从13年以来玩Android、玩Python、…

Java 多输入框查询需求实现

💗wei_shuo的个人主页 💫wei_shuo的学习社区 🌐Hello World ! 多输入框查询 需求分析 任意一个输入框,输入内容点击搜索都可以精准搜索到对应的内容 代码实现 Controller接口编写 PostMapping("merchant/manage&…

文献阅读:RLAIF: Scaling Reinforcement Learning from Human Feedback with AI Feedback

文献阅读:RLAIF: Scaling Reinforcement Learning from Human Feedback with AI Feedback 1. 文章简介2. 方法介绍 1. 整体方法说明 3. 实验结果 1. RLHF vs RLAIF2. Prompt的影响3. Self-Consistency4. Labeler Size的影响5. 标注数据的影响 4. 总结 & 思考 文…

SpringCloudGateway网关中各个过滤器的作用与介绍

文章目录 RemoveCachedBodyFilterAdaptCachedBodyGlobalFilterNettyWriteResponseFilterForwardPathFilterRouteToRequestUrlFilterWebSocketRoutingFilterNettyRoutingFilterForwardRoutingFilterDispatcherHandler 是什么?⭐如何确定最后的路由路径?下…

一文拿捏Spring之AOP

Spring 1.Spring的理解 1.狭义上 指SpringFramework,特别的控制反转、依赖注入、面向切面、等特性 2.广义上 Spring家族的一系列产品,像SpringMVC、SpringBoot、SpringCloud等 2.aop 🌟面试题(aop): 简单介绍一下AOP? aop…

【userfaultfd】2021强网杯notebook

程序分析 老规矩,看下启动脚本 开启了 smep、smap、kaslr 保护,当然 kvm64 默认开启 kpti 保护 文件系统初始化脚本 #!/bin/sh /bin/mount -t devtmpfs devtmpfs /dev chown root:tty /dev/console chown root:tty /dev/ptmx chown root:tty /dev/tty…

SAAJ:SOAP with Attachments API for Java

介绍 支持带附件的SOAP消息Java接口(SAAJ:SOAP with Attachments API for Java),定义了一套API,使开发者可以产生、消费遵从SOAP 1.1, SOAP 1.2, 和SOAP Attachments Feature的消息。 2019年SAAJ更改为新的名字&…

MIP精确算法的关键——确定界

目录 1.界是什么? 2. 如何更新上下界 2.1 以分支定界为框架的一系列算法 2.2 benders分解 MIP精确算法包含,分支定界、分支切割、分支定价还有benders分解等等。前者是以分支定界为框架的一类算法;后者是以分解为框架的一类算法。甚至还包…

Springboot学习笔记——1

Springboot学习笔记——1 一、快速上手Springboot1.1、Springboot入门程序开发1.1.1、IDEA创建Springboot项目1.1.2、官网创建Springboot项目1.1.3、阿里云创建Springboot项目1.1.4、手工制作Springboot项目 1.2、隐藏文件或文件夹1.3、入门案例解析1.3.1、parent1.3.2、starte…

嵌入式软件架构基础设施设计方法

大家好,今天分享一篇嵌入式软件架构设计相关的文章。 软件架构这东西,众说纷纭,各有观点。在我看来,软件架构是软件系统的基本结构,包含其组件、组件之间的关系、组件设计与演进的规则,以及体现这些规则的基…

Nginx高级 第一部分:扩容

Nginx高级 第一部分:扩容 通过扩容提升整体吞吐量 1.单机垂直扩容:硬件资源增加 云服务资源增加 整机:IBM、浪潮、DELL、HP等 CPU/主板:更新到主流 网卡:10G/40G网卡 磁盘:SAS(SCSI) HDD(机械…

【C/C++笔试练习】——常量指针和指针常量、结构体内存分配、统计输入的字母个数、排序子序列、倒置字符串

文章目录 C/C笔试练习1.常量指针和指针常量(1)常量指针和指针常量的定义(2)判别常量指针和指针常量(3)常量指针和指针常量的特性 2.结构体内存分配(4)计算结构体大小(5&a…

【计算机】CPU,芯片以及操作系统概述

1.CPU 什么是CPU? CPU(Central Processing Unit)是计算机系统的运算和控制核心,是信息处理、程序运行的最终执行单元,相当于系统的“大脑”。 CPU的工作流程? CPU 的工作流程分为以下 5 个阶段:取指令…

C++ -- 学习系列 关联式容器 set 与 map

一 关联式容器是什么? c 中有两种容器类型:关联式容器与序列式容器(顺序容器) 关联式中的容器是按照关键字来存储与访问的,序列式容器(顺序容器)则是元素在容器中的相对位置来存储与访问的。…

C++标准模板(STL)- 类型支持 ()

对象、引用、函数&#xff08;包括函数模板特化&#xff09;和表达式具有称为类型的性质&#xff0c;它限制了对这些实体所容许的操作&#xff0c;并给原本寻常的位序列提供了语义含义。 附加性基本类型及宏 实现定义的空指针常量 NULL 定义于头文件 <clocale> 定义于…

2.类与对象 拜访对象村

2.1 椅子大战 在图形接口画出正方形、圆形与三角形。当用户选点图形时&#xff0c;图形需要顺时针转360并依据形状的不同播放播放不同的AIF文件。胜者可以坐上名贵宝椅。 面向过程&#xff1a; rotate(shapeNum) {//旋转360 } playSound(shapeNum) {//查询播放哪个AIF文件//播…