Compare commits

..

24 Commits

Author SHA1 Message Date
2dd01f8a15 Account for swapped current limits 2024-09-23 14:50:52 +08:00
ce4fb6090a README: polarity_swapped 2024-09-23 14:48:54 +08:00
f3d5b1dc6b add swap status to the report
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-09-23 14:48:54 +08:00
ca096c119d Add command for flipping output polarity
Needed for IDC cable connections, since the IDC connector and front
panel connectors have flipped polarities.
2024-09-23 14:48:54 +08:00
dfd4d355f6 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-23 14:47:20 +08:00
680193b34b Fix incorrect dac calibration algo
- Fix abnormally long calibration time
2024-09-20 21:21:14 +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
14 changed files with 321 additions and 302 deletions

3
.gitignore vendored
View File

@ -1,2 +1,5 @@
target/ target/
result 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 ### dfu-util on Linux
* Install the DFU USB tool (dfu-util). * 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. * 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. * Add jumper to Thermostat v2.0 across 2-pin jumper adjacent to JTAG connector.
* Cycle board power to put it in DFU update mode * Cycle board power to put it in DFU update mode
@ -94,43 +94,43 @@ The scope of this setting is per TCP session.
Send commands as simple text string terminated by `\n`. Responses are Send commands as simple text string terminated by `\n`. Responses are
formatted as line-delimited JSON. formatted as line-delimited JSON.
| Syntax | Function | | Syntax | Function |
|----------------------------------|-------------------------------------------------------------------------------| |-------------------------------------------|-------------------------------------------------------------------------------|
| `report` | Show current input | | `report` | Show current input |
| `report mode` | Show current report mode | | `report mode` | Show current report mode |
| `report mode <off/on>` | Set report mode | | `report mode <off/on>` | Set report mode |
| `pwm` | Show current PWM settings | | `pwm` | Show current PWM settings |
| `pwm <0/1> max_i_pos <amp>` | Set maximum positive output current | | `pwm <0/1> max_i_pos <amp>` | Set maximum positive output current |
| `pwm <0/1> max_i_neg <amp>` | Set maximum negative output current | | `pwm <0/1> max_i_neg <amp>` | Set maximum negative output current |
| `pwm <0/1> max_v <volt>` | Set maximum output voltage | | `pwm <0/1> max_v <volt>` | Set maximum output voltage |
| `pwm <0/1> i_set <amp>` | Disengage PID, set fixed output current | | `pwm <0/1> i_set <amp>` | Disengage PID, set fixed output current |
| `pwm <0/1> pid` | Let output current to be controlled by the PID | | `pwm <0/1> polarity_swapped <false/true>` | Swap output current polarity on channel |
| `center <0/1> <volt>` | Set the MAX1968 0A-centerpoint to the specified fixed voltage | | `pwm <0/1> pid` | Let output current to be controlled by the PID |
| `center <0/1> vref` | Set the MAX1968 0A-centerpoint to measure from VREF | | `center <0/1> <volt>` | Set the MAX1968 0A-centerpoint to the specified fixed voltage |
| `pid` | Show PID configuration | | `center <0/1> vref` | Set the MAX1968 0A-centerpoint to measure from VREF |
| `pid <0/1> target <deg_celsius>` | Set the PID controller target temperature | | `pid` | Show PID configuration |
| `pid <0/1> kp <value>` | Set proportional gain | | `pid <0/1> target <deg_celsius>` | Set the PID controller target temperature |
| `pid <0/1> ki <value>` | Set integral gain | | `pid <0/1> kp <value>` | Set proportional gain |
| `pid <0/1> kd <value>` | Set differential gain | | `pid <0/1> ki <value>` | Set integral gain |
| `pid <0/1> output_min <amp>` | Set mininum output | | `pid <0/1> kd <value>` | Set differential gain |
| `pid <0/1> output_max <amp>` | Set maximum output | | `pid <0/1> output_min <amp>` | Set mininum output |
| `s-h` | Show Steinhart-Hart equation parameters | | `pid <0/1> output_max <amp>` | Set maximum output |
| `s-h <0/1> <t0/b/r0> <value>` | Set Steinhart-Hart parameter for a channel | | `s-h` | Show Steinhart-Hart equation parameters |
| `postfilter` | Show postfilter settings | | `s-h <0/1> <t0/b/r0> <value>` | Set Steinhart-Hart parameter for a channel |
| `postfilter <0/1> off` | Disable postfilter | | `postfilter` | Show postfilter settings |
| `postfilter <0/1> rate <rate>` | Set postfilter output data rate | | `postfilter <0/1> off` | Disable postfilter |
| `load [0/1]` | Restore configuration for channel all/0/1 from flash | | `postfilter <0/1> rate <rate>` | Set postfilter output data rate |
| `save [0/1]` | Save configuration for channel all/0/1 to flash | | `load [0/1]` | Restore configuration for channel all/0/1 from flash |
| `reset` | Reset the device | | `save [0/1]` | Save configuration for channel all/0/1 to flash |
| `dfu` | Reset device and enters USB device firmware update (DFU) mode | | `reset` | Reset the device |
| `ipv4 <X.X.X.X/L> [Y.Y.Y.Y]` | Configure IPv4 address, netmask length, and optional default gateway | | `dfu` | Reset device and enters USB device firmware update (DFU) mode |
| `fan` | Show current fan settings and sensors' measurements | | `ipv4 <X.X.X.X/L> [Y.Y.Y.Y]` | Configure IPv4 address, netmask length, and optional default gateway |
| `fan <value>` | Set fan power with values from 1 to 100 | | `fan` | Show current fan settings and sensors' measurements |
| `fan auto` | Enable automatic fan speed control | | `fan <value>` | Set fan power with values from 1 to 100 |
| `fcurve <a> <b> <c>` | Set fan controller curve coefficients (see *Fan control* section) | | `fan auto` | Enable automatic fan speed control |
| `fcurve default` | Set fan controller curve coefficients to defaults (see *Fan control* section) | | `fcurve <a> <b> <c>` | Set fan controller curve coefficients (see *Fan control* section) |
| `hwrev` | Show hardware revision, and settings related to it | | `fcurve default` | Set fan controller curve coefficients to defaults (see *Fan control* section) |
| `swap [0/1]` | Swap TEC polarities for channel all/0/1 (For use with Zotino) | | `hwrev` | Show hardware revision, and settings related to it |
## USB ## USB
@ -181,11 +181,9 @@ postfilter rate can be tuned with the `postfilter` command.
## Thermo-Electric Cooling (TEC) ## Thermo-Electric Cooling (TEC)
- Connect TEC module device 0 to TEC0-/T0- and TEC0+/T0+. - Connect TEC module device 0 to TEC0- and TEC0+.
- Connect TEC module device 1 to TEC1-/T1- and TEC1+/T1+. - Connect TEC module device 1 to TEC1- and TEC1+.
- The GND pin is for shielding, not for sinking TEC module currents. - The GND pin is for shielding not for sinking TEC module currents.
For Zotino temperature regulation, connect an IDC cable to the internal Zotino header labeled TEC1/TEC2. For pre 3.0 Thermostats, use the `swap` command for those channels.
When using a TEC module with the Thermostat, the Thermostat expects the thermal load (where the thermistor is located) to cool down with a positive software current set point, and heat up with a negative current set point. When using a TEC module with the Thermostat, the Thermostat expects the thermal load (where the thermistor is located) to cool down with a positive software current set point, and heat up with a negative current set point.
@ -274,7 +272,7 @@ with the following keys.
| `tec_u_meas` | Volts | Measurement of the voltage across the TEC | | `tec_u_meas` | Volts | Measurement of the voltage across the TEC |
| `pid_output` | Amperes | PID control output | | `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 ## PID Tuning

48
flake.lock generated
View File

@ -1,41 +1,45 @@
{ {
"nodes": { "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": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1691421349, "lastModified": 1722791413,
"narHash": "sha256-RRJyX0CUrs4uW4gMhd/X4rcDG8PTgaaCQM5rXEJOx6g=", "narHash": "sha256-rCTrlCWvHzMCNcKxPE3Z/mMK2gDZ+BvvpEVyRM4tKmU=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "011567f35433879aae5024fc6ec53f2a0568a6c4", "rev": "8b5b6723aca5a51edf075936439d9cd3947b7b2c",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "NixOS", "owner": "NixOS",
"ref": "nixos-23.05", "ref": "nixos-24.05",
"repo": "nixpkgs", "repo": "nixpkgs",
"type": "github" "type": "github"
} }
}, },
"root": { "root": {
"inputs": { "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"; description = "Firmware for the Sinara 8451 Thermostat";
inputs.nixpkgs.url = github:NixOS/nixpkgs/nixos-23.05; inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.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 let
pkgs = import nixpkgs { system = "x86_64-linux"; overlays = [ (import mozilla-overlay) ]; }; pkgs = import nixpkgs { system = "x86_64-linux"; overlays = [ (import rust-overlay) ]; };
rustManifest = pkgs.fetchurl {
url = "https://static.rust-lang.org/dist/2022-12-15/channel-rust-stable.toml";
hash = "sha256-S7epLlflwt0d1GZP44u5Xosgf6dRrmr8xxC+Ml2Pq7c=";
};
targets = [ rust = pkgs.rust-bin.stable."1.66.0".default.override {
"thumbv7em-none-eabihf" extensions = [ "rust-src" ];
]; targets = [ "thumbv7em-none-eabihf" ];
rustChannelOfTargets = _channel: _date: targets: };
(pkgs.lib.rustLib.fromManifestFile rustManifest { rustPlatform = pkgs.makeRustPlatform {
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; rustc = rust;
cargo = rust; cargo = rust;
}); };
thermostat = rustPlatform.buildRustPackage { thermostat = rustPlatform.buildRustPackage {
name = "thermostat"; name = "thermostat";
version = "0.0.0"; version = "0.0.0";
@ -54,24 +47,26 @@
''; '';
dontFixup = true; dontFixup = true;
auditable = false;
}; };
in { in {
packages.x86_64-linux = { packages.x86_64-linux = {
inherit thermostat; inherit thermostat;
default = thermostat;
}; };
hydraJobs = { hydraJobs = {
inherit thermostat; inherit thermostat;
}; };
devShell.x86_64-linux = pkgs.mkShell { devShells.x86_64-linux.default = pkgs.mkShellNoCC {
name = "thermostat-dev-shell"; name = "thermostat-dev-shell";
buildInputs = with pkgs; [ packages = with pkgs; [
rust openocd dfu-util rust llvm
openocd dfu-util rlwrap
] ++ (with python3Packages; [ ] ++ (with python3Packages; [
numpy matplotlib numpy matplotlib
]); ]);
}; };
defaultPackage.x86_64-linux = thermostat;
}; };
} }

View File

@ -24,7 +24,7 @@ pub struct Channel<C: ChannelPins> {
pub vref_meas: ElectricPotential, pub vref_meas: ElectricPotential,
pub shdn: C::Shdn, pub shdn: C::Shdn,
pub vref_pin: C::VRefPin, pub vref_pin: C::VRefPin,
pub itec_pin: C::ItecPin, pub itec_pin: C::ITecPin,
/// feedback from `dac` output /// feedback from `dac` output
pub dac_feedback_pin: C::DacFeedbackPin, pub dac_feedback_pin: C::DacFeedbackPin,
pub tec_u_meas_pin: C::TecUMeasPin, pub tec_u_meas_pin: C::TecUMeasPin,

View File

@ -35,7 +35,7 @@ pub struct ChannelState {
pub pid_engaged: bool, pub pid_engaged: bool,
pub pid: pid::Controller, pub pid: pid::Controller,
pub sh: sh::Parameters, pub sh: sh::Parameters,
pub swap_tec_polarity: bool, pub polarity_swapped: bool,
} }
impl ChannelState { impl ChannelState {
@ -52,7 +52,7 @@ impl ChannelState {
pid_engaged: false, pid_engaged: false,
pid: pid::Controller::new(pid::Parameters::default()), pid: pid::Controller::new(pid::Parameters::default()),
sh: sh::Parameters::default(), sh: sh::Parameters::default(),
swap_tec_polarity: false, polarity_swapped: false,
} }
} }

View File

@ -1,5 +1,6 @@
use core::cmp::max_by; use core::marker::PhantomData;
use heapless::{consts::U2, Vec}; use heapless::{consts::U2, Vec};
use num_traits::Zero;
use serde::{Serialize, Serializer}; use serde::{Serialize, Serializer};
use smoltcp::time::Instant; use smoltcp::time::Instant;
use stm32f4xx_hal::hal; use stm32f4xx_hal::hal;
@ -20,31 +21,53 @@ use crate::{
command_handler::JsonBuffer, command_handler::JsonBuffer,
pins::{self, Channel0VRef, Channel1VRef}, pins::{self, Channel0VRef, Channel1VRef},
steinhart_hart, steinhart_hart,
hw_rev,
}; };
use crate::timer::sleep;
pub enum PinsAdcReadTarget {
VREF,
DacVfb,
ITec,
VTec,
}
pub const CHANNELS: usize = 2; pub const CHANNELS: usize = 2;
pub const R_SENSE: f64 = 0.05; pub const R_SENSE: f64 = 0.05;
// as stated in the MAX1968 datasheet // From design specs
pub const MAX_TEC_I: f64 = 3.0; 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 // 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 // TODO: -pub
pub struct Channels<'a> { pub struct Channels {
channel0: Channel<Channel0>, channel0: Channel<Channel0>,
channel1: Channel<Channel1>, channel1: Channel<Channel1>,
pub adc: ad7172::Adc<pins::AdcSpi, pins::AdcNss>, pub adc: ad7172::Adc<pins::AdcSpi, pins::AdcNss>,
/// stm32f4 integrated adc /// stm32f4 integrated adc
pins_adc: pins::PinsAdc, pins_adc: pins::PinsAdc,
pub pwm: pins::PwmPins, pub pwm: pins::PwmPins,
hwrev: &'a hw_rev::HWRev,
} }
impl<'a> Channels<'a> { impl Channels {
pub fn new(pins: pins::Pins, hwrev: &'a hw_rev::HWRev) -> Self { pub fn new(pins: pins::Pins) -> Self {
let mut adc = ad7172::Adc::new(pins.adc_spi, pins.adc_nss).unwrap(); let mut adc = ad7172::Adc::new(pins.adc_spi, pins.adc_nss).unwrap();
// Feature not used // Feature not used
adc.set_sync_enable(false).unwrap(); adc.set_sync_enable(false).unwrap();
@ -62,7 +85,7 @@ impl<'a> Channels<'a> {
let channel1 = Channel::new(pins.channel1, adc_calibration1); let channel1 = Channel::new(pins.channel1, adc_calibration1);
let pins_adc = pins.pins_adc; let pins_adc = pins.pins_adc;
let pwm = pins.pwm; 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 { for channel in 0..CHANNELS {
channels.calibrate_dac_value(channel); channels.calibrate_dac_value(channel);
channels.set_i(channel, ElectricCurrent::new::<ampere>(0.0)); 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 { pub fn get_center(&mut self, channel: usize) -> ElectricPotential {
match self.channel_state(channel).center { match self.channel_state(channel).center {
CenterPoint::Vref => CenterPoint::Vref =>
self.read_vref(channel), self.adc_read(channel, PinsAdcReadTarget::VREF, 8),
CenterPoint::Override(center_point) => CenterPoint::Override(center_point) =>
ElectricPotential::new::<volt>(center_point.into()), ElectricPotential::new::<volt>(center_point.into()),
} }
@ -118,16 +141,12 @@ impl<'a> Channels<'a> {
pub fn get_i(&mut self, channel: usize) -> ElectricCurrent { pub fn get_i(&mut self, channel: usize) -> ElectricCurrent {
let i_set = self.channel_state(channel).i_set; let i_set = self.channel_state(channel).i_set;
if self.channel_state(channel).swap_tec_polarity { i_set
-i_set
} else {
i_set
}
} }
/// i_set DAC /// i_set DAC
fn set_dac(&mut self, channel: usize, voltage: ElectricPotential) -> ElectricPotential { 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 { match channel {
0 => self.channel0.dac.set(value).unwrap(), 0 => self.channel0.dac.set(value).unwrap(),
1 => self.channel1.dac.set(value).unwrap(), 1 => self.channel1.dac.set(value).unwrap(),
@ -138,135 +157,123 @@ impl<'a> Channels<'a> {
} }
pub fn set_i(&mut self, channel: usize, i_set: ElectricCurrent) -> ElectricCurrent { pub fn set_i(&mut self, channel: usize, i_set: ElectricCurrent) -> ElectricCurrent {
// Silently clamp i_set let mut i_set = i_set.min(MAX_TEC_I).max(-MAX_TEC_I);
let i_ceiling = ElectricCurrent::new::<ampere>(MAX_TEC_I); self.channel_state(channel).i_set = i_set;
let i_floor = ElectricCurrent::new::<ampere>(-MAX_TEC_I); if self.channel_state(channel).polarity_swapped {
let mut i_set = i_set.min(i_ceiling).max(i_floor); i_set = -i_set;
}
let vref_meas = match channel.into() { let vref_meas = match channel.into() {
0 => self.channel0.vref_meas, 0 => self.channel0.vref_meas,
1 => self.channel1.vref_meas, 1 => self.channel1.vref_meas,
_ => unreachable!(), _ => unreachable!(),
}; };
if self.channel_state(channel).swap_tec_polarity {
i_set = -i_set;
}
let center_point = vref_meas; let center_point = vref_meas;
let r_sense = ElectricalResistance::new::<ohm>(R_SENSE); let r_sense = ElectricalResistance::new::<ohm>(R_SENSE);
let voltage = i_set * 10.0 * r_sense + center_point; let voltage = i_set * 10.0 * r_sense + center_point;
let voltage = self.set_dac(channel, voltage); let voltage = self.set_dac(channel, voltage);
let i_set = (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 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 { match channel {
0 => { 0 => {
let sample = self.pins_adc.convert( sample = match adc_read_target {
&self.channel0.dac_feedback_pin, PinsAdcReadTarget::VREF => {
stm32f4xx_hal::adc::config::SampleTime::Cycles_480 match &self.channel0.vref_pin {
); Channel0VRef::Analog(vref_pin) => {
let mv = self.pins_adc.sample_to_millivolts(sample); 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) ElectricPotential::new::<millivolt>(mv as f64)
} }
1 => { 1 => {
let sample = self.pins_adc.convert( sample = match adc_read_target {
&self.channel1.dac_feedback_pin, PinsAdcReadTarget::VREF => {
stm32f4xx_hal::adc::config::SampleTime::Cycles_480 match &self.channel1.vref_pin {
); Channel1VRef::Analog(vref_pin) => {
let mv = self.pins_adc.sample_to_millivolts(sample); 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) 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!(),
} }
} }
@ -296,8 +303,7 @@ impl<'a> Channels<'a> {
let mut start_value = 1; let mut start_value = 1;
let mut best_error = ElectricPotential::new::<volt>(100.0); 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) { for value in (start_value..=ad5680::MAX_VALUE).step_by(1 << step) {
match channel { match channel {
0 => { 0 => {
@ -308,24 +314,23 @@ impl<'a> Channels<'a> {
} }
_ => unreachable!(), _ => 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; let error = target_voltage - dac_feedback;
if error < ElectricPotential::new::<volt>(0.0) { if error < ElectricPotential::new::<volt>(0.0) {
break; break;
} else if error < best_error { } else if error < best_error {
best_error = error; best_error = error;
start_value = prev_value; start_value = 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 { match channel {
0 => self.channel0.vref_meas = vref, 0 => self.channel0.vref_meas = vref,
1 => self.channel1.vref_meas = vref, 1 => self.channel1.vref_meas = vref,
_ => unreachable!(), _ => unreachable!(),
} }
} }
prev_value = value;
} }
} }
@ -377,28 +382,26 @@ 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 max = 4.0 * ElectricPotential::new::<volt>(3.3);
let duty = self.get_pwm(channel, PwmPin::MaxV); 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) { 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); 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) { 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); 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 // Get current passing through TEC
pub fn get_tec_i(&mut self, channel: usize) -> ElectricCurrent { pub fn get_tec_i(&mut self, channel: usize) -> ElectricCurrent {
let tec_i = (self.read_itec(channel) - self.read_vref(channel)) / ElectricalResistance::new::<ohm>(0.4); let tec_i = (self.adc_read(channel, PinsAdcReadTarget::ITec, 16) - self.adc_read(channel, PinsAdcReadTarget::VREF, 16)) / ElectricalResistance::new::<ohm>(0.4);
if self.channel_state(channel).swap_tec_polarity { if self.channel_state(channel).polarity_swapped {
-tec_i -tec_i
} else { } else {
tec_i tec_i
@ -407,7 +410,7 @@ impl<'a> Channels<'a> {
// Get voltage across TEC // Get voltage across TEC
pub fn get_tec_v(&mut self, channel: usize) -> ElectricPotential { 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 { fn set_pwm(&mut self, channel: usize, pin: PwmPin, duty: f64) -> f64 {
@ -439,29 +442,29 @@ impl<'a> Channels<'a> {
pub fn set_max_v(&mut self, channel: usize, max_v: ElectricPotential) -> (ElectricPotential, ElectricPotential) { pub fn set_max_v(&mut self, channel: usize, max_v: ElectricPotential) -> (ElectricPotential, ElectricPotential) {
let max = 4.0 * ElectricPotential::new::<volt>(3.3); 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); let duty = self.set_pwm(channel, PwmPin::MaxV, duty);
(duty * max, max) (duty * max, max)
} }
pub fn set_max_i_pos(&mut self, channel: usize, max_i_pos: ElectricCurrent) -> (ElectricCurrent, ElectricCurrent) { pub fn set_max_i_pos(&mut self, channel: usize, max_i_pos: ElectricCurrent) -> (ElectricCurrent, ElectricCurrent) {
let max = ElectricCurrent::new::<ampere>(3.0); let max = ElectricCurrent::new::<ampere>(3.0);
let duty = (max_i_pos / max).get::<ratio>(); let duty = (max_i_pos.min(MAX_TEC_I).max(ElectricCurrent::zero()) / MAX_TEC_I_DUTY_TO_CURRENT_RATE).get::<ratio>();
let duty = self.set_pwm(channel, PwmPin::MaxIPos, duty); 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) { pub fn set_max_i_neg(&mut self, channel: usize, max_i_neg: ElectricCurrent) -> (ElectricCurrent, ElectricCurrent) {
let max = ElectricCurrent::new::<ampere>(3.0); let max = ElectricCurrent::new::<ampere>(3.0);
let duty = (max_i_neg / max).get::<ratio>(); let duty = (max_i_neg.min(MAX_TEC_I).max(ElectricCurrent::zero()) / MAX_TEC_I_DUTY_TO_CURRENT_RATE).get::<ratio>();
let duty = self.set_pwm(channel, PwmPin::MaxINeg, duty); 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 { fn report(&mut self, channel: usize) -> Report {
let i_set = self.get_i(channel); let i_set = self.get_i(channel);
let i_tec = if self.hwrev.major > 2 {Some(self.read_itec(channel))} else {None}; let i_tec = self.adc_read(channel, PinsAdcReadTarget::ITec, 16);
let tec_i = if self.hwrev.major > 2 {Some(self.get_tec_i(channel))} else {None}; let tec_i = self.get_tec_i(channel);
let dac_value = self.get_dac(channel); let dac_value = self.get_dac(channel);
let state = self.channel_state(channel); let state = self.channel_state(channel);
let pid_output = ElectricCurrent::new::<ampere>(state.pid.y1); let pid_output = ElectricCurrent::new::<ampere>(state.pid.y1);
@ -474,10 +477,10 @@ impl<'a> Channels<'a> {
temperature: state.get_temperature() temperature: state.get_temperature()
.map(|temperature| temperature.get::<degree_celsius>()), .map(|temperature| temperature.get::<degree_celsius>()),
pid_engaged: state.pid_engaged, pid_engaged: state.pid_engaged,
current_swapped: state.swap_tec_polarity, current_swapped: state.polarity_swapped,
i_set, i_set,
dac_value, dac_value,
dac_feedback: self.read_dac_feedback(channel), dac_feedback: self.adc_read(channel, PinsAdcReadTarget::DacVfb, 1),
i_tec, i_tec,
tec_i, tec_i,
tec_u_meas: self.get_tec_v(channel), tec_u_meas: self.get_tec_v(channel),
@ -514,8 +517,8 @@ impl<'a> Channels<'a> {
PwmSummary { PwmSummary {
channel, channel,
center: CenterPointJson(self.channel_state(channel).center.clone()), center: CenterPointJson(self.channel_state(channel).center.clone()),
i_set: (self.get_i(channel), ElectricCurrent::new::<ampere>(3.0)).into(), i_set: (self.get_i(channel), MAX_TEC_I).into(),
max_v: (self.get_max_v(channel), ElectricPotential::new::<volt>(5.0)).into(), max_v: self.get_max_v(channel).into(),
max_i_pos: self.get_max_i_pos(channel).into(), max_i_pos: self.get_max_i_pos(channel).into(),
max_i_neg: self.get_max_i_neg(channel).into(), max_i_neg: self.get_max_i_neg(channel).into(),
} }
@ -557,9 +560,10 @@ impl<'a> Channels<'a> {
} }
pub fn current_abs_max_tec_i(&mut self) -> ElectricCurrent { pub fn current_abs_max_tec_i(&mut self) -> ElectricCurrent {
max_by(self.get_tec_i(0).abs(), (0..CHANNELS)
self.get_tec_i(1).abs(), .map(|channel| self.get_tec_i(channel).abs())
|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal)) .max_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal))
.unwrap()
} }
} }
@ -576,8 +580,8 @@ pub struct Report {
i_set: ElectricCurrent, i_set: ElectricCurrent,
dac_value: ElectricPotential, dac_value: ElectricPotential,
dac_feedback: ElectricPotential, dac_feedback: ElectricPotential,
i_tec: Option<ElectricPotential>, i_tec: ElectricPotential,
tec_i: Option<ElectricCurrent>, tec_i: ElectricCurrent,
tec_u_meas: ElectricPotential, tec_u_meas: ElectricPotential,
pid_output: ElectricCurrent, pid_output: ElectricCurrent,
} }

View File

@ -181,6 +181,15 @@ impl Handler {
Ok(Handler::Handled) Ok(Handler::Handled)
} }
fn swap_polarity (socket: &mut TcpSocket, channels: &mut Channels, channel: usize, swapped: bool) -> Result<Handler, Error> {
channels.channel_state(channel).polarity_swapped = swapped;
let channel_state = channels.channel_state(channel);
let i_set = channel_state.i_set;
channels.set_i(channel, i_set);
send_line(socket, b"{}");
Ok(Handler::Handled)
}
fn set_pwm (socket: &mut TcpSocket, channels: &mut Channels, channel: usize, pin: PwmPin, value: f64) -> Result<Handler, Error> { fn set_pwm (socket: &mut TcpSocket, channels: &mut Channels, channel: usize, pin: PwmPin, value: f64) -> Result<Handler, Error> {
match pin { match pin {
PwmPin::ISet => { PwmPin::ISet => {
@ -195,11 +204,19 @@ impl Handler {
} }
PwmPin::MaxIPos => { PwmPin::MaxIPos => {
let current = ElectricCurrent::new::<ampere>(value); let current = ElectricCurrent::new::<ampere>(value);
channels.set_max_i_pos(channel, current); if channels.channel_state(channel).polarity_swapped {
channels.set_max_i_neg(channel, current);
} else {
channels.set_max_i_pos(channel, current);
}
} }
PwmPin::MaxINeg => { PwmPin::MaxINeg => {
let current = ElectricCurrent::new::<ampere>(value); let current = ElectricCurrent::new::<ampere>(value);
channels.set_max_i_neg(channel, current); if channels.channel_state(channel).polarity_swapped {
channels.set_max_i_pos(channel, current);
} else {
channels.set_max_i_neg(channel, current);
}
} }
} }
send_line(socket, b"{}"); send_line(socket, b"{}");
@ -345,7 +362,7 @@ impl Handler {
fn set_fan(socket: &mut TcpSocket, fan_pwm: u32, fan_ctrl: &mut FanCtrl) -> Result<Handler, Error> { fn set_fan(socket: &mut TcpSocket, fan_pwm: u32, fan_ctrl: &mut FanCtrl) -> Result<Handler, Error> {
if !fan_ctrl.fan_available() { 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); return Ok(Handler::Handled);
} }
fan_ctrl.set_auto_mode(false); fan_ctrl.set_auto_mode(false);
@ -374,7 +391,7 @@ impl Handler {
fn fan_auto(socket: &mut TcpSocket, fan_ctrl: &mut FanCtrl) -> Result<Handler, Error> { fn fan_auto(socket: &mut TcpSocket, fan_ctrl: &mut FanCtrl) -> Result<Handler, Error> {
if !fan_ctrl.fan_available() { 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); return Ok(Handler::Handled);
} }
fan_ctrl.set_auto_mode(true); fan_ctrl.set_auto_mode(true);
@ -412,16 +429,6 @@ impl Handler {
} }
} }
fn swap_tec_polarity (socket: &mut TcpSocket, channels: &mut Channels, channel: Option<usize>) -> Result<Handler, Error> {
for c in 0..CHANNELS {
if channel.is_none() || channel == Some(c) {
channels.channel_state(c).swap_tec_polarity = !channels.channel_state(c).swap_tec_polarity;
}
}
send_line(socket, b"{}");
Ok(Handler::Handled)
}
pub fn handle_command(command: Command, socket: &mut TcpSocket, channels: &mut Channels, session: &Session, store: &mut FlashStore, ipv4_config: &mut Ipv4Config, fan_ctrl: &mut FanCtrl, hwrev: HWRev) -> Result<Self, Error> { pub fn handle_command(command: Command, socket: &mut TcpSocket, channels: &mut Channels, session: &Session, store: &mut FlashStore, ipv4_config: &mut Ipv4Config, fan_ctrl: &mut FanCtrl, hwrev: HWRev) -> Result<Self, Error> {
match command { match command {
Command::Quit => Ok(Handler::CloseSocket), Command::Quit => Ok(Handler::CloseSocket),
@ -434,6 +441,7 @@ impl Handler {
Command::Show(ShowCommand::PostFilter) => Handler::show_post_filter(socket, channels), Command::Show(ShowCommand::PostFilter) => Handler::show_post_filter(socket, channels),
Command::Show(ShowCommand::Ipv4) => Handler::show_ipv4(socket, ipv4_config), Command::Show(ShowCommand::Ipv4) => Handler::show_ipv4(socket, ipv4_config),
Command::PwmPid { channel } => Handler::engage_pid(socket, channels, channel), Command::PwmPid { channel } => Handler::engage_pid(socket, channels, channel),
Command::PwmPolaritySwapped { channel, swapped } => Handler::swap_polarity(socket, channels, channel, swapped),
Command::Pwm { channel, pin, value } => Handler::set_pwm(socket, channels, channel, pin, value), Command::Pwm { channel, pin, value } => Handler::set_pwm(socket, channels, channel, pin, value),
Command::CenterPoint { channel, center } => Handler::set_center_point(socket, channels, channel, center), Command::CenterPoint { channel, center } => Handler::set_center_point(socket, channels, channel, center),
Command::Pid { channel, parameter, value } => Handler::set_pid(socket, channels, channel, parameter, value), Command::Pid { channel, parameter, value } => Handler::set_pid(socket, channels, channel, parameter, value),
@ -451,7 +459,6 @@ impl Handler {
Command::FanCurve { k_a, k_b, k_c } => Handler::fan_curve(socket, fan_ctrl, k_a, k_b, k_c), Command::FanCurve { k_a, k_b, k_c } => Handler::fan_curve(socket, fan_ctrl, k_a, k_b, k_c),
Command::FanCurveDefaults => Handler::fan_defaults(socket, fan_ctrl), Command::FanCurveDefaults => Handler::fan_defaults(socket, fan_ctrl),
Command::ShowHWRev => Handler::show_hwrev(socket, hwrev), Command::ShowHWRev => Handler::show_hwrev(socket, hwrev),
Command::SwapTECPolarity { channel } => Handler::swap_tec_polarity(socket, channels, channel),
} }
} }
} }

View File

@ -159,6 +159,10 @@ pub enum Command {
PwmPid { PwmPid {
channel: usize, channel: usize,
}, },
PwmPolaritySwapped {
channel: usize,
swapped: bool,
},
CenterPoint { CenterPoint {
channel: usize, channel: usize,
center: CenterPoint, center: CenterPoint,
@ -191,9 +195,6 @@ pub enum Command {
}, },
FanCurveDefaults, FanCurveDefaults,
ShowHWRev, ShowHWRev,
SwapTECPolarity {
channel: Option<usize>,
},
} }
fn end(input: &[u8]) -> IResult<&[u8], ()> { fn end(input: &[u8]) -> IResult<&[u8], ()> {
@ -242,6 +243,12 @@ fn off_on(input: &[u8]) -> IResult<&[u8], bool> {
))(input) ))(input)
} }
fn boolean(input: &[u8]) -> IResult<&[u8], bool> {
alt((value(false, tag("false")),
value(true, tag("true"))
))(input)
}
fn channel(input: &[u8]) -> IResult<&[u8], usize> { fn channel(input: &[u8]) -> IResult<&[u8], usize> {
map(one_of("01"), |c| (c as usize) - ('0' as usize))(input) map(one_of("01"), |c| (c as usize) - ('0' as usize))(input)
} }
@ -324,6 +331,16 @@ fn pwm_pid(input: &[u8]) -> IResult<&[u8], ()> {
value((), tag("pid"))(input) value((), tag("pid"))(input)
} }
fn pwm_polarity_swapped(input: &[u8]) -> IResult<&[u8], bool> {
preceded(
tag("polarity_swapped"),
preceded(
whitespace,
boolean,
)
)(input)
}
fn pwm(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> { fn pwm(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
let (input, _) = tag("pwm")(input)?; let (input, _) = tag("pwm")(input)?;
alt(( alt((
@ -336,6 +353,10 @@ fn pwm(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
let (input, ()) = pwm_pid(input)?; let (input, ()) = pwm_pid(input)?;
Ok((input, Ok(Command::PwmPid { channel }))) Ok((input, Ok(Command::PwmPid { channel })))
}, },
|input| {
let (input, swapped) = pwm_polarity_swapped(input)?;
Ok((input, Ok(Command::PwmPolaritySwapped { channel, swapped })))
},
|input| { |input| {
let (input, config) = pwm_setup(input)?; let (input, config) = pwm_setup(input)?;
match config { match config {
@ -587,22 +608,6 @@ fn fan_curve(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
))(input) ))(input)
} }
fn swap(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
let (input, _) = tag("swap")(input)?;
let (input, channel) = alt((
|input| {
let (input, _) = whitespace(input)?;
let (input, channel) = channel(input)?;
let (input, _) = end(input)?;
Ok((input, Some(channel)))
},
value(None, end)
))(input)?;
let result = Ok(Command::SwapTECPolarity { channel });
Ok((input, result))
}
fn command(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> { fn command(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
alt((value(Ok(Command::Quit), tag("quit")), alt((value(Ok(Command::Quit), tag("quit")),
load, load,
@ -619,7 +624,6 @@ fn command(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
fan, fan,
fan_curve, fan_curve,
value(Ok(Command::ShowHWRev), tag("hwrev")), value(Ok(Command::ShowHWRev), tag("hwrev")),
swap,
))(input) ))(input)
} }

View File

@ -1,3 +1,4 @@
use num_traits::Zero;
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use uom::si::{ use uom::si::{
electric_potential::volt, electric_potential::volt,
@ -18,11 +19,11 @@ pub struct ChannelConfig {
pid: pid::Parameters, pid: pid::Parameters,
pid_target: f32, pid_target: f32,
pid_engaged: bool, pid_engaged: bool,
i_set: ElectricCurrent,
sh: steinhart_hart::Parameters, sh: steinhart_hart::Parameters,
pwm: PwmLimits, pwm: PwmLimits,
/// uses variant `PostFilter::Invalid` instead of `None` to save space /// uses variant `PostFilter::Invalid` instead of `None` to save space
adc_postfilter: PostFilter, adc_postfilter: PostFilter,
swap_tec_polarity: bool,
} }
impl ChannelConfig { impl ChannelConfig {
@ -34,15 +35,20 @@ impl ChannelConfig {
.unwrap_or(PostFilter::Invalid); .unwrap_or(PostFilter::Invalid);
let state = channels.channel_state(channel); let state = channels.channel_state(channel);
let i_set = if state.pid_engaged {
ElectricCurrent::zero()
} else {
state.i_set
};
ChannelConfig { ChannelConfig {
center: state.center.clone(), center: state.center.clone(),
pid: state.pid.parameters.clone(), pid: state.pid.parameters.clone(),
pid_target: state.pid.target as f32, pid_target: state.pid.target as f32,
pid_engaged: state.pid_engaged, pid_engaged: state.pid_engaged,
i_set: i_set,
sh: state.sh.clone(), sh: state.sh.clone(),
pwm, pwm,
adc_postfilter, adc_postfilter,
swap_tec_polarity: state.swap_tec_polarity,
} }
} }
@ -53,7 +59,6 @@ impl ChannelConfig {
state.pid.target = self.pid_target.into(); state.pid.target = self.pid_target.into();
state.pid_engaged = self.pid_engaged; state.pid_engaged = self.pid_engaged;
state.sh = self.sh.clone(); state.sh = self.sh.clone();
state.swap_tec_polarity = self.swap_tec_polarity;
self.pwm.apply(channels, channel); self.pwm.apply(channels, channel);
@ -62,6 +67,7 @@ impl ChannelConfig {
adc_postfilter => Some(adc_postfilter), adc_postfilter => Some(adc_postfilter),
}; };
let _ = channels.adc.set_postfilter(channel as u8, adc_postfilter); let _ = channels.adc.set_postfilter(channel as u8, adc_postfilter);
let _ = channels.set_i(channel, self.i_set);
} }
} }
@ -74,7 +80,7 @@ struct PwmLimits {
impl PwmLimits { impl PwmLimits {
pub fn new(channels: &mut Channels, channel: usize) -> Self { 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_pos, _) = channels.get_max_i_pos(channel);
let (max_i_neg, _) = channels.get_max_i_neg(channel); let (max_i_neg, _) = channels.get_max_i_neg(channel);
PwmLimits { PwmLimits {

View File

@ -54,7 +54,7 @@ impl FanCtrl {
pub fn cycle(&mut self, abs_max_tec_i: ElectricCurrent) { pub fn cycle(&mut self, abs_max_tec_i: ElectricCurrent) {
self.abs_max_tec_i = abs_max_tec_i.get::<ampere>() as f32; self.abs_max_tec_i = abs_max_tec_i.get::<ampere>() as f32;
if self.fan_auto && self.hw_settings.fan_available { 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() // 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; 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); self.set_pwm(pwm);

View File

@ -138,7 +138,7 @@ fn main() -> ! {
let mut store = flash_store::store(dp.FLASH); 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 { for c in 0..CHANNELS {
match store.read_value::<ChannelConfig>(CHANNEL_CONFIG_KEY[c]) { match store.read_value::<ChannelConfig>(CHANNEL_CONFIG_KEY[c]) {
Ok(Some(config)) => Ok(Some(config)) =>

View File

@ -54,15 +54,13 @@ impl Controller {
// + x0 * (kp + ki + kd) // + x0 * (kp + ki + kd)
// - x1 * (kp + 2kd) // - x1 * (kp + 2kd)
// + x2 * kd // + x2 * kd
// + kp * (u0 - u1)
// y0 = clip(y0', ymin, ymax) // y0 = clip(y0', ymin, ymax)
pub fn update(&mut self, input: f64) -> f64 { pub fn update(&mut self, input: f64) -> f64 {
let mut output: f64 = self.y1 - self.target * f64::from(self.parameters.ki) 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) + 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.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() { if output < self.parameters.output_min.into() {
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 DacSync: OutputPin;
type Shdn: OutputPin; type Shdn: OutputPin;
type VRefPin; type VRefPin;
type ItecPin; type ITecPin;
type DacFeedbackPin; type DacFeedbackPin;
type TecUMeasPin; type TecUMeasPin;
} }
@ -76,7 +76,7 @@ impl ChannelPins for Channel0 {
type DacSync = PE4<Output<PushPull>>; type DacSync = PE4<Output<PushPull>>;
type Shdn = PE10<Output<PushPull>>; type Shdn = PE10<Output<PushPull>>;
type VRefPin = Channel0VRef; type VRefPin = Channel0VRef;
type ItecPin = PA6<Analog>; type ITecPin = PA6<Analog>;
type DacFeedbackPin = PA4<Analog>; type DacFeedbackPin = PA4<Analog>;
type TecUMeasPin = PC2<Analog>; type TecUMeasPin = PC2<Analog>;
} }
@ -91,7 +91,7 @@ impl ChannelPins for Channel1 {
type DacSync = PF6<Output<PushPull>>; type DacSync = PF6<Output<PushPull>>;
type Shdn = PE15<Output<PushPull>>; type Shdn = PE15<Output<PushPull>>;
type VRefPin = Channel1VRef; type VRefPin = Channel1VRef;
type ItecPin = PB0<Analog>; type ITecPin = PB0<Analog>;
type DacFeedbackPin = PA5<Analog>; type DacFeedbackPin = PA5<Analog>;
type TecUMeasPin = PC3<Analog>; type TecUMeasPin = PC3<Analog>;
} }
@ -108,7 +108,7 @@ pub struct ChannelPinSet<C: ChannelPins> {
pub dac_sync: C::DacSync, pub dac_sync: C::DacSync,
pub shdn: C::Shdn, pub shdn: C::Shdn,
pub vref_pin: C::VRefPin, pub vref_pin: C::VRefPin,
pub itec_pin: C::ItecPin, pub itec_pin: C::ITecPin,
pub dac_feedback_pin: C::DacFeedbackPin, pub dac_feedback_pin: C::DacFeedbackPin,
pub tec_u_meas_pin: C::TecUMeasPin, pub tec_u_meas_pin: C::TecUMeasPin,
} }