ThreadX移植RISC-V64(基于Milk-V Duo开发板)

前言

本文为将Eclipse ThreadX实时操作系统移植到RISC-V64架构,并运行于Milk-V Duo开发板上的步骤记录,实现ThreadX运行于无MMU的小核C906L上,Linux运行于大核C906B上。如有错误欢迎指出。

移植仓库:https://github.com/saicogn/ThreadX-to-RISC-V64

PLCT实验室实习生长期招聘:招聘信息链接

软硬件相关介绍

Milk-V Duo 是一款基于 CV1800B 芯片的超紧凑型嵌入式开发平台,可以运行 Linux 和 RTOS。CV180x系列芯片为双核RISC-V64芯片,内部为2个阿里平头哥的玄铁C906处理器,指令集为RV64GCV(RV64IMAFDCV)。

详细内容见Milk-V Duo的官方Wiki

Eclipse ThreadX为微软的一款实时操作系统 (RTOS),适用于由微控制器 (MCU) 驱动的物联网 (IoT) 和边缘设备,具有完善的中间件和高等级的安全认证,同时支持多核(AMP和SMP)以及应用程序动态加载。目前已经捐赠给Eclipse基金会并开源。

详细内容见ThreadX的官方仓库

本文主要正对ThreadX实时操作系统内核进行移植。

ThreadX中与架构相关内容

ThreadX源码目录

ThreadX内核源码结构如下图。

ThreadX内核与架构相关文件均在ports目录下,按照架构和构建工具链分类(ports_arch中为ARMv7和ARMv8相关的通用移植文件)。

目前,ThreadX内核官方已经支持RISC-V32(基于IAR的工程),RISC-V64目录下有相关文件(基于GNU,经后续测试,除了缺少浮点运算单元相关配置外,基本可用)。module和多核组件暂不支持RISC-V。

具体架构的ports目录内容如下图,以已经支持的RISC-V32为例,RISC-V64类似。

example_build:开箱即用的构建示例demo

tx_port.h:数据类型和其他宏定义

tx_initialize_low_level.s:初始化、支持时间片轮转调度所需的定时器中断服务函数(调用系统定时器函数)

tx_thread_context_restore.s:中断上下文恢复

tx_thread_context_restore.s:中断上下文保存

tx_thread_interrupt_control.s:使用Mask进行中断开关控制

tx_thread_schedul.s:任务调度

tx_thread_stack_build.s:为需要创建的任务构建栈帧(保存寄存器)

tx_thread_system_return.s:任务调用系统API函数的返回(根据需要进入任务调度)

tx_timer_interrupt.s:系统定时器函数

ThreadX启动流程和RISC-V下的任务调度

ThreadX的基本启动流程如下:

启动引导代码(非ThreadX负责)——》main函数——》部分硬件初始化、调用tx_kernel_enter()(_tx_initialize_kernel_enter函数的宏定义)。

_tx_initialize_kernel_enter函数工作为ThreadX内核的初始化和启动调度,主要函数包括:_tx_initialize_low_level()(初始化系统栈指针、空闲内存等)、
_tx_initialize_high_level()(为各种模块初始化、创建系统timer任务等)、
tx_application_define()(用户定义,创建任务以及其他内核相关功能、其他初始化)、

_tx_thread_schedule()开启调度(读取第一个任务的指针)。

ThreadX在RISC-V上不像其在ARM-M中使用软件触发的PendSV中断进行任务上下文切换,而是在任务进程中通过调用的系统函数或者在中断处理函数返回时按需调用_tx_thread_system_return从而进入调度器,下面以tx_thread_sleep这个系统API的调用流程为例

(RISC-V也可用通过ecall指令触发系统调用的软件中断,如FreeRTOS在RISC-V上的调度)

任务调用ThreadX系统API,如tx_thread_sleep——》 _tx_thread_system_suspend(thread_ptr);——》 _tx_thread_system_returnport文件)——》 _tx_thread_scheduleport文件)。

Milk-V Duo中RTOS的启动流程

关于Milk-V Duo详细的多级启动流程可以参考汪辰老师的博文

对于Milk-V Duo的RTOS而言,其将会和FSBL(First Stage Boot Loader)、OpenSBI、U-Boot等基础固件一起编译为fip.bin文件。在大核运行FSBL会先将RTOS载入指定内存空间后,交由小核运行RTOS。

从官方链接脚本cvitek/scripts/cv180x_lscript.ld可以看出,RTOS的入口地址文件为cvitek/arch/riscv64/src/start.S,经过一系列的初始化后直接跳入main函数。

此外,链接脚本中还定义RTOS在Milk-V Duo的内存空间中的其实地址和大小。

INCLUDE cvi_board_memmap.ld

_STACK_SIZE = DEFINED(_STACK_SIZE) ? _STACK_SIZE : 0x20000;
/* _HEAP_SIZE = DEFINED(_HEAP_SIZE) ? _HEAP_SIZE : 0x1000000; */
/*_HEAP_SIZE =  0x20000;*/

_EL0_STACK_SIZE = DEFINED(_EL0_STACK_SIZE) ? _EL0_STACK_SIZE : 1024;
_EL1_STACK_SIZE = DEFINED(_EL1_STACK_SIZE) ? _EL1_STACK_SIZE : 2048;
_EL2_STACK_SIZE = DEFINED(_EL2_STACK_SIZE) ? _EL2_STACK_SIZE : 1024;

/* Define Memories in the system */

MEMORY
{
   psu_ddr_0_MEM_0 : ORIGIN = CVIMMAP_FREERTOS_ADDR , LENGTH = CVIMMAP_FREERTOS_SIZE
}
MEMORY
{
   psu_ddr_0_MEM_0 : ORIGIN = CVIMMAP_FREERTOS_ADDR , LENGTH = CVIMMAP_FREERTOS_SIZE
}

其中,CVIMMAP_FREERTOS_ADDR 和CVIMMAP_FREERTOS_SIZE定义在cvi_board_memmap.ld文件中,其是构建整个SDK过程中,在由脚本build/boards/cv180x/cv1800b_milkv_duo_sd/memmap.py生成的。

默认的起始地址为0x83f40000,大小为0xc0000。

关于Milk-V Duo官方SDK的详细解析可以参考燕十三大佬的系列博文

移植到RISCV-V需要完成的工作

移植一款RTOS的基本内容有任务创建时的任务栈分配、任务切换和任务切换时的上下文保存和恢复、中断处理和管理(包括临界区控制)、系统定时器、内存分配等等。ThreadX官方已经堆RISC-V64有了基本的支持,这里需要完成的工作主要为增加中断处理函数、配置系统定时器、划分分配给任务栈的内存空间、以及系统构建相关内容。此外,实机测试时发现ThreadX官方代码对于支持浮点单元的RISC-V芯片存在一些可能导致死机的Bug,故进行了修复。

移植步骤

移植内容参考了Milk-V Duo官方SDK中已经支持的FreeRTOS、以及ThreadX官方已经支持的RISC-V32的IAR工程。

准备工作

本地clone拉取Milk-V Duo官方SDK。为了不影响原SDK内容,新建个threadx目录,并将freertos目录下文件拷贝进来,并删除threadx中的FreeRTOS目录(留着也行)。

在threadx目录下clone拉取ThreadX的源码

除了工程构建时需要修改Milk-V Duo官方SDK的build相关配置,其余移植工作均可在threadx目录下完成,以下内容的默认目录为threadx。

1._tx_initialize_low_level的修改内容

主要为设置空闲内存地址、中断入口、定时器配置入口。

ThreadX会将空闲内存的首地址_tx_initialize_unused_memory传入tx_application_define()函数供用户自行分配(非必须),一般在_tx_initialize_low_level()函数中设置。

修改Milk-V Duo官方的链接脚本cv180x_lscript.ld,在最后加上标识符作为空闲内存的首地址,即作为ThreadX空闲地址。此处需要再往后8个字节,避免覆盖到栈的最后一块内存。

……

.stack (NOLOAD) : {
   . = ALIGN(64);
   _stack_end_end = .;
   . += _STACK_SIZE;
   _stack_top = .;
} > psu_ddr_0_MEM_0

_end = .;
}

设置mtvec寄存器,将后续的中断处理函数threadx_trap_entry作为中断入口地址。这里没有采用类似ARM-M中NVIC的中断向量表,而是采用单一硬件中断入口地址,软件判断中断源并进行分发处理的方式。

保存将保存有函数返回地址的RA寄存器压栈后,调用系统定时器和定时器中断配置函数port_specific_pre_initialization。

_tx_initialize_low_level:
    sd      sp, _tx_thread_system_stack_ptr, t0     // Save system stack pointer
    
    la      t0, _end                                // Pickup first free address
    addi    t0, t0, 8                               // add 8 or 4?
    
    sd      t0, _tx_initialize_unused_memory, t1    // Save unused memory address

#ifdef __riscv_flen
    fscsr x0
#endif

    // set up interrupt and timer(mtime)
#if( portasmHAS_SIFIVE_CLINT != 0 )
	/* If there is a clint use a unified interrupt/trap entry */
	la t0, threadx_trap_entry
	csrw mtvec, t0
#endif

	addi sp, sp, -1*REGBYTES
	STORE ra, 1*REGBYTES(sp)  // save ra
	call port_specific_pre_initialization  // setup timer interrupt
	LOAD ra, 1*REGBYTES(sp)
	addi sp, sp, 1*REGBYTES

    ret

2.中断处理函数

中断处理函数入口为threadx_trap_entry

首先在任务栈上预留相应大小的现场(处理器寄存器)保护内存,保存RA返回地址后调用_tx_thread_context_save函数进行上下文保存。

再判断是同步触发的异常还是异步的中断。

如果为同步触发的异常则跳转handle_synchronous,并将返回地址执行触发地址的下一个地址(RISC-V同步异常触发后,返回地址不会自动指向下一指令)。再判断是否为系统调用,为系统调用的话直接跳转最后的context_restore,调用_tx_thread_context_restore恢复上下文,并按需进入调度器。如果非系统调用则是触发了系统错误的异常,进入is_exception死循环。

如果为异步中断,则先判断是否为定时器中断,为定时器中断话更新定时器比较寄存器为下一次触发的比较值,并跳入ThreadX定时器中断的处理函数,处理任务超时和时间片相关内容。返回后同样跳转最后的context_restore

如果非定时器中断,则判断是否为外部中断,为外部中断的话调用portasmHANDLE_INTERRUPT,进入外部中断的处理函数。返回后同样跳转最后的context_restore。

补充:

  1. portasmHANDLE_INTERRUPT为do_irq的宏定义,do_irq函数位于cvitek/driver/common/src/system.c文件,通过g_irq_action数组处理各个中断号的中断,使用前需要调用request_irq申请中断号和注册中断处理函数。
  2. ThreadX默认是不给IDLE任务分配任务栈的,故如果当前任务为IDLE任务的话,就跳过保存上下文的内容。
.align 8
.func
threadx_trap_entry: 
#if defined(__riscv_float_abi_single) || defined(__riscv_float_abi_double)
	addi sp, sp, -65*REGBYTES         // Allocate space for all registers - with floating point enabled
#else
	addi sp, sp, -32*REGBYTES         // Allocate space for all registers - without floating point enabled
#endif

	STORE   x1, 28*REGBYTES(sp)       // Store RA, 28*REGBYTES(224 for riscv64)
    
    call    _tx_thread_context_save                 // Call ThreadX context save

	/* exception handle part */
	
	csrr a0, mcause
	csrr a1, mepc

test_if_asynchronous:
	srli a2, a0, __riscv_xlen - 1		/* MSB of mcause is 1 if handing an asynchronous interrupt - shift to LSB to clear other bits. */
	beq a2, x0, handle_synchronous		/* Branch past interrupt handing if not asynchronous. */

	/* unmodified exception return address(mepc) was saved in tx_thread_context_save.S */

handle_asynchronous:

#if( portasmHAS_MTIME != 0 )

	test_if_mtimer:						/* If there is a CLINT then the mtimer is used to generate the tick interrupt. */

		addi t0, x0, 1  /* t0 = 1*/

		slli t0, t0, __riscv_xlen - 1   /* LSB is already set, shift into MSB.  Shift 31 on 32-bit or 63 on 64-bit cores. */
		addi t1, t0, 7					/* 0x8000[]0007 == machine timer interrupt. */
		bne a0, t1, test_if_external_interrupt

    #ifdef THEAD_C906
		    LOAD t0, pulMachineTimerCompareRegisterL  /* Load address of compare Lo register into t0. */
    #else
		    LOAD t0, pullMachineTimerCompareRegister  /* Load address of compare register into t0. */
    #endif
		
        LOAD t1, pullNextTime           /* Load the address of ullNextTime into t1. */

    #ifdef THEAD_C906
			/* Update the 64-bit mtimer compare match value in two 32-bit writes. */
			ld t2, 0(t1)			 	/* Load ullNextTime into t2. */
			sw t2, 0(t0)				/* Store ullNextTime into compare register L. */
			srli t3, t2, 32
			sw t3, 4(t0)				/* Store ullNextTime into compare register H. */
			ld t0, uxTimerIncrementsForOneTick  /* Load the value of ullTimerIncrementForOneTick into t0 (could this be optimized by storing in an array next to pullNextTime?). */
			add t4, t0, t2				/* Add ullNextTime to the timer increments for one tick. */
			sd t4, 0(t1)				/* Store ullNextTime. */
    #else
			/* Update the 64-bit mtimer compare match value. */
			ld t2, 0(t1)			 	/* Load ullNextTime into t2. */
			sd t2, 0(t0)				/* Store ullNextTime into compare register. */
			ld t0, uxTimerIncrementsForOneTick  /* Load the value of ullTimerIncrementForOneTick into t0 (could this be optimized by storing in an array next to pullNextTime?). */
			add t4, t0, t2				/* Add ullNextTime to the timer increments for one tick. */
			sd t4, 0(t1)				/* Store ullNextTime. */
    #endif

        // use _tx_thread_system_stack_ptr, switched in tx_thread_context_save.S
		//LOAD sp, xISRStackTop			/* Switch to ISR stack before function call. */
		jal _tx_timer_interrupt         /* threadx do not return yield flag */
        j context_restore               /* jump to _tx_thread_context_restore */

	test_if_external_interrupt:			/* If there is a CLINT and the mtimer interrupt is not pending then check to see if an external interrupt is pending. */
		addi t1, t1, 4					/* 0x80000007 + 4 = 0x8000000b == Machine external interrupt. */
		bne a0, t1, as_yet_unhandled	/* Something as yet unhandled. */

#endif /* portasmHAS_MTIME */

    // use _tx_thread_system_stack_ptr, switched in tx_thread_context_save.S
	//LOAD sp, xISRStackTop				/* Switch to ISR stack before function call. */
	jal portasmHANDLE_INTERRUPT			/* Jump to the interrupt handler if there is no CLINT or if there is a CLINT and it has been determined that an external interrupt is pending. */
	j context_restore                   /* jump to _tx_thread_context_restore */

handle_synchronous:
	addi a1, a1, 4						/* Synchronous so updated exception return address to the instruction after the instruction that generated the exeption. */

    /* Save updated exception return address. */
	LOAD    t0, _tx_thread_current_ptr   // Pickup current thread pointer
    beqz    t0, _tx_thread_idle_system   // If NULL, idle system was interrupted
	STORE   a1, 30*REGBYTES(sp)          // Save new mepc again 
	/* idle system do not need save mepc as it will directly jump to schedule */
_tx_thread_idle_system: 

test_if_environment_call:
	li t0, 11 							/* 11 == environment call. */
	bne a0, t0, is_exception			/* Not an M environment call, so some other exception. */
	j context_restore                   /* jump to _tx_thread_context_restore */

is_exception:
	csrr t0, mcause						/* For viewing in the debugger only. */
	csrr t1, mepc						/* For viewing in the debugger only */
	csrr t2, mstatus
		
	j is_exception						/* No other exceptions handled yet. */

as_yet_unhandled:
	csrr t0, mcause						/* For viewing in the debugger only. */
	
	j as_yet_unhandled

context_restore:
    j       _tx_thread_context_restore  /* Jump to ThreadX context restore function. Note: this does not return!*/

	.endfunc

3.定时器和定时器中断配置

Milk-V Duo的C906采用CLINT(核本地中断控制器)并通过比较系统定时器MTIME和定时器比较寄存器MTIMEMCMP值来触发定时器中断,即当mtime >= mtimecmp时,CLINT 会产生一个定时器中断(通过rdtime指令读取MTIME的影子寄存器TIME)。产生中断后,需要软件设置新的mtimecmp值,如上文中断处理中所属。

此外,由于C906的IO Bus宽度为32bit,故读取64bit的寄存器需要2个变量进行2次读取。而MTIMECMP寄存器的读写则是通过读写映射的内存地址,故需要定义2个32bit的指针变量。

MTIMECMP每次增长的值根据处理器频率和RTOS运行频率而定。

最后开启mie中机器模式的外部中断和定时器中断。

uint64_t ullNextTime = 0ULL;
const uint64_t *pullNextTime = &ullNextTime;

const size_t uxTimerIncrementsForOneTick = (size_t)((configSYS_CLOCK_HZ) / (configTICK_RATE_HZ)); /* Assumes increment won't go over 32-bits. */

uint32_t const ullMachineTimerCompareRegisterBase = configMTIMECMP_BASE_ADDRESS;

#if (defined THEAD_C906) && (!defined configMTIME_BASE_ADDRESS) && (configMTIMECMP_BASE_ADDRESS != 0)
volatile uint32_t *pulMachineTimerCompareRegisterL = NULL;
volatile uint32_t *pulMachineTimerCompareRegisterH = NULL;
#else
volatile uint64_t *pullMachineTimerCompareRegister = NULL;
#error define failed---------------------------------
#endif

void port_specific_pre_initialization(void)
{

    asm volatile("csrc mstatus, %0" ::"r"(0x08));  // disable enterrupt

// setup time
#ifdef THEAD_C906
    /* If there is a CLINT then it is ok to use the default implementation
    in this file, otherwise vPortSetupTimerInterrupt() must be implemented to
    configure whichever clock is to be used to generate the tick interrupt. */
    uint64_t ullCurrentTime;
    volatile uint32_t ulHartId;
    asm volatile("csrr %0, mhartid" : "=r"(ulHartId));

    // 32bit IO bus, need to get hi/lo seperately
    pulMachineTimerCompareRegisterL =
        (volatile uint32_t *)(ullMachineTimerCompareRegisterBase + (ulHartId * sizeof(uint64_t)));
    pulMachineTimerCompareRegisterH =
        (volatile uint32_t *)(ullMachineTimerCompareRegisterBase + sizeof(uint32_t) + (ulHartId * sizeof(uint64_t)));

    asm volatile("rdtime %0" : "=r"(ullCurrentTime));

    ullNextTime = (uint64_t)ullCurrentTime;
    ullNextTime += (uint64_t)uxTimerIncrementsForOneTick;
    *pulMachineTimerCompareRegisterL = (uint32_t)(ullNextTime & 0xFFFFFFFF);
    *pulMachineTimerCompareRegisterH = (uint32_t)(ullNextTime >> 32);

    /* Prepare the time to use after the next tick interrupt. */
    ullNextTime += (uint64_t)uxTimerIncrementsForOneTick;
#endif

// enable mtime
#if ((configMTIME_BASE_ADDRESS != 0) && (configMTIMECMP_BASE_ADDRESS != 0) || (defined THEAD_C906))
    {
        /* Enable mtime and external interrupts.  1<<7 for timer interrupt, 1<<11
        for external interrupt.  _RB_ What happens here when mtime is not present as
        with pulpino? */
        asm volatile("csrs mie, %0" ::"r"(0x880));
    }
#else
    {
        /* Enable external interrupts. */
        __asm volatile("csrs mie, %0" ::"r"(0x800));
    }
#endif /* ( configMTIME_BASE_ADDRESS != 0 ) && ( configMTIMECMP_BASE_ADDRESS != 0 ) */

}

4.上下文切换时mstatus寄存器的设置

Milk-V Duo的C906支持浮点运算,但ThreadX在RISC-V64中中断上下文恢复和任务调度器恢复任务上下文时对mstatus的处理依然和RISC-V32的一致,即直接向mstatus写入0x1880,从而恢复发生Trap前的权限MPP为Machine机器模式、全局中断状态MPIE为机器模式下的状态,并关闭所有模式的全局中断MIE。(Milk-V Duo小核也只工作在机器模式)。

但该代码会导致在实际的硬件上运行时,当运行到浮点寄存器保存代码时导致系统死机。

    li      t0, 0x1880                                  // Prepare MPIP
    csrw    mstatus, t0                                 // Enable MPIP

根据RISC-V的私有权限手册的内容,mstatus寄存器的各个bit功能如下。

其中FS[1:0]为浮点扩展F的相关内容,如果值为0,即关闭时,读写相关寄存器将会造成非法指令异常。(FS不同的状态主要是为了在任务切换时,如果浮点相关寄存器没有被修改过,即为clean就不需要额外压栈存储浮点相关的寄存器上下文,从而加快任务切换速度)

The FS field encodes the status of the floating-point unit state, including the floating-point registers f0–f31 and the CSRs fcsr, frm, and fflags.

……

If the F extension is implemented, the FS field shall not be read-only zero.

……

When an extension’s status is set to Off, any instruction that attempts to read or write the corresponding state will cause an illegal instruction exception.

ThreadX的RISC-V代码中只要编译器定义了浮点就会开启全部浮点寄存器的保存和恢复,而C906的启动代码Start.S中明确开启了浮点单元FPU,且SDK的编译器选项也包含F扩展指令。

	# enable fp
	li x3, 0x1 << 13
	csrs mstatus, x3

这里修改ThreadX的代码来避免该问题,使用按位清除和按位写入的指令设置0x1880,避免修改mstatus其他的bit。

#if defined(__riscv_float_abi_double) || defined(__riscv_float_abi_single)
    li      t0, 0x1880                                  // Set MPP, MPIE
    csrs   mstatus, t0                                  // avoid to clear FPU field
    li      t0, 0x77f
    csrc   mstatus, t0
#else
    li      t0, 0x1880                                  // Prepare MPIP
    csrw    mstatus, t0                                 // Enable MPIP
#endif 

由于csr系列的立即数只能12bit,小于0x1880所需bit位数,故需要使用li将0x1880先读入寄存器,再进行相关操作。

5.增加相关宏定义

主要增加增加移植的一些宏定义。

注意__ASSEMBLER__的条件编译。

// 全局定义, 主要针对汇编
#define portasmHAS_SIFIVE_CLINT 1
#define portasmHAS_MTIME 1
#define portasmHANDLE_INTERRUPT do_irq

// 针对C语言, 汇编代码不会包括
#define _WINDOWS_
#ifdef RISCV_QEMU
#define configMTIME_BASE_ADDRESS		( CLINT_ADDR + CLINT_MTIME )
#else
#undef configMTIME_BASE_ADDRESS  // get MTIME from rdtime
#endif
#define configMTIMECMP_BASE_ADDRESS		( CLINT_ADDR + CLINT_MTIMECMP )
#define configTICK_RATE_HZ				( ( unsigned long long ) 200UL )

#define TX_TIMER_TICKS_PER_SECOND configTICK_RATE_HZ

// cvitek/arch/riscv64/include/arch_cpu.h
#define configSYS_CLOCK_HZ              ( 25000000 )

#ifndef FAST_IMAGE_ENABLE
#define configTOTAL_HEAP_SIZE			( ( size_t ) 1 * 512 * 1024 )
#else
#define configTOTAL_HEAP_SIZE			( ( size_t ) 1 * 650 * 1024 )
#endif

#define TX_MINIMUM_STACK 1024

6.系统堆栈分配和测试demo

测试demo使用ThreadX官方的演示demo并增加串口打印内容方便观察结果。

ThreadX支持用户自定义一块内存作为ThreadX的内存池,并调用API从中分配任务和IPC所需的动态内存。(官方的示例Demo也是如此)

这里将该内存定位到链接脚本的heap段中。

UCHAR byte_pool_memory[DEMO_BYTE_POOL_SIZE] __attribute__ ( (section( ".heap" )) );

内存池创建、内存分配和任务创建代码如下。

#define DEMO_STACK_SIZE 1024
#define DEMO_BYTE_POOL_SIZE configTOTAL_HEAP_SIZE

TX_THREAD thread_0;
void thread_0_entry(ULONG thread_input);

/* Define what the initial system looks like.  */
void tx_application_define(void *first_unused_memory)
{
	(void)first_unused_memory;
    CHAR* pointer = TX_NULL;

	int ret;

    /* Create a byte memory pool from which to allocate the thread stacks.  */
    ret = tx_byte_pool_create(&byte_pool_0, "byte pool 0", byte_pool_memory, DEMO_BYTE_POOL_SIZE);
	
	IS_TX_ERROR(ret);

    /* Put system definition stuff in here, e.g. thread creates and other assorted
       create information.  */

    /* Allocate the stack for thread 0.  */
    ret = tx_byte_allocate(&byte_pool_0, (VOID **) &pointer, DEMO_STACK_SIZE, TX_NO_WAIT);

	IS_TX_ERROR(ret);

    /* Create the main thread.  */
    ret = tx_thread_create(&thread_0, "thread 0", thread_0_entry, 0,  
            pointer, DEMO_STACK_SIZE, 
            1, 1, TX_NO_TIME_SLICE, TX_AUTO_START);
}

详细的demo代码见仓库的/cvitek/task/comm/src/riscv64/comm_main.c

7.基于Milk-V Duo官方SDK的编译构建

按照本文仓库中的README操作即可,主要为将RTOS目录(即顶层的threadx目录)下CMake的编译路径从freertos改为threadx,以及修改Milk-V Duo官方SDK中duo-buildroot-sdk/build/milkvsetup.sh脚本的FREERTOS_PATH环境变量为本代码的相对路径。

#FREERTOS_PATH="$TOP_DIR"/freertos # 修改此项
FREERTOS_PATH="$TOP_DIR"/threadx

建议先完整构建一遍Milk-V Duo官方的SDK,并向SD卡中烧写编译好镜像或烧写官方的Release镜像也行。构建完成官方的SDK后替换并构建本文的threadx仓库,使用新生成的fip.bin覆盖掉旧的即可。

移植效果

将编译得到的新的fip.bin文件拷贝至启动SD卡的boot分区(如先前SD卡中已有fip.bin则将其覆盖)。

开发板的串口Uart0连接上USB转TTL模块,并连接PC,PC开启串口调试软件(这里使用的是MobaXterm),最后连接上Type-C口的电源线即可启动开发板,系统启动后串口将被Linux和ThreadX共用(Milk-V Duo的SDK中串口已经做好互斥保护)。打印结果如下。

参考

Milk-V Duo 官方Wiki:https://milkv.io/zh/docs/duo/overview

Milk-V Duo repo: https://github.com/milkv-duo/duo-buildroot-sdk/

ThreadX repo:https://github.com/eclipse-threadx

milk-v duo 编译流程二之小核 FreeRTOS 编译

聊一聊 MilkV-Duo 的启动流程

  • 20
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值