RT-Thread启动过程 :从汇编开始的启动流程
这个系列参考了《嵌入式实时操作系统RT-Thread设计与实现》,会详细介绍RT-Thread的启动流程,即是如何从零开始在开发板上运行起一个RTOS内核的。本文将会以 ch32v307VCT6 开发板为例展开进行详细介绍。主要包括:startup.S、初始化与系统相关的硬件、初始化系统内核对象(例如定时器、调度器、信号)、创建main线程、初始化线程并启动调度器 这五大部分。
在这一小节中,本文将讲述通过wsl烧录RT-Thread的基本流程。并在最后讲解startup_ch32v30x.S文件的执行过程。
一、系统运行环境
本文在Linux系统上通过命令行的方式实现RT-Thread的下载与调试。这有几个准备步骤:
- RT-Thread的源码获取, 在linux命令行中输入如下命令:
git clone https://gitee.com/rtthread/rt-thread.git- 交叉编译工具链的下载:MRS_Toolchain_Linux_x64_V1.92.1.tar.xz 下载地址。下载好后在linux环境中解压。记录好下载路径。
- Scons工具下载: 在Linux命令行中输入以下命令:
sudo apt install scons
准备完毕后,切换目录到:./RT_thread/rt-thread/bsp/wch/risc-v/ch32v307v-r1 下运行(下文称其为根目录):
scons --exec-path=../../../../../Toolchain_linux/MRS_Toolchain_Linux_x64_V1.92.1/RISC-V_Embedded_GCC/bin
其中--exec-path=后接下载的交叉编译工具链的路径。运行成功如下所示:

至此在当前目录下会生成rtthread.bin 和 rtthread.elf两个文件。前期的准备工作就到此结束。
二、烧录至开发板
本文使用wsl,安装教程:WSL2 最新最全帮助小白一步步详细安装教程。
由于wsl本身不支持usb连接,因此需要把插入电脑的usb(Windows下)接入到wsl(Linux下)中,所以还需要安装usbipd-win开源项目:usbipd-win安装。想要便捷操作的可以额外下载图形化界面wsl usb gui:wsl usb gui 下载。
下载完成,并且usb成功接入到wsl后,为了方便操作一般都会有一个Makefile执行相关命令,其中的相对路径均从之前下载的交叉编译工具链给出:
CROSS_COMPILE := ../../../../../Toolchain_linux/MRS_Toolchain_Linux_x64_V1.92.1/RISC-V_Embedded_GCC/bin/riscv-none-embed-
OPENOCD_DIR = ../../../../../Toolchain_linux/MRS_Toolchain_Linux_x64_V1.92.1/OpenOCD/bin
OPENOCD = ${OPENOCD_DIR}/openocd
CFG = ${OPENOCD_DIR}/wch-riscv.cfg
GDB := ${CROSS_COMPILE}gdb
OBJDUMP := ${CROSS_COMPILE}objdump
.DEFAULT_GOAL := allall:scons --exec-path=../../../../../Toolchain_linux/MRS_Toolchain_Linux_x64_V1.92.1/RISC-V_Embedded_GCC/bin.PHONY : flash
flash: @echo "------------------------"@echo "Flashing os.bin to board"@echo "------------------------"@${OPENOCD} -f ${CFG} -c init -c halt -c "program rtthread.bin" -c exit.PHONY : run_openocd
run_openocd: @echo "-----------------------------------------------"@echo "Please manually kill the openocd after gbd quit"@echo "-----------------------------------------------"@${OPENOCD} -f ${CFG} &.PHONY : run_gdb
run_gdb:@${GDB} rtthread.elf -q -ex "target remote : 3333".PHONY : code
code: @${OBJDUMP} -d rtthread.elf > rtthread.asm.PHONY : clean
clean:rm -rf *.o *.bin *.elf
把以上Makefile文件放在根目录下,并执行sudo make flash,即可成功烧录:

使用picocom串口工具观察现象,picocom工具直接命令行输入sudo apt install picocom下载。在这里波特率为115200,默认8个数据位,1个停止位,不启用校验位。 /dev/ttyACM0为Linux系统所识别的新插入的开发板USB设备文件。按下ctrl+A ctrl+X退出。
当出现如下图所示的 RT 字样即说明成功启动RT-Thread。

三、startup_ch32v30x.S
现在本文将从汇编文件开始,讲述RT-Thread是如何一步一步启动的。startup_ch32v30x.S位置在:RT_thread/rt-thread/bsp/wch/risc-v/Libraries/ch32v30x_libraries/bmsis/source。
汇编文件中的入口地址为 _start (由链接脚本ENTRY( _start )指定)。汇编文件中第13行至332行均在设置中断向量入口地址。
第345行在设置堆栈指针:
![]()
第348行至357行在将.data段中的数据从flash搬移至ram:

第360行至366行在清零 .bss 段中的数据:

第368行至373行在写控制状态寄存器(具体作用不知):

第377行至378行在写mstatus寄存器,mstatus寄存器结构如下图,其中比较重要的位有:

MIE:全局trap(中断和异常)开关。
MPIE:trap前系统MIE位的状态,也可以理解为:执行mret时,MIE会被赋值位MPIE。
MPP:trap前系统运行的模式。也可以理解为:mret时系统会运行于哪种模式。 为11则表示系统会运行在机器模式。
FS:11表示启动浮点运算
因此这里在把MPP写为11后,执行mret就打开了全局trap(中断和异常)的开关 。

第380行至382行在设置中断向量入口基地址,其中_vector_base在此汇编文件第29行指定。

第384行至387行在执行开发板时钟初始化(SystemInit),并进入RT-Thread的初始化函数:

在这里,mepc寄存器中存入了entry函数地址,在mret时,系统会跳转到mepc寄存器存放的地址继续执行,从而实现了entry函数的跳转。
SystemInit函数位置:
RT_thread/rt-thread/bsp/wch/risc-v/Libraries/ch32v30x_libraries/bmsis/source/system_ch32v30x.c:85首先执行SystemInit,个人认为由于系统复位,不管是哪种复位形式,寄存器都会被设置为复位值,所以SystemInit中,除了SetSysClock()函数的跳转,其他操作意义不明(我太菜了)。本文使用的ch32v307VCT6属于CH32V30x_D8C类别。
除非是软件直接跳转到handel_reset执行复位逻辑。
void SystemInit (void) {RCC->CTLR |= (uint32_t)0x00000001; #ifdef CH32V30x_D8CRCC->CFGR0 &= (uint32_t)0xF8FF0000; #elseRCC->CFGR0 &= (uint32_t)0xF0FF0000; #endif RCC->CTLR &= (uint32_t)0xFEF6FFFF;RCC->CTLR &= (uint32_t)0xFFFBFFFF;RCC->CFGR0 &= (uint32_t)0xFF80FFFF;#ifdef CH32V30x_D8CRCC->CTLR &= (uint32_t)0xEBFFFFFF;RCC->INTR = 0x00FF0000;RCC->CFGR2 = 0x00000000; #elseRCC->INTR = 0x009F0000; #endif SetSysClock(); }在SetSysClock中将系统时钟设置为定义好的144MHz:
static void SetSysClock(void) { #ifdef SYSCLK_FREQ_HSESetSysClockToHSE(); #elif defined SYSCLK_FREQ_24MHzSetSysClockTo24(); #elif defined SYSCLK_FREQ_48MHzSetSysClockTo48(); #elif defined SYSCLK_FREQ_56MHzSetSysClockTo56(); #elif defined SYSCLK_FREQ_72MHzSetSysClockTo72(); #elif defined SYSCLK_FREQ_96MHzSetSysClockTo96(); #elif defined SYSCLK_FREQ_120MHzSetSysClockTo120(); #elif defined SYSCLK_FREQ_144MHz //仅144MHz有宏定义SetSysClockTo144();#endif/* If none of the define above is enabled, the HSI is used as System clock* source (default after reset) */ }在SetSysClockTo144()函数中。主要工作在 启用HSE时钟,配置PLL倍频、设置系统时钟、HCLK、PB1\PB2外设时钟。
逻辑是先启用HSE外部8MHz高速时钟,并PLL 18倍频至 144MHz 作为SYSCLOCK系统时钟。 系统时钟不分频作为HCLK的输入。 HCLK不分频作为PB2CLK输入, HCLK 2分频作为PB1CLK输入。
static void SetSysClockTo144(void) {__IO uint32_t StartUpCounter = 0, HSEStatus = 0;RCC->CTLR |= ((uint32_t)RCC_HSEON); //启用外部高速时钟/* 等待HSE时钟准备就绪 */do{HSEStatus = RCC->CTLR & RCC_HSERDY;StartUpCounter++;} while((HSEStatus == 0) && (StartUpCounter != HSE_STARTUP_TIMEOUT));/* 判断HSE是否准备就绪 */if ((RCC->CTLR & RCC_HSERDY) != RESET){HSEStatus = (uint32_t)0x01;}else{HSEStatus = (uint32_t)0x00;}/* 如果HSE准备就绪则配置PLL倍频、SYSCLK、HCLK、PB1CLK、PB2CLK */if (HSEStatus == (uint32_t)0x01){/* HCLK = SYSCLK */RCC->CFGR0 |= (uint32_t)RCC_HPRE_DIV1; //设置HCLK为SYSCLK不分频输入/* PCLK2 = HCLK */RCC->CFGR0 |= (uint32_t)RCC_PPRE2_DIV1; //设置PB2CLK为HCLK不分频输入/* PCLK1 = HCLK/2 */RCC->CFGR0 |= (uint32_t)RCC_PPRE1_DIV2; //设置PB1CLK为HCLK2分频输入/* PLL 配置: PLLCLK = HSE * 18 = 144 MHz */RCC->CFGR0 &= (uint32_t)((uint32_t)~(RCC_PLLSRC | RCC_PLLMULL));#ifdef CH32V30x_D8RCC->CFGR0 |= (uint32_t)(RCC_PLLSRC_HSE | RCC_PLLXTPRE_HSE | RCC_PLLMULL18); #elseRCC->CFGR0 |= (uint32_t)(RCC_PLLSRC_HSE | RCC_PLLMULL18_EXTEN); //配置PLL时钟源为HSE,并设置18倍频。 实际上这里配置的PLL时钟源是prediv1,只不过prediv1系统默认输入是HSE不分频输入。 #endif/* 启用 PLL */RCC->CTLR |= RCC_PLLON;/* 等待 PLL 准备就绪 */while((RCC->CTLR & RCC_PLLRDY) == 0){}/* 设置 PLL倍频时钟 作为系统时钟 */RCC->CFGR0 &= (uint32_t)((uint32_t)~(RCC_SW));RCC->CFGR0 |= (uint32_t)RCC_SW_PLL;/* Wait till PLL is used as system clock source */while ((RCC->CFGR0 & (uint32_t)RCC_SWS) != (uint32_t)0x08){}}else{/** If HSE fails to start-up, the application will have wrong clock* configuration. User can add here some code to deal with this error*/} }
至此系统从汇编文件启动,到跳转到RT-Thread的初始化入口entry函数的逻辑就讲解完毕。这一部分主要是开发板启动的常规操作(数据段搬运、bss段清零、中断向量设置),以及 系统时钟的配置。接下来将介绍entry函数中是如何对RT-Thread进行初始化的。
四、RT-Thread初始化
单核初始化过程包括:中断关闭、板级初始化、定时器初始化、调度器初始化、信号初始化、初始化创建应用main线程、初始化创建定时器线程、空闲线程初始化、僵尸线程初始化、打开中断并进行线程调度。
int rtthread_startup(void)
{
#ifdef RT_USING_SMPrt_hw_spin_lock_init(&_cpus_lock);
#endifrt_hw_local_irq_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(); //调度器初始化#ifdef RT_USING_SIGNALS/* signal system initialization */rt_system_signal_init();
#endif /* RT_USING_SIGNALS *//* create init_thread */rt_application_init(); //创建用户线程/* timer thread initialization */rt_system_timer_thread_init(); //定时器线程初始化/* idle thread initialization */ rt_thread_idle_init(); //空闲线程初始化/* defunct thread initialization */rt_thread_defunct_init(); //僵尸线程初始化#ifdef RT_USING_SMPrt_hw_spin_lock(&_cpus_lock);
#endif /* RT_USING_SMP *//* start scheduler */rt_system_scheduler_start(); //线程调度,开中断并执行main函数/* never reach here */return 0;
}
其中rt_hw_boart_init板级初始化过程需要我们根据具体的开发板进行修改。需要在其中完成系统时钟配置、为系统提供心跳、串口初始化、将系统输入输出终端绑定到这个串口。
extern uint32_t SystemCoreClock;static uint32_t _SysTick_Config(rt_uint32_t ticks)
{NVIC_SetPriority(SysTicK_IRQn, 0xf0);NVIC_SetPriority(Software_IRQn, 0xf0);NVIC_EnableIRQ(SysTicK_IRQn);NVIC_EnableIRQ(Software_IRQn);SysTick->CTLR = 0;SysTick->SR = 0;SysTick->CNT = 0;SysTick->CMP = ticks - 1;SysTick->CTLR = 0xF;return 0;
}/*** This function will initial your board.*/
void rt_hw_board_init()
{/* System Tick Configuration */_SysTick_Config(SystemCoreClock / RT_TICK_PER_SECOND);#if defined(RT_USING_USER_MAIN) && defined(RT_USING_HEAP)rt_system_heap_init((void *) HEAP_BEGIN, (void *) HEAP_END);
#endif/* USART driver initialization is open by default */
#ifdef RT_USING_SERIALrt_hw_usart_init();
#endif
#ifdef RT_USING_CONSOLErt_console_set_device(RT_CONSOLE_DEVICE_NAME);
#endif
#ifdef RT_USING_PIN/* pin must initialized before i2c */rt_hw_pin_init();
#endif/* Call components board initial (use INIT_BOARD_EXPORT()) */
#ifdef RT_USING_COMPONENTS_INITrt_components_board_init();
#endif}void SysTick_Handler(void) __attribute__((interrupt("WCH-Interrupt-fast")));
void SysTick_Handler(void)
{GET_INT_SP();/* enter interrupt */rt_interrupt_enter();SysTick->SR = 0;rt_tick_increase();/* leave interrupt */rt_interrupt_leave();FREE_INT_SP();}
相关文章:
RT-Thread启动过程 :从汇编开始的启动流程
这个系列参考了《嵌入式实时操作系统RT-Thread设计与实现》,会详细介绍RT-Thread的启动流程,即是如何从零开始在开发板上运行起一个RTOS内核的。本文将会以 ch32v307VCT6 开发板为例展开进行详细介绍。主要包括:startup.S、初始化与系统相关的…...
Scala—“==“和“equals“用法(附与Java对比)
Scala 字符串比较—""和"equals"用法 Scala 的 在 Scala 中, 是一个方法调用,实际上等价于调用 equals 方法。不仅适用于字符串,还可以用于任何类型,并且自动处理 null。 Demo: Java 的 在 J…...
$route和$router的区别
在 Vue.js 中,$route 和 $router 都是 Vue Router 提供的对象,但它们有不同的用途和功能。 1. $router $router 是 Vue Router 实例的引用,它允许你通过 JavaScript 进行路由的控制和导航。你可以通过 $router 来执行路由的操作,…...
[工具升级问题] 钉钉(linux版)升级带来的小麻烦
本文由Markdown语法编辑器编辑完成。 1. 背景: 今日钉钉又发布了新的升级版本。由于我工作时使用的是Ubuntu 20.04版本,收到的升级推送信息是,可以升级到最新的7.6.25-Release版本。根据钉钉官方给出的历次更新版说明,这个新的版本…...
Leetcode经典题13--接雨水
题目描述 给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。 输入输出示例 输入:height [0,1,0,2,1,0,1,3,2,1,2,1] 输出:6 解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1…...
yarn修改缓存位置
查看缓存位置 以下三个命令分别为:bin是yarn存储命令的二进制文件,global存储全局node_modules ,cache存储用下下载缓存,查看本机目前的目录: 查看bin目录命令:yarn global bin 查看global目录命令&…...
OpenHarmony-3.HDF input子系统(5)
HDF input 子系统OpenHarmony-4.0-Release 1.Input 概述 输入设备是用户与计算机系统进行人机交互的主要装置之一,是用户与计算机或者其他设备通信的桥梁。常见的输入设备有键盘、鼠标、游戏杆、触摸屏等。本文档将介绍基于 HDF_Input 模型的触摸屏器件 IC 为 GT91…...
RabbitMQ 消息持久化/镜像队列/lazy对时延影响
测试背景: 不同条件下RabbitMQ不同队列类型的生产时延测试: 测试环境: 机型:rabbimtq.2u4g.cluster 背景流量:1000 TPS 测试条件: 消息大小 4k,消息条数为1000条,时延取值为平均…...
【深度学习】深刻理解Swin Transformer
Swin Transformer 是一种基于 Transformer 的视觉模型,由 Microsoft 研究团队提出,旨在解决传统 Transformer 模型在计算机视觉任务中的高计算复杂度问题。其全称是 Shifted Window Transformer,通过引入分层架构和滑动窗口机制,S…...
[2015~2024]SmartMediaKit音视频直播技术演进之路
技术背景 2015年,因应急指挥项目需求,我们实现了RTMP推送音视频采集推送(采集摄像头和麦克风数据)模块,在我们做好了RTMP推送模块后,苦于没有一个满足我们毫秒级延迟诉求的RTMP播放器,于是第一…...
redis 使用Lettuce 当redis挂掉重启之后 网络是怎么重新连接
Lettuce是一个高性能的Java Redis客户端,支持同步、异步和反应式编程模式 Lettuce的核心功能包括: 高性能:通过使用Netty作为底层网络通信框架,实现了非阻塞IO,提高了性能。丰富的API:提供了丰富…...
【IntelliJ IDEA 集成工具】TalkX - AI编程助手
前言 在数字化时代,技术的迅猛发展给软件开发者带来了更多的挑战和机遇。为了提高技术开发群体在繁多项目中的编码效率和质量,他们需要一个强大而专业的工具来辅助开发过程,而正是为了满足这一需求,TalkX 应运而生。 一、概述 1…...
二叉搜索树Ⅲ【东北大学oj数据结构8-3】C++
二叉搜索树 III B:在二叉搜索树II中加入delete指令,创建程序对二叉搜索树T执行如下指令。 插入 k:将key k 插入到 T 中。 find k:报告T中是否存在key k。 delete k:删除key为 k 的节点。 打印:使用中序树遍…...
【面试笔记】CPU 缓存机制
CPU 缓存机制 1. CPU Cache 与 MMU1.1 MMU 是什么?TLB 又是什么?他们是怎么工作的?2.2 简述 Cache 与 MMU 的协作关系?2.3 简述 Cache 与 MMU 的协作工作流程? 2. CPU 多层次缓存2.1 什么是 CPU 的多层次缓存结构&…...
MySQL基础函数使用
目录 简介 1. 单行函数 1.1 字符串函数 1.2 日期函数 1.3 数值函数 1.4 转换函数 1.5 其他函数 2. 多行函数 示例: 3. 数据分组 示例: 4. DQL单表关键字执行顺序 示例: 5. 多表查询 示例: 6. 表与表的外连接 示例…...
解决docker环境下aspose-words转换word成pdf后乱码问题
描述 环境:docker 部署工具:Jenkins 需求:本地上传的word文档需要转换成pdf 问题:转换之后的pdf文档出现小框框(乱码) 转换成PDF的操作 pom: <dependency><groupId>org.apach…...
C# 生成随机数的方法
C# 提供了一种强大而方便的工具类 Random ,用于生成随机数。这里将分类讨论如何通过 C# 实现随机数生成,以及应用于实际情况中的一些具体方案。 一、Random 类概述 Random 类表示一个伪随机数生成器,用于生成满足随机性统计要求的数字序列。…...
ip_done
文章目录 路由结论 IP分片 数据链路层重谈Mac地址MAC帧报头局域网的通信原理MSS,以及MAC帧对上层的影响ARP协议 1.公司是不是这样呢? 类似的要给运营商交钱,构建公司的子网,具有公司级别的入口路由器 2.为什么要这样呢?? IP地…...
3D可视化引擎HOOPS Visualize与HOOPS Luminate Bridge的功能与应用
HOOPS Visualize HPS / HOOPS Luminate Bridge为开发者提供了强大的工具,用于在CAD应用中集成逼真的渲染能力。本文旨在梳理该桥接产品的核心功能、使用方法及应用场景,为用户快速上手并充分利用产品特性提供指导。 桥接产品的核心功能概述 HOOPS Lumi…...
Docder 搭建Redis分片集群 散片插槽 数据分片 故障转移 Java连接
介绍 使多个 Redis 实例共同工作,实现数据的水平扩展。通过将数据分片到多个节点上,Redis 集群能够在不牺牲性能的前提下扩展存储容量和处理能力,从而支持更高并发的请求。Redis 集群不仅支持数据分片,还提供了自动故障转移和高可…...
第19节 Node.js Express 框架
Express 是一个为Node.js设计的web开发框架,它基于nodejs平台。 Express 简介 Express是一个简洁而灵活的node.js Web应用框架, 提供了一系列强大特性帮助你创建各种Web应用,和丰富的HTTP工具。 使用Express可以快速地搭建一个完整功能的网站。 Expre…...
【快手拥抱开源】通过快手团队开源的 KwaiCoder-AutoThink-preview 解锁大语言模型的潜力
引言: 在人工智能快速发展的浪潮中,快手Kwaipilot团队推出的 KwaiCoder-AutoThink-preview 具有里程碑意义——这是首个公开的AutoThink大语言模型(LLM)。该模型代表着该领域的重大突破,通过独特方式融合思考与非思考…...
页面渲染流程与性能优化
页面渲染流程与性能优化详解(完整版) 一、现代浏览器渲染流程(详细说明) 1. 构建DOM树 浏览器接收到HTML文档后,会逐步解析并构建DOM(Document Object Model)树。具体过程如下: (…...
【AI学习】三、AI算法中的向量
在人工智能(AI)算法中,向量(Vector)是一种将现实世界中的数据(如图像、文本、音频等)转化为计算机可处理的数值型特征表示的工具。它是连接人类认知(如语义、视觉特征)与…...
UR 协作机器人「三剑客」:精密轻量担当(UR7e)、全能协作主力(UR12e)、重型任务专家(UR15)
UR协作机器人正以其卓越性能在现代制造业自动化中扮演重要角色。UR7e、UR12e和UR15通过创新技术和精准设计满足了不同行业的多样化需求。其中,UR15以其速度、精度及人工智能准备能力成为自动化领域的重要突破。UR7e和UR12e则在负载规格和市场定位上不断优化…...
GC1808高性能24位立体声音频ADC芯片解析
1. 芯片概述 GC1808是一款24位立体声音频模数转换器(ADC),支持8kHz~96kHz采样率,集成Δ-Σ调制器、数字抗混叠滤波器和高通滤波器,适用于高保真音频采集场景。 2. 核心特性 高精度:24位分辨率,…...
Typeerror: cannot read properties of undefined (reading ‘XXX‘)
最近需要在离线机器上运行软件,所以得把软件用docker打包起来,大部分功能都没问题,出了一个奇怪的事情。同样的代码,在本机上用vscode可以运行起来,但是打包之后在docker里出现了问题。使用的是dialog组件,…...
用机器学习破解新能源领域的“弃风”难题
音乐发烧友深有体会,玩音乐的本质就是玩电网。火电声音偏暖,水电偏冷,风电偏空旷。至于太阳能发的电,则略显朦胧和单薄。 不知你是否有感觉,近两年家里的音响声音越来越冷,听起来越来越单薄? —…...
QT3D学习笔记——圆台、圆锥
类名作用Qt3DWindow3D渲染窗口容器QEntity场景中的实体(对象或容器)QCamera控制观察视角QPointLight点光源QConeMesh圆锥几何网格QTransform控制实体的位置/旋转/缩放QPhongMaterialPhong光照材质(定义颜色、反光等)QFirstPersonC…...
Redis:现代应用开发的高效内存数据存储利器
一、Redis的起源与发展 Redis最初由意大利程序员Salvatore Sanfilippo在2009年开发,其初衷是为了满足他自己的一个项目需求,即需要一个高性能的键值存储系统来解决传统数据库在高并发场景下的性能瓶颈。随着项目的开源,Redis凭借其简单易用、…...
