红外发射:编码并发送遥控命令

2026-09-10 00:00    #ESP32-S3   #ESP-IDF   #外设   #阶段3  

本课对应《DNESP32S3 使用指南(IDF 版)》第 27 章。配套完整工程:examples/22-rmt-ir-transmit。

环境与验收边界: DNESP32S3 V1.2,ESP-IDF v5.5.5;本机 macOS 构建通过。Ubuntu 26、外设接线、屏幕效果和实际测量仍待实板验收。下文输出均为预期格式,不能当作已采集的硬件日志。

本课完成后能做什么

硬件连接与资源

信号或资源对应关系
发射控制GPIO8,经 P3 AIN↔RMT 跳线
驱动级Q2 MOSFET 驱动红外 LED,GPIO 高电平使能
载波38 kHz,约 1/3 占空比
验收接收端第二块板的第 21 课或支持 NEC 的解码设备

以下为正点原子 DNESP32S3 V1.2 原理图的相关页,保留原厂标识;表格用于快速定位,遇到板卡版本差异以实际原理图为准。点击图片可查看引脚与网络名。

DNESP32S3 原理图第 1 页

DNESP32S3 原理图第 2 页

本课信号与数据处理流程

编码与调制分别负责什么

编码决定“一段亮多久、灭多久”,调制决定“亮的时间里以多快的载波闪烁”。本例 nec_encode() 生成 34 个 symbol:一个前导、32 位数据、一个结束 mark;RMT 再把高电平段调制成 38 kHz 脉冲。

GPIO8 只是 MOSFET 的控制信号,不直接承担红外 LED 电流。原理图中 Q2 低侧开关在高电平驱动时导通,所以编码的 mark 使用高电平。这与第 21 课解调接收头的低电平 mark 恰好相反。

从一个命令计算完整帧

地址 0x00、命令 0x45 对应四个字节 00 FF 45 BA。每个字节低位先发;0 用 560/560 μs,1 用 560/1690 μs。发送普通帧结束后等待一秒,再发下一帧。本课发送完整帧,不模拟按住遥控器时的短重复帧。

调用 rmt_transmit() 只是提交发送,并不意味着传输已经完成。代码调用 rmt_tx_wait_all_done(),给出 100 ms 上限;等待完成前不能修改 frame 数组。硬件负责微秒时序,应用只负责较慢的发送周期。

怎样验收“真的发对了”

串口打印 sent 只表示驱动完成传输,不能证明光学信号被正确接收。最直接的验收方法是让第二块开发板运行第 21 课,接收日志应输出 address=00、command=45;也可用支持 NEC 的红外解码设备。

未知型号的家电不一定采用相同的地址、命令,甚至可能使用不同的协议。不要以“电视没有反应”直接判断 RMT 代码错误。手机相机可能看见红外光,但它也不能校验载波频率与编码。

GPIO8 同时是 ADC 输入。此课必须把 P3 从 RV↔AIN 改为 AIN↔RMT,不可把旋钮分压同时接到数字输出上。

工程如何组织

驱动放在 components/BSP/,应用放在 main/main.c。本工程包含 NEC 模块。ESP-IDF 会自动发现项目根目录下的 components/BSP。main 通过 PRIV_REQUIRES BSP 声明组件依赖,INCLUDE_DIRS 则导出编译时使用的头文件搜索路径,不涉及运行时文件查找。

固定引脚通过 CONFIG_LESSON_* 常量定义在本工程的 Kconfig.projbuild 中,构建后可在 sdkconfig 中核对。这些默认值对应本板连线,仅修改配置数字并不能改变硬件连接。

应用代码:入口与处理流程

下面代码直接取自配套工程的 main/main.c,头文件和 BSP 实现在仓库中一并提供。初学时先看 app_main(),再追踪它调用的模块。

 1#include "sdkconfig.h"
 2#include <stdio.h>
 3#include <stdint.h>
 4#include <stdbool.h>
 5#include <string.h>
 6#include <stdlib.h>
 7#include <assert.h>
 8#include "esp_err.h"
 9#include "esp_log.h"
10#include "esp_timer.h"
11#include "freertos/FreeRTOS.h"
12#include "freertos/task.h"
13#include "nec.h"
14#include "driver/rmt_tx.h"
15#include "driver/rmt_encoder.h"
16void app_main(void)
17{
18    rmt_channel_handle_t tx;
19    rmt_tx_channel_config_t c={.gpio_num=CONFIG_LESSON_IR_TX,.clk_src=RMT_CLK_SRC_DEFAULT,
20        .resolution_hz=1000000,.mem_block_symbols=64,.trans_queue_depth=1};
21    ESP_ERROR_CHECK(rmt_new_tx_channel(&c,&tx));
22    rmt_carrier_config_t carrier={.frequency_hz=38000,.duty_cycle=0.333f};
23    ESP_ERROR_CHECK(rmt_apply_carrier(tx,&carrier));
24    rmt_encoder_handle_t encoder;
25    rmt_copy_encoder_config_t copy={};
26    ESP_ERROR_CHECK(rmt_new_copy_encoder(&copy,&encoder));
27    ESP_ERROR_CHECK(rmt_enable(tx));
28    rmt_symbol_word_t frame[34];
29    size_t n=nec_encode(0x00,0x45,frame);
30    rmt_transmit_config_t send={.loop_count=0};
31    while(1) {
32        ESP_ERROR_CHECK(rmt_transmit(tx,encoder,frame,n*sizeof(frame[0]),&send));
33        ESP_ERROR_CHECK(rmt_tx_wait_all_done(tx,100)); // 完成前不能改写 frame。
34        ESP_LOGI("ir_tx","sent NEC address=00 command=45");
35        vTaskDelay(pdMS_TO_TICKS(1000));
36    }
37}

BSP 关键实现:NEC

以下是本课对应驱动的完整源文件。对外声明见 nec.h。

 1#include "nec.h"
 2static bool near(unsigned actual,unsigned expected)
 3{
 4    return actual>=expected*3/4 && actual<=expected*5/4;
 5}
 6size_t nec_encode(uint8_t address,uint8_t command,rmt_symbol_word_t *out)
 7{
 8    uint8_t b[]={address,(uint8_t)~address,command,(uint8_t)~command};
 9    out[0]=(rmt_symbol_word_t){.level0=1,.duration0=9000,.level1=0,.duration1=4500};
10    for(int bit=0;bit<32;bit++)
11        out[bit+1]=(rmt_symbol_word_t){.level0=1,.duration0=560,.level1=0,
12            .duration1=(b[bit/8]&(1U<<(bit%8))) ? 1690 : 560};
13    out[33]=(rmt_symbol_word_t){.level0=1,.duration0=560,.level1=0,.duration1=1000};
14    return 34;
15}
16nec_result_t nec_decode(const rmt_symbol_word_t *s,size_t n,uint8_t *address,uint8_t *command)
17{
18    // 解调接收头输出低电平表示红外载波存在,极性与发射端相反。
19    if(n<2 || s[0].level0!=0 || s[0].level1!=1 || !near(s[0].duration0,9000))
20        return NEC_INVALID;
21    if(near(s[0].duration1,2250))
22        return s[1].level0==0 && near(s[1].duration0,560) ? NEC_REPEAT : NEC_INVALID;
23    if(n<34 || !near(s[0].duration1,4500)) return NEC_INVALID;
24    uint8_t b[4]={0};
25    for(int bit=0;bit<32;bit++) {
26        rmt_symbol_word_t v=s[bit+1];
27        if(v.level0!=0 || v.level1!=1 || !near(v.duration0,560)) return NEC_INVALID;
28        if(near(v.duration1,1690)) b[bit/8] |= 1U<<(bit%8);
29        else if(!near(v.duration1,560)) return NEC_INVALID;
30    }
31    if(s[33].level0!=0 || !near(s[33].duration0,560)) return NEC_INVALID;
32    if((uint8_t)(b[0]^b[1])!=255 || (uint8_t)(b[2]^b[3])!=255) return NEC_INVALID;
33    *address=b[0]; *command=b[2];
34    return NEC_FRAME;
35}

CMake 与中文注释

1# 私有依赖只供本组件源码使用。
2idf_component_register(SRCS "main.c" INCLUDE_DIRS "."
3    PRIV_REQUIRES esp_timer BSP esp_driver_rmt)

每个工程有自己的 project() 名称和 sdkconfig.defaults。VS Code 已关闭普通 CMake 自动配置,C/C++ 语言服务从 build/compile_commands.json 读取编译信息。首次构建前,这个文件还不存在,需要先激活 IDF 并完成构建,无需手工拼接头文件路径。

编译、烧录与观察

 1# 激活 EIM 安装的 IDF;每个新终端都需要激活。
 2source ~/.espressif/tools/activate_idf_v5.5.5.sh
 3# 进入单个示例根目录,不是在 examples 总目录运行。
 4cd ~/mycode/esp32-blog/examples/22-rmt-ir-transmit
 5# 首次配置目标;已有正确配置时不用每次重复 set-target。
 6idf.py set-target esp32s3
 7# 编译会生成 build/compile_commands.json,供 VS Code 使用。
 8idf.py build
 9# 把这个值换成实际连接开发板的端口;macOS 通常为 /dev/cu.*。
10PORT=/dev/ttyUSB0
11idf.py -p "$PORT" flash monitor
12# 串口监视退出:Ctrl+]。

预期输出或屏幕内容(示意;数值、日志前缀和启动时间依现场而变):

1ir_tx: sent NEC address=00 command=45
2# 另一块接收板应显示:
3nec: address=0x00 command=0x45

故障定位与逐项验收

先确认目标为 esp32s3,再检查实际端口、供电,并按本文引脚表核对跳线。编译成功只能说明源码与工具链兼容,模块型号、物理线路和时序仍需实板确认。

遇到通信错误,先检查错误码和对应模块,不要用“错误时返回 0”掩盖异常。文中的预期日志只供阅读程序时参考;实板验收时,应记录实际现象,保存日志和接线照片。

与教材和官方实现对照

对照教材第 27 章正文:保留 GPIO8 发射级;使用新 RMT copy encoder 发送显式 symbol,借助硬件载波调制,避免忙等手动产生载波。

本地验证记录与参考

返回课程目录