目录
6.board.c里面没有进行串口引脚初始化,还是无法使用串口
7.移植后发现第使用keil5发现没有芯片并且下载芯片时Install+是灰色
我们要实现移植RT-Thread,并且用led和串口证明移植成功。
一. 准备工作
- 下载 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
,如下图所示:
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 主界面。
2.创建基础工程
在 CubeMX 主界面的菜单栏中 File
选择 New Project
,如下图所示
新建工程之后,在弹出界面芯片型号中输入某一芯片型号,方便锁定查找需要的芯片,双击被选中的芯片,如下图所示
时钟树的配置直接使用默认即可,然后还需要配置下载方式。
二.添加RT-Thread Nano到工程
1.选择Nano组件
选中芯片型号之后,点击 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 组件的代码,FinSH
的移植详见 《在 RT-Thread Nano 上添加控制台与 FinSH》。再选择
device 表示使用 rt-thread 的 device 框架,用户基于此框架编写
外设驱动并注册后,就可以使用 device 统一接口操作外设。
2.配置Nano
选择组件之后,对组件参数进行配置。在工程界面 Pinout & Configuration
中,进入所选组件参数配置区,按照下图进行配置
3. 配置MCU()
可以按照自己需求来配置,下面是我对MCU的配置
(1)配置RCC与Debug
进入System Core-SYS设置,选择Debug为Serial wire。其他参数默认
进入System Core-RCC设置,分别配置HSE为Crystal/Ceramic Resonator(晶体/陶瓷谐振器)。其他参数默认
(2)选择GPIO引脚与功能
在右侧的单片机上选择连接有LED灯,比如我的单片机系统的PA0连接了LED灯
配置输出引脚。
在连接了LED灯的引脚上单击左键,选择Output功能。
在PA0上右键,选择Enter User Label,键入别名,输入LED。。
(3)配置GPIO
进入System Core-GPIO设置,上方选择GPIO引脚设置。
配置输出引脚PA0.。选中上方的PA0。
1. 引脚上电时的默认状态。(高电平/低电平) 因我的LED灯的阴极连接的单片机引脚,所以选择高电平,表示默认熄灭。
2. 引脚模式。(推挽输出/开漏输出) 这里选择推挽输出。
3. 开启引脚外部上拉或下拉。(浮空/上拉/下拉) 这里选择上拉。
4. 引脚输出速度。(低/中/高/很高) 默认即可。
5. 引脚别名。 这里之前选择GPIO时已经配置过就不用在配置。
(4)时钟配置
进入时钟配置界面。根据单片机系统中采用的晶振频率设置HSE,我的单片机系统采用的8M晶振。这里必须使能System Core-RCC中的HSE才可以设置。刚才我们已经配置过了。
配置系统主频,时钟源选择HSE,系统主频选择PLLCLK,再在HCLK框中输入系统推荐的主频,点击回车,软件即可自动配置各个分频器的值。(对于我的stm32f407vet6的时钟配置)
4.工程管理
给工程取名、选择代码存放位置、选择生成代码的 Toolchain/IDE
。Cube MX
不仅能够生成 Keil4/Keil5
的工程,而且还能够生成 IAR7/IAR8
等 IDE 的工程,功能强大,本文从下拉框中选择 MDK5,操作如图所示
左侧选择Code Generator设置,选择仅复制需要的库文件,勾选外设初始化生成独立的.c/.h文件。这样生成的工程文件比较小并且后期容易修改。
三.适配RT-Thread Nano
1.终端与异常处理
RT-Thread 操作系统重定义 HardFault_Handler
、PendSV_Handler
、SysTick_Handler
中断函数,为了避免重复定义的问题,在生成工程之前,需要在中断配置中,代码生成的选项中,取消选择三个中断函数(对应注释选项是 Hard fault interrupt
, Pendable request
, Time base :System tick timer
),最后点击生成代码,具体操作如下图 所示:
等待工程生成完毕,点击打开工程,如下图所示,即可进入 MDK5 工程中。
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_malloc、rt_free 以及各种系统动态创建对象的 API。若需要使用系统内存堆功能,则打开 RT_USING_HEAP 宏定义即可,此时内存堆初始化函数 rt_system_heap_init() 将被调用,如下所示:
初始化内存堆需要堆的起始地址与结束地址这两个参数,系统中默认使用数组作为 heap,并获取了 heap 的起始地址与结束地址,该数组大小可手动更改,如下所示:
注意:开启 heap 动态内存功能后,heap 默认值较小,在使用的时候需要改大,否则可能会有申请内存失败或者创建线程失败的情况,修改方法有以下两种:
- 可以直接修改数组中定义的 RT_HEAP_SIZE 的大小,至少大于各个动态申请内存大小之和,但要小于芯片 RAM 总大小。
- 也可以参考《RT-Thread Nano 移植原理》——实现动态内存堆 章节进行修改,使用 RAM ZI 段结尾处作为 HEAP 的起始地址,使用 RAM 的结尾地址作为 HEAP 的结尾地址,这是 heap 能设置的最大值的方法。
四.编写一个应用
移植好 RT-Thread Nano 之后,则可以开始编写第一个应用代码。此时 main() 函数就转变成 RT-Thread 操作系统的一个线程,现在可以在 main() 函数中实现第一个应用:LED 指示灯闪烁。
- 首先在文件首部包含 RT-Thread 的相关头文件
<rtthread.h>
。 - 在 main() 函数中(也就是在 main 线程中)写 LED 闪烁代码:初始化 LED 引脚、在循环中点亮 / 熄灭 LED。
- 延时函数使用 RT-Thread 提供的延时函数 rt_thread_mdelay(),该函数会引起系统调度,切换到其他线程运行,体现了线程实时性的特点。
编译程序之后下载到芯片就可以看到基于 RT-Thread 的程序运行起来了,LED 正常闪烁。
注:当添加 RT-Thread 之后,裸机中的 main() 函数会自动变成 RT-Thread 系统中 main 线程 的入口函数。由于线程不能一直独占 CPU,所以此时在 main() 中使用 while(1) 时,需要有让出 CPU 的动作,比如使用 rt_thread_mdelay() 系列的函数让出 CPU。
与裸机 LED 闪烁应用代码的不同:
1). 延时函数不同: RT-Thread 提供的 rt_thread_mdelay()
函数可以引起操作系统进行调度,当调用该函数进行延时时,本线程将不占用 CPU,调度器切换到系统的其他线程开始运行。而裸机的 delay 函数是一直占用 CPU 运行的。
2). 初始化系统时钟的位置不同:移植好 RT-Thread Nano 之后,不需要再在 main() 中做相应的系统配置(如 hal 初始化、时钟初始化等),这是因为 RT-Thread 在系统启动时,已经做好了系统时钟初始化等的配置。
五.出现过的问题以及解决办法
1.cubemx没有生成.s文件
cubemx生成的工程一定不能在中文路径下,并且工程文件夹名不能是中文,需要自己把.s添加进去
2.有的地方得注释(原因不清楚)
3.又有地方得注释(原因不清楚)
4.systemclock Config报错
extern void SystemClock_Config(void)放里面不行,得放到外面才可以(在board.c中)
5.生成的board.c中串口初始化报错
因为我们之前勾选外设初始化生成独立的.c/.h文件,所以里面是没有串口的库函数的,我们得自己添加串口的.c.h文件,并在stm32f4xx_hal_cont.h中引用uart库函数的头文件。
6.board.c里面没有进行串口引脚初始化,还是无法使用串口
得自己写串口的引脚初始化,以串口1为例.HAL_UART_MspInit被HAL_UART_Init自动调用。
void HAL_UART_MspInit(UART_HandleTypeDef* huart)
{
GPIO_InitTypeDef GPIO_Initure;
if(huart->Instance==USART1)//如果是串口1,进行串口1 MSP初始化
{
__HAL_RCC_GPIOA_CLK_ENABLE(); //使能GPIOA时钟
__HAL_RCC_USART1_CLK_ENABLE(); //使能USART1时钟
GPIO_Initure.Pin=GPIO_PIN_9; //PA9
GPIO_Initure.Mode=GPIO_MODE_AF_PP; //复用推挽输出
GPIO_Initure.Pull=GPIO_PULLUP; //上拉
GPIO_Initure.Speed=GPIO_SPEED_FAST; //高速
GPIO_Initure.Alternate=GPIO_AF7_USART1; //复用为USART1
HAL_GPIO_Init(GPIOA,&GPIO_Initure); //初始化PA9
GPIO_Initure.Pin=GPIO_PIN_10; //PA10
HAL_GPIO_Init(GPIOA,&GPIO_Initure); //初始化PA10
#if EN_USART1_RX
HAL_NVIC_EnableIRQ(USART1_IRQn); //使能USART1中断通道
HAL_NVIC_SetPriority(USART1_IRQn,3,3); //抢占优先级3,子优先级3
#endif
}
}
7.移植后发现第使用keil5发现没有芯片并且下载芯片时Install+是灰色
解决办法
1.点击如图所示
2.选择你需要的芯片,点击右边的install
8.board.c文件中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);
//API:进入临界区,退出前系统不会发生任务调度
rt_enter_critical();
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);
}
//API:退出临界区
rt_exit_critical();
}
#endif
六.结果
串口打印
七.RT-Thread程序执行流程分析
1.RT-Thread 入口
我们可以在 components.c 文件的 131 行看到#ifdef RT_USING_USER_MAIN 宏定义判断,这个宏是定义在 rtconfig.h 文件内的,而且处于开启状态。同时 我们可以在 137 行看到#if defined (__CC_ARM)的宏定义判断,__CC_ARM 就是 指 keil 的交叉编译器名称。 我们可以在这里看到定义了 2 个函数:$Sub$$main()和$Super$$main()函数; 这里通过$Sub$$main()函数在程序就如主程序之前插入一个例程,实现在不改变 源代码的情况下扩展函数功能。链接器通过调用$Sub$$Main()函数取代 main(), 然后通过$Super$$main 再次回到 main()
#if defined(__CC_ARM) || defined(__CLANG_ARM)
extern int $Super$$main(void);
/* re-define main function */
int $Sub$$main(void)
{
rtthread_startup();
return 0;
}
在这里,$Sub$$main 函数仅调用了 rtthread_startup() 函数。RT-Thread支持 多种平台和多种编译器,而 rtthread_startup() 函数是 RT-Thread 规定的统一入 口点,所以 $Sub$$main 函数只需调用 rtthread_startup() 函数即可。例如采用 GNU GCC 编译器编译的 RT-Thread,就是直接从汇编启动代码部分跳转到 rtthread_startup() 函数中,并开始第一个 C 代码的执行的。在 components.c的 代码中找到 rtthread_startup() 函数,我们将可以看到 RT-Thread 的启动流程
int rtthread_startup(void)
{
rt_hw_interrupt_disable();
/* board level initialization
* NOTE: please initialize heap inside board initialization.
*/
rt_hw_board_init();
/* show RT-Thread version */
rt_show_version();
/* timer system initialization */
rt_system_timer_init();
/* scheduler system initialization */
rt_system_scheduler_init();
/* create init_thread */
rt_application_init();
/* timer thread initialization */
rt_system_timer_thread_init();
/* idle thread initialization */
rt_thread_idle_init();
/* start scheduler */
rt_system_scheduler_start();
/* never reach here */
return 0;
}
这部分启动代码,大致可以分为四个部分: 初始化与系统相关的硬件; 初始化系统内核对象,例如定时器,调度器; 初始化系统设备,这个主要是为 RT-Thread 的设备框架做的初始化; 初始化各个应用线程,并启动调度器。 rt_hw_board_init():该函数定义在 board.c 文件内,需要修改 systick 配置 rt_system_timer_init()/rt_system_timer_thread_init():timer 初始化/启 动 rt_thread_idle_init():idle 任务创建 rt_application_init():应用线程初始化 rt_system_scheduler_start():调度器启动
2.应用线程入口
void rt_application_init(void)
{
rt_thread_t tid;
#ifdef RT_USING_HEAP
tid = rt_thread_create("main", main_thread_entry, RT_NULL,
RT_MAIN_THREAD_STACK_SIZE, RT_MAIN_THREAD_PRIORITY,
20);
RT_ASSERT(tid != RT_NULL);
#else
rt_err_t result;
tid = &main_thread;
result = rt_thread_init(tid, "main", main_thread_entry, RT_NULL,
main_stack, sizeof(main_stack), RT_MAIN_THREAD_PRIORITY,
20);
RT_ASSERT(result == RT_EOK);
/* if not define RT_USING_HEAP, using to eliminate the warning */
(void)result;
#endif
rt_thread_startup(tid);
}
在这里,我们可以看到应用线程创建了一个名为 main_thread_entry 的任务, 并且已经启动了该任务。我们再次来看一下 main_thread_entry 任务。
/* the system main thread */
void main_thread_entry(void *parameter)
{
extern int main(void);
extern int $Super$$main(void);
#ifdef RT_USING_COMPONENTS_INIT
/* RT-Thread components initialization */
rt_components_init();
#endif
/* invoke system main function */
#if defined(__CC_ARM) || defined(__CLANG_ARM)
$Super$$main(); /* for ARMCC. */
#elif defined(__ICCARM__) || defined(__GNUC__)
main();
#endif
}
main_thread_entry 任务完成了 2 个工作:调用 rt_components_init()、进入 应用代码真正的 main 函数。 在这里我们看到了$$Super$$main()的调用,在前 面我们讲了调用该函数可用来回到 main()的。
从以上分析可以,正是由于在 rtconfig.h 内开启了 RT_USING_USER_MAIN 选 项,编译器在 main 之前插入了$$Sub$$main(),完成了 RT-Thread 初始化及调 度器启动工作。并且通过创建 main_thread_entry 任务,并通过$$Super$$main() 回到 main()函数。这样看来 main()函数其实只是 RT-Thread 的一个任务,该 任务的优先级为 RT_THREAD_PRIORITY_MAX / 3,任务栈为 RT_MAIN_THREAD_STACK_SIZE。
七.参考链接:
2.STM32CubeMX系列教程1:GPIO输入与输出_cube中读取gpio口上拉输入-CSDN博客
3.gitee工程链接: