Initial commit

This commit is contained in:
Kevin Thomas
2026-07-06 14:32:12 -04:00
commit f62db776e1
615 changed files with 72344 additions and 0 deletions
+102
View File
@@ -0,0 +1,102 @@
# SPDX-License-Identifier: MIT OR Apache-2.0
#
# Copyright (c) 20212024 The rp-rs Developers
# Copyright (c) 2021 rp-rs organization
# Copyright (c) 2025 Raspberry Pi Ltd.
#
# Cargo Configuration for the https://github.com/rp-rs/rp-hal.git repository.
#
# You might want to make a similar file in your own repository if you are
# writing programs for Raspberry Silicon microcontrollers.
#
[build]
target = "thumbv8m.main-none-eabihf"
# Set the default target to match the Cortex-M33 in the RP2350
# target = "thumbv8m.main-none-eabihf"
# target = "thumbv6m-none-eabi"
# target = "riscv32imac-unknown-none-elf"
# Target specific options
[target.thumbv6m-none-eabi]
# Pass some extra options to rustc, some of which get passed on to the linker.
#
# * linker argument --nmagic turns off page alignment of sections (which saves
# flash space)
# * linker argument -Tlink.x tells the linker to use link.x as the linker
# script. This is usually provided by the cortex-m-rt crate, and by default
# the version in that crate will include a file called `memory.x` which
# describes the particular memory layout for your specific chip.
# * no-vectorize-loops turns off the loop vectorizer (seeing as the M0+ doesn't
# have SIMD)
linker = "flip-link"
rustflags = [
"-C", "link-arg=--nmagic",
"-C", "link-arg=-Tlink.x",
"-C", "link-arg=-Tdefmt.x",
"-C", "no-vectorize-loops",
]
# Use picotool for loading.
#
# Load an elf, skipping unchanged flash sectors, verify it, and execute it
runner = "${PICOTOOL_PATH} load -u -v -x -t elf"
#runner = "probe-rs run --chip ${CHIP} --protocol swd"
# This is the hard-float ABI for Arm mode.
#
# The FPU is enabled by default, and float function arguments use FPU
# registers.
[target.thumbv8m.main-none-eabihf]
# Pass some extra options to rustc, some of which get passed on to the linker.
#
# * linker argument --nmagic turns off page alignment of sections (which saves
# flash space)
# * linker argument -Tlink.x tells the linker to use link.x as a linker script.
# This is usually provided by the cortex-m-rt crate, and by default the
# version in that crate will include a file called `memory.x` which describes
# the particular memory layout for your specific chip.
# * linker argument -Tdefmt.x also tells the linker to use `defmt.x` as a
# secondary linker script. This is required to make defmt_rtt work.
rustflags = [
"-C", "link-arg=--nmagic",
"-C", "link-arg=-Tlink.x",
"-C", "link-arg=-Tdefmt.x",
"-C", "target-cpu=cortex-m33",
]
# Use picotool for loading.
#
# Load an elf, skipping unchanged flash sectors, verify it, and execute it
runner = "${PICOTOOL_PATH} load -u -v -x -t elf"
#runner = "probe-rs run --chip ${CHIP} --protocol swd"
# This is the soft-float ABI for RISC-V mode.
#
# Hazard 3 does not have an FPU and so float function arguments use integer
# registers.
[target.riscv32imac-unknown-none-elf]
# Pass some extra options to rustc, some of which get passed on to the linker.
#
# * linker argument --nmagic turns off page alignment of sections (which saves
# flash space)
# * linker argument -Trp235x_riscv.x also tells the linker to use
# `rp235x_riscv.x` as a linker script. This adds in RP2350 RISC-V specific
# things that the riscv-rt crate's `link.x` requires and then includes
# `link.x` automatically. This is the reverse of how we do it on Cortex-M.
# * linker argument -Tdefmt.x also tells the linker to use `defmt.x` as a
# secondary linker script. This is required to make defmt_rtt work.
rustflags = [
"-C", "link-arg=--nmagic",
"-C", "link-arg=-Trp2350_riscv.x",
"-C", "link-arg=-Tdefmt.x",
]
# Use picotool for loading.
#
# Load an elf, skipping unchanged flash sectors, verify it, and execute it
runner = "${PICOTOOL_PATH} load -u -v -x -t elf"
#runner = "probe-rs run --chip ${CHIP} --protocol swd"
[env]
DEFMT_LOG = "debug"
+113
View File
@@ -0,0 +1,113 @@
# Created by https://www.toptal.com/developers/gitignore/api/rust,visualstudiocode,macos,windows,linux
# Edit at https://www.toptal.com/developers/gitignore?templates=rust,visualstudiocode,macos,windows,linux
### Linux ###
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### macOS Patch ###
# iCloud generated files
*.icloud
### Rust ###
# Generated by Cargo
# will have compiled files and executables
debug/
target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
### VisualStudioCode Patch ###
# Ignore all local history of files
.history
.ionide
### Windows ###
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# End of https://www.toptal.com/developers/gitignore/api/rust,visualstudiocode,macos,windows,linux
+1
View File
@@ -0,0 +1 @@
rp2350
+8
View File
@@ -0,0 +1,8 @@
{
"recommendations": [
"marus25.cortex-debug",
"rust-lang.rust-analyzer",
"probe-rs.probe-rs-debugger",
"raspberry-pi.raspberry-pi-pico"
]
}
+41
View File
@@ -0,0 +1,41 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Pico Debug (probe-rs)",
"cwd": "${workspaceFolder}",
"request": "launch",
"type": "probe-rs-debug",
"connectUnderReset": false,
"speed": 5000,
"runtimeExecutable": "probe-rs",
"chip": "${command:raspberry-pi-pico.getChip}",
"runtimeArgs": [
"dap-server"
],
"flashingConfig": {
"flashingEnabled": true,
"haltAfterReset": false
},
"coreConfigs": [
{
"coreIndex": 0,
"programBinary": "${command:raspberry-pi-pico.launchTargetPath}",
"rttEnabled": true,
"svdFile": "${command:raspberry-pi-pico.getSVDPath}",
"rttChannelFormats": [
{
"channelNumber": 0,
"dataFormat": "Defmt",
"mode": "NoBlockSkip",
"showTimestamps": true
}
]
}
],
"preLaunchTask": "Build + Generate SBOM (debug)",
"consoleLogLevel": "Debug",
"wireProtocol": "Swd"
}
]
}
+8
View File
@@ -0,0 +1,8 @@
{
"rust-analyzer.cargo.target": "thumbv8m.main-none-eabihf",
"rust-analyzer.check.allTargets": false,
"editor.formatOnSave": true,
"files.exclude": {
".pico-rs": true
}
}
+124
View File
@@ -0,0 +1,124 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Compile Project",
"type": "process",
"isBuildCommand": true,
"command": "cargo",
"args": [
"build",
"--release"
],
"group": {
"kind": "build",
"isDefault": true
},
"presentation": {
"reveal": "always",
"panel": "dedicated"
},
"problemMatcher": "$rustc",
"options": {
"env": {
"PICOTOOL_PATH": "${command:raspberry-pi-pico.getPicotoolPath}",
"CHIP": "${command:raspberry-pi-pico.getChip}"
}
}
},
{
"label": "Build + Generate SBOM (release)",
"type": "shell",
"command": "bash",
"args": [
"-lc",
"cargo sbom > ${command:raspberry-pi-pico.sbomTargetPathRelease}"
],
"windows": {
"command": "powershell",
"args": [
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-Command",
"cargo sbom | Set-Content -Encoding utf8 ${command:raspberry-pi-pico.sbomTargetPathRelease}"
]
},
"dependsOn": "Compile Project",
"presentation": {
"reveal": "silent",
"panel": "shared"
},
"problemMatcher": []
},
{
"label": "Compile Project (debug)",
"type": "process",
"isBuildCommand": true,
"command": "cargo",
"args": [
"build"
],
"group": {
"kind": "build",
"isDefault": false
},
"presentation": {
"reveal": "always",
"panel": "dedicated"
},
"problemMatcher": "$rustc",
"options": {
"env": {
"PICOTOOL_PATH": "${command:raspberry-pi-pico.getPicotoolPath}",
"CHIP": "${command:raspberry-pi-pico.getChip}"
}
}
},
{
"label": "Build + Generate SBOM (debug)",
"type": "shell",
"command": "bash",
"args": [
"-lc",
"cargo sbom > ${command:raspberry-pi-pico.sbomTargetPathDebug}"
],
"windows": {
"command": "powershell",
"args": [
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-Command",
"cargo sbom | Set-Content -Encoding utf8 ${command:raspberry-pi-pico.sbomTargetPathDebug}"
]
},
"dependsOn": "Compile Project (debug)",
"presentation": {
"reveal": "silent",
"panel": "shared"
},
"problemMatcher": []
},
{
"label": "Run Project",
"type": "shell",
"dependsOn": [
"Build + Generate SBOM (release)"
],
"command": "${command:raspberry-pi-pico.getPicotoolPath}",
"args": [
"load",
"-x",
"${command:raspberry-pi-pico.launchTargetPathRelease}",
"-t",
"elf"
],
"presentation": {
"reveal": "always",
"panel": "dedicated"
},
"problemMatcher": []
}
]
}
+80
View File
@@ -0,0 +1,80 @@
[package]
edition = "2024"
name = "servo"
version = "0.1.0"
license = "MIT or Apache-2.0"
[lib]
name = "servo_lib"
path = "src/lib.rs"
[[bin]]
name = "servo"
path = "src/main.rs"
[dependencies]
cortex-m = "0.7"
cortex-m-rt = "0.7"
embedded-hal = "1.0.0"
fugit = "0.3"
defmt = "1"
defmt-rtt = "1"
[target.'cfg( target_arch = "arm" )'.dependencies]
panic-probe = { version = "1", features = ["print-defmt"] }
[target.'cfg( target_arch = "riscv32" )'.dependencies]
panic-halt = { version = "1.0.0" }
[target.thumbv6m-none-eabi.dependencies]
rp2040-boot2 = "0.3"
rp2040-hal = { version = "0.11", features = ["rt", "critical-section-impl"] }
[target.riscv32imac-unknown-none-elf.dependencies]
rp235x-hal = { version = "0.3", features = ["rt", "critical-section-impl"] }
[target."thumbv8m.main-none-eabihf".dependencies]
rp235x-hal = { version = "0.3", features = ["rt", "critical-section-impl"] }
# cargo build/run
[profile.dev]
debug = 2
debug-assertions = true
opt-level = 2
overflow-checks = true
# cargo build/run --release
[profile.release]
debug = 2
debug-assertions = false
lto = "fat"
opt-level = 2
overflow-checks = false
# do not optimize proc-macro crates = faster builds from scratch
[profile.dev.build-override]
debug = false
debug-assertions = false
overflow-checks = false
opt-level = 0
[profile.release.build-override]
debug = false
debug-assertions = false
overflow-checks = false
opt-level = 0
# cargo test
[profile.test]
debug = 2
debug-assertions = true
opt-level = 2
overflow-checks = true
# cargo test --release
[profile.bench]
debug = 2
debug-assertions = false
lto = "fat"
opt-level = 3
+204
View File
@@ -0,0 +1,204 @@
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 (c) 20212024 The rp-rs Developers
Copyright (c) 2021 rp-rs organization
Copyright (c) 2025 Raspberry Pi Ltd.
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.
+24
View File
@@ -0,0 +1,24 @@
MIT License
Copyright (c) 20212024 The rp-rs Developers
Copyright (c) 2021 rp-rs organization
Copyright (c) 2025 Raspberry Pi Ltd.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+117
View File
@@ -0,0 +1,117 @@
# 0x05 Servo Rust Driver
This repository contains a Bare-Metal Rust driver for controlling standard 50Hz hobby Servos via PWM on the **RP2350** (and RP2040) microcontrollers.
It includes:
- A thin demo (`src/main.rs`) that repeatedly sweeps a servo horn back and forth between 0 and 180 degrees while printing the current angle over UART.
- A reusable library module (`src/servo.rs`) providing a hardware-agnostic `servo_lib` with helper math to map angles to precise pulse-width microsecond targets.
- Board initialization logic (`src/board.rs`).
## 🚀 Getting Started from Scratch
If you're starting with a fresh machine, follow these exact steps to install the toolchain, build the code, and flash it to your microcontroller.
### 1. Install Rust
First, install `rustup` (the Rust toolchain installer) if you haven't already:
```bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
```
*Note: Restart your terminal or run `source $HOME/.cargo/env` after this finishes.*
Ensure your Rust compiler is up to date:
```bash
rustup update
```
### 2. Install the Target Architecture
This project is configured for the **RP2350** (ARM Cortex-M33). We need to install the cross-compilation target for it:
```bash
rustup target add thumbv8m.main-none-eabihf
```
*(If you were targeting the RP2040, you would use `thumbv6m-none-eabi` instead).*
### 3. Install Build Tools
You will need a few extra tools to help link and format the firmware for the RP-series chips.
Install `flip-link` (adds zero-cost stack overflow protection):
```bash
cargo install flip-link
```
Install `picotool` (used by `cargo run` to flash the chip):
- **macOS:** `brew install picotool`
- **Linux/Windows:** Follow the official Raspberry Pi documentation to install `picotool` or build it from source.
### 4. Building the Code
To compile the code for the microcontroller, simply run:
```bash
cargo build
```
To build a highly optimized release version (smaller and faster):
```bash
cargo build --release
```
### 5. Flashing to the Microcontroller
This project is pre-configured in `.cargo/config.toml` to use `picotool` as the custom runner.
To flash the code:
1. Hold down the **BOOTSEL** button on your RP2350 board.
2. Plug it into your computer via USB (or press the RUN/RESET button while holding BOOTSEL).
3. Run the following command:
```bash
cargo run --release
```
*`cargo` will compile the code and automatically use `picotool` to upload the `.elf` file directly to your board and start executing it!*
### 6. Testing on the Host
Because the Servo mapping logic is separated into a reusable math library without touching hardware registers, you can run the unit tests natively on your computer!
However, because this project sets a default bare-metal target (`thumbv8m.main-none-eabihf`) in `.cargo/config.toml`, running a plain `cargo test` will fail because the standard library doesn't exist on the microcontroller. You must explicitly tell Cargo to compile the tests for your host computer's processor architecture:
**Mac (Apple Silicon):**
```bash
cargo test --lib --target aarch64-apple-darwin
```
**Linux (Intel/AMD 64-bit):**
```bash
cargo test --lib --target x86_64-unknown-linux-gnu
```
**Windows (64-bit):**
```bash
cargo test --lib --target x86_64-pc-windows-msvc
```
## 🧠 Code Walkthrough
This section explains exactly how the code works, where the entry point is, and traces the flow of execution as if you were stepping through it line-by-line.
### 1. The Entry Point (`src/main.rs`)
Unlike a standard computer program, bare-metal microcontrollers do not have an operating system to call `main()`. Instead, we use the `#[entry]` macro from the HAL (Hardware Abstraction Layer) to define the very first function that runs after the chip boots up.
* **`main() -> !`**: This is the absolute start of our code. It takes ownership of all the hardware peripherals (`hal::pac::Peripherals::take().unwrap()`) and immediately passes them into `board::run(...)`. The `-> !` means this function never returns (because embedded devices run in an infinite loop).
### 2. Board Initialization (`src/board.rs`)
Once execution enters `board.rs`, we initialize the system clocks, pins, UART (for logging), SysTick (for delay), and PWM (for servo control).
* **`run(...)`**: The master setup function. Calls the helper initialization functions below, prints an announcement over UART, and enters the servo sweep loop.
* **`init_clocks(...)`**: Wakes up the external 12 MHz crystal (`XOSC`) and configures the PLLs (Phase-Locked Loops) to drive the system clock at its maximum speed.
* **`init_pins(...)`**: Takes control of physical pins across `IO_BANK0`.
* **`init_uart(...)`**: Configures the hardware `UART0` peripheral to operate at a standard `115200` baud rate with an `8N1` configuration for debug printing.
* **`init_delay(...)`**: Captures the ARM Cortex-M `SYST` (SysTick) peripheral to create a blocking delay timer.
* **`init_servo_pwm(...)`**: Obtains `PWM Slice 3` and binds its `channel_a` to `GPIO 6`. Sets up the clock divider and TOP value to achieve a standard 50 Hz PWM frequency.
* **`servo_loop(...)`**: Loops infinitely, sweeping the servo angle from 0 to 180 degrees and back down to 0.
* **`sweep_angle_up(...)`** / **`sweep_angle_down(...)`**: Walks through angles in 10-degree increments.
* **`apply_angle(...)`**: Computes the hardware register level using `servo_lib`, sets the new duty cycle, formats an `Angle: XXX deg` string without heap allocation, transmits it, and delays 150ms to allow the physical servo motor to arrive at the target angle.
### 3. The Reusable Servo Math Library (`src/servo.rs`)
Unlike basic LEDs which simply vary duty cycle from 0-100%, Servos expect a precise 50 Hz signal (20ms period) where the "HIGH" time represents the angle: usually ~1ms for 0 degrees, ~1.5ms for 90 degrees, and ~2ms for 180 degrees.
* **`calc_clk_div(...)`**: Calculates the precise floating-point clock divider required to step down the fast system clock so the counter overflows exactly 50 times per second.
* **`angle_to_pulse_us(...)`**: Linearly maps an angle (0.0 to 180.0 degrees) to a pulse width (e.g. 1000 to 2000 microseconds).
* **`pulse_us_to_level(...)`**: Translates the desired microsecond pulse into a raw integer counter compare-value for the PWM hardware registers.
* **`clamp_pulse_us(...)`** / **`clamp_degrees(...)`**: Safety constraints to ensure we do not physically damage the servo by driving it past its mechanical limits.
+105
View File
@@ -0,0 +1,105 @@
//! Implementation module
//!
//! **File:** `build.rs`
//! **Author:** Kevin Thomas
//! **Date:** 2025
//!
//! MIT License
//!
//! Copyright (c) 2025 Kevin Thomas
//!
//! Permission is hereby granted, free of charge, to any person obtaining a copy
//! of this software and associated documentation files (the "Software"), to deal
//! in the Software without restriction, including without limitation the rights
//! to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//! copies of the Software, and to permit persons to whom the Software is
//! furnished to do so, subject to the following conditions:
//!
//! The above copyright notice and this permission notice shall be included in
//! all copies or substantial portions of the Software.
//!
//! THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//! IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//! FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//! AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//! LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//! OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
//! SOFTWARE.
// Import dependencies from std::fs
use std::fs::{read_to_string, File};
// Import std::io::Write
use std::io::Write;
// Import std::path::{Path, PathBuf}
use std::path::{Path, PathBuf};
/// The main entry point.
fn main() {
let out = PathBuf::from(std::env::var_os("OUT_DIR").unwrap());
let c = read_to_string(".pico-rs").unwrap_or_default().trim().to_lowercase();
setup_target(&c, &out);
write_riscv(&out);
print_cfgs(&out);
}
/// Executes the setup target operation.
///
/// # Arguments
///
/// * `c` - Target string from config.
/// * `out` - Output path.
fn setup_target(c: &str, out: &Path) {
if c == "rp2040" {
write_rp2040(out);
} else {
write_rp2350(out);
}
}
/// Executes the write rp2040 operation.
///
/// # Arguments
///
/// * `out` - Output path.
fn write_rp2040(out: &Path) {
let b = include_bytes!("rp2040.x");
File::create(out.join("memory.x")).unwrap().write_all(b).unwrap();
println!("cargo::rustc-cfg=rp2040");
println!("cargo:rerun-if-changed=rp2040.x");
}
/// Executes the write rp2350 operation.
///
/// # Arguments
///
/// * `out` - Output path.
fn write_rp2350(out: &Path) {
let b = include_bytes!("rp2350.x");
File::create(out.join("memory.x")).unwrap().write_all(b).unwrap();
println!("cargo::rustc-cfg=rp2350");
println!("cargo:rerun-if-changed=rp2350.x");
}
/// Executes the write riscv operation.
///
/// # Arguments
///
/// * `out` - Output path.
fn write_riscv(out: &Path) {
let b = include_bytes!("rp2350_riscv.x");
File::create(out.join("rp2350_riscv.x")).unwrap().write_all(b).unwrap();
}
/// Executes the print cfgs operation.
///
/// # Arguments
///
/// * `out` - Output path.
fn print_cfgs(out: &Path) {
println!("cargo::rustc-check-cfg=cfg(rp2040)");
println!("cargo::rustc-check-cfg=cfg(rp2350)");
println!("cargo:rustc-link-search={}", out.display());
println!("cargo:rerun-if-changed=.pico-rs");
println!("cargo:rerun-if-changed=rp2350_riscv.x");
println!("cargo:rerun-if-changed=build.rs");
}
+91
View File
@@ -0,0 +1,91 @@
/*
* SPDX-License-Identifier: MIT OR Apache-2.0
*
* Copyright (c) 20212024 The rp-rs Developers
* Copyright (c) 2021 rp-rs organization
* Copyright (c) 2025 Raspberry Pi Ltd.
*/
MEMORY {
BOOT2 : ORIGIN = 0x10000000, LENGTH = 0x100
/*
* Here we assume you have 2048 KiB of Flash. This is what the Pi Pico
* has, but your board may have more or less Flash and you should adjust
* this value to suit.
*/
FLASH : ORIGIN = 0x10000100, LENGTH = 2048K - 0x100
/*
* RAM consists of 4 banks, SRAM0-SRAM3, with a striped mapping.
* This is usually good for performance, as it distributes load on
* those banks evenly.
*/
RAM : ORIGIN = 0x20000000, LENGTH = 256K
/*
* RAM banks 4 and 5 use a direct mapping. They can be used to have
* memory areas dedicated for some specific job, improving predictability
* of access times.
* Example: Separate stacks for core0 and core1.
*/
SRAM4 : ORIGIN = 0x20040000, LENGTH = 4k
SRAM5 : ORIGIN = 0x20041000, LENGTH = 4k
/* SRAM banks 0-3 can also be accessed directly. However, those ranges
alias with the RAM mapping, above. So don't use them at the same time!
SRAM0 : ORIGIN = 0x21000000, LENGTH = 64k
SRAM1 : ORIGIN = 0x21010000, LENGTH = 64k
SRAM2 : ORIGIN = 0x21020000, LENGTH = 64k
SRAM3 : ORIGIN = 0x21030000, LENGTH = 64k
*/
}
EXTERN(BOOT2_FIRMWARE)
SECTIONS {
/* ### Boot loader
*
* An executable block of code which sets up the QSPI interface for
* 'Execute-In-Place' (or XIP) mode. Also sends chip-specific commands to
* the external flash chip.
*
* Must go at the start of external flash, where the Boot ROM expects it.
*/
.boot2 ORIGIN(BOOT2) :
{
KEEP(*(.boot2));
} > BOOT2
} INSERT BEFORE .text;
SECTIONS {
/* ### Boot ROM info
*
* Goes after .vector_table, to keep it in the first 512 bytes of flash,
* where picotool can find it
*/
.boot_info : ALIGN(4)
{
KEEP(*(.boot_info));
} > FLASH
} INSERT AFTER .vector_table;
/* move .text to start /after/ the boot info */
_stext = ADDR(.boot_info) + SIZEOF(.boot_info);
SECTIONS {
/* ### Picotool 'Binary Info' Entries
*
* Picotool looks through this block (as we have pointers to it in our
* header) to find interesting information.
*/
.bi_entries : ALIGN(4)
{
/* We put this in the header */
__bi_entries_start = .;
/* Here are the entries */
KEEP(*(.bi_entries));
/* Keep this block a nice round size */
. = ALIGN(4);
/* We put this in the header */
__bi_entries_end = .;
} > FLASH
} INSERT AFTER .text;
+83
View File
@@ -0,0 +1,83 @@
/*
* SPDX-License-Identifier: MIT OR Apache-2.0
*
* Copyright (c) 20212024 The rp-rs Developers
* Copyright (c) 2021 rp-rs organization
* Copyright (c) 2025 Raspberry Pi Ltd.
*/
MEMORY {
/*
* The RP2350 has either external or internal flash.
*
* 2 MiB is a safe default here, although a Pico 2 has 4 MiB.
*/
FLASH : ORIGIN = 0x10000000, LENGTH = 2048K
/*
* RAM consists of 8 banks, SRAM0-SRAM7, with a striped mapping.
* This is usually good for performance, as it distributes load on
* those banks evenly.
*/
RAM : ORIGIN = 0x20000000, LENGTH = 512K
/*
* RAM banks 8 and 9 use a direct mapping. They can be used to have
* memory areas dedicated for some specific job, improving predictability
* of access times.
* Example: Separate stacks for core0 and core1.
*/
SRAM4 : ORIGIN = 0x20080000, LENGTH = 4K
SRAM5 : ORIGIN = 0x20081000, LENGTH = 4K
}
SECTIONS {
/* ### Boot ROM info
*
* Goes after .vector_table, to keep it in the first 4K of flash
* where the Boot ROM (and picotool) can find it
*/
.start_block : ALIGN(4)
{
__start_block_addr = .;
KEEP(*(.start_block));
} > FLASH
} INSERT AFTER .vector_table;
/* move .text to start /after/ the boot info */
_stext = ADDR(.start_block) + SIZEOF(.start_block);
SECTIONS {
/* ### Picotool 'Binary Info' Entries
*
* Picotool looks through this block (as we have pointers to it in our
* header) to find interesting information.
*/
.bi_entries : ALIGN(4)
{
/* We put this in the header */
__bi_entries_start = .;
/* Here are the entries */
KEEP(*(.bi_entries));
/* Keep this block a nice round size */
. = ALIGN(4);
/* We put this in the header */
__bi_entries_end = .;
} > FLASH
} INSERT AFTER .text;
SECTIONS {
/* ### Boot ROM extra info
*
* Goes after everything in our program, so it can contain a signature.
*/
.end_block : ALIGN(4)
{
__end_block_addr = .;
KEEP(*(.end_block));
} > FLASH
} INSERT AFTER .uninit;
PROVIDE(start_to_end = __end_block_addr - __start_block_addr);
PROVIDE(end_to_start = __start_block_addr - __end_block_addr);
+259
View File
@@ -0,0 +1,259 @@
/*
* SPDX-License-Identifier: MIT OR Apache-2.0
*
* Copyright (c) 20212024 The rp-rs Developers
* Copyright (c) 2021 rp-rs organization
* Copyright (c) 2025 Raspberry Pi Ltd.
*/
MEMORY {
/*
* The RP2350 has either external or internal flash.
*
* 2 MiB is a safe default here, although a Pico 2 has 4 MiB.
*/
FLASH : ORIGIN = 0x10000000, LENGTH = 2048K
/*
* RAM consists of 8 banks, SRAM0-SRAM7, with a striped mapping.
* This is usually good for performance, as it distributes load on
* those banks evenly.
*/
RAM : ORIGIN = 0x20000000, LENGTH = 512K
/*
* RAM banks 8 and 9 use a direct mapping. They can be used to have
* memory areas dedicated for some specific job, improving predictability
* of access times.
* Example: Separate stacks for core0 and core1.
*/
SRAM4 : ORIGIN = 0x20080000, LENGTH = 4K
SRAM5 : ORIGIN = 0x20081000, LENGTH = 4K
}
/* # Developer notes
- Symbols that start with a double underscore (__) are considered "private"
- Symbols that start with a single underscore (_) are considered "semi-public"; they can be
overridden in a user linker script, but should not be referred from user code (e.g. `extern "C" {
static mut _heap_size }`).
- `EXTERN` forces the linker to keep a symbol in the final binary. We use this to make sure a
symbol is not dropped if it appears in or near the front of the linker arguments and "it's not
needed" by any of the preceding objects (linker arguments)
- `PROVIDE` is used to provide default values that can be overridden by a user linker script
- On alignment: it's important for correctness that the VMA boundaries of both .bss and .data *and*
the LMA of .data are all `32`-byte aligned. These alignments are assumed by the RAM
initialization routine. There's also a second benefit: `32`-byte aligned boundaries
means that you won't see "Address (..) is out of bounds" in the disassembly produced by `objdump`.
*/
PROVIDE(_stext = ORIGIN(FLASH));
PROVIDE(_stack_start = ORIGIN(RAM) + LENGTH(RAM));
PROVIDE(_max_hart_id = 0);
PROVIDE(_hart_stack_size = 2K);
PROVIDE(_heap_size = 0);
PROVIDE(InstructionMisaligned = ExceptionHandler);
PROVIDE(InstructionFault = ExceptionHandler);
PROVIDE(IllegalInstruction = ExceptionHandler);
PROVIDE(Breakpoint = ExceptionHandler);
PROVIDE(LoadMisaligned = ExceptionHandler);
PROVIDE(LoadFault = ExceptionHandler);
PROVIDE(StoreMisaligned = ExceptionHandler);
PROVIDE(StoreFault = ExceptionHandler);
PROVIDE(UserEnvCall = ExceptionHandler);
PROVIDE(SupervisorEnvCall = ExceptionHandler);
PROVIDE(MachineEnvCall = ExceptionHandler);
PROVIDE(InstructionPageFault = ExceptionHandler);
PROVIDE(LoadPageFault = ExceptionHandler);
PROVIDE(StorePageFault = ExceptionHandler);
PROVIDE(SupervisorSoft = DefaultHandler);
PROVIDE(MachineSoft = DefaultHandler);
PROVIDE(SupervisorTimer = DefaultHandler);
PROVIDE(MachineTimer = DefaultHandler);
PROVIDE(SupervisorExternal = DefaultHandler);
PROVIDE(MachineExternal = DefaultHandler);
PROVIDE(DefaultHandler = DefaultInterruptHandler);
PROVIDE(ExceptionHandler = DefaultExceptionHandler);
/* # Pre-initialization function */
/* If the user overrides this using the `#[pre_init]` attribute or by creating a `__pre_init` function,
then the function this points to will be called before the RAM is initialized. */
PROVIDE(__pre_init = default_pre_init);
/* A PAC/HAL defined routine that should initialize custom interrupt controller if needed. */
PROVIDE(_setup_interrupts = default_setup_interrupts);
/* # Multi-processing hook function
fn _mp_hook() -> bool;
This function is called from all the harts and must return true only for one hart,
which will perform memory initialization. For other harts it must return false
and implement wake-up in platform-dependent way (e.g. after waiting for a user interrupt).
*/
PROVIDE(_mp_hook = default_mp_hook);
/* # Start trap function override
By default uses the riscv crates default trap handler
but by providing the `_start_trap` symbol external crates can override.
*/
PROVIDE(_start_trap = default_start_trap);
SECTIONS
{
.text.dummy (NOLOAD) :
{
/* This section is intended to make _stext address work */
. = ABSOLUTE(_stext);
} > FLASH
.text _stext :
{
/* Put reset handler first in .text section so it ends up as the entry */
/* point of the program. */
KEEP(*(.init));
KEEP(*(.init.rust));
. = ALIGN(4);
__start_block_addr = .;
KEEP(*(.start_block));
. = ALIGN(4);
*(.trap);
*(.trap.rust);
*(.text.abort);
*(.text .text.*);
. = ALIGN(4);
} > FLASH
/* ### Picotool 'Binary Info' Entries
*
* Picotool looks through this block (as we have pointers to it in our
* header) to find interesting information.
*/
.bi_entries : ALIGN(4)
{
/* We put this in the header */
__bi_entries_start = .;
/* Here are the entries */
KEEP(*(.bi_entries));
/* Keep this block a nice round size */
. = ALIGN(4);
/* We put this in the header */
__bi_entries_end = .;
} > FLASH
.rodata : ALIGN(4)
{
*(.srodata .srodata.*);
*(.rodata .rodata.*);
/* 4-byte align the end (VMA) of this section.
This is required by LLD to ensure the LMA of the following .data
section will have the correct alignment. */
. = ALIGN(4);
} > FLASH
.data : ALIGN(32)
{
_sidata = LOADADDR(.data);
__sidata = LOADADDR(.data);
_sdata = .;
__sdata = .;
/* Must be called __global_pointer$ for linker relaxations to work. */
PROVIDE(__global_pointer$ = . + 0x800);
*(.sdata .sdata.* .sdata2 .sdata2.*);
*(.data .data.*);
. = ALIGN(32);
_edata = .;
__edata = .;
} > RAM AT > FLASH
.bss (NOLOAD) : ALIGN(32)
{
_sbss = .;
*(.sbss .sbss.* .bss .bss.*);
. = ALIGN(32);
_ebss = .;
} > RAM
.end_block : ALIGN(4)
{
__end_block_addr = .;
KEEP(*(.end_block));
} > FLASH
/* fictitious region that represents the memory available for the heap */
.heap (NOLOAD) :
{
_sheap = .;
. += _heap_size;
. = ALIGN(4);
_eheap = .;
} > RAM
/* fictitious region that represents the memory available for the stack */
.stack (NOLOAD) :
{
_estack = .;
. = ABSOLUTE(_stack_start);
_sstack = .;
} > RAM
/* fake output .got section */
/* Dynamic relocations are unsupported. This section is only used to detect
relocatable code in the input files and raise an error if relocatable code
is found */
.got (INFO) :
{
KEEP(*(.got .got.*));
}
.eh_frame (INFO) : { KEEP(*(.eh_frame)) }
.eh_frame_hdr (INFO) : { *(.eh_frame_hdr) }
}
PROVIDE(start_to_end = __end_block_addr - __start_block_addr);
PROVIDE(end_to_start = __start_block_addr - __end_block_addr);
/* Do not exceed this mark in the error messages above | */
ASSERT(ORIGIN(FLASH) % 4 == 0, "
ERROR(riscv-rt): the start of the FLASH must be 4-byte aligned");
ASSERT(ORIGIN(RAM) % 32 == 0, "
ERROR(riscv-rt): the start of the RAM must be 32-byte aligned");
ASSERT(_stext % 4 == 0, "
ERROR(riscv-rt): `_stext` must be 4-byte aligned");
ASSERT(_sdata % 32 == 0 && _edata % 32 == 0, "
BUG(riscv-rt): .data is not 32-byte aligned");
ASSERT(_sidata % 32 == 0, "
BUG(riscv-rt): the LMA of .data is not 32-byte aligned");
ASSERT(_sbss % 32 == 0 && _ebss % 32 == 0, "
BUG(riscv-rt): .bss is not 32-byte aligned");
ASSERT(_sheap % 4 == 0, "
BUG(riscv-rt): start of .heap is not 4-byte aligned");
ASSERT(_stext + SIZEOF(.text) < ORIGIN(FLASH) + LENGTH(FLASH), "
ERROR(riscv-rt): The .text section must be placed inside the FLASH region.
Set _stext to an address smaller than 'ORIGIN(FLASH) + LENGTH(FLASH)'");
ASSERT(SIZEOF(.stack) > (_max_hart_id + 1) * _hart_stack_size, "
ERROR(riscv-rt): .stack section is too small for allocating stacks for all the harts.
Consider changing `_max_hart_id` or `_hart_stack_size`.");
ASSERT(SIZEOF(.got) == 0, "
.got section detected in the input files. Dynamic relocations are not
supported. If you are linking to C code compiled using the `gcc` crate
then modify your build script to compile the C code _without_ the
-fPIC flag. See the documentation of the `gcc::Config.fpic` method for
details.");
/* Do not exceed this mark in the error messages above | */
+481
View File
@@ -0,0 +1,481 @@
//! Implementation module
//!
//! **File:** `board.rs`
//! **Author:** Kevin Thomas
//! **Date:** 2025
//!
//! MIT License
//!
//! Copyright (c) 2025 Kevin Thomas
//!
//! Permission is hereby granted, free of charge, to any person obtaining a copy
//! of this software and associated documentation files (the "Software"), to deal
//! in the Software without restriction, including without limitation the rights
//! to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//! copies of the Software, and to permit persons to whom the Software is
//! furnished to do so, subject to the following conditions:
//!
//! The above copyright notice and this permission notice shall be included in
//! all copies or substantial portions of the Software.
//!
//! THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//! IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//! FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//! AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//! LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//! OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
//! SOFTWARE.
// PWM duty-cycle trait for .set_duty_cycle()
use embedded_hal::pwm::SetDutyCycle;
// Rate extension trait for .Hz() baud rate construction
use fugit::RateExtU32;
// Clock trait for accessing system clock frequency
use hal::Clock;
// GPIO pin types and function selectors
use hal::gpio::{FunctionNull, FunctionUart, Pin, PullDown, PullNone};
// UART configuration and peripheral types
use hal::uart::{DataBits, Enabled, StopBits, UartConfig, UartPeripheral};
// Alias our HAL crate
#[cfg(rp2350)]
// Import rp235x_hal as hal
use rp235x_hal as hal;
#[cfg(rp2040)]
// Import rp2040_hal as hal
use rp2040_hal as hal;
/// External crystal frequency in Hz (12 MHz).
pub(crate) const XTAL_FREQ_HZ: u32 = 12_000_000u32;
/// UART baud rate in bits per second.
pub(crate) const UART_BAUD: u32 = 115_200;
/// Angle increment per sweep step in degrees.
pub(crate) const STEP_DEGREES: i32 = 10;
/// Delay between sweep steps in milliseconds.
pub(crate) const STEP_DELAY_MS: u32 = 150;
/// Type alias for the configured TX pin (GPIO 0, UART function, no pull).
pub(crate) type TxPin = Pin<hal::gpio::bank0::Gpio0, FunctionUart, PullNone>;
/// Type alias for the configured RX pin (GPIO 1, UART function, no pull).
pub(crate) type RxPin = Pin<hal::gpio::bank0::Gpio1, FunctionUart, PullNone>;
/// Type alias for the default TX pin state from `Pins::new()`.
pub(crate) type TxPinDefault = Pin<hal::gpio::bank0::Gpio0, FunctionNull, PullDown>;
/// Type alias for the default RX pin state from `Pins::new()`.
pub(crate) type RxPinDefault = Pin<hal::gpio::bank0::Gpio1, FunctionNull, PullDown>;
/// Type alias for the fully-enabled UART0 peripheral with TX/RX p.
pub(crate) type EnabledUart = UartPeripheral<Enabled, hal::pac::UART0, (TxPin, RxPin)>;
/// Initialise system clocks and PLLs from the external 12 MHz crystal.
///
/// # Arguments
///
/// * `xosc` - XOSC peripheral singleton.
/// * `clocks` - CLOCKS peripheral singleton.
/// * `pll_sys` - PLL_SYS peripheral singleton.
/// * `pll_usb` - PLL_USB peripheral singleton.
/// * `resets` - Mutable reference to the RESETS peripheral.
/// * `watchdog` - Mutable reference to the watchdog timer.
///
/// # Returns
///
/// Configured clocks manager.
///
/// # Panics
///
/// Panics if clock initialisation fails.
pub(crate) fn init_clocks(
xosc: hal::pac::XOSC,
clocks: hal::pac::CLOCKS,
pll_sys: hal::pac::PLL_SYS,
pll_usb: hal::pac::PLL_USB,
resets: &mut hal::pac::RESETS,
watchdog: &mut hal::Watchdog,
) -> hal::clocks::ClocksManager {
hal::clocks::init_clocks_and_plls(
XTAL_FREQ_HZ,
xosc,
clocks,
pll_sys,
pll_usb,
resets,
watchdog,
)
.unwrap()
}
/// Unlock the GPIO bank and return the pin set.
///
/// # Arguments
///
/// * `io_bank0` - IO_BANK0 peripheral singleton.
/// * `pads_bank0` - PADS_BANK0 peripheral singleton.
/// * `sio` - SIO peripheral singleton.
/// * `resets` - Mutable reference to the RESETS peripheral.
///
/// # Returns
///
/// GPIO pin set for the entire bank.
pub(crate) fn init_pins(
io_bank0: hal::pac::IO_BANK0,
pads_bank0: hal::pac::PADS_BANK0,
sio: hal::pac::SIO,
resets: &mut hal::pac::RESETS,
) -> hal::gpio::Pins {
let sio = hal::Sio::new(sio);
hal::gpio::Pins::new(io_bank0, pads_bank0, sio.gpio_bank0, resets)
}
/// Initialise UART0 for serial output (stdio equivalent).
///
/// # Arguments
///
/// * `uart0` - PAC UART0 peripheral singleton.
/// * `tx_pin` - GPIO pin to use as UART0 TX (GPIO 0).
/// * `rx_pin` - GPIO pin to use as UART0 RX (GPIO 1).
/// * `resets` - Mutable reference to the RESETS peripheral.
/// * `clocks` - Reference to the initialised clock configuration.
///
/// # Returns
///
/// Enabled UART0 peripheral ready for blocking writes.
///
/// # Panics
///
/// Panics if the HAL cannot achieve the requested baud rate.
pub(crate) fn init_uart(
uart0: hal::pac::UART0,
tx_pin: TxPinDefault,
rx_pin: RxPinDefault,
resets: &mut hal::pac::RESETS,
clocks: &hal::clocks::ClocksManager,
) -> EnabledUart {
let pins = (
tx_pin.reconfigure::<FunctionUart, PullNone>(),
rx_pin.reconfigure::<FunctionUart, PullNone>(),
);
let cfg = UartConfig::new(UART_BAUD.Hz(), DataBits::Eight, None, StopBits::One);
UartPeripheral::new(uart0, pins, resets)
.enable(cfg, clocks.peripheral_clock.freq())
.unwrap()
}
/// Create a blocking delay timer from the ARM SysTick peripheral.
///
/// # Arguments
///
/// * `clocks` - Reference to the initialised clock configuration.
///
/// # Returns
///
/// Blocking delay provider.
///
/// # Panics
///
/// Panics if the cortex-m core peripherals have already been taken.
///
/// # Arguments
///
/// * `clocks` - The `clocks` parameter.
///
/// # Returns
///
/// A value of type `cortex_m::delay::Delay`.
pub(crate) fn init_delay(clocks: &hal::clocks::ClocksManager) -> cortex_m::delay::Delay {
let core = cortex_m::Peripherals::take().unwrap();
cortex_m::delay::Delay::new(core.SYST, clocks.system_clock.freq().to_Hz())
}
/// Write 3-character right-justified angle digits into `buf`.
///
/// # Arguments
///
/// * `buf` - The `buf` parameter.
/// * `a` - The `a` parameter.
///
/// # Returns
///
/// A value of type `usize`.
///
/// # Arguments
///
/// * `buf` - The `buf` parameter.
/// * `a` - The `a` parameter.
///
/// # Returns
///
/// A value of type `usize`.
fn write_angle_digits(buf: &mut [u8], a: u32) -> usize {
if a >= 100 { write_angle_hundreds(buf, a); }
else if a >= 10 { write_angle_tens(buf, a); }
else { write_angle_ones(buf, a); }
3
}
/// Write digits for angles >= 100.
///
/// # Arguments
///
/// * `buf` - The `buf` parameter.
/// * `a` - The `a` parameter.
///
/// # Arguments
///
/// * `buf` - The `buf` parameter.
/// * `a` - The `a` parameter.
fn write_angle_hundreds(buf: &mut [u8], a: u32) {
buf[0] = b'0' + (a / 100) as u8;
buf[1] = b'0' + ((a / 10) % 10) as u8;
buf[2] = b'0' + (a % 10) as u8;
}
/// Write digits for angles 10..99 with leading space.
///
/// # Arguments
///
/// * `buf` - The `buf` parameter.
/// * `a` - The `a` parameter.
///
/// # Arguments
///
/// * `buf` - The `buf` parameter.
/// * `a` - The `a` parameter.
fn write_angle_tens(buf: &mut [u8], a: u32) {
buf[0] = b' ';
buf[1] = b'0' + (a / 10) as u8;
buf[2] = b'0' + (a % 10) as u8;
}
/// Write digit for angles 0..9 with leading spaces.
///
/// # Arguments
///
/// * `buf` - The `buf` parameter.
/// * `a` - The `a` parameter.
///
/// # Arguments
///
/// * `buf` - The `buf` parameter.
/// * `a` - The `a` parameter.
fn write_angle_ones(buf: &mut [u8], a: u32) {
buf[0] = b' ';
buf[1] = b' ';
buf[2] = b'0' + a as u8;
}
/// Format an angle into "Angle: NNN deg\r\n".
///
/// # Arguments
///
/// * `buf` - Mutable byte slice (must be at least 20 bytes).
/// * `angle` - Angle in degrees (0..180).
///
/// # Returns
///
/// Number of bytes written into the buffer.
///
/// # Arguments
///
/// * `buf` - The `buf` parameter.
/// * `angle` - The `angle` parameter.
///
/// # Returns
///
/// A value of type `usize`.
pub(crate) fn format_angle(buf: &mut [u8], angle: i32) -> usize {
buf[..7].copy_from_slice(b"Angle: ");
let pos = 7 + write_angle_digits(&mut buf[7..], if angle < 0 { 0 } else { angle as u32 });
buf[pos..pos + 6].copy_from_slice(b" deg\r\n");
pos + 6
}
/// Sweep the servo angle upward from 0 to 180 in STEP_DEGREES increments.
///
/// # Arguments
///
/// * `uart` - UART peripheral for serial output.
/// * `channel` - PWM channel implementing SetDutyCycle.
/// * `delay` - Delay provider for pause between steps.
/// * `buf` - Scratch buffer for formatting output.
pub(crate) fn sweep_angle_up(u: &EnabledUart, ch: &mut impl SetDutyCycle, d: &mut cortex_m::delay::Delay, buf: &mut [u8; 20]) {
let mut angle: i32 = 0;
while angle <= 180 { apply_angle(u, ch, d, buf, angle); angle += STEP_DEGREES; }
}
/// Sweep the servo angle downward from 180 to 0 in STEP_DEGREES decrements.
///
/// # Arguments
///
/// * `uart` - UART peripheral for serial output.
/// * `channel` - PWM channel implementing SetDutyCycle.
/// * `delay` - Delay provider for pause between steps.
/// * `buf` - Scratch buffer for formatting output.
pub(crate) fn sweep_angle_down(u: &EnabledUart, ch: &mut impl SetDutyCycle, d: &mut cortex_m::delay::Delay, buf: &mut [u8; 20]) {
let mut angle: i32 = 180;
while angle >= 0 { apply_angle(u, ch, d, buf, angle); angle -= STEP_DEGREES; }
}
/// Apply a single angle step: compute pulse, set PWM, format, print, delay.
fn apply_angle(u: &EnabledUart, ch: &mut impl SetDutyCycle, d: &mut cortex_m::delay::Delay, buf: &mut [u8; 20], a: i32) {
ch.set_duty_cycle(compute_servo_level(a) as u16).ok();
let n = format_angle(buf, a);
u.write_full_blocking(&buf[..n]);
d.delay_ms(STEP_DELAY_MS);
}
/// Compute the pulse width in microseconds for the given angle.
///
/// # Arguments
///
/// * `angle` - The `angle` parameter.
///
/// # Returns
///
/// A 32-bit unsigned integer value.
///
/// # Arguments
///
/// * `angle` - The `angle` parameter.
///
/// # Returns
///
/// A 32-bit unsigned integer value.
fn compute_pulse_us(a: i32) -> u32 {
servo_lib::servo::angle_to_pulse_us(a as f32, servo_lib::servo::SERVO_DEFAULT_MIN_US, servo_lib::servo::SERVO_DEFAULT_MAX_US) as u32
}
/// Compute the PWM level for a given angle using servo constants.
///
/// # Arguments
///
/// * `angle` - The `angle` parameter.
///
/// # Returns
///
/// A 32-bit unsigned integer value.
///
/// # Arguments
///
/// * `angle` - The `angle` parameter.
///
/// # Returns
///
/// A 32-bit unsigned integer value.
fn compute_servo_level(a: i32) -> u32 {
servo_lib::servo::pulse_us_to_level(compute_pulse_us(a), servo_lib::servo::SERVO_WRAP, servo_lib::servo::SERVO_HZ)
}
/// Type alias for PWM slice 3 (servo on GPIO 6, channel A).
type PwmSlice3 = hal::pwm::Slice<hal::pwm::Pwm3, hal::pwm::FreeRunning>;
/// Initialise all peripherals and run the servo sweep demo.
///
/// # Arguments
///
/// * `pac` - PAC Peripherals singleton (consumed).
///
/// # Returns
///
/// A value of type `!`.
///
/// # Arguments
///
/// * `pac` - The `pac` parameter.
///
/// # Returns
///
/// A value of type `!`.
pub(crate) fn run(mut p: hal::pac::Peripherals) -> ! {
let mut w = hal::Watchdog::new(p.WATCHDOG);
let c = init_clocks(p.XOSC, p.CLOCKS, p.PLL_SYS, p.PLL_USB, &mut p.RESETS, &mut w);
let pins = init_pins(p.IO_BANK0, p.PADS_BANK0, p.SIO, &mut p.RESETS);
let u = init_uart(p.UART0, pins.gpio0, pins.gpio1, &mut p.RESETS, &c);
announce_servo(&u);
servo_loop(&u, &mut init_servo_pwm(p.PWM, &mut p.RESETS, &c, pins.gpio6), &mut init_delay(&c))
}
/// Configure PWM slice 3 for 50 Hz servo output on channel A (GPIO 6).
///
/// # Arguments
///
/// * `pwm_pac` - PAC PWM peripheral singleton.
/// * `resets` - Mutable reference to the RESETS peripheral.
/// * `clocks` - Reference to the initialised clock configuration.
/// * `servo_pin` - Default GPIO 6 pin to bind to PWM channel A.
///
/// # Returns
///
/// Configured PWM slice 3 in free-running mode.
fn init_servo_pwm(
p: hal::pac::PWM, r: &mut hal::pac::RESETS, c: &hal::clocks::ClocksManager, servo: Pin<hal::gpio::bank0::Gpio6, FunctionNull, PullDown>
) -> PwmSlice3 {
let mut s = hal::pwm::Slices::new(p, r).pwm3;
configure_servo_div(&mut s, c);
s.enable();
s.channel_a.output_to(servo);
s
}
/// Set the clock divider and wrap for a servo PWM slice.
///
/// # Arguments
///
/// * `slice` - Mutable reference to the PWM slice to configure.
/// * `clocks` - Reference to the initialised clock configuration.
///
/// # Arguments
///
/// * `slice` - The `slice` parameter.
/// * `clocks` - The `clocks` parameter.
fn configure_servo_div(s: &mut PwmSlice3, c: &hal::clocks::ClocksManager) {
let div = servo_lib::servo::calc_clk_div(c.system_clock.freq().to_Hz(), servo_lib::servo::SERVO_HZ, servo_lib::servo::SERVO_WRAP);
s.set_div_int(div as u8);
s.set_div_frac((((div - (div as f32)) * 16.0) as u8).min(15));
s.set_top(servo_lib::servo::SERVO_WRAP as u16);
}
/// Print the servo initialisation banner over UART.
///
/// # Arguments
///
/// * `uart` - Reference to the enabled UART peripheral for serial output.
///
/// # Arguments
///
/// * `uart` - The `uart` parameter.
fn announce_servo(uart: &EnabledUart) {
uart.write_full_blocking(b"Servo driver initialized on GPIO 6\r\n");
uart.write_full_blocking(b"Sweeping 0 -> 180 -> 0 degrees in 10-degree steps\r\n");
}
/// Run the servo angle sweep loop forever.
///
/// # Arguments
///
/// * `uart` - Reference to the enabled UART peripheral for serial output.
/// * `pwm` - Mutable reference to the configured PWM slice.
/// * `delay` - Mutable reference to the blocking delay provider.
///
/// # Returns
///
/// A value of type `!`.
///
/// # Arguments
///
/// * `uart` - The `uart` parameter.
/// * `pwm` - The `pwm` parameter.
/// * `delay` - Delay value.
///
/// # Returns
///
/// A value of type `!`.
fn servo_loop(u: &EnabledUart, p: &mut PwmSlice3, d: &mut cortex_m::delay::Delay) -> ! {
let mut buf = [0u8; 20];
loop { sweep_angle_up(u, &mut p.channel_a, d, &mut buf); sweep_angle_down(u, &mut p.channel_a, d, &mut buf); }
}
+6
View File
@@ -0,0 +1,6 @@
//! Driver crate
#![deny(missing_docs)]
#![deny(clippy::missing_docs_in_private_items)]
#![cfg_attr(not(test), no_std)]
pub mod servo;
+102
View File
@@ -0,0 +1,102 @@
//! Driver crate
#![deny(missing_docs)]
#![deny(clippy::missing_docs_in_private_items)]
//! Implementation module
//!
//! **File:** `main.rs`
//! **Author:** Kevin Thomas
//! **Date:** 2025
//!
//! MIT License
//!
//! Copyright (c) 2025 Kevin Thomas
//!
//! Permission is hereby granted, free of charge, to any person obtaining a copy
//! of this software and associated documentation files (the "Software"), to deal
//! in the Software without restriction, including without limitation the rights
//! to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//! copies of the Software, and to permit persons to whom the Software is
//! furnished to do so, subject to the following conditions:
//!
//! The above copyright notice and this permission notice shall be included in
//! all copies or substantial portions of the Software.
//!
//! THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//! IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//! FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//! AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//! LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//! OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
//! SOFTWARE.
#![no_std]
#![no_main]
// Board-level helpers: constants, type aliases, and init functions
mod board;
// Servo driver module — suppress warnings for unused public API functions
// Debugging output over RTT
use defmt_rtt as _;
// Panic handler for RISC-V targets
#[cfg(target_arch = "riscv32")]
// Import panic_halt as _
use panic_halt as _;
// Panic handler for ARM targets
#[cfg(target_arch = "arm")]
// Import panic_probe as _
use panic_probe as _;
// HAL entry-point macro
use hal::entry;
// Alias our HAL crate
#[cfg(rp2350)]
// Import rp235x_hal as hal
use rp235x_hal as hal;
#[cfg(rp2040)]
// Import rp2040_hal as hal
use rp2040_hal as hal;
/// Second-stage boot loader for RP2040
#[unsafe(link_section = ".boot2")]
#[used]
#[cfg(rp2040)]
pub static BOOT2: [u8; 256] = rp2040_boot2::BOOT_LOADER_W25Q080;
/// Boot metadata for the RP2350 Boot ROM
#[unsafe(link_section = ".start_block")]
#[used]
#[cfg(rp2350)]
pub static IMAGE_DEF: hal::block::ImageDef = hal::block::ImageDef::secure_exe();
/// Application entry point for the servo sweep demo.
///
/// # Returns
///
/// A value of type `!`.
///
/// # Returns
///
/// A value of type `!`.
#[entry]
fn main() -> ! {
board::run(hal::pac::Peripherals::take().unwrap())
}
/// Picotool binary info metadata
#[unsafe(link_section = ".bi_entries")]
#[used]
pub static PICOTOOL_ENTRIES: [hal::binary_info::EntryAddr; 5] = [
hal::binary_info::rp_cargo_bin_name!(),
hal::binary_info::rp_cargo_version!(),
hal::binary_info::rp_program_description!(c"SG90 Servo Sweep Demo"),
hal::binary_info::rp_cargo_homepage_url!(),
hal::binary_info::rp_program_build_attribute!(),
];
#[cfg(test)]
mod tests {
// Import all parent module items
use super::*;
}
+293
View File
@@ -0,0 +1,293 @@
//! Implementation module
//!
//! **File:** `servo.rs`
//! **Author:** Kevin Thomas
//! **Date:** 2025
//!
//! MIT License
//!
//! Copyright (c) 2025 Kevin Thomas
//!
//! Permission is hereby granted, free of charge, to any person obtaining a copy
//! of this software and associated documentation files (the "Software"), to deal
//! in the Software without restriction, including without limitation the rights
//! to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//! copies of the Software, and to permit persons to whom the Software is
//! furnished to do so, subject to the following conditions:
//!
//! The above copyright notice and this permission notice shall be included in
//! all copies or substantial portions of the Software.
//!
//! THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//! IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//! FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//! AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//! LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//! OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
//! SOFTWARE.
/// Default minimum pulse width in microseconds (0 degrees).
pub const SERVO_DEFAULT_MIN_US: u16 = 1000;
/// Default maximum pulse width in microseconds (180 degrees).
pub const SERVO_DEFAULT_MAX_US: u16 = 2000;
/// Default PWM wrap value for 50 Hz servo (20 000 - 1).
pub const SERVO_WRAP: u32 = 20000 - 1;
/// Default servo frequency in Hz.
pub const SERVO_HZ: f32 = 50.0;
/// Convert a pulse width in microseconds to a PWM counter level.
///
/// Uses the configured PWM wrap and servo frequency to map pulse time
/// into the channel compare value expected by the PWM hardware.
///
/// # Arguments
///
/// * `pulse_us` - Pulse width in microseconds.
/// * `wrap` - PWM counter wrap value.
/// * `hz` - PWM frequency in Hz.
///
/// # Returns
///
/// PWM level suitable for the channel compare register.
///
/// # Arguments
///
/// * `pulse_us` - The `pulse_us` parameter.
/// * `wrap` - The `wrap` parameter.
/// * `hz` - The `hz` parameter.
///
/// # Returns
///
/// A 32-bit unsigned integer value.
#[inline]
pub fn pulse_us_to_level(pulse_us: u32, wrap: u32, hz: f32) -> u32 {
let period_us = 1_000_000.0f32 / hz;
let counts_per_us = (wrap + 1) as f32 / period_us;
(pulse_us as f32 * counts_per_us + 0.5f32) as u32
}
/// Clamp a pulse width to the valid servo range.
///
/// Values below min_us are raised to min_us; values above max_us are
/// lowered to max_us.
///
/// # Arguments
///
/// * `pulse_us` - Raw pulse width in microseconds.
/// * `min_us` - Minimum allowed pulse width.
/// * `max_us` - Maximum allowed pulse width.
///
/// # Returns
///
/// Clamped pulse width.
///
/// # Arguments
///
/// * `pulse_us` - The `pulse_us` parameter.
/// * `min_us` - The `min_us` parameter.
/// * `max_us` - The `max_us` parameter.
///
/// # Returns
///
/// A 16-bit unsigned integer value.
#[inline]
pub fn clamp_pulse_us(pulse_us: u16, min_us: u16, max_us: u16) -> u16 {
if pulse_us < min_us {
min_us
} else if pulse_us > max_us {
max_us
} else {
pulse_us
}
}
/// Clamp a floating-point angle to the valid servo range [0.0, 180.0].
///
/// # Arguments
///
/// * `degrees` - The `degrees` parameter.
///
/// # Returns
///
/// A value of type `f32`.
///
/// # Arguments
///
/// * `degrees` - The `degrees` parameter.
///
/// # Returns
///
/// A value of type `f32`.
#[inline]
fn clamp_degrees(degrees: f32) -> f32 {
if degrees < 0.0f32 {
0.0f32
} else if degrees > 180.0f32 {
180.0f32
} else {
degrees
}
}
/// Map a servo angle in degrees to a pulse width in microseconds.
///
/// Clamps degrees to [0, 180], then linearly maps to the pulse range.
///
/// # Arguments
///
/// * `degrees` - Angle in degrees (0.0 to 180.0).
/// * `min_us` - Pulse width at 0 degrees.
/// * `max_us` - Pulse width at 180 degrees.
///
/// # Returns
///
/// Pulse width in microseconds corresponding to the given angle.
///
/// # Arguments
///
/// * `degrees` - The `degrees` parameter.
/// * `min_us` - The `min_us` parameter.
/// * `max_us` - The `max_us` parameter.
///
/// # Returns
///
/// A 16-bit unsigned integer value.
#[inline]
pub fn angle_to_pulse_us(degrees: f32, min_us: u16, max_us: u16) -> u16 {
let d = clamp_degrees(degrees);
let ratio = d / 180.0f32;
let span = (max_us - min_us) as f32;
(min_us as f32 + ratio * span + 0.5f32) as u16
}
/// Compute the PWM clock divider for the servo frequency.
///
/// # Arguments
///
/// * `sys_hz` - System clock frequency in Hz.
/// * `servo_hz` - Desired servo PWM frequency in Hz.
/// * `wrap` - PWM counter wrap value.
///
/// # Returns
///
/// Clock divider value.
///
/// # Arguments
///
/// * `sys_hz` - The `sys_hz` parameter.
/// * `servo_hz` - The `servo_hz` parameter.
/// * `wrap` - The `wrap` parameter.
///
/// # Returns
///
/// A value of type `f32`.
#[inline]
pub fn calc_clk_div(sys_hz: u32, servo_hz: f32, wrap: u32) -> f32 {
sys_hz as f32 / (servo_hz * (wrap + 1) as f32)
}
#[cfg(test)]
mod tests {
// Import all parent module items
use super::*;
/// Executes the pulse us to level 1000us operation.
#[test]
fn pulse_us_to_level_1000us() {
let level = pulse_us_to_level(1000, SERVO_WRAP, SERVO_HZ);
assert_eq!(level, 1000);
}
/// Executes the pulse us to level 2000us operation.
#[test]
fn pulse_us_to_level_2000us() {
let level = pulse_us_to_level(2000, SERVO_WRAP, SERVO_HZ);
assert_eq!(level, 2000);
}
/// Executes the pulse us to level 1500us operation.
#[test]
fn pulse_us_to_level_1500us() {
let level = pulse_us_to_level(1500, SERVO_WRAP, SERVO_HZ);
assert_eq!(level, 1500);
}
/// Executes the pulse us to level zero operation.
#[test]
fn pulse_us_to_level_zero() {
let level = pulse_us_to_level(0, SERVO_WRAP, SERVO_HZ);
assert_eq!(level, 0);
}
/// Executes the clamp pulse us below min operation.
#[test]
fn clamp_pulse_us_below_min() {
assert_eq!(
clamp_pulse_us(500, SERVO_DEFAULT_MIN_US, SERVO_DEFAULT_MAX_US),
1000
);
}
/// Executes the clamp pulse us above max operation.
#[test]
fn clamp_pulse_us_above_max() {
assert_eq!(
clamp_pulse_us(3000, SERVO_DEFAULT_MIN_US, SERVO_DEFAULT_MAX_US),
2000
);
}
/// Executes the clamp pulse us within range operation.
#[test]
fn clamp_pulse_us_within_range() {
assert_eq!(
clamp_pulse_us(1500, SERVO_DEFAULT_MIN_US, SERVO_DEFAULT_MAX_US),
1500
);
}
/// Executes the angle to pulse us zero operation.
#[test]
fn angle_to_pulse_us_zero() {
let pulse = angle_to_pulse_us(0.0, SERVO_DEFAULT_MIN_US, SERVO_DEFAULT_MAX_US);
assert_eq!(pulse, 1000);
}
/// Executes the angle to pulse us 180 operation.
#[test]
fn angle_to_pulse_us_180() {
let pulse = angle_to_pulse_us(180.0, SERVO_DEFAULT_MIN_US, SERVO_DEFAULT_MAX_US);
assert_eq!(pulse, 2000);
}
/// Executes the angle to pulse us 90 operation.
#[test]
fn angle_to_pulse_us_90() {
let pulse = angle_to_pulse_us(90.0, SERVO_DEFAULT_MIN_US, SERVO_DEFAULT_MAX_US);
assert_eq!(pulse, 1500);
}
/// Executes the angle to pulse us clamped negative operation.
#[test]
fn angle_to_pulse_us_clamped_negative() {
let pulse = angle_to_pulse_us(-10.0, SERVO_DEFAULT_MIN_US, SERVO_DEFAULT_MAX_US);
assert_eq!(pulse, 1000);
}
/// Executes the angle to pulse us clamped above operation.
#[test]
fn angle_to_pulse_us_clamped_above() {
let pulse = angle_to_pulse_us(200.0, SERVO_DEFAULT_MIN_US, SERVO_DEFAULT_MAX_US);
assert_eq!(pulse, 2000);
}
/// Executes the calc clk div 150mhz operation.
#[test]
fn calc_clk_div_150mhz() {
let div = calc_clk_div(150_000_000, SERVO_HZ, SERVO_WRAP);
assert!((div - 150.0).abs() < 0.01);
}
}