【问题标题】:How to achieve multitasking in a microcontroller?如何在微控制器中实现多任务处理?
【发布时间】:2014-07-30 12:00:14
【问题描述】:

我使用嵌入式 (C) 的 8051 微控制器编写了一个手表程序。总共有 6 个 7 段显示:

         _______________________
        |      |       |        |   two 7-segments for showing HOURS
        | HR   | MIN   |   SEC  |   two 7-segments for showing MINUTES and
        |______._______.________|   two 7-segments for showing SECONDS
          7-segment LED display

为了更新小时、分钟和秒,我们使用了 3 个for 循环。这意味着首先会更新秒数,然后是分钟数,然后是小时数。然后我问我的教授为什么我们不能同时更新(我的意思是一小时后增加小时而不等待分钟更新)。他告诉我,由于指令的顺序执行,我们不能进行并行处理。

问题:

一张数字生日贺卡,可在 LED 指示灯同时闪烁的同时连续播放音乐。数字闹钟会在特定时间发出哔哔声。在它发出声音的同时,时间会不断更新。所以声音和时间增量都是并行运行的。他们是如何通过顺序执行获得这些结果的?

如何在一个微控制器中同时运行多个任务(调度)?

【问题讨论】:

  • 你了解多任务和并行的区别吗? en.wikipedia.org/wiki/Computer_multitasking 。真正的并行意味着你有几个 cpu。多任务意味着单个(但不一定)cpu 可以在不同任务之间快速切换。因此,您可以在任何系统上实现多任务处理(更容易,更难)
  • 现在,我的问题有效吗? @btolfa
  • 有人可以查看建议的编辑队列吗?我相信它可以解决这个问题。
  • 根据 8051 的具体型号,可用于 RAM(数据)的实际内存可能很少,而用于 PROM(程序)的固定区域很小。 8051还有一个非常小的堆栈区域(某些型号有一个可以专用于堆栈的一页区域(256字节)。)使用timer0进行基本的心跳。在 timer0 中断处理程序中,跟踪自上电以来经过的时间。检查某些操作功能之间的所需间隔是否已过期。如果是这样,如果设置了该函数的标志,则错误,否则,为该函数设置标志。
  • 在主循环中,如果设置了特定的函数标志,则执行该函数,重置标志。从函数返回时,重新启动标志检查。按照最常运行到最不常运行的顺序检查标志。在后台函数中(在没有其他内容运行时运行),启用/禁用跨关键代码部分的中断,尤其是对于 BIT 操作。初始化函数在复位时运行,以设置 I/O 并执行上电 BIT。

标签: c concurrency parallel-processing embedded


【解决方案1】:

首先,这个顺序执行是怎么回事。只有一个内核、一个程序空间、一个计数器。 MPU 一次执行一条指令,然后按顺序移动到另一条指令。在这个系统中,没有固有的机制让它停止做一件事并开始做另一件事——这都是一个程序,完全掌握在程序员手中,顺序是什么,它将做什么;只要 MPU 正在运行,它将持续不间断,一次一条指令,并且不会发生其他任何事情,除非程序员首先让它发生。

现在,多任务处理:

通常,操作系统提供多任务处理,具有相当复杂的调度算法。

通常,微控制器在没有操作系统的情况下运行。

那么,如何在微控制器中实现多任务处理?

简单的答案是“你不知道”。但与往常一样,简单的答案很少涵盖超过 5% 的情况......

编写真正的抢先式多任务处理会非常困难。大多数微控制器都没有这样的功能,而英特尔 CPU 通过几个特定指令执行的操作需要您编写数英里的代码。最好忘记微控制器的经典多任务处理,除非您真的无事可做。

现在,有两种常用的方法经常被使用,而且麻烦得多。

中断

大多数微控制器都有不同的中断源,通常包括定时器。因此,主循环连续运行一个任务,当定时器计数为零时,发出中断。主循环停止,执行跳转到称为“中断向量”的地址。在那里,启动了不同的程序,执行不同的一次性任务。一旦完成(如果需要,可能重置计时器),您从中断返回并恢复主循环。

微控制器通常有几个计时器,您可以为每个计时器分配一个任务,更不用说其他外部中断(例如,键盘输入 - 按键或通过 RS232 到达的数据)上的任务。

虽然这种方法非常有限,但对于大多数情况来说确实足够了;特别是您的:将定时器设置为循环 1 秒,在中断时计算新小时,更改显示,然后离开中断。在主循环中等待日期到达生日,然后它开始播放音乐并闪烁 LED。

协作式多任务处理

这就是早期的做法。您需要将您的“任务”编写为子程序,每个子程序内部都有一个有限状态机(或单次循环),“OS”是一个简单的循环,按顺序跳转到连续任务。

在每次跳转后,MPU 开始执行给定的任务,并将继续执行,直到任务返回控制权,先保存其状态,然后在再次启动时恢复它。任务作业的每次传递都应该很短。任何延迟循环都必须在有限状态引擎中替换为等待状态(如果条件不满足,则返回。如果满足,则更改状态。)所有较长的循环必须展开为不同的状态(“状态:复制数据块,复制字节N,增加N,N=end?yes:下一个状态,no:返回控制)

这样写更困难,但解决方案更健壮。在您的情况下,您可能有四个任务:

  • 时钟
  • 显示更新
  • 播放声音
  • 闪烁 LED

如果没有新的秒到达,时钟返回控制。如果是,它会重新计算秒数、分钟数、小时数、日期,然后返回。

Display 更新显示的值。如果您在 8 段显示器上多路复用数字,则每次通过将更新一个数字,下一次通过 - 下一个等。

在不是生日时播放声音会等待(让步)。如果是生日,从内存中选择样本值,输出到扬声器,yield。如果您在应该输出下一个声音之前被调用,则可以选择屈服。

闪烁 - 好吧,将正确的状态输出到 LED,yield。

仍然允许非常短的循环(例如,5 行的 10 次迭代),但任何更长的循环都应转换为进程所在的有限状态引擎的状态。

现在,如果你觉得硬核,你可以试试……

抢先式多任务处理。

每个任务都是一个通常会无限执行的过程,只做自己的事情。正常编写,尽量不踩到其他程序的内存,而是使用资源,就好像世界上没有其他东西需要它们一样。

您的操作系统任务从定时器中断启动。

在被中断启动后,OS 任务必须保存上一个任务的所有当前易失状态 - 寄存器、中断返回地址(应从中恢复任务)、当前堆栈指针,并将其保存在那个任务。

然后,使用调度程序算法,它从列表中选择另一个进程,该进程应该现在开始;恢复其所有状态,然后用先前被抢占时该进程停止的地址覆盖自己的从中断返回地址。在中断结束后,被抢占进程的正常操作被恢复,直到另一个中断再次将控制权切换到操作系统。

如您所见,保存和恢复程序的完整状态而不是当前任务所需的状态有很多开销,但程序不需要编写为有限状态机- 正常的顺序样式就足够了。

【讨论】:

    【解决方案2】:

    虽然 SF 提供了对多任务处理的出色概述,但大多数微控制器还拥有一些额外的硬件,可以让它们同时处理事情。

    同时执行的错觉 - 从技术上讲,您的教授是正确的,不能同时更新。但是,处理器非常快。对于许多任务,它们可以按顺序执行,例如一次更新每个 7 段显示器,但速度如此之快,以至于人类无法判断每个显示器是按顺序更新的。这同样适用于声音。大多数可听声音在千赫范围内,而处理器在兆赫范围内运行。处理器有足够的时间播放声音的一部分,做其他事情,然后在您的耳朵无法察觉差异的情况下返回播放声音。

    中断 - SF 很好地涵盖了中断的执行,所以我将掩盖机制并更多地讨论硬件。大多数微控制器具有与指令执行同时运行的小型硬件模块。定时器、UARTS 和 SPI 是在处理器的主要部分执行指令时执行特定操作的常见模块。当给定模块完成其任务时,它会通知处理器并且处理器跳转到该模块的中断代码。这种机制允许您在执行指令时执行诸如通过 uart 传输一个字节(相对较慢)之类的事情。

    PWM - PWM(脉冲宽度调制)是一个硬件模块,它本质上是产生一个方波,一次两个,但正方形不必是偶数(我在这里简化) .一个可能比另一个长,或者它们的大小相同。您在硬件​​中配置正方形的大小,然后 PWM 连续生成它们。该模块可用于驱动电机甚至产生声音,其中电机的速度或声音的频率取决于两个平方的比率。为了播放音乐,处理器只需要在需要改变音符时(可能基于定时器中断)改变比率,同时它可以执行其他指令。

    DMA - DMA(直接内存访问)是一种特定类型的硬件,可自动将字节从一个内存位置复制到另一个内存位置。像 ADC 这样的东西可能会不断地将转换后的值写入内存中的特定寄存器。可以将 DMA 控制器配置为从一个地址(ADC 输出)连续读取,同时按顺序写入一系列存储器(例如在平均之前接收多个 ADC 转换的缓冲区)。所有这些都发生在硬件中,而主处理器执行指令。

    定时器、UART、SPI、ADC 等 - 还有许多其他硬件模块(这里不一一介绍)在执行程序的同时执行特定任务。

    TL/DR - 虽然程序指令只能按顺序执行,但处理器通常可以足够快地执行它们,以至于它们看起来是同时发生的。同时,大多数微控制器都有额外的硬件,可以在执行程序的同时完成特定任务。

    【讨论】:

      【解决方案3】:

      ZackSF. 的回答很好地涵盖了全局。但有时一个可行的例子很有价值。

      虽然我可以轻松地建议将源工具包浏览到 Linux 内核(它既是开源的,甚至在单核机器上也提供多任务处理),但这并不是了解如何实际实现调度程序。

      一个更好的起点是使用数百个(如果不是数千个)实时操作系统之一的源工具包。其中许多是开源的,大多数甚至可以在极小的处理器上运行,包括 8051。我将在这里更详细地描述 Micrium 的 uC/OS-II,因为它具有一组典型的特性,而且它是我的已经广泛使用。我过去评估过的其他产品包括 OS-9、eCos 和 FreeRTOS。以这些名称为起点,再加上“RTOS”等关键字,Google 将奖励您许多其他名称。

      我第一次接触 RTOS 内核是uC/OS-II(或其较新的家族成员 uC/OS-III)。这是一款商业产品,最初是作为嵌入式系统设计杂志读者的教育练习。杂志文章及其随附的源代码成为该主题的更好书籍之一的主题。该操作系统是开源的,但对商业用途有许可限制。为了披露,我是 uC/OS-II 到 ColdFire MCF5307 端口的作者。

      由于它最初是作为教育工具编写的,因此源代码有据可查。这本教科书(至少在我书架上的第 2 版)也写得很好,并且对它支持的每个功能都有很多理论背景。

      我在多个产品开发项目中成功使用了它,并且会在需要多任务处理但不需要像 Linux 那样承载完整操作系统重量的项目中再次考虑使用它。

      uC/OS-II 提供抢占式任务调度器,以及有用的任务间通信原语(信号量、互斥体、邮箱、消息队列)、计时器和线程安全池内存分配器的集合。

      它还支持任务优先级,如果使用正确,还可以防止死锁。

      它完全用标准 C 的子集编写(满足 MISRA-C:1998 指南的几乎所有要求),这有助于它获得各种安全关键认证。

      虽然我的应用程序从未在安全关键系统中使用,但很高兴知道我所在的操作系统内核已达到这些评级。它保证了我遇到错误的最可能原因是对原语如何工作的误解,或者更有可能实际上是我的应用程序逻辑中的错误。

      大多数 RTOS(尤其是 uC/OS-II)能够在有限的资源中运行。 uC/OS-II 可以构建在低至 6KB 的代码中,而 OS 结构所需的 RAM 则低至 1KB。

      底线是可以通过多种方式实现表观并发,其中一种方式是使用 OS 内核,该内核旨在通过在所有系统之间共享顺序 CPU 的资源来并行调度和执行每个并发任务任务。对于简单的情况,您可能只需要中断处理程序和一个主循环,但是当您的需求增长到实现多个协议、管理显示、管理用户输入、后台计算和监控整体系统运行状况时,站在井上- 设计的 RTOS 内核以及已知可工作的通信原语可以节省大量的开发和调试工作。

      【讨论】:

        【解决方案4】:

        嗯,我看到其他答案涵盖了很多领域;所以,希望我最终不会把它变成比我想要的更大的东西。 (TL;DR:拯救女孩!:D)。但是,我确实(我认为)提供了一个非常好的解决方案;所以我希望你能利用它。我对 8051[☆] 的经验很少;尽管我确实在另一个微控制器上工作了大约 3 个月(加上大约 3 个全职),但取得了一定的成功。在此过程中,我几乎完成了这个小东西必须提供的所有功能:串行通信、SPI、PWM 信号、伺服控制、DIO、热电偶等等。当我在研究它时,我很幸运地遇到了一个出色的(IMO)解决方案,用于(合作)“线程”调度,它与一些由 PIC 上的中断完成的少量额外实时工作混合得很好。当然,还有其他设备的其他中断处理程序。

        pt_thread: 由 Adam Dunkels(与 Oliver Schmidt)发明(v1.0 于 2005 年 2 月发布),他的网站是对它们的一个很好的介绍,wand 包括从 2006 年 10 月到 v1.4 的下载;我很高兴再次去看,因为我找到了;但是 2009 年 1 月的一篇文章指出,Larry Ruane 使用了事件驱动技术“[用于] [使用 GCC;并使用] 非常好的语法的完整重新实现”and available on sourceforge。不幸的是,自 2009 年左右以来,两者似乎都没有更新。但 2006 年的版本非常适合我。最后一条新闻(从 2009 年 12 月开始)指出,“Sonic Unleashed”在其手册中指出使用了 protothreads!

        认为 pt_threads 很棒的一件事是它们非常简单;而且,无论更新的(Ruane)版本有什么好处,它肯定更复杂。尽管可能值得一看,但我将在这里坚持使用 Dunkels 的原始实现。他原来的pt_threads“库”包括:五个头文件。而且,真的,这似乎有点夸大其词,因为一旦我缩小了一些宏和其他东西,删除了 doxygen 部分,示例,并将 cmets 剔除到最低限度,我仍然觉得给出了一个解释,它大约在115 行(包括在下面。)

        源 tarball 中包含一些示例,他的网站上提供了非常好的 .pdf 文档(或 .html)(上面链接)。但是,让我通过一个快速示例来阐明一些概念。 (不是宏本身,我花了一些时间来理解这些,而且它们并不是仅仅为了使用该功能所必需的。:D)

        很遗憾,我今晚没时间了;但我会在明天的某个时候重新开始写一个小例子;无论哪种方式,他的网站上都有 ton 的资源,链接在上面;这是一个相当简单的过程,对我来说棘手的部分(因为我想它与任何协作多线程一样;Win 3.1 有人吗?:D)是确保我对代码进行了正确的循环计数,以免超出时间我需要在产生 pt_thread 之前处理下一件事。

        我希望这能给你一个开始;如果您尝试一下,请告诉我结果如何!

        文件: pt.h
        #ifndef __PT_H__ #define __PT_H__ #包括“lc.h” // 注意:枚举是我的压缩空间;最初都是#defines 枚举 PT_STATUS_ENUM { PT_WAITING, PT_YIELDED, PT_EXITED, PT_ENDED }; 结构 pt { lc_t lc; } // protothread 控制结构(pt_thread) #define PT_INIT(pt) LC_INIT((pt)->lc) // 使用前初始化pt_thread // 你可以使用它来声明 pt_thread 函数 #define PT_THREAD(name_args) char name_args // 注意:看这个,我想我可能会定义自己的宏如下,以免 // 每次都必须重新声明 struct pt *pt。 //#define PT_DECLARE(name, args) char name(struct pt *pt, args) // 开始/结束 pt_thread (内部实现 fn);必须始终配对 #define PT_BEGIN(pt) { char PT_YIELD_FLAG = 1; LC_RESUME((pt)->lc) #define PT_END(pt) LC_END((pt)->lc);PT_YIELD_FLAG = 0;PT_INIT(pt);返回 PT_ENDED;} // {block, yield} 'pt' {until,while} 'c' 为真 #define PT_WAIT_UNTIL(pt,c) 做 { \ LC_SET((pt)->lc); if(!(c)) {return PT_WAITING;} \ } 而(0) #define PT_WAIT_WHILE(pt, cond) PT_WAIT_UNTIL((pt), !(cond)) #define PT_YIELD_UNTIL(pt, cond) \ 做{ PT_YIELD_FLAG = 0; LC_SET((pt)->lc); \ if((PT_YIELD_FLAG == 0) || !(cond)) { return PT_YIELDED; } } 而(0) // 注意:不存在对应的“YIELD_WHILE”;监督? [贝壳蝴蝶] //#define PT_YIELD_WHILE(pt,cond) PT_YIELD_UNTIL((pt), !(cond)) // 阻塞 pt_thread 'pt',等待子 'thread' 完成 #define PT_WAIT_THREAD(pt, thread) PT_WAIT_WHILE((pt), PT_SCHEDULE(thread)) // 生成 pt_thread 'ch' 作为 'pt' 的子线程,等待 'thr' 退出 #define PT_SPAWN(pt,ch,thr) 做 { \ PT_INIT((孩子)); PT_WAIT_THREAD((pt),(thread)); } 而(0) // 阻塞并导致 pt_thread 在其 PT_BEGIN() 处重新开始执行 #define PT_RESTART(pt) 做 { PT_INIT(pt);返回 PT_WAITING; } 而(0) // 退出 pt_thread;如果是孩子,则父母将取消阻止并运行 #define PT_EXIT(pt) 做 { PT_INIT(pt);返回 PT_EXITED; } 而(0) // 调度 pt_thread: fn ret != 0 如果 pt 正在运行,或者 0 如果退出 #define PT_SCHEDULE(f) ((f) lc); \ if(PT_YIELD_FLAG == 0) { 返回 PT_YIELDED; } } 而(0)


        文件: lc.h
        #ifndef __LC_H__ #define __LC_H__ #ifdef LC_INCLUDE #include LC_INCLUDE #别的 #include "lc-switch.h" #endif /* LC_INCLUDE */ #endif /* __LC_H__ */


        文件: lc-switch.h
        // 警告:使用 switch() 的实现不能与 switch() 中的 LC_SET() 一起使用! #ifndef __LC_SWITCH_H__ #define __LC_SWITCH_H__ typedef 无符号短 lc_t; #define LC_INIT(s) s = 0; #define LC_RESUME(s) switch(s) { case 0: #define LC_SET(s) s = __LINE__;案例__LINE__: #define LC_END(s) } #endif /* __LC_SWITCH_H__ */


        文件: lc-addrlabels.h
        #ifndef __LC_ADDRLABELS_H__ #define __LC_ADDRLABELS_H__ 类型定义无效* lc_t; #define LC_INIT(s) s = NULL #define LC_RESUME(s) do { if(s != NULL) { goto *s; } } 而(0) #define LC_CONCAT2(s1, s2) s1##s2 #define LC_CONCAT(s1, s2) LC_CONCAT2(s1, s2) #define LC_END(s) #define LC_SET(s) \ 做 {LC_CONCAT(LC_LABEL, __LINE__):(s)=&&LC_CONCAT(LC_LABEL,__LINE__);} while(0) #endif /* __LC_ADDRLABELS_H__ */


        文件: pt-sem.h
        #ifndef __PT_SEM_H__ #define __PT_SEM_H__ #include "pt.h" struct pt_sem { 无符号整数计数; }; // 用于初始化、等待和发出 pt_sem 信号量的宏 #define PT_SEM_INIT(s, c) (s)->count = c #define PT_SEM_WAIT(pt, s) 做\ { PT_WAIT_UNTIL(pt, (s)->count > 0); -(s)->计数; } 而(0) #define PT_SEM_SIGNAL(pt, s) ++(s)->count #endif /* __PT_SEM_H__ */


        [☆]  *大约一周学习微控制器[†]并在评估期间使用它一周,看看它是否符合我们的要求需要一个小的线路可更换远程 I/O 单元。 (长话短说:没有)

        [†]  The 8051 Microcontroller, Third Edition *被建议给我作为 8051 编程“圣经”我不知道是不是,但是我当然能够使用它来解决问题。[‡]

        [‡]  甚至现在再看一遍,我也没有什么不喜欢的地方。 :) 好吧,我的意思是……我希望我没有买两本;但他们是如此便宜!

        许可协议(如适用)
        这篇文章包含基于(或取自)“Protothreads 库”(本文及以下简称“PTLIB”)的代码; 包括 v1.4 和更早的版本)广泛依赖源代码以及 PTLIB 的文档。 PTLIB 原始源代码和文档来自作者的 PTLIB 站点,可免费下载 'http://dunkels.com/adam/pt/',可通过下载页面上的链接获得'http://dunkels.com/adam/pt/download.html' 或直接通过“http://dunkels.com/adam/download/pt-1.4.tar.gz”。 这篇文章由原始文本组成,我特此给你(带着爱!)在完全放弃任何版权的情况下 我可能有兴趣,使用以下术语:“copyheart ♥ 2014,雪莱蝴蝶,与爱分享!”;或者,如果您愿意, 适用于材料的完全非限制性、仅限署名的许可(例如用于软件的 Apache 2.0;或 CC-BY 文本许可),以便您可以根据需要使用它,使其最适合您的需要。 这篇文章还包含源代码,几乎完全是通过删除解释性材料从原始源创建的, 重新格式化和解释内联文档/cmets,以及我的一些修改/添加 (stackexchange 网络上的 shelleybutterfly)。我可能合法获得的任何 PTLIB 衍生品 版权或其他利益,我特此将所有此类利益和原作品的所有版权利益归还给 遵循本协议的 PTLIB 许可中指定的原始版权所有者。 在任何司法管辖区,无论出于何种原因,上述条款都不可能适用于您,那么,无论出于何种原因 我对材料感兴趣,我特此根据您的任何非限制性、仅归属的许可向您提供 选择;或者,如果这也不可能,那么我允许 stack exchange inc 将其提供给您 您确定在您的管辖范围内可以接受的任何条款。 来自 PTLIB 的所有源代码,以及 PTLIB 的派生代码,上面详述的其他条款未涵盖的所有源代码 根据以下协议,特此提供给“stack exchange inc”和您:


        “The Protothreads Library”许可协议
        版权所有 (c) 2004-2005,瑞典计算机科学研究所。版权所有。 允许以源代码和二进制形式重新分发和使用,无论是否修改,前提是 满足以下条件: 1. 源代码的再分发必须保留上述版权声明、本条件列表和以下内容 免责声明。 2. 以二进制形式重新分发必须复制上述版权声明、此条件列表和以下内容 随分发提供的文档和/或其他材料中的免责声明。 3. 研究所的名称或其贡献者的姓名均不得用于认可或推广衍生产品 未经事先书面许可,不得使用本软件。
        本软件由研究所和贡献者“按原样”提供,并提供任何明示或暗示的保证,包括: 但不限于,对适销性和特定用途适用性的默示保证不予承认。没有 事件机构或贡献者应对任何直接的、间接的、偶然的、特殊的、示例性的或 间接损害(包括但不限于采购替代商品或服务;使用、数据或 利润;或业务中断)无论是由何种原因引起的,以及基于任何责任理论,无论是合同责任、严格责任、 或以任何方式因使用本软件而引起的侵权(包括疏忽或其他),即使已被告知 此类损坏的可能性。
        作者:亚当·邓克斯

        【讨论】:

        • 由于这些源是对现有源文件的改编,您应该说明它们是根据哪些版权许可提供的以及谁拥有代码的版权。
        • facepalm 是的,我的错;我现在把它放进去; :)
        【解决方案5】:

        我对@9​​87654321@ 有过很好的体验,尽管它使用了相当多的内存。 Freertos 为您提供真正的抢先线程,如果您想升级那些尘土飞扬的旧 8051,有大量的端口,有信号量和消息队列和优先级以及各种东西,而且它是完全免费的。我个人只使用过 arduino 端口,但它似乎是最受欢迎的免费 rtosses 之一。

        我认为他们卖的书不是免费的,但在他们的网站和 arduino 示例中有足够的信息,几乎可以弄清楚。

        【讨论】:

        • 您能否提供指向您提到的网站和书籍的链接。它使寻找答案的人更容易。
        • 添加到主站点的链接,这本书可以在那里找到。
        【解决方案6】:

        这里有一些非常好的答案,但是在深入了解更长的答案之前,关于您的生日贺卡示例的更多上下文可能是一个很好的线索。

        单个 cpu 似乎可以同时执行多项操作的方式是通过在任务之间快速切换,以及利用计时器、中断和独立硬件单元的帮助,这些单元可以独立于 cpu 执行操作。 (请参阅@Zack 的回答以获得很好的讨论和硬件入门列表)所以对于你的生日贺卡,cpu 可能会告诉一些音频硬件“播放这段声音”,然后让 LED 闪烁,然后回来加载在第一部分完成播放之前的下一点声音。在这种情况下,cpu 可能需要 1 毫秒的时间来加载可能会实时播放 5 毫秒的音频,而在加载下一个声音之前,您有 4 毫秒的时间来做其他事情。

        数字时钟可能会通过设置一些 PWM 硬件以某种频率输出到压电蜂鸣器、中断定时器以停止蜂鸣声,然后关闭并检查实时计数器以查看时间是否显示 LED 需要更新。当定时器触发中断时,您的代码会关闭 PWM。

        详细信息将根据芯片的硬件而有所不同,查看数据表是了解给定微控制器可能具有哪些功能以及如何访问它的方法。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2017-05-18
          • 1970-01-01
          • 2015-08-20
          • 1970-01-01
          • 1970-01-01
          • 2021-12-03
          • 1970-01-01
          相关资源
          最近更新 更多