diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..9392bae --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +max_line_length = 80 +indent_style = space +indent_size = 4 +insert_final_newline = true +trim_trailing_whitespace = true + +[Makefile] +indent_style = tab + +[*.{json,md,nix,toml,yml}] +indent_size = 2 diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..aa600ae --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +github: phip1611 diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..9ab178d --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,15 @@ +version: 2 +updates: + - package-ecosystem: cargo + directory: "/" + schedule: + interval: monthly + open-pull-requests-limit: 10 + ignore: + - dependency-name: "*" + update-types: [ "version-update:semver-patch" ] + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: monthly + open-pull-requests-limit: 10 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index e9c2e9a..0000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,73 +0,0 @@ -name: Build - -on: - push: - pull_request: - schedule: - - cron: '40 4 * * *' # every day at 4:40 - -jobs: - test: - name: "Test" - - strategy: - matrix: - platform: [ - ubuntu-latest, - macos-latest, - windows-latest - ] - - runs-on: ${{ matrix.platform }} - timeout-minutes: 15 - - steps: - - name: "Checkout Repository" - uses: actions/checkout@v1 - - - name: "Print Rust Version" - run: | - rustc -Vv - cargo -Vv - - - name: "Run cargo build (x86) [nightly]" - run: | - rustup toolchain add nightly-2023-07-09 - rustup component add rust-src --toolchain nightly-2023-07-09 - cargo +nightly-2023-07-09 build --target test/x86-unknown-none.json -Z build-std=core,alloc,compiler_builtins -Z build-std-features=compiler-builtins-mem - - - name: "Run cargo build (x86_64)" - run: | - rustup target add x86_64-unknown-none - cargo build --target x86_64-unknown-none - - - name: "Run cargo build (aarch64)" - run: | - rustup target add aarch64-unknown-none - cargo build --target aarch64-unknown-none - - - name: "Run cargo test" - run: cargo test - - - name: "Run cargo build for stable" - run: cargo build --no-default-features --features stable - if: runner.os != 'Windows' - - - name: "Run cargo test for stable" - run: cargo test --no-default-features --features stable - if: runner.os != 'Windows' - - - name: "Run cargo doc" - run: cargo doc - - - name: 'Deny Warnings' - run: cargo rustc -- -D warnings - - check_formatting: - name: "Check Formatting" - runs-on: ubuntu-latest - timeout-minutes: 2 - steps: - - uses: actions/checkout@v1 - - run: rustup toolchain install nightly --profile minimal --component rustfmt - - run: cargo +nightly fmt -- --check diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml new file mode 100644 index 0000000..89ccc0e --- /dev/null +++ b/.github/workflows/dependabot-auto-merge.yml @@ -0,0 +1,26 @@ +# We auto-merge all dependabot updates as soon as the CI are met. +# +# More info: https://docs.github.com/en/code-security/dependabot/working-with-dependabot/automating-dependabot-with-github-actions + +name: Dependabot PR auto-merge +on: pull_request + +permissions: + contents: write + pull-requests: write + +jobs: + dependabot: + runs-on: ubuntu-latest + if: github.event.pull_request.user.login == 'dependabot[bot]' && github.repository == 'rust-osdev/uart_16550' + steps: + - name: Approve + run: gh pr review --approve "$PR_URL" + env: + PR_URL: ${{github.event.pull_request.html_url}} + GH_TOKEN: ${{secrets.GITHUB_TOKEN}} + - name: Enable auto-merge + run: gh pr merge --auto --merge "$PR_URL" + env: + PR_URL: ${{github.event.pull_request.html_url}} + GH_TOKEN: ${{secrets.GITHUB_TOKEN}} diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml new file mode 100644 index 0000000..636fbf2 --- /dev/null +++ b/.github/workflows/qa.yml @@ -0,0 +1,12 @@ +name: QA + +on: [ push, pull_request, merge_group ] + +jobs: + spellcheck: + name: Spellcheck + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + # Executes "typos ." + - uses: crate-ci/typos@v1.41.0 diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000..0860c93 --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,110 @@ +name: Build + +on: [ push, pull_request, merge_group ] + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + runs-on: "${{ matrix.runs-on }}" + strategy: + fail-fast: false + matrix: + runs-on: + - ubuntu-latest + - windows-latest + rust: + - stable + - nightly + - 1.85.1 # MSVR + steps: + - uses: actions/checkout@v6 + - name: Setup Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + toolchain: "${{ matrix.rust }}" + targets: x86_64-unknown-linux-gnu,x86_64-unknown-uefi,i686-unknown-uefi,thumbv7em-none-eabihf + - uses: Swatinem/rust-cache@v2 + with: + key: "${{ matrix.runs-on }}-${{ matrix.rust }}" + - name: Build + run: cargo build --all-targets --verbose + - name: Build (no_std) + run: | + cargo build --verbose --target x86_64-unknown-uefi + cargo build --verbose --target i686-unknown-uefi + - name: Build (no_std, aarch64) + run: cargo build --verbose --target thumbv7em-none-eabihf + - name: Run tests + run: cargo test --verbose + + integration_test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Setup Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + - uses: Swatinem/rust-cache@v2 + with: + key: "${{ matrix.runs-on }}-${{ matrix.rust }}" + cache-directories: + ./test/target + - name: Install QEMU + run: | + sudo apt-get update + sudo apt-get install -y qemu-system-i386 + + - name: Verify QEMU installation + run: qemu-system-i386 --version + - name: Build + run: cd test && make + - name: Run + run: cd test && make run + + miri: + runs-on: "${{ matrix.runs-on }}" + needs: + # Logical dependency and wait for cache to be present + - build + strategy: + matrix: + runs-on: + - ubuntu-latest + rust: + - nightly + steps: + - uses: actions/checkout@v6 + - name: Setup Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + toolchain: "${{ matrix.rust }}" + - uses: Swatinem/rust-cache@v2 + with: + key: "${{ matrix.runs-on }}-${{ matrix.rust }}" + - run: rustup component add miri + - run: cargo miri test --tests + + style_checks: + runs-on: ubuntu-latest + strategy: + matrix: + rust: + - stable + steps: + - uses: actions/checkout@v6 + - name: Setup Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + toolchain: "${{ matrix.rust }}" + - uses: Swatinem/rust-cache@v2 + with: + key: "${{ matrix.runs-on }}-${{ matrix.rust }}" + - name: Rustfmt + run: cargo fmt --all -- --check + - name: Clippy + run: cargo clippy --all-targets + - name: Rustdoc + run: cargo doc --no-deps --document-private-items diff --git a/.gitignore b/.gitignore index 2ebc5ea..ea8c4bf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1 @@ /target -/Cargo.lock \ No newline at end of file diff --git a/.typos.toml b/.typos.toml new file mode 100644 index 0000000..d39b71d --- /dev/null +++ b/.typos.toml @@ -0,0 +1,17 @@ +# Configuration for the typos spell checker utility (). + +[files] +extend-exclude = [ + "test/link.ld" +] + +[default] +extend-ignore-identifiers-re = [ + +] + +[default.extend-words] +THR = "THR" + +[default.extend-identifiers] + diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..cfdaf56 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,16 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "uart_16550" +version = "0.1.0" +dependencies = [ + "bitflags", +] diff --git a/Cargo.toml b/Cargo.toml index 3089c39..380a695 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,30 +1,25 @@ [package] name = "uart_16550" -version = "0.4.0" -authors = ["Lachlan Sneff ", "Philipp Oppermann "] -description = "Minimal support for uart_16550 serial output." -license = "MIT" +description = """ +Simple yet highly configurable low-level driver for 16550 UART devices, +typically known and used as serial ports or COM ports. Easy integration into +Rust while providing fine-grained control where needed (e.g., for kernel +drivers). +""" +version = "0.1.0" +edition = "2024" +rust-version = "1.85.1" +keywords = ["uart-16550", "serial", "com", "driver", "tty"] +categories = ["embedded", "hardware-support", "no-std", "no-std::no-alloc"] +readme = "README.md" +license = "MIT OR Apache-2.0" +homepage = "https://github.com/rust-osdev/uart_16550" repository = "https://github.com/rust-osdev/uart_16550" -edition = "2018" - -[dependencies] -bitflags = "2" -rustversion = "1.0.5" - -[target.'cfg(any(target_arch = "x86", target_arch = "x86_64"))'.dependencies] -x86 = "0.52" - -[features] -default = [] -# TOOD: Remove these deprecated features on next breaking release -stable = [] -nightly = [] - -[package.metadata.release] -pre-release-replacements = [ - { file="Changelog.md", search="# Unreleased", replace="# Unreleased\n\n# {{version}} – {{date}}", exactly=1 }, +documentation = "https://docs.rs/uart_16550" +authors = [ + "Philipp Schuster ", ] -pre-release-commit-message = "Release version {{version}}" +resolver = "3" -[package.metadata.docs.rs] -rustdoc-args = ["--cfg", "docsrs"] +[dependencies] +bitflags = { version = "2.10", default-features = false, features = [] } diff --git a/Changelog.md b/Changelog.md deleted file mode 100644 index eee1f46..0000000 --- a/Changelog.md +++ /dev/null @@ -1,98 +0,0 @@ -# Unreleased - -# 0.4.0 – 2025-07-24 - -- [Update `send` function to replace `\n` with `\r\n`](https://github.com/rust-osdev/uart_16550/pull/40) - -# 0.3.2 – 2024-11-13 - -- Add `MmioSerialPort::new_with_stride` function ([#36](https://github.com/rust-osdev/uart_16550/pull/36)) - -# 0.3.1 – 2024-07-11 - -- Add `try_send_raw` and `try_receive` ([#34](https://github.com/rust-osdev/uart_16550/pull/34)) -- Update bitflags dependency to version 2 ([#33](https://github.com/rust-osdev/uart_16550/pull/33)) - -# 0.3.0 – 2023-08-04 - -- Internal rewrite of port operations to work on both `x86` and `x86_64` ([#29](https://github.com/rust-osdev/uart_16550/pull/29)) - -# 0.2.19 – 2023-07-07 - -- Make crate usable for 32-bit `x86` ([#28](https://github.com/rust-osdev/uart_16550/pull/28)) - -# 0.2.18 – 2022-04-16 - -- Remove use of `stable` and `nightly` features ([#24](https://github.com/rust-osdev/uart_16550/pull/24)) - -# 0.2.17 – 2022-03-28 - -- Remove stabilized nightly feature 'const_ptr_offset' ([#22](https://github.com/rust-osdev/uart_16550/pull/22)) - -# 0.2.16 – 2022-01-08 - -- Add `send_raw()` function to allow sending arbitrary binary data using the serial port ([#21](https://github.com/rust-osdev/uart_16550/pull/21)) - -# 0.2.15 – 2021-06-06 - -- Add support for memory mapped UARTs ([#15](https://github.com/rust-osdev/uart_16550/pull/15)) -- Improvements to new MMIO support ([#18](https://github.com/rust-osdev/uart_16550/pull/18)) - -# 0.2.14 – 2021-05-14 - -- `SerialPort::new()` no longer requires `nightly` feature ([#16](https://github.com/rust-osdev/uart_16550/pull/16)) - -# 0.2.13 – 2021-04-30 - -- Update x86_64 dependency and make it more robust ([#14](https://github.com/rust-osdev/uart_16550/pull/14)) - -# 0.2.12 – 2021-02-02 - -- Fix build on nightly by updating to x86_64 v0.13.2 ([#12](https://github.com/rust-osdev/uart_16550/pull/12)) - -# 0.2.11 – 2021-01-15 - -- Use stabilized `hint::spin_loop` instead of deprecated `atomic::spin_loop_hint` - -# 0.2.10 – 2020-10-01 - -- Fix default feature breakage ([#11](https://github.com/rust-osdev/uart_16550/pull/11)) - -# 0.2.9 – 2020-09-29 - -- Update `x86_64` dependency to version `0.12.2` - -# 0.2.8 – 2020-09-24 - -- Update `x86_64` dependency to version `0.12.1` - -# 0.2.7 - -- Update `x86_64` dependency to version `0.11.0` - -# 0.2.6 - -- Use `spin_loop_hint` while waiting for data ([#9](https://github.com/rust-osdev/uart_16550/pull/9)) -- Update `x86_64` dependency to version `0.10.2` - -# 0.2.5 - -- Support receiving bytes from serial ports ([#8](https://github.com/rust-osdev/uart_16550/pull/8)) - -# 0.2.4 - -- Enable usage with non-nightly rust ([#7](https://github.com/rust-osdev/uart_16550/pull/7)) - -# 0.2.3 - -- Cargo.toml: update x86_64 dependency ([#5](https://github.com/rust-osdev/uart_16550/pull/5)) -- Switch CI to GitHub Actions ([#6](https://github.com/rust-osdev/uart_16550/pull/6)) - -# 0.2.2 - -- Update internal x86_64 dependency to version 0.8.3 ([#4](https://github.com/rust-osdev/uart_16550/pull/4)) - -# 0.2.1 - -- Update to x86_64 0.7.3 and bitflags 1.1.0 ([#1](https://github.com/rust-osdev/uart_16550/pull/1)) -- Document how serial port is configured by default ([#2](https://github.com/rust-osdev/uart_16550/pull/1)) diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/LICENSE b/LICENSE-MIT similarity index 93% rename from LICENSE rename to LICENSE-MIT index 1b6b3fe..500b29c 100644 --- a/LICENSE +++ b/LICENSE-MIT @@ -1,7 +1,6 @@ MIT License -Copyright (c) 2019 Lachlan Sneff -Copyright (c) 2019 Philipp Oppermann +Copyright (c) 2025 Philipp Schuster Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index e876d27..ef68153 100644 --- a/README.md +++ b/README.md @@ -1,57 +1,86 @@ # uart_16550 -[![Build Status](https://github.com/rust-osdev/uart_16550/workflows/Build/badge.svg)](https://github.com/rust-osdev/uart_16550/actions?query=workflow%3ABuild) [![Docs.rs Badge](https://docs.rs/uart_16550/badge.svg)](https://docs.rs/uart_16550/) +Simple yet highly configurable low-level driver for +[16550 UART devices][uart], typically known and used as serial ports or +COM ports. Easy integration into Rust while providing fine-grained control +where needed (e.g., for kernel drivers). -Minimal support for [serial communication](https://en.wikipedia.org/wiki/Asynchronous_serial_communication) through [UART](https://en.wikipedia.org/wiki/Universal_asynchronous_receiver-transmitter) devices, which are compatible to the [16550 UART](https://en.wikipedia.org/wiki/16550_UART). This crate supports I/O port-mapped (x86 only) and memory-mapped UARTS. +The "serial device" or "COM port" in typical x86 machines is almost always +backed by a **16550 UART devices**, may it be physical or emulated. This +crate offers convenient and powerful abstractions for these devices, and +also works for other architectures, such as ARM or RISC-V, by offering +support for MMIO-mapped devices. -## Usage +Serial ports are especially useful for debugging or operating system +learning projects. See [`Uart16550`] to get started. -Depending on the system architecture, the UART can be either accessed through [port-mapped I/O](https://wiki.osdev.org/Port_IO) or [memory-mapped I/O](https://en.wikipedia.org/wiki/Memory-mapped_I/O). +## Features -### With port-mappd I/O +- ✅ Full configure, transmit, receive, and interrupt support for UART + 16550–compatible devices +- ✅ High-level, ergonomic abstractions and types paired with support for + plain integers +- ✅ Very easy to integrate, highly configurable when needed +- ✅ Validated on **real hardware** as well as across different virtual + machines +- ✅ Fully type-safe and derived directly from the official + [specification][uart] +- ✅ Supports both **x86 port-mapped I/O** and **memory-mapped I/O** (MMIO) +- ✅ `no_std`-compatible and allocation-free by design -The UART is accessed through port-mapped I/O on architectures such as `x86_64`. On these architectures, the [`SerialPort`](https://docs.rs/uart_16550/~0.2/uart_16550/struct.SerialPort.html) type can be used: +## Focus, Scope & Limitations +While serial ports are often used in conjunction with VT102-like terminal +emulation, the primary focus of this crate is strict specification +compliance and convenient direct access to the underlying hardware for +transmitting and receiving bytes, including all necessary device +configuration. -```rust -use uart_16550::SerialPort; +For basic terminal-related functionality, such as newline normalization and +backspace handling, we provide `Uart16550Tty` as a **basic** convenience +layer. -const SERIAL_IO_PORT: u16 = 0x3F8; +# Overview -let mut serial_port = unsafe { SerialPort::new(SERIAL_IO_PORT) }; -serial_port.init(); +Use `Uart16550Tty` for a quick start. For more fine-grained low-level +control, please have a look at `Uart16550` instead. -// Now the serial port is ready to be used. To send a byte: -serial_port.send(42); +# Example (Minimalistic) -// To receive a byte: -let data = serial_port.receive(); +```rust +use uart_16550::{Config, Uart16550Tty}; +use core::fmt::Write; + +fn main() { + // SAFETY: The I/O port is valid and we have exclusive access. + let mut uart = unsafe { Uart16550Tty::new_port(0x3f8, Config::default()).expect("should initialize device") }; + // ^ you could also use `new_mmio(0x1000 as *mut _)` here + uart.write_str("hello world\nhow's it going?"); +} ``` -### With memory mapped serial port - -Most other architectures, such as [RISC-V](https://en.wikipedia.org/wiki/RISC-V), use memory-mapped I/O for accessing the UARTs. On these architectures, the [`MmioSerialPort`](https://docs.rs/uart_16550/~0.2/uart_16550/struct.MmioSerialPort.html) type can be used: +# Example (More low-level control) ```rust -use uart_16550::MmioSerialPort; - -const SERIAL_PORT_BASE_ADDRESS: usize = 0x1000_0000; - -let mut serial_port = unsafe { MmioSerialPort::new(SERIAL_PORT_BASE_ADDRESS) }; -serial_port.init(); - -// Now the serial port is ready to be used. To send a byte: -serial_port.send(42); - -// To receive a byte: -let data = serial_port.receive(); +use uart_16550::{Config, Uart16550}; + +fn main() { + // SAFETY: The I/O port is valid and we have exclusive access. + let mut uart = unsafe { Uart16550::new_port(0x3f8).expect("should be valid port") }; + // ^ you could also use `new_mmio(0x1000 as *mut _)` here + uart.init(Config::default()).expect("should init device successfully"); + uart.test_loopback().expect("should have working loopback mode"); + uart.check_remote_ready_to_receive().expect("should have physically connected receiver"); + uart.send_bytes_all(b"hello world!"); +} ``` -## Building with stable rust +## License -This needs to have the [compile-time requirements](https://github.com/alexcrichton/cc-rs#compile-time-requirements) of the `cc` crate installed on your system. -It was currently only tested on Linux and MacOS. +This project is licensed under either of + +- MIT license ([LICENSE-MIT](LICENSE-MIT)) +- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE)) -## License -Licensed under the MIT license ([LICENSE](LICENSE) or ). +[uart]: https://en.wikipedia.org/wiki/16550_UART diff --git a/rust-toolchain b/rust-toolchain deleted file mode 100644 index 07ade69..0000000 --- a/rust-toolchain +++ /dev/null @@ -1 +0,0 @@ -nightly \ No newline at end of file diff --git a/src/backend.rs b/src/backend.rs new file mode 100644 index 0000000..429ebab --- /dev/null +++ b/src/backend.rs @@ -0,0 +1,227 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 + +//! Abstraction over the I/O backend (Hardware Abstraction Layer (HAL)). +//! +//! Main exports: +//! - [`Backend`] +//! - [`PioBackend`] +//! - [`MmioBackend`] + +use crate::spec::NUM_REGISTERS; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +use core::arch::asm; +use core::fmt::Debug; +use core::ptr::{self, read_volatile, write_volatile}; + +/// Abstraction over register addresses in [`Backend`]. +pub trait RegisterAddress: Copy + Clone + Debug + Sized { + /// Adds a byte offset onto the base register address. + fn add_offset(self, offset: u8) -> Self; +} + +/// x86 port I/O address. +/// +/// See [`RegisterAddress`]. +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Hash)] +pub struct PortIoAddress(pub(crate) u16); + +/// Memory-mapped I/O (MMIO) address. +/// +/// See [`RegisterAddress`]. +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Hash)] +pub struct MmioAddress(pub(crate) *mut u8); + +impl Default for MmioAddress { + fn default() -> Self { + Self(ptr::null_mut()) + } +} + +// SAFETY: We allow moving between threads. +unsafe impl Send for MmioAddress {} + +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +impl RegisterAddress for PortIoAddress { + #[inline(always)] + fn add_offset(self, offset: u8) -> Self { + let port = self.0 + offset as u16; + Self(port) + } +} + +impl RegisterAddress for MmioAddress { + #[inline(always)] + fn add_offset(self, offset: u8) -> Self { + // SAFETY: We ensure on a higher level that the base address is valid + // and that this will not wrap. + let address = unsafe { self.0.add(offset as usize) }; + Self(address) + } +} + +#[track_caller] +fn assert_offset(offset: u8) { + assert!( + offset < NUM_REGISTERS as u8, + "the offset should be within the expected range: expected {offset} to be less than {NUM_REGISTERS}", + ); +} + +/// Abstraction over the I/O backend of a UART 16550 microcontroller. +/// +/// This acts as Hardware Abstraction Layer (HAL) and abstracts over x86 port +/// I/O and generic MMIO. +/// +/// Users should directly use [`Backend::read`] and[`Backend::write`]. +pub trait Backend: Send { + /// The [`RegisterAddress`] that naturally belongs to the [`Backend`]. + type Address: RegisterAddress; + + /* convenience with default impl */ + + /// Reads one byte from the specified register at the given offset. + /// + /// This needs a mutable reference as reads can have side effects on the + /// device, depending on the register. + /// + /// # Arguments + /// + /// - `offset`: The register offset regarding the base register. The offset + /// **must** be less than [`NUM_REGISTERS`]. + /// + /// # Safety + /// + /// Callers must ensure that the effective address consisting of + /// [`Self::base`] and `offset` is valid and safe to read. + #[inline(always)] + unsafe fn read(&mut self, offset: u8) -> u8 { + assert_offset(offset); + let addr = self.base().add_offset(offset); + // SAFETY: The caller ensured that the register address is safe to use. + unsafe { self._read_register(addr) } + } + + /// Writes one byte to the specified register at the given offset. + /// + /// Writes can have side effects on the device, depending on the register. + /// + /// # Arguments + /// + /// - `offset`: The register offset regarding the base register. The offset + /// **must** be less than [`NUM_REGISTERS`]. + /// + /// # Safety + /// + /// Callers must ensure that the effective address consisting of + /// [`Self::base`] and `offset` is valid and safe to write. + #[inline(always)] + unsafe fn write(&mut self, offset: u8, value: u8) { + assert_offset(offset); + let addr = self.base().add_offset(offset); + // SAFETY: The caller ensured that the register address is safe to use. + unsafe { self._write_register(addr, value) } + } + + /* needs impl */ + + /// Returns the base [`RegisterAddress`]. + fn base(&self) -> Self::Address; + + /// PRIVATE API! + /// + /// Reads one byte from the specified register. + /// + /// This needs a mutable reference as reads can have side effects on the + /// device, depending on the register. + /// + /// # Arguments + /// + /// - `address`: The total address of the register. + /// + /// # Safety + /// + /// Callers must ensure that the provided address is valid and safe to read. + unsafe fn _read_register(&mut self, address: Self::Address) -> u8; + + /// PRIVATE API! + /// + /// Writes one byte to the specified register. + /// + /// Writes can have side effects on the device, depending on the register. + /// + /// # Arguments + /// + /// - `address`: The total address of the register. + /// + /// # Safety + /// + /// Callers must ensure that the provided address is valid and safe to write. + unsafe fn _write_register(&mut self, address: Self::Address, value: u8); +} + +/// x86 Port I/O backed UART 16550. +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Hash)] +pub struct PioBackend(pub(crate) PortIoAddress /* base port */); + +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +impl Backend for PioBackend { + type Address = PortIoAddress; + + fn base(&self) -> Self::Address { + self.0 + } + + #[inline(always)] + unsafe fn _read_register(&mut self, port: PortIoAddress) -> u8 { + // SAFETY: The caller ensured that the I/O port is safe to use. + unsafe { + let ret: u8; + asm!( + "inb %dx, %al", + in("dx") port.0, + out("al") ret, + options(att_syntax, nostack, preserves_flags) + ); + ret + } + } + + #[inline(always)] + unsafe fn _write_register(&mut self, port: PortIoAddress, value: u8) { + // SAFETY: The caller ensured that the I/O port is safe to use. + unsafe { + asm!( + "outb %al, %dx", + in("al") value, + in("dx") port.0, + options(att_syntax, nostack, preserves_flags) + ); + } + } +} + +/// MMIO-mapped UART 16550. +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Hash)] +pub struct MmioBackend(pub(crate) MmioAddress /* base address, non-null */); + +impl Backend for MmioBackend { + type Address = MmioAddress; + + fn base(&self) -> Self::Address { + self.0 + } + + #[inline(always)] + unsafe fn _read_register(&mut self, address: MmioAddress) -> u8 { + // SAFETY: The caller ensured that the MMIO address is safe to use. + unsafe { read_volatile(address.0) } + } + + #[inline(always)] + unsafe fn _write_register(&mut self, address: MmioAddress, value: u8) { + // SAFETY: The caller ensured that the MMIO address is safe to use. + unsafe { write_volatile(address.0, value) } + } +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..bbea095 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 + +//! Configuration for [`Uart16550`]. +//! +//! [`Uart16550`]: crate::Uart16550 + +use crate::spec::CLK_FREQUENCY_HZ; +use crate::spec::registers::{FifoTriggerLevel, IER, Parity, WordLength}; +use core::cmp::Ordering; + +/// The speed of data transmission, measured in symbols (bits) per second. +/// +/// This type is a convenient and non-ABI compatible abstraction. Use +/// [`calc_divisor`] to get the divisor for [`DLL`] and [`DLM`]. +/// +/// [`DLL`]: crate::spec::registers::DLL +/// [`DLM`]: crate::spec::registers::DLM +/// [`calc_divisor`]: crate::spec::calc_divisor +#[allow(missing_docs)] +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)] +pub enum BaudRate { + // List of typical baud rates. + #[default] + Baud115200, + Baud57600, + Baud38400, + Baud9600, + Baud4800, + Baud2400, + Baud1200, + Baud600, + Baud300, + Baud150, + Baud110, + Custom(u32), +} + +impl BaudRate { + /// Returns the value as corresponding integer. + #[must_use] + pub const fn to_integer(self) -> u32 { + match self { + Self::Baud115200 => 115200, + Self::Baud57600 => 57600, + Self::Baud38400 => 38400, + Self::Baud9600 => 9600, + Self::Baud4800 => 4800, + Self::Baud2400 => 2400, + Self::Baud1200 => 1200, + Self::Baud600 => 600, + Self::Baud300 => 300, + Self::Baud150 => 150, + Self::Baud110 => 110, + Self::Custom(val) => val, + } + } + + /// Try to create the type from an integer representation of the baud rate. + #[must_use] + pub const fn from_integer(value: u32) -> Self { + match value { + 115200 => Self::Baud115200, + 57600 => Self::Baud57600, + 38400 => Self::Baud38400, + 9600 => Self::Baud9600, + 4800 => Self::Baud4800, + 2400 => Self::Baud2400, + 1200 => Self::Baud1200, + 600 => Self::Baud600, + 300 => Self::Baud300, + 150 => Self::Baud150, + 110 => Self::Baud110, + baud_rate => Self::Custom(baud_rate), + } + } +} + +impl PartialOrd for BaudRate { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for BaudRate { + fn cmp(&self, other: &Self) -> Ordering { + self.to_integer().cmp(&other.to_integer()) + } +} + +/// Configuration for a [`Uart16550`]. +/// +/// Please note that sender and receiver **must agree** on the transmission +/// settings, otherwise one side will receive garbage. +/// +/// # Default Configuration +/// +/// The [`Default`] configuration uses a [8-N-1] transmission with a baud rate +/// of [`BaudRate::Baud115200`]. It also activates the FIFO. +/// +/// [`Uart16550`]: crate::Uart16550 +/// [8-N-1]: https://en.wikipedia.org/wiki/Serial_port#Conventional_notation +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Config { + // Device Config + /// Interrupts to enable. + pub interrupts: IER, + /// The frequency which typically is [`CLK_FREQUENCY_HZ`]. + pub frequency: u32, + /// The optional prescaler division factor. + /// + /// This is a non-standard functionality (i.e., it is not present in the + /// industry standard 16550 UART). Its purpose is to provide a second + /// division factor that could be useful in systems which are driven by a + /// clock multiple of one of the typical frequencies used with this UART. + pub prescaler_division_factor: Option, + /// The [`FifoTriggerLevel`]. If this is `None`, the FIFO feature will not + /// be enabled. + pub fifo_trigger_level: Option, + + // Transmission Config + /// The baud rate to use. + pub baud_rate: BaudRate, + /// The length of each transmitted word. + pub data_bits: WordLength, + /// Whether extra stop bits should be used. + /// + /// See [`LCR::MORE_STOP_BITS`] for more info. + /// + /// [`LCR::MORE_STOP_BITS`]: crate::spec::registers::LCR::MORE_STOP_BITS + pub extra_stop_bits: bool, + /// Whether parity bits should be used. + pub parity: Parity, +} + +impl Default for Config { + // 8-N-1, 115200, FIFO enabled + fn default() -> Self { + Self { + interrupts: IER::DATA_READY, + frequency: CLK_FREQUENCY_HZ, + prescaler_division_factor: None, + fifo_trigger_level: Some(FifoTriggerLevel::Fourteen), + + baud_rate: BaudRate::Baud115200, + data_bits: WordLength::EightBits, + extra_stop_bits: false, + parity: Parity::Disabled, + } + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..7799c27 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,198 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 + +//! Errors that can happen when working with [`Uart16550`]. + +#[cfg(doc)] +use crate::Uart16550; +use crate::backend::RegisterAddress; +use crate::spec::{FIFO_SIZE, NonIntegerDivisorError}; +use core::error::Error; +use core::fmt; +use core::fmt::Display; + +/// The specified address is invalid because it is either null or doesn't allow +/// for [NUM_REGISTERS] - 1 subsequent addresses. +/// +/// [NUM_REGISTERS]: crate::spec::NUM_REGISTERS +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct InvalidAddressError(pub(crate) A); + +impl Display for InvalidAddressError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "invalid register address: {:x?}", self.0) + } +} + +impl Error for InvalidAddressError {} + +/// The loopback test failed. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum LoopbackError { + /// The device wasn't ready to send a byte. + /// + /// This is very unlikely as the device was previously cleared from any + /// remaining data. + SendError(ByteSendError), + /// Failed to read the same byte that was just written to the device. + UnexpectedLoopbackByte { + /// The expected byte. + expected: u8, + /// The actual received byte. + actual: u8, + }, + /// Failed to read a whole string that was just written to the device. + UnexpectedLoopbackMsg { + /// The expected message (a valid UTF-8 string). + expected: [u8; FIFO_SIZE], + /// The actual received message. + actual: [u8; FIFO_SIZE], + }, +} + +impl Display for LoopbackError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::SendError(e) => { + write!(f, "loopback test failed: couldn't send data: {e}") + } + Self::UnexpectedLoopbackByte { expected, actual } => { + write!( + f, + "loopback test failed: read unexpected byte! expected={expected}, actual={actual}" + ) + } + Self::UnexpectedLoopbackMsg { expected, actual } => { + let expected = core::str::from_utf8(expected); + let maybe_actual_str = core::str::from_utf8(actual); + write!( + f, + "loopback test failed: read unexpected string! expected (str)={expected:?}, actual (str)={maybe_actual_str:?}, actual (bytes)={actual:?}" + ) + } + } + } +} + +impl Error for LoopbackError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + Self::SendError(e) => Some(e), + _ => None, + } + } +} + +/// Errors that can happen when a [`Uart16550`] initialized in +/// [`Uart16550::init`]. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum InitError { + /// The device could not be detected. + DeviceNotPresent, + /// The loopback self-test after initialization failed. + LoopbackTestFailed, + /// The configured baud rate can not be set as it results in an invalid + /// divisor. + InvalidBaudRate(NonIntegerDivisorError), +} + +impl Display for InitError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::DeviceNotPresent => { + write!(f, "the device could not be detected") + } + Self::LoopbackTestFailed => { + write!(f, "the loopback self-test after initialization failed") + } + Self::InvalidBaudRate(e) => { + write!(f, "invalid baud rate: {e}") + } + } + } +} + +impl Error for InitError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + Self::InvalidBaudRate(err) => Some(err), + _ => None, + } + } +} + +/// There is currently no data to read. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct ByteReceiveError; + +impl Display for ByteReceiveError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "there is no data to read") + } +} + +impl Error for ByteReceiveError {} + +/// Errors that happen when trying to send a byte +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum ByteSendError { + /// There is currently no capacity to send another byte. + /// + /// For example, the FIFO might be full. + NoCapacity, + /// The remote is not (yet) ready to receive more data. + /// + /// This can for example mean that it is still processing input data. + RemoteNotClearToSend, +} + +impl Display for ByteSendError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::NoCapacity => write!( + f, + "device has no capacity to send another byte (the FIFO might be full)" + ), + Self::RemoteNotClearToSend => { + write!(f, "the remote didn't raised its clear to send signal") + } + } + } +} + +impl Error for ByteSendError {} + +/// Errors indicating the device is not ready to send data. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum RemoteReadyToReceiveError { + /// There is no remote connected. + NoRemoteConnected, + /// A remote endpoint is present but has not asserted readiness to receive + /// data. + /// + /// This reflects only the remote's configured receive readiness and does + /// not imply anything about its actual buffering capacity. + RemoteNotConfigured, + /// The remote is not (yet) ready to receive more data. + /// + /// This can for example mean that it is still processing input data. + RemoteNotClearToSend, +} + +impl Display for RemoteReadyToReceiveError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::NoRemoteConnected => { + write!(f, "there is no remote connection") + } + Self::RemoteNotConfigured => write!( + f, + "remote is connected but did not signal it is ready to receive data" + ), + Self::RemoteNotClearToSend => { + write!(f, "remote is not (yet) ready to receive more data") + } + } + } +} + +impl Error for RemoteReadyToReceiveError {} diff --git a/src/lib.rs b/src/lib.rs index c985e58..fb1c609 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,124 +1,771 @@ -//! Minimal support for -//! [serial communication](https://en.wikipedia.org/wiki/Asynchronous_serial_communication) -//! through [UART](https://en.wikipedia.org/wiki/Universal_asynchronous_receiver-transmitter) -//! devices, which are compatible to the [16550 UART](https://en.wikipedia.org/wiki/16550_UART). -//! -//! This crate supports I/O port-mapped (x86 only) and memory-mapped UARTS. +// SPDX-License-Identifier: MIT OR Apache-2.0 + +//! # uart_16550 //! -//! ## Usage +//! Simple yet highly configurable low-level driver for +//! [16550 UART devices][uart], typically known and used as serial ports or +//! COM ports. Easy integration into Rust while providing fine-grained control +//! where needed (e.g., for kernel drivers). //! -//! Depending on the system architecture, the UART can be either accessed through -//! [port-mapped I/O](https://wiki.osdev.org/Port_IO) or -//! [memory-mapped I/O](https://en.wikipedia.org/wiki/Memory-mapped_I/O). +//! The "serial device" or "COM port" in typical x86 machines is almost always +//! backed by a **16550 UART devices**, may it be physical or emulated. This +//! crate offers convenient and powerful abstractions for these devices, and +//! also works for other architectures, such as ARM or RISC-V, by offering +//! support for MMIO-mapped devices. //! -//! ### With port-mappd I/O +//! Serial ports are especially useful for debugging or operating system +//! learning projects. See [`Uart16550`] to get started. //! -//! The UART is accessed through port-mapped I/O on architectures such as `x86_64`. -//! On these architectures, the [`SerialPort`] type can be used: +//! ## Features //! +//! - ✅ Full configure, transmit, receive, and interrupt support for UART +//! 16550–compatible devices +//! - ✅ High-level, ergonomic abstractions and types paired with support for +//! plain integers +//! - ✅ Very easy to integrate, highly configurable when needed +//! - ✅ Validated on **real hardware** as well as across different virtual +//! machines +//! - ✅ Fully type-safe and derived directly from the official +//! [specification][uart] +//! - ✅ Supports both **x86 port-mapped I/O** and **memory-mapped I/O** (MMIO) +//! - ✅ `no_std`-compatible and allocation-free by design //! -//! ```no_run -//! # #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] -//! # fn main() { -//! use uart_16550::SerialPort; +//! ## Focus, Scope & Limitations //! -//! const SERIAL_IO_PORT: u16 = 0x3F8; +//! While serial ports are often used in conjunction with VT102-like terminal +//! emulation, the primary focus of this crate is strict specification +//! compliance and convenient direct access to the underlying hardware for +//! transmitting and receiving bytes, including all necessary device +//! configuration. //! -//! let mut serial_port = unsafe { SerialPort::new(SERIAL_IO_PORT) }; -//! serial_port.init(); +//! For basic terminal-related functionality, such as newline normalization and +//! backspace handling, we provide [`Uart16550Tty`] as a **basic** convenience +//! layer. //! -//! // Now the serial port is ready to be used. To send a byte: -//! serial_port.send(42); +//! # Overview //! -//! // To receive a byte: -//! let data = serial_port.receive(); -//! # } -//! # #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))] -//! # fn main() {} -//! ``` +//! Use [`Uart16550Tty`] for a quick start. For more fine-grained low-level +//! control, please have a look at [`Uart16550`] instead. //! -//! ### With memory mapped serial port +//! # Example (Minimalistic) //! -//! Most other architectures, such as [RISC-V](https://en.wikipedia.org/wiki/RISC-V), use -//! memory-mapped I/O for accessing the UARTs. On these architectures, the [`MmioSerialPort`] -//! type can be used: +//! ```rust,no_run +//! use uart_16550::{Config, Uart16550Tty}; +//! use core::fmt::Write; //! -//! ```no_run -//! use uart_16550::MmioSerialPort; +//! // SAFETY: The I/O port is valid and we have exclusive access. +//! let mut uart = unsafe { Uart16550Tty::new_port(0x3f8, Config::default()).expect("should initialize device") }; +//! // ^ you could also use `new_mmio(0x1000 as *mut _)` here +//! uart.write_str("hello world\nhow's it going?"); +//! ``` //! -//! const SERIAL_PORT_BASE_ADDRESS: usize = 0x1000_0000; +//! See [`Uart16550Tty`] for more details. //! -//! let mut serial_port = unsafe { MmioSerialPort::new(SERIAL_PORT_BASE_ADDRESS) }; -//! serial_port.init(); +//! # Example (More low-level control) //! -//! // Now the serial port is ready to be used. To send a byte: -//! serial_port.send(42); +//! ```rust,no_run +//! use uart_16550::{Config, Uart16550}; //! -//! // To receive a byte: -//! let data = serial_port.receive(); +//! // SAFETY: The I/O port is valid and we have exclusive access. +//! let mut uart = unsafe { Uart16550::new_port(0x3f8).expect("should be valid port") }; +//! // ^ you could also use `new_mmio(0x1000 as *mut _)` here +//! uart.init(Config::default()).expect("should init device successfully"); +//! uart.test_loopback().expect("should have working loopback mode"); +//! uart.check_remote_ready_to_receive().expect("should have physically connected receiver"); +//! uart.send_bytes_all(b"hello world!"); //! ``` +//! +//! See [`Uart16550`] for more details. +//! +//! [uart]: https://en.wikipedia.org/wiki/16550_UART #![no_std] -#![warn(missing_docs)] -#![cfg_attr(docsrs, feature(doc_cfg))] +#![deny( + clippy::all, + clippy::cargo, + clippy::nursery, + clippy::must_use_candidate, + clippy::missing_safety_doc, + clippy::undocumented_unsafe_blocks, + clippy::needless_pass_by_value +)] +#![deny(missing_docs)] +#![deny(missing_debug_implementations)] +#![deny(rustdoc::all)] -use core::fmt; +#[cfg(test)] +extern crate std; -use bitflags::bitflags; +pub use crate::config::*; +pub use crate::error::*; +pub use crate::tty::*; -macro_rules! retry_until_ok { - ($cond:expr) => { - loop { - if let Ok(ok) = $cond { - break ok; - } - core::hint::spin_loop(); - } - }; +use crate::backend::{Backend, MmioAddress, MmioBackend}; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +use crate::backend::{PioBackend, PortIoAddress}; +use crate::spec::registers::{DLL, DLM, FCR, IER, ISR, LCR, LSR, MCR, MSR, SPR, offsets}; +use crate::spec::{FIFO_SIZE, NUM_REGISTERS, calc_baud_rate, calc_divisor}; +use core::cmp; + +pub mod backend; +pub mod spec; + +mod config; +mod error; +mod tty; + +/// Powerful abstraction over a [16550 UART device][uart] with access to +/// low-level details paired with strong flexibility for higher-level layers. +/// +/// All reads and writes involving device register from/to that device operate +/// on the underlying hardware. +/// +/// This type is generic over x86 port I/O and MMIO via the corresponding +/// constructors (`[Uart16550::new_port()]` and [`Uart16550::new_mmio()]`. +/// +/// # Example (Minimal) +/// +/// ```rust,no_run +/// use uart_16550::{Config, Uart16550}; +/// +/// // SAFETY: The I/O port is valid and we have exclusive access. +/// let mut uart = unsafe { Uart16550::new_port(0x3f8).unwrap() }; +/// // ^ you could also use `new_mmio(0x1000 as *mut _)` here +/// uart.init(Config::default()).expect("should init device successfully"); +/// uart.send_bytes_all(b"hello world!"); +/// ``` +/// +/// # Example (Recommended) +/// +/// ```rust,no_run +/// use uart_16550::{Config, Uart16550}; +/// +/// // SAFETY: The I/O port is valid and we have exclusive access. +/// let mut uart = unsafe { Uart16550::new_port(0x3f8).expect("should be valid port") }; +/// // ^ you could also use `new_mmio(0x1000 as *mut _)` here +/// uart.init(Config::default()).expect("should init device successfully"); +/// uart.test_loopback().expect("should have working loopback mode"); +/// uart.check_remote_ready_to_receive().expect("should have physically connected receiver"); +/// uart.send_bytes_all(b"hello world!"); +/// ``` +/// +/// # Sending and Receiving Data +/// +/// - [`Uart16550::try_send_byte`]: try to send a single byte +/// - [`Uart16550::try_send_bytes`]: try to send provided bytes and return `n` +/// - [`Uart16550::send_bytes_all`]: send all provided bytes +/// - [`Uart16550::try_receive_byte`]: try to receive a single byte +/// - [`Uart16550::receive_bytes`]: try to receive bytes into a buffer and return +/// `n` +/// - [`Uart16550::receive_bytes_all`]: receive bytes until provided buffer is +/// filled +/// +/// # MMIO and Port I/O +/// +/// The constructors `new_port()` and `new_mmio()` create an abstraction with +/// the corresponding backend. +/// +/// [uart]: https://en.wikipedia.org/wiki/16550_UART +#[derive(Debug)] +pub struct Uart16550 { + backend: B, + base_address: B::Address, + // The currently active config. + config: Config, } -/// Memory mapped implementation -mod mmio; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] -/// Port asm commands implementation -mod port; +impl Uart16550 { + /// Creates a new [`Uart16550`] backed by x86 port I/O. + /// + /// # Safety + /// + /// Callers must ensure that the base port is valid and safe to use for the + /// **whole lifetime** of the device. Further, all [`NUM_REGISTERS`] + /// registers must be safely reachable from the base address. + pub unsafe fn new_port(base_port: u16) -> Result> { + let base_address = PortIoAddress(base_port); + if base_port.checked_add(NUM_REGISTERS as u16 - 1).is_none() { + return Err(InvalidAddressError(base_address)); + } -pub use crate::mmio::MmioSerialPort; -#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] -pub use crate::port::SerialPort; - -bitflags! { - /// Interrupt enable flags - #[repr(transparent)] - #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] - struct IntEnFlags: u8 { - const RECEIVED = 1; - const SENT = 1 << 1; - const ERRORED = 1 << 2; - const STATUS_CHANGE = 1 << 3; - // 4 to 7 are unused + let backend = PioBackend(base_address); + + Ok(Self { + backend, + base_address, + // Will be replaced by the actual config in init() afterwards. + config: Config::default(), + }) } } -bitflags! { - /// Line status flags - #[repr(transparent)] - #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] - struct LineStsFlags: u8 { - const INPUT_FULL = 1; - // 1 to 4 unknown - const OUTPUT_EMPTY = 1 << 5; - // 6 and 7 unknown +impl Uart16550 { + /// Creates a new [`Uart16550`] backed by MMIO. + /// + /// # Safety + /// + /// Callers must ensure that the base address is valid and safe to use for + /// the **whole lifetime** of the device. Further, all [`NUM_REGISTERS`] + /// registers must be safely reachable from the base address. + pub unsafe fn new_mmio( + base_address: *mut u8, + ) -> Result> { + let base_address = MmioAddress(base_address); + if base_address.0.is_null() { + return Err(InvalidAddressError(base_address)); + } + if (base_address.0 as usize) + .checked_add(NUM_REGISTERS - 1) + .is_none() + { + return Err(InvalidAddressError(base_address)); + } + + let backend = MmioBackend(base_address); + + Ok(Self { + backend, + base_address, + // Will be replaced by the actual config in init() afterwards. + config: Config::default(), + }) } } -/// The `WouldBlockError` error indicates that the serial device was not ready immediately. -#[non_exhaustive] -#[derive(Clone, PartialEq, Eq, Debug)] -pub struct WouldBlockError; +impl Uart16550 { + /* ----- Init, Setup, Tests --------------------------------------------- */ + + /// Initializes the devices according to the provided [`Config`] including a + /// few typical as well as opinionated settings. + /// + /// This function also tries to detect if the UART is present at all by + /// writing a byte to the [`SPR`] register and read it back afterwards. + /// + /// It is **recommended** to call [`Self::test_loopback`] next to check the + /// device works. Further, a call to + /// [`Self::check_remote_ready_to_receive`] helps to detect if a remote + /// is connected. + /// + /// # Caution + /// + /// Callers must ensure that using this type with the underlying hardware + /// is done only in a context where such operations are valid and safe + /// (e.g., you have exclusive device access). + /// + /// Further, the serial config must match the expectations of the receiver + /// on the other side. Otherwise, garbage will be received. + pub fn init(&mut self, config: Config) -> Result<(), InitError> { + // It is important to set this early as some helpers rely on that. + self.config = config; + + // SPR test: write something and try to read it again. + // => detect if UART16550 is there + // SAFETY: We operate on valid register addresses. + unsafe { + let write = 0x42; + self.backend.write(offsets::SPR as u8, write); + let read = self.backend.read(offsets::SPR as u8); + + if read != write { + return Err(InitError::DeviceNotPresent); + } + } + + // Disable all interrupts (for now). + // SAFETY: We operate on valid register addresses. + unsafe { + self.backend.write(offsets::IER as u8, 0); + } + + // Set baud rate. + // SAFETY: We operate on valid register addresses. + unsafe { + // Set Divisor Latch Access Bit (DLAB) to access DLL and DLM next + self.backend.write(offsets::LCR as u8, LCR::DLAB.bits()); + + let divisor = calc_divisor( + self.config.frequency, + self.config.baud_rate.to_integer(), + self.config.prescaler_division_factor, + ) + .map_err(InitError::InvalidBaudRate)?; + + let low = (divisor & 0xff) as u8; + let high = ((divisor >> 8) & 0xff) as u8; + self.backend.write(offsets::DLL as u8, low); + self.backend.write(offsets::DLM as u8, high); + + // Clear DLAB + self.backend.write(offsets::LCR as u8, 0); + } + + // Set line control register. + // SAFETY: We operate on valid register addresses. + unsafe { + let mut lcr = LCR::from_bits_retain(0); + lcr = lcr.set_word_length(self.config.data_bits); + if self.config.extra_stop_bits { + lcr |= LCR::MORE_STOP_BITS; + } + lcr = lcr.set_parity(self.config.parity); + // don't set break + // don't set DLAB + self.backend.write(offsets::LCR as u8, lcr.bits()); + } + + // SAFETY: We have exclusive access to the device. + unsafe { + self.configure_fcr(); + } + + // Set modem control register. + // SAFETY: We operate on valid register addresses. + unsafe { + let mut mcr = MCR::from_bits_retain(0); + // signal that we are powered one + mcr |= MCR::DTR; + // signal that we are ready and configured + mcr |= MCR::RTS; + // enable interrupt routing to the interrupt controller + // (so far individual interrupts are still disabled in IER) + mcr |= MCR::OUT_2_INT_ENABLE; + + self.backend.write(offsets::MCR as u8, mcr.bits()); + } + + // Set interrupts. + // SAFETY: We operate on valid register addresses. + unsafe { + self.backend + .write(offsets::IER as u8, self.config.interrupts.bits()); + } + Ok(()) + } + + /// Tests the device in loopback mode. + /// + /// It is **recommended** to call this function **after** [`Self::init`]. + /// + /// # Caution + /// + /// - No other data **must** be send over the device while this test isn't + /// finished + /// - The FIFO will be re-configured and activated by this test. + // NEVER CHANGE this function without testing on real hardware! + pub fn test_loopback(&mut self) -> Result<(), LoopbackError> { + /// Single test byte. Chosen arbitrarily. + const TEST_BYTE: u8 = 0x42; + /// Test message. Must be smaller than [`FIFO_SIZE`]. + const TEST_MESSAGE: [u8; FIFO_SIZE] = *b"hello world!1337"; + + // SAFETY: We operate on valid register addresses. + unsafe { + let old_mcr = self.mcr(); + + // We also disable interrupts here. + self.backend + .write(offsets::MCR as u8, MCR::LOOP_BACK.bits()); + + // Reset send and receive FIFOs. + self.configure_fcr(); + + // Drain any data that might be still there + while self.receive_bytes(&mut [0]) > 0 {} + + // First: check a single byte + { + self.try_send_byte(TEST_BYTE) + .map_err(LoopbackError::SendError)?; + + let mut read_buf = [0]; + // Tests on real hardware showed that there can be a short delay + // until we can read the data. Therefore, we use the blocking + // API rather than `try_receive_byte()`. + self.receive_bytes_all(&mut read_buf); + let read = read_buf[0]; + if read != TEST_BYTE { + return Err(LoopbackError::UnexpectedLoopbackByte { + expected: TEST_BYTE, + actual: read, + }); + } + } + + // Now check sending and reading a whole message. + // This requires the FIFO to be activated. + { + self.send_bytes_all(&TEST_MESSAGE); + + let mut read_buffer = [0_u8; TEST_MESSAGE.len()]; + // Tests on real hardware showed that there can be a short delay + // until we can read the data. Therefore, we use the blocking + // API rather than `receive_bytes()`. + self.receive_bytes_all(&mut read_buffer); + let read = read_buffer; + + if read != TEST_MESSAGE { + return Err(LoopbackError::UnexpectedLoopbackMsg { + expected: TEST_MESSAGE, + actual: read_buffer, + }); + } + } + + // restore MCR + self.backend.write(offsets::MCR as u8, old_mcr.bits()); + } + + Ok(()) + } + + /// Performs some checks to see if the UART is connected to a physical + /// device. + /// + /// Once this check succeeds, one can see the connection as established. + /// A [`InterruptType::ModemStatus`] may indicate that this check needs to + /// be performed again. + /// + /// [`InterruptType::ModemStatus`]: crate::spec::registers::InterruptType::ModemStatus + pub fn check_remote_ready_to_receive(&mut self) -> Result<(), RemoteReadyToReceiveError> { + // SAFETY: We operate on valid register addresses. + let msr = unsafe { self.backend.read(offsets::MSR as u8) }; + // SAFETY: All possible bits are typed. + let msr = unsafe { MSR::from_bits(msr).unwrap_unchecked() }; + if !msr.contains(MSR::DSR) { + return Err(RemoteReadyToReceiveError::NoRemoteConnected); + } + if !msr.contains(MSR::CD) { + return Err(RemoteReadyToReceiveError::RemoteNotConfigured); + } + if !msr.contains(MSR::CTS) { + return Err(RemoteReadyToReceiveError::RemoteNotClearToSend); + } + Ok(()) + } + + /* ----- User I/O ------------------------------------------------------- */ + + /// Tries to read a raw byte from the device. + /// + /// This will receive whatever a remote has sent to us. + pub fn try_receive_byte(&mut self) -> Result { + let lsr = self.lsr(); + + if !lsr.contains(LSR::DATA_READY) { + return Err(ByteReceiveError); + } + + // SAFETY: We operate on valid register addresses. + let byte = unsafe { self.backend.read(offsets::DATA as u8) }; + + Ok(byte) + } + + /// Tries to write a raw byte to the device. + /// + /// This will be transmitted to the remote. + #[inline] + pub fn try_send_byte(&mut self, byte: u8) -> Result<(), ByteSendError> { + // bytes are typically written in chunks for higher performance, + // therefore `try_send_bytes()` is our base here. Further, UART16550 + // do not allow us to check if there is capacity left in the FIFO. + self.try_send_bytes(&[byte]).map(|_| ()) + } + + /// Tries to receive bytes from the device and writes them into the provided + /// buffer. + /// + /// This function returns the number of bytes that have been received and + /// put into the buffer. + pub fn receive_bytes(&mut self, buffer: &mut [u8]) -> usize { + buffer + .iter_mut() + .map_while(|slot: &mut u8| { + self.try_receive_byte().ok().map(|byte| { + *slot = byte; + }) + }) + .count() + } + + /// Tries to send bytes from the device to the remote. + /// + /// This function returns the number of bytes that have been sent to the + /// remote. + /// + /// In non-FIFO mode, only a single byte is written at a time. In FIFO mode, + /// if [`LSR::THR_EMPTY`] is set, up to [`FIFO_SIZE`] bytes are written, + /// otherwise `0`. + pub fn try_send_bytes(&mut self, buffer: &[u8]) -> Result { + let lsr = self.lsr(); + let msr = self.msr(); + let mcr = self.mcr(); + + let fifo_enabled = self.config.fifo_trigger_level.is_some(); + + // We either write 1 byte or up to FIFO_SIZE bytes. + // We do not have a mechanism to check the remaining capacity of the + // FIFO. + let bytes = if fifo_enabled { + let max_index = cmp::min(FIFO_SIZE, buffer.len()); + &buffer[..max_index] + } else { + &buffer[..1] + }; + + if bytes.is_empty() { + return Ok(0); + } + + if !lsr.contains(LSR::THR_EMPTY) { + return Err(ByteSendError::NoCapacity); + } + + if !mcr.contains(MCR::LOOP_BACK) && !msr.contains(MSR::CTS) { + return Err(ByteSendError::RemoteNotClearToSend); + } + + for byte in bytes { + // SAFETY: We operate on valid register addresses. + unsafe { + self.backend.write(offsets::DATA as u8, *byte); + } + } + + Ok(bytes.len()) + } + + /// Similar to [`Self::receive_bytes`] but blocks until enough bytes were + /// read to fully fill the buffer. + pub fn receive_bytes_all(&mut self, buffer: &mut [u8]) { + for slot in buffer { + // Loop until we can fill the slot. + loop { + if let Ok(byte) = self.try_receive_byte() { + *slot = byte; + break; + } + } + } + } + + /// Similar to [`Self::try_send_bytes`] but blocks until all bytes were + /// written entirely to the remote. + pub fn send_bytes_all(&mut self, bytes: &[u8]) { + let mut remaining_bytes = bytes; + while !remaining_bytes.is_empty() { + if let Ok(n) = self.try_send_bytes(remaining_bytes) { + remaining_bytes = &remaining_bytes[n..]; + } + } + } + + /* ----- Typed Register Getters ----------------------------------------- */ + + /// Fetches the current value from the [`IER`]. + pub fn ier(&mut self) -> IER { + // SAFETY: We operate on valid register addresses. + let val = unsafe { self.backend.read(offsets::IER as u8) }; + // SAFETY: All possible bits are typed. + unsafe { IER::from_bits(val).unwrap_unchecked() } + } + + /// Fetches the current value from the [`ISR`]. + pub fn isr(&mut self) -> ISR { + // SAFETY: We operate on valid register addresses. + let val = unsafe { self.backend.read(offsets::ISR as u8) }; + // SAFETY: All possible bits are typed. + unsafe { ISR::from_bits(val).unwrap_unchecked() } + } + + /// Fetches the current value from the [`LCR`]. + pub fn lcr(&mut self) -> LCR { + // SAFETY: We operate on valid register addresses. + let val = unsafe { self.backend.read(offsets::LCR as u8) }; + // SAFETY: All possible bits are typed. + unsafe { LCR::from_bits(val).unwrap_unchecked() } + } + + /// Fetches the current value from the [`MCR`]. + pub fn mcr(&mut self) -> MCR { + // SAFETY: We operate on valid register addresses. + let val = unsafe { self.backend.read(offsets::MCR as u8) }; + // SAFETY: All possible bits are typed. + unsafe { MCR::from_bits(val).unwrap_unchecked() } + } + + /// Fetches the current value from the [`LSR`]. + pub fn lsr(&mut self) -> LSR { + // SAFETY: We operate on valid register addresses. + let val = unsafe { self.backend.read(offsets::LSR as u8) }; + // SAFETY: All possible bits are typed. + unsafe { LSR::from_bits(val).unwrap_unchecked() } + } + + /// Fetches the current value from the [`MSR`]. + pub fn msr(&mut self) -> MSR { + // SAFETY: We operate on valid register addresses. + let val = unsafe { self.backend.read(offsets::MSR as u8) }; + // SAFETY: All possible bits are typed. + unsafe { MSR::from_bits(val).unwrap_unchecked() } + } + + /// Fetches the current value from the [`SPR`]. + pub fn spr(&mut self) -> SPR { + // SAFETY: We operate on valid register addresses. + unsafe { self.backend.read(offsets::SPR as u8) } + } + + /// Fetches the current values from the [`DLL`] and [`DLM`]. + /// + /// # Safety + /// + /// This is unsafe as it will temporarily set the DLAB bit which may hinder + /// or negatively influence normal operation. + pub unsafe fn dll_dlm(&mut self) -> (DLL, DLM) { + let old_lcr = self.lcr(); + // SAFETY: We operate on valid register addresses. + unsafe { + self.backend + .write(offsets::LCR as u8, (old_lcr | LCR::DLAB).bits()); + + let dll = self.backend.read(offsets::DLL as u8); + let dlm = self.backend.read(offsets::DLM as u8); + self.backend.write(offsets::LCR as u8, old_lcr.bits()); + (dll, dlm) + } + } + + /// Configures the [`FCR`] from the currently active [`Config`]. + /// + /// It will always flush all FIFO queues (RX and TX) but activate the FIFO + /// only if it is configured to be active. + /// + /// # Safety + /// + /// This will reset the send and receive FIFOs, so data loss is possible. + /// Call this only before setting up an actual connection. + unsafe fn configure_fcr(&mut self) { + // Set fifo control register. + // SAFETY: We operate on valid register addresses. + unsafe { + let mut fcr = FCR::from_bits_retain(0); + if self.config.fifo_trigger_level.is_some() { + fcr |= FCR::FIFO_ENABLE; + } + fcr |= FCR::RX_FIFO_RESET; + fcr |= FCR::TX_FIFO_RESET; + // don't set DMA mode + if let Some(level) = self.config.fifo_trigger_level { + fcr = fcr.set_fifo_trigger_level(level); + } + + self.backend.write(offsets::FCR as u8, fcr.bits()); + } + } + + /* ----- Misc ----------------------------------------------------------- */ + + /// Returns the config from the last call to [`Self::init`] together with + /// the base address of the underlying hardware. + /// + /// To get the values that are currently in the registers, consider using + /// [`Self::config_register_dump`]. + pub const fn config(&self) -> (&Config, B::Address) { + (&self.config, self.base_address) + } + + /// Queries the device and returns a [`ConfigRegisterDump`]. + /// + /// # Safety + /// + /// Reading on some registers may have side-effects, for example in the + /// [`LSR`]. + pub unsafe fn config_register_dump(&mut self) -> ConfigRegisterDump { + // SAFETY: Caller ensured access is okay. + let (dll, dlm) = unsafe { self.dll_dlm() }; + ConfigRegisterDump { + ier: self.ier(), + isr: self.isr(), + lcr: self.lcr(), + mcr: self.mcr(), + lsr: self.lsr(), + msr: self.msr(), + spr: self.spr(), + dll, + dlm, + } + } +} + +// SAFETY: We allow moving between threads. +unsafe impl Send for Uart16550 {} + +/// A dump of all (readable) config registers of [`Uart16550`]. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] +pub struct ConfigRegisterDump { + /// The current [`IER`]. + pub ier: IER, + /// The current [`ISR`]. + pub isr: ISR, + /// The current [`LCR`]. + pub lcr: LCR, + /// The current [`MCR`]. + pub mcr: MCR, + /// The current [`LSR`]. + pub lsr: LSR, + /// The current [`MSR`]. + pub msr: MSR, + /// The current [`SPR`]. + pub spr: SPR, + /// The current [`DLL`]. + pub dll: DLL, + /// The current [`DLM`]. + pub dlm: DLM, +} + +impl ConfigRegisterDump { + /// Returns the effective divisor. + /// + /// Using [`calc_baud_rate`], you can calculate the effective baud rate. You + /// can also use [`Self::baud_rate()`]. + #[must_use] + pub const fn divisor(&self) -> u16 { + let dll = self.dll as u16; + let dlm = self.dlm as u16; + (dlm << 8) | dll + } + + /// Returns the effective divisor. + /// + /// Using [`calc_baud_rate`], you can calculate the effective + /// [`BaudRate`]. + #[must_use] + pub fn baud_rate(&self, config: &Config) -> BaudRate { + let divisor = self.divisor(); + let baud_rate = calc_baud_rate( + config.frequency, + divisor as u32, + config.prescaler_division_factor, + ) + .expect("should be able to calculate baud rate from the given valid values"); + BaudRate::from_integer(baud_rate) + } +} + +#[cfg(test)] +mod tests { + use crate::Uart16550; + use crate::backend::{MmioBackend, PioBackend}; + + #[test] + fn is_send() { + fn accept() {} + + accept::>(); + accept::>(); -impl fmt::Display for WouldBlockError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("serial device not ready") + // TODO: add test that the type is not Sync? } } diff --git a/src/mmio.rs b/src/mmio.rs deleted file mode 100644 index 090d337..0000000 --- a/src/mmio.rs +++ /dev/null @@ -1,143 +0,0 @@ -use core::{ - fmt, - sync::atomic::{AtomicPtr, Ordering}, -}; - -use crate::{LineStsFlags, WouldBlockError}; - -/// A memory-mapped UART. -#[derive(Debug)] -pub struct MmioSerialPort { - data: AtomicPtr, - int_en: AtomicPtr, - fifo_ctrl: AtomicPtr, - line_ctrl: AtomicPtr, - modem_ctrl: AtomicPtr, - line_sts: AtomicPtr, -} - -impl MmioSerialPort { - /// Creates a new UART interface on the given memory mapped address. - /// - /// This function is unsafe because the caller must ensure that the given base address - /// really points to a serial port device. - #[rustversion::attr(since(1.61), const)] - pub unsafe fn new(base: usize) -> Self { - Self::new_with_stride(base, 1) - } - - /// Creates a new UART interface on the given memory mapped address with a given - /// register stride. - /// - /// This function is unsafe because the caller must ensure that the given base address - /// really points to a serial port device. - #[rustversion::attr(since(1.61), const)] - pub unsafe fn new_with_stride(base: usize, stride: usize) -> Self { - let base_pointer = base as *mut u8; - Self { - data: AtomicPtr::new(base_pointer), - int_en: AtomicPtr::new(base_pointer.add(1 * stride)), - fifo_ctrl: AtomicPtr::new(base_pointer.add(2 * stride)), - line_ctrl: AtomicPtr::new(base_pointer.add(3 * stride)), - modem_ctrl: AtomicPtr::new(base_pointer.add(4 * stride)), - line_sts: AtomicPtr::new(base_pointer.add(5 * stride)), - } - } - - /// Initializes the memory-mapped UART. - /// - /// The default configuration of [38400/8-N-1](https://en.wikipedia.org/wiki/8-N-1) is used. - pub fn init(&mut self) { - let self_int_en = self.int_en.load(Ordering::Relaxed); - let self_line_ctrl = self.line_ctrl.load(Ordering::Relaxed); - let self_data = self.data.load(Ordering::Relaxed); - let self_fifo_ctrl = self.fifo_ctrl.load(Ordering::Relaxed); - let self_modem_ctrl = self.modem_ctrl.load(Ordering::Relaxed); - unsafe { - // Disable interrupts - self_int_en.write(0x00); - - // Enable DLAB - self_line_ctrl.write(0x80); - - // Set maximum speed to 38400 bps by configuring DLL and DLM - self_data.write(0x03); - self_int_en.write(0x00); - - // Disable DLAB and set data word length to 8 bits - self_line_ctrl.write(0x03); - - // Enable FIFO, clear TX/RX queues and - // set interrupt watermark at 14 bytes - self_fifo_ctrl.write(0xC7); - - // Mark data terminal ready, signal request to send - // and enable auxilliary output #2 (used as interrupt line for CPU) - self_modem_ctrl.write(0x0B); - - // Enable interrupts - self_int_en.write(0x01); - } - } - - fn line_sts(&mut self) -> LineStsFlags { - unsafe { LineStsFlags::from_bits_truncate(*self.line_sts.load(Ordering::Relaxed)) } - } - - /// Sends a byte on the serial port. - pub fn send(&mut self, data: u8) { - match data { - 8 | 0x7F => { - self.send_raw(8); - self.send_raw(b' '); - self.send_raw(8); - } - data => { - self.send_raw(data); - } - } - } - - /// Sends a raw byte on the serial port, intended for binary data. - pub fn send_raw(&mut self, data: u8) { - retry_until_ok!(self.try_send_raw(data)) - } - - /// Tries to send a raw byte on the serial port, intended for binary data. - pub fn try_send_raw(&mut self, data: u8) -> Result<(), WouldBlockError> { - if self.line_sts().contains(LineStsFlags::OUTPUT_EMPTY) { - let self_data = self.data.load(Ordering::Relaxed); - unsafe { - self_data.write(data); - } - Ok(()) - } else { - Err(WouldBlockError) - } - } - - /// Receives a byte on the serial port. - pub fn receive(&mut self) -> u8 { - retry_until_ok!(self.try_receive()) - } - - /// Tries to receive a byte on the serial port. - pub fn try_receive(&mut self) -> Result { - if self.line_sts().contains(LineStsFlags::INPUT_FULL) { - let self_data = self.data.load(Ordering::Relaxed); - let data = unsafe { self_data.read() }; - Ok(data) - } else { - Err(WouldBlockError) - } - } -} - -impl fmt::Write for MmioSerialPort { - fn write_str(&mut self, s: &str) -> fmt::Result { - for byte in s.bytes() { - self.send(byte); - } - Ok(()) - } -} diff --git a/src/port.rs b/src/port.rs deleted file mode 100644 index f5dca40..0000000 --- a/src/port.rs +++ /dev/null @@ -1,162 +0,0 @@ -use core::fmt; - -use crate::{LineStsFlags, WouldBlockError}; - -/// A x86 I/O port-mapped UART. -#[cfg_attr(docsrs, doc(cfg(any(target_arch = "x86", target_arch = "x86_64"))))] -#[derive(Debug)] -pub struct SerialPort(u16 /* base port */); - -impl SerialPort { - /// Base port. - fn port_base(&self) -> u16 { - self.0 - } - - /// Data port. - /// - /// Read and write. - fn port_data(&self) -> u16 { - self.port_base() - } - - /// Interrupt enable port. - /// - /// Write only. - fn port_int_en(&self) -> u16 { - self.port_base() + 1 - } - - /// Fifo control port. - /// - /// Write only. - fn port_fifo_ctrl(&self) -> u16 { - self.port_base() + 2 - } - - /// Line control port. - /// - /// Write only. - fn port_line_ctrl(&self) -> u16 { - self.port_base() + 3 - } - - /// Modem control port. - /// - /// Write only. - fn port_modem_ctrl(&self) -> u16 { - self.port_base() + 4 - } - - /// Line status port. - /// - /// Read only. - fn port_line_sts(&self) -> u16 { - self.port_base() + 5 - } - - /// Creates a new serial port interface on the given I/O base port. - /// - /// This function is unsafe because the caller must ensure that the given base address - /// really points to a serial port device and that the caller has the necessary rights - /// to perform the I/O operation. - pub const unsafe fn new(base: u16) -> Self { - Self(base) - } - - /// Initializes the serial port. - /// - /// The default configuration of [38400/8-N-1](https://en.wikipedia.org/wiki/8-N-1) is used. - pub fn init(&mut self) { - unsafe { - // Disable interrupts - x86::io::outb(self.port_int_en(), 0x00); - - // Enable DLAB - x86::io::outb(self.port_line_ctrl(), 0x80); - - // Set maximum speed to 38400 bps by configuring DLL and DLM - x86::io::outb(self.port_data(), 0x03); - x86::io::outb(self.port_int_en(), 0x00); - - // Disable DLAB and set data word length to 8 bits - x86::io::outb(self.port_line_ctrl(), 0x03); - - // Enable FIFO, clear TX/RX queues and - // set interrupt watermark at 14 bytes - x86::io::outb(self.port_fifo_ctrl(), 0xc7); - - // Mark data terminal ready, signal request to send - // and enable auxilliary output #2 (used as interrupt line for CPU) - x86::io::outb(self.port_modem_ctrl(), 0x0b); - - // Enable interrupts - x86::io::outb(self.port_int_en(), 0x01); - } - } - - fn line_sts(&mut self) -> LineStsFlags { - unsafe { LineStsFlags::from_bits_truncate(x86::io::inb(self.port_line_sts())) } - } - - /// Sends a byte on the serial port. - /// 0x08 (backspace) and 0x7F (delete) get replaced with 0x08, 0x20, 0x08 and 0x0A (\n) gets replaced with \r\n. - /// If this replacement is unwanted use [SerialPort::send_raw] instead. - pub fn send(&mut self, data: u8) { - match data { - 8 | 0x7F => { - self.send_raw(8); - self.send_raw(b' '); - self.send_raw(8); - } - 0x0A => { - self.send_raw(0x0D); - self.send_raw(0x0A); - } - data => { - self.send_raw(data); - } - } - } - - /// Sends a raw byte on the serial port, intended for binary data. - pub fn send_raw(&mut self, data: u8) { - retry_until_ok!(self.try_send_raw(data)) - } - - /// Tries to send a raw byte on the serial port, intended for binary data. - pub fn try_send_raw(&mut self, data: u8) -> Result<(), WouldBlockError> { - if self.line_sts().contains(LineStsFlags::OUTPUT_EMPTY) { - unsafe { - x86::io::outb(self.port_data(), data); - } - Ok(()) - } else { - Err(WouldBlockError) - } - } - - /// Receives a byte on the serial port. - pub fn receive(&mut self) -> u8 { - retry_until_ok!(self.try_receive()) - } - - /// Tries to receive a byte on the serial port. - pub fn try_receive(&mut self) -> Result { - if self.line_sts().contains(LineStsFlags::INPUT_FULL) { - let data = unsafe { x86::io::inb(self.port_data()) }; - Ok(data) - } else { - Err(WouldBlockError) - } - } -} - -impl fmt::Write for SerialPort { - fn write_str(&mut self, s: &str) -> fmt::Result { - for byte in s.bytes() { - self.send(byte); - } - Ok(()) - } -} diff --git a/src/spec.rs b/src/spec.rs new file mode 100644 index 0000000..99e5d85 --- /dev/null +++ b/src/spec.rs @@ -0,0 +1,1144 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 + +//! # Constants, Register Offsets, and Register Bits. +//! +//! Models the raw low-level details as of the [datasheet], and avoids too +//! opinionated abstractions. +//! +//! [datasheet]: https://caro.su/msx/ocm_de1/16550.pdf + +pub use crate::spec::errors::*; + +/// Most typical 16550 clock frequency of 1.8432 Mhz. +pub const CLK_FREQUENCY_HZ: u32 = 1_843_200; + +/// The maximum size of the internal read and write FIFO. +/// +/// Each channel (tx: transmission, rx: reception) has its own queue. +pub const FIFO_SIZE: usize = 16; + +/// Number of registers of the device. +/// +/// The maximum register index is this value minus `1`. +pub const NUM_REGISTERS: usize = 8; + +mod errors { + use core::error::Error; + use core::fmt::{self, Display, Formatter}; + + /// Error that is returned when [`calc_baud_rate`] could not calculate an even + /// baud rate, i.e., a baud rate that is representable as integer. + /// + /// [`calc_baud_rate`]: crate::spec::calc_baud_rate + #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Hash)] + pub struct NonIntegerBaudRateError { + /// The frequency of the UART 16550. + pub frequency: u32, + /// The divisor. + pub divisor: u32, + /// The optional prescaler division factor. + pub prescaler_division_factor: Option, + } + + impl Display for NonIntegerBaudRateError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!( + f, + "Input values do not result in even (integer representable) baud rate! frequency={}, divisor={}, prescaler_division_factor={:?}", + self.frequency, self.divisor, self.prescaler_division_factor + ) + } + } + + impl Error for NonIntegerBaudRateError {} + + /// Error that is returned when [`calc_divisor`] could not calculate an even + /// baud rate, i.e., a baud rate that is representable as integer. + /// + /// [`calc_divisor`]: crate::spec::calc_divisor + #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Hash)] + pub struct NonIntegerDivisorError { + /// The frequency of the UART 16550. + pub frequency: u32, + /// The divisor. + pub baud_rate: u32, + /// The optional prescaler division factor. + pub prescaler_division_factor: Option, + } + + impl Display for NonIntegerDivisorError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!( + f, + "input values do not result in even (integer representable) baud rate! frequency={}, baud_rate={}, prescaler_division_factor={:?}", + self.frequency, self.baud_rate, self.prescaler_division_factor, + ) + } + } + + impl Error for NonIntegerDivisorError {} +} + +/// Calculates the baud rate from the frequency. +/// +/// # Arguments +/// - `frequency`: The frequency of the microcontroller, typically +/// [`CLK_FREQUENCY_HZ`]. +/// - `divisor`: The divisor to use. +/// - `prescaler_division_factor`: An optional additional division factor in +/// some more modern UART 16550 variants. +pub fn calc_baud_rate( + frequency: u32, + divisor: u32, + prescaler_division_factor: Option, +) -> Result { + let psd = prescaler_division_factor.map_or(0, |psd| psd); + let a = frequency; + let b = 16 * (psd + 1) * divisor; + + if a % b == 0 { + Ok(a / b) + } else { + Err(NonIntegerBaudRateError { + frequency, + prescaler_division_factor, + divisor, + }) + } +} + +/// Similar to [`calc_baud_rate`] but with known baud rate to calculate the +/// frequency. +#[must_use] +pub const fn calc_frequency( + baud_rate: u32, + divisor: u32, + prescaler_division_factor: Option, +) -> u32 { + let psd = if let Some(psd) = prescaler_division_factor { + psd + } else { + 0 + }; + baud_rate * (16 * (psd + 1) * divisor) +} + +/// Similar to [`calc_baud_rate`] but with known frequency to calculate the +/// divisor. +pub fn calc_divisor( + frequency: u32, + baud_rate: u32, + prescaler_division_factor: Option, +) -> Result { + calc_baud_rate(frequency, baud_rate, prescaler_division_factor) + .map_err(|e| NonIntegerDivisorError { + frequency: e.frequency, + prescaler_division_factor: e.prescaler_division_factor, + baud_rate, + }) + // Unlikely but better be safe with an explicit panic. + .map(|val| u16::try_from(val).unwrap()) +} + +/// Exposes low-level information about the on-chip register layout and provides +/// types that model individual registers. +/// +/// The getters and setters in this module operate exclusively on raw bit +/// representations within the local computing context. They are limited to +/// extracting or updating the corresponding fields and do not perform direct +/// hardware access. +pub mod registers { + use bitflags::bitflags; + + /// Provides the register offset from the base register. + pub mod offsets { + + /// For reads the Receiver Holding Register (RHR) and for writes the + /// Transmitter Holding Register (THR), effectively acting as + /// **data** register. + pub const DATA: usize = 0; + + /// Interrupt Enable Register (IER). + pub const IER: usize = 1; + + /// Interrupt Status Register (ISR). + /// + /// This register is used on **reads** from offset `2`. + pub const ISR: usize = 2; + + /// FIFO Control Register (FSR). + /// + /// This register is used on **writes** to offset `2`. + pub const FCR: usize = 2; + + /// Line Control Register (LCR). + pub const LCR: usize = 3; + + /// Modem Control Register (MCR). + pub const MCR: usize = 4; + + /// Line Status Register (LSR). + pub const LSR: usize = 5; + + /// Modem Status Register (MSR). + pub const MSR: usize = 6; + + /// Scratch Pad Register (SPR). + pub const SPR: usize = 7; + + /* Registers accessible only when DLAB = 1 */ + + /// Divisor Latch, Least significant byte (DLL). + /// + /// This is the low byte of the 16 bit divisor. + pub const DLL: usize = 0; + + /// Divisor Latch, Most significant byte (DLL). + /// + /// This is the high byte of the 16 bit divisor. + pub const DLM: usize = 1; + + /// Prescaler Division. + /// + /// This is a non-standard register (i.e., it is not present in the + /// industry standard 16550 UART). + pub const PSD: usize = 6; + } + + /// Typing of the data register (RHR / THR). + pub type DATA = u8; + + bitflags! { + /// Typing of the Interrupt Enable Register (IER). + /// + /// This register individually enables each of the possible interrupt + /// sources. A logic "1" in any of these bits enables the corresponding + /// interrupt, while a logic "0" disables it. + /// + /// This is a **read/write** register. + #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] + pub struct IER: u8 { + /// Enables the data ready interrupt. + /// + /// This means data can be read (again). + const DATA_READY = 1 << 0; + /// Enables the THR Empty interrupt. + /// + /// This means data can be written (again). + const THR_EMPTY = 1 << 1; + /// Enables the Receiver Line Status interrupt. + /// + /// This means an error occurred: parity, framing, overrun. + const RECEIVER_LINE_STATUS = 1 << 2; + /// Enables the Modem Status interrupt. + /// + /// This tells you if the remote is ready for receive. + const MODEM_STATUS = 1 << 3; + /// Reserved. + const _RESERVED0 = 1 << 4; + /// Reserved. + const _RESERVED1 = 1 << 5; + /// Enables the non-standard interrupt issued when a DMA reception + /// transfer is finished. + const DMA_RX_END = 1 << 6; + /// Enables the non-standard interrupt issued when a DMA + /// transmission transfer is finished. + const DMA_TX_END = 1 << 7; + } + } + + bitflags! { + /// Typing of the Interrupt Status Register (ISR). + /// + /// **Read-only** register at offset [`offsets::ISR`] for identifying + /// the interrupt with the highest priority that is currently pending. + #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] + pub struct ISR: u8 { + /// Indicates whether an interrupt is pending (0) or not + /// (1). An interrupt is pending if this bit is cleared ('0'). + const INTERRUPT_STATUS = 1 << 0; + /// Interrupt Identification Code (IIC, bit 0). + const IIC_0 = 1 << 1; + /// Interrupt Identification Code (IIC, bit 1). + const IIC_1 = 1 << 2; + /// Interrupt Identification Code (IIC, bit 2). + const IIC_2 = 1 << 3; + /// Reflects the state of the dmarx_end input pin which + /// signals the end of a complete DMA transfer for received data. + /// + /// This is a non-standard flag that is enabled only if DMA End + /// signaling has been enabled with bit 4 of FCR register. Otherwise + /// it will always be read as '0'. + const DMA_RX_END = 1 << 4; + /// Reflects the state of the dmatx_end input pin which + /// signals the end of a complete DMA transfer for transmitted data. + /// This is a non-standard flag that is enabled only if DMA End + /// signaling has been enabled with bit 4 of FCR register. Otherwise + /// it will always be read as '0'. + const DMA_TX_END = 1 << 5; + /// Set if FIFOs are implemented and enabled (by setting FCR bit 0). + /// + /// Cleared in non-FIFO (16450) mode. + const FIFOS_ENABLED0 = 1 << 6; + /// Set if FIFOs are implemented and enabled (by setting FCR bit 0). + /// + /// Cleared in non-FIFO (16450) mode. + const FIFOS_ENABLED1 = 1 << 7; + } + } + + impl ISR { + /// Returns the matching [`InterruptType`], if there is an interrupt. + /// + /// The priority of the interrupt is available via + /// [`InterruptType::priority`]. + #[must_use] + pub fn interrupt_type(self) -> Option { + InterruptType::from_bits(self.bits()) + } + } + + /// The possible interrupt types reported by the [`ISR`]. + /// + /// + /// This type is a convenient and non-ABI compatible abstraction. + #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub enum InterruptType { + /// There is an overrun error, parity error, framing error or break + /// interrupt indication corresponding to the received data on top of + /// the receiver's FIFO. + /// + /// Note that the FIFO error flag in LSR does not + /// influence this interrupt, which is related only to the data on top + /// of the Rx FIFO. This is directly related to the presence of a 1 in + /// any of the LSR bits 1 to 4. + /// + /// **Interrupt reset method:** Read the Line Status Register (LSR). + ReceiverLineStatus, + /// In non-FIFO mode, there is received data available in the RHR + /// register. + /// + /// In FIFO-mode, the number of characters in the reception FIFO is + /// equal or greater than the trigger level programmed in FCR. Note that + /// this is not directly related to LSR bit 0, which always indicates + /// that there is at least one word ready. + /// + /// **Interrupt reset method:** Read the Receiver Holding Register (RHR). + ReceivedDataReady, + /// There is at least one character in the receiver's FIFO and during a + /// time corresponding to four characters at the selected baud rate no + /// new character has been received and no reading has been executed on + /// the receiver's FIFO. + /// + /// **Interrupt reset method:** Read the Receiver Holding Register (RHR). + ReceptionTimeout, + /// In non-FIFO mode, the 1-byte THR is empty. In FIFO mode, the + /// complete 16-byte transmitter's FIFO is empty, so 1 to 16 characters + /// can be written to THR. + /// + /// That is to say, THR Empty bit in LSR is one. + /// + /// **Interrupt reset method:** Write the data register. Alternatively, + /// reading the Interrupt Status Register (ISR) will also clear the + /// interrupt if this is the interrupt type being currently indicated + /// (this will not clear the flag in the LSR). + TransmitterHoldingRegisterEmpty, + /// A change has been detected in the Clear To Send (CTS), Data Set + /// Ready (DSR) or Carrier Detect (CD) input lines or a trailing edge + /// in the Ring Indicator (RI) input line. + /// + /// That is to say, at least one of MSR bits 0 to 3 is one. + /// + /// **Interrupt reset method:** Read the Modem Status Register (MSR) . + ModemStatus, + /// A '1' has been detected in the dmarx_end input pin. This is supposed + /// to imply the end of a complete DMA transfer for received data, + /// executed by a DMA controller that provides this signal. + /// + /// **Interrupt reset method:** Read the Interrupt Status Register (ISR) + /// (return of dmarx_end to zero does not reset the interrupt). + DmaReceptionEndOfTransfer, + /// A '1' has been detected in the dmatx_end input pin. This is supposed + /// to imply the end of a complete DMA transfer for received data, + /// executed by a DMA controller that provides this signal. + /// + /// **Interrupt reset method:** Read the Interrupt Status Register (ISR) + /// (return of dmatx_end to zero does not reset the interrupt). + DmaTransmissionEndOfTransfer, + } + + impl InterruptType { + /// Returns the priority level. + /// + /// Priority 1 is highest and 6 is lowest. + /// + /// The last two priority levels are not found in standard 16550 UART + /// and may appear only if the DMA End signaling is enabled + /// (bit 4 of FCR). + #[must_use] + pub const fn priority(self) -> u8 { + match self { + Self::ReceiverLineStatus => 1, + Self::ReceivedDataReady => 2, + Self::ReceptionTimeout => 2, + Self::TransmitterHoldingRegisterEmpty => 3, + Self::ModemStatus => 4, + Self::DmaReceptionEndOfTransfer => 5, + Self::DmaTransmissionEndOfTransfer => 6, + } + } + + /// Returns a [`InterruptType`] that corresponds to the bits in + /// [`ISR`] + #[must_use] + pub fn from_bits(isr_bits: u8) -> Option { + let bits = isr_bits & 0xf; + + let has_interrupt = (bits & 1) == 0; + if !has_interrupt { + return None; + } + let bits = bits >> 1; + + // Taken from the table on page 11/18 in + let typ = match bits { + 0b011 => Self::ReceiverLineStatus, + 0b010 => Self::ReceivedDataReady, + 0b110 => Self::ReceptionTimeout, + 0b001 => Self::TransmitterHoldingRegisterEmpty, + 0b000 => Self::ModemStatus, + 0b111 => Self::DmaReceptionEndOfTransfer, + 0b101 => Self::DmaTransmissionEndOfTransfer, + _ => panic!("unexpected bit sequence: {bits:x}"), + }; + + Some(typ) + } + } + + bitflags! { + /// Typing of the FIFO Control Register (FCR). + /// + /// **Write-only** register at offset [`offsets::FCR`] used to enable or + /// disable FIFOs, clear receive/transmit FIFOs, and set the receiver + /// trigger level. + #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] + pub struct FCR: u8 { + /// When set ('1') this bits enables both the transmitter and + /// receiver FIFOs. + /// + /// In any writing to FCR, this bit must be set in order to affect + /// the rest of the bits, except for bit 4. Changing this bit + /// automatically resets both FIFOs. + const FIFO_ENABLE = 1 << 0; + /// Writing a one to this bit resets the receiver's FIFO (the + /// pointers are reset and all the words are cleared). + /// + /// The Receiver Shift Register is not cleared, so any reception + /// active will continue. The bit will automatically return to zero. + const RX_FIFO_RESET = 1 << 1; + /// Writing a one to this bit resets the transmitter's FIFO (the + /// pointers are reset). + /// + /// The Transmitter Shift Register is not cleared, so any + /// transmission active will continue. The bit will automatically + /// return to zero. + const TX_FIFO_RESET = 1 << 2; + /// Selects the DMA mode. The DMA mode affects the way in + /// which the DMA signaling outputs pins (txrdy, rxrdy and their + /// inverted versions) behave. + /// + /// See the DMA signals explanation in the [datasheet] for details. + /// + /// [datasheet]: https://caro.su/msx/ocm_de1/16550.pdf + /// + /// Mode 0 is intended to transfer one character at a time. Mode 1 + /// is intended to transfer a set of characters at a time. + /// + /// # Recommendation + /// This is typically not set in a kernel. + const DMA_MODE = 1 << 3; + /// Enables the DMA End signaling. + /// + /// This non-standard feature is useful when the UART is connected + /// to a DMA controller which provides signals to indicate when a + /// complete DMA transfer has been completed, either for reception + /// or transmission (dmaend_rx and dmaend_tx input pins). + const ENABLE_DMA_END = 1 << 4; + /// Reserved. + const _RESERVED0 = 1 << 5; + /// First bit of [`FifoTriggerLevel`]. + const RX_FIFO_TRIGGER_LEVEL0 = 1 << 6; + /// Second bit of [`FifoTriggerLevel`]. + const RX_FIFO_TRIGGER_LEVEL1 = 1 << 7; + } + } + + impl FCR { + /// Returns the trigger level of the FIFO. + #[must_use] + pub const fn fifo_trigger_level(self) -> FifoTriggerLevel { + let bits = (self.bits() >> 6) & 0b11; + FifoTriggerLevel::from_raw_bits(bits) + } + + /// Sets the trigger level of the FIFO. + #[must_use] + pub fn set_fifo_trigger_level(self, value: FifoTriggerLevel) -> Self { + self | Self::from_bits_retain(value.to_raw_bits()) + } + } + + /// The trigger level for the receiver's FIFO defined in [`FCR`]. + /// + /// In FIFO mode an interrupt will be generated (if enabled) when the number + /// of words in the receiver's FIFO is equal or greater than this trigger + /// level. + /// + /// Besides, for FIFO mode operation a time out mechanism is implemented. + /// Independently of the trigger level of the FIFO, an interrupt will be + /// generated if there is at least one word in the FIFO and for a time + /// equivalent to the transmission of four characters. + /// + /// This type is a convenient and non-ABI compatible abstraction. ABI + /// compatibility is given via [`FifoTriggerLevel::from_raw_bits`] and + /// [`FifoTriggerLevel::to_raw_bits`]. + #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub enum FifoTriggerLevel { + /// Interrupt is created after every character. + One, + /// Interrupt is created after every four characters. + Four, + /// Interrupt is created after every eight characters. + Eight, + /// Interrupt is created after every fourteen characters. + /// + /// # Recommendation + /// This is the recommended default for best system performance. + #[default] + Fourteen, + } + + impl FifoTriggerLevel { + /// Translates the raw encoding into the corresponding value. + /// + /// This function operates on the value as-is and does not perform any + /// shifting bits. + #[must_use] + pub const fn from_raw_bits(bits: u8) -> Self { + let bits = bits & 0b11; + match bits { + 0b00 => Self::One, + 0b01 => Self::Four, + 0b10 => Self::Eight, + 0b11 => Self::Fourteen, + _ => unreachable!(), + } + } + + /// Translates the value into the corresponding raw encoding. + /// + /// This function operates on the value as-is and does not perform any + /// shifting bits. + #[must_use] + pub const fn to_raw_bits(self) -> u8 { + match self { + Self::One => 0b00, + Self::Four => 0b01, + Self::Eight => 0b10, + Self::Fourteen => 0b11, + } + } + } + + bitflags! { + /// Typing of the Line Control Register (LCR). + /// + /// Configures the serial frame format including word length, stop bits, + /// parity, and controls access to the divisor latches via DLAB. + #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] + pub struct LCR: u8 { + /// First bit of [`WordLength`]. + const WORD_LENGTH0 = 1 << 0; + /// Second bit of [`WordLength`]. + const WORD_LENGTH1 = 1 << 1; + /// If cleared, only one stop bit will be transmitted. If set, two + /// stop bits (1.5 with 5-bit data) will be transmitted before the + /// start bit of the next character. + const MORE_STOP_BITS = 1 << 2; + /// Second bit of [`Parity`]. + const PARITY0 = 1 << 3; + /// Second bit of [`Parity`]. + const PARITY1 = 1 << 4; + /// Second bit of [`Parity`]. + const PARITY2 = 1 << 5; + /// When this bit is set a break condition is forced in the + /// transmission line. The serial output pin (txd) is forced to the + /// spacing state (zero). + /// + /// When this bit is cleared, the break state is removed. + /// + /// # Recommendation + /// Typically, this is not set in console/TTY use-cases. + const SET_BREAK = 1 << 6; + /// This is Divisor Latch Access Bit (DLAB). + /// + /// This bit **must** be set in order to access the [`DLL`], + /// [`DLM`], and [`PSD`]. + const DLAB = 1 << 7; + } + } + + impl LCR { + /// Returns the [`WordLength`]. + #[must_use] + pub const fn word_length(self) -> WordLength { + let bits = self.bits() & 0b11; + WordLength::from_raw_bits(bits) + } + + /// Sets the [`WordLength`]. + #[must_use] + pub fn set_word_length(self, value: WordLength) -> Self { + self | Self::from_bits_retain(value.to_raw_bits()) + } + + /// Returns the [`Parity`]. + #[must_use] + pub const fn parity(self) -> Parity { + let bits = (self.bits() >> 3) & 0b111; + Parity::from_raw_bits(bits) + } + + /// Sets the [`Parity`]. + #[must_use] + pub fn set_parity(self, value: Parity) -> Self { + self | Self::from_bits_retain(value.to_raw_bits()) + } + } + + /// The length of words for the transmission and reception in [`LCR`]. + /// + /// This type is a convenient and non-ABI compatible abstraction. ABI + /// compatibility is given via [`WordLength::from_raw_bits`] and + /// [`WordLength::to_raw_bits`]. + #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub enum WordLength { + /// Interrupt is created after every character. + FiveBits, + /// Interrupt is created after every four characters. + SixBits, + /// Interrupt is created after every eight characters. + SevenBits, + /// Interrupt is created after every fourteen characters. + /// + /// # Recommendation + /// This is the recommended default. + #[default] + EightBits, + } + + impl WordLength { + /// Translates the raw encoding into the corresponding value. + /// + /// This function operates on the value as-is and does not perform any + /// shifting bits. + #[must_use] + pub const fn from_raw_bits(bits: u8) -> Self { + let bits = bits & 0b11; + match bits { + 0b00 => Self::FiveBits, + 0b01 => Self::SixBits, + 0b10 => Self::SevenBits, + 0b11 => Self::EightBits, + _ => unreachable!(), + } + } + + /// Translates the value into the corresponding raw encoding. + /// + /// This function operates on the value as-is and does not perform any + /// shifting bits. + #[must_use] + pub const fn to_raw_bits(self) -> u8 { + match self { + Self::FiveBits => 0b00, + Self::SixBits => 0b01, + Self::SevenBits => 0b10, + Self::EightBits => 0b11, + } + } + + /// Try to create the type from an integer representation of the baud rate. + #[must_use] + pub const fn from_integer(value: u8) -> Self { + match value { + 5 => Self::FiveBits, + 6 => Self::SixBits, + 7 => Self::SevenBits, + 8 => Self::EightBits, + _ => Self::EightBits, + } + } + + /// Returns the value as corresponding integer. + #[must_use] + pub const fn to_integer(self) -> u32 { + match self { + Self::FiveBits => 5, + Self::SixBits => 6, + Self::SevenBits => 7, + Self::EightBits => 8, + } + } + } + + /// The length of words for the transmission as well as reception. + /// + /// This type is a convenient and non-ABI compatible abstraction. ABI + /// compatibility is given via [`Parity::from_raw_bits`] and + /// [`Parity::to_raw_bits`]. + #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub enum Parity { + /// No parity bit is transmitted nor expected. + #[default] + Disabled, + /// The number of bits including the parity bit must be odd. + Odd, + /// The number of bits including the parity bit must be even. + Even, + /// The parity bit is sent as/checked to be `1`. + Forced1, + /// The parity bit is sent as/checked to be `0`. + Forced0, + } + + impl Parity { + /// Translates the raw encoding into the corresponding value. + /// + /// This function operates on the value as-is and does not perform any + /// shifting bits. + #[must_use] + pub const fn from_raw_bits(bits: u8) -> Self { + let bits = bits & 0b111; + let disabled = (bits & 1) == 0; + if disabled { + return Self::Disabled; + } + let bits = bits >> 1; + match bits { + 0b00 => Self::Odd, + 0b01 => Self::Even, + 0b10 => Self::Forced1, + 0b11 => Self::Forced0, + // We only have two bits left to check + _ => unreachable!(), + } + } + + /// Translates the value into the corresponding raw encoding. + /// + /// This function operates on the value as-is and does not perform any + /// shifting bits. + #[must_use] + pub const fn to_raw_bits(self) -> u8 { + match self { + Self::Disabled => 0b000, + Self::Odd => 0b001, + Self::Even => 0b011, + Self::Forced1 => 0b101, + Self::Forced0 => 0b111, + } + } + } + + bitflags! { + /// Typing of the Modem Control Register (MCR). + /// + /// Controls modem interface output signal. + /// + /// This is a **read/write** register. + #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] + pub struct MCR: u8 { + /// Controls the "data terminal ready" active low output (dtr_n). + /// + /// Signals the remote that the local UART device is powered on. + /// + /// A 1 in this bit makes dtr_n output a 0. When the bit is cleared, + /// dtr_n outputs a 1. + const DTR = 1 << 0; + /// Controls the "request to send" active low output (rts_n) in the + /// same way as bit 0 controls dtr_n. + /// + /// Signals the remote that the local UART is ready to receive data. + const RTS = 1 << 1; + /// Controls the general purpose, active low, output out1_n in the + /// same way as bit 0 controls dtr_n. + const OUT_1 = 1 << 2; + /// Controls the general purpose, active low, output out2_n in + /// the same way as bit 0 controls dtr_n. + /// + /// Besides, in typical x86 systems this acts as a global interrupt + /// enable bit as it is connected to the systems interrupt + /// controller. In this case, the complementary interrupt lines + /// irq and irq_n will become active (1 and 0 respectively) only if + /// this bit is 1 (and an interrupt condition is taken place). + const OUT_2_INT_ENABLE = 1 << 3; + /// Activate the loop back mode. Loop back mode is intended to test + /// the UART communication. + /// + /// The serial output is connected internally to the serial input, + /// so every character sent is looped back and received. + const LOOP_BACK = 1 << 4; + /// Reserved. + const _RESERVED0 = 1 << 5; + /// Reserved. + const _RESERVED1 = 1 << 6; + /// Reserved. + const _RESERVED2 = 1 << 7; + } + } + + bitflags! { + /// Typing of the Line Status Register (MCR). + /// + /// Reports the current status of the transmitter and receiver, + /// including data readiness, errors, and transmitter emptiness. + /// + /// This is a **read-only** register. + #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] + pub struct LSR: u8 { + /// It is set if one of more characters have been received and are + /// waiting in the receiver's FIFO for the user to read them. + /// + /// It is zero if there is no available data in the receiver's FIFO. + const DATA_READY = 1 << 0; + /// Overrun Error flag. When it is set, a character has been + /// completely assembled in the Receiver Shift Register without + /// having free space to put it in the receiver's FIFO or holding + /// register. + /// + /// When an overrun condition appears, the result is different + /// depending on whether the 16-byte FIFO is active or not. + const OVERRUN_ERROR = 1 << 1; + /// Parity Error flag. When it is set, it indicates that the parity + /// of the received character is wrong according to the current + /// setting in LCR. + /// + /// This bit is cleared as soon as the LSR is read. + const PARITY_ERROR = 1 << 2; + /// Framing Error flag. It indicates that the received character did + /// not have a valid stop bit (i.e., a 0 was detected in the (first) + /// stop bit position instead of a 1). + /// + /// This bit is cleared as soon as the LSR is read + const FRAMING_ERROR = 1 << 3; + /// Break Interrupt indicator. It is set to 1 if the receiver's line + /// input rxd was held at zero for a complete character time. + /// + /// It is to say, the positions corresponding to the start bit, the + /// data, the parity bit (if any) and the (first) stop bit were all + /// detected as zeroes. Note that a Frame Error flag always + /// accompanies this flag. + /// + /// This bit is cleared as soon as the LSR is read. + const BREAK_INTERRUPT = 1 << 4; + /// Transmit Holding Register Empty flag aka "ready to send". + /// In non-FIFO mode, this bit is set whenever the 1-byte THR is + /// empty. + /// + /// If the THR holds data to be transmitted, THR is immediately set + /// when this data is passed to the TSR (Transmitter Shift Register). + /// **In FIFO mode, this bit is set when the transmitter's FIFO is + /// completely empty, being 0 if there is at least one byte in the + /// FIFO waiting to be passed to the TSR for transmission.** + /// + /// This bit is cleared when the microprocessor writes new data in + /// the THR (the data register). + const THR_EMPTY = 1 << 5; + /// Transmitter Empty flag. It is 1 when both the THR (or + /// transmitter's FIFO) and the TSR are empty. + /// + /// Reading this bit as 1 means that no transmission is currently + /// taking place in the txd output pin, the transmission line is + /// idle. + /// + /// As soon as new data is written in the THR, this bit will be + /// cleared. + const TRANSMITTER_EMPTY = 1 << 6; + /// This the FIFO data error bit. If the FIFO is not implemented or + /// disabled (16450 mode), this bit is always zero. + /// + /// If the FIFO is active, this bit will be set as soon as any data + /// character in the receiver's FIFO has parity or framing error or + /// the break indication active. + /// + /// The bit is cleared when the microprocessor reads the LSR and the + /// rest of the data in the receiver's FIFO do not have any of these + /// three associated flags on. + const FIFO_DATA_ERROR = 1 << 7; + } + } + + bitflags! { + /// Typing of the Modem Status Register (MSR). + /// + /// Reflects the current state and change status of modem input. + /// + /// This is a **read-only** register. + #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] + pub struct MSR: u8 { + /// delta-CTS flag. If set, it means that the cts_n input has + /// changed since the last time the microprocessor read this + /// register. + const DELTA_CTS = 1 << 0; + /// delta-DSR flag. If set, it means that the dsr_n input has + /// changed since the last time the microprocessor read this + /// register. + const DELTA_DSR = 1 << 1; + /// Set when a trailing edge is detected in the ri_n input pin, it + /// is to say, when ri_n changes from 0 to 1. + const TRAILING_EDGE_RI = 1 << 2; + /// delta-CD flag. If set, it means that the cd_n input has changed + /// since the last time the microprocessor read this register. + const DELTA_CD = 1 << 3; + /// Clear To Send (CTS) is the complement of the cts_n input. + /// + /// This information comes from the remote side and tells if + /// the remote can receive more data. + const CTS = 1 << 4; + /// Data Set Ready (DSR) is the complement of the dsr_n input. + /// + /// This information comes from the remote side and tells if + /// the remote is powered on (hardware is present). + const DSR = 1 << 5; + /// Ring Indicator (RI) is the complement of the ri_n input. + const RI = 1 << 6; + /// Carrier Detect (CD) is the complement of the cd_n input. + /// + /// This information comes from the remote side and tells if + /// the remote is actively messaged it is there. + const CD = 1 << 7; + } + } + + /// Typing of the Scratch Pad Register (SPR). + /// + /// General-purpose read/write register with no defined hardware function, + /// intended for software use or probing UART presence. + /// + /// This is a **read/write** register. + pub type SPR = u8; + + /// Typing of the divisor latch register (low byte). + /// + /// Used to control the effective baud rate (see [`super::calc_baud_rate`]). + /// + /// This is a **read/write** register. + pub type DLL = u8; + + /// Typing of the divisor latch register (high byte). + /// + /// Used to control the effective baud rate (see [`super::calc_baud_rate`]). + /// + /// This is a **read/write** register. + pub type DLM = u8; + + /// All legal divisors for [`DLL`] and [`DLM`] that can create a valid and + /// even baud rate using [`super::calc_baud_rate`]. + #[allow(missing_docs)] + #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] + #[repr(u16)] + pub enum Divisor { + #[default] // => baud rate 115200 + Divisor1, + Divisor2, + Divisor3, + Divisor4, + Divisor5, + Divisor6, + Divisor8, + Divisor9, + Divisor10, + Divisor12, + Divisor15, + Divisor16, + Divisor18, + Divisor20, + Divisor24, + Divisor25, + Divisor30, + Divisor32, + Divisor36, + Divisor40, + Divisor45, + Divisor48, + Divisor50, + Divisor60, + Divisor64, + Divisor72, + Divisor75, + Divisor80, + Divisor90, + Divisor96, + Divisor100, + Divisor120, + Divisor128, + Divisor144, + Divisor150, + Divisor160, + Divisor180, + Divisor192, + Divisor200, + Divisor225, + Divisor240, + Divisor256, + Divisor288, + Divisor300, + Divisor320, + Divisor360, + Divisor384, + Divisor400, + Divisor450, + Divisor480, + Divisor512, + Divisor576, + Divisor600, + Divisor640, + Divisor720, + Divisor768, + Divisor800, + Divisor900, + Divisor960, + Divisor1152, + Divisor1200, + Divisor1280, + Divisor1440, + Divisor1536, + Divisor1600, + Divisor1800, + Divisor1920, + Divisor2304, + Divisor2400, + Divisor2560, + Divisor2880, + Divisor3200, + Divisor3600, + Divisor3840, + Divisor4608, + Divisor4800, + Divisor5760, + Divisor6400, + Divisor7200, + Divisor7680, + Divisor9600, + Divisor11520, + Divisor12800, + Divisor14400, + Divisor19200, + Divisor23040, + Divisor28800, + Divisor38400, + Divisor57600, + } + + bitflags! { + /// Typing of the Prescaler Division (PSD) register. + /// + /// This is a non-standard register (i.e., it is not present in the + /// industry standard 16550 UART). Its purpose is to provide a second + /// division factor that could be useful in systems which are driven by + /// a clock multiple of one of the typical frequencies used with this + /// UART. + #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] + pub struct PSD: u8 { + /// Prescaler's Division Factor (bit 0). + const PDF0 = 1 << 0; + /// Prescaler's Division Factor (bit 1). + const PDF1 = 1 << 1; + /// Prescaler's Division Factor (bit 2). + const PDF2 = 1 << 2; + /// Prescaler's Division Factor (bit 3). + const PDF3 = 1 << 3; + /// Reserved. + const _RESERVED0 = 1 << 4; + /// Reserved. + const _RESERVED1 = 1 << 5; + /// Reserved. + const _RESERVED2 = 1 << 6; + /// Reserved. + const _RESERVED3 = 1 << 7; + } + } + + impl PSD { + /// Returns the Prescaler's Division Factor (PDF). + #[must_use] + pub const fn pdf(self) -> u8 { + self.bits() & 0xf + } + + /// Sets the Prescaler's Division Factor (PDF). + #[must_use] + pub const fn set_pdf(self, pdf: u8) -> Self { + Self::from_bits_retain(pdf) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_calc_baud_rate() { + assert_eq!(calc_baud_rate(CLK_FREQUENCY_HZ, 1, None), Ok(115200)); + assert_eq!(calc_baud_rate(CLK_FREQUENCY_HZ, 2, None), Ok(57600)); + assert_eq!(calc_baud_rate(CLK_FREQUENCY_HZ, 3, None), Ok(38400)); + assert_eq!(calc_baud_rate(CLK_FREQUENCY_HZ, 6, None), Ok(19200)); + assert_eq!(calc_baud_rate(CLK_FREQUENCY_HZ, 9, None), Ok(12800)); + assert_eq!(calc_baud_rate(CLK_FREQUENCY_HZ, 12, None), Ok(9600)); + assert_eq!(calc_baud_rate(CLK_FREQUENCY_HZ, 15, None), Ok(7680)); + assert_eq!(calc_baud_rate(CLK_FREQUENCY_HZ, 16, None), Ok(7200)); + assert_eq!( + calc_baud_rate(CLK_FREQUENCY_HZ, 73, None), + Err(NonIntegerBaudRateError { + frequency: CLK_FREQUENCY_HZ, + divisor: 73, + prescaler_division_factor: None, + }) + ); + } + + #[test] + fn test_calc_frequency() { + assert_eq!(calc_frequency(115200, 1, None), CLK_FREQUENCY_HZ); + assert_eq!(calc_frequency(57600, 2, None), CLK_FREQUENCY_HZ); + assert_eq!(calc_frequency(38400, 3, None), CLK_FREQUENCY_HZ); + assert_eq!(calc_frequency(19200, 6, None), CLK_FREQUENCY_HZ); + assert_eq!(calc_frequency(12800, 9, None), CLK_FREQUENCY_HZ); + assert_eq!(calc_frequency(9600, 12, None), CLK_FREQUENCY_HZ); + assert_eq!(calc_frequency(7680, 15, None), CLK_FREQUENCY_HZ); + assert_eq!(calc_frequency(7200, 16, None), CLK_FREQUENCY_HZ); + } + + #[test] + fn test_calc_divisor() { + assert_eq!(calc_divisor(CLK_FREQUENCY_HZ, 115200, None), Ok(1)); + assert_eq!(calc_divisor(CLK_FREQUENCY_HZ, 57600, None,), Ok(2)); + assert_eq!(calc_divisor(CLK_FREQUENCY_HZ, 38400, None,), Ok(3)); + assert_eq!(calc_divisor(CLK_FREQUENCY_HZ, 19200, None,), Ok(6)); + assert_eq!(calc_divisor(CLK_FREQUENCY_HZ, 12800, None,), Ok(9)); + assert_eq!(calc_divisor(CLK_FREQUENCY_HZ, 9600, None,), Ok(12)); + assert_eq!(calc_divisor(CLK_FREQUENCY_HZ, 7680, None,), Ok(15)); + assert_eq!(calc_divisor(CLK_FREQUENCY_HZ, 7200, None,), Ok(16)); + assert_eq!( + calc_divisor(CLK_FREQUENCY_HZ, 7211, None), + Err(NonIntegerDivisorError { + frequency: CLK_FREQUENCY_HZ, + baud_rate: 7211, + prescaler_division_factor: None, + }) + ); + } +} diff --git a/src/tty.rs b/src/tty.rs new file mode 100644 index 0000000..e354db7 --- /dev/null +++ b/src/tty.rs @@ -0,0 +1,175 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 + +//! Provides a thin abstraction over a [`Uart16550`] for VT102-like terminal +//! emulators on the receiving side. +//! +//! This module is suited for basic use cases and toy projects, but full VT102 +//! compatibility is explicitly not a goal. +//! +//! For lower-level access of the underlying hardware, use [`Uart16550`] +//! instead. +//! +//! See [`Uart16550Tty`]. + +use crate::backend::{Backend, MmioAddress, MmioBackend, RegisterAddress}; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +use crate::backend::{PioBackend, PortIoAddress}; +use crate::{Config, InitError, InvalidAddressError, LoopbackError, Uart16550}; +use core::error::Error; +use core::fmt::{self, Display, Formatter}; + +/// Errors that [`Uart16550Tty::new_port`] and [`Uart16550Tty::new_mmio`] may +/// return. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum Uart16550TtyError { + /// The underlying address is invalid. + AddressError(InvalidAddressError), + /// Error initializing the device. + InitError(InitError), + /// The device could not be tested. + TestError(LoopbackError), +} + +impl Display for Uart16550TtyError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Self::AddressError(e) => { + write!(f, "{e}") + } + Self::InitError(e) => { + write!(f, "error initializing the device: {e}") + } + Self::TestError(e) => { + write!(f, "error testing the device: {e}") + } + } + } +} + +impl Error for Uart16550TtyError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + Self::AddressError(e) => Some(e), + Self::InitError(e) => Some(e), + Self::TestError(e) => Some(e), + } + } +} + +/// Thin opinionated abstraction over [`Uart16550`] that helps to send Rust +/// strings easily to the other side, assuming the remote is a TTY (terminal). +/// +/// It is especially suited as very easy way to see something when you develop +/// and test things in a VM. +/// +/// It implements [`fmt::Write`]. +/// +/// # Example +/// ```rust,no_run +/// use uart_16550::{Config, Uart16550Tty}; +/// use core::fmt::Write; +/// +/// let mut uart = unsafe { Uart16550Tty::new_port(0x3f8, Config::default()).expect("should initialize device") }; +/// // ^ you could also use `new_mmio(0x1000 as *mut _)` here +/// uart.write_str("hello world\nhow's it going?"); +/// ``` +/// +/// # MMIO and Port I/O +/// +/// The constructors `new_port()` and `new_mmio()` create an abstraction with +/// the corresponding backend. +#[derive(Debug)] +pub struct Uart16550Tty(Uart16550); + +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +impl Uart16550Tty { + /// Creates a new [`Uart16550Tty`] backed by x86 port I/O. + /// + /// Initializes the device and performs a self-test. + /// + /// # Safety + /// + /// Callers must ensure that the base port is valid and safe to use for the + /// **whole lifetime** of the device. Further, all [`NUM_REGISTERS`] + /// registers must be safely reachable from the base address. + /// + /// [`NUM_REGISTERS`]: crate::spec::NUM_REGISTERS + pub unsafe fn new_port( + base_port: u16, + config: Config, + ) -> Result> { + // SAFETY: The I/O port is valid and we have exclusive access. + let mut inner = + unsafe { Uart16550::new_port(base_port).map_err(Uart16550TtyError::AddressError)? }; + inner.init(config).map_err(Uart16550TtyError::InitError)?; + inner + .test_loopback() + .map_err(Uart16550TtyError::TestError)?; + Ok(Self(inner)) + } +} + +impl Uart16550Tty { + /// Creates a new [`Uart16550Tty`] backed by MMIO. + /// + /// Initializes the device and performs a self-test. + /// + /// # Safety + /// + /// Callers must ensure that the base address is valid and safe to use for + /// the **whole lifetime** of the device. Further, all [`NUM_REGISTERS`] + /// registers must be safely reachable from the base address. + /// + /// [`NUM_REGISTERS`]: crate::spec::NUM_REGISTERS + pub unsafe fn new_mmio( + base_address: *mut u8, + config: Config, + ) -> Result> { + // SAFETY: The I/O port is valid and we have exclusive access. + let mut inner = + unsafe { Uart16550::new_mmio(base_address).map_err(Uart16550TtyError::AddressError)? }; + + inner.init(config).map_err(Uart16550TtyError::InitError)?; + inner + .test_loopback() + .map_err(Uart16550TtyError::TestError)?; + Ok(Self(inner)) + } +} + +impl Uart16550Tty { + /// Returns a reference to the underlying [`Uart16550`]. + pub const fn inner(&self) -> &Uart16550 { + &self.0 + } + + /// Returns a mutable reference to the underlying [`Uart16550`]. + pub const fn inner_mut(&mut self) -> &mut Uart16550 { + &mut self.0 + } +} + +impl fmt::Write for Uart16550Tty { + fn write_str(&mut self, s: &str) -> fmt::Result { + for &byte in s.as_bytes() { + match byte { + // backspace or delete + 8 | 0x7F => { + self.0.send_bytes_all(&[8]); + self.0.send_bytes_all(b" "); + self.0.send_bytes_all(&[8]); + } + // Normal Rust newlines to terminal-compatible newlines. + b'\n' => { + self.0.send_bytes_all(b"\r"); + self.0.send_bytes_all(b"\n"); + } + data => { + self.0.send_bytes_all(&[data]); + } + } + } + + Ok(()) + } +} diff --git a/test/.gitignore b/test/.gitignore new file mode 100644 index 0000000..f540534 --- /dev/null +++ b/test/.gitignore @@ -0,0 +1,4 @@ +target + +serial.mmio.txt +serial.pio.txt diff --git a/test/Cargo.lock b/test/Cargo.lock new file mode 100644 index 0000000..7b3d671 --- /dev/null +++ b/test/Cargo.lock @@ -0,0 +1,44 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "kernel" +version = "0.1.0" +dependencies = [ + "anyhow", + "log", + "qemu-exit", + "uart_16550", +] + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "qemu-exit" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bb0fd6580eeed0103c054e3fba2c2618ff476943762f28a645b63b8692b21c9" + +[[package]] +name = "uart_16550" +version = "0.1.0" +dependencies = [ + "bitflags", +] diff --git a/test/Cargo.toml b/test/Cargo.toml new file mode 100644 index 0000000..c095881 --- /dev/null +++ b/test/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "kernel" +version = "0.1.0" +edition = "2024" +publish = false + +[profile.release] +codegen-units = 1 +lto = true +opt-level = "s" + +[dependencies] +anyhow = { version = "1.0", default-features = false } +log = { version = "0.4", default-features = false } +uart_16550 = { path = "../." } +qemu-exit = { version = "3.0.2", default-features = false } diff --git a/test/Makefile b/test/Makefile new file mode 100644 index 0000000..8e19eef --- /dev/null +++ b/test/Makefile @@ -0,0 +1,37 @@ +.PHONY: default +default: build + + +.PHONY: build +build: + cargo build --release \ + --target ./x86-unknown-none.json \ + -Z build-std=core,alloc,compiler_builtins \ + -Z build-std-features=compiler-builtins-mem + + +.PHONY: run +run: | build + @# QEMU/TCG allows better debuggability over QEMU/KVM + qemu-system-i386 \ + -debugcon stdio \ + -device isa-debug-exit,iobase=0xf4,iosize=0x04 \ + -display none \ + -kernel target/x86-unknown-none/release/kernel \ + -m 64M \ + -machine q35,accel=tcg \ + -monitor none \ + -no-reboot \ + -nodefaults \ + -serial file:serial.pio.txt; \ + rc=$$? ; \ + if [ $$rc -ne 73 ]; then \ + echo "Test Run Failed" >&2 ; \ + exit 1 ; \ + fi + grep "hello from serial via x86 port I/O" ./serial.pio.txt + + +.PHONY: clean +clean: + cargo clean diff --git a/test/build.rs b/test/build.rs new file mode 100644 index 0000000..c6fef93 --- /dev/null +++ b/test/build.rs @@ -0,0 +1,6 @@ +fn main() { + let linker_script = "link.ld"; + + println!("cargo:rerun-if-changed={linker_script}"); + println!("cargo:rustc-link-arg=-T{linker_script}"); +} diff --git a/test/link.ld b/test/link.ld new file mode 100644 index 0000000..889dfa8 --- /dev/null +++ b/test/link.ld @@ -0,0 +1,51 @@ +/* Set entry point to symbol from start.S. */ +ENTRY(start) + +PHDRS +{ + kernel_rx PT_LOAD FLAGS(5); + kernel_rw PT_LOAD FLAGS(4); + kernel_ro PT_LOAD FLAGS(6); +} + +SECTIONS +{ + /* + * Physical LOAD address 8M doesn't clash with firmware or MMIO in QEMU. + */ + .text 8M : AT(8M) ALIGN(4K) + { + /* + * Multiboot (v1) header must be within the first 8192 byte of the ELF. + */ + KEEP(*(.multiboot_header)); + *(.text .text.*) + } : kernel_rx + + .rodata : + { + *(.rodata .rodata.*) + } : kernel_ro + + .data : + { + *(.data .data.*) + } : kernel_rw + + .bss : + { + *(COMMON) + *(.bss .bss.*) + } : kernel_rw + + /DISCARD/ : + { + *(.comment .comment.*) + *(.dynamic) + *(.eh_frame*) + *(.got .got.*) + *(.note.*) + *(.plt .plt.*) + *(.rela .rela.*) + } +} diff --git a/test/rust-toolchain.toml b/test/rust-toolchain.toml new file mode 100644 index 0000000..0244e44 --- /dev/null +++ b/test/rust-toolchain.toml @@ -0,0 +1,19 @@ +# rust-toolchain for rustup +# https://rust-lang.github.io/rustup/overrides.html + +# targets and components are additive to the systems default values +[toolchain] +# rustc 1.94 - nightly needed for the build-std feature +channel = "nightly-2026-01-15" +profile = "minimal" +targets = [ "x86_64-unknown-linux-gnu" ] +components = [ + "cargo", + "clippy", + #"miri", + # for build-std feature + "rust-src", + "rust-std", + "rustc", + "rustfmt", +] diff --git a/test/src/debugcon.rs b/test/src/debugcon.rs new file mode 100644 index 0000000..9b02d74 --- /dev/null +++ b/test/src/debugcon.rs @@ -0,0 +1,69 @@ +//! Driver for QEMU's debugcon device. + +use core::fmt::{Arguments, Write}; +use log::{LevelFilter, Log, Metadata, Record, info}; + +static LOGGER: DebugconLogger = DebugconLogger; + +/// Internal API for the `println!` macro. +pub fn _print(args: Arguments) { + Debugcon.write_fmt(args).unwrap(); +} + +struct Debugcon; + +impl Debugcon { + /// I/O port of QEMUs debugcon device on x86. + const IO_PORT: u16 = 0xe9; + + pub fn write_byte(byte: u8) { + unsafe { + core::arch::asm!( + "out %al, %dx", + in("al") byte, + in("dx") Self::IO_PORT, + options(att_syntax, nomem, nostack, preserves_flags) + ) + } + } +} + +impl Write for Debugcon { + fn write_str(&mut self, s: &str) -> core::fmt::Result { + for &byte in s.as_bytes() { + Debugcon::write_byte(byte); + } + Ok(()) + } +} + +pub struct DebugconLogger; + +impl DebugconLogger { + pub fn init() { + // Ignore, as we can't do anything about it here. + let _ = log::set_logger(&LOGGER); + log::set_max_level(LevelFilter::Trace); + info!("Logger initialized!"); + } +} + +impl Log for DebugconLogger { + fn enabled(&self, _metadata: &Metadata) -> bool { + true + } + + fn log(&self, record: &Record) { + // Ignore result as we can't do anything about it. + let _ = writeln!( + Debugcon, + "[{:>5}: {}@{}]: {}", + record.level(), + record.file().unwrap_or(""), + record.line().unwrap_or(0), + record.args() + ); + } + + fn flush(&self) {} +} diff --git a/test/src/main.rs b/test/src/main.rs new file mode 100644 index 0000000..97281cf --- /dev/null +++ b/test/src/main.rs @@ -0,0 +1,68 @@ +#![no_main] +#![no_std] + +core::arch::global_asm!(include_str!("start.S"), options(att_syntax)); + +struct DummyGlobalAlloc; + +#[global_allocator] +static GLOBAL_ALLOCATOR: DummyGlobalAlloc = DummyGlobalAlloc; + +unsafe impl GlobalAlloc for DummyGlobalAlloc { + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + panic!("unsupported! layout={layout:?}"); + } + + unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { + panic!("unsupported! ptr={ptr:?}, layout={layout:?}"); + } +} + +use core::alloc::{GlobalAlloc, Layout}; +use core::fmt::Write; +use core::panic::PanicInfo; +use log::error; +use qemu_exit::QEMUExit; +use uart_16550::{Config, Uart16550Tty}; + +mod debugcon; + +/// Entry into the Rust code. +#[unsafe(no_mangle)] +extern "C" fn rust_entry() -> ! { + main().expect("Should run kernel"); + unreachable!(); +} + +/// Exits QEMU via the shutdown device on the i440fx board. +fn exit_qemu(success: bool) -> ! { + // configured in Makefile + let port = 0xf4; + let exit = qemu_exit::X86::new(port, 73); + if success { + exit.exit_success() + } else { + exit.exit_failure() + } +} + +/// Executes the kernel's main logic. +fn main() -> anyhow::Result<()> { + debugcon::DebugconLogger::init(); + + // SAFETY: we have exclusive access and the port is valid + unsafe { + let mut uart = Uart16550Tty::new_port(0x3f8, Config::default())?; + uart.write_str("hello from serial via x86 port I/O")?; + } + + // TODO MMIO test? QEMU doesn't offer this (on x86). + + exit_qemu(true); +} + +#[panic_handler] +fn panic_handler(info: &PanicInfo) -> ! { + error!("error: {}", info); + exit_qemu(false); +} diff --git a/test/src/start.S b/test/src/start.S new file mode 100644 index 0000000..ccff51b --- /dev/null +++ b/test/src/start.S @@ -0,0 +1,50 @@ +# GNU Assembler Syntax (GAS) with AT&T Assembly Flavor +# +# This is understood by LLVM. + +# Symbol from Rust +.extern rust_entry + +.code32 + +.section .multiboot_header, "a", @progbits + +/* + * Multiboot v1 Header. + * Required so that we can be booted by QEMU via the "-kernel" parameter. + */ +.align 8 +.global multiboot_header +multiboot_header: + .long 0x1badb002 + .long 0x0 + .long -0x1badb002 + +.section .text + +.global start +start: + # Set-up stack (stack grows downwards) + mov $stack_end, %esp + mov $stack_end, %ebp + + # Pass some parameters to the kernel + mov $0x42 , %eax + mov $0x1337, %ebx + mov $0xc0ffee, %ecx + + # x86 (i386) calling convention: + # push arguments on stack in reverse order + push %ecx # 3rd parameter + push %ebx # 2nd parameter + push %eax # 1st parameter + call rust_entry + ud2 + +.section .data + +# 16-byte aligned 16k stack. +.align 16 +stack_begin: + .zero 16384 +stack_end: diff --git a/test/x86-unknown-none.json b/test/x86-unknown-none.json index e4f8bd1..7e79840 100644 --- a/test/x86-unknown-none.json +++ b/test/x86-unknown-none.json @@ -1,16 +1,18 @@ { - "llvm-target": "i686-unknown-none", - "data-layout": "e-m:e-i32:32-f80:128-n8:16:32-S128-p:32:32", "arch": "x86", - "target-endian": "little", - "target-pointer-width": "32", - "target-c-int-width": "32", - "os": "none", + "data-layout": "e-m:e-p:32:32-p270:32:32-p271:32:32-p272:64:64-i128:128-f64:32:64-f80:32-n8:16:32-S128", + "disable-redzone": true, "executables": true, - "linker-flavor": "ld.lld", + "features": "-mmx,-sse,-sse2,-sse3,-ssse3,-sse4.1,-sse4.2,-avx,-avx2,+soft-float", "linker": "rust-lld", + "linker-flavor": "gnu-lld", + "llvm-target": "i686-unknown-none", + "rustc-abi": "x86-softfloat", + "os": "none", "panic-strategy": "abort", - "disable-redzone": true, + "position-independent-executables": false, "relocation-model": "static", - "features": "+soft-float,-x87,-mmx,-sse,-sse2,-sse3,-ssse3,-sse4.1,-sse4.2,-avx,-avx2,-fma,-3dnow,-3dnowa" + "target-c-int-width": 32, + "target-endian": "little", + "target-pointer-width": 32 }