Infrared Rust

Introduction

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-tutorial repo in my github

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 this file.

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 being declared as globals so that the interrupt routine can access them.

type Receiver = InfraredReceiver<PB8<Input<Floating>>, Rc6>;

static mut RECEIVER: Option<Receiver> = None;

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

let receiver = InfraredReceiver::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() {
    static mut SAMPLECOUNTER: u32 = 0;

    let receiver = unsafe { RECEIVER.as_mut().unwrap() };

    if let Ok(Some(cmd)) = receiver.sample(*SAMPLECOUNTER) {
        let _ = hprintln!("Cmd: {} {}", cmd.address(), cmd.command());
    }

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

    *SAMPLECOUNTER = SAMPLECOUNTER.wrapping_add(1);
}

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. There’s of course a macro for that.

remotecontrol_standardbutton!(
    Rc6Tv,           // Type name
    ProtocolId::Rc6, // The protocol id
    "Philips TV",    // Name
    DeviceType::TV,  // DeviceType
    0,               // Address
    Rc6Command,      // The Command type
    [
        // Command number to Button mapping
        (1, One),
        (2, Two),
        (3, Three),
        (4, Four),
        (5, Five),
        (6, Six),
        (7, Seven),
        (8, Eight),
        (9, Nine),
        (12, Power),
        (76, VolumeUp),
        (77, VolumeDown),
        (60, Teletext),
    ]
);

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


if let Ok(Some(button)) = receiver.sample_as_button::<Rc6Tv>(*SAMPLECOUNTER) {
    use StandardButton::*;

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

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.