pdf-icon

Arduino Quick Start

2. Devices & Examples

6. Applications

StickS3 IR NEC

StickS3 IR infrared transmit/receive related APIs and example program.

Example

Compilation Requirements

  • M5Stack Board Manager version >= 3.2.5
  • Development board option = M5StickS3
  • M5Unified library version >= 0.2.12
  • M5GFX library version >= 0.2.18

Transmitter

cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238
#include "M5Unified.h"
#include "driver/rmt_tx.h"
#include "driver/rmt_encoder.h"

#define IR_SEND_PIN    46      // GPIO pin connected to IR LED transmitter

// NEC protocol parameters
uint16_t address = 0x0000;     // NEC address (8-bit or 16-bit)
uint8_t  command = 0x55;       // NEC command byte
uint8_t  repeats = 0;          // Number of repeat frames (0 = no repeat)

rmt_channel_handle_t tx_chan = NULL;
rmt_encoder_handle_t copy_encoder = NULL;

// NEC protocol timing constants (microseconds)
#define NEC_HEADER_MARK     9000
#define NEC_HEADER_SPACE    4500
#define NEC_BIT_MARK        560
#define NEC_BIT_0_SPACE     560
#define NEC_BIT_1_SPACE     1690
#define NEC_REPEAT_MARK     9000
#define NEC_REPEAT_SPACE    2250

// IR carrier configuration
#define IR_CARRIER_FREQ_HZ  38000
#define IR_DUTY_CYCLE       0.33

// Function prototypes
void setup_rmt_tx();
bool sendNEC(uint16_t address, uint8_t command, uint8_t repeats);
void encodeNEC(uint32_t raw_data, rmt_symbol_word_t *symbols, size_t *symbol_count);
uint32_t NECRaw(uint16_t address, uint8_t command);

void setup() {
    M5.begin();
    Serial.begin(115200);
    
    // Display initialization
    M5.Display.setRotation(3);
    M5.Display.setTextFont(&fonts::FreeMonoBold9pt7b);
    M5.Display.clear();
    M5.Display.setCursor(0, 0);
    M5.Display.printf("StickS3 IR example");
    
    Serial.println("StickS3 IR example");
    
    // Initialize RMT TX channel
    setup_rmt_tx();
    
    Serial.printf("IR Send Pin: %d\n", IR_SEND_PIN);
    
    // Enable external power output for IR LED module
    M5.Power.setExtOutput(true, m5::ext_none);
    delay(100);
}

void loop() {
    // Build 32-bit NEC frame data
    uint32_t raw = NECRaw(address, command);
    
    Serial.printf("Send NEC: addr=0x%04X, cmd=0x%02X, raw=0x%08X\n", address, command, raw);
    
    // -------- Send NEC frame --------
    sendNEC(address, command, repeats);
    
    M5.Display.fillRect(0, 30, 240, 105, TFT_BLACK);
    M5.Display.setCursor(0, 30);
    M5.Display.printf("Send NEC:\n");
    M5.Display.printf(" addr=0x%04X\n", address);
    M5.Display.printf(" cmd =0x%02X\n", command);
    M5.Display.printf(" raw =0x%08X\n", raw);
    
    address += 0x0001;
    command += 0x01;
    repeats = 0;
    
    delay(2000);
}

// Initialize RMT TX channel
void setup_rmt_tx() {
    // Configure RMT TX channel
    rmt_tx_channel_config_t tx_chan_config = {
        .gpio_num = (gpio_num_t)IR_SEND_PIN,
        .clk_src = RMT_CLK_SRC_DEFAULT,
        .resolution_hz = 1000000, // 1 us per tick
        .mem_block_symbols = 64,
        .trans_queue_depth = 4,
        .flags = {
            .invert_out = false,
            .with_dma = false,
        }
    };
    ESP_ERROR_CHECK(rmt_new_tx_channel(&tx_chan_config, &tx_chan));
    
    // Configure 38kHz carrier wave for IR transmission
    rmt_carrier_config_t carrier_cfg = {
        .frequency_hz = IR_CARRIER_FREQ_HZ,
        .duty_cycle = IR_DUTY_CYCLE,
        .flags = {
            .polarity_active_low = false,
        }
    };
    ESP_ERROR_CHECK(rmt_apply_carrier(tx_chan, &carrier_cfg));
    
    // Create copy encoder for pre-encoded symbols
    rmt_copy_encoder_config_t encoder_config = {};
    ESP_ERROR_CHECK(rmt_new_copy_encoder(&encoder_config, &copy_encoder));
    
    // Enable TX channel
    ESP_ERROR_CHECK(rmt_enable(tx_chan));
}

/*
 * Send NEC IR frame via RMT.
 *
 * @param address NEC address (8-bit or 16-bit)
 * @param command NEC command byte
 * @param repeats Number of repeat frames to send
 *
 * @return true if transmission successful
 */
bool sendNEC(uint16_t address, uint8_t command, uint8_t repeats) {
    // Build 32-bit NEC raw data
    uint32_t raw = NECRaw(address, command);
    
    // Buffer for RMT symbols
    rmt_symbol_word_t symbols[68]; // Header + 32 bits + ending mark
    size_t symbol_count = 0;
    
    encodeNEC(raw, symbols, &symbol_count);
    
    // RMT transmit configuration
    rmt_transmit_config_t tx_config = {
        .loop_count = 0,
        .flags = {
            .eot_level = 0,
        }
    };
    
    // Transmit the frame
    esp_err_t ret = rmt_transmit(tx_chan, copy_encoder, symbols, 
                                  symbol_count * sizeof(rmt_symbol_word_t), 
                                  &tx_config);
    
    if (ret == ESP_OK) {
        // Wait for transmission completion
        ret = rmt_tx_wait_all_done(tx_chan, 1000);
    }
    
    // Send repeat frames if requested
    for (int i = 0; i < repeats; i++) {
        delay(108); // NEC repeat frame interval
        
        // TODO: Implement repeat frame
        // Repeat frame: 9ms mark + 2.25ms space + 560us mark
    }
    
    return (ret == ESP_OK);
}

/*
 * Encode NEC protocol data into RMT symbols.
 *
 * @param raw_data     32-bit NEC frame data
 * @param symbols      Output buffer for RMT symbols
 * @param symbol_count Number of symbols generated
 */
void encodeNEC(uint32_t raw_data, rmt_symbol_word_t *symbols, size_t *symbol_count) {
    size_t idx = 0;
    
    // NEC header: ~9 ms mark + ~4.5 ms space
    symbols[idx].duration0 = NEC_HEADER_MARK;
    symbols[idx].level0 = 1;
    symbols[idx].duration1 = NEC_HEADER_SPACE;
    symbols[idx].level1 = 0;
    idx++;
    
    // Encode 32 data bits (LSB first)
    for (int i = 0; i < 32; i++) {
        // Mark duration: always 560 us
        symbols[idx].duration0 = NEC_BIT_MARK;
        symbols[idx].level0 = 1;
        
        // Space duration distinguishes logic 0 and logic 1
        if (raw_data & (1UL << i)) {
            symbols[idx].duration1 = NEC_BIT_1_SPACE; // Logic 1: 1690 us
        } else {
            symbols[idx].duration1 = NEC_BIT_0_SPACE; // Logic 0: 560 us
        }
        symbols[idx].level1 = 0;
        idx++;
    }
    
    // Ending mark: 560 us
    symbols[idx].duration0 = NEC_BIT_MARK;
    symbols[idx].level0 = 1;
    symbols[idx].duration1 = 0;
    symbols[idx].level1 = 0;
    idx++;
    
    *symbol_count = idx;
}

/*
 * Build 32-bit NEC raw data from address and command.
 *
 * NEC frame format (LSB first):
 *   bit  0-15 : Address field (8-bit + inverse, or full 16-bit)
 *   bit 16-23 : Command byte
 *   bit 24-31 : Inverse of command byte
 *
 * @param address NEC address (8-bit with auto-inverse, or 16-bit extended)
 * @param command NEC command byte
 *
 * @return 32-bit NEC raw data ready for encoding
 */
uint32_t NECRaw(uint16_t address, uint8_t command) {
    uint16_t nec_addr;
    
    // Standard NEC: 8-bit address + inverse byte
    if (address <= 0x00FF) {
        uint8_t addr8 = address & 0xFF;
        nec_addr = ((uint16_t)(~addr8) << 8) | addr8;
    }
    // Extended NEC: full 16-bit address
    else {
        nec_addr = address;
    }
    
    // Assemble 32-bit NEC frame
    uint32_t raw = 0;
    raw |= (uint32_t)nec_addr;            // Address field
    raw |= (uint32_t)command << 16;       // Command byte
    raw |= (uint32_t)(~command) << 24;    // Inverted command byte
    
    return raw;
}

Receiver

Note
When using the IR receive function, you need to disable the speaker amplifier (i.e., cfg.internal_spk = false; in the code below), otherwise it will not receive correctly.
cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153
#include "M5Unified.h"
#include "driver/rmt_rx.h"

#define IR_RECEIVE_PIN 42

rmt_channel_handle_t rx_chan = NULL;

bool decodeNEC(rmt_symbol_word_t *rx_raw_symbols, uint32_t *out_raw, bool *out_repeat);

// Initialize RMT RX channel
void setup_rmt_rx() {
    rmt_rx_channel_config_t rx_chan_config = {
        .gpio_num = (gpio_num_t)IR_RECEIVE_PIN,
        .clk_src = RMT_CLK_SRC_DEFAULT,
        .resolution_hz = 1000000, // 1 us per tick
        .mem_block_symbols = 128,
    };
    ESP_ERROR_CHECK(rmt_new_rx_channel(&rx_chan_config, &rx_chan));
    ESP_ERROR_CHECK(rmt_enable(rx_chan));
}

void setup() {
    auto cfg = M5.config();
    cfg.internal_spk = false; // Disable speaker amp to avoid IR RX interference
    M5.begin(cfg);

    Serial.begin(115200);

    // Display initialization
    M5.Display.setRotation(3);
    M5.Display.setFont(&fonts::FreeMonoBold9pt7b);
    M5.Display.clear();
    M5.Display.println("StickS3 IR example");
    M5.Display.setCursor(0, 30);
    M5.Display.println("Waiting for NEC...");

    setup_rmt_rx();

    // Enable external power output for IR receiver module
    M5.Power.setExtOutput(true, m5::ext_none);
}

void loop() {
    M5.update();

    // Buffer for received RMT symbols
    rmt_symbol_word_t rx_raw_symbols[64];

    // RMT receive configuration
    rmt_receive_config_t receive_config = {
        .signal_range_min_ns = 1000,
        .signal_range_max_ns = 20000000
    };

    if (rmt_receive(rx_chan, rx_raw_symbols, sizeof(rx_raw_symbols), &receive_config) == ESP_OK)
    {
        delay(100); // Allow DMA buffer to be fully populated

        uint32_t rx_data = 0;
        bool repeat_frame = false;

        // -------- Decode NEC frame --------
        bool valid = decodeNEC(rx_raw_symbols, &rx_data, &repeat_frame);

        if (repeat_frame) {
            Serial.println("NEC Repeat Frame");

            M5.Display.fillRect(0, 30, 240, 105, TFT_BLACK);
            M5.Display.setCursor(0, 30);
            M5.Display.setTextColor(YELLOW);
            M5.Display.println("NEC Repeat");
        }
        else if (valid) {
            uint16_t rx_addr = rx_data & 0xFFFF;
            uint8_t  rx_cmd  = (rx_data >> 16) & 0xFF;

            Serial.printf( "Received NEC: Addr: 0x%04X, Cmd: 0x%02X, Raw: 0x%08X\n", rx_addr, rx_cmd, rx_data);

            M5.Display.fillRect(0, 30, 240, 105, TFT_BLACK);
            M5.Display.setCursor(0, 30);
            M5.Display.setTextColor(GREEN);
            M5.Display.printf("Received NEC:\n");
            M5.Display.printf("Addr: 0x%04X\n", rx_addr);
            M5.Display.printf("Cmd:  0x%02X\n", rx_cmd);
            M5.Display.printf("Raw:  0x%08X\n", rx_data);
        }
        else {
            Serial.println("Signal received, but not a valid NEC frame.");
        }
    }

    delay(10);
    M5.Display.setTextColor(WHITE);
}

/*
 * Decode a NEC IR frame from RMT symbols.
 *
 * @param rx_raw_symbols Pointer to RMT RX symbol buffer
 * @param out_raw        Decoded 32-bit NEC raw data (LSB first)
 * @param out_repeat     Set to true if a NEC repeat frame is detected
 *
 * @return true if a valid NEC data frame is decoded
 */
bool decodeNEC(rmt_symbol_word_t *rx_raw_symbols, uint32_t *out_raw, bool *out_repeat) {

    *out_raw = 0;
    *out_repeat = false;

    uint32_t header_low  = rx_raw_symbols[0].duration0;
    uint32_t header_high = rx_raw_symbols[0].duration1;

    // Standard NEC header: ~9 ms LOW + ~4.5 ms HIGH
    if (header_low > 8000 && header_high > 4000) {
        // Valid NEC header, continue decoding
    }
    // NEC repeat frame: ~9 ms LOW + ~2.25 ms HIGH
    else if (header_low > 8000 &&
             header_high > 2000 &&
             header_high < 3000) {
        *out_repeat = true;
        return false;
    }
    else {
        return false;
    }

    // Decode 32 NEC data bits (LSB first)
    for (int i = 0; i < 32; i++) {
        uint32_t mark  = rx_raw_symbols[i + 1].duration0;
        uint32_t space = rx_raw_symbols[i + 1].duration1;

        // NEC mark duration should be ~560 us
        if (mark < 300 || mark > 800) {
            return false;
        }

        // Space duration distinguishes logic 0 and logic 1
        if (space > 1000) {
            *out_raw |= (1UL << i);
        }
    }

    // Verify command byte and its inverse
    uint8_t cmd     = (*out_raw >> 16) & 0xFF;
    uint8_t cmd_inv = (*out_raw >> 24) & 0xFF;

    if ((cmd ^ cmd_inv) != 0xFF) {
        return false;
    }

    return true;
}

The transmitter and receiver run on separate StickS3 devices. The transmitter sends an NEC infrared signal every two seconds. After receiving the signal, the receiver displays the address and command on both the serial port and the screen.

Note
1. The reflective surface on the right side is not shown in the image below. In practical use, it is recommended to use a reflective surface to reflect the infrared signal from the transmitter to the receiver for optimal reception.
2. When the user code (Address) does not exceed 0x00FF, the transmitter sends the infrared signal in the NEC standard format. In this case, the address field is composed of 8-bit user code and its inverse. The address parsed by the receiver is not exactly the same as the transmitter. Conversely, when the user code exceeds 0x00FF, the transmitter sends the infrared signal in the NEC extended format. In this case, the address field is the complete 16-bit user code, and the address parsed by the receiver is consistent with the transmitter.
On This Page