正文

MDK-ARM 集成开发环境因为其完全的 STM32F103 软件仿真环境,也让我们有机会在不使用真实硬件环境的情况下直接在电脑上运行目标代码。这套软件仿真模拟器能够完整地虚拟出 ARM Cortex-M3 的各种运行模式、外设,如中断异常,时钟定时器,串口等,这几乎和真实的硬件环境完全一致。实践也证明,本文使用到的这份 RT-Thread 入门例程,在编译成二进制代码后,不仅能够在模拟器上软件模拟运行,也能够不需要修改地在真实硬件平台上正常运行。

下面我们将选择 MDK-ARM 集成开发环境作为目标硬件平台来观察 RT-Thread 操作系统是如何运行的。

准备工作

MDK 开发环境:需要安装 MDK-ARM 5.24 (正式版或评估版,5.14 版本及以上版本均可),这个版本也是当前比较新的版本,它能够提供相对比较完善的调试功能。安装方法可以参考 Keil MDK安装。

使用 STM32F103 软件仿真 ,还需要下载安装 STM32F103 pack 文件,如果在 MDK 中下载较慢,也可以点击此处下载,下载后双击安装即可。

初识 RT-Thread

作为一个操作系统,RT-Thread 的代码规模怎么样呢?在弄清楚这些之前,我们先要做的就是获得与本文相对应的 RT-Thread 的例子,这份例子可以从以下链接获得:

RT-Thread Simulator 例程

这个例子是一个压缩包文件,将它解压,我们这里解压到 D:/。解压完成后的目录结构如下图所示:

stm32f103 nes模拟器 stm32 fc模拟器_stm32f103 nes模拟器

各个目录所包含的文件类型的描述如下表所示:

目录名

描述

applications

RT-Thread 应用程序。

rt-thread

RT-Thread 的源文件。

- components

RT-Thread 的各个组件目录。

- include

RT-Thread 内核的头文件。

- libcpu

各类芯片的移植代码,此处包含了 STM32 的移植文件。

- src

RT-Thread 内核的源文件。

- tools

RT-Thread 命令构建工具的脚本文件。

drivers

RT-Thread 的驱动,不同平台的底层驱动具体实现。

Libraries

ST 的 STM32 固件库文件。

kernel-sample-0.1.0

RT-Thread 的内核例程。

在目录下,有一个 project.uvprojx 文件,它是本文内容所引述的例程中的一个 MDK5 工程文件,双击 “project.uvprojx” 图标,打开此工程文件:

stm32f103 nes模拟器 stm32 fc模拟器_linux_02

在工程主窗口的左侧 Project 栏里可以看到该工程的文件列表,这些文件被分别存放到如下几个组内,分别是:

目录组

描述

Applications

对应的目录为 rtthread_simulator_v0.1.0/applications,它用于存放用户应用代码。

Drivers

对应的目录为 rtthread_simulator_v0.1.0/drivers,它用于存放 RT-Thread 底层的驱动代码。

STM32_HAL

对应的目录为 rtthread_simulator_v0.1.0/Libraries/CMSIS/Device/ST/STM32F1xx,它用于存放 STM32 的固件库文件。

kernel-sample

对应的目录为 rtthread_simulator_v0.1.0/kernel-sample-0.1.0,它用于存放 RT-Thread 的内核例程。

Kernel

对应的目录为 rtthread_simulator_v0.1.0/src,它用于存放 RT-Thread 内核核心代码。

CORTEX-M3

对应的目录为 rtthread_simulator_v0.1.0/rt-thread/libcpu,它用于存放 ARM Cortex-M3 移植代码。

DeviceDrivers

对应的目录为 rtthread_simulator_v0.1.0/rt-thread/components/drivers,它用于存放 RT-Thread 驱动框架源码。

finsh

对应的目录为 rtthread_simulator_v0.1.0/rt-thread/components/finsh,它用于存放 RT-Thread 命令行 finsh 命令行组件。

现在我们点击一下窗口上方工具栏中的按钮

stm32f103 nes模拟器 stm32 fc模拟器_linux_03

,对该工程进行编译,如图所示:

stm32f103 nes模拟器 stm32 fc模拟器_linux_04

编译的结果显示在窗口下方的 “Build” 栏中,没什么意外的话,最后一行会显示“0 Error(s), * Warning(s).”,即无任何错误和警告。

注:由于工程中包含的内核例程代码较多,若使用的是 MDK 试用版本,则会有 16KB 限制,此时可以只保留某个目标例程的代码(例如内核例程只保留一个 thread_sample.c 参与编译),将其他不用的例程先从工程中移除,然后编译。

在编译完 RT-Thread/STM32 后,我们可以通过 MDK-ARM 的模拟器来仿真运行 RT-Thread。点击窗口右上方的按钮

stm32f103 nes模拟器 stm32 fc模拟器_初始化_05

或直接按 “Ctrl+F5” 进入仿真界面,再按 F5 开始运行,然后点击该图工具栏中的按钮或者选择菜单栏中的 “View→Serial Windows→UART#1”,打开串口 1 窗口,可以看到串口的输出只显示了 RT-Thread 的 LOGO,这是因为用户代码是空的,其模拟运行的结果如图所示:

stm32f103 nes模拟器 stm32 fc模拟器_stm32_06

提示:我们可以通过输入Tab键或者 help + 回车 输出当前系统所支持的所有命令,如下图所示。

stm32f103 nes模拟器 stm32 fc模拟器_linux_07

系统启动代码

一般了解一份代码大多从启动部分开始,同样这里也采用这种方式,先寻找启动的源头。以 MDK-ARM 为例,MDK-ARM 的用户程序入口为 main() 函数,位于 main.c 文件中。系统启动后先从汇编代码 startup_stm32f103xe.s 开始运行,然后跳转到 C 代码,进行 RT-Thread 系统功能初始化,最后进入用户程序入口 main()。

下面我们来看看在 components.c 中定义的这段代码:

//components.c 中定义
 /* re-define main function */
 int $main(void)
 {
 rt_hw_interrupt_disable();
 rtthread_startup();
 return 0;


}复制错误复制成功

在这里 $Sub$$main 函数仅仅调用了 rtthread_startup() 函数。RT-Thread 支持多种平台和多种编译器,而 rtthread_startup() 函数是 RT-Thread 规定的统一入口点,所以 $Sub$$main 函数只需调用 rtthread_startup() 函数即可。例如采用 GNU GCC 编译器编译的 RT-Thread,就是直接从汇编启动代码部分跳转到 rtthread_startup() 函数中,并开始第一个 C 代码的执行的。在 components.c 的代码中找到 rtthread_startup() 函数,我们将可以看到 RT-Thread 的启动流程:

int rtthread_startup(void)
 {
 rt_hw_interrupt_disable();/* board level initalization
 * NOTE: please initialize heap inside board initialization.
 */
 rt_hw_board_init();/* show RT-Thread version */
 rt_show_version();/* timer system initialization */
 rt_system_timer_init();/* scheduler system initialization */
 rt_system_scheduler_init();#ifdef RT_USING_SIGNALS
 /* signal system initialization */
 rt_system_signal_init();
 #endif/* create init_thread */
 rt_application_init();/* timer thread initialization */
 rt_system_timer_thread_init();/* idle thread initialization */
 rt_thread_idle_init();/* start scheduler */
 rt_system_scheduler_start();/* never reach here */
 return 0;


}复制错误复制成功

这部分启动代码,大致可以分为四个部分

  • 初始化与系统相关的硬件;
  • 初始化系统内核对象,例如定时器,调度器;
  • 初始化系统设备,这个主要是为 RT-Thread 的设备框架做的初始化;
  • 初始化各个应用线程,并启动调度器。

用户入口代码

上面的启动代码基本上可以说都是和 RT-Thread 系统相关的,那么用户如何加入自己的应用程序的初始化代码呢?RT-Thread 将 main 函数作为了用户代码入口,只需要在 main 函数里添加自己的代码即可。

int main(void)
 {
 /* user app entry */
 return 0;