为Quard-star移植FreeRTOS-移植代码构建
移植FreeRTOS
1. 内存布局与移植代码架构
1.1 内存启动地址修改
之前我们通过OpenSBI
划分了Domain
,HART7
是留给FreeRTOS
的,起始地址设置为了0xb0000000
,我们修改一下,将FreeRTOS
的起始地址设置为0xBF800000
,第一个修改的代码为boot
的start.s
基于opensbi为quard_star创建domain | TimerのBlog (yanglianoo.github.io)
//load trusted_fw.bin |
trusted_fw.bin
我们是写入到flash
的0x20400000
地址处的,大小为4k
然后需要对应修改设备树的下级跳转地址:
可以看见在设备树中将trusted-domain
的启动地址设置成了0xBF800000
,启动的核为cpu7
,next-mode = 1
代表下级程序启动的cpu
模式为s
模式,我们是将FreeRTOS
移植到S
态而不是M
态
1.2 移植文件架构
原本trusted_domain
目录下就只有statup.S
和一个Makefile
用于在uart2
上输出一些字符,添加移植文件后文件夹中文件如上图:
driver
:串口驱动代码FreeRTOS-Kernel
: FreeRTOS内核代码riscv
:和riscv
架构相关的代码FreeRTOSConfig.h
:FreeRTOS的配置文件main.c
:主应用程序Makefile,link.lds
:编译trusted_domain
编译规则和链接文件startup.S
:启动汇编,执行一些初始化工作然后跳转到FreeRTOS内核
2. 平台架构相关代码剖析
2.1 driver
在进行domain
划分时,设备树文件有这样一个配置:
stdout-path = "/soc/uart0@10000000"; |
这里是告知OpenSBI
标准输出将被重定向到位于内存地址 0x10000000
的UART设备上,这个uart
设备我们在下面定义了:
uart0: uart0@10000000 { |
在cpu7
即trusted_domain
的配置中,我们使用的是uart2
,因此我们需要手动编写uart2
的串口驱动代码而不是使用OpenSBI
的标准输入输出串口,uart0
已经被我们的TimerOS
使用了,根据设备树可知,uart2
是ns16550a
这款芯片,qemu
默认的虚拟串口也是使用的这款芯片,我们先来看看这款串口芯片的芯片手册:
- 总共有10个寄存器,但是有的寄存器是可以复用的,
A2 A1 A0
代表此寄存器的映射地址,实际去操作此寄存器的时候需要加上串口设备的偏移地址,这里是uart2 : 0x10002000
,每个寄存器都是8个bit
ns16550a
串口芯片的编程手册我放在了ref
目录下,可以去看看这个视频:第7章(下)-Hello RVOS_哔哩哔哩_bilibili
接下来来看看driver
目录下的代码:
quard_star.h
|
- 定义了一些宏定义的值,主要是
#define NS16550_ADDR CONS(0x10002000, UL)
定义了串口的映射地址
ns16550.c
|
定义了
ns16550_tx
函数来向串口写入一个字节的数据,NS16550
的数据寄存器为RHR
和THR
,他们都在偏移地址为0处,可复用为读模式和写模式LSR
寄存器有8个bit位,用于反应串口的硬件状态,bit5
具体代表“Transmitter Holding Register Empty”,当LSR_THRE
为1时,它表示发送保持寄存器为空,UART准备好接受新的字节进行发送。因此在ns16550_tx
发送数据时需要先判断此bit位,当此bit位为1时说明可以写入新的字节数据,THR
8个bit刚好代表一个字节,写入的一个字节数据就放入了THR
debug_log.c
|
debug_log.c
中定义了一个_puts
函数用于将一个字符串一个字节一个字节的写入到串口中进行输出,返回输出的字符数
2.2 riscv
riscv
目录下是和架构以及OpenSBI
相关的代码,我们在移植FreeRTOS到cpu7
的S态时需要用到OpenSBI
的一些和中断相关的功能,因此需要首先添加调用OpenSBI
服务的代码:sbi.c
和sbi.h
,这里我直接给出代码,和之前Timeros
一样
|
|
riscv_reg.h
|
__riscv_xlen
是编译器预定义的宏,使用宏定义的方式来控制在32位和64位平台上编译的统一性- 定义了加载和存储指令以及寄存器大小
sbi_const.h
|
- 定义了一些宏用于添加符号后缀、生成
bitmask
、宏参数转换成字符串等功能
/* |
定义了和
riscv
架构寄存器相关的代码,在riscv
架构中,各个特权级的控制寄存器是被统一映射到了一个确定的地址,可以直接操作这个地址来操作csr
,可以在官方文档中找到The RISC-V Instruction Set Manual
,我将此文档放在了ref
目录下,举个例子可以看见上面文档中定义了控制寄存器对应的映射地址,在
riscv_encoding.h
做了对应的定义:/* ===== Supervisor-level CSRs ===== */
/* Supervisor Trap Setup */
/* Supervisor Trap Handling */
/* Supervisor Protection and Translation */
riscv_encoding.h
|
- 定义了一些读取控制寄存器的函数,使用内联汇编的方式来实现
3. FreeRTOS移植相关代码
3.1 FreeRTOS
源码组织
移植之前先看一下FreeRTOS
的源码组织架构:
Demo
为各个架构平台的示例代码,可以整个删除掉source/portable
:此文件夹下的文件就是FreeRTOS
适配不同平台的代码
以STM32
为例子,源码目录构建如下
我们的目标是为riscv
做移植,因此只需要实现portable
文件夹下所需文件,核心源文件和include
文件架下的头文件复制即可,我们使用的GCC编译,不是使用keil
、IAR
这样的IDE
平台,因此如下图portable
文件夹下只需要保留两个文件夹的内容即可,其他全部删除
在GCC
目录下也有很多平台的实现,我们全部删除,新建一个RISC-V
目录,我们移植相关的代码就放在此文件夹下,在MenMang
目录下有5个用于分配内存的源文件,我们使用一个就行,保留heap_4.c
,其余删除。剪枝后trusted_domain
的代码目录为:
3.2 FreeRTOS配置
FreeRTOS
是支持很多配置的,需要实现定义一些宏,用于开启和关闭 FreeRTOS
内核的某些功能,这些宏定义在FreeRTOSConfig.h
中
|
比较重要的几个配置为:
configCPU_CLOCK_HZ
: RTC 的时钟频率configTICK_RATE_HZ
:时钟中断的频率configUSE_PREEMPTION
:开启抢占式任务调度
在qemu
中rtc
的时钟频率为设备树中定义的换算成10进制即10000000
,时钟中断的频率可以配置。
关于时钟的使用和定义:分时多任务系统与抢占式调度 | TimerのBlog
3.3 portable
目录下移植文件
明确我们的目标是移植FreeRTOS
到RISCV的S态运行,M态运行的是OpenSBI
,在官方的移植文件中:portable/GCC/RISC-V
目录下有移植的Demo
,但是官方是移植FreeRTOS
到M态运行,我们参考着官方的移植代码来进行实现S态的移植,移植的文件涉及到三个:portmacro.h
、port.c
、portASM.S
,我先直接给出这三个和架构相关的移植代码,在下一小节中我们从FreeRTOS的启动依次来详细描述内部的函数细节
portmacro.h
|
这里说几个有点坑的地方:
一是使用
configUSE_PORT_OPTIMISED_TASK_SELECTION
控制的使用一个32bit
的位图来存储优先级的几个操作宏,如果位图中的某一位被置为了1,代表此优先级上有任务就绪portRECORD_READY_PRIORITY(uxPriority, uxReadyPriorities)
:设置位图中相应优先级的位,表示该优先级有任务就绪。portRESET_READY_PRIORITY(uxPriority, uxReadyPriorities)
:清除位图中相应优先级的位,表示该优先级没有任务就绪。portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities )
这个宏用于确定位图中的最高的就绪优先级,因为在FreeRTOS
中,数字越大优先级越高,那么在位图中从高位向低位搜寻判断即可,因此调用了ucPortCountLeadingZeros(uint32_t x)
去计算前导0的个数,然后使用31
减去前导0的个数就可以知道此时最高的就绪优先级,在官方的移植代码中是使用编译器的一个函数__builtin_clz
来计算前导0的个数的:但是不知道为啥,会有问题,估计是编译器的问题,于是自定义了一个计算前导0的函数
二是任务主动请求调度的函数,在M态的话直接使用
ecall
指令即可,此时就会触发软中断,但是如果在S
态使用ecall
则会导致特权级切换,而且S态不能直接访问或者控制中断控制器如CLINT或者PLIC,所以需要M态固件介入来处理跨HART的中断请求。/* 通过sbi触发自己核心的软中断 */
extern void sbi_send_ipi(const unsigned long *hart_mask);
//#define portYIELD() __asm volatile( "ecall" ); 原本是在m态通过ecall触发软中断- 在M态,可以通过设置M态的中断挂起寄存器(
mip
)中的SSIP位来请求S态的软件中断。这需要M态的代码(通常是固件或操作系统的更低层部分)显式进行。 - SSIP位设置后,如果S态已经启用软中断(通过
sie
寄存器的SSIP位)且全局中断使能(通过mstatus
或sstatus
寄存器的全局中断使能位),则S态的软件中断被触发。 - 这里我就有个问题了,多核间的寄存器访问核设置
OpenSBI
是如何实现的,后面搞明白了在叙述吧,总之这里就是通过sbi_send_ipi
就可以实现触发(0x1<<PRIM_HART))
这个核心的软中断,即促发cpu7
的软中断从而实现主动的任务切换。
- 在M态,可以通过设置M态的中断挂起寄存器(
portASM.S
#include "riscv_encoding.h" |
portASM.S
是移植到riscv架构的核心文件,在FreeRTOS中,每个任务都有自己的栈,这是在创建任务的时候分配的,所谓的切换任务就是将当前任务的任务上下文即寄存器的值压入自己的栈中,然后取出下一个要运行的任务的地址,从此任务的栈中将任务上下文恢复,然后跳转执行。FreeRTOS和我们的TimerOS
一样都是通过RTC中断来实现任务切换的,而不同架构由于寄存器组不一样因此需要在portASM.S
实现任务压栈和出栈的汇编函数,以及和处理中断有关的函数
port.c
|
4. FreeRTOS启动过程
4.1 FreeRTOS使用实例
我在main.c
中创建了三个任务,三个任务之间会使用消息队列进行通信:
|
4.2 编译和链接
首先来看Makefile
,Makefile
会将trusted_domain
下的源文件编译生成一个固件,会对FreeRTOS
内核的代码以及我们自己编写的源文件进行编译:main.c
,startup.S
,riscv/**
,FreeRTOS-Kernel/**
,driver/**
,编译后生成的固件会放在build
目录下
########################################################################################################################## |
链接脚本如下:
OUTPUT_ARCH( "riscv" ) |
指定了程序入口地址为
_start
为start.S
中的代码写入的内存区域有两部分:
rom (rxa) : ORIGIN = 0xBF800000, LENGTH = 1M
:可读可执行,起始地址为0xBF800000
,大小为1Mram (wxa) : ORIGIN = 0xBF900000, LENGTH = 6M
:可写可执行,起始地址为0xBF900000
,大小为6M
链接脚本中
AT
的是用来定义各个节应该在哪里被加载(LMA),而节的位置(放置位置,VMA)则是通过>
指定的,例如.data.start :
{
_data_lma = LOADADDR(.data.start);
} >ram AT>rom.data
节包含了初始化的全局变量,它在运行时应位于 RAM(通过>ram
指定)。- 然而,在程序加载时,这些数据的初始值存储在 ROM 中(通过
AT>rom
指定)。 - 简单来说就是数据段的数据是放在
rom
中的,代码段的程序在运行时需要去rom
中拿到数据后加载到ram
处执行,.data
段和.bss
段都是这样的特性
在链接脚本最后定义了内核程序使用的主栈栈顶起始地址
_stack_top
,栈大小为__stack_size = 0x4000
4.3 启动代码
startup.S
#include "riscv_encoding.h" |
- 启动代码主要做一些初始化工作:首先禁用所有中断,然后设置主栈指针,加载
rom
上的数据段到ram
里,将bss
段清零。bss
段通常放的都是未初始化的全局变量,因此os手动清零 - 跳转到
main
函数开始执行:jal main
4.4 FreeRTOS任务启动
在main
函数中可以看见当创建完成任务后,会调用vTaskStartScheduler()
函数来开启任务调度:
我们进入vTaskStartScheduler()
内部,可以看见首先创建了一个IDLE
任务,即空闲任务,如果我们用户没用手动创建任务,那么IDLE
任务会一直运行,此任务被设置为最低优先级0
然后就会开启时钟中断,然后启动第一个任务:
先调用
portDISABLE_INTERRUPTS()
这个宏来关闭中断,然后在xPortStartScheduler()
函数中构建好第一个任务执行的上下文后,会打开中断,这两个和cpu架构有关的宏或者函数就是定义在之前portable
文件夹下的
xPortStartScheduler()
定义在port.c
中,在此函数中会恢复第一个要执行任务的任务上下文,开启时钟中断,开始执行调度。
/*-----------------------------------------------------------*/ |
调用
vPortSetupTimerInterrupt();
设置下次时钟中断的计数值const size_t uxTimerIncrementsForOneTick = ( size_t ) ( ( configCPU_CLOCK_HZ ) / ( configTICK_RATE_HZ ) ); /* Assumes increment won't go over 32-bits. */
void vPortSetupTimerInterrupt( void )
{
/* 通过sbi设置下次tick中断 */
sbi_set_timer(get_ticks() + uxTimerIncrementsForOneTick);
}uxTimerIncrementsForOneTick
为下次时钟中断到来时的计数值,通过时钟频率和在FreeRTOSConfig.h
中定义的时钟中断频率来计算使能SIE中S模式Timer中断和Soft中断,此时用于没有开启S态的全局中断,所以不会立即响应,会在
xPortStartFirstTask();
函数中恢复第一个任务的上下文后才开启
xPortStartFirstTask()
定义在portASM.S
中:
.align 8 |
首先设置异常处理地址,即设置
stvec
寄存器的值la t0, freertos_risc_v_trap_handler //设置异常处理地址
csrw stvec, t0下一次
trap
来临时就会进入freertos_risc_v_trap_handler
这个地址去进行处理,和我们TimerOS
实现类似,在freertos_risc_v_trap_handler
函数内部会判断是什么类型的异常,比如时钟中断、外部中断、异常等等,如果是时钟中断,则会去执行任务切换然后是根据当前任务的
TCB
的指针,拿到此任务的栈地址,根据任务TCB
的定义发现栈指针是放在TCB
最开始的位置所以先取出
TCB
的指针,再从TCB
的第一个地址取出任务栈,从此任务的栈中恢复任务上下文,pxCurrentTCB
是FreeRTOS
中定义的一个全局指针,用来指向当前正在执行的任务的TCB
,由于我们最后创建的任务是IDLE
任务,所以此时pxCurrentTCB
肯定是指向IDLE
任务的TCB
的,xTaskCreate
内部会去调用prvAddNewTaskToReadyList
来初始化设置pxCurrentTCB
的值此时你可能有一个疑问,既然要从栈中恢复任务上下文,是不是在创建任务的时候需要先为每个任务初始化任务上下文,那就对了,
xTaskCreate
内部会去调用prvInitialiseNewTask
函数,而中prvInitialiseNewTask
会去调用pxPortInitialiseStack
函数来初始化任务栈
pxPortInitialiseStack
此函数用于初始化一个任务的任务栈,定义在portASM.S
中:
.align 8 |
首先来看调用此函数的调用时传入的参数和返回值
pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxTaskCode, pvParameters );
pxTopOfStack
:任务栈的栈顶地址,portSTACK_GROWTH
这个宏用于判断栈的增长方向,此宏在portmacro.h
中被定义成了-1
,因此往地址增长,pxTopOfStack
指向了栈顶地址,注意:
pxTopOfStack
是个临时变量,指向此时栈的最低地址,不是TCB
结构体顶部的那个pxTopOfStack
pxTopOfStack
会被放入a0
寄存器中,pxTaskCode
放入a1
寄存器中,pvParameters
放入a2
寄存器中
pxPortInitialiseStack
函数内部会依次压栈初始化任务上下文,栈中的数据如下:此时再来回看
xPortStartFirstTask
中的恢复第一个任务的上下文就很清晰了,上面提到xPortStartFirstTask
函数内部会在恢复上下文后开启全局中断,这一步就发生在csrrw x0, sstatus, x5 /* Interrupts enabled from here! */
,我们在初始化任务上下文时会构造一个sstatus
的初始值,会去设置相应的bit位来开启S态的全局中断,这样将ssatus
的值从栈中恢复时就会开启全局中断了,从而开启基于RTC中断的任务调度csrr t0, sstatus /* t0 = sstatus 保存当前sstatus寄存器的值到t0寄存器中 */
andi t0, t0, ~0x2 /* Ensure interrupts are disabled when the stack is restored within an ISR. Required when a task is created after the schedulre has been started, otherwise interrupts would be disabled anyway. */
addi t1, x0, 0x120 /* Generate the value 0x120, which are the SPIE and SPP bits to set in sstatus. */
or t0, t0, t1 /* Set SPIE and SPP bits in sstatus value. */
回顾一下整个启动流程:
4.5 内核启动后的任务切换
在调用xPortStartFirstTask()
之后,此时会启动IDLE
任务,同时开启了时钟中断,那么在下一次时钟中断到来时,我们需要去进行处理,在xPortStartFirstTask()
函数的开头我们设置了stvec
的值,当产生trap
时会跳转到stvec
寄存器指向的地址处执行,这个地址是freertos_risc_v_trap_handler
,这个函数定义在portASM
的开头:
.align 8 |
我当前有一个任务正在暂用
cpu
运行,此时时钟中断来临,会打断当前任务的执行,然后执行任务切换,因此在中断处理函数中,需要先保存当前任务的上下文,x2、x3、x4
不需要保存,保存任务上下文时先增加sp
的值,增加的大小为portCONTEXT_SIZE
,使用store_x
依次保存寄存器后,将此时任务的栈顶地址保存到pxNewTCB->pxTopOfStack
中,用于在任务恢复时找到栈顶保存完毕任务上下文后,将
scause
写入了a0
寄存器,sepc
寄存器写入了a1
寄存器,用于后续针对不同类型的异常进行处理//判断是中断还是异常
test_if_asynchronous: //参数为 scause的值,存放在a0寄存器中
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. */
store_x a1, 0( sp )srli
是右移位指令,scause
的最高位表明了此次trap
是中断还是异常,因此将a0
寄存器右移__riscv_xlen - 1
位后保存到a2
寄存器中,此时a2
寄存器中就保存了scause
的最高位- 如果
a2 == 0
则表示是异常,跳转到handle_synchronous
处执行可以看见为空,否则为中断,将sepc
寄存器的值写入到栈顶位置
如果为中断则会继续向下执行
test_if_ipi
标签处的代码,这里会根据scause
的最后一位判断是软件中断还是其他中断,如果是软件中断则会执行:load_x sp, xISRStackTop /* 切换到ISR(中断服务例程)专用的堆栈。 */
jal vPortClearIpiInterrupt
jal vTaskSwitchContext //
j processed_source如果是软件中断会切换到
ISR
专用的栈:xISRStackTop
,这是在port.c
中定义的一片内存的最高地址。然后跳转到
vPortClearIpiInterrupt
函数处执行,此函数也是定义在port.c
中上面提到过任务可以通过调用
portYIELD()
来手动触发软中断,从而实现任务切换,此时就是这样的情况,在清楚软中断的标志位后,就会去执行任务切换,调用vTaskSwitchContext
,这是FreeRTOS
内部定义的选择下一个可执行任务上下文的函数,去看源码就发发现此函数从就绪链表中挑选出最高优先级的任务,然后将全局任务指针pxCurrentTCB
指向此任务的TCB
,然后就可以从此要执行任务的栈中恢复上下文了,然后跳转执行,恢复任务上下文的函数是processed_source
如果不是主动触发的软中断,则就跳转到
test_if_mtimer
处执行,此时会判断是时钟中断还是其他外部中断,如果是时钟中断,此时也要进行任务切换,同样切换ISR专用栈,不过需要设置下一次时钟中断到来的计数值,通过调用vPortSetupTimerInterrupt
函数实现,然后调用FreeRTOS
内部定义的xTaskIncrementTick
函数,此函数用于告知系统增加了依次时钟计数,其内部机制我们暂不剖析,主要是用于延时和任务调度相关的。然后调用vTaskSwitchContext
选择下一个就绪任务,恢复任务上下文,跳转执行test_if_mtimer: /* If there is a CLINT then the mtimer is used to generate the tick interrupt. */
addi t1, t1, 4 /* 0x80000001 + 4 = 0x80000005 == Supervisor timer interrupt. */
bne a0, t1, test_if_external_interrupt
/* 处理时钟中断 */
load_x sp, xISRStackTop /* 切换到ISR(中断服务例程)专用的堆栈。 */
jal vPortSetupTimerInterrupt /* 设置定时器中断计数 */
jal xTaskIncrementTick
beqz a0, processed_source /* Don't switch context if incrementing tick didn't unblock a task. */
jal vTaskSwitchContext
j processed_source如果是外部中断的话,会跳转到
handle_interrupt
进行处理,如果是其他中断会跳转到processed_trap
处理我们重点来看任务恢复函数
processed_source
,通过执行vTaskSwitchContext
,将pxCurrentTCB
指向了下一个要执行任务的TCB
,通过pxCurrentTCB
指针就可以访问到要执行任务的任务上下文的栈空间,因此恢复任务上下文,通过sret
指令会跳转到sepc
寄存器的地址执行,sepc
寄存器在任务被中断后会自动设置,保存了中断返回后指令地址,在freertos_risc_v_trap_handler
函数中进行了压栈,因此从栈中恢复后,sret
指令就能跳转到正确的地址执行了processed_source:
load_x t1, pxCurrentTCB /* Load pxCurrentTCB. */
load_x sp, 0( t1 ) /* Read sp from first TCB member. */
/* Load sret with the address of the next instruction in the task to run next. */
load_x t0, 0( sp )
csrw sepc, t0
/* Load mstatus with the interrupt enable bits used by the task. */
load_x t0, 29 * portWORD_SIZE( sp ) //读取保存在栈中的sstatus寄存器的值
csrw sstatus, t0 /* Required for SPIE bit. */
//从栈中恢复上下文寄存器
load_x x1, 1 * portWORD_SIZE( sp )
load_x x5, 2 * portWORD_SIZE( sp ) /* t0 */
load_x x6, 3 * portWORD_SIZE( sp ) /* t1 */
load_x x7, 4 * portWORD_SIZE( sp ) /* t2 */
load_x x8, 5 * portWORD_SIZE( sp ) /* s0/fp */
load_x x9, 6 * portWORD_SIZE( sp ) /* s1 */
load_x x10, 7 * portWORD_SIZE( sp ) /* a0 */
load_x x11, 8 * portWORD_SIZE( sp ) /* a1 */
load_x x12, 9 * portWORD_SIZE( sp ) /* a2 */
load_x x13, 10 * portWORD_SIZE( sp ) /* a3 */
load_x x14, 11 * portWORD_SIZE( sp ) /* a4 */
load_x x15, 12 * portWORD_SIZE( sp ) /* a5 */
load_x x16, 13 * portWORD_SIZE( sp ) /* a6 */
load_x x17, 14 * portWORD_SIZE( sp ) /* a7 */
load_x x18, 15 * portWORD_SIZE( sp ) /* s2 */
load_x x19, 16 * portWORD_SIZE( sp ) /* s3 */
load_x x20, 17 * portWORD_SIZE( sp ) /* s4 */
load_x x21, 18 * portWORD_SIZE( sp ) /* s5 */
load_x x22, 19 * portWORD_SIZE( sp ) /* s6 */
load_x x23, 20 * portWORD_SIZE( sp ) /* s7 */
load_x x24, 21 * portWORD_SIZE( sp ) /* s8 */
load_x x25, 22 * portWORD_SIZE( sp ) /* s9 */
load_x x26, 23 * portWORD_SIZE( sp ) /* s10 */
load_x x27, 24 * portWORD_SIZE( sp ) /* s11 */
load_x x28, 25 * portWORD_SIZE( sp ) /* t3 */
load_x x29, 26 * portWORD_SIZE( sp ) /* t4 */
load_x x30, 27 * portWORD_SIZE( sp ) /* t5 */
load_x x31, 28 * portWORD_SIZE( sp ) /* t6 */
addi sp, sp, portCONTEXT_SIZE
sret
.endfunc
5. 编译运行测试
在build.sh
中需要将执行编译trusted_domain
,并将生成的固件打包,主要修改的地方为:
固件打包的地方不用修改,还是写入到原本的地址
运行测试:如下三个任务,两个任务写队列,一个任务接收队列消息,运行成功