文章目录
- [1. 项目背景](#1. 项目背景)
- [2. 硬件连接与外设选择](#2. 硬件连接与外设选择)
- [3. 为什么使用 TIMER 输入捕获](#3. 为什么使用 TIMER 输入捕获)
- [4. TIMER1_CH2 初始化](#4. TIMER1_CH2 初始化)
- [5. RT-Thread 下的中断处理](#5. RT-Thread 下的中断处理)
- [6. 脉宽记录与调试缓冲](#6. 脉宽记录与调试缓冲)
- [7. RC5 协议解码](#7. RC5 协议解码)
- [8. NEC 协议解码](#8. NEC 协议解码)
- [9. 遥控器按键映射](#9. 遥控器按键映射)
- [10. ISR 与线程解耦](#10. ISR 与线程解耦)
- [11. 与天气时钟的联动接口](#11. 与天气时钟的联动接口)
- [12. 快捷城市切换](#12. 快捷城市切换)
- [13. 9 位城市 ID 输入](#13. 9 位城市 ID 输入)
- [14. 9 位城市码显示城市名](#14. 9 位城市码显示城市名)
- [15. 显示开关与背光硬件限制](#15. 显示开关与背光硬件限制)
- [16. MSH 调试命令](#16. MSH 调试命令)
- [17. 构建系统处理](#17. 构建系统处理)
- [18. 调试过程中的典型问题](#18. 调试过程中的典型问题)
-
- [18.1 模块启动但没有按键](#18.1 模块启动但没有按键)
- [18.2 有边沿但解不出协议](#18.2 有边沿但解不出协议)
- [18.3 数字键映射错误](#18.3 数字键映射错误)
- [18.4 输入城市码后显示数字而不是城市名](#18.4 输入城市码后显示数字而不是城市名)
- [18.5 输入城市码时缺少反馈](#18.5 输入城市码时缺少反馈)
- [19. 启用 hwtimer/pwm 的取舍](#19. 启用 hwtimer/pwm 的取舍)
- [20. 构建方法](#20. 构建方法)
- [21. 总结](#21. 总结)
1. 项目背景
上一篇博文介绍了天气时钟的设计与实现。本文记录在 GD32VW553H-EVAL 开发板上为天气时钟程序增加红外遥控功能的过程。系统基于 RT-Thread,红外接收端使用 PB11 引脚和 TIMER1_CH2 输入捕获,支持 RC5 与 NEC 两类红外协议,并将遥控器按键与天气时钟功能联动:
- 数字键快速切换常用城市
0键恢复公网 IP 自动定位城市- 电源键切换 ILI9341 显示状态,并说明当前硬件背光常亮限制
HOME键进入 9 位城市 ID 输入模式- 输入城市 ID 时屏幕实时显示已输入数字,避免输错
- 输入中国天气网 9 位城市码后切换到指定城市
这个功能看似只是"加一个遥控器",但对嵌入式系统设计人员来说,里面涉及多个典型问题:
- 定时器输入捕获
- 红外协议脉宽解码
- ISR 与线程之间的数据交接
- RT-Thread MSH 调试命令
- 裸机样例到 RTOS 工程的移植
- 外部输入与业务模块解耦
- 网络天气应用中的城市切换状态管理
- 小屏幕交互中的输入反馈设计
主要代码文件:
text
applications/ir_remote.c
applications/weather_clock.c
applications/SConscript
参考裸机样例:
text
D:\课程\开发板试用\GD32VW55x\Projects\17_IFRP
2. 硬件连接与外设选择
裸机样例 17_IFRP 使用的是 GD32VW553H-EVAL 板载红外收发资源,其中红外接收输入为:
c
#define IR_TIMER TIMER1
#define IR_TIMER_CH TIMER_CH_2
#define IR_GPIO_PORT GPIOB
#define IR_GPIO_PIN GPIO_PIN_11
#define IR_GPIO_AF GPIO_AF_1
移植到 RT-Thread 工程后保持一致:
c
#define IR_TIMER TIMER1
#define IR_TIMER_CLK RCU_TIMER1
#define IR_TIMER_IRQn TIMER1_IRQn
#define IR_TIMER_CH TIMER_CH_2
#define IR_GPIO_PORT GPIOB
#define IR_GPIO_CLK RCU_GPIOB
#define IR_GPIO_PIN GPIO_PIN_11
#define IR_GPIO_AF GPIO_AF_1
PB11 配置为 TIMER1_CH2 复用输入,通过双边沿输入捕获获取红外接收头输出波形的高低电平持续时间。
红外接收头通常输出解调后的数字波形,不需要 MCU 处理 38kHz 载波。MCU 只需要测量高低电平脉宽即可。
3. 为什么使用 TIMER 输入捕获
红外协议本质上是脉宽编码。例如 NEC 协议的典型时序:
text
引导码:低电平约 9ms,高电平约 4.5ms
数据 0:低电平约 560us,高电平约 560us
数据 1:低电平约 560us,高电平约 1.69ms
如果使用普通 GPIO 中断,再在中断中读取 tick,分辨率和抖动都不理想。RT-Thread tick 通常是 1ms 量级,不适合区分 560us 与 1.69ms。
因此使用 TIMER 输入捕获更合理:
- 硬件记录边沿时间
- 软件只计算相邻捕获值差值
- 可以达到微秒级脉宽测量
- 中断处理逻辑较短
当前定时器预分频:
c
#define IR_TIMER_PRESCALER 159U
初始化后会根据 APB1 时钟计算计数频率:
c
g_timer_clk_khz = ir_timer_counter_clk_get() / 1000;
然后用这个频率把协议的微秒阈值换算成 timer tick。
4. TIMER1_CH2 初始化
红外初始化函数为 ir_remote_hw_init()。核心步骤如下。
首先打开 GPIO 和 TIMER 时钟:
c
rcu_periph_clock_enable(IR_GPIO_CLK);
rcu_periph_clock_enable(IR_TIMER_CLK);
PB11 配置为复用功能:
c
gpio_mode_set(IR_GPIO_PORT, GPIO_MODE_AF, GPIO_PUPD_NONE, IR_GPIO_PIN);
gpio_output_options_set(IR_GPIO_PORT, GPIO_OTYPE_PP,
GPIO_OSPEED_25MHZ, IR_GPIO_PIN);
gpio_af_set(IR_GPIO_PORT, IR_GPIO_AF, IR_GPIO_PIN);
配置 TIMER1:
c
timer_deinit(IR_TIMER);
init.prescaler = 0;
init.alignedmode = TIMER_COUNTER_EDGE;
init.counterdirection = TIMER_COUNTER_UP;
init.period = 65535;
init.clockdivision = TIMER_CKDIV_DIV1;
timer_init(IR_TIMER, &init);
timer_prescaler_config(IR_TIMER, IR_TIMER_PRESCALER, TIMER_PSC_RELOAD_NOW);
配置 CH2 双边沿输入捕获:
c
ic.icpolarity = TIMER_IC_POLARITY_BOTH_EDGE;
ic.icselection = TIMER_IC_SELECTION_DIRECTTI;
ic.icprescaler = TIMER_IC_PSC_DIV1;
ic.icfilter = 0;
timer_input_capture_config(IR_TIMER, IR_TIMER_CH, &ic);
开启中断:
c
timer_interrupt_flag_clear(IR_TIMER, TIMER_INT_UP | IR_TIMER_INT_FLAG_CH);
timer_interrupt_enable(IR_TIMER, IR_TIMER_INT_CH);
eclic_irq_enable(IR_TIMER_IRQn, 1, 1);
timer_enable(IR_TIMER);
5. RT-Thread 下的中断处理
TIMER1 中断函数如下:
c
void TIMER1_IRQHandler(void)
{
rt_interrupt_enter();
if (timer_interrupt_flag_get(IR_TIMER, IR_TIMER_INT_FLAG_CH) != RESET)
{
timer_interrupt_flag_clear(IR_TIMER, IR_TIMER_INT_FLAG_CH);
capture = timer_channel_capture_value_register_read(IR_TIMER, IR_TIMER_CH);
level = gpio_input_bit_get(IR_GPIO_PORT, IR_GPIO_PIN) ? 1 : 0;
...
}
rt_interrupt_leave();
}
在 RT-Thread 中写 ISR 时,必须使用:
c
rt_interrupt_enter();
rt_interrupt_leave();
否则内核无法正确感知中断上下文,可能影响调度、临界区和中断嵌套统计。
ISR 中只做几件事:
- 读取捕获值
- 计算脉宽
- 更新最近脉宽 dump 缓冲
- 调用轻量状态机采样
- 置位"收到完整帧"的标志
不会在中断里打印日志,也不会直接刷新屏幕。
6. 脉宽记录与调试缓冲
为了定位遥控器协议和时序问题,程序保存最近 64 个脉冲:
c
#define IR_PULSE_DUMP_SIZE 64U
static volatile rt_uint16_t g_pulse_dump[IR_PULSE_DUMP_SIZE];
static volatile rt_uint8_t g_level_dump[IR_PULSE_DUMP_SIZE];
在 ISR 中记录:
c
g_pulse_dump[g_pulse_dump_pos] = pulse;
g_level_dump[g_pulse_dump_pos] = previous_level;
g_pulse_dump_pos = (g_pulse_dump_pos + 1) % IR_PULSE_DUMP_SIZE;
MSH 命令 ir_remote_dump 会打印最近脉宽:
text
ir_remote: dump newest pulses, format: level:us
ir_remote: L:9000 H:4500 L:560 H:560 ...
这对于红外协议调试非常关键。只看"是否解码成功"不够,必须能看到实际波形。
7. RC5 协议解码
裸机样例主要实现的是 RC5 协议,因此移植时保留了 RC5 状态机。
RC5 采用曼彻斯特编码,典型半位时间约 900us:
c
#define RC5_T_US 900U
#define RC5_T_TOLERANCE_US 300U
#define RC5_PACKET_BIT_COUNT 13U
程序根据相邻边沿间隔判断 1T 或 2T:
c
if (pulse_length > g_rc5_min_t && pulse_length < g_rc5_max_t)
return 0;
if (pulse_length > g_rc5_min_2t && pulse_length < g_rc5_max_2t)
return 1;
然后根据上一个 bit 和当前边沿方向查表:
c
static const enum rc5_last_bit_type rc5_logic_table_rising_edge[2][2] =
{
{RC5_ZER, RC5_INV},
{RC5_NAN, RC5_ZER},
};
static const enum rc5_last_bit_type rc5_logic_table_falling_edge[2][2] =
{
{RC5_NAN, RC5_ONE},
{RC5_ONE, RC5_INV},
};
完整帧解析后得到:
c
frame->address = (data >> 6) & 0x1F;
frame->command = data & 0x3F;
frame->field_bit = (data >> 12) & 0x01;
frame->toggle_bit = (data >> 11) & 0x01;
如果使用 RC5 遥控器,数字键通常可以直接映射到 command 0 到 9。
8. NEC 协议解码
实际调试时,手上的遥控器不是 RC5,而是 NEC 类协议。因此增加了 NEC 解码。
NEC 阈值定义如下:
c
#define NEC_LEAD_LOW_MIN_US 7000U
#define NEC_LEAD_LOW_MAX_US 11000U
#define NEC_LEAD_HIGH_MIN_US 3000U
#define NEC_LEAD_HIGH_MAX_US 6000U
#define NEC_BIT_LOW_MIN_US 250U
#define NEC_BIT_LOW_MAX_US 950U
#define NEC_BIT_0_HIGH_MIN_US 250U
#define NEC_BIT_0_HIGH_MAX_US 1100U
#define NEC_BIT_1_HIGH_MIN_US 1000U
#define NEC_BIT_1_HIGH_MAX_US 2500U
这里的窗口比理论值更宽,是为了适配实际接收头、遥控器电池电量和计时误差。嵌入式调试中不建议一开始就用非常窄的协议阈值。
NEC 状态机:
c
enum nec_state
{
NEC_IDLE = 0,
NEC_LEAD_HIGH,
NEC_BIT_LOW,
NEC_BIT_HIGH
};
处理流程:
text
NEC_IDLE
|
+-- 检测 9ms 低电平
v
NEC_LEAD_HIGH
|
+-- 检测 4.5ms 高电平
v
NEC_BIT_LOW
|
+-- 检测 560us 低电平
v
NEC_BIT_HIGH
|
+-- 根据高电平宽度判定 0 或 1
NEC 数据是 LSB first:
c
if (bit_is_1)
g_nec_data |= (1UL << g_nec_bit_count);
收到 32 bit 后校验 command 反码:
c
cmd = (g_nec_data >> 16) & 0xFF;
cmd_inv = (g_nec_data >> 24) & 0xFF;
if ((rt_uint8_t)(cmd ^ cmd_inv) != 0xFF)
{
g_decode_error_count++;
nec_reset();
return;
}
9. 遥控器按键映射
当前遥控器的日志如下:
text
power: command=0x0d
1: command=0x01
2: command=0x02
3: command=0x03
4: command=0x04
5: command=0x05
6: command=0x06
7: command=0x07
8: command=0x08
9: command=0x09
0: command=0x00
home: command=0x94
因此 NEC 数字映射首先判断 command 是否为 0 到 9:
c
static int nec_command_to_digit(rt_uint8_t command)
{
if (command <= 9)
return command;
switch (command)
{
case 0x16:
return 0;
...
default:
return -1;
}
}
这里保留了常见 21 键 NEC 遥控器的映射作为兼容,但优先支持当前遥控器的 0x00 到 0x09。
特殊按键:
c
#define IR_NEC_POWER_CMD 0x0DU
#define IR_NEC_HOME_CMD 0x94U
10. ISR 与线程解耦
红外线程由 ir_remote_start() 创建:
c
g_ir_thread = rt_thread_create("ir_remote",
ir_remote_thread,
RT_NULL,
IR_THREAD_STACK_SIZE,
IR_THREAD_PRIORITY,
IR_THREAD_TICK);
参数:
c
#define IR_THREAD_STACK_SIZE 1024
#define IR_THREAD_PRIORITY 24
#define IR_THREAD_TICK 10
ISR 只负责接收和置位标志:
c
g_nec_frame_received = RT_TRUE;
g_rc5_frame_received = RT_TRUE;
线程中再解析并输出日志:
c
if (g_nec_frame_received)
{
...
digit = nec_command_to_digit(command);
...
}
这样设计的好处:
- 中断处理时间短
- 串口输出不阻塞 ISR
- 后续业务处理不会影响边沿捕获
- 与天气模块之间通过函数接口联动
11. 与天气时钟的联动接口
红外模块不直接操作天气模块内部状态,而是调用三个外部接口:
c
extern void weather_clock_remote_digit(int digit);
extern void weather_clock_remote_power(void);
extern void weather_clock_remote_city_id(const char *city_id);
extern void weather_clock_remote_city_input(const char *partial, rt_bool_t active);
数字键调用:
c
weather_clock_remote_digit(digit);
电源键调用:
c
weather_clock_remote_power();
9 位城市码输入完成后调用:
c
weather_clock_remote_city_id(g_city_id_buf);
城市码输入过程中调用:
c
weather_clock_remote_city_input(g_city_id_buf, RT_TRUE);
这种做法比红外模块直接改天气模块全局变量更清晰,后续如果天气模块内部结构调整,红外模块只需要保持接口不变。
12. 快捷城市切换
天气模块中定义了快捷城市表:
c
static const struct wc_city_entry g_city_table[] =
{
{1, "BEIJING", "beijing"},
{2, "SHANGHAI", "shanghai"},
{3, "GUANGZHOU", "guangzhou"},
{4, "SHENZHEN", "shenzhen"},
{5, "SANYA", "sanya"},
{6, "MOHE", "mohe"},
{7, "URUMQI", "wulumuqi"},
{8, "LHASA", "lasa"},
{9, "CHENGDU", "chengdu"},
};
遥控器数字键映射:
text
0: AUTO IP
1: BEIJING
2: SHANGHAI
3: GUANGZHOU
4: SHENZHEN
5: SANYA
6: MOHE
7: URUMQI
8: LHASA
9: CHENGDU
按下数字后,天气模块会更新当前城市、清空旧天气有效标志,并触发刷新:
c
g_selected_city_digit = digit;
rt_strncpy(g_weather.city, city->name_en, sizeof(g_weather.city) - 1);
g_weather.valid = RT_FALSE;
g_city_changed = RT_TRUE;
g_force_refresh = RT_TRUE;
天气查询时使用:
c
/?app=weather.today&weaid=<city_id>&...
例如:
text
weaid=sanya
weaid=mohe
weaid=wulumuqi
weaid=lasa
13. 9 位城市 ID 输入
常用城市表不可能覆盖所有城市,因此增加了 9 位城市码输入功能。
触发方式:
text
按 HOME
输入 9 位数字城市码
例如北京海淀:
text
HOME 1 0 1 0 1 0 2 0 0
红外模块状态:
c
#define IR_CITY_ID_DIGITS 9U
static rt_bool_t g_city_id_input_mode;
static char g_city_id_buf[IR_CITY_ID_DIGITS + 1];
static rt_uint8_t g_city_id_len;
按下 HOME:
c
static void ir_city_id_start(void)
{
g_city_id_input_mode = RT_TRUE;
g_city_id_len = 0;
rt_memset(g_city_id_buf, 0, sizeof(g_city_id_buf));
}
输入数字:
c
g_city_id_buf[g_city_id_len++] = (char)('0' + digit);
g_city_id_buf[g_city_id_len] = '\0';
if (g_city_id_len >= IR_CITY_ID_DIGITS)
{
g_city_id_input_mode = RT_FALSE;
weather_clock_remote_city_id(g_city_id_buf);
}
如果输入过程中按了非数字键,会取消本次输入。按电源键也会取消输入并切换 ILI9341 显示状态。
为了避免用户不知道已经输入了哪些数字,程序还增加了屏幕实时提示。输入状态下,中间提示区域会显示:
text
ID _________
ID 1________
ID 10_______
ID 101______
满 9 位后自动提交并隐藏提示。
红外模块在 HOME、每次数字输入、取消输入时都会通知天气模块:
c
extern void weather_clock_remote_city_input(const char *partial, rt_bool_t active);
HOME 键进入输入模式:
c
static void ir_city_id_start(void)
{
g_city_id_input_mode = RT_TRUE;
g_city_id_len = 0;
rt_memset(g_city_id_buf, 0, sizeof(g_city_id_buf));
weather_clock_remote_city_input(g_city_id_buf, RT_TRUE);
}
每输入一位数字:
c
g_city_id_buf[g_city_id_len++] = (char)('0' + digit);
g_city_id_buf[g_city_id_len] = '\0';
weather_clock_remote_city_input(g_city_id_buf, RT_TRUE);
取消输入:
c
weather_clock_remote_city_input(RT_NULL, RT_FALSE);
天气模块保存输入状态:
c
static volatile rt_bool_t g_city_input_active;
static char g_city_input_buf[10];
渲染消息区时优先显示城市码输入状态:
c
if (g_city_input_active)
wc_draw_city_input_message();
else if (!wifi_ready)
wc_draw_text(45, 207, "WIFI WAIT", 2, C_ERROR);
else if (!g_weather.valid)
wc_draw_text(40, 207, "LIVE DATA WAIT", 1, C_AMBER);
提示文本由 wc_draw_city_input_message() 生成:
c
rt_strncpy(text, "ID ", sizeof(text) - 1);
for (i = 0; i < 9; i++)
{
char ch = g_city_input_buf[i];
text[len++] = ch ? ch : '_';
}
wc_draw_text(45, 207, text, 1, C_AMBER);
这个功能没有全屏刷新,只会触发提示区域局部刷新。对 SPI TFT 来说,这种交互反馈开销很小,但用户体验提升明显。
14. 9 位城市码显示城市名
城市码用于查询,但屏幕上不应该长期显示数字编码。天气接口返回中包含 cityno,例如:
text
cityno=haidian
因此天气解析时做了区分:
- 快捷城市:显示表内英文名,如
SANYA - 9 位城市码:查询用数字 ID,显示优先使用接口返回的
cityno - IP 自动定位:显示接口返回的
cityno
关键逻辑:
c
if (g_selected_city_digit > 0)
{
rt_strncpy(weather->city, wc_selected_city_name(),
sizeof(weather->city) - 1);
}
else if (cityno[0])
{
wc_upper_copy(weather->city, sizeof(weather->city), cityno);
}
这样输入 101010200 查询海淀时,屏幕可以显示:
text
HAIDIAN
而不是一直显示:
text
101010200
15. 显示开关与背光硬件限制
电源键 0x0d 用于切换 TFT 显示状态。软件侧已经给 ILI9341 驱动增加了显示开关接口:
c
void ili9341_display_on(rt_bool_t on)
{
ili9341_write_cmd(on ? ILI9341_CMD_DISPON : ILI9341_CMD_DISPOFF);
rt_pin_write(ILI9341_BL_PIN, on ? PIN_HIGH : PIN_LOW);
}
其中:
text
0x28: ILI9341 DISPOFF
0x29: ILI9341 DISPON
天气模块中电源键处理如下:
c
void weather_clock_remote_power(void)
{
g_display_on = g_display_on ? RT_FALSE : RT_TRUE;
ili9341_display_on(g_display_on);
g_render_cache.valid = RT_FALSE;
g_force_refresh = RT_TRUE;
}
但是结合实际原理图可以看到,当前 LCD 接口的 LED/POWER+ 直接连接到 +3V3,没有经过 MCU GPIO、三极管、MOS 管或 PWM 控制网络:

也就是说:
DISPOFF可以关闭 ILI9341 显示输出,所以屏幕不再显示内容。- 背光 LED 仍然由
+3V3直接供电,所以屏幕仍然发亮。 - 代码中的
ILI9341_BL_PIN即使拉高或拉低,也不会影响这一路硬件背光。
这解释了实际现象:
text
按下电源键后,屏幕内容消失,但屏幕仍然亮。
这不是 ILI9341 命令没有生效,而是硬件没有提供可控背光开关。
如果要真正熄灭背光,需要修改硬件,例如增加低边 N-MOS 控制:
text
LED/POWER- -> N-MOS -> GND
^
|
MCU GPIO
或者增加高边 P-MOS 控制:
text
+3V3 -> P-MOS -> LED/POWER+
^
|
MCU GPIO
后续如果希望支持亮度调节,可以把控制脚接到 PWM 输出,通过占空比调节背光亮度。
当前软件关屏策略仍然有价值:关屏后不继续刷新 LCD 内容,减少 SPI 传输和 LCD 控制器活动;只是不能降低背光 LED 的功耗。
渲染函数中增加保护:
c
if (!g_display_on)
{
WC_LOG("render skipped display off");
return;
}
关屏期间不再向 LCD 写屏幕内容,减少 SPI 传输。
15.1 实际运行效果
下面三张图是天气时钟与红外遥控联动后的实际显示效果。数字键可以快速切换城市,HOME 后输入 9 位城市码可以切换到区县级城市。
拉萨

漠河

海淀
海淀是通过城市ID切换的,海淀的城市ID是101010200。

16. MSH 调试命令
红外模块提供三个命令:
text
ir_remote_start
ir_remote_status
ir_remote_dump
ir_remote_status 输出:
text
ir_remote: status started=1 edges=118 last_pulse=324us level=1 protocol=NEC last_digit=-1 address=0x00 command=0x94 toggle=0 city_input=1 len=3 buf=101 rc5_frames=0 nec_frames=5 errors=0
字段含义:
text
started 模块是否启动
edges 捕获到的边沿数量
last_pulse 最近一次脉宽,单位 us
level 当前边沿后的电平
protocol 最近解出的协议
last_digit 最近数字键,-1 表示不是数字
command 最近命令码
city_input 是否处于 9 位城市码输入模式
len 已输入位数
buf 当前输入缓存
rc5_frames RC5 成功帧数
nec_frames NEC 成功帧数
errors 解码错误计数
当 city_input=1 时,屏幕上也会同步显示 buf 对应的已输入城市码。
ir_remote_dump 输出最近 64 个脉宽:
text
ir_remote: L:9000 H:4500 L:560 H:560 ...
天气模块提供城市表命令:
text
weather_clock_city_map
输出:
text
weather_clock: city digit 0: AUTO IP
weather_clock: city id input: press HOME then 9 digits, e.g. 101010100
weather_clock: city digit 1: BEIJING id=beijing
...
17. 构建系统处理
红外接收直接使用 timer_* 标准外设 API,例如:
c
timer_deinit()
timer_init()
timer_input_capture_config()
timer_interrupt_enable()
但当前 BSP 没有启用 RT_USING_HWTIMER 或 RT_USING_PWM,默认不会编译 gd32vw55x_timer.c,链接时会出现:
text
undefined reference to `timer_deinit'
undefined reference to `timer_init'
undefined reference to `timer_input_capture_config'
解决方法是在 applications/SConscript 中,如果存在 ir_remote.c 且没有启用 hwtimer/pwm,就补入标准外设 timer 源码:
python
if os.path.exists(os.path.join(cwd, 'ir_remote.c')) and \
not (GetDepend('RT_USING_HWTIMER') or GetDepend('RT_USING_PWM')):
src += [os.path.join(cwd, '..', 'packages',
'gd32-riscv-series-latest', 'GD32VW55x',
'GD32VW55x_standard_peripheral', 'Source',
'gd32vw55x_timer.c')]
这样不需要打开完整 hwtimer/pwm 设备框架,也能让当前红外模块正常链接。
18. 调试过程中的典型问题
18.1 模块启动但没有按键
现象:
text
ir_remote: status started=1 last_digit=-1
此时需要看 edges:
text
edges=0
表示 PB11 没有收到边沿,优先检查:
- 红外接收头供电
- 输出脚是否接 PB11
- GND 是否共地
- 接收头输出是否需要上拉
- 遥控器是否有电
18.2 有边沿但解不出协议
现象:
text
edges=118
nec_frames=0
rc5_frames=0
说明硬件基本通了,但协议或时序不匹配。执行:
text
ir_remote_dump
如果看到:
text
L:9000 H:4500
基本就是 NEC。如果是 900us/1800us 交替,更像 RC5。
18.3 数字键映射错误
曾经出现 command=0x08 被识别为数字 4 的问题。原因是通用 21 键遥控器映射表与当前遥控器不同。
解决方法是优先处理当前遥控器的 0x00 到 0x09:
c
if (command <= 9)
return command;
然后再保留通用映射作为 fallback。
18.4 输入城市码后显示数字而不是城市名
最初自定义城市码模式直接把顶部城市名设置为 9 位数字。后来改成查询用城市码,显示用接口返回的 cityno。
这是一个典型的"查询标识"和"显示名称"分离问题。嵌入式 UI 中也应该避免把内部 ID 直接暴露给用户。
18.5 输入城市码时缺少反馈
红外遥控器没有本地显示屏,如果 MCU 屏幕不提示已输入内容,用户很容易输错 9 位城市码。
解决方法是在天气时钟界面的提示区域显示输入进度:
text
ID 101______
输入完成、取消输入或按下电源键后清除提示。这个设计不需要额外页面,也不会打断天气主界面。
19. 启用 hwtimer/pwm 的取舍
本项目没有启用 RT-Thread hwtimer/pwm 设备框架,而是直接操作 TIMER1。
如果启用 RT_USING_HWTIMER:
- 好处是 timer 设备由 RT-Thread 统一管理。
- 标准外设 timer 源码会自动编译。
- 适合普通周期定时和超时回调。
但对当前红外输入捕获来说,hwtimer 设备接口并不一定封装输入捕获功能。同时,如果 hwtimer 驱动也注册并占用 TIMER1,就可能与红外接收冲突。
如果启用 RT_USING_PWM:
- 适合红外发射、背光调光、蜂鸣器等输出功能。
- 但对红外接收没有直接帮助。
因此当前设计选择直接使用 TIMER1_CH2 输入捕获,避免引入额外设备框架。
20. 构建方法
在 BSP 目录下执行:
powershell
scons -j4
成功后生成:
text
rtthread.elf
rtthread.bin
当前工程已经包含天气时钟、WiFi、HTTP、NTP、ILI9341、红外解码等功能,RAM 使用率较高。继续扩展功能时需要注意:
- 控制线程栈大小
- 避免大数组
- 避免在 ISR 中做复杂处理
- 复用缓冲区
- 谨慎引入大型协议库
21. 总结
这个红外遥控功能的关键点不是"收到一个按键",而是完整处理从硬件波形到业务控制的链路:
text
PB11 输入波形
|
TIMER1_CH2 双边沿捕获
|
ISR 计算脉宽
|
RC5/NEC 状态机解码
|
红外线程解析按键
|
调用天气时钟控制接口
|
切换城市、输入城市码、控制显示状态
对嵌入式系统设计人员来说,这个案例体现了几个实用原则:
- 微秒级协议应使用硬件定时器,不依赖系统 tick。
- ISR 中只做最小工作,日志和业务逻辑放到线程。
- 协议调试必须能看到原始脉宽。
- 按键码映射要以实际遥控器为准。
- 输入 ID 和显示名称应分离。
- 多位数字输入必须提供可见反馈,否则遥控器交互很容易误操作。
- 外设模块和业务模块通过明确接口交互,避免直接耦合全局变量。
红外遥控接入后,天气时钟从一个被动显示设备变成了可以远程交互的嵌入式终端,也为后续增加菜单、设置、城市收藏、亮度调节等功能打下了基础。