Add Sonata ADC driver
This commit adds a simple Analogue-to-Digital Converter (ADC) driver for
Sonata. For now this only exposes a small subset of the XADC
functionality so that software is able to read XADC registers, modify
the clock speed, and power down the ADC to reduce power draw. The driver
supports extension to add and expose more features as is needed.
diff --git a/sdk/include/platform/sunburst/platform-adc.hh b/sdk/include/platform/sunburst/platform-adc.hh
new file mode 100644
index 0000000..242576c
--- /dev/null
+++ b/sdk/include/platform/sunburst/platform-adc.hh
@@ -0,0 +1,353 @@
+#pragma once
+#include <debug.hh>
+#include <stdint.h>
+#include <utils.hh>
+
+/**
+ * A simple driver for Sonata's XADC (Xilinx Analogue to Digital Converter).
+ *
+ * Documentation source can be found at:
+ * https://github.com/lowRISC/sonata-system/blob/97a525c48f7bf051b999d0178dba04859819bc5e/doc/ip/adc.md
+ *
+ * Rendered documentation is served from:
+ * https://lowrisc.github.io/sonata-system/doc/ip/adc.html
+ */
+class SonataAnalogueDigitalConverter : private utils::NoCopyNoMove
+{
+ /**
+ * Flag to set when debugging the driver for UART log messages.
+ */
+ static constexpr bool DebugDriver = true;
+
+ /**
+ * Helper for conditional debug logs and assertions.
+ */
+ using Debug = ConditionalDebug<DebugDriver, "ADC">;
+
+ /**
+ * Results of measurements / analogue conversions are stored in Dynamic
+ * Reconfiguration Port (DRP) status registers as most-significant-bit
+ * justified 12 bit values, represented by this mask.
+ */
+ static constexpr uint16_t MeasurementMask = 0xFFF0;
+
+ /**
+ * The location (offset) of the Xilinx Analogue-to-Digital Converter's
+ * Dynamic Reconfiguration Port (DRP) registers, which are 16-bit registers
+ * that are sequentially mapped to memory in 4-byte (word) intervals, in the
+ * lower 2 bytes of each word. This includes both status (read-only) and
+ * control (read/write) registers.
+ *
+ * https://docs.amd.com/r/en-US/ug480_7Series_XADC/XADC-Register-Interface
+ */
+ enum class RegisterOffset : uint8_t
+ {
+ Temperature = 0x00,
+ VoltageInternalSupply = 0x01,
+ VoltageAuxiliarySupply = 0x02,
+ VoltageDedicated = 0x03,
+ VoltageInternalPositiveReference = 0x04,
+ VoltageInternalNegativeReference = 0x05,
+ VoltageBlockRamSupply = 0x06,
+
+ /* Offset 0x07 is undefined. */
+
+ /* ADC A's calibration coefficient status registers omitted. */
+
+ /* Offsets 0x0B and 0x0C are undefined. */
+
+ /* Zynq-700 SoC-specific voltage status registers omitted. */
+
+ VoltageAuxiliary0 = 0x10,
+ VoltageAuxiliary1 = 0x11,
+ VoltageAuxiliary2 = 0x12,
+ VoltageAuxiliary3 = 0x13,
+ VoltageAuxiliary4 = 0x14,
+ VoltageAuxiliary5 = 0x15,
+ VoltageAuxiliary6 = 0x16,
+ VoltageAuxiliary7 = 0x17,
+ VoltageAuxiliary8 = 0x18,
+ VoltageAuxiliary9 = 0x19,
+ VoltageAuxiliary10 = 0x1A,
+ VoltageAuxiliary11 = 0x1B,
+ VoltageAuxiliary12 = 0x1C,
+ VoltageAuxiliary13 = 0x1D,
+ VoltageAuxiliary14 = 0x1E,
+ VoltageAuxiliary15 = 0x1F,
+
+ /* Maximum & minimum sensor measurement status registers omitted. */
+
+ /* Offsets 0x2B and 0x2F are undefined. */
+
+ /* ADC B's calibration coefficient status registers omitted. */
+
+ /* Offsets 0x33 to 0x3E are undefined. */
+
+ FlagRegister = 0x3F,
+ ConfigRegister0 = 0x40,
+ ConfigRegister1 = 0x41,
+ ConfigRegister2 = 0x42,
+
+ /* 0x43 to 0x47 are factory test registers and so are omitted. */
+
+ /* Sequence and Alarm control registers are emitted. */
+ };
+
+ /**
+ * Definitions of fields (and their locations) within the Xilinx
+ * Analogue-to-digital Converter's Config Register 2 (offset 0x42).
+ *
+ * https://docs.amd.com/r/en-US/ug480_7Series_XADC/Control-Registers?section=XREF_53021_Configuration
+ */
+ enum ConfigRegister2Field : uint16_t
+ {
+ /* Bits 0-3 are invalid and should not be interacted with. */
+
+ /**
+ * Power-down bits for the Analogue-to-Digital Converter.
+ */
+ PowerDownMask = 0x3 << 4,
+
+ /* Bits 6-7 are invalid and should not be interacted with. */
+
+ /**
+ * Bits used to select the division ratio between the Dynamic
+ * Reconfiguration Port clock (DCLK) and the lower frequency
+ * Analogue-to- Digital Converter Clock (ADCCLK). Values of 0 and 1 are
+ * mapped to a divider of 2 by the DCLK divider selection specification.
+ * All other values are mapped identically; the minimum division ratio
+ * is 2.
+ */
+ ClockDividerMask = 0xFF << 8,
+ };
+
+ /**
+ * A helper that returns a pointer to the Analogue-to-Digital Converter
+ * (ADC)'s memory, which is mapped to the Dynamic Reconfiguration Port (DRP)
+ * registers used by the ADC.
+ */
+ [[nodiscard, gnu::always_inline]] volatile uint32_t *registers() const
+ {
+ return MMIO_CAPABILITY(uint32_t, adc);
+ }
+
+ /**
+ * Read the contents of a Dynamic Reconfiguration Port (DRP) register from
+ * memory. DRP registers are 16 bits wide but are mapped sequentially in
+ * memory, using 4 bytes per register, with the relevant data written in the
+ * lower 16 bits.
+ *
+ * The single argument is the register to read the value of.
+ */
+ [[nodiscard]] uint16_t register_read(RegisterOffset reg) const
+ {
+ uint8_t registerOffset = static_cast<uint8_t>(reg);
+ return static_cast<uint16_t>(registers()[registerOffset]);
+ }
+
+ /**
+ * Write to the contents of a Dynamic Reconfiguration Port (DRP) register in
+ * memory. DRP Registers are 16 bits wide but are mapped sequentially in
+ * memory, using 4 bytes per register, with the relevant data written to the
+ * lower 16 bits.
+ *
+ * The first argument is the register to write to.
+ * The second argument is the value to write to the register.
+ */
+ void register_write(RegisterOffset reg, uint16_t value) const
+ {
+ uint8_t registerOffset = static_cast<uint8_t>(reg);
+ registers()[registerOffset] = static_cast<uint32_t>(value);
+ }
+
+ /**
+ * Sets the relevant bits of a Dynamic Reconfiguration Port (DRP) register
+ * in memory. This acts like `register_write`, but additionally takes a
+ * mask so that it only overwrites specified bits of the register, and
+ * retains the value of all unselected bits.
+ *
+ * The first argument is the register to write to.
+ * The second argument is the mask of bits in the register to write to.
+ * The third argument is the values that will be written to the register
+ * according to the mask.
+ */
+ void register_set_bits(RegisterOffset reg,
+ uint16_t bitMask,
+ uint16_t bitValues) const
+ {
+ uint16_t registerBits = register_read(reg);
+ registerBits &= ~bitMask; /* Clear bits in the mask. */
+ registerBits |= bitMask & bitValues; /* Set values of masked bits. */
+ return register_write(reg, registerBits);
+ }
+
+ public:
+ /**
+ * Represents the offsets of the Xilinx Analogue-to-Digital Converter's
+ * (XADC) Dynamic Reconfiguration Port (DRP) status registers that are used
+ * to store values that are measured/sampled by the XADC, forming a mapping
+ * that provides a more comprehensible interface.
+ *
+ * https://lowrisc.github.io/sonata-system/doc/ip/adc.html
+ */
+ enum class MeasurementRegister : uint8_t
+ {
+ ArduinoA0 = static_cast<uint8_t>(RegisterOffset::VoltageAuxiliary4),
+ ArduinoA1 = static_cast<uint8_t>(RegisterOffset::VoltageAuxiliary12),
+ ArduinoA2 = static_cast<uint8_t>(RegisterOffset::VoltageAuxiliary5),
+ ArduinoA3 = static_cast<uint8_t>(RegisterOffset::VoltageAuxiliary13),
+ ArduinoA4 = static_cast<uint8_t>(RegisterOffset::VoltageAuxiliary6),
+ ArduinoA5 = static_cast<uint8_t>(RegisterOffset::VoltageAuxiliary14),
+ Temperature = static_cast<uint8_t>(RegisterOffset::Temperature),
+ VoltageInternalSupply =
+ static_cast<uint8_t>(RegisterOffset::VoltageInternalSupply),
+ VoltageAuxiliarySupply =
+ static_cast<uint8_t>(RegisterOffset::VoltageAuxiliarySupply),
+ VoltageInternalReferencePositive = static_cast<uint8_t>(
+ RegisterOffset::VoltageInternalPositiveReference),
+ VoltageInternalReferenceNegative = static_cast<uint8_t>(
+ RegisterOffset::VoltageInternalNegativeReference),
+ VoltageBlockRamSupply =
+ static_cast<uint8_t>(RegisterOffset::VoltageBlockRamSupply),
+ };
+
+ /**
+ * Possible power down modes that can be set in Config Register 2.
+ *
+ * https://docs.amd.com/r/en-US/ug480_7Series_XADC/Control-Registers?section=XREF_93518_Power_Down
+ */
+ enum class PowerDownMode : uint8_t
+ {
+ None = 0b00, /* Default */
+ /* 0b01 is not valid, and should not be selected. */
+ ConverterB = 0b10,
+ BothConverters = 0b11,
+ };
+
+ /**
+ * The Xilinx Analogue-to-Digtal Converter can sample at a maximum rate of 1
+ * Megasample per second. It uses 16 bit Dynamic Reconfiguration Port (DRP)
+ * registers, and stores 12-bit measurements, which are stored most-
+ * significant-bit justified.
+ *
+ * https://docs.amd.com/r/en-US/ug480_7Series_XADC/XADC-Overview
+ */
+ static constexpr size_t MaxSamples = 1 * 1000 * 1000;
+ static constexpr size_t RegisterSize = 16;
+ static constexpr size_t MeasurementBitWidth = 12;
+
+ /**
+ * The minimum permissible Analogue-to-Digital Clock speed is 1 MHz, and the
+ * maximum is 26 MHz, as per the data sheet.
+ *
+ * See table 65 on page 57:
+ * https://docs.amd.com/v/u/en-US/ds181_Artix_7_Data_Sheet
+ */
+ static constexpr size_t MinClockFrequencyHz = 1 * 1000 * 1000;
+ static constexpr size_t MaxClockFrequencyHz = 26 * 1000 * 1000;
+
+ /**
+ * A clock divider value to be used by the Xilinx Analogue-to-Digital
+ * Converter (XADC), which divides its input clock (the system clock)
+ * by this divider to determine the clock of the XADC. This clock
+ * must fall between specified maximum and minimum frequency, and the
+ * divider must be an 8-bit integer, greater than 2.
+ */
+ typedef uint8_t ClockDivider;
+
+ /**
+ * Constructor - initialises the MMIO capability for the Analogue-to-
+ * Digital converter, and then sets its clock divider and power down
+ * mode.
+ *
+ * Takes the clock divider and power down modes to initialise. The
+ * clock divider should divide the system clock to create a signal
+ * between 1 and 26 MHz, and should be at least 2.
+ */
+ SonataAnalogueDigitalConverter(ClockDivider divider,
+ PowerDownMode powerDown)
+ {
+ set_clock_divider(divider);
+ set_power_down(powerDown);
+ /* The analogue-to-digital converter starts up in independent ADC mode
+ by default, monitoring all channels, and so no further initialisation
+ logic is needed. */
+ }
+
+ /**
+ * Constructor, without any manual setting of the clock divider. Initialises
+ * the MMIO capability for the Analogue-to-Digtal converter, and then sets
+ * its power down mode.
+ *
+ * Takes the power down mode to initialise.
+ */
+ SonataAnalogueDigitalConverter(PowerDownMode powerDown)
+ {
+ set_power_down(powerDown);
+ /* The Analogue-to-digital converter starts up in independent ADC mode
+ by default, monitoring all channels, and so no further initialisation
+ logic is needed. */
+ }
+
+ /**
+ * Sets the clock divider for the Sonata Analogue-to-Digital Converter
+ * (ADC), which is used to divide the system clock to create the ADC clock.
+ *
+ * The clock divider to use is provided as the single argument. It must be
+ * such that it creates a signal between 1 and 26 MHz, and should be at
+ * least 2.
+ */
+ void set_clock_divider(ClockDivider divider)
+ {
+ Debug::Assert((divider >= 2), "The ADC divider must be at least 2");
+ Debug::Assert(
+ (CPU_TIMER_HZ / divider >= MinClockFrequencyHz),
+ "The given divider causes the ADC clock to underclock its minimum.");
+ Debug::Assert(
+ (CPU_TIMER_HZ / divider <= MaxClockFrequencyHz),
+ "The given divider causes the ADC clock to overclock its maximum.");
+ register_set_bits(
+ RegisterOffset::ConfigRegister2, ClockDividerMask, (divider << 8));
+ }
+
+ /**
+ * Sets the power down configuration for the Sonata Analogue-to-Digital
+ * Converter (ADC). Calling this function allows either ADC B or the entire
+ * XADC to be **permanently** powered down.
+ *
+ * The power down mode to set is provided as an argument.
+ */
+ void set_power_down(PowerDownMode powerdown)
+ {
+ register_set_bits(RegisterOffset::ConfigRegister2,
+ PowerDownMask,
+ (static_cast<uint16_t>(powerdown) << 4));
+ }
+
+ /**
+ * Reads the most recent output of the analogue-to-digital converter's
+ * measurements from a specified status register, corresponding to
+ * measurement of some channel. This currently assumes unipolar operation,
+ * which means all values are positive.
+ *
+ * The status register to read the last measurement of is given as the
+ * single argument.
+ *
+ * The output values can be translated to the relevant units being measured
+ * by using the transfer functions defined in documentation:
+ * https://docs.amd.com/r/en-US/ug480_7Series_XADC/ADC-Transfer-Functions
+ */
+ [[nodiscard]] int16_t read_last_measurement(MeasurementRegister reg) const
+ {
+ uint16_t measurement = register_read(static_cast<RegisterOffset>(reg));
+ measurement &= MeasurementMask;
+ measurement >>= (RegisterSize - MeasurementBitWidth);
+
+ /* Currently just simple logic that assumes a unipolar analogue
+ measurement: to extend support to bipolar measurement, a mechanism for
+ tracking whether a measurement is bipolar or not must be introduced, and
+ if so, then the negative 12-bit two's complement values must be sign
+ extended first. */
+ return static_cast<int16_t>(measurement);
+ }
+};