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 := all

all:
	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_D8C
  RCC->CFGR0 &= (uint32_t)0xF8FF0000; 
#else
  RCC->CFGR0 &= (uint32_t)0xF0FF0000; 
#endif 

  RCC->CTLR &= (uint32_t)0xFEF6FFFF;
  RCC->CTLR &= (uint32_t)0xFFFBFFFF;
  RCC->CFGR0 &= (uint32_t)0xFF80FFFF;

#ifdef CH32V30x_D8C
  RCC->CTLR &= (uint32_t)0xEBFFFFFF;
  RCC->INTR = 0x00FF0000;
  RCC->CFGR2 = 0x00000000;
#else
  RCC->INTR = 0x009F0000;   
#endif   
  SetSysClock();
}

在SetSysClock中将系统时钟设置为定义好的144MHz:

static void SetSysClock(void)
{
#ifdef SYSCLK_FREQ_HSE
  SetSysClockToHSE();
#elif defined SYSCLK_FREQ_24MHz
  SetSysClockTo24();
#elif defined SYSCLK_FREQ_48MHz
  SetSysClockTo48();
#elif defined SYSCLK_FREQ_56MHz
  SetSysClockTo56();  
#elif defined SYSCLK_FREQ_72MHz
  SetSysClockTo72();
#elif defined SYSCLK_FREQ_96MHz
  SetSysClockTo96();
#elif defined SYSCLK_FREQ_120MHz
  SetSysClockTo120();
#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_D8
        RCC->CFGR0 |= (uint32_t)(RCC_PLLSRC_HSE | RCC_PLLXTPRE_HSE | RCC_PLLMULL18);
#else
        RCC->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_SMP
    rt_hw_spin_lock_init(&_cpus_lock);
#endif
    rt_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_SMP
    rt_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_SERIAL
    rt_hw_usart_init();
#endif
#ifdef RT_USING_CONSOLE
    rt_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_INIT
    rt_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();

}

嵌入式系统中Systick(System Tick)通常是一个定时器,用于提供精确的时间管理功能,特别是在实时操作系统(RTOS)中。`systick_init`函数的主要作用是初始化这个定时器,使其按照预设的周期(定时周期)定期触发中断,以便调度任务、测量时间间隔等。 以下是基本的`systick_init`流程图示意图: 1. **硬件配置**: - 确认Systick组件是否已连接到微控制器,并了解其寄存器地址。 - 检查Systick的工作模式和分频因子设置。 2. **读取当前值** (如果需要): - 可能需要获取Systick当前计数值,作为初始化基准。 3. **清除溢出标志**: - 如果Systick有溢出标志位,先将其清零,防止上一次运行遗留的数据影响初始化结果。 4. **配置定时器**: - 设置Systick的周期(通常是系统频率除以所需的定时精度),例如如果系统时钟为72MHz,每秒500毫秒,那么周期就是72M / (1000 * 500) = 1.44。 5. **启用定时器**: - 开启Systick计数器,使其开始从配置的周期递减。 6. **配置中断服务程序**: - 设置Systick中断,当计数器到达0时,会触发对应的中断,然后通过中断服务程序(ISRs)处理。 7. **开启中断**: - 向处理器内核请求开启Systick中断。 8. **保存设置**: - 可能需要将这些设置记录下来,以便于后续调整或诊断。 流程图简化后的伪代码可能如下: ``` - 初始化Systick寄存器 - 清除溢出标志 - 设定Systick周期 - 设置中断处理函数 - 开启Systick - 开启中断 - 结束初始化
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值