StickS3 IR infrared transmit/receive related APIs and example program.
#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, ©_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;
}cfg.internal_spk = false; in the code below), otherwise it will not receive correctly.#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.
