https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-nano/an0038-nano-introduction
RT-Thread Nano
是一个极简版的硬实时内核,它是由 C
语言开发,采用面向对象的编程思维,具有良好的代码风格,是一款可裁剪的、抢占式实时多任务的 RTOS
。其内存资源占用极小,功能包括任务处理、软件定时器、信号量、邮箱和实时调度等相对完整的实时操作系统特性。适用于家电、消费电子、医疗设备、工控等领域大量使用的 32
位 ARM
入门级 MCU
的场合。
下图是 RT-Thread Nano
的软件框图,包含支持的 CPU
架构与内核源码,还有可拆卸的 FinSH
组件:
支持架构:ARM
:Cortex M0/ M3/ M4/ M7
等、RISC-V
及其他。
功能:线程管理、线程间同步与通信、时钟管理、中断管理、内存管理。
RT-Thread Nano
以软件包的方式集成在 Keil MDK
与 CubeMX
中,可以直接在软件中下载 Nano
软件包获取源码。
与 RT-Thread
完整版不同的是,Nano
不含 Scons
构建系统,不需要 Kconfig
以及 Env
配置工具,也去除了完整版特有的 device
框架和组件,仅是一个纯净的内核。
由于 Nano
的极简特性,使 Nano
的移植过程变得极为简单。添加 Nano
源码到工程,就已完成 90% 的移植工作。
RT-Thread Nano
在使用上也非常简单,带给开发者友好的开发体验。
Nano
的配置文件为 rtconfig.h
,该文件中列出了内核中的所有宏定义,有些默认没有打开,如需使用,打开即可。FinSH
组件:FinSH
组件 可以很方便的在 Nano
上进行移植,而不再依赖 device
框架。ST
的 STD
库、HAL
库、LL
库等,可以自行选择。资源占用小:对 RAM
与 ROM
的开销非常小,在支持 semaphore
和 mailbox
特性,并运行两个线程 (main
线程 + idle
线程) 情况下,ROM
和 RAM
依然保持着极小的尺寸,RAM
占用约 1K 左右,ROM
占用 4K 左右。
本片文档介绍 Nano
移植原理,针对的是不同 MCU
的移植,如 Cortex M
,RISC-V
,或者是其他 MCU
的移植。移植过程主要分为两个部分:libcpu
移植与板级移植,在讲解移植之前,本文档对 RT-Thread Nano
的启动流程与移植目录结构先进行说明。
RT-Thread
启动流程如下所示,在图中标出颜色的部分需要用户特别注意(黄色表示 libcpu
移植相关的内容,绿色部分表示板级移植相关的内容)。
RT-Thread
启动代码统一入口为 rtthread_startup()
,芯片启动文件在完成必要工作(如初始化时钟、配置中断向量表、初始化堆栈等)后,最终会在程序跳转时,跳转至 RT-Thread
的启动入口中。RT-Thread
的启动流程如下:
main
线程(同时会初始化线程栈),在 main
线程中对各类模块依次进行初始化。main
线程),并打开全局中断。在 rtthread-nano
源码中,与移植相关的文件位于下图中有颜色标记的路径下(黄色表示 libcpu
移植相关的文件,绿色部分表示板级移植相关的文件):
RT-Thread
的 libcpu
抽象层向下提供了一套统一的 CPU
架构移植接口,这部分接口包含了全局中断开关函数、线程上下文切换函数、时钟节拍的配置和中断函数、Cache
等等内容,RT-Thread
支持的 cpu
架构在源码的 libcpu
文件夹下。
启动文件由芯片厂商提供,位于芯片固件库中。每款芯片都有相对应的启动文件,在不同开发环境下启动文件也不相同。当系统加入 RT-Thread
之后,会将 RT-Thread
的启动放在调用 main()
函数之前,如下图所示:
startup.s
:主要完成初始化时钟、配置中断向量表;完成全局 / 静态变量的初始化工作;初始化堆栈;库函数的初始化;程序的跳转等内容。
程序跳转:芯片在 KEIL MDK
与 IAR
下的启动文件不用做修改,会自动转到 RT-Thread
系统启动函数 rtthread_startup()
。GCC
下的启动文件需要修改,让其跳转到 RT-Thread
提供的 entry()
函数,其中 entry()
函数调用了 RT-Thread
系统启动函数 rtthread_startup()
。
最终调用 main()
函数进入用户 main()
。
上下文切换表示 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()
里完成。
在 RT-Thread
中,线程具有独立的栈,当进行线程切换时,会将当前线程的上下文存在栈中,当线程要恢复运行时,再从栈中读取上下文信息,进行恢复。
故障异常处理函数 rt_hw_hard_fault_exception()
,在发生硬件错误时,执行 HardFault_Handler
中断,会执行该函数。
该文件中主要实现线程栈的初始化 rt_hw_stack_init()
与 hard fault
异常处理函数。
需实现的函数 | 描述 |
---|---|
rt_hw_stack_init() | 实现线程栈的初始化 |
rt_hw_hard_fault_exception() | 异常函数:系统硬件错误 |
注意:在 Cortex-M
内核上,所有中断都采用中断向量表的方式进行处理,即当一个中断触发时,处理器将直接判定是哪个中断源,然后直接跳转到相应的固定位置进行处理,不需要再自行实现中断管理。
在一些非 Cortex-M
架构中,系统没有实现类似中断向量表的功能,物理中断要和用户的中断服务例程相关联,就需要使用中断管理接口对中断进行管理,这样当发生中断时就可以触发相应的中断,执行中断服务例程。
需实现的中断管理接口 | 描述 |
---|---|
rt_hw_interrupt_init() | 硬件中断初始化 |
rt_hw_interrupt_install() | 中断服务程序挂接 |
rt_hw_interrupt_mask() | 屏蔽指定的中断源 |
rt_hw_interrupt_umask() | 打开被屏蔽的中断源 |
注:board.c
、rtconfig.h
是与硬件 / 板级相关的文件,在移植时需自行实现。Cortex M
架构可参考 Nano
源码 bsp
文件夹中已有的的 board.c
、rtconfig.h
。
板级移植主要是针对 rt_hw_board_init()
函数内容的实现,该函数在板级配置文件 board.c
中,函数中做了许多系统启动必要的工作,其中包含:
OS
节拍。(其中步骤 1 和 2 为 3.1.5 版本中 #error TODO 1
的部分:#error "TODO 1: OS Tick Configuration."
)GPIO/UART
等等,若需要请在此处调用。INIT_BOARD_EXPORT()
自动初始化的函数会在此处被初始化。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
/* 第六部分:其他初始化 */
}
系统时钟是给各个硬件模块提供工作时钟的基础,在 rt_hw_board_init()
函数中完成,可以调用库函数实现配置,也可以自行实现。
如下是配置系统时钟调用示例:
/* board.c */
void rt_hw_board_init()
{
/* 第一部分:系统初始化、系统时钟配置等 */
rt_hw_clock_init() // 时钟初始化,函数名不做要求,函数自行实现,如 SystemClock_Config()、SystemCoreClockUpdate()
...
}
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
。通过修改该宏的值,可以修改系统中一个时钟节拍的时间长度。
硬件初始化,如 UART
初始化等(对接控制台),需要在 rt_hw_board_init()
函数中手动调用 UART
初始化函数。
/* board.c */
void rt_hw_board_init(void)
{
....
/* 第三部分:初始化硬件外设,若有需要,则放在此处调用 */
uart_init();
....
}
注意,uart_init()
或者其他的外设初始化函数,若已经使用了宏 INIT_BOARD_EXPORT()
进行初始化,则不需要在此进行显式调用。两种初始化方法选择一种即可。
RT-Thread Nano
默认不开启动态内存堆功能,开启 RT_USING_HEAP
将可以使用动态内存功能,即可以使用 rt_malloc
、rt_free
以及各种系统动态创建对象的 API。动态内存堆管理功能的初始化是通过 rt_system_heap_init()
函数完成的,动态内存堆的初始化需要指定堆内存的起始地址和结束地址,函数原型如下:
void rt_system_heap_init(void *begin_addr, void *end_addr)
开启 RT_USING_HEA
P 后,系统默认使用数组作为 heap
,heap
的起始地址与结束地址作为参数传入 heap
初始化函数,heap
初始化函数 rt_system_heap_init()
将在 rt_hw_board_init()
中被调用。
开启 heap
后,系统中默认使用数组作为 heap
(heap
默认较小,实际使用时请根据芯片 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
....
}
链接脚本,也称分散加载文件,决定在生成 image
文件时如何来分配相关数据的存放基址,如果不指定特定的链接脚本,连接器就会自动采用默认的链接脚本来生成镜像。
举例 stm32
在 KEIL 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
结尾处之后的内存空间作为系统动态内存堆使用。
nano
版本选择工程使用芯片内部 HSI
时钟,如需修改,则请修改 drv_clk.c
。
注:可以通过修改 drv_clk.c
的 SystemClock_Config()
更改系统时钟。
工程创建完毕,连接硬件,可直接进行编译下载,如下所示:
由于在创建工程向导中配置了控制台串口号及其引脚号,所以工程中已经实现了 uart
的驱动以及 rt_hw_console_output()
,默认可以进行打印。打开串口终端,可以发现在终端中执行了打印。
双击 RT-Thread Settings
进入配置,打开组件,勾选 FinSH Shell
,保存配置。此操作将把 FinSH
组件的源码加入工程中。
其中,rt_hw_console_getchar()
已经在 drv_uart.c
中实现,无需再实现对接 FinSH
的代码。
链接硬件,编译下载后,在串口终端中按下 Tab
键,可查看系统中的命令:
本文介绍如何基于 Keil MDK
移植 RT-Thread Nano
,并以一个 stm32f103
的基础工程作为示例进行讲解。
RT-Thread Nano
已集成在 Keil MDK
中,可以直接在 IDE
中进行下载添加。本文档介绍了如何使用 MDK
移植 RT-Thread Nano
,并以一个 stm32f103
的基础工程作为示例进行讲解。
移植 Nano
的主要步骤:
keil MDK
工程,并获取 RT-Thread Nano pack
安装包并进行安装。RT-Thread Nano
源码。Nano
,主要从 中断、时钟、内存这几个方面进行适配,实现移植。RT-Thread Nano
闪烁 LED
。Nano
进行配置:Nano
是可裁剪的,通过配置文件 rtconfig.h
实现对系统的裁剪。1、准备一份基础的裸机源码工程。
2、在 KEIL
上安装 RT-Thread Nano Pack
。
在移植 RT-Thread Nano
之前,我们需要准备一个能正常运行的裸机工程。
Nano Pack
可以通过在 Keil MDK IDE
内进行安装,也可以手动安装。下面开始介绍两种安装方式。
打开 MDK
软件,点击工具栏的 Pack Installer
图标:
点击右侧的 Pack
,展开 Generic
,可以找到 RealThread::RT-Thread
,点击 Action
栏对应的 Install ,就可以在线安装 Nano Pack
了。另外,如果需要安装其他版本,则需要展开 RealThread::RT-Thread
,进行选择,箭头所指代表已经安装的版本。
我们也可以从官网下载安装文件,RT-Thread Nano 离线安装包下载,下载结束后双击文件进行安装:
打开已经准备好的可以运行的裸机程序,将 RT-Thread
添加到工程。如下图,点击 Manage Run-Time Environment
。
在 Manage Rum-Time Environment
里 "Software Component"
栏找到 RTOS
,Variant
栏选择 RT-Thread
,然后勾选 kernel
,点击 "OK"
就添加 RT-Thread
内核到工程了。
现在可以在 Project
看到 RT-Thread RTOS
已经添加进来了,展开 RTOS
,可以看到添加到工程的文件:
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
RT-Thread
会接管异常处理函数 HardFault_Handler()
和悬挂处理函数 PendSV_Handler()
,这两个函数已由 RT-Thread
实现,所以需要删除工程里中断服务例程文件中的这两个函数,避免在编译时产生重复定义。如果此时对工程进行编译,没有出现函数重复定义的错误,则不用做修改。
需要在 board.c
中实现 系统时钟配置
(为 MCU
、外设提供工作时钟)与 os tick
的配置 (为操作系统提供心跳 / 节拍)。
如下代码所示,用户需要在 board.c
文件中系统初始化和 OS Tick
的配置,用户需在 timer
定时器中断服务函数调用 rt_os_tick_callback function
,cortex-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
函数,实现时间片切换(也可以使用其他定时器)。
注:以上部分需要自己实现
系统内存堆的初始化在 board.c
中的 rt_hw_board_init()
函数中完成,内存堆功能是否使用取决于宏 RT_USING_HEAP
是否开启,RT-Thread Nano
默认不开启内存堆功能,这样可以保持一个较小的体积,不用为内存堆开辟空间。
开启系统 heap
将可以使用动态内存功能,如使用 rt_malloc
、rt_free
以及各种系统动态创建对象的 API
。若需要使用系统内存堆功能,则打开 RT_USING_HEAP
宏定义即可,此时内存堆初始化函数 rt_system_heap_init()
将被调用,如下所示:
初始化内存堆需要堆的起始地址与结束地址这两个参数,系统中默认使用数组作为 heap
,并获取了 heap
的起始地址与结束地址,该数组大小可手动更改,如下所示:
注意:开启 heap
动态内存功能后,heap
默认值较小,在使用的时候需要改大,否则可能会有申请内存失败或者创建线程失败的情况,修改方法有以下两种:
RT_HEAP_SIZE
的大小,至少大于各个动态申请内存大小之和,但要小于芯片 RAM
总大小。RAM ZI
段结尾处作为 HEAP
的起始地址,使用 RAM
的结尾地址作为 HEAP
的结尾地址,这是 heap
能设置的最大值的方法。移植好 RT-Thread Nano
之后,则可以开始编写第一个应用代码验证移植结果。此时 main()
函数就转变成 RT-Thread
操作系统的一个线程,现在可以在 main()
函数中实现第一个应用:从串口1打印信息。
RT-Thread
的相关头文件
。main()
函数中(也就是在 main
线程中)实现输出"RT-Thread\r\n"
字符串。RT-Thread
提供的延时函数 rt_thread_mdelay()
。该函数会引起系统调度,切换到其他线程运行,体现了线程实时性的特点。编译程序之后下载到芯片就可以看到基于 RT-Thread
的程序运行起来了,串口1输出"RT-Thread\r\n"
。
注:当添加 RT-Thread
之后,裸机中的 main()
函数会自动变成 RT-Thread
系统中 main
线程 的入口函数。由于线程不能一直独占 CPU
,所以此时在 main()
中使用 while(1)
时,需要有让出 CPU
的动作,比如使用 rt_thread_mdelay()
系列的函数让出 CPU
。
1、延时函数不同
RT-Thread
提供的 rt_thread_mdelay()
函数可以引起操作系统进行调度,当调用该函数进行延时时,本线程将不占用 CPU
,调度器切换到系统的其他线程开始运行。而裸机的 delay
函数是一直占用 CPU
运行的。
2、初始化系统时钟的位置不同
移植好 RT-Thread Nano
之后,不需要再在 main()
中做相应的系统配置(如 hal
初始化、时钟初始化等),这是因为 RT-Thread
在系统启动时,已经做好了系统时钟初始化等的配置。
用户可以根据自己的需要通过修改 rtconfig.h
文件里面的宏定义配置相应功能。
RT-Thread Nano
默认未开启宏 RT_USING_HEAP
,故只支持静态方式创建任务、信号量等对象。若要通过动态方式创建对象则需要在 rtconfig.h
文件里开启 RT_USING_HEAP
宏定义。
MDK
的配置向导 configuration Wizard
可以很方便的对工程进行配置,Value
一栏可以选中对应功能及修改相关值,等同于直接修改配置文件 rtconfig.h
。
本文介绍了如何基于 CubeMX
移植 RT-Thread Nano
,并说明生成代码工程的步骤。
RT-Thread Nano
已集成在 CubeMX
中,可以直接在 IDE
中进行下载添加。本文档介绍了如何使用 CubeMX
移植 RT-Thread Nano
,并以一个 stm32f103
的基础工程作为示例进行讲解。
移植 Nano
的主要步骤:
CubeMX
基础工程,并获取 RT-Thread Nano pack
安装包进行安装。RT-Thread Nano
源码。Nano
,主要从 中断、时钟、内存、应用 这几个方面进行适配,实现移植。Nano
进行配置:Nano
是可裁剪的,可以通过配置文件 rtconfig.h
实现对系统的裁剪。Cube MX 5.0
,下载地址 https://www.st.com/en/development-tools/stm32cubemx.html 。CubeMX
上下载 RT-Thread 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
,如下图所示:
check
通过后,点击 OK 回到 User Defined Packs Manager
界面,再次点击 OK,CubeMX 自动连接服务器,获取包描述文件。回到 Manage embedded software packages
界面,就会发现 RT-Thread Nano 3.1.5
软件包,选择该软件包,点击 Install Now
,如下图所示:
点击安装之后,弹出 Licensing Agreement
,同意协议,点击 Finish
,如下图所示:
等待安装完成,成功安装后,版本前面的小蓝色框变成填充的黄绿色,现象如下图所示:
至此,RT-Thread Nano
软件包安装完毕,退出 Manage embedded software packages
界面,进入 CubeMX
主界面。
在 CubeMX
主界面的菜单栏中 File
选择 New Project
,如下图所示
新建工程之后,在弹出界面芯片型号中输入某一芯片型号,方便锁定查找需要的芯片,双击被选中的芯片,如下图所示
时钟树的配置直接使用默认即可,然后还需要配置下载方式。
选中芯片型号之后,点击 Softwares Packages
->Select Components
,进入组件配置界面,选择 RealThread
, 然后根据需求选择 RT-Thread
组件,然后点击 OK
按钮,如下图所示:
注意:RT-Thread Nano
软件包中包含 kernel
, shell
和 device
三个部分,仅选择 kernel
表示只使用 RT-Thread
内核,工程中会添加内核代码;选择 kernel
与 shell
表示在使用 RT-Thread Nano
的基础上使用 FinSH Shell
组件,工程中会添加内核代码与 FinSH
组件的代码。再选择 device
表示使用 rt-thread
的 device
框架,用户基于此框架编写外设驱动并注册后,就可以使用 device
统一接口操作外设。
选择组件之后,对组件参数进行配置。在工程界面 Pinout & Configuration
中,进入所选组件参数配置区,按照下图进行配置
给工程取名、选择代码存放位置、选择生成代码的 Toolchain/IDE
。Cube MX
不仅能够生成 Keil4/Keil5
的工程,而且还能够生成 IAR7/IAR8
等 IDE 的工程,功能强大,本文从下拉框中选择 MDK5,操作如图所示
根据需求配置 MCU
的功能。
RT-Thread
操作系统重定义 HardFault_Handler
、PendSV_Handler
、SysTick_Handler
中断函数,为了避免重复定义的问题,在生成工程之前,需要在中断配置中,代码生成的选项中,取消选择三个中断函数(对应注释选项是 Hard fault interrupt
, Pendable request
, Time base :System tick timer
),最后点击生成代码,具体操作如下图 所示:
等待工程生成完毕,点击打开工程,如下图所示,即可进入 MDK5
工程中。
需要在 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
}
系统内存堆的初始化在 board.c
中的 rt_hw_board_init()
函数中完成,内存堆功能是否使用取决于宏 RT_USING_HEAP
是否开启,RT-Thread Nano
默认不开启内存堆功能,这样可以保持一个较小的体积,不用为内存堆开辟空间。
开启系统 heap
将可以使用动态内存功能,如使用 rt_malloc
、rt_free
以及各种系统动态创建对象的 API
。若需要使用系统内存堆功能,则打开 RT_USING_HEAP
宏定义即可,此时内存堆初始化函数 rt_system_heap_init()
将被调用,如下所示:
初始化内存堆需要堆的起始地址与结束地址这两个参数,系统中默认使用数组作为 heap
,并获取了 heap
的起始地址与结束地址,该数组大小可手动更改,如下所示:
注意:开启 heap
动态内存功能后,heap
默认值较小,在使用的时候需要改大,否则可能会有申请内存失败或者创建线程失败的情况,修改方法有以下两种:
RT_HEAP_SIZE
的大小,至少大于各个动态申请内存大小之和,但要小于芯片 RAM
总大小。RAM ZI
段结尾处作为 HEAP
的起始地址,使用 RAM
的结尾地址作为 HEAP
的结尾地址,这是 heap
能设置的最大值的方法。移植好 RT-Thread Nano
之后,则可以开始编写第一个应用代码。此时 main()
函数就转变成 RT-Thread
操作系统的一个线程,现在可以在 main()
函数中实现第一个应用:从串口1打印“CubeMX RT-Thread\r\n”
。
RT-Thread
的相关头文件
。main()
函数中(也就是在 main
线程中)写代码:初始化 串口1、在循环中从串口1打印“CubeMX RT-Thread\r\n”
。RT-Thread
提供的延时函数 rt_thread_mdelay()
,该函数会引起系统调度,切换到其他线程运行,体现了线程实时性的特点。编译程序之后下载到芯片就可以看到基于 RT-Thread
的程序运行起来了,LED
正常闪烁。
注:当添加 RT-Thread
之后,裸机中的 main()
函数会自动变成 RT-Thread
系统中 main
线程 的入口函数。由于线程不能一直独占 CPU
,所以此时在 main()
中使用 while(1)
时,需要有让出 CPU
的动作,比如使用 rt_thread_mdelay()
系列的函数让出 CPU
。
1、延时函数不同
RT-Thread
提供的 rt_thread_mdelay()
函数可以引起操作系统进行调度,当调用该函数进行延时时,本线程将不占用 CPU
,调度器切换到系统的其他线程开始运行。而裸机的 delay 函数是一直占用 CPU
运行的。
2、初始化系统时钟的位置不同
移植好 RT-Thread Nano
之后,不需要再在 main()
中做相应的系统配置(如 hal
初始化、时钟初始化等),这是因为 RT-Thread
在系统启动时,已经做好了系统时钟初始化等的配置。
配置 RT-Thread Nano
可以在上面小节 添加 RT-Thread Nano -> 配置 Nano
中,这是在生成工程之前做的配置。如果生成工程之后,想直接在目标工程的 IDE
中配置,那么直接修改工程中 rtconfig.h
文件即可。
RT-Thread Nano
的配置在 rtconfig.h
中进行,通过开关宏定义来使能或关闭某些功能,接下来对该配置文件中的宏定义进行说明。
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
定义 RT_DEBUG
宏则开启 debug
模式。若开启系统调试,则在实现打印之后可以打印系统 LOG
日志。请在代码开发与调试过程中打开该项,帮助调试定位问题,在代码发布时关闭该项。
//#define RT_DEBUG // 关闭 debug
#define RT_DEBUG_INIT 0 // 启用组件初始化调试配置,设置为 1 则会打印自动初始化的函数名称
//#define RT_USING_OVERFLOW_CHECK // 关闭栈溢出检查
设置是否使用钩子函数,默认关闭。
//#define RT_USING_HOOK // 是否 开启系统钩子功能
//#define RT_USING_IDLE_HOOK // 是否 开启空闲线程钩子功能
设置是否启用软件定时器,以及相关参数的配置,默认关闭。
#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 字节
系统支持的 IPC
有:信号量、互斥量、事件集、邮箱、消息队列。通过定义相应的宏打开或关闭该 IPC
的使用。
#define RT_USING_SEMAPHORE // 设置是否使用 信号量,默认打开
//#define RT_USING_MUTEX // 设置是否使用 互斥量
//#define RT_USING_EVENT // 设置是否使用 事件集
#define RT_USING_MAILBOX // 设置是否使用 邮箱
//#define RT_USING_MESSAGEQUEUE // 设置是否使用 消息队列
RT-Thread
内存管理包含:内存池、内存堆、小内存算法。通过开启相应的宏定义使用相应的功能。
//#define RT_USING_MEMPOOL // 是否使用 内存池
#define RT_USING_HEAP // 是否使用 内存堆
#define RT_USING_SMALL_MEM // 是否使用 小内存管理
//#define RT_USING_TINY_SIZE // 是否使用 小体积的算法,牵扯到 rt_memset、rt_memcpy 所产生的体积
当系统加入 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
组件源码,请勿开启此项。
当系统中加入 device
框架源码时,则需要在 rtconfig.h
中开启以下项
#define RT_USING_DEVICE
开启该项则将加入 device
框架源码。
注意:若未加入 device
源码,请勿开启此项。
本篇文档分为两部分:
UART
控制台(实现打印):用来向控制台对接的终端输出打印信息;该部分只需要实现两个函数,串口初始化和系统输出函数,即可完成 UART
控制台打印功能。FinSH
组件(实现命令输入),用以在控制台输入命令调试系统;该部分的实现基于第一部分,只需要添加 FinSH
组件源码并再对接一个系统输入函数即可实现。在 RT-Thread Nano
上添加 UART
控制台打印功能后,就可以在代码中使用 RT-Thread
提供的打印函数 rt_kprintf()
进行信息打印,从而获取自定义的打印信息,方便定位代码 bug
或者获取系统当前运行状态等。实现控制台打印(需要确认 rtconfig.h
中已使能 RT_USING_CONSOLE
宏定义),需要完成基本的硬件初始化,以及对接一个系统输出字符的函数,本小节将详细说明。
注:此部分为 3.1.5 版本中 #error TODO 2
的部分:#error "TODO 2: Enable the hardware uart and config baudrate."
使用串口对接控制台的打印,首先需要初始化串口,如引脚、波特率等。 初始化的串口函数 uart_init()
有以下两种调用方式,二选一:
INIT_BOARD_EXPORT()
进行自动初始化,不需要显式调用,如下所示。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 函数中直接调用 串口初始化 函数 */
....
}
注:此部分为 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);
}
}
在应用代码中编写含有 rt_kprintf()
打印的代码,编译下载,打开串口助手进行验证。如下图是一个在 main()
函数中每隔 1 秒进行循环打印 Hello RT-Thread
的示例效果:
RT-Thread FinSH是 RT-Thread 的命令行组件(shell),提供一套供用户在命令行调用的操作接口,主要用于调试或查看系统信息。它可以使用串口 / 以太网 / USB 等与 PC 机进行通信,使用 FinSH 组件基本命令的效果图如下所示:
本文以串口 UART
作为 FinSH
的输入输出端口与 PC
进行通信,描述如何在 Nano
上实现 FinSH shell
功能。
在 RT-Thread Nano
上添加 FinSH
组件,实现 FinSH
功能的步骤主要如下:
FinSH
源码到工程。
勾选 shell
,这将自动把 FinSH
组件的源码到工程:
然后在 rtconfig.h
中打开 finsh
相关选项,如下图:
打开一个 cube
工程,点击 Additional Software
,在 Pack Vendor
中可勾选 RealThread
快速定位 RT-Thread
软件包,然后在 RT-Thread
软件包中勾选 shell
,即可添加 FinSH
组件的源码到工程中。
然后在生成后的代码中,找到 rtconfig.h
,使能 #include "finsh_config.h"
。
注:此部分为 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;
}
编译下载代码,打开串口助手,可以在串口助手中打印输入 help
命令,回车查看系统支持的命令:
如果没有成功运行,请检查对接的函数实现是否正确。
如下是基于 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);
}
}