Compare commits

...

106 Commits

Author SHA1 Message Date
Joe Britton af0d78237f Clarify purpose of GND pin adjacent to TECn+/-. 2020-12-06 15:11:42 +08:00
Astro ae77a5c163 update cargoSha256 2020-10-30 15:50:14 +01:00
Astro ffb70bde0a command_parser: fix input_remain match 2020-10-30 15:04:14 +01:00
Astro d517dd75fe update dependencies 2020-10-30 15:03:57 +01:00
Astro a943308203 pid: never reset 2020-10-13 23:55:22 +02:00
Astro 150d6c2f87 README: use frontpanel LED names 2020-10-12 00:26:49 +02:00
Astro c005784df5 steinhart_hart: rm outdated doc 2020-10-11 23:20:56 +02:00
Astro d574ccb5f4 pid: change signedness from heating to cooling 2020-10-11 23:12:18 +02:00
Astro 6ba1459a3c main: send error on invalid command 2020-10-11 23:11:27 +02:00
Astro aee279b579 README: doc 2020-10-11 21:50:36 +02:00
Astro 1939490410 README: fix syntax 2020-10-11 21:09:15 +02:00
Astro 2e9d22cf47 README: doc 2020-10-11 21:08:02 +02:00
Astro a332b5fcdc main: fix saving new_ipv4_address 2020-10-11 01:59:39 +02:00
Astro 83a266852a pid: move ki coefficient inside integration 2020-10-11 01:59:39 +02:00
Astro f12214a4df README: doc usb 2020-10-11 01:59:39 +02:00
Astro 175b88d0e6 s/tecpak/thermostat/g 2020-10-11 01:59:39 +02:00
Sebastien Bourdeauducq 9e23b14ace update cargosha256 2020-10-08 14:43:10 +08:00
Astro 6fd5328042 channels: rm debug output 2020-10-01 02:09:43 +02:00
Astro 59103cb2a1 add support for ipv4 address reconfiguration 2020-10-01 01:34:46 +02:00
Astro 5acebbef9f pytec: doc set_param 2020-10-01 00:35:16 +02:00
Astro 12e713dc19 init_log: bump max level to Debug for USB 2020-10-01 00:34:28 +02:00
Astro 1b4a030e7e pytec: more methods, doc 2020-10-01 00:21:43 +02:00
Astro 438da74721 main: remove all plain-text responses 2020-09-30 23:53:13 +02:00
Astro 026dd1ed9c main: close tx half of sockets with closed rx half
fixes Gitea issue #9
2020-09-30 23:39:31 +02:00
Astro d4e7036fab session: rename SessionOutput to SessionInput 2020-09-30 23:35:11 +02:00
Astro bfdb64ffd6 pytec: add configuration getters 2020-09-30 23:13:11 +02:00
Astro 6e0cf26d6a export postfilter + s-h as json 2020-09-30 22:53:21 +02:00
Astro d4901cbab1 channels: add dac_value to Report 2020-09-30 22:53:03 +02:00
Astro 62d89a68a1 pwm: export summary as json 2020-09-30 22:10:42 +02:00
Astro 5521563c91 pid: export summary as json 2020-09-30 20:06:47 +02:00
Astro 11f2ebe961 channels: DRY get_center(), update vref if used 2020-09-30 19:14:15 +02:00
Astro 4b75c6147d pytec plot: update legend, rm debug add pid_output 2020-09-30 19:13:50 +02:00
Astro 445cde6ae8 channels: add test report_to_json 2020-09-30 18:01:18 +02:00
Astro 6951489545 channels: add pid_output to Report 2020-09-30 18:00:16 +02:00
Astro 97490e5e1b pytec: init 2020-09-29 02:52:46 +02:00
Astro 87287e83b3 update cargoSha256 2020-09-28 13:16:54 +02:00
Astro 407c0998af ad7172: set output data rate to 10 Hz 2020-09-28 01:24:32 +02:00
Astro 9e5a58cafd main: switch reports to json serialization 2020-09-27 23:58:03 +02:00
Astro aea306cf17 config: save postfilter setting 2020-09-26 01:40:01 +02:00
Astro 97a09e422b main: add support for disabling postfilters 2020-09-26 01:29:35 +02:00
Astro 61d2cd6ecf channels: init i_set at centerpoint 2020-09-25 22:56:23 +02:00
Astro f3ec96f425 channels: fix doc 2020-09-25 22:55:48 +02:00
Astro 9f70ef2e0a main: power_down channels before sys_reset 2020-09-25 22:24:43 +02:00
Astro 83589610b5 implement reset command 2020-09-25 00:14:29 +02:00
Astro a2caac0fe5 channels: power_down TEC when thermistor is not connected to ADC 2020-09-25 00:01:08 +02:00
Astro 20059aff5c channel_state: recognize unplugged thermistor 2020-09-24 23:34:09 +02:00
Astro f690599f9e config: update tests 2020-09-24 23:21:54 +02:00
Astro b3e9a1b636 init_log: set USB_LOGGER max level 2020-09-24 23:11:23 +02:00
Astro bfbf037006 config: add load/save code 2020-09-24 23:10:47 +02:00
Astro a1ad9b2456 main: load config from eeprom on boot 2020-09-24 23:08:42 +02:00
Astro 8d70c03520 config: finalize load/save 2020-09-24 23:04:29 +02:00
Astro 3b050347d4 config: add test_encode_decode 2020-09-24 21:35:15 +02:00
Astro 254c1c3d73 remove now unneeded uom feature use_serde 2020-09-24 21:33:42 +02:00
Astro 5a293a0ada config: convert steinhart_hart::Parameters to SteinhartHartConfig for f32 storage 2020-09-24 21:32:56 +02:00
Astro daa398cb5e config: add pwm limits 2020-09-24 21:10:27 +02:00
Astro 17e89b2041 config: add test_fit_eeprom 2020-09-24 20:59:04 +02:00
Astro 58e648b5e0 pid::Parameters, CenterPoint: demote f32 fields to save config space 2020-09-24 20:47:02 +02:00
Astro c5c0ce5625 channels: rm debug output 2020-09-24 19:49:11 +02:00
Sebastien Bourdeauducq d3606d25b6 fix imports 2020-09-24 15:49:13 +08:00
Sebastien Bourdeauducq 27278db1ba update cargoSha256 2020-09-24 15:49:03 +08:00
Astro d40a038c2f config: switch to postcard encoding 2020-09-24 02:06:53 +02:00
Astro 5d0d75d395 config: encode with serde_cbor 2020-09-24 01:18:33 +02:00
Astro 93f14523d7 command_parser: parse load/save 2020-09-24 01:17:50 +02:00
Astro 201701ee8b implement mac address generation with reading eui48 from i2c eeprom 2020-09-24 00:19:07 +02:00
Astro a84242fb1f implement setting i_set centerpoint 2020-09-23 22:30:04 +02:00
Astro b394cfa3d4 main: remove special handling for PwmPin::ISet 2020-09-23 20:50:50 +02:00
Astro b9902929a9 test: #[allow(unused)] 2020-09-18 01:22:41 +02:00
Astro edc675f5af command_parser: rename a test 2020-09-18 01:16:19 +02:00
Astro a4dde1b8ca delint 2020-09-18 00:55:53 +02:00
Astro 7361619a53 pid: update default parameters 2020-09-18 00:41:32 +02:00
Astro 34543c8660 pid: only reset after target change 2020-09-18 00:24:00 +02:00
Astro 83a209397e fix tests 2020-09-18 00:23:30 +02:00
Astro ba84295ec5 reconnect the pid controller 2020-09-18 00:09:30 +02:00
Astro fb81380955 fix tests
run with `cargo test --target=x86_64-unknown-linux-gnu`
2020-09-17 01:48:27 +02:00
Astro 94e0525002 tec_u_meas: subtract vref offset 2020-09-17 01:20:47 +02:00
Astro 1157b73f7f max_i_pos/max_i_neg: remove vref from calculation
full duty pwm is at vref already
2020-09-17 01:03:57 +02:00
Astro dd06ae1075 main: improve output 2020-09-16 23:32:48 +02:00
Astro f76ee9a607 separate adc and max vref 2020-09-16 23:31:49 +02:00
Astro fc0ca8b581 calculate i_set current 2020-09-16 22:22:48 +02:00
Astro 8c80062da8 use proper units for pwm pins 2020-09-16 22:06:15 +02:00
Astro 7d45d5ad32 adc: complete temperature calculation 2020-09-16 20:50:23 +02:00
Astro dda1f2f0b4 use ThermodynamicTemperature 2020-09-16 18:40:07 +02:00
Astro b1b6d1ea94 fixes 2020-09-14 00:12:28 +02:00
Astro 1849e6f5e7 cargosha256.nix: update 2020-09-13 23:24:44 +02:00
Astro b80fcc430b channel_state: move adc_calibration into ChannelState 2020-09-13 23:15:48 +02:00
Astro bb26490153 unit: replace with uom 2020-09-13 23:13:51 +02:00
Astro 4a1ce342a0 pins: enable pwm pins 2020-09-12 00:35:58 +02:00
Sebastien Bourdeauducq 8cddbc5173 update cargosha256 2020-09-11 18:38:12 +08:00
Astro 37a7898f92 cargosha256.nix: update 2020-09-11 02:07:17 +02:00
Astro 277f239ed7 cargosha256.nix: update 2020-09-10 23:44:14 +02:00
Astro 10208e1ac0 main: bump WATCHDOG_INTERVAL to 1s
required for running Channels.calibrate_dac_value()
2020-09-10 23:28:00 +02:00
Astro 42587810cd usb: add serial logger 2020-09-10 23:17:31 +02:00
Astro c11b71cc0d adc: don't calibrate but convert using ChannelCalibration 2020-09-09 23:10:33 +02:00
Astro 2617895460 update cargosha256 2020-09-06 22:59:12 +02:00
Astro 5244077144 update dependencies 2020-09-06 21:59:28 +02:00
Astro 4e6aa5fe0c leds: init 2020-09-06 21:10:10 +02:00
Astro 9a912392be channels: fix dac calibration 2020-09-06 19:28:33 +02:00
Astro 50dcee0c8a pins: reuse definitions 2020-09-06 19:28:33 +02:00
Astro f76ae453a9 ad5680: cap value to MAX_VALUE 2020-09-06 19:28:33 +02:00
Sebastien Bourdeauducq e5c9ee8ed0 update cargosha256 2020-09-04 12:16:31 +08:00
Astro a3df2bc685 update to newer stm32-eth 2020-09-03 21:38:56 +02:00
Astro 1711feae84 timer: define sleep() as pub 2020-05-31 19:54:18 +02:00
Astro f9b55508dd ad5680: keep sync high for 1ms 2020-05-28 20:45:42 +02:00
Astro cf03613ac5 main: fix peripheral params 2020-05-28 20:43:52 +02:00
Astro 3c94342448 replace tec_u_meas_adc with pins_adc 2020-05-28 02:06:32 +02:00
Astro 5418488a2f replace channel[01].adc with pins_adc 2020-05-28 02:01:55 +02:00
29 changed files with 2297 additions and 701 deletions

291
Cargo.lock generated
View File

@ -2,29 +2,30 @@
# It is not intended for manual editing.
[[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 +37,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"
@ -70,20 +83,21 @@ checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
[[package]]
name = "cortex-m"
version = "0.6.2"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2954942fbbdd49996704e6f048ce57567c3e1a4e2dc59b41ae9fde06a01fc763"
checksum = "88cdafeafba636c00c467ded7f1587210725a1adfab0c24028a7844b87738263"
dependencies = [
"aligned",
"bare-metal",
"bare-metal 0.2.5",
"bitfield",
"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-semihosting",
@ -92,9 +106,9 @@ dependencies = [
[[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",
"r0",
@ -121,12 +135,30 @@ dependencies = [
]
[[package]]
name = "embedded-hal"
version = "0.2.3"
name = "eeprom24x"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee4908a155094da7723c2d60d617b820061e3b4efcc3d9e293d206a5a76c170b"
checksum = "f680e8d81a559a97de04c5fab25f17f22a55770120c868ef8fbdea6398d44107"
dependencies = [
"nb",
"embedded-hal",
]
[[package]]
name = "embedded-dma"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46c8c02e4347a0267ca60813c952017f4c5948c232474c6010a381a337f1bda4"
dependencies = [
"stable_deref_trait",
]
[[package]]
name = "embedded-hal"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa998ce59ec9765d15216393af37a58961ddcefb14c753b4816ba2191d865fcb"
dependencies = [
"nb 0.1.3",
"void",
]
@ -149,10 +181,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,36 +220,45 @@ 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",
@ -199,9 +266,9 @@ dependencies = [
[[package]]
name = "num-traits"
version = "0.2.11"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c62be47e61d1842b9170f0fdeec8eba98e60e90e5446449a0545e5152acd7096"
checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
dependencies = [
"autocfg",
"libm",
@ -215,28 +282,45 @@ checksum = "4e20e6499bbbc412f280b04a42346b356c6fa0753d5fd22b7bd752ff34c778ee"
[[package]]
name = "panic-semihosting"
version = "0.5.3"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c03864ac862876c16a308f5286f4aa217f1a69ac45df87ad3cd2847f818a642c"
checksum = "aed16eb761d0ee9161dd1319cb38c8007813b20f9720a5a682b283e7b8cdfe58"
dependencies = [
"cortex-m",
"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",
]
@ -277,6 +361,36 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
[[package]]
name = "serde"
version = "1.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b88fa983de7720629c9387e9f517353ed404164b1e482c970a90c1a4aaf7dc1a"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde-json-core"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbf406405ada9ef326ca78677324ac66994ff348fc48a16030be08caeed29825"
dependencies = [
"heapless",
"serde",
]
[[package]]
name = "serde_derive"
version = "1.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbd1ae72adb44aab48f325a02444a5fc079349a8d804c1fc922aed3f7454c74e"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "smoltcp"
version = "0.6.0"
@ -291,17 +405,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#4d6b29bf1ecdd1f68e5bc304a3d4f170049896c8"
dependencies = [
"aligned",
"log",
"cortex-m",
"smoltcp",
"stm32f4xx-hal",
"volatile-register",
@ -309,11 +423,11 @@ dependencies = [
[[package]]
name = "stm32f4"
version = "0.10.0"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44a3d6c58b14e63926273694e7dd644894513c5e35ce6928c4657ddb62cae976"
checksum = "11460b4de3a84f072e2cf6e76306c64d27f405a0e83bace0a726f555ddf4bf33"
dependencies = [
"bare-metal",
"bare-metal 0.2.5",
"cortex-m",
"cortex-m-rt",
"vcell",
@ -321,63 +435,112 @@ dependencies = [
[[package]]
name = "stm32f4xx-hal"
version = "0.7.0"
source = "git+https://github.com/thalesfragoso/stm32f4xx-hal?branch=pwm-impl#cfd073e094daa9be9dd2b0a1f859a4e1c6be2b77"
version = "0.8.3"
source = "git+https://github.com/stm32-rs/stm32f4xx-hal.git#e80925770d2fe72f0f01a7b46147f4e31d512689"
dependencies = [
"bare-metal",
"bare-metal 0.2.5",
"cast",
"cortex-m",
"cortex-m-rt",
"embedded-dma",
"embedded-hal",
"nb",
"nb 0.1.3",
"rand_core",
"stm32f4",
"synopsys-usb-otg",
"void",
]
[[package]]
name = "syn"
version = "1.0.17"
version = "1.0.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0df0eb663f387145cab623dea85b09c2c5b4b0aef44e945d928e682fce71bb03"
checksum = "cc371affeffc477f42a221a1e4297aedcea33d47d19b61455588bd9d8f6b19ac"
dependencies = [
"proc-macro2",
"quote",
"unicode-xid",
]
[[package]]
name = "synopsys-usb-otg"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "461676dcf123675b3d3b02e2390e6a690cd186aacf2f439af7673c79e2561d53"
dependencies = [
"cortex-m",
"usb-device",
"vcell",
]
[[package]]
name = "thermostat"
version = "0.0.0"
dependencies = [
"bare-metal",
"bare-metal 1.0.0",
"bit_field",
"byteorder",
"cortex-m",
"cortex-m-log",
"cortex-m-rt",
"hash2hwaddr",
"eeprom24x",
"heapless",
"log",
"nb 1.0.0",
"nom",
"num-traits",
"panic-abort",
"panic-semihosting",
"postcard",
"serde",
"serde-json-core",
"smoltcp",
"stm32-eth",
"stm32f4xx-hal",
"uom",
"usb-device",
"usbd-serial",
]
[[package]]
name = "typenum"
version = "1.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 +550,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"

View File

@ -14,30 +14,36 @@ features = []
default-target = "thumbv7em-none-eabihf"
[dependencies]
panic-abort = "0.3.1"
panic-semihosting = { version = "0.5.1", optional = true }
panic-abort = "0.3"
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" }
stm32f4xx-hal = { version = "0.8", features = ["rt", "stm32f427", "usb_fs"] }
stm32-eth = { version = "0.2", features = ["stm32f427", "smoltcp-phy"], git = "https://github.com/stm32-rs/stm32-eth.git" }
smoltcp = { version = "0.6.0", default-features = false, features = ["proto-ipv4", "socket-tcp", "log"] }
hash2hwaddr = { version = "0.0", optional = true }
bit_field = "0.10"
byteorder = { version = "1", default-features = false }
nom = { version = "5", default-features = false }
num-traits = { version = "0.2", default-features = false, features = ["libm"] }
usb-device = "0.2"
usbd-serial = "0.1"
nb = "1"
uom = { version = "0.30", default-features = false, features = ["autoconvert", "si", "f64", "use_serde"] }
eeprom24x = "0.3"
serde = { version = "1.0", default-features = false, features = ["derive"] }
postcard = "0.5"
heapless = "0.5"
serde-json-core = "0.1"
[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" }
stm32f4xx-hal = { git = "https://github.com/stm32-rs/stm32f4xx-hal.git" }
[features]
semihosting = ["panic-semihosting", "cortex-m-log/semihosting"]
generate-hwaddr = ["hash2hwaddr"]
default = ["generate-hwaddr"]
[profile.release]
codegen-units = 1

169
README.md
View File

@ -46,26 +46,149 @@ The scope of this setting is per TCP session.
### Commands
| Syntax | Function |
| --- | --- |
| `report` | Show current input |
| `report mode` | Show current report mode |
| `report mode <off/on>` | Set report mode |
| `pwm` | Show current PWM settings |
| `pwm <0/1> max_i_pos <ratio>` | Set PWM duty cycle for **max_i_pos** to *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 |
| Syntax | Function |
| --- | --- |
| `report` | Show current input |
| `report mode` | Show current report mode |
| `report mode <off/on>` | Set report mode |
| `pwm` | Show current PWM settings |
| `pwm <0/1> max_i_pos <ratio>` | Set PWM duty cycle for **max_i_pos** to *ampere* |
| `pwm <0/1> max_i_neg <ratio>` | Set PWM duty cycle for **max_i_neg** to *ampere* |
| `pwm <0/1> max_v <ratio>` | Set PWM duty cycle for **max_v** to *volt* |
| `pwm <0/1> <volts>` | Disengage PID, set **i_set** DAC to *ampere* |
| `pwm <0/1> pid` | Set PWM to be controlled by PID |
| `center <0/1> <volts>` | Set the MAX1968 0A-centerpoint to *volts* |
| `center <0/1> vref` | Set the MAX1968 0A-centerpoint to measure from VREF |
| `pid` | Show PID configuration |
| `pid <0/1> target <value>` | Set the PID controller target |
| `pid <0/1> kp <value>` | Set proportional gain |
| `pid <0/1> ki <value>` | Set integral gain |
| `pid <0/1> kd <value>` | Set differential gain |
| `pid <0/1> output_min <value>` | Set mininum output |
| `pid <0/1> output_max <value>` | Set maximum output |
| `pid <0/1> integral_min <value>` | Set integral lower bound |
| `pid <0/1> integral_max <value>` | Set integral upper bound |
| `s-h` | Show Steinhart-Hart equation parameters |
| `s-h <0/1> <t0/b/r0> <value>` | Set Steinhart-Hart parameter for a channel |
| `postfilter` | Show postfilter settings |
| `postfilter <0/1> off` | Disable postfilter |
| `postfilter <0/1> rate <rate>` | Set postfilter output data rate |
| `load` | Restore configuration from EEPROM |
| `save` | Save configuration to EEPROM |
| `reset` | Reset the device |
| `ipv4 <X.X.X.X>` | Configure IPv4 address |
## USB
The firmware includes experimental support for acting as a USB-Serial
peripheral. Debug logging will be sent there by default (unless build
with logging via semihosting.)
**Caveat:** This logging does not flush its output. Doing so would
hang indefinitely if the output is not read by the USB host. Therefore
output will be truncated once buffers are full.
## Temperature measurement
Connect the thermistor with the SENS pins of the
device. Temperature-depending resistance is measured by the AD7172
ADC. To prepare conversion to a temperature, set the Beta parameters
for the Steinhart-Hart equation.
Set the base temperature in degrees celsius for the channel 0 thermistor:
```
s-h 0 t0 20
```
Set the resistance in Ohms measured at the base temperature t0:
```
s-h 0 r0 10000
```
Set the Beta parameter:
```
s-h 0 b 3800
```
## Thermo-Electric Cooling (TEC)
- Connect Peltier device 0 to TEC0- and TEC0+.
- Connect Peliter device 1 to TEC1- and TEC1+.
- The GND pin is for shielding not for sinking Peltier currents.
### Limits
Each of the MAX1968 TEC driver has analog/PWM inputs for setting
output limits.
Use the `pwm` command to see current settings and maximum values.
| Limit | Unit | Description |
| --- | :---: | --- |
| `max_v` | Volts | Maximum voltage |
| `max_i_pos` | Amperes | Maximum positive current |
| `max_i_neg` | Amperes | Maximum negative current |
| | Amperes | Output current control |
Example: set the maximum voltage of channel 0 to 1.5 V.
```
pwm 0 max_v 1.5
```
### Open-loop mode
To manually control TEC output current, omit the limit parameter of
the `pwm` command. Doing so will disengage the PID control for that
channel.
Example: set output current of channel 0 to 0 A.
```
pwm 0 0
```
## PID-stabilized temperature control
Set the target temperature of channel 0 to 20 degrees celsius:
```
pid 0 target 20
```
Enter closed-loop mode by switching control of the TEC output current
of channel 0 to the PID algorithm:
```
pwm 0 pid
```
## LED indicators
| Name | Color | Meaning |
| --- | :---: | --- |
| L1 | Red | Firmware initializing |
| L3 | Green | Closed-loop mode (PID engaged) |
| L4 | Green | Firmware busy |
## Reports
Use the bare `report` command to obtain a single report. Enable
continuous reporting with `report mode on`. Reports are JSON objects
with the following keys.
| Key | Unit | Description |
| --- | :---: | --- |
| `channel` | Integer | Channel `0`, or `1` |
| `time` | Milliseconds | Temperature measurement time |
| `adc` | Volts | AD7172 input |
| `sens` | Ohms | Thermistor resistance derived from `adc` |
| `temperature` | Degrees Celsius | Steinhart-Hart conversion result derived from `sens` |
| `pid_engaged` | Boolean | `true` if in closed-loop mode |
| `i_set` | Amperes | TEC output current |
| `vref` | Volts | MAX1968 VREF (1.5 V) |
| `dac_value` | Volts | AD5680 output derived from `i_set` |
| `dac_feedback` | Volts | ADC measurement of the AD5680 output |
| `i_tec` | Volts | MAX1968 TEC current monitor |
| `tec_i` | Amperes | TEC output current feedback derived from `i_tec` |
| `tec_u_meas` | Volts | Measurement of the voltage across the TEC |
| `pid_output` | Amperes | PID control output |

View File

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

124
pytec/plot.py Normal file
View File

@ -0,0 +1,124 @@
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from threading import Thread, Lock
from pytec.client import Client
TIME_WINDOW = 300.0
tec = Client()
target_temperature = tec.get_pid()[0]['target']
print("Channel 0 target temperature: {:.3f}".format(target_temperature))
class Series:
def __init__(self, conv=lambda x: x):
self.conv = conv
self.x_data = []
self.y_data = []
def append(self, x, y):
self.x_data.append(x)
self.y_data.append(self.conv(y))
def clip(self, min_x):
drop = 0
while drop < len(self.x_data) and self.x_data[drop] < min_x:
drop += 1
self.x_data = self.x_data[drop:]
self.y_data = self.y_data[drop:]
series = {
'adc': Series(),
'sens': Series(lambda x: x * 0.0001),
'temperature': Series(lambda t: t - target_temperature),
'i_set': Series(),
'pid_output': Series(),
'vref': Series(),
'dac_value': Series(),
'dac_feedback': Series(),
'i_tec': Series(),
'tec_i': Series(),
'tec_u_meas': Series(),
}
series_lock = Lock()
quit = False
def recv_data(tec):
for data in tec.report_mode():
if data['channel'] == 0:
series_lock.acquire()
try:
time = data['time'] / 1000.0
for k, s in series.iteritems():
v = data[k]
if data.has_key(k) and type(v) is float:
s.append(time, v)
finally:
series_lock.release()
if quit:
break
thread = Thread(target=recv_data, args=(tec,))
thread.start()
fig, ax = plt.subplots()
for k, s in series.iteritems():
s.plot, = ax.plot([], [], label=k)
legend = ax.legend()
def animate(i):
min_x, max_x, min_y, max_y = None, None, None, None
series_lock.acquire()
try:
for k, s in series.iteritems():
s.plot.set_data(s.x_data, s.y_data)
if len(s.y_data) > 0:
s.plot.set_label("{}: {:.3f}".format(k, s.y_data[-1]))
if len(s.x_data) > 0:
min_x_ = min(s.x_data)
if min_x is None:
min_x = min_x_
else:
min_x = min(min_x, min_x_)
max_x_ = max(s.x_data)
if max_x is None:
max_x = max_x_
else:
max_x = max(max_x, max_x_)
if len(s.y_data) > 0:
min_y_ = min(s.y_data)
if min_y is None:
min_y = min_y_
else:
min_y = min(min_y, min_y_)
max_y_ = max(s.y_data)
if max_y is None:
max_y = max_y_
else:
max_y = max(max_y, max_y_)
if min_x is not None and max_x - TIME_WINDOW > min_x:
for s in series.itervalues():
s.clip(max_x - TIME_WINDOW)
finally:
series_lock.release()
margin_y = 0.01 * (max_y - min_y)
ax.set_xlim(min_x, max_x)
ax.set_ylim(min_y - margin_y, max_y + margin_y)
global legend
legend.remove()
legend = ax.legend()
ani = animation.FuncAnimation(
fig, animate, interval=1, blit=False, save_count=50)
plt.show()
quit = True
thread.join()

0
pytec/pytec/__init__.py Normal file
View File

163
pytec/pytec/client.py Normal file
View File

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

12
pytec/setup.py Normal file
View File

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

8
pytec/test.py Normal file
View File

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

View File

@ -6,6 +6,7 @@ use stm32f4xx_hal::{
time::MegaHertz,
spi,
};
use crate::timer::sleep;
/// SPI Mode 1
pub const SPI_MODE: spi::Mode = spi::Mode {
@ -33,23 +34,25 @@ impl<SPI: Transfer<u8>, S: OutputPin> Dac<SPI, S> {
}
}
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 = [
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(buf)
self.write(&mut buf)?;
Ok(value)
}
}

View File

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

View File

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

View File

@ -1,6 +1,7 @@
use stm32f4xx_hal::hal::digital::v2::OutputPin;
use crate::{
ad5680,
ad7172,
channel_state::ChannelState,
pins::{ChannelPins, ChannelPinSet},
};
@ -19,8 +20,6 @@ pub struct Channel<C: ChannelPins> {
/// 1 / Volts
pub dac_factor: f64,
pub shdn: C::Shdn,
/// stm32f4 integrated adc
pub adc: C::Adc,
pub vref_pin: C::VRefPin,
pub itec_pin: C::ItecPin,
/// feedback from `dac` output
@ -29,12 +28,10 @@ pub struct Channel<C: ChannelPins> {
}
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;
@ -42,11 +39,20 @@ impl<C: ChannelPins> Channel<C> {
state,
dac, dac_factor,
shdn: pins.shdn,
adc: pins.adc,
vref_pin: pins.vref_pin,
itec_pin: pins.itec_pin,
dac_feedback_pin: pins.dac_feedback_pin,
tec_u_meas_pin: pins.tec_u_meas_pin,
}
}
// power up TEC
pub fn power_up(&mut self) {
let _ = self.shdn.set_high();
}
// power down TEC
pub fn power_down(&mut self) {
let _ = self.shdn.set_low();
}
}

View File

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

View File

@ -1,53 +1,63 @@
use serde::{Serialize, Serializer};
use smoltcp::time::Instant;
use log::info;
use stm32f4xx_hal::hal;
use uom::si::{
f64::{ElectricCurrent, ElectricPotential, ElectricalResistance},
electric_potential::{millivolt, volt},
electric_current::ampere,
electrical_resistance::ohm,
ratio::ratio,
thermodynamic_temperature::degree_celsius,
};
use crate::{
ad5680,
ad7172,
channel::{Channel, Channel0, Channel1},
channel_state::ChannelState,
command_parser::{CenterPoint, PwmPin},
pins,
units::Volts,
steinhart_hart,
};
pub const CHANNELS: usize = 2;
pub const R_SENSE: f64 = 0.05;
// TODO: -pub
pub struct Channels {
channel0: Channel<Channel0>,
channel1: Channel<Channel1>,
pub adc: ad7172::Adc<pins::AdcSpi, pins::AdcNss>,
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();
let adc_calibration0 = adc.get_calibration(0)
.expect("adc_calibration0");
adc.setup_channel(1, ad7172::Input::Ain2, ad7172::Input::Ain3).unwrap();
let adc_calibration1 = adc.get_calibration(1)
.expect("adc_calibration1");
adc.start_continuous_conversion().unwrap();
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.channel_state(channel).vref = channels.read_vref(channel);
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 {
@ -63,191 +73,547 @@ impl Channels {
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 => {
let vref = self.read_vref(channel);
self.channel_state(channel).vref = vref;
vref
},
CenterPoint::Override(center_point) =>
ElectricPotential::new::<volt>(center_point.into()),
}
}
/// i_set DAC
pub fn set_dac(&mut self, channel: usize, voltage: Volts) {
fn get_dac(&mut self, channel: usize) -> (ElectricPotential, ElectricPotential) {
let dac_factor = match channel.into() {
0 => self.channel0.dac_factor,
1 => self.channel1.dac_factor,
_ => unreachable!(),
};
let value = (voltage.0 * dac_factor) as u32;
match channel {
0 => {
self.channel0.dac.set(value).unwrap();
self.channel0.state.dac_value = voltage;
}
1 => {
self.channel1.dac.set(value).unwrap();
self.channel1.state.dac_value = voltage;
}
_ => unreachable!(),
}
let voltage = self.channel_state(channel).dac_value;
let max = ElectricPotential::new::<volt>(ad5680::MAX_VALUE as f64 / dac_factor);
(voltage, max)
}
pub fn read_dac_feedback(&mut self, channel: usize) -> Volts {
pub fn get_i(&mut self, channel: usize) -> (ElectricCurrent, ElectricCurrent) {
let center_point = self.get_center(channel);
let r_sense = ElectricalResistance::new::<ohm>(R_SENSE);
let (voltage, max) = self.get_dac(channel);
let i_tec = (voltage - center_point) / (10.0 * r_sense);
let max = (max - center_point) / (10.0 * r_sense);
(i_tec, max)
}
/// i_set DAC
fn set_dac(&mut self, channel: usize, voltage: ElectricPotential) -> (ElectricPotential, ElectricPotential) {
let dac_factor = match channel.into() {
0 => self.channel0.dac_factor,
1 => self.channel1.dac_factor,
_ => unreachable!(),
};
let value = (voltage.get::<volt>() * dac_factor) as u32;
let value = match channel {
0 => self.channel0.dac.set(value).unwrap(),
1 => self.channel1.dac.set(value).unwrap(),
_ => unreachable!(),
};
let voltage = ElectricPotential::new::<volt>(value as f64 / dac_factor);
self.channel_state(channel).dac_value = voltage;
let max = ElectricPotential::new::<volt>(ad5680::MAX_VALUE as f64 / dac_factor);
(voltage, max)
}
pub fn set_i(&mut self, channel: usize, i_tec: ElectricCurrent) -> (ElectricCurrent, ElectricCurrent) {
let center_point = self.get_center(channel);
let r_sense = ElectricalResistance::new::<ohm>(R_SENSE);
let voltage = i_tec * 10.0 * r_sense + center_point;
let (voltage, max) = self.set_dac(channel, voltage);
let i_tec = (voltage - center_point) / (10.0 * r_sense);
let max = (max - center_point) / (10.0 * r_sense);
(i_tec, max)
}
pub fn read_dac_feedback(&mut self, channel: usize) -> ElectricPotential {
match channel {
0 => {
let sample = self.channel0.adc.convert(
let sample = self.pins_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)
let mv = self.pins_adc.sample_to_millivolts(sample);
ElectricPotential::new::<millivolt>(mv as f64)
}
1 => {
let sample = self.channel1.adc.convert(
let sample = self.pins_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)
let mv = self.pins_adc.sample_to_millivolts(sample);
ElectricPotential::new::<millivolt>(mv as f64)
}
_ => unreachable!(),
}
}
pub fn read_dac_feedback_until_stable(&mut self, channel: usize, tolerance: f64) -> Volts {
pub fn read_dac_feedback_until_stable(&mut self, channel: usize, tolerance: ElectricPotential) -> ElectricPotential {
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 {
if (current - prev).abs() < tolerance {
return current;
}
prev = current;
}
}
pub fn read_itec(&mut self, channel: usize) -> Volts {
pub fn read_itec(&mut self, channel: usize) -> ElectricPotential {
match channel {
0 => {
let sample = self.channel0.adc.convert(
let sample = self.pins_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)
let mv = self.pins_adc.sample_to_millivolts(sample);
ElectricPotential::new::<millivolt>(mv as f64)
}
1 => {
let sample = self.channel1.adc.convert(
let sample = self.pins_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)
let mv = self.pins_adc.sample_to_millivolts(sample);
ElectricPotential::new::<millivolt>(mv as f64)
}
_ => unreachable!(),
}
}
/// should be 1.5V
pub fn read_vref(&mut self, channel: usize) -> Volts {
pub fn read_vref(&mut self, channel: usize) -> ElectricPotential {
match channel {
0 => {
let sample = self.channel0.adc.convert(
let sample = self.pins_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)
let mv = self.pins_adc.sample_to_millivolts(sample);
ElectricPotential::new::<millivolt>(mv as f64)
}
1 => {
let sample = self.channel1.adc.convert(
let sample = self.pins_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)
let mv = self.pins_adc.sample_to_millivolts(sample);
ElectricPotential::new::<millivolt>(mv as f64)
}
_ => unreachable!(),
}
}
pub fn read_tec_u_meas(&mut self, channel: usize) -> Volts {
pub fn read_tec_u_meas(&mut self, channel: usize) -> ElectricPotential {
match channel {
0 => {
let sample = self.tec_u_meas_adc.convert(
let sample = self.pins_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)
let mv = self.pins_adc.sample_to_millivolts(sample);
ElectricPotential::new::<millivolt>(mv as f64)
}
1 => {
let sample = self.tec_u_meas_adc.convert(
let sample = self.pins_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)
let mv = self.pins_adc.sample_to_millivolts(sample);
ElectricPotential::new::<millivolt>(mv as f64)
}
_ => unreachable!(),
}
}
/// for i_set
/// Calibrate the I_SET DAC using the DAC_FB ADC pin.
///
/// These loops perform a breadth-first search for the DAC setting
/// that will produce a `target_voltage`.
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 target_voltage = ElectricPotential::new::<volt>(2.5);
let mut start_value = 1;
let mut best_error = ElectricPotential::new::<volt>(100.0);
let dac_factor = value as f64 / vref.0;
match channel {
0 => self.channel0.dac_factor = dac_factor,
1 => self.channel1.dac_factor = dac_factor,
_ => unreachable!(),
}
}
fn calibrate_dac_value_for_voltage(&mut self, channel: usize, voltage: Volts) -> u32 {
let mut best_value = 0;
let mut best_error = Volts(100.0);
for step in (1..=12).rev() {
for value in (best_value..=ad5680::MAX_VALUE).step_by(2usize.pow(step)) {
for step in (0..18).rev() {
let mut prev_value = start_value;
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!(),
}
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.read_dac_feedback_until_stable(channel, ElectricPotential::new::<volt>(0.001));
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 = prev_value;
let dac_factor = value as f64 / dac_feedback.get::<volt>();
match channel {
0 => self.channel0.dac_factor = dac_factor,
1 => self.channel1.dac_factor = dac_factor,
_ => unreachable!(),
}
}
prev_value = value;
}
}
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!(),
}
}
fn get_pwm(&self, channel: usize, pin: PwmPin) -> f64 {
fn get<P: hal::PwmPin<Duty=u16>>(pin: &P) -> f64 {
let duty = pin.get_duty();
let max = pin.get_max_duty();
duty as f64 / (max as f64)
}
match (channel, pin) {
(_, PwmPin::ISet) =>
panic!("i_set is no pwm pin"),
(0, PwmPin::MaxIPos) =>
get(&self.pwm.max_i_pos0),
(0, PwmPin::MaxINeg) =>
get(&self.pwm.max_i_neg0),
(0, PwmPin::MaxV) =>
get(&self.pwm.max_v0),
(1, PwmPin::MaxIPos) =>
get(&self.pwm.max_i_pos1),
(1, PwmPin::MaxINeg) =>
get(&self.pwm.max_i_neg1),
(1, PwmPin::MaxV) =>
get(&self.pwm.max_v1),
_ =>
unreachable!(),
}
}
pub fn get_max_v(&mut self, channel: usize) -> (ElectricPotential, ElectricPotential) {
let vref = self.channel_state(channel).vref;
let max = 4.0 * vref;
let duty = self.get_pwm(channel, PwmPin::MaxV);
(duty * max, max)
}
pub fn get_max_i_pos(&mut self, channel: usize) -> (ElectricCurrent, ElectricCurrent) {
let max = ElectricCurrent::new::<ampere>(3.0);
let duty = self.get_pwm(channel, PwmPin::MaxIPos);
(duty * max, max)
}
pub fn get_max_i_neg(&mut self, channel: usize) -> (ElectricCurrent, ElectricCurrent) {
let max = ElectricCurrent::new::<ampere>(3.0);
let duty = self.get_pwm(channel, PwmPin::MaxINeg);
(duty * max, max)
}
fn set_pwm(&mut self, channel: usize, pin: PwmPin, duty: f64) -> f64 {
fn set<P: hal::PwmPin<Duty=u16>>(pin: &mut P, duty: f64) -> f64 {
let max = pin.get_max_duty();
let value = ((duty * (max as f64)) as u16).min(max);
pin.set_duty(value);
value as f64 / (max as f64)
}
match (channel, pin) {
(_, PwmPin::ISet) =>
panic!("i_set is no pwm pin"),
(0, PwmPin::MaxIPos) =>
set(&mut self.pwm.max_i_pos0, duty),
(0, PwmPin::MaxINeg) =>
set(&mut self.pwm.max_i_neg0, duty),
(0, PwmPin::MaxV) =>
set(&mut self.pwm.max_v0, duty),
(1, PwmPin::MaxIPos) =>
set(&mut self.pwm.max_i_pos1, duty),
(1, PwmPin::MaxINeg) =>
set(&mut self.pwm.max_i_neg1, duty),
(1, PwmPin::MaxV) =>
set(&mut self.pwm.max_v1, duty),
_ =>
unreachable!(),
}
}
pub fn set_max_v(&mut self, channel: usize, max_v: ElectricPotential) -> (ElectricPotential, ElectricPotential) {
let vref = self.channel_state(channel).vref;
let max = 4.0 * vref;
let duty = (max_v / max).get::<ratio>();
let duty = self.set_pwm(channel, PwmPin::MaxV, duty);
(duty * max, max)
}
pub fn set_max_i_pos(&mut self, channel: usize, max_i_pos: ElectricCurrent) -> (ElectricCurrent, ElectricCurrent) {
let max = ElectricCurrent::new::<ampere>(3.0);
let duty = (max_i_pos / max).get::<ratio>();
let duty = self.set_pwm(channel, PwmPin::MaxIPos, duty);
(duty * max, max)
}
pub fn set_max_i_neg(&mut self, channel: usize, max_i_neg: ElectricCurrent) -> (ElectricCurrent, ElectricCurrent) {
let max = ElectricCurrent::new::<ampere>(3.0);
let duty = (max_i_neg / max).get::<ratio>();
let duty = self.set_pwm(channel, PwmPin::MaxINeg, duty);
(duty * max, max)
}
pub fn report(&mut self, channel: usize) -> Report {
let vref = self.channel_state(channel).vref;
let (i_set, _) = self.get_i(channel);
let i_tec = self.read_itec(channel);
let tec_i = (i_tec - vref) / ElectricalResistance::new::<ohm>(0.4);
let (dac_value, _) = self.get_dac(channel);
let state = self.channel_state(channel);
let pid_output = state.pid.last_output.map(|last_output|
ElectricCurrent::new::<ampere>(last_output)
);
Report {
channel,
time: state.adc_time.total_millis(),
adc: state.get_adc(),
sens: state.get_sens(),
temperature: state.get_temperature()
.map(|temperature| temperature.get::<degree_celsius>()),
pid_engaged: state.pid_engaged,
i_set,
vref,
dac_value,
dac_feedback: self.read_dac_feedback(channel),
i_tec,
tec_i,
tec_u_meas: self.read_tec_u_meas(channel),
pid_output,
}
}
pub fn pwm_summary(&mut self, channel: usize) -> PwmSummary {
PwmSummary {
channel,
center: CenterPointJson(self.channel_state(channel).center.clone()),
i_set: self.get_i(channel).into(),
max_v: self.get_max_v(channel).into(),
max_i_pos: self.get_max_i_pos(channel).into(),
max_i_neg: self.get_max_i_neg(channel).into(),
}
}
pub fn postfilter_summary(&mut self, channel: usize) -> PostFilterSummary {
let rate = self.adc.get_postfilter(channel as u8).unwrap()
.and_then(|filter| filter.output_rate());
PostFilterSummary { channel, rate }
}
pub fn steinhart_hart_summary(&mut self, channel: usize) -> SteinhartHartSummary {
let params = self.channel_state(channel).sh.clone();
SteinhartHartSummary { channel, params }
}
}
type JsonBuffer = heapless::Vec<u8, heapless::consts::U512>;
#[derive(Serialize)]
pub struct Report {
channel: usize,
time: i64,
adc: Option<ElectricPotential>,
sens: Option<ElectricalResistance>,
temperature: Option<f64>,
pid_engaged: bool,
i_set: ElectricCurrent,
vref: ElectricPotential,
dac_value: ElectricPotential,
dac_feedback: ElectricPotential,
i_tec: ElectricPotential,
tec_i: ElectricCurrent,
tec_u_meas: ElectricPotential,
pid_output: Option<ElectricCurrent>,
}
impl Report {
pub fn to_json(&self) -> Result<JsonBuffer, serde_json_core::ser::Error> {
serde_json_core::to_vec(self)
}
}
pub struct CenterPointJson(CenterPoint);
// used in JSON encoding, not for config
impl Serialize for CenterPointJson {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self.0 {
CenterPoint::Vref =>
serializer.serialize_str("vref"),
CenterPoint::Override(vref) =>
serializer.serialize_f32(vref),
}
}
}
#[derive(Serialize)]
pub struct PwmSummaryField<T: Serialize> {
value: T,
max: T,
}
impl<T: Serialize> From<(T, T)> for PwmSummaryField<T> {
fn from((value, max): (T, T)) -> Self {
PwmSummaryField { value, max }
}
}
#[derive(Serialize)]
pub struct PwmSummary {
channel: usize,
center: CenterPointJson,
i_set: PwmSummaryField<ElectricCurrent>,
max_v: PwmSummaryField<ElectricPotential>,
max_i_pos: PwmSummaryField<ElectricCurrent>,
max_i_neg: PwmSummaryField<ElectricCurrent>,
}
impl PwmSummary {
pub fn to_json(&self) -> Result<JsonBuffer, serde_json_core::ser::Error> {
serde_json_core::to_vec(self)
}
}
#[derive(Serialize)]
pub struct PostFilterSummary {
channel: usize,
rate: Option<f32>,
}
impl PostFilterSummary {
pub fn to_json(&self) -> Result<JsonBuffer, serde_json_core::ser::Error> {
serde_json_core::to_vec(self)
}
}
#[derive(Serialize)]
pub struct SteinhartHartSummary {
channel: usize,
params: steinhart_hart::Parameters,
}
impl SteinhartHartSummary {
pub fn to_json(&self) -> Result<JsonBuffer, serde_json_core::ser::Error> {
serde_json_core::to_vec(self)
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn report_to_json() {
// `/ 1.1` results in values with a really long serialization
let report = Report {
channel: 0,
time: 3200,
adc: Some(ElectricPotential::new::<volt>(0.65 / 1.1)),
sens: Some(ElectricalResistance::new::<ohm>(10000.0 / 1.1)),
temperature: Some(30.0 / 1.1),
pid_engaged: false,
i_set: ElectricCurrent::new::<ampere>(0.5 / 1.1),
vref: ElectricPotential::new::<volt>(1.5 / 1.1),
dac_value: ElectricPotential::new::<volt>(2.0 / 1.1),
dac_feedback: ElectricPotential::new::<volt>(2.0 / 1.1),
i_tec: ElectricPotential::new::<volt>(2.0 / 1.1),
tec_i: ElectricCurrent::new::<ampere>(0.2 / 1.1),
tec_u_meas: ElectricPotential::new::<volt>(2.0 / 1.1),
pid_output: Some(ElectricCurrent::new::<ampere>(0.5 / 1.1)),
};
let buf = report.to_json().unwrap();
assert_eq!(buf[0], b'{');
assert_eq!(buf[buf.len() - 1], b'}');
}
#[test]
fn pwm_summary_to_json() {
let value = 1.0 / 1.1;
let max = 5.0 / 1.1;
let pwm_summary = PwmSummary {
channel: 0,
center: CenterPointJson(CenterPoint::Vref),
i_set: PwmSummaryField {
value: ElectricCurrent::new::<ampere>(value),
max: ElectricCurrent::new::<ampere>(max),
},
max_v: PwmSummaryField {
value: ElectricPotential::new::<volt>(value),
max: ElectricPotential::new::<volt>(max),
},
max_i_pos: PwmSummaryField {
value: ElectricCurrent::new::<ampere>(value),
max: ElectricCurrent::new::<ampere>(max),
},
max_i_neg: PwmSummaryField {
value: ElectricCurrent::new::<ampere>(value),
max: ElectricCurrent::new::<ampere>(max),
},
};
let buf = pwm_summary.to_json().unwrap();
assert_eq!(buf[0], b'{');
assert_eq!(buf[buf.len() - 1], b'}');
}
}

View File

@ -12,6 +12,7 @@ use nom::{
error::ErrorKind,
};
use num_traits::{Num, ParseFloatError};
use serde::{Serialize, Deserialize};
#[derive(Clone, Debug, PartialEq)]
@ -122,32 +123,35 @@ pub enum PwmPin {
MaxV,
}
impl PwmPin {
pub fn name(&self) -> &'static str {
match self {
PwmPin::ISet => "i_set",
PwmPin::MaxIPos => "max_i_pos",
PwmPin::MaxINeg => "max_i_neg",
PwmPin::MaxV => "max_v",
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum CenterPoint {
Vref,
Override(f32),
}
#[derive(Debug, Clone, PartialEq)]
pub enum Command {
Quit,
Load,
Save,
Reset,
Ipv4([u8; 4]),
Show(ShowCommand),
Reporting(bool),
/// PWM parameter setting
Pwm {
channel: usize,
pin: PwmPin,
duty: f64,
value: f64,
},
/// Enable PID control for `i_set`
PwmPid {
channel: usize,
},
CenterPoint {
channel: usize,
center: CenterPoint,
},
/// PID parameter setting
Pid {
channel: usize,
@ -161,7 +165,7 @@ pub enum Command {
},
PostFilter {
channel: usize,
rate: f32,
rate: Option<f32>,
},
}
@ -242,7 +246,7 @@ fn report(input: &[u8]) -> IResult<&[u8], Command> {
fn pwm_setup(input: &[u8]) -> IResult<&[u8], Result<(PwmPin, f64), Error>> {
let result_with_pin = |pin: PwmPin|
move |result: Result<f64, Error>|
result.map(|duty| (pin, duty));
result.map(|value| (pin, value));
alt((
map(
@ -300,8 +304,8 @@ fn pwm(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
|input| {
let (input, config) = pwm_setup(input)?;
match config {
Ok((pin, duty)) =>
Ok((input, Ok(Command::Pwm { channel, pin, duty }))),
Ok((pin, value)) =>
Ok((input, Ok(Command::Pwm { channel, pin, value }))),
Err(e) =>
Ok((input, Err(e))),
}
@ -314,6 +318,25 @@ fn pwm(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
))(input)
}
fn center_point(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
let (input, _) = tag("center")(input)?;
let (input, _) = whitespace(input)?;
let (input, channel) = channel(input)?;
let (input, _) = whitespace(input)?;
let (input, center) = alt((
value(Ok(CenterPoint::Vref), tag("vref")),
|input| {
let (input, value) = float(input)?;
Ok((input, value.map(|value| CenterPoint::Override(value as f32))))
}
))(input)?;
end(input)?;
Ok((input, center.map(|center| Command::CenterPoint {
channel,
center,
})))
}
/// `pid <0-1> <parameter> <value>`
fn pid_parameter(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
let (input, channel) = channel(input)?;
@ -383,25 +406,56 @@ fn postfilter(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
|input| {
let (input, channel) = channel(input)?;
let (input, _) = whitespace(input)?;
let (input, _) = tag("rate")(input)?;
let (input, _) = whitespace(input)?;
let (input, rate) = float(input)?;
let result = rate
.map(|rate| Command::PostFilter {
alt((
value(Ok(Command::PostFilter {
channel,
rate: rate as f32,
});
Ok((input, result))
rate: None,
}), tag("off")),
move |input| {
let (input, _) = tag("rate")(input)?;
let (input, _) = whitespace(input)?;
let (input, rate) = float(input)?;
let result = rate
.map(|rate| Command::PostFilter {
channel,
rate: Some(rate as f32),
});
Ok((input, result))
}
))(input)
}
),
value(Ok(Command::Show(ShowCommand::PostFilter)), end)
))(input)
}
fn ipv4(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
let (input, _) = tag("ipv4")(input)?;
let (input, _) = whitespace(input)?;
let (input, a) = unsigned(input)?;
let (input, _) = tag(".")(input)?;
let (input, b) = unsigned(input)?;
let (input, _) = tag(".")(input)?;
let (input, c) = unsigned(input)?;
let (input, _) = tag(".")(input)?;
let (input, d) = unsigned(input)?;
end(input)?;
let result = a.and_then(|a| b.and_then(|b| c.and_then(|c| d.map(|d|
Command::Ipv4([a as u8, b as u8, c as u8, d as u8])
))));
Ok((input, result))
}
fn command(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
alt((value(Ok(Command::Quit), tag("quit")),
value(Ok(Command::Load), tag("load")),
value(Ok(Command::Save), tag("save")),
value(Ok(Command::Reset), tag("reset")),
ipv4,
map(report, Ok),
pwm,
center_point,
pid,
steinhart_hart,
postfilter,
@ -411,7 +465,7 @@ fn command(input: &[u8]) -> IResult<&[u8], Result<Command, Error>> {
impl Command {
pub fn parse(input: &[u8]) -> Result<Self, Error> {
match command(input) {
Ok((b"", result)) =>
Ok((input_remain, result)) if input_remain.len() == 0 =>
result,
Ok((input_remain, _)) =>
Err(Error::UnexpectedInput(input_remain[0])),
@ -431,6 +485,24 @@ mod test {
assert_eq!(command, Ok(Command::Quit));
}
#[test]
fn parse_load() {
let command = Command::parse(b"load");
assert_eq!(command, Ok(Command::Load));
}
#[test]
fn parse_save() {
let command = Command::parse(b"save");
assert_eq!(command, Ok(Command::Save));
}
#[test]
fn parse_ipv4() {
let command = Command::parse(b"ipv4 192.168.1.26");
assert_eq!(command, Ok(Command::Ipv4([192, 168, 1, 26])));
}
#[test]
fn parse_report() {
let command = Command::parse(b"report");
@ -461,7 +533,7 @@ mod test {
assert_eq!(command, Ok(Command::Pwm {
channel: 1,
pin: PwmPin::ISet,
duty: 16383,
value: 16383.0,
}));
}
@ -470,7 +542,6 @@ mod test {
let command = Command::parse(b"pwm 0 pid");
assert_eq!(command, Ok(Command::PwmPid {
channel: 0,
pin: PwmPin::ISet,
}));
}
@ -480,7 +551,7 @@ mod test {
assert_eq!(command, Ok(Command::Pwm {
channel: 0,
pin: PwmPin::MaxIPos,
duty: 7,
value: 7.0,
}));
}
@ -490,7 +561,7 @@ mod test {
assert_eq!(command, Ok(Command::Pwm {
channel: 0,
pin: PwmPin::MaxINeg,
duty: 128,
value: 128.0,
}));
}
@ -500,7 +571,7 @@ mod test {
assert_eq!(command, Ok(Command::Pwm {
channel: 0,
pin: PwmPin::MaxV,
duty: 32768,
value: 32768.0,
}));
}
@ -537,7 +608,7 @@ mod test {
}
#[test]
fn parse_steinhart_hart_parallel_r() {
fn parse_steinhart_hart_set() {
let command = Command::parse(b"s-h 1 t0 23.05");
assert_eq!(command, Ok(Command::SteinhartHart {
channel: 1,
@ -546,12 +617,45 @@ mod test {
}));
}
#[test]
fn parse_postfilter() {
let command = Command::parse(b"postfilter");
assert_eq!(command, Ok(Command::Show(ShowCommand::PostFilter)));
}
#[test]
fn parse_postfilter_off() {
let command = Command::parse(b"postfilter 1 off");
assert_eq!(command, Ok(Command::PostFilter {
channel: 1,
rate: None,
}));
}
#[test]
fn parse_postfilter_rate() {
let command = Command::parse(b"postfilter 0 rate 21");
assert_eq!(command, Ok(Command::PostFilter {
channel: 0,
rate: 21.0,
rate: Some(21.0),
}));
}
#[test]
fn parse_center_point() {
let command = Command::parse(b"center 0 1.5");
assert_eq!(command, Ok(Command::CenterPoint {
channel: 0,
center: CenterPoint::Override(1.5),
}));
}
#[test]
fn parse_center_point_vref() {
let command = Command::parse(b"center 1 vref");
assert_eq!(command, Ok(Command::CenterPoint {
channel: 1,
center: CenterPoint::Vref,
}));
}
}

253
src/config.rs Normal file
View File

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

View File

@ -1,5 +1,11 @@
use crate::usb;
#[cfg(not(feature = "semihosting"))]
pub fn init_log() {}
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() {

44
src/leds.rs Normal file
View File

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

View File

@ -1,35 +1,50 @@
#![no_std]
#![no_main]
#![cfg_attr(not(test), no_std)]
#![cfg_attr(not(test), no_main)]
#![feature(maybe_uninit_extra, maybe_uninit_ref)]
#![cfg_attr(test, allow(unused))]
// TODO: #![deny(warnings, unused)]
#[cfg(not(feature = "semihosting"))]
#[cfg(not(any(feature = "semihosting", test)))]
use panic_abort as _;
#[cfg(feature = "semihosting")]
#[cfg(all(feature = "semihosting", not(test)))]
use panic_semihosting as _;
use log::{info, warn};
use log::{error, info, warn};
use core::ops::DerefMut;
use core::fmt::Write;
use cortex_m::asm::wfi;
use cortex_m_rt::entry;
use stm32f4xx_hal::{
hal::{
self,
watchdog::{WatchdogEnable, Watchdog},
},
hal::watchdog::{WatchdogEnable, Watchdog},
rcc::RccExt,
watchdog::IndependentWatchdog,
time::{U32Ext, MegaHertz},
stm32::{CorePeripherals, Peripherals},
stm32::{CorePeripherals, Peripherals, SCB},
};
use smoltcp::{
time::Instant,
wire::EthernetAddress,
socket::TcpSocket,
wire::{EthernetAddress, Ipv4Address},
};
use uom::{
si::{
f64::{
ElectricCurrent,
ElectricPotential,
ElectricalResistance,
ThermodynamicTemperature,
},
electric_current::ampere,
electric_potential::volt,
electrical_resistance::ohm,
thermodynamic_temperature::degree_celsius,
},
};
mod init_log;
use init_log::init_log;
mod usb;
mod leds;
mod pins;
use pins::Pins;
mod ad7172;
@ -38,44 +53,81 @@ 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;
mod channels;
use channels::{CHANNELS, Channels};
mod channel;
mod channel_state;
mod config;
use config::Config;
const HSE: MegaHertz = MegaHertz(8);
#[cfg(not(feature = "semihosting"))]
const WATCHDOG_INTERVAL: u32 = 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];
pub const EEPROM_PAGE_SIZE: usize = 8;
pub const EEPROM_SIZE: usize = 128;
pub const DEFAULT_IPV4_ADDRESS: Ipv4Address = Ipv4Address([192, 168, 1, 26]);
const TCP_PORT: u16 = 23;
fn send_line(socket: &mut TcpSocket, data: &[u8]) -> bool {
let send_free = socket.send_capacity() - socket.send_queue();
if data.len() > send_free + 1 {
// Not enough buffer space, skip report for now
warn!(
"TCP socket has only {}/{} needed {}",
send_free + 1, socket.send_capacity(), data.len(),
);
} else {
match socket.send_slice(&data) {
Ok(sent) if sent == data.len() => {
let _ = socket.send_slice(b"\n");
// success
return true
}
Ok(sent) =>
warn!("sent only {}/{} bytes", sent, data.len()),
Err(e) =>
error!("error sending line: {:?}", e),
}
}
// not success
false
}
fn report_to(channel: usize, channels: &mut Channels, socket: &mut TcpSocket) -> bool {
match channels.report(channel).to_json() {
Ok(buf) =>
send_line(socket, &buf[..]),
Err(e) => {
error!("unable to serialize report: {:?}", e);
false
}
}
}
/// Initialization and main loop
#[cfg(not(test))]
#[entry]
fn main() -> ! {
init_log();
info!("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()
.cfgr
.use_hse(HSE)
@ -89,28 +141,46 @@ 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))
};
info!("Net hwaddr: {}", hwaddr);
let (pins, mut leds, mut eeprom, eth_pins, usb) = Pins::setup(
clocks, dp.TIM1, dp.TIM3,
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,
);
net::run(dp.ETHERNET_MAC, dp.ETHERNET_DMA, hwaddr, |iface| {
leds.r1.on();
leds.g3.off();
leds.g4.off();
usb::State::setup(usb);
let mut ipv4_address = DEFAULT_IPV4_ADDRESS;
let mut channels = Channels::new(pins);
let _ = Config::load(&mut eeprom)
.map(|config| {
config.apply(&mut channels);
ipv4_address = Ipv4Address::from_bytes(&config.ipv4_address);
})
.map_err(|e| warn!("error loading config: {:?}", e));
info!("IPv4 address: {}", ipv4_address);
// EEPROM ships with a read-only EUI-48 identifier
let mut eui48 = [0; 6];
eeprom.read_data(0xFA, &mut eui48).unwrap();
let hwaddr = EthernetAddress(eui48);
info!("EEPROM MAC address: {}", hwaddr);
net::run(clocks, dp.ETHERNET_MAC, dp.ETHERNET_DMA, eth_pins, hwaddr, ipv4_address, |iface| {
let mut new_ipv4_address = None;
Server::<Session>::run(iface, |server| {
leds.r1.off();
loop {
let instant = Instant::from_millis(i64::from(timer::now()));
let updated_channel = channels.poll_adc(instant);
@ -130,178 +200,103 @@ fn main() -> ! {
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 {
} else if socket.may_send() && !socket.may_recv() {
socket.close()
} else if socket.can_send() && socket.can_recv() {
match socket.recv(|buf| session.feed(buf)) {
Ok(SessionOutput::Nothing) => {}
Ok(SessionOutput::Command(command)) => match command {
Ok(SessionInput::Nothing) => {}
Ok(SessionInput::Command(command)) => match command {
Command::Quit =>
socket.close(),
Command::Reporting(reporting) => {
let _ = writeln!(socket, "report={}", if reporting { "on" } else { "off" });
Command::Reporting(_reporting) => {
// handled by session
}
Command::Show(ShowCommand::Reporting) => {
let _ = writeln!(socket, "report={}", if session.reporting() { "on" } else { "off" });
let _ = writeln!(socket, "{{ \"report\": {:?} }}", session.reporting());
}
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,
);
}
report_to(channel, &mut channels, &mut socket);
}
}
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
);
};
match channels.channel_state(channel).pid.summary(channel).to_json() {
Ok(buf) => {
send_line(&mut socket, &buf);
}
Err(e) =>
error!("unable to serialize pid summary: {:?}", e),
}
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);
match channels.pwm_summary(channel).to_json() {
Ok(buf) => {
send_line(&mut socket, &buf);
}
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!(),
Err(e) =>
error!("unable to serialize pwm summary: {:?}", e),
}
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, "");
match channels.steinhart_hart_summary(channel).to_json() {
Ok(buf) => {
send_line(&mut socket, &buf);
}
Err(e) =>
error!("unable to serialize steinhart-hart summary: {:?}", e),
}
}
}
Command::Show(ShowCommand::PostFilter) => {
for channel in 0..CHANNELS {
match channels.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
);
match channels.postfilter_summary(channel).to_json() {
Ok(buf) => {
send_line(&mut socket, &buf);
}
Err(e) =>
error!("unable to serialize postfilter summary: {:?}", e),
}
}
}
Command::PwmPid { channel } => {
channels.channel_state(channel).pid_engaged = true;
let _ = writeln!(socket, "channel {}: PID enabled to control PWM", channel
);
leds.g3.on();
}
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)
Command::Pwm { channel, pin, value } => {
match pin {
PwmPin::ISet => {
channels.channel_state(channel).pid_engaged = false;
leds.g3.off();
let current = ElectricCurrent::new::<ampere>(value);
channels.set_i(channel, current);
channels.power_up(channel);
}
PwmPin::MaxV => {
let voltage = ElectricPotential::new::<volt>(value);
channels.set_max_v(channel, voltage);
}
PwmPin::MaxIPos => {
let current = ElectricCurrent::new::<ampere>(value);
channels.set_max_i_pos(channel, current);
}
PwmPin::MaxINeg => {
let current = ElectricCurrent::new::<ampere>(value);
channels.set_max_i_neg(channel, current);
}
}
}
Command::CenterPoint { channel, center } => {
let (i_tec, _) = channels.get_i(channel);
let state = channels.channel_state(channel);
state.center = center;
if !state.pid_engaged {
channels.set_i(channel, i_tec);
}
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;
@ -310,80 +305,105 @@ fn main() -> ! {
Target =>
pid.target = value,
KP =>
pid.parameters.kp = value,
pid.parameters.kp = value as f32,
KI =>
pid.parameters.ki = value,
pid.parameters.ki = value as f32,
KD =>
pid.parameters.kd = value,
pid.parameters.kd = value as f32,
OutputMin =>
pid.parameters.output_min = value,
pid.parameters.output_min = value as f32,
OutputMax =>
pid.parameters.output_max = value,
pid.parameters.output_max = value as f32,
IntegralMin =>
pid.parameters.integral_min = value,
pid.parameters.integral_min = value as f32,
IntegralMax =>
pid.parameters.integral_max = value,
pid.parameters.integral_max = value as f32,
}
// 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,
T0 => sh.t0 = ThermodynamicTemperature::new::<degree_celsius>(value),
B => sh.b = value,
R0 => sh.r0 = value,
R0 => sh.r0 = ElectricalResistance::new::<ohm>(value),
}
let _ = writeln!(socket, "Steinhart-Hart equation parameter updated");
}
Command::PostFilter { channel, rate } => {
Command::PostFilter { channel, rate: None } => {
channels.adc.set_postfilter(channel as u8, None).unwrap();
}
Command::PostFilter { channel, rate: Some(rate) } => {
let filter = ad7172::PostFilter::closest(rate);
match filter {
Some(filter) => {
channels.adc.set_postfilter(channel as u8, Some(filter)).unwrap();
let _ = writeln!(
socket, "channel {}: postfilter set to {:.2} SPS",
channel, filter.output_rate().unwrap()
);
}
None => {
let _ = writeln!(socket, "Unable to choose postfilter");
}
Some(filter) =>
channels.adc.set_postfilter(channel as u8, Some(filter)).unwrap(),
None =>
error!("unable to choose postfilter for rate {:.3}", rate),
}
}
Command::Load => {
match Config::load(&mut eeprom) {
Ok(config) => {
config.apply(&mut channels);
new_ipv4_address = Some(Ipv4Address::from_bytes(&config.ipv4_address));
}
Err(e) =>
error!("unable to load eeprom config: {:?}", e),
}
}
Command::Save => {
let config = Config::new(&mut channels, ipv4_address);
match config.save(&mut eeprom) {
Ok(()) => {},
Err(e) =>
error!("unable to save eeprom config: {:?}", e),
}
}
Command::Ipv4(address) => {
new_ipv4_address = Some(Ipv4Address::from_bytes(&address));
}
Command::Reset => {
for i in 0..CHANNELS {
channels.power_down(i);
}
SCB::sys_reset();
}
}
Ok(SessionOutput::Error(e)) => {
let _ = writeln!(socket, "Command error: {:?}", e);
Ok(SessionInput::Error(e)) => {
error!("session input: {:?}", e);
send_line(&mut socket, b"{ \"error\": \"invalid input\" }");
}
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(|_| {
} else if socket.can_send() {
if let Some(channel) = session.is_report_pending() {
if report_to(channel, &mut channels, &mut socket) {
session.mark_report_sent(channel);
});
}
}
}
});
// Apply new IPv4 address
new_ipv4_address.map(|new_ipv4_address| {
server.set_ipv4_address(ipv4_address);
ipv4_address = new_ipv4_address;
});
// Update watchdog
wd.feed();
leds.g4.off();
cortex_m::interrupt::free(|cs| {
if !net::is_pending(cs) {
// Wait for interrupts
// (Ethernet or SysTick)
// (Ethernet, SysTick, or USB)
wfi();
}
});
leds.g4.on();
}
});
});

View File

@ -2,14 +2,15 @@
//! declared once and globally.
use core::cell::RefCell;
use cortex_m::interrupt::Mutex;
use bare_metal::CriticalSection;
use cortex_m::interrupt::{CriticalSection, Mutex};
use stm32f4xx_hal::{
rcc::Clocks,
stm32::{interrupt, Peripherals, ETHERNET_MAC, ETHERNET_DMA},
};
use smoltcp::wire::{EthernetAddress, IpAddress, IpCidr};
use smoltcp::wire::{EthernetAddress, IpCidr, Ipv4Address};
use smoltcp::iface::{NeighborCache, EthernetInterfaceBuilder, EthernetInterface};
use stm32_eth::{Eth, RingEntry, RxDescriptor, TxDescriptor};
use stm32_eth::{Eth, RingEntry, PhyAddress, RxDescriptor, TxDescriptor};
use crate::pins::EthernetPins;
/// Not on the stack so that stack can be placed in CCMRAM (which the
/// ethernet peripheral cannot access)
@ -24,8 +25,12 @@ static NET_PENDING: Mutex<RefCell<bool>> = Mutex::new(RefCell::new(false));
/// Run callback `f` with ethernet driver and TCP/IP stack
pub fn run<F>(
clocks: Clocks,
ethernet_mac: ETHERNET_MAC, ethernet_dma: ETHERNET_DMA,
ethernet_addr: EthernetAddress, f: F
eth_pins: EthernetPins,
ethernet_addr: EthernetAddress,
local_addr: Ipv4Address,
f: F
) where
F: FnOnce(EthernetInterface<&mut stm32_eth::Eth<'static, 'static>>),
{
@ -38,13 +43,17 @@ pub fn run<F>(
// Ethernet driver
let mut eth_dev = Eth::new(
ethernet_mac, ethernet_dma,
&mut rx_ring[..], &mut tx_ring[..]
);
&mut rx_ring[..], &mut tx_ring[..],
PhyAddress::_0,
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)];
// Netmask 0 means we expect any IP address on the local segment.
// No routing.
let mut ip_addrs = [IpCidr::new(local_addr.into(), 0)];
let mut neighbor_storage = [None; 16];
let neighbor_cache = NeighborCache::new(&mut neighbor_storage[..]);
let iface = EthernetInterfaceBuilder::new(&mut eth_dev)

View File

@ -1,24 +1,26 @@
#[derive(Clone, Copy)]
use serde::{Serialize, Deserialize};
#[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
pub kp: f32,
pub ki: f32,
pub kd: f32,
pub output_min: f32,
pub output_max: f32,
pub integral_min: f32,
pub integral_max: f32
}
impl Default for Parameters {
fn default() -> Self {
Parameters {
kp: 0.5,
ki: 0.05,
kd: 0.45,
kp: 1.5,
ki: 0.1,
kd: 150.0,
output_min: 0.0,
output_max: 5.0,
integral_min: 0.0,
integral_max: 1.0,
output_max: 2.0,
integral_min: -10.0,
integral_max: 10.0,
}
}
}
@ -44,40 +46,64 @@ impl Controller {
}
pub fn update(&mut self, input: f64) -> f64 {
let error = self.target - input;
// error
let error = input - self.target;
let p = self.parameters.kp * error;
// partial
let p = f64::from(self.parameters.kp) * error;
self.integral += error;
if self.integral < self.parameters.integral_min {
self.integral = self.parameters.integral_min;
// integral
self.integral += f64::from(self.parameters.ki) * error;
if self.integral < self.parameters.integral_min.into() {
self.integral = self.parameters.integral_min.into();
}
if self.integral > self.parameters.integral_max {
self.integral = self.parameters.integral_max;
if self.integral > self.parameters.integral_max.into() {
self.integral = self.parameters.integral_max.into();
}
let i = self.parameters.ki * self.integral;
let i = self.integral;
// derivative
let d = match self.last_input {
None => 0.0,
Some(last_input) => self.parameters.kd * (last_input - input)
Some(last_input) => f64::from(self.parameters.kd) * (input - last_input),
};
self.last_input = Some(input);
// output
let mut output = p + i + d;
if output < self.parameters.output_min {
output = self.parameters.output_min;
if output < self.parameters.output_min.into() {
output = self.parameters.output_min.into();
}
if output > self.parameters.output_max {
output = self.parameters.output_max;
if output > self.parameters.output_max.into() {
output = self.parameters.output_max.into();
}
self.last_output = Some(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,
integral: self.integral,
}
}
}
type JsonBuffer = heapless::Vec<u8, heapless::consts::U360>;
#[derive(Clone, Serialize, Deserialize)]
pub struct Summary {
channel: usize,
parameters: Parameters,
target: f64,
integral: f64,
}
impl Summary {
pub fn to_json(&self) -> Result<JsonBuffer, serde_json_core::ser::Error> {
serde_json_core::to_vec(self)
}
}
@ -98,12 +124,12 @@ mod test {
#[test]
fn test_controller() {
const DEFAULT: f64 = 0.0;
const TARGET: f64 = 1234.56;
const TARGET: f64 = -1234.56;
const ERROR: f64 = 0.01;
const DELAY: usize = 10;
let mut pid = Controller::new(PARAMETERS.clone());
pid.set_target(TARGET);
pid.target = TARGET;
let mut values = [DEFAULT; DELAY];
let mut t = 0;
@ -113,11 +139,19 @@ mod test {
let next_t = (t + 1) % DELAY;
// Feed the oldest temperature
let output = pid.update(values[next_t]);
// Overwrite oldest with previous temperature + output
values[next_t] = values[t] + output;
// Overwrite oldest with previous temperature - output
values[next_t] = values[t] - output;
t = next_t;
total_t += 1;
}
dbg!(values[t], total_t);
}
#[test]
fn summary_to_json() {
let mut pid = Controller::new(PARAMETERS.clone());
pid.target = 30.0 / 1.1;
let buf = pid.summary(0).to_json().unwrap();
assert_eq!(buf[0], b'{');
assert_eq!(buf[buf.len() - 1], b'}');
}
}

View File

@ -1,8 +1,7 @@
use stm32f4xx_hal::{
adc::Adc,
hal::{blocking::spi::Transfer, digital::v2::OutputPin},
gpio::{
AF5, Alternate, Analog,
AF5, Alternate, AlternateOD, Analog, Floating, Input,
gpioa::*,
gpiob::*,
gpioc::*,
@ -11,22 +10,55 @@ use stm32f4xx_hal::{
gpiog::*,
GpioExt,
Output, PushPull,
Speed::VeryHigh,
},
hal::{self, blocking::spi::Transfer, digital::v2::OutputPin},
i2c::I2c,
otg_fs::USB,
rcc::Clocks,
pwm::{self, PwmChannels},
spi::{Spi, NoMiso},
stm32::{ADC1, ADC2, ADC3, GPIOA, GPIOB, GPIOC, GPIOE, GPIOF, GPIOG, SPI2, SPI4, SPI5, TIM1, TIM3},
stm32::{
ADC1,
GPIOA, GPIOB, GPIOC, GPIOD, GPIOE, GPIOF, GPIOG,
I2C1,
OTG_FS_GLOBAL, OTG_FS_DEVICE, OTG_FS_PWRCLK,
SPI2, SPI4, SPI5,
TIM1, TIM3,
},
time::U32Ext,
};
use crate::channel::{Channel0, Channel1};
use eeprom24x::{self, Eeprom24x};
use stm32_eth::EthPins;
use crate::{
channel::{Channel0, Channel1},
leds::Leds,
};
pub type Eeprom = Eeprom24x<
I2c<I2C1, (
PB8<AlternateOD<stm32f4xx_hal::gpio::AF4>>,
PB9<AlternateOD<stm32f4xx_hal::gpio::AF4>>
)>,
eeprom24x::page_size::B8,
eeprom24x::addr_size::OneByte
>;
pub type EthernetPins = EthPins<
PA1<Input<Floating>>,
PA2<Input<Floating>>,
PC1<Input<Floating>>,
PA7<Input<Floating>>,
PB11<Input<Floating>>,
PG13<Input<Floating>>,
PB13<Input<Floating>>,
PC4<Input<Floating>>,
PC5<Input<Floating>>,
>;
pub trait ChannelPins {
type DacSpi: Transfer<u8>;
type DacSync: OutputPin;
type Shdn: OutputPin;
type Adc;
type VRefPin;
type ItecPin;
type DacFeedbackPin;
@ -37,7 +69,6 @@ impl ChannelPins for Channel0 {
type DacSpi = Dac0Spi;
type DacSync = PE4<Output<PushPull>>;
type Shdn = PE10<Output<PushPull>>;
type Adc = Adc<ADC1>;
type VRefPin = PA0<Analog>;
type ItecPin = PA6<Analog>;
type DacFeedbackPin = PA4<Analog>;
@ -48,7 +79,6 @@ impl ChannelPins for Channel1 {
type DacSpi = Dac1Spi;
type DacSync = PF6<Output<PushPull>>;
type Shdn = PE15<Output<PushPull>>;
type Adc = Adc<ADC2>;
type VRefPin = PA3<Analog>;
type ItecPin = PB0<Analog>;
type DacFeedbackPin = PA5<Analog>;
@ -60,14 +90,12 @@ pub type AdcSpi = Spi<SPI2, (PB10<Alternate<AF5>>, PB14<Alternate<AF5>>, PB15<Al
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>;
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 dac_feedback_pin: C::DacFeedbackPin,
@ -77,7 +105,7 @@ pub struct ChannelPinSet<C: ChannelPins> {
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>,
@ -88,26 +116,24 @@ impl Pins {
pub fn setup(
clocks: Clocks,
tim1: TIM1, tim3: TIM3,
gpioa: GPIOA, gpiob: GPIOB, gpioc: GPIOC, gpioe: GPIOE, gpiof: GPIOF, gpiog: GPIOG,
gpioa: GPIOA, gpiob: GPIOB, gpioc: GPIOC, gpiod: GPIOD, gpioe: GPIOE, gpiof: GPIOF, gpiog: GPIOG,
i2c1: I2C1,
spi2: SPI2, spi4: SPI4, spi5: SPI5,
adc1: ADC1, adc2: ADC2, adc3: ADC3,
) -> Self {
adc1: ADC1,
otg_fs_global: OTG_FS_GLOBAL, otg_fs_device: OTG_FS_DEVICE, otg_fs_pwrclk: OTG_FS_PWRCLK,
) -> (Self, Leds, Eeprom, EthernetPins, USB) {
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,
@ -122,8 +148,6 @@ impl Pins {
);
let mut shdn0 = gpioe.pe10.into_push_pull_output();
let _ = shdn0.set_low();
let mut adc0 = Adc::adc1(adc1, true, Default::default());
adc0.enable();
let vref0_pin = gpioa.pa0.into_analog();
let itec0_pin = gpioa.pa6.into_analog();
let dac_feedback0_pin = gpioa.pa4.into_analog();
@ -132,7 +156,6 @@ 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,
@ -145,8 +168,6 @@ impl Pins {
);
let mut shdn1 = gpioe.pe15.into_push_pull_output();
let _ = shdn1.set_low();
let mut adc1 = Adc::adc2(adc2, true, Default::default());
adc1.enable();
let vref1_pin = gpioa.pa3.into_analog();
let itec1_pin = gpiob.pb0.into_analog();
let dac_feedback1_pin = gpioa.pa5.into_analog();
@ -155,20 +176,49 @@ 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 {
let pins = Pins {
adc_spi, adc_nss,
tec_u_meas_adc,
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_af4().set_open_drain();
let eeprom_sda = gpiob.pb9.into_alternate_af4().set_open_drain();
let eeprom_i2c = I2c::i2c1(i2c1, (eeprom_scl, eeprom_sda), 400.khz(), clocks);
let eeprom = Eeprom24x::new_24x02(eeprom_i2c, eeprom24x::SlaveAddr::default());
let eth_pins = EthPins {
ref_clk: gpioa.pa1,
md_io: gpioa.pa2,
md_clk: gpioc.pc1,
crs: gpioa.pa7,
tx_en: gpiob.pb11,
tx_d0: gpiog.pg13,
tx_d1: gpiob.pb13,
rx_d0: gpioc.pc4,
rx_d1: gpioc.pc5,
};
let usb = USB {
usb_global: otg_fs_global,
usb_device: otg_fs_device,
usb_pwrclk: otg_fs_pwrclk,
pin_dm: gpioa.pa11.into_alternate_af10(),
pin_dp: gpioa.pa12.into_alternate_af10(),
hclk: clocks.hclk(),
};
(pins, leds, eeprom, eth_pins, usb)
}
/// Configure the GPIO pins for SPI operation, and initialize SPI
@ -195,7 +245,7 @@ impl Pins {
fn setup_dac0<M1, M2, M3>(
clocks: Clocks, spi4: SPI4,
sclk: PE2<M1>, sync: PE4<M2>, sdin: PE6<M3>
) -> (Dac0Spi, PE4<Output<PushPull>>) {
) -> (Dac0Spi, <Channel0 as ChannelPins>::DacSync) {
let sclk = sclk.into_alternate_af5();
let sdin = sdin.into_alternate_af5();
let spi = Spi::spi4(
@ -213,7 +263,7 @@ 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>>) {
) -> (Dac1Spi, <Channel1 as ChannelPins>::DacSync) {
let sclk = sclk.into_alternate_af5();
let sdin = sdin.into_alternate_af5();
let spi = Spi::spi5(
@ -227,32 +277,6 @@ impl Pins {
(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 {
@ -278,11 +302,17 @@ impl PwmPins {
) -> PwmPins {
let freq = 20u32.khz();
fn init_pwm_pin<P: hal::PwmPin<Duty=u16>>(pin: &mut P) {
pin.set_duty(0);
pin.enable();
}
let channels = (
max_v0.into_alternate_af2(),
max_v1.into_alternate_af2(),
);
let (max_v0, max_v1) = pwm::tim3(tim3, channels, clocks, freq);
let (mut max_v0, mut max_v1) = pwm::tim3(tim3, channels, clocks, freq);
init_pwm_pin(&mut max_v0);
init_pwm_pin(&mut max_v1);
let channels = (
max_i_pos0.into_alternate_af1(),
@ -290,8 +320,12 @@ impl PwmPins {
max_i_neg0.into_alternate_af1(),
max_i_neg1.into_alternate_af1(),
);
let (max_i_pos0, max_i_pos1, max_i_neg0, max_i_neg1) =
let (mut max_i_pos0, mut max_i_pos1, mut max_i_neg0, mut max_i_neg1) =
pwm::tim1(tim1, channels, clocks, freq);
init_pwm_pin(&mut max_i_pos0);
init_pwm_pin(&mut max_i_neg0);
init_pwm_pin(&mut max_i_pos1);
init_pwm_pin(&mut max_i_neg1);
PwmPins {
max_v0, max_v1,

View File

@ -3,6 +3,7 @@ use smoltcp::{
iface::EthernetInterface,
socket::{SocketSet, SocketHandle, TcpSocket, TcpSocketBuffer, SocketRef},
time::Instant,
wire::{IpCidr, Ipv4Address, Ipv4Cidr},
};
@ -83,4 +84,21 @@ impl<'a, 'b, S: Default> Server<'a, 'b, S> {
callback(socket, &mut state.state);
}
}
pub fn set_ipv4_address(&mut self, ipv4_address: Ipv4Address) {
self.net.update_ip_addrs(|addrs| {
for addr in addrs.iter_mut() {
match addr {
IpCidr::Ipv4(_) => {
*addr = IpCidr::Ipv4(Ipv4Cidr::new(ipv4_address, 0));
// done
break
}
_ => {
// skip
}
}
}
});
}
}

View File

@ -38,16 +38,16 @@ 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)
}
}
@ -106,7 +106,7 @@ impl Session {
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;
@ -125,6 +125,6 @@ impl Session {
None => {}
}
}
(buf_bytes, SessionOutput::Nothing)
(buf_bytes, SessionInput::Nothing)
}
}

View File

@ -1,31 +1,46 @@
use num_traits::float::Float;
use uom::si::{
f64::{
ElectricalResistance,
ThermodynamicTemperature,
},
electrical_resistance::ohm,
ratio::ratio,
thermodynamic_temperature::{degree_celsius, kelvin},
};
use serde::Serialize;
type JsonBuffer = heapless::Vec<u8, heapless::consts::U200>;
/// Steinhart-Hart equation parameters
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Serialize)]
pub struct Parameters {
pub t0: f64,
/// Base temperature
pub t0: ThermodynamicTemperature,
/// Base resistance
pub r0: ElectricalResistance,
/// Beta
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
pub fn get_temperature(&self, r: ElectricalResistance) -> ThermodynamicTemperature {
let inv_temp = 1.0 / self.t0.get::<kelvin>() + (r / self.r0).get::<ratio>().ln() / self.b;
ThermodynamicTemperature::new::<kelvin>(1.0 / inv_temp)
}
pub fn to_json(&self) -> Result<JsonBuffer, serde_json_core::ser::Error> {
serde_json_core::to_vec(self)
}
}
impl Default for Parameters {
fn default() -> Self {
Parameters {
t0: 0.001_4,
b: 0.000_000_099,
r0: 5_110.0,
t0: ThermodynamicTemperature::new::<degree_celsius>(25.0),
r0: ElectricalResistance::new::<ohm>(10_000.0),
b: 3800.0,
}
}
}

View File

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

View File

@ -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)
}
}

103
src/usb.rs Normal file
View File

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