目录
RT-thread移植指南-RISC-V
1. 概述
1.1 移植资料参考
1.2 移植开发环境准备
2. 移植步骤
2.1 全局中断开关函数
2.2 线程上下文切换函数
2.3 线程栈的初始化
2.4 时钟节拍的配置
2.5 中断函数(中断时现场保护、中断注册和使能)
2.5.1 interrupt_gcc.S:
2.5.2 中断注册、使能、和分发
2.6 RT-thread启动流程
2.7 配置宏、全局符号的适配
2.7.1 宏适配
2.7.2 全局符号适配
3. SMP移植
3.1 自旋锁
3.2 为不同core准备不同栈
3.3 核间通信ipi中断
3.4 次级cpu的启动代码
3.4.1 rt_hw_secondary_cpu_up()
3.4.2 secondary_cpu_c_start()
3.4.3 rt_hw_secondary_cpu_idle_exec()
4. debug问题及解决
文档中附件及其完整word资源:https://download.csdn.net/download/ty1121466568/24399940
本文主要记录将RT-thread标准版(v4.0.3)移植到risc-v双核U74的移植内容及其步骤。本文主要目的是记录移植步骤方便有需要者能快速了解移植的几个步骤和工作内容以便快速开展。由于环境和笔者对该系统的了解有限,已有参考的实现该文不再赘述。移植步骤为先将RT-thread在单核环境上跑通,再支持SMP,以便调试问题,提高效率。对于内核部分的移植工作,单核主要参考RT-Thread 已支持的e310,多核主要参考k210的移植。
RT-thread 官网含有丰富切详尽的资料,建议先阅读相关文档,再进行下一步动作。这里我们选择移植标准板,故参考文档选择标准版文档。
文档官网:https://www.rt-thread.org/document/site/#/
内核移植章节:https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/porting/porting
SMP移植章节:https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/smp/smp
RT-thread启动流程:https://www.rt-thread.org/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标准板移植到双核U74上。通过官方文档和代码可以得出,要想将RT-thread移植到单核U74上,最少工作量为只移植内核+串口(方便调试)。本次移植采用策略为先将RT-thread在单核环境上跑通,再支持SMP,以便调试问题,提高效率。对于此内核部分的移植工作,单核主要参考RT-Thread 已支持的e310,多核主要参考k210的移植。
内核移植工作拆分:
对于U74,这部分代码可以直接使用libcpu/risc-v/common目录下的context_gcc.S已有的相关实现,不再需要重新实现。
RT-Thread 的 libcpu 抽象层向下提供了一套统一的 CPU 架构移植接口,这部分接口包含了全局中断开关函数、线程上下文切换函数、时钟节拍的配置和中断函数、Cache 等等内容。下表是 CPU 架构移植需要实现的接口和变量。
libcpu 移植相关 API
函数和变量 |
描述 |
rt_base_t rt_hw_interrupt_disable(void); |
关闭全局中断 |
void rt_hw_interrupt_enable(rt_base_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_uint32 to); |
从 from 线程切换到 to 线程,用于中断里面进行切换的时候使用 |
rt_uint32_t rt_thread_switch_interrupt_flag; |
表示需要在中断里进行切换的标志 |
rt_uint32_t rt_interrupt_from_thread, rt_interrupt_to_thread; |
在线程进行上下文切换时候,用来保存 from 和 to 线程 |
对于U74,这部分代码可以直接使用libcpu/risc-v/common目录下已有的context_gcc.S相关实现,不再需要重新实现。只需要在rtconfig.h 中定义宏ARCH_CPU_64BIT、ARCH_RISCV_FPU、ARCH_RISCV_FPU_D。
对于U74,这部分代码可以直接使用libcpu/risc-v/common目录下已有的context_gcc.S相关实现,不再需要重新实现。
有了开关全局中断和上下文切换功能的基础,RTOS 就可以进行线程的创建、运行、调度等功能了。有了时钟节拍支持,RT-Thread 可以实现对相同优先级的线程采用时间片轮转的方式来调度,实现定时器功能,实现 rt_thread_delay() 延时函数等等。
libcpu 的移植需要完成的工作,就是确保 rt_tick_increase() 函数会在时钟节拍的中断里被周期性的调用,调用周期取决于 rtconfig.h 的宏 RT_TICK_PER_SECOND 的值。
在 Cortex M 中,实现 SysTick 的中断处理函数即可实现时钟节拍功能。
void SysTick_Handler(void) { /* enter interrupt */ rt_interrupt_enter(); rt_tick_increase(); /* leave interrupt */ rt_interrupt_leave(); } |
在U74中,在rt_hw_board_init中调用rt_hw_timer_init对cpu定时器初始化,注册中断。在中断中调用rt_tick_increase()执行RTOS的操作并且重新设置定时定时值。
注:RT-thread 有一个小bug,rt_tick_increase()中会调用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_increase(); metal_cpu_set_mtimecmp(cpu[hartid], metal_cpu_get_mtime(cpu[hartid]) + TICK_COUNT); } static void rt_hw_timer_init(void) { int hartid = metal_cpu_get_current_hartid(); //FIXME: call rt_tick_increase must after rt_system_timer_init,so set more delay for first time metal_cpu_set_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(); } |
该部分主要实现进入中断时保存现场(栈极其相关寄存器的值)、中断控制器相关接口提供(plic中断注册和使能)。其中中断注册和使能接口主要是封装freedom-metal中的接口提供给rt-thread使用。所有中断第一入口为trap_entry,中断处理函数实现在libcpu/risc-v相关目录下的interrupt_gcc.S。
在单核情况下,该部分代码主要参考e310目录下interrupt_gcc.S的代码。
在多核情况下,该部分代码主要结合e310、k20目录下interrupt_gcc.S的代码。下面分析代码为多核代码。
该文件的代码主要做如下动作:
interrupt_gcc.S实现:
cpu的总中断注册由如下代码完成:
/* config interrupt vector*/ asm volatile( "la t0, trap_entry\n" "csrw mtvec, t0" ); |
中断注册:
/** * This function will install a interrupt service routine to a interrupt. * @param vector the interrupt number * @param handler the interrupt service routine to be installed * @param param the interrupt service 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; irq = REVEAL_PLIC_EXT_IRQ(id); /* get interrupt service routine */ isr_func = irq_desc[irq].handler; param = irq_desc[irq].param; /* turn to interrupt service routine */ isr_func(irq, param); #ifdef RT_USING_INTERRUPT_INFO irq_desc[irq].counter ++; #endif } |
具体启动流程可见:
https://www.rt-thread.org/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 线程中对各类模块依次进行初始化;
对于笔者而言,由于移植RT-thread是基于之前的一个裸机U74工程,故没有从startup_xx.S启动,而是在之前裸机工程的汇编代码启动main函数处替换为entry函数。
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)、等。
注:笔者前期经常遇到cpu执行到某条指令挂死,主要原因就是cpu字长等宏没有定义正确。
配置文件:
RT-thread 的一些自动初始化功能和组件需要在lds文件中预留符号表位置。RT-thread堆栈功能也依赖lds文件中的堆栈符号定义。
堆适配符号引用示例:
extern void *metal_segment_heap_target_start; extern void *metal_segment_heap_target_end; #define HEAP_BEGIN &metal_segment_heap_target_start #define HEAP_END &metal_segment_heap_target_end |
在lds中需要定义的符号:
.text : { *(.text.unlikely .text.unlikely.*) *(.text.startup .text.startup.*) *(.text .text.*) *(.gnu.linkonce.t.*)
/* section information for finsh shell */ . = ALIGN(8); __fsymtab_start = .; KEEP(*(FSymTab)) __fsymtab_end = .; . = ALIGN(8); __vsymtab_start = .; KEEP(*(VSymTab)) __vsymtab_end = .; . = ALIGN(8); . = ALIGN(8); __rt_init_start = .; KEEP(*(SORT(.rti_fn*))) __rt_init_end = .; . = ALIGN(8); /* section information for modules */ . = ALIGN(8); __rtmsymtab_start = .; KEEP(*(RTMSymTab)) __rtmsymtab_end = .;
/* section information for utest */ . = ALIGN(8); __rt_utest_tc_tab_start = .; KEEP(*(UtestTcTab)) __rt_utest_tc_tab_end = .;
. = ALIGN(8); _etext = .; } >ram AT>ram :ram |
在上述lds文件定义的符号中,__fsymtab_start,__fsymtab_end,__vsymtab_start, __vsymtab_end主要用于finsh的实现,在finsh_system_init函数中使用;.rti_fn*主要用于RT-thread 启动流程中的自动初始化功能,在INIT_EXPORT宏中使用;__rtmsymtab_start,__rtmsymtab_end主要用于libc模块的支持;__rt_utest_tc_tab_start,__rt_utest_tc_tab_end,UtestTcTab主要用于自带测试框架功能实现,在utest_init,UTEST_TC_EXPORT中使用。
参考官方文档,移植SMP主要有如下工作
此处参考k20 自旋锁实现,使用GCC自带的原子操作函数__sync_lock_test_and_set(&lock->lock, -1)实现自旋锁。
自旋锁实现如下:
#define atomic_set(ptr, val) (*(volatile typeof(*(ptr))*)(ptr) = val) #define atomic_read(ptr) (*(volatile typeof(*(ptr))*)(ptr)) #ifndef __riscv_atomic #error "atomic extension is required." #endif #define atomic_add(ptr, inc) __sync_fetch_and_add(ptr, inc) #define atomic_or(ptr, inc) __sync_fetch_and_or(ptr, inc) #define atomic_swap(ptr, swp) __sync_lock_test_and_set(ptr, swp) #define atomic_cas(ptr, cmp, swp) __sync_val_compare_and_swap(ptr, cmp, swp) typedef struct _spinlock { int lock; } spinlock_t; static inline int spinlock_trylock(spinlock_t *lock) { int res = atomic_swap(&lock->lock, -1); /* Use memory barrier to keep coherency */ mb(); return res; } static inline void spinlock_lock(spinlock_t *lock) { while (spinlock_trylock(lock)); } static inline void spinlock_unlock(spinlock_t *lock) { /* Use memory barrier to keep coherency */ mb(); atomic_set(&lock->lock, 0); asm volatile ("nop"); } |
适配到RT-thread api 实现如下:
#include "atomic_support.h" void rt_hw_spin_lock_init(rt_hw_spinlock_t *lock) { ((spinlock_t *)lock)->lock = 0; } void rt_hw_spin_lock(rt_hw_spinlock_t *lock) { spinlock_lock((spinlock_t *)lock); } void rt_hw_spin_unlock(rt_hw_spinlock_t *lock) { spinlock_unlock((spinlock_t *)lock); } |
多核情况下,不同core有不同中断函数栈,需在lds中分配。
__STACKSIZE__ = DEFINED(__STACKSIZE__) ? __STACKSIZE__ : 40M; PROVIDE(__STACKSIZE__ = __STACKSIZE__); .stack : { PROVIDE(metal_segment_stack_begin = .); __stack_start__ = .; . += __STACKSIZE__; __stack_cpu0 = .; . += __STACKSIZE__; __stack_cpu1 = .; PROVIDE(metal_segment_stack_end = .); } >ram AT>ram :ram |
对于启动函数和中断函数,不同cpu需要使用不同栈。
中断函数中:
/* switch interrupt stack of current cpu */ la sp, __stack_start__ addi t1, t0, 1 la t2, __STACKSIZE__ mul t1, t1, t2 add sp, sp, t1 /* sp = (cpuid + 1) * __STACKSIZE__ + __stack_start__ */ |
线程切换汇编文件libcpu/risc-v/commoncontex_gcc.S中,修改li t2, __STACKSIZE__为la t2, __STACKSIZE__ (注:此处为rt-thread riscv架构cpu的公共函数,理应不修改此处,但这里实现为主要适配k20,这个符号在头文件中进行宏定义和lds文件中进行符号定义,笔者觉得该符号定义过于冗余,故进行修改,只使用lds文件中的符号即可)
#ifdef RT_USING_SMP #ifdef RT_USING_SIGNALS mv a0, sp csrr t0, mhartid /* switch interrupt stack of current cpu */ la sp, __stack_start__ addi t1, t0, 1 la t2, __STACKSIZE__ mul t1, t1, t2 add sp, sp, t1 /* sp = (cpuid + 1) * __STACKSIZE__ + __stack_start__ */ call rt_signal_check mv sp, a0 #endif #endif |
SMP调度需要核间通信,调整其他cpu进行任务调度。U74 ipi使用的是clint
的软件中断。
其ipi中断注册实现如下:
void rt_hw_ipi_irq_enable(void) { int hartid = metal_cpu_get_current_hartid(); metal_interrupt_enable(cpu_sw_ipi_intr[hartid], metal_cpu_software_get_interrupt_id(cpu[hartid])); return 0; } void rt_hw_ipi_irq_disable(void) { int hartid = metal_cpu_get_current_hartid(); metal_interrupt_disable(cpu_sw_ipi_intr[hartid], metal_cpu_software_get_interrupt_id(cpu[hartid])); return 0; } void rt_hw_ipi_handler_install(int ipi_vector, rt_isr_handler_t ipi_isr_handler) { /* note: ipi_vector maybe different with irq_vector */ int hartid = metal_cpu_get_current_hartid(); metal_interrupt_register_handler(cpu_sw_ipi_intr[hartid], metal_cpu_software_get_interrupt_id(cpu[hartid]), ipi_isr_handler, (void*)NULL); } |
ipi中断处理函数:
void handle_hw_ipi_interrupt(int vector, void *param) { int hartid = metal_cpu_get_current_hartid(); metal_cpu_software_clear_ipi(cpu[hartid], hartid); rt_scheduler_ipi_handler(vector, param); } void rt_scheduler_ipi_handler(int vector, void *param) { rt_schedule(); } |
ipi 发送函数:
void rt_hw_ipi_send(int ipi_vector, unsigned int cpu_mask) { int idx; for (idx = 0; idx < RT_CPUS_NR; idx ++) { if (cpu_mask & (1 << idx)) { metal_cpu_software_set_ipi(cpu[idx] ,idx); } } } |
按照文档,内核开发者需要提供以下三个函数:
上述三个函数定义在文件 drivers/platsmp.c 中。其中,只有函数 rt_hw_secondary_cpu_up() 的功能实现与芯片密切相关,需要移植者根据芯片特性提供。如果芯片使用的不是 GIC 中断控制器和 Generic Timer 定时器,那么同样需要重新实现函数 secondary_cpu_c_start()。
该函数用于启动次级cpu。未执行该函数前,次级cpu处于暂停状态。暂停和启动次级cpu有两种方案:a. 次级cpu轮询判断clint的MIP_MSIP,主cpu通过发送ipi启动次级cpu;b. 次级cpu轮询全局变量,为指定值时启动
这里笔者没有采用发送ipi中断的方式启动,因为RT-thread可能在执行启动次级cpu函数前就发送ipi中断了,影响启动时序。
次级cpu启动函数:
void rt_hw_secondary_cpu_up(void) { mb(); secondary_boot_flag = 0xa55a; } |
次级cpu暂停并和恢复实现:
secondary_main: addi sp, sp, -16 #if __riscv_xlen == 32 sw ra, 4(sp) #else sd ra, 8(sp) #endif csrr t0, mhartid la t1, __metal_boot_hart beq t0, t1, 2f 1: secondary_cpu_entry: la a0, secondary_boot_flag ld a0, 0(a0) li a1, 0xa55a //beq a0, a1, 1f beq a0, a1, .enter_secondary_cpu_c_start j secondary_cpu_entry .enter_secondary_cpu_c_start: call secondary_cpu_c_start
2: call entry |
在上述实现中,判断当前cpu id是否是定义的启动cpu,如果是则进入entry函数进而进入RT-thread 启动流程,否则改cpu为次级cpu,在此处循环,直到主cpu调用rt_hw_secondary_cpu_up函数启动次级cpu。
void rt_hw_ipi_send(int ipi_vector, unsigned int cpu_mask) { int idx; for (idx = 0; idx < RT_CPUS_NR; idx ++) { if (cpu_mask & (1 << idx)) { metal_cpu_software_set_ipi(cpu[idx] ,idx); } } } |
secondary_cpu_c_start()实现如下:
void secondary_cpu_c_start(void) { int hartid = metal_cpu_get_current_hartid(); rt_hw_spin_lock(&_cpus_lock); /* initialize interrupt controller */ rt_hw_secondary_interrupt_init(); /* install IPI handle */ /*note: in smp, to prevent the deadlock, shouled clear ipi before enable interrupt */ rt_hw_ipi_irq_disable(); metal_cpu_software_clear_ipi(cpu[hartid], hartid); rt_hw_ipi_handler_install(RT_SCHEDULE_IPI, handle_hw_ipi_interrupt); rt_hw_ipi_irq_enable(); rt_hw_timer_init(); rt_system_scheduler_start(); } |
注:此处打开ipi中断前之所以进行disable和clear操作是为了防止在进入该函数前ipi中断已置位,在这种情况下使能中断后会直接进入ipi中断处理函数,而ipi中断处理函数会进行线程调度,会获取自旋锁_cpus_lock,导致死锁。此处的自旋锁会在后续rt_hw_context_switch_to函数切换线程时调用rt_cpus_lock_status_restore函数解锁。
rt_hw_secondary_cpu_idle_exec()函数主要实现次级cpu空闲任务,其实现如下:
void rt_hw_secondary_cpu_idle_exec(void) { asm volatile ("wfi"); } |
在移植过程中,遇到几个cpu执行到某条指令就挂死的问题,例ld、sd指令。这里进行汇总。一般出现该问题都是该条汇编指令的64位,而指令操作的对象内存没有对齐到64bit,导致指令异常。在本次移植中,对于该类型问题进行了如下修复动作:
RT-thread有一个小bug,rt_tick_increase()中会调用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中断来得晚一些。
笔者移植的是v4.0.3版本,自带测试框架中测例的主要设计思想是单核,对SMP不适用,例thread_detach测例可能会在A core上释放B core上正在运行的线程,导致无法释放,而A core又认为自己释放了,然后继续往下执行的情况发送。
该问题可优先检查是否是线程栈溢出,RT-thread只会在调度前检查栈溢出,而线程运行中栈增加无法检测,可能导致该问题。可调大栈的大小debug。