RTThread与CubeMX – (3)初识RTThread


说了那么多的CubeMX,现在终于轮到主角RT-Thread出场了。RT-Thread是一款开源的实时操作系统,代码完全开源,可以从官方网站下载,也可以从github上clone一个最新的版本。我选择从git上clone一个,这样能顺便追踪到我做的修改。

rtthread用了scons作为编译工具,这个工具是基于python做的。python算是比较奇葩的一个东西,2.x版本和3.x版本不兼容。这个工具是2.x版本的python做的,所以要用这个工具构建之前要先下载python2x,然后安装scons。用命令行切换到scons解压后的目录,运行python setup.py install,就能安装scons了。如果提示“python不是内部命令或可执行程序”,说明python没有添加PATH环境变量中,可以通过在“我的电脑”中修改全局环境变量,也可以在命令行中用“set PATH=%PATH%;C:\Pyhton27”命令将其加入环境变量,不过这样的环境变量在命令行窗口退出后就失效了,好处是不会修改全局的环境变量。我比较喜欢这样的方式,建立一个bat文件,把设置环境变量的工作放在bat文件中,并启动一个命令行界面,然后在这个命令行界面中编译代码。下面是我建立rtthread编译环境的bat脚本代码,先设置pyhton和scons的目录,然后启动cmd。在这个bat文件执行后启动的命令行界面中执行 scons –target=”mdk5″ 就能进行rtthread的编译了。

set PATH=%PATH%;C:/Python27;C:/Python27/Scripts
cmd

当一台电脑上有多个不同的工具链的时候,这样的配置方式工具链间不会互相影响。关于如何使用scons来管理rtthread的代码,这里有更加详细的说明。

实际测试了一下,用scons编译代码比用MDK编译要快一些,开发过程中我会用scons编译代码,然后再用MDK来debug。scons编译生成axf文件在项目的根目录中,而用scons生成的mdk工程生成的axf文件在build目录中。为了让mdk能够下载调试,把生成的axf文件复制到了build目录下。

rtthread的代码目录结构如下图,bsp目录是针对不同硬件的平台的代码。我们的开发板代码就放到这个目录下面,在bsp目录下新取一个名叫stm32f767zi-nucleo目录,把我们这块板子相关的代码放到这个目录里面。在这个新建的目录下面放入CubeMX生成的初始化代码。通过bsp中有其他示例工程,可以帮助我们构建我们的开发板工程。这里我选stm32f7-disco里面的工程,这也是一个M7内核的开发板,移植起来相对更容易。简单分析一下原来工程中的代码,对rtthread的配置都在rtconfig.h这个头文件中,运行scons的时候会根据这个配置文件把相关的模块加入编译。目录结构如下

.sct .ld .icf分别是mdk,gcc和iar平台下的链接脚本,定义了器件的内存布局。初看了一下mdk的链接脚本,里面的地址定义有些不对,顺手修改一下。关于scons*和rtconfig*文件,在rtt官网有较详细的说明。这些文件可以用记事本打开后编辑,点开看看会发现rtconfig.py是关于工具链的一些配置,rtconfig.h是rtt模块的配置。sconsturct是关于整个工程的一些配置,sconscript是遍历了一下当前目录下的子目录,然后对子目录下的sconscript做了一些操作。看起来源代码就是sconscript组织起来的,看看rtt的说明中也确实如此。

这个bsp下有三个子目录,libraries,drivers和applications,分别对应着库,驱动和应用程序。库里面又分了芯片对应的HAL库和cmsis库。drivers目录中是当前开发版所需要的驱动,applicaions目录中就是开发板中要跑的应用了。看看不同目录下的sconscript文件,基本上差不多,都是把一些文件找出来,加入到一个group中。同时把一些头文件的路径也加到group中。如果当前目录中没文件,那就继续遍历子目录。最后是把遍历的结果或者新定义的group返回。

drivers下面有很多的驱动,我们只是把板子跑起来,可以只重点关注led和uart这两个驱动以及最重要的board驱动。led驱动里面有配置led引脚的代码,还有一个启动线程闪灯的函数led_init。这个led_init函数通过一个INIT_APP_EXPORT进行导出。这种XXX_EXPORT的宏在rtthread中用得很普遍。看起不需要注册,不需要调用就可以用起来了。比如这个led,不需要在别的代码中调用led_init,就可以闪灯了。其实rtthtread这里用了一个链接技巧。在rtdef.h文件中,各种XXX_EXPORT实际上是定义了一个__rt_init开头的变量,这个变量放在了一个特定section里面。这个变量的值就是要export的函数地址。编译器会把相同section的代码放到一起,在 components.c文件中,rtthread用一个特殊的方式找到了这些变量的起止地址,然后调用这些export的函数。这样看起来这些函数没有被调用,实际上会执行。这样的写法将代码和编译器耦合在了一起,并且在source insight这样的代码阅读工具中查找不到调用关系。关于components初始化,在rtthread的官方网站上有更详细的说明。

components.c还做了别的一些事情,对于armcc编译器,他定义了一个$super$$main的函数,在这个函数里面调用了rtthread_startup函数。这个startup是rtt系统启动函数,在这个里面完成了rtt个模块的初始化工作,最后启动线程调度,开始干活。没去看编译器手册,猜测这个super main应该会先于普通的main函数调用,并且是在crt完成初始化之后调用。startup中的rt_application_init函数会创建一个叫“main”的线程来调用main函数。这样看来,在rtt的设计逻辑中,main函数执行前所有的东西都已经初始化完成,main只不过是一个叫main的线程,和其他线程并无二致。startup中的 rt_hw_board_init在不同的硬件中有不同的实现,由不同的工程各自实现。需要注意的是,在components的starup函数中,处理流程是先进行硬件初始化,然后初始化rtt相关模块,再创建main线程,空闲线程,最后启动调度器。调度器启动之后代码再也不会回到startup中来了,只会执行之前创建的线程。因此为启动代码分配的栈空间可以回收了用作他途。在rtt中,栈空间被处理成了堆(heap)的一部分,这里又用到了一个编译器相关的东西。这样设计启动代码的好处是启动代码占用的栈空间可以回收,但是要注意的是,在hw init中,系统调度还没有开始,全局中断也处于关闭状态。如果init函数中用到了延时一类的函数,有可能无法正常工作。比如st的USB的库函数用到了延时,如果放到board init中调用会工作不正常。

applications下面的代码就更简单了,sram我们的开发板没有,不去管他,main里面是空的,啥都没有。

在rtconfig.py中,把stm32的类型从756改成767,注意下面的宏定义也需要修改。默认的cross_tool改成armcc,这样编译的时候只要简单敲一个scons就行了。

简单梳理之后,我们要新建一个rtt工程,需要在rtconfig.h中定义好我们需要的模块,至少需要一个board硬件配置的文件,需要一个实现main函数的文件,其他的驱动文件根据项目实际需要添加。