前言

最近做的一个小项目需要使用到RT-Thread,借此机会了解了一下RT-Thread和它提供的BSP框架,并简单分析移植方法。

关于RT-Thread和它的BSP,官方已经提供了比较详尽的文档,并且目前已经有部分厂家(比如最常见的STM32)的BSP模版可供使用:

BSP工程目录总览

本文以笔者手上的weact的stm32h750开发板BSP为例进行分析

  • Applications:应用层代码
  • Compiler:ARM的C语言库
  • DeviceDrivers:RT-Thread的Device框架驱动程序
  • Drivers:包括Device框架中各种设备在STM32外设上的实现以及开发板上外设的驱动程序
  • FAL,Filesystem,Finsh:都是一些RT-Thread的可选组件
  • Kernel:RT-Thread内核文件
  • libcpu:RT-Thread内核移植需要进行实现的API和一些与单片机内核相关的函数
  • Libraries:HAL库

如何让内核在你的芯片上跑起来

内核的移植首先是拷贝操作系统内核源码,然后对中断使能,上下文切换等依赖于具体单片机内核的功能进行实现

操作系统源码

这部分代码放在rt-thread仓库的src目录下,对应的头文件在include目录下。注意到这堆文件都加了个编译预处理选项:

找了一下发现这个宏定义出现在了include/rtsched.h里:

/**
 * NOTE: user should NEVER use these APIs directly. See rt_thread_.* or IPC
 * methods instead.
 */
#if defined(__RT_KERNEL_SOURCE__) || defined(__RT_IPC_SOURCE__)

/* thread initialization and startup routine */
void rt_sched_thread_init_ctx(struct rt_thread *thread, rt_uint32_t tick, rt_uint8_t priority);
void rt_sched_thread_init_priv(struct rt_thread *thread, rt_uint32_t tick, rt_uint8_t priority);
void rt_sched_thread_startup(struct rt_thread *thread);

/* scheduler related routine */
void rt_sched_post_ctx_switch(struct rt_thread *thread);
rt_err_t rt_sched_tick_increase(void);

/* thread status operation */
rt_uint8_t rt_sched_thread_get_stat(struct rt_thread *thread);
rt_uint8_t rt_sched_thread_get_curr_prio(struct rt_thread *thread);
rt_uint8_t rt_sched_thread_get_init_prio(struct rt_thread *thread);
rt_err_t rt_sched_thread_yield(struct rt_thread *thread);
rt_err_t rt_sched_thread_close(struct rt_thread *thread);
rt_err_t rt_sched_thread_ready(struct rt_thread *thread);
rt_err_t rt_sched_thread_suspend(struct rt_thread *thread, rt_sched_lock_level_t level);
rt_err_t rt_sched_thread_change_priority(struct rt_thread *thread, rt_uint8_t priority);
rt_err_t rt_sched_thread_bind_cpu(struct rt_thread *thread, int cpu);
rt_uint8_t rt_sched_thread_is_suspended(struct rt_thread *thread);
rt_err_t rt_sched_thread_timer_stop(struct rt_thread *thread);
rt_err_t rt_sched_thread_timer_start(struct rt_thread *thread);
void rt_sched_insert_thread(struct rt_thread *thread);
void rt_sched_remove_thread(struct rt_thread *thread);

#endif /* defined(__RT_KERNEL_SOURCE__) || defined(__RT_IPC_SOURCE__) */

这个文件主要是一些线程管理相关的API,可以看出来这个宏定义是为了确保这些API只能由内核调用,防止用户在奇怪的地方调用。如果内核文件没加上这个宏定义会报符号未定义错误(笔者自己踩的坑),如果自己手动移植要注意这一点。

内核API实现

虽然RT-Thread的内核教程说要自己实现开关中断之类的函数,但是它已经针对一些常见单片机内核进行了实现,比如说h750的cortex-m7内核。这些API实现在rt-thread仓库的libcpu目录下,主要在context_rvds.Scpuport.c这两个文件里。

经过这两步RT-Thread内核已经可以成功编译并且在你的芯片上运行起来了,如果你只需要使用到这些功能的话其实可以选择RT-Thread的阉割nano版本,nano版本提供了完整的内核功能和一个不需要Device框架(后面会提到)版本的Finsh组件(就是一个命令行工具)

Device框架

嵌入式系统经常需要用到各种片上或者片外的设备,RT-Thread把硬件上的外设进行了抽象,由此诞生出了Device框架,也许是为了方便应用在不同硬件平台之间的移植?RT-Thread中的内核对象和设备等都采用了类似c++的类继承思想,常用的PIN设备,ADC设备,UART设备等都是在rt_device这个基类上派生出来的,当然你也可以创建自己的设备。

每种设备需要实现的API在官方文档中都有详细说明,以最简单的PIN设备为例,下面是在drv_gpio.c中实现的一个PIN设备硬件初始化函数:

int rt_hw_pin_init(void)
{
#if defined(__HAL_RCC_GPIOA_CLK_ENABLE)
    __HAL_RCC_GPIOA_CLK_ENABLE();
#endif

#if defined(__HAL_RCC_GPIOB_CLK_ENABLE)
    __HAL_RCC_GPIOB_CLK_ENABLE();
#endif

#if defined(__HAL_RCC_GPIOC_CLK_ENABLE)
    __HAL_RCC_GPIOC_CLK_ENABLE();
#endif

#if defined(__HAL_RCC_GPIOD_CLK_ENABLE)
    __HAL_RCC_GPIOD_CLK_ENABLE();
#endif

#if defined(__HAL_RCC_GPIOE_CLK_ENABLE)
    __HAL_RCC_GPIOE_CLK_ENABLE();
#endif

#if defined(__HAL_RCC_GPIOF_CLK_ENABLE)
    __HAL_RCC_GPIOF_CLK_ENABLE();
#endif

#if defined(__HAL_RCC_GPIOG_CLK_ENABLE)
#ifdef SOC_SERIES_STM32L4
    HAL_PWREx_EnableVddIO2();
#endif
    __HAL_RCC_GPIOG_CLK_ENABLE();
#endif

#if defined(__HAL_RCC_GPIOH_CLK_ENABLE)
    __HAL_RCC_GPIOH_CLK_ENABLE();
#endif

#if defined(__HAL_RCC_GPIOI_CLK_ENABLE)
    __HAL_RCC_GPIOI_CLK_ENABLE();
#endif

#if defined(__HAL_RCC_GPIOJ_CLK_ENABLE)
    __HAL_RCC_GPIOJ_CLK_ENABLE();
#endif

#if defined(__HAL_RCC_GPIOK_CLK_ENABLE)
    __HAL_RCC_GPIOK_CLK_ENABLE();
#endif

#if defined(__HAL_RCC_GPIOM_CLK_ENABLE)
    __HAL_RCC_GPIOM_CLK_ENABLE();
#endif

#if defined(__HAL_RCC_GPION_CLK_ENABLE)
    __HAL_RCC_GPION_CLK_ENABLE();
#endif

#if defined(__HAL_RCC_GPIOO_CLK_ENABLE)
    __HAL_RCC_GPIOO_CLK_ENABLE();
#endif

#if defined(__HAL_RCC_GPIOP_CLK_ENABLE)
    __HAL_RCC_GPIOP_CLK_ENABLE();
#endif

    return rt_device_pin_register("pin", &_stm32_pin_ops, RT_NULL);
}

该函数在rt_hw_board_init()调用(RT-Thread初始化过程可以参见官方文档),可以看到它的硬件初始化就是把用到的I/O口时钟打开,然后注册一个PIN设备, rt_device_pin_register中的第二个参数就是一堆API的函数指针:

static const struct rt_pin_ops _stm32_pin_ops =
{
    stm32_pin_mode,
    stm32_pin_write,
    stm32_pin_read,
    stm32_pin_attach_irq,
    stm32_pin_dettach_irq,
    stm32_pin_irq_enable,
    stm32_pin_get,
};

其他类型设备的初始化也大体遵循这个顺序,先把硬件初始化好,然后把相关的API注册上去

CUBEMX配置

经常用STM32的朋友都知道,我们可以用CUBEMX配置各种初始化配置。RT-Thread的Device框架写了很多现成的HAL库外设初始化函数,在rt-thread/bsp/stm32/libraries目录下,因此不需要用CUBEMX进行外设的配置,但是时钟树和外设的引脚配置还是需要用到CUBEMX,可以看到BSP目录底下开了一个CUBEMX工程:

这个工程里面其实就只有它生成的那个msp文件有用

注意这个选项不要勾,这样所有的初始化函数都会生成到一个c文件里,在外面的工程中包含这个c文件即可

至于时钟树的初始化函数,CUBEMX会把它生成到main.c里面,需要把它复制下来粘到工程里的board.c文件里

KConfig文件

RT-Thread提供了一个ENV工具方便对不需要的设备或组件进行裁剪,其实就是自动生成一个rtconfig.h文件,里面有一堆宏定义,用户在初始化函数中用#ifdef进行条件编译实现裁剪,下面是该BSP的KConfig文件:

menu "Hardware Drivers Config"

config SOC_STM32H750VBT6
    bool
    select SOC_SERIES_STM32H7
    select RT_USING_COMPONENTS_INIT
    select RT_USING_USER_MAIN
    default y

menu "Onboard Peripheral Drivers"

    config BSP_USING_SPI_FLASH
        bool "Enable SPI FLASH (W25Q64 spi1)"
        select BSP_USING_SPI
        select BSP_USING_SPI1
        select RT_USING_DFS
        select PKG_USING_LITTLEFS
        select RT_USING_MTD_NOR
        select RT_USING_FAL
        select FAL_USING_SFUD_PORT
        select RT_USING_SFUD
        select RT_SFUD_USING_SFDP
        default n

    config BSP_USING_QSPI_FLASH
        bool "Enable QSPI FLASH (W25Q64)"
        select BSP_USING_QSPI
        select FAL_USING_SFUD_PORT
        select RT_USING_SFUD
        select RT_SFUD_USING_QSPI
        default n

    config BSP_USING_LCD_SPI
        bool "Enable 0.96' TFT-LCD(ST7735S)"
        select BSP_USING_GPIO
        select BSP_USING_SPI
        select BSP_USING_SPI4
        # select BSP_SPI4_TX_USING_DMA
        select BSP_USING_PWM
        select BSP_USING_PWM1
        select BSP_USING_PWM1_CH2
        default n

        if BSP_USING_LCD_SPI
            choice
                prompt "choice back light"
                default LCD_BACKLIGHT_USING_GPIO
                config LCD_BACKLIGHT_USING_PWM
                    bool "LCD_BACKLIGHT_USING_PWM(tim1_ch2 pwm1)"
                config LCD_BACKLIGHT_USING_GPIO
                    bool "LCD_BACKLIGHT_USING_GPIO(PE10)"
            endchoice
        endif

endmenu

menu "On-chip Peripheral Drivers"

    config BSP_USING_GPIO
        bool "Enable GPIO"
        select RT_USING_PIN
        default y

    menuconfig BSP_USING_UART
        bool "Enable UART"
        default y
        select RT_USING_SERIAL
        if BSP_USING_UART
            config BSP_USING_UART1
                bool "Enable UART1"
                default y

            config BSP_UART1_RX_USING_DMA
                bool "Enable UART1 RX DMA"
                depends on BSP_USING_UART1 && RT_SERIAL_USING_DMA
                default n
        endif

    config BSP_USING_QSPI
        bool "Enable QSPI BUS"
        select RT_USING_QSPI
        select RT_USING_SPI
        default n

    menuconfig BSP_USING_SPI
        bool "Enable SPI BUS"
        default n
        select RT_USING_SPI
        if BSP_USING_SPI
            config BSP_USING_SPI1
                bool "Enable SPI1 BUS"
                default n

            config BSP_USING_SPI4
                bool "Enable SPI4 BUS"
                default n
        endif

    menuconfig BSP_USING_I2C1
        bool "Enable I2C1 BUS (software simulation)"
        default n
        select RT_USING_I2C
        select RT_USING_I2C_BITOPS
        select RT_USING_PIN
        if BSP_USING_I2C1
            config BSP_I2C1_SCL_PIN
                int "i2c1 scl pin number"
                range 1 216
                default 135
            config BSP_I2C1_SDA_PIN
                int "I2C1 sda pin number"
                range 1 216
                default 95
        endif

    menuconfig BSP_USING_PWM
        bool "Enable pwm"
        default n
        select RT_USING_PWM
        if BSP_USING_PWM
        menuconfig BSP_USING_PWM1
            bool "Enable timer1 output pwm"
            default n
            if BSP_USING_PWM1
                config BSP_USING_PWM1_CH1
                    bool "Enable PWM1 channel1"
                    default n

                config BSP_USING_PWM1_CH2
                    bool "Enable PWM1 channel2"
                    default n

                config BSP_USING_PWM1_CH3
                    bool "Enable PWM1 channel3"
                    default n

                config BSP_USING_PWM1_CH4
                    bool "Enable PWM1 channel4"
                    default n
            endif
        endif

    menuconfig BSP_USING_ADC
        bool "Enable ADC"
        default n
        select RT_USING_ADC
        if BSP_USING_ADC
            config BSP_USING_ADC1
                bool "Enable ADC1"
                default n

            config BSP_USING_ADC2
                bool "Enable ADC2"
                default n

            config BSP_USING_ADC3
                bool "Enable ADC3"
                default n
        endif

    config BSP_USING_ON_CHIP_FLASH
        bool "Enable on-chip FLASH"
        default n

    config BSP_USING_SDIO
        bool "Enable SDIO"
        select RT_USING_SDIO
        select RT_USING_DFS
        default n

    config BSP_USING_USBD
        bool "Enable OTGHS as USB device"
        select RT_USING_USB_DEVICE
        select BSP_USBD_TYPE_HS
        select BSP_USBD_SPEED_HS
        select BSP_USBD_PHY_ULPI
        default n

    source "$BSP_DIR/../libraries/HAL_Drivers/drivers/Kconfig"

endmenu

menu "Board extended module Drivers"

endmenu

endmenu

关于详细的语法描述可以参考KConfig语法及示例,配置好该文件后在BSP目录下打开ENV工具就可以看到一个这样的菜单:

在菜单里面勾选或者取消勾选,对应的宏定义就会在生成的rtconfig.h头文件里面出现或者消失,也可以进行一些参数的配置,最后在工程中包含该头文件即可

最后修改:2024 年 07 月 02 日
V我五十