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.
Figure description and sources
From top to bottom. Your application talks to one se050::Device
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¶
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
TransportorTimeout. 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¶
- Build it: the port on this page is compiled by the host CI from
tests/docs/, with every driver class instantiated over it. - Run
ChipWarmReset()andGetAnswerToReset(): both succeeding proves writes, reads, NACK handling and the CRC. SelectDefaultIoTApplet()answering90 00, thenGetRandom(32, ...): APDUs work end to end.- Run the
se050_smoke_examplelogic 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.