RT-Thread Nano移植使用整理

https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-nano/an0038-nano-introduction

一、RT-Thread Nano 简介

RT-Thread Nano 是一个极简版硬实时内核,它是由 C 语言开发,采用面向对象的编程思维,具有良好的代码风格,是一款可裁剪的、抢占式实时多任务RTOS。其内存资源占用极小功能包括任务处理软件定时器信号量邮箱实时调度等相对完整的实时操作系统特性。适用于家电、消费电子、医疗设备、工控等领域大量使用的 32ARM 入门级 MCU 的场合。
下图是 RT-Thread Nano 的软件框图,包含支持的 CPU 架构与内核源码,还有可拆卸的 FinSH 组件:
RT-Thread Nano移植使用整理_第1张图片
支持架构ARMCortex M0/ M3/ M4/ M7 等、RISC-V 及其他。
功能:线程管理、线程间同步与通信、时钟管理、中断管理、内存管理。

1、Nano的特点

1、下载简单

RT-Thread Nano 以软件包的方式集成在 Keil MDKCubeMX 中,可以直接在软件中下载 Nano 软件包获取源码。

2、代码简单

RT-Thread 完整版不同的是,Nano 不含 Scons 构建系统,不需要 Kconfig 以及 Env 配置工具,也去除了完整版特有的 device 框架和组件,仅是一个纯净的内核。

3、移植简单

由于 Nano 的极简特性,使 Nano 的移植过程变得极为简单。添加 Nano 源码到工程,就已完成 90% 的移植工作。

4、使用简单

RT-Thread Nano 在使用上也非常简单,带给开发者友好的开发体验。

  • 易裁剪:Nano 的配置文件为 rtconfig.h,该文件中列出了内核中的所有宏定义,有些默认没有打开,如需使用,打开即可。
  • 易添加 FinSH 组件:FinSH 组件 可以很方便的在 Nano 上进行移植,而不再依赖 device 框架。
  • 自选驱动库:可以使用厂商提供的固件驱动库,如 STSTD 库、HAL 库、LL 库等,可以自行选择。
  • 完善的文档

2、小巧

资源占用小:对 RAMROM 的开销非常小,在支持 semaphoremailbox 特性,并运行两个线程 (main 线程 + idle 线程) 情况下,ROMRAM 依然保持着极小的尺寸,RAM 占用约 1K 左右,ROM 占用 4K 左右。

二、RT-Thread Nano 移植原理

本片文档介绍 Nano 移植原理,针对的是不同 MCU 的移植,如 Cortex MRISC-V,或者是其他 MCU 的移植。移植过程主要分为两个部分:libcpu 移植与板级移植,在讲解移植之前,本文档对 RT-Thread Nano 的启动流程与移植目录结构先进行说明。

1、启动流程

RT-Thread 启动流程如下所示,在图中标出颜色的部分需要用户特别注意(黄色表示 libcpu 移植相关的内容,绿色部分表示板级移植相关的内容)。
RT-Thread Nano移植使用整理_第2张图片
RT-Thread 启动代码统一入口为 rtthread_startup() ,芯片启动文件在完成必要工作(如初始化时钟、配置中断向量表、初始化堆栈等)后,最终会在程序跳转时,跳转至 RT-Thread 的启动入口中。RT-Thread 的启动流程如下:

  1. 全局关中断,初始化与系统相关的硬件。
  2. 打印系统版本信息初始化系统内核对象(如定时器、调度器)。
  3. 初始化用户 main 线程(同时会初始化线程栈),在 main 线程中对各类模块依次进行初始化。
  4. 初始化软件定时器线程、初始化空闲线程
  5. 启动调度器,系统切换到第一个线程开始运行(如 main 线程),并打开全局中断。

2、移植目录结构

rtthread-nano 源码中,与移植相关的文件位于下图中有颜色标记的路径下(黄色表示 libcpu 移植相关的文件,绿色部分表示板级移植相关的文件):
RT-Thread Nano移植使用整理_第3张图片

3、libcpu 移植

RT-Threadlibcpu 抽象层向下提供了一套统一的 CPU 架构移植接口,这部分接口包含了全局中断开关函数线程上下文切换函数时钟节拍的配置中断函数Cache 等等内容,RT-Thread 支持的 cpu 架构在源码的 libcpu 文件夹下。

4、启动文件 startup.s

启动文件芯片厂商提供位于芯片固件库中。每款芯片都有相对应的启动文件,在不同开发环境下启动文件也不相同。当系统加入 RT-Thread 之后,会将 RT-Thread 的启动放在调用 main() 函数之前,如下图所示:
在这里插入图片描述
startup.s:主要完成初始化时钟配置中断向量表;完成全局 / 静态变量初始化工作;初始化堆栈库函数初始化程序的跳转等内容。

程序跳转:芯片在 KEIL MDKIAR 下的启动文件不用做修改,会自动转到 RT-Thread 系统启动函数 rtthread_startup()GCC 下的启动文件需要修改,让其跳转到 RT-Thread 提供的 entry() 函数,其中 entry() 函数调用了 RT-Thread 系统启动函数 rtthread_startup()

最终调用 main() 函数进入用户 main()

5、上下文切换 context_xx.S

上下文切换表示 CPU 从一个线程切换到另一个线程、或者线程与中断之间的切换等。在上下文切换过程中,CPU 一般会停止处理当前运行的代码,并保存当前程序运行的具体位置以便之后继续运行。

在该文件中除了实现上下文切换的函数外,还需完成全局开关中断函数。
需实现的函数 | 描述

需实现的函数 描述
rt_base_t rt_hw_interrupt_disable(void); 关闭全局中断
void rt_hw_interrupt_enable(rt_base_t level); 打开全局中断
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 线程,用于中断里面进行切换的时候使用

注意:在 Cortex-M 中,PendSV 中断处理函数是 PendSV_Handler(),线程切换的实际工作在 PendSV_Handler() 里完成。

6、线程栈初始化 cpuport.c

RT-Thread 中,线程具有独立的栈,当进行线程切换时,会将当前线程的上下文存在栈中,当线程要恢复运行时,再从栈中读取上下文信息,进行恢复。

故障异常处理函数 rt_hw_hard_fault_exception(),在发生硬件错误时,执行 HardFault_Handler 中断,会执行该函数。

该文件中主要实现线程栈的初始化 rt_hw_stack_init()hard fault 异常处理函数。

需实现的函数 描述
rt_hw_stack_init() 实现线程栈的初始化
rt_hw_hard_fault_exception() 异常函数:系统硬件错误

7、中断与异常挂接 interrupt.c

注意:在 Cortex-M 内核上,所有中断都采用中断向量表的方式进行处理,即当一个中断触发时,处理器将直接判定是哪个中断源,然后直接跳转到相应的固定位置进行处理,不需要再自行实现中断管理

在一些Cortex-M 架构中,系统没有实现类似中断向量表的功能,物理中断要和用户的中断服务例程相关联,就需要使用中断管理接口对中断进行管理,这样当发生中断时就可以触发相应的中断,执行中断服务例程。

需实现的中断管理接口 描述
rt_hw_interrupt_init() 硬件中断初始化
rt_hw_interrupt_install() 中断服务程序挂接
rt_hw_interrupt_mask() 屏蔽指定的中断源
rt_hw_interrupt_umask() 打开被屏蔽的中断源

8、板级移植 board.c

注:board.crtconfig.h 是与硬件 / 板级相关的文件,在移植时需自行实现Cortex M 架构可参考 Nano 源码 bsp 文件夹中已有的的 board.crtconfig.h

板级移植主要是针对 rt_hw_board_init() 函数内容的实现,该函数在板级配置文件 board.c 中,函数中做了许多系统启动必要的工作,其中包含:

  1. 配置系统时钟。
  2. 实现 OS 节拍。(其中步骤 1 和 2 为 3.1.5 版本中 #error TODO 1 的部分:#error "TODO 1: OS Tick Configuration."
  3. 初始化外设:如 GPIO/UART 等等,若需要请在此处调用。
  4. 初始化系统内存堆,实现动态堆内存管理。
  5. 板级自动初始化,使用 INIT_BOARD_EXPORT() 自动初始化的函数会在此处被初始化。
  6. 其他必要的初始化,如 MMU 配置(需要时请自行在 rt_hw_board_init 函数中调用应用函数实现)。
/* board.c */
void rt_hw_board_init(void)
{
    /* 第一部分:系统初始化、系统时钟配置等 */
    HAL_init();                // 一些系统层初始化,若需要则增加此部分
    SystemClock_Config();      // 配置系统时钟
    SystemCoreClockUpdate();   // 更新系统时钟频率 SystemCoreClock

    /* 第二部分:配置 OS Tick 的频率,实现 OS 节拍(并在中断服务例程中实现 OS Tick 递增) */
    _SysTick_Config(SystemCoreClock / RT_TICK_PER_SECOND);

    /* 第三部分:初始化硬件外设,若有需要,则放在此处调用 */

    /* 第四部分:系统动态内存堆初始化 */
#if defined(RT_USING_USER_MAIN) && defined(RT_USING_HEAP)
    rt_system_heap_init(rt_heap_begin_get(), rt_heap_end_get());
#endif

    /* 第五部分:使用 INIT_BOARD_EXPORT() 进行的初始化 */
#ifdef RT_USING_COMPONENTS_INIT
    rt_components_board_init();
#endif

    /* 第六部分:其他初始化 */
}

1、配置系统时钟

系统时钟是给各个硬件模块提供工作时钟的基础,在 rt_hw_board_init() 函数中完成,可以调用库函数实现配置,也可以自行实现。

如下是配置系统时钟调用示例:

/* board.c */
void rt_hw_board_init()
{
    /* 第一部分:系统初始化、系统时钟配置等 */
    rt_hw_clock_init()    // 时钟初始化,函数名不做要求,函数自行实现,如 SystemClock_Config()、SystemCoreClockUpdate()
    ...
}

2、实现 OS 节拍

OS 节拍也叫时钟节拍或 OS tick。任何操作系统都需要提供一个时钟节拍,以供系统处理所有和时间有关的事件。

时钟节拍的实现:通过硬件 timer 实现周期性中断,在定时器中断中调用 rt_tick_increase() 函数实现全局变量 rt_tick 自加,从而实现时钟节拍。一般地,在 Cortex M 上直接使用内部的滴答定时器 Systick 实现

示例:如下是 stm32 配置 OS 节拍示例,在初始化时钟节拍后,直接在 SysTick_Handler() 中断服务例程中调用 rt_tick_increase()

/* board.c */
void rt_hw_board_init()
{
    ...
    /* 第二部分:配置 OS Tick 的频率,实现 OS 节拍(并在中断服务例程中实现 OS Tick 递增) */
    _SysTick_Config(SystemCoreClock / RT_TICK_PER_SECOND);  // 使用 SysTick 实现时钟节拍
    ...
}

/* systick 中断服务例程 */
void SysTick_Handler(void)
{
    /* enter interrupt */
    rt_interrupt_enter();

    rt_tick_increase();

    /* leave interrupt */
    rt_interrupt_leave();
}

对于使用了 RT-Thread 中断管理的 CPU 架构,中断服务例程需要通过 rt_hw_interrupt_install() 进行装载,如下示例:

/* board.c */
void rt_hw_board_init()
{
    ...
    /* 第二部分:配置 OS Tick 的频率,实现 OS 节拍(并在中断服务例程中实现 OS Tick 递增) */
    rt_hw_timer_init();      // 使用 硬件定时器 实现时钟节拍,一般命名为 rt_hw_timer_init()
    ...
}

int rt_hw_timer_init(void)   // 函数自行实现,并需要装载中断服务例程
{
    ...
    rt_hw_interrupt_install(IRQ_PBA8_TIMER2_3, rt_hw_timer_isr, RT_NULL, "tick");
    rt_hw_interrupt_umask(IRQ_PBA8_TIMER2_3);
}

/* TIMER 中断服务例程 */
static void rt_hw_timer_isr(int vector, void *param)
{
    rt_interrupt_enter();
    rt_tick_increase();
    rt_interrupt_leave();
}

注:在初始化时钟节拍的时候,会用到宏 RT_TICK_PER_SECOND。通过修改该宏的值,可以修改系统中一个时钟节拍的时间长度。

3、硬件外设初始化

硬件初始化,如 UART 初始化等(对接控制台),需要在 rt_hw_board_init() 函数中手动调用 UART 初始化函数。

/* board.c */
void rt_hw_board_init(void)
{
    ....
    /* 第三部分:初始化硬件外设,若有需要,则放在此处调用 */
    uart_init();
    ....
}

注意,uart_init() 或者其他的外设初始化函数,若已经使用了宏 INIT_BOARD_EXPORT() 进行初始化,则不需要在此进行显式调用。两种初始化方法选择一种即可。

4、实现动态内存堆

RT-Thread Nano 默认不开启动态内存堆功能,开启 RT_USING_HEAP 将可以使用动态内存功能,即可以使用 rt_mallocrt_free 以及各种系统动态创建对象的 API。动态内存堆管理功能的初始化是通过 rt_system_heap_init() 函数完成的,动态内存堆的初始化需要指定堆内存的起始地址结束地址,函数原型如下:

void rt_system_heap_init(void *begin_addr, void *end_addr)

开启 RT_USING_HEAP 后,系统默认使用数组作为 heapheap 的起始地址与结束地址作为参数传入 heap 初始化函数,heap 初始化函数 rt_system_heap_init() 将在 rt_hw_board_init() 中被调用。

开启 heap 后,系统中默认使用数组作为 heapheap 默认较小,实际使用时请根据芯片 RAM 情况改大),获得的 heap 的起始地址与结束地址,作为参数传入 heap 初始化函数:

#define RT_HEAP_SIZE 1024
static uint32_t rt_heap[RT_HEAP_SIZE];
RT_WEAK void *rt_heap_begin_get(void)
{
    return rt_heap;
}

RT_WEAK void *rt_heap_end_get(void)
{
    return rt_heap + RT_HEAP_SIZE;
}

void rt_hw_board_init(void)
{
    ....
#if defined(RT_USING_USER_MAIN) && defined(RT_USING_HEAP)
    rt_system_heap_init(rt_heap_begin_get(), rt_heap_end_get());    //传入 heap 的起始地址与结束地址
#endif
    ....
}

如果不想使用数组作为动态内存堆,则可以重新指定系统 HEAP 的大小,例如使用 RAM ZI 段结尾处作为 HEAP 的起始地址(这里需检查与链接脚本是否对应),使用 RAM 的结尾地址作为 HEAP 的结尾地址,这样可以将空余RAM 全部作为动态内存 heap 使用。如下示例重新定义了 HEAP 的起始地址与结尾地址,并作为初始化参数进行系统 HEAP 初始化。

#define STM32_SRAM1_START   (0x20000000)
#define STM32_SRAM1_END     (STM32_SRAM1_START + 20 * 1024)   // 结束地址 = 0x20000000(基址) + 20K(RAM大小)

#if defined(__CC_ARM) || defined(__CLANG_ARM)
extern int Image$$RW_IRAM1$$ZI$$Limit;                   // RW_IRAM1,需与链接脚本中运行时域名相对应
#define HEAP_BEGIN      ((void *)&Image$$RW_IRAM1$$ZI$$Limit)
#endif

#define HEAP_END                       STM32_SRAM1_END
void rt_hw_board_init(void)
{
    ....
#if defined(RT_USING_USER_MAIN) && defined(RT_USING_HEAP)
    rt_system_heap_init((void *)HEAP_BEGIN, (void *)HEAP_END);
#endif
    ....
}

5、链接脚本

链接脚本,也称分散加载文件,决定在生成 image 文件时如何来分配相关数据的存放基址,如果不指定特定的链接脚本,连接器就会自动采用默认的链接脚本来生成镜像。

举例 stm32KEIL MDK 开发环境下的链接脚本文件 xxx.sct

LR_IROM1 0x08000000 0x00020000  {    ; load region size_region
  ER_IROM1 0x08000000 0x00020000  {  ; load address = execution address
   *.o (RESET, +First)
   *(InRoot$$Sections)
   .ANY (+RO)
  }
  RW_IRAM1 0x20000000 0x00005000  {  ; RW data
   .ANY (+RW +ZI)
  }
}

其中 RW_IRAM1 0x20000000 0x00005000 表示定义一个运行时域 RW_IRAM1(默认域名),域基址为 0x20000000,域大小为 0x00005000(即 20K ),对应实际 RAM 大小。.ANY (+RW +ZI) 表示加载所有匹配目标文件的可读写数据 RW-Data、清零数据 ZI-Data。所以运行时所占内存的结尾处就是 ZI 段结尾处,可以将 ZI 结尾处之后的内存空间作为系统动态内存堆使用。

三、在 RT-Thread Studio 上使用 RT-Thread Nano

1、安装 RT-Thread Studio

2、新建 Nano 工程

RT-Thread Nano移植使用整理_第4张图片
进入新建工程的配置向导:

  1. 设置工程名称
  2. 工程保存路径
  3. 基于芯片创建,nano 版本选择
  4. 芯片选择
  5. 串口控制台与引脚号配置
  6. 调试配置

工程使用芯片内部 HSI 时钟,如需修改,则请修改 drv_clk.c
RT-Thread Nano移植使用整理_第5张图片
注:可以通过修改 drv_clk.cSystemClock_Config() 更改系统时钟。

工程创建完毕,连接硬件,可直接进行编译下载,如下所示:
RT-Thread Nano移植使用整理_第6张图片
RT-Thread Nano移植使用整理_第7张图片
由于在创建工程向导中配置了控制台串口号及其引脚号,所以工程中已经实现了 uart 的驱动以及 rt_hw_console_output() ,默认可以进行打印。打开串口终端,可以发现在终端中执行了打印。
RT-Thread Nano移植使用整理_第8张图片

3、基于 Nano 添加 FinSH

双击 RT-Thread Settings 进入配置,打开组件,勾选 FinSH Shell,保存配置。此操作将把 FinSH 组件的源码加入工程中。

其中,rt_hw_console_getchar() 已经在 drv_uart.c 中实现,无需再实现对接 FinSH 的代码。
RT-Thread Nano移植使用整理_第9张图片
链接硬件,编译下载后,在串口终端中按下 Tab 键,可查看系统中的命令:
RT-Thread Nano移植使用整理_第10张图片

四、基于 Keil MDK 移植 RT-Thread Nano

本文介绍如何基于 Keil MDK 移植 RT-Thread Nano ,并以一个 stm32f103 的基础工程作为示例进行讲解。

RT-Thread Nano 已集成在 Keil MDK,可以直接在 IDE 中进行下载添加。本文档介绍了如何使用 MDK 移植 RT-Thread Nano,并以一个 stm32f103 的基础工程作为示例进行讲解。

移植 Nano 的主要步骤:

  1. 准备一个基础的 keil MDK 工程,并获取 RT-Thread Nano pack 安装包并进行安装。
  2. 在基础工程中添加 RT-Thread Nano 源码。
  3. 适配 Nano,主要从 中断、时钟、内存这几个方面进行适配,实现移植。
  4. 验证移植结果:编写第一个应用代码,基于 RT-Thread Nano 闪烁 LED
  5. 最后可对 Nano 进行配置:Nano 是可裁剪的,通过配置文件 rtconfig.h 实现对系统的裁剪。

1、准备工作

1、准备一份基础的裸机源码工程。

2、在 KEIL 上安装 RT-Thread Nano Pack

2、基础工程准备

在移植 RT-Thread Nano 之前,我们需要准备一个能正常运行的裸机工程。

3、Nano Pack 安装

Nano Pack 可以通过在 Keil MDK IDE 内进行安装,也可以手动安装。下面开始介绍两种安装方式。

1、在 IDE 内安装

打开 MDK 软件,点击工具栏的 Pack Installer 图标:
RT-Thread Nano移植使用整理_第11张图片
点击右侧的 Pack,展开 Generic,可以找到 RealThread::RT-Thread,点击 Action 栏对应的 Install ,就可以在线安装 Nano Pack 了。另外,如果需要安装其他版本,则需要展开 RealThread::RT-Thread,进行选择,箭头所指代表已经安装的版本。
RT-Thread Nano移植使用整理_第12张图片

2、手动安装

我们也可以从官网下载安装文件,RT-Thread Nano 离线安装包下载,下载结束后双击文件进行安装:
RT-Thread Nano移植使用整理_第13张图片

4、添加RT-Thread Nano 到工程

打开已经准备好的可以运行的裸机程序,将 RT-Thread 添加到工程。如下图,点击 Manage Run-Time Environment
RT-Thread Nano移植使用整理_第14张图片
Manage Rum-Time Environment"Software Component" 栏找到 RTOSVariant 栏选择 RT-Thread,然后勾选 kernel,点击 "OK" 就添加 RT-Thread 内核到工程了。
RT-Thread Nano移植使用整理_第15张图片
现在可以在 Project 看到 RT-Thread RTOS 已经添加进来了,展开 RTOS,可以看到添加到工程的文件:
RT-Thread Nano移植使用整理_第16张图片
Cortex-M 芯片内核移植代码:

context_rvds.s
cpuport.c

Kernel 文件包括:

clock.c
components.c
device.c
idle.c
ipc.c
irq.c
kservice.c
mem.c
mempool.c
object.c
scheduler.c
thread.c
timer.c

配置文件:

board.c
rtconfig.h

5、适配 RT-Thread Nano

1、中断与异常处理

RT-Thread接管异常处理函数 HardFault_Handler() 和悬挂处理函数 PendSV_Handler(),这两个函数已由 RT-Thread 实现,所以需要删除工程里中断服务例程文件中的这两个函数,避免在编译时产生重复定义。如果此时对工程进行编译,没有出现函数重复定义的错误,则不用做修改。

2、系统时钟配置

需要在 board.c实现 系统时钟配置(为 MCU、外设提供工作时钟)与 os tick 的配置 (为操作系统提供心跳 / 节拍)。

如下代码所示,用户需要在 board.c 文件中系统初始化和 OS Tick 的配置,用户需在 timer 定时器中断服务函数调用 rt_os_tick_callback functioncortex-m 架构使用 SysTick_Handler()

/* board.c */

/* timer 定时器中断服务函数调用 rt_os_tick_callback function,cortex-m 架构使用 SysTick_Handler() */
void rt_os_tick_callback(void)
{
  rt_interrupt_enter(); /* 进入中断时必须调用 */

  rt_tick_increase();  /* RT-Thread 系统时钟计数 */

  rt_interrupt_leave(); /* 退出中断时必须调用 */
}

/* cortex-m 架构使用 SysTick_Handler() */
SysTick_Handler()
{
    rt_os_tick_callback();
}

void rt_hw_board_init(void)
{
  /*
   * TODO 1: OS Tick Configuration
   * Enable the hardware timer and call the rt_os_tick_callback function
   * periodically with the frequency RT_TICK_PER_SECOND.
   */

  /* 1、系统、时钟初始化 */
  HAL_Init(); // 初始化 HAL 库
  SystemClock_Config(); // 配置系统时钟
  SystemCoreClockUpdate(); // 对系统时钟进行更新

  /* 2、OS Tick 频率配置,RT_TICK_PER_SECOND = 1000 表示 1ms 触发一次中断 */
  _SysTick_Config(SystemCoreClock / RT_TICK_PER_SECOND);

  /* Call components board initial (use INIT_BOARD_EXPORT()) */
#ifdef RT_USING_COMPONENTS_INIT
  rt_components_board_init();
#endif

#if defined(RT_USING_USER_MAIN) && defined(RT_USING_HEAP)
  rt_system_heap_init(rt_heap_begin_get(), rt_heap_end_get());
#endif
}

1、将源工程中在main函数中初始化移动到rt_hw_board_init中。

2、初始化系统滴答定时器作为OS Tick

3、在SysTick_Handler中调用rt_os_tick_callback函数,实现时间片切换(也可以使用其他定时器)。

注:以上部分需要自己实现

3、内存堆初始化

系统内存堆的初始化在 board.c 中的 rt_hw_board_init() 函数中完成,内存堆功能是否使用取决于宏 RT_USING_HEAP 是否开启,RT-Thread Nano 默认不开启内存堆功能,这样可以保持一个较小的体积,不用为内存堆开辟空间。

开启系统 heap可以使用动态内存功能,如使用 rt_mallocrt_free 以及各种系统动态创建对象的 API。若需要使用系统内存堆功能,则打开 RT_USING_HEAP 宏定义即可,此时内存堆初始化函数 rt_system_heap_init() 将被调用,如下所示:
RT-Thread Nano移植使用整理_第17张图片
初始化内存堆需要堆的起始地址结束地址这两个参数,系统中默认使用数组作为 heap,并获取了 heap 的起始地址与结束地址,该数组大小可手动更改,如下所示:
RT-Thread Nano移植使用整理_第18张图片
注意:开启 heap 动态内存功能后,heap 默认值较小,在使用的时候需要改大,否则可能会有申请内存失败或者创建线程失败的情况,修改方法有以下两种:

  • 可以直接修改数组中定义的 RT_HEAP_SIZE 的大小,至少大于各个动态申请内存大小之和,但要小于芯片 RAM 总大小。
  • 使用 RAM ZI 段结尾处作为 HEAP 的起始地址,使用 RAM 的结尾地址作为 HEAP 的结尾地址,这是 heap 能设置的最大值的方法。

6、编写第一个应用

移植好 RT-Thread Nano 之后,则可以开始编写第一个应用代码验证移植结果。此时 main() 函数就转变成 RT-Thread 操作系统的一个线程,现在可以在 main() 函数中实现第一个应用:从串口1打印信息。

  1. 首先在文件首部增加 RT-Thread 的相关头文件
  2. main() 函数中(也就是在 main 线程中)实现输出"RT-Thread\r\n"字符串。
  3. 将延时函数替换为 RT-Thread 提供的延时函数 rt_thread_mdelay()。该函数会引起系统调度,切换到其他线程运行,体现了线程实时性的特点。
    RT-Thread Nano移植使用整理_第19张图片
    RT-Thread Nano移植使用整理_第20张图片

编译程序之后下载到芯片就可以看到基于 RT-Thread 的程序运行起来了,串口1输出"RT-Thread\r\n"

注:当添加 RT-Thread 之后,裸机中的 main() 函数会自动变成 RT-Thread 系统中 main 线程 的入口函数。由于线程不能一直独占 CPU,所以此时在 main() 中使用 while(1) 时,需要有让出 CPU 的动作,比如使用 rt_thread_mdelay() 系列的函数让出 CPU

1、与裸机应用代码的不同

1、延时函数不同

RT-Thread 提供的 rt_thread_mdelay() 函数可以引起操作系统进行调度,当调用该函数进行延时时,本线程将不占用 CPU,调度器切换到系统的其他线程开始运行。而裸机的 delay 函数是一直占用 CPU 运行的。

2、初始化系统时钟的位置不同

移植好 RT-Thread Nano 之后,不需要再在 main() 中做相应的系统配置(如 hal 初始化、时钟初始化等),这是因为 RT-Thread 在系统启动时,已经做好了系统时钟初始化等的配置。

7、配置 RT-Thread Nano

用户可以根据自己的需要通过修改 rtconfig.h 文件里面的宏定义配置相应功能。

RT-Thread Nano 默认未开启宏 RT_USING_HEAP,故只支持静态方式创建任务、信号量等对象。若要通过动态方式创建对象则需要在 rtconfig.h 文件里开启 RT_USING_HEAP 宏定义。

MDK 的配置向导 configuration Wizard 可以很方便的对工程进行配置,Value 一栏可以选中对应功能及修改相关值,等同于直接修改配置文件 rtconfig.h
RT-Thread Nano移植使用整理_第21张图片

五、基于 CubeMX 移植 RT-Thread Nano

本文介绍了如何基于 CubeMX 移植 RT-Thread Nano,并说明生成代码工程的步骤。

RT-Thread Nano 已集成在 CubeMX 中,可以直接在 IDE 中进行下载添加。本文档介绍了如何使用 CubeMX 移植 RT-Thread Nano,并以一个 stm32f103 的基础工程作为示例进行讲解。

移植 Nano主要步骤

  1. 准备一个 CubeMX 基础工程,并获取 RT-Thread Nano pack 安装包进行安装。
  2. 在基础工程中添加 RT-Thread Nano 源码。
  3. 适配 Nano,主要从 中断、时钟、内存、应用 这几个方面进行适配,实现移植。
  4. 最后可对 Nano 进行配置:Nano 是可裁剪的,可以通过配置文件 rtconfig.h 实现对系统的裁剪。

1、准备工作

  • 下载 Cube MX 5.0 ,下载地址 https://www.st.com/en/development-tools/stm32cubemx.html 。
  • CubeMX 上下载 RT-Thread Nano pack 安装包。

1、Nano pack 安装

要获取 RT-Thread Nano 软件包,需要在 CubeMX 中添加 https://www.rt-thread.org/download/cube/RealThread.RT-Thread.pdsc 。

具体步骤:进入打开 CubeMX,从菜单栏 help 进入 Manage embedded software packages 界面,点击 From Url 按钮,进入 User Defined Packs Manager 界面,其次点击 new,填入上述网址,然后点击 check,如下图所示:
RT-Thread Nano移植使用整理_第22张图片
check 通过后,点击 OK 回到 User Defined Packs Manager 界面,再次点击 OK,CubeMX 自动连接服务器,获取包描述文件。回到 Manage embedded software packages 界面,就会发现 RT-Thread Nano 3.1.5 软件包,选择该软件包,点击 Install Now,如下图所示:
RT-Thread Nano移植使用整理_第23张图片
点击安装之后,弹出 Licensing Agreement ,同意协议,点击 Finish,如下图所示:
RT-Thread Nano移植使用整理_第24张图片
等待安装完成,成功安装后,版本前面的小蓝色框变成填充的黄绿色,现象如下图所示:
RT-Thread Nano移植使用整理_第25张图片
至此,RT-Thread Nano 软件包安装完毕,退出 Manage embedded software packages 界面,进入 CubeMX 主界面。

2、创建基础工程

CubeMX 主界面的菜单栏中 File 选择 New Project,如下图所示
RT-Thread Nano移植使用整理_第26张图片
新建工程之后,在弹出界面芯片型号中输入某一芯片型号,方便锁定查找需要的芯片,双击被选中的芯片,如下图所示
RT-Thread Nano移植使用整理_第27张图片
时钟树的配置直接使用默认即可,然后还需要配置下载方式。

2、添加 RT-Thread Nano 到工程

1、选择 Nano 组件

选中芯片型号之后,点击 Softwares Packages->Select Components,进入组件配置界面,选择 RealThread, 然后根据需求选择 RT-Thread 组件,然后点击 OK 按钮,如下图所示:
RT-Thread Nano移植使用整理_第28张图片
注意:RT-Thread Nano 软件包中包含 kernel, shelldevice 三个部分,仅选择 kernel 表示只使用 RT-Thread 内核,工程中会添加内核代码;选择 kernelshell 表示在使用 RT-Thread Nano 的基础上使用 FinSH Shell 组件,工程中会添加内核代码与 FinSH 组件的代码。再选择 device 表示使用 rt-threaddevice 框架,用户基于此框架编写外设驱动并注册后,就可以使用 device 统一接口操作外设。

2、配置 Nano

选择组件之后,对组件参数进行配置。在工程界面 Pinout & Configuration 中,进入所选组件参数配置区,按照下图进行配置
RT-Thread Nano移植使用整理_第29张图片

3、工程管理

给工程取名、选择代码存放位置、选择生成代码的 Toolchain/IDECube MX 不仅能够生成 Keil4/Keil5 的工程,而且还能够生成 IAR7/IAR8 等 IDE 的工程,功能强大,本文从下拉框中选择 MDK5,操作如图所示
RT-Thread Nano移植使用整理_第30张图片

4、配置 MCU

根据需求配置 MCU 的功能。

3、适配 RT-Thread Nano

1、中断与异常处理

RT-Thread 操作系统重定义 HardFault_HandlerPendSV_HandlerSysTick_Handler 中断函数,为了避免重复定义的问题,在生成工程之前,需要在中断配置中,代码生成的选项中,取消选择三个中断函数(对应注释选项是 Hard fault interrupt, Pendable request, Time base :System tick timer),最后点击生成代码,具体操作如下图 所示:
RT-Thread Nano移植使用整理_第31张图片
等待工程生成完毕,点击打开工程,如下图所示,即可进入 MDK5 工程中。
RT-Thread Nano移植使用整理_第32张图片

2、系统时钟配置

需要在 board.c 中实现 系统时钟配置(为 MCU、外设提供工作时钟)与 OS Tick 的配置(为操作系统提供心跳 / 节拍)。

如下代码所示, HAL_Init() 初始化 HAL 库, SystemClock_Config()配置了系统时钟, SystemCoreClockUpdate() 对系统时钟进行更新,_SysTick_Config() 配置了 OS Tick。此处 OS Tick 使用滴答定时器 systick 实现,需要用户在 board.c 中实现 SysTick_Handler() 中断服务例程,调用 RT-Thread 提供的 rt_tick_increase() ,如下图所示。

/* board.c */
void rt_hw_board_init()
{
    HAL_Init();
    SystemClock_Config();

    /* System Clock Update */
    SystemCoreClockUpdate();

    /* System Tick Configuration */
    _SysTick_Config(SystemCoreClock / RT_TICK_PER_SECOND);

    /* Call components board initial (use INIT_BOARD_EXPORT()) */
#ifdef RT_USING_COMPONENTS_INIT
    rt_components_board_init();
#endif

#if defined(RT_USING_USER_MAIN) && defined(RT_USING_HEAP)
    rt_system_heap_init(rt_heap_begin_get(), rt_heap_end_get());
#endif
}

3、内存堆初始化

系统内存堆的初始化在 board.c 中的 rt_hw_board_init() 函数中完成,内存堆功能是否使用取决于宏 RT_USING_HEAP 是否开启,RT-Thread Nano 默认不开启内存堆功能,这样可以保持一个较小的体积,不用为内存堆开辟空间。

开启系统 heap 将可以使用动态内存功能,如使用 rt_mallocrt_free 以及各种系统动态创建对象的 API。若需要使用系统内存堆功能,则打开 RT_USING_HEAP 宏定义即可,此时内存堆初始化函数 rt_system_heap_init() 将被调用,如下所示:
RT-Thread Nano移植使用整理_第33张图片
初始化内存堆需要堆的起始地址与结束地址这两个参数,系统中默认使用数组作为 heap,并获取了 heap 的起始地址与结束地址,该数组大小可手动更改,如下所示:
RT-Thread Nano移植使用整理_第34张图片
注意:开启 heap 动态内存功能后,heap 默认值较小,在使用的时候需要改大,否则可能会有申请内存失败或者创建线程失败的情况,修改方法有以下两种:

  • 可以直接修改数组中定义的 RT_HEAP_SIZE 的大小,至少大于各个动态申请内存大小之和,但要小于芯片 RAM 总大小。
  • 使用 RAM ZI 段结尾处作为 HEAP 的起始地址,使用 RAM 的结尾地址作为 HEAP 的结尾地址,这是 heap 能设置的最大值的方法。

4、编写第一个应用

移植好 RT-Thread Nano 之后,则可以开始编写第一个应用代码。此时 main() 函数就转变成 RT-Thread 操作系统的一个线程,现在可以在 main() 函数中实现第一个应用:从串口1打印“CubeMX RT-Thread\r\n”

  1. 首先在文件首部包含 RT-Thread 的相关头文件
  2. main() 函数中(也就是在 main 线程中)写代码:初始化 串口1、在循环中从串口1打印“CubeMX RT-Thread\r\n”
  3. 延时函数使用 RT-Thread 提供的延时函数 rt_thread_mdelay(),该函数会引起系统调度,切换到其他线程运行,体现了线程实时性的特点。

编译程序之后下载到芯片就可以看到基于 RT-Thread 的程序运行起来了,LED 正常闪烁。

注:当添加 RT-Thread 之后,裸机中的 main() 函数会自动变成 RT-Thread 系统中 main 线程 的入口函数。由于线程不能一直独占 CPU,所以此时在 main() 中使用 while(1) 时,需要有让出 CPU 的动作,比如使用 rt_thread_mdelay() 系列的函数让出 CPU

1、与裸机应用代码的不同

1、延时函数不同

RT-Thread 提供的 rt_thread_mdelay() 函数可以引起操作系统进行调度,当调用该函数进行延时时,本线程将不占用 CPU,调度器切换到系统的其他线程开始运行。而裸机的 delay 函数是一直占用 CPU 运行的。

2、初始化系统时钟的位置不同

移植好 RT-Thread Nano 之后,不需要再在 main() 中做相应的系统配置(如 hal 初始化、时钟初始化等),这是因为 RT-Thread 在系统启动时,已经做好了系统时钟初始化等的配置。

5、配置 RT-Thread Nano

配置 RT-Thread Nano 可以在上面小节 添加 RT-Thread Nano -> 配置 Nano 中,这是在生成工程之前做的配置。如果生成工程之后,想直接在目标工程的 IDE 中配置,那么直接修改工程中 rtconfig.h 文件即可。

六、RT-Thread Nano 配置

RT-Thread Nano 的配置在 rtconfig.h 中进行,通过开关宏定义来使能或关闭某些功能,接下来对该配置文件中的宏定义进行说明。

1、基础配置

1、设置系统最大优先级,可设置范围 8 到 256,默认值 32,可修改。

#define RT_THREAD_PRIORITY_MAX  32

2、设置 RT-Thread 操作系统节拍,表示多少 tick 每秒,如默认值为 100 ,表示一个时钟节拍(os tick)长度为 10ms。常用值为 100 或 1000。时钟节拍率越快,系统的额外开销就越大。

#define RT_TICK_PER_SECOND  1000

3、字节对齐时设定对齐的字节个数,默认 4,常使用 ALIGN(RT_ALIGN_SIZE) 进行字节对齐。

#define RT_ALIGN_SIZE   4

4、设置对象名称的最大长度,默认 8 个字符,一般无需修改。

#define RT_NAME_MAX    8

5、设置使用组件自动初始化功能,默认需要使用,开启该宏则可以使用自动初始化功能。

#define RT_USING_COMPONENTS_INIT

6、开启 RT_USING_USER_MAIN 宏,则打开 user_main 功能,默认需要开启,这样才能调用 RT-Thread 的启动代码;main 线程的栈大小可修改。

#define RT_USING_USER_MAIN
#define RT_MAIN_THREAD_STACK_SIZE     512

2、内核调试功能配置

定义 RT_DEBUG 宏则开启 debug 模式。若开启系统调试,则在实现打印之后可以打印系统 LOG 日志。请在代码开发与调试过程中打开该项,帮助调试定位问题,在代码发布时关闭该项。

//#define RT_DEBUG                 // 关闭 debug

#define RT_DEBUG_INIT 0            // 启用组件初始化调试配置,设置为 1 则会打印自动初始化的函数名称

//#define RT_USING_OVERFLOW_CHECK  // 关闭栈溢出检查

3、钩子函数配置

设置是否使用钩子函数,默认关闭。

//#define RT_USING_HOOK                         // 是否 开启系统钩子功能

//#define RT_USING_IDLE_HOOK                    // 是否 开启空闲线程钩子功能

4、软件定时器配置

设置是否启用软件定时器,以及相关参数的配置,默认关闭。

#define RT_USING_TIMER_SOFT       0             // 关闭软件定时器功能,为 1 则打开
#if RT_USING_TIMER_SOFT == 0
#undef RT_USING_TIMER_SOFT
#endif

#define RT_TIMER_THREAD_PRIO        4           // 设置软件定时器线程的优先级,默认为 4

#define RT_TIMER_THREAD_STACK_SIZE  512         // 设置软件定时器线程的栈大小,默认为 512 字节

5、IPC 配置

系统支持的 IPC 有:信号量、互斥量、事件集、邮箱、消息队列。通过定义相应的宏打开或关闭该 IPC 的使用。

#define RT_USING_SEMAPHORE         // 设置是否使用 信号量,默认打开

//#define RT_USING_MUTEX           // 设置是否使用 互斥量

//#define RT_USING_EVENT           // 设置是否使用 事件集

#define RT_USING_MAILBOX           // 设置是否使用  邮箱

//#define RT_USING_MESSAGEQUEUE    // 设置是否使用 消息队列

6、内存配置

RT-Thread 内存管理包含:内存池、内存堆、小内存算法。通过开启相应的宏定义使用相应的功能。

//#define RT_USING_MEMPOOL      // 是否使用 内存池

#define RT_USING_HEAP           // 是否使用 内存堆

#define RT_USING_SMALL_MEM      // 是否使用 小内存管理

//#define RT_USING_TINY_SIZE    // 是否使用 小体积的算法,牵扯到 rt_memset、rt_memcpy 所产生的体积

8、FinSH 配置

当系统加入 FinSH 组件源码后,需要在 rtconfig.h 中开启以下项

#include "finsh_config.h"

该头文件中包含了对 FinSH 组件的配置。如下是该头文件中包含的 FinSH 组件的配置项:

/* 打开 FinSH 组件 */
#define RT_USING_FINSH

/* 使用 MSH 模式 */
#define FINSH_USING_MSH
#define FINSH_USING_MSH_ONLY

/* tshell 线程的优先级与线程栈大小 */
#define FINSH_THREAD_PRIORITY       21   // 请检查系统最大优先级的值,该值必须在系统支持的优先级范围之内
#define FINSH_THREAD_STACK_SIZE     1024

/* 使用符号表,使用命令描述 */
#define FINSH_USING_SYMTAB
#define FINSH_USING_DESCRIPTION

注意:若未加入 FinSH 组件源码,请勿开启此项。

8、DEVICE 框架配置

当系统中加入 device 框架源码时,则需要在 rtconfig.h 中开启以下项

#define RT_USING_DEVICE

开启该项则将加入 device 框架源码。

注意:若未加入 device 源码,请勿开启此项。

七、在 RT-Thread Nano 上添加控制台与 FinSH

本篇文档分为两部分:

  • 第一部分是添加 UART 控制台(实现打印):用来向控制台对接的终端输出打印信息;该部分只需要实现两个函数串口初始化系统输出函数,即可完成 UART 控制台打印功能。
  • 第二部分是移植 FinSH 组件(实现命令输入),用以在控制台输入命令调试系统;该部分的实现基于第一部分,只需要添加 FinSH 组件源码并再对接一个系统输入函数即可实现

1、在 Nano 上添加 UART 控制台(实现打印)

RT-Thread Nano 上添加 UART 控制台打印功能后,就可以在代码中使用 RT-Thread 提供的打印函数 rt_kprintf() 进行信息打印,从而获取自定义的打印信息,方便定位代码 bug 或者获取系统当前运行状态等。实现控制台打印(需要确认 rtconfig.h 中已使能 RT_USING_CONSOLE 宏定义),需要完成基本的硬件初始化,以及对接一个系统输出字符的函数,本小节将详细说明。

1、实现串口初始化

注:此部分为 3.1.5 版本中 #error TODO 2 的部分:#error "TODO 2: Enable the hardware uart and config baudrate."

使用串口对接控制台的打印,首先需要初始化串口,如引脚、波特率等。 初始化的串口函数 uart_init() 有以下两种调用方式,二选一:

  1. 方法一:默认使用宏 INIT_BOARD_EXPORT() 进行自动初始化,不需要显式调用,如下所示。
  2. 方法二:可以使用显式调用:uart_init() 需要在 board.c 中的 rt_hw_board_init() 函数中调用。
/* 实现 1:初始化串口 */
static int uart_init(void);

示例代码:如下是基于 HAL 库的 STM32F103 串口驱动,完成添加控制台的示例代码,仅做参考。

static int uart_init(void)
{
    /* 初始化串口参数,如波特率、停止位等等 */
    UartHandle.Instance = USART1;
    UartHandle.Init.BaudRate   = 115200;
    UartHandle.Init.HwFlowCtl  = UART_HWCONTROL_NONE;
    UartHandle.Init.Mode       = UART_MODE_TX_RX;
    UartHandle.Init.OverSampling = UART_OVERSAMPLING_16;
    UartHandle.Init.WordLength = UART_WORDLENGTH_8B;
    UartHandle.Init.StopBits   = UART_STOPBITS_1;
    UartHandle.Init.Parity     = UART_PARITY_NONE;

    /* 初始化串口引脚等 */
    if (HAL_UART_Init(&UartHandle) != HAL_OK)
    {
        while(1);
    }

    return 0;
}
INIT_BOARD_EXPORT(uart_init);  /* 默认选择初始化方法一:使用宏 INIT_BOARD_EXPORT 进行自动初始化 */
/* board.c */
void rt_hw_board_init(void)
{
    ....
    uart_init();   /* 初始化方法二:可以选择在 rt_hw_board_init 函数中直接调用 串口初始化 函数 */
    ....
}

2、实现 rt_hw_console_output

注:此部分为 3.1.5 版本中 #error TODO 3 的部分:#error "TODO 3: Output the string 'str' through the uart."

实现 finsh 组件输出一个字符,即在该函数中实现 uart 输出字符:

/* 实现 2:输出一个字符,系统函数,函数名不可更改 */
void rt_hw_console_output(const char *str);

注意:RT-Thread 系统中已有的打印均以 \n 结尾,而并非 \r\n,所以在字符输出时,需要在输出 \n 之前输出 \r,完成回车与换行,否则系统打印出来的信息将只有换行。

示例代码:如下是基于STM32F103 HAL 串口驱动对接的 rt_hw_console_output() 函数,实现控制台字符输出,示例仅做参考。

void rt_hw_console_output(const char *str)
{
    rt_size_t i = 0, size = 0;
    char a = '\r';

    __HAL_UNLOCK(&UartHandle);

    size = rt_strlen(str);
    for (i = 0; i < size; i++)
    {
        if (*(str + i) == '\n')
        {
            HAL_UART_Transmit(&UartHandle, (uint8_t *)&a, 1, 1);
        }
        HAL_UART_Transmit(&UartHandle, (uint8_t *)(str + i), 1, 1);
    }
}

3、结果验证

在应用代码中编写含有 rt_kprintf() 打印的代码,编译下载,打开串口助手进行验证。如下图是一个在 main() 函数中每隔 1 秒进行循环打印 Hello RT-Thread 的示例效果:
RT-Thread Nano移植使用整理_第35张图片

2、在 Nano 上添加 FinSH 组件(实现命令输入)

RT-Thread FinSH是 RT-Thread 的命令行组件(shell),提供一套供用户在命令行调用的操作接口,主要用于调试查看系统信息。它可以使用串口 / 以太网 / USB 等PC 机进行通信,使用 FinSH 组件基本命令的效果图如下所示:

本文以串口 UART 作为 FinSH 的输入输出端口与 PC 进行通信,描述如何在 Nano 上实现 FinSH shell 功能。

RT-Thread Nano 上添加 FinSH 组件,实现 FinSH 功能的步骤主要如下:

  1. 添加 FinSH 源码到工程。
  2. 实现函数对接。

1、添加 FinSH 源码到工程

1、KEIL 添加 FinSH 源码

RT-Thread Nano移植使用整理_第36张图片
勾选 shell,这将自动把 FinSH 组件的源码到工程:
RT-Thread Nano移植使用整理_第37张图片
然后在 rtconfig.h 中打开 finsh 相关选项,如下图:
RT-Thread Nano移植使用整理_第38张图片

2、Cube MX 添加 FinSH 源码

打开一个 cube 工程,点击 Additional Software,在 Pack Vendor 中可勾选 RealThread 快速定位 RT-Thread 软件包,然后在 RT-Thread 软件包中勾选 shell,即可添加 FinSH 组件的源码到工程中。
RT-Thread Nano移植使用整理_第39张图片
然后在生成后的代码中,找到 rtconfig.h,使能 #include "finsh_config.h"

2、实现 rt_hw_console_getchar

注:此部分为 3.1.5 版本中 #error TODO 4 的部分:#error "TODO 4: Read a char from the uart and assign it to 'ch'."

要实现 FinSH 组件功能:既可以打印也能输入命令进行调试,控制台已经实现了打印功能,现在还需要在 board.c 中对接控制台输入函数,实现字符输入:

/* 实现 3:finsh 获取一个字符,系统函数,函数名不可更改 */
char rt_hw_console_getchar(void);

rt_hw_console_getchar():控制台获取一个字符,即在该函数中实现 uart 获取字符,可以使用查询方式获取(注意不要死等,在未获取到字符时,需要让出 CPU),推荐使用中断方式获取。

示例代码:如下是基于 STM32F103 HAL 串口驱动对接的 rt_hw_console_getchar(),完成对接 FinSH 组件,其中获取字符采用查询方式,示例仅做参考,可自行实现中断方式获取字符。

char rt_hw_console_getchar(void)
{
    int ch = -1;

    if (__HAL_UART_GET_FLAG(&UartHandle, UART_FLAG_RXNE) != RESET)
    {
        ch = UartHandle.Instance->DR & 0xff;
    }
    else
    {
        if(__HAL_UART_GET_FLAG(&UartHandle, UART_FLAG_ORE) != RESET)
        {
            __HAL_UART_CLEAR_OREFLAG(&UartHandle);
        }
        rt_thread_mdelay(10);
    }
    return ch;
}

3、结果验证

编译下载代码,打开串口助手,可以在串口助手中打印输入 help 命令,回车查看系统支持的命令:
RT-Thread Nano移植使用整理_第40张图片
如果没有成功运行,请检查对接的函数实现是否正确。

4、移植示例代码

1、中断示例

如下是基于 STM32F103 HAL 串口驱动,实现控制台输出与 FinSH Shell,其中获取字符采用中断方式。原理是,在 uart 接收到数据时产生中断,在中断中把数据存入 ringbuffer 缓冲区,然后释放信号量,tshell 线程接收信号量,然后读取存在 ringbuffer 中的数据。示例仅做参考。

/* 第一部分:ringbuffer 实现部分 */
#include 
#include 

#define rt_ringbuffer_space_len(rb) ((rb)->buffer_size - rt_ringbuffer_data_len(rb))

struct rt_ringbuffer
{
    rt_uint8_t *buffer_ptr;

    rt_uint16_t read_mirror : 1;
    rt_uint16_t read_index : 15;
    rt_uint16_t write_mirror : 1;
    rt_uint16_t write_index : 15;

    rt_int16_t buffer_size;
};

enum rt_ringbuffer_state
{
    RT_RINGBUFFER_EMPTY,
    RT_RINGBUFFER_FULL,
    /* half full is neither full nor empty */
    RT_RINGBUFFER_HALFFULL,
};

rt_inline enum rt_ringbuffer_state rt_ringbuffer_status(struct rt_ringbuffer *rb)
{
    if (rb->read_index == rb->write_index)
    {
        if (rb->read_mirror == rb->write_mirror)
            return RT_RINGBUFFER_EMPTY;
        else
            return RT_RINGBUFFER_FULL;
    }
    return RT_RINGBUFFER_HALFFULL;
}

/**
 * get the size of data in rb
 */
rt_size_t rt_ringbuffer_data_len(struct rt_ringbuffer *rb)
{
    switch (rt_ringbuffer_status(rb))
    {
    case RT_RINGBUFFER_EMPTY:
        return 0;
    case RT_RINGBUFFER_FULL:
        return rb->buffer_size;
    case RT_RINGBUFFER_HALFFULL:
    default:
        if (rb->write_index > rb->read_index)
            return rb->write_index - rb->read_index;
        else
            return rb->buffer_size - (rb->read_index - rb->write_index);
    };
}

void rt_ringbuffer_init(struct rt_ringbuffer *rb,
                        rt_uint8_t           *pool,
                        rt_int16_t            size)
{
    RT_ASSERT(rb != RT_NULL);
    RT_ASSERT(size > 0);

    /* initialize read and write index */
    rb->read_mirror = rb->read_index = 0;
    rb->write_mirror = rb->write_index = 0;

    /* set buffer pool and size */
    rb->buffer_ptr = pool;
    rb->buffer_size = RT_ALIGN_DOWN(size, RT_ALIGN_SIZE);
}

/**
 * put a character into ring buffer
 */
rt_size_t rt_ringbuffer_putchar(struct rt_ringbuffer *rb, const rt_uint8_t ch)
{
    RT_ASSERT(rb != RT_NULL);

    /* whether has enough space */
    if (!rt_ringbuffer_space_len(rb))
        return 0;

    rb->buffer_ptr[rb->write_index] = ch;

    /* flip mirror */
    if (rb->write_index == rb->buffer_size-1)
    {
        rb->write_mirror = ~rb->write_mirror;
        rb->write_index = 0;
    }
    else
    {
        rb->write_index++;
    }

    return 1;
}
/**
 * get a character from a ringbuffer
 */
rt_size_t rt_ringbuffer_getchar(struct rt_ringbuffer *rb, rt_uint8_t *ch)
{
    RT_ASSERT(rb != RT_NULL);

    /* ringbuffer is empty */
    if (!rt_ringbuffer_data_len(rb))
        return 0;

    /* put character */
    *ch = rb->buffer_ptr[rb->read_index];

    if (rb->read_index == rb->buffer_size-1)
    {
        rb->read_mirror = ~rb->read_mirror;
        rb->read_index = 0;
    }
    else
    {
        rb->read_index++;
    }

    return 1;
}


/* 第二部分:finsh 移植对接部分 */
#define UART_RX_BUF_LEN 16
rt_uint8_t uart_rx_buf[UART_RX_BUF_LEN] = {0};
struct rt_ringbuffer  uart_rxcb;         /* 定义一个 ringbuffer cb */
static UART_HandleTypeDef UartHandle;
static struct rt_semaphore shell_rx_sem; /* 定义一个静态信号量 */

/* 初始化串口,中断方式 */
static int uart_init(void)
{
    /* 初始化串口接收 ringbuffer  */
    rt_ringbuffer_init(&uart_rxcb, uart_rx_buf, UART_RX_BUF_LEN);

    /* 初始化串口接收数据的信号量 */
    rt_sem_init(&(shell_rx_sem), "shell_rx", 0, 0);

    /* 初始化串口参数,如波特率、停止位等等 */
    UartHandle.Instance = USART2;
    UartHandle.Init.BaudRate   = 115200;
    UartHandle.Init.HwFlowCtl  = UART_HWCONTROL_NONE;
    UartHandle.Init.Mode       = UART_MODE_TX_RX;
    UartHandle.Init.OverSampling = UART_OVERSAMPLING_16;
    UartHandle.Init.WordLength = UART_WORDLENGTH_8B;
    UartHandle.Init.StopBits   = UART_STOPBITS_1;
    UartHandle.Init.Parity     = UART_PARITY_NONE;

    /* 初始化串口引脚等 */
    if (HAL_UART_Init(&UartHandle) != HAL_OK)
    {
        while (1);
    }

    /* 中断配置 */
    __HAL_UART_ENABLE_IT(&UartHandle, UART_IT_RXNE);
    HAL_NVIC_EnableIRQ(USART2_IRQn);
    HAL_NVIC_SetPriority(USART2_IRQn, 3, 3);

    return 0;
}
INIT_BOARD_EXPORT(uart_init);

/* 移植控制台,实现控制台输出, 对接 rt_hw_console_output */
void rt_hw_console_output(const char *str)
{
    rt_size_t i = 0, size = 0;
    char a = '\r';

    __HAL_UNLOCK(&UartHandle);

    size = rt_strlen(str);
    for (i = 0; i < size; i++)
    {
        if (*(str + i) == '\n')
        {
            HAL_UART_Transmit(&UartHandle, (uint8_t *)&a, 1, 1);
        }
        HAL_UART_Transmit(&UartHandle, (uint8_t *)(str + i), 1, 1);
    }
}

/* 移植 FinSH,实现命令行交互, 需要添加 FinSH 源码,然后再对接 rt_hw_console_getchar */
/* 中断方式 */
char rt_hw_console_getchar(void)
{
    char ch = 0;

    /* 从 ringbuffer 中拿出数据 */
    while (rt_ringbuffer_getchar(&uart_rxcb, (rt_uint8_t *)&ch) != 1)
    {
        rt_sem_take(&shell_rx_sem, RT_WAITING_FOREVER);
    }
    return ch;
}

/* uart 中断 */
void USART2_IRQHandler(void)
{
    int ch = -1;
    rt_base_t level;
    /* enter interrupt */
    rt_interrupt_enter();          //在中断中一定要调用这对函数,进入中断

    if ((__HAL_UART_GET_FLAG(&(UartHandle), UART_FLAG_RXNE) != RESET) &&
        (__HAL_UART_GET_IT_SOURCE(&(UartHandle), UART_IT_RXNE) != RESET))
    {
        while (1)
        {
            ch = -1;
            if (__HAL_UART_GET_FLAG(&(UartHandle), UART_FLAG_RXNE) != RESET)
            {
                ch =  UartHandle.Instance->DR & 0xff;
            }
            if (ch == -1)
            {
                break;
            }
            /* 读取到数据,将数据存入 ringbuffer */
            rt_ringbuffer_putchar(&uart_rxcb, ch);
        }
        rt_sem_release(&shell_rx_sem);
    }

    /* leave interrupt */
    rt_interrupt_leave();    //在中断中一定要调用这对函数,离开中断
}

#define USART_TX_Pin GPIO_PIN_2
#define USART_RX_Pin GPIO_PIN_3

void HAL_UART_MspInit(UART_HandleTypeDef *huart)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    if (huart->Instance == USART2)
    {
        __HAL_RCC_USART2_CLK_ENABLE();

        __HAL_RCC_GPIOA_CLK_ENABLE();
        /**USART2 GPIO Configuration
        PA2     ------> USART2_TX
        PA3     ------> USART2_RX
        */
        GPIO_InitStruct.Pin = USART_TX_Pin | USART_RX_Pin;
        GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
        GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
        HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
    }
}

你可能感兴趣的:(RT-Thread,RT-Thread)