Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

riscv-rt: Support for vectored mode interrupt handling #200

Merged
merged 8 commits into from
May 16, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/riscv-rt.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
on:
push:
branches: [ master, riscv-rt-asm ]
branches: [ master, vectored-rt ]
pull_request:
merge_group:

Expand Down Expand Up @@ -39,6 +39,8 @@ jobs:
run: RUSTFLAGS="-C link-arg=-Triscv-rt/examples/device.x" cargo build --package riscv-rt --target ${{ matrix.target }} --example ${{ matrix.example }} --features=s-mode
- name : Build (single-hart)
run: RUSTFLAGS="-C link-arg=-Triscv-rt/examples/device.x" cargo build --package riscv-rt --target ${{ matrix.target }} --example ${{ matrix.example }} --features=single-hart
- name : Build (v-trap)
run: RUSTFLAGS="-C link-arg=-Triscv-rt/examples/device.x" cargo build --package riscv-rt --target ${{ matrix.target }} --example ${{ matrix.example }} --features=v-trap
- name: Build (all features)
run: RUSTFLAGS="-C link-arg=-Triscv-rt/examples/device.x" cargo build --package riscv-rt --target ${{ matrix.target }} --example ${{ matrix.example }} --all-features

Expand Down
3 changes: 3 additions & 0 deletions riscv-rt/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
### Added

- Add `pre_init_trap` to detect early errors during the boot process.
- Add `v-trap` feature to enable interrupt handling in vectored mode.
- Add `interrupt` proc macro to help defining interrupt handlers.
If `v-trap` feature is enabled, this macro also generates its corresponding trap.

### Changed

Expand Down
9 changes: 5 additions & 4 deletions riscv-rt/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@ license = "ISC"
edition = "2021"
links = "riscv-rt" # Prevent multiple versions of riscv-rt being linked

[features]
s-mode = []
single-hart = []

[dependencies]
riscv = {path = "../riscv", version = "0.11.1"}
riscv-rt-macros = { path = "macros", version = "0.2.1" }

[dev-dependencies]
panic-halt = "0.2.0"

[features]
s-mode = ["riscv-rt-macros/s-mode"]
single-hart = []
v-trap = ["riscv-rt-macros/v-trap"]
8 changes: 7 additions & 1 deletion riscv-rt/examples/empty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,16 @@
extern crate panic_halt;
extern crate riscv_rt;

use riscv_rt::entry;
use riscv_rt::{entry, interrupt};

#[entry]
fn main() -> ! {
// do something here
loop {}
}

#[interrupt]
fn MachineSoft() {
// do something here
loop {}
}
17 changes: 17 additions & 0 deletions riscv-rt/link.x.in
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,23 @@ PROVIDE(_max_hart_id = 0);
PROVIDE(_hart_stack_size = 2K);
PROVIDE(_heap_size = 0);

/** TRAP ENTRY POINTS **/

/* Default trap entry point. The riscv-rt crate provides a weak alias of this function,
which saves caller saved registers, calls _start_trap_rust, restores caller saved registers
and then returns. Users can override this alias by defining the symbol themselves */
EXTERN(_start_trap);

/* When vectored trap mode is enabled, each interrupt source must implement its own
trap entry point. By default, all interrupts start in _start_trap. However, users can
override these alias by defining the symbol themselves */
PROVIDE(_start_SupervisorSoft_trap = _start_trap);
PROVIDE(_start_MachineSoft_trap = _start_trap);
PROVIDE(_start_SupervisorTimer_trap = _start_trap);
PROVIDE(_start_MachineTimer_trap = _start_trap);
PROVIDE(_start_SupervisorExternal_trap = _start_trap);
PROVIDE(_start_MachineExternal_trap = _start_trap);

/** EXCEPTION HANDLERS **/

/* Default exception handler. The riscv-rt crate provides a weak alias of this function,
Expand Down
4 changes: 4 additions & 0 deletions riscv-rt/macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,7 @@ proc-macro2 = "1.0"
[dependencies.syn]
version = "1.0"
features = ["extra-traits", "full"]

[features]
s-mode = []
v-trap = []
248 changes: 248 additions & 0 deletions riscv-rt/macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,3 +312,251 @@ pub fn loop_global_asm(input: TokenStream) -> TokenStream {
let res = format!("core::arch::global_asm!(\n\"{}\"\n);", instructions);
res.parse().unwrap()
}

#[derive(Clone, Copy)]
enum RiscvArch {
Rv32,
Rv64,
}

const TRAP_SIZE: usize = 16;

#[rustfmt::skip]
const TRAP_FRAME: [&str; TRAP_SIZE] = [
"ra",
"t0",
"t1",
"t2",
"t3",
"t4",
"t5",
"t6",
"a0",
"a1",
"a2",
"a3",
"a4",
"a5",
"a6",
"a7",
];

fn store_trap<T: FnMut(&str) -> bool>(arch: RiscvArch, mut filter: T) -> String {
let (width, store) = match arch {
RiscvArch::Rv32 => (4, "sw"),
RiscvArch::Rv64 => (8, "sd"),
};
let mut stores = Vec::new();
for (i, reg) in TRAP_FRAME
.iter()
.enumerate()
.filter(|(_, &reg)| filter(reg))
{
stores.push(format!("{store} {reg}, {i}*{width}(sp)"));
}
stores.join("\n")
}

fn load_trap(arch: RiscvArch) -> String {
let (width, load) = match arch {
RiscvArch::Rv32 => (4, "lw"),
RiscvArch::Rv64 => (8, "ld"),
};
let mut loads = Vec::new();
for (i, reg) in TRAP_FRAME.iter().enumerate() {
loads.push(format!("{load} {reg}, {i}*{width}(sp)"));
}
loads.join("\n")
romancardenas marked this conversation as resolved.
Show resolved Hide resolved
}

#[proc_macro]
pub fn weak_start_trap_riscv32(_input: TokenStream) -> TokenStream {
weak_start_trap(RiscvArch::Rv32)
}

#[proc_macro]
pub fn weak_start_trap_riscv64(_input: TokenStream) -> TokenStream {
weak_start_trap(RiscvArch::Rv64)
}

fn weak_start_trap(arch: RiscvArch) -> TokenStream {
let width = match arch {
RiscvArch::Rv32 => 4,
RiscvArch::Rv64 => 8,
};
// ensure we do not break that sp is 16-byte aligned
if (TRAP_SIZE * width) % 16 != 0 {
return parse::Error::new(Span::call_site(), "Trap frame size must be 16-byte aligned")
.to_compile_error()
.into();
}
let store = store_trap(arch, |_| true);
let load = load_trap(arch);

#[cfg(feature = "s-mode")]
let ret = "sret";
#[cfg(not(feature = "s-mode"))]
let ret = "mret";

let instructions: proc_macro2::TokenStream = format!(
"
core::arch::global_asm!(
\".section .trap, \\\"ax\\\"
.align {width}
.weak _start_trap
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One interesting data point, which might not be required here, is that in esp-riscv-rt, with high opt level + lto = 'fat' we found instances where weak assembly symbols were duplicated by the linker. To fix this we had to sprinkle some weird llvm specific attributes before the weak definitions:

https://github.com/esp-rs/esp-hal/blob/3c057595561a7a5dba68ae4df09c9960217c33fc/esp-riscv-rt/src/lib.rs#L830-L835

probably not required here, but if you run into issue you'll know what to do :D.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! This is my goal. If riscv-rt is also suitable for ESP32 Cx, then it is flexible enough for any RISC-V target. The issue you point out is interesting, we will monitor it in case it appears in future versions

_start_trap:
addi sp, sp, - {TRAP_SIZE} * {width}
{store}
add a0, sp, zero
jal ra, _start_trap_rust
{load}
addi sp, sp, {TRAP_SIZE} * {width}
{ret}
\");"
romancardenas marked this conversation as resolved.
Show resolved Hide resolved
)
.parse()
.unwrap();

#[cfg(feature = "v-trap")]
let v_trap = v_trap::continue_interrupt_trap(arch);
#[cfg(not(feature = "v-trap"))]
let v_trap = proc_macro2::TokenStream::new();

quote!(
#instructions
#v_trap
)
.into()
}

#[proc_macro_attribute]
pub fn interrupt_riscv32(args: TokenStream, input: TokenStream) -> TokenStream {
interrupt(args, input, RiscvArch::Rv32)
}

#[proc_macro_attribute]
pub fn interrupt_riscv64(args: TokenStream, input: TokenStream) -> TokenStream {
interrupt(args, input, RiscvArch::Rv64)
}

fn interrupt(args: TokenStream, input: TokenStream, _arch: RiscvArch) -> TokenStream {
let f = parse_macro_input!(input as ItemFn);

// check the function arguments
if !f.sig.inputs.is_empty() {
return parse::Error::new(
f.sig.inputs.first().unwrap().span(),
"`#[interrupt]` function should not have arguments",
)
.to_compile_error()
.into();
}

// check the function signature
let valid_signature = f.sig.constness.is_none()
&& f.sig.asyncness.is_none()
&& f.vis == Visibility::Inherited
&& f.sig.abi.is_none()
&& f.sig.generics.params.is_empty()
&& f.sig.generics.where_clause.is_none()
&& f.sig.variadic.is_none()
&& match f.sig.output {
ReturnType::Default => true,
ReturnType::Type(_, ref ty) => matches!(**ty, Type::Never(_)),
};

if !valid_signature {
return parse::Error::new(
f.span(),
"`#[interrupt]` function must have signature `[unsafe] fn() [-> !]`",
)
.to_compile_error()
.into();
}

if !args.is_empty() {
return parse::Error::new(Span::call_site(), "This attribute accepts no arguments")
.to_compile_error()
.into();
}

// XXX should we blacklist other attributes?
let ident = &f.sig.ident;
let export_name = format!("{:#}", ident);

#[cfg(not(feature = "v-trap"))]
let start_trap = proc_macro2::TokenStream::new();
#[cfg(feature = "v-trap")]
let start_trap = v_trap::start_interrupt_trap(ident, _arch);

quote!(
#start_trap
#[export_name = #export_name]
#f
)
.into()
}

#[cfg(feature = "v-trap")]
mod v_trap {
use super::*;

pub(crate) fn start_interrupt_trap(
ident: &syn::Ident,
arch: RiscvArch,
) -> proc_macro2::TokenStream {
let interrupt = ident.to_string();
let width = match arch {
RiscvArch::Rv32 => 4,
RiscvArch::Rv64 => 8,
};
let store = store_trap(arch, |r| r == "a0");

let instructions = format!(
"
core::arch::global_asm!(
\".section .trap, \\\"ax\\\"
.align {width}
.global _start_{interrupt}_trap
_start_{interrupt}_trap:
addi sp, sp, -{TRAP_SIZE} * {width} // allocate space for trap frame
{store} // store trap partially (only register a0)
la a0, {interrupt} // load interrupt handler address into a0
j _continue_interrupt_trap // jump to common part of interrupt trap
\");"
);

instructions.parse().unwrap()
}

pub(crate) fn continue_interrupt_trap(arch: RiscvArch) -> proc_macro2::TokenStream {
let width = match arch {
RiscvArch::Rv32 => 4,
RiscvArch::Rv64 => 8,
};
let store = store_trap(arch, |reg| reg != "a0");
let load = load_trap(arch);

#[cfg(feature = "s-mode")]
let ret = "sret";
#[cfg(not(feature = "s-mode"))]
let ret = "mret";

let instructions = format!(
"
core::arch::global_asm!(
\".section .trap, \\\"ax\\\"
.align {width} // TODO is this necessary?
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my understanding, we only need _continue_interrupt_trap to be a valid target for j instruction. j has LSB set to 0 so we'd need to make sure this is align(2) even if there's multiple items placed in .trap section. I can't see why it would ever need to be 4 or 8 byte aligned. I could be wrong.

.global _continue_interrupt_trap
_continue_interrupt_trap:
{store} // store trap partially (all registers except a0)
jalr ra, a0, 0 // jump to corresponding interrupt handler (address stored in a0)
{load} // restore trap frame
addi sp, sp, {TRAP_SIZE} * {width} // deallocate space for trap frame
{ret} // return from interrupt
\");"
);

instructions.parse().unwrap()
}
}
Loading
Loading