IOT-OS之RT-Thread(二)--- CPU架构与BSP移植过程

一、RT-Thread内核简介

前篇系统启动与初始化过程介绍了RT-Thread总体架构及系统启动和初始化过程,本篇在介绍该系统移植过程前,先介绍下系统内核架构,RT-Thread内核架构图如下:
RT-Thread内核架构图
从上图可以看出,将RT-Thread内核移植到某款芯片或板卡上,可以分为两大部分:CPU芯片移植和板级支持包BSP移植。RT-Thread为了能更方便的在不同CPU架构和不同板卡上移植,分别抽象出libcpu抽象层和BSP设备驱动层,两个抽象层向上对内核提供统一的调用接口,向下分别提供一套CPU架构和BSP移植接口,方便用户将RT-Thread移植到多数CPU架构和板卡上,降低了移植难度和工作量。

最上层是RT-Thread的内核部分,对比前面介绍的UCOS内核部分(UCOS任务调度器任务间通信),最大的区别主要有两个:一个是使用了统一的对象管理架构,另一个是增加了设备管理层。其余的部分如线程管理与调度器、线程间同步与通信、时钟管理与内存管理等RTOS内核基本要素的实现原理跟UCOS类似,只是API接口不同。下面先简单看下RT-Thread的内核对象管理架构:
RT-Thread内核对象模型
RT-Thread采用内核对象管理系统来访问、管理所有内核对象(包括线程、信号量、互斥量、事件、邮箱、消息队列、定时器、内存池、设备驱动等),各类内核对象间有类似面向对象的派生和继承关系,对于每一种具体的内核对象和对象控制块,除了基本结构(抽象对象rt_object)外,还有自己的扩展私有属性。从面向对象的观点,可以认为每一种具体对象是抽象对象的派生,继承了基本对象的属性并在此基础上扩展了与自己相关的属性。

这种设计方法的优点主要有两个:

  • 提高了系统的可重用性和扩展性,增加新的对象类别很容易,只需要继承通用对象的属性再加少量扩展即可;
  • 提供统一的对象操作方式,简化了各种具体对象的操作,提高了系统的可靠性。

RT-Thread的对象管理架构和设备驱动管理层在后面的博客中再详细介绍,下面看RT-Thread的CPU架构移植与BSP移植过程,以手边的STM32L475潘多拉开发板为例。

二、RT-Thread CPU架构移植

在嵌入式领域有很多种不同的CPU架构(例如Cortex-M / MIPS32 / RISC-V等),为了使RT-Thread能够在不同CPU架构的芯片上运行,RT-Thread提供了一个libcpu抽象层来适配不同的CPU架构,libcpu向上对内核提供统一的接口,包括全局中断开关、线程栈初始化、上下文切换等。下面以Cortex-M CPU 架构为例介绍其移植过程。

2.1 Cortex-M CPU 架构简介

之前介绍UCOS中断管理与定时器时介绍过Cortex-M3的一些基础,本文移植对象STM32L475是Cortex-M4架构的,M4相比M3主要增加了FPU浮点运算单元,相应的增加了不少跟FPU相关的寄存器,Cortex-M4的寄存器主要如下图所示:
Cortex-M寄存器简介
通用寄存器R0-R15每个寄存器的作用如下图所示(参考自:Procedure Call Standard for the ARM® Architecture):
Cortex-M通用寄存器作用
特殊功能寄存器作用如下:
Crotex-M特殊功能寄存器
具有浮点单元的 Cortex-M4 或者 Cortex-M7,控制寄存器也用来指示浮点单元当前是否在使用,浮点单元包含了 32 个浮点通用寄存器 S0~S31 和特殊 FPSCR 寄存器(Floating point status and control register)。

Cortex-M 引入了操作模式和特权级别的概念,分别为线程模式和处理者模式,如果进入异常或中断处理则进入处理模式,其他情况则为线程模式,特权状态或工作模式可由CONTROL特殊寄存器控制,工作模式状态切换情况如下图:
Crotex-M工作模式切换
Cortex-M的中断过程在之前的博客UCOS中断管理与定时器已经介绍过了,这里就不再赘述了,下面介绍RT-Thread对中断的管理机制。

2.2 RT-Thread 中断机制

RT-Thread 中断管理中,将中断处理程序分为中断前导程序、用户中断服务程序、中断后续程序三部分,如下图:
RT-Thread中断处理过程
三部分的主要作用如下:

  • 中断前导程序:保存 CPU 中断现场,处理器硬件将当前运行部分的上下文寄存器( PSR、PC、LR、R12、R3-R0 寄存器)自动压入中断栈中,并通知内核进入中断状态;
  • 用户中断服务程序:分为两种情况,第一种情况是不进行线程切换,用户中断服务程序和中断后续程序运行完毕后退出中断模式,返回被中断的线程;另一种情况是,在中断处理过程中需要进行线程切换,这种情况会调用 rt_hw_context_switch_interrupt() 函数进行上下文切换,该函数跟 CPU 架构相关,不同 CPU 架构的实现方式有差异;
  • 中断后续程序:通知内核离开中断状态,恢复中断前的 CPU 上下文,如果在中断处理过程中未进行线程切换,那么恢复 from 线程的 CPU 上下文,如果在中断中进行了线程切换,那么恢复 to 线程的 CPU 上下文,这部分实现也跟 CPU 架构相关,不同 CPU 架构的实现方式有差异。

在进行中断处理时(实质是调用用户的中断服务程序函数),中断处理函数中很可能会有自己的局部变量,这些都需要相应的栈空间来保存,所以中断响应依然需要一个栈空间来做为上下文,运行中断处理函数。中断栈可以保存在打断线程的栈中,当从中断中退出时,返回相应的线程继续执行;也可以与线程栈完全分离开来,即每次进入中断时,在保存完打断线程上下文后,切换到新的中断栈中独立运行,在中断退出时,再做相应的上下文恢复。RT-Thread 采用的方式是提供独立的中断栈,即中断发生时,中断的前期处理程序会将用户的栈指针更换到系统事先留出的中断栈空间中,等中断退出时再恢复用户的栈指针。这样中断就不会占用线程的栈空间,从而提高了内存空间的利用率,且随着线程的增加,这种减少内存占用的效果也越明显。

RT-Thread 不对中断服务程序所需要的处理时间做任何假设、限制,但用户需要保证所有的中断服务程序在尽可能短的时间内完成(中断服务程序在系统中相当于拥有最高的优先级,会抢占所有线程优先执行)。如果中断服务程序在取得硬件状态或数据以后,还需要进行一系列更耗时的处理过程,通常需要将该中断分割为两部分,即上半部分(Top Half)和下半部分(Bottom Half)。在上半部分中,取得硬件状态和数据后,打开被屏蔽的中断,给相关线程发送一条通知(可以是 RT-Thread 所提供的信号量、事件、邮箱或消息队列等方式),然后结束中断服务程序;而接下来,相关的线程在接收到通知后,接着对状态或数据进行进一步的处理,这一过程称之为下半部处理(Linux也有丰富的下半部处理和推后执行机制,比如软中断、Tasklet、工作队列等)。

为了把操作系统和系统底层的异常、中断硬件隔离开来,RT-Thread 把中断和异常封装为一组抽象接口,如下图所示:
RT-Thread中断管理接口
上面的接口函数中有一部分需要在CPU架构移植时实现,但有些API不会出现在每个移植分支中,例如rt_hw_interrupt_install()、rt_hw_interrupt_mask()/unmask()就不会出现在Cortex-M0/M3/M4 的移植分支中,其余中断接口函数的实现见下文CPU架构移植。

2.3 CPU 架构移植

RT-Thread 的 libcpu 抽象层向下提供了一套统一的 CPU 架构移植接口,这部分接口包含了全局中断开关函数、线程上下文切换函数、时钟节拍的配置和中断函数、Cache 等等内容,下表是 CPU 架构移植需要实现的接口和变量:

函数和变量 描述
rt_base_t rt_hw_interrupt_disable(void); 关闭全局中断;
void rt_hw_interrupt_enable(rt_base_t level); 打开全局中断;
rt_uint8_t *rt_hw_stack_init(void *tentry,
void *parameter, rt_uint8_t *stack_addr, void *texit);
线程栈的初始化,内核在线程创建和
线程初始化里面会调用这个函数;
void rt_hw_context_switch_to(rt_uint32 to); 没有来源线程的上下文切换,在调度器
启动第一个线程的时候调用,以及在
signal 里面会调用;
void rt_hw_context_switch(rt_uint32 from, rt_uint32 to); 从 from 线程切换到 to 线程,用于线程
和线程之间的切换;
void rt_hw_context_switch_interrupt(rt_uint32 from,
rt_uint32 to);
从 from 线程切换到 to 线程,用于中断
里面进行切换的时候使用;
rt_uint32_t rt_thread_switch_interrupt_flag; 表示需要在中断里进行切换的标志;
rt_uint32_t rt_interrupt_from_thread,
rt_interrupt_to_thread;
在线程进行上下文切换时候,用来保存
from 和 to 线程;

全局中断开关与堆栈初始化跟UCOS系统移植基本一致,Cortex-M 使用 CPS 指令(CPSID I / CPSIE I)实现全局快速开关中断。剩下最主要的就是在线程与中断中实现上下文切换的函数实现,首先是调度器启动第一个线程时,只有目标线程而没有来源线程,在rt_hw_context_switch_to实现切换到指定线程的功能,流程图如下(Crotex-M4架构MDK版该函数代码见:rt-thread-4.0.1\libcpu\arm\cortex-m4\context_rvds.S):
RT-Thread无来源线程切换到目标线程
第一个线程启动了,接下来的线程切换主要分两种:一种是线程到线程的上下文切换,另一种是中断到线程的上下文切换,两种上下文切换的过程对比如下:
线程到线程上下文切换
中断到线程上下文切换
对比两个过程可以发现,在 Cortex-M 内核里 rt_hw_context_switch() 和 rt_hw_context_switch_interrupt() 功能一致,都是在 PendSV 里完成剩余上下文的保存和回复。所以我们仅仅需要实现一份代码,简化移植的工作。这两个函数的流程图如下:
RT-Thread上下文切换流程
前面的线程上下文切换都是通过触发PendSV异常实现的,最后的切换还是要在PendSV中断处理函数 PendSV_Handler()中完成线程切换的实际工作,具体的流程图如下:
RT-Thread实现PendSV中断处理流程
RT-Thread 针对大多数常见的CPU架构,已经帮我们实现了CPU架构移植的接口函数(在目录rt-thread-4.0.1\libcpu中),只有少数不常见的CPU架构需要我们自己实现这些接口函数。我们开发板STM32L475的CPU架构是Cortex-M4,MDK版的移植接口函数实现在rt-thread-4.0.1\libcpu\arm\cortex-m4\context_rvds.S中,我们只需要在rtconfig.h中配置几个宏定义即可,比如针对我们开发板配置的宏定义如下:

//  rt-thread-4.0.1\bsp\stm32\libraries\templates\stm32l4xx\rtconfig.h

#define ARCH_ARM
#define ARCH_ARM_CORTEX_M
#define ARCH_ARM_CORTEX_M4

宏定义起作用的过程简单如下:

// rt-thread-4.0.1\libcpu\Kconfig
config ARCH_ARM
    bool

config ARCH_ARM_CORTEX_M
    bool
    select ARCH_ARM

config ARCH_ARM_CORTEX_M4
    bool
    select ARCH_ARM_CORTEX_M

// rt-thread-4.0.1\bsp\stm32\libraries\templates\stm32l4xx\rtconfig.py
# toolchains options
ARCH='arm'
CPU='cortex-m4'
CROSS_TOOL='gcc'

// rt-thread-4.0.1\libcpu\SConscript
if rtconfig.ARCH in list:
    group = group + SConscript(os.path.join(rtconfig.ARCH, 'SConscript'))

实现了libcpu接口函数,RT-Thread 就可以进行线程的创建、运行和切换了,但周期性的线程切换、对相同优先级线程的时间片轮转调度还需要时钟节拍的支持,下面看看时钟节拍的配置和处理函数实现代码:

// rt-thread-4.0.1\bsp\stm32\libraries\templates\stm32l4xx\rtconfig.h
#define RT_TICK_PER_SECOND 1000

// rt-thread-4.0.1\bsp\stm32\libraries\HAL_Drivers\drv_common.c
/* SysTick configuration */
void rt_hw_systick_init(void)
{
    HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq() / RT_TICK_PER_SECOND);
    HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);
    HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0);
}

/* This is the timer interrupt service routine.*/
void SysTick_Handler(void)
{
    /* enter interrupt */
    rt_interrupt_enter();

    HAL_IncTick();
    rt_tick_increase();

    /* leave interrupt */
    rt_interrupt_leave();
}

到这里RT-Thread 针对STM32L475的CPU架构移植就完成了,接下来介绍其BSP移植过程。

三、RT-Thread BSP移植过程

熟悉了RT-Thread的启动流程,接下来移植就简单了,要使用finsh组件需要控制终端,这里使用串口USART1作为控制终端,移植后想验证是否有问题,就使用RGB LED作为操作对象吧。所以,我们需要在CubeMX中配置USART1与RGB PIN。

3.1 RT-Thread移植过程

RT-Thread软件包中有大量的bsp文件,我们只针对手边的STM32L475潘多拉开发板作为移植对象,首先新建一个工程文件夹(这里取的文件夹名为RT-Thread_Projects,可任取),里面有四个文件夹docs、libraries、projects、rt-thread-4.0.1,分别保存文档、库文件、bsp工程文件、RT-Thread系统文件,这四个文件夹的内容来源如下表所示(下载的rt-thread-4.0.1源码在下表使用rt-thread-4.0.1-source表示):

文件夹名 文件夹内容
rt-thread-4.0.1 rt-thread-4.0.1-source中除bsp文件夹外所有内容;
libraries rt-thread-4.0.1-source\bsp\stm32\libraries中
HAL_Drivers、STM32L4xx_HAL与Kconfig;
docs rt-thread-4.0.1-source\bsp\stm32\docs中内容;
projects rt-thread-4.0.1-source\bsp\stm32\libraries\templates中的stm32l4xx;

整理好的目录结构如下图所示:
RT-Thread移植文件目录
我们的STM32L475潘多拉开发板,官方有提供移植好的代码,在.\rt-thread-4.0.1-source\bsp\stm32\stm32l475-atk-pandora目录下,这里是想了解移植过程,所以并没有直接使用官方移植好的代码,而是使用.\rt-thread-4.0.1-source\bsp\stm32\libraries \templates\stm32l4xx针对STM32L4系列的模板文件开始我们的系统移植,官方移植好的代码可以作为我们纠错对照的参考。

首先打开.\RT-Thread_Projects\projects\stm32l4xx\board\CubeMX_Config目录下的CubeMX_Config.ioc文件开始选择芯片型号并配置RGB LED PIN与USART1(对CubeMX不熟悉的可以参考博客:HAL详解与CubeMX使用),打开后芯片型号是STM32L475VETx,跟我们开发板的芯片型号和封装方式都一样,不用修改芯片型号了,USART1、RCC时钟树也为我们配置好了,我们添加RGB LED PIN的引脚,配置如下图所示:
CubeMX RGB LED PIN配置
USART1、NVIC、RCC及时钟树配置都不需要修改,工程管理需要修改下,模板文件中使用的HAL库版本是STM32Cube_FW_L4_V1.13.0,我这儿只下载了STM32Cube_FW_L4_V1.14.0版本,所以需要使用L4_V1.14.0版本重新生成工程代码,配置如下图所示:
CubeMX选择HAL库
选择好HAL库的路径后点击GENERATE CODE生成工程代码,这里需要注意的是基于不同版本的HAL库生成工程代码,.\RT-Thread_Projects\libraries\STM32L4xx_HAL中的HAL库文件也要跟着更新,所以我们把.\RT-Thread_Projects\projects\stm32l4xx\board \CubeMX_Config\Drivers目录的HAL库文件剪切到.\RT-Thread_Projects\libraries \STM32L4xx_HAL中。

如果想使用RT-Thread中的库文件,需要把.\RT-Thread_Projects\libraries \STM32L4xx_HAL这个文件夹名STM32L4xx_HAL改为Drivers,然后在CubeMX工程管理中选择HAL库路径时选择.\RT-Thread_Projects\libraries即可。改变目录名需要顺带改变Kconfig与SConstruct中的文件路径名,加之STM32L4xx_HAL目录名更具有辨识性,这里就保留原来的目录名,采用替换HAL库文件的方式处理,读者也可以将STM32L4xx_HAL复制到工程外再修改路径名并选择其作为CubeMX生成工程代码时调用的HAL库文件。

前面我们说到,需要CubeMX生成具体引脚的MspInit() / MspDeinit()函数,这对儿函数生成在文件projects\stm32l4xx\board\CubeMX_Config\Src\stm32l4xx_hal_msp.c中,我们在生成工程时,自然要将其包含进去,该文件是如何被包含到工程中的呢?CubeMX生成的文件中还有哪些文件也被调用了呢?这就需要我们到编译构建环境scons的脚本中寻找答案了,SConscript脚本中调用CubeMX生成文件的相关代码如下:

// projects\stm32l4xx\board\SConscript

Import('SDK_LIB')

cwd = GetCurrentDir()

# add general drivers
src = Split('''
board.c
CubeMX_Config/Src/stm32l4xx_hal_msp.c
''')

path =  [cwd]
path += [cwd + '/CubeMX_Config/Inc']

startup_path_prefix = SDK_LIB

从上面脚本代码看(python语法),projects\stm32l4xx\board\SConscript主要包含了CubeMX生成的CubeMX_Config/Src/stm32l4xx_hal_msp.c源文件和/CubeMX_Config /Inc头文件路径(实际主要是\Inc\stm32l4xx_hal_conf.h)。

下面开始修改代码,前面介绍rt_hw_board_init提到过board.c与board.h在移植中比较重要,一般需要修改board.c中的SystemClock_Config函数定义和board.h中的FLASH/RAM宏定义。下面先将projects\stm32l4xx\board\CubeMX_Config\Src\main.c中的SystemClock_Config拷贝到projects\stm32l4xx\board\board.c中,代码如下:

// projects\stm32l4xx\board\board.c

/**
  * @brief System Clock Configuration
  * @retval None
  */
void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
  RCC_PeriphCLKInitTypeDef PeriphClkInit = {0};

  /** Initializes the CPU, AHB and APB busses clocks 
  */
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
  RCC_OscInitStruct.HSEState = RCC_HSE_ON;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
  RCC_OscInitStruct.PLL.PLLM = 1;
  RCC_OscInitStruct.PLL.PLLN = 20;
  RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV7;
  RCC_OscInitStruct.PLL.PLLQ = RCC_PLLQ_DIV2;
  RCC_OscInitStruct.PLL.PLLR = RCC_PLLR_DIV2;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }
  /** Initializes the CPU, AHB and APB busses clocks 
  */
  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                              |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4) != HAL_OK)
  {
    Error_Handler();
  }
  PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_USART1;
  PeriphClkInit.Usart1ClockSelection = RCC_USART1CLKSOURCE_PCLK2;
  if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK)
  {
    Error_Handler();
  }
  /** Configure the main internal regulator output voltage 
  */
  if (HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1) != HAL_OK)
  {
    Error_Handler();
  }
}

接下来修改board.h中FLASH/SRAM的宏定义,我们的STM32L475开发板FLASH为512KB,SRAM为128KB,board.h中配置的FLASH为512KB、SARM1为96KB,我们要将SRAM1改为128KB吗?查阅SMT32L475VE Reference manual有一句话“96 Kbytes SRAM1 and 32 Kbyte SRAM2 on STM32L475xx/476xx/486xx devices”,也就是说STM32L475中SRAM虽然有128KB,但分为两部分SRAM1与SRAM2,两部分也并非连续的,我们能用的最大连续空间SRAM1只有96KB,所以board.h中FLASH与SRAM的配置维持默认即可,配置宏定义如下(如果FLASH / SRAM SIZE修改的话,对于MDK V5 IDE链接脚本文件projects\stm32l4xx\board\linker_scripts\link.sct也需要一起修改哈,详见docs\STM32系列BSP制作教程.md):

// projects\stm32l4xx\board\board.h

#define STM32_FLASH_START_ADRESS       ((uint32_t)0x08000000)
#define STM32_FLASH_SIZE               (512 * 1024)
#define STM32_FLASH_END_ADDRESS        ((uint32_t)(STM32_FLASH_START_ADRESS + STM32_FLASH_SIZE))

#define STM32_SRAM1_SIZE               (96)
#define STM32_SRAM1_START              (0x20000000)
#define STM32_SRAM1_END                (STM32_SRAM1_START + STM32_SRAM1_SIZE * 1024)

到这里RT-Thread移植基本就完成了,看起来很简单,接下来使用ENV开发辅助工具通过图形化系统配置工具menconfig和编译构建环境scons重新生成工程,我们将工作目录切换到projects\stm32l4xx下,运行menuconfig系统图像配置命令(可以添加参数–help获取命令帮助信息,比如menuconfig --help),出现如下的提示:
menuconfig运行警告
运行menuconfig提示变量BSP_ROOT与RTT_ROOT未定义,且不能打开文件"…/…/…/Kconfig",menuconfig就是调用Kconfig中的配置信息,结构找不到文件,且两个路径名变量BSP_ROOT与RTT_ROOT未定义,我们在创建工程时重新组织了目录结构,所以这是提示我们根据新的目录结构修改路径名变量定义。我们打开projects\stm32l4xx\Kconfig文件,修改路径名变量定义后的代码如下:

// projects\stm32l4xx\Kconfig

mainmenu "RT-Thread Configuration"

config BSP_DIR
    string
    option env="BSP_ROOT"
    default "."

config RTT_DIR
    string
    option env="RTT_ROOT"
    default "../../rt-thread-4.0.1"

config PKGS_DIR
    string
    option env="PKGS_ROOT"
    default "packages"
 
source "$RTT_DIR/Kconfig"
source "$PKGS_DIR/Kconfig"
source "../../libraries/Kconfig"
source "board/Kconfig"

重新运行menuconfig正常进入系统图形配置界面,需要使能GPIO与USART1并保存配置,配置界面如下(为了提高效率,这里使能了USART1 RX DMA,想了解USART DMA的可以参考博客:USART + DMA + HAL):
menuconfig使能USART1
接下来使用编译构建环境scons重新生成工程,我们使用的是MDK V5 IDE,所以执行命令scons --target = mdk5(可以通过scons --help查看该命令的帮助信息,比如支持哪些参数,提供哪些功能),命令执行结果如下:
scons运行警告
跟前面运行Kconfig结构类似,也是因为我们重新组织了目录结构,导致找不到相关文件,需要重新定义路径名变量,打开projects\stm32l4xx\SConstruct修改RTT_ROOT和SDK_ROOT两个路径名变量,修改后的部分代码如下(该文件代码较长,只截取修改部分代码):

// projects\stm32l4xx\SConstruct
......
if os.getenv('RTT_ROOT'):
    RTT_ROOT = os.getenv('RTT_ROOT')
else:
    RTT_ROOT = os.path.normpath(os.getcwd() + '/../../rt-thread-4.0.1')

sys.path = sys.path + [os.path.join(RTT_ROOT, 'tools')]
......
SDK_ROOT = os.path.abspath('../../')

if os.path.exists(SDK_ROOT + '/libraries'):
    libraries_path_prefix = SDK_ROOT + '/libraries'
else:
    libraries_path_prefix = os.path.dirname(SDK_ROOT) + '/libraries'

SDK_LIB = libraries_path_prefix
Export('SDK_LIB')

接下来重新运行scons --target=mdk5命令,编译正常完成,运行结果如下:
scons编译目标工程完成
接下来就可以打开projects\stm32l4xx\project.uvprojx工程文件,在main.c中编写我们的应用程序了。在编写应用程序前,这里再提下工程模板projects\stm32l4xx \template.uvprojx的配置,有时候我们选择的模板\bsp\stm32\libraries\templates里面的芯片型号跟我们手头开发板的芯片型号并不一致,除了需要在CubeMX中重新选择芯片进行配置外,工程模板projects\stm32l4xx\template.uvprojx也需要重新选择芯片进行配置,本例模板中的芯片型号一致,我们选择ARM Compiler V6以便加快MDK V5的编译速度(如何配置可参考博客:HAL详解与CubeMX使用),修改完工程模板后重新运行scons --target=mdk5命令生成工程文件projects\stm32l4xx\project.uvprojx,接下来开始编写应用程序。

3.2 编写应用验证移植结果

我们使用CubeMX配置了RGB LED PIN与USART1,USART1移植结果可以通过finsh组件或者rt_kprintf函数验证,RGB LED PIN可以通过控制点亮RGB LED的颜色实现。我们先定义RGB LED PIN的引脚及RGB的颜色枚举类型,代码如下:

// projects\stm32l4xx\applications\main.c

/* defined the LED_R/LED_G/LED_B pin: PE7/PE8/PE9 */
#define LED_R    GET_PIN(E, 7)
#define LED_G    GET_PIN(E, 8)
#define LED_B    GET_PIN(E, 9)

/* defined RGB LED Color enum */
typedef enum{
	RED,
	GREEN,
	BLUE,
	YELLOW,
	PURPLE,
	CYAN,
	WHITE,
	BLACK,
	MAX_NUM
}RGB_Color;

接着定义控制RGB LED点亮指定颜色的函数,红、绿、蓝三种颜色的亮灭可以组合出8种颜色,实现代码如下:

// projects\stm32l4xx\applications\main.c

void RGB_ON(RGB_Color RGB_LED)
{
	switch(RGB_LED % MAX_NUM)
		{
			case RED:
				rt_pin_write(LED_R, PIN_LOW);
				rt_pin_write(LED_G, PIN_HIGH);
				rt_pin_write(LED_B, PIN_HIGH);
        		rt_kprintf("red led on.\n");
				break;
			case GREEN:
				rt_pin_write(LED_R, PIN_HIGH);
				rt_pin_write(LED_G, PIN_LOW);
				rt_pin_write(LED_B, PIN_HIGH);
        		rt_kprintf("green led on.\n");
				break;
			case BLUE:
				rt_pin_write(LED_R, PIN_HIGH);
				rt_pin_write(LED_G, PIN_HIGH);
				rt_pin_write(LED_B, PIN_LOW);
        		rt_kprintf("blue led on.\n");
				break;
			case YELLOW:
				rt_pin_write(LED_R, PIN_LOW);
				rt_pin_write(LED_G, PIN_LOW);
				rt_pin_write(LED_B, PIN_HIGH);
        		rt_kprintf("yellow led on.\n");
				break;
			case PURPLE:
				rt_pin_write(LED_R, PIN_LOW);
				rt_pin_write(LED_G, PIN_HIGH);
				rt_pin_write(LED_B, PIN_LOW);
        		rt_kprintf("purple led on.\n");
				break;
			case CYAN:
				rt_pin_write(LED_R, PIN_HIGH);
				rt_pin_write(LED_G, PIN_LOW);
				rt_pin_write(LED_B, PIN_LOW);
        		rt_kprintf("cyan led on.\n");
				break;
			case WHITE:
				rt_pin_write(LED_R, PIN_LOW);
				rt_pin_write(LED_G, PIN_LOW);
				rt_pin_write(LED_B, PIN_LOW);
        		rt_kprintf("white led on.\n");
				break;
			default:
				rt_pin_write(LED_R, PIN_HIGH);
				rt_pin_write(LED_G, PIN_HIGH);
				rt_pin_write(LED_B, PIN_HIGH);
        		rt_kprintf("led off.\n");
				break;
		}
}

最后在main函数中实现控制逻辑,代码如下:

// projects\stm32l4xx\applications\main.c

int main(void)
{
    unsigned int count = 0;

	/* set LED_R/LED_G/LED_B pin mode to output */
	rt_pin_mode(LED_R, PIN_MODE_OUTPUT);
    rt_pin_mode(LED_G, PIN_MODE_OUTPUT);
    rt_pin_mode(LED_B, PIN_MODE_OUTPUT);

    while (count < 8)
    {
        RGB_ON(count);
        rt_thread_mdelay(1000);
		count++;
    }
    return RT_EOK;
}

上面的代码逻辑比较简单,就不详细解释了,到这里应用就编写完成了,编译无错误,烧录到我们的STM32L475潘多拉开发板中运行正常,串口输出结果如下(这里使用了putty而没有使用传统的串口工具,主要是觉得putty更方便finsh组件shell式的交互,putty需要以管理员身份运行,否则可能会运行异常,想了解更多putty玩法可参考博客:精彩PuTTY 中文教程):
RT-Thread移植验证RGB LED
从运行结果来看,移植基本成功,RGB LED等运行正常,USART1打印输出也正常,但按下Tab键或者发送help命令,finsh组件无响应,看来我们的移植还是有点问题,问题在哪呢?

前面介绍Finsh组件和RT-Thread自动初始化机制时了解到,RT-Thread自动初始化机制维护了一个自定义RTI符号段用来暂存初始化函数表,Finsh组件也维护了两个自定义符号段FSymTab与VSymTab,这三个自定义符号段是不是被编译器优化掉了?根据之前使用mdk的经验,假如有一个有用的函数你定义了但是没有显式调用,mdk在默认方式下会把这个函数从整个程序中删除掉,以节省ROM,所以我们的怀疑也是有理由的。接下来就是到百度/谷歌搜索,看怎么保留这三个符号段。

根据搜索到的信息,我们需要在如下图所示的链接选项中,添加链接控制项“–keep .o(.rti_fn.) --keep *.o(FSymTab) --keep *.o(VSymTab)”,以便在MDK V5 IDE编译链接时保留RT-Thread自定义符号段,配置界面如下:
编译链接时保留RT-Thread自定义符号段
同时为了验证RT-Thread自动初始化和Finsh组件功能,我们把main函数内的三个rt_pin_mode函数放到外面一个RGB_LED_init函数中,并使用INIT_APP_EXPORT(RGB_LED_init)进行自动初始化(main函数中这三个函数调用就可以移除了);我们再新编写一个带参数的函数RGB_Control(int argc, char **argv),并使用MSH_CMD_EXPORT_ALIAS(RGB_Control,RGB,RGB Sample: RGB <0-7>)导出带参数的自定义函数,添加代码如下:

// projects\stm32l4xx\applications\main.c

#include <stdlib.h>

int RGB_LED_init(void)
{
    /* set LED_R/LED_G/LED_B pin mode to output */
    rt_pin_mode(LED_R, PIN_MODE_OUTPUT);
    rt_pin_mode(LED_G, PIN_MODE_OUTPUT);
    rt_pin_mode(LED_B, PIN_MODE_OUTPUT);

    return 0;
}

INIT_APP_EXPORT(RGB_LED_init);

static int RGB_Control(int argc, char **argv)
{
	if(argc != 2){
		rt_kprintf("Please input 'RGB_ON <0-7>'\n");
		return -1;
	}
	
	RGB_ON(atoi(argv[1]));

	return 0;
}

MSH_CMD_EXPORT_ALIAS(RGB_Control,RGB,RGB Sample: RGB <0-7>);

编译出现了一条警告信息“.\build\keil\Obj\rt-thread.axf: Warning: L6319W: Ignoring --keep command. Cannot find section *.o(VSymTab).”,说找不到自定义符号段VSymTab,查看下projects\stm32l4xx\rtconfig.h中finsh相关的配置,发现#define FINSH_USING_MSH_ONLY宏定义开启,相当于关闭了finsh c-style模式,自然用不上VSymTab了。考虑到开启finsh c-style模式会占用更多的空间,msh模式够用且友好,所以保留#define FINSH_USING_MSH_ONLY宏定义开启配置,在上面添加的MDK V5 IDE链接控制项中去除“–keep *.o(VSymTab)”即可,当然忽略该警告也可以。

再次编译运行正常,RT-Thread自动初始化机制运行也正常(从RGB LED灯正常被点亮,说明RGB LED PIN被正常初始化了),Finsh组件运行也正常(finsh为我们提供了一部分常用的命令,比如查看线程与设备列表命令),按我们导出的自定义函数格式输入命令,比如“RGB 1”则开发板绿灯亮起,运行结果如下:
RT-Thread移植finsh组件验证结果
如果在menuconfig中更改了配置,使用scons重新编译生成工程后,每次都需要添加链接控制项“–keep .o(.rti_fn.) --keep *.o(FSymTab)”,觉得比较麻烦,能否让scons编译生成的工程自带该链接控制项呢?

我们根据scons脚本语法,对于自定义符号段RTI依赖于宏定义RT_USING_COMPONENTS_INIT,属于RT-Thread内核部分,我们修改内核部分的编译脚本rt-thread-4.0.1\src\SConscript,添加如下代码:

// rt-thread-4.0.1\src\SConscript

if rtconfig.CROSS_TOOL == 'keil':
    if GetDepend('RT_USING_COMPONENTS_INIT'):
        LINKFLAGS = '--keep *.o(.rti_fn.*)'
else:
    LINKFLAGS = ''

group = DefineGroup('Kernel', src, depend = [''], CPPPATH = CPPPATH, LINKFLAGS = LINKFLAGS)

Return('group')

上面的编译脚本新增了链接选项LINKFLAGS,该链接选项依赖于编译工具keil和宏定义RT_USING_COMPONENTS_INIT。

下面看FinSH自定义符号段FSymTab与VSymTab如何添加到链接选项,这两个自定义符号段属于FinSH组件,我们找到FinSH组件的编译脚本rt-thread-4.0.1\components \finsh\SConscript,添加如下代码:

// rt-thread-4.0.1\components\finsh\SConscript

if rtconfig.CROSS_TOOL == 'keil':
    LINKFLAGS = '--keep *.o(FSymTab)'

    if not GetDepend('FINSH_USING_MSH_ONLY'):
        LINKFLAGS = LINKFLAGS + '--keep *.o(VSymTab)'
else:
    LINKFLAGS = ''

group = DefineGroup('finsh', src, depend = ['RT_USING_FINSH'], CPPPATH = CPPPATH, LINKFLAGS = LINKFLAGS)

Return('group')

上面的编译脚本同样新增了链接选项LINKFLAGS,两个自定义符号段依赖的宏定义有所不同,能根据宏定义RT_USING_FINSH判断是否添加FSymTab自定义符号段;在宏定义RT_USING_FINSH生效的情况下,根据宏定义FINSH_USING_MSH_ONLY判断是否添加VSymTab自定义符号段。

在上面的两个编译脚本新增链接选项后,重新使用scons --target=mdk5命令编译生成keil MDK V5工程文件,打开工程发现需要的链接选项已经自动添加进去了,不需要我们每次重新生成工程再手动添加了。

到这里RT-Thread就移植完成了,而且也经过验证了,目前看移植没什么问题,移植代码下载地址:https://github.com/StreamAI/RT-Thread_Projects/tree/master/projects/stm32l475

更多文章:

发布了65 篇原创文章 · 获赞 35 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/m0_37621078/article/details/100715601