ws63_hal/lsadc.rs
1//! Low-Speed ADC (LSADC) driver for WS63 — v154 controller.
2//!
3//! Register map and bit fields are cross-checked against the WS63 C SDK
4//! (`hal_adc_v154_regs_def.h`, `hal_adc_v154_regs_op.{c,h}`, `hal_adc_v154.c`).
5//! The control bank is the contiguous `adc_regs_t` struct at base `0x4400_C000`:
6//!
7//! | Register | Offset | Purpose |
8//! |---------------|--------|----------------------------------------------------|
9//! | `LSADC_CTRL_0` | 0x00 | scan config: per-channel enable + sample timing |
10//! | `LSADC_CTRL_1` | 0x04 | FIFO status (`rne`/`rff`/`bsy`) + waterline |
11//! | `LSADC_CTRL_2` | 0x08 | interrupt mask/status |
12//! | `LSADC_CTRL_8` | 0x1C | scan start/stop |
13//! | `LSADC_CTRL_9` | 0x20 | FIFO read data (`data[13:0]`, `channel[16:14]`) |
14//! | `LSADC_CTRL_11` | 0x24 | analog enable (`da_lsadc_en`) + reset (`rstn`@16) |
15//! | `CFG_DATA_SEL` | 0xDC | data output select |
16//! | `CFG_OFFSET` | 0xE0 | offset correction |
17//! | `CFG_GAIN` | 0xE4 | gain correction |
18//! | `CFG_CIC_FILTER_EN` | 0xE8 | CIC filter enable |
19//! | `CFG_CIC_OSR` | 0xEC | CIC oversampling ratio |
20//!
21//! # Status
22//!
23//! Not validated on silicon. The full analog power-up sequence (the SDK's
24//! `hal_adc_simulation_cfg` magic writes to `da_lsadc_en` and the `da_lsadc_rwreg`
25//! registers, plus offset/cap/gain calibration) is **not** implemented here;
26//! [`LsAdc::set_analog_enable`] exposes `da_lsadc_en` for callers that port it.
27
28use crate::peripherals::Lsadc;
29
30/// LSADC channel (0-5).
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum AdcChannel {
33 Channel0 = 0,
34 Channel1 = 1,
35 Channel2 = 2,
36 Channel3 = 3,
37 Channel4 = 4,
38 Channel5 = 5,
39}
40
41impl AdcChannel {
42 /// Convert a channel index (0-5) to an `AdcChannel`.
43 pub fn from_index(idx: u8) -> Option<Self> {
44 match idx {
45 0 => Some(Self::Channel0),
46 1 => Some(Self::Channel1),
47 2 => Some(Self::Channel2),
48 3 => Some(Self::Channel3),
49 4 => Some(Self::Channel4),
50 5 => Some(Self::Channel5),
51 _ => None,
52 }
53 }
54}
55
56/// Averaging mode (`equ_model_sel`, `LSADC_CTRL_0[7:6]`).
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum Averaging {
59 /// 1 sample averaged.
60 One = 0,
61 /// 2 samples averaged.
62 Two = 1,
63 /// 4 samples averaged.
64 Four = 2,
65 /// 8 samples averaged.
66 Eight = 3,
67}
68
69/// Scan-mode sample timing (`LSADC_CTRL_0`). Defaults match the SDK's
70/// `hal_adc_auto_scan_mode_set` (8× averaging, `sample_cnt=8`, `start_cnt=0x18`).
71#[derive(Debug, Clone, Copy)]
72pub struct AdcConfig {
73 /// Averaging mode (`equ_model_sel`, 2-bit).
74 pub averaging: Averaging,
75 /// Sample count (`sample_cnt`, 5-bit).
76 pub sample_count: u8,
77 /// Start count (`start_cnt` / SDK `satrt_cnt`, 8-bit).
78 pub start_count: u8,
79 /// Cast count (`cast_cnt`, 7-bit).
80 pub cast_count: u8,
81}
82
83impl Default for AdcConfig {
84 fn default() -> Self {
85 Self { averaging: Averaging::Eight, sample_count: 0x8, start_count: 0x18, cast_count: 0x0 }
86 }
87}
88
89/// ADC conversion result.
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub struct AdcSample {
92 /// 14-bit conversion code.
93 pub data: u16,
94 /// Channel that produced this sample (0-5).
95 pub channel: u8,
96}
97
98/// Parse a raw `LSADC_CTRL_9` word into a sample: `data` = bits `[13:0]`,
99/// `channel` = bits `[16:14]` (matches `adc_fifo_data_str` in the SDK).
100#[inline]
101const fn parse_sample(raw: u32) -> AdcSample {
102 AdcSample { data: (raw & 0x3FFF) as u16, channel: ((raw >> 14) & 0x07) as u8 }
103}
104
105/// LSADC driver.
106pub struct LsAdc<'d> {
107 _lsadc: Lsadc<'d>,
108}
109
110impl<'d> LsAdc<'d> {
111 /// Create a new LSADC driver from the LSADC peripheral.
112 pub fn new(lsadc: Lsadc<'d>) -> Self {
113 Self { _lsadc: lsadc }
114 }
115
116 fn regs(&self) -> &'static ws63_pac::lsadc::RegisterBlock {
117 // SAFETY: PAC peripheral pointer is a static physical MMIO address, always valid.
118 unsafe { &*Lsadc::ptr() }
119 }
120
121 /// Release the analog reset (`LSADC_CTRL_11.da_lsadc_rstn = 1`, active-low).
122 pub fn enable(&mut self) {
123 self.regs().lsadc_ctrl_11().modify(|_, w| w.da_lsadc_rstn().set_bit());
124 }
125
126 /// Assert the analog reset (`LSADC_CTRL_11.da_lsadc_rstn = 0`).
127 pub fn disable(&mut self) {
128 self.regs().lsadc_ctrl_11().modify(|_, w| w.da_lsadc_rstn().clear_bit());
129 }
130
131 /// Write the 16-bit analog-enable field (`LSADC_CTRL_11.da_lsadc_en`).
132 ///
133 /// The SDK power-up sequence ORs in `0x7000`, `0xE7F`, `0x100`, `0x80`
134 /// across several steps; this exposes the raw field for porting it.
135 pub fn set_analog_enable(&mut self, bits: u16) {
136 self.regs().lsadc_ctrl_11().modify(|_, w| unsafe { w.da_lsadc_en().bits(bits) });
137 }
138
139 /// Enable a channel and program the scan timing (`LSADC_CTRL_0`).
140 ///
141 /// Sets the per-channel enable bit (preserving any already-enabled channels)
142 /// and the averaging/sample/start/cast counts, matching
143 /// `hal_adc_auto_scan_mode_set`.
144 pub fn configure_scan(&mut self, channel: AdcChannel, config: &AdcConfig) {
145 self.regs().lsadc_ctrl_0().modify(|r, w| {
146 let ch = r.channel().bits() | (1 << (channel as u8));
147 unsafe {
148 w.channel().bits(ch);
149 w.equ_model_sel().bits(config.averaging as u8);
150 w.sample_cnt().bits(config.sample_count & 0x1F);
151 w.start_cnt().bits(config.start_count);
152 w.cast_cnt().bits(config.cast_count & 0x7F)
153 }
154 });
155 }
156
157 /// Start an ADC scan (`LSADC_CTRL_8.lsadc_start = 1`).
158 pub fn start_scan(&mut self) {
159 self.regs().lsadc_ctrl_8().write(|w| w.lsadc_start().set_bit());
160 }
161
162 /// Stop an ADC scan (`LSADC_CTRL_8.lsadc_stop = 1`).
163 pub fn stop_scan(&mut self) {
164 self.regs().lsadc_ctrl_8().write(|w| w.lsadc_stop().set_bit());
165 }
166
167 /// Set the RX-FIFO interrupt waterline (`LSADC_CTRL_1.rxintsize`, 3-bit).
168 pub fn set_fifo_waterline(&mut self, level: u8) {
169 self.regs().lsadc_ctrl_1().modify(|_, w| unsafe { w.rxintsize().bits(level & 0x07) });
170 }
171
172 /// True if the RX FIFO holds at least one sample (`LSADC_CTRL_1.rne`).
173 ///
174 /// This is the reliable empty check — read it before [`Self::read_sample`].
175 pub fn data_ready(&self) -> bool {
176 self.regs().lsadc_ctrl_1().read().rne().bit_is_set()
177 }
178
179 /// Read one sample from the FIFO (`LSADC_CTRL_9`).
180 ///
181 /// Returns `None` when the FIFO is empty (checked via `rne`), so a genuine
182 /// 0-code reading is **not** mistaken for "no data".
183 pub fn read_sample(&self) -> Option<AdcSample> {
184 if !self.data_ready() {
185 return None;
186 }
187 Some(parse_sample(self.regs().lsadc_ctrl_9().read().bits()))
188 }
189
190 /// Enable the CIC filter with the given oversampling ratio.
191 pub fn enable_cic_filter(&mut self, oversampling_ratio: u8) {
192 let r = self.regs();
193 unsafe {
194 r.cfg_cic_osr().write(|w| w.cic_osr().bits(oversampling_ratio));
195 }
196 r.cfg_cic_filter_en().write(|w| w.cic_filter_en().set_bit());
197 }
198
199 /// Disable the CIC filter.
200 pub fn disable_cic_filter(&mut self) {
201 self.regs().cfg_cic_filter_en().write(|w| w.cic_filter_en().clear_bit());
202 }
203
204 /// Set the ADC offset correction value (`CFG_OFFSET`).
205 pub fn set_offset(&mut self, offset: u16) {
206 self.regs().cfg_offset().write(|w| unsafe { w.offset().bits(offset) });
207 }
208
209 /// Set the ADC gain correction value (`CFG_GAIN`).
210 pub fn set_gain(&mut self, gain: u16) {
211 self.regs().cfg_gain().write(|w| unsafe { w.gain().bits(gain) });
212 }
213
214 /// Select the data source: `true` = post-processed, `false` = raw ADC code.
215 pub fn set_data_select(&mut self, processed: bool) {
216 self.regs().cfg_data_sel().write(|w| w.data_sel().bit(processed));
217 }
218}
219
220// ── Tests ──────────────────────────────────────────────────────
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225
226 #[test]
227 fn test_parse_sample_data_and_channel() {
228 // channel 3, code 0x0AAA -> raw = (3 << 14) | 0x0AAA
229 let raw: u32 = (3 << 14) | 0x0AAA;
230 let s = parse_sample(raw);
231 assert_eq!(s.data, 0x0AAA);
232 assert_eq!(s.channel, 3);
233 }
234
235 #[test]
236 fn test_parse_sample_max_code() {
237 let s = parse_sample(0x3FFF); // all data bits, channel 0
238 assert_eq!(s.data, 0x3FFF);
239 assert_eq!(s.channel, 0);
240 }
241
242 #[test]
243 fn test_parse_sample_channel_field_is_3_bits() {
244 // bits above [16:14] must not leak into channel
245 let raw: u32 = 0xFFFF_FFFF;
246 let s = parse_sample(raw);
247 assert_eq!(s.channel, 0x07);
248 assert_eq!(s.data, 0x3FFF);
249 }
250
251 #[test]
252 fn test_channel_from_index() {
253 assert_eq!(AdcChannel::from_index(0), Some(AdcChannel::Channel0));
254 assert_eq!(AdcChannel::from_index(5), Some(AdcChannel::Channel5));
255 assert_eq!(AdcChannel::from_index(6), None);
256 assert_eq!(AdcChannel::from_index(255), None);
257 }
258
259 #[test]
260 fn test_default_config_matches_sdk() {
261 let c = AdcConfig::default();
262 assert_eq!(c.averaging as u8, 3); // AVERAGE_OF_EIGHT_SAMPLES
263 assert_eq!(c.sample_count, 0x8);
264 assert_eq!(c.start_count, 0x18);
265 assert_eq!(c.cast_count, 0x0);
266 }
267}
268
269// ── Property-based fuzz tests ──────────────────────────────────
270
271#[cfg(test)]
272mod proptests {
273 use super::*;
274 use proptest::prelude::*;
275
276 proptest! {
277 /// Fuzz: channel field is always within 3 bits regardless of input.
278 #[test]
279 fn parse_channel_always_3_bits(raw in any::<u32>()) {
280 prop_assert!(parse_sample(raw).channel <= 0x07);
281 }
282
283 /// Fuzz: data field is always within 14 bits.
284 #[test]
285 fn parse_data_always_14_bits(raw in any::<u32>()) {
286 prop_assert!(parse_sample(raw).data <= 0x3FFF);
287 }
288
289 /// Fuzz: data and channel are extracted from the correct, disjoint lanes.
290 #[test]
291 fn parse_sample_lanes(raw in any::<u32>()) {
292 let s = parse_sample(raw);
293 prop_assert_eq!(s.data, (raw & 0x3FFF) as u16);
294 prop_assert_eq!(s.channel, ((raw >> 14) & 0x07) as u8);
295 }
296
297 /// Fuzz: AdcChannel::from_index returns Some for 0-5, None otherwise.
298 #[test]
299 fn channel_from_index_coverage(idx in any::<u8>()) {
300 prop_assert_eq!(AdcChannel::from_index(idx).is_some(), idx <= 5);
301 }
302 }
303}
304
305// ── Async LSADC (bespoke; LSADC_INTR = IRQ 72) ──────────────────────────────
306#[cfg(feature = "async")]
307mod asynch_impl {
308 use super::{AdcSample, LsAdc};
309 use crate::asynch::IrqSignal;
310 use crate::interrupt::{self, Interrupt};
311 use core::future::Future;
312 use core::pin::Pin;
313 use core::task::{Context, Poll};
314
315 static LSADC_SIGNAL: IrqSignal = IrqSignal::new();
316
317 /// LSADC trap hook (IRQ 72): wake the awaiting conversion. The sample is
318 /// read-cleared by `read_sample`, so the ISR just signals + clears pending.
319 pub fn on_interrupt() {
320 LSADC_SIGNAL.signal();
321 interrupt::clear_pending(Interrupt::LSADC_INTR);
322 }
323
324 struct ConvFuture;
325 impl Future for ConvFuture {
326 type Output = ();
327 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
328 if LSADC_SIGNAL.take_fired() {
329 Poll::Ready(())
330 } else {
331 LSADC_SIGNAL.register(cx.waker());
332 Poll::Pending
333 }
334 }
335 }
336
337 impl LsAdc<'_> {
338 /// Start a scan and await conversion-complete (IRQ 72), returning a sample.
339 /// On hardware this parks until the IRQ; the WS63 model fills the FIFO
340 /// synchronously, so the fast path returns without parking.
341 pub async fn read_async(&mut self) -> Option<AdcSample> {
342 LSADC_SIGNAL.reset();
343 // SAFETY: enabling a known, fixed WS63 IRQ line.
344 unsafe { interrupt::enable(Interrupt::LSADC_INTR) };
345 self.start_scan();
346 if !self.data_ready() {
347 ConvFuture.await;
348 }
349 self.read_sample()
350 }
351 }
352}
353
354#[cfg(feature = "async")]
355pub use asynch_impl::on_interrupt;