一、ESP-IDF结构分析

能够清楚的认识IDF框架的结构对之后移植RT-thread具有很大的帮助

以下结构均为本文作者通过查询手册、网上搜索和查看源码自行分析而来,不保证权威性与正确性,如有问题欢迎联系修改

总体结构:

  • components:各种组件目录
    • esp32:esp32启动前代码
    • freertos:freertos组件
  • make:整个系统总的Makefile
    • project.mk:整个系统工具链配置(CFLAGS等参数)
  • tools:构建工具及脚本
    • cmake:cmake配置
      • build.cmake:整个系统工具链配置(CFLAGS等参数)
  • docs:帮助文档

重点关注components/freertos组件,其目录为:

  • include/freertos:头文件

  • CMakeLists.txt:组件cmake

  • component.mk:组件Makefile

  • portasm.S:freertos移植汇编文件(该文件需重点关注)

    • port_IntStack:中断栈底
    • port_IntStackTop:中断栈顶
    • port_switch_flag:调度标志
    • _frxt_setup_switch()
    • _frxt_int_enter():XT_RTOS_INT_ENTER宏的具体实现,进入中断,用来保存其余的中断上下文并进入RTOS,设置C环境
    • _frxt_int_exit():XT_RTOS_INT_EXIT宏的具体实现,退出中断,开始调度(此时仍在中断中,调度器将返回到XT_STK_EXIT/XT_SOL_EXIT处保存在中断栈帧中的退出点)
    • _frxt_timer_int():systick中断
    • _frxt_tick_timer_init():systick初始化
    • _frxt_dispatch():上下文切换(核心调度函数,真正的上下文切换在这里)
    • vPortYield():调度函数
    • vPortYieldFromInt():从中断中调度
    • _frxt_task_coproc_state():协处理器状态
  • xtensa_context.S:该文件中是一些上下文保存和恢复(包括协处理器)相关的函数

    • _xt_context_save()
    • _xt_context_restore()
    • _xt_coproc_init()
    • _xt_coproc_release()
    • _xt_coproc_savecs()
    • _xt_coproc_restorecs()
  • xtensa_init.c:freertos中xtensa初始化相关函数

    • _xt_tick_divisor_init()
    • xt_clock_freq()
  • xtensa_intr.c:主要用于注册中断

    • xt_set_exception_handler()
    • xt_set_interrupt_handler()
  • xtensa_intr_asm.S:上述文件的汇编部分

    • _xt_intdata:下面两个变量的起始地址
    • _xt_intenable
    • _xt_vpri_mask
    • _xt_interrupt_table ([0]=xt_unhandled_interrupt):中断向量表
    • _xt_exception_table ([0]=xt_unhandled_exception):异常向量表
    • xt_ints_on()
    • xt_ints_off()
  • xtensa_overlay_os_hook.c:

    • xt_overlay_init_os()
    • xt_overlay_lock()
    • xt_overlay_unlock()
  • xtensa_vector_defaults.S:xtensa中默认的中断

    • _xt_debugexception():调试异常中断
    • _xt_highint2()
    • _xt_highint3()
    • _xt_highint4()
    • _xt_highint5()
    • _xt_highint6()
    • _xt_nmi()
  • xtensa_vectors.S:xtensa中的中断向量(重要)

    • get_percpu_entry_for reg scratch 宏
    • extract_msb aout ain 宏
    • dispatch_c_isr level mask 宏:当中断的C语言环境设置好后通过该宏来调用注册的具体的C语言函数
      • .L_xt_user_int_&level&
      • .L_xt_user_int_timer_&level&

    • _xt_panic():panic异常中断
    • _xt_intexc_hooks
    • _DebugExceptionVector():调用xt_debugexception()
    • _DoubleExceptionVector():最终调用_xt_panic()
    • _KernelExceptionVector():-> _xt_kernel_exc() -> _xt_panic()
    • _UserExceptionVector(): -> _xt_user_exc():
      • 有可能调用_xt_lowint1()
      • 保存一部分上文
      • _xt_context_save()
      • 调用_xt_intexc_hooks中的钩子函数(需要XT_INTEXC_HOOKS宏)
      • 调用_xt_exception_table表中注册的中断
      • _xt_context_restore()
      • 恢复一部分上文
    • _xt_user_exit():调度退出时,在这里修改最后的ps、pc、sp(stack pointer)值,修改完毕后立马返回,之后就执行新的线程
    • _xt_coproc_sa_offset
    • _xt_coproc_owner_sa
    • _xt_lowint1():私有函数(1级中断函数
      • 保存一部分上文(设置出口点为_xt_user_exit())
      • 调用XT_RTOS_INT_ENTER宏
      • dispatch_c_isr 1 XCHAL_INTLEVEL1_MASK
      • 调用XT_RTOS_INT_EXIT宏

    • _Level2Vector(): -> _xt_medint2():
      • 保存一部分上文(设置出口点为_xt_medint2_exit())
      • 调用XT_RTOS_INT_ENTER宏
      • dispatch_c_isr 2 XCHAL_INTLEVEL2_MASK
      • 调用XT_RTOS_INT_EXIT宏
    • _xt_medint2_exit()
    • _Level3Vector(): -> _xt_medint3()
    • _xt_medint3_exit()
    • _Level4Vector(): -> _xt_medint4()
    • _xt_medint4_exit()
    • _Level5Vector(): -> _xt_medint5()
    • _xt_medint5_exit()
    • _Level6Vector(): -> _xt_medint6()
    • _xt_medint6_exit()

    • _Level2Vector(): -> xt_highint2()
    • _Level3Vector(): -> xt_highint3()
    • _Level4Vector(): -> xt_highint4()
    • _Level5Vector(): -> xt_highint5()
    • _Level6Vector(): -> xt_highint6()

    • _NMIExceptionVector(): -> xt_nmi()
    • _WindowOverflow4
    • _WindowUnderflow4
    • _WindowOverflow8
    • _WindowUnderflow8
    • _WindowOverflow12
    • _WindowUnderflow12
    • call_user_start():空函数

二、CPU架构移植

参考RT-thread官方手册进行cpu架构移植,其中需要移植以下函数:

函数和变量 描述
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 线程

1.1 中断相关

rtt中中断相关函数为rt_base_t rt_hw_interrupt_disable(void);void rt_hw_interrupt_enable(rt_base_t level);,即关闭和打开全局中断,rtt内核要求:

在 rt_hw_interrupt_disable() 函数里面需要依序完成的功能是:

  • 保存当前的全局中断状态,并把状态作为函数的返回值
  • 关闭全局中断

在 rt_hw_interrupt_enable(rt_base_t level) 里:

  • 将变量 level 作为需要恢复的状态,覆盖芯片的全局中断状态
  • 恢复全局中断

在IDF框架中已经有如下函数能够参考:

不带返回值:

  • include\freertos\portmacro.h中的portDISABLE_INTERRUPTS宏(关闭中断,禁用所有可屏蔽中断):

    1
    #define portDISABLE_INTERRUPTS()      do { XTOS_SET_INTLEVEL(XCHAL_EXCM_LEVEL); portbenchmarkINTERRUPT_DISABLE(); } while (0)
    • idf中XCHAL_EXCM_LEVEL为3(PS.EXCM屏蔽的级别)

    • 其中portbenchmarkINTERRUPT_RESTORE宏为freertos的插件,idf中未启用,为空

    • XTOS_SET_INTLEVEL的具体实现(设置PS.INTLEVEL):

      1
      2
      3
      4
      # define XTOS_SET_INTLEVEL(intlevel)		({ unsigned __tmp; \
      __asm__ __volatile__( "rsil %0, " XTSTR(intlevel) "\n" \
      : "=a" (__tmp) : : "memory" ); \
      __tmp;})
  • include\freertos\portmacro.h中的portENABLE_INTERRUPTS宏(使能中断):

    1
    #define portENABLE_INTERRUPTS()       do { portbenchmarkINTERRUPT_RESTORE(0); XTOS_SET_INTLEVEL(0); } while (0)

带返回值:

  • include\freertos\portmacro.h中的portSET_INTERRUPT_MASK_FROM_ISR宏(关闭中断):

    1
    2
    3
    4
    5
    6
    7
    static inline unsigned portENTER_CRITICAL_NESTED() {
    unsigned state = XTOS_SET_INTLEVEL(XCHAL_EXCM_LEVEL);
    portbenchmarkINTERRUPT_DISABLE();
    return state;
    }

    #define portSET_INTERRUPT_MASK_FROM_ISR() portENTER_CRITICAL_NESTED()
  • include\freertos\portmacro.h中的portCLEAR_INTERRUPT_MASK_FROM_ISR宏(使能中断):

    1
    2
    3
    #define portEXIT_CRITICAL_NESTED(state)   do { portbenchmarkINTERRUPT_RESTORE(state); XTOS_RESTORE_JUST_INTLEVEL(state); } while (0)

    #define portCLEAR_INTERRUPT_MASK_FROM_ISR(state) portEXIT_CRITICAL_NESTED(state)

1.2 线程栈初始化

在动态创建线程和初始化线程的时候,会使用到内部的线程初始化函数_rt_thread_init()_rt_thread_init()函数会调用栈初始化函数rt_hw_stack_init(),在栈初始化函数里会手动构造一个上下文内容,这个上下文内容将被作为每个线程第一次执行的初始值。上下文在栈里的排布如下图所示(Cortex-M架构):

Cortex-M架构的栈中的上下文信息

xtensa架构中栈也是自顶向下增长,堆为自底向上增长

xtensa架构中初始化第一个线程时默认的线程栈如下所示(包括协处理器栈):

xtensa默认线程栈

其中最后一部分为任务的上文。关于这块xtensa又分为两部分:

  • STK帧:如下图所示为STK栈帧,初始化的线程栈中也是STK栈帧。该栈帧用于线程被中断打断时保存上文使用,会将全部的寄存器压入栈中

    STK栈帧

  • SOL帧:如下所以为SOL栈帧,该栈帧仅压入最关键的寄存器到栈中,一般为线程间切换时使用

    SOL栈帧

上述两种栈的类型可以通过栈帧中的exit成员判断。STK中exit成员用于保存函数退出后的函数,而SOL中的exit为0。

1.2.1 源码分析

rtt中cortex-m3架构的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
truct exception_stack_frame
{
rt_uint32_t r0;
rt_uint32_t r1;
rt_uint32_t r2;
rt_uint32_t r3;
rt_uint32_t r12;
rt_uint32_t lr;
rt_uint32_t pc;
rt_uint32_t psr;
};

struct stack_frame
{
/* r4 ~ r11 register */
rt_uint32_t r4;
rt_uint32_t r5;
rt_uint32_t r6;
rt_uint32_t r7;
rt_uint32_t r8;
rt_uint32_t r9;
rt_uint32_t r10;
rt_uint32_t r11;

struct exception_stack_frame exception_stack_frame;
};

rt_uint8_t *rt_hw_stack_init(void *tentry,
void *parameter,
rt_uint8_t *stack_addr,
void *texit)
{
struct stack_frame *stack_frame;
rt_uint8_t *stk;
unsigned long i;
/* 对传入的栈指针做对齐处理 */
stk = stack_addr + sizeof(rt_uint32_t);
stk = (rt_uint8_t *)RT_ALIGN_DOWN((rt_uint32_t)stk, 8);
stk -= sizeof(struct stack_frame);
/* 得到上下文的栈帧的指针 */
stack_frame = (struct stack_frame *)stk;

/* 把所有寄存器的默认值设置为 0xdeadbeef */
for (i = 0; i < sizeof(struct stack_frame) / sizeof(rt_uint32_t); i ++)
{
((rt_uint32_t *)stack_frame)[i] = 0xdeadbeef;
}
/* 根据 ARM APCS 调用标准,将第一个参数保存在 r0 寄存器 */
stack_frame->exception_stack_frame.r0 = (unsigned long)parameter; /* r0 : argument */
/* 将剩下的参数寄存器都设置为 0 */
stack_frame->exception_stack_frame.r1 = 0; /* r1 */
stack_frame->exception_stack_frame.r2 = 0; /* r2 */
stack_frame->exception_stack_frame.r3 = 0; /* r3 */
/* 将 IP(Intra-Procedure-call scratch register.) 设置为 0 */
stack_frame->exception_stack_frame.r12 = 0; /* r12 */
/* 将线程退出函数的地址保存在 lr 寄存器 */
stack_frame->exception_stack_frame.lr = (unsigned long)texit; /* lr */
/* 将线程入口函数的地址保存在 pc 寄存器 */
stack_frame->exception_stack_frame.pc = (unsigned long)tentry; /* entry point, pc */
/* 设置 psr 的值为 0x01000000L,表示默认切换过去是 Thumb 模式 */
stack_frame->exception_stack_frame.psr = 0x01000000L; /* PSR */

/* 返回当前线程的栈地址 */
return stk;
}

xtensa架构中的rtt移植函数(cpuport.c):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
rt_uint8_t *rt_hw_stack_init(void       *tentry,
void *parameter,
rt_uint8_t *stack_addr,
void *texit) //线程退出地址,即rt_thread_exit,暂时未考虑
{
StackType_t *sp, *tp;
XtExcFrame *frame;
#if XCHAL_CP_NUM > 0
uint32_t *p;
#endif
uint32_t *threadptr;
void *task_thread_local_start;
extern int _thread_local_start, _thread_local_end, _rodata_start;

uint32_t thread_local_sz = (uint8_t *)&_thread_local_end - (uint8_t *)&_thread_local_start;

thread_local_sz = ALIGNUP(0x10, thread_local_sz);

/* Initialize task's stack so that we have the following structure at the top:

----LOW ADDRESSES ----------------------------------------HIGH ADDRESSES----------
task stack | interrupt stack frame | thread local vars | co-processor save area |
----------------------------------------------------------------------------------
| |
SP stack_addr

All parts are aligned to 16 byte boundary. */
sp = (StackType_t *) (((UBaseType_t)(stack_addr + 1) - XT_CP_SIZE - thread_local_sz - XT_STK_FRMSZ) & ~0xf);

/* Clear the entire frame (do not use memset() because we don't depend on C library) */
for (tp = sp; tp <= stack_addr; ++tp)
*tp = 0;

frame = (XtExcFrame *) sp;

/* Explicitly initialize certain saved registers */
#if CONFIG_FREERTOS_TASK_FUNCTION_WRAPPER
frame->pc = (UBaseType_t) vPortTaskWrapper; /* task wrapper */
#else
frame->pc = (UBaseType_t) tentry; /* task entrypoint */
#endif
frame->a0 = 0; /* to terminate GDB backtrace */
frame->a1 = (UBaseType_t) sp + XT_STK_FRMSZ; /* physical top of stack frame */
frame->exit = (UBaseType_t) _xt_user_exit; /* user exception exit dispatcher */

// rt_kprintf("frame addr:0x%08x exit[0x%08x] addr:0x%08x pc addr:0x%08x\n", frame, frame->exit, &frame->exit, &frame->pc);// by jz

/* Set initial PS to int level 0, EXCM disabled ('rfe' will enable), user mode. */
/* Also set entry point argument parameter. */
#ifdef __XTENSA_CALL0_ABI__
#if CONFIG_FREERTOS_TASK_FUNCTION_WRAPPER
frame->a2 = (UBaseType_t) tentry;
frame->a3 = (UBaseType_t) parameter;
#else
frame->a2 = (UBaseType_t) parameter;
#endif
frame->ps = PS_UM | PS_EXCM;
#else
/* + for windowed ABI also set WOE and CALLINC (pretend task was 'call4'd). */
#if CONFIG_FREERTOS_TASK_FUNCTION_WRAPPER
frame->a6 = (UBaseType_t) tentry;
frame->a7 = (UBaseType_t) parameter;
#else
frame->a6 = (UBaseType_t) parameter;
#endif
frame->ps = PS_UM | PS_EXCM | PS_WOE | PS_CALLINC(1);
#endif

#ifdef XT_USE_SWPRI
/* Set the initial virtual priority mask value to all 1's. */
frame->vpri = 0xFFFFFFFF;
#endif

/* Init threadptr reg and TLS vars */
task_thread_local_start = (void *)(((uint32_t)stack_addr - XT_CP_SIZE - thread_local_sz) & ~0xf);
memcpy(task_thread_local_start, &_thread_local_start, thread_local_sz);
threadptr = (uint32_t *)(sp + XT_STK_EXTRA);
/* shift threadptr by the offset of _thread_local_start from DROM start;
need to take into account extra 16 bytes offset */
*threadptr = (uint32_t)task_thread_local_start - ((uint32_t)&_thread_local_start - (uint32_t)&_rodata_start) - 0x10;

#if XCHAL_CP_NUM > 0
/* Init the coprocessor save area (see xtensa_context.h) */
/* No access to TCB here, so derive indirectly. Stack growth is top to bottom.
* //p = (uint32_t *) xMPUSettings->coproc_area;
*/
p = (uint32_t *)(((uint32_t) stack_addr - XT_CP_SIZE) & ~0xf);
p[0] = 0;
p[1] = 0;
p[2] = (((uint32_t) p) + 12 + XCHAL_TOTAL_SA_ALIGN - 1) & -XCHAL_TOTAL_SA_ALIGN;
#endif

return sp;
}
  • 受限于xtensa中STK栈帧的影响,线程退出函数exit只能存放_xt_user_exit函数,所以这里暂不支持线程退出。可以通过全局修改调用exit函数的地方固定为_xt_user_exit函数

  • _thread_local_start_thread_local_end_rodata_start为链接脚本components/esp32/ld/esp32.project.ld.in中定义的三个地址

  • rtt中断stack_frame就是idf中的XtExcFramecomponents/freertos/include/freertos/xtensa_context.h中定义),对应上述代码中的frame,其结构如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    #define STRUCT_BEGIN            typedef struct {
    #define STRUCT_FIELD(ctype,size,asname,name) ctype name;
    #define STRUCT_AFIELD(ctype,size,asname,name,n) ctype name[n];
    #define STRUCT_END(sname) } sname;
    //结构体如下
    STRUCT_BEGIN
    STRUCT_FIELD (long, 4, XT_STK_EXIT, exit) /* exit point for dispatch */
    STRUCT_FIELD (long, 4, XT_STK_PC, pc) /* return PC */
    STRUCT_FIELD (long, 4, XT_STK_PS, ps) /* return PS */
    STRUCT_FIELD (long, 4, XT_STK_A0, a0)
    STRUCT_FIELD (long, 4, XT_STK_A1, a1) /* stack pointer before interrupt */
    STRUCT_FIELD (long, 4, XT_STK_A2, a2)
    STRUCT_FIELD (long, 4, XT_STK_A3, a3)
    STRUCT_FIELD (long, 4, XT_STK_A4, a4)
    STRUCT_FIELD (long, 4, XT_STK_A5, a5)
    STRUCT_FIELD (long, 4, XT_STK_A6, a6)
    STRUCT_FIELD (long, 4, XT_STK_A7, a7)
    STRUCT_FIELD (long, 4, XT_STK_A8, a8)
    STRUCT_FIELD (long, 4, XT_STK_A9, a9)
    STRUCT_FIELD (long, 4, XT_STK_A10, a10)
    STRUCT_FIELD (long, 4, XT_STK_A11, a11)
    STRUCT_FIELD (long, 4, XT_STK_A12, a12)
    STRUCT_FIELD (long, 4, XT_STK_A13, a13)
    STRUCT_FIELD (long, 4, XT_STK_A14, a14)
    STRUCT_FIELD (long, 4, XT_STK_A15, a15)
    STRUCT_FIELD (long, 4, XT_STK_SAR, sar)
    STRUCT_FIELD (long, 4, XT_STK_EXCCAUSE, exccause)
    STRUCT_FIELD (long, 4, XT_STK_EXCVADDR, excvaddr)
    #if XCHAL_HAVE_LOOPS
    STRUCT_FIELD (long, 4, XT_STK_LBEG, lbeg)
    STRUCT_FIELD (long, 4, XT_STK_LEND, lend)
    STRUCT_FIELD (long, 4, XT_STK_LCOUNT, lcount)
    #endif
    #ifndef __XTENSA_CALL0_ABI__
    /* Temporary space for saving stuff during window spill */
    STRUCT_FIELD (long, 4, XT_STK_TMP0, tmp0)
    STRUCT_FIELD (long, 4, XT_STK_TMP1, tmp1)
    STRUCT_FIELD (long, 4, XT_STK_TMP2, tmp2)
    #endif
    #ifdef XT_USE_SWPRI
    /* Storage for virtual priority mask */
    STRUCT_FIELD (long, 4, XT_STK_VPRI, vpri)
    #endif
    #ifdef XT_USE_OVLY
    /* Storage for overlay state */
    STRUCT_FIELD (long, 4, XT_STK_OVLY, ovly)
    #endif
    STRUCT_END(XtExcFrame)

1.3 上下文切换

RT-Thread的libcpu抽象层需要实现三个线程切换相关的函数:

  • rt_hw_context_switch_to():没有来源线程,切换到目标线程,在调度器启动第一个线程的时候被调用
  • rt_hw_context_switch():在线程环境下,从当前线程切换到目标线程
  • rt_hw_context_switch_interrupt ():在中断环境下,从当前线程切换到目标线程

线程环境下,如果调用rt_hw_context_switch()函数,那么可以马上进行上下文切换;而在中断环境下,需要等待中断处理函数完成之后才能进行切换。

在中断处理程序里如果触发了线程的调度,调度函数里会调用 rt_hw_context_switch_interrupt() 触发上下文切换。中断处理程序里处理完中断事务之后,中断退出之前,检查 rt_thread_switch_interrupt_flag 变量,如果该变量的值为 1,就根据 rt_interrupt_from_thread 变量和 rt_interrupt_to_thread 变量,完成线程的上下文切换。

在IDF框架中,整个上下文切换是通过汇编实现的(portasm.S中的_frxt_dispatch函数),充当所有上下文切换功能(包括vPortYield()vPortYieldFromInt()(均为汇编),其中vPortYieldFromInt()通过_frxt_int_exit()间接调用)的共享退出路径

portasm.S文件相关函数即变量:

变量:

  • port_switch_flag:类似rtt中的rt_thread_switch_interrupt_flag 变量,表示中断返回时需要切换任务

函数:

  • _frxt_setup_switch:设置port_switch_flag=1
  • _frxt_int_enter:实现freertos中的XT_RTOS_INT_ENTER宏(include\freertos\xtensa_rtos.h),通过_xt_context_save保存其他尚未保存的中断上下文,只能通过call0从汇编调用,并且需要关闭中断
    • port_xSchedulerRunning:调度启动时置1,每个核对应一个
    • port_interruptNesting:中断嵌套级别,每个核对应一个
  • _frxt_int_exit:实现freertos中的XT_RTOS_INT_EXIT宏(include\freertos\xtensa_rtos.h),如果需要,请调用vPortYieldFromInt()以执行任务上下文切换,还原(可能是)新任务的上下文,然后返回保存在任务堆栈框架中XT_STK_EXIT的出口调度程序。只能通过call0从汇编调用,不会返回到调用者。内部会调用以下函数:
    • vPortYieldFromInt:
    • _frxt_dispatch:下面
  • _frxt_timer_int:实现freertos中的XT_RTOS_TIMER_INT宏。调用每个计时器中断。管理滴答计时器,每个滴答都调用xPortSysTickHandler()
  • _frxt_tick_timer_init:初始化计时器和计时器中断处理程序(已调用_xt_tick_divisor_init()
  • _frxt_dispatch:将上下文切换到优先级最高的就绪任务,还原其状态并向其调度控制。公共调度程序,充当所有上下文切换功能(包括vPortYield()vPortYieldFromInt())的共享退出路径,所有这些功能都会最终调用此调度程序
  • vPortYield:执行请求的上下文切换(来自任务),保存挂起任务所需的最小状态,清除CPENABLE,最终调用_frxt_dispatch执行上下文切换,不会返回
  • vPortYieldFromInt:执行未经请求的上下文切换(来自中断),保存并清除CPENABLE,最终调用_frxt_dispatch执行上下文切换,不会返回
  • _frxt_task_coproc_state:实现freertos中的XT_RTOS_CP_STATE宏。只能在任务正在运行时调用,而不是在中断处理程序内调用(在这种情况下返回0),只能从汇编调用

相关宏:

XT_RTOS_INT_ENTER宏(_frxt_int_enter):通知RTOS进入中断处理程序。允许RTOS管理切换到任何系统堆栈并统计嵌套级别

XT_RTOS_INT_EXIT宏(_frxt_int_exit):通知RTOS中断处理程序的完成,并将控制权交给RTOS以执行线程/任务调度,从任何系统堆栈切换回并恢复上下文,然后返回保存在堆栈帧中XT_STK_EXIT的出口调度程序。RTOS端口可以调用_xt_context_restore来恢复在XT_RTOS_INT_ENTER中通过_xt_context_save保存的上下文,而仅剩下一小部分上下文由出口调度程序恢复。此函数不会返回到调用它的位置

XT_RTOS_CP_STATE宏(_frxt_task_coproc_state):在a15中返回触发协处理器异常的线程的协处理器状态保存区的基地址,如果没有线程在运行,则返回0。

1.3.1 xtensa中freertos的调度

支持两种主流场景的任务调度:

  • 任务主动调度:主要用于当前工作任务结束、等待,或者需要别的事件输入时,任务将主动放弃当前调度并通过FreeRTOS的TaskYield处理实现任务调度
    • vTaskDelay
  • 基于中断的调度
    • SysTick调度:ESP32保持了一个1000Hz的SysTick中断,除了实现CPU运转Tick计数和基于Tick的某些定时功能外,还可以用来触发产生任务调度。这样即便没有外围其它中断触发,高优先级任务也能再合适的时间得到调度
    • 各种中断事件调度:所有中断处理的最后(包括SysTick中断)都会通过XT_RTOS_INT_ENTER()XT_RTOS_INT_EXIT()函数实现当前任务堆栈的保存和调度切换(根据优先级和当前任务执行时间),这样任何外部事件或者中断触发都可以得到一次任务调度机会

1.3.2 xtensa架构的中断相关知识

xtensa的中断向量:

  • _DebugExceptionVector
    • xt_debugexception
  • _DoubleExceptionVector
    • _xt_panic
  • _KernelExceptionVector
    • _xt_panic
  • _UserExceptionVector
  • _Level2Vector
  • _Level3Vector
  • _Level4Vector
  • _Level5Vector
  • _Level6Vector
  • _NMIExceptionVector

注意事项:

  • 用户可以通过调用xt_set_interrupt_handler()为低级和中级中断安装特定于应用程序的中断处理程序(蓝牙中有使用)(安装到数组_xt_interrupt_table中)
    • 最后会在汇编宏dispatch_c_isr中调用注册的中断
    • 汇编宏dispatch_c_isr会在1-6级(ESP32中只有3级)中断中调用,会通通过XT_RTOS_INT_ENTER()XT_RTOS_INT_EXIT()函数保护
  • 用户还可以通过调用xt_set_exception_handler()以相同的方式安装特定于应用程序的异常处理程序(目前貌似还没看到使用)(安装到数组_xt_exception_table中)
    • 每个处理程序都将指向异常帧的指针(xt_exc_handler)作为其单个参数传递给它。异常帧是在堆栈上创建的,并保存了发生异常的线程上下文。如果处理程序返回,将还原上下文,并重试导致异常的指令
    • 最后会在_xt_user_exc函数中调用(User exception handler)注册的函数
  • 程序跳转时使用call0而不是j指令

1.3.3 rtt中的xtensa架构上下文切换的实现

rtt中xtensa架构的上下文切换的实现主要在libcpu/xtensa文件夹下,具体来说主要由以下三个函数:

  • rt_hw_context_switch:线程间切换
  • rt_hw_context_switch_interrupt:中断中切换
  • rt_hw_context_switch_to:切换到第一个线程

rt_hw_context_switch:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
    .global rt_hw_context_switch
.type rt_hw_context_switch,@function
.align 4
.literal_position
.align 4
rt_hw_context_switch:
ENTRY0

l32i a2, a2, 0 // 取出from thread的ps指针
l32i a3, a3, 0 // 取出to thread的ps指针


//给全局变量 rt_interrupt_from_thread 赋值
movi a4, rt_interrupt_from_thread
s32i a2, a4, 0

//给全局变量 rt_interrupt_to_thread 赋值
movi a4, rt_interrupt_to_thread
s32i a3, a4, 0

//call4 rt_debug_test2

//开始调度
#ifdef __XTENSA_CALL0_ABI__
call0 vPortYield
#else
call4 vPortYield
#endif
RET0

rt_hw_context_switch_interrupt:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
    .global rt_hw_context_switch_interrupt
.type rt_hw_context_switch_interrupt,@function
.align 4
.literal_position
.align 4
rt_hw_context_switch_interrupt:
ENTRY0

l32i a2, a2, 0 // 取出from thread的ps指针
l32i a3, a3, 0 // 取出to thread的ps指针

//判断 rt_thread_switch_interrupt_flag 是否为 1
movi a4, rt_thread_switch_interrupt_flag /* address of switch flag */
l32i a5, a4, 0 /* a5 = rt_thread_switch_interrupt_flag */
bnez a5, .reswitch1 /* flag != 0 则直接跳转 */
movi a5, 1
s32i a5, a4, 0 /* rt_thread_switch_interrupt_flag 赋值为 1 表示需要调度 */

//给全局变量 rt_interrupt_from_thread 赋值
movi a4, rt_interrupt_from_thread
s32i a2, a4, 0

.reswitch1:
//给全局变量 rt_interrupt_to_thread 赋值
movi a4, rt_interrupt_to_thread
s32i a3, a4, 0
RET0

rt_hw_context_switch_to:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
    .global rt_hw_context_switch_to
.type rt_hw_context_switch_to,@function
.align 4
.literal_position
.align 4
rt_hw_context_switch_to:
ENTRY0 /* a2 = to thread */

l32i a2, a2, 0 // 取出to thread的ps指针

//保存到 rt_interrupt_to_thread
movi a3, rt_interrupt_to_thread
s32i a2, a3, 0

//清空 rt_interrupt_from_thread
movi a3, rt_interrupt_from_thread
movi a2, 0
s32i a2, a3, 0

//开始调度
#ifdef __XTENSA_CALL0_ABI__
call0 vPortYieldFromInt /* call dispatch inside the function; never returns */
#else
call4 vPortYieldFromInt /* this one returns */
call0 _frxt_dispatch /* tail-call dispatcher */
/* Never returns here. */
#endif

RET0

三、其他事项

3.1 控制台移植

控制台只需要实现一个rt_hw_console_output接口即可,将传入的参数通过串口输出到控制台上。这里比较简单,直接调用IDF中提供的接口函数ets_printf即可

1
2
3
4
5
void rt_hw_console_output(const char *str)
{
/* empty console output */
ets_printf(str);
}

3.2 内核中的修改部分

  1. 由于需要兼容IDF框架以及各种组件,尤其是其中的newlib组件,需要每个线程中提供一个struct _reent结构体,故在struct rt_thread中增加了该成员,并在_rt_thread_init中对该成员进行了初始化,在rt_thread_detachrt_thread_delete中对该成员进行了销毁。
  2. 还是newlib库中带来的问题。由于newlib库中需要在整个系统运行前申请一些互斥量或信号量,而此时正由于系统没有起来所以会导致宕机。解决方法是将rt_system_timer_initrt_system_scheduler_init提前到初始化前,并将rtthread_startup中相关函数进行注释

附一:xtensa架构上的汇编指令

MOVE指令

  • MOVI:12bit常量赋值到寄存器中
  • MOVEQZ:为零则赋值
  • MOVNEZ:非零则赋值
  • MOVLTZ:小于零则赋值
  • MOVGEZ:大于等于则赋值

算术指令

  • ADD ar, as, at:ar=as+at,无溢出检测,32bit加法
  • ADDX2 ar, as, at:ar=(as<<1)+at,无溢出检测,32bit加法
  • ADDX4 ar, as, at:ar=(as<<2)+at,无溢出检测,32bit加法
  • ADDX8 ar, as, at:ar=(as<<3)+at,无溢出检测,32bit加法
  • ADDI at, as, -128-127:at=as+( -128-127),无溢出检测,32bit加法

存储指令

如果启用了区域转换选项(Region Translation Option,Xtensa® Instruction Set Architecture(ISA) Reference Manual第156页)或MMU选项(Xtensa® Instruction Set Architecture(ISA) Reference Manual第158页),则虚拟地址将转换为物理地址。 如果不是,则物理地址与虚拟地址相同。 如果转换或内存引用遇到错误(例如,违反保护或不存在内存),则处理器会引发几种异常之一(Xtensa® Instruction Set Architecture(ISA) Reference Manual第89页的4.4.1.5节)

  • S8I at, as, 0-255:将数据从寄存器at存储到虚拟地址中(as+偏移0-255),只存储at中的低8bit
  • S16I at, as, 0-510:将数据从寄存器at存储到虚拟地址中(as+偏移0-510),只存储at中的低16bit
  • S32I at, as, 0-1020:将数据从寄存器at存储到虚拟地址中(as+偏移0-1020),只存储at中的低32bit

加载指令

加载指令通过添加一个基本寄存器和一个8位无符号偏移形成一个虚拟地址。必要时,这个虚拟地址被翻译成物理地址。然后,该物理地址被用来访问内存系统(通常通过高速缓存)。存储器系统返回一个数据项(根据配置,可以是32、64或128位)。然后,加载指令从该内存项中提取引用的数据,并将结果零扩展或符号扩展写入寄存器中。除非启用不对齐异常选项,否则处理器不会处理不对齐的数据。或当使用了一个错误对齐的地址时,会出现陷阱;相反,它只是简单地加载对齐的数据项。含有计算出的虚拟地址。这就允许漏斗移位器与一个包含计算出的虚拟地址的 对的负载来引用任意字节地址上的数据。

只有loads L32I、L32I.N和L32R可以访问InstRAM和InstROM位置。

  • L8UI at, as, 0-255:从虚拟地址as+(0-255)处加载一个无符号8bit数据到at中
  • L16SI at, as, 0…510:从虚拟地址as+((0-510)<<1)处加载一个有符号16bit数据到at中
  • L16UI at, as, 0…510:从虚拟地址as+((0-510)<<1)处加载一个无符号16bit数据到at中
  • L32I at, as, 0-1020:从虚拟地址as+((0-1020)<<2)处加载一个32bit数据到at中
  • L32R at, label:

处理器控制指令

RSR:读特殊寄存器

RSR是RSR.*的汇编器宏,它提供了与包含特殊寄存器名称或编号的旧版本指令的兼容性。

  • RSR.* at:
  • RSR at, *:
  • RSR at, 0-255:

由指令字的8位sr字段指定的特殊寄存器的内容被写入地址寄存器’at’。在上述汇编器语法中用特殊寄存器的名称代替’*',并由汇编器翻译到8位sr字段。

WSR:写特殊寄存器

  • WSR.* at:
  • WSR at, *:
  • WSR at, 0-255:

地址寄存器at的内容被写入指令字的8位sr字段指定的特殊寄存器中。在上述汇编器语法中用特殊寄存器的名称代替’*',由汇编器翻译到8位sr字段。

XSR:交换特殊寄存器(RSR+WSR)

其他指令

ENTRY as, 0-32760:

ENTRY是用CALL4,CALL8,CALL12,CALLX4,CALLX8或CALLX12调用的所有子例程的第一条指令。 该指令不能由CALL0或CALLX0调用的例程使用。主要用途:

  1. 按调用者要求的数量递增寄存器窗口指针(WindowBase)(记录在PS.CALLINC字段中)
  2. 它将堆栈指针从调用者复制到被调用者,并分配被调用者的堆栈帧。操作数’as’指定了堆栈指针寄存器;它必须指定a0…a3中的一个,否则ENTRY的操作就没有定义。在窗口移动之前,先读取它,减去堆栈框架的大小,然后写入被移动窗口中的as寄存器。

堆栈帧大小被指定为8位字节的12位无符号imm12字段。大小为零扩展,向左移动3,然后从调用者的堆栈指针中减去以获取被调用者的堆栈指针。因此,最多可以指定32760字节的堆栈帧。初始堆栈帧大小必须为常数,但随后可以使用MOVSP指令在堆栈上分配动态大小的对象,或进一步扩展大于32760字节的恒定堆栈帧。


RSIL at, 0-15:

首先读取PS寄存器(程序状态寄存器),将参数0-15写到寄存器at中,然后设置PS.INTLEVEL为该值,在PS.INTLEVEL级别及以下的中断将被禁用。

附二:xtensa架构上相关寄存器说明

PS寄存器

PS(Miscellaneous Program State Register)寄存器,杂项程序状态寄存器,复位默认PS.INTLEVEL为15,PS.EXCM为1,其他字段为零(Region Translation Option,Xtensa® Instruction Set Architecture(ISA) Reference Manual第5.3.5节“处理器状态特殊寄存器”详细描述了该寄存器的字段)

PS寄存器

  • INTLEVEL:中断等级禁用选项,用于设置处理器当前中断级别(中断选项),在PS.INTLEVEL级别及以下的中断将被禁用。
  • EXCM:异常模式(异常选项)
    • 0:正常运行
    • 1:异常模式
  • UM:用户向量模式(异常选项)
    • 0:内核向量模式——异常不需要切换堆栈
    • 1:用户向量模式——异常情况下需要切换堆栈
  • RING:特权级别(MMU选项)
  • OWB:Old window base(窗口寄存器选项),窗口溢出或下溢前的WindowBase值。
  • CALLINC:调用增量(窗口寄存器选项),由CALL指令设置窗口增量。由ENTRY用于旋转窗口。
  • WOE:窗口溢出检测启用(窗口寄存器选项),用于计算当前窗口溢出的启用情况(4.4.1.4节)