diff --git a/.woodpecker.yml b/.woodpecker.yml new file mode 100644 index 0000000..943053b --- /dev/null +++ b/.woodpecker.yml @@ -0,0 +1,28 @@ +pipeline: + fmt: + group: default + image: rust_avr + commands: + - cd firmware/rust/ + - cargo fmt --all -- --check + + clippy: + group: default + image: rust_avr + commands: + - cd firmware/rust/ + - cargo clippy --all-features + + doc: + group: default + image: rust_avr + commands: + - cd firmware/rust/ + - cargo doc --all-features + + build: + group: default + image: rust_avr + commands: + - cd firmware/rust/ + - cargo build --all-features diff --git a/README.md b/README.md index 1b02b01..2d51b42 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,11 @@ -# Clock Generator +# Clock Generator [![status-badge](https://ci.onders.org/api/badges/finga/clock_generator/status.svg)](https://ci.onders.org/finga/clock_generator) -A simple board operating another Si5351 clock generator board. +A simple board to control an Si5351 clock generator board with the +TWI. Though, this is not limited to the Si5351 board as it is designed +to be universal usable. -## Motherboard +## Board +The board is populated with an ATmega328(p), a display and its +backlight driver, a switchable rotary encoder and powered by a mini +USB port. All unneeded MCU pins are accessible via pin headers. -The board is populated by an ATmega328(p), powered by a mini USB port, -a display and its backlight driver as well as a switchable rotary -encoder. The board is designed to be used not only for this project, -but also for similar projects where peripherals such as a display and -a rotary encoder are needed. All free mcu pins are accessible via pin -headers. diff --git a/firmware/rust/.cargo/config.toml b/firmware/rust/.cargo/config.toml new file mode 100644 index 0000000..bdea107 --- /dev/null +++ b/firmware/rust/.cargo/config.toml @@ -0,0 +1,6 @@ +[build] +target = "avr-specs/avr-atmega328p.json" + +[unstable] +build-std = ["core"] +build-std-features = ["compiler-builtins-mangled-names"] diff --git a/firmware/rust/.gitignore b/firmware/rust/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/firmware/rust/.gitignore @@ -0,0 +1 @@ +/target diff --git a/firmware/rust/Cargo.lock b/firmware/rust/Cargo.lock new file mode 100644 index 0000000..74b8068 --- /dev/null +++ b/firmware/rust/Cargo.lock @@ -0,0 +1,233 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "atmega-hal" +version = "0.1.0" +source = "git+https://github.com/rahix/avr-hal?branch=main#e897783816437a677aa577ddfdaa34e9a1e86d96" +dependencies = [ + "avr-device", + "avr-hal-generic", +] + +[[package]] +name = "avr-device" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eb440a38bef22cef233b3eb46ef13e588eb8dcc70a1bdce3f00ebc6bf6b48dd" +dependencies = [ + "avr-device-macros", + "bare-metal", + "cfg-if", + "vcell", +] + +[[package]] +name = "avr-device-macros" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c62997db86a92e73b68ab491d4379e1ddb12e94a9eb542180bc6b60f0e09f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "avr-hal-generic" +version = "0.1.0" +source = "git+https://github.com/rahix/avr-hal?branch=main#e897783816437a677aa577ddfdaa34e9a1e86d96" +dependencies = [ + "avr-device", + "cfg-if", + "embedded-hal", + "nb 0.1.3", + "paste", + "ufmt", + "void", +] + +[[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 = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "clock-generator" +version = "0.1.0-dev" +dependencies = [ + "atmega-hal", + "avr-device", + "embedded-hal", + "nb 1.0.0", + "panic-halt", + "si5351", +] + +[[package]] +name = "embedded-hal" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35949884794ad573cf46071e41c9b60efb0cb311e3ca01f7af807af1debc66ff" +dependencies = [ + "nb 0.1.3", + "void", +] + +[[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 = "panic-halt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de96540e0ebde571dc55c73d60ef407c653844e6f9a1e2fdbd40c07b9252d812" + +[[package]] +name = "paste" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0744126afe1a6dd7f394cb50a716dbe086cb06e255e53d8d0185d82828358fb5" + +[[package]] +name = "proc-macro-hack" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" + +[[package]] +name = "proc-macro2" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" +dependencies = [ + "proc-macro2", +] + +[[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 = "si5351" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06c1f1f280165963ce5fdff953a5303ac186b2b723aa1ed33916db70ceabfb8" +dependencies = [ + "bitflags", + "embedded-hal", +] + +[[package]] +name = "syn" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "ufmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e7ecea7ef79d3f8f878eee614afdf5256475c63ad76139d4da6125617c784a0" +dependencies = [ + "proc-macro-hack", + "ufmt-macros", + "ufmt-write", +] + +[[package]] +name = "ufmt-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed813e34a2bfa9dc58ee2ed8c8314d25e6d70c911486d64b8085cb695cfac069" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ufmt-write" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e87a2ed6b42ec5e28cc3b94c09982969e9227600b2e3dcbc1db927a84c06bd69" + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "vcell" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77439c1b53d2303b20d9459b1ade71a83c716e3f9c34f3228c00e6f185d6c002" + +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" diff --git a/firmware/rust/Cargo.toml b/firmware/rust/Cargo.toml new file mode 100644 index 0000000..50a1fe9 --- /dev/null +++ b/firmware/rust/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "clock-generator" +version = "0.1.0-dev" +authors = ["finga "] +edition = "2018" +license = "GPL-3.0-or-later" + +[[bin]] +name = "clock-generator" +test = false +bench = false + +[dependencies] +panic-halt = "0.2" +nb = "1.0" +embedded-hal = "0.2" +avr-device = { version = "0.3", features = ["atmega328p"] } +si5351 = "0.2" + +[dependencies.atmega-hal] +git = "https://github.com/rahix/avr-hal" +branch = "main" +features = ["atmega328p", "rt"] + +[profile.dev] +panic = "abort" +lto = true +opt-level = "s" + +[profile.release] +panic = "abort" +codegen-units = 1 +debug = true +lto = true +opt-level = "s" diff --git a/firmware/rust/Makefile.toml b/firmware/rust/Makefile.toml new file mode 100644 index 0000000..e47cb7d --- /dev/null +++ b/firmware/rust/Makefile.toml @@ -0,0 +1,45 @@ +[env] +MCU = "atmega328p" +PROGRAMMER = "usbasp" +EXTENDED_FUSE = "0xFF" +HIGH_FUSE = "0xD6" +LOW_FUSE = "0xE2" + +[tasks.size] +description = "Print usage of memory segments" +dependencies = ["build"] +command = "avr-size" +args = ["--format=avr", "--mcu=${MCU}", "target/avr-atmega328p/debug/clock-generator.elf"] + +[tasks.copy_flash] +description = "Extract the flash" +dependencies = ["build"] +command = "avr-objcopy" +args = ["-O", "ihex", "-j", ".text", "-j", ".data", "target/avr-atmega328p/debug/clock-generator.elf", "target/avr-atmega328p/debug/clock-generator.hex"] + +[tasks.flash] +description = "Flash the firmware" +dependencies = ["copy_flash", "size"] +command = "avrdude" +args = ["-p", "${MCU}", "-c", "${PROGRAMMER}", "-U", "flash:w:target/avr-atmega328p/debug/clock-generator.hex:a"] + +[tasks.copy_eeprom] +description = "Extract the EEPROM" +dependencies = ["build"] +command = "avr-objcopy" +args = ["--change-section-lma", ".eeprom=0", "-O", "ihex", "-j", ".eeprom", "target/avr-atmega328p/debug/clock-generator.elf", "target/avr-atmega328p/debug/clock-generator.eep"] + +[tasks.eeprom] +description = "Flash the eeprom" +dependencies = ["copy_eeprom", "size"] +command = "avrdude" +args = ["-p", "${MCU}", "-c", "${PROGRAMMER}", "-U", "eeprom:w:target/avr-atmega328p/debug/clock-generator.eep:a"] + +[tasks.fuses] +description = "Burn the fuses" +command = "avrdude" +args = ["-p", "${MCU}", "-c", "${PROGRAMMER}", "-U", "efuse:w:${EXTENDED_FUSE}:m", "-U", "hfuse:w:${HIGH_FUSE}:m", "-U", "lfuse:w:${LOW_FUSE}:m"] + +[tasks.all] +description = "Execute all tasks" +run_task = { name = ["fuses", "eeprom", "flash"] } diff --git a/firmware/rust/avr-specs/avr-atmega328p.json b/firmware/rust/avr-specs/avr-atmega328p.json new file mode 100644 index 0000000..e236b08 --- /dev/null +++ b/firmware/rust/avr-specs/avr-atmega328p.json @@ -0,0 +1,27 @@ +{ + "arch": "avr", + "atomic-cas": false, + "cpu": "atmega328p", + "data-layout": "e-P1-p:16:8-i8:8-i16:8-i32:8-i64:8-f32:8-f64:8-n8-a:8", + "eh-frame-header": false, + "exe-suffix": ".elf", + "executables": true, + "late-link-args": { + "gcc": [ + "-lgcc" + ] + }, + "linker": "avr-gcc", + "linker-is-gnu": true, + "llvm-target": "avr-unknown-unknown", + "max-atomic-width": 8, + "no-default-libraries": false, + "pre-link-args": { + "gcc": [ + "-mmcu=atmega328p", + "-Wl,--as-needed" + ] + }, + "target-c-int-width": "16", + "target-pointer-width": "16" +} diff --git a/firmware/rust/rust-toolchain.toml b/firmware/rust/rust-toolchain.toml new file mode 100644 index 0000000..6616dd3 --- /dev/null +++ b/firmware/rust/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "nightly-2021-01-07" +components = [ "rust-src" ] +profile = "minimal" diff --git a/firmware/rust/src/assets.rs b/firmware/rust/src/assets.rs new file mode 100644 index 0000000..3874534 --- /dev/null +++ b/firmware/rust/src/assets.rs @@ -0,0 +1,294 @@ +// TODO: Use https://github.com/rust-lang/rust/issues/85077 when stabilized +pub const SACRED_CHAO: [[u8; 40]; 5] = [ + [ + 0x00, 0x00, 0x00, 0x00, 0x80, 0xC0, 0xE0, 0xF0, 0xF0, 0xF8, 0xFC, 0xFC, 0xFE, 0xFE, 0xFE, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFE, 0xFE, 0xFC, 0xFC, + 0xF8, 0xF0, 0xF0, 0xE0, 0xC0, 0x80, 0x00, 0x00, 0x00, 0x00, + ], + [ + 0x80, 0xF0, 0xFC, 0xFE, 0xFF, 0xFF, 0x7F, 0x3F, 0x1F, 0x3F, 0x3F, 0x7F, 0x7F, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x1F, 0x0F, 0x0F, 0x07, 0x07, 0x07, 0x03, 0xC3, + 0xE3, 0x73, 0x37, 0x17, 0x07, 0x0F, 0x1E, 0x3C, 0xF0, 0x80, + ], + [ + 0xFF, 0xFF, 0xFF, 0xE7, 0xC3, 0x81, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0xFC, 0xFE, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFC, 0x30, 0x00, 0x00, 0xFF, + ], + [ + 0x01, 0x0F, 0x3F, 0x4F, 0x9F, 0x3F, 0x3E, 0x3C, 0x7C, 0x7C, 0x7C, 0x7C, 0x3E, 0x3E, 0x3E, + 0x1F, 0x1F, 0x0F, 0x07, 0x01, 0x00, 0x00, 0x00, 0x01, 0x03, 0x07, 0x07, 0x0F, 0x0F, 0x0F, + 0x0F, 0x0F, 0x07, 0x07, 0x03, 0x81, 0x40, 0x30, 0x0E, 0x01, + ], + [ + 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x04, 0x08, 0x08, 0x10, 0x20, 0x20, 0x40, 0x40, 0x40, + 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x40, 0x40, 0x40, 0x20, 0x20, + 0x10, 0x08, 0x08, 0x04, 0x02, 0x01, 0x00, 0x00, 0x00, 0x00, + ], +]; + +// TODO: Use https://github.com/rust-lang/rust/issues/85077 when stabilized +pub const ONDERS_ORG: [[u8; 48]; 2] = [ + [ + 0xE0, 0x60, 0xE0, 0x00, 0x00, 0xE0, 0x60, 0xE0, 0x00, 0x00, 0xE0, 0x60, 0xF8, 0x00, 0x00, + 0xE0, 0xA0, 0xE0, 0x00, 0x00, 0xE0, 0x60, 0xE0, 0x00, 0x00, 0xE0, 0xA0, 0xA0, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x60, 0xE0, 0x00, 0x00, 0xE0, 0x60, 0xE0, 0x00, 0x00, + 0xE0, 0x60, 0xE0, + ], + [ + 0x03, 0x02, 0x03, 0x00, 0x00, 0x03, 0x00, 0x03, 0x00, 0x00, 0x03, 0x02, 0x03, 0x00, 0x00, + 0x03, 0x02, 0x02, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x02, 0x02, 0x03, 0x00, 0x00, + 0x00, 0x02, 0x00, 0x00, 0x00, 0x03, 0x02, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, + 0x0B, 0x0A, 0x0F, + ], +]; + +// TODO: Use https://github.com/rust-lang/rust/issues/85077 when stabilized +pub const ON: [u8; 11] = [ + 0x00, 0x7E, 0x66, 0x7E, 0x00, 0x7E, 0x0C, 0x18, 0x30, 0x7E, 0x00, +]; + +pub const OFF: [u8; 11] = [ + 0x7E, 0x66, 0x7E, 0x00, 0x7E, 0x16, 0x16, 0x00, 0x7E, 0x16, 0x16, +]; + +pub const PLL_A: [u8; 6] = [0x00, 0x7C, 0x12, 0x12, 0x7C, 0x00]; + +pub const PLL_B: [u8; 6] = [0x00, 0x7E, 0x4A, 0x4A, 0x74, 0x00]; + +// TODO: Use https://github.com/rust-lang/rust/issues/85077 when stabilized +const SYM_DOT: [&[u8]; 2] = [&[0x00, 0x00], &[0x30, 0x30]]; + +// TODO: Use https://github.com/rust-lang/rust/issues/85077 when stabilized +const SYM_0: [&[u8]; 2] = [ + &[0xF8, 0xFC, 0x0C, 0xFC, 0xF8], + &[0x1F, 0x3F, 0x30, 0x3F, 0x1F], +]; + +// TODO: Use https://github.com/rust-lang/rust/issues/85077 when stabilized +const SYM_1: [&[u8]; 2] = [ + &[0x30, 0x30, 0xFC, 0xFC, 0x00], + &[0x30, 0x30, 0x3F, 0x3F, 0x30], +]; + +// TODO: Use https://github.com/rust-lang/rust/issues/85077 when stabilized +const SYM_2: [&[u8]; 2] = [ + &[0x18, 0x1C, 0x8C, 0xFC, 0xF8], + &[0x38, 0x3E, 0x3F, 0x33, 0x30], +]; + +// TODO: Use https://github.com/rust-lang/rust/issues/85077 when stabilized +const SYM_3: [&[u8]; 2] = [ + &[0x18, 0x9C, 0x8C, 0xFC, 0x78], + &[0x18, 0x39, 0x31, 0x3F, 0x1E], +]; + +// TODO: Use https://github.com/rust-lang/rust/issues/85077 when stabilized +const SYM_4: [&[u8]; 2] = [ + &[0x80, 0xE0, 0x78, 0xFC, 0xFC], + &[0x07, 0x07, 0x06, 0x3F, 0x3F], +]; + +// TODO: Use https://github.com/rust-lang/rust/issues/85077 when stabilized +const SYM_5: [&[u8]; 2] = [ + &[0xFC, 0xFC, 0x8C, 0x8C, 0x0C], + &[0x1C, 0x3D, 0x31, 0x3F, 0x1F], +]; + +// TODO: Use https://github.com/rust-lang/rust/issues/85077 when stabilized +const SYM_6: [&[u8]; 2] = [ + &[0xF8, 0xFC, 0x8C, 0xBC, 0x38], + &[0x1F, 0x3F, 0x31, 0x3F, 0x1F], +]; + +// TODO: Use https://github.com/rust-lang/rust/issues/85077 when stabilized +const SYM_7: [&[u8]; 2] = [ + &[0x0C, 0x0C, 0xEC, 0xFC, 0x1C], + &[0x00, 0x3E, 0x3F, 0x01, 0x00], +]; + +// TODO: Use https://github.com/rust-lang/rust/issues/85077 when stabilized +const SYM_8: [&[u8]; 2] = [ + &[0x78, 0xFC, 0x8C, 0xFC, 0x78], + &[0x1E, 0x3F, 0x31, 0x3F, 0x1E], +]; + +// TODO: Use https://github.com/rust-lang/rust/issues/85077 when stabilized +const SYM_9: [&[u8]; 2] = [ + &[0xF8, 0xFC, 0x8C, 0xFC, 0xF8], + &[0x1C, 0x3D, 0x31, 0x3F, 0x1F], +]; + +// TODO: Use https://github.com/rust-lang/rust/issues/85077 when stabilized +const SYM_COLON: [&[u8]; 2] = [&[0x30, 0x30], &[0x0C, 0x0C]]; + +// TODO: Use https://github.com/rust-lang/rust/issues/85077 when stabilized +const SYM_A: [&[u8]; 2] = [ + &[0xC0, 0xF0, 0x3C, 0x3C, 0xF0, 0xC0], + &[0x3F, 0x3F, 0x06, 0x06, 0x3F, 0x3F], +]; + +// TODO: Use https://github.com/rust-lang/rust/issues/85077 when stabilized +const SYM_B: [&[u8]; 2] = [ + &[0xFC, 0xFC, 0x8C, 0x8C, 0xFC, 0x78], + &[0x3F, 0x3F, 0x31, 0x31, 0x3F, 0x1E], +]; + +// TODO: Use https://github.com/rust-lang/rust/issues/85077 when stabilized +const SYM_C: [&[u8]; 2] = [ + &[0xF8, 0xFC, 0x0C, 0x1C, 0x18], + &[0x1F, 0x3F, 0x30, 0x38, 0x18], +]; + +// TODO: Use https://github.com/rust-lang/rust/issues/85077 when stabilized +const SYM_E: [&[u8]; 2] = [ + &[0xFC, 0xFC, 0x8C, 0x8C, 0x0C, 0x0C], + &[0x3F, 0x3F, 0x31, 0x31, 0x30, 0x30], +]; + +// TODO: Use https://github.com/rust-lang/rust/issues/85077 when stabilized +const SYM_F: [&[u8]; 2] = [ + &[0xFC, 0xFC, 0x8C, 0x8C, 0x0C, 0x0C], + &[0x3F, 0x3F, 0x01, 0x01, 0x00, 0x00], +]; + +// TODO: Use https://github.com/rust-lang/rust/issues/85077 when stabilized +const SYM_G: [&[u8]; 2] = [ + &[0xF8, 0xFC, 0x0C, 0x0C, 0x3C, 0x38], + &[0x1F, 0x3F, 0x30, 0x33, 0x3F, 0x1F], +]; + +// TODO: Use https://github.com/rust-lang/rust/issues/85077 when stabilized +const SYM_H: [&[u8]; 2] = [ + &[0xFC, 0xFC, 0x80, 0x80, 0xFC, 0xFC], + &[0x3F, 0x3F, 0x01, 0x01, 0x3F, 0x3F], +]; + +// TODO: Use https://github.com/rust-lang/rust/issues/85077 when stabilized +const SYM_I: [&[u8]; 2] = [&[0x0C, 0xFC, 0xFC, 0x0C], &[0x30, 0x3F, 0x3F, 0x30]]; + +// TODO: Use https://github.com/rust-lang/rust/issues/85077 when stabilized +const SYM_K: [&[u8]; 2] = [ + &[0xFC, 0xFC, 0xC0, 0xF0, 0x7C, 0x1C], + &[0x3F, 0x3F, 0x03, 0x0F, 0x3E, 0x38], +]; + +// TODO: Use https://github.com/rust-lang/rust/issues/85077 when stabilized +const SYM_L: [&[u8]; 2] = [ + &[0xFC, 0xFC, 0x00, 0x00, 0x00], + &[0x3F, 0x3F, 0x30, 0x30, 0x30], +]; + +// TODO: Use https://github.com/rust-lang/rust/issues/85077 when stabilized +const SYM_N: [&[u8]; 2] = [ + &[0xFC, 0xFC, 0xF0, 0xC0, 0x00, 0xFC, 0xFC], + &[0x3F, 0x3F, 0x00, 0x03, 0x0F, 0x3F, 0x3F], +]; + +// TODO: Use https://github.com/rust-lang/rust/issues/85077 when stabilized +const SYM_O: [&[u8]; 2] = [ + &[0xF8, 0xFC, 0x0C, 0x0C, 0xFC, 0xF8], + &[0x1F, 0x3F, 0x30, 0x30, 0x3F, 0x1F], +]; + +// TODO: Use https://github.com/rust-lang/rust/issues/85077 when stabilized +const SYM_P: [&[u8]; 2] = [ + &[0xFC, 0xFC, 0x8C, 0x8C, 0xFC, 0xF8], + &[0x3F, 0x3F, 0x01, 0x01, 0x01, 0x00], +]; + +// TODO: Use https://github.com/rust-lang/rust/issues/85077 when stabilized +const SYM_Q: [&[u8]; 2] = [ + &[0xF8, 0xFC, 0x0C, 0x0C, 0xFC, 0xF8], + &[0x1F, 0x3F, 0x30, 0x38, 0x7F, 0x6F], +]; + +// TODO: Use https://github.com/rust-lang/rust/issues/85077 when stabilized +const SYM_R: [&[u8]; 2] = [ + &[0xFC, 0xFC, 0x8C, 0x8C, 0xFC, 0xF8], + &[0x3F, 0x3F, 0x01, 0x03, 0x3F, 0x3E], +]; + +// TODO: Use https://github.com/rust-lang/rust/issues/85077 when stabilized +const SYM_S: [&[u8]; 2] = [ + &[0xF8, 0xFC, 0x8C, 0x8C, 0x9C, 0x18], + &[0x18, 0x39, 0x31, 0x31, 0x3F, 0x1F], +]; + +// TODO: Use https://github.com/rust-lang/rust/issues/85077 when stabilized +const SYM_T: [&[u8]; 2] = [ + &[0x0C, 0x0C, 0xFC, 0xFC, 0x0C, 0x0C], + &[0x00, 0x00, 0x3F, 0x3F, 0x00, 0x00], +]; + +// TODO: Use https://github.com/rust-lang/rust/issues/85077 when stabilized +const SYM_U: [&[u8]; 2] = [ + &[0xFC, 0xFC, 0x00, 0x00, 0xFC, 0xFC], + &[0x1F, 0x3F, 0x30, 0x30, 0x3F, 0x1F], +]; + +// TODO: Use https://github.com/rust-lang/rust/issues/85077 when stabilized +const SYM_UNDERSCORE: [&[u8]; 2] = [ + &[0x00, 0x00, 0x00, 0x00, 0x00], + &[0x30, 0x30, 0x30, 0x30, 0x30], +]; + +// TODO: Use https://github.com/rust-lang/rust/issues/85077 when stabilized +const SYM_INVALID: [&[u8]; 2] = [ + &[0x80, 0xE0, 0x98, 0xCC, 0x4C, 0x18, 0xE0, 0x80], + &[0x01, 0x07, 0x1F, 0x24, 0x25, 0x1F, 0x07, 0x01], +]; + +// TODO: Use https://github.com/rust-lang/rust/issues/85077 when stabilized +pub const SYMBOL_TABLE: [&[&[u8]; 2]; 50] = [ + &SYM_DOT, // '.' + &SYM_INVALID, // '/' + &SYM_0, // '0' + &SYM_1, // '1' + &SYM_2, // '2' + &SYM_3, // '3' + &SYM_4, // '4' + &SYM_5, // '5' + &SYM_6, // '6' + &SYM_7, // '7' + &SYM_8, // '8' + &SYM_9, // '9' + &SYM_COLON, // ':' + &SYM_INVALID, // ';' + &SYM_INVALID, // '<' + &SYM_INVALID, // '=' + &SYM_INVALID, // '>' + &SYM_INVALID, // '?' + &SYM_INVALID, // '@' + &SYM_A, // 'A' + &SYM_B, // 'B' + &SYM_C, // 'C' + &SYM_INVALID, // 'D' + &SYM_E, // 'E' + &SYM_F, // 'F' + &SYM_G, // 'G' + &SYM_H, // 'H' + &SYM_I, // 'I' + &SYM_INVALID, // 'J' + &SYM_K, // 'K' + &SYM_L, // 'L' + &SYM_INVALID, // 'M' + &SYM_N, // 'N' + &SYM_O, // 'O' + &SYM_P, // 'P' + &SYM_Q, // 'Q' + &SYM_R, // 'R' + &SYM_S, // 'S' + &SYM_T, // 'T' + &SYM_U, // 'U' + &SYM_INVALID, // 'V' + &SYM_INVALID, // 'W' + &SYM_INVALID, // 'X' + &SYM_INVALID, // 'Y' + &SYM_INVALID, // 'Z' + &SYM_INVALID, // '[' + &SYM_INVALID, // '\' + &SYM_INVALID, // ']' + &SYM_INVALID, // '^' + &SYM_UNDERSCORE, // '_' +]; diff --git a/firmware/rust/src/eeprom.rs b/firmware/rust/src/eeprom.rs new file mode 100644 index 0000000..dad5600 --- /dev/null +++ b/firmware/rust/src/eeprom.rs @@ -0,0 +1,48 @@ +use atmega_hal::Peripherals; +use avr_device::interrupt; +use core::convert::Infallible; +use nb::Error::WouldBlock; + +pub fn read_byte(p_addr: *const u8) -> nb::Result { + let dp = unsafe { Peripherals::steal() }; + let eeprom = dp.EEPROM; + + // Wait for completion of previous access + if eeprom.eecr.read().eepe().bit_is_set() { + return Err(WouldBlock); + } + + interrupt::free(|_cs| { + // Write address into EEPROM address register + eeprom.eear.write(|w| unsafe { w.bits(p_addr as u16) }); + // Start to read from EEPROM by setting EERE + eeprom.eecr.write(|w| w.eere().set_bit()); + }); + + // Return data from EEPROM data register + Ok(eeprom.eedr.read().bits()) +} + +pub fn write_byte(p_addr: *const u8, data: u8) -> nb::Result<(), Infallible> { + let dp = unsafe { Peripherals::steal() }; + let eeprom = dp.EEPROM; + + // Wait for completion of previous access + if eeprom.eecr.read().eepe().bit_is_set() { + return Err(WouldBlock); + } + + interrupt::free(|_cs| { + // Write address into EEPROM address register + eeprom.eear.write(|w| unsafe { w.bits(p_addr as u16) }); + // Write data into EEPROM data register + eeprom.eedr.write(|w| unsafe { w.bits(data) }); + // Enable writing to the EEPROM by setting EEMPE + eeprom.eecr.write(|w| w.eempe().set_bit()); + // Start to write to EEPROM by setting EEPE + eeprom.eecr.write(|w| w.eepe().set_bit()); + }); + + // Return data from EEPROM data register + Ok(()) +} diff --git a/firmware/rust/src/lcd.rs b/firmware/rust/src/lcd.rs new file mode 100644 index 0000000..4d25a18 --- /dev/null +++ b/firmware/rust/src/lcd.rs @@ -0,0 +1,359 @@ +use atmega_hal::{ + delay::Delay, + port::{mode::Output, Pin, PB0, PB1}, + Spi, +}; +use core::convert::TryInto; +use embedded_hal::{blocking::delay::DelayMs, spi::FullDuplex}; +use nb::block; + +use crate::{assets::SYMBOL_TABLE, eeprom, DefaultClock, CONTRAST}; + +// TODO: Make `cd` and `rst` pins generic pins +pub struct Lcd { + pub spi: Spi, + pub cd: Pin, + rst: Pin, +} + +impl Lcd { + pub fn new(spi: Spi, cd: Pin, rst: Pin) -> Self { + Self { spi, cd, rst } + } + + pub fn init(&mut self) { + let mut delay = Delay::::new(); + + // TODO: Test if delay is really needed + delay.delay_ms(1_u8); + self.rst.set_high(); + // TODO: Try to reduce delay to a minimum + delay.delay_ms(1_u8); + + let init_sequence = [ + 0x40, // (6) Set Scroll Line: Display start line 0 + 0xA1, // (13) Set SEG direction: SEG reverse + 0xC0, // (14) Set COM direction: Normal COM0 - COM63 + 0xA6, // (11) Set Inverse Display: Display inverse off + 0xA2, // (17) Set LCD Bias Ratio: Set Bias 1/9 (Duty 1/65) + 0x2F, // (5) Set Power Control: Booster, Regulator and Follower on + 0x27, // (8) Set VLCD Resistor Ratio: Set Contrast + 0xEE, // (18) Reset Cursor Update Mode + 0x81, // (9) Set Electronic Volume: Set Contrast + nb::block!(eeprom::read_byte(&CONTRAST)).unwrap(), // (9) Set Electronic Volume: Set Contrast + 0xAF, // (12) Set Display Enable: Display on + ]; + + for i in init_sequence.iter() { + block!(self.spi.send(*i)).unwrap(); + } + + // TODO: This delay fixes issues, try find a better solution + delay.delay_ms(1_u8); + } + + pub fn set_contrast(&mut self, contrast: u8) { + assert!(contrast <= 63); + + block!(self.spi.send(0x81)).unwrap(); // (9) Set Electronic Volume: Set Contrast + block!(self.spi.send(contrast)).unwrap(); // (9) Set Electronic Volume: Set Contrast + } + + pub fn move_cursor(&mut self, segment: u8, page: u8) { + assert!(segment < 102); + assert!(page < 8); + + block!(self.spi.send(0x0F & segment)).unwrap(); + block!(self.spi.send(0x10 + (segment >> 4))).unwrap(); + block!(self.spi.send(0xB0 + page)).unwrap(); + } + + fn fill(&mut self, segment: u8, page: u8, width: u8, data: u8) { + assert!(segment + width <= 102); + let mut delay = Delay::::new(); + + self.move_cursor(segment, page); + + // TODO: This delay fixes issues, try find a better solution + delay.delay_ms(1_u8); + self.cd.set_high(); + + for _ in 0..width { + block!(self.spi.send(data)).unwrap(); + } + + // TODO: This delay fixes issues, try find a better solution + delay.delay_ms(1_u8); + self.cd.set_low(); + } + + pub fn fill_area(&mut self, segment: u8, page: u8, width: u8, height: u8, data: u8) { + assert!(page + height <= 8); + + for i in 0..height { + self.fill(segment, page + i, width, data); + } + } + + pub fn print(&mut self, segment: u8, page: u8, string: &str) { + let mut delay = Delay::::new(); + + for i in 0..2 { + self.move_cursor(segment, page + i); + + // TODO: This delay fixes issues, try find a better solution + delay.delay_ms(1_u8); + self.cd.set_high(); + + block!(self.spi.send(0x00)).unwrap(); + block!(self.spi.send(0x00)).unwrap(); + for c in string.chars() { + for segment in SYMBOL_TABLE[c as usize - 46][i as usize] { + block!(self.spi.send(*segment)).unwrap(); + } + block!(self.spi.send(0x00)).unwrap(); + } + block!(self.spi.send(0x00)).unwrap(); + + // TODO: This delay fixes issues, try find a better solution + delay.delay_ms(1_u8); + self.cd.set_low(); + } + } + + pub fn print_inverted(&mut self, segment: u8, page: u8, string: &str) { + let mut delay = Delay::::new(); + + for i in 0..2 { + self.move_cursor(segment, page + i); + + // TODO: This delay fixes issues, try find a better solution + delay.delay_ms(1_u8); + self.cd.set_high(); + + block!(self.spi.send(0xFF)).unwrap(); + block!(self.spi.send(0xFF)).unwrap(); + for c in string.chars() { + for segment in SYMBOL_TABLE[c as usize - 46][i as usize] { + block!(self.spi.send(!*segment)).unwrap(); + } + block!(self.spi.send(0xFF)).unwrap(); + } + block!(self.spi.send(0xFF)).unwrap(); + + // TODO: This delay fixes issues, try find a better solution + delay.delay_ms(1_u8); + self.cd.set_low(); + } + } + + pub fn print_u8(&mut self, segment: u8, page: u8, digits: u8, data: u8) { + assert!(digits <= 3); + let mut delay = Delay::::new(); + + let mut array = [0_usize; 3]; + for (i, item) in array.iter_mut().enumerate() { + *item = (((data / 10_u8.pow(i.try_into().unwrap())) % 10) + 2).into(); + } + array.reverse(); + + for i in 0..2 { + self.move_cursor(segment, page + i); + + // TODO: This delay fixes issues, try find a better solution + delay.delay_ms(1_u8); + self.cd.set_high(); + + block!(self.spi.send(0x00)).unwrap(); + block!(self.spi.send(0x00)).unwrap(); + for j in 3 - digits..3 { + for segment in SYMBOL_TABLE[array[j as usize]][i as usize] { + block!(self.spi.send(*segment)).unwrap(); + } + block!(self.spi.send(0x00)).unwrap(); + } + block!(self.spi.send(0x00)).unwrap(); + + // TODO: This delay fixes issues, try find a better solution + delay.delay_ms(1_u8); + self.cd.set_low(); + } + } + + pub fn print_u8_inverted(&mut self, segment: u8, page: u8, digits: u8, data: u8) { + assert!(digits <= 3); + let mut delay = Delay::::new(); + + let mut array = [0usize; 3]; + for (i, item) in array.iter_mut().enumerate() { + *item = (((data / 10_u8.pow(i.try_into().unwrap())) % 10) + 2).into(); + } + array.reverse(); + + for i in 0..2 { + self.move_cursor(segment, page + i); + + // TODO: This delay fixes issues, try find a better solution + delay.delay_ms(1_u8); + self.cd.set_high(); + + block!(self.spi.send(0xFF)).unwrap(); + block!(self.spi.send(0xFF)).unwrap(); + for j in 3 - digits..3 { + for segment in SYMBOL_TABLE[array[j as usize]][i as usize] { + block!(self.spi.send(!*segment)).unwrap(); + } + block!(self.spi.send(0xFF)).unwrap(); + } + block!(self.spi.send(0xFF)).unwrap(); + + // TODO: This delay fixes issues, try find a better solution + delay.delay_ms(1_u8); + self.cd.set_low(); + } + } + + pub fn print_freq(&mut self, segment: u8, page: u8, data: u32) { + let mut delay = Delay::::new(); + + let mut array = [0usize; 9]; + for (i, item) in array.iter_mut().enumerate() { + *item = (((data / 10_u32.pow(i.try_into().unwrap())) % 10) + 2) + .try_into() + .unwrap(); + } + array.reverse(); + + for i in 0..2 { + self.move_cursor(segment, page + i); + + // TODO: This delay fixes issues, try find a better solution + delay.delay_ms(1_u8); + self.cd.set_high(); + + block!(self.spi.send(0x00)).unwrap(); + block!(self.spi.send(0x00)).unwrap(); + for j in 0..3 { + for segment in SYMBOL_TABLE[array[j as usize]][i as usize] { + block!(self.spi.send(*segment)).unwrap(); + } + block!(self.spi.send(0x00)).unwrap(); + } + for segment in SYMBOL_TABLE[0][i as usize] { + block!(self.spi.send(*segment)).unwrap(); + } + block!(self.spi.send(0x00)).unwrap(); + for j in 3..6 { + for segment in SYMBOL_TABLE[array[j as usize]][i as usize] { + block!(self.spi.send(*segment)).unwrap(); + } + block!(self.spi.send(0x00)).unwrap(); + } + for segment in SYMBOL_TABLE[0][i as usize] { + block!(self.spi.send(*segment)).unwrap(); + } + block!(self.spi.send(0x00)).unwrap(); + for j in 6..9 { + for segment in SYMBOL_TABLE[array[j as usize]][i as usize] { + block!(self.spi.send(*segment)).unwrap(); + } + block!(self.spi.send(0x00)).unwrap(); + } + block!(self.spi.send(0x00)).unwrap(); + + // TODO: This delay fixes issues, try find a better solution + delay.delay_ms(1_u8); + self.cd.set_low(); + } + } + + pub fn print_freq_digit(&mut self, segment: u8, page: u8, data: u32, digit: u8) { + let mut delay = Delay::::new(); + + let mut array = [0usize; 9]; + for (i, item) in array.iter_mut().enumerate() { + *item = (((data / 10_u32.pow(i.try_into().unwrap())) % 10) + 2) + .try_into() + .unwrap(); + } + array.reverse(); + let digit = 8 - digit; + + for i in 0..2 { + self.move_cursor(segment, page + i); + + // TODO: This delay fixes issues, try find a better solution + delay.delay_ms(1_u8); + self.cd.set_high(); + + block!(self.spi.send(0x00)).unwrap(); + block!(self.spi.send(0x00)).unwrap(); + for j in 0..3 { + if j == digit { + for segment in SYMBOL_TABLE[array[j as usize]][i as usize] { + block!(self.spi.send(!*segment)).unwrap(); + } + } else { + for segment in SYMBOL_TABLE[array[j as usize]][i as usize] { + block!(self.spi.send(*segment)).unwrap(); + } + } + block!(self.spi.send(0x00)).unwrap(); + } + for segment in SYMBOL_TABLE[0][i as usize] { + block!(self.spi.send(*segment)).unwrap(); + } + block!(self.spi.send(0x00)).unwrap(); + for j in 3..6 { + if j == digit { + for segment in SYMBOL_TABLE[array[j as usize]][i as usize] { + block!(self.spi.send(!*segment)).unwrap(); + } + } else { + for segment in SYMBOL_TABLE[array[j as usize]][i as usize] { + block!(self.spi.send(*segment)).unwrap(); + } + } + block!(self.spi.send(0x00)).unwrap(); + } + for segment in SYMBOL_TABLE[0][i as usize] { + block!(self.spi.send(*segment)).unwrap(); + } + block!(self.spi.send(0x00)).unwrap(); + for j in 6..9 { + if j == digit { + for segment in SYMBOL_TABLE[array[j as usize]][i as usize] { + block!(self.spi.send(!*segment)).unwrap(); + } + } else { + for segment in SYMBOL_TABLE[array[j as usize]][i as usize] { + block!(self.spi.send(*segment)).unwrap(); + } + } + block!(self.spi.send(0x00)).unwrap(); + } + block!(self.spi.send(0x00)).unwrap(); + + // TODO: This delay fixes issues, try find a better solution + delay.delay_ms(1_u8); + self.cd.set_low(); + } + } + + pub fn print_icon(&mut self, segment: u8, page: u8, symbol: &[u8]) { + let mut delay = Delay::::new(); + self.move_cursor(segment, page); + + // TODO: This delay fixes issues, try find a better solution + delay.delay_ms(1_u8); + self.cd.set_high(); + + for c in symbol { + block!(self.spi.send(*c)).unwrap(); + } + + // TODO: This delay fixes issues, try find a better solution + delay.delay_ms(1_u8); + self.cd.set_low(); + } +} diff --git a/firmware/rust/src/main.rs b/firmware/rust/src/main.rs new file mode 100644 index 0000000..18fe0d4 --- /dev/null +++ b/firmware/rust/src/main.rs @@ -0,0 +1,207 @@ +#![feature(abi_avr_interrupt)] +#![no_std] +#![no_main] + +use atmega_hal::{ + clock::MHz8, + delay::Delay, + pac::{EXINT, TC1}, + pins, + port::{self, mode::PullUp, Pin, PB6, PB7, PC0}, + spi::{DataOrder, SerialClockRate, Settings, Spi}, + Peripherals, +}; +use avr_device::interrupt; +use core::sync::atomic::{AtomicBool, Ordering}; +use embedded_hal::{ + blocking::delay::DelayMs, + spi::{Mode, Phase, Polarity}, +}; +use panic_halt as _; + +mod assets; +mod eeprom; +mod lcd; +mod rotary; +mod screen; + +use rotary::{Direction, Rotary}; +use screen::{Screen, Screens}; + +pub type DefaultClock = MHz8; +pub type I2c = atmega_hal::i2c::I2c; + +#[used] +#[link_section = ".eeprom"] +static BACKLIGHT: u8 = 0; + +#[used] +#[link_section = ".eeprom"] +static CONTRAST: u8 = 8; + +pub enum Input { + Next, + Previous, + Select, + Back, +} + +struct ClockGenerator { + screen: Screen, + tc1: TC1, + exint: EXINT, + encoder: Rotary, PB6>, Pin, PB7>>, + button: Pin, PC0>, + delay: Delay, +} + +impl ClockGenerator { + fn new() -> Self { + let dp = Peripherals::take().unwrap(); + let pins = pins!(dp); + + // Init SPI + let (spi, _) = Spi::new( + dp.SPI, + pins.pb5.into_output(), + pins.pb3.into_output(), + pins.pb4.into_pull_up_input(), + pins.pb2.into_output(), + Settings { + data_order: DataOrder::MostSignificantFirst, + clock: SerialClockRate::OscfOver2, + mode: Mode { + polarity: Polarity::IdleLow, + phase: Phase::CaptureOnFirstTransition, + }, + }, + ); + + Self { + screen: Screen::new( + dp.TC0, + spi, + pins.pd5.into_output(), + pins.pb0.into_output(), + pins.pb1.into_output(), + I2c::new( + dp.TWI, + pins.pc4.into_pull_up_input(), + pins.pc5.into_pull_up_input(), + 50000, + ), + ), + tc1: dp.TC1, + exint: dp.EXINT, + delay: Delay::::new(), + encoder: Rotary::new(pins.pb6.into_pull_up_input(), pins.pb7.into_pull_up_input()), + button: pins.pc0.into_pull_up_input(), + } + } + + fn init(&mut self) { + // Init Timer/Counter1 + self.tc1.ocr1a.write(|w| unsafe { w.bits(65535) }); + + // Enable interrupts for encoder, button and timer + self.exint.pcicr.write(|w| w.pcie().bits(0b0000_0011)); + self.exint.pcmsk0.write(|w| w.pcint().bits(0b1100_0000)); + self.exint.pcmsk1.write(|w| w.pcint().bits(0b0000_0001)); + self.tc1.timsk1.write(|w| w.ocie1a().set_bit()); + + // Init screen + self.screen.init(); + + // Show splash screen for a moment + self.delay.delay_ms(2000_u16); + + // Set home screen + self.screen.change(Screens::Home(screen::Home::new())); + + // Enable interrupts globally + unsafe { interrupt::enable() }; + } + + fn update(&mut self) { + if UPDATE_ENCODER.load(Ordering::SeqCst) { + interrupt::free(|_cs| { + self.delay.delay_ms(3_u8); + + match self.encoder.update() { + Direction::Clockwise => { + self.screen.input(&Input::Next); + } + Direction::CounterClockwise => { + self.screen.input(&Input::Previous); + } + Direction::None => {} + } + UPDATE_ENCODER.store(false, Ordering::SeqCst); + }) + } + + if UPDATE_TIMER.load(Ordering::SeqCst) { + self.tc1.tccr1b.write(|w| w.cs1().no_clock()); + self.screen.input(&Input::Back); + UPDATE_TIMER.store(false, Ordering::SeqCst); + } + + if UPDATE_BUTTON.load(Ordering::SeqCst) { + self.delay.delay_ms(2_u8); + + if self.button.is_low() { + // Press + if self.tc1.tccr1b.read().cs1().is_no_clock() { + self.tc1.tcnt1.write(|w| unsafe { w.bits(0) }); + self.tc1.tccr1b.write(|w| w.cs1().prescale_64()); + } + } else { + // Release + if self.tc1.tccr1b.read().cs1().is_prescale_64() { + self.tc1.tccr1b.write(|w| w.cs1().no_clock()); + self.screen.input(&Input::Select); + self.delay.delay_ms(3_u8); + } + } + UPDATE_BUTTON.store(false, Ordering::SeqCst); + } + } +} + +static UPDATE_ENCODER: AtomicBool = AtomicBool::new(false); +static UPDATE_BUTTON: AtomicBool = AtomicBool::new(false); +static UPDATE_TIMER: AtomicBool = AtomicBool::new(false); + +#[interrupt(atmega328p)] +#[allow(non_snake_case)] +fn PCINT0() { + interrupt::free(|_cs| { + UPDATE_ENCODER.store(true, Ordering::SeqCst); + }) +} + +#[interrupt(atmega328p)] +#[allow(non_snake_case)] +fn PCINT1() { + interrupt::free(|_cs| { + UPDATE_BUTTON.store(true, Ordering::SeqCst); + }) +} + +#[interrupt(atmega328p)] +#[allow(non_snake_case)] +fn TIMER1_COMPA() { + interrupt::free(|_cs| { + UPDATE_TIMER.store(true, Ordering::SeqCst); + }) +} + +#[atmega_hal::entry] +fn main() -> ! { + let mut cg = ClockGenerator::new(); + cg.init(); + + loop { + cg.update(); + } +} diff --git a/firmware/rust/src/rotary.rs b/firmware/rust/src/rotary.rs new file mode 100644 index 0000000..21919dc --- /dev/null +++ b/firmware/rust/src/rotary.rs @@ -0,0 +1,51 @@ +use embedded_hal::digital::v2::InputPin; + +pub struct Rotary { + pin_a: A, + pin_b: B, + state: u8, +} + +pub enum Direction { + Clockwise, + CounterClockwise, + None, +} + +impl Rotary +where + A: InputPin, + B: InputPin, +{ + pub fn new(pin_a: A, pin_b: B) -> Self { + Self { + pin_a, + pin_b, + state: 0, + } + } + + pub fn update(&mut self) -> Direction { + let a = self.pin_a.is_low(); + let b = self.pin_b.is_low(); + + let new_state = match (a, b) { + (Ok(true), Ok(true)) => 0b00, + (Ok(true), Ok(false)) => 0b01, + (Ok(false), Ok(true)) => 0b10, + (Ok(false), Ok(false)) => 0b11, + _ => return Direction::None, + }; + + let direction = match (self.state, new_state) { + (0b10, 0b11) => Direction::Clockwise, + (0b01, 0b00) => Direction::Clockwise, + (0b01, 0b11) => Direction::CounterClockwise, + (0b10, 0b00) => Direction::CounterClockwise, + _ => Direction::None, + }; + + self.state = new_state; + direction + } +} diff --git a/firmware/rust/src/screen/channel.rs b/firmware/rust/src/screen/channel.rs new file mode 100644 index 0000000..0ee5dc3 --- /dev/null +++ b/firmware/rust/src/screen/channel.rs @@ -0,0 +1,191 @@ +use super::{ClockChannel, Event, Home, Screens}; +use crate::{lcd::Lcd, Input}; +use si5351::{ClockOutput, PLL}; + +enum Selection { + Frequency, + FrequencyDigit(u8), + Digit(u8), + Enabled, + Pll, + Back, +} + +pub struct Channel { + active: Selection, + channel: ClockChannel, +} + +impl Channel { + pub fn new(channel: ClockChannel) -> Self { + Self { + active: Selection::Frequency, + channel, + } + } + + pub fn input(&mut self, input: &Input) -> Event { + self.active = match self.active { + Selection::Frequency => match input { + Input::Next => Selection::Enabled, + Input::Previous => Selection::Back, + Input::Select => Selection::FrequencyDigit(8), + Input::Back => return Event::Screen(Screens::Home(Home::new())), + }, + Selection::FrequencyDigit(digit) => match input { + Input::Next => { + if digit == u8::MIN { + Selection::FrequencyDigit(8) + } else { + Selection::FrequencyDigit(digit - 1) + } + } + Input::Previous => { + if digit >= 8 { + Selection::FrequencyDigit(u8::MIN) + } else { + Selection::FrequencyDigit(digit + 1) + } + } + Input::Select => Selection::Digit(digit), + Input::Back => Selection::Frequency, + }, + Selection::Digit(digit) => match input { + Input::Next => { + let new_freq = self.channel.freq + 10_u32.pow(digit.into()); + if new_freq < 162_000_000 { + self.channel.freq = new_freq; + } else { + self.channel.freq = 162_000_000 + } + return Event::Channel(self.channel); + } + Input::Previous => { + let difference = 10_u32.pow(digit.into()); + if self.channel.freq > difference { + let new_freq = self.channel.freq - difference; + if new_freq > 15_000 { + self.channel.freq = new_freq; + } else { + self.channel.freq = 15_000; + } + } else { + self.channel.freq = 15_000; + } + return Event::Channel(self.channel); + } + Input::Select => Selection::Frequency, + Input::Back => Selection::FrequencyDigit(digit), + }, + Selection::Enabled => match input { + Input::Next => Selection::Pll, + Input::Previous => Selection::Frequency, + Input::Select => { + self.channel.enabled = !self.channel.enabled; + return Event::Channel(self.channel); + } + Input::Back => return Event::Screen(Screens::Home(Home::new())), + }, + Selection::Pll => match input { + Input::Next => Selection::Back, + Input::Previous => Selection::Enabled, + Input::Select => { + self.channel.pll = match self.channel.pll { + PLL::A => PLL::B, + PLL::B => PLL::A, + }; + return Event::Channel(self.channel); + } + Input::Back => return Event::Screen(Screens::Home(Home::new())), + }, + Selection::Back => match input { + Input::Next => Selection::Frequency, + Input::Previous => Selection::Pll, + _ => return Event::Screen(Screens::Home(Home::new())), + }, + }; + + Event::None + } + + fn draw_enabled(&self, lcd: &mut Lcd, inverted: bool) { + if inverted { + match self.channel.enabled { + false => lcd.print_inverted(13, 4, "OFF"), + true => lcd.print_inverted(16, 4, "ON"), + } + } else { + match self.channel.enabled { + false => lcd.print(13, 4, "OFF"), + true => lcd.print(16, 4, "ON"), + } + } + } + + pub fn draw(&self, lcd: &mut Lcd) { + lcd.fill_area(0, 0, 19, 2, 0xFF); + match self.channel.output { + ClockOutput::Clk0 => lcd.print_inverted(19, 0, "CHANNEL_1"), + ClockOutput::Clk1 => lcd.print_inverted(19, 0, "CHANNEL_2"), + ClockOutput::Clk2 => lcd.print_inverted(19, 0, "CHANNEL_3"), + _ => unimplemented!(), + } + lcd.fill_area(83, 0, 19, 2, 0xFF); + + match &self.active { + Selection::Frequency => { + lcd.print_inverted(0, 2, "FREQ:"); + lcd.print_freq(39, 2, self.channel.freq); + self.draw_enabled(lcd, false); + match self.channel.pll { + PLL::A => lcd.print(59, 4, "PLL_A"), + PLL::B => lcd.print(59, 4, "PLL_B"), + } + lcd.print(36, 6, "BACK"); + } + Selection::FrequencyDigit(digit) | Selection::Digit(digit) => { + lcd.print(0, 2, "FREQ:"); + lcd.print_freq_digit(39, 2, self.channel.freq, *digit); + self.draw_enabled(lcd, false); + match self.channel.pll { + PLL::A => lcd.print(59, 4, "PLL_A"), + PLL::B => lcd.print(59, 4, "PLL_B"), + } + lcd.print(36, 6, "BACK"); + } + Selection::Enabled => { + lcd.print(0, 2, "FREQ:"); + lcd.print_freq(39, 2, self.channel.freq); + self.draw_enabled(lcd, true); + match self.channel.pll { + PLL::A => lcd.print(59, 4, "PLL_A"), + PLL::B => lcd.print(59, 4, "PLL_B"), + } + lcd.print(36, 6, "BACK"); + } + Selection::Pll => { + lcd.print(0, 2, "FREQ:"); + lcd.print_freq(39, 2, self.channel.freq); + self.draw_enabled(lcd, false); + match self.channel.pll { + PLL::A => lcd.print_inverted(59, 4, "PLL_A"), + PLL::B => lcd.print_inverted(59, 4, "PLL_B"), + } + lcd.print(36, 6, "BACK"); + } + Selection::Back => { + lcd.print(0, 2, "FREQ:"); + lcd.print_freq(39, 2, self.channel.freq); + match self.channel.enabled { + false => lcd.print(13, 4, "OFF"), + true => lcd.print(16, 4, "ON"), + } + match self.channel.pll { + PLL::A => lcd.print(59, 4, "PLL_A"), + PLL::B => lcd.print(59, 4, "PLL_B"), + } + lcd.print_inverted(36, 6, "BACK"); + } + } + } +} diff --git a/firmware/rust/src/screen/home.rs b/firmware/rust/src/screen/home.rs new file mode 100644 index 0000000..68c064d --- /dev/null +++ b/firmware/rust/src/screen/home.rs @@ -0,0 +1,93 @@ +use super::{Channel, Event, Screens, Setup}; +use crate::{lcd::Lcd, screen::ClockChannel, Input}; + +enum Selection { + Ch1, + Ch2, + Ch3, + Setup, +} + +pub struct Home { + active: Selection, +} + +impl Home { + pub fn new() -> Self { + Self { + active: Selection::Ch1, + } + } + + pub fn input(&mut self, input: &Input, channels: [ClockChannel; 3]) -> Event { + self.active = match self.active { + Selection::Ch1 => match input { + Input::Next => Selection::Ch2, + Input::Previous => Selection::Setup, + Input::Select => return Event::Screen(Screens::Channel(Channel::new(channels[0]))), + Input::Back => Selection::Ch1, + }, + Selection::Ch2 => match input { + Input::Next => Selection::Ch3, + Input::Previous => Selection::Ch1, + Input::Select => return Event::Screen(Screens::Channel(Channel::new(channels[1]))), + Input::Back => Selection::Ch2, + }, + Selection::Ch3 => match input { + Input::Next => Selection::Setup, + Input::Previous => Selection::Ch2, + Input::Select => return Event::Screen(Screens::Channel(Channel::new(channels[2]))), + Input::Back => Selection::Ch3, + }, + Selection::Setup => match input { + Input::Next => Selection::Ch1, + Input::Previous => Selection::Ch3, + Input::Select => return Event::Screen(Screens::Setup(Setup::new())), + Input::Back => Selection::Setup, + }, + }; + + Event::None + } + + pub fn draw(&self, lcd: &mut Lcd, channels: [ClockChannel; 3]) { + match &self.active { + Selection::Ch1 => { + lcd.print_inverted(0, 0, "CH1"); + channels[0].print(lcd, 0); + lcd.print(0, 2, "CH2"); + channels[1].print(lcd, 2); + lcd.print(0, 4, "CH3"); + channels[2].print(lcd, 4); + lcd.print(33, 6, "SETUP"); + } + Selection::Ch2 => { + lcd.print(0, 0, "CH1"); + channels[0].print(lcd, 0); + lcd.print_inverted(0, 2, "CH2"); + channels[1].print(lcd, 2); + lcd.print(0, 4, "CH3"); + channels[2].print(lcd, 4); + lcd.print(33, 6, "SETUP"); + } + Selection::Ch3 => { + lcd.print(0, 0, "CH1"); + channels[0].print(lcd, 0); + lcd.print(0, 2, "CH2"); + channels[1].print(lcd, 2); + lcd.print_inverted(0, 4, "CH3"); + channels[2].print(lcd, 4); + lcd.print(33, 6, "SETUP"); + } + Selection::Setup => { + lcd.print(0, 0, "CH1"); + channels[0].print(lcd, 0); + lcd.print(0, 2, "CH2"); + channels[1].print(lcd, 2); + lcd.print(0, 4, "CH3"); + channels[2].print(lcd, 4); + lcd.print_inverted(33, 6, "SETUP"); + } + } + } +} diff --git a/firmware/rust/src/screen/mod.rs b/firmware/rust/src/screen/mod.rs new file mode 100644 index 0000000..622f761 --- /dev/null +++ b/firmware/rust/src/screen/mod.rs @@ -0,0 +1,206 @@ +use atmega_hal::{ + pac::TC0, + port::{mode::Output, Pin, PB0, PB1, PD5}, + Spi, +}; +use si5351::{ClockOutput, Si5351, Si5351Device, PLL}; + +mod channel; +mod home; +mod setup; +mod splash; + +use crate::{ + assets::{OFF, ON, PLL_A, PLL_B}, + eeprom, + lcd::Lcd, + I2c, Input, BACKLIGHT, +}; +pub use channel::Channel; +pub use home::Home; +pub use setup::Setup; +pub use splash::Splash; + +// TODO: Only update changes instead of whole screen + +pub enum Event { + Screen(Screens), + Backlight(u8), + Contrast(u8), + Channel(ClockChannel), + None, +} + +#[derive(Clone, Copy)] +pub struct ClockChannel { + output: ClockOutput, + freq: u32, + enabled: bool, + pll: PLL, +} + +impl ClockChannel { + fn print(&self, lcd: &mut Lcd, page: u8) { + lcd.print_freq(25, page, self.freq); + lcd.print_icon(91, page, if self.enabled { &ON } else { &OFF }); + lcd.print_icon( + 94, + page + 1, + match self.pll { + PLL::A => &PLL_A, + PLL::B => &PLL_B, + }, + ); + } + + fn update(&self, si5351: &mut Si5351Device) { + si5351 + .set_frequency(self.pll, self.output, self.freq) + .unwrap(); + si5351.set_clock_enabled(self.output, self.enabled); + si5351.flush_output_enabled().unwrap(); + } +} + +pub enum Screens { + Splash(Splash), + Home(Home), + Setup(Setup), + Channel(Channel), +} + +impl Screens { + pub fn input(&mut self, input: &Input, channels: [ClockChannel; 3]) -> Event { + match self { + Screens::Splash(_) => Event::None, + Screens::Home(home) => home.input(input, channels), + Screens::Setup(setup) => setup.input(input), + Screens::Channel(channel) => channel.input(input), + } + } +} + +pub struct Screen { + lcd: Lcd, + tc0: TC0, + pwm: Pin, + screen: Screens, + si5351: Si5351Device, + channels: [ClockChannel; 3], +} + +impl Screen { + pub fn new( + tc0: TC0, + spi: Spi, + pwm: Pin, + cd: Pin, + rst: Pin, + i2c: I2c, + ) -> Self { + Self { + lcd: Lcd::new(spi, cd, rst), + tc0, + pwm, + screen: Screens::Splash(Splash), + si5351: Si5351Device::new_adafruit_module(i2c), + channels: [ + ClockChannel { + output: ClockOutput::Clk0, + freq: 1_000_000, + enabled: false, + pll: PLL::A, + }, + ClockChannel { + output: ClockOutput::Clk1, + freq: 1_000_000, + enabled: false, + pll: PLL::A, + }, + ClockChannel { + output: ClockOutput::Clk2, + freq: 1_000_000, + enabled: false, + pll: PLL::A, + }, + ], + } + } + + pub fn init(&mut self) { + // Init display backlight + self.tc0.ocr0a.write(|w| unsafe { w.bits(255) }); + self.tc0.tccr0a.write(|w| { + w.wgm0().pwm_fast(); + w.com0b().match_clear() + }); + self.set_backlight(nb::block!(eeprom::read_byte(&BACKLIGHT)).unwrap()); + + // Init lcd display + self.lcd.init(); + self.draw(); + + // Init Si5351 + self.si5351.init_adafruit_module().unwrap(); + } + + fn set_backlight(&mut self, backlight: u8) { + match backlight { + 0 => { + self.tc0.tccr0b.write(|w| w.cs0().no_clock()); + self.pwm.set_low(); + } + 1..=5 => { + self.tc0.tccr0b.write(|w| { + w.wgm02().set_bit(); + w.cs0().prescale_256() + }); + self.tc0.ocr0b.write(|w| unsafe { w.bits(backlight - 1) }); + } + _ => { + self.tc0.tccr0b.write(|w| { + w.wgm02().set_bit(); + w.cs0().prescale_64() + }); + self.tc0.ocr0b.write(|w| unsafe { w.bits(backlight - 6) }); + } + } + } + + pub fn draw(&mut self) { + self.lcd.fill_area(0, 0, 102, 8, 0x00); + + match &self.screen { + Screens::Splash(splash) => splash.draw(&mut self.lcd), + Screens::Home(home) => home.draw(&mut self.lcd, self.channels), + Screens::Setup(setup) => setup.draw(&mut self.lcd), + Screens::Channel(channel) => channel.draw(&mut self.lcd), + } + } + + pub fn input(&mut self, input: &Input) { + match self.screen.input(input, self.channels) { + Event::Screen(screen) => self.screen = screen, + Event::Backlight(backlight) => self.set_backlight(backlight), + Event::Contrast(contrast) => self.lcd.set_contrast(contrast), + Event::Channel(channel) => { + match channel.output { + ClockOutput::Clk0 => self.channels[0] = channel, + ClockOutput::Clk1 => self.channels[1] = channel, + ClockOutput::Clk2 => self.channels[2] = channel, + _ => unimplemented!(), + } + + channel.update(&mut self.si5351); + } + Event::None => {} + } + + self.draw(); + } + + pub fn change(&mut self, screen: Screens) { + self.screen = screen; + self.draw(); + } +} diff --git a/firmware/rust/src/screen/setup.rs b/firmware/rust/src/screen/setup.rs new file mode 100644 index 0000000..a06ee70 --- /dev/null +++ b/firmware/rust/src/screen/setup.rs @@ -0,0 +1,152 @@ +use super::{Event, Home, Screens}; +use crate::{eeprom, lcd::Lcd, Input, BACKLIGHT, CONTRAST}; + +enum Selection { + Backlight, + BacklightEdit, + Contrast, + ContrastEdit, + Back, +} + +pub struct Setup { + active: Selection, + backlight: u8, + contrast: u8, +} + +impl Setup { + pub fn new() -> Self { + Self { + active: Selection::Backlight, + backlight: nb::block!(eeprom::read_byte(&BACKLIGHT)).unwrap(), + contrast: nb::block!(eeprom::read_byte(&CONTRAST)).unwrap(), + } + } + + pub fn input(&mut self, input: &Input) -> Event { + self.active = match self.active { + Selection::Backlight => match input { + Input::Next => Selection::Contrast, + Input::Previous => Selection::Back, + Input::Select => Selection::BacklightEdit, + Input::Back => return Event::Screen(Screens::Home(Home::new())), + }, + Selection::BacklightEdit => match input { + Input::Next => { + self.backlight = if self.backlight == u8::MAX { + u8::MIN + } else { + self.backlight + 1 + }; + + return Event::Backlight(self.backlight); + } + Input::Previous => { + self.backlight = if self.backlight == u8::MIN { + u8::MAX + } else { + self.backlight - 1 + }; + + return Event::Backlight(self.backlight); + } + Input::Select => { + nb::block!(eeprom::write_byte(&BACKLIGHT, self.backlight)).unwrap(); + Selection::Backlight + } + Input::Back => { + self.active = Selection::Backlight; + self.backlight = nb::block!(eeprom::read_byte(&BACKLIGHT)).unwrap(); + return Event::Backlight(self.backlight); + } + }, + Selection::Contrast => match input { + Input::Next => Selection::Back, + Input::Previous => Selection::Backlight, + Input::Select => Selection::ContrastEdit, + Input::Back => return Event::Screen(Screens::Home(Home::new())), + }, + Selection::ContrastEdit => match input { + Input::Next => { + self.contrast = if self.contrast >= 63 { + u8::MIN + } else { + self.contrast + 1 + }; + + return Event::Contrast(self.contrast); + } + Input::Previous => { + self.contrast = if self.contrast == u8::MIN { + 63 + } else { + self.contrast - 1 + }; + + return Event::Contrast(self.contrast); + } + Input::Select => { + nb::block!(eeprom::write_byte(&CONTRAST, self.contrast)).unwrap(); + Selection::Contrast + } + Input::Back => { + self.active = Selection::Contrast; + self.contrast = nb::block!(eeprom::read_byte(&CONTRAST)).unwrap(); + return Event::Contrast(self.contrast); + } + }, + Selection::Back => match input { + Input::Next => Selection::Backlight, + Input::Previous => Selection::Contrast, + _ => return Event::Screen(Screens::Home(Home::new())), + }, + }; + + Event::None + } + + pub fn draw(&self, lcd: &mut Lcd) { + lcd.fill_area(0, 0, 33, 2, 0xFF); + lcd.print_inverted(33, 0, "SETUP"); + lcd.fill_area(69, 0, 33, 2, 0xFF); + + match &self.active { + Selection::Contrast => { + lcd.print(0, 2, "BACKLIGHT:"); + lcd.print_u8(81, 2, 3, self.backlight); + lcd.print_inverted(0, 4, "CONTRAST:"); + lcd.print_u8(87, 4, 2, self.contrast); + lcd.print(36, 6, "BACK"); + } + Selection::ContrastEdit => { + lcd.print(0, 2, "BACKLIGHT:"); + lcd.print_u8(81, 2, 3, self.backlight); + lcd.print(0, 4, "CONTRAST:"); + lcd.print_u8_inverted(87, 4, 2, self.contrast); + lcd.print(36, 6, "BACK"); + } + Selection::Backlight => { + lcd.print_inverted(0, 2, "BACKLIGHT:"); + lcd.print_u8(81, 2, 3, self.backlight); + lcd.print(0, 4, "CONTRAST:"); + lcd.print_u8(87, 4, 2, self.contrast); + lcd.print(36, 6, "BACK"); + } + Selection::BacklightEdit => { + lcd.print(0, 2, "BACKLIGHT:"); + lcd.print_u8_inverted(81, 2, 3, self.backlight); + lcd.print(0, 4, "CONTRAST:"); + lcd.print_u8(87, 4, 2, self.contrast); + lcd.print(36, 6, "BACK"); + } + Selection::Back => { + lcd.print(0, 2, "BACKLIGHT:"); + lcd.print_u8(81, 2, 3, self.backlight); + lcd.print(0, 4, "CONTRAST:"); + lcd.print_u8(87, 4, 2, self.contrast); + lcd.print_inverted(36, 6, "BACK"); + } + } + } +} diff --git a/firmware/rust/src/screen/splash.rs b/firmware/rust/src/screen/splash.rs new file mode 100644 index 0000000..1f95039 --- /dev/null +++ b/firmware/rust/src/screen/splash.rs @@ -0,0 +1,48 @@ +use crate::{ + assets::{ONDERS_ORG, SACRED_CHAO}, + lcd::Lcd, + DefaultClock, +}; +use atmega_hal::delay::Delay; +use embedded_hal::{blocking::delay::DelayMs, spi::FullDuplex}; +use nb::block; + +pub struct Splash; + +impl Splash { + pub fn draw(&self, lcd: &mut Lcd) { + let mut delay = Delay::::new(); + + for (i, page) in SACRED_CHAO.iter().enumerate() { + lcd.move_cursor(31, 1 + i as u8); + + // TODO: This delay fixes issues, try find a better solution + delay.delay_ms(1_u8); + lcd.cd.set_high(); + + for segment in page { + block!(lcd.spi.send(*segment)).unwrap(); + } + + // TODO: This delay fixes issues, try find a better solution + delay.delay_ms(1_u8); + lcd.cd.set_low(); + } + + for (i, page) in ONDERS_ORG.iter().enumerate() { + lcd.move_cursor(27, 6 + i as u8); + + // TODO: This delay fixes issues, try find a better solution + delay.delay_ms(1_u8); + lcd.cd.set_high(); + + for segment in page { + block!(lcd.spi.send(*segment)).unwrap(); + } + + // TODO: This delay fixes issues, try find a better solution + delay.delay_ms(1_u8); + lcd.cd.set_low(); + } + } +} diff --git a/knob/knob.scad b/knob/knob.scad new file mode 100644 index 0000000..943fac8 --- /dev/null +++ b/knob/knob.scad @@ -0,0 +1,21 @@ +$fn = 200; + +module shaft() { + difference() { + cylinder(h = 5, d = 6); + translate([2, -2.5, 0]) + cube(5); + } + + translate([0, 0, -5]) + cylinder(h = 5, d = 8); +} + +difference() { + cylinder(h = 14, d1 = 18, d2 = 16); + translate([0, 0, 5]) + shaft(); +} + +translate([0, 0, 14]) + cylinder(h = 2, d1 = 16, d2 = 12);