Compare commits

...

101 Commits

Author SHA1 Message Date
linuswck 8c1cb3117c README: Add notes on `i_tec` & `tec_i`readouts 2024-05-02 17:48:47 +08:00
linuswck 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
linuswck 9fce19a418 Revert "Disable feedback current readout on flawed HW Revs"
This reverts commit ae3d8b51d4.
2024-05-02 14:38:40 +08:00
atse 00d5feaa8d Limit i_set within range of MAX1968 chip 2024-04-24 18:05:20 +08:00
atse 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
atse 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
atse 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
atse ae3d8b51d4 Disable feedback current readout on flawed HW Revs
Thermostats v2.2 and below have a noisy and offset feedback current
`tec_i` caused by missing hardware on 2 MAX1968 TEC driver pins:

1. A missing RC filter on the ITEC pin that would have isolated CPU
sampling pulses from the signal; and
2. Some missing buffering on the VREF pin that would have avoided
loading the VREF signal, preventing voltage drops from the nominal 1.5V.

Since the resulting signal `tec_i` derived from these two signals can
have an error of around +/- 100mA, and readback may affect the stability
performance of the Thermostat, disable current readback entirely on
affected hardware revisions for now.

See https://github.com/sinara-hw/Thermostat/issues/117 and
https://github.com/sinara-hw/Thermostat/issues/120.

On hardware revisions v3.x and above, this would be fixed.
2024-01-31 12:12:22 +08:00
atse 17edae44fb README: Proofread fan control documentation 2024-01-30 12:43:19 +08:00
atse 03b4561142 Refactor current_abs_max_tec_i to use uom 2024-01-30 11:41:52 +08:00
atse 631a10938d README: Remove VREF 2024-01-26 17:00:27 +08:00
atse 6cd6a6a2c2 Fix warning '...not permit being left uninit..d'
Put SocketState initialisation logic in new. This avoids using an unsafe
and unnerving MaybeUninit::uninit().assume_init() to initialise an
array, which the compiler yells at since it causes undefined behavior.
2024-01-17 15:29:56 +08:00
atse b93e2fbb7b Update rust edition 2024-01-17 15:29:56 +08:00
atse 76b95f66e0 Use latest working stable rust 2024-01-17 15:29:41 +08:00
atse 8008870bc1 Switch panic_handler to panic_halt
Move away from panic_abort as it uses intrinsics, which is nightly only.
2024-01-17 15:29:15 +08:00
atse 7646ff9037 README: Avoid deprecated OpenOCD ST-Link config
The config file interface/stlink-v2-1.cfg is deprecated, and the warning
message encourages the switch to interface/stlink.cfg. Do accordingly.
2024-01-04 12:44:52 +08:00
atse 6f81a63d12 Remove unused LED parameters 2023-09-20 15:51:37 +08:00
atse 78012f6fdd flake: Use rust from manifest, not from pkgs
Fix the rustPlatform deprecation warnings properly.
2023-09-20 11:29:38 +08:00
atse bb4f43fe1c Remove stale reference to channel_state vref 2023-08-22 17:16:25 +08:00
atse 9df0fe406f Remove VREF in reports
Since VREF is an implementation detail, there shouldn't be a need to
include it in reports.

The ChannelState vref is removed along with it as its only use was to
save VREF measurements for later reporting.
2023-08-22 11:40:42 +08:00
topquark12 5ba74c6d9b README: Correct expected TEC polarity
Adhere to the general convention of TECs cooling down with positive
voltages.
2023-08-15 16:37:51 +08:00
atse 6f0acc73b8 Update LED L3 for PID status on every cycle
Check if PID is engaged on any channel every cycle, and match the status
with LED L3.
2023-08-10 16:43:19 +08:00
atse f29e86310d Update nix repos 2023-08-09 11:23:32 +08:00
atse b04a61c414 Turn off LED L3 only when all channels have no PID
Change the behaviour of LED L3 to turn off only when all channels have
PID disengaged, as opposed to when any channel disengages PID.

Otherwise, when disengaging PID on a Thermostat that has had both
channels engaged in PID, the LED would turn off, even when PID is still
engaged on the other channel.

This lets the LED better reflect the status of the Thermostat as a
whole, as it would stay on as long as PID is engaged on at least one
channel.
2023-08-07 16:09:54 +08:00
atse cd680dd6cd README: Correct unit of time in reports 2023-07-20 17:45:16 +08:00
Egor Savkin e3e3237d2f Emit warning when current/voltage limits are near zero
Signed-off-by: Egor Savkin <es@m-labs.hk>
2023-03-23 16:58:05 +08:00
Egor Savkin 570c0324b3 implement support for fan PWM
Co-authored-by: Egor Savkin <es@m-labs.hk>
Co-committed-by: Egor Savkin <es@m-labs.hk>
2023-03-22 17:15:49 +08:00
mwojcik 5688b2f1bb flake: update stm32-eth sha256 hash 2022-03-02 17:21:17 +08:00
mwojcik 1b2f2f3888 update code to use stm32f4xx-hal 0.10.1 API 2022-03-02 17:21:17 +08:00
mwojcik e6f63ec940 cargo/nix: update rustc, stm32-eth, smoltcp 2022-03-02 17:21:17 +08:00
mwojcik 67446ae99e cargo/flake: update package versions 2022-03-02 17:21:17 +08:00
topquark12 26ad2f0119 rewrite PID
Rewrite of PID according to https://hackmd.io/IACbwcOTSt6Adj3_F9bKuw PID implementation.

To migrate:
- TEC+/- pin polarity has to be reversed.
- Some saved settings might be wiped upon flashing of new firmware, back up settings before upgrade
- Min / Max integral parameters no longer exist
- kp, ki, kd will likely need to be retuned

The software has been tested on hardware with good temperature control stability.

Reviewed-on: M-Labs/thermostat#68
Co-authored-by: topquark12 <aw@m-labs.hk>
Co-committed-by: topquark12 <aw@m-labs.hk>
2022-02-24 20:16:47 +08:00
Alex Wong 69dabf5aa1 update hash 2022-01-24 17:20:32 +08:00
mwojcik a26cdfabb1 readme: update CI links 2022-01-19 11:37:01 +08:00
mwojcik 4d43709f76 nix flakes support (#64)
Reviewed-on: M-Labs/thermostat#64
Co-authored-by: mwojcik <mw@m-labs.hk>
Co-committed-by: mwojcik <mw@m-labs.hk>
2022-01-19 11:13:47 +08:00
Sebastien Bourdeauducq 1c00e732fa README: minor corrections 2022-01-05 08:22:43 +08:00
Sebastien Bourdeauducq 09082b24a5 README: update build instructions 2022-01-05 08:04:25 +08:00
Sebastien Bourdeauducq 85e8273d51 shell.nix: follow nix-scripts 2022-01-05 07:54:07 +08:00
Sebastien Bourdeauducq e81c6d1692 README: fix objcopy command 2022-01-05 07:50:53 +08:00
Sebastien Bourdeauducq 1f644fd62c README: fix nix-scripts folder 2021-07-14 08:43:59 +08:00
Sebastien Bourdeauducq 4f1d865d2b README: fix Hydra links 2021-07-14 08:42:53 +08:00
topquark12 e6a5c31db6 main.rs refactor to reduce length (#60)
Move command handling to command_handler.rs to shorten main.rs

Reviewed-on: M-Labs/thermostat#60
Co-authored-by: topquark12 <aw@m-labs.hk>
Co-committed-by: topquark12 <aw@m-labs.hk>
2021-06-07 10:07:05 +08:00
topquark12 a5d8661b10 main: fix handling of incomplete data received (#55)
Reviewed-on: M-Labs/thermostat#55
Co-Authored-By: topquark12 <aw@m-labs.hk>
Co-Committed-By: topquark12 <aw@m-labs.hk>
2021-01-29 16:18:07 +08:00
topquark12 7cb0ed70be Reset all TCP sockets before MCU reset (#53)
Co-Authored-By: topquark12 <aw@m-labs.hk>
Co-Committed-By: topquark12 <aw@m-labs.hk>
2021-01-26 17:45:14 +08:00
topquark12 6b9d61737e docs: update docs to reflect improved stability performance 2021-01-25 13:51:50 +08:00
topquark12 16844a1dc1 dac: fix inconsistent current output behavior due to repeated sampling of noisy vref 2021-01-25 13:51:50 +08:00
topquark12 96f52ace8b pytec: simpler default graphs in plot 2021-01-25 13:51:50 +08:00
topquark12 a1a8efd51a readme: update docs on max_i_neg command and heat flow directions 2021-01-25 13:51:50 +08:00
Sebastien Bourdeauducq 8eb3cc4307 dfu: style 2021-01-18 16:59:13 +08:00
topquark12 f3661ac8e3 dfu: refactor 2021-01-18 16:45:01 +08:00
topquark12 c4e3be1d05 fix pid.rs test, exclude dfu from test 2021-01-16 11:04:24 +08:00
topquark12 cf3ace4d2d flash_store: get addresses from linker
Reviewed-on: M-Labs/thermostat#49
Co-Authored-By: topquark12 <aw@m-labs.hk>
Co-Committed-By: topquark12 <aw@m-labs.hk>
2021-01-13 17:30:12 +08:00
topquark12 f6802635a4 add command to reboot into DFU
Co-Authored-By: topquark12 <aw@m-labs.hk>
Co-Committed-By: topquark12 <aw@m-labs.hk>
2021-01-13 11:59:06 +08:00
topquark12 9e4d06fdbc clarify comment 2021-01-11 16:24:43 +08:00
topquark12 3433881d0f remove dead code 2021-01-11 14:10:50 +08:00
topquark12 193d54a0a6 pid: anti-windup when compliance voltage reached 2021-01-11 14:00:52 +08:00
topquark12 3067b356c5 channels: add methods to retrieve actual voltage and current of TEC 2021-01-08 16:18:20 +08:00
topquark12 3ba2cc9ddc channels: voltage measurement refalect actual voltage across TEC 2021-01-08 15:52:35 +08:00
topquark12 1539b624bd pid: more sensible default parameters 2021-01-08 11:31:33 +08:00
topquark12 5c84b7438b Integral rescaling 2021-01-08 11:25:01 +08:00
topquark12 cc0126636c report save success
save does not hang, it just did not report save success. Closes #33

Reviewed-on: M-Labs/thermostat#44
Co-Authored-By: topquark12 <aw@m-labs.hk>
Co-Committed-By: topquark12 <aw@m-labs.hk>
2021-01-07 18:01:43 +08:00
topquark12 45b7c4e669 add documentation about PID setup and using Python tools
Co-Authored-By: topquark12 <aw@m-labs.hk>
Co-Committed-By: topquark12 <aw@m-labs.hk>
2021-01-07 14:48:39 +08:00
topquark12 73dd6d9154 add PID autotune code
Co-Authored-By: topquark12 <aw@m-labs.hk>
Co-Committed-By: topquark12 <aw@m-labs.hk>
2021-01-06 11:02:52 +08:00
topquark12 e94601f54f pid: fix derivative calculation
Reviewed-on: M-Labs/thermostat#40
Co-Authored-By: topquark12 <aw@m-labs.hk>
Co-Committed-By: topquark12 <aw@m-labs.hk>
2020-12-29 17:09:03 +08:00
topquark12 8c9e12587f fix simulation math, provided simulation control loop is stable and passes test, reaching simulation cycle limit before settling fails test 2020-12-28 17:38:10 +08:00
topquark12 7c013ff4a4 PID fixes
Flipped error calculation method to correct behavior of kP and kI terms.

Added anti integral windup to integral handling.

Changed how the i and integral term is calculated, to prevent old kI settings from affecting the current i term calculation when kI is being tuned. Especially noticable when kI is set from a non-zero value to zero.
Co-Authored-By: topquark12 <aw@m-labs.hk>
Co-Committed-By: topquark12 <aw@m-labs.hk>
2020-12-26 11:47:21 +08:00
Sebastien Bourdeauducq 50a1b9f52d pid: partial -> proportional 2020-12-26 11:01:40 +08:00
Astro 9852b32646 command_parser, main: implement ShowCommand::Ipv4
Fixes Gitea issue #30
2020-12-20 20:44:10 +01:00
Astro 22b0c9fcad main: don't re- set_ipv4_config every tick 2020-12-20 20:43:20 +01:00
Astro e13ed37271 pid: fix tests 2020-12-20 20:24:24 +01:00
Astro 5987d9c881 README: rlwrap nc, 60 Hz 2020-12-18 21:27:09 +01:00
Astro 7c55e34145 pytec: remove obsolete conversions 2020-12-18 19:37:25 +01:00
Astro b176fc2788 pid: doc parameters 2020-12-18 16:29:53 +01:00
Astro b717ac5495 pid: update default gain parameters 2020-12-18 16:27:47 +01:00
Astro 980d27ebfc pytec: remove client-side interval calculation 2020-12-18 15:44:11 +01:00
Astro e9e46b29cf pid: integrate time_delta to free gain parameters from sampling period
Fixes Gitea issue #22
2020-12-18 15:40:05 +01:00
Astro b7e6cdbec2 pytec: measure interval 2020-12-16 22:14:21 +01:00
Astro 93ea46d512 README: doc postfilter
Fixes Gitea issue #21
2020-12-16 20:47:18 +01:00
Astro dc41473493 update dependencies 2020-12-16 19:21:23 +01:00
Astro 7a28cb1cd4 shell.nix: use rustPlaform from <nix-scripts> by default
Fixes Gitea issue #25
2020-12-13 20:42:29 +01:00
Astro c3dd03dcf3 update cargoSha256 2020-12-13 02:41:58 +01:00
Astro b2f455b2cf config: save/store pid_engaged
Fixes Gitea issue #17
2020-12-13 02:33:59 +01:00
Astro 2e7be3fe01 shell.nix: add df-util 2020-12-13 02:31:19 +01:00
Astro ff91dd7baa Cargo.toml: obtain sfkv and stm32f4xx-hal via git 2020-12-13 02:29:35 +01:00
Astro ecc00a6aeb init_log: delint 2020-12-13 02:24:29 +01:00
Astro 97813f917d flash_store: get to a working state 2020-12-13 01:17:03 +01:00
Astro 880a887c40 new flash-based ipv4 config with additional mask_len, gateway 2020-12-12 23:44:16 +01:00
Astro 383ebcd8e4 rewrite config for sfkv-based flash_store 2020-12-12 01:25:07 +01:00
Sebastien Bourdeauducq 088bd6eb76 README: cleanup build and flashing instructions 2020-12-09 10:16:40 +08:00
Sebastien Bourdeauducq 35d1e2e205 Revert "not yet ready note"
This reverts commit 7af5d8582d.

M-Labs/thermostat#23 (comment)
2020-12-09 10:02:36 +08:00
Astro 1090d0f5b5 Merge pull request 'flashing with dfu-util' (#20) from jbqubit/thermostat:flashdoc into master
Update based on two issues.
M-Labs/thermostat#10
https://github.com/sinara-hw/Thermostat/issues/87/

Reviewed-on: M-Labs/thermostat#20
2020-12-09 08:16:45 +08:00
Astro 23d0c470e5 pytec: rename test.py to example.py 2020-12-09 01:08:34 +01:00
Astro 5c8bb47e11 command_parser: require the explicit `i_set` symbol 2020-12-09 01:07:08 +01:00
Astro b92a5f18cd README: fix units 2020-12-09 00:56:17 +01:00
Astro c125e20bdb shell.nix: init 2020-12-09 00:46:31 +01:00
Joe Britton 7af5d8582d not yet ready note 2020-12-08 12:00:00 -05:00
Joe Britton fca2de665c flashing with dfu-util 2020-12-08 11:07:48 -05:00
Astro ffcc3f661b README: add pid units 2020-12-07 23:18:44 +01:00
Astro 2a6f8ed874 pytec: use py3 dict methods
Fixes Gitea issue #14
2020-12-07 16:57:50 +01:00
Astro 5e8bf0e765 README: add Debugging and Flashing
Fixes Gitea issue #10
2020-12-07 00:22:00 +01:00
Astro 5ddd4d250e channels: swap adc inputs
Fixes Gitea issue #12
2020-12-07 00:22:00 +01:00
32 changed files with 2481 additions and 1042 deletions

174
Cargo.lock generated
View File

@ -1,5 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "aligned"
version = "0.3.4"
@ -68,12 +70,9 @@ checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de"
[[package]]
name = "cast"
version = "0.2.3"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b9434b9a5aa1450faa3f9cb14ea0e8c53bb5d2b3c1bfd1ab4fc03e9f33fbfb0"
dependencies = [
"rustc_version",
]
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]]
name = "cfg-if"
@ -82,14 +81,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
[[package]]
name = "cortex-m"
version = "0.6.4"
name = "chrono"
version = "0.4.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88cdafeafba636c00c467ded7f1587210725a1adfab0c24028a7844b87738263"
checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
dependencies = [
"num-integer",
"num-traits",
]
[[package]]
name = "cortex-m"
version = "0.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9075300b07c6a56263b9b582c214d0ff037b00d45ec9fde1cc711490c56f1bb9"
dependencies = [
"aligned",
"bare-metal 0.2.5",
"bitfield",
"cortex-m 0.7.4",
"volatile-register",
]
[[package]]
name = "cortex-m"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37ff967e867ca14eba0c34ac25cd71ea98c678e741e3915d923999bb2fe7c826"
dependencies = [
"bare-metal 0.2.5",
"bitfield",
"embedded-hal",
"volatile-register",
]
@ -99,7 +121,7 @@ version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d63959cb1e003dd97233fee6762351540253237eadf06fcdcb98cbfa3f9be4a"
dependencies = [
"cortex-m",
"cortex-m 0.6.7",
"cortex-m-semihosting",
"log",
]
@ -110,10 +132,19 @@ version = "0.6.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "980c9d0233a909f355ed297ef122f257942de5e0a2cb1c39f60684b65bcb90fb"
dependencies = [
"cortex-m-rt-macros",
"cortex-m-rt-macros 0.1.8",
"r0",
]
[[package]]
name = "cortex-m-rt"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c433da385b720d5bb9f52362fa2782420798e68d40d67bfe4b0d992aba5dfe7"
dependencies = [
"cortex-m-rt-macros 0.7.0",
]
[[package]]
name = "cortex-m-rt-macros"
version = "0.1.8"
@ -126,12 +157,23 @@ dependencies = [
]
[[package]]
name = "cortex-m-semihosting"
version = "0.3.5"
name = "cortex-m-rt-macros"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "113ef0ecffee2b62b58f9380f4469099b30e9f9cbee2804771b4203ba1762cfa"
checksum = "f0f6f3e36f203cfedbc78b357fb28730aa2c6dc1ab060ee5c2405e843988d3c7"
dependencies = [
"cortex-m",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "cortex-m-semihosting"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bffa6c1454368a6aa4811ae60964c38e6996d397ff8095a8b9211b1c1f749bc"
dependencies = [
"cortex-m 0.7.4",
]
[[package]]
@ -154,9 +196,9 @@ dependencies = [
[[package]]
name = "embedded-hal"
version = "0.2.4"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa998ce59ec9765d15216393af37a58961ddcefb14c753b4816ba2191d865fcb"
checksum = "e36cfb62ff156596c892272f3015ef952fe1525e85261fa3a7f327bd6b384ab9"
dependencies = [
"nb 0.1.3",
"void",
@ -264,6 +306,16 @@ dependencies = [
"version_check",
]
[[package]]
name = "num-integer"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
dependencies = [
"autocfg",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.14"
@ -275,18 +327,18 @@ dependencies = [
]
[[package]]
name = "panic-abort"
version = "0.3.2"
name = "panic-halt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e20e6499bbbc412f280b04a42346b356c6fa0753d5fd22b7bd752ff34c778ee"
checksum = "de96540e0ebde571dc55c73d60ef407c653844e6f9a1e2fdbd40c07b9252d812"
[[package]]
name = "panic-semihosting"
version = "0.5.4"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aed16eb761d0ee9161dd1319cb38c8007813b20f9720a5a682b283e7b8cdfe58"
checksum = "c3d55dedd501dfd02514646e0af4d7016ce36bc12ae177ef52056989966a1eec"
dependencies = [
"cortex-m",
"cortex-m 0.7.4",
"cortex-m-semihosting",
]
@ -333,9 +385,18 @@ checksum = "e2a38df5b15c8d5c7e8654189744d8e396bddc18ad48041a500ce52d6948941f"
[[package]]
name = "rand_core"
version = "0.5.1"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
[[package]]
name = "rtcc"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef35f9dcbf434a34dcc99b3ebba1c1945d49c70832958e932e83dc63a5273994"
dependencies = [
"chrono",
]
[[package]]
name = "rustc_version"
@ -363,9 +424,9 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
[[package]]
name = "serde"
version = "1.0.117"
version = "1.0.118"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b88fa983de7720629c9387e9f517353ed404164b1e482c970a90c1a4aaf7dc1a"
checksum = "06c64263859d87aa2eb554587e2d23183398d617427327cf2b3d0ed8c69e4800"
dependencies = [
"serde_derive",
]
@ -382,9 +443,9 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.117"
version = "1.0.118"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbd1ae72adb44aab48f325a02444a5fc079349a8d804c1fc922aed3f7454c74e"
checksum = "c84d3526699cd55261af4b941e4e725444df67aa4f9e6a3564f18030d12672df"
dependencies = [
"proc-macro2",
"quote",
@ -392,10 +453,21 @@ dependencies = [
]
[[package]]
name = "smoltcp"
version = "0.6.0"
name = "sfkv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fe46639fd2ec79eadf8fe719f237a7a0bd4dac5d957f1ca5bbdbc1c3c39e53a"
checksum = "25f5bfac3f66a7c10a6f37ee81aeaa471f4d35dc21665b59ad7c555adcb9e8aa"
dependencies = [
"byteorder",
"postcard",
"serde",
]
[[package]]
name = "smoltcp"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e4a069bef843d170df47e7c0a8bf8d037f217d9f5b325865acc3e466ffe40d3"
dependencies = [
"bitflags",
"byteorder",
@ -412,10 +484,10 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "stm32-eth"
version = "0.2.0"
source = "git+https://github.com/stm32-rs/stm32-eth.git#4d6b29bf1ecdd1f68e5bc304a3d4f170049896c8"
source = "git+https://github.com/stm32-rs/stm32-eth.git?rev=3759c5c9#3759c5c99c0ab69bb71759030766bc0fba0b6cde"
dependencies = [
"aligned",
"cortex-m",
"cortex-m 0.7.4",
"smoltcp",
"stm32f4xx-hal",
"volatile-register",
@ -423,29 +495,31 @@ dependencies = [
[[package]]
name = "stm32f4"
version = "0.11.0"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11460b4de3a84f072e2cf6e76306c64d27f405a0e83bace0a726f555ddf4bf33"
checksum = "da3d56009c8f32e4f208dbea17df72484154d1040a8969b75d8c73eb7b18fe8f"
dependencies = [
"bare-metal 0.2.5",
"cortex-m",
"cortex-m-rt",
"cortex-m 0.7.4",
"cortex-m-rt 0.6.13",
"vcell",
]
[[package]]
name = "stm32f4xx-hal"
version = "0.8.3"
source = "git+https://github.com/stm32-rs/stm32f4xx-hal.git#e80925770d2fe72f0f01a7b46147f4e31d512689"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a06fde2dd27c0ba934c9e69b62af66eb1c20dbb6d741b187a763912e9892d13"
dependencies = [
"bare-metal 0.2.5",
"bare-metal 1.0.0",
"cast",
"cortex-m",
"cortex-m-rt",
"cortex-m 0.7.4",
"cortex-m-rt 0.7.1",
"embedded-dma",
"embedded-hal",
"nb 0.1.3",
"nb 1.0.0",
"rand_core",
"rtcc",
"stm32f4",
"synopsys-usb-otg",
"void",
@ -453,9 +527,9 @@ dependencies = [
[[package]]
name = "syn"
version = "1.0.48"
version = "1.0.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc371affeffc477f42a221a1e4297aedcea33d47d19b61455588bd9d8f6b19ac"
checksum = "9a2af957a63d6bd42255c359c93d9bfdb97076bd3b820897ce55ffbfbf107f44"
dependencies = [
"proc-macro2",
"quote",
@ -468,7 +542,7 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "461676dcf123675b3d3b02e2390e6a690cd186aacf2f439af7673c79e2561d53"
dependencies = [
"cortex-m",
"cortex-m 0.6.7",
"usb-device",
"vcell",
]
@ -480,20 +554,20 @@ dependencies = [
"bare-metal 1.0.0",
"bit_field",
"byteorder",
"cortex-m",
"cortex-m 0.6.7",
"cortex-m-log",
"cortex-m-rt",
"cortex-m-rt 0.6.13",
"eeprom24x",
"heapless",
"log",
"nb 1.0.0",
"nom",
"num-traits",
"panic-abort",
"panic-halt",
"panic-semihosting",
"postcard",
"serde",
"serde-json-core",
"sfkv",
"smoltcp",
"stm32-eth",
"stm32f4xx-hal",

View File

@ -7,23 +7,23 @@ authors = ["Astro <astro@spaceboyz.net>"]
version = "0.0.0"
keywords = ["thermostat", "laser", "physics"]
repository = "https://git.m-labs.hk/M-Labs/thermostat"
edition = "2018"
edition = "2021"
[package.metadata.docs.rs]
features = []
default-target = "thumbv7em-none-eabihf"
[dependencies]
panic-abort = "0.3"
panic-halt = "0.2"
panic-semihosting = { version = "0.5", optional = true }
log = "0.4"
bare-metal = "1"
cortex-m = "0.6"
cortex-m-rt = { version = "0.6", features = ["device"] }
cortex-m-log = { version = "0.6", features = ["log-integration"] }
stm32f4xx-hal = { version = "0.8", features = ["rt", "stm32f427", "usb_fs"] }
stm32-eth = { version = "0.2", features = ["stm32f427", "smoltcp-phy"], git = "https://github.com/stm32-rs/stm32-eth.git" }
smoltcp = { version = "0.6.0", default-features = false, features = ["proto-ipv4", "socket-tcp", "log"] }
stm32f4xx-hal = { version = "=0.10.1", features = ["rt", "stm32f427", "usb_fs"] }
stm32-eth = { rev = "3759c5c9", features = ["stm32f427", "smoltcp-phy"], git = "https://github.com/stm32-rs/stm32-eth.git" }
smoltcp = { version = "0.7.5", default-features = false, features = ["proto-ipv4", "socket-tcp", "log"] }
bit_field = "0.10"
byteorder = { version = "1", default-features = false }
nom = { version = "5", default-features = false }
@ -34,13 +34,9 @@ nb = "1"
uom = { version = "0.30", default-features = false, features = ["autoconvert", "si", "f64", "use_serde"] }
eeprom24x = "0.3"
serde = { version = "1.0", default-features = false, features = ["derive"] }
postcard = "0.5"
heapless = "0.5"
serde-json-core = "0.1"
[patch.crates-io]
stm32f4xx-hal = { git = "https://github.com/stm32-rs/stm32f4xx-hal.git" }
sfkv = "0.1"
[features]
semihosting = ["panic-semihosting", "cortex-m-log/semihosting"]

207
README.md
View File

@ -1,26 +1,71 @@
# Firmware for the Sinara 8451 Thermostat
- [x] [Continuous Integration](https://nixbld.m-labs.hk/job/stm32/stm32/thermostat)
- [x] [Download latest firmware build](https://nixbld.m-labs.hk/job/stm32/stm32/thermostat/latest/download-by-type/file/binary-dist)
- [x] [Continuous Integration](https://nixbld.m-labs.hk/job/mcu/thermostat/thermostat)
- [x] Download latest firmware build: [ELF](https://nixbld.m-labs.hk/job/mcu/thermostat/thermostat/latest/download/1) [BIN](https://nixbld.m-labs.hk/job/mcu/thermostat/thermostat/latest/download/2)
## Building
### Debian-based systems (tested on Ubuntu 19.10)
### Reproducible build with Nix
- install git, clone this repository
- install [rustup](https://rustup.rs/)
Thermostat firmware is packaged using the [Nix](https://nixos.org) Flakes system. Install Nix 2.4+ and enable flakes by adding ``experimental-features = nix-command flakes`` to ``nix.conf`` (e.g. ``~/.config/nix/nix.conf``).
Once you have Flakes enabled, you can use ``nix build`` to build the firmware.
### Development environment
Clone this repository and with Nix Flakes enabled, use the following commands:
```shell
rustup toolchain install nightly
rustup update
rustup target add thumbv7em-none-eabihf --toolchain nightly
rustup default nightly
nix develop
cargo build --release
```
The resulting ELF file will be located under `target/thumbv7em-none-eabihf/release/thermostat`
The resulting ELF file will be located under `target/thumbv7em-none-eabihf/release/thermostat`.
Alternatively, you can install the Rust toolchain without Nix using rustup; see the Rust manifest file pulled in `flake.nix` to determine which Rust version to use.
## Debugging
Connect SWDIO/SWCLK/RST/GND to a programmer such as ST-Link v2.1. Run OpenOCD:
```shell
openocd -f interface/stlink.cfg -f target/stm32f4x.cfg
```
You may need to power up the programmer before powering the device.
Leave OpenOCD running. Run the GNU debugger:
```shell
gdb target/thumbv7em-none-eabihf/release/thermostat
(gdb) source openocd.gdb
```
## Flashing
There are several options for flashing Thermostat. DFU requires only a micro-USB connector, whereas OpenOCD needs a JTAG/SWD adapter.
### 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)
* 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
* Push firmware to flash: `dfu-util -a 0 -s 0x08000000:leave -D thermostat.bin`
* Remove jumper
* Cycle power to leave DFU update mode
### st.com DfuSe tool on Windows
On a Windows machine install [st.com](https://st.com) DfuSe USB device firmware upgrade (DFU) software. [link](https://www.st.com/en/development-tools/stsw-stm32080.html).
- add jumper to Thermostat v2.0 across 2-pin jumper adjacent to JTAG connector
- cycle board power to put it in DFU update mode
- connect micro-USB to PC
- use st.com software to upload firmware
- remove jumper
- cycle power to leave DFU update mode
### OpenOCD
```shell
openocd -f interface/stlink.cfg -f target/stm32f4x.cfg -c "program target/thumbv7em-none-eabihf/release/thermostat verify reset;exit"
```
## Network
@ -30,7 +75,7 @@ Ethernet, IP: 192.168.1.26/24
Use netcat to connect to port 23/tcp (telnet)
```sh
nc -vv 192.168.1.26 23
rlwrap nc -vv 192.168.1.26 23
```
telnet clients send binary data after connect. Enter \n once to
@ -44,39 +89,47 @@ Set report mode to `on` for a continuous stream of input data.
The scope of this setting is per TCP session.
### Commands
### TCP commands
| Syntax | Function |
| --- | --- |
| `report` | Show current input |
| `report mode` | Show current report mode |
| `report mode <off/on>` | Set report mode |
| `pwm` | Show current PWM settings |
| `pwm <0/1> max_i_pos <ratio>` | Set PWM duty cycle for **max_i_pos** to *ampere* |
| `pwm <0/1> max_i_neg <ratio>` | Set PWM duty cycle for **max_i_neg** to *ampere* |
| `pwm <0/1> max_v <ratio>` | Set PWM duty cycle for **max_v** to *volt* |
| `pwm <0/1> <volts>` | Disengage PID, set **i_set** DAC to *ampere* |
| `pwm <0/1> pid` | Set PWM to be controlled by PID |
| `center <0/1> <volts>` | Set the MAX1968 0A-centerpoint to *volts* |
| `center <0/1> vref` | Set the MAX1968 0A-centerpoint to measure from VREF |
| `pid` | Show PID configuration |
| `pid <0/1> target <value>` | Set the PID controller target |
| `pid <0/1> kp <value>` | Set proportional gain |
| `pid <0/1> ki <value>` | Set integral gain |
| `pid <0/1> kd <value>` | Set differential gain |
| `pid <0/1> output_min <value>` | Set mininum output |
| `pid <0/1> output_max <value>` | Set maximum output |
| `pid <0/1> integral_min <value>` | Set integral lower bound |
| `pid <0/1> integral_max <value>` | Set integral upper bound |
| `s-h` | Show Steinhart-Hart equation parameters |
| `s-h <0/1> <t0/b/r0> <value>` | Set Steinhart-Hart parameter for a channel |
| `postfilter` | Show postfilter settings |
| `postfilter <0/1> off` | Disable postfilter |
| `postfilter <0/1> rate <rate>` | Set postfilter output data rate |
| `load` | Restore configuration from EEPROM |
| `save` | Save configuration to EEPROM |
| `reset` | Reset the device |
| `ipv4 <X.X.X.X>` | Configure IPv4 address |
Send commands as simple text string terminated by `\n`. Responses are
formatted as line-delimited JSON.
| Syntax | Function |
|----------------------------------|-------------------------------------------------------------------------------|
| `report` | Show current input |
| `report mode` | Show current report mode |
| `report mode <off/on>` | Set report mode |
| `pwm` | Show current PWM settings |
| `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_v <volt>` | Set maximum output voltage |
| `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 |
| `center <0/1> <volt>` | Set the MAX1968 0A-centerpoint to the specified fixed voltage |
| `center <0/1> vref` | Set the MAX1968 0A-centerpoint to measure from VREF |
| `pid` | Show PID configuration |
| `pid <0/1> target <deg_celsius>` | Set the PID controller target temperature |
| `pid <0/1> kp <value>` | Set proportional gain |
| `pid <0/1> ki <value>` | Set integral gain |
| `pid <0/1> kd <value>` | Set differential gain |
| `pid <0/1> output_min <amp>` | Set mininum output |
| `pid <0/1> output_max <amp>` | Set maximum output |
| `s-h` | Show Steinhart-Hart equation parameters |
| `s-h <0/1> <t0/b/r0> <value>` | Set Steinhart-Hart parameter for a channel |
| `postfilter` | Show postfilter settings |
| `postfilter <0/1> off` | Disable postfilter |
| `postfilter <0/1> rate <rate>` | Set postfilter output data rate |
| `load [0/1]` | Restore configuration for channel all/0/1 from flash |
| `save [0/1]` | Save configuration for channel all/0/1 to flash |
| `reset` | Reset the device |
| `dfu` | Reset device and enters USB device firmware update (DFU) mode |
| `ipv4 <X.X.X.X/L> [Y.Y.Y.Y]` | Configure IPv4 address, netmask length, and optional default gateway |
| `fan` | Show current fan settings and sensors' measurements |
| `fan <value>` | Set fan power with values from 1 to 100 |
| `fan auto` | Enable automatic fan speed control |
| `fcurve <a> <b> <c>` | Set fan controller curve coefficients (see *Fan control* section) |
| `fcurve default` | Set fan controller curve coefficients to defaults (see *Fan control* section) |
| `hwrev` | Show hardware revision, and settings related to it |
## USB
@ -87,7 +140,7 @@ with logging via semihosting.)
**Caveat:** This logging does not flush its output. Doing so would
hang indefinitely if the output is not read by the USB host. Therefore
output will be truncated once buffers are full.
output will be truncated when USB buffers are full.
## Temperature measurement
@ -112,12 +165,28 @@ Set the Beta parameter:
s-h 0 b 3800
```
### 50/60 Hz filtering
The AD7172-2 ADC on the SENS inputs supports simultaneous rejection of
50 Hz ± 1 Hz and 60 Hz ± 1 Hz (dB). Affecting sampling rate, the
postfilter rate can be tuned with the `postfilter` command.
| Postfilter rate | Rejection | Effective sampling rate |
| --- | :---: | --- |
| 16.67 Hz | 92 dB | 8.4 Hz |
| 20 Hz | 86 dB | 9.1 Hz |
| 21.25 Hz | 62 dB | 10 Hz |
| 27 Hz | 47 dB | 10.41 Hz |
## Thermo-Electric Cooling (TEC)
- Connect Peltier device 0 to TEC0- and TEC0+.
- Connect Peliter device 1 to TEC1- and TEC1+.
- The GND pin is for shielding not for sinking Peltier currents.
- Connect TEC module device 0 to TEC0- and TEC0+.
- Connect TEC module device 1 to TEC1- and TEC1+.
- The GND pin is for shielding not for sinking TEC module currents.
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.
Testing heat flow direction with a low set current is recommended before installation of the TEC module.
### Limits
@ -126,18 +195,28 @@ output limits.
Use the `pwm` command to see current settings and maximum values.
| Limit | Unit | Description |
| --- | :---: | --- |
| `max_v` | Volts | Maximum voltage |
| `max_i_pos` | Amperes | Maximum positive current |
| `max_i_neg` | Amperes | Maximum negative current |
| | Amperes | Output current control |
| Limit | Unit | Description |
| --- | :---: | --- |
| `max_v` | Volts | Maximum voltage |
| `max_i_pos` | Amperes | Maximum positive current |
| `max_i_neg` | Amperes | Maximum negative current |
| `i_set` | Amperes | (Not a limit; Open-loop mode) |
Example: set the maximum voltage of channel 0 to 1.5 V.
```
pwm 0 max_v 1.5
```
Example: set the maximum negative current of channel 0 to -3 A.
```
pwm 0 max_i_neg 3
```
Example: set the maximum positive current of channel 1 to 3 A.
```
pwm 0 max_i_pos 3
```
### Open-loop mode
To manually control TEC output current, omit the limit parameter of
@ -146,7 +225,7 @@ channel.
Example: set output current of channel 0 to 0 A.
```
pwm 0 0
pwm 0 i_set 0
```
## PID-stabilized temperature control
@ -179,16 +258,32 @@ with the following keys.
| Key | Unit | Description |
| --- | :---: | --- |
| `channel` | Integer | Channel `0`, or `1` |
| `time` | Milliseconds | Temperature measurement time |
| `time` | Seconds | Temperature measurement time |
| `adc` | Volts | AD7172 input |
| `sens` | Ohms | Thermistor resistance derived from `adc` |
| `temperature` | Degrees Celsius | Steinhart-Hart conversion result derived from `sens` |
| `pid_engaged` | Boolean | `true` if in closed-loop mode |
| `i_set` | Amperes | TEC output current |
| `vref` | Volts | MAX1968 VREF (1.5 V) |
| `dac_value` | Volts | AD5680 output derived from `i_set` |
| `dac_feedback` | Volts | ADC measurement of the AD5680 output |
| `i_tec` | Volts | MAX1968 TEC current monitor |
| `tec_i` | Amperes | TEC output current feedback derived from `i_tec` |
| `tec_u_meas` | Volts | Measurement of the voltage across the TEC |
| `pid_output` | Amperes | PID control output |
Note: With Thermostat v2 and below, the voltage and current readouts `i_tec` and `tec_i` are noisy without the hardware fix shown in [this PR][https://git.m-labs.hk/M-Labs/thermostat/pulls/105].
## PID Tuning
The thermostat implements a PID control loop for each of the TEC channels, more details on setting up the PID control loop can be found [here](./doc/PID%20tuning.md).
## Fan control
Fan control commands are available for thermostat revisions with an integrated fan system:
1. `fan` - show fan stats: `fan_pwm`, `abs_max_tec_i`, `auto_mode`, `k_a`, `k_b`, `k_c`.
2. `fan auto` - enable auto speed controller mode, where fan speed is controlled by the fan curve `fcurve`.
3. `fan <value>` - set the fan power with the value from `1` to `100` and disable auto mode. There is no way to completely disable the fan.
Please note that power doesn't correlate with the actual speed linearly.
4. `fcurve <a> <b> <c>` - set coefficients of the controlling curve `a*x^2 + b*x + c`, where `x` is `abs_max_tec_i/MAX_TEC_I`, a normalized value in range [0,1],
i.e. the (linear) proportion of current output capacity used, on the channel with the largest current flow. The controlling curve is also clamped to [0,1].
5. `fcurve default` - restore fan curve coefficients to defaults: `a = 1.0, b = 0.0, c = 0.0`.

View File

@ -1 +0,0 @@
"055x3b3kqi7bi17ya6iaiq9hlsiy8f3v6bn47s6dizc6y4xn9v2y"

81
doc/PID tuning.md Normal file
View File

@ -0,0 +1,81 @@
# PID Tuning
## Note on hardware setup
The heat sinking side of the TEC module should be thermally bonded to a large heat-sinking thermal mass to ensure maximum temperature stability, a large optical table had provided good results in tests.
The thermal load under control should be well insulated from the surrounding for maximum stability, closed cell foam had been tested showing good results.
## Real time plot
When tuning Thermostat PID parameters, it is helpful to view the temperature, PID output and other data in the form of a real time graph.
To use the Python real-time plotting utility, run
```shell
python pytec/plot.py
```
![default view](./assets/default%20view.png)
## Temperature Setpoints and Thermal Load
A PID controller with the same set of PID parameters may not work identically across all temperatures, especially when comparing the performance of a TEC module cooling a load versus heating a load. This is due to self ohmic heating of the TEC module aiding efficiency when heating, but harming efficiency when cooling.
When a PID loop is expected to operate the TEC in both heating and cooling modes, it is important to verify the loop performance in both modes.
For systems expected to operate at a narrow range of temperatures, it is a good idea to tune the PID loop at the temperature region of interest.
The same is also true for controlling loads that are expected to produce heat, e.g. laser cooling blocks. Testing the loop performance across varying amount of thermal load is needed to ensure stability in operation.
## Manual Tuning
Below are some general guidelines for manually tuning PID loops. Note that every system is different, and some of the values mentioned below may not apply to all systems.
1. To start the manual tuning process, set the kp, ki and kd parameters to 0.
2. Begin by increasing kp until the temperature begins to oscillate. Offset between the target temperature and the actual temperature can be ignored for now.
3. Reduce kp by 30%, increase ki until the offset between target and actual temperature is eliminated.
4. Increase kd until the maximum allowable amount of overshoot is observed.
5. Some tweaking will be needed to obtain the desired result, especially when trying to balance between minimizing overshoot and maximizing response speed.
## Auto Tuning
A PID auto tuning utility is provided in the Pytec library. The auto tuning utility drives the the load to a controlled oscillation, observes the ultimate gain and oscillation period and calculates a set of PID parameters.
To run the auto tuning utility, run
```shell
python pytec/autotune.py
```
After some time, the auto tuning utility will output the auto tuning results, below is a sample output
```shell
Ku: 0.7553203471147422
Pu: 75.93899999999977
rule: ziegler-nichols
kp: 0.45319220826884526
Ki: 0.011935690706194357
Kd: 4.301870387965967
rule: tyreus-luyben
kp: 0.3432930977636503
Ki: 0.0020549280832497956
Kd: 4.137825730504864
.
.
.
```
At the end of the test, the ultimate gain `Ku`, oscillation period `Pu` and a few sets of recommended PID parameters are calculated and displayed.
Multiple suggested sets of PID parameters based on different calculation rules are displayed. While all sets are expected to work, the different sets trade off response time with overshoot differently, and testing is needed to see which set works best for the system on hand.
With a well designed and constructed setup, the PID parameters calculated by the auto tune utility together with some manual tweaking can yield sub-mK control stability.
Below shows data captured on an experiment setup, with 300uK stability over 12 hours.
![twelve_hours](./assets/twelve_hours.png)

BIN
doc/assets/default view.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
doc/assets/twelve_hours.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

44
flake.lock Normal file
View File

@ -0,0 +1,44 @@
{
"nodes": {
"mozilla-overlay": {
"flake": false,
"locked": {
"lastModified": 1690536331,
"narHash": "sha256-aRIf2FB2GTdfF7gl13WyETmiV/J7EhBGkSWXfZvlxcA=",
"owner": "mozilla",
"repo": "nixpkgs-mozilla",
"rev": "db89c8707edcffefcd8e738459d511543a339ff5",
"type": "github"
},
"original": {
"owner": "mozilla",
"repo": "nixpkgs-mozilla",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1691421349,
"narHash": "sha256-RRJyX0CUrs4uW4gMhd/X4rcDG8PTgaaCQM5rXEJOx6g=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "011567f35433879aae5024fc6ec53f2a0568a6c4",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-23.05",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"mozilla-overlay": "mozilla-overlay",
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

77
flake.nix Normal file
View File

@ -0,0 +1,77 @@
{
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; };
outputs = { self, nixpkgs, mozilla-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=";
};
targets = [
"thumbv7em-none-eabihf"
];
rustChannelOfTargets = _channel: _date: targets:
(pkgs.lib.rustLib.fromManifestFile rustManifest {
inherit (pkgs) stdenv lib fetchurl patchelf;
}).rust.override {
inherit targets;
extensions = ["rust-src"];
};
rust = rustChannelOfTargets "stable" null targets;
rustPlatform = pkgs.recurseIntoAttrs (pkgs.makeRustPlatform {
rustc = rust;
cargo = rust;
});
thermostat = rustPlatform.buildRustPackage {
name = "thermostat";
version = "0.0.0";
src = self;
cargoLock = {
lockFile = ./Cargo.lock;
outputHashes = {
"stm32-eth-0.2.0" = "sha256-48RpZgagUqgVeKm7GXdk3Oo0v19ScF9Uby0nTFlve2o=";
};
};
nativeBuildInputs = [ pkgs.llvm ];
buildPhase = ''
cargo build --release --bin thermostat
'';
installPhase = ''
mkdir -p $out $out/nix-support
cp target/thumbv7em-none-eabihf/release/thermostat $out/thermostat.elf
echo file binary-dist $out/thermostat.elf >> $out/nix-support/hydra-build-products
llvm-objcopy -O binary target/thumbv7em-none-eabihf/release/thermostat $out/thermostat.bin
echo file binary-dist $out/thermostat.bin >> $out/nix-support/hydra-build-products
'';
dontFixup = true;
};
in {
packages.x86_64-linux = {
inherit thermostat;
};
hydraJobs = {
inherit thermostat;
};
devShell.x86_64-linux = pkgs.mkShell {
name = "thermostat-dev-shell";
buildInputs = with pkgs; [
rust openocd dfu-util
] ++ (with python3Packages; [
numpy matplotlib
]);
};
defaultPackage.x86_64-linux = thermostat;
};
}

View File

@ -1,10 +1,17 @@
MEMORY
{
FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 2048K
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 112K
FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 1024K
/* reserved for config data */
CONFIG (rx) : ORIGIN = 0x8100000, LENGTH = 16K
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 112K - 4
/* reserved for DFU trigger message */
DFU_MSG (wrx) : ORIGIN = 0x2001BFFC, LENGTH = 4
RAM2 (xrw) : ORIGIN = 0x2001C000, LENGTH = 16K
RAM3 (xrw) : ORIGIN = 0x20020000, LENGTH = 64K
CCMRAM (rw) : ORIGIN = 0x10000000, LENGTH = 64K
}
_flash_start = ORIGIN(FLASH);
_config_start = ORIGIN(CONFIG);
_dfu_msg = ORIGIN(DFU_MSG);
_stack_start = ORIGIN(CCMRAM) + LENGTH(CCMRAM);

262
pytec/autotune.py Normal file
View File

@ -0,0 +1,262 @@
import math
import logging
from collections import deque, namedtuple
from enum import Enum
from pytec.client import Client
# Based on hirshmann pid-autotune libiary
# See https://github.com/hirschmann/pid-autotune
# Which is in turn based on a fork of Arduino PID AutoTune Library
# See https://github.com/t0mpr1c3/Arduino-PID-AutoTune-Library
class PIDAutotuneState(Enum):
STATE_OFF = 'off'
STATE_RELAY_STEP_UP = 'relay step up'
STATE_RELAY_STEP_DOWN = 'relay step down'
STATE_SUCCEEDED = 'succeeded'
STATE_FAILED = 'failed'
class PIDAutotune:
PIDParams = namedtuple('PIDParams', ['Kp', 'Ki', 'Kd'])
PEAK_AMPLITUDE_TOLERANCE = 0.05
_tuning_rules = {
"ziegler-nichols": [0.6, 1.2, 0.075],
"tyreus-luyben": [0.4545, 0.2066, 0.07214],
"ciancone-marlin": [0.303, 0.1364, 0.0481],
"pessen-integral": [0.7, 1.75, 0.105],
"some-overshoot": [0.333, 0.667, 0.111],
"no-overshoot": [0.2, 0.4, 0.0667]
}
def __init__(self, setpoint, out_step=10, lookback=60,
noiseband=0.5, sampletime=1.2):
if setpoint is None:
raise ValueError('setpoint must be specified')
self._inputs = deque(maxlen=round(lookback / sampletime))
self._setpoint = setpoint
self._outputstep = out_step
self._noiseband = noiseband
self._out_min = -out_step
self._out_max = out_step
self._state = PIDAutotuneState.STATE_OFF
self._peak_timestamps = deque(maxlen=5)
self._peaks = deque(maxlen=5)
self._output = 0
self._last_run_timestamp = 0
self._peak_type = 0
self._peak_count = 0
self._initial_output = 0
self._induced_amplitude = 0
self._Ku = 0
self._Pu = 0
def state(self):
"""Get the current state."""
return self._state
def output(self):
"""Get the last output value."""
return self._output
def tuning_rules(self):
"""Get a list of all available tuning rules."""
return self._tuning_rules.keys()
def get_pid_parameters(self, tuning_rule='ziegler-nichols'):
"""Get PID parameters.
Args:
tuning_rule (str): Sets the rule which should be used to calculate
the parameters.
"""
divisors = self._tuning_rules[tuning_rule]
kp = self._Ku * divisors[0]
ki = divisors[1] * self._Ku / self._Pu
kd = divisors[2] * self._Ku * self._Pu
return PIDAutotune.PIDParams(kp, ki, kd)
def run(self, input_val, time_input):
"""To autotune a system, this method must be called periodically.
Args:
input_val (float): The temperature input value.
time_input (float): Current time in seconds.
Returns:
`true` if tuning is finished, otherwise `false`.
"""
now = time_input * 1000
if (self._state == PIDAutotuneState.STATE_OFF
or self._state == PIDAutotuneState.STATE_SUCCEEDED
or self._state == PIDAutotuneState.STATE_FAILED):
self._state = PIDAutotuneState.STATE_RELAY_STEP_UP
self._last_run_timestamp = now
# check input and change relay state if necessary
if (self._state == PIDAutotuneState.STATE_RELAY_STEP_UP
and input_val > self._setpoint + self._noiseband):
self._state = PIDAutotuneState.STATE_RELAY_STEP_DOWN
logging.debug('switched state: {0}'.format(self._state))
logging.debug('input: {0}'.format(input_val))
elif (self._state == PIDAutotuneState.STATE_RELAY_STEP_DOWN
and input_val < self._setpoint - self._noiseband):
self._state = PIDAutotuneState.STATE_RELAY_STEP_UP
logging.debug('switched state: {0}'.format(self._state))
logging.debug('input: {0}'.format(input_val))
# set output
if (self._state == PIDAutotuneState.STATE_RELAY_STEP_UP):
self._output = self._initial_output - self._outputstep
elif self._state == PIDAutotuneState.STATE_RELAY_STEP_DOWN:
self._output = self._initial_output + self._outputstep
# respect output limits
self._output = min(self._output, self._out_max)
self._output = max(self._output, self._out_min)
# identify peaks
is_max = True
is_min = True
for val in self._inputs:
is_max = is_max and (input_val >= val)
is_min = is_min and (input_val <= val)
self._inputs.append(input_val)
# we don't trust the maxes or mins until the input array is full
if len(self._inputs) < self._inputs.maxlen:
return False
# increment peak count and record peak time for maxima and minima
inflection = False
# peak types:
# -1: minimum
# +1: maximum
if is_max:
if self._peak_type == -1:
inflection = True
self._peak_type = 1
elif is_min:
if self._peak_type == 1:
inflection = True
self._peak_type = -1
# update peak times and values
if inflection:
self._peak_count += 1
self._peaks.append(input_val)
self._peak_timestamps.append(now)
logging.debug('found peak: {0}'.format(input_val))
logging.debug('peak count: {0}'.format(self._peak_count))
# check for convergence of induced oscillation
# convergence of amplitude assessed on last 4 peaks (1.5 cycles)
self._induced_amplitude = 0
if inflection and (self._peak_count > 4):
abs_max = self._peaks[-2]
abs_min = self._peaks[-2]
for i in range(0, len(self._peaks) - 2):
self._induced_amplitude += abs(self._peaks[i]
- self._peaks[i+1])
abs_max = max(self._peaks[i], abs_max)
abs_min = min(self._peaks[i], abs_min)
self._induced_amplitude /= 6.0
# check convergence criterion for amplitude of induced oscillation
amplitude_dev = ((0.5 * (abs_max - abs_min)
- self._induced_amplitude)
/ self._induced_amplitude)
logging.debug('amplitude: {0}'.format(self._induced_amplitude))
logging.debug('amplitude deviation: {0}'.format(amplitude_dev))
if amplitude_dev < PIDAutotune.PEAK_AMPLITUDE_TOLERANCE:
self._state = PIDAutotuneState.STATE_SUCCEEDED
# if the autotune has not already converged
# terminate after 10 cycles
if self._peak_count >= 20:
self._output = 0
self._state = PIDAutotuneState.STATE_FAILED
return True
if self._state == PIDAutotuneState.STATE_SUCCEEDED:
self._output = 0
logging.debug('peak finding successful')
# calculate ultimate gain
self._Ku = 4.0 * self._outputstep / \
(self._induced_amplitude * math.pi)
print('Ku: {0}'.format(self._Ku))
# calculate ultimate period in seconds
period1 = self._peak_timestamps[3] - self._peak_timestamps[1]
period2 = self._peak_timestamps[4] - self._peak_timestamps[2]
self._Pu = 0.5 * (period1 + period2) / 1000.0
print('Pu: {0}'.format(self._Pu))
for rule in self._tuning_rules:
params = self.get_pid_parameters(rule)
print('rule: {0}'.format(rule))
print('Kp: {0}'.format(params.Kp))
print('Ki: {0}'.format(params.Ki))
print('Kd: {0}'.format(params.Kd))
return True
return False
def main():
# Auto tune parameters
# Thermostat channel
channel = 0
# Target temperature of the autotune routine, celcius
target_temperature = 20
# Value by which output will be increased/decreased from zero, amps
output_step = 1
# Reference period for local minima/maxima, seconds
lookback = 3
# Determines by how much the input value must
# overshoot/undershoot the setpoint, celcius
noiseband = 1.5
# logging.basicConfig(level=logging.DEBUG)
tec = Client()
data = next(tec.report_mode())
ch = data[channel]
tuner = PIDAutotune(target_temperature, output_step,
lookback, noiseband, ch['interval'])
for data in tec.report_mode():
ch = data[channel]
temperature = ch['temperature']
if (tuner.run(temperature, ch['time'])):
break
tuner_out = tuner.output()
tec.set_param("pwm", channel, "i_set", tuner_out)
tec.set_param("pwm", channel, "i_set", 0)
if __name__ == "__main__":
main()

View File

@ -2,7 +2,10 @@ from pytec.client import Client
tec = Client() #(host="localhost", port=6667)
tec.set_param("s-h", 1, "t0", 20)
print(tec.get_pwm())
print(tec.get_pid())
print(tec.get_pwm())
print(tec.get_postfilter())
print(tec.get_steinhart_hart())
for data in tec.report_mode():
print(data)

View File

@ -28,34 +28,36 @@ class Series:
self.y_data = self.y_data[drop:]
series = {
'adc': Series(),
'sens': Series(lambda x: x * 0.0001),
'temperature': Series(lambda t: t - target_temperature),
'i_set': Series(),
# 'adc': Series(),
# 'sens': Series(lambda x: x * 0.0001),
'temperature': Series(),
# 'i_set': Series(),
'pid_output': Series(),
'vref': Series(),
'dac_value': Series(),
'dac_feedback': Series(),
'i_tec': Series(),
# 'vref': Series(),
# 'dac_value': Series(),
# 'dac_feedback': Series(),
# 'i_tec': Series(),
'tec_i': Series(),
'tec_u_meas': Series(),
# 'interval': Series(),
}
series_lock = Lock()
quit = False
def recv_data(tec):
global last_packet_time
for data in tec.report_mode():
if data['channel'] == 0:
series_lock.acquire()
try:
time = data['time'] / 1000.0
for k, s in series.iteritems():
v = data[k]
if data.has_key(k) and type(v) is float:
s.append(time, v)
finally:
series_lock.release()
ch0 = data[0]
series_lock.acquire()
try:
for k, s in series.items():
if k in ch0:
v = ch0[k]
if type(v) is float:
s.append(ch0['time'], v)
finally:
series_lock.release()
if quit:
break
@ -65,7 +67,7 @@ thread.start()
fig, ax = plt.subplots()
for k, s in series.iteritems():
for k, s in series.items():
s.plot, = ax.plot([], [], label=k)
legend = ax.legend()
@ -74,7 +76,7 @@ def animate(i):
series_lock.acquire()
try:
for k, s in series.iteritems():
for k, s in series.items():
s.plot.set_data(s.x_data, s.y_data)
if len(s.y_data) > 0:
s.plot.set_label("{}: {:.3f}".format(k, s.y_data[-1]))
@ -102,15 +104,17 @@ def animate(i):
else:
max_y = max(max_y, max_y_)
if min_x is not None and max_x - TIME_WINDOW > min_x:
for s in series.itervalues():
if min_x and max_x - TIME_WINDOW > min_x:
for s in series.values():
s.clip(max_x - TIME_WINDOW)
finally:
series_lock.release()
margin_y = 0.01 * (max_y - min_y)
ax.set_xlim(min_x, max_x)
ax.set_ylim(min_y - margin_y, max_y + margin_y)
if min_x != max_x:
ax.set_xlim(min_x, max_x)
if min_y != max_y:
margin_y = 0.01 * (max_y - min_y)
ax.set_ylim(min_y - margin_y, max_y + margin_y)
global legend
legend.remove()

View File

@ -1,16 +1,23 @@
import socket
import json
import logging
CHANNELS = 2
class CommandError(Exception):
pass
class Client:
def __init__(self, host="192.168.1.26", port=23, timeout=None):
self._socket = socket.create_connection((host, port), timeout)
self._lines = [""]
self._check_zero_limits()
def _check_zero_limits(self):
pwm_report = self.get_pwm()
for pwm_channel in pwm_report:
for limit in ["max_i_neg", "max_i_pos", "max_v"]:
if pwm_channel[limit]["value"] == 0.0:
logging.warning("`{}` limit is set to zero on channel {}".format(limit, pwm_channel["channel"]))
def _command(self, *command):
self._socket.sendall((" ".join(command) + "\n").encode('utf-8'))
def _read_line(self):
# read more lines
while len(self._lines) <= 1:
@ -24,13 +31,19 @@ class Client:
self._lines = self._lines[1:]
return line
def _command(self, *command):
self._socket.sendall((" ".join(command) + "\n").encode('utf-8'))
line = self._read_line()
response = json.loads(line)
if "error" in response:
raise CommandError(response["error"])
return response
def _get_conf(self, topic):
self._command(topic)
result = []
for channel in range(0, CHANNELS):
line = self._read_line()
conf = json.loads(line)
result.append(conf)
result = [None, None]
for item in self._command(topic):
result[int(item["channel"])] = item
return result
def get_pwm(self):
@ -63,22 +76,16 @@ class Client:
'ki': 0.02,
'kd': 0.0,
'output_min': 0.0,
'output_max': 3.0,
'integral_min': -100.0,
'integral_max': 100.0},
'target': 37.0,
'integral': 38.41138597026372},
'output_max': 3.0},
'target': 37.0},
{'channel': 1,
'parameters': {
'kp': 10.0,
'ki': 0.02,
'kd': 0.0,
'output_min': 0.0,
'output_max': 3.0,
'integral_min': -100.0,
'integral_max': 100.0},
'target': 36.5,
'integral': nan}]
'output_max': 3.0},
'target': 36.5}]
"""
return self._get_conf("pid")
@ -160,4 +167,3 @@ class Client:
def load_config(self):
"""Load current configuration from EEPROM"""
self._command("load")

View File

@ -1,4 +1,8 @@
use stm32f4xx_hal::hal::digital::v2::OutputPin;
use uom::si::{
f64::ElectricPotential,
electric_potential::volt,
};
use crate::{
ad5680,
ad7172,
@ -12,13 +16,12 @@ pub struct Channel0;
/// Marker type for the second channel
pub struct Channel1;
pub struct Channel<C: ChannelPins> {
pub state: ChannelState,
/// for `i_set`
pub dac: ad5680::Dac<C::DacSpi, C::DacSync>,
/// 1 / Volts
pub dac_factor: f64,
/// Measured vref of MAX driver chip
pub vref_meas: ElectricPotential,
pub shdn: C::Shdn,
pub vref_pin: C::VRefPin,
pub itec_pin: C::ItecPin,
@ -32,12 +35,12 @@ impl<C: ChannelPins> Channel<C> {
let state = ChannelState::new(adc_calibration);
let mut dac = ad5680::Dac::new(pins.dac_spi, pins.dac_sync);
let _ = dac.set(0);
// sensible dummy preset. calibrate_i_set() must be used.
let dac_factor = ad5680::MAX_VALUE as f64 / 5.0;
// sensible dummy preset taken from datasheet. calibrate_dac_value() should be used to override this value.
let vref_meas = ElectricPotential::new::<volt>(1.5);
Channel {
state,
dac, dac_factor,
dac, vref_meas,
shdn: pins.shdn,
vref_pin: pins.vref_pin,
itec_pin: pins.itec_pin,

View File

@ -1,13 +1,17 @@
use smoltcp::time::Instant;
use smoltcp::time::{Duration, Instant};
use uom::si::{
f64::{
ElectricPotential,
ElectricCurrent,
ElectricalResistance,
ThermodynamicTemperature,
Time,
},
electric_potential::volt,
electric_current::ampere,
electrical_resistance::ohm,
thermodynamic_temperature::degree_celsius,
time::millisecond,
};
use crate::{
ad7172,
@ -23,11 +27,11 @@ pub struct ChannelState {
pub adc_data: Option<u32>,
pub adc_calibration: ad7172::ChannelCalibration,
pub adc_time: Instant,
/// VREF for the TEC (1.5V)
pub vref: ElectricPotential,
pub adc_interval: Duration,
/// 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,
@ -39,10 +43,11 @@ impl ChannelState {
adc_data: None,
adc_calibration,
adc_time: Instant::from_secs(0),
// updated later with Channels.read_vref()
vref: ElectricPotential::new::<volt>(1.5),
// default: 10 Hz
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(),
@ -56,6 +61,7 @@ impl ChannelState {
} else {
Some(adc_data)
};
self.adc_interval = now - self.adc_time;
self.adc_time = now;
}
@ -67,6 +73,14 @@ impl ChannelState {
Some(pid_output)
}
pub fn get_adc_time(&self) -> Time {
Time::new::<millisecond>(self.adc_time.total_millis() as f64)
}
pub fn get_adc_interval(&self) -> Time {
Time::new::<millisecond>(self.adc_interval.total_millis() as f64)
}
pub fn get_adc(&self) -> Option<ElectricPotential> {
Some(self.adc_calibration.convert_data(self.adc_data?))
}

View File

@ -1,8 +1,10 @@
use core::cmp::max_by;
use heapless::{consts::U2, Vec};
use serde::{Serialize, Serializer};
use smoltcp::time::Instant;
use stm32f4xx_hal::hal;
use uom::si::{
f64::{ElectricCurrent, ElectricPotential, ElectricalResistance},
f64::{ElectricCurrent, ElectricPotential, ElectricalResistance, Time},
electric_potential::{millivolt, volt},
electric_current::ampere,
electrical_resistance::ohm,
@ -15,13 +17,27 @@ use crate::{
channel::{Channel, Channel0, Channel1},
channel_state::ChannelState,
command_parser::{CenterPoint, PwmPin},
pins,
command_handler::JsonBuffer,
pins::{self, Channel0VRef, Channel1VRef},
steinhart_hart,
};
pub enum PinsAdcReadTarget {
VREF,
DacVfb,
ITec,
VTec,
}
pub const CHANNELS: usize = 2;
pub const R_SENSE: f64 = 0.05;
// as stated in the MAX1968 datasheet
pub const MAX_TEC_I: f64 = 3.0;
// 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;
// TODO: -pub
pub struct Channels {
channel0: Channel<Channel0>,
@ -39,10 +55,10 @@ impl Channels {
adc.set_sync_enable(false).unwrap();
// Setup channels and start ADC
adc.setup_channel(0, ad7172::Input::Ain0, ad7172::Input::Ain1).unwrap();
adc.setup_channel(0, ad7172::Input::Ain2, ad7172::Input::Ain3).unwrap();
let adc_calibration0 = adc.get_calibration(0)
.expect("adc_calibration0");
adc.setup_channel(1, ad7172::Input::Ain2, ad7172::Input::Ain3).unwrap();
adc.setup_channel(1, ad7172::Input::Ain0, ad7172::Input::Ain1).unwrap();
let adc_calibration1 = adc.get_calibration(1)
.expect("adc_calibration1");
adc.start_continuous_conversion().unwrap();
@ -53,7 +69,6 @@ impl Channels {
let pwm = pins.pwm;
let mut channels = Channels { channel0, channel1, adc, pins_adc, pwm };
for channel in 0..CHANNELS {
channels.channel_state(channel).vref = channels.read_vref(channel);
channels.calibrate_dac_value(channel);
channels.set_i(channel, ElectricCurrent::new::<ampere>(0.0));
}
@ -72,7 +87,6 @@ impl Channels {
pub fn poll_adc(&mut self, instant: Instant) -> Option<u8> {
self.adc.data_ready().unwrap().map(|channel| {
let data = self.adc.read_data().unwrap();
let state = self.channel_state(channel);
state.update(instant, data);
match state.update_pid() {
@ -94,92 +108,162 @@ impl Channels {
/// calculate the TEC i_set centerpoint
pub fn get_center(&mut self, channel: usize) -> ElectricPotential {
match self.channel_state(channel).center {
CenterPoint::Vref => {
let vref = self.read_vref(channel);
self.channel_state(channel).vref = vref;
vref
},
CenterPoint::Vref =>
self.adc_read(channel, PinsAdcReadTarget::VREF, 8),
CenterPoint::Override(center_point) =>
ElectricPotential::new::<volt>(center_point.into()),
}
}
/// i_set DAC
fn get_dac(&mut self, channel: usize) -> (ElectricPotential, ElectricPotential) {
let dac_factor = match channel.into() {
0 => self.channel0.dac_factor,
1 => self.channel1.dac_factor,
_ => unreachable!(),
};
fn get_dac(&mut self, channel: usize) -> ElectricPotential {
let voltage = self.channel_state(channel).dac_value;
let max = ElectricPotential::new::<volt>(ad5680::MAX_VALUE as f64 / dac_factor);
(voltage, max)
voltage
}
pub fn get_i(&mut self, channel: usize) -> (ElectricCurrent, ElectricCurrent) {
let center_point = self.get_center(channel);
let r_sense = ElectricalResistance::new::<ohm>(R_SENSE);
let (voltage, max) = self.get_dac(channel);
let i_tec = (voltage - center_point) / (10.0 * r_sense);
let max = (max - center_point) / (10.0 * r_sense);
(i_tec, max)
pub fn get_i(&mut self, channel: usize) -> ElectricCurrent {
let i_set = self.channel_state(channel).i_set;
i_set
}
/// i_set DAC
fn set_dac(&mut self, channel: usize, voltage: ElectricPotential) -> (ElectricPotential, ElectricPotential) {
let dac_factor = match channel.into() {
0 => self.channel0.dac_factor,
1 => self.channel1.dac_factor,
_ => unreachable!(),
};
let value = (voltage.get::<volt>() * dac_factor) as u32;
let value = match channel {
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 ;
match channel {
0 => self.channel0.dac.set(value).unwrap(),
1 => self.channel1.dac.set(value).unwrap(),
_ => unreachable!(),
};
let voltage = ElectricPotential::new::<volt>(value as f64 / dac_factor);
self.channel_state(channel).dac_value = voltage;
let max = ElectricPotential::new::<volt>(ad5680::MAX_VALUE as f64 / dac_factor);
(voltage, max)
voltage
}
pub fn set_i(&mut self, channel: usize, i_tec: ElectricCurrent) -> (ElectricCurrent, ElectricCurrent) {
let center_point = self.get_center(channel);
pub fn set_i(&mut self, channel: usize, i_set: ElectricCurrent) -> ElectricCurrent {
// Silently clamp i_set
let i_ceiling = ElectricCurrent::new::<ampere>(MAX_TEC_I);
let i_floor = ElectricCurrent::new::<ampere>(-MAX_TEC_I);
let i_set = i_set.min(i_ceiling).max(i_floor);
let vref_meas = match channel.into() {
0 => self.channel0.vref_meas,
1 => self.channel1.vref_meas,
_ => unreachable!(),
};
let center_point = vref_meas;
let r_sense = ElectricalResistance::new::<ohm>(R_SENSE);
let voltage = i_tec * 10.0 * r_sense + center_point;
let (voltage, max) = self.set_dac(channel, voltage);
let i_tec = (voltage - center_point) / (10.0 * r_sense);
let max = (max - center_point) / (10.0 * r_sense);
(i_tec, max)
let voltage = i_set * 10.0 * r_sense + center_point;
let voltage = self.set_dac(channel, voltage);
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(
&self.channel0.dac_feedback_pin,
stm32f4xx_hal::adc::config::SampleTime::Cycles_480
);
let mv = self.pins_adc.sample_to_millivolts(sample);
sample = match adc_read_target {
PinsAdcReadTarget::VREF => {
match &self.channel0.vref_pin {
Channel0VRef::Analog(vref_pin) => {
for _ in (0..avg_pt).rev() {
sample += self
.pins_adc
.convert(vref_pin, stm32f4xx_hal::adc::config::SampleTime::Cycles_480)
as u32;
}
sample / avg_pt as u32
},
Channel0VRef::Disabled(_) => {2048 as u32}
}
}
PinsAdcReadTarget::DacVfb => {
for _ in (0..avg_pt).rev() {
sample += self
.pins_adc
.convert(&self.channel0.dac_feedback_pin,stm32f4xx_hal::adc::config::SampleTime::Cycles_480)
as u32;
}
sample / avg_pt as u32
}
PinsAdcReadTarget::ITec => {
for _ in (0..avg_pt).rev() {
sample += self
.pins_adc
.convert(&self.channel0.itec_pin, stm32f4xx_hal::adc::config::SampleTime::Cycles_480)
as u32;
}
sample / avg_pt as u32
}
PinsAdcReadTarget::VTec => {
for _ in (0..avg_pt).rev() {
sample += self
.pins_adc
.convert(&self.channel0.tec_u_meas_pin, stm32f4xx_hal::adc::config::SampleTime::Cycles_480)
as u32;
}
sample / avg_pt as u32
}
};
let mv = self.pins_adc.sample_to_millivolts(sample as u16);
ElectricPotential::new::<millivolt>(mv as f64)
}
1 => {
let sample = self.pins_adc.convert(
&self.channel1.dac_feedback_pin,
stm32f4xx_hal::adc::config::SampleTime::Cycles_480
);
let mv = self.pins_adc.sample_to_millivolts(sample);
sample = match adc_read_target {
PinsAdcReadTarget::VREF => {
match &self.channel1.vref_pin {
Channel1VRef::Analog(vref_pin) => {
for _ in (0..avg_pt).rev() {
sample += self
.pins_adc
.convert(vref_pin, stm32f4xx_hal::adc::config::SampleTime::Cycles_480)
as u32;
}
sample / avg_pt as u32
},
Channel1VRef::Disabled(_) => {2048 as u32}
}
}
PinsAdcReadTarget::DacVfb => {
for _ in (0..avg_pt).rev() {
sample += self
.pins_adc
.convert(&self.channel1.dac_feedback_pin, stm32f4xx_hal::adc::config::SampleTime::Cycles_480)
as u32;
}
sample / avg_pt as u32
}
PinsAdcReadTarget::ITec => {
for _ in (0..avg_pt).rev() {
sample += self
.pins_adc
.convert(&self.channel1.itec_pin, stm32f4xx_hal::adc::config::SampleTime::Cycles_480)
as u32;
}
sample / avg_pt as u32
}
PinsAdcReadTarget::VTec => {
for _ in (0..avg_pt).rev() {
sample += self
.pins_adc
.convert(&self.channel1.tec_u_meas_pin, stm32f4xx_hal::adc::config::SampleTime::Cycles_480)
as u32;
}
sample / avg_pt as u32
}
};
let mv = self.pins_adc.sample_to_millivolts(sample as u16);
ElectricPotential::new::<millivolt>(mv as f64)
}
_ => unreachable!(),
_ => unreachable!()
}
}
pub fn read_dac_feedback_until_stable(&mut self, 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;
}
@ -187,79 +271,29 @@ impl Channels {
}
}
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!(),
}
}
/// Calibrate the I_SET DAC using the DAC_FB ADC pin.
/// Calibrates the DAC output to match vref of the MAX driver to reduce zero-current offset of the MAX driver output.
///
/// These loops perform a breadth-first search for the DAC setting
/// that will produce a `target_voltage`.
/// The thermostat DAC applies a control voltage signal to the CTLI pin of MAX driver chip to control its output current.
/// The CTLI input signal is centered around VREF of the MAX chip. Applying VREF to CTLI sets the output current to 0.
///
/// This calibration routine measures the VREF voltage and the DAC output with the STM32 ADC, and uses a breadth-first
/// search to find the DAC setting that will produce a DAC output voltage closest to VREF. This DAC output voltage will
/// be stored and used in subsequent i_set routines to bias the current control signal to the measured VREF, reducing
/// the offset error of the current control signal.
///
/// The input offset of the STM32 ADC is eliminated by using the same ADC for the measurements, and by only using the
/// difference in VREF and DAC output for the calibration.
///
/// This routine should be called only once after boot, repeated reading of the vref signal and changing of the stored
/// VREF measurement can introduce significant noise at the current output, degrading the stabilily performance of the
/// thermostat.
pub fn calibrate_dac_value(&mut self, channel: usize) {
let target_voltage = ElectricPotential::new::<volt>(2.5);
let samples = 50;
let mut target_voltage = ElectricPotential::new::<volt>(0.0);
for _ in 0..samples {
target_voltage = target_voltage + self.get_center(channel);
}
target_voltage = target_voltage / samples as f64;
let mut start_value = 1;
let mut best_error = ElectricPotential::new::<volt>(100.0);
@ -284,10 +318,10 @@ impl Channels {
best_error = error;
start_value = prev_value;
let dac_factor = value as f64 / dac_feedback.get::<volt>();
let vref = (value as f64 / ad5680::MAX_VALUE as f64) * ElectricPotential::new::<volt>(DAC_OUT_V_MAX);
match channel {
0 => self.channel0.dac_factor = dac_factor,
1 => self.channel1.dac_factor = dac_factor,
0 => self.channel0.vref_meas = vref,
1 => self.channel1.vref_meas = vref,
_ => unreachable!(),
}
}
@ -344,11 +378,10 @@ impl Channels {
}
}
pub fn get_max_v(&mut self, channel: usize) -> (ElectricPotential, ElectricPotential) {
let vref = self.channel_state(channel).vref;
let max = 4.0 * vref;
pub fn get_max_v(&mut self, channel: usize) -> ElectricPotential {
let max = 4.0 * ElectricPotential::new::<volt>(3.3);
let duty = self.get_pwm(channel, PwmPin::MaxV);
(duty * max, max)
duty * max
}
pub fn get_max_i_pos(&mut self, channel: usize) -> (ElectricCurrent, ElectricCurrent) {
@ -363,6 +396,16 @@ impl Channels {
(duty * max, max)
}
// Get current passing through TEC
pub fn get_tec_i(&mut self, channel: usize) -> ElectricCurrent {
(self.adc_read(channel, PinsAdcReadTarget::ITec, 16) - self.adc_read(channel, PinsAdcReadTarget::VREF, 16)) / ElectricalResistance::new::<ohm>(0.4)
}
// Get voltage across TEC
pub fn get_tec_v(&mut self, channel: usize) -> ElectricPotential {
(self.adc_read(channel, PinsAdcReadTarget::VTec, 16) - ElectricPotential::new::<volt>(1.5)) * 4.0
}
fn set_pwm(&mut self, channel: usize, pin: PwmPin, duty: f64) -> f64 {
fn set<P: hal::PwmPin<Duty=u16>>(pin: &mut P, duty: f64) -> f64 {
let max = pin.get_max_duty();
@ -391,8 +434,7 @@ impl Channels {
}
pub fn set_max_v(&mut self, channel: usize, max_v: ElectricPotential) -> (ElectricPotential, ElectricPotential) {
let vref = self.channel_state(channel).vref;
let max = 4.0 * vref;
let max = 4.0 * ElectricPotential::new::<volt>(3.3);
let duty = (max_v / max).get::<ratio>();
let duty = self.set_pwm(channel, PwmPin::MaxV, duty);
(duty * max, max)
@ -412,82 +454,126 @@ impl Channels {
(duty * max, max)
}
pub fn report(&mut self, channel: usize) -> Report {
let vref = self.channel_state(channel).vref;
let (i_set, _) = self.get_i(channel);
let i_tec = self.read_itec(channel);
let tec_i = (i_tec - vref) / ElectricalResistance::new::<ohm>(0.4);
let (dac_value, _) = self.get_dac(channel);
fn report(&mut self, channel: usize) -> Report {
let i_set = self.get_i(channel);
let i_tec = self.adc_read(channel, PinsAdcReadTarget::ITec, 16);
let tec_i = self.get_tec_i(channel);
let dac_value = self.get_dac(channel);
let state = self.channel_state(channel);
let pid_output = state.pid.last_output.map(|last_output|
ElectricCurrent::new::<ampere>(last_output)
);
let pid_output = ElectricCurrent::new::<ampere>(state.pid.y1);
Report {
channel,
time: state.adc_time.total_millis(),
time: state.get_adc_time(),
interval: state.get_adc_interval(),
adc: state.get_adc(),
sens: state.get_sens(),
temperature: state.get_temperature()
.map(|temperature| temperature.get::<degree_celsius>()),
pid_engaged: state.pid_engaged,
i_set,
vref,
dac_value,
dac_feedback: self.read_dac_feedback(channel),
dac_feedback: self.adc_read(channel, PinsAdcReadTarget::DacVfb, 1),
i_tec,
tec_i,
tec_u_meas: self.read_tec_u_meas(channel),
tec_u_meas: self.get_tec_v(channel),
pid_output,
}
}
pub fn pwm_summary(&mut self, channel: usize) -> PwmSummary {
pub fn reports_json(&mut self) -> Result<JsonBuffer, serde_json_core::ser::Error> {
let mut reports = Vec::<_, U2>::new();
for channel in 0..CHANNELS {
let _ = reports.push(self.report(channel));
}
serde_json_core::to_vec(&reports)
}
pub fn pid_summaries_json(&mut self) -> Result<JsonBuffer, serde_json_core::ser::Error> {
let mut summaries = Vec::<_, U2>::new();
for channel in 0..CHANNELS {
let _ = summaries.push(self.channel_state(channel).pid.summary(channel));
}
serde_json_core::to_vec(&summaries)
}
pub fn pid_engaged(&mut self) -> bool {
for channel in 0..CHANNELS {
if self.channel_state(channel).pid_engaged {
return true;
}
}
false
}
fn pwm_summary(&mut self, channel: usize) -> PwmSummary {
PwmSummary {
channel,
center: CenterPointJson(self.channel_state(channel).center.clone()),
i_set: self.get_i(channel).into(),
max_v: self.get_max_v(channel).into(),
i_set: (self.get_i(channel), ElectricCurrent::new::<ampere>(3.0)).into(),
max_v: (self.get_max_v(channel), ElectricPotential::new::<volt>(5.0)).into(),
max_i_pos: self.get_max_i_pos(channel).into(),
max_i_neg: self.get_max_i_neg(channel).into(),
}
}
pub fn postfilter_summary(&mut self, channel: usize) -> PostFilterSummary {
pub fn pwm_summaries_json(&mut self) -> Result<JsonBuffer, serde_json_core::ser::Error> {
let mut summaries = Vec::<_, U2>::new();
for channel in 0..CHANNELS {
let _ = summaries.push(self.pwm_summary(channel));
}
serde_json_core::to_vec(&summaries)
}
fn postfilter_summary(&mut self, channel: usize) -> PostFilterSummary {
let rate = self.adc.get_postfilter(channel as u8).unwrap()
.and_then(|filter| filter.output_rate());
PostFilterSummary { channel, rate }
}
pub fn steinhart_hart_summary(&mut self, channel: usize) -> SteinhartHartSummary {
pub fn postfilter_summaries_json(&mut self) -> Result<JsonBuffer, serde_json_core::ser::Error> {
let mut summaries = Vec::<_, U2>::new();
for channel in 0..CHANNELS {
let _ = summaries.push(self.postfilter_summary(channel));
}
serde_json_core::to_vec(&summaries)
}
fn steinhart_hart_summary(&mut self, channel: usize) -> SteinhartHartSummary {
let params = self.channel_state(channel).sh.clone();
SteinhartHartSummary { channel, params }
}
}
type JsonBuffer = heapless::Vec<u8, heapless::consts::U512>;
pub fn steinhart_hart_summaries_json(&mut self) -> Result<JsonBuffer, serde_json_core::ser::Error> {
let mut summaries = Vec::<_, U2>::new();
for channel in 0..CHANNELS {
let _ = summaries.push(self.steinhart_hart_summary(channel));
}
serde_json_core::to_vec(&summaries)
}
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))
}
}
#[derive(Serialize)]
pub struct Report {
channel: usize,
time: i64,
time: Time,
interval: Time,
adc: Option<ElectricPotential>,
sens: Option<ElectricalResistance>,
temperature: Option<f64>,
pid_engaged: bool,
i_set: ElectricCurrent,
vref: ElectricPotential,
dac_value: ElectricPotential,
dac_feedback: ElectricPotential,
i_tec: ElectricPotential,
tec_i: ElectricCurrent,
tec_u_meas: ElectricPotential,
pid_output: Option<ElectricCurrent>,
}
impl Report {
pub fn to_json(&self) -> Result<JsonBuffer, serde_json_core::ser::Error> {
serde_json_core::to_vec(self)
}
pid_output: ElectricCurrent,
}
pub struct CenterPointJson(CenterPoint);
@ -529,91 +615,14 @@ pub struct PwmSummary {
max_i_neg: PwmSummaryField<ElectricCurrent>,
}
impl PwmSummary {
pub fn to_json(&self) -> Result<JsonBuffer, serde_json_core::ser::Error> {
serde_json_core::to_vec(self)
}
}
#[derive(Serialize)]
pub struct PostFilterSummary {
channel: usize,
rate: Option<f32>,
}
impl PostFilterSummary {
pub fn to_json(&self) -> Result<JsonBuffer, serde_json_core::ser::Error> {
serde_json_core::to_vec(self)
}
}
#[derive(Serialize)]
pub struct SteinhartHartSummary {
channel: usize,
params: steinhart_hart::Parameters,
}
impl SteinhartHartSummary {
pub fn to_json(&self) -> Result<JsonBuffer, serde_json_core::ser::Error> {
serde_json_core::to_vec(self)
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn report_to_json() {
// `/ 1.1` results in values with a really long serialization
let report = Report {
channel: 0,
time: 3200,
adc: Some(ElectricPotential::new::<volt>(0.65 / 1.1)),
sens: Some(ElectricalResistance::new::<ohm>(10000.0 / 1.1)),
temperature: Some(30.0 / 1.1),
pid_engaged: false,
i_set: ElectricCurrent::new::<ampere>(0.5 / 1.1),
vref: ElectricPotential::new::<volt>(1.5 / 1.1),
dac_value: ElectricPotential::new::<volt>(2.0 / 1.1),
dac_feedback: ElectricPotential::new::<volt>(2.0 / 1.1),
i_tec: ElectricPotential::new::<volt>(2.0 / 1.1),
tec_i: ElectricCurrent::new::<ampere>(0.2 / 1.1),
tec_u_meas: ElectricPotential::new::<volt>(2.0 / 1.1),
pid_output: Some(ElectricCurrent::new::<ampere>(0.5 / 1.1)),
};
let buf = report.to_json().unwrap();
assert_eq!(buf[0], b'{');
assert_eq!(buf[buf.len() - 1], b'}');
}
#[test]
fn pwm_summary_to_json() {
let value = 1.0 / 1.1;
let max = 5.0 / 1.1;
let pwm_summary = PwmSummary {
channel: 0,
center: CenterPointJson(CenterPoint::Vref),
i_set: PwmSummaryField {
value: ElectricCurrent::new::<ampere>(value),
max: ElectricCurrent::new::<ampere>(max),
},
max_v: PwmSummaryField {
value: ElectricPotential::new::<volt>(value),
max: ElectricPotential::new::<volt>(max),
},
max_i_pos: PwmSummaryField {
value: ElectricCurrent::new::<ampere>(value),
max: ElectricCurrent::new::<ampere>(max),
},
max_i_neg: PwmSummaryField {
value: ElectricCurrent::new::<ampere>(value),
max: ElectricCurrent::new::<ampere>(max),
},
};
let buf = pwm_summary.to_json().unwrap();
assert_eq!(buf[0], b'{');
assert_eq!(buf[buf.len() - 1], b'}');
}
}

446
src/command_handler.rs Normal file
View File

@ -0,0 +1,446 @@
use smoltcp::socket::TcpSocket;
use log::{error, warn};
use core::fmt::Write;
use heapless::{consts::U1024, Vec};
use super::{
net,
command_parser::{
Ipv4Config,
Command,
ShowCommand,
CenterPoint,
PidParameter,
PwmPin,
ShParameter
},
ad7172,
CHANNEL_CONFIG_KEY,
channels::{
Channels,
CHANNELS
},
config::ChannelConfig,
dfu,
flash_store::FlashStore,
session::Session,
FanCtrl,
hw_rev::HWRev,
};
use uom::{
si::{
f64::{
ElectricCurrent,
ElectricPotential,
ElectricalResistance,
ThermodynamicTemperature,
},
electric_current::ampere,
electric_potential::volt,
electrical_resistance::ohm,
thermodynamic_temperature::degree_celsius,
},
};
#[derive(Debug, Clone, PartialEq)]
pub enum Handler {
Handled,
CloseSocket,
NewIPV4(Ipv4Config),
Reset,
}
#[derive(Clone, Debug, PartialEq)]
pub enum Error {
ReportError,
PostFilterRateError,
FlashError
}
pub type JsonBuffer = Vec<u8, U1024>;
fn send_line(socket: &mut TcpSocket, data: &[u8]) -> bool {
let send_free = socket.send_capacity() - socket.send_queue();
if data.len() > send_free + 1 {
// Not enough buffer space, skip report for now,
// instead of sending incomplete line
warn!(
"TCP socket has only {}/{} needed {}",
send_free + 1, socket.send_capacity(), data.len(),
);
} else {
match socket.send_slice(&data) {
Ok(sent) if sent == data.len() => {
let _ = socket.send_slice(b"\n");
// success
return true
}
Ok(sent) =>
warn!("sent only {}/{} bytes", sent, data.len()),
Err(e) =>
error!("error sending line: {:?}", e),
}
}
// not success
false
}
impl Handler {
fn reporting(socket: &mut TcpSocket) -> Result<Handler, Error> {
send_line(socket, b"{}");
Ok(Handler::Handled)
}
fn show_report_mode(socket: &mut TcpSocket, session: &Session) -> Result<Handler, Error> {
let _ = writeln!(socket, "{{ \"report\": {:?} }}", session.reporting());
Ok(Handler::Handled)
}
fn show_report(socket: &mut TcpSocket, channels: &mut Channels) -> Result<Handler, Error> {
match channels.reports_json() {
Ok(buf) => {
send_line(socket, &buf[..]);
}
Err(e) => {
error!("unable to serialize report: {:?}", e);
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
return Err(Error::ReportError);
}
}
Ok(Handler::Handled)
}
fn show_pid(socket: &mut TcpSocket, channels: &mut Channels) -> Result<Handler, Error> {
match channels.pid_summaries_json() {
Ok(buf) => {
send_line(socket, &buf);
}
Err(e) => {
error!("unable to serialize pid summary: {:?}", e);
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
return Err(Error::ReportError);
}
}
Ok(Handler::Handled)
}
fn show_pwm(socket: &mut TcpSocket, channels: &mut Channels) -> Result<Handler, Error> {
match channels.pwm_summaries_json() {
Ok(buf) => {
send_line(socket, &buf);
}
Err(e) => {
error!("unable to serialize pwm summary: {:?}", e);
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
return Err(Error::ReportError);
}
}
Ok(Handler::Handled)
}
fn show_steinhart_hart(socket: &mut TcpSocket, channels: &mut Channels) -> Result<Handler, Error> {
match channels.steinhart_hart_summaries_json() {
Ok(buf) => {
send_line(socket, &buf);
}
Err(e) => {
error!("unable to serialize steinhart-hart summaries: {:?}", e);
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
return Err(Error::ReportError);
}
}
Ok(Handler::Handled)
}
fn show_post_filter (socket: &mut TcpSocket, channels: &mut Channels) -> Result<Handler, Error> {
match channels.postfilter_summaries_json() {
Ok(buf) => {
send_line(socket, &buf);
}
Err(e) => {
error!("unable to serialize postfilter summary: {:?}", e);
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
return Err(Error::ReportError);
}
}
Ok(Handler::Handled)
}
fn show_ipv4 (socket: &mut TcpSocket, ipv4_config: &mut Ipv4Config) -> Result<Handler, Error> {
let (cidr, gateway) = net::split_ipv4_config(ipv4_config.clone());
let _ = write!(socket, "{{\"addr\":\"{}\"", cidr);
gateway.map(|gateway| write!(socket, ",\"gateway\":\"{}\"", gateway));
let _ = writeln!(socket, "}}");
Ok(Handler::Handled)
}
fn engage_pid (socket: &mut TcpSocket, channels: &mut Channels, channel: usize) -> Result<Handler, Error> {
channels.channel_state(channel).pid_engaged = true;
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> {
match pin {
PwmPin::ISet => {
channels.channel_state(channel).pid_engaged = false;
let current = ElectricCurrent::new::<ampere>(value);
channels.set_i(channel, current);
channels.power_up(channel);
}
PwmPin::MaxV => {
let voltage = ElectricPotential::new::<volt>(value);
channels.set_max_v(channel, voltage);
}
PwmPin::MaxIPos => {
let current = ElectricCurrent::new::<ampere>(value);
channels.set_max_i_pos(channel, current);
}
PwmPin::MaxINeg => {
let current = ElectricCurrent::new::<ampere>(value);
channels.set_max_i_neg(channel, current);
}
}
send_line(socket, b"{}");
Ok(Handler::Handled)
}
fn set_center_point(socket: &mut TcpSocket, channels: &mut Channels, channel: usize, center: CenterPoint) -> Result<Handler, Error> {
let i_set = channels.get_i(channel);
let state = channels.channel_state(channel);
state.center = center;
if !state.pid_engaged {
channels.set_i(channel, i_set);
}
send_line(socket, b"{}");
Ok(Handler::Handled)
}
fn set_pid (socket: &mut TcpSocket, channels: &mut Channels, channel: usize, parameter: PidParameter, value: f64) -> Result<Handler, Error> {
let pid = &mut channels.channel_state(channel).pid;
use super::command_parser::PidParameter::*;
match parameter {
Target =>
pid.target = value,
KP =>
pid.parameters.kp = value as f32,
KI =>
pid.update_ki(value as f32),
KD =>
pid.parameters.kd = value as f32,
OutputMin =>
pid.parameters.output_min = value as f32,
OutputMax =>
pid.parameters.output_max = value as f32,
}
send_line(socket, b"{}");
Ok(Handler::Handled)
}
fn set_steinhart_hart (socket: &mut TcpSocket, channels: &mut Channels, channel: usize, parameter: ShParameter, value: f64) -> Result<Handler, Error> {
let sh = &mut channels.channel_state(channel).sh;
use super::command_parser::ShParameter::*;
match parameter {
T0 => sh.t0 = ThermodynamicTemperature::new::<degree_celsius>(value),
B => sh.b = value,
R0 => sh.r0 = ElectricalResistance::new::<ohm>(value),
}
send_line(socket, b"{}");
Ok(Handler::Handled)
}
fn reset_post_filter (socket: &mut TcpSocket, channels: &mut Channels, channel: usize) -> Result<Handler, Error> {
channels.adc.set_postfilter(channel as u8, None).unwrap();
send_line(socket, b"{}");
Ok(Handler::Handled)
}
fn set_post_filter (socket: &mut TcpSocket, channels: &mut Channels, channel: usize, rate: f32) -> Result<Handler, Error> {
let filter = ad7172::PostFilter::closest(rate);
match filter {
Some(filter) => {
channels.adc.set_postfilter(channel as u8, Some(filter)).unwrap();
send_line(socket, b"{}");
}
None => {
error!("unable to choose postfilter for rate {:.3}", rate);
send_line(socket, b"{{\"error\": \"unable to choose postfilter rate\"}}");
return Err(Error::PostFilterRateError);
}
}
Ok(Handler::Handled)
}
fn load_channel (socket: &mut TcpSocket, channels: &mut Channels, store: &mut FlashStore, channel: Option<usize>) -> Result<Handler, Error> {
for c in 0..CHANNELS {
if channel.is_none() || channel == Some(c) {
match store.read_value::<ChannelConfig>(CHANNEL_CONFIG_KEY[c]) {
Ok(Some(config)) => {
config.apply(channels, c);
send_line(socket, b"{}");
}
Ok(None) => {
error!("flash config not found");
send_line(socket, b"{{\"error\": \"flash config not found\"}}");
}
Err(e) => {
error!("unable to load config from flash: {:?}", e);
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
return Err(Error::FlashError);
}
}
}
}
Ok(Handler::Handled)
}
fn save_channel (socket: &mut TcpSocket, channels: &mut Channels, channel: Option<usize>, store: &mut FlashStore) -> Result<Handler, Error> {
for c in 0..CHANNELS {
let mut store_value_buf = [0u8; 256];
if channel.is_none() || channel == Some(c) {
let config = ChannelConfig::new(channels, c);
match store.write_value(CHANNEL_CONFIG_KEY[c], &config, &mut store_value_buf) {
Ok(()) => {
send_line(socket, b"{}");
}
Err(e) => {
error!("unable to save channel {} config to flash: {:?}", c, e);
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
return Err(Error::FlashError);
}
}
}
}
Ok(Handler::Handled)
}
fn set_ipv4 (socket: &mut TcpSocket, store: &mut FlashStore, config: Ipv4Config) -> Result<Handler, Error> {
let _ = store
.write_value("ipv4", &config, [0; 16])
.map_err(|e| error!("unable to save ipv4 config to flash: {:?}", e));
let new_ipv4_config = Some(config);
send_line(socket, b"{}");
Ok(Handler::NewIPV4(new_ipv4_config.unwrap()))
}
fn reset (channels: &mut Channels) -> Result<Handler, Error> {
for i in 0..CHANNELS {
channels.power_down(i);
}
// should_reset = true;
Ok(Handler::Reset)
}
fn dfu (channels: &mut Channels) -> Result<Handler, Error> {
for i in 0..CHANNELS {
channels.power_down(i);
}
unsafe {
dfu::set_dfu_trigger();
}
// should_reset = true;
Ok(Handler::Reset)
}
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!\" }");
return Ok(Handler::Handled);
}
fan_ctrl.set_auto_mode(false);
fan_ctrl.set_pwm(fan_pwm);
if fan_ctrl.fan_pwm_recommended() {
send_line(socket, b"{}");
} else {
send_line(socket, b"{ \"warning\": \"this fan doesn't have full PWM support. Use it at your own risk!\" }");
}
Ok(Handler::Handled)
}
fn show_fan(socket: &mut TcpSocket, fan_ctrl: &mut FanCtrl) -> Result<Handler, Error> {
match fan_ctrl.summary() {
Ok(buf) => {
send_line(socket, &buf);
Ok(Handler::Handled)
}
Err(e) => {
error!("unable to serialize fan summary: {:?}", e);
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
Err(Error::ReportError)
}
}
}
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!\" }");
return Ok(Handler::Handled);
}
fan_ctrl.set_auto_mode(true);
if fan_ctrl.fan_pwm_recommended() {
send_line(socket, b"{}");
} else {
send_line(socket, b"{ \"warning\": \"this fan doesn't have full PWM support. Use it at your own risk!\" }");
}
Ok(Handler::Handled)
}
fn fan_curve(socket: &mut TcpSocket, fan_ctrl: &mut FanCtrl, k_a: f32, k_b: f32, k_c: f32) -> Result<Handler, Error> {
fan_ctrl.set_curve(k_a, k_b, k_c);
send_line(socket, b"{}");
Ok(Handler::Handled)
}
fn fan_defaults(socket: &mut TcpSocket, fan_ctrl: &mut FanCtrl) -> Result<Handler, Error> {
fan_ctrl.restore_defaults();
send_line(socket, b"{}");
Ok(Handler::Handled)
}
fn show_hwrev(socket: &mut TcpSocket, hwrev: HWRev) -> Result<Handler, Error> {
match hwrev.summary() {
Ok(buf) => {
send_line(socket, &buf);
Ok(Handler::Handled)
}
Err(e) => {
error!("unable to serialize HWRev summary: {:?}", e);
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
Err(Error::ReportError)
}
}
}
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 {
Command::Quit => Ok(Handler::CloseSocket),
Command::Reporting(_reporting) => Handler::reporting(socket),
Command::Show(ShowCommand::Reporting) => Handler::show_report_mode(socket, session),
Command::Show(ShowCommand::Input) => Handler::show_report(socket, channels),
Command::Show(ShowCommand::Pid) => Handler::show_pid(socket, channels),
Command::Show(ShowCommand::Pwm) => Handler::show_pwm(socket, channels),
Command::Show(ShowCommand::SteinhartHart) => Handler::show_steinhart_hart(socket, channels),
Command::Show(ShowCommand::PostFilter) => Handler::show_post_filter(socket, channels),
Command::Show(ShowCommand::Ipv4) => Handler::show_ipv4(socket, ipv4_config),
Command::PwmPid { channel } => Handler::engage_pid(socket, channels, channel),
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::Pid { channel, parameter, value } => Handler::set_pid(socket, channels, channel, parameter, value),
Command::SteinhartHart { channel, parameter, value } => Handler::set_steinhart_hart(socket, channels, channel, parameter, value),
Command::PostFilter { channel, rate: None } => Handler::reset_post_filter(socket, channels, channel),
Command::PostFilter { channel, rate: Some(rate) } => Handler::set_post_filter(socket, channels, channel, rate),
Command::Load { channel } => Handler::load_channel(socket, channels, store, channel),
Command::Save { channel } => Handler::save_channel(socket, channels, channel, store),
Command::Ipv4(config) => Handler::set_ipv4(socket, store, config),
Command::Reset => Handler::reset(channels),
Command::Dfu => Handler::dfu(channels),
Command::FanSet {fan_pwm} => Handler::set_fan(socket, fan_pwm, fan_ctrl),
Command::ShowFan => Handler::show_fan(socket, fan_ctrl),
Command::FanAuto => Handler::fan_auto(socket, fan_ctrl),
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::ShowHWRev => Handler::show_hwrev(socket, hwrev),
}
}
}

View File

@ -10,6 +10,7 @@ use nom::{
sequence::preceded,
multi::{fold_many0, fold_many1},
error::ErrorKind,
Needed,
};
use num_traits::{Num, ParseFloatError};
use serde::{Serialize, Deserialize};
@ -85,6 +86,13 @@ impl fmt::Display for Error {
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Ipv4Config {
pub address: [u8; 4],
pub mask_len: u8,
pub gateway: Option<[u8; 4]>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ShowCommand {
Input,
@ -93,6 +101,7 @@ pub enum ShowCommand {
Pid,
SteinhartHart,
PostFilter,
Ipv4,
}
#[derive(Debug, Clone, PartialEq)]
@ -103,8 +112,6 @@ pub enum PidParameter {
KD,
OutputMin,
OutputMax,
IntegralMin,
IntegralMax,
}
/// Steinhart-Hart equation parameter
@ -132,10 +139,14 @@ pub enum CenterPoint {
#[derive(Debug, Clone, PartialEq)]
pub enum Command {
Quit,
Load,
Save,
Load {
channel: Option<usize>,
},
Save {
channel: Option<usize>,
},
Reset,
Ipv4([u8; 4]),
Ipv4(Ipv4Config),
Show(ShowCommand),
Reporting(bool),
/// PWM parameter setting
@ -167,6 +178,19 @@ pub enum Command {
channel: usize,
rate: Option<f32>,
},
Dfu,
FanSet {
fan_pwm: u32
},
FanAuto,
ShowFan,
FanCurve {
k_a: f32,
k_b: f32,
k_c: f32,
},
FanCurveDefaults,
ShowHWRev,
}
fn end(input: &[u8]) -> IResult<&[u8], ()> {
@ -249,6 +273,16 @@ fn pwm_setup(input: &[u8]) -> IResult<&[u8], Result<(PwmPin, f64), Error>> {
result.map(|value| (pin, value));
alt((
map(
preceded(
tag("i_set"),
preceded(
whitespace,
float
)
),
result_with_pin(PwmPin::ISet)
),
map(
preceded(
tag("max_i_pos"),
@ -278,8 +312,6 @@ fn pwm_setup(input: &[u8]) -> IResult<&[u8], Result<(PwmPin, f64), Error>> {
)
),
result_with_pin(PwmPin::MaxV)
),
map(float, result_with_pin(PwmPin::ISet)
))
)(input)
}
@ -348,8 +380,6 @@ fn pid_parameter(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
value(PidParameter::KD, tag("kd")),
value(PidParameter::OutputMin, tag("output_min")),
value(PidParameter::OutputMax, tag("output_max")),
value(PidParameter::IntegralMin, tag("integral_min")),
value(PidParameter::IntegralMax, tag("integral_max"))
))(input)?;
let (input, _) = whitespace(input)?;
let (input, value) = float(input)?;
@ -429,9 +459,39 @@ fn postfilter(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
))(input)
}
fn ipv4(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
let (input, _) = tag("ipv4")(input)?;
let (input, _) = whitespace(input)?;
fn load(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
let (input, _) = tag("load")(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::Load { channel });
Ok((input, result))
}
fn save(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
let (input, _) = tag("save")(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::Save { channel });
Ok((input, result))
}
fn ipv4_addr(input: &[u8]) -> IResult<&[u8], Result<[u8; 4], Error>> {
let (input, a) = unsigned(input)?;
let (input, _) = tag(".")(input)?;
let (input, b) = unsigned(input)?;
@ -439,18 +499,95 @@ fn ipv4(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
let (input, c) = unsigned(input)?;
let (input, _) = tag(".")(input)?;
let (input, d) = unsigned(input)?;
end(input)?;
let address = move || Ok([a? as u8, b? as u8, c? as u8, d? as u8]);
Ok((input, address()))
}
let result = a.and_then(|a| b.and_then(|b| c.and_then(|c| d.map(|d|
Command::Ipv4([a as u8, b as u8, c as u8, d as u8])
))));
Ok((input, result))
fn ipv4(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
let (input, _) = tag("ipv4")(input)?;
alt((
|input| {
let (input, _) = whitespace(input)?;
let (input, address) = ipv4_addr(input)?;
let (input, _) = tag("/")(input)?;
let (input, mask_len) = unsigned(input)?;
let (input, gateway) = alt((
|input| {
let (input, _) = whitespace(input)?;
let (input, gateway) = ipv4_addr(input)?;
Ok((input, gateway.map(Some)))
},
value(Ok(None), end),
))(input)?;
let result = move || {
Ok(Command::Ipv4(Ipv4Config {
address: address?,
mask_len: mask_len? as u8,
gateway: gateway?,
}))
};
Ok((input, result()))
},
value(Ok(Command::Show(ShowCommand::Ipv4)), end),
))(input)
}
fn fan(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
let (input, _) = tag("fan")(input)?;
alt((
|input| {
let (input, _) = whitespace(input)?;
let (input, result) = alt((
|input| {
let (input, _) = tag("auto")(input)?;
Ok((input, Ok(Command::FanAuto)))
},
|input| {
let (input, value) = unsigned(input)?;
Ok((input, Ok(Command::FanSet { fan_pwm: value.unwrap_or(0)})))
},
))(input)?;
Ok((input, result))
},
value(Ok(Command::ShowFan), end)
))(input)
}
fn fan_curve(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
let (input, _) = tag("fcurve")(input)?;
alt((
|input| {
let (input, _) = whitespace(input)?;
let (input, result) = alt((
|input| {
let (input, _) = tag("default")(input)?;
Ok((input, Ok(Command::FanCurveDefaults)))
},
|input| {
let (input, k_a) = float(input)?;
let (input, _) = whitespace(input)?;
let (input, k_b) = float(input)?;
let (input, _) = whitespace(input)?;
let (input, k_c) = float(input)?;
if k_a.is_ok() && k_b.is_ok() && k_c.is_ok() {
Ok((input, Ok(Command::FanCurve { k_a: k_a.unwrap() as f32, k_b: k_b.unwrap() as f32, k_c: k_c.unwrap() as f32 })))
} else {
Err(nom::Err::Incomplete(Needed::Size(3)))
}
},
))(input)?;
Ok((input, result))
},
value(Err(Error::Incomplete), end)
))(input)
}
fn command(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
alt((value(Ok(Command::Quit), tag("quit")),
value(Ok(Command::Load), tag("load")),
value(Ok(Command::Save), tag("save")),
load,
save,
value(Ok(Command::Reset), tag("reset")),
ipv4,
map(report, Ok),
@ -459,6 +596,10 @@ fn command(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
pid,
steinhart_hart,
postfilter,
value(Ok(Command::Dfu), tag("dfu")),
fan,
fan_curve,
value(Ok(Command::ShowHWRev), tag("hwrev")),
))(input)
}
@ -488,19 +629,51 @@ mod test {
#[test]
fn parse_load() {
let command = Command::parse(b"load");
assert_eq!(command, Ok(Command::Load));
assert_eq!(command, Ok(Command::Load { channel: None }));
}
#[test]
fn parse_load_channel() {
let command = Command::parse(b"load 0");
assert_eq!(command, Ok(Command::Load { channel: Some(0) }));
}
#[test]
fn parse_save() {
let command = Command::parse(b"save");
assert_eq!(command, Ok(Command::Save));
assert_eq!(command, Ok(Command::Save { channel: None }));
}
#[test]
fn parse_save_channel() {
let command = Command::parse(b"save 0");
assert_eq!(command, Ok(Command::Save { channel: Some(0) }));
}
#[test]
fn parse_show_ipv4() {
let command = Command::parse(b"ipv4");
assert_eq!(command, Ok(Command::Show(ShowCommand::Ipv4)));
}
#[test]
fn parse_ipv4() {
let command = Command::parse(b"ipv4 192.168.1.26");
assert_eq!(command, Ok(Command::Ipv4([192, 168, 1, 26])));
let command = Command::parse(b"ipv4 192.168.1.26/24");
assert_eq!(command, Ok(Command::Ipv4(Ipv4Config {
address: [192, 168, 1, 26],
mask_len: 24,
gateway: None,
})));
}
#[test]
fn parse_ipv4_and_gateway() {
let command = Command::parse(b"ipv4 10.42.0.126/8 10.1.0.1");
assert_eq!(command, Ok(Command::Ipv4(Ipv4Config {
address: [10, 42, 0, 126],
mask_len: 8,
gateway: Some([10, 1, 0, 1]),
})));
}
#[test]
@ -528,8 +701,8 @@ mod test {
}
#[test]
fn parse_pwm_manual() {
let command = Command::parse(b"pwm 1 16383");
fn parse_pwm_i_set() {
let command = Command::parse(b"pwm 1 i_set 16383");
assert_eq!(command, Ok(Command::Pwm {
channel: 1,
pin: PwmPin::ISet,
@ -591,16 +764,6 @@ mod test {
}));
}
#[test]
fn parse_pid_integral_max() {
let command = Command::parse(b"pid 1 integral_max 2000");
assert_eq!(command, Ok(Command::Pid {
channel: 1,
parameter: PidParameter::IntegralMax,
value: 2000.0,
}));
}
#[test]
fn parse_steinhart_hart() {
let command = Command::parse(b"s-h");
@ -658,4 +821,44 @@ mod test {
center: CenterPoint::Vref,
}));
}
#[test]
fn parse_fan_show() {
let command = Command::parse(b"fan");
assert_eq!(command, Ok(Command::ShowFan));
}
#[test]
fn parse_fan_set() {
let command = Command::parse(b"fan 42");
assert_eq!(command, Ok(Command::FanSet {fan_pwm: 42}));
}
#[test]
fn parse_fan_auto() {
let command = Command::parse(b"fan auto");
assert_eq!(command, Ok(Command::FanAuto));
}
#[test]
fn parse_fcurve_set() {
let command = Command::parse(b"fcurve 1.2 3.4 5.6");
assert_eq!(command, Ok(Command::FanCurve {
k_a: 1.2,
k_b: 3.4,
k_c: 5.6
}));
}
#[test]
fn parse_fcurve_default() {
let command = Command::parse(b"fcurve default");
assert_eq!(command, Ok(Command::FanCurveDefaults));
}
#[test]
fn parse_hwrev() {
let command = Command::parse(b"hwrev");
assert_eq!(command, Ok(Command::ShowHWRev));
}
}

View File

@ -1,102 +1,24 @@
use postcard::{from_bytes, to_slice};
use serde::{Serialize, Deserialize};
use smoltcp::wire::Ipv4Address;
use stm32f4xx_hal::i2c;
use uom::si::{
electric_potential::volt,
electric_current::ampere,
electrical_resistance::ohm,
f64::{ElectricCurrent, ElectricPotential, ElectricalResistance, ThermodynamicTemperature},
thermodynamic_temperature::degree_celsius,
f64::{ElectricCurrent, ElectricPotential},
};
use crate::{
ad7172::PostFilter,
channels::{CHANNELS, Channels},
channels::Channels,
command_parser::CenterPoint,
EEPROM_SIZE, EEPROM_PAGE_SIZE,
pid,
pins,
steinhart_hart,
};
#[derive(Debug)]
pub enum Error {
Eeprom(eeprom24x::Error<i2c::Error>),
Encode(postcard::Error),
}
impl From<eeprom24x::Error<i2c::Error>> for Error {
fn from(e: eeprom24x::Error<i2c::Error>) -> Self {
Error::Eeprom(e)
}
}
impl From<postcard::Error> for Error {
fn from(e: postcard::Error) -> Self {
Error::Encode(e)
}
}
/// Just for encoding/decoding, actual state resides in ChannelState
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct Config {
channels: [ChannelConfig; CHANNELS],
pub ipv4_address: [u8; 4],
}
impl Config {
pub fn new(channels: &mut Channels, ipv4_address: Ipv4Address) -> Self {
Config {
channels: [
ChannelConfig::new(channels, 0),
ChannelConfig::new(channels, 1),
],
ipv4_address: ipv4_address.0,
}
}
/// apply loaded config to system
pub fn apply(&self, channels: &mut Channels) {
for i in 0..CHANNELS {
self.channels[i].apply(channels, i);
}
}
pub fn load(eeprom: &mut pins::Eeprom) -> Result<Self, Error> {
let mut buffer = [0; EEPROM_SIZE];
eeprom.read_data(0, &mut buffer)?;
log::info!("load: {:?}", buffer);
let config = from_bytes(&mut buffer)?;
Ok(config)
}
pub fn save(&self, eeprom: &mut pins::Eeprom) -> Result<(), Error> {
let mut buffer = [0; EEPROM_SIZE];
let config_buffer = to_slice(self, &mut buffer)?;
log::info!("save: {:?}", config_buffer);
let mut addr = 0;
for chunk in config_buffer.chunks(EEPROM_PAGE_SIZE) {
'write_retry: loop {
match eeprom.write_page(addr, chunk) {
Ok(()) => break 'write_retry,
Err(eeprom24x::Error::I2C(i2c::Error::NACK)) => {},
Err(e) => Err(e)?,
}
}
addr += chunk.len() as u32;
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ChannelConfig {
center: CenterPoint,
pid: pid::Parameters,
pid_target: f32,
sh: SteinhartHartConfig,
pid_engaged: bool,
sh: steinhart_hart::Parameters,
pwm: PwmLimits,
/// uses variant `PostFilter::Invalid` instead of `None` to save space
adc_postfilter: PostFilter,
@ -115,7 +37,8 @@ impl ChannelConfig {
center: state.center.clone(),
pid: state.pid.parameters.clone(),
pid_target: state.pid.target as f32,
sh: (&state.sh).into(),
pid_engaged: state.pid_engaged,
sh: state.sh.clone(),
pwm,
adc_postfilter,
}
@ -126,7 +49,8 @@ impl ChannelConfig {
state.center = self.center.clone();
state.pid.parameters = self.pid.clone();
state.pid.target = self.pid_target.into();
state.sh = (&self.sh).into();
state.pid_engaged = self.pid_engaged;
state.sh = self.sh.clone();
self.pwm.apply(channels, channel);
@ -138,116 +62,28 @@ impl ChannelConfig {
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
struct SteinhartHartConfig {
t0: f32,
r0: f32,
b: f32,
}
impl From<&steinhart_hart::Parameters> for SteinhartHartConfig {
fn from(sh: &steinhart_hart::Parameters) -> Self {
SteinhartHartConfig {
t0: sh.t0.get::<degree_celsius>() as f32,
r0: sh.r0.get::<ohm>() as f32,
b: sh.b as f32,
}
}
}
impl Into<steinhart_hart::Parameters> for &SteinhartHartConfig {
fn into(self) -> steinhart_hart::Parameters {
steinhart_hart::Parameters {
t0: ThermodynamicTemperature::new::<degree_celsius>(self.t0.into()),
r0: ElectricalResistance::new::<ohm>(self.r0.into()),
b: self.b.into(),
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
struct PwmLimits {
max_v: f32,
max_i_pos: f32,
max_i_neg: f32,
max_v: f64,
max_i_pos: f64,
max_i_neg: f64,
}
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 {
max_v: max_v.get::<volt>() as f32,
max_i_pos: max_i_pos.get::<ampere>() as f32,
max_i_neg: max_i_neg.get::<ampere>() as f32,
max_v: max_v.get::<volt>(),
max_i_pos: max_i_pos.get::<ampere>(),
max_i_neg: max_i_neg.get::<ampere>(),
}
}
pub fn apply(&self, channels: &mut Channels, channel: usize) {
channels.set_max_v(channel, ElectricPotential::new::<volt>(self.max_v.into()));
channels.set_max_i_pos(channel, ElectricCurrent::new::<ampere>(self.max_i_pos.into()));
channels.set_max_i_neg(channel, ElectricCurrent::new::<ampere>(self.max_i_neg.into()));
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::DEFAULT_IPV4_ADDRESS;
#[test]
fn test_fit_eeprom() {
let channel_config = ChannelConfig {
center: CenterPoint::Override(1.5),
pid: pid::Parameters::default(),
pid_target: 93.7,
sh: (&steinhart_hart::Parameters::default()).into(),
pwm: PwmLimits {
max_v: 1.65,
max_i_pos: 2.1,
max_i_neg: 2.25,
},
adc_postfilter: PostFilter::F21SPS,
};
let config = Config {
channels: [
channel_config.clone(),
channel_config.clone(),
],
ipv4_address: DEFAULT_IPV4_ADDRESS.0,
};
let mut buffer = [0; EEPROM_SIZE];
let buffer = to_slice(&config, &mut buffer).unwrap();
assert!(buffer.len() <= EEPROM_SIZE);
}
#[test]
fn test_encode_decode() {
let channel_config = ChannelConfig {
center: CenterPoint::Override(1.5),
pid: pid::Parameters::default(),
pid_target: 93.7,
sh: (&steinhart_hart::Parameters::default()).into(),
pwm: PwmLimits {
max_v: 1.65,
max_i_pos: 2.1,
max_i_neg: 2.25,
},
adc_postfilter: PostFilter::F21SPS,
};
let config = Config {
channels: [
channel_config.clone(),
channel_config.clone(),
],
ipv4_address: DEFAULT_IPV4_ADDRESS.0,
};
let mut buffer = [0; EEPROM_SIZE];
to_slice(&config, &mut buffer).unwrap();
let decoded: Config = from_bytes(&buffer).unwrap();
assert_eq!(decoded, config);
channels.set_max_v(channel, ElectricPotential::new::<volt>(self.max_v));
channels.set_max_i_pos(channel, ElectricCurrent::new::<ampere>(self.max_i_pos));
channels.set_max_i_neg(channel, ElectricCurrent::new::<ampere>(self.max_i_neg));
}
}

46
src/dfu.rs Normal file
View File

@ -0,0 +1,46 @@
use core::arch::asm;
use cortex_m_rt::pre_init;
use stm32f4xx_hal::stm32::{RCC, SYSCFG};
const DFU_TRIG_MSG: u32 = 0xDECAFBAD;
extern "C" {
// This symbol comes from memory.x
static mut _dfu_msg: u32;
}
pub unsafe fn set_dfu_trigger() {
_dfu_msg = DFU_TRIG_MSG;
}
/// Called by reset handler in lib.rs immediately after reset.
/// This function should not be called outside of reset handler as
/// bootloader expects MCU to be in reset state when called.
#[cfg(target_arch = "arm")]
#[pre_init]
unsafe fn __pre_init() {
if _dfu_msg == DFU_TRIG_MSG {
_dfu_msg = 0x00000000;
// Enable system config controller clock
let rcc = &*RCC::ptr();
rcc.apb2enr.modify(|_, w| w.syscfgen().set_bit());
// Bypass BOOT pins and remap bootloader to 0x00000000
let syscfg = &*SYSCFG::ptr() ;
syscfg.memrm.write(|w| w.mem_mode().bits(0b01));
// Impose instruction and memory barriers
cortex_m::asm::isb();
cortex_m::asm::dsb();
asm!(
// Set stack pointer to bootloader location
"LDR R0, =0x1FFF0000",
"LDR SP,[R0, #0]",
// Jump to bootloader
"LDR R0,[R0, #4]",
"BX R0",
);
}
}

152
src/fan_ctrl.rs Normal file
View File

@ -0,0 +1,152 @@
use num_traits::Float;
use serde::Serialize;
use stm32f4xx_hal::{
pwm::{self, PwmChannels},
pac::TIM8,
};
use uom::si::{
f64::ElectricCurrent,
electric_current::ampere,
};
use crate::{
hw_rev::HWSettings,
command_handler::JsonBuffer,
channels::MAX_TEC_I,
};
pub type FanPin = PwmChannels<TIM8, pwm::C4>;
const MAX_USER_FAN_PWM: f32 = 100.0;
const MIN_USER_FAN_PWM: f32 = 1.0;
pub struct FanCtrl {
fan: Option<FanPin>,
fan_auto: bool,
pwm_enabled: bool,
k_a: f32,
k_b: f32,
k_c: f32,
abs_max_tec_i: f32,
hw_settings: HWSettings,
}
impl FanCtrl {
pub fn new(fan: Option<FanPin>, hw_settings: HWSettings) -> Self {
let mut fan_ctrl = FanCtrl {
fan,
// do not enable auto mode by default,
// but allow to turn it at the user's own risk
fan_auto: hw_settings.fan_pwm_recommended,
pwm_enabled: false,
k_a: hw_settings.fan_k_a,
k_b: hw_settings.fan_k_b,
k_c: hw_settings.fan_k_c,
abs_max_tec_i: 0f32,
hw_settings,
};
if fan_ctrl.fan_auto {
fan_ctrl.enable_pwm();
}
fan_ctrl
}
pub fn cycle(&mut self, abs_max_tec_i: ElectricCurrent) {
self.abs_max_tec_i = abs_max_tec_i.get::<ampere>() as f32;
if self.fan_auto && self.hw_settings.fan_available {
let scaled_current = self.abs_max_tec_i / MAX_TEC_I as f32;
// do not limit upper bound, as it will be limited in the set_pwm()
let pwm = (MAX_USER_FAN_PWM * (scaled_current * (scaled_current * self.k_a + self.k_b) + self.k_c)) as u32;
self.set_pwm(pwm);
}
}
pub fn summary(&mut self) -> Result<JsonBuffer, serde_json_core::ser::Error> {
if self.hw_settings.fan_available {
let summary = FanSummary {
fan_pwm: self.get_pwm(),
abs_max_tec_i: self.abs_max_tec_i,
auto_mode: self.fan_auto,
k_a: self.k_a,
k_b: self.k_b,
k_c: self.k_c,
};
serde_json_core::to_vec(&summary)
} else {
let summary: Option<()> = None;
serde_json_core::to_vec(&summary)
}
}
pub fn set_auto_mode(&mut self, fan_auto: bool) {
self.fan_auto = fan_auto;
}
pub fn set_curve(&mut self, k_a: f32, k_b: f32, k_c: f32) {
self.k_a = k_a;
self.k_b = k_b;
self.k_c = k_c;
}
pub fn restore_defaults(&mut self) {
self.set_curve(self.hw_settings.fan_k_a,
self.hw_settings.fan_k_b,
self.hw_settings.fan_k_c);
}
pub fn set_pwm(&mut self, fan_pwm: u32) -> f32 {
if self.fan.is_none() || (!self.pwm_enabled && !self.enable_pwm()) {
return 0f32;
}
let fan = self.fan.as_mut().unwrap();
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, self.hw_settings.max_fan_pwm, MIN_USER_FAN_PWM, MAX_USER_FAN_PWM);
let max = fan.get_max_duty();
let value = ((duty * (max as f32)) as u16).min(max);
fan.set_duty(value);
value as f32 / (max as f32)
}
pub fn fan_pwm_recommended(&self) -> bool {
self.hw_settings.fan_pwm_recommended
}
pub fn fan_available(&self) -> bool {
self.hw_settings.fan_available
}
fn get_pwm(&self) -> u32 {
if let Some(fan) = &self.fan {
let duty = fan.get_duty();
let max = fan.get_max_duty();
scale_number(duty as f32 / (max as f32), MIN_USER_FAN_PWM, MAX_USER_FAN_PWM, self.hw_settings.min_fan_pwm, self.hw_settings.max_fan_pwm).round() as u32
} else { 0 }
}
fn enable_pwm(&mut self) -> bool {
if self.fan.is_some() && self.hw_settings.fan_available {
let fan = self.fan.as_mut().unwrap();
fan.set_duty(0);
fan.enable();
self.pwm_enabled = true;
true
} else {
false
}
}
}
fn scale_number(unscaled: f32, to_min: f32, to_max: f32, from_min: f32, from_max: f32) -> f32 {
(to_max - to_min) * (unscaled - from_min) / (from_max - from_min) + to_min
}
#[derive(Serialize)]
pub struct FanSummary {
fan_pwm: u32,
abs_max_tec_i: f32,
auto_mode: bool,
k_a: f32,
k_b: f32,
k_c: f32,
}

69
src/flash_store.rs Normal file
View File

@ -0,0 +1,69 @@
use log::{info, error};
use stm32f4xx_hal::{
flash::{Error, FlashExt},
stm32::FLASH,
};
use sfkv::{Store, StoreBackend};
/// 16 KiB
pub const FLASH_SECTOR_SIZE: usize = 0x4000;
pub const FLASH_SECTOR: u8 = 12;
static mut BACKUP_SPACE: [u8; FLASH_SECTOR_SIZE] = [0; FLASH_SECTOR_SIZE];
extern "C" {
// These are from memory.x
static _config_start: usize;
static _flash_start: usize;
}
pub struct FlashBackend {
flash: FLASH,
}
fn get_offset() -> usize {
unsafe {
(&_config_start as *const usize as usize) - (&_flash_start as *const usize as usize)
}
}
impl StoreBackend for FlashBackend {
type Data = [u8];
fn data(&self) -> &Self::Data {
&self.flash.read()[get_offset()..(get_offset() + FLASH_SECTOR_SIZE)]
}
type Error = Error;
fn erase(&mut self) -> Result<(), Self::Error> {
info!("erasing store flash");
self.flash.unlocked().erase(FLASH_SECTOR)
}
fn program(&mut self, offset: usize, payload: &[u8]) -> Result<(), Self::Error> {
self.flash.unlocked()
.program(get_offset() + offset, payload.iter())
}
fn backup_space(&self) -> &'static mut [u8] {
unsafe { &mut BACKUP_SPACE }
}
}
pub type FlashStore = Store<FlashBackend>;
pub fn store(flash: FLASH) -> FlashStore {
let backend = FlashBackend { flash };
let mut store = FlashStore::new(backend);
// just try to read the store
match store.get_bytes_used() {
Ok(_) => {}
Err(e) => {
error!("corrupt store, erasing. error: {:?}", e);
let _ = store.erase()
.map_err(|e| error!("flash erase failed: {:?}", e));
}
}
store
}

82
src/hw_rev.rs Normal file
View File

@ -0,0 +1,82 @@
use serde::Serialize;
use crate::{
pins::HWRevPins,
command_handler::JsonBuffer,
};
#[derive(Serialize, Copy, Clone)]
pub struct HWRev {
pub major: u8,
pub minor: u8,
}
#[derive(Serialize, Clone)]
pub struct HWSettings {
pub fan_k_a: f32,
pub fan_k_b: f32,
pub fan_k_c: f32,
pub min_fan_pwm: f32,
pub max_fan_pwm: f32,
pub fan_pwm_freq_hz: u32,
pub fan_available: bool,
pub fan_pwm_recommended: bool,
}
#[derive(Serialize, Clone)]
struct HWSummary<'a> {
rev: &'a HWRev,
settings: &'a HWSettings,
}
impl HWRev {
pub fn detect_hw_rev(hwrev_pins: &HWRevPins) -> Self {
let (h0, h1, h2, h3) = (hwrev_pins.hwrev0.is_high(), hwrev_pins.hwrev1.is_high(),
hwrev_pins.hwrev2.is_high(), hwrev_pins.hwrev3.is_high());
match (h0, h1, h2, h3) {
(true, true, true, false) => HWRev { major: 1, minor: 0 },
(true, false, false, false) => HWRev { major: 2, minor: 0 },
(false, true, false, false) => HWRev { major: 2, minor: 2 },
(_, _, _, _) => HWRev { major: 0, minor: 0 }
}
}
pub fn settings(&self) -> HWSettings {
match (self.major, self.minor) {
(2, 2) => HWSettings {
fan_k_a: 1.0,
fan_k_b: 0.0,
fan_k_c: 0.0,
// below this value motor's autostart feature may fail,
// according to internal experiments
min_fan_pwm: 0.04,
max_fan_pwm: 1.0,
// According to `SUNON DC Brushless Fan & Blower(255-E)` catalogue p.36-37
// model MF35101V1-1000U-G99 doesn't have a PWM wire, but we'll follow their others models'
// recommended frequency, as it is said by the Thermostat's schematics that we can
// use PWM, but not stated at which frequency
fan_pwm_freq_hz: 25_000,
fan_available: true,
// see https://github.com/sinara-hw/Thermostat/issues/115 and
// https://git.m-labs.hk/M-Labs/thermostat/issues/69#issuecomment-6464 for explanation
fan_pwm_recommended: false,
},
(_, _) => HWSettings {
fan_k_a: 0.0,
fan_k_b: 0.0,
fan_k_c: 0.0,
min_fan_pwm: 0.0,
max_fan_pwm: 0.0,
fan_pwm_freq_hz: 0,
fan_available: false,
fan_pwm_recommended: false,
}
}
}
pub fn summary(&self) -> Result<JsonBuffer, serde_json_core::ser::Error> {
let settings = self.settings();
let summary = HWSummary { rev: self, settings: &settings };
serde_json_core::to_vec(&summary)
}
}

View File

@ -1,3 +1,4 @@
#[cfg(not(feature = "semihosting"))]
use crate::usb;
#[cfg(not(feature = "semihosting"))]

View File

@ -1,44 +1,27 @@
#![cfg_attr(not(test), no_std)]
#![cfg_attr(not(test), no_main)]
#![feature(maybe_uninit_extra, maybe_uninit_ref)]
#![cfg_attr(test, allow(unused))]
// TODO: #![deny(warnings, unused)]
#[cfg(not(any(feature = "semihosting", test)))]
use panic_abort as _;
use panic_halt as _;
#[cfg(all(feature = "semihosting", not(test)))]
use panic_semihosting as _;
use log::{error, info, warn};
use core::fmt::Write;
use cortex_m::asm::wfi;
use cortex_m_rt::entry;
use stm32f4xx_hal::{
hal::watchdog::{WatchdogEnable, Watchdog},
rcc::RccExt,
watchdog::IndependentWatchdog,
time::{U32Ext, MegaHertz},
stm32::{CorePeripherals, Peripherals, SCB},
time::{U32Ext, MegaHertz},
watchdog::IndependentWatchdog,
};
use smoltcp::{
time::Instant,
socket::TcpSocket,
wire::{EthernetAddress, Ipv4Address},
};
use uom::{
si::{
f64::{
ElectricCurrent,
ElectricPotential,
ElectricalResistance,
ThermodynamicTemperature,
},
electric_current::ampere,
electric_potential::volt,
electrical_resistance::ohm,
thermodynamic_temperature::degree_celsius,
},
wire::EthernetAddress,
};
mod init_log;
@ -55,7 +38,7 @@ use server::Server;
mod session;
use session::{Session, SessionInput};
mod command_parser;
use command_parser::{Command, ShowCommand, PwmPin};
use command_parser::Ipv4Config;
mod timer;
mod pid;
mod steinhart_hart;
@ -64,8 +47,14 @@ use channels::{CHANNELS, Channels};
mod channel;
mod channel_state;
mod config;
use config::Config;
use config::ChannelConfig;
mod flash_store;
mod dfu;
mod command_handler;
use command_handler::Handler;
mod fan_ctrl;
use fan_ctrl::FanCtrl;
mod hw_rev;
const HSE: MegaHertz = MegaHertz(8);
#[cfg(not(feature = "semihosting"))]
@ -73,17 +62,15 @@ const WATCHDOG_INTERVAL: u32 = 1_000;
#[cfg(feature = "semihosting")]
const WATCHDOG_INTERVAL: u32 = 30_000;
pub const EEPROM_PAGE_SIZE: usize = 8;
pub const EEPROM_SIZE: usize = 128;
const CHANNEL_CONFIG_KEY: [&str; 2] = ["ch0", "ch1"];
pub const DEFAULT_IPV4_ADDRESS: Ipv4Address = Ipv4Address([192, 168, 1, 26]);
const TCP_PORT: u16 = 23;
fn send_line(socket: &mut TcpSocket, data: &[u8]) -> bool {
let send_free = socket.send_capacity() - socket.send_queue();
if data.len() > send_free + 1 {
// Not enough buffer space, skip report for now
// Not enough buffer space, skip report for now,
// instead of sending incomplete line
warn!(
"TCP socket has only {}/{} needed {}",
send_free + 1, socket.send_capacity(), data.len(),
@ -105,17 +92,6 @@ fn send_line(socket: &mut TcpSocket, data: &[u8]) -> bool {
false
}
fn report_to(channel: usize, channels: &mut Channels, socket: &mut TcpSocket) -> bool {
match channels.report(channel).to_json() {
Ok(buf) =>
send_line(socket, &buf[..]),
Err(e) => {
error!("unable to serialize report: {:?}", e);
false
}
}
}
/// Initialization and main loop
#[cfg(not(test))]
#[entry]
@ -143,8 +119,8 @@ fn main() -> ! {
timer::setup(cp.SYST, clocks);
let (pins, mut leds, mut eeprom, eth_pins, usb) = Pins::setup(
clocks, dp.TIM1, dp.TIM3,
let (pins, mut leds, mut eeprom, eth_pins, usb, fan, hwrev, hw_settings) = Pins::setup(
clocks, dp.TIM1, dp.TIM3, dp.TIM8,
dp.GPIOA, dp.GPIOB, dp.GPIOC, dp.GPIOD, dp.GPIOE, dp.GPIOF, dp.GPIOG,
dp.I2C1,
dp.SPI2, dp.SPI4, dp.SPI5,
@ -160,15 +136,35 @@ fn main() -> ! {
usb::State::setup(usb);
let mut ipv4_address = DEFAULT_IPV4_ADDRESS;
let mut store = flash_store::store(dp.FLASH);
let mut channels = Channels::new(pins);
let _ = Config::load(&mut eeprom)
.map(|config| {
config.apply(&mut channels);
ipv4_address = Ipv4Address::from_bytes(&config.ipv4_address);
})
.map_err(|e| warn!("error loading config: {:?}", e));
info!("IPv4 address: {}", ipv4_address);
for c in 0..CHANNELS {
match store.read_value::<ChannelConfig>(CHANNEL_CONFIG_KEY[c]) {
Ok(Some(config)) =>
config.apply(&mut channels, c),
Ok(None) =>
error!("flash config not found for channel {}", c),
Err(e) =>
error!("unable to load config {} from flash: {:?}", c, e),
}
}
let mut fan_ctrl = FanCtrl::new(fan, hw_settings);
// default net config:
let mut ipv4_config = Ipv4Config {
address: [192, 168, 1, 26],
mask_len: 24,
gateway: None,
};
match store.read_value("ipv4") {
Ok(Some(config)) =>
ipv4_config = config,
Ok(None) => {}
Err(e) =>
error!("cannot read ipv4 config: {:?}", e),
}
// EEPROM ships with a read-only EUI-48 identifier
let mut eui48 = [0; 6];
@ -176,18 +172,27 @@ fn main() -> ! {
let hwaddr = EthernetAddress(eui48);
info!("EEPROM MAC address: {}", hwaddr);
net::run(clocks, dp.ETHERNET_MAC, dp.ETHERNET_DMA, eth_pins, hwaddr, ipv4_address, |iface| {
let mut new_ipv4_address = None;
net::run(clocks, dp.ETHERNET_MAC, dp.ETHERNET_DMA, eth_pins, hwaddr, ipv4_config.clone(), |iface| {
Server::<Session>::run(iface, |server| {
leds.r1.off();
let mut should_reset = false;
loop {
let mut new_ipv4_config = None;
let instant = Instant::from_millis(i64::from(timer::now()));
let updated_channel = channels.poll_adc(instant);
if let Some(channel) = updated_channel {
server.for_each(|_, session| session.set_report_pending(channel.into()));
}
fan_ctrl.cycle(channels.current_abs_max_tec_i());
if channels.pid_engaged() {
leds.g3.on();
} else {
leds.g3.off();
}
let instant = Instant::from_millis(i64::from(timer::now()));
cortex_m::interrupt::free(net::clear_pending);
server.poll(instant)
@ -195,202 +200,74 @@ fn main() -> ! {
warn!("poll: {:?}", e);
});
// TCP protocol handling
server.for_each(|mut socket, session| {
if ! socket.is_active() {
let _ = socket.listen(TCP_PORT);
session.reset();
} else if socket.may_send() && !socket.may_recv() {
socket.close()
} else if socket.can_send() && socket.can_recv() {
match socket.recv(|buf| session.feed(buf)) {
Ok(SessionInput::Nothing) => {}
Ok(SessionInput::Command(command)) => match command {
Command::Quit =>
if ! should_reset {
// TCP protocol handling
server.for_each(|mut socket, session| {
if ! socket.is_active() {
let _ = socket.listen(TCP_PORT);
session.reset();
} else if socket.may_send() && !socket.may_recv() {
socket.close()
} else if socket.can_send() && socket.can_recv() {
match socket.recv(|buf| session.feed(buf)) {
// SessionInput::Nothing happens when the line reader parses a string of characters that is not
// followed by a newline character. Could be due to partial commands not terminated with newline,
// socket RX ring buffer wraps around, or when the command is sent as seperate TCP packets etc.
// Do nothing and feed more data to the line reader in the next loop cycle.
Ok(SessionInput::Nothing) => {}
Ok(SessionInput::Command(command)) => {
match Handler::handle_command(command, &mut socket, &mut channels, session, &mut store, &mut ipv4_config, &mut fan_ctrl, hwrev) {
Ok(Handler::NewIPV4(ip)) => new_ipv4_config = Some(ip),
Ok(Handler::Handled) => {},
Ok(Handler::CloseSocket) => socket.close(),
Ok(Handler::Reset) => should_reset = true,
Err(_) => {},
}
}
Ok(SessionInput::Error(e)) => {
error!("session input: {:?}", e);
send_line(&mut socket, b"{ \"error\": \"invalid input\" }");
}
Err(_) =>
socket.close(),
Command::Reporting(_reporting) => {
// handled by session
}
Command::Show(ShowCommand::Reporting) => {
let _ = writeln!(socket, "{{ \"report\": {:?} }}", session.reporting());
}
Command::Show(ShowCommand::Input) => {
for channel in 0..CHANNELS {
report_to(channel, &mut channels, &mut socket);
}
}
Command::Show(ShowCommand::Pid) => {
for channel in 0..CHANNELS {
match channels.channel_state(channel).pid.summary(channel).to_json() {
Ok(buf) => {
send_line(&mut socket, &buf);
}
Err(e) =>
error!("unable to serialize pid summary: {:?}", e),
}
}
}
Command::Show(ShowCommand::Pwm) => {
for channel in 0..CHANNELS {
match channels.pwm_summary(channel).to_json() {
Ok(buf) => {
send_line(&mut socket, &buf);
}
Err(e) =>
error!("unable to serialize pwm summary: {:?}", e),
}
}
}
Command::Show(ShowCommand::SteinhartHart) => {
for channel in 0..CHANNELS {
match channels.steinhart_hart_summary(channel).to_json() {
Ok(buf) => {
send_line(&mut socket, &buf);
}
Err(e) =>
error!("unable to serialize steinhart-hart summary: {:?}", e),
}
}
}
Command::Show(ShowCommand::PostFilter) => {
for channel in 0..CHANNELS {
match channels.postfilter_summary(channel).to_json() {
Ok(buf) => {
send_line(&mut socket, &buf);
}
Err(e) =>
error!("unable to serialize postfilter summary: {:?}", e),
}
}
}
Command::PwmPid { channel } => {
channels.channel_state(channel).pid_engaged = true;
leds.g3.on();
}
Command::Pwm { channel, pin, value } => {
match pin {
PwmPin::ISet => {
channels.channel_state(channel).pid_engaged = false;
leds.g3.off();
let current = ElectricCurrent::new::<ampere>(value);
channels.set_i(channel, current);
channels.power_up(channel);
}
PwmPin::MaxV => {
let voltage = ElectricPotential::new::<volt>(value);
channels.set_max_v(channel, voltage);
}
PwmPin::MaxIPos => {
let current = ElectricCurrent::new::<ampere>(value);
channels.set_max_i_pos(channel, current);
}
PwmPin::MaxINeg => {
let current = ElectricCurrent::new::<ampere>(value);
channels.set_max_i_neg(channel, current);
}
}
}
Command::CenterPoint { channel, center } => {
let (i_tec, _) = channels.get_i(channel);
let state = channels.channel_state(channel);
state.center = center;
if !state.pid_engaged {
channels.set_i(channel, i_tec);
}
}
Command::Pid { channel, parameter, value } => {
let pid = &mut channels.channel_state(channel).pid;
use command_parser::PidParameter::*;
match parameter {
Target =>
pid.target = value,
KP =>
pid.parameters.kp = value as f32,
KI =>
pid.parameters.ki = value as f32,
KD =>
pid.parameters.kd = value as f32,
OutputMin =>
pid.parameters.output_min = value as f32,
OutputMax =>
pid.parameters.output_max = value as f32,
IntegralMin =>
pid.parameters.integral_min = value as f32,
IntegralMax =>
pid.parameters.integral_max = value as f32,
}
}
Command::SteinhartHart { channel, parameter, value } => {
let sh = &mut channels.channel_state(channel).sh;
use command_parser::ShParameter::*;
match parameter {
T0 => sh.t0 = ThermodynamicTemperature::new::<degree_celsius>(value),
B => sh.b = value,
R0 => sh.r0 = ElectricalResistance::new::<ohm>(value),
}
}
Command::PostFilter { channel, rate: None } => {
channels.adc.set_postfilter(channel as u8, None).unwrap();
}
Command::PostFilter { channel, rate: Some(rate) } => {
let filter = ad7172::PostFilter::closest(rate);
match filter {
Some(filter) =>
channels.adc.set_postfilter(channel as u8, Some(filter)).unwrap(),
None =>
error!("unable to choose postfilter for rate {:.3}", rate),
}
}
Command::Load => {
match Config::load(&mut eeprom) {
Ok(config) => {
config.apply(&mut channels);
new_ipv4_address = Some(Ipv4Address::from_bytes(&config.ipv4_address));
}
Err(e) =>
error!("unable to load eeprom config: {:?}", e),
}
}
Command::Save => {
let config = Config::new(&mut channels, ipv4_address);
match config.save(&mut eeprom) {
Ok(()) => {},
Err(e) =>
error!("unable to save eeprom config: {:?}", e),
}
}
Command::Ipv4(address) => {
new_ipv4_address = Some(Ipv4Address::from_bytes(&address));
}
Command::Reset => {
for i in 0..CHANNELS {
channels.power_down(i);
}
} else if socket.can_send() {
if let Some(channel) = session.is_report_pending() {
match channels.reports_json() {
Ok(buf) => {
send_line(&mut socket, &buf[..]);
session.mark_report_sent(channel);
}
Err(e) => {
error!("unable to serialize report: {:?}", e);
SCB::sys_reset();
}
}
}
Ok(SessionInput::Error(e)) => {
error!("session input: {:?}", e);
send_line(&mut socket, b"{ \"error\": \"invalid input\" }");
}
Err(_) =>
socket.close(),
}
} else if socket.can_send() {
if let Some(channel) = session.is_report_pending() {
if report_to(channel, &mut channels, &mut socket) {
session.mark_report_sent(channel);
}
}
}
});
});
} else {
// Should reset, close all TCP sockets.
let mut any_socket_alive = false;
server.for_each(|mut socket, _| {
if socket.is_active() {
socket.abort();
any_socket_alive = true;
}
});
// Must let loop run for one more cycle to poll server for RST to be sent,
// this makes sure system does not reset right after socket.abort() is called.
if !any_socket_alive {
SCB::sys_reset();
}
}
// Apply new IPv4 address
new_ipv4_address.map(|new_ipv4_address| {
server.set_ipv4_address(ipv4_address);
ipv4_address = new_ipv4_address;
});
// Apply new IPv4 address/gateway
new_ipv4_config.take()
.map(|config| {
server.set_ipv4_config(config.clone());
ipv4_config = config;
});
// Update watchdog
wd.feed();

View File

@ -5,11 +5,15 @@ use core::cell::RefCell;
use cortex_m::interrupt::{CriticalSection, Mutex};
use stm32f4xx_hal::{
rcc::Clocks,
stm32::{interrupt, Peripherals, ETHERNET_MAC, ETHERNET_DMA},
pac::{interrupt, Peripherals, ETHERNET_MAC, ETHERNET_DMA},
};
use smoltcp::wire::{EthernetAddress, IpCidr, Ipv4Address};
use smoltcp::iface::{NeighborCache, EthernetInterfaceBuilder, EthernetInterface};
use stm32_eth::{Eth, RingEntry, PhyAddress, RxDescriptor, TxDescriptor};
use smoltcp::wire::{EthernetAddress, Ipv4Address, Ipv4Cidr};
use smoltcp::iface::{
EthernetInterfaceBuilder, EthernetInterface,
NeighborCache, Routes,
};
use stm32_eth::{Eth, RingEntry, RxDescriptor, TxDescriptor};
use crate::command_parser::Ipv4Config;
use crate::pins::EthernetPins;
/// Not on the stack so that stack can be placed in CCMRAM (which the
@ -29,7 +33,7 @@ pub fn run<F>(
ethernet_mac: ETHERNET_MAC, ethernet_dma: ETHERNET_DMA,
eth_pins: EthernetPins,
ethernet_addr: EthernetAddress,
local_addr: Ipv4Address,
ipv4_config: Ipv4Config,
f: F
) where
F: FnOnce(EthernetInterface<&mut stm32_eth::Eth<'static, 'static>>),
@ -44,22 +48,24 @@ pub fn run<F>(
let mut eth_dev = Eth::new(
ethernet_mac, ethernet_dma,
&mut rx_ring[..], &mut tx_ring[..],
PhyAddress::_0,
clocks,
eth_pins,
).unwrap();
eth_dev.enable_interrupt();
// IP stack
// Netmask 0 means we expect any IP address on the local segment.
// No routing.
let mut ip_addrs = [IpCidr::new(local_addr.into(), 0)];
let (ipv4_cidr, gateway) = split_ipv4_config(ipv4_config);
let mut ip_addrs = [ipv4_cidr.into()];
let mut neighbor_storage = [None; 16];
let neighbor_cache = NeighborCache::new(&mut neighbor_storage[..]);
let mut routes_storage = [None; 1];
let mut routes = Routes::new(&mut routes_storage[..]);
gateway.map(|gateway| routes.add_default_ipv4_route(gateway).unwrap());
let iface = EthernetInterfaceBuilder::new(&mut eth_dev)
.ethernet_addr(ethernet_addr)
.ip_addrs(&mut ip_addrs[..])
.neighbor_cache(neighbor_cache)
.routes(routes)
.finalize();
f(iface);
@ -90,3 +96,10 @@ pub fn clear_pending(cs: &CriticalSection) {
*NET_PENDING.borrow(cs)
.borrow_mut() = false;
}
/// utility for destructuring into smoltcp types
pub fn split_ipv4_config(config: Ipv4Config) -> (Ipv4Cidr, Option<Ipv4Address>) {
let cidr = Ipv4Cidr::new(Ipv4Address(config.address), config.mask_len);
let gateway = config.gateway.map(Ipv4Address);
(cidr, gateway)
}

View File

@ -2,25 +2,26 @@ use serde::{Serialize, Deserialize};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Parameters {
/// Gain coefficient for proportional term
pub kp: f32,
/// Gain coefficient for integral term
pub ki: f32,
/// Gain coefficient for derivative term
pub kd: f32,
/// Output limit minimum
pub output_min: f32,
/// Output limit maximum
pub output_max: f32,
pub integral_min: f32,
pub integral_max: f32
}
impl Default for Parameters {
fn default() -> Self {
Parameters {
kp: 1.5,
ki: 0.1,
kd: 150.0,
output_min: 0.0,
kp: 0.0,
ki: 0.0,
kd: 0.0,
output_min: -2.0,
output_max: 2.0,
integral_min: -10.0,
integral_max: 10.0,
}
}
}
@ -28,56 +29,50 @@ impl Default for Parameters {
#[derive(Clone)]
pub struct Controller {
pub parameters: Parameters,
pub target: f64,
integral: f64,
last_input: Option<f64>,
pub last_output: Option<f64>,
pub target : f64,
u1 : f64,
x1 : f64,
x2 : f64,
pub y1 : f64,
}
impl Controller {
pub const fn new(parameters: Parameters) -> Controller {
Controller {
parameters: parameters,
target: 0.0,
last_input: None,
integral: 0.0,
last_output: None,
target : 0.0,
u1 : 0.0,
x1 : 0.0,
x2 : 0.0,
y1 : 0.0,
}
}
// Based on https://hackmd.io/IACbwcOTSt6Adj3_F9bKuw PID implementation
// Input x(t), target u(t), output y(t)
// y0' = y1 - ki * u0
// + 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 {
// error
let error = input - self.target;
// partial
let p = f64::from(self.parameters.kp) * error;
// integral
self.integral += f64::from(self.parameters.ki) * error;
if self.integral < self.parameters.integral_min.into() {
self.integral = self.parameters.integral_min.into();
}
if self.integral > self.parameters.integral_max.into() {
self.integral = self.parameters.integral_max.into();
}
let i = self.integral;
// derivative
let d = match self.last_input {
None => 0.0,
Some(last_input) => f64::from(self.parameters.kd) * (input - last_input),
};
self.last_input = Some(input);
// output
let mut output = p + i + d;
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);
if output < self.parameters.output_min.into() {
output = self.parameters.output_min.into();
}
if output > self.parameters.output_max.into() {
output = self.parameters.output_max.into();
}
self.last_output = Some(output);
self.x2 = self.x1;
self.x1 = input;
self.u1 = self.target;
self.y1 = output;
output
}
@ -86,25 +81,19 @@ impl Controller {
channel,
parameters: self.parameters.clone(),
target: self.target,
integral: self.integral,
}
}
}
type JsonBuffer = heapless::Vec<u8, heapless::consts::U360>;
pub fn update_ki(&mut self, new_ki: f32) {
self.parameters.ki = new_ki;
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct Summary {
channel: usize,
parameters: Parameters,
target: f64,
integral: f64,
}
impl Summary {
pub fn to_json(&self) -> Result<JsonBuffer, serde_json_core::ser::Error> {
serde_json_core::to_vec(self)
}
}
#[cfg(test)]
@ -112,21 +101,27 @@ mod test {
use super::*;
const PARAMETERS: Parameters = Parameters {
kp: 0.055,
ki: 0.005,
kd: 0.04,
kp: 0.03,
ki: 0.002,
kd: 0.15,
output_min: -10.0,
output_max: 10.0,
integral_min: -100.0,
integral_max: 100.0,
};
#[test]
fn test_controller() {
const DEFAULT: f64 = 0.0;
const TARGET: f64 = -1234.56;
const ERROR: f64 = 0.01;
const DELAY: usize = 10;
// Initial and ambient temperature
const DEFAULT: f64 = 20.0;
// Target temperature
const TARGET: f64 = 40.0;
// Control tolerance
const ERROR: f64 = 0.01;
// System response delay
const DELAY: usize = 10;
// Heat lost
const LOSS: f64 = 0.05;
// Limit simulation cycle, reaching this limit before settling fails test
const CYCLE_LIMIT: u32 = 1000;
let mut pid = Controller::new(PARAMETERS.clone());
pid.target = TARGET;
@ -134,24 +129,18 @@ mod test {
let mut values = [DEFAULT; DELAY];
let mut t = 0;
let mut total_t = 0;
let mut output: f64 = 0.0;
let target = (TARGET - ERROR)..=(TARGET + ERROR);
while !values.iter().all(|value| target.contains(value)) {
while !values.iter().all(|value| target.contains(value)) && total_t < CYCLE_LIMIT {
let next_t = (t + 1) % DELAY;
// Feed the oldest temperature
let output = pid.update(values[next_t]);
output = pid.update(values[next_t]);
// Overwrite oldest with previous temperature - output
values[next_t] = values[t] - output;
values[next_t] = values[t] - output - (values[t] - DEFAULT) * LOSS;
t = next_t;
total_t += 1;
println!("{}", values[t].to_string());
}
}
#[test]
fn summary_to_json() {
let mut pid = Controller::new(PARAMETERS.clone());
pid.target = 30.0 / 1.1;
let buf = pid.summary(0).to_json().unwrap();
assert_eq!(buf[0], b'{');
assert_eq!(buf[buf.len() - 1], b'}');
assert_ne!(CYCLE_LIMIT, total_t);
}
}

View File

@ -16,15 +16,16 @@ use stm32f4xx_hal::{
otg_fs::USB,
rcc::Clocks,
pwm::{self, PwmChannels},
spi::{Spi, NoMiso},
stm32::{
spi::{Spi, NoMiso, TransferModeNormal},
pac::{
ADC1,
GPIOA, GPIOB, GPIOC, GPIOD, GPIOE, GPIOF, GPIOG,
I2C1,
OTG_FS_GLOBAL, OTG_FS_DEVICE, OTG_FS_PWRCLK,
SPI2, SPI4, SPI5,
TIM1, TIM3,
TIM1, TIM3, TIM8
},
timer::Timer,
time::U32Ext,
};
use eeprom24x::{self, Eeprom24x};
@ -32,12 +33,14 @@ use stm32_eth::EthPins;
use crate::{
channel::{Channel0, Channel1},
leds::Leds,
fan_ctrl::FanPin,
hw_rev::{HWRev, HWSettings},
};
pub type Eeprom = Eeprom24x<
I2c<I2C1, (
PB8<AlternateOD<stm32f4xx_hal::gpio::AF4>>,
PB9<AlternateOD<stm32f4xx_hal::gpio::AF4>>
PB8<AlternateOD<{ stm32f4xx_hal::gpio::AF4 }>>,
PB9<AlternateOD<{ stm32f4xx_hal::gpio::AF4 }>>
)>,
eeprom24x::page_size::B8,
eeprom24x::addr_size::OneByte
@ -45,8 +48,6 @@ pub type Eeprom = Eeprom24x<
pub type EthernetPins = EthPins<
PA1<Input<Floating>>,
PA2<Input<Floating>>,
PC1<Input<Floating>>,
PA7<Input<Floating>>,
PB11<Input<Floating>>,
PG13<Input<Floating>>,
@ -65,31 +66,41 @@ 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>;
}
/// SPI peripheral used for communication with the ADC
pub type AdcSpi = Spi<SPI2, (PB10<Alternate<AF5>>, PB14<Alternate<AF5>>, PB15<Alternate<AF5>>)>;
pub type AdcSpi = Spi<SPI2, (PB10<Alternate<AF5>>, PB14<Alternate<AF5>>, PB15<Alternate<AF5>>), TransferModeNormal>;
pub type AdcNss = PB12<Output<PushPull>>;
type Dac0Spi = Spi<SPI4, (PE2<Alternate<AF5>>, NoMiso, PE6<Alternate<AF5>>)>;
type Dac1Spi = Spi<SPI5, (PF7<Alternate<AF5>>, NoMiso, PF9<Alternate<AF5>>)>;
type Dac0Spi = Spi<SPI4, (PE2<Alternate<AF5>>, NoMiso, PE6<Alternate<AF5>>), TransferModeNormal>;
type Dac1Spi = Spi<SPI5, (PF7<Alternate<AF5>>, NoMiso, PF9<Alternate<AF5>>), TransferModeNormal>;
pub type PinsAdc = Adc<ADC1>;
pub struct ChannelPinSet<C: ChannelPins> {
@ -102,6 +113,13 @@ pub struct ChannelPinSet<C: ChannelPins> {
pub tec_u_meas_pin: C::TecUMeasPin,
}
pub struct HWRevPins {
pub hwrev0: stm32f4xx_hal::gpio::gpiod::PD0<Input<Floating>>,
pub hwrev1: stm32f4xx_hal::gpio::gpiod::PD1<Input<Floating>>,
pub hwrev2: stm32f4xx_hal::gpio::gpiod::PD2<Input<Floating>>,
pub hwrev3: stm32f4xx_hal::gpio::gpiod::PD3<Input<Floating>>,
}
pub struct Pins {
pub adc_spi: AdcSpi,
pub adc_nss: AdcNss,
@ -115,13 +133,13 @@ impl Pins {
/// Setup GPIO pins and configure MCU peripherals
pub fn setup(
clocks: Clocks,
tim1: TIM1, tim3: TIM3,
tim1: TIM1, tim3: TIM3, tim8: TIM8,
gpioa: GPIOA, gpiob: GPIOB, gpioc: GPIOC, gpiod: GPIOD, gpioe: GPIOE, gpiof: GPIOF, gpiog: GPIOG,
i2c1: I2C1,
spi2: SPI2, spi4: SPI4, spi5: SPI5,
adc1: ADC1,
otg_fs_global: OTG_FS_GLOBAL, otg_fs_device: OTG_FS_DEVICE, otg_fs_pwrclk: OTG_FS_PWRCLK,
) -> (Self, Leds, Eeprom, EthernetPins, USB) {
) -> (Self, Leds, Eeprom, EthernetPins, USB, Option<FanPin>, HWRev, HWSettings) {
let gpioa = gpioa.split();
let gpiob = gpiob.split();
let gpioc = gpioc.split();
@ -142,13 +160,17 @@ impl Pins {
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();
let _ = 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();
@ -168,7 +190,7 @@ impl Pins {
);
let mut shdn1 = gpioe.pe15.into_push_pull_output();
let _ = 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();
@ -192,15 +214,13 @@ impl Pins {
let leds = Leds::new(gpiod.pd9, gpiod.pd10.into_push_pull_output(), gpiod.pd11.into_push_pull_output());
let eeprom_scl = gpiob.pb8.into_alternate_af4().set_open_drain();
let eeprom_sda = gpiob.pb9.into_alternate_af4().set_open_drain();
let eeprom_i2c = I2c::i2c1(i2c1, (eeprom_scl, eeprom_sda), 400.khz(), clocks);
let eeprom_scl = gpiob.pb8.into_alternate().set_open_drain();
let eeprom_sda = gpiob.pb9.into_alternate().set_open_drain();
let eeprom_i2c = I2c::new(i2c1, (eeprom_scl, eeprom_sda), 400.khz(), clocks);
let eeprom = Eeprom24x::new_24x02(eeprom_i2c, eeprom24x::SlaveAddr::default());
let eth_pins = EthPins {
ref_clk: gpioa.pa1,
md_io: gpioa.pa2,
md_clk: gpioc.pc1,
crs: gpioa.pa7,
tx_en: gpiob.pb11,
tx_d0: gpiog.pg13,
@ -213,12 +233,16 @@ impl Pins {
usb_global: otg_fs_global,
usb_device: otg_fs_device,
usb_pwrclk: otg_fs_pwrclk,
pin_dm: gpioa.pa11.into_alternate_af10(),
pin_dp: gpioa.pa12.into_alternate_af10(),
pin_dm: gpioa.pa11.into_alternate(),
pin_dp: gpioa.pa12.into_alternate(),
hclk: clocks.hclk(),
};
(pins, leds, eeprom, eth_pins, usb)
let fan = if hw_settings.fan_available {
Some(Timer::new(tim8, &clocks).pwm(gpioc.pc9.into_alternate(), hw_settings.fan_pwm_freq_hz.hz()))
} else { None };
(pins, leds, eeprom, eth_pins, usb, fan, hwrev, hw_settings)
}
/// Configure the GPIO pins for SPI operation, and initialize SPI
@ -230,14 +254,14 @@ impl Pins {
mosi: PB15<M3>,
) -> AdcSpi
{
let sck = sck.into_alternate_af5();
let miso = miso.into_alternate_af5();
let mosi = mosi.into_alternate_af5();
Spi::spi2(
let sck = sck.into_alternate();
let miso = miso.into_alternate();
let mosi = mosi.into_alternate();
Spi::new(
spi2,
(sck, miso, mosi),
crate::ad7172::SPI_MODE,
crate::ad7172::SPI_CLOCK.into(),
crate::ad7172::SPI_CLOCK,
clocks
)
}
@ -246,13 +270,13 @@ impl Pins {
clocks: Clocks, spi4: SPI4,
sclk: PE2<M1>, sync: PE4<M2>, sdin: PE6<M3>
) -> (Dac0Spi, <Channel0 as ChannelPins>::DacSync) {
let sclk = sclk.into_alternate_af5();
let sdin = sdin.into_alternate_af5();
let spi = Spi::spi4(
let sclk = sclk.into_alternate();
let sdin = sdin.into_alternate();
let spi = Spi::new(
spi4,
(sclk, NoMiso, sdin),
(sclk, NoMiso {}, sdin),
crate::ad5680::SPI_MODE,
crate::ad5680::SPI_CLOCK.into(),
crate::ad5680::SPI_CLOCK,
clocks
);
let sync = sync.into_push_pull_output();
@ -264,13 +288,13 @@ impl Pins {
clocks: Clocks, spi5: SPI5,
sclk: PF7<M1>, sync: PF6<M2>, sdin: PF9<M3>
) -> (Dac1Spi, <Channel1 as ChannelPins>::DacSync) {
let sclk = sclk.into_alternate_af5();
let sdin = sdin.into_alternate_af5();
let spi = Spi::spi5(
let sclk = sclk.into_alternate();
let sdin = sdin.into_alternate();
let spi = Spi::new(
spi5,
(sclk, NoMiso, sdin),
(sclk, NoMiso {}, sdin),
crate::ad5680::SPI_MODE,
crate::ad5680::SPI_CLOCK.into(),
crate::ad5680::SPI_CLOCK,
clocks
);
let sync = sync.into_push_pull_output();
@ -307,21 +331,22 @@ impl PwmPins {
pin.enable();
}
let channels = (
max_v0.into_alternate_af2(),
max_v1.into_alternate_af2(),
max_v0.into_alternate(),
max_v1.into_alternate(),
);
let (mut max_v0, mut max_v1) = pwm::tim3(tim3, channels, clocks, freq);
//let (mut max_v0, mut max_v1) = pwm::tim3(tim3, channels, clocks, freq);
let (mut max_v0, mut max_v1) = Timer::new(tim3, &clocks).pwm(channels, freq);
init_pwm_pin(&mut max_v0);
init_pwm_pin(&mut max_v1);
let channels = (
max_i_pos0.into_alternate_af1(),
max_i_pos1.into_alternate_af1(),
max_i_neg0.into_alternate_af1(),
max_i_neg1.into_alternate_af1(),
max_i_pos0.into_alternate(),
max_i_pos1.into_alternate(),
max_i_neg0.into_alternate(),
max_i_neg1.into_alternate(),
);
let (mut max_i_pos0, mut max_i_pos1, mut max_i_neg0, mut max_i_neg1) =
pwm::tim1(tim1, channels, clocks, freq);
Timer::new(tim1, &clocks).pwm(channels, freq);
init_pwm_pin(&mut max_i_pos0);
init_pwm_pin(&mut max_i_neg0);
init_pwm_pin(&mut max_i_pos1);

View File

@ -1,17 +1,29 @@
use core::mem::MaybeUninit;
use smoltcp::{
iface::EthernetInterface,
socket::{SocketSet, SocketHandle, TcpSocket, TcpSocketBuffer, SocketRef},
time::Instant,
wire::{IpCidr, Ipv4Address, Ipv4Cidr},
wire::{IpAddress, IpCidr, Ipv4Address, Ipv4Cidr},
};
use crate::command_parser::Ipv4Config;
use crate::net::split_ipv4_config;
pub struct SocketState<S> {
handle: SocketHandle,
state: S,
}
impl<'a, S: Default> SocketState<S>{
fn new(sockets: &mut SocketSet<'a>, tcp_rx_storage: &'a mut [u8; TCP_RX_BUFFER_SIZE], tcp_tx_storage: &'a mut [u8; TCP_TX_BUFFER_SIZE]) -> SocketState<S> {
let tcp_rx_buffer = TcpSocketBuffer::new(&mut tcp_rx_storage[..]);
let tcp_tx_buffer = TcpSocketBuffer::new(&mut tcp_tx_storage[..]);
let tcp_socket = TcpSocket::new(tcp_rx_buffer, tcp_tx_buffer);
SocketState::<S> {
handle: sockets.add(tcp_socket),
state: S::default()
}
}
}
/// Number of server sockets and therefore concurrent client
/// sessions. Many data structures in `Server::run()` correspond to
/// this const.
@ -23,39 +35,38 @@ const TCP_TX_BUFFER_SIZE: usize = 2048;
/// Contains a number of server sockets that get all sent the same
/// data (through `fmt::Write`).
pub struct Server<'a, 'b, S> {
net: EthernetInterface<'a, 'a, 'a, &'a mut stm32_eth::Eth<'static, 'static>>,
sockets: SocketSet<'b, 'b, 'b>,
net: EthernetInterface<'a, &'a mut stm32_eth::Eth<'static, 'static>>,
sockets: SocketSet<'b>,
states: [SocketState<S>; SOCKET_COUNT],
}
impl<'a, 'b, S: Default> Server<'a, 'b, S> {
/// Run a server with stack-allocated sockets
pub fn run<F>(net: EthernetInterface<'a, 'a, 'a, &'a mut stm32_eth::Eth<'static, 'static>>, f: F)
pub fn run<F>(net: EthernetInterface<'a, &'a mut stm32_eth::Eth<'static, 'static>>, f: F)
where
F: FnOnce(&mut Server<'a, '_, S>),
{
let mut sockets_storage: [_; SOCKET_COUNT] = Default::default();
let mut sockets = SocketSet::new(&mut sockets_storage[..]);
let mut states: [SocketState<S>; SOCKET_COUNT] = unsafe { MaybeUninit::uninit().assume_init() };
macro_rules! create_socket {
($set:ident, $rx_storage:ident, $tx_storage:ident, $target:expr) => {
macro_rules! create_rtx_storage {
($rx_storage:ident, $tx_storage:ident) => {
let mut $rx_storage = [0; TCP_RX_BUFFER_SIZE];
let mut $tx_storage = [0; TCP_TX_BUFFER_SIZE];
let tcp_rx_buffer = TcpSocketBuffer::new(&mut $rx_storage[..]);
let tcp_tx_buffer = TcpSocketBuffer::new(&mut $tx_storage[..]);
let tcp_socket = TcpSocket::new(tcp_rx_buffer, tcp_tx_buffer);
$target = $set.add(tcp_socket);
}
}
create_socket!(sockets, tcp_rx_storage0, tcp_tx_storage0, states[0].handle);
create_socket!(sockets, tcp_rx_storage1, tcp_tx_storage1, states[1].handle);
create_socket!(sockets, tcp_rx_storage2, tcp_tx_storage2, states[2].handle);
create_socket!(sockets, tcp_rx_storage3, tcp_tx_storage3, states[3].handle);
for state in &mut states {
state.state = S::default();
}
create_rtx_storage!(tcp_rx_storage0, tcp_tx_storage0);
create_rtx_storage!(tcp_rx_storage1, tcp_tx_storage1);
create_rtx_storage!(tcp_rx_storage2, tcp_tx_storage2);
create_rtx_storage!(tcp_rx_storage3, tcp_tx_storage3);
let mut sockets_storage: [_; SOCKET_COUNT] = Default::default();
let mut sockets = SocketSet::new(&mut sockets_storage[..]);
let states: [SocketState<S>; SOCKET_COUNT] = [
SocketState::<S>::new(&mut sockets, &mut tcp_rx_storage0, &mut tcp_tx_storage0),
SocketState::<S>::new(&mut sockets, &mut tcp_rx_storage1, &mut tcp_tx_storage1),
SocketState::<S>::new(&mut sockets, &mut tcp_rx_storage2, &mut tcp_tx_storage2),
SocketState::<S>::new(&mut sockets, &mut tcp_rx_storage3, &mut tcp_tx_storage3),
];
let mut server = Server {
states,
@ -85,12 +96,12 @@ impl<'a, 'b, S: Default> Server<'a, 'b, S> {
}
}
pub fn set_ipv4_address(&mut self, ipv4_address: Ipv4Address) {
fn set_ipv4_address(&mut self, ipv4_address: Ipv4Cidr) {
self.net.update_ip_addrs(|addrs| {
for addr in addrs.iter_mut() {
match addr {
IpCidr::Ipv4(_) => {
*addr = IpCidr::Ipv4(Ipv4Cidr::new(ipv4_address, 0));
*addr = IpCidr::Ipv4(ipv4_address);
// done
break
}
@ -101,4 +112,23 @@ impl<'a, 'b, S: Default> Server<'a, 'b, S> {
}
});
}
fn set_gateway(&mut self, gateway: Option<Ipv4Address>) {
let routes = self.net.routes_mut();
match gateway {
None =>
routes.update(|routes_storage| {
routes_storage.remove(&IpCidr::new(IpAddress::v4(0, 0, 0, 0), 0));
}),
Some(gateway) => {
routes.add_default_ipv4_route(gateway).unwrap();
}
}
}
pub fn set_ipv4_config(&mut self, config: Ipv4Config) {
let (address, gateway) = split_ipv4_config(config);
self.set_ipv4_address(address);
self.set_gateway(gateway);
}
}

View File

@ -8,12 +8,10 @@ use uom::si::{
ratio::ratio,
thermodynamic_temperature::{degree_celsius, kelvin},
};
use serde::Serialize;
type JsonBuffer = heapless::Vec<u8, heapless::consts::U200>;
use serde::{Deserialize, Serialize};
/// Steinhart-Hart equation parameters
#[derive(Clone, Debug, Serialize)]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Parameters {
/// Base temperature
pub t0: ThermodynamicTemperature,
@ -29,10 +27,6 @@ impl Parameters {
let inv_temp = 1.0 / self.t0.get::<kelvin>() + (r / self.r0).get::<ratio>().ln() / self.b;
ThermodynamicTemperature::new::<kelvin>(1.0 / inv_temp)
}
pub fn to_json(&self) -> Result<JsonBuffer, serde_json_core::ser::Error> {
serde_json_core::to_vec(self)
}
}
impl Default for Parameters {

View File

@ -18,8 +18,10 @@ static TIMER_MS: Mutex<RefCell<u32>> = Mutex::new(RefCell::new(0));
/// Setup SysTick exception
pub fn setup(syst: SYST, clocks: Clocks) {
let mut timer = Timer::syst(syst, TIMER_RATE.hz(), clocks);
timer.listen(TimerEvent::TimeOut);
let timer = Timer::syst(syst, &clocks);
let mut countdown = timer.start_count_down(TIMER_RATE.hz());
countdown.listen(TimerEvent::TimeOut);
}
/// SysTick exception (Timer)