ESP32-C3 三路红外自动回充定位系统:从 NEC 信标到转向决策
> 平台: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 任务并触发看门狗。
更多推荐



所有评论(0)