Fundamentals 17 min read

Understanding Linux NEC Infrared Remote Drivers: From Protocol Basics to Kernel Implementation

This article explains how infrared remote controls work, details the NEC protocol timing and encoding, and walks through the Linux kernel driver implementation—including GPIO‑IR receiver code, NEC decoder logic, device‑tree configuration, and key‑event reporting—providing a complete technical guide for developers.

Liangxu Linux
Liangxu Linux
Liangxu Linux
Understanding Linux NEC Infrared Remote Drivers: From Protocol Basics to Kernel Implementation

Overview of the NEC infrared protocol

The NEC protocol modulates a 38 kHz carrier with pulse‑width‑modulated (PWM) bursts. Each transmission consists of an 8‑bit address and an 8‑bit command, both sent twice for reliability. A logical 1 is encoded as a 560 µs carrier pulse followed by a 1.68 ms low period (total 2.25 ms). A logical 0 uses the same 560 µs pulse followed by a 560 µs low period (total 1.125 ms). The carrier is always 38 kHz.

Linux GPIO‑IR receiver driver (gpio‑ir‑recv.c)

This driver registers a platform device that reads a GPIO line connected to an IR receiver. It configures the GPIO as an input, requests an interrupt on both rising and falling edges, and forwards raw edge events to the IR core.

#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/interrupt.h>
#include <linux/gpio.h>
#include <linux/slab.h>
#include <linux/of.h>
#include <linux/of_gpio.h>
#include <linux/platform_device.h>
#include <linux/irq.h>
#include <media/rc-core.h>
#include <media/gpio-ir-recv.h>

#define GPIO_IR_DRIVER_NAME "gpio-rc-recv"
#define GPIO_IR_DEVICE_NAME "gpio_ir_recv"

struct gpio_rc_dev {
    struct rc_dev *rcdev;
    int gpio_nr;
    bool active_low;
};

#ifdef CONFIG_OF
static int gpio_ir_recv_get_devtree_pdata(struct device *dev,
                                          struct gpio_ir_recv_platform_data *pdata)
{
    struct device_node *np = dev->of_node;
    enum of_gpio_flags flags;
    int gpio;

    gpio = of_get_gpio_flags(np, 0, &flags);
    if (gpio < 0) {
        if (gpio != -EPROBE_DEFER)
            dev_err(dev, "Failed to get gpio flags (%d)
", gpio);
        return gpio;
    }

    pdata->gpio_nr = gpio;
    pdata->active_low = (flags & OF_GPIO_ACTIVE_LOW);
    pdata->map_name = of_get_property(np, "linux,rc-map-name", NULL);
    pdata->allowed_protos = 0;
    return 0;
}
#else
#define gpio_ir_recv_get_devtree_pdata(dev, pdata) (-ENOSYS)
#endif

static irqreturn_t gpio_ir_recv_irq(int irq, void *dev_id)
{
    struct gpio_rc_dev *gpio_dev = dev_id;
    int gval;
    int rc = 0;
    enum raw_event_type type = IR_SPACE;

    gval = gpio_get_value(gpio_dev->gpio_nr);
    if (gval < 0)
        goto err_get_value;

    if (gpio_dev->active_low)
        gval = !gval;

    if (gval == 1)
        type = IR_PULSE;

    rc = ir_raw_event_store_edge(gpio_dev->rcdev, type);
    if (rc < 0)
        goto err_get_value;

    ir_raw_event_handle(gpio_dev->rcdev);

err_get_value:
    return IRQ_HANDLED;
}

static int gpio_ir_recv_probe(struct platform_device *pdev)
{
    struct gpio_rc_dev *gpio_dev;
    struct rc_dev *rcdev;
    const struct gpio_ir_recv_platform_data *pdata = pdev->dev.platform_data;
    int rc;

    if (pdev->dev.of_node) {
        struct gpio_ir_recv_platform_data *dtpdata;
        dtpdata = devm_kzalloc(&pdev->dev, sizeof(*dtpdata), GFP_KERNEL);
        if (!dtpdata)
            return -ENOMEM;
        rc = gpio_ir_recv_get_devtree_pdata(&pdev->dev, dtpdata);
        if (rc)
            return rc;
        pdata = dtpdata;
    }
    if (!pdata || pdata->gpio_nr < 0)
        return -EINVAL;

    gpio_dev = kzalloc(sizeof(*gpio_dev), GFP_KERNEL);
    if (!gpio_dev)
        return -ENOMEM;

    rcdev = rc_allocate_device();
    if (!rcdev) {
        rc = -ENOMEM;
        goto err_allocate_device;
    }

    rcdev->priv = gpio_dev;
    rcdev->driver_type = RC_DRIVER_IR_RAW;
    rcdev->input_name = GPIO_IR_DEVICE_NAME;
    rcdev->input_phys = GPIO_IR_DEVICE_NAME "/input0";
    rcdev->input_id.bustype = BUS_HOST;
    rcdev->input_id.vendor = 0x0001;
    rcdev->input_id.product = 0x0001;
    rcdev->input_id.version = 0x0100;
    rcdev->dev.parent = &pdev->dev;
    rcdev->driver_name = GPIO_IR_DRIVER_NAME;
    rcdev->allowed_protocols = pdata->allowed_protos ? pdata->allowed_protos : RC_BIT_ALL;
    rcdev->map_name = pdata->map_name ?: RC_MAP_EMPTY;

    gpio_dev->rcdev = rcdev;
    gpio_dev->gpio_nr = pdata->gpio_nr;
    gpio_dev->active_low = pdata->active_low;

    rc = gpio_request(pdata->gpio_nr, "gpio-ir-recv");
    if (rc < 0)
        goto err_gpio_request;
    rc = gpio_direction_input(pdata->gpio_nr);
    if (rc < 0)
        goto err_gpio_direction_input;

    rc = rc_register_device(rcdev);
    if (rc < 0) {
        dev_err(&pdev->dev, "failed to register rc device
");
        goto err_register_rc_device;
    }

    platform_set_drvdata(pdev, gpio_dev);

    rc = request_any_context_irq(gpio_to_irq(pdata->gpio_nr),
                                gpio_ir_recv_irq,
                                IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING,
                                "gpio-ir-recv-irq", gpio_dev);
    if (rc < 0)
        goto err_request_irq;

    return 0;

err_request_irq:
    rc_unregister_device(rcdev);
    rcdev = NULL;
err_register_rc_device:
    gpio_free(pdata->gpio_nr);
err_gpio_direction_input:
    gpio_free(pdata->gpio_nr);
err_gpio_request:
    rc_free_device(rcdev);
err_allocate_device:
    kfree(gpio_dev);
    return rc;
}

static int gpio_ir_recv_remove(struct platform_device *pdev)
{
    struct gpio_rc_dev *gpio_dev = platform_get_drvdata(pdev);
    free_irq(gpio_to_irq(gpio_dev->gpio_nr), gpio_dev);
    rc_unregister_device(gpio_dev->rcdev);
    gpio_free(gpio_dev->gpio_nr);
    kfree(gpio_dev);
    return 0;
}

#ifdef CONFIG_PM
static int gpio_ir_recv_suspend(struct device *dev)
{
    struct platform_device *pdev = to_platform_device(dev);
    struct gpio_rc_dev *gpio_dev = platform_get_drvdata(pdev);
    if (device_may_wakeup(dev))
        enable_irq_wake(gpio_to_irq(gpio_dev->gpio_nr));
    else
        disable_irq(gpio_to_irq(gpio_dev->gpio_nr));
    return 0;
}

static int gpio_ir_recv_resume(struct device *dev)
{
    struct platform_device *pdev = to_platform_device(dev);
    struct gpio_rc_dev *gpio_dev = platform_get_drvdata(pdev);
    if (device_may_wakeup(dev))
        disable_irq_wake(gpio_to_irq(gpio_dev->gpio_nr));
    else
        enable_irq(gpio_to_irq(gpio_dev->gpio_nr));
    return 0;
}

static const struct dev_pm_ops gpio_ir_recv_pm_ops = {
    .suspend = gpio_ir_recv_suspend,
    .resume  = gpio_ir_recv_resume,
};
#endif

static struct platform_driver gpio_ir_recv_driver = {
    .probe = gpio_ir_recv_probe,
    .remove = gpio_ir_recv_remove,
    .driver = {
        .name = GPIO_IR_DRIVER_NAME,
        .of_match_table = of_match_ptr(gpio_ir_recv_of_match),
#ifdef CONFIG_PM
        .pm = &gpio_ir_recv_pm_ops,
#endif
    },
};
module_platform_driver(gpio_ir_recv_driver);

MODULE_DESCRIPTION("GPIO IR Receiver driver");
MODULE_LICENSE("GPL v2");

NEC decoder implementation (ir‑nec‑decoder.c)

The decoder implements a state machine that converts the raw pulse/space stream produced by the driver into NEC scancodes. It recognises the standard NEC header, repeat frames, and both normal (address + command) and extended (address + ~address + command) formats. Checksums are verified by comparing each byte with its logical inverse; if the checksum fails the full 32‑bit value is still reported.

#include <linux/bitrev.h>
#include <linux/module.h>
#include "rc-core-priv.h"

#define NEC_NBITS        32
#define NEC_UNIT       562500   /* ns */
#define NEC_HEADER_PULSE (16 * NEC_UNIT)
#define NECX_HEADER_PULSE (8  * NEC_UNIT)   /* less common variant */
#define NEC_HEADER_SPACE  (8  * NEC_UNIT)
#define NEC_REPEAT_SPACE (4  * NEC_UNIT)
#define NEC_BIT_PULSE    (1  * NEC_UNIT)
#define NEC_BIT_0_SPACE  (1  * NEC_UNIT)
#define NEC_BIT_1_SPACE  (3  * NEC_UNIT)
#define NEC_TRAILER_PULSE (1 * NEC_UNIT)
#define NEC_TRAILER_SPACE (10* NEC_UNIT)   /* actual hardware may be longer */
#define NECX_REPEAT_BITS 1

enum nec_state {
    STATE_INACTIVE,
    STATE_HEADER_SPACE,
    STATE_BIT_PULSE,
    STATE_BIT_SPACE,
    STATE_TRAILER_PULSE,
    STATE_TRAILER_SPACE,
};

static int ir_nec_decode(struct rc_dev *dev, struct ir_raw_event ev)
{
    struct nec_dec *data = &dev->raw->nec;
    u32 scancode;
    u8 address, not_address, command, not_command;
    bool send_32bits = false;

    if (!(dev->enabled_protocols & RC_BIT_NEC))
        return 0;

    if (!is_timing_event(ev)) {
        if (ev.reset)
            data->state = STATE_INACTIVE;
        return 0;
    }

    switch (data->state) {
    case STATE_INACTIVE:
        if (!ev.pulse)
            break;
        if (eq_margin(ev.duration, NEC_HEADER_PULSE, NEC_UNIT * 2)) {
            data->is_nec_x = false;
            data->necx_repeat = false;
        } else if (eq_margin(ev.duration, NECX_HEADER_PULSE, NEC_UNIT / 2)) {
            data->is_nec_x = true;
        } else {
            break;
        }
        data->count = 0;
        data->state = STATE_HEADER_SPACE;
        return 0;

    case STATE_HEADER_SPACE:
        if (ev.pulse)
            break;
        if (eq_margin(ev.duration, NEC_HEADER_SPACE, NEC_UNIT)) {
            data->state = STATE_BIT_PULSE;
            return 0;
        } else if (eq_margin(ev.duration, NEC_REPEAT_SPACE, NEC_UNIT / 2)) {
            if (dev->keypressed) {
                rc_repeat(dev);
                data->state = STATE_TRAILER_PULSE;
            }
            return 0;
        }
        break;

    case STATE_BIT_PULSE:
        if (!ev.pulse)
            break;
        if (!eq_margin(ev.duration, NEC_BIT_PULSE, NEC_UNIT / 2))
            break;
        data->state = STATE_BIT_SPACE;
        return 0;

    case STATE_BIT_SPACE:
        if (ev.pulse)
            break;
        if (data->necx_repeat && data->count == NECX_REPEAT_BITS &&
            geq_margin(ev.duration, NEC_TRAILER_SPACE, NEC_UNIT / 2)) {
            rc_repeat(dev);
            data->state = STATE_INACTIVE;
            return 0;
        }
        data->bits <<= 1;
        if (eq_margin(ev.duration, NEC_BIT_1_SPACE, NEC_UNIT / 2))
            data->bits |= 1;
        else if (!eq_margin(ev.duration, NEC_BIT_0_SPACE, NEC_UNIT / 2))
            break;
        data->count++;
        data->state = (data->count == NEC_NBITS) ? STATE_TRAILER_PULSE : STATE_BIT_PULSE;
        return 0;

    case STATE_TRAILER_PULSE:
        if (!ev.pulse)
            break;
        if (!eq_margin(ev.duration, NEC_TRAILER_PULSE, NEC_UNIT / 2))
            break;
        data->state = STATE_TRAILER_SPACE;
        return 0;

    case STATE_TRAILER_SPACE:
        if (ev.pulse)
            break;
        if (!geq_margin(ev.duration, NEC_TRAILER_SPACE, NEC_UNIT / 2))
            break;
        address     = bitrev8((data->bits >> 24) & 0xff);
        not_address = bitrev8((data->bits >> 16) & 0xff);
        command     = bitrev8((data->bits >> 8)  & 0xff);
        not_command = bitrev8((data->bits >> 0)  & 0xff);

        if ((command ^ not_command) != 0xff)
            send_32bits = true;

        if (send_32bits) {
            scancode = data->bits;
        } else if ((address ^ not_address) != 0xff) {
            scancode = (address << 16) | (not_address << 8) | command; /* extended */
        } else {
            scancode = (address << 8) | command; /* normal */
        }
        if (data->is_nec_x)
            data->necx_repeat = true;
        rc_keydown(dev, RC_TYPE_NEC, scancode, 0);
        data->state = STATE_INACTIVE;
        return 0;
    }

    data->state = STATE_INACTIVE;
    return -EINVAL;
}

static struct ir_raw_handler nec_handler = {
    .protocols = RC_BIT_NEC,
    .decode    = ir_nec_decode,
};

static int __init ir_nec_decode_init(void)
{
    ir_raw_handler_register(&nec_handler);
    printk(KERN_INFO "IR NEC protocol handler initialized
");
    return 0;
}

static void __exit ir_nec_decode_exit(void)
{
    ir_raw_handler_unregister(&nec_handler);
}
module_init(ir_nec_decode_init);
module_exit(ir_nec_decode_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Mauro Carvalho Chehab");
MODULE_DESCRIPTION("NEC IR protocol decoder");

Device‑tree configuration

The DTS node binds the GPIO line to the driver and supplies the required properties:

gpio-ir-receiver {
    compatible = "gpio-ir-receiver";
    gpios = <&gpio4 19 GPIO_ACTIVE_HIGH>;   // GPIO that carries the IR edge signal
    active_low = <1>;                     // Set to 1 if the receiver inverts the signal (e.g., HX1838)
    linux,rc-map-name = "rc-hx18380-carmp3"; // Mapping table for scancode → input keycode
    allowed_protos = <0x100>;               // Bit mask for NEC (currently unused by driver)
};

Integration and key‑reporting flow

When the IR receiver toggles the GPIO, the interrupt handler records the edge timing with ir_raw_event_store_edge() and immediately invokes ir_raw_event_handle(). The raw handler forwards the timing to the NEC decoder state machine defined in ir_nec_decode(). After the full 32‑bit frame is reconstructed, checksum validation determines whether the frame is normal, extended, or a modified NEC variant. The decoder then reports a key‑down event via rc_keydown(). The rc core translates the scancode to a Linux input event using the mapping table referenced by linux,rc-map-name, making the remote usable by user‑space applications.

This combination of device‑tree configuration, GPIO edge interrupt handling, and the NEC state‑machine decoder provides a complete Linux solution for receiving and interpreting NEC infrared remote controls.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Linuxkernel driverDevice TreeGPIOIR remoteNEC protocol
Liangxu Linux
Written by

Liangxu Linux

Liangxu, a self‑taught IT professional now working as a Linux development engineer at a Fortune 500 multinational, shares extensive Linux knowledge—fundamentals, applications, tools, plus Git, databases, Raspberry Pi, etc. (Reply “Linux” to receive essential resources.)

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.