[opentitantool] Add non-SPI rescue protocol for bootstrapping

Ti50 firmware supports a serial protocol for flashing a replacement
image, which we would like opentitantool to be able to use.  In order
to add this protocol a few changes to generic code has been made:

1. Refactoring of bootstrap/mod.rs to remove assumption of SPI and
bootstrap pin being used for all protocols.

2. Minor modifications to io/uart.rs trait and implementations, to
ensure uniform behavior of read_timeout(), and to simplify callers use
of write().

Signed-off-by: Jes B. Klinke <jbk@chromium.org>
Change-Id: I2a612abb2f2de0b2f1f72cdc7bf94c0a51ec99de
diff --git a/sw/host/opentitanlib/BUILD b/sw/host/opentitanlib/BUILD
index bb1e86a..907799e 100644
--- a/sw/host/opentitanlib/BUILD
+++ b/sw/host/opentitanlib/BUILD
@@ -16,6 +16,7 @@
         "src/bootstrap/legacy.rs",
         "src/bootstrap/mod.rs",
         "src/bootstrap/primitive.rs",
+        "src/bootstrap/rescue.rs",
         "src/crypto/mod.rs",
         "src/crypto/sha256.rs",
         "src/io/gpio.rs",
diff --git a/sw/host/opentitanlib/Cargo.toml b/sw/host/opentitanlib/Cargo.toml
index c975a69..71dd937 100644
--- a/sw/host/opentitanlib/Cargo.toml
+++ b/sw/host/opentitanlib/Cargo.toml
@@ -33,6 +33,7 @@
 num-bigint-dig = "0.7.0"
 num-traits = "0.2.14"
 sha2 = "0.10.1"
+humantime = "2.1.0"
 
 serde = { version="1", features=["serde_derive"] }
 serde_json = "1"
diff --git a/sw/host/opentitanlib/src/bootstrap/legacy.rs b/sw/host/opentitanlib/src/bootstrap/legacy.rs
index 1d9d5ff..d857bd9 100644
--- a/sw/host/opentitanlib/src/bootstrap/legacy.rs
+++ b/sw/host/opentitanlib/src/bootstrap/legacy.rs
@@ -8,8 +8,10 @@
 use thiserror::Error;
 use zerocopy::AsBytes;
 
-use crate::bootstrap::{BootstrapOptions, UpdateProtocol};
-use crate::io::spi::{Target, Transfer};
+use crate::app::TransportWrapper;
+use crate::bootstrap::{Bootstrap, BootstrapOptions, UpdateProtocol};
+use crate::io::spi::Transfer;
+use crate::transport::Capability;
 
 #[derive(AsBytes, Debug, Default)]
 #[repr(C)]
@@ -205,8 +207,31 @@
 }
 
 impl UpdateProtocol for Legacy {
+    fn verify_capabilities(
+        &self,
+        _container: &Bootstrap,
+        transport: &TransportWrapper,
+    ) -> Result<()> {
+        transport
+            .capabilities()
+            .request(Capability::GPIO | Capability::SPI)
+            .ok()?;
+        Ok(())
+    }
+
+    fn uses_common_bootstrap_reset(&self) -> bool {
+        true
+    }
+
     /// Performs the update protocol using the `transport` with the firmware `payload`.
-    fn update(&self, spi: &dyn Target, payload: &[u8]) -> Result<()> {
+    fn update(
+        &self,
+        container: &Bootstrap,
+        transport: &TransportWrapper,
+        payload: &[u8],
+    ) -> Result<()> {
+        let spi = container.spi_params.create(transport)?;
+
         let frames = Frame::from_payload(payload);
 
         // All frames up to but not including this index have been ack'ed by the bootloader.
diff --git a/sw/host/opentitanlib/src/bootstrap/mod.rs b/sw/host/opentitanlib/src/bootstrap/mod.rs
index beb6883..b29ba99 100644
--- a/sw/host/opentitanlib/src/bootstrap/mod.rs
+++ b/sw/host/opentitanlib/src/bootstrap/mod.rs
@@ -3,16 +3,22 @@
 // SPDX-License-Identifier: Apache-2.0
 
 use anyhow::Result;
+use humantime::parse_duration;
 use serde::Deserialize;
+use std::rc::Rc;
 use std::time::Duration;
 use structopt::clap::arg_enum;
+use structopt::StructOpt;
 use thiserror::Error;
 
+use crate::app::TransportWrapper;
 use crate::io::gpio::GpioPin;
-use crate::io::spi::Target;
+use crate::io::spi::SpiParams;
+use crate::io::uart::UartParams;
 
 mod legacy;
 mod primitive;
+mod rescue;
 
 #[derive(Debug, Error)]
 pub enum BootstrapError {
@@ -22,9 +28,10 @@
 
 arg_enum! {
     /// `BootstrapProtocol` describes the supported types of bootstrap.
-    /// The `Primitive` protocol is used by OpenTitan during development.
-    /// The `Legacy` protocol is used by previous generations of Google Titan-class chips.
-    /// The `Eeprom` protocol is planned to be implemented for OpenTitan.
+    /// The `Primitive` SPI protocol is used by OpenTitan during development.
+    /// The `Legacy` SPI protocol is used by previous generations of Google Titan-class chips.
+    /// The `Eeprom` SPI protocol is planned to be implemented for OpenTitan.
+    /// The `Rescue` UART protocol is used by Google Ti50 firmware.
     /// The 'Emulator' value indicates that this tool has a direct way
     /// of communicating with the OpenTitan emulator, to replace the
     /// contents of the emulated flash storage.
@@ -33,42 +40,81 @@
         Primitive,
         Legacy,
         Eeprom,
+        Rescue,
         Emulator,
     }
 }
 
 // Implementations of bootstrap need to implement the `UpdateProtocol` trait.
 trait UpdateProtocol {
-    fn update(&self, spi: &dyn Target, payload: &[u8]) -> Result<()>;
+    /// Called before any action is taken, to allow the protocol to verify that the transport
+    /// supports SPI/UART or whatever it needs.
+    fn verify_capabilities(
+        &self,
+        container: &Bootstrap,
+        transport: &TransportWrapper,
+    ) -> Result<()>;
+    /// Indicates whether the caller should assert the bootstrap pin and reset the chip, before
+    /// invoking update().
+    fn uses_common_bootstrap_reset(&self) -> bool;
+    /// Invoked to perform the actual transfer of an executable image to the OpenTitan chip.
+    fn update(
+        &self,
+        container: &Bootstrap,
+        transport: &TransportWrapper,
+        payload: &[u8],
+    ) -> Result<()>;
 }
 
 /// Options which control bootstrap behavior.
 /// The meaning of each of these values depends on the specific bootstrap protocol being used.
-#[derive(Debug, Default)]
+#[derive(Debug, StructOpt)]
 pub struct BootstrapOptions {
-    /// How long to hold the reset pin during the reset sequence.
+    #[structopt(flatten)]
+    pub uart_params: UartParams,
+    #[structopt(flatten)]
+    pub spi_params: SpiParams,
+    #[structopt(
+        short,
+        long,
+        possible_values = &BootstrapProtocol::variants(),
+        case_insensitive = true,
+        default_value = "primitive",
+        help = "Bootstrap protocol to use"
+    )]
+    pub protocol: BootstrapProtocol,
+    #[structopt(long, parse(try_from_str=parse_duration), help = "Duration of the reset delay")]
     pub reset_delay: Option<Duration>,
-    /// How long to delay between sending bootstrap frames.
+    #[structopt(long, parse(try_from_str=parse_duration), help = "Duration of the inter-frame delay")]
     pub inter_frame_delay: Option<Duration>,
-    /// How long to delay during a flash erase operation.
+    #[structopt(long, parse(try_from_str=parse_duration), help = "Duration of the flash-erase delay")]
     pub flash_erase_delay: Option<Duration>,
 }
 
 /// Bootstrap wraps and drives the various bootstrap protocols.
-pub struct Bootstrap {
+pub struct Bootstrap<'a> {
     pub protocol: BootstrapProtocol,
+    pub uart_params: &'a UartParams,
+    pub spi_params: &'a SpiParams,
+    reset_pin: Rc<dyn GpioPin>,
+    bootstrap_pin: Rc<dyn GpioPin>,
     reset_delay: Duration,
-    updater: Box<dyn UpdateProtocol>,
 }
 
-impl Bootstrap {
+impl<'a> Bootstrap<'a> {
     const RESET_DELAY: Duration = Duration::from_millis(200);
 
-    /// Consrtuct a `Bootstrap` struct configured to use `protocol` and `options`.
-    pub fn new(protocol: BootstrapProtocol, options: BootstrapOptions) -> Result<Self> {
-        let updater: Box<dyn UpdateProtocol> = match protocol {
+    /// Perform the update, sending the firmware `payload` to a SPI or UART target depending on
+    /// given `options`, which specifies protocol and port to use.
+    pub fn update(
+        transport: &TransportWrapper,
+        options: &BootstrapOptions,
+        payload: &[u8],
+    ) -> Result<()> {
+        let updater: Box<dyn UpdateProtocol> = match options.protocol {
             BootstrapProtocol::Primitive => Box::new(primitive::Primitive::new(&options)),
             BootstrapProtocol::Legacy => Box::new(legacy::Legacy::new(&options)),
+            BootstrapProtocol::Rescue => Box::new(rescue::Rescue::new(&options)),
             BootstrapProtocol::Eeprom => {
                 unimplemented!();
             }
@@ -77,36 +123,44 @@
                 unimplemented!();
             }
         };
-        Ok(Bootstrap {
-            protocol,
+        Bootstrap {
+            protocol: options.protocol,
+            uart_params: &options.uart_params,
+            spi_params: &options.spi_params,
+            reset_pin: transport.gpio_pin("RESET")?,
+            bootstrap_pin: transport.gpio_pin("BOOTSTRAP")?,
             reset_delay: options.reset_delay.unwrap_or(Self::RESET_DELAY),
-            updater: updater,
-        })
+        }
+        .do_update(updater, transport, payload)
     }
 
-    /// Perform the update, sending the firmware `payload` to the `spi` target,
-    /// using `gpio` to sequence the reset and bootstrap pins.
-    pub fn update(
+    fn do_update(
         &self,
-        spi: &dyn Target,
-        reset: &dyn GpioPin,
-        bootstrap: &dyn GpioPin,
+        updater: Box<dyn UpdateProtocol>,
+        transport: &TransportWrapper,
         payload: &[u8],
     ) -> Result<()> {
-        log::info!("Asserting bootstrap pins...");
-        bootstrap.write(true)?;
+        updater.verify_capabilities(&self, transport)?;
+        let perform_bootstrap_reset = updater.uses_common_bootstrap_reset();
 
-        log::info!("Restting the target...");
-        reset.write(false)?; // Low active
-        std::thread::sleep(self.reset_delay);
-        reset.write(true)?; // Release reset
-        std::thread::sleep(self.reset_delay);
+        if perform_bootstrap_reset {
+            log::info!("Asserting bootstrap pins...");
+            self.bootstrap_pin.write(true)?;
 
-        log::info!("Performing bootstrap...");
-        self.updater.update(spi, payload)?;
+            log::info!("Reseting the target...");
+            self.reset_pin.write(false)?; // Low active
+            std::thread::sleep(self.reset_delay);
+            self.reset_pin.write(true)?; // Release reset
+            std::thread::sleep(self.reset_delay);
 
-        log::info!("Releasing bootstrap pins...");
-        bootstrap.write(false)?;
-        Ok(())
+            log::info!("Performing bootstrap...");
+        }
+        let result = updater.update(&self, transport, payload);
+
+        if perform_bootstrap_reset {
+            log::info!("Releasing bootstrap pins...");
+            self.bootstrap_pin.write(false)?;
+        }
+        result
     }
 }
diff --git a/sw/host/opentitanlib/src/bootstrap/primitive.rs b/sw/host/opentitanlib/src/bootstrap/primitive.rs
index 4c7d4e2..37ed60e 100644
--- a/sw/host/opentitanlib/src/bootstrap/primitive.rs
+++ b/sw/host/opentitanlib/src/bootstrap/primitive.rs
@@ -7,8 +7,10 @@
 use std::time::Duration;
 use zerocopy::AsBytes;
 
-use crate::bootstrap::{BootstrapOptions, UpdateProtocol};
-use crate::io::spi::{Target, Transfer};
+use crate::app::TransportWrapper;
+use crate::bootstrap::{Bootstrap, BootstrapOptions, UpdateProtocol};
+use crate::io::spi::Transfer;
+use crate::transport::Capability;
 
 #[derive(AsBytes, Debug, Default)]
 #[repr(C)]
@@ -105,8 +107,31 @@
 }
 
 impl UpdateProtocol for Primitive {
+    fn verify_capabilities(
+        &self,
+        _container: &Bootstrap,
+        transport: &TransportWrapper,
+    ) -> Result<()> {
+        transport
+            .capabilities()
+            .request(Capability::GPIO | Capability::SPI)
+            .ok()?;
+        Ok(())
+    }
+
+    fn uses_common_bootstrap_reset(&self) -> bool {
+        true
+    }
+
     /// Performs the update protocol using the `transport` with the firmware `payload`.
-    fn update(&self, spi: &dyn Target, payload: &[u8]) -> Result<()> {
+    fn update(
+        &self,
+        container: &Bootstrap,
+        transport: &TransportWrapper,
+        payload: &[u8],
+    ) -> Result<()> {
+        let spi = container.spi_params.create(transport)?;
+
         let frames = Frame::from_payload(payload);
 
         let mut i = 0;
diff --git a/sw/host/opentitanlib/src/bootstrap/rescue.rs b/sw/host/opentitanlib/src/bootstrap/rescue.rs
new file mode 100644
index 0000000..d6585c2
--- /dev/null
+++ b/sw/host/opentitanlib/src/bootstrap/rescue.rs
@@ -0,0 +1,356 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+use anyhow::{bail, ensure, Result};
+use mundane::hash::{Digest, Hasher, Sha256};
+use std::time::{Duration, Instant};
+use thiserror::Error;
+use zerocopy::AsBytes;
+
+use crate::app::TransportWrapper;
+use crate::bootstrap::{Bootstrap, BootstrapOptions, UpdateProtocol};
+use crate::io::uart::Uart;
+use crate::transport::Capability;
+
+#[derive(AsBytes, Debug, Default)]
+#[repr(C)]
+struct FrameHeader {
+    hash: [u8; Frame::HASH_LEN],
+    frame_num: u32,
+    flash_offset: u32,
+}
+
+#[derive(AsBytes, Debug)]
+#[repr(C)]
+struct Frame {
+    header: FrameHeader,
+    data: [u8; Frame::DATA_LEN],
+}
+
+impl Default for Frame {
+    fn default() -> Self {
+        Frame {
+            header: Default::default(),
+            data: [0xff; Frame::DATA_LEN],
+        }
+    }
+}
+
+impl Frame {
+    const EOF: u32 = 0x8000_0000;
+    const FLASH_SECTOR_SIZE: usize = 2048;
+    const FLASH_SECTOR_MASK: usize = Self::FLASH_SECTOR_SIZE - 1;
+    const FLASH_BUFFER_SIZE: usize = 128;
+    const FLASH_BUFFER_MASK: usize = Self::FLASH_BUFFER_SIZE - 1;
+    const DATA_LEN: usize = 1024 - std::mem::size_of::<FrameHeader>();
+    const HASH_LEN: usize = 32;
+    const MAGIC_HEADER: [u8; 4] = [0xfd, 0xff, 0xff, 0xff];
+
+    /// Computes the hash in the header.
+    fn header_hash(&self) -> [u8; Frame::HASH_LEN] {
+        let frame = self.as_bytes();
+        let sha = Sha256::hash(&frame[Frame::HASH_LEN..]);
+        sha.bytes()
+    }
+
+    /// Computes the hash over the entire frame.
+    fn frame_hash(&self) -> [u8; Frame::HASH_LEN] {
+        let sha = Sha256::hash(self.as_bytes());
+        let mut digest = sha.bytes();
+        // Touch up zeroes into ones, as that is what the old chips are doing.
+        for b in &mut digest {
+            if *b == 0 {
+                *b = 1;
+            }
+        }
+        digest
+    }
+
+    /// Creates a sequence of frames based on a `payload` binary.
+    fn from_payload(payload: &[u8]) -> Result<Vec<Frame>> {
+        // The given payload will contain up to four sections concatenated together:
+        // RO_A, RW_A optionally follwed by RO_B, RW_B
+        // Each section starts with a magic number on at least a 256 byte boundary.
+
+        // This rescue protocol uses the RW_A section only, which will start at the second
+        // occurrance of the magic value, and end at the third occurrence or at the end of the
+        // file.
+
+        ensure!(
+            payload.starts_with(&Self::MAGIC_HEADER),
+            RescueError::ImageFormatError
+        );
+
+        // Find second occurrence of magic value.
+        let min_addr = match payload[256..]
+            .chunks(256)
+            .position(|c| &c[0..4] == &Self::MAGIC_HEADER)
+        {
+            Some(n) => (n + 1) * 256,
+            None => bail!(RescueError::ImageFormatError),
+        };
+
+        // Find third occurrence of magic value.
+        let max_addr = match payload[min_addr + 256..]
+            .chunks(256)
+            .position(|c| &c[0..4] == &Self::MAGIC_HEADER)
+        {
+            Some(n) => (n + 1) * 256 + min_addr,
+            None => payload.len(),
+        };
+
+        // Trim trailing 0xff bytes.
+        let max_addr = (payload[..max_addr]
+            .chunks(4)
+            .rposition(|c| c != &[0xff; 4])
+            .unwrap_or(0)
+            + 1)
+            * 4;
+
+        let mut frames = Vec::new();
+        let mut frame_num = 0;
+        let mut addr = min_addr;
+        while addr < max_addr {
+            // Try skipping over 0xffffffff words.
+            let nonempty_addr = addr
+                + payload[addr..]
+                    .chunks(4)
+                    .position(|c| c != &[0xff; 4])
+                    .unwrap()
+                    * 4;
+            let skip_addr = nonempty_addr & !Self::FLASH_SECTOR_MASK;
+            if skip_addr > addr && (addr == 0 || addr & Self::FLASH_BUFFER_MASK != 0) {
+                // Can only skip from the start or if the last addr wasn't an exact multiple of
+                // 128 (per H1D boot rom).
+                addr = skip_addr;
+            }
+
+            let mut frame = Frame {
+                header: FrameHeader {
+                    frame_num,
+                    flash_offset: addr as u32,
+                    ..Default::default()
+                },
+                ..Default::default()
+            };
+            let slice_size = Self::DATA_LEN.min(payload.len() - addr);
+            frame.data[..slice_size].copy_from_slice(&payload[addr..addr + slice_size]);
+            frames.push(frame);
+
+            addr += Self::DATA_LEN;
+            frame_num += 1;
+        }
+        frames.last_mut().map(|f| f.header.frame_num |= Self::EOF);
+        frames
+            .iter_mut()
+            .for_each(|f| f.header.hash = f.header_hash());
+        Ok(frames)
+    }
+}
+
+#[derive(Debug, Error)]
+pub enum RescueError {
+    #[error("Unrecognized image file format")]
+    ImageFormatError,
+    #[error("Synchronization error communicating with boot rom")]
+    SyncError,
+    #[error("Repeated errors communicating with boot rom")]
+    RepeatedErrors,
+}
+
+/// Implements the UART rescue protocol of Google Ti50 firmware.
+pub struct Rescue {}
+
+impl Rescue {
+    /// Abort if a block has not been accepted after this number of retries.
+    const MAX_CONSECUTIVE_ERRORS: u32 = 50;
+    /// Take some measure to regain protocol synchronization, in case of this number of retries
+    /// of the same block.
+    const RESYNC_AFTER_CONSECUTIVE_ERRORS: u32 = 3;
+
+    /// Creates a new `Rescue` protocol updater from `options`.
+    pub fn new(_options: &BootstrapOptions) -> Self {
+        Self {}
+    }
+
+    /// Waits for some time for a character, returns None on timeout.
+    fn read_char(&self, uart: &dyn Uart) -> Option<char> {
+        let mut buf = [0u8; 1];
+        match uart.read_timeout(&mut buf, Duration::from_millis(100)) {
+            Ok(1) => Some(buf[0] as char),
+            Ok(_) => None,
+            _ => None,
+        }
+    }
+
+    /// Waits some time for data, returning true if the given string was seen in full, or false
+    /// as soon as a non-matching character is received or on timeout.
+    fn expect_string(&self, uart: &dyn Uart, s: &str) -> bool {
+        for expected_ch in s.chars() {
+            match self.read_char(uart) {
+                Some(ch) if ch == expected_ch => (),
+                _ => return false,
+            }
+        }
+        true
+    }
+
+    /// Reads and discards any characters in the receive buffer, waiting a little while for any
+    /// more which will also be discarded.
+    fn flush_rx(&self, uart: &dyn Uart) {
+        let mut response = [0u8; Frame::HASH_LEN];
+        loop {
+            match uart.read_timeout(&mut response, Duration::from_millis(500)) {
+                Ok(0) | Err(_) => break,
+                Ok(_) => continue,
+            }
+        }
+    }
+
+    /// As the 1024 byte blocks sent to the chip have no discernible header, the sender and
+    /// receiver could be "out of sync".  This is resolved by sending one byte at a time, and
+    /// observing when the chip sends a response (which will be a rejection due to checksum).
+    fn synchronize(&self, uart: &dyn Uart) -> Result<()> {
+        // Most likely, only a few "extra" bytes have been sent during initial negotiation.
+        // Send almost a complete block in one go, and then send each of the last 16 bytes one
+        // at a time, slowly enough to detect a response before sending the next byte.
+        uart.write(&[0u8; 1008])?;
+        let mut response = [0u8; 1];
+        let limit = match uart.read_timeout(&mut response, Duration::from_millis(50)) {
+            Ok(0) | Err(_) => 16,
+            Ok(_) => {
+                // A response at this point must mean that more than 16 bytes had already been
+                // sent before entering this method.  This will be resolved by doing another
+                // slower round of 1024 bytes with delay in between every one.
+                self.flush_rx(uart);
+                1024
+            }
+        };
+        for _ in 0..limit {
+            uart.write(&[0u8; 1])?;
+            match uart.read_timeout(&mut response, Duration::from_millis(50)) {
+                Ok(0) | Err(_) => (),
+                Ok(_) => {
+                    self.flush_rx(uart);
+                    return Ok(());
+                }
+            }
+        }
+        Err(RescueError::SyncError.into())
+    }
+
+    /// Reset the chip and send the magic 'r' character at the opportune moment during boot in
+    /// order to enter rescue more, repeat if necessary.
+    fn enter_rescue_mode(&self, container: &Bootstrap, uart: &dyn Uart) -> Result<()> {
+        // Attempt getting the attention of the bootloader.
+        let timeout = Duration::from_millis(2000);
+        for _ in 0..Self::MAX_CONSECUTIVE_ERRORS {
+            eprint!("Resetting...");
+            container.reset_pin.write(false)?; // Low active
+            std::thread::sleep(container.reset_delay);
+            container.reset_pin.write(true)?; // Release reset
+            let stopwatch = Instant::now();
+            while stopwatch.elapsed() < timeout {
+                if !self.expect_string(uart, "Bldr |") {
+                    continue;
+                }
+                uart.write(&['r' as u8])?;
+                eprint!("a.");
+                while stopwatch.elapsed() < timeout {
+                    if !self.expect_string(uart, "oops?|") {
+                        continue;
+                    }
+                    uart.write(&['r' as u8])?;
+                    eprint!("b.");
+                    if self.expect_string(uart, "escue") {
+                        eprintln!("c: Entered rescue mode!");
+                        self.synchronize(uart)?;
+                        return Ok(());
+                    }
+                }
+            }
+            eprintln!(" Failed to enter rescue mode.");
+        }
+        bail!(RescueError::RepeatedErrors);
+    }
+}
+
+impl UpdateProtocol for Rescue {
+    fn verify_capabilities(
+        &self,
+        _container: &Bootstrap,
+        transport: &TransportWrapper,
+    ) -> Result<()> {
+        transport
+            .capabilities()
+            .request(Capability::GPIO | Capability::UART)
+            .ok()?;
+        Ok(())
+    }
+
+    /// Returns false, in order to as the containing Bootstrap struct to not perform standard
+    /// BOOTSTRAP/RESET sequence.
+    fn uses_common_bootstrap_reset(&self) -> bool {
+        false
+    }
+
+    /// Performs the update protocol using the `transport` with the firmware `payload`.
+    fn update(
+        &self,
+        container: &Bootstrap,
+        transport: &TransportWrapper,
+        payload: &[u8],
+    ) -> Result<()> {
+        let frames = Frame::from_payload(payload)?;
+        let uart = container.uart_params.create(transport)?;
+
+        self.enter_rescue_mode(container, &*uart)?;
+
+        // Send frames one at a time.
+        'next_block: for (idx, frame) in frames.iter().enumerate() {
+            for consecutive_errors in 0..Self::MAX_CONSECUTIVE_ERRORS {
+                eprint!("{}.", idx);
+                uart.write(frame.as_bytes())?;
+                let mut response = [0u8; Frame::HASH_LEN];
+                let mut index = 0;
+                while index < Frame::HASH_LEN {
+                    let timeout = if index == 0 {
+                        Duration::from_millis(1000)
+                    } else {
+                        Duration::from_millis(10)
+                    };
+                    match uart.read_timeout(&mut response[index..], timeout) {
+                        Ok(0) | Err(_) => break,
+                        Ok(n) => index += n,
+                    }
+                }
+                if index < Frame::HASH_LEN {
+                    eprint!("sync.");
+                    self.synchronize(&*uart)?;
+                    continue;
+                }
+                if response[4..].chunks(4).all(|x| x == &response[..4]) {
+                    eprint!("sync.");
+                    self.synchronize(&*uart)?;
+                } else if response == frame.frame_hash() {
+                    continue 'next_block;
+                } else {
+                    self.flush_rx(&*uart);
+                    if consecutive_errors >= Self::RESYNC_AFTER_CONSECUTIVE_ERRORS {
+                        eprint!("sync.");
+                        self.synchronize(&*uart)?;
+                    }
+                }
+            }
+            bail!(RescueError::RepeatedErrors);
+        }
+
+        // Reset, in order to leave rescue mode.
+        container.reset_pin.write(false)?; // Low active
+        std::thread::sleep(container.reset_delay);
+        container.reset_pin.write(true)?; // Release reset
+        eprintln!("Success!");
+        Ok(())
+    }
+}
diff --git a/sw/host/opentitanlib/src/io/uart.rs b/sw/host/opentitanlib/src/io/uart.rs
index 0eea670..7728be0 100644
--- a/sw/host/opentitanlib/src/io/uart.rs
+++ b/sw/host/opentitanlib/src/io/uart.rs
@@ -45,5 +45,5 @@
     fn read_timeout(&self, buf: &mut [u8], timeout: Duration) -> Result<usize>;
 
     /// Writes data from `buf` to the UART.
-    fn write(&self, buf: &[u8]) -> Result<usize>;
+    fn write(&self, buf: &[u8]) -> Result<()>;
 }
diff --git a/sw/host/opentitanlib/src/transport/cw310/uart.rs b/sw/host/opentitanlib/src/transport/cw310/uart.rs
index 7f33680..1038e4e 100644
--- a/sw/host/opentitanlib/src/transport/cw310/uart.rs
+++ b/sw/host/opentitanlib/src/transport/cw310/uart.rs
@@ -79,7 +79,11 @@
     }
 
     /// Writes data from `buf` to the UART.
-    fn write(&self, buf: &[u8]) -> Result<usize> {
-        Ok(self.port.borrow_mut().write(buf)?)
+    fn write(&self, mut buf: &[u8]) -> Result<()> {
+        while buf.len() > 0 {
+            let written = self.port.borrow_mut().write(buf)?;
+            buf = &buf[written..];
+        }
+        Ok(())
     }
 }
diff --git a/sw/host/opentitanlib/src/transport/hyperdebug/uart.rs b/sw/host/opentitanlib/src/transport/hyperdebug/uart.rs
index 1157e57..2e13d82 100644
--- a/sw/host/opentitanlib/src/transport/hyperdebug/uart.rs
+++ b/sw/host/opentitanlib/src/transport/hyperdebug/uart.rs
@@ -43,8 +43,12 @@
         Ok(self.port.borrow_mut().read(buf)?)
     }
 
-    fn write(&self, buf: &[u8]) -> Result<usize> {
-        Ok(self.port.borrow_mut().write(buf)?)
+    fn write(&self, mut buf: &[u8]) -> Result<()> {
+        while buf.len() > 0 {
+            let written = self.port.borrow_mut().write(buf)?;
+            buf = &buf[written..];
+        }
+        Ok(())
     }
 
     fn get_baudrate(&self) -> u32 {
diff --git a/sw/host/opentitanlib/src/transport/ultradebug/uart.rs b/sw/host/opentitanlib/src/transport/ultradebug/uart.rs
index c6475c4..7af655b 100644
--- a/sw/host/opentitanlib/src/transport/ultradebug/uart.rs
+++ b/sw/host/opentitanlib/src/transport/ultradebug/uart.rs
@@ -5,7 +5,9 @@
 use anyhow::Result;
 use safe_ftdi as ftdi;
 use std::cell::RefCell;
-use std::time::Duration;
+use std::cmp;
+use std::thread;
+use std::time::{Duration, Instant};
 
 use crate::io::uart::Uart;
 use crate::transport::ultradebug::Ultradebug;
@@ -48,11 +50,21 @@
         Ok(())
     }
 
-    fn read_timeout(&self, buf: &mut [u8], _timeout: Duration) -> Result<usize> {
-        // Note: my recollection is that there is no way to set a read timeout
-        // for the UART.  If there are no characters ready, the FTDI device
-        // simply returns a zero-length read.
-        self.read(buf)
+    fn read_timeout(&self, buf: &mut [u8], timeout: Duration) -> Result<usize> {
+        let now = Instant::now();
+        let count = self.read(buf)?;
+        if count > 0 {
+            return Ok(count);
+        }
+        let short_delay = cmp::min(timeout.mul_f32(0.1), Duration::from_millis(20));
+        while now.elapsed() < timeout {
+            thread::sleep(short_delay);
+            let count = self.read(buf)?;
+            if count > 0 {
+                return Ok(count);
+            }
+        }
+        Ok(0)
     }
 
     fn read(&self, buf: &mut [u8]) -> Result<usize> {
@@ -60,13 +72,12 @@
         Ok(n as usize)
     }
 
-    fn write(&self, buf: &[u8]) -> Result<usize> {
-        let mut total = 0usize;
+    fn write(&self, mut buf: &[u8]) -> Result<()> {
         let inner = self.inner.borrow();
-        while total < buf.len() {
-            let n = inner.device.write_data(&buf[total..])?;
-            total += n as usize;
+        while buf.len() > 0 {
+            let n = inner.device.write_data(buf)?;
+            buf = &buf[n as usize..];
         }
-        Ok(total)
+        Ok(())
     }
 }
diff --git a/sw/host/opentitanlib/src/transport/verilator/uart.rs b/sw/host/opentitanlib/src/transport/verilator/uart.rs
index f74cc28..3759ca7 100644
--- a/sw/host/opentitanlib/src/transport/verilator/uart.rs
+++ b/sw/host/opentitanlib/src/transport/verilator/uart.rs
@@ -47,8 +47,7 @@
         Ok(self.file.borrow_mut().read(buf)?)
     }
 
-    fn write(&self, buf: &[u8]) -> Result<usize> {
-        self.file.borrow_mut().write_all(buf)?;
-        Ok(buf.len())
+    fn write(&self, buf: &[u8]) -> Result<()> {
+        Ok(self.file.borrow_mut().write_all(buf)?)
     }
 }
diff --git a/sw/host/opentitantool/Cargo.toml b/sw/host/opentitantool/Cargo.toml
index aa6ea01..44f0b80 100644
--- a/sw/host/opentitantool/Cargo.toml
+++ b/sw/host/opentitantool/Cargo.toml
@@ -24,7 +24,6 @@
 regex = "1"
 nix = "0.17.0"
 indicatif = "0.16.2"
-humantime = "2.1.0"
 directories = "4.0.1"
 shellwords = "1.1.0"
 
diff --git a/sw/host/opentitantool/src/command/bootstrap.rs b/sw/host/opentitantool/src/command/bootstrap.rs
index 2b0874c..aa552d5 100644
--- a/sw/host/opentitantool/src/command/bootstrap.rs
+++ b/sw/host/opentitantool/src/command/bootstrap.rs
@@ -4,18 +4,14 @@
 
 use anyhow::{ensure, Result};
 use erased_serde::Serialize;
-use humantime::parse_duration;
 use std::any::Any;
 use std::path::PathBuf;
-use std::time::Duration;
 use structopt::StructOpt;
 
 use opentitanlib::app::command::CommandDispatch;
 use opentitanlib::app::TransportWrapper;
 use opentitanlib::bootstrap::{Bootstrap, BootstrapOptions, BootstrapProtocol};
-use opentitanlib::io::spi::SpiParams;
 use opentitanlib::transport;
-use opentitanlib::transport::Capability;
 use opentitanlib::util::image::ImageAssembler;
 use opentitanlib::util::parse_int::ParseInt;
 
@@ -23,22 +19,7 @@
 #[derive(Debug, StructOpt)]
 pub struct BootstrapCommand {
     #[structopt(flatten)]
-    params: SpiParams,
-    #[structopt(
-        short,
-        long,
-        possible_values = &BootstrapProtocol::variants(),
-        case_insensitive = true,
-        default_value = "primitive",
-        help = "Bootstrap protocol to use"
-    )]
-    protocol: BootstrapProtocol,
-    #[structopt(long, parse(try_from_str=parse_duration), help = "Duration of the reset delay")]
-    reset_delay: Option<Duration>,
-    #[structopt(long, parse(try_from_str=parse_duration), help = "Duration of the inter-frame delay")]
-    inter_frame_delay: Option<Duration>,
-    #[structopt(long, parse(try_from_str=parse_duration), help = "Duration of the flash-erase delay")]
-    flash_erase_delay: Option<Duration>,
+    bootstrap_options: BootstrapOptions,
     #[structopt(
         long,
         parse(try_from_str=usize::from_str),
@@ -97,27 +78,11 @@
             !self.filename.is_empty(),
             "You must supply at least one filename"
         );
-        if self.protocol == BootstrapProtocol::Emulator {
+        if self.bootstrap_options.protocol == BootstrapProtocol::Emulator {
             return self.bootstrap_using_direct_emulator_integration(transport);
         }
 
-        transport
-            .capabilities()
-            .request(Capability::GPIO | Capability::SPI)
-            .ok()?;
-
-        let options = BootstrapOptions {
-            reset_delay: self.reset_delay,
-            inter_frame_delay: self.inter_frame_delay,
-            flash_erase_delay: self.flash_erase_delay,
-        };
-        let bootstrap = Bootstrap::new(self.protocol, options)?;
-
-        let spi = self.params.create(transport)?;
-        let reset_pin = transport.gpio_pin("RESET")?;
-        let bootstrap_pin = transport.gpio_pin("BOOTSTRAP")?;
-        let payload = self.payload()?;
-        bootstrap.update(&*spi, &*reset_pin, &*bootstrap_pin, &payload)?;
+        Bootstrap::update(&transport, &self.bootstrap_options, &self.payload()?)?;
         Ok(None)
     }
 }
diff --git a/third_party/cargo/crates.bzl b/third_party/cargo/crates.bzl
index bf6dd5f..95a633a 100644
--- a/third_party/cargo/crates.bzl
+++ b/third_party/cargo/crates.bzl
@@ -18,6 +18,7 @@
         "deser-hjson": "@raze__deser_hjson__1_0_2//:deser_hjson",
         "erased-serde": "@raze__erased_serde__0_3_16//:erased_serde",
         "hex": "@raze__hex__0_4_3//:hex",
+        "humantime": "@raze__humantime__2_1_0//:humantime",
         "lazy_static": "@raze__lazy_static__1_4_0//:lazy_static",
         "log": "@raze__log__0_4_14//:log",
         "nix": "@raze__nix__0_17_0//:nix",
@@ -48,7 +49,6 @@
         "env_logger": "@raze__env_logger__0_8_4//:env_logger",
         "erased-serde": "@raze__erased_serde__0_3_16//:erased_serde",
         "hex": "@raze__hex__0_4_3//:hex",
-        "humantime": "@raze__humantime__2_1_0//:humantime",
         "indicatif": "@raze__indicatif__0_16_2//:indicatif",
         "log": "@raze__log__0_4_14//:log",
         "nix": "@raze__nix__0_17_0//:nix",