-
Notifications
You must be signed in to change notification settings - Fork 0
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
CSR rework #1
base: rivos/main
Are you sure you want to change the base?
CSR rework #1
Conversation
c2cc3ce
to
351acdf
Compare
4564bfa
to
bb3945d
Compare
713fe57
to
ec6225b
Compare
ec6225b
to
920d41b
Compare
Because the CSR modules are going to be rewritten, this was done to ensure that the crate can still build while this is happening.
This adds a couple of macros that will create a set of functions for a given CSR. Most of the time, only `ro_csr!` and `rw_csr!` will need to be invoked directly.
920d41b
to
ebe2c27
Compare
assert!(index < 4); | ||
pub fn atomic_replace(val_to_set: $size) -> $size { | ||
let r: $size; | ||
unsafe { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This and other unsafe blocks could use a comment about what makes them safe for all values written to all CSRs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we look at enabling undocumented-unsafe-blocks
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed to both, I'll change it.
@@ -0,0 +1,678 @@ | |||
//! Addresses for CSRs. Automatically generated by parse_opcodes. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A link to how this was generated would be nice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I looked into parse_opcodes
, but it doesn't actually seem like that is capable of generating CSR information. The official riscv-opcodes library does so I can see about using that to do the generation instead.
|
||
pub mod asm; | ||
pub mod delay; | ||
pub mod interrupt; | ||
//pub mod delay; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
commented code
@@ -0,0 +1,7 @@ | |||
[toolchain] | |||
channel = "nightly" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What nightly features do you need?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, do you want to pin this to a specific version of nightly?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nightly is needed for the asm-const feature. In retrospect, I may be able to circumvent this as inline assembly is just implemented with format strings.
// example would be the PMP CSRs, there are multiple with the exact same | ||
// layout. Using this allows the same bitfield to be used multiple times. | ||
// Ex: | ||
// rw_csr!(pmpcfg0, pmpcfg, usize); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what type is pmpcfg
in this example? is it generated by rw_csr
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be generated using register_bitfields
Reference
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Furquan is right, it would come from register_bitfields
. I'll make that clearer in the documentation.
#[cfg(target_pointer_width = "32")] | ||
pub mcause [ | ||
cause OFFSET(0) NUMBITS(31) [], | ||
is_interrupt OFFSET(31) NUMBITS(1) [], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe call this interrupt
, it is a bit confusing when seeing references to this field.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, I'll change it.
], | ||
|
||
#[cfg(target_pointer_width = "64")] | ||
pub mcause [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could use mem::size_of::<usize>()
rather than hardcode 32 or 64, but in this case I think it is simple enough it doesn't matter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like how tock defines XLEN
and uses that instead of the #cfg
in each module: https://github.com/tock/tock/blob/9cc3d42572ce1ad117895ad81767f671e906d3a6/arch/riscv/src/lib.rs
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't notice the XLEN
thing, I like that too. I'll use that where I can.
@@ -52,13 +93,10 @@ impl Interrupt { | |||
#[inline] | |||
pub fn from(nr: usize) -> Self { | |||
match nr { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you just give the nums values?
pub enum Interrupt {
SupervisorSoft = 1,
MachineSoft = 3,
...
Then when you want the integer value do Interrupt::SupervisorSoft as u32
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is for a usize
-> Interrupt
conversion. There is no core support for doing that as a cast, but I could use num-derive to automatically generate methods to cast between integers. I didn't do this initially as I'm a bit wary of adding new dependencies for this sort of crate, but I think in this case it may be justified, as having casts available to any integer size would be nice.
pub mod uie; | ||
pub mod ustatus; | ||
pub mod utvec; | ||
//pub mod uie; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this all commented out?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I get flooded with errors otherwise as I got rid of the macros. The alternative is to just outright delete everything and bring stuff back as I need it.
@@ -0,0 +1,7 @@ | |||
[toolchain] | |||
channel = "nightly" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, do you want to pin this to a specific version of nightly?
targets = [ "riscv32i-unknown-none-elf", "riscv32imc-unknown-none-elf", | ||
"riscv32imac-unknown-none-elf", "riscv64imac-unknown-none-elf", | ||
"riscv64gc-unknown-none-elf"] | ||
profile = "minimal" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just curious: Why "minimal" and not just "default"? Is it because we don't want the components rust-docs
and clippy
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just copied the file from another riscv project, forgot to fix that. I definitely want rust-docs
and clippy
.
r | ||
} | ||
|
||
/// Atomically read a CSR and set bits specified in a bitmask |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/set/clear
|
||
/// Atomically read a CSR and set bits specified in a bitmask | ||
/// | ||
/// This method corresponds to the RISC-V `CSRRS rd, csr, rs1` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/CSRRS/CSRRC
// example would be the PMP CSRs, there are multiple with the exact same | ||
// layout. Using this allows the same bitfield to be used multiple times. | ||
// Ex: | ||
// rw_csr!(pmpcfg0, pmpcfg, usize); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be generated using register_bitfields
Reference
], | ||
|
||
#[cfg(target_pointer_width = "64")] | ||
pub mcause [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like how tock defines XLEN
and uses that instead of the #cfg
in each module: https://github.com/tock/tock/blob/9cc3d42572ce1ad117895ad81767f671e906d3a6/arch/riscv/src/lib.rs
/// Generates the read functions for a CSR | ||
macro_rules! csr_reads { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like we are generating these read, write and other functions for each csr that is defined. Why not define a CSR type and provide implementations for that type instead? I am just looking at the example of tock and trying to understand why this design was chosen as opposed to what tock does. [Reference]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried that at first, but that would require one of two things:
- Implement each CSR as an instance of a generic type. This would mean that CSR-specific functionality would need to be wrapped in a module (in which the instance of the CSR would also need to be created), so an access to a generic function would look something like
register::mcause.CSR.read()
, and a CSR-specific function would look likeregister::mcause::is_interrupt()
. Not a fan of this as it would mean there's different syntax for using the methods. - Implement each CSR as its own type that implements a generic CSR trait. This would allow CSR-specific functions to also be implemented on the type, so the syntax would be identical. However, this means creating a new type for every CSR, and we'd still need to either create an instance of it in the library (makes the codebase more confusing) or expect users to create their own (adds a needless burden to the users). I'm just in general not a fan of that pattern, but if we want to go with something using a CSR type then this option is, in my opinion, the best.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was mostly looking at how tock instantiated the CSRs and thinking along the same lines. Salus seems to have inherited the same design. I think this kind of falls into the first category you defined. And it looks like the inconsistent access you described was solved by Tock by implementing functions directly as part of the CSR type e.g. https://github.com/tock/tock/blob/9cc3d42572ce1ad117895ad81767f671e906d3a6/arch/riscv/src/csr/mod.rs#L315
Since I haven't played with either of the interfaces much, I don't have a strong opinion on which way to go. It was more of a curiosity to understand why we are choosing this design.
The way it was written in tock/salus by introducing a generic type for RW-CSR (ReadWriteRiscvCsr) and an interface for it and then using that to define a struct CSR for all CSRs in RV seemed like a fine separation. But as you pointed out, there might be other limitations/drawbacks of this. Maybe @abrestic-rivos has more thoughts on this based on his usage of the tock registers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I should also note that the first category makes it difficult to implement CSRs with layouts similar to the PMP configuration registers (The same pattern repeated). You can see here the impact of doing it this way. The bitfields are far more convoluted because of this setup.
And it looks like the inconsistent access you described was solved by Tock by implementing functions directly as part of the CSR type
The problem with this is that all functions are in the same namespace, so it's not as easy to add CSR-specific functions for each CSR without the names getting too convoluted.
Since I haven't played with either of the interfaces much, I don't have a strong opinion on which way to go. It was more of a curiosity to understand why we are choosing this design.
While I'm definitely in favor of the approach I've taken here, if there are notable benefits for the other method I don't mind switching.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand your point about having to define all the functions within the same namespace. That was one of the things that bothered me as well about the approach.
Anyways, I didn't mean to question your approach. I just wanted to understand your thoughts and reasoning behind choosing the approach. I trust your judgement and evaluation and we can go ahead with that. Thanks for the details! :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The way it was written in tock/salus by introducing a generic type for RW-CSR (ReadWriteRiscvCsr) and an interface for it and then using that to define a struct CSR for all CSRs in RV seemed like a fine separation. But as you pointed out, there might be other limitations/drawbacks of this. Maybe @abrestic-rivos has more thoughts on this based on his usage of the tock registers.
FWIW, this works pretty well for us (with one exception1) so if the eventual goal is for Salus to use this too, I'd like to see a compelling reason for why we should change it :)
The CSR-specific-methods issue can be handled by defining and implementing traits for ReadWriteRiscvCsr<CSR>
or LocalRegisterCopy<CSR>
. For example, if you wanted to define a cause()
method like you've done in the subsequent patch, you could do something like:
trait XCauseHelpers {
fn cause(&self) -> Trap;
fn is_interrupt(&self) -> bool {
matches!(self.cause(), Trap::Interrupt(_))
}
}
trait XCause: LongRegisterName {}
impl XCause for mcause::Register {}
impl XCause for scause::Register {}
impl<R: XCause> XCauseHelpers for ReadWriteRiscvCsr<usize, R> {
fn cause(&self) -> Trap { ... }
}
And then you'd use it like this:
let trap = CSR.mcause.cause();
That doesn't seem any less ergonomic that what you're proposing, and involves less use of macros (which IMO are less readable than traits/generics).
Footnotes
-
The tock
Readable
/Writable
traits aren't object safe, so things get annoying if you want to create trait objects from CSRs, which is useful when dealing with a range of identically-defined CSRs (thinkhpmcounter
). ↩
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
... if the eventual goal is for Salus to use this too, I'd like to see a compelling reason for why we should change it :)
I would definitely like this PR to end up producing the one crate that rules them all, regardless of if it's what I've proposed here or something else.
The CSR-specific-methods issue can be handled by defining and implementing traits for ReadWriteRiscvCsr or LocalRegisterCopy. For example, if you wanted to define a cause() method like you've done in the subsequent patch, you could do something like:
trait XCauseHelpers { fn cause(&self) -> Trap; fn is_interrupt(&self) -> bool { matches!(self.cause(), Trap::Interrupt(_)) } } trait XCause: LongRegisterName {} impl XCause for mcause::Register {} impl XCause for scause::Register {} impl<R: XCause> XCauseHelpers for ReadWriteRiscvCsr<usize, R> { fn cause(&self) -> Trap { ... } }
And then you'd use it like this:
let trap = CSR.mcause.cause();
That doesn't seem any less ergonomic that what you're proposing, and involves less use of macros (which IMO are less readable than traits/generics).
I didn't think of that. This makes a struct/trait based approach a lot more palatable (although I still like modules more :)), especially because we can reuse some traits between CSRs for different privilege levels.
The tock Readable/Writable traits aren't object safe, so things get annoying if you want to create trait objects from CSRs, which is useful when dealing with a range of identically-defined CSRs (think hpmcounter)
This was another issue I had too. Having to make the array for HPM counters dyn
(here) is not ideal (I believe this requires a vtable lookup?). I think that the easiest way around this is to instead provide indexed functions, where the caller just provides an integer for the CSR they want and the function uses a match
to call the correct function. As long as the function calls in the match
are inlined this should basically become a jump table.
My one remaining issue, however, is with CSRs like the PMPs. If we wanted to use something like ReadWriteRiscvCsr
for everything, the PMP created by register_bitfields!
would have to look like this, which is terrible. That being said, in 99% of cases the user will likely want to use an indexed function for CSRs like this so they don't have to be exposed to this interface directly. However, that still means the internals aren't going to be very pretty. An easy way around this would be to implement PMPs as a separate type with the exact same functions, so the user wouldn't be aware of the difference without looking at the code/docs. This would only cause an issue if the user wanted to implement some sort of generic function using a ReadWriteRiscvCsr
, but that's not something my design makes possible either so I don't think it's a big deal.
Any other thoughts? With what Andrew's pointed out here I'm more willing to go with a struct/trait based setup than before, although I still think a module-based structure makes more sense for CSRs given that they're effectively singletons.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was another issue I had too. Having to make the array for HPM counters dyn (here) is not ideal (I believe this requires a vtable lookup?). I think that the easiest way around this is to instead provide indexed functions, where the caller just provides an integer for the CSR they want and the function uses a match to call the correct function. As long as the function calls in the match are inlined this should basically become a jump table.
Yeah, it's a vtable lookup. There's definitely room to improve how we've handled the counter CSRs. I like the indexed approach if we care about the generated assembly for the particular CSRs. (It is effectively what Linux does for the counter CSRs, via a mess of cpp macros).
My one remaining issue, however, is with CSRs like the PMPs.
Yes, that's a bit of a mess; we don't have the PMP CSRs in Salus so I've ignored them :)
Given that it's not a pattern that's oft repeated in CSR definitions (CSR divided into independent, identical sub-registers), I would be tempted to skip the register_bitfields!
and implement setting of an individual PMP entry via a helper trait.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, it's a vtable lookup.
Definitely not something we want on firmware, but I think the indexed approach is acceptable for us.
Yes, that's a bit of a mess; we don't have the PMP CSRs in Salus so I've ignored them :)
Can't blame you there.
Given that it's not a pattern that's oft repeated in CSR definitions (CSR divided into independent, identical sub-registers), I would be tempted to skip the register_bitfields! and implement setting of an individual PMP entry via a helper trait.
I think that's fine. I'll look further into a design that uses traits; I had already put some stuff together but got fed up with the problems I had initially and decided to switch. I'll probably push that out as a separate PR so we can review both separately.
//! | ||
//! As the CSRs are implemented the `#[allow(unused)] around the respective | ||
//! index should be removed. | ||
#[allow(unused)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you put a single #![allow(unused)]
at the top of the file to avoid repeating this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did not realize I could do that. Will fix.
/// Generates the read functions for a CSR | ||
macro_rules! csr_reads { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The way it was written in tock/salus by introducing a generic type for RW-CSR (ReadWriteRiscvCsr) and an interface for it and then using that to define a struct CSR for all CSRs in RV seemed like a fine separation. But as you pointed out, there might be other limitations/drawbacks of this. Maybe @abrestic-rivos has more thoughts on this based on his usage of the tock registers.
FWIW, this works pretty well for us (with one exception1) so if the eventual goal is for Salus to use this too, I'd like to see a compelling reason for why we should change it :)
The CSR-specific-methods issue can be handled by defining and implementing traits for ReadWriteRiscvCsr<CSR>
or LocalRegisterCopy<CSR>
. For example, if you wanted to define a cause()
method like you've done in the subsequent patch, you could do something like:
trait XCauseHelpers {
fn cause(&self) -> Trap;
fn is_interrupt(&self) -> bool {
matches!(self.cause(), Trap::Interrupt(_))
}
}
trait XCause: LongRegisterName {}
impl XCause for mcause::Register {}
impl XCause for scause::Register {}
impl<R: XCause> XCauseHelpers for ReadWriteRiscvCsr<usize, R> {
fn cause(&self) -> Trap { ... }
}
And then you'd use it like this:
let trap = CSR.mcause.cause();
That doesn't seem any less ergonomic that what you're proposing, and involves less use of macros (which IMO are less readable than traits/generics).
Footnotes
-
The tock
Readable
/Writable
traits aren't object safe, so things get annoying if you want to create trait objects from CSRs, which is useful when dealing with a range of identically-defined CSRs (thinkhpmcounter
). ↩
This is an initial attempt to rework the
register
module to use a better set of APIs based ontock-registers
. The goal here is to build something that both meets the needs of all of our teams and can be contributed back upstream.If everyone is alright with the general gist of what I'm aiming for here, I'll go ahead and put out the rest of the CSRs over the next few weeks.