forked from M-Labs/thermostat
Compare commits
261 Commits
Author | SHA1 | Date | |
---|---|---|---|
8db4867ebf | |||
130bde480e | |||
36d80ebdff | |||
09300b5d44 | |||
9743dca775 | |||
11131deda2 | |||
764774fbce | |||
4beeec6021 | |||
6b8a5f5bb8 | |||
8dd58b364d | |||
ae0d593139 | |||
adc25c9b2a | |||
9af86be674 | |||
eabc7f6a12 | |||
52e35d2a98 | |||
f1da910c11 | |||
9848c65de5 | |||
069d791802 | |||
bd9ae997ae | |||
45eb55d36d | |||
eddf05cae7 | |||
f68ae12c8d | |||
101a68fcfc | |||
9d1adbc7f7 | |||
47fa9f757e | |||
d1df6b8e3a | |||
0ffd784e91 | |||
d8ec083dbc | |||
1962135e79 | |||
fcb5cf1d4e | |||
d517087e10 | |||
798b400aa5 | |||
93dc39e943 | |||
5c3b759d0c | |||
6224486662 | |||
32bd49b258 | |||
ad54842c43 | |||
b336c4f993 | |||
680193b34b | |||
ae4bea0c8a | |||
1f2de942e4 | |||
1041d3ecbb | |||
c6040899dd | |||
9d89104f50 | |||
136c7a0b52 | |||
5000cae1b1 | |||
78ec77509f | |||
52aa3890c1 | |||
1ae6a6fdd4 | |||
7333d2cea5 | |||
44e9130010 | |||
5b0c6f7018 | |||
1007982b48 | |||
925601f4f5 | |||
8c1cb3117c | |||
1fcfe41a63 | |||
9fce19a418 | |||
00d5feaa8d | |||
09be55e12a | |||
76547be90a | |||
8b975e656e | |||
ae3d8b51d4 | |||
17edae44fb | |||
03b4561142 | |||
631a10938d | |||
6cd6a6a2c2 | |||
b93e2fbb7b | |||
76b95f66e0 | |||
8008870bc1 | |||
7646ff9037 | |||
6f81a63d12 | |||
78012f6fdd | |||
bb4f43fe1c | |||
9df0fe406f | |||
5ba74c6d9b | |||
6f0acc73b8 | |||
f29e86310d | |||
b04a61c414 | |||
cd680dd6cd | |||
e3e3237d2f | |||
570c0324b3 | |||
5688b2f1bb | |||
1b2f2f3888 | |||
e6f63ec940 | |||
67446ae99e | |||
26ad2f0119 | |||
|
69dabf5aa1 | ||
a26cdfabb1 | |||
4d43709f76 | |||
1c00e732fa | |||
09082b24a5 | |||
85e8273d51 | |||
e81c6d1692 | |||
1f644fd62c | |||
4f1d865d2b | |||
e6a5c31db6 | |||
a5d8661b10 | |||
7cb0ed70be | |||
6b9d61737e | |||
16844a1dc1 | |||
96f52ace8b | |||
a1a8efd51a | |||
8eb3cc4307 | |||
f3661ac8e3 | |||
c4e3be1d05 | |||
cf3ace4d2d | |||
f6802635a4 | |||
9e4d06fdbc | |||
3433881d0f | |||
193d54a0a6 | |||
3067b356c5 | |||
3ba2cc9ddc | |||
1539b624bd | |||
5c84b7438b | |||
cc0126636c | |||
45b7c4e669 | |||
73dd6d9154 | |||
e94601f54f | |||
8c9e12587f | |||
7c013ff4a4 | |||
50a1b9f52d | |||
9852b32646 | |||
22b0c9fcad | |||
e13ed37271 | |||
5987d9c881 | |||
7c55e34145 | |||
b176fc2788 | |||
b717ac5495 | |||
980d27ebfc | |||
e9e46b29cf | |||
b7e6cdbec2 | |||
93ea46d512 | |||
dc41473493 | |||
7a28cb1cd4 | |||
c3dd03dcf3 | |||
b2f455b2cf | |||
2e7be3fe01 | |||
ff91dd7baa | |||
ecc00a6aeb | |||
97813f917d | |||
880a887c40 | |||
383ebcd8e4 | |||
088bd6eb76 | |||
35d1e2e205 | |||
1090d0f5b5 | |||
23d0c470e5 | |||
5c8bb47e11 | |||
b92a5f18cd | |||
c125e20bdb | |||
7af5d8582d | |||
fca2de665c | |||
ffcc3f661b | |||
2a6f8ed874 | |||
5e8bf0e765 | |||
5ddd4d250e | |||
af0d78237f | |||
ae77a5c163 | |||
ffb70bde0a | |||
d517dd75fe | |||
a943308203 | |||
150d6c2f87 | |||
c005784df5 | |||
d574ccb5f4 | |||
6ba1459a3c | |||
aee279b579 | |||
1939490410 | |||
2e9d22cf47 | |||
a332b5fcdc | |||
83a266852a | |||
f12214a4df | |||
175b88d0e6 | |||
9e23b14ace | |||
6fd5328042 | |||
59103cb2a1 | |||
5acebbef9f | |||
12e713dc19 | |||
1b4a030e7e | |||
438da74721 | |||
026dd1ed9c | |||
d4e7036fab | |||
bfdb64ffd6 | |||
6e0cf26d6a | |||
d4901cbab1 | |||
62d89a68a1 | |||
5521563c91 | |||
11f2ebe961 | |||
4b75c6147d | |||
445cde6ae8 | |||
6951489545 | |||
97490e5e1b | |||
87287e83b3 | |||
407c0998af | |||
9e5a58cafd | |||
aea306cf17 | |||
97a09e422b | |||
61d2cd6ecf | |||
f3ec96f425 | |||
9f70ef2e0a | |||
83589610b5 | |||
a2caac0fe5 | |||
20059aff5c | |||
f690599f9e | |||
b3e9a1b636 | |||
bfbf037006 | |||
a1ad9b2456 | |||
8d70c03520 | |||
3b050347d4 | |||
254c1c3d73 | |||
5a293a0ada | |||
daa398cb5e | |||
17e89b2041 | |||
58e648b5e0 | |||
c5c0ce5625 | |||
d3606d25b6 | |||
27278db1ba | |||
d40a038c2f | |||
5d0d75d395 | |||
93f14523d7 | |||
201701ee8b | |||
a84242fb1f | |||
b394cfa3d4 | |||
b9902929a9 | |||
edc675f5af | |||
a4dde1b8ca | |||
7361619a53 | |||
34543c8660 | |||
83a209397e | |||
ba84295ec5 | |||
fb81380955 | |||
94e0525002 | |||
1157b73f7f | |||
dd06ae1075 | |||
f76ee9a607 | |||
fc0ca8b581 | |||
8c80062da8 | |||
7d45d5ad32 | |||
dda1f2f0b4 | |||
b1b6d1ea94 | |||
1849e6f5e7 | |||
b80fcc430b | |||
bb26490153 | |||
4a1ce342a0 | |||
8cddbc5173 | |||
37a7898f92 | |||
277f239ed7 | |||
10208e1ac0 | |||
42587810cd | |||
c11b71cc0d | |||
2617895460 | |||
5244077144 | |||
4e6aa5fe0c | |||
9a912392be | |||
50dcee0c8a | |||
f76ae453a9 | |||
e5c9ee8ed0 | |||
a3df2bc685 | |||
1711feae84 | |||
f9b55508dd | |||
cf03613ac5 | |||
3c94342448 | |||
5418488a2f |
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,2 +1,5 @@
|
||||
target/
|
||||
result
|
||||
*.bin
|
||||
|
||||
__pycache__/
|
||||
|
421
Cargo.lock
generated
421
Cargo.lock
generated
@ -1,30 +1,33 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "aligned"
|
||||
version = "0.3.2"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb1ce8b3382016136ab1d31a1b5ce807144f8b7eb2d5f16b2108f0f07edceb94"
|
||||
checksum = "c19796bd8d477f1a9d4ac2465b464a8b1359474f06a96bb3cda650b4fca309bf"
|
||||
dependencies = [
|
||||
"as-slice",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "as-slice"
|
||||
version = "0.1.3"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37dfb65bc03b2bc85ee827004f14a6817e04160e3b1a28931986a666a9290e70"
|
||||
checksum = "bb4d1c23475b74e3672afa8c2be22040b8b7783ad9b461021144ed10a46bb0e6"
|
||||
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.0"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d"
|
||||
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
|
||||
|
||||
[[package]]
|
||||
name = "bare-metal"
|
||||
@ -36,10 +39,22 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bit_field"
|
||||
version = "0.10.0"
|
||||
name = "bare-metal"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a165d606cf084741d4ac3a28fb6e9b1eb0bd31f6cd999098cfddb0b2ab381dc0"
|
||||
checksum = "f8fe8f5a8a398345e52358e18ff07cc17a568fbca5c6f73873d3a62056309603"
|
||||
|
||||
[[package]]
|
||||
name = "bit_field"
|
||||
version = "0.10.1"
|
||||
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"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
@ -55,12 +70,9 @@ checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de"
|
||||
|
||||
[[package]]
|
||||
name = "cast"
|
||||
version = "0.2.3"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b9434b9a5aa1450faa3f9cb14ea0e8c53bb5d2b3c1bfd1ab4fc03e9f33fbfb0"
|
||||
dependencies = [
|
||||
"rustc_version",
|
||||
]
|
||||
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
@ -69,37 +81,70 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
|
||||
|
||||
[[package]]
|
||||
name = "cortex-m"
|
||||
version = "0.6.2"
|
||||
name = "chrono"
|
||||
version = "0.4.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2954942fbbdd49996704e6f048ce57567c3e1a4e2dc59b41ae9fde06a01fc763"
|
||||
checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
|
||||
dependencies = [
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cortex-m"
|
||||
version = "0.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9075300b07c6a56263b9b582c214d0ff037b00d45ec9fde1cc711490c56f1bb9"
|
||||
dependencies = [
|
||||
"aligned",
|
||||
"bare-metal",
|
||||
"bare-metal 0.2.5",
|
||||
"bitfield",
|
||||
"cortex-m 0.7.4",
|
||||
"volatile-register",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cortex-m"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37ff967e867ca14eba0c34ac25cd71ea98c678e741e3915d923999bb2fe7c826"
|
||||
dependencies = [
|
||||
"bare-metal 0.2.5",
|
||||
"bitfield",
|
||||
"embedded-hal",
|
||||
"volatile-register",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cortex-m-log"
|
||||
version = "0.6.1"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "978caafe65d1023d38b00c76b83564788fc351d954a5005fb72cf992c0d61458"
|
||||
checksum = "1d63959cb1e003dd97233fee6762351540253237eadf06fcdcb98cbfa3f9be4a"
|
||||
dependencies = [
|
||||
"cortex-m",
|
||||
"cortex-m 0.6.7",
|
||||
"cortex-m-semihosting",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cortex-m-rt"
|
||||
version = "0.6.12"
|
||||
version = "0.6.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00d518da72bba39496024b62607c1d8e37bcece44b2536664f1132a73a499a28"
|
||||
checksum = "980c9d0233a909f355ed297ef122f257942de5e0a2cb1c39f60684b65bcb90fb"
|
||||
dependencies = [
|
||||
"cortex-m-rt-macros",
|
||||
"cortex-m-rt-macros 0.1.8",
|
||||
"r0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cortex-m-rt"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c433da385b720d5bb9f52362fa2782420798e68d40d67bfe4b0d992aba5dfe7"
|
||||
dependencies = [
|
||||
"cortex-m-rt-macros 0.7.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cortex-m-rt-macros"
|
||||
version = "0.1.8"
|
||||
@ -112,21 +157,50 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cortex-m-semihosting"
|
||||
version = "0.3.5"
|
||||
name = "cortex-m-rt-macros"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "113ef0ecffee2b62b58f9380f4469099b30e9f9cbee2804771b4203ba1762cfa"
|
||||
checksum = "f0f6f3e36f203cfedbc78b357fb28730aa2c6dc1ab060ee5c2405e843988d3c7"
|
||||
dependencies = [
|
||||
"cortex-m",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cortex-m-semihosting"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6bffa6c1454368a6aa4811ae60964c38e6996d397ff8095a8b9211b1c1f749bc"
|
||||
dependencies = [
|
||||
"cortex-m 0.7.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
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.3"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee4908a155094da7723c2d60d617b820061e3b4efcc3d9e293d206a5a76c170b"
|
||||
checksum = "e36cfb62ff156596c892272f3015ef952fe1525e85261fa3a7f327bd6b384ab9"
|
||||
dependencies = [
|
||||
"nb",
|
||||
"nb 0.1.3",
|
||||
"void",
|
||||
]
|
||||
|
||||
@ -149,10 +223,36 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hash2hwaddr"
|
||||
version = "0.0.1"
|
||||
name = "generic-array"
|
||||
version = "0.14.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "857afb5ee9e767c3a73b2ad7212b6deea0c3761a27db1e20ea0ed57ee352cfef"
|
||||
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",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
@ -162,81 +262,117 @@ checksum = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.8"
|
||||
version = "0.4.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7"
|
||||
checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "managed"
|
||||
version = "0.7.1"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fdcec5e97041c7f0f1c5b7d93f12e57293c831c646f4cc7a5db59460c7ea8de6"
|
||||
checksum = "c75de51135344a4f8ed3cfe2720dc27736f7711989703a0b43aadf3753c55577"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.3.3"
|
||||
version = "2.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400"
|
||||
checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525"
|
||||
|
||||
[[package]]
|
||||
name = "nb"
|
||||
version = "0.1.2"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1411551beb3c11dedfb0a90a0fa256b47d28b9ec2cdff34c25a2fa59e45dbdc"
|
||||
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"
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "5.1.1"
|
||||
version = "5.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b471253da97532da4b61552249c521e01e736071f71c1a4f7ebbfbf0a06aad6"
|
||||
checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.11"
|
||||
name = "num-integer"
|
||||
version = "0.1.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c62be47e61d1842b9170f0fdeec8eba98e60e90e5446449a0545e5152acd7096"
|
||||
checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"libm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "panic-abort"
|
||||
version = "0.3.2"
|
||||
name = "panic-halt"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e20e6499bbbc412f280b04a42346b356c6fa0753d5fd22b7bd752ff34c778ee"
|
||||
checksum = "de96540e0ebde571dc55c73d60ef407c653844e6f9a1e2fdbd40c07b9252d812"
|
||||
|
||||
[[package]]
|
||||
name = "panic-semihosting"
|
||||
version = "0.5.3"
|
||||
version = "0.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c03864ac862876c16a308f5286f4aa217f1a69ac45df87ad3cd2847f818a642c"
|
||||
checksum = "c3d55dedd501dfd02514646e0af4d7016ce36bc12ae177ef52056989966a1eec"
|
||||
dependencies = [
|
||||
"cortex-m",
|
||||
"cortex-m 0.7.4",
|
||||
"cortex-m-semihosting",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.9"
|
||||
name = "postcard"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c09721c6781493a2a492a96b5a5bf19b65917fe6728884e7c44dd0c60ca3435"
|
||||
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"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71"
|
||||
dependencies = [
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.3"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2bdc6c187c65bca4260c9011c9e3132efe4909da44726bad24cf7572ae338d7f"
|
||||
checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
@ -249,9 +385,18 @@ checksum = "e2a38df5b15c8d5c7e8654189744d8e396bddc18ad48041a500ce52d6948941f"
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.5.1"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
|
||||
checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
|
||||
|
||||
[[package]]
|
||||
name = "rtcc"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef35f9dcbf434a34dcc99b3ebba1c1945d49c70832958e932e83dc63a5273994"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
@ -278,10 +423,51 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
|
||||
|
||||
[[package]]
|
||||
name = "smoltcp"
|
||||
version = "0.6.0"
|
||||
name = "serde"
|
||||
version = "1.0.118"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fe46639fd2ec79eadf8fe719f237a7a0bd4dac5d957f1ca5bbdbc1c3c39e53a"
|
||||
checksum = "06c64263859d87aa2eb554587e2d23183398d617427327cf2b3d0ed8c69e4800"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde-json-core"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cbf406405ada9ef326ca78677324ac66994ff348fc48a16030be08caeed29825"
|
||||
dependencies = [
|
||||
"heapless",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.118"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c84d3526699cd55261af4b941e4e725444df67aa4f9e6a3564f18030d12672df"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sfkv"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25f5bfac3f66a7c10a6f37ee81aeaa471f4d35dc21665b59ad7c555adcb9e8aa"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"postcard",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smoltcp"
|
||||
version = "0.7.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e4a069bef843d170df47e7c0a8bf8d037f217d9f5b325865acc3e466ffe40d3"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"byteorder",
|
||||
@ -291,17 +477,17 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.1.1"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dba1a27d3efae4351c8051072d619e3ade2820635c3958d826bfea39d59b54c8"
|
||||
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
||||
|
||||
[[package]]
|
||||
name = "stm32-eth"
|
||||
version = "0.1.2"
|
||||
source = "git+https://github.com/stm32-rs/stm32-eth.git#2c5dce379b85a31fb0b9c58a028b6454be1727aa"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/stm32-rs/stm32-eth.git?rev=3759c5c9#3759c5c99c0ab69bb71759030766bc0fba0b6cde"
|
||||
dependencies = [
|
||||
"aligned",
|
||||
"log",
|
||||
"cortex-m 0.7.4",
|
||||
"smoltcp",
|
||||
"stm32f4xx-hal",
|
||||
"volatile-register",
|
||||
@ -309,75 +495,126 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "stm32f4"
|
||||
version = "0.10.0"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44a3d6c58b14e63926273694e7dd644894513c5e35ce6928c4657ddb62cae976"
|
||||
checksum = "da3d56009c8f32e4f208dbea17df72484154d1040a8969b75d8c73eb7b18fe8f"
|
||||
dependencies = [
|
||||
"bare-metal",
|
||||
"cortex-m",
|
||||
"cortex-m-rt",
|
||||
"bare-metal 0.2.5",
|
||||
"cortex-m 0.7.4",
|
||||
"cortex-m-rt 0.6.13",
|
||||
"vcell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stm32f4xx-hal"
|
||||
version = "0.7.0"
|
||||
source = "git+https://github.com/thalesfragoso/stm32f4xx-hal?branch=pwm-impl#cfd073e094daa9be9dd2b0a1f859a4e1c6be2b77"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a06fde2dd27c0ba934c9e69b62af66eb1c20dbb6d741b187a763912e9892d13"
|
||||
dependencies = [
|
||||
"bare-metal",
|
||||
"bare-metal 1.0.0",
|
||||
"cast",
|
||||
"cortex-m",
|
||||
"cortex-m-rt",
|
||||
"cortex-m 0.7.4",
|
||||
"cortex-m-rt 0.7.1",
|
||||
"embedded-dma",
|
||||
"embedded-hal",
|
||||
"nb",
|
||||
"nb 1.0.0",
|
||||
"rand_core",
|
||||
"rtcc",
|
||||
"stm32f4",
|
||||
"synopsys-usb-otg",
|
||||
"void",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.17"
|
||||
version = "1.0.54"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0df0eb663f387145cab623dea85b09c2c5b4b0aef44e945d928e682fce71bb03"
|
||||
checksum = "9a2af957a63d6bd42255c359c93d9bfdb97076bd3b820897ce55ffbfbf107f44"
|
||||
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 0.6.7",
|
||||
"usb-device",
|
||||
"vcell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thermostat"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"bare-metal",
|
||||
"bare-metal 1.0.0",
|
||||
"bit_field",
|
||||
"byteorder",
|
||||
"cortex-m",
|
||||
"cortex-m 0.6.7",
|
||||
"cortex-m-log",
|
||||
"cortex-m-rt",
|
||||
"hash2hwaddr",
|
||||
"cortex-m-rt 0.6.13",
|
||||
"eeprom24x",
|
||||
"heapless",
|
||||
"log",
|
||||
"nb 1.0.0",
|
||||
"nom",
|
||||
"num-traits",
|
||||
"panic-abort",
|
||||
"panic-halt",
|
||||
"panic-semihosting",
|
||||
"serde",
|
||||
"serde-json-core",
|
||||
"sfkv",
|
||||
"smoltcp",
|
||||
"stm32-eth",
|
||||
"stm32f4xx-hal",
|
||||
"uom",
|
||||
"usb-device",
|
||||
"usbd-serial",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.11.2"
|
||||
version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d2783fe2d6b8c1101136184eb41be8b1ad379e4657050b8aaff0c79ee7575f9"
|
||||
checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.0"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c"
|
||||
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",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vcell"
|
||||
@ -387,9 +624,9 @@ checksum = "876e32dcadfe563a4289e994f7cb391197f362b6315dc45e8ba4aa6f564a4b3c"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.1"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "078775d0255232fb988e6fccf26ddc9d1ac274299aaedcedce21c6f72cc533ce"
|
||||
checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed"
|
||||
|
||||
[[package]]
|
||||
name = "void"
|
||||
|
30
Cargo.toml
30
Cargo.toml
@ -7,37 +7,39 @@ authors = ["Astro <astro@spaceboyz.net>"]
|
||||
version = "0.0.0"
|
||||
keywords = ["thermostat", "laser", "physics"]
|
||||
repository = "https://git.m-labs.hk/M-Labs/thermostat"
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
features = []
|
||||
default-target = "thumbv7em-none-eabihf"
|
||||
|
||||
[dependencies]
|
||||
panic-abort = "0.3.1"
|
||||
panic-semihosting = { version = "0.5.1", optional = true }
|
||||
panic-halt = "0.2"
|
||||
panic-semihosting = { version = "0.5", optional = true }
|
||||
log = "0.4"
|
||||
bare-metal = "0.2"
|
||||
bare-metal = "1"
|
||||
cortex-m = "0.6"
|
||||
cortex-m-rt = { version = "0.6", features = ["device"] }
|
||||
cortex-m-log = { version = "0.6", features = ["log-integration"] }
|
||||
stm32f4xx-hal = { version = "0.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 }
|
||||
stm32f4xx-hal = { version = "=0.10.1", features = ["rt", "stm32f427", "usb_fs"] }
|
||||
stm32-eth = { rev = "3759c5c9", features = ["stm32f427", "smoltcp-phy"], git = "https://github.com/stm32-rs/stm32-eth.git" }
|
||||
smoltcp = { version = "0.7.5", default-features = false, features = ["proto-ipv4", "socket-tcp", "log"] }
|
||||
bit_field = "0.10"
|
||||
byteorder = { version = "1", default-features = false }
|
||||
nom = { version = "5", default-features = false }
|
||||
num-traits = { version = "0.2", default-features = false, features = ["libm"] }
|
||||
|
||||
[patch.crates-io]
|
||||
# TODO: pending https://github.com/stm32-rs/stm32f4xx-hal/pull/125
|
||||
stm32f4xx-hal = { git = "https://github.com/thalesfragoso/stm32f4xx-hal", branch = "pwm-impl" }
|
||||
usb-device = "0.2"
|
||||
usbd-serial = "0.1"
|
||||
nb = "1"
|
||||
uom = { version = "0.30", default-features = false, features = ["autoconvert", "si", "f64", "use_serde"] }
|
||||
eeprom24x = "0.3"
|
||||
serde = { version = "1.0", default-features = false, features = ["derive"] }
|
||||
heapless = "0.5"
|
||||
serde-json-core = "0.1"
|
||||
sfkv = "0.1"
|
||||
|
||||
[features]
|
||||
semihosting = ["panic-semihosting", "cortex-m-log/semihosting"]
|
||||
generate-hwaddr = ["hash2hwaddr"]
|
||||
default = ["generate-hwaddr"]
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
308
README.md
308
README.md
@ -1,26 +1,71 @@
|
||||
# Firmware for the Sinara 8451 Thermostat
|
||||
|
||||
- [x] [Continuous Integration](https://nixbld.m-labs.hk/job/stm32/stm32/thermostat)
|
||||
- [x] [Download latest firmware build](https://nixbld.m-labs.hk/job/stm32/stm32/thermostat/latest/download-by-type/file/binary-dist)
|
||||
- [x] [Continuous Integration](https://nixbld.m-labs.hk/job/mcu/thermostat/thermostat)
|
||||
- [x] Download latest firmware build: [ELF](https://nixbld.m-labs.hk/job/mcu/thermostat/thermostat/latest/download/1) [BIN](https://nixbld.m-labs.hk/job/mcu/thermostat/thermostat/latest/download/2)
|
||||
|
||||
|
||||
## Building
|
||||
|
||||
### Debian-based systems (tested on Ubuntu 19.10)
|
||||
### Reproducible build with Nix
|
||||
|
||||
- install git, clone this repository
|
||||
- install [rustup](https://rustup.rs/)
|
||||
Thermostat firmware is packaged using the [Nix](https://nixos.org) Flakes system. Install Nix 2.4+ and enable flakes by adding ``experimental-features = nix-command flakes`` to ``nix.conf`` (e.g. ``~/.config/nix/nix.conf``).
|
||||
|
||||
Once you have Flakes enabled, you can use ``nix build`` to build the firmware.
|
||||
|
||||
### Development environment
|
||||
|
||||
Clone this repository and with Nix Flakes enabled, use the following commands:
|
||||
|
||||
```shell
|
||||
rustup toolchain install nightly
|
||||
rustup update
|
||||
rustup target add thumbv7em-none-eabihf --toolchain nightly
|
||||
rustup default nightly
|
||||
nix develop
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
The resulting ELF file will be located under `target/thumbv7em-none-eabihf/release/thermostat`
|
||||
The resulting ELF file will be located under `target/thumbv7em-none-eabihf/release/thermostat`.
|
||||
|
||||
Alternatively, you can install the Rust toolchain without Nix using rustup; see the `rust` variable in `flake.nix` to determine which Rust version to use.
|
||||
|
||||
## Debugging
|
||||
|
||||
Connect SWDIO/SWCLK/RST/GND to a programmer such as ST-Link v2.1. Run OpenOCD:
|
||||
```shell
|
||||
openocd -f interface/stlink.cfg -f target/stm32f4x.cfg
|
||||
```
|
||||
|
||||
You may need to power up the programmer before powering the device.
|
||||
Leave OpenOCD running. Run the GNU debugger:
|
||||
```shell
|
||||
gdb target/thumbv7em-none-eabihf/release/thermostat
|
||||
|
||||
(gdb) source openocd.gdb
|
||||
```
|
||||
|
||||
## Flashing
|
||||
There are several options for flashing Thermostat. DFU requires only a micro-USB connector, whereas OpenOCD needs a JTAG/SWD adapter.
|
||||
|
||||
### dfu-util on Linux
|
||||
* Install the DFU USB tool (dfu-util).
|
||||
* Convert firmware from ELF to BIN: `llvm-objcopy -O binary target/thumbv7em-none-eabihf/release/thermostat thermostat.bin` (you can skip this step if using the BIN from Hydra)
|
||||
* Connect to the Micro USB connector to Thermostat below the RJ45.
|
||||
* Add jumper to Thermostat v2.0 across 2-pin jumper adjacent to JTAG connector.
|
||||
* Cycle board power to put it in DFU update mode
|
||||
* Push firmware to flash: `dfu-util -a 0 -s 0x08000000:leave -D thermostat.bin`
|
||||
* Remove jumper
|
||||
* Cycle power to leave DFU update mode
|
||||
|
||||
### st.com DfuSe tool on Windows
|
||||
On a Windows machine install [st.com](https://st.com) DfuSe USB device firmware upgrade (DFU) software. [link](https://www.st.com/en/development-tools/stsw-stm32080.html).
|
||||
- add jumper to Thermostat v2.0 across 2-pin jumper adjacent to JTAG connector
|
||||
- cycle board power to put it in DFU update mode
|
||||
- connect micro-USB to PC
|
||||
- use st.com software to upload firmware
|
||||
- remove jumper
|
||||
- cycle power to leave DFU update mode
|
||||
|
||||
### OpenOCD
|
||||
```shell
|
||||
openocd -f interface/stlink.cfg -f target/stm32f4x.cfg -c "program target/thumbv7em-none-eabihf/release/thermostat verify reset;exit"
|
||||
```
|
||||
|
||||
## Network
|
||||
|
||||
@ -30,7 +75,7 @@ Ethernet, IP: 192.168.1.26/24
|
||||
|
||||
Use netcat to connect to port 23/tcp (telnet)
|
||||
```sh
|
||||
nc -vv 192.168.1.26 23
|
||||
rlwrap nc -vv 192.168.1.26 23
|
||||
```
|
||||
|
||||
telnet clients send binary data after connect. Enter \n once to
|
||||
@ -39,33 +84,220 @@ invalidate the first line of input.
|
||||
|
||||
### Reading ADC input
|
||||
|
||||
Set report mode to `on` for a continuous stream of input data.
|
||||
|
||||
The scope of this setting is per TCP session.
|
||||
ADC input data is provided in reports. Query for the latest report with the command `report`. See the *Reports* section below.
|
||||
|
||||
|
||||
### Commands
|
||||
### TCP commands
|
||||
|
||||
| Syntax | Function |
|
||||
| --- | --- |
|
||||
| `report` | Show current input |
|
||||
| `report mode` | Show current report mode |
|
||||
| `report mode <off/on>` | Set report mode |
|
||||
| `pwm` | Show current PWM settings |
|
||||
| `pwm <0/1> max_i_pos <ratio>` | Set PWM duty cycle for **max_i_pos** to *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 |
|
||||
Send commands as simple text string terminated by `\n`. Responses are
|
||||
formatted as line-delimited JSON.
|
||||
|
||||
| Syntax | Function |
|
||||
|-------------------------------------------|-------------------------------------------------------------------------------|
|
||||
| `report` | Show latest report of channel parameters (see *Reports* section) |
|
||||
| `output` | Show current output settings |
|
||||
| `output <0/1> max_i_pos <amp>` | Set maximum positive output current, clamped to [0, 2] |
|
||||
| `output <0/1> max_i_neg <amp>` | Set maximum negative output current, clamped to [0, 2] |
|
||||
| `output <0/1> max_v <volt>` | Set maximum output voltage, clamped to [0, 4] |
|
||||
| `output <0/1> i_set <amp>` | Disengage PID, set fixed output current, clamped to [-2, 2] |
|
||||
| `output <0/1> polarity <normal/reversed>` | Set output current polarity, with 'normal' being the front panel polarity |
|
||||
| `output <0/1> pid` | Let output current to be controlled by the PID |
|
||||
| `center <0/1> <volt>` | Set the MAX1968 0A-centerpoint to the specified fixed voltage |
|
||||
| `center <0/1> vref` | Set the MAX1968 0A-centerpoint to measure from VREF |
|
||||
| `pid` | Show PID configuration |
|
||||
| `pid <0/1> target <deg_celsius>` | Set the PID controller target temperature |
|
||||
| `pid <0/1> kp <value>` | Set proportional gain |
|
||||
| `pid <0/1> ki <value>` | Set integral gain |
|
||||
| `pid <0/1> kd <value>` | Set differential gain |
|
||||
| `pid <0/1> output_min <amp>` | Set mininum output |
|
||||
| `pid <0/1> output_max <amp>` | Set maximum output |
|
||||
| `b-p` | Show B-Parameter equation parameters |
|
||||
| `b-p <0/1> <t0/b/r0> <value>` | Set B-Parameter for a channel |
|
||||
| `postfilter` | Show postfilter settings |
|
||||
| `postfilter <0/1> off` | Disable postfilter |
|
||||
| `postfilter <0/1> rate <rate>` | Set postfilter output data rate |
|
||||
| `load [0/1]` | Restore configuration for channel all/0/1 from flash |
|
||||
| `save [0/1]` | Save configuration for channel all/0/1 to flash |
|
||||
| `reset` | Reset the device |
|
||||
| `dfu` | Reset device and enters USB device firmware update (DFU) mode |
|
||||
| `ipv4 <X.X.X.X/L> [Y.Y.Y.Y]` | Configure IPv4 address, netmask length, and optional default gateway |
|
||||
| `fan` | Show current fan settings and sensors' measurements |
|
||||
| `fan <value>` | Set fan power with values from 1 to 100 |
|
||||
| `fan auto` | Enable automatic fan speed control |
|
||||
| `fcurve <a> <b> <c>` | Set fan controller curve coefficients (see *Fan control* section) |
|
||||
| `fcurve default` | Set fan controller curve coefficients to defaults (see *Fan control* section) |
|
||||
| `hwrev` | Show hardware revision, and settings related to it |
|
||||
|
||||
|
||||
## USB
|
||||
|
||||
The firmware includes experimental support for acting as a USB-Serial
|
||||
peripheral. Debug logging will be sent there by default (unless build
|
||||
with logging via semihosting.)
|
||||
|
||||
**Caveat:** This logging does not flush its output. Doing so would
|
||||
hang indefinitely if the output is not read by the USB host. Therefore
|
||||
output will be truncated when USB buffers are full.
|
||||
|
||||
|
||||
## Temperature measurement
|
||||
|
||||
Connect the thermistor with the SENS pins of the
|
||||
device. Temperature-depending resistance is measured by the AD7172
|
||||
ADC. To prepare conversion to a temperature, set the parameters
|
||||
for the B-Parameter equation.
|
||||
|
||||
Set the base temperature in degrees celsius for the channel 0 thermistor:
|
||||
```
|
||||
b-p 0 t0 20
|
||||
```
|
||||
|
||||
Set the resistance in Ohms measured at the base temperature t0:
|
||||
```
|
||||
b-p 0 r0 10000
|
||||
```
|
||||
|
||||
Set the Beta parameter:
|
||||
```
|
||||
b-p 0 b 3800
|
||||
```
|
||||
|
||||
### 50/60 Hz filtering
|
||||
|
||||
The AD7172-2 ADC on the SENS inputs supports simultaneous rejection of
|
||||
50 Hz ± 1 Hz and 60 Hz ± 1 Hz (dB). Affecting sampling rate, the
|
||||
postfilter rate can be tuned with the `postfilter` command.
|
||||
|
||||
| Postfilter rate | Rejection | Effective sampling rate |
|
||||
| --- | :---: | --- |
|
||||
| 16.67 Hz | 92 dB | 8.4 Hz |
|
||||
| 20 Hz | 86 dB | 9.1 Hz |
|
||||
| 21.25 Hz | 62 dB | 10 Hz |
|
||||
| 27 Hz | 47 dB | 10.41 Hz |
|
||||
|
||||
## Thermo-Electric Cooling (TEC)
|
||||
|
||||
- Connect TEC module device 0 to TEC0- and TEC0+.
|
||||
- Connect TEC module device 1 to TEC1- and TEC1+.
|
||||
- The GND pin is for shielding not for sinking TEC module currents.
|
||||
|
||||
When using a TEC module with the Thermostat, the Thermostat expects the thermal load (where the thermistor is located) to cool down with a positive software current set point, and heat up with a negative current set point.
|
||||
|
||||
If the Thermostat is used for temperature control with the Sinara 5432 DAC "Zotino", and is connected via an IDC cable, the TEC polarity may need to be reversed with the `output <ch> polarity reversed` TCP command.
|
||||
|
||||
Testing heat flow direction with a low set current is recommended before installation of the TEC module.
|
||||
|
||||
### Limits
|
||||
|
||||
Each channel has maximum value settings, for setting
|
||||
output limits.
|
||||
|
||||
Use the `output` command to see them.
|
||||
|
||||
| Limit | Unit | Description |
|
||||
| --- | :---: | --- |
|
||||
| `max_v` | Volts | Maximum voltage |
|
||||
| `max_i_pos` | Amperes | Maximum positive current |
|
||||
| `max_i_neg` | Amperes | Maximum negative current |
|
||||
|
||||
Example: set the maximum voltage of channel 0 to 1.5 V.
|
||||
```
|
||||
output 0 max_v 1.5
|
||||
```
|
||||
|
||||
Example: set the maximum negative current of channel 0 to -2 A.
|
||||
```
|
||||
output 0 max_i_neg 2
|
||||
```
|
||||
|
||||
Example: set the maximum positive current of channel 1 to 2 A.
|
||||
```
|
||||
output 1 max_i_pos 2
|
||||
```
|
||||
|
||||
### Open-loop mode
|
||||
|
||||
To manually control TEC output current, set a fixed output current with
|
||||
the `output` command. Doing so will disengage the PID control for that
|
||||
channel.
|
||||
|
||||
Example: set output current of channel 0 to 0 A.
|
||||
```
|
||||
output 0 i_set 0
|
||||
```
|
||||
|
||||
## PID-stabilized temperature control
|
||||
|
||||
Set the target temperature of channel 0 to 20 degrees celsius:
|
||||
```
|
||||
pid 0 target 20
|
||||
```
|
||||
|
||||
Enter closed-loop mode by switching control of the TEC output current
|
||||
of channel 0 to the PID algorithm:
|
||||
```
|
||||
output 0 pid
|
||||
```
|
||||
|
||||
### PID output clamping
|
||||
|
||||
It is possible to clamp the PID algorithm output independently of channel output limits. This is desirable when e.g. there is a need to keep the current value above a certain threshold in closed-loop mode.
|
||||
|
||||
Note that the actual output will still ultimately be limited by the `max_i_pos` and `max_i_neg` values.
|
||||
|
||||
Set PID maximum output of channel 0 to 1.5 A.
|
||||
```
|
||||
pid 0 output_max 1.5
|
||||
```
|
||||
|
||||
Set PID minimum output of channel 0 to 0.1 A.
|
||||
```
|
||||
pid 0 output_min 0.1
|
||||
```
|
||||
|
||||
## 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. Reports are JSON objects
|
||||
with the following keys.
|
||||
|
||||
| Key | Unit | Description |
|
||||
| --- | :---: | --- |
|
||||
| `channel` | Integer | Channel `0`, or `1` |
|
||||
| `time` | Seconds | Temperature measurement time |
|
||||
| `interval` | Seconds | Time elapsed since last report update on channel |
|
||||
| `adc` | Volts | AD7172 input |
|
||||
| `sens` | Ohms | Thermistor resistance derived from `adc` |
|
||||
| `temperature` | Degrees Celsius | B-Parameter conversion result derived from `sens` |
|
||||
| `pid_engaged` | Boolean | `true` if in closed-loop mode |
|
||||
| `i_set` | Amperes | TEC output current |
|
||||
| `dac_value` | Volts | AD5680 output derived from `i_set` |
|
||||
| `dac_feedback` | Volts | ADC measurement of the AD5680 output |
|
||||
| `i_tec` | Volts | MAX1968 TEC current monitor |
|
||||
| `tec_i` | Amperes | TEC output current feedback derived from `i_tec` |
|
||||
| `tec_u_meas` | Volts | Measurement of the voltage across the TEC |
|
||||
| `pid_output` | Amperes | PID control output |
|
||||
|
||||
Note: Prior to Thermostat hardware revision v2.2.4, the voltage and current readouts `i_tec` and `tec_i` are noisy without the hardware fix shown in [this PR](https://git.m-labs.hk/M-Labs/thermostat/pulls/105).
|
||||
|
||||
## PID Tuning
|
||||
|
||||
The thermostat implements a PID control loop for each of the TEC channels, more details on setting up the PID control loop can be found [here](./doc/PID%20tuning.md).
|
||||
|
||||
## Fan control
|
||||
|
||||
Fan control commands are available for thermostat revisions with an integrated fan system:
|
||||
1. `fan` - show fan stats: `fan_pwm`, `abs_max_tec_i`, `auto_mode`, `k_a`, `k_b`, `k_c`.
|
||||
2. `fan auto` - enable auto speed controller mode, where fan speed is controlled by the fan curve `fcurve`.
|
||||
3. `fan <value>` - set the fan power with the value from `1` to `100` and disable auto mode. There is no way to completely disable the fan.
|
||||
Please note that power doesn't correlate with the actual speed linearly.
|
||||
4. `fcurve <a> <b> <c>` - set coefficients of the controlling curve `a*x^2 + b*x + c`, where `x` is `abs_max_tec_i/MAX_TEC_I`, a normalized value in range [0,1],
|
||||
i.e. the (linear) proportion of current output capacity used, on the channel with the largest current flow. The controlling curve is also clamped to [0,1].
|
||||
5. `fcurve default` - restore fan curve coefficients to defaults: `a = 1.0, b = 0.0, c = 0.0`.
|
||||
|
@ -1 +0,0 @@
|
||||
"0ma8dxsw90jrbxb3cd873k98g3pixnqvb059blvg7kf4m5aj9fnq"
|
81
doc/PID tuning.md
Normal file
81
doc/PID tuning.md
Normal file
@ -0,0 +1,81 @@
|
||||
# PID Tuning
|
||||
|
||||
## Note on hardware setup
|
||||
|
||||
The heat sinking side of the TEC module should be thermally bonded to a large heat-sinking thermal mass to ensure maximum temperature stability, a large optical table had provided good results in tests.
|
||||
|
||||
The thermal load under control should be well insulated from the surrounding for maximum stability, closed cell foam had been tested showing good results.
|
||||
|
||||
## Real time plot
|
||||
|
||||
When tuning Thermostat PID parameters, it is helpful to view the temperature, PID output and other data in the form of a real time graph.
|
||||
|
||||
To use the Python real-time plotting utility, run
|
||||
|
||||
```shell
|
||||
python pythermostat/pythermostat/plot.py
|
||||
```
|
||||
|
||||
![default view](./assets/default%20view.png)
|
||||
|
||||
## Temperature Setpoints and Thermal Load
|
||||
|
||||
A PID controller with the same set of PID parameters may not work identically across all temperatures, especially when comparing the performance of a TEC module cooling a load versus heating a load. This is due to self ohmic heating of the TEC module aiding efficiency when heating, but harming efficiency when cooling.
|
||||
|
||||
When a PID loop is expected to operate the TEC in both heating and cooling modes, it is important to verify the loop performance in both modes.
|
||||
|
||||
For systems expected to operate at a narrow range of temperatures, it is a good idea to tune the PID loop at the temperature region of interest.
|
||||
|
||||
The same is also true for controlling loads that are expected to produce heat, e.g. laser cooling blocks. Testing the loop performance across varying amount of thermal load is needed to ensure stability in operation.
|
||||
|
||||
## Manual Tuning
|
||||
|
||||
Below are some general guidelines for manually tuning PID loops. Note that every system is different, and some of the values mentioned below may not apply to all systems.
|
||||
|
||||
1. To start the manual tuning process, set the kp, ki and kd parameters to 0.
|
||||
|
||||
2. Begin by increasing kp until the temperature begins to oscillate. Offset between the target temperature and the actual temperature can be ignored for now.
|
||||
|
||||
3. Reduce kp by 30%, increase ki until the offset between target and actual temperature is eliminated.
|
||||
|
||||
4. Increase kd until the maximum allowable amount of overshoot is observed.
|
||||
|
||||
5. Some tweaking will be needed to obtain the desired result, especially when trying to balance between minimizing overshoot and maximizing response speed.
|
||||
|
||||
## Auto Tuning
|
||||
|
||||
A PID auto tuning utility is provided in the PyThermostat library. The auto tuning utility drives the the load to a controlled oscillation, observes the ultimate gain and oscillation period and calculates a set of PID parameters.
|
||||
|
||||
To run the auto tuning utility, run
|
||||
|
||||
```shell
|
||||
python pythermostat/pythermostat/autotune.py
|
||||
```
|
||||
|
||||
After some time, the auto tuning utility will output the auto tuning results, below is a sample output
|
||||
|
||||
```shell
|
||||
Ku: 0.7553203471147422
|
||||
Pu: 75.93899999999977
|
||||
rule: ziegler-nichols
|
||||
kp: 0.45319220826884526
|
||||
Ki: 0.011935690706194357
|
||||
Kd: 4.301870387965967
|
||||
rule: tyreus-luyben
|
||||
kp: 0.3432930977636503
|
||||
Ki: 0.0020549280832497956
|
||||
Kd: 4.137825730504864
|
||||
.
|
||||
.
|
||||
.
|
||||
```
|
||||
|
||||
At the end of the test, the ultimate gain `Ku`, oscillation period `Pu` and a few sets of recommended PID parameters are calculated and displayed.
|
||||
|
||||
Multiple suggested sets of PID parameters based on different calculation rules are displayed. While all sets are expected to work, the different sets trade off response time with overshoot differently, and testing is needed to see which set works best for the system on hand.
|
||||
|
||||
With a well designed and constructed setup, the PID parameters calculated by the auto tune utility together with some manual tweaking can yield sub-mK control stability.
|
||||
|
||||
Below shows data captured on an experiment setup, with 300uK stability over 12 hours.
|
||||
|
||||
![twelve_hours](./assets/twelve_hours.png)
|
BIN
doc/assets/default view.png
Normal file
BIN
doc/assets/default view.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 40 KiB |
BIN
doc/assets/twelve_hours.png
Normal file
BIN
doc/assets/twelve_hours.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 64 KiB |
48
flake.lock
generated
Normal file
48
flake.lock
generated
Normal file
@ -0,0 +1,48 @@
|
||||
{
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1722791413,
|
||||
"narHash": "sha256-rCTrlCWvHzMCNcKxPE3Z/mMK2gDZ+BvvpEVyRM4tKmU=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "8b5b6723aca5a51edf075936439d9cd3947b7b2c",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-24.05",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs",
|
||||
"rust-overlay": "rust-overlay"
|
||||
}
|
||||
},
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1719281921,
|
||||
"narHash": "sha256-LIBMfhM9pMOlEvBI757GOK5l0R58SRi6YpwfYMbf4yc=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "b6032d3a404d8a52ecfc8571ff0c26dfbe221d07",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
103
flake.nix
Normal file
103
flake.nix
Normal file
@ -0,0 +1,103 @@
|
||||
{
|
||||
description = "Firmware for the Sinara 8451 Thermostat";
|
||||
|
||||
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
|
||||
inputs.rust-overlay = {
|
||||
url = "github:oxalica/rust-overlay";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
outputs =
|
||||
{
|
||||
self,
|
||||
nixpkgs,
|
||||
rust-overlay,
|
||||
}:
|
||||
let
|
||||
pkgs = import nixpkgs {
|
||||
system = "x86_64-linux";
|
||||
overlays = [ (import rust-overlay) ];
|
||||
};
|
||||
|
||||
rust = pkgs.rust-bin.stable."1.66.0".default.override {
|
||||
extensions = [ "rust-src" ];
|
||||
targets = [ "thumbv7em-none-eabihf" ];
|
||||
};
|
||||
rustPlatform = pkgs.makeRustPlatform {
|
||||
rustc = rust;
|
||||
cargo = rust;
|
||||
};
|
||||
|
||||
thermostat = rustPlatform.buildRustPackage {
|
||||
name = "thermostat";
|
||||
version = "0.0.0";
|
||||
|
||||
src = self;
|
||||
cargoLock = {
|
||||
lockFile = ./Cargo.lock;
|
||||
outputHashes = {
|
||||
"stm32-eth-0.2.0" = "sha256-48RpZgagUqgVeKm7GXdk3Oo0v19ScF9Uby0nTFlve2o=";
|
||||
};
|
||||
};
|
||||
|
||||
nativeBuildInputs = [ pkgs.llvm ];
|
||||
|
||||
buildPhase = ''
|
||||
cargo build --release --bin thermostat
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
mkdir -p $out $out/nix-support
|
||||
cp target/thumbv7em-none-eabihf/release/thermostat $out/thermostat.elf
|
||||
echo file binary-dist $out/thermostat.elf >> $out/nix-support/hydra-build-products
|
||||
llvm-objcopy -O binary target/thumbv7em-none-eabihf/release/thermostat $out/thermostat.bin
|
||||
echo file binary-dist $out/thermostat.bin >> $out/nix-support/hydra-build-products
|
||||
'';
|
||||
|
||||
dontFixup = true;
|
||||
auditable = false;
|
||||
};
|
||||
|
||||
pythermostat = pkgs.python3Packages.buildPythonPackage {
|
||||
pname = "pythermostat";
|
||||
version = "0.0.0";
|
||||
format = "pyproject";
|
||||
src = "${self}/pythermostat";
|
||||
|
||||
propagatedBuildInputs =
|
||||
with pkgs.python3Packages; [
|
||||
numpy
|
||||
matplotlib
|
||||
];
|
||||
};
|
||||
in
|
||||
{
|
||||
packages.x86_64-linux = {
|
||||
inherit thermostat pythermostat;
|
||||
default = thermostat;
|
||||
};
|
||||
|
||||
hydraJobs = {
|
||||
inherit thermostat;
|
||||
};
|
||||
|
||||
devShells.x86_64-linux.default = pkgs.mkShellNoCC {
|
||||
name = "thermostat-dev-shell";
|
||||
packages =
|
||||
with pkgs;
|
||||
[
|
||||
rust
|
||||
llvm
|
||||
openocd
|
||||
dfu-util
|
||||
rlwrap
|
||||
]
|
||||
++ (with python3Packages; [
|
||||
numpy
|
||||
matplotlib
|
||||
]);
|
||||
};
|
||||
|
||||
formatter.x86_64-linux = nixpkgs.legacyPackages.x86_64-linux.nixfmt-rfc-style;
|
||||
};
|
||||
}
|
11
memory.x
11
memory.x
@ -1,10 +1,17 @@
|
||||
MEMORY
|
||||
{
|
||||
FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 2048K
|
||||
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 112K
|
||||
FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 1024K
|
||||
/* reserved for config data */
|
||||
CONFIG (rx) : ORIGIN = 0x8100000, LENGTH = 16K
|
||||
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 112K - 4
|
||||
/* reserved for DFU trigger message */
|
||||
DFU_MSG (wrx) : ORIGIN = 0x2001BFFC, LENGTH = 4
|
||||
RAM2 (xrw) : ORIGIN = 0x2001C000, LENGTH = 16K
|
||||
RAM3 (xrw) : ORIGIN = 0x20020000, LENGTH = 64K
|
||||
CCMRAM (rw) : ORIGIN = 0x10000000, LENGTH = 64K
|
||||
}
|
||||
|
||||
_flash_start = ORIGIN(FLASH);
|
||||
_config_start = ORIGIN(CONFIG);
|
||||
_dfu_msg = ORIGIN(DFU_MSG);
|
||||
_stack_start = ORIGIN(CCMRAM) + LENGTH(CCMRAM);
|
||||
|
13
pythermostat/example.py
Normal file
13
pythermostat/example.py
Normal file
@ -0,0 +1,13 @@
|
||||
import time
|
||||
from pythermostat.client import Client
|
||||
|
||||
tec = Client() #(host="localhost", port=6667)
|
||||
tec.set_param("b-p", 1, "t0", 20)
|
||||
print(tec.get_output())
|
||||
print(tec.get_pid())
|
||||
print(tec.get_output())
|
||||
print(tec.get_postfilter())
|
||||
print(tec.get_b_parameter())
|
||||
while True:
|
||||
print(tec.get_report())
|
||||
time.sleep(0.05)
|
18
pythermostat/pyproject.toml
Normal file
18
pythermostat/pyproject.toml
Normal file
@ -0,0 +1,18 @@
|
||||
[build-system]
|
||||
requires = ["setuptools"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "pythermostat"
|
||||
version = "0.0"
|
||||
authors = [{name = "M-Labs"}]
|
||||
description = "Python utilities for the Sinara 8451 Thermostat"
|
||||
urls.Repository = "https://git.m-labs.hk/M-Labs/thermostat"
|
||||
license = {text = "GPLv3"}
|
||||
|
||||
[project.gui-scripts]
|
||||
thermostat_plot = "pythermostat.plot:main"
|
||||
|
||||
[project.scripts]
|
||||
thermostat_autotune = "pythermostat.autotune:main"
|
||||
thermostat_test = "pythermostat.test:main"
|
0
pythermostat/pythermostat/__init__.py
Normal file
0
pythermostat/pythermostat/__init__.py
Normal file
266
pythermostat/pythermostat/autotune.py
Normal file
266
pythermostat/pythermostat/autotune.py
Normal file
@ -0,0 +1,266 @@
|
||||
import math
|
||||
import logging
|
||||
import time
|
||||
from collections import deque, namedtuple
|
||||
from enum import Enum
|
||||
|
||||
from pythermostat.client import Client
|
||||
|
||||
# Based on hirshmann pid-autotune libiary
|
||||
# See https://github.com/hirschmann/pid-autotune
|
||||
# Which is in turn based on a fork of Arduino PID AutoTune Library
|
||||
# See https://github.com/t0mpr1c3/Arduino-PID-AutoTune-Library
|
||||
|
||||
|
||||
class PIDAutotuneState(Enum):
|
||||
STATE_OFF = 'off'
|
||||
STATE_RELAY_STEP_UP = 'relay step up'
|
||||
STATE_RELAY_STEP_DOWN = 'relay step down'
|
||||
STATE_SUCCEEDED = 'succeeded'
|
||||
STATE_FAILED = 'failed'
|
||||
|
||||
|
||||
class PIDAutotune:
|
||||
PIDParams = namedtuple('PIDParams', ['Kp', 'Ki', 'Kd'])
|
||||
|
||||
PEAK_AMPLITUDE_TOLERANCE = 0.05
|
||||
|
||||
_tuning_rules = {
|
||||
"ziegler-nichols": [0.6, 1.2, 0.075],
|
||||
"tyreus-luyben": [0.4545, 0.2066, 0.07214],
|
||||
"ciancone-marlin": [0.303, 0.1364, 0.0481],
|
||||
"pessen-integral": [0.7, 1.75, 0.105],
|
||||
"some-overshoot": [0.333, 0.667, 0.111],
|
||||
"no-overshoot": [0.2, 0.4, 0.0667]
|
||||
}
|
||||
|
||||
def __init__(self, setpoint, out_step=10, lookback=60,
|
||||
noiseband=0.5, sampletime=1.2):
|
||||
if setpoint is None:
|
||||
raise ValueError('setpoint must be specified')
|
||||
|
||||
self._inputs = deque(maxlen=round(lookback / sampletime))
|
||||
self._setpoint = setpoint
|
||||
self._outputstep = out_step
|
||||
self._noiseband = noiseband
|
||||
self._out_min = -out_step
|
||||
self._out_max = out_step
|
||||
self._state = PIDAutotuneState.STATE_OFF
|
||||
self._peak_timestamps = deque(maxlen=5)
|
||||
self._peaks = deque(maxlen=5)
|
||||
self._output = 0
|
||||
self._last_run_timestamp = 0
|
||||
self._peak_type = 0
|
||||
self._peak_count = 0
|
||||
self._initial_output = 0
|
||||
self._induced_amplitude = 0
|
||||
self._Ku = 0
|
||||
self._Pu = 0
|
||||
|
||||
def state(self):
|
||||
"""Get the current state."""
|
||||
return self._state
|
||||
|
||||
def output(self):
|
||||
"""Get the last output value."""
|
||||
return self._output
|
||||
|
||||
def tuning_rules(self):
|
||||
"""Get a list of all available tuning rules."""
|
||||
return self._tuning_rules.keys()
|
||||
|
||||
def get_pid_parameters(self, tuning_rule='ziegler-nichols'):
|
||||
"""Get PID parameters.
|
||||
|
||||
Args:
|
||||
tuning_rule (str): Sets the rule which should be used to calculate
|
||||
the parameters.
|
||||
"""
|
||||
divisors = self._tuning_rules[tuning_rule]
|
||||
kp = self._Ku * divisors[0]
|
||||
ki = divisors[1] * self._Ku / self._Pu
|
||||
kd = divisors[2] * self._Ku * self._Pu
|
||||
return PIDAutotune.PIDParams(kp, ki, kd)
|
||||
|
||||
def run(self, input_val, time_input):
|
||||
"""To autotune a system, this method must be called periodically.
|
||||
|
||||
Args:
|
||||
input_val (float): The temperature input value.
|
||||
time_input (float): Current time in seconds.
|
||||
|
||||
Returns:
|
||||
`true` if tuning is finished, otherwise `false`.
|
||||
"""
|
||||
now = time_input * 1000
|
||||
|
||||
if (self._state == PIDAutotuneState.STATE_OFF
|
||||
or self._state == PIDAutotuneState.STATE_SUCCEEDED
|
||||
or self._state == PIDAutotuneState.STATE_FAILED):
|
||||
self._state = PIDAutotuneState.STATE_RELAY_STEP_UP
|
||||
|
||||
self._last_run_timestamp = now
|
||||
|
||||
# check input and change relay state if necessary
|
||||
if (self._state == PIDAutotuneState.STATE_RELAY_STEP_UP
|
||||
and input_val > self._setpoint + self._noiseband):
|
||||
self._state = PIDAutotuneState.STATE_RELAY_STEP_DOWN
|
||||
logging.debug('switched state: {0}'.format(self._state))
|
||||
logging.debug('input: {0}'.format(input_val))
|
||||
elif (self._state == PIDAutotuneState.STATE_RELAY_STEP_DOWN
|
||||
and input_val < self._setpoint - self._noiseband):
|
||||
self._state = PIDAutotuneState.STATE_RELAY_STEP_UP
|
||||
logging.debug('switched state: {0}'.format(self._state))
|
||||
logging.debug('input: {0}'.format(input_val))
|
||||
|
||||
# set output
|
||||
if (self._state == PIDAutotuneState.STATE_RELAY_STEP_UP):
|
||||
self._output = self._initial_output - self._outputstep
|
||||
elif self._state == PIDAutotuneState.STATE_RELAY_STEP_DOWN:
|
||||
self._output = self._initial_output + self._outputstep
|
||||
|
||||
# respect output limits
|
||||
self._output = min(self._output, self._out_max)
|
||||
self._output = max(self._output, self._out_min)
|
||||
|
||||
# identify peaks
|
||||
is_max = True
|
||||
is_min = True
|
||||
|
||||
for val in self._inputs:
|
||||
is_max = is_max and (input_val >= val)
|
||||
is_min = is_min and (input_val <= val)
|
||||
|
||||
self._inputs.append(input_val)
|
||||
|
||||
# we don't trust the maxes or mins until the input array is full
|
||||
if len(self._inputs) < self._inputs.maxlen:
|
||||
return False
|
||||
|
||||
# increment peak count and record peak time for maxima and minima
|
||||
inflection = False
|
||||
|
||||
# peak types:
|
||||
# -1: minimum
|
||||
# +1: maximum
|
||||
if is_max:
|
||||
if self._peak_type == -1:
|
||||
inflection = True
|
||||
self._peak_type = 1
|
||||
elif is_min:
|
||||
if self._peak_type == 1:
|
||||
inflection = True
|
||||
self._peak_type = -1
|
||||
|
||||
# update peak times and values
|
||||
if inflection:
|
||||
self._peak_count += 1
|
||||
self._peaks.append(input_val)
|
||||
self._peak_timestamps.append(now)
|
||||
logging.debug('found peak: {0}'.format(input_val))
|
||||
logging.debug('peak count: {0}'.format(self._peak_count))
|
||||
|
||||
# check for convergence of induced oscillation
|
||||
# convergence of amplitude assessed on last 4 peaks (1.5 cycles)
|
||||
self._induced_amplitude = 0
|
||||
|
||||
if inflection and (self._peak_count > 4):
|
||||
abs_max = self._peaks[-2]
|
||||
abs_min = self._peaks[-2]
|
||||
for i in range(0, len(self._peaks) - 2):
|
||||
self._induced_amplitude += abs(self._peaks[i]
|
||||
- self._peaks[i+1])
|
||||
abs_max = max(self._peaks[i], abs_max)
|
||||
abs_min = min(self._peaks[i], abs_min)
|
||||
|
||||
self._induced_amplitude /= 6.0
|
||||
|
||||
# check convergence criterion for amplitude of induced oscillation
|
||||
amplitude_dev = ((0.5 * (abs_max - abs_min)
|
||||
- self._induced_amplitude)
|
||||
/ self._induced_amplitude)
|
||||
|
||||
logging.debug('amplitude: {0}'.format(self._induced_amplitude))
|
||||
logging.debug('amplitude deviation: {0}'.format(amplitude_dev))
|
||||
|
||||
if amplitude_dev < PIDAutotune.PEAK_AMPLITUDE_TOLERANCE:
|
||||
self._state = PIDAutotuneState.STATE_SUCCEEDED
|
||||
|
||||
# if the autotune has not already converged
|
||||
# terminate after 10 cycles
|
||||
if self._peak_count >= 20:
|
||||
self._output = 0
|
||||
self._state = PIDAutotuneState.STATE_FAILED
|
||||
return True
|
||||
|
||||
if self._state == PIDAutotuneState.STATE_SUCCEEDED:
|
||||
self._output = 0
|
||||
logging.debug('peak finding successful')
|
||||
|
||||
# calculate ultimate gain
|
||||
self._Ku = 4.0 * self._outputstep / \
|
||||
(self._induced_amplitude * math.pi)
|
||||
print('Ku: {0}'.format(self._Ku))
|
||||
|
||||
# calculate ultimate period in seconds
|
||||
period1 = self._peak_timestamps[3] - self._peak_timestamps[1]
|
||||
period2 = self._peak_timestamps[4] - self._peak_timestamps[2]
|
||||
self._Pu = 0.5 * (period1 + period2) / 1000.0
|
||||
print('Pu: {0}'.format(self._Pu))
|
||||
|
||||
for rule in self._tuning_rules:
|
||||
params = self.get_pid_parameters(rule)
|
||||
print('rule: {0}'.format(rule))
|
||||
print('Kp: {0}'.format(params.Kp))
|
||||
print('Ki: {0}'.format(params.Ki))
|
||||
print('Kd: {0}'.format(params.Kd))
|
||||
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
# Auto tune parameters
|
||||
# Thermostat channel
|
||||
channel = 0
|
||||
# Target temperature of the autotune routine, celcius
|
||||
target_temperature = 20
|
||||
# Value by which output will be increased/decreased from zero, amps
|
||||
output_step = 1
|
||||
# Reference period for local minima/maxima, seconds
|
||||
lookback = 3
|
||||
# Determines by how much the input value must
|
||||
# overshoot/undershoot the setpoint, celcius
|
||||
noiseband = 1.5
|
||||
|
||||
# logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
tec = Client()
|
||||
|
||||
data = tec.get_report()
|
||||
ch = data[channel]
|
||||
|
||||
tuner = PIDAutotune(target_temperature, output_step,
|
||||
lookback, noiseband, ch['interval'])
|
||||
|
||||
while True:
|
||||
data = tec.get_report()
|
||||
|
||||
ch = data[channel]
|
||||
|
||||
temperature = ch['temperature']
|
||||
|
||||
if (tuner.run(temperature, ch['time'])):
|
||||
break
|
||||
|
||||
tuner_out = tuner.output()
|
||||
|
||||
tec.set_param("output", channel, "i_set", tuner_out)
|
||||
|
||||
time.sleep(0.05)
|
||||
|
||||
tec.set_param("output", channel, "i_set", 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
207
pythermostat/pythermostat/client.py
Normal file
207
pythermostat/pythermostat/client.py
Normal file
@ -0,0 +1,207 @@
|
||||
import socket
|
||||
import json
|
||||
import logging
|
||||
|
||||
|
||||
class CommandError(Exception):
|
||||
pass
|
||||
|
||||
class Client:
|
||||
def __init__(self, host="192.168.1.26", port=23, timeout=None):
|
||||
self._socket = socket.create_connection((host, port), timeout)
|
||||
self._lines = [""]
|
||||
self._check_zero_limits()
|
||||
|
||||
def disconnect(self):
|
||||
self._socket.shutdown(socket.SHUT_RDWR)
|
||||
self._socket.close()
|
||||
|
||||
def _check_zero_limits(self):
|
||||
output_report = self.get_output()
|
||||
for output_channel in output_report:
|
||||
for limit in ["max_i_neg", "max_i_pos", "max_v"]:
|
||||
if output_channel[limit] == 0.0:
|
||||
logging.warning("`{}` limit is set to zero on channel {}".format(limit, output_channel["channel"]))
|
||||
|
||||
def _read_line(self):
|
||||
# read more lines
|
||||
while len(self._lines) <= 1:
|
||||
chunk = self._socket.recv(4096)
|
||||
if not chunk:
|
||||
return None
|
||||
buf = self._lines[-1] + chunk.decode('utf-8', errors='ignore')
|
||||
self._lines = buf.split("\n")
|
||||
|
||||
line = self._lines[0]
|
||||
self._lines = self._lines[1:]
|
||||
return line
|
||||
|
||||
def _command(self, *command):
|
||||
self._socket.sendall((" ".join(command) + "\n").encode('utf-8'))
|
||||
|
||||
line = self._read_line()
|
||||
response = json.loads(line)
|
||||
if "error" in response:
|
||||
raise CommandError(response["error"])
|
||||
return response
|
||||
|
||||
def _get_conf(self, topic):
|
||||
result = [None, None]
|
||||
for item in self._command(topic):
|
||||
result[int(item["channel"])] = item
|
||||
return result
|
||||
|
||||
def get_output(self):
|
||||
"""Retrieve output limits for the TEC
|
||||
|
||||
Example::
|
||||
[{'channel': 0,
|
||||
'center': 'vref',
|
||||
'i_set': -0.02002179650216762,
|
||||
'max_i_neg': 2.0,
|
||||
'max_v': 3.988,
|
||||
'max_i_pos': 2.0,
|
||||
'polarity': 'normal',
|
||||
{'channel': 1,
|
||||
'center': 'vref',
|
||||
'i_set': -0.02002179650216762,
|
||||
'max_i_neg': 2.0,
|
||||
'max_v': 3.988,
|
||||
'max_i_pos': 2.0}
|
||||
'polarity': 'normal',
|
||||
]
|
||||
"""
|
||||
return self._get_conf("output")
|
||||
|
||||
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},
|
||||
'target': 37.0},
|
||||
{'channel': 1,
|
||||
'parameters': {
|
||||
'kp': 10.0,
|
||||
'ki': 0.02,
|
||||
'kd': 0.0,
|
||||
'output_min': 0.0,
|
||||
'output_max': 3.0},
|
||||
'target': 36.5}]
|
||||
"""
|
||||
return self._get_conf("pid")
|
||||
|
||||
def get_b_parameter(self):
|
||||
"""Retrieve B-Parameter equation 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("b-p")
|
||||
|
||||
def get_postfilter(self):
|
||||
"""Retrieve DAC postfilter configuration
|
||||
|
||||
Example::
|
||||
[{'rate': None, 'channel': 0},
|
||||
{'rate': 21.25, 'channel': 1}]
|
||||
"""
|
||||
return self._get_conf("postfilter")
|
||||
|
||||
def get_report(self):
|
||||
"""Obtain one-time report on measurement values
|
||||
|
||||
Example of yielded data::
|
||||
{'channel': 0,
|
||||
'time': 2302524,
|
||||
'interval': 0.12
|
||||
'adc': 0.6199188965423515,
|
||||
'sens': 6138.519310282602,
|
||||
'temperature': 36.87032392655527,
|
||||
'pid_engaged': True,
|
||||
'i_set': 2.0635816680889123,
|
||||
'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}
|
||||
"""
|
||||
return self._get_conf("report")
|
||||
|
||||
def get_ipv4(self):
|
||||
"""Get the IPv4 settings of the Thermostat"""
|
||||
return self._command("ipv4")
|
||||
|
||||
def get_fan(self):
|
||||
"""Get Thermostat current fan settings"""
|
||||
return self._command("fan")
|
||||
|
||||
def get_hwrev(self):
|
||||
"""Get Thermostat hardware revision"""
|
||||
return self._command("hwrev")
|
||||
|
||||
def set_param(self, topic, channel, field="", value=""):
|
||||
"""Set configuration parameters
|
||||
|
||||
Examples::
|
||||
tec.set_param("output", 0, "max_v", 2.0)
|
||||
tec.set_param("pid", 1, "output_max", 2.5)
|
||||
tec.set_param("b-p", 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("output", channel, "pid")
|
||||
|
||||
def save_config(self, channel=""):
|
||||
"""Save current configuration to EEPROM"""
|
||||
self._command("save", channel)
|
||||
if channel != "":
|
||||
self._read_line() # read the extra {}
|
||||
|
||||
def load_config(self, channel=""):
|
||||
"""Load current configuration from EEPROM"""
|
||||
self._command("load", channel)
|
||||
if channel != "":
|
||||
self._read_line() # read the extra {}
|
||||
|
||||
def reset(self):
|
||||
"""Reset the device"""
|
||||
self._socket.sendall("reset".encode("utf-8"))
|
||||
self.disconnect() # resetting ends the TCP session, disconnect anyway
|
||||
|
||||
def enter_dfu_mode(self):
|
||||
"""Reset device and enters USB device firmware update (DFU) mode"""
|
||||
self._socket.sendall("dfu".encode("utf-8"))
|
||||
self.disconnect() # resetting ends the TCP session, disconnect anyway
|
||||
|
||||
def set_ipv4(self, address, netmask, gateway=""):
|
||||
"""Configure IPv4 address, netmask length, and optional default gateway"""
|
||||
self._command("ipv4", f"{address}/{netmask}", gateway)
|
||||
|
||||
def set_fan(self, power=None):
|
||||
"""Set fan power with values from 1 to 100. If omitted, set according to fcurve"""
|
||||
if power is None:
|
||||
power = "auto"
|
||||
self._command("fan", power)
|
||||
|
||||
def set_fcurve(self, a=1.0, b=0.0, c=0.0):
|
||||
"""Set fan controller curve coefficients"""
|
||||
self._command("fcurve", a, b, c)
|
137
pythermostat/pythermostat/plot.py
Normal file
137
pythermostat/pythermostat/plot.py
Normal file
@ -0,0 +1,137 @@
|
||||
import time
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.animation as animation
|
||||
from threading import Thread, Lock
|
||||
from pythermostat.client import Client
|
||||
|
||||
|
||||
def main():
|
||||
TIME_WINDOW = 300.0
|
||||
|
||||
tec = Client()
|
||||
target_temperature = tec.get_pid()[0]['target']
|
||||
print("Channel 0 target temperature: {:.3f}".format(target_temperature))
|
||||
|
||||
class Series:
|
||||
def __init__(self, conv=lambda x: x):
|
||||
self.conv = conv
|
||||
self.x_data = []
|
||||
self.y_data = []
|
||||
|
||||
def append(self, x, y):
|
||||
self.x_data.append(x)
|
||||
self.y_data.append(self.conv(y))
|
||||
|
||||
def clip(self, min_x):
|
||||
drop = 0
|
||||
while drop < len(self.x_data) and self.x_data[drop] < min_x:
|
||||
drop += 1
|
||||
self.x_data = self.x_data[drop:]
|
||||
self.y_data = self.y_data[drop:]
|
||||
|
||||
series = {
|
||||
# 'adc': Series(),
|
||||
# 'sens': Series(lambda x: x * 0.0001),
|
||||
'temperature': Series(),
|
||||
# 'i_set': Series(),
|
||||
'pid_output': Series(),
|
||||
# 'vref': Series(),
|
||||
# 'dac_value': Series(),
|
||||
# 'dac_feedback': Series(),
|
||||
# 'i_tec': Series(),
|
||||
'tec_i': Series(),
|
||||
'tec_u_meas': Series(),
|
||||
# 'interval': Series(),
|
||||
}
|
||||
series_lock = Lock()
|
||||
|
||||
quit = False
|
||||
|
||||
def recv_data(tec):
|
||||
global last_packet_time
|
||||
while True:
|
||||
data = tec.get_report()
|
||||
ch0 = data[0]
|
||||
series_lock.acquire()
|
||||
try:
|
||||
for k, s in series.items():
|
||||
if k in ch0:
|
||||
v = ch0[k]
|
||||
if type(v) is float:
|
||||
s.append(ch0['time'], v)
|
||||
finally:
|
||||
series_lock.release()
|
||||
|
||||
if quit:
|
||||
break
|
||||
time.sleep(0.05)
|
||||
|
||||
thread = Thread(target=recv_data, args=(tec,))
|
||||
thread.start()
|
||||
|
||||
fig, ax = plt.subplots()
|
||||
|
||||
for k, s in series.items():
|
||||
s.plot, = ax.plot([], [], label=k)
|
||||
legend = ax.legend()
|
||||
|
||||
def animate(i):
|
||||
min_x, max_x, min_y, max_y = None, None, None, None
|
||||
|
||||
series_lock.acquire()
|
||||
try:
|
||||
for k, s in series.items():
|
||||
s.plot.set_data(s.x_data, s.y_data)
|
||||
if len(s.y_data) > 0:
|
||||
s.plot.set_label("{}: {:.3f}".format(k, s.y_data[-1]))
|
||||
|
||||
if len(s.x_data) > 0:
|
||||
min_x_ = min(s.x_data)
|
||||
if min_x is None:
|
||||
min_x = min_x_
|
||||
else:
|
||||
min_x = min(min_x, min_x_)
|
||||
max_x_ = max(s.x_data)
|
||||
if max_x is None:
|
||||
max_x = max_x_
|
||||
else:
|
||||
max_x = max(max_x, max_x_)
|
||||
if len(s.y_data) > 0:
|
||||
min_y_ = min(s.y_data)
|
||||
if min_y is None:
|
||||
min_y = min_y_
|
||||
else:
|
||||
min_y = min(min_y, min_y_)
|
||||
max_y_ = max(s.y_data)
|
||||
if max_y is None:
|
||||
max_y = max_y_
|
||||
else:
|
||||
max_y = max(max_y, max_y_)
|
||||
|
||||
if min_x and max_x - TIME_WINDOW > min_x:
|
||||
for s in series.values():
|
||||
s.clip(max_x - TIME_WINDOW)
|
||||
finally:
|
||||
series_lock.release()
|
||||
|
||||
if min_x != max_x:
|
||||
ax.set_xlim(min_x, max_x)
|
||||
if min_y != max_y:
|
||||
margin_y = 0.01 * (max_y - min_y)
|
||||
ax.set_ylim(min_y - margin_y, max_y + margin_y)
|
||||
|
||||
nonlocal legend
|
||||
legend.remove()
|
||||
legend = ax.legend()
|
||||
|
||||
ani = animation.FuncAnimation(
|
||||
fig, animate, interval=1, blit=False, save_count=50)
|
||||
|
||||
plt.show()
|
||||
quit = True
|
||||
thread.join()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
81
pythermostat/pythermostat/test.py
Normal file
81
pythermostat/pythermostat/test.py
Normal file
@ -0,0 +1,81 @@
|
||||
import argparse
|
||||
from contextlib import contextmanager
|
||||
from pythermostat.client import Client
|
||||
|
||||
|
||||
CHANNELS = 2
|
||||
|
||||
|
||||
def get_argparser():
|
||||
parser = argparse.ArgumentParser(description="Thermostat hardware testing script")
|
||||
|
||||
parser.add_argument("host", metavar="HOST", default="192.168.1.26", nargs="?")
|
||||
parser.add_argument("port", metavar="PORT", default=23, nargs="?")
|
||||
parser.add_argument(
|
||||
"-r",
|
||||
"--testing_resistance",
|
||||
default=10_000,
|
||||
help="Testing resistance value through SENS pin in Ohms",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-d",
|
||||
"--deviation",
|
||||
default=1,
|
||||
help="Allowed deviation of resistance in percentage",
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def main():
|
||||
args = get_argparser().parse_args()
|
||||
|
||||
min_allowed_resistance = args.testing_resistance * (1 - args.deviation / 100)
|
||||
max_allowed_resistance = args.testing_resistance * (1 + args.deviation / 100)
|
||||
|
||||
print(min_allowed_resistance, max_allowed_resistance)
|
||||
|
||||
thermostat = Client(args.host, args.port)
|
||||
for channel in range(CHANNELS):
|
||||
print(f"Channel {channel} is active")
|
||||
|
||||
print("Checking resistance through SENS input ....", end=" ")
|
||||
sens_resistance = thermostat.get_report()[channel]["sens"]
|
||||
if sens_resistance is not None:
|
||||
print(sens_resistance, "Ω")
|
||||
if min_allowed_resistance <= sens_resistance <= max_allowed_resistance:
|
||||
print("PASSED")
|
||||
else:
|
||||
print("FAILED")
|
||||
else:
|
||||
print("Floating SENS input! Is the channel connected?")
|
||||
|
||||
with preserve_thermostat_output_settings(thermostat, channel):
|
||||
test_output_settings = {
|
||||
"max_i_pos": 2,
|
||||
"max_i_neg": 2,
|
||||
"max_v": 4,
|
||||
"i_set": 0.1,
|
||||
"polarity": "normal",
|
||||
}
|
||||
for field, value in test_output_settings.items():
|
||||
thermostat.set_param("output", channel, field, value)
|
||||
|
||||
input(f"Check if channel {channel} current = 0.1 A, and press ENTER...")
|
||||
|
||||
input(f"Channel {channel} testing done, press ENTER to continue.")
|
||||
print()
|
||||
|
||||
print("Testing complete.")
|
||||
|
||||
|
||||
@contextmanager
|
||||
def preserve_thermostat_output_settings(client, channel):
|
||||
original_output_settings = client.get_output()[channel]
|
||||
yield original_output_settings
|
||||
for setting in "max_i_pos", "max_i_neg", "max_v", "i_set", "polarity":
|
||||
client.set_param("output", channel, setting, original_output_settings[setting])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
@ -1,10 +1,8 @@
|
||||
use crate::timer::sleep;
|
||||
use stm32f4xx_hal::{
|
||||
hal::{
|
||||
blocking::spi::Transfer,
|
||||
digital::v2::OutputPin,
|
||||
},
|
||||
time::MegaHertz,
|
||||
hal::{blocking::spi::Transfer, digital::v2::OutputPin},
|
||||
spi,
|
||||
time::MegaHertz,
|
||||
};
|
||||
|
||||
/// SPI Mode 1
|
||||
@ -26,30 +24,25 @@ pub struct Dac<SPI: Transfer<u8>, S: OutputPin> {
|
||||
impl<SPI: Transfer<u8>, S: OutputPin> Dac<SPI, S> {
|
||||
pub fn new(spi: SPI, mut sync: S) -> Self {
|
||||
let _ = sync.set_low();
|
||||
|
||||
Dac {
|
||||
spi,
|
||||
sync,
|
||||
}
|
||||
|
||||
Dac { spi, sync }
|
||||
}
|
||||
|
||||
fn write(&mut self, mut buf: [u8; 3]) -> Result<(), SPI::Error> {
|
||||
fn write(&mut self, buf: &mut [u8]) -> 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();
|
||||
cortex_m::asm::nop();
|
||||
// must be high for >= 33 ns
|
||||
sleep(1);
|
||||
let _ = self.sync.set_low();
|
||||
|
||||
self.spi.transfer(&mut buf)?;
|
||||
self.spi.transfer(buf)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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(buf)
|
||||
pub fn set(&mut self, value: u32) -> Result<u32, SPI::Error> {
|
||||
let value = value.min(MAX_VALUE);
|
||||
let mut buf = [(value >> 14) as u8, (value >> 6) as u8, (value << 2) as u8];
|
||||
self.write(&mut buf)?;
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,12 @@
|
||||
use super::{
|
||||
checksum::{Checksum, ChecksumMode},
|
||||
regs::{self, Register, RegisterData},
|
||||
DigitalFilterOrder, Input, Mode, PostFilter, RefSource,
|
||||
};
|
||||
use core::fmt;
|
||||
use log::{info, warn};
|
||||
use stm32f4xx_hal::hal::{
|
||||
blocking::spi::Transfer,
|
||||
digital::v2::OutputPin,
|
||||
};
|
||||
use super::{
|
||||
regs::{self, Register, RegisterData},
|
||||
checksum::{ChecksumMode, Checksum},
|
||||
Mode, Input, RefSource, PostFilter, DigitalFilterOrder,
|
||||
};
|
||||
use stm32f4xx_hal::hal::{blocking::spi::Transfer, digital::v2::OutputPin};
|
||||
use uom::si::{electric_potential::volt, f64::ElectricPotential};
|
||||
|
||||
/// AD7172-2 implementation
|
||||
///
|
||||
@ -23,7 +21,8 @@ impl<SPI: Transfer<u8, Error = E>, NSS: OutputPin, E: fmt::Debug> Adc<SPI, NSS>
|
||||
pub fn new(spi: SPI, mut nss: NSS) -> Result<Self, SPI::Error> {
|
||||
let _ = nss.set_high();
|
||||
let mut adc = Adc {
|
||||
spi, nss,
|
||||
spi,
|
||||
nss,
|
||||
checksum_mode: ChecksumMode::Off,
|
||||
};
|
||||
adc.reset()?;
|
||||
@ -51,8 +50,7 @@ impl<SPI: Transfer<u8, Error = E>, NSS: OutputPin, E: fmt::Debug> Adc<SPI, NSS>
|
||||
|
||||
/// `0x00DX` for AD7172-2
|
||||
pub fn identify(&mut self) -> Result<u16, SPI::Error> {
|
||||
self.read_reg(®s::Id)
|
||||
.map(|id| id.id())
|
||||
self.read_reg(®s::Id).map(|id| id.id())
|
||||
}
|
||||
|
||||
pub fn set_checksum_mode(&mut self, mode: ChecksumMode) -> Result<(), SPI::Error> {
|
||||
@ -72,7 +70,10 @@ impl<SPI: Transfer<u8, Error = E>, NSS: OutputPin, E: fmt::Debug> Adc<SPI, NSS>
|
||||
}
|
||||
|
||||
pub fn setup_channel(
|
||||
&mut self, index: u8, in_pos: Input, in_neg: Input
|
||||
&mut self,
|
||||
index: u8,
|
||||
in_pos: Input,
|
||||
in_neg: Input,
|
||||
) -> Result<(), SPI::Error> {
|
||||
self.update_reg(®s::SetupCon { index }, |data| {
|
||||
data.set_bipolar(false);
|
||||
@ -86,6 +87,8 @@ 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);
|
||||
@ -96,45 +99,15 @@ impl<SPI: Transfer<u8, Error = E>, NSS: OutputPin, E: fmt::Debug> Adc<SPI, NSS>
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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 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 start_continuous_conversion(&mut self) -> Result<(), SPI::Error> {
|
||||
@ -147,44 +120,43 @@ impl<SPI: Transfer<u8, Error = E>, NSS: OutputPin, E: fmt::Debug> Adc<SPI, NSS>
|
||||
}
|
||||
|
||||
pub fn get_postfilter(&mut self, index: u8) -> Result<Option<PostFilter>, SPI::Error> {
|
||||
self.read_reg(®s::FiltCon { index })
|
||||
.map(|data| {
|
||||
if data.enh_filt_en() {
|
||||
Some(data.enh_filt())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
self.read_reg(®s::FiltCon { index }).map(|data| {
|
||||
if data.enh_filt_en() {
|
||||
Some(data.enh_filt())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_postfilter(&mut self, index: u8, filter: Option<PostFilter>) -> Result<(), SPI::Error> {
|
||||
self.update_reg(®s::FiltCon { index }, |data| {
|
||||
match filter {
|
||||
None => data.set_enh_filt_en(false),
|
||||
Some(filter) => {
|
||||
data.set_enh_filt_en(true);
|
||||
data.set_enh_filt(filter);
|
||||
}
|
||||
pub fn set_postfilter(
|
||||
&mut self,
|
||||
index: u8,
|
||||
filter: Option<PostFilter>,
|
||||
) -> Result<(), SPI::Error> {
|
||||
self.update_reg(®s::FiltCon { index }, |data| match filter {
|
||||
None => data.set_enh_filt_en(false),
|
||||
Some(filter) => {
|
||||
data.set_enh_filt_en(true);
|
||||
data.set_enh_filt(filter);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the channel the data is from
|
||||
pub fn data_ready(&mut self) -> Result<Option<u8>, SPI::Error> {
|
||||
self.read_reg(®s::Status)
|
||||
.map(|status| {
|
||||
if status.ready() {
|
||||
Some(status.channel())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
self.read_reg(®s::Status).map(|status| {
|
||||
if status.ready() {
|
||||
Some(status.channel())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Get data
|
||||
pub fn read_data(&mut self) -> Result<u32, SPI::Error> {
|
||||
self.read_reg(®s::Data)
|
||||
.map(|data| data.data())
|
||||
self.read_reg(®s::Data).map(|data| data.data())
|
||||
}
|
||||
|
||||
fn read_reg<R: regs::Register>(&mut self, reg: &R) -> Result<R::Data, SPI::Error> {
|
||||
@ -203,12 +175,21 @@ impl<SPI: Transfer<u8, Error = E>, NSS: OutputPin, E: fmt::Debug> Adc<SPI, NSS>
|
||||
break;
|
||||
}
|
||||
// Retry
|
||||
warn!("read_reg {:02X}: checksum error: {:?}!={:?}, retrying", reg.address(), checksum_expected, checksum_in);
|
||||
warn!(
|
||||
"read_reg {:02X}: checksum error: {:?}!={:?}, retrying",
|
||||
reg.address(),
|
||||
checksum_expected,
|
||||
checksum_in
|
||||
);
|
||||
}
|
||||
Ok(reg_data)
|
||||
}
|
||||
|
||||
fn write_reg<R: regs::Register>(&mut self, reg: &R, reg_data: &mut R::Data) -> Result<(), SPI::Error> {
|
||||
fn write_reg<R: regs::Register>(
|
||||
&mut self,
|
||||
reg: &R,
|
||||
reg_data: &mut R::Data,
|
||||
) -> Result<(), SPI::Error> {
|
||||
loop {
|
||||
let address = reg.address();
|
||||
let mut checksum = Checksum::new(match self.checksum_mode {
|
||||
@ -218,7 +199,7 @@ impl<SPI: Transfer<u8, Error = E>, NSS: OutputPin, E: fmt::Debug> Adc<SPI, NSS>
|
||||
ChecksumMode::Crc => ChecksumMode::Crc,
|
||||
});
|
||||
checksum.feed(&[address]);
|
||||
checksum.feed(®_data);
|
||||
checksum.feed(reg_data);
|
||||
let checksum_out = checksum.result();
|
||||
|
||||
let mut data = reg_data.clone();
|
||||
@ -229,7 +210,10 @@ impl<SPI: Transfer<u8, Error = E>, NSS: OutputPin, E: fmt::Debug> Adc<SPI, NSS>
|
||||
if *readback_data == **reg_data {
|
||||
return Ok(());
|
||||
}
|
||||
warn!("write_reg {:02X}: readback error, {:?}!={:?}, retrying", address, &*readback_data, &**reg_data);
|
||||
warn!(
|
||||
"write_reg {:02X}: readback error, {:?}!={:?}, retrying",
|
||||
address, &*readback_data, &**reg_data
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -253,7 +237,12 @@ impl<SPI: Transfer<u8, Error = E>, NSS: OutputPin, E: fmt::Debug> Adc<SPI, NSS>
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn transfer<'w>(&mut self, addr: u8, reg_data: &'w mut [u8], checksum: Option<u8>) -> Result<Option<u8>, SPI::Error> {
|
||||
fn transfer(
|
||||
&mut self,
|
||||
addr: u8,
|
||||
reg_data: &mut [u8],
|
||||
checksum: Option<u8>,
|
||||
) -> Result<Option<u8>, SPI::Error> {
|
||||
let mut addr_buf = [addr];
|
||||
|
||||
let _ = self.nss.set_low();
|
||||
@ -262,8 +251,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(None),
|
||||
(Ok(_), Some(checksum_out)) => {
|
||||
let mut checksum_buf = [checksum_out; 1];
|
||||
match self.spi.transfer(&mut checksum_buf) {
|
||||
@ -271,11 +259,33 @@ impl<SPI: Transfer<u8, Error = E>, NSS: OutputPin, E: fmt::Debug> Adc<SPI, NSS>
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
(Err(e), _) =>
|
||||
Err(e),
|
||||
(Err(e), _) => Err(e),
|
||||
};
|
||||
let _ = self.nss.set_high();
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -29,13 +29,13 @@ impl Checksum {
|
||||
|
||||
fn feed_byte(&mut self, input: u8) {
|
||||
match self.mode {
|
||||
ChecksumMode::Off => {},
|
||||
ChecksumMode::Off => {}
|
||||
ChecksumMode::Xor => self.state ^= input,
|
||||
ChecksumMode::Crc => {
|
||||
for i in 0..8 {
|
||||
let input_mask = 0x80 >> i;
|
||||
self.state = (self.state << 1) ^
|
||||
if ((self.state & 0x80) != 0) != ((input & input_mask) != 0) {
|
||||
self.state = (self.state << 1)
|
||||
^ if ((self.state & 0x80) != 0) != ((input & input_mask) != 0) {
|
||||
0x07 /* x8 + x2 + x + 1 */
|
||||
} else {
|
||||
0
|
||||
@ -54,7 +54,7 @@ impl Checksum {
|
||||
pub fn result(&self) -> Option<u8> {
|
||||
match self.mode {
|
||||
ChecksumMode::Off => None,
|
||||
_ => Some(self.state)
|
||||
_ => Some(self.state),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,10 @@
|
||||
use core::fmt;
|
||||
use num_traits::float::Float;
|
||||
use stm32f4xx_hal::{
|
||||
time::MegaHertz,
|
||||
spi,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use stm32f4xx_hal::{spi, time::MegaHertz};
|
||||
|
||||
pub mod regs;
|
||||
mod checksum;
|
||||
pub mod regs;
|
||||
pub use checksum::ChecksumMode;
|
||||
mod adc;
|
||||
pub use adc::*;
|
||||
@ -21,7 +19,6 @@ pub const SPI_CLOCK: MegaHertz = MegaHertz(2);
|
||||
|
||||
pub const MAX_VALUE: u32 = 0xFF_FFFF;
|
||||
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[repr(u8)]
|
||||
pub enum Mode {
|
||||
@ -104,7 +101,8 @@ impl fmt::Display for Input {
|
||||
RefPos => "ref+",
|
||||
RefNeg => "ref-",
|
||||
_ => "<INVALID>",
|
||||
}.fmt(fmt)
|
||||
}
|
||||
.fmt(fmt)
|
||||
}
|
||||
}
|
||||
|
||||
@ -140,11 +138,12 @@ impl fmt::Display for RefSource {
|
||||
Internal => "internal",
|
||||
Avdd1MinusAvss => "avdd1-avss",
|
||||
_ => "<INVALID>",
|
||||
}.fmt(fmt)
|
||||
}
|
||||
.fmt(fmt)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[repr(u8)]
|
||||
pub enum PostFilter {
|
||||
/// 27 SPS, 47 dB rejection, 36.7 ms settling
|
||||
|
@ -1,6 +1,6 @@
|
||||
use core::ops::{Deref, DerefMut};
|
||||
use byteorder::{BigEndian, ByteOrder};
|
||||
use bit_field::BitField;
|
||||
use byteorder::{BigEndian, ByteOrder};
|
||||
use core::ops::{Deref, DerefMut};
|
||||
|
||||
use super::*;
|
||||
|
||||
@ -9,7 +9,7 @@ pub trait Register {
|
||||
fn address(&self) -> u8;
|
||||
}
|
||||
|
||||
pub trait RegisterData: Clone + Deref<Target=[u8]> + DerefMut {
|
||||
pub trait RegisterData: Clone + Deref<Target = [u8]> + DerefMut {
|
||||
fn empty() -> Self;
|
||||
}
|
||||
|
||||
@ -49,7 +49,9 @@ macro_rules! def_reg {
|
||||
}
|
||||
};
|
||||
($Reg: ident, u8, $reg: ident, $addr: expr, $size: expr) => {
|
||||
pub struct $Reg { pub index: u8, }
|
||||
pub struct $Reg {
|
||||
pub index: u8,
|
||||
}
|
||||
impl Register for $Reg {
|
||||
type Data = $reg::Data;
|
||||
fn address(&self) -> u8 {
|
||||
@ -76,7 +78,7 @@ macro_rules! def_reg {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! reg_bit {
|
||||
@ -146,7 +148,7 @@ def_reg!(Status, status, 0x00, 1);
|
||||
impl status::Data {
|
||||
/// Is there new data to read?
|
||||
pub fn ready(&self) -> bool {
|
||||
! self.not_ready()
|
||||
!self.not_ready()
|
||||
}
|
||||
|
||||
reg_bit!(not_ready, 0, 7, "No data ready indicator");
|
||||
@ -159,9 +161,21 @@ impl status::Data {
|
||||
def_reg!(AdcMode, adc_mode, 0x01, 2);
|
||||
impl adc_mode::Data {
|
||||
reg_bits!(delay, set_delay, 0, 0..=2, "Delay after channel switch");
|
||||
reg_bit!(sing_cyc, set_sing_cyc, 0, 5, "Can only used with single channel");
|
||||
reg_bit!(
|
||||
sing_cyc,
|
||||
set_sing_cyc,
|
||||
0,
|
||||
5,
|
||||
"Can only used with single channel"
|
||||
);
|
||||
reg_bit!(hide_delay, set_hide_delay, 0, 6, "Hide delay");
|
||||
reg_bit!(ref_en, set_ref_en, 0, 7, "Enable internal reference, output buffered 2.5 V to REFOUT");
|
||||
reg_bit!(
|
||||
ref_en,
|
||||
set_ref_en,
|
||||
0,
|
||||
7,
|
||||
"Enable internal reference, output buffered 2.5 V to REFOUT"
|
||||
);
|
||||
reg_bits!(clockset, set_clocksel, 1, 2..=3, "Clock source");
|
||||
reg_bits!(mode, set_mode, 1, 4..=6, Mode, "Operating mode");
|
||||
}
|
||||
@ -174,15 +188,19 @@ impl if_mode::Data {
|
||||
def_reg!(Data, data, 0x04, 3);
|
||||
impl data::Data {
|
||||
pub fn data(&self) -> u32 {
|
||||
(u32::from(self.0[0]) << 16) |
|
||||
(u32::from(self.0[1]) << 8) |
|
||||
u32::from(self.0[2])
|
||||
(u32::from(self.0[0]) << 16) | (u32::from(self.0[1]) << 8) | u32::from(self.0[2])
|
||||
}
|
||||
}
|
||||
|
||||
def_reg!(GpioCon, gpio_con, 0x06, 2);
|
||||
impl gpio_con::Data {
|
||||
reg_bit!(sync_en, set_sync_en, 0, 3, "Enables the SYNC/ERROR pin as a sync input");
|
||||
reg_bit!(
|
||||
sync_en,
|
||||
set_sync_en,
|
||||
0,
|
||||
3,
|
||||
"Enables the SYNC/ERROR pin as a sync input"
|
||||
);
|
||||
}
|
||||
|
||||
def_reg!(Id, id, 0x07, 2);
|
||||
@ -200,8 +218,7 @@ impl channel::Data {
|
||||
/// Which input is connected to positive input of this channel
|
||||
#[allow(unused)]
|
||||
pub fn a_in_pos(&self) -> Input {
|
||||
((self.0[0].get_bits(0..=1) << 3) |
|
||||
self.0[1].get_bits(5..=7)).into()
|
||||
((self.0[0].get_bits(0..=1) << 3) | self.0[1].get_bits(5..=7)).into()
|
||||
}
|
||||
/// Set which input is connected to positive input of this channel
|
||||
#[allow(unused)]
|
||||
@ -210,27 +227,66 @@ impl channel::Data {
|
||||
self.0[0].set_bits(0..=1, value >> 3);
|
||||
self.0[1].set_bits(5..=7, value & 0x7);
|
||||
}
|
||||
reg_bits!(a_in_neg, set_a_in_neg, 1, 0..=4, Input,
|
||||
"Which input is connected to negative input of this channel");
|
||||
reg_bits!(
|
||||
a_in_neg,
|
||||
set_a_in_neg,
|
||||
1,
|
||||
0..=4,
|
||||
Input,
|
||||
"Which input is connected to negative input of this channel"
|
||||
);
|
||||
}
|
||||
|
||||
def_reg!(SetupCon, u8, setup_con, 0x20, 2);
|
||||
impl setup_con::Data {
|
||||
reg_bit!(bipolar, set_bipolar, 0, 4, "Unipolar (`false`) or bipolar (`true`) coded output");
|
||||
reg_bit!(
|
||||
bipolar,
|
||||
set_bipolar,
|
||||
0,
|
||||
4,
|
||||
"Unipolar (`false`) or bipolar (`true`) coded output"
|
||||
);
|
||||
reg_bit!(refbuf_pos, set_refbuf_pos, 0, 3, "Enable REF+ input buffer");
|
||||
reg_bit!(refbuf_neg, set_refbuf_neg, 0, 2, "Enable REF- input buffer");
|
||||
reg_bit!(ainbuf_pos, set_ainbuf_pos, 0, 1, "Enable AIN+ input buffer");
|
||||
reg_bit!(ainbuf_neg, set_ainbuf_neg, 0, 0, "Enable AIN- input buffer");
|
||||
reg_bit!(burnout_en, 1, 7, "enables a 10 µA current source on the positive analog input selected and a 10 µA current sink on the negative analog input selected");
|
||||
reg_bits!(ref_sel, set_ref_sel, 1, 4..=5, RefSource, "Select reference source for conversion");
|
||||
reg_bits!(
|
||||
ref_sel,
|
||||
set_ref_sel,
|
||||
1,
|
||||
4..=5,
|
||||
RefSource,
|
||||
"Select reference source for conversion"
|
||||
);
|
||||
}
|
||||
|
||||
def_reg!(FiltCon, u8, filt_con, 0x28, 2);
|
||||
impl filt_con::Data {
|
||||
reg_bit!(sinc3_map, 0, 7, "If set, mapping of filter register changes to directly program the decimation rate of the sinc3 filter");
|
||||
reg_bit!(enh_filt_en, set_enh_filt_en, 0, 3, "Enable postfilters for enhanced 50Hz and 60Hz rejection");
|
||||
reg_bits!(enh_filt, set_enh_filt, 0, 0..=2, PostFilter, "Select postfilters for enhanced 50Hz and 60Hz rejection");
|
||||
reg_bits!(order, set_order, 1, 5..=6, DigitalFilterOrder, "order of the digital filter that processes the modulator data");
|
||||
reg_bit!(
|
||||
enh_filt_en,
|
||||
set_enh_filt_en,
|
||||
0,
|
||||
3,
|
||||
"Enable postfilters for enhanced 50Hz and 60Hz rejection"
|
||||
);
|
||||
reg_bits!(
|
||||
enh_filt,
|
||||
set_enh_filt,
|
||||
0,
|
||||
0..=2,
|
||||
PostFilter,
|
||||
"Select postfilters for enhanced 50Hz and 60Hz rejection"
|
||||
);
|
||||
reg_bits!(
|
||||
order,
|
||||
set_order,
|
||||
1,
|
||||
5..=6,
|
||||
DigitalFilterOrder,
|
||||
"order of the digital filter that processes the modulator data"
|
||||
);
|
||||
reg_bits!(odr, set_odr, 1, 0..=4, "Output data rate");
|
||||
}
|
||||
|
||||
@ -238,9 +294,7 @@ def_reg!(Offset, u8, offset, 0x30, 3);
|
||||
impl offset::Data {
|
||||
#[allow(unused)]
|
||||
pub fn offset(&self) -> u32 {
|
||||
(u32::from(self.0[0]) << 16) |
|
||||
(u32::from(self.0[1]) << 8) |
|
||||
u32::from(self.0[2])
|
||||
(u32::from(self.0[0]) << 16) | (u32::from(self.0[1]) << 8) | u32::from(self.0[2])
|
||||
}
|
||||
#[allow(unused)]
|
||||
pub fn set_offset(&mut self, value: u32) {
|
||||
@ -254,9 +308,7 @@ def_reg!(Gain, u8, gain, 0x38, 3);
|
||||
impl gain::Data {
|
||||
#[allow(unused)]
|
||||
pub fn gain(&self) -> u32 {
|
||||
(u32::from(self.0[0]) << 16) |
|
||||
(u32::from(self.0[1]) << 8) |
|
||||
u32::from(self.0[2])
|
||||
(u32::from(self.0[0]) << 16) | (u32::from(self.0[1]) << 8) | u32::from(self.0[2])
|
||||
}
|
||||
#[allow(unused)]
|
||||
pub fn set_gain(&mut self, value: u32) {
|
||||
|
38
src/b_parameter.rs
Normal file
38
src/b_parameter.rs
Normal file
@ -0,0 +1,38 @@
|
||||
use num_traits::float::Float;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uom::si::{
|
||||
electrical_resistance::ohm,
|
||||
f64::{ElectricalResistance, TemperatureInterval, ThermodynamicTemperature},
|
||||
ratio::ratio,
|
||||
temperature_interval::kelvin as kelvin_interval,
|
||||
thermodynamic_temperature::{degree_celsius, kelvin},
|
||||
};
|
||||
|
||||
/// B-Parameter equation parameters
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Parameters {
|
||||
/// Base temperature
|
||||
pub t0: ThermodynamicTemperature,
|
||||
/// Thermistor resistance at base temperature
|
||||
pub r0: ElectricalResistance,
|
||||
/// Beta (average slope of the function ln R vs. 1/T)
|
||||
pub b: TemperatureInterval,
|
||||
}
|
||||
|
||||
impl Parameters {
|
||||
/// Perform the resistance to temperature conversion.
|
||||
pub fn get_temperature(&self, r: ElectricalResistance) -> ThermodynamicTemperature {
|
||||
let temp = (self.t0.recip() + (r / self.r0).get::<ratio>().ln() / self.b).recip();
|
||||
ThermodynamicTemperature::new::<kelvin>(temp.get::<kelvin_interval>())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Parameters {
|
||||
fn default() -> Self {
|
||||
Parameters {
|
||||
t0: ThermodynamicTemperature::new::<degree_celsius>(25.0),
|
||||
r0: ElectricalResistance::new::<ohm>(10_000.0),
|
||||
b: TemperatureInterval::new::<kelvin_interval>(3800.0),
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +1,10 @@
|
||||
use stm32f4xx_hal::hal::digital::v2::OutputPin;
|
||||
use crate::{
|
||||
ad5680,
|
||||
ad5680, ad7172,
|
||||
channel_state::ChannelState,
|
||||
pins::{ChannelPins, ChannelPinSet},
|
||||
pins::{ChannelPinSet, ChannelPins},
|
||||
};
|
||||
use stm32f4xx_hal::hal::digital::v2::OutputPin;
|
||||
use uom::si::{electric_potential::volt, f64::ElectricPotential};
|
||||
|
||||
/// Marker type for the first channel
|
||||
pub struct Channel0;
|
||||
@ -11,42 +12,47 @@ pub struct Channel0;
|
||||
/// Marker type for the second channel
|
||||
pub struct Channel1;
|
||||
|
||||
|
||||
pub struct Channel<C: ChannelPins> {
|
||||
pub state: ChannelState,
|
||||
/// for `i_set`
|
||||
pub dac: ad5680::Dac<C::DacSpi, C::DacSync>,
|
||||
/// 1 / Volts
|
||||
pub dac_factor: f64,
|
||||
/// Measured vref of MAX driver chip
|
||||
pub vref_meas: ElectricPotential,
|
||||
pub shdn: C::Shdn,
|
||||
/// stm32f4 integrated adc
|
||||
pub adc: C::Adc,
|
||||
pub vref_pin: C::VRefPin,
|
||||
pub itec_pin: C::ItecPin,
|
||||
pub itec_pin: C::ITecPin,
|
||||
/// feedback from `dac` output
|
||||
pub dac_feedback_pin: C::DacFeedbackPin,
|
||||
pub tec_u_meas_pin: C::TecUMeasPin,
|
||||
}
|
||||
|
||||
impl<C: ChannelPins> Channel<C> {
|
||||
pub fn new(mut pins: ChannelPinSet<C>) -> Self {
|
||||
let state = ChannelState::default();
|
||||
pub fn new(pins: ChannelPinSet<C>, adc_calibration: ad7172::ChannelCalibration) -> Self {
|
||||
let state = ChannelState::new(adc_calibration);
|
||||
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;
|
||||
// sensible dummy preset taken from datasheet. calibrate_dac_value() should be used to override this value.
|
||||
let vref_meas = ElectricPotential::new::<volt>(1.5);
|
||||
|
||||
Channel {
|
||||
state,
|
||||
dac, dac_factor,
|
||||
dac,
|
||||
vref_meas,
|
||||
shdn: pins.shdn,
|
||||
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,43 +1,106 @@
|
||||
use smoltcp::time::Instant;
|
||||
use crate::{
|
||||
ad7172,
|
||||
ad7172, b_parameter as bp,
|
||||
command_parser::{CenterPoint, Polarity},
|
||||
config::PwmLimits,
|
||||
pid,
|
||||
steinhart_hart as sh,
|
||||
units::Volts,
|
||||
};
|
||||
use num_traits::Zero;
|
||||
use smoltcp::time::{Duration, Instant};
|
||||
use uom::si::{
|
||||
electric_current::ampere,
|
||||
electric_potential::volt,
|
||||
electrical_resistance::ohm,
|
||||
f64::{
|
||||
ElectricCurrent, ElectricPotential, ElectricalResistance, ThermodynamicTemperature, Time,
|
||||
},
|
||||
thermodynamic_temperature::degree_celsius,
|
||||
time::millisecond,
|
||||
};
|
||||
|
||||
const R_INNER: f64 = 2.0 * 5100.0;
|
||||
const VREF_SENS: f64 = 3.3 / 2.0;
|
||||
|
||||
pub struct ChannelState {
|
||||
pub adc_data: Option<u32>,
|
||||
pub adc_calibration: ad7172::ChannelCalibration,
|
||||
pub adc_time: Instant,
|
||||
pub dac_value: Volts,
|
||||
pub adc_interval: Duration,
|
||||
/// i_set 0A center point
|
||||
pub center: CenterPoint,
|
||||
pub dac_value: ElectricPotential,
|
||||
pub i_set: ElectricCurrent,
|
||||
pub pwm_limits: PwmLimits,
|
||||
pub pid_engaged: bool,
|
||||
pub pid: pid::Controller,
|
||||
pub sh: sh::Parameters,
|
||||
}
|
||||
|
||||
impl Default for ChannelState {
|
||||
fn default() -> Self {
|
||||
ChannelState {
|
||||
adc_data: None,
|
||||
adc_time: Instant::from_secs(0),
|
||||
dac_value: Volts(0.0),
|
||||
pid_engaged: false,
|
||||
pid: pid::Controller::new(pid::Parameters::default()),
|
||||
sh: sh::Parameters::default(),
|
||||
}
|
||||
}
|
||||
pub bp: bp::Parameters,
|
||||
pub polarity: Polarity,
|
||||
}
|
||||
|
||||
impl ChannelState {
|
||||
/// Update PID state on ADC input, calculate new DAC 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 new(adc_calibration: ad7172::ChannelCalibration) -> Self {
|
||||
ChannelState {
|
||||
adc_data: None,
|
||||
adc_calibration,
|
||||
adc_time: Instant::from_secs(0),
|
||||
// default: 10 Hz
|
||||
adc_interval: Duration::from_millis(100),
|
||||
center: CenterPoint::VRef,
|
||||
dac_value: ElectricPotential::new::<volt>(0.0),
|
||||
i_set: ElectricCurrent::new::<ampere>(0.0),
|
||||
pwm_limits: PwmLimits {
|
||||
max_v: ElectricPotential::zero(),
|
||||
max_i_pos: ElectricCurrent::zero(),
|
||||
max_i_neg: ElectricCurrent::zero(),
|
||||
},
|
||||
pid_engaged: false,
|
||||
pid: pid::Controller::new(pid::Parameters::default()),
|
||||
bp: bp::Parameters::default(),
|
||||
polarity: Polarity::Normal,
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
pub fn update(&mut self, now: Instant, adc_data: u32) {
|
||||
self.adc_data = if adc_data == ad7172::MAX_VALUE {
|
||||
// this means there is no thermistor plugged into the ADC.
|
||||
None
|
||||
} else {
|
||||
Some(adc_data)
|
||||
};
|
||||
self.adc_interval = now - self.adc_time;
|
||||
self.adc_time = now;
|
||||
}
|
||||
|
||||
/// 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 get_adc_time(&self) -> Time {
|
||||
Time::new::<millisecond>(self.adc_time.total_millis() as f64)
|
||||
}
|
||||
|
||||
pub fn get_adc_interval(&self) -> Time {
|
||||
Time::new::<millisecond>(self.adc_interval.total_millis() as f64)
|
||||
}
|
||||
|
||||
pub fn get_adc(&self) -> Option<ElectricPotential> {
|
||||
Some(self.adc_calibration.convert_data(self.adc_data?))
|
||||
}
|
||||
|
||||
/// Get `SENS[01]` input resistance
|
||||
pub fn get_sens(&self) -> Option<ElectricalResistance> {
|
||||
let r_inner = ElectricalResistance::new::<ohm>(R_INNER);
|
||||
let vref = ElectricPotential::new::<volt>(VREF_SENS);
|
||||
let adc_input = self.get_adc()?;
|
||||
let r = r_inner * adc_input / (vref - adc_input);
|
||||
Some(r)
|
||||
}
|
||||
|
||||
pub fn get_temperature(&self) -> Option<ThermodynamicTemperature> {
|
||||
let r = self.get_sens()?;
|
||||
let temperature = self.bp.get_temperature(r);
|
||||
Some(temperature)
|
||||
}
|
||||
}
|
||||
|
749
src/channels.rs
749
src/channels.rs
@ -1,53 +1,100 @@
|
||||
use smoltcp::time::Instant;
|
||||
use log::info;
|
||||
use crate::timer::sleep;
|
||||
use crate::{
|
||||
ad5680,
|
||||
ad7172,
|
||||
ad5680, ad7172, b_parameter,
|
||||
channel::{Channel, Channel0, Channel1},
|
||||
channel_state::ChannelState,
|
||||
pins,
|
||||
units::Volts,
|
||||
command_handler::JsonBuffer,
|
||||
command_parser::{CenterPoint, Polarity, PwmPin},
|
||||
pins::{self, Channel0VRef, Channel1VRef},
|
||||
};
|
||||
use core::marker::PhantomData;
|
||||
use heapless::{consts::U2, Vec};
|
||||
use num_traits::Zero;
|
||||
use serde::{Serialize, Serializer};
|
||||
use smoltcp::time::Instant;
|
||||
use stm32f4xx_hal::hal;
|
||||
use uom::si::{
|
||||
electric_current::ampere,
|
||||
electric_potential::{millivolt, volt},
|
||||
electrical_resistance::ohm,
|
||||
f64::{ElectricCurrent, ElectricPotential, ElectricalResistance, Time},
|
||||
ratio::ratio,
|
||||
thermodynamic_temperature::degree_celsius,
|
||||
};
|
||||
|
||||
pub const CHANNELS: usize = 2;
|
||||
pub enum PinsAdcReadTarget {
|
||||
VRef,
|
||||
DacVfb,
|
||||
ITec,
|
||||
VTec,
|
||||
}
|
||||
|
||||
pub const CHANNELS: usize = 2;
|
||||
pub const R_SENSE: f64 = 0.05;
|
||||
|
||||
// From design specs
|
||||
pub const MAX_TEC_I: ElectricCurrent = ElectricCurrent {
|
||||
dimension: PhantomData,
|
||||
units: PhantomData,
|
||||
value: 2.0,
|
||||
};
|
||||
pub const MAX_TEC_V: ElectricPotential = ElectricPotential {
|
||||
dimension: PhantomData,
|
||||
units: PhantomData,
|
||||
value: 4.0,
|
||||
};
|
||||
const MAX_TEC_I_DUTY_TO_CURRENT_RATE: ElectricCurrent = ElectricCurrent {
|
||||
dimension: PhantomData,
|
||||
units: PhantomData,
|
||||
value: 1.0 / (10.0 * R_SENSE / 3.3),
|
||||
};
|
||||
// DAC chip outputs 0-5v, which is then passed through a resistor dividor to provide 0-3v range
|
||||
const DAC_OUT_V_MAX: ElectricPotential = ElectricPotential {
|
||||
dimension: PhantomData,
|
||||
units: PhantomData,
|
||||
value: 3.0,
|
||||
};
|
||||
// TODO: -pub
|
||||
pub struct Channels {
|
||||
channel0: Channel<Channel0>,
|
||||
channel1: Channel<Channel1>,
|
||||
pub adc: ad7172::Adc<pins::AdcSpi, pins::AdcNss>,
|
||||
tec_u_meas_adc: pins::TecUMeasAdc,
|
||||
/// stm32f4 integrated adc
|
||||
pins_adc: pins::PinsAdc,
|
||||
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();
|
||||
adc.setup_channel(1, ad7172::Input::Ain2, ad7172::Input::Ain3).unwrap();
|
||||
adc.setup_channel(0, ad7172::Input::Ain2, ad7172::Input::Ain3)
|
||||
.unwrap();
|
||||
let adc_calibration0 = adc.get_calibration(0).expect("adc_calibration0");
|
||||
adc.setup_channel(1, ad7172::Input::Ain0, ad7172::Input::Ain1)
|
||||
.unwrap();
|
||||
let adc_calibration1 = adc.get_calibration(1).expect("adc_calibration1");
|
||||
adc.start_continuous_conversion().unwrap();
|
||||
|
||||
Channels { channel0, channel1, adc, tec_u_meas_adc, pwm }
|
||||
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.calibrate_dac_value(channel);
|
||||
channels.set_i(channel, ElectricCurrent::new::<ampere>(0.0));
|
||||
}
|
||||
channels
|
||||
}
|
||||
|
||||
pub fn channel_state<I: Into<usize>>(&mut self, channel: I) -> &mut ChannelState {
|
||||
@ -62,192 +109,544 @@ impl Channels {
|
||||
pub fn poll_adc(&mut self, instant: Instant) -> Option<u8> {
|
||||
self.adc.data_ready().unwrap().map(|channel| {
|
||||
let data = self.adc.read_data().unwrap();
|
||||
|
||||
let 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
|
||||
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);
|
||||
}
|
||||
};
|
||||
if let Some(dac_value) = dac_value {
|
||||
// Forward PID output to i_set DAC
|
||||
self.set_dac(channel.into(), Volts(dac_value));
|
||||
None if state.pid_engaged => {
|
||||
self.power_down(channel);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
channel
|
||||
})
|
||||
}
|
||||
|
||||
/// calculate the TEC i_set centerpoint
|
||||
pub fn get_center(&mut self, channel: usize) -> ElectricPotential {
|
||||
match self.channel_state(channel).center {
|
||||
CenterPoint::VRef => self.adc_read(channel, PinsAdcReadTarget::VRef, 8),
|
||||
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,
|
||||
fn get_dac(&mut self, channel: usize) -> ElectricPotential {
|
||||
let voltage = self.channel_state(channel).dac_value;
|
||||
voltage
|
||||
}
|
||||
|
||||
pub fn get_i_set(&mut self, channel: usize) -> ElectricCurrent {
|
||||
let i_set = self.channel_state(channel).i_set;
|
||||
i_set
|
||||
}
|
||||
|
||||
/// i_set DAC
|
||||
fn set_dac(&mut self, channel: usize, voltage: ElectricPotential) -> ElectricPotential {
|
||||
let value = ((voltage / DAC_OUT_V_MAX).get::<ratio>() * (ad5680::MAX_VALUE as f64)) as u32;
|
||||
match channel {
|
||||
0 => self.channel0.dac.set(value).unwrap(),
|
||||
1 => self.channel1.dac.set(value).unwrap(),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let value = (voltage.0 * dac_factor) as u32;
|
||||
self.channel_state(channel).dac_value = voltage;
|
||||
voltage
|
||||
}
|
||||
|
||||
pub fn set_i(&mut self, channel: usize, i_set: ElectricCurrent) -> ElectricCurrent {
|
||||
let i_set = i_set.min(MAX_TEC_I).max(-MAX_TEC_I);
|
||||
self.channel_state(channel).i_set = i_set;
|
||||
let negate = match self.channel_state(channel).polarity {
|
||||
Polarity::Normal => 1.0,
|
||||
Polarity::Reversed => -1.0,
|
||||
};
|
||||
let vref_meas = match channel {
|
||||
0 => self.channel0.vref_meas,
|
||||
1 => self.channel1.vref_meas,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let center_point = vref_meas;
|
||||
let r_sense = ElectricalResistance::new::<ohm>(R_SENSE);
|
||||
let voltage = negate * i_set * 10.0 * r_sense + center_point;
|
||||
let voltage = self.set_dac(channel, voltage);
|
||||
|
||||
negate * (voltage - center_point) / (10.0 * r_sense)
|
||||
}
|
||||
|
||||
/// AN4073: ADC Reading Dispersion can be reduced through Averaging
|
||||
pub fn adc_read(
|
||||
&mut self,
|
||||
channel: usize,
|
||||
adc_read_target: PinsAdcReadTarget,
|
||||
avg_pt: u16,
|
||||
) -> ElectricPotential {
|
||||
let mut sample: u32 = 0;
|
||||
match channel {
|
||||
0 => {
|
||||
self.channel0.dac.set(value).unwrap();
|
||||
self.channel0.state.dac_value = voltage;
|
||||
sample = match adc_read_target {
|
||||
PinsAdcReadTarget::VRef => match &self.channel0.vref_pin {
|
||||
Channel0VRef::Analog(vref_pin) => {
|
||||
for _ in (0..avg_pt).rev() {
|
||||
sample += self.pins_adc.convert(
|
||||
vref_pin,
|
||||
stm32f4xx_hal::adc::config::SampleTime::Cycles_480,
|
||||
) as u32;
|
||||
}
|
||||
sample / avg_pt as u32
|
||||
}
|
||||
Channel0VRef::Disabled(_) => 2048_u32,
|
||||
},
|
||||
PinsAdcReadTarget::DacVfb => {
|
||||
for _ in (0..avg_pt).rev() {
|
||||
sample += self.pins_adc.convert(
|
||||
&self.channel0.dac_feedback_pin,
|
||||
stm32f4xx_hal::adc::config::SampleTime::Cycles_480,
|
||||
) as u32;
|
||||
}
|
||||
sample / avg_pt as u32
|
||||
}
|
||||
PinsAdcReadTarget::ITec => {
|
||||
for _ in (0..avg_pt).rev() {
|
||||
sample += self.pins_adc.convert(
|
||||
&self.channel0.itec_pin,
|
||||
stm32f4xx_hal::adc::config::SampleTime::Cycles_480,
|
||||
) as u32;
|
||||
}
|
||||
sample / avg_pt as u32
|
||||
}
|
||||
PinsAdcReadTarget::VTec => {
|
||||
for _ in (0..avg_pt).rev() {
|
||||
sample += self.pins_adc.convert(
|
||||
&self.channel0.tec_u_meas_pin,
|
||||
stm32f4xx_hal::adc::config::SampleTime::Cycles_480,
|
||||
) as u32;
|
||||
}
|
||||
sample / avg_pt as u32
|
||||
}
|
||||
};
|
||||
let mv = self.pins_adc.sample_to_millivolts(sample as u16);
|
||||
ElectricPotential::new::<millivolt>(mv as f64)
|
||||
}
|
||||
1 => {
|
||||
self.channel1.dac.set(value).unwrap();
|
||||
self.channel1.state.dac_value = voltage;
|
||||
sample = match adc_read_target {
|
||||
PinsAdcReadTarget::VRef => match &self.channel1.vref_pin {
|
||||
Channel1VRef::Analog(vref_pin) => {
|
||||
for _ in (0..avg_pt).rev() {
|
||||
sample += self.pins_adc.convert(
|
||||
vref_pin,
|
||||
stm32f4xx_hal::adc::config::SampleTime::Cycles_480,
|
||||
) as u32;
|
||||
}
|
||||
sample / avg_pt as u32
|
||||
}
|
||||
Channel1VRef::Disabled(_) => 2048_u32,
|
||||
},
|
||||
PinsAdcReadTarget::DacVfb => {
|
||||
for _ in (0..avg_pt).rev() {
|
||||
sample += self.pins_adc.convert(
|
||||
&self.channel1.dac_feedback_pin,
|
||||
stm32f4xx_hal::adc::config::SampleTime::Cycles_480,
|
||||
) as u32;
|
||||
}
|
||||
sample / avg_pt as u32
|
||||
}
|
||||
PinsAdcReadTarget::ITec => {
|
||||
for _ in (0..avg_pt).rev() {
|
||||
sample += self.pins_adc.convert(
|
||||
&self.channel1.itec_pin,
|
||||
stm32f4xx_hal::adc::config::SampleTime::Cycles_480,
|
||||
) as u32;
|
||||
}
|
||||
sample / avg_pt as u32
|
||||
}
|
||||
PinsAdcReadTarget::VTec => {
|
||||
for _ in (0..avg_pt).rev() {
|
||||
sample += self.pins_adc.convert(
|
||||
&self.channel1.tec_u_meas_pin,
|
||||
stm32f4xx_hal::adc::config::SampleTime::Cycles_480,
|
||||
) as u32;
|
||||
}
|
||||
sample / avg_pt as u32
|
||||
}
|
||||
};
|
||||
let mv = self.pins_adc.sample_to_millivolts(sample as u16);
|
||||
ElectricPotential::new::<millivolt>(mv as f64)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_dac_feedback(&mut self, channel: usize) -> Volts {
|
||||
match channel {
|
||||
0 => {
|
||||
let sample = self.channel0.adc.convert(
|
||||
&self.channel0.dac_feedback_pin,
|
||||
stm32f4xx_hal::adc::config::SampleTime::Cycles_480
|
||||
);
|
||||
let mv = self.channel0.adc.sample_to_millivolts(sample);
|
||||
Volts(mv as f64 / 1000.0)
|
||||
}
|
||||
1 => {
|
||||
let sample = self.channel1.adc.convert(
|
||||
&self.channel1.dac_feedback_pin,
|
||||
stm32f4xx_hal::adc::config::SampleTime::Cycles_480
|
||||
);
|
||||
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: f64) -> Volts {
|
||||
let mut prev = self.read_dac_feedback(channel);
|
||||
loop {
|
||||
let current = self.read_dac_feedback(channel);
|
||||
use num_traits::float::Float;
|
||||
if (current - prev).0.abs() < tolerance {
|
||||
return current;
|
||||
}
|
||||
prev = current;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_itec(&mut self, channel: usize) -> Volts {
|
||||
match channel {
|
||||
0 => {
|
||||
let sample = self.channel0.adc.convert(
|
||||
&self.channel0.itec_pin,
|
||||
stm32f4xx_hal::adc::config::SampleTime::Cycles_480
|
||||
);
|
||||
let mv = self.channel0.adc.sample_to_millivolts(sample);
|
||||
Volts(mv as f64 / 1000.0)
|
||||
}
|
||||
1 => {
|
||||
let sample = self.channel1.adc.convert(
|
||||
&self.channel1.itec_pin,
|
||||
stm32f4xx_hal::adc::config::SampleTime::Cycles_480
|
||||
);
|
||||
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) -> Volts {
|
||||
match channel {
|
||||
0 => {
|
||||
let sample = self.channel0.adc.convert(
|
||||
&self.channel0.vref_pin,
|
||||
stm32f4xx_hal::adc::config::SampleTime::Cycles_480
|
||||
);
|
||||
let mv = self.channel0.adc.sample_to_millivolts(sample);
|
||||
Volts(mv as f64 / 1000.0)
|
||||
}
|
||||
1 => {
|
||||
let sample = self.channel1.adc.convert(
|
||||
&self.channel1.vref_pin,
|
||||
stm32f4xx_hal::adc::config::SampleTime::Cycles_480
|
||||
);
|
||||
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) -> Volts {
|
||||
match channel {
|
||||
0 => {
|
||||
let sample = self.tec_u_meas_adc.convert(
|
||||
&self.channel0.tec_u_meas_pin,
|
||||
stm32f4xx_hal::adc::config::SampleTime::Cycles_480
|
||||
);
|
||||
let mv = self.tec_u_meas_adc.sample_to_millivolts(sample);
|
||||
Volts(mv as f64 / 1000.0)
|
||||
}
|
||||
1 => {
|
||||
let sample = self.tec_u_meas_adc.convert(
|
||||
&self.channel1.tec_u_meas_pin,
|
||||
stm32f4xx_hal::adc::config::SampleTime::Cycles_480
|
||||
);
|
||||
let mv = self.tec_u_meas_adc.sample_to_millivolts(sample);
|
||||
Volts(mv as f64 / 1000.0)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
/// for i_set
|
||||
/// Calibrates the DAC output to match vref of the MAX driver to reduce zero-current offset of the MAX driver output.
|
||||
///
|
||||
/// The thermostat DAC applies a control voltage signal to the CTLI pin of MAX driver chip to control its output current.
|
||||
/// The CTLI input signal is centered around VREF of the MAX chip. Applying VREF to CTLI sets the output current to 0.
|
||||
///
|
||||
/// This calibration routine measures the VREF voltage and the DAC output with the STM32 ADC, and uses a breadth-first
|
||||
/// search to find the DAC setting that will produce a DAC output voltage closest to VREF. This DAC output voltage will
|
||||
/// be stored and used in subsequent i_set routines to bias the current control signal to the measured VREF, reducing
|
||||
/// the offset error of the current control signal.
|
||||
///
|
||||
/// The input offset of the STM32 ADC is eliminated by using the same ADC for the measurements, and by only using the
|
||||
/// difference in VREF and DAC output for the calibration.
|
||||
///
|
||||
/// This routine should be called only once after boot, repeated reading of the vref signal and changing of the stored
|
||||
/// VREF measurement can introduce significant noise at the current output, degrading the stabilily performance of the
|
||||
/// thermostat.
|
||||
pub fn calibrate_dac_value(&mut self, channel: usize) {
|
||||
let vref = self.read_vref(channel);
|
||||
let value = self.calibrate_dac_value_for_voltage(channel, vref);
|
||||
info!("best dac value for {}: {}", vref, value);
|
||||
|
||||
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!(),
|
||||
let samples = 50;
|
||||
let mut target_voltage = ElectricPotential::new::<volt>(0.0);
|
||||
for _ in 0..samples {
|
||||
target_voltage += self.get_center(channel);
|
||||
}
|
||||
}
|
||||
target_voltage /= samples as f64;
|
||||
let mut start_value = 1;
|
||||
let mut best_error = ElectricPotential::new::<volt>(100.0);
|
||||
|
||||
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)) {
|
||||
for step in (5..18).rev() {
|
||||
for value in (start_value..=ad5680::MAX_VALUE).step_by(1 << 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!(),
|
||||
}
|
||||
sleep(10);
|
||||
|
||||
let dac_feedback = self.read_dac_feedback_until_stable(channel, 0.001);
|
||||
let error = voltage - dac_feedback;
|
||||
if error < Volts(0.0) {
|
||||
let dac_feedback = self.adc_read(channel, PinsAdcReadTarget::DacVfb, 64);
|
||||
let error = target_voltage - dac_feedback;
|
||||
if error < ElectricPotential::new::<volt>(0.0) {
|
||||
break;
|
||||
} else if error < best_error {
|
||||
best_value = value;
|
||||
best_error = error;
|
||||
start_value = value;
|
||||
|
||||
let vref = (value as f64 / ad5680::MAX_VALUE as f64) * DAC_OUT_V_MAX;
|
||||
match channel {
|
||||
0 => self.channel0.vref_meas = vref,
|
||||
1 => self.channel1.vref_meas = vref,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.set_dac(channel, Volts(0.0));
|
||||
best_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!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_max_v(&mut self, channel: usize) -> ElectricPotential {
|
||||
self.channel_state(channel).pwm_limits.max_v
|
||||
}
|
||||
|
||||
pub fn get_max_i_pos(&mut self, channel: usize) -> ElectricCurrent {
|
||||
self.channel_state(channel).pwm_limits.max_i_pos
|
||||
}
|
||||
|
||||
pub fn get_max_i_neg(&mut self, channel: usize) -> ElectricCurrent {
|
||||
self.channel_state(channel).pwm_limits.max_i_neg
|
||||
}
|
||||
|
||||
// Get current passing through TEC
|
||||
pub fn get_tec_i(&mut self, channel: usize) -> ElectricCurrent {
|
||||
let tec_i = (self.adc_read(channel, PinsAdcReadTarget::ITec, 16)
|
||||
- self.adc_read(channel, PinsAdcReadTarget::VRef, 16))
|
||||
/ ElectricalResistance::new::<ohm>(0.4);
|
||||
match self.channel_state(channel).polarity {
|
||||
Polarity::Normal => tec_i,
|
||||
Polarity::Reversed => -tec_i,
|
||||
}
|
||||
}
|
||||
|
||||
// Get voltage across TEC
|
||||
pub fn get_tec_v(&mut self, channel: usize) -> ElectricPotential {
|
||||
(self.adc_read(channel, PinsAdcReadTarget::VTec, 16) - ElectricPotential::new::<volt>(1.5))
|
||||
* 4.0
|
||||
}
|
||||
|
||||
fn set_pwm(&mut self, channel: usize, pin: PwmPin, duty: f64) -> f64 {
|
||||
fn set<P: hal::PwmPin<Duty = u16>>(pin: &mut P, duty: f64) -> f64 {
|
||||
let max = pin.get_max_duty();
|
||||
let value = ((duty * (max as f64)) as u16).min(max);
|
||||
pin.set_duty(value);
|
||||
value as f64 / (max as f64)
|
||||
}
|
||||
match (channel, pin) {
|
||||
(_, PwmPin::ISet) => panic!("i_set is no pwm pin"),
|
||||
(0, PwmPin::MaxIPos) => set(&mut self.pwm.max_i_pos0, duty),
|
||||
(0, PwmPin::MaxINeg) => set(&mut self.pwm.max_i_neg0, duty),
|
||||
(0, PwmPin::MaxV) => set(&mut self.pwm.max_v0, duty),
|
||||
(1, PwmPin::MaxIPos) => set(&mut self.pwm.max_i_pos1, duty),
|
||||
(1, PwmPin::MaxINeg) => set(&mut self.pwm.max_i_neg1, duty),
|
||||
(1, PwmPin::MaxV) => set(&mut self.pwm.max_v1, duty),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_max_v(
|
||||
&mut self,
|
||||
channel: usize,
|
||||
max_v: ElectricPotential,
|
||||
) -> (ElectricPotential, ElectricPotential) {
|
||||
let max = 4.0 * ElectricPotential::new::<volt>(3.3);
|
||||
let max_v = max_v.min(MAX_TEC_V).max(ElectricPotential::zero());
|
||||
let duty = (max_v / max).get::<ratio>();
|
||||
let duty = self.set_pwm(channel, PwmPin::MaxV, duty);
|
||||
self.channel_state(channel).pwm_limits.max_v = max_v;
|
||||
(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 max_i_pos = max_i_pos.min(MAX_TEC_I).max(ElectricCurrent::zero());
|
||||
let duty = (max_i_pos / MAX_TEC_I_DUTY_TO_CURRENT_RATE).get::<ratio>();
|
||||
let duty = match self.channel_state(channel).polarity {
|
||||
Polarity::Normal => self.set_pwm(channel, PwmPin::MaxIPos, duty),
|
||||
Polarity::Reversed => self.set_pwm(channel, PwmPin::MaxINeg, duty),
|
||||
};
|
||||
self.channel_state(channel).pwm_limits.max_i_pos = max_i_pos;
|
||||
(duty * MAX_TEC_I_DUTY_TO_CURRENT_RATE, max)
|
||||
}
|
||||
|
||||
pub fn set_max_i_neg(
|
||||
&mut self,
|
||||
channel: usize,
|
||||
max_i_neg: ElectricCurrent,
|
||||
) -> (ElectricCurrent, ElectricCurrent) {
|
||||
let max = ElectricCurrent::new::<ampere>(3.0);
|
||||
let max_i_neg = max_i_neg.min(MAX_TEC_I).max(ElectricCurrent::zero());
|
||||
let duty = (max_i_neg / MAX_TEC_I_DUTY_TO_CURRENT_RATE).get::<ratio>();
|
||||
let duty = match self.channel_state(channel).polarity {
|
||||
Polarity::Normal => self.set_pwm(channel, PwmPin::MaxINeg, duty),
|
||||
Polarity::Reversed => self.set_pwm(channel, PwmPin::MaxIPos, duty),
|
||||
};
|
||||
self.channel_state(channel).pwm_limits.max_i_neg = max_i_neg;
|
||||
(duty * MAX_TEC_I_DUTY_TO_CURRENT_RATE, max)
|
||||
}
|
||||
|
||||
pub fn set_polarity(&mut self, channel: usize, polarity: Polarity) {
|
||||
if self.channel_state(channel).polarity != polarity {
|
||||
let i_set = self.channel_state(channel).i_set;
|
||||
let max_i_pos = self.get_max_i_pos(channel);
|
||||
let max_i_neg = self.get_max_i_neg(channel);
|
||||
self.channel_state(channel).polarity = polarity;
|
||||
|
||||
self.set_i(channel, i_set);
|
||||
self.set_max_i_pos(channel, max_i_pos);
|
||||
self.set_max_i_neg(channel, max_i_neg);
|
||||
}
|
||||
}
|
||||
|
||||
fn report(&mut self, channel: usize) -> Report {
|
||||
let i_set = self.get_i_set(channel);
|
||||
let i_tec = self.adc_read(channel, PinsAdcReadTarget::ITec, 16);
|
||||
let tec_i = self.get_tec_i(channel);
|
||||
let dac_value = self.get_dac(channel);
|
||||
let state = self.channel_state(channel);
|
||||
let pid_output = ElectricCurrent::new::<ampere>(state.pid.y1);
|
||||
Report {
|
||||
channel,
|
||||
time: state.get_adc_time(),
|
||||
interval: state.get_adc_interval(),
|
||||
adc: state.get_adc(),
|
||||
sens: state.get_sens(),
|
||||
temperature: state
|
||||
.get_temperature()
|
||||
.map(|temperature| temperature.get::<degree_celsius>()),
|
||||
pid_engaged: state.pid_engaged,
|
||||
i_set,
|
||||
dac_value,
|
||||
dac_feedback: self.adc_read(channel, PinsAdcReadTarget::DacVfb, 1),
|
||||
i_tec,
|
||||
tec_i,
|
||||
tec_u_meas: self.get_tec_v(channel),
|
||||
pid_output,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reports_json(&mut self) -> Result<JsonBuffer, serde_json_core::ser::Error> {
|
||||
let mut reports = Vec::<_, U2>::new();
|
||||
for channel in 0..CHANNELS {
|
||||
let _ = reports.push(self.report(channel));
|
||||
}
|
||||
serde_json_core::to_vec(&reports)
|
||||
}
|
||||
|
||||
pub fn pid_summaries_json(&mut self) -> Result<JsonBuffer, serde_json_core::ser::Error> {
|
||||
let mut summaries = Vec::<_, U2>::new();
|
||||
for channel in 0..CHANNELS {
|
||||
let _ = summaries.push(self.channel_state(channel).pid.summary(channel));
|
||||
}
|
||||
serde_json_core::to_vec(&summaries)
|
||||
}
|
||||
|
||||
pub fn pid_engaged(&mut self) -> bool {
|
||||
for channel in 0..CHANNELS {
|
||||
if self.channel_state(channel).pid_engaged {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn output_summary(&mut self, channel: usize) -> OutputSummary {
|
||||
OutputSummary {
|
||||
channel,
|
||||
center: CenterPointJson(self.channel_state(channel).center.clone()),
|
||||
i_set: self.get_i_set(channel),
|
||||
max_v: self.get_max_v(channel),
|
||||
max_i_pos: self.get_max_i_pos(channel),
|
||||
max_i_neg: self.get_max_i_neg(channel),
|
||||
polarity: PolarityJson(self.channel_state(channel).polarity.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn output_summaries_json(&mut self) -> Result<JsonBuffer, serde_json_core::ser::Error> {
|
||||
let mut summaries = Vec::<_, U2>::new();
|
||||
for channel in 0..CHANNELS {
|
||||
let _ = summaries.push(self.output_summary(channel));
|
||||
}
|
||||
serde_json_core::to_vec(&summaries)
|
||||
}
|
||||
|
||||
fn postfilter_summary(&mut self, channel: usize) -> PostFilterSummary {
|
||||
let rate = self
|
||||
.adc
|
||||
.get_postfilter(channel as u8)
|
||||
.unwrap()
|
||||
.and_then(|filter| filter.output_rate());
|
||||
PostFilterSummary { channel, rate }
|
||||
}
|
||||
|
||||
pub fn postfilter_summaries_json(&mut self) -> Result<JsonBuffer, serde_json_core::ser::Error> {
|
||||
let mut summaries = Vec::<_, U2>::new();
|
||||
for channel in 0..CHANNELS {
|
||||
let _ = summaries.push(self.postfilter_summary(channel));
|
||||
}
|
||||
serde_json_core::to_vec(&summaries)
|
||||
}
|
||||
|
||||
fn b_parameter_summary(&mut self, channel: usize) -> BParameterSummary {
|
||||
let params = self.channel_state(channel).bp.clone();
|
||||
BParameterSummary { channel, params }
|
||||
}
|
||||
|
||||
pub fn b_parameter_summaries_json(
|
||||
&mut self,
|
||||
) -> Result<JsonBuffer, serde_json_core::ser::Error> {
|
||||
let mut summaries = Vec::<_, U2>::new();
|
||||
for channel in 0..CHANNELS {
|
||||
let _ = summaries.push(self.b_parameter_summary(channel));
|
||||
}
|
||||
serde_json_core::to_vec(&summaries)
|
||||
}
|
||||
|
||||
pub fn current_abs_max_tec_i(&mut self) -> ElectricCurrent {
|
||||
(0..CHANNELS)
|
||||
.map(|channel| self.get_tec_i(channel).abs())
|
||||
.max_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal))
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct Report {
|
||||
channel: usize,
|
||||
time: Time,
|
||||
interval: Time,
|
||||
adc: Option<ElectricPotential>,
|
||||
sens: Option<ElectricalResistance>,
|
||||
temperature: Option<f64>,
|
||||
pid_engaged: bool,
|
||||
i_set: ElectricCurrent,
|
||||
dac_value: ElectricPotential,
|
||||
dac_feedback: ElectricPotential,
|
||||
i_tec: ElectricPotential,
|
||||
tec_i: ElectricCurrent,
|
||||
tec_u_meas: ElectricPotential,
|
||||
pid_output: ElectricCurrent,
|
||||
}
|
||||
|
||||
pub struct CenterPointJson(CenterPoint);
|
||||
|
||||
// used in JSON encoding, not for config
|
||||
impl Serialize for CenterPointJson {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
match self.0 {
|
||||
CenterPoint::VRef => serializer.serialize_str("vref"),
|
||||
CenterPoint::Override(vref) => serializer.serialize_f32(vref),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PolarityJson(Polarity);
|
||||
|
||||
// used in JSON encoding, not for config
|
||||
impl Serialize for PolarityJson {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(match self.0 {
|
||||
Polarity::Normal => "normal",
|
||||
Polarity::Reversed => "reversed",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct OutputSummary {
|
||||
channel: usize,
|
||||
center: CenterPointJson,
|
||||
i_set: ElectricCurrent,
|
||||
max_v: ElectricPotential,
|
||||
max_i_pos: ElectricCurrent,
|
||||
max_i_neg: ElectricCurrent,
|
||||
polarity: PolarityJson,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct PostFilterSummary {
|
||||
channel: usize,
|
||||
rate: Option<f32>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct BParameterSummary {
|
||||
channel: usize,
|
||||
params: b_parameter::Parameters,
|
||||
}
|
||||
|
528
src/command_handler.rs
Normal file
528
src/command_handler.rs
Normal file
@ -0,0 +1,528 @@
|
||||
use super::{
|
||||
ad7172,
|
||||
channels::{Channels, CHANNELS},
|
||||
command_parser::{
|
||||
BpParameter, CenterPoint, Command, Ipv4Config, PidParameter, Polarity, PwmPin, ShowCommand,
|
||||
},
|
||||
config::ChannelConfig,
|
||||
dfu,
|
||||
flash_store::FlashStore,
|
||||
hw_rev::HWRev,
|
||||
net, FanCtrl, CHANNEL_CONFIG_KEY,
|
||||
};
|
||||
use core::fmt::Write;
|
||||
use heapless::{consts::U1024, Vec};
|
||||
use log::{error, warn};
|
||||
use smoltcp::socket::TcpSocket;
|
||||
|
||||
use uom::si::{
|
||||
electric_current::ampere,
|
||||
electric_potential::volt,
|
||||
electrical_resistance::ohm,
|
||||
f64::{
|
||||
ElectricCurrent, ElectricPotential, ElectricalResistance, TemperatureInterval,
|
||||
ThermodynamicTemperature,
|
||||
},
|
||||
temperature_interval::kelvin,
|
||||
thermodynamic_temperature::degree_celsius,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Handler {
|
||||
Handled,
|
||||
CloseSocket,
|
||||
NewIPV4(Ipv4Config),
|
||||
Reset,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Error {
|
||||
Report,
|
||||
PostFilterRate,
|
||||
Flash,
|
||||
}
|
||||
|
||||
pub type JsonBuffer = Vec<u8, U1024>;
|
||||
|
||||
fn send_line(socket: &mut TcpSocket, data: &[u8]) -> bool {
|
||||
let send_free = socket.send_capacity() - socket.send_queue();
|
||||
if data.len() > send_free + 1 {
|
||||
// Not enough buffer space, skip report for now,
|
||||
// instead of sending incomplete line
|
||||
warn!(
|
||||
"TCP socket has only {}/{} needed {}",
|
||||
send_free + 1,
|
||||
socket.send_capacity(),
|
||||
data.len(),
|
||||
);
|
||||
} else {
|
||||
match socket.send_slice(data) {
|
||||
Ok(sent) if sent == data.len() => {
|
||||
let _ = socket.send_slice(b"\n");
|
||||
// success
|
||||
return true;
|
||||
}
|
||||
Ok(sent) => warn!("sent only {}/{} bytes", sent, data.len()),
|
||||
Err(e) => error!("error sending line: {:?}", e),
|
||||
}
|
||||
}
|
||||
// not success
|
||||
false
|
||||
}
|
||||
|
||||
impl Handler {
|
||||
fn show_report(socket: &mut TcpSocket, channels: &mut Channels) -> Result<Handler, Error> {
|
||||
match channels.reports_json() {
|
||||
Ok(buf) => {
|
||||
send_line(socket, &buf[..]);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("unable to serialize report: {:?}", e);
|
||||
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
|
||||
return Err(Error::Report);
|
||||
}
|
||||
}
|
||||
Ok(Handler::Handled)
|
||||
}
|
||||
|
||||
fn show_pid(socket: &mut TcpSocket, channels: &mut Channels) -> Result<Handler, Error> {
|
||||
match channels.pid_summaries_json() {
|
||||
Ok(buf) => {
|
||||
send_line(socket, &buf);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("unable to serialize pid summary: {:?}", e);
|
||||
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
|
||||
return Err(Error::Report);
|
||||
}
|
||||
}
|
||||
Ok(Handler::Handled)
|
||||
}
|
||||
|
||||
fn show_pwm(socket: &mut TcpSocket, channels: &mut Channels) -> Result<Handler, Error> {
|
||||
match channels.output_summaries_json() {
|
||||
Ok(buf) => {
|
||||
send_line(socket, &buf);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("unable to serialize pwm summary: {:?}", e);
|
||||
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
|
||||
return Err(Error::Report);
|
||||
}
|
||||
}
|
||||
Ok(Handler::Handled)
|
||||
}
|
||||
|
||||
fn show_b_parameter(socket: &mut TcpSocket, channels: &mut Channels) -> Result<Handler, Error> {
|
||||
match channels.b_parameter_summaries_json() {
|
||||
Ok(buf) => {
|
||||
send_line(socket, &buf);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("unable to serialize b parameter summaries: {:?}", e);
|
||||
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
|
||||
return Err(Error::Report);
|
||||
}
|
||||
}
|
||||
Ok(Handler::Handled)
|
||||
}
|
||||
|
||||
fn show_post_filter(socket: &mut TcpSocket, channels: &mut Channels) -> Result<Handler, Error> {
|
||||
match channels.postfilter_summaries_json() {
|
||||
Ok(buf) => {
|
||||
send_line(socket, &buf);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("unable to serialize postfilter summary: {:?}", e);
|
||||
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
|
||||
return Err(Error::Report);
|
||||
}
|
||||
}
|
||||
Ok(Handler::Handled)
|
||||
}
|
||||
|
||||
fn show_ipv4(socket: &mut TcpSocket, ipv4_config: &mut Ipv4Config) -> Result<Handler, Error> {
|
||||
let (cidr, gateway) = net::split_ipv4_config(ipv4_config.clone());
|
||||
let _ = write!(socket, "{{\"addr\":\"{}\"", cidr);
|
||||
gateway.map(|gateway| write!(socket, ",\"gateway\":\"{}\"", gateway));
|
||||
let _ = writeln!(socket, "}}");
|
||||
Ok(Handler::Handled)
|
||||
}
|
||||
|
||||
fn engage_pid(
|
||||
socket: &mut TcpSocket,
|
||||
channels: &mut Channels,
|
||||
channel: usize,
|
||||
) -> Result<Handler, Error> {
|
||||
channels.channel_state(channel).pid_engaged = true;
|
||||
send_line(socket, b"{}");
|
||||
Ok(Handler::Handled)
|
||||
}
|
||||
|
||||
fn set_polarity(
|
||||
socket: &mut TcpSocket,
|
||||
channels: &mut Channels,
|
||||
channel: usize,
|
||||
polarity: Polarity,
|
||||
) -> Result<Handler, Error> {
|
||||
channels.set_polarity(channel, polarity);
|
||||
send_line(socket, b"{}");
|
||||
Ok(Handler::Handled)
|
||||
}
|
||||
|
||||
fn set_pwm(
|
||||
socket: &mut TcpSocket,
|
||||
channels: &mut Channels,
|
||||
channel: usize,
|
||||
pin: PwmPin,
|
||||
value: f64,
|
||||
) -> Result<Handler, Error> {
|
||||
match pin {
|
||||
PwmPin::ISet => {
|
||||
channels.channel_state(channel).pid_engaged = false;
|
||||
let current = ElectricCurrent::new::<ampere>(value);
|
||||
channels.set_i(channel, current);
|
||||
channels.power_up(channel);
|
||||
}
|
||||
PwmPin::MaxV => {
|
||||
let voltage = ElectricPotential::new::<volt>(value);
|
||||
channels.set_max_v(channel, voltage);
|
||||
}
|
||||
PwmPin::MaxIPos => {
|
||||
let current = ElectricCurrent::new::<ampere>(value);
|
||||
channels.set_max_i_pos(channel, current);
|
||||
}
|
||||
PwmPin::MaxINeg => {
|
||||
let current = ElectricCurrent::new::<ampere>(value);
|
||||
channels.set_max_i_neg(channel, current);
|
||||
}
|
||||
}
|
||||
send_line(socket, b"{}");
|
||||
Ok(Handler::Handled)
|
||||
}
|
||||
|
||||
fn set_center_point(
|
||||
socket: &mut TcpSocket,
|
||||
channels: &mut Channels,
|
||||
channel: usize,
|
||||
center: CenterPoint,
|
||||
) -> Result<Handler, Error> {
|
||||
let i_set = channels.get_i_set(channel);
|
||||
let state = channels.channel_state(channel);
|
||||
state.center = center;
|
||||
if !state.pid_engaged {
|
||||
channels.set_i(channel, i_set);
|
||||
}
|
||||
send_line(socket, b"{}");
|
||||
Ok(Handler::Handled)
|
||||
}
|
||||
|
||||
fn set_pid(
|
||||
socket: &mut TcpSocket,
|
||||
channels: &mut Channels,
|
||||
channel: usize,
|
||||
parameter: PidParameter,
|
||||
value: f64,
|
||||
) -> Result<Handler, Error> {
|
||||
let pid = &mut channels.channel_state(channel).pid;
|
||||
use super::command_parser::PidParameter::*;
|
||||
match parameter {
|
||||
Target => pid.target = value,
|
||||
KP => pid.parameters.kp = value as f32,
|
||||
KI => pid.update_ki(value as f32),
|
||||
KD => pid.parameters.kd = value as f32,
|
||||
OutputMin => pid.parameters.output_min = value as f32,
|
||||
OutputMax => pid.parameters.output_max = value as f32,
|
||||
}
|
||||
send_line(socket, b"{}");
|
||||
Ok(Handler::Handled)
|
||||
}
|
||||
|
||||
fn set_b_parameter(
|
||||
socket: &mut TcpSocket,
|
||||
channels: &mut Channels,
|
||||
channel: usize,
|
||||
parameter: BpParameter,
|
||||
value: f64,
|
||||
) -> Result<Handler, Error> {
|
||||
let bp = &mut channels.channel_state(channel).bp;
|
||||
use super::command_parser::BpParameter::*;
|
||||
match parameter {
|
||||
T0 => bp.t0 = ThermodynamicTemperature::new::<degree_celsius>(value),
|
||||
B => bp.b = TemperatureInterval::new::<kelvin>(value),
|
||||
R0 => bp.r0 = ElectricalResistance::new::<ohm>(value),
|
||||
}
|
||||
send_line(socket, b"{}");
|
||||
Ok(Handler::Handled)
|
||||
}
|
||||
|
||||
fn reset_post_filter(
|
||||
socket: &mut TcpSocket,
|
||||
channels: &mut Channels,
|
||||
channel: usize,
|
||||
) -> Result<Handler, Error> {
|
||||
channels.adc.set_postfilter(channel as u8, None).unwrap();
|
||||
send_line(socket, b"{}");
|
||||
Ok(Handler::Handled)
|
||||
}
|
||||
|
||||
fn set_post_filter(
|
||||
socket: &mut TcpSocket,
|
||||
channels: &mut Channels,
|
||||
channel: usize,
|
||||
rate: f32,
|
||||
) -> Result<Handler, Error> {
|
||||
let filter = ad7172::PostFilter::closest(rate);
|
||||
match filter {
|
||||
Some(filter) => {
|
||||
channels
|
||||
.adc
|
||||
.set_postfilter(channel as u8, Some(filter))
|
||||
.unwrap();
|
||||
send_line(socket, b"{}");
|
||||
}
|
||||
None => {
|
||||
error!("unable to choose postfilter for rate {:.3}", rate);
|
||||
send_line(
|
||||
socket,
|
||||
b"{{\"error\": \"unable to choose postfilter rate\"}}",
|
||||
);
|
||||
return Err(Error::PostFilterRate);
|
||||
}
|
||||
}
|
||||
Ok(Handler::Handled)
|
||||
}
|
||||
|
||||
fn load_channel(
|
||||
socket: &mut TcpSocket,
|
||||
channels: &mut Channels,
|
||||
store: &mut FlashStore,
|
||||
channel: Option<usize>,
|
||||
) -> Result<Handler, Error> {
|
||||
for (c, key) in CHANNEL_CONFIG_KEY.iter().enumerate().take(CHANNELS) {
|
||||
if channel.is_none() || channel == Some(c) {
|
||||
match store.read_value::<ChannelConfig>(key) {
|
||||
Ok(Some(config)) => {
|
||||
config.apply(channels, c);
|
||||
send_line(socket, b"{}");
|
||||
}
|
||||
Ok(None) => {
|
||||
error!("flash config not found");
|
||||
send_line(socket, b"{{\"error\": \"flash config not found\"}}");
|
||||
}
|
||||
Err(e) => {
|
||||
error!("unable to load config from flash: {:?}", e);
|
||||
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
|
||||
return Err(Error::Flash);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Handler::Handled)
|
||||
}
|
||||
|
||||
fn save_channel(
|
||||
socket: &mut TcpSocket,
|
||||
channels: &mut Channels,
|
||||
channel: Option<usize>,
|
||||
store: &mut FlashStore,
|
||||
) -> Result<Handler, Error> {
|
||||
for (c, key) in CHANNEL_CONFIG_KEY.iter().enumerate().take(CHANNELS) {
|
||||
let mut store_value_buf = [0u8; 256];
|
||||
if channel.is_none() || channel == Some(c) {
|
||||
let config = ChannelConfig::new(channels, c);
|
||||
match store.write_value(key, &config, &mut store_value_buf) {
|
||||
Ok(()) => {
|
||||
send_line(socket, b"{}");
|
||||
}
|
||||
Err(e) => {
|
||||
error!("unable to save channel {} config to flash: {:?}", c, e);
|
||||
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
|
||||
return Err(Error::Flash);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Handler::Handled)
|
||||
}
|
||||
|
||||
fn set_ipv4(
|
||||
socket: &mut TcpSocket,
|
||||
store: &mut FlashStore,
|
||||
config: Ipv4Config,
|
||||
) -> Result<Handler, Error> {
|
||||
let _ = store
|
||||
.write_value("ipv4", &config, [0; 16])
|
||||
.map_err(|e| error!("unable to save ipv4 config to flash: {:?}", e));
|
||||
let new_ipv4_config = Some(config);
|
||||
send_line(socket, b"{}");
|
||||
Ok(Handler::NewIPV4(new_ipv4_config.unwrap()))
|
||||
}
|
||||
|
||||
fn reset(channels: &mut Channels) -> Result<Handler, Error> {
|
||||
for i in 0..CHANNELS {
|
||||
channels.power_down(i);
|
||||
}
|
||||
// should_reset = true;
|
||||
Ok(Handler::Reset)
|
||||
}
|
||||
|
||||
fn dfu(channels: &mut Channels) -> Result<Handler, Error> {
|
||||
for i in 0..CHANNELS {
|
||||
channels.power_down(i);
|
||||
}
|
||||
unsafe {
|
||||
dfu::set_dfu_trigger();
|
||||
}
|
||||
// should_reset = true;
|
||||
Ok(Handler::Reset)
|
||||
}
|
||||
|
||||
fn set_fan(
|
||||
socket: &mut TcpSocket,
|
||||
fan_pwm: u32,
|
||||
fan_ctrl: &mut FanCtrl,
|
||||
) -> Result<Handler, Error> {
|
||||
if !fan_ctrl.fan_available() {
|
||||
send_line(
|
||||
socket,
|
||||
b"{ \"warning\": \"this thermostat doesn't have a fan!\" }",
|
||||
);
|
||||
return Ok(Handler::Handled);
|
||||
}
|
||||
fan_ctrl.set_auto_mode(false);
|
||||
fan_ctrl.set_pwm(fan_pwm);
|
||||
if fan_ctrl.fan_pwm_recommended() {
|
||||
send_line(socket, b"{}");
|
||||
} else {
|
||||
send_line(socket, b"{ \"warning\": \"this fan doesn't have full PWM support. Use it at your own risk!\" }");
|
||||
}
|
||||
Ok(Handler::Handled)
|
||||
}
|
||||
|
||||
fn show_fan(socket: &mut TcpSocket, fan_ctrl: &mut FanCtrl) -> Result<Handler, Error> {
|
||||
match fan_ctrl.summary() {
|
||||
Ok(buf) => {
|
||||
send_line(socket, &buf);
|
||||
Ok(Handler::Handled)
|
||||
}
|
||||
Err(e) => {
|
||||
error!("unable to serialize fan summary: {:?}", e);
|
||||
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
|
||||
Err(Error::Report)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn fan_auto(socket: &mut TcpSocket, fan_ctrl: &mut FanCtrl) -> Result<Handler, Error> {
|
||||
if !fan_ctrl.fan_available() {
|
||||
send_line(
|
||||
socket,
|
||||
b"{ \"warning\": \"this thermostat doesn't have a fan!\" }",
|
||||
);
|
||||
return Ok(Handler::Handled);
|
||||
}
|
||||
fan_ctrl.set_auto_mode(true);
|
||||
if fan_ctrl.fan_pwm_recommended() {
|
||||
send_line(socket, b"{}");
|
||||
} else {
|
||||
send_line(socket, b"{ \"warning\": \"this fan doesn't have full PWM support. Use it at your own risk!\" }");
|
||||
}
|
||||
Ok(Handler::Handled)
|
||||
}
|
||||
|
||||
fn fan_curve(
|
||||
socket: &mut TcpSocket,
|
||||
fan_ctrl: &mut FanCtrl,
|
||||
k_a: f32,
|
||||
k_b: f32,
|
||||
k_c: f32,
|
||||
) -> Result<Handler, Error> {
|
||||
fan_ctrl.set_curve(k_a, k_b, k_c);
|
||||
send_line(socket, b"{}");
|
||||
Ok(Handler::Handled)
|
||||
}
|
||||
|
||||
fn fan_defaults(socket: &mut TcpSocket, fan_ctrl: &mut FanCtrl) -> Result<Handler, Error> {
|
||||
fan_ctrl.restore_defaults();
|
||||
send_line(socket, b"{}");
|
||||
Ok(Handler::Handled)
|
||||
}
|
||||
|
||||
fn show_hwrev(socket: &mut TcpSocket, hwrev: HWRev) -> Result<Handler, Error> {
|
||||
match hwrev.summary() {
|
||||
Ok(buf) => {
|
||||
send_line(socket, &buf);
|
||||
Ok(Handler::Handled)
|
||||
}
|
||||
Err(e) => {
|
||||
error!("unable to serialize HWRev summary: {:?}", e);
|
||||
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
|
||||
Err(Error::Report)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_command(
|
||||
command: Command,
|
||||
socket: &mut TcpSocket,
|
||||
channels: &mut Channels,
|
||||
store: &mut FlashStore,
|
||||
ipv4_config: &mut Ipv4Config,
|
||||
fan_ctrl: &mut FanCtrl,
|
||||
hwrev: HWRev,
|
||||
) -> Result<Self, Error> {
|
||||
match command {
|
||||
Command::Quit => Ok(Handler::CloseSocket),
|
||||
Command::Show(ShowCommand::Input) => Handler::show_report(socket, channels),
|
||||
Command::Show(ShowCommand::Pid) => Handler::show_pid(socket, channels),
|
||||
Command::Show(ShowCommand::Output) => Handler::show_pwm(socket, channels),
|
||||
Command::Show(ShowCommand::BParameter) => Handler::show_b_parameter(socket, channels),
|
||||
Command::Show(ShowCommand::PostFilter) => Handler::show_post_filter(socket, channels),
|
||||
Command::Show(ShowCommand::Ipv4) => Handler::show_ipv4(socket, ipv4_config),
|
||||
Command::OutputPid { channel } => Handler::engage_pid(socket, channels, channel),
|
||||
Command::OutputPolarity { channel, polarity } => {
|
||||
Handler::set_polarity(socket, channels, channel, polarity)
|
||||
}
|
||||
Command::Output {
|
||||
channel,
|
||||
pin,
|
||||
value,
|
||||
} => Handler::set_pwm(socket, channels, channel, pin, value),
|
||||
Command::CenterPoint { channel, center } => {
|
||||
Handler::set_center_point(socket, channels, channel, center)
|
||||
}
|
||||
Command::Pid {
|
||||
channel,
|
||||
parameter,
|
||||
value,
|
||||
} => Handler::set_pid(socket, channels, channel, parameter, value),
|
||||
Command::BParameter {
|
||||
channel,
|
||||
parameter,
|
||||
value,
|
||||
} => Handler::set_b_parameter(socket, channels, channel, parameter, value),
|
||||
Command::PostFilter {
|
||||
channel,
|
||||
rate: None,
|
||||
} => Handler::reset_post_filter(socket, channels, channel),
|
||||
Command::PostFilter {
|
||||
channel,
|
||||
rate: Some(rate),
|
||||
} => Handler::set_post_filter(socket, channels, channel, rate),
|
||||
Command::Load { channel } => Handler::load_channel(socket, channels, store, channel),
|
||||
Command::Save { channel } => Handler::save_channel(socket, channels, channel, store),
|
||||
Command::Ipv4(config) => Handler::set_ipv4(socket, store, config),
|
||||
Command::Reset => Handler::reset(channels),
|
||||
Command::Dfu => Handler::dfu(channels),
|
||||
Command::FanSet { fan_pwm } => Handler::set_fan(socket, fan_pwm, fan_ctrl),
|
||||
Command::ShowFan => Handler::show_fan(socket, fan_ctrl),
|
||||
Command::FanAuto => Handler::fan_auto(socket, fan_ctrl),
|
||||
Command::FanCurve { k_a, k_b, k_c } => {
|
||||
Handler::fan_curve(socket, fan_ctrl, k_a, k_b, k_c)
|
||||
}
|
||||
Command::FanCurveDefaults => Handler::fan_defaults(socket, fan_ctrl),
|
||||
Command::ShowHWRev => Handler::show_hwrev(socket, hwrev),
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
99
src/config.rs
Normal file
99
src/config.rs
Normal file
@ -0,0 +1,99 @@
|
||||
use crate::{
|
||||
ad7172::PostFilter,
|
||||
b_parameter,
|
||||
channels::Channels,
|
||||
command_parser::{CenterPoint, Polarity},
|
||||
pid,
|
||||
};
|
||||
use num_traits::Zero;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uom::si::f64::{ElectricCurrent, ElectricPotential};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ChannelConfig {
|
||||
center: CenterPoint,
|
||||
pid: pid::Parameters,
|
||||
pid_target: f32,
|
||||
pid_engaged: bool,
|
||||
i_set: ElectricCurrent,
|
||||
polarity: Polarity,
|
||||
bp: b_parameter::Parameters,
|
||||
pwm: PwmLimits,
|
||||
/// uses variant `PostFilter::Invalid` instead of `None` to save space
|
||||
adc_postfilter: PostFilter,
|
||||
}
|
||||
|
||||
impl ChannelConfig {
|
||||
pub fn new(channels: &mut Channels, channel: usize) -> Self {
|
||||
let pwm = PwmLimits::new(channels, channel);
|
||||
|
||||
let adc_postfilter = channels
|
||||
.adc
|
||||
.get_postfilter(channel as u8)
|
||||
.unwrap()
|
||||
.unwrap_or(PostFilter::Invalid);
|
||||
|
||||
let state = channels.channel_state(channel);
|
||||
let i_set = if state.pid_engaged {
|
||||
ElectricCurrent::zero()
|
||||
} else {
|
||||
state.i_set
|
||||
};
|
||||
ChannelConfig {
|
||||
center: state.center.clone(),
|
||||
pid: state.pid.parameters.clone(),
|
||||
pid_target: state.pid.target as f32,
|
||||
pid_engaged: state.pid_engaged,
|
||||
i_set,
|
||||
polarity: state.polarity.clone(),
|
||||
bp: state.bp.clone(),
|
||||
pwm,
|
||||
adc_postfilter,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply(&self, channels: &mut Channels, channel: usize) {
|
||||
let state = channels.channel_state(channel);
|
||||
state.center = self.center.clone();
|
||||
state.pid.parameters = self.pid.clone();
|
||||
state.pid.target = self.pid_target.into();
|
||||
state.pid_engaged = self.pid_engaged;
|
||||
state.bp = self.bp.clone();
|
||||
|
||||
self.pwm.apply(channels, channel);
|
||||
|
||||
let adc_postfilter = match self.adc_postfilter {
|
||||
PostFilter::Invalid => None,
|
||||
adc_postfilter => Some(adc_postfilter),
|
||||
};
|
||||
let _ = channels.adc.set_postfilter(channel as u8, adc_postfilter);
|
||||
let _ = channels.set_i(channel, self.i_set);
|
||||
channels.set_polarity(channel, self.polarity.clone());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct PwmLimits {
|
||||
pub max_v: ElectricPotential,
|
||||
pub max_i_pos: ElectricCurrent,
|
||||
pub max_i_neg: ElectricCurrent,
|
||||
}
|
||||
|
||||
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_i_pos,
|
||||
max_i_neg,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply(&self, channels: &mut Channels, channel: usize) {
|
||||
channels.set_max_v(channel, self.max_v);
|
||||
channels.set_max_i_pos(channel, self.max_i_pos);
|
||||
channels.set_max_i_neg(channel, self.max_i_neg);
|
||||
}
|
||||
}
|
46
src/dfu.rs
Normal file
46
src/dfu.rs
Normal file
@ -0,0 +1,46 @@
|
||||
use core::arch::asm;
|
||||
use cortex_m_rt::pre_init;
|
||||
use stm32f4xx_hal::stm32::{RCC, SYSCFG};
|
||||
|
||||
const DFU_TRIG_MSG: u32 = 0xDECAFBAD;
|
||||
|
||||
extern "C" {
|
||||
// This symbol comes from memory.x
|
||||
static mut _dfu_msg: u32;
|
||||
}
|
||||
|
||||
pub unsafe fn set_dfu_trigger() {
|
||||
_dfu_msg = DFU_TRIG_MSG;
|
||||
}
|
||||
|
||||
/// Called by reset handler in lib.rs immediately after reset.
|
||||
/// This function should not be called outside of reset handler as
|
||||
/// bootloader expects MCU to be in reset state when called.
|
||||
#[cfg(target_arch = "arm")]
|
||||
#[pre_init]
|
||||
unsafe fn __pre_init() {
|
||||
if _dfu_msg == DFU_TRIG_MSG {
|
||||
_dfu_msg = 0x00000000;
|
||||
|
||||
// Enable system config controller clock
|
||||
let rcc = &*RCC::ptr();
|
||||
rcc.apb2enr.modify(|_, w| w.syscfgen().set_bit());
|
||||
|
||||
// Bypass BOOT pins and remap bootloader to 0x00000000
|
||||
let syscfg = &*SYSCFG::ptr();
|
||||
syscfg.memrm.write(|w| w.mem_mode().bits(0b01));
|
||||
|
||||
// Impose instruction and memory barriers
|
||||
cortex_m::asm::isb();
|
||||
cortex_m::asm::dsb();
|
||||
|
||||
asm!(
|
||||
// Set stack pointer to bootloader location
|
||||
"LDR R0, =0x1FFF0000",
|
||||
"LDR SP,[R0, #0]",
|
||||
// Jump to bootloader
|
||||
"LDR R0,[R0, #4]",
|
||||
"BX R0",
|
||||
);
|
||||
}
|
||||
}
|
162
src/fan_ctrl.rs
Normal file
162
src/fan_ctrl.rs
Normal file
@ -0,0 +1,162 @@
|
||||
use crate::{channels::MAX_TEC_I, command_handler::JsonBuffer, hw_rev::HWSettings};
|
||||
use num_traits::Float;
|
||||
use serde::Serialize;
|
||||
use stm32f4xx_hal::{
|
||||
pac::TIM8,
|
||||
pwm::{self, PwmChannels},
|
||||
};
|
||||
use uom::si::{electric_current::ampere, f64::ElectricCurrent};
|
||||
|
||||
pub type FanPin = PwmChannels<TIM8, pwm::C4>;
|
||||
|
||||
const MAX_USER_FAN_PWM: f32 = 100.0;
|
||||
const MIN_USER_FAN_PWM: f32 = 1.0;
|
||||
|
||||
pub struct FanCtrl {
|
||||
fan: Option<FanPin>,
|
||||
fan_auto: bool,
|
||||
pwm_enabled: bool,
|
||||
k_a: f32,
|
||||
k_b: f32,
|
||||
k_c: f32,
|
||||
abs_max_tec_i: f32,
|
||||
hw_settings: HWSettings,
|
||||
}
|
||||
|
||||
impl FanCtrl {
|
||||
pub fn new(fan: Option<FanPin>, hw_settings: HWSettings) -> Self {
|
||||
let mut fan_ctrl = FanCtrl {
|
||||
fan,
|
||||
// do not enable auto mode by default,
|
||||
// but allow to turn it at the user's own risk
|
||||
fan_auto: hw_settings.fan_pwm_recommended,
|
||||
pwm_enabled: false,
|
||||
k_a: hw_settings.fan_k_a,
|
||||
k_b: hw_settings.fan_k_b,
|
||||
k_c: hw_settings.fan_k_c,
|
||||
abs_max_tec_i: 0f32,
|
||||
hw_settings,
|
||||
};
|
||||
if fan_ctrl.fan_auto {
|
||||
fan_ctrl.enable_pwm();
|
||||
}
|
||||
fan_ctrl
|
||||
}
|
||||
|
||||
pub fn cycle(&mut self, abs_max_tec_i: ElectricCurrent) {
|
||||
self.abs_max_tec_i = abs_max_tec_i.get::<ampere>() as f32;
|
||||
if self.fan_auto && self.hw_settings.fan_available {
|
||||
let scaled_current = self.abs_max_tec_i / MAX_TEC_I.get::<ampere>() as f32;
|
||||
// do not limit upper bound, as it will be limited in the set_pwm()
|
||||
let pwm = (MAX_USER_FAN_PWM
|
||||
* (scaled_current * (scaled_current * self.k_a + self.k_b) + self.k_c))
|
||||
as u32;
|
||||
self.set_pwm(pwm);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn summary(&mut self) -> Result<JsonBuffer, serde_json_core::ser::Error> {
|
||||
if self.hw_settings.fan_available {
|
||||
let summary = FanSummary {
|
||||
fan_pwm: self.get_pwm(),
|
||||
abs_max_tec_i: self.abs_max_tec_i,
|
||||
auto_mode: self.fan_auto,
|
||||
k_a: self.k_a,
|
||||
k_b: self.k_b,
|
||||
k_c: self.k_c,
|
||||
};
|
||||
serde_json_core::to_vec(&summary)
|
||||
} else {
|
||||
let summary: Option<()> = None;
|
||||
serde_json_core::to_vec(&summary)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_auto_mode(&mut self, fan_auto: bool) {
|
||||
self.fan_auto = fan_auto;
|
||||
}
|
||||
|
||||
pub fn set_curve(&mut self, k_a: f32, k_b: f32, k_c: f32) {
|
||||
self.k_a = k_a;
|
||||
self.k_b = k_b;
|
||||
self.k_c = k_c;
|
||||
}
|
||||
|
||||
pub fn restore_defaults(&mut self) {
|
||||
self.set_curve(
|
||||
self.hw_settings.fan_k_a,
|
||||
self.hw_settings.fan_k_b,
|
||||
self.hw_settings.fan_k_c,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn set_pwm(&mut self, fan_pwm: u32) -> f32 {
|
||||
if self.fan.is_none() || (!self.pwm_enabled && !self.enable_pwm()) {
|
||||
return 0f32;
|
||||
}
|
||||
let fan = self.fan.as_mut().unwrap();
|
||||
let fan_pwm = fan_pwm.clamp(MIN_USER_FAN_PWM as u32, MAX_USER_FAN_PWM as u32);
|
||||
let duty = scale_number(
|
||||
fan_pwm as f32,
|
||||
self.hw_settings.min_fan_pwm,
|
||||
self.hw_settings.max_fan_pwm,
|
||||
MIN_USER_FAN_PWM,
|
||||
MAX_USER_FAN_PWM,
|
||||
);
|
||||
let max = fan.get_max_duty();
|
||||
let value = ((duty * (max as f32)) as u16).min(max);
|
||||
fan.set_duty(value);
|
||||
value as f32 / (max as f32)
|
||||
}
|
||||
|
||||
pub fn fan_pwm_recommended(&self) -> bool {
|
||||
self.hw_settings.fan_pwm_recommended
|
||||
}
|
||||
|
||||
pub fn fan_available(&self) -> bool {
|
||||
self.hw_settings.fan_available
|
||||
}
|
||||
|
||||
fn get_pwm(&self) -> u32 {
|
||||
if let Some(fan) = &self.fan {
|
||||
let duty = fan.get_duty();
|
||||
let max = fan.get_max_duty();
|
||||
scale_number(
|
||||
duty as f32 / (max as f32),
|
||||
MIN_USER_FAN_PWM,
|
||||
MAX_USER_FAN_PWM,
|
||||
self.hw_settings.min_fan_pwm,
|
||||
self.hw_settings.max_fan_pwm,
|
||||
)
|
||||
.round() as u32
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
fn enable_pwm(&mut self) -> bool {
|
||||
if self.fan.is_some() && self.hw_settings.fan_available {
|
||||
let fan = self.fan.as_mut().unwrap();
|
||||
fan.set_duty(0);
|
||||
fan.enable();
|
||||
self.pwm_enabled = true;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn scale_number(unscaled: f32, to_min: f32, to_max: f32, from_min: f32, from_max: f32) -> f32 {
|
||||
(to_max - to_min) * (unscaled - from_min) / (from_max - from_min) + to_min
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct FanSummary {
|
||||
fan_pwm: u32,
|
||||
abs_max_tec_i: f32,
|
||||
auto_mode: bool,
|
||||
k_a: f32,
|
||||
k_b: f32,
|
||||
k_c: f32,
|
||||
}
|
69
src/flash_store.rs
Normal file
69
src/flash_store.rs
Normal file
@ -0,0 +1,69 @@
|
||||
use log::{error, info};
|
||||
use sfkv::{Store, StoreBackend};
|
||||
use stm32f4xx_hal::{
|
||||
flash::{Error, FlashExt},
|
||||
stm32::FLASH,
|
||||
};
|
||||
|
||||
/// 16 KiB
|
||||
pub const FLASH_SECTOR_SIZE: usize = 0x4000;
|
||||
pub const FLASH_SECTOR: u8 = 12;
|
||||
static mut BACKUP_SPACE: [u8; FLASH_SECTOR_SIZE] = [0; FLASH_SECTOR_SIZE];
|
||||
|
||||
extern "C" {
|
||||
// These are from memory.x
|
||||
static _config_start: usize;
|
||||
static _flash_start: usize;
|
||||
}
|
||||
|
||||
pub struct FlashBackend {
|
||||
flash: FLASH,
|
||||
}
|
||||
|
||||
fn get_offset() -> usize {
|
||||
unsafe { (&_config_start as *const usize as usize) - (&_flash_start as *const usize as usize) }
|
||||
}
|
||||
|
||||
impl StoreBackend for FlashBackend {
|
||||
type Data = [u8];
|
||||
|
||||
fn data(&self) -> &Self::Data {
|
||||
&self.flash.read()[get_offset()..(get_offset() + FLASH_SECTOR_SIZE)]
|
||||
}
|
||||
|
||||
type Error = Error;
|
||||
fn erase(&mut self) -> Result<(), Self::Error> {
|
||||
info!("erasing store flash");
|
||||
self.flash.unlocked().erase(FLASH_SECTOR)
|
||||
}
|
||||
|
||||
fn program(&mut self, offset: usize, payload: &[u8]) -> Result<(), Self::Error> {
|
||||
self.flash
|
||||
.unlocked()
|
||||
.program(get_offset() + offset, payload.iter())
|
||||
}
|
||||
|
||||
fn backup_space(&self) -> &'static mut [u8] {
|
||||
unsafe { &mut BACKUP_SPACE }
|
||||
}
|
||||
}
|
||||
|
||||
pub type FlashStore = Store<FlashBackend>;
|
||||
|
||||
pub fn store(flash: FLASH) -> FlashStore {
|
||||
let backend = FlashBackend { flash };
|
||||
let mut store = FlashStore::new(backend);
|
||||
|
||||
// just try to read the store
|
||||
match store.get_bytes_used() {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
error!("corrupt store, erasing. error: {:?}", e);
|
||||
let _ = store
|
||||
.erase()
|
||||
.map_err(|e| error!("flash erase failed: {:?}", e));
|
||||
}
|
||||
}
|
||||
|
||||
store
|
||||
}
|
86
src/hw_rev.rs
Normal file
86
src/hw_rev.rs
Normal file
@ -0,0 +1,86 @@
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{command_handler::JsonBuffer, pins::HWRevPins};
|
||||
|
||||
#[derive(Serialize, Copy, Clone)]
|
||||
pub struct HWRev {
|
||||
pub major: u8,
|
||||
pub minor: u8,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct HWSettings {
|
||||
pub fan_k_a: f32,
|
||||
pub fan_k_b: f32,
|
||||
pub fan_k_c: f32,
|
||||
pub min_fan_pwm: f32,
|
||||
pub max_fan_pwm: f32,
|
||||
pub fan_pwm_freq_hz: u32,
|
||||
pub fan_available: bool,
|
||||
pub fan_pwm_recommended: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
struct HWSummary<'a> {
|
||||
rev: &'a HWRev,
|
||||
settings: &'a HWSettings,
|
||||
}
|
||||
|
||||
impl HWRev {
|
||||
pub fn detect_hw_rev(hwrev_pins: &HWRevPins) -> Self {
|
||||
let (h0, h1, h2, h3) = (
|
||||
hwrev_pins.hwrev0.is_high(),
|
||||
hwrev_pins.hwrev1.is_high(),
|
||||
hwrev_pins.hwrev2.is_high(),
|
||||
hwrev_pins.hwrev3.is_high(),
|
||||
);
|
||||
match (h0, h1, h2, h3) {
|
||||
(true, true, true, false) => HWRev { major: 1, minor: 0 },
|
||||
(true, false, false, false) => HWRev { major: 2, minor: 0 },
|
||||
(false, true, false, false) => HWRev { major: 2, minor: 2 },
|
||||
(_, _, _, _) => HWRev { major: 0, minor: 0 },
|
||||
}
|
||||
}
|
||||
|
||||
pub fn settings(&self) -> HWSettings {
|
||||
match (self.major, self.minor) {
|
||||
(2, 2) => HWSettings {
|
||||
fan_k_a: 1.0,
|
||||
fan_k_b: 0.0,
|
||||
fan_k_c: 0.0,
|
||||
// below this value motor's autostart feature may fail,
|
||||
// according to internal experiments
|
||||
min_fan_pwm: 0.04,
|
||||
max_fan_pwm: 1.0,
|
||||
// According to `SUNON DC Brushless Fan & Blower(255-E)` catalogue p.36-37
|
||||
// model MF35101V1-1000U-G99 doesn't have a PWM wire, but we'll follow their others models'
|
||||
// recommended frequency, as it is said by the Thermostat's schematics that we can
|
||||
// use PWM, but not stated at which frequency
|
||||
fan_pwm_freq_hz: 25_000,
|
||||
fan_available: true,
|
||||
// see https://github.com/sinara-hw/Thermostat/issues/115 and
|
||||
// https://git.m-labs.hk/M-Labs/thermostat/issues/69#issuecomment-6464 for explanation
|
||||
fan_pwm_recommended: false,
|
||||
},
|
||||
(_, _) => HWSettings {
|
||||
fan_k_a: 0.0,
|
||||
fan_k_b: 0.0,
|
||||
fan_k_c: 0.0,
|
||||
min_fan_pwm: 0.0,
|
||||
max_fan_pwm: 0.0,
|
||||
fan_pwm_freq_hz: 0,
|
||||
fan_available: false,
|
||||
fan_pwm_recommended: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn summary(&self) -> Result<JsonBuffer, serde_json_core::ser::Error> {
|
||||
let settings = self.settings();
|
||||
let summary = HWSummary {
|
||||
rev: self,
|
||||
settings: &settings,
|
||||
};
|
||||
serde_json_core::to_vec(&summary)
|
||||
}
|
||||
}
|
@ -1,19 +1,24 @@
|
||||
#[cfg(not(feature = "semihosting"))]
|
||||
pub fn init_log() {}
|
||||
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);
|
||||
}
|
||||
|
||||
#[cfg(feature = "semihosting")]
|
||||
pub fn init_log() {
|
||||
use cortex_m_log::log::{init, Logger};
|
||||
use cortex_m_log::printer::semihosting::{hio::HStdout, InterruptOk};
|
||||
use log::LevelFilter;
|
||||
use cortex_m_log::log::{Logger, init};
|
||||
use cortex_m_log::printer::semihosting::{InterruptOk, hio::HStdout};
|
||||
static mut LOGGER: Option<Logger<InterruptOk<HStdout>>> = None;
|
||||
let logger = Logger {
|
||||
inner: InterruptOk::<_>::stdout().expect("semihosting stdout"),
|
||||
level: LevelFilter::Info,
|
||||
};
|
||||
let logger = unsafe {
|
||||
LOGGER.get_or_insert(logger)
|
||||
};
|
||||
let logger = unsafe { LOGGER.get_or_insert(logger) };
|
||||
|
||||
init(logger).expect("set logger");
|
||||
}
|
||||
|
44
src/leds.rs
Normal file
44
src/leds.rs
Normal file
@ -0,0 +1,44 @@
|
||||
use stm32f4xx_hal::{
|
||||
gpio::{
|
||||
gpiod::{PD10, PD11, PD9},
|
||||
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();
|
||||
}
|
||||
}
|
520
src/main.rs
520
src/main.rs
@ -1,82 +1,108 @@
|
||||
#![no_std]
|
||||
#![no_main]
|
||||
#![cfg_attr(not(test), no_std)]
|
||||
#![cfg_attr(not(test), no_main)]
|
||||
#![cfg_attr(test, allow(unused))]
|
||||
// TODO: #![deny(warnings, unused)]
|
||||
|
||||
#[cfg(not(feature = "semihosting"))]
|
||||
use panic_abort as _;
|
||||
#[cfg(feature = "semihosting")]
|
||||
#[cfg(not(any(feature = "semihosting", test)))]
|
||||
use panic_halt as _;
|
||||
#[cfg(all(feature = "semihosting", not(test)))]
|
||||
use panic_semihosting as _;
|
||||
|
||||
use log::{info, warn};
|
||||
|
||||
use core::ops::DerefMut;
|
||||
use core::fmt::Write;
|
||||
use cortex_m::asm::wfi;
|
||||
use cortex_m_rt::entry;
|
||||
use log::{error, info, warn};
|
||||
use smoltcp::{socket::TcpSocket, time::Instant, wire::EthernetAddress};
|
||||
use stm32f4xx_hal::{
|
||||
hal::{
|
||||
self,
|
||||
watchdog::{WatchdogEnable, Watchdog},
|
||||
},
|
||||
hal::watchdog::{Watchdog, WatchdogEnable},
|
||||
rcc::RccExt,
|
||||
stm32::{CorePeripherals, Peripherals, SCB},
|
||||
time::{MegaHertz, U32Ext},
|
||||
watchdog::IndependentWatchdog,
|
||||
time::{U32Ext, MegaHertz},
|
||||
stm32::{CorePeripherals, Peripherals},
|
||||
};
|
||||
use smoltcp::{
|
||||
time::Instant,
|
||||
wire::EthernetAddress,
|
||||
};
|
||||
|
||||
mod init_log;
|
||||
use init_log::init_log;
|
||||
mod leds;
|
||||
mod pins;
|
||||
mod usb;
|
||||
use pins::Pins;
|
||||
mod ad7172;
|
||||
mod ad5680;
|
||||
mod ad7172;
|
||||
mod net;
|
||||
mod server;
|
||||
use server::Server;
|
||||
mod session;
|
||||
use session::{Session, SessionOutput};
|
||||
use session::{Session, SessionInput};
|
||||
mod command_parser;
|
||||
use command_parser::{Command, ShowCommand, PwmPin};
|
||||
mod timer;
|
||||
mod units;
|
||||
use units::{Ohms, Volts};
|
||||
mod pid;
|
||||
mod steinhart_hart;
|
||||
use command_parser::Ipv4Config;
|
||||
mod b_parameter;
|
||||
mod channels;
|
||||
use channels::{CHANNELS, Channels};
|
||||
mod pid;
|
||||
mod timer;
|
||||
use channels::{Channels, CHANNELS};
|
||||
mod channel;
|
||||
mod channel_state;
|
||||
|
||||
mod config;
|
||||
use config::ChannelConfig;
|
||||
mod command_handler;
|
||||
mod dfu;
|
||||
mod flash_store;
|
||||
use command_handler::Handler;
|
||||
mod fan_ctrl;
|
||||
use fan_ctrl::FanCtrl;
|
||||
mod hw_rev;
|
||||
|
||||
const HSE: MegaHertz = MegaHertz(8);
|
||||
#[cfg(not(feature = "semihosting"))]
|
||||
const WATCHDOG_INTERVAL: u32 = 100;
|
||||
const WATCHDOG_INTERVAL: u32 = 1_000;
|
||||
#[cfg(feature = "semihosting")]
|
||||
const WATCHDOG_INTERVAL: u32 = 30_000;
|
||||
|
||||
#[cfg(not(feature = "generate-hwaddr"))]
|
||||
const NET_HWADDR: [u8; 6] = [0x02, 0x00, 0xDE, 0xAD, 0xBE, 0xEF];
|
||||
const CHANNEL_CONFIG_KEY: [&str; 2] = ["ch0", "ch1"];
|
||||
|
||||
const TCP_PORT: u16 = 23;
|
||||
|
||||
fn send_line(socket: &mut TcpSocket, data: &[u8]) -> bool {
|
||||
let send_free = socket.send_capacity() - socket.send_queue();
|
||||
if data.len() > send_free + 1 {
|
||||
// Not enough buffer space, skip report for now,
|
||||
// instead of sending incomplete line
|
||||
warn!(
|
||||
"TCP socket has only {}/{} needed {}",
|
||||
send_free + 1,
|
||||
socket.send_capacity(),
|
||||
data.len(),
|
||||
);
|
||||
} else {
|
||||
match socket.send_slice(data) {
|
||||
Ok(sent) if sent == data.len() => {
|
||||
let _ = socket.send_slice(b"\n");
|
||||
// success
|
||||
return true;
|
||||
}
|
||||
Ok(sent) => warn!("sent only {}/{} bytes", sent, data.len()),
|
||||
Err(e) => error!("error sending line: {:?}", e),
|
||||
}
|
||||
}
|
||||
// not success
|
||||
false
|
||||
}
|
||||
|
||||
/// Initialization and main loop
|
||||
#[cfg(not(test))]
|
||||
#[entry]
|
||||
fn main() -> ! {
|
||||
init_log();
|
||||
info!("tecpak");
|
||||
info!("thermostat");
|
||||
|
||||
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()
|
||||
let clocks = dp
|
||||
.RCC
|
||||
.constrain()
|
||||
.cfgr
|
||||
.use_hse(HSE)
|
||||
.sysclk(168.mhz())
|
||||
@ -89,304 +115,166 @@ fn main() -> ! {
|
||||
wd.start(WATCHDOG_INTERVAL.ms());
|
||||
wd.feed();
|
||||
|
||||
let pins = Pins::setup(
|
||||
clocks, dp.TIM1, dp.TIM3,
|
||||
dp.GPIOA, dp.GPIOB, dp.GPIOC, dp.GPIOE, dp.GPIOF, dp.GPIOG,
|
||||
dp.SPI2, dp.SPI4, dp.SPI5,
|
||||
dp.ADC1, dp.ADC2, dp.ADC3,
|
||||
);
|
||||
let mut channels = Channels::new(pins);
|
||||
channels.calibrate_dac_value(0);
|
||||
|
||||
timer::setup(cp.SYST, clocks);
|
||||
|
||||
#[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))
|
||||
let (pins, mut leds, mut eeprom, eth_pins, usb, fan, hwrev, hw_settings) = Pins::setup(
|
||||
clocks,
|
||||
(dp.TIM1, dp.TIM3, dp.TIM8),
|
||||
(
|
||||
dp.GPIOA, dp.GPIOB, dp.GPIOC, dp.GPIOD, dp.GPIOE, dp.GPIOF, dp.GPIOG,
|
||||
),
|
||||
dp.I2C1,
|
||||
(dp.SPI2, dp.SPI4, dp.SPI5),
|
||||
dp.ADC1,
|
||||
(dp.OTG_FS_GLOBAL, dp.OTG_FS_DEVICE, dp.OTG_FS_PWRCLK),
|
||||
);
|
||||
|
||||
leds.r1.on();
|
||||
leds.g3.off();
|
||||
leds.g4.off();
|
||||
|
||||
usb::State::setup(usb);
|
||||
|
||||
let mut store = flash_store::store(dp.FLASH);
|
||||
|
||||
let mut channels = Channels::new(pins);
|
||||
for (c, key) in CHANNEL_CONFIG_KEY.iter().enumerate().take(CHANNELS) {
|
||||
match store.read_value::<ChannelConfig>(key) {
|
||||
Ok(Some(config)) => config.apply(&mut channels, c),
|
||||
Ok(None) => error!("flash config not found for channel {}", c),
|
||||
Err(e) => error!("unable to load config {} from flash: {:?}", c, e),
|
||||
}
|
||||
}
|
||||
|
||||
let mut fan_ctrl = FanCtrl::new(fan, hw_settings);
|
||||
|
||||
// default net config:
|
||||
let mut ipv4_config = Ipv4Config {
|
||||
address: [192, 168, 1, 26],
|
||||
mask_len: 24,
|
||||
gateway: None,
|
||||
};
|
||||
info!("Net hwaddr: {}", hwaddr);
|
||||
match store.read_value("ipv4") {
|
||||
Ok(Some(config)) => ipv4_config = config,
|
||||
Ok(None) => {}
|
||||
Err(e) => error!("cannot read ipv4 config: {:?}", e),
|
||||
}
|
||||
|
||||
net::run(dp.ETHERNET_MAC, dp.ETHERNET_DMA, hwaddr, |iface| {
|
||||
Server::<Session>::run(iface, |server| {
|
||||
loop {
|
||||
let instant = Instant::from_millis(i64::from(timer::now()));
|
||||
let updated_channel = channels.poll_adc(instant);
|
||||
if let Some(channel) = updated_channel {
|
||||
server.for_each(|_, session| session.set_report_pending(channel.into()));
|
||||
}
|
||||
// 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);
|
||||
|
||||
let instant = Instant::from_millis(i64::from(timer::now()));
|
||||
cortex_m::interrupt::free(net::clear_pending);
|
||||
server.poll(instant)
|
||||
.unwrap_or_else(|e| {
|
||||
net::run(
|
||||
clocks,
|
||||
dp.ETHERNET_MAC,
|
||||
dp.ETHERNET_DMA,
|
||||
eth_pins,
|
||||
hwaddr,
|
||||
ipv4_config.clone(),
|
||||
|iface| {
|
||||
Server::<Session>::run(iface, |server| {
|
||||
leds.r1.off();
|
||||
let mut should_reset = false;
|
||||
|
||||
loop {
|
||||
let mut new_ipv4_config = None;
|
||||
let instant = Instant::from_millis(i64::from(timer::now()));
|
||||
channels.poll_adc(instant);
|
||||
|
||||
fan_ctrl.cycle(channels.current_abs_max_tec_i());
|
||||
|
||||
if channels.pid_engaged() {
|
||||
leds.g3.on();
|
||||
} else {
|
||||
leds.g3.off();
|
||||
}
|
||||
|
||||
let instant = Instant::from_millis(i64::from(timer::now()));
|
||||
cortex_m::interrupt::free(net::clear_pending);
|
||||
server.poll(instant).unwrap_or_else(|e| {
|
||||
warn!("poll: {:?}", e);
|
||||
});
|
||||
|
||||
// TCP protocol handling
|
||||
server.for_each(|mut socket, session| {
|
||||
if ! socket.is_active() {
|
||||
let _ = socket.listen(TCP_PORT);
|
||||
session.reset();
|
||||
} else if socket.can_send() && socket.can_recv() && socket.send_capacity() - socket.send_queue() > 1024 {
|
||||
match socket.recv(|buf| session.feed(buf)) {
|
||||
Ok(SessionOutput::Nothing) => {}
|
||||
Ok(SessionOutput::Command(command)) => match command {
|
||||
Command::Quit =>
|
||||
socket.close(),
|
||||
Command::Reporting(reporting) => {
|
||||
let _ = writeln!(socket, "report={}", if reporting { "on" } else { "off" });
|
||||
}
|
||||
Command::Show(ShowCommand::Reporting) => {
|
||||
let _ = writeln!(socket, "report={}", if session.reporting() { "on" } else { "off" });
|
||||
}
|
||||
Command::Show(ShowCommand::Input) => {
|
||||
for channel in 0..CHANNELS {
|
||||
if let Some(adc_data) = channels.channel_state(channel).adc_data {
|
||||
let vref = channels.read_vref(channel);
|
||||
let dac_feedback = channels.read_dac_feedback(channel);
|
||||
|
||||
let itec = channels.read_itec(channel);
|
||||
let tec_i = -(itec - Volts(1.5)) / Ohms(0.4);
|
||||
|
||||
let tec_u_meas = channels.read_tec_u_meas(channel);
|
||||
|
||||
let state = channels.channel_state(channel);
|
||||
let _ = writeln!(
|
||||
socket, "t={} adc_raw{}=0x{:06X} vref={} dac_feedback={} itec={} tec={} tec_u_meas={}",
|
||||
state.adc_time, channel, adc_data,
|
||||
vref, dac_feedback,
|
||||
itec, tec_i,
|
||||
tec_u_meas,
|
||||
);
|
||||
if !should_reset {
|
||||
// TCP protocol handling
|
||||
server.for_each(|mut socket, session| {
|
||||
if !socket.is_active() {
|
||||
let _ = socket.listen(TCP_PORT);
|
||||
session.reset();
|
||||
} else if socket.may_send() && !socket.may_recv() {
|
||||
socket.close()
|
||||
} else if socket.can_send() && socket.can_recv() {
|
||||
match socket.recv(|buf| session.feed(buf)) {
|
||||
// SessionInput::Nothing happens when the line reader parses a string of characters that is not
|
||||
// followed by a newline character. Could be due to partial commands not terminated with newline,
|
||||
// socket RX ring buffer wraps around, or when the command is sent as seperate TCP packets etc.
|
||||
// Do nothing and feed more data to the line reader in the next loop cycle.
|
||||
Ok(SessionInput::Nothing) => {}
|
||||
Ok(SessionInput::Command(command)) => {
|
||||
match Handler::handle_command(
|
||||
command,
|
||||
&mut socket,
|
||||
&mut channels,
|
||||
&mut store,
|
||||
&mut ipv4_config,
|
||||
&mut fan_ctrl,
|
||||
hwrev,
|
||||
) {
|
||||
Ok(Handler::NewIPV4(ip)) => new_ipv4_config = Some(ip),
|
||||
Ok(Handler::Handled) => {}
|
||||
Ok(Handler::CloseSocket) => socket.close(),
|
||||
Ok(Handler::Reset) => should_reset = true,
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Command::Show(ShowCommand::Pid) => {
|
||||
for channel in 0..CHANNELS {
|
||||
let state = channels.channel_state(channel);
|
||||
let _ = writeln!(socket, "PID settings for channel {}", channel);
|
||||
let pid = &state.pid;
|
||||
let _ = writeln!(socket, "- target={:.4}", pid.target);
|
||||
macro_rules! show_pid_parameter {
|
||||
($p: tt) => {
|
||||
let _ = writeln!(
|
||||
socket, "- {}={:.4}",
|
||||
stringify!($p), pid.parameters.$p
|
||||
);
|
||||
};
|
||||
}
|
||||
show_pid_parameter!(kp);
|
||||
show_pid_parameter!(ki);
|
||||
show_pid_parameter!(kd);
|
||||
show_pid_parameter!(integral_min);
|
||||
show_pid_parameter!(integral_max);
|
||||
show_pid_parameter!(output_min);
|
||||
show_pid_parameter!(output_max);
|
||||
if let Some(last_output) = pid.last_output {
|
||||
let _ = writeln!(socket, "- last_output={:.4}", last_output);
|
||||
}
|
||||
let _ = writeln!(socket, "");
|
||||
}
|
||||
}
|
||||
Command::Show(ShowCommand::Pwm) => {
|
||||
for channel in 0..CHANNELS {
|
||||
let state = channels.channel_state(channel);
|
||||
let _ = writeln!(
|
||||
socket, "channel {}: PID={}",
|
||||
channel,
|
||||
if state.pid_engaged { "engaged" } else { "disengaged" }
|
||||
);
|
||||
let _ = writeln!(socket, "- i_set={}", state.dac_value);
|
||||
fn show_pwm_channel<S, P>(mut socket: S, name: &str, pin: &P)
|
||||
where
|
||||
S: core::fmt::Write,
|
||||
P: hal::PwmPin<Duty=u16>,
|
||||
{
|
||||
let _ = writeln!(
|
||||
socket,
|
||||
"- {}={}/{}",
|
||||
name, pin.get_duty(), pin.get_max_duty()
|
||||
);
|
||||
}
|
||||
match channel {
|
||||
0 => {
|
||||
show_pwm_channel(socket.deref_mut(), "max_v", &channels.pwm.max_v0);
|
||||
show_pwm_channel(socket.deref_mut(), "max_i_pos", &channels.pwm.max_i_pos0);
|
||||
show_pwm_channel(socket.deref_mut(), "max_i_neg", &channels.pwm.max_i_neg0);
|
||||
}
|
||||
1 => {
|
||||
show_pwm_channel(socket.deref_mut(), "max_v", &channels.pwm.max_v1);
|
||||
show_pwm_channel(socket.deref_mut(), "max_i_pos", &channels.pwm.max_i_pos1);
|
||||
show_pwm_channel(socket.deref_mut(), "max_i_neg", &channels.pwm.max_i_neg1);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
let _ = writeln!(socket, "");
|
||||
}
|
||||
}
|
||||
Command::Show(ShowCommand::SteinhartHart) => {
|
||||
for channel in 0..CHANNELS {
|
||||
let state = channels.channel_state(channel);
|
||||
let _ = writeln!(
|
||||
socket, "channel {}: Steinhart-Hart equation parameters",
|
||||
channel,
|
||||
);
|
||||
let _ = writeln!(socket, "- t0={}", state.sh.t0);
|
||||
let _ = writeln!(socket, "- b={}", state.sh.b);
|
||||
let _ = writeln!(socket, "- r0={}", state.sh.r0);
|
||||
let _ = writeln!(socket, "");
|
||||
}
|
||||
}
|
||||
Command::Show(ShowCommand::PostFilter) => {
|
||||
for channel in 0..CHANNELS {
|
||||
match channels.adc.get_postfilter(channel as u8).unwrap() {
|
||||
Some(filter) => {
|
||||
let _ = writeln!(
|
||||
socket, "channel {}: postfilter={:.2} SPS",
|
||||
channel, filter.output_rate().unwrap()
|
||||
);
|
||||
}
|
||||
None => {
|
||||
let _ = writeln!(
|
||||
socket, "channel {}: no postfilter",
|
||||
channel
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Command::PwmPid { channel } => {
|
||||
channels.channel_state(channel).pid_engaged = true;
|
||||
let _ = writeln!(socket, "channel {}: PID enabled to control PWM", channel
|
||||
);
|
||||
}
|
||||
Command::Pwm { channel, pin: PwmPin::ISet, duty } => {
|
||||
channels.channel_state(channel).pid_engaged = false;
|
||||
let voltage = Volts(duty);
|
||||
channels.set_dac(channel, voltage);
|
||||
let _ = writeln!(
|
||||
socket, "channel {}: PWM duty cycle manually set to {}",
|
||||
channel, voltage
|
||||
);
|
||||
}
|
||||
Command::Pwm { channel, pin, duty } => {
|
||||
fn set_pwm_channel<P: hal::PwmPin<Duty=u16>>(pin: &mut P, duty: f64) -> (u16, u16) {
|
||||
let max = pin.get_max_duty();
|
||||
let value = (duty * (max as f64)) as u16;
|
||||
pin.set_duty(value);
|
||||
(value, max)
|
||||
}
|
||||
let (value, max) = match (channel, pin) {
|
||||
(_, PwmPin::ISet) =>
|
||||
// Handled above
|
||||
unreachable!(),
|
||||
(0, PwmPin::MaxIPos) =>
|
||||
set_pwm_channel(&mut channels.pwm.max_i_pos0, duty),
|
||||
(0, PwmPin::MaxINeg) =>
|
||||
set_pwm_channel(&mut channels.pwm.max_i_neg0, duty),
|
||||
(0, PwmPin::MaxV) =>
|
||||
set_pwm_channel(&mut channels.pwm.max_v0, duty),
|
||||
(1, PwmPin::MaxIPos) =>
|
||||
set_pwm_channel(&mut channels.pwm.max_i_pos1, duty),
|
||||
(1, PwmPin::MaxINeg) =>
|
||||
set_pwm_channel(&mut channels.pwm.max_i_neg1, duty),
|
||||
(1, PwmPin::MaxV) =>
|
||||
set_pwm_channel(&mut channels.pwm.max_v1, duty),
|
||||
_ =>
|
||||
unreachable!(),
|
||||
};
|
||||
let _ = writeln!(
|
||||
socket, "channel {}: PWM {} reconfigured to {}/{}",
|
||||
channel, pin.name(), value, max
|
||||
);
|
||||
}
|
||||
Command::Pid { channel, parameter, value } => {
|
||||
let pid = &mut channels.channel_state(channel).pid;
|
||||
use command_parser::PidParameter::*;
|
||||
match parameter {
|
||||
Target =>
|
||||
pid.target = value,
|
||||
KP =>
|
||||
pid.parameters.kp = value,
|
||||
KI =>
|
||||
pid.parameters.ki = value,
|
||||
KD =>
|
||||
pid.parameters.kd = value,
|
||||
OutputMin =>
|
||||
pid.parameters.output_min = value,
|
||||
OutputMax =>
|
||||
pid.parameters.output_max = value,
|
||||
IntegralMin =>
|
||||
pid.parameters.integral_min = value,
|
||||
IntegralMax =>
|
||||
pid.parameters.integral_max = value,
|
||||
}
|
||||
// TODO: really reset PID state
|
||||
// after each parameter change?
|
||||
pid.reset();
|
||||
let _ = writeln!(socket, "PID parameter updated");
|
||||
}
|
||||
Command::SteinhartHart { channel, parameter, value } => {
|
||||
let sh = &mut channels.channel_state(channel).sh;
|
||||
use command_parser::ShParameter::*;
|
||||
match parameter {
|
||||
T0 => sh.t0 = value,
|
||||
B => sh.b = value,
|
||||
R0 => sh.r0 = value,
|
||||
}
|
||||
let _ = writeln!(socket, "Steinhart-Hart equation parameter updated");
|
||||
}
|
||||
Command::PostFilter { channel, rate } => {
|
||||
let filter = ad7172::PostFilter::closest(rate);
|
||||
match filter {
|
||||
Some(filter) => {
|
||||
channels.adc.set_postfilter(channel as u8, Some(filter)).unwrap();
|
||||
let _ = writeln!(
|
||||
socket, "channel {}: postfilter set to {:.2} SPS",
|
||||
channel, filter.output_rate().unwrap()
|
||||
);
|
||||
}
|
||||
None => {
|
||||
let _ = writeln!(socket, "Unable to choose postfilter");
|
||||
}
|
||||
Ok(SessionInput::Error(e)) => {
|
||||
error!("session input: {:?}", e);
|
||||
send_line(&mut socket, b"{ \"error\": \"invalid input\" }");
|
||||
}
|
||||
Err(_) => socket.close(),
|
||||
}
|
||||
}
|
||||
Ok(SessionOutput::Error(e)) => {
|
||||
let _ = writeln!(socket, "Command error: {:?}", e);
|
||||
});
|
||||
} else {
|
||||
// Should reset, close all TCP sockets.
|
||||
let mut any_socket_alive = false;
|
||||
server.for_each(|mut socket, _| {
|
||||
if socket.is_active() {
|
||||
socket.abort();
|
||||
any_socket_alive = true;
|
||||
}
|
||||
Err(_) =>
|
||||
socket.close(),
|
||||
}
|
||||
} else if socket.can_send() && socket.send_capacity() - socket.send_queue() > 256 {
|
||||
while let Some(channel) = session.is_report_pending() {
|
||||
let state = &mut channels.channel_state(usize::from(channel));
|
||||
let _ = writeln!(
|
||||
socket, "t={} raw{}=0x{:06X}",
|
||||
state.adc_time, channel, state.adc_data.unwrap_or(0)
|
||||
).map(|_| {
|
||||
session.mark_report_sent(channel);
|
||||
});
|
||||
});
|
||||
// Must let loop run for one more cycle to poll server for RST to be sent,
|
||||
// this makes sure system does not reset right after socket.abort() is called.
|
||||
if !any_socket_alive {
|
||||
SCB::sys_reset();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Update watchdog
|
||||
wd.feed();
|
||||
// Apply new IPv4 address/gateway
|
||||
if let Some(config) = new_ipv4_config.take() {
|
||||
server.set_ipv4_config(config.clone());
|
||||
ipv4_config = config;
|
||||
};
|
||||
|
||||
cortex_m::interrupt::free(|cs| {
|
||||
if !net::is_pending(cs) {
|
||||
// Wait for interrupts
|
||||
// (Ethernet or SysTick)
|
||||
wfi();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
// Update watchdog
|
||||
wd.feed();
|
||||
|
||||
leds.g4.off();
|
||||
cortex_m::interrupt::free(|cs| {
|
||||
if !net::is_pending(cs) {
|
||||
// Wait for interrupts
|
||||
// (Ethernet, SysTick, or USB)
|
||||
wfi();
|
||||
}
|
||||
});
|
||||
leds.g4.on();
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
unreachable!()
|
||||
}
|
||||
|
68
src/net.rs
68
src/net.rs
@ -1,15 +1,17 @@
|
||||
//! As there is only one peripheral, supporting data structures are
|
||||
//! declared once and globally.
|
||||
|
||||
use crate::command_parser::Ipv4Config;
|
||||
use crate::pins::EthernetPins;
|
||||
use core::cell::RefCell;
|
||||
use cortex_m::interrupt::Mutex;
|
||||
use bare_metal::CriticalSection;
|
||||
use stm32f4xx_hal::{
|
||||
stm32::{interrupt, Peripherals, ETHERNET_MAC, ETHERNET_DMA},
|
||||
};
|
||||
use smoltcp::wire::{EthernetAddress, IpAddress, IpCidr};
|
||||
use smoltcp::iface::{NeighborCache, EthernetInterfaceBuilder, EthernetInterface};
|
||||
use cortex_m::interrupt::{CriticalSection, Mutex};
|
||||
use smoltcp::iface::{EthernetInterface, EthernetInterfaceBuilder, NeighborCache, Routes};
|
||||
use smoltcp::wire::{EthernetAddress, Ipv4Address, Ipv4Cidr};
|
||||
use stm32_eth::{Eth, RingEntry, RxDescriptor, TxDescriptor};
|
||||
use stm32f4xx_hal::{
|
||||
pac::{interrupt, Peripherals, ETHERNET_DMA, ETHERNET_MAC},
|
||||
rcc::Clocks,
|
||||
};
|
||||
|
||||
/// Not on the stack so that stack can be placed in CCMRAM (which the
|
||||
/// ethernet peripheral cannot access)
|
||||
@ -24,33 +26,43 @@ 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>(
|
||||
ethernet_mac: ETHERNET_MAC, ethernet_dma: ETHERNET_DMA,
|
||||
ethernet_addr: EthernetAddress, f: F
|
||||
clocks: Clocks,
|
||||
ethernet_mac: ETHERNET_MAC,
|
||||
ethernet_dma: ETHERNET_DMA,
|
||||
eth_pins: EthernetPins,
|
||||
ethernet_addr: EthernetAddress,
|
||||
ipv4_config: Ipv4Config,
|
||||
f: F,
|
||||
) where
|
||||
F: FnOnce(EthernetInterface<&mut stm32_eth::Eth<'static, 'static>>),
|
||||
{
|
||||
let rx_ring = unsafe {
|
||||
RX_RING.get_or_insert(Default::default())
|
||||
};
|
||||
let tx_ring = unsafe {
|
||||
TX_RING.get_or_insert(Default::default())
|
||||
};
|
||||
let rx_ring = unsafe { RX_RING.get_or_insert(Default::default()) };
|
||||
let tx_ring = unsafe { TX_RING.get_or_insert(Default::default()) };
|
||||
// Ethernet driver
|
||||
let mut eth_dev = Eth::new(
|
||||
ethernet_mac, ethernet_dma,
|
||||
&mut rx_ring[..], &mut tx_ring[..]
|
||||
);
|
||||
ethernet_mac,
|
||||
ethernet_dma,
|
||||
&mut rx_ring[..],
|
||||
&mut tx_ring[..],
|
||||
clocks,
|
||||
eth_pins,
|
||||
)
|
||||
.unwrap();
|
||||
eth_dev.enable_interrupt();
|
||||
|
||||
// IP stack
|
||||
let local_addr = IpAddress::v4(192, 168, 1, 26);
|
||||
let mut ip_addrs = [IpCidr::new(local_addr, 24)];
|
||||
let (ipv4_cidr, gateway) = split_ipv4_config(ipv4_config);
|
||||
let mut ip_addrs = [ipv4_cidr.into()];
|
||||
let mut neighbor_storage = [None; 16];
|
||||
let neighbor_cache = NeighborCache::new(&mut neighbor_storage[..]);
|
||||
let mut routes_storage = [None; 1];
|
||||
let mut routes = Routes::new(&mut routes_storage[..]);
|
||||
gateway.map(|gateway| routes.add_default_ipv4_route(gateway).unwrap());
|
||||
let iface = EthernetInterfaceBuilder::new(&mut eth_dev)
|
||||
.ethernet_addr(ethernet_addr)
|
||||
.ip_addrs(&mut ip_addrs[..])
|
||||
.neighbor_cache(neighbor_cache)
|
||||
.routes(routes)
|
||||
.finalize();
|
||||
|
||||
f(iface);
|
||||
@ -61,8 +73,7 @@ pub fn run<F>(
|
||||
#[interrupt]
|
||||
fn ETH() {
|
||||
cortex_m::interrupt::free(|cs| {
|
||||
*NET_PENDING.borrow(cs)
|
||||
.borrow_mut() = true;
|
||||
*NET_PENDING.borrow(cs).borrow_mut() = true;
|
||||
});
|
||||
|
||||
let p = unsafe { Peripherals::steal() };
|
||||
@ -71,13 +82,18 @@ fn ETH() {
|
||||
|
||||
/// Has an interrupt occurred since last call to `clear_pending()`?
|
||||
pub fn is_pending(cs: &CriticalSection) -> bool {
|
||||
*NET_PENDING.borrow(cs)
|
||||
.borrow()
|
||||
*NET_PENDING.borrow(cs).borrow()
|
||||
}
|
||||
|
||||
/// Clear the interrupt pending flag before polling the interface for
|
||||
/// data.
|
||||
pub fn clear_pending(cs: &CriticalSection) {
|
||||
*NET_PENDING.borrow(cs)
|
||||
.borrow_mut() = false;
|
||||
*NET_PENDING.borrow(cs).borrow_mut() = false;
|
||||
}
|
||||
|
||||
/// utility for destructuring into smoltcp types
|
||||
pub fn split_ipv4_config(config: Ipv4Config) -> (Ipv4Cidr, Option<Ipv4Address>) {
|
||||
let cidr = Ipv4Cidr::new(Ipv4Address(config.address), config.mask_len);
|
||||
let gateway = config.gateway.map(Ipv4Address);
|
||||
(cidr, gateway)
|
||||
}
|
||||
|
148
src/pid.rs
148
src/pid.rs
@ -1,24 +1,27 @@
|
||||
#[derive(Clone, Copy)]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Parameters {
|
||||
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
|
||||
/// Gain coefficient for proportional term
|
||||
pub kp: f32,
|
||||
/// Gain coefficient for integral term
|
||||
pub ki: f32,
|
||||
/// Gain coefficient for derivative term
|
||||
pub kd: f32,
|
||||
/// Output limit minimum
|
||||
pub output_min: f32,
|
||||
/// Output limit maximum
|
||||
pub output_max: f32,
|
||||
}
|
||||
|
||||
impl Default for Parameters {
|
||||
fn default() -> Self {
|
||||
Parameters {
|
||||
kp: 0.5,
|
||||
ki: 0.05,
|
||||
kd: 0.45,
|
||||
output_min: 0.0,
|
||||
output_max: 5.0,
|
||||
integral_min: 0.0,
|
||||
integral_max: 1.0,
|
||||
kp: 0.0,
|
||||
ki: 0.0,
|
||||
kd: 0.0,
|
||||
output_min: -2.0,
|
||||
output_max: 2.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -27,58 +30,67 @@ impl Default for Parameters {
|
||||
pub struct Controller {
|
||||
pub parameters: Parameters,
|
||||
pub target: f64,
|
||||
integral: f64,
|
||||
last_input: Option<f64>,
|
||||
pub last_output: Option<f64>,
|
||||
u1: f64,
|
||||
x1: f64,
|
||||
x2: f64,
|
||||
pub y1: f64,
|
||||
}
|
||||
|
||||
impl Controller {
|
||||
pub const fn new(parameters: Parameters) -> Controller {
|
||||
Controller {
|
||||
parameters: parameters,
|
||||
parameters,
|
||||
target: 0.0,
|
||||
last_input: None,
|
||||
integral: 0.0,
|
||||
last_output: None,
|
||||
u1: 0.0,
|
||||
x1: 0.0,
|
||||
x2: 0.0,
|
||||
y1: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
// Based on https://hackmd.io/IACbwcOTSt6Adj3_F9bKuw PID implementation
|
||||
// Input x(t), target u(t), output y(t)
|
||||
// y0' = y1 - ki * u0
|
||||
// + x0 * (kp + ki + kd)
|
||||
// - x1 * (kp + 2kd)
|
||||
// + x2 * kd
|
||||
// y0 = clip(y0', ymin, ymax)
|
||||
pub fn update(&mut self, input: f64) -> f64 {
|
||||
let error = self.target - input;
|
||||
|
||||
let p = self.parameters.kp * error;
|
||||
|
||||
self.integral += error;
|
||||
if self.integral < self.parameters.integral_min {
|
||||
self.integral = self.parameters.integral_min;
|
||||
let mut output: f64 = self.y1 - self.target * f64::from(self.parameters.ki)
|
||||
+ input * f64::from(self.parameters.kp + self.parameters.ki + self.parameters.kd)
|
||||
- self.x1 * f64::from(self.parameters.kp + 2.0 * self.parameters.kd)
|
||||
+ self.x2 * f64::from(self.parameters.kd);
|
||||
if output < self.parameters.output_min.into() {
|
||||
output = self.parameters.output_min.into();
|
||||
}
|
||||
if self.integral > self.parameters.integral_max {
|
||||
self.integral = self.parameters.integral_max;
|
||||
if output > self.parameters.output_max.into() {
|
||||
output = self.parameters.output_max.into();
|
||||
}
|
||||
let i = self.parameters.ki * self.integral;
|
||||
|
||||
let d = match self.last_input {
|
||||
None => 0.0,
|
||||
Some(last_input) => self.parameters.kd * (last_input - input)
|
||||
};
|
||||
self.last_input = Some(input);
|
||||
|
||||
let mut output = p + i + d;
|
||||
if output < self.parameters.output_min {
|
||||
output = self.parameters.output_min;
|
||||
}
|
||||
if output > self.parameters.output_max {
|
||||
output = self.parameters.output_max;
|
||||
}
|
||||
self.last_output = Some(output);
|
||||
self.x2 = self.x1;
|
||||
self.x1 = input;
|
||||
self.u1 = self.target;
|
||||
self.y1 = output;
|
||||
output
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn reset(&mut self) {
|
||||
self.integral = 0.0;
|
||||
self.last_input = None;
|
||||
pub fn summary(&self, channel: usize) -> Summary {
|
||||
Summary {
|
||||
channel,
|
||||
parameters: self.parameters.clone(),
|
||||
target: self.target,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_ki(&mut self, new_ki: f32) {
|
||||
self.parameters.ki = new_ki;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct Summary {
|
||||
channel: usize,
|
||||
parameters: Parameters,
|
||||
target: f64,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@ -86,38 +98,46 @@ mod test {
|
||||
use super::*;
|
||||
|
||||
const PARAMETERS: Parameters = Parameters {
|
||||
kp: 0.055,
|
||||
ki: 0.005,
|
||||
kd: 0.04,
|
||||
kp: 0.03,
|
||||
ki: 0.002,
|
||||
kd: 0.15,
|
||||
output_min: -10.0,
|
||||
output_max: 10.0,
|
||||
integral_min: -100.0,
|
||||
integral_max: 100.0,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_controller() {
|
||||
const DEFAULT: f64 = 0.0;
|
||||
const TARGET: f64 = 1234.56;
|
||||
// Initial and ambient temperature
|
||||
const DEFAULT: f64 = 20.0;
|
||||
// Target temperature
|
||||
const TARGET: f64 = 40.0;
|
||||
// Control tolerance
|
||||
const ERROR: f64 = 0.01;
|
||||
// System response delay
|
||||
const DELAY: usize = 10;
|
||||
// Heat lost
|
||||
const LOSS: f64 = 0.05;
|
||||
// Limit simulation cycle, reaching this limit before settling fails test
|
||||
const CYCLE_LIMIT: u32 = 1000;
|
||||
|
||||
let mut pid = Controller::new(PARAMETERS.clone());
|
||||
pid.set_target(TARGET);
|
||||
pid.target = TARGET;
|
||||
|
||||
let mut values = [DEFAULT; DELAY];
|
||||
let mut t = 0;
|
||||
let mut total_t = 0;
|
||||
let mut output: f64 = 0.0;
|
||||
let target = (TARGET - ERROR)..=(TARGET + ERROR);
|
||||
while !values.iter().all(|value| target.contains(value)) {
|
||||
while !values.iter().all(|value| target.contains(value)) && total_t < CYCLE_LIMIT {
|
||||
let next_t = (t + 1) % DELAY;
|
||||
// Feed the oldest temperature
|
||||
let output = pid.update(values[next_t]);
|
||||
// Overwrite oldest with previous temperature + output
|
||||
values[next_t] = values[t] + output;
|
||||
output = pid.update(values[next_t]);
|
||||
// Overwrite oldest with previous temperature - output
|
||||
values[next_t] = values[t] - output - (values[t] - DEFAULT) * LOSS;
|
||||
t = next_t;
|
||||
total_t += 1;
|
||||
println!("{}", values[t].to_string());
|
||||
}
|
||||
dbg!(values[t], total_t);
|
||||
assert_ne!(CYCLE_LIMIT, total_t);
|
||||
}
|
||||
}
|
||||
|
382
src/pins.rs
382
src/pins.rs
@ -1,83 +1,129 @@
|
||||
use crate::{
|
||||
channel::{Channel0, Channel1},
|
||||
fan_ctrl::FanPin,
|
||||
hw_rev::{HWRev, HWSettings},
|
||||
leds::Leds,
|
||||
};
|
||||
use eeprom24x::{self, Eeprom24x};
|
||||
use stm32_eth::EthPins;
|
||||
use stm32f4xx_hal::{
|
||||
adc::Adc,
|
||||
hal::{blocking::spi::Transfer, digital::v2::OutputPin},
|
||||
gpio::{
|
||||
AF5, Alternate, Analog,
|
||||
gpioa::*,
|
||||
gpiob::*,
|
||||
gpioc::*,
|
||||
gpioe::*,
|
||||
gpiof::*,
|
||||
gpiog::*,
|
||||
GpioExt,
|
||||
Output, PushPull,
|
||||
Speed::VeryHigh,
|
||||
gpioa::*, gpiob::*, gpioc::*, gpioe::*, gpiof::*, gpiog::*, Alternate, AlternateOD, Analog,
|
||||
Floating, GpioExt, Input, Output, PushPull, AF5,
|
||||
},
|
||||
hal::{self, blocking::spi::Transfer, digital::v2::OutputPin},
|
||||
i2c::I2c,
|
||||
otg_fs::USB,
|
||||
pac::{
|
||||
ADC1, GPIOA, GPIOB, GPIOC, GPIOD, GPIOE, GPIOF, GPIOG, I2C1, OTG_FS_DEVICE, OTG_FS_GLOBAL,
|
||||
OTG_FS_PWRCLK, SPI2, SPI4, SPI5, TIM1, TIM3, TIM8,
|
||||
},
|
||||
rcc::Clocks,
|
||||
pwm::{self, PwmChannels},
|
||||
spi::{Spi, NoMiso},
|
||||
stm32::{ADC1, ADC2, ADC3, GPIOA, GPIOB, GPIOC, GPIOE, GPIOF, GPIOG, SPI2, SPI4, SPI5, TIM1, TIM3},
|
||||
rcc::Clocks,
|
||||
spi::{NoMiso, Spi, TransferModeNormal},
|
||||
time::U32Ext,
|
||||
timer::Timer,
|
||||
};
|
||||
use crate::channel::{Channel0, Channel1};
|
||||
|
||||
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>>,
|
||||
PA7<Input<Floating>>,
|
||||
PB11<Input<Floating>>,
|
||||
PG13<Input<Floating>>,
|
||||
PB13<Input<Floating>>,
|
||||
PC4<Input<Floating>>,
|
||||
PC5<Input<Floating>>,
|
||||
>;
|
||||
|
||||
pub trait ChannelPins {
|
||||
type DacSpi: Transfer<u8>;
|
||||
type DacSync: OutputPin;
|
||||
type Shdn: OutputPin;
|
||||
type Adc;
|
||||
type VRefPin;
|
||||
type ItecPin;
|
||||
type ITecPin;
|
||||
type DacFeedbackPin;
|
||||
type TecUMeasPin;
|
||||
}
|
||||
|
||||
pub enum Channel0VRef {
|
||||
Analog(PA0<Analog>),
|
||||
Disabled(PA0<Input<Floating>>),
|
||||
}
|
||||
|
||||
impl ChannelPins for Channel0 {
|
||||
type DacSpi = Dac0Spi;
|
||||
type DacSync = PE4<Output<PushPull>>;
|
||||
type Shdn = PE10<Output<PushPull>>;
|
||||
type Adc = Adc<ADC1>;
|
||||
type VRefPin = PA0<Analog>;
|
||||
type ItecPin = PA6<Analog>;
|
||||
type VRefPin = Channel0VRef;
|
||||
type ITecPin = PA6<Analog>;
|
||||
type DacFeedbackPin = PA4<Analog>;
|
||||
type TecUMeasPin = PC2<Analog>;
|
||||
}
|
||||
|
||||
pub enum Channel1VRef {
|
||||
Analog(PA3<Analog>),
|
||||
Disabled(PA3<Input<Floating>>),
|
||||
}
|
||||
|
||||
impl ChannelPins for Channel1 {
|
||||
type DacSpi = Dac1Spi;
|
||||
type DacSync = PF6<Output<PushPull>>;
|
||||
type Shdn = PE15<Output<PushPull>>;
|
||||
type Adc = Adc<ADC2>;
|
||||
type VRefPin = PA3<Analog>;
|
||||
type ItecPin = PB0<Analog>;
|
||||
type VRefPin = Channel1VRef;
|
||||
type ITecPin = PB0<Analog>;
|
||||
type DacFeedbackPin = PA5<Analog>;
|
||||
type TecUMeasPin = PC3<Analog>;
|
||||
}
|
||||
|
||||
/// SPI peripheral used for communication with the ADC
|
||||
pub type AdcSpi = Spi<SPI2, (PB10<Alternate<AF5>>, PB14<Alternate<AF5>>, PB15<Alternate<AF5>>)>;
|
||||
pub type AdcSpi = Spi<
|
||||
SPI2,
|
||||
(
|
||||
PB10<Alternate<AF5>>,
|
||||
PB14<Alternate<AF5>>,
|
||||
PB15<Alternate<AF5>>,
|
||||
),
|
||||
TransferModeNormal,
|
||||
>;
|
||||
pub type AdcNss = PB12<Output<PushPull>>;
|
||||
type Dac0Spi = Spi<SPI4, (PE2<Alternate<AF5>>, NoMiso, PE6<Alternate<AF5>>)>;
|
||||
type Dac1Spi = Spi<SPI5, (PF7<Alternate<AF5>>, NoMiso, PF9<Alternate<AF5>>)>;
|
||||
|
||||
pub type TecUMeasAdc = Adc<ADC3>;
|
||||
type Dac0Spi = Spi<SPI4, (PE2<Alternate<AF5>>, NoMiso, PE6<Alternate<AF5>>), TransferModeNormal>;
|
||||
type Dac1Spi = Spi<SPI5, (PF7<Alternate<AF5>>, NoMiso, PF9<Alternate<AF5>>), TransferModeNormal>;
|
||||
pub type PinsAdc = Adc<ADC1>;
|
||||
|
||||
pub struct ChannelPinSet<C: ChannelPins> {
|
||||
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 itec_pin: C::ITecPin,
|
||||
pub dac_feedback_pin: C::DacFeedbackPin,
|
||||
pub tec_u_meas_pin: C::TecUMeasPin,
|
||||
}
|
||||
|
||||
pub struct HWRevPins {
|
||||
pub hwrev0: stm32f4xx_hal::gpio::gpiod::PD0<Input<Floating>>,
|
||||
pub hwrev1: stm32f4xx_hal::gpio::gpiod::PD1<Input<Floating>>,
|
||||
pub hwrev2: stm32f4xx_hal::gpio::gpiod::PD2<Input<Floating>>,
|
||||
pub hwrev3: stm32f4xx_hal::gpio::gpiod::PD3<Input<Floating>>,
|
||||
}
|
||||
|
||||
pub struct Pins {
|
||||
pub adc_spi: AdcSpi,
|
||||
pub adc_nss: AdcNss,
|
||||
pub tec_u_meas_adc: TecUMeasAdc,
|
||||
pub pins_adc: PinsAdc,
|
||||
pub pwm: PwmPins,
|
||||
pub channel0: ChannelPinSet<Channel0>,
|
||||
pub channel1: ChannelPinSet<Channel1>,
|
||||
@ -87,44 +133,71 @@ impl Pins {
|
||||
/// Setup GPIO pins and configure MCU peripherals
|
||||
pub fn setup(
|
||||
clocks: Clocks,
|
||||
tim1: TIM1, tim3: TIM3,
|
||||
gpioa: GPIOA, gpiob: GPIOB, gpioc: GPIOC, gpioe: GPIOE, gpiof: GPIOF, gpiog: GPIOG,
|
||||
spi2: SPI2, spi4: SPI4, spi5: SPI5,
|
||||
adc1: ADC1, adc2: ADC2, adc3: ADC3,
|
||||
) -> Self {
|
||||
(tim1, tim3, tim8): (TIM1, TIM3, TIM8),
|
||||
(gpioa, gpiob, gpioc, gpiod, gpioe, gpiof, gpiog): (
|
||||
GPIOA,
|
||||
GPIOB,
|
||||
GPIOC,
|
||||
GPIOD,
|
||||
GPIOE,
|
||||
GPIOF,
|
||||
GPIOG,
|
||||
),
|
||||
i2c1: I2C1,
|
||||
(spi2, spi4, spi5): (SPI2, SPI4, SPI5),
|
||||
adc1: ADC1,
|
||||
(otg_fs_global, otg_fs_device, otg_fs_pwrclk): (
|
||||
OTG_FS_GLOBAL,
|
||||
OTG_FS_DEVICE,
|
||||
OTG_FS_PWRCLK,
|
||||
),
|
||||
) -> (
|
||||
Self,
|
||||
Leds,
|
||||
Eeprom,
|
||||
EthernetPins,
|
||||
USB,
|
||||
Option<FanPin>,
|
||||
HWRev,
|
||||
HWSettings,
|
||||
) {
|
||||
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 tec_u_meas_adc = Adc::adc3(adc3, true, Default::default());
|
||||
let pins_adc = Adc::adc1(adc1, true, Default::default());
|
||||
|
||||
let pwm = PwmPins::setup(
|
||||
clocks, tim1, tim3,
|
||||
gpioc.pc6, gpioc.pc7,
|
||||
gpioe.pe9, gpioe.pe11,
|
||||
gpioe.pe13, gpioe.pe14
|
||||
clocks,
|
||||
(tim1, tim3),
|
||||
(gpioc.pc6, gpioc.pc7),
|
||||
(gpioe.pe9, gpioe.pe11),
|
||||
(gpioe.pe13, gpioe.pe14),
|
||||
);
|
||||
|
||||
let (dac0_spi, dac0_sync) = Self::setup_dac0(
|
||||
clocks, spi4,
|
||||
gpioe.pe2, gpioe.pe4, gpioe.pe6
|
||||
);
|
||||
let hwrev = HWRev::detect_hw_rev(&HWRevPins {
|
||||
hwrev0: gpiod.pd0,
|
||||
hwrev1: gpiod.pd1,
|
||||
hwrev2: gpiod.pd2,
|
||||
hwrev3: gpiod.pd3,
|
||||
});
|
||||
let hw_settings = hwrev.settings();
|
||||
|
||||
let (dac0_spi, dac0_sync) = Self::setup_dac0(clocks, spi4, gpioe.pe2, gpioe.pe4, gpioe.pe6);
|
||||
let mut shdn0 = gpioe.pe10.into_push_pull_output();
|
||||
let _ = shdn0.set_low();
|
||||
let mut adc0 = Adc::adc1(adc1, true, Default::default());
|
||||
adc0.enable();
|
||||
let vref0_pin = gpioa.pa0.into_analog();
|
||||
shdn0.set_low();
|
||||
let vref0_pin = if hwrev.major > 2 {
|
||||
Channel0VRef::Analog(gpioa.pa0.into_analog())
|
||||
} else {
|
||||
Channel0VRef::Disabled(gpioa.pa0)
|
||||
};
|
||||
let itec0_pin = gpioa.pa6.into_analog();
|
||||
let dac_feedback0_pin = gpioa.pa4.into_analog();
|
||||
let tec_u_meas0_pin = gpioc.pc2.into_analog();
|
||||
@ -132,22 +205,20 @@ 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,
|
||||
tec_u_meas_pin: tec_u_meas0_pin,
|
||||
};
|
||||
|
||||
let (dac1_spi, dac1_sync) = Self::setup_dac1(
|
||||
clocks, spi5,
|
||||
gpiof.pf7, gpiof.pf6, gpiof.pf9
|
||||
);
|
||||
let (dac1_spi, dac1_sync) = Self::setup_dac1(clocks, spi5, gpiof.pf7, gpiof.pf6, gpiof.pf9);
|
||||
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();
|
||||
shdn1.set_low();
|
||||
let vref1_pin = if hwrev.major > 2 {
|
||||
Channel1VRef::Analog(gpioa.pa3.into_analog())
|
||||
} else {
|
||||
Channel1VRef::Disabled(gpioa.pa3)
|
||||
};
|
||||
let itec1_pin = gpiob.pb0.into_analog();
|
||||
let dac_feedback1_pin = gpioa.pa5.into_analog();
|
||||
let tec_u_meas1_pin = gpioc.pc3.into_analog();
|
||||
@ -155,20 +226,61 @@ 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,
|
||||
};
|
||||
|
||||
Pins {
|
||||
adc_spi, adc_nss,
|
||||
tec_u_meas_adc,
|
||||
let pins = Pins {
|
||||
adc_spi,
|
||||
adc_nss,
|
||||
pins_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().set_open_drain();
|
||||
let eeprom_sda = gpiob.pb9.into_alternate().set_open_drain();
|
||||
let eeprom_i2c = I2c::new(i2c1, (eeprom_scl, eeprom_sda), 400.khz(), clocks);
|
||||
let eeprom = Eeprom24x::new_24x02(eeprom_i2c, eeprom24x::SlaveAddr::default());
|
||||
|
||||
let eth_pins = EthPins {
|
||||
ref_clk: gpioa.pa1,
|
||||
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(),
|
||||
pin_dp: gpioa.pa12.into_alternate(),
|
||||
hclk: clocks.hclk(),
|
||||
};
|
||||
|
||||
let fan = if hw_settings.fan_available {
|
||||
Some(
|
||||
Timer::new(tim8, &clocks)
|
||||
.pwm(gpioc.pc9.into_alternate(), hw_settings.fan_pwm_freq_hz.hz()),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
(pins, leds, eeprom, eth_pins, usb, fan, hwrev, hw_settings)
|
||||
}
|
||||
|
||||
/// Configure the GPIO pins for SPI operation, and initialize SPI
|
||||
@ -178,32 +290,34 @@ impl Pins {
|
||||
sck: PB10<M1>,
|
||||
miso: PB14<M2>,
|
||||
mosi: PB15<M3>,
|
||||
) -> AdcSpi
|
||||
{
|
||||
let sck = sck.into_alternate_af5();
|
||||
let miso = miso.into_alternate_af5();
|
||||
let mosi = mosi.into_alternate_af5();
|
||||
Spi::spi2(
|
||||
) -> AdcSpi {
|
||||
let sck = sck.into_alternate();
|
||||
let miso = miso.into_alternate();
|
||||
let mosi = mosi.into_alternate();
|
||||
Spi::new(
|
||||
spi2,
|
||||
(sck, miso, mosi),
|
||||
crate::ad7172::SPI_MODE,
|
||||
crate::ad7172::SPI_CLOCK.into(),
|
||||
clocks
|
||||
crate::ad7172::SPI_CLOCK,
|
||||
clocks,
|
||||
)
|
||||
}
|
||||
|
||||
fn setup_dac0<M1, M2, M3>(
|
||||
clocks: Clocks, spi4: SPI4,
|
||||
sclk: PE2<M1>, sync: PE4<M2>, sdin: PE6<M3>
|
||||
) -> (Dac0Spi, PE4<Output<PushPull>>) {
|
||||
let sclk = sclk.into_alternate_af5();
|
||||
let sdin = sdin.into_alternate_af5();
|
||||
let spi = Spi::spi4(
|
||||
clocks: Clocks,
|
||||
spi4: SPI4,
|
||||
sclk: PE2<M1>,
|
||||
sync: PE4<M2>,
|
||||
sdin: PE6<M3>,
|
||||
) -> (Dac0Spi, <Channel0 as ChannelPins>::DacSync) {
|
||||
let sclk = sclk.into_alternate();
|
||||
let sdin = sdin.into_alternate();
|
||||
let spi = Spi::new(
|
||||
spi4,
|
||||
(sclk, NoMiso, sdin),
|
||||
(sclk, NoMiso {}, sdin),
|
||||
crate::ad5680::SPI_MODE,
|
||||
crate::ad5680::SPI_CLOCK.into(),
|
||||
clocks
|
||||
crate::ad5680::SPI_CLOCK,
|
||||
clocks,
|
||||
);
|
||||
let sync = sync.into_push_pull_output();
|
||||
|
||||
@ -211,48 +325,25 @@ impl Pins {
|
||||
}
|
||||
|
||||
fn setup_dac1<M1, M2, M3>(
|
||||
clocks: Clocks, spi5: SPI5,
|
||||
sclk: PF7<M1>, sync: PF6<M2>, sdin: PF9<M3>
|
||||
) -> (Dac1Spi, PF6<Output<PushPull>>) {
|
||||
let sclk = sclk.into_alternate_af5();
|
||||
let sdin = sdin.into_alternate_af5();
|
||||
let spi = Spi::spi5(
|
||||
clocks: Clocks,
|
||||
spi5: SPI5,
|
||||
sclk: PF7<M1>,
|
||||
sync: PF6<M2>,
|
||||
sdin: PF9<M3>,
|
||||
) -> (Dac1Spi, <Channel1 as ChannelPins>::DacSync) {
|
||||
let sclk = sclk.into_alternate();
|
||||
let sdin = sdin.into_alternate();
|
||||
let spi = Spi::new(
|
||||
spi5,
|
||||
(sclk, NoMiso, sdin),
|
||||
(sclk, NoMiso {}, sdin),
|
||||
crate::ad5680::SPI_MODE,
|
||||
crate::ad5680::SPI_CLOCK.into(),
|
||||
clocks
|
||||
crate::ad5680::SPI_CLOCK,
|
||||
clocks,
|
||||
);
|
||||
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 {
|
||||
@ -267,36 +358,43 @@ pub struct PwmPins {
|
||||
impl PwmPins {
|
||||
fn setup<M1, M2, M3, M4, M5, M6>(
|
||||
clocks: Clocks,
|
||||
tim1: TIM1,
|
||||
tim3: TIM3,
|
||||
max_v0: PC6<M1>,
|
||||
max_v1: PC7<M2>,
|
||||
max_i_pos0: PE9<M3>,
|
||||
max_i_pos1: PE11<M4>,
|
||||
max_i_neg0: PE13<M5>,
|
||||
max_i_neg1: PE14<M6>,
|
||||
(tim1, tim3): (TIM1, TIM3),
|
||||
(max_v0, max_v1): (PC6<M1>, PC7<M2>),
|
||||
(max_i_pos0, max_i_pos1): (PE9<M3>, PE11<M4>),
|
||||
(max_i_neg0, max_i_neg1): (PE13<M5>, PE14<M6>),
|
||||
) -> PwmPins {
|
||||
let freq = 20u32.khz();
|
||||
|
||||
let channels = (
|
||||
max_v0.into_alternate_af2(),
|
||||
max_v1.into_alternate_af2(),
|
||||
);
|
||||
let (max_v0, max_v1) = pwm::tim3(tim3, channels, clocks, freq);
|
||||
fn init_pwm_pin<P: hal::PwmPin<Duty = u16>>(pin: &mut P) {
|
||||
pin.set_duty(0);
|
||||
pin.enable();
|
||||
}
|
||||
let channels = (max_v0.into_alternate(), max_v1.into_alternate());
|
||||
//let (mut max_v0, mut max_v1) = pwm::tim3(tim3, channels, clocks, freq);
|
||||
let (mut max_v0, mut max_v1) = Timer::new(tim3, &clocks).pwm(channels, freq);
|
||||
init_pwm_pin(&mut max_v0);
|
||||
init_pwm_pin(&mut max_v1);
|
||||
|
||||
let channels = (
|
||||
max_i_pos0.into_alternate_af1(),
|
||||
max_i_pos1.into_alternate_af1(),
|
||||
max_i_neg0.into_alternate_af1(),
|
||||
max_i_neg1.into_alternate_af1(),
|
||||
max_i_pos0.into_alternate(),
|
||||
max_i_pos1.into_alternate(),
|
||||
max_i_neg0.into_alternate(),
|
||||
max_i_neg1.into_alternate(),
|
||||
);
|
||||
let (max_i_pos0, max_i_pos1, max_i_neg0, max_i_neg1) =
|
||||
pwm::tim1(tim1, channels, clocks, freq);
|
||||
let (mut max_i_pos0, mut max_i_pos1, mut max_i_neg0, mut max_i_neg1) =
|
||||
Timer::new(tim1, &clocks).pwm(channels, freq);
|
||||
init_pwm_pin(&mut max_i_pos0);
|
||||
init_pwm_pin(&mut max_i_neg0);
|
||||
init_pwm_pin(&mut max_i_pos1);
|
||||
init_pwm_pin(&mut max_i_neg1);
|
||||
|
||||
PwmPins {
|
||||
max_v0, max_v1,
|
||||
max_i_pos0, max_i_pos1,
|
||||
max_i_neg0, max_i_neg1,
|
||||
max_v0,
|
||||
max_v1,
|
||||
max_i_pos0,
|
||||
max_i_pos1,
|
||||
max_i_neg0,
|
||||
max_i_neg1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +1,33 @@
|
||||
use core::mem::MaybeUninit;
|
||||
use crate::command_parser::Ipv4Config;
|
||||
use crate::net::split_ipv4_config;
|
||||
use smoltcp::{
|
||||
iface::EthernetInterface,
|
||||
socket::{SocketSet, SocketHandle, TcpSocket, TcpSocketBuffer, SocketRef},
|
||||
socket::{SocketHandle, SocketRef, SocketSet, TcpSocket, TcpSocketBuffer},
|
||||
time::Instant,
|
||||
wire::{IpAddress, IpCidr, Ipv4Address, Ipv4Cidr},
|
||||
};
|
||||
|
||||
|
||||
pub struct SocketState<S> {
|
||||
handle: SocketHandle,
|
||||
state: S,
|
||||
}
|
||||
|
||||
impl<'a, S: Default> SocketState<S> {
|
||||
fn new(
|
||||
sockets: &mut SocketSet<'a>,
|
||||
tcp_rx_storage: &'a mut [u8; TCP_RX_BUFFER_SIZE],
|
||||
tcp_tx_storage: &'a mut [u8; TCP_TX_BUFFER_SIZE],
|
||||
) -> SocketState<S> {
|
||||
let tcp_rx_buffer = TcpSocketBuffer::new(&mut tcp_rx_storage[..]);
|
||||
let tcp_tx_buffer = TcpSocketBuffer::new(&mut tcp_tx_storage[..]);
|
||||
let tcp_socket = TcpSocket::new(tcp_rx_buffer, tcp_tx_buffer);
|
||||
SocketState::<S> {
|
||||
handle: sockets.add(tcp_socket),
|
||||
state: S::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Number of server sockets and therefore concurrent client
|
||||
/// sessions. Many data structures in `Server::run()` correspond to
|
||||
/// this const.
|
||||
@ -22,39 +39,38 @@ const TCP_TX_BUFFER_SIZE: usize = 2048;
|
||||
/// Contains a number of server sockets that get all sent the same
|
||||
/// data (through `fmt::Write`).
|
||||
pub struct Server<'a, 'b, S> {
|
||||
net: EthernetInterface<'a, 'a, 'a, &'a mut stm32_eth::Eth<'static, 'static>>,
|
||||
sockets: SocketSet<'b, 'b, 'b>,
|
||||
net: EthernetInterface<'a, &'a mut stm32_eth::Eth<'static, 'static>>,
|
||||
sockets: SocketSet<'b>,
|
||||
states: [SocketState<S>; SOCKET_COUNT],
|
||||
}
|
||||
|
||||
impl<'a, 'b, S: Default> Server<'a, 'b, S> {
|
||||
/// Run a server with stack-allocated sockets
|
||||
pub fn run<F>(net: EthernetInterface<'a, 'a, 'a, &'a mut stm32_eth::Eth<'static, 'static>>, f: F)
|
||||
pub fn run<F>(net: EthernetInterface<'a, &'a mut stm32_eth::Eth<'static, 'static>>, f: F)
|
||||
where
|
||||
F: FnOnce(&mut Server<'a, '_, S>),
|
||||
{
|
||||
let mut sockets_storage: [_; SOCKET_COUNT] = Default::default();
|
||||
let mut sockets = SocketSet::new(&mut sockets_storage[..]);
|
||||
let mut states: [SocketState<S>; SOCKET_COUNT] = unsafe { MaybeUninit::uninit().assume_init() };
|
||||
|
||||
macro_rules! create_socket {
|
||||
($set:ident, $rx_storage:ident, $tx_storage:ident, $target:expr) => {
|
||||
macro_rules! create_rtx_storage {
|
||||
($rx_storage:ident, $tx_storage:ident) => {
|
||||
let mut $rx_storage = [0; TCP_RX_BUFFER_SIZE];
|
||||
let mut $tx_storage = [0; TCP_TX_BUFFER_SIZE];
|
||||
let tcp_rx_buffer = TcpSocketBuffer::new(&mut $rx_storage[..]);
|
||||
let tcp_tx_buffer = TcpSocketBuffer::new(&mut $tx_storage[..]);
|
||||
let tcp_socket = TcpSocket::new(tcp_rx_buffer, tcp_tx_buffer);
|
||||
$target = $set.add(tcp_socket);
|
||||
}
|
||||
};
|
||||
}
|
||||
create_socket!(sockets, tcp_rx_storage0, tcp_tx_storage0, states[0].handle);
|
||||
create_socket!(sockets, tcp_rx_storage1, tcp_tx_storage1, states[1].handle);
|
||||
create_socket!(sockets, tcp_rx_storage2, tcp_tx_storage2, states[2].handle);
|
||||
create_socket!(sockets, tcp_rx_storage3, tcp_tx_storage3, states[3].handle);
|
||||
|
||||
for state in &mut states {
|
||||
state.state = S::default();
|
||||
}
|
||||
create_rtx_storage!(tcp_rx_storage0, tcp_tx_storage0);
|
||||
create_rtx_storage!(tcp_rx_storage1, tcp_tx_storage1);
|
||||
create_rtx_storage!(tcp_rx_storage2, tcp_tx_storage2);
|
||||
create_rtx_storage!(tcp_rx_storage3, tcp_tx_storage3);
|
||||
|
||||
let mut sockets_storage: [_; SOCKET_COUNT] = Default::default();
|
||||
let mut sockets = SocketSet::new(&mut sockets_storage[..]);
|
||||
|
||||
let states: [SocketState<S>; SOCKET_COUNT] = [
|
||||
SocketState::<S>::new(&mut sockets, &mut tcp_rx_storage0, &mut tcp_tx_storage0),
|
||||
SocketState::<S>::new(&mut sockets, &mut tcp_rx_storage1, &mut tcp_tx_storage1),
|
||||
SocketState::<S>::new(&mut sockets, &mut tcp_rx_storage2, &mut tcp_tx_storage2),
|
||||
SocketState::<S>::new(&mut sockets, &mut tcp_rx_storage3, &mut tcp_tx_storage3),
|
||||
];
|
||||
|
||||
let mut server = Server {
|
||||
states,
|
||||
@ -83,4 +99,34 @@ impl<'a, 'b, S: Default> Server<'a, 'b, S> {
|
||||
callback(socket, &mut state.state);
|
||||
}
|
||||
}
|
||||
|
||||
fn set_ipv4_address(&mut self, ipv4_address: Ipv4Cidr) {
|
||||
self.net.update_ip_addrs(|addrs| {
|
||||
for addr in addrs.iter_mut() {
|
||||
if let IpCidr::Ipv4(_) = addr {
|
||||
*addr = IpCidr::Ipv4(ipv4_address);
|
||||
// done
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn set_gateway(&mut self, gateway: Option<Ipv4Address>) {
|
||||
let routes = self.net.routes_mut();
|
||||
match gateway {
|
||||
None => routes.update(|routes_storage| {
|
||||
routes_storage.remove(&IpCidr::new(IpAddress::v4(0, 0, 0, 0), 0));
|
||||
}),
|
||||
Some(gateway) => {
|
||||
routes.add_default_ipv4_route(gateway).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_ipv4_config(&mut self, config: Ipv4Config) {
|
||||
let (address, gateway) = split_ipv4_config(config);
|
||||
self.set_ipv4_address(address);
|
||||
self.set_gateway(gateway);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
use super::command_parser::{Command, Error as ParserError};
|
||||
use super::channels::CHANNELS;
|
||||
|
||||
const MAX_LINE_LEN: usize = 64;
|
||||
|
||||
@ -38,23 +37,22 @@ impl LineReader {
|
||||
}
|
||||
}
|
||||
|
||||
pub enum SessionOutput {
|
||||
pub enum SessionInput {
|
||||
Nothing,
|
||||
Command(Command),
|
||||
Error(ParserError),
|
||||
}
|
||||
|
||||
impl From<Result<Command, ParserError>> for SessionOutput {
|
||||
impl From<Result<Command, ParserError>> for SessionInput {
|
||||
fn from(input: Result<Command, ParserError>) -> Self {
|
||||
input.map(SessionOutput::Command)
|
||||
.unwrap_or_else(SessionOutput::Error)
|
||||
input
|
||||
.map(SessionInput::Command)
|
||||
.unwrap_or_else(SessionInput::Error)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Session {
|
||||
reader: LineReader,
|
||||
reporting: bool,
|
||||
report_pending: [bool; CHANNELS],
|
||||
}
|
||||
|
||||
impl Default for Session {
|
||||
@ -67,64 +65,23 @@ impl Session {
|
||||
pub fn new() -> Self {
|
||||
Session {
|
||||
reader: LineReader::new(),
|
||||
reporting: false,
|
||||
report_pending: [false; CHANNELS],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.reader = LineReader::new();
|
||||
self.reporting = false;
|
||||
self.report_pending = [false; CHANNELS];
|
||||
}
|
||||
|
||||
pub fn reporting(&self) -> bool {
|
||||
self.reporting
|
||||
}
|
||||
|
||||
pub fn set_report_pending(&mut self, channel: usize) {
|
||||
if self.reporting {
|
||||
self.report_pending[channel] = true;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_report_pending(&self) -> Option<usize> {
|
||||
if ! self.reporting {
|
||||
None
|
||||
} else {
|
||||
self.report_pending.iter()
|
||||
.enumerate()
|
||||
.fold(None, |result, (channel, report_pending)| {
|
||||
result.or_else(|| {
|
||||
if *report_pending { Some(channel) } else { None }
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mark_report_sent(&mut self, channel: usize) {
|
||||
self.report_pending[channel] = false;
|
||||
}
|
||||
|
||||
pub fn feed(&mut self, buf: &[u8]) -> (usize, SessionOutput) {
|
||||
pub fn feed(&mut self, buf: &[u8]) -> (usize, SessionInput) {
|
||||
let mut buf_bytes = 0;
|
||||
for (i, b) in buf.iter().enumerate() {
|
||||
buf_bytes = i + 1;
|
||||
let line = self.reader.feed(*b);
|
||||
match line {
|
||||
Some(line) => {
|
||||
let command = Command::parse(&line);
|
||||
match command {
|
||||
Ok(Command::Reporting(reporting)) => {
|
||||
self.reporting = reporting;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
return (buf_bytes, command.into());
|
||||
}
|
||||
None => {}
|
||||
if let Some(line) = line {
|
||||
let command = Command::parse(line);
|
||||
return (buf_bytes, command.into());
|
||||
}
|
||||
}
|
||||
(buf_bytes, SessionOutput::Nothing)
|
||||
(buf_bytes, SessionInput::Nothing)
|
||||
}
|
||||
}
|
||||
|
@ -1,31 +0,0 @@
|
||||
use num_traits::float::Float;
|
||||
|
||||
/// Steinhart-Hart equation parameters
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Parameters {
|
||||
pub t0: f64,
|
||||
pub b: f64,
|
||||
pub r0: f64,
|
||||
}
|
||||
|
||||
impl Parameters {
|
||||
/// Perform the voltage to temperature conversion.
|
||||
///
|
||||
/// 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: 0.001_4,
|
||||
b: 0.000_000_099,
|
||||
r0: 5_110.0,
|
||||
}
|
||||
}
|
||||
}
|
24
src/timer.rs
24
src/timer.rs
@ -4,9 +4,9 @@ use cortex_m::interrupt::Mutex;
|
||||
use cortex_m_rt::exception;
|
||||
use stm32f4xx_hal::{
|
||||
rcc::Clocks,
|
||||
time::U32Ext,
|
||||
timer::{Timer, Event as TimerEvent},
|
||||
stm32::SYST,
|
||||
time::U32Ext,
|
||||
timer::{Event as TimerEvent, Timer},
|
||||
};
|
||||
|
||||
/// Rate in Hz
|
||||
@ -18,24 +18,26 @@ static TIMER_MS: Mutex<RefCell<u32>> = Mutex::new(RefCell::new(0));
|
||||
|
||||
/// Setup SysTick exception
|
||||
pub fn setup(syst: SYST, clocks: Clocks) {
|
||||
let mut timer = Timer::syst(syst, TIMER_RATE.hz(), clocks);
|
||||
timer.listen(TimerEvent::TimeOut);
|
||||
let timer = Timer::syst(syst, &clocks);
|
||||
let mut countdown = timer.start_count_down(TIMER_RATE.hz());
|
||||
countdown.listen(TimerEvent::TimeOut);
|
||||
}
|
||||
|
||||
/// SysTick exception (Timer)
|
||||
#[exception]
|
||||
fn SysTick() {
|
||||
cortex_m::interrupt::free(|cs| {
|
||||
*TIMER_MS.borrow(cs)
|
||||
.borrow_mut() += TIMER_DELTA;
|
||||
*TIMER_MS.borrow(cs).borrow_mut() += TIMER_DELTA;
|
||||
});
|
||||
}
|
||||
|
||||
/// Obtain current time in milliseconds
|
||||
pub fn now() -> u32 {
|
||||
cortex_m::interrupt::free(|cs| {
|
||||
*TIMER_MS.borrow(cs)
|
||||
.borrow()
|
||||
.deref()
|
||||
})
|
||||
cortex_m::interrupt::free(|cs| *TIMER_MS.borrow(cs).borrow().deref())
|
||||
}
|
||||
|
||||
/// block for at least `amount` milliseconds
|
||||
pub fn sleep(amount: u32) {
|
||||
let start = now();
|
||||
while now() - start <= amount {}
|
||||
}
|
||||
|
65
src/units.rs
65
src/units.rs
@ -1,65 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
105
src/usb.rs
Normal file
105
src/usb.rs
Normal file
@ -0,0 +1,105 @@
|
||||
use core::{
|
||||
fmt::{self, Write},
|
||||
mem::MaybeUninit,
|
||||
};
|
||||
use cortex_m::interrupt::free;
|
||||
use log::{Log, Metadata, Record};
|
||||
use stm32f4xx_hal::{
|
||||
otg_fs::{UsbBus as Bus, USB},
|
||||
stm32::{interrupt, Interrupt, NVIC},
|
||||
};
|
||||
use usb_device::{
|
||||
class_prelude::UsbBusAllocator,
|
||||
prelude::{UsbDevice, UsbDeviceBuilder, UsbVidPid},
|
||||
};
|
||||
use usbd_serial::SerialPort;
|
||||
|
||||
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