forked from M-Labs/thermostat
Compare commits
1 Commits
Author | SHA1 | Date | |
---|---|---|---|
f323c1be63 |
286
Cargo.lock
generated
286
Cargo.lock
generated
@ -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",
|
||||
@ -134,31 +120,13 @@ 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.4"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aed16eb761d0ee9161dd1319cb38c8007813b20f9720a5a682b283e7b8cdfe58"
|
||||
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,36 +277,6 @@ version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b88fa983de7720629c9387e9f517353ed404164b1e482c970a90c1a4aaf7dc1a"
|
||||
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.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cbd1ae72adb44aab48f325a02444a5fc079349a8d804c1fc922aed3f7454c74e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smoltcp"
|
||||
version = "0.6.0"
|
||||
@ -405,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",
|
||||
@ -423,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",
|
||||
@ -435,112 +321,64 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "stm32f4xx-hal"
|
||||
version = "0.8.3"
|
||||
source = "git+https://github.com/stm32-rs/stm32f4xx-hal.git#e80925770d2fe72f0f01a7b46147f4e31d512689"
|
||||
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.48"
|
||||
version = "1.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc371affeffc477f42a221a1e4297aedcea33d47d19b61455588bd9d8f6b19ac"
|
||||
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",
|
||||
"postcard",
|
||||
"serde",
|
||||
"serde-json-core",
|
||||
"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"
|
||||
@ -550,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"
|
||||
|
27
Cargo.toml
27
Cargo.toml
@ -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"] }
|
||||
postcard = "0.5"
|
||||
heapless = "0.5"
|
||||
serde-json-core = "0.1"
|
||||
nb = "0.1"
|
||||
|
||||
[patch.crates-io]
|
||||
stm32f4xx-hal = { git = "https://github.com/stm32-rs/stm32f4xx-hal.git" }
|
||||
|
||||
# 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
|
||||
|
169
README.md
169
README.md
@ -46,149 +46,26 @@ The scope of this setting is per TCP session.
|
||||
|
||||
### Commands
|
||||
|
||||
| Syntax | Function |
|
||||
| --- | --- |
|
||||
| `report` | Show current input |
|
||||
| `report mode` | Show current report mode |
|
||||
| `report mode <off/on>` | Set report mode |
|
||||
| `pwm` | Show current PWM settings |
|
||||
| `pwm <0/1> max_i_pos <ratio>` | Set PWM duty cycle for **max_i_pos** to *ampere* |
|
||||
| `pwm <0/1> max_i_neg <ratio>` | Set PWM duty cycle for **max_i_neg** to *ampere* |
|
||||
| `pwm <0/1> max_v <ratio>` | Set PWM duty cycle for **max_v** to *volt* |
|
||||
| `pwm <0/1> <volts>` | Disengage PID, set **i_set** DAC to *ampere* |
|
||||
| `pwm <0/1> pid` | Set PWM to be controlled by PID |
|
||||
| `center <0/1> <volts>` | Set the MAX1968 0A-centerpoint to *volts* |
|
||||
| `center <0/1> vref` | Set the MAX1968 0A-centerpoint to measure from VREF |
|
||||
| `pid` | Show PID configuration |
|
||||
| `pid <0/1> target <value>` | Set the PID controller target |
|
||||
| `pid <0/1> kp <value>` | Set proportional gain |
|
||||
| `pid <0/1> ki <value>` | Set integral gain |
|
||||
| `pid <0/1> kd <value>` | Set differential gain |
|
||||
| `pid <0/1> output_min <value>` | Set mininum output |
|
||||
| `pid <0/1> output_max <value>` | Set maximum output |
|
||||
| `pid <0/1> integral_min <value>` | Set integral lower bound |
|
||||
| `pid <0/1> integral_max <value>` | Set integral upper bound |
|
||||
| `s-h` | Show Steinhart-Hart equation parameters |
|
||||
| `s-h <0/1> <t0/b/r0> <value>` | Set Steinhart-Hart parameter for a channel |
|
||||
| `postfilter` | Show postfilter settings |
|
||||
| `postfilter <0/1> off` | Disable postfilter |
|
||||
| `postfilter <0/1> rate <rate>` | Set postfilter output data rate |
|
||||
| `load` | Restore configuration from EEPROM |
|
||||
| `save` | Save configuration to EEPROM |
|
||||
| `reset` | Reset the device |
|
||||
| `ipv4 <X.X.X.X>` | Configure IPv4 address |
|
||||
|
||||
|
||||
## 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 once 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
|
||||
```
|
||||
|
||||
|
||||
## Thermo-Electric Cooling (TEC)
|
||||
|
||||
- Connect Peltier device 0 to TEC0- and TEC0+.
|
||||
- Connect Peliter device 1 to TEC1- and TEC1+.
|
||||
- The GND pin is for shielding not for sinking Peltier currents.
|
||||
|
||||
### 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 |
|
||||
| | Amperes | Output current control |
|
||||
|
||||
Example: set the maximum voltage of channel 0 to 1.5 V.
|
||||
```
|
||||
pwm 0 max_v 1.5
|
||||
```
|
||||
|
||||
### 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 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 |
|
||||
| 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 |
|
||||
|
@ -1 +1 @@
|
||||
"055x3b3kqi7bi17ya6iaiq9hlsiy8f3v6bn47s6dizc6y4xn9v2y"
|
||||
"0ma8dxsw90jrbxb3cd873k98g3pixnqvb059blvg7kf4m5aj9fnq"
|
||||
|
124
pytec/plot.py
124
pytec/plot.py
@ -1,124 +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(lambda t: t - target_temperature),
|
||||
'i_set': Series(),
|
||||
'pid_output': Series(),
|
||||
'vref': Series(),
|
||||
'dac_value': Series(),
|
||||
'dac_feedback': Series(),
|
||||
'i_tec': Series(),
|
||||
'tec_i': Series(),
|
||||
'tec_u_meas': Series(),
|
||||
}
|
||||
series_lock = Lock()
|
||||
|
||||
quit = False
|
||||
|
||||
def recv_data(tec):
|
||||
for data in tec.report_mode():
|
||||
if data['channel'] == 0:
|
||||
series_lock.acquire()
|
||||
try:
|
||||
time = data['time'] / 1000.0
|
||||
for k, s in series.iteritems():
|
||||
v = data[k]
|
||||
if data.has_key(k) and type(v) is float:
|
||||
s.append(time, v)
|
||||
finally:
|
||||
series_lock.release()
|
||||
|
||||
if quit:
|
||||
break
|
||||
|
||||
thread = Thread(target=recv_data, args=(tec,))
|
||||
thread.start()
|
||||
|
||||
fig, ax = plt.subplots()
|
||||
|
||||
for k, s in series.iteritems():
|
||||
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.iteritems():
|
||||
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 is not None and max_x - TIME_WINDOW > min_x:
|
||||
for s in series.itervalues():
|
||||
s.clip(max_x - TIME_WINDOW)
|
||||
finally:
|
||||
series_lock.release()
|
||||
|
||||
margin_y = 0.01 * (max_y - min_y)
|
||||
ax.set_xlim(min_x, max_x)
|
||||
ax.set_ylim(min_y - margin_y, max_y + margin_y)
|
||||
|
||||
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()
|
@ -1,163 +0,0 @@
|
||||
import socket
|
||||
import json
|
||||
|
||||
CHANNELS = 2
|
||||
|
||||
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 _command(self, *command):
|
||||
self._socket.sendall((" ".join(command) + "\n").encode('utf-8'))
|
||||
|
||||
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 _get_conf(self, topic):
|
||||
self._command(topic)
|
||||
result = []
|
||||
for channel in range(0, CHANNELS):
|
||||
line = self._read_line()
|
||||
conf = json.loads(line)
|
||||
result.append(conf)
|
||||
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")
|
||||
|
@ -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(),
|
||||
)
|
@ -1,8 +0,0 @@
|
||||
from pytec.client import Client
|
||||
|
||||
tec = Client() #(host="localhost", port=6667)
|
||||
tec.set_param("s-h", 1, "t0", 20)
|
||||
print(tec.get_pid())
|
||||
print(tec.get_steinhart_hart())
|
||||
for data in tec.report_mode():
|
||||
print(data)
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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(®s::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(®s::Offset { index })?.offset();
|
||||
let gain = self.read_reg(®s::Gain { index })?.gain();
|
||||
let bipolar = self.read_reg(®s::SetupCon { index })?.bipolar();
|
||||
Ok(ChannelCalibration { offset, gain, bipolar })
|
||||
pub fn disable_channel(
|
||||
&mut self, index: u8
|
||||
) -> Result<(), SPI::Error> {
|
||||
self.update_reg(®s::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(®s::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(®s::AdcMode, |adc_mode| {
|
||||
adc_mode.set_mode(Mode::InternalOffsetCalibration);
|
||||
})?;
|
||||
while ! self.read_reg(®s::Status)?.ready() {}
|
||||
|
||||
// system offset calibration
|
||||
self.update_reg(®s::AdcMode, |adc_mode| {
|
||||
adc_mode.set_mode(Mode::SystemOffsetCalibration);
|
||||
})?;
|
||||
while ! self.read_reg(®s::Status)?.ready() {}
|
||||
|
||||
// system gain calibration
|
||||
self.update_reg(®s::AdcMode, |adc_mode| {
|
||||
adc_mode.set_mode(Mode::SystemGainCalibration);
|
||||
})?;
|
||||
while ! self.read_reg(®s::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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -1,7 +1,6 @@
|
||||
use stm32f4xx_hal::hal::digital::v2::OutputPin;
|
||||
use crate::{
|
||||
ad5680,
|
||||
ad7172,
|
||||
channel_state::ChannelState,
|
||||
pins::{ChannelPins, ChannelPinSet},
|
||||
};
|
||||
@ -20,6 +19,8 @@ pub struct Channel<C: ChannelPins> {
|
||||
/// 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
|
||||
@ -28,10 +29,12 @@ 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);
|
||||
// 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;
|
||||
|
||||
@ -39,20 +42,11 @@ impl<C: ChannelPins> Channel<C> {
|
||||
state,
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
@ -1,88 +1,43 @@
|
||||
use smoltcp::time::Instant;
|
||||
use uom::si::{
|
||||
f64::{
|
||||
ElectricPotential,
|
||||
ElectricalResistance,
|
||||
ThermodynamicTemperature,
|
||||
},
|
||||
electric_potential::volt,
|
||||
electrical_resistance::ohm,
|
||||
thermodynamic_temperature::degree_celsius,
|
||||
};
|
||||
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,
|
||||
/// 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),
|
||||
// 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_time = now;
|
||||
}
|
||||
|
||||
impl ChannelState {
|
||||
/// Update PID state on ADC input, calculate new DAC output
|
||||
pub fn update_pid(&mut self) -> Option<f64> {
|
||||
let temperature = self.get_temperature()?
|
||||
.get::<degree_celsius>();
|
||||
let pid_output = self.pid.update(temperature);
|
||||
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(&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)
|
||||
}
|
||||
}
|
||||
|
580
src/channels.rs
580
src/channels.rs
@ -1,63 +1,53 @@
|
||||
use serde::{Serialize, Serializer};
|
||||
use smoltcp::time::Instant;
|
||||
use stm32f4xx_hal::hal;
|
||||
use uom::si::{
|
||||
f64::{ElectricCurrent, ElectricPotential, ElectricalResistance},
|
||||
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;
|
||||
|
||||
// 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::Ain0, ad7172::Input::Ain1).unwrap();
|
||||
let adc_calibration0 = adc.get_calibration(0)
|
||||
.expect("adc_calibration0");
|
||||
adc.setup_channel(1, ad7172::Input::Ain2, ad7172::Input::Ain3).unwrap();
|
||||
let adc_calibration1 = adc.get_calibration(1)
|
||||
.expect("adc_calibration1");
|
||||
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 {
|
||||
@ -73,547 +63,191 @@ impl Channels {
|
||||
self.adc.data_ready().unwrap().map(|channel| {
|
||||
let data = self.adc.read_data().unwrap();
|
||||
|
||||
let state = self.channel_state(channel);
|
||||
state.update(instant, data);
|
||||
match state.update_pid() {
|
||||
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, ElectricPotential) {
|
||||
let dac_factor = match channel.into() {
|
||||
0 => self.channel0.dac_factor,
|
||||
1 => self.channel1.dac_factor,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let voltage = self.channel_state(channel).dac_value;
|
||||
let max = ElectricPotential::new::<volt>(ad5680::MAX_VALUE as f64 / dac_factor);
|
||||
(voltage, max)
|
||||
}
|
||||
|
||||
pub fn get_i(&mut self, channel: usize) -> (ElectricCurrent, ElectricCurrent) {
|
||||
let center_point = self.get_center(channel);
|
||||
let r_sense = ElectricalResistance::new::<ohm>(R_SENSE);
|
||||
let (voltage, max) = self.get_dac(channel);
|
||||
let i_tec = (voltage - center_point) / (10.0 * r_sense);
|
||||
let max = (max - center_point) / (10.0 * r_sense);
|
||||
(i_tec, max)
|
||||
}
|
||||
|
||||
/// i_set DAC
|
||||
fn set_dac(&mut self, channel: usize, voltage: ElectricPotential) -> (ElectricPotential, ElectricPotential) {
|
||||
let dac_factor = match channel.into() {
|
||||
0 => self.channel0.dac_factor,
|
||||
1 => self.channel1.dac_factor,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let value = (voltage.get::<volt>() * dac_factor) as u32;
|
||||
let value = match channel {
|
||||
0 => self.channel0.dac.set(value).unwrap(),
|
||||
1 => self.channel1.dac.set(value).unwrap(),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let voltage = ElectricPotential::new::<volt>(value as f64 / dac_factor);
|
||||
self.channel_state(channel).dac_value = voltage;
|
||||
let max = ElectricPotential::new::<volt>(ad5680::MAX_VALUE as f64 / dac_factor);
|
||||
(voltage, max)
|
||||
}
|
||||
|
||||
pub fn set_i(&mut self, channel: usize, i_tec: ElectricCurrent) -> (ElectricCurrent, ElectricCurrent) {
|
||||
let center_point = self.get_center(channel);
|
||||
let r_sense = ElectricalResistance::new::<ohm>(R_SENSE);
|
||||
let voltage = i_tec * 10.0 * r_sense + center_point;
|
||||
let (voltage, max) = self.set_dac(channel, voltage);
|
||||
let i_tec = (voltage - center_point) / (10.0 * r_sense);
|
||||
let max = (max - center_point) / (10.0 * r_sense);
|
||||
(i_tec, max)
|
||||
}
|
||||
|
||||
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!(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Calibrate the I_SET DAC using the DAC_FB ADC pin.
|
||||
///
|
||||
/// These loops perform a breadth-first search for the DAC setting
|
||||
/// that will produce a `target_voltage`.
|
||||
/// for i_set
|
||||
pub fn calibrate_dac_value(&mut self, channel: usize) {
|
||||
let target_voltage = ElectricPotential::new::<volt>(2.5);
|
||||
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 dac_factor = value as f64 / dac_feedback.get::<volt>();
|
||||
match channel {
|
||||
0 => self.channel0.dac_factor = dac_factor,
|
||||
1 => self.channel1.dac_factor = dac_factor,
|
||||
_ => 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, ElectricPotential) {
|
||||
let vref = self.channel_state(channel).vref;
|
||||
let max = 4.0 * vref;
|
||||
let duty = self.get_pwm(channel, PwmPin::MaxV);
|
||||
(duty * max, 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)
|
||||
}
|
||||
|
||||
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 vref = self.channel_state(channel).vref;
|
||||
let max = 4.0 * vref;
|
||||
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)
|
||||
}
|
||||
|
||||
pub fn report(&mut self, channel: usize) -> Report {
|
||||
let vref = self.channel_state(channel).vref;
|
||||
let (i_set, _) = self.get_i(channel);
|
||||
let i_tec = self.read_itec(channel);
|
||||
let tec_i = (i_tec - vref) / ElectricalResistance::new::<ohm>(0.4);
|
||||
let (dac_value, _) = self.get_dac(channel);
|
||||
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.adc_time.total_millis(),
|
||||
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.read_tec_u_meas(channel),
|
||||
pid_output,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pwm_summary(&mut self, channel: usize) -> PwmSummary {
|
||||
PwmSummary {
|
||||
channel,
|
||||
center: CenterPointJson(self.channel_state(channel).center.clone()),
|
||||
i_set: self.get_i(channel).into(),
|
||||
max_v: self.get_max_v(channel).into(),
|
||||
max_i_pos: self.get_max_i_pos(channel).into(),
|
||||
max_i_neg: self.get_max_i_neg(channel).into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn postfilter_summary(&mut self, channel: usize) -> PostFilterSummary {
|
||||
let rate = self.adc.get_postfilter(channel as u8).unwrap()
|
||||
.and_then(|filter| filter.output_rate());
|
||||
PostFilterSummary { channel, rate }
|
||||
}
|
||||
|
||||
pub fn steinhart_hart_summary(&mut self, channel: usize) -> SteinhartHartSummary {
|
||||
let params = self.channel_state(channel).sh.clone();
|
||||
SteinhartHartSummary { channel, params }
|
||||
}
|
||||
}
|
||||
|
||||
type JsonBuffer = heapless::Vec<u8, heapless::consts::U512>;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct Report {
|
||||
channel: usize,
|
||||
time: i64,
|
||||
adc: Option<ElectricPotential>,
|
||||
sens: Option<ElectricalResistance>,
|
||||
temperature: Option<f64>,
|
||||
pid_engaged: bool,
|
||||
i_set: ElectricCurrent,
|
||||
vref: ElectricPotential,
|
||||
dac_value: ElectricPotential,
|
||||
dac_feedback: ElectricPotential,
|
||||
i_tec: ElectricPotential,
|
||||
tec_i: ElectricCurrent,
|
||||
tec_u_meas: ElectricPotential,
|
||||
pid_output: Option<ElectricCurrent>,
|
||||
}
|
||||
|
||||
impl Report {
|
||||
pub fn to_json(&self) -> Result<JsonBuffer, serde_json_core::ser::Error> {
|
||||
serde_json_core::to_vec(self)
|
||||
}
|
||||
}
|
||||
|
||||
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>,
|
||||
}
|
||||
|
||||
impl PwmSummary {
|
||||
pub fn to_json(&self) -> Result<JsonBuffer, serde_json_core::ser::Error> {
|
||||
serde_json_core::to_vec(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct PostFilterSummary {
|
||||
channel: usize,
|
||||
rate: Option<f32>,
|
||||
}
|
||||
|
||||
impl PostFilterSummary {
|
||||
pub fn to_json(&self) -> Result<JsonBuffer, serde_json_core::ser::Error> {
|
||||
serde_json_core::to_vec(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct SteinhartHartSummary {
|
||||
channel: usize,
|
||||
params: steinhart_hart::Parameters,
|
||||
}
|
||||
|
||||
impl SteinhartHartSummary {
|
||||
pub fn to_json(&self) -> Result<JsonBuffer, serde_json_core::ser::Error> {
|
||||
serde_json_core::to_vec(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn report_to_json() {
|
||||
// `/ 1.1` results in values with a really long serialization
|
||||
let report = Report {
|
||||
channel: 0,
|
||||
time: 3200,
|
||||
adc: Some(ElectricPotential::new::<volt>(0.65 / 1.1)),
|
||||
sens: Some(ElectricalResistance::new::<ohm>(10000.0 / 1.1)),
|
||||
temperature: Some(30.0 / 1.1),
|
||||
pid_engaged: false,
|
||||
i_set: ElectricCurrent::new::<ampere>(0.5 / 1.1),
|
||||
vref: ElectricPotential::new::<volt>(1.5 / 1.1),
|
||||
dac_value: ElectricPotential::new::<volt>(2.0 / 1.1),
|
||||
dac_feedback: ElectricPotential::new::<volt>(2.0 / 1.1),
|
||||
i_tec: ElectricPotential::new::<volt>(2.0 / 1.1),
|
||||
tec_i: ElectricCurrent::new::<ampere>(0.2 / 1.1),
|
||||
tec_u_meas: ElectricPotential::new::<volt>(2.0 / 1.1),
|
||||
pid_output: Some(ElectricCurrent::new::<ampere>(0.5 / 1.1)),
|
||||
};
|
||||
let buf = report.to_json().unwrap();
|
||||
assert_eq!(buf[0], b'{');
|
||||
assert_eq!(buf[buf.len() - 1], b'}');
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pwm_summary_to_json() {
|
||||
let value = 1.0 / 1.1;
|
||||
let max = 5.0 / 1.1;
|
||||
|
||||
let pwm_summary = PwmSummary {
|
||||
channel: 0,
|
||||
center: CenterPointJson(CenterPoint::Vref),
|
||||
i_set: PwmSummaryField {
|
||||
value: ElectricCurrent::new::<ampere>(value),
|
||||
max: ElectricCurrent::new::<ampere>(max),
|
||||
},
|
||||
max_v: PwmSummaryField {
|
||||
value: ElectricPotential::new::<volt>(value),
|
||||
max: ElectricPotential::new::<volt>(max),
|
||||
},
|
||||
max_i_pos: PwmSummaryField {
|
||||
value: ElectricCurrent::new::<ampere>(value),
|
||||
max: ElectricCurrent::new::<ampere>(max),
|
||||
},
|
||||
max_i_neg: PwmSummaryField {
|
||||
value: ElectricCurrent::new::<ampere>(value),
|
||||
max: ElectricCurrent::new::<ampere>(max),
|
||||
},
|
||||
};
|
||||
let buf = pwm_summary.to_json().unwrap();
|
||||
assert_eq!(buf[0], b'{');
|
||||
assert_eq!(buf[buf.len() - 1], b'}');
|
||||
self.set_dac(channel, Volts(0.0));
|
||||
best_value
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,6 @@ use nom::{
|
||||
error::ErrorKind,
|
||||
};
|
||||
use num_traits::{Num, ParseFloatError};
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@ -123,35 +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,
|
||||
Save,
|
||||
Reset,
|
||||
Ipv4([u8; 4]),
|
||||
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,
|
||||
@ -165,7 +161,7 @@ pub enum Command {
|
||||
},
|
||||
PostFilter {
|
||||
channel: usize,
|
||||
rate: Option<f32>,
|
||||
rate: f32,
|
||||
},
|
||||
}
|
||||
|
||||
@ -246,7 +242,7 @@ 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(
|
||||
@ -304,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))),
|
||||
}
|
||||
@ -318,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)?;
|
||||
@ -406,56 +383,25 @@ 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 ipv4(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
|
||||
let (input, _) = tag("ipv4")(input)?;
|
||||
let (input, _) = whitespace(input)?;
|
||||
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)?;
|
||||
end(input)?;
|
||||
|
||||
let result = a.and_then(|a| b.and_then(|b| c.and_then(|c| d.map(|d|
|
||||
Command::Ipv4([a as u8, b as u8, c as u8, d as u8])
|
||||
))));
|
||||
Ok((input, result))
|
||||
}
|
||||
|
||||
fn command(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
|
||||
alt((value(Ok(Command::Quit), tag("quit")),
|
||||
value(Ok(Command::Load), tag("load")),
|
||||
value(Ok(Command::Save), tag("save")),
|
||||
value(Ok(Command::Reset), tag("reset")),
|
||||
ipv4,
|
||||
map(report, Ok),
|
||||
pwm,
|
||||
center_point,
|
||||
pid,
|
||||
steinhart_hart,
|
||||
postfilter,
|
||||
@ -465,7 +411,7 @@ fn command(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
|
||||
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])),
|
||||
@ -485,24 +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));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_save() {
|
||||
let command = Command::parse(b"save");
|
||||
assert_eq!(command, Ok(Command::Save));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_ipv4() {
|
||||
let command = Command::parse(b"ipv4 192.168.1.26");
|
||||
assert_eq!(command, Ok(Command::Ipv4([192, 168, 1, 26])));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_report() {
|
||||
let command = Command::parse(b"report");
|
||||
@ -533,7 +461,7 @@ mod test {
|
||||
assert_eq!(command, Ok(Command::Pwm {
|
||||
channel: 1,
|
||||
pin: PwmPin::ISet,
|
||||
value: 16383.0,
|
||||
duty: 16383,
|
||||
}));
|
||||
}
|
||||
|
||||
@ -542,6 +470,7 @@ mod test {
|
||||
let command = Command::parse(b"pwm 0 pid");
|
||||
assert_eq!(command, Ok(Command::PwmPid {
|
||||
channel: 0,
|
||||
pin: PwmPin::ISet,
|
||||
}));
|
||||
}
|
||||
|
||||
@ -551,7 +480,7 @@ mod test {
|
||||
assert_eq!(command, Ok(Command::Pwm {
|
||||
channel: 0,
|
||||
pin: PwmPin::MaxIPos,
|
||||
value: 7.0,
|
||||
duty: 7,
|
||||
}));
|
||||
}
|
||||
|
||||
@ -561,7 +490,7 @@ mod test {
|
||||
assert_eq!(command, Ok(Command::Pwm {
|
||||
channel: 0,
|
||||
pin: PwmPin::MaxINeg,
|
||||
value: 128.0,
|
||||
duty: 128,
|
||||
}));
|
||||
}
|
||||
|
||||
@ -571,7 +500,7 @@ mod test {
|
||||
assert_eq!(command, Ok(Command::Pwm {
|
||||
channel: 0,
|
||||
pin: PwmPin::MaxV,
|
||||
value: 32768.0,
|
||||
duty: 32768,
|
||||
}));
|
||||
}
|
||||
|
||||
@ -608,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,
|
||||
@ -617,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,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
253
src/config.rs
253
src/config.rs
@ -1,253 +0,0 @@
|
||||
use postcard::{from_bytes, to_slice};
|
||||
use serde::{Serialize, Deserialize};
|
||||
use smoltcp::wire::Ipv4Address;
|
||||
use stm32f4xx_hal::i2c;
|
||||
use uom::si::{
|
||||
electric_potential::volt,
|
||||
electric_current::ampere,
|
||||
electrical_resistance::ohm,
|
||||
f64::{ElectricCurrent, ElectricPotential, ElectricalResistance, ThermodynamicTemperature},
|
||||
thermodynamic_temperature::degree_celsius,
|
||||
};
|
||||
use crate::{
|
||||
ad7172::PostFilter,
|
||||
channels::{CHANNELS, Channels},
|
||||
command_parser::CenterPoint,
|
||||
EEPROM_SIZE, EEPROM_PAGE_SIZE,
|
||||
pid,
|
||||
pins,
|
||||
steinhart_hart,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Eeprom(eeprom24x::Error<i2c::Error>),
|
||||
Encode(postcard::Error),
|
||||
}
|
||||
|
||||
impl From<eeprom24x::Error<i2c::Error>> for Error {
|
||||
fn from(e: eeprom24x::Error<i2c::Error>) -> Self {
|
||||
Error::Eeprom(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<postcard::Error> for Error {
|
||||
fn from(e: postcard::Error) -> Self {
|
||||
Error::Encode(e)
|
||||
}
|
||||
}
|
||||
|
||||
/// Just for encoding/decoding, actual state resides in ChannelState
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
channels: [ChannelConfig; CHANNELS],
|
||||
pub ipv4_address: [u8; 4],
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn new(channels: &mut Channels, ipv4_address: Ipv4Address) -> Self {
|
||||
Config {
|
||||
channels: [
|
||||
ChannelConfig::new(channels, 0),
|
||||
ChannelConfig::new(channels, 1),
|
||||
],
|
||||
ipv4_address: ipv4_address.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// apply loaded config to system
|
||||
pub fn apply(&self, channels: &mut Channels) {
|
||||
for i in 0..CHANNELS {
|
||||
self.channels[i].apply(channels, i);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load(eeprom: &mut pins::Eeprom) -> Result<Self, Error> {
|
||||
let mut buffer = [0; EEPROM_SIZE];
|
||||
eeprom.read_data(0, &mut buffer)?;
|
||||
log::info!("load: {:?}", buffer);
|
||||
let config = from_bytes(&mut buffer)?;
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub fn save(&self, eeprom: &mut pins::Eeprom) -> Result<(), Error> {
|
||||
let mut buffer = [0; EEPROM_SIZE];
|
||||
let config_buffer = to_slice(self, &mut buffer)?;
|
||||
log::info!("save: {:?}", config_buffer);
|
||||
|
||||
let mut addr = 0;
|
||||
for chunk in config_buffer.chunks(EEPROM_PAGE_SIZE) {
|
||||
'write_retry: loop {
|
||||
match eeprom.write_page(addr, chunk) {
|
||||
Ok(()) => break 'write_retry,
|
||||
Err(eeprom24x::Error::I2C(i2c::Error::NACK)) => {},
|
||||
Err(e) => Err(e)?,
|
||||
}
|
||||
}
|
||||
addr += chunk.len() as u32;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ChannelConfig {
|
||||
center: CenterPoint,
|
||||
pid: pid::Parameters,
|
||||
pid_target: f32,
|
||||
sh: SteinhartHartConfig,
|
||||
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,
|
||||
sh: (&state.sh).into(),
|
||||
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.sh = (&self.sh).into();
|
||||
|
||||
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 SteinhartHartConfig {
|
||||
t0: f32,
|
||||
r0: f32,
|
||||
b: f32,
|
||||
}
|
||||
|
||||
impl From<&steinhart_hart::Parameters> for SteinhartHartConfig {
|
||||
fn from(sh: &steinhart_hart::Parameters) -> Self {
|
||||
SteinhartHartConfig {
|
||||
t0: sh.t0.get::<degree_celsius>() as f32,
|
||||
r0: sh.r0.get::<ohm>() as f32,
|
||||
b: sh.b as f32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<steinhart_hart::Parameters> for &SteinhartHartConfig {
|
||||
fn into(self) -> steinhart_hart::Parameters {
|
||||
steinhart_hart::Parameters {
|
||||
t0: ThermodynamicTemperature::new::<degree_celsius>(self.t0.into()),
|
||||
r0: ElectricalResistance::new::<ohm>(self.r0.into()),
|
||||
b: self.b.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
struct PwmLimits {
|
||||
max_v: f32,
|
||||
max_i_pos: f32,
|
||||
max_i_neg: f32,
|
||||
}
|
||||
|
||||
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>() as f32,
|
||||
max_i_pos: max_i_pos.get::<ampere>() as f32,
|
||||
max_i_neg: max_i_neg.get::<ampere>() as f32,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply(&self, channels: &mut Channels, channel: usize) {
|
||||
channels.set_max_v(channel, ElectricPotential::new::<volt>(self.max_v.into()));
|
||||
channels.set_max_i_pos(channel, ElectricCurrent::new::<ampere>(self.max_i_pos.into()));
|
||||
channels.set_max_i_neg(channel, ElectricCurrent::new::<ampere>(self.max_i_neg.into()));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::DEFAULT_IPV4_ADDRESS;
|
||||
|
||||
#[test]
|
||||
fn test_fit_eeprom() {
|
||||
let channel_config = ChannelConfig {
|
||||
center: CenterPoint::Override(1.5),
|
||||
pid: pid::Parameters::default(),
|
||||
pid_target: 93.7,
|
||||
sh: (&steinhart_hart::Parameters::default()).into(),
|
||||
pwm: PwmLimits {
|
||||
max_v: 1.65,
|
||||
max_i_pos: 2.1,
|
||||
max_i_neg: 2.25,
|
||||
},
|
||||
adc_postfilter: PostFilter::F21SPS,
|
||||
};
|
||||
let config = Config {
|
||||
channels: [
|
||||
channel_config.clone(),
|
||||
channel_config.clone(),
|
||||
],
|
||||
ipv4_address: DEFAULT_IPV4_ADDRESS.0,
|
||||
};
|
||||
|
||||
let mut buffer = [0; EEPROM_SIZE];
|
||||
let buffer = to_slice(&config, &mut buffer).unwrap();
|
||||
assert!(buffer.len() <= EEPROM_SIZE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encode_decode() {
|
||||
let channel_config = ChannelConfig {
|
||||
center: CenterPoint::Override(1.5),
|
||||
pid: pid::Parameters::default(),
|
||||
pid_target: 93.7,
|
||||
sh: (&steinhart_hart::Parameters::default()).into(),
|
||||
pwm: PwmLimits {
|
||||
max_v: 1.65,
|
||||
max_i_pos: 2.1,
|
||||
max_i_neg: 2.25,
|
||||
},
|
||||
adc_postfilter: PostFilter::F21SPS,
|
||||
};
|
||||
let config = Config {
|
||||
channels: [
|
||||
channel_config.clone(),
|
||||
channel_config.clone(),
|
||||
],
|
||||
ipv4_address: DEFAULT_IPV4_ADDRESS.0,
|
||||
};
|
||||
|
||||
let mut buffer = [0; EEPROM_SIZE];
|
||||
to_slice(&config, &mut buffer).unwrap();
|
||||
let decoded: Config = from_bytes(&buffer).unwrap();
|
||||
assert_eq!(decoded, config);
|
||||
}
|
||||
}
|
@ -1,11 +1,5 @@
|
||||
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() {
|
||||
|
44
src/leds.rs
44
src/leds.rs
@ -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();
|
||||
}
|
||||
}
|
433
src/main.rs
433
src/main.rs
@ -1,133 +1,82 @@
|
||||
#![cfg_attr(not(test), no_std)]
|
||||
#![cfg_attr(not(test), no_main)]
|
||||
#![feature(maybe_uninit_extra, maybe_uninit_ref)]
|
||||
#![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,
|
||||
watchdog::IndependentWatchdog,
|
||||
time::{U32Ext, MegaHertz},
|
||||
stm32::{CorePeripherals, Peripherals, SCB},
|
||||
stm32::{CorePeripherals, Peripherals},
|
||||
};
|
||||
use smoltcp::{
|
||||
time::Instant,
|
||||
socket::TcpSocket,
|
||||
wire::{EthernetAddress, Ipv4Address},
|
||||
};
|
||||
use uom::{
|
||||
si::{
|
||||
f64::{
|
||||
ElectricCurrent,
|
||||
ElectricPotential,
|
||||
ElectricalResistance,
|
||||
ThermodynamicTemperature,
|
||||
},
|
||||
electric_current::ampere,
|
||||
electric_potential::volt,
|
||||
electrical_resistance::ohm,
|
||||
thermodynamic_temperature::degree_celsius,
|
||||
},
|
||||
wire::EthernetAddress,
|
||||
};
|
||||
|
||||
mod init_log;
|
||||
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::{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::Config;
|
||||
|
||||
|
||||
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;
|
||||
|
||||
pub const EEPROM_PAGE_SIZE: usize = 8;
|
||||
pub const EEPROM_SIZE: usize = 128;
|
||||
|
||||
pub const DEFAULT_IPV4_ADDRESS: Ipv4Address = Ipv4Address([192, 168, 1, 26]);
|
||||
#[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
|
||||
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
|
||||
}
|
||||
|
||||
fn report_to(channel: usize, channels: &mut Channels, socket: &mut TcpSocket) -> bool {
|
||||
match channels.report(channel).to_json() {
|
||||
Ok(buf) =>
|
||||
send_line(socket, &buf[..]),
|
||||
Err(e) => {
|
||||
error!("unable to serialize report: {:?}", e);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialization and main loop
|
||||
#[cfg(not(test))]
|
||||
#[entry]
|
||||
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)
|
||||
@ -143,44 +92,26 @@ 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 ipv4_address = DEFAULT_IPV4_ADDRESS;
|
||||
let mut channels = Channels::new(pins);
|
||||
let _ = Config::load(&mut eeprom)
|
||||
.map(|config| {
|
||||
config.apply(&mut channels);
|
||||
ipv4_address = Ipv4Address::from_bytes(&config.ipv4_address);
|
||||
})
|
||||
.map_err(|e| warn!("error loading config: {:?}", e));
|
||||
info!("IPv4 address: {}", ipv4_address);
|
||||
channels.calibrate_dac_value(0);
|
||||
|
||||
// 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);
|
||||
#[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))
|
||||
};
|
||||
info!("Net hwaddr: {}", hwaddr);
|
||||
|
||||
net::run(clocks, dp.ETHERNET_MAC, dp.ETHERNET_DMA, eth_pins, hwaddr, ipv4_address, |iface| {
|
||||
let mut new_ipv4_address = None;
|
||||
net::run(dp.ETHERNET_MAC, dp.ETHERNET_DMA, hwaddr, |iface| {
|
||||
Server::<Session>::run(iface, |server| {
|
||||
leds.r1.off();
|
||||
|
||||
loop {
|
||||
let instant = Instant::from_millis(i64::from(timer::now()));
|
||||
let updated_channel = channels.poll_adc(instant);
|
||||
@ -200,103 +131,178 @@ fn main() -> ! {
|
||||
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() {
|
||||
} else if socket.can_send() && socket.can_recv() && socket.send_capacity() - socket.send_queue() > 1024 {
|
||||
match socket.recv(|buf| session.feed(buf)) {
|
||||
Ok(SessionInput::Nothing) => {}
|
||||
Ok(SessionInput::Command(command)) => match command {
|
||||
Ok(SessionOutput::Nothing) => {}
|
||||
Ok(SessionOutput::Command(command)) => match command {
|
||||
Command::Quit =>
|
||||
socket.close(),
|
||||
Command::Reporting(_reporting) => {
|
||||
// handled by session
|
||||
Command::Reporting(reporting) => {
|
||||
let _ = writeln!(socket, "report={}", if reporting { "on" } else { "off" });
|
||||
}
|
||||
Command::Show(ShowCommand::Reporting) => {
|
||||
let _ = writeln!(socket, "{{ \"report\": {:?} }}", session.reporting());
|
||||
let _ = writeln!(socket, "report={}", if session.reporting() { "on" } else { "off" });
|
||||
}
|
||||
Command::Show(ShowCommand::Input) => {
|
||||
for channel in 0..CHANNELS {
|
||||
report_to(channel, &mut channels, &mut socket);
|
||||
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 {
|
||||
match channels.channel_state(channel).pid.summary(channel).to_json() {
|
||||
Ok(buf) => {
|
||||
send_line(&mut socket, &buf);
|
||||
}
|
||||
Err(e) =>
|
||||
error!("unable to serialize pid summary: {:?}", e),
|
||||
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 {
|
||||
match channels.pwm_summary(channel).to_json() {
|
||||
Ok(buf) => {
|
||||
send_line(&mut socket, &buf);
|
||||
}
|
||||
Err(e) =>
|
||||
error!("unable to serialize pwm summary: {:?}", e),
|
||||
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 {
|
||||
match channels.steinhart_hart_summary(channel).to_json() {
|
||||
Ok(buf) => {
|
||||
send_line(&mut socket, &buf);
|
||||
}
|
||||
Err(e) =>
|
||||
error!("unable to serialize steinhart-hart summary: {:?}", e),
|
||||
}
|
||||
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.postfilter_summary(channel).to_json() {
|
||||
Ok(buf) => {
|
||||
send_line(&mut socket, &buf);
|
||||
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
|
||||
);
|
||||
}
|
||||
Err(e) =>
|
||||
error!("unable to serialize postfilter summary: {:?}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
Command::PwmPid { channel } => {
|
||||
channels.channel_state(channel).pid_engaged = true;
|
||||
leds.g3.on();
|
||||
let _ = writeln!(socket, "channel {}: PID enabled to control PWM", channel
|
||||
);
|
||||
}
|
||||
Command::Pwm { channel, pin, value } => {
|
||||
match pin {
|
||||
PwmPin::ISet => {
|
||||
channels.channel_state(channel).pid_engaged = false;
|
||||
leds.g3.off();
|
||||
let current = ElectricCurrent::new::<ampere>(value);
|
||||
channels.set_i(channel, current);
|
||||
channels.power_up(channel);
|
||||
}
|
||||
PwmPin::MaxV => {
|
||||
let voltage = ElectricPotential::new::<volt>(value);
|
||||
channels.set_max_v(channel, voltage);
|
||||
}
|
||||
PwmPin::MaxIPos => {
|
||||
let current = ElectricCurrent::new::<ampere>(value);
|
||||
channels.set_max_i_pos(channel, current);
|
||||
}
|
||||
PwmPin::MaxINeg => {
|
||||
let current = ElectricCurrent::new::<ampere>(value);
|
||||
channels.set_max_i_neg(channel, current);
|
||||
}
|
||||
}
|
||||
Command::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::CenterPoint { channel, center } => {
|
||||
let (i_tec, _) = channels.get_i(channel);
|
||||
let state = channels.channel_state(channel);
|
||||
state.center = center;
|
||||
if !state.pid_engaged {
|
||||
channels.set_i(channel, i_tec);
|
||||
Command::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;
|
||||
@ -305,105 +311,80 @@ fn main() -> ! {
|
||||
Target =>
|
||||
pid.target = value,
|
||||
KP =>
|
||||
pid.parameters.kp = value as f32,
|
||||
pid.parameters.kp = value,
|
||||
KI =>
|
||||
pid.parameters.ki = value as f32,
|
||||
pid.parameters.ki = value,
|
||||
KD =>
|
||||
pid.parameters.kd = value as f32,
|
||||
pid.parameters.kd = value,
|
||||
OutputMin =>
|
||||
pid.parameters.output_min = value as f32,
|
||||
pid.parameters.output_min = value,
|
||||
OutputMax =>
|
||||
pid.parameters.output_max = value as f32,
|
||||
pid.parameters.output_max = value,
|
||||
IntegralMin =>
|
||||
pid.parameters.integral_min = value as f32,
|
||||
pid.parameters.integral_min = value,
|
||||
IntegralMax =>
|
||||
pid.parameters.integral_max = value as f32,
|
||||
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 = ThermodynamicTemperature::new::<degree_celsius>(value),
|
||||
T0 => sh.t0 = value,
|
||||
B => sh.b = value,
|
||||
R0 => sh.r0 = ElectricalResistance::new::<ohm>(value),
|
||||
R0 => sh.r0 = value,
|
||||
}
|
||||
let _ = writeln!(socket, "Steinhart-Hart equation parameter updated");
|
||||
}
|
||||
Command::PostFilter { channel, rate: None } => {
|
||||
channels.adc.set_postfilter(channel as u8, None).unwrap();
|
||||
}
|
||||
Command::PostFilter { channel, rate: Some(rate) } => {
|
||||
Command::PostFilter { channel, rate } => {
|
||||
let filter = ad7172::PostFilter::closest(rate);
|
||||
match filter {
|
||||
Some(filter) =>
|
||||
channels.adc.set_postfilter(channel as u8, Some(filter)).unwrap(),
|
||||
None =>
|
||||
error!("unable to choose postfilter for rate {:.3}", rate),
|
||||
}
|
||||
}
|
||||
Command::Load => {
|
||||
match Config::load(&mut eeprom) {
|
||||
Ok(config) => {
|
||||
config.apply(&mut channels);
|
||||
new_ipv4_address = Some(Ipv4Address::from_bytes(&config.ipv4_address));
|
||||
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");
|
||||
}
|
||||
Err(e) =>
|
||||
error!("unable to load eeprom config: {:?}", e),
|
||||
}
|
||||
}
|
||||
Command::Save => {
|
||||
let config = Config::new(&mut channels, ipv4_address);
|
||||
match config.save(&mut eeprom) {
|
||||
Ok(()) => {},
|
||||
Err(e) =>
|
||||
error!("unable to save eeprom config: {:?}", e),
|
||||
}
|
||||
}
|
||||
Command::Ipv4(address) => {
|
||||
new_ipv4_address = Some(Ipv4Address::from_bytes(&address));
|
||||
}
|
||||
Command::Reset => {
|
||||
for i in 0..CHANNELS {
|
||||
channels.power_down(i);
|
||||
}
|
||||
|
||||
SCB::sys_reset();
|
||||
}
|
||||
}
|
||||
Ok(SessionInput::Error(e)) => {
|
||||
error!("session input: {:?}", e);
|
||||
send_line(&mut socket, b"{ \"error\": \"invalid input\" }");
|
||||
Ok(SessionOutput::Error(e)) => {
|
||||
let _ = writeln!(socket, "Command error: {:?}", e);
|
||||
}
|
||||
Err(_) =>
|
||||
socket.close(),
|
||||
}
|
||||
} else if socket.can_send() {
|
||||
if let Some(channel) = session.is_report_pending() {
|
||||
if report_to(channel, &mut channels, &mut socket) {
|
||||
} 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Apply new IPv4 address
|
||||
new_ipv4_address.map(|new_ipv4_address| {
|
||||
server.set_ipv4_address(ipv4_address);
|
||||
ipv4_address = new_ipv4_address;
|
||||
});
|
||||
|
||||
// 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();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
27
src/net.rs
27
src/net.rs
@ -2,15 +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, IpCidr, Ipv4Address};
|
||||
use smoltcp::wire::{EthernetAddress, IpAddress, IpCidr};
|
||||
use smoltcp::iface::{NeighborCache, EthernetInterfaceBuilder, EthernetInterface};
|
||||
use stm32_eth::{Eth, RingEntry, PhyAddress, RxDescriptor, TxDescriptor};
|
||||
use crate::pins::EthernetPins;
|
||||
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)
|
||||
@ -25,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,
|
||||
local_addr: Ipv4Address,
|
||||
f: F
|
||||
ethernet_addr: EthernetAddress, f: F
|
||||
) where
|
||||
F: FnOnce(EthernetInterface<&mut stm32_eth::Eth<'static, 'static>>),
|
||||
{
|
||||
@ -43,17 +38,13 @@ 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
|
||||
// Netmask 0 means we expect any IP address on the local segment.
|
||||
// No routing.
|
||||
let mut ip_addrs = [IpCidr::new(local_addr.into(), 0)];
|
||||
let 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 iface = EthernetInterfaceBuilder::new(&mut eth_dev)
|
||||
|
106
src/pid.rs
106
src/pid.rs
@ -1,26 +1,24 @@
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct Parameters {
|
||||
pub kp: f32,
|
||||
pub ki: f32,
|
||||
pub kd: f32,
|
||||
pub output_min: f32,
|
||||
pub output_max: f32,
|
||||
pub integral_min: f32,
|
||||
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: 1.5,
|
||||
ki: 0.1,
|
||||
kd: 150.0,
|
||||
kp: 0.5,
|
||||
ki: 0.05,
|
||||
kd: 0.45,
|
||||
output_min: 0.0,
|
||||
output_max: 2.0,
|
||||
integral_min: -10.0,
|
||||
integral_max: 10.0,
|
||||
output_max: 5.0,
|
||||
integral_min: 0.0,
|
||||
integral_max: 1.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -46,64 +44,40 @@ impl Controller {
|
||||
}
|
||||
|
||||
pub fn update(&mut self, input: f64) -> f64 {
|
||||
// error
|
||||
let error = input - self.target;
|
||||
let error = self.target - input;
|
||||
|
||||
// partial
|
||||
let p = f64::from(self.parameters.kp) * error;
|
||||
let p = self.parameters.kp * error;
|
||||
|
||||
// integral
|
||||
self.integral += f64::from(self.parameters.ki) * error;
|
||||
if self.integral < self.parameters.integral_min.into() {
|
||||
self.integral = self.parameters.integral_min.into();
|
||||
self.integral += error;
|
||||
if self.integral < self.parameters.integral_min {
|
||||
self.integral = self.parameters.integral_min;
|
||||
}
|
||||
if self.integral > self.parameters.integral_max.into() {
|
||||
self.integral = self.parameters.integral_max.into();
|
||||
if self.integral > self.parameters.integral_max {
|
||||
self.integral = self.parameters.integral_max;
|
||||
}
|
||||
let i = self.integral;
|
||||
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) * (input - last_input),
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type JsonBuffer = heapless::Vec<u8, heapless::consts::U360>;
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct Summary {
|
||||
channel: usize,
|
||||
parameters: Parameters,
|
||||
target: f64,
|
||||
integral: f64,
|
||||
}
|
||||
|
||||
impl Summary {
|
||||
pub fn to_json(&self) -> Result<JsonBuffer, serde_json_core::ser::Error> {
|
||||
serde_json_core::to_vec(self)
|
||||
#[allow(dead_code)]
|
||||
pub fn reset(&mut self) {
|
||||
self.integral = 0.0;
|
||||
self.last_input = None;
|
||||
}
|
||||
}
|
||||
|
||||
@ -124,12 +98,12 @@ mod test {
|
||||
#[test]
|
||||
fn test_controller() {
|
||||
const DEFAULT: f64 = 0.0;
|
||||
const TARGET: f64 = -1234.56;
|
||||
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;
|
||||
@ -139,19 +113,11 @@ mod test {
|
||||
let next_t = (t + 1) % DELAY;
|
||||
// Feed the oldest temperature
|
||||
let output = pid.update(values[next_t]);
|
||||
// Overwrite oldest with previous temperature - output
|
||||
values[next_t] = values[t] - output;
|
||||
// Overwrite oldest with previous temperature + output
|
||||
values[next_t] = values[t] + output;
|
||||
t = next_t;
|
||||
total_t += 1;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn summary_to_json() {
|
||||
let mut pid = Controller::new(PARAMETERS.clone());
|
||||
pid.target = 30.0 / 1.1;
|
||||
let buf = pid.summary(0).to_json().unwrap();
|
||||
assert_eq!(buf[0], b'{');
|
||||
assert_eq!(buf[buf.len() - 1], b'}');
|
||||
dbg!(values[t], total_t);
|
||||
}
|
||||
}
|
||||
|
194
src/pins.rs
194
src/pins.rs
@ -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,
|
||||
|
@ -3,7 +3,6 @@ use smoltcp::{
|
||||
iface::EthernetInterface,
|
||||
socket::{SocketSet, SocketHandle, TcpSocket, TcpSocketBuffer, SocketRef},
|
||||
time::Instant,
|
||||
wire::{IpCidr, Ipv4Address, Ipv4Cidr},
|
||||
};
|
||||
|
||||
|
||||
@ -84,21 +83,4 @@ impl<'a, 'b, S: Default> Server<'a, 'b, S> {
|
||||
callback(socket, &mut state.state);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_ipv4_address(&mut self, ipv4_address: Ipv4Address) {
|
||||
self.net.update_ip_addrs(|addrs| {
|
||||
for addr in addrs.iter_mut() {
|
||||
match addr {
|
||||
IpCidr::Ipv4(_) => {
|
||||
*addr = IpCidr::Ipv4(Ipv4Cidr::new(ipv4_address, 0));
|
||||
// done
|
||||
break
|
||||
}
|
||||
_ => {
|
||||
// skip
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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
147
src/softspi.rs
Normal 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 {}
|
||||
}
|
@ -1,46 +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::Serialize;
|
||||
|
||||
type JsonBuffer = heapless::Vec<u8, heapless::consts::U200>;
|
||||
|
||||
/// Steinhart-Hart equation parameters
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[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)
|
||||
}
|
||||
|
||||
pub fn to_json(&self) -> Result<JsonBuffer, serde_json_core::ser::Error> {
|
||||
serde_json_core::to_vec(self)
|
||||
///
|
||||
/// 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
65
src/units.rs
Normal 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)
|
||||
}
|
||||
}
|
103
src/usb.rs
103
src/usb.rs
@ -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(())
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user