Skip to main content

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}