opentitanlib/transport/hyperdebug/
dfu.rs

1// Copyright lowRISC contributors (OpenTitan project).
2// Licensed under the Apache License, Version 2.0, see LICENSE for details.
3// SPDX-License-Identifier: Apache-2.0
4
5// Firmware update protocol for HyperDebug
6
7use std::any::Any;
8use std::cell::RefCell;
9use std::sync::LazyLock;
10
11use anyhow::{Result, anyhow, bail};
12use regex::Regex;
13use serde_annotate::Annotate;
14
15use crate::transport::{
16    Capabilities, Capability, ProgressIndicator, Transport, TransportError, UpdateFirmware,
17};
18use crate::util::usb::UsbBackend;
19
20const VID_ST_MICROELECTRONICS: u16 = 0x0483;
21const PID_DFU_BOOTLOADER: u16 = 0xdf11;
22
23/// This transport is to be used if a Nucleo board is already in DFU bootloader mode at the time
24/// of the `opentitantool` invocation (and presenting itself with STMs VID:DID, rather than
25/// Google's).
26pub struct HyperdebugDfu {
27    // Handle to USB device which may or may not already be in DFU mode
28    usb_backend: RefCell<UsbBackend>,
29    current_firmware_version: Option<String>,
30    // expected USB VID:PID of the HyperDebug device when not in DFU mode
31    usb_vid: u16,
32    usb_pid: u16,
33}
34
35impl HyperdebugDfu {
36    /// Establish connection with a particular Nucleo-L552ZE board possibly already in DFU
37    /// bootloader mode.
38    pub fn open(
39        usb_vid: Option<u16>,
40        usb_pid: Option<u16>,
41        usb_serial: Option<&str>,
42    ) -> Result<Self> {
43        // Look for a device with given USB serial, carrying either the VID:DID of STM32 DFU
44        // bootloader, or that of HyperDebug in ordinary mode.  This allows scripts to start with
45        // `opentitantool --interface hyperdebug_dfu transport update-firmware`, knowing that it
46        // will put the desired firmware on the HyperDebug, both in the case of previous
47        // interrupted update, as well as the ordinary case of outdated or current HyperDebug
48        // firmware already running.
49        if let Ok(usb_backend) =
50            UsbBackend::new(VID_ST_MICROELECTRONICS, PID_DFU_BOOTLOADER, usb_serial)
51        {
52            // HyperDebug device is already in DFU mode, we cannot query firmware version through
53            // USB strings.  (And the fact that it was left in DFU mode, probably as a result of a
54            // previous incomplete update attempt, should mean that we would not want to trust the
55            // version, even if we could extract it through the DFU firmware.)
56            return Ok(Self {
57                usb_backend: RefCell::new(usb_backend),
58                current_firmware_version: None,
59                usb_vid: usb_vid.unwrap_or(super::VID_GOOGLE),
60                usb_pid: usb_pid.unwrap_or(super::PID_HYPERDEBUG),
61            });
62        }
63
64        let usb_backend = UsbBackend::new(
65            usb_vid.unwrap_or(super::VID_GOOGLE),
66            usb_pid.unwrap_or(super::PID_HYPERDEBUG),
67            usb_serial,
68        )?;
69        // HyperDebug device in operational mode, look at the USB strings for the running firmware
70        // version.
71        let config_desc = usb_backend.active_config_descriptor()?;
72        let current_firmware_version = if let Some(idx) = config_desc.description_string_index() {
73            usb_backend.read_string_descriptor_ascii(idx).ok()
74        } else {
75            None
76        };
77        Ok(Self {
78            usb_backend: RefCell::new(usb_backend),
79            current_firmware_version,
80            usb_vid: usb_vid.unwrap_or(super::VID_GOOGLE),
81            usb_pid: usb_pid.unwrap_or(super::PID_HYPERDEBUG),
82        })
83    }
84}
85
86/// The device does not support any part of the Transport trait, except the UpdateFirmware action.
87impl Transport for HyperdebugDfu {
88    fn capabilities(&self) -> Result<Capabilities> {
89        Ok(Capabilities::new(Capability::NONE))
90    }
91
92    fn dispatch(&self, action: &dyn Any) -> Result<Option<Box<dyn Annotate>>> {
93        if let Some(update_firmware_action) = action.downcast_ref::<UpdateFirmware>() {
94            update_firmware(
95                &mut self.usb_backend.borrow_mut(),
96                self.current_firmware_version.as_deref(),
97                &update_firmware_action.firmware,
98                update_firmware_action.progress.as_ref(),
99                update_firmware_action.force,
100                self.usb_vid,
101                self.usb_pid,
102            )
103        } else {
104            bail!(TransportError::UnsupportedOperation)
105        }
106    }
107}
108
109const USB_CLASS_APP: u8 = 0xFE;
110const USB_SUBCLASS_DFU: u8 = 0x01;
111
112const DFUSE_ERASE_PAGE: u8 = 0x41;
113const DFUSE_PROGRAM_PAGE: u8 = 0x21;
114
115const DFU_STATUS_OK: u8 = 0x00;
116
117const DFU_STATE_APP_IDLE: u8 = 0x00;
118const DFU_STATE_DFU_IDLE: u8 = 0x02;
119const DFU_STATE_DOWNLOAD_BUSY: u8 = 0x04;
120const DFU_STATE_DOWNLOAD_IDLE: u8 = 0x05;
121
122const USB_DFU_DETACH: u8 = 0;
123const USB_DFU_DNLOAD: u8 = 1;
124const USB_DFU_GETSTATUS: u8 = 3;
125
126#[cfg(not(feature = "include_hyperdebug_firmware"))]
127const OFFICIAL_FIRMWARE: Option<&'static [u8]> = None;
128#[cfg(feature = "include_hyperdebug_firmware")]
129const OFFICIAL_FIRMWARE: Option<&'static [u8]> = Some(include_bytes!(env!("hyperdebug_firmware")));
130
131pub fn official_firmware_version() -> Result<Option<&'static str>> {
132    if let Some(fw) = OFFICIAL_FIRMWARE {
133        Ok(Some(get_hyperdebug_firmware_version(fw)?))
134    } else {
135        Ok(None)
136    }
137}
138
139/// Helper method to verify that the given binary image looks like a HyperDebug firmware image.
140fn validate_firmware_image(firmware: &[u8]) -> Result<()> {
141    get_hyperdebug_firmware_version(firmware)?;
142    Ok(())
143}
144
145const EC_COOKIE: [u8; 4] = [0x99, 0x88, 0x77, 0xce];
146const EC_FIRMWARE_NAME_LEN: usize = 32;
147
148fn get_hyperdebug_firmware_version(firmware: &[u8]) -> Result<&str> {
149    let Some(pos) = firmware[0..1024]
150        .chunks(4)
151        .position(|c| c[0..4] == EC_COOKIE)
152    else {
153        bail!(TransportError::FirmwareProgramFailed(
154            "File is not a HyperDebug firmware image".to_string()
155        ));
156    };
157    let firmware_name_field = &firmware[(pos + 1) * 4..(pos + 1) * 4 + EC_FIRMWARE_NAME_LEN];
158    let end = firmware_name_field
159        .iter()
160        .rev()
161        .position(|b| *b != 0x00)
162        .map(|j| EC_FIRMWARE_NAME_LEN - j)
163        .unwrap_or(0);
164    Ok(std::str::from_utf8(&firmware_name_field[0..end])?)
165}
166
167/// Helper method to perform flash programming using ST's DfuSe variant of the DFU protocol.
168/// This method is used both by the `Hyperdebug` and the `HyperdebugDfu` structs.
169pub fn update_firmware(
170    usb_device: &mut UsbBackend,
171    current_firmware_version: Option<&str>,
172    firmware: &Option<Vec<u8>>,
173    progress: &dyn ProgressIndicator,
174    force: bool,
175    usb_vid: u16,
176    usb_pid: u16,
177) -> Result<Option<Box<dyn Annotate>>> {
178    let firmware: &[u8] = if let Some(vec) = firmware.as_ref() {
179        validate_firmware_image(vec)?;
180        vec
181    } else {
182        OFFICIAL_FIRMWARE.ok_or_else(|| anyhow!("No build-in firmware, use --filename"))?
183    };
184
185    if !force && let Some(current_version) = current_firmware_version {
186        let new_version = get_hyperdebug_firmware_version(firmware)?;
187        if new_version == current_version {
188            log::warn!(
189                "HyperDebug already running firmware version {}.  Consider --force.",
190                new_version,
191            );
192            return Ok(None);
193        }
194    }
195
196    let dfu_desc = scan_usb_descriptor(usb_device)?;
197
198    // Exclusively claim DFU interface, preparing for control requests.
199    usb_device.claim_interface(dfu_desc.dfu_interface)?;
200
201    if wait_for_idle(usb_device, dfu_desc.dfu_interface)? != DFU_STATE_APP_IDLE {
202        // Device is already running DFU bootloader, proceed to firmware transfer.
203        do_update_firmware(usb_device, dfu_desc, firmware, progress)?;
204        log::info!("Connecting to newly flashed firmware...");
205        if restablish_connection(usb_vid, usb_pid, usb_device.get_serial_number()).is_none() {
206            bail!(TransportError::FirmwareProgramFailed(
207                "Unable to establish connection after flashing.  Possibly bad image.".to_string()
208            ));
209        }
210        return Ok(None);
211    }
212
213    // Device is running the HyperDebug firmware, not DFU bootloader.  Ask for switch to
214    // bootloader, and then restablish USB connection.  Switching is expected to cause loss of USB
215    // connection, so we ignore any errors.
216    log::info!("Requesting switch to DFU mode...");
217    let _ = usb_device
218        .write_control(
219            rusb::request_type(
220                rusb::Direction::Out,
221                rusb::RequestType::Class,
222                rusb::Recipient::Interface,
223            ),
224            USB_DFU_DETACH,
225            1000,
226            dfu_desc.dfu_interface as u16,
227            &[],
228        )
229        .and_then(|_| wait_for_idle(usb_device, dfu_desc.dfu_interface));
230
231    // We get here most likely as a result of an `Err()` from the above block, as the device reset
232    // and disconnected from the USB bus.  Wait up to five seconds, repeatedly testing if the
233    // device can be found on the USB bus with the DID:VID of the STM DFU bootloader, but same
234    // serial number as before.
235    std::thread::sleep(std::time::Duration::from_millis(1000));
236    log::info!("Connecting to DFU bootloader...");
237    let Some(mut dfu_device) = restablish_connection(
238        VID_ST_MICROELECTRONICS,
239        PID_DFU_BOOTLOADER,
240        usb_device.get_serial_number(),
241    ) else {
242        bail!(TransportError::FirmwareProgramFailed(
243            "Unable to establish connection with DFU bootloader.".to_string()
244        ));
245    };
246    log::info!("Connected to DFU bootloader");
247
248    let dfu_desc = scan_usb_descriptor(&dfu_device)?;
249    dfu_device.claim_interface(dfu_desc.dfu_interface)?;
250    do_update_firmware(&dfu_device, dfu_desc, firmware, progress)?;
251    // The new firmware has been completely transferred, and the USB device is resetting and
252    // booting the new firmware.  Wait up to five seconds, repeatedly testing if the device can be
253    // found on the USB bus with the original DID:VID.
254    log::info!("Connecting to newly flashed firmware...");
255    if restablish_connection(usb_vid, usb_pid, usb_device.get_serial_number()).is_none() {
256        bail!(TransportError::FirmwareProgramFailed(
257            "Unable to establish connection after flashing.  Possibly bad image.".to_string()
258        ));
259    }
260    Ok(None)
261}
262
263fn restablish_connection(usb_vid: u16, usb_pid: u16, serial_number: &str) -> Option<UsbBackend> {
264    for _ in 0..10 {
265        std::thread::sleep(std::time::Duration::from_millis(500));
266        if let Ok(usb_backend) = UsbBackend::new(usb_vid, usb_pid, Some(serial_number)) {
267            return Some(usb_backend);
268        }
269    }
270    None
271}
272
273fn do_update_firmware(
274    usb_device: &UsbBackend,
275    dfu_desc: DfuDescriptor,
276    firmware: &[u8],
277    progress: &dyn ProgressIndicator,
278) -> Result<()> {
279    let DfuDescriptor {
280        dfu_interface,
281        xfer_size,
282        page_size,
283        flash_size,
284        base_address,
285    } = dfu_desc;
286
287    if page_size == 0 || flash_size != 0x80000 || xfer_size == 0 {
288        bail!(TransportError::UsbOpenError(
289            "Unrecognized DFU layout (not a Nucleo-L552ZE?)".to_string()
290        ));
291    }
292
293    log::info!("Erasing flash storage...");
294    let firmware_len = firmware.len() as u32;
295    progress.new_stage("Erasing", firmware_len as usize);
296    let mut bytes_erased: u32 = 0;
297    while bytes_erased < firmware_len {
298        let mut request = [0u8; 5];
299        request[0] = DFUSE_ERASE_PAGE;
300        request[1..5].copy_from_slice(&(base_address + bytes_erased).to_le_bytes());
301        usb_device.write_control(
302            rusb::request_type(
303                rusb::Direction::Out,
304                rusb::RequestType::Class,
305                rusb::Recipient::Interface,
306            ),
307            USB_DFU_DNLOAD,
308            0,
309            dfu_interface as u16,
310            &request,
311        )?;
312        wait_for_idle(usb_device, dfu_interface)?;
313        bytes_erased += page_size;
314        progress.progress(bytes_erased as usize);
315    }
316
317    log::info!("Programming flash storage...");
318    progress.new_stage("Writing", firmware_len as usize);
319    let mut bytes_sent: u32 = 0;
320    while bytes_sent < firmware_len {
321        let chunk_size = std::cmp::min(firmware_len - bytes_sent, xfer_size);
322
323        let mut request = [0u8; 5];
324        request[0] = DFUSE_PROGRAM_PAGE;
325        request[1..5].copy_from_slice(&(base_address + bytes_sent).to_le_bytes());
326        usb_device.write_control(
327            rusb::request_type(
328                rusb::Direction::Out,
329                rusb::RequestType::Class,
330                rusb::Recipient::Interface,
331            ),
332            USB_DFU_DNLOAD,
333            0,
334            dfu_interface as u16,
335            &request,
336        )?;
337        wait_for_idle(usb_device, dfu_interface)?;
338
339        usb_device.write_control(
340            rusb::request_type(
341                rusb::Direction::Out,
342                rusb::RequestType::Class,
343                rusb::Recipient::Interface,
344            ),
345            USB_DFU_DNLOAD,
346            2,
347            dfu_interface as u16,
348            &firmware[bytes_sent as usize..(bytes_sent + chunk_size) as usize],
349        )?;
350        wait_for_idle(usb_device, dfu_interface)?;
351        bytes_sent += chunk_size;
352        progress.progress(bytes_sent as usize);
353    }
354
355    // Request to leave DFU bootloader, and transfer control to newly flashed firmware.
356    usb_device.write_control(
357        rusb::request_type(
358            rusb::Direction::Out,
359            rusb::RequestType::Class,
360            rusb::Recipient::Interface,
361        ),
362        USB_DFU_DNLOAD,
363        0,
364        dfu_interface as u16,
365        &[],
366    )?;
367    // The device resetting will cause USB error here (STM32L5 devices do not execute the request
368    // to transfer control until queried for its status, so we have to query).
369    let _ = wait_for_idle(usb_device, dfu_interface);
370    Ok(())
371}
372
373struct DfuDescriptor {
374    dfu_interface: u8,
375    xfer_size: u32,
376    page_size: u32,
377    flash_size: u32,
378    base_address: u32,
379}
380
381/// Inspect USB interface descriptors, looking for DFU-related ones.
382fn scan_usb_descriptor(usb_device: &UsbBackend) -> Result<DfuDescriptor> {
383    let mut dfu_interface = 0;
384    let mut xfer_size = 0;
385    let mut page_size = 0;
386    let mut flash_size = 0;
387    let mut base_address = 0;
388
389    let config_desc = usb_device.active_config_descriptor()?;
390    for interface in config_desc.interfaces() {
391        for interface_desc in interface.descriptors() {
392            let idx = match interface_desc.description_string_index() {
393                Some(idx) => idx,
394                None => continue,
395            };
396            let interface_name = match usb_device.read_string_descriptor_ascii(idx) {
397                Ok(interface_name) => interface_name,
398                _ => continue,
399            };
400            if interface_desc.class_code() != USB_CLASS_APP
401                || interface_desc.sub_class_code() != USB_SUBCLASS_DFU
402                || (interface_desc.protocol_code() != 0x01
403                    && interface_desc.protocol_code() != 0x02)
404            {
405                continue;
406            }
407            dfu_interface = interface.number();
408            let extra_bytes = interface_desc.extra();
409            // Extra bytes contains inforation encoded according to DFU specification.
410            if extra_bytes.len() >= 9 {
411                xfer_size = extra_bytes[5] as u32 | (extra_bytes[6] as u32) << 8;
412            }
413            static DFU_SECTION_REGEX: LazyLock<Regex> = LazyLock::new(|| {
414                Regex::new("^@([^/]*)/0x([0-9a-fA-F]+)/([0-9]+)\\*([0-9]+)(..)").unwrap()
415            });
416            let Some(captures) = DFU_SECTION_REGEX.captures(&interface_name) else {
417                continue;
418            };
419            let section_name = captures.get(1).unwrap().as_str().trim();
420            if section_name != "Internal Flash" {
421                continue;
422            }
423            // We have the string describing the flash section (as opposed to fuses or
424            // once-programmable section), extract the relevant information.
425            base_address = u32::from_str_radix(captures.get(2).unwrap().as_str(), 16).unwrap();
426            let num_pages = captures.get(3).unwrap().as_str().parse::<u32>().unwrap();
427            page_size = captures.get(4).unwrap().as_str().parse::<u32>().unwrap();
428            let suffix = captures.get(5).unwrap().as_str();
429            if suffix.starts_with('K') {
430                page_size *= 1024;
431            }
432            flash_size = num_pages * page_size;
433        }
434    }
435    Ok(DfuDescriptor {
436        dfu_interface,
437        xfer_size,
438        page_size,
439        flash_size,
440        base_address,
441    })
442}
443
444/// Poll the bootloader using GETSTATUS request, until it leaves the "busy" state.
445fn wait_for_idle(dfu_device: &UsbBackend, dfu_interface: u8) -> Result<u8> {
446    loop {
447        let mut response = [0u8; 6];
448        let rc = dfu_device.read_control(
449            rusb::request_type(
450                rusb::Direction::In,
451                rusb::RequestType::Class,
452                rusb::Recipient::Interface,
453            ),
454            USB_DFU_GETSTATUS,
455            0,
456            dfu_interface as u16,
457            &mut response,
458        )?;
459        if rc != response.len() {
460            bail!(TransportError::FirmwareProgramFailed("".to_string()));
461        }
462        let command_status = response[0];
463        let minimum_delay_ms =
464            u64::from_le_bytes([response[1], response[2], response[3], 0, 0, 0, 0, 0]);
465        let device_state = response[4];
466        if command_status != DFU_STATUS_OK {
467            bail!(TransportError::FirmwareProgramFailed(format!(
468                "Unexpected DFU status {}",
469                response[0]
470            )));
471        }
472        if device_state == DFU_STATE_APP_IDLE
473            || device_state == DFU_STATE_DFU_IDLE
474            || device_state == DFU_STATE_DOWNLOAD_IDLE
475        {
476            return Ok(device_state);
477        } else if device_state == DFU_STATE_DOWNLOAD_BUSY {
478            std::thread::sleep(std::time::Duration::from_millis(minimum_delay_ms));
479        } else {
480            bail!(TransportError::FirmwareProgramFailed(format!(
481                "Unexpected DFU state {}",
482                response[4]
483            )));
484        }
485    }
486}