第2章_freeRTOS入门与工程实践之单片机程序设计模式

本教程基于韦东山百问网出的 DShanMCU-F103开发板 进行编写,需要的同学可以在这里获取: https://item.taobao.com/item.htm?id=724601559592

配套资料获取:https://rtos.100ask.net/zh/freeRTOS/DShanMCU-F103

freeRTOS系列教程之freeRTOS入门与工程实践章节汇总: https://blog.csdn.net/qq_35181236/article/details/132842016


第2章 单片机程序设计模式

本章目标

  • 理解裸机程序设计模式
  • 了解多任务系统中程序设计的不同

2.1 裸机程序设计模式

裸机程序的设计模式可以分为:轮询、前后台、定时器驱动、基于状态机。前面三种方法都无法解决一个问题:假设有A、B两个都很耗时的函数,无法降低它们相互之间的影响。第4种方法可以解决这个问题,但是实践起来有难度。

假设一位职场妈妈需要同时解决2个问题:给小孩喂饭、回复工作信息,场景如图所示,后面将会演示各类模式下如何写程序:

2.1.1 轮询模式

示例代码如下:

// 经典单片机程序: 轮询
void main()
{while (1){喂一口饭();回一个信息();}
}

在main函数中是一个while循环,里面依次调用2个函数,这两个函数相互之间有影响:如果“喂一口饭”太花时间,就会导致迟迟无法“回一个信息”;如果“回一个信息”太花时间,就会导致迟迟无法“喂下一口饭”。

使用轮询模式编写程序看起来很简单,但是要求while循环里调用到的函数要执行得非常快,在复杂场景里反而增加了编程难度。

2.1.1 前后台

所谓“前后台”就是使用中断程序。假设收到同事发来的信息时,电脑会发出“滴”的一声,这时候妈妈才需要去回复信息。示例程序如下:

// 前后台程序
void main()
{while (1){// 后台程序喂一口饭();}
}// 前台程序
void 滴_中断()
{回一个信息();
}
  • main函数里while循环里的代码是后台程序,平时都是while循环在运行;
  • 当同事发来信息,电脑发出“滴”的一声,触发了中断。妈妈暂停喂饭,去执行“滴_中断”给同事回复信息;

在这个场景里,给同事回复信息非常及时:即使正在喂饭也会暂停下来去回复信息。“喂一口饭”无法影响到“回一个信息”。但是,如果“回一个信息”太花时间,就会导致 “喂一口饭”迟迟无法执行。

继续改进,假设小孩吞下饭菜后会发出“啊”的一声,妈妈听到后才会喂下一口饭。喂饭、回复信息都是使用中断函数来处理。示例程序如下:

// 前后台程序
void main()
{while (1){// 后台程序}
}// 前台程序
void 滴_中断()
{回一个信息();
}// 前台程序
void 啊_中断()
{喂一口饭();
}

main函数中的while循环是空的,程序的运行靠中断来驱使。如果电脑声音“滴”、小孩声音“啊”不会同时、相近发出,那么“回一个信息”、“喂一口饭”相互之间没有影响。在不能满足这个前提的情况下,比如“滴”、“啊”同时响起,先“回一个信息”时就会耽误“喂一口饭”,这种场景下程序遭遇到了轮询模式的缺点:函数相互之间有影响。

2.1.2 定时器驱动

定时器驱动模式,是前后台模式的一种,可以按照不用的频率执行各种函数。比如需要每2分钟给小孩喂一口饭,需要每5分钟给同事回复信息。那么就可以启动一个定时器,让它每1分钟产生一次中断,让中断函数在合适的时间调用对应函数。示例代码如下:

// 前后台程序: 定时器驱动
void main()
{while (1){// 后台程序}
}// 前台程序: 每1分钟触发一次中断
void 定时器_中断()
{static int cnt = 0;cnt++;if (cnt % 2 == 0){喂一口饭();}else if (cnt % 5 == 0){回一个信息();}
}
  • main函数中的while循环是空的,程序的运行靠定时器中断来驱使。
  • 定时器中断每1分钟发生一次,在中断函数里让cnt变量累加(代码第14行)。
  • 第15行:进行求模运算,如果对2取模为0,就“喂一口饭”。这相当于每发生2次中断就“喂一口饭”。
  • 第19行:进行求模运算,如果对5取模为0,就“回一个信息”。这相当于每发生5次中断就“回一个信息”。

这种模式适合调用周期性的函数,并且每一个函数执行的时间不能超过一个定时器周期。如果“喂一口饭”很花时间,比如长达10分钟,那么就会耽误“回一个信息”;反过来也是一样的,如果“回一个信息”很花时间也会影响到“喂一口饭”;这种场景下程序遭遇到了轮询模式的缺点:函数相互之间有影响。

2.1.3 基于状态机

当“喂一口饭”、“回一个信息”都需要花很长的时间,无论使用前面的哪种设计模式,都会退化到轮询模式的缺点:函数相互之间有影响。可以使用状态机来解决这个缺点,示例代码如下:

// 状态机
void main()
{while (1){喂一口饭();回一个信息();}
}

在main函数里,还是使用轮询模式依次调用2个函数。

关键在于这2个函数的内部实现:使用状态机,每次只执行一个状态的代码,减少每次执行的时间,代码如下:

void 喂一口饭(void)
{static int state = 0;switch (state){case 0:{/* 舀饭 *//* 进入下一个状态 */state++;break;}case 1:{/* 喂饭 *//* 进入下一个状态 */state++;break;}case 2:{/* 舀菜 *//* 进入下一个状态 */state++;break;}case 3:{/* 喂菜 *//* 恢复到初始状态 */state = 0;break;}}
}void 回一个信息(void)
{static int state = 0;switch (state){case 0:{/* 查看信息 *//* 进入下一个状态 */state++;break;}case 1:{/* 打字 *//* 进入下一个状态 */state++;break;}case 2:{/* 发送 *//* 恢复到初始状态 */state = 0;break;}}
}

以“喂一口饭”为例,函数内部拆分为4个状态:舀饭、喂饭、舀菜、喂菜。每次执行“喂一口饭”函数时,都只会执行其中的某一状态对应的代码。以前执行一次“喂一口饭”函数可能需要4秒钟,现在可能只需要1秒钟,就降低了对后面“回一个信息”的影响。

同样的,“回一个信息”函数内部也被拆分为3个状态:查看信息、打字、发送。每次执行这个函数时,都只是执行其中一小部分代码,降低了对“喂一口饭”的影响。

使用状态机模式,可以解决裸机程序的难题:假设有A、B两个都很耗时的函数,怎样降低它们相互之间的影响。但是很多场景里,函数A、B并不容易拆分为多个状态,并且这些状态执行的时间并不好控制。所以这并不是最优的解决方法,需要使用多任务系统。

2.2 多任务系统

2.2.1 多任务模式

对于裸机程序,无论使用哪种模式进行精心的设计,在最差的情况下都无法解决这个问题:假设有A、B两个都很耗时的函数,无法降低它们相互之间的影响。使用状态机模式时,如果函数拆分得不好,也会导致这个问题。本质原因是:函数是轮流执行的。假设“喂一口饭”需要t1t5这5段时间,“回一个信息需要”tate这5段时间,轮流执行时:先执行完t1t5,再执行tate,如下图所示:

对于职场妈妈,她怎么解决这个问题呢?她是一个眼明手快的人,可以一心多用,她这样做:

  • 左手拿勺子,给小孩喂饭
  • 右手敲键盘,回复同事
  • 两不耽误,小孩“以为”妈妈在专心喂饭,同事“以为”她在专心聊天
  • 但是脑子只有一个啊,虽然说“一心多用”,但是谁能同时思考两件事?
  • 只是她反应快,上一秒钟在考虑夹哪个菜给小孩,下一秒钟考虑给同事回复什么信息
  • 本质是:交叉执行,t1t5和tate交叉执行,如下图所示:

基于多任务系统编写程序时,示例代码如下:

// RTOS程序
喂饭任务()
{while (1){喂一口饭();}
}回信息任务()
{while (1){回一个信息();}
}void main()
{// 创建2个任务create_task(喂饭任务);create_task(回信息任务);// 启动调度器start_scheduler();
}
  • 第21、22行,创建2个任务;
  • 第25行,启动调度器;
  • 之后,这2个任务就会交叉执行了;

基于多任务系统编写程序时,反而更简单了:

  1. 上面第2~8行是“喂饭任务”的代码;
  2. 第10~16行是“回信息任务”的代码,编写它们时甚至都不需要考虑它和其他函数的相互影响。就好像有2个单板:一个只运行“喂饭任务”这个函数、另一个只运行“回信息任务”这个函数。

多任务系统会依次给这些任务分配时间:你执行一会,我执行一会,如此循环。只要切换的间隔足够短,用户会“感觉这些任务在同时运行”。如下图所示:

2.2.2 互斥操作

多任务系统中,多个任务可能会“同时”访问某些资源,需要增加保护措施以防止混乱。比如任务A、B都要使用串口,能否使用一个全局变量让它们独占地、互斥地使用串口?示例代码如下:

// RTOS程序
int g_canuse = 1;void uart_print(char *str)
{if (g_canuse){g_canuse = 0;printf(str);g_canuse = 1;}
}task_A()
{while (1){uart_print("0123456789\n");}
}task_B()
{while (1){uart_print("abcdefghij");}
}void main()
{// 创建2个任务create_task(task_A);create_task(task_B);// 启动调度器start_scheduler();
}

程序的意图是:task_A打印“0123456789”,task_B打印“abcdefghij”。在task_A或task_B打印的过程中,另一个任务不能打印,以避免数字、字母混杂在一起,比如避免打印这样的字符:“012abc”。

第6行使用全局变量g_canuse实现互斥打印,它等于1时表示“可以打印”。在进行实际打印之前,先把g_canuse设置为0,目的是防止别的任务也来打印。

这个程序大部分时间是没问题的,但是只要它运行的时间足够长,就会出现数字、字母混杂的情况。下图把uart_print函数标记为①~④个步骤:

void uart_print(char *str)
{if( g_canuse ){g_canuse = 0;printf(str);   ③g_canuse = 1;}
}

如果task_A执行完①,进入if语句里面执行②之前被切换为task_B:在这一瞬间,g_canuse还是1。

task_B执行①时也会成功进入if语句,假设它执行到③,在printf打印完部分字符比如“abc”后又再次被切换为task_A。

task_A继续从上次被暂停的地方继续执行,即从②那里继续执行,成功打印出“0123456789”。这时在串口上可以看到打印的结果为:“abc0123456789”。

是不是“①判断”、“②清零”间隔太远了,uart_print函数改进成如下的代码呢?

void uart_print(char *str)
{g_canuse--;            ① 减一if( g_canuse == 0 )    ② 判断{printf(str);     ③ 打印}g_canuse++;          ④ 加一
}

即使改进为上述代码,仍然可能产生两个任务同时使用串口的情况。因为“①减一”这个操作会分为3个步骤:a.从内存读取变量的值放入寄存器里,b.修改寄存器的值让它减一,c.把寄存器的值写到内存上的变量上去。

如果task_A执行完步骤a、b,还没来得及把新值写到内存的变量里,就被切换为task_B:在这一瞬间,g_canuse还是1。

task_B执行①②时也会成功进入if语句,假设它执行到③,在printf打印完部分字符比如“abc”后又再次被切换为task_A。

task_A继续从上次被暂停的地方继续执行,即从步骤c那里继续执行,成功打印出“0123456789”。这时在串口上可以看到打印的结果为:“abc0123456789”。

从上面的例子可以看到,基于多任务系统编写程序时,访问公用的资源的时候要考虑“互斥操作”。任何一种多任务系统都会提供相应的函数。

2.2.3 同步操作

如果任务之间有依赖关系,比如任务A执行了某个操作之后,需要任务B进行后续的处理。如果代码如下编写的话,任务B大部分时间做的都是无用功。

// RTOS程序
int flag = 0;void task_A()
{while (1){// 做某些复杂的事情// 完成后把flag设置为1flag = 1;}
}void task_B()
{while (1){if (flag){// 做后续的操作}}
}void main()
{// 创建2个任务create_task(task_A);create_task(task_B);// 启动调度器start_scheduler();
}

上述代码中,在任务A没有设置flag为1之前,任务B的代码都只是去判断flag。而任务A、B的函数是依次轮流运行的,假设系统运行了100秒,其中任务A总共运行了50秒,任务B总共运行了50秒,任务A在努力处理复杂的运算,任务B仅仅是浪费CPU资源。

如果可以让任务B阻塞,即让任务B不参与调度,那么任务A就可以独占CPU资源加快处理复杂的事情。当任务A处理完事情后,再唤醒任务B。示例代码如下:

// RTOS程序
void task_A()
{while (1){// 做某些复杂的事情// 释放信号量,会唤醒任务B;}
}void task_B()
{while (1){// 等待信号量, 会让任务B阻塞// 做后续的操作}
}void main()
{// 创建2个任务create_task(task_A);create_task(task_B);// 启动调度器start_scheduler();
}
  • 第15行:任务B运行时,等待信号量,不成功时就会阻塞,不在参与任务调度。
  • 第7行:任务A处理完复杂的事情后,释放信号量会唤醒任务B。
  • 第16行:任务B被唤醒后,从这里继续运行。

在这个过程中,任务A处理复杂事情的时候可以独占CPU资源,加快处理速度。


本章完

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

AMS爆炸来袭,上线即巅峰

1.关于首发项目Antmons(AMS)空投结果 Gate.io Startup 首发项目Antmons代币AMS于Aug15th,AM 07:00开始下单,24小时内下单同等对待总共有15,950人下单,下单总价值超过1,000万美金分发系数约为0.001640495298341。根据上线规则AMS项目认购成功,…

刷一下算法

记录下自己的思路与能理解的解法,可能并不是最优解法,不定期持续更新~ 1.盛最多水的容器 给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。 找出其中的两条线,使得它们与 x 轴共同构成的容器可以容…

linux查看进程对应的线程(数)

首先,top或ps查看进程列表,确定要查看的进程pid,如下面40698 查看进程的线程情况 查看进程:top -p 40698 查看线程:top -p 40698 -d 3 -H 其中-d是刷新频率 可看到此进程共211个线程,运行中的是211个。…

QStandardItem通过setCheckable添加复选框后无法再次通过setCheckable取消复选框的问题

前言 如题所示&#xff0c;通过setCheckable添加复选框后&#xff0c;想要通过setCheckable(false)取消复选框&#xff0c;你会发现根本没有作用的。 问题复现 #include "widget.h" #include "ui_widget.h" #include <QList>Widget::Widget(QWidg…

DockerCompose

DockerCompose Docker Compose可以基于Compose文件帮我们快速的部署分布式应用&#xff0c;而无需手动一个个创建和运行容器&#xff01; 初识DockerCompose Compose文件是一个文本文件&#xff0c;通过指令定义集群中的每个容器如何运行。格式如下&#xff1a; version: &…

Spring注解家族介绍: @RequestMapping

前言&#xff1a; 今天我们来介绍RequestMapping这个注解&#xff0c;这个注解的内容相对来讲比较少&#xff0c;篇幅会比较短。 目录 前言&#xff1a; RequestMapping 应用场景&#xff1a; 总结&#xff1a; RequestMapping RequestMapping 是一个用于映射 HTTP 请求…

Jprofiler的使用查看oom

一、安装 idea安装插件 安装客户端 链接 IDEA配置Jprofiler执行文件 二、产生oom import java.util.ArrayList; import java.util.List;//测试代码 public class TestHeap {public static void main(String[] args) {int num 0;List<Heap> list new ArrayList&l…

超详细!魔改为中文sqlmap的使用教程

超详细&#xff01;魔改为中文sqlmap的使用教程 1.SQLMAP介绍2.Qitian基础信息3.Qitian绕过WAF 魔改了一版中文版的SQLMAP&#xff0c;如下图&#xff1a; 1.SQLMAP介绍 Sqlmap是一个自动化的sql注入工具&#xff0c;其主要功能是扫描、发现并利用给定url的SQL注入漏洞&#…

【SpringMVC】JSR303与拦截器的使用

文章目录 一、JSR3031.1 JSR303是什么1.2 JSR 303的好处包括1.3 常用注解1.4 实例1.4.1 导入JSR303依赖1.4.2 规则配置1.4.3 编写校验方法1.4.4 编写前端 二、拦截器2.1 拦截器是什么2.2 拦截器与过滤器的区别2.3.应用场景2.4 快速入门2.5.拦截器链2.6 登录拦截权限案例2.6.1 L…

Docker搭建ELK日志采集服务及Kibana可视化图表展示

架构 ES docker network create elkmkdir -p /opt/ELK/es/datachmod 777 /opt/ELK/esdocker run -d --name elasticsearch --net elk -p 9200:9200 -p 9300:9300 -e "discovery.typesingle-node" -v /opt/ELK/es/plugins:/usr/share/elasticsearch/plugins -v /opt/…

蓝桥杯2023年第十四届省赛真题-更小的数--题解

目录 蓝桥杯2023年第十四届省赛真题-更小的数 题目描述 输入格式 输出格式 样例输入 样例输出 提示 【思路解析】 【代码实现】 蓝桥杯2023年第十四届省赛真题-更小的数 时间限制: 3s 内存限制: 320MB 提交: 895 解决: 303 题目描述 小蓝有一个长度均为 n 且仅由数字…

RK3588 点亮imx586摄像头

一.硬件原理图 mipi摄像头硬件确认点&#xff1a; 1.供电&#xff1a;5V&#xff0c;2.8V&#xff0c;1.2V&#xff0c;1.8V&#xff0c;reset脚&#xff08;硬拉3.3&#xff0c;上电的时候从低到高&#xff09;&#xff0c;pwron脚外接 3.3V。 2,时钟&#xff1a;MCLKOUT是2…

Linux 文件创建、查看

touch、cat、more命令 ①touch命令——创建文件 ②cat命令——查看文件内容全部显示 这是txt.txt文件内容 使用cat命令查看 ③more命令——查看文件内容支持翻页 在查看的过程中&#xff0c;通过空格翻页&#xff0c;通过q退出查看

基于Xml方式Bean的配置-初始化方法和销毁方法

SpringBean的配置详解 Bean的初始化和销毁方法配置 Bean在被实例化后&#xff0c;可以执行指定的初始化方法完成一些初始化的操作&#xff0c;Bean在销毁之前也可以执行指定的销毁方法完成一些操作&#xff0c;初始化方法名称和销毁方法名称通过 <bean id"userService…

linux万字图文学习进程信号

1. 信号概念 信号是进程之间事件异步通知的一种方式&#xff0c;属于软中断。 1.1 linux中我们常用Ctrlc来杀死一个前台进程 1. Ctrl-C 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。2. S…

微服务架构介绍

系统架构的演变 1、技术架构发展历史时间轴 ①单机垂直拆分&#xff1a;应用间进行了解耦&#xff0c;系统容错提高了&#xff0c;也解决了独立应用发布的问题&#xff0c;存在单机计算能力瓶颈。 ②集群化负载均衡可有效解决单机情况下并发量不足瓶颈。 ③服务改造架构 虽然系…

解决Springboot使用Junit测试时对数据库的修改无效

现象 在使用Junit做单元测试的过程中&#xff0c;比如对mybatis的dao进行单元测试&#xff0c;发现对数据库的select操作正常&#xff0c;可以获取数据&#xff0c;但insert、update、delete操作即使运行不报错&#xff0c;仍然不能不能对数据产生修改和插入。 原因和解决 原…

FreeSWITCH 1.10.10 简单图形化界面10 - 鼎兴FXO网关SIP注册公网IPPBX落地

FreeSWITCH 1.10.10 简单图形化界面10 - 鼎兴FXO网关SIP注册公网IPPBX落地 0、 界面预览1、创建一个话务台2、创建PBX SIP中继并设置呼入权限3、设置呼出规则4、设置分机呼出权限5、设置FXO 网关相关信息6、设置FXO网关端口组呼入号码7、设置FXO网关呼叫路由&#xff08;呼入及…

千兆以太网硬件设计及链路层 MAC 协议格式

以太网系列文章&#xff1a; &#xff08;1&#xff09;千兆以太网硬件设计及链路层 MAC 协议格式 &#xff08;2&#xff09;千兆以太网网络层 ARP 协议的原理与 FPGA 实现 &#xff08;3&#xff09;CRC校验代码原理 文章目录 前言一、以太网 MAC 层接口介绍1.MII 接口2.GMII…

进制转换问题

进制 二进制 &#xff08;Binary&#xff09;&#xff1a;0、1。简写为B 八进制&#xff08;Octonary&#xff09;&#xff1a;0、1、2、3、4、5、6、7。简写为O 十进制&#xff08;decimalism&#xff09;&#xff1a;0、1、2、3、4、5、6、7、8、9 简写为D 十六进制&#xff…