Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4002a886a5 | ||
|
|
c64d145a1a | ||
|
|
58b3a97d73
|
||
|
|
2d6cfdb031
|
||
|
|
2744589110
|
||
|
|
71bfcd4cfd
|
||
|
|
1dba2e85f2
|
||
|
|
b3df9fedad
|
||
|
|
1c0ad62b79
|
||
|
|
739065409c
|
||
|
|
bf2ad52e0f
|
||
|
|
2332b507b2
|
||
|
|
83f54d5953
|
||
|
|
0437d30d04
|
Generated
+71
-53
@@ -46,8 +46,8 @@ dependencies = [
|
|||||||
"ch32-metapac 0.0.6",
|
"ch32-metapac 0.0.6",
|
||||||
"embassy-executor",
|
"embassy-executor",
|
||||||
"embassy-futures",
|
"embassy-futures",
|
||||||
"embassy-sync 0.7.2",
|
"embassy-sync",
|
||||||
"embassy-time 0.4.0",
|
"embassy-time",
|
||||||
"embassy-usb",
|
"embassy-usb",
|
||||||
"embedded-hal 1.0.0",
|
"embedded-hal 1.0.0",
|
||||||
"heapless",
|
"heapless",
|
||||||
@@ -57,6 +57,7 @@ dependencies = [
|
|||||||
"panic-halt",
|
"panic-halt",
|
||||||
"qingke",
|
"qingke",
|
||||||
"qingke-rt",
|
"qingke-rt",
|
||||||
|
"static_cell",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -75,23 +76,24 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "ch32-hal"
|
name = "ch32-hal"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/ju6ge/ch32-hal?branch=feature%2Fi2c-slave-api#9b1df89f5e8de21c7daad0f22fa7186dcfcbecdf"
|
source = "git+https://github.com/ju6ge/ch32-hal?branch=feature%2Fi2c-slave-api#dd285961d7c4604caf1f9cf3c306d4b0929e36c6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitmaps",
|
"bitmaps",
|
||||||
"ch32-metapac 0.1.0",
|
"ch32-metapac 0.1.0",
|
||||||
"critical-section",
|
"critical-section",
|
||||||
"embassy-futures",
|
"embassy-futures",
|
||||||
"embassy-hal-internal",
|
"embassy-hal-internal",
|
||||||
"embassy-sync 0.7.2",
|
"embassy-sync",
|
||||||
"embassy-time 0.5.0",
|
"embassy-time",
|
||||||
"embassy-time-driver",
|
"embassy-time-driver",
|
||||||
"embassy-time-queue-utils",
|
"embassy-time-queue-utils",
|
||||||
"embassy-usb-driver 0.2.0",
|
"embassy-usb-driver",
|
||||||
"embedded-can",
|
"embedded-can",
|
||||||
"embedded-hal 0.2.7",
|
"embedded-hal 0.2.7",
|
||||||
"embedded-hal 1.0.0",
|
"embedded-hal 1.0.0",
|
||||||
"embedded-hal-async",
|
"embedded-hal-async",
|
||||||
"embedded-hal-nb",
|
"embedded-hal-nb",
|
||||||
|
"embedded-storage",
|
||||||
"futures",
|
"futures",
|
||||||
"nb 1.1.0",
|
"nb 1.1.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -115,12 +117,21 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "ch32-metapac"
|
name = "ch32-metapac"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/ch32-rs/ch32-metapac?rev=b1cbc7a98e43af3fd3170821654784e2c01cb26b#b1cbc7a98e43af3fd3170821654784e2c01cb26b"
|
source = "git+https://github.com/ch32-rs/ch32-metapac?rev=7cd6fb70d07de3a987c63d0d8f5547bb8261e403#7cd6fb70d07de3a987c63d0d8f5547bb8261e403"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"riscv 0.11.1",
|
"riscv 0.12.1",
|
||||||
"vcell",
|
"vcell",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chrono"
|
||||||
|
version = "0.4.44"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
|
||||||
|
dependencies = [
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "critical-section"
|
name = "critical-section"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
@@ -229,21 +240,7 @@ checksum = "b7b2739fbcf6cd206ae08779c7d709087b16577d255f2ea4a45bc4bbbf305b3f"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"embassy-futures",
|
"embassy-futures",
|
||||||
"embassy-net-driver",
|
"embassy-net-driver",
|
||||||
"embassy-sync 0.7.2",
|
"embassy-sync",
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "embassy-sync"
|
|
||||||
version = "0.6.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8d2c8cdff05a7a51ba0087489ea44b0b1d97a296ca6b1d6d1a33ea7423d34049"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"critical-section",
|
|
||||||
"embedded-io-async",
|
|
||||||
"futures-sink",
|
|
||||||
"futures-util",
|
|
||||||
"heapless",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -260,22 +257,6 @@ dependencies = [
|
|||||||
"heapless",
|
"heapless",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "embassy-time"
|
|
||||||
version = "0.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f820157f198ada183ad62e0a66f554c610cdcd1a9f27d4b316358103ced7a1f8"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"critical-section",
|
|
||||||
"document-features",
|
|
||||||
"embassy-time-driver",
|
|
||||||
"embedded-hal 0.2.7",
|
|
||||||
"embedded-hal 1.0.0",
|
|
||||||
"embedded-hal-async",
|
|
||||||
"futures-util",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "embassy-time"
|
name = "embassy-time"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@@ -286,6 +267,7 @@ dependencies = [
|
|||||||
"critical-section",
|
"critical-section",
|
||||||
"document-features",
|
"document-features",
|
||||||
"embassy-time-driver",
|
"embassy-time-driver",
|
||||||
|
"embassy-time-queue-utils",
|
||||||
"embedded-hal 0.2.7",
|
"embedded-hal 0.2.7",
|
||||||
"embedded-hal 1.0.0",
|
"embedded-hal 1.0.0",
|
||||||
"embedded-hal-async",
|
"embedded-hal-async",
|
||||||
@@ -313,28 +295,20 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "embassy-usb"
|
name = "embassy-usb"
|
||||||
version = "0.3.0"
|
version = "0.5.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8d0b882133fa684b9d4652351cd7aac5afe8a2c2bf4a7da59f442ff61087cda2"
|
checksum = "dc4462e48b19a4f401a11901bdd981aab80c6a826608016a0bdc73cbbab31954"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"embassy-futures",
|
"embassy-futures",
|
||||||
"embassy-net-driver-channel",
|
"embassy-net-driver-channel",
|
||||||
"embassy-sync 0.6.2",
|
"embassy-sync",
|
||||||
"embassy-usb-driver 0.1.1",
|
"embassy-usb-driver",
|
||||||
|
"embedded-io-async",
|
||||||
"heapless",
|
"heapless",
|
||||||
"ssmarshal",
|
"ssmarshal",
|
||||||
"usbd-hid",
|
"usbd-hid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "embassy-usb-driver"
|
|
||||||
version = "0.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "340c5ce591ef58c6449e43f51d2c53efe1bf0bb6a40cbf80afa0d259c7d52c76"
|
|
||||||
dependencies = [
|
|
||||||
"embedded-io-async",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "embassy-usb-driver"
|
name = "embassy-usb-driver"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -403,6 +377,12 @@ dependencies = [
|
|||||||
"embedded-io",
|
"embedded-io",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "embedded-storage"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a21dea9854beb860f3062d10228ce9b976da520a73474aed3171ec276bc0c032"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "encode_unicode"
|
name = "encode_unicode"
|
||||||
version = "0.3.6"
|
version = "0.3.6"
|
||||||
@@ -513,6 +493,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad"
|
checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hash32",
|
"hash32",
|
||||||
|
"portable-atomic",
|
||||||
"stable_deref_trait",
|
"stable_deref_trait",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -536,6 +517,7 @@ dependencies = [
|
|||||||
name = "lib-bms-protocol"
|
name = "lib-bms-protocol"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
"embedded-hal 1.0.0",
|
"embedded-hal 1.0.0",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
@@ -620,6 +602,9 @@ name = "portable-atomic"
|
|||||||
version = "1.13.0"
|
version = "1.13.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950"
|
checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950"
|
||||||
|
dependencies = [
|
||||||
|
"critical-section",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro-error"
|
name = "proc-macro-error"
|
||||||
@@ -713,6 +698,19 @@ dependencies = [
|
|||||||
"embedded-hal 1.0.0",
|
"embedded-hal 1.0.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "riscv"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5ea8ff73d3720bdd0a97925f0bf79ad2744b6da8ff36be3840c48ac81191d7a7"
|
||||||
|
dependencies = [
|
||||||
|
"critical-section",
|
||||||
|
"embedded-hal 1.0.0",
|
||||||
|
"paste",
|
||||||
|
"riscv-macros 0.1.0",
|
||||||
|
"riscv-pac",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "riscv"
|
name = "riscv"
|
||||||
version = "0.15.0"
|
version = "0.15.0"
|
||||||
@@ -722,10 +720,21 @@ dependencies = [
|
|||||||
"critical-section",
|
"critical-section",
|
||||||
"embedded-hal 1.0.0",
|
"embedded-hal 1.0.0",
|
||||||
"paste",
|
"paste",
|
||||||
"riscv-macros",
|
"riscv-macros 0.3.0",
|
||||||
"riscv-pac",
|
"riscv-pac",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "riscv-macros"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f265be5d634272320a7de94cea15c22a3bfdd4eb42eb43edc528415f066a1f25"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.111",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "riscv-macros"
|
name = "riscv-macros"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
@@ -794,6 +803,15 @@ version = "1.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "static_cell"
|
||||||
|
version = "2.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0530892bb4fa575ee0da4b86f86c667132a94b74bb72160f58ee5a4afec74c23"
|
||||||
|
dependencies = [
|
||||||
|
"portable-atomic",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strsim"
|
name = "strsim"
|
||||||
version = "0.11.1"
|
version = "0.11.1"
|
||||||
|
|||||||
@@ -10,5 +10,7 @@ rustflags = [
|
|||||||
# runner = "gdb -q -x openocd.gdb"
|
# runner = "gdb -q -x openocd.gdb"
|
||||||
# runner = "wlink -v flash"
|
# runner = "wlink -v flash"
|
||||||
|
|
||||||
runner = "wlink -v flash --enable-sdi-print --watch-serial --erase"
|
|
||||||
|
runner = "wchisp flash"
|
||||||
|
#runner = "wlink -v flash --enable-sdi-print --watch-serial --erase"
|
||||||
# runner = "wlink -v flash"
|
# runner = "wlink -v flash"
|
||||||
|
|||||||
+10
-4
@@ -14,14 +14,20 @@ ch32-hal = { git = "https://github.com/ju6ge/ch32-hal", branch = "feature/i2c-sl
|
|||||||
], default-features = false }
|
], default-features = false }
|
||||||
|
|
||||||
embassy-executor = { version = "0.7.0", features = [
|
embassy-executor = { version = "0.7.0", features = [
|
||||||
"arch-riscv32",
|
# arch-riscv32's WFI-based sleep races with the USB interrupt wake signal,
|
||||||
|
# so tasks awaiting USB (e.g. wait_connection/write_packet) can hang
|
||||||
|
# forever even though the device enumerates fine. arch-spin busy-polls
|
||||||
|
# instead of sleeping, sidestepping the race. See CAN_Sensor/Cargo.toml
|
||||||
|
# for the same fix on the same ch32-hal/embassy-executor combo.
|
||||||
|
"arch-spin",
|
||||||
"executor-thread",
|
"executor-thread",
|
||||||
] }
|
] }
|
||||||
|
|
||||||
embassy-time = { version = "0.4.0" }
|
embassy-time = { version = "0.5.0", features = ["generic-queue-8"] }
|
||||||
embassy-usb = { version = "0.3.0" }
|
embassy-usb = { version = "0.5.1" }
|
||||||
embassy-futures = { version = "0.1.0" }
|
embassy-futures = { version = "0.1.0" }
|
||||||
embassy-sync = { version = "0.7.2" }
|
embassy-sync = { version = "0.7.2" }
|
||||||
|
static_cell = "2.1.1"
|
||||||
|
|
||||||
# This is okay because we should automatically use whatever ch32-hal uses
|
# This is okay because we should automatically use whatever ch32-hal uses
|
||||||
qingke-rt = "*"
|
qingke-rt = "*"
|
||||||
@@ -30,7 +36,7 @@ qingke = "*"
|
|||||||
panic-halt = "1.0"
|
panic-halt = "1.0"
|
||||||
|
|
||||||
embedded-hal = "1.0.0"
|
embedded-hal = "1.0.0"
|
||||||
heapless = "0.8.0"
|
heapless = { version = "0.8.0", features = ["portable-atomic-critical-section"] }
|
||||||
micromath = { version = "2.1.0", features = ["num-traits"] }
|
micromath = { version = "2.1.0", features = ["num-traits"] }
|
||||||
ch32-metapac = { version = "0.0.6", features = ["ch32v203c8t6"] }
|
ch32-metapac = { version = "0.0.6", features = ["ch32v203c8t6"] }
|
||||||
ina219 = "0.2.0"
|
ina219 = "0.2.0"
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
# Hardware implementor's notes
|
||||||
|
|
||||||
|
Reference for anyone touching `firmware/src/*.rs`: what's wired to what, which
|
||||||
|
peripherals/pins are used and why, and the non-obvious dependency-version
|
||||||
|
constraints this firmware relies on. Written against the real, currently
|
||||||
|
maintained schematic at `/home/empire/workspace/PlantCtrl/Hardware/open-bms/bms/bms.kicad_sch`.
|
||||||
|
|
||||||
|
**The KiCad project checked into *this* repo (`board/bms/`) is a stale, older
|
||||||
|
board revision.** Its component references and pin assignments do not match
|
||||||
|
the hardware this firmware actually targets — do not use it as a source of
|
||||||
|
truth. Re-derive facts from the real schematic (e.g. via
|
||||||
|
`kicad-cli sch export netlist`) if anything below is in doubt.
|
||||||
|
|
||||||
|
Target chip: **CH32V203C8T6** (RISC-V, `riscv32imc-unknown-none-elf`), no
|
||||||
|
external crystal populated — clocked from HSI only (see RCC note below).
|
||||||
|
|
||||||
|
## Pin map
|
||||||
|
|
||||||
|
| Pin | Function | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| PA8 | D7 LED | Active-high, 2.2kΩ (R13) to GND on the cathode side |
|
||||||
|
| PB12 | D3 LED | Active-high, 2.2kΩ (R14) to GND |
|
||||||
|
| PB13 | D4 LED | Active-high, 2.2kΩ (R15) to GND |
|
||||||
|
| PB14 | D5 LED | Active-high, 2.2kΩ (R16) to GND |
|
||||||
|
| PB15 | D6 LED | Active-high, 2.2kΩ (R17) to GND |
|
||||||
|
| PA11 | USB D− | Fixed-function USB pin on this chip, not remappable |
|
||||||
|
| PA12 | USB D+ | Fixed-function USB pin on this chip, not remappable |
|
||||||
|
| PA4 | SPI1 NSS (flash CS) | Driven manually as a GPIO `Output` — `ch32-hal`'s `Spi` has no CS management. Idle-high, matches the board's 10kΩ pull-up (R7) |
|
||||||
|
| PA5 | SPI1 SCK | Default/non-remapped SPI1 pin |
|
||||||
|
| PA6 | SPI1 MISO | Default/non-remapped SPI1 pin |
|
||||||
|
| PA7 | SPI1 MOSI | Default/non-remapped SPI1 pin |
|
||||||
|
| PB9 | INA219 SDA | **Bit-banged**, not hardware I2C — see gotcha below |
|
||||||
|
| PB10 | INA219 SCL | **Bit-banged**, not hardware I2C — see gotcha below |
|
||||||
|
| PB2 | Spare GPIO input | Pulled low via 10kΩ (R8), nothing else attached. Only real "input" available on this board |
|
||||||
|
| PB6 | I2C1 SDA (`S_SDA`) | Battery-pack-facing bus (XT30 connector pins 3/4, BAV99 protection diodes). Currently unused — see I2C1 note below |
|
||||||
|
| PB7 | I2C1 SCL (`S_SCL`) | Battery-pack-facing bus. Currently unused — see I2C1 note below |
|
||||||
|
|
||||||
|
Not wired to anything: PA0-3, PA9-10, PA15, PB0-1, PB3-5, PB8, PB11, PC13-15.
|
||||||
|
`Boot1`/`Reset1` are the BOOT0 strap and NRST reset buttons respectively —
|
||||||
|
not runtime-readable GPIOs, don't try to poll them as inputs.
|
||||||
|
|
||||||
|
## Peripherals in use
|
||||||
|
|
||||||
|
- **USBD** — USB CDC-ACM virtual serial console. `hal::usbd::Driver::new(p.USBD, Irqs, p.PA12 /*dp*/, p.PA11 /*dm*/)`. Interrupt vector is shared with CAN1: `bind_interrupts!` needs `USB_LP_CAN1_RX0 => hal::usbd::InterruptHandler<hal::peripherals::USBD>`. Max packet size is 64 bytes — `write_packet()` on a single call **fails** (`EndpointError::BufferOverflow`, does not panic) for anything longer; chunk before sending (see `usb_writer` in `main.rs`).
|
||||||
|
- **SPI1** — blocking, talks to the W25Q128JVE NOR flash (U5). `hal::spi::Spi::new_blocking::<0>(p.SPI1, sck, mosi, miso, config)` — note the argument order is `(sck, mosi, miso)`, not alphabetical. No hardware CS support in this driver; drive it yourself.
|
||||||
|
- **I2C1** — hardware peripheral, PB6/PB7 (remap 0). Intended for the battery-pack-facing slave bus (device acts as an I2C *slave*, address `0x55` in the existing but currently-commented-out code). **Not currently enabled** — `I2c::listen_blocking()` is a genuine busy-spin with no `.await`, which starves every other embassy task (USB included) on this single-threaded executor the instant it's called. Don't re-enable it without either moving it off the main task's critical path or replacing it with a non-blocking variant.
|
||||||
|
- **I2C2** — hardware peripheral, fixed to PB10 (SCL) / PB11 (SDA), no remap. **Do not use for the INA219.** PB11 is completely unconnected on this board. See the bit-bang gotcha below.
|
||||||
|
- **TIM2** — claimed by `ch32-hal`'s `time-driver-tim2` feature as the embassy time driver's tick source. Don't reuse TIM2 directly for anything else.
|
||||||
|
|
||||||
|
## Hardware gotcha: the INA219 bus needs software (bit-banged) I2C
|
||||||
|
|
||||||
|
The schematic labels a bus "SCL"/"SDA" going to the INA219 (U3), wired to
|
||||||
|
PB10 and PB9. This looks like it should be a normal hardware I2C bus, but
|
||||||
|
**it isn't reachable by any single hardware I2C peripheral on this chip**:
|
||||||
|
|
||||||
|
- I2C2 is fixed to PB10=SCL, **PB11**=SDA (no remap available). PB11 is
|
||||||
|
unconnected on this board.
|
||||||
|
- I2C1 remap 1 is **PB8**=SCL, PB9=SDA. PB8 is unconnected on this board.
|
||||||
|
|
||||||
|
PB10 only pairs (in hardware) with PB11; PB9 only pairs with PB8. Neither
|
||||||
|
partner pin is wired to the sensor. This is a board wiring quirk, not
|
||||||
|
something fixable via `ch32-hal` configuration. `firmware/src/selfcheck.rs`
|
||||||
|
therefore implements a minimal bit-banged I2C driver (`BitbangI2c`) over
|
||||||
|
plain open-drain GPIOs (`hal::gpio::Flex`) on PB9/PB10, implementing
|
||||||
|
`embedded_hal::i2c::I2c` via its single required `transaction()` method (the
|
||||||
|
`read`/`write`/`write_read` provided methods come for free from that). It
|
||||||
|
does **not** support clock stretching — fine for the INA219, which doesn't
|
||||||
|
stretch, but keep that in mind if another device ever goes on this bus.
|
||||||
|
|
||||||
|
INA219 address: **0x48** — `A0`→GND, `A1`→SDA on this board, i.e.
|
||||||
|
`ina219::address::Address::from_pins(Pin::Gnd, Pin::Sda)`. Not the more
|
||||||
|
common default addresses seen in INA219 examples/datasheet defaults.
|
||||||
|
|
||||||
|
Current-sense shunt: R4 ‖ R5, 100mΩ each → **50mΩ** combined. Current is
|
||||||
|
derived in software from `shunt_voltage_uv() / 50`, not from the INA219's
|
||||||
|
own calibration registers (`IntCalibration`) — this board's real max current
|
||||||
|
draw isn't documented anywhere, so guessing a `current_lsb` for hardware
|
||||||
|
calibration seemed worse than a simple, honest Ohm's-law calculation using
|
||||||
|
the known shunt value.
|
||||||
|
|
||||||
|
## RCC / clock configuration
|
||||||
|
|
||||||
|
`hal::init` is called with `rcc: hal::rcc::Config::SYSCLK_FREQ_144MHZ_HSI`,
|
||||||
|
**not** the HAL's default config. This is required for USB to get a valid
|
||||||
|
48MHz derived clock, and it's HSI-based because there's no external crystal
|
||||||
|
on this board (`OSC_IN`/`OSC_OUT` are unconnected). If you ever add code
|
||||||
|
that assumes a different clock tree, check this first.
|
||||||
|
|
||||||
|
## Dependency versions that matter (not arbitrary pins in `Cargo.toml`)
|
||||||
|
|
||||||
|
These aren't just "whatever version worked" — each one fixes a real,
|
||||||
|
previously-hit build or runtime failure. Don't casually bump/change them
|
||||||
|
without re-verifying:
|
||||||
|
|
||||||
|
- **`embassy-executor` features = `["arch-spin", "executor-thread"]`** — NOT
|
||||||
|
`arch-riscv32`. `arch-riscv32`'s WFI-based sleep races with the USB
|
||||||
|
interrupt's wake signal: if the interrupt fires right as the core is about
|
||||||
|
to sleep, the wake can be missed and anything `.await`ing on it (e.g.
|
||||||
|
`wait_connection()`/`write_packet()`) hangs forever, even though the
|
||||||
|
device still enumerates fine. `arch-spin` busy-polls instead of sleeping,
|
||||||
|
sidestepping the race. (Symptom if this regresses: USB enumerates, but no
|
||||||
|
console output ever appears.) Confirmed the same fix is needed in the
|
||||||
|
sibling `PlantCtrl/Software/CAN_Sensor` project on the same HAL fork.
|
||||||
|
- **`embassy-time` features = `["generic-queue-8"]`, version `"0.5.0"`** —
|
||||||
|
needs to match the `embassy-time` version `ch32-hal` itself pulls in
|
||||||
|
transitively (currently 0.5.x); a direct-dependency version mismatch
|
||||||
|
causes a Cargo `links` conflict (`embassy-time-queue-utils` can't resolve
|
||||||
|
two different versions simultaneously). The `generic-queue-8` feature
|
||||||
|
makes `embassy-time` use its own self-contained timer queue instead of
|
||||||
|
requiring `embassy-executor` to implement an "integrated timer queue"
|
||||||
|
symbol that our pinned `embassy-executor 0.7.0` doesn't provide (that
|
||||||
|
support landed in later `embassy-executor` versions).
|
||||||
|
- **`embassy-usb = "0.5.1"`** — NOT 0.3.0/0.4.0. Those depend on
|
||||||
|
`embassy-usb-driver ^0.1.0`; `ch32-hal`'s `hal::usbd::Driver` implements
|
||||||
|
the `embassy-usb-driver 0.2.0` traits. `embassy-usb` versions ≥0.5.0 are
|
||||||
|
the first to depend on `embassy-usb-driver ^0.2.0`. Using an older
|
||||||
|
`embassy-usb` here is a hard type-check failure, not a subtle bug.
|
||||||
|
- **`heapless` feature = `"portable-atomic-critical-section"`** — needed for
|
||||||
|
`static_cell` (used for the `'static` USB buffers/class/device via the
|
||||||
|
`mk_static!` macro in `main.rs`) to build at all. `riscv32imc` has no
|
||||||
|
native atomic compare-and-swap instruction; without this feature,
|
||||||
|
`static_cell`'s internal `AtomicBool::compare_exchange` fails to compile
|
||||||
|
for this target. This feature makes `portable-atomic` (a shared transitive
|
||||||
|
dependency) emulate CAS via critical sections instead.
|
||||||
|
- **`ch32-hal`** — git dependency on `ju6ge/ch32-hal`, branch
|
||||||
|
`feature/i2c-slave-api` (upstream `ch32-rs/ch32-hal` does **not** have the
|
||||||
|
I2C slave-mode API this firmware's (currently disabled) I2C1 code depends
|
||||||
|
on — checked directly, no `SlaveConfig`/`listen_blocking` there). This
|
||||||
|
branch has been force-pushed/rebased before (the previously-locked commit
|
||||||
|
disappeared from GitHub entirely) — if `cargo build` ever fails to fetch
|
||||||
|
the pinned commit in `Cargo.lock`, that's almost certainly why; re-run
|
||||||
|
`cargo update -p ch32-hal` to pick up the branch's current tip.
|
||||||
|
- Entry point is `qingke_rt::entry` (via
|
||||||
|
`#[embassy_executor::main(entry = "qingke_rt::entry")]`), matching
|
||||||
|
`ch32-hal`'s own examples for this chip family.
|
||||||
|
|
||||||
|
## Logging
|
||||||
|
|
||||||
|
`println!` in `main.rs` is a **local macro**, not `ch32_hal::println!` — the
|
||||||
|
latter writes over WCH's SDI single-wire debug protocol (needs a WCH-Link
|
||||||
|
probe attached, see `hal`'s `debug.rs`). The local one formats into a
|
||||||
|
`heapless::String<128>` and pushes it onto a channel (`LOG_CH`) that a
|
||||||
|
spawned `usb_writer` task drains and writes out over the USB CDC-ACM
|
||||||
|
console. It's `#[macro_export]`ed with `$crate::LOG_CH` internally so other
|
||||||
|
modules (e.g. `selfcheck.rs`) can call `crate::println!(...)` and have it
|
||||||
|
land in the same place. Sends are non-blocking (`try_send`) and silently
|
||||||
|
drop the line if the channel (capacity 8) is full — there's no backpressure,
|
||||||
|
so a burst of many `println!` calls in a tight loop with no `.await` in
|
||||||
|
between can lose messages if nothing's draining the channel yet (e.g. no
|
||||||
|
terminal connected). USB CDC devices also re-enumerate on every reflash;
|
||||||
|
most terminal programs need to be manually reconnected afterward.
|
||||||
+159
-29
@@ -4,24 +4,53 @@
|
|||||||
#![feature(impl_trait_in_assoc_type)]
|
#![feature(impl_trait_in_assoc_type)]
|
||||||
|
|
||||||
mod measurement;
|
mod measurement;
|
||||||
|
mod selfcheck;
|
||||||
|
|
||||||
use ch32_hal::i2c::mode::OperatingMode;
|
use ch32_hal::i2c::mode::OperatingMode;
|
||||||
use ch32_hal::mode::Mode;
|
use ch32_hal::mode::Mode;
|
||||||
use ch32_hal::println;
|
use ch32_hal::peripherals::USBD;
|
||||||
use ch32_hal::time::Hertz;
|
use core::fmt::Write as _;
|
||||||
use embassy_executor::Spawner;
|
use embassy_executor::Spawner;
|
||||||
use embassy_sync::blocking_mutex::raw::{CriticalSectionRawMutex, NoopRawMutex};
|
use embassy_sync::blocking_mutex::raw::{CriticalSectionRawMutex, NoopRawMutex};
|
||||||
|
use embassy_sync::channel::Channel;
|
||||||
use embassy_sync::lazy_lock::LazyLock;
|
use embassy_sync::lazy_lock::LazyLock;
|
||||||
use embassy_sync::mutex::Mutex;
|
use embassy_sync::mutex::Mutex;
|
||||||
use embassy_sync::rwlock::RwLock;
|
use embassy_sync::rwlock::RwLock;
|
||||||
//use embassy_time::{Duration, Timer};
|
use embassy_time::Timer;
|
||||||
|
use embassy_usb::class::cdc_acm::{CdcAcmClass, State};
|
||||||
|
use embassy_usb::{Builder, UsbDevice};
|
||||||
use hal::bind_interrupts;
|
use hal::bind_interrupts;
|
||||||
use hal::i2c::{Config, I2c, SlaveAddress, SlaveConfig};
|
use hal::gpio::{Flex, Input, Level, Output, Pull};
|
||||||
|
use hal::i2c::I2c;
|
||||||
|
use hal::spi::Spi;
|
||||||
|
use hal::usbd::Driver;
|
||||||
use {ch32_hal as hal, panic_halt as _};
|
use {ch32_hal as hal, panic_halt as _};
|
||||||
|
|
||||||
|
macro_rules! mk_static {
|
||||||
|
($t:ty,$val:expr) => {{
|
||||||
|
static STATIC_CELL: static_cell::StaticCell<$t> = static_cell::StaticCell::new();
|
||||||
|
#[deny(unused_attributes)]
|
||||||
|
let x = STATIC_CELL.uninit().write(($val));
|
||||||
|
x
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) static LOG_CH: Channel<CriticalSectionRawMutex, heapless::String<128>, 8> = Channel::new();
|
||||||
|
|
||||||
|
/// Formats like `std::println!`, but sends the line over USB CDC instead of stdout.
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! println {
|
||||||
|
($($arg:tt)*) => {{
|
||||||
|
let mut s: heapless::String<128> = heapless::String::new();
|
||||||
|
let _ = core::write!(&mut s, $($arg)*);
|
||||||
|
let _ = $crate::LOG_CH.try_send(s);
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
bind_interrupts!(struct Irqs {
|
bind_interrupts!(struct Irqs {
|
||||||
I2C1_EV => ch32_hal::i2c::EventInterruptHandler<ch32_hal::peripherals::I2C1>;
|
I2C1_EV => ch32_hal::i2c::EventInterruptHandler<ch32_hal::peripherals::I2C1>;
|
||||||
I2C1_ER => ch32_hal::i2c::ErrorInterruptHandler<ch32_hal::peripherals::I2C1>;
|
I2C1_ER => ch32_hal::i2c::ErrorInterruptHandler<ch32_hal::peripherals::I2C1>;
|
||||||
|
USB_LP_CAN1_RX0 => hal::usbd::InterruptHandler<hal::peripherals::USBD>;
|
||||||
});
|
});
|
||||||
|
|
||||||
const MEASUREMENT_SAMPLES_CAPACITY: usize = 1024;
|
const MEASUREMENT_SAMPLES_CAPACITY: usize = 1024;
|
||||||
@@ -38,36 +67,137 @@ async fn measurement_task<'m, M: Mode, O: OperatingMode>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[embassy_executor::main(entry = "qingke_rt::entry")]
|
#[embassy_executor::main(entry = "qingke_rt::entry")]
|
||||||
async fn main(_spawner: Spawner) -> ! {
|
async fn main(spawner: Spawner) {
|
||||||
let p = hal::init(Default::default());
|
let p = hal::init(hal::Config {
|
||||||
|
rcc: hal::rcc::Config::SYSCLK_FREQ_144MHZ_HSI,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
let config = Config::default();
|
// D7 startup blink so we can visually confirm the board booted before it
|
||||||
let mut i2c_slave = I2c::new_blocking(p.I2C1, p.PB6, p.PB7, Hertz::khz(100), config)
|
// goes quiet waiting on the I2C bus.
|
||||||
.into_slave(SlaveConfig {
|
let mut led = Output::new(p.PA8, Level::Low, Default::default());
|
||||||
address: SlaveAddress::SevenBit(0x55),
|
for _ in 0..5 {
|
||||||
general_call: false,
|
led.toggle();
|
||||||
});
|
Timer::after_millis(200).await;
|
||||||
|
}
|
||||||
|
led.set_low();
|
||||||
|
|
||||||
|
// USB CDC-ACM console: lets us `println!` self-test output over the USB
|
||||||
|
// cable instead of needing a WCH-Link SWD probe attached.
|
||||||
|
let driver = Driver::new(p.USBD, Irqs, p.PA12, p.PA11);
|
||||||
|
|
||||||
|
let mut usb_config = embassy_usb::Config::new(0xC0DE, 0xCAFE);
|
||||||
|
usb_config.manufacturer = Some("ch32-bms");
|
||||||
|
usb_config.product = Some("BMS debug console");
|
||||||
|
usb_config.serial_number = Some("12345678");
|
||||||
|
usb_config.max_power = 100;
|
||||||
|
usb_config.max_packet_size_0 = 64;
|
||||||
|
// Windows compatibility requires these; CDC-ACM
|
||||||
|
usb_config.device_class = 0x02;
|
||||||
|
usb_config.device_sub_class = 0x02;
|
||||||
|
usb_config.device_protocol = 0x00;
|
||||||
|
usb_config.composite_with_iads = false;
|
||||||
|
|
||||||
|
let mut builder = Builder::new(
|
||||||
|
driver,
|
||||||
|
usb_config,
|
||||||
|
mk_static!([u8; 256], [0; 256]),
|
||||||
|
mk_static!([u8; 256], [0; 256]),
|
||||||
|
&mut [], // no msos descriptors
|
||||||
|
mk_static!([u8; 64], [0; 64]),
|
||||||
|
);
|
||||||
|
let class = mk_static!(
|
||||||
|
CdcAcmClass<'static, Driver<'static, USBD>>,
|
||||||
|
CdcAcmClass::new(&mut builder, mk_static!(State, State::new()), 64)
|
||||||
|
);
|
||||||
|
let usb = mk_static!(UsbDevice<Driver<USBD>>, builder.build());
|
||||||
|
|
||||||
|
spawner.spawn(usb_task(usb)).unwrap();
|
||||||
|
spawner.spawn(usb_writer(class)).unwrap();
|
||||||
|
|
||||||
|
println!("BMS booted, D7 blink done, USB console up");
|
||||||
|
|
||||||
|
// Power-on self-check: exercises the other LEDs, the spare input pin,
|
||||||
|
// the INA219 current sensor, and the SPI flash, reporting over USB.
|
||||||
|
let mut led_d3 = Output::new(p.PB12, Level::Low, Default::default());
|
||||||
|
let mut led_d4 = Output::new(p.PB13, Level::Low, Default::default());
|
||||||
|
let mut led_d5 = Output::new(p.PB14, Level::Low, Default::default());
|
||||||
|
let mut led_d6 = Output::new(p.PB15, Level::Low, Default::default());
|
||||||
|
let input_pb2 = Input::new(p.PB2, Pull::None);
|
||||||
|
let mut spi = Spi::new_blocking::<0>(p.SPI1, p.PA5, p.PA7, p.PA6, Default::default());
|
||||||
|
let mut flash_cs = Output::new(p.PA4, Level::High, Default::default());
|
||||||
|
let mut i2c_scl = Flex::new(p.PB10);
|
||||||
|
let mut i2c_sda = Flex::new(p.PB9);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
match i2c_slave.listen_blocking() {
|
selfcheck::run(
|
||||||
Ok(command) => {
|
&mut led,
|
||||||
match command {
|
&mut led_d3,
|
||||||
ch32_hal::i2c::SlaveCommand::GeneralCall => { /* will not be triggered because we disabled it */
|
&mut led_d4,
|
||||||
}
|
&mut led_d5,
|
||||||
ch32_hal::i2c::SlaveCommand::ReadCommand => {
|
&mut led_d6,
|
||||||
// send empty response
|
&input_pb2,
|
||||||
let _ = i2c_slave.blocking_write_timeout(&[0x05, 0x01]);
|
&mut spi,
|
||||||
}
|
&mut flash_cs,
|
||||||
ch32_hal::i2c::SlaveCommand::WriteCommand => {
|
&mut i2c_scl,
|
||||||
let mut buf: [u8; 3] = [0x0, 0x0, 0x0];
|
&mut i2c_sda,
|
||||||
let _ = i2c_slave.blocking_read_timeout(&mut buf);
|
)
|
||||||
println!("received byte 0x{buf:x?}");
|
.await;
|
||||||
}
|
Timer::after_secs(5).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
// I2C slave listen loop disabled for now: it busy-waits with no `.await`,
|
||||||
|
// which would starve the USB tasks above on this single-threaded executor.
|
||||||
|
// let config = hal::i2c::Config::default();
|
||||||
|
// let mut i2c_slave = I2c::new_blocking(p.I2C1, p.PB6, p.PB7, hal::time::Hertz::khz(100), config)
|
||||||
|
// .into_slave(hal::i2c::SlaveConfig {
|
||||||
|
// address: hal::i2c::SlaveAddress::SevenBit(0x55),
|
||||||
|
// general_call: false,
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// loop {
|
||||||
|
// match i2c_slave.listen_blocking() {
|
||||||
|
// Ok(command) => {
|
||||||
|
// match command {
|
||||||
|
// ch32_hal::i2c::SlaveCommand::GeneralCall => { /* will not be triggered because we disabled it */
|
||||||
|
// }
|
||||||
|
// ch32_hal::i2c::SlaveCommand::ReadCommand => {
|
||||||
|
// // send empty response
|
||||||
|
// let _ = i2c_slave.blocking_write_timeout(&[0x05, 0x01]);
|
||||||
|
// }
|
||||||
|
// ch32_hal::i2c::SlaveCommand::WriteCommand => {
|
||||||
|
// let mut buf: [u8; 3] = [0x0, 0x0, 0x0];
|
||||||
|
// let _ = i2c_slave.blocking_read_timeout(&mut buf);
|
||||||
|
// println!("received byte 0x{buf:x?}");
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// Err(err) => {
|
||||||
|
// println!("{:?}", err)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[embassy_executor::task]
|
||||||
|
async fn usb_task(usb: &'static mut UsbDevice<'static, Driver<'static, USBD>>) {
|
||||||
|
usb.run().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[embassy_executor::task]
|
||||||
|
async fn usb_writer(class: &'static mut CdcAcmClass<'static, Driver<'static, USBD>>) {
|
||||||
|
loop {
|
||||||
|
class.wait_connection().await;
|
||||||
|
'connected: loop {
|
||||||
|
let msg = LOG_CH.receive().await;
|
||||||
|
// write_packet() fails outright for any single write over the
|
||||||
|
// endpoint's max packet size (64 bytes here), so chunk it.
|
||||||
|
for chunk in msg.as_bytes().chunks(64) {
|
||||||
|
if class.write_packet(chunk).await.is_err() {
|
||||||
|
// Disconnected or endpoint disabled; wait for reconnection.
|
||||||
|
break 'connected;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
|
||||||
println!("{:?}", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
|
|
||||||
use embassy_usb::msos::ConfigurationSubsetHeader;
|
|
||||||
use lib_bms_protocol::BatteryState;
|
use lib_bms_protocol::BatteryState;
|
||||||
|
|
||||||
use ina219::{address::Address, calibration::{IntCalibration, MicroAmpere, MicroWatt, UnCalibrated}, configuration::Configuration, errors::{ConfigurationReadError, InitializationError}, measurements::{CurrentRegister, Measurements, PowerRegister}, SyncIna219};
|
use ina219::{address::Address, calibration::{IntCalibration, MicroAmpere, MicroWatt, UnCalibrated}, configuration::Configuration, errors::{ConfigurationReadError, InitializationError}, measurements::{CurrentRegister, Measurements, PowerRegister}, SyncIna219};
|
||||||
|
|||||||
@@ -0,0 +1,219 @@
|
|||||||
|
//! Power-on self-check: exercises every external component on the board and
|
||||||
|
//! reports pass/fail plus readings over the USB console.
|
||||||
|
|
||||||
|
use core::fmt::Write as _;
|
||||||
|
use embassy_time::Timer;
|
||||||
|
use embedded_hal::i2c::{ErrorKind, ErrorType, I2c, NoAcknowledgeSource, Operation};
|
||||||
|
use hal::gpio::{Flex, Input, Output, Speed};
|
||||||
|
use hal::mode::Blocking;
|
||||||
|
use hal::spi::Spi;
|
||||||
|
use ina219::address::{Address, Pin as Ina219Pin};
|
||||||
|
use ina219::SyncIna219;
|
||||||
|
use {ch32_hal as hal, ch32_hal::peripherals::SPI1};
|
||||||
|
|
||||||
|
/// Bit-banged I2C on two plain GPIOs.
|
||||||
|
///
|
||||||
|
/// The INA219's I2C bus is wired to PB9 (SDA) and PB10 (SCL) on this board,
|
||||||
|
/// but those two pins belong to *different* hardware I2C peripherals (PB10 is
|
||||||
|
/// I2C2's SCL, whose SDA partner PB11 is unconnected here; PB9 is only valid
|
||||||
|
/// as I2C1's SDA under pin-remap, whose SCL partner PB8 is also unconnected).
|
||||||
|
/// No single hardware I2C instance can reach both pins as wired, so this bus
|
||||||
|
/// has to be driven as plain open-drain GPIOs instead. No clock stretching
|
||||||
|
/// support, same as most minimal bit-bang I2C implementations.
|
||||||
|
pub struct BitbangI2c<'a, 'd> {
|
||||||
|
scl: &'a mut Flex<'d>,
|
||||||
|
sda: &'a mut Flex<'d>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct BitbangI2cError;
|
||||||
|
|
||||||
|
impl embedded_hal::i2c::Error for BitbangI2cError {
|
||||||
|
fn kind(&self) -> ErrorKind {
|
||||||
|
ErrorKind::NoAcknowledge(NoAcknowledgeSource::Unknown)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'd> BitbangI2c<'a, 'd> {
|
||||||
|
pub fn new(scl: &'a mut Flex<'d>, sda: &'a mut Flex<'d>) -> Self {
|
||||||
|
scl.set_as_output_open_drain(Speed::Low);
|
||||||
|
sda.set_as_output_open_drain(Speed::Low);
|
||||||
|
scl.set_high();
|
||||||
|
sda.set_high();
|
||||||
|
let this = Self { scl, sda };
|
||||||
|
Self::half_delay();
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
fn half_delay() {
|
||||||
|
// ~10us at 144MHz -> ~50kHz bit-bang clock, well within I2C standard mode.
|
||||||
|
qingke::riscv::asm::delay(1500);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start(&mut self) {
|
||||||
|
self.sda.set_high();
|
||||||
|
self.scl.set_high();
|
||||||
|
Self::half_delay();
|
||||||
|
self.sda.set_low();
|
||||||
|
Self::half_delay();
|
||||||
|
self.scl.set_low();
|
||||||
|
Self::half_delay();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stop(&mut self) {
|
||||||
|
self.sda.set_low();
|
||||||
|
Self::half_delay();
|
||||||
|
self.scl.set_high();
|
||||||
|
Self::half_delay();
|
||||||
|
self.sda.set_high();
|
||||||
|
Self::half_delay();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_bit(&mut self, bit: bool) {
|
||||||
|
if bit {
|
||||||
|
self.sda.set_high();
|
||||||
|
} else {
|
||||||
|
self.sda.set_low();
|
||||||
|
}
|
||||||
|
Self::half_delay();
|
||||||
|
self.scl.set_high();
|
||||||
|
Self::half_delay();
|
||||||
|
self.scl.set_low();
|
||||||
|
Self::half_delay();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_bit(&mut self) -> bool {
|
||||||
|
self.sda.set_high(); // release so the slave can drive it
|
||||||
|
Self::half_delay();
|
||||||
|
self.scl.set_high();
|
||||||
|
Self::half_delay();
|
||||||
|
let bit = self.sda.is_high();
|
||||||
|
self.scl.set_low();
|
||||||
|
Self::half_delay();
|
||||||
|
bit
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_byte(&mut self, byte: u8) -> Result<(), BitbangI2cError> {
|
||||||
|
for i in (0..8).rev() {
|
||||||
|
self.write_bit((byte >> i) & 1 != 0);
|
||||||
|
}
|
||||||
|
if self.read_bit() {
|
||||||
|
// NACK
|
||||||
|
Err(BitbangI2cError)
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_byte(&mut self, ack: bool) -> u8 {
|
||||||
|
let mut byte = 0u8;
|
||||||
|
for _ in 0..8 {
|
||||||
|
byte = (byte << 1) | u8::from(self.read_bit());
|
||||||
|
}
|
||||||
|
self.write_bit(!ack);
|
||||||
|
byte
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'd> ErrorType for BitbangI2c<'a, 'd> {
|
||||||
|
type Error = BitbangI2cError;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'd> I2c for BitbangI2c<'a, 'd> {
|
||||||
|
fn transaction(&mut self, address: u8, operations: &mut [Operation<'_>]) -> Result<(), Self::Error> {
|
||||||
|
self.start();
|
||||||
|
let mut last_read: Option<bool> = None;
|
||||||
|
for op in operations.iter_mut() {
|
||||||
|
let is_read = matches!(op, Operation::Read(_));
|
||||||
|
if last_read != Some(is_read) {
|
||||||
|
if last_read.is_some() {
|
||||||
|
self.start(); // repeated start
|
||||||
|
}
|
||||||
|
self.write_byte((address << 1) | u8::from(is_read))?;
|
||||||
|
}
|
||||||
|
match op {
|
||||||
|
Operation::Write(data) => {
|
||||||
|
for &b in data.iter() {
|
||||||
|
self.write_byte(b)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Operation::Read(buf) => {
|
||||||
|
let len = buf.len();
|
||||||
|
for (i, b) in buf.iter_mut().enumerate() {
|
||||||
|
*b = self.read_byte(i + 1 != len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
last_read = Some(is_read);
|
||||||
|
}
|
||||||
|
self.stop();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Current-sense shunt on this board: R4 (100mOhm) parallel with R5 (100mOhm).
|
||||||
|
const SHUNT_MILLIOHM: i32 = 50;
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub async fn run<'d>(
|
||||||
|
d7: &mut Output<'d>,
|
||||||
|
d3: &mut Output<'d>,
|
||||||
|
d4: &mut Output<'d>,
|
||||||
|
d5: &mut Output<'d>,
|
||||||
|
d6: &mut Output<'d>,
|
||||||
|
input_pb2: &Input<'d>,
|
||||||
|
spi: &mut Spi<'d, SPI1, Blocking>,
|
||||||
|
flash_cs: &mut Output<'d>,
|
||||||
|
i2c_scl: &mut Flex<'d>,
|
||||||
|
i2c_sda: &mut Flex<'d>,
|
||||||
|
) {
|
||||||
|
crate::println!("=== self-check start ===");
|
||||||
|
|
||||||
|
crate::println!("-- LED outputs --");
|
||||||
|
let leds: [(&str, &mut Output<'d>); 5] = [("D3", d3), ("D4", d4), ("D5", d5), ("D6", d6), ("D7", d7)];
|
||||||
|
for (name, led) in leds {
|
||||||
|
crate::println!(" {name}: on");
|
||||||
|
led.set_high();
|
||||||
|
Timer::after_millis(150).await;
|
||||||
|
led.set_low();
|
||||||
|
}
|
||||||
|
|
||||||
|
crate::println!("-- input --");
|
||||||
|
crate::println!(" PB2: {}", if input_pb2.is_high() { "high" } else { "low" });
|
||||||
|
|
||||||
|
crate::println!("-- current sensor (INA219 via bit-banged I2C on PB9/PB10) --");
|
||||||
|
let i2c = BitbangI2c::new(i2c_scl, i2c_sda);
|
||||||
|
match SyncIna219::new(i2c, Address::from_pins(Ina219Pin::Gnd, Ina219Pin::Sda)) {
|
||||||
|
Ok(mut dev) => match (dev.bus_voltage(), dev.shunt_voltage()) {
|
||||||
|
(Ok(bus), Ok(shunt)) => {
|
||||||
|
let bus_mv = bus.voltage_mv();
|
||||||
|
let shunt_uv = shunt.shunt_voltage_uv();
|
||||||
|
let current_ma = shunt_uv / SHUNT_MILLIOHM;
|
||||||
|
crate::println!(
|
||||||
|
" PASS: bus={bus_mv}mV shunt={shunt_uv}uV current={current_ma}mA (shunt={SHUNT_MILLIOHM}mOhm)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_ => crate::println!(" FAIL: INA219 responded but a measurement read failed"),
|
||||||
|
},
|
||||||
|
Err(_) => crate::println!(" FAIL: INA219 not responding (addr 0x48)"),
|
||||||
|
}
|
||||||
|
|
||||||
|
crate::println!("-- SPI flash (W25Q128, JEDEC ID) --");
|
||||||
|
flash_cs.set_low();
|
||||||
|
let mut buf: [u8; 4] = [0x9F, 0, 0, 0];
|
||||||
|
let result = spi.blocking_transfer_in_place(&mut buf);
|
||||||
|
flash_cs.set_high();
|
||||||
|
match result {
|
||||||
|
Ok(()) => {
|
||||||
|
let [_, mfg, mem_type, capacity] = buf;
|
||||||
|
if mfg == 0xEF {
|
||||||
|
crate::println!(" PASS: JEDEC ID {mfg:02x} {mem_type:02x} {capacity:02x}");
|
||||||
|
} else {
|
||||||
|
crate::println!(" FAIL: unexpected JEDEC ID {mfg:02x} {mem_type:02x} {capacity:02x}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => crate::println!(" FAIL: SPI transfer error"),
|
||||||
|
}
|
||||||
|
|
||||||
|
crate::println!("=== self-check done ===");
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ version = "0.1.0"
|
|||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
chrono = { version = "0.4.44", default-features = false }
|
||||||
embedded-hal = "1.0.0"
|
embedded-hal = "1.0.0"
|
||||||
thiserror = { version = "2.0.17", default-features = false }
|
thiserror = { version = "2.0.17", default-features = false }
|
||||||
|
|
||||||
|
|||||||
+40
-19
@@ -3,7 +3,13 @@
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
extern crate std;
|
extern crate std;
|
||||||
|
|
||||||
|
extern crate chrono;
|
||||||
|
|
||||||
mod types;
|
mod types;
|
||||||
|
mod timestamp;
|
||||||
|
|
||||||
|
pub use types::{Stored_U26, Transit_U26, Transit_I26, U26Error};
|
||||||
|
pub use timestamp::{Store_DateTimeUtc, Transit_DateTimeUtc, DateTimeUtcError};
|
||||||
|
|
||||||
use embedded_hal::i2c::{I2c, Operation, SevenBitAddress};
|
use embedded_hal::i2c::{I2c, Operation, SevenBitAddress};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
@@ -27,6 +33,8 @@ pub trait BmsWriteable {
|
|||||||
pub enum BmsProtocolError {
|
pub enum BmsProtocolError {
|
||||||
#[error("i2c communication failed")]
|
#[error("i2c communication failed")]
|
||||||
I2cCommunicationError,
|
I2cCommunicationError,
|
||||||
|
#[error("checksum error in received data")]
|
||||||
|
ChecksumError,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<E> From<E> for BmsProtocolError
|
impl<E> From<E> for BmsProtocolError
|
||||||
@@ -38,6 +46,15 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<crate::types::U26Error> for BmsProtocolError {
|
||||||
|
fn from(error: crate::types::U26Error) -> Self {
|
||||||
|
match error {
|
||||||
|
crate::types::U26Error::ChecksumError => Self::ChecksumError,
|
||||||
|
_ => Self::I2cCommunicationError,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct BmsSoftwareReset;
|
pub struct BmsSoftwareReset;
|
||||||
|
|
||||||
@@ -73,7 +90,8 @@ impl BmsReadable for ProtocolVersion {
|
|||||||
Operation::Read(&mut version),
|
Operation::Read(&mut version),
|
||||||
],
|
],
|
||||||
)?;
|
)?;
|
||||||
Ok(ProtocolVersion(u32::from_be_bytes(version)))
|
let transit = Transit_U26::from_be_bytes(version)?;
|
||||||
|
Ok(ProtocolVersion(transit.into()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +114,8 @@ impl BmsReadable for FirmwareVersion {
|
|||||||
Operation::Read(&mut version),
|
Operation::Read(&mut version),
|
||||||
],
|
],
|
||||||
)?;
|
)?;
|
||||||
Ok(FirmwareVersion(u32::from_be_bytes(version)))
|
let transit = Transit_U26::from_be_bytes(version)?;
|
||||||
|
Ok(FirmwareVersion(transit.into()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,9 +146,9 @@ impl BmsReadable for Config {
|
|||||||
],
|
],
|
||||||
)?;
|
)?;
|
||||||
Ok(Config {
|
Ok(Config {
|
||||||
capacity_mah: u32::from_be_bytes(capacity_mah),
|
capacity_mah: Transit_U26::from_be_bytes(capacity_mah)?.into(),
|
||||||
v_full_mv: u32::from_be_bytes(v_full_mv),
|
v_full_mv: Transit_U26::from_be_bytes(v_full_mv)?.into(),
|
||||||
v_empty_mv: u32::from_be_bytes(v_empty_mv),
|
v_empty_mv: Transit_U26::from_be_bytes(v_empty_mv)?.into(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -167,11 +186,12 @@ impl BmsReadable for BatteryState {
|
|||||||
],
|
],
|
||||||
)?;
|
)?;
|
||||||
Ok(BatteryState {
|
Ok(BatteryState {
|
||||||
lifetime_capacity_mah: u32::from_be_bytes(lifetime_capacity_mah),
|
lifetime_capacity_mah: Transit_U26::from_be_bytes(lifetime_capacity_mah)?.into(),
|
||||||
remaining_capacity_mah: u32::from_be_bytes(remaining_capacity_mah),
|
remaining_capacity_mah: Transit_U26::from_be_bytes(remaining_capacity_mah)?.into(),
|
||||||
current_mv: u32::from_be_bytes(current_mv),
|
current_mv: Transit_U26::from_be_bytes(current_mv)?.into(),
|
||||||
temperature_celcius: i32::from_be_bytes(temperature_celcius),
|
temperature_celcius: crate::types::Transit_I26::from_be_bytes(temperature_celcius)?
|
||||||
health_percent: u32::from_be_bytes(health_percent),
|
.into(),
|
||||||
|
health_percent: Transit_U26::from_be_bytes(health_percent)?.into(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -194,9 +214,9 @@ impl BmsReadable for ChargeInfoWindowSec {
|
|||||||
Operation::Read(&mut charge_info_window_sec),
|
Operation::Read(&mut charge_info_window_sec),
|
||||||
],
|
],
|
||||||
)?;
|
)?;
|
||||||
Ok(ChargeInfoWindowSec(u32::from_be_bytes(
|
Ok(ChargeInfoWindowSec(
|
||||||
charge_info_window_sec,
|
Transit_U26::from_be_bytes(charge_info_window_sec)?.into(),
|
||||||
)))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,11 +226,12 @@ impl BmsWriteable for ChargeInfoWindowSec {
|
|||||||
I: I2c<SevenBitAddress>,
|
I: I2c<SevenBitAddress>,
|
||||||
{
|
{
|
||||||
let cmd = BmsRegisterMap::ChargoInfoWindowTotalSecs;
|
let cmd = BmsRegisterMap::ChargoInfoWindowTotalSecs;
|
||||||
|
let transit = Transit_U26::try_from(self.0)?;
|
||||||
i2c_dev.transaction(
|
i2c_dev.transaction(
|
||||||
BMS_I2C_ADDRESS,
|
BMS_I2C_ADDRESS,
|
||||||
&mut [
|
&mut [
|
||||||
Operation::Write(&(cmd as u32).to_be_bytes()),
|
Operation::Write(&(cmd as u32).to_be_bytes()),
|
||||||
Operation::Write(&self.0.to_be_bytes()),
|
Operation::Write(&transit.to_be_bytes()),
|
||||||
],
|
],
|
||||||
)?;
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -250,11 +271,11 @@ impl BmsReadable for ChargeInfo {
|
|||||||
],
|
],
|
||||||
)?;
|
)?;
|
||||||
Ok(ChargeInfo {
|
Ok(ChargeInfo {
|
||||||
total_charge_ma: u32::from_be_bytes(total_charge_ma),
|
total_charge_ma: Transit_U26::from_be_bytes(total_charge_ma)?.into(),
|
||||||
total_discharge_ma: u32::from_be_bytes(total_discharge_ma),
|
total_discharge_ma: Transit_U26::from_be_bytes(total_discharge_ma)?.into(),
|
||||||
max_charge_mw: u32::from_be_bytes(max_charge_mw),
|
max_charge_mw: Transit_U26::from_be_bytes(max_charge_mw)?.into(),
|
||||||
max_discharge_mw: u32::from_be_bytes(max_discharge_mw),
|
max_discharge_mw: Transit_U26::from_be_bytes(max_discharge_mw)?.into(),
|
||||||
avg_voltage_mv: u32::from_be_bytes(avg_voltage_mv),
|
avg_voltage_mv: Transit_U26::from_be_bytes(avg_voltage_mv)?.into(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,377 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::types::{Stored_U26, Transit_U26, U26Error};
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Error)]
|
||||||
|
/// Error type for DateTimeUtc wrapper types
|
||||||
|
pub enum DateTimeUtcError {
|
||||||
|
#[error("timestamp overflow")]
|
||||||
|
Overflow,
|
||||||
|
#[error("timestamp invalid")]
|
||||||
|
InvalidTimestamp,
|
||||||
|
#[error("timestamp conversion error")]
|
||||||
|
ConversionError,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<U26Error> for DateTimeUtcError {
|
||||||
|
fn from(error: U26Error) -> Self {
|
||||||
|
match error {
|
||||||
|
U26Error::Overflow => DateTimeUtcError::Overflow,
|
||||||
|
U26Error::InvalidCodeWord => DateTimeUtcError::InvalidTimestamp,
|
||||||
|
U26Error::IncorrectibleError => DateTimeUtcError::InvalidTimestamp,
|
||||||
|
U26Error::ChecksumError => DateTimeUtcError::InvalidTimestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
/// Wrapper type for storing DateTime<Utc> with error correction
|
||||||
|
/// Uses two Stored_U26 values internally to store 52 bits of milliseconds since Unix epoch
|
||||||
|
pub struct Store_DateTimeUtc(Stored_U26, Stored_U26);
|
||||||
|
|
||||||
|
impl Store_DateTimeUtc {
|
||||||
|
/// Maximum timestamp value that can be stored (in milliseconds)
|
||||||
|
const MAX_TIMESTAMP_MS: u64 = (1u64 << 52) - 1;
|
||||||
|
|
||||||
|
/// Create a new Store_DateTimeUtc from a DateTime<Utc>
|
||||||
|
pub fn new(dt: DateTime<Utc>) -> Result<Self, DateTimeUtcError> {
|
||||||
|
let timestamp_ms = dt.timestamp_millis();
|
||||||
|
if timestamp_ms < 0 || timestamp_ms > Self::MAX_TIMESTAMP_MS as i64 {
|
||||||
|
return Err(DateTimeUtcError::Overflow);
|
||||||
|
}
|
||||||
|
let timestamp_u64 = timestamp_ms as u64;
|
||||||
|
let low_26 = (timestamp_u64 & 0x03FFFFFF) as u32;
|
||||||
|
let high_26 = ((timestamp_u64 >> 26) & 0x03FFFFFF) as u32;
|
||||||
|
let stored_low = Stored_U26::try_from(low_26)?;
|
||||||
|
let stored_high = Stored_U26::try_from(high_26)?;
|
||||||
|
Ok(Self(stored_low, stored_high))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the DateTime<Utc> value
|
||||||
|
pub fn into_datetime(self) -> Result<DateTime<Utc>, DateTimeUtcError> {
|
||||||
|
let low_26: u32 = self.0.into();
|
||||||
|
let high_26: u32 = self.1.into();
|
||||||
|
let timestamp_u64 = ((high_26 as u64) << 26) | (low_26 as u64);
|
||||||
|
DateTime::from_timestamp_millis(timestamp_u64 as i64)
|
||||||
|
.ok_or(DateTimeUtcError::ConversionError)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the DateTime<Utc> value (borrowed)
|
||||||
|
pub fn to_datetime(&self) -> Result<DateTime<Utc>, DateTimeUtcError> {
|
||||||
|
let low_26: u32 = self.0.clone().into();
|
||||||
|
let high_26: u32 = self.1.clone().into();
|
||||||
|
let timestamp_u64 = ((high_26 as u64) << 26) | (low_26 as u64);
|
||||||
|
DateTime::from_timestamp_millis(timestamp_u64 as i64)
|
||||||
|
.ok_or(DateTimeUtcError::ConversionError)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serialize to big-endian bytes with CRC values
|
||||||
|
/// Format: [high_bytes[0], high_bytes[1], high_bytes[2], high_bytes[3], high_crc, low_bytes[0], low_bytes[1], low_bytes[2], low_bytes[3], low_crc]
|
||||||
|
pub fn to_be_bytes(&self) -> [u8; 10] {
|
||||||
|
let mut bytes = [0u8; 10];
|
||||||
|
let high_bytes = self.1.to_be_bytes();
|
||||||
|
let high_crc = self.1.calc_crc6();
|
||||||
|
let low_bytes = self.0.to_be_bytes();
|
||||||
|
let low_crc = self.0.calc_crc6();
|
||||||
|
|
||||||
|
bytes[0] = high_bytes[0];
|
||||||
|
bytes[1] = high_bytes[1];
|
||||||
|
bytes[2] = high_bytes[2];
|
||||||
|
bytes[3] = high_bytes[3];
|
||||||
|
bytes[4] = high_crc;
|
||||||
|
bytes[5] = low_bytes[0];
|
||||||
|
bytes[6] = low_bytes[1];
|
||||||
|
bytes[7] = low_bytes[2];
|
||||||
|
bytes[8] = low_bytes[3];
|
||||||
|
bytes[9] = low_crc;
|
||||||
|
|
||||||
|
bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deserialize from big-endian bytes with CRC check
|
||||||
|
/// Format: [high_bytes[0], high_bytes[1], high_bytes[2], high_bytes[3], high_crc, low_bytes[0], low_bytes[1], low_bytes[2], low_bytes[3], low_crc]
|
||||||
|
pub fn from_be_bytes(bytes: [u8; 10]) -> Result<Self, DateTimeUtcError> {
|
||||||
|
let high_bytes = [bytes[0], bytes[1], bytes[2], bytes[3]];
|
||||||
|
let high_crc = bytes[4];
|
||||||
|
let low_bytes = [bytes[5], bytes[6], bytes[7], bytes[8]];
|
||||||
|
let low_crc = bytes[9];
|
||||||
|
|
||||||
|
let stored_high = Stored_U26::from_be_bytes(high_bytes, high_crc)?;
|
||||||
|
let stored_low = Stored_U26::from_be_bytes(low_bytes, low_crc)?;
|
||||||
|
Ok(Self(stored_low, stored_high))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Store_DateTimeUtc> for DateTime<Utc> {
|
||||||
|
fn from(value: Store_DateTimeUtc) -> Self {
|
||||||
|
value.into_datetime().unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<DateTime<Utc>> for Store_DateTimeUtc {
|
||||||
|
type Error = DateTimeUtcError;
|
||||||
|
|
||||||
|
fn try_from(value: DateTime<Utc>) -> Result<Self, Self::Error> {
|
||||||
|
Self::new(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Store_DateTimeUtc> for u64 {
|
||||||
|
fn from(value: Store_DateTimeUtc) -> Self {
|
||||||
|
let low_26: u32 = value.0.into();
|
||||||
|
let high_26: u32 = value.1.into();
|
||||||
|
((high_26 as u64) << 26) | (low_26 as u64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
/// Wrapper type for transmitting DateTime<Utc> with CRC error detection
|
||||||
|
/// Uses two Transit_U26 values internally to store 52 bits of milliseconds since Unix epoch
|
||||||
|
pub struct Transit_DateTimeUtc(Transit_U26, Transit_U26);
|
||||||
|
|
||||||
|
impl Transit_DateTimeUtc {
|
||||||
|
/// Maximum timestamp value that can be transmitted (in milliseconds)
|
||||||
|
const MAX_TIMESTAMP_MS: u64 = (1u64 << 52) - 1;
|
||||||
|
|
||||||
|
/// Create a new Transit_DateTimeUtc from a DateTime<Utc>
|
||||||
|
pub fn new(dt: DateTime<Utc>) -> Result<Self, DateTimeUtcError> {
|
||||||
|
let timestamp_ms = dt.timestamp_millis();
|
||||||
|
if timestamp_ms < 0 || timestamp_ms > Self::MAX_TIMESTAMP_MS as i64 {
|
||||||
|
return Err(DateTimeUtcError::Overflow);
|
||||||
|
}
|
||||||
|
let timestamp_u64 = timestamp_ms as u64;
|
||||||
|
let low_26 = (timestamp_u64 & 0x03FFFFFF) as u32;
|
||||||
|
let high_26 = ((timestamp_u64 >> 26) & 0x03FFFFFF) as u32;
|
||||||
|
let transit_low = Transit_U26::try_from(low_26)?;
|
||||||
|
let transit_high = Transit_U26::try_from(high_26)?;
|
||||||
|
Ok(Self(transit_low, transit_high))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the DateTime<Utc> value
|
||||||
|
pub fn into_datetime(self) -> Result<DateTime<Utc>, DateTimeUtcError> {
|
||||||
|
let low_26: u32 = self.0.into();
|
||||||
|
let high_26: u32 = self.1.into();
|
||||||
|
let timestamp_u64 = ((high_26 as u64) << 26) | (low_26 as u64);
|
||||||
|
DateTime::from_timestamp_millis(timestamp_u64 as i64)
|
||||||
|
.ok_or(DateTimeUtcError::ConversionError)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the DateTime<Utc> value (borrowed)
|
||||||
|
pub fn to_datetime(&self) -> Result<DateTime<Utc>, DateTimeUtcError> {
|
||||||
|
let low_26: u32 = self.0.clone().into();
|
||||||
|
let high_26: u32 = self.1.clone().into();
|
||||||
|
let timestamp_u64 = ((high_26 as u64) << 26) | (low_26 as u64);
|
||||||
|
DateTime::from_timestamp_millis(timestamp_u64 as i64)
|
||||||
|
.ok_or(DateTimeUtcError::ConversionError)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serialize to big-endian bytes
|
||||||
|
/// Format: [high_bytes[0], high_bytes[1], high_bytes[2], high_bytes[3], low_bytes[0], low_bytes[1], low_bytes[2], low_bytes[3]]
|
||||||
|
pub fn to_be_bytes(&self) -> [u8; 8] {
|
||||||
|
let mut bytes = [0u8; 8];
|
||||||
|
let high_bytes = self.1.to_be_bytes();
|
||||||
|
let low_bytes = self.0.to_be_bytes();
|
||||||
|
|
||||||
|
bytes[0] = high_bytes[0];
|
||||||
|
bytes[1] = high_bytes[1];
|
||||||
|
bytes[2] = high_bytes[2];
|
||||||
|
bytes[3] = high_bytes[3];
|
||||||
|
bytes[4] = low_bytes[0];
|
||||||
|
bytes[5] = low_bytes[1];
|
||||||
|
bytes[6] = low_bytes[2];
|
||||||
|
bytes[7] = low_bytes[3];
|
||||||
|
|
||||||
|
bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deserialize from big-endian bytes with CRC check
|
||||||
|
/// Format: [high_bytes[0], high_bytes[1], high_bytes[2], high_bytes[3], low_bytes[0], low_bytes[1], low_bytes[2], low_bytes[3]]
|
||||||
|
pub fn from_be_bytes(bytes: [u8; 8]) -> Result<Self, DateTimeUtcError> {
|
||||||
|
let high_bytes = [bytes[0], bytes[1], bytes[2], bytes[3]];
|
||||||
|
let low_bytes = [bytes[4], bytes[5], bytes[6], bytes[7]];
|
||||||
|
|
||||||
|
let transit_high = Transit_U26::from_be_bytes(high_bytes)?;
|
||||||
|
let transit_low = Transit_U26::from_be_bytes(low_bytes)?;
|
||||||
|
Ok(Self(transit_low, transit_high))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Transit_DateTimeUtc> for DateTime<Utc> {
|
||||||
|
fn from(value: Transit_DateTimeUtc) -> Self {
|
||||||
|
value.into_datetime().unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<DateTime<Utc>> for Transit_DateTimeUtc {
|
||||||
|
type Error = DateTimeUtcError;
|
||||||
|
|
||||||
|
fn try_from(value: DateTime<Utc>) -> Result<Self, Self::Error> {
|
||||||
|
Self::new(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Transit_DateTimeUtc> for u64 {
|
||||||
|
fn from(value: Transit_DateTimeUtc) -> Self {
|
||||||
|
let low_26: u32 = value.0.into();
|
||||||
|
let high_26: u32 = value.1.into();
|
||||||
|
((high_26 as u64) << 26) | (low_26 as u64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod timestamp_test {
|
||||||
|
use super::*;
|
||||||
|
use chrono::{DateTime, TimeZone, Utc};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn store_datetimeutc_basic_operations() {
|
||||||
|
// Test with Unix epoch
|
||||||
|
let epoch = Utc.timestamp_millis_opt(0).unwrap();
|
||||||
|
let stored = Store_DateTimeUtc::new(epoch).unwrap();
|
||||||
|
let retrieved: DateTime<Utc> = stored.into();
|
||||||
|
assert_eq!(retrieved, epoch);
|
||||||
|
|
||||||
|
// Test with a specific date (within 52-bit range)
|
||||||
|
let dt = Utc.with_ymd_and_hms(2020, 1, 15, 12, 30, 45).unwrap();
|
||||||
|
let stored = Store_DateTimeUtc::new(dt).unwrap();
|
||||||
|
let retrieved: DateTime<Utc> = stored.into();
|
||||||
|
assert_eq!(retrieved, dt);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn store_datetimeutc_serialization() {
|
||||||
|
let dt = Utc.with_ymd_and_hms(2020, 5, 20, 8, 15, 30).unwrap();
|
||||||
|
let stored = Store_DateTimeUtc::new(dt).unwrap();
|
||||||
|
let bytes = stored.to_be_bytes();
|
||||||
|
|
||||||
|
// Deserialize and verify
|
||||||
|
let deserialized = Store_DateTimeUtc::from_be_bytes(bytes).unwrap();
|
||||||
|
let retrieved: DateTime<Utc> = deserialized.into();
|
||||||
|
assert_eq!(retrieved, dt);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn store_datetimeutc_error_correction() {
|
||||||
|
let dt = Utc.with_ymd_and_hms(2020, 3, 10, 14, 25, 10).unwrap();
|
||||||
|
let stored = Store_DateTimeUtc::new(dt).unwrap();
|
||||||
|
let mut bytes = stored.to_be_bytes();
|
||||||
|
|
||||||
|
// Introduce a single bit error in low bytes that can be corrected
|
||||||
|
bytes[7] ^= 0x01;
|
||||||
|
let deserialized = Store_DateTimeUtc::from_be_bytes(bytes).unwrap();
|
||||||
|
let retrieved: DateTime<Utc> = deserialized.into();
|
||||||
|
assert_eq!(retrieved, dt);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn store_datetimeutc_overflow() {
|
||||||
|
// Test with timestamp beyond max value
|
||||||
|
let max_timestamp = Store_DateTimeUtc::MAX_TIMESTAMP_MS;
|
||||||
|
let dt = Utc.timestamp_millis_opt(max_timestamp as i64 + 1).unwrap();
|
||||||
|
let result = Store_DateTimeUtc::new(dt);
|
||||||
|
assert!(matches!(result, Err(DateTimeUtcError::Overflow)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn store_datetimeutc_borrowed_method() {
|
||||||
|
let dt = Utc.with_ymd_and_hms(2020, 7, 22, 18, 45, 20).unwrap();
|
||||||
|
let stored = Store_DateTimeUtc::new(dt).unwrap();
|
||||||
|
|
||||||
|
// Test the borrowed to_datetime method
|
||||||
|
let retrieved = stored.to_datetime().unwrap();
|
||||||
|
assert_eq!(retrieved, dt);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn store_datetimeutc_try_from() {
|
||||||
|
let dt = Utc.with_ymd_and_hms(2020, 9, 15, 10, 10, 10).unwrap();
|
||||||
|
let stored: Store_DateTimeUtc = dt.try_into().unwrap();
|
||||||
|
let retrieved: DateTime<Utc> = stored.into();
|
||||||
|
assert_eq!(retrieved, dt);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn store_datetimeutc_crc_error_detection() {
|
||||||
|
let dt = Utc.with_ymd_and_hms(2020, 12, 25, 0, 0, 0).unwrap();
|
||||||
|
let stored = Store_DateTimeUtc::new(dt).unwrap();
|
||||||
|
let mut bytes = stored.to_be_bytes();
|
||||||
|
|
||||||
|
// Introduce multiple bit errors in low bytes that can't be corrected
|
||||||
|
bytes[5] ^= 0x01;
|
||||||
|
bytes[6] ^= 0x01;
|
||||||
|
let result = Store_DateTimeUtc::from_be_bytes(bytes);
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn transit_datetimeutc_basic_operations() {
|
||||||
|
// Test with Unix epoch
|
||||||
|
let epoch = Utc.timestamp_millis_opt(0).unwrap();
|
||||||
|
let transit = Transit_DateTimeUtc::new(epoch).unwrap();
|
||||||
|
let retrieved: DateTime<Utc> = transit.into();
|
||||||
|
assert_eq!(retrieved, epoch);
|
||||||
|
|
||||||
|
// Test with a specific date (within 52-bit range)
|
||||||
|
let dt = Utc.with_ymd_and_hms(2020, 1, 15, 12, 30, 45).unwrap();
|
||||||
|
let transit = Transit_DateTimeUtc::new(dt).unwrap();
|
||||||
|
let retrieved: DateTime<Utc> = transit.into();
|
||||||
|
assert_eq!(retrieved, dt);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn transit_datetimeutc_serialization() {
|
||||||
|
let dt = Utc.with_ymd_and_hms(2020, 5, 20, 8, 15, 30).unwrap();
|
||||||
|
let transit = Transit_DateTimeUtc::new(dt).unwrap();
|
||||||
|
let bytes = transit.to_be_bytes();
|
||||||
|
|
||||||
|
// Deserialize and verify
|
||||||
|
let deserialized = Transit_DateTimeUtc::from_be_bytes(bytes).unwrap();
|
||||||
|
let retrieved: DateTime<Utc> = deserialized.into();
|
||||||
|
assert_eq!(retrieved, dt);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn transit_datetimeutc_crc_error_detection() {
|
||||||
|
let dt = Utc.with_ymd_and_hms(2020, 12, 25, 0, 0, 0).unwrap();
|
||||||
|
let transit = Transit_DateTimeUtc::new(dt).unwrap();
|
||||||
|
let mut bytes = transit.to_be_bytes();
|
||||||
|
|
||||||
|
// Introduce bit errors in low bytes that will cause CRC mismatch
|
||||||
|
bytes[4] ^= 0x01;
|
||||||
|
bytes[5] ^= 0x01;
|
||||||
|
let result = Transit_DateTimeUtc::from_be_bytes(bytes);
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn transit_datetimeutc_overflow() {
|
||||||
|
// Test with timestamp beyond max value
|
||||||
|
let max_timestamp = Transit_DateTimeUtc::MAX_TIMESTAMP_MS;
|
||||||
|
let dt = Utc.timestamp_millis_opt(max_timestamp as i64 + 1).unwrap();
|
||||||
|
let result = Transit_DateTimeUtc::new(dt);
|
||||||
|
assert!(matches!(result, Err(DateTimeUtcError::Overflow)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn transit_datetimeutc_borrowed_method() {
|
||||||
|
let dt = Utc.with_ymd_and_hms(2020, 7, 22, 18, 45, 20).unwrap();
|
||||||
|
let transit = Transit_DateTimeUtc::new(dt).unwrap();
|
||||||
|
|
||||||
|
// Test the borrowed to_datetime method
|
||||||
|
let retrieved = transit.to_datetime().unwrap();
|
||||||
|
assert_eq!(retrieved, dt);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn transit_datetimeutc_try_from() {
|
||||||
|
let dt = Utc.with_ymd_and_hms(2020, 9, 15, 10, 10, 10).unwrap();
|
||||||
|
let transit: Transit_DateTimeUtc = dt.try_into().unwrap();
|
||||||
|
let retrieved: DateTime<Utc> = transit.into();
|
||||||
|
assert_eq!(retrieved, dt);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -44,19 +44,26 @@
|
|||||||
//! errors random errors in the transmission have a probabilty of 2**-6 = 0.015625 of matching the original messages CRC code.
|
//! errors random errors in the transmission have a probabilty of 2**-6 = 0.015625 of matching the original messages CRC code.
|
||||||
//! Meaning the chance of errors going undetected is <2%.
|
//! Meaning the chance of errors going undetected is <2%.
|
||||||
|
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
const U26_VALUE_MASK: u32 = 0xFFFFFFc0;
|
const U26_VALUE_MASK: u32 = 0xFFFFFFc0;
|
||||||
const U26_MAX_VALUE: u32 = 0x03FFFFFF;
|
const U26_MAX_VALUE: u32 = 0x03FFFFFF;
|
||||||
const U26_HEMMING_MASK: u32 = 0x0000003e;
|
const U26_HEMMING_MASK: u32 = 0x0000003e;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq, Error)]
|
||||||
pub enum U26Error {
|
pub enum U26Error {
|
||||||
|
#[error("data read from bytes has unexpected format")]
|
||||||
InvalidCodeWord,
|
InvalidCodeWord,
|
||||||
|
#[error("detected an uncorrectible error, data is invalid")]
|
||||||
IncorrectibleError,
|
IncorrectibleError,
|
||||||
|
#[error("data does not fit in u26")]
|
||||||
Overflow,
|
Overflow,
|
||||||
|
#[error("checksum error")]
|
||||||
ChecksumError,
|
ChecksumError,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
/// this type is used for storing data on memory devices the concern here is mostly protecting against memory corruption
|
/// this type is used for storing data on memory devices the concern here is mostly protecting against memory corruption
|
||||||
/// du to faulty memory cells. Here one bit errors are the most probabl cause of errors so this types uses a build in
|
/// du to faulty memory cells. Here one bit errors are the most probabl cause of errors so this types uses a build in
|
||||||
/// hemming code for one mit error correction.
|
/// hemming code for one mit error correction.
|
||||||
@@ -116,7 +123,7 @@ impl Stored_U26 {
|
|||||||
|
|
||||||
impl From<Stored_U26> for u32 {
|
impl From<Stored_U26> for u32 {
|
||||||
fn from(value: Stored_U26) -> Self {
|
fn from(value: Stored_U26) -> Self {
|
||||||
value.0 & U26_VALUE_MASK >> 6
|
(value.0 & U26_VALUE_MASK) >> 6
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,7 +139,8 @@ impl TryFrom<u32> for Stored_U26 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
/// this type is used for transmitting data, the concern here is protection against transmission errors which most likely occur
|
/// this type is used for transmitting data, the concern here is protection against transmission errors which most likely occur
|
||||||
/// as burst errors effecting multiple bits, hemming codes would be a waist of space here so instead a 6 bit crc code is used
|
/// as burst errors effecting multiple bits, hemming codes would be a waist of space here so instead a 6 bit crc code is used
|
||||||
///
|
///
|
||||||
@@ -165,7 +173,7 @@ impl Transit_U26 {
|
|||||||
|
|
||||||
impl From<Transit_U26> for u32 {
|
impl From<Transit_U26> for u32 {
|
||||||
fn from(value: Transit_U26) -> Self {
|
fn from(value: Transit_U26) -> Self {
|
||||||
value.0 & U26_VALUE_MASK >> 6
|
(value.0 & U26_VALUE_MASK) >> 6
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,6 +189,59 @@ impl TryFrom<u32> for Transit_U26 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
/// this type is used for transmitting signed 26-bit integer data
|
||||||
|
/// It wraps the Transit_U26 type to provide i32 support
|
||||||
|
/// The i32 value is converted to u32 by casting, preserving the bit pattern
|
||||||
|
/// This ensures proper sign extension when converting between signed and unsigned
|
||||||
|
/// Valid range: -33554432 to 33554431 (26-bit signed)
|
||||||
|
pub struct Transit_I26(Transit_U26);
|
||||||
|
|
||||||
|
impl Transit_I26 {
|
||||||
|
pub fn new(value: i32) -> Result<Self, U26Error> {
|
||||||
|
// Check if value is within 26-bit signed range
|
||||||
|
if value < -0x02000000 || value > 0x01FFFFFF {
|
||||||
|
return Err(U26Error::Overflow);
|
||||||
|
}
|
||||||
|
// Extract only the lower 26 bits
|
||||||
|
let u26_val = (value as u32) & 0x03FFFFFF;
|
||||||
|
let transit_u26 = Transit_U26::try_from(u26_val)?;
|
||||||
|
Ok(Self(transit_u26))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_be_bytes(bytes: [u8; 4]) -> Result<Self, U26Error> {
|
||||||
|
let transit_u26 = Transit_U26::from_be_bytes(bytes)?;
|
||||||
|
Ok(Self(transit_u26))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_be_bytes(&self) -> [u8; 4] {
|
||||||
|
self.0.to_be_bytes()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Transit_I26> for i32 {
|
||||||
|
fn from(value: Transit_I26) -> Self {
|
||||||
|
// Convert u32 back to i32 by casting, preserving the exact bit pattern
|
||||||
|
let u32_val: u32 = value.0.into();
|
||||||
|
// Interpret as signed 26-bit integer
|
||||||
|
if u32_val & 0x02000000 != 0 {
|
||||||
|
// Sign-extend to 32 bits
|
||||||
|
(u32_val as i32) | 0xFC000000u32 as i32
|
||||||
|
} else {
|
||||||
|
u32_val as i32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<i32> for Transit_I26 {
|
||||||
|
type Error = U26Error;
|
||||||
|
|
||||||
|
fn try_from(value: i32) -> Result<Self, Self::Error> {
|
||||||
|
Self::new(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// only calc hemming code for the first 24 data bits including the 5 hemming bits
|
// only calc hemming code for the first 24 data bits including the 5 hemming bits
|
||||||
// pos | | p1 | p2 | p3 | p4 | p5 |
|
// pos | | p1 | p2 | p3 | p4 | p5 |
|
||||||
// 00001 | p1 | o | | | | |
|
// 00001 | p1 | o | | | | |
|
||||||
@@ -360,7 +421,7 @@ pub fn check_hemming(data: u32, hemming: u8) -> Result<(), HemmingCorrectionValu
|
|||||||
pub fn calc_crc6(data: u32) -> u8 {
|
pub fn calc_crc6(data: u32) -> u8 {
|
||||||
let mut crc: u8 = 0x00;
|
let mut crc: u8 = 0x00;
|
||||||
for pos in 0..32 {
|
for pos in 0..32 {
|
||||||
if ((data >> (31-pos)) & 0x1) ^ ((crc >> 5) & 0x1) as u32 != 0 {
|
if ((data >> (31 - pos)) & 0x1) ^ ((crc >> 5) & 0x1) as u32 != 0 {
|
||||||
crc = ((crc << 1) ^ 0x03) & 0x3F;
|
crc = ((crc << 1) ^ 0x03) & 0x3F;
|
||||||
} else {
|
} else {
|
||||||
crc = (crc << 1) & 0x3F;
|
crc = (crc << 1) & 0x3F;
|
||||||
@@ -376,7 +437,7 @@ pub fn calc_crc6(data: u32) -> u8 {
|
|||||||
pub fn check_crc6(data: u32, crc: u8) -> bool {
|
pub fn check_crc6(data: u32, crc: u8) -> bool {
|
||||||
let mut check_crc: u8 = 0x00;
|
let mut check_crc: u8 = 0x00;
|
||||||
for pos in 0..32 {
|
for pos in 0..32 {
|
||||||
if ((data >> (31-pos)) & 0x1) ^ ((check_crc >> 5) & 0x1) as u32 != 0 {
|
if ((data >> (31 - pos)) & 0x1) ^ ((check_crc >> 5) & 0x1) as u32 != 0 {
|
||||||
check_crc = ((check_crc << 1) ^ 0x03) & 0x3F;
|
check_crc = ((check_crc << 1) ^ 0x03) & 0x3F;
|
||||||
} else {
|
} else {
|
||||||
check_crc = (check_crc << 1) & 0x3F;
|
check_crc = (check_crc << 1) & 0x3F;
|
||||||
@@ -388,7 +449,8 @@ pub fn check_crc6(data: u32, crc: u8) -> bool {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod base_type_tests {
|
mod base_type_tests {
|
||||||
use super::{
|
use super::{
|
||||||
Stored_U26, Transit_U26, U26_MAX_VALUE, calc_crc6, calc_hemming, check_crc6, check_hemming,
|
calc_crc6, calc_hemming, check_crc6, check_hemming, Stored_U26, Transit_I26, Transit_U26,
|
||||||
|
U26_MAX_VALUE,
|
||||||
};
|
};
|
||||||
use crate::types::U26Error;
|
use crate::types::U26Error;
|
||||||
|
|
||||||
@@ -469,7 +531,10 @@ mod base_type_tests {
|
|||||||
assert_eq!(Stored_U26::from_be_bytes(bytes, crc).unwrap(), data);
|
assert_eq!(Stored_U26::from_be_bytes(bytes, crc).unwrap(), data);
|
||||||
// introduce second bit error
|
// introduce second bit error
|
||||||
bytes[0] = bytes[0] ^ 0x01;
|
bytes[0] = bytes[0] ^ 0x01;
|
||||||
assert_eq!(Stored_U26::from_be_bytes(bytes, crc).unwrap_err(), U26Error::IncorrectibleError);
|
assert_eq!(
|
||||||
|
Stored_U26::from_be_bytes(bytes, crc).unwrap_err(),
|
||||||
|
U26Error::IncorrectibleError
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -515,4 +580,52 @@ mod base_type_tests {
|
|||||||
assert!(!check_crc6(corrupted_data, original_crc));
|
assert!(!check_crc6(corrupted_data, original_crc));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn transit_i26_positive_values() {
|
||||||
|
let data = Transit_I26::new(12345).unwrap();
|
||||||
|
let bytes = data.to_be_bytes();
|
||||||
|
let decoded = Transit_I26::from_be_bytes(bytes).unwrap();
|
||||||
|
assert_eq!(i32::from(decoded), 12345);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn transit_i26_negative_values() {
|
||||||
|
let data = Transit_I26::new(-12345).unwrap();
|
||||||
|
let bytes = data.to_be_bytes();
|
||||||
|
let decoded = Transit_I26::from_be_bytes(bytes).unwrap();
|
||||||
|
assert_eq!(i32::from(decoded), -12345);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn transit_i26_zero() {
|
||||||
|
let data = Transit_I26::new(0).unwrap();
|
||||||
|
let bytes = data.to_be_bytes();
|
||||||
|
let decoded = Transit_I26::from_be_bytes(bytes).unwrap();
|
||||||
|
assert_eq!(i32::from(decoded), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn transit_i26_max_positive() {
|
||||||
|
let data = Transit_I26::new(0x01FFFFFF).unwrap();
|
||||||
|
let bytes = data.to_be_bytes();
|
||||||
|
let decoded = Transit_I26::from_be_bytes(bytes).unwrap();
|
||||||
|
assert_eq!(i32::from(decoded), 0x01FFFFFF as i32);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn transit_i26_max_negative() {
|
||||||
|
let data = Transit_I26::new(-0x01FFFFFF).unwrap();
|
||||||
|
let bytes = data.to_be_bytes();
|
||||||
|
let decoded = Transit_I26::from_be_bytes(bytes).unwrap();
|
||||||
|
assert_eq!(i32::from(decoded), -0x01FFFFFF as i32);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn transit_i26_checksum_error() {
|
||||||
|
let data = Transit_I26::new(12345).unwrap();
|
||||||
|
let mut bytes = data.to_be_bytes();
|
||||||
|
bytes[1] ^= 0x01; // Corrupt one byte
|
||||||
|
assert!(Transit_I26::from_be_bytes(bytes).is_err());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user