diff --git a/Cargo.toml b/Cargo.toml index 382702aa..763da7d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ members = [ "boards/pimoroni-plasma-2040", "boards/pimoroni-servo2040", "boards/pimoroni-tiny2040", + "boards/pimoroni-tufty2040", "boards/rp-pico", "boards/seeeduino-xiao-rp2040", "boards/solderparty-rp2040-stamp", diff --git a/boards/pimoroni-tufty2040/CHANGELOG.md b/boards/pimoroni-tufty2040/CHANGELOG.md new file mode 100644 index 00000000..a4af7e83 --- /dev/null +++ b/boards/pimoroni-tufty2040/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## Unreleased diff --git a/boards/pimoroni-tufty2040/Cargo.toml b/boards/pimoroni-tufty2040/Cargo.toml new file mode 100644 index 00000000..fe0e6446 --- /dev/null +++ b/boards/pimoroni-tufty2040/Cargo.toml @@ -0,0 +1,54 @@ +[package] +name = "pimoroni-tufty2040" +version = "0.1.0" +authors = ["The rp-rs Developers"] +edition = "2018" +homepage = "https://github.com/rp-rs/rp-hal-boards/tree/main/boards/pimoroni-tufty2040" +description = "Board Support Package for the Pimoroni Tufty2040" +license = "MIT OR Apache-2.0" +repository = "https://github.com/rp-rs/rp-hal-boards.git" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +cortex-m.workspace = true +cortex-m-rt = { workspace = true, optional = true } +embedded-hal.workspace = true +fugit.workspace = true +rp2040-boot2 = { workspace = true, optional = true } +rp2040-hal.workspace = true +st7789.workspace = true +display-interface.workspace = true +embedded-graphics.workspace = true +pio.workspace = true +pio-proc.workspace = true + +[dev-dependencies] +panic-halt.workspace = true +nb.workspace = true + +[features] +# This is the set of features we enable by default +default = ["boot2", "rt", "critical-section-impl", "rom-func-cache", "rom-v2-intrinsics"] + +# critical section that is safe for multicore use +critical-section-impl = ["rp2040-hal/critical-section-impl"] + +# 2nd stage bootloaders for rp2040 +boot2 = ["rp2040-boot2"] + +# Minimal startup / runtime for Cortex-M microcontrollers +rt = ["cortex-m-rt", "rp2040-hal/rt"] + +# This enables a fix for USB errata 5: USB device fails to exit RESET state on busy USB bus. +# Only required for RP2040 B0 and RP2040 B1, but it also works for RP2040 B2 and above +rp2040-e5 = ["rp2040-hal/rp2040-e5"] + +# Memoize(cache) ROM function pointers on first use to improve performance +rom-func-cache = ["rp2040-hal/rom-func-cache"] + +# Disable automatic mapping of language features (like floating point math) to ROM functions +disable-intrinsics = ["rp2040-hal/disable-intrinsics"] + +# This enables ROM functions for f64 math that were not present in the earliest RP2040s +rom-v2-intrinsics = ["rp2040-hal/rom-v2-intrinsics"] diff --git a/boards/pimoroni-tufty2040/README.md b/boards/pimoroni-tufty2040/README.md new file mode 100644 index 00000000..5a8c29ee --- /dev/null +++ b/boards/pimoroni-tufty2040/README.md @@ -0,0 +1,96 @@ +# [pimoroni-tufty2040] - Board Support for the [Pimoroni Tufty2040] + +You should include this crate if you are writing code that you want to run on +a [Pimoroni Tufty2040] - a hackable, programmable badge with a LCD colour +display, powered by a Raspberry Pi RP2040. + +This crate includes the [rp2040-hal], but also configures each pin of the +RP2040 chip according to how it is connected up on the Tufty2040. + +[Pimoroni Tufty2040]: https://shop.pimoroni.com/products/tufty-2040/ +[pimoroni-tufty2040]: https://github.com/rp-rs/rp-hal-boards/tree/main/boards/pimoroni-tufty2040 +[rp2040-hal]: https://github.com/rp-rs/rp-hal/tree/main/rp2040-hal +[Raspberry Silicon RP2040]: https://www.raspberrypi.org/products/rp2040/ + +## Using + +To use this crate, your `Cargo.toml` file should contain: + +```toml +pimoroni_tufty2040 = "0.1.0" +``` + +In your program, you will need to call `pimoroni_tufty2040::Board::take().unwrap()` to create +a new `Boards` structure. This will set up all the GPIOs for any on-board +devices and configure common clocks. See the [examples](./examples) folder for more details. + +## Examples + +### General Instructions + +To compile an example, clone the _rp-hal-boards_ repository and run: + +```console +rp-hal-boards/boards/pimoroni-tufty2040 $ cargo build --release --example +``` + +You will get an ELF file called +`./target/thumbv6m-none-eabi/release/examples/`, where the `target` +folder is located at the top of the _rp-hal-boards_ repository checkout. Normally +you would also need to specify `--target=thumbv6m-none-eabi` but when +building examples from this git repository, that is set as the default. + +If you want to convert the ELF file to a UF2 and automatically copy it to the +USB drive exported by the RP2040 bootloader, simply boot your board into +bootloader mode and run: + +```console +rp-hal-boards/boards/pimoroni-tufty2040 $ cargo run --release --example +``` + +If you get an error about not being able to find `elf2uf2-rs`, try: + +```console +$ cargo install elf2uf2-rs +``` +then try repeating the `cargo run` command above. + +### [tufty_demo](./examples/tufty_demo) + +Flashes the Tufty2040's LED and draws a circle on the screen. + +## Contributing + +Contributions are what make the open source community such an amazing place to +be learn, inspire, and create. Any contributions you make are **greatly +appreciated**. + +The steps are: + +1. Fork the Project by clicking the 'Fork' button at the top of the page. +2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) +3. Make some changes to the code or documentation. +4. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) +5. Push to the Feature Branch (`git push origin feature/AmazingFeature`) +6. Create a [New Pull Request](https://github.com/rp-rs/rp-hal-boards/pulls) +7. An admin will review the Pull Request and discuss any changes that may be required. +8. Once everyone is happy, the Pull Request can be merged by an admin, and your work is part of our project! + +## Code of Conduct + +Contribution to this crate is organized under the terms of the [Rust Code of +Conduct][CoC], and the maintainer of this crate, the [rp-rs team], promises +to intervene to uphold that code of conduct. + +[CoC]: CODE_OF_CONDUCT.md +[rp-rs team]: https://github.com/orgs/rp-rs/teams/rp-rs + +## License + +The contents of this repository are dual-licensed under the _MIT OR Apache +2.0_ License. That means you can chose either the MIT licence or the +Apache-2.0 licence when you re-use this code. See `MIT` or `APACHE2.0` for more +information on each specific licence. + +Any submissions to this project (e.g. as Pull Requests) must be made available +under these terms. diff --git a/boards/pimoroni-tufty2040/examples/tufty_demo.rs b/boards/pimoroni-tufty2040/examples/tufty_demo.rs new file mode 100644 index 00000000..1f139750 --- /dev/null +++ b/boards/pimoroni-tufty2040/examples/tufty_demo.rs @@ -0,0 +1,152 @@ +//! # Tufty2040 Demo Example +//! +//! Draws a circle on the LCD screen and then blinks the user LED on the Tufty 2040. +//! +//! See the `Cargo.toml` file for Copyright and licence details. + +#![no_std] +#![no_main] + +use pimoroni_tufty2040 as tufty; + +// The macro for our start-up function +use tufty::entry; + +// GPIO traits +use embedded_hal::digital::v2::{OutputPin, PinState}; + +// Ensure we halt the program on panic (if we don't mention this crate it won't +// be linked) +use panic_halt as _; + +// A shorter alias for the Hardware Abstraction Layer, which provides +// higher-level drivers. +use tufty::hal; + +// A shorter alias for the Peripheral Access Crate, which provides low-level +// register access +use hal::pac; + +use hal::clocks::ClockSource; +use hal::gpio::{FunctionPio0, PullNone}; +use hal::Clock; +use hal::Timer; + +use tufty::DummyPin; + +// A few traits required for using the CountDown timer +use embedded_graphics::draw_target::DrawTarget; +use embedded_graphics::geometry::Point; +use embedded_graphics::pixelcolor::{Rgb565, RgbColor}; +use embedded_graphics::primitives::{Circle, Primitive, PrimitiveStyleBuilder}; +use embedded_graphics::Drawable; +use embedded_hal::timer::CountDown; +use fugit::ExtU32; +use st7789::ST7789; + +#[entry] +fn main() -> ! { + // Grab our singleton objects + let mut pac = pac::Peripherals::take().unwrap(); + let cp = pac::CorePeripherals::take().unwrap(); + + // Set up the watchdog driver - needed by the clock setup code + let mut watchdog = hal::Watchdog::new(pac.WATCHDOG); + + // Configure the clocks + // + // The default is to generate a 125 MHz system clock + let clocks = hal::clocks::init_clocks_and_plls( + tufty::XOSC_CRYSTAL_FREQ, + pac.XOSC, + pac.CLOCKS, + pac.PLL_SYS, + pac.PLL_USB, + &mut pac.RESETS, + &mut watchdog, + ) + .ok() + .unwrap(); + + // The single-cycle I/O block controls our GPIO pins + let sio = hal::Sio::new(pac.SIO); + + // Set the pins up according to their function on this particular board + let pins = tufty::Pins::new( + pac.IO_BANK0, + pac.PADS_BANK0, + sio.gpio_bank0, + &mut pac.RESETS, + ); + + // Configure the timer peripheral to be a CountDown timer for our blinky delay + let timer = Timer::new(pac.TIMER, &mut pac.RESETS, &clocks); + let mut delay_timer = timer.count_down(); + + let mut delay = cortex_m::delay::Delay::new(cp.SYST, clocks.system_clock.get_freq().to_Hz()); + + // Set the LED to be an output + let mut led_pin = pins.led.into_push_pull_output(); + + pins.lcd_backlight + .into_push_pull_output_in_state(PinState::High); + pins.lcd_rd.into_push_pull_output_in_state(PinState::High); + + let display_data = { + use hal::dma::DMAExt; + use hal::pio::PIOExt; + + let dma = pac.DMA.split(&mut pac.RESETS); + let (mut pio, sm0, _, _, _) = pac.PIO0.split(&mut pac.RESETS); + + let wr = pins.lcd_wr.reconfigure::(); + let d0 = pins.lcd_db0.reconfigure::(); + pins.lcd_db1.reconfigure::(); + pins.lcd_db2.reconfigure::(); + pins.lcd_db3.reconfigure::(); + pins.lcd_db4.reconfigure::(); + pins.lcd_db5.reconfigure::(); + pins.lcd_db6.reconfigure::(); + pins.lcd_db7.reconfigure::(); + + tufty::PioDataLines::new( + &mut pio, + clocks.system_clock.freq(), + wr.id(), + d0.id(), + sm0, + dma.ch0, + ) + }; + + let display_interface = tufty::ParallelDisplayInterface::new( + pins.lcd_cs.into_push_pull_output_in_state(PinState::High), + pins.lcd_dc.into_push_pull_output_in_state(PinState::High), + display_data, + ); + + let mut display = ST7789::new(display_interface, DummyPin, 240, 320); + display.init(&mut delay).unwrap(); + display.clear(Rgb565::BLUE).unwrap(); + + let style = PrimitiveStyleBuilder::default() + .fill_color(Rgb565::RED) + .build(); + Circle::new(Point::new(50, 50), 10) + .into_styled(style) + .draw(&mut display) + .unwrap(); + + // Blink the LED at 1 Hz + loop { + // LED on, and wait for 500ms + led_pin.set_high().unwrap(); + delay_timer.start(500.millis()); + let _ = nb::block!(delay_timer.wait()); + + // LED off, and wait for 500ms + led_pin.set_low().unwrap(); + delay_timer.start(500.millis()); + let _ = nb::block!(delay_timer.wait()); + } +} diff --git a/boards/pimoroni-tufty2040/src/lib.rs b/boards/pimoroni-tufty2040/src/lib.rs new file mode 100644 index 00000000..e8938a0a --- /dev/null +++ b/boards/pimoroni-tufty2040/src/lib.rs @@ -0,0 +1,359 @@ +#![no_std] + +pub extern crate rp2040_hal as hal; + +pub use hal::pac; + +use display_interface::{DataFormat, DisplayError, WriteOnlyDataCommand}; +use embedded_hal::digital::v2::OutputPin; +use fugit::HertzU32; +use hal::dma::{single_buffer, Channel, ChannelIndex, WriteTarget}; +use hal::gpio::PinId; +use hal::pio::{ + Buffers, PIOBuilder, PIOExt, PinDir, PinState, StateMachineIndex, Tx, UninitStateMachine, +}; +use pio_proc::pio_file; + +#[cfg(feature = "rt")] +pub use rp2040_hal::entry; + +/// The linker will place this boot block at the start of our program image. We +/// need this to help the ROM bootloader get our code up and running. +#[cfg(feature = "boot2")] +#[link_section = ".boot2"] +#[no_mangle] +#[used] +pub static BOOT2_FIRMWARE: [u8; 256] = rp2040_boot2::BOOT_LOADER_W25Q080; + +hal::bsp_pins!( + Gpio0 { + name: gpio0, + aliases: { + /// UART Function alias for pin [Pins::gpio0]. + FunctionUart, PullNone: UartTx + } + }, + Gpio1 { + name: gpio1, + aliases: { + /// UART Function alias for pin [Pins::gpio1]. + FunctionUart, PullNone: UartRx + } + }, + Gpio2 { name: lcd_backlight }, + Gpio3 { name: i2c_int }, + Gpio4 { + name: gpio4, + aliases: { + /// I2C Function alias for pin [Pins::gpio4]. + FunctionI2C, PullUp: I2cSda + } + }, + Gpio5 { + name: gpio5, + aliases: { + /// I2C Function alias for pin [Pins::gpio5]. + FunctionI2C, PullUp: I2cScl + } + }, + Gpio6 { name: sw_down }, + Gpio7 { name: sw_a }, + Gpio8 { name: sw_b }, + Gpio9 { name: sw_c }, + Gpio10 { name: lcd_cs }, + Gpio11 { name: lcd_dc }, + Gpio12 { name: lcd_wr }, + Gpio13 { name: lcd_rd }, + Gpio14 { name: lcd_db0 }, + Gpio15 { name: lcd_db1 }, + Gpio16 { name: lcd_db2 }, + Gpio17 { name: lcd_db3 }, + Gpio18 { name: lcd_db4 }, + Gpio19 { name: lcd_db5 }, + Gpio20 { name: lcd_db6 }, + Gpio21 { name: lcd_db7 }, + Gpio22 { name: sw_up }, + Gpio23 { name: user_sw }, + Gpio24 { name: vbus_detect }, + Gpio25 { name: led }, + Gpio26 { name: light_sense }, + Gpio27 { name: sensor_power }, + Gpio28 { name: vref_1v24 }, + Gpio29 { name: vbat_sense }, +); + +pub const XOSC_CRYSTAL_FREQ: u32 = 12_000_000; + +#[inline] +fn set_pin_bit(pin: &mut P, bit: u8, value: u8) -> Result<(), DisplayError> { + pin.set_state(((bit & value) != 0).into()) + .map_err(|_| DisplayError::BusWriteError) +} + +struct WriteBytes(T); + +// Allow DMA to do byte-size writes to an existing target, +// SAFETY: This is only used with the PIO as a target, which is valid to write +// byte-width. +unsafe impl WriteTarget for WriteBytes { + type TransmittedWord = u8; + + #[inline] + fn tx_treq() -> Option { + T::tx_treq() + } + + #[inline] + fn tx_address_count(&mut self) -> (u32, u32) { + self.0.tx_address_count() + } + + #[inline] + fn tx_increment(&self) -> bool { + self.0.tx_increment() + } +} + +pub trait DisplayDataLines { + fn flush(&mut self) {} + + fn write_u8(&mut self, value: u8) -> Result<(), DisplayError>; + + fn write_slice(&mut self, data: &[u8]) -> Result<(), DisplayError> { + for b in data.iter().copied() { + self.write_u8(b)?; + } + + Ok(()) + } + + fn write_format(&mut self, data: DataFormat<'_>) -> Result<(), DisplayError> { + match data { + DataFormat::U8(bytes) => self.write_slice(bytes)?, + DataFormat::U16(items) => { + for value in items.iter().copied() { + self.write_u8(value as u8)?; + self.write_u8((value >> 8) as u8)?; + } + } + DataFormat::U16BE(items) => { + for value in items.iter().copied() { + self.write_u8((value >> 8) as u8)?; + self.write_u8(value as u8)?; + } + } + DataFormat::U16LE(items) => { + for value in items.iter().copied() { + self.write_u8(value as u8)?; + self.write_u8((value >> 8) as u8)?; + } + } + DataFormat::U8Iter(iter) => { + for value in iter { + self.write_u8(value)?; + } + } + DataFormat::U16BEIter(iter) => { + for value in iter { + self.write_u8((value >> 8) as u8)?; + self.write_u8(value as u8)?; + } + } + DataFormat::U16LEIter(iter) => { + for value in iter { + self.write_u8(value as u8)?; + self.write_u8((value >> 8) as u8)?; + } + } + _ => unimplemented!(), + } + + Ok(()) + } +} + +pub struct GpioDataLines { + pub wr: WR, + pub d0: D0, + pub d1: D1, + pub d2: D2, + pub d3: D3, + pub d4: D4, + pub d5: D5, + pub d6: D6, + pub d7: D7, +} + +impl< + WR: OutputPin, + D0: OutputPin, + D1: OutputPin, + D2: OutputPin, + D3: OutputPin, + D4: OutputPin, + D5: OutputPin, + D6: OutputPin, + D7: OutputPin, + > GpioDataLines +{ + #[inline] + fn write_u8_inner(&mut self, value: u8) -> Result<(), DisplayError> { + set_pin_bit(&mut self.d0, value, 1 << 0)?; + set_pin_bit(&mut self.d1, value, 1 << 1)?; + set_pin_bit(&mut self.d2, value, 1 << 2)?; + set_pin_bit(&mut self.d3, value, 1 << 3)?; + set_pin_bit(&mut self.d4, value, 1 << 4)?; + set_pin_bit(&mut self.d5, value, 1 << 5)?; + set_pin_bit(&mut self.d6, value, 1 << 6)?; + set_pin_bit(&mut self.d7, value, 1 << 7)?; + Ok(()) + } +} + +impl< + WR: OutputPin, + D0: OutputPin, + D1: OutputPin, + D2: OutputPin, + D3: OutputPin, + D4: OutputPin, + D5: OutputPin, + D6: OutputPin, + D7: OutputPin, + > DisplayDataLines for GpioDataLines +{ + fn write_u8(&mut self, value: u8) -> Result<(), DisplayError> { + self.wr.set_low().map_err(|_| DisplayError::BusWriteError)?; + let err = self.write_u8_inner(value); + self.wr.set_high().ok(); + err + } +} + +type PioTx = (Tx<(P, SM)>, Channel); + +pub struct PioDataLines { + tx: Option>, +} + +impl PioDataLines { + pub fn new( + pio: &mut hal::pio::PIO

, + sys_freq: HertzU32, + wr: impl PinId, + d0: impl PinId, + sm: UninitStateMachine<(P, SM)>, + ch: Channel, + ) -> PioDataLines { + let d0 = d0.as_dyn().num; + let wr = wr.as_dyn().num; + + let max_pio_clk = HertzU32::MHz(32); + let divider = (sys_freq + max_pio_clk - HertzU32::Hz(1)) / max_pio_clk; + + let program = pio_file!("./src/st7789_parallel.pio"); + let program = pio.install(&program.program).unwrap(); + let (mut sm, _rx, tx) = PIOBuilder::from_program(program) + .out_pins(d0, 8) + .side_set_pin_base(wr) + .buffers(Buffers::OnlyTx) + .pull_threshold(8) + .autopull(true) + .clock_divisor_fixed_point(divider as u16, 0) + .build(sm); + sm.set_pindirs([ + (d0, PinDir::Output), + (d0 + 1, PinDir::Output), + (d0 + 2, PinDir::Output), + (d0 + 3, PinDir::Output), + (d0 + 4, PinDir::Output), + (d0 + 5, PinDir::Output), + (d0 + 6, PinDir::Output), + (d0 + 7, PinDir::Output), + (wr, PinDir::Output), + ]); + sm.set_pins([(wr, PinState::High)]); + sm.start(); + + PioDataLines { tx: Some((tx, ch)) } + } +} + +impl DisplayDataLines + for PioDataLines +{ + fn flush(&mut self) { + if let Some((tx, _)) = self.tx.as_mut() { + while !tx.is_empty() {} + } + } + + fn write_u8(&mut self, value: u8) -> Result<(), DisplayError> { + if let Some((tx, _)) = self.tx.as_mut() { + while !tx.write(value as u32) {} + Ok(()) + } else { + Err(DisplayError::BusWriteError) + } + } + + fn write_slice(&mut self, data: &[u8]) -> Result<(), DisplayError> { + // SAFETY: transmute away lifetime, since we will always wait for DMA completion here. + let data: &'static [u8] = unsafe { core::mem::transmute(data) }; + + let (tx, ch) = self.tx.take().expect("DMA already in use"); + let xfer = single_buffer::Config::new(ch, data, WriteBytes(tx)).start(); + let (ch, _, WriteBytes(tx)) = xfer.wait(); + self.tx = Some((tx, ch)); + Ok(()) + } +} + +pub struct ParallelDisplayInterface { + cs: CS, + dc: DC, + data_lines: D, +} + +impl ParallelDisplayInterface { + pub fn new(cs: CS, dc: DC, data_lines: D) -> ParallelDisplayInterface { + ParallelDisplayInterface { cs, dc, data_lines } + } +} + +impl WriteOnlyDataCommand + for ParallelDisplayInterface +{ + fn send_commands(&mut self, cmds: DataFormat<'_>) -> Result<(), DisplayError> { + self.cs.set_low().map_err(|_| DisplayError::CSError)?; + self.dc.set_low().map_err(|_| DisplayError::DCError)?; + + let err = self.data_lines.write_format(cmds); + self.data_lines.flush(); + + err + } + + fn send_data(&mut self, buf: DataFormat<'_>) -> Result<(), DisplayError> { + self.dc.set_high().map_err(|_| DisplayError::DCError)?; + + let err = self.data_lines.write_format(buf); + self.data_lines.flush(); + + err + } +} + +pub struct DummyPin; + +impl OutputPin for DummyPin { + type Error = (); + + fn set_high(&mut self) -> Result<(), Self::Error> { + Ok(()) + } + + fn set_low(&mut self) -> Result<(), Self::Error> { + Ok(()) + } +} diff --git a/boards/pimoroni-tufty2040/src/st7789_parallel.pio b/boards/pimoroni-tufty2040/src/st7789_parallel.pio new file mode 100644 index 00000000..e9890e06 --- /dev/null +++ b/boards/pimoroni-tufty2040/src/st7789_parallel.pio @@ -0,0 +1,5 @@ +.program st7789_parallel +.side_set 1 + +out pins, 32 side 0 +nop side 1