赞
踩
STM32:STM32F103C8T6
RT-Thread:RT-Thread Nano 3.15
STM32CubeIDE:v1.6.0
RT-Thread Nano
RT-Thread Nano 是一个极简版的硬实时内核,它是由 C 语言开发,采用面向对象的编程思维,具有良好的代码风格,是一款可裁剪的、抢占式实时多任务的 RTOS。其内存资源占用极小,功能包括任务处理、软件定时器、信号量、邮箱和实时调度等相对完整的实时操作系统特性。适用于家电、消费电子、医疗设备、工控等领域大量使用的 32 位 ARM 入门级 MCU 的场合。
RT-Thread Nano 以软件包的方式集成在 Keil MDK 与 CubeMX 中,可以直接在软件中下载 Nano 软件包获取源码。
与 RT-Thread 完整版不同的是,Nano 不含 Scons 构建系统,不需要 Kconfig 以及 Env 配置工具,也去除了完整版特有的 device 框架和组件,仅是一个纯净的内核。
由于 Nano 的极简特性,使 Nano 的移植过程变得极为简单。添加 Nano 源码到工程,就已完成 90% 的移植工作。在 Keil MDK 与 Cube MX 中还提供了 Nano 的软件包,可以一键下载加入到工程。另外,在 RT-Thread Studio 中可以基于 Nano 创建工程直接使用。
CubeMax下的Nano移植比较多,但我习惯在STM32CubeIDE下编程,但存在一个问题,那就是通过软件包的方式移植rtthread Nano后,修改过rtthread相关的配置文件后再配置ioc文件,rtthread相关的配置又会初始化,所以本例不以软件包的形式移植rtthread Nano。
本例使用外部高速时钟(HSE),并配置好时钟树。
SYS Timebase Source 是STM32的HAL库中的新增部分,主要用于实现 HAL_Delay() 以及作为各种 timeout 的时钟基准。
在使用了OS(操作系统)之后,OS的运行也需要一个时钟基准(简称“时基”),来对任务和时间等进行管理。而OS的这个时基一般也都是通过 SysTick(滴答定时器) 来维护的,这时就需要考虑 “HAL的时基” 和 “OS的时基” 是否要共用 SysTick(滴答定时器) 了。
由于共用时基可能出现未知问题,加上STM32有多个时钟源资源,故选用TIM3作为HAL时基。
选用TIM3作为HAL时基后,会在工程下生成一个stm32f1xx_hal_timebase_tim.c文件
该文件下的HAL_InitTick会覆盖掉库文件里的弱函数HAL_InitTick,从而使得TIM3替代掉原来的SysTick
/**
* @brief This function configures the TIM3 as a time base source.
* The time source is configured to have 1ms time base with a dedicated
* Tick interrupt priority.
* @note This function is called automatically at the beginning of program after
* reset by HAL_Init() or at any time when clock is configured, by HAL_RCC_ClockConfig().
* @param TickPriority: Tick interrupt priority.
* @retval HAL status
*/
HAL_StatusTypeDef HAL_InitTick(uint32_t TickPriority)
选用usart1作为rtt的console 输出口
中断与异常处理
RT-Thread 操作系统重定义 HardFault_Handler、PendSV_Handler、SysTick_Handler 中断函数,为了避免重复定义的问题,在生成工程之前,需要在中断配置中,代码生成的选项中,取消选择三个中断函数(对应注释选项是 Hard fault interrupt, Pendable request, Time base :System tick timer),最后点击生成代码。
https://github.com/RT-Thread/rtthread-nano
#include <rthw.h>
#include <rtthread.h>
#include "main.h"
#include "usart.h"
#include "gpio.h"
#if defined(RT_USING_USER_MAIN) && defined(RT_USING_HEAP)
/*
* Please modify RT_HEAP_SIZE if you enable RT_USING_HEAP
* the RT_HEAP_SIZE max value = (sram size - ZI size), 1024 means 1024 bytes
*/
#define RT_HEAP_SIZE (10*1024)
static rt_uint8_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;
}
#endif
void SysTick_Handler(void)
{
rt_interrupt_enter();
rt_tick_increase();
rt_interrupt_leave();
}
/**
* This function will initial your board.
*/
void rt_hw_board_init(void)
{
extern void SystemClock_Config(void);
//第一部分:系统初始化,系统时钟配置等
//系统时钟是给各个硬件模块提供工作时钟的基础,在 rt_hw_board_init() 函数中完成,可以调用库函数实现配置,也可以自行实现
HAL_Init(); // 一些系统层初始化,若需要则增加此部分
SystemClock_Config(); // 配置系统时钟
SystemCoreClockUpdate(); // 更新系统时钟频率 SystemCoreClock
//第二部分:配置 OS Tick的频率,实现 OS节拍(并在中断服务例程中实现 OS Tick 递增)
HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq() / RT_TICK_PER_SECOND);
//第三部分:初始化硬件外设,若有需要,则放在此处调用 ,注意,uart_init() 或者其他的外设初始化函数,若已经使用了宏 INIT_BOARD_EXPORT() 进行初始化,则不需要在此进行显式调用。两种初始化方法选择一种即可。
MX_GPIO_Init();
MX_USART1_UART_Init();
/* 第四部分:系统动态内存堆初始化 */
#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() 进行的初始化 Call components board initial (use INIT_BOARD_EXPORT()) */
#ifdef RT_USING_COMPONENTS_INIT
rt_components_board_init();
#endif
/* 第六部分:其他初始化 */
}
#ifdef RT_USING_CONSOLE
void rt_hw_console_output(const char *str)
{
rt_size_t i = 0, size = 0;
char a = '\r';
__HAL_UNLOCK(&huart1);
size = rt_strlen(str);
for (i = 0; i < size; i++)
{
if (*(str + i) == '\n')
{
HAL_UART_Transmit(&huart1, (uint8_t *)&a, 1, 1);
}
HAL_UART_Transmit(&huart1, (uint8_t *)(str + i), 1, 1);
}
}
#endif
#ifdef RT_USING_FINSH
char rt_hw_console_getchar(void)
{
/* Note: the initial value of ch must < 0 */
int ch = -1;
if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXNE) != RESET)
{
ch = huart1.Instance->DR & 0xff;
}
else
{
if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_ORE) != RESET)
{
__HAL_UART_CLEAR_OREFLAG(&huart1);
}
rt_thread_mdelay(10);
}
return ch;
}
#endif
需要在 board.c 中实现 系统时钟配置(为 MCU、外设提供工作时钟)与 OS Tick 的配置 (为操作系统提供心跳 / 节拍)。
默认的写法如下:
static UART_HandleTypeDef UartHandle;
static int uart_init(void)
{
/* TODO: Please modify the UART port number according to your needs */
UartHandle.Instance = USART2;
UartHandle.Init.BaudRate = 115200;
UartHandle.Init.WordLength = UART_WORDLENGTH_8B;
UartHandle.Init.StopBits = UART_STOPBITS_1;
UartHandle.Init.Parity = UART_PARITY_NONE;
UartHandle.Init.Mode = UART_MODE_TX_RX;
UartHandle.Init.HwFlowCtl = UART_HWCONTROL_NONE;
UartHandle.Init.OverSampling = UART_OVERSAMPLING_16;
if (HAL_UART_Init(&UartHandle) != HAL_OK)
{
while (1);
}
return 0;
}
INIT_BOARD_EXPORT(uart_init);
串口的初始化有两种,官方默认使用宏 INIT_BOARD_EXPORT() 进行自动初始化,自己改下串口号也可以用。
也可以使用显式调用,需要在 board.c 中的 rt_hw_board_init() 函数中调用uart_init() 。
由于CubeIDE已自动生成了串口的初始化函数,所以本例采用的是直接自己初始化,不去写uart_init() 函数,个人觉得更简单易读。
MX_USART1_UART_Init();
rt_hw_console_output() 是rrt的 rt_kprintf() 函数会调用到的,实现控制台字符输出。
#ifdef RT_USING_CONSOLE
void rt_hw_console_output(const char *str)
{
rt_size_t i = 0, size = 0;
char a = '\r';
__HAL_UNLOCK(&huart1);
size = rt_strlen(str);
for (i = 0; i < size; i++)
{
if (*(str + i) == '\n')
{
HAL_UART_Transmit(&huart1, (uint8_t *)&a, 1, 1);
}
HAL_UART_Transmit(&huart1, (uint8_t *)(str + i), 1, 1);
}
}
#endif
开启 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_HEAP 后,系统默认使用数组作为 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
....
}
系统的运行应该是先去启动rtos,然后再在os里去执行生成的main函数,所以涉及到这个设置;
芯片在 KEIL MDK 与 IAR 下的启动文件一般不用做修改,会自动转到 RT-Thread 系统启动函数 rtthread_startup() 。
而GCC 下的启动文件则需要修改,让其跳转到 RT-Thread 提供的 entry() 函数。
修改 startup_stm32f103c8tx.s文件,将main改为entry。
/* Call the application's entry point.*/
bl main
bx lr
/* Call the application's entry point.*/
bl entry
bx lr
包含 rtthread.h
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "rtthread.h"
编写main
int main(void)
{
/* USER CODE BEGIN 1 */
#ifndef RT_USING_USER_MAIN
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
#endif
MX_RT_Thread_Init();
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
rt_kprintf("main_test\r\n");
rt_thread_mdelay(500);
}
/* USER CODE END 3 */
}
注意: 由于相关的初始化工作已在board文件中初始化完毕,故在此处使用RT_USING_USER_MAIN宏来取消这部分的代码的编译,该宏默认开启,后续即使对图形化配置更新后,也不会覆盖掉此设定。
建立文件app_rt_thread.c
#include "rtthread.h"
#include "main.h"
#include "stdio.h"
/* 定义线程控制块 */
void MX_RT_Thread_Init(void);
//添加LED闪烁线程
static struct rt_thread led_thread;
static char led_thread_stack[256];
static void led_thread_entry(void *parameter);
void MX_RT_Thread_Init(void)
{
//初始化线程
rt_err_t rst;
rst = rt_thread_init(&led_thread,
(const char *)"ledshine", /* 线程名字 */
led_thread_entry, /* 线程入口函数 */
RT_NULL, /* 线程入口函数参数 */
&led_thread_stack[0],
sizeof(led_thread_stack), /* 线程栈大小 */
RT_THREAD_PRIORITY_MAX-2, /* 线程的优先级 */
20); /* 线程时间片 */
if(rst == RT_EOK)
{///* 启动线程,开启调度 */
rt_thread_startup(&led_thread);
}
}
static void led_thread_entry(void *parameter)
{
while(1)
{
rt_kprintf("led_test\r\n");
rt_thread_mdelay(500);
}
}
//MSH_CMD_EXPORT(led_thread_entry,run_led_thread);
在main函数中while之前初始化
int main(void)
{
......
MX_RT_Thread_Init();
while (1)
{
rt_kprintf("main_test\r\n");
rt_thread_mdelay(500);
}
}
编译下载代码,打开串口助手,可以在串口助手看到交替打印2个线程的rt_kprintf信息;
RT-Thread FinSH 是 RT-Thread 的命令行组件(shell),提供一套供用户在命令行调用的操作接口,主要用于调试或查看系统信息。它可以使用串口 / 以太网 / USB 等与 PC 机进行通信,使用 FinSH 组件基本命令的效果图如下所示:
在 RT-Thread Nano 上添加 FinSH 组件,以串口 UART 作为 FinSH 的输入输出端口与 PC 进行通信,实现 FinSH 功能的步骤主要如下:
把finsh的源码拷贝到components下。
然后在 rtconfig.h 中打开 finsh 相关选项
#include "finsh_config.h"
在 finsh_port.c 中去掉2个error
#include <rthw.h>
#include <rtconfig.h>
#ifndef RT_USING_FINSH
//#error Please uncomment the line <#include "finsh_config.h"> in the rtconfig.h
#endif
#ifdef RT_USING_FINSH
RT_WEAK 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'."
return ch;
}
#endif /* RT_USING_FINSH */
确保之前的 board.c 中rt_hw_console_getchar函数已设置
要实现 FinSH 组件功能:既可以打印也能输入命令进行调试,控制台已经实现了打印功能,现在还需要在 board.c 中对接控制台输入函数,实现字符输入。
#ifdef RT_USING_FINSH
char rt_hw_console_getchar(void)
{
/* Note: the initial value of ch must < 0 */
int ch = -1;
if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXNE) != RESET)
{
ch = huart1.Instance->DR & 0xff;
}
else
{
if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_ORE) != RESET)
{
__HAL_UART_CLEAR_OREFLAG(&huart1);
}
//rt_thread_mdelay(10);
}
return ch;
}
#endif
由于finsh也是作为线程,在系统初始化时自动初始化,但CubeIDE出现过未初始化成功的情况,后发现需要在STM32F103C8TX_FLASH.ld 链接脚本下添加如下内容:
/* section information for finsh shell */
. = ALIGN(4);
__fsymtab_start = .;
KEEP(*(FSymTab))
__fsymtab_end = .;
. = ALIGN(4);
__vsymtab_start = .;
KEEP(*(VSymTab))
__vsymtab_end = .;
. = ALIGN(4);
/* section information for initial. */
. = ALIGN(4);
__rt_init_start = .;
KEEP(*(SORT(.rti_fn*)))
__rt_init_end = .;
位置如下:
/* The program code and other data into "FLASH" Rom type memory */
.text :
{
. = ALIGN(4);
*(.text) /* .text sections (code) */
*(.text*) /* .text* sections (code) */
*(.glue_7) /* glue arm to thumb code */
*(.glue_7t) /* glue thumb to arm code */
*(.eh_frame)
KEEP (*(.init))
KEEP (*(.fini))
. = ALIGN(4);
_etext = .; /* define a global symbols at end of code */
/* section information for finsh shell */
. = ALIGN(4);
__fsymtab_start = .;
KEEP(*(FSymTab))
__fsymtab_end = .;
. = ALIGN(4);
__vsymtab_start = .;
KEEP(*(VSymTab))
__vsymtab_end = .;
. = ALIGN(4);
/* section information for initial. */
. = ALIGN(4);
__rt_init_start = .;
KEEP(*(SORT(.rti_fn*)))
__rt_init_end = .;
} >FLASH
由于既存在着打印信息又需要输入命令,这里先把main下列代码注释掉
MX_RT_Thread_Init();
rt_kprintf("main_test\r\n");
将led_thread线程添加至终端,去掉app_rt_thread.c文件MSH_CMD_EXPORT的注释:
int main(void)
{
//MX_RT_Thread_Init();
while (1)
{
//rt_kprintf("main_test\r\n");
rt_thread_mdelay(500);
}
}
#include "rtthread.h"
#include "main.h"
#include "stdio.h"
/* 定义线程控制块 */
void MX_RT_Thread_Init(void);
//添加LED闪烁线程
static struct rt_thread led_thread;
static char led_thread_stack[256];
static void led_thread_entry(void *parameter);
void MX_RT_Thread_Init(void)
{
//初始化线程
rt_err_t rst;
rst = rt_thread_init(&led_thread,
(const char *)"ledshine", /* 线程名字 */
led_thread_entry, /* 线程入口函数 */
RT_NULL, /* 线程入口函数参数 */
&led_thread_stack[0],
sizeof(led_thread_stack), /* 线程栈大小 */
RT_THREAD_PRIORITY_MAX-2, /* 线程的优先级 */
20); /* 线程时间片 */
if(rst == RT_EOK)
{///* 启动线程,开启调度 */
rt_thread_startup(&led_thread);
}
}
static void led_thread_entry(void *parameter)
{
while(1)
{
rt_kprintf("led_test\r\n");
rt_thread_mdelay(500);
}
}
MSH_CMD_EXPORT(led_thread_entry,run_led_thread);
编译下载代码,打开串口助手,可以在串口助手中打印输入 help 命令,回车查看系统支持的命令。
可以发现led_thread_entry函数已经可以作为命令了,可通过终端执行。
低优先级的线程不运行,可能是使用Finsh造成的;
查看shell.c文件可发现 finsh_thread_entry中有while(1)循环,其中的finsh_getchar 会调用到rt_hw_console_getchar函数。finsh需要适当的让出CPU资源。
void finsh_thread_entry(void *parameter)
{
....
while (1)
{
ch = finsh_getchar();
if (ch < 0)
{
continue;
}
....
方法1:把rt_hw_console_getchar函数里的rt_thread_mdelay(10)加上,但这有可能造成终端输入命令时不连续。
#ifdef RT_USING_FINSH
char rt_hw_console_getchar(void)
{
/* Note: the initial value of ch must < 0 */
int ch = -1;
if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXNE) != RESET)
{
ch = huart1.Instance->DR & 0xff;
}
else
{
if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_ORE) != RESET)
{
__HAL_UART_CLEAR_OREFLAG(&huart1);
}
rt_thread_mdelay(10);//在未获取到字符时,需要让出 CPU
}
return ch;
}
#endif
方法2:rt_hw_console_getchar函数采用中断得方式实现。原理是,在 uart 接收到数据时产生中断,在中断中把数据存入 ringbuffer 缓冲区,然后释放信号量,tshell 线程接收信号量,然后读取存在 ringbuffer 中的数据。这部分官方有说明。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。