forked from M-Labs/thermostat
Compare commits
10 Commits
1ae6a6fdd4
...
3932bce9f3
Author | SHA1 | Date |
---|---|---|
linuswck | 3932bce9f3 | |
atse | ae4bea0c8a | |
atse | 1f2de942e4 | |
atse | 1041d3ecbb | |
atse | c6040899dd | |
atse | 9d89104f50 | |
atse | 136c7a0b52 | |
atse | 5000cae1b1 | |
atse | 78ec77509f | |
atse | 52aa3890c1 |
|
@ -1,2 +1,5 @@
|
|||
target/
|
||||
result
|
||||
*.bin
|
||||
|
||||
__pycache__/
|
||||
|
|
|
@ -45,7 +45,7 @@ There are several options for flashing Thermostat. DFU requires only a micro-USB
|
|||
|
||||
### dfu-util on Linux
|
||||
* Install the DFU USB tool (dfu-util).
|
||||
* Convert firmware from ELF to BIN: `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
|
||||
|
|
|
@ -2,16 +2,16 @@
|
|||
"nodes": {
|
||||
"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"
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
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.rust-overlay = {
|
||||
url = "github:oxalica/rust-overlay";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
|
@ -47,6 +47,7 @@
|
|||
'';
|
||||
|
||||
dontFixup = true;
|
||||
auditable = false;
|
||||
};
|
||||
in {
|
||||
packages.x86_64-linux = {
|
||||
|
@ -61,7 +62,8 @@
|
|||
devShells.x86_64-linux.default = pkgs.mkShellNoCC {
|
||||
name = "thermostat-dev-shell";
|
||||
packages = with pkgs; [
|
||||
rust openocd dfu-util
|
||||
rust llvm
|
||||
openocd dfu-util rlwrap
|
||||
] ++ (with python3Packages; [
|
||||
numpy matplotlib
|
||||
]);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use core::{cmp::max_by, marker::PhantomData};
|
||||
use core::marker::PhantomData;
|
||||
use heapless::{consts::U2, Vec};
|
||||
use num_traits::Zero;
|
||||
use serde::{Serialize, Serializer};
|
||||
|
@ -22,6 +22,7 @@ use crate::{
|
|||
pins::{self, Channel0VRef, Channel1VRef},
|
||||
steinhart_hart,
|
||||
};
|
||||
use crate::timer::sleep;
|
||||
|
||||
pub enum PinsAdcReadTarget {
|
||||
VREF,
|
||||
|
@ -45,6 +46,12 @@ pub const MAX_TEC_V: ElectricPotential = ElectricPotential {
|
|||
value: 4.0,
|
||||
};
|
||||
|
||||
// From datasheet, V_MAX_IPos/INeg = 10 * (I_limit * R_SENSE)
|
||||
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: ElectricPotential = ElectricPotential {
|
||||
dimension: PhantomData,
|
||||
|
@ -269,17 +276,6 @@ impl Channels {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn read_dac_feedback_until_stable(&mut self, channel: usize, tolerance: ElectricPotential) -> ElectricPotential {
|
||||
let mut prev = self.adc_read(channel, PinsAdcReadTarget::DacVfb, 1);
|
||||
loop {
|
||||
let current = self.adc_read(channel, PinsAdcReadTarget::DacVfb, 1);
|
||||
if (current - prev).abs() < tolerance {
|
||||
return current;
|
||||
}
|
||||
prev = current;
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
|
@ -306,7 +302,7 @@ impl Channels {
|
|||
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 {
|
||||
|
@ -318,8 +314,9 @@ impl Channels {
|
|||
}
|
||||
_ => 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;
|
||||
|
@ -394,15 +391,13 @@ impl Channels {
|
|||
}
|
||||
|
||||
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_TEC_I)
|
||||
(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_TEC_I)
|
||||
(duty * MAX_TEC_I_DUTY_TO_CURRENT_RATE, MAX_TEC_I)
|
||||
}
|
||||
|
||||
// Get current passing through TEC
|
||||
|
@ -451,14 +446,14 @@ impl Channels {
|
|||
|
||||
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.min(MAX_TEC_I).max(ElectricCurrent::zero()) / max).get::<ratio>();
|
||||
let duty = self.set_pwm(channel, PwmPin::MaxIPos, duty);
|
||||
let duty = (max_i_pos / MAX_TEC_I_DUTY_TO_CURRENT_RATE).get::<ratio>();
|
||||
let duty: f64 = self.set_pwm(channel, PwmPin::MaxIPos, duty);
|
||||
(duty * max, max)
|
||||
}
|
||||
|
||||
pub fn set_max_i_neg(&mut self, channel: usize, max_i_neg: ElectricCurrent) -> (ElectricCurrent, ElectricCurrent) {
|
||||
let max = ElectricCurrent::new::<ampere>(3.0);
|
||||
let duty = (max_i_neg.min(MAX_TEC_I).max(ElectricCurrent::zero()) / max).get::<ratio>();
|
||||
let duty = (max_i_neg / MAX_TEC_I_DUTY_TO_CURRENT_RATE).get::<ratio>();
|
||||
let duty = self.set_pwm(channel, PwmPin::MaxINeg, duty);
|
||||
(duty * max, max)
|
||||
}
|
||||
|
@ -561,9 +556,10 @@ impl Channels {
|
|||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -61,7 +61,7 @@ pub trait ChannelPins {
|
|||
type DacSync: OutputPin;
|
||||
type Shdn: OutputPin;
|
||||
type VRefPin;
|
||||
type ItecPin;
|
||||
type ITecPin;
|
||||
type DacFeedbackPin;
|
||||
type TecUMeasPin;
|
||||
}
|
||||
|
@ -76,7 +76,7 @@ impl ChannelPins for Channel0 {
|
|||
type DacSync = PE4<Output<PushPull>>;
|
||||
type Shdn = PE10<Output<PushPull>>;
|
||||
type VRefPin = Channel0VRef;
|
||||
type ItecPin = PA6<Analog>;
|
||||
type ITecPin = PA6<Analog>;
|
||||
type DacFeedbackPin = PA4<Analog>;
|
||||
type TecUMeasPin = PC2<Analog>;
|
||||
}
|
||||
|
@ -91,7 +91,7 @@ impl ChannelPins for Channel1 {
|
|||
type DacSync = PF6<Output<PushPull>>;
|
||||
type Shdn = PE15<Output<PushPull>>;
|
||||
type VRefPin = Channel1VRef;
|
||||
type ItecPin = PB0<Analog>;
|
||||
type ITecPin = PB0<Analog>;
|
||||
type DacFeedbackPin = PA5<Analog>;
|
||||
type TecUMeasPin = PC3<Analog>;
|
||||
}
|
||||
|
@ -108,7 +108,7 @@ pub struct ChannelPinSet<C: ChannelPins> {
|
|||
pub dac_sync: C::DacSync,
|
||||
pub shdn: C::Shdn,
|
||||
pub vref_pin: C::VRefPin,
|
||||
pub itec_pin: C::ItecPin,
|
||||
pub itec_pin: C::ITecPin,
|
||||
pub dac_feedback_pin: C::DacFeedbackPin,
|
||||
pub tec_u_meas_pin: C::TecUMeasPin,
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue