Scaffolding the crate
This driver is an ordinary Rust library crate. It needs no board support package, cross toolchain, or custom linker setup.
The recommended starting point is
drive-rs, a
cargo-generate
template that scaffolds a no_std, embedded-hal driver crate on top
of the device-driver toolkit.
After three prompts, it generates a compiling crate with a register map,
a transport layer, a high-level API, hardware-free mock tests, and a
Pico de Gallo example. No TMP102-specific code is required yet.
To assemble the crate by hand, skip to starting from an empty crate at the end of this chapter.
Prerequisites
Install cargo-generate to run the template. The templateβs
post-generation hook also uses the ddc compiler from
device-driver-cli to turn the register manifest into Rust:
$ cargo install cargo-generate
$ cargo install device-driver-cli
Running the template
$ cargo generate --git https://github.com/OpenDevicePartnership/drive-rs template --allow-commands
The template asks three questions:
- Enter the project name in kebab-case. It becomes the crate name, and its PascalCase form becomes the driver struct name.
- Select one or more interfaces from
gpio,i2c,spi, anduart. - Choose
sync,async, orboth. The answer is resolved at generation time, not behind a Cargo feature:syncemits only the blocking surface,asynconly the async one, andbothemits the two side by side.
TMP102 is a plain I2C part with no discrete control lines, so
select i2c as the only interface. Select both to support blocking and
async users:
π€· Project Name: tmp102
π€· Which bus/interface(s) does the device use? [i2c]
π€· Generate blocking, async, or both APIs? both
Generated files
tmp102/
βββ Cargo.toml # embedded-hal + embedded-hal-async + device-driver runtime
βββ README.md
βββ device.ddsl # device-driver manifest, the source of truth
βββ src/
β βββ lib.rs # #![no_std] crate root
β βββ error.rs # generic Error<E> that preserves the underlying bus error
β βββ registers.rs # generated from device.ddsl by ddc (don't edit by hand)
β βββ interface.rs # I2cInterface: bridges embedded-hal to device-driver
β βββ driver.rs # the high-level Tmp102 type and its constructors
βββ tests/
β βββ integration.rs # mock-based tests, no hardware required
βββ examples/
βββ pico.rs # runs against real hardware over a Pico de Gallo bridge
Because this configuration selects only i2c, the template does not
emit code for gpio, spi, or uart. It omits unused interfaces
instead of placing them behind feature flags.
The generated Cargo.toml contains the driver dependencies and the
host-side test harness:
[package]
name = "tmp102"
version = "0.1.0"
edition = "2024"
rust-version = "1.94"
license = "MIT OR Apache-2.0"
[dependencies]
embedded-hal = "1.0"
embedded-hal-async = "1.0"
device-driver = { version = "2.1.0", default-features = false }
defmt = { version = "1", optional = true }
[dev-dependencies]
pico-de-gallo-hal = { version = "0.7", default-features = false }
tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread"] }
[features]
default = []
defmt = ["dep:defmt", "device-driver/defmt"]
embedded-hal provides the blocking driver surface, while
embedded-hal-async provides the async API. Both are ordinary,
non-optional dependencies: the both answer was already resolved when
the crate was generated, so there is no async feature to turn on or
off. Answering sync would have emitted neither embedded-hal-async
nor any async code at all. device-driver supplies the generated
register accessors. The optional defmt dependency supports logging on
the target.
The development dependencies serve the host-side code:
pico-de-gallo-hal lets tests and examples communicate with real
hardware, and tokio runs async examples and hardware-in-the-loop
tests.
device-driver uses default-features = false.
The register map is compiled ahead of time by ddc, so the crate only
needs the runtime. Users do not compile its procedural macro or manifest
parser.
Tip
Keep
pico-de-gallo-halin[dev-dependencies], not in normal[dependencies]. Your end users should depend on your driver crate, not on the host-side test harness you used while writing it.
Build checks
The generated crate compiles and passes its tests before it contains any TMP102-specific code:
$ cd tmp102
$ cargo test
$ cargo clippy -- -D warnings
Itβs no_std, so it builds for a bare-metal target too:
$ rustup target add thumbv7em-none-eabihf
$ cargo build --target thumbv7em-none-eabihf
Next, replace the templateβs placeholder register map with the one from the TMP102 datasheet, then build the driver API on top of it.
device.ddsl is the single source of truth for that map. It is written
in DDSL, device-driverβs own specification language, and it names the
generated device itself. There is no separate device-name flag:
device Tmp102Registers {
default-byte-order: LE,
register-address-type: u8,
...
}
After editing it to match the datasheet, regenerate src/registers.rs,
which is committed to the crate:
$ ddc build rust -s device.ddsl -o src/registers.rs --rust-defmt-feature=defmt
Optional: starting from an empty crate
The template is optional. To add each piece by hand, start with a fresh library:
$ cargo new --lib tmp102
Creating library `tmp102` package
$ cd tmp102
Then add the dependencies we know we will need:
$ cargo add embedded-hal
$ cargo add embedded-hal-async
$ cargo add device-driver --no-default-features
$ cargo add --dev pico-de-gallo-hal
$ cargo add --dev tokio -F rt-multi-thread,time,macros
$ cargo install device-driver-cli
This produces the same dependency set as the generated crate, but leaves
src/lib.rs empty. It does not create error.rs, interface.rs,
driver.rs, mock tests, or examples/pico.rs. The following chapters
write those files from scratch, so this route involves more typing and
teaches more of the process.
Both routes produce a library-first, embedded-hal-based driver with an
async API and host-side hardware testing. The next chapter describes the
TMP102 registers in the format the generator accepts.