kirdy/src/thermostat/max1968.rs

370 lines
11 KiB
Rust

use core::marker::PhantomData;
use core::u16;
use crate::thermostat::ad5680;
use fugit::KilohertzU32;
use stm32f4xx_hal::{
adc::{
config::{self, AdcConfig},
Adc,
},
gpio::{gpioa::*, gpiob::*, gpioc::*, Alternate, Analog, Output, PushPull},
hal::{self, blocking::spi::Transfer, digital::v2::OutputPin},
pac::{ADC1, SPI1, TIM4},
rcc::Clocks,
spi::{NoMiso, Spi, TransferModeNormal},
timer::pwm::{PwmChannel, PwmExt},
};
use uom::si::{
electric_current::ampere,
electric_potential::{millivolt, volt},
electrical_resistance::ohm,
f64::{ElectricCurrent, ElectricPotential, ElectricalResistance},
ratio::ratio,
};
pub const PWM_FREQ_KHZ: KilohertzU32 = KilohertzU32::from_raw(20);
pub const R_SENSE: ElectricalResistance = ElectricalResistance {
dimension: PhantomData,
units: PhantomData,
value: 0.05,
};
// Rev 0_2: DAC Chip connects 3V3 reference voltage and thus provide 0-3.3V output range
// TODO: Rev 0_3: DAC Chip connects 3V3 reference voltage,
// which is then passed through a resistor divider to provide 0-3V output range
const DAC_OUT_V_MAX: f64 = 3.3;
const TEC_VSEC_BIAS_V: ElectricPotential = ElectricPotential {
dimension: PhantomData,
units: PhantomData,
value: 1.65,
};
// Kirdy Design Specs:
// MaxV = 5.0V
// MAX Current = +- 1.0A
const MAX_V_DUTY_TO_CURRENT_RATE: ElectricPotential = ElectricPotential {
dimension: PhantomData,
units: PhantomData,
value: 4.0 * 3.3,
};
pub const MAX_V_MAX: ElectricPotential = ElectricPotential {
dimension: PhantomData,
units: PhantomData,
value: 5.0,
};
const MAX_V_DUTY_MAX: f64 = MAX_V_MAX.value / MAX_V_DUTY_TO_CURRENT_RATE.value;
const MAX_I_POS_NEG_DUTY_TO_CURRENT_RATE: ElectricCurrent = ElectricCurrent {
dimension: PhantomData,
units: PhantomData,
value: 1.0 / (10.0 * R_SENSE.value / 3.3),
};
pub const MAX_I_POS_CURRENT: ElectricCurrent = ElectricCurrent {
dimension: PhantomData,
units: PhantomData,
value: 1.0,
};
pub const MAX_I_NEG_CURRENT: ElectricCurrent = ElectricCurrent {
dimension: PhantomData,
units: PhantomData,
value: 1.0,
};
// .get::<ratio>() is not implemented for const
const MAX_I_POS_DUTY_MAX: f64 = MAX_I_POS_CURRENT.value / MAX_I_POS_NEG_DUTY_TO_CURRENT_RATE.value;
const MAX_I_NEG_DUTY_MAX: f64 = MAX_I_NEG_CURRENT.value / MAX_I_POS_NEG_DUTY_TO_CURRENT_RATE.value;
pub trait ChannelPins {
type DacSpi: Transfer<u8>;
type DacSync: OutputPin;
type ShdnPin: OutputPin;
type VRefPin;
type ItecPin;
type DacFeedbackPin;
type VTecPin;
type MaxVPin;
type MaxIPosPin;
type MAXINegPin;
}
pub struct Channel0;
impl ChannelPins for Channel0 {
type DacSpi = DacSpi;
type DacSync = DacSync;
type ShdnPin = PA5<Output<PushPull>>;
type VRefPin = PA6<Analog>;
type ItecPin = PB1<Analog>;
type DacFeedbackPin = PC0<Analog>;
type VTecPin = PB0<Analog>;
type MaxVPin = PwmChannel<TIM4, 1>;
type MaxIPosPin = PwmChannel<TIM4, 2>;
type MAXINegPin = PwmChannel<TIM4, 0>;
}
pub struct MAX1968Phy<C: ChannelPins> {
// state
pub center_pt: ElectricPotential,
pub dac: ad5680::Dac<C::DacSpi, C::DacSync>,
pub shdn: C::ShdnPin,
pub vref_pin: C::VRefPin,
pub itec_pin: C::ItecPin,
pub dac_feedback_pin: C::DacFeedbackPin,
pub vtec_pin: C::VTecPin,
pub max_v: C::MaxVPin,
pub max_i_pos: C::MaxIPosPin,
pub max_i_neg: C::MAXINegPin,
}
pub struct MAX1968PinSet<C: ChannelPins> {
pub dac: ad5680::Dac<C::DacSpi, C::DacSync>,
pub shdn: C::ShdnPin,
pub vref_pin: C::VRefPin,
pub itec_pin: C::ItecPin,
pub dac_feedback_pin: C::DacFeedbackPin,
pub vtec_pin: C::VTecPin,
pub max_v: C::MaxVPin,
pub max_i_pos: C::MaxIPosPin,
pub max_i_neg: C::MAXINegPin,
}
type DacSpi = Spi<SPI1, (PB3<Alternate<5>>, NoMiso, PB5<Alternate<5>>), TransferModeNormal>;
type DacSync = PB4<Output<PushPull>>;
pub struct MaxAdcPins {
pub dac_vfb: PC0<Analog>,
pub vref: PA6<Analog>,
pub itec: PB1<Analog>,
pub vtec: PB0<Analog>,
}
pub struct MAX1968 {
// settings
pub phy: MAX1968Phy<Channel0>,
pub pins_adc: Adc<ADC1>,
}
pub struct PwmPins {
pub max_v0: PwmChannel<TIM4, 1>,
pub max_i_pos0: PwmChannel<TIM4, 2>,
pub max_i_neg0: PwmChannel<TIM4, 0>,
}
enum PwmPinsEnum {
MaxV,
MaxPosI,
MaxNegI,
}
pub enum AdcReadTarget {
VREF,
DacVfb,
ITec,
VTec,
}
impl PwmPins {
fn setup(clocks: Clocks, tim4: TIM4, max_v0: PB7, max_i_pos0: PB8, max_i_neg0: PB6) -> PwmPins {
fn init_pwm_pin<P: hal::PwmPin<Duty = u16>>(pin: &mut P) {
pin.set_duty(0);
pin.enable();
}
let channels = (
max_i_neg0.into_alternate::<2>(),
max_v0.into_alternate::<2>(),
max_i_pos0.into_alternate::<2>(),
);
let (mut max_i_neg0, mut max_v0, mut max_i_pos0) =
tim4.pwm_hz(channels, PWM_FREQ_KHZ.convert(), &clocks).split();
init_pwm_pin(&mut max_v0);
init_pwm_pin(&mut max_i_neg0);
init_pwm_pin(&mut max_i_pos0);
PwmPins {
max_v0,
max_i_pos0,
max_i_neg0,
}
}
}
impl<C: ChannelPins> MAX1968Phy<C> {
pub fn new(pins: MAX1968PinSet<C>) -> Self {
MAX1968Phy {
center_pt: ElectricPotential::new::<volt>(1.5),
dac: pins.dac,
shdn: pins.shdn,
vref_pin: pins.vref_pin,
itec_pin: pins.itec_pin,
dac_feedback_pin: pins.dac_feedback_pin,
vtec_pin: pins.vtec_pin,
max_v: pins.max_v,
max_i_pos: pins.max_i_pos,
max_i_neg: pins.max_i_neg,
}
}
}
impl MAX1968 {
pub fn new(phy_ch0: MAX1968Phy<Channel0>, adc1: ADC1) -> Self {
let config = AdcConfig::default()
.clock(config::Clock::Pclk2_div_2)
.default_sample_time(config::SampleTime::Cycles_480);
let pins_adc = Adc::adc1(adc1, true, config);
MAX1968 {
phy: phy_ch0,
pins_adc: pins_adc,
}
}
pub fn setup(&mut self) {
self.power_down();
let vref = self.adc_read(AdcReadTarget::VREF, 2048);
self.set_center_point(vref);
// Todo: Add Calibration here
self.set_max_v(ElectricPotential::new::<volt>(5.0));
self.set_max_i_pos(ElectricCurrent::new::<ampere>(1.0));
self.set_max_i_neg(ElectricCurrent::new::<ampere>(1.0));
self.set_i(ElectricCurrent::new::<ampere>(0.0));
}
pub fn power_down(&mut self) {
let _ = self.phy.shdn.set_low();
}
pub fn power_up(&mut self) {
let _ = self.phy.shdn.set_high();
}
pub fn set_center_point(&mut self, value: ElectricPotential) {
self.phy.center_pt = value;
}
fn set_dac(&mut self, voltage: ElectricPotential) -> ElectricPotential {
let value = ((voltage / ElectricPotential::new::<volt>(DAC_OUT_V_MAX)).get::<ratio>()
* (ad5680::MAX_VALUE as f64)) as u32;
self.phy.dac.set(value).unwrap();
// TODO: Store the set-ed DAC Voltage Value
voltage
}
pub fn set_i(&mut self, i_tec: ElectricCurrent) -> ElectricCurrent {
let center_point = self.phy.center_pt;
let r_sense = R_SENSE;
let voltage = i_tec * 10.0 * r_sense + center_point;
let voltage = self.set_dac(voltage);
let i_tec = (voltage - center_point) / (10.0 * r_sense);
i_tec
}
// AN4073: ADC Reading Dispersion can be reduced through Averaging
// Upon test, 16 Point Averaging = +-3 LSB Dispersion
fn adc_read(&mut self, adc_read_target: AdcReadTarget, avg_pt: u16) -> ElectricPotential {
let mut sample: u32 = 0;
sample = match adc_read_target {
AdcReadTarget::VREF => {
for _ in (0..avg_pt).rev() {
sample += self.pins_adc.convert(
&self.phy.vref_pin,
stm32f4xx_hal::adc::config::SampleTime::Cycles_480,
) as u32;
}
sample / avg_pt as u32
}
AdcReadTarget::DacVfb => {
for _ in (0..avg_pt).rev() {
sample += self.pins_adc.convert(
&self.phy.dac_feedback_pin,
stm32f4xx_hal::adc::config::SampleTime::Cycles_480,
) as u32;
}
sample / avg_pt as u32
}
AdcReadTarget::ITec => {
for _ in (0..avg_pt).rev() {
sample += self.pins_adc.convert(
&self.phy.itec_pin,
stm32f4xx_hal::adc::config::SampleTime::Cycles_480,
) as u32;
}
sample / avg_pt as u32
}
AdcReadTarget::VTec => {
for _ in (0..avg_pt).rev() {
sample += self.pins_adc.convert(
&self.phy.vtec_pin,
stm32f4xx_hal::adc::config::SampleTime::Cycles_480,
) as u32;
}
sample / avg_pt as u32
}
};
let mv = self.pins_adc.sample_to_millivolts(sample as u16);
ElectricPotential::new::<millivolt>(mv as f64)
}
pub fn get_vref(&mut self) -> ElectricPotential {
self.adc_read(AdcReadTarget::VREF, 16)
}
pub fn get_dac_vfb(&mut self) -> ElectricPotential {
// Fixme: Rev0_2 does not have this feature
unimplemented!()
//self.adc_read(AdcReadTarget:: DacVfb, 1)
}
pub fn get_tec_i(&mut self) -> ElectricCurrent {
(self.adc_read(AdcReadTarget::ITec, 1) - self.phy.center_pt)
/ ElectricalResistance::new::<ohm>(0.4)
}
pub fn get_tec_v(&mut self) -> ElectricPotential {
// Fixme: Rev0_2 has Analog Input Polarity Reversed
// Remove the -ve sign for Rev0_3
-(self.adc_read(AdcReadTarget::VTec, 1) - TEC_VSEC_BIAS_V) * 4.0
}
fn set_pwm(&mut self, pwm_pin: PwmPinsEnum, duty: f64, max_duty: f64) -> f64 {
fn set<P: hal::PwmPin<Duty = u16>>(pin: &mut P, duty: f64) -> f64 {
let max = pin.get_max_duty();
let value = ((duty * (max as f64)) as u16).min(max);
pin.set_duty(value);
pin.enable();
value as f64 / (max as f64)
}
let duty = duty.min(max_duty);
match pwm_pin {
PwmPinsEnum::MaxV => set(&mut self.phy.max_v, duty),
PwmPinsEnum::MaxPosI => set(&mut self.phy.max_i_pos, duty),
PwmPinsEnum::MaxNegI => set(&mut self.phy.max_i_neg, duty),
}
}
pub fn set_max_v(&mut self, max_v: ElectricPotential) -> ElectricPotential {
let duty = (max_v / MAX_V_DUTY_TO_CURRENT_RATE).get::<ratio>();
let duty = self.set_pwm(PwmPinsEnum::MaxV, duty, MAX_V_DUTY_MAX);
duty * MAX_V_DUTY_TO_CURRENT_RATE
}
pub fn set_max_i_pos(&mut self, max_i_pos: ElectricCurrent) -> ElectricCurrent {
let duty = (max_i_pos / MAX_I_POS_NEG_DUTY_TO_CURRENT_RATE).get::<ratio>();
let duty = self.set_pwm(PwmPinsEnum::MaxPosI, duty, MAX_I_POS_DUTY_MAX);
duty * MAX_I_POS_NEG_DUTY_TO_CURRENT_RATE
}
pub fn set_max_i_neg(&mut self, max_i_neg: ElectricCurrent) -> ElectricCurrent {
let duty = (max_i_neg / MAX_I_POS_NEG_DUTY_TO_CURRENT_RATE).get::<ratio>();
let duty = self.set_pwm(PwmPinsEnum::MaxNegI, duty, MAX_I_NEG_DUTY_MAX);
duty * MAX_I_POS_NEG_DUTY_TO_CURRENT_RATE
}
}