FuShenxiao

  • 2025-02-05
  • 加入了学习《赛灵思FPGA开发板图像及其数字处理》,观看 实现 CMOS 图像采集_GC0308

  • 加入了学习《赛灵思FPGA开发板图像及其数字处理》,观看 赛灵思 FPGA 开发板 图像及其数字处理

  • 2025-02-04
  • 发表了主题帖: 《RISC-V 体系结构编程与实践(第2版)》——异常与中断处理

    本帖最后由 FuShenxiao 于 2025-2-4 23:30 编辑 异常处理 异常触发和返回的流程如下图所示   与M模式相关的异常寄存器包括mstatus、mtvec、mcause、mie、mtval、mip、mideleg以及medeleg等 与S模式相关的异常寄存器有sstatus、sie、sip、scause以及stvec等 异常发生时需要保存发生异常的现场,以免破坏异常发生前正在处理的数据和程序状态,在S模式下的异常上下文保存和恢复如下     在M模式下的异常上下文保存和恢复如下     实验8-1:在SBI中实现串口输入功能 在MySBI固件中实现SBI_CONSOLE_GETCHAR的服务接口并测试 首先需要调用ECALL指令 #define SBI_CALL(which, arg0, arg1, arg2) ({ \ register unsigned long a0 asm ("a0") = (unsigned long)(arg0); \ register unsigned long a1 asm ("a1") = (unsigned long)(arg1); \ register unsigned long a2 asm ("a2") = (unsigned long)(arg2); \ register unsigned long a7 asm ("a7") = (unsigned long)(which); \ asm volatile ("ecall" \ : "+r" (a0) \ : "r" (a1), "r" (a2), "r" (a7) \ : "memory"); \ a0; \ }) 其中,which参数用于表示SBI固件的扩展ID,arg0~arg2为要传递的三个参数,为了方便调用,实现如下3个宏。 /* * 陷入到M模式,调用M模式提供的服务。 * SBI运行到M模式下 */ #define SBI_CALL_0(which) SBI_CALL(which, 0, 0, 0) #define SBI_CALL_1(which, arg0) SBI_CALL(which, arg0, 0, 0) #define SBI_CALL_2(which, arg0, arg1) SBI_CALL(which, arg0, arg1, 0) 根据SBI规范,定义串口的调用号 #define SBI_CONSOLE_PUTCHAR 0x1 #define SBI_CONSOLE_GETCHAR 0x2 static inline char sbi_getchar(void) { return SBI_CALL_1(SBI_CONSOLE_GETCHAR, 0); } 当处理器从S模式陷入M模式时,处理器首先会跳转到M模式的异常向量表,设置M模式下的异常向量表 extern void sbi_exception_vector(void); void sbi_trap_init(void) { /* 设置异常向量表地址 */ write_csr(mtvec, sbi_exception_vector); /* 关闭所有中断 */ write_csr(mie, 0); } 接着在MySBI固件中保存异常上下文 /* sbi_exception_vector M模式的异常向量入口 8字节对齐 */ .align 3 .global sbi_exception_vector sbi_exception_vector: /* 从mscratch获取M模式的sp,把S模式的SP保存到mscratch*/ csrrw sp, mscratch, sp addi sp, sp, -(PT_SIZE) sd x1, PT_RA(sp) sd x3, PT_GP(sp) sd x5, PT_T0(sp) sd x6, PT_T1(sp) sd x7, PT_T2(sp) sd x8, PT_S0(sp) sd x9, PT_S1(sp) sd x10, PT_A0(sp) sd x11, PT_A1(sp) sd x12, PT_A2(sp) sd x13, PT_A3(sp) sd x14, PT_A4(sp) sd x15, PT_A5(sp) sd x16, PT_A6(sp) sd x17, PT_A7(sp) sd x18, PT_S2(sp) sd x19, PT_S3(sp) sd x20, PT_S4(sp) sd x21, PT_S5(sp) sd x22, PT_S6(sp) sd x23, PT_S7(sp) sd x24, PT_S8(sp) sd x25, PT_S9(sp) sd x26, PT_S10(sp) sd x27, PT_S11(sp) sd x28, PT_T3(sp) sd x29, PT_T4(sp) sd x30, PT_T5(sp) sd x31, PT_T6(sp) /*保存mepc*/ csrr t0, mepc sd t0, PT_MEPC(sp) /*保存mstatus*/ csrr t0, mstatus sd t0, PT_MSTATUS(sp) /* 这里有两个目的: 1. 保存S模式的SP保存到 sbi_trap_regs->sp 2. 把M模式的SP保存到mscratch, 以便下次陷入到M模式时候可以得到SP */ addi t0, sp, PT_SIZE /* 此时的SP为M模式的SP, mscratch保存的是S模式的SP */ /* 把M模式的SP保存到mscratch,把S模式的SP保存到 栈框sbi_trap_regs->sp里*/ csrrw t0, mscratch, t0 sd t0, PT_SP(sp) /* 调用C语言的sbi_trap_handler */ mv a0, sp /* sbi_trap_regs */ call sbi_trap_handler 接着在MySBI固件入口函数_start(()中,把M模式下的SP保存到mscratch寄存器中 .globl _start _start: /* 关闭M模式的中断*/ csrw mie, zero /* 设置栈, 栈的大小为4KB */ la sp, stacks_start li t0, 4096 add sp, sp, t0 /* 把M模式的SP设置到mscratch寄存器, 下次陷入到M模式可以获取SP */ csrw mscratch, sp /* 跳转到C语言 */ tail sbi_main 接着在sbi_trap.c文件中实现sbi_trap_handler()函数 void sbi_trap_handler(struct sbi_trap_regs *regs) { unsigned long mcause = read_csr(mcause); unsigned long ecall_id = regs->a7; int rc = SBI_ENOTSUPP; const char *msg = "trap handler failed"; switch (mcause) { case CAUSE_SUPERVISOR_ECALL: rc = sbi_ecall_handle(ecall_id, regs); msg = "ecall handler failed"; break; default: break; } if (rc) { sbi_trap_error(regs, msg, rc); } } 这里通过mcause寄存器获取异常的类型,暂时忽略了中断的影响,然而如果判断异常类型为系统调用,则调用sbi_ecall_handle()函数来处理 static int sbi_ecall_handle(unsigned int id, struct sbi_trap_regs *regs) { int ret = 0; switch (id) { case SBI_CONSOLE_PUTCHAR: putchar(regs->a0); ret = 0; break; case SBI_CONSOLE_GETCHAR: regs->a0 = uart_get(); ret = 0; break; } /* 系统调用返回的是系统调用指令(例如ECALL指令)的下一条指令 */ if (!ret) regs->mepc += 4; return ret; } 异常处理完成后,恢复异常上下文,并从异常现场返回 /* save context*/ ld t0, PT_MSTATUS(sp) csrw mstatus, t0 ld t0, PT_MEPC(sp) csrw mepc, t0 ld x1, PT_RA(sp) ld x3, PT_GP(sp) ld x5, PT_T0(sp) ld x6, PT_T1(sp) ld x7, PT_T2(sp) ld x8, PT_S0(sp) ld x9, PT_S1(sp) ld x10, PT_A0(sp) ld x11, PT_A1(sp) ld x12, PT_A2(sp) ld x13, PT_A3(sp) ld x14, PT_A4(sp) ld x15, PT_A5(sp) ld x16, PT_A6(sp) ld x17, PT_A7(sp) ld x18, PT_S2(sp) ld x19, PT_S3(sp) ld x20, PT_S4(sp) ld x21, PT_S5(sp) ld x22, PT_S6(sp) ld x23, PT_S7(sp) ld x24, PT_S8(sp) ld x25, PT_S9(sp) ld x26, PT_S10(sp) ld x27, PT_S11(sp) ld x28, PT_T3(sp) ld x29, PT_T4(sp) ld x30, PT_T5(sp) ld x31, PT_T6(sp) ld sp, PT_SP(sp) mret 在主函数中,调用sbi_getchar()获取回车键是否按下 char c = sbi_getchar(); if (c == '\r') printk("enter has pressed\n"); 实验8-2:在BenOS中触发非法指令异常 通过篡改代码段里的指令代码实现非法指令异常的触发,使用如下代码将trigger_load_access_fault()汇编函数的第1行代码篡改了 void create_illegal_intr(void) { int *p = (int *)trigger_load_access_fault; *p = 0xbadbeef; } 其中原始的代码如下 .global trigger_load_access_fault trigger_load_access_fault: li a0, 0x70000000 ld a0, (a0) ret 实验8-3:输出触发异常时函数栈的调用过程 在BenOS中触发一个异常之后,输出函数栈的调用过程 显示触发异常后寄存器值 void show_regs(struct pt_regs *regs) { printk("sepc: %016lx ra : %016lx sp : %016lx\n", regs->sepc, regs->ra, regs->sp); printk(" gp : %016lx tp : %016lx t0 : %016lx\n", regs->gp, regs->tp, regs->t0); printk(" t1 : %016lx t2 : %016lx t3 : %016lx\n", regs->t1, regs->t2, regs->s0); printk(" s1 : %016lx a0 : %016lx a1 : %016lx\n", regs->s1, regs->a0, regs->a1); printk(" a2 : %016lx a3 : %016lx a4 : %016lx\n", regs->a2, regs->a3, regs->a4); printk(" a5 : %016lx a6 : %016lx a7 : %016lx\n", regs->a5, regs->a6, regs->a7); printk(" s2 : %016lx s3 : %016lx s4 : %016lx\n", regs->s2, regs->s3, regs->s4); printk(" s5 : %016lx s6 : %016lx s7 : %016lx\n", regs->s5, regs->s6, regs->s7); printk(" s8 : %016lx s9 : %016lx s10: %016lx\n", regs->s8, regs->s9, regs->s10); printk(" s11: %016lx t3 : %016lx t4: %016lx\n", regs->s11, regs->t3, regs->t4); printk(" t5 : %016lx t6 : %016lx\n", regs->t5, regs->t6); } 显示函数栈的调用过程 static void walk_stackframe(struct pt_regs *regs, bool (*fn)(unsigned long, void *), void *arg) { unsigned long sp, pc, fp; struct stackframe *frame; unsigned long low; if (regs) { pc = regs->sepc; sp = regs->sp; fp = regs->s0; } else { const register unsigned long current_sp __asm__ ("sp"); sp = current_sp; pc = (unsigned long)walk_stackframe; fp = (unsigned long)__builtin_frame_address(0); } while (1) { if (!kernel_text(pc) || (fn)(pc, arg)) break; /* 检查fp是否有效 */ low = sp + sizeof(struct stackframe); if ((fp < low || fp & 0x7)) break; /* * fp 指向上一级函数的栈底SP_p * 减去16个字节,正好是struct stackframe */ frame = (struct stackframe *)(fp - 16); sp = fp; fp = frame->fp; pc = frame->ra - 0x4; } } void show_stack(struct pt_regs *regs) { printk("Call Trace:\n"); walk_stackframe(regs, print_trace_address, NULL); } void dump_stack(struct pt_regs *regs) { show_stack(regs); } 将两者结合输出异常信息 static void do_trap_error(struct pt_regs *regs, const char *str) { printk("Oops - %s\n", str); dump_stack(regs); show_regs(regs); printk("sstatus:0x%016lx sbadaddr:0x%016lx scause:0x%016lx\n", regs->sstatus, regs->sbadaddr, regs->scause); panic(); } 所产生的异常与实验8-2相同 最后在终端显示寄存器值和函数栈的调用过程 do_exception, scause:0x5 Oops - Load access fault Call Trace: [<0x0000000080201a78>] trigger_load_access_fault+0x4/0xc [<0x00000000802016e0>] test_fault+0x10/0x28 [<0x0000000080201904>] kernel_main+0x6c/0x74 sepc: 0000000080201a78 ra : 00000000802016bc sp : 0000000080205fd0 gp : 0000000000000000 tp : 0000000000000000 t0 : 0000000000000005 t1 : 0000000000000005 t2 : 0000000080200020 t3 : 0000000080205fe0 s1 : 0000000080200010 a0 : 0000000070000000 a1 : 0000000000000000 a2 : 0000000000000000 a3 : 0000000080201028 a4 : 0000000000000031 a5 : 0000000000000031 a6 : 0000000000000000 a7 : 0000000000000001 s2 : 0000000000000000 s3 : 0000000000000000 s4 : 0000000000000000 s5 : 0000000000000000 s6 : 0000000000000000 s7 : 0000000000000000 s8 : 0000000080200034 s9 : 0000000000000000 s10: 0000000000000000 s11: 0000000000000000 t3 : 00510133000012b7 t4: 0000000000000000 t5 : 0000000000000000 t6 : 0000000000000000 sstatus:0x0000000000000100 sbadaddr:0x0000000070000000 scause:0x0000000000000005 Kernel panic 思考题: 8-1. 在RISC-V处理器中,异常有哪几种? 异常的类型主要有3种:中断、异常和系统调用 8-2. 同步异常和异步异常有什么区别? 同步异常是指处理器执行某条指令直接导致的异常,往往需要在异常处理程序里处理完该异常之后,处理器才能继续执行。 异步异常是指触发的原因与处理器当前正在执行的指令无关的异常。 8-3. 在RISC-V处理器中,异常发生时CPU自动做了哪些事情?操作系统需要做哪些事情? CPU做的事情: 1. 保存当前PC值到mepc寄存器,把异常的类型更新到mcause寄存器,把发生异常时的虚拟地址更新到mtval寄存器中 2. 保存异常发生前的中断状态,即把异常发生前的MIE字段保存到mstatus寄存器的MPIE字段中 3. 保存异常发生前的处理模式,即把异常发生前的处理器模式保存到mstatus寄存器的MPP字段中 4. 关闭本地中断,即设置mstatus寄存器中的MIE字段为0 5. 设置处理器模式为M模式 6. 跳转到异常向量表,即把mtvec寄存器的值设置到PC寄存器中 操作系统做的事情: 1. 保存异常发生时的上下文,包括所有通用寄存器的值和部分M模式下的寄存器的值。上下文保存到栈里 2. 查询mcause寄存器中的异常以及中断编号,跳转到对应的异常处理程序中 3. 异常或者中断处理完成之后,恢复保存在栈里的上下文 4. 执行MRET指令,返回异常现场 8-4. 返回时,异常是返回到发生异常的指令还是下一条指令? 对于中断,返回第一条还没执行或由于中断没有成功执行的指令 对于不是系统调用的同步异常,返回触发同步异常的那条指令 对于系统调用,返回的是系统调用指令的下一条指令 8-5. 返回时,异常如何选择处理器的执行状态? 根据MPP字段的值确定返回后的执行状态,触发异常时CPU运行在何种模式,异常处理结束后依然运行到那种模式 8-6. 请简述RISC-V体系结构的异常向量表。 当RISC-V处理器发生异常或中断时,会根据异常编码字段,结合异常向量表的配置,跳转到对应的处理程序。 8-7. 异常发生后,软件需要保存异常上下文,异常上下文包括哪些内容? 需要保存异常相关的寄存器以及各个通用寄存器的内容 8-8. 异常发生后,软件如何知道异常类型? 结合异常编号和异常向量表确定异常类型 中断处理与中断控制器 与异常处理类似,许多RISC-V处理器同时支持M模式和S模式下的中断。在默认情况下,中断会在M模式下处理,如果处理器支持S模式,那么可以有选择地把部分中断委托给S模式处理。 中断可分为4类:软件中断、定时器中断、外部中断、调试终端。按照功能可以分为两类:本地中断和全局中断。中断框图如下图所示。   与异常处理过程类似,中断处理过程如下图所示。   中断处理中有两个中断控制器:处理器内核本地中断器(CLINT)和平台级别的中断控制器(PLIC),其中PLIC处理外部设备(如串口设备等)的中断。 对于CLINT,一般支持软件中断、定时器中断这两种本地中断,它们属于处理器内核私有的中断,直接发送到处理器内核,而不需要经过中断控制器的路由,其结构如下图。CLINT支持的中断采用固定优先级策略,高优先级的中断可以抢占低优先级的中断。   PLIC主要用来管理外部中断,支持多个中断源和多个中断硬件上下文,某处理器一共支持8个中断硬件上下文,如下图所示。   实验9-1:定时器中断 (1) 在QEMU虚拟机中实现mtimer中断处理,并且在QEMU虚拟机上单步调试和观察。 设置定时器的系统调用号为0,定义接口为SBI_SET_TIMER /* * SBI提供timer服务 */ #define SBI_SET_TIMER 0 static inline void sbi_set_timer(unsigned long stime_value) { SBI_CALL_1(SBI_SET_TIMER, stime_value); } 使用sbi_set_timer()函数初始化mtimer #define CLINT_TIMEBASE_FREQ 10000000 #define HZ 1000 static inline unsigned long get_cycles(void) { return readq(VIRT_CLINT_TIMER_VAL); } #endif void reset_timer() { sbi_set_timer(get_cycles() + CLINT_TIMEBASE_FREQ/HZ); csr_set(sie, SIE_STIE); } void timer_init(void) { reset_timer(); } 在MySBI固件中实现定时器的系统调用服务并设置中断委托。在sbi_ecall_handle()函数中增加对SBI_SET_TIMER系统调用的处理 static int sbi_ecall_handle(unsigned int id, struct sbi_trap_regs *regs) { int ret = 0; switch (id) { case SBI_SET_TIMER: clint_timer_event_start(regs->a0); ret = 0; break; case SBI_CONSOLE_PUTCHAR: putchar(regs->a0); ret = 0; break; } /* 系统调用返回的是系统调用指令(例如ECALL指令)的下一条指令 */ if (!ret) regs->mepc += 4; return ret; } 其中,clint_timer_event_start()定义如下,该函数用于把下一次预设的定时器值next_event设置到mtimecmp寄存器中。 void clint_timer_event_start(unsigned long next_event) { /* Program CLINT Time Compare */ writeq(next_event, VIRT_CLINT_TIMER_CMP); /* 清S模式的timer pending中断,然后使能M模式的timer中断 */ csr_clear(mip, MIP_STIP); csr_set(mie, MIP_MTIP); } 触发定时器中断后,处理器首先在M模式响应该中断,因此需要先关闭M模式下的定时器中断,然后委托给S模式处理。 void sbi_trap_handler(struct sbi_trap_regs *regs) { unsigned long mcause = read_csr(mcause); unsigned long ecall_id = regs->a7; int rc = SBI_ENOTSUPP; const char *msg = "trap handler failed"; if (mcause & MCAUSE_IRQ) { mcause &=~ MCAUSE_IRQ; switch (mcause) { case IRQ_M_TIMER: sbi_timer_process(); break; default: msg = "unhandled external interrupt"; goto trap_error; } return; } switch (mcause) { case CAUSE_SUPERVISOR_ECALL: rc = sbi_ecall_handle(ecall_id, regs); msg = "ecall handler failed"; break; case CAUSE_LOAD_ACCESS: case CAUSE_STORE_ACCESS: msg = "load store access failed"; break; default: break; } trap_error: if (rc) { sbi_trap_error(regs, msg, rc); } } 当中断为定时器中断时,调用sbi_timer_process()函数来处理 void sbi_timer_process(void) { /* 关闭M模式timer的中断,然后设置S模式的timer pending中断*/ csr_clear(mie, MIP_MTIP); csr_set(mip, MIP_STIP); } 此外,还需要设置mideleg寄存器把M模式下的时钟中断委托给S模式 void delegate_traps(void) { unsigned long interrupts; interrupts = MIP_SSIP | MIP_STIP | MIP_SEIP; write_csr(mideleg, interrupts); } 当把中断委托给S模式后,CPU自动跳转到S模式下的异常向量表入口地址,即do_exception_vector()汇编函数。在do_exception_vector()汇编函数里,首先需要保存中断现场,然后跳转到中断处理函数do_exception(),该函数实现如下。 #define INTERRUPT_CAUSE_SOFTWARE 1 #define INTERRUPT_CAUSE_TIMER 5 #define INTERRUPT_CAUSE_EXTERNAL 9 void do_exception(struct pt_regs *regs, unsigned long scause) { const struct fault_info *inf; //printk("%s, scause:0x%lx, sstatus=0x%lx\n", __func__, scause, regs->sstatus); if (is_interrupt_fault(scause)) { switch (scause &~ SCAUSE_INT) { case INTERRUPT_CAUSE_TIMER: handle_timer_irq(); break; case INTERRUPT_CAUSE_EXTERNAL: /* handle IRQ */ break; case INTERRUPT_CAUSE_SOFTWARE: /* handle IPI */ break; default: printk("unexpected interrupt cause"); panic(); } } else { inf = ec_to_fault_info(scause); if (!inf->fn(regs, inf->name)) return; } } 需要调用handle_timer_irq()函数进行定时器中断。首先关闭S模式下的定时器中断,然后调用reset_timer()函数来重新给定时器设置初始值。 void handle_timer_irq(void) { csr_clear(sie, SIE_STIE); reset_timer(); jiffies++; printk("Core0 Timer interrupt received, jiffies=%lu\r\n", jiffies); } 执行完成handle_timer_irq()之后会返回do_exception_vector()汇编函数,调用kernel_exit宏来恢复中断现场,最后调用SRET指令返回中断现场 当然,还需要打开中断总开关,这一步需要完成:1. 使能sie寄存器中相应的中断类型; 2. 使能CPU的中断总开关,即sstatus寄存器中的SIE字段,代码实现如下 /* enable interrupts */ static inline void arch_local_irq_enable(void) { csr_set(sstatus, SR_SIE); } /* disable interrupts */ static inline void arch_local_irq_disable(void) { csr_clear(sstatus, SR_SIE); } 最后,在主函数里调用定时器初始化和开启中断的函数。 timer_init(); arch_local_irq_enable(); 得到输出结果如下   (2) 在汇编函数里实现保存中断现场的kernel_entry宏以及恢复中断现场的kernel_exit宏 kernel_entry宏 .macro kernel_entry addi sp, sp, -(PT_SIZE) sd x1, PT_RA(sp) sd x3, PT_GP(sp) sd x5, PT_T0(sp) sd x6, PT_T1(sp) sd x7, PT_T2(sp) sd x8, PT_S0(sp) sd x9, PT_S1(sp) sd x10, PT_A0(sp) sd x11, PT_A1(sp) sd x12, PT_A2(sp) sd x13, PT_A3(sp) sd x14, PT_A4(sp) sd x15, PT_A5(sp) sd x16, PT_A6(sp) sd x17, PT_A7(sp) sd x18, PT_S2(sp) sd x19, PT_S3(sp) sd x20, PT_S4(sp) sd x21, PT_S5(sp) sd x22, PT_S6(sp) sd x23, PT_S7(sp) sd x24, PT_S8(sp) sd x25, PT_S9(sp) sd x26, PT_S10(sp) sd x27, PT_S11(sp) sd x28, PT_T3(sp) sd x29, PT_T4(sp) sd x30, PT_T5(sp) sd x31, PT_T6(sp) csrr s1, sstatus sd s1, PT_SSTATUS(sp) /*保存sepc*/ csrr s2, sepc sd s2, PT_SEPC(sp) /*保存sbadaddr*/ csrr s3, sbadaddr sd s3, PT_SBADADDR(sp) /*保存scause*/ csrr s4, scause sd s4, PT_SCAUSE(sp) /*保存ssratch*/ csrr s5, sscratch sd s5, PT_TP(sp) /*保存SP*/ addi s0, sp, PT_SIZE sd s0, PT_SP(sp) .endm kernel_exit宏 .macro kernel_exit ld a0, PT_SSTATUS(sp) csrw sstatus, a0 ld a2, PT_SEPC(sp) csrw sepc, a2 ld x1, PT_RA(sp) ld x3, PT_GP(sp) ld x4, PT_TP(sp) ld x5, PT_T0(sp) ld x6, PT_T1(sp) ld x7, PT_T2(sp) ld x8, PT_S0(sp) ld x9, PT_S1(sp) ld x10, PT_A0(sp) ld x11, PT_A1(sp) ld x12, PT_A2(sp) ld x13, PT_A3(sp) ld x14, PT_A4(sp) ld x15, PT_A5(sp) ld x16, PT_A6(sp) ld x17, PT_A7(sp) ld x18, PT_S2(sp) ld x19, PT_S3(sp) ld x20, PT_S4(sp) ld x21, PT_S5(sp) ld x22, PT_S6(sp) ld x23, PT_S7(sp) ld x24, PT_S8(sp) ld x25, PT_S9(sp) ld x26, PT_S10(sp) ld x27, PT_S11(sp) ld x28, PT_T3(sp) ld x29, PT_T4(sp) ld x30, PT_T5(sp) ld x31, PT_T6(sp) ld x2, PT_SP(sp) .endm 实验9-2:使用汇编函数保存和恢复中断现场 (1) 在实验9-1的基础上,把kernel_entry和kernel_exit两个宏修改成使用汇编函数实现。思考若修改成用汇编函数实现,需要注意什么地方? 用汇编函数实现kernel_entry kernel_entry_func: addi sp, sp, -(PT_SIZE) /* 不用从把ra保存到栈里,此时的ra不是中断现场发生时的ra*/ // sd x1, PT_RA(sp) sd x3, PT_GP(sp) sd x5, PT_T0(sp) sd x6, PT_T1(sp) sd x7, PT_T2(sp) sd x8, PT_S0(sp) sd x9, PT_S1(sp) sd x10, PT_A0(sp) sd x11, PT_A1(sp) sd x12, PT_A2(sp) sd x13, PT_A3(sp) sd x14, PT_A4(sp) sd x15, PT_A5(sp) sd x16, PT_A6(sp) sd x17, PT_A7(sp) sd x18, PT_S2(sp) sd x19, PT_S3(sp) sd x20, PT_S4(sp) sd x21, PT_S5(sp) sd x22, PT_S6(sp) sd x23, PT_S7(sp) sd x24, PT_S8(sp) sd x25, PT_S9(sp) sd x26, PT_S10(sp) sd x27, PT_S11(sp) sd x28, PT_T3(sp) sd x29, PT_T4(sp) sd x30, PT_T5(sp) sd x31, PT_T6(sp) /* 禁止管理员访问用户内存 关闭FPU */ li t0, SR_SUM | SR_FS csrrc s1, sstatus, t0 sd s1, PT_SSTATUS(sp) /*保存sepc*/ csrr s2, sepc sd s2, PT_SEPC(sp) /*保存sbadaddr*/ csrr s3, sbadaddr sd s3, PT_SBADADDR(sp) /*保存scause*/ csrr s4, scause sd s4, PT_SCAUSE(sp) /*保存ssratch*/ csrr s5, sscratch sd s5, PT_TP(sp) /*保存SP*/ addi s0, sp, PT_SIZE sd s0, PT_SP(sp) ret 用汇编函数实现kernel_exit kernel_exit_func:     ld a0, PT_SSTATUS(sp)     csrw sstatus, a0     ld a2, PT_SEPC(sp)     csrw sepc, a2     /* 不用从栈中 取出 ra*/ //    ld x1,  PT_RA(sp)     ld x3,  PT_GP(sp)     ld x4,  PT_TP(sp)     ld x5,  PT_T0(sp)     ld x6,  PT_T1(sp)     ld x7,  PT_T2(sp)     ld x8,  PT_S0(sp)     ld x9,  PT_S1(sp)     ld x10, PT_A0(sp)     ld x11, PT_A1(sp)     ld x12, PT_A2(sp)     ld x13, PT_A3(sp)     ld x14, PT_A4(sp)     ld x15, PT_A5(sp)     ld x16, PT_A6(sp)     ld x17, PT_A7(sp)     ld x18, PT_S2(sp)     ld x19, PT_S3(sp)     ld x20, PT_S4(sp)     ld x21, PT_S5(sp)     ld x22, PT_S6(sp)     ld x23, PT_S7(sp)     ld x24, PT_S8(sp)     ld x25, PT_S9(sp)     ld x26, PT_S10(sp)     ld x27, PT_S11(sp)     ld x28, PT_T3(sp)     ld x29, PT_T4(sp)     ld x30, PT_T5(sp)     ld x31, PT_T6(sp)     ld x2,  PT_SP(sp)     ret (2) 使用QEMU虚拟机和GDB单步调试中断处理过程,重点观察保存中断现场和恢复中断现场的寄存器的变化以及栈的变化情况。 在保存中断现场阶段,mstatus,mcause,mepc,mscratch及其他通用寄存器等发生变化;在恢复中断现场阶段,mstatus,mepc寄存器及其他通用寄存器发生变化 在保存中断现场阶段,栈会被用来保存寄存器的值,因此需要分配一段栈空间,用于保存通用寄存器、部分 CSR 寄存器等;在恢复中断现场阶段,之前压入栈中的寄存器值会被弹出,恢复到相应的寄存器中。栈指针会相应地调整,恢复到中断发生前的状态。 实验9-3:实现并调试串口0中断 首先初始化PLIC,通过宏定义PLIC的寄存器 /* 设置每个中断的优先级 */ #define PLIC_PRIORITY(id) (PLIC_BASE + (id) * 4) /* 每个中断的pending位,一个bit表示一个中断 */ #define PLIC_PENDING(id) (PLIC_BASE + 0x1000 + ((id) / 32) * 4) /* 中断使能位: 每个处理器核心都有对应的中断使能位*/ #define PLIC_MENABLE(context) (PLIC_BASE + 0x2000 + (context) * 0x80) /* 设置每个处理器中每个中断的优先级threshold,当中断优先级大于threshold才会触发中断 */ #define PLIC_MTHRESHOLD(context) (PLIC_BASE + 0x200000 + (context) * 0x1000) /* Interrupt Claim Process*/ #define PLIC_MCLAIM(context) (PLIC_BASE + 0x200004 + (context) * 0x1000) /* Interrupt Completion*/ #define PLIC_MCOMPLETE(context) (PLIC_BASE + 0x200004 + (context) * 0x1000) 接着用plic_init()函数初始化PLIC int plic_init(void) { int i; int hwirq; for (i = 0; i < MAX_CPUS; i++) { /* 设置所有M模式处理器核心的中断优先级阈值为0*/ writel(0, PLIC_MTHRESHOLD(CPU_TO_CONTEXT(i))); for (hwirq = 1; hwirq <= MAX_PLIC_IRQS; hwirq++) { /*关闭PLIC中所有外部中断*/ plic_enable_irq(i, hwirq, 0); /* 预先设置所有中断号的优先级为1*/ plic_set_prority(hwirq, 1); } } csr_set(sie, SIE_SEIE); return 0; } 其中,plic_set_prority()和plic_enable_irq()实现如下 void plic_set_prority(int hwirq, int pro) { unsigned int reg = PLIC_PRIORITY(hwirq); writel(1, reg); } void plic_enable_irq(int cpu, int hwirq, int enable) { unsigned int hwirq_mask = 1 << (hwirq % 32); int context = CPU_TO_CONTEXT(cpu); unsigned int reg = PLIC_MENABLE(context) + 4*(hwirq / 32); printk("reg: 0x%x, hwirq:%d\n", reg, hwirq); if (enable) writel(readl(reg) | hwirq_mask, reg); else writel(readl(reg) & ~hwirq_mask, reg); } 使能串口0的接收中断,包括接收缓冲区满中断,以及使能PLIC中串口0对应的中断源 void uart_init(void) { unsigned int divisor = uart16550_clock / (16 * UART_DEFAULT_BAUD); /* disable interrupt */ writeb(0, UART_IER); /* Enable DLAB (set baud rate divisor)*/ writeb(0x80, UART_LCR); writeb((unsigned char)divisor, UART_DLL); writeb((unsigned char)(divisor >> 8), UART_DLM); /*8 bits, no parity, one stop bit*/ writeb(0x3, UART_LCR); /* 使能FIFO,清空FIFO,设置14字节threshold*/ writeb(0xc7, UART_FCR); /* 使能接收缓冲区满中断*/ writeb(0x1, UART_IER); } void enable_uart_plic() { /* TODO: using CPU0 now*/ int cpu = 0; uart_init(); plic_enable_irq(cpu, UART0_IRQ, 1); } 最后处理串口中断 当串口0的中断触发后,可以从scause寄存器读取中断类型 void do_exception(struct pt_regs *regs, unsigned long scause) { const struct fault_info *inf; //printk("%s, scause:0x%lx, sstatus=0x%lx\n", __func__, scause, regs->sstatus); if (is_interrupt_fault(scause)) { switch (scause &~ SCAUSE_INT) { case INTERRUPT_CAUSE_TIMER: handle_timer_irq(); break; case INTERRUPT_CAUSE_EXTERNAL: plic_handle_irq(regs); break; case INTERRUPT_CAUSE_SOFTWARE: /* handle IPI */ break; default: printk("unexpected interrupt cause"); panic(); } } else { inf = ec_to_fault_info(scause); if (!inf->fn(regs, inf->name)) return; } } 其中plic_handle_irq()实现如下 void plic_handle_irq(struct pt_regs *regs) { int hwirq; /*TODO: only CPU0 handle it*/ int context = CPU_TO_CONTEXT(0); unsigned int claim_reg = PLIC_MCLAIM(context); csr_clear(sie, SIE_SEIE); while((hwirq = readl(claim_reg))) { if (hwirq == UART0_IRQ) handle_uart_irq(); writel(hwirq, claim_reg); } csr_set(sie, SIE_SEIE); } 其中的handle_uart_irq()实现如下,可见这个串口中断的目的在于接收回车 void handle_uart_irq(void) { char c; c = uart_get(); if (c < 0) return; else if (c == '\r') { printk("%s occurred\n", __func__); } } 在按下回车键后可以看到输出   思考题: 9-1. 请简述中断处理的一般过程。 1. 保存中断发生前的中断状态,即把中断发生前的SIE位保存到sstatus寄存器的SPIE字段 2. 保存中断发生前的处理器模式状态,即把异常发生前的处理器模式编码保存到sstatus寄存器的SPP字段 3. 关闭本地中断,即设置sstatus寄存器中的SIE字段为0 4. 把中断类型更新到scause寄存器中 5. 把触发中断时的虚拟地址更新到stval寄存器中 6. 把当前PC值保存到sepc寄存器中 7. 跳转到异常向量表,即把stvec寄存器的值设置到PC寄存器中 操作系统软件读取以及解析scause寄存器的值来确定中断类型,然后跳转到相应的中断处理函数中 9-2. 什么是中断现场?对于RISC-V处理器来说,中断现场应该保存哪些内容? 中断现场是指当CPU接收到中断信号时,保存当前正在执行的程序的状态,包括程序计数器(PC)、寄存器状态以及其他与程序执行相关的上下文信息。 中断现场需要保存PC值、堆栈指针、中断相关的状态寄存器以及一些通用寄存器,以及中断上下文信息。 9-3. 中断现场保存到什么地方? 保存到中断相关的状态寄存器以及堆栈中。

  • 2025-02-02
  • 发表了主题帖: 《RISC-V 体系结构编程与实践(第2版)》——函数调用、GNU汇编器、链接器与内嵌汇编

    本帖最后由 FuShenxiao 于 2025-2-2 23:17 编辑 函数调用规范与栈 本章介绍了函数调用,其中函数调用的实现依赖于栈。在RISC-V中,栈布局和栈回溯的流程依赖于是否使用栈帧指针FP 不使用FP的函数栈布局的关键点: 1. 所有的函数调用栈都是从高地址向低地址扩展 2. SP永远指向栈顶(栈的最低地址) 3. 如果调用了子函数,函数的返回地址需要保存到栈里,即s_ra位置 4. 栈的大小为16字节的倍数 5. 函数返回时需要先把返回地址从栈中恢复到ra寄存器,然后执行RET指令 使用FP的函数栈布局的关键点: 1. 所有的函数调用栈都会组成一个单链表 2. 每个栈使用两个地址来构成这个链表,这两个地址都是64位宽的,并且他们都位于栈底。s_fp的值指向上一个栈帧的栈底;s_ra保存当前函数的返回地址 3. 函数返回时,RISC-V处理器先把返回地址从栈的s_ra位置处载入当前ra寄存器,后执行RET指令 4. 最末端函数不用保存ra寄存器,因为最末端函数的ra寄存器不会被破坏     实验4-1:观察栈布局 在BenOS里实现函数调用关系kernel_main()→func1()→func2(),然后用GDB 工具观察栈的变化情况,并画出栈布局 以书本中例4-3解释,该例首先在kernel_main()函数中调用子函数func1(),然后在func1()函数中调用add_c()函数 在boot.S汇编文件中分配栈空间,使SP指向0x8020 3000,然后跳转到C语言的kernel_main()函数 boot.S汇编文件如下 .section ".text.boot" .globl _start _start: /* 关闭中断 */ csrw sie, zero /* 设置栈, 栈的大小为4KB */ la sp, stacks_start li t0, 4096 add sp, sp, t0 /* 跳转到C语言 */ tail kernel_main .section .data .align 12 .global stacks_start stacks_start: .skip 4096 kernel.c文件如下 int add_c(int a, int b) { return a + b; } int func1(void) { int a = 1; int b = 2; return add_c(a, b); } void kernel_main(void) { func1(); } 在GDB调试器中,我们首先观察代码运行到kernel_main()函数的反汇编代码 (gdb) disassemble Dump of assembler code for function kernel_main: 0x0000000080200160 <+0>: addi sp,sp,-16 0x0000000080200162 <+2>: sd ra,8(sp) => 0x0000000080200164 <+4>: jal ra,0x8020013e <func1> 0x0000000080200168 <+8>: nop 0x000000008020016a <+10>: ld ra,8(sp) 0x000000008020016c <+12>: addi sp,sp,16 0x000000008020016e <+14>: ret 可以看到首先使用ADDI指令扩展栈空间,SP向低地址扩展16个字节,此时SP指向0x8020 2FF0 接着将kernel_main()函数的返回地址(ra寄存器)存储到SP+8的位置(s_ra) 创建栈之后,使用JAL指令跳转到func1()函数 进入func1()函数,再次进行反汇编得到代码 (gdb) disassemble Dump of assembler code for function func1: 0x000000008020013e <+0>: addi sp,sp,-32 0x0000000080200140 <+2>: sd ra,24(sp) => 0x0000000080200142 <+4>: li a5,1 0x0000000080200144 <+6>: sw a5,12(sp) 0x0000000080200146 <+8>: li a5,2 0x0000000080200148 <+10>: sw a5,8(sp) 0x000000008020014a <+12>: lw a4,8(sp) 0x000000008020014c <+14>: lw a5,12(sp) 0x000000008020014e <+16>: mv a1,a4 0x0000000080200150 <+18>: mv a0,a5 0x0000000080200152 <+20>: jal ra,0x80200124 <add_c> 0x0000000080200156 <+24>: mv a5,a0 0x0000000080200158 <+26>: mv a0,a5 0x000000008020015a <+28>: ld ra,24(sp) 0x000000008020015c <+30>: addi sp,sp,32 0x000000008020015e <+32>: ret 可以看到当跳转到func1()函数时,SP首先向低地址扩展32个字节,为func1()创建一个栈帧,此时SP指向0x8020 2FD0 接着将func1()函数的返回地址存储到SP+24的位置(s_ra) 由于func1()有两个临时变量a和b,观察反汇编代码箭头下方的代码,可以看到将二者分别存储到SP+12和SP+8的位置 最后跳转到add_c()函数,得到反汇编代码 (gdb) disassemble Dump of assembler code for function add_c: 0x0000000080200124 <+0>: addi sp,sp,-16 0x0000000080200126 <+2>: mv a5,a0 0x0000000080200128 <+4>: mv a4,a1 0x000000008020012a <+6>: sw a5,12(sp) 0x000000008020012c <+8>: mv a5,a4 0x000000008020012e <+10>: sw a5,8(sp) => 0x0000000080200130 <+12>: lw a4,12(sp) 0x0000000080200132 <+14>: lw a5,8(sp) 0x0000000080200134 <+16>: addw a5,a5,a4 0x0000000080200136 <+18>: sext.w a5,a5 0x0000000080200138 <+20>: mv a0,a5 0x000000008020013a <+22>: addi sp,sp,16 0x000000008020013c <+24>: ret 首先SP向低地址扩展16字节,为add_c()函数创建一个栈帧,此时SP指向0x8020 2FC0 在完成add_c()函数后,调用RET指令返回,首先编译器根据ra寄存器的返回地址,跳转到上一级函数 接着编译器释放空间栈,即SP指向func1()函数的栈顶 此时再观察func1()的反汇编代码,可以看到箭头已经跳转到func1()函数的栈顶 => 0x0000000080200158 <+26>: mv a0,a5 0x000000008020015a <+28>: ld ra,24(sp) 0x000000008020015c <+30>: addi sp,sp,32 0x000000008020015e <+32>: ret 或者也可以使用FP的栈布局 只需将Makefile中的编译选项从-fomit-frame-pointer改成-fno-omit-frame-pointer 对kernel_main()反汇编 (gdb) disassemble Dump of assembler code for function kernel_main: 0x00000000802001a4 <+0>: addi sp,sp,-16 0x00000000802001a6 <+2>: sd ra,8(sp) 0x00000000802001a8 <+4>: sd s0,0(sp) 0x00000000802001aa <+6>: addi s0,sp,16 => 0x00000000802001ac <+8>: jal ra,0x80200174 <func1> 0x00000000802001b0 <+12>: nop 0x00000000802001b2 <+14>: ld ra,8(sp) 0x00000000802001b4 <+16>: ld s0,0(sp) 0x00000000802001b6 <+18>: addi sp,sp,16 0x00000000802001b8 <+20>: ret 相较于不使用FP的栈布局,这里将fp寄存器的值存储到SP的位置(s_fp),并更新fp寄存器的值,其值对应kernel_main()函数的栈底,即0x8020 3000 对func1()反汇编 (gdb) disassemble Dump of assembler code for function func1: 0x0000000080200174 <+0>: addi sp,sp,-32 0x0000000080200176 <+2>: sd ra,24(sp) 0x0000000080200178 <+4>: sd s0,16(sp) 0x000000008020017a <+6>: addi s0,sp,32 => 0x000000008020017c <+8>: li a5,1 0x000000008020017e <+10>: sw a5,-20(s0) 0x0000000080200182 <+14>: li a5,2 0x0000000080200184 <+16>: sw a5,-24(s0) 0x0000000080200188 <+20>: lw a4,-24(s0) 0x000000008020018c <+24>: lw a5,-20(s0) 0x0000000080200190 <+28>: mv a1,a4 0x0000000080200192 <+30>: mv a0,a5 0x0000000080200194 <+32>: jal ra,0x8020014c <add_c> 0x0000000080200198 <+36>: mv a5,a0 0x000000008020019a <+38>: mv a0,a5 0x000000008020019c <+40>: ld ra,24(sp) 0x000000008020019e <+42>: ld s0,16(sp) 0x00000000802001a0 <+44>: addi sp,sp,32 0x00000000802001a2 <+46>: ret 可以看到这里将FP指向的值存储到SP+16位置(s_fp),此时s_fp上存储的值为上一个栈帧的底部 接着更新FP 指向的值为func1()函数的栈底 对于add_c()函数 (gdb) disassemble Dump of assembler code for function add_c: 0x000000008020014c <+0>: addi sp,sp,-32 0x000000008020014e <+2>: sd s0,24(sp) 0x0000000080200150 <+4>: addi s0,sp,32 0x0000000080200152 <+6>: mv a5,a0 0x0000000080200154 <+8>: mv a4,a1 0x0000000080200156 <+10>: sw a5,-20(s0) 0x000000008020015a <+14>: mv a5,a4 0x000000008020015c <+16>: sw a5,-24(s0) => 0x0000000080200160 <+20>: lw a4,-20(s0) 0x0000000080200164 <+24>: lw a5,-24(s0) 0x0000000080200168 <+28>: addw a5,a5,a4 0x000000008020016a <+30>: sext.w a5,a5 0x000000008020016c <+32>: mv a0,a5 0x000000008020016e <+34>: ld s0,24(sp) 0x0000000080200170 <+36>: addi sp,sp,32 0x0000000080200172 <+38>: ret 这里的操作方式就和上文类似了 实验4-2:观察栈回溯 在BenOS里实现函数调用关系kernel_main()到func1()到func2(),并实现栈回溯功能,输出栈的地址范围和大小,并通过GDB工具观察栈是如何回溯的 在stacktrace.c中实现栈回溯 #include "printk.h" #include "type.h" // 定义一个结构体描述栈结构中的s_fp和s_ra struct stackframe { unsigned long s_fp; unsigned long s_ra; }; // 检查地址是否在代码段中 extern char _text[], _etext[]; static int kernel_text(unsigned long addr) { if (addr >= (unsigned long)_text && addr < (unsigned long)_etext) return 1; return 0; } static void walk_stackframe(void ) { unsigned long sp, fp, pc; struct stackframe *frame; unsigned long low; const register unsigned long current_sp __asm__ ("sp");//通过内嵌汇编方式直接获取SP的值 sp = current_sp; pc = (unsigned long)walk_stackframe; fp = (unsigned long)__builtin_frame_address(0); while (1) { if (!kernel_text(pc)) break; /* 检查fp是否有效 */ low = sp + sizeof(struct stackframe); if ((fp < low || fp & 0xf)) break; /* * fp 指向上一级函数的栈底 * 减去16个字节,正好是struct stackframe */ frame = (struct stackframe *)(fp - 16); sp = fp; fp = frame->s_fp; pc = frame->s_ra - 4; if (kernel_text(pc)) printk("[0x%016lx - 0x%016lx] pc 0x%016lx\n", sp, fp, pc); } } void dump_stack(void) { printk("Call Frame:\n"); walk_stackframe(); } 得到每个栈的范围以及调用该函数时的PC值 Call Frame: [0x0000000080202fa0 - 0x0000000080202fb0] pc 0x0000000080200f32 [0x0000000080202fb0 - 0x0000000080202fd0] pc 0x000000008020114a [0x0000000080202fd0 - 0x0000000080202ff0] pc 0x0000000080201184 [0x0000000080202ff0 - 0x0000000080203000] pc 0x00000000802011a4 思考题 4-1. 请阐述RISC-V的函数调用规范 1. 函数的前8个参数使用a0~a7寄存器来传递。 2. 如果函数参数大于8个,前8个参数使用寄存器来传递,后面的参数使用栈来传递。 3. 如果传递的参数小于寄存器宽度(64位),那么先按符号扩展到32位,再按符号扩展到64位。如果传递的参数为寄存器宽度的2倍(128位),那么将使用一对寄存器来传递该参数。 4. 函数的返回参数保存在a0和a1寄存器中。 5. 函数的返回地址保存在ra寄存器中。 6. 如果子函数里使用s0~s11寄存器,那么子函数在使用前需要把这些寄存器的内容保存到栈中,使用完之后再从栈中将内容恢复到这些寄存器里。 7. 栈向下增长(向较低的地址),sp寄存器在进入函数时要对齐到16字节的边界上。传递给栈的第一个参数位于sp寄存器的偏移量0处,后续的参数存储则在相应的较高地址处。 8. 如果GCC使用-fno-omit-frame-pointer编译选项,那么编译器将使用s0寄存器作为栈帧指针。 4-2. 在函数调用过程中,如果函数传递的参数大于8个,该如何传递参数? 前8个参数使用寄存器来传递,后面的参数使用栈来传递。 4-5. 请阐述在RISC-V体系结构下如何通过FP回溯整个栈 1. 获取当前帧指针:从当前函数的FP寄存器获取当前栈帧的指针。 2. 获取返回地址:从当前栈帧中获取返回地址(通常存储在ra寄存器的栈位置)。 3. 更新帧指针:通过当前栈帧中的帧指针值(s0的值),找到上一个栈帧的指针。 4. 重复上述过程:不断更新帧指针,直到到达栈底(通常可以通过比较帧指针与栈顶或栈底地址来判断) GNU汇编器 汇编器用于将汇编代码翻译为机器目标代码,在编译过程中的位置如下图所示   本章介绍了相关汇编语法、常用的伪指令以及RISC-V特有的命令行选项和伪指令,熟悉了汇编的使用以及汇编和C语言的交互。 实验5-4:使用汇编伪指令实现一张表 使用汇编的数据定义伪指令,实现表的定义 在Linux内核中,.QUAD用于在代码段或数据段中分配并初始化一个 64 位的存储空间。它通常用于定义 64 位的常量或变量。.ASCIZ用于定义以空字符结尾的字符串。 .align 3 .global func_addr func_addr: .quad 0x800800 .quad 0x800860 .quad 0x800880 .align 3 .global func_string func_string: .asciz "func_a" .asciz "func_b" .asciz "func_c" 在主函数中打印相应地址的函数名 print_func_name(0x800880); print_func_name(0x800860); print_func_name(0x800800); 输出函数名 func_c func_b func_a 实验5-5:汇编宏的使用 在汇编文件中通过一个宏实现如下两个汇编函数 long add_1(a, b) long add_2(a, b) 该宏的定义如下 .macro op_func, label, a, b // 这里调用add_1()或者add_2()函数,label等于1或者2 .endm  编写对应的宏 .align 3 .macro op_func op, a, b mv a0, \a mv a1, \b call op_\()\op .endm .align 3 .global op_1 op_1: add a0, a0, a1 ret .global op_2 op_2: sub a0, a0, a1 ret .global macro_test_1 macro_test_1: addi sp, sp, -16 sd ra, 8(sp) mv t0, a0 mv t1, a1 op_func 1, t0, t1 ld ra, 8(sp) addi sp, sp, 16 ret .global macro_test_2 macro_test_2: addi sp, sp, -16 sd ra, 8(sp) mv t0, a0 mv t1, a1 op_func 2, t0, t1 ld ra, 8(sp) addi sp, sp, 16 ret 在主函数中调用这两个宏 val1 = macro_test_1(5, 5); if (val1 == 10) uart_send_string("lab5-5: macro_test_1 ok\n"); val2 = macro_test_2(5, 5); if (val2 == 0) uart_send_string("lab5-5: macro_test_2 ok\n"); 得到输出结果 lab5-5: macro_test_1 ok lab5-5: macro_test_2 ok 思考题: 5-1. 什么是汇编器? 汇编器是将汇编代码翻译为及其目标代码的程序。 5-2. 如何给汇编代码添加注释? 以“//”或“#”开始,其后同行的内容为注释。 “/* */”用于添加跨行注释。 5-3. 什么是符号? 用于标记程序或数据的位置。 5-4. 什么是伪指令? 伪指令是对汇编器发出的命令,它在源程序汇编期间由汇编器处理。伪指令是由汇编器预处理的指令,可以分解为几条指令的集合。 5-5. 在RISC-V汇编语言中,".align 3"表示什么意思? 按照8字节对齐。 5-6. 下面这条使用了伪指令的语句表示什么意思? .section ".my.text","awx" 接下来的汇编会链接到".my.text"段 这个段具有“可分配属性”“可写属性”“可执行属性” 5-7. 在汇编宏里,如何使用参数? 使用.MACRO和.ENDM伪指令可以用来组成一个宏 .MACRO伪指令后依次是宏名称与宏的参数 5-8. 下面是my_entry宏的定义 .macro my_entry, rv, label j rv\()\rv\()_\label .endm 下面的语句调用my_entry宏,请解释该宏是如何展开的 my_entry 1, irq 第一个"rv"表示rv字符,第一个"\()"在汇编宏视线中可以用来表示宏参数的结束字符,第二个"\rv"表示宏的参数rv,第二个"\()"用来表示结束字符,最后的"\label"表示宏的参数label 宏展开后,上述的J指令变成j rv1_irq 5-9. 请阐述.SECTION和.PREVIOUS伪指令的作用 两条伪指令通常配对使用,用来把一段汇编代码链接到特定的段。.SECTION伪指令表示开始一个新的段,.PREVIOUS伪指令表示恢复到.SECTION定义之前的段,以那个段作为当前段。 链接器与链接脚本 链接器和链接脚本常用于代码编译过程,这一章中,链接脚本的设计和重定位让程序的加载地址、运行地址、链接地址能根据实际进行配置。链接器的松弛优化可以减少指令数量来提高代码效率。 实验6-1:分析链接脚本 分析以下链接脚本中每条语句的含义 SECTIONS { /* * 设置benos的加载入口地址为0x80200000 * * 这里“.”表示location counter,当前位置 */ . = 0x80200000, /* * 这里是第一个段text.boot,起始地址就是0x80200000 * 这个段存放了benos的第一条指令 */ .text.boot : { *(.text.boot) } /* * text代码段 */ .text : { *(.text) } /* * 只读数据段 */ .rodata : { *(.rodata) } /* * 数据段 */ .data : { *(.data) } /* * bss段 * * ALIGN(8)表示8个字节对齐 * bss_begin的起始地址以8字节对齐 */ . = ALIGN(0x8); bss_begin = .; .bss : { *(.bss*) } bss_end = .; } 实验6-2:输出内存布局 (1) 在C语言中输出BenOS镜像文件的内存布局 使用如下代码显示BenOS镜像文件的内存布局 static void print_mem(void) { printk("BenOS image layout:\n"); printk(" .text.boot: 0x%08lx - 0x%08lx (%6ld B)\n", (unsigned long)_text_boot, (unsigned long)_etext_boot, (unsigned long)(_etext_boot - _text_boot)); printk(" .text: 0x%08lx - 0x%08lx (%6ld B)\n", (unsigned long)_text, (unsigned long)_etext, (unsigned long)(_etext - _text)); printk(" .rodata: 0x%08lx - 0x%08lx (%6ld B)\n", (unsigned long)_rodata, (unsigned long)_erodata, (unsigned long)(_erodata - _rodata)); printk(" .data: 0x%08lx - 0x%08lx (%6ld B)\n", (unsigned long)_data, (unsigned long)_edata, (unsigned long)(_edata - _data)); printk(" .bss: 0x%08lx - 0x%08lx (%6ld B)\n", (unsigned long)_bss, (unsigned long)_ebss, (unsigned long)(_ebss - _bss)); } 在终端运行程序后看到如下显示 BenOS image layout: .text.boot: 0x80200000 - 0x8020003c ( 60 B) .text: 0x8020003c - 0x802019a4 ( 6504 B) .rodata: 0x802019a4 - 0x80201c98 ( 756 B) .data: 0x80201c98 - 0x80203000 ( 4968 B) .bss: 0x80203010 - 0x80223420 (132112 B) 在benos.map中观察内存布局,可以看到与终端显示的运行地址和benos.map中的链接地址有些存在一些差异 内存配置 名称 来源 长度 属性 *default* 0x0000000000000000 0xffffffffffffffff 链结器命令稿和内存映射 0x0000000080200000 . = 0x80200000 0x0000000080200000 _text_boot = . .text.boot 0x0000000080200000 0x3c ... .text 0x0000000080200040 0x1964 ... .rodata 0x00000000802019a8 0x2f0 ... .data 0x0000000080202000 0x1000 ... .bss 0x0000000080203010 0x20410 (2) 修改链接脚本,把.data段的VMA修改成0x8020 9000,然后输出内存布局观察是否有变化 只需在linker.ld文件中修改数据段的起始值即可 /* * 数据段 */ . = 0x80209000, _data = .; .data : { *(.data) } _edata = .; 再次观察输出结果,可以看到.data的段的VMA变化会导致其之后的.bss段的VMA变化,且.data段的内存大小似乎变小了,这个问题与上面一个小问出现的问题似乎与数据对齐有关 BenOS image layout: .text.boot: 0x80200000 - 0x8020003c ( 60 B) .text: 0x8020003c - 0x802019a4 ( 6504 B) .rodata: 0x802019a4 - 0x80201c98 ( 756 B) .data: 0x80209000 - 0x8020a000 ( 4096 B) .bss: 0x8020a010 - 0x8022a420 (132112 B) (3) 编写C语言函数将.bss段的内容清零 static void clean_bss(void) { unsigned long start = (unsigned long)_bss; unsigned long end = (unsigned long)_ebss; unsigned size = end - start; memset((void *)start, 0, size); } 实验6-3:加载地址不等于运行地址 代码段存储在ROM中,运行地址在RAM里面,其他段的加载地址和运行地址都在RAM中。请修改BenOS的链接脚本以及汇编源代码,让BenOS可以正确运行 将代码段从加载地址(LMA)拷贝到运行地址(VMA) .globl _start _start: /* 假设代码段存储在ROM中(LMA),而ROM的地址在0x80300000 我们需要把代码段 从加载地址(LMA)拷贝到 运行地址(VMA) */ la t0, TEXT_ROM la t1, _text la t2, _etext .L0: ld a5, (t0) sd a5, (t1) addi t1, t1, 8 addi t0, t0, 8 bltu t1, t2, .L0 实验6-4:设置链接地址 修改BenOS的链接脚本,让其链接地址为0xFFFF 0000 0000 0000;查看benos.map文件,指出运行地址和链接地址的区别 在linker.ld的开头设置BenOS的链接地址为0xFFFF 0000 0000 0000 * 设置benos链接地址0xffff000000000000 */ . = 0xffff000000000000, 终端显示运行地址 BenOS image layout: .text.boot: 0x80200000 - 0x8020003c ( 60 B) .text: 0x80200040 - 0x802019a8 ( 6504 B) .rodata: 0x802019a8 - 0x80201c98 ( 752 B) .data: 0x80201c98 - 0x80203000 ( 4968 B) .bss: 0x80203010 - 0x80223420 (132112 B) benos.map中显示链接地址 内存配置 名称 来源 长度 属性 *default* 0x0000000000000000 0xffffffffffffffff 链结器命令稿和内存映射 0xffff000000000000 . = 0xffff000000000000 0xffff000000000000 _text_boot = . .text.boot 0xffff000000000000 0x3c ... .text 0xffff000000000040 0x1964 ... .rodata 0xffff0000000019a8 0x2f0 ... .data 0xffff000000002000 0x1000 ... .bss 0xffff000000003010 0x20410 可见BenOS的MMU能把内存映射到内存空间 实验6-5:链接器松弛优化1 在BenOS中,构造一个无法使用函数跳转优化的场景 int foo(void) { } int main(void) { foo(); } 实验6-6:链接器松弛优化2 在BenOS中,使能和测试符号地址访问优化 #include "uart.h" #include "type.h" #include "memset.h" #include "printk.h" long a = 5; long b = 10; long data(void) { return a | b; } void kernel_main(void) { uart_init(); init_printk_done(); data(); while(1) ; } 实验6-7:分析Linux 5.15内核的链接脚本 链接脚本位于arch/riscv/kernel/vmlinux.lds.S /* SPDX-License-Identifier: GPL-2.0-only */ /* * Copyright (C) 2012 Regents of the University of California * Copyright (C) 2017 SiFive */ // 定义只读异常表的对齐值为 16 字节 #define RO_EXCEPTION_TABLE_ALIGN 16 #ifdef CONFIG_XIP_KERNEL #include "vmlinux-xip.lds.S" #else #include <asm/pgtable.h> #define LOAD_OFFSET KERNEL_LINK_ADDR #include <asm/vmlinux.lds.h> #include <asm/page.h> #include <asm/cache.h> #include <asm/thread_info.h> #include <asm/set_memory.h> #include "image-vars.h" #include <linux/sizes.h> OUTPUT_ARCH(riscv) // 指定目标架构为 RISC-V ENTRY(_start) // 指定入口点为 _start jiffies = jiffies_64; // 将 jiffies(系统启动以来的时钟滴答数)指向 jiffies_64 // 定义了 PECOFF(Portable Executable and Common Object File Format)格式的对齐参数 PECOFF_SECTION_ALIGNMENT = 0x1000; PECOFF_FILE_ALIGNMENT = 0x200; SECTIONS { /* Beginning of code and text segment */ // 代码段 . = LOAD_OFFSET; _start = .; HEAD_TEXT_SECTION . = ALIGN(PAGE_SIZE); .text : { _text = .; _stext = .; TEXT_TEXT SCHED_TEXT CPUIDLE_TEXT LOCK_TEXT KPROBES_TEXT ENTRY_TEXT IRQENTRY_TEXT SOFTIRQENTRY_TEXT *(.fixup) _etext = .; } // 初始化代码段 . = ALIGN(SECTION_ALIGN); __init_begin = .; __init_text_begin = .; .init.text : AT(ADDR(.init.text) - LOAD_OFFSET) ALIGN(SECTION_ALIGN) { \ _sinittext = .; \ INIT_TEXT \ _einittext = .; \ } // 早期初始化表 . = ALIGN(8); __soc_early_init_table : { __soc_early_init_table_start = .; KEEP(*(__soc_early_init_table)) __soc_early_init_table_end = .; } __soc_builtin_dtb_table : { __soc_builtin_dtb_table_start = .; KEEP(*(__soc_builtin_dtb_table)) __soc_builtin_dtb_table_end = .; } /* we have to discard exit text and such at runtime, not link time */ .exit.text : { EXIT_TEXT } __init_text_end = .; . = ALIGN(SECTION_ALIGN); #ifdef CONFIG_EFI . = ALIGN(PECOFF_SECTION_ALIGNMENT); __pecoff_text_end = .; #endif /* Start of init data section */ __init_data_begin = .; INIT_DATA_SECTION(16) // 数据段 .exit.data : { EXIT_DATA } PERCPU_SECTION(L1_CACHE_BYTES) .rel.dyn : { *(.rel.dyn*) } __init_data_end = .; . = ALIGN(8); .alternative : { __alt_start = .; *(.alternative) __alt_end = .; } __init_end = .; /* Start of data section */ // 只读数据段 _sdata = .; RO_DATA(SECTION_ALIGN) .srodata : { *(.srodata*) } // 可读写数据段 . = ALIGN(SECTION_ALIGN); _data = .; RW_DATA(L1_CACHE_BYTES, PAGE_SIZE, THREAD_ALIGN) .sdata : { __global_pointer$ = . + 0x800; *(.sdata*) } #ifdef CONFIG_EFI .pecoff_edata_padding : { BYTE(0); . = ALIGN(PECOFF_FILE_ALIGNMENT); } __pecoff_data_raw_size = ABSOLUTE(. - __pecoff_text_end); #endif /* End of data section */ _edata = .; // BSS段 BSS_SECTION(PAGE_SIZE, PAGE_SIZE, 0) #ifdef CONFIG_EFI . = ALIGN(PECOFF_SECTION_ALIGNMENT); __pecoff_data_virt_size = ABSOLUTE(. - __pecoff_text_end); #endif _end = .; // 调试信息 STABS_DEBUG DWARF_DEBUG ELF_DETAILS DISCARDS } #endif /* CONFIG_XIP_KERNEL */ 思考题: 6-1. 什么是链接器?为什么链接器简称LD? 链接器是用来完成将目标文件(也包括用到的标准库函数目标文件)的代码段、数据段以及符号表等内容收集起来的工具。 早期操作系统的加载器(LD)做了所有的工作,后来LD成了链接器的代名词。 6-2. 链接脚本中的输入段和输出段有什么区别? 在链接脚本中,把输入文件中的段称为输入段,把输出文件中的段称为输出段。 输入段高速链接器如何将输入文件映射到内存布局,输出段高速链接器最终的可执行文件在内存中是如何布局的。 6-3. 什么是加载地址和虚拟地址? 加载地址是加载时段所在的地址 虚拟地址是运行时段所在的地址,也称为运行地址 6-4. 在链接脚本中定义如下符号 foo = 0x100 foo和0x100分别代表什么? foo为链接器在符号表中创建的一个名为foo的符号 0x100表示内存地址的位置 6-5. 在C语言中,如何引用链接脚本定义的符号? C语言中可以定义变量并用段为其赋值,或使用extern、memcpy的方法引用链接脚本定义的符号 6-6. 为了构建一个基于ROM的镜像文件,常常将输出段的虚拟地址和加载地址设置得不一致,在一个输入段中,如何表示一个段的虚拟地址和加载地址? 在链接脚本中,可以通过AT关键字来指定一个段的加载地址,同时使用ADDR函数来设置虚拟地址。 6-7. 什么是链接地址? 链接地址是在编译、链接时指定的地址,编程人员设想的程序将来要运行的地址。 6-8. 当一个程序的代码段和链接地址的加载地址不一致时,应该怎么做才能让程序正确运行? 需要将代码段从加载地址复制到链接地址。 6-9. 什么是与位置无关的指令?什么是与位置有关的指令?请举例说明RISC-V指令集中哪些指令是与位置无关的指令,哪些是与位置有关的指令。 与位置无关的指令是指那些在运行时不需要根据其加载地址进行调整的指令。这些指令的执行不依赖于其在内存中的具体位置。 与位置有关的指令是指那些在运行时需要根据其加载地址进行调整的指令。这些指令的执行依赖于其在内存中的具体位置。如果这些指令被加载到不同的内存地址,它们可能无法正确执行。 与位置无关的指令有ADD、SUB、AND、OR、XOR、JALR、LW、SW等指令 与位置有关的指令有LUI、JAL等指令 6-10. 什么是加载重定位和链接重定位? 加载重定位是指在程序被加载到内存时,根据程序的实际加载地址对代码和数据进行调整的过程。 链接重定位是指在程序链接阶段,将多个目标文件中的代码和数据段合并,并根据链接地址对符号引用进行调整的过程。 6-11. OpenSBI和Linux内核是如何实现重定位的? 如下图所示       6-12. 在Linux内核中,打开MMU之后如何实现重定位? 初始化页表→配置系统控制寄存器→开启MMU→重定位内核→处理重定位表→清除缓存→继续初始化 6-13. 什么是链接器松弛优化? 链接器松弛优化是一种在链接阶段进行的优化技术,目的是通过减少指令数量来提高代码的效率。这种优化通常发生在链接器处理目标文件(.o文件)时,通过将某些指令序列替换为更紧凑的指令来实现。 内嵌汇编代码 内嵌汇编代码在C语言中嵌入汇编代码,从而对特别重要和时间敏感的代码进行优化,同时在C语言中访问某些特殊指令来实现特殊功能。 本章内容以实验和思考题进行分析。 实验7-1:实现简单的memcpy()函数 使用内嵌汇编代码实现简单的memcpy()函数:从0x8020 000地址复制32字节到0x8021 000地址处 编写内嵌汇编代码 static void my_memcpy_asm_test1(unsigned long src, unsigned long dst, unsigned long size) { unsigned long tmp = 0; unsigned long end = src + size; asm volatile ( "1: ld %1, (%2)\n" "sd %1, (%0)\n" "addi %0, %0, 8\n" "addi %2, %2, 8\n" "blt %2, %3, 1b" : "+r" (dst), "+r" (tmp), "+r" (src) : "r" (end) : "memory"); } void inline_asm_test(void) { my_memcpy_asm_test1(0x80200000, 0x80210000, 32); } 在GDB调试工具中查看 (gdb) x 0x8020000 0x8020000: 0x00000000 (gdb) x 0x8021000 0x8021000: 0x00000000 实验7-2:使用汇编符号编写内嵌汇编代码 在实验7-1的基础上尝试使用汇编符号名编写内嵌代码的编写 使用汇编符号名的方式来编写内嵌汇编 static void my_memcpy_asm_test2(unsigned long src, unsigned long dst, unsigned long size) { unsigned long tmp = 0; unsigned long end = src + size; asm volatile ( "1: ld %[tmp], (%[src])\n" "sd %[tmp], (%[dst])\n" "addi %[dst], %[dst], 8\n" "addi %[src], %[src], 8\n" "blt %[src], %[end], 1b" : [dst] "+r" (dst), [tmp] "+r" (tmp), [src] "+r" (src) : [end] "r" (end) : "memory"); } 在GDB调试工具中查看相应地址的数据 (gdb) x 0x8020000 0x8020000: 0x00000000 (gdb) x 0x8021000 0x8021000: 0x00000000 实验7-3:实现内嵌汇编代码完善memset()函数 使用内嵌汇编代码完成__memset_16bytes()函数的编写 编写汇编代码 .global __memset_16bytes_asm __memset_16bytes_asm: li t0, 0 .loop: sd a1, (a0) sd a1, 8(a0) addi t0, t0, 16 blt t0, a2, .loop ret 在主函数中编写代码 memset((void *)0x80210005, 0x55, 40); 查看这个地址的数据 (gdb) x 0x80210005 0x80210005 <log_buf+52213>: 0x55555555 实验7-4:实现内嵌汇编代码与宏的结合 实现一个宏MY_OPS(ops, instruction),使其可以对某个内存地址实现or、xor、and、sub等操作 编写代码 static void my_ops_test(void) { unsigned long p; p = 0xf; my_asm_and(0x2, &p); printk("test and: p=0x%x\n", p); p = 0x80; my_asm_orr(0x3, &p); printk("test orr: p=0x%x\n", p); p = 0x3; my_asm_add(0x2, &p); printk("test add: p=0x%x\n", p); } 得到结果 test and: p=0x2 test orr: p=0x83 test add: p=0x5 实验7-5:实现读和写系统寄存器的宏 实现read_csr(csr)宏以及write_csr(val, csr)宏,读取RISC-V中的系统寄存器 代码实现 static void test_sysregs(void) { unsigned long val; val = read_csr(sstatus); printk("sstatus =0x%x\n", val); } 得到RISC-V中的系统寄存器 sstatus =0x0 实验7-6:基于goto模板的内嵌汇编代码 使用goto模板实现一个内嵌汇编函数,判断函数的参数是否为1。如果为1,则跳转到label,并且输出参数的值;否则,直接返回 int test_asm_goto(int a) 代码实现 static int test_asm_goto(int a) { asm goto ( "addi %0, %0, -1\n" "beqz %0, %l[label]\n" : : "r" (a) : "memory" : label); return 0; label: printk("%s: a = %d\n", __func__, a); return 1; } 同时设置相关输出 int a = 1; if (test_asm_goto(a) == 1) printk("asm_goto: return 1\n"); int b = 0; if (test_asm_goto(b) == 0) printk("asm_goto: b is not 1\n"); 得到终端输出 test_asm_goto: a = 1 asm_goto: return 1 asm_goto: b is not 1 思考题: 7-1. 在内嵌汇编代码中,修饰词"volatile""inline""goto"的作用分别是什么? volatile:用于关闭GCC优化 inline:用于内联,GCC会把汇编代码编译成尽可能短的代码 goto:用于从内嵌汇编代码跳转到C语言的标签处 7-2. 在内嵌汇编代码的输出部分里,"="和"+"分别代表什么意思? "=":被修饰的操作数只具有可写属性 "+":被修饰的操作数具有可读、可写属性 7-3. 在内嵌汇编代码中,如何表示输出部分和输入部分的参数? 使用内嵌汇编代码修饰符、汇编符号名字等 7-4. 内嵌汇编代码与C语言宏结合时,"#"与"##"分别代表什么意思? 在宏的参数前面添加"#",预处理器会把这个参数转换成一个字符串 "##"用于连接参数和另一个标识符,形成新的标识符

  • 2025-01-11
  • 回复了主题帖: 毕设记录(一)——无刷直流电机的无感控制

    Jacktang 发表于 2025-1-11 10:01 由于代码调用的是ST官方电机库的函数,因此底层机理难以修改,部分变量只能在ST的电机控制工具中查看,无法 ... 就是比如说我要写PID,ST电机库就有PID,但是这个PID就是单纯的PID,我想用改进PID算法,比如模糊PID啥的就比较难修改了。然后相关变量的值得在Motor Pilot里面看,具体寄存器值在程序里怎么看我还没写好查看方法。

  • 2025-01-10
  • 发表了主题帖: 毕设记录(一)——无刷直流电机的无感控制

    本帖最后由 FuShenxiao 于 2025-1-10 21:33 编辑 实验对象:P-NUCLEO-IHM03电机开发套件,包含无刷电机控制板、无刷电机驱动板以及三相云台电机,如图1。   图1 电机开发套件 由于三相云台电机没有霍尔传感器和光电编码器用于有感控制,因此采用转子电压过零检测和直流母线电流检测用于无刷电机速度和电流的双闭环控制,其中,电机驱动电路与电压过零检测电路如图2,其控制框图如图3,实际信号传输框图如图4。其中转速调节器和电流调节器均采用带饱和的PI控制,如图5。   图2 电机驱动与电压过零检测电路   图3 无刷电机双闭环控制框图   图4 实际信号传输框图   图5 带饱和环节的PI控制器 对于速度环,取Kp=3.85,Ki=0.0124 为了实现无刷电机转速和电流双闭环控制,对程序配置如下: 电机基本驱动实现:实现电机的基础驱动函数,启停、6步换向组合等。 初始化ADC:初始化ADC通道IO,设置ADC工作方式、开启DMA。 PID闭环控制实现:实现PID的初始参数赋值、PID计算等。 上位机通信协议:编写上位机通信代码,可在上位机实时显示当前速度。 编写中断服务函数:PWM中断用于换向控制、堵转检测等,添加PID的周期计算调用。 程序流程图如图6。   图6 控制程序流程图 对于无刷电机旋转状态,采用分阶段启动方式,其相关参数如表1,阶段1-阶段3相关参数的变化曲线如图7。 表1 无刷电机无感启动参数   持续时间(ms) 目标转速(rpm) 相电压(V rms) 阶段1 500 0 0.61 阶段2 1000 236 4.99 阶段3 500 236 4.99 阶段4   1000 4.99   图7 启动目标转速和相电压与时间关系 以1000rpm为无刷电机目标转速,得到无刷电机转速随时间变化曲线如图8。   图8 无刷电机转速变化   目前存在的问题:由于代码调用的是ST官方电机库的函数,因此底层机理难以修改,部分变量只能在ST的电机控制工具中查看,无法导出分析。   下一步计划:基于市面上其他基于STM32的电机控制案例重构代码,并建立基于电机开发套件中三相云台电机的模糊控制器控制规律。   工程文件

  • 回复了主题帖: 【测评入围名单(最后1批)】年终回炉:FPGA、AI、高性能MCU、书籍等65个测品邀你来~

    个人信息无误,确认可以完成测评计划

  • 2025-01-09
  • 发表了主题帖: FRDM-MCXA156开发板测评(一)——GPIO控制

    感谢EEWORLD提供的FRDM-MCXA156开发板测评机会,对开发板的测试首先从点灯开始。 GPIO的控制可以说是最基础的外设控制了,在MCUXprsso IDE中,对于外设引脚的配置可以说和STM32Cube IDE是类似的。 观察原理图,可以看到有一个用户按键、一个唤醒按键、一个RGB灯可以用于GPIO控制。           进入MCUXpresso IDE,选择 文件-新建-Create a new C/C++ project,选择开发板(选择开发板之后的好处在于board.h中已经定义了相关引脚的别名,方便调用)   接着为文件命名,并选择相关的依赖文件,最后点击完成生成工程文件   配置芯片的引脚信息,这里我们选择用户按键和RGB灯的三个引脚。其中用户按键设置为输入,且默认为高电平,RGB灯三个引脚设置为推挽输出,默认为低电平。   编写主函数如下,用于实现按键按下时,RGB灯三色均点亮,即发出白光 int main(void) { /* Init board hardware. */ BOARD_InitBootPins(); BOARD_InitBootClocks(); BOARD_InitBootPeripherals(); LED_RED_INIT(LOGIC_LED_OFF); LED_BLUE_INIT(LOGIC_LED_OFF); LED_GREEN_INIT(LOGIC_LED_OFF); /* Init FSL debug console. */ BOARD_InitDebugConsole(); while(1) { if(GPIO_PinRead(BOARD_SW3_GPIO, BOARD_SW3_GPIO_PIN) == 0) { LED_RED_TOGGLE(); LED_BLUE_TOGGLE(); LED_GREEN_TOGGLE(); while(GPIO_PinRead(BOARD_SW3_GPIO, BOARD_SW3_GPIO_PIN) == 0); } } return 0 ; } 演示视频: [localvideo]b70b628d73e1beb0778346677ac755c0[/localvideo] 代码文件:

  • 2025-01-08
  • 回复了主题帖: 【测评入围名单(最后1批)】年终回炉:FPGA、AI、高性能MCU、书籍等65个测品邀你来~

    个人信息无误,确认可以完成测评计划

  • 发表了主题帖: 《RISC-V 体系结构编程与实践(第2版)》——基础指令集

    本帖最后由 FuShenxiao 于 2025-1-8 10:49 编辑 在结束了考研/期末/毕设三者中的前两个之后,终于有时间来做测评了,这本书也是放了一个多月了才重新拿起来看。 在正式食用这本书之前,需要对系统进行配置,我采用的是利用VMware虚拟机在移动硬盘上安装Ubuntu20.04,这是由于去年8月微软的补丁,导致u盘启动可能会被认为是不安全的。我参考的是这篇CSDN上的文章:利用Vmware将Ubuntu系统安装到移动硬盘_如何做移动vmware的渗透镜像部署到移动硬盘-CSDN博客。如此制作好的移动硬盘就可以以双系统的方式启动。 随后安装QEMU实验平台以及其他相关依赖,即可在QEMU中进行RISC-V相关的实验。   基础指令集的知识似乎是通俗易懂的,各位通过其字面意思也能理解这些指令的含义,于是我打算直接借助实验和思考题对本章内容进行讲解。   实验3-1:熟悉加载指令集 (1)将0x80200000加载到a0寄存器,将立即数16加载到a1寄存器;(2)从0x80200000地址中读取4字节的数据;(3)从0x80200010地址中读取8字节的数据;(4)给出lui指令的直行结果 li rd, imm表示把imm(立即数)加载到寄存器中 lw rd, offset(rs1)表示以rs1寄存器的值为基地址,在偏移offset的地址处加载4字节数据,经过符号扩展之后写入目标寄存器rd中 ld rd, offset(rs1)表示以rs1寄存器的值为基地址,在偏移offset的地址处加载8字节数据,写入寄存器rd中 lui rd, imm表示先把imm(立即数)左移12位,然后进行符号扩展,最后把结果写入rd寄存器中 符号扩展指将得到的结果扩展为32位或64位,其高字节部分填充为1 同理,零扩展高字节部分填充为0 实现代码如下: load_store_test: li a0, 0x80200000 li a1, 16 li t0, 0x80200000 li t1, 0x80200010 lw a2, (t0) ld a3, (t1) lui t0, 0x8034f lui t1, 0x400 ret 在GDB调试器中查看得到结果如下:      实验3-2:PC相对寻址 auipc rd, imm将imm(立即数)左移12位并带符号扩展到64位后,得到一个新的立即数,这个新的立即数是一个有符号的立即数,再加上当前PC值,然后存储到rd寄存器中 addi rd, rs1, imm将rs1寄存器的值和12位的立即数imm相加,并将结果存入rd寄存器中 auipc指令通常和addi指令联合使用来实现32位地址空间的PC相对寻址。其中auipc指令可以寻址与被访问地址按4KB对齐的地方,即被访问地址的高20位;addi指令可以在[-2048, 2047]范围内寻址,即被访问地址的低12位。 代码实现如下: @define MY_OFFSET -2048 pc_related_test: auipc t0, 1 addi t0, t0, MY_OFFSET ld t1, MY_OFFSET(t0) ret 得到结果如下:     实验3-3:memcpy()函数的实现 sd rs2, offset(rs1)将rs2寄存器的值存储到以rs1寄存器的值位基地址加上offset的地址处 blt rs1, rs2, label表示如果rs1寄存器的值小于rs2寄存器的值,则跳转到label处 代码实现如下: my_memcpy_test: li t0, 0x80200000 li t1, 0x80210000 addi t2, t0, 32 .loop: ld t3, (t0) sd t3, (t1) addi t0, t0, 8 addi t1, t1, 8 blt t0, t2, .loop ret 得到结果如下,可以看到地址0x80200000地址的内容已经复制到0x80210000地址处     实验3-4:memset()函数的实现 memset.c代码如下: #include "memset.h" extern void *__memset_16bytes_asm(void *s, unsigned long val, unsigned long count); static void __memset_16bytes_inline_asm(void *p, unsigned long val, int count) { int i = 0; asm volatile ( "1: sd %[val], (%[p])\n" "sd %[val], 8(%[p])\n" "addi %[i], %[i], 16\n" "blt %[i], %[count], 1b" : [p] "+r" (p), [count]"+r" (count), [i]"+r" (i) : [val]"r" (val) : "memory" ); } static void *__memset_1bytes(void *s, int c, size_t count) { char *xs = s; while (count--) *xs++ = c; return s; } static void *__memset(char *s, int c, size_t count) { char *p = s; unsigned long align = 16; size_t size, left = count; int n, i; unsigned long address = (unsigned long)p; unsigned long data = 0ULL; /* 这里c必须转换成unsigned long类型 * 否则 只能设置4字节,因为c变量是int类型 */ for (i = 0; i < 8; i++) data |= (((unsigned long)c) & 0xff) << (i * 8); /*1. check start address is align with 16 bytes */ if (address & (align - 1)) { //fixme: 这里应该是 对齐的后半段 size = address & (align - 1); size = align - size; __memset_1bytes(p, c, size); p = p + size; left = count - size; } /*align 16 bytes*/ if (left > align) { n = left / align; left = left % align; #if 0 __memset_16bytes_asm(p, data, 16*n); #else __memset_16bytes_inline_asm(p, data, 16*n); #endif if (left) __memset_1bytes(p + 16*n, c, left); } return s; } void *memset(void *s, int c, size_t count) { return __memset(s, c, count); } memset.S代码如下: __memset_16bytes_asm: li t0, 0 .loop: sd a1, (a0) sd a1, 8(a0) addi t0, t0, 16 blt t0, a2, .loop ret 得到结果如下:     实验3-5:条件跳转指令1 实现当a>=b时,返回值为0,否则返回值为0xffffffffffffffff 代码实现如下: bltu与blt指令类似,只不过rs1寄存器的值和rs2寄存器的值位无符号数 compare_and_return: bltu a0,a1,.L2 li a5,0 j .L3 .L2: li a5,-1 .L3: mv a0,a5 ret 在主程序中编写如下代码显示大小比较结果 val1 = compare_and_return(10, 9); if (val1 == 0) uart_send_string("compare_and_return ok\n"); else uart_send_string("compare_and_return fail\n"); val2 = compare_and_return(9, 10); if (val2 == 0xffffffffffffffff) uart_send_string("compare_and_return ok\n"); else uart_send_string("compare_and_return fail\n"); 得到结果如下:   实验3-6:条件跳转指令2 当a=0时,返回b+2,否则返回b-1 代码实现如下: sel_test: beqz a0, .L4 addi a5, a1, -1 j .L5 .L4: addi a5, a1, 2 .L5: mv a0, a5 ret 在主程序中编写如下代码显示结果 val1 = sel_test(0, 9); if (val1 == 11) uart_send_string("sel test ok\n"); val2 = sel_test(5, 2); if (val2 == 1) uart_send_string("sel test ok\n"); 得到结果如下:   实验3-7:子函数跳转 为了完成子函数跳转,首先将ra保存到栈,接着调用子函数,在完成子函数程序后从栈中恢复ra返回地址 具体代码如下: branch_test: /*把返回地址ra寄存器保存到栈里*/ addi sp,sp,-8 sd ra,(sp) li a0, 1 li a1, 2 /* 调用add_test子函数 */ call add_test nop /* 从栈中恢复ra返回地址*/ ld ra,(sp) addi sp,sp,8 ret 实验3-8:在汇编中实现串口输出功能 在boot.S中,程序的初始化中首先调用了__init_uart和print_asm两个子函数,随后才进入C语言的主程序 .globl _start _start: /* 关闭中断 */ csrw sie, zero call __init_uart call print_asm /* 设置栈, 栈的大小为4KB */ la sp, stacks_start li t0, 4096 add sp, sp, t0 /* 跳转到C语言 */ tail kernel_main print_asm: /*此时SP栈空间还没分配,把返回地址ra保存到临时寄存器中*/ mv s1, ra la a0, boot_string call put_string_uart /*恢复返回地址ra*/ mv ra, s1 ret .section .data .align 12 .global stacks_start stacks_start: .skip 4096 .section .rodata .align 3 .globl boot_string boot_string: .string "Booting at asm\n" C语言中的main函数如下: void kernel_main(void) { uart_init(); uart_send_string("Welcome RISC-V!\r\n"); while (1) { ; } } 可以看到最后首先输出"Booting at asm"接着输出"Welcome RISC-V!"   思考题1:RISC-V指令集有什么特点? RISC-V采用模块化的设计方法,即设计一个最小的和最基础的指令集,这个最小的指令集可以完整地实现一个软件栈,其他特殊功能的指令集可以在最小指令集的基础上通过模块化的方式叠加实现,用于支持浮点数运算指令、乘法和除法指令等。 思考题2:RISC-V指令编码格式可以分成几类? R类型:寄存器和寄存器算术指令 I类型:寄存器和立即数算术指令或者加载指令 S类型:存储指令 B类型:条件跳转指令 U类型:长立即数操作指令 J类型:无条件跳转指令 思考题3:什么是零扩展和符号扩展? 零扩展为计算机系统把小字节数据转换成大字节数据,将符号位扩展至所需要的位数,高位填充0 符号扩展与零扩展类似,高位填充1(0xFF) 思考题4:什么是PC相对寻址? PC加上指令中给出的偏移量,得到操作数的实际地址。 思考题5:假设当前PC值位0x80200000,分别执行如下指令,a5和a6寄存器的值分别是多少? auipc a5, 0x2 lui a6, 0x2 a5寄存器的值为 PC+sign_extend(0x2 << 12) = 0x80200000 + 0x2000 = 0x80202000 a6寄存器的值为 0x2 << 12 = 0x2000 思考题6:在下面的指令中,a1和t1寄存器的值分别是多少? li t0, 0x8000008a00000000 srai a1, t0, 1 srli t1, t0, 1 srai为算数右移,高位需要进行符号扩展,a1寄存器的值为0xC000 0045 0000 0000 srli为立即数逻辑右移,高位需要进行零扩展,t1寄存器的值为0x4000 0045 0000 0000 思考题7:假设执行如下各条指令时当前的PC值位0x80200000,则下面那些指令是非法指令? jal a0, 0x800fffff jal a0, 0x80300000 两条指令都会出错,因为两条指令都超过了jal指令的跳转范围,jal指令的跳转范围为[0x8010 0000, 0x802F FFFE] 思考题8:请解析下面这条指令的含义 csrrw tp, sscratch, tp 先读取sscratch寄存器的旧值并写入tp寄存器,再将tp寄存器的旧值写入sscratch寄存器 用C语言伪代码实现如下: tp = sscratch; sscratch = tp; 思考题9:在RISC-V指令集中,如何实现大范围和小范围内跳转? 使用auipc与jalr指令实现基于当前PC偏移量±2GB范围的跳转 使用jal指令实现基于当前PC偏移量±1MB范围的跳转

  • 2024-12-19
  • 回复了主题帖: 测评入围名单: NXP MCX A系列 FRDM-MCXA156开发板

    个人信息无误,确认可以完成测评分享计划

  • 2024-11-27
  • 发表了主题帖: 《RISC-V 体系结构编程与实践(第2版)》——一些阅读前的准备工作

    本帖最后由 FuShenxiao 于 2024-11-27 09:52 编辑 感谢EEWORLD提供的《RISC-V 体系结构编程与实践(第2版)》书籍测评机会。由于目前考研在即,这里就写一些我的准备工作,以及一些资料网站等,具体阅读任务将在12月底考研完成后进行,并在寒假参与中科院的“一生一芯”项目。   相关操作环境配置: 操作系统:Ubuntu Linux 20.04 GCC版本:9(riscv64-linux-gnu-gcc) QEMU版本:4.2.1 GDB版本:gdb-multiarch   一些官方网站: RISC-V官网:RISC-V International – RISC-V: The Open Standard RISC Instruction Set Architecture (riscv.org) 香山处理器文档:香山 XiangShan (openxiangshan.cc) “一生一芯”项目官网:一生一芯 (oscc.cc) “甲辰计划”官网:甲辰计划 (RISC-V Prosperity 2036) | 甲辰计划 (rv2036.org)   一些RISC-V相关的资料: RISC-V指令集手册   RISC-V体系结构手册   GCC官方手册   汇编器(AS)官方手册   链接器(LD)手册   RVV手册   RISC-V高速缓存维护指令扩展手册   PLIC手册   RISC-V ABI接口手册   RISC-V SBI接口手册   一些视频教程: [完结] 循序渐进,学习开发一个RISC-V上的操作系统 - 汪辰 - 2021春_哔哩哔哩_bilibili “一生一芯”概述 [第六期“一生一芯”计划 - P1]_哔哩哔哩_bilibili    

  • 2024-11-20
  • 回复了主题帖: 读书入围名单:《RISC-V 体系结构编程与实践(第2版)》

    个人信息无误,确认可以完成阅读计划和打卡任务

  • 2024-11-06
  • 回复了主题帖: 【求助】小学六年级的题目,我硬是不会做

    okhxyyo 发表于 2024-11-6 17:23 嘿,你这个图画的好。我感觉一定不是这么算,一定是哪里有个小窍门。我想想啊 这几天在考研,这种是二重积分极坐标法的经典题型:Sad:

  • 回复了主题帖: 【求助】小学六年级的题目,我硬是不会做

    okhxyyo 发表于 2024-11-6 16:07 不知道怎么入手了都 感觉像是考研难度的,得用到二重积分

  • 2024-11-02
  • 加入了学习《DIY作品演示》,观看 阿尔达H-30T高温休眠版恒温电烙铁试用体验

  • 2024-10-24
  • 回复了主题帖: STM32H7S78-DK测评(四)——RTC测试

    freebsder 发表于 2024-10-24 19:19 S又是个主打哪个方向的型号? 我觉得主要是它的高性能图像显示能力吧,主要吸引人的是它DMA2D,也就是GPU功能,这块开发板的屏幕有480*800,已经挺大的了。

  • 发表了主题帖: STM32H7S78-DK测评(四)——RTC测试

    本帖最后由 FuShenxiao 于 2024-10-24 16:58 编辑 STM32的RTC指的是实时时钟(Real-Time Clock),它是一种用于计时和日期记录的硬件模块。在STM32微控制器中,RTC模块是由一个32位的计数器和一组用于保存日期和时间的寄存器组成的。由于RTC具有较高的精度和稳定性,从而能保持准确的日期和时间信息,因此它常用于需要精确定时和实时数据处理的应用场景。 本来以为RTC实现挺简单的,没想到这里还有那么多坑,主要是CubeMX代码生成存在一些问题。 在CubeMX中配置 选择STM32H7S78-DK模板 配置RTC      为了能输出时间和日期,还需要配置UART4   代码编写 生成RTC初始化代码如下 void MX_RTC_Init(void) { /* USER CODE BEGIN RTC_Init 0 */ /* USER CODE END RTC_Init 0 */ RTC_PrivilegeStateTypeDef privilegeState = {0}; RTC_TimeTypeDef sTime = {0}; RTC_DateTypeDef sDate = {0}; /* USER CODE BEGIN RTC_Init 1 */ /* USER CODE END RTC_Init 1 */ /** Initialize RTC Only */ hrtc.Instance = RTC; hrtc.Init.HourFormat = RTC_HOURFORMAT_24; hrtc.Init.AsynchPrediv = 127; hrtc.Init.SynchPrediv = 255; hrtc.Init.OutPut = RTC_OUTPUT_DISABLE; hrtc.Init.OutPutRemap = RTC_OUTPUT_REMAP_NONE; hrtc.Init.OutPutPolarity = RTC_OUTPUT_POLARITY_HIGH; hrtc.Init.OutPutType = RTC_OUTPUT_TYPE_OPENDRAIN; hrtc.Init.OutPutPullUp = RTC_OUTPUT_PULLUP_NONE; hrtc.Init.BinMode = RTC_BINARY_NONE; if (HAL_RTC_Init(&hrtc) != HAL_OK) { Error_Handler(); } privilegeState.rtcPrivilegeFull = RTC_PRIVILEGE_FULL_NO; privilegeState.backupRegisterPrivZone = RTC_PRIVILEGE_BKUP_ZONE_NONE; privilegeState.backupRegisterStartZone2 = RTC_BKP_DR0; privilegeState.backupRegisterStartZone3 = RTC_BKP_DR0; if (HAL_RTCEx_PrivilegeModeSet(&hrtc, &privilegeState) != HAL_OK) { Error_Handler(); } /* USER CODE BEGIN Check_RTC_BKUP */ /* USER CODE END Check_RTC_BKUP */ /** Initialize RTC and set the Time and Date */ sTime.Hours = 0x15; sTime.Minutes = 0x0; sTime.Seconds = 0x0; sTime.DayLightSaving = RTC_DAYLIGHTSAVING_NONE; sTime.StoreOperation = RTC_STOREOPERATION_RESET; if (HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BCD) != HAL_OK) { Error_Handler(); } sDate.WeekDay = RTC_WEEKDAY_THURSDAY; sDate.Month = RTC_MONTH_OCTOBER; sDate.Date = 0x24; sDate.Year = 0x24; if (HAL_RTC_SetDate(&hrtc, &sDate, RTC_FORMAT_BCD) != HAL_OK) { Error_Handler(); } /* USER CODE BEGIN RTC_Init 2 */ /* USER CODE END RTC_Init 2 */ } 编写printf重定向 int fputc(int ch,FILE *p) { char c=ch; HAL_UART_Transmit(&huart4,(unsigned char *)&c,1,50); return ch; } void UART4_SendByte(char c) { HAL_UART_Transmit(&huart4,(unsigned char *)&c,1,50); } void UART4_SendData(char *p,int len) { HAL_UART_Transmit(&huart4,(unsigned char *)p,len,50); } 编写输出RTC日历的代码 static void RTC_CalendarShow(uint8_t *showtime, uint8_t *showdate) { RTC_DateTypeDef sdatestructureget; RTC_TimeTypeDef stimestructureget; /* Get the RTC current Time */ HAL_RTC_GetTime(&hrtc, &stimestructureget, RTC_FORMAT_BIN); /* Get the RTC current Date */ HAL_RTC_GetDate(&hrtc, &sdatestructureget, RTC_FORMAT_BIN); /* Display time Format : hh:mm:ss */ sprintf((char *)showtime, "%2d:%2d:%2d", stimestructureget.Hours, stimestructureget.Minutes, stimestructureget.Seconds); /* Display date Format : mm-dd-yyyy */ sprintf((char *)showdate, "%2d-%2d-%2d", sdatestructureget.Month, sdatestructureget.Date, 2000 + sdatestructureget.Year); printf("%s\r\n", showtime); printf("%s\r\n", showdate); } 初始化时间/日期字符串 uint8_t aShowTime[16] = "hh:ms:ss"; uint8_t aShowDate[16] = "mm-dd-yyyy"; 在主函数循环中加入如下代码 RTC_CalendarShow(aShowTime, aShowDate); HAL_Delay(1000); 但是,这样还是有问题的。 问题出在 HAL_StatusTypeDef RTC_EnterInitMode(RTC_HandleTypeDef *hrtc) 其代码如下 HAL_StatusTypeDef RTC_EnterInitMode(RTC_HandleTypeDef *hrtc) { uint32_t tickstart; HAL_StatusTypeDef status = HAL_OK; /* Check if the Initialization mode is set */ if (READ_BIT(RTC->ICSR, RTC_ICSR_INITF) == 0U) { /* Set the Initialization mode */ SET_BIT(RTC->ICSR, RTC_ICSR_INIT); tickstart = HAL_GetTick(); /* Wait till RTC is in INIT state and if Time out is reached exit */ while ((READ_BIT(RTC->ICSR, RTC_ICSR_INITF) == 0U) && (status != HAL_TIMEOUT)) { if ((HAL_GetTick() - tickstart) > RTC_TIMEOUT_VALUE) { /* New check to avoid false timeout detection in case of preemption */ if (READ_BIT(RTC->ICSR, RTC_ICSR_INITF) == 0U) { status = HAL_TIMEOUT; /* Change RTC state */ hrtc->State = HAL_RTC_STATE_TIMEOUT; } else { break; } } } } return status; } 问题出现在执行 SET_BIT(RTC->ICSR, RTC_ICSR_INIT); 之后,寄存器的初始化位并没有置1。 阅读手册可知需要对DBP置1才能使能写RTC寄存器。   在进入RTC之前会调用 HAL_RTC_MspInit(hrtc);   于是我们就可以进入 HAL_RTC_MspInit 中使能写RTC寄存器,添加一句 HAL_PWR_EnableBkUpAccess(); 用于允许访问备份区。 void HAL_RTC_MspInit(RTC_HandleTypeDef* rtcHandle) { RCC_PeriphCLKInitTypeDef PeriphClkInit = {0}; if(rtcHandle->Instance==RTC) { /* USER CODE BEGIN RTC_MspInit 0 */ HAL_PWR_EnableBkUpAccess(); /* USER CODE END RTC_MspInit 0 */ /** Initializes the peripherals clock */ PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_RTC; PeriphClkInit.RTCClockSelection = RCC_RTCCLKSOURCE_LSI; if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK) { Error_Handler(); } /* RTC clock enable */ __HAL_RCC_RTC_ENABLE(); __HAL_RCC_RTCAPB_CLK_ENABLE(); /* USER CODE BEGIN RTC_MspInit 1 */ /* USER CODE END RTC_MspInit 1 */ } } 于是RTC就可以正常使用了。 结果展示     完整工程代码

  • 2024-10-13
  • 回复了主题帖: STM32H7S78-DK测评(三)——OV5640摄像头测试

    lugl4313820 发表于 2024-10-13 19:41 楼主是太历害了呀,可以分享开发板的设计吗? 这块开发板是ST官方的开发板,ST官网上能找到开发板资料的。如果你说的是OV5640转接板,我已经在嘉立创开源了。

  • 2024-10-08
  • 回复了主题帖: STM32H7S78-DK测评(三)——OV5640摄像头测试

    cc1989summer 发表于 2024-10-8 14:00 楼主。ST官方手册有说的,配套摄像头模块型号是MB1683,Mouser上售价400+元人民币。 抱歉我没仔细读文档,不过这个摄像头也是基于OV5640的,我看e络盟要将近300块,但是感觉都挺贵的,还是自己画个转接板划算  

统计信息

已有88人来访过

  • 芯积分:358
  • 好友:--
  • 主题:19
  • 回复:17

留言

你需要登录后才可以留言 登录 | 注册


现在还没有留言