Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sensors #161

Merged
merged 10 commits into from
Sep 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading