Self-Aligning Dish in Rust: Command

ian_ndeda

Ian Ndeda

Posted on November 21, 2024

Self-Aligning Dish in Rust: Command

Our project is designed to receive commands remotely via Bluetooth. To facilitate this transfer, the UART peripheral in the Pico will have to be set up. The UART is a protocol that controls serial communication between supported devices. 

The Pico has two independent UARTs. We'll use UART1 for this task.

Table of Contents

Flowchart

command-algo

Every time a command byte is received via Bluetooth, a UART interrupt is fired. The received byte is processed, and the command to our system is updated.

Requirements

  • 1 x Raspberry Pico board
  • 1 x USB Cable type 2.0
  • 1 x HC-05 Bluetooth module
  • 10 x M-M jumper wires
  • 2 x Mini Breadboards
  • Serial Bluetooth Terminal App

Implementation

Remote commands will be sent from a Serial Bluetooth Terminal App on a phone and will be a byte each. They are shown below.

Command Execution
A Toggle the system between Manual and Auto modes
B Move the PTZ kit Clockwise
C Move the PTZ kit Counter-clockwise
D Move the PTZ kit Up
E Move the PTZ kit Down
F Move the PTZ kit to Position Zero

We will organize the above options into a Direction enum.

enum Direction {
    Cw,
    Ccw,
    Up,
    Down,
    Zero,
}
Enter fullscreen mode Exit fullscreen mode

Create a global static variable DIRECTION that will hold the position of the system at any given time.

static DIRECTION: Mutex<RefCell<Option<Direction>>> = Mutex::new(RefCell::new(None));
Enter fullscreen mode Exit fullscreen mode

The Serial Bluetooth App is linked to the HC-05 module which is in turn physically connected to gp8 and gp9 pins in the Pico. A complete path is therefore created from the phone issuing the command to the RP2040 processing it.

Serial Bluetooth App

As mentioned earlier we will be using the Serial Bluetooth App to send our commands to our system remotely.

We first have to set up the app so that it sends precisely one byte per press. Navigate to settings and under Newline select None. This is to enable the transmission of the command byte only and therefore match the UART interrupt, which we'll also set up to receive only one byte at a time. 

We will also allow local echo to help us visualize the commands we are sending.

command-setup-1

command-setup-2

Also edit the macro rows to have the commands discussed above i.e. Auto/Manual, CW, CCW, UP, DOWN and Zero.

You should have something of this sort:

command-setup-result

Connections

command-connection

The electrical connections will be as shown above.

UART Configuration

To begin we must configure the pins on the Pico board associated with UART1. These shall provide the physical connection to the HC-05 module in our program.

gpio-functions

We can see from above table that gp8 and gp9 can be used for UART1. We'll configure them as push-pull uart pins.

// Configure gp8 as UART1 Tx Pin
// Will be connected to HC-05 Rx Pin
pads_bank0.gpio(8).modify(|_, w| w
                       .pue().set_bit()
                       .pde().set_bit()
                       .od().clear_bit()
                       .ie().set_bit()
                       );

io_bank0.gpio(8).gpio_ctrl().modify(|_, w| w.funcsel().uart());

// Configure gp9 as UART1 Rx Pin
// Will be connected to HC-05 Tx Pin
pads_bank0.gpio(9).modify(|_, w| w
                       .pue().set_bit()
                       .pde().set_bit()
                       .od().clear_bit()
                       .ie().set_bit()
                       );

io_bank0.gpio(9).gpio_ctrl().modify(|_, w| w.funcsel().uart());
Enter fullscreen mode Exit fullscreen mode

Creating a handle for UART:

// Configuring UART1; command reception
let uart_cmd = dp.UART1;
resets.reset().modify(|_, w| w.uart1().clear_bit());// Deassert uart_cmd
Enter fullscreen mode Exit fullscreen mode

We then program the UART to run at 9600 baud-rate. The manual provides the formula for acquiring the baud-rate divisors.

Baud Rate Divisor=UARTCLK(16×Baud Rate)=BRDi+BRDf \begin{split} Baud\ Rate\ Divisor &= {UARTCLK \over (16 \times Baud\ Rate)}\\ &= BRD_i + BRD_f\\ \end{split}

where BRDi is the integer part and BRDf the fractional part.

Replacing for our desired baud-rate of 9600:

BRDi+BRDf=125,000,000÷(16×9,600)=813.8021=813+(0.8021×64+0.5)=813+51 \begin{split} BRD_i + BRD_f &= 125,000,000 \div (16 \times 9,600)\\&=813.8021\\&=813 + (0.8021 \times 64 + 0.5) \\&=813+51 \end{split}

The reference manual states the fractional part BRDf is acquired by taking the integer part of the calculation

BRDf×64+0.5BRD_f \times 64 + 0.5
.

For a baudrate of 9600 we therefore require a divisor with integer part as 813 and fractional part as 51.

// Set baudrate at 96 00
uart_cmd.uartibrd().write(|w| unsafe { w.bits(813)});
uart_cmd.uartfbrd().write(|w| unsafe { w.bits(51)});
Enter fullscreen mode Exit fullscreen mode

We finally have to set the word length to 8 and enable the peripheral.

uart_cmd.uartlcr_h().modify(|_, w| unsafe { w
    .wlen().bits(0b11)// Set word length as 8
});
uart_cmd.uartcr().modify(|_, w| w
                .uarten().set_bit()// Enable uart_cmd
                .txe().set_bit()// Enable tx
                .rxe().set_bit()// Enable rx
                );
Enter fullscreen mode Exit fullscreen mode

We now enable the UART1 interrupt and unmask it.

uart_cmd.uartimsc().modify(|_, w| w
    .rtim().set_bit());// set interrupt for when there's one byte in uart rx fifo
unsafe {
    cortex_m::peripheral::NVIC::unmask(interrupt::UART1_IRQ);// Unmask interrupt
}
Enter fullscreen mode Exit fullscreen mode

Since the UART peripheral will be accessed from both the main application and the interrupt service routine it will need to be behind a critical section to prevent data races. To facilitate this we create a UART1 global variable and move uart_cmd into it.

static UARTCMD: Mutex<RefCell<Option<UART1>>> = Mutex::new(RefCell::new(None));
Enter fullscreen mode Exit fullscreen mode

Import UART1

use rp2040_pac::UART1;
Enter fullscreen mode Exit fullscreen mode

Move uart_cmd into its global variable.

cortex_m::interrupt::free(|cs| {
    UARTCMD.borrow(cs).replace(Some(uart_cmd));
});
Enter fullscreen mode Exit fullscreen mode

Test

We can conduct a quick test for UART1 by sending a number of test bytes through. We should see the results on the Bluetooth App on our phone.

First, let's create the string that will act as the buffer holding all the data to be received.

// Buffers
let mut serialbuf = &mut String::<164>::new();// buffer to hold data to be serially transmitted

writeln!(serialbuf, "\nUart command test.").unwrap();
Enter fullscreen mode Exit fullscreen mode

We need to add a crate that will enable us use Strings in our program since there is no core library support.

heapless = "0.8.0"
Enter fullscreen mode Exit fullscreen mode

Importing into the project:

use heapless::String;
Enter fullscreen mode Exit fullscreen mode

To use writeln we have to import fmt::Write.

use core::fmt::Write;
Enter fullscreen mode Exit fullscreen mode

We now have to create functions we'll use in receiving and transmitting data via UART.

fn receive_uart_cmd(uart: &UART1) -> u8 {// receive 1 byte
    while uart.uartfr().read().rxfe().bit_is_set() {} // wait until byte received

    uart.uartdr().read().data().bits()// Received data
}

fn transmit_uart_cmd(uart: &UART1, buffer: &mut String<164>) {
    for ch in buffer.chars() {
        uart.uartdr().modify(|_, w| unsafe { w.data().bits(ch as u8)});// Send data
        while uart.uartfr().read().busy().bit_is_set()  {}// Wait until tx finished
    }

    buffer.clear()
}
Enter fullscreen mode Exit fullscreen mode

Continuing with our serial test...

transmit_uart_cmd(&uart_cmd, serialbuf);
Enter fullscreen mode Exit fullscreen mode

The above code snippet transmits data via usart_cmd using serialbuf as buffer.

Flash the program thus far into the Pico while the HC-05 is connected and check the App terminal. You should see the message Uart command test displayed there.

test-result

Now that we have confirmed UART is working, we can proceed with writing the interrupt service routine that'll receive the command bytes.

Interrupt

The algorithm below gives the general flow of the UART interrupt service routine.

command-int

The flags AUTO and AUTO_FIRST_ITER need to be set as global static variables. They tell us whether we are in auto or manual modes and if an auto iteration in the super loop is the first. These will be utilized later in the project.

static AUTO: Mutex<Cell<bool>> = Mutex::new(Cell::new(false));
static AUTO_FIRST_ITER: Mutex<Cell<bool>> = Mutex::new(Cell::new(false));
Enter fullscreen mode Exit fullscreen mode

Import Cell

use core::cell::Cell;
Enter fullscreen mode Exit fullscreen mode
#[interrupt]
fn UART1_IRQ() {
    // Enter critical section
    cortex_m::interrupt::free(|cs| {
    // peripheral handles
        let mut uart = UARTCMD.borrow(cs).borrow_mut();
        let mut dir = DIRECTION.borrow(cs).borrow_mut();

        uart.as_mut().unwrap().uarticr().modify(|_, w| w.rtic().bit(true));// clear rx interrupt 

        // receive commands
        let b = receive_uart_cmd(uart.as_mut().unwrap() );// Received byte

        if b == b'A' { // toggle between auto and manual modes
            if AUTO.borrow(cs).get() {
                AUTO.borrow(cs).set(false);
            } else {
                AUTO.borrow(cs).set(true);
                // if toggling to auto; set first iter true
                AUTO_FIRST_ITER.borrow(cs).set(true);
            }
        } else {
            // update direction cmd only in manual mode
            if !AUTO.borrow(cs).get() {
                if b == b'B' {
                    *dir = Some(Direction::Cw);
                } else if b == b'C' {
                    *dir = Some(Direction::Ccw);
                } else if b == b'D' {
                    *dir = Some(Direction::Up);
                } else if b == b'E' {
                    *dir = Some(Direction::Down);
                } else if b == b'F' {
                    *dir = Some(Direction::Zero);
                }
            }
        }
    });
}
Enter fullscreen mode Exit fullscreen mode

Final Code

After rearranging all the code up to now we get this final copy.

Here are highlights of the changes made from the previous program.

Running this program will flash it into the Pico and we'll be set to receive commands remotely via Bluetooth.

In the next part of the series we shall finish up on command reception by implementing the application section of the program.

💖 💪 🙅 🚩
ian_ndeda
Ian Ndeda

Posted on November 21, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related