Compare commits

...

19 Commits

Author SHA1 Message Date
d2a57245b2 Fix wrong current limit duty cycle calculation
- prev commit assumed setting 3.3V -> 3A current limit which is wrong
- Please refer to the MAX1968 datasheet for the duty cycle
calculation equation
2024-09-19 18:28:18 +08:00
ae4bea0c8a gitignore: Ignore .bin files and __pycache__ 2024-09-19 10:08:58 +08:00
1f2de942e4 flake: Add rlwrap to devShell 2024-09-19 10:06:45 +08:00
1041d3ecbb Improve the VREF calibration routine
* Fix wrong calibration of VREF on startup. Caused new v2.2.2 boards to
wrongly calibrate the zero-point to ~2.2 V instead of 1.5 V.

* Fix bootloop on some boards.

* Adjust watchdog interval accordingly.
2024-09-16 18:14:47 +08:00
c6040899dd ItecPin -> ITecPin 2024-08-12 13:02:22 +08:00
9d89104f50 README: Fix command to make firmware BIN (#115)
Co-authored-by: atse <atse@m-labs.hk>
Co-committed-by: atse <atse@m-labs.hk>
2024-08-07 18:22:48 +08:00
136c7a0b52 Calculate current_abs_max_tec_i from all channels
Instead of picking channels 0 and 1. Helps to generalise for more than 2
channels.
2024-08-07 16:14:28 +08:00
5000cae1b1 Grammar fixes 2024-08-07 16:09:30 +08:00
78ec77509f flake: Install LLVM in devShell too
So that developers can use `llvm-objcopy` in their devShells as well.
2024-08-07 16:06:19 +08:00
52aa3890c1 Update nix repos 2024-08-06 13:38:58 +08:00
1ae6a6fdd4 flake: More concise devShell
No need for C compiler in development shell + use "packages" to
explicitly refer to devShell packages
2024-08-06 11:14:25 +08:00
7333d2cea5 flake: Don't use deprecated flake output schemas 2024-08-06 11:09:51 +08:00
44e9130010 Use oxalica's rust-overlay
Follow ARTIQ, and in this project lets us include the version number
directly in flake.nix instead of linking to the toml file of a specific
release date, as we use stable Rust.

Also, from nixpkgs manual:
    both oxalica's overlay and fenix better integrate with nix and cache
    optimizations. Because of this and ergonomics, either of those
    community projects should be preferred to the Mozilla's Rust overlay
    (nixpkgs-mozilla).
2024-06-27 12:42:00 +08:00
5b0c6f7018 Save i_set into ChannelConfig 2024-05-18 10:50:54 +08:00
1007982b48 clamp TEC settings to a valid & design specs range
- Not respecting the design specs can cause hardware to get stuck in unrecoverable state
2024-05-10 15:17:46 +08:00
925601f4f5 rm pid setpoint change kick 2024-05-10 10:29:08 +08:00
8c1cb3117c README: Add notes on i_tec & tec_ireadouts 2024-05-02 17:48:47 +08:00
1fcfe41a63 Add averaging filter on the pin_adc readings
- Adapted from Kirdy Firmware
- Can reduce the i_tec readings noise dispersion
2024-05-02 16:49:55 +08:00
9fce19a418 Revert "Disable feedback current readout on flawed HW Revs"
This reverts commit ae3d8b51d4.
2024-05-02 14:38:40 +08:00
12 changed files with 228 additions and 208 deletions

3
.gitignore vendored
View File

@ -1,2 +1,5 @@
target/
result
*.bin
__pycache__/

View File

@ -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: `arm-none-eabi-objcopy -O binary thermostat thermostat.bin` (you can skip this step if using the BIN from Hydra)
* 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)
* 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 disabled and null due to faulty hardware that introduces a lot of noise in the signal.
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].
## PID Tuning

48
flake.lock generated
View File

@ -1,41 +1,45 @@
{
"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": 1691421349,
"narHash": "sha256-RRJyX0CUrs4uW4gMhd/X4rcDG8PTgaaCQM5rXEJOx6g=",
"lastModified": 1722791413,
"narHash": "sha256-rCTrlCWvHzMCNcKxPE3Z/mMK2gDZ+BvvpEVyRM4tKmU=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "011567f35433879aae5024fc6ec53f2a0568a6c4",
"rev": "8b5b6723aca5a51edf075936439d9cd3947b7b2c",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-23.05",
"ref": "nixos-24.05",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"mozilla-overlay": "mozilla-overlay",
"nixpkgs": "nixpkgs"
"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"
}
}
},

View File

@ -1,32 +1,25 @@
{
description = "Firmware for the Sinara 8451 Thermostat";
inputs.nixpkgs.url = github:NixOS/nixpkgs/nixos-23.05;
inputs.mozilla-overlay = { url = github:mozilla/nixpkgs-mozilla; flake = false; };
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
inputs.rust-overlay = {
url = "github:oxalica/rust-overlay";
inputs.nixpkgs.follows = "nixpkgs";
};
outputs = { self, nixpkgs, mozilla-overlay }:
outputs = { self, nixpkgs, rust-overlay }:
let
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=";
};
pkgs = import nixpkgs { system = "x86_64-linux"; overlays = [ (import rust-overlay) ]; };
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 {
rust = pkgs.rust-bin.stable."1.66.0".default.override {
extensions = [ "rust-src" ];
targets = [ "thumbv7em-none-eabihf" ];
};
rustPlatform = pkgs.makeRustPlatform {
rustc = rust;
cargo = rust;
});
};
thermostat = rustPlatform.buildRustPackage {
name = "thermostat";
version = "0.0.0";
@ -54,24 +47,26 @@
'';
dontFixup = true;
auditable = false;
};
in {
packages.x86_64-linux = {
inherit thermostat;
default = thermostat;
};
hydraJobs = {
inherit thermostat;
};
devShell.x86_64-linux = pkgs.mkShell {
devShells.x86_64-linux.default = pkgs.mkShellNoCC {
name = "thermostat-dev-shell";
buildInputs = with pkgs; [
rust openocd dfu-util
packages = with pkgs; [
rust llvm
openocd dfu-util rlwrap
] ++ (with python3Packages; [
numpy matplotlib
]);
};
defaultPackage.x86_64-linux = thermostat;
};
}

View File

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

View File

@ -1,5 +1,6 @@
use core::cmp::max_by;
use core::marker::PhantomData;
use heapless::{consts::U2, Vec};
use num_traits::Zero;
use serde::{Serialize, Serializer};
use smoltcp::time::Instant;
use stm32f4xx_hal::hal;
@ -20,31 +21,53 @@ 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;
// as stated in the MAX1968 datasheet
pub const MAX_TEC_I: f64 = 3.0;
// 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),
};
// DAC chip outputs 0-5v, which is then passed through a resistor dividor to provide 0-3v range
const DAC_OUT_V_MAX: f64 = 3.0;
const DAC_OUT_V_MAX: ElectricPotential = ElectricPotential {
dimension: PhantomData,
units: PhantomData,
value: 3.0,
};
// TODO: -pub
pub struct Channels<'a> {
pub struct Channels {
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<'a> Channels<'a> {
pub fn new(pins: pins::Pins, hwrev: &'a hw_rev::HWRev) -> Self {
impl Channels {
pub fn new(pins: pins::Pins) -> Self {
let mut adc = ad7172::Adc::new(pins.adc_spi, pins.adc_nss).unwrap();
// Feature not used
adc.set_sync_enable(false).unwrap();
@ -62,7 +85,7 @@ impl<'a> Channels<'a> {
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, hwrev };
let mut channels = Channels { channel0, channel1, adc, pins_adc, pwm };
for channel in 0..CHANNELS {
channels.calibrate_dac_value(channel);
channels.set_i(channel, ElectricCurrent::new::<ampere>(0.0));
@ -104,7 +127,7 @@ impl<'a> Channels<'a> {
pub fn get_center(&mut self, channel: usize) -> ElectricPotential {
match self.channel_state(channel).center {
CenterPoint::Vref =>
self.read_vref(channel),
self.adc_read(channel, PinsAdcReadTarget::VREF, 8),
CenterPoint::Override(center_point) =>
ElectricPotential::new::<volt>(center_point.into()),
}
@ -123,7 +146,7 @@ impl<'a> Channels<'a> {
/// i_set DAC
fn set_dac(&mut self, channel: usize, voltage: ElectricPotential) -> ElectricPotential {
let value = ((voltage / ElectricPotential::new::<volt>(DAC_OUT_V_MAX)).get::<ratio>() * (ad5680::MAX_VALUE as f64)) as u32 ;
let value = ((voltage / 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(),
@ -134,11 +157,7 @@ impl<'a> Channels<'a> {
}
pub fn set_i(&mut self, channel: usize, i_set: ElectricCurrent) -> ElectricCurrent {
// 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 i_set = i_set.min(MAX_TEC_I).max(-MAX_TEC_I);
let vref_meas = match channel.into() {
0 => self.channel0.vref_meas,
1 => self.channel1.vref_meas,
@ -153,113 +172,105 @@ impl<'a> Channels<'a> {
i_set
}
pub fn read_dac_feedback(&mut self, channel: usize) -> ElectricPotential {
/// 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;
match channel {
0 => {
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);
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);
ElectricPotential::new::<millivolt>(mv as f64)
}
1 => {
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);
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);
ElectricPotential::new::<millivolt>(mv as f64)
}
_ => 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!(),
_ => unreachable!()
}
}
@ -289,7 +300,7 @@ impl<'a> Channels<'a> {
let mut start_value = 1;
let mut best_error = ElectricPotential::new::<volt>(100.0);
for step in (0..18).rev() {
for step in (5..18).rev() {
let mut prev_value = start_value;
for value in (start_value..=ad5680::MAX_VALUE).step_by(1 << step) {
match channel {
@ -301,8 +312,9 @@ impl<'a> Channels<'a> {
}
_ => unreachable!(),
}
sleep(10);
let dac_feedback = self.read_dac_feedback_until_stable(channel, ElectricPotential::new::<volt>(0.001));
let dac_feedback = self.adc_read(channel, PinsAdcReadTarget::DacVfb, 64);
let error = target_voltage - dac_feedback;
if error < ElectricPotential::new::<volt>(0.0) {
break;
@ -310,7 +322,7 @@ impl<'a> Channels<'a> {
best_error = error;
start_value = prev_value;
let vref = (value as f64 / ad5680::MAX_VALUE as f64) * ElectricPotential::new::<volt>(DAC_OUT_V_MAX);
let vref = (value as f64 / ad5680::MAX_VALUE as f64) * DAC_OUT_V_MAX;
match channel {
0 => self.channel0.vref_meas = vref,
1 => self.channel1.vref_meas = vref,
@ -370,32 +382,30 @@ impl<'a> Channels<'a> {
}
}
pub fn get_max_v(&mut self, channel: usize) -> ElectricPotential {
pub fn get_max_v(&mut self, channel: usize) -> (ElectricPotential, ElectricPotential) {
let max = 4.0 * ElectricPotential::new::<volt>(3.3);
let duty = self.get_pwm(channel, PwmPin::MaxV);
duty * max
(duty * max, MAX_TEC_V)
}
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, max)
(duty * MAX_TEC_I_DUTY_TO_CURRENT_RATE, MAX_TEC_I)
}
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, max)
(duty * MAX_TEC_I_DUTY_TO_CURRENT_RATE, MAX_TEC_I)
}
// Get current passing through TEC
pub fn get_tec_i(&mut self, channel: usize) -> ElectricCurrent {
(self.read_itec(channel) - self.read_vref(channel)) / ElectricalResistance::new::<ohm>(0.4)
(self.adc_read(channel, PinsAdcReadTarget::ITec, 16) - self.adc_read(channel, PinsAdcReadTarget::VREF, 16)) / ElectricalResistance::new::<ohm>(0.4)
}
// Get voltage across TEC
pub fn get_tec_v(&mut self, channel: usize) -> ElectricPotential {
(self.read_tec_u_meas(channel) - ElectricPotential::new::<volt>(1.5)) * 4.0
(self.adc_read(channel, PinsAdcReadTarget::VTec, 16) - ElectricPotential::new::<volt>(1.5)) * 4.0
}
fn set_pwm(&mut self, channel: usize, pin: PwmPin, duty: f64) -> f64 {
@ -427,29 +437,29 @@ impl<'a> Channels<'a> {
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 / max).get::<ratio>();
let duty = (max_v.min(MAX_TEC_V).max(ElectricPotential::zero()) / 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).get::<ratio>();
let duty = (max_i_pos.max(ElectricCurrent::zero()).min(max) / MAX_TEC_I_DUTY_TO_CURRENT_RATE).get::<ratio>();
let duty = self.set_pwm(channel, PwmPin::MaxIPos, duty);
(duty * max, max)
(duty * MAX_TEC_I_DUTY_TO_CURRENT_RATE, 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).get::<ratio>();
let duty = (max_i_neg.max(ElectricCurrent::zero()).min(max) / MAX_TEC_I_DUTY_TO_CURRENT_RATE).get::<ratio>();
let duty = self.set_pwm(channel, PwmPin::MaxINeg, duty);
(duty * max, max)
(duty * MAX_TEC_I_DUTY_TO_CURRENT_RATE, max)
}
fn report(&mut self, channel: usize) -> Report {
let i_set = self.get_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 i_tec = self.adc_read(channel, PinsAdcReadTarget::ITec, 16);
let tec_i = self.get_tec_i(channel);
let dac_value = self.get_dac(channel);
let state = self.channel_state(channel);
let pid_output = ElectricCurrent::new::<ampere>(state.pid.y1);
@ -464,7 +474,7 @@ impl<'a> Channels<'a> {
pid_engaged: state.pid_engaged,
i_set,
dac_value,
dac_feedback: self.read_dac_feedback(channel),
dac_feedback: self.adc_read(channel, PinsAdcReadTarget::DacVfb, 1),
i_tec,
tec_i,
tec_u_meas: self.get_tec_v(channel),
@ -501,8 +511,8 @@ impl<'a> Channels<'a> {
PwmSummary {
channel,
center: CenterPointJson(self.channel_state(channel).center.clone()),
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(),
i_set: (self.get_i(channel), MAX_TEC_I).into(),
max_v: self.get_max_v(channel).into(),
max_i_pos: self.get_max_i_pos(channel).into(),
max_i_neg: self.get_max_i_neg(channel).into(),
}
@ -544,9 +554,10 @@ impl<'a> Channels<'a> {
}
pub fn current_abs_max_tec_i(&mut self) -> ElectricCurrent {
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))
(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()
}
}
@ -562,8 +573,8 @@ pub struct Report {
i_set: ElectricCurrent,
dac_value: ElectricPotential,
dac_feedback: ElectricPotential,
i_tec: Option<ElectricPotential>,
tec_i: Option<ElectricCurrent>,
i_tec: ElectricPotential,
tec_i: ElectricCurrent,
tec_u_meas: ElectricPotential,
pid_output: ElectricCurrent,
}

View File

@ -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 fan!\" }");
send_line(socket, b"{ \"warning\": \"this thermostat doesn't have a 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 fan!\" }");
send_line(socket, b"{ \"warning\": \"this thermostat doesn't have a fan!\" }");
return Ok(Handler::Handled);
}
fan_ctrl.set_auto_mode(true);

View File

@ -1,3 +1,4 @@
use num_traits::Zero;
use serde::{Serialize, Deserialize};
use uom::si::{
electric_potential::volt,
@ -18,6 +19,7 @@ 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
@ -33,11 +35,17 @@ 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,
@ -59,6 +67,7 @@ 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);
}
}
@ -71,7 +80,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 {

View File

@ -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 as f32;
let scaled_current = self.abs_max_tec_i / MAX_TEC_I.get::<ampere>() 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);

View File

@ -58,7 +58,7 @@ mod hw_rev;
const HSE: MegaHertz = MegaHertz(8);
#[cfg(not(feature = "semihosting"))]
const WATCHDOG_INTERVAL: u32 = 1_000;
const WATCHDOG_INTERVAL: u32 = 2_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, &hwrev);
let mut channels = Channels::new(pins);
for c in 0..CHANNELS {
match store.read_value::<ChannelConfig>(CHANNEL_CONFIG_KEY[c]) {
Ok(Some(config)) =>

View File

@ -54,15 +54,13 @@ 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)
+ f64::from(self.parameters.kp) * (self.target - self.u1);
+ self.x2 * f64::from(self.parameters.kd);
if output < self.parameters.output_min.into() {
output = self.parameters.output_min.into();
}

View File

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