Skip to content

Porting

Run the driver on a new MCU by writing one class: the I2C interface.

The driver never calls your SDK directly. It calls six methods on a class you derive from se050::I2cTransceiveInterface<Self> (in inc/se050_i2c_transport_interface.hpp). The base class uses CRTP: the calls are resolved at compile time, so there are no virtual functions, no vtable and nothing to allocate.

Layer diagram: the application calls the hardware-agnostic SE050 Device and its commands, which go through the T=1 session and the optional SCP03 channel, then the CRTP I2C interface, which calls the platform port you write, which drives the I2C bus and the ENA pin of the SE050.Layer diagram: the application calls the hardware-agnostic SE050 Device and its commands, which go through the T=1 session and the optional SCP03 channel, then the CRTP I2C interface, which calls the platform port you write, which drives the I2C bus and the ENA pin of the SE050.
Three layers, one of them yours
Figure description and sources

From top to bottom. Your application talks to one se050::Device object, whose public commands are listed from inc/se050_device.hpp. Each command builds an APDU, which the T=1 session (inc/se050_t1_session.hpp) frames into blocks with a CRC-16 EDC, chains and acknowledges; when an SCP03 session is open the APDU is wrapped first. The communication interface (se050::I2cTransceiveInterface in inc/se050_i2c_transport_interface.hpp) is a CRTP base. The platform port is the only code you write: the methods listed from that header's requirements. Below it is the SE050 on I2C.

Drawn from: inc/se050_device.hpp Device commands; inc/se050_t1_session.hpp T1Session; inc/se050_i2c_transport_interface.hpp Derived requirements; figure module docs/diagrams/figures/driver-layers.py.

What you implement

Method Contract
bool EnsureInitialized() Return true once the bus is ready. Called before every exchange: make it cheap after the first call
Error I2cWrite(tx, tx_len, timeout_ms) One I2C write of exactly tx_len bytes to the SE050's address: START, address + W, the bytes, STOP. tx_len is never 0
Error I2cRead(rx, rx_len, timeout_ms) One I2C read of exactly rx_len bytes. Return Transport or Timeout on a NACK: the driver retries those two
Error Transceive(tx, tx_len, rx, rx_cap, rx_len_out, timeout_ms) A write then a read, for Device::TransceiveRaw() during bring-up only; T=1 does not use it
Error HardwareReset() Power-cycle the SE050 (ENA low, then high) if your board can; return Ok if it cannot
void delay_ms_impl(ms) Block for at least ms milliseconds (an RTOS delay is fine)

All methods are noexcept. Return Error::Ok on success; any other value aborts the exchange (after retries, for reads).

The port

my_i2c.hpp
class MyI2c : public se050::I2cTransceiveInterface<MyI2c> {
public:
  explicit MyI2c(std::uint8_t addr7 = se050::kDefaultI2cAddress7, int ena_gpio = -1) noexcept
      : addr7_(addr7), ena_gpio_(ena_gpio) {}

  // Bus and pins are set up by your board code; report whether they are ready.
  bool EnsureInitialized() noexcept {
    return true;
  }

  // One complete T=1 block in one I2C write (START, address + W, bytes, STOP). Never zero-length.
  se050::Error I2cWrite(const std::uint8_t* tx, std::size_t tx_len,
                        std::uint32_t timeout_ms) noexcept {
    if (tx == nullptr || tx_len == 0U) {
      return se050::Error::InvalidArgument;
    }
    return platform_i2c_write(addr7_, tx, tx_len, timeout_ms) ? se050::Error::Ok
                                                              : se050::Error::Transport;
  }

  // Exactly rx_len bytes in one I2C read. A NACK (the SE050 is still busy) is Transport: the driver
  // retries.
  se050::Error I2cRead(std::uint8_t* rx, std::size_t rx_len, std::uint32_t timeout_ms) noexcept {
    if (rx == nullptr || rx_len == 0U) {
      return se050::Error::InvalidArgument;
    }
    return platform_i2c_read(addr7_, rx, rx_len, timeout_ms) ? se050::Error::Ok
                                                             : se050::Error::Transport;
  }

  // Bring-up helper only (Device::TransceiveRaw); T=1 uses I2cWrite / I2cRead.
  se050::Error Transceive(const std::uint8_t* tx, std::size_t tx_len, std::uint8_t* rx,
                          std::size_t rx_cap, std::size_t* rx_len_out,
                          std::uint32_t timeout_ms) noexcept {
    *rx_len_out = 0;
    se050::Error e = I2cWrite(tx, tx_len, timeout_ms);
    if (e != se050::Error::Ok || rx == nullptr || rx_cap == 0U) {
      return e;
    }
    delay_ms_impl(2);
    e = I2cRead(rx, rx_cap, timeout_ms);
    if (e == se050::Error::Ok) {
      *rx_len_out = rx_cap;
    }
    return e;
  }

  // Power-cycle through ENA (deep power-down while low). Return Ok when ENA is not wired.
  se050::Error HardwareReset() noexcept {
    if (ena_gpio_ < 0) {
      return se050::Error::Ok;
    }
    platform_gpio_write(ena_gpio_, false);
    delay_ms_impl(2);
    platform_gpio_write(ena_gpio_, true);
    delay_ms_impl(10);
    return se050::Error::Ok;
  }

  void delay_ms_impl(std::uint32_t ms) noexcept {
    platform_delay_ms(ms);
  }

private:
  std::uint8_t addr7_;
  int ena_gpio_;
};

Rules the transfers must follow:

  • One block, one write. The SE050 expects the whole T=1 block in one I2C transaction. Never split it, never add a register address in front of it.
  • No zero-length writes, no bus scans. An address-only frame can hang the SE050 (erratum I2C.2, see Hardware).
  • Report NACKs as Transport or Timeout. The SE050 NACKs reads while it works; the driver polls through them. Other values are not retried.
  • No delays of your own. The driver waits the inter-frame delay and the retry delay through delay_ms_impl().
  • Pull-ups and idle-high lines before the SE050 powers up (Hardware).

Reference implementation

The ESP32 examples use hf_se050_esp_i2c.hpp: an ESP-IDF v5 i2c_master port with an optional ENA GPIO and FreeRTOS delays. Start from it for any ESP-IDF target.

On HardFOC boards the SE050 is used through Se050Handler in hf-core: it implements this interface over the HAL's BaseI2c and an optional BaseGpio for the reset line (wire it to ENA), adds a mutex and owns the Device. Build it with HF_CORE_ENABLE_SE050.

Check your port

  1. Build it: the port on this page is compiled by the host CI from tests/docs/, with every driver class instantiated over it.
  2. Run ChipWarmReset() and GetAnswerToReset(): both succeeding proves writes, reads, NACK handling and the CRC.
  3. SelectDefaultIoTApplet() answering 90 00, then GetRandom(32, ...): APDUs work end to end.
  4. Run the se050_smoke_example logic on your board: version, random and free memory.

Threads

The driver is not thread-safe, and the SE050 must not see a second command before the first response has been read (erratum I2C.1). Give each SE050 one owner task, or guard every call on a device object with one mutex. Other devices on the same I2C bus are fine as long as your bus driver serialises transfers.