Compare commits

..

23 Commits

Author SHA1 Message Date
7e6013b07b MAX_TEC_I -> MAX_I 2024-08-06 15:51:29 +08:00
cee633845e channels: Start to make reports make sense
Names in the report make no sense, and expose implementation details.
Start simplifying the interface.
2024-08-06 15:51:29 +08:00
ef362643a7 get_i -> get_i_set 2024-08-06 15:51:29 +08:00
9de872f469 abs_max_tec_i -> max_abs_i_measured 2024-08-06 15:51:29 +08:00
370c7d8ad4 Grammar 2024-08-06 15:50:12 +08:00
ce86dc27fd clippy 2024-08-06 15:50:12 +08:00
a947e25e8f cargo fmt 2024-08-06 15:50:11 +08:00
22cb5168ec channels: remove redundant pub 2024-08-06 15:50:11 +08:00
10b6406ad6 README: Add full path to compiled binary 2024-08-06 15:47:53 +08:00
b037e8e4ca flake: Install LLVM in devShell too
Lets the developer use `cargo build` instead of `nix build` to obtain
the final firmware build.
2024-08-06 15:45:53 +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
00d5feaa8d Limit i_set within range of MAX1968 chip 2024-04-24 18:05:20 +08:00
09be55e12a Don't load REF pin of MAX1968 chip on HWRevs < 3.0
The REF pin of the MAX1968 on hardware revisions v2.x is missing a
buffer, loading the pin on every CPU ADC read. Avoid reading from it and
leave the pin floating on affected hardware revisions, and return the
nominal 1.5V instead.
2024-04-03 16:32:57 +08:00
76547be90a i_tec -> i_set
i_tec is reserved for the voltage signal coming out of the MAX1968 chip
for now.
2024-02-14 17:27:12 +08:00
8b975e656e Stop i_set from fluctuating in every report
i_set is a user-provided value that shouldn't fluctuate with every VREF
measurement. Storing i_set as channel state is the simplest way to avoid
that.
2024-02-14 17:21:39 +08:00
13 changed files with 292 additions and 218 deletions

View File

@ -268,7 +268,7 @@ with the following keys.
| `i_measured` | Amperes | Measured current passing through TEC |
| `v_measured` | Volts | Measured voltage across TEC |
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

40
flake.lock generated
View File

@ -1,21 +1,5 @@
{
"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,
@ -34,8 +18,28 @@
},
"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

@ -2,31 +2,24 @@
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.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 = pkgs.rust-bin.stable."1.66.0".default.override {
extensions = [ "rust-src" ];
targets = [ "thumbv7em-none-eabihf" ];
};
rust = rustChannelOfTargets "stable" null targets;
rustPlatform = pkgs.recurseIntoAttrs (pkgs.makeRustPlatform {
rustPlatform = pkgs.makeRustPlatform {
rustc = rust;
cargo = rust;
});
};
thermostat = rustPlatform.buildRustPackage {
name = "thermostat";
version = "0.0.0";
@ -58,21 +51,21 @@
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; [
packages = with pkgs; [
rust llvm
openocd dfu-util
] ++ (with python3Packages; [
numpy matplotlib
]);
};
defaultPackage.x86_64-linux = thermostat;
};
}

View File

@ -1,9 +1,12 @@
use crate::{ad7172, command_parser::CenterPoint, pid, steinhart_hart as sh};
use smoltcp::time::{Duration, Instant};
use uom::si::{
electric_current::ampere,
electric_potential::volt,
electrical_resistance::ohm,
f64::{ElectricPotential, ElectricalResistance, ThermodynamicTemperature, Time},
f64::{
ElectricCurrent, ElectricPotential, ElectricalResistance, ThermodynamicTemperature, Time,
},
thermodynamic_temperature::degree_celsius,
time::millisecond,
};
@ -19,6 +22,7 @@ pub struct ChannelState {
/// i_set 0A center point
pub center: CenterPoint,
pub dac_value: ElectricPotential,
pub i_set: ElectricCurrent,
pub pid_engaged: bool,
pub pid: pid::Controller,
pub sh: sh::Parameters,
@ -34,6 +38,7 @@ impl ChannelState {
adc_interval: Duration::from_millis(100),
center: CenterPoint::Vref,
dac_value: ElectricPotential::new::<volt>(0.0),
i_set: ElectricCurrent::new::<ampere>(0.0),
pid_engaged: false,
pid: pid::Controller::new(pid::Parameters::default()),
sh: sh::Parameters::default(),

View File

@ -4,10 +4,12 @@ use crate::{
channel_state::ChannelState,
command_handler::JsonBuffer,
command_parser::{CenterPoint, PwmPin},
hw_rev, pins, steinhart_hart,
pins::{self, Channel0VRef, Channel1VRef},
steinhart_hart,
};
use core::cmp::max_by;
use core::{cmp::max_by, marker::PhantomData};
use heapless::{consts::U2, Vec};
use num_traits::Zero;
use serde::{Serialize, Serializer};
use smoltcp::time::Instant;
use stm32f4xx_hal::hal;
@ -20,24 +22,46 @@ use uom::si::{
thermodynamic_temperature::degree_celsius,
};
pub enum PinsAdcReadTarget {
VREF,
DacVfb,
ITec,
VTec,
}
pub const CHANNELS: usize = 2;
const R_SENSE: f64 = 0.05;
// 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;
// From design specs
pub const MAX_I: ElectricCurrent = ElectricCurrent {
dimension: PhantomData,
units: PhantomData,
value: 2.0,
};
pub const MAX_TEC_V: ElectricPotential = ElectricPotential {
dimension: PhantomData,
units: PhantomData,
value: 4.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,
};
// 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,
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();
@ -61,7 +85,6 @@ impl<'a> Channels<'a> {
adc,
pins_adc,
pwm,
hwrev,
};
for channel in 0..CHANNELS {
channels.calibrate_dac_value(channel);
@ -103,7 +126,7 @@ impl<'a> Channels<'a> {
/// calculate the TEC i_set centerpoint
pub fn get_center(&mut self, channel: usize) -> ElectricPotential {
match self.channel_state(channel).center {
CenterPoint::Vref => self.read_vref(channel),
CenterPoint::Vref => self.adc_read(channel, PinsAdcReadTarget::VREF, 8),
CenterPoint::Override(center_point) => {
ElectricPotential::new::<volt>(center_point.into())
}
@ -117,16 +140,13 @@ impl<'a> Channels<'a> {
}
pub fn get_i_set(&mut self, channel: usize) -> ElectricCurrent {
let center_point = self.get_center(channel);
let r_sense = ElectricalResistance::new::<ohm>(R_SENSE);
let voltage = self.get_dac(channel);
(voltage - center_point) / (10.0 * r_sense)
let i_set = self.channel_state(channel).i_set;
i_set
}
/// 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(),
@ -137,6 +157,7 @@ impl<'a> Channels<'a> {
}
pub fn set_i(&mut self, channel: usize, i_set: ElectricCurrent) -> ElectricCurrent {
let i_set = i_set.min(MAX_I).max(-MAX_I);
let vref_meas = match channel {
0 => self.channel0.vref_meas,
1 => self.channel1.vref_meas,
@ -146,25 +167,108 @@ impl<'a> Channels<'a> {
let r_sense = ElectricalResistance::new::<ohm>(R_SENSE);
let voltage = i_set * 10.0 * r_sense + center_point;
let voltage = self.set_dac(channel, voltage);
(voltage - center_point) / (10.0 * r_sense)
let i_set = (voltage - center_point) / (10.0 * r_sense);
self.channel_state(channel).i_set = i_set;
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(
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_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,
);
let mv = self.pins_adc.sample_to_millivolts(sample);
) 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(
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_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,
);
let mv = self.pins_adc.sample_to_millivolts(sample);
) 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!(),
@ -176,9 +280,9 @@ impl<'a> Channels<'a> {
channel: usize,
tolerance: ElectricPotential,
) -> ElectricPotential {
let mut prev = self.read_dac_feedback(channel);
let mut prev = self.adc_read(channel, PinsAdcReadTarget::DacVfb, 1);
loop {
let current = self.read_dac_feedback(channel);
let current = self.adc_read(channel, PinsAdcReadTarget::DacVfb, 1);
if (current - prev).abs() < tolerance {
return current;
}
@ -186,73 +290,6 @@ impl<'a> Channels<'a> {
}
}
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 => {
let sample = self.pins_adc.convert(
&self.channel0.vref_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.vref_pin,
stm32f4xx_hal::adc::config::SampleTime::Cycles_480,
);
let mv = self.pins_adc.sample_to_millivolts(sample);
ElectricPotential::new::<millivolt>(mv as f64)
}
_ => 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!(),
}
}
/// Calibrates the DAC output to match vref of the MAX driver to reduce zero-current offset of the MAX driver output.
///
/// The thermostat DAC applies a control voltage signal to the CTLI pin of MAX driver chip to control its output current.
@ -301,8 +338,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,
@ -354,32 +390,35 @@ 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, MAX_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, MAX_I)
}
// Measure current passing through TEC
pub fn get_i_measured(&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)
}
// Measure voltage across TEC
pub fn get_v_measured(&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 {
@ -407,7 +446,7 @@ impl<'a> Channels<'a> {
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)
}
@ -418,7 +457,7 @@ impl<'a> Channels<'a> {
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.min(MAX_I).max(ElectricCurrent::zero()) / max).get::<ratio>();
let duty = self.set_pwm(channel, PwmPin::MaxIPos, duty);
(duty * max, max)
}
@ -429,13 +468,17 @@ impl<'a> Channels<'a> {
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.min(MAX_I).max(ElectricCurrent::zero()) / max).get::<ratio>();
let duty = self.set_pwm(channel, PwmPin::MaxINeg, duty);
(duty * max, max)
}
fn report(&mut self, channel: usize) -> Report {
let i_set = self.get_i_set(channel);
let i_measured = self.get_i_measured(channel);
let v_measured = self.get_v_measured(channel);
let state = self.channel_state(channel);
let pid_output = ElectricCurrent::new::<ampere>(state.pid.y1);
Report {
channel,
time: state.get_adc_time(),
@ -445,14 +488,10 @@ impl<'a> Channels<'a> {
.get_temperature()
.map(|temperature| temperature.get::<degree_celsius>()),
pid_engaged: state.pid_engaged,
pid_output: ElectricCurrent::new::<ampere>(state.pid.y1),
i_set: self.get_i_set(channel),
i_measured: if self.hwrev.major > 2 {
Some(self.get_i_measured(channel))
} else {
None
},
v_measured: self.get_v_measured(channel),
pid_output,
i_set,
i_measured,
v_measured,
}
}
@ -485,8 +524,8 @@ impl<'a> Channels<'a> {
PwmSummary {
channel,
center: CenterPointJson(self.channel_state(channel).center.clone()),
i_set: (self.get_i_set(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_set(channel), MAX_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(),
}
@ -533,11 +572,9 @@ impl<'a> Channels<'a> {
}
pub fn max_abs_i_measured(&mut self) -> ElectricCurrent {
max_by(
self.get_i_measured(0).abs(),
self.get_i_measured(1).abs(),
|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal),
)
max_by(self.get_i_measured(0).abs(), self.get_i_measured(1).abs(), |a, b| {
a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal)
})
}
}
@ -551,7 +588,7 @@ pub struct Report {
pid_engaged: bool,
pid_output: ElectricCurrent,
i_set: ElectricCurrent,
i_measured: Option<ElectricCurrent>,
i_measured: ElectricCurrent,
v_measured: ElectricPotential,
}

View File

@ -35,9 +35,9 @@ pub enum Handler {
#[derive(Clone, Debug, PartialEq)]
pub enum Error {
Report,
PostFilterRate,
Flash,
ReportError,
PostFilterRateError,
FlashError,
}
pub type JsonBuffer = Vec<u8, U1024>;
@ -87,7 +87,7 @@ impl Handler {
Err(e) => {
error!("unable to serialize report: {:?}", e);
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
return Err(Error::Report);
return Err(Error::ReportError);
}
}
Ok(Handler::Handled)
@ -101,7 +101,7 @@ impl Handler {
Err(e) => {
error!("unable to serialize pid summary: {:?}", e);
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
return Err(Error::Report);
return Err(Error::ReportError);
}
}
Ok(Handler::Handled)
@ -115,7 +115,7 @@ impl Handler {
Err(e) => {
error!("unable to serialize pwm summary: {:?}", e);
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
return Err(Error::Report);
return Err(Error::ReportError);
}
}
Ok(Handler::Handled)
@ -132,7 +132,7 @@ impl Handler {
Err(e) => {
error!("unable to serialize steinhart-hart summaries: {:?}", e);
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
return Err(Error::Report);
return Err(Error::ReportError);
}
}
Ok(Handler::Handled)
@ -146,7 +146,7 @@ impl Handler {
Err(e) => {
error!("unable to serialize postfilter summary: {:?}", e);
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
return Err(Error::Report);
return Err(Error::ReportError);
}
}
Ok(Handler::Handled)
@ -207,11 +207,11 @@ impl Handler {
channel: usize,
center: CenterPoint,
) -> Result<Handler, Error> {
let i_tec = channels.get_i_set(channel);
let i_set = channels.get_i_set(channel);
let state = channels.channel_state(channel);
state.center = center;
if !state.pid_engaged {
channels.set_i(channel, i_tec);
channels.set_i(channel, i_set);
}
send_line(socket, b"{}");
Ok(Handler::Handled)
@ -287,7 +287,7 @@ impl Handler {
socket,
b"{{\"error\": \"unable to choose postfilter rate\"}}",
);
return Err(Error::PostFilterRate);
return Err(Error::PostFilterRateError);
}
}
Ok(Handler::Handled)
@ -313,7 +313,7 @@ impl Handler {
Err(e) => {
error!("unable to load config from flash: {:?}", e);
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
return Err(Error::Flash);
return Err(Error::FlashError);
}
}
}
@ -338,7 +338,7 @@ impl Handler {
Err(e) => {
error!("unable to save channel {} config to flash: {:?}", c, e);
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
return Err(Error::Flash);
return Err(Error::FlashError);
}
}
}
@ -409,7 +409,7 @@ impl Handler {
Err(e) => {
error!("unable to serialize fan summary: {:?}", e);
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
Err(Error::Report)
Err(Error::ReportError)
}
}
}
@ -458,7 +458,7 @@ impl Handler {
Err(e) => {
error!("unable to serialize HWRev summary: {:?}", e);
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
Err(Error::Report)
Err(Error::ReportError)
}
}
}

View File

@ -1,6 +1,7 @@
use crate::{
ad7172::PostFilter, channels::Channels, command_parser::CenterPoint, pid, steinhart_hart,
};
use num_traits::Zero;
use serde::{Deserialize, Serialize};
use uom::si::{
electric_current::ampere,
@ -14,6 +15,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
@ -31,11 +33,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,
sh: state.sh.clone(),
pwm,
adc_postfilter,
@ -57,6 +65,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);
}
}
@ -69,7 +78,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

@ -1,4 +1,4 @@
use crate::{command_handler::JsonBuffer, hw_rev::HWSettings};
use crate::{channels::MAX_I, command_handler::JsonBuffer, hw_rev::HWSettings};
use num_traits::Float;
use serde::Serialize;
use stm32f4xx_hal::{
@ -9,9 +9,6 @@ use uom::si::{electric_current::ampere, f64::ElectricCurrent};
pub type FanPin = PwmChannels<TIM8, pwm::C4>;
// as stated in the schematics
const MAX_I: f32 = 3.0;
const MAX_USER_FAN_PWM: f32 = 100.0;
const MIN_USER_FAN_PWM: f32 = 1.0;
@ -49,7 +46,7 @@ impl FanCtrl {
pub fn cycle(&mut self, max_abs_i_measured: ElectricCurrent) {
self.max_abs_i_measured = max_abs_i_measured.get::<ampere>() as f32;
if self.fan_auto && self.hw_settings.fan_available {
let scaled_current = self.max_abs_i_measured / MAX_I;
let scaled_current = self.max_abs_i_measured / MAX_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))
@ -62,7 +59,7 @@ impl FanCtrl {
if self.hw_settings.fan_available {
let summary = FanSummary {
fan_pwm: self.get_pwm(),
max_abs_i_measured: self.max_abs_i_measured,
abs_max_tec_i: self.max_abs_i_measured,
auto_mode: self.fan_auto,
k_a: self.k_a,
k_b: self.k_b,
@ -98,7 +95,9 @@ impl FanCtrl {
return 0f32;
}
let fan = self.fan.as_mut().unwrap();
let fan_pwm = fan_pwm.clamp(MIN_USER_FAN_PWM as u32, MAX_USER_FAN_PWM as u32);
let fan_pwm = fan_pwm
.min(MAX_USER_FAN_PWM as u32)
.max(MIN_USER_FAN_PWM as u32);
let duty = scale_number(
fan_pwm as f32,
self.hw_settings.min_fan_pwm,
@ -157,7 +156,7 @@ fn scale_number(unscaled: f32, to_min: f32, to_max: f32, from_min: f32, from_max
#[derive(Serialize)]
pub struct FanSummary {
fan_pwm: u32,
max_abs_i_measured: f32,
abs_max_tec_i: f32,
auto_mode: bool,
k_a: f32,
k_b: f32,

View File

@ -147,7 +147,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)) => config.apply(&mut channels, c),
@ -280,10 +280,10 @@ fn main() -> ! {
}
// Apply new IPv4 address/gateway
if let Some(config) = new_ipv4_config.take() {
new_ipv4_config.take().map(|config| {
server.set_ipv4_config(config.clone());
ipv4_config = config;
}
});
// Update watchdog
wd.feed();

View File

@ -54,14 +54,12 @@ 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

@ -58,21 +58,31 @@ pub trait ChannelPins {
type TecUMeasPin;
}
pub enum Channel0VRef {
Analog(PA0<Analog>),
Disabled(PA0<Input<Floating>>),
}
impl ChannelPins for Channel0 {
type DacSpi = Dac0Spi;
type DacSync = PE4<Output<PushPull>>;
type Shdn = PE10<Output<PushPull>>;
type VRefPin = PA0<Analog>;
type VRefPin = Channel0VRef;
type ItecPin = PA6<Analog>;
type DacFeedbackPin = PA4<Analog>;
type TecUMeasPin = PC2<Analog>;
}
pub enum Channel1VRef {
Analog(PA3<Analog>),
Disabled(PA3<Input<Floating>>),
}
impl ChannelPins for Channel1 {
type DacSpi = Dac1Spi;
type DacSync = PF6<Output<PushPull>>;
type Shdn = PE15<Output<PushPull>>;
type VRefPin = PA3<Analog>;
type VRefPin = Channel1VRef;
type ItecPin = PB0<Analog>;
type DacFeedbackPin = PA5<Analog>;
type TecUMeasPin = PC3<Analog>;
@ -168,10 +178,22 @@ impl Pins {
clocks, tim1, tim3, gpioc.pc6, gpioc.pc7, gpioe.pe9, gpioe.pe11, gpioe.pe13, gpioe.pe14,
);
let hwrev = HWRev::detect_hw_rev(&HWRevPins {
hwrev0: gpiod.pd0,
hwrev1: gpiod.pd1,
hwrev2: gpiod.pd2,
hwrev3: gpiod.pd3,
});
let hw_settings = hwrev.settings();
let (dac0_spi, dac0_sync) = Self::setup_dac0(clocks, spi4, gpioe.pe2, gpioe.pe4, gpioe.pe6);
let mut shdn0 = gpioe.pe10.into_push_pull_output();
shdn0.set_low();
let vref0_pin = gpioa.pa0.into_analog();
let vref0_pin = if hwrev.major > 2 {
Channel0VRef::Analog(gpioa.pa0.into_analog())
} else {
Channel0VRef::Disabled(gpioa.pa0)
};
let itec0_pin = gpioa.pa6.into_analog();
let dac_feedback0_pin = gpioa.pa4.into_analog();
let tec_u_meas0_pin = gpioc.pc2.into_analog();
@ -188,7 +210,11 @@ impl Pins {
let (dac1_spi, dac1_sync) = Self::setup_dac1(clocks, spi5, gpiof.pf7, gpiof.pf6, gpiof.pf9);
let mut shdn1 = gpioe.pe15.into_push_pull_output();
shdn1.set_low();
let vref1_pin = gpioa.pa3.into_analog();
let vref1_pin = if hwrev.major > 2 {
Channel1VRef::Analog(gpioa.pa3.into_analog())
} else {
Channel1VRef::Disabled(gpioa.pa3)
};
let itec1_pin = gpiob.pb0.into_analog();
let dac_feedback1_pin = gpioa.pa5.into_analog();
let tec_u_meas1_pin = gpioc.pc3.into_analog();
@ -211,14 +237,6 @@ impl Pins {
channel1,
};
let hwrev = HWRev::detect_hw_rev(&HWRevPins {
hwrev0: gpiod.pd0,
hwrev1: gpiod.pd1,
hwrev2: gpiod.pd2,
hwrev3: gpiod.pd3,
});
let hw_settings = hwrev.settings();
let leds = Leds::new(
gpiod.pd9,
gpiod.pd10.into_push_pull_output(),

View File

@ -103,11 +103,16 @@ impl<'a, 'b, S: Default> Server<'a, 'b, S> {
fn set_ipv4_address(&mut self, ipv4_address: Ipv4Cidr) {
self.net.update_ip_addrs(|addrs| {
for addr in addrs.iter_mut() {
if let IpCidr::Ipv4(_) = addr {
match addr {
IpCidr::Ipv4(_) => {
*addr = IpCidr::Ipv4(ipv4_address);
// done
break;
}
_ => {
// skip
}
}
}
});
}

View File

@ -111,13 +111,19 @@ impl Session {
for (i, b) in buf.iter().enumerate() {
buf_bytes = i + 1;
let line = self.reader.feed(*b);
if let Some(line) = line {
match line {
Some(line) => {
let command = Command::parse(line);
if let Ok(Command::Reporting(reporting)) = command {
match command {
Ok(Command::Reporting(reporting)) => {
self.reporting = reporting;
}
_ => {}
}
return (buf_bytes, command.into());
}
None => {}
}
}
(buf_bytes, SessionInput::Nothing)
}