第1章_瑞萨MCU零基础入门系列教程之单片机程序的设计模式
本教程基于韦东山百问网出的 DShanMCU-RA6M5开发板 进行编写,需要的同学可以在这里获取: https://item.taobao.com/item.htm?id=728461040949
配套资料获取:https://renesas-docs.100ask.net
瑞萨MCU零基础入门系列教程汇总: https://blog.csdn.net/qq_35181236/article/details/132779862
第1章 单片机程序的设计模式
本章目标
- 理解裸机程序设计模式
- 了解多任务系统中程序设计的不同
1.1 裸机程序设计模式
裸机程序的设计模式可以分为:轮询、前后台、定时器驱动、基于状态机。前面三种方法都无法解决一个问题:假设有 A、B 两个都很耗时的函数,无法降低它们相互之间的影响。第 4 种方法可以解决这个问题,但是实践起来有难度。
假设一位职场妈妈需要同时解决 2 个问题:给小孩喂饭、回复工作信息,场景如图所示,
后面将会演示各类模式下如何写程序:

1.1.1 轮询模式
示例代码如下:
// 经典单片机程序: 轮询void main(){while (1){喂一口饭();回一个信息();}}
在 main 函数中是一个 while 循环,里面依次调用 2 个函数,这两个函数相互之间有影响:如果“喂一口饭”太花时间,就会导致迟迟无法“回一个信息”;如果“回一个信息”太花时间,就会导致迟迟无法“喂下一口饭”。
使用轮询模式编写程序看起来很简单,但是要求 while 循环里调用到的函数要执行得非常快,在复杂场景里反而增加了编程难度。
1.1.2 前后台
所谓“前后台”就是使用中断程序。假设收到同事发来的信息时,电脑会发出“滴”的一声,这时候妈妈才需要去回复信息。示例程序如下:
// 前后台程序void main(){while (1){// 后台程序喂一口饭();}}// 前台程序
void 滴_中断()
{回一个信息();
}
- main 函数里 while 循环里的代码是后台程序,平时都是 while 循环在运行;
- 当同事发来信息,电脑发出“滴”的一声,触发了中断。妈妈暂停喂饭,去执行“滴_中断”给同事回复信息;
在这个场景里,给同事回复信息非常及时:即使正在喂饭也会暂停下来去回复信息。“喂一口饭”无法影响到“回一个信息”。但是,如果“回一个信息”太花时间,就会导致 “喂一口饭”迟迟无法执行。
继续改进,假设小孩吞下饭菜后会发出“啊”的一声,妈妈听到后才会喂下一口饭。喂饭、回复信息都是使用中断函数来处理。示例程序如下:
// 前后台程序
void main()
{while (1){// 后台程序}
}// 前台程序
void 滴_中断()
{回一个信息();
}// 前台程序
void 啊_中断()
{喂一口饭();
}
main 函数中的 while 循环是空的,程序的运行靠中断来驱使。如果电脑声音“滴”、小孩声音“啊”不会同时、相近发出,那么“回一个信息”、“喂一口饭”相互之间没有影响。在不能满足这个前提的情况下,比如“滴”、“啊”同时响起,先“回一个信息”时就会耽误“喂一口饭”,这种场景下程序遭遇到了轮询模式的缺点:函数相互之间有影响。
1.1.3 定时器驱动
定时器驱动模式,是前后台模式的一种,可以按照不用的频率执行各种函数。比如需要每 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 分钟,那么就会耽误“回一个信息”;反过来也是一样的,如果“回一个信息”很花时间也会影响到“喂一口饭”;这种场景下程序遭遇到了轮询模式的缺点:函数相互之间有影响。
1.1.4 基于状态机
当“喂一口饭”、“回一个信息”都需要花很长的时间,无论使用前面的哪种设计模式,都会退化到轮询模式的缺点:函数相互之间有影响。可以使用状态机来解决这个缺点,示例代码如下:
// 状态机
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 2:{/* 喂菜 *//* 恢复到初始状态 */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 并不容易拆分为多个状态,并且这些状态执行的时间并不好控制。所以这并不是最优的解决方法,需要使用多任务系统。
1.2 多任务系统
1.2.1 多任务模式
对于裸机程序,无论使用哪种模式进行精心的设计,在最差的情况下都无法解决这个问题:假设有 A、B 两个都很耗时的函数,无法降低它们相互之间的影响。使用状态机模式时,如果函数拆分得不好,也会导致这个问题。本质原因是:函数是轮流执行的。假设“喂一口饭”需要 t1~t5 这 5 段时间,“回一个信息需要”ta~te 这 5 段时间,轮流执行时:先执行完 t1~t5,再执行 ta~te,如下图所示:

对于职场妈妈,她怎么解决这个问题呢?她是一个眼明手快的人,可以一心多用,她这
样做:
- 左手拿勺子,给小孩喂饭
- 右手敲键盘,回复同事
- 两不耽误,小孩“以为”妈妈在专心喂饭,同事“以为”她在专心聊天
- 但是脑子只有一个啊,虽然说“一心多用”,但是谁能同时思考两件事?
- 只是她反应快,上一秒钟在考虑夹哪个菜给小孩,下一秒钟考虑给同事回复什么信息
- 本质是:交叉执行,t1~t5 和 ta~te 交叉执行,如下图所示:

基于多任务系统编写程序时,示例代码如下:
// RTOS 程序
喂饭任务()
{while (1){喂一口饭();}
}回信息任务()
{while (1){回一个信息();}
}void main()
{// 创建 2 个任务create_task(喂饭任务);create_task(回信息任务);// 启动调度器start_scheduler();
}
- 第 21、22 行,创建 2 个任务;
- 第 25 行,启动调度器;
- 之后,这 2 个任务就会交叉执行了;
基于多任务系统编写程序时,反而更简单了:
- 上面第 2~8 行是“喂饭任务”的代码;
- 第 10~16 行是“回信息任务”的代码,编写它们时甚至都不需要考虑它和其他函数的相互影响。就好像有 2 个单板:一个只运行“喂饭任务”这个函数、另一个只运行“回信息任务”这个函数。
多任务系统会依次给这些任务分配时间:你执行一会,我执行一会,如此循环。只要切换的间隔足够短,用户会“感觉这些任务在同时运行”。如下图所示:

1.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”。
从上面的例子可以看到,基于多任务系统编写程序时,访问公用的资源的时候要考虑“互斥操作”。任何一种多任务系统都会提供相应的函数。
1.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 资源,加快处理速度。
相关文章:
第1章_瑞萨MCU零基础入门系列教程之单片机程序的设计模式
本教程基于韦东山百问网出的 DShanMCU-RA6M5开发板 进行编写,需要的同学可以在这里获取: https://item.taobao.com/item.htm?id728461040949 配套资料获取:https://renesas-docs.100ask.net 瑞萨MCU零基础入门系列教程汇总: ht…...
【UE】刀光粒子效果——part2 材质函数部分
效果 步骤 1. 新建一个材质函数,这里命名为“MF_TextureCommon” 2. 新建一个材质,这里命名为“Mat_GuangBan1”,添加如下节点 3. 接下来将该材质的逻辑添加到材质函数上,复制材质“Mat_GuangBan1”中的如下节点,粘贴…...
为什么项目经理的时间观念这么重?
项目经理的时间观念强是因为项目管理涉及到时间、成本和质量的平衡。 项目经理需要按时按质地交付项目,这不仅关乎项目本身的质量和进度,还关乎团队的士气和客户的满意度。 在项目管理过程中,存在大量的时间浪费现象,也可以把它…...
编码转换流
同理,创建f1和f2方法,分别测试OutputStreamWriter和InputStreamReader 也是主要分三步,即1创建流 2使用流 3关流 OutputStreamWriter f1方法 因为要操作流,所以先创建一个try-catch-finally结构,创建流对象Out…...
Pycharm创建项目时如何自动添加头部信息
1.打开PyCharm,选择File--Settings 2.依次选择Editor---Code Style-- File and Code Templates---Python Script 3..添加头部内容 可以根据需要添加相应的信息 #!/usr/bin/python3可用的预定义文件模板变量为:$ {PROJECT_NAME} - 当前项目的名称。$ {NAM…...
DAY48
#ifndef QUEUE_H #define QUEUE_H#include<iostream>using namespace std;#define MAX 10typedef int datatype;template <typename T> class queue {T data[MAX];T front;T tail;public:queue();~queue();queue(const T &other);//创建循环队列T *queue_crea…...
光栅和矢量图像处理:Graphics Mill 11.4.1 Crack
Graphics Mill 是适用于 .NET 和 ASP.NET 开发人员的最强大的成像工具集。它允许用户轻松向 .NET 应用程序添加复杂的光栅和矢量图像处理功能。 光栅图形 加载和保存 JPEG、PNG 和另外 8 种图像格式 调整大小、裁剪、自动修复、色度键和 30 多种其他图像操作 可处理任何尺寸&am…...
vue3中组件没有被调用,没进去也没报错
在父页面引用了一个组件,然后父级调用子组件方法,但是根本没进去,也不报错 父级页面挂载组件 <!-- 视频插件组件 --> <div><VideoPluginView ref"video_perview_ref"></VideoPluginView> </div> …...
Postgresql中ParamListInfoData的作用
ParamListInfoData是参数的统一抽象,例如 在pl中执行raise notice %, n;n的值会拼成select n到SQL层取值,但值在哪呢,还是在pl层。对sql层来说,n的一种可能性是参数,在这种可能性中,n的数据放在ParamListI…...
《计算机视觉中的多视图几何》笔记(1)
1 Introduction – a Tour of Multiple View Geometry 本章介绍了本书的主要思想。 1.1 Introduction – the ubiquitous projective geometry 为了了解为什么我们需要射影几何,我们从熟悉的欧几里得几何开始。 欧几里得几何在二维中认为平行线是不会相交的&…...
YOLO目标检测——火焰检测数据集+已标注xml和txt格式标签下载分享
实际项目应用:火灾预警系统、智能监控系统、工业安全管理、森林火灾监测以及城市规划和消防设计等应用场景中具有广泛的应用潜力,可以提高火灾检测的准确性和效率,保障人员和财产的安全。数据集说明:YOLO火焰目标检测数据集&#…...
tkinter四大按钮:Button,Checkbutton, Radiobutton, Menubutton
文章目录 四大按钮Button连击MenubuttonCheckbuttonRadiobutton tkinter系列: GUI初步💎布局💎绑定变量💎绑定事件💎消息框💎文件对话框控件样式扫雷小游戏💎强行表白神器 四大按钮 tkinter中…...
Sudowrite:基于人工智能的AI写作文章生成工具
【 产品介绍】 名称 Sudowrite 成立/上线时间 2023年 具体描述 Sudowrite是一个基于GPT-3的人工智能写作工具,可以帮助你快速生成高质量的文本内容, 无论是小说、博客、营销文案还是学术论文。 Sudowrite可以根据你的输入和指…...
加密狗软件有什么作用?
加密狗软件是一种用于加密和保护计算机软件和数据的安全设备。它通常是一个硬件设备,可以通过USB接口连接到计算机上。加密狗软件的作用主要体现在以下几个方面: 软件保护:加密狗软件可以对软件进行加密和授权,防止未经授权的用户…...
嵌入式Linux驱动开发(I2C专题)(二)
I2C系统的重要结构体 参考资料: Linux驱动程序: drivers/i2c/i2c-dev.cI2CTools: https://mirrors.edge.kernel.org/pub/software/utils/i2c-tools/ 1. I2C硬件框架 2. I2C传输协议 3. Linux软件框架 4. 重要结构体 使用一句话概括I2C传输:APP通过I2…...
SMT贴片制造:发挥的作用和价值]
SMT贴片制造作为一项重要的电子制造技术,发挥着举足轻重的作用,并提供了巨大的价值。 首先,SMT贴片制造为电子产品的制造商提供了高效、准确和可靠的生产方式。相比于传统的手工焊接,SMT贴片制造具有更高的自动化和智能化程度&am…...
蓝桥杯官网练习题(幸运数字)
问题描述 小蓝认为如果一个数含有偶数个数位,并且前面一半的数位之和等于后面一半的数位之和,则这个数是他的幸运数字。例如 2314 是一个幸运数字, 因为它有 4 个数位, 并且 2314 。现在请你帮他计算从 1 至 100000000 之间共有多少个不同的幸运数字。 …...
pandas笔记:显示中间的省略号
比如我们有这样一个数据(Geolife中的数据) 如何把中间的省略号完整地输出呢? pd.set_option(display.max_rows, None) data...
解决vagrant安装的centos7,在window主机重装系统过后,再次用vagrant启动centos7却无法启动
场景: vagrant安装的centos7,在window主机重装系统过后,再次用vagrant启动centos7却无法启动 检查 VirtualBox 版本:确保你安装的 VirtualBox 版本与 Vagrant 兼容。如果你更新了 VirtualBox,可能需要同时更新 Vagran…...
嵌入式这个领域会变得过于内卷吗?
今日话题,嵌入式这个领域会变得过于内卷吗?嵌入式开发主要服务于第二产业,尤其是制造业,包括电器、电气、机械、汽车、装备、航空航天等行业的“智能制造”部门,稳定性较强,不像互联网行业那样波动大。因此…...
SkyWalking 10.2.0 SWCK 配置过程
SkyWalking 10.2.0 & SWCK 配置过程 skywalking oap-server & ui 使用Docker安装在K8S集群以外,K8S集群中的微服务使用initContainer按命名空间将skywalking-java-agent注入到业务容器中。 SWCK有整套的解决方案,全安装在K8S群集中。 具体可参…...
TDengine 快速体验(Docker 镜像方式)
简介 TDengine 可以通过安装包、Docker 镜像 及云服务快速体验 TDengine 的功能,本节首先介绍如何通过 Docker 快速体验 TDengine,然后介绍如何在 Docker 环境下体验 TDengine 的写入和查询功能。如果你不熟悉 Docker,请使用 安装包的方式快…...
用docker来安装部署freeswitch记录
今天刚才测试一个callcenter的项目,所以尝试安装freeswitch 1、使用轩辕镜像 - 中国开发者首选的专业 Docker 镜像加速服务平台 编辑下面/etc/docker/daemon.json文件为 {"registry-mirrors": ["https://docker.xuanyuan.me"] }同时可以进入轩…...
Xen Server服务器释放磁盘空间
disk.sh #!/bin/bashcd /run/sr-mount/e54f0646-ae11-0457-b64f-eba4673b824c # 全部虚拟机物理磁盘文件存储 a$(ls -l | awk {print $NF} | cut -d. -f1) # 使用中的虚拟机物理磁盘文件 b$(xe vm-disk-list --multiple | grep uuid | awk {print $NF})printf "%s\n"…...
Git 3天2K星标:Datawhale 的 Happy-LLM 项目介绍(附教程)
引言 在人工智能飞速发展的今天,大语言模型(Large Language Models, LLMs)已成为技术领域的焦点。从智能写作到代码生成,LLM 的应用场景不断扩展,深刻改变了我们的工作和生活方式。然而,理解这些模型的内部…...
uniapp 字符包含的相关方法
在uniapp中,如果你想检查一个字符串是否包含另一个子字符串,你可以使用JavaScript中的includes()方法或者indexOf()方法。这两种方法都可以达到目的,但它们在处理方式和返回值上有所不同。 使用includes()方法 includes()方法用于判断一个字…...
【Android】Android 开发 ADB 常用指令
查看当前连接的设备 adb devices 连接设备 adb connect 设备IP 断开已连接的设备 adb disconnect 设备IP 安装应用 adb install 安装包的路径 卸载应用 adb uninstall 应用包名 查看已安装的应用包名 adb shell pm list packages 查看已安装的第三方应用包名 adb shell pm list…...
MacOS下Homebrew国内镜像加速指南(2025最新国内镜像加速)
macos brew国内镜像加速方法 brew install 加速formula.jws.json下载慢加速 🍺 最新版brew安装慢到怀疑人生?别怕,教你轻松起飞! 最近Homebrew更新至最新版,每次执行 brew 命令时都会自动从官方地址 https://formulae.…...
关于easyexcel动态下拉选问题处理
前些日子突然碰到一个问题,说是客户的导入文件模版想支持部分导入内容的下拉选,于是我就找了easyexcel官网寻找解决方案,并没有找到合适的方案,没办法只能自己动手并分享出来,针对Java生成Excel下拉菜单时因选项过多导…...
Kubernetes 网络模型深度解析:Pod IP 与 Service 的负载均衡机制,Service到底是什么?
Pod IP 的本质与特性 Pod IP 的定位 纯端点地址:Pod IP 是分配给 Pod 网络命名空间的真实 IP 地址(如 10.244.1.2)无特殊名称:在 Kubernetes 中,它通常被称为 “Pod IP” 或 “容器 IP”生命周期:与 Pod …...
