第1章_瑞萨MCU零基础入门系列教程之单片机程序的设计模式

news2024/12/28 20:32:05

本教程基于韦东山百问网出的 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 个问题:给小孩喂饭、回复工作信息,场景如图所示,

后面将会演示各类模式下如何写程序:

image1

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,如下图所示:

image2

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

样做:

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

image3

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

// 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 个单板:一个只运行“喂饭任务”这个函数、另一个只运行“回信息任务”这个函数。

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

image4

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 设置为 1
        flag = 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.coloradmin.cn/o/995153.html

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

相关文章

华为云云耀云服务器L实例评测|安装Java8环境 配置环境变量 spring项目部署 【!】存在问题未解决

目录 引出安装JDK8环境查看是否有默认jar上传Linux版本的jar包解压压缩包配置环境变量 上传jar包以及运行问题上传Jar包运行控制台开放端口访问失败—见问题记录关闭Jar的方式1.进程kill -92.ctrl c退出 问题记录:【!】未解决各种方式查看端口情况联系工程师最后排查…

自学Python05-学会Python中的函数定义

亲爱的同学们,今天我们将开始学习 Python 中的函数。函数就像一个魔法盒子,可以让我们在程序中执行一段代码,并且可以反复使用。这样,我们的程序就可以变得更加简洁和易于理解。现在,让我们一起来学习如何使用函数吧&a…

无涯教程-JavaScript - OCT2HEX函数

描述 OCT2HEX函数将八进制数转换为十六进制。 语法 OCT2HEX (number, [places])争论 Argument描述Required/OptionalNumber 您要转换的八进制数。 数字不得超过10个八进制字符(30位)。数字的最高有效位是符号位。其余的29位是幅度位。 负数使用二进制补码表示。 RequiredPl…

报考浙江工业大学MBA项目如何选择合适的辅导班?

浙江工业大学MBA项目每年有数百人报考,在浙江省内除了浙大以外算是人数比较多的一个项目。2023级的招生中第一志愿也通过复试刷掉了百来人,在省内其实作为第一志愿报考的风险在逐渐增大,考生们如果坚持报考,则在针对联考初试的备考…

B站:AB Test 知识全解

AB Test的实质:假设检验,主要有以下几个步骤: 1、在实验开始前,找产品、项目经理等确认:实验需要验证的改动点(一次只能看一个!!!) 2、数据分析师设计需要去观…

day55 补

392.判断子序列 力扣题目链接(opens new window) 给定字符串 s 和 t ,判断 s 是否为 t 的子序列。 字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,&quo…

【MFC】Button控件美化(自绘)

在MFC中Button控件不能通过OnCtlColor()函数对外观做太多的改变。 HBRUSH C按钮控件自绘Dlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor) {HBRUSH hbr CDialogEx::OnCtlColor(pDC, pWnd, nCtlColor);switch (pWnd->GetDlgCtrlID()){case ID…

【已解决】ORA-00001: unique constraint (USR_JXZX_DSJKF_MODEL.SYS_C00912833) violated

已解决: nested exception is java.sql.SQLIntegrityConstraintViolationException: ORA-00001: unique constraint (USR_JXZX_DSJKF_MODEL.SYS_C00912833) violated 问题 学号主键ID重复了 思路 在 Oracle 中,可以使用以下方法找出重复的主键&#…

Vue中实现3D得球自动旋转

具体实现 安装echarts 在终端下安装echarts npm install -D echarts 安装echarts-gl 在终端下安装echarts-gl npm install -D echarts-gl earth3D组件 earth3D.vue <template><div class"globe3d-earth-container" ><div class"globe3d-earth&qu…

桉木做的建筑模板质量评价

桉木作为一种常见的建筑模板材料&#xff0c;以其独特的特性在建筑行业中得到广泛应用。然而&#xff0c;评估桉木建筑模板的质量是确保建筑项目成功的关键因素之一。本文将对桉木建筑模板的质量进行评价&#xff0c;从材料特性、强度和耐久性等方面进行分析。 首先&#xff0c…

Pinyin4j介绍和简单使用

前言 Pinyin4j是一个Java库&#xff0c;用于将汉字转换为拼音。它是由中国清华大学的Tsinghua University和中国科学院计算技术研究所的研究人员开发的。Pinyin4j可以用于Java应用程序中&#xff0c;以便在需要时将汉字转换为拼音。例如&#xff0c;它可以用于中文输入法、文本…

大模型tokenizer流式响应解决词句连贯性问题

大模型tokenizer词句连贯性问题 现象 from transformers import LlamaTokenizerFast import numpy as nptokenizer LlamaTokenizerFast.from_pretrained("heilerich/llama-tokenizer-fast") origin_prompt "Hi, Im Minwoo Park from seoul, korea." id…

使用 multiprocessing 多进程处理批量数据

示例代码 import multiprocessingdef process_data(data):# 这里是处理单个数据的过程return data * 2# 待处理的数据 data [1, 2, 3, 4, 5]def normal_func():# 普通处理方式result []for obj in data:result.append(process_data(obj)return resultdef parallel_func():# …

JavaScript编程语法作业

目录 目录 前言 思维导图 1&#xff0c;作业资源 2&#xff0c;if语句练习 2.1代码解读: 2.2,结果展示: 3&#xff0c;switch语句练习 3.1,代码解读: 3.2,结果展示: 4.while循环练习 4.1,代码解读: 4.2.结果展示: 5.do-while循环练习 5.1,代码解读: 5.2,结果展…

STM32 CAN/CANFD软件快速配置(HAL库版本)

STM32 CAN/CANFD软件快速配置&#xff08;HAL库版本&#xff09; 目录 STM32 CAN/CANFD软件快速配置&#xff08;HAL库版本&#xff09;前言1 软件编程1.1 建立工程1.2 初始化1.2.1 引脚设置1.2.2 CAN基本参数设置1.2.3 CAN收发初始化设置1.2.4 中断设置 1.3 CAN发送1.4 CAN接收…

自定义Dynamics 365实施和发布业务解决方案 3. 开箱即用自定义

在本章中,您将开始开发SBMA会员应用程序。在开发的最初阶段,主要关注开箱即用的定制。在第2章中,我们讨论了如何创建基本解决方案的细节,在本章中,将创建作为解决方案补丁的基本自定义,并展示将解决方案添加到源代码管理和目标环境的步骤。 表单自定义 若要开始表单自定…

连续 3 年 40% 增长 续费率近 110%:纷享销客增长的底层逻辑

经营管理没有一招鲜&#xff0c;持续增长的底层逻辑&#xff0c;就是持续稳定的建设&#xff0c;不被外界所干扰。 笔记整理 | 张保文 SaaS 增长&#xff0c;比拼的是战略、组织、人才、市场、销售、客户成功等全价值链的基本功。连续 3 年 40% 增长&#xff0c;续费率近 110%…

【图解RabbitMQ-5】RabbitMQ Web管控台图文介绍

&#x1f9d1;‍&#x1f4bb;作者名称&#xff1a;DaenCode &#x1f3a4;作者简介&#xff1a;CSDN实力新星&#xff0c;后端开发两年经验&#xff0c;曾担任甲方技术代表&#xff0c;业余独自创办智源恩创网络科技工作室。会点点Java相关技术栈、帆软报表、低代码平台快速开…

C++ 结构体

前文 C中的结构体是一种非常有用的数据类型&#xff0c;它允许我们将不同的变量组合在一起&#xff0c;形成一个自定义的数据结构。 结构体在C中的应用非常广泛&#xff0c;它可以用来表示和管理各种实体、对象或数据的属性。比如&#xff0c;在一个学生管理系统中&#xff0c…

2.2 Java中的变量

1. 变量与赋值 在程序运行期间&#xff0c;随时可能产生一些临时数据&#xff0c;应用程序会将这些数据保存在内存单元中&#xff0c;每个内存单元都用一个标识符标识&#xff0c;这些用于标识内存单元的标识符就称为变量&#xff0c;内存单元中存储的数据就是变量的值。 下面…