本书的硬件开发平台为苏州大学EAI&IoT实验室(简称SD-EAI&IoT)研发的以意法半导体(ST)的STM32L431芯片为核心的通用嵌入式计算机,型号为AHL-STM32L431。嵌入式软件开发平台为SD-EAI&IoT研制的适用于多种类型微控制器的金葫芦集成开发环境AHL-GEC-IDE,对于本书的样例工程,兼容ST的集成开发环境STM32CubeIDE。本节首先介绍GEC架构,接着介绍软硬件平台,然后介绍本书配套的电子资源。
为了能够提高编程颗粒度和可移植性,可以借鉴通用计算机(General Computer)的概念与做法,在一定条件下,利用通用嵌入式计算机,把基本输入输出系统(Basic Input and Output System,BIOS)与用户程序分离开来,实现彻底的工作分工。GEC虽然不能涵盖所有嵌入式开发,但可涵盖其中大部分。
GEC概念的实质是把面向寄存器的编程提高到面向知识要素的编程,从而提高编程颗粒度。但是,这样做也会降低实时性。弥补实时性的方法是提高芯片的运行时钟频率。目前,MCU的总线频率是早期MCU总线频率的几十倍,甚至几百倍,因此更高的总线频率给提高编程颗粒度提供了物理支撑。
另外,软件构件技术的发展与普及也为提出GEC概念提供了机遇。嵌入式软件开发工程师越来越认识到软件工程对嵌入式软件开发的重要支撑作用,意识到掌握和应用软件工程的基本原理对嵌入式软件的设计、升级、芯片迭代与维护等方面具有不可或缺的作用。因此,从“零”开始的编程,将逐步分化为构件制作与构件使用两个不同层次,这为嵌入式人工智能提供先导基础。
一个具有特定功能的GEC,体现在硬件与软件两个方面。在硬件上,把MCU硬件最小系统及面向具体应用的共性电路封装成一个整体,为用户提供SOC级芯片的可重用的硬件实体,并按照硬件构件要求进行原理图绘制、文档撰写及硬件测试用例设计。在软件上,把嵌入式软件分为BIOS程序与User程序两部分。BIOS程序先于User程序固化于MCU内的非易失存储器(如Flash)中。启动时,BIOS程序先运行,随后转向User程序。BIOS提供工作时钟及面向知识要素的底层驱动构件,并为User程序提供函数原型级调用接口。
与MCU对比,GEC具有硬件直接可测性、用户软件编程快捷性与可移植性三个基本特点。
1)GEC硬件的直接可测性
与一般MCU不同,GEC类似于个人计算机,通电后可以直接运行内部BIOS程序,BIOS驱动保留使用的小灯引脚,高低电平切换(在AHL-STM32L431开发套件上,可直接观察到小灯闪烁)。可利用金葫芦GEC集成开发环境AHL-GEC-IDE使用串口连接GEC,直接将用户程序User写入GEC,User程序中包含类似于个人计算机程序调试的printf语句,通过串口向个人计算机机输出信息,实现了GEC硬件的直接可测性。
2)GEC用户软件的编程快捷性
与一般MCU不同,GEC内部驻留的BIOS与个人计算机上电过程类似,完成系统总线时钟初始化;BIOS包含一个系统定时器,提供时间设置与获取函数接口;BIOS内驻留了实时操作系统内核程序和嵌入式常用驱动,如GPIO(General Purpose Input/Output,通用输入输出)、UART、ADC、Flash、I2C、SPI、PWM等,并提供了函数原型级调用接口。利用User程序不同框架,用户软件不需要从“零”编起,而是在相应框架基础上,充分应用BIOS资源,实现快捷编程。
3)GEC用户软件的可移植性
与一般MCU软件不同,GEC的BIOS软件由GEC提供者研发完成,随GEC芯片提供给用户,即软件被硬件化,具有通用性。BIOS驻留了大部分面向知识要素的驱动,提供了函数原型级调用接口。在此基础上编程,只要遵循软件工程的基本原则,GEC用户软件则具有较高的可移植性。
嵌入式软件开发区别于个人计算机软件开发的一个显著的特点在于,它需要一个交叉编译和调试环境,即工程的编辑和编译所使用的软件通常在个人计算机上运行,而编译生成的嵌入式软件的机器码文件则需要通过写入工具下载到目标机上执行。由于主机和目标机的体系结构存在差异,增加了嵌入式软件开发的难度,因此选择好的开发套件将有助于学习与开发。
学习实时操作系统应该在一个实际的硬件系统中进行,在具备基本硬件条件下,不建议读者使用仿真平台进行学习,所谓“仿真”不真,无法实现实际的学习目标。实际上,随着技术的不断发展和芯片制造成本的下降,可以买到价格十分低廉、功能却十分强大的实时操作系统硬件学习平台。
本书介绍的可用于实时操作系统学习的开发套件的型号为AHL-STM32L431,其主要特点如下。
(1)核心芯片为64引脚LQFP封装的STM32L431RC芯片。内含256KB的Flash(共有128个扇区)、64KB的RAM,包含SysTick、GPIO、串口、A/D、D/A、I2C、SPI等模块。
(2)开发套件由硬件最小系统、红绿蓝三色灯、触摸按键、温度传感器、两路TTL-USB等构成。引出所有MCU引脚。其中的三色灯部件,内含蓝、绿、红三个发光二极管,俗称小灯,这三个小灯的正极过1kΩ电阻接电源正极,三个小灯的负极分别接MCU的三个引脚,具体接在MCU的哪几个引脚,参见样例工程“...\CH3.3-Nos\05_UserBoard\User.h”文件,用户使用的所有硬件引脚应该在此进行宏定义,这样符合嵌入式软件设计规范。
(3)开发套件硬件的扩展底板上还有个Type-C接口。实际上,它是两路TTL串口,默认它与个人计算机进行串行通信,将USB-Type-C数据线的USB端口连接个人计算机机的USB口,数据线的Type-C端接硬件底板上的Type-C口,就可以使用printf输出进行跟踪调试,printf输出的字符信息将送到个人计算机的串口工具显示栏,方便嵌入式程序的调试。
(4)可扩展应用。AHL-STM32L431开发套件不仅可以用于Mbed OS实时操作系统的学习,还可以用于通过板上的开放式外围引脚,外接其他接口模块进行创新性实验。
当然,读者可以使用自己的硬件平台,参考本书的工程框架,完成自身硬件平台下的工程框架设计。
AHL-STM32L431嵌入式开发套件分迷你型(见图3-1)、扩展型两种型号,更详细的介绍见本书配套电子资源。迷你型可以完成本书第1~12章所有实验,扩展型可以完成本书所有实验,并用于实践创新。
图3-1 AHL-STM32L431嵌入式开发套件(迷你型)
具体引出脚含义及相关内容参见本书配套的电子资源。
目前,大多数嵌入式集成开发环境(Integrated Development Environment,IDE)基于Eclipse架构 开发。本书使用的IDE主要有两种:SD-EAI&IoT推出的AHL-GEC-IDE与ST推出的STM32CubeIDE。本书给出的基于STM32L431程序实例兼容AHL-GEC-IDE与STM32CubeIDE。
建议使用AHL-GEC-IDE,必要时,利用AHL-GEC-IDE的“外接软件”菜单,将STM32CubeIDE作为外接软件使用。
AHL-GEC-IDE是SD-EAI&IoT于2018年推出的免费嵌入式集成开发环境,优点是操作简单、功能实用、兼容几个芯片公司的常用开发环境及厂家工程模板。其集成GNU编译器、汇编器等,面向ARM Cortex-M微处理器开发,具有编辑、编译、程序下载、printf打桩调试等功能,为设计人员提供了一个简捷易用的嵌入式开发工具。
AHL-GEC-IDE与其他常用开发环境相比,有以下特点。
(1)常用开发环境兼容性。对于STM32芯片,兼容STM32CubeIDE及Keil开发环境;对于TI芯片,兼容CCS(Code Composer Studio)开发环境;对于NXP芯片,兼容KDS(Kinetis Design Studio)开发环境。
(2)支持串口下载调试。基于BIOS与User框架,支持通过串口的下载调试,无须其他烧录工具,下载后User程序立即执行,可应用类似于个人计算机编程的printf输出调试语句,跟踪程序运行过程,提示信息立即显示在个人计算机显示屏的文本框中,使嵌入式编程与个人计算机编程过程几乎一致。
(3)外接软件功能。可自行外接其他软件,随后在菜单栏中打开用户需要的软件运行,方便功能集成与开发应用。
(4)丰富的常用工具。程序调试过程可以通过串口实现对存储器的某个区域进行读取和修改,支持对Flash、RAM区域读出;也可以直接通过软件中的串口工具,观察串口输出情况,不需要借助其他外部串口工具。
(5)简化工程配置。当工程文件中有新增的文件或文件夹时,其他的开发环境需要通过工程配置操作将该文件包含在工程中,而AHL-GEC-IDE默认工程下级文件夹为工程编译所需,不须在工程中设置,自动支持C语言、汇编语言等。在该AHL-GEC-IDE环境下,通过自行识别,可直接编译C语言或者汇编语言下的工程,不需要对编译器进行选择。
(6)可扩展功能。AHL-GEC-IDE除了具备开发的基本功能(导入工程、编辑、查找和替换、程序编译和烧写等),还提供很多扩展功能。例如,支持远程更新,当目标芯片配置好相应的远程通信硬件后,在AHL-GEC-IDE开发环境中可以通过NB-IoT、2G、4G等无线方式实现远程的程序更新;支持动态命令,可将机器码下载到特定的Flash区域直接运行该机器码,实现命令的动态扩充。
STM32CubeIDE是适用于ST公司的MCU的免费集成开发环境,集成GNU编译器集合(GCC)、GNU调试器(GDB)等在内的免费开源软件,为设计人员提供一个简单易用的开发工具,具有编辑、编译和调试等功能。本书提供的样例工程兼容AHL-GEC-IDE与STM32CubeIDE。
Mbed OS金葫芦电子资源 ,内含所有源程序、文档资料及常用软件工具等,内含有六个子文件夹:01-Information、02-Document、03-Hardware、04-Softwareware、05-Tool、06-Other。如表3-1所示为电子资源中各子文件夹的内容索引表。其中,第1~7章的程序对Mbed OS内核程序的调用是通过对外接口函数的方式(BIOS提供的Mbed OS对外接口函数可查看04-Softwareware内各工程文件夹下05_UserBoard文件夹中的Os_Self_API.c、Os_Self_API.h和Os_United_API.h文件),第8~14章程序中提供了Mbed OS内核源代码程序(在04-Softwareware内各工程文件夹下的 05_UserBoard\mebedOS_Src文件中),是对源代码程序进行剖析和应用。
表3-1 电子资源中各子文件夹的内容索引表
为了更好地理解实时操作系统下的编程,基于同样的程序功能,分别通过NOS工程和Mbed OS工程来进行编程实现。
样例程序的硬件是红、绿、蓝三色一体的发光二极管(小灯),由三个GPIO引脚控制其亮暗。
软件控制红灯每5s、绿灯每10s、蓝灯每20s变化一次,对外表现为三色灯的合成色,经过分析,三色灯的实际效果如图3-2所示,即开始时为暗,依次变化为红、绿、黄(红+绿)、蓝、紫(红+蓝)、青(蓝+绿)、白(红+蓝+绿),周而复始。
图3-2 三色灯的实际效果
良好的工程框架是编程工作的重要一环,建立一个组织合理、易于理解的嵌入式软件工程框架需要较深入的思考与斟酌。
所谓工程框架是指工程内文件夹的命名、文件的存放位置、文件内容的分置规则。软件工程与一件建筑作品、一幅画作等是一致的,软件工程框架是整个工程的脊梁,其主要线程不是完成一个单独的模块功能,而是指出工程应该包含哪些文件夹,这些文件夹里面应该放置什么文件,各个文件的内容又是如何定位等。
因此,工程框架设计的基本原则应该是分门别类,各有归处,建立工程文件夹,并考虑随后内容安排及内容定位,建立其子文件夹。
人们常常可以看到一些工程框架混乱、子文件夹命名不规范、文件内容定位不清晰、文件包含冗余的样例工程,把学习者与开发者弄得一头雾水,这样的工程框架是不符合软件工程要求的。甚至在一些机构给出的底层驱动中,包含不少操作系统的内容,违背了底层驱动设计独立于上层软件的基本要求,一旦更换操作系统,该驱动难以使用,给软件开发工程师带来烦恼。
表3-2给出了无操作系统的工程框架的树型结构,并对MCU文件夹、用户板文件夹、应用程序文件夹等进行了补充说明。
表3-2 无操作系统的工程框架的树型结构及补充说明
(1)MCU文件夹。把链接文件、MCU的启动文件、MCU底层驱动(MCU基础构件)放入这个文件夹中,分别建立linker_file、startup、MCU_drivers三个子文件夹。linker_file文件夹内的链接文件,给出了芯片存储器的基本信息;startup文件夹含有芯片的启动文件;MCU_drivers存放与MCU硬件直接相关的基础构件。
(2)用户板文件夹。开发者选好一款MCU,要做成产品总要设计自己的硬件板,这就是用户板。这个板上可能有LCD、传感器、开关等,这些硬件必须由软件干预才能工作,干预这些硬件的软件构件被称为应用构件。应用构件一般需要调用MCU基础构件,它被放置在该文件夹中。
(3)应用程序文件夹。其包含总头文件(includes.h)、中断服务程序源程序文件(isr.c)、主程序文件(main.c)等,这些文件是软件开发工程师进行编程的主要对象。总头文件includes.h是isr.c及main.c使用的头文件,包含用到的构件、全局变量声明、常数宏定义等。中断服务程序文件isr.c是中断处理函数编程的地方。主程序文件main.c是应用程序启动后的总入口,main函数即在该文件中实现。main函数包含一个永久循环,对具体事务过程的操作几乎都是添加在该主循环中的。应用程序的执行有两条独立的线路,一条是主循环运行路线,在main.c文件中编程;另一条是中断线,在isr.c文件中编程。若有操作系统,则可在main.c中启动操作系统调度器。
(4)此外,编译输出还会产生Debug文件夹,含有编译链接生成的.elf、.hex、.list、.map等文件。
.elf(Executable and Linking Format),即可执行链接格式,最初由UNIX系统实验室(UNIX System Laboratories,USL)作为应用程序二进制接口(Application Binary Interface,ABI)的一部分而制定和发布的,其最大的特点在于它有比较广泛的适用性,通用的二进制接口定义使之可以平滑地移植到多种不同的操作环境中。UltraEdit软件工具可查看.elf文件内容。
.hex(Intel HEX)文件是由一行行符合Intel HEX文件格式的文本所构成的ASCII文本文件。在Intel HEX文件中,每一行包含一个HEX记录,这些记录由对应机器语言码(含常量数据)的十六进制编码数字组成。
.list文件提供了函数编译后机器码与源代码的对应关系,用于程序分析。
.map文件提供了查看程序、堆栈设置、全局变量、常量等存放的地址信息。.map文件中给出的地址在一定程度上是动态分配的(由编译器决定),工程有任何修改,这些地址都可能发生变动。
基于无操作系统的样例工程(见“...\04-Software\CH3.3.3-NOS”文件夹)可用AHL-GECIDE打开。该程序有两条执行路线,一条是主循环线,为了衔接操作系统概念,可称为线程线;另一条为中断线,分别对应main.c中的for循环,以及isr.c中的中断处理程序。
线程线:程序通过判断全局变量gSec(秒累加变量)来控制三色小灯的开关状态,实现红灯每5s闪烁一次、绿灯每10s闪烁一次、蓝灯每20s闪烁一次,同时通过串口输出开关信息。
中断线:定时器TIM2定时周期为1s,时间每经过1s,TIM2会触发定时器的中断服务程序,在中断服务程序中对变量gSec进行累加。
程序运行时先执行线程线,在执行线程线的过程中若触发定时器中断(即计时时间达到1s),程序便会从线程线跳转到中断线执行定时器中断服务程序(实现对gSec的累加)。当中断服务程序执行完成后,程序会回到线程线从刚才跳转的地方继续执行下去。
1)线程线:main函数
可以从main函数起点开始理解执行过程 ,在for(;;)的永久循环体内,程序通过判断gSec来控制三色小灯的开关状态。
2)中断线:isr.c中断处理程序
当定时器到达定时时间1s时,会执行定时器中断服务程序。在定时器中断服务程序中,先判断是否由TIM2触发的中断,若是,则对变量gSec累加(即秒加1),清除中断标志位。
将样例工程编译,通过Type-C线将AHL-STM32L431与个人计算机的USB接口进行连接,进入AHL-GEC-IDE中的“下载”→“串口更新”,单击“连接GEC”按钮,连接成功后,导入编译产生的机器码.hex文件,单击“一键自动更新”按钮将程序下载到目标板上,程序将自动运行 。图3-3所示为无操作系统的样例工程下载后图示。随后,观察AHL-STM32L431开发板上的红灯、蓝灯和绿灯的闪烁情况,若与图3-2所示的情况一致,则正确。
图3-3 NOS样例工程下载后图示
Mbed OS的工程框架与无操作系统的工程框架完全一致,不同的地方如下。
(1)在工程的05_UserBoard文件夹中增加了Os_Self_API.h和Os_United_API.h两个头文件。其中,Os_Self_API.h头文件给出了Mbed OS对外接口函数API,如事件(mbed_event)、消息队列(mbed__messagequeue)、信号量(mbed__semaphore)、互斥量(mbed__mutex)等有关函数,实际函数代码驻留于BIOS中;Os_United_API.h头文件给出了实时操作系统的统一对外接口API,目的是实现不同实时操作系统的应用程序可移植,可以涵盖实时操作系统的基本要素函数。
(2)在工程的“...\07_AppPrg\includes.h”文件中,给出了线程函数声明。
(3)工程的“...\07_AppPrg\main.c”文件中,给出了操作系统的启动。
(4)工程的07_AppPrg文件夹中的threadauto_appinit.c文件是主线程文件,在该文件的main函数中调用OS_start函数启动操作系统。app_init函数是OS_start函数的入口参数,在实时操作系统启动过程中作为主线程的执行函数。因此,main_thread线程也被称为自启动线程,app_init函数被称为自启动线程函数,它的作用是将该文件夹中的其他三个文件中的函数(即thread_redlight、thread_greenlight和thread_bluelight)变成线程函数,被调度运行。
(5)工程的07_AppPrg文件夹中的三个功能性函数文件为thread_bluelight.c、thread_greenlight.c和thread_redlight.c,它们内部的函数由于变成了线程函数,因此可分别称为蓝灯线程、绿灯线程和红灯线程,它们在内核调度下运行。至此,可以认为有三个独立的主函数在操作系统的调度下独立地运行,一个大工程变成了三个独立运行的小工程。
基于Mbed OS的样例工程(见“...\04-Software\CH3.3.4-mbedOS”文件夹),可用AHLGEC-IDE导入。在该样例工程中,共创建了六个线程。表3-3所示为样例工程线程一览表。
表3-3 样例工程线程一览表
执行OS_start(app_init)进行Mbed OS的启动,在启动过程中依次创建了主线程(main_thread)、空闲线程(osRtxInfo.thread.idle)和定时器线程(osRtxInfo.timer.thread)。主线程执行函数app_init源代码是在本工程中直接给出的,空闲线程执行函数osRtxIdleThread和定时器线程执行函数osRtxTimerThread被驻留在BIOS中(这一内容将在第8章中进行解释)。
1)主线程功能概要
主线程被内核调度先运行,过程概要如下。
(1)在主线程中依次创建蓝灯线程、绿灯线程和红灯线程,红灯线程实现红灯每5s闪烁一次,绿灯线程实现绿灯每10s闪烁一次,蓝灯线程实现蓝灯每20s闪烁一次,创建完这些用户线程之后主线程被终止。
(2)此时,在就绪列表中剩下红灯线程、绿灯线程、蓝灯线程和空闲线程四个线程。
(3)由于就绪列表优先级最高的一个线程是thd_redlight,它优先得到激活运行。thd_redlight线程每隔5000ms控制一次红灯的亮暗状态,当thd_redlight线程调用系统服务delay_ms执行延时,调度系统暂时剥夺该线程对CPU的使用权,将该线程从就绪列表中移出,放入延时列表中。
(4)系统开始依次调度执行thd_bluelight线程和thd_greenlight线程,将线程从就绪列表中移出,根据延时时长将线程放入延时列表中。
(5)当这三个线程都被放入延时列表时,就绪列表中就只剩下空闲线程,此时空闲线程会得到运行。
基于每1ms(时间嘀嗒)的SysTick中断,在SysTick中断处理程序中,查看延时列表中的线程是否到期,若有到期的线程,则将线程从延时列表中移出,并放入就绪列表中。同时,由于到期线程的优先级大于空闲线程的优先级,会抢占空闲线程,通过上下文切换激活,再次得到运行。
由于这蓝、绿、红三个小灯物理上对外表现是一盏灯,所以样例工程的对外表现应该达到图3-2的效果(与无操作系统的样例工程运行效果相同)。
2)主线程源代码剖析
主线程的运行函数app_init主要完成全局变量初始化、外设初始化、创建其他用户线程、启动用户线程等工作,它在07_AppPrg\threadauto_appinit.c文件中定义。
(1)创建用户线程。在threadauto_appinit.c文件中,首先创建三个用户线程,即红灯线程thd_redlight、蓝灯线程thd_bluelight和绿灯线程thd_greenlight,它们的优先级都设置为10 ,堆栈空间设置为512字节。
(2)启动用户线程。在07_AppPrg文件夹下创建thread_redlight.c、thread_bluelight.c和thread_greenlight.c三个文件,在这三个文件中分别定义三个用户线程执行函数,即thread_redlight、thread_bluelight和thread_greenlight。这三个用户线程执行函数在定义上与普通函数无差别,但是在使用上不是作为子函数进行调用的,而是由Mbed OS进行调度的。此外,这三个用户线程执行函数基本上是一个无限循环,在执行过程中由Mbed OS分配CPU使用权。
3)主函数app_init代码注释
根据Mbed OS样例程序的功能,设计了红灯thd_redlight、蓝灯thd_bluelight和绿灯thd_greenlight三个小灯闪烁线程,对应工程07_AppPrg文件夹下的thread_redlight.c、thread_bluelight.c和thread_greenlight.c这三个文件。
小灯闪烁线程先将小灯初始设置为暗,然后在while(1)的永久循环体内,通过delay_ms函数实现延时,每隔指定的时间间隔切换灯的亮暗一次。delay_ms函数的延时操作并非停止其他操作的空跑等待,而是通过延时列表管理延时线程,从而实现对线程的延时,延时函数将在4.2节和9.2节中详细介绍。在延时期间,线程被放入延时列表中,实时操作系统可以调度执行其他的线程。下面给出红灯线程函数thread_redlight的具体实现代码,蓝灯线程函数thread_bluelight和绿灯线程函数thread_greenlight与红灯线程函数thread_redlight类似,读者可自行分析。
测试过程可参照无操作系统的样例工程,从中可以观察到三色灯随时间的变化与图3-2一致。Mbed OS样例工程测试结果如图3-4所示。由此体会无操作系统下编程与实时操作系统下编程的异同点。至此,实时操作系统可以较好地服务于用户程序设计。
图3-4 Mbed OS样例工程测试结果