Keil版本:V5.28
RT-Thread Nano版本:3.1.5
开发板单片机:自制最小系统(单片机:STM32F103RCT6)
例程:基于正点原子MINI开发板的“跑马灯例程”(确保裸机例程正常下载并运行。)
参考资料:RTT文档中心(RT-Thread 文档中心)
MDK本身集成了RT-Thread Nano的Pack包,V5.28版本的MDK打开“Pack install”可以看到Nano Pack包默认为3.1.2版本,该版本较早,本次移植主要基于3.1.5,获取3.1.5的可有以下:
1)更新MDK版本,高版本的Nano Pack包默认版本会更高,从MDK软件端直接install下载。
2)Arm Keil | RealThread RT-Thread,进入keil的官网下载Pack包自行安装。
3)https://www.rt-thread.org/download/mdk/RealThread.RT-Thread.3.1.5.pack,直接点击进入下载。
安装完成后显示如下:
进入“Manage Run-Time Environment”,找到刚添加的RTT对应文件,勾选确认后添加进本工程。
添加完毕,工程目录左侧可以看到已添加的SDK,其中“board.c”和“rtconfig.h”则为用户需要移植的文件。
其中“rtconfig.h”提供了图像界面设置,默认设置如下:
编译编译工程,此时工程会提示error 。查看对应的错误位置,即“board.c”
****#error这个语句的作用可以理解成就是要告诉用户此处需要额外关注(即可能需要用户将某些接口函数添加进对应的位置)。因此可以直接注释该语句,同时进入下一步,对“rt_hw_board_init”函数的移植。
查看官方文档对于该函数的介绍如下,但官方文档主要是针对HAL库的移植,对于标准库其实也一样,主要实现的即是对系统时钟和OS节拍的配置。
在正点原子的例程中,对时钟的初始化使用的是自己写的“delay_init();”函数。在该函数中可以看到宏定义“SYSTEM_SUPPORT_OS”,该宏定义的目的是正点原子在移植ucos时需要打开。因此也侧面证明了“delay_init();”函数在没打开该宏定义的情况下,并不适合于跑RTOS。而且由于RTT和ucos并不同,在此处对系统时钟的配置并不推荐直接使用正点原子的“delay_init();”函数。
因此“时钟配置”可写成以下形式:(注意需要添加进头文件“stm32f10x.h”)
“msCnt = SystemCoreClock / RT_TICK_PER_SECOND;”此处的RT_TICK_PER_SECOND即“rtconfig.h”文件中的宏定义,默认为1000,即1ms进入一次滴答定时器的中断。数字越大系统节拍率越快,系统的额外开销就越大。
void rt_hw_board_init(void)
{
//#error "TODO 1: OS Tick Configuration."
/*
* 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.
*/
uint32_t msCnt; // count value of 1ms
SystemCoreClockUpdate();
msCnt = SystemCoreClock / RT_TICK_PER_SECOND;
SysTick_Config(msCnt);
/* 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
}
“OS节拍配置”目的是连接滴答定时器。增加如下代码:
/* cortex-m 架构使用 SysTick_Handler() */
void SysTick_Handler()
{
rt_os_tick_callback();
}
添加完毕后重新编译工程,工程报错HardFault_Handler、PendSV_Handler和SysTick_Handler函数重复。(这三个函数在使用RTT后,会由RTT接收,因此RTT本身会重新定义而造成error)因此需要到对应的“stm32f10x_it.c”文件中注释掉对应的函数。
至此,RTT-Nano已经算是初步移植完毕,可先通过RTT的延时函数”进行移植验证。
其中用户所使用到的RTT函数统一存放在“rtthread.h”中,延时函数为“rt_thread_mdelay(rt_int32_t ms);”,因此在main函数中,将延时函数进行替换,改为如下后进行下载验证,若LED正常按照延时进行闪烁,则证明RTT-Nano初步移植成功。
#include "led.h"
#include "delay.h"
#include "sys.h"
#include "rtthread.h"
int main(void)
{
// delay_init(); //延时函数初始化
LED_Init(); //初始化与LED连接的硬件接口
while(1)
{
LED0=0;
rt_thread_mdelay(500);
LED0=1;
rt_thread_mdelay(500);
}
}
RTT重新映射了printf函数,变成“rt_kprintf();”,但使用该函数需要进行以下移植。
首先需要在“rtconfig.h”添加UART控制台,勾选对应位置后编译。报错提醒如下:
移植“uart_init()”函数,可直接通过修改正点原子的串口初始化程序实现。改动的地方主要有两处:
1)删除中断相关的初始化和中断函数;(后续如果需要移植finsh组件时想通过“rt_hw_console_getchar() ”的方式,那么此处的串口初始化不能使能中断)
2)改变函数名。(原函数名与RTT的“uart_init()”名称重复)
void myuart_init(u32 bound){
//GPIO端口设置
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1|RCC_APB2Periph_GPIOA, ENABLE); //使能USART1,GPIOA时钟
//USART1_TX GPIOA.9
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //PA.9
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出
GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.9
//USART1_RX GPIOA.10初始化
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;//PA10
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入
GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.10
//USART 初始化设置
USART_InitStructure.USART_BaudRate = bound;//串口波特率
USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长为8位数据格式
USART_InitStructure.USART_StopBits = USART_StopBits_1;//一个停止位
USART_InitStructure.USART_Parity = USART_Parity_No;//无奇偶校验位
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//无硬件数据流控制
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; //收发模式
USART_Init(USART1, &USART_InitStructure); //初始化串口1
USART_Cmd(USART1, ENABLE); //使能串口1
}
改动串口初始化函数后,通过函数调用放至board.c文件中,其中官方提供了两种方式。
1)方法一,将串口初始化的函数直接添加到uart_init()中,此时会通过后一句语句“INIT_BOARD_EXPORT(uart_init);”链接到底层。(此种方法在后续开启“rt_config.h”的“enable components initialization debug configuration”会出现bug,不推荐使用)
static int uart_init(void)
{
//#error "TODO 2: Enable the hardware uart and config baudrate."
myuart_init(115200);
return 0;
}
INIT_BOARD_EXPORT(uart_init);
2)方法二,将串口初始化的函数放至“rt_hw_board_init()”函数中,因此该函数改动如下,推荐该方式。
/**
* This function will initial your board.
*/
void rt_hw_board_init(void)
{
//#error "TODO 1: OS Tick Configuration."
/*
* 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.
*/
uint32_t msCnt; // count value of 1ms
SystemCoreClockUpdate();
msCnt = SystemCoreClock / RT_TICK_PER_SECOND;
SysTick_Config(msCnt);
myuart_init(115200);
/* 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
}
移植“rt_hw_console_output()”函数,该函数主要用于实现串口控制台的字符输出。官方文档介绍如下(基于HAL库):
使用标准库时可做如下改动:
void rt_hw_console_output(const char *str)
{
//#error "TODO 3: Output the string 'str' through the uart."
/* 进入临界段 */
//禁止操作系统的调度,进入临界段的代码不允许打断,当rt_scheduler_lock_nest>=1时,调度器停止调度。
rt_enter_critical();
while(*str!='\0')
{
/* 换行 */
if (*str == '\n')//RT-Thread 系统中已有的打印均以 \n 结尾,而并非 \r\n,所以在字符输出时,需要在输出 \n 之前输出 \r,完成回车与换行,否则系统打印出来的信息将只有换行
{
USART_SendData(USART1, '\r');
while(USART_GetFlagStatus(USART1, USART_FLAG_TC)== RESET);
}
USART_SendData(USART1, *(str++));
while(USART_GetFlagStatus(USART1, USART_FLAG_TXE)== RESET); //该标志位使用TC时会导致finsh组件有出错的可能
}
/* 退出临界段 */
rt_exit_critical(); //注意:使用进入临界段语句rt_enter_critical(); 一定要使用退出临界段语句 rt_exit_critical();否则调度器锁住,无法进行调度
}
添加完毕,验证时可直接在main函数中直接添加打印内容,正常打印说明移植成功。
移植成功后,建议将“rt_config.h”的“enable components initialization debug configuration”打开,该功能有助于在查看初始化是否正常。(注意该功能使能打开时,前文提到的串口初始化位置十分重要,否则反而会导致程序异常)
使能打开后,下载程序后,串口默认输出如下:
通过上述的程序移植,已基本验证RTT-Nano在MDK的配置已完成,在实现具体的线程流程前,需要注意以下几点:
1)RTT与freeRTOS不同,RTT在系统启动时,入口函数为“int $Sub$$main(void)”,之后会依次在系统的调度下,依次调用以下函数,所以在main函数中,用户无需添加其他与操作系统初始化相关的函数,由底层自主调用倒最后,才进入传统的main函数。
在这个过程中,RTT会将main函数作为操作系统的一个线程,其线程优先级为“最大线程优先级/3”。因此,若用户在main函数中使用while(1)函数,while(1)内必须添加相应的延时函数,来让出 CPU 的动作以执行其他线程的任务。
2)RTT-Nano默认设置在“rtconfig.h”中为了保持占用RAM最小,仅使能了信号量和邮箱,因此在RAM足够的情况下,可将其他互斥量、事件和消息队列皆使能。
3)RTT-Nano默认设置在“rtconfig.h”中为了保持占用RAM最小,其线程优先级最大设置为32。而RTT的线程优先级定义为数字越小,其优先级越大。若需要移植finsh组件时,finsh组件的默认优先级为“21”,main线程的优先级为“32/3=10”,因此用户在创建自己的线程时,应注意finsh线程和main线程对其的影响。
4)如果用户在移植完后出现hard fault,可首先查看其栈空间设置,rt_config.h中默认设置为256,但实际使用中,由于main函数可能加入了其他代码的初始化导致栈空间不足,可根据情况调大栈空间。
线程的相关操作包括创建/初始化线程、启动线程、删除/脱离线程。
其中线程的创建方式分为静态创建和动态创建两种方式。其主要区别在于创建的线程其大小是否为提前指定,或者为系统动态提供。
静态创建方式如下,其中“ thread1_entry”和“ thread2_entry”为线程入口函数,“thread1_stack”和“thread2_stack”分别为数组,数组元素256,因此这两个线程申请的栈空间都是256字节。(注意不能将同一个数组的首地址放在两个不同的线程中,否则会导致异常)
/* 初始化线程 1,名称是 thread1,入口是 thread1_entry*/
rt_thread_init(&tid1,
"thread1",
thread1_entry,
RT_NULL,
&thread1_stack[0],
sizeof(thread1_stack),
20,
5);
/* 初始化线程 2,名称是 thread2,入口是 thread2_entry*/
rt_thread_init(&tid2,
"thread2",
thread2_entry,
RT_NULL,
&thread2_stack[0],
sizeof(thread2_stack),
20,
5);
动态创建方式如下,同样给该线程一个256字节的栈空间,创建完毕后分别对三个线程进行启动。
/* 初始化线程 2,名称是 thread3,入口是 thread3_entry*/
tid3 = rt_thread_create("thread3",
thread3_entry,
RT_NULL,
256,
20,
5);
/* 启动线程 */
rt_thread_startup(&tid1);
rt_thread_startup(&tid2);
rt_thread_startup(tid3);
线程函数和串口输出如下:
/* 线程 1 的入口函数 */
static void thread1_entry(void *parameter)
{
while (1)
{
rt_kprintf("thread1_entry\r\n\r\n");
rt_thread_mdelay(500);
}
}
/* 线程 2 的入口函数 */
static void thread2_entry(void *parameter)
{
while (1)
{
LED0=!LED0;
rt_kprintf("thread2 LED闪烁\r\n\r\n");
rt_thread_mdelay(500); //延时500ms
}
}
/* 线程 3 的入口函数 */
static void thread3_entry(void *parameter)
{
while (1)
{
rt_kprintf("thread3 entry\r\n\r\n");
rt_thread_mdelay(500);
}
}
finsh组件的官方介绍如下。
可以理解成用户在使用finsh组件后,可直接通过串口的命令形式,来访问线程的使用情况,以此来更好的调试线程程序。
finsh组件开启需要通过“rt_config.h”使能其配置。
使能完成后,编译会发现出现了很多error的报错,接下来需要添加finsh组件相关的文件进入工程。相关的文件默认路径如下:(该文件夹在第一步安装RTT-Nano的pack包后,就自动添加到MDK安装的盘符中)
将“components”整个文件夹复制到我们的工程中,依次将“finsh”文件内的.c文件添加入工程,添加路径后,工程目录如下:
此时编译报错如下。
针对报错信息,移植步骤如下:
1)由于报错的依旧是起到提醒作用的#error语句,报错文件为finsh_port.c,该文件默认为只读。因此需要先改动文件属性后,再注释该条报错语句。
2)该函数由“RT_WEAK”进行声明,即弱函数声明,因此允许我们在其他地方对该函数进行重定义。为了方便代码管理,我们将该函数移植board.c,同时添加以下代码:
/*finsh控制台作为一个线程也在不断的执行,默认优先级为21,主线程的函数优先级需大于21(数字小于21),否则会无法响应主线程*/
char rt_hw_console_getchar(void)
{
/* Note: the initial value of ch must < 0 */
int ch = -1;
//#error "TODO 4: Read a char from the uart and assign it to 'ch'."
if (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) != RESET)
{
ch = (char)USART_ReceiveData(USART1);
}
else
{
if (USART_GetFlagStatus(USART1, USART_FLAG_ORE) != RESET)
{
USART_ClearFlag(USART1, USART_FLAG_TC);
}
}
return ch;
}
这里再次强调,串口初始化不要初始化和使能中断,否则finsh将会移植失败(因为这个原因我查了几个小时)
至此finsh组件移植完毕,验证是否成功,可通过串口助手验证,串口助手勾选“发送新行”,发送空数据时,系统回复“msh >”
用户可通过发送help,查看finsh组件所支持的命令。
注意:由于上述移植“rt_hw_console_output()”函数时,对串口的标志位有严格要求,我本人试出标志位在使用USART_FLAG_TXE时才正常,若两个标志位都是USART_FLAG_TC,则有可能会出现发送字节丢失的可能。
正常执行的情况如下:
最后,finsh组件除了支持内置命令,还支持自定义命令,具体使用方法这里就不一一演示了,具体可查看RTT官方文档,文档路径:RT-Thread 文档中心