单片机工程架构 –by cube

如果我们有一个项目,其基于STM32F4xxx实现,我们需要调用其库函数(无论是标准库/HAL/LL)。

例如,我们定义LED_ON()函数,其本质上我们调用将GPIO设为高电平的库函数;

再进一步,假设我们的设备需要在加热状态下点亮这颗LED,那么我们就要在这个任务函数下调用LED_ON();

现在我们的项目已经呈现了一个层状的架构,库函数被一层层地向上调用。

现在我们有一个问题:现在我们有两颗LED,这两颗LED显示不能使用同一个LED函数;当我们在cubemx里面又加了一个GPIO的配置,并改成了Red_LED_ON()、Green_LED_ON()之后,又在不同的应用函数中分别调用了两颗LED…忽然要求你在另一种单片机上实现同一个方案….

我们能不能想到一个办法,能够比较便利地增加外设,又能统一地调换接口以适配不同的单片机呢?

我参考了网络上很多教程以及AI,创建了一个自己的架构。

注意:本文只记录自己的理解,并不是一个值得被学习or绝对契合实际的经验分享。还请各位大佬不啬赐教。

C语言的面向对象思想

解决上述第一个问题的方式,是借鉴CPP的面向对象思想,比如我们可以声明一个对象,这个对象叫Red_LED,他属于LED类。而LED类,他需要被指定Port、Pin,需要指定初始电平,同时操作他的函数有:开、关、反转等等。如果我们有了第二个LED,我们只需要为他声明一个新的对象,为他绑定GPIO,设置初始电平。之后操作这个新的LED,只需要指定是调用这个对象的开函数即可。相较于我们之前的工程,面向对象的优势显而易见。

那么我是如何借助面向对象思想设置架构的呢?

Core层

本层是一个通用工具基类设置,例如:LED与Buzzer,我们可以说他们虽然功能不同,但是操作他们的功能上有很多的相似之处:初始化一个GPIO、ON、OFF、Toggle。

我们可以在这一层规定一个开关类设备的操作接口,我们把共性很强的设备的操作接口在此做一个统一。

这一层本质上是一个辅助层,我们基于此层将外设之间的联系拉紧,避免重复的封装

HAL层

这一层为硬件抽象层,我们在这一层把所有对外设库函数的操作封装成自己的函数,这样,当我们换一个MCU后,我们只需要将此层的库函数换成新MCU对应的名称即可。

按照之前的方案,当我们换一个MCU,我们需要把LED驱动库里面的GPIO库函数换一下,需要把Buzzer库函数换一下…

但是现在我们不需要了,因为LED与Buzzer调用的都是GPIO_WritePin()(一个我们自己编写的函数)这个函数代替各种外设去调用库函数里面GPIO的写高电平,如果我们要换MCU,我们只需要修改这一个函数即可。

这一层,我们解决了换MCU后移植代码过于繁琐的问题。

Device层

简单来说,这一层的目标是设定我们的外设是什么?需要什么?可以做什么?

对接HAL层

当我们把MCU库函数抽象一层之后,我们给我们的外设分别调用HAL层的抽象库函数。当然我们可以在此处加一些条件判断,例如当前对象是否被初始化?如果没有,自然无法操作。

对接Core层

对于一个具体的外设,他的信息可以从Core层一个基础类设备继承而来,这也是面向对象拒绝重复封装的思想。

对于外设要什么?我们可以在一个结构体指明他的GPIO、以及其他的配置信息。关注到GPIO配置信息有极强的重复性(指明Core、Pin)我们完全有道理把GPIO的封装另设一个结构体,每一个外设在配置信息时,都通过这个结构体配置它的IO信息。

对于外设要做什么,我们可以进行一次ops封装,即将操作他的函数放置在一个结构体中,并注册到类中。这样,我们调用红色LED以及绿色LED相同操作时不用新建函数,直接指明是哪一个对象的哪一个操作函数即可。

Board层

本层我们的目标是设定对象,暴露对象地址,并配置对象的基本信息,。

假设我们手上有 两块开发板,一个开发板上把LED绑在PC13上,一个开发板有两个LED,分别绑在PA0、PB8上。我们需要在此层调用Device层的配置信息结构体,告知本项目调用外设的基本信息。

对于一个外设的操作函数,例如我们调用LED_ON(led_t LED);我们在Device层就指定LED_ON实际上是调用ops里面的on成员。

一般上我们把Board层+上述Device层合并为BSP层。此处我希望Board层作为一个为不同Board而设定的层。

这么做还有一个好处,假设我们本项目并不需要某一个外设,我们也只需要简单删掉一个对象即可。

Facade层

在本层中,我们会调用Device层指定的操作函数以及Board层指定的对象,构成对某一特定外设的特定操作,例如LED_ON(&Red_LED);如果我们现在有一个任务需要红色LED闪烁,我们就可以在这一层通过来回调用特定对象的开关函数实现这一功能。

这一层的代码面向业务而不是实现业务,或者我们简单理解为针对业务繁杂的问题,我们提前做好部分代码的包装,以此进行预封装。

App层

本层是基于项目而生的应用层,你的项目是什么?请在此处根据你的项目调用不同的面向业务代码抑或是直接调用对象及其操作函数实现我们本项目的具体业务。

Lib层

本层独立于其他层之外,提供数据/算法等等无关封装但包含大量信息的接口。


综上,我们的项目中各层的调用关系大抵如此:

当前的项目架构还有很多没有考虑到的地方,之后会不断深入写第二篇,如果有佬看到的话,欢迎你多多指点,我也会给出我的思考,希望和大家多多交流~