> 平台:ESP32-C3 + ESP-IDF 5.5.4  
> 工程类型:自动回充原型机  
> 文档日期:2026-08-13  
> 适用读者:嵌入式初学者、移动机器人爱好者、需要移植到其他 MCU 的开发者

## 摘要

本文记录一套已经在两块 ESP32-C3 上完成基本联调的三路红外自动回充定位原型。

充电座一侧放置左、中、右三路红外发射模块,依次发送不同的 NEC 命令;机器人一侧放置左、中、右三个红外接收模块。接收端根据“哪个接收头收到了哪个发射区的编号”,生成左转、右转、直行或原地搜索的控制建议。

这套方案的重点不是测距,也不是直接测出真实方位角,而是用低成本红外器件获得一个离散方向误差,再把它变成机器人能够执行的转向控制量。当前输出中的“15°、20°、35°”是人工标定的转向建议值,不是传感器测得的物理角度。

系统已实现:

- 三路 NEC 红外信标分时发送;
- 三个接收通道同时捕获边沿;
- 软件 NEC 解码及地址、命令反码校验;
- 600 ms 时间窗内的多观测融合;
- 每秒一次中文转向提示;
- `main / app / bsp` 分层;
- 修复接收任务空转触发 ESP32-C3 任务看门狗的问题。

---

## 1. 需求与边界

### 1.1 要完成什么

当机器人位于充电座前方时,系统需要给出以下四种结果之一:

- `转向:原地搜索`
- `转向:左转 N°`
- `转向:右转 N°`
- `转向:直行`

原型阶段先通过串口观察结果,用手移动、旋转接收端模拟机器人。后续再把这个方向建议转换成左右轮速度。

### 1.2 当前系统不能直接完成什么

本方案目前不能独立给出:

- 机器人到充电座的距离;
- 机器人相对充电座的真实几何角度;
- 最后几厘米是否已经接触充电片;
- 路径中是否存在障碍物;
- 仅凭一次红外接收结果完成高精度定位。

因此,正式机器人通常还需要轮速计、碰撞或充电接触检测,条件允许时还可增加 ToF、超声波或视觉传感器。

---

## 2. 系统组成

系统使用两块 ESP32-C3:

| 设备  | 作用  | GPIO4 | GPIO5 | GPIO6 |
| --- | --- | --- | --- | --- |
| 充电座发送端 | 分区广播信标 | 左发射 | 中发射 | 右发射 |
| 机器人接收端 | 判断信号来向 | 左接收 | 中接收 | 右接收 |

完整信号链如下:

```text
充电座 ESP32-C3
       │
       ├─ GPIO4 ─ 左红外发射管 ─ NEC 命令 0x11
       ├─ GPIO5 ─ 中红外发射管 ─ NEC 命令 0x22
       └─ GPIO6 ─ 右红外发射管 ─ NEC 命令 0x33
                    │
                    │ 38 kHz 调制红外光,三路分时发送
                    ▼
机器人左/中/右接收头
       │
       ├─ GPIO4 ─ 边沿中断 ─┐
       ├─ GPIO5 ─ 边沿中断 ─┼─ NEC 解码 ─ 观测融合 ─ 转向建议
       └─ GPIO6 ─ 边沿中断 ─┘
```

发送端的三个方向不是同时发射,而是按“左 → 中 → 右”循环。这样可以避免三套 NEC 波形在空气中重叠后无法解码。

---

## 3. 硬件接线

### 3.1 发送端

三个红外发射模块的信号脚分别连接:

```text
左发射模块 S  -> GPIO4
中发射模块 S  -> GPIO5
右发射模块 S  -> GPIO6
三个模块 +     -> 模块规定的电源
三个模块 -     -> GND
```

这里的 GPIO6 是正常的第三路信号输出,不再把它当作临时 GND 使用。

如果使用的是带限流电阻和驱动器件的成品模块,可以按模块规格连接。若换成裸红外 LED,不应让 GPIO 长时间直接承担大电流,应增加限流电阻,并根据目标距离使用三极管或 MOSFET 驱动。

### 3.2 接收端

三个解调型红外接收模块的信号脚分别连接:

```text
左接收模块 S  -> GPIO4
中接收模块 S  -> GPIO5
右接收模块 S  -> GPIO6
三个模块 +     -> 3.3 V(以实际模块规格为准)
三个模块 -     -> GND
```

接收 GPIO 开启内部上拉。解调接收头通常空闲为高电平,检测到 38 kHz 红外载波时输出低电平包络。

### 3.3 电气注意事项

- 所有模块必须和 ESP32-C3 共地。
- 金属挡板不要触碰模块焊点、排针或裸露导线,以免短路。
- 如果模块明显发烫,应立即断电并核对 VCC、GND、S 的排列,不能仅凭排针外观猜引脚。
- 手机摄像头常能看到红外 LED 发出紫白色亮点,可用来确认“是否在发光”,但不能证明 NEC 时序正确。

---

## 4. 为什么使用 NEC 协议

若只让红外 LED 常亮,接收端最多知道“这里有红外光”,无法分辨左、中、右区域,也容易受到灯光和阳光干扰。

NEC 是常见的遥控协议,使用约 38 kHz 载波,并通过脉冲宽度编码数据。解调型接收头会滤除大部分环境中的缓慢光照变化,输出数字包络,MCU 只需测量高低电平持续时间。

本项目的 32 位数据排列为:

```text
地址        地址反码       命令         命令反码
8 bit       8 bit          8 bit        8 bit
0x42        0xBD           0x11/22/33   对应反码
```

协议约定:

| 字段  | 数值  | 含义  |
| --- | --- | --- |
| 地址  | `0x42` | 本充电座的信标地址 |
| 左区命令 | `0x11` | 信号来自充电座左区 |
| 中区命令 | `0x22` | 信号来自充电座中心区 |
| 右区命令 | `0x33` | 信号来自充电座右区 |

地址和命令都带反码。接收端要求:

```c
(address ^ inverted_address) == 0xFF
(command ^ inverted_command) == 0xFF
```

这能排除大量由干扰或丢边沿造成的错误帧。

### 4.1 一帧的时序

```text
9 ms 引导载波
4.5 ms 空闲
32 个数据位(低位先发)
560 us 结束载波
约 33 ms 帧间空闲
```

每个数据位都有约 560 μs 的载波段:

```text
逻辑 0:560 us 载波 + 560 us 空闲
逻辑 1:560 us 载波 + 1690 us 空闲
```

---

## 5. 软件分层

两个工程采用相同的分层思想:

```text
main/
└── main.c                 只启动应用

components/bsp/
├── bsp_ir_tx.c            发送端 RMT、GPIO、NEC 波形
├── bsp_ir_rx.c            接收端 GPIO 中断、脉宽捕获、NEC 解码
└── include/
    ├── bsp_board.h        引脚定义
    ├── bsp_ir_tx.h
    └── bsp_ir_rx.h

components/app/
├── dock_app.c             任务、队列和业务流程
├── dock_tracker.c         接收端定位与转向算法
└── include/
    └── dock_protocol.h    两端共同遵循的地址、命令
```

各层职责如下:

| 层   | 负责  | 不负责 |
| --- | --- | --- |
| `main` | 启动应用 | 不直接操作 GPIO,不写业务循环 |
| `bsp` | GPIO、RMT、中断、波形捕获和单帧解码 | 不决定机器人往哪转 |
| `app` | 协议含义、状态维护、方向决策和串口输出 | 不依赖具体寄存器 |

这种结构便于把算法移植到 GD32、STM32 等 MCU:更换 BSP 层,APP 层的协议和定位思路可以继续使用。

---

## 6. 发送端实现

### 6.1 为什么发射使用 RMT

ESP32-C3 的 RMT 外设适合生成微秒级脉冲。CPU 只需准备一组符号,硬件就能稳定输出 38 kHz 调制后的 NEC 波形,不必依赖忙等待精确翻转 GPIO。

当前配置为:

```c
#define IR_RESOLUTION_HZ  1000000U   // 1 tick = 1 us
#define IR_CARRIER_HZ       38000U
#define IR_SYMBOL_COUNT         34
```

RMT 分辨率设置为 1 MHz,因此代码中的 `9000`、`560`、`1690` 可以直接理解成微秒。

### 6.2 构造 32 位数据

```c
const uint32_t raw_data = (uint32_t)address |
                          ((uint32_t)(uint8_t)~address << 8) |
                          ((uint32_t)command << 16) |
                          ((uint32_t)(uint8_t)~command << 24);
```

随后从 bit 0 到 bit 31 依次判断。NEC 低位先发,因此不能把发送顺序反过来。

### 6.3 一个 RMT 通道驱动三路发射

当前实现没有为三个发射管各占一个 RMT 通道,而是让一个通道在 GPIO4、GPIO5、GPIO6 之间切换:

```text
等待上一帧结束
     ↓
关闭 RMT 通道
     ↓
把 RMT 输出切到下一个 GPIO
     ↓
把旧 GPIO 固定为低电平
     ↓
重新启用 RMT,发送下一帧
```

这样节省外设资源,也保证任一时刻只有一个区域在发射。

### 6.4 发送调度

应用层使用一张很小的表描述三路信标:

```c
static const beacon_slot_t s_beacon_slots[] = {
    {BSP_IR_EMITTER_LEFT,   0x11},
    {BSP_IR_EMITTER_CENTER, 0x22},
    {BSP_IR_EMITTER_RIGHT,  0x33},
};
```

任务不断遍历这张表,每发完一路额外等待 15 ms。由于 NEC 帧自身长度与尾部空闲时间,一个左中右周期约为 0.33~0.36 s,实际值会随 32 位数据中“1”的数量略有变化。

---

## 7. 接收端实现

### 7.1 为什么接收端没有使用 RMT

完成这个功能不要求更换芯片,也不强制接收端使用 RMT。ESP32-C3 的 GPIO 双边沿中断配合微秒时间戳,足以处理当前三路低速 NEC 信标。

选择 GPIO 中断的原因:

- 原型代码直观,容易理解和移植;
- 三个通道可以用同一套状态机;
- NEC 帧速率低,边沿数量可控;
- 接收模块已经完成 38 kHz 解调,MCU 测的是包络,不需要采样 38 kHz 载波本身。

若以后信号密度更高、CPU 负载更重、需要更强抗抖动能力,可以再改成 RMT RX 或定时器输入捕获。

### 7.2 中断服务函数只记录边沿

三个 GPIO 都配置为任意边沿中断。ISR 中只做三件事:

1. 读取哪个接收头触发;
2. 读取当前电平与 `esp_timer_get_time()` 时间戳;
3. 把事件送进 FreeRTOS 队列。

```c
typedef struct {
    bsp_ir_sensor_t sensor;
    gpio_num_t gpio;
    uint8_t level;
    int64_t timestamp_us;
} edge_event_t;
```

NEC 解码没有放在 ISR 内完成。这样可以避免中断占用过久,减少丢边沿和影响系统其他任务的风险。

### 7.3 每个传感器独立保存捕获状态

```c
typedef struct {
    bool capturing;
    uint8_t current_level;
    int64_t last_edge_us;
    size_t pulse_count;
    ir_pulse_t pulses[72];
} capture_state_t;
```

三个接收头各有一个 `capture_state_t`。某一路正在接收时,不会覆盖另外两路的脉冲数据。

下降沿可能代表一帧开始。后续每次电平变化时,用当前时间减去上一次边沿时间,得到上一段高电平或低电平持续时间。

如果高电平持续超过 7000 μs,认为一帧已经结束并开始解码。

### 7.4 NEC 解码容差

实际模块、任务调度和光路会让测量值产生误差,因此解码器没有要求时间完全等于协议标称值:

| 波形  | 标称值 | 接收容差 |
| --- | --- | --- |
| 引导低电平 | 9000 μs | 7000~11000 μs |
| 引导高电平 | 4500 μs | 3000~6000 μs |
| 数据低电平 | 560 μs | 300~900 μs |
| 逻辑 0 高电平 | 560 μs | 300~900 μs |
| 逻辑 1 高电平 | 1690 μs | 1200~2200 μs |

解码时按照低位在前的顺序重新组成 32 位整数,再检查地址反码和命令反码。

当前 HW-477 接收模块在个别情况下可能没有完整输出引导码,因此程序保留了一个兼容分支:若已经捕获至少 64 段数据脉冲,可以尝试从数据位直接解码,并只对第一个 mark 放宽范围。这个分支是针对当前原型模块的折中,换接收器后应重新评估是否还需要。

### 7.5 两级队列

系统有两条数据通路:

```text
GPIO ISR
  │  edge_event_t,队列长度 256
  ▼
BSP 接收任务(脉宽捕获 + NEC 解码)
  │  bsp_ir_frame_t,队列长度 24
  ▼
APP 任务(区域记录 + 转向计算 + 串口输出)
```

第一条队列隔离中断和解码;第二条队列隔离硬件解码和应用决策。即便以后更换接收实现,定位算法仍只需要接收“传感器编号、地址、命令、时间戳”。

---

## 8. 三路定位算法

### 8.1 一条有效观测包含两个方向信息

一帧有效数据同时告诉系统:

1. `sensor`:机器人左、中、右哪个接收头看到了信号;
2. `zone`:充电座左、中、右哪个发射管发出了这帧。

只看其中一个信息会浪费另一半空间关系。例如:

- 中接收头收到左区:机器人更可能需要向右修正;
- 左接收头收到中心区:目标出现在车头左侧,更可能需要向左修正;
- 中接收头收到中心区:基本对准,可以直行。

### 8.2 时间窗

发送端一轮左中右约 0.33 s。接收端保留最近 600 ms 的观测:

```c
#define SIGNAL_VALID_US 600000
```

这使一次决策通常能包含完整的一轮信标,又不会让几秒前的旧数据长期影响当前方向。

每个观测的状态可以表示为:

```text
last_seen_us[接收头][发射区]
```

它是一个 3 × 3 的时间戳矩阵。

### 8.3 转向模型

定义右转为正、左转为负:

```text
发射区修正 Z:左区 +20°,中区 0°,右区 -20°
接收头修正 S:左头 -15°,中头 0°,右头 +15°
```

一条观测的控制建议为:

```text
u(sensor, zone) = Z(zone) + S(sensor)
```

九种组合如下:

| 接收头 \ 发射区 | 左区 `+20` | 中区 `0` | 右区 `-20` |
| --- | --- | --- | --- |
| 左接收头 `-15` | 右转 5° | 左转 15° | 左转 35° |
| 中接收头 `0` | 右转 20° | 直行  | 左转 20° |
| 右接收头 `+15` | 右转 35° | 右转 15° | 左转 5° |

在 600 ms 内有多条有效观测时,取它们的平均值:

```text
turn = (u1 + u2 + ... + un) / n
```

最后做两个限制:

- 最大转向量限制为 ±35°;
- 绝对值不超过 3° 时归零,输出直行,防止在中心附近左右抖动。

### 8.4 为什么这不是角度测量

当前系统没有测量光强,也没有三角测量,更不知道机器人与信标的距离,所以无法从几何关系唯一解出真实夹角。

这里的“度数”是给运动控制器的离散控制强度,可以理解成:

```text
5°   = 很小修正
15°  = 小幅修正
20°  = 中等修正
35°  = 强修正
```

如果希望术语更严谨,后续可以把变量从 `turn_degrees` 改名为 `steering_command`,输出改为 `左转强度 15`。若确实需要真实角度,应使用经过标定的多接收器光强模型、编码器与机器人运动模型,或加入视觉定位。

### 8.5 为什么只每秒输出一次

红外帧会连续到达。逐帧打印既刷屏,又会让人误以为方向一直剧烈变化,还会增加串口和任务负担。

应用层仍持续接收和更新数据,但只每 1 s 计算并显示一次当前结果:

```c
#define STATUS_PERIOD_US 1000000
```

该限制只影响日志,不会降低底层接收频率。

---

## 9. 一次看门狗故障的原因与修复

早期版本曾出现:

```text
task_wdt: Task watchdog got triggered
IDLE (CPU 0) did not reset the watchdog
CPU 0: bsp_ir_rx
```

当时接收任务调用:

```c
xQueueReceive(queue, &event, pdMS_TO_TICKS(2));
```

工程的 FreeRTOS tick 频率为 100 Hz,即一个 tick 为 10 ms。`pdMS_TO_TICKS(2)` 经过整数换算后得到 0,于是 `xQueueReceive()` 根本不阻塞。接收任务优先级较高,会在单核 ESP32-C3 上持续空转,IDLE 任务得不到运行机会,最终触发任务看门狗。

修复为:

```c
#define EDGE_QUEUE_WAIT_MS 10
xQueueReceive(queue, &event, pdMS_TO_TICKS(EDGE_QUEUE_WAIT_MS));
```

这样无事件时至少阻塞一个 tick,CPU 能调度 IDLE 和其他任务。实机连续观察约 49 秒未再次触发该错误,并成功得到有效转向输出。

这个问题说明:使用 `pdMS_TO_TICKS()` 时必须知道系统 tick 频率。小于一个 tick 的延时可能变成 0,而不是“接近一个 tick”。

---

## 10. 光学结构与挡板

三路系统能否区分方向,很大程度取决于光学结构,而不仅是代码。

### 10.1 挡板的作用

如果没有挡板,三个发射管的光束和三个接收头的视场会大面积重叠。此时九个组合可能同时有效,平均后经常恰好接近 0,程序输出“直行”,但这并不代表真的对准了。

挡板用于:

- 限制每个发射管的水平出光范围;
- 限制每个接收头的水平视场;
- 降低相邻通道串扰;
- 让左、中、右区域有可重复的空间边界。

### 10.2 原型推荐尺寸

尺寸要通过实测调整,下面仅作为起点:

| 位置  | 建议起始值 |
| --- | --- |
| 接收端通道深度 | 15~25 mm |
| 接收端通道宽度 | 10~12 mm |
| 接收端通道高度 | 15~20 mm |
| 发射端隔板前伸 | 10~15 mm |
| 左右通道外偏角 | 约 20~30° |

内表面应使用哑光黑色材料或黑色胶带。明亮金属会反射红外,虽然肉眼看起来“挡住了”,红外光却可能经过反射进入相邻通道。

建议先做三个独立的短 U 形通道,而不是只在模块之间竖一块平板。挡板不要贴住 LED 或接收窗,应让通道边缘略微伸到器件前方。

### 10.3 正确的重叠关系

三个区域不能完全隔绝,也不能大面积重叠:

```text
错误: [左区]   空白   [中区]   空白   [右区]  -> 存在盲区
错误: [------三路大面积重叠------]            -> 无法区分方向
理想: [左区---]
             [---中区---]
                    [---右区]                  -> 边缘少量重叠
```

少量重叠有助于平滑切换,大面积重叠则会损失方向信息。

---

## 11. 实机测试方法

### 11.1 单路电气测试

1. 先只接一个发射模块和一个接收模块。
2. 烧录发送端和接收端程序。
3. 用手机摄像头观察发射管是否在闪烁。
4. 将发射管与接收窗正面对准,距离从 10~30 cm 开始。
5. 确认接收端不再总是输出“原地搜索”。

### 11.2 三路身份测试

依次遮住两路发射,只保留一路:

| 测试  | 预期  |
| --- | --- |
| 只露出左发射 | 接收数据主要归入左区 |
| 只露出中发射 | 接收数据主要归入中心区 |
| 只露出右发射 | 接收数据主要归入右区 |

如果软件目前只输出最终转向、不打印逐帧信息,可在调试阶段临时增加“接收头编号 + 命令”日志;确认后再关闭,避免刷屏。

### 11.3 九宫格组合测试

固定发射端,只移动一个接收头,依次验证第 8.3 节的九种组合。重点不是数值必须完全一致,而是方向符号必须正确。

### 11.4 手持模拟机器人

1. 三个接收头并排固定在同一块板上,保持朝向稳定。
2. 在充电座前约 0.5~1 m 处开始。
3. 向左横移、向右横移以及原地旋转。
4. 每次移动后停留至少 1 s,让 600 ms 历史数据过期。
5. 观察输出是否引导接收板回到中心。

### 11.5 挡板标定

在地面画出充电座中心线,并在多个距离做测试,例如 20、40、60、80 cm。记录每个位置下三个接收头能够收到哪些区域。

建议以表格记录:

| 距离  | 横向偏移 | 机头角度 | 左头收到 | 中头收到 | 右头收到 | 输出  | 是否合理 |
| --- | --- | --- | --- | --- | --- | --- | --- |
| 60 cm | -20 cm | 0°  |     |     |     |     |     |

根据结果逐步调整隔板长度、通道角度和算法中的 `20 / 15 / 35` 参数。一次只改一个变量,否则很难判断是哪项调整产生了效果。

---

## 12. 串口输出的解释

典型输出:

```text
I (...) dock_app: 转向:原地搜索
I (...) dock_app: 转向:左转15°
I (...) dock_app: 转向:直行
I (...) dock_app: 转向:右转20°
```

含义:

- `原地搜索`:最近 600 ms 没有有效的地址 `0x42` 和命令;
- `左转/右转`:存在有效信标,当前融合结果偏向该方向;
- `直行`:融合控制量位于 ±3° 死区内;
- 一秒出现一条是设计行为,不代表每秒只接收一帧。

如果始终原地搜索,按以下顺序检查:

1. 发射管是否真的发光;
2. 发射、接收模块的正负极和供电;
3. GPIO4/5/6 是否与软件定义一致;
4. 发射与接收器件是否正面对准;
5. 距离是否太远或环境阳光是否太强;
6. 地址、命令、载波频率和 NEC 时序是否一致;
7. GPIO 是否确实产生边沿;
8. 挡板是否完全挡死光路或产生了强反射。

---

## 13. 后续接入机器人底盘

当前 `turn_degrees` 可以先映射为差速轮速度。设基础前进速度为 `v`,方向控制量为 `u`:

```text
left_motor  = v + K × u
right_motor = v - K × u
```

本项目约定右转为正。如果底盘电机方向定义不同,需要交换符号。

推荐把自动回充分成三个阶段:

### 阶段 A:搜索

最近 600 ms 无信号时,低速原地旋转;一旦发现信标,停止盲目旋转并进入导引阶段。

### 阶段 B:远距离导引

根据红外方向控制量边修正边前进。偏差较大时降低线速度,避免高速冲出信标视场。

### 阶段 C:近距离接触

接近充电座后进一步降低速度。最终不能只用“看到中心信标”判断完成,应使用充电电压、触点开关或电流检测确认已经接触。

可采用简单规则:

```text
无信号             -> 原地搜索
|u| > 20           -> 原地或小半径转向
3 < |u| <= 20      -> 慢速前进并差速修正
|u| <= 3           -> 直行靠近
检测到充电电压      -> 停车并进入充电状态
```

---

## 14. 参数调节指南

关键参数集中在几个宏中:

| 参数  | 当前值 | 调大后的影响 |
| --- | --- | --- |
| `SIGNAL_VALID_US` | 600000 μs | 更平滑,但旧方向消失更慢 |
| `ZONE_CORRECTION_DEG` | 20  | 更看重充电座发射分区 |
| `SENSOR_CORRECTION_DEG` | 15  | 更看重机器人接收头位置 |
| `MAX_TURN_DEG` | 35  | 允许更激进的最大转向 |
| `STRAIGHT_DEADBAND_DEG` | 3   | 中心附近更稳定,但精度下降 |
| `STATUS_PERIOD_US` | 1000000 μs | 串口输出更慢,不影响接收 |
| `FRAME_IDLE_US` | 7000 μs | 分帧需要更长空闲时间 |

推荐调节顺序:

1. 先确认单路 NEC 能稳定收发;
2. 再调整挡板,使三个区域有清晰但略微重叠的边界;
3. 再调 `SIGNAL_VALID_US`,解决偶发跳变或响应过慢;
4. 最后调区域修正、接收头修正和底盘速度系数。

不要用软件权重掩盖严重的光学串扰。若三个接收头在大多数位置都同时收到三种区域,首先应改挡板和摆放。

---

## 15. 当前实现的局限与改进方向

### 15.1 局限

- 使用数字解调接收头,只知道某帧“收到/未收到”,没有连续光强值;
- 目前平均所有有效组合,没有给新观测、更稳定通道或中心区设置不同权重;
- 未根据机器人运动方向预测下一时刻状态;
- 未检测接收队列溢出;
- 原型版本没有做复杂故障恢复;
- 三路发射按顺序工作,更新速度低于三路独立但同步协调的高级方案;
- 阳光、镜面地板和附近遥控器仍可能影响红外链路。

### 15.2 可行改进

- 对每个通道统计最近多轮成功率,用成功率而不是单一时间戳加权;
- 为不同观测组合做实测标定表,而不是只用两个常量相加;
- 增加连续多次一致才切换方向的滞回逻辑;
- 增加丢帧、队列满和非法帧计数,调试时按低频输出;
- 将接收改为 RMT RX 或定时器输入捕获,降低高负载下的软件抖动;
- 增加第二种近距离信标或充电接触检测,完成两阶段回充;
- 使用 3D 打印哑光黑色光学通道,使批次和安装角度更一致;
- 给左右轮加入速度闭环,避免同一 PWM 下两轮速度不一致。

---

## 16. 移植到 GD32、STM32 或其他 MCU

移植时无需照搬 ESP-IDF API,只要保留相同的数据边界。

发送端需要:

- 一个能输出约 38 kHz PWM 的定时器;
- 一个微秒级定时器或 DMA/输出比较,用来门控载波;
- 三个 GPIO 或定时器通道选择逻辑;
- 完全相同的地址、命令、位顺序和 NEC 脉宽。

接收端需要:

- 三个 GPIO 双边沿外部中断,或三个定时器输入捕获通道;
- 1 μs 左右分辨率的单调递增时间基准;
- 中断安全的环形缓冲区或队列;
- 三份独立捕获状态;
- 在主循环/任务中执行 NEC 解码和 3 × 3 时间窗融合。

建议保留以下平台无关的数据结构:

```c
typedef struct {
    uint8_t sensor;          // 左、中、右接收头
    uint8_t address;
    uint8_t command;
    uint64_t received_at_us;
} ir_frame_t;
```

硬件层只负责产生 `ir_frame_t`,应用层只消费它。这样更换 MCU 时,不需要重写定位逻辑。

---

## 17. 对实现逻辑的简化伪代码

### 17.1 发送端

```text
初始化三个 GPIO
初始化一个 1 MHz RMT TX 通道
开启 38 kHz、33% 占空比载波

循环:
    切换到左发射 GPIO
    发送 NEC(0x42, 0x11)
    等待 15 ms

    切换到中发射 GPIO
    发送 NEC(0x42, 0x22)
    等待 15 ms

    切换到右发射 GPIO
    发送 NEC(0x42, 0x33)
    等待 15 ms
```

### 17.2 接收 BSP

```text
GPIO 任意边沿中断:
    读取 sensor、level、timestamp
    放入边沿队列

BSP 任务:
    等待边沿队列,至少阻塞 1 个 FreeRTOS tick
    更新对应 sensor 的脉宽数组
    若空闲超过 7 ms:
        尝试 NEC 解码
        校验地址和命令反码
        成功则把 frame 交给 APP
```

### 17.3 接收 APP

```text
收到 frame:
    如果 address != 0x42:忽略
    将 command 映射为左/中/右区
    更新 last_seen_us[sensor][zone]

每 1 秒:
    找出最近 600 ms 的所有观测
    如果没有:输出原地搜索
    否则:
        对每个观测计算 zone_turn + sensor_turn
        求平均
        限制在 -35 到 +35
        在 ±3 内归零
        输出左转、右转或直行
```

---

## 18. 验收标准

在把该原型接入电机之前,建议至少达到以下条件:

- [ ] 两块 ESP32-C3 连续运行 10 分钟无看门狗复位;
- [ ] 每个发射管都能被正确识别成自己的区域命令;
- [ ] 三个接收头均能独立接收;
- [ ] 挡住任意两路后,剩余一路的方向判断符合九宫格表;
- [ ] 无信号后约 0.6~1.6 s 内进入原地搜索;
- [ ] 居中时大多数输出为直行,不持续左右跳变;
- [ ] 左右偏移时,输出能把机器人引回中心,而不是越偏越远;
- [ ] 强环境光下失败时能够安全停止或搜索,不盲目前进;
- [ ] 最终靠站使用独立的充电接触信号确认完成。

---

## 19. 总结

这个原型把自动回充方向引导拆成了三个清晰问题:

1. 发送端用 NEC 编码告诉机器人“光来自充电座哪个区域”;
2. 接收端用三个物理接收头告诉算法“光落在机器人车头哪个位置”;
3. 定位层把二者组合成一个简单、可标定的转向控制量。

它的优势是成本低、计算量小、无需更换 ESP32-C3,也容易移植;限制是只能提供粗方向信息,效果高度依赖挡板和安装标定。对于原型机,它适合先验证“能否找到并对准充电座”。真正进入自动驾驶和可靠充电阶段后,应再加入底盘闭环、近距离接触检测和更完善的状态机。

最重要的工程经验有三点:

- 红外定位首先是光学结构问题,其次才是算法问题;
- 输出中的角度是控制建议,不要误当作真实测量角;
- RTOS 延时必须结合 tick 频率检查,非阻塞的高优先级死循环会饿死 IDLE 任务并触发看门狗。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐