掌上单片机实验室 - 程序框架设计(7)

一、背景

        前面已将掌上单片机实验室的硬件激活,并且推荐了相应的软件开发环境,在此基础上即可开始单片机的编程学习。

        单片机学习很大比例是编程的学习,即:如何将现实需求转换为程序语言,让单片机执行,从而解决现实世界的问题。

        而编程的最佳方式是构建一个良好的程序框架,以此为基础,再根据具体要解决的问题,增加相应的处理部分即可,这样程序的构建和调试会很高效。

        何为“良好的程序框架”?我认为至少要具备以下特征:

        1)程序逻辑关系清晰

        2)模块化,功能之间耦合度小

        3)具备可扩展性,功能增、减方便

        4)便于调试,具备调试信息输出能力

        5)具备一定的通用性和可移植性,可用于不同项目。

        为了能更高效使用掌上实验室学习,拟按上述特征设计一个程序框架,作为学习的基础。

二、需求

        程序框架应该实现:

        1)提供单片机控制常用的基础功能,如:标志信息传递、功能间数据信息交互等;

        2)提供基础的调试功能,目前单片机均包含串口,可方便的在任何位置输出调试信息,虽说好的IDE环境支持断点、单步等调试手段,但这样会过分依赖某款产品,产品选择面受限,自己在程序中根据需要输出调试信息更灵活,而且可以在非仿真环境下实现调试信息输出。对于小车类移动平台,借助于无线透传模块,还可以在运动过程中观察程序运行状况。

        3)实现对程序运行状况的监护功能,以便及时发现异常,并借助上述调试信息输出功能准确的确定故障发生的场景,为程序调试、为何提供方便。一般看门狗功能只能复位重启程序,无法输出当时的状况,况且多数故障不一定非要复位。

        4)具备串口通讯收发功能;目前嵌入式产品多数包含通讯功能,主流MCU一般都有2个以上的串口;将通讯作为框架的一部分,可为实现人机交互及测试提供方便。

三、设计思路

        以往单片机内存有限,特别是 RAM 资源,使用 C 编程都担心变量设置过多导致栈空间不足。

        在80C51年代,使用 OS 是无法企及的事,出了一个TinyOS,无奈功能太弱而未成气候。那时,单片机上编程只能自己构思框架,部分实现上述需求。

        随着 Cortex_M 系列单片机问世,单片机内存资源已经不是问题,RAM资源从 51 系列的 K 级别提升到几十K 级别,此时,基于 OS 编程成为可能。

        现在编程,再自己编写程序框架已不可取,应选择合适的RTOS构建程序框架,因为 OS 的核心就是解决上述需求中最基础的第一点。RTOS 是专业团队所为,考虑得比个人编程全面;它的功能是从很多行业的真实需求中提取的,尤其针对嵌入式控制的实时需求,做了很好的优化。

        单片机上所选择的是 RTOS,和Windows、linux这类OS(操作系统)不同,它侧重于系统事件响应的实时性,所以称为 RTOS —— Real Time Operating System。

        RTOS 的实时性侧重的不只是快,还有延时的确定性、统一性,这点很重要!自己编程是能实现所关注的事件很快响应,但长时间运行后就会发现有些事件严重滞后,而且是随机的,导致程序不稳定,甚至是出错。这就是个人能力有限所致。

        因此,程序框架的设计最好是以 RTOS 为基础!

        目前市面上 RTOS 有很多,作为学习一般选择免费的。可选的有 uCOS、FreeRTOS 以及国产的 RT-Thread(还有一些小众的暂不考虑)。

        uCOS 最大的优势是资料详实,因为是最早作为 RTOS 学习素材的,有详尽的代码分析。我自己学习 RTOS 就是基于 uCOSII(订阅号中另一个主题“机器人足球”有学习的过程)。但由于其只对非商业应用免费,导致其目前应用数量减少。

        FreeRTOS 是真正免费的,该团队虽被亚马逊收购,但保持了其完全免费的特征,故目前在嵌入式应用中选择较为普遍。关键是有很多环境将其整合,使用尤为方便,如:ST 公司在其 STM32Cube 中就集成了 FreeRTOS。

        前面所推荐的编程环境 STM32Duino (支持STM32的Arduino)也有 FreeRTOS 库可以方便使用,且在不断维护:

        掌上实验室的程序框架将基于 FreeRTOS 构建。

        RT-Thread 是针对物联网产品设计的,其优势是包含了大量的网络组件,而 FreeRTOS 的网络部分好像要收费。在初步学习编程的过程中,需要的主要是 OS 的内核调度功能,暂时还用不上网络协议。

        随着学习的深入,程序功能的增加,或许会引入 RT-Thread。

四、详细设计

        基于 RTOS 设计程序,核心是构建任务。RTOS是以多任务为核心,每个任务均为一个死循环,完成特定的功能。各任务的调度和任务间的信息传递,均由 OS 完成。

        程序构建过程实际上是:任务的设立和实现、任务间消息的设计。

        程序框架设计除完成前面的基础功能外,拟设计两个示意性的功能任务,为后续编写实际功能提供基础。

4.1 总体设计

        根据上述需求,程序框架设立如下任务

        1)串口命令接收:作为人机交互通道,代替以往通过按键实现的程序操控手段。操作命令的产生可以用PC、手机之类有丰富交互手段的设备,比实体按键更为灵活、直观、丰富。

        2)串口数据发送:作为人机交互通道,代替以往通过显示器实现的信息输出,同样可以用PC、手机之类的设备接收后显示,界面设计远比显示屏灵活、丰富、随心所欲。之所以将收、发分开,是考虑到输出信息需要服务于所有任务,以便功能设计更为合理。

        3)调试信息输出:因为使用 Arduino 环境编程,没有MDK之类的IDE那些调试手段,需要通过在程序中输出相应的调试信息,以实现 Debug 过程;由于输出调试信息的串口只有一个,多任务下会产生争抢,RTOS 的典型处理方式为加互斥锁,从而消除单一资源共享使用的问题。但互斥锁会导致输出等待,影响信息输出及程序运行的实时性,任务优先级还会导致信息输出顺序颠倒,降低信息输出的调试价值。设计一个任务专门处理各任务的信息输出,可以及时将各任务需要输出的信息取走,放在自己的缓冲中逐一按顺序发送。

        4)看护任务:利用 RTOS 的信息交互机制,周期性的和各任务交换信息,当出现不应答时,说明对应的任务出现异常,可以做相应的对策,这样处理无需复位,导致其它任务被意外中断,增加了程序的可靠性。即便不处理,也能利用调试信息输出及时发现是哪个任务异常,以便消除隐患。

        5)主应用任务:前面几个任务属于框架的基础任务,未涉及程序需要执行的实际功能,主应用任务就是未来实现具体功能的载体。设计主应用任务是考虑到一般来说,程序均有一个核心的部分,用于管理、协调一些子功能的工作,使得程序运行有序。相当于一个管理者。

        6)其它任务:作为应用示例,相当于执行者。构建其是为了展示程序框架的功能扩展性和消息传递机制,在后续编写实际功能时作为模板。

        任务确定后,其次要确定任务间消息传递方式。

        从 FreeRTOS 的资料中,可以看出其消息交互的基础是消息队列,信号量、互斥锁等均为消息队列的特例。故程序框架中用消息队列(Queue)实现所有的任务间信息传递。

        此外,单片机程序中最常见的交互还有一种是标志,即:某一个或几个标志成立后,程序执行相应的功能,多个标志的组合关系有“与”、“或”逻辑。恰好 FreeRTOS 提供了事件组(EventGroup)功能,其机制完全符合程序中对标志逻辑的需求。

        为降低学习梯度,本程序框架只使用消息队列(Queue)、事件组(EventGroup)两个工具作为任务间消息传递的手段,应该可以满足99%的应用场景需求。

        基于 RTOS 编程,虽说每个任务都是由一个死循环组成,但这个和非OS下(裸奔)的死循环不同,循环中必须有调用  OS  延时或等待消息的函数,这样 OS 才能调度任务,实现多任务运行。任务中是死循环只是感觉上自己在一直运行,实际上并非如此,在执行 OS 等待或延时函数时,任务会进入待命状态,OS 根据任务的状态分配 MCU 的工作时间给各个任务,各任务分时使用 MCU。

        任务的最佳设计就是构建一个阻塞等待(触发事件未出现,就一直在等待函数中)的条件,当满足这个条件时,快速执行相应的处理(不要有过长时间的处理,尽量设计的可以即时完成),完成后再次进入阻塞等待状态。这样OS 才有充分的时间调度任务、分发消息,MCU 才能达到最佳运行状态。

        事件组消息定义:

        上述事件均为了构成阻塞等待所设,任务中均先阻塞等待这些消息,利用EventGroup 的与、或机制,实现多个消息的组合等待,使一个任务中只存在一个阻塞等待;类似于以往程序中对标志的查询处理。

        事件触发后,再根据具体事件读取对应队列信息,队列的读取只需使用查询方式即可。

        消息队列定义如下:

 

4.2 初始化过程设计

        初始化是单片机实现具体功能的基础,包含程序内的相关变量初始化,以及外接硬件的初始化。在 RTOS 编程中,通常称之为 BSP (board Support Program)程序,汇总在一个文件中。

        编写初始化程序有不同的方式:

        A)按初始化的对象归类,程序开始一并完成,

        B)按功能属性归类,程序开始时一并完成。

        在基于 RTOS 编程时,我偏爱将初始化归在各自任务中,一个任务一个文件,包含本任务的所有内容:初始化部分和执行部分。因为,多任务机制下的编程,通常会多人合作完成,一个任务只涉及一个文件(或增加一个头文件),便于多人合作。

        但这样会产生一个问题:因为初始化是分散在各个任务中的,任务有不同的优先级,创建有先后,创建过程中,任务就会执行,从而导致有的任务初始化已经完成,进入执行循环;有的任务还在初始化,相应的变量、标志尚未建立,在涉及外部硬件初始化(如外接通讯模块)时易于发生。此时有可能会导致程序崩溃。(我曾遇到这种状况)

        为化解此问题,程序框架中利用事件组功能设计了初始化同步机制,由看护任务管理,在所有任务完成初始化后,才通知各个任务进入执行循环。

        因为初始化过程有可能需要调试信息输出,利用此机制先单独启动调试信息输出任务。

4.3 串口命令接收任务设计

        串口是作为单片机的操作输入通道,取代传统模式下的按键操作。通过串口命令操作单片机远比设计实体按键灵活方便,而且硬件资源占用也少。

        此任务要实现对串口命令的接收和初步解析,将命令内容转发给主应用任务处理。

        核心是能可靠的监测和接收符合通讯协议的命令帧。

        通讯协议的定义也是串口命令的重点,要考虑的简洁性和可扩展性的平衡,同时要兼顾应用场景的需求。

        目前协议是参考 ROS (机器人操作系统)中的 ROS Serial 协议设计的。因为小车有无线通讯的需求,故在协议中有相应的通讯地址,以便在一个通道上实现多机通讯。

        串口通讯协议如下:

        字符格式:115200 8 N  1

        帧格式:(借鉴 ROS 的 ROS Serial 协议)

        0xFF 0xFE(2字节帧同步字) 帧长L  帧长H  帧长校验和 目标地址 源地址 帧数据区 帧校验和

        其中:

        帧同步字 —— 2字节特征字,暂定为0xFF 0xFE,借鉴的ROS Serial协议。

        帧长 —— 帧数据区数据字节数,不含帧校验和、目标地址和源地址;先低后高,最大支持65535字节,实际不一定需要,但有可能超过256字节,所以用2字节。

        帧长校验和 —— 2字节帧长算数和取反,取最低字节。借鉴ROS Serial协议

        目标地址 —— 接收方的通讯地址, 1字节。

        源地址 —— 发送此帧的通讯地址,1字节,应答时使用。

        帧数据区 —— 通讯数据,字节数为帧长

        帧校验和 —— 数据区所有字节的算数和取反,取最低字节。如果数据长度为0,则CS为0xFF。

        按此设计,最短的帧为 8字节,数据区无数据,可作为心跳帧或命令应答帧。

        帧的传输方向由两个地址确定,无需再设计上、下行(应答帧)标志。

        帧数据区定义如下:

        Key:操作命令,1字节

        Len:数据长度,2字节,先低后高,单位 - 字节

        Val[Len] :N 字节数据

        至于 Val 的数据如何定义,取决于 Key,可以定义为结构、数据、或者更复杂的数据,也可以简单的定义为整形。

        串口数据帧的可靠接收源于可靠的帧提取方式,因为有可能使用无线通讯(串口接无线透传模块即可),就存在串口接收的数据并非都是有效的、应该收的,需要从接收的数据流中检出发给自己的数据帧,不能根据数据绝对位置提取。

        对于从连续数据流中检出一段符合要求的数据,使用滑窗比较方式较为可靠。由于上述协议定义的是变长帧,无法对整个数据帧进行滑窗比较,只能基于协议,找到帧头的特征后,再对整个数据帧接收,之后再通过校验判断此帧是否正确。

        此处所用的帧头特征为:同步字、帧长格式(2字节+校验)、目标地址。

        除数据帧的可靠接收外,在串口命令接收任务中,还设计了两个操作命令:读内存、写内存。一方面是作为示例,演示如何接收并执行一个有效命令;另一方面也是丰富调试手段。

        在传统单片机调试环境中,使用断点、单步时,通常是为了停下来观察相关变量的值、相应硬件资源的状态(寄存器值)。

        在此设计一个读内存命令,就可以实现此功能。

        将关注的变量设计为全局变量或静态变量,在编译产生的 map 文件中可以查到相应的地址,通过读内存操作可随时观察变量的变化。由于 STM32 的内存是线性空间,其 RAM、ROM、硬件工作寄存器均在一个地址空间,因此还可以通过读内存功能监测 MCU 相应硬件的工作状态,以确定初始化是否正常,运行是否正确。

        这个功能作为调试信息输出的补充,可以使调试手段更加丰富。调试信息输出需要预先嵌入一段代码,而读内存操作可以随时使用,只要对象不是动态变量。

        写内存功能也是作为调试手段的补充,可以通过串口修改程序中的相应状态,从而激励程序执行所需的操作。

4.4 串口发送任务设计

        在传统的单片机系统中,通常会设计显示屏、至少是 LED 数码管作为信息输出手段;但目前多数单片机系统已不需要这样设计,通过串口输出信息,使用 PC、手机这类显示功能完善的设备作为单片机系统信息输出的呈现手段,比 LED 数码管、LCD 屏更为直观、灵活、美观,而且占用硬件资源极少。掌上实验室更是如此,因其载体是小车,上面设置显示输出基本无用。

        因程序框架是多任务方式,理论上各个任务都有输出信息的需求,故将串口发送功能独立设计为一个任务,可以服务于所有任务。

        为减少内存消耗,发送数据传递消息只传输存放指针,发送任务根据数据结构定义,取出要发送的数据发送。串口发送速度和内存操作相比慢很多,要发送的数据放置在各自任务中,在每次需要发送前,需要确定上次数据是否取走,以避免数据覆盖,导致发送数据错误。

        前面说过,任务的最佳状态是:阻塞等待消息,触发后快速执行完相应处理后回到阻塞等待状态。

        使用 Arduino 的串口发送函数 Write()时,库函数处理机制为:

        如果数据长度小于发送缓冲,则一次性将所有数据以中断方式发送,并立即从Write 函数中返回,不等待数据发完。如果数据长度大于发送缓冲,则先发缓冲长度的数据,发送完成之后再发送剩余的,直到发完后才会从 Write 函数中返回。

        此时,发送任务由于串口发送速度慢而占用时间偏长,Write 函数内部似乎没有相应的处理以允许 OS 调度,此时只有高优先级的任务可以执行,降低的 MCU 运行效率。

        为此,程序设计时首先获取 Arduino 库中的发送缓冲长度,后续在发送过程中,一次 Write 只发送小于缓冲的数据,在 Write 函数外使用 Osdelay()函数将控制权交给 OS,以提高 MCU 的运行效率,确保程序实时性。

 

 

4.5 调试信息输出任务设计

        调试信息输出通过串口,因为是所有任务共享一个串口,而输出信息又是随机的,在多任务情况下,冲突是必然的。前面说过,用OS的互斥锁机制虽可以化解,但会产生输出滞后,导致要输出信息的任务运行受阻,产生副作用。

        为此,设计了独立的调试信息输出任务,开一个较大的缓存,先将各任务的输出信息收下,放入缓存,再逐一通过串口输出。这样既不会阻塞发送信息的任务运行,又能保证输出顺序符合运行逻辑。

        调试信息一般均为字符输出,用常见的超级终端程序或串口助手即可监测,或用 Arduino IDE 环境集成的 Monitor 功能。

        为了便于在编程中使用调试信息输出功能,模拟 C 语言的打印函数构建了一个输出函数,可以直接使用字符串输出。由于每个任务都需要自己的输出函数,功能一样,但使用各自的内存。想简化编写,在 C 中通常定义一个可重入函数实现共享。由于 Arduino 编译环境支持C++,故尝试用类定义方式实现,也算是增加一个学习内容吧。类定义如下:

        这样使用就很方便,先在各自任务中定义:

         在需要输出信息的地方插入:

        调试信息输出任务处理没有什么特别,和串口发送任务类似,唯一的区别是:调试信息输出任务先把要发送的数据取到自己的缓存中。

4.6 看护任务设计

        看护任务的设计目前只是一个示意性的,没有实质的恢复处理。因为恢复处理需要根据具体功能确定,没有统一的方法。

        但编写好一个处理框架,后续如果需要,增加相应的处理会方便一些。

        作为看护任务,除了通过和各任务交互,以确定任务是否在正常运行外,还顺带完成了运行指示功能。目前单片机系统虽无需设计显示器,但工作指示一般都有,可直观的反应系统是否在正常运行,通常是通过LED的闪烁变化呈现。

        此处参考 FreeRTOS 的异常指示方式设计了 LED 显示功能,正常时,LED 等间隔闪烁,当发现某个任务异常时,按任务顺序会出现间断闪烁。具体方式为:

        将一个完整的显示周期定为 10 次闪烁,正常时一个周期闪烁 10 次。

        如果是1号任务异常,则一个周期闪烁1次,其余9次对应暗状态;如果是2号,一个周期闪2次、暗8次,以此类推,最多可以支持9个任务的异常指示。

4.7 主应用任务设计

        主应用任务的设计也是模板性质,主要为了展示信息交互过程。

        应用任务一般有主次之分,由一个主任务管理多个子任务,实现完整的功能。主任务负责调度其它子任务的执行。此处就是按此思路设计。

        在主任务中,要接收来自串口命令接收任务的操作,执行属于自己的,将执行结果通过串口发送任务输出。

        如果收到的操作是由子任务完成的,将操作命令转发给对应子任务,并通过消息机制接收子任务操作结果,做相应处理。

        主应用任务相当于这个系统中的管理者

        主应用任务的设计主要是消息的定义,采用数据结构方式,便于修改、补充:

 

4.8 其它应用任务设计

        其它应用任务的设计是配合主任务的,两者组合,构成了完整的应用处理流程,也属于模板性质,重点关注消息的定义和收发方式。

五、结语

        程序框架设计基本完成,按我以往的经验,它可以基本应对绝大多数场景下的需求,扩展性和可维护性都能满足要求,只要所做的应用没有那种极端的需求。

        对于编程学习,我的感受是:不要把精力耗在编程环境的学习上,越是先进的 IDE(集成开发环境),功能越多,也越难使用。对应初学者而言,还是将时间花在理解编程语言的实质、单片机工作的原理以及对现实需求的提取上,编程的目的是用程序解决问题,不是秀技。

        IDE 只是工具,不是目的。IDE 所提供的手段是能帮助调试程序,但真正能高效调试的秘诀是:你能在自己的大脑中清晰的运行程序,对整个程序的逻辑关系熟稔于心。如果能做到,根据程序运行的异常现象,利用上述两个自己构建的工具(调试信息输出、读内存)就可以快速确定故障点。

        与其费力去学 IDE 的调试手段,不如在程序上多花点功夫,这样一方面可以帮助你调试程序,最关键的是可以使程序更加可靠;还少了依赖,不会因芯片替换而失去自己熟悉的工具,导致工作受阻。

        在嵌入式应用圈内好像也有鄙视链,使用 Arduino 应该就在鄙视链的低端,我在初次接触时也曾有过此想法,认为其不够“专业”。但这些年使用后,觉得很方便,尤其是 STM32、ESP8266、ESP32 等芯片做了完善的支持后,使其实用性大大增加。虽说在某些方面还有不足,但相对于省去的熟悉 IDE 时间,我觉得还是值的!

        而且 Arduino编译环境还支持 C++ 编程,目前MCU程序空间越来越大,可以采用面向对象的方式构建程序,能增加程序的可读性、可维护性。

        个人之见,仅供参考。

————————————

程序框架代码下载:

链接:https://pan.baidu.com/s/1VZrq9k8leHmPiV8REcSYqg 

提取码:o172

  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值