Skip to content

earlgrey: Properly ack flash operation done interrupt - #390

Open
jesultra wants to merge 1 commit into
OpenPRoT:earlgrey-hwefrom
jesultra:blocking_flash
Open

earlgrey: Properly ack flash operation done interrupt#390
jesultra wants to merge 1 commit into
OpenPRoT:earlgrey-hwefrom
jesultra:blocking_flash

Conversation

@jesultra

@jesultra jesultra commented Aug 2, 2026

Copy link
Copy Markdown

Delay ack'ing of interrupts with the PLIC until the handling routine has run, and has had the opportunity to address the situation with the IP, to cause interrupt request to be deasserted.

Before this change, the PLIC would re-latch the interrupt immediately after it being ack'ed, as the interrupt request was still active at the flash IP. As a result, next flash operation would erroneously be considered done immediately.

@linux-foundation-easycla

linux-foundation-easycla Bot commented Aug 2, 2026

Copy link
Copy Markdown

CLA Signed
The committers listed above are authorized under a signed CLA.

  • ✅ login: jesultra / name: Jes B. Klinke (ea33d2e)

@jesultra jesultra changed the title earlgrey: Avoid spurious completion on flash operations earlgrey: Properly ack flash operation done interrupt Aug 5, 2026
@jesultra
jesultra marked this pull request as ready for review August 5, 2026 11:31
@jesultra
jesultra force-pushed the blocking_flash branch 3 times, most recently from 7508dfb to a0d893d Compare August 10, 2026 07:42

@jesultra jesultra left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

With this change, my Teacup board is able to copy an image from external flash to slot B in the internal flash. I suppose the existing code worked on FPGAs only because the simulation of writing to internal flash must be unrealistic, in the sense that it completes instantly, thereby hiding the improper handling of flash operation completion interrupts.

After the transport image has flashed the slot B, we expect the new image to boot.  On my board, however, the same transport image boots again (and attempts flashing again repeatedly.)
I guess I have a few things to do:

  1. Understand why the new image does not boot, this could be either because it is improperly signed/built, or because version numbers or some other header field causes ROM_EXT to choose the transport image instead, or I suppose, it could be that the transport image fails to make the requests to ROM_EXT to boot the new image once.  I assume that is what we want, booting it once.  Then the new image should be responsible for telling ROM_EXT to permanently switch to booting the new image, after it has successfully booted.

  2. We should have a guard against wearing down the internal flash by repeated writing of the same image.  Would it be too much to compare the content if the internal flash slot B with the content of the external flash, and only write the pages that differ?

  3. I think the proposal above leaves a tiny tiny gap.  If power is lost while writing the last flash page, I assume that it could happen that the flash write did not have quite enough time to build up/drain charges from the storage capacitors, and worst case, the data will read correctly the next few days, but fail before the rated retention time, because the programming did not adhere to specification.  To guard against something like that, we need to keep a "transaction log" in flash elsewhere (in slot A), and only after writing the last page of slot B, we write a log entry stating that flashing completed (with a checksum of the written image probably.)  Then on next boot, if we see such a record (even if written incompletely) we know that the slot B image was written properly, and can skip re-writing if the checksum in the record matches the checksum of the image in external flash. Such a transaction log would also eliminate the need to calculate checksum of the internal flash when deciding whether to skip re-writing, thereby avoiding a slowdown to the "happy case".

@cfrantz
cfrantz self-requested a review August 10, 2026 15:18
Delay ack'ing of the interrupt with the PLIC until the handling
routine has run, and has had the opportunity to address the situation
with the IP, to cause interrupt request to be deasserted.

Before this change, the PLIC would re-latch the interrupt immediately
after it being ack'ed, as the interrupt request was still active at
the flash IP.  As a result, next flash operation would erroneously be
considered done immediately.
@jesultra

Copy link
Copy Markdown
Author

About item 1 on my list above, I have found my issue to be that I had incorrectly signed the image on the external SPI flash. With that fixed, the transport image was able to flash and boot the next image. Though I discovered then that the next time, the transport image under test is not loaded correctly by "opentitantool rescue firmware" (ROM_EXT continues to boot slot B) due to a missing cherry-pick:
lowRISC/opentitan#25887

Item 2 and 3 remain.

@jesultra

Copy link
Copy Markdown
Author

I have noticed one more issue directly related to the flash driver. Some operations in eflash_driver.rs are asynchronous, relying on the caller to call complete_op() in response to an interrupt, but read() is synchronous, which is fine. But when read() runs, it will nevertheless trigger an interrupt request, meaning that if start_erase() or start_program() is subsequently called, the interrupt will have already latched, and complete_op() will be called prematurely, resulting in an error when complete_op() inspects the status.

Comment thread util/blocking/lib.rs
/// an event or notification from another part of the system (e.g., an interrupt).
pub trait Blocking {
/// Waits until a notification is received.
fn wait_for_notification(&self) -> impl Drop;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think it might be better to have a specific trait for notification acknowledgement and declare that as the bound rather than Drop:

trait BlockingAck {
  fn ack(&self);
}

We can still implement Drop on the type implementing BlockingAck:

struct InterruptAckToken { ... }

impl BlockingAck for InterruptAckToken {
  fn ack(&self) { let _ = syscall::interrupt_ack(...); }
}

impl Drop for InterruptAckToken {
  fn drop(&mut self) { self.ack(); }
}

Also, I think we want to keep the trait definitions separate from the implementations. We want to eventually get to a point where there can be non-pigweed OS uses of some of the Earlgrey driver code.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I get your point about keeping the traits separate from the structs that implement on top of Pigweed. Can you suggest which directory should depend on Pigweed and which should be for the generic traits? (I just blindly added under util.)

About the Drop vs. explicit ack() I am not sure I agree. It would be confusing if traits that implement Blocking can choose whether the ack() is automatic or not. If the BlockingInterrupt is implemented for some kernel besides Pigweed, we do not want to have to edit hal/blocking/flash/flash.rs to possibly call ack() explicitly. I think we should either say that ack() is always needed, or that it is always done through Drop. Or am I missing something?

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.

2 participants