diff --git a/README.md b/README.md index 622d66b..8241ddc 100644 --- a/README.md +++ b/README.md @@ -514,12 +514,9 @@ Self-contained C driver libraries for the Raspberry Pi Pico 2 (RP2350). Each dri # Rust Drivers Rust ports of the C drivers above using `rp235x-hal`. Each Rust driver folder contains a thin demo (`main.rs`) and a reusable library module (`driver.rs`) with full Rust doc comments. -### Building & Testing +### Testing ```bash -# Build for the RP2350 target (from any driver folder) -cargo build - # Run unit tests on the host cargo test --lib --target x86_64-pc-windows-msvc ``` @@ -530,6 +527,7 @@ cargo test --lib --target x86_64-pc-windows-msvc | [0x02_blink_rust](drivers/0x02_blink_rust) | GPIO output LED blink | `rp235x-hal`, `embedded-hal` | | [0x03_button_rust](drivers/0x03_button_rust) | Push-button input with debounce | `rp235x-hal`, `embedded-hal` | | [0x04_pwm_rust](drivers/0x04_pwm_rust) | Generic PWM output | `rp235x-hal`, `embedded-hal` | +| [0x05_servo_rust](drivers/0x05_servo_rust) | SG90 servo motor control | `rp235x-hal`, `embedded-hal` |
diff --git a/drivers/0x05_servo_rust/.cargo/config.toml b/drivers/0x05_servo_rust/.cargo/config.toml new file mode 100644 index 0000000..1791d74 --- /dev/null +++ b/drivers/0x05_servo_rust/.cargo/config.toml @@ -0,0 +1,102 @@ +# SPDX-License-Identifier: MIT OR Apache-2.0 +# +# Copyright (c) 2021–2024 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" diff --git a/drivers/0x05_servo_rust/.gitignore b/drivers/0x05_servo_rust/.gitignore new file mode 100644 index 0000000..03381c1 --- /dev/null +++ b/drivers/0x05_servo_rust/.gitignore @@ -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 diff --git a/drivers/0x05_servo_rust/.pico-rs b/drivers/0x05_servo_rust/.pico-rs new file mode 100644 index 0000000..1b6702a --- /dev/null +++ b/drivers/0x05_servo_rust/.pico-rs @@ -0,0 +1 @@ +rp2350 \ No newline at end of file diff --git a/drivers/0x05_servo_rust/.vscode/extensions.json b/drivers/0x05_servo_rust/.vscode/extensions.json new file mode 100644 index 0000000..5f2fd0c --- /dev/null +++ b/drivers/0x05_servo_rust/.vscode/extensions.json @@ -0,0 +1,8 @@ +{ + "recommendations": [ + "marus25.cortex-debug", + "rust-lang.rust-analyzer", + "probe-rs.probe-rs-debugger", + "raspberry-pi.raspberry-pi-pico" + ] +} \ No newline at end of file diff --git a/drivers/0x05_servo_rust/.vscode/launch.json b/drivers/0x05_servo_rust/.vscode/launch.json new file mode 100644 index 0000000..0bc38c3 --- /dev/null +++ b/drivers/0x05_servo_rust/.vscode/launch.json @@ -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" + } + ] +} \ No newline at end of file diff --git a/drivers/0x05_servo_rust/.vscode/settings.json b/drivers/0x05_servo_rust/.vscode/settings.json new file mode 100644 index 0000000..b03cd57 --- /dev/null +++ b/drivers/0x05_servo_rust/.vscode/settings.json @@ -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 + } +} \ No newline at end of file diff --git a/drivers/0x05_servo_rust/.vscode/tasks.json b/drivers/0x05_servo_rust/.vscode/tasks.json new file mode 100644 index 0000000..4194af4 --- /dev/null +++ b/drivers/0x05_servo_rust/.vscode/tasks.json @@ -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": [] + } + ] +} \ No newline at end of file diff --git a/drivers/0x05_servo_rust/Cargo.toml b/drivers/0x05_servo_rust/Cargo.toml new file mode 100644 index 0000000..8504505 --- /dev/null +++ b/drivers/0x05_servo_rust/Cargo.toml @@ -0,0 +1,40 @@ +[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" + +[build-dependencies] +regex = "1.11.0" + +[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"] } diff --git a/drivers/0x05_servo_rust/LICENSE-APACHE b/drivers/0x05_servo_rust/LICENSE-APACHE new file mode 100644 index 0000000..8d99cbc --- /dev/null +++ b/drivers/0x05_servo_rust/LICENSE-APACHE @@ -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) 2021–2024 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. \ No newline at end of file diff --git a/drivers/0x05_servo_rust/LICENSE-MIT b/drivers/0x05_servo_rust/LICENSE-MIT new file mode 100644 index 0000000..5369c70 --- /dev/null +++ b/drivers/0x05_servo_rust/LICENSE-MIT @@ -0,0 +1,24 @@ +MIT License + +Copyright (c) 2021–2024 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. + \ No newline at end of file diff --git a/drivers/0x05_servo_rust/build.rs b/drivers/0x05_servo_rust/build.rs new file mode 100644 index 0000000..e6de3f1 --- /dev/null +++ b/drivers/0x05_servo_rust/build.rs @@ -0,0 +1,68 @@ +//! SPDX-License-Identifier: MIT OR Apache-2.0 +//! +//! Copyright (c) 2021–2024 The rp-rs Developers +//! Copyright (c) 2021 rp-rs organization +//! Copyright (c) 2025 Raspberry Pi Ltd. +//! +//! Set up linker scripts + +use std::fs::{ File, read_to_string }; +use std::io::Write; +use std::path::PathBuf; + +use regex::Regex; + +fn main() { + println!("cargo::rustc-check-cfg=cfg(rp2040)"); + println!("cargo::rustc-check-cfg=cfg(rp2350)"); + + // Put the linker script somewhere the linker can find it + let out = PathBuf::from(std::env::var_os("OUT_DIR").unwrap()); + println!("cargo:rustc-link-search={}", out.display()); + + println!("cargo:rerun-if-changed=.pico-rs"); + let contents = read_to_string(".pico-rs") + .map(|s| s.trim().to_string().to_lowercase()) + .unwrap_or_else(|e| { + eprintln!("Failed to read file: {}", e); + String::new() + }); + + // The file `memory.x` is loaded by cortex-m-rt's `link.x` script, which + // is what we specify in `.cargo/config.toml` for Arm builds + let target; + if contents == "rp2040" { + target = "thumbv6m-none-eabi"; + let memory_x = include_bytes!("rp2040.x"); + let mut f = File::create(out.join("memory.x")).unwrap(); + f.write_all(memory_x).unwrap(); + println!("cargo::rustc-cfg=rp2040"); + println!("cargo:rerun-if-changed=rp2040.x"); + } else { + if contents.contains("riscv") { + target = "riscv32imac-unknown-none-elf"; + } else { + target = "thumbv8m.main-none-eabihf"; + } + let memory_x = include_bytes!("rp2350.x"); + let mut f = File::create(out.join("memory.x")).unwrap(); + f.write_all(memory_x).unwrap(); + println!("cargo::rustc-cfg=rp2350"); + println!("cargo:rerun-if-changed=rp2350.x"); + } + + let re = Regex::new(r"target = .*").unwrap(); + let config_toml = include_str!(".cargo/config.toml"); + let result = re.replace(config_toml, format!("target = \"{}\"", target)); + let mut f = File::create(".cargo/config.toml").unwrap(); + f.write_all(result.as_bytes()).unwrap(); + + // The file `rp2350_riscv.x` is what we specify in `.cargo/config.toml` for + // RISC-V builds + let rp2350_riscv_x = include_bytes!("rp2350_riscv.x"); + let mut f = File::create(out.join("rp2350_riscv.x")).unwrap(); + f.write_all(rp2350_riscv_x).unwrap(); + println!("cargo:rerun-if-changed=rp2350_riscv.x"); + + println!("cargo:rerun-if-changed=build.rs"); +} diff --git a/drivers/0x05_servo_rust/rp2040.x b/drivers/0x05_servo_rust/rp2040.x new file mode 100644 index 0000000..0cc665b --- /dev/null +++ b/drivers/0x05_servo_rust/rp2040.x @@ -0,0 +1,91 @@ +/* +* SPDX-License-Identifier: MIT OR Apache-2.0 +* +* Copyright (c) 2021–2024 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; diff --git a/drivers/0x05_servo_rust/rp2350.x b/drivers/0x05_servo_rust/rp2350.x new file mode 100644 index 0000000..bda94d8 --- /dev/null +++ b/drivers/0x05_servo_rust/rp2350.x @@ -0,0 +1,83 @@ +/* +* SPDX-License-Identifier: MIT OR Apache-2.0 +* +* Copyright (c) 2021–2024 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); + \ No newline at end of file diff --git a/drivers/0x05_servo_rust/rp2350_riscv.x b/drivers/0x05_servo_rust/rp2350_riscv.x new file mode 100644 index 0000000..84388aa --- /dev/null +++ b/drivers/0x05_servo_rust/rp2350_riscv.x @@ -0,0 +1,259 @@ +/* +* SPDX-License-Identifier: MIT OR Apache-2.0 +* +* Copyright (c) 2021–2024 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 | */ diff --git a/drivers/0x05_servo_rust/src/lib.rs b/drivers/0x05_servo_rust/src/lib.rs new file mode 100644 index 0000000..70d0b5f --- /dev/null +++ b/drivers/0x05_servo_rust/src/lib.rs @@ -0,0 +1,8 @@ +//! @file lib.rs +//! @brief Library root for the servo driver crate +//! @author Kevin Thomas +//! @date 2025 + +#![no_std] + +pub mod servo; diff --git a/drivers/0x05_servo_rust/src/main.rs b/drivers/0x05_servo_rust/src/main.rs new file mode 100644 index 0000000..e00aa52 --- /dev/null +++ b/drivers/0x05_servo_rust/src/main.rs @@ -0,0 +1,324 @@ +//! @file main.rs +//! @brief SG90 servo motor driver demonstration +//! @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. +//! +//! ----------------------------------------------------------------------------- +//! +//! Demonstrates SG90 servo control using the servo driver (servo.rs). +//! PWM at 50 Hz on GPIO 6 sweeps the servo from 0 degrees to 180 degrees +//! and back in 10-degree increments, printing each angle over UART. +//! +//! Wiring: +//! GPIO6 -> Servo signal wire (orange or yellow) +//! 5V -> Servo power wire (red) -- use external 5 V supply for load +//! GND -> Servo ground wire (brown or black) + +#![no_std] +#![no_main] + +#[allow(dead_code)] +mod servo; + +use defmt_rtt as _; +#[cfg(target_arch = "riscv32")] +use panic_halt as _; +#[cfg(target_arch = "arm")] +use panic_probe as _; + +use embedded_hal::pwm::SetDutyCycle; +use fugit::RateExtU32; +use hal::entry; +use hal::Clock; +use hal::gpio::{FunctionNull, FunctionUart, Pin, PullDown, PullNone}; +use hal::uart::{DataBits, Enabled, StopBits, UartConfig, UartPeripheral}; + +#[cfg(rp2350)] +use rp235x_hal as hal; + +#[cfg(rp2040)] +use rp2040_hal as hal; + +#[unsafe(link_section = ".boot2")] +#[used] +#[cfg(rp2040)] +pub static BOOT2: [u8; 256] = rp2040_boot2::BOOT_LOADER_W25Q080; + +#[unsafe(link_section = ".start_block")] +#[used] +#[cfg(rp2350)] +pub static IMAGE_DEF: hal::block::ImageDef = hal::block::ImageDef::secure_exe(); + +const XTAL_FREQ_HZ: u32 = 12_000_000u32; + +const UART_BAUD: u32 = 115_200; +const STEP_DEGREES: i32 = 10; +const STEP_DELAY_MS: u32 = 150; + +type TxPin = Pin; +type RxPin = Pin; +type TxPinDefault = Pin; +type RxPinDefault = Pin; +type EnabledUart = UartPeripheral; + +/// 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. +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. +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. +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::(), + rx_pin.reconfigure::(), + ); + 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. +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()) +} + +/// 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. +fn format_angle(buf: &mut [u8], angle: i32) -> usize { + let prefix = b"Angle: "; + buf[..7].copy_from_slice(prefix); + let mut pos = 7; + let a = if angle < 0 { 0 } else { angle as u32 }; + if a >= 100 { + buf[pos] = b'0' + (a / 100) as u8; pos += 1; + buf[pos] = b'0' + ((a / 10) % 10) as u8; pos += 1; + buf[pos] = b'0' + (a % 10) as u8; pos += 1; + } else if a >= 10 { + buf[pos] = b' '; pos += 1; + buf[pos] = b'0' + (a / 10) as u8; pos += 1; + buf[pos] = b'0' + (a % 10) as u8; pos += 1; + } else { + buf[pos] = b' '; pos += 1; + buf[pos] = b' '; pos += 1; + buf[pos] = b'0' + a as u8; pos += 1; + } + let suffix = b" deg\r\n"; + buf[pos..pos + 6].copy_from_slice(suffix); + 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. +fn sweep_angle_up( + uart: &EnabledUart, + channel: &mut impl SetDutyCycle, + delay: &mut cortex_m::delay::Delay, + buf: &mut [u8; 20], +) { + let mut angle: i32 = 0; + while angle <= 180 { + let pulse = servo::angle_to_pulse_us(angle as f32, servo::SERVO_DEFAULT_MIN_US, servo::SERVO_DEFAULT_MAX_US); + let level = servo::pulse_us_to_level(pulse as u32, servo::SERVO_WRAP, servo::SERVO_HZ) as u16; + channel.set_duty_cycle(level).ok(); + let n = format_angle(buf, angle); + uart.write_full_blocking(&buf[..n]); + delay.delay_ms(STEP_DELAY_MS); + 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. +fn sweep_angle_down( + uart: &EnabledUart, + channel: &mut impl SetDutyCycle, + delay: &mut cortex_m::delay::Delay, + buf: &mut [u8; 20], +) { + let mut angle: i32 = 180; + while angle >= 0 { + let pulse = servo::angle_to_pulse_us(angle as f32, servo::SERVO_DEFAULT_MIN_US, servo::SERVO_DEFAULT_MAX_US); + let level = servo::pulse_us_to_level(pulse as u32, servo::SERVO_WRAP, servo::SERVO_HZ) as u16; + channel.set_duty_cycle(level).ok(); + let n = format_angle(buf, angle); + uart.write_full_blocking(&buf[..n]); + delay.delay_ms(STEP_DELAY_MS); + angle -= STEP_DEGREES; + } +} + +/// Application entry point for the servo sweep demo. +/// +/// Initializes the servo on GPIO 6 and continuously sweeps 0-180-0 +/// degrees in 10-degree increments, reporting each angle over UART. +/// +/// # Returns +/// +/// Does not return. +#[entry] +fn main() -> ! { + let mut pac = hal::pac::Peripherals::take().unwrap(); + let clocks = init_clocks( + pac.XOSC, pac.CLOCKS, pac.PLL_SYS, pac.PLL_USB, &mut pac.RESETS, + &mut hal::Watchdog::new(pac.WATCHDOG), + ); + let pins = init_pins(pac.IO_BANK0, pac.PADS_BANK0, pac.SIO, &mut pac.RESETS); + let uart = init_uart(pac.UART0, pins.gpio0, pins.gpio1, &mut pac.RESETS, &clocks); + let mut delay = init_delay(&clocks); + let pwm_slices = hal::pwm::Slices::new(pac.PWM, &mut pac.RESETS); + let mut pwm = pwm_slices.pwm3; + let sys_hz = clocks.system_clock.freq().to_Hz(); + let div = servo::calc_clk_div(sys_hz, servo::SERVO_HZ, servo::SERVO_WRAP); + let div_int = div as u8; + pwm.set_div_int(div_int); + pwm.set_div_frac((((div - div_int as f32) * 16.0) as u8).min(15)); + pwm.set_top(servo::SERVO_WRAP as u16); + pwm.enable(); + pwm.channel_a.output_to(pins.gpio6); + 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"); + let mut buf = [0u8; 20]; + loop { + sweep_angle_up(&uart, &mut pwm.channel_a, &mut delay, &mut buf); + sweep_angle_down(&uart, &mut pwm.channel_a, &mut delay, &mut buf); + } +} + +#[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!(), +]; + +// End of file diff --git a/drivers/0x05_servo_rust/src/servo.rs b/drivers/0x05_servo_rust/src/servo.rs new file mode 100644 index 0000000..72b3eb5 --- /dev/null +++ b/drivers/0x05_servo_rust/src/servo.rs @@ -0,0 +1,205 @@ +//! @file servo.rs +//! @brief Implementation of a simple SG90 servo driver (pure-logic helpers) +//! @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. +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. +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 + } +} + +/// 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. +pub fn angle_to_pulse_us(degrees: f32, min_us: u16, max_us: u16) -> u16 { + let d = if degrees < 0.0f32 { + 0.0f32 + } else if degrees > 180.0f32 { + 180.0f32 + } else { + 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. +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 { + use super::*; + + #[test] + fn pulse_us_to_level_1000us() { + let level = pulse_us_to_level(1000, SERVO_WRAP, SERVO_HZ); + assert_eq!(level, 1000); + } + + #[test] + fn pulse_us_to_level_2000us() { + let level = pulse_us_to_level(2000, SERVO_WRAP, SERVO_HZ); + assert_eq!(level, 2000); + } + + #[test] + fn pulse_us_to_level_1500us() { + let level = pulse_us_to_level(1500, SERVO_WRAP, SERVO_HZ); + assert_eq!(level, 1500); + } + + #[test] + fn pulse_us_to_level_zero() { + let level = pulse_us_to_level(0, SERVO_WRAP, SERVO_HZ); + assert_eq!(level, 0); + } + + #[test] + fn clamp_pulse_us_below_min() { + assert_eq!(clamp_pulse_us(500, SERVO_DEFAULT_MIN_US, SERVO_DEFAULT_MAX_US), 1000); + } + + #[test] + fn clamp_pulse_us_above_max() { + assert_eq!(clamp_pulse_us(3000, SERVO_DEFAULT_MIN_US, SERVO_DEFAULT_MAX_US), 2000); + } + + #[test] + fn clamp_pulse_us_within_range() { + assert_eq!(clamp_pulse_us(1500, SERVO_DEFAULT_MIN_US, SERVO_DEFAULT_MAX_US), 1500); + } + + #[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); + } + + #[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); + } + + #[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); + } + + #[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); + } + + #[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); + } + + #[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); + } +} + +// End of file