Compare commits

...

21 Commits

Author SHA1 Message Date
linuswck 5bea3f0e5f boot: init analog wdg & power up laser after cfg
- Verified to be boot-able on Rev0_3 board
2024-01-24 12:19:07 +08:00
linuswck af283b17ac laser_diode: use analog wdg fns 2024-01-24 12:18:42 +08:00
linuswck c3022e9db1 Analog_Wdg: Add GPIO init 2024-01-24 12:18:42 +08:00
linuswck c02181c80c laser diode: Add Analog Watchdog initial commit
- rm pid_state for laser diode
2024-01-24 12:18:42 +08:00
linuswck 095fe8ea69 AD7172: Rm deprecated warning for Rev0_2 hw bug 2024-01-24 12:18:42 +08:00
linuswck a0b67cdb09 MAX1968: Patch DacVfb pinout for rev0_3 2024-01-24 12:18:42 +08:00
linuswck 381f25f036 AD7172: correct wrong NSS pinout definition 2024-01-24 12:18:42 +08:00
linuswck c768bdc93a Thermostat: Add fn to calibrate ADC's VDDA
- ADC1 exclusive feature
- Calibrated VDDA val can be passed to other ADCs to adjust gain error
2024-01-24 12:18:42 +08:00
linuswck 0d12c902fc pd_mon: Separate ld_power calculation to a file 2024-01-24 12:18:37 +08:00
linuswck 3cfdee917a rename current_source -> ld_ctrl 2024-01-24 12:16:06 +08:00
linuswck 5f582be143 rename ld_drive to laser_diode 2024-01-24 12:16:06 +08:00
linuswck 2f7ca2a706 Thermostat: Add fns to report status & settings
- Report all system status, PID Settings, NTC parameter, TEC Settings
2024-01-24 12:16:06 +08:00
linuswck ff3d9b790a thermostat: pid_state rename variables 2024-01-24 12:16:06 +08:00
linuswck bc7bf9a6e7 rename channel_state -> pid_state 2024-01-24 12:16:02 +08:00
linuswck ca110962f7 Thermostat: Add pid_ctrl parms & its related fns
- setup: include the pid_ctrl
- poll_adc: Fetch Temperature and Update PID to set the current output
2024-01-24 12:16:02 +08:00
linuswck d0f226ce03 Thermostat: Add default to ChannelState 2024-01-24 12:16:02 +08:00
linuswck f49fd08c69 ad7172: impl default for ch_calibration 2024-01-24 12:16:02 +08:00
linuswck ccaf728c75 thermostat: Add channel_state mod for PID Control 2024-01-24 12:15:57 +08:00
linuswck 3ac287ace2 steinhart_hart: Add Steinhart-Hart eq fns
- Port from thermostat firmware
2024-01-17 12:20:34 +08:00
linuswck cdf900a5b6 gpio: Add AD7172 phy gpio init
- Will not function on rev0_2 kirdy due to wrong hardware connection
2024-01-16 17:36:16 +08:00
linuswck af8d361b95 AD7172: Add AD7172 drivers fns
- Port from Thermostat Firmware
2024-01-16 16:19:01 +08:00
20 changed files with 1512 additions and 130 deletions

2
Cargo.lock generated
View File

@ -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",

View File

@ -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 = []

View File

@ -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();

View File

@ -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)
}

View 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();
}
)
}

View File

@ -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!()
}
}

View File

@ -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;

View File

@ -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
View 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),
}
}
}

View File

@ -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()));

View File

@ -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,
}

View 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(&regs::AdcMode, &mut adc_mode)?;
Ok(adc)
}
/// `0x00DX` for AD7172-2
pub fn identify(&mut self) -> Result<u16, SPI::Error> {
self.read_reg(&regs::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(&regs::IfMode)?;
ifmode.set_crc(mode);
self.checksum_mode = mode;
self.write_reg(&regs::IfMode, &mut ifmode)?;
Ok(())
}
pub fn set_sync_enable(&mut self, enable: bool) -> Result<(), SPI::Error> {
self.update_reg(&regs::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(&regs::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(&regs::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(&regs::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(&regs::Offset { index })?.offset();
let gain = self.read_reg(&regs::Gain { index })?.gain();
let bipolar = self.read_reg(&regs::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(&regs::AdcMode, &mut adc_mode)?;
Ok(())
}
pub fn get_postfilter(&mut self, index: u8) -> Result<Option<PostFilter>, SPI::Error> {
self.read_reg(&regs::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(&regs::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(&regs::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(&regs::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(&reg_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(&reg_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)
}
}

View 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)
}
}
}

View 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,
}
}
}

View 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;
}
}

View File

@ -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();
}

View File

@ -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
View 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)
}
}

View 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,
}
}
}

View File

@ -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,
}