From acaf4f0a16c2220c323dc6045a644716d467d6ac Mon Sep 17 00:00:00 2001 From: Bruno de Carvalho Date: Thu, 5 Oct 2023 20:24:36 -0700 Subject: [PATCH] Initial commit --- .devcontainer/Dockerfile.dev | 6 + .devcontainer/devcontainer.json | 24 + .devcontainer/postStartCommand.sh | 3 + .editorconfig | 17 + .github/workflows/rust.yml | 22 + .gitignore | 7 + .vscode/extensions.json | 14 + CHANGELOG.md | 3 + CODE_OF_CONDUCT.md | 133 ++++++ CONTRIBUTING.md | 37 ++ Cargo.lock | 222 +++++++++ Cargo.toml | 16 + LICENSE | 21 + README.md | 255 ++++++++++ bin/.rustup-1.25.2.pkg | 1 + bin/README.hermit.md | 7 + bin/activate-hermit | 21 + bin/cargo | 1 + bin/cargo-clippy | 1 + bin/cargo-fmt | 1 + bin/cargo-miri | 1 + bin/clippy-driver | 1 + bin/hermit | 43 ++ bin/hermit.hcl | 0 bin/rls | 1 + bin/rust-analyzer | 1 + bin/rust-gdb | 1 + bin/rust-gdbgui | 1 + bin/rust-lldb | 1 + bin/rustc | 1 + bin/rustdoc | 1 + bin/rustfmt | 1 + bin/rustup | 1 + rustfmt.toml | 4 + src/debug.rs | 159 +++++++ src/lib.rs | 746 ++++++++++++++++++++++++++++++ src/mask.rs | 165 +++++++ src/program.rs | 373 +++++++++++++++ src/register.rs | 138 ++++++ src/types.rs | 104 +++++ 40 files changed, 2555 insertions(+) create mode 100644 .devcontainer/Dockerfile.dev create mode 100644 .devcontainer/devcontainer.json create mode 100755 .devcontainer/postStartCommand.sh create mode 100644 .editorconfig create mode 100644 .github/workflows/rust.yml create mode 100644 .gitignore create mode 100644 .vscode/extensions.json create mode 100644 CHANGELOG.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 120000 bin/.rustup-1.25.2.pkg create mode 100644 bin/README.hermit.md create mode 100755 bin/activate-hermit create mode 120000 bin/cargo create mode 120000 bin/cargo-clippy create mode 120000 bin/cargo-fmt create mode 120000 bin/cargo-miri create mode 120000 bin/clippy-driver create mode 100755 bin/hermit create mode 100644 bin/hermit.hcl create mode 120000 bin/rls create mode 120000 bin/rust-analyzer create mode 120000 bin/rust-gdb create mode 120000 bin/rust-gdbgui create mode 120000 bin/rust-lldb create mode 120000 bin/rustc create mode 120000 bin/rustdoc create mode 120000 bin/rustfmt create mode 120000 bin/rustup create mode 100644 rustfmt.toml create mode 100644 src/debug.rs create mode 100644 src/lib.rs create mode 100644 src/mask.rs create mode 100644 src/program.rs create mode 100644 src/register.rs create mode 100644 src/types.rs diff --git a/.devcontainer/Dockerfile.dev b/.devcontainer/Dockerfile.dev new file mode 100644 index 0000000..85df6da --- /dev/null +++ b/.devcontainer/Dockerfile.dev @@ -0,0 +1,6 @@ +FROM mcr.microsoft.com/devcontainers/base:ubuntu + +WORKDIR /home/vscode +RUN echo '\nsource bin/activate-hermit' >> .bashrc + +CMD ["/bin/bash"] diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..83ef78a --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,24 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/ubuntu +{ + "name": "ti-lp55231-devcontainer", + "build": { + "dockerfile": "Dockerfile.dev" + }, + "postStartCommand": ".devcontainer/postStartCommand.sh", + "customizations": { + "vscode": { + "extensions": [ + "editorConfig.editorConfig", + "rust-lang.rust-analyzer", + "tamasfe.even-better-toml" + ] + } + }, + "remoteEnv": { + // Add hermit bin folder to PATH for rust-analyzer plugin to work. This is + // redundant for shells (terminal sessions) but required by rust-analyzer + // since it does not pick up env vars + "PATH": "${containerEnv:PATH}:${containerWorkspaceFolder}/bin" + } +} diff --git a/.devcontainer/postStartCommand.sh b/.devcontainer/postStartCommand.sh new file mode 100755 index 0000000..e2f5be5 --- /dev/null +++ b/.devcontainer/postStartCommand.sh @@ -0,0 +1,3 @@ +#!/bin/bash +source bin/activate-hermit +rustup default 1.73.0 diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c1624df --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +# https://EditorConfig.org +root = true + +# Project-wide settings. Per-container overrides (e.g. tabs for Makefile or +# 4 spaces for python) should go in sub-folders' .editorconfig + +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 +indent_style = space +indent_size = 2 + +[**.md] +# Markdown files are sensitive to number of spaces to identify sub-blocks +indent_size = 4 diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000..a2849e0 --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,22 @@ +name: Rust + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Build + run: cargo build --verbose + - name: Test packaging + run: cargo package --verbose diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8e333b5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.hermit + +# Generated by Cargo, will have compiled files and executables +target/ + +# Backup files generated by rustfmt +**/*.rs.bk diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..93b8dad --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,14 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. + // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp + // List of extensions which should be recommended for users of this workspace. + "recommendations": [ + // For rust development + "rust-lang.rust-analyzer", + "tamasfe.even-better-toml", + // Project format settings (while editing, pre-lint/format) + "editorconfig.editorconfig", + // Dev containers, for non-linux dev envs + "ms-vscode-remote.remote-containers" + ] +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..7a88ea5 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +# V1.0.0 + +First release! diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..45d257b --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,133 @@ + +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +[INSERT CONTACT METHOD]. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..c6c216e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,37 @@ +# Contributing + +:tada::clinking_glasses: First off, thanks for taking the time to contribute! :tada::clinking_glasses: + +Contributions are always welcome, no matter how small. + +The following is a small set of guidelines for how to contribute to the project + +## Where to start + +### Code of Conduct + +This project adheres to the Contributor Covenant [Code of Conduct](CODE_OF_CONDUCT.md). +By participating you are expected to adhere to these expectations. Please report unacceptable behaviour to [contact@neurosity.co](mailto:contact@neurosity.co). + +### Contributing on Github + +If you're new to Git and want to learn how to fork this repo, make your own additions, and include those additions in the master version of this project, check out this [great tutorial](http://blog.davidecoppola.com/2016/11/howto-contribute-to-open-source-project-on-github/). + +### Community + +This project is maintained by [Neurosity](https://neurosity.co). Join the [Neurosity Discord Chat](https://discord.gg/E4dvX6g), where discussions about the Neurosity SDK take place. + +## How can I contribute? + +This is currently a small, humble project so our contribution process is rather casual. If there's a feature you'd be interested in building, go ahead! Let us know on the [Neurosity Discord](https://discord.gg/E4dvX6g) or [open an issue](../../issues) so others can follow along and we'll support you as much as we can. When you're finished submit a pull request to the master branch referencing the specific issue you addressed. + +If you find a bug, or have a suggestion on how to improve the project, please fill out a [Github issue](../../issues). + +### Steps to Contribute + +1. Fork it! +2. Create your feature branch: `git checkout -b my-new-feature` +3. Make changes +4. Commit your changes: `git commit -m 'Add some feature'` +5. Push to the branch: `git push origin my-new-feature` +6. Submit a pull request! diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..1480066 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,222 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[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 = "gpio-cdev" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "409296415b8abc7b47e5b77096faae14595c53724972da227434fc8f4b05ec8b" +dependencies = [ + "bitflags 1.3.2", + "libc", + "nix", +] + +[[package]] +name = "i2cdev" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fe61341e9ce588ede54fd131bf0df63eed3c6e45fcc7fa0e548ea176f39358" +dependencies = [ + "bitflags 1.3.2", + "byteorder", + "libc", + "nix", +] + +[[package]] +name = "ioctl-rs" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7970510895cee30b3e9128319f2cefd4bde883a39f38baa279567ba3a7eb97d" +dependencies = [ + "libc", +] + +[[package]] +name = "libc" +version = "0.2.148" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" + +[[package]] +name = "linux-embedded-hal" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61861762ac50cb746e6137f1aeb2ca9812e1b1f2b923cfc83550befdc0b9915d" +dependencies = [ + "cast", + "embedded-hal", + "gpio-cdev", + "i2cdev", + "nb 0.1.3", + "serial-core", + "serial-unix", + "spidev", + "sysfs_gpio", + "void", +] + +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "nb" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "801d31da0513b6ec5214e9bf433a77966320625a37860f910be265be6e18d06f" +dependencies = [ + "nb 1.1.0", +] + +[[package]] +name = "nb" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d5439c4ad607c3c23abf66de8c8bf57ba8adcd1f129e699851a6e43935d339d" + +[[package]] +name = "nix" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c" +dependencies = [ + "bitflags 1.3.2", + "cc", + "cfg-if", + "libc", + "memoffset", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serial-core" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f46209b345401737ae2125fe5b19a77acce90cd53e1658cda928e4fe9a64581" +dependencies = [ + "libc", +] + +[[package]] +name = "serial-unix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f03fbca4c9d866e24a459cbca71283f545a37f8e3e002ad8c70593871453cab7" +dependencies = [ + "ioctl-rs", + "libc", + "serial-core", + "termios", +] + +[[package]] +name = "spidev" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a204ca68d7f2109ffaf6326b79d8abf0014870625b78c8aff1941a5e4b9ff7d" +dependencies = [ + "bitflags 1.3.2", + "libc", + "nix", +] + +[[package]] +name = "sysfs_gpio" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ef9c9bcbfeb596ce4da59b2c59736235f35dcd516f03958ea10834473224157" +dependencies = [ + "nix", +] + +[[package]] +name = "termios" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5d9cf598a6d7ce700a4e6a9199da127e6819a61e64b68609683cc9a01b5683a" +dependencies = [ + "libc", +] + +[[package]] +name = "ti-lp55231" +version = "1.0.0" +dependencies = [ + "bitflags 2.4.0", + "linux-embedded-hal", + "scopeguard", +] + +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..49e956f --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "ti-lp55231" +version = "1.0.0" +description = "Linux I2C driver for Texas Instruments LP55231 LED controller" +authors = ["Bruno de Carvalho "] +repository = "https://github.com/neurosity/ti-lp55231/" +homepage = "https://neurosity.co/" +license = "MIT" +keywords = ["lp55231", "i2c", "hal", "embedded", "led"] +categories = ["embedded", "hardware-support"] +edition = "2021" + +[dependencies] +bitflags = "2.4.0" +linux-embedded-hal = "0.3.2" +scopeguard = "1.2.0" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9d69259 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Neurosity, Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..83a1011 --- /dev/null +++ b/README.md @@ -0,0 +1,255 @@ +LP55231 Linux Rust Driver +------------------------- + +[![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Containers&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/neurosity/ti-lp55231) + +Linux driver for [Texas Instruments LP55231](https://www.ti.com/product/LP55231), +a 9 channel RGB/White LED controller with internal program memory and integrated +charge Pump. + +**Features:** +- Full implementation of I2C control interface in [datasheet](https://www.ti.com/lit/ds/symlink/lp55231.pdf) +- Ergonomic API to leverage the [programming engine](#example-effect-blinking) +- Easy to debug with optional features: + - [read-after-write checks](#read-after-write-verifications) to validate register writes + - [debug output](#debug-output) to see the value in a register before and after a write + +## Initialization, setup, and preparing animations + +This example covers a typical initialization of the driver, preparing three +LEDs (R, G, B) for animated effects. + +```rust +use ti_lp55231::{ + Channel, + ChargePumpMode, + ClockSelection, + Direction, + Engine, + EngineExec, + EngineMode, + Instruction, + LP55231, + PreScale, +} + +// Create the driver +let path = "/dev/i2c-2"; +let i2c_addr = 0x32; +let ic = LP55231::create(path, i2c_addr)?; + +// Power and configure the driver. +ic.set_enabled(true)?; +ic.set_misc_settings(Misc { + auto_increment_enabled: true, + powersave_enabled: true, + charge_pump_mode: ChargePumpMode::Auto, + pwm_powersave_enabled: true, + clock_selection: ClockSelection::ForceInternal, +})?; + +// Channel assignment. +let (r, g, b) = (Channel::D7, Channel::D1, Channel::D2); + +// Enable logarithmic brightness for a smoother ramp up effect. +ic.set_log_brightness(r, true)?; +ic.set_log_brightness(g, true)?; +ic.set_log_brightness(b, true)?; + +// Enable ratiometric dimming to preserve the ratio between the +// RGB components of all mapped channels during animations +ic.set_ratiometric_dimming(r, true)?; +ic.set_ratiometric_dimming(g, true)?; +ic.set_ratiometric_dimming(b, true)?; + +// Set color to orange +ic.set_channel_pwm(r, 255)?; +ic.set_channel_pwm(g, 128)?; +ic.set_channel_pwm(b, 0)?; + +// Program the IC (see other example for implementations of `create_program`) +let instructions = create_program(&[r, g, b])?; +ic.load_program(&instructions)?; + +// Wait for the ENGINE_BUSY bit to clear, +// indicating that all instructions have been loaded. +ic.wait_while_engine_busy(Duration::from_millis(10))?; + +// Set up one of the programming engines to Halt & Hold (ready to execute). +let engine = Engine::E1; +ic.set_engine_exec(engine, EngineExec::Hold)?; +ic.set_engine_mode(engine, EngineMode::Halt)?; + +// Run the effect +ic.set_engine_exec(engine, EngineExec::Free)?; +ic.set_engine_mode(engine, EngineMode::RunProgram)?; +``` + +## Example effect: blinking + +This example is an implementation of `create_program` that prepares a blinking +effect to run in an endless loop. + +```rust +fn create_program(channels_to_control: &[Channel]) -> [Instruction; 8] { + [ + // ----- LED-to-Engine mapping table + // 00. Map all target output channels to the programming engine for control. + Instruction::map_channels(channels_to_control), + + // ----- blink effect start + // 01-02. Set LED mapping table start/end index + activation. + Instruction::mux_map_start(0), + Instruction::mux_ld_end(0), + // 03. Power all mapped LEDs off. + Instruction::set_pwm(0), + // 04. Wait ~0.5 seconds (15.625ms * 30). + Instruction::wait(PreScale::CT15_625, 30), + // 05. Set all LEDs to max brightness. + Instruction::set_pwm(255), + // 06. Wait ~0.5 seconds (15.625ms * 30). + Instruction::wait(PreScale::CT15_625, 30), + // 07. Loop back to beginning of blink effect index. + Instruction::branch(1, 0), + ] +} +``` + +## Example effect: glow + +```rust +fn create_program(channels_to_control: &[Channel]) -> [Instruction; 9] { + [ + // ----- LED-to-Engine mapping table + // 00. Map all target output channels to the programming engine for control. + Instruction::map_channels(channels_to_control), + + // ----- glow effect start + // 01-02. Set LED mapping table start/end index + activation. + Instruction::mux_map_start(0), + Instruction::mux_ld_end(0), + // 03. Quickly ramp up to max brightness. + Instruction::ramp(PreScale::CT0_488, 4, Direction::Up, 255), + // 04. Wait ~0.5 seconds (15.625ms * 30 = 468.75ms). + Instruction::wait(PreScale::CT15_625, 30), + // 05. Begin ramping brightness down to half (255 - 127 = 128). + Instruction::ramp(PreScale::CT15_625, 4, Direction::Down, 127), + // 06. Wait ~0.5 seconds (15.625ms * 30 = 468.75ms). + Instruction::wait(PreScale::CT15_625, 30), + // 07. Begin ramping brightness up to max (128 + 127 = 255). + Instruction::ramp(PreScale::CT15_625, 4, Direction::Up, 127), + // 08. Loop back to first step of effect. + Instruction::branch(1, 0), + ] +} +``` + +## Switching between effects + +The programming engine supports up to 96 instructions, which gives you plenty of +room to set up multiple effects. To switch between effects: + +- pause the programming engine +- update the program counter to first index of next effect +- unpause the programming engine. + +Example: + +```rust +// Pause engine execution. +ic.set_engine_exec(Engine::E1, EngineExec::Hold)?; +ic.wait_while_engine_busy(Duration::from_millis(1))?; + +// Update the program counter to the starting instruction of the desired effect +// This example assumes we're jumping to instruction 42, of the possible 96 +// programming memory addresses. +ic.set_engine_program_counter(Engine::E1, 42)?; + +// Unpause the engine and begin the new animation. +ic.set_engine_exec(Engine::E1, EngineExec::Free)?; +ic.set_engine_mode(Engine::E1, EngineMode::RunProgram)?; +``` + +# Debugging + +## Read-after-write verifications + +Read-after-write checks can be enabled with: + +```rust +let ic = LP55231::create(...)?; +ic.verify_writes = true; +``` + +This will cause the driver to perform a read after every I2C write instruction +to compare the value in the register. It will throw an exception if the read +value does not match the written value. + +**Note:** +This is useful during development, especially around using the programming +engines which must be in the correct internal state in order to allow changes. + +## Debug output + +When enabled via `debug_enabled` property, the driver will emit useful (but +rather verbose) output to help you understand the state of registers with every +read and write operation. Example: + +```rust +let ic = LP55231::create(...)?; +ic.debug_enabled = true; +ic.set_enabled(true)?; +``` + +Will produce output: + +``` +set_enabled(true) { + 00000000 << 0x00 ENABLE_ENGINE_CNTRL1 + 00100000 >> 0x00 ENABLE_ENGINE_CNTRL1 +} +``` + +### Scoping debug output for multiple I2C calls + +Scope for multiple debug calls can be combined with the `debug::scope!` macro: + +```rust +fn multiple_i2c_calls( + ic: &mut LP55231, + value: bool, +) -> Result<(), LinuxI2CError> { + debug::scope!(ic, "example({})", value); + ic.set_enabled(value)?; + ic.set_enabled(!value)?; + Ok(()) +} + +multiple_i2_calls(true)?; +``` + +Would result in the following output: +``` +example(true) { + set_enabled(true) { + 00000000 << 0x00 ENABLE_ENGINE_CNTRL1 + 00100000 >> 0x00 ENABLE_ENGINE_CNTRL1 + } + set_enabled(false) { + 00100000 << 0x00 ENABLE_ENGINE_CNTRL1 + 00000000 >> 0x00 ENABLE_ENGINE_CNTRL1 + } +} +``` + +See [debug.rs](src/debug.rs) docs for more details. + +# Getting started with development + +1. Clone the project and open the folder in VS Code +2. Accept plugin suggestions (dev container required in non-linux envs) +3. Re-open in dev container + +## TODO + +- [ ] Read/write pages in blocks (`at_once` param in `read/write_program_page`) diff --git a/bin/.rustup-1.25.2.pkg b/bin/.rustup-1.25.2.pkg new file mode 120000 index 0000000..383f451 --- /dev/null +++ b/bin/.rustup-1.25.2.pkg @@ -0,0 +1 @@ +hermit \ No newline at end of file diff --git a/bin/README.hermit.md b/bin/README.hermit.md new file mode 100644 index 0000000..e889550 --- /dev/null +++ b/bin/README.hermit.md @@ -0,0 +1,7 @@ +# Hermit environment + +This is a [Hermit](https://github.com/cashapp/hermit) bin directory. + +The symlinks in this directory are managed by Hermit and will automatically +download and install Hermit itself as well as packages. These packages are +local to this environment. diff --git a/bin/activate-hermit b/bin/activate-hermit new file mode 100755 index 0000000..fe28214 --- /dev/null +++ b/bin/activate-hermit @@ -0,0 +1,21 @@ +#!/bin/bash +# This file must be used with "source bin/activate-hermit" from bash or zsh. +# You cannot run it directly +# +# THIS FILE IS GENERATED; DO NOT MODIFY + +if [ "${BASH_SOURCE-}" = "$0" ]; then + echo "You must source this script: \$ source $0" >&2 + exit 33 +fi + +BIN_DIR="$(dirname "${BASH_SOURCE[0]:-${(%):-%x}}")" +if "${BIN_DIR}/hermit" noop > /dev/null; then + eval "$("${BIN_DIR}/hermit" activate "${BIN_DIR}/..")" + + if [ -n "${BASH-}" ] || [ -n "${ZSH_VERSION-}" ]; then + hash -r 2>/dev/null + fi + + echo "Hermit environment $("${HERMIT_ENV}"/bin/hermit env HERMIT_ENV) activated" +fi diff --git a/bin/cargo b/bin/cargo new file mode 120000 index 0000000..5046e66 --- /dev/null +++ b/bin/cargo @@ -0,0 +1 @@ +.rustup-1.25.2.pkg \ No newline at end of file diff --git a/bin/cargo-clippy b/bin/cargo-clippy new file mode 120000 index 0000000..5046e66 --- /dev/null +++ b/bin/cargo-clippy @@ -0,0 +1 @@ +.rustup-1.25.2.pkg \ No newline at end of file diff --git a/bin/cargo-fmt b/bin/cargo-fmt new file mode 120000 index 0000000..5046e66 --- /dev/null +++ b/bin/cargo-fmt @@ -0,0 +1 @@ +.rustup-1.25.2.pkg \ No newline at end of file diff --git a/bin/cargo-miri b/bin/cargo-miri new file mode 120000 index 0000000..5046e66 --- /dev/null +++ b/bin/cargo-miri @@ -0,0 +1 @@ +.rustup-1.25.2.pkg \ No newline at end of file diff --git a/bin/clippy-driver b/bin/clippy-driver new file mode 120000 index 0000000..5046e66 --- /dev/null +++ b/bin/clippy-driver @@ -0,0 +1 @@ +.rustup-1.25.2.pkg \ No newline at end of file diff --git a/bin/hermit b/bin/hermit new file mode 100755 index 0000000..7fef769 --- /dev/null +++ b/bin/hermit @@ -0,0 +1,43 @@ +#!/bin/bash +# +# THIS FILE IS GENERATED; DO NOT MODIFY + +set -eo pipefail + +export HERMIT_USER_HOME=~ + +if [ -z "${HERMIT_STATE_DIR}" ]; then + case "$(uname -s)" in + Darwin) + export HERMIT_STATE_DIR="${HERMIT_USER_HOME}/Library/Caches/hermit" + ;; + Linux) + export HERMIT_STATE_DIR="${XDG_CACHE_HOME:-${HERMIT_USER_HOME}/.cache}/hermit" + ;; + esac +fi + +export HERMIT_DIST_URL="${HERMIT_DIST_URL:-https://github.com/cashapp/hermit/releases/download/stable}" +HERMIT_CHANNEL="$(basename "${HERMIT_DIST_URL}")" +export HERMIT_CHANNEL +export HERMIT_EXE=${HERMIT_EXE:-${HERMIT_STATE_DIR}/pkg/hermit@${HERMIT_CHANNEL}/hermit} + +if [ ! -x "${HERMIT_EXE}" ]; then + echo "Bootstrapping ${HERMIT_EXE} from ${HERMIT_DIST_URL}" 1>&2 + INSTALL_SCRIPT="$(mktemp)" + # This value must match that of the install script + INSTALL_SCRIPT_SHA256="180e997dd837f839a3072a5e2f558619b6d12555cd5452d3ab19d87720704e38" + if [ "${INSTALL_SCRIPT_SHA256}" = "BYPASS" ]; then + curl -fsSL "${HERMIT_DIST_URL}/install.sh" -o "${INSTALL_SCRIPT}" + else + # Install script is versioned by its sha256sum value + curl -fsSL "${HERMIT_DIST_URL}/install-${INSTALL_SCRIPT_SHA256}.sh" -o "${INSTALL_SCRIPT}" + # Verify install script's sha256sum + openssl dgst -sha256 "${INSTALL_SCRIPT}" | \ + awk -v EXPECTED="$INSTALL_SCRIPT_SHA256" \ + '$2!=EXPECTED {print "Install script sha256 " $2 " does not match " EXPECTED; exit 1}' + fi + /bin/bash "${INSTALL_SCRIPT}" 1>&2 +fi + +exec "${HERMIT_EXE}" --level=fatal exec "$0" -- "$@" diff --git a/bin/hermit.hcl b/bin/hermit.hcl new file mode 100644 index 0000000..e69de29 diff --git a/bin/rls b/bin/rls new file mode 120000 index 0000000..5046e66 --- /dev/null +++ b/bin/rls @@ -0,0 +1 @@ +.rustup-1.25.2.pkg \ No newline at end of file diff --git a/bin/rust-analyzer b/bin/rust-analyzer new file mode 120000 index 0000000..5046e66 --- /dev/null +++ b/bin/rust-analyzer @@ -0,0 +1 @@ +.rustup-1.25.2.pkg \ No newline at end of file diff --git a/bin/rust-gdb b/bin/rust-gdb new file mode 120000 index 0000000..5046e66 --- /dev/null +++ b/bin/rust-gdb @@ -0,0 +1 @@ +.rustup-1.25.2.pkg \ No newline at end of file diff --git a/bin/rust-gdbgui b/bin/rust-gdbgui new file mode 120000 index 0000000..5046e66 --- /dev/null +++ b/bin/rust-gdbgui @@ -0,0 +1 @@ +.rustup-1.25.2.pkg \ No newline at end of file diff --git a/bin/rust-lldb b/bin/rust-lldb new file mode 120000 index 0000000..5046e66 --- /dev/null +++ b/bin/rust-lldb @@ -0,0 +1 @@ +.rustup-1.25.2.pkg \ No newline at end of file diff --git a/bin/rustc b/bin/rustc new file mode 120000 index 0000000..5046e66 --- /dev/null +++ b/bin/rustc @@ -0,0 +1 @@ +.rustup-1.25.2.pkg \ No newline at end of file diff --git a/bin/rustdoc b/bin/rustdoc new file mode 120000 index 0000000..5046e66 --- /dev/null +++ b/bin/rustdoc @@ -0,0 +1 @@ +.rustup-1.25.2.pkg \ No newline at end of file diff --git a/bin/rustfmt b/bin/rustfmt new file mode 120000 index 0000000..5046e66 --- /dev/null +++ b/bin/rustfmt @@ -0,0 +1 @@ +.rustup-1.25.2.pkg \ No newline at end of file diff --git a/bin/rustup b/bin/rustup new file mode 120000 index 0000000..5046e66 --- /dev/null +++ b/bin/rustup @@ -0,0 +1 @@ +.rustup-1.25.2.pkg \ No newline at end of file diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..13a58fe --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,4 @@ +tab_spaces = 2 +max_width = 80 +wrap_comments = true +comment_width = 80 diff --git a/src/debug.rs b/src/debug.rs new file mode 100644 index 0000000..e6f2f9f --- /dev/null +++ b/src/debug.rs @@ -0,0 +1,159 @@ +/// Macros for low level debugging when developing against I2C devices. + +/// Create a debugging scope on the provided context. +/// +/// A debugging scope causes any subsequent [`text!`] or [`byte!`] calls to be +/// printed with additional padding and automatically ends once the current code +/// scope ends. +/// +/// Scopes can be nested, where the inner scope will further pad any output. +/// +/// The provided context (`ctx`) can be any struct that has two fields +/// accessible to these macros: +/// 1. `debug_enabled: bool` +/// 2. `debug_depth: Rc>` +/// +/// Example: +/// ``` +/// debug::scope!(ctx, "entering scope 1"); +/// debug::text!(ctx, "message 1") +/// debug::scope!(ctx, "entering scope 2"); +/// debug::text!(ctx, "message 2") +/// { +/// debug::scope!(ctx, "entering scope 3"); +/// debug::text!(ctx, "message 3") +/// } // scope 3 ends +/// debug::text!("message 4") +/// ``` +/// Prints: +/// ```text +/// entering scope 1 +/// message 1 +/// entering scope 2 +/// message 2 +/// entering scope 3 +/// message 3 +/// message 4 +/// ``` +/// +/// The nesting ability of scopes becomes really useful when composing I2C +/// operations; example: +/// ``` +/// fn start() { +/// debug::scope!("start()"); +/// let byte = 1; +/// debug::byte!(byte, "write start byte", byte) +/// device.write(byte); +/// } +/// +/// fn stop() { +/// debug::scope!("stop()"); +/// let byte = 0; +/// debug::byte!(byte, "write stop byte", byte) +/// device.write(byte); +/// } +/// +/// fn restart() { +/// debug::scope!("restart()"); +/// stop(); +/// start(); +/// } +/// +/// restart(); +/// // ... +/// stop(); +/// ``` +/// +/// Would result in the output: +/// ```text +/// restart() +/// stop() +/// 00000000 write stop byte +/// start() +/// 00000001 write start byte +/// stop() +/// 00000000 write stop byte +/// ``` +#[macro_export] +macro_rules! scope { + ($ctx:ident, $fmt:expr $(, $arg:expr)* $(,)?) => { + // _unused is dropped at end of scope where this macro is called, which + // decrements debug depth. + let _unused = if $ctx.debug_enabled { + let mut cur_depth = $ctx.debug_depth.lock().unwrap(); + let padding = " ".repeat(*cur_depth); + println!("{}{} {{", padding, format!($fmt $(, $arg)*)); + *cur_depth += 1; + // lock releases after return + + // debug_depth ArcMutex needs to be cloned since guard() takes ownership + // of the arg. Code in block below is executed at the end of the code + // scope where the `scope` macro is called. + Some(scopeguard::guard($ctx.debug_depth.clone(), |depth| { + let mut cur_depth = depth.lock().unwrap(); + if *cur_depth > 0 { + *cur_depth -= 1; + } + println!("{}}}", " ".repeat(*cur_depth)); + })) + } else { + None + }; + }; +} +pub use scope; + +/// Print a formated message at current debug depth. +/// +/// Example: +/// ``` +/// debug::text!(ctx, "1. top-level debug text"); +/// debug::scope!(ctx, "2. in-scope"); +/// debug::text!(ctx, "2a. in-scope debug text"); +/// ``` +/// +/// Prints: +/// ```text +/// 1. top-level debug text +/// 2. in-scope +/// 2a. in-scope debug text +/// ``` +#[macro_export] +macro_rules! text { + ($ctx:expr, $fmt:expr $(, $arg:expr)* $(,)?) => { + if $ctx.debug_enabled { + let padding = " ".repeat(*$ctx.debug_depth.lock().unwrap()); + println!("{}{}", padding, format!($fmt $(, $arg)*)); + }; + }; +} +pub use text; + +/// Print the binary representation for the given byte, along with a formatted +/// description at the current debug depth. +/// +/// Example: +/// ``` +/// let value = 0b0010_1010; +/// debug::byte(value, "is binary for {}", value); +/// ``` +/// Prints: +/// ```text +/// 00101010 is binary for 42 +/// ``` +#[macro_export] +macro_rules! byte { + ($ctx:expr, $value:expr, $($description:tt)*) => { + if $ctx.debug_enabled { + let value: u8 = $value; + let formatted_string = format!($($description)*); + debug::text!( + $ctx, + "{:08b} {}", + value, + formatted_string, + ); + } + }; +} +pub use byte; diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..6f2da77 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,746 @@ +use std::{ + sync::{Arc, Mutex}, + thread::sleep, + time::Duration, +}; + +use linux_embedded_hal::i2cdev::{ + core::I2CDevice, + linux::{LinuxI2CDevice, LinuxI2CError}, +}; + +pub mod debug; +mod mask; +mod program; +mod register; +mod types; + +pub use mask::*; +pub use program::*; +pub use register::*; +pub use types::*; + +/// Driver for Texas Instruments LP55231 I²C via [embedded-hal]. +/// +/// For more details, please refer to the [technical specs]. +/// +/// [embedded-hal]: https://docs.rs/embedded-hal +/// [technical specs]: (https://www.ti.com/lit/ds/symlink/lp55231.pdf). +pub struct LP55231 { + device: LinuxI2CDevice, + /// Enable debug output. + /// + /// Will print address and values for every I2C read and write instruction. + pub debug_enabled: bool, + /// Read-after-write verification + pub verify_writes: bool, + #[doc(hidden)] + pub debug_depth: Arc>, +} + +impl LP55231 { + /// Create a new LP55231 abstraction for the specified path and I2C address. + pub fn create(path: &str, i2c_addr: u16) -> Result { + let device = LinuxI2CDevice::new(path, i2c_addr)?; + Ok(Self { + device, + debug_enabled: false, + verify_writes: false, + debug_depth: Arc::new(Mutex::new(0)), + }) + } + + /// Reset the IC. + pub fn reset(&mut self) -> Result<(), LinuxI2CError> { + debug::scope!(self, "reset()"); + + // From spec: "Writing 11111111 into this register resets the LP55231" + self.write_register(Register::RESET, 0b1111_1111)?; + + Ok(()) + } + + /// Test whether the IC is currently enabled. + pub fn is_enabled(&mut self) -> Result { + debug::scope!(self, "is_enabled()"); + + let value = self.read_register(Register::ENABLE_ENGINE_CNTRL1)?; + Ok((value & Mask::CHIP_EN.bits()) > 0) + } + + /// Enable or disable the IC. + pub fn set_enabled(&mut self, enabled: bool) -> Result<(), LinuxI2CError> { + debug::scope!(self, "set_enabled({})", enabled); + + let current_value = self.read_register(Register::ENABLE_ENGINE_CNTRL1)?; + let new_value = Mask::CHIP_EN.apply(enabled as u8, current_value); + self.write_register(Register::ENABLE_ENGINE_CNTRL1, new_value) + } + + /// Read the current [misc](Misc) settings from the IC. + pub fn get_misc_settings(&mut self) -> Result { + debug::scope!(self, "get_misc_settings()"); + + let value = self.read_register(Register::MISC)?; + let misc = Misc { + auto_increment_enabled: Mask::EN_AUTO_INCR.is_set(value), + powersave_enabled: Mask::POWERSAVE_EN.is_set(value), + charge_pump_mode: ChargePumpMode::from(Mask::CP_MODE.value(value)), + pwm_powersave_enabled: (value & Mask::PWM_PS_EN.bits()) > 0, + clock_selection: ClockSelection::from(Mask::CLK_DET_EN.value(value)), + }; + + Ok(misc) + } + + /// Set [misc](Misc) settings for the IC. + /// + /// Overrides all existing settings. + pub fn set_misc_settings(&mut self, misc: Misc) -> Result<(), LinuxI2CError> { + debug::scope!(self, "set_misc_settings({:?})", misc); + + let en_auto_incr = + Mask::EN_AUTO_INCR.with(misc.auto_increment_enabled as u8); + let powersave_en = Mask::POWERSAVE_EN.with(misc.powersave_enabled as u8); + let cp_mode = Mask::CP_MODE.with(misc.charge_pump_mode as u8); + let pwm_ps_en = Mask::PWM_PS_EN.with(misc.pwm_powersave_enabled as u8); + let clk_det_en_int_clk_en = + Mask::CLK_DET_EN.with(misc.clock_selection as u8); + + let value = + en_auto_incr | powersave_en | cp_mode | pwm_ps_en | clk_det_en_int_clk_en; + + self.write_register(Register::MISC, value) + } + + /// Set the Pulse-Width Modulation (PWM) value for the specified [`Channel`]. + /// + /// PWM controls luminance. + pub fn set_channel_pwm( + &mut self, + channel: Channel, + pwm: u8, + ) -> Result<(), LinuxI2CError> { + debug::scope!( + self, + "set_channel_pwm(channel: {:?}, pwm: {})", + channel, + pwm + ); + + self.write_register(Register::pwm_for(channel), pwm)?; + + Ok(()) + } + + /// Set the current value for the specified [`Channel`]. + /// + /// Current controls luminous intensity (brightness). + pub fn set_channel_current( + &mut self, + channel: Channel, + current: u8, + ) -> Result<(), LinuxI2CError> { + debug::scope!( + self, + "set_channel_current(channel: {:?}, current: {})", + channel, + current + ); + + self.write_register(Register::current_control_for(channel), current)?; + + Ok(()) + } + + /// Enable or disable logarithmic brightness for the specified [`Channel`]. + pub fn set_log_brightness( + &mut self, + channel: Channel, + enabled: bool, + ) -> Result<(), LinuxI2CError> { + debug::scope!( + self, + "set_log_brightness(channel: {:?}, enabled: {})", + channel, + enabled + ); + + // LOG_EN is a bit in D1_CTL; to change only that bit, the current value of + // the byte must be read, modified, and written back (if different). + let current_value = self.read_register(Register::control_for(channel))?; + let new_value = Mask::LOG_EN.apply(enabled as u8, current_value); + if new_value != current_value { + self.write_register(Register::control_for(channel), new_value)?; + } + + Ok(()) + } + + /// Enable or disable radiometric dimming for the specified [`Channel`]. + pub fn set_ratiometric_dimming( + &mut self, + channel: Channel, + enabled: bool, + ) -> Result<(), LinuxI2CError> { + debug::scope!( + self, + "set_ratiometric_dimming(channel: {:?}, enabled: {})", + channel, + enabled + ); + + // Since D9 has its own register, there's no need to read-modify-write. + if channel == Channel::D9 { + return self.write_register( + Register::OUTPUT_DIRECT_RATIOMETRIC_MSB, + enabled as u8, + ); + } + + // Registers D1 through D8 share same registry (bit 0 = D1, bit 7 = D8). To + // change only the specified channel, whole register must be read and the + // appropriate bit changed (if different). + let current_value = + self.read_register(Register::OUTPUT_DIRECT_RATIOMETRIC_LSB)?; + let new_value = Mask::ratiometric_dimming_for(channel) + .apply(enabled as u8, current_value); + if new_value != current_value { + self + .write_register(Register::OUTPUT_DIRECT_RATIOMETRIC_LSB, new_value)?; + } + + Ok(()) + } + + /// Enable or disable the specified [`Channel`]. + pub fn set_channel_enabled( + &mut self, + channel: Channel, + enabled: bool, + ) -> Result<(), LinuxI2CError> { + debug::scope!( + self, + "set_channel_enabled(channel: {:?}, enabled: {})", + channel, + enabled + ); + + // D9 has its own register; no need to read-modify-write. + if channel == Channel::D9 { + return self + .write_register(Register::OUTPUT_ON_OFF_CONTROL_MSB, enabled as u8); + } + + let current_value = + self.read_register(Register::OUTPUT_ON_OFF_CONTROL_LSB)?; + let new_value = + Mask::on_off_for(channel).apply(enabled as u8, current_value); + if new_value != current_value { + self.write_register(Register::OUTPUT_ON_OFF_CONTROL_LSB, new_value)?; + } + + Ok(()) + } + + /// Assign the specified [`Channel`] to the specified [`Fader`]. + /// Removes [`Fader`] associations if `None` is supplied as an argument. + /// + /// [`Channel`] and [`Fader`] can be associated many-to-many, and any + /// subsequent intensity adjustments to the fader will result in the same + /// change to all of its assigned channels. + pub fn assign_to_fader( + &mut self, + channel: Channel, + fader: Option, + ) -> Result<(), LinuxI2CError> { + debug::scope!( + self, + "assign_to_fader(channel: {:?}, fader: {:?})", + channel, + fader + ); + + let current_value = self.read_register(Register::control_for(channel))?; + + // 00 - none, 01 - F1, 02 - F2, 03 - F3 + let fader_assignment_bits = fader.map(|f| f as u8 + 1).unwrap_or(0b00); + let new_value = Mask::MAPPING.apply(fader_assignment_bits, current_value); + if new_value != current_value { + self.write_register(Register::control_for(channel), new_value)?; + } + + Ok(()) + } + + /// Adjust the intensity of the specified [`Fader`]. + /// + /// Will result in the adjustment of the intensity of every [`Channel`] + /// previously associated with the fader. + pub fn set_fader_intensity( + &mut self, + fader: Fader, + intensity: u8, + ) -> Result<(), LinuxI2CError> { + debug::scope!( + self, + "set_fader_intensity(fader: {:?}, intensity: {})", + fader, + intensity + ); + + self.write_register(Register::intensity_for(fader), intensity) + } + + pub fn clear_interrupt(&mut self) -> Result<(), LinuxI2CError> { + debug::scope!(self, "clear_interrupt()"); + + self.read_register(Register::STATUS_INTERRUPT)?; + + Ok(()) + } + + /// Load the specified program. + /// + /// Accepts up to [`MAX_INSTRUCTIONS`], writing them over as many pages as + /// necessary to fit the whole program. + /// + /// Since programming registers can only be accessed while the programming + /// engines are in LOAD PROGRAM, this method: + /// 1. Puts all engines in LOAD PROGRAM mode + /// 2. Waits for the engine busy bit to clear + /// 3. Writes program instructions to programming registers + /// 4. Puts all engines in disabled mode + /// + /// After this method is called, relevant engines must be manually switched + /// to run mode. + /// + /// After the program is loaded all `ENG* PROG START ADDR` values reset to + /// default (see [`Self::set_engine_entry_point`]). + pub fn load_program( + &mut self, + instructions: &[Instruction], + ) -> Result<(), LinuxI2CError> { + debug::scope!(self, "load_program([{} instructions])", instructions.len()); + + validate_total_instruction_count(instructions)?; + + // 1. Set all engines to _load program_ mode. + // + // From the spec (section 7.6.2, page 28): + // "Load program mode can be entered from the disabled mode only. + // Entering load program mode from the run program mode is not allowed." + // + // Not clear in spec, but all engines must be disabled. + self.set_all_engines_mode(EngineMode::Disabled)?; + // From the spec (section 7.6.3, page 37): + // "in order to access program memory the operation mode needs to be + // load program" + // + // Not clear in spec, but all engines must be in load mode, otherwise + // writes do not work (read-after-write returns empty program registers). + self.set_all_engines_mode(EngineMode::LoadProgram)?; + + // 2. Wait until clear to enter load mode; from the spec (7.6.2, pg 28): + // "Serial bus master should check the busy bit before writing to program + // memory or allow at least 1ms delay after entering to load mode before + // memory write (...)" + let poll_interval = Duration::from_millis(1); + self.wait_while_engine_busy(poll_interval)?; + sleep(poll_interval * 10); + + // optional step: ensure auto-increment is set to allow single I2C write + // per program page (vs `2 * instructions.len()` writes if writing + // instructions one-by-one). + // + // From the spec (section 7.5.2.3, page 20): + // "The auto-increment feature allows writing several consecutive + // registers within one transmission" + let auto_incr = false; + // TODO uncomment and change above to true. + // let mut misc = self.get_misc_settings()?; + // if !misc.auto_increment_enabled { + // misc.auto_increment_enabled = true; + // self.set_misc_settings(misc)?; + // } + + // 3. Break program into pages of 16 instructions and write each page. + let pages: Vec<&[Instruction]> = instructions.chunks(16).collect(); + for (page_num, page_instructions) in pages.iter().enumerate() { + self.write_program_page(page_num as u8, page_instructions, auto_incr)?; + } + + // 4. Set all engines back to disabled. + self.set_all_engines_mode(EngineMode::Disabled) + } + + /// Read a single program [`Instruction`] at the specified `index`, from the + /// current page (i.e. the page selected via + /// [PROG MEM PAGE SEL](Register::PROG_MEM_PAGE_SEL) register). + pub fn read_program_instruction( + &mut self, + index: u8, + ) -> Result { + validate_instruction_index(index)?; + + let register = (Register::PROG_MEM_BASE as u8) + (index * 2); + let msb = self.device.smbus_read_byte_data(register)?; + let lsb = self.device.smbus_read_byte_data(register + 1)?; + debug::text!( + self, + "[{:02}] << {:02x} & {:02x} {:08b} {:08b} (0x{:02x}{:02x})", + index, + register, + register + 1, + msb, + lsb, + msb, + lsb, + ); + Ok(Instruction { msb, lsb }) + } + + /// Write up to [`INSTRUCTIONS_PER_PAGE`] program [instructions](Instruction) + /// to the specified `page`. + /// + /// Arguments: + /// * `page` - The page number to write. Must be in range \[0:5\] + /// * `instructions` - List of instructions to write. + /// * `at_once` - Whether to write all instructions in a single I2C write + /// or use individual writes (each instruction is 16 bytes, which could result + /// in up to 32 writes). + /// + /// `at_once` Should only be set to true if the device is configured with + /// `EN_AUTO_INCR` (see [`Self::set_misc_settings`]). + pub fn write_program_page( + &mut self, + page: u8, + instructions: &[Instruction], + at_once: bool, + ) -> Result<(), LinuxI2CError> { + validate_page(page)?; + validate_per_page_instruction_count(instructions)?; + + debug::scope!( + self, + "write_program_page(page: {}, [{} instructions])", + page, + instructions.len() + ); + + // Set the page number... + self.write_register(Register::PROG_MEM_PAGE_SEL, page)?; + // ... and write the instructions. + if at_once { + panic!("not yet implemented"); + // TODO test single I2C writes relying on auto-increment + // self.device.smbus_write_block_data(Register::PROG_MEM_BASE, ???) + } else { + for (index, instruction) in instructions.iter().enumerate() { + self.write_program_instruction(index as u8, instruction)?; + } + } + + Ok(()) + } + + /// Write a single program [`Instruction`] at the specified index, to the + /// current page (i.e. the page currently selected via + /// [PROG MEM PAGE SEL](Register::PROG_MEM_PAGE_SEL)). + pub fn write_program_instruction( + &mut self, + index: u8, + instr: &Instruction, + ) -> Result<(), LinuxI2CError> { + validate_instruction_index(index)?; + + let register = (Register::PROG_MEM_BASE as u8) + (index * 2); + // TODO single u16 write (requires auto-increment) + self.device.smbus_write_byte_data(register, instr.msb)?; + self.device.smbus_write_byte_data(register + 1, instr.lsb)?; + debug::text!( + self, + "[{:02}] >> {:02x} & {:02x} {:08b} {:08b} (0x{:02x}{:02x})", + index, + register, + register + 1, + instr.msb, + instr.lsb, + instr.msb, + instr.lsb, + ); + Ok(()) + } + + /// Read a program page. + /// + /// Each page contains up to [`INSTRUCTIONS_PER_PAGE`] + /// [instructions](Instruction). + pub fn read_program_page( + &mut self, + page: u8, + at_once: bool, + ) -> Result, LinuxI2CError> { + validate_page(page)?; + + debug::scope!(self, "read_program_page(page: {})", page); + + self.write_register(Register::PROG_MEM_PAGE_SEL, page)?; + let mut instructions: Vec = vec![]; + if at_once { + // TODO read whole page at once + panic!("not implemented") + } else { + for i in 0..16 { + let instruction = self.read_program_instruction(i)?; + instructions.push(instruction); + } + } + + Ok(instructions) + } + + /// Set the starting address for the specified [`Engine`] program instructions. + /// + /// Defaults: + /// - Engine 1: 0 + /// - Engine 2: 8 + /// - Engine 3: 16 + pub fn set_engine_entry_point( + &mut self, + engine: Engine, + entry_point: u8, + ) -> Result<(), LinuxI2CError> { + // TODO validate entry_point value fits 7 bits (i.e. <= MASK_ADDR) + debug::scope!( + self, + "set_engine_entry_point(engine: {:?}, entry_point: {})", + engine, + entry_point + ); + + self.write_register(Register::program_start_for(engine), entry_point) + } + + /// Set program counter value for the specified [`Engine`]. + /// + /// NB: Program counter can only be modified if the engines are not running. + pub fn set_engine_program_counter( + &mut self, + engine: Engine, + pc: u8, + ) -> Result<(), LinuxI2CError> { + validate_program_counter(pc)?; + + debug::scope!( + self, + "set_engine_program_counter(engine: {:?}, pc: {})", + engine, + pc + ); + + let register = Register::program_counter_for(engine); + self.write_register(register, pc)?; + + Ok(()) + } + + /// Set [program execution control](EngineExec) for the specified [`Engine`]. + pub fn set_engine_exec( + &mut self, + engine: Engine, + exec_mode: EngineExec, + ) -> Result<(), LinuxI2CError> { + debug::scope!( + self, + "set_engine_exec(engine: {:?}, exec_mode: {:?})", + engine, + exec_mode + ); + + let current_value = self.read_register(Register::ENABLE_ENGINE_CNTRL1)?; + let new_value = + Mask::exec_for(engine).apply(exec_mode as u8, current_value); + if new_value != current_value { + self.write_register(Register::ENABLE_ENGINE_CNTRL1, new_value)?; + } + + Ok(()) + } + + /// Convenience alias for [`Self::set_engine_modes`] + /// that applies the same mode to all engines. + pub fn set_all_engines_mode( + &mut self, + op_mode: EngineMode, + ) -> Result<(), LinuxI2CError> { + self.set_engine_modes(op_mode, op_mode, op_mode) + } + + /// Set [`EngineMode`] for each of the programming engines. + pub fn set_engine_modes( + &mut self, + engine1: EngineMode, + engine2: EngineMode, + engine3: EngineMode, + ) -> Result<(), LinuxI2CError> { + debug::scope!( + self, + "set_engine_modes(engine1: {:?}, engine2: {:?}, engine3: {:?})", + engine1, + engine2, + engine3 + ); + + let e1_bits = Mask::ENGINE1_MODE.with(engine1 as u8); + let e2_bits = Mask::ENGINE2_MODE.with(engine2 as u8); + let e3_bits = Mask::ENGINE2_MODE.with(engine3 as u8); + + let value = e1_bits | e2_bits | e3_bits; + + self.write_register(Register::ENGINE_CNTRL_2, value) + } + + /// Set the [`EngineMode`] for the specified [`Engine`]. + pub fn set_engine_mode( + &mut self, + engine: Engine, + op_mode: EngineMode, + ) -> Result<(), LinuxI2CError> { + debug::scope!(self, "set_engine_mode({:?}, {:?})", engine, op_mode); + + let current_value = self.read_register(Register::ENGINE_CNTRL_2)?; + let new_value = Mask::mode_for(engine).apply(op_mode as u8, current_value); + if new_value != current_value { + self.write_register(Register::ENGINE_CNTRL_2, new_value)?; + } + + Ok(()) + } + + /// Read a byte from the specified [`Register`]. + pub fn read_register( + &mut self, + register: Register, + ) -> Result { + let value = self.device.smbus_read_byte_data(register as u8)?; + debug::byte!(self, value, "<< {:02x} {:?}", register as u8, register); + Ok(value) + } + + /// Write a byte to the specified [`Register`]. + pub fn write_register( + &mut self, + register: Register, + value: u8, + ) -> Result<(), LinuxI2CError> { + debug::byte!(self, value, ">> {:02x} {:?}", register as u8, register); + self.device.smbus_write_byte_data(register as u8, value)?; + + if self.verify_writes { + let post_write_value = + self.device.smbus_read_byte_data(register as u8)?; + if post_write_value != value { + return Err(LinuxI2CError::Io(std::io::Error::new( + std::io::ErrorKind::Other, + format!( + "write to register {:02x} {:?} failed; read-after-write expecting {:08b} but got {:08b}", + register as u8, register, value, post_write_value, + ), + ))); + } + } + + Ok(()) + } + + /// Wait for the `ENGINE_BUSY` bit to clear, polling at intervals of + /// specified duration. + /// + /// Returns immediately if busy bit is not set. + pub fn wait_while_engine_busy( + &mut self, + poll_interval: Duration, + ) -> Result<(), LinuxI2CError> { + loop { + let value = self.read_register(Register::STATUS_INTERRUPT)?; + if !Mask::ENGINE_BUSY.is_set(value) { + return Ok(()); + } + sleep(poll_interval); + } + } +} + +fn validate_page(page: u8) -> Result<(), LinuxI2CError> { + if page < 6 { + return Ok(()); + } + + Err(LinuxI2CError::Io(std::io::Error::new( + std::io::ErrorKind::Other, + format!("invalid page ({}); must be in range [0:5]", page), + ))) +} + +fn validate_instruction_index(index: u8) -> Result<(), LinuxI2CError> { + if index < 16 { + return Ok(()); + } + + Err(LinuxI2CError::Io(std::io::Error::new( + std::io::ErrorKind::Other, + format!( + "invalid instruction index ({}); must be in range [0:15]", + index + ), + ))) +} + +fn validate_per_page_instruction_count( + instructions: &[Instruction], +) -> Result<(), LinuxI2CError> { + if instructions.len() <= INSTRUCTIONS_PER_PAGE as usize { + return Ok(()); + } + + Err(LinuxI2CError::Io(std::io::Error::new( + std::io::ErrorKind::Other, + format!( + "too many instructions for a page ({}); limit is {}", + instructions.len(), + INSTRUCTIONS_PER_PAGE + ), + ))) +} + +fn validate_total_instruction_count( + instructions: &[Instruction], +) -> Result<(), LinuxI2CError> { + if instructions.len() <= MAX_INSTRUCTIONS as usize { + return Ok(()); + } + + Err(LinuxI2CError::Io(std::io::Error::new( + std::io::ErrorKind::Other, + format!( + "too many instructions ({}); limit is {}", + instructions.len(), + MAX_INSTRUCTIONS + ), + ))) +} + +fn validate_program_counter(counter: u8) -> Result<(), LinuxI2CError> { + if counter < MAX_INSTRUCTIONS { + return Ok(()); + } + + Err(LinuxI2CError::Io(std::io::Error::new( + std::io::ErrorKind::Other, + format!( + "invalid program counter ({}); must be in range [0:{}]", + counter, MAX_INSTRUCTIONS + ), + ))) +} diff --git a/src/mask.rs b/src/mask.rs new file mode 100644 index 0000000..b109c9d --- /dev/null +++ b/src/mask.rs @@ -0,0 +1,165 @@ +use bitflags::bitflags; + +use crate::{types::Engine, Channel}; + +bitflags! { + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] + /// Masks. + /// + /// Sorted by register they apply to. + /// *Not* a comprehensive list. + /// + /// Refer to spec sections 7.6.1 and 7.6.2 + pub struct Mask: u8 { + // 00, ENABLE / ENGINE CNTRL1 + const CHIP_EN = 0b0100_0000; + const ENGINE1_EXEC = 0b0011_0000; + const ENGINE2_EXEC = 0b0000_1100; + const ENGINE3_EXEC = 0b0000_0011; + + // 01 ENGINE CNTRL2 + const ENGINE1_MODE = 0b0011_0000; + const ENGINE2_MODE = 0b0000_1100; + const ENGINE3_MODE = 0b0000_0011; + + // 02 OUTPUT DIRECT/RATIOMETRIC MSB + const D9_RATIO_EN = 0b0000_0001; + + // 03 OUTPUT DIRECT/RATIOMETRIC LSB + const D8_RATIO_EN = 0b1000_0000; + const D7_RATIO_EN = 0b0100_0000; + const D6_RATIO_EN = 0b0010_0000; + const D5_RATIO_EN = 0b0001_0000; + const D4_RATIO_EN = 0b0000_1000; + const D3_RATIO_EN = 0b0000_0100; + const D2_RATIO_EN = 0b0000_0010; + const D1_RATIO_EN = 0b0000_0001; + + // 04 OUTPUT ON/OFF CONTROL MSB + const D9_ON = 0b0000_0001; + + // 05 OUTPUT ON/OFF CONTROL LSB + const D8_ON = 0b1000_0000; + const D7_ON = 0b0100_0000; + const D6_ON = 0b0010_0000; + const D5_ON = 0b0001_0000; + const D4_ON = 0b0000_1000; + const D3_ON = 0b0000_0100; + const D2_ON = 0b0000_0010; + const D1_ON = 0b0000_0001; + + // 06-0E, D1-D9 CONTROL + const LOG_EN = 0b0010_0000; + const MAPPING = 0b1100_0000; + + // 36 MISC + const EN_AUTO_INCR = 0b0100_0000; + const POWERSAVE_EN = 0b0010_0000; + const CP_MODE = 0b0001_1000; + const PWM_PS_EN = 0b0000_0100; + const CLK_DET_EN = 0b0000_0011; + + // 3A, STATUS/INTERRUPT + const LEDTEST_MEAS_DONE = 0b1000_0000; + const MASK_BUSY = 0b0100_0000; + const STARTUP_BUSY = 0b0010_0000; + const ENGINE_BUSY = 0b0001_0000; + const EXT_CLK_USED = 0b0000_1000; + const ENG1_INT = 0b0000_0100; + const ENG2_INT = 0b0000_0010; + const ENG3_INT = 0b0000_0001; + + // 3D, RESET + const RESET = 0b1111_1111; + + // 4F, PROG MEM PAGE SELECT + const PAGE_SEL = 0b0000_0111; + } +} + +impl Mask { + /// Apply the specified `value` using the [Mask] to `byte`. + /// + /// Example; given: + /// - A mask `0b0000_1100` + /// - A value `0b10` + /// - A byte `0b1111_1111` + /// Then: + /// - `mask.apply(value, byte)` will produce `0b1111_1011` + pub fn apply(&self, value: u8, to_byte: u8) -> u8 { + let byte_with_mask_bits_cleared = to_byte & !self.bits(); + let value_moved_to_mask_bits = value << self.bits().trailing_zeros(); + + byte_with_mask_bits_cleared | value_moved_to_mask_bits + } + + pub fn with(&self, value: u8) -> u8 { + value << self.bits().trailing_zeros() + } + + /// Returns the value set at the mask bits. + /// + /// Applies [Mask] bits to byte and shifts everything right `n` times, where + /// `n` is the number of trailing zeroes. + /// + /// Example; given: + /// - A byte `0b1100_1100` + /// - A mask `0b0000_1100` + /// + /// Then: + /// - `mask.value(byte)` will produce `0b11` + pub fn value(&self, byte: u8) -> u8 { + let value_at_mask_bits = byte & self.bits(); + value_at_mask_bits >> self.bits().trailing_zeros() + } + + pub fn is_set(&self, byte: u8) -> bool { + self.value(byte) > 0 + } +} + +impl Mask { + pub fn exec_for(engine: Engine) -> Mask { + match engine { + Engine::E1 => Mask::ENGINE1_EXEC, + Engine::E2 => Mask::ENGINE2_EXEC, + Engine::E3 => Mask::ENGINE3_EXEC, + } + } + + pub fn mode_for(engine: Engine) -> Mask { + match engine { + Engine::E1 => Mask::ENGINE1_MODE, + Engine::E2 => Mask::ENGINE2_MODE, + Engine::E3 => Mask::ENGINE3_MODE, + } + } + + pub fn ratiometric_dimming_for(channel: Channel) -> Mask { + match channel { + Channel::D1 => Mask::D1_RATIO_EN, + Channel::D2 => Mask::D2_RATIO_EN, + Channel::D3 => Mask::D3_RATIO_EN, + Channel::D4 => Mask::D4_RATIO_EN, + Channel::D5 => Mask::D5_RATIO_EN, + Channel::D6 => Mask::D6_RATIO_EN, + Channel::D7 => Mask::D7_RATIO_EN, + Channel::D8 => Mask::D8_RATIO_EN, + Channel::D9 => Mask::D9_RATIO_EN, + } + } + + pub fn on_off_for(channel: Channel) -> Mask { + match channel { + Channel::D1 => Mask::D1_ON, + Channel::D2 => Mask::D2_ON, + Channel::D3 => Mask::D3_ON, + Channel::D4 => Mask::D4_ON, + Channel::D5 => Mask::D5_ON, + Channel::D6 => Mask::D6_ON, + Channel::D7 => Mask::D7_ON, + Channel::D8 => Mask::D8_ON, + Channel::D9 => Mask::D9_ON, + } + } +} diff --git a/src/program.rs b/src/program.rs new file mode 100644 index 0000000..8fe91a7 --- /dev/null +++ b/src/program.rs @@ -0,0 +1,373 @@ +use crate::Channel; + +/// Maximum number of instructions supported by programming engine. +/// +/// There are 6 pages, each with 16 instructions. +pub const MAX_INSTRUCTIONS: u8 = 96; +/// Number of instructions per page. +pub const INSTRUCTIONS_PER_PAGE: u8 = 16; +/// Number of pages supported by programming engine. +pub const MAX_PAGES: u8 = 6; +/// Number of variables supported by programming engine. +pub const MAX_VARS: u8 = 4; + +/// Programming engine variables. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[repr(u8)] +pub enum Variable { + A = 0, + B, + C, + D, +} + +/// Ramp step time span. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[repr(u8)] +pub enum PreScale { + // Cycle time of 0.488ms + CT0_488 = 0, + // Cycle time of 15.625ms + CT15_625 = 1, +} + +/// Ramp direction. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[repr(u8)] +pub enum Direction { + Up = 0, + Down = 1, +} + +/// Representation for a programming engine instruction. +/// +/// Refer to spec sections 7.6.3 through 7.6.7 +pub struct Instruction { + pub msb: u8, + pub lsb: u8, +} + +impl Instruction { + /// Word (u16) representation for an instruction. + pub fn as_u16(&self) -> u16 { + (self.msb as u16) << 8 | self.lsb as u16 + } +} + +impl From for Instruction { + /// Convert a word (u16) to an [Instruction]. + fn from(value: u16) -> Self { + Self { + msb: ((value >> 8) & 0xFF) as u8, + lsb: (value & 0xFF) as u8, + } + } +} + +impl Instruction { + // Driver instructions + + pub fn ramp( + cycle_time: PreScale, + cycles_per_step: u8, + direction: Direction, + number_of_steps: u8, + ) -> Self { + let mut msb = cycles_per_step << 1; // TODO check bounds + msb |= (cycle_time as u8) << 6; + msb |= direction as u8; + + Self { + msb, + lsb: number_of_steps, + } + } + + pub fn ramp_from_vars( + pre_scale: bool, + ascending: bool, + step_time_var: Variable, + increments_var: Variable, + ) -> Self { + let mut lsb = ((step_time_var as u8) << 2) | (increments_var as u8); + if pre_scale { + lsb |= 1 << 6; + } + if ascending { + lsb |= 1 << 5; + } + Self { + msb: 0b1000_0100, + lsb, + } + } + + pub fn set_pwm(value: u8) -> Self { + Self { + msb: 0b0100_0000, + lsb: value, + } + } + + pub fn set_pwm_from_var(var: Variable) -> Self { + Self { + msb: 0b1000_0100, + lsb: 0b0110_0000 | (var as u8), + } + } + + pub fn wait(cycle_time: PreScale, cycles: u8) -> Self { + let mut msb = cycles << 1; // TODO check bounds + msb |= (cycle_time as u8) << 6; + Self { + msb, + lsb: 0b0000_0000, + } + } + + // Mapping instructions + + /// Create LED engine-to-LED mapping instruction. + /// + /// Associates the supplied [channels](Channel) with the the active engine. + /// This information is not present in the main spec, but can be found in the + /// [LP55231 evaluation kit](https://www.ti.com/lit/ug/snvu214b/snvu214b.pdf) + /// User's Guide, on page 23. + /// + /// |Bit |15|14|13|12|11|10|09|08|07|06|05|04|03|02|01|00| + /// |-------|--|--|--|--|--|--|--|--|--|--|--|--|--|--|--|--| + /// |Channel| -| -| -| -| -| -| -|D9|D8|D7|D6|D5|D4|D3|D2|D1| + pub fn map_channels(channels: &[Channel]) -> Self { + let mut map_bits = 0b0000_0000_0000_0000; + for channel in channels.iter() { + map_bits |= 1 << (*channel as u8); + } + Self::from(map_bits) + } + + pub fn mux_ld_start(sram_address: u8) -> Self { + Self { + msb: 0b1001_1110, + lsb: check_addr(sram_address), + } + } + + pub fn mux_map_start(sram_address: u8) -> Self { + Self { + msb: 0b1001_1100, + lsb: check_addr(sram_address), + } + } + + pub fn mux_ld_end(sram_address: u8) -> Self { + Self { + msb: 0b1001_1100, + lsb: 0b1000_0000 | check_addr(sram_address), + } + } + + pub fn mux_sel(led_select: u8) -> Self { + Self { + msb: 0b1001_1101, + lsb: led_select, + } + } + + pub fn mux_clr() -> Self { + Self { + msb: 0b1001_1101, + lsb: 0b0000_0000, + } + } + + pub fn mux_map_next() -> Self { + Self { + msb: 0b1001_1101, + lsb: 0b1000_0000, + } + } + + pub fn mux_map_prev() -> Self { + Self { + msb: 0b1001_1101, + lsb: 0b1100_0000, + } + } + + pub fn mux_ld_next() -> Self { + Self { + msb: 0b1001_1101, + lsb: 0b1000_0001, + } + } + + pub fn mux_ld_prev() -> Self { + Self { + msb: 0b1001_1101, + lsb: 0b1100_0001, + } + } + + pub fn mux_ld_addr(sram_address: u8) -> Self { + Self { + msb: 0b1001_1111, + lsb: check_addr(sram_address), + } + } + + pub fn mux_map_addr(sram_address: u8) -> Self { + Self { + msb: 0b1001_1111, + lsb: 0b1000_0000 | check_addr(sram_address), + } + } + + // Branch instructions + + pub fn rst() -> Self { + Self { + msb: 0b0000_0000, + lsb: 0b0000_0000, + } + } + + pub fn branch(step_number: u8, loop_count: u8) -> Self { + let mut bits: u16 = 0b1010_0000_0000_0000; + bits |= step_number as u16; // TODO validate bounds + bits |= (loop_count as u16) << 7; // TODO validate bounds + Self::from(bits) + } + + pub fn branch_vars(step_number: u8, loop_count_var: Variable) -> Self { + let mut bits: u16 = 0b1000_0110_0000_0000; + bits |= loop_count_var as u16; + bits |= (step_number as u16) << 2; // TODO check bounds + Self::from(bits) + } + + pub fn int() -> Self { + Self { + msb: 0b1100_0100, + lsb: 0b0000_0000, + } + } + + pub fn end(interrupt: bool, reset_program_counter: bool) -> Self { + let mut msb = 0b1100_0000; + if interrupt { + msb |= 1 << 4; + } + if reset_program_counter { + msb |= 1 << 3 + } + Self { + msb, + lsb: 0b0000_0000, + } + } + + pub fn jne( + num_instructions_to_skip: u8, + var_1: Variable, + var_2: Variable, + ) -> Self { + let mut instr: u16 = 0b1000_1000_0000_0000; + instr |= var_1 as u16; + instr |= (var_2 as u16) << 2; + instr |= (num_instructions_to_skip as u16) << 4; // TODO check bounds. + + Self::from(instr) + } + + pub fn jl( + num_instructions_to_skip: u8, + var_1: Variable, + var_2: Variable, + ) -> Self { + let mut instr: u16 = 0b1000_1010_0000_0000; + instr |= var_2 as u16; + instr |= (var_1 as u16) << 2; + instr |= (num_instructions_to_skip as u16) << 4; // TODO check bounds. + + Self::from(instr) + } + + pub fn jge( + num_instructions_to_skip: u8, + var_1: Variable, + var_2: Variable, + ) -> Self { + let mut instr: u16 = 0b1000_1100_0000_0000; + instr |= var_2 as u16; + instr |= (var_1 as u16) << 2; + instr |= (num_instructions_to_skip as u16) << 4; // TODO check bounds. + + Self::from(instr) + } + + pub fn je( + num_instructions_to_skip: u8, + var_1: Variable, + var_2: Variable, + ) -> Self { + let mut instr: u16 = 0b1000_1110_0000_0000; + instr |= var_2 as u16; + instr |= (var_1 as u16) << 2; + instr |= (num_instructions_to_skip as u16) << 4; // TODO check bounds. + + Self::from(instr) + } + + pub fn ld(target_var: Variable, value: u8) -> Self { + Self { + msb: 0b1001_0000 | ((target_var as u8) << 2), + lsb: value, + } + } + + pub fn add_numerical(target_var: Variable, value: u8) -> Self { + Self { + msb: 0b1001_0001 | ((target_var as u8) << 2), + lsb: value, + } + } + + pub fn add_vars( + target_var: Variable, + var_1: Variable, + var_2: Variable, + ) -> Self { + Self { + msb: 0b1001_0011 | ((target_var as u8) << 2), + lsb: ((var_1 as u8) << 2) | (var_2 as u8), + } + } + + pub fn sub_numerical(target_var: Variable, value: Variable) -> Self { + Self { + msb: 0b1001_0010 | ((target_var as u8) << 2), + lsb: value as u8, + } + } + + pub fn sub_vars( + target_var: Variable, + var_1: Variable, + var_2: Variable, + ) -> Self { + Self { + msb: 0b1001_0011 | ((target_var as u8) << 2), + lsb: 0b0001_0000 | ((var_1 as u8) << 2) | (var_2 as u8), + } + } +} + +fn check_addr(addr: u8) -> u8 { + if addr > MAX_INSTRUCTIONS { + panic!( + "invalid sram_address {} - max is {}", + addr, MAX_INSTRUCTIONS + ) + } + addr +} diff --git a/src/register.rs b/src/register.rs new file mode 100644 index 0000000..7fb3410 --- /dev/null +++ b/src/register.rs @@ -0,0 +1,138 @@ +use crate::types::{Channel, Engine, Fader}; + +// I2C registers. +#[allow(dead_code, non_camel_case_types, clippy::upper_case_acronyms)] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[repr(u8)] +pub enum Register { + ENABLE_ENGINE_CNTRL1 = 0x00, + ENGINE_CNTRL_2 = 0x01, + OUTPUT_DIRECT_RATIOMETRIC_MSB = 0x02, + OUTPUT_DIRECT_RATIOMETRIC_LSB = 0x03, + OUTPUT_ON_OFF_CONTROL_MSB = 0x04, + OUTPUT_ON_OFF_CONTROL_LSB = 0x05, + D1_CONTROL = 0x06, + D2_CONTROL = 0x07, + D3_CONTROL = 0x08, + D4_CONTROL = 0x09, + D5_CONTROL = 0x0A, + D6_CONTROL = 0x0B, + D7_CONTROL = 0x0C, + D8_CONTROL = 0x0D, + D9_CONTROL = 0x0E, + // 0f to 15 reserved + D1_PWM = 0x16, + D2_PWM = 0x17, + D3_PWM = 0x18, + D4_PWM = 0x19, + D5_PWM = 0x1A, + D6_PWM = 0x1B, + D7_PWM = 0x1C, + D8_PWM = 0x1D, + D9_PWM = 0x1E, + // 1f to 25 reserved + D1_CURRENT_CONTROL = 0x26, + D2_CURRENT_CONTROL = 0x27, + D3_CURRENT_CONTROL = 0x28, + D4_CURRENT_CONTROL = 0x29, + D5_CURRENT_CONTROL = 0x2A, + D6_CURRENT_CONTROL = 0x2B, + D7_CURRENT_CONTROL = 0x2C, + D8_CURRENT_CONTROL = 0x2D, + D9_CURRENT_CONTROL = 0x2E, + // 2f to 35 reserved + MISC = 0x36, + ENGINE1_PC = 0x37, + ENGINE2_PC = 0x38, + ENGINE3_PC = 0x39, + STATUS_INTERRUPT = 0x3A, + INT_GPO = 0x3B, + VARIABLE = 0x3C, + RESET = 0x3D, + TEMP_ADC_CONTROL = 0x3E, + TEMPERATURE_READ = 0x3F, + TEMPERATURE_WRITE = 0x40, + LED_TEST_CONTROL = 0x41, + LED_TEST_ADC = 0x42, + // 43 and 44 reserved + ENGINE1_VARIABLE_A = 0x45, + ENGINE1_VARIABLE_B = 0x46, + ENGINE1_VARIABLE_C = 0x47, + MASTER_FADER1 = 0x48, + MASTER_FADER2 = 0x49, + MASTER_FADER3 = 0x4A, + // 4b reserved + ENG1_PROG_START_ADDR = 0x4C, + ENG2_PROG_START_ADDR = 0x4D, + ENG3_PROG_START_ADDR = 0x4E, + PROG_MEM_PAGE_SEL = 0x4F, + PROG_MEM_BASE = 0x50, +} + +impl Register { + pub fn control_for(channel: Channel) -> Register { + match channel { + Channel::D1 => Register::D1_CONTROL, + Channel::D2 => Register::D2_CONTROL, + Channel::D3 => Register::D3_CONTROL, + Channel::D4 => Register::D4_CONTROL, + Channel::D5 => Register::D5_CONTROL, + Channel::D6 => Register::D6_CONTROL, + Channel::D7 => Register::D7_CONTROL, + Channel::D8 => Register::D8_CONTROL, + Channel::D9 => Register::D9_CONTROL, + } + } + + pub fn pwm_for(channel: Channel) -> Register { + match channel { + Channel::D1 => Register::D1_PWM, + Channel::D2 => Register::D2_PWM, + Channel::D3 => Register::D3_PWM, + Channel::D4 => Register::D4_PWM, + Channel::D5 => Register::D5_PWM, + Channel::D6 => Register::D6_PWM, + Channel::D7 => Register::D7_PWM, + Channel::D8 => Register::D8_PWM, + Channel::D9 => Register::D9_PWM, + } + } + + pub fn current_control_for(channel: Channel) -> Register { + match channel { + Channel::D1 => Register::D1_CURRENT_CONTROL, + Channel::D2 => Register::D2_CURRENT_CONTROL, + Channel::D3 => Register::D3_CURRENT_CONTROL, + Channel::D4 => Register::D4_CURRENT_CONTROL, + Channel::D5 => Register::D5_CURRENT_CONTROL, + Channel::D6 => Register::D6_CURRENT_CONTROL, + Channel::D7 => Register::D7_CURRENT_CONTROL, + Channel::D8 => Register::D8_CURRENT_CONTROL, + Channel::D9 => Register::D9_CURRENT_CONTROL, + } + } + + pub fn intensity_for(fader: Fader) -> Register { + match fader { + Fader::F1 => Register::MASTER_FADER1, + Fader::F2 => Register::MASTER_FADER2, + Fader::F3 => Register::MASTER_FADER3, + } + } + + pub fn program_start_for(engine: Engine) -> Register { + match engine { + Engine::E1 => Register::ENG1_PROG_START_ADDR, + Engine::E2 => Register::ENG2_PROG_START_ADDR, + Engine::E3 => Register::ENG3_PROG_START_ADDR, + } + } + + pub fn program_counter_for(engine: Engine) -> Register { + match engine { + Engine::E1 => Register::ENGINE1_PC, + Engine::E2 => Register::ENGINE2_PC, + Engine::E3 => Register::ENGINE3_PC, + } + } +} diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 0000000..fe4dfae --- /dev/null +++ b/src/types.rs @@ -0,0 +1,104 @@ +/// Output channels. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum Channel { + D1 = 0, + D2, + D3, + D4, + D5, + D6, + D7, + D8, + D9, +} + +/// Master faders. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum Fader { + F1 = 0, + F2, + F3, +} + +/// Programming engines. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum Engine { + E1 = 0, + E2, + E3, +} + +/// Engine execution control modes. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum EngineExec { + Hold = 0, + Step, + Free, + ExecuteOnce, +} + +/// Engine modes (i.e. state). +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum EngineMode { + Disabled = 0, + LoadProgram, + RunProgram, + Halt, +} + +/// Charge pump modes. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum ChargePumpMode { + Off = 0, + Bypass, + Boosted, + Auto, +} + +impl From for ChargePumpMode { + fn from(value: u8) -> Self { + match value { + 0b00 => Self::Off, + 0b01 => Self::Bypass, + 0b10 => Self::Boosted, + 0b11 => Self::Auto, + _ => panic!("invalid value for ChargePumpMode {:b}", value), + } + } +} + +/// IC clock selection. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum ClockSelection { + ForceExternal = 0, + ForceInternal, + Automatic, + PreferInternal, +} + +impl From for ClockSelection { + fn from(value: u8) -> Self { + match value { + 0b00 => Self::ForceExternal, + 0b01 => Self::ForceInternal, + 0b10 => Self::Automatic, + 0b11 => Self::PreferInternal, + _ => panic!("invalid value for ClockSelection {:b}", value), + } + } +} + +/// Miscellaneous settings. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub struct Misc { + /// EN_AUTO_INCR + pub auto_increment_enabled: bool, + /// POWERSAVE_EN + pub powersave_enabled: bool, + /// CHARGE_PUMP_EN + pub charge_pump_mode: ChargePumpMode, + /// PWM_PS_EN + pub pwm_powersave_enabled: bool, + /// CLK_DET_EN and INT_CLK_EN + pub clock_selection: ClockSelection, +}