1.难题

作为一名嵌入式开发者,想必各位小伙伴对以下场景早已司空见惯:当你正埋头于调试那几行关键代码,或者准备给项目打包成完工版本的时候。总有一个声音会适时响起:“咱再加个小功能呗?”通常这小功能,相当于要求你的自行车瞬间变身成摩托车。运气好点,是场局部“微创手术”;运气不好呢,直接项目重写

为啥咱总能精准踩坑?别怀疑,这大概率不是命运的捉弄,而是咱自己挖的。软件设计原则?好像听说过。设计模式?听起来像时尚界的潮流,咱搞硬件的实在人用不上吧?于是,咱的代码就成功进化成了“面向需求变更崩溃编程”,这种代码的结构之“精妙”,让任何试图修改它的人,都想给自己点一首《凉凉》。

为了避免下次需求变更时,咱的代码再次表演“原地爆炸”,我将以一个代码来介绍如何遵循软件设计原则,如何使用良好的设计模式和架构。
添加图片注释,不超过 140 字(可选)

2.背景

前文提到,作为一名硬件工程师因为生活饮食不规律,缺少运动,导致最近做体检时发现我目前的血糖偏高。为实现血糖的动态监测,我还网购了一个家用的血糖仪,每天定时测量血糖数据。由于自己从事多年的仪器仪表行业,对这个每天用来测血糖的“小家伙”充满好奇,犹豫了很久将它拆开后发现一个惊人事实:整个电路板只有一颗芯片!一款专用于血糖仪的单片机BH67F2472。

添加图片注释,不超过 140 字(可选)

详细查看电路板后,我发现仪器的制造商居然把芯片的程序下载口预留出来了,我网购了一个Holtek的下载器,安装了开发工具HT-IDE3000,并将这个自己写的程序下载到了血糖仪电路板的芯片中。接下来,我将以BH67F2472的一个代码来介绍如何遵循软件设计原则,如何使用良好的设计模式和架构。

添加图片注释,不超过 140 字(可选)

3.电路介绍

程序使用了以下硬件资源:

  • 按键:GPIO口PA3连接按键,通过读取PA3的电平信号来检测按键是否按下;

  • 蜂鸣器:GPIO口PB6连接蜂鸣器,过控制PB6的电平驱动蜂鸣器,让蜂鸣器发出声音;

  • 液晶屏:LCD驱动引脚COM0COM3,SEG1SEG8连接到了段码液晶屏,微控制器内部的 LCD 驱动控制器按照特定的扫描时序在 COM 和SEG 线上产生驱动电压,点亮或熄灭液晶屏上特定的字段;

  • 温度测量:ADC通道1的PB3连接NTC热敏电阻,NTC的电阻值随环境温度变化而变化,当温度变化时NTC上的分压值随之改变。ADC通道读取PB3上的模拟电压值,实现温度测量;

  • 串口通信:UART0的TX/RX连接串口,实现输出调试打印信息。

添加图片注释,不超过 140 字(可选)

4.程序介绍

4.1.模块化设计

程序采用了模块化设计,每个功能独立成一个模块。简单来说,就是把软件这个大工程,像搭乐高积木一样,拆成了一个个独立的功能模块 —— 每个模块负责一件事,谁也别抢谁的活儿。
这种设计方法的核心思想就是 :分而治之。通俗的讲就是:当你面对一个复杂的大问题,最明智的做法就是把它“化整为零”,拆解成一系列小到可以轻松搞定的小问题,然后挨个解决掉。程序的模块化设计如下图:

添加图片注释,不超过 140 字(可选)

程序包含三个任务:

  • 任务一GPIO任务,GPIO口PA3连接按键,GPIO口PB6连接蜂鸣器,程序通过按键实现用户对显示内容的控制,短按按键实现循环切换显示模式:温度→血糖→电量→温度,每切换一次蜂鸣器会发出短鸣提示。
  • 任务二LCD任务,程序控制微控制器内部的 LCD
    驱动控制器点亮或熄灭液晶屏上特定的字段,实现3位7段数字的显示,同时段码液晶屏还可以显示不同数据的单位。
  • 任务三NTC任务,ADC通道1的PB3连接NTC热敏电阻,NTC的电阻值随环境温度变化而变化,当温度变化时NTC
    上的分压值随之改变。ADC通道读取 PB3上的模拟电压值,实现温度测量。

这种模块化设计严格遵循了单一职责原则——每个模块只专心做好自己那一摊事儿,绝不越界抢活干,模块之间奉行“君子之交淡如水”,彼此低耦合,互不依赖。这样一来,修改一个模块的代码,完全不用担心会“城门失火,殃及池鱼”,各干各的,互不打扰,世界和平!
模块化设计提高了软件系统的扩展性,软件工程源码中功能模块如下:

添加图片注释,不超过 140 字(可选)

4.2.调度器

RTOS 通常需要额外的内存开销用于任务栈、内核数据结构以及提供任务调度。由于BH67F2472有限的计算资源(如 RAM、ROM 容量较小)和相对较低的运算性能,无法有效地承载一个完整的实时操作系统(RTOS)运行环境。为了在资源受限的条件下实现多任务逻辑的轮转执行,开作者设计并实现了一个精简的轮询式任务调度器。
HOLTEK开发环境所使用的 C 编译器不支持函数指针,函数指针是构建动态任务调用机制的常用且高效手段,缺失函数指针实现调度器将变得比较笨拙,只能使用枚举量和switch语句实现,在scheduler文件中实现了一个轮询执行的“伪调度器”,关键代码如下:

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

这种设计实现的调度器被称为“伪调度器”,因为这个调度器有以下特点:

  • 任务执行是顺序执行、非抢占执行。一个任务必须主动执行完毕并返回(break 出
    case)后,调度器才能切换到下一个任务,不存在由中断或系统调用触发的任务强制切换。
  • 静态绑定: 任务与枚举值、case 分支是静态编译时绑定的,缺乏运行时动态创建、删除或修改任务列表的能力。
  • 轻量级: 其实现极其简洁,仅需一个枚举变量、一个 switch 语句和若干函数调用,几乎不消耗额外的 RAM
    资源(栈空间除外),代码体积(ROM)也很小,完美契合资源受限环境。
4.3.分层设计

每一个任务都采用了分层设计,分层设计的核心思想也是“分而治之”,分层设计将软件功能水平分割成合理的多个子系统,软件中紧密关联的部分被集中放在一个层内。分层架构有以下优点:

  • 每一层都把一个具体功能抽象化。
  • 可以降低代码的相互依赖程度,更改代码时影响的层很少。
  • 层可以被复用。

程序中采用了2层的分层设计,第1层处理MCU寄存器相关操作,第2层处理驱动控制和逻辑控制,分层设计提高了软件系统的移植性,如果项目更换了MCU那么只用修改第1层,如果更改了业务逻辑那么只用修改第2层。分层架构框图如下:

添加图片注释,不超过 140 字(可选)

以GPIO任务为例,GPIO的BSP层的接口函数是gpio_bsp_operation,上层文件可以通过gpio_bsp_operation函数完成GPIO的寄存器初始化、读操作、写操作。GPIO任务的BSP层代码如下:

添加图片注释,不超过 140 字(可选)

4.4.隔离设计

程序中的任务相互隔离,所有任务只与调度器进行数据交互,然后调度器将消息推送给其他任务。各个任务之间的信息交互模式如下:

添加图片注释,不超过 140 字(可选)

这种设计模式为中间者模式。在中间者模式,对象之间不能直接通信,而是间接地通过中间者进行通信。中间者收到信息后,再将信息转发给相关对象,这样减少了对象之间的相互依赖。中间者模式有以下优点:

  • 对象之间是松耦合。
  • 将多对多的关系通过中间者转换成一对一的关系。
  • 修改一个对象,不需要考虑其它对象通信适应问题。

这种设计减少了任务之间的耦合,提高了软件的扩展性,消息交互代码如下:

添加图片注释,不超过 140 字(可选)

4.5.程序流程图

程序主要分为四个过程:

  • 初始化系统时钟,配置MCU系统时钟为8MHZ
  • 执行调度器初始化动作,调度器依次调用所有任务中的initialization函数,执行各个任务初始化。
  • 执行调度器依次调用所有任务read函数获取改任务输出信息,并将读取到的信息通过调用其他任务write函数写入执行操作。
  • 执行调度器依次调用所有任务run函数,然后每个任务在后台运行。

程序流程图如下:

添加图片注释,不超过 140 字(可选)

感兴趣的小伙伴,希望获取资料的小伙伴,可以评论区留言或者私信作者。

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐