裸机编程的几种模式、架构与缺陷。

大多数嵌入式的初学者都是从单片机裸机编程开始的,对于初学者来说,裸机编程更加直观、简单,代码所见及所得,调试也非常方便,区别于使用操作系统需要先了解大量的操作系统基础知识,调度的基本常识,还需要注意各种资源的共享与竞争等概念,并且调试也没有那么直观等等。裸机编程在一些比较简单的项目上还是具有一定的优势的。

接下来我们来看看裸机编程的常见模式和架构。

1.主循环轮询模式

主循环轮询模式就是在主函数中使用一个永不退出的 while(1) 来承载所有的应用逻辑,如下:

int main(void) {
    while(1){
        do_a();
        do_b();
        do_c();
    }
}

do_a、do_b、do_c 三个函数依次执行,全部执行完毕后再次从 do_a 逻辑开始,以此不断循环。

这种模式是最简单也是最初级的模式,但其也存在很多问题。由于上述三个逻辑会依次执行,那么就会相互影响,do_b 必须要等 do_a 执行完后再执行,do_c 必须要等 do_a 和 do_b 都执行完后才执行,一旦前置逻辑中存在大量的延时,后续逻辑就无法得到及时的运行。

比如后续逻辑中存在一些交互行为,do_b 会判断一个按键的按下状态并做出响应,而此时还在 do_a 中执行延时指令,那么整体运行就会显得非常卡顿,甚至还会因为错过用户按键的时机而导致即使按下了按键,也没有执行对应的反馈。

2.中断执行模式

针对于上面的问题,很多人就会使用中断来解决。对于一些需要立即响应的操作,将其放在中断中,从而避免其被主程序中的其他逻辑所影响,此时代码可能如下所示:

//按键中断
void key_isr(void){
    do_b(); //按键按下的操作
}

int main(void) {
    while(1){
        do_a();
        do_c();
    }
}

主循环中还是正常执行非交互式的逻辑,而对于上例中按键交互的逻辑 do_b,则放到对应的按键信号捕获中断中(如 GPIO 外部中断)。此时即使在执行主循环中的其他逻辑,由于中断会打断主循环立即运行,所以按键信号会被立刻检测到并响应。

无法及时得到响应的问题解决了,对于一些非常简单的逻辑,这种模式就足够了,但如果主循环中的逻辑有一定的周期性要求,如 do_a 需要每隔 100 毫秒执行一次, do_c 需要 50 毫秒执行一次,于是 do_a 和 do_c 下就会存在 delay(100) 和 delay(50) 的代码:

// 按键中断
void key_isr(void) {
    do_b();  // 按键按下的操作
}

void do_a(void) {
    delay(100);  // 延时100ms
    // do_a 逻辑
}

void do_c(void) {
    delay(50);  // 延时50ms
    // do_c 逻辑
}

int main(void) {
    while (1) {
        do_a();
        do_c();
    }
}

此时无论 do_a 和 do_c 谁前谁后,他们的执行周期都会拉长到至少 150 毫秒!因为顺序执行的原因,你必须等待上一个逻辑执行完才能执行下一个逻辑。

这种情况下 do_a 和 do_c 任何一个逻辑的周期都无法被满足,这种模式的缺陷也就显现出来了。

3.中断+定时器+主循环的前后台架构

上例的一个最大问题就是主循环的每次执行都要完整地将所有逻辑都执行一遍,而每个逻辑中为了控制自身的周期又用了延时。各个延时就不可避免地影响到其他逻辑的执行,再由于顺序执行的逻辑,其他逻辑的执行又影响到了自身,产生恶性循环,最终没有一个逻辑是符合其自身的周期的。

既然如此,我们可以使用定时器产生一个时间标志,这个标志代表了当前系统运行的时间,主循环中的逻辑再检测这个时间,如果满足自身执行的时间,那么就执行自身逻辑,如果不满足则直接跳出,让其他逻辑执行,中断逻辑仍然不变。这种情况下前台就是中断,后台就是主循环,其代码形式如下:

// 按键中断
void key_isr(void) {
    do_b();  // 按键按下的操作
}

// 定时器中断 1ms 进一次
unsigned int tick = 0;
void timer_isr(void) {
    tick++;
    if (tick > 10000) tick = 0;
}

void do_a(void) {
    if (tick % 100 == 0) {
        // do_a 逻辑
    } else {
        return;
    }
}

void do_c(void) {
    if (tick % 50 == 0) {
        // do c 逻辑
    } else {
        return;
    }
}

int main(void) {
    while (1) {
        do_a();
        do_c();
    }
}

由上述代码可以看到定时器中断为 1 毫秒,每进一次中断 tick 加 1,在主循环中的 do_a 和 do_c 会首先判断 tick 的值,一旦发现与自己的运行周期相同,则执行自身逻辑,否则退出。此时理想的运行图如下:

由于去掉了每个逻辑中的延时,取而代之的是标志位的判断,其执行速度是非常快的,如上图所示 ,灰色的块表示在运行判断逻辑并且没有满足运行要求。这种情况下每个逻辑都能在其指定的周期内得到执行。

这种架构在裸机编程中可以算得上一种中高级的架构,能够满足大多数不是特别复杂的需求。当然,在上图中我们可以看到 do_a 和 do_b 一个为 100 毫秒,一个为 50 毫秒,存在公倍数情况,也就是说在某一时刻,如这里的 0 毫秒和 100 毫秒,就会出现两个逻辑同时运行的场景。实际在项目中如果要求比较严格,会对这个周期进行一个控制和计算,尽量减少各逻辑同时执行的概率,避免由于同时执行的逻辑过多且过于频繁,执行时间的总和仍然会太长,从而影响整体运行稳定性的问题。

到这里请思考一下,假如 do_a 逻辑本身的执行时间就很长,比如进行一个非常复杂的运算,或者需要读取一个 G 级别的文件,导致单一逻辑的执行时间就超过了最小周期(如例子中的 50 毫秒),那即使 50 毫秒的周期到了,由于 do_a 还没运行完,do_c 也无法得到运行,这时候时间标志已经形同虚设,甚至由于此处是取余判断,假如 do_a 运行了 51 毫秒结束,do_b 在判断的时候已经是 52 毫秒,52%50 不为零,do_b 直接无法执行,时间标志甚至产生了负面影响!

虽说将 “通过取余运算判断是否可以执行的逻辑” 修改为 “设置多个时间标志(如 50ms_flag、100ms_flag等),在中断中判断满足时间就将这些标志置位,主循环中直接对这些标志进行判断的逻辑” 可以避免由于时间后延导致的无法触发逻辑执行问题,但仍然无法解决周期被影响的本质。

怎么办?

4.前后台 + 状态机架构 

既然上面的问题是由于主循环中单个应用逻辑自身执行时间太长导致,那么我们就将其拆分,原本一个逻辑只能一次执行完,现在就拆分成多个步骤,每次执行只运行一个步骤而不是完整的逻辑,再用一个变量去记录当前执行到了哪个步骤,下次进入就执行下一个步骤。

这就是状态机编程(以 do_a 为例,其他主循环逻辑同 do_a ):

void do_a(void) {
    static unsigned char step = 0;
    if (tick % 100 == 0) {
        switch (step) {
            case 0:
                // 执行第一步
                step++;
                break;
            case 1:
                // 执行第二步
                step++;
                break;
            case 2:
                // 执行第三步
                step = 0;
                break;
            default:
                //  未知步骤,归零重来
                step = 0;
                break;
        }
    } else {
        return;
    }
}

可以看到原本 do_a 我们将它看作一个完整不可分割的逻辑,执行完整个 do_a 才会退出,而现在我们将其拆分成了3个步骤,每执行完一个步骤就会退出 do_a 函数,直到下一次进入才会执行下一个步骤,这样一来就能有效缩短一次 do_a 执行的时间,从而大大降低其一次执行时间会超过所有逻辑中最小周期的可能性。主循环中其他应用逻辑也和 do_a 一样,利用更加细分的状态机模式来加快主循环的响应效率,进一步提高了裸机编程的稳定性和时间可控性。

状态机的加入也使得裸机编程走向了其终极形态,使其能够处理更加复杂的逻辑与应用,与此同时,其代码量和复杂度也极速上升,尤其是当你的主循环中有十几个甚至几十个任务逻辑,此时你就会面临地狱级的编程难度。

当然,即使你能够接受地狱级挑战,最终也仍然会遇到一个问题 —— 随着应用逻辑的增多,同一时间执行了大量的状态机分支步骤,这些步骤仅凭人工已经很难再进行拆分了,并且很不幸,它们执行时间的总和超过了预定的周期,最终导致了各种各样的问题。

此时恭喜你,已经走到了裸机编程的巅峰,同时也是裸机编程的尽头。是时候迈开脚步,走向操作系统编程这条路了!

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【JavaEE Spring 项目】消息队列的设计

消息队列的设计 一、消息队列的背景知识二、需求分析核心概念⼀个⽣产者, ⼀个消费者N 个⽣产者, N 个消费者Broker Server 中的相关概念核⼼ API交换机类型 (Exchange Type)持久化⽹络通信消息应答 三、 模块划分四、 项⽬创建五、创建核心类创建 Exchange创建 MSGQUeue创建 B…

C语言数据结构基础笔记——树、二叉树简介

1.树 树是一种 非线性 的数据结构,它是由 n ( n>0 )个有限结点组成一个具有层次关系的集合。 把它叫做树是因 为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。 (图片来源于网络)…

计算机考研|王道四本书够吗?

如果你是跨考生,王道的四本书只能覆盖你需要的80% 如果你是计算机专业的考生,王道四本书可以覆盖你需要的90% 我已经说的很明显了,王道的内容覆盖不了408考研的全部大纲,有的知识点虽然在王道书上提到了,但是因为不是…

拿捏指针(二)

个人主页:秋邱博客 所属栏目:C语言 (感谢您的光临,您的光临蓬荜生辉) 目录 前言 数组与指针 数组名的理解 指针数组与数组指针 指针数组 数组指针 数组传参 一维数组传参的本质 二维数组传参的本质 二维数组…

【数据结构与算法】:选择排序与快速排序

🔥个人主页: Quitecoder 🔥专栏:数据结构与算法 我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:腾讯云 欢迎来到排序的第二个部分:选择排序与快速排序! 目录 1.选择排序1.…

【网站项目】325企业OA管理系统

🙊作者简介:拥有多年开发工作经验,分享技术代码帮助学生学习,独立完成自己的项目或者毕业设计。 代码可以私聊博主获取。🌹赠送计算机毕业设计600个选题excel文件,帮助大学选题。赠送开题报告模板&#xff…

SpringBoot启动后出现Please sign in页面

1. 问题 项目启动后&#xff0c;出现莫名其妙的页面&#xff0c;如下 2. 原因 当您启动 Spring Web 应用程序后出现 “Please sign in” 页面时&#xff0c;这通常是由于引用依赖Spring Security默认的身份验证方式导致的。 <dependency><groupId>org.springfr…

Element 选择季度组件

<template><el-dialogtitle"选择季度":show-close"false":close-on-click-modal"false":close-on-press-escape"false":visible"visiable"class"dialog list"append-to-body><div><div>&…

如何本地搭建hMailServer邮件服务

文章目录 前言1. 安装hMailServer2. 设置hMailServer3. 客户端安装添加账号4. 测试发送邮件5. 安装cpolar6. 创建公网地址7. 测试远程发送邮件8. 固定连接公网地址9. 测试固定远程地址发送邮件 前言 hMailServer 是一个邮件服务器,通过它我们可以搭建自己的邮件服务,通过cpola…

51单片机基础篇系列-定时/计数器的控制工作方式

&#x1f308;个人主页&#xff1a;会编程的果子君 &#x1f4ab;个人格言:“成为自己未来的主人~” 定时/计数器的控制 80C51单片机定时/计数器的工作由两个特殊功能寄存器控制&#xff0c;TMOD用于设置其工作方式&#xff1a; 1.工作方式寄存器TMOD 工作方式寄存器TMO…

C++的类和对象(七):友元、内部类

目录 友元 友元函数 友元类 内部类 匿名对象 拷贝对象时的一些编译器优化 再次理解类和对象 友元 基本概念&#xff1a;友元提供了一种突破封装的方式&#xff0c;有时提供了便利&#xff0c;但是友元会增加耦合度&#xff0c;破坏了封装&#xff0c;所以友元不宜多用&…

AXI CANFD MicroBlaze 测试笔记

文章目录 前言测试用的硬件连接Vivado 配置Vitis MicroBlaze CANFD 代码测试代码测试截图Github Link 前言 官网: CAN with Flexible Data Rate (CAN FD) (xilinx.com) 特征: 支持8Mb/s的CANFD多达 3 个数据位发送器延迟补偿(TDC, transmitter delay compensation)32-deep T…

Jenkins 面试题及答案整理,最新面试题

Jenkins中如何实现持续集成与持续部署&#xff1f; Jenkins通过自动化构建、测试和部署应用程序来实现持续集成与持续部署&#xff08;CI/CD&#xff09;。这个过程包括以下步骤&#xff1a; 1、源代码管理&#xff1a; Jenkins支持与多种版本控制系统集成&#xff0c;如Git、…

数据结构 之 优先级队列(堆) (PriorityQueue)

&#x1f389;欢迎大家观看AUGENSTERN_dc的文章(o゜▽゜)o☆✨✨ &#x1f389;感谢各位读者在百忙之中抽出时间来垂阅我的文章&#xff0c;我会尽我所能向的大家分享我的知识和经验&#x1f4d6; &#x1f389;希望我们在一篇篇的文章中能够共同进步&#xff01;&#xff01;&…

oops-framework框架 之 启动流程(三)

引擎&#xff1a; CocosCreator 3.8.0 环境&#xff1a; Mac Gitee: oops-game-kit 回顾 上篇博客中我们通过 oops-game-kit 模版构建了基础的项目&#xff0c;另外讲解了下assets目录结构和游戏配置文件的基本使用相关&#xff0c;详情内容可参考&#xff1a; oops-framewo…

mysql5.7离线安装 windows

windows上离线安装mysql5.7 下载安装包 去官网下载对应版本的mysql官网 点击archives,接着选择自己要下载的版本&#xff0c;选择windows系统&#xff0c;并根据自己电脑的位数选择相应的版本【找到“此电脑”&#xff0c;鼠标右击&#xff0c;出来下拉框&#xff0c;选择“属性…

频率响应概述与波特图

频率响应的定义 在放大电路中&#xff0c;存在电抗元件&#xff08;如电容、电感&#xff09;、半导体管&#xff08;存在极间电容&#xff09;。由于电抗元件和极间电容的存在&#xff0c;当输入信号频率过高或过低时&#xff0c;不但放大倍数的数值会减小&#xff0c;而且将…

Python 3.x 快速安装 pip 包管理工具

目录 ℹ️ 1. 查看是否安装 pip1.1 方法一1.2 方法二 &#x1f6e0;️ 2. 安装方法2.1 通过 ensurepip 进行安装2.2 通过 get-pip.py 进行安装 参考文档&#xff1a; pip 官方安装文档&#xff1a;https://pip.pypa.io/en/stable/installation/ ℹ️ 1. 查看是否安装 pip 【…

最详细数据仓库项目实现:从0到1的电商数仓建设(数仓部分)

1、数仓 数据仓库是一个为数据分析而设计的企业级数据管理系统&#xff0c;它是一个系统&#xff0c;不是一个框架。可以独立运行的&#xff0c;不需要你参与&#xff0c;只要运行起来就可以自己运行。 数据仓库不是为了存储&#xff08;但是能存&#xff09;&#xff0c;而是…

hcia复习总结7

1&#xff0c;AR2发送2.0网段的信息给AR1&#xff0c;如果&#xff0c;AR1本身并不存在该网段的路由 信息&#xff0c;则将直接 刷新 到本地的路由表中。 Destination/Mask Proto Pre Cost Flags NextHop Interface 2.2.2.0/24 RIP 100…