Files
Embedded-Hacking/drivers/0x08_lcd1602_rust/README.md
T
Kevin Thomas f62db776e1 Initial commit
2026-07-06 14:32:12 -04:00

6.5 KiB

0x08 LCD1602 Rust Driver

This repository contains a Bare-Metal Rust driver for the HD44780-based LCD1602 character display (interfaced via a PCF8574 I2C backpack) on the RP2350 (and RP2040) microcontrollers.

It includes:

  • A demo (src/main.rs) that displays a title on the first row and a continuously incrementing 6-digit counter on the second row.
  • A reusable library module (src/lcd1602.rs) providing a hardware-agnostic lcd1602_lib with helper math and string formatting functions to assemble commands, DDRAM addresses, and zero-padded counter strings.
  • Board initialization logic (src/board.rs).

🚀 Getting Started from Scratch

If you're starting with a fresh machine, follow these exact steps to install the toolchain, build the code, and flash it to your microcontroller.

1. Install Rust

First, install rustup (the Rust toolchain installer) if you haven't already:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Note: Restart your terminal or run source $HOME/.cargo/env after this finishes.

Ensure your Rust compiler is up to date:

rustup update

2. Install the Target Architecture

This project is configured for the RP2350 (ARM Cortex-M33). We need to install the cross-compilation target for it:

rustup target add thumbv8m.main-none-eabihf

(If you were targeting the RP2040, you would use thumbv6m-none-eabi instead).

3. Install Build Tools

You will need a few extra tools to help link and format the firmware for the RP-series chips.

Install flip-link (adds zero-cost stack overflow protection):

cargo install flip-link

Install picotool (used by cargo run to flash the chip):

  • macOS: brew install picotool
  • Linux/Windows: Follow the official Raspberry Pi documentation to install picotool or build it from source.

4. Building the Code

To compile the code for the microcontroller, simply run:

cargo build

To build a highly optimized release version (smaller and faster):

cargo build --release

5. Flashing to the Microcontroller

This project is pre-configured in .cargo/config.toml to use picotool as the custom runner.

To flash the code:

  1. Hold down the BOOTSEL button on your RP2350 board.
  2. Plug it into your computer via USB (or press the RUN/RESET button while holding BOOTSEL).
  3. Run the following command:
cargo run --release

cargo will compile the code and automatically use picotool to upload the .elf file directly to your board and start executing it!

6. Testing on the Host

Because the LCD command/nibble construction and string formatting logic is separated into a reusable math library without touching hardware registers, you can run the unit tests natively on your computer!

However, because this project sets a default bare-metal target (thumbv8m.main-none-eabihf) in .cargo/config.toml, running a plain cargo test will fail because the standard library doesn't exist on the microcontroller. You must explicitly tell Cargo to compile the tests for your host computer's processor architecture:

Mac (Apple Silicon):

cargo test --lib --target aarch64-apple-darwin

Linux (Intel/AMD 64-bit):

cargo test --lib --target x86_64-unknown-linux-gnu

Windows (64-bit):

cargo test --lib --target x86_64-pc-windows-msvc

🧠 Code Walkthrough

This section explains exactly how the code works, where the entry point is, and traces the flow of execution as if you were stepping through it line-by-line.

1. The Entry Point (src/main.rs)

Unlike a standard computer program, bare-metal microcontrollers do not have an operating system to call main(). Instead, we use the #[entry] macro from the HAL (Hardware Abstraction Layer) to define the very first function that runs after the chip boots up.

  • main() -> !: This is the absolute start of our code. It takes ownership of all the hardware peripherals (hal::pac::Peripherals::take().unwrap()) and immediately passes them into board::run(...). The -> ! means this function never returns (because embedded devices run in an infinite loop).

2. Board Initialization (src/board.rs)

Once execution enters board.rs, we initialize the system clocks, pins, UART (for logging), SysTick (for delay), and the I2C peripheral (for LCD communication).

  • run(...): The master setup function. Calls the helper initialization functions below, triggers the LCD setup sequence, and enters the infinite counter loop.
  • init_i2c(...): Initializes the hardware I2C1 peripheral, reconfigures GPIO 2 and GPIO 3 as SDA and SCL lines (with internal pull-ups enabled), and sets the clock speed to 100 kHz.
  • lcd_hd44780_reset(...) & lcd_hd44780_configure(...): Executes the strict timing requirements of the HD44780 datasheet to force the LCD into 4-bit mode (using three successive 0x03 nibbles followed by 0x02), then configures a 2-line display, turns on the screen, and clears the RAM.
  • lcd_write4(...) & lcd_send(...): The core functions for pushing data to the screen. Because the I2C backpack (PCF8574) uses an 8-bit GPIO expander, we must send an 8-bit LCD command by breaking it into two 4-bit nibbles.
  • pcf_pulse_enable(...): Simulates the physical EN (Enable) pin being toggled high, then low, to clock data into the HD44780 shift register over I2C.
  • update_counter(...): Uses the lcd1602_lib to format the current count into a byte array without memory allocation, then sends those bytes over I2C and UART.

3. The Reusable LCD1602 Library (src/lcd1602.rs)

Because manipulating bits for a 4-bit LCD over an 8-bit I2C GPIO expander can be messy, this module cleanly separates the bitwise logic from the hardware I/O.

  • build_nibble(...): Takes a 4-bit data value and combines it with the Register Select (RS) bit and the Backlight enable bit to construct the raw byte that must be written to the PCF8574 expander.
  • nibble_with_en(...) & nibble_without_en(...): Trivial helpers that apply/remove the Enable (EN) mask bit to the assembled I2C byte.
  • cursor_address(...): Safely translates a (line, position) coordinate into the memory-mapped DDRAM address required by the HD44780 controller.
  • format_counter(...): Efficiently splits a 32-bit integer into its component ASCII decimal digits using division, right-justifies them into a 6-character field (Count: 12), and returns the exact number of bytes written to the buffer so the caller knows how much to transmit.