[sw/silicon_creator] Add program/erase to flash_ctrl

Adds program and erase functionality to the mask rom flash_ctrl driver.

Signed-off-by: Jon Flatley <jflat@google.com>
diff --git a/sw/device/silicon_creator/lib/drivers/flash_ctrl.c b/sw/device/silicon_creator/lib/drivers/flash_ctrl.c
index 11d1b0f..839a040 100644
--- a/sw/device/silicon_creator/lib/drivers/flash_ctrl.c
+++ b/sw/device/silicon_creator/lib/drivers/flash_ctrl.c
@@ -23,39 +23,10 @@
   return !bitfield_bit32_read(bitfield, FLASH_CTRL_CTRL_REGWEN_EN_BIT);
 }
 
-void flash_ctrl_init(void) {
-  // Initialize the flash controller.
-  abs_mmio_write32(kBase + FLASH_CTRL_INIT_REG_OFFSET,
-                   bitfield_bit32_write(0u, FLASH_CTRL_INIT_VAL_BIT, true));
-}
-
-rom_error_t flash_ctrl_status_get(flash_ctrl_status_t *status) {
-  if (status == NULL) {
-    return kErrorFlashCtrlInvalidArgument;
-  }
-
-  // Read flash operation status.
-  uint32_t op_status = abs_mmio_read32(kBase + FLASH_CTRL_OP_STATUS_REG_OFFSET);
-  // Read flash controller status.
-  uint32_t fc_status = abs_mmio_read32(kBase + FLASH_CTRL_STATUS_REG_OFFSET);
-
-  // Extract operation status bits.
-  status->done = bitfield_bit32_read(op_status, FLASH_CTRL_OP_STATUS_DONE_BIT);
-  status->err = bitfield_bit32_read(op_status, FLASH_CTRL_OP_STATUS_ERR_BIT);
-
-  // Extract flash controller status bits.
-  status->rd_full =
-      bitfield_bit32_read(fc_status, FLASH_CTRL_STATUS_RD_FULL_BIT);
-  status->rd_empty =
-      bitfield_bit32_read(fc_status, FLASH_CTRL_STATUS_RD_EMPTY_BIT);
-  status->init_wip =
-      bitfield_bit32_read(fc_status, FLASH_CTRL_STATUS_INIT_WIP_BIT);
-
-  return kErrorOk;
-}
-
-rom_error_t flash_ctrl_read_start(uint32_t addr, uint32_t word_count,
-                                  flash_ctrl_partition_t region) {
+static rom_error_t transaction_start(uint32_t addr, uint32_t word_count,
+                                     flash_ctrl_partition_t region,
+                                     flash_ctrl_erase_type_t erase_type,
+                                     uint32_t op) {
   if (is_busy()) {
     return kErrorFlashCtrlBusy;
   }
@@ -64,8 +35,8 @@
   abs_mmio_write32(kBase + FLASH_CTRL_ADDR_REG_OFFSET, addr);
 
   // Set the operation of the transaction: read, program, or erase.
-  uint32_t control_reg_val = bitfield_field32_write(
-      0, FLASH_CTRL_CONTROL_OP_FIELD, FLASH_CTRL_CONTROL_OP_VALUE_READ);
+  uint32_t control_reg_val =
+      bitfield_field32_write(0, FLASH_CTRL_CONTROL_OP_FIELD, op);
 
   // Ensure special enum values match register definitions.
   static_assert(kFlashCtrlRegionData == 0u,
@@ -81,13 +52,20 @@
       kFlashCtrlRegionInfo2 == (1u << FLASH_CTRL_CONTROL_PARTITION_SEL_BIT |
                                 2u << FLASH_CTRL_CONTROL_INFO_SEL_OFFSET),
       "Incorrect enum value for kFlashCtrlRegionInfo2");
+  static_assert(kFlashCtrlErasePage == 0u,
+                "Incorrect enum value for kFlashCtrlErasePage");
+  static_assert(kFlashCtrlEraseBank == 1u << FLASH_CTRL_CONTROL_ERASE_SEL_BIT,
+                "Incorrect enum value for kFlashCtrlEraseBank");
 
   // Set the partition.
   control_reg_val |= (uint32_t)region;
 
-  // Set the number of words.
+  // Set the erase type.
+  control_reg_val |= (uint32_t)erase_type;
+
+  // Set the number of words as `word_count - 1` as noted in #3353.
   control_reg_val = bitfield_field32_write(
-      control_reg_val, FLASH_CTRL_CONTROL_NUM_FIELD, word_count);
+      control_reg_val, FLASH_CTRL_CONTROL_NUM_FIELD, word_count - 1);
 
   // Start the transaction.
   control_reg_val =
@@ -99,16 +77,115 @@
   return kErrorOk;
 }
 
-size_t flash_ctrl_fifo_read(size_t word_count, uint32_t *data_out) {
+static void fifo_read(size_t word_count, uint32_t *data_out) {
   // Keep reading until no words remain. For large reads this may create back
   // pressure.
-  size_t words_read = 0;
-  for (; words_read < word_count; ++words_read) {
+  for (size_t words_read = 0; words_read < word_count; ++words_read) {
     data_out[words_read] =
         abs_mmio_read32(kBase + FLASH_CTRL_RD_FIFO_REG_OFFSET);
   }
+}
 
-  return words_read;
+static void fifo_push(size_t word_count, const uint32_t *data) {
+  // Keep writing until no words remain. For large writes this may create back
+  // pressure.
+  for (size_t words_programmed = 0; words_programmed < word_count;
+       ++words_programmed) {
+    abs_mmio_write32(kBase + FLASH_CTRL_PROG_FIFO_REG_OFFSET,
+                     data[words_programmed]);
+  }
+}
+
+static rom_error_t check_errors(void) {
+  uint32_t op_status = abs_mmio_read32(kBase + FLASH_CTRL_OP_STATUS_REG_OFFSET);
+  if (bitfield_bit32_read(op_status, FLASH_CTRL_OP_STATUS_ERR_BIT)) {
+    return kErrorFlashCtrlInternal;
+  }
+  return kErrorOk;
+}
+
+static rom_error_t wait_for_done(void) {
+  uint32_t op_status;
+  do {
+    op_status = abs_mmio_read32(kBase + FLASH_CTRL_OP_STATUS_REG_OFFSET);
+  } while (!bitfield_bit32_read(op_status, FLASH_CTRL_OP_STATUS_DONE_BIT));
+
+  rom_error_t res = check_errors();
+
+  // Clear OP_STATUS.
+  abs_mmio_write32(kBase + FLASH_CTRL_OP_STATUS_REG_OFFSET, 0u);
+
+  return res;
+}
+
+void flash_ctrl_init(void) {
+  // Initialize the flash controller.
+  abs_mmio_write32(kBase + FLASH_CTRL_INIT_REG_OFFSET,
+                   bitfield_bit32_write(0u, FLASH_CTRL_INIT_VAL_BIT, true));
+}
+
+void flash_ctrl_status_get(flash_ctrl_status_t *status) {
+  // Read flash controller status.
+  uint32_t fc_status = abs_mmio_read32(kBase + FLASH_CTRL_STATUS_REG_OFFSET);
+
+  // Extract flash controller status bits.
+  status->rd_full =
+      bitfield_bit32_read(fc_status, FLASH_CTRL_STATUS_RD_FULL_BIT);
+  status->rd_empty =
+      bitfield_bit32_read(fc_status, FLASH_CTRL_STATUS_RD_EMPTY_BIT);
+  status->prog_full =
+      bitfield_bit32_read(fc_status, FLASH_CTRL_STATUS_PROG_FULL_BIT);
+  status->prog_empty =
+      bitfield_bit32_read(fc_status, FLASH_CTRL_STATUS_PROG_EMPTY_BIT);
+  status->init_wip =
+      bitfield_bit32_read(fc_status, FLASH_CTRL_STATUS_INIT_WIP_BIT);
+}
+
+rom_error_t flash_ctrl_read(uint32_t addr, uint32_t word_count,
+                            flash_ctrl_partition_t region, uint32_t *data) {
+  // Start the read transaction, the value of `erase_type` doesn't matter.
+  RETURN_IF_ERROR(transaction_start(addr, word_count, region,
+                                    kFlashCtrlErasePage,
+                                    FLASH_CTRL_CONTROL_OP_VALUE_READ));
+  fifo_read(word_count, data);
+  return wait_for_done();
+}
+
+rom_error_t flash_ctrl_prog(uint32_t addr, uint32_t word_count,
+                            flash_ctrl_partition_t region,
+                            const uint32_t *data) {
+  uint32_t window_offset = addr % FLASH_CTRL_PARAM_REG_BUS_PGM_RES_BYTES;
+
+  while (word_count > 0) {
+    // Program operations can't cross window boundaries.
+    uint32_t max_bytes = FLASH_CTRL_PARAM_REG_BUS_PGM_RES_BYTES - window_offset;
+    uint32_t write_size = max_bytes / sizeof(uint32_t);
+    write_size = word_count < write_size ? word_count : write_size;
+    window_offset = 0;
+
+    // Start the program transaction, the value of `erase_type` doesn't matter.
+    RETURN_IF_ERROR(transaction_start(addr, write_size, region,
+                                      kFlashCtrlErasePage,
+                                      FLASH_CTRL_CONTROL_OP_VALUE_PROG));
+
+    fifo_push(write_size, data);
+    RETURN_IF_ERROR(wait_for_done());
+
+    addr += write_size * sizeof(uint32_t);
+    data += write_size;
+    word_count -= write_size;
+  }
+
+  return kErrorOk;
+}
+
+rom_error_t flash_ctrl_erase(uint32_t addr, flash_ctrl_partition_t region,
+                             flash_ctrl_erase_type_t type) {
+  // Start the erase transaction, the value of `word_count` doesn't matter.
+  RETURN_IF_ERROR(transaction_start(addr, 1u, region, type,
+                                    FLASH_CTRL_CONTROL_OP_VALUE_ERASE));
+
+  return wait_for_done();
 }
 
 void flash_ctrl_exec_set(flash_ctrl_exec_t enable) {
diff --git a/sw/device/silicon_creator/lib/drivers/flash_ctrl.h b/sw/device/silicon_creator/lib/drivers/flash_ctrl.h
index dfce36b..1ad9ac1 100644
--- a/sw/device/silicon_creator/lib/drivers/flash_ctrl.h
+++ b/sw/device/silicon_creator/lib/drivers/flash_ctrl.h
@@ -33,17 +33,17 @@
    */
   bool rd_empty;
   /**
+   * Flash program FIFO full, software must consume data.
+   */
+  bool prog_full;
+  /**
+   * Flash program FIFO empty.
+   */
+  bool prog_empty;
+  /**
    * Flash controller undergoing init.
    */
   bool init_wip;
-  /**
-   * Flash operation done.
-   */
-  bool done;
-  /**
-   * Flash operation error.
-   */
-  bool err;
 } flash_ctrl_status_t;
 
 /**
@@ -54,10 +54,8 @@
  *
  * @param flash_ctrl flash controller device to check the status bits for.
  * @param[out] status_out The current status of the flash controller.
- * @return `kErrorFlashCtrlInvalidArgument` if `status` is NULL, `kErrorOk`
- * otherwise.
  */
-rom_error_t flash_ctrl_status_get(flash_ctrl_status_t *status);
+void flash_ctrl_status_get(flash_ctrl_status_t *status);
 
 /**
  * Region selection enumeration. Represents both the partition and the info
@@ -83,34 +81,85 @@
 } flash_ctrl_partition_t;
 
 /**
- * Start a read transaction.
+ * Perform a read transaction.
  *
  * The flash controller will truncate to the closest, lower word aligned
  * address. For example, if 0x13 is supplied, the controller will perform a read
  * at address 0x10.
  *
+ * On success, `data` is populated with the read data.
+ *
+ * For operations that fail with `kErrorFlashCtrlInternal`, `err` is set to the
+ * internal error mask for flash_ctrl, which can be checked against the
+ * `kFlashCtrlErr*` bits. The internal error state is cleared after each call.
+ *
  * @param addr The address to read from.
  * @param word_count The number of bus words the flash operation should read.
  * @param region The region to read from.
+ * @param[out] data The buffer to store the read data.
+ * @param[out] err The internal error state of flash_ctrl.
  * @return `kErrorFlashCtrlBusy` if the flash controller is already processing a
- * transaction, `kErrorOk` otherwise.
+ * transaction, `kErrorFlashCtrlInternal` if the operations fails, `kErrorOk`
+ * otherwise.
  */
-rom_error_t flash_ctrl_read_start(uint32_t addr, uint32_t word_count,
-                                  flash_ctrl_partition_t region);
+rom_error_t flash_ctrl_read(uint32_t addr, uint32_t word_count,
+                            flash_ctrl_partition_t region, uint32_t *data);
+
+typedef enum flash_ctrl_erase_type {
+  /**
+   * Erase a page.
+   */
+  kFlashCtrlErasePage = 0x0000,
+  /**
+   * Erase a bank.
+   */
+  kFlashCtrlEraseBank = 0x0080,
+} flash_ctrl_erase_type_t;
 
 /**
- * Read data from the read FIFO.
+ * Perform a program transaction.
  *
- * Attempts to read `word_count` words from the read FIFO.
+ * The flash controller will truncate to the closest, lower word aligned
+ * address. For example, if 0x13 is supplied, the controller will start writing
+ * at address 0x10.
  *
- * It is up to the caller to call `flash_ctrl_status_get()` to ensure the
- * flash controller completed this transaction successfully.
+ * For operations that fail with `kErrorFlashCtrlInternal`, `err` is set to the
+ * internal error mask for flash_ctrl, which can be checked against the
+ * `kFlashCtrlErr*` bits. The internal error state is cleared after each call.
  *
- * @param word_count The number of words to read.
- * @param data_out The region in memory to store the data read off the FIFO.
- * @return The number of words read from the FIFO.
+ * @param addr The address to write to.
+ * @param word_count The number of bus words the flash operation should program.
+ * @param region The region to program.
+ * @param data The buffer containing the data to program to flash.
+ * @param[out] err The internal error state of flash_ctrl.
+ * @return `kErrorFlashCtrlBusy` if the flash controller is already processing a
+ * transaction, `kErrorFlashCtrlInternal` if the operations fails, `kErrorOk`
+ * otherwise.
  */
-size_t flash_ctrl_fifo_read(size_t word_count, uint32_t *data_out);
+rom_error_t flash_ctrl_prog(uint32_t addr, uint32_t word_count,
+                            flash_ctrl_partition_t region,
+                            const uint32_t *data);
+
+/**
+ * Invoke a blocking erase transaction.
+ *
+ * The flash controller will truncate to the closest page boundary for page
+ * erase operations, and to the nearest bank aligned boundary for bank erase
+ * operations.
+ *
+ * For operations that fail with `kErrorFlashCtrlInternal`, `err` is set to the
+ * internal error mask for flash_ctrl, which can be checked against the
+ * `kFlashCtrlErr*` bits. The internal error state is cleared after each call.
+ *
+ * @param addr The address that falls within the bank or page being deleted.
+ * @param region The region that contains the bank or page being deleted.
+ * @param[out] err The internal error state of flash_ctrl.
+ * @return `kErrorFlashCtrlBusy` if the flash controller is already processing a
+ * transaction, `kErrorFlashCtrlInternal` if the operations fails, `kErrorOk`
+ * otherwise.
+ */
+rom_error_t flash_ctrl_erase(uint32_t addr, flash_ctrl_partition_t region,
+                             flash_ctrl_erase_type_t type);
 
 typedef enum flash_ctrl_exec {
   kFlashCtrlExecDisable = kMultiBitBool4False,
diff --git a/sw/device/silicon_creator/lib/drivers/flash_ctrl_unittest.cc b/sw/device/silicon_creator/lib/drivers/flash_ctrl_unittest.cc
index b7d1169..1e0e187 100644
--- a/sw/device/silicon_creator/lib/drivers/flash_ctrl_unittest.cc
+++ b/sw/device/silicon_creator/lib/drivers/flash_ctrl_unittest.cc
@@ -15,7 +15,6 @@
 
 namespace flash_ctrl_unittest {
 namespace {
-using ::testing::ElementsAre;
 
 class FlashCtrlTest : public mask_rom_test::MaskRomTest {
  protected:
@@ -34,16 +33,7 @@
 
 class StatusCheckTest : public FlashCtrlTest {};
 
-TEST_F(StatusCheckTest, InvalidArgument) {
-  EXPECT_EQ(flash_ctrl_status_get(NULL), kErrorFlashCtrlInvalidArgument);
-}
-
 TEST_F(StatusCheckTest, DefaultStatus) {
-  EXPECT_ABS_READ32(base_ + FLASH_CTRL_OP_STATUS_REG_OFFSET,
-                    {
-                        {FLASH_CTRL_OP_STATUS_DONE_BIT, false},
-                        {FLASH_CTRL_OP_STATUS_ERR_BIT, false},
-                    });
   EXPECT_ABS_READ32(base_ + FLASH_CTRL_STATUS_REG_OFFSET,
                     {
                         {FLASH_CTRL_STATUS_RD_FULL_BIT, false},
@@ -52,21 +42,14 @@
                     });
 
   flash_ctrl_status_t status;
-  EXPECT_EQ(flash_ctrl_status_get(&status), kErrorOk);
+  flash_ctrl_status_get(&status);
 
-  EXPECT_EQ(status.done, false);
-  EXPECT_EQ(status.err, false);
   EXPECT_EQ(status.init_wip, false);
   EXPECT_EQ(status.rd_empty, false);
   EXPECT_EQ(status.rd_full, false);
 }
 
 TEST_F(StatusCheckTest, AllSetStatus) {
-  EXPECT_ABS_READ32(base_ + FLASH_CTRL_OP_STATUS_REG_OFFSET,
-                    {
-                        {FLASH_CTRL_OP_STATUS_DONE_BIT, true},
-                        {FLASH_CTRL_OP_STATUS_ERR_BIT, true},
-                    });
   EXPECT_ABS_READ32(base_ + FLASH_CTRL_STATUS_REG_OFFSET,
                     {
                         {FLASH_CTRL_STATUS_RD_FULL_BIT, true},
@@ -75,74 +58,160 @@
                     });
 
   flash_ctrl_status_t status;
-  EXPECT_EQ(flash_ctrl_status_get(&status), kErrorOk);
+  flash_ctrl_status_get(&status);
 
-  EXPECT_EQ(status.done, true);
-  EXPECT_EQ(status.err, true);
   EXPECT_EQ(status.init_wip, true);
   EXPECT_EQ(status.rd_empty, true);
   EXPECT_EQ(status.rd_full, true);
 }
 
-class ReadStartTest : public FlashCtrlTest {
+class TransferTest : public FlashCtrlTest {
  protected:
+  const std::vector<uint32_t> words_ = {0x12345678, 0x90ABCDEF, 0x0F1E2D3C,
+                                        0x4B5A6978};
   void ExpectCheckBusy(bool busy) {
     EXPECT_ABS_READ32(base_ + FLASH_CTRL_CTRL_REGWEN_REG_OFFSET,
                       {{FLASH_CTRL_CTRL_REGWEN_EN_BIT, !busy}});
   }
-  void ExpectReadStart(uint8_t part_sel, uint8_t info_sel) {
-    EXPECT_ABS_WRITE32(base_ + FLASH_CTRL_ADDR_REG_OFFSET, 0x01234567);
-    EXPECT_ABS_WRITE32(
-        base_ + FLASH_CTRL_CONTROL_REG_OFFSET,
-        {
-            {FLASH_CTRL_CONTROL_OP_OFFSET, FLASH_CTRL_CONTROL_OP_VALUE_READ},
-            {FLASH_CTRL_CONTROL_PARTITION_SEL_BIT, part_sel},
-            {FLASH_CTRL_CONTROL_INFO_SEL_OFFSET, info_sel},
-            {FLASH_CTRL_CONTROL_NUM_OFFSET, 42},
-            {FLASH_CTRL_CONTROL_START_BIT, 1},
-        });
+
+  void ExpectWaitForDone(bool done, bool error) {
+    EXPECT_ABS_READ32(base_ + FLASH_CTRL_OP_STATUS_REG_OFFSET,
+                      {{FLASH_CTRL_OP_STATUS_DONE_BIT, done}});
+    if (done) {
+      EXPECT_ABS_READ32(base_ + FLASH_CTRL_OP_STATUS_REG_OFFSET,
+                        {{FLASH_CTRL_OP_STATUS_ERR_BIT, error}});
+      EXPECT_ABS_WRITE32(base_ + FLASH_CTRL_OP_STATUS_REG_OFFSET, 0u);
+    }
   }
-};
 
-TEST_F(ReadStartTest, Busy) {
-  ExpectCheckBusy(true);
-  EXPECT_EQ(flash_ctrl_read_start(0, 0, kFlashCtrlRegionData),
-            kErrorFlashCtrlBusy);
-}
+  void ExpectTransferStart(uint8_t part_sel, uint8_t info_sel,
+                           uint8_t erase_sel, uint32_t op, uint32_t addr,
+                           uint32_t word_count) {
+    ExpectCheckBusy(false);
+    EXPECT_ABS_WRITE32(base_ + FLASH_CTRL_ADDR_REG_OFFSET, addr);
+    EXPECT_ABS_WRITE32(base_ + FLASH_CTRL_CONTROL_REG_OFFSET,
+                       {
+                           {FLASH_CTRL_CONTROL_OP_OFFSET, op},
+                           {FLASH_CTRL_CONTROL_PARTITION_SEL_BIT, part_sel},
+                           {FLASH_CTRL_CONTROL_INFO_SEL_OFFSET, info_sel},
+                           {FLASH_CTRL_CONTROL_ERASE_SEL_BIT, erase_sel},
+                           {FLASH_CTRL_CONTROL_NUM_OFFSET, word_count - 1},
+                           {FLASH_CTRL_CONTROL_START_BIT, 1},
+                       });
+  }
 
-TEST_F(ReadStartTest, ReadData) {
-  ExpectCheckBusy(false);
-  ExpectReadStart(0, 0);
-  EXPECT_EQ(flash_ctrl_read_start(0x01234567, 42, kFlashCtrlRegionData),
-            kErrorOk);
-}
-
-TEST_F(ReadStartTest, ReadInfo) {
-  ExpectCheckBusy(false);
-  ExpectReadStart(1, 2);
-  EXPECT_EQ(flash_ctrl_read_start(0x01234567, 42, kFlashCtrlRegionInfo2),
-            kErrorOk);
-}
-
-class ReadTest : public FlashCtrlTest {
- protected:
-  const std::vector<uint32_t> words_ = {0x12345678, 0x90ABCDEF, 0x0F1E2D3C,
-                                        0x4B5A6978};
   void ExpectReadData(const std::vector<uint32_t> &data) {
     for (auto val : data) {
       EXPECT_ABS_READ32(base_ + FLASH_CTRL_RD_FIFO_REG_OFFSET, val);
     }
   }
+
+  void ExpectProgData(const std::vector<uint32_t> &data) {
+    for (auto val : data) {
+      EXPECT_ABS_WRITE32(base_ + FLASH_CTRL_PROG_FIFO_REG_OFFSET, val);
+    }
+  }
 };
 
-TEST_F(ReadTest, ReadAll) {
+TEST_F(TransferTest, ReadBusy) {
+  ExpectCheckBusy(true);
+  EXPECT_EQ(flash_ctrl_read(0, 0, kFlashCtrlRegionData, NULL),
+            kErrorFlashCtrlBusy);
+}
+
+TEST_F(TransferTest, ProgBusy) {
+  ExpectCheckBusy(true);
+  EXPECT_EQ(flash_ctrl_prog(0, 4, kFlashCtrlRegionData, NULL),
+            kErrorFlashCtrlBusy);
+}
+
+TEST_F(TransferTest, EraseBusy) {
+  ExpectCheckBusy(true);
+  EXPECT_EQ(flash_ctrl_erase(0, kFlashCtrlRegionData, kFlashCtrlErasePage),
+            kErrorFlashCtrlBusy);
+}
+
+TEST_F(TransferTest, ReadDataOk) {
+  ExpectTransferStart(0, 0, 0, FLASH_CTRL_CONTROL_OP_VALUE_READ, 0x01234567,
+                      words_.size());
   ExpectReadData(words_);
+  ExpectWaitForDone(true, false);
   std::vector<uint32_t> words_out(words_.size());
-  EXPECT_EQ(flash_ctrl_fifo_read(words_.size(), &words_out.front()),
-            words_.size());
+  EXPECT_EQ(flash_ctrl_read(0x01234567, words_.size(), kFlashCtrlRegionData,
+                            &words_out.front()),
+            kErrorOk);
   EXPECT_EQ(words_out, words_);
 }
 
+TEST_F(TransferTest, ProgDataOk) {
+  ExpectTransferStart(0, 0, 0, FLASH_CTRL_CONTROL_OP_VALUE_PROG, 0x01234567,
+                      words_.size());
+  ExpectProgData(words_);
+  ExpectWaitForDone(true, false);
+  EXPECT_EQ(flash_ctrl_prog(0x01234567, words_.size(), kFlashCtrlRegionData,
+                            &words_.front()),
+            kErrorOk);
+}
+
+TEST_F(TransferTest, EraseDataPageOk) {
+  ExpectTransferStart(0, 0, 0, FLASH_CTRL_CONTROL_OP_VALUE_ERASE, 0x01234567,
+                      1);
+  ExpectWaitForDone(true, false);
+  EXPECT_EQ(
+      flash_ctrl_erase(0x01234567, kFlashCtrlRegionData, kFlashCtrlErasePage),
+      kErrorOk);
+}
+
+TEST_F(TransferTest, ProgAcrossWindows) {
+  static const uint32_t kWinSize = FLASH_CTRL_PARAM_REG_BUS_PGM_RES_BYTES;
+  static const uint32_t kManyWordsSize = 2 * kWinSize / sizeof(uint32_t);
+
+  std::array<uint32_t, kManyWordsSize> many_words;
+  for (uint32_t i = 0; i < many_words.size(); ++i) {
+    many_words[i] = i;
+  }
+
+  auto iter = many_words.begin();
+  size_t half_step = kWinSize / sizeof(uint32_t) / 2;
+
+  // Program address range [0x20, 0x40)
+  ExpectTransferStart(0, 0, 0, FLASH_CTRL_CONTROL_OP_VALUE_PROG, kWinSize / 2,
+                      half_step);
+  ExpectProgData(std::vector<uint32_t>(iter, iter + half_step));
+  ExpectWaitForDone(true, false);
+  iter += half_step;
+
+  // Program address range [0x40, 0x80)
+  ExpectTransferStart(0, 0, 0, FLASH_CTRL_CONTROL_OP_VALUE_PROG, kWinSize,
+                      2 * half_step);
+  ExpectProgData(std::vector<uint32_t>(iter, iter + 2 * half_step));
+  ExpectWaitForDone(true, false);
+  iter += 2 * half_step;
+
+  // Programm address range [0x80, 0xA0)
+  ExpectTransferStart(0, 0, 0, FLASH_CTRL_CONTROL_OP_VALUE_PROG, 2 * kWinSize,
+                      half_step);
+  ExpectProgData(std::vector<uint32_t>(iter, iter + half_step));
+  ExpectWaitForDone(true, false);
+
+  EXPECT_EQ(iter + half_step, many_words.end());
+
+  EXPECT_EQ(flash_ctrl_prog(kWinSize / 2, many_words.size(),
+                            kFlashCtrlRegionData, &many_words.front()),
+            kErrorOk);
+}
+
+TEST_F(TransferTest, TransferInternalError) {
+  ExpectTransferStart(0, 0, 0, FLASH_CTRL_CONTROL_OP_VALUE_READ, 0x01234567,
+                      words_.size());
+  ExpectReadData(words_);
+  ExpectWaitForDone(true, true);
+  std::vector<uint32_t> words_out(words_.size());
+  EXPECT_EQ(flash_ctrl_read(0x01234567, words_.size(), kFlashCtrlRegionData,
+                            &words_out.front()),
+            kErrorFlashCtrlInternal);
+}
+
 class ExecTest : public FlashCtrlTest {};
 
 TEST_F(ExecTest, Enable) {
diff --git a/sw/device/silicon_creator/lib/error.h b/sw/device/silicon_creator/lib/error.h
index bc881a0..81d9d32 100644
--- a/sw/device/silicon_creator/lib/error.h
+++ b/sw/device/silicon_creator/lib/error.h
@@ -97,6 +97,7 @@
   X(kErrorOtbnUnavailable,            ERROR_(4, kModuleOtbn, kFailedPrecondition)), \
   X(kErrorFlashCtrlInvalidArgument,   ERROR_(1, kModuleFlashCtrl, kInvalidArgument)), \
   X(kErrorFlashCtrlBusy,              ERROR_(2, kModuleFlashCtrl, kUnavailable)), \
+  X(kErrorFlashCtrlInternal,          ERROR_(3, kModuleFlashCtrl, kInternal)), \
   X(kErrorSecMmioRegFileSize,         ERROR_(0, kModuleSecMmio, kResourceExhausted)), \
   X(kErrorSecMmioReadFault,           ERROR_(1, kModuleSecMmio, kInternal)), \
   X(kErrorSecMmioWriteFault,          ERROR_(2, kModuleSecMmio, kInternal)), \