forked from M-Labs/thermostat
Compare commits
No commits in common. "fix_wrong_current_limit_duty_cycle_cal" and "master" have entirely different histories.
fix_wrong_
...
master
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,5 +1,2 @@
|
||||
target/
|
||||
result
|
||||
*.bin
|
||||
|
||||
__pycache__/
|
||||
|
@ -45,7 +45,7 @@ There are several options for flashing Thermostat. DFU requires only a micro-USB
|
||||
|
||||
### dfu-util on Linux
|
||||
* Install the DFU USB tool (dfu-util).
|
||||
* Convert firmware from ELF to BIN: `llvm-objcopy -O binary target/thumbv7em-none-eabihf/release/thermostat thermostat.bin` (you can skip this step if using the BIN from Hydra)
|
||||
* Convert firmware from ELF to BIN: `arm-none-eabi-objcopy -O binary thermostat thermostat.bin` (you can skip this step if using the BIN from Hydra)
|
||||
* Connect to the Micro USB connector to Thermostat below the RJ45.
|
||||
* Add jumper to Thermostat v2.0 across 2-pin jumper adjacent to JTAG connector.
|
||||
* Cycle board power to put it in DFU update mode
|
||||
@ -271,7 +271,7 @@ with the following keys.
|
||||
| `tec_u_meas` | Volts | Measurement of the voltage across the TEC |
|
||||
| `pid_output` | Amperes | PID control output |
|
||||
|
||||
Note: With Thermostat v2 and below, the voltage and current readouts `i_tec` and `tec_i` are noisy without the hardware fix shown in [this PR][https://git.m-labs.hk/M-Labs/thermostat/pulls/105].
|
||||
Note: With Thermostat v2 and below, the voltage and current readouts `i_tec` and `tec_i` are disabled and null due to faulty hardware that introduces a lot of noise in the signal.
|
||||
|
||||
## PID Tuning
|
||||
|
||||
|
48
flake.lock
generated
48
flake.lock
generated
@ -1,45 +1,41 @@
|
||||
{
|
||||
"nodes": {
|
||||
"mozilla-overlay": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1690536331,
|
||||
"narHash": "sha256-aRIf2FB2GTdfF7gl13WyETmiV/J7EhBGkSWXfZvlxcA=",
|
||||
"owner": "mozilla",
|
||||
"repo": "nixpkgs-mozilla",
|
||||
"rev": "db89c8707edcffefcd8e738459d511543a339ff5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "mozilla",
|
||||
"repo": "nixpkgs-mozilla",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1722791413,
|
||||
"narHash": "sha256-rCTrlCWvHzMCNcKxPE3Z/mMK2gDZ+BvvpEVyRM4tKmU=",
|
||||
"lastModified": 1691421349,
|
||||
"narHash": "sha256-RRJyX0CUrs4uW4gMhd/X4rcDG8PTgaaCQM5rXEJOx6g=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "8b5b6723aca5a51edf075936439d9cd3947b7b2c",
|
||||
"rev": "011567f35433879aae5024fc6ec53f2a0568a6c4",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-24.05",
|
||||
"ref": "nixos-23.05",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs",
|
||||
"rust-overlay": "rust-overlay"
|
||||
}
|
||||
},
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1719281921,
|
||||
"narHash": "sha256-LIBMfhM9pMOlEvBI757GOK5l0R58SRi6YpwfYMbf4yc=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "b6032d3a404d8a52ecfc8571ff0c26dfbe221d07",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"type": "github"
|
||||
"mozilla-overlay": "mozilla-overlay",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
45
flake.nix
45
flake.nix
@ -1,25 +1,32 @@
|
||||
{
|
||||
description = "Firmware for the Sinara 8451 Thermostat";
|
||||
|
||||
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
|
||||
inputs.rust-overlay = {
|
||||
url = "github:oxalica/rust-overlay";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
inputs.nixpkgs.url = github:NixOS/nixpkgs/nixos-23.05;
|
||||
inputs.mozilla-overlay = { url = github:mozilla/nixpkgs-mozilla; flake = false; };
|
||||
|
||||
outputs = { self, nixpkgs, rust-overlay }:
|
||||
outputs = { self, nixpkgs, mozilla-overlay }:
|
||||
let
|
||||
pkgs = import nixpkgs { system = "x86_64-linux"; overlays = [ (import rust-overlay) ]; };
|
||||
|
||||
rust = pkgs.rust-bin.stable."1.66.0".default.override {
|
||||
extensions = [ "rust-src" ];
|
||||
targets = [ "thumbv7em-none-eabihf" ];
|
||||
pkgs = import nixpkgs { system = "x86_64-linux"; overlays = [ (import mozilla-overlay) ]; };
|
||||
rustManifest = pkgs.fetchurl {
|
||||
url = "https://static.rust-lang.org/dist/2022-12-15/channel-rust-stable.toml";
|
||||
hash = "sha256-S7epLlflwt0d1GZP44u5Xosgf6dRrmr8xxC+Ml2Pq7c=";
|
||||
};
|
||||
rustPlatform = pkgs.makeRustPlatform {
|
||||
|
||||
targets = [
|
||||
"thumbv7em-none-eabihf"
|
||||
];
|
||||
rustChannelOfTargets = _channel: _date: targets:
|
||||
(pkgs.lib.rustLib.fromManifestFile rustManifest {
|
||||
inherit (pkgs) stdenv lib fetchurl patchelf;
|
||||
}).rust.override {
|
||||
inherit targets;
|
||||
extensions = ["rust-src"];
|
||||
};
|
||||
rust = rustChannelOfTargets "stable" null targets;
|
||||
rustPlatform = pkgs.recurseIntoAttrs (pkgs.makeRustPlatform {
|
||||
rustc = rust;
|
||||
cargo = rust;
|
||||
};
|
||||
|
||||
});
|
||||
thermostat = rustPlatform.buildRustPackage {
|
||||
name = "thermostat";
|
||||
version = "0.0.0";
|
||||
@ -47,26 +54,24 @@
|
||||
'';
|
||||
|
||||
dontFixup = true;
|
||||
auditable = false;
|
||||
};
|
||||
in {
|
||||
packages.x86_64-linux = {
|
||||
inherit thermostat;
|
||||
default = thermostat;
|
||||
};
|
||||
|
||||
hydraJobs = {
|
||||
inherit thermostat;
|
||||
};
|
||||
|
||||
devShells.x86_64-linux.default = pkgs.mkShellNoCC {
|
||||
devShell.x86_64-linux = pkgs.mkShell {
|
||||
name = "thermostat-dev-shell";
|
||||
packages = with pkgs; [
|
||||
rust llvm
|
||||
openocd dfu-util rlwrap
|
||||
buildInputs = with pkgs; [
|
||||
rust openocd dfu-util
|
||||
] ++ (with python3Packages; [
|
||||
numpy matplotlib
|
||||
]);
|
||||
};
|
||||
defaultPackage.x86_64-linux = thermostat;
|
||||
};
|
||||
}
|
@ -24,7 +24,7 @@ pub struct Channel<C: ChannelPins> {
|
||||
pub vref_meas: ElectricPotential,
|
||||
pub shdn: C::Shdn,
|
||||
pub vref_pin: C::VRefPin,
|
||||
pub itec_pin: C::ITecPin,
|
||||
pub itec_pin: C::ItecPin,
|
||||
/// feedback from `dac` output
|
||||
pub dac_feedback_pin: C::DacFeedbackPin,
|
||||
pub tec_u_meas_pin: C::TecUMeasPin,
|
||||
|
301
src/channels.rs
301
src/channels.rs
@ -1,6 +1,5 @@
|
||||
use core::marker::PhantomData;
|
||||
use core::cmp::max_by;
|
||||
use heapless::{consts::U2, Vec};
|
||||
use num_traits::Zero;
|
||||
use serde::{Serialize, Serializer};
|
||||
use smoltcp::time::Instant;
|
||||
use stm32f4xx_hal::hal;
|
||||
@ -21,53 +20,31 @@ use crate::{
|
||||
command_handler::JsonBuffer,
|
||||
pins::{self, Channel0VRef, Channel1VRef},
|
||||
steinhart_hart,
|
||||
hw_rev,
|
||||
};
|
||||
use crate::timer::sleep;
|
||||
|
||||
pub enum PinsAdcReadTarget {
|
||||
VREF,
|
||||
DacVfb,
|
||||
ITec,
|
||||
VTec,
|
||||
}
|
||||
|
||||
pub const CHANNELS: usize = 2;
|
||||
pub const R_SENSE: f64 = 0.05;
|
||||
|
||||
// From design specs
|
||||
pub const MAX_TEC_I: ElectricCurrent = ElectricCurrent {
|
||||
dimension: PhantomData,
|
||||
units: PhantomData,
|
||||
value: 2.0,
|
||||
};
|
||||
pub const MAX_TEC_V: ElectricPotential = ElectricPotential {
|
||||
dimension: PhantomData,
|
||||
units: PhantomData,
|
||||
value: 4.0,
|
||||
};
|
||||
const MAX_TEC_I_DUTY_TO_CURRENT_RATE: ElectricCurrent = ElectricCurrent {
|
||||
dimension: PhantomData,
|
||||
units: PhantomData,
|
||||
value: 1.0 / (10.0 * R_SENSE / 3.3),
|
||||
};
|
||||
// as stated in the MAX1968 datasheet
|
||||
pub const MAX_TEC_I: f64 = 3.0;
|
||||
|
||||
// DAC chip outputs 0-5v, which is then passed through a resistor dividor to provide 0-3v range
|
||||
const DAC_OUT_V_MAX: ElectricPotential = ElectricPotential {
|
||||
dimension: PhantomData,
|
||||
units: PhantomData,
|
||||
value: 3.0,
|
||||
};
|
||||
const DAC_OUT_V_MAX: f64 = 3.0;
|
||||
|
||||
// TODO: -pub
|
||||
pub struct Channels {
|
||||
pub struct Channels<'a> {
|
||||
channel0: Channel<Channel0>,
|
||||
channel1: Channel<Channel1>,
|
||||
pub adc: ad7172::Adc<pins::AdcSpi, pins::AdcNss>,
|
||||
/// stm32f4 integrated adc
|
||||
pins_adc: pins::PinsAdc,
|
||||
pub pwm: pins::PwmPins,
|
||||
hwrev: &'a hw_rev::HWRev,
|
||||
}
|
||||
|
||||
impl Channels {
|
||||
pub fn new(pins: pins::Pins) -> Self {
|
||||
impl<'a> Channels<'a> {
|
||||
pub fn new(pins: pins::Pins, hwrev: &'a hw_rev::HWRev) -> Self {
|
||||
let mut adc = ad7172::Adc::new(pins.adc_spi, pins.adc_nss).unwrap();
|
||||
// Feature not used
|
||||
adc.set_sync_enable(false).unwrap();
|
||||
@ -85,7 +62,7 @@ impl Channels {
|
||||
let channel1 = Channel::new(pins.channel1, adc_calibration1);
|
||||
let pins_adc = pins.pins_adc;
|
||||
let pwm = pins.pwm;
|
||||
let mut channels = Channels { channel0, channel1, adc, pins_adc, pwm };
|
||||
let mut channels = Channels { channel0, channel1, adc, pins_adc, pwm, hwrev };
|
||||
for channel in 0..CHANNELS {
|
||||
channels.calibrate_dac_value(channel);
|
||||
channels.set_i(channel, ElectricCurrent::new::<ampere>(0.0));
|
||||
@ -127,7 +104,7 @@ impl Channels {
|
||||
pub fn get_center(&mut self, channel: usize) -> ElectricPotential {
|
||||
match self.channel_state(channel).center {
|
||||
CenterPoint::Vref =>
|
||||
self.adc_read(channel, PinsAdcReadTarget::VREF, 8),
|
||||
self.read_vref(channel),
|
||||
CenterPoint::Override(center_point) =>
|
||||
ElectricPotential::new::<volt>(center_point.into()),
|
||||
}
|
||||
@ -146,7 +123,7 @@ impl Channels {
|
||||
|
||||
/// i_set DAC
|
||||
fn set_dac(&mut self, channel: usize, voltage: ElectricPotential) -> ElectricPotential {
|
||||
let value = ((voltage / DAC_OUT_V_MAX).get::<ratio>() * (ad5680::MAX_VALUE as f64)) as u32 ;
|
||||
let value = ((voltage / ElectricPotential::new::<volt>(DAC_OUT_V_MAX)).get::<ratio>() * (ad5680::MAX_VALUE as f64)) as u32 ;
|
||||
match channel {
|
||||
0 => self.channel0.dac.set(value).unwrap(),
|
||||
1 => self.channel1.dac.set(value).unwrap(),
|
||||
@ -157,7 +134,11 @@ impl Channels {
|
||||
}
|
||||
|
||||
pub fn set_i(&mut self, channel: usize, i_set: ElectricCurrent) -> ElectricCurrent {
|
||||
let i_set = i_set.min(MAX_TEC_I).max(-MAX_TEC_I);
|
||||
// Silently clamp i_set
|
||||
let i_ceiling = ElectricCurrent::new::<ampere>(MAX_TEC_I);
|
||||
let i_floor = ElectricCurrent::new::<ampere>(-MAX_TEC_I);
|
||||
let i_set = i_set.min(i_ceiling).max(i_floor);
|
||||
|
||||
let vref_meas = match channel.into() {
|
||||
0 => self.channel0.vref_meas,
|
||||
1 => self.channel1.vref_meas,
|
||||
@ -172,105 +153,113 @@ impl Channels {
|
||||
i_set
|
||||
}
|
||||
|
||||
/// AN4073: ADC Reading Dispersion can be reduced through Averaging
|
||||
pub fn adc_read(&mut self, channel: usize, adc_read_target: PinsAdcReadTarget, avg_pt: u16) -> ElectricPotential {
|
||||
let mut sample: u32 = 0;
|
||||
pub fn read_dac_feedback(&mut self, channel: usize) -> ElectricPotential {
|
||||
match channel {
|
||||
0 => {
|
||||
sample = match adc_read_target {
|
||||
PinsAdcReadTarget::VREF => {
|
||||
match &self.channel0.vref_pin {
|
||||
Channel0VRef::Analog(vref_pin) => {
|
||||
for _ in (0..avg_pt).rev() {
|
||||
sample += self
|
||||
.pins_adc
|
||||
.convert(vref_pin, stm32f4xx_hal::adc::config::SampleTime::Cycles_480)
|
||||
as u32;
|
||||
}
|
||||
sample / avg_pt as u32
|
||||
},
|
||||
Channel0VRef::Disabled(_) => {2048 as u32}
|
||||
}
|
||||
}
|
||||
PinsAdcReadTarget::DacVfb => {
|
||||
for _ in (0..avg_pt).rev() {
|
||||
sample += self
|
||||
.pins_adc
|
||||
.convert(&self.channel0.dac_feedback_pin,stm32f4xx_hal::adc::config::SampleTime::Cycles_480)
|
||||
as u32;
|
||||
}
|
||||
sample / avg_pt as u32
|
||||
}
|
||||
PinsAdcReadTarget::ITec => {
|
||||
for _ in (0..avg_pt).rev() {
|
||||
sample += self
|
||||
.pins_adc
|
||||
.convert(&self.channel0.itec_pin, stm32f4xx_hal::adc::config::SampleTime::Cycles_480)
|
||||
as u32;
|
||||
}
|
||||
sample / avg_pt as u32
|
||||
}
|
||||
PinsAdcReadTarget::VTec => {
|
||||
for _ in (0..avg_pt).rev() {
|
||||
sample += self
|
||||
.pins_adc
|
||||
.convert(&self.channel0.tec_u_meas_pin, stm32f4xx_hal::adc::config::SampleTime::Cycles_480)
|
||||
as u32;
|
||||
}
|
||||
sample / avg_pt as u32
|
||||
}
|
||||
};
|
||||
let mv = self.pins_adc.sample_to_millivolts(sample as u16);
|
||||
let sample = self.pins_adc.convert(
|
||||
&self.channel0.dac_feedback_pin,
|
||||
stm32f4xx_hal::adc::config::SampleTime::Cycles_480
|
||||
);
|
||||
let mv = self.pins_adc.sample_to_millivolts(sample);
|
||||
ElectricPotential::new::<millivolt>(mv as f64)
|
||||
}
|
||||
1 => {
|
||||
sample = match adc_read_target {
|
||||
PinsAdcReadTarget::VREF => {
|
||||
match &self.channel1.vref_pin {
|
||||
Channel1VRef::Analog(vref_pin) => {
|
||||
for _ in (0..avg_pt).rev() {
|
||||
sample += self
|
||||
.pins_adc
|
||||
.convert(vref_pin, stm32f4xx_hal::adc::config::SampleTime::Cycles_480)
|
||||
as u32;
|
||||
}
|
||||
sample / avg_pt as u32
|
||||
},
|
||||
Channel1VRef::Disabled(_) => {2048 as u32}
|
||||
}
|
||||
}
|
||||
PinsAdcReadTarget::DacVfb => {
|
||||
for _ in (0..avg_pt).rev() {
|
||||
sample += self
|
||||
.pins_adc
|
||||
.convert(&self.channel1.dac_feedback_pin, stm32f4xx_hal::adc::config::SampleTime::Cycles_480)
|
||||
as u32;
|
||||
}
|
||||
sample / avg_pt as u32
|
||||
}
|
||||
PinsAdcReadTarget::ITec => {
|
||||
for _ in (0..avg_pt).rev() {
|
||||
sample += self
|
||||
.pins_adc
|
||||
.convert(&self.channel1.itec_pin, stm32f4xx_hal::adc::config::SampleTime::Cycles_480)
|
||||
as u32;
|
||||
}
|
||||
sample / avg_pt as u32
|
||||
}
|
||||
PinsAdcReadTarget::VTec => {
|
||||
for _ in (0..avg_pt).rev() {
|
||||
sample += self
|
||||
.pins_adc
|
||||
.convert(&self.channel1.tec_u_meas_pin, stm32f4xx_hal::adc::config::SampleTime::Cycles_480)
|
||||
as u32;
|
||||
}
|
||||
sample / avg_pt as u32
|
||||
}
|
||||
};
|
||||
let mv = self.pins_adc.sample_to_millivolts(sample as u16);
|
||||
let sample = self.pins_adc.convert(
|
||||
&self.channel1.dac_feedback_pin,
|
||||
stm32f4xx_hal::adc::config::SampleTime::Cycles_480
|
||||
);
|
||||
let mv = self.pins_adc.sample_to_millivolts(sample);
|
||||
ElectricPotential::new::<millivolt>(mv as f64)
|
||||
}
|
||||
_ => unreachable!()
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_dac_feedback_until_stable(&mut self, channel: usize, tolerance: ElectricPotential) -> ElectricPotential {
|
||||
let mut prev = self.read_dac_feedback(channel);
|
||||
loop {
|
||||
let current = self.read_dac_feedback(channel);
|
||||
if (current - prev).abs() < tolerance {
|
||||
return current;
|
||||
}
|
||||
prev = current;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_itec(&mut self, channel: usize) -> ElectricPotential {
|
||||
match channel {
|
||||
0 => {
|
||||
let sample = self.pins_adc.convert(
|
||||
&self.channel0.itec_pin,
|
||||
stm32f4xx_hal::adc::config::SampleTime::Cycles_480
|
||||
);
|
||||
let mv = self.pins_adc.sample_to_millivolts(sample);
|
||||
ElectricPotential::new::<millivolt>(mv as f64)
|
||||
}
|
||||
1 => {
|
||||
let sample = self.pins_adc.convert(
|
||||
&self.channel1.itec_pin,
|
||||
stm32f4xx_hal::adc::config::SampleTime::Cycles_480
|
||||
);
|
||||
let mv = self.pins_adc.sample_to_millivolts(sample);
|
||||
ElectricPotential::new::<millivolt>(mv as f64)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
/// should be 1.5V
|
||||
pub fn read_vref(&mut self, channel: usize) -> ElectricPotential {
|
||||
match channel {
|
||||
0 => {
|
||||
match &self.channel0.vref_pin {
|
||||
Channel0VRef::Analog(vref_pin) => {
|
||||
let sample = self.pins_adc.convert(
|
||||
vref_pin,
|
||||
stm32f4xx_hal::adc::config::SampleTime::Cycles_480
|
||||
);
|
||||
let mv = self.pins_adc.sample_to_millivolts(sample);
|
||||
ElectricPotential::new::<millivolt>(mv as f64)
|
||||
},
|
||||
Channel0VRef::Disabled(_) => ElectricPotential::new::<volt>(1.5)
|
||||
}
|
||||
}
|
||||
1 => {
|
||||
match &self.channel1.vref_pin {
|
||||
Channel1VRef::Analog(vref_pin) => {
|
||||
let sample = self.pins_adc.convert(
|
||||
vref_pin,
|
||||
stm32f4xx_hal::adc::config::SampleTime::Cycles_480
|
||||
);
|
||||
let mv = self.pins_adc.sample_to_millivolts(sample);
|
||||
ElectricPotential::new::<millivolt>(mv as f64)
|
||||
},
|
||||
Channel1VRef::Disabled(_) => ElectricPotential::new::<volt>(1.5)
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_tec_u_meas(&mut self, channel: usize) -> ElectricPotential {
|
||||
match channel {
|
||||
0 => {
|
||||
let sample = self.pins_adc.convert(
|
||||
&self.channel0.tec_u_meas_pin,
|
||||
stm32f4xx_hal::adc::config::SampleTime::Cycles_480
|
||||
);
|
||||
let mv = self.pins_adc.sample_to_millivolts(sample);
|
||||
ElectricPotential::new::<millivolt>(mv as f64)
|
||||
}
|
||||
1 => {
|
||||
let sample = self.pins_adc.convert(
|
||||
&self.channel1.tec_u_meas_pin,
|
||||
stm32f4xx_hal::adc::config::SampleTime::Cycles_480
|
||||
);
|
||||
let mv = self.pins_adc.sample_to_millivolts(sample);
|
||||
ElectricPotential::new::<millivolt>(mv as f64)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -300,7 +289,7 @@ impl Channels {
|
||||
let mut start_value = 1;
|
||||
let mut best_error = ElectricPotential::new::<volt>(100.0);
|
||||
|
||||
for step in (5..18).rev() {
|
||||
for step in (0..18).rev() {
|
||||
let mut prev_value = start_value;
|
||||
for value in (start_value..=ad5680::MAX_VALUE).step_by(1 << step) {
|
||||
match channel {
|
||||
@ -312,9 +301,8 @@ impl Channels {
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
sleep(10);
|
||||
|
||||
let dac_feedback = self.adc_read(channel, PinsAdcReadTarget::DacVfb, 64);
|
||||
let dac_feedback = self.read_dac_feedback_until_stable(channel, ElectricPotential::new::<volt>(0.001));
|
||||
let error = target_voltage - dac_feedback;
|
||||
if error < ElectricPotential::new::<volt>(0.0) {
|
||||
break;
|
||||
@ -322,7 +310,7 @@ impl Channels {
|
||||
best_error = error;
|
||||
start_value = prev_value;
|
||||
|
||||
let vref = (value as f64 / ad5680::MAX_VALUE as f64) * DAC_OUT_V_MAX;
|
||||
let vref = (value as f64 / ad5680::MAX_VALUE as f64) * ElectricPotential::new::<volt>(DAC_OUT_V_MAX);
|
||||
match channel {
|
||||
0 => self.channel0.vref_meas = vref,
|
||||
1 => self.channel1.vref_meas = vref,
|
||||
@ -382,30 +370,32 @@ impl Channels {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_max_v(&mut self, channel: usize) -> (ElectricPotential, ElectricPotential) {
|
||||
pub fn get_max_v(&mut self, channel: usize) -> ElectricPotential {
|
||||
let max = 4.0 * ElectricPotential::new::<volt>(3.3);
|
||||
let duty = self.get_pwm(channel, PwmPin::MaxV);
|
||||
(duty * max, MAX_TEC_V)
|
||||
duty * max
|
||||
}
|
||||
|
||||
pub fn get_max_i_pos(&mut self, channel: usize) -> (ElectricCurrent, ElectricCurrent) {
|
||||
let max = ElectricCurrent::new::<ampere>(3.0);
|
||||
let duty = self.get_pwm(channel, PwmPin::MaxIPos);
|
||||
(duty * MAX_TEC_I_DUTY_TO_CURRENT_RATE, MAX_TEC_I)
|
||||
(duty * max, max)
|
||||
}
|
||||
|
||||
pub fn get_max_i_neg(&mut self, channel: usize) -> (ElectricCurrent, ElectricCurrent) {
|
||||
let max = ElectricCurrent::new::<ampere>(3.0);
|
||||
let duty = self.get_pwm(channel, PwmPin::MaxINeg);
|
||||
(duty * MAX_TEC_I_DUTY_TO_CURRENT_RATE, MAX_TEC_I)
|
||||
(duty * max, max)
|
||||
}
|
||||
|
||||
// Get current passing through TEC
|
||||
pub fn get_tec_i(&mut self, channel: usize) -> ElectricCurrent {
|
||||
(self.adc_read(channel, PinsAdcReadTarget::ITec, 16) - self.adc_read(channel, PinsAdcReadTarget::VREF, 16)) / ElectricalResistance::new::<ohm>(0.4)
|
||||
(self.read_itec(channel) - self.read_vref(channel)) / ElectricalResistance::new::<ohm>(0.4)
|
||||
}
|
||||
|
||||
// Get voltage across TEC
|
||||
pub fn get_tec_v(&mut self, channel: usize) -> ElectricPotential {
|
||||
(self.adc_read(channel, PinsAdcReadTarget::VTec, 16) - ElectricPotential::new::<volt>(1.5)) * 4.0
|
||||
(self.read_tec_u_meas(channel) - ElectricPotential::new::<volt>(1.5)) * 4.0
|
||||
}
|
||||
|
||||
fn set_pwm(&mut self, channel: usize, pin: PwmPin, duty: f64) -> f64 {
|
||||
@ -437,29 +427,29 @@ impl Channels {
|
||||
|
||||
pub fn set_max_v(&mut self, channel: usize, max_v: ElectricPotential) -> (ElectricPotential, ElectricPotential) {
|
||||
let max = 4.0 * ElectricPotential::new::<volt>(3.3);
|
||||
let duty = (max_v.min(MAX_TEC_V).max(ElectricPotential::zero()) / max).get::<ratio>();
|
||||
let duty = (max_v / max).get::<ratio>();
|
||||
let duty = self.set_pwm(channel, PwmPin::MaxV, duty);
|
||||
(duty * max, max)
|
||||
}
|
||||
|
||||
pub fn set_max_i_pos(&mut self, channel: usize, max_i_pos: ElectricCurrent) -> (ElectricCurrent, ElectricCurrent) {
|
||||
let max = ElectricCurrent::new::<ampere>(3.0);
|
||||
let duty = (max_i_pos.max(ElectricCurrent::zero()).min(max) / MAX_TEC_I_DUTY_TO_CURRENT_RATE).get::<ratio>();
|
||||
let duty = (max_i_pos / max).get::<ratio>();
|
||||
let duty = self.set_pwm(channel, PwmPin::MaxIPos, duty);
|
||||
(duty * MAX_TEC_I_DUTY_TO_CURRENT_RATE, max)
|
||||
(duty * max, max)
|
||||
}
|
||||
|
||||
pub fn set_max_i_neg(&mut self, channel: usize, max_i_neg: ElectricCurrent) -> (ElectricCurrent, ElectricCurrent) {
|
||||
let max = ElectricCurrent::new::<ampere>(3.0);
|
||||
let duty = (max_i_neg.max(ElectricCurrent::zero()).min(max) / MAX_TEC_I_DUTY_TO_CURRENT_RATE).get::<ratio>();
|
||||
let duty = (max_i_neg / max).get::<ratio>();
|
||||
let duty = self.set_pwm(channel, PwmPin::MaxINeg, duty);
|
||||
(duty * MAX_TEC_I_DUTY_TO_CURRENT_RATE, max)
|
||||
(duty * max, max)
|
||||
}
|
||||
|
||||
fn report(&mut self, channel: usize) -> Report {
|
||||
let i_set = self.get_i(channel);
|
||||
let i_tec = self.adc_read(channel, PinsAdcReadTarget::ITec, 16);
|
||||
let tec_i = self.get_tec_i(channel);
|
||||
let i_tec = if self.hwrev.major > 2 {Some(self.read_itec(channel))} else {None};
|
||||
let tec_i = if self.hwrev.major > 2 {Some(self.get_tec_i(channel))} else {None};
|
||||
let dac_value = self.get_dac(channel);
|
||||
let state = self.channel_state(channel);
|
||||
let pid_output = ElectricCurrent::new::<ampere>(state.pid.y1);
|
||||
@ -474,7 +464,7 @@ impl Channels {
|
||||
pid_engaged: state.pid_engaged,
|
||||
i_set,
|
||||
dac_value,
|
||||
dac_feedback: self.adc_read(channel, PinsAdcReadTarget::DacVfb, 1),
|
||||
dac_feedback: self.read_dac_feedback(channel),
|
||||
i_tec,
|
||||
tec_i,
|
||||
tec_u_meas: self.get_tec_v(channel),
|
||||
@ -511,8 +501,8 @@ impl Channels {
|
||||
PwmSummary {
|
||||
channel,
|
||||
center: CenterPointJson(self.channel_state(channel).center.clone()),
|
||||
i_set: (self.get_i(channel), MAX_TEC_I).into(),
|
||||
max_v: self.get_max_v(channel).into(),
|
||||
i_set: (self.get_i(channel), ElectricCurrent::new::<ampere>(3.0)).into(),
|
||||
max_v: (self.get_max_v(channel), ElectricPotential::new::<volt>(5.0)).into(),
|
||||
max_i_pos: self.get_max_i_pos(channel).into(),
|
||||
max_i_neg: self.get_max_i_neg(channel).into(),
|
||||
}
|
||||
@ -554,10 +544,9 @@ impl Channels {
|
||||
}
|
||||
|
||||
pub fn current_abs_max_tec_i(&mut self) -> ElectricCurrent {
|
||||
(0..CHANNELS)
|
||||
.map(|channel| self.get_tec_i(channel).abs())
|
||||
.max_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal))
|
||||
.unwrap()
|
||||
max_by(self.get_tec_i(0).abs(),
|
||||
self.get_tec_i(1).abs(),
|
||||
|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal))
|
||||
}
|
||||
}
|
||||
|
||||
@ -573,8 +562,8 @@ pub struct Report {
|
||||
i_set: ElectricCurrent,
|
||||
dac_value: ElectricPotential,
|
||||
dac_feedback: ElectricPotential,
|
||||
i_tec: ElectricPotential,
|
||||
tec_i: ElectricCurrent,
|
||||
i_tec: Option<ElectricPotential>,
|
||||
tec_i: Option<ElectricCurrent>,
|
||||
tec_u_meas: ElectricPotential,
|
||||
pid_output: ElectricCurrent,
|
||||
}
|
||||
|
@ -345,7 +345,7 @@ impl Handler {
|
||||
|
||||
fn set_fan(socket: &mut TcpSocket, fan_pwm: u32, fan_ctrl: &mut FanCtrl) -> Result<Handler, Error> {
|
||||
if !fan_ctrl.fan_available() {
|
||||
send_line(socket, b"{ \"warning\": \"this thermostat doesn't have a fan!\" }");
|
||||
send_line(socket, b"{ \"warning\": \"this thermostat doesn't have fan!\" }");
|
||||
return Ok(Handler::Handled);
|
||||
}
|
||||
fan_ctrl.set_auto_mode(false);
|
||||
@ -374,7 +374,7 @@ impl Handler {
|
||||
|
||||
fn fan_auto(socket: &mut TcpSocket, fan_ctrl: &mut FanCtrl) -> Result<Handler, Error> {
|
||||
if !fan_ctrl.fan_available() {
|
||||
send_line(socket, b"{ \"warning\": \"this thermostat doesn't have a fan!\" }");
|
||||
send_line(socket, b"{ \"warning\": \"this thermostat doesn't have fan!\" }");
|
||||
return Ok(Handler::Handled);
|
||||
}
|
||||
fan_ctrl.set_auto_mode(true);
|
||||
|
@ -1,4 +1,3 @@
|
||||
use num_traits::Zero;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use uom::si::{
|
||||
electric_potential::volt,
|
||||
@ -19,7 +18,6 @@ pub struct ChannelConfig {
|
||||
pid: pid::Parameters,
|
||||
pid_target: f32,
|
||||
pid_engaged: bool,
|
||||
i_set: ElectricCurrent,
|
||||
sh: steinhart_hart::Parameters,
|
||||
pwm: PwmLimits,
|
||||
/// uses variant `PostFilter::Invalid` instead of `None` to save space
|
||||
@ -35,17 +33,11 @@ impl ChannelConfig {
|
||||
.unwrap_or(PostFilter::Invalid);
|
||||
|
||||
let state = channels.channel_state(channel);
|
||||
let i_set = if state.pid_engaged {
|
||||
ElectricCurrent::zero()
|
||||
} else {
|
||||
state.i_set
|
||||
};
|
||||
ChannelConfig {
|
||||
center: state.center.clone(),
|
||||
pid: state.pid.parameters.clone(),
|
||||
pid_target: state.pid.target as f32,
|
||||
pid_engaged: state.pid_engaged,
|
||||
i_set: i_set,
|
||||
sh: state.sh.clone(),
|
||||
pwm,
|
||||
adc_postfilter,
|
||||
@ -67,7 +59,6 @@ impl ChannelConfig {
|
||||
adc_postfilter => Some(adc_postfilter),
|
||||
};
|
||||
let _ = channels.adc.set_postfilter(channel as u8, adc_postfilter);
|
||||
let _ = channels.set_i(channel, self.i_set);
|
||||
}
|
||||
}
|
||||
|
||||
@ -80,7 +71,7 @@ struct PwmLimits {
|
||||
|
||||
impl PwmLimits {
|
||||
pub fn new(channels: &mut Channels, channel: usize) -> Self {
|
||||
let (max_v, _) = channels.get_max_v(channel);
|
||||
let max_v = channels.get_max_v(channel);
|
||||
let (max_i_pos, _) = channels.get_max_i_pos(channel);
|
||||
let (max_i_neg, _) = channels.get_max_i_neg(channel);
|
||||
PwmLimits {
|
||||
|
@ -54,7 +54,7 @@ impl FanCtrl {
|
||||
pub fn cycle(&mut self, abs_max_tec_i: ElectricCurrent) {
|
||||
self.abs_max_tec_i = abs_max_tec_i.get::<ampere>() as f32;
|
||||
if self.fan_auto && self.hw_settings.fan_available {
|
||||
let scaled_current = self.abs_max_tec_i / MAX_TEC_I.get::<ampere>() as f32;
|
||||
let scaled_current = self.abs_max_tec_i / MAX_TEC_I as f32;
|
||||
// do not limit upper bound, as it will be limited in the set_pwm()
|
||||
let pwm = (MAX_USER_FAN_PWM * (scaled_current * (scaled_current * self.k_a + self.k_b) + self.k_c)) as u32;
|
||||
self.set_pwm(pwm);
|
||||
|
@ -58,7 +58,7 @@ mod hw_rev;
|
||||
|
||||
const HSE: MegaHertz = MegaHertz(8);
|
||||
#[cfg(not(feature = "semihosting"))]
|
||||
const WATCHDOG_INTERVAL: u32 = 2_000;
|
||||
const WATCHDOG_INTERVAL: u32 = 1_000;
|
||||
#[cfg(feature = "semihosting")]
|
||||
const WATCHDOG_INTERVAL: u32 = 30_000;
|
||||
|
||||
@ -138,7 +138,7 @@ fn main() -> ! {
|
||||
|
||||
let mut store = flash_store::store(dp.FLASH);
|
||||
|
||||
let mut channels = Channels::new(pins);
|
||||
let mut channels = Channels::new(pins, &hwrev);
|
||||
for c in 0..CHANNELS {
|
||||
match store.read_value::<ChannelConfig>(CHANNEL_CONFIG_KEY[c]) {
|
||||
Ok(Some(config)) =>
|
||||
|
@ -54,13 +54,15 @@ impl Controller {
|
||||
// + x0 * (kp + ki + kd)
|
||||
// - x1 * (kp + 2kd)
|
||||
// + x2 * kd
|
||||
// + kp * (u0 - u1)
|
||||
// y0 = clip(y0', ymin, ymax)
|
||||
pub fn update(&mut self, input: f64) -> f64 {
|
||||
|
||||
let mut output: f64 = self.y1 - self.target * f64::from(self.parameters.ki)
|
||||
+ input * f64::from(self.parameters.kp + self.parameters.ki + self.parameters.kd)
|
||||
- self.x1 * f64::from(self.parameters.kp + 2.0 * self.parameters.kd)
|
||||
+ self.x2 * f64::from(self.parameters.kd);
|
||||
+ self.x2 * f64::from(self.parameters.kd)
|
||||
+ f64::from(self.parameters.kp) * (self.target - self.u1);
|
||||
if output < self.parameters.output_min.into() {
|
||||
output = self.parameters.output_min.into();
|
||||
}
|
||||
|
@ -61,7 +61,7 @@ pub trait ChannelPins {
|
||||
type DacSync: OutputPin;
|
||||
type Shdn: OutputPin;
|
||||
type VRefPin;
|
||||
type ITecPin;
|
||||
type ItecPin;
|
||||
type DacFeedbackPin;
|
||||
type TecUMeasPin;
|
||||
}
|
||||
@ -76,7 +76,7 @@ impl ChannelPins for Channel0 {
|
||||
type DacSync = PE4<Output<PushPull>>;
|
||||
type Shdn = PE10<Output<PushPull>>;
|
||||
type VRefPin = Channel0VRef;
|
||||
type ITecPin = PA6<Analog>;
|
||||
type ItecPin = PA6<Analog>;
|
||||
type DacFeedbackPin = PA4<Analog>;
|
||||
type TecUMeasPin = PC2<Analog>;
|
||||
}
|
||||
@ -91,7 +91,7 @@ impl ChannelPins for Channel1 {
|
||||
type DacSync = PF6<Output<PushPull>>;
|
||||
type Shdn = PE15<Output<PushPull>>;
|
||||
type VRefPin = Channel1VRef;
|
||||
type ITecPin = PB0<Analog>;
|
||||
type ItecPin = PB0<Analog>;
|
||||
type DacFeedbackPin = PA5<Analog>;
|
||||
type TecUMeasPin = PC3<Analog>;
|
||||
}
|
||||
@ -108,7 +108,7 @@ pub struct ChannelPinSet<C: ChannelPins> {
|
||||
pub dac_sync: C::DacSync,
|
||||
pub shdn: C::Shdn,
|
||||
pub vref_pin: C::VRefPin,
|
||||
pub itec_pin: C::ITecPin,
|
||||
pub itec_pin: C::ItecPin,
|
||||
pub dac_feedback_pin: C::DacFeedbackPin,
|
||||
pub tec_u_meas_pin: C::TecUMeasPin,
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user