RT-thread移植指南-RISC-V
⽬录
夫妻之间如何相处
⽂档中附件及其完整word资源:
1. 概述
本⽂主要记录将RT-thread标准版(v4.0.3)移植到risc-v双核U74的移植内容及其步骤。本⽂主要⽬的是记录移植步骤⽅便有需要者能快速了解移植的⼏个步骤和⼯作内容以便快速开展。由于环境和笔者对该系统的了解有限,已有参考的实现该⽂不再赘述。移植步骤为先将RT-thread在单核环境上跑通,再⽀持SMP,以便调试问题,提⾼效率。对于内核部分的移植⼯作,单核主要参考RT-Thread 已⽀持的e310,多核主要参考k210的移植。
1.1 移植资料参考
RT-thread 官⽹含有丰富切详尽的资料,建议先阅读相关⽂档,再进⾏下⼀步动作。这⾥我们选择移植标准板,故参考⽂档选择标准版⽂档。
⽂档官⽹:/document/site/#/
内核移植章节:/document/site/#/rt-thread-version/rt-thread-standard/programming-
manual/porting/porting
SMP移植章节:/document/site/#/rt-thread-version/rt-thread-standard/programming-
manual/smp/smp
RT-thread启动流程:/document/site/#/rt-thread-version/rt-thread-standard/programming-
manual/basic/basic?id=rt-thread-%e5%90%af%e5%8a%a8%e6%b5%81%e7%a8%8b
1.2 移植开发环境准备
1. 硬件:RISC-V 开发板+jtag⼯具+串⼝⼯具
2. 软件:RISC-V编译环境+sons
3. 代码:国内镜像云:
2. 移植步骤
本次移植最终⽬标是将RT-thread标准板移植到双核U74上。通过官⽅⽂档和代码可以得出,要想将RT-thread移植到单核U74上,最少⼯作量为只移植内核+串⼝(⽅便调试)。本次移植采⽤策略为先将RT-thread在单核环境上跑通,再⽀持SMP,以便调试问题,提⾼效率。对于此内核部分的移植⼯作,单核主要参考RT-Thread 已⽀持的e310,多核主要参考k210的移植。
内核移植⼯作拆分:
1. 全局中断开关函数
2. 线程上下⽂切换函数
3. 线程栈的初始化
4. 时钟节拍的配置
5. 中断函数(中断时现场保护、中断注册和使能)
6. RT-thread启动流程
7. 配置宏、全局符号的适配
2.1 全局中断开关函数
对于U74,这部分代码可以直接使⽤libcpu/risc-v/common⽬录下的context_gcc.S已有的相关实现,不再需要重新实现。
2.2 线程上下⽂切换函数
函数和变量
描述
rt_ba_t rt_hw_interrupt_disable(void);关闭全局中断void rt_hw_interrupt_enable(rt_ba_t level);
打开全局中断
rt_uint8_t *rt_hw_stack_init(void *tentry, void *parameter,rt_uint8_t *stack_addr, void *texit);
线程栈的初始化,内核在线程创建和线程初始化⾥⾯会调⽤这个函数
void rt_hw_context_switch_to(rt_uint32 to);
没有来源线程的上下⽂切换,在调度器启动第⼀个线程的时候调⽤,以及在 signal ⾥⾯会调⽤
void rt_hw_context_switch(rt_uint32 from, rt_uint32 to);从 from 线程切换到 to 线程,⽤于线程和线程之间的切换void rt_hw_context_switch_interrupt(rt_uint32 from, rt_uint32to);
从 from 线程切换到 to 线程,⽤于中断⾥⾯进⾏切换的时候使⽤
rt_uint32_t rt_thread_switch_interrupt_flag;
表⽰需要在中断⾥进⾏切换的标志
rt_uint32_t rt_interrupt_from_thread, rt_interrupt_to_thread;
在线程进⾏上下⽂切换时候,⽤来保存 from 和 to 线程
RT-Thread 的 libcpu 抽象层向下提供了⼀套统⼀的 CPU 架构移植接⼝,这部分接⼝包含了全局中断开关函数、线程上下⽂切换函数、时钟节拍的配置和中断函数、Cache 等等内容。下表是 CPU 架构移植需要实现的接⼝和变量。libcpu 移植相关 API
对于U74,这部分代码可以直接使⽤libcpu/risc-v/common⽬录下已有的context_gcc.S相关实现,不再需要重新实现。只需要在rtconfig.h 中定义宏ARCH_CPU_64BIT、ARCH_RISCV_FPU、ARCH_RISCV_FPU_D。
2.3 线程栈的初始化冷静英语
对于U74,这部分代码可以直接使⽤libcpu/risc-v/common⽬录下已有的context_gcc.S相关实现,不再需要重新实现。
2.4 时钟节拍的配置
有了开关全局中断和上下⽂切换功能的基础,RTOS 就可以进⾏线程的创建、运⾏、调度等功能了。有了时钟节拍⽀持,RT-Thread 可以实现对相同优先级的线程采⽤时间⽚轮转的⽅式来调度,实现定时器功能,实现 rt_thread_delay() 延时函数等等。
libcpu 的移植需要完成的⼯作,就是确保 rt_tick_increa() 函数会在时钟节拍的中断⾥被周期性的调⽤,调⽤周期取决于 rtconfig.h 的宏RT_TICK_PER_SECOND 的值。
在 Cortex M 中,实现 SysTick 的中断处理函数即可实现时钟节拍功能。void SysTick_Handler(void){
/
* enter interrupt */ rt_interrupt_enter(); rt_tick_increa(); /* leave interrupt */ rt_interrupt_leave();}
在U74中,在rt_hw_board_init中调⽤rt_hw_timer_init对cpu定时器初始化,注册中断。在中断中调⽤rt_tick_increa()执⾏RTOS的操作并且重新设置定时定时值。
注:RT-thread 有⼀个⼩bug,rt_tick_increa()中会调⽤rt_timer_check函数检查定时器,在rt_timer_check中会判断链表
rt_timer_list[RT_TIMER_SKIP_LIST_LEVEL - 1]是否为空,⽽timer链表初始化在后续的rt_system_timer_init才执⾏,故若第⼀次定时器中断到来前该链表还未初始化,rt_timer_check会将链表错误地检查为不为空,然后进⾏后续操作,会导致程序挂死,故笔者第⼀次将cpu timer超时时间设置1s,让第⼀次timer中断来得晚⼀些。
/* system tick interrupt */
void handle_m_time_interrupt(int id, void *priv)
{
int hartid = metal_cpu_get_current_hartid();
rt_tick_increa();
metal_cpu_t_mtimecmp(cpu[hartid], metal_cpu_get_mtime(cpu[hartid]) + TICK_COUNT);
}
static void rt_hw_timer_init(void)
{
cndint hartid = metal_cpu_get_current_hartid();
//FIXME: call rt_tick_increa must after rt_system_timer_init,so t more delay for first time
雅思网校metal_cpu_t_mtimecmp(cpu[hartid], metal_cpu_get_mtime(cpu[hartid]) + SF_CPU_RTC_TOGGLE_HZ);
// /* enable timer interrupt*/
rt_hw_timer_irq_register(handle_m_time_interrupt, NULL);
rt_hw_timer_irq_enable();
}
2.5 中断函数(中断时现场保护、中断注册和使能)
该部分主要实现进⼊中断时保存现场(栈极其相关寄存器的值)、中断控制器相关接⼝提供(plic中断注册和使能)。其中中断注册和使能接⼝主要是封装freedom-metal中的接⼝提供给rt-thread使⽤。所有中断第⼀⼊⼝为trap_entry,中断处理函数实现在libcpu/risc-v相关⽬录下的interrupt_gcc.S。
2.5.1 interrupt_gcc.S:
在单核情况下,该部分代码主要参考e310⽬录下interrupt_gcc.S的代码。
在多核情况下,该部分代码主要结合e310、k20⽬录下interrupt_gcc.S的代码。下⾯分析代码为多核代码。
该⽂件的代码主要做如下动作:
1. 进⼊中断前保存栈信息、fpu寄存器f0-f32、risc-v通⽤寄存器x1-x32、mstatus、mepc、的值到线程栈中。切换当前栈为cpu 中断
栈中(lds⽂件中预留,各cpu独⽴)
2. 调⽤rt_interrupt_enter函数进⾏中断嵌套计数等操作。
3. 调⽤⾃⼰实现的全局中断处理函数,进⾏中断分发和处理。在此处笔者调⽤freedom-metal的__metal_exception_handler函数来处
理中断,在该函数中会调⽤笔者⾃⼰实现的中断分发函数(通过freedom-metal的接⼝注册)。然后在笔者⾃⼰的中断分发函数中通过查表通过调⽤RT-thread 中断注册api注册的中断。(注:freedom-metal的__metal_exception_handler函数使⽤了
__attribute__((interrupt, aligned(128)))宏,该宏编译代码后会⾃动在该函数前后加⼊堆栈保持和恢复,和适配的代码冲突,故需要注释掉该宏,⼿动对堆栈操作。)
4. 调⽤rt_interrupt_leave函数进⾏中断嵌套计数等操作。
5. 如果使能了SMP,则调⽤rt_scheduler_do_irq_switch,该函数主要进⾏线程调度和切换。(若中断处理函数中需要进⾏线程调度和切
换,会延后到此处进⾏,主要减⼩互斥锁等共享资源的占⽤时间,单核情况下该动作在前⾯的中断中执⾏)。
6. 调⽤rt_hw_context_switch_exit,恢复线程栈中的环境,返回到中断前的状态。
interrupt_gcc.S实现:
2.5.2 中断注册、使能、和分发
cpu的总中断注册由如下代码完成:
/* config interrupt vector*/
asm volatile(
in your head
"la t0, trap_entry\n"
"csrw mtvec, t0"
);
中断注册:
/**
* This function will install a interrupt rvice routine to a interrupt.
* @param vector the interrupt number
* @param handler the interrupt rvice routine to be installed
* @param param the interrupt rvice function parameter
* @param name the interrupt name
* @return old handler
*/
rt_isr_handler_t rt_hw_interrupt_install(int vector, rt_isr_handler_t handler,
void *param, const char *name)
{
rt_isr_handler_t old_handler = RT_NULL;
if(vector < MAX_HANDLERS)
{
old_handler = irq_desc[vector].handler;
if (handler != RT_NULL)
{
irq_desc[vector].handler = (rt_isr_handler_t)handler;
irq_desc[vector].param = param;
#ifdef RT_USING_INTERRUPT_INFO
rt_snprintf(irq_desc[vector].name, RT_NAME_MAX - 1, "%s", name);
irq_desc[vector].counter = 0;
#endif
metal_interrupt_register_handler(plic[plic_int_hart], PLIC_EXT_IRQ(vector), handle_m_ext_interrupt, NULL); }
}
return old_handler;
}
中断使能:
/**
* This function will mask a interrupt.
* @param vector the interrupt number
*/
void rt_hw_interrupt_mask(int irq)
{
metal_interrupt_disable(plic[plic_int_hart], PLIC_EXT_IRQ(irq));
}
/**
* This function will un-mask a interrupt.
* @param vector the interrupt number
*/
void rt_hw_interrupt_unmask(int irq)
{
metal_interrupt_enable(plic[plic_int_hart], PLIC_EXT_IRQ(irq));
}
中断分发处理:
/**
* This function will be call when external machine-level
* interrupt from PLIC occurred.
*/
static void handle_m_ext_interrupt(int id, void *priv)
{
rt_isr_handler_t isr_func;
rt_uint32_t irq;
void *param;nikita第四季
weirdo
irq = REVEAL_PLIC_EXT_IRQ(id);
/* get interrupt rvice routine */
isr_func = irq_desc[irq].handler;
param = irq_desc[irq].param;
/* turn to interrupt rvice routine */
isr_func(irq, param);
#ifdef RT_USING_INTERRUPT_INFO
irq_desc[irq].counter ++;
#endif
}
2.6 RT-thread启动流程
具体启动流程可见:
/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/basic/basic?id=rt-thread-%e5%90%af%e5%8a%a8%e6%b5%81%e7%a8%8b
启动流程可见下图:
RT-thread 是通过startup_xx.S 来调⽤entry函数(对于gcc),然后调⽤rtthread_startup函数,在该函数中⼤致进⾏如下动作:
(1)初始化与系统相关的硬件;
(2)初始化系统内核对象,例如定时器、调度器、信号;
(3)创建 main 线程,在 main 线程中对各类模块依次进⾏初始化;
1. 初始化定时器线程、空闲线程,并启动调度器。在职研究生报名入口
对于笔者⽽⾔,由于移植RT-thread是基于之前的⼀个裸机U74⼯程,故没有从startup_xx.S启动,⽽是在之前裸机⼯程的汇编代码启动main函数处替换为entry函数。
2.7 配置宏、全局符号的适配
2.7.1 宏适配
RT-thread 主要配置宏是在bsp/xxxxx/rtconfig.h⽂件中。对于移植到U74,主要是在hifi1的配置⽂件上修改,主要修改栈⼤⼩,cpu位宽(ARCH_CPU_64BIT、ARCH_RISCV_FPU、ARCH_RISCV_FPU_D)、系统位宽(RT_ALIGN_SIZE)、SMP⽀持
(RT_USING_SMP)、cpu数量(RT_CPUS_NR)、⾃带测试框架(使能RT_USING_TC,注释掉FINSH_USING_MSH_ONLY)、等。
pederasty
注:笔者前期经常遇到cpu执⾏到某条指令挂死,主要原因就是cpu字长等宏没有定义正确。
配置⽂件:
frickin
2.7.2 全局符号适配
RT-thread 的⼀些⾃动初始化功能和组件需要在lds⽂件中预留符号表位置。RT-thread堆栈功能也依赖lds⽂件中的堆栈符号定义。
堆适配符号引⽤⽰例:
extern void *metal_gment_heap_target_start;
extern void *metal_gment_heap_target_end;
#define HEAP_BEGIN &metal_gment_heap_target_start
#define HEAP_END &metal_gment_heap_target_end