5.3. Kernel与异常
这里所说的异常是指包括了中断和例外,在需要区分处会特别的说明,其他情况都按照异常来说。
5.3.1. 异常的入口地址选择
LoongArch的异常配置比较灵活,可满足不同的使用场景。全局的中断使能我们上节已经说明,除此之外,我们 需要明确异常的处理入口。LoongArch通过配置CSR.ECFG.VS位,来决定处理的入口偏移问题。
Note
CSR.ECFG.VS:配置例外和中断入口的间距。
当 VS =0 时,所有例外和中断的入口地址是同一个。
当 VS!=0 时,各例外和中断之间的入口地址间距是 2VS 条指令。
TLB重填例外和机器错误例外有独立的入口基址,所以二者的例外入口不受 VS域的影响。
下面我们详细的说明我们常见的两种方式。
5.3.1.1. “统一式”的入口地址
“统一式”指所有的异常和中断都是使用同一个入口地址,也就是来自CSR.EENTRY寄存器。此时需要配置
CSR.ECFG.VS = 0。
如何区分中断和异常:
中断: 此时读取 CSR.ESTAT.Ecode域,如果
Ecode = 0则表明是处于中断。
再读取CSR.ESTAT.IS[12:0],如IS[12:0]!=0,则对应的位有了中断,然后再具体的处理对应中断。异常: 读取 CSR.ESTAT.Ecode域,如果
Ecode != 0则表明是处于异常,
此时可根据下面的例外代码确认具体的异常类型。
| Ecode | EsubCode | 例外代号 | 例外类型 |
|---|---|---|---|
| 0x1 | PIL | load 操作页无效例外 | |
| 0x2 | PIS | store 操作页无效例外 | |
| 0x3 | PIF | 取指操作页无效例外 | |
| 0x4 | PME | 页修改例外 | |
| 0x5 | PNR | 页不可读例外 | |
| 0x6 | PNX | 页不可执行例外 | |
| 0x7 | PPI | 页特权等级不合规例外 | |
| 0x8 | 0 | ADEF | 取指地址错例外 |
| 0x8 | 1 | ADEM | 访存指令地址错例外 |
| 0x9 | ALE | 地址非对齐例外 | |
| 0xA | BCE | 边界检查错例外 | |
| 0xB | SYS | 系统调用例外 | |
| 0xC | BRK | 断点例外 | |
| 0xD | INE | 指令不存在例外 | |
| 0xE | IPE | 指令特权等级错例外 | |
| 0xF | FPD | 浮点指令未使能例外 | |
| 0x10 | SXD | 128 位向量扩展指令未使能例外 | |
| 0x11 | ASXD | 256 位向量扩展指令未使能例外 | |
| 0x12 | 0 | FPE | 基础浮点指令例外 |
| 0x12 | 1 | VFPE | 向量浮点指令例外 |
| 0x13 | 0 | WPEF | 取指监测点例外 |
| 0x13 | 1 | WPEM | load/store 操作监测点例外 |
| 0x14 | BTD | 二进制翻译扩展指令未使能例外 | |
| 0x15 | BTE | 二进制翻译相关例外 | |
| 0x16 | GSPR | 客户机敏感特权资源例外 | |
| 0x17 | HVC | 虚拟机监控调用例外 | |
| 0x18 | 0 | GCSC | 客户机 CSR 软件修改例外 |
| 0x18 | 1 | GCHC | 客户机 CSR 硬件修改例外 |
| 0x1A-0x3E | 保留编码 |
Caution
统一式异常和中断入口都来自于CSR.EENTRY,因此需要处理程序判断是来自于中断,还是来自于异常。
如果来自中断,则判断Ecode=0,再跳转到中断处理程序。
如果来自异常,则判断Ecode!=0,具体再判断Ecode和EsubCode确定是那个异常,然后再通过
异常表,跳转到各自异常的执行程序中去处理!
统一式异常和中断入口处理起来比较简单,但是对于所有的中断和异常都进入一个入口,因此需要花费 指令判断异常类型。因此中断和异常处理程序比较烦躁,要进行各自判断。
5.3.1.2. “分离式”的入口地址
“分离式”指所有的异常和中断都有各自的入口地址,入口的基址来自CSR.EENTRY寄存器。
计算方式为: 入口地址 = CSR.EENTRY + 页内偏移
此时需要配置CSR.ECFG.VS != 0。
此时异常和中断的入口地址如下所示:
Note
入口地址 = CSR.EENTRY + ecode * 2CSR.ECFG.VS+2
各例外和中断之间的入口地址间距是 2VS 条指令。
VS配置的大小,具体看Kernel设计的情况。比如,Linux中,将VS=7;而有些嵌入式则设置为1,此时入口间距是2条指令。
注意:
异常Ecode的值按照下面的方式计算:
| Ecode | EsubCode | 例外代号 | 例外类型 |
|---|---|---|---|
| 0x1 | PIL | load 操作页无效例外 | |
| 0x2 | PIS | store 操作页无效例外 | |
| 0x3 | PIF | 取指操作页无效例外 | |
| 0x4 | PME | 页修改例外 | |
| 0x5 | PNR | 页不可读例外 | |
| 0x6 | PNX | 页不可执行例外 | |
| 0x7 | PPI | 页特权等级不合规例外 | |
| 0x8 | 0 | ADEF | 取指地址错例外 |
| 0x8 | 1 | ADEM | 访存指令地址错例外 |
| 0x9 | ALE | 地址非对齐例外 | |
| 0xA | BCE | 边界检查错例外 | |
| 0xB | SYS | 系统调用例外 | |
| 0xC | BRK | 断点例外 | |
| 0xD | INE | 指令不存在例外 | |
| 0xE | IPE | 指令特权等级错例外 | |
| 0xF | FPD | 浮点指令未使能例外 | |
| 0x10 | SXD | 128 位向量扩展指令未使能例外 | |
| 0x11 | ASXD | 256 位向量扩展指令未使能例外 | |
| 0x12 | 0 | FPE | 基础浮点指令例外 |
| 0x12 | 1 | VFPE | 向量浮点指令例外 |
| 0x13 | 0 | WPEF | 取指监测点例外 |
| 0x13 | 1 | WPEM | load/store 操作监测点例外 |
| 0x14 | BTD | 二进制翻译扩展指令未使能例外 | |
| 0x15 | BTE | 二进制翻译相关例外 | |
| 0x16 | GSPR | 客户机敏感特权资源例外 | |
| 0x17 | HVC | 虚拟机监控调用例外 | |
| 0x18 | 0 | GCSC | 客户机 CSR 软件修改例外 |
| 0x18 | 1 | GCHC | 客户机 CSR 硬件修改例外 |
| 0x1A-0x3E | 保留编码 |
中断Ecode的值按照下面的方式计算:
| Ecode(十六进制) | Ecode(十进制) | 例外代号 | 例外类型 |
|---|---|---|---|
| 0x40 | 64 | INT_SWI0 | SWI0对应的中断(ESTAT.IS[0]) |
| 0x41 | 65 | INT_SWI1 | SWI1对应的中断(ESTAT.IS[1]) |
| 0x42 | 66 | INT_HWI0 | HWI0对应的中断(ESTAT.IS[2]) |
| 0x43 | 67 | INT_HWI1 | HWI1对应的中断(ESTAT.IS[3]) |
| 0x44 | 68 | INT_HWI2 | HWI2对应的中断(ESTAT.IS[4]) |
| 0x45 | 69 | INT_HWI3 | HWI3对应的中断(ESTAT.IS[5]) |
| 0x46 | 70 | INT_HWI4 | HWI4对应的中断(ESTAT.IS[6]) |
| 0x47 | 71 | INT_HWI5 | HWI5对应的中断(ESTAT.IS[7]) |
| 0x48 | 72 | INT_HWI6 | HWI6对应的中断(ESTAT.IS[8]) |
| 0x49 | 73 | INT_HWI7 | HWI7对应的中断(ESTAT.IS[9]) |
| 0x4A | 74 | INT_PMI | 性能计数器中断(ESTAT.IS[10]) |
| 0x4B | 75 | INT_TI | Timer中断 (ESTAT.IS[11]) |
| 0x4C | 76 | INT_IPI | 核间中断 (ESTAT.IS[12]) |
5.3.2. 异常的初始化
这里我们讨论上述两种方式的初始化,以及需要主要的事项。
不管是分离式还是同一的方式,中断的全局使能(CSR.CRMD.IE)、局部(CSR.ECFG.LIE)都是一样的,
他们只是入口地址不一样而已,其他都是相同的!
下面我们以例子列举初始化的过程,以及需要注意的!
5.3.2.1. 统一式的初始化
初始化流程如下:
设置异常处理通用代码,主要是按照上面的说明,区分中断和异常,分别处理。
配置
CSR.ECFG.VS = 0,表示使用统一式的中断处理。Note
CSR.ECFG.VS默认为0,也可以不用配置,表示使用同一的入口。配置中断的相应的局部使能位:
CSR.ECFG.LIE[12:0]对应于CSR.ESTAT.IS[12:0],如果想要使能哪一位中断源,则将其对应的LIE[x]位置1。
将异常处理程序的地址写入CSR.EENTRY。发生异常时,从这里开始执行。
Warning
CSR.EENTRY写入的时候,必须按照4K页对齐,也就是说,异常处理程序必须4K页对齐!
通过写CSR.CRMD.IE来控制全局的中断使能。
Warning
例外没有使能位控制位,如果发生则会执行相应的代码!
5.3.2.1.1. 异常处理程序的例子
下面是一个内核的异常处理过程。
// 将所有寄存器的值恢复(除SP寄存器指针)
.macro POP_GENERAL_REGS
ld.d $ra, $sp, 8*1
ld.d $tp, $sp, 8*2
ld.d $a0, $sp, 8*4
ld.d $a1, $sp, 8*5
ld.d $a2, $sp, 8*6
ld.d $a3, $sp, 8*7
ld.d $a4, $sp, 8*8
ld.d $a5, $sp, 8*9
ld.d $a6, $sp, 8*10
ld.d $a7, $sp, 8*11
ld.d $t0, $sp, 8*12
ld.d $t1, $sp, 8*13
ld.d $t2, $sp, 8*14
ld.d $t3, $sp, 8*15
ld.d $t4, $sp, 8*16
ld.d $t5, $sp, 8*17
ld.d $t6, $sp, 8*18
ld.d $t7, $sp, 8*19
ld.d $t8, $sp, 8*20
ld.d $r21,$sp, 8*21
ld.d $fp, $sp, 8*22
ld.d $s0, $sp, 8*23
ld.d $s1, $sp, 8*24
ld.d $s2, $sp, 8*25
ld.d $s3, $sp, 8*26
ld.d $s4, $sp, 8*27
ld.d $s5, $sp, 8*28
ld.d $s6, $sp, 8*29
ld.d $s7, $sp, 8*30
ld.d $s8, $sp, 8*31
.endm
// 将所有寄存器的值保存(除SP寄存器指针)
.macro PUSH_GENERAL_REGS
st.d $ra, $sp, 8*1
st.d $tp, $sp, 8*2
st.d $a0, $sp, 8*4
st.d $a1, $sp, 8*5
st.d $a2, $sp, 8*6
st.d $a3, $sp, 8*7
st.d $a4, $sp, 8*8
st.d $a5, $sp, 8*9
st.d $a6, $sp, 8*10
st.d $a7, $sp, 8*11
st.d $t0, $sp, 8*12
st.d $t1, $sp, 8*13
st.d $t2, $sp, 8*14
st.d $t3, $sp, 8*15
st.d $t4, $sp, 8*16
st.d $t5, $sp, 8*17
st.d $t6, $sp, 8*18
st.d $t7, $sp, 8*19
st.d $t8, $sp, 8*20
st.d $r21,$sp, 8*21
st.d $fp, $sp, 8*22
st.d $s0, $sp, 8*23
st.d $s1, $sp, 8*24
st.d $s2, $sp, 8*25
st.d $s3, $sp, 8*26
st.d $s4, $sp, 8*27
st.d $s5, $sp, 8*28
st.d $s6, $sp, 8*29
st.d $s7, $sp, 8*30
st.d $s8, $sp, 8*31
.endm
.section .text
.balign 4096
.global exception_entry_base
exception_entry_base:
csrwr $sp, KSAVE_KSP
bnez $sp, .Ltrap_entry
csrrd $sp, KSAVE_KSP
addi.d $sp, $sp, -{trapframe_size}
.Ltrap_entry:
PUSH_GENERAL_REGS
csrrd $t0, KSAVE_KSP
csrwr $r0, KSAVE_KSP
csrrd $t1, LA_CSR_PRMD
csrrd $t2, LA_CSR_ERA
st.d $t0, $sp, 3
st.d $t1, $sp, 32 // prmd
st.d $t2, $sp, 33 // era
move $a0, $sp
andi $t1, $t1, 0x3
bnez $t1, .Lexit_user
la.abs $ra, .Ltrap_return
b loongarch64_trap_handler
.Lexit_user:
ld.d $sp, $a0, 0
st.d $r0, $a0, 0
ld.d $ra, $sp, 0
ld.d $tp, $sp, 1
ld.d $fp, $sp, 2
ld.d $r21,$sp, 3
ld.d $s0, $sp, 4
ld.d $s1, $sp, 5
ld.d $s2, $sp, 6
ld.d $s3, $sp, 7
ld.d $s4, $sp, 8
ld.d $s5, $sp, 9
ld.d $s6, $sp, 10
ld.d $s7, $sp, 11
ld.d $s8, $sp, 12
addi.d $sp, $sp, 14 * 8
ret
.global enter_user
enter_user:
addi.d $sp, $sp, -14 * 8
st.d $ra, $sp, 0
st.d $tp, $sp, 1
st.d $fp, $sp, 2
st.d $r21,$sp, 3
st.d $s0, $sp, 4
st.d $s1, $sp, 5
st.d $s2, $sp, 6
st.d $s3, $sp, 7
st.d $s4, $sp, 8
st.d $s5, $sp, 9
st.d $s6, $sp, 10
st.d $s7, $sp, 11
st.d $s8, $sp, 12
st.d $sp, $a0, 0
move $sp, $a0
csrwr $a0, KSAVE_KSP
.Ltrap_return:
ld.d $t0, $sp, 32 // prmd
ld.d $t1, $sp, 33 // era
csrwr $t0, LA_CSR_PRMD
csrwr $t1, LA_CSR_ERA
POP_GENERAL_REGS
ld.d $sp, $sp, 3
ertn
5.3.2.2. 分离式的初始化
初始化流程如下:
设置异常处理通用代码,这个时候和统一方式不同的是,每个例外都有各自对应的历程,
而中断则一般使用一个统一的代码处理逻辑。比如Linux中LoongArch相关的异常例外都是这种方式组织的。
但是也可以写成一个,多个不同的例外都使用同一个也是可以的。
配置
CSR.ECFG.VS != 0,表示使用分离式的中断处理。Note
CSR.ECFG.VS配置成多少,表示每个例外的可以放2VS 条指令配置中断的相应的局部使能位:
CSR.ECFG.LIE[12:0]对应于CSR.ESTAT.IS[12:0],如果想要使能哪一位中断源,则将其对应的LIE[x]位置1。
申请一个4K页对齐的内存空间,我们作为异常的基址EVaddr。
将上面申请的EVaddr地址写入CSR.EENTRY。发生异常时,用于参与计算入口地址。计算方法按照
Note
入口地址 = CSR.EENTRY + ecode * 2CSR.ECFG.VS+2
将我们的例外的处理函数以及中断的处理函数指令片段,复制到以EVaddr为Base地址相应的入口处。
Warning
拷贝指令时,不能超过2VS+2个字节,如果超过有可能操作例外程序的覆盖,因此在
组织汇编代码的历程时,一定要控制代码指令条数。可以使用绝对跳转指令la.abs sym
这种方式来处理。通过写CSR.CRMD.IE来控制全局的中断使能。
5.3.2.2.1. 异常处理程序的例子
下面是一个Linux内核的异常处理过程。
SYM_CODE_START(handle_vint)
UNWIND_HINT_UNDEFINED
BACKUP_T0T1
SAVE_ALL
la_abs t1, __arch_cpu_idle
LONG_L t0, sp, PT_ERA
/* 32 byte rollback region */
ori t0, t0, 0x1f
xori t0, t0, 0x1f
bne t0, t1, 1f
LONG_S t0, sp, PT_ERA
1: move a0, sp
move a1, sp
la_abs t0, do_vint
jirl ra, t0, 0
RESTORE_ALL_AND_RET
SYM_CODE_END(handle_vint)
处理中断的函数是do_vint,具体如下:
asmlinkage void noinstr do_vint(struct pt_regs *regs, unsigned long sp)
{
register int cpu;
register unsigned long stack;
irqentry_state_t state = irqentry_enter(regs);
cpu = smp_processor_id();
if (on_irq_stack(cpu, sp))
handle_loongarch_irq(regs);
else {
stack = per_cpu(irq_stack, cpu) + IRQ_STACK_START;
/* Save task's sp on IRQ stack for unwinding */
*(unsigned long *)stack = sp;
__asm__ __volatile__(
"move $s0, $sp \n" /* Preserve sp */
"move $sp, %[stk] \n" /* Switch stack */
"move $a0, %[regs] \n"
"bl handle_loongarch_irq \n"
"move $sp, $s0 \n" /* Restore sp */
: /* No outputs */
: [stk] "r" (stack), [regs] "r" (regs)
: "$a0", "$a1", "$a2", "$a3", "$a4", "$a5", "$a6", "$a7", "$s0",
"$t0", "$t1", "$t2", "$t3", "$t4", "$t5", "$t6", "$t7", "$t8",
"memory");
}
irqentry_exit(regs, state);
}
下面这是处理其他异常,比如ADE, ALE, BP, FPE, FPU, LSX等异常的流程。
.macro BUILD_HANDLER exception handler prep
.align 5
SYM_CODE_START(handle_\exception)
UNWIND_HINT_UNDEFINED
666:
BACKUP_T0T1
SAVE_ALL
build_prep_\prep
move a0, sp
la_abs t0, do_\handler
jirl ra, t0, 0
668:
RESTORE_ALL_AND_RET
SYM_CODE_END(handle_\exception)
.pushsection ".data", "aw", %progbits
SYM_DATA(unwind_hint_\exception, .word 668b - 666b)
.popsection
.endm
BUILD_HANDLER ade ade badv
BUILD_HANDLER ale ale badv
BUILD_HANDLER bce bce none
BUILD_HANDLER bp bp none
BUILD_HANDLER fpe fpe fcsr
BUILD_HANDLER fpu fpu none
BUILD_HANDLER lsx lsx none
BUILD_HANDLER lasx lasx none
BUILD_HANDLER lbt lbt none
BUILD_HANDLER ri ri none
BUILD_HANDLER watch watch none
BUILD_HANDLER reserved reserved none /* others */
下面代码段时,Linux中将各个例外处理函数的拷贝过程。
#define VECSIZE 0x200
static inline void setup_vint_size(unsigned int size)
{
unsigned int vs;
vs = ilog2(size/4);
if (vs == 0 || vs > 7)
panic("vint_size %d Not support yet", vs);
csr_xchg32(vs<<CSR_ECFG_VS_SHIFT, CSR_ECFG_VS, LOONGARCH_CSR_ECFG);
}
void per_cpu_trap_init(int cpu) {
// ......
setup_vint_size(VECSIZE);
configure_exception_vector();
// ......
}
/* Install CPU exception handler */
void set_handler(unsigned long offset, void *addr, unsigned long size)
{
memcpy((void *)(eentry + offset), addr, size);
local_flush_icache_range(eentry + offset, eentry + offset + size);
}
/* Interrupt numbers */
#define INT_SWI0 0 /* Software Interrupts */
#define INT_SWI1 1
#define INT_HWI0 2 /* Hardware Interrupts */
#define INT_HWI1 3
#define INT_HWI2 4
#define INT_HWI3 5
#define INT_HWI4 6
#define INT_HWI5 7
#define INT_HWI6 8
#define INT_HWI7 9
#define INT_PCOV 10 /* Performance Counter Overflow */
#define INT_TI 11 /* Timer */
#define INT_IPI 12
#define INT_NMI 13
#define INT_AVEC 14
/* ExcCodes corresponding to interrupts */
#define EXCCODE_INT_NUM (INT_AVEC + 1)
#define EXCCODE_INT_START 64
#define EXCCODE_INT_END (EXCCODE_INT_START + EXCCODE_INT_NUM - 1)
void __init trap_init(void)
{
long i;
/* Set interrupt vector handler */
for (i = EXCCODE_INT_START; i <= EXCCODE_INT_END; i++)
set_handler(i * VECSIZE, handle_vint, VECSIZE);
/* Set exception vector handler */
for (i = EXCCODE_ADE; i <= EXCCODE_BTDIS; i++)
set_handler(i * VECSIZE, exception_table[i], VECSIZE);
cache_error_setup();
local_flush_icache_range(eentry, eentry + 0x400);
}
unsigned long eentry;
unsigned long tlbrentry;
long exception_handlers[VECSIZE * 128 / sizeof(long)] __aligned(SZ_64K);
static void configure_exception_vector(void)
{
eentry = (unsigned long)exception_handlers;
tlbrentry = (unsigned long)exception_handlers + 80*VECSIZE;
csr_write64(eentry, LOONGARCH_CSR_EENTRY);
csr_write64(eentry, LOONGARCH_CSR_MERRENTRY);
csr_write64(tlbrentry, LOONGARCH_CSR_TLBRENTRY);
}
TLB相关的处理历程也是在函数setup_tlb_handler中拷贝到了目的入口处。
static void setup_tlb_handler(int cpu) {
/* The tlb handlers are generated only once */
if (cpu == 0) {
memcpy((void *)tlbrentry, handle_tlb_refill, 0x80);
local_flush_icache_range(tlbrentry, tlbrentry + 0x80);
for (int i = EXCCODE_TLBL; i <= EXCCODE_TLBPE; i++)
set_handler(i * VECSIZE, exception_table[i], VECSIZE);
}
}
上述代码的eentry是分配的4KB对齐的内存空间,在trap_init初始化的时候,将各自的
处理代码通过函数set_handler复制到了各自的入口处。
5.3.3. 异常发生时,CPU做了什么
当触发普通例外时,处理器硬件会进行如下操作:
❖ 将 CSR.CRMD 的 PLV、IE 分别存到 CSR.PRMD 的 PPLV、PIE 中,然后将 CSR.CRMD 的 PLV 置
为 0,IE 置为 0;❖ 对于支持 Watch 功能的实现,还要将 CSR.CRMD 的 WE 存到 CSR.PRMD 的 PWE 中,然后将
CSR.CRMD 的 WE 置为 0;❖ 将触发例外指令的 PC 值记录到 CSR.ERA 中;
❖ 跳转到例外入口处取指。
当软件执行 ERTN 指令从普通例外执行返回时,处理器硬件会完成如下操作:❖ 将 CSR.PRMD 中的 PPLV、PIE 值恢复到 CSR.CRMD 的 PLV、IE 中;
❖ 对于支持 Watch 功能的实现,还要将 CSR.PRMD 中的 PWE 值恢复到 CSR.CRMD 的 WE 中;
❖ 跳转到 CSR.ERA 所记录的地址处取指。
针对上述硬件实现,软件在例外处理过程的中途如果需要开启中断,需要保存 CSR.PRMD 中的 PPLV、
PIE 等信息,并在例外返回前,将所保存的信息恢复到 CSR.PRMD 中。
5.4. 特殊异常的处理
下面我们详细介绍下,LoongArch上几个特殊的异常或者中断的使用方法。
5.4.1. 定时器中断
LoongArch相关的定时器的状态控制寄存器如下所示:
TID: 定时器编号
处理器中每个定时器都有一个唯一可识别的编号,由软件配置在该寄存器中。每个定时器也同时唯一
对应着一个计时器,当软件使用 RDTIME 指令读取计时器数值时,一并返回的计时器 ID 号也就是与之对应
的定时器编号。
| 位 | 名字 | 读写 | 描述 |
|---|---|---|---|
| 31:0 | TID | RW | 定时器编号。软件可配置。处理器核复位期间,硬件可以将其复位成与 CSR.CPUID 中 CoreID 相同的值。 |
TCFG: 定时器配置
该寄存器是软件配置定时器的接口。定时器的有效位数由实现决定,因此该寄存器中 InitVal 域的位 宽也将随之变化。
| 位 | 名字 | 读写 | 描述 |
|---|---|---|---|
| 0 | En | RW | 定时器使能位。仅当该位为 1 时,定时器才会进行倒计时自减,并在减为 0 值时置起 定时中断信号。 |
| 1 | Periodic | RW | 定时器循环模式控制位。若该位为 1,定时器在倒计时自减至 0 时,在置起定时中断 信号的同时,还会自动定时器重新装载成 InitVal 域中配置的初始值,然后再下一个 时钟周期继续自减。若该位为 0,定时器在倒计时自减至 0 时,将停止计数直至软件 再次配置该定时器。 |
| n-1:2 | InitVal | RW | 定时器倒计时自减计数的初始值。要求该初始值必须是 4 的整倍数。硬件将自动在该 域数值的最低位补上两比特 0 后再使用。 |
| GRLEN-1:n | 0 | R | 只读恒为 0,写被忽略。 |
TVAL: 定时器数值
软件可通过读取该寄存器来获知定时器当前的计数值。定时器的有效位数由实现决定,因此该寄存器
中 TimeVal 域的位宽也将随之变化
| 位 | 名字 | 读写 | 描述 |
|---|---|---|---|
| n-1:0 | TimeVal | R | 当前定时器的计数值。 |
| GRLEN-1:n | 0 | R | 只读恒为 0,写被忽略。 |
CNTC: 计时器补偿
软件可以通过配置该寄存器来对计时器的读出值进行修正,最终的读出值为:计时器原始计数值+计时
器补偿值。需知配置该寄存器不可直接改变计时器的计数值。
| 位 | 名字 | 读写 | 描述 |
|---|---|---|---|
| GRLEN-1:0 | Compensation | RW | 软件可配置的计时器补偿值。 |
TICLR: 定时中断清除
软件通过对该寄存器位 0 写 1 来清除定时器置起的定时中断信号。
| 位 | 名字 | 读写 | 描述 |
|---|---|---|---|
| 0 | CLR | W1 | 当对该 bit 写值 1 时,将清除时钟中断标记。该寄存器读出结果总为 0。 |
下面是Linux中,定时器的一些处理函数,比如周期的方式,或者oneshot的方式等!
static irqreturn_t constant_timer_interrupt(int irq, void *data)
{
int cpu = smp_processor_id();
struct clock_event_device *cd;
/* Clear Timer Interrupt */
write_csr_tintclear(CSR_TINTCLR_TI);
cd = &per_cpu(constant_clockevent_device, cpu);
cd->event_handler(cd);
return IRQ_HANDLED;
}
static int constant_set_state_oneshot(struct clock_event_device *evt)
{
unsigned long timer_config;
raw_spin_lock(&state_lock);
timer_config = csr_read64(LOONGARCH_CSR_TCFG);
timer_config |= CSR_TCFG_EN;
timer_config &= ~CSR_TCFG_PERIOD;
csr_write64(timer_config, LOONGARCH_CSR_TCFG);
raw_spin_unlock(&state_lock);
return 0;
}
static int constant_set_state_periodic(struct clock_event_device *evt)
{
unsigned long period;
unsigned long timer_config;
raw_spin_lock(&state_lock);
period = const_clock_freq / HZ;
timer_config = period & CSR_TCFG_VAL;
timer_config |= (CSR_TCFG_PERIOD | CSR_TCFG_EN);
csr_write64(timer_config, LOONGARCH_CSR_TCFG);
raw_spin_unlock(&state_lock);
return 0;
}
需要注意的是,定时器中断通过向CSR.TICLR寄存器写1来清除定时中断信号!
/* Clear Timer Interrupt */
write_csr_tintclear(CSR_TINTCLR_TI);
5.4.2. 系统调用
系统调用(System Call)是用户空间程序与内核空间交互的唯一合法接口,允许应用程序
请求内核执行特权操作(如文件读写、进程控制等)。其核心作用包括:
资源访问控制:用户程序无法直接操作硬件或内核数据,需通过系统调用委托内核完成。
抽象硬件差异:提供统一的接口(如open/read),屏蔽底层设备差异。
安全隔离:通过权限校验(如UID/GID)防止非法访问。
LoongArch使用syscall 0产生系统调用异常。
SYM_CODE_START(handle_sys)
UNWIND_HINT_UNDEFINED
la_abs t0, handle_syscall
jr t0
SYM_CODE_END(handle_sys)
SYM_CODE_START(handle_syscall)
UNWIND_HINT_UNDEFINED
csrrd t0, PERCPU_BASE_KS
la.pcrel t1, kernelsp
add.d t1, t1, t0
move t2, sp
ld.d sp, t1, 0
addi.d sp, sp, -PT_SIZE
cfi_st t2, PT_R3
cfi_rel_offset sp, PT_R3
st.d zero, sp, PT_R0
csrrd t2, LOONGARCH_CSR_PRMD
st.d t2, sp, PT_PRMD
csrrd t2, LOONGARCH_CSR_CRMD
st.d t2, sp, PT_CRMD
csrrd t2, LOONGARCH_CSR_EUEN
st.d t2, sp, PT_EUEN
csrrd t2, LOONGARCH_CSR_ECFG
st.d t2, sp, PT_ECFG
csrrd t2, LOONGARCH_CSR_ESTAT
st.d t2, sp, PT_ESTAT
cfi_st ra, PT_R1
cfi_st a0, PT_R4
cfi_st a1, PT_R5
cfi_st a2, PT_R6
cfi_st a3, PT_R7
cfi_st a4, PT_R8
cfi_st a5, PT_R9
cfi_st a6, PT_R10
cfi_st a7, PT_R11
csrrd ra, LOONGARCH_CSR_ERA
st.d ra, sp, PT_ERA
cfi_rel_offset ra, PT_ERA
cfi_st tp, PT_R2
cfi_st u0, PT_R21
cfi_st fp, PT_R22
SAVE_STATIC
UNWIND_HINT_REGS
#ifdef CONFIG_KGDB
li.w t1, CSR_CRMD_WE
csrxchg t1, t1, LOONGARCH_CSR_CRMD
#endif
move u0, t0
li.d tp, ~_THREAD_MASK
and tp, tp, sp
move a0, sp
bl do_syscall
RESTORE_ALL_AND_RET
SYM_CODE_END(handle_syscall)
通过执行do_syscall函数,来处理具体的系统调用!
用户态使用下面的内敛汇编进入系统调用。
#define SYSCALL_CLOBBERLIST \
"$t0", "$t1", "$t2", "$t3", \
"$t4", "$t5", "$t6", "$t7", "$t8", "memory"
static inline long __syscall0(long n)
{
register long a7 __asm__("$a7") = n;
register long a0 __asm__("$a0");
__asm__ __volatile__ (
"syscall 0"
: "=r"(a0)
: "r"(a7)
: SYSCALL_CLOBBERLIST);
return a0;
}
static inline long __syscall1(long n, long a)
{
register long a7 __asm__("$a7") = n;
register long a0 __asm__("$a0") = a;
__asm__ __volatile__ (
"syscall 0"
: "+r"(a0)
: "r"(a7)
: SYSCALL_CLOBBERLIST);
return a0;
}
static inline long __syscall2(long n, long a, long b)
{
register long a7 __asm__("$a7") = n;
register long a0 __asm__("$a0") = a;
register long a1 __asm__("$a1") = b;
__asm__ __volatile__ (
"syscall 0"
: "+r"(a0)
: "r"(a7), "r"(a1)
: SYSCALL_CLOBBERLIST);
return a0;
}
5.4.3. 非对齐访问
所有取指操作的访存地址必须 4 字节边界对齐,否则将触发取指地址错例外(ADEF)(这个是错误,一般都是终止进程)。
除了原子访存指令、整数边界检查访存指令和浮点数边界检查访存指令外,其余的 load/store 访存指令
可以实现为允许访存地址不对齐。(这是架构允许的)
不过,在一个允许访存地址不对齐的实现中,系统态软件可以通过配置
CSR.MISC 中的 ALCL0~ALCL3 控制位,在 PLV0~PLV3 特权等级下,对这些 load/store 访存指令也进行地
址对齐检查。对于需要进行地址对齐检查的访存指令,如果其访问的地址不是自然对齐1的,将触发地址非
对齐例外(ALE)。
Note
除了以下指令:
原子访存指令
整数边界检查访存指令
浮点数边界检查访存指令
其余的Load/Store指令都支持非对齐访问。手册不强制实现!
但是在以下考虑成本的芯片中,也可以不实现硬件的非对齐访问。
处理的方式为:当检查访存指令为非对齐访问时,抛出ALE地址非对齐异常, 然后使用软件模拟此条访存指令的行为。
模拟非对齐访问的执行过程如下:
检查load/store指令的地址是否对齐,如果不对齐则抛出ALE异常。
此时当前的指令PC保存在CSR.ERA中,非对齐的地址保存在CSR.BADV中。
然后进入非对齐访问的处理函数。
读取当前的指令通过CSR.ERA,返回软件译码分析当前的指令INSN。
然后获得当前出错指令的需要加载或者存储的字节数。
如果是load操作,从内存中按照字节读出内存,然后陪凑成目标的值,然后写入到目标寄存器。
如果是store操作,则将寄存器的值,按照字节负责到指定的CSR.BADV内存中。
然后CSR.ERA+4,执行下一条指令。
最好使用指令era返回。
更加详细的代码可访问Linux内核源码arch/loongarch/kernel/unaligned.c。