Skip to main content

lager/nets/
spi.rs

1//! SPI bus nets.
2//!
3//! Bus transactions run box-side under the physical device's shared lock,
4//! so a full transfer can never interleave with a concurrent request on the
5//! same device (e.g. parallel cargo tests).
6
7use serde::Deserialize;
8use serde_json::{Map, Value};
9
10use super::net_handle;
11use crate::wire::lenient;
12
13/// SPI bit order.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum BitOrder {
16    /// Most-significant bit first.
17    Msb,
18    /// Least-significant bit first.
19    Lsb,
20}
21
22impl BitOrder {
23    /// Wire encoding.
24    pub fn as_str(&self) -> &'static str {
25        match self {
26            BitOrder::Msb => "msb",
27            BitOrder::Lsb => "lsb",
28        }
29    }
30}
31
32/// Chip-select active polarity.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum CsActive {
35    /// CS is active-low (typical).
36    Low,
37    /// CS is active-high.
38    High,
39}
40
41impl CsActive {
42    /// Wire encoding.
43    pub fn as_str(&self) -> &'static str {
44        match self {
45            CsActive::Low => "low",
46            CsActive::High => "high",
47        }
48    }
49}
50
51/// Chip-select handling mode.
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum CsMode {
54    /// The driver asserts/deasserts CS around each transaction.
55    Auto,
56    /// The caller controls CS (e.g. via a GPIO net).
57    Manual,
58}
59
60impl CsMode {
61    /// Wire encoding.
62    pub fn as_str(&self) -> &'static str {
63        match self {
64            CsMode::Auto => "auto",
65            CsMode::Manual => "manual",
66        }
67    }
68}
69
70/// Configuration overrides for [`Spi::configure`]. `None` fields keep the
71/// net's saved value; explicit fields are applied to the live driver and
72/// persisted on the box.
73#[derive(Debug, Clone, Copy, Default)]
74pub struct SpiConfig {
75    /// SPI mode 0–3 (CPOL/CPHA).
76    pub mode: Option<u8>,
77    /// Bit order.
78    pub bit_order: Option<BitOrder>,
79    /// Clock frequency in Hz.
80    pub frequency_hz: Option<u32>,
81    /// Word size in bits (8, 16, or 32).
82    pub word_size: Option<u8>,
83    /// CS active polarity.
84    pub cs_active: Option<CsActive>,
85    /// CS handling mode.
86    pub cs_mode: Option<CsMode>,
87}
88
89/// The effective SPI configuration the box reports after `configure`.
90#[derive(Debug, Clone, Deserialize)]
91pub struct SpiEffectiveConfig {
92    /// SPI mode 0–3.
93    #[serde(default, deserialize_with = "lenient::opt_i64")]
94    pub mode: Option<i64>,
95    /// Clock frequency in Hz.
96    #[serde(default, deserialize_with = "lenient::opt_i64")]
97    pub frequency_hz: Option<i64>,
98    /// Word size in bits.
99    #[serde(default, deserialize_with = "lenient::opt_i64")]
100    pub word_size: Option<i64>,
101    /// Bit order (`"msb"`/`"lsb"`).
102    #[serde(default)]
103    pub bit_order: Option<String>,
104    /// CS active polarity (`"low"`/`"high"`).
105    #[serde(default)]
106    pub cs_active: Option<String>,
107    /// CS handling mode (`"auto"`/`"manual"`).
108    #[serde(default)]
109    pub cs_mode: Option<String>,
110    /// Any extra driver-specific fields.
111    #[serde(flatten)]
112    pub extra: Map<String, Value>,
113}
114
115/// Result of an SPI transaction: the words clocked in, plus the word size
116/// they were clocked at.
117#[derive(Debug, Clone)]
118pub struct SpiTransfer {
119    /// Words read back (one entry per word, regardless of word size).
120    pub words: Vec<u32>,
121    /// Word size in bits the transaction ran at.
122    pub word_size: u32,
123}
124
125/// Per-transaction options. The defaults match the CLI: fill `0xFF`,
126/// CS released at the end of the transaction.
127#[derive(Debug, Clone, Copy)]
128pub struct SpiOptions {
129    /// Fill word clocked out when reading (or padding a transfer).
130    pub fill: u32,
131    /// Keep CS asserted after the transaction (for multi-part transfers).
132    pub keep_cs: bool,
133}
134
135impl Default for SpiOptions {
136    fn default() -> Self {
137        SpiOptions {
138            fill: 0xFF,
139            keep_cs: false,
140        }
141    }
142}
143
144pub(crate) mod ops {
145    use serde_json::{json, Map, Value};
146
147    use super::{SpiConfig, SpiEffectiveConfig, SpiOptions, SpiTransfer};
148    use crate::error::{Error, Result};
149    use crate::wire::{
150        as_i64, net_command, value_as, value_int_list, CommandResponse, Op, Timeout,
151    };
152
153    const ROLE: &str = "spi";
154
155    fn parse_transfer(resp: CommandResponse) -> Result<SpiTransfer> {
156        let word_size = resp
157            .extra
158            .get("word_size")
159            .and_then(as_i64)
160            .and_then(|n| u32::try_from(n).ok())
161            .ok_or_else(|| Error::Decode("SPI response missing 'word_size'".to_string()))?;
162        let words = value_int_list(resp)?;
163        Ok(SpiTransfer { words, word_size })
164    }
165
166    pub(crate) fn configure(name: &str, config: &SpiConfig) -> Op<SpiEffectiveConfig> {
167        let mut params = Map::new();
168        if let Some(m) = config.mode {
169            params.insert("mode".to_string(), json!(m));
170        }
171        if let Some(b) = config.bit_order {
172            params.insert("bit_order".to_string(), json!(b.as_str()));
173        }
174        if let Some(f) = config.frequency_hz {
175            params.insert("frequency_hz".to_string(), json!(f));
176        }
177        if let Some(w) = config.word_size {
178            params.insert("word_size".to_string(), json!(w));
179        }
180        if let Some(c) = config.cs_active {
181            params.insert("cs_active".to_string(), json!(c.as_str()));
182        }
183        if let Some(c) = config.cs_mode {
184            params.insert("cs_mode".to_string(), json!(c.as_str()));
185        }
186        Op {
187            req: net_command(name, ROLE, "config", Value::Object(params), Timeout::Default),
188            parse: value_as::<SpiEffectiveConfig>,
189        }
190    }
191
192    pub(crate) fn read(name: &str, n_words: u32, opts: &SpiOptions) -> Op<SpiTransfer> {
193        Op {
194            req: net_command(
195                name,
196                ROLE,
197                "read",
198                json!({ "n_words": n_words, "fill": opts.fill, "keep_cs": opts.keep_cs }),
199                Timeout::Default,
200            ),
201            parse: parse_transfer,
202        }
203    }
204
205    pub(crate) fn write(name: &str, data: &[u32], opts: &SpiOptions) -> Op<SpiTransfer> {
206        // "write" is a full-duplex transfer box-side; the words clocked in
207        // are returned.
208        Op {
209            req: net_command(
210                name,
211                ROLE,
212                "write",
213                json!({ "data": data, "keep_cs": opts.keep_cs }),
214                Timeout::Default,
215            ),
216            parse: parse_transfer,
217        }
218    }
219
220    pub(crate) fn read_write(name: &str, data: &[u32], opts: &SpiOptions) -> Op<SpiTransfer> {
221        Op {
222            req: net_command(
223                name,
224                ROLE,
225                "read_write",
226                json!({ "data": data, "keep_cs": opts.keep_cs }),
227                Timeout::Default,
228            ),
229            parse: parse_transfer,
230        }
231    }
232
233    pub(crate) fn transfer(
234        name: &str,
235        data: &[u32],
236        n_words: u32,
237        opts: &SpiOptions,
238    ) -> Op<SpiTransfer> {
239        Op {
240            req: net_command(
241                name,
242                ROLE,
243                "transfer",
244                json!({
245                    "data": data,
246                    "n_words": n_words,
247                    "fill": opts.fill,
248                    "keep_cs": opts.keep_cs,
249                }),
250                Timeout::Default,
251            ),
252            parse: parse_transfer,
253        }
254    }
255}
256
257net_handle! {
258    /// Handle for an SPI bus net.
259    sync: Spi,
260    async: AsyncSpi,
261    methods: {
262        /// Apply configuration overrides (persisted on the box) and return
263        /// the effective configuration.
264        fn configure(config: &SpiConfig) -> SpiEffectiveConfig = ops::configure;
265        /// Clock in `n_words` words, clocking out the fill word.
266        fn read(n_words: u32, opts: &SpiOptions) -> SpiTransfer = ops::read;
267        /// Clock out exactly `data` (full duplex; the words clocked in are
268        /// returned).
269        fn write(data: &[u32], opts: &SpiOptions) -> SpiTransfer = ops::write;
270        /// Full-duplex transfer of exactly `data`.
271        fn read_write(data: &[u32], opts: &SpiOptions) -> SpiTransfer = ops::read_write;
272        /// Transfer `n_words` words, padding/truncating `data` with the
273        /// fill word.
274        fn transfer(data: &[u32], n_words: u32, opts: &SpiOptions) -> SpiTransfer = ops::transfer;
275    }
276}