tele0592 1.2.0

Control an alternate firmware for the DFR0592 DC motor driver hat
Documentation
//! High-level interface with the DC motor driver hat firmware

use core::slice;
#[cfg(feature = "controller")]
use core::time::Duration;
use low_level::{
    CMD_DEVICE_FAMILY, CMD_FIRMWARE_VERSION, CMD_FLASH_SIZE, CMD_MCU_IDCODE,
    CMD_REBOOT_TO_BOOTLOADER, CMD_RESET, CMD_WHO_AM_I,
};

const I2C_ADDR: u8 = 0x57;

#[derive(Debug)]
pub enum Error<I2cError> {
    I2c(I2cError),
    #[cfg(feature = "controller")]
    InvalidFrequency(u32),
    #[cfg(feature = "controller")]
    InvalidDuration(Duration),
    #[cfg(feature = "controller")]
    InvalidRawSpeed,
    #[cfg(feature = "controller")]
    InvalidVersion((u8, u8, u8)),
    #[cfg(feature = "controller")]
    UnknownFirmwareFound,
}

impl<I2CError: core::fmt::Display> core::fmt::Display for Error<I2CError> {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        match self {
            Error::I2c(error) => write!(f, "I2C error: {error}"),
            #[cfg(feature = "controller")]
            Error::InvalidFrequency(freq) => write!(f, "invalid frequency {freq} Hz"),
            #[cfg(feature = "controller")]
            Error::InvalidDuration(d) => write!(f, "invalid duration {d:?}"),
            #[cfg(feature = "controller")]
            Error::InvalidRawSpeed => write!(f, "invalid raw speed -128"),
            #[cfg(feature = "controller")]
            Error::InvalidVersion((major, minor, patch)) => {
                let (req_major, req_minor, req_patch) =
                    crate::controller::REQUIRED_FIRMWARE_VERSION;
                write!(
                    f,
                    "invalid firmware version {major}.{minor}.{patch} (expected {req_major}.{req_minor}.{req_patch})",
                )
            }
            #[cfg(feature = "controller")]
            Error::UnknownFirmwareFound => write!(f, "unknown program found on controller"),
        }
    }
}

impl<I2CError: core::error::Error> core::error::Error for Error<I2CError> {}

pub type Result<T, I2cError> = core::result::Result<T, Error<I2cError>>;

/// The `Device` trait lets us interact with the board and contains methods
/// available both in controller and in bootloader mode.
pub trait Device {
    type I2cError;

    /// Write arbitrary data to the board.
    ///
    /// # Errors
    /// This method may fail if the communication with the board fails.
    fn write(&mut self, buf: impl AsRef<[u8]>) -> Result<(), Self::I2cError>;

    /// Exchange data with the board using a repeated start I²C event.
    ///
    /// # Errors
    /// This method may fail if the communication with the board fails.
    fn write_read(
        &mut self,
        write: impl AsRef<[u8]>,
        read: impl AsMut<[u8]>,
    ) -> Result<(), Self::I2cError>;

    /// Retrieve the firmware version as a (major, minor, patch) tuple.
    ///
    /// # Errors
    /// This method may fail if the communication with the board fails.
    fn firmware_version(&mut self) -> Result<(u8, u8, u8), Self::I2cError> {
        let mut result = [0; 3];
        self.write_read([CMD_FIRMWARE_VERSION], &mut result)?;
        Ok(result.into())
    }

    /// Identify the program currently running on the board as the
    /// controller, the bootloader, or an unknown program.
    ///
    /// # Errors
    /// This method may fail if the communication with the board fails.
    fn who_am_i(&mut self) -> Result<WhoAmI, Self::I2cError> {
        let mut me: u8 = 0;
        self.write_read([CMD_WHO_AM_I], slice::from_mut(&mut me))?;
        Ok(match me {
            addr if addr == I2C_ADDR | 0x80 => WhoAmI::Bootloader,
            addr if addr == I2C_ADDR => WhoAmI::Controller,
            addr => WhoAmI::Other(addr),
        })
    }

    /// Reset the board.
    /// After issuing this command, some grace period must be respected
    /// in order to let the board restart.
    ///
    /// # Errors
    /// This method may fail if the communication with the board fails.
    fn reset(&mut self) -> Result<(), Self::I2cError> {
        self.write([CMD_RESET])
    }

    /// Return the identity code and continuation code of the microcontroller
    /// running on the board if available.
    ///
    /// The [`brand_name`] function might help further identifying the
    /// microcontroller.
    ///
    /// # Errors
    /// This method may fail if the communication with the board fails.
    fn device_family(&mut self) -> Result<Option<(u8, u8)>, Self::I2cError> {
        let mut data = [0; 2];
        self.write_read([CMD_DEVICE_FAMILY], &mut data)?;
        Ok((data != [0, 0]).then_some((data[0], data[1])))
    }

    /// Return the device id and revision id from the `IDCODE` field of the
    /// `DBGMCU` register of the microcontroller if available.
    ///
    /// Note that some microcontrollers require that the JTAG or SWD interface
    /// has been used in order for this information to be available.
    ///
    /// This information may be further analyzed using the
    /// [`mcu_kind`] function.
    ///
    /// # Errors
    /// This method may fail if the communication with the board fails.
    fn mcu_kind(&mut self) -> Result<Option<(u16, u16)>, Self::I2cError> {
        let mut data = [0; 4];
        self.write_read([CMD_MCU_IDCODE], &mut data)?;
        Ok((data != [0; 4]).then_some((
            u16::from_le_bytes([data[0], data[1]]),
            u16::from_le_bytes([data[2], data[3]]),
        )))
    }

    /// Retrieve the flash size, in KiB, from the microcontroller
    /// present on the board.
    ///
    /// # Errors
    /// This method may fail if the communication with the board fails.
    fn flash_size(&mut self) -> Result<u16, Self::I2cError> {
        let mut data = [0; 2];
        self.write_read([CMD_FLASH_SIZE], &mut data)?;
        Ok(u16::from_le_bytes([data[0], data[1]]))
    }

    /// Force the currently running program to switch to bootloader mode
    /// even when an application is present.
    ///
    /// # Errors
    /// This method may fail if the communication with the board fails.
    fn switch_to_bootloader(&mut self) -> Result<(), Self::I2cError> {
        self.write([CMD_REBOOT_TO_BOOTLOADER])
    }
}

impl<I2c: embedded_hal::i2c::I2c> Device for I2c {
    type I2cError = I2c::Error;

    fn write(&mut self, buf: impl AsRef<[u8]>) -> Result<(), Self::I2cError> {
        (self as &mut I2c)
            .write(I2C_ADDR, buf.as_ref())
            .map_err(Error::I2c)
    }

    fn write_read(
        &mut self,
        write: impl AsRef<[u8]>,
        mut read: impl AsMut<[u8]>,
    ) -> Result<(), Self::I2cError> {
        (self as &mut I2c)
            .write_read(I2C_ADDR, write.as_ref(), read.as_mut())
            .map_err(Error::I2c)
    }
}

/// Currently running program on the board.
pub enum WhoAmI {
    Bootloader,
    Controller,
    Other(u8),
}

/// Decode identity code and continuation code into a brand name.
#[must_use]
pub fn brand_name(identity_code: u8, continuation_code: u8) -> Option<&'static str> {
    match (identity_code, continuation_code) {
        (0x20, 0x00) => Some("STMicroelectronics"),
        (0x3b, 0x04) => Some("ARM generic (APM32?)"),
        _ => None,
    }
}

/// Decode device id and revision id obtained from the IDCODE field of the DBGMCU register
/// into a density information, and a model variant.
#[must_use]
pub fn mcu_kind(dev_id: u16, rev_id: u16) -> (&'static str, &'static str) {
    match (dev_id, rev_id) {
        (0x412, 0x1000) => ("low-density", "A"),
        (0x412, _) => ("low-density", "?"),
        (0x410, 0x0000) => ("medium-density", "A"),
        (0x410, 0x2000) => ("medium-density", "B"),
        (0x410, 0x2001) => ("medium-density", "Z"),
        (0x410, 0x2003) => ("medium-density", "1/2/3/X/Y"),
        (0x410, _) => ("medium-density", "?"),
        (0x414, 0x1000) => ("high-density", "1/A"),
        (0x414, 0x1001) => ("high-density", "Z"),
        (0x414, 0x1003) => ("high-density", "1/2/3/X/Y"),
        (0x414, _) => ("high-density", "?"),
        (0x430, 0x1000) => ("XL-density", "1/A"),
        (0x430, _) => ("XL-density", "?"),
        (0x418, 0x1000) => ("connectivity", "A"),
        (0x418, 0x1001) => ("connectivity", "Z"),
        (0x418, _) => ("connectivity", "?"),
        _ => ("?", "?"),
    }
}

// Constants used in the low-level protocol between the host and the
// controller or bootloader firmware.
//
// *Note: this module requires that the `low-level` feature is
// selected.*
pub(crate) mod low_level {
    pub const CMD_FIRMWARE_VERSION: u8 = 0x08;
    pub const CMD_WHO_AM_I: u8 = 0x0f;
    pub const CMD_RESET: u8 = 0xe0;
    pub const CMD_REBOOT_TO_BOOTLOADER: u8 = 0xe1;
    #[cfg(feature = "low-level")]
    pub const CMD_DEVICE_ID: u8 = 0xf0;
    pub const CMD_DEVICE_FAMILY: u8 = 0xf1;
    pub const CMD_MCU_IDCODE: u8 = 0xf2;
    pub const CMD_FLASH_SIZE: u8 = 0xf3;
}