Skip to content

Commit

Permalink
Merge pull request #161 from quartiq/sensors
Browse files Browse the repository at this point in the history
sensors
  • Loading branch information
jordens authored Sep 17, 2024
2 parents b5a327c + 8328875 commit 6c46269
Show file tree
Hide file tree
Showing 8 changed files with 336 additions and 161 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ jobs:
features: ''
continue-on-error: true
- toolchain: nightly
features: nightly
features: ''
continue-on-error: true
steps:
- uses: actions/checkout@v2
Expand Down
3 changes: 1 addition & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ rust-version = "1.75.0"
[features]
default = ["all_differential"]
all_differential = []
ai_artiq = []
nightly = []
all_single_ended = []

[dependencies]
cortex-m = { version = "0.7.7", features = [
Expand Down
18 changes: 10 additions & 8 deletions py/thermostat/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,14 +143,16 @@ async def run():

thermostat = Miniconf(client, prefix)

# TODO: sequence!
await thermostat.set(
f"/output/{args.output}/state",
"Hold",
)
for (
k
) in "pid/min pid/max pid/setpoint pid/ki pid/kp pid/kd pid/li pid/ld voltage_limit weights state".split():
if args.state == "On":
await thermostat.set(
f"/output/{args.output}/state",
"Hold",
)

for k in (
"pid/min pid/max pid/setpoint pid/ki pid/kp pid/kd pid/li pid/ld "
"voltage_limit weights state"
).split():
await thermostat.set(
f"/output/{args.output}/{k}",
getattr(args, k.split("/")[-1]),
Expand Down
186 changes: 106 additions & 80 deletions src/hardware/adc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,6 @@ use super::hal::{
/// Might be extended to support different input types (other NTCs, ref resistors etc.) in the future.
#[derive(Copy, Clone, Debug)]
pub struct AdcCode(u32);
impl AdcCode {
const GAIN: f32 = 0x555555 as _; // Default ADC gain from datasheet.
const R_REF: f32 = 2.0 * 5000.0; // Ratiometric 5.0K high and low side or single ended 10K.
const ZERO_C: f32 = 273.15; // 0°C in °K
const B: f32 = 3988.0; // NTC beta value.
const T_N: f32 = 25.0 + AdcCode::ZERO_C; // Reference Temperature for B-parameter equation.
const R_N: f32 = 10000.0; // TEC resistance at T_N.

// ADC relative full scale per LSB
// Inverted equation from datasheet p. 40 with V_Ref normalized to 1 as this cancels out in resistance.
const FS_PER_LSB: f32 = 0x400000 as f32 / (2.0 * (1 << 23) as f32 * AdcCode::GAIN * 0.75);
// Relative resistance
const R_REF_N: f32 = AdcCode::R_REF / AdcCode::R_N;
}

impl From<u32> for AdcCode {
/// Construct an ADC code from a provided binary (ADC-formatted) code.
Expand All @@ -49,41 +35,12 @@ impl From<AdcCode> for u32 {
}

impl From<AdcCode> for f32 {
/// Convert raw ADC codes to temperature value in °C using the AD7172 input voltage to code
/// relation, the ratiometric resistor setup and the "B-parameter" equation (a simple form of the
/// Steinhart-Hart equation). This is a tradeoff between computation and absolute temperature
/// accuracy. The f32 output dataformat leads to an output quantization of about 31 uK.
/// Additionally there is some error (in addition to the re-quantization) introduced during the
/// various computation steps. If the input data has less than about 5 bit RMS noise, f32 should be
/// avoided.
/// Valid under the following conditions:
/// * Unipolar ADC input
/// * Unchanged ADC GAIN and OFFSET registers (default reset values)
/// * Resistor setup as on Thermostat-EEM breakout board/AI-ARTIQ headboard
/// (either ratiometric 5.0K high and low side or single ended 10K)
/// * Input values not close to minimum/maximum (~1000 codes difference)
///
/// Maybe this will be extended in the future to support more complex temperature sensing configurations.
fn from(code: AdcCode) -> f32 {
let relative_voltage = code.0 as f32 * AdcCode::FS_PER_LSB;
// Voltage divider normalized to V_Ref = 1, inverted to get to NTC resistance.
let relative_resistance = relative_voltage / (1.0 - relative_voltage) * AdcCode::R_REF_N;
// https://en.wikipedia.org/wiki/Thermistor#B_or_%CE%B2_parameter_equation
let temperature_kelvin_inv =
1.0 / AdcCode::T_N + 1.0 / AdcCode::B * relative_resistance.ln();
1.0 / temperature_kelvin_inv - AdcCode::ZERO_C
}
}

impl From<AdcCode> for f64 {
/// Like `From<AdcCode> for f32` but for `f64` and correspondingly higher dynamic range.
fn from(code: AdcCode) -> f64 {
let relative_voltage = (code.0 as f32 * AdcCode::FS_PER_LSB) as f64;
let relative_resistance =
relative_voltage / (1.0 - relative_voltage) * AdcCode::R_REF_N as f64;
let temperature_kelvin_inv =
1.0 / AdcCode::T_N as f64 + 1.0 / AdcCode::B as f64 * relative_resistance.ln();
1.0 / temperature_kelvin_inv - AdcCode::ZERO_C as f64
fn from(value: AdcCode) -> Self {
const GAIN: f32 = 0x555555 as _; // Default ADC gain from datasheet.
// ADC relative full scale per LSB
// Inverted equation from datasheet p. 40 with V_Ref normalized to 1
const FS_PER_LSB: f32 = 0x400000 as f32 / (2.0 * (1 << 23) as f32 * GAIN * 0.75);
value.0 as Self * FS_PER_LSB
}
}

Expand Down Expand Up @@ -134,22 +91,109 @@ pub struct AdcPins {
pub sync: gpiob::PB11<gpio::Output<gpio::PushPull>>,
}

#[derive(Clone, Copy, Debug)]
pub struct Mux {
pub ainpos: ad7172::Mux,
pub ainneg: ad7172::Mux,
}

/// ADC configuration structure.
/// Could be extended with further configuration options for the ADCs in the future.
#[derive(Clone, Copy, Debug)]
pub struct AdcConfig {
/// Configuration for all ADC inputs. Four ADCs with four inputs each.
/// `Some(([AdcInput], [AdcInput]))` positive and negative channel inputs or None to disable the channel.
pub input_config: [[Option<(ad7172::Mux, ad7172::Mux)>; 4]; 4],
pub enum Sensor {
/// code * gain + offset
Linear {
/// Units: output
offset: f32,
/// Units: output/input
gain: f32,
},
/// Beta equation (Steinhart-Hart with c=0)
Ntc {
t0_inv: f32, // inverse reference temperature (1/K)
r_rel: f32, // reference resistor over NTC resistance at t0,
beta_inv: f32, // inverse beta
},
/// DT-670 Silicon diode
Dt670 {
v_ref: f32, // effective reference voltage (V)
},
}

impl Sensor {
pub fn linear(offset: f32, gain: f32) -> Self {
Self::Linear { offset, gain }
}

pub fn ntc(t0: f32, r0: f32, r_ref: f32, beta: f32) -> Self {
Self::Ntc {
t0_inv: 1.0 / (t0 + ZERO_C),
r_rel: r_ref / r0,
beta_inv: 1.0 / beta,
}
}

pub fn dt670(v_ref: f32) -> Self {
Self::Dt670 { v_ref }
}
}

const ZERO_C: f32 = 273.15; // 0°C in °K

impl Default for Sensor {
fn default() -> Self {
Self::linear(0.0, 1.0)
}
}

impl Sensor {
pub fn convert(&self, code: AdcCode) -> f64 {
match self {
Self::Linear { offset, gain } => (f32::from(code) * gain) as f64 + *offset as f64,
Self::Ntc {
t0_inv,
r_rel,
beta_inv,
} => {
// Convert raw ADC codes to temperature value in °C using the AD7172 input voltage to code
// relation, the ratiometric resistor setup and the "B-parameter" equation (a simple form of the
// Steinhart-Hart equation). This is a tradeoff between computation and absolute temperature
// accuracy. A f32 output dataformat leads to an output quantization of about 31 uK.
// Additionally there is some error (in addition to the re-quantization) introduced during the
// various computation steps. If the input data has less than about 5 bit RMS noise, f32 should be
// avoided.
// Valid under the following conditions:
// * Unchanged ADC GAIN and OFFSET registers (default reset values)
// * Input values not close to minimum/maximum (~1000 codes difference)
//
// Voltage divider normalized to V_Ref = 1, inverted to get to NTC resistance.
let relative_voltage = f32::from(code) as f64;
let relative_resistance =
relative_voltage / (1.0 - relative_voltage) * *r_rel as f64;
// https://en.wikipedia.org/wiki/Thermistor#B_or_%CE%B2_parameter_equation
1.0 / (*t0_inv as f64 + *beta_inv as f64 * relative_resistance.ln()) - ZERO_C as f64
}
Self::Dt670 { v_ref } => {
let voltage = f32::from(code) * v_ref;
let curve = &super::dt670::CURVE;
let idx = curve.partition_point(|&(_t, v, _dvdt)| v < voltage);
curve
.get(idx)
.or(curve.last())
.map(|&(t, v, dvdt)| (t + (voltage - v) * 1.0e3 / dvdt) as f64)
.unwrap()
}
}
}
}

pub type AdcConfig = [[Option<(Mux, Sensor)>; 4]; 4];

/// Full Adc structure which holds all the ADC peripherals and auxillary pins on Thermostat-EEM and the configuration.
pub struct Adc {
adcs: ad7172::Ad7172<hal::spi::Spi<hal::stm32::SPI4, hal::spi::Enabled>>,
cs: [gpio::ErasedPin<gpio::Output>; 4],
rdyn: gpioc::PC11<gpio::Input>,
sync: gpiob::PB11<gpio::Output<gpio::PushPull>>,
config: AdcConfig,
}

impl Adc {
Expand All @@ -167,7 +211,7 @@ impl Adc {
spi4_rec: rcc::rec::Spi4,
spi4: stm32::SPI4,
pins: AdcPins,
config: AdcConfig,
config: &AdcConfig,
) -> Result<Self, Error> {
let rdyn_pullup = pins.rdyn.internal_pull_up(true);
// SPI MODE_3: idle high, capture on second transition
Expand All @@ -179,15 +223,14 @@ impl Adc {
cs: pins.cs,
rdyn: rdyn_pullup,
sync: pins.sync,
config,
};

adc.setup(delay, config)?;
Ok(adc)
}

/// Setup all ADCs to the specifies [AdcConfig].
fn setup(&mut self, delay: &mut impl DelayUs<u16>, config: AdcConfig) -> Result<(), Error> {
fn setup(&mut self, delay: &mut impl DelayUs<u16>, config: &AdcConfig) -> Result<(), Error> {
// deassert all CS first
for pin in self.cs.iter_mut() {
pin.set_state(PinState::High);
Expand All @@ -198,31 +241,14 @@ impl Adc {

for phy in AdcPhy::iter() {
log::info!("AD7172 {:?}", phy);
self.selected(phy, |adc| {
adc.setup_adc(delay, config.input_config[phy as usize])
})?;
self.selected(phy, |adc| adc.setup_adc(delay, &config[phy as usize]))?;
}

// set sync high after initialization of all ADCs
self.sync.set_high();
Ok(())
}

/// Returns the configuration of which ADC channels are enabled.
pub fn channels(&self) -> [[bool; 4]; 4] {
let mut result = [[false; 4]; 4];
for (cfg, ch) in self
.config
.input_config
.iter()
.flatten()
.zip(result.iter_mut().flatten())
{
*ch = cfg.is_some();
}
result
}

/// Call a closure while the given `AdcPhy` is selected (while its chip
/// select is asserted).
fn selected<F, R>(&mut self, phy: AdcPhy, func: F) -> R
Expand Down Expand Up @@ -362,7 +388,7 @@ impl Adc {
fn setup_adc(
&mut self,
delay: &mut impl DelayUs<u16>,
input_config: [Option<(ad7172::Mux, ad7172::Mux)>; 4],
input_config: &[Option<(Mux, Sensor)>; 4],
) -> Result<(), Error> {
self.adcs.reset();
delay.delay_us(500);
Expand Down Expand Up @@ -402,9 +428,9 @@ impl Adc {
ad7172::Register::CH3,
]) {
let ch = ad7172::Channel::DEFAULT;
let ch = if let Some(cfg) = cfg {
ch.with_ainneg(cfg.1)
.with_ainpos(cfg.0)
let ch = if let Some((mux, _sensor)) = cfg {
ch.with_ainneg(mux.ainneg)
.with_ainpos(mux.ainpos)
.with_setup_sel(u2::new(0)) // only Setup 0 for now
.with_en(true)
} else {
Expand Down
Loading

0 comments on commit 6c46269

Please sign in to comment.