RT-Thread Nano - Keil移植学习笔记

一、开发环境

        Keil版本:V5.28

        RT-Thread Nano版本:3.1.5

        开发板单片机:自制最小系统(单片机:STM32F103RCT6)

        例程:基于正点原子MINI开发板的“跑马灯例程”(确保裸机例程正常下载并运行。)

        参考资料:RTT文档中心(RT-Thread 文档中心)

二、移植记录

1、MDK Nano的pack包添加及移植        

RT-Thread Nano - Keil移植学习笔记_第1张图片

         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对应文件,勾选确认后添加进本工程。

RT-Thread Nano - Keil移植学习笔记_第2张图片

        添加完毕,工程目录左侧可以看到已添加的SDK,其中“board.c”和“rtconfig.h”则为用户需要移植的文件。

        其中“rtconfig.h”提供了图像界面设置,默认设置如下:

RT-Thread Nano - Keil移植学习笔记_第3张图片

         编译编译工程,此时工程会提示error 。查看对应的错误位置,即“board.c”

RT-Thread Nano - Keil移植学习笔记_第4张图片

RT-Thread Nano - Keil移植学习笔记_第5张图片

  ****#error这个语句的作用可以理解成就是要告诉用户此处需要额外关注(即可能需要用户将某些接口函数添加进对应的位置)。因此可以直接注释该语句,同时进入下一步,对“rt_hw_board_init”函数的移植。

2、rt_hw_board_init移植

     

RT-Thread Nano - Keil移植学习笔记_第6张图片

         查看官方文档对于该函数的介绍如下,但官方文档主要是针对HAL库的移植,对于标准库其实也一样,主要实现的即是对系统时钟和OS节拍的配置。

RT-Thread Nano - Keil移植学习笔记_第7张图片

         在正点原子的例程中,对时钟的初始化使用的是自己写的“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”文件中注释掉对应的函数。

RT-Thread Nano - Keil移植学习笔记_第8张图片

3、移植验证

        至此,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);	
	}
 }

4、 rt_kprintf函数打印的移植

        RTT重新映射了printf函数,变成“rt_kprintf();”,但使用该函数需要进行以下移植。

        首先需要在“rtconfig.h”添加UART控制台,勾选对应位置后编译。报错提醒如下:

RT-Thread Nano - Keil移植学习笔记_第9张图片

RT-Thread Nano - Keil移植学习笔记_第10张图片

         移植“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文件中,其中官方提供了两种方式。

RT-Thread Nano - Keil移植学习笔记_第11张图片

 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库):

RT-Thread Nano - Keil移植学习笔记_第12张图片

         使用标准库时可做如下改动:

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-Thread Nano - Keil移植学习笔记_第13张图片

RT-Thread Nano - Keil移植学习笔记_第14张图片

         移植成功后,建议将“rt_config.h”的“enable components initialization debug configuration”打开,该功能有助于在查看初始化是否正常。(注意该功能使能打开时,前文提到的串口初始化位置十分重要,否则反而会导致程序异常)

        

         使能打开后,下载程序后,串口默认输出如下:

RT-Thread Nano - Keil移植学习笔记_第15张图片

三、应用笔记(重要)

        通过上述的程序移植,已基本验证RTT-Nano在MDK的配置已完成,在实现具体的线程流程前,需要注意以下几点:

        1)RTT与freeRTOS不同,RTT在系统启动时,入口函数为“int $Sub$$main(void)”,之后会依次在系统的调度下,依次调用以下函数,所以在main函数中,用户无需添加其他与操作系统初始化相关的函数,由底层自主调用倒最后,才进入传统的main函数。

        在这个过程中,RTT会将main函数作为操作系统的一个线程,其线程优先级为“最大线程优先级/3”。因此,若用户在main函数中使用while(1)函数,while(1)内必须添加相应的延时函数,来让出 CPU 的动作以执行其他线程的任务。

RT-Thread Nano - Keil移植学习笔记_第16张图片

        2)RTT-Nano默认设置在“rtconfig.h”中为了保持占用RAM最小,仅使能了信号量和邮箱,因此在RAM足够的情况下,可将其他互斥量、事件和消息队列皆使能。

RT-Thread Nano - Keil移植学习笔记_第17张图片

         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函数可能加入了其他代码的初始化导致栈空间不足,可根据情况调大栈空间。

四、线程的创建

        线程的相关操作包括创建/初始化线程、启动线程、删除/脱离线程。

RT-Thread Nano - Keil移植学习笔记_第18张图片

         其中线程的创建方式分为静态创建和动态创建两种方式。其主要区别在于创建的线程其大小是否为提前指定,或者为系统动态提供。

        静态创建方式如下,其中“ 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);
    }
}

RT-Thread Nano - Keil移植学习笔记_第19张图片

 五、finsh组件的移植

        finsh组件的官方介绍如下。

RT-Thread Nano - Keil移植学习笔记_第20张图片

        可以理解成用户在使用finsh组件后,可直接通过串口的命令形式,来访问线程的使用情况,以此来更好的调试线程程序。

        finsh组件开启需要通过“rt_config.h”使能其配置。

         使能完成后,编译会发现出现了很多error的报错,接下来需要添加finsh组件相关的文件进入工程。相关的文件默认路径如下:(该文件夹在第一步安装RTT-Nano的pack包后,就自动添加到MDK安装的盘符中)

RT-Thread Nano - Keil移植学习笔记_第21张图片

         将“components”整个文件夹复制到我们的工程中,依次将“finsh”文件内的.c文件添加入工程,添加路径后,工程目录如下:

RT-Thread Nano - Keil移植学习笔记_第22张图片

         此时编译报错如下。

RT-Thread Nano - Keil移植学习笔记_第23张图片

         针对报错信息,移植步骤如下:

        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 >”

RT-Thread Nano - Keil移植学习笔记_第24张图片

        用户可通过发送help,查看finsh组件所支持的命令。

RT-Thread Nano - Keil移植学习笔记_第25张图片

        注意:由于上述移植“rt_hw_console_output()”函数时,对串口的标志位有严格要求,我本人试出标志位在使用USART_FLAG_TXE时才正常,若两个标志位都是USART_FLAG_TC,则有可能会出现发送字节丢失的可能。

RT-Thread Nano - Keil移植学习笔记_第26张图片

RT-Thread Nano - Keil移植学习笔记_第27张图片

         正常执行的情况如下:

RT-Thread Nano - Keil移植学习笔记_第28张图片

        最后,finsh组件除了支持内置命令,还支持自定义命令,具体使用方法这里就不一一演示了,具体可查看RTT官方文档,文档路径:RT-Thread 文档中心

你可能感兴趣的:(stm32,单片机,mcu,嵌入式实时数据库,arm)