Infrared Rust

Introduction

Updated 2020-09-21 for Infrared 0.7

This tutorial will show you how to add infrared remote control support to your embedded rust project using the infrared library.

For an introduction to infrared technology I can really recommend SB-Projects pages. Great resource with lots of information about the different commonly used protocols.

Infrared the library

Infrared is a rust library that I wrote for working with remote controls. It has support for both receiving and transmitting IR remote control signals. Protocols supported are: NEC, Philip Rc5 and Rc6, and variants of these.

Tutorial 1: Receiving infrared with Rust

All source code for the examples in this tutorial can be found in the infrared-examples repo.

Components

Wiring it up

The IR-module has three legs, power ground and data. Connect the power pins to the corresponding GND and 3.3V on the dev board, and the data pin to a suitable gpio in pin, on the bluepill. The data sheets for the TSOP suggest that a capacitor over Power and GND and a resistor in series with the 3.3V could be used to “improve the robustnes against electrical overstress”, so you might want to do that in a real application.

The board

Part 1: Determining the button mappings

In this first part of the tutorial we will set up the receiver and determine the address and commands the remote uses. The source code for this part can be found in here.

The remote control

The remote control we are gonna use is a generic replacement control for a Philips TV. As it is a replacement to be used with a Philips TV we can be pretty sure it uses Rc5 or Rc6 as those protocols were developed by Philips.

The Remote Control

Infrared setup

Setup the software for your board and configure the pin connected to the IR receiver module as an input. In my example I will use pin 8 on Port B (PB8) on the bluepill board as my input from the IR receiver module.

We also need a timer interrupt that periodically samples the pin and let the internal state machine update its state depending on the level of the pin.

The receiver and the timer are declared as global variables, so that the interrupt routine can access them.

type Receiver = PeriodicReceiver<Rc6, PB8<Input<Floating>>>;
static mut RECEIVER: Option<Receiver> = None;

In the main function we create the receiver and place it in the global Receiver Option.

let receiver = PeriodicReceiver::new(pin, TIMER_FREQ);

unsafe {
    RECEIVER.replace(receiver);
}

Timer setup:


let mut timer = Timer::tim2(device.TIM2, &clocks, &mut rcc.apb1)
                         .start_count_down(TIMER_FREQ.hz());

timer.listen(Event::Update);

In the timer interrupt we then want to call the sample function on the receiver to eventually get a command back from it when the internal state machine has detected a command.

#[interrupt]
fn TIM2() {
    let receiver = unsafe { RECEIVER.as_mut().unwrap() };

    if let Ok(Some(cmd)) = receiver.poll() {
        rprintln!("Cmd: {} {}", cmd.address(), cmd.data());
    }

    // Clear the interrupt
    let timer = unsafe { TIMER.as_mut().unwrap() };
    timer.clear_update_interrupt_flag();
}

Running this example

cargo run --release --example tutorial1a

It should produce an output, on the debug terminal, similar to this:

Ready!
Cmd: 0 2
Cmd: 0 3
Cmd: 0 5
Cmd: 0 8
Cmd: 0 77
Cmd: 0 76

From this we can tell that the device address of the remote is 0.

Next it is time to create a mapping for the buttons we want to use.

Part 2 – Creating a remote control

Infrared comes with some support for remote controls and even has a few bundled, but this Rc6 TV is not one of them, so to be able to use more conveniently we have to create a type that implements the RemoteControl trait.

struct Rc6Tv;

impl RemoteControl for Rc6Tv {
    const MODEL: &'static str = "Rc6 Tv";
    const DEVTYPE: DeviceType = DeviceType::TV;
    const ADDRESS: u32 = 0;
    type Cmd = Rc6Cmd;
    const BUTTONS: &'static [(u8, Button)] = &[
        // Cmdid to Button mappings
        (1, Button::One),
        (2, Button::Two),
        (3, Button::Three),
        (4, Button::Four),
        (5, Button::Five),
        (6, Button::Six),
        (7, Button::Seven),
        (8, Button::Eight),
        (9, Button::Nine),
        (12, Button::Power),
        (76, Button::VolumeUp),
        (77, Button::VolumeDown),
        (60, Button::Teletext),
    ];
}

In the interrupt, change the call from poll() to poll_button() and specify the type of RemoteControl Infrared should try to decode the command into.

#[interrupt]
fn TIM2() {
    let receiver = unsafe { RECEIVER.as_mut().unwrap() };

    if let Ok(Some(button)) = receiver.poll_button::<Rc6Tv>() {
        use Button::*;

        match button {
            Teletext => rprintln!("Teletext!"),
            Power => rprintln!("Power on/off"),
            _ => rprintln!("Button: {:?}", button),
        };
    }

    // Clear the interrupt
    let timer = unsafe { TIMER.as_mut().unwrap() };
    timer.clear_update_interrupt_flag();
}

And that’s it!

Conclusion and notes

The infrared library is in a state where I think it is usable by others. Contributions in the form of support for more protocols and remote are very welcome! I will try to follow up this tutorial with a one with an example of transmitting command as well.

Note about the use of unsafe

In this example both the receiver, and the timer, are only used in the interrupt after being constructed, so the unsafe is safe – to the best of my knowledge.