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

MVP implementation of HostFlash for Cosmo #1851

Merged
merged 13 commits into from
Aug 23, 2024
Merged

MVP implementation of HostFlash for Cosmo #1851

merged 13 commits into from
Aug 23, 2024

Conversation

mkeeter
Copy link
Collaborator

@mkeeter mkeeter commented Aug 20, 2024

This PR implements the HostFlash API for the Cosmo / Grapefruit flash IC, mediated by an FPGA.

gimlet-hf is renamed to hf in a few places, because we're now using the same API for both Gimlet and Cosmo systems. There are no changes to the API itself.

drv-fmc-nor-flash is renamed to drv-cosmo-hf for symmetry with Gimlet. It is split into main.rs (implementing the actual chip wrangling) and hf.rs (implementing the HostFlash Idol API).

Many functions are left as stubs, to be implemented in later PRs. However, this PR is sufficient to program and verify the flash chip with humility qspi!

github-actions[bot]

This comment was marked as resolved.

out.raw Outdated Show resolved Hide resolved
Copy link
Contributor

@aapoalas aapoalas left a comment

Choose a reason for hiding this comment

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

Peanut gallery checking in.

drv/cosmo-hf/src/hf.rs Outdated Show resolved Hide resolved
drv/cosmo-hf/src/hf.rs Outdated Show resolved Hide resolved
return Err(HfError::HashError.into());
}
let begin = addr as usize;
// TODO: Begin may be an address beyond physical end of

This comment was marked as resolved.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think the checked add in this case is only checking for integer wrap -- the flash part is likely to be much smaller than 2**32 so the flash addressing will wrap before the integer will.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, of course! Thank you <3

let begin = addr as usize;
// TODO: Begin may be an address beyond physical end of
// flash part and may wrap around.
let end = match begin.checked_add(len as usize) {
Copy link
Contributor

@aapoalas aapoalas Aug 20, 2024

Choose a reason for hiding this comment

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

suggestion: Could maybe use begin.checked_add(len as usize).and_then(|end| u32::try_from(end)) to get rid of the inner u32::MAX check.

I suppose this is always running on 32 bit hardware though?

Copy link
Collaborator

Choose a reason for hiding this comment

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

For better or worse, a lot of existing Hubris code assumes a 32-bit platform, yeah.

drv/cosmo-hf/src/hf.rs Outdated Show resolved Hide resolved
}
};
// If we knew the flash part size, we'd check against those limits.
let mut buf = [0u8; PAGE_SIZE_BYTES];
Copy link
Contributor

Choose a reason for hiding this comment

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

question: Is the idea that being is aligned at the start of a page and this can then read the flash part page-by-page (not counting the last, possibly not full page)? Is start guaranteed to be page-aligned?

Not sure if it would really make much of a difference, but you could split the range into full pages (with or without a not-full prefix, depending on if start is aligned or not) plus a not-necessarily-full suffix. That would mean you don't need to calculate size for the full pages.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think the comment on line 159 is slightly misplaced. What this code appears to be doing is just ensuring that reads are under 256 bytes in size by reusing a buffer; it doesn't appear to be sensitive to alignment, afaict.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yup, that comment isn't quite relevant here (we're limited by FPGA buffers, not the flash chip itself). Deleted!

drv/cosmo-hf/src/main.rs Show resolved Hide resolved
}

fn clear_fifos(&self) {
self.write_reg(reg::SPICR, 0x8080);

This comment was marked as resolved.

Copy link
Contributor

Choose a reason for hiding this comment

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

There is no wait required here. The FPGA FIFO clears take single-digit clock cycles to complete, as the reset may have to cross clock domains to clear read/write pointers, but the SP is hanging off the FMC interface and can't even finish executing another read/write transaction to the FPGA before the FIFO clear has completed so this can be treated as if it happens instantly.

}

impl ServerImpl {
fn flash_read_id(&self) -> [u8; 20] {
Copy link
Contributor

Choose a reason for hiding this comment

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

praise: For an outsider like me, it's really cool seeing this lowest-of-the-low level register work happening here.

suggestion: A mountain of comments wouldn't maybe go amiss here and would be a terrific pleasure to read :)

drv/cosmo-hf/src/hf.rs Outdated Show resolved Hide resolved
&mut self,
_: &RecvMessage,
) -> Result<HfMuxState, RequestError<HfError>> {
todo!()
Copy link
Collaborator

Choose a reason for hiding this comment

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

There's a lot of TODOs here providing remote panic opportunities. Are these operations going to make sense for Cosmo? I ask because, if not, we should have two different idl files, since we can't cfg things in IDL files at the moment.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

From talking with Nathanael, we do think that the existing API makes sense for Cosmo!

There are PRs coming down the line that implement these functions, e.g. #1853, but I didn't want to do too much in the MVP PR.

drv/cosmo-hf/src/main.rs Outdated Show resolved Hide resolved
let k = i * 4 + j;
if k < len {
let Some(d) = data.read() else {
return Err(());
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should this use an error type? I'm surprised Clippy isn't mad about this tbh.

Copy link
Contributor

Choose a reason for hiding this comment

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

Mildly off-topic, but I've had good(?) use of Result<T, ()> in TryFrom<ParentEnum> for SubEnum places. In that case the error case is just "it wasn't in the subgroup you checked for". So I'm not that surprised clippy doesn't complain about this: There are some minor use cases where it's not an entirely bad choice.

Copy link
Collaborator

@cbiffle cbiffle left a comment

Choose a reason for hiding this comment

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

For me the highest priority things to look at here are:

  1. Switch the generic flash read functions to use dyn so we don't generate multiple copies of them in flash. (The traits appear object-safe.)
  2. The use of Result<(), ()> seems suspicious, consider whether the error type could be something more useful. (I think this might have been inherited from BufWriter, but I think it's arguably wrong there too; no reason to propagate the wrongness when your IPC error types are right there.)
  3. Comment that unwrap please, it's subtle
  4. Make sure those todo!s will get todone, otherwise the operations shouldn't be in the IPC protocol (and we need to fork it).
  5. The flash_foo functions toward the end could use doc comments explaining any assumptions and failure cases.

drv/cosmo-hf/src/main.rs Show resolved Hide resolved
#[count(skip)]
None,

FpgaBusy(u32),
Copy link
Member

Choose a reason for hiding this comment

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

What does the u32 mean here? It might be nice if this was a struct-like variant with a named field, so that someone inspecting the ring buffer in humility immediately knows what the integer refers to?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Renamed to spi_sr

Comment on lines 59 to 60
// Wait for the FMC to be configured
userlib::hl::sleep_for(1000);
Copy link
Member

Choose a reason for hiding this comment

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

how do we know that it will take less than 1000 ticks to do this? What happens if we sleep and the FMC still hasn't been configured?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

What happens if we sleep and the FMC still hasn't been configured?

Panic! at the ID check

(I added a basic ping function to the sequencer API, so cosmo-hf now waits for that to return before starting up)

fn wait_fpga_rx(&self) {
for i in 0.. {
let status = self.read_reg(reg::SPISR);
// Wait for the
Copy link
Member

Choose a reason for hiding this comment

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

Wait for the...?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

BALEETED

Comment on lines +227 to +241
for i in 0..len.div_ceil(4) {
self.wait_fpga_rx();
let v = self.read_reg(reg::RX_FIFO);
let v = v.to_le_bytes();
for (j, byte) in v.iter().enumerate() {
let k = i * 4 + j;
if k < len {
dest.write(*byte)?;
}
}
}
Copy link
Member

Choose a reason for hiding this comment

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

wondered a bit if this structure, which is repeated a few times, is easily generalizable to both the flash_read and flash_write methods, but honestly...trying to factor it out is probably more trouble than it's worth...

Copy link
Member

Choose a reason for hiding this comment

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

also, it could maybe be nice if there was a comment explaining that this kinda weird loop is because we read bytes out of the FIFO in 32-bit words, and depending on the length to read, we might overshoot the requisite length and have to bail early? i was able to figure it out fairly quickly but it might not be totally obvious to everyone...

Copy link
Contributor

Choose a reason for hiding this comment

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

opinion: It's not too difficult code to read or understand, but a quick comment wouldn't be out of place. Words are still generally easier to understand. Also I personally love the comment-rich coding style I often see in Oxide's code, so more comments! :)

@mkeeter
Copy link
Collaborator Author

mkeeter commented Aug 21, 2024

For me the highest priority things to look at here are:

  1. Switch the generic flash read functions to use dyn so we don't generate multiple copies of them in flash. (The traits appear object-safe.)

  2. The use of Result<(), ()> seems suspicious, consider whether the error type could be something more useful. (I think this might have been inherited from BufWriter, but I think it's arguably wrong there too; no reason to propagate the wrongness when your IPC error types are right there.)

  3. Comment that unwrap please, it's subtle

  4. Make sure those todo!s will get todone, otherwise the operations shouldn't be in the IPC protocol (and we need to fork it).

  5. The flash_foo functions toward the end could use doc comments explaining any assumptions and failure cases.

This is all done except 2, because I opened oxidecomputer/idolatry#55 to fix it upstream.

Comment on lines 97 to 101
if addr < SECTOR_SIZE_BYTES
&& !matches!(protect, HfProtectMode::AllowModificationsToSector0)
{
return Err(HfError::Sector0IsReserved.into());
}
Copy link
Member

Choose a reason for hiding this comment

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

nit, take it or leave it: we could maybe have a function like:

fn check_addr_writable(addr: u32, protect: HfProtectMode) -> Result<(), HfError> {
    if addr < SECTOR_SIZE_BYTES && 
        !matches!(protect, HfProtectMode::AllowModificationsToSector0)
    {
          return Err(HfError::Sector0IsReserved);
    }
    Ok(())
}

since we seem to repeat this in a few places. potentially, factoring it out it would mean that a future change adding other checks against HfProtectModes wouldn't have to add the requisite checks in every function that currently checks if sector 0 is writable?

not a big deal either way though.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I like it, done in 0f4cd3c

drv/cosmo-hf/src/hf.rs Show resolved Hide resolved
fn main() -> ! {
// Wait for the FPGA to be configured
let seq = drv_grapefruit_seq_api::Sequencer::from(SEQ.get_task_id());
seq.ping(); // waits until the sequencer has completed configuration
Copy link
Member

Choose a reason for hiding this comment

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

Cute, I like this approach. :)

Copy link
Member

@hawkw hawkw left a comment

Choose a reason for hiding this comment

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

Looks good to me, no blockers! I agree with Cliff that it would be good to prioritize checking off the todo!s relatively soon, but everything looks good to me!

Comment on lines +186 to +189
self.write_reg(reg::DATA_BYTES, 1);
self.write_reg(reg::ADDR, 0);
self.write_reg(reg::DUMMY_CYCLES, 0);
self.write_reg(reg::INSTR, instr::READ_STATUS_1);
Copy link
Member

Choose a reason for hiding this comment

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

there is a part of me that wonders if it's worth taking all this and putting it in a function that takes the number of bytes and the instruction value, but, honestly, I'm not sure if it would be worth doing, because you'd still have to provide the number of bytes, address, dummy cycles, and instructions to that function so i don't really think it would be abstracting over anything meaningful...and this feels more explicit, i guess?

Comment on lines +227 to +241
for i in 0..len.div_ceil(4) {
self.wait_fpga_rx();
let v = self.read_reg(reg::RX_FIFO);
let v = v.to_le_bytes();
for (j, byte) in v.iter().enumerate() {
let k = i * 4 + j;
if k < len {
dest.write(*byte)?;
}
}
}
Copy link
Member

Choose a reason for hiding this comment

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

also, it could maybe be nice if there was a comment explaining that this kinda weird loop is because we read bytes out of the FIFO in 32-bit words, and depending on the length to read, we might overshoot the requisite length and have to bail early? i was able to figure it out fairly quickly but it might not be totally obvious to everyone...

@mkeeter mkeeter enabled auto-merge (squash) August 23, 2024 17:36
@mkeeter mkeeter merged commit 66721b1 into master Aug 23, 2024
106 checks passed
@mkeeter mkeeter deleted the mkeeter/cosmo-hf branch August 23, 2024 17:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants