diff --git a/dc/hbj11/README.md b/dc/hbj11/README.md new file mode 100644 index 00000000..9649da16 --- /dev/null +++ b/dc/hbj11/README.md @@ -0,0 +1,16 @@ +hbj11 - the Hackerspace Blade JBOD adapter +=== + +Replacement cards for M610 blades at the hackerspace. + +![](doc/hbj11-a0-photo.jpg) + +Hardware +-------- + +To be published. + +EEPROM & flasher +---------------- + +See [flasher](flasher) for a WebUSB/STM32 based flasher for the cards. diff --git a/dc/hbj11/doc/bluepill.jpg b/dc/hbj11/doc/bluepill.jpg new file mode 100644 index 00000000..23667b35 Binary files /dev/null and b/dc/hbj11/doc/bluepill.jpg differ diff --git a/dc/hbj11/doc/hbj11-a0-photo.jpg b/dc/hbj11/doc/hbj11-a0-photo.jpg new file mode 100644 index 00000000..7ff08a9f Binary files /dev/null and b/dc/hbj11/doc/hbj11-a0-photo.jpg differ diff --git a/dc/hbj11/doc/webi2c.png b/dc/hbj11/doc/webi2c.png new file mode 100644 index 00000000..3a3b020e Binary files /dev/null and b/dc/hbj11/doc/webi2c.png differ diff --git a/dc/hbj11/flasher/README.md b/dc/hbj11/flasher/README.md new file mode 100644 index 00000000..b6a69e7e --- /dev/null +++ b/dc/hbj11/flasher/README.md @@ -0,0 +1,20 @@ +HBJ11 Flasher +============= + +This is a EEPROM flashing tool for the HBJ11 cards designed at the Warsaw Hackerspace. + +It's made up of two parts: + +The Device (Bluepill) +--------------------- + +An USB/I2C adapter based on an STM32 Bluepill devboard. See [bluepill](bluepill/) for more information. You will need one physically plugged into your machine and wired up to a PCIe socket to insert the HBJ11s into. + +![](../doc/bluepill.jpg) + +The Web Interface (WebI2C) +--------------------------- + +A WebUSB-based flashing tool that will run under any Chromium-based browser (eg. Chrome, Edge. See [web](web/) for more information and a link to a publicly available instance. + +![](../doc/webi2c.png) diff --git a/dc/hbj11/flasher/bluepill/.cargo/config b/dc/hbj11/flasher/bluepill/.cargo/config new file mode 100644 index 00000000..128b9c4d --- /dev/null +++ b/dc/hbj11/flasher/bluepill/.cargo/config @@ -0,0 +1,3 @@ +[build] +target = "thumbv7m-none-eabi" +rustflags = [ "-C", "link-arg=-Tlink.x", "-C", "inline-threshold=255"] diff --git a/dc/hbj11/flasher/bluepill/.gitignore b/dc/hbj11/flasher/bluepill/.gitignore new file mode 100644 index 00000000..eb5a316c --- /dev/null +++ b/dc/hbj11/flasher/bluepill/.gitignore @@ -0,0 +1 @@ +target diff --git a/dc/hbj11/flasher/bluepill/Cargo.lock b/dc/hbj11/flasher/bluepill/Cargo.lock new file mode 100644 index 00000000..ff7488cd --- /dev/null +++ b/dc/hbj11/flasher/bluepill/Cargo.lock @@ -0,0 +1,460 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "aligned" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c19796bd8d477f1a9d4ac2465b464a8b1359474f06a96bb3cda650b4fca309bf" +dependencies = [ + "as-slice", +] + +[[package]] +name = "as-slice" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "bare-metal" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5deb64efa5bd81e31fcd1938615a6d98c82eafcbcd787162b6f63b91d6bac5b3" +dependencies = [ + "rustc_version", +] + +[[package]] +name = "bitfield" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46afbd2983a5d5a7bd740ccb198caf5b82f45c40c09c0eed36052d91cb92e719" + +[[package]] +name = "byteorder" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae44d1a3d5a19df61dd0c8beb138458ac2a53a7ac09eba97d55592540004306b" + +[[package]] +name = "cast" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b9434b9a5aa1450faa3f9cb14ea0e8c53bb5d2b3c1bfd1ab4fc03e9f33fbfb0" +dependencies = [ + "rustc_version", +] + +[[package]] +name = "cortex-m" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9075300b07c6a56263b9b582c214d0ff037b00d45ec9fde1cc711490c56f1bb9" +dependencies = [ + "aligned", + "bare-metal", + "bitfield", + "cortex-m 0.7.1", + "volatile-register", +] + +[[package]] +name = "cortex-m" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0b756a8bffc56025de45218a48ff9b801180440c0ee49a722b32d49dcebc771" +dependencies = [ + "bare-metal", + "bitfield", + "embedded-hal", + "volatile-register", +] + +[[package]] +name = "cortex-m-rt" +version = "0.6.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "980c9d0233a909f355ed297ef122f257942de5e0a2cb1c39f60684b65bcb90fb" +dependencies = [ + "cortex-m-rt-macros", + "r0", +] + +[[package]] +name = "cortex-m-rt-macros" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4717562afbba06e760d34451919f5c3bf3ac15c7bb897e8b04862a7428378647" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "cortex-m-rtic" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b30efcb6b7920d9016182c485687f0012487032a14c415d2fce6e9862ef8260e" +dependencies = [ + "cortex-m 0.6.7", + "cortex-m-rt", + "cortex-m-rtic-macros", + "heapless", + "rtic-core", + "version_check", +] + +[[package]] +name = "cortex-m-rtic-macros" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a1a6a4c9550373038c0e21a78d44d529bd697c25bbf6b8004bddc6e63b119c7" +dependencies = [ + "proc-macro2", + "quote", + "rtic-syntax", + "syn", +] + +[[package]] +name = "cortex-m-semihosting" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bffa6c1454368a6aa4811ae60964c38e6996d397ff8095a8b9211b1c1f749bc" +dependencies = [ + "cortex-m 0.7.1", +] + +[[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", +] + +[[package]] +name = "generic-array" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c68f0274ae0e023facc3c97b2e00f076be70e254bc851d972503b328db79b2ec" +dependencies = [ + "typenum", +] + +[[package]] +name = "generic-array" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ed1e761351b56f54eb9dcd0cfaca9fd0daecf93918e1cfc01c8a3d26ee7adcd" +dependencies = [ + "typenum", +] + +[[package]] +name = "generic-array" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "hash32" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4041af86e63ac4298ce40e5cca669066e75b6f1aa3390fe2561ffa5e1d9f4cc" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" + +[[package]] +name = "hbj11-flasher-bluepill" +version = "0.1.0" +dependencies = [ + "cortex-m 0.6.7", + "cortex-m-rt", + "cortex-m-rtic", + "cortex-m-semihosting", + "embedded-hal", + "nb 0.1.3", + "num-derive", + "num-traits", + "panic-halt", + "panic-semihosting", + "stm32f1xx-hal", + "usb-device", + "usbd-webusb", +] + +[[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", + "stable_deref_trait", +] + +[[package]] +name = "indexmap" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb1fa934250de4de8aef298d81c729a7d33d8c239daa3a7575e6b92bfc7313b" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "nb" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "801d31da0513b6ec5214e9bf433a77966320625a37860f910be265be6e18d06f" +dependencies = [ + "nb 1.0.0", +] + +[[package]] +name = "nb" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "546c37ac5d9e56f55e73b677106873d9d9f5190605e41a856503623648488cae" + +[[package]] +name = "num-derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + +[[package]] +name = "panic-halt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de96540e0ebde571dc55c73d60ef407c653844e6f9a1e2fdbd40c07b9252d812" + +[[package]] +name = "panic-semihosting" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d55dedd501dfd02514646e0af4d7016ce36bc12ae177ef52056989966a1eec" +dependencies = [ + "cortex-m 0.7.1", + "cortex-m-semihosting", +] + +[[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.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r0" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2a38df5b15c8d5c7e8654189744d8e396bddc18ad48041a500ce52d6948941f" + +[[package]] +name = "rtic-core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bd58a6949de8ff797a346a28d9f13f7b8f54fa61bb5e3cb0985a4efb497a5ef" + +[[package]] +name = "rtic-syntax" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8152fcaa845720d61e6cc570548b89144c2c307f18a480bbd97e55e9f6eeff04" +dependencies = [ + "indexmap", + "proc-macro2", + "syn", +] + +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver", +] + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "stm32-usbd" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d13eca735cae37df697f599777b000cc0ee924df8452f2b4bfaa6798ab0338" +dependencies = [ + "cortex-m 0.6.7", + "usb-device", + "vcell", +] + +[[package]] +name = "stm32f1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "849b1e8d9bcfd792c9d9178cf86165d299a661c26e35d9322ae9382d3f3fe460" +dependencies = [ + "bare-metal", + "cortex-m 0.6.7", + "cortex-m-rt", + "vcell", +] + +[[package]] +name = "stm32f1xx-hal" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af9b9e5d7c2901ee39fc9527412327a1fe08f1d84e9d7f4b3497448e655e5098" +dependencies = [ + "as-slice", + "cast", + "cortex-m 0.6.7", + "cortex-m-rt", + "embedded-hal", + "nb 0.1.3", + "stm32-usbd", + "stm32f1", + "void", +] + +[[package]] +name = "syn" +version = "1.0.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c700597eca8a5a762beb35753ef6b94df201c81cca676604f547495a0d7f0081" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "typenum" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33" + +[[package]] +name = "unicode-xid" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" + +[[package]] +name = "usb-device" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "849eed9b4dc61a1f17ba1d7a5078ceb095b9410caa38a506eb281ed5eff12fbd" + +[[package]] +name = "usbd-webusb" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed33ecaa7a26365f13059e753bfa23f0a4a557565499f46d255c51e737464bd8" +dependencies = [ + "usb-device", +] + +[[package]] +name = "vcell" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77439c1b53d2303b20d9459b1ade71a83c716e3f9c34f3228c00e6f185d6c002" + +[[package]] +name = "version_check" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" + +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + +[[package]] +name = "volatile-register" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d67cb4616d99b940db1d6bd28844ff97108b498a6ca850e5b6191a532063286" +dependencies = [ + "vcell", +] diff --git a/dc/hbj11/flasher/bluepill/Cargo.toml b/dc/hbj11/flasher/bluepill/Cargo.toml new file mode 100644 index 00000000..efa78924 --- /dev/null +++ b/dc/hbj11/flasher/bluepill/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "hbj11-flasher-bluepill" +version = "0.1.0" +authors = ["Serge Bazanski "] +edition = "2018" + +[profile.dev] +opt-level = 3 + +[profile.release] +opt-level = 'z' +lto = true + +[dependencies] +cortex-m = "^0.6.3" +cortex-m-rtic = "^0.5.5" +cortex-m-rt = "^0.6.12" +cortex-m-semihosting = "^0.3.7" +embedded-hal = "^0.2.4" +panic-halt = "^0.2.0" +usb-device = "^0.2.7" +usbd-webusb = "^1.0.2" +panic-semihosting = "^0.5.0" +nb = "^0.1.3" +num-derive = "0.3" + +[dependencies.num-traits] +default-features = false +features = [] +version = "0.2" + +[dependencies.stm32f1xx-hal] +features = ["stm32f103", "rt", "medium", "stm32-usbd"] +version = "^0.6.1" diff --git a/dc/hbj11/flasher/bluepill/README.md b/dc/hbj11/flasher/bluepill/README.md new file mode 100644 index 00000000..67af4914 --- /dev/null +++ b/dc/hbj11/flasher/bluepill/README.md @@ -0,0 +1,79 @@ +STM32 Bluepill-based I2C Flasher +================================ + + +[TOC] + +![](../../doc/bluepill.jpg) + +This is a Rust project that runs on an STM32F103C8T6 on a common [bluepill](https://stm32-base.org/boards/STM32F103C8T6-Blue-Pill.html) board. + +It acts as a USB device, exposing an interface to perform arbitrary I2C operations. + +Hardware +-------- + +You will need a buepill with an STM32103C8T6 and a way to flash ELFs on it. An ST-Link or BlackMagicProbe (potentially running on another Bluepill) are good choices. + +For flashing the HBJ11 (or any other Dell M610 storage card) you will also need a PCIe x8 socket. The connections to make are as follows: + +| Bluepill/STM32 | Function | PCIe Slot | +| -------------- | -------- | --------- | +| G/GND | Ground | B7 | +| 3.3/VCC | 3.3V | B10 | +| B6 | SCL | B11 | +| B7 | SDA | B12 | + +Note: the PCIe slot pin numbering follows the same convention as Dell parts (they have A1-A49/B1-B49 markers) and as [the Wikipedia article on PCIe](https://en.wikipedia.org/wiki/PCI_Express#Pinout). + +Note: you will need to add pull up resistors for SCL and SDA. 4k7 is a good value to start with. Use a scope to make sure the open drain/pullup behaviour looks sensible. + +Note: we run the I2C bus and EEPROM at 3.3V, even though it runs at 5V while in a server. This is fine for HBJ11 flashing, but might lead to issues when attempting to read/program Dell parts, like CERC6/i or the JM475. + +Firmware +-------- + +To build the firmware, you will need Rust with the thumbv7m-none-eabi target. We unfortunately don't have Bazel integration yet, as rules\_rust don't integrate fully with Bazel's toolchain/configurability system. This should be revisited at some point. + +To get Rust with the right target, rustup is recommended (Nix users: `nix-shell -p rustup`): + + $ rustup update + $ rustup default stable + $ rustup target add thumbv7m-none-eabi + +Then, to build: + + $ cargo build --release + $ file target/thumbv7m-none-eabi/release/hbj11-flasher-bluepill + target/thumbv7m-none-eabi/release/hbj11-flasher-bluepill: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, with debug_info, not stripped + +Debug builds are also available, but they require a semihosting debugger attached - otherwise, they will immediately get stuck trying to log debug messages to the host. + +Flashing Firmware +----------------- + +If using a BlackMagicProbe: + + $ arm-none-eabi-gdb -x flash.gdb target/thumbv7m-none-eabi/release/hbj11-flasher-bluepill + [...] + Loading section .vector_table, size 0x130 lma 0x8000000 + Loading section .text, size 0x3820 lma 0x8000130 + Loading section .rodata, size 0xd88 lma 0x8003950 + Start address 0x08000130, load size 18136 + Transfer rate: 15 KB/sec, 906 bytes/write. + +You can then C-c C-d and let the device run, or keep running it under the debugger. It should enumerate via USB: + + $ lsusb -v | grep -A 4 0x16c0 + idVendor 0x16c0 Van Ooijen Technische Informatica + idProduct 0x27d8 libusb-bound devices + bcdDevice 0.10 + iManufacturer 1 Warsaw Hackerspace + iProduct 2 Web I2C Programmer + +We currently use an, uh, _community_ VID/PID. This will change in the future as we apply for a pair from pid.codes or elsewhere. + +Usage +----- + +The flasher is controller via [WebI2C](../web/) (through WebUSB). diff --git a/dc/hbj11/flasher/bluepill/flash.gdb b/dc/hbj11/flasher/bluepill/flash.gdb new file mode 100644 index 00000000..e447bfc8 --- /dev/null +++ b/dc/hbj11/flasher/bluepill/flash.gdb @@ -0,0 +1,7 @@ +# Flash script for BlackMagicProbe +target extended-remote /dev/ttyACM0 +monitor swdp_scan +attach 1 +load +# Attach to the running process. C-c and C-d to detach. +run diff --git a/dc/hbj11/flasher/bluepill/memory.x b/dc/hbj11/flasher/bluepill/memory.x new file mode 100644 index 00000000..d1529270 --- /dev/null +++ b/dc/hbj11/flasher/bluepill/memory.x @@ -0,0 +1,7 @@ +MEMORY +{ + /* Flash memory begins at 0x80000000 and has a size of 64kB*/ + FLASH : ORIGIN = 0x08000000, LENGTH = 64K + /* RAM begins at 0x20000000 and has a size of 20kB*/ + RAM : ORIGIN = 0x20000000, LENGTH = 20K +} diff --git a/dc/hbj11/flasher/bluepill/src/i2c.rs b/dc/hbj11/flasher/bluepill/src/i2c.rs new file mode 100644 index 00000000..35f52d68 --- /dev/null +++ b/dc/hbj11/flasher/bluepill/src/i2c.rs @@ -0,0 +1,372 @@ +/// USB Device Class for I2C transactions. +// +// It's not very good, and the API is weird. Someone with more USB device design experience could +// easily come up with something better. +// +// Control OUT transactions are used to perform I2C transfers to/from an internal buffer. +// Bulk IN/OUT transactions are used to transfer contents of the buffer to the host. It has not +// been optimized for speed or pipelining. +// +// To perform an I2C read: +// 1) Control OUT: ReadI2C(Address: 0xAA, Length: N) +// (0xAA is the device address, N is the amount of bytes to read. Cannot be larger than +// BUFFER_SIZE). +// This performs an I2C read of N bytes into the inne buffer of the device, starting at +// address 0. +// 2) Control IN: GetStatus() +// The host ensures that the transaction was either ACK or NACK by getting one byte of status +// from the device. +// 3) Control OUT: ReadBuffer(Address: X, Length: N) +// (X is the address within the buffer, N is the amount of bytes to transfer to the host. N +// cannot be larger than PACKET_SIZE). +// 4) Bulk IN: Read PACKET_SIZE bytes. +// Steps 3/4 can be skipped for scanning (the device won't mind the inner buffer not being read). +// +// To perform an I2C write: +// 1) Control OUT: SetWritePointer(Addrss: X) +// 2) Bulk OUT: Write at most PACKET_SIZE bytes. +// Repeat steps 1/2 to fill buffer with an I2c transaction. +// 3) Control OUT: WriteI2C(Address: 0x00, Length: N) +// (0xAA is the device address, N is the amount of bytes to write. Cannot be larger than +// BUFFER_SIZE). +// 4) Control IN: GetStatus() +// The host ensures that the transaction was either ACK or NACK by getting one byte of status +// from the device. + +use embedded_hal::digital::v2::OutputPin; +use usb_device::class_prelude::*; +use nb::Error as NbError; +use stm32f1xx_hal::{ + gpio::{gpiob::*, Alternate, OpenDrain}, + i2c::{BlockingI2c, Error as I2CError}, + pac::I2C1, + prelude::*, +}; + +use num_derive::FromPrimitive; +use num_traits::FromPrimitive; + +use crate::{hprint, hprintln}; + +// Size of buffer within class, in bytes. Dictates maximum I2C transaction size. +const BUFFER_SIZE: usize = 1024; +// Size of bulk packets. +const PACKET_SIZE: usize = 64; + +// All IN/OUT references bellow conform to typical USB naming, where IN: from device to host; OUT: +// from host to device. + +/// Request number passed within Control IN requests to the I2C interface (ie. 'gets' from device). +#[derive(FromPrimitive)] +#[repr(u8)] +enum ControlInRequest { + /// Write the current status as a single byte in response. + GetStatus = 1, +} + +/// Request number passed within Control OUT requests to the I2C interface (ie. 'sets' from the +/// host). +#[derive(FromPrimitive)] +#[repr(u8)] +enum ControlOutRequest { + /// Set LED on or off (value == 0 -> off; on otherwise). + SetLED = 1, + + /// Perform I2C bus read of a given length from a given I2C address. + /// I2C Address: lower 8 bits of value. + /// Read Length: upper 8 bits of value. + ReadI2C = 2, + + /// Schedule a BULK IN transaction on the USB bus with the contents of the inner buffer. + /// Buffer start address: lower 8 bits of value + /// Read Length: upper 8 bits of value. + ReadBuffer = 3, + + /// Perform I2C bus write of a given length to a given I2C address. + /// I2C Address: lower 8 bits of value. + /// Read Length: upper 8 bits of value. + WriteI2C = 4, + + /// Set inner buffer write pointer. Any subsequent BULK OUT will write to the buffer at that + /// address (but will not auto advance the pointer). + SetWritePointer = 5, +} + +/// Status of the I2C class. Combines information about requested transactions and I2C bus +/// responses. +#[derive(Copy, Clone)] +#[repr(u8)] +enum Status { + /// Last request okay. + OK = 0, + /// Last request contained an invalid argument. + InvalidArgument = 1, + /// Last request okay, resulted in a successful I2C transaction. + Ack = 2, + /// Last request okay, resulted in a NACKd I2C transaction. + Nack = 3, + /// Last request okay, resulted in a fully failed I2C transaction. + BusError = 4, +} + +pub struct I2CClass<'a, B: UsbBus, LED> { + interface: InterfaceNumber, + /// Bulk IN endpoint for buffer transfers to host. + ep_in: EndpointIn<'a, B>, + /// Bulk OUT endpoint for buffer transfers from host. + ep_out: EndpointOut<'a, B>, + + /// LED used for debugging. + led: LED, + + /// The underlying I2C device. + i2c_dev: BlockingI2c>, PB7>)>, + + /// Marker that is true when the host requested a BULK OUT via ReadBuffer. + expect_bulk_out: bool, + + /// The underlying buffer and its write pointer. + buffer: [u8; BUFFER_SIZE], + write_pointer: usize, + + /// The device's main status byte, used by host to check whether operations were succesful. + status: Status, +} + +impl I2CClass<'_, B, LED> { + pub fn new( + alloc: &UsbBusAllocator, + led: LED, + i2c_dev: BlockingI2c>, PB7>)>, + ) -> I2CClass<'_, B, LED> { + I2CClass { + interface: alloc.interface(), + ep_in: alloc.bulk(PACKET_SIZE as u16), + ep_out: alloc.bulk(PACKET_SIZE as u16), + led, i2c_dev, + + expect_bulk_out: false, + + buffer: [0; BUFFER_SIZE], + write_pointer: 0usize, + status: Status::OK, + } + } +} + +impl<'a, B: UsbBus, LED: OutputPin> UsbClass for I2CClass<'a, B, LED> { + fn reset(&mut self) { + self.expect_bulk_out = false; + self.status = Status::OK, + } + + fn control_in(&mut self, xfer: ControlIn) { + let req = xfer.request(); + + if req.request_type != control::RequestType::Vendor + || req.recipient != control::Recipient::Interface + || req.index != u8::from(self.interface) as u16 { + return + } + + match FromPrimitive::from_u8(req.request) { + /// Serve GetStatus: return this.status. + Some(ControlInRequest::GetStatus) => { + let status = self.status.clone() as u8; + xfer.accept(|buf| { + buf[0] = status; + Ok(1usize) + }).ok(); + }, + _ => { + hprintln!("Unhandled control in on iface: {:?}", req).unwrap(); + }, + } + } + + fn control_out(&mut self, xfer: ControlOut) { + let req = xfer.request(); + + if req.request_type != control::RequestType::Vendor + || req.recipient != control::Recipient::Interface + || req.index != u8::from(self.interface) as u16 { + return + } + + match FromPrimitive::from_u8(req.request) { + // Serve SetLED. + Some(ControlOutRequest::SetLED) => { + let on: bool = req.value > 0; + match on { + true => self.led.set_low(), + false => self.led.set_high(), + }.ok(); + xfer.accept().ok(); + }, + + // Serve ReadI2C: read len bytes from I2C addr into internal buffer. + Some(ControlOutRequest::ReadI2C) => { + let addr: u8 = (req.value & 0xff) as u8; + let len: u8 = (req.value >> 8) as u8; + if len as usize > BUFFER_SIZE || len < 1u8 { + self.status = Status::InvalidArgument; + xfer.accept().ok(); + return + } + if addr > 127u8 { + self.status = Status::InvalidArgument; + xfer.accept().ok(); + return + } + match self.i2c_dev.read(addr, &mut self.buffer[0usize..(len as usize)]) { + Ok(_) => { + self.status = Status::Ack; + }, + Err(NbError::Other(I2CError::Acknowledge)) => { + self.status = Status::Nack; + }, + Err(e) => { + hprintln!("When reading I2C (addr {}, {} bytes): {:?}", addr, len, e).ok(); + self.status = Status::BusError; + }, + } + xfer.accept().ok(); + }, + + // Serve ReadBuffer: send BULK IN with slice of buffer. + Some(ControlOutRequest::ReadBuffer) => { + let addr: u8 = (req.value & 0xff) as u8; + let len: u8 = (req.value >> 8) as u8; + + if len as usize > PACKET_SIZE || len < 1u8 { + self.status = Status::InvalidArgument; + xfer.accept().ok(); + return + } + + let start = addr as usize; + let end = (addr + len) as usize; + if end as usize > BUFFER_SIZE { + self.status = Status::InvalidArgument; + xfer.accept().ok(); + return + } + + hprintln!("READ BUFFER, addr: {}, len: {}", addr, len).ok(); + + self.status = Status::OK; + xfer.accept().ok(); + match self.ep_in.write(&self.buffer[start..end]) { + Ok(count) => { + }, + Err(UsbError::WouldBlock) => {}, + Err(err) => { + hprintln!("bulk write failed: {:?}", err).ok(); + }, + } + }, + + // Serve WriteI2C: write len bytes to I2C bus at addr from internal buffer. + Some(ControlOutRequest::WriteI2C) => { + let addr: u8 = (req.value & 0xff) as u8; + let len: u8 = (req.value >> 8) as u8; + if len as usize > BUFFER_SIZE || len < 1u8 { + self.status = Status::InvalidArgument; + xfer.accept().ok(); + return + } + if addr > 127u8 { + self.status = Status::InvalidArgument; + xfer.accept().ok(); + return + } + + hprintln!("WRITE I2C, addr: {}, len: {}", addr, len).ok(); + match self.i2c_dev.write(addr, &self.buffer[0usize..(len as usize)]) { + Ok(_) => { + self.status = Status::Ack; + }, + Err(NbError::Other(I2CError::Acknowledge)) => { + self.status = Status::Nack; + }, + Err(e) => { + hprintln!("When writing I2C (addr {}, {} bytes): {:?}", addr, len, e).ok(); + self.status = Status::BusError; + }, + } + xfer.accept().ok(); + }, + + // Serve SetWritePointer: set start address at which bytes from a BULK OUT will be + // written to. The write pointer does _not_ increment on every write, so will need to + // be manually controler after every BULK transfer. + Some(ControlOutRequest::SetWritePointer) => { + let pointer = req.value; + if (pointer as usize) >= BUFFER_SIZE { + self.status = Status::InvalidArgument; + xfer.accept().ok(); + return + } + hprintln!("SET WRITE PTR, pointer: {}", pointer).ok(); + self.write_pointer = pointer as usize; + self.status = Status::OK; + xfer.accept().ok(); + }, + _ => { + hprintln!("Unhandled control out on iface: {:?}", req).ok(); + }, + } + } + + fn get_configuration_descriptors( + &self, + writer: &mut DescriptorWriter, + ) -> usb_device::Result<()> { + writer.interface( + self.interface, + 0xff, + 21, 37, + )?; + writer.endpoint(&self.ep_in)?; + writer.endpoint(&self.ep_out)?; + + Ok(()) + } + + fn poll(&mut self) { + let mut temp_buf = [0; PACKET_SIZE]; + // Serve BULK OUT writes - copy bytes into internal buffer. + match self.ep_out.read(&mut temp_buf) { + Ok(count) => { + if self.expect_bulk_out { + self.expect_bulk_out = false; + } else { + panic!("unexpectedly read data from bulk out endpoint"); + } + hprintln!("SET BUFFER: ptr {}, {} bytes", self.write_pointer, count).ok(); + for (i, c) in temp_buf.iter().enumerate() { + let ptr = self.write_pointer + i; + // Silently drop bytes that do not fit in buffer. + if ptr >= BUFFER_SIZE { + continue; + } + self.buffer[ptr] = c.clone(); + } + }, + Err(UsbError::WouldBlock) => {}, + Err(err) => panic!("bulk read {:?}", err), + } + } + + fn endpoint_out(&mut self, addr: EndpointAddress) { + if addr == self.ep_out.address() { + self.expect_bulk_out = true; + } + } + + fn endpoint_in_complete(&mut self, addr: EndpointAddress) { + if addr == self.ep_in.address() { + // TODO(q3k): should we be doing something here? + } + } +} diff --git a/dc/hbj11/flasher/bluepill/src/main.rs b/dc/hbj11/flasher/bluepill/src/main.rs new file mode 100644 index 00000000..da6cefdc --- /dev/null +++ b/dc/hbj11/flasher/bluepill/src/main.rs @@ -0,0 +1,136 @@ +#![no_main] +#![no_std] + +extern crate panic_semihosting; + +use rtic::app; + +use cortex_m::asm::delay; +use stm32f1xx_hal::{ + gpio::{gpioc::*, Output, PushPull}, + i2c::{BlockingI2c, Mode}, + pac::{Peripherals}, + prelude::*, + usb::{Peripheral, UsbBus, UsbBusType}, +}; +use embedded_hal::digital::v2::OutputPin; + +use usb_device::bus; +use usb_device::prelude::*; + +use usbd_webusb::WebUsb; + +mod i2c; +mod print; + +/// The main RTIC application object. See RTIC documentation for more information about how to read +/// this. +#[app(device = stm32f1xx_hal::stm32, peripherals = true)] +const APP: () = { + struct Resources { + usb_dev: UsbDevice<'static, UsbBusType>, + webusb: WebUsb, + // The I2C USB device class that performs the main logic of accessing the I2C bus over USB + // for users of the device. + i2c: i2c::I2CClass<'static, UsbBusType, PC13>>, + } + + /// Idle loop to prevent WFI which in turn prevents debugging. + // TODO: make this only happen on debug builds? + #[idle] + fn idle(_: idle::Context) -> ! { + loop {} + } + + #[init] + fn init(cx: init::Context) -> init::LateResources { + static mut USB_BUS: Option> = None; + + let mut flash = cx.device.FLASH.constrain(); + let mut rcc = cx.device.RCC.constrain(); + + let clocks = rcc + .cfgr + .use_hse(8.mhz()) + .sysclk(48.mhz()) + .pclk1(24.mhz()) + .freeze(&mut flash.acr); + + assert!(clocks.usbclk_valid()); + + let mut gpioa = cx.device.GPIOA.split(&mut rcc.apb2); + let mut gpiob = cx.device.GPIOB.split(&mut rcc.apb2); + let mut gpioc = cx.device.GPIOC.split(&mut rcc.apb2); + + // Active-low LED on bluepill board. + let mut led = gpioc.pc13.into_push_pull_output(&mut gpioc.crh); + led.set_high().ok(); + + let mut afio = cx.device.AFIO.constrain(&mut rcc.apb2); + + // BluePill board has a pull-up resistor on the D+ line. + // Pull the D+ pin down to send a RESET condition to the USB bus. + // This forced reset is needed only for development, without it host + // will not reset your device when you upload new firmware. + let mut usb_dp = gpioa.pa12.into_push_pull_output(&mut gpioa.crh); + usb_dp.set_low().unwrap(); + delay(clocks.sysclk().0 / 100); + + let usb_dm = gpioa.pa11; + let usb_dp = usb_dp.into_floating_input(&mut gpioa.crh); + + let usb = Peripheral { + usb: cx.device.USB, + pin_dm: usb_dm, + pin_dp: usb_dp, + }; + + *USB_BUS = Some(UsbBus::new(usb)); + + let i2c_pins = ( + gpiob.pb6.into_alternate_open_drain(&mut gpiob.crl), + gpiob.pb7.into_alternate_open_drain(&mut gpiob.crl), + ); + + // Blocking I2C peripheral for use by the I2C app. + let i2c_dev = BlockingI2c::i2c1( + cx.device.I2C1, + i2c_pins, + &mut afio.mapr, + Mode::standard(100.khz()), + clocks, + &mut rcc.apb1, + 1000, 10, 1000, 1000, + ); + + // I2C app. + let i2c = i2c::I2CClass::new( + USB_BUS.as_ref().unwrap(), + led, i2c_dev, + ); + + let usb_dev = UsbDeviceBuilder::new(USB_BUS.as_ref().unwrap(), UsbVidPid(0x16c0, 0x27d8)) + .manufacturer("Warsaw Hackerspace") + .product("Web I2C Programmer") + // TODO(q3k): generate serial at build time? + .serial_number("2137") + .build(); + + init::LateResources { + usb_dev, i2c, + webusb: WebUsb::new( + USB_BUS.as_ref().unwrap(), + usbd_webusb::url_scheme::HTTPS, + "hackdoc.hackerspace.pl/dc/hbj11/flasher", + ), + } + } + + #[task(binds = USB_LP_CAN_RX0, resources = [usb_dev, webusb, i2c])] + fn usb_lp(cx: usb_lp::Context) { + cx.resources + .usb_dev + .poll(&mut [cx.resources.webusb, cx.resources.i2c]); + } +}; + diff --git a/dc/hbj11/flasher/bluepill/src/print.rs b/dc/hbj11/flasher/bluepill/src/print.rs new file mode 100644 index 00000000..3693b00d --- /dev/null +++ b/dc/hbj11/flasher/bluepill/src/print.rs @@ -0,0 +1,40 @@ +// Wrappers around hprint(ln) that get disabled during release builds. This prevents us from +// getting stuck in an hprint when a debugger is detached. + +#[cfg(debug_assertions)] +#[macro_export] +macro_rules! hprint { + ($s:expr) => { + cortex_m_semihosting::export::hstdout_str($s) + }; + ($($tt:tt)*) => { + cortex_m_semihosting::export::hstdout_fmt(format_args!($($tt)*)) + }; +} +#[cfg(debug_assertions)] +#[macro_export] +macro_rules! hprintln { + () => { + cortex_m_semihosting::export::hstdout_str("\n") + }; + ($s:expr) => { + cortex_m_semihosting::export::hstdout_str(concat!($s, "\n")) + }; + ($s:expr, $($tt:tt)*) => { + cortex_m_semihosting::export::hstdout_fmt(format_args!(concat!($s, "\n"), $($tt)*)) + }; +} + +#[cfg(not(debug_assertions))] +#[macro_export] +macro_rules! hprint { + () => { Result::<(), ()>::Ok(()) }; + ($s:expr, $($tt:tt)*) => { Result::<(), ()>::Ok(()) }; +} +#[cfg(not(debug_assertions))] +#[macro_export] +macro_rules! hprintln { + () => { Result::<(), ()>::Ok(()) }; + ($s:expr) => { Result::<(), ()>::Ok(()) }; + ($s:expr, $($tt:tt)*) => { Result::<(), ()>::Ok(()) }; +} diff --git a/dc/hbj11/flasher/web/README.md b/dc/hbj11/flasher/web/README.md new file mode 100644 index 00000000..73012558 --- /dev/null +++ b/dc/hbj11/flasher/web/README.md @@ -0,0 +1,18 @@ +WebI2C +====== + +A WebUSB interface for flashing I2C EEPROMs, notably the HJB11 FRU EEPROM. + +Usage +----- + +TODO(q3k): host public instance + +Development +----------- + +Plain javascript, bring a static file server, eg. Python's builtin one: + + $ cd hscloud/dc/hjb11/flasher/web/ + $ python3 -m http.server + diff --git a/dc/hbj11/flasher/web/fru.js b/dc/hbj11/flasher/web/fru.js new file mode 100644 index 00000000..a06fec94 --- /dev/null +++ b/dc/hbj11/flasher/web/fru.js @@ -0,0 +1,284 @@ +/* + * Platform Management FRU Information Storage Definition v1.0 + * Document Revision 1.3, March 24, 2015 + * + * From: https://www.intel.com/content/www/us/en/servers/ipmi/ipmi-platform-mgt-fru-infostorage-def-v1-0-rev-1-3-spec-update.html + */ +export class FRUParser { + constructor(data) { + this.data = data + } + + parseCommon(data) { + // 8. Common Header Format + let version = data[0]; + if ((version >> 4) !== 0) throw new Error("Invalid Common Header version"); + if ((version & 0b1111) !== 1) throw new Error("Invalid Common Header version"); + + let res = {}; + res.version = version; + res.internalUseStart = data[1] * 8; + res.chassisInfoStart = data[2] * 8; + res.boardInfoStart = data[3] * 8; + res.productInfoStart = data[4] * 8; + res.multiRecordInfoStart = data[5] * 8; + + let sum = data.reduce((a, b) => a + b, 0) & 0xff; + if (sum !== 0) throw new Error("Common area checksum error"); + + return res; + } + + parseBoardInfo(data) { + // 11. Board Info Area Format + let res = {}; + + let version = data[0]; + if ((version >> 4) !== 0) throw new Error("Invalid Board Info version"); + if ((version & 0b1111) !== 1) throw new Error("Invalid Board Info version"); + res.version = version; + + let areaLength = data[1] * 8; + if (areaLength > data.length) throw new Error("Invalid Board Info length"); + data = data.slice(0, areaLength); + + let sum = data.reduce((a, b) => a + b, 0) & 0xff; + if (sum !== 0) throw new Error("Board Info Area checksum error"); + + let r = new Reader(data); + r.skip(2); + + res.language = r.readLanguageCode(); + res.manufacturingDate = r.readDateTime(); + res.manufacturerName = r.readTypeLength(res.language); + res.productName = r.readTypeLength(res.language); + res.serialNumber = r.readTypeLength(res.language); + res.partNumber = r.readTypeLength(res.language); + res.fruFileID = r.readTypeLength(res.language); + res.custom = r.readTypeLength(res.language); + // Not sure if this is up to standard - the standard seems to say that + // C1 must always appear, but the Dell storage cards I've looked at + // skip it. There's an earlier C1, but that's part of the FRU File ID. + if (res.length > 0) { + if (r.readByte() !== 0xc1) throw new Error("Custom area must end with C1"); + } + return res; + } + + parseInternalUseDell(data) { + let version = data[0]; + if ((version >> 4) !== 0) throw new Error("Invalid Internal Use version"); + if ((version & 0b1111) !== 1) throw new Error("Invalid Internal Use version"); + + if ((new TextDecoder().decode(data.slice(1,5))) !== "DELL") { + throw new Error("Invalid 'DELL' magic in internal area"); + } + + let sum = data.reduce((a, b) => a + b, 0) & 0xff; + if (sum !== 0) throw new Error("Dell Internal Area checksum error"); + + return {}; + } + + parse() { + this.common = this.parseCommon(this.data.slice(0, 8)) + + if (this.common.boardInfoStart !== 0) { + let data = this.data.slice(this.common.boardInfoStart, this.data.length); + this.boardInfo = this.parseBoardInfo(data); + } else { + this.boardInfo = {}; + } + this.internalUse = {}; + if (this.common.internalUseStart !== 0) { + let data = this.data.slice(this.common.internalUseStart, this.data.length); + this.internalUse.dell = this.parseInternalUseDell(data); + } + } + + stringify() { + let res = []; + res.push(`Version: ${this.common.version}`) + res.push(`Board Info:`) + let bi = this.boardInfo; + res.push(` Language: ${bi.language}`) + if (bi.manufacturingDate !== undefined) + res.push(` Manufacturing Date: ${bi.manufacturingDate}`); + res.push(` Manufacturer Name: ${bi.manufacturerName}`) + res.push(` Product Name: ${bi.productName}`) + res.push(` Serial Number: ${bi.serialNumber}`) + res.push(` PartNumber: ${bi.partNumber}`) + res.push(` FRU File ID: ${bi.fruFileID}`) + + if (this.internalUse.dell !== undefined) { + res.push("Internal Use: DELL-specific") + } + return res.join("\n"); + } +} + +class Reader { + constructor(data) { + this.data = data; + } + skip(n) { + this.data = this.data.slice(n); + } + readByte() { + let num = this.data[0]; + this.data = this.data.slice(1); + return num; + } + readLanguageCode() { + let num = this.readByte(); + let encoding = num >> 6; + let language = [ + "en", "aa", "ab", "af", "am", "ar", "as", "ay", "az", "ba", "be", + "bg", "bh", "bi", "bn", "bo", "br", "ca", "co", "cs", "cy", "da", + "de", "dz", "el", "en", "eo", "es", "et", "eu", "fa", "fi", "fj", + "fo", "fr", "fy", "ga", "gd", "gl", "gn", "gu", "ha", "hi", "hr", + "hu", "hy", "ia", "ie", "ik", "in", "is", "it", "iw", "ja", "ji", + "jw", "ka", "kk", "kl", "km", "kn", "ko", "ks", "ku", "ky", "la", + "ln", "lo", "lt", "lv", "mg", "mi", "mk", "ml", "mn", "mo", "mr", + "ms", "mt", "my", "na", "ne", "nl", "no", "oc", "om", "or", "pa", + "pl", "ps", "pt", "qu", "rm", "rn", "ro", "ru", "rw", "sa", "sd", + "sg", "sh", "di", "sk", "dl", "sm", "sn", "so", "sq", "sr", "ss", + "st", "su", "sv", "sw", "ta", "te", "tg", "th", "ti", "tk", "tl", + "tn", "to", "tr", "ts", "tt", "tw", "uk", "ur", "uz", "vi", "vo", + "wo", "xh", "yo", "zh", "zu", + ][num & 0b111111]; + return language; + } + readTypeLength(language) { + let tag = this.readByte(); + let type = tag >> 6; + let len = tag & 0b111111; + switch (type) { + case 0: + return this.readTLBinary(len); + case 1: + return this.readTLBCDPlus(len); + case 2: + return this.readTL6BASCII(len); + case 3: + return this.readTLString(len, language); + } + } + readTLBinary(len) { + let data = this.data.slice(0, len); + this.data = this.data.slice(len); + return data; + } + readTLBCDPlus(len) { + let data = this.data.slice(0, len); + this.data = this.data.slice(len); + const lookup = "012345689 -.???"; + let res = []; + for (const c of data) { + let upper = lookup[c >> 4]; + let lower = lookup[c & 0b1111]; + if ((upper === "?") || (lower === "?")) { + throw new Error("Invalid BCD Plus data"); + } + res.push(upper); + res.push(lower); + } + return res.join(""); + } + readTL6BASCII(len) { + let data = this.data.slice(0, len); + this.data = this.data.slice(len); + const lookup = + " !\"#$%&'()*+,-./" + + "0123456789:;<=>?" + + "@ABCDEFGHIJKLMNO" + + "PQRSTUVWXYZ[\\]^_"; + + let res = []; + let availbits = 0; + let bits = 0; + while ((data.length > 0) || (availbits >= 6)) { + if (availbits < 6) { + bits |= (data[0] << availbits); + availbits += 8; + data = data.slice(1); + } + let n = bits & 0b111111; + availbits -= 6; + bits >>= 6; + res.push(lookup[n]); + } + return res.join(""); + } + readTLString(len, language) { + let data = this.data.slice(0, len); + this.data = this.data.slice(len); + // 13. Type/Length Byte Format + // Yikes, Intel. + if (language !== "en") { + throw new Error("Unicode unimplemented"); + } + // This should be 'ASCII + Latin 1', but this is a good enough approximation. + return new TextDecoder().decode(data); + } + readDateTime() { + let minutes = this.readByte() | (this.readByte() << 8) | (this.readByte() << 16); + if (minutes !== 0) throw new Error("Datetime parsing not implemented"); + return undefined; + } +} + +export class HBJ11FRUAssembler { + constructor(serial) { + this.serial = serial; + } + + assemble() { + // Strings can be longer in FRU spec, but let's keep it conservative. + if (this.serial.length > 8) { + throw new Error("Serial too long"); + } + // Same layout as DELL FRUs, board specific after common, internal use after board specific. + let common = [0x01, 0x0a, 0x00, 0x01, 0x00, 0x00, 0x00, 0xf4]; + + // Similar layout to DELL FRUs, 72 bytes. + let board = [ + 0x01, // Version 1 + 0x09, // Length (9*8 == 72 bytes) + 0x00, 0x00, 0x00, 0x00, // Manufacturing time (unspecified) + 0xC7, 98, 103, 112, 46, 119, 116, 102, // Manufacturer: bgp.wtf + 0xDE, 83, 65, 84, 65, 32, 82, 101, 112, 101, 97, 116, 101, 114, // Product name: SATA Repeater + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, // ... pad above to 30 chars. + ]; + // Serial number tag/length. + board.push(0xC0 | (this.serial.length)); + // Serial number. + for (const c of this.serial) { + board.push(c.charCodeAt()); + } + board = board.concat([ + 0xC7, 72, 66, 74, 49, 49, 65, 48, // Part number: HBJ11A0 + 0xC1, 0x02, 0xC1, 0x00, // FRU File ID 2, one-byte custom area/end? Weird shit. + ]); + if (board.length > 71) { + throw new Error("Board Area too long!"); + } + // Pad with zeroes. + board = board.concat(Array(71 - board.length).fill(0)); + // Calculate checksum. + let sum = (0xff ^ (board.reduce((a, b) => a + b, 0) & 0xff)); + board.push((sum + 1) & 0xff); + + // Dell internal use. + let dell = [ + 0x01, 0x44, 0x45, 0x4c, 0x4c, 0xf7, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0x01, 0x13, 0x58, 0x01, + 0x0f, 0x68, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x0d, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x01, + 0x00, 0x01, 0x01, 0x00, + ]; + + let eeprom = common.concat(board).concat(dell); + // Pad to 256 bytes. + eeprom = eeprom.concat(Array(256 - eeprom.length).fill(0)); + return new Uint8Array(eeprom); + } +} diff --git a/dc/hbj11/flasher/web/i2c.js b/dc/hbj11/flasher/web/i2c.js new file mode 100644 index 00000000..6aa4ed7c --- /dev/null +++ b/dc/hbj11/flasher/web/i2c.js @@ -0,0 +1,159 @@ +/** + * Low-level interface to programmer's I2C USB class. + * + * See //dc/hbj11/flasher/bluepill/src/i2c.rs for more information. + */ + +const ControlInRequest = Object.freeze({ + 'GetStatus': 1, +}); + +const ControlOutRequest = Object.freeze({ + 'SetLED': 1, + 'ReadI2C': 2, + 'ReadBuffer': 3, + 'WriteI2C': 4, + 'SetWritePointer': 5, +}); + +export const Status = Object.freeze({ + 'Idle': 0, + 'InvalidArgument': 1, + 'Ack': 2, + 'Nack': 3, + 'BusError': 4, +}); + +export const StatusFromU8 = function(u8) { + for (const label of Object.keys(Status)) { + let val = Status[label]; + if (val === u8) { + return label; + } + } +}; + +/** + * Low-level interface to programmer's I2C USB class. Thinly wraps available + * USB transfers. + */ +export class USBI2CClassInterface { + /** + * @param {USBDevice} usb - The WebUSB device that backs this USB class. + */ + constructor(device) { + this.usb = device; + this.BUFFER_SIZE = 1024; + this.PACKET_SIZE = 64; + } + + /** + * Open this programmer via WebUSB and finds all required endpoints. + */ + async open() { + await this.usb.open(); + await this.usb.selectConfiguration(1); + await this.usb.claimInterface(0); + + let eps = this.usb.configuration.interfaces[0].alternate.endpoints; + this.bulk_out = null; + this.bulk_in = null; + for (const ep of eps) { + if (ep.direction == "out" && ep.type == "bulk") { + this.bulk_out = ep; + } + if (ep.direction == "in" && ep.type == "bulk") { + this.bulk_in = ep; + } + } + if (this.bulk_out === null) { + throw new Error("Could not find bulk out endpoint"); + } + if (this.bulk_in === null) { + throw new Error("Could not find bulk in endpoint"); + } + } + + /** + * Performs a USB Control OUT request to the I2C class. + * @param {number} request - Request number for transfer (0-255). + * @param {number} value - Value for transfer (0-65535). + * @returns {Promise} The underlying WebUSB transfer result. + */ + async controlOut(request, value) { + return await this.usb.controlTransferOut({ + requestType: "vendor", + recipient: "interface", + request: request, + value: value, + index: 0, + }); + } + + /** + * Read status from programmer. + * @returns {Status} The status of the programmer. + */ + async getStatus() { + let res = await this.usb.controlTransferIn({ + requestType: "vendor", + recipient: "interface", + request: ControlInRequest.GetStatus, + value: 0, + index: 0 + }, 1); + if (res.data.byteLength < 1) { + throw new Error('returned data too short') + } + return res.data.getInt8(0); + } + + /** + * Sends SetLED control OUT request. + */ + async setLED(on) { + return await this.controlOut(ControlOutRequest.SetLED, on ? 1 : 0); + } + + /** + * Sends ReadI2C control OUT request. + */ + async readI2C(addr, length) { + return await this.controlOut(ControlOutRequest.ReadI2C, (length << 8) | addr); + } + + /** + * Sends ReadBuffer control OUT request. + */ + async readBuffer(addr, length) { + return await this.controlOut(ControlOutRequest.ReadBuffer, (length << 8) | addr); + } + + /** + * Sends WriteI2C control OUT request. + */ + async writeI2C(addr, length) { + return await this.controlOut(ControlOutRequest.WriteI2C, (length << 8) | addr); + } + + /** + * Sends SetWritePointer control OUT request. + */ + async setWritePointer(addr) { + return await this.controlOut(ControlOutRequest.SetWritePointer, addr); + } + + /** + * Requets bulk IN data. + */ + async bulkIn(length) { + return await this.usb.transferIn(this.bulk_in.endpointNumber, length); + } + + /** + * Sends bulk OUT data. + */ + async bulkOut(data) { + return await this.usb.transferOut(this.bulk_out.endpointNumber, data); + } +} diff --git a/dc/hbj11/flasher/web/index.html b/dc/hbj11/flasher/web/index.html new file mode 100644 index 00000000..24e1a64b --- /dev/null +++ b/dc/hbj11/flasher/web/index.html @@ -0,0 +1,142 @@ + + + + + Web I2C Flasher + + + +
+

Web I2C Flash

+
+
+ +
+
+
+ + + diff --git a/dc/hbj11/flasher/web/main.js b/dc/hbj11/flasher/web/main.js new file mode 100644 index 00000000..9691f9ce --- /dev/null +++ b/dc/hbj11/flasher/web/main.js @@ -0,0 +1,472 @@ +/* + * WebI2C, a web interface for flashing I2C EEPROMS, notably FRU EEPROMs for + * the HBJ11. + */ + +import { FRUParser, HBJ11FRUAssembler } from './fru.js'; +import { Status, StatusFromU8, USBI2CClassInterface } from './i2c.js'; + +/** + * I2CDevice is an I2C device (eg. EEPROM) on the I2C bus, attached via a + * Programmer. + */ +class I2CDevice { + constructor(programmer, addr) { + this.programmer = programmer; + this.addr = addr; + this.dump = ""; + } + + /** + * Treat this device as an I2C EEPROM (eg. 24C02) and read its content. + * @param {number} addr - The address in the EEPROM to start reading at. + * @param {number} length - Count of bytes to read starting at address. + * @returns {Promise} Contents of the EEPROM. + */ + async readFlash(addr, length) { + // Always send a non-zero seek, otherwise 24C02 sometimes NACKs? + await this.programmer.writeI2C(this.addr, new Uint8Array([1])); + await this.programmer.readI2C(this.addr, 1); + + // Chunk up reads into 128 bytes. + let i = 0; + const max_chunk_size = 128; + let flash = new Uint8Array(length); + while (i < length) { + await this.programmer.writeI2C(this.addr, new Uint8Array([i])); + let chunk_size = length - i; + if (chunk_size > max_chunk_size) { + chunk_size = max_chunk_size; + } + let res = await this.programmer.readI2C(this.addr, chunk_size); + flash.set(new Uint8Array(res.buffer), i); + i += chunk_size; + } + return flash; + } + + /** + * Threat this devices as an I2C EEPROM on a HBJ11 and flash it with a given + * serial nyumber. + * @param {string} serial - The serial number of the HJB11 to brand it with. + * @param {HTMLButtonelement} button - Button used to trigger this action, + * will be disabled while the flashing is + * performed. + */ + async writeHBJ11(serial, button) { + // Always send a non-zero seek, otherwise 24C02 sometimes NACKs? + await this.programmer.writeI2C(this.addr, new Uint8Array([1])); + await this.programmer.readI2C(this.addr, 1); + + let text = button.innerText; + button.disabled = true; + button.innerText = "Flashing..."; + + let data = new HBJ11FRUAssembler(serial).assemble(); + + // Chunk up writes into 16 bytes. + let chunks = []; + for (let i = 0; i < data.length; i+= 16) { + chunks.push([i].concat(Array.from(data.slice(i, i+16)))); + } + + for (const chunk of chunks) { + await this.programmer.writeI2C(this.addr, new Uint8Array(chunk)); + } + + button.disabled = false; + button.innerText = text; + } + + render(div) { + div.innerHTML = ""; + let deviceName = document.createElement("div"); + deviceName.className = "deviceName"; + deviceName.appendChild(document.createTextNode(`Device 0x${this.addr.toString(16)}`)); + + div.appendChild(deviceName); + + let deviceOptions = document.createElement("div"); + deviceOptions.className = "deviceOptions"; + + let readButton = document.createElement("button"); + readButton.appendChild(document.createTextNode("Read flash")); + readButton.onclick = async () => { + let res = await this.readFlash(this.addr, 256); + + this.dump = ""; + const hex = "0123456789ABCDEF"; + for (let i = 0; i < res.length; i += 16) { + let block = res.slice(i, Math.min(i+16, res.length)); + let addr = ("0000" + i.toString(16)).slice(-4); + let codes = Array.from(block.values()).map((code) => { + return " " + hex[(0xF0 & code) >> 4] + hex[0x0f & code]; + }).join(""); + codes += " ".repeat(16 - block.length); + let chars = Array.from(block.values()).map((code) => { + if (code < 0x20 || code > 0x7e) { + return "."; + } + return String.fromCharCode(code); + }).join(""); + codes += " ".repeat(16 - block.length); + this.dump += (addr + " " + codes + " " + chars + "\n"); + } + + let p = new FRUParser(res); + try { + p.parse(); + this.dump += "\nFRU EEPROM:\n"; + this.dump += p.stringify(); + } catch(err) { + this.dump += "\nNot an FRU EEPROM: " + err; + } + + console.log(this.dump); + + this.render(div); + }; + deviceOptions.appendChild(readButton); + + let makeButton = document.createElement("button"); + makeButton.appendChild(document.createTextNode("Make HBJ11")); + makeButton.onclick = async () => { + await this.writeHBJ11(window.prompt("Enter HBJ11 Serial", "A0000"), makeButton); + }; + deviceOptions.appendChild(makeButton); + + deviceName.appendChild(deviceOptions); + + if (this.dump.length > 0) { + let deviceDump = document.createElement("pre"); + + deviceDump.className = "deviceDump"; + deviceDump.innerText = this.dump; + div.appendChild(deviceDump); + } + } +} + +/** + * A list of I2CDevices, eg. EEPROMs. Used for DOM rendering. + */ +class I2CDeviceList { + constructor() { + this.list = []; + } + + set(devices) { + this.list = devices; + } + + render(div) { + if (this.list.length === 0) { + div.innerHTML = "No devices..."; + return; + } + + for (const device of this.list) { + let deviceDiv = document.createElement("div"); + deviceDiv.className = "device"; + device.render(deviceDiv); + div.appendChild(deviceDiv); + } + } +} + +/** + * A WebI2C compatible programmer accessed over USB. + */ +class Programmer { + /** + * @param {USBDevice} usb - The WebUSB device that backs this programmer. + */ + constructor(usb) { + this.usb = usb; + this.i2c = new USBI2CClassInterface(usb); + this.devices = new I2CDeviceList(); + } + /** + * Get programmer manufacturer name. + * @returns {string} The name. + */ + get manufacturerName() { + return this.usb.manufacturerName; + } + + /** + * Get programmer product name. + * @returns {string} The name. + */ + get productName() { + return this.usb.productName; + } + + /** + * Get programmer serial number. + * @returns {string} The name. + */ + get serialNumber() { + return this.usb.serialNumber; + } + + /** + * Compares two Programmers and checks if they're using the same WebUSB + * device underneath. This is used for housekeeping of the ProgrammerList. + */ + equal(other) { + let one = this.usb; + let two = other.usb; + return (one.vendorId == two.vendorId) + && (one.productId == two.productId) + && (one.serialNumber == two.serialNumber); + } + + /** + * Performs an I2C read on the bus of the programmer and reads the resulting + * data from the buffer. The readout is performed in chunks over multiple + * Bulk transfer. + * @param {number} addr - Address of the I2C device to read from. + * @param {number} length - Number of bytes to read from I2C (not larger than + * BUFFER_SIZE). + * @returns {object} Object with status and bufer keys. TODO(q3k): declare type. + */ + async readI2C(addr, length) { + await this.i2c.readI2C(addr, length); + let status = await this.i2c.getStatus(); + if (status !== Status.Ack) { + return {status: status, buffer: null}; + } + let buffer = new Uint8Array(length); + let i = 0; + while (i < length) { + let chunkSize = length - i; + if (chunkSize > this.i2c.PACKET_SIZE) { + chunkSize = this.i2c.PACKET_SIZE; + } + let chunk = await this.readBuffer(i, chunkSize); + buffer.set(new Uint8Array(chunk.buffer), i); + i += chunkSize; + } + return {status: status, buffer: buffer}; + } + + /** + * Transfers data to internal buffer of programmers and performs an I2C write + * with the given data. + * @param {number} addr - Address of the I2C to write data to. + * @param {ArrayBuffer} data - Data to write to device. + */ + async writeI2C(addr, data) { + let i = 0; + while (i < data.length) { + let end = i + this.i2c.PACKET_SIZE; + if (end > data.length) { + end = data.length; + } + let chunk = data.slice(i, end); + await this.writeBuffer(i, chunk); + i = end; + } + await this.i2c.writeI2C(addr, data.length); + } + + /** + * Performs a scan of the I2C bus for all connected devices and upgrades the + * internal I2CDeviceList with found I2CDevices. + * @param {HTMLButtonElement} button - Button that will be disabled when the + * Scan is performed. + */ + async scan(button) { + let text = button.innerText; + button.innerText = "Scanning..."; + button.disabled = true; + + let present = []; + for (let i = 0; i < 127; i++) { + let res = await this.readI2C(i, 1); + switch (res.status) { + case Status.Ack: + present.push(new I2CDevice(this, i)); + break; + case Status.Nack: + break; + default: + throw new Error(`When scanning ${i}: ${StatusFromU8(res.status)}`); + } + } + this.devices.set(present); + + button.disabled = false; + button.innerText = text; + } + + /** + * Blinks the programmer's LED. + * @param {HTMLButtonElement} button - Button that will be disabled when the + * LED blinks. + */ + async blink(button) { + let on = true; + button.disabled = true; + let text = button.innerText; + button.innerText = "Blinking..."; + for (let i = 0; i < 20; i++) { + await this.i2c.setLED(on); + await new Promise(r => setTimeout(r, 100)); + on = !on; + } + button.disabled = false; + button.innerText = text; + } + + /** + * Requests buffer readout from device via ReadBuffer control transfer and + * then performs a single read via the Bulk IN endpoint. + * @param {number} addr - Address within the buffer to start read at. + * @param {number} length - Number of bytes to read (not larger than + * PACKET_SIZE). + * @returns {ArrayBuffer} Data read from buffer. + */ + async readBuffer(addr, length) { + await this.i2c.readBuffer(addr, length); + let status = await this.i2c.getStatus(); + if (status !== Status.Idle) { + throw new Error(`When requesting buffer: ${StatusFromU8(res.status)}`); + } + let res = await this.i2c.bulkIn(length); + return res.data; + } + + /** + * Writes bytes to internal buffer. + * @param {number} addr - Address within the buffer to start write at. + * @param {ArrayBuffer} data - Data to write to buffer (must not be longer + * than PACKET_SIZE). + */ + async writeBuffer(addr, data) { + await this.i2c.setWritePointer(addr); + let status = await this.i2c.getStatus(); + if (status !== Status.Idle) { + throw new Error(`When setting pointer: ${StatusFromU8(res.status)}`); + } + await this.i2c.bulkOut(data); + } + + + render(div) { + let programmer = document.createElement("div"); + programmer.className = "programmer"; + let programmerName = document.createElement("div"); + programmerName.className = "programmerName"; + programmerName.appendChild(document.createTextNode(this.manufacturerName)); + programmerName.appendChild(document.createTextNode(" ")); + let b = document.createElement("b"); + b.textContent = this.productName; + programmerName.appendChild(b); + + let programmerOptions = document.createElement("div"); + programmerOptions.className = "programmerOptions"; + + let blinkButton = document.createElement("button"); + blinkButton.appendChild(document.createTextNode("Blink LED")); + blinkButton.onclick = async () => { + await this.blink(blinkButton); + }; + programmerOptions.appendChild(blinkButton); + + let devices = document.createElement("div"); + devices.className = "devices"; + + let scanButton = document.createElement("button"); + scanButton.appendChild(document.createTextNode("Scan I2C Bus")); + scanButton.onclick = async () => { + await this.scan(scanButton); + devices.innerText = ""; + this.devices.render(devices) + }; + programmerOptions.appendChild(scanButton); + + programmerName.appendChild(programmerOptions); + programmer.appendChild(programmerName); + + this.devices.render(devices) + programmer.append(devices); + + div.appendChild(programmer); + } +} + +/** + * List of Programmers, used for rendering to DOM. + */ +class ProgrammerList { + constructor(list) { + this.list = []; + for (const l of list) { + this.list.push(l); + } + this.status = {}; + } + async addProgrammer(programmer) { + let existing = this.list.filter(d => d.equal(programmer)); + if (existing.length == 0) { + this.list.push(programmer); + await programmer.i2c.open(); + } + } + removeProgrammer(programmer) { + this.list = this.list.filter(d => !d.equal(programmer)); + } + render() { + let div = document.querySelector("#programmers"); + div.innerText = ""; + for (const programmer of this.list) { + programmer.render(div); + } + } +} + +if (navigator.usb === undefined || navigator.usb.requestDevice === undefined) { + alert("No WebUSB support! Please use a Chromium-based browser."); +} + +// 'global' ProgrammerList, modified by document/USB events. +let list = null; + +document.addEventListener('DOMContentLoaded', async () => { + let programmers = (await navigator.usb.getDevices()).map(d => new Programmer(d)); + for (const programmer of programmers) { + await programmer.i2c.open(); + } + list = new ProgrammerList(programmers); + list.render(); +}); + +navigator.usb.addEventListener('connect', async event => { + await list.addProgrammer(new Programmer(event.device)); + list.render(); +}); + +navigator.usb.addEventListener('disconnect', event => { + list.removeProgrammer(new Programmer(event.device)); + list.render(); +}); + +document.getElementById("connect").onclick = async () => { + let device; + try { + device = await navigator.usb.requestDevice({ + filters: [{ + vendorId: 0x16c0, + productId: 0x27d8, + }] + }); + } catch (err) { + return; + }; + if (device !== undefined) { + await list.addProgrammer(new Programmer(device)); + list.render(); + } +};