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

news2024/12/28 10:05:21

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

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

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 一样,利用更加细分的状态机模式来加快主循环的响应效率,进一步提高了裸机编程的稳定性和时间可控性。

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

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

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

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

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

相关文章

2024RKDC,新一代AIOT 处理器RK3576发布 !

触觉智能已成功推出RK3576相关开发板核心板,RK3576采用瑞芯微八核芯片,专为 AI0I设计,可用于平板电脑、AI0T应用程序、电子墨水显示器、Arm PC和汽车电子中。集成独立的6TOPS NPU,支持4K视频编解码,性能定位于RK3588和…

包装类 --java学习笔记

包装类 包装类就是把基本类型的数据包装成对象 基本数据类型与其包装类: 将整型数据包装成对象: 自动装箱:可以自动把基本类型的数据转换成对象 例:Interger a3 12; 自动拆箱:可以自动把包装类型的对象…

##天气预报爬虫 项目

//主要功能 #include "head.h" #include "cJSON.h"void FunWeather(void); void RecvSendWeather(void); int RealTimeWeather(void); int CreateTcpClient(char *p,int port); int SendHttpRequest(int sockfd,char *q); void RecvSendWeather(void);char…

Socket通信Demo(Unity客户端和C#)

新建一个Unity项目新建脚本编写客户端 using System.Net.Sockets; using System.Net; using System; using System.Text;public class Client : MonoBehaviour {private Socket socket;//定义用来存消息的容器private byte[] buffer new byte[1024];// Start is called befor…

国际妇女节 | 竹云董事长董宁在第五届大湾区木棉论坛主题演讲

3月7日-8日,由深圳市妇女联合会指导,深商会联合党委主办的第五届大湾区木棉论坛在深商报告厅圆满举行。 本次论坛以“木棉盛开”为主题,邀请深圳市妇联党组书记、主席刘蕾,深圳市霖峰投资董事长王秀娥,深圳市国富黄金股…

速卖通商品采集API:关键字搜索商品item_search、获取商品详情item_get

item_get-获得aliexpress商品详情 item_search-按关键字搜索aliexpress商品 公共参数 请求地址: aliexpress.item_search/aliexpress.item_get 名称类型必须描述keyString是调用key(必须以GET方式拼接在URL中)secretString是调用密钥api_nameString是…

Buran勒索病毒通过Microsoft Excel Web查询文件进行传播

Buran勒索病毒首次出现在2019年5月,是一款新型的基于RaaS模式进行传播的新型勒索病毒,在一个著名的俄罗斯论坛中进行销售,与其他基于RaaS勒索病毒(如GandCrab)获得30%-40%的收入不同,Buran勒索病毒的作者仅占感染产生的25%的收入,…

Unity的滑动控制相机跟随和第三人称视角三

Unity的相机跟随和第三人称视角三 第三人称相机优化介绍讲解拖动事件相机逻辑人物移动逻辑总结 第三人称相机优化 Unity第三人称相机视角一 Unity第三人称相机视角二 介绍 之前相机视角讲过了两篇文章了,但是都是自动旋转视角,今天来了新需求&#xf…

FPGA高端项目:FPGA基于GS2971的SDI视频接收+HLS多路视频融合叠加,提供1套工程源码和技术支持

目录 1、前言免责声明 2、相关方案推荐本博已有的 SDI 编解码方案本方案的SDI接收转HDMI输出应用本方案的SDI接收图像缩放应用本方案的SDI接收纯verilog图像缩放纯verilog多路视频拼接应用本方案的SDI接收HLS图像缩放Video Mixer多路视频拼接应用本方案的SDI接收OSD动态字符叠加…

Day23:安全开发-PHP应用后台模块SessionCookieToken身份验证唯一性

目录 具体安全知识点 身份验证-Cookie使用 身份验证-Session使用 唯一性判断-Token使用 总结 源码 思维导图 PHP知识点: 功能:新闻列表,会员中心,资源下载,留言版,后台模块,模版引用&…

Java 拦截器Interceptor详解

1、拦截器概念 先看一下 3、代码示例 4、使用类解读 5、使用注解解读 6、与filter区别 7、优化策略 8、常见问题分析 学海无涯苦作舟!!!

无尘擦拭布:保持清洁,确保品质

在现代工业和科技领域中,保持工作环境的清洁和设备的无尘是至关重要的。无尘布作为一种重要的清洁工具,在这一领域发挥着不可或缺的作用。它具有一系列优秀的特性,使其成为各种行业中的*选之一。 优秀的除尘效果与防静电功能 首先&#xff0…

Jupyter Notebook使用教程——从Anaconda环境构建到Markdown、LaTex语法介绍

0. 前言 按照国际惯例,首先声明:本文只是我自己学习的理解,虽然参考了他人的宝贵见解及成果,但是内容可能存在不准确的地方。如果发现文中错误,希望批评指正,共同进步。 你是否在视频教程或说明文档或Githu…

ejs模版引擎使用

ejs express渲染页面需要借助ejs官网传送门使用流程 安装ejs引擎 npm i ejs -S 在app.js入口文件里面设置模版引擎和目录 const path require(path)// 设置项目里面模版存放位置app.set(views, path.join(__dirname, views))// 设置模版引擎app.set(view engine, ejs)在项…

重学SpringBoot3-路径匹配机制

重学SpringBoot3-路径匹配机制 AntPathMatcherPathPatternParser 和 PathPattern演示AntPathMatcher 示例PathPattern 示例性能和精确度的提升 选择使用哪一种 在 Spring Framework 5.3 及 Spring Boot 2.4 之后,引入了一种新的路径匹配机制,这一变化在 …

Buildroot 之一 详解源码及架构

在之前的博文中,我们学习了直接通过 Makefile 手动来进行构建 U-Boot 和 Linux Kernel 等,其实,目前存在多种嵌入式 Linux 环境的构建工具,其中,Buildroot 就是被广泛应用的一种。今天就来详细学习一个 Buildroot 这个自动化构建工具。 Buildroot Buildroot 是一个运行于…

福派斯课堂:怎样选择品质比较好的猫粮?

亲爱的猫友们,我们都知道,猫咪的健康离不开好的饮食,而猫粮作为它们日常的主食,选择一款品质优良的猫粮就显得尤为重要了。那么,如何选择品质比较好的猫粮呢?今天,就让我来给大家支支招吧&#…

PaddlePaddle框架安装

提示:可在python环境中进行安装,避免环境污染,创建命令conda create -n xxx_name python3.9,激活conda activate xxx_name 第一步:查看计算机平台版本 在窗口输入查看命令,查看CUDA的版本 nvidia-smi 二、根据以下条件…

Redis主从架构和管道Lua(一)

Redis主从架构 架构 Redis主从工作原理 如果为master配置了一个slave,不管这个slave是否是第一次连接上Master,它都会发送一个PSYNC命令给master请求复制数据。master受到PSYNC命令,会在后台进行数据持久化通过bgsave生成最新的 RDB快照文件,持久化期间…

Linux之线程控制

目录 一、POSIX线程库 二、线程的创建 三、线程等待 四、线程终止 五、分离线程 六、线程ID:pthread_t 1、获取线程ID 2、pthread_t 七、线程局部存储:__thread 一、POSIX线程库 由于Linux下的线程并没有独立特有的结构,所以Linux并…