Skip to content

Instantly share code, notes, and snippets.

@chmanie
Created February 18, 2022 14:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save chmanie/abe818ba291609dacbd8d0a4f9775969 to your computer and use it in GitHub Desktop.
Save chmanie/abe818ba291609dacbd8d0a4f9775969 to your computer and use it in GitHub Desktop.
MAX11300 rust-embedded driver prototype
use core::cell::RefCell;
use embedded_hal::blocking::spi::{Transfer, Write};
use embedded_hal::digital::v2::OutputPin;
use heapless::Vec;
use seq_macro::seq;
/// Static device id
const DEVICE_ID: u16 = 0x0424;
/// Highest addressable register address
const MAX_ADDRESS: u8 = 0x73;
/// Device ID Register
const REG_DEVICE_ID: u8 = 0x00;
/// Device Control Register
const REG_DEVICE_CTRL: u8 = 0x10;
/// First port config register
const REG_PORT_CONFIG: u8 = 0x20;
/// First ADC data register
const REG_ADC_DATA: u8 = 0x40;
/// First DAC data register
const REG_DAC_DATA: u8 = 0x60;
#[derive(Debug)]
pub enum Error<S, P> {
/// SPI bus error
Spi(S),
/// Pin error
Pin(P),
/// Connection error (device not found)
Conn,
/// Address error (invalid or out of bounds)
Address,
}
/// Available Ports
#[repr(u8)]
#[derive(Clone, Copy)]
pub enum Port {
/// Port 0
P0,
/// Port 1
P1,
/// Port 2
P2,
/// Port 3
P3,
/// Port 4
P4,
/// Port 5
P5,
/// Port 6
P6,
/// Port 7
P7,
/// Port 8
P8,
/// Port 9
P9,
/// Port 10
P10,
/// Port 11
P11,
/// Port 12
P12,
/// Port 13
P13,
/// Port 14
P14,
/// Port 15
P15,
/// Port 16
P16,
/// Port 17
P17,
/// Port 18
P18,
/// Port 19
P19,
}
impl Port {
fn as_config_addr(&self) -> u8 {
REG_PORT_CONFIG + *self as u8
}
pub fn as_u16(&self) -> u16 {
*self as u16
}
}
/// ADC conversion mode selection
#[repr(u8)]
#[derive(Clone, Copy)]
pub enum ADCCTL {
/// Idle mode – The ADC does not perform any conversion (default)
IdleMode,
/// Single sweep – The ADC performs one conversion for each of the ADC-configured ports sequentially. The assertion of CNVT triggers the single sweep. The sweep starts with the ADC-configured port of lowest index and stops with the ADC-configured port of highest index.
SingleSweep,
/// Single conversion – The ADC performs one conversion for the current port. It starts with the lowest index port that is ADC-configured, and it progresses to higher index ports as CNVT is asserted.
SingleConversion,
/// Continuous sweep – This mode is not controlled by CNVT. The ADC continuously sweeps the ADC-configured ports
ContinuousSweep,
}
/// DAC mode selection
#[repr(u8)]
#[derive(Clone, Copy)]
pub enum DACCTL {
/// Sequential update mode for DAC-configured ports (default)
Sequential,
/// Immediate update mode for DAC-configured ports. The DAC-configured port that received new data is the next port to be updated. After updating that port, the DACconfigured port update sequence continues from that port onward. A minimum of 80µs must be observed before requesting another immediate update
Immediate,
/// All DAC-configured ports use the same data stored in DACPRSTDAT1[11:0]
PresetData1,
/// All DAC-configured ports use the same data stored in DACPRSTDAT2[11:0]
PresetData2,
}
/// ADC conversion rate selection
#[repr(u8)]
#[derive(Clone, Copy)]
pub enum ADCCONV {
/// ADC conversion rate of 200ksps (default)
Rate200,
/// ADC conversion rate of 250ksps
Rate250,
/// ADC conversion rate of 333ksps
Rate333,
/// ADC conversion rate of 400ksps
Rate400,
}
/// DAC voltage reference selection
#[repr(u8)]
#[derive(Clone, Copy)]
pub enum DACREF {
/// External reference voltage (default)
ExternalRef,
/// Internal reference voltage
InternalRef,
}
/// Thermal shutdown enable
#[repr(u8)]
#[derive(Clone, Copy)]
pub enum THSHDN {
/// Thermal shutdown function disabled.
Disabled,
// Thermal shutdown function enabled. If the internal temperature monitor is enabled, and if the internal temperature is measured to be larger than 145°C, the device is reset, thus bringing all channels to high-impedance mode and setting all registers to their default value.
Enabled,
}
/// Temperature monitor selection
/// TMPCTL[0]: Internal temperature monitor (0: disabled; 1: enabled)
/// TMPCTL[1]: 1st external temperature monitor (0: disabled; 1: enabled)
/// TMPCTL[2]: 2nd external temperature monitor (0: disabled; 1: enabled)
struct TMPCTL(u8, u8, u8);
impl From<TMPCTL> for u16 {
fn from(tmpctl: TMPCTL) -> Self {
(tmpctl.0 as u16) << 2 | (tmpctl.1 as u16) << 1 | tmpctl.0 as u16
}
}
/// Temperature conversion time control
#[repr(u8)]
#[derive(Clone, Copy)]
pub enum TMPPER {
/// Default conversion time setting. Selected for junction capacitance filter < 100pF.
Default,
/// Extended conversion time setting. Selected for junction capacitance filter from 100pF to 390pF
Extended,
}
/// Temperature sensor series resistor cancellation mode
#[repr(u8)]
#[derive(Clone, Copy)]
pub enum RSCANCEL {
/// Temperature sensor series resistance cancellation disabled.
Disabled,
/// Temperature sensor series resistance cancellation enabled.
Enabled,
}
/// Power mode selection
#[repr(u8)]
#[derive(Clone, Copy)]
pub enum LPEN {
/// Default power mode for normal operations
Default,
/// Lower power mode. The analog ports are in high-impedance mode. The device can be brought out of the lower power mode by deasserting this bit. The device would then undergo the regular power-on sequence
LowPower,
}
/// Serial interface burst-mode selection
#[repr(u8)]
#[derive(Clone, Copy)]
pub enum BRST {
/// Default address incrementing mode. The address is automatically incremented by “1” in burst mode.
Default,
/// Contextual address incrementing mode. In burst mode, the address automatically points to the next ADC- or DAC-configured port data register. Specifically, when reading ADC data (writing DAC data), the serial interface reads (writes to) only the data registers of those ports that are ADC-configured (DAC-configured). This mode applies to ADC data read and DAC data write, not DAC data read.
Contextual,
}
pub struct DeviceConfig {
adcctl: ADCCTL,
dacctl: DACCTL,
adcconv: ADCCONV,
dacref: DACREF,
thshdn: THSHDN,
tmpctl: TMPCTL,
tmpper: TMPPER,
rscancel: RSCANCEL,
lpen: LPEN,
brst: BRST,
}
impl Default for DeviceConfig {
fn default() -> Self {
Self {
adcctl: ADCCTL::IdleMode,
dacctl: DACCTL::Sequential,
adcconv: ADCCONV::Rate200,
dacref: DACREF::ExternalRef,
thshdn: THSHDN::Disabled,
tmpctl: TMPCTL(0, 0, 0),
tmpper: TMPPER::Default,
rscancel: RSCANCEL::Disabled,
lpen: LPEN::Default,
brst: BRST::Default,
}
}
}
impl From<DeviceConfig> for u16 {
fn from(cfg: DeviceConfig) -> Self {
(cfg.brst as u16) << 14
| (cfg.lpen as u16) << 13
| (cfg.rscancel as u16) << 12
| (cfg.tmpper as u16) << 11
| u16::from(cfg.tmpctl) << 8
| (cfg.thshdn as u16) << 7
| (cfg.dacref as u16) << 6
| (cfg.adcconv as u16) << 4
| (cfg.dacctl as u16) << 2
| cfg.adcctl as u16
}
}
/// INV (for GPI-controlled functional modes only). Asserted to invert the data received by the GPI-configured port.
#[repr(u8)]
#[derive(Clone, Copy)]
pub enum INV {
/// Data received from GPI-configured port is not inverted
NotInverted,
/// Data received from GPI-configured port is inverted
Inverted,
}
impl INV {
pub fn mask() -> u16 {
0xf7ff
}
pub fn as_u16(&self) -> u16 {
(*self as u16) << 11
}
}
/// AVR (for ADC-related functional modes only). ADC voltage reference selection.
#[repr(u8)]
#[derive(Clone, Copy)]
pub enum AVR {
/// ADC internal voltage reference
InternalRef,
/// ADC external voltage reference (all modes except mode 6) or DAC voltage reference determined by DACREF (mode 6 only)
ExternalRef,
}
impl AVR {
pub fn mask() -> u16 {
0xf7ff
}
pub fn as_u16(&self) -> u16 {
(*self as u16) << 11
}
}
/// ADC Voltage Range. Determines the input voltage range of ports configured in input modes, or the output voltage range of ports configured in output modes.
#[repr(u8)]
#[derive(Clone, Copy)]
pub enum ADCRANGE {
/// 0 to +10v
Rg0_10v = 1,
/// -5 to +5v
RgNeg5_5v,
/// -10 to 0v
RgNeg10_0v,
/// 0 to 2.5v
Rg0_2v5,
/// 0 to 2.5v
Rg0_2v5_1 = 6,
}
impl ADCRANGE {
pub fn mask() -> u16 {
0xf8ff
}
pub fn as_u16(&self) -> u16 {
(*self as u16) << 8
}
}
/// DAC Voltage Range. Determines the input voltage range of ports configured in input modes, or the output voltage range of ports configured in output modes.
#[repr(u8)]
#[derive(Clone, Copy)]
pub enum DACRANGE {
/// 0 to +10v
Rg0_10v = 1,
/// -5 to +5v
RgNeg5_5v,
/// -10 to 0v
RgNeg10_0v,
/// -5 to +5v
RgNeg5_5v1,
/// 0 to +10v
Rg0_10v1 = 6,
}
impl DACRANGE {
pub fn mask() -> u16 {
0xf8ff
}
pub fn as_u16(&self) -> u16 {
(*self as u16) << 8
}
}
/// # Of Samples (for ADC-related functional modes only). Defines the number of samples to be captured and averaged before loading the result in the port’s ADC data register. The coding of the number of samples is 2# OF SAMPLES. The number of samples to average can be 1, 2, 4, 8, 16, 32, 64, or 128.
#[repr(u8)]
#[derive(Clone, Copy)]
pub enum NSAMPLES {
/// Just take one sample
Samples1,
/// Average over 2 samples
Samples2,
/// Average over 4 samples
Samples4,
/// Average over 8 samples
Samples8,
/// Average over 16 samples
Samples16,
/// Average over 32 samples
Samples32,
/// Average over 64 samples
Samples64,
/// Average over 128 samples
Samples128,
}
impl NSAMPLES {
pub fn mask() -> u16 {
0xff1f
}
pub fn as_u16(&self) -> u16 {
(*self as u16) << 5
}
}
struct SPIBus<SPI, EN> {
spi: SPI,
enable: EN,
}
impl<SPI, EN, S, P> SPIBus<SPI, EN>
where
SPI: Transfer<u8, Error = S> + Write<u8, Error = S>,
EN: OutputPin<Error = P>,
{
fn init(spi: SPI, mut enable: EN) -> Result<Self, Error<S, P>> {
enable.set_high().map_err(Error::Pin)?;
let mut bus = Self { spi, enable };
if bus.read_register(REG_DEVICE_ID)? != DEVICE_ID {
return Err(Error::Conn);
}
Ok(bus)
}
fn read_register(&mut self, address: u8) -> Result<u16, Error<S, P>> {
if address > MAX_ADDRESS {
return Err(Error::Address);
}
self.enable.set_low().map_err(Error::Pin)?;
let mut buf = [(address << 1 | 1), 0, 0];
self.spi.transfer(&mut buf).map_err(Error::Spi)?;
self.enable.set_high().map_err(Error::Pin)?;
Ok((buf[1] as u16) << 8 | buf[2] as u16)
}
fn read_registers<'a>(
&mut self,
start_address: u8,
data: &'a mut [u16],
) -> Result<&'a [u16], Error<S, P>> {
if data.len() > 20 || start_address + data.len() as u8 > MAX_ADDRESS {
return Err(Error::Address);
}
// 1 address byte, 2x20 data bytes maximum
let mut buf: Vec<u8, 41> = Vec::new();
// Actual size of u16 output buffer times two plus address byte
buf.resize(data.len() * 2 + 1, 0).ok();
// Read instruction
buf[0] = start_address << 1 | 1;
// Read values into buf
self.enable.set_low().map_err(Error::Pin)?;
self.spi.transfer(&mut buf).map_err(Error::Spi)?;
self.enable.set_high().map_err(Error::Pin)?;
// Copy to data buffer
for (i, bytes) in buf[1..].chunks(2).enumerate() {
data[i] = (bytes[0] as u16) << 8 | bytes[1] as u16;
}
Ok(data)
}
fn write_register(&mut self, address: u8, data: u16) -> Result<(), Error<S, P>> {
if address > MAX_ADDRESS {
return Err(Error::Address);
}
self.enable.set_low().map_err(Error::Pin)?;
self.spi
.write(&[address << 1, (data >> 8) as u8, (data & 0xff) as u8])
.map_err(Error::Spi)?;
self.enable.set_high().map_err(Error::Pin)?;
Ok(())
}
fn write_registers(&mut self, start_address: u8, data: &[u16]) -> Result<(), Error<S, P>> {
if data.len() > 20 || start_address + data.len() as u8 > MAX_ADDRESS {
return Err(Error::Address);
}
// 1 address byte, 2x20 data bytes maximum
let mut buf: Vec<u8, 41> = Vec::new();
// Actual size of u16 data buffer times two plus address byte
buf.resize(data.len() * 2 + 1, 0).ok();
// Write instruction
buf[0] = start_address << 1;
for (i, &data_u16) in data.iter().enumerate() {
buf[i * 2 + 1] = (data_u16 >> 8) as u8;
buf[i * 2 + 2] = (data_u16 & 0xff) as u8;
}
self.enable.set_low().map_err(Error::Pin)?;
self.spi.write(&buf).map_err(Error::Spi)?;
self.enable.set_high().map_err(Error::Pin)?;
Ok(())
}
}
/// High impedance Mode
pub struct ConfigMode0;
impl ConfigMode0 {
fn as_u16(&self) -> u16 {
0x0
}
}
/// Digital input with programmable threshold, GPI
pub struct ConfigMode1;
impl ConfigMode1 {
fn as_u16(&self) -> u16 {
0x1 << 12
}
}
/// Bidirectional level translator terminal
pub struct ConfigMode2;
impl ConfigMode2 {
fn as_u16(&self) -> u16 {
0x2 << 12
}
}
/// Register-driven digital output with DACcontrolled level, GPO
pub struct ConfigMode3;
impl ConfigMode3 {
fn as_u16(&self) -> u16 {
0x3 << 12
}
}
/// Unidirectional path output with DACcontrolled level, GPO
pub struct ConfigMode4(pub INV, pub Port);
impl ConfigMode4 {
fn as_u16(&self) -> u16 {
0x4 << 12 | self.0.as_u16() | self.1 as u16
}
}
/// Analog output for DAC
pub struct ConfigMode5(pub DACRANGE);
impl ConfigMode5 {
fn as_u16(&self) -> u16 {
0x5 << 12 | self.0.as_u16()
}
}
/// Analog output for DAC with ADC monitoring
pub struct ConfigMode6(pub AVR, pub DACRANGE);
impl ConfigMode6 {
fn as_u16(&self) -> u16 {
0x6 << 12 | self.0.as_u16() | self.1.as_u16()
}
}
/// Positive analog input to single-ended ADC
pub struct ConfigMode7(pub AVR, pub ADCRANGE, pub NSAMPLES);
impl ConfigMode7 {
fn as_u16(&self) -> u16 {
0x7 << 12 | self.0.as_u16() | self.1.as_u16() | self.2.as_u16()
}
}
/// Positive analog input to differential ADC
pub struct ConfigMode8(pub AVR, pub ADCRANGE, pub NSAMPLES, pub Port);
impl ConfigMode8 {
fn as_u16(&self) -> u16 {
0x8 << 12 | self.0.as_u16() | self.1.as_u16() | self.2.as_u16() | self.3.as_u16()
}
}
/// Negative analog input to differential ADC
pub struct ConfigMode9(pub AVR, pub ADCRANGE);
impl ConfigMode9 {
fn as_u16(&self) -> u16 {
0x9 << 12 | self.0.as_u16() | self.1.as_u16()
}
}
/// Analog output for DAC and negative analog input to differential ADC (pseudo-differential mode)
pub struct ConfigMode10(pub AVR, pub DACRANGE);
impl ConfigMode10 {
fn as_u16(&self) -> u16 {
0xa << 12 | self.0.as_u16() | self.1.as_u16()
}
}
/// Terminal to GPIcontrolled analog switch
pub struct ConfigMode11(pub INV, pub Port);
impl ConfigMode11 {
fn as_u16(&self) -> u16 {
0xb << 12 | self.0.as_u16() | self.1.as_u16()
}
}
/// Terminal to registercontrolled analog switch
pub struct ConfigMode12(pub INV, pub Port);
impl ConfigMode12 {
fn as_u16(&self) -> u16 {
0xc << 12
}
}
seq!(N in 0..=12 {
pub struct PortMode~N<'a, SPI, EN> {
config: ConfigMode~N,
port: Port,
bus: &'a RefCell<SPIBus<SPI, EN>>,
}
});
impl<'a, SPI, EN, S, P> PortMode5<'a, SPI, EN>
where
SPI: Transfer<u8, Error = S> + Write<u8, Error = S>,
EN: OutputPin<Error = P>,
{
pub fn set_value(&self, data: u16) -> Result<(), Error<S, P>> {
self.bus
.borrow_mut()
.write_register(REG_DAC_DATA + (self.port as u8), data)
}
pub fn configure_range(&self, range: DACRANGE) -> Result<(), Error<S, P>> {
let data = self.config.as_u16() & DACRANGE::mask() | range.as_u16();
self.bus
.borrow_mut()
.write_register(self.port.as_config_addr(), data)
}
}
impl<'a, SPI, EN, S, P> PortMode7<'a, SPI, EN>
where
SPI: Transfer<u8, Error = S> + Write<u8, Error = S>,
EN: OutputPin<Error = P>,
{
pub fn get_value(&self) -> Result<u16, Error<S, P>> {
self.bus
.borrow_mut()
.read_register(REG_ADC_DATA + (self.port as u8))
}
pub fn configure_avr(&self, avr: AVR) -> Result<(), Error<S, P>> {
let data = self.config.as_u16() & AVR::mask() | avr.as_u16();
self.bus
.borrow_mut()
.write_register(self.port.as_config_addr(), data)
}
pub fn configure_range(&self, range: ADCRANGE) -> Result<(), Error<S, P>> {
let data = self.config.as_u16() & ADCRANGE::mask() | range.as_u16();
self.bus
.borrow_mut()
.write_register(self.port.as_config_addr(), data)
}
pub fn configure_nsamples(&self, nsamples: NSAMPLES) -> Result<(), Error<S, P>> {
let data = self.config.as_u16() & NSAMPLES::mask() | nsamples.as_u16();
self.bus
.borrow_mut()
.write_register(self.port.as_config_addr(), data)
}
}
// TODO: Implement Mode5MultiPort (have to be successive ports)
pub trait ConfigurePort<'a, MODE, CONFIG, S, P> {
fn configure_port(&'a mut self, port: Port, config: CONFIG) -> Result<MODE, Error<S, P>>;
}
pub struct MAX11300<SPI, EN> {
bus: RefCell<SPIBus<SPI, EN>>,
}
impl<SPI, EN, S, P> MAX11300<SPI, EN>
where
SPI: Transfer<u8, Error = S> + Write<u8, Error = S>,
EN: OutputPin<Error = P>,
{
pub fn init(spi: SPI, enable: EN, config: DeviceConfig) -> Result<Self, Error<S, P>> {
let bus = RefCell::new(SPIBus::init(spi, enable)?);
bus.borrow_mut()
.write_register(REG_DEVICE_CTRL, config.into())?;
Ok(Self { bus })
}
pub fn reset(&mut self) -> Result<(), Error<S, P>> {
self.bus
.borrow_mut()
.write_register(REG_DEVICE_CTRL, 1 << 15)
}
}
seq!(N in 0..=12 {
impl<'a, SPI, EN, S, P> ConfigurePort<'a, PortMode~N<'a, SPI, EN>, ConfigMode~N, S, P>
for MAX11300<SPI, EN>
where
SPI: Transfer<u8, Error = S> + Write<u8, Error = S>,
EN: OutputPin<Error = P>,
{
fn configure_port(
&'a mut self,
port: Port,
config: ConfigMode~N,
) -> Result<PortMode~N<'a, SPI, EN>, Error<S, P>> {
self.bus
.borrow_mut()
.write_register(port.as_config_addr(), config.as_u16())?;
Ok(PortMode~N {
config,
port,
bus: &self.bus,
})
}
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment