Xtensa 微处理器介绍

Xtensa使用的指令集架构属于类RISC架构,主要针对嵌入式应用场合。在移植/编写操作系统的时候需要额外注意以下几点:

  1. 不同的指令其宽度可能不同
  2. window寄存器的使用
  3. 处理器的可配置性
  4. 处理器的扩展性(指令集可扩展)

Xtensa 硬件抽象层HAL

编译时HAL——CHAL

包括C语言预处理器和汇编语言的宏定义(用来表征不同xtensa处理器的不同配置)

链接时HAL——LHAL

  1. 给操作系统移植层调用
  2. 给底层软件(需要处理ISA相关的功能)调用,例如提供保存现场window frame的接口

窗寄存器函数调用规范(Windowed Calling Convention)

现代处理器为了更好的支持高级编程语言的高效编译,通常处理器所拥有的通用寄存器的数目有16个甚至32个之多,如此多的寄存器在比较复杂的应用程序上实现深度嵌套调用的时候,为了保证程序的正确执行,寄存器要频繁地进行入栈和出栈的操作,这样频繁的堆栈memory的访问将明显恶化应用程序的性能。为了有效解决这一问题,Xtensa架构设计了一种Windows旋转方式的寄存器管理机制,将逻辑寄存器和物理寄存器分开,在函数调用的时候通过windows滑动切换逻辑寄存器,从而避免寄存器覆盖,减少压栈和出栈的操作。

AR物理寄存器环形Buffer

AR物理寄存器环形buffer
AR物理寄存器环形buffer

基本实现原理:使用更多的物理AR寄存器组成一个环形的buffer。这些寄存器每4个为一组(pane),WindowStart中的每个比特依次表示该组是否作为逻辑寄存器窗口的起始位置或者被占用。当前的逻辑寄存器的起始位置则用WindowBase状态寄存器来表示。在发生函数调用的时候是通过修改WindowBase寄存器,滑动逻辑寄存器窗口,从而父子函数看到的是不同的物理寄存器,避免了寄存器的压栈和出栈。

以每4个寄存器(pane)为单位,函数调用的时候窗口可以滑动4个,8个或者12个物理寄存器,分别可以用call4,call8,call12指令来实现,而最典型的应用则为call8。

call8 Windows ABI调用规范
call8 Windows ABI调用规范
  • a0用来保存函数返回地址
  • a1保存sp堆栈指针
  • a2~a7用来传递函数入参,参数超过6个的时候则需要使用堆栈
  • 对调用者函数和被调用函数来说,a0~a7是独立的寄存器,可以自由使用,而a8~a15则为scratch寄存器,随时会被子函数使用,调用者函数如果要使用,则在调用子函数前进行压栈保存

为了方便寄存器正常的保存与恢复,还要调用栈的高效回溯,还有必要对函数的Frame栈空间做统一的安排。

Window ABI 堆栈布局
Window ABI 堆栈布局
  • Base Area用于存储其父函数的基本寄存器a0~a3

Windows寄存器覆盖问题

在发生函数调用,执行call指令的时候,窗递增值(call4,call8,call12分别对应1,2,3)存入PS处理器状态寄存器的CALLINC域,在进入函数的入口处用entry指令进行Window重叠检测,条件满足的时候将触发相应的windows overflow异常,引导程序进行覆盖寄存器的入栈保护。

Windows寄存器underflow问题

当子函数返回时,RETW或者RETW.N指令执行,此时也仅此时处理器将进行上溢检查。如果当Windowbase所在的位置的前3个windows pane的WindowStart比特都为0,则意味着它返回后的父函数发生过WindowOverflow,父函数的窗口寄存器曾经被压入stack。如果不是全为0,则应该不为零0的点和正常window返回的点对应,就返回,如果不同,则说明发生了不正常的调用,a0被破坏掉了,要产生非法指令错误。

参数传递

前6个参数会传递给 AR 寄存器,剩余的参数会被保存在stack中。

对于callN指令(N取值4,8或者12)来说,函数调用者会将参数保存到寄存器AR[N+2]到AR[N+7]中(特别注意,call12指令是能用于调用只有两个或者更少参数的函数,只能使用AR[N+2]和AR[N+3]寄存器),函数被调用者会从寄存器AR[2]到AR[7]中接收这些参数。

如果参数的数量多于6个,剩下的参数就会被保存在函数调用者的堆栈,即第七个参数保存在[sp+0]处,第八个参数保存在[sp+4]处,依此类推。函数被调用者需要从内存的[sp+FRAMESIZE]处获取这些额外的参数,其中FRAMESIZE是被调用者的stack frame大小,通常会用entry指令指定。

以下C程序代码

1
2
3
4
5
6
7
int func( int a, int b, int c, int d, int e, int f,
int g, int h, int i )
{
int j;
j = a + b + c + d + e + f + g + h + i;
return j;
}

对应的汇编程序代码为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.align 4
.global func
func:
.frame a1, 32
.LBB1_func:
entry a1,32 // 此函数的 FRAMESIZE 是32字节
l32i.n a10,a1,40
.LBB2_func:
l32i.n a8,a1,32
add.n a12,a5,a6
add.n a11,a2,a3
l32i.n a2,a1,36
add.n a11,a4,a11
add.n a11,a11,a12
add.n a8,a8,a7
add.n a2,a2,a10
add.n a8,a8,a11
add.n a2,a2,a8 //返回值如果不超过4字节,就会被保存在 a2 寄存器中
retw.n

函数调用和返回

编写Xtensa汇编代码

部分底层驱动只能使用汇编语言编写,比如:

  • 用户异常处理
  • 内核异常处理
  • window处理
  • 复位处理

使用汇编实现16比特点积运算

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
#include "dsls_dotprod_16s_m_ae32.S"
#include "dsl_err_codes.h"

.text //保存在代码段
.align 4 // 与PC有关的跳转指令需要目标地址4字节对齐
.global dsls_dotprod_16s_ae32 // 声明该函数全局可访问
.type dsls_dotprod_16s_ae32,@function // 声明符号是函数类型(方便调试器更好地展示信息)


// 良好的习惯是在汇编函数开始前,注释其C语言原型
// esp_err_t dsls_dotprod_16s_ae32(int16_t* src1, int16_t* src2, int16_t* dest, int len, int8_t shift);
dsls_dotprod_16s_ae32:
// src1 - a2
// src2 - a3
// dest - a4
// len - a5
// shift - a6

entry a1, 32 // 每个函数都以一条entry指令开头,这是window寄存器调用规范所要求的

// Check minimum length
movi a8, 4
blt a5, a8, dsls_dotprod_16s_ae32_error

// Clear accumulator
movi a8, 0
wsr a8, acchi

// Prepare and load round value
movi a8, 0x7fff
ssr a6
srl a8, a8
wsr a8, acclo // initialize acc with shifted round value

// Compensate for pre-increment
// Right shift to 16 bits
// RS = -shift + 15
neg a6, a6
addi a6, a6, 15

/* number of loop iterations (see below):
* a7 = count / 4 - 1
*/

srli a7, a5, 2
addi a7, a7, -1

movi.n a10, 0 // load 0 to the a10 to increment second array

dotprod_16s_ae32_full a2, a3, a7, a5

/* Get accumulator */
ssr a6
rsr a2, acchi
rsr a3, acclo
src a2, a2, a3

s16i a2, a4, 0
movi.n a2, 0 //返回值保存在 a2 寄存器中
retw.n
dsls_dotprod_16s_ae32_error:
movi.n a2, ESP_ERR_DSL_INVALID_LENGTH
retw.n
  • 有些指令会以.n作为后缀,Xtensa处理器为了进一步提高代码密度,提供了一些常用指令的16比特版本,这里的n代表narrow