Skip to main content

panelspec/
chips.rs

1//! Driver-chip library (`config/chips/*.toml`).
2//!
3//! The vendor's default register table for a chip, the chip-control block
4//! the card's config carries for it, and the rules the vendor applies when
5//! it builds record 0x84.
6
7use anyhow::{bail, Context, Result};
8use serde::Deserialize;
9use std::collections::BTreeMap;
10use std::path::Path;
11
12#[derive(Debug, Clone, Deserialize)]
13#[serde(deny_unknown_fields)]
14pub struct ChipLibrary {
15    pub name: String,
16    /// Who makes the chip.
17    #[serde(default)]
18    pub vendor: Option<String>,
19    /// Datasheet.
20    #[serde(default)]
21    pub datasheet: Option<String>,
22    pub family_id: u16,
23    pub sub_id: Option<u16>,
24    /// Vendor default serial clock for the chip (record +0x021).
25    pub serial_clock: u16,
26    /// The 20-byte `SChipControl` block the vendor's `ResetChipControl`
27    /// emits for this chip (record 0x01 +0x0C4).
28    pub chip_control: [u8; 20],
29    /// Register ids in record order. Absent for chips without an addressed
30    /// register table (the non-SH S-PWM parts, e.g. SM16169S).
31    #[serde(default)]
32    pub order: Vec<u8>,
33    /// Register id (spelled `0x..` in the file) → R, G, B values.
34    #[serde(default, deserialize_with = "hex_keys")]
35    pub registers: BTreeMap<u8, [u8; 3]>,
36    /// The 16-byte `SChipCustom` block (record 0x01 +0x06A) when the chip's
37    /// configuration lives there instead of in record 0x84. When absent the
38    /// generator writes only the PWM-flag/serial-clock pair the SH chips use.
39    pub chip_custom: Option<[u8; 16]>,
40    /// The scan patch the vendor applies to `chip_custom` on load:
41    /// `byte = base | ((scan - 1) & mask)` for each listed byte.
42    pub chip_custom_scan_patch: Option<ScanPatch>,
43    /// `SChipCustomEX` (record 0x01 +0x0E0..+0x0E3).
44    pub chip_custom_ex: Option<[u8; 4]>,
45    /// The vendor omits record 0x84 for non-addressed chips; a zeroed record
46    /// is not the same file.
47    #[serde(default = "default_true")]
48    pub emit_record_84: bool,
49    /// Grey depth as a literal, for chips whose depth is not derived from
50    /// registers 0x07/0x03.
51    pub gray_bits: Option<u8>,
52    /// Record 0x01 bytes this chip id sets differently from the 0x14C
53    /// baseline; applied before the spec's own overrides.
54    #[serde(default, deserialize_with = "record01_offsets")]
55    pub record01_overrides: BTreeMap<usize, u8>,
56}
57
58/// `0x..` map keys parsed at load. Two spellings of one key (`0x02F`/`0x2F`)
59/// are refused rather than letting the map pick one.
60fn hex_keys<'de, D, K, V>(d: D) -> std::result::Result<BTreeMap<K, V>, D::Error>
61where
62    D: serde::Deserializer<'de>,
63    K: HexKey,
64    V: serde::Deserialize<'de>,
65{
66    let raw: BTreeMap<String, V> = serde::Deserialize::deserialize(d)?;
67    let mut out = BTreeMap::new();
68    for (key, value) in raw {
69        let k = K::parse_hex(&key).map_err(serde::de::Error::custom)?;
70        if out.insert(k, value).is_some() {
71            return Err(serde::de::Error::custom(format!(
72                "key {key} given twice (spelled differently)"
73            )));
74        }
75    }
76    Ok(out)
77}
78
79/// `hex_keys` for record 0x01 offsets, which must lie inside the record.
80pub(crate) fn record01_offsets<'de, D>(d: D) -> std::result::Result<BTreeMap<usize, u8>, D::Error>
81where
82    D: serde::Deserializer<'de>,
83{
84    let map: BTreeMap<usize, u8> = hex_keys(d)?;
85    if let Some(at) = map.keys().find(|&&at| at >= crate::RECORD01_LEN) {
86        return Err(serde::de::Error::custom(format!(
87            "record01_overrides offset {at:#05x} is past the record"
88        )));
89    }
90    Ok(map)
91}
92
93/// The reverse of `record01_offsets`: keys written as `"0x02F"`.
94pub(crate) fn hex_offsets<S>(map: &BTreeMap<usize, u8>, s: S) -> std::result::Result<S::Ok, S::Error>
95where
96    S: serde::Serializer,
97{
98    s.collect_map(map.iter().map(|(at, v)| (format!("{at:#05X}"), v)))
99}
100
101pub(crate) trait HexKey: Ord {
102    fn parse_hex(s: &str) -> Result<Self>
103    where
104        Self: Sized;
105}
106
107impl HexKey for u8 {
108    fn parse_hex(s: &str) -> Result<Self> {
109        Self::from_str_radix(s.trim_start_matches("0x"), 16)
110            .with_context(|| format!("bad register id {s:?}"))
111    }
112}
113
114impl HexKey for usize {
115    fn parse_hex(s: &str) -> Result<Self> {
116        Self::from_str_radix(s.trim_start_matches("0x"), 16)
117            .with_context(|| format!("bad offset {s:?}"))
118    }
119}
120
121#[derive(Debug, Clone, Deserialize)]
122#[serde(deny_unknown_fields)]
123pub struct ScanPatch {
124    pub bytes: Vec<usize>,
125    pub mask: u8,
126    pub base: u8,
127}
128
129const fn default_true() -> bool {
130    true
131}
132
133impl ChipLibrary {
134    /// # Errors
135    /// Fails on a missing or malformed file.
136    pub fn load(path: impl AsRef<Path>) -> Result<Self> {
137        let path = path.as_ref();
138        let text =
139            std::fs::read_to_string(path).with_context(|| format!("read {}", path.display()))?;
140        Self::parse(&text).with_context(|| format!("parse {}", path.display()))
141    }
142
143    /// Read a library from TOML text.
144    ///
145    /// # Errors
146    /// Fails on malformed TOML, an unknown field or a bad hex key.
147    pub fn parse(text: &str) -> Result<Self> {
148        Ok(toml::from_str(text)?)
149    }
150
151    fn reg(&self, id: u8) -> Result<[u8; 3]> {
152        self.registers
153            .get(&id)
154            .copied()
155            .with_context(|| format!("{}: no values for register {id:#04x}", self.name))
156    }
157
158    /// Record 0x84: `(register, R, G, B)` quads in library order, zero-filled
159    /// to 256 bytes, with the vendor's post-load patch of register 0x02 to
160    /// `scan - 1` (`ResetChipCustom`, chip 0x14C case).
161    ///
162    /// # Errors
163    /// Fails if the order names a register the table lacks, or overflows.
164    pub fn record_84(&self, scan: u8) -> Result<Option<[u8; 256]>> {
165        if !self.emit_record_84 || self.order.is_empty() {
166            return Ok(None);
167        }
168        if self.order.len() * 4 > 256 {
169            bail!(
170                "{}: {} registers do not fit a 256-byte record",
171                self.name,
172                self.order.len()
173            );
174        }
175        let mut out = [0u8; 256];
176        // as_chunks_mut keeps the fixed width in the type.
177        let (quads, _) = out.as_chunks_mut::<4>();
178        for (quad, &reg) in quads.iter_mut().zip(&self.order) {
179            let rgb = if reg == 0x02 {
180                [scan.wrapping_sub(1) & 0x3F; 3]
181            } else {
182                self.reg(reg)?
183            };
184            quad[0] = reg;
185            quad[1..].copy_from_slice(&rgb);
186        }
187        Ok(Some(out))
188    }
189
190    /// The `SChipCustom` block for a scan count, with the vendor's load-time
191    /// patch applied; `None` for chips configured through record 0x84.
192    #[must_use]
193    pub fn chip_custom_block(&self, scan: u8) -> Option<[u8; 16]> {
194        let mut block = self.chip_custom?;
195        if let Some(p) = &self.chip_custom_scan_patch {
196            for &i in &p.bytes {
197                if let Some(b) = block.get_mut(i) {
198                    *b = p.base | (scan.wrapping_sub(1) & p.mask);
199                }
200            }
201        }
202        Some(block)
203    }
204
205    /// Grayscale depth the vendor derives from the registers
206    /// (`GetSupporttedGray`, chip 0x14C branch): line-gray from reg 0x07
207    /// bits 4:3 times a multiplier from reg 0x03, bucketed into 12..16 bits.
208    ///
209    /// # Errors
210    /// Fails if registers 0x03 or 0x07 are missing.
211    pub fn gray_bits(&self) -> Result<u8> {
212        if let Some(g) = self.gray_bits {
213            return Ok(g);
214        }
215        let r07 = self.reg(0x07)?[0];
216        let r03 = self.reg(0x03)?[0];
217        let g = 128u32 << ((r07 >> 3) & 3);
218        let m = if r03 < 0x40 { 64 } else { 32 };
219        Ok(match m * g {
220            x if x < 0x1000 => 12,
221            x if x < 0x2000 => 13,
222            x if x < 0x4000 => 14,
223            x if x < 0x8000 => 15,
224            _ => 16,
225        })
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232
233    fn lib() -> ChipLibrary {
234        toml::from_str(
235            r#"
236            name = "t"
237            family_id = 1
238            serial_clock = 15
239            chip_control = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
240            order = [0x02, 0x03, 0x07, 0xf0]
241            [registers]
242            0x02 = [0x3f, 0x3f, 0x3f]
243            0x03 = [0x3f, 0x3f, 0x3f]
244            0x07 = [0x04, 0x04, 0x04]
245            0xf0 = [4, 5, 6]
246            "#,
247        )
248        .unwrap()
249    }
250
251    #[test]
252    fn quads_land_in_order_with_scan_patch_and_zero_fill() {
253        let r = lib().record_84(16).unwrap().unwrap();
254        assert_eq!(&r[..4], &[0x02, 15, 15, 15], "reg 0x02 = scan - 1");
255        assert_eq!(&r[12..16], &[0xf0, 4, 5, 6]);
256        assert!(r[16..].iter().all(|&b| b == 0));
257    }
258
259    #[test]
260    fn hex_keys_are_typed_and_range_checked_at_load() {
261        let lib: ChipLibrary = toml::from_str(
262            r#"
263            name = "t"
264            family_id = 1
265            serial_clock = 15
266            chip_control = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
267            [registers]
268            0x2 = [1, 2, 3]
269            [record01_overrides]
270            "0x02F" = 1
271            "0x2fb" = 2
272            "#,
273        )
274        .unwrap();
275        assert_eq!(lib.registers.get(&2), Some(&[1, 2, 3]));
276        assert_eq!(
277            lib.record01_overrides.iter().collect::<Vec<_>>(),
278            vec![(&0x2F, &1), (&0x2FB, &2)]
279        );
280
281        let past: std::result::Result<ChipLibrary, _> = toml::from_str(
282            "name = \"t\"\nfamily_id = 1\nserial_clock = 1\nchip_control = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]\n[record01_overrides]\n\"0x2FC\" = 1\n",
283        );
284        assert!(past.unwrap_err().to_string().contains("past the record"));
285        let twice: std::result::Result<ChipLibrary, _> = toml::from_str(
286            "name = \"t\"\nfamily_id = 1\nserial_clock = 1\nchip_control = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]\n[record01_overrides]\n\"0x02F\" = 1\n\"0x2F\" = 0\n",
287        );
288        assert!(twice.unwrap_err().to_string().contains("twice"));
289    }
290
291    #[test]
292    fn gray_bits_follow_the_vendor_formula() {
293        assert_eq!(lib().gray_bits().unwrap(), 14); // 128 x 64 = 0x2000 -> 14
294    }
295}