diff --git a/CMakeLists.txt b/CMakeLists.txt index f3f3389..86c4c63 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,18 +14,20 @@ cmake_minimum_required(VERSION 3.15) +if(DEFINED ENV{PICO_SDK_PATH}) +set(PICO_CXX_ENABLE_EXCEPTIONS 1) +set(PICO_NO_PICOTOOL 1) +include($ENV{PICO_SDK_PATH}/external/pico_sdk_import.cmake) +endif() set(CMAKE_COLOR_DIAGNOSTICS ON) -project(libhal-arm-mcu LANGUAGES CXX) +project(libhal-arm-mcu LANGUAGES CXX C ASM) -libhal_test_and_make_library( - LIBRARY_NAME libhal-arm-mcu - - SOURCES - # Exceptions code size reducer - src/terminate_handler.cpp - - # cortex_m +# Should probably refine this to actually not compile +# drivers for all platforms all the time, but for now this +# serves my purposes +if(NOT DEFINED ENV{PICO_SDK_PATH}) +set(source_list src/system_controller.cpp src/dwt_counter.cpp src/interrupt.cpp @@ -92,8 +94,10 @@ libhal_test_and_make_library( # stm32f40 src/stm32f40/output_pin.cpp +) - TEST_SOURCES +set( + test_sources # cortex_m tests/dwt_counter.test.cpp tests/interrupt.test.cpp @@ -124,6 +128,49 @@ libhal_test_and_make_library( # stm32f411 tests/stm32f411/output_pin.test.cpp tests/stm32f411/spi.test.cpp +) + +else() +set(source_list + src/rp/gpio.cpp + src/rp/serial.cpp + src/rp/i2c.cpp + src/rp/pwm.cpp + src/rp/adc.cpp + src/rp/spi.cpp + src/rp/time.cpp + src/system_controller.cpp + src/dwt_counter.cpp + src/interrupt.cpp +) + +pico_sdk_init() + +set(pico_dep + hardware_gpio_headers + pico_time_headers + pico_stdio_headers + hardware_sync_headers + hardware_i2c_headers + hardware_pwm_headers + hardware_adc_headers + hardware_spi_headers + hardware_uart_headers + hardware_timer_headers + hardware_dma_headers +) + +endif() + +libhal_test_and_make_library( + LIBRARY_NAME libhal-arm-mcu + + SOURCES + ${source_list} + src/terminate_handler.cpp + + TEST_SOURCES + ${test_sources} PACKAGES libhal @@ -136,4 +183,6 @@ libhal_test_and_make_library( libhal::util nonstd::ring-span-lite nonstd::scope-lite + ${pico_dep} ) + diff --git a/conanfile.py b/conanfile.py index 419bfa6..d9f72f9 100644 --- a/conanfile.py +++ b/conanfile.py @@ -15,6 +15,9 @@ # limitations under the License. from conan import ConanFile +from conan.tools.cmake import CMakeDeps, CMakeToolchain +from conan.tools.env import VirtualBuildEnv +from conan.errors import ConanInvalidConfiguration from pathlib import Path import os @@ -43,6 +46,8 @@ class libhal_arm_mcu_conan(ConanFile): "use_libhal_exceptions": [True, False], "use_picolibc": [True, False], "use_default_linker_script": [True, False], + "variant": [None, "ANY"], + "board": [None, "ANY"], "replace_std_terminate": [True, False], "use_semihosting": [True, False], } @@ -53,6 +58,8 @@ class libhal_arm_mcu_conan(ConanFile): "use_default_linker_script": True, "replace_std_terminate": True, "use_semihosting": True, + "variant": None, + "board": None, } options_description = { "platform": "Specifies which platform to provide binaries and build information for", @@ -83,6 +90,13 @@ def requirements(self): OSLIB = "semihost" if self.options.use_semihosting else None self.requires("prebuilt-picolibc/" + CV, options={"crt0": CRT0, "oslib": OSLIB}) + + if str(self.options.platform).startswith("rp2"): + self.requires("picosdk/2.2.1-alpha") + self.tool_requires("pioasm/2.2.0") + if self.options.board.value.startswith("libhal_"): + board = self.options.board.value.removeprefix('libhal_').replace('_', '-') + self.requires(f"rp-board-header-{board}/latest", visible=True) def handle_stm32f1_linker_scripts(self): linker_script_name = list(str(self.options.platform)) @@ -97,6 +111,40 @@ def handle_stm32f1_linker_scripts(self): "-T" + str(Path("libhal-stm32f1") / linker_script_name + ".ld"), ]) + def _macro(self, string): + return string.upper().replace("-", "_") + + def generate(self): + virt = VirtualBuildEnv(self) + virt.generate() + tc = CMakeToolchain(self) + if str(self.options.platform).startswith("rp2"): + tc.cache_variables["DO_NOT_BUILD_BOOT_HAL"] = True + tc.preprocessor_definitions["PICO_STDIO_SHORT_CIRCUIT_CLIB_FUNCS"] = "0" + if self.options.board: + tc.cache_variables["PICO_BOARD"] = str(self.options.board) + if self.options.variant: + tc.preprocessor_definitions["LIBHAL_VARIANT_" + self._macro(str(self.options.variant))] = "1" + tc.preprocessor_definitions["LIBHAL_PLATFORM_" + self._macro(str(self.options.platform))] = "1" + tc.generate() + cmake = CMakeDeps(self) + cmake.generate() + + def validate(self): + if str(self.options.platform).startswith("rp2"): + if self.options.use_default_linker_script: + raise ConanInvalidConfiguration("Default linker scripts are not compatible with RP chips, use pico-sdk linker scripts instead") + if not self.options.board: + raise ConanInvalidConfiguration("RP board not specified") + if "rp2350" in str(self.options.platform): + if not self.options.variant: + raise ConanInvalidConfiguration("RP2350 variant not specified") + if self.options.variant not in ["rp2350a", "rp2350b"]: + raise ConanInvalidConfiguration("Invalid RP2350 variant specified") + if not self.options.board: + raise ConanInvalidConfiguration("Board must be specified during build") + super().validate() + def package_info(self): self.cpp_info.libs = ["libhal-arm-mcu"] self.cpp_info.set_property("cmake_target_name", "libhal::arm-mcu") @@ -104,11 +152,19 @@ def package_info(self): "libhal::lpc40", "libhal::stm32f1", "libhal::stm32f4", + "libhal::rp2350" ]) PLATFORM = str(self.options.platform) self.buildenv_info.define("LIBHAL_PLATFORM", PLATFORM) self.buildenv_info.define("LIBHAL_PLATFORM_LIBRARY", "arm-mcu") + if str(self.options.platform).startswith("rp2"): + defines = [] + if self.options.variant: + defines.append("LIBHAL_VARIANT_" + self._macro(str(self.options.variant)) + "=1") + defines.append("LIBHAL_PLATFORM_" + self._macro(str(self.options.platform)) + "=1") + defines.append("PICO_STDIO_SHORT_CIRCUIT_CLIB_FUNCS=0") + self.cpp_info.defines = defines self.cpp_info.exelinkflags = [] if self.settings.os == "baremetal": diff --git a/demos/CMakeLists.txt b/demos/CMakeLists.txt index 734634d..b74235d 100644 --- a/demos/CMakeLists.txt +++ b/demos/CMakeLists.txt @@ -14,10 +14,53 @@ cmake_minimum_required(VERSION 4.0) -project(demos LANGUAGES CXX) +if(DEFINED ENV{PICO_SDK_PATH}) +include($ENV{PICO_SDK_PATH}/external/pico_sdk_import.cmake) +endif() -libhal_build_demos( - DEMOS +project(demos LANGUAGES CXX C ASM) + +find_package(libhal-arm-mcu CONFIG REQUIRED) + +if(DEFINED ENV{PICO_SDK_PATH}) +pico_sdk_init() + +pico_enable_stdio_usb(libhal::arm-mcu 1) +pico_enable_stdio_uart(libhal::arm-mcu 0) + +set(demo_deps + pico_stdlib + hardware_gpio + pico_stdio + hardware_i2c + hardware_adc + hardware_spi +) + +# not all of the demos work at the moment +set(demos + adc + blinker + # can + # dac + gpio + i2c + interrupt_pin + uart + pwm + pwm16 + spi + blank + # stream_dac + # watchdog + # timer + # usb_cdc_raw + # semihost +) + +else() + +set(demos adc blinker can @@ -30,11 +73,23 @@ libhal_build_demos( pwm16 spi blank + stream_dac watchdog - # stream_dac timer usb_cdc_raw semihost +) + +set(demo_deps + minimp3::minimp3 +) +endif() + +set(ENV{LIBHAL_PLATFORM_LIBRARY} arm-mcu) + +libhal_build_demos( + DEMOS + ${demos} PACKAGES minimp3 @@ -43,5 +98,5 @@ libhal_build_demos( . LINK_LIBRARIES - minimp3::minimp3 + ${demo_deps} ) diff --git a/demos/conanfile.py b/demos/conanfile.py index e46dda7..9c1bca4 100644 --- a/demos/conanfile.py +++ b/demos/conanfile.py @@ -14,8 +14,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -from conan import ConanFile +import os +from conan import ConanFile +from conan.tools.cmake import CMake class demos(ConanFile): python_requires = "libhal-bootstrap/[>=4.3.0 <5]" @@ -24,3 +26,15 @@ class demos(ConanFile): def requirements(self): self.requires("libhal-arm-mcu/latest") self.requires("minimp3/cci.20211201") + + # This is kinda sketch, but needs to be done manually until https://github.com/conan-io/conan/issues/13372 + # gets implemented + def build(self): + cmake = CMake(self) + defs = { + "CMAKE_ASM_FLAGS_INIT": "-mcpu=cortex-m33 -mfloat-abi=soft", + "PICO_BOARD": "rp2350_micromod", + "PICO_CXX_ENABLE_EXCEPTIONS": "1" + } + cmake.configure(variables = defs) + cmake.build() diff --git a/demos/flash.sh b/demos/flash.sh new file mode 100755 index 0000000..645e735 --- /dev/null +++ b/demos/flash.sh @@ -0,0 +1,11 @@ +#!/usr/bin/sh + +set -e + +BUILDDIR=build/rp2350-arm-s/RelWithDebInfo +source $BUILDDIR/generators/conanbuild.sh + +picotool uf2 convert $BUILDDIR/$1.elf $BUILDDIR/$1.uf2 +picotool load $BUILDDIR/$1.uf2 -f +sleep 1 +plink -serial /dev/ttyACM0 -sercfg 115200,8,1,n,X diff --git a/demos/main.cpp b/demos/main.cpp index 4662321..5fd3e7e 100644 --- a/demos/main.cpp +++ b/demos/main.cpp @@ -18,13 +18,35 @@ #include #include +#include +#include -#include +#include "resource_list.hpp" + +[[noreturn]] void terminate_handler() noexcept +{ + + // spin here forever + while (true) { + continue; + } +} int main() { - initialize_platform(); - application(); + hal::set_terminate(terminate_handler); + + try { + application(); + } catch (hal::bad_optional_ptr_access const& e) { + auto console = resources::console(); + hal::print(*console, + "A resource required by the application was not" + "available!\n" + "Calling terminate!\n"); + + } // Allow any other exceptions to terminate the application + std::terminate(); } diff --git a/demos/platforms/rp2350-arm-s.cpp b/demos/platforms/rp2350-arm-s.cpp new file mode 100644 index 0000000..c6379a0 --- /dev/null +++ b/demos/platforms/rp2350-arm-s.cpp @@ -0,0 +1,156 @@ +#include "resource_list.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace rp = hal::rp; + +namespace resources { +namespace h5 = hal::v5; +static std::array buffer = {}; +std::pmr::monotonic_buffer_resource memory(buffer.data(), + buffer.size(), + std::pmr::null_memory_resource()); + +std::pmr::polymorphic_allocator<> driver_allocator() +{ + return &memory; +} +template +using opt = h5::optional_ptr; + +template +using sptr = h5::strong_ptr; + +opt clk; +sptr clock() +{ + if (not clk) { + clk = h5::make_strong_ptr(&memory); + } + return clk; +} + +opt out; +sptr console() +{ + if (not out) { + out = h5::make_strong_ptr(&memory); + } + return out; +} + +opt led; +sptr status_led() +{ + if (not led) { + // Actually for adafruit board. Change once micromods arrive. + led = h5::make_strong_ptr(&memory, hal::pin<46>); + } + return led; +} + +opt adc0; +sptr adc() +{ + if (not adc0) { + adc0 = h5::make_strong_ptr(&memory, hal::pin<40>); + } + return adc0; +} + +opt g0; +sptr input_pin() +{ + if (not g0) { + g0 = h5::make_strong_ptr(&memory, hal::pin<0>); + } + return g0; +} + +opt i2c0; +sptr i2c() +{ + if (not i2c0) { + i2c0 = h5::make_strong_ptr( + &memory, hal::pin<16>, hal::pin<17>, hal::bus<0>); + } + return i2c0; +} + +opt g1; +hal::callback do_nothing = [](bool) {}; +sptr interrupt_pin() +{ + if (not g1) { + g1 = + h5::make_strong_ptr(&memory, hal::pin<1>, do_nothing); + } + return g1; +} + +// TODO: add timer +sptr timed_interrupt(); + +opt slice; +opt pwm_pin; + +sptr pwm_frequency() +{ + if (not slice) { + slice = h5::make_strong_ptr>(&memory, hal::channel<7>); + } + return slice; +} + +sptr pwm_channel() +{ + (void)pwm_frequency(); // Need to initalize this first + if (not pwm_pin) { + pwm_pin = h5::make_strong_ptr( + &memory, 31, hal::rp::v5::pwm_pin_configuration{}, hal::unsafe{}); + } + return pwm_pin; +} + +opt ppin; +sptr pwm() +{ + if (not ppin) { + ppin = h5::make_strong_ptr(&memory, hal::unsafe{}, 32); + } + return ppin; +} + +opt spi0; +sptr spi() +{ + if (not spi0) { + spi0 = h5::make_strong_ptr( + &memory, hal::pin<35>, hal::pin<36>, hal::pin<34>); + } + return spi0; +} + +opt spi_cs0; +sptr spi_chip_select() +{ + if (not spi_cs0) { + spi_cs0 = h5::make_strong_ptr(&memory, hal::pin<33>); + } + return spi_cs0; +} + +} // namespace resources diff --git a/demos/run.sh b/demos/run.sh new file mode 100644 index 0000000..8ec999f --- /dev/null +++ b/demos/run.sh @@ -0,0 +1,15 @@ +#!/usr/bin/sh + +set -ex + +BUILDDIR=build/rp2350-arm-s/RelWithDebInfo + +conan build . -pr:h=rp2350b -pr:h=arm-gcc-12.3 --build=missing + +source $BUILDDIR/generators/conanbuild.sh + +picotool uf2 convert $BUILDDIR/blinker.elf $BUILDDIR/blinker.uf2 +picotool load $BUILDDIR/blinker.uf2 -f +sleep 1 +plink -serial /dev/ttyACM0 -sercfg 115200,8,1,n,X + diff --git a/include/libhal-arm-mcu/rp/adc.hpp b/include/libhal-arm-mcu/rp/adc.hpp new file mode 100644 index 0000000..4c40330 --- /dev/null +++ b/include/libhal-arm-mcu/rp/adc.hpp @@ -0,0 +1,127 @@ +#pragma once + +#include "rp.hpp" +#include "time.hpp" +#include +#include +#include + +namespace hal::rp { + +inline namespace v4 { +struct adc final : public hal::adc +{ + adc(pin_param auto pin) + : adc(pin()) + { + static_assert(internal::pin_max != 30 || (pin() >= 26 && pin() < 30), + "ADC pin is invalid!"); + static_assert(internal::pin_max != 48 || (pin() >= 40 && pin() < 48), + "ADC pin is invalid!"); + } + adc(adc&&) = delete; + ~adc() override; + +private: + adc(u8 pin); + /*Because the rp chips only have one ADC that's muxed + across different pins, we just initialize and mux the ADC + every time we want to read. */ + float driver_read() override; + u8 m_pin; +}; +} // namespace v4 + +namespace v5 { +// The ADC is 12 bit +struct adc16 final : public hal::adc16 +{ + adc16(pin_param auto pin) + : adc16(pin()) + { + static_assert(internal::pin_max != 30 || (pin() >= 26 && pin() < 30), + "ADC pin is invalid!"); + static_assert(internal::pin_max != 48 || (pin() >= 40 && pin() < 48), + "ADC pin is invalid!"); + } + ~adc16() override; + +private: + adc16(u8 gpio); + /*Because the rp chips only have one ADC that's muxed + across different pins, we just initialize and mux the ADC + every time we want to read. */ + u16 driver_read() override; + u8 m_pin; +}; +} // namespace v5 + +namespace nonstandard { +struct adc16_pack +{ + struct read_session; + + template + adc16_pack(Pins... ps) + : adc16_pack((to_mask(ps) | ...)) + { + } + adc16_pack(adc16_pack const&) = delete; + adc16_pack(adc16_pack&&) = delete; + + void read_many_now(std::span); + // Uses up 1 DMA channel + read_session async(); + +private: + adc16_pack(u8 pin_mask); + u8 to_mask(pin_param auto pin) + { + if constexpr (internal::pin_max == 30) { + static_assert(pin() >= 26 && pin() < 30); + return 1 << (pin() - 26); + } else if constexpr (internal::pin_max == 48) { + static_assert(pin() >= 40 && pin() < 48); + return 1 << (pin() - 40); + } + } + u8 m_read_size, m_first_pin; +}; + +struct adc16_pack::read_session +{ + struct promise; + promise read(std::span); + ~read_session(); + read_session(read_session const&) = delete; + read_session(read_session&&) = delete; + + struct promise + { + microseconds poll(); + promise(promise const&) = default; + + private: + friend read_session; + promise(u8 dma, u8 first_pin) // NOLINT + : m_dma(dma) + , m_first_pin(first_pin) + { + } + u8 m_dma, m_first_pin; + }; + +private: + friend adc16_pack; + read_session(u8 dma, u8 read_size, u8 first_pin) // NOLINT + : m_dma(dma) + , m_read_size(read_size) + , m_first_pin(first_pin) + { + } + u8 m_dma, m_read_size, m_first_pin; +}; + +} // namespace nonstandard + +} // namespace hal::rp diff --git a/include/libhal-arm-mcu/rp/i2c.hpp b/include/libhal-arm-mcu/rp/i2c.hpp new file mode 100644 index 0000000..3bc9d98 --- /dev/null +++ b/include/libhal-arm-mcu/rp/i2c.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include "rp.hpp" +#include +#include +#include + +namespace hal::rp::inline v4 { +/* +RP2350 supports a baud rate of up to 1 MHz +*/ +struct i2c final : public hal::i2c +{ + i2c(pin_param auto sda, + pin_param auto scl, + bus_param auto bus, + settings const& s = {}) + : i2c(sda(), scl(), bus(), s) + { + static_assert(bus() == 0 || bus() == 1, "Invalid bus selected!"); + static_assert(sda() % 4 == 0 || bus() != 0, "SDA pin for I2C0 is invalid!"); + static_assert(scl() % 4 == 1 || bus() != 0, "SCL pin for I2C0 is invalid!"); + static_assert(sda() % 4 == 2 || bus() != 1, "SDA pin for I2C1 is invalid!"); + static_assert(scl() % 4 == 3 || bus() != 1, "SCL pin for I2C1 is invalid!"); + } + i2c(i2c&&) = delete; + ~i2c() override; + +private: + i2c(u8 sda, u8 scl, u8 chan, settings const&); + void driver_configure(settings const&) override; + + /* + This function does not correctly use the timeout function, and will + throw it's own timeout exceptions if a transaction takes any longer + than 10 ms. + */ + void driver_transaction(hal::byte addr, + std::span out, + std::span in, + hal::function_ref) override; + + u8 m_sda, m_scl, m_chan; +}; + +} // namespace hal::rp::inline v4 diff --git a/include/libhal-arm-mcu/rp/input_pin.hpp b/include/libhal-arm-mcu/rp/input_pin.hpp new file mode 100644 index 0000000..05291d8 --- /dev/null +++ b/include/libhal-arm-mcu/rp/input_pin.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include "rp.hpp" +#include + +namespace hal::rp::inline v4 { +struct input_pin final : public hal::input_pin +{ + + input_pin(pin_param auto pin, settings const& s = {}) + : input_pin(pin(), s) + { + } + input_pin(input_pin&&) = delete; + ~input_pin() override; + +private: + input_pin(u8, settings const&); + void driver_configure(settings const&) override; + bool driver_level() override; + + u8 m_pin{}; +}; +} // namespace hal::rp::inline v4 diff --git a/include/libhal-arm-mcu/rp/interrupt_pin.hpp b/include/libhal-arm-mcu/rp/interrupt_pin.hpp new file mode 100644 index 0000000..2d5686c --- /dev/null +++ b/include/libhal-arm-mcu/rp/interrupt_pin.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include "rp.hpp" +#include +#include + +namespace hal::rp::inline v4 { +/* +Interrupt pin uses hidden globals to implement interrupts because +the interrupt callback function doesn't quite match the hal::handler type. +Because of that, it is unsafe to construct multiple interrupts across cores +or in an interrupt. Not that you'd do that anyways. +*/ +struct interrupt_pin final : public hal::interrupt_pin +{ + interrupt_pin(pin_param auto pin, + hal::callback callback, + settings const& options = {}) + : interrupt_pin(pin(), callback, options) + { + } + interrupt_pin(interrupt_pin&&) = delete; + ~interrupt_pin() override; + +private: + interrupt_pin(u8 pin, hal::callback, settings const&); + void driver_configure(settings const&) override; + void driver_on_trigger(hal::callback) override; + u8 m_pin; +}; +} // namespace hal::rp::inline v4 diff --git a/include/libhal-arm-mcu/rp/output_pin.hpp b/include/libhal-arm-mcu/rp/output_pin.hpp new file mode 100644 index 0000000..bca1c69 --- /dev/null +++ b/include/libhal-arm-mcu/rp/output_pin.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include "rp.hpp" +#include + +namespace hal::rp::inline v4 { +struct output_pin final : public hal::output_pin +{ + + output_pin(pin_param auto pin, settings const& options = {}) + : output_pin(pin(), options) + { + } + output_pin(output_pin&&) = delete; + ~output_pin() override; + +private: + output_pin(u8, settings const&); + + void driver_configure(settings const&) override; + bool driver_level() override; + void driver_level(bool) override; + + u8 m_pin{}; +}; +} // namespace hal::rp::inline v4 diff --git a/include/libhal-arm-mcu/rp/pwm.hpp b/include/libhal-arm-mcu/rp/pwm.hpp new file mode 100644 index 0000000..fd0d70a --- /dev/null +++ b/include/libhal-arm-mcu/rp/pwm.hpp @@ -0,0 +1,162 @@ +#pragma once + +#include "rp.hpp" +#include +#include +#include +#include + +namespace hal::rp { + +inline namespace v4 { +// this a bad backport to v4, since the pwm interface cannot +// guarantee two pwm's won't interfere +struct pwm_pin final : hal::pwm +{ + // By constructing this class you forfeit all claims to any + // warranty, implied or otherwise. You acknowledge you will + // read the relevant datasheet (RP2350 or RP2040 datasheet) + // and verify they are on different PWM slices. You waive + // any right to complain about two different PWM pins interfering + // with each other because they are on the same slice. + pwm_pin(hal::unsafe, u8 pin); + pwm_pin(pwm_pin&&) = delete; + ~pwm_pin() override; + +private: + void driver_frequency(hertz p_frequency) override; + void driver_duty_cycle(float p_duty_cycle) override; + + u8 m_pin; +}; +} // namespace v4 + +namespace v5 { +enum struct pwm_ch : u8 +{ + a, + b +}; + +struct pwm_pin; + +// See https://gcc.gnu.org/bugzilla/show_bug.cgi?id=88165 + +struct pwm_pin_configuration +{ + u16 duty_cycle = 0; + bool autostart = true; +}; + +/* This is the base runtime class for PWM. It cannot +be instantiated normally */ +struct pwm_slice_runtime : hal::pwm_group_manager +{ + + struct configuration + { + bool phase_correct = false; + }; + + pwm_slice_runtime(pwm_slice_runtime&&) = delete; + ~pwm_slice_runtime() override; + + void enable(bool enable = true); + +protected: + pwm_slice_runtime(u8 slice_num, configuration const&); + + pwm_pin get_pin_raw(u8 pin, pwm_pin_configuration const&); + /* + At frequencies above ~2288 Hz, the wrap counter + begins to lose resolution, reducing the duty cycle resolution + down from its theoretical 16 bits of resolution. + */ + void driver_frequency(u32 p_frequency) final; + u8 m_number; + bool m_phase_correct; +}; + +/* This cannot be constructed normally, and needs to be + * obtained from a pwm_slice type. */ +struct pwm_pin final : hal::pwm16_channel +{ + + pwm_pin(pwm_pin&&) = delete; + ~pwm_pin() override; + + friend pwm_slice_runtime; + + // not intended to be normally used. Use pwm_slice::get_pin() whenever + // possible + pwm_pin(u8 pin, pwm_pin_configuration const& c, hal::unsafe); + +private: + u32 driver_frequency() override; + + /* + When set to a 0% duty cycle, it disables the timer + completely. + */ + void driver_duty_cycle(u16 p_duty_cycle) override; + + u8 m_pin; + u8 m_slice; + bool m_autostart; +}; + +/* This globally starts all of the timers at once, which may be useful for +aligning their phases. Set argument to false to stop all of them at once */ +void enable_all_pwm(bool start = true); + +/* This is the actual pwm channel type that's meant to be used. It is also + * necessary to get the pins since pins cannot be constructed on their own.*/ +template +struct pwm_slice final : pwm_slice_runtime +{ + + pwm_slice(pwm_slice&&) = delete; + pwm_slice(channel_param auto ch, + pwm_slice_runtime::configuration const& cfg = {}) + : pwm_slice_runtime(ch(), cfg) + { + using enum internal::processor_type; + static_assert(internal::type == rp2040 || internal::type == rp2350, + "Update PWM channels for new RP!"); + static_assert(ch() < 8 || internal::pin_max != 30, + "PWM channel is invalid!"); + static_assert(ch() < 11 || internal::pin_max != 48, + "PWM channel is invalid!"); + static_assert(ch() < max_slices(), "Invalid PWM slice!"); + } + + pwm_pin get_pin(pin_param auto pin, pwm_pin_configuration const& config = {}) + { + static_assert(get_slice_number(pin) == chan, "Slice pin is incorrect!"); + return get_pin_raw(pin(), config); + } + + static constexpr u8 max_slices() + { + using enum hal::rp::internal::processor_type; + if constexpr (hal::rp::internal::type == rp2040) { + return 8; + } else if constexpr (hal::rp::internal::type == rp2350) { + return 12; + } + static_assert("Unknown RP type"); + } + + static constexpr u8 get_slice_number(pin_param auto pin) + { + if (pin() < 32) { + return (pin() / 2) % 8; + } else { + return 8 + ((pin() >> 1) & 3); + } + } +}; +template +pwm_slice(p pin) -> pwm_slice; +} // namespace v5 +} // namespace hal::rp diff --git a/include/libhal-arm-mcu/rp/rp.hpp b/include/libhal-arm-mcu/rp/rp.hpp new file mode 100644 index 0000000..f80d113 --- /dev/null +++ b/include/libhal-arm-mcu/rp/rp.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include +#include + +namespace hal::rp { + +namespace internal { + +#if defined(LIBHAL_PLATFORM_RP2040) || defined(LIBHAL_VARIANT_RP2350A) +constexpr u8 pin_max = 30; +#elif defined(LIBHAL_VARIANT_RP2350B) +constexpr u8 pin_max = 48; +#else +#error "Unknown pico variant" +#endif + +enum struct processor_type : uint8_t +{ + rp2040, + rp2350 +}; + +#if defined(LIBHAL_PLATFORM_RP2040) +constexpr inline processor_type type = type::rp2040; +#elif defined(LIBHAL_PLATFORM_RP2350_ARM_S) +constexpr inline processor_type type = processor_type::rp2350; +#else +#error "Unknown Pico model" +#endif + +} // namespace internal +template +concept pin_param = + std::is_same_v, T> && (T::val < internal::pin_max); +} // namespace hal::rp diff --git a/include/libhal-arm-mcu/rp/serial.hpp b/include/libhal-arm-mcu/rp/serial.hpp new file mode 100644 index 0000000..ed815c7 --- /dev/null +++ b/include/libhal-arm-mcu/rp/serial.hpp @@ -0,0 +1,68 @@ +#pragma once + +#include "rp.hpp" +#include + +namespace hal::rp::inline v4 { +/* +The RP series chips use a native ROM USB bootloader, +meaning most setups use USB directly, necessitating +using TinyUSB serial instead of a serial-USB converter +This means that DTR has no effect, and also web serial behaves +very weirdly + +The stdio serial actually wraps stdio, and may or may not +buffer or be asynchronous. Do not depend on the asynchronality +of this class as it is not guaranteed. +*/ +struct stdio_serial final : public hal::serial +{ + stdio_serial(); + stdio_serial(stdio_serial&&) = delete; + +private: + // This function is a sham that does nothing + void driver_configure(settings const&) override; + write_t driver_write(std::span) override; + // The stdio interface doesn't actually provide us with how many + // bytes are left. It's not too hard to implement an actual buffer + // system, but honestly most people aren't going to use this anyways + read_t driver_read(std::span) override; + void driver_flush() override; +}; + +// RP chips support CTS and RTS, but libhal has no support for them currently +// This is not really intended for debug purposes, and as such is blocking +// and has no internal buffering. +struct uart final : public hal::serial +{ + + uart(bus_param auto bus, + pin_param auto tx, + pin_param auto rx, + settings const& options = {}) + : uart(bus(), tx(), rx(), options) + { + static_assert(bus() == 0 || bus() == 1, "Invalid UART bus selected!"); + static_assert(tx() % 4 == 0, "UART TX pin is invalid!"); + static_assert(rx() % 4 == 1, "UART TX pin is invalid!"); + static_assert(((tx() + 4) / 8) % 2 == bus(), + "UART TX pin and bus do not match!"); + static_assert(((rx() + 4) / 8) % 2 == bus(), + "UART RX pin and bus do not match!"); + } + uart(uart&&) = delete; + ~uart() override; + +private: + uart(u8 bus, u8 tx, u8 rx, settings const&); + void driver_configure(settings const&) override; + write_t driver_write(std::span) override; + // available bytes is always read as 0 or 1, since there's no actual way + // to read number of bytes in the fifo + read_t driver_read(std::span) override; + void driver_flush() override; + u8 m_tx, m_rx, m_bus; +}; + +} // namespace hal::rp::inline v4 diff --git a/include/libhal-arm-mcu/rp/spi.hpp b/include/libhal-arm-mcu/rp/spi.hpp new file mode 100644 index 0000000..dbf4c34 --- /dev/null +++ b/include/libhal-arm-mcu/rp/spi.hpp @@ -0,0 +1,154 @@ +#pragma once + +#include "rp.hpp" +#include +#include +#include +#include + +namespace hal::rp { +constexpr u8 bus_from_tx_pin(u8 tx) +{ + return (tx / 8) % 2; +} + +inline namespace v4 { +// Backfill because much of libhal will stay on v4 for the time being +struct spi final : public hal::spi +{ + spi(pin_param auto copi, + pin_param auto cipo, + pin_param auto sck, + spi::settings const& options = {}) + : spi(bus_from_tx_pin(copi()), copi(), cipo(), sck(), options) + { + static_assert(cipo() % 4 == 0, "SPI RX pin is invalid"); + static_assert(sck() % 4 == 2, "SPI SCK pin is invalid"); + static_assert(copi() % 4 == 3, "SPI CS pin is invalid"); + } + spi(spi&&) = delete; + ~spi() override; + +private: + spi(u8 bus, u8 copi, u8 cipo, u8 sck, spi::settings const&); + void driver_configure(settings const&) override; + + void driver_transfer(std::span out, + std::span in, + byte) override; + + u8 m_bus, m_tx, m_rx, m_sck; +}; +} // namespace v4 +namespace v5 { +struct spi_channel; +template +struct spi_bus; +template<> +struct spi_bus +{ + spi_bus(bus_param auto bus, + pin_param auto copi, + pin_param auto cipo, + pin_param auto sck) + : spi_bus(bus(), copi(), cipo(), sck()) + { + static_assert(bus() == bus_from_tx_pin(copi()), + "Bus parameter does not match pins"); + static_assert(cipo() % 4 == 0, "SPI RX pin is invalid"); + static_assert(sck() % 4 == 2, "SPI SCK pin is invalid"); + static_assert(copi() % 4 == 3, "SPI CS pin is invalid"); + } + ~spi_bus(); + + /* Acquires a device assciated with a certain pin. Due to usage of software + * CS, a steady clock is necessary to wait the prerequesite time since SPI + * Peripheral unsets itself a little early */ + spi_channel acquire_device(pin_param auto pin, + hal::steady_clock&, + hal::v5::spi_channel::settings const& settings); + +protected: + spi_channel acquire_device(u8 pin, + hal::pollable_lock* lock, + hal::steady_clock&, + hal::v5::spi_channel::settings const& settings); + spi_bus(u8 bus, u8 tx, u8 rx, u8 sck); + u8 m_bus, m_tx, m_rx, m_sck; +}; + +template +struct spi_bus : spi_bus +{ + template + spi_bus(bus_param auto bus, + pin_param auto copi, + pin_param auto cipo, + pin_param auto sck, + Ts... args) + : spi_bus(bus, copi, cipo, sck) + , m_lock(std::forward(args)...) + { + } + + spi_channel acquire_device(pin_param auto pin, + hal::steady_clock&, + hal::v5::spi_channel::settings const& settings); + + Lock m_lock; +}; + +/* +RP chips suppport 16 bit transfers. It may be worthwhile to add an option +to transfer 16 bits as a time. TODO fix to add spi channel manager +*/ +struct spi_channel final : public hal::spi_channel +{ + friend spi_bus; + ~spi_channel() override; + +private: + spi_channel(u8 bus, + u8 cs, + hal::pollable_lock*, + hal::steady_clock*, + settings const&); + void driver_configure(settings const&) override; + u32 driver_clock_rate() override; + void driver_chip_select(bool p_select) override; + + void driver_transfer(std::span out, + std::span in, + byte) override; + hal::steady_clock* m_clk; + hal::pollable_lock* m_lock; + u8 m_bus, m_cs; +}; + +inline spi_channel spi_bus::acquire_device( + pin_param auto pin, + hal::steady_clock& clk, + hal::v5::spi_channel::settings const& s) +{ + return acquire_device(pin(), nullptr, clk, s); +} +template +spi_channel spi_bus::acquire_device( + pin_param auto pin, + hal::steady_clock& clk, + hal::v5::spi_channel::settings const& s) +{ + return acquire_device(pin(), &m_lock, clk, s); +} + +inline spi_channel spi_bus::acquire_device( + u8 pin, + hal::pollable_lock* lock, + hal::steady_clock& clk, + hal::v5::spi_channel::settings const& settings) +{ + return { m_bus, pin, lock, &clk, settings }; +} + +} // namespace v5 +} // namespace hal::rp diff --git a/include/libhal-arm-mcu/rp/time.hpp b/include/libhal-arm-mcu/rp/time.hpp new file mode 100644 index 0000000..cc31b94 --- /dev/null +++ b/include/libhal-arm-mcu/rp/time.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include +#include + +namespace hal::rp::inline v4 { + +// This has to be the least interesting clock ever +struct clock final : public hal::steady_clock +{ + clock() = default; + clock(clock const&) = default; + hertz driver_frequency() override; + u64 driver_uptime() override; +}; + +// returns configured clock speed for use with dwt_counter +hertz core_clock(); + +using microseconds = std::chrono::duration; + +void sleep(std::chrono::duration time) noexcept; + +} // namespace hal::rp::inline v4 diff --git a/src/rp/adc.cpp b/src/rp/adc.cpp new file mode 100644 index 0000000..7ca12b0 --- /dev/null +++ b/src/rp/adc.cpp @@ -0,0 +1,182 @@ +#include "libhal-arm-mcu/rp/adc.hpp" +#include "libhal-arm-mcu/rp/rp.hpp" +#include "libhal-arm-mcu/rp/time.hpp" + +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace hal::rp { + +inline namespace v4 { +adc::adc(u8 pin) + : m_pin(pin) +{ + if (pin < ADC_BASE_PIN || pin >= ADC_BASE_PIN + NUM_ADC_CHANNELS - 1) { + hal::safe_throw( + hal::argument_out_of_domain(reinterpret_cast(pin))); // NOLINT + } + adc_init(); + adc_gpio_init(pin); +} + +adc::~adc() +{ + gpio_deinit(m_pin); +} + +float adc::driver_read() +{ + adc_select_input(m_pin - ADC_BASE_PIN); + u16 result = adc_read(); + // weirdly enough the sdk doesn't provide a function to read the error bits + if (adc_hw->cs & ADC_CS_ERR_BITS) { + hal::safe_throw(hal::io_error(this)); + } + + return float(result) / float((1 << 12) - 1); +} +}; // namespace v4 + +namespace v5 { + +adc16::adc16(u8 pin) + : m_pin(pin) +{ + adc_init(); + if (pin < ADC_BASE_PIN || pin >= ADC_BASE_PIN + NUM_ADC_CHANNELS - 1) { + hal::safe_throw( + hal::argument_out_of_domain(reinterpret_cast(pin))); // NOLINT + } + adc_gpio_init(pin); +} + +adc16::~adc16() +{ + gpio_deinit(m_pin); +} + +u16 adc16::driver_read() +{ + adc_select_input(m_pin - ADC_BASE_PIN); + u16 result = adc_read(); + // weirdly enough the sdk doesn't provide a function to read the error bits + if (adc_hw->cs & ADC_CS_ERR_BITS) { + hal::safe_throw(hal::io_error(this)); + } + // TODO: Use hal::upscale to actually make this 16 bit + // for some reason the docs reference a function that + // doesn't exist yet + return result; +} + +} // namespace v5 + +namespace nonstandard { +adc16_pack::adc16_pack(u8 mask) +{ + adc_init(); + u32 base_pin = 0; + if constexpr (internal::pin_max == 30) { + base_pin = 26; + } else if constexpr (internal::pin_max == 48) { + base_pin = 40; + } + static_assert(internal::pin_max == 30 || internal::pin_max == 48); + u8 pinnum = 0; + bool first_selected = false; + for (u32 i = 0; i < 8; ++i) { + if (mask & (1 << i)) { + adc_gpio_init(i + base_pin); + pinnum += 1; + if (!first_selected) { + adc_select_input(i); + m_first_pin = i; + first_selected = true; + } + } + } + adc_set_round_robin(mask); + m_read_size = pinnum; +} + +void adc16_pack::read_many_now(std::span s) +{ + if (s.size() < m_read_size) + safe_throw(hal::io_error(this)); + for (u8 i = 0; i < m_read_size; ++i) { + s[i] = adc_read(); + } +} + +adc16_pack::read_session adc16_pack::async() +{ + int read_dma = dma_claim_unused_channel(false); + if (read_dma == -1) { + hal::safe_throw(hal::device_or_resource_busy(this)); + } + adc_fifo_setup(false, // do not enable FIFO yet + true, // Enable DMA data request (DREQ) + 1, // DREQ (and IRQ) asserted when at least 1 sample present + true, // Enable ADC errors + false // Shift each sample to 8 bits when pushing to FIFO + ); + + dma_channel_config cfg = dma_channel_get_default_config(read_dma); + channel_config_set_transfer_data_size(&cfg, DMA_SIZE_16); + channel_config_set_read_increment(&cfg, false); + channel_config_set_write_increment(&cfg, true); + channel_config_set_dreq(&cfg, DREQ_ADC); + dma_channel_set_config(read_dma, &cfg, false); + return { static_cast(read_dma), m_read_size, m_first_pin }; +} +adc16_pack::read_session::~read_session() +{ + adc_run(false); + adc_hw->fcs &= ADC_FCS_DREQ_EN_BITS; + dma_channel_config_t read_cfg = dma_get_channel_config(m_dma); + channel_config_set_enable(&read_cfg, false); + dma_channel_set_config(m_dma, &read_cfg, false); + dma_channel_unclaim(m_dma); +} + +adc16_pack::read_session::promise adc16_pack::read_session::read( + std::span span) +{ + auto chan = dma_get_channel_config(m_dma); + dma_channel_configure(m_dma, + &chan, + span.data(), // dst + &adc_hw->fifo, // src + span.size(), // transfer count + true // start immediately + ); + adc_run(false); + adc_fifo_drain(); + adc_select_input(m_first_pin); + adc_hw->fcs |= bool_to_bit(true) << ADC_FCS_EN_LSB; + adc_run(true); + return promise{ m_dma, m_first_pin }; +} + +microseconds adc16_pack::read_session::promise::poll() +{ + u32 transfers = (dma_channel_hw_addr(m_dma)->transfer_count & + DMA_CH0_TRANS_COUNT_COUNT_BITS) >> + DMA_CH0_TRANS_COUNT_COUNT_LSB; + if (transfers == 0) { + adc_hw->fcs &= ~ADC_FCS_EN_BITS; + adc_run(false); + } + return transfers * microseconds(2); +} + +} // namespace nonstandard + +} // namespace hal::rp diff --git a/src/rp/gpio.cpp b/src/rp/gpio.cpp new file mode 100644 index 0000000..85e8ae5 --- /dev/null +++ b/src/rp/gpio.cpp @@ -0,0 +1,279 @@ +#include "libhal-arm-mcu/rp/input_pin.hpp" +#include "libhal-arm-mcu/rp/interrupt_pin.hpp" +#include "libhal-arm-mcu/rp/output_pin.hpp" +#include "libhal-arm-mcu/rp/rp.hpp" +#include "libhal/error.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { +struct interrupt_manager +{ + + struct lock + { + + interrupt_manager* operator->() + { + return m_ptr; + } + + interrupt_manager& operator*() + { + return *m_ptr; + } + + // locks are uncopiable + lock(lock const&) = delete; + // locks are immovable to prevent moved-from states + lock(lock&&) = delete; + + ~lock() + { + restore_interrupts_from_disabled(m_status); + } + + private: + interrupt_manager* m_ptr; + uint32_t m_status; + lock(interrupt_manager* m) + : m_ptr(m) + , m_status(save_and_disable_interrupts()) + { + } + friend struct interrupt_manager; + }; + + struct interrupt + { + hal::interrupt_pin::trigger_edge edge; + hal::callback callback; + }; + + static lock get() + { + if (!global) { + global = new interrupt_manager(); + gpio_set_irq_callback(&irq); + } + return { global }; + } + + void insert(hal::u8 pin, interrupt handler) + { + m_interrupts[pin] = std::move(handler); + } + + interrupt& at(hal::u8 pin) + { + return m_interrupts[pin].value(); + } + + void remove(hal::u8 pin) + { + m_interrupts[pin].reset(); + } + + // I don't control the callback types clang-tidy + static void irq(uint gpio, std::uint32_t event) // NOLINT + { + using enum hal::interrupt_pin::trigger_edge; + auto g = get(); + + // If there is no handler then we don't do anything + if (!g->m_interrupts[gpio].has_value()) { + return; + } + + auto& handler = *g->m_interrupts[gpio]; + + bool should_activate = false; + switch (handler.edge) { + case rising: + should_activate = event & GPIO_IRQ_EDGE_RISE; + break; + case falling: + should_activate = event & GPIO_IRQ_EDGE_FALL; + break; + case both: + should_activate = event & (GPIO_IRQ_EDGE_RISE | GPIO_IRQ_EDGE_FALL); + break; + } + if (should_activate) { + bool value = gpio_get(gpio); + handler.callback(value); + } + } + +private: + // yes I could save 8 * 48 bytes by doing union stuff instead of + // std::optional no I'm too lazy to do that + std::array, hal::rp::internal::pin_max> m_interrupts; + interrupt_manager() = default; + ~interrupt_manager() = default; + // todo check if this should be thread_local + static inline interrupt_manager* global = nullptr; +}; + +} // namespace + +namespace hal::rp::inline v4 { +void sleep_ms(uint32_t ms) +{ + ::sleep_ms(ms); +} + +input_pin::input_pin(u8 pin, settings const& options) + : m_pin(pin) +{ + if (pin >= NUM_BANK0_GPIOS) { + hal::safe_throw(hal::argument_out_of_domain(this)); + } + + gpio_init(pin); + gpio_set_function(pin, gpio_function_t::GPIO_FUNC_SIO); + gpio_set_dir(pin, GPIO_IN); + + driver_configure(options); +} + +input_pin::~input_pin() +{ + gpio_deinit(m_pin); +} + +void input_pin::driver_configure(settings const& p_settings) +{ + switch (p_settings.resistor) { + case pin_resistor::pull_down: + gpio_pull_down(m_pin); + break; + case pin_resistor::pull_up: + gpio_pull_up(m_pin); + break; + default: + [[fallthrough]]; + case pin_resistor::none: + gpio_disable_pulls(m_pin); + break; + } +} + +bool input_pin::driver_level() +{ + return gpio_get(m_pin); +} + +output_pin::output_pin(u8 pin, settings const& options) + : m_pin(pin) +{ + if (pin >= NUM_BANK0_GPIOS) { + hal::safe_throw(hal::argument_out_of_domain(this)); + } + + gpio_init(pin); + gpio_set_function(pin, gpio_function_t::GPIO_FUNC_SIO); + gpio_set_dir(pin, GPIO_OUT); + + driver_configure(options); +} + +output_pin::~output_pin() +{ + gpio_deinit(m_pin); +} + +void output_pin::driver_configure(settings const& options) +{ + // RP2* series chips don't seem to have any explicit support for + // open drain mode, so we fail loud rather than silently + if (options.open_drain) { + hal::safe_throw(hal::operation_not_supported(this)); + } + + switch (options.resistor) { + case pin_resistor::pull_down: + gpio_pull_down(m_pin); + break; + case pin_resistor::pull_up: + gpio_pull_up(m_pin); + break; + default: + [[fallthrough]]; + case pin_resistor::none: + gpio_disable_pulls(m_pin); + break; + } +} + +void output_pin::driver_level(bool level) +{ + gpio_put(m_pin, level); +} + +bool output_pin::driver_level() +{ + return gpio_get(m_pin); +} + +interrupt_pin::interrupt_pin(u8 pin, + hal::callback callback, + settings const& options) + : m_pin(pin) +{ + gpio_init(pin); + gpio_set_dir(pin, false); + gpio_set_function(pin, gpio_function_t::GPIO_FUNC_SIO); + { + auto g = interrupt_manager::get(); + g->insert(m_pin, + { .edge = options.trigger, .callback = std::move(callback) }); + // drop the lock + } + driver_configure(options); +} + +interrupt_pin::~interrupt_pin() +{ + auto g = interrupt_manager::get(); + g->remove(m_pin); +} + +void interrupt_pin::driver_configure(settings const& options) +{ + auto g = interrupt_manager::get(); + switch (options.resistor) { + case pin_resistor::none: + gpio_disable_pulls(m_pin); + break; + case pin_resistor::pull_up: + gpio_pull_up(m_pin); + break; + // pulldown seems reasonable, although this should never trigger anyways + default: + [[fallthrough]]; + case pin_resistor::pull_down: + gpio_pull_down(m_pin); + break; + } + g->at(m_pin).edge = options.trigger; +} + +void interrupt_pin::driver_on_trigger(hal::callback callback) +{ + auto g = interrupt_manager::get(); + std::swap(g->at(m_pin).callback, callback); +} + +} // namespace hal::rp::inline v4 diff --git a/src/rp/i2c.cpp b/src/rp/i2c.cpp new file mode 100644 index 0000000..4f77daa --- /dev/null +++ b/src/rp/i2c.cpp @@ -0,0 +1,80 @@ +#include "libhal-arm-mcu/rp/i2c.hpp" + +#include +#include +#include +#include +#include + +// pico macros interfere with ours +#undef i2c0 +#undef i2c1 + +namespace { +auto inst(hal::u8 c, void* instance) +{ + switch (c) { + case 0: + return &i2c0_inst; + case 1: + return &i2c1_inst; + default: + // realistically should never happen + hal::safe_throw(hal::io_error(instance)); + } +} +} // namespace + +namespace hal::rp::inline v4 { +i2c::i2c(u8 sda, u8 scl, u8 chan, settings const& options) // NOLINT + : m_sda(sda) + , m_scl(scl) + , m_chan(chan) +{ + i2c_init(inst(chan, this), static_cast(options.clock_rate)); + gpio_pull_up(sda); + gpio_pull_up(scl); + gpio_set_function(sda, gpio_function_t::GPIO_FUNC_I2C); + gpio_set_function(scl, gpio_function_t::GPIO_FUNC_I2C); +} + +i2c::~i2c() +{ + i2c_deinit(inst(m_chan, this)); + gpio_deinit(m_sda); + gpio_deinit(m_scl); +} + +void i2c::driver_configure(settings const& options) +{ + i2c_set_baudrate(inst(m_chan, this), static_cast(options.clock_rate)); +} + +void i2c::driver_transaction(hal::byte addr, + std::span out, + std::span in, + hal::function_ref timeout) +{ + bool is_read = in.size() != 0; + int write_result = i2c_write_timeout_us( + inst(m_chan, this), addr, out.data(), out.size_bytes(), is_read, 5000); + if (write_result == PICO_ERROR_GENERIC) { + safe_throw(no_such_device(addr, this)); + } + if (write_result == PICO_ERROR_TIMEOUT) { + safe_throw(timed_out(this)); + } + if (is_read) { + int read_result = i2c_read_timeout_us( + inst(m_chan, this), addr, in.data(), in.size_bytes(), false, 5000); + if (read_result == PICO_ERROR_GENERIC) { + safe_throw(no_such_device(addr, this)); + } + if (read_result == PICO_ERROR_TIMEOUT) { + safe_throw(timed_out(this)); + } + } + timeout(); +} + +} // namespace hal::rp::inline v4 diff --git a/src/rp/pwm.cpp b/src/rp/pwm.cpp new file mode 100644 index 0000000..16c9ba2 --- /dev/null +++ b/src/rp/pwm.cpp @@ -0,0 +1,208 @@ +#include "libhal-arm-mcu/rp/pwm.hpp" + +#include +#include +#include +#include +#include +#include + +/* +TODO: Specify SYS_CLK_HZ in a board header so that pico actually knows what +frequency it should run at + +*/ + +namespace hal::rp { +inline namespace v4 { +pwm_pin::pwm_pin(hal::unsafe, u8 pin) + : m_pin(pin) +{ + gpio_set_function(m_pin, GPIO_FUNC_PWM); + auto config = pwm_get_default_config(); + pwm_init(pwm_gpio_to_slice_num(m_pin), &config, false); +} + +pwm_pin::~pwm_pin() +{ + gpio_deinit(m_pin); +} + +void pwm_pin::driver_duty_cycle(float duty) +{ + uint channel = pwm_gpio_to_channel(m_pin); + uint slice = pwm_gpio_to_slice_num(m_pin); + if (duty == 0) { + pwm_set_chan_level(slice, channel, 0); + return; + } + + float percentage = float(duty) / std::numeric_limits::max(); + u16 top = pwm_hw->slice[slice].top; + pwm_set_chan_level(slice, channel, u16(float(top) * percentage)); + pwm_set_enabled(slice, true); +} + +void pwm_pin::driver_frequency(float f) +{ + if (f > SYS_CLK_HZ) { + hal::safe_throw(argument_out_of_domain(this)); + } + + auto frequency = static_cast(f); + auto clock = static_cast(SYS_CLK_HZ); + // We try to adjust clock divider to maximize counter resolution + float wrap_val = std::numeric_limits::max(); + float clock_div = clock / frequency / wrap_val; + // cap the clock divider at 256 + if (clock_div > 256) { + clock_div = 256; + wrap_val = clock / frequency / 256; + } + // maintain a minimum clock divider of 1 + if (clock_div < 1.0) { + clock_div = 1; + wrap_val = clock / frequency; + } + + // frequency too low + if (wrap_val > std::numeric_limits::max()) { + hal::safe_throw(argument_out_of_domain(this)); + } + + uint slice = pwm_gpio_to_slice_num(m_pin); + pwm_set_wrap(slice, static_cast(wrap_val)); + pwm_set_clkdiv(slice, clock_div); +} +} // namespace v4 + +namespace v5 { + +pwm_slice_runtime::pwm_slice_runtime(u8 num, configuration const& cfg) + : m_number(num) + , m_phase_correct(cfg.phase_correct) +{ + if (num >= NUM_PWM_SLICES) { + hal::safe_throw(argument_out_of_domain(this)); + } + auto config = pwm_get_default_config(); + pwm_init(num, &config, false); +} + +pwm_slice_runtime::~pwm_slice_runtime() +{ + pwm_set_enabled(m_number, false); +} + +void pwm_slice_runtime::driver_frequency(u32 f) +{ + // frequency too high + if (f > SYS_CLK_HZ) { + hal::safe_throw(argument_out_of_domain(this)); + } + + auto frequency = static_cast(f); + // phase correct mode halfs output frequency, so we need to double it to + // compensate + if (m_phase_correct) + frequency *= 2; + auto clock = static_cast(SYS_CLK_HZ); + // We try to adjust clock divider to maximize counter resolution + float wrap_val = std::numeric_limits::max(); + float clock_div = clock / frequency / wrap_val; + // cap the clock divider at 256 + if (clock_div > 256) { + clock_div = 256; + wrap_val = clock / frequency / 256; + } + // maintain a minimum clock divider of 1 + if (clock_div < 1.0) { + clock_div = 1; + wrap_val = clock / frequency; + } + + // frequency too low + if (wrap_val > std::numeric_limits::max()) { + hal::safe_throw(argument_out_of_domain(this)); + } + + pwm_set_wrap(m_number, static_cast(wrap_val)); + pwm_set_clkdiv(m_number, clock_div); +} + +pwm_pin pwm_slice_runtime::get_pin_raw(u8 pin, pwm_pin_configuration const& c) +{ + return pwm_pin(pin, c, {}); +} + +pwm_pin::pwm_pin(u8 pin, pwm_pin_configuration const& c, hal::unsafe) + : m_pin(pin) + , m_slice(pwm_gpio_to_slice_num(pin)) + , m_autostart(c.autostart) +{ + gpio_set_function(m_pin, GPIO_FUNC_PWM); + driver_duty_cycle(c.duty_cycle); +} + +pwm_pin::~pwm_pin() +{ + gpio_deinit(m_pin); +} + +void pwm_pin::driver_duty_cycle(u16 duty) +{ + auto channel = pwm_gpio_to_channel(m_pin); + if (duty == 0) { + pwm_set_chan_level(m_slice, channel, 0); + return; + } + + float percentage = float(duty) / std::numeric_limits::max(); + u16 top = pwm_hw->slice[m_slice].top; + pwm_set_chan_level(m_slice, channel, u16(float(top) * percentage)); + if (m_autostart) { + pwm_set_enabled(m_slice, true); + } +} + +void enable_all_pwm(bool start) +{ + // Literally no idea if this is required, but let's be cautious and only + // enable slices that exist + uint32_t enable_mask; + if constexpr (rp::internal::type == rp::internal::processor_type::rp2040) { + enable_mask = 0b1111'1111; + } else if constexpr (rp::internal::type == + rp::internal::processor_type::rp2350) { + enable_mask = 0b1111'1111'1111; + } + + if (start) { + pwm_set_mask_enabled(enable_mask); + } else { + pwm_set_mask_enabled(0x0); + } +} + +void pwm_slice_runtime::enable(bool enable) +{ + pwm_set_enabled(m_number, enable); +} + +u32 pwm_pin::driver_frequency() +{ + uint8_t num = + hal::bit_extract<{ .position = PWM_CH0_DIV_INT_LSB, .width = 8 }>( + pwm_hw->slice[m_slice].div); + uint8_t den = + hal::bit_extract<{ .position = PWM_CH0_DIV_FRAC_LSB, .width = 4 }>( + pwm_hw->slice[m_slice].div); + + float divider = float(num) / float(den); + u16 top = pwm_hw->slice[m_slice].top; + + return static_cast(static_cast(top) * divider); +} + +} // namespace v5 +} // namespace hal::rp diff --git a/src/rp/serial.cpp b/src/rp/serial.cpp new file mode 100644 index 0000000..a9b9c0e --- /dev/null +++ b/src/rp/serial.cpp @@ -0,0 +1,130 @@ +#include "libhal-arm-mcu/rp/serial.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { +auto get_uart(hal::u8 bus) +{ + switch (bus) { + case 0: + return uart0; + case 1: + return uart1; + default: + hal::safe_throw(hal::io_error(nullptr)); + } +} +} // namespace + +namespace hal::rp::inline v4 { +stdio_serial::stdio_serial() +{ + stdio_init_all(); +} + +void stdio_serial::driver_configure(settings const&) +{ +} + +void stdio_serial::driver_flush() +{ + stdio_flush(); +} + +serial::write_t stdio_serial::driver_write(std::span in) +{ + int write_length = stdio_put_string(reinterpret_cast(in.data()), + static_cast(in.size_bytes()), + false, + false); + return write_t{ in.subspan(0, write_length) }; +} + +serial::read_t stdio_serial::driver_read(std::span output) +{ + int len = stdio_get_until(reinterpret_cast(output.data()), + static_cast(output.size_bytes()), + 0); + if (len == PICO_ERROR_TIMEOUT) + len = 0; + if (len < 0) + hal::safe_throw(hal::io_error(this)); + return read_t{ .data = output.subspan(0, len), + .available = 0, + .capacity = 1 }; +} + +uart::uart(u8 bus, u8 tx, u8 rx, settings const& options) // NOLINT + : m_tx(tx) + , m_rx(rx) + , m_bus(bus) +{ + + auto func = (bus == 0) ? GPIO_FUNC_UART : GPIO_FUNC_UART_AUX; + + driver_configure(options); + gpio_set_function(tx, func); + gpio_set_function(rx, func); +} + +uart::~uart() +{ + gpio_deinit(m_tx); + gpio_deinit(m_rx); + uart_deinit(get_uart(m_bus)); +} + +void uart::driver_configure(settings const& options) +{ + uint stop = 1; + switch (options.stop) { + case serial::settings::stop_bits::one: + stop = 1; + break; + case serial::settings::stop_bits::two: + stop = 2; + break; + } + uart_parity_t parity = UART_PARITY_NONE; + switch (options.parity) { + case serial::settings::parity::none: + parity = UART_PARITY_NONE; + break; + case serial::settings::parity::odd: + parity = UART_PARITY_ODD; + break; + case serial::settings::parity::even: + parity = UART_PARITY_EVEN; + break; + case serial::settings::parity::forced1: + case serial::settings::parity::forced0: + hal::safe_throw(operation_not_supported(this)); + } + + uart_init(get_uart(m_bus), uint(options.baud_rate)); + uart_set_format(get_uart(m_bus), 8, stop, parity); +} + +serial::write_t uart::driver_write(std::span in) +{ + uart_write_blocking(get_uart(m_bus), in.data(), in.size_bytes()); + // always writes everything + return { in }; +} + +serial::read_t uart::driver_read(std::span out) +{ + uart_read_blocking(get_uart(m_bus), out.data(), out.size_bytes()); + return { .data = out, + .available = uart_is_readable(get_uart(m_bus)), + .capacity = 32 }; +} + +} // namespace hal::rp::inline v4 diff --git a/src/rp/spi.cpp b/src/rp/spi.cpp new file mode 100644 index 0000000..9af4cbe --- /dev/null +++ b/src/rp/spi.cpp @@ -0,0 +1,193 @@ +#include "libhal-arm-mcu/rp/spi.hpp" + +#include +#include +#include +#include +#include +#include + +namespace { +auto get_bus(hal::u8 busnum) +{ + switch (busnum) { + case 0: + return spi0; + case 1: + return spi1; + default: + hal::safe_throw(hal::io_error(nullptr)); + } +} +} // namespace +namespace hal::rp { + +inline namespace v4 { +// TODO shuddup clang-tidy I'll fix it later +spi::spi(u8 bus, u8 tx, u8 rx, u8 sck, spi::settings const& s) // NOLINT + : m_bus(bus) + , m_tx(tx) + , m_rx(rx) + , m_sck(sck) +{ + driver_configure(s); + gpio_set_function(tx, GPIO_FUNC_SPI); + gpio_set_function(rx, GPIO_FUNC_SPI); + gpio_set_function(sck, GPIO_FUNC_SPI); +} + +void spi::driver_configure(spi::settings const& s) +{ + spi_cpol_t polarity = s.clock_polarity ? SPI_CPOL_1 : SPI_CPOL_0; + spi_cpha_t phase = s.clock_phase ? SPI_CPHA_1 : SPI_CPHA_0; + + spi_init(get_bus(m_bus), static_cast(s.clock_rate)); + spi_set_format(get_bus(m_bus), 8, polarity, phase, SPI_MSB_FIRST); +} + +void spi::driver_transfer(std::span out, + std::span in, + byte filler) +{ + auto out_size = out.size_bytes(); + auto in_size = in.size_bytes(); + auto size = std::min(out_size, in_size); + spi_write_read_blocking(get_bus(m_bus), out.data(), in.data(), size); + if (out_size > in_size) { + spi_write_blocking( + get_bus(m_bus), out.data() + in_size, out_size - in_size); + } else if (in_size > out_size) { + spi_read_blocking( + get_bus(m_bus), filler, in.data() + out_size, in_size - out_size); + } + // Does this wreck performance? probably + // Is this a bad idea? probably + // Is it necessary? Absolutely. See below for implementation details + sleep_us(10); +} + +spi::~spi() +{ + gpio_deinit(m_tx); + gpio_deinit(m_rx); + gpio_deinit(m_sck); + spi_deinit(get_bus(m_bus)); +} + +} // namespace v4 + +namespace v5 { + +spi_bus::spi_bus(u8 bus, u8 tx, u8 rx, u8 sck) // NOLINT + : m_bus(bus) + , m_tx(tx) + , m_rx(rx) + , m_sck(sck) +{ + gpio_set_function(tx, GPIO_FUNC_SPI); + gpio_set_function(rx, GPIO_FUNC_SPI); + gpio_set_function(sck, GPIO_FUNC_SPI); +} + +spi_bus::~spi_bus() +{ + gpio_deinit(m_tx); + gpio_deinit(m_rx); + gpio_deinit(m_sck); + spi_deinit(get_bus(m_bus)); +} + +spi_channel::spi_channel(u8 bus, // NOLINT + u8 cs, + hal::pollable_lock* lock, + hal::steady_clock* clock, + settings const& s) + : m_clk(clock) + , m_lock(lock) + , m_bus(bus) + , m_cs(cs) +{ + gpio_init(cs); + gpio_set_dir(cs, GPIO_OUT); + driver_configure(s); +} + +void spi_channel::driver_configure(spi_channel::settings const& s) +{ + spi_cpol_t polarity = SPI_CPOL_0; + spi_cpha_t phase = SPI_CPHA_0; + switch (s.bus_mode) { + case spi_channel::mode::m0: + polarity = SPI_CPOL_0; + phase = SPI_CPHA_0; + break; + case spi_channel::mode::m1: + polarity = SPI_CPOL_0; + phase = SPI_CPHA_1; + break; + case spi_channel::mode::m2: + polarity = SPI_CPOL_1; + phase = SPI_CPHA_0; + break; + case spi_channel::mode::m3: + polarity = SPI_CPOL_1; + phase = SPI_CPHA_1; + break; + default: + break; + } + spi_init(get_bus(m_bus), s.clock_rate); + spi_set_format(get_bus(m_bus), 8, polarity, phase, SPI_MSB_FIRST); +} + +u32 spi_channel::driver_clock_rate() +{ + return spi_get_baudrate(get_bus(m_bus)); +} + +void spi_channel::driver_chip_select(bool sel) +{ + // https://github.com/raspberrypi/pico-examples/tree/master/spi + // The pico examples add NOPs explicitly, which seems to be to guard against + // the CS deasserting early. We wait explicitly, so that won't be necessary + using namespace std::chrono_literals; + if (sel) { + if (m_lock) + m_lock->lock(); + gpio_put(m_cs, false); + } else if (!sel) { + // Arm Primecell Synchronous Serial Port :tm: unsets the busy bit early, + // necessitating this + auto freq = this->driver_clock_rate(); + auto sleep_time = std::chrono::duration(1'000'000 / freq); + hal::delay(*m_clk, sleep_time); + gpio_put(m_cs, true); + + if (m_lock) + m_lock->unlock(); + } +} + +void spi_channel::driver_transfer(std::span out, + std::span in, + byte filler) +{ + auto out_size = out.size_bytes(); + auto in_size = in.size_bytes(); + auto size = std::min(out_size, in_size); + spi_write_read_blocking(get_bus(m_bus), out.data(), in.data(), size); + if (out_size > in_size) { + spi_write_blocking( + get_bus(m_bus), out.data() + in_size, out_size - in_size); + } else if (in_size > out_size) { + spi_read_blocking( + get_bus(m_bus), filler, in.data() + out_size, in_size - out_size); + } +} + +spi_channel::~spi_channel() +{ + gpio_deinit(m_cs); +} +} // namespace v5 +} // namespace hal::rp diff --git a/src/rp/time.cpp b/src/rp/time.cpp new file mode 100644 index 0000000..78e1f1b --- /dev/null +++ b/src/rp/time.cpp @@ -0,0 +1,30 @@ +#include "libhal-arm-mcu/rp/time.hpp" +#include + +#include +#include +#include + +namespace hal::rp::inline v4 { + +hertz clock::driver_frequency() +{ + return 1'000'000; +} + +u64 clock::driver_uptime() +{ + return time_us_64(); +} + +hertz core_clock() +{ + return SYS_CLK_HZ; +} + +void sleep(std::chrono::duration time) noexcept +{ + sleep_us(time.count()); +} + +} // namespace hal::rp::inline v4