Compare commits
21 Commits
e355e83d28
...
5bea3f0e5f
Author | SHA1 | Date | |
---|---|---|---|
5bea3f0e5f | |||
af283b17ac | |||
c3022e9db1 | |||
c02181c80c | |||
095fe8ea69 | |||
a0b67cdb09 | |||
381f25f036 | |||
c768bdc93a | |||
0d12c902fc | |||
3cfdee917a | |||
5f582be143 | |||
2f7ca2a706 | |||
ff3d9b790a | |||
bc7bf9a6e7 | |||
ca110962f7 | |||
d0f226ce03 | |||
f49fd08c69 | |||
ccaf728c75 | |||
3ac287ace2 | |||
cdf900a5b6 | |||
af8d361b95 |
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -366,6 +366,8 @@ name = "kirdy"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"bare-metal 1.0.0",
|
||||
"bit_field",
|
||||
"byteorder",
|
||||
"cortex-m",
|
||||
"cortex-m-log",
|
||||
"cortex-m-rt",
|
||||
|
@ -34,6 +34,8 @@ rtt-target = { version = "0.3.1", features = ["cortex-m"] }
|
||||
miniconf = "0.6.3"
|
||||
serde = { version = "1.0.158", features = ["derive"], default-features = false }
|
||||
sfkv = "0.1"
|
||||
bit_field = "0.10"
|
||||
byteorder = { version = "1", default-features = false }
|
||||
[features]
|
||||
semihosting = ["cortex-m-log/semihosting"]
|
||||
RTT = []
|
||||
|
@ -1,8 +1,7 @@
|
||||
use core::marker::PhantomData;
|
||||
use super::{gpio, sys_timer, usb};
|
||||
use crate::device::flash_store::{self, FlashStore};
|
||||
use crate::laser_diode::current_sources::{*};
|
||||
use crate::laser_diode::ld_drive::{self, LdDrive};
|
||||
use crate::laser_diode::ld_ctrl::{*};
|
||||
use crate::laser_diode::laser_diode::LdDrive;
|
||||
use crate::thermostat::max1968::MAX1968;
|
||||
use crate::thermostat::thermostat::Thermostat;
|
||||
use fugit::ExtU32;
|
||||
@ -41,7 +40,7 @@ pub fn bootup(
|
||||
|
||||
sys_timer::setup(core_perif.SYST, clocks);
|
||||
|
||||
let (_eth_pins, usb, current_source_phy, max1968_phy) = gpio::setup(
|
||||
let (_eth_pins, usb, current_source_phy, ad7172_phy, max1968_phy, pd_mon_phy) = gpio::setup(
|
||||
clocks,
|
||||
perif.TIM4,
|
||||
perif.GPIOA,
|
||||
@ -51,6 +50,7 @@ pub fn bootup(
|
||||
perif.GPIOG,
|
||||
perif.SPI1,
|
||||
perif.SPI2,
|
||||
perif.SPI3,
|
||||
perif.OTG_FS_GLOBAL,
|
||||
perif.OTG_FS_DEVICE,
|
||||
perif.OTG_FS_PWRCLK,
|
||||
@ -58,21 +58,18 @@ pub fn bootup(
|
||||
|
||||
usb::State::setup(usb);
|
||||
|
||||
let current_source = CurrentSource::new(current_source_phy, perif.ADC2);
|
||||
let current_source = LdCtrl::new(current_source_phy);
|
||||
|
||||
let mut laser = LdDrive::new(current_source);
|
||||
let mut laser = LdDrive::new(current_source, perif.ADC2, pd_mon_phy);
|
||||
laser.setup();
|
||||
laser.ld_open();
|
||||
laser.power_up();
|
||||
laser.set_ld_drive_current_limit(ElectricCurrent::new::<ampere>(0.2));
|
||||
laser.ld_set_i(ElectricCurrent::new::<ampere>(0.15));
|
||||
|
||||
// Set a non-zero value so that there is a non NAN power reading
|
||||
laser.set_pd_i_to_out_pwr(ld_drive::IToPowerUnit {dimension: PhantomData, units: PhantomData, value: 0.001});
|
||||
laser.power_up();
|
||||
|
||||
let tec_driver = MAX1968::new(max1968_phy, perif.ADC1);
|
||||
|
||||
let mut thermostat = Thermostat::new(tec_driver);
|
||||
let mut thermostat = Thermostat::new(tec_driver, ad7172_phy);
|
||||
thermostat.setup();
|
||||
|
||||
thermostat.power_up();
|
||||
|
@ -1,13 +1,15 @@
|
||||
use crate::laser_diode::current_sources::{self, CurrentSourcePhy};
|
||||
use crate::laser_diode::ld_ctrl::{self, LdCtrlPhy};
|
||||
use crate::laser_diode::max5719;
|
||||
use crate::laser_diode::analog_wdg::LdAnalogWdgPhy;
|
||||
use crate::thermostat::ad5680;
|
||||
use crate::thermostat::max1968::{self, MAX1968PinSet, MAX1968Phy, PWM_FREQ_KHZ};
|
||||
use crate::thermostat::ad7172;
|
||||
use stm32_eth::EthPins;
|
||||
use stm32f4xx_hal::{
|
||||
gpio::{gpioa::*, gpiob::*, gpioc::*, gpiog::*, GpioExt, Input},
|
||||
otg_fs::USB,
|
||||
pac::{
|
||||
GPIOA, GPIOB, GPIOC, GPIOD, GPIOG, OTG_FS_DEVICE, OTG_FS_GLOBAL, OTG_FS_PWRCLK, SPI1, SPI2,
|
||||
GPIOA, GPIOB, GPIOC, GPIOD, GPIOG, OTG_FS_DEVICE, OTG_FS_GLOBAL, OTG_FS_PWRCLK, SPI1, SPI2, SPI3,
|
||||
TIM4,
|
||||
},
|
||||
rcc::Clocks,
|
||||
@ -28,15 +30,17 @@ pub fn setup(
|
||||
gpiog: GPIOG,
|
||||
spi1: SPI1,
|
||||
spi2: SPI2,
|
||||
spi3: SPI3,
|
||||
otg_fs_global: OTG_FS_GLOBAL,
|
||||
otg_fs_device: OTG_FS_DEVICE,
|
||||
otg_fs_pwrclk: OTG_FS_PWRCLK,
|
||||
) -> (
|
||||
EthernetPins,
|
||||
USB,
|
||||
CurrentSourcePhy<current_sources::Channel0>,
|
||||
LdCtrlPhy<ld_ctrl::Channel0>,
|
||||
ad7172::AdcPhy,
|
||||
MAX1968Phy<max1968::Channel0>,
|
||||
// thermostat_phy
|
||||
LdAnalogWdgPhy
|
||||
) {
|
||||
let gpioa = gpioa.split();
|
||||
let gpiob = gpiob.split();
|
||||
@ -63,7 +67,7 @@ pub fn setup(
|
||||
rx_d1: gpioc.pc5,
|
||||
};
|
||||
|
||||
let current_source_phy = CurrentSourcePhy {
|
||||
let current_source_phy = LdCtrlPhy {
|
||||
dac: max5719::Dac::new(Spi::new(
|
||||
spi2,
|
||||
(
|
||||
@ -77,11 +81,13 @@ pub fn setup(
|
||||
), gpiod.pd8.into_push_pull_output(),
|
||||
gpiob.pb14.into_push_pull_output(),
|
||||
),
|
||||
current_source_ldo_en_pin: gpiod.pd9.into_push_pull_output(),
|
||||
current_source_short_pin: gpioa.pa4.into_push_pull_output(),
|
||||
pd_mon_pin: gpioa.pa3.into_analog()
|
||||
};
|
||||
|
||||
let pd_mon_phy = LdAnalogWdgPhy {
|
||||
_pd_mon_ch0: gpioa.pa3.into_analog(),
|
||||
pwr_en_ch0: gpiod.pd9.into_push_pull_output(),
|
||||
};
|
||||
|
||||
let pwm_chs = (
|
||||
gpiob.pb6.into_alternate(),
|
||||
@ -109,14 +115,26 @@ pub fn setup(
|
||||
shdn: gpioa.pa5.into_push_pull_output(),
|
||||
vref_pin: gpioa.pa6.into_analog(),
|
||||
itec_pin: gpiob.pb1.into_analog(),
|
||||
// Fixme: Flywire is added to Rev0_2 prototype. In Rev0_3, it is connected to PC0
|
||||
//dac_feedback_pin: gpioc.pc0.into_analog(),
|
||||
dac_feedback_pin: gpioc.pc3.into_analog(),
|
||||
dac_feedback_pin: gpioc.pc0.into_analog(),
|
||||
vtec_pin: gpiob.pb0.into_analog(),
|
||||
max_v: max_v0,
|
||||
max_i_pos: max_i_pos0,
|
||||
max_i_neg: max_i_neg0,
|
||||
});
|
||||
|
||||
(eth_pins, usb, current_source_phy, max1968_phy)
|
||||
let ad7172_phy = ad7172::Adc::new(
|
||||
Spi::new(spi3,
|
||||
(
|
||||
gpioc.pc10.into_alternate(),
|
||||
gpioc.pc11.into_alternate(),
|
||||
gpioc.pc12.into_alternate(),
|
||||
),
|
||||
ad7172::SPI_MODE,
|
||||
ad7172::SPI_CLOCK_MHZ.convert(),
|
||||
&clocks
|
||||
),
|
||||
gpioa.pa15.into_push_pull_output(),
|
||||
).unwrap();
|
||||
|
||||
(eth_pins, usb, current_source_phy, ad7172_phy, max1968_phy, pd_mon_phy)
|
||||
}
|
||||
|
208
src/laser_diode/analog_wdg.rs
Normal file
208
src/laser_diode/analog_wdg.rs
Normal file
@ -0,0 +1,208 @@
|
||||
// stm32f4xx_hal does not provide config and driver for analog watchdog yet
|
||||
use stm32f4xx_hal::{
|
||||
pac::{ADC2, NVIC},
|
||||
gpio::{Analog, Output, PushPull, gpioa::PA3, gpiod::PD9},
|
||||
interrupt,
|
||||
};
|
||||
use uom::si::electric_potential::millivolt;
|
||||
use uom::si::f64::ElectricPotential;
|
||||
|
||||
// 12 bit Resolution
|
||||
const MAX_SAMPLE: u16 = 4095;
|
||||
|
||||
pub type LdPwrEnPinType = PD9<Output<PushPull>>;
|
||||
pub type PdMonAdcPinType = PA3<Analog>;
|
||||
|
||||
static mut ANALOG_WDG: Option<LdAnalogWdg> = None;
|
||||
|
||||
pub struct LdAnalogWdgPhy {
|
||||
// To make sure PA3 is configured to Analog mode
|
||||
pub _pd_mon_ch0: PdMonAdcPinType,
|
||||
pub pwr_en_ch0: LdPwrEnPinType,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AlarmStatus {
|
||||
alarm: bool,
|
||||
val: u16,
|
||||
}
|
||||
|
||||
pub struct LdAnalogWdg {
|
||||
pac: ADC2,
|
||||
phy: LdAnalogWdgPhy,
|
||||
alarm_status: AlarmStatus,
|
||||
//Calibrated VDDA in millivolt from Adc<ADC1>.calibrate()
|
||||
calibrated_vdda: u32,
|
||||
}
|
||||
|
||||
impl LdAnalogWdg {
|
||||
/// ADC interrupt is disabled and continuous conversion is started by default
|
||||
pub fn setup(pac: ADC2, phy: LdAnalogWdgPhy){
|
||||
unsafe { ANALOG_WDG = Some(LdAnalogWdg{
|
||||
pac: pac,
|
||||
phy: phy,
|
||||
alarm_status: AlarmStatus {
|
||||
alarm: false,
|
||||
val: 0x0000,
|
||||
},
|
||||
calibrated_vdda: 3300,
|
||||
});
|
||||
}
|
||||
|
||||
unsafe {
|
||||
if let Some(ref mut wdg ) = LdAnalogWdg::get() {
|
||||
wdg.pac.cr1.write(|w| w
|
||||
// 12 Bit Resolution
|
||||
.res().twelve_bit()
|
||||
// Enable Analog Watchdog on Single Regular Channel
|
||||
.awden().enabled()
|
||||
.awdsgl().single_channel()
|
||||
.jawden().disabled()
|
||||
// Disable Analog Watchdog Interrupt
|
||||
.awdie().disabled()
|
||||
// Select Analog Watchdog Channel 3 (PA3) on ADC2
|
||||
.awdch().bits(0x03)
|
||||
);
|
||||
wdg.pac.cr2.write(|w| w
|
||||
// Continous Conversion mode
|
||||
.cont().set_bit()
|
||||
// Enable ADC
|
||||
.adon().set_bit()
|
||||
// Start ADC Conversion
|
||||
.swstart().set_bit()
|
||||
);
|
||||
// Set Sampling Time for Channel 3 to 480 Cycle
|
||||
wdg.pac.smpr2.write(|w| w
|
||||
.smp3().cycles480()
|
||||
);
|
||||
// Set the high threshold to be max value initially
|
||||
wdg.pac.htr.write(|w| w.bits(0xFFFF_FFFF));
|
||||
// Set the high threshold to be min value initially
|
||||
wdg.pac.ltr.write(|w| w.bits(0x0000_0000));
|
||||
// Set the Conversion Sequence to only have Channel 3 (PA3)
|
||||
wdg.pac.sqr3.write(|w| w
|
||||
.sq1().bits(0x03)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get() -> Option<&'static mut Self> {
|
||||
unsafe { ANALOG_WDG.as_mut() }
|
||||
}
|
||||
|
||||
/// This fn accepts the calibrated vdda value from Adc<ADC1>.calibrate()
|
||||
pub fn set_calibrated_vdda(val: u32) {
|
||||
if let Some(ref mut wdg ) = LdAnalogWdg::get() {
|
||||
wdg.calibrated_vdda = val;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_pd_v() -> ElectricPotential {
|
||||
if let Some(ref mut wdg ) = LdAnalogWdg::get() {
|
||||
let sample = wdg.pac.dr.read().data().bits();
|
||||
|
||||
return ElectricPotential::new::<millivolt>(((u32::from(sample) * wdg.calibrated_vdda) / u32::from(MAX_SAMPLE)) as f64)
|
||||
}
|
||||
ElectricPotential::new::<millivolt>(0.0)
|
||||
}
|
||||
|
||||
fn set_alarm(){
|
||||
if let Some(ref mut wdg ) = LdAnalogWdg::get() {
|
||||
wdg.alarm_status = AlarmStatus {
|
||||
alarm: true,
|
||||
val: wdg.pac.dr.read().data().bits(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_alarm_status() -> AlarmStatus {
|
||||
if let Some(ref mut wdg ) = LdAnalogWdg::get() {
|
||||
return wdg.alarm_status.clone()
|
||||
}
|
||||
AlarmStatus {
|
||||
alarm: false,
|
||||
val: 0x0000,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_alarm_status(){
|
||||
if let Some(ref mut wdg ) = LdAnalogWdg::get() {
|
||||
wdg.alarm_status = AlarmStatus {
|
||||
alarm: false,
|
||||
val: 0x0000,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Set ADC Watchdog Higher threshold register
|
||||
/// Interrupt is triggered when ADC value is ABOVE the value set
|
||||
pub fn set_htr(htr: u32){
|
||||
if let Some(ref mut wdg ) = LdAnalogWdg::get() {
|
||||
wdg.pac.htr.write(|w| unsafe {w.bits(htr)});
|
||||
}
|
||||
}
|
||||
|
||||
/// Set ADC Watchdog Lower threshold register
|
||||
/// Interrupt is triggered when ADC value is BELOW the value set
|
||||
#[allow(unused)]
|
||||
pub fn set_ltr(ltr:u32){
|
||||
if let Some(ref mut wdg ) = LdAnalogWdg::get() {
|
||||
wdg.pac.ltr.write(|w| unsafe {w.bits(ltr)});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn enable_watchdog_interrupt(){
|
||||
if let Some(ref mut wdg ) = LdAnalogWdg::get() {
|
||||
wdg.pac.cr1.modify(|_, w| w
|
||||
.awdie().set_bit()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn disable_watchdog_interrupt(){
|
||||
if let Some(ref mut wdg ) = LdAnalogWdg::get() {
|
||||
wdg.pac.cr1.modify(|_, w| w
|
||||
.awdie().clear_bit()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_interrupt_bit(){
|
||||
unsafe{
|
||||
NVIC::unmask(interrupt::ADC);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_pwr_engaged() -> bool{
|
||||
if let Some(ref mut wdg ) = LdAnalogWdg::get() {
|
||||
return wdg.phy.pwr_en_ch0.is_set_high()
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn pwr_engage(){
|
||||
if let Some(ref mut wdg ) = LdAnalogWdg::get() {
|
||||
wdg.phy.pwr_en_ch0.set_high()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pwr_disengage(){
|
||||
if let Some(ref mut wdg ) = LdAnalogWdg::get() {
|
||||
wdg.phy.pwr_en_ch0.set_low()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[interrupt]
|
||||
fn ADC(){
|
||||
cortex_m::interrupt::free(|_| {
|
||||
LdAnalogWdg::set_alarm();
|
||||
LdAnalogWdg::pwr_disengage();
|
||||
// Disable interrupt to avoid getting stuck in infinite interrupt loop
|
||||
LdAnalogWdg::disable_watchdog_interrupt();
|
||||
LdAnalogWdg::clear_interrupt_bit();
|
||||
}
|
||||
)
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
use miniconf::Miniconf;
|
||||
use crate::laser_diode::current_sources::CurrentSource;
|
||||
use stm32f4xx_hal::pac::ADC2;
|
||||
use crate::laser_diode::ld_ctrl::LdCtrl;
|
||||
use crate::laser_diode::analog_wdg::{LdAnalogWdg, self};
|
||||
use core::{marker::PhantomData, f64::NAN};
|
||||
|
||||
use uom::si::{
|
||||
@ -55,12 +57,14 @@ impl Default for Settings {
|
||||
}
|
||||
|
||||
pub struct LdDrive{
|
||||
ctrl: CurrentSource,
|
||||
ctrl: LdCtrl,
|
||||
settings: Settings,
|
||||
}
|
||||
|
||||
impl LdDrive{
|
||||
pub fn new(current_source: CurrentSource)-> Self{
|
||||
pub fn new(current_source: LdCtrl, pins_adc: ADC2, phy: analog_wdg::LdAnalogWdgPhy)-> Self {
|
||||
LdAnalogWdg::setup(pins_adc, phy);
|
||||
|
||||
LdDrive {
|
||||
ctrl: current_source,
|
||||
settings: Settings::default()
|
||||
@ -68,19 +72,11 @@ impl LdDrive{
|
||||
}
|
||||
|
||||
pub fn setup(&mut self) {
|
||||
self.power_down();
|
||||
LdAnalogWdg::pwr_disengage();
|
||||
self.ld_set_i(ElectricCurrent::new::<milliampere>(0.0));
|
||||
self.ld_short();
|
||||
}
|
||||
|
||||
pub fn power_up(&mut self){
|
||||
self.ctrl.power_up();
|
||||
}
|
||||
|
||||
pub fn power_down(&mut self){
|
||||
self.ctrl.power_down();
|
||||
}
|
||||
|
||||
pub fn get_ld_drive_current(&mut self) -> ElectricCurrent{
|
||||
self.settings.ld_drive_current
|
||||
}
|
||||
@ -89,10 +85,6 @@ impl LdDrive{
|
||||
self.settings.ld_drive_current_limit = i_limit;
|
||||
}
|
||||
|
||||
pub fn set_pd_i_to_out_pwr(&mut self, val: IToPowerUnit){
|
||||
self.settings.pd_i_to_out_pwr = val;
|
||||
}
|
||||
|
||||
pub fn ld_short(&mut self) {
|
||||
self.ctrl.ld_short_enable();
|
||||
}
|
||||
@ -101,13 +93,16 @@ impl LdDrive{
|
||||
self.ctrl.ld_short_disable();
|
||||
}
|
||||
|
||||
pub fn get_ld_power_output(&mut self) -> Power {
|
||||
let pd_i = self.get_pd_i();
|
||||
pd_i * self.settings.pd_i_to_out_pwr
|
||||
pub fn power_up(&mut self){
|
||||
LdAnalogWdg::pwr_engage();
|
||||
}
|
||||
|
||||
pub fn power_down(&mut self){
|
||||
LdAnalogWdg::pwr_disengage();
|
||||
}
|
||||
|
||||
pub fn get_pd_i(&mut self) -> ElectricCurrent {
|
||||
self.ctrl.get_pd_mon_v() * Settings::PD_MON_TRANSCONDUCTANCE
|
||||
LdAnalogWdg::get_pd_v() * Settings::PD_MON_TRANSCONDUCTANCE
|
||||
}
|
||||
|
||||
pub fn ld_set_i(&mut self, i: ElectricCurrent) -> ElectricCurrent {
|
||||
@ -116,4 +111,30 @@ impl LdDrive{
|
||||
self.settings.ld_drive_current = ld_i_set;
|
||||
ld_i_set
|
||||
}
|
||||
|
||||
// Set the calibrated VDDA value obtained from ADC1 calibration
|
||||
pub fn set_pd_mon_calibrated_vdda(val_cal: u32) {
|
||||
LdAnalogWdg::set_calibrated_vdda(val_cal)
|
||||
}
|
||||
|
||||
pub fn pd_mon_status() -> analog_wdg::AlarmStatus {
|
||||
LdAnalogWdg::get_alarm_status()
|
||||
}
|
||||
|
||||
pub fn pd_mon_clear_alarm(&mut self) {
|
||||
LdAnalogWdg::clear_alarm_status();
|
||||
}
|
||||
|
||||
pub fn pd_mon_engage(){
|
||||
LdAnalogWdg::enable_watchdog_interrupt()
|
||||
}
|
||||
|
||||
pub fn pd_mon_disengage(){
|
||||
LdAnalogWdg::disable_watchdog_interrupt()
|
||||
}
|
||||
|
||||
pub fn set_ld_power_limit(pwr_limit: Power){
|
||||
// LdAnalogWdg::set_htr(convert pwr_limit to raw adc code)
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
@ -1,43 +1,32 @@
|
||||
use stm32f4xx_hal::{
|
||||
adc::{
|
||||
config::{self, AdcConfig},
|
||||
Adc,
|
||||
},
|
||||
gpio::{gpioa::*, gpiob::*, gpiod::*, Alternate, Output, PushPull, Analog, PD9},
|
||||
gpio::{gpioa::*, gpiob::*, gpiod::*, Alternate, Output, PushPull},
|
||||
hal::{blocking::spi::Transfer, digital::v2::OutputPin},
|
||||
pac::{SPI2, ADC2},
|
||||
pac::SPI2,
|
||||
spi::{NoMiso, Spi, TransferModeNormal},
|
||||
};
|
||||
|
||||
use uom::si::{
|
||||
ratio::ratio,
|
||||
electric_potential::millivolt,
|
||||
f64::{ElectricPotential, ElectricCurrent},
|
||||
};
|
||||
|
||||
use crate::laser_diode::max5719::{self, Dac};
|
||||
use crate::laser_diode::ld_drive::TransimpedanceUnit;
|
||||
use crate::laser_diode::laser_diode::TransimpedanceUnit;
|
||||
|
||||
pub trait ChannelPins {
|
||||
type PdMonPin;
|
||||
type CurrentSourceLdoEn: OutputPin;
|
||||
type CurrentSourceShort: OutputPin;
|
||||
type Max5719Load: OutputPin;
|
||||
type Max5719Cs: OutputPin;
|
||||
type Max5719Spi: Transfer<u8>;
|
||||
}
|
||||
|
||||
pub struct CurrentSourcePhy<C: ChannelPins> {
|
||||
pub struct LdCtrlPhy<C: ChannelPins> {
|
||||
pub dac: Dac<C::Max5719Spi, C::Max5719Cs, C::Max5719Load>,
|
||||
pub current_source_ldo_en_pin: C::CurrentSourceLdoEn,
|
||||
pub current_source_short_pin: C::CurrentSourceShort,
|
||||
pub pd_mon_pin : C::PdMonPin,
|
||||
}
|
||||
pub struct Channel0;
|
||||
|
||||
impl ChannelPins for Channel0 {
|
||||
type PdMonPin = PA3<Analog>;
|
||||
type CurrentSourceLdoEn = PD9<Output<PushPull>>;
|
||||
type CurrentSourceShort = PA4<Output<PushPull>>;
|
||||
type Max5719Load = DacLoad;
|
||||
type Max5719Cs = DacCs;
|
||||
@ -48,38 +37,17 @@ type DacSpi = Spi<SPI2, (PB10<Alternate<5>>, NoMiso, PB15<Alternate<5>>), Transf
|
||||
type DacCs = PD8<Output<PushPull>>;
|
||||
type DacLoad = PB14<Output<PushPull>>;
|
||||
|
||||
|
||||
pub struct CurrentSource{
|
||||
pub phy: CurrentSourcePhy<Channel0>,
|
||||
pub pins_adc: Adc<ADC2>
|
||||
pub struct LdCtrl{
|
||||
pub phy: LdCtrlPhy<Channel0>,
|
||||
}
|
||||
|
||||
impl CurrentSource {
|
||||
pub fn new(phy_ch0: CurrentSourcePhy<Channel0>, adc2: ADC2) -> Self {
|
||||
let config = AdcConfig::default()
|
||||
.clock(config::Clock::Pclk2_div_2)
|
||||
.default_sample_time(config::SampleTime::Cycles_480);
|
||||
|
||||
let pins_adc = Adc::adc2(adc2, true, config);
|
||||
|
||||
CurrentSource {
|
||||
impl LdCtrl {
|
||||
pub fn new(phy_ch0: LdCtrlPhy<Channel0>) -> Self {
|
||||
LdCtrl {
|
||||
phy: phy_ch0,
|
||||
pins_adc: pins_adc,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn power_up(&mut self) {
|
||||
self.phy.current_source_ldo_en_pin.set_high();
|
||||
}
|
||||
|
||||
#[deprecated(note=
|
||||
"To be removed when rev0_3 has arrived
|
||||
Rev0_2 has hardware connection bug for LD_EN.
|
||||
LD_EN will always be enabled.")]
|
||||
pub fn power_down(&mut self) {
|
||||
self.phy.current_source_ldo_en_pin.set_low();
|
||||
}
|
||||
|
||||
// LD Terminals are shorted together
|
||||
pub fn ld_short_enable(&mut self) {
|
||||
self.phy.current_source_short_pin.set_low();
|
||||
@ -90,15 +58,6 @@ impl CurrentSource {
|
||||
self.phy.current_source_short_pin.set_high();
|
||||
}
|
||||
|
||||
pub fn get_pd_mon_v(&mut self) -> ElectricPotential{
|
||||
let sample = self.pins_adc.convert(
|
||||
&self.phy.pd_mon_pin,
|
||||
stm32f4xx_hal::adc::config::SampleTime::Cycles_480,
|
||||
);
|
||||
let mv = self.pins_adc.sample_to_millivolts(sample as u16);
|
||||
ElectricPotential::new::<millivolt>(mv as f64)
|
||||
}
|
||||
|
||||
pub fn set_dac(&mut self, voltage: ElectricPotential, dac_out_v_max: ElectricPotential) -> ElectricPotential {
|
||||
let value = ((voltage / dac_out_v_max).get::<ratio>()
|
||||
* (max5719::MAX_VALUE as f64)) as u32;
|
@ -1,3 +1,5 @@
|
||||
pub mod current_sources;
|
||||
pub mod ld_ctrl;
|
||||
pub mod max5719;
|
||||
pub mod ld_drive;
|
||||
pub mod laser_diode;
|
||||
pub mod pd_mon;
|
||||
pub mod analog_wdg;
|
37
src/laser_diode/pd_mon.rs
Normal file
37
src/laser_diode/pd_mon.rs
Normal file
@ -0,0 +1,37 @@
|
||||
use core::{f64::NAN, marker::PhantomData};
|
||||
use uom::si::{
|
||||
f64::{
|
||||
ElectricCurrent,
|
||||
Power
|
||||
},
|
||||
electric_current::microampere,
|
||||
};
|
||||
use uom::{si::{ISQ, SI, Quantity}, typenum::*};
|
||||
use miniconf::Miniconf;
|
||||
|
||||
// Ampere / Watt
|
||||
pub type ResponsitivityUnit = Quantity<ISQ<N2, N1, P3, P1, Z0, Z0, Z0>, SI<f64>, f64>;
|
||||
|
||||
/// Steinhart-Hart equation Photodiode
|
||||
#[derive(Clone, Debug, PartialEq, Miniconf)]
|
||||
pub struct Parameters {
|
||||
/// Responsitivity
|
||||
pub responsitivity: ResponsitivityUnit,
|
||||
pub i_dark: ElectricCurrent,
|
||||
}
|
||||
|
||||
impl Parameters {
|
||||
pub fn get_ld_output_power(&self, i: ElectricCurrent) -> Power {
|
||||
let ld_power = (i - self.i_dark) / self.responsitivity;
|
||||
ld_power
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Parameters {
|
||||
fn default() -> Self {
|
||||
Parameters {
|
||||
responsitivity: ResponsitivityUnit {dimension: PhantomData, units: PhantomData, value: NAN},
|
||||
i_dark: ElectricCurrent::new::<microampere>(0.0),
|
||||
}
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ use stm32f4xx_hal::pac::{CorePeripherals, Peripherals};
|
||||
mod device;
|
||||
mod laser_diode;
|
||||
mod thermostat;
|
||||
mod pid;
|
||||
|
||||
use device::{boot::bootup, log_setup, sys_timer};
|
||||
use uom::fmt::DisplayStyle::Abbreviation;
|
||||
@ -73,7 +74,6 @@ fn main() -> ! {
|
||||
wd.feed();
|
||||
|
||||
info!("looping");
|
||||
info!("curr_ld_power: {:?}", mili_watt_fmt.with(laser.get_ld_power_output()));
|
||||
info!("curr_ld_drive_cuurent: {:?}", mili_amp_fmt.with(laser.get_ld_drive_current()));
|
||||
|
||||
info!("curr_dac_vfb: {:?}", volt_fmt.with(thermostat.get_dac_vfb()));
|
||||
|
@ -2,7 +2,7 @@
|
||||
use miniconf::Miniconf;
|
||||
use miniconf::serde::{Serialize, Deserialize};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Miniconf)]
|
||||
pub struct Parameters {
|
||||
/// Gain coefficient for proportional term
|
||||
pub kp: f32,
|
||||
@ -28,8 +28,9 @@ impl Default for Parameters {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug, PartialEq, Miniconf)]
|
||||
pub struct Controller {
|
||||
#[miniconf(defer)]
|
||||
pub parameters: Parameters,
|
||||
pub target : f64,
|
||||
u1 : f64,
|
||||
@ -88,6 +89,7 @@ impl Controller {
|
||||
|
||||
#[derive(Clone, Debug, Miniconf)]
|
||||
pub struct Summary {
|
||||
#[miniconf(defer)]
|
||||
parameters: Parameters,
|
||||
target: f64,
|
||||
}
|
||||
|
296
src/thermostat/ad7172/adc.rs
Normal file
296
src/thermostat/ad7172/adc.rs
Normal file
@ -0,0 +1,296 @@
|
||||
use core::fmt;
|
||||
use log::{info, warn};
|
||||
use stm32f4xx_hal::
|
||||
{
|
||||
spi::{Spi, TransferModeNormal},
|
||||
pac::SPI3,
|
||||
gpio::{PC10, PC11, PC12, PA15, Alternate, Output, PushPull},
|
||||
hal::{
|
||||
blocking::spi::Transfer,
|
||||
digital::v2::OutputPin,
|
||||
},
|
||||
};
|
||||
use uom::si::{
|
||||
f64::ElectricPotential,
|
||||
electric_potential::volt,
|
||||
};
|
||||
use super::{
|
||||
regs::{self, Register, RegisterData},
|
||||
checksum::{ChecksumMode, Checksum},
|
||||
Mode, Input, RefSource, PostFilter, DigitalFilterOrder,
|
||||
};
|
||||
|
||||
/// AD7172-2 implementation
|
||||
///
|
||||
/// [Manual](https://www.analog.com/media/en/technical-documentation/data-sheets/AD7172-2.pdf)
|
||||
pub struct Adc<SPI: Transfer<u8>, NSS: OutputPin> {
|
||||
spi: SPI,
|
||||
nss: NSS,
|
||||
checksum_mode: ChecksumMode,
|
||||
}
|
||||
|
||||
type AdcSpi = Spi<SPI3, (PC10<Alternate<6>>, PC11<Alternate<6>>, PC12<Alternate<6>>), TransferModeNormal>;
|
||||
type AdcNss = PA15<Output<PushPull>>;
|
||||
pub type AdcPhy = Adc<AdcSpi, AdcNss>;
|
||||
|
||||
impl<SPI: Transfer<u8, Error = E>, NSS: OutputPin, E: fmt::Debug> Adc<SPI, NSS> {
|
||||
pub fn new(spi: SPI, mut nss: NSS) -> Result<Self, SPI::Error> {
|
||||
let _ = nss.set_high();
|
||||
let mut adc = Adc {
|
||||
spi, nss,
|
||||
checksum_mode: ChecksumMode::Off,
|
||||
};
|
||||
adc.reset()?;
|
||||
adc.set_checksum_mode(ChecksumMode::Crc).unwrap();
|
||||
|
||||
let mut retries = 0;
|
||||
let mut adc_id;
|
||||
loop {
|
||||
adc_id = adc.identify()?;
|
||||
if adc_id & 0xFFF0 == 0x00D0 {
|
||||
break;
|
||||
} else {
|
||||
retries += 1;
|
||||
}
|
||||
}
|
||||
info!("ADC id: {:04X} ({} retries)", adc_id, retries);
|
||||
|
||||
let mut adc_mode = <regs::AdcMode as Register>::Data::empty();
|
||||
adc_mode.set_ref_en(true);
|
||||
adc_mode.set_mode(Mode::Standby);
|
||||
adc.write_reg(®s::AdcMode, &mut adc_mode)?;
|
||||
|
||||
Ok(adc)
|
||||
}
|
||||
|
||||
/// `0x00DX` for AD7172-2
|
||||
pub fn identify(&mut self) -> Result<u16, SPI::Error> {
|
||||
self.read_reg(®s::Id)
|
||||
.map(|id| id.id())
|
||||
}
|
||||
|
||||
pub fn set_checksum_mode(&mut self, mode: ChecksumMode) -> Result<(), SPI::Error> {
|
||||
// Cannot use update_reg() here because checksum_mode is
|
||||
// updated between read_reg() and write_reg().
|
||||
let mut ifmode = self.read_reg(®s::IfMode)?;
|
||||
ifmode.set_crc(mode);
|
||||
self.checksum_mode = mode;
|
||||
self.write_reg(®s::IfMode, &mut ifmode)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_sync_enable(&mut self, enable: bool) -> Result<(), SPI::Error> {
|
||||
self.update_reg(®s::GpioCon, |data| {
|
||||
data.set_sync_en(enable);
|
||||
})
|
||||
}
|
||||
|
||||
pub fn setup_channel(
|
||||
&mut self, index: u8, in_pos: Input, in_neg: Input
|
||||
) -> Result<(), SPI::Error> {
|
||||
self.update_reg(®s::SetupCon { index }, |data| {
|
||||
data.set_bipolar(false);
|
||||
data.set_refbuf_pos(true);
|
||||
data.set_refbuf_neg(true);
|
||||
data.set_ainbuf_pos(true);
|
||||
data.set_ainbuf_neg(true);
|
||||
data.set_ref_sel(RefSource::External);
|
||||
})?;
|
||||
self.update_reg(®s::FiltCon { index }, |data| {
|
||||
data.set_enh_filt_en(true);
|
||||
data.set_enh_filt(PostFilter::F16SPS);
|
||||
data.set_order(DigitalFilterOrder::Sinc5Sinc1);
|
||||
// output data rate: 10 Hz
|
||||
data.set_odr(0b10011);
|
||||
})?;
|
||||
self.update_reg(®s::Channel { index }, |data| {
|
||||
data.set_setup(index);
|
||||
data.set_enabled(true);
|
||||
data.set_a_in_pos(in_pos);
|
||||
data.set_a_in_neg(in_neg);
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_calibration(&mut self, index: u8) -> Result<ChannelCalibration, SPI::Error> {
|
||||
let offset = self.read_reg(®s::Offset { index })?.offset();
|
||||
let gain = self.read_reg(®s::Gain { index })?.gain();
|
||||
let bipolar = self.read_reg(®s::SetupCon { index })?.bipolar();
|
||||
Ok(ChannelCalibration { offset, gain, bipolar })
|
||||
}
|
||||
|
||||
pub fn start_continuous_conversion(&mut self) -> Result<(), SPI::Error> {
|
||||
let mut adc_mode = <regs::AdcMode as Register>::Data::empty();
|
||||
adc_mode.set_ref_en(true);
|
||||
adc_mode.set_mode(Mode::ContinuousConversion);
|
||||
self.write_reg(®s::AdcMode, &mut adc_mode)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_postfilter(&mut self, index: u8) -> Result<Option<PostFilter>, SPI::Error> {
|
||||
self.read_reg(®s::FiltCon { index })
|
||||
.map(|data| {
|
||||
if data.enh_filt_en() {
|
||||
Some(data.enh_filt())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_postfilter(&mut self, index: u8, filter: Option<PostFilter>) -> Result<(), SPI::Error> {
|
||||
self.update_reg(®s::FiltCon { index }, |data| {
|
||||
match filter {
|
||||
None => data.set_enh_filt_en(false),
|
||||
Some(filter) => {
|
||||
data.set_enh_filt_en(true);
|
||||
data.set_enh_filt(filter);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the channel the data is from
|
||||
pub fn data_ready(&mut self) -> Result<Option<u8>, SPI::Error> {
|
||||
self.read_reg(®s::Status)
|
||||
.map(|status| {
|
||||
if status.ready() {
|
||||
Some(status.channel())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Get data
|
||||
pub fn read_data(&mut self) -> Result<u32, SPI::Error> {
|
||||
self.read_reg(®s::Data)
|
||||
.map(|data| data.data())
|
||||
}
|
||||
|
||||
fn read_reg<R: regs::Register>(&mut self, reg: &R) -> Result<R::Data, SPI::Error> {
|
||||
let mut reg_data = R::Data::empty();
|
||||
let address = 0x40 | reg.address();
|
||||
let mut checksum = Checksum::new(self.checksum_mode);
|
||||
checksum.feed(&[address]);
|
||||
let checksum_out = checksum.result();
|
||||
|
||||
loop {
|
||||
let checksum_in = self.transfer(address, reg_data.as_mut(), checksum_out)?;
|
||||
|
||||
checksum.feed(®_data);
|
||||
let checksum_expected = checksum.result();
|
||||
if checksum_expected == checksum_in {
|
||||
break;
|
||||
}
|
||||
// Retry
|
||||
warn!("read_reg {:02X}: checksum error: {:?}!={:?}, retrying", reg.address(), checksum_expected, checksum_in);
|
||||
}
|
||||
Ok(reg_data)
|
||||
}
|
||||
|
||||
fn write_reg<R: regs::Register>(&mut self, reg: &R, reg_data: &mut R::Data) -> Result<(), SPI::Error> {
|
||||
loop {
|
||||
let address = reg.address();
|
||||
let mut checksum = Checksum::new(match self.checksum_mode {
|
||||
ChecksumMode::Off => ChecksumMode::Off,
|
||||
// write checksums are always crc
|
||||
ChecksumMode::Xor => ChecksumMode::Crc,
|
||||
ChecksumMode::Crc => ChecksumMode::Crc,
|
||||
});
|
||||
checksum.feed(&[address]);
|
||||
checksum.feed(®_data);
|
||||
let checksum_out = checksum.result();
|
||||
|
||||
let mut data = reg_data.clone();
|
||||
self.transfer(address, data.as_mut(), checksum_out)?;
|
||||
|
||||
// Verification
|
||||
let readback_data = self.read_reg(reg)?;
|
||||
if *readback_data == **reg_data {
|
||||
return Ok(());
|
||||
}
|
||||
warn!("write_reg {:02X}: readback error, {:?}!={:?}, retrying", address, &*readback_data, &**reg_data);
|
||||
}
|
||||
}
|
||||
|
||||
fn update_reg<R, F, A>(&mut self, reg: &R, f: F) -> Result<A, SPI::Error>
|
||||
where
|
||||
R: regs::Register,
|
||||
F: FnOnce(&mut R::Data) -> A,
|
||||
{
|
||||
let mut reg_data = self.read_reg(reg)?;
|
||||
let result = f(&mut reg_data);
|
||||
self.write_reg(reg, &mut reg_data)?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) -> Result<(), SPI::Error> {
|
||||
let mut buf = [0xFFu8; 8];
|
||||
let _ = self.nss.set_low();
|
||||
let result = self.spi.transfer(&mut buf);
|
||||
let _ = self.nss.set_high();
|
||||
result?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn transfer<'w>(&mut self, addr: u8, reg_data: &'w mut [u8], checksum: Option<u8>) -> Result<Option<u8>, SPI::Error> {
|
||||
let mut addr_buf = [addr];
|
||||
|
||||
let _ = self.nss.set_low();
|
||||
let result = match self.spi.transfer(&mut addr_buf) {
|
||||
Ok(_) => self.spi.transfer(reg_data),
|
||||
Err(e) => Err(e),
|
||||
};
|
||||
let result = match (result, checksum) {
|
||||
(Ok(_), None) =>
|
||||
Ok(None),
|
||||
(Ok(_), Some(checksum_out)) => {
|
||||
let mut checksum_buf = [checksum_out; 1];
|
||||
match self.spi.transfer(&mut checksum_buf) {
|
||||
Ok(_) => Ok(Some(checksum_buf[0])),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
(Err(e), _) =>
|
||||
Err(e),
|
||||
};
|
||||
let _ = self.nss.set_high();
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ChannelCalibration {
|
||||
offset: u32,
|
||||
gain: u32,
|
||||
bipolar: bool,
|
||||
}
|
||||
|
||||
impl Default for ChannelCalibration {
|
||||
fn default() -> Self {
|
||||
ChannelCalibration {
|
||||
offset: 0,
|
||||
gain: 0,
|
||||
bipolar: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ChannelCalibration {
|
||||
pub fn convert_data(&self, data: u32) -> ElectricPotential {
|
||||
let data = if self.bipolar {
|
||||
(data as i32 - 0x80_0000) as f64
|
||||
} else {
|
||||
data as f64 / 2.0
|
||||
};
|
||||
let data = data / (self.gain as f64 / (0x40_0000 as f64));
|
||||
let data = data + (self.offset as i32 - 0x80_0000) as f64;
|
||||
let data = data / (2 << 23) as f64;
|
||||
|
||||
const V_REF: f64 = 3.3;
|
||||
ElectricPotential::new::<volt>(data * V_REF / 0.75)
|
||||
}
|
||||
}
|
60
src/thermostat/ad7172/checksum.rs
Normal file
60
src/thermostat/ad7172/checksum.rs
Normal file
@ -0,0 +1,60 @@
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
#[repr(u8)]
|
||||
pub enum ChecksumMode {
|
||||
Off = 0b00,
|
||||
/// Seems much less reliable than `Crc`
|
||||
Xor = 0b01,
|
||||
Crc = 0b10,
|
||||
}
|
||||
|
||||
impl From<u8> for ChecksumMode {
|
||||
fn from(x: u8) -> Self {
|
||||
match x {
|
||||
0 => ChecksumMode::Off,
|
||||
1 => ChecksumMode::Xor,
|
||||
_ => ChecksumMode::Crc,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Checksum {
|
||||
mode: ChecksumMode,
|
||||
state: u8,
|
||||
}
|
||||
|
||||
impl Checksum {
|
||||
pub fn new(mode: ChecksumMode) -> Self {
|
||||
Checksum { mode, state: 0 }
|
||||
}
|
||||
|
||||
fn feed_byte(&mut self, input: u8) {
|
||||
match self.mode {
|
||||
ChecksumMode::Off => {},
|
||||
ChecksumMode::Xor => self.state ^= input,
|
||||
ChecksumMode::Crc => {
|
||||
for i in 0..8 {
|
||||
let input_mask = 0x80 >> i;
|
||||
self.state = (self.state << 1) ^
|
||||
if ((self.state & 0x80) != 0) != ((input & input_mask) != 0) {
|
||||
0x07 /* x8 + x2 + x + 1 */
|
||||
} else {
|
||||
0
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn feed(&mut self, input: &[u8]) {
|
||||
for &b in input {
|
||||
self.feed_byte(b);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn result(&self) -> Option<u8> {
|
||||
match self.mode {
|
||||
ChecksumMode::Off => None,
|
||||
_ => Some(self.state)
|
||||
}
|
||||
}
|
||||
}
|
221
src/thermostat/ad7172/mod.rs
Normal file
221
src/thermostat/ad7172/mod.rs
Normal file
@ -0,0 +1,221 @@
|
||||
use core::fmt;
|
||||
use num_traits::float::Float;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use fugit::MegahertzU32;
|
||||
use stm32f4xx_hal::spi;
|
||||
|
||||
pub mod regs;
|
||||
mod checksum;
|
||||
pub use checksum::ChecksumMode;
|
||||
mod adc;
|
||||
pub use adc::*;
|
||||
|
||||
/// SPI Mode 3
|
||||
pub const SPI_MODE: spi::Mode = spi::Mode {
|
||||
polarity: spi::Polarity::IdleHigh,
|
||||
phase: spi::Phase::CaptureOnSecondTransition,
|
||||
};
|
||||
/// 2 MHz
|
||||
pub const SPI_CLOCK_MHZ: MegahertzU32 = MegahertzU32::from_raw(2);
|
||||
|
||||
pub const MAX_VALUE: u32 = 0xFF_FFFF;
|
||||
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[repr(u8)]
|
||||
pub enum Mode {
|
||||
ContinuousConversion = 0b000,
|
||||
SingleConversion = 0b001,
|
||||
Standby = 0b010,
|
||||
PowerDown = 0b011,
|
||||
InternalOffsetCalibration = 0b100,
|
||||
Invalid,
|
||||
SystemOffsetCalibration = 0b110,
|
||||
SystemGainCalibration = 0b111,
|
||||
}
|
||||
|
||||
impl From<u8> for Mode {
|
||||
fn from(x: u8) -> Self {
|
||||
use Mode::*;
|
||||
match x {
|
||||
0b000 => ContinuousConversion,
|
||||
0b001 => SingleConversion,
|
||||
0b010 => Standby,
|
||||
0b011 => PowerDown,
|
||||
0b100 => InternalOffsetCalibration,
|
||||
0b110 => SystemOffsetCalibration,
|
||||
0b111 => SystemGainCalibration,
|
||||
_ => Invalid,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[repr(u8)]
|
||||
pub enum Input {
|
||||
Ain0 = 0,
|
||||
Ain1 = 1,
|
||||
Ain2 = 2,
|
||||
Ain3 = 3,
|
||||
Ain4 = 4,
|
||||
TemperaturePos = 17,
|
||||
TemperatureNeg = 18,
|
||||
AnalogSupplyPos = 19,
|
||||
AnalogSupplyNeg = 20,
|
||||
RefPos = 21,
|
||||
RefNeg = 22,
|
||||
Invalid = 0b11111,
|
||||
}
|
||||
|
||||
impl From<u8> for Input {
|
||||
fn from(x: u8) -> Self {
|
||||
match x {
|
||||
0 => Input::Ain0,
|
||||
1 => Input::Ain1,
|
||||
2 => Input::Ain2,
|
||||
3 => Input::Ain3,
|
||||
4 => Input::Ain4,
|
||||
17 => Input::TemperaturePos,
|
||||
18 => Input::TemperatureNeg,
|
||||
19 => Input::AnalogSupplyPos,
|
||||
20 => Input::AnalogSupplyNeg,
|
||||
21 => Input::RefPos,
|
||||
22 => Input::RefNeg,
|
||||
_ => Input::Invalid,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Input {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||
use Input::*;
|
||||
|
||||
match self {
|
||||
Ain0 => "ain0",
|
||||
Ain1 => "ain1",
|
||||
Ain2 => "ain2",
|
||||
Ain3 => "ain3",
|
||||
Ain4 => "ain4",
|
||||
TemperaturePos => "temperature+",
|
||||
TemperatureNeg => "temperature-",
|
||||
AnalogSupplyPos => "analogsupply+",
|
||||
AnalogSupplyNeg => "analogsupply-",
|
||||
RefPos => "ref+",
|
||||
RefNeg => "ref-",
|
||||
_ => "<INVALID>",
|
||||
}.fmt(fmt)
|
||||
}
|
||||
}
|
||||
|
||||
/// Reference source for ADC conversion
|
||||
#[repr(u8)]
|
||||
pub enum RefSource {
|
||||
/// External reference
|
||||
External = 0b00,
|
||||
/// Internal 2.5V reference
|
||||
Internal = 0b10,
|
||||
/// AVDD1 − AVSS
|
||||
Avdd1MinusAvss = 0b11,
|
||||
Invalid = 0b01,
|
||||
}
|
||||
|
||||
impl From<u8> for RefSource {
|
||||
fn from(x: u8) -> Self {
|
||||
match x {
|
||||
0 => RefSource::External,
|
||||
1 => RefSource::Internal,
|
||||
2 => RefSource::Avdd1MinusAvss,
|
||||
_ => RefSource::Invalid,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for RefSource {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||
use RefSource::*;
|
||||
|
||||
match self {
|
||||
External => "external",
|
||||
Internal => "internal",
|
||||
Avdd1MinusAvss => "avdd1-avss",
|
||||
_ => "<INVALID>",
|
||||
}.fmt(fmt)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[repr(u8)]
|
||||
pub enum PostFilter {
|
||||
/// 27 SPS, 47 dB rejection, 36.7 ms settling
|
||||
F27SPS = 0b010,
|
||||
/// 21.25 SPS, 62 dB rejection, 40 ms settling
|
||||
F21SPS = 0b011,
|
||||
/// 20 SPS, 86 dB rejection, 50 ms settling
|
||||
F20SPS = 0b101,
|
||||
/// 16.67 SPS, 92 dB rejection, 60 ms settling
|
||||
F16SPS = 0b110,
|
||||
Invalid = 0b111,
|
||||
}
|
||||
|
||||
impl PostFilter {
|
||||
pub const VALID_VALUES: &'static [Self] = &[
|
||||
PostFilter::F27SPS,
|
||||
PostFilter::F21SPS,
|
||||
PostFilter::F20SPS,
|
||||
PostFilter::F16SPS,
|
||||
];
|
||||
|
||||
pub fn closest(rate: f32) -> Option<Self> {
|
||||
let mut best: Option<(f32, Self)> = None;
|
||||
for value in Self::VALID_VALUES {
|
||||
let error = (rate - value.output_rate().unwrap()).abs();
|
||||
let better = best
|
||||
.map(|(best_error, _)| error < best_error)
|
||||
.unwrap_or(true);
|
||||
if better {
|
||||
best = Some((error, *value));
|
||||
}
|
||||
}
|
||||
best.map(|(_, best)| best)
|
||||
}
|
||||
|
||||
/// Samples per Second
|
||||
pub fn output_rate(&self) -> Option<f32> {
|
||||
match self {
|
||||
PostFilter::F27SPS => Some(27.0),
|
||||
PostFilter::F21SPS => Some(21.25),
|
||||
PostFilter::F20SPS => Some(20.0),
|
||||
PostFilter::F16SPS => Some(16.67),
|
||||
PostFilter::Invalid => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u8> for PostFilter {
|
||||
fn from(x: u8) -> Self {
|
||||
match x {
|
||||
0b010 => PostFilter::F27SPS,
|
||||
0b011 => PostFilter::F21SPS,
|
||||
0b101 => PostFilter::F20SPS,
|
||||
0b110 => PostFilter::F16SPS,
|
||||
_ => PostFilter::Invalid,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(u8)]
|
||||
pub enum DigitalFilterOrder {
|
||||
Sinc5Sinc1 = 0b00,
|
||||
Sinc3 = 0b11,
|
||||
Invalid = 0b10,
|
||||
}
|
||||
|
||||
impl From<u8> for DigitalFilterOrder {
|
||||
fn from(x: u8) -> Self {
|
||||
match x {
|
||||
0b00 => DigitalFilterOrder::Sinc5Sinc1,
|
||||
0b11 => DigitalFilterOrder::Sinc3,
|
||||
_ => DigitalFilterOrder::Invalid,
|
||||
}
|
||||
}
|
||||
}
|
267
src/thermostat/ad7172/regs.rs
Normal file
267
src/thermostat/ad7172/regs.rs
Normal file
@ -0,0 +1,267 @@
|
||||
use core::ops::{Deref, DerefMut};
|
||||
use byteorder::{BigEndian, ByteOrder};
|
||||
use bit_field::BitField;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub trait Register {
|
||||
type Data: RegisterData;
|
||||
fn address(&self) -> u8;
|
||||
}
|
||||
|
||||
pub trait RegisterData: Clone + Deref<Target=[u8]> + DerefMut {
|
||||
fn empty() -> Self;
|
||||
}
|
||||
|
||||
macro_rules! def_reg {
|
||||
($Reg: ident, $reg: ident, $addr: expr, $size: expr) => {
|
||||
/// AD7172 register
|
||||
pub struct $Reg;
|
||||
impl Register for $Reg {
|
||||
/// Register contents
|
||||
type Data = $reg::Data;
|
||||
/// Register address
|
||||
fn address(&self) -> u8 {
|
||||
$addr
|
||||
}
|
||||
}
|
||||
mod $reg {
|
||||
/// Register contents
|
||||
#[derive(Clone)]
|
||||
pub struct Data(pub [u8; $size]);
|
||||
impl super::RegisterData for Data {
|
||||
/// Generate zeroed register contents
|
||||
fn empty() -> Self {
|
||||
Data([0; $size])
|
||||
}
|
||||
}
|
||||
impl core::ops::Deref for Data {
|
||||
type Target = [u8];
|
||||
fn deref(&self) -> &[u8] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
impl core::ops::DerefMut for Data {
|
||||
fn deref_mut(&mut self) -> &mut [u8] {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
($Reg: ident, u8, $reg: ident, $addr: expr, $size: expr) => {
|
||||
pub struct $Reg { pub index: u8, }
|
||||
impl Register for $Reg {
|
||||
type Data = $reg::Data;
|
||||
fn address(&self) -> u8 {
|
||||
$addr + self.index
|
||||
}
|
||||
}
|
||||
mod $reg {
|
||||
#[derive(Clone)]
|
||||
pub struct Data(pub [u8; $size]);
|
||||
impl super::RegisterData for Data {
|
||||
fn empty() -> Self {
|
||||
Data([0; $size])
|
||||
}
|
||||
}
|
||||
impl core::ops::Deref for Data {
|
||||
type Target = [u8];
|
||||
fn deref(&self) -> &[u8] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
impl core::ops::DerefMut for Data {
|
||||
fn deref_mut(&mut self) -> &mut [u8] {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! reg_bit {
|
||||
($getter: ident, $byte: expr, $bit: expr, $doc: expr) => {
|
||||
#[allow(unused)]
|
||||
#[doc = $doc]
|
||||
pub fn $getter(&self) -> bool {
|
||||
self.0[$byte].get_bit($bit)
|
||||
}
|
||||
};
|
||||
($getter: ident, $setter: ident, $byte: expr, $bit: expr, $doc: expr) => {
|
||||
#[allow(unused)]
|
||||
#[doc = $doc]
|
||||
pub fn $getter(&self) -> bool {
|
||||
self.0[$byte].get_bit($bit)
|
||||
}
|
||||
#[allow(unused)]
|
||||
#[doc = $doc]
|
||||
pub fn $setter(&mut self, value: bool) {
|
||||
self.0[$byte].set_bit($bit, value);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! reg_bits {
|
||||
($getter: ident, $byte: expr, $bits: expr, $doc: expr) => {
|
||||
#[allow(unused)]
|
||||
#[doc = $doc]
|
||||
pub fn $getter(&self) -> u8 {
|
||||
self.0[$byte].get_bits($bits)
|
||||
}
|
||||
};
|
||||
($getter: ident, $setter: ident, $byte: expr, $bits: expr, $doc: expr) => {
|
||||
#[allow(unused)]
|
||||
#[doc = $doc]
|
||||
pub fn $getter(&self) -> u8 {
|
||||
self.0[$byte].get_bits($bits)
|
||||
}
|
||||
#[allow(unused)]
|
||||
#[doc = $doc]
|
||||
pub fn $setter(&mut self, value: u8) {
|
||||
self.0[$byte].set_bits($bits, value);
|
||||
}
|
||||
};
|
||||
($getter: ident, $byte: expr, $bits: expr, $ty: ty, $doc: expr) => {
|
||||
#[allow(unused)]
|
||||
#[doc = $doc]
|
||||
pub fn $getter(&self) -> $ty {
|
||||
self.0[$byte].get_bits($bits) as $ty
|
||||
}
|
||||
};
|
||||
($getter: ident, $setter: ident, $byte: expr, $bits: expr, $ty: ty, $doc: expr) => {
|
||||
#[allow(unused)]
|
||||
#[doc = $doc]
|
||||
pub fn $getter(&self) -> $ty {
|
||||
self.0[$byte].get_bits($bits).into()
|
||||
}
|
||||
#[allow(unused)]
|
||||
#[doc = $doc]
|
||||
pub fn $setter(&mut self, value: $ty) {
|
||||
self.0[$byte].set_bits($bits, value as u8);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
def_reg!(Status, status, 0x00, 1);
|
||||
impl status::Data {
|
||||
/// Is there new data to read?
|
||||
pub fn ready(&self) -> bool {
|
||||
! self.not_ready()
|
||||
}
|
||||
|
||||
reg_bit!(not_ready, 0, 7, "No data ready indicator");
|
||||
reg_bits!(channel, 0, 0..=1, "Channel for which data is ready");
|
||||
reg_bit!(adc_error, 0, 6, "ADC error");
|
||||
reg_bit!(crc_error, 0, 5, "SPI CRC error");
|
||||
reg_bit!(reg_error, 0, 4, "Register error");
|
||||
}
|
||||
|
||||
def_reg!(AdcMode, adc_mode, 0x01, 2);
|
||||
impl adc_mode::Data {
|
||||
reg_bits!(delay, set_delay, 0, 0..=2, "Delay after channel switch");
|
||||
reg_bit!(sing_cyc, set_sing_cyc, 0, 5, "Can only used with single channel");
|
||||
reg_bit!(hide_delay, set_hide_delay, 0, 6, "Hide delay");
|
||||
reg_bit!(ref_en, set_ref_en, 0, 7, "Enable internal reference, output buffered 2.5 V to REFOUT");
|
||||
reg_bits!(clockset, set_clocksel, 1, 2..=3, "Clock source");
|
||||
reg_bits!(mode, set_mode, 1, 4..=6, Mode, "Operating mode");
|
||||
}
|
||||
|
||||
def_reg!(IfMode, if_mode, 0x02, 2);
|
||||
impl if_mode::Data {
|
||||
reg_bits!(crc, set_crc, 1, 2..=3, ChecksumMode, "SPI checksum mode");
|
||||
}
|
||||
|
||||
def_reg!(Data, data, 0x04, 3);
|
||||
impl data::Data {
|
||||
pub fn data(&self) -> u32 {
|
||||
(u32::from(self.0[0]) << 16) |
|
||||
(u32::from(self.0[1]) << 8) |
|
||||
u32::from(self.0[2])
|
||||
}
|
||||
}
|
||||
|
||||
def_reg!(GpioCon, gpio_con, 0x06, 2);
|
||||
impl gpio_con::Data {
|
||||
reg_bit!(sync_en, set_sync_en, 0, 3, "Enables the SYNC/ERROR pin as a sync input");
|
||||
}
|
||||
|
||||
def_reg!(Id, id, 0x07, 2);
|
||||
impl id::Data {
|
||||
pub fn id(&self) -> u16 {
|
||||
BigEndian::read_u16(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
def_reg!(Channel, u8, channel, 0x10, 2);
|
||||
impl channel::Data {
|
||||
reg_bit!(enabled, set_enabled, 0, 7, "Channel enabled");
|
||||
reg_bits!(setup, set_setup, 0, 4..=5, "Setup number");
|
||||
|
||||
/// Which input is connected to positive input of this channel
|
||||
#[allow(unused)]
|
||||
pub fn a_in_pos(&self) -> Input {
|
||||
((self.0[0].get_bits(0..=1) << 3) |
|
||||
self.0[1].get_bits(5..=7)).into()
|
||||
}
|
||||
/// Set which input is connected to positive input of this channel
|
||||
#[allow(unused)]
|
||||
pub fn set_a_in_pos(&mut self, value: Input) {
|
||||
let value = value as u8;
|
||||
self.0[0].set_bits(0..=1, value >> 3);
|
||||
self.0[1].set_bits(5..=7, value & 0x7);
|
||||
}
|
||||
reg_bits!(a_in_neg, set_a_in_neg, 1, 0..=4, Input,
|
||||
"Which input is connected to negative input of this channel");
|
||||
}
|
||||
|
||||
def_reg!(SetupCon, u8, setup_con, 0x20, 2);
|
||||
impl setup_con::Data {
|
||||
reg_bit!(bipolar, set_bipolar, 0, 4, "Unipolar (`false`) or bipolar (`true`) coded output");
|
||||
reg_bit!(refbuf_pos, set_refbuf_pos, 0, 3, "Enable REF+ input buffer");
|
||||
reg_bit!(refbuf_neg, set_refbuf_neg, 0, 2, "Enable REF- input buffer");
|
||||
reg_bit!(ainbuf_pos, set_ainbuf_pos, 0, 1, "Enable AIN+ input buffer");
|
||||
reg_bit!(ainbuf_neg, set_ainbuf_neg, 0, 0, "Enable AIN- input buffer");
|
||||
reg_bit!(burnout_en, 1, 7, "enables a 10 µA current source on the positive analog input selected and a 10 µA current sink on the negative analog input selected");
|
||||
reg_bits!(ref_sel, set_ref_sel, 1, 4..=5, RefSource, "Select reference source for conversion");
|
||||
}
|
||||
|
||||
def_reg!(FiltCon, u8, filt_con, 0x28, 2);
|
||||
impl filt_con::Data {
|
||||
reg_bit!(sinc3_map, 0, 7, "If set, mapping of filter register changes to directly program the decimation rate of the sinc3 filter");
|
||||
reg_bit!(enh_filt_en, set_enh_filt_en, 0, 3, "Enable postfilters for enhanced 50Hz and 60Hz rejection");
|
||||
reg_bits!(enh_filt, set_enh_filt, 0, 0..=2, PostFilter, "Select postfilters for enhanced 50Hz and 60Hz rejection");
|
||||
reg_bits!(order, set_order, 1, 5..=6, DigitalFilterOrder, "order of the digital filter that processes the modulator data");
|
||||
reg_bits!(odr, set_odr, 1, 0..=4, "Output data rate");
|
||||
}
|
||||
|
||||
def_reg!(Offset, u8, offset, 0x30, 3);
|
||||
impl offset::Data {
|
||||
#[allow(unused)]
|
||||
pub fn offset(&self) -> u32 {
|
||||
(u32::from(self.0[0]) << 16) |
|
||||
(u32::from(self.0[1]) << 8) |
|
||||
u32::from(self.0[2])
|
||||
}
|
||||
#[allow(unused)]
|
||||
pub fn set_offset(&mut self, value: u32) {
|
||||
self.0[0] = (value >> 16) as u8;
|
||||
self.0[1] = (value >> 8) as u8;
|
||||
self.0[2] = value as u8;
|
||||
}
|
||||
}
|
||||
|
||||
def_reg!(Gain, u8, gain, 0x38, 3);
|
||||
impl gain::Data {
|
||||
#[allow(unused)]
|
||||
pub fn gain(&self) -> u32 {
|
||||
(u32::from(self.0[0]) << 16) |
|
||||
(u32::from(self.0[1]) << 8) |
|
||||
u32::from(self.0[2])
|
||||
}
|
||||
#[allow(unused)]
|
||||
pub fn set_gain(&mut self, value: u32) {
|
||||
self.0[0] = (value >> 16) as u8;
|
||||
self.0[1] = (value >> 8) as u8;
|
||||
self.0[2] = value as u8;
|
||||
}
|
||||
}
|
@ -42,9 +42,7 @@ impl ChannelPins for Channel0 {
|
||||
type ShdnPin = PA5<Output<PushPull>>;
|
||||
type VRefPin = PA6<Analog>;
|
||||
type ItecPin = PB1<Analog>;
|
||||
// Fixme: Flywire is added to Rev0_2 prototype. In Rev0_3, it is connected to PC0
|
||||
//type DacFeedbackPin = PC0<Analog>;
|
||||
type DacFeedbackPin = PC3<Analog>;
|
||||
type DacFeedbackPin = PC0<Analog>;
|
||||
type VTecPin = PB0<Analog>;
|
||||
type MaxVPin = PwmChannel<TIM4, 1>;
|
||||
type MaxIPosPin = PwmChannel<TIM4, 2>;
|
||||
@ -115,11 +113,17 @@ impl<C: ChannelPins> MAX1968Phy<C> {
|
||||
|
||||
impl MAX1968 {
|
||||
pub fn new(phy_ch0: MAX1968Phy<Channel0>, adc1: ADC1) -> Self {
|
||||
// Set ADC to a slowest sampling interval for more accurate calibration
|
||||
let config = AdcConfig::default()
|
||||
.clock(config::Clock::Pclk2_div_2)
|
||||
.clock(config::Clock::Pclk2_div_8)
|
||||
.default_sample_time(config::SampleTime::Cycles_480);
|
||||
let mut pins_adc = Adc::adc1(adc1, true, config);
|
||||
pins_adc.calibrate();
|
||||
|
||||
let pins_adc = Adc::adc1(adc1, true, config);
|
||||
let config = AdcConfig::default()
|
||||
.clock(config::Clock::Pclk2_div_8)
|
||||
.default_sample_time(config::SampleTime::Cycles_480);
|
||||
pins_adc.apply_config(config);
|
||||
|
||||
MAX1968 {
|
||||
phy: phy_ch0,
|
||||
@ -127,6 +131,12 @@ impl MAX1968 {
|
||||
}
|
||||
}
|
||||
|
||||
// Return the calibrated VDDA Voltage
|
||||
// Can be used to set reference voltage for other ADC
|
||||
pub fn get_calibrated_vdda(&mut self) -> u32 {
|
||||
self.pins_adc.reference_voltage()
|
||||
}
|
||||
|
||||
pub fn power_down(&mut self) {
|
||||
self.phy.shdn.set_low();
|
||||
}
|
||||
|
@ -1,3 +1,6 @@
|
||||
pub mod ad5680;
|
||||
pub mod max1968;
|
||||
pub mod thermostat;
|
||||
pub mod thermostat;
|
||||
pub mod ad7172;
|
||||
pub mod steinhart_hart;
|
||||
pub mod pid_state;
|
||||
|
110
src/thermostat/pid_state.rs
Normal file
110
src/thermostat/pid_state.rs
Normal file
@ -0,0 +1,110 @@
|
||||
use smoltcp::time::{Duration, Instant};
|
||||
use uom::si::{
|
||||
f64::{
|
||||
ElectricPotential,
|
||||
ElectricalResistance,
|
||||
ThermodynamicTemperature,
|
||||
Time,
|
||||
},
|
||||
electric_potential::volt,
|
||||
electrical_resistance::ohm,
|
||||
thermodynamic_temperature::degree_celsius,
|
||||
time::millisecond,
|
||||
};
|
||||
use crate::thermostat::{
|
||||
ad7172,
|
||||
steinhart_hart as sh,
|
||||
};
|
||||
use crate::pid::pid;
|
||||
|
||||
const R_INNER: f64 = 2.0 * 5100.0;
|
||||
const VREF_SENS: f64 = 3.3 / 2.0;
|
||||
|
||||
pub struct PidState {
|
||||
pub adc_data: Option<u32>,
|
||||
pub adc_calibration: ad7172::ChannelCalibration,
|
||||
pub update_ts: Time,
|
||||
pub update_interval: Time,
|
||||
/// i_set 0A center point
|
||||
pub center_point: ElectricPotential,
|
||||
pub dac_volt: ElectricPotential,
|
||||
pub pid_engaged: bool,
|
||||
pub pid: pid::Controller,
|
||||
pub sh: sh::Parameters,
|
||||
}
|
||||
|
||||
impl PidState {
|
||||
fn adc_calibration(mut self, adc_calibration: ad7172::ChannelCalibration) -> Self {
|
||||
self.adc_calibration = adc_calibration;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PidState {
|
||||
fn default() -> Self {
|
||||
PidState {
|
||||
adc_data: None,
|
||||
adc_calibration: ad7172::ChannelCalibration::default(),
|
||||
update_ts: Time::new::<millisecond>(0.0),
|
||||
// default: 10 Hz
|
||||
update_interval: Time::new::<millisecond>(100.0),
|
||||
center_point: ElectricPotential::new::<volt>(1.5),
|
||||
dac_volt: ElectricPotential::new::<volt>(0.0),
|
||||
pid_engaged: false,
|
||||
pid: pid::Controller::new(pid::Parameters::default()),
|
||||
sh: sh::Parameters::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PidState {
|
||||
pub fn new(adc_calibration: ad7172::ChannelCalibration) -> Self {
|
||||
PidState::default().adc_calibration(adc_calibration)
|
||||
}
|
||||
|
||||
pub fn update(&mut self, now: Instant, adc_data: u32) {
|
||||
self.adc_data = if adc_data == ad7172::MAX_VALUE {
|
||||
// this means there is no thermistor plugged into the ADC.
|
||||
None
|
||||
} else {
|
||||
Some(adc_data)
|
||||
};
|
||||
self.update_interval = Time::new::<millisecond>(now.millis() as f64) - self.update_ts;
|
||||
self.update_ts = Time::new::<millisecond>(now.millis() as f64);
|
||||
}
|
||||
|
||||
/// Update PID state on ADC input, calculate new DAC output
|
||||
pub fn update_pid(&mut self) -> Option<f64> {
|
||||
let temperature = self.get_temperature()?
|
||||
.get::<degree_celsius>();
|
||||
let pid_output = self.pid.update(temperature);
|
||||
Some(pid_output)
|
||||
}
|
||||
|
||||
pub fn get_update_ts(&self) -> Time {
|
||||
self.update_ts
|
||||
}
|
||||
|
||||
pub fn get_update_interval(&self) -> Time {
|
||||
self.update_interval
|
||||
}
|
||||
|
||||
pub fn get_adc(&self) -> Option<ElectricPotential> {
|
||||
Some(self.adc_calibration.convert_data(self.adc_data?))
|
||||
}
|
||||
|
||||
/// Get `SENS[01]` input resistance
|
||||
pub fn get_sens(&self) -> Option<ElectricalResistance> {
|
||||
let r_inner = ElectricalResistance::new::<ohm>(R_INNER);
|
||||
let vref = ElectricPotential::new::<volt>(VREF_SENS);
|
||||
let adc_input = self.get_adc()?;
|
||||
let r = r_inner * adc_input / (vref - adc_input);
|
||||
Some(r)
|
||||
}
|
||||
|
||||
pub fn get_temperature(&self) -> Option<ThermodynamicTemperature> {
|
||||
let r = self.get_sens()?;
|
||||
let temperature = self.sh.get_temperature(r);
|
||||
Some(temperature)
|
||||
}
|
||||
}
|
40
src/thermostat/steinhart_hart.rs
Normal file
40
src/thermostat/steinhart_hart.rs
Normal file
@ -0,0 +1,40 @@
|
||||
use num_traits::float::Float;
|
||||
use uom::si::{
|
||||
f64::{
|
||||
ElectricalResistance,
|
||||
ThermodynamicTemperature,
|
||||
},
|
||||
electrical_resistance::ohm,
|
||||
ratio::ratio,
|
||||
thermodynamic_temperature::{degree_celsius, kelvin},
|
||||
};
|
||||
use miniconf::Miniconf;
|
||||
|
||||
/// Steinhart-Hart equation parameters
|
||||
#[derive(Clone, Debug, PartialEq, Miniconf)]
|
||||
pub struct Parameters {
|
||||
/// Base temperature
|
||||
pub t0: ThermodynamicTemperature,
|
||||
/// Base resistance
|
||||
pub r0: ElectricalResistance,
|
||||
/// Beta
|
||||
pub b: f64,
|
||||
}
|
||||
|
||||
impl Parameters {
|
||||
/// Perform the voltage to temperature conversion.
|
||||
pub fn get_temperature(&self, r: ElectricalResistance) -> ThermodynamicTemperature {
|
||||
let inv_temp = 1.0 / self.t0.get::<kelvin>() + (r / self.r0).get::<ratio>().ln() / self.b;
|
||||
ThermodynamicTemperature::new::<kelvin>(1.0 / inv_temp)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Parameters {
|
||||
fn default() -> Self {
|
||||
Parameters {
|
||||
t0: ThermodynamicTemperature::new::<degree_celsius>(25.0),
|
||||
r0: ElectricalResistance::new::<ohm>(10_000.0),
|
||||
b: 3800.0,
|
||||
}
|
||||
}
|
||||
}
|
@ -1,13 +1,17 @@
|
||||
use core::marker::PhantomData;
|
||||
use crate::sys_timer;
|
||||
use smoltcp::time::Instant;
|
||||
use crate::{sys_timer, pid::pid};
|
||||
use crate::thermostat::ad5680;
|
||||
use crate::thermostat::max1968::{MAX1968, AdcReadTarget, PwmPinsEnum};
|
||||
use crate::thermostat::ad7172;
|
||||
use crate::thermostat::pid_state::PidState;
|
||||
use crate::thermostat::steinhart_hart;
|
||||
use log::info;
|
||||
use uom::si::{
|
||||
electric_current::ampere,
|
||||
electric_potential::volt,
|
||||
electrical_resistance::ohm,
|
||||
f64::{ElectricCurrent, ElectricPotential, ElectricalResistance},
|
||||
f64::{ElectricCurrent, ElectricPotential, ElectricalResistance, Time, ThermodynamicTemperature},
|
||||
ratio::ratio,
|
||||
};
|
||||
use miniconf::Miniconf;
|
||||
@ -19,7 +23,7 @@ pub const R_SENSE: ElectricalResistance = ElectricalResistance {
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Miniconf)]
|
||||
pub struct Settings {
|
||||
pub struct TecSettings {
|
||||
pub center_pt: ElectricPotential,
|
||||
pub max_v_set: ElectricPotential,
|
||||
pub max_i_pos_set: ElectricCurrent,
|
||||
@ -27,7 +31,7 @@ pub struct Settings {
|
||||
pub i_set: ElectricCurrent,
|
||||
}
|
||||
|
||||
impl Settings{
|
||||
impl TecSettings{
|
||||
// FixMe: Rev0_2 is 3.3V max while Rev0_3 is 3.0V max
|
||||
pub const DAC_OUT_V_MAX: ElectricPotential = ElectricPotential {
|
||||
dimension: PhantomData,
|
||||
@ -43,6 +47,11 @@ impl Settings{
|
||||
// Kirdy Design Specs:
|
||||
// MaxV = 5.0V
|
||||
// MAX Current = +- 1.0A
|
||||
const MAX_I_SET : ElectricCurrent = ElectricCurrent {
|
||||
dimension: PhantomData,
|
||||
units: PhantomData,
|
||||
value: 1.0,
|
||||
};
|
||||
const MAX_V_DUTY_TO_CURRENT_RATE: ElectricPotential = ElectricPotential {
|
||||
dimension: PhantomData,
|
||||
units: PhantomData,
|
||||
@ -53,7 +62,7 @@ impl Settings{
|
||||
units: PhantomData,
|
||||
value: 5.0,
|
||||
};
|
||||
const MAX_V_DUTY_MAX: f64 = Settings::MAX_V_MAX.value / Settings::MAX_V_DUTY_TO_CURRENT_RATE.value;
|
||||
const MAX_V_DUTY_MAX: f64 = TecSettings::MAX_V_MAX.value / TecSettings::MAX_V_DUTY_TO_CURRENT_RATE.value;
|
||||
const MAX_I_POS_NEG_DUTY_TO_CURRENT_RATE: ElectricCurrent = ElectricCurrent {
|
||||
dimension: PhantomData,
|
||||
units: PhantomData,
|
||||
@ -70,11 +79,11 @@ impl Settings{
|
||||
value: 1.0,
|
||||
};
|
||||
// .get::<ratio>() is not implemented for const
|
||||
const MAX_I_POS_DUTY_MAX: f64 = Settings::MAX_I_POS_CURRENT.value / Settings::MAX_I_POS_NEG_DUTY_TO_CURRENT_RATE.value;
|
||||
const MAX_I_NEG_DUTY_MAX: f64 = Settings::MAX_I_NEG_CURRENT.value / Settings::MAX_I_POS_NEG_DUTY_TO_CURRENT_RATE.value;
|
||||
const MAX_I_POS_DUTY_MAX: f64 = TecSettings::MAX_I_POS_CURRENT.value / TecSettings::MAX_I_POS_NEG_DUTY_TO_CURRENT_RATE.value;
|
||||
const MAX_I_NEG_DUTY_MAX: f64 = TecSettings::MAX_I_NEG_CURRENT.value / TecSettings::MAX_I_POS_NEG_DUTY_TO_CURRENT_RATE.value;
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
impl Default for TecSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
center_pt: ElectricPotential::new::<volt>(1.5),
|
||||
@ -88,21 +97,30 @@ impl Default for Settings {
|
||||
|
||||
pub struct Thermostat {
|
||||
max1968: MAX1968,
|
||||
pub tec_setting: Settings,
|
||||
// TADC
|
||||
ad7172: ad7172::AdcPhy,
|
||||
pub tec_setting: TecSettings,
|
||||
pid_ctrl_ch0: PidState,
|
||||
}
|
||||
|
||||
impl Thermostat{
|
||||
pub fn new (max1968: MAX1968) -> Self {
|
||||
pub fn new (max1968: MAX1968, ad7172: ad7172::AdcPhy) -> Self {
|
||||
Thermostat{
|
||||
max1968: max1968,
|
||||
tec_setting: Settings::default(),
|
||||
ad7172: ad7172,
|
||||
tec_setting: TecSettings::default(),
|
||||
pid_ctrl_ch0: PidState::default(),
|
||||
}
|
||||
}
|
||||
pub fn setup(&mut self) {
|
||||
pub fn setup(&mut self){
|
||||
self.tec_setup();
|
||||
let t_adc_ch0_cal = self.t_adc_setup();
|
||||
self.pid_ctrl_ch0.adc_calibration = t_adc_ch0_cal;
|
||||
}
|
||||
|
||||
fn tec_setup(&mut self) {
|
||||
self.power_down();
|
||||
|
||||
self.tec_setting = Settings::default();
|
||||
self.tec_setting = TecSettings::default();
|
||||
|
||||
self.set_i(self.tec_setting.i_set);
|
||||
|
||||
@ -111,6 +129,36 @@ impl Thermostat{
|
||||
self.set_max_i_neg(self.tec_setting.max_i_neg_set);
|
||||
}
|
||||
|
||||
fn t_adc_setup(&mut self)->ad7172::ChannelCalibration{
|
||||
self.ad7172.set_sync_enable(false).unwrap();
|
||||
self.ad7172.setup_channel(0, ad7172::Input::Ain0, ad7172::Input::Ain1).unwrap();
|
||||
let adc_calibration0 = self.ad7172.get_calibration(0)
|
||||
.expect("adc_calibration0");
|
||||
self.ad7172.start_continuous_conversion().unwrap();
|
||||
adc_calibration0
|
||||
}
|
||||
|
||||
pub fn poll_adc(&mut self, instant: Instant) -> Option<u8> {
|
||||
self.ad7172.data_ready().unwrap().map(|channel| {
|
||||
let data = self.ad7172.read_data().unwrap();
|
||||
let state: &mut PidState = &mut self.pid_ctrl_ch0;
|
||||
state.update(instant, data);
|
||||
match state.update_pid() {
|
||||
Some(pid_output) if state.pid_engaged => {
|
||||
// Forward PID output to i_set DAC
|
||||
self.set_i(ElectricCurrent::new::<ampere>(pid_output));
|
||||
self.power_up();
|
||||
}
|
||||
None if state.pid_engaged => {
|
||||
self.power_down();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
channel
|
||||
})
|
||||
}
|
||||
|
||||
pub fn power_up(&mut self){
|
||||
self.max1968.power_up();
|
||||
}
|
||||
@ -126,29 +174,29 @@ impl Thermostat{
|
||||
|
||||
pub fn set_i(&mut self, i_tec: ElectricCurrent) -> ElectricCurrent {
|
||||
let voltage = i_tec * 10.0 * R_SENSE + self.tec_setting.center_pt;
|
||||
let voltage = self.max1968.set_dac(voltage, Settings::DAC_OUT_V_MAX);
|
||||
let voltage = self.max1968.set_dac(voltage, TecSettings::DAC_OUT_V_MAX);
|
||||
self.tec_setting.i_set = (voltage - self.tec_setting.center_pt) / (10.0 * R_SENSE);
|
||||
self.tec_setting.i_set
|
||||
}
|
||||
|
||||
pub fn set_max_v(&mut self, max_v: ElectricPotential) -> ElectricPotential {
|
||||
let duty = (max_v / Settings::MAX_V_DUTY_TO_CURRENT_RATE).get::<ratio>();
|
||||
let duty = self.max1968.set_pwm(PwmPinsEnum::MaxV, duty, Settings::MAX_V_DUTY_MAX);
|
||||
self.tec_setting.max_v_set = duty * Settings::MAX_V_DUTY_TO_CURRENT_RATE;
|
||||
let duty = (max_v / TecSettings::MAX_V_DUTY_TO_CURRENT_RATE).get::<ratio>();
|
||||
let duty = self.max1968.set_pwm(PwmPinsEnum::MaxV, duty, TecSettings::MAX_V_DUTY_MAX);
|
||||
self.tec_setting.max_v_set = duty * TecSettings::MAX_V_DUTY_TO_CURRENT_RATE;
|
||||
self.tec_setting.max_v_set
|
||||
}
|
||||
|
||||
pub fn set_max_i_pos(&mut self, max_i_pos: ElectricCurrent) -> ElectricCurrent {
|
||||
let duty = (max_i_pos / Settings::MAX_I_POS_NEG_DUTY_TO_CURRENT_RATE).get::<ratio>();
|
||||
let duty = self.max1968.set_pwm(PwmPinsEnum::MaxPosI, duty, Settings::MAX_I_POS_DUTY_MAX);
|
||||
self.tec_setting.max_i_pos_set = duty * Settings::MAX_I_POS_NEG_DUTY_TO_CURRENT_RATE;
|
||||
let duty = (max_i_pos / TecSettings::MAX_I_POS_NEG_DUTY_TO_CURRENT_RATE).get::<ratio>();
|
||||
let duty = self.max1968.set_pwm(PwmPinsEnum::MaxPosI, duty, TecSettings::MAX_I_POS_DUTY_MAX);
|
||||
self.tec_setting.max_i_pos_set = duty * TecSettings::MAX_I_POS_NEG_DUTY_TO_CURRENT_RATE;
|
||||
self.tec_setting.max_i_pos_set
|
||||
}
|
||||
|
||||
pub fn set_max_i_neg(&mut self, max_i_neg: ElectricCurrent) -> ElectricCurrent {
|
||||
let duty = (max_i_neg / Settings::MAX_I_POS_NEG_DUTY_TO_CURRENT_RATE).get::<ratio>();
|
||||
let duty = self.max1968.set_pwm(PwmPinsEnum::MaxNegI, duty, Settings::MAX_I_NEG_DUTY_MAX);
|
||||
self.tec_setting.max_i_neg_set = duty * Settings::MAX_I_POS_NEG_DUTY_TO_CURRENT_RATE;
|
||||
let duty = (max_i_neg / TecSettings::MAX_I_POS_NEG_DUTY_TO_CURRENT_RATE).get::<ratio>();
|
||||
let duty = self.max1968.set_pwm(PwmPinsEnum::MaxNegI, duty, TecSettings::MAX_I_NEG_DUTY_MAX);
|
||||
self.tec_setting.max_i_neg_set = duty * TecSettings::MAX_I_POS_NEG_DUTY_TO_CURRENT_RATE;
|
||||
self.tec_setting.max_i_neg_set
|
||||
}
|
||||
|
||||
@ -168,7 +216,7 @@ impl Thermostat{
|
||||
pub fn get_tec_v(&mut self) -> ElectricPotential {
|
||||
// Fixme: Rev0_2 has Analog Input Polarity Reversed
|
||||
// Remove the -ve sign for Rev0_3
|
||||
-(self.max1968.adc_read(AdcReadTarget::VTec, 16) - Settings::TEC_VSEC_BIAS_V) * 4.0
|
||||
-(self.max1968.adc_read(AdcReadTarget::VTec, 16) - TecSettings::TEC_VSEC_BIAS_V) * 4.0
|
||||
}
|
||||
|
||||
/// Calibrates the DAC output to match vref of the MAX driver to reduce zero-current offset of the MAX driver output.
|
||||
@ -207,7 +255,7 @@ impl Thermostat{
|
||||
best_error = error;
|
||||
start_value = prev_value;
|
||||
|
||||
let vref = (value as f64 / ad5680::MAX_VALUE as f64) * Settings::DAC_OUT_V_MAX;
|
||||
let vref = (value as f64 / ad5680::MAX_VALUE as f64) * TecSettings::DAC_OUT_V_MAX;
|
||||
self.set_center_pt(vref);
|
||||
}
|
||||
prev_value = value;
|
||||
@ -216,4 +264,83 @@ impl Thermostat{
|
||||
info!("Best Error: {:?}", best_error);
|
||||
}
|
||||
|
||||
}
|
||||
pub fn pid_engaged(&mut self) -> bool {
|
||||
if self.pid_ctrl_ch0.pid_engaged {
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn get_status_report(&mut self) -> StatusReport {
|
||||
StatusReport {
|
||||
pid_update_ts: self.pid_ctrl_ch0.get_update_ts(),
|
||||
pid_update_interval: self.pid_ctrl_ch0.get_update_interval(),
|
||||
pid_engaged: self.pid_engaged(),
|
||||
temperature: self.pid_ctrl_ch0.get_temperature(),
|
||||
i_set: self.tec_setting.i_set,
|
||||
tec_i: self.get_tec_i(),
|
||||
tec_v: self.get_tec_v(),
|
||||
tec_vref: self.get_vref(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_pid_settings(&mut self) -> pid::Controller {
|
||||
self.pid_ctrl_ch0.pid.clone()
|
||||
}
|
||||
|
||||
pub fn get_steinhart_hart(&mut self) -> steinhart_hart::Parameters {
|
||||
self.pid_ctrl_ch0.sh.clone()
|
||||
}
|
||||
|
||||
pub fn get_tec_settings(&mut self) -> TecSettingSummary {
|
||||
TecSettingSummary {
|
||||
center_point: self.tec_setting.center_pt,
|
||||
i_set: TecSettingsSummaryField { value: self.tec_setting.i_set, max: TecSettings::MAX_I_SET },
|
||||
max_v: TecSettingsSummaryField { value: self.tec_setting.max_v_set, max: TecSettings::MAX_V_MAX },
|
||||
max_i_pos: TecSettingsSummaryField { value: self.tec_setting.max_i_pos_set, max: TecSettings::MAX_I_POS_CURRENT },
|
||||
max_i_neg: TecSettingsSummaryField { value: self.tec_setting.max_i_neg_set, max: TecSettings::MAX_I_NEG_CURRENT },
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_calibrated_vdda(&mut self) -> u32 {
|
||||
self.max1968.get_calibrated_vdda()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[derive(Miniconf)]
|
||||
pub struct StatusReport {
|
||||
pid_update_ts: Time,
|
||||
pid_update_interval: Time,
|
||||
pid_engaged: bool,
|
||||
temperature: Option<ThermodynamicTemperature>,
|
||||
i_set: ElectricCurrent,
|
||||
tec_i: ElectricCurrent,
|
||||
tec_v: ElectricPotential,
|
||||
tec_vref: ElectricPotential,
|
||||
}
|
||||
|
||||
#[derive(Miniconf)]
|
||||
pub struct TecSettingsSummaryField<T> {
|
||||
value: T,
|
||||
max: T,
|
||||
}
|
||||
|
||||
#[derive(Miniconf)]
|
||||
pub struct TecSettingSummary {
|
||||
center_point: ElectricPotential,
|
||||
#[miniconf(defer)]
|
||||
i_set: TecSettingsSummaryField<ElectricCurrent>,
|
||||
#[miniconf(defer)]
|
||||
max_v: TecSettingsSummaryField<ElectricPotential>,
|
||||
#[miniconf(defer)]
|
||||
max_i_pos: TecSettingsSummaryField<ElectricCurrent>,
|
||||
#[miniconf(defer)]
|
||||
max_i_neg: TecSettingsSummaryField<ElectricCurrent>,
|
||||
}
|
||||
|
||||
#[derive(Miniconf)]
|
||||
pub struct SteinhartHartSummary {
|
||||
#[miniconf(defer)]
|
||||
params: steinhart_hart::Parameters,
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user