rivet_bsp_support/stellaris_i2c.rs
1//! Stellaris/LM3S6965 I2C Master driver: `embedded_hal::i2c::I2c`
2//! (polling) and `embedded_hal_async::i2c::I2c`
3//! (completion via a real interrupt through [`rivet::sync::Signal`]) on
4//! the same struct — only `transaction()` is a required method on
5//! either trait, `read`/`write`/`write_read` come free as default
6//! methods that call it.
7//!
8//! This is the CI reference for async I2C, run against a real
9//! `at24c-eeprom` QEMU device on `lm3s6965evb` (`stellaris-i2c` at
10//! `0x4002_0000`, NVIC IRQ 8). Three real quirks in QEMU's model of this
11//! peripheral (confirmed directly against `hw/i2c/bitbang.c`/
12//! `hw/arm/stellaris.c`'s `stellaris_i2c` device on this machine's QEMU
13//! 8.2.2, not assumed from the datasheet) shape this driver:
14//!
15//! 1. **Address-NAK raises no interrupt.** The model sets `MCS.ERROR`
16//! (and halts the transfer) *before* it would set `MRIS` — so a
17//! failed transaction never fires the ISR. An await-only design would
18//! hang forever on a NAK. This driver checks `MCS` synchronously
19//! immediately after every command, before ever calling
20//! [`Signal::wait`] — since this model also completes successful
21//! transfers instantaneously inside the MMIO store (the ISR has
22//! *already* fired by the time the next instruction runs), that same
23//! synchronous check doubles as the fast path for success too; `wait`
24//! is the fallback for real hardware, where the transfer is still
25//! genuinely in flight at that point.
26//! 2. **`MIMR` can never be re-masked once set** — writing *any* value
27//! enables the interrupt permanently in this model. The ISR
28//! ([`isr_ack`]) clears the condition via `MICR` (which the real
29//! hardware protocol requires anyway), never by touching `MIMR`.
30//! 3. **Repeated START is broken** — `start_transfer` is skipped when
31//! `BUSBSY` is already set, so a write-then-repeated-start-read never
32//! changes bus direction. This driver issues a real STOP after a
33//! `Write` operation that isn't the transaction's last, then a fresh
34//! START for the next operation, instead of a true repeated start.
35//! The AT24C EEPROM's internal address pointer persists across a
36//! STOP/START the same way it would across a repeated START, so the
37//! read-back this is tested against still succeeds — this is a real,
38//! documented divergence from the I2C spec's repeated-start contract,
39//! not something to rely on against a stricter real slave device.
40
41use embedded_hal::i2c::{Operation, SevenBitAddress};
42use rivet::sync::Signal;
43
44const I2CMSA: usize = 0x000;
45const I2CMCS: usize = 0x004;
46const I2CMDR: usize = 0x008;
47const I2CMTPR: usize = 0x00C;
48const I2CMIMR: usize = 0x010;
49const I2CMICR: usize = 0x01C;
50const I2CMCR: usize = 0x020;
51
52const CS_RUN: u32 = 1 << 0;
53const CS_START: u32 = 1 << 1;
54const CS_STOP: u32 = 1 << 2;
55const CS_ACK: u32 = 1 << 3;
56const CS_BUSY: u32 = 1 << 0;
57const CS_ERROR: u32 = 1 << 1;
58
59const CR_MFE: u32 = 1 << 4;
60const IMR_IM: u32 = 1 << 0;
61const ICR_IC: u32 = 1 << 0;
62
63/// A single failed transaction: the slave NAK'd an address or data byte,
64/// or the controller lost arbitration. This driver doesn't distinguish
65/// which — every `MCS.ERROR` case maps here.
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub struct Nak;
68
69impl embedded_hal::i2c::Error for Nak {
70 fn kind(&self) -> embedded_hal::i2c::ErrorKind {
71 embedded_hal::i2c::ErrorKind::NoAcknowledge(
72 embedded_hal::i2c::NoAcknowledgeSource::Unknown,
73 )
74 }
75}
76
77/// One physical Stellaris I2C Master controller: a fixed register base
78/// and the [`Signal`] its interrupt handler completes.
79///
80/// Construct via [`stellaris_i2c_instance!`], not directly — see
81/// `rivet_bsp_support::pl022::Pl022::new`'s doc for why a bare `fn
82/// isr<const BASE: usize>()` with an inline `static Signal` wouldn't work
83/// (statics inside a generic function aren't monomorphized per
84/// instantiation).
85pub struct StellarisI2c {
86 base: usize,
87 sig: &'static Signal,
88}
89
90impl StellarisI2c {
91 /// # Safety
92 /// `base` must be a real, exclusively-owned Stellaris I2C Master
93 /// register block, and `sig` must be the exact [`Signal`] the ISR
94 /// registered for this instance calls [`Signal::signal`] on.
95 pub const unsafe fn new(base: usize, sig: &'static Signal) -> Self {
96 Self { base, sig }
97 }
98
99 fn reg(&self, offset: usize) -> *mut u32 {
100 (self.base + offset) as *mut u32
101 }
102
103 /// Bring the controller up: master function enable, a conservative
104 /// timer-period divisor (untuned — this model's transfers are
105 /// instantaneous regardless; real hardware picks its own for the
106 /// target bus speed), and unmask the interrupt once up front (see
107 /// module docs — this model can never re-mask it anyway).
108 pub fn init(&self) {
109 // SAFETY: fixed Stellaris I2C registers, exclusively owned per
110 // the constructor's contract.
111 unsafe {
112 core::ptr::write_volatile(self.reg(I2CMCR), CR_MFE);
113 core::ptr::write_volatile(self.reg(I2CMTPR), 7);
114 core::ptr::write_volatile(self.reg(I2CMIMR), IMR_IM);
115 }
116 }
117
118 fn set_address(&self, address: u8, read: bool) {
119 // SAFETY: as in `init`.
120 unsafe {
121 let val = ((address as u32) << 1) | u32::from(read);
122 core::ptr::write_volatile(self.reg(I2CMSA), val);
123 }
124 }
125
126 fn mcs(&self) -> u32 {
127 // SAFETY: as in `init`.
128 unsafe { core::ptr::read_volatile(self.reg(I2CMCS)) }
129 }
130
131 fn write_dr(&self, byte: u8) {
132 // SAFETY: as in `init`.
133 unsafe { core::ptr::write_volatile(self.reg(I2CMDR), byte as u32) };
134 }
135
136 fn read_dr(&self) -> u8 {
137 // SAFETY: as in `init`.
138 unsafe { core::ptr::read_volatile(self.reg(I2CMDR)) as u8 }
139 }
140
141 fn run_sync(&self, cmd: u32) -> Result<(), Nak> {
142 // SAFETY: as in `init`. Real hardware genuinely needs this poll
143 // (the model just resolves it instantaneously).
144 unsafe { core::ptr::write_volatile(self.reg(I2CMCS), cmd) };
145 while self.mcs() & CS_BUSY != 0 {
146 core::hint::spin_loop();
147 }
148 if self.mcs() & CS_ERROR != 0 {
149 Err(Nak)
150 } else {
151 Ok(())
152 }
153 }
154
155 /// See module docs item 1: this model resolves both success and
156 /// failure synchronously inside the `MCS` write, so the immediate
157 /// post-write check below is what actually observes the outcome in
158 /// practice; `Signal::wait` is the correct fallback for real
159 /// hardware, where the transfer may still be genuinely in flight.
160 async fn run_async(&self, cmd: u32) -> Result<(), Nak> {
161 self.sig.reset();
162 // SAFETY: as in `init`.
163 unsafe { core::ptr::write_volatile(self.reg(I2CMCS), cmd) };
164 if self.mcs() & CS_BUSY == 0 {
165 let status = self.mcs();
166 // The ISR may have already fired (success case) with
167 // nothing left registered to wake — consume the latch so it
168 // doesn't leak into the next command.
169 self.sig.try_take();
170 return if status & CS_ERROR != 0 { Err(Nak) } else { Ok(()) };
171 }
172 self.sig.wait().await;
173 if self.mcs() & CS_ERROR != 0 {
174 Err(Nak)
175 } else {
176 Ok(())
177 }
178 }
179}
180
181/// Shared ISR body for every Stellaris I2C instance, called from the
182/// `fn()` [`stellaris_i2c_instance!`] generates. Clears the interrupt
183/// condition via `MICR` (module docs item 2 — `MIMR` can't be
184/// re-masked in this model) and hands off via `Signal`.
185pub fn isr_ack(base: usize, sig: &Signal) {
186 // SAFETY: `base` is a real Stellaris I2C register block passed by
187 // the `stellaris_i2c_instance!` caller, who owns it exclusively.
188 unsafe {
189 core::ptr::write_volatile((base + I2CMICR) as *mut u32, ICR_IC);
190 }
191 sig.signal();
192}
193
194/// Declares a `static Signal` and a named `fn()` ISR bound to it, for
195/// one physical Stellaris I2C instance's completion interrupt. See
196/// [`StellarisI2c::new`] for why this can't just be a generic function
197/// with an inline static.
198#[macro_export]
199macro_rules! stellaris_i2c_instance {
200 ($sig_name:ident, $isr_name:ident, base = $base:expr) => {
201 static $sig_name: ::rivet::sync::Signal = ::rivet::sync::Signal::new();
202
203 fn $isr_name() {
204 $crate::stellaris_i2c::isr_ack($base, &$sig_name);
205 }
206 };
207}
208
209/// Runs `operations` against `address`, generic over the sync/async byte
210/// transfer primitive (`run_sync`/`run_async`, threaded through as a
211/// plain function pointer's worth of inlined logic via the two thin
212/// trait impls below — kept as one shared function so the STOP/START
213/// bookkeeping (module docs item 3) exists in exactly one place).
214macro_rules! impl_transaction {
215 ($self:ident, $address:ident, $operations:ident, $run:ident $(. $await_kw:ident)?) => {{
216 let op_count = $operations.len();
217 for (i, op) in $operations.iter_mut().enumerate() {
218 let is_last_op = i + 1 == op_count;
219 match op {
220 Operation::Write(bytes) => {
221 $self.set_address($address, false);
222 let n = bytes.len();
223 for (j, &b) in bytes.iter().enumerate() {
224 $self.write_dr(b);
225 let is_first = j == 0;
226 let is_last = j + 1 == n;
227 let mut cmd = CS_RUN;
228 if is_first {
229 cmd |= CS_START;
230 }
231 if is_last {
232 // STOP after every Write's last byte — even
233 // when another operation follows — since
234 // repeated START is broken in this model
235 // (item 3); the next operation issues a
236 // fresh START instead of relying on one.
237 cmd |= CS_STOP;
238 }
239 $self.$run(cmd)$(.$await_kw)??;
240 }
241 }
242 Operation::Read(bytes) => {
243 $self.set_address($address, true);
244 let n = bytes.len();
245 for (j, slot) in bytes.iter_mut().enumerate() {
246 let is_first = j == 0;
247 let is_last = j + 1 == n;
248 let mut cmd = CS_RUN;
249 if is_first {
250 cmd |= CS_START;
251 }
252 if is_last {
253 cmd |= CS_STOP;
254 } else {
255 cmd |= CS_ACK;
256 }
257 $self.$run(cmd)$(.$await_kw)??;
258 *slot = $self.read_dr();
259 }
260 }
261 }
262 let _ = is_last_op;
263 }
264 Ok(())
265 }};
266}
267
268impl embedded_hal::i2c::ErrorType for StellarisI2c {
269 type Error = Nak;
270}
271
272impl embedded_hal::i2c::I2c<SevenBitAddress> for StellarisI2c {
273 fn transaction(
274 &mut self,
275 address: SevenBitAddress,
276 operations: &mut [Operation<'_>],
277 ) -> Result<(), Self::Error> {
278 impl_transaction!(self, address, operations, run_sync)
279 }
280}
281
282impl embedded_hal_async::i2c::I2c<SevenBitAddress> for StellarisI2c {
283 async fn transaction(
284 &mut self,
285 address: SevenBitAddress,
286 operations: &mut [Operation<'_>],
287 ) -> Result<(), Self::Error> {
288 impl_transaction!(self, address, operations, run_async.await)
289 }
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295
296 // Compile-only: proves `StellarisI2c` is usable through generic
297 // embedded-hal/-async code, not just directly, for both trait
298 // families on the same struct.
299 #[allow(dead_code)]
300 fn generic_sync<I: embedded_hal::i2c::I2c>(i2c: &mut I, addr: u8, buf: &mut [u8]) {
301 let _ = i2c.read(addr, buf);
302 }
303
304 #[allow(dead_code)]
305 async fn generic_async<I: embedded_hal_async::i2c::I2c>(i2c: &mut I, addr: u8, buf: &mut [u8]) {
306 let _ = i2c.read(addr, buf).await;
307 }
308
309 #[test]
310 fn type_checks() {
311 let _ = generic_sync::<StellarisI2c>;
312 let _ = generic_async::<StellarisI2c>;
313 }
314}