diff --git a/libs/bao1x-hal/src/udma/adc.rs b/libs/bao1x-hal/src/udma/adc.rs new file mode 100644 index 000000000..1c44ae5b2 --- /dev/null +++ b/libs/bao1x-hal/src/udma/adc.rs @@ -0,0 +1,450 @@ +//! UDMA ADC block driver +//! +//! Supports internal temperature sensor and external voltage measurement on +//! PA04–PA07 via the uDMA RX channel. +//! +//! # `std` (Xous userspace) +//! ```no_run +//! let mut adc = unsafe { Adc::new(perclk_freq) }; +//! let raw = adc.read_raw(AdcSource::Temperature); +//! let temp_c = Adc::raw_to_temp_celsius(raw as f64); +//! +//! let raw = adc.read_raw(AdcSource::Ext(AdcExtChannel::Pa04)); +//! let volts = Adc::raw_to_voltage(raw); +//! ``` +//! +//! # `no_std` (loader / bare-metal) +//! ```no_run +//! let mut adc = unsafe { Adc::new_baremetal(perclk_freq, dma_buf_phys) }; // no_std +//! ``` + +use utralib::utra::udma_adc; +use utralib::*; + +use crate::ifram::IframRange; +use crate::udma::*; + +// ============================================================================ +// Fine-grained field definitions for REG_CR_ADC +// +// The auto-generated UTRA only exposes one monolithic 28-bit field +// (REG_CR_ADC_CR_ADC). We break it out here to match the datasheet's +// per-bit definitions for ADC_CR_CFG (byte offset 0x10, word offset 4). +// ============================================================================ +pub mod cr_adc { + use super::*; + + /// Bit[0]: Bandgap chopper enable (0 = disable, 1 = enable) + pub const CHOPPER_EN: Field = Field::new(1, 0, udma_adc::REG_CR_ADC); + /// Bit[1]: Temperature-related voltage buffer (0 = disable, 1 = enable) + pub const TEMP_BUF_EN: Field = Field::new(1, 1, udma_adc::REG_CR_ADC); + /// Bit[2]: Bandgap voltage buffer / reference select + /// 0 = disable buffer, use AVDD as reference + /// 1 = enable buffer, use bandgap voltage as reference + pub const BANDGAP_BUF_EN: Field = Field::new(1, 2, udma_adc::REG_CR_ADC); + /// Bit[3]: External voltage buffer (0 = disable, 1 = enable) + pub const EXT_BUF_EN: Field = Field::new(1, 3, udma_adc::REG_CR_ADC); + /// Bit[4]: Temperature-related voltage control (0 = voltage 1, 1 = voltage 2) + pub const TEMP_V_CTRL: Field = Field::new(1, 4, udma_adc::REG_CR_ADC); + /// Bit[5]: Temperature voltage filter bypass (0 = filter, 1 = bypass) + pub const TEMP_FILTER_BYPASS: Field = Field::new(1, 5, udma_adc::REG_CR_ADC); + /// Bit[6]: Bandgap voltage filter bypass (0 = filter, 1 = bypass) + pub const BANDGAP_FILTER_BYPASS: Field = Field::new(1, 6, udma_adc::REG_CR_ADC); + /// Bits[12:8]: ADC clock cycles per conversion (must be ≥ 14) + pub const DATA_COUNT: Field = Field::new(5, 8, udma_adc::REG_CR_ADC); + /// Bit[13]: Bandgap / temperature sensor enable + pub const SENSOR_EN: Field = Field::new(1, 13, udma_adc::REG_CR_ADC); + /// Bit[14]: ADC enable + pub const ADC_EN: Field = Field::new(1, 14, udma_adc::REG_CR_ADC); + /// Bit[15]: ADC reset (write 1 to reset) + pub const ADC_RST: Field = Field::new(1, 15, udma_adc::REG_CR_ADC); + /// Bits[23:16]: Clock frequency divider. + /// adc_clk = perclk / (2 × FD). The resulting adc_clk must be 0.2–1.6 MHz. + pub const CLK_FD: Field = Field::new(8, 16, udma_adc::REG_CR_ADC); + /// Bits[25:24]: ADC input mux + /// 00 = temperature-related voltage (internal) + /// 01 = external voltage + pub const ADC_SEL: Field = Field::new(2, 24, udma_adc::REG_CR_ADC); + /// Bits[27:26]: External analog input select + /// 00 = PA04 (ADC0), 01 = PA05 (ADC1), + /// 10 = PA06 (ADC2), 11 = PA07 (ADC3) + pub const VIN_SEL: Field = Field::new(2, 26, udma_adc::REG_CR_ADC); +} + +// ============================================================================ +// Public types +// ============================================================================ + +/// External analog input channel (PA04–PA07). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(usize)] +pub enum AdcExtChannel { + /// PA04 / ADC0 + Pa04 = 0, + /// PA05 / ADC1 + Pa05 = 1, + /// PA06 / ADC2 + Pa06 = 2, + /// PA07 / ADC3 + Pa07 = 3, +} + +/// ADC measurement source. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AdcSource { + /// Internal temperature sensor + Temperature, + /// External voltage on the specified channel + Ext(AdcExtChannel), +} + +/// Target ADC conversion clock in kHz. The divider is computed from `perclk` +/// so that adc_clk lands as close to this as possible within the valid +/// 200–1600 kHz range. +const ADC_TARGET_CLK_KHZ: u32 = 1_000; + +/// Minimum clock cycles per conversion (from the datasheet). +const ADC_MIN_DATA_COUNT: u32 = 14; + +/// The ADC is RX-only; the entire IFRAM page is the receive buffer. +const ADC_RX_BUF_SIZE: usize = 4096; + +// ============================================================================ +// Driver +// ============================================================================ + +/// UDMA ADC block driver. +pub struct Adc { + csr: CSR, + /// IFRAM allocation backing the uDMA RX buffer. + #[allow(dead_code)] + ifram: IframRange, + /// Current peripheral clock frequency in Hz, used to compute the ADC + /// clock divider. Updated via [`update_perclk`]. + perclk_freq: u32, +} + +/// Blanket [`Udma`] trait implementation so the generic uDMA helpers +/// (`udma_busy`, `udma_enqueue`, etc.) work on `Adc`. +impl Udma for Adc { + fn csr_mut(&mut self) -> &mut CSR { &mut self.csr } + + fn csr(&self) -> &CSR { &self.csr } +} + +impl Adc { + // --- Construction -------------------------------------------------------- + + /// Create a new ADC driver, mapping the hardware registers and allocating + /// an IFRAM DMA buffer. + /// + /// `perclk_freq` is the UDMA peripheral clock frequency in Hz (derived + /// from the system clock and the `fdper` fractional divider). + /// + /// # Safety + /// + /// Caller must ensure the global UDMA state has been initialized (clocks + /// un-gated, events configured) before calling this, and must manually + /// gate the ADC clock again when the driver is no longer needed. + #[cfg(feature = "std")] + pub unsafe fn new(perclk_freq: u32) -> Self { + let adc_mem = xous::syscall::map_memory( + xous::MemoryAddress::new(udma_adc::HW_UDMA_ADC_BASE), + None, + 4096, + xous::MemoryFlags::R | xous::MemoryFlags::W, + ) + .expect("couldn't map ADC registers"); + + let csr = CSR::new(adc_mem.as_mut_ptr() as *mut u32); + let ifram = IframRange::request(ADC_RX_BUF_SIZE, None).expect("couldn't allocate IFRAM for ADC"); + + let mut adc = Adc { csr, ifram, perclk_freq }; + adc.init(); + adc + } + + /// Create a bare-metal ADC driver (no MMU, physical addresses used + /// directly). + /// + /// # Safety + /// + /// Same preconditions as [`new`], plus the caller is responsible for + /// ensuring `udma_buf_phys` points to a valid, word-aligned DMA buffer + /// region of at least [`ADC_RX_BUF_SIZE`] bytes that is accessible to the + /// uDMA engine. + #[cfg(not(feature = "std"))] + pub unsafe fn new_baremetal(perclk_freq: u32, udma_buf_phys: usize) -> Self { + let csr = CSR::new(udma_adc::HW_UDMA_ADC_BASE as *mut u32); + let ifram = IframRange::from_raw_parts( + udma_buf_phys, + udma_buf_phys, // virt == phys in bare-metal + ADC_RX_BUF_SIZE, + ); + + let mut adc = Adc { csr, ifram, perclk_freq }; + adc.init(); + adc + } + + /// Re-acquire a handle to a previously initialized ADC. + /// + /// This is the equivalent of the UART's `get_handle` — use it when the + /// hardware was already configured (e.g. by the loader) and you just need + /// a driver object to operate on it. + /// + /// # Safety + /// + /// Only safe when the ADC hardware is already initialized and the + /// provided addresses are valid. + pub unsafe fn get_handle( + csr_virt_addr: usize, + udma_phys_addr: usize, + udma_virt_addr: usize, + perclk_freq: u32, + ) -> Self { + let csr = CSR::new(csr_virt_addr as *mut u32); + let ifram = IframRange::from_raw_parts(udma_phys_addr, udma_virt_addr, ADC_RX_BUF_SIZE); + Adc { csr, ifram, perclk_freq } + } + + // --- Clock management --------------------------------------------------- + + /// Compute the clock frequency divider for a given peripheral clock. + /// + /// `perclk_khz`: peripheral clock in kHz. + /// `target_khz`: desired ADC clock in kHz (clamped to 200–1600). + /// + /// The hardware divides: adc_clk = perclk / (2 × FD). + fn calc_clk_fd(perclk_khz: u32, target_khz: u32) -> u32 { + let target_khz = target_khz.clamp(200, 1600); + let fd = perclk_khz / (2 * target_khz); + fd.clamp(1, 0xFF) + } + + /// Notify the driver that the peripheral clock frequency has changed + /// (e.g. after a power-mode transition). Re-computes and writes the + /// ADC clock divider. + /// + /// `perclk_freq`: new peripheral clock in **Hz**. + pub fn update_perclk(&mut self, perclk_freq: u32) { + self.perclk_freq = perclk_freq; + + let fd = Self::calc_clk_fd(perclk_freq / 1_000, ADC_TARGET_CLK_KHZ); + + // Read-modify-write: touch only CLK_FD[23:16] + let mut cr = self.csr.r(udma_adc::REG_CR_ADC); + cr &= !((cr_adc::CLK_FD.mask() as u32) << cr_adc::CLK_FD.offset()); + cr |= fd << cr_adc::CLK_FD.offset(); + self.csr.wo(udma_adc::REG_CR_ADC, cr); + } + + // --- Delays ------------------------------------------------------------- + + #[cfg(feature = "std")] + fn delay_us(us: u64) { std::thread::sleep(std::time::Duration::from_micros(us)); } + + /// Bare-metal delay stub. + #[cfg(not(feature = "std"))] + fn delay_us(us: u64) { + // abuse the d11ctime timer to create some time-out like thing + let mut d11c = CSR::new(utra::d11ctime::HW_D11CTIME_BASE as *mut u32); + d11c.wfo(utra::d11ctime::CONTROL_COUNT, 333); // 1.0us per interval + let mut polarity = d11c.rf(utra::d11ctime::HEARTBEAT_BEAT); + for _ in 0..us { + while polarity == d11c.rf(utra::d11ctime::HEARTBEAT_BEAT) {} + polarity = d11c.rf(utra::d11ctime::HEARTBEAT_BEAT); + } + // we have to split this because we don't know where we caught the previous interval + if us == 1 { + while polarity == d11c.rf(utra::d11ctime::HEARTBEAT_BEAT) {} + } + } + + // --- Initialization ----------------------------------------------------- + + /// Full ADC power-on sequence per the datasheet start timing spec: + /// + /// 1. Stop any in-flight DMA + /// 2. Assert reset, set clock divider + data count + sensor enable + /// 3. Wait ≥ 10 µs (t1) + /// 4. Enable bandgap reference + voltage buffers + /// 5. Wait (t2 — spec says 0 µs min, we add margin) + /// 6. Enable ADC + /// 7. Wait ≥ 90 µs (t3) + /// + /// Defaults to temperature sensor source after init. + fn init(&mut self) { + // Step 0 — stop any prior DMA + let rx_cfg = self.csr.r(udma_adc::REG_RX_CFG); + if (rx_cfg & ((1 << 4) | (1 << 5))) != 0 { + self.csr.rmwf(udma_adc::REG_RX_CFG_R_RX_CLR, 1); + } + + // Step 1 — reset + base config + self.csr.wo(udma_adc::REG_CR_ADC, 0); + + let fd = Self::calc_clk_fd(self.perclk_freq / 1_000, ADC_TARGET_CLK_KHZ); + + let mut cr: u32 = 0; + cr |= 1 << cr_adc::ADC_RST.offset(); + cr |= fd << cr_adc::CLK_FD.offset(); + cr |= ADC_MIN_DATA_COUNT << cr_adc::DATA_COUNT.offset(); + cr |= 1 << cr_adc::SENSOR_EN.offset(); + self.csr.wo(udma_adc::REG_CR_ADC, cr); + + // Step 2 — t1 + Self::delay_us(20); + + // Step 3 — bandgap + temperature buffers + cr |= 1 << cr_adc::BANDGAP_BUF_EN.offset(); + cr |= 1 << cr_adc::TEMP_BUF_EN.offset(); + cr |= 1 << cr_adc::TEMP_FILTER_BYPASS.offset(); + cr |= 1 << cr_adc::BANDGAP_FILTER_BYPASS.offset(); + self.csr.wo(udma_adc::REG_CR_ADC, cr); + + // Step 4 — t2 + Self::delay_us(20); + + // Step 5 — enable ADC + cr |= 1 << cr_adc::ADC_EN.offset(); + self.csr.wo(udma_adc::REG_CR_ADC, cr); + + // Step 6 — t3 + Self::delay_us(120); + } + + // --- Source selection ---------------------------------------------------- + + /// Reconfigure the analog mux for the given source. + /// + /// Read-modify-writes only the buffer-enable and mux-select bits. + pub fn configure_source(&mut self, source: AdcSource) { + let mut cr = self.csr.r(udma_adc::REG_CR_ADC); + + // Clear mux-related bits in one shot + cr &= !((1u32 << cr_adc::TEMP_BUF_EN.offset()) + | (1u32 << cr_adc::EXT_BUF_EN.offset()) + | ((cr_adc::ADC_SEL.mask() as u32) << cr_adc::ADC_SEL.offset()) + | ((cr_adc::VIN_SEL.mask() as u32) << cr_adc::VIN_SEL.offset())); + + match source { + AdcSource::Temperature => { + cr |= 1 << cr_adc::TEMP_BUF_EN.offset(); + // ADC_SEL = 00 (temperature), VIN_SEL = don't-care + } + AdcSource::Ext(ch) => { + cr |= 1 << cr_adc::EXT_BUF_EN.offset(); + cr |= 1 << cr_adc::ADC_SEL.offset(); // 01 = external + cr |= (ch as u32) << cr_adc::VIN_SEL.offset(); + } + } + + self.csr.wo(udma_adc::REG_CR_ADC, cr); + Self::delay_us(20); + } + + // --- Measurement -------------------------------------------------------- + + /// Blocking single-shot read. Returns the raw 10-bit result (0–1023). + pub fn read_raw(&mut self, source: AdcSource) -> u16 { + self.configure_source(source); + + self.csr.rmwf(udma_adc::REG_RX_CFG_R_RX_CLR, 1); + + // Enqueue a 1-word (4-byte) DMA transfer + #[cfg(feature = "std")] + unsafe { + self.udma_enqueue(Bank::Rx, &self.ifram.as_phys_slice::()[..4], CFG_EN | CFG_SIZE_32); + } + #[cfg(not(feature = "std"))] + unsafe { + self.udma_enqueue(Bank::Rx, &self.ifram.as_phys_slice::()[..4], CFG_EN | CFG_SIZE_32); + } + + while self.udma_busy(Bank::Rx) { + #[cfg(feature = "std")] + xous::yield_slice(); + } + + #[cfg(any(feature = "std", feature = "kernel"))] + let val = self.ifram.as_slice::()[0]; + #[cfg(not(any(feature = "std", feature = "kernel")))] + let val = unsafe { self.ifram.as_phys_slice::()[0] }; + + (val & 0x3FF) as u16 + } + + /// Read `n` samples and return the average (raw 10-bit value). + pub fn read_raw_averaged(&mut self, source: AdcSource, n: usize) -> u16 { + let max_words = ADC_RX_BUF_SIZE / core::mem::size_of::(); + assert!(n > 0 && n <= max_words, "n must be 1..={}", max_words); + + self.configure_source(source); + + self.csr.rmwf(udma_adc::REG_RX_CFG_R_RX_CLR, 1); + + let byte_len = n * core::mem::size_of::(); + #[cfg(feature = "std")] + unsafe { + self.udma_enqueue(Bank::Rx, &self.ifram.as_phys_slice::()[..byte_len], CFG_EN | CFG_SIZE_32); + } + #[cfg(not(feature = "std"))] + unsafe { + self.udma_enqueue(Bank::Rx, &self.ifram.as_phys_slice::()[..byte_len], CFG_EN | CFG_SIZE_32); + } + + while self.udma_busy(Bank::Rx) { + #[cfg(feature = "std")] + xous::yield_slice(); + } + + let mut sum: u32 = 0; + for i in 0..n { + #[cfg(any(feature = "std", feature = "kernel"))] + let val = self.ifram.as_slice::()[i]; + #[cfg(not(any(feature = "std", feature = "kernel")))] + let val = unsafe { self.ifram.as_phys_slice::()[i] }; + + sum += val & 0x3FF; + } + (sum / n as u32) as u16 + } + + /// Arm continuous DMA conversion. The uDMA will repeatedly fill the + /// buffer and wrap. Use the ADC RX interrupt (IRQn 121 - irqarray8::EV_PENDING_ADC_RX) or poll + /// `udma_busy(Bank::Rx)` to track progress. + pub fn start_continuous(&mut self, source: AdcSource) { + self.configure_source(source); + + self.csr.rmwf(udma_adc::REG_RX_CFG_R_RX_CLR, 1); + + #[cfg(feature = "std")] + unsafe { + self.udma_enqueue(Bank::Rx, self.ifram.as_phys_slice::(), CFG_EN | CFG_SIZE_32 | CFG_CONT); + } + #[cfg(not(feature = "std"))] + unsafe { + self.udma_enqueue(Bank::Rx, self.ifram.as_phys_slice::(), CFG_EN | CFG_SIZE_32 | CFG_CONT); + } + } + + /// Stop continuous conversion. + pub fn stop_continuous(&mut self) { self.csr.rmwf(udma_adc::REG_RX_CFG_R_RX_CLR, 1); } + + // --- Conversion helpers ------------------------------------------------- + + /// Raw → temperature in °C (float). + /// + /// Linear regression on the datasheet table + /// T = −0.5877 × raw + 328.68, R² ≈ 0.99997 + pub fn raw_to_temp_celsius(raw: u16) -> f32 { -0.587_744_8 * raw as f32 + 328.679_84 } + + /// Raw → temperature in tenths of °C (integer). + /// e.g. 253 means 25.3 °C. + pub fn raw_to_temp_tenths_c(raw: u16) -> i16 { 3287i16 - (raw as i32 * 587_745 / 100_000) as i16 } + + /// Raw → voltage in volts (bandgap ref, Vbg = 1.208 V). + /// V_in = raw / 1023 × 1.208 + pub fn raw_to_voltage(raw: u16) -> f32 { (raw as f32 / 1023.0) * 1.208 } +} diff --git a/libs/bao1x-hal/src/udma/mod.rs b/libs/bao1x-hal/src/udma/mod.rs index bf2064959..593bb40e4 100644 --- a/libs/bao1x-hal/src/udma/mod.rs +++ b/libs/bao1x-hal/src/udma/mod.rs @@ -1,9 +1,11 @@ +pub mod adc; pub mod i2c; pub mod spim; pub mod uart; use core::mem::size_of; +pub use adc::*; use bao1x_api::*; pub use i2c::*; pub use spim::*;