Compare commits

..

1 Commits

Author SHA1 Message Date
Astro f323c1be63 migrate ad5680 to softspi 2020-05-27 23:16:22 +02:00
39 changed files with 985 additions and 3252 deletions

301
Cargo.lock generated
View File

@ -2,30 +2,29 @@
# It is not intended for manual editing.
[[package]]
name = "aligned"
version = "0.3.4"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c19796bd8d477f1a9d4ac2465b464a8b1359474f06a96bb3cda650b4fca309bf"
checksum = "eb1ce8b3382016136ab1d31a1b5ce807144f8b7eb2d5f16b2108f0f07edceb94"
dependencies = [
"as-slice",
]
[[package]]
name = "as-slice"
version = "0.1.4"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb4d1c23475b74e3672afa8c2be22040b8b7783ad9b461021144ed10a46bb0e6"
checksum = "37dfb65bc03b2bc85ee827004f14a6817e04160e3b1a28931986a666a9290e70"
dependencies = [
"generic-array 0.12.3",
"generic-array 0.13.2",
"generic-array 0.14.4",
"stable_deref_trait",
]
[[package]]
name = "autocfg"
version = "1.0.1"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d"
[[package]]
name = "bare-metal"
@ -36,23 +35,11 @@ dependencies = [
"rustc_version",
]
[[package]]
name = "bare-metal"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fe8f5a8a398345e52358e18ff07cc17a568fbca5c6f73873d3a62056309603"
[[package]]
name = "bit_field"
version = "0.10.1"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcb6dd1c2376d2e096796e234a70e17e94cc2d5d54ff8ce42b28cef1d0d359a4"
[[package]]
name = "bitfield"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46afbd2983a5d5a7bd740ccb198caf5b82f45c40c09c0eed36052d91cb92e719"
checksum = "a165d606cf084741d4ac3a28fb6e9b1eb0bd31f6cd999098cfddb0b2ab381dc0"
[[package]]
name = "bitflags"
@ -83,21 +70,20 @@ checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
[[package]]
name = "cortex-m"
version = "0.6.4"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88cdafeafba636c00c467ded7f1587210725a1adfab0c24028a7844b87738263"
checksum = "2954942fbbdd49996704e6f048ce57567c3e1a4e2dc59b41ae9fde06a01fc763"
dependencies = [
"aligned",
"bare-metal 0.2.5",
"bitfield",
"bare-metal",
"volatile-register",
]
[[package]]
name = "cortex-m-log"
version = "0.6.2"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d63959cb1e003dd97233fee6762351540253237eadf06fcdcb98cbfa3f9be4a"
checksum = "978caafe65d1023d38b00c76b83564788fc351d954a5005fb72cf992c0d61458"
dependencies = [
"cortex-m",
"cortex-m-semihosting",
@ -106,9 +92,9 @@ dependencies = [
[[package]]
name = "cortex-m-rt"
version = "0.6.13"
version = "0.6.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "980c9d0233a909f355ed297ef122f257942de5e0a2cb1c39f60684b65bcb90fb"
checksum = "00d518da72bba39496024b62607c1d8e37bcece44b2536664f1132a73a499a28"
dependencies = [
"cortex-m-rt-macros",
"r0",
@ -127,38 +113,20 @@ dependencies = [
[[package]]
name = "cortex-m-semihosting"
version = "0.3.7"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bffa6c1454368a6aa4811ae60964c38e6996d397ff8095a8b9211b1c1f749bc"
checksum = "113ef0ecffee2b62b58f9380f4469099b30e9f9cbee2804771b4203ba1762cfa"
dependencies = [
"cortex-m",
]
[[package]]
name = "eeprom24x"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f680e8d81a559a97de04c5fab25f17f22a55770120c868ef8fbdea6398d44107"
dependencies = [
"embedded-hal",
]
[[package]]
name = "embedded-dma"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46c8c02e4347a0267ca60813c952017f4c5948c232474c6010a381a337f1bda4"
dependencies = [
"stable_deref_trait",
]
[[package]]
name = "embedded-hal"
version = "0.2.4"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa998ce59ec9765d15216393af37a58961ddcefb14c753b4816ba2191d865fcb"
checksum = "ee4908a155094da7723c2d60d617b820061e3b4efcc3d9e293d206a5a76c170b"
dependencies = [
"nb 0.1.3",
"nb",
"void",
]
@ -181,36 +149,10 @@ dependencies = [
]
[[package]]
name = "generic-array"
version = "0.14.4"
name = "hash2hwaddr"
version = "0.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "hash32"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4041af86e63ac4298ce40e5cca669066e75b6f1aa3390fe2561ffa5e1d9f4cc"
dependencies = [
"byteorder",
]
[[package]]
name = "heapless"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74911a68a1658cfcfb61bc0ccfbd536e3b6e906f8c2f7883ee50157e3e2184f1"
dependencies = [
"as-slice",
"generic-array 0.13.2",
"hash32",
"serde",
"stable_deref_trait",
]
checksum = "857afb5ee9e767c3a73b2ad7212b6deea0c3761a27db1e20ea0ed57ee352cfef"
[[package]]
name = "libm"
@ -220,45 +162,36 @@ checksum = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a"
[[package]]
name = "log"
version = "0.4.11"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b"
checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7"
dependencies = [
"cfg-if",
]
[[package]]
name = "managed"
version = "0.7.2"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c75de51135344a4f8ed3cfe2720dc27736f7711989703a0b43aadf3753c55577"
checksum = "fdcec5e97041c7f0f1c5b7d93f12e57293c831c646f4cc7a5db59460c7ea8de6"
[[package]]
name = "memchr"
version = "2.3.4"
version = "2.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525"
checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400"
[[package]]
name = "nb"
version = "0.1.3"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "801d31da0513b6ec5214e9bf433a77966320625a37860f910be265be6e18d06f"
dependencies = [
"nb 1.0.0",
]
[[package]]
name = "nb"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "546c37ac5d9e56f55e73b677106873d9d9f5190605e41a856503623648488cae"
checksum = "b1411551beb3c11dedfb0a90a0fa256b47d28b9ec2cdff34c25a2fa59e45dbdc"
[[package]]
name = "nom"
version = "5.1.2"
version = "5.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af"
checksum = "0b471253da97532da4b61552249c521e01e736071f71c1a4f7ebbfbf0a06aad6"
dependencies = [
"memchr",
"version_check",
@ -266,9 +199,9 @@ dependencies = [
[[package]]
name = "num-traits"
version = "0.2.14"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
checksum = "c62be47e61d1842b9170f0fdeec8eba98e60e90e5446449a0545e5152acd7096"
dependencies = [
"autocfg",
"libm",
@ -282,45 +215,28 @@ checksum = "4e20e6499bbbc412f280b04a42346b356c6fa0753d5fd22b7bd752ff34c778ee"
[[package]]
name = "panic-semihosting"
version = "0.5.6"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d55dedd501dfd02514646e0af4d7016ce36bc12ae177ef52056989966a1eec"
checksum = "c03864ac862876c16a308f5286f4aa217f1a69ac45df87ad3cd2847f818a642c"
dependencies = [
"cortex-m",
"cortex-m-semihosting",
]
[[package]]
name = "postcard"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3e3f5c2e9a91383c6594ec68aa2dfdfe19a3c86f34b088ba7203f2483d2682f"
dependencies = [
"heapless",
"postcard-cobs",
"serde",
]
[[package]]
name = "postcard-cobs"
version = "0.1.5-pre"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c68cb38ed13fd7bc9dd5db8f165b7c8d9c1a315104083a2b10f11354c2af97f"
[[package]]
name = "proc-macro2"
version = "1.0.24"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71"
checksum = "6c09721c6781493a2a492a96b5a5bf19b65917fe6728884e7c44dd0c60ca3435"
dependencies = [
"unicode-xid",
]
[[package]]
name = "quote"
version = "1.0.7"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37"
checksum = "2bdc6c187c65bca4260c9011c9e3132efe4909da44726bad24cf7572ae338d7f"
dependencies = [
"proc-macro2",
]
@ -361,47 +277,6 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
[[package]]
name = "serde"
version = "1.0.118"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06c64263859d87aa2eb554587e2d23183398d617427327cf2b3d0ed8c69e4800"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde-json-core"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbf406405ada9ef326ca78677324ac66994ff348fc48a16030be08caeed29825"
dependencies = [
"heapless",
"serde",
]
[[package]]
name = "serde_derive"
version = "1.0.118"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c84d3526699cd55261af4b941e4e725444df67aa4f9e6a3564f18030d12672df"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "sfkv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25f5bfac3f66a7c10a6f37ee81aeaa471f4d35dc21665b59ad7c555adcb9e8aa"
dependencies = [
"byteorder",
"postcard",
"serde",
]
[[package]]
name = "smoltcp"
version = "0.6.0"
@ -416,17 +291,17 @@ dependencies = [
[[package]]
name = "stable_deref_trait"
version = "1.2.0"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
checksum = "dba1a27d3efae4351c8051072d619e3ade2820635c3958d826bfea39d59b54c8"
[[package]]
name = "stm32-eth"
version = "0.2.0"
source = "git+https://github.com/stm32-rs/stm32-eth.git#4d6b29bf1ecdd1f68e5bc304a3d4f170049896c8"
version = "0.1.2"
source = "git+https://github.com/stm32-rs/stm32-eth.git#2c5dce379b85a31fb0b9c58a028b6454be1727aa"
dependencies = [
"aligned",
"cortex-m",
"log",
"smoltcp",
"stm32f4xx-hal",
"volatile-register",
@ -434,11 +309,11 @@ dependencies = [
[[package]]
name = "stm32f4"
version = "0.11.0"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11460b4de3a84f072e2cf6e76306c64d27f405a0e83bace0a726f555ddf4bf33"
checksum = "44a3d6c58b14e63926273694e7dd644894513c5e35ce6928c4657ddb62cae976"
dependencies = [
"bare-metal 0.2.5",
"bare-metal",
"cortex-m",
"cortex-m-rt",
"vcell",
@ -446,112 +321,64 @@ dependencies = [
[[package]]
name = "stm32f4xx-hal"
version = "0.8.3"
source = "git+https://github.com/astro/stm32f4xx-hal.git?branch=flash#9171ef176a90b1177f350fe2bc1eac625769a041"
version = "0.7.0"
source = "git+https://github.com/thalesfragoso/stm32f4xx-hal?branch=pwm-impl#cfd073e094daa9be9dd2b0a1f859a4e1c6be2b77"
dependencies = [
"bare-metal 0.2.5",
"bare-metal",
"cast",
"cortex-m",
"cortex-m-rt",
"embedded-dma",
"embedded-hal",
"nb 0.1.3",
"nb",
"rand_core",
"stm32f4",
"synopsys-usb-otg",
"void",
]
[[package]]
name = "syn"
version = "1.0.54"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2af957a63d6bd42255c359c93d9bfdb97076bd3b820897ce55ffbfbf107f44"
checksum = "0df0eb663f387145cab623dea85b09c2c5b4b0aef44e945d928e682fce71bb03"
dependencies = [
"proc-macro2",
"quote",
"unicode-xid",
]
[[package]]
name = "synopsys-usb-otg"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "461676dcf123675b3d3b02e2390e6a690cd186aacf2f439af7673c79e2561d53"
dependencies = [
"cortex-m",
"usb-device",
"vcell",
]
[[package]]
name = "thermostat"
version = "0.0.0"
dependencies = [
"bare-metal 1.0.0",
"bare-metal",
"bit_field",
"byteorder",
"cortex-m",
"cortex-m-log",
"cortex-m-rt",
"eeprom24x",
"heapless",
"hash2hwaddr",
"log",
"nb 1.0.0",
"nb",
"nom",
"num-traits",
"panic-abort",
"panic-semihosting",
"serde",
"serde-json-core",
"sfkv",
"smoltcp",
"stm32-eth",
"stm32f4xx-hal",
"uom",
"usb-device",
"usbd-serial",
]
[[package]]
name = "typenum"
version = "1.12.0"
version = "1.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33"
checksum = "6d2783fe2d6b8c1101136184eb41be8b1ad379e4657050b8aaff0c79ee7575f9"
[[package]]
name = "unicode-xid"
version = "0.2.1"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
[[package]]
name = "uom"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e76503e636584f1e10b9b3b9498538279561adcef5412927ba00c2b32c4ce5ed"
dependencies = [
"num-traits",
"serde",
"typenum",
]
[[package]]
name = "usb-device"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "849eed9b4dc61a1f17ba1d7a5078ceb095b9410caa38a506eb281ed5eff12fbd"
[[package]]
name = "usbd-serial"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db75519b86287f12dcf0d171c7cf4ecc839149fe9f3b720ac4cfce52959e1dfe"
dependencies = [
"embedded-hal",
"nb 0.1.3",
"usb-device",
]
checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c"
[[package]]
name = "vcell"
@ -561,9 +388,9 @@ checksum = "876e32dcadfe563a4289e994f7cb391197f362b6315dc45e8ba4aa6f564a4b3c"
[[package]]
name = "version_check"
version = "0.9.2"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed"
checksum = "078775d0255232fb988e6fccf26ddc9d1ac274299aaedcedce21c6f72cc533ce"
[[package]]
name = "void"

View File

@ -14,36 +14,31 @@ features = []
default-target = "thumbv7em-none-eabihf"
[dependencies]
panic-abort = "0.3"
panic-semihosting = { version = "0.5", optional = true }
panic-abort = "0.3.1"
panic-semihosting = { version = "0.5.1", optional = true }
log = "0.4"
bare-metal = "1"
bare-metal = "0.2"
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" }
stm32f4xx-hal = { version = "0.7", features = ["rt", "stm32f427"] }
stm32-eth = { version = "0.1.2", features = ["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"] }
hash2hwaddr = { version = "0.0", optional = true }
bit_field = "0.10"
byteorder = { version = "1", default-features = false }
nom = { version = "5", default-features = false }
num-traits = { version = "0.2", default-features = false, features = ["libm"] }
usb-device = "0.2"
usbd-serial = "0.1"
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"] }
heapless = "0.5"
serde-json-core = "0.1"
sfkv = "0.1"
nb = "0.1"
[patch.crates-io]
# TODO: pending https://github.com/stm32-rs/stm32f4xx-hal/pull/239
stm32f4xx-hal = { git = "https://github.com/astro/stm32f4xx-hal.git", branch = "flash" }
# TODO: pending https://github.com/stm32-rs/stm32f4xx-hal/pull/125
stm32f4xx-hal = { git = "https://github.com/thalesfragoso/stm32f4xx-hal", branch = "pwm-impl" }
[features]
semihosting = ["panic-semihosting", "cortex-m-log/semihosting"]
generate-hwaddr = ["hash2hwaddr"]
default = ["generate-hwaddr"]
[profile.release]
codegen-units = 1

268
README.md
View File

@ -1,69 +1,26 @@
# Firmware for the Sinara 8451 Thermostat
- [x] [Continuous Integration](https://nixbld.m-labs.hk/job/mcu/mcu/thermostat)
- [x] Download latest firmware build: [ELF](https://nixbld.m-labs.hk/job/mcu/mcu/thermostat/latest/download/1) [BIN](https://nixbld.m-labs.hk/job/mcu/mcu/thermostat/latest/download/2)
- [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)
## Building
### Reproducible build with Nix
### Debian-based systems (tested on Ubuntu 19.10)
See the `mcu` folder of the [nix-scripts repository](https://git.m-labs.hk/M-Labs/nix-scripts).
### Development environment
Clone this repository and [nix-scripts](https://git.m-labs.hk/M-Labs/nix-scripts).
- install git, clone this repository
- install [rustup](https://rustup.rs/)
```shell
nix-shell -I nix-scripts=[path to nix-scripts checkout]
rustup toolchain install nightly
rustup update
rustup target add thumbv7em-none-eabihf --toolchain nightly
rustup default nightly
cargo build --release
```
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 channel manifest file in nix-scripts (`channel-rust-nightly.toml`) 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-v2-1.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-v2-1.cfg -f target/stm32f4x.cfg -c "program target/thumbv7em-none-eabihf/release/thermostat verify reset;exit"
```
## Network
@ -73,7 +30,7 @@ Ethernet, IP: 192.168.1.26/24
Use netcat to connect to port 23/tcp (telnet)
```sh
rlwrap nc -vv 192.168.1.26 23
nc -vv 192.168.1.26 23
```
telnet clients send binary data after connect. Enter \n once to
@ -87,185 +44,28 @@ Set report mode to `on` for a continuous stream of input data.
The scope of this setting is per TCP session.
### TCP commands
### Commands
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 |
| `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 [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 |
## USB
The firmware includes experimental support for acting as a USB-Serial
peripheral. Debug logging will be sent there by default (unless build
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 when USB buffers are full.
## Temperature measurement
Connect the thermistor with the SENS pins of the
device. Temperature-depending resistance is measured by the AD7172
ADC. To prepare conversion to a temperature, set the Beta parameters
for the Steinhart-Hart equation.
Set the base temperature in degrees celsius for the channel 0 thermistor:
```
s-h 0 t0 20
```
Set the resistance in Ohms measured at the base temperature t0:
```
s-h 0 r0 10000
```
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 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 heat up with a positive software current set point, and cool down with a negative current set point.
Testing heat flow direction with a low set current is recommended before installation of the TEC module.
### Limits
Each of the MAX1968 TEC driver has analog/PWM inputs for setting
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 |
| `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
the `pwm` command. Doing so will disengage the PID control for that
channel.
Example: set output current of channel 0 to 0 A.
```
pwm 0 i_set 0
```
## PID-stabilized temperature control
Set the target temperature of channel 0 to 20 degrees celsius:
```
pid 0 target 20
```
Enter closed-loop mode by switching control of the TEC output current
of channel 0 to the PID algorithm:
```
pwm 0 pid
```
## LED indicators
| Name | Color | Meaning |
| --- | :---: | --- |
| L1 | Red | Firmware initializing |
| L3 | Green | Closed-loop mode (PID engaged) |
| L4 | Green | Firmware busy |
## Reports
Use the bare `report` command to obtain a single report. Enable
continuous reporting with `report mode on`. Reports are JSON objects
with the following keys.
| Key | Unit | Description |
| --- | :---: | --- |
| `channel` | Integer | Channel `0`, or `1` |
| `time` | Milliseconds | 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 |
## 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).
| 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 *ratio* |
| `pwm <0/1> max_i_neg <ratio>` | Set PWM duty cycle for **max_i_neg** to *ratio* |
| `pwm <0/1> max_v <ratio>` | Set PWM duty cycle for **max_v** to *ratio* |
| `pwm <0/1> <volts>` | Disengage PID, set **i_set** DAC to *volts* |
| `pwm <0/1> pid` | Set PWM to be controlled by PID |
| `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> <t/b/r0> <value>` | Set Steinhart-Hart parameter for a channel |
| `postfilter <0/1> rate <rate>` | Set postfilter output data rate |

View File

@ -1 +1 @@
"0qb4s06jwgj3i9df6qq9gwcnyr3jq6dh4l5ygjghq5x1bmcqliix"
"0ma8dxsw90jrbxb3cd873k98g3pixnqvb059blvg7kf4m5aj9fnq"

View File

@ -1,81 +0,0 @@
# 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)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

View File

@ -1,17 +1,10 @@
MEMORY
{
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
FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 2048K
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 112K
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);

View File

@ -1,262 +0,0 @@
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 = 30
# 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

@ -1,11 +0,0 @@
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

@ -1,128 +0,0 @@
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from threading import Thread, Lock
from pytec.client import Client
TIME_WINDOW = 300.0
tec = Client()
target_temperature = tec.get_pid()[0]['target']
print("Channel 0 target temperature: {:.3f}".format(target_temperature))
class Series:
def __init__(self, conv=lambda x: x):
self.conv = conv
self.x_data = []
self.y_data = []
def append(self, x, y):
self.x_data.append(x)
self.y_data.append(self.conv(y))
def clip(self, min_x):
drop = 0
while drop < len(self.x_data) and self.x_data[drop] < min_x:
drop += 1
self.x_data = self.x_data[drop:]
self.y_data = self.y_data[drop:]
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(),
'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():
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
thread = Thread(target=recv_data, args=(tec,))
thread.start()
fig, ax = plt.subplots()
for k, s in series.items():
s.plot, = ax.plot([], [], label=k)
legend = ax.legend()
def animate(i):
min_x, max_x, min_y, max_y = None, None, None, None
series_lock.acquire()
try:
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]))
if len(s.x_data) > 0:
min_x_ = min(s.x_data)
if min_x is None:
min_x = min_x_
else:
min_x = min(min_x, min_x_)
max_x_ = max(s.x_data)
if max_x is None:
max_x = max_x_
else:
max_x = max(max_x, max_x_)
if len(s.y_data) > 0:
min_y_ = min(s.y_data)
if min_y is None:
min_y = min_y_
else:
min_y = min(min_y, min_y_)
max_y_ = max(s.y_data)
if max_y is None:
max_y = max_y_
else:
max_y = max(max_y, max_y_)
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()
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()
legend = ax.legend()
ani = animation.FuncAnimation(
fig, animate, interval=1, blit=False, save_count=50)
plt.show()
quit = True
thread.join()

View File

@ -1,166 +0,0 @@
import socket
import json
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 = [""]
def _read_line(self):
# read more lines
while len(self._lines) <= 1:
chunk = self._socket.recv(4096)
if not chunk:
return None
buf = self._lines[-1] + chunk.decode('utf-8', errors='ignore')
self._lines = buf.split("\n")
line = self._lines[0]
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):
result = [None, None]
for item in self._command(topic):
result[int(item["channel"])] = item
return result
def get_pwm(self):
"""Retrieve PWM limits for the TEC
Example::
[{'channel': 0,
'center': 'vref',
'i_set': {'max': 2.9802790335151985, 'value': -0.02002179650216762},
'max_i_neg': {'max': 3.0, 'value': 3.0},
'max_v': {'max': 5.988, 'value': 5.988},
'max_i_pos': {'max': 3.0, 'value': 3.0}},
{'channel': 1,
'center': 'vref',
'i_set': {'max': 2.9802790335151985, 'value': -0.02002179650216762},
'max_i_neg': {'max': 3.0, 'value': 3.0},
'max_v': {'max': 5.988, 'value': 5.988},
'max_i_pos': {'max': 3.0, 'value': 3.0}}
]
"""
return self._get_conf("pwm")
def get_pid(self):
"""Retrieve PID control state
Example::
[{'channel': 0,
'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': 37.0,
'integral': 38.41138597026372},
{'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}]
"""
return self._get_conf("pid")
def get_steinhart_hart(self):
"""Retrieve Steinhart-Hart parameters for resistance to temperature conversion
Example::
[{'params': {'b': 3800.0, 'r0': 10000.0, 't0': 298.15}, 'channel': 0},
{'params': {'b': 3800.0, 'r0': 10000.0, 't0': 298.15}, 'channel': 1}]
"""
return self._get_conf("s-h")
def get_postfilter(self):
"""Retrieve DAC postfilter configuration
Example::
[{'rate': None, 'channel': 0},
{'rate': 21.25, 'channel': 1}]
"""
return self._get_conf("postfilter")
def report_mode(self):
"""Start reporting measurement values
Example of yielded data::
{'channel': 0,
'time': 2302524,
'adc': 0.6199188965423515,
'sens': 6138.519310282602,
'temperature': 36.87032392655527,
'pid_engaged': True,
'i_set': 2.0635816680889123,
'vref': 1.494,
'dac_value': 2.527790834044456,
'dac_feedback': 2.523,
'i_tec': 2.331,
'tec_i': 2.0925,
'tec_u_meas': 2.5340000000000003,
'pid_output': 2.067581958092247}
"""
self._command("report mode", "on")
while True:
line = self._read_line()
if not line:
break
try:
yield json.loads(line)
except json.decoder.JSONDecodeError:
pass
def set_param(self, topic, channel, field="", value=""):
"""Set configuration parameters
Examples::
tec.set_param("pwm", 0, "max_v", 2.0)
tec.set_param("pid", 1, "output_max", 2.5)
tec.set_param("s-h", 0, "t0", 20.0)
tec.set_param("center", 0, "vref")
tec.set_param("postfilter", 1, 21)
See the firmware's README.md for a full list.
"""
if type(value) is float:
value = "{:f}".format(value)
if type(value) is not str:
value = str(value)
self._command(topic, str(channel), field, value)
def power_up(self, channel, target):
"""Start closed-loop mode"""
self.set_param("pid", channel, "target", value=target)
self.set_param("pwm", channel, "pid")
def save_config(self):
"""Save current configuration to EEPROM"""
self._command("save")
def load_config(self):
"""Load current configuration from EEPROM"""
self._command("load")

View File

@ -1,12 +0,0 @@
from setuptools import setup, find_packages
setup(
name="pytec",
version="0.0",
author="M-Labs",
url="https://git.m-labs.hk/M-Labs/thermostat",
description="Control TEC",
license="GPLv3",
install_requires=["setuptools"],
packages=find_packages(),
)

View File

@ -1,23 +0,0 @@
{ mozillaOverlay ? builtins.fetchTarball "https://github.com/mozilla/nixpkgs-mozilla/archive/master.tar.gz",
latestRustNightly ? false,
}:
let
pkgs = import <nixpkgs> {
overlays = [ (import mozillaOverlay) ];
};
rust =
if latestRustNightly
then pkgs.rustChannelOfTargets "nightly" null [ "thumbv7em-none-eabihf" ]
else (pkgs.recurseIntoAttrs (
pkgs.callPackage (import <nix-scripts/mcu/rustPlatform.nix>) {}
)).rust.cargo;
in
pkgs.mkShell {
name = "thermostat-env";
buildInputs = with pkgs; [
rust gcc
openocd dfu-util
] ++ (with python3Packages; [
numpy matplotlib
]);
}

View File

@ -6,7 +6,6 @@ use stm32f4xx_hal::{
time::MegaHertz,
spi,
};
use crate::timer::sleep;
/// SPI Mode 1
pub const SPI_MODE: spi::Mode = spi::Mode {
@ -34,25 +33,23 @@ impl<SPI: Transfer<u8>, S: OutputPin> Dac<SPI, S> {
}
}
fn write(&mut self, buf: &mut [u8]) -> Result<(), SPI::Error> {
fn write(&mut self, mut buf: [u8; 3]) -> Result<(), SPI::Error> {
// pulse sync to start a new transfer. leave sync idle low
// afterwards to save power as recommended per datasheet.
let _ = self.sync.set_high();
// must be high for >= 33 ns
sleep(1);
cortex_m::asm::nop();
let _ = self.sync.set_low();
self.spi.transfer(buf)?;
self.spi.transfer(&mut buf)?;
Ok(())
}
pub fn set(&mut self, value: u32) -> Result<u32, SPI::Error> {
let value = value.min(MAX_VALUE);
let mut buf = [
pub fn set(&mut self, value: u32) -> Result<(), SPI::Error> {
let buf = [
(value >> 14) as u8,
(value >> 6) as u8,
(value << 2) as u8,
];
self.write(&mut buf)?;
Ok(value)
self.write(buf)
}
}

View File

@ -4,10 +4,6 @@ use stm32f4xx_hal::hal::{
blocking::spi::Transfer,
digital::v2::OutputPin,
};
use uom::si::{
f64::ElectricPotential,
electric_potential::volt,
};
use super::{
regs::{self, Register, RegisterData},
checksum::{ChecksumMode, Checksum},
@ -90,8 +86,6 @@ impl<SPI: Transfer<u8, Error = E>, NSS: OutputPin, E: fmt::Debug> Adc<SPI, NSS>
data.set_enh_filt_en(true);
data.set_enh_filt(PostFilter::F16SPS);
data.set_order(DigitalFilterOrder::Sinc5Sinc1);
// output data rate: 10 Hz
data.set_odr(0b10011);
})?;
self.update_reg(&regs::Channel { index }, |data| {
data.set_setup(index);
@ -102,11 +96,45 @@ impl<SPI: Transfer<u8, Error = E>, NSS: OutputPin, E: fmt::Debug> Adc<SPI, NSS>
Ok(())
}
pub fn get_calibration(&mut self, index: u8) -> Result<ChannelCalibration, SPI::Error> {
let offset = self.read_reg(&regs::Offset { index })?.offset();
let gain = self.read_reg(&regs::Gain { index })?.gain();
let bipolar = self.read_reg(&regs::SetupCon { index })?.bipolar();
Ok(ChannelCalibration { offset, gain, bipolar })
pub fn disable_channel(
&mut self, index: u8
) -> Result<(), SPI::Error> {
self.update_reg(&regs::Channel { index }, |data| {
data.set_enabled(false);
})?;
Ok(())
}
pub fn disable_all_channels(&mut self) -> Result<(), SPI::Error> {
for index in 0..4 {
self.update_reg(&regs::Channel { index }, |data| {
data.set_enabled(false);
})?;
}
Ok(())
}
/// Calibrates offset registers
pub fn calibrate(&mut self) -> Result<(), SPI::Error> {
// internal offset calibration
self.update_reg(&regs::AdcMode, |adc_mode| {
adc_mode.set_mode(Mode::InternalOffsetCalibration);
})?;
while ! self.read_reg(&regs::Status)?.ready() {}
// system offset calibration
self.update_reg(&regs::AdcMode, |adc_mode| {
adc_mode.set_mode(Mode::SystemOffsetCalibration);
})?;
while ! self.read_reg(&regs::Status)?.ready() {}
// system gain calibration
self.update_reg(&regs::AdcMode, |adc_mode| {
adc_mode.set_mode(Mode::SystemGainCalibration);
})?;
while ! self.read_reg(&regs::Status)?.ready() {}
Ok(())
}
pub fn start_continuous_conversion(&mut self) -> Result<(), SPI::Error> {
@ -234,7 +262,7 @@ impl<SPI: Transfer<u8, Error = E>, NSS: OutputPin, E: fmt::Debug> Adc<SPI, NSS>
Err(e) => Err(e),
};
let result = match (result, checksum) {
(Ok(_), None) =>
(Ok(_),None) =>
Ok(None),
(Ok(_), Some(checksum_out)) => {
let mut checksum_buf = [checksum_out; 1];
@ -251,26 +279,3 @@ impl<SPI: Transfer<u8, Error = E>, NSS: OutputPin, E: fmt::Debug> Adc<SPI, NSS>
result
}
}
#[derive(Debug, Clone)]
pub struct ChannelCalibration {
offset: u32,
gain: u32,
bipolar: bool,
}
impl ChannelCalibration {
pub fn convert_data(&self, data: u32) -> ElectricPotential {
let data = if self.bipolar {
(data as i32 - 0x80_0000) as f64
} else {
data as f64 / 2.0
};
let data = data / (self.gain as f64 / (0x40_0000 as f64));
let data = data + (self.offset as i32 - 0x80_0000) as f64;
let data = data / (2 << 23) as f64;
const V_REF: f64 = 3.3;
ElectricPotential::new::<volt>(data * V_REF / 0.75)
}
}

View File

@ -1,6 +1,5 @@
use core::fmt;
use num_traits::float::Float;
use serde::{Serialize, Deserialize};
use stm32f4xx_hal::{
time::MegaHertz,
spi,
@ -145,7 +144,7 @@ impl fmt::Display for RefSource {
}
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[derive(Clone, Copy)]
#[repr(u8)]
pub enum PostFilter {
/// 27 SPS, 47 dB rejection, 36.7 ms settling

View File

@ -1,11 +1,6 @@
use stm32f4xx_hal::hal::digital::v2::OutputPin;
use uom::si::{
f64::ElectricPotential,
electric_potential::volt,
};
use crate::{
ad5680,
ad7172,
channel_state::ChannelState,
pins::{ChannelPins, ChannelPinSet},
};
@ -16,13 +11,16 @@ 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>,
/// Measured vref of MAX driver chip
pub vref_meas: ElectricPotential,
/// 1 / Volts
pub dac_factor: f64,
pub shdn: C::Shdn,
/// stm32f4 integrated adc
pub adc: C::Adc,
pub vref_pin: C::VRefPin,
pub itec_pin: C::ItecPin,
/// feedback from `dac` output
@ -31,31 +29,24 @@ pub struct Channel<C: ChannelPins> {
}
impl<C: ChannelPins> Channel<C> {
pub fn new(pins: ChannelPinSet<C>, adc_calibration: ad7172::ChannelCalibration) -> Self {
let state = ChannelState::new(adc_calibration);
pub fn new(mut pins: ChannelPinSet<C>) -> Self {
let state = ChannelState::default();
let mut dac = ad5680::Dac::new(pins.dac_spi, pins.dac_sync);
let _ = dac.set(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);
// power up TEC
let _ = pins.shdn.set_high();
// sensible dummy preset. calibrate_i_set() must be used.
let dac_factor = ad5680::MAX_VALUE as f64 / 5.0;
Channel {
state,
dac, vref_meas,
dac, dac_factor,
shdn: pins.shdn,
adc: pins.adc,
vref_pin: pins.vref_pin,
itec_pin: pins.itec_pin,
dac_feedback_pin: pins.dac_feedback_pin,
tec_u_meas_pin: pins.tec_u_meas_pin,
}
}
// power up TEC
pub fn power_up(&mut self) {
let _ = self.shdn.set_high();
}
// power down TEC
pub fn power_down(&mut self) {
let _ = self.shdn.set_low();
}
}

View File

@ -1,103 +1,43 @@
use smoltcp::time::{Duration, Instant};
use uom::si::{
f64::{
ElectricPotential,
ElectricalResistance,
ElectricCurrent,
ThermodynamicTemperature,
Time,
},
electric_potential::volt,
electrical_resistance::ohm,
thermodynamic_temperature::degree_celsius,
time::millisecond,
};
use smoltcp::time::Instant;
use crate::{
ad7172,
pid,
steinhart_hart as sh,
command_parser::CenterPoint,
units::Volts,
};
const R_INNER: f64 = 2.0 * 5100.0;
const VREF_SENS: f64 = 3.3 / 2.0;
pub struct ChannelState {
pub adc_data: Option<u32>,
pub adc_calibration: ad7172::ChannelCalibration,
pub adc_time: Instant,
pub adc_interval: Duration,
/// VREF for the TEC (1.5V)
pub vref: ElectricPotential,
/// i_set 0A center point
pub center: CenterPoint,
pub dac_value: ElectricPotential,
pub dac_value: Volts,
pub pid_engaged: bool,
pub pid: pid::Controller,
pub sh: sh::Parameters,
}
impl ChannelState {
pub fn new(adc_calibration: ad7172::ChannelCalibration) -> Self {
impl Default for ChannelState {
fn default() -> Self {
ChannelState {
adc_data: None,
adc_calibration,
adc_time: Instant::from_secs(0),
// default: 10 Hz
adc_interval: Duration::from_millis(100),
// updated later with Channels.read_vref()
vref: ElectricPotential::new::<volt>(1.5),
center: CenterPoint::Vref,
dac_value: ElectricPotential::new::<volt>(0.0),
dac_value: Volts(0.0),
pid_engaged: false,
pid: pid::Controller::new(pid::Parameters::default()),
sh: sh::Parameters::default(),
}
}
}
pub fn update(&mut self, now: Instant, adc_data: u32) {
self.adc_data = if adc_data == ad7172::MAX_VALUE {
// this means there is no thermistor plugged into the ADC.
None
} else {
Some(adc_data)
};
self.adc_interval = now - self.adc_time;
self.adc_time = now;
}
impl ChannelState {
/// Update PID state on ADC input, calculate new DAC output
pub fn update_pid(&mut self, current: ElectricCurrent) -> Option<f64> {
let temperature = self.get_temperature()?
.get::<degree_celsius>();
let pid_output = self.pid.update(temperature, self.get_adc_interval(), current);
Some(pid_output)
}
pub fn update_pid(&mut self, now: Instant, adc_data: u32) -> f64 {
self.adc_data = Some(adc_data);
self.adc_time = now;
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?))
}
/// Get `SENS[01]` input resistance
pub fn get_sens(&self) -> Option<ElectricalResistance> {
let r_inner = ElectricalResistance::new::<ohm>(R_INNER);
let vref = ElectricPotential::new::<volt>(VREF_SENS);
let adc_input = self.get_adc()?;
let r = r_inner * adc_input / (vref - adc_input);
Some(r)
}
pub fn get_temperature(&self) -> Option<ThermodynamicTemperature> {
let r = self.get_sens()?;
let temperature = self.sh.get_temperature(r);
Some(temperature)
// Update PID controller
let input = (adc_data as f64) / (ad7172::MAX_VALUE as f64);
let temperature = self.sh.get_temperature(input);
self.pid.update(temperature)
}
}

View File

@ -1,66 +1,53 @@
use heapless::{consts::{U2, U1024}, Vec};
use serde::{Serialize, Serializer};
use smoltcp::time::Instant;
use stm32f4xx_hal::hal;
use uom::si::{
f64::{ElectricCurrent, ElectricPotential, ElectricalResistance, Time},
electric_potential::{millivolt, volt},
electric_current::ampere,
electrical_resistance::ohm,
ratio::ratio,
thermodynamic_temperature::degree_celsius,
};
use log::info;
use crate::{
ad5680,
ad7172,
channel::{Channel, Channel0, Channel1},
channel_state::ChannelState,
command_parser::{CenterPoint, PwmPin},
pins,
steinhart_hart,
units::Volts,
};
pub const CHANNELS: usize = 2;
pub const R_SENSE: f64 = 0.05;
// DAC chip outputs 0-5v, which is then passed through a resistor dividor to provide 0-3v range
const DAC_OUT_V_MAX: f64 = 3.0;
// TODO: -pub
pub struct Channels {
channel0: Channel<Channel0>,
channel1: Channel<Channel1>,
pub adc: ad7172::Adc<pins::AdcSpi, pins::AdcNss>,
/// stm32f4 integrated adc
pins_adc: pins::PinsAdc,
tec_u_meas_adc: pins::TecUMeasAdc,
pub pwm: pins::PwmPins,
}
impl Channels {
pub fn new(pins: pins::Pins) -> Self {
let channel0 = Channel::new(pins.channel0);
let channel1 = Channel::new(pins.channel1);
let tec_u_meas_adc = pins.tec_u_meas_adc;
let pwm = pins.pwm;
let mut adc = ad7172::Adc::new(pins.adc_spi, pins.adc_nss).unwrap();
// Feature not used
adc.set_sync_enable(false).unwrap();
// Calibrate ADC channels individually
adc.disable_all_channels().unwrap();
adc.setup_channel(0, ad7172::Input::Ain0, ad7172::Input::Ain1).unwrap();
adc.calibrate().unwrap();
adc.disable_channel(0).unwrap();
adc.setup_channel(1, ad7172::Input::Ain2, ad7172::Input::Ain3).unwrap();
adc.calibrate().unwrap();
adc.disable_channel(1).unwrap();
// Setup channels and start ADC
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::Ain0, ad7172::Input::Ain1).unwrap();
let adc_calibration1 = adc.get_calibration(1)
.expect("adc_calibration1");
adc.setup_channel(0, ad7172::Input::Ain0, ad7172::Input::Ain1).unwrap();
adc.setup_channel(1, ad7172::Input::Ain2, ad7172::Input::Ain3).unwrap();
adc.start_continuous_conversion().unwrap();
let channel0 = Channel::new(pins.channel0, adc_calibration0);
let channel1 = Channel::new(pins.channel1, adc_calibration1);
let pins_adc = pins.pins_adc;
let pwm = pins.pwm;
let mut channels = Channels { channel0, channel1, adc, pins_adc, pwm };
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));
}
channels
Channels { channel0, channel1, adc, tec_u_meas_adc, pwm }
}
pub fn channel_state<I: Into<usize>>(&mut self, channel: I) -> &mut ChannelState {
@ -75,522 +62,192 @@ 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 current = self.get_tec_i(channel.into());
let state = self.channel_state(channel);
state.update(instant, data);
match state.update_pid(current) {
Some(pid_output) if state.pid_engaged => {
// Forward PID output to i_set DAC
self.set_i(channel.into(), ElectricCurrent::new::<ampere>(pid_output));
self.power_up(channel);
let dac_value = {
let state = self.channel_state(channel);
let pid_output = state.update_pid(instant, data);
if state.pid_engaged {
Some(pid_output)
} else {
None
}
None if state.pid_engaged => {
self.power_down(channel);
}
_ => {}
};
if let Some(dac_value) = dac_value {
// Forward PID output to i_set DAC
self.set_dac(channel.into(), Volts(dac_value));
}
channel
})
}
/// 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::Override(center_point) =>
ElectricPotential::new::<volt>(center_point.into()),
/// i_set DAC
pub fn set_dac(&mut self, channel: usize, voltage: Volts) {
let dac_factor = match channel.into() {
0 => self.channel0.dac_factor,
1 => self.channel1.dac_factor,
_ => unreachable!(),
};
let value = (voltage.0 * dac_factor) as u32;
match channel {
0 => {
self.channel0.dac.set(value).unwrap();
self.channel0.state.dac_value = voltage;
}
1 => {
self.channel1.dac.set(value).unwrap();
self.channel1.state.dac_value = voltage;
}
_ => unreachable!(),
}
}
/// i_set DAC
fn get_dac(&mut self, channel: usize) -> ElectricPotential {
let voltage = self.channel_state(channel).dac_value;
voltage
}
pub fn get_i(&mut self, channel: usize) -> ElectricCurrent {
let center_point = self.get_center(channel);
let r_sense = ElectricalResistance::new::<ohm>(R_SENSE);
let voltage = self.get_dac(channel);
let i_tec = (voltage - center_point) / (10.0 * r_sense);
i_tec
}
/// i_set DAC
fn set_dac(&mut self, channel: usize, voltage: ElectricPotential) -> ElectricPotential {
let value = ((voltage / ElectricPotential::new::<volt>(DAC_OUT_V_MAX)).get::<ratio>() * (ad5680::MAX_VALUE as f64)) as u32 ;
match channel {
0 => self.channel0.dac.set(value).unwrap(),
1 => self.channel1.dac.set(value).unwrap(),
_ => unreachable!(),
};
self.channel_state(channel).dac_value = voltage;
voltage
}
pub fn set_i(&mut self, channel: usize, i_tec: ElectricCurrent) -> ElectricCurrent {
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 = self.set_dac(channel, voltage);
let i_tec = (voltage - center_point) / (10.0 * r_sense);
i_tec
}
pub fn read_dac_feedback(&mut self, channel: usize) -> ElectricPotential {
pub fn read_dac_feedback(&mut self, channel: usize) -> Volts {
match channel {
0 => {
let sample = self.pins_adc.convert(
let sample = self.channel0.adc.convert(
&self.channel0.dac_feedback_pin,
stm32f4xx_hal::adc::config::SampleTime::Cycles_480
);
let mv = self.pins_adc.sample_to_millivolts(sample);
ElectricPotential::new::<millivolt>(mv as f64)
let mv = self.channel0.adc.sample_to_millivolts(sample);
Volts(mv as f64 / 1000.0)
}
1 => {
let sample = self.pins_adc.convert(
let sample = self.channel1.adc.convert(
&self.channel1.dac_feedback_pin,
stm32f4xx_hal::adc::config::SampleTime::Cycles_480
);
let mv = self.pins_adc.sample_to_millivolts(sample);
ElectricPotential::new::<millivolt>(mv as f64)
let mv = self.channel1.adc.sample_to_millivolts(sample);
Volts(mv as f64 / 1000.0)
}
_ => unreachable!(),
}
}
pub fn read_dac_feedback_until_stable(&mut self, channel: usize, tolerance: ElectricPotential) -> ElectricPotential {
pub fn read_dac_feedback_until_stable(&mut self, channel: usize, tolerance: f64) -> Volts {
let mut prev = self.read_dac_feedback(channel);
loop {
let current = self.read_dac_feedback(channel);
if (current - prev).abs() < tolerance {
use num_traits::float::Float;
if (current - prev).0.abs() < tolerance {
return current;
}
prev = current;
}
}
pub fn read_itec(&mut self, channel: usize) -> ElectricPotential {
pub fn read_itec(&mut self, channel: usize) -> Volts {
match channel {
0 => {
let sample = self.pins_adc.convert(
let sample = self.channel0.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)
let mv = self.channel0.adc.sample_to_millivolts(sample);
Volts(mv as f64 / 1000.0)
}
1 => {
let sample = self.pins_adc.convert(
let sample = self.channel1.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)
let mv = self.channel1.adc.sample_to_millivolts(sample);
Volts(mv as f64 / 1000.0)
}
_ => unreachable!(),
}
}
/// should be 1.5V
pub fn read_vref(&mut self, channel: usize) -> ElectricPotential {
pub fn read_vref(&mut self, channel: usize) -> Volts {
match channel {
0 => {
let sample = self.pins_adc.convert(
let sample = self.channel0.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)
let mv = self.channel0.adc.sample_to_millivolts(sample);
Volts(mv as f64 / 1000.0)
}
1 => {
let sample = self.pins_adc.convert(
let sample = self.channel1.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)
let mv = self.channel1.adc.sample_to_millivolts(sample);
Volts(mv as f64 / 1000.0)
}
_ => unreachable!(),
}
}
pub fn read_tec_u_meas(&mut self, channel: usize) -> ElectricPotential {
pub fn read_tec_u_meas(&mut self, channel: usize) -> Volts {
match channel {
0 => {
let sample = self.pins_adc.convert(
let sample = self.tec_u_meas_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)
let mv = self.tec_u_meas_adc.sample_to_millivolts(sample);
Volts(mv as f64 / 1000.0)
}
1 => {
let sample = self.pins_adc.convert(
let sample = self.tec_u_meas_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)
let mv = self.tec_u_meas_adc.sample_to_millivolts(sample);
Volts(mv as f64 / 1000.0)
}
_ => unreachable!(),
}
}
/// Calibrates the DAC output to match vref of the MAX driver to reduce zero-current offset of the MAX driver output.
///
/// The thermostat DAC applies a control voltage signal to the CTLI pin of MAX driver chip to control its output current.
/// 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.
/// for i_set
pub fn calibrate_dac_value(&mut self, channel: usize) {
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);
let vref = self.read_vref(channel);
let value = self.calibrate_dac_value_for_voltage(channel, vref);
info!("best dac value for {}: {}", vref, value);
for step in (0..18).rev() {
let mut prev_value = start_value;
for value in (start_value..=ad5680::MAX_VALUE).step_by(1 << step) {
let dac_factor = value as f64 / vref.0;
match channel {
0 => self.channel0.dac_factor = dac_factor,
1 => self.channel1.dac_factor = dac_factor,
_ => unreachable!(),
}
}
fn calibrate_dac_value_for_voltage(&mut self, channel: usize, voltage: Volts) -> u32 {
let mut best_value = 0;
let mut best_error = Volts(100.0);
for step in (1..=12).rev() {
for value in (best_value..=ad5680::MAX_VALUE).step_by(2usize.pow(step)) {
match channel {
0 => {
self.channel0.dac.set(value).unwrap();
// self.channel0.shdn.set_high().unwrap();
}
1 => {
self.channel1.dac.set(value).unwrap();
// self.channel1.shdn.set_high().unwrap();
}
_ => unreachable!(),
}
let dac_feedback = self.read_dac_feedback_until_stable(channel, ElectricPotential::new::<volt>(0.001));
let error = target_voltage - dac_feedback;
if error < ElectricPotential::new::<volt>(0.0) {
let dac_feedback = self.read_dac_feedback_until_stable(channel, 0.001);
let error = voltage - dac_feedback;
if error < Volts(0.0) {
break;
} else if error < best_error {
best_value = value;
best_error = error;
start_value = prev_value;
let vref = (value as f64 / ad5680::MAX_VALUE as f64) * ElectricPotential::new::<volt>(DAC_OUT_V_MAX);
match channel {
0 => self.channel0.vref_meas = vref,
1 => self.channel1.vref_meas = vref,
_ => unreachable!(),
}
}
prev_value = value;
}
}
// Reset
self.set_dac(channel, ElectricPotential::new::<volt>(0.0));
}
// power up TEC
pub fn power_up<I: Into<usize>>(&mut self, channel: I) {
match channel.into() {
0 => self.channel0.power_up(),
1 => self.channel1.power_up(),
_ => unreachable!(),
}
}
// power down TEC
pub fn power_down<I: Into<usize>>(&mut self, channel: I) {
match channel.into() {
0 => self.channel0.power_down(),
1 => self.channel1.power_down(),
_ => unreachable!(),
}
}
fn get_pwm(&self, channel: usize, pin: PwmPin) -> f64 {
fn get<P: hal::PwmPin<Duty=u16>>(pin: &P) -> f64 {
let duty = pin.get_duty();
let max = pin.get_max_duty();
duty as f64 / (max as f64)
}
match (channel, pin) {
(_, PwmPin::ISet) =>
panic!("i_set is no pwm pin"),
(0, PwmPin::MaxIPos) =>
get(&self.pwm.max_i_pos0),
(0, PwmPin::MaxINeg) =>
get(&self.pwm.max_i_neg0),
(0, PwmPin::MaxV) =>
get(&self.pwm.max_v0),
(1, PwmPin::MaxIPos) =>
get(&self.pwm.max_i_pos1),
(1, PwmPin::MaxINeg) =>
get(&self.pwm.max_i_neg1),
(1, PwmPin::MaxV) =>
get(&self.pwm.max_v1),
_ =>
unreachable!(),
}
}
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
}
pub fn get_max_i_pos(&mut self, channel: usize) -> (ElectricCurrent, ElectricCurrent) {
let max = ElectricCurrent::new::<ampere>(3.0);
let duty = self.get_pwm(channel, PwmPin::MaxIPos);
(duty * max, max)
}
pub fn get_max_i_neg(&mut self, channel: usize) -> (ElectricCurrent, ElectricCurrent) {
let max = ElectricCurrent::new::<ampere>(3.0);
let duty = self.get_pwm(channel, PwmPin::MaxINeg);
(duty * max, max)
}
// Get current passing through TEC
pub fn get_tec_i(&mut self, channel: usize) -> ElectricCurrent {
(self.read_itec(channel) - self.read_vref(channel)) / ElectricalResistance::new::<ohm>(0.4)
}
// Get voltage across TEC
pub fn get_tec_v(&mut self, channel: usize) -> ElectricPotential {
(self.read_tec_u_meas(channel) - ElectricPotential::new::<volt>(1.5)) * 4.0
}
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();
let value = ((duty * (max as f64)) as u16).min(max);
pin.set_duty(value);
value as f64 / (max as f64)
}
match (channel, pin) {
(_, PwmPin::ISet) =>
panic!("i_set is no pwm pin"),
(0, PwmPin::MaxIPos) =>
set(&mut self.pwm.max_i_pos0, duty),
(0, PwmPin::MaxINeg) =>
set(&mut self.pwm.max_i_neg0, duty),
(0, PwmPin::MaxV) =>
set(&mut self.pwm.max_v0, duty),
(1, PwmPin::MaxIPos) =>
set(&mut self.pwm.max_i_pos1, duty),
(1, PwmPin::MaxINeg) =>
set(&mut self.pwm.max_i_neg1, duty),
(1, PwmPin::MaxV) =>
set(&mut self.pwm.max_v1, duty),
_ =>
unreachable!(),
}
}
pub fn set_max_v(&mut self, channel: usize, max_v: ElectricPotential) -> (ElectricPotential, ElectricPotential) {
let max = 4.0 * ElectricPotential::new::<volt>(3.3);
let duty = (max_v / max).get::<ratio>();
let duty = self.set_pwm(channel, PwmPin::MaxV, duty);
(duty * max, max)
}
pub fn set_max_i_pos(&mut self, channel: usize, max_i_pos: ElectricCurrent) -> (ElectricCurrent, ElectricCurrent) {
let max = ElectricCurrent::new::<ampere>(3.0);
let duty = (max_i_pos / max).get::<ratio>();
let duty = self.set_pwm(channel, PwmPin::MaxIPos, duty);
(duty * max, max)
}
pub fn set_max_i_neg(&mut self, channel: usize, max_i_neg: ElectricCurrent) -> (ElectricCurrent, ElectricCurrent) {
let max = ElectricCurrent::new::<ampere>(3.0);
let duty = (max_i_neg / max).get::<ratio>();
let duty = self.set_pwm(channel, PwmPin::MaxINeg, duty);
(duty * max, max)
}
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 = 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)
);
Report {
channel,
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),
i_tec,
tec_i,
tec_u_meas: self.get_tec_v(channel),
pid_output,
}
}
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)
}
fn pwm_summary(&mut self, channel: usize) -> PwmSummary {
PwmSummary {
channel,
center: CenterPointJson(self.channel_state(channel).center.clone()),
i_set: (self.get_i(channel), ElectricCurrent::new::<ampere>(3.0)).into(),
max_v: (self.get_max_v(channel), ElectricPotential::new::<volt>(5.0)).into(),
max_i_pos: self.get_max_i_pos(channel).into(),
max_i_neg: self.get_max_i_neg(channel).into(),
}
}
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 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 }
}
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)
self.set_dac(channel, Volts(0.0));
best_value
}
}
type JsonBuffer = Vec<u8, U1024>;
#[derive(Serialize)]
pub struct Report {
channel: usize,
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>,
}
pub struct CenterPointJson(CenterPoint);
// used in JSON encoding, not for config
impl Serialize for CenterPointJson {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self.0 {
CenterPoint::Vref =>
serializer.serialize_str("vref"),
CenterPoint::Override(vref) =>
serializer.serialize_f32(vref),
}
}
}
#[derive(Serialize)]
pub struct PwmSummaryField<T: Serialize> {
value: T,
max: T,
}
impl<T: Serialize> From<(T, T)> for PwmSummaryField<T> {
fn from((value, max): (T, T)) -> Self {
PwmSummaryField { value, max }
}
}
#[derive(Serialize)]
pub struct PwmSummary {
channel: usize,
center: CenterPointJson,
i_set: PwmSummaryField<ElectricCurrent>,
max_v: PwmSummaryField<ElectricPotential>,
max_i_pos: PwmSummaryField<ElectricCurrent>,
max_i_neg: PwmSummaryField<ElectricCurrent>,
}
#[derive(Serialize)]
pub struct PostFilterSummary {
channel: usize,
rate: Option<f32>,
}
#[derive(Serialize)]
pub struct SteinhartHartSummary {
channel: usize,
params: steinhart_hart::Parameters,
}

View File

@ -1,373 +0,0 @@
use smoltcp::socket::TcpSocket;
use log::{error, warn};
use core::fmt::Write;
use super::{
net,
command_parser::{
Ipv4Config,
Command,
ShowCommand,
CenterPoint,
PidParameter,
PwmPin,
ShParameter
},
leds::Leds,
ad7172,
CHANNEL_CONFIG_KEY,
channels::{
Channels,
CHANNELS
},
config::ChannelConfig,
dfu,
flash_store::FlashStore,
session::Session
};
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
}
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, leds: &mut Leds, channel: usize) -> Result<Handler, Error> {
channels.channel_state(channel).pid_engaged = true;
leds.g3.on();
send_line(socket, b"{}");
Ok(Handler::Handled)
}
fn set_pwm (socket: &mut TcpSocket, channels: &mut Channels, leds: &mut Leds, channel: usize, pin: PwmPin, value: f64) -> Result<Handler, Error> {
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);
}
}
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_tec = channels.get_i(channel);
let state = channels.channel_state(channel);
state.center = center;
if !state.pid_engaged {
channels.set_i(channel, i_tec);
}
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,
IntegralMin =>
pid.parameters.integral_min = value as f32,
IntegralMax =>
pid.parameters.integral_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)
}
pub fn handle_command (command: Command, socket: &mut TcpSocket, channels: &mut Channels, session: &Session, leds: &mut Leds, store: &mut FlashStore, ipv4_config: &mut Ipv4Config) -> 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, leds, channel),
Command::Pwm { channel, pin, value } => Handler::set_pwm(socket, channels, leds, 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)
}
}
}

View File

@ -12,7 +12,6 @@ use nom::{
error::ErrorKind,
};
use num_traits::{Num, ParseFloatError};
use serde::{Serialize, Deserialize};
#[derive(Clone, Debug, PartialEq)]
@ -85,13 +84,6 @@ 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,
@ -100,7 +92,6 @@ pub enum ShowCommand {
Pid,
SteinhartHart,
PostFilter,
Ipv4,
}
#[derive(Debug, Clone, PartialEq)]
@ -131,39 +122,32 @@ pub enum PwmPin {
MaxV,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum CenterPoint {
Vref,
Override(f32),
impl PwmPin {
pub fn name(&self) -> &'static str {
match self {
PwmPin::ISet => "i_set",
PwmPin::MaxIPos => "max_i_pos",
PwmPin::MaxINeg => "max_i_neg",
PwmPin::MaxV => "max_v",
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Command {
Quit,
Load {
channel: Option<usize>,
},
Save {
channel: Option<usize>,
},
Reset,
Ipv4(Ipv4Config),
Show(ShowCommand),
Reporting(bool),
/// PWM parameter setting
Pwm {
channel: usize,
pin: PwmPin,
value: f64,
duty: f64,
},
/// Enable PID control for `i_set`
PwmPid {
channel: usize,
},
CenterPoint {
channel: usize,
center: CenterPoint,
},
/// PID parameter setting
Pid {
channel: usize,
@ -177,9 +161,8 @@ pub enum Command {
},
PostFilter {
channel: usize,
rate: Option<f32>,
rate: f32,
},
Dfu,
}
fn end(input: &[u8]) -> IResult<&[u8], ()> {
@ -259,19 +242,9 @@ fn report(input: &[u8]) -> IResult<&[u8], Command> {
fn pwm_setup(input: &[u8]) -> IResult<&[u8], Result<(PwmPin, f64), Error>> {
let result_with_pin = |pin: PwmPin|
move |result: Result<f64, Error>|
result.map(|value| (pin, value));
result.map(|duty| (pin, duty));
alt((
map(
preceded(
tag("i_set"),
preceded(
whitespace,
float
)
),
result_with_pin(PwmPin::ISet)
),
map(
preceded(
tag("max_i_pos"),
@ -301,6 +274,8 @@ fn pwm_setup(input: &[u8]) -> IResult<&[u8], Result<(PwmPin, f64), Error>> {
)
),
result_with_pin(PwmPin::MaxV)
),
map(float, result_with_pin(PwmPin::ISet)
))
)(input)
}
@ -325,8 +300,8 @@ fn pwm(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
|input| {
let (input, config) = pwm_setup(input)?;
match config {
Ok((pin, value)) =>
Ok((input, Ok(Command::Pwm { channel, pin, value }))),
Ok((pin, duty)) =>
Ok((input, Ok(Command::Pwm { channel, pin, duty }))),
Err(e) =>
Ok((input, Err(e))),
}
@ -339,25 +314,6 @@ fn pwm(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
))(input)
}
fn center_point(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
let (input, _) = tag("center")(input)?;
let (input, _) = whitespace(input)?;
let (input, channel) = channel(input)?;
let (input, _) = whitespace(input)?;
let (input, center) = alt((
value(Ok(CenterPoint::Vref), tag("vref")),
|input| {
let (input, value) = float(input)?;
Ok((input, value.map(|value| CenterPoint::Override(value as f32))))
}
))(input)?;
end(input)?;
Ok((input, center.map(|center| Command::CenterPoint {
channel,
center,
})))
}
/// `pid <0-1> <parameter> <value>`
fn pid_parameter(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
let (input, channel) = channel(input)?;
@ -427,123 +383,35 @@ fn postfilter(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
|input| {
let (input, channel) = channel(input)?;
let (input, _) = whitespace(input)?;
alt((
value(Ok(Command::PostFilter {
let (input, _) = tag("rate")(input)?;
let (input, _) = whitespace(input)?;
let (input, rate) = float(input)?;
let result = rate
.map(|rate| Command::PostFilter {
channel,
rate: None,
}), tag("off")),
move |input| {
let (input, _) = tag("rate")(input)?;
let (input, _) = whitespace(input)?;
let (input, rate) = float(input)?;
let result = rate
.map(|rate| Command::PostFilter {
channel,
rate: Some(rate as f32),
});
Ok((input, result))
}
))(input)
rate: rate as f32,
});
Ok((input, result))
}
),
value(Ok(Command::Show(ShowCommand::PostFilter)), end)
))(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)?;
let (input, _) = tag(".")(input)?;
let (input, c) = unsigned(input)?;
let (input, _) = tag(".")(input)?;
let (input, d) = unsigned(input)?;
let address = move || Ok([a? as u8, b? as u8, c? as u8, d? as u8]);
Ok((input, address()))
}
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 command(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
alt((value(Ok(Command::Quit), tag("quit")),
load,
save,
value(Ok(Command::Reset), tag("reset")),
ipv4,
map(report, Ok),
pwm,
center_point,
pid,
steinhart_hart,
postfilter,
value(Ok(Command::Dfu), tag("dfu")),
))(input)
}
impl Command {
pub fn parse(input: &[u8]) -> Result<Self, Error> {
match command(input) {
Ok((input_remain, result)) if input_remain.len() == 0 =>
Ok((b"", result)) =>
result,
Ok((input_remain, _)) =>
Err(Error::UnexpectedInput(input_remain[0])),
@ -563,56 +431,6 @@ mod test {
assert_eq!(command, Ok(Command::Quit));
}
#[test]
fn parse_load() {
let command = Command::parse(b"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 { 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/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]
fn parse_report() {
let command = Command::parse(b"report");
@ -638,12 +456,12 @@ mod test {
}
#[test]
fn parse_pwm_i_set() {
let command = Command::parse(b"pwm 1 i_set 16383");
fn parse_pwm_manual() {
let command = Command::parse(b"pwm 1 16383");
assert_eq!(command, Ok(Command::Pwm {
channel: 1,
pin: PwmPin::ISet,
value: 16383.0,
duty: 16383,
}));
}
@ -652,6 +470,7 @@ mod test {
let command = Command::parse(b"pwm 0 pid");
assert_eq!(command, Ok(Command::PwmPid {
channel: 0,
pin: PwmPin::ISet,
}));
}
@ -661,7 +480,7 @@ mod test {
assert_eq!(command, Ok(Command::Pwm {
channel: 0,
pin: PwmPin::MaxIPos,
value: 7.0,
duty: 7,
}));
}
@ -671,7 +490,7 @@ mod test {
assert_eq!(command, Ok(Command::Pwm {
channel: 0,
pin: PwmPin::MaxINeg,
value: 128.0,
duty: 128,
}));
}
@ -681,7 +500,7 @@ mod test {
assert_eq!(command, Ok(Command::Pwm {
channel: 0,
pin: PwmPin::MaxV,
value: 32768.0,
duty: 32768,
}));
}
@ -718,7 +537,7 @@ mod test {
}
#[test]
fn parse_steinhart_hart_set() {
fn parse_steinhart_hart_parallel_r() {
let command = Command::parse(b"s-h 1 t0 23.05");
assert_eq!(command, Ok(Command::SteinhartHart {
channel: 1,
@ -727,45 +546,12 @@ mod test {
}));
}
#[test]
fn parse_postfilter() {
let command = Command::parse(b"postfilter");
assert_eq!(command, Ok(Command::Show(ShowCommand::PostFilter)));
}
#[test]
fn parse_postfilter_off() {
let command = Command::parse(b"postfilter 1 off");
assert_eq!(command, Ok(Command::PostFilter {
channel: 1,
rate: None,
}));
}
#[test]
fn parse_postfilter_rate() {
let command = Command::parse(b"postfilter 0 rate 21");
assert_eq!(command, Ok(Command::PostFilter {
channel: 0,
rate: Some(21.0),
}));
}
#[test]
fn parse_center_point() {
let command = Command::parse(b"center 0 1.5");
assert_eq!(command, Ok(Command::CenterPoint {
channel: 0,
center: CenterPoint::Override(1.5),
}));
}
#[test]
fn parse_center_point_vref() {
let command = Command::parse(b"center 1 vref");
assert_eq!(command, Ok(Command::CenterPoint {
channel: 1,
center: CenterPoint::Vref,
rate: 21.0,
}));
}
}

View File

@ -1,89 +0,0 @@
use serde::{Serialize, Deserialize};
use uom::si::{
electric_potential::volt,
electric_current::ampere,
f64::{ElectricCurrent, ElectricPotential},
};
use crate::{
ad7172::PostFilter,
channels::Channels,
command_parser::CenterPoint,
pid,
steinhart_hart,
};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ChannelConfig {
center: CenterPoint,
pid: pid::Parameters,
pid_target: f32,
pid_engaged: bool,
sh: steinhart_hart::Parameters,
pwm: PwmLimits,
/// uses variant `PostFilter::Invalid` instead of `None` to save space
adc_postfilter: PostFilter,
}
impl ChannelConfig {
pub fn new(channels: &mut Channels, channel: usize) -> Self {
let pwm = PwmLimits::new(channels, channel);
let adc_postfilter = channels.adc.get_postfilter(channel as u8)
.unwrap()
.unwrap_or(PostFilter::Invalid);
let state = channels.channel_state(channel);
ChannelConfig {
center: state.center.clone(),
pid: state.pid.parameters.clone(),
pid_target: state.pid.target as f32,
pid_engaged: state.pid_engaged,
sh: state.sh.clone(),
pwm,
adc_postfilter,
}
}
pub fn apply(&self, channels: &mut Channels, channel: usize) {
let state = channels.channel_state(channel);
state.center = self.center.clone();
state.pid.parameters = self.pid.clone();
state.pid.target = self.pid_target.into();
state.pid_engaged = self.pid_engaged;
state.sh = self.sh.clone();
self.pwm.apply(channels, channel);
let adc_postfilter = match self.adc_postfilter {
PostFilter::Invalid => None,
adc_postfilter => Some(adc_postfilter),
};
let _ = channels.adc.set_postfilter(channel as u8, adc_postfilter);
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
struct PwmLimits {
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_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>(),
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));
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));
}
}

View File

@ -1,45 +0,0 @@
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",
);
}
}

View File

@ -1,69 +0,0 @@
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().cloned())
}
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
}

View File

@ -1,12 +1,5 @@
#[cfg(not(feature = "semihosting"))]
use crate::usb;
#[cfg(not(feature = "semihosting"))]
pub fn init_log() {
static USB_LOGGER: usb::Logger = usb::Logger;
let _ = log::set_logger(&USB_LOGGER);
log::set_max_level(log::LevelFilter::Debug);
}
pub fn init_log() {}
#[cfg(feature = "semihosting")]
pub fn init_log() {

View File

@ -1,44 +0,0 @@
use stm32f4xx_hal::{
gpio::{
gpiod::{PD9, PD10, PD11},
Output, PushPull,
},
hal::digital::v2::OutputPin,
};
pub struct Leds {
/// Red LED L1
pub r1: Led<PD9<Output<PushPull>>>,
/// Green LED L3
pub g3: Led<PD10<Output<PushPull>>>,
/// Green LED L4
pub g4: Led<PD11<Output<PushPull>>>,
}
impl Leds {
pub fn new<M1, M2, M3>(r1: PD9<M1>, g3: PD10<M2>, g4: PD11<M3>) -> Self {
Leds {
r1: Led::new(r1.into_push_pull_output()),
g3: Led::new(g3.into_push_pull_output()),
g4: Led::new(g4.into_push_pull_output()),
}
}
}
pub struct Led<P> {
pin: P,
}
impl<P: OutputPin> Led<P> {
pub fn new(pin: P) -> Self {
Led { pin }
}
pub fn on(&mut self) {
let _ = self.pin.set_high();
}
pub fn off(&mut self) {
let _ = self.pin.set_low();
}
}

View File

@ -1,108 +1,82 @@
#![cfg_attr(not(test), no_std)]
#![cfg_attr(not(test), no_main)]
#![feature(maybe_uninit_extra, maybe_uninit_ref, asm)]
#![cfg_attr(test, allow(unused))]
#![no_std]
#![no_main]
// TODO: #![deny(warnings, unused)]
#[cfg(not(any(feature = "semihosting", test)))]
#[cfg(not(feature = "semihosting"))]
use panic_abort as _;
#[cfg(all(feature = "semihosting", not(test)))]
#[cfg(feature = "semihosting")]
use panic_semihosting as _;
use log::{error, info, warn};
use log::{info, warn};
use core::ops::DerefMut;
use core::fmt::Write;
use cortex_m::asm::wfi;
use cortex_m_rt::entry;
use stm32f4xx_hal::{
hal::watchdog::{WatchdogEnable, Watchdog},
hal::{
self,
watchdog::{WatchdogEnable, Watchdog},
},
rcc::RccExt,
stm32::{CorePeripherals, Peripherals, SCB},
time::{U32Ext, MegaHertz},
watchdog::IndependentWatchdog,
time::{U32Ext, MegaHertz},
stm32::{CorePeripherals, Peripherals},
};
use smoltcp::{
time::Instant,
socket::TcpSocket,
wire::EthernetAddress,
};
mod init_log;
use init_log::init_log;
mod usb;
mod leds;
mod pins;
use pins::Pins;
mod softspi;
mod ad7172;
mod ad5680;
mod net;
mod server;
use server::Server;
mod session;
use session::{Session, SessionInput};
use session::{Session, SessionOutput};
mod command_parser;
use command_parser::Ipv4Config;
use command_parser::{Command, ShowCommand, PwmPin};
mod timer;
mod units;
use units::{Ohms, Volts};
mod pid;
mod steinhart_hart;
mod channels;
use channels::{CHANNELS, Channels};
mod channel;
mod channel_state;
mod config;
use config::ChannelConfig;
mod flash_store;
mod dfu;
mod command_handler;
use command_handler::Handler;
const HSE: MegaHertz = MegaHertz(8);
#[cfg(not(feature = "semihosting"))]
const WATCHDOG_INTERVAL: u32 = 1_000;
const WATCHDOG_INTERVAL: u32 = 100;
#[cfg(feature = "semihosting")]
const WATCHDOG_INTERVAL: u32 = 30_000;
const CHANNEL_CONFIG_KEY: [&str; 2] = ["ch0", "ch1"];
#[cfg(not(feature = "generate-hwaddr"))]
const NET_HWADDR: [u8; 6] = [0x02, 0x00, 0xDE, 0xAD, 0xBE, 0xEF];
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,
// 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
}
/// Initialization and main loop
#[cfg(not(test))]
#[entry]
fn main() -> ! {
init_log();
info!("thermostat");
info!("tecpak");
let mut cp = CorePeripherals::take().unwrap();
cp.SCB.enable_icache();
cp.SCB.enable_dcache(&mut cp.CPUID);
let dp = Peripherals::take().unwrap();
stm32_eth::setup(&dp.RCC, &dp.SYSCFG);
let clocks = dp.RCC.constrain()
.cfgr
.use_hse(HSE)
@ -118,65 +92,27 @@ fn main() -> ! {
timer::setup(cp.SYST, clocks);
let (pins, mut leds, mut eeprom, eth_pins, usb) = Pins::setup(
let pins = Pins::setup(
clocks, dp.TIM1, dp.TIM3,
dp.GPIOA, dp.GPIOB, dp.GPIOC, dp.GPIOD, dp.GPIOE, dp.GPIOF, dp.GPIOG,
dp.I2C1,
dp.GPIOA, dp.GPIOB, dp.GPIOC, dp.GPIOE, dp.GPIOF, dp.GPIOG,
dp.SPI2, dp.SPI4, dp.SPI5,
dp.ADC1,
dp.OTG_FS_GLOBAL,
dp.OTG_FS_DEVICE,
dp.OTG_FS_PWRCLK,
dp.ADC1, dp.ADC2, dp.ADC3,
);
leds.r1.on();
leds.g3.off();
leds.g4.off();
usb::State::setup(usb);
let mut store = flash_store::store(dp.FLASH);
let mut channels = Channels::new(pins);
for c in 0..CHANNELS {
match store.read_value::<ChannelConfig>(CHANNEL_CONFIG_KEY[c]) {
Ok(Some(config)) =>
config.apply(&mut channels, c),
Ok(None) =>
error!("flash config not found for channel {}", c),
Err(e) =>
error!("unable to load config {} from flash: {:?}", c, e),
}
}
channels.calibrate_dac_value(0);
// default net config:
let mut ipv4_config = Ipv4Config {
address: [192, 168, 1, 26],
mask_len: 24,
gateway: None,
#[cfg(not(feature = "generate-hwaddr"))]
let hwaddr = EthernetAddress(NET_HWADDR);
#[cfg(feature = "generate-hwaddr")]
let hwaddr = {
let uid = stm32f4xx_hal::signature::Uid::get();
EthernetAddress(hash2hwaddr::generate_hwaddr(uid))
};
match store.read_value("ipv4") {
Ok(Some(config)) =>
ipv4_config = config,
Ok(None) => {}
Err(e) =>
error!("cannot read ipv4 config: {:?}", e),
}
info!("Net hwaddr: {}", hwaddr);
// EEPROM ships with a read-only EUI-48 identifier
let mut eui48 = [0; 6];
eeprom.read_data(0xFA, &mut eui48).unwrap();
let hwaddr = EthernetAddress(eui48);
info!("EEPROM MAC address: {}", hwaddr);
net::run(clocks, dp.ETHERNET_MAC, dp.ETHERNET_DMA, eth_pins, hwaddr, ipv4_config.clone(), |iface| {
net::run(dp.ETHERNET_MAC, dp.ETHERNET_DMA, hwaddr, |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 {
@ -190,87 +126,265 @@ fn main() -> ! {
warn!("poll: {:?}", e);
});
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 leds, &mut store, &mut ipv4_config) {
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(_) =>
// TCP protocol handling
server.for_each(|mut socket, session| {
if ! socket.is_active() {
let _ = socket.listen(TCP_PORT);
session.reset();
} else if socket.can_send() && socket.can_recv() && socket.send_capacity() - socket.send_queue() > 1024 {
match socket.recv(|buf| session.feed(buf)) {
Ok(SessionOutput::Nothing) => {}
Ok(SessionOutput::Command(command)) => match command {
Command::Quit =>
socket.close(),
}
} 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);
Command::Reporting(reporting) => {
let _ = writeln!(socket, "report={}", if reporting { "on" } else { "off" });
}
Command::Show(ShowCommand::Reporting) => {
let _ = writeln!(socket, "report={}", if session.reporting() { "on" } else { "off" });
}
Command::Show(ShowCommand::Input) => {
for channel in 0..CHANNELS {
if let Some(adc_data) = channels.channel_state(channel).adc_data {
let vref = channels.read_vref(channel);
let dac_feedback = channels.read_dac_feedback(channel);
let itec = channels.read_itec(channel);
let tec_i = -(itec - Volts(1.5)) / Ohms(0.4);
let tec_u_meas = channels.read_tec_u_meas(channel);
let state = channels.channel_state(channel);
let _ = writeln!(
socket, "t={} adc_raw{}=0x{:06X} vref={} dac_feedback={} itec={} tec={} tec_u_meas={}",
state.adc_time, channel, adc_data,
vref, dac_feedback,
itec, tec_i,
tec_u_meas,
);
}
}
}
Command::Show(ShowCommand::Pid) => {
for channel in 0..CHANNELS {
let state = channels.channel_state(channel);
let _ = writeln!(socket, "PID settings for channel {}", channel);
let pid = &state.pid;
let _ = writeln!(socket, "- target={:.4}", pid.target);
macro_rules! show_pid_parameter {
($p: tt) => {
let _ = writeln!(
socket, "- {}={:.4}",
stringify!($p), pid.parameters.$p
);
};
}
show_pid_parameter!(kp);
show_pid_parameter!(ki);
show_pid_parameter!(kd);
show_pid_parameter!(integral_min);
show_pid_parameter!(integral_max);
show_pid_parameter!(output_min);
show_pid_parameter!(output_max);
if let Some(last_output) = pid.last_output {
let _ = writeln!(socket, "- last_output={:.4}", last_output);
}
let _ = writeln!(socket, "");
}
}
Command::Show(ShowCommand::Pwm) => {
for channel in 0..CHANNELS {
let state = channels.channel_state(channel);
let _ = writeln!(
socket, "channel {}: PID={}",
channel,
if state.pid_engaged { "engaged" } else { "disengaged" }
);
let _ = writeln!(socket, "- i_set={}", state.dac_value);
fn show_pwm_channel<S, P>(mut socket: S, name: &str, pin: &P)
where
S: core::fmt::Write,
P: hal::PwmPin<Duty=u16>,
{
let _ = writeln!(
socket,
"- {}={}/{}",
name, pin.get_duty(), pin.get_max_duty()
);
}
match channel {
0 => {
show_pwm_channel(socket.deref_mut(), "max_v", &channels.pwm.max_v0);
show_pwm_channel(socket.deref_mut(), "max_i_pos", &channels.pwm.max_i_pos0);
show_pwm_channel(socket.deref_mut(), "max_i_neg", &channels.pwm.max_i_neg0);
}
1 => {
show_pwm_channel(socket.deref_mut(), "max_v", &channels.pwm.max_v1);
show_pwm_channel(socket.deref_mut(), "max_i_pos", &channels.pwm.max_i_pos1);
show_pwm_channel(socket.deref_mut(), "max_i_neg", &channels.pwm.max_i_neg1);
}
_ => unreachable!(),
}
let _ = writeln!(socket, "");
}
}
Command::Show(ShowCommand::SteinhartHart) => {
for channel in 0..CHANNELS {
let state = channels.channel_state(channel);
let _ = writeln!(
socket, "channel {}: Steinhart-Hart equation parameters",
channel,
);
let _ = writeln!(socket, "- t0={}", state.sh.t0);
let _ = writeln!(socket, "- b={}", state.sh.b);
let _ = writeln!(socket, "- r0={}", state.sh.r0);
let _ = writeln!(socket, "");
}
}
Command::Show(ShowCommand::PostFilter) => {
for channel in 0..CHANNELS {
match channels.adc.get_postfilter(channel as u8).unwrap() {
Some(filter) => {
let _ = writeln!(
socket, "channel {}: postfilter={:.2} SPS",
channel, filter.output_rate().unwrap()
);
}
None => {
let _ = writeln!(
socket, "channel {}: no postfilter",
channel
);
}
}
}
}
Command::PwmPid { channel } => {
channels.channel_state(channel).pid_engaged = true;
let _ = writeln!(socket, "channel {}: PID enabled to control PWM", channel
);
}
Command::Pwm { channel, pin: PwmPin::ISet, duty } => {
channels.channel_state(channel).pid_engaged = false;
let voltage = Volts(duty);
channels.set_dac(channel, voltage);
let _ = writeln!(
socket, "channel {}: PWM duty cycle manually set to {}",
channel, voltage
);
}
Command::Pwm { channel, pin, duty } => {
fn set_pwm_channel<P: hal::PwmPin<Duty=u16>>(pin: &mut P, duty: f64) -> (u16, u16) {
let max = pin.get_max_duty();
let value = (duty * (max as f64)) as u16;
pin.set_duty(value);
(value, max)
}
let (value, max) = match (channel, pin) {
(_, PwmPin::ISet) =>
// Handled above
unreachable!(),
(0, PwmPin::MaxIPos) =>
set_pwm_channel(&mut channels.pwm.max_i_pos0, duty),
(0, PwmPin::MaxINeg) =>
set_pwm_channel(&mut channels.pwm.max_i_neg0, duty),
(0, PwmPin::MaxV) =>
set_pwm_channel(&mut channels.pwm.max_v0, duty),
(1, PwmPin::MaxIPos) =>
set_pwm_channel(&mut channels.pwm.max_i_pos1, duty),
(1, PwmPin::MaxINeg) =>
set_pwm_channel(&mut channels.pwm.max_i_neg1, duty),
(1, PwmPin::MaxV) =>
set_pwm_channel(&mut channels.pwm.max_v1, duty),
_ =>
unreachable!(),
};
let _ = writeln!(
socket, "channel {}: PWM {} reconfigured to {}/{}",
channel, pin.name(), value, max
);
}
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,
KI =>
pid.parameters.ki = value,
KD =>
pid.parameters.kd = value,
OutputMin =>
pid.parameters.output_min = value,
OutputMax =>
pid.parameters.output_max = value,
IntegralMin =>
pid.parameters.integral_min = value,
IntegralMax =>
pid.parameters.integral_max = value,
}
// TODO: really reset PID state
// after each parameter change?
pid.reset();
let _ = writeln!(socket, "PID parameter updated");
}
Command::SteinhartHart { channel, parameter, value } => {
let sh = &mut channels.channel_state(channel).sh;
use command_parser::ShParameter::*;
match parameter {
T0 => sh.t0 = value,
B => sh.b = value,
R0 => sh.r0 = value,
}
let _ = writeln!(socket, "Steinhart-Hart equation parameter updated");
}
Command::PostFilter { channel, rate } => {
let filter = ad7172::PostFilter::closest(rate);
match filter {
Some(filter) => {
channels.adc.set_postfilter(channel as u8, Some(filter)).unwrap();
let _ = writeln!(
socket, "channel {}: postfilter set to {:.2} SPS",
channel, filter.output_rate().unwrap()
);
}
None => {
let _ = writeln!(socket, "Unable to choose postfilter");
}
}
}
}
Ok(SessionOutput::Error(e)) => {
let _ = writeln!(socket, "Command error: {:?}", e);
}
Err(_) =>
socket.close(),
}
});
} 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;
} else if socket.can_send() && socket.send_capacity() - socket.send_queue() > 256 {
while let Some(channel) = session.is_report_pending() {
let state = &mut channels.channel_state(usize::from(channel));
let _ = writeln!(
socket, "t={} raw{}=0x{:06X}",
state.adc_time, channel, state.adc_data.unwrap_or(0)
).map(|_| {
session.mark_report_sent(channel);
});
}
});
// 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/gateway
new_ipv4_config.take()
.map(|config| {
server.set_ipv4_config(config.clone());
ipv4_config = config;
});
}
});
// Update watchdog
wd.feed();
leds.g4.off();
cortex_m::interrupt::free(|cs| {
if !net::is_pending(cs) {
// Wait for interrupts
// (Ethernet, SysTick, or USB)
// (Ethernet or SysTick)
wfi();
}
});
leds.g4.on();
}
});
});

View File

@ -2,19 +2,14 @@
//! declared once and globally.
use core::cell::RefCell;
use cortex_m::interrupt::{CriticalSection, Mutex};
use cortex_m::interrupt::Mutex;
use bare_metal::CriticalSection;
use stm32f4xx_hal::{
rcc::Clocks,
stm32::{interrupt, Peripherals, ETHERNET_MAC, ETHERNET_DMA},
};
use smoltcp::wire::{EthernetAddress, Ipv4Address, Ipv4Cidr};
use smoltcp::iface::{
EthernetInterfaceBuilder, EthernetInterface,
NeighborCache, Routes,
};
use stm32_eth::{Eth, RingEntry, PhyAddress, RxDescriptor, TxDescriptor};
use crate::command_parser::Ipv4Config;
use crate::pins::EthernetPins;
use smoltcp::wire::{EthernetAddress, IpAddress, IpCidr};
use smoltcp::iface::{NeighborCache, EthernetInterfaceBuilder, EthernetInterface};
use stm32_eth::{Eth, RingEntry, RxDescriptor, TxDescriptor};
/// Not on the stack so that stack can be placed in CCMRAM (which the
/// ethernet peripheral cannot access)
@ -29,12 +24,8 @@ static NET_PENDING: Mutex<RefCell<bool>> = Mutex::new(RefCell::new(false));
/// Run callback `f` with ethernet driver and TCP/IP stack
pub fn run<F>(
clocks: Clocks,
ethernet_mac: ETHERNET_MAC, ethernet_dma: ETHERNET_DMA,
eth_pins: EthernetPins,
ethernet_addr: EthernetAddress,
ipv4_config: Ipv4Config,
f: F
ethernet_addr: EthernetAddress, f: F
) where
F: FnOnce(EthernetInterface<&mut stm32_eth::Eth<'static, 'static>>),
{
@ -47,26 +38,19 @@ pub fn run<F>(
// Ethernet driver
let mut eth_dev = Eth::new(
ethernet_mac, ethernet_dma,
&mut rx_ring[..], &mut tx_ring[..],
PhyAddress::_0,
clocks,
eth_pins,
).unwrap();
&mut rx_ring[..], &mut tx_ring[..]
);
eth_dev.enable_interrupt();
// IP stack
let (ipv4_cidr, gateway) = split_ipv4_config(ipv4_config);
let mut ip_addrs = [ipv4_cidr.into()];
let local_addr = IpAddress::v4(192, 168, 1, 26);
let mut ip_addrs = [IpCidr::new(local_addr, 24)];
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);
@ -97,10 +81,3 @@ 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

@ -1,41 +1,24 @@
use serde::{Serialize, Deserialize};
use uom::si::{
f64::{Time, ElectricCurrent},
time::second,
electric_current::ampere,
};
/// Allowable current error for integral accumulation
const CURRENT_ERROR_MAX: f64 = 0.1;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[derive(Clone, Copy)]
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,
/// Integral clipping minimum
pub integral_min: f32,
/// Integral clipping maximum
pub integral_max: f32
pub kp: f64,
pub ki: f64,
pub kd: f64,
pub output_min: f64,
pub output_max: f64,
pub integral_min: f64,
pub integral_max: f64
}
impl Default for Parameters {
fn default() -> Self {
Parameters {
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,
kp: 0.5,
ki: 0.05,
kd: 0.45,
output_min: 0.0,
output_max: 5.0,
integral_min: 0.0,
integral_max: 1.0,
}
}
}
@ -60,81 +43,42 @@ impl Controller {
}
}
pub fn update(&mut self, input: f64, time_delta: Time, current: ElectricCurrent) -> f64 {
let time_delta = time_delta.get::<second>();
// error
pub fn update(&mut self, input: f64) -> f64 {
let error = self.target - input;
// proportional
let p = f64::from(self.parameters.kp) * error;
let p = self.parameters.kp * error;
// integral
if let Some(last_output_val) = self.last_output {
let electric_current_error = ElectricCurrent::new::<ampere>(last_output_val) - current;
// anti integral windup
if last_output_val < self.parameters.output_max.into() &&
last_output_val > self.parameters.output_min.into() &&
electric_current_error < ElectricCurrent::new::<ampere>(CURRENT_ERROR_MAX) &&
electric_current_error > -ElectricCurrent::new::<ampere>(CURRENT_ERROR_MAX) {
self.integral += error * time_delta;
}
self.integral += error;
if self.integral < self.parameters.integral_min {
self.integral = self.parameters.integral_min;
}
if self.integral < self.parameters.integral_min.into() {
self.integral = self.parameters.integral_min.into();
if self.integral > self.parameters.integral_max {
self.integral = self.parameters.integral_max;
}
if self.integral > self.parameters.integral_max.into() {
self.integral = self.parameters.integral_max.into();
}
let i = self.integral * f64::from(self.parameters.ki);
let i = self.parameters.ki * self.integral;
// derivative
let d = match self.last_input {
None =>
0.0,
Some(last_input) =>
f64::from(self.parameters.kd) * (last_input - input) / time_delta,
None => 0.0,
Some(last_input) => self.parameters.kd * (last_input - input)
};
self.last_input = Some(input);
// output
let mut output = p + i + d;
if output < self.parameters.output_min.into() {
output = self.parameters.output_min.into();
if output < self.parameters.output_min {
output = self.parameters.output_min;
}
if output > self.parameters.output_max.into() {
output = self.parameters.output_max.into();
if output > self.parameters.output_max {
output = self.parameters.output_max;
}
self.last_output = Some(output);
output
}
pub fn summary(&self, channel: usize) -> Summary {
Summary {
channel,
parameters: self.parameters.clone(),
target: self.target,
integral: self.integral,
}
#[allow(dead_code)]
pub fn reset(&mut self) {
self.integral = 0.0;
self.last_input = None;
}
pub fn update_ki(&mut self, new_ki: f32) {
if new_ki == 0.0 {
self.integral = 0.0;
} else {
// Rescale integral with changes to kI, aka "Bumpless operation"
self.integral = f64::from(self.parameters.ki) * self.integral / f64::from(new_ki);
}
self.parameters.ki = new_ki;
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct Summary {
channel: usize,
parameters: Parameters,
target: f64,
integral: f64,
}
#[cfg(test)]
@ -142,48 +86,38 @@ mod test {
use super::*;
const PARAMETERS: Parameters = Parameters {
kp: 0.03,
ki: 0.002,
kd: 0.15,
kp: 0.055,
ki: 0.005,
kd: 0.04,
output_min: -10.0,
output_max: 10.0,
integral_min: -1000.0,
integral_max: 1000.0,
integral_min: -100.0,
integral_max: 100.0,
};
#[test]
fn test_controller() {
// 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;
const DEFAULT: f64 = 0.0;
const TARGET: f64 = 1234.56;
const ERROR: f64 = 0.01;
const DELAY: usize = 10;
let mut pid = Controller::new(PARAMETERS.clone());
pid.target = TARGET;
pid.set_target(TARGET);
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)) && total_t < CYCLE_LIMIT {
while !values.iter().all(|value| target.contains(value)) {
let next_t = (t + 1) % DELAY;
// Feed the oldest temperature
output = pid.update(values[next_t], Time::new::<second>(1.0), ElectricCurrent::new::<ampere>(output));
// Overwrite oldest with previous temperature - output
values[next_t] = values[t] + output - (values[t] - DEFAULT) * LOSS;
let output = pid.update(values[next_t]);
// Overwrite oldest with previous temperature + output
values[next_t] = values[t] + output;
t = next_t;
total_t += 1;
println!("{}", values[t].to_string());
}
assert_ne!(CYCLE_LIMIT, total_t);
dbg!(values[t], total_t);
}
}

View File

@ -1,7 +1,8 @@
use stm32f4xx_hal::{
adc::Adc,
hal::{blocking::spi::Transfer, digital::v2::{InputPin, OutputPin}},
gpio::{
AF5, Alternate, AlternateOD, Analog, Floating, Input,
AF5, Alternate, Analog,
gpioa::*,
gpiob::*,
gpioc::*,
@ -10,55 +11,36 @@ use stm32f4xx_hal::{
gpiog::*,
GpioExt,
Output, PushPull,
Speed::VeryHigh,
},
hal::{self, blocking::spi::Transfer, digital::v2::OutputPin},
i2c::I2c,
otg_fs::USB,
rcc::Clocks,
pwm::{self, PwmChannels},
spi::{Spi, NoMiso},
stm32::{
ADC1,
GPIOA, GPIOB, GPIOC, GPIOD, GPIOE, GPIOF, GPIOG,
I2C1,
OTG_FS_GLOBAL, OTG_FS_DEVICE, OTG_FS_PWRCLK,
SPI2, SPI4, SPI5,
TIM1, TIM3,
},
stm32::{ADC1, ADC2, ADC3, GPIOA, GPIOB, GPIOC, GPIOE, GPIOF, GPIOG, SPI2, SPI4, SPI5, TIM1, TIM3},
time::U32Ext,
};
use eeprom24x::{self, Eeprom24x};
use stm32_eth::EthPins;
use crate::{
channel::{Channel0, Channel1},
leds::Leds,
};
use crate::channel::{Channel0, Channel1};
use crate::softspi::SoftSpi;
pub type Eeprom = Eeprom24x<
I2c<I2C1, (
PB8<AlternateOD<stm32f4xx_hal::gpio::AF4>>,
PB9<AlternateOD<stm32f4xx_hal::gpio::AF4>>
)>,
eeprom24x::page_size::B8,
eeprom24x::addr_size::OneByte
>;
pub type EthernetPins = EthPins<
PA1<Input<Floating>>,
PA2<Input<Floating>>,
PC1<Input<Floating>>,
PA7<Input<Floating>>,
PB11<Input<Floating>>,
PG13<Input<Floating>>,
PB13<Input<Floating>>,
PC4<Input<Floating>>,
PC5<Input<Floating>>,
>;
pub struct DummyInputPin;
impl InputPin for DummyInputPin {
type Error = (); // `Void`
fn is_high(&self) -> Result<bool, Self::Error> {
Ok(false)
}
fn is_low(&self) -> Result<bool, Self::Error> {
Ok(true)
}
}
pub trait ChannelPins {
type DacSpi: Transfer<u8>;
type DacSync: OutputPin;
type Shdn: OutputPin;
type Adc;
type VRefPin;
type ItecPin;
type DacFeedbackPin;
@ -69,6 +51,7 @@ impl ChannelPins for Channel0 {
type DacSpi = Dac0Spi;
type DacSync = PE4<Output<PushPull>>;
type Shdn = PE10<Output<PushPull>>;
type Adc = Adc<ADC1>;
type VRefPin = PA0<Analog>;
type ItecPin = PA6<Analog>;
type DacFeedbackPin = PA4<Analog>;
@ -79,6 +62,7 @@ impl ChannelPins for Channel1 {
type DacSpi = Dac1Spi;
type DacSync = PF6<Output<PushPull>>;
type Shdn = PE15<Output<PushPull>>;
type Adc = Adc<ADC2>;
type VRefPin = PA3<Analog>;
type ItecPin = PB0<Analog>;
type DacFeedbackPin = PA5<Analog>;
@ -88,14 +72,16 @@ impl ChannelPins for Channel1 {
/// SPI peripheral used for communication with the ADC
pub type AdcSpi = Spi<SPI2, (PB10<Alternate<AF5>>, PB14<Alternate<AF5>>, PB15<Alternate<AF5>>)>;
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>>)>;
pub type PinsAdc = Adc<ADC1>;
type Dac0Spi = SoftSpi<PE2<Output<PushPull>>, PE6<Output<PushPull>>, DummyInputPin>;
type Dac1Spi = SoftSpi<PF7<Output<PushPull>>, PF9<Output<PushPull>>, DummyInputPin>;
pub type TecUMeasAdc = Adc<ADC3>;
pub struct ChannelPinSet<C: ChannelPins> {
pub dac_spi: C::DacSpi,
pub dac_sync: C::DacSync,
pub shdn: C::Shdn,
pub adc: C::Adc,
pub vref_pin: C::VRefPin,
pub itec_pin: C::ItecPin,
pub dac_feedback_pin: C::DacFeedbackPin,
@ -105,7 +91,7 @@ pub struct ChannelPinSet<C: ChannelPins> {
pub struct Pins {
pub adc_spi: AdcSpi,
pub adc_nss: AdcNss,
pub pins_adc: PinsAdc,
pub tec_u_meas_adc: TecUMeasAdc,
pub pwm: PwmPins,
pub channel0: ChannelPinSet<Channel0>,
pub channel1: ChannelPinSet<Channel1>,
@ -116,24 +102,26 @@ impl Pins {
pub fn setup(
clocks: Clocks,
tim1: TIM1, tim3: TIM3,
gpioa: GPIOA, gpiob: GPIOB, gpioc: GPIOC, gpiod: GPIOD, gpioe: GPIOE, gpiof: GPIOF, gpiog: GPIOG,
i2c1: I2C1,
gpioa: GPIOA, gpiob: GPIOB, gpioc: GPIOC, gpioe: GPIOE, gpiof: GPIOF, gpiog: GPIOG,
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) {
adc1: ADC1, adc2: ADC2, adc3: ADC3,
) -> Self {
let gpioa = gpioa.split();
let gpiob = gpiob.split();
let gpioc = gpioc.split();
let gpiod = gpiod.split();
let gpioe = gpioe.split();
let gpiof = gpiof.split();
let gpiog = gpiog.split();
Self::setup_ethernet(
gpioa.pa1, gpioa.pa2, gpioc.pc1, gpioa.pa7,
gpioc.pc4, gpioc.pc5, gpiob.pb11, gpiog.pg13,
gpiob.pb13
);
let adc_spi = Self::setup_spi_adc(clocks, spi2, gpiob.pb10, gpiob.pb14, gpiob.pb15);
let adc_nss = gpiob.pb12.into_push_pull_output();
let pins_adc = Adc::adc1(adc1, true, Default::default());
let tec_u_meas_adc = Adc::adc3(adc3, true, Default::default());
let pwm = PwmPins::setup(
clocks, tim1, tim3,
@ -148,6 +136,8 @@ impl Pins {
);
let mut shdn0 = gpioe.pe10.into_push_pull_output();
let _ = shdn0.set_low();
let mut adc0 = Adc::adc1(adc1, true, Default::default());
adc0.enable();
let vref0_pin = gpioa.pa0.into_analog();
let itec0_pin = gpioa.pa6.into_analog();
let dac_feedback0_pin = gpioa.pa4.into_analog();
@ -156,6 +146,7 @@ impl Pins {
dac_spi: dac0_spi,
dac_sync: dac0_sync,
shdn: shdn0,
adc: adc0,
vref_pin: vref0_pin,
itec_pin: itec0_pin,
dac_feedback_pin: dac_feedback0_pin,
@ -168,6 +159,8 @@ impl Pins {
);
let mut shdn1 = gpioe.pe15.into_push_pull_output();
let _ = shdn1.set_low();
let mut adc1 = Adc::adc2(adc2, true, Default::default());
adc1.enable();
let vref1_pin = gpioa.pa3.into_analog();
let itec1_pin = gpiob.pb0.into_analog();
let dac_feedback1_pin = gpioa.pa5.into_analog();
@ -176,49 +169,20 @@ impl Pins {
dac_spi: dac1_spi,
dac_sync: dac1_sync,
shdn: shdn1,
adc: adc1,
vref_pin: vref1_pin,
itec_pin: itec1_pin,
dac_feedback_pin: dac_feedback1_pin,
tec_u_meas_pin: tec_u_meas1_pin,
};
let pins = Pins {
Pins {
adc_spi, adc_nss,
pins_adc,
tec_u_meas_adc,
pwm,
channel0,
channel1,
};
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 = 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,
tx_d1: gpiob.pb13,
rx_d0: gpioc.pc4,
rx_d1: gpioc.pc5,
};
let usb = USB {
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(),
hclk: clocks.hclk(),
};
(pins, leds, eeprom, eth_pins, usb)
}
}
/// Configure the GPIO pins for SPI operation, and initialize SPI
@ -245,15 +209,11 @@ impl Pins {
fn setup_dac0<M1, M2, M3>(
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(
spi4,
(sclk, NoMiso, sdin),
crate::ad5680::SPI_MODE,
crate::ad5680::SPI_CLOCK.into(),
clocks
) -> (Dac0Spi, PE4<Output<PushPull>>) {
let sclk = sclk.into_push_pull_output();
let sdin = sdin.into_push_pull_output();
let spi = SoftSpi::new(
sclk, sdin, DummyInputPin,
);
let sync = sync.into_push_pull_output();
@ -263,20 +223,42 @@ impl Pins {
fn setup_dac1<M1, M2, M3>(
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(
spi5,
(sclk, NoMiso, sdin),
crate::ad5680::SPI_MODE,
crate::ad5680::SPI_CLOCK.into(),
clocks
) -> (Dac1Spi, PF6<Output<PushPull>>) {
let sclk = sclk.into_push_pull_output();
let sdin = sdin.into_push_pull_output();
let spi = SoftSpi::new(
sclk, sdin, DummyInputPin,
);
let sync = sync.into_push_pull_output();
(spi, sync)
}
/// Configure the GPIO pins for Ethernet operation
fn setup_ethernet<M1, M2, M3, M4, M5, M6, M7, M8, M9>(
pa1: PA1<M1>, pa2: PA2<M2>, pc1: PC1<M3>, pa7: PA7<M4>,
pc4: PC4<M5>, pc5: PC5<M6>, pb11: PB11<M7>, pg13: PG13<M8>,
pb13: PB13<M9>
) {
// PA1 RMII Reference Clock - SB13 ON
pa1.into_alternate_af11().set_speed(VeryHigh);
// PA2 RMII MDIO - SB160 ON
pa2.into_alternate_af11().set_speed(VeryHigh);
// PC1 RMII MDC - SB164 ON
pc1.into_alternate_af11().set_speed(VeryHigh);
// PA7 RMII RX Data Valid D11 JP6 ON
pa7.into_alternate_af11().set_speed(VeryHigh);
// PC4 RMII RXD0 - SB178 ON
pc4.into_alternate_af11().set_speed(VeryHigh);
// PC5 RMII RXD1 - SB181 ON
pc5.into_alternate_af11().set_speed(VeryHigh);
// PB11 RMII TX Enable - SB183 ON
pb11.into_alternate_af11().set_speed(VeryHigh);
// PG13 RXII TXD0 - SB182 ON
pg13.into_alternate_af11().set_speed(VeryHigh);
// PB13 RMII TXD1 I2S_A_CK JP7 ON
pb13.into_alternate_af11().set_speed(VeryHigh);
}
}
pub struct PwmPins {
@ -302,17 +284,11 @@ impl PwmPins {
) -> PwmPins {
let freq = 20u32.khz();
fn init_pwm_pin<P: hal::PwmPin<Duty=u16>>(pin: &mut P) {
pin.set_duty(0);
pin.enable();
}
let channels = (
max_v0.into_alternate_af2(),
max_v1.into_alternate_af2(),
);
let (mut max_v0, mut max_v1) = pwm::tim3(tim3, channels, clocks, freq);
init_pwm_pin(&mut max_v0);
init_pwm_pin(&mut max_v1);
let (max_v0, max_v1) = pwm::tim3(tim3, channels, clocks, freq);
let channels = (
max_i_pos0.into_alternate_af1(),
@ -320,12 +296,8 @@ impl PwmPins {
max_i_neg0.into_alternate_af1(),
max_i_neg1.into_alternate_af1(),
);
let (mut max_i_pos0, mut max_i_pos1, mut max_i_neg0, mut max_i_neg1) =
let (max_i_pos0, max_i_pos1, max_i_neg0, max_i_neg1) =
pwm::tim1(tim1, channels, clocks, freq);
init_pwm_pin(&mut max_i_pos0);
init_pwm_pin(&mut max_i_neg0);
init_pwm_pin(&mut max_i_pos1);
init_pwm_pin(&mut max_i_neg1);
PwmPins {
max_v0, max_v1,

View File

@ -3,10 +3,8 @@ use smoltcp::{
iface::EthernetInterface,
socket::{SocketSet, SocketHandle, TcpSocket, TcpSocketBuffer, SocketRef},
time::Instant,
wire::{IpAddress, IpCidr, Ipv4Address, Ipv4Cidr},
};
use crate::command_parser::Ipv4Config;
use crate::net::split_ipv4_config;
pub struct SocketState<S> {
handle: SocketHandle,
@ -85,40 +83,4 @@ impl<'a, 'b, S: Default> Server<'a, 'b, S> {
callback(socket, &mut state.state);
}
}
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(ipv4_address);
// done
break
}
_ => {
// skip
}
}
}
});
}
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

@ -38,16 +38,16 @@ impl LineReader {
}
}
pub enum SessionInput {
pub enum SessionOutput {
Nothing,
Command(Command),
Error(ParserError),
}
impl From<Result<Command, ParserError>> for SessionInput {
impl From<Result<Command, ParserError>> for SessionOutput {
fn from(input: Result<Command, ParserError>) -> Self {
input.map(SessionInput::Command)
.unwrap_or_else(SessionInput::Error)
input.map(SessionOutput::Command)
.unwrap_or_else(SessionOutput::Error)
}
}
@ -106,7 +106,7 @@ impl Session {
self.report_pending[channel] = false;
}
pub fn feed(&mut self, buf: &[u8]) -> (usize, SessionInput) {
pub fn feed(&mut self, buf: &[u8]) -> (usize, SessionOutput) {
let mut buf_bytes = 0;
for (i, b) in buf.iter().enumerate() {
buf_bytes = i + 1;
@ -125,6 +125,6 @@ impl Session {
None => {}
}
}
(buf_bytes, SessionInput::Nothing)
(buf_bytes, SessionOutput::Nothing)
}
}

147
src/softspi.rs Normal file
View File

@ -0,0 +1,147 @@
use stm32f4xx_hal::hal::{
spi::FullDuplex,
digital::v2::{InputPin, OutputPin},
blocking::spi::{Transfer, transfer},
};
use nb::{block, Error, Error::WouldBlock};
use crate::timer::now;
/// Bit-banged Mode3 SPI
pub struct SoftSpi<SCK, MOSI, MISO> {
sck: SCK,
mosi: MOSI,
miso: MISO,
state: State,
input: Option<u8>,
}
#[derive(PartialEq)]
enum State {
Idle,
Transfer {
clock_phase: bool,
mask: u8,
output: u8,
input: u8,
},
}
impl<SCK: OutputPin, MOSI: OutputPin, MISO: InputPin> SoftSpi<SCK, MOSI, MISO> {
pub fn new(mut sck: SCK, mut mosi: MOSI, miso: MISO) -> Self {
let _ = sck.set_high();
let _ = mosi.set_low();
SoftSpi {
sck, mosi, miso,
state: State::Idle,
input: None,
}
}
/// Call this at twice the data rate
pub fn tick(&mut self) {
match self.state {
State::Idle => {}
State::Transfer { clock_phase: false,
mask, output, input } => {
if output & mask != 0 {
let _ = self.mosi.set_high();
} else {
let _ = self.mosi.set_low();
}
let _ = self.sck.set_low();
self.state = State::Transfer {
clock_phase: true,
mask, output, input,
};
}
State::Transfer { clock_phase: true,
mask, output, mut input } => {
if self.miso.is_high().unwrap_or(false) {
input |= mask;
}
let _ = self.sck.set_high();
if mask != 1 {
self.state = State::Transfer {
clock_phase: false,
mask: mask >> 1,
output, input,
};
} else {
self.input = Some(input);
self.state = State::Idle;
}
}
}
}
pub fn run(&mut self) {
while self.state != State::Idle {
self.tick();
spi_delay();
}
}
fn retry<R, E, F>(&mut self, f: &F) -> Result<R, E>
where
F: Fn(&'_ mut SoftSpi<SCK, MOSI, MISO>) -> Result<R, nb::Error<E>>
{
loop {
match f(self) {
Ok(r) => return Ok(r),
Err(nb::Error::Other(e)) => return Err(e),
Err(WouldBlock) => self.run(),
}
}
}
}
impl<SCK: OutputPin, MOSI: OutputPin, MISO: InputPin> FullDuplex<u8> for SoftSpi<SCK, MOSI, MISO> {
type Error = ();
fn read(&mut self) -> Result<u8, nb::Error<Self::Error>> {
match self.input.take() {
Some(input) =>
Ok(input),
None if self.state == State::Idle =>
Err(nb::Error::Other(())),
None =>
Err(WouldBlock),
}
}
fn send(&mut self, output: u8) -> Result<(), nb::Error<Self::Error>> {
match self.state {
State::Idle => {
self.state = State::Transfer {
clock_phase: false,
mask: 0x80,
output,
input: 0,
};
Ok(())
}
_ => Err(WouldBlock)
}
}
}
impl<SCK: OutputPin, MOSI: OutputPin, MISO: InputPin> Transfer<u8> for SoftSpi<SCK, MOSI, MISO> {
// TODO: proper type
type Error = ();
fn transfer<'w>(&mut self, words: &'w mut [u8]) -> Result<&'w [u8], Self::Error> {
for b in words.iter_mut() {
self.retry(&|spi| spi.send(*b))?;
*b = self.retry(&|spi| spi.read())?;
}
Ok(words)
}
}
fn spi_delay() {
const DELAY: u32 = 1;
let start = now();
while now() - start < DELAY {}
}

View File

@ -1,40 +1,31 @@
use num_traits::float::Float;
use uom::si::{
f64::{
ElectricalResistance,
ThermodynamicTemperature,
},
electrical_resistance::ohm,
ratio::ratio,
thermodynamic_temperature::{degree_celsius, kelvin},
};
use serde::{Deserialize, Serialize};
/// Steinhart-Hart equation parameters
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[derive(Clone, Debug)]
pub struct Parameters {
/// Base temperature
pub t0: ThermodynamicTemperature,
/// Base resistance
pub r0: ElectricalResistance,
/// Beta
pub t0: f64,
pub b: f64,
pub r0: f64,
}
impl Parameters {
/// Perform the voltage to temperature conversion.
pub fn get_temperature(&self, r: ElectricalResistance) -> ThermodynamicTemperature {
let inv_temp = 1.0 / self.t0.get::<kelvin>() + (r / self.r0).get::<ratio>().ln() / self.b;
ThermodynamicTemperature::new::<kelvin>(1.0 / inv_temp)
///
/// Result unit: Kelvin
///
/// TODO: verify
pub fn get_temperature(&self, r: f64) -> f64 {
let inv_temp = 1.0 / self.t0 + (r / self.r0).ln() / self.b;
1.0 / inv_temp
}
}
impl Default for Parameters {
fn default() -> Self {
Parameters {
t0: ThermodynamicTemperature::new::<degree_celsius>(25.0),
r0: ElectricalResistance::new::<ohm>(10_000.0),
b: 3800.0,
t0: 0.001_4,
b: 0.000_000_099,
r0: 5_110.0,
}
}
}

View File

@ -39,9 +39,3 @@ pub fn now() -> u32 {
.deref()
})
}
/// block for at least `amount` milliseconds
pub fn sleep(amount: u32) {
let start = now();
while now() - start <= amount {}
}

65
src/units.rs Normal file
View File

@ -0,0 +1,65 @@
use core::{
fmt,
ops::{Add, Div, Neg, Sub},
};
macro_rules! impl_add_sub {
($Type: ident) => {
impl Add<$Type> for $Type {
type Output = $Type;
fn add(self, rhs: $Type) -> $Type {
$Type(self.0 + rhs.0)
}
}
impl Sub<$Type> for $Type {
type Output = $Type;
fn sub(self, rhs: $Type) -> $Type {
$Type(self.0 - rhs.0)
}
}
impl Neg for $Type {
type Output = $Type;
fn neg(self) -> $Type {
$Type(-self.0)
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Volts(pub f64);
impl_add_sub!(Volts);
impl fmt::Display for Volts {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:.3}V", self.0)
}
}
impl Div<Ohms> for Volts {
type Output = Amps;
fn div(self, rhs: Ohms) -> Amps {
Amps(self.0 / rhs.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Amps(pub f64);
impl_add_sub!(Amps);
impl fmt::Display for Amps {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:.3}A", self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Ohms(pub f64);
impl_add_sub!(Ohms);
impl fmt::Display for Ohms {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:.3}Ω", self.0)
}
}

View File

@ -1,103 +0,0 @@
use core::{fmt::{self, Write}, mem::MaybeUninit};
use cortex_m::interrupt::free;
use stm32f4xx_hal::{
otg_fs::{USB, UsbBus as Bus},
stm32::{interrupt, Interrupt, NVIC},
};
use usb_device::{
class_prelude::{UsbBusAllocator},
prelude::{UsbDevice, UsbDeviceBuilder, UsbVidPid},
};
use usbd_serial::SerialPort;
use log::{Record, Log, Metadata};
static mut EP_MEMORY: [u32; 1024] = [0; 1024];
static mut BUS: MaybeUninit<UsbBusAllocator<Bus<USB>>> = MaybeUninit::uninit();
// static mut SERIAL_DEV: Option<(SerialPort<'static, Bus<USB>>, UsbDevice<'static, Bus<USB>>)> = None;
static mut STATE: Option<State> = None;
pub struct State {
serial: SerialPort<'static, Bus<USB>>,
dev: UsbDevice<'static, Bus<USB>>,
}
impl State {
pub fn setup(usb: USB) {
unsafe { BUS.write(Bus::new(usb, &mut EP_MEMORY)) };
let bus = unsafe { BUS.assume_init_ref() };
let serial = SerialPort::new(bus);
let dev = UsbDeviceBuilder::new(bus, UsbVidPid(0x16c0, 0x27dd))
.manufacturer("M-Labs")
.product("thermostat")
.device_release(0x20)
.self_powered(true)
.device_class(usbd_serial::USB_CLASS_CDC)
.build();
free(|_| {
unsafe { STATE = Some(State { serial, dev }); }
});
unsafe {
NVIC::unmask(Interrupt::OTG_FS);
}
}
pub fn get() -> Option<&'static mut Self> {
unsafe { STATE.as_mut() }
}
pub fn poll() {
if let Some(ref mut s) = Self::get() {
if s.dev.poll(&mut [&mut s.serial]) {
// discard any input
let mut buf = [0u8; 64];
let _ = s.serial.read(&mut buf);
}
}
}
}
#[interrupt]
fn OTG_FS() {
free(|_| {
State::poll();
});
}
pub struct Logger;
impl Log for Logger {
fn enabled(&self, _: &Metadata) -> bool {
true
}
fn log(&self, record: &Record) {
if self.enabled(record.metadata()) {
let mut output = SerialOutput;
let _ = writeln!(&mut output, "{} - {}", record.level(), record.args());
}
}
fn flush(&self) {
if let Some(ref mut state) = State::get() {
let _ = free(|_| state.serial.flush());
}
}
}
pub struct SerialOutput;
impl Write for SerialOutput {
fn write_str(&mut self, s: &str) -> core::result::Result<(), core::fmt::Error> {
if let Some(ref mut state) = State::get() {
for chunk in s.as_bytes().chunks(16) {
free(|_| state.serial.write(chunk))
.map_err(|_| fmt::Error)?;
}
}
Ok(())
}
}