1. 项目概述与核心思路

最近在捣鼓一个基于FPGA的小玩意儿,想用红外遥控来控制几个LED灯。手头正好有一套红外对射模块,发射端是个红外LED,接收端用的是IRM-3638这个解码芯片。听起来挺简单对吧?不就是按个键,发个信号,对面收到就亮灯嘛。我一开始也是这么想的,结果在Verilog实现上卡了好几天,差点没把自己给整郁闷了。问题就出在,我把红外通信想得太“数字”了,以为就是简单的“发1收1,发0收0”,完全忽略了红外通信中“载波”这个关键角色。这个坑,我相信很多从纯数字逻辑转向涉及简单模拟信号处理的工程师都可能会踩。

IRM-3638是一个很常见的红外接收头,内部集成了光电二极管、前置放大器、限幅器、带通滤波器、解调器和输出整形电路。它的核心任务,是把被38kHz(常见)或其它频率(如我用的33kHz)载波调制的红外光脉冲信号,解调成干净的数字电平输出。这意味着,发送端不能直接发送高/低电平给红外LED,而是必须用特定频率的载波去“携带”你的数据。对于IRM-3638而言,其逻辑定义往往是反直觉的: 持续接收到对应频率的载波时,它输出低电平;没有载波或载波频率不对时,它输出高电平 。理解这一点,是整个项目成败的第一个关键。

我的项目目标很明确:在FPGA开发板上,用三个物理按键(sw1, sw2, sw3)作为输入,当其中任意一个按键被按下时,通过红外发射管发送信号;接收端的IRM-3638收到信号后,驱动三个LED灯(led_d2, led_d3, led_d4)同时点亮。这实际上实现了一个最简单的“红外遥控灯”功能,是理解红外编解码通信原理的绝佳实践。

2. 红外通信原理与IRM-3638工作机制深度解析

2.1 为何需要载波?抗干扰与提高驱动效率

如果你直接用直流信号驱动红外LED发光来表示“1”,熄灭来表示“0”,会有什么问题?首先,环境中的自然光、日光灯都含有红外成分,会造成极强的干扰,接收端根本无法区分是信号还是噪声。其次,红外LED需要一定的驱动电流才能获得足够的发射功率,使其能在几米甚至十几米的距离被接收到。持续导通的大电流,不仅耗电,还可能损坏LED。

引入载波调制完美解决了这两个问题。 载波,就是一个高频的方波信号(例如33kHz) 。我们要发送的数据信号(称为“基带信号”)通过这个载波进行“调制”。最常见的调制方式是“幅移键控”(ASK),即:发送“0”时,关闭载波;发送“1”时,开启载波。这样,红外LED实际上是在高频闪烁。对于接收端IRM-3638来说,其内部的带通滤波器中心频率就设计在33kHz,只有这个频率附近的信号才能被有效放大和解调,环境中的直流红外噪声和频率不匹配的干扰都被极大地抑制了。同时,LED工作在高频脉冲状态,其平均电流远小于持续导通的电流,既保证了发射强度,又提高了效率和使用寿命。

2.2 IRM-3638的输出逻辑:与直觉相反

这是最容易让人困惑的地方,也是我最初栽跟头的原因。我们习惯性认为:发送“有效信号”时,接收端应该输出“有效电平”(比如1)。但在很多红外接收头,包括IRM-3638的典型应用里,逻辑是反的。

查阅IRM-3638的数据手册(Datasheet),其输出部分通常会说明:当接收到 有效载波 时,输出端 输出低电平 ;当 未接收到有效载波 时,输出端 输出高电平 (通常通过内部上拉电阻实现)。

为什么这样设计?一个合理的解释是与抗干扰有关。在空闲状态(无信号),输出保持高电平。当有载波信号(即有效信号)到来时,才拉低。这样,任何非载波引起的干扰(如瞬间的强光),都不会产生一个低电平脉冲,从而降低了误触发的概率。你可以这样记忆: 载波 = 活动状态 = 输出低电平

因此,在发送端:

  • 当我们要向接收端传递一个 逻辑‘0’ (让接收端输出高电平)时,我们应该 持续发送载波
  • 当我们要向接收端传递一个 逻辑‘1’ (让接收端输出低电平)时,我们应该 停止发送载波

在我的简单演示项目中,逻辑被简化了: 只要按键按下,就持续发送载波 。那么根据上述规则,接收端IRM-3638在收到载波后,会输出低电平。我的代码里用这个低电平去控制LED点亮(低电平有效或经反相后驱动)。

2.3 载波参数计算:从系统时钟到精准脉冲

我的FPGA板载晶振是50MHz,周期为20ns。IRM-3638要求(或适配)的载波频率是33kHz。我们需要用Verilog生成一个尽可能接近33kHz的方波。

  1. 计算载波周期 :33kHz信号的周期 T_carrier = 1 / 33,000 Hz ≈ 30.303 μs。
  2. 计算系统时钟周期数 :N_carrier = T_carrier / T_clk = 30.303 μs / 20 ns ≈ 1515.15。这不是整数,我们需要取整。在代码中,作者使用了 cnt_1315 这个11位计数器,其比较值是1315,这对应的是 26.3μs 周期,即约 38kHz 的载波。这里可能存在一个选择:是使用更常见的38kHz载波,还是数据手册指定的33kHz?有时为了兼容通用遥控器,会选择38kHz。我们需要根据手头IRM-3638的具体型号决定。 这是一个关键细节,频率不对,接收头可能无法解调或灵敏度大幅下降。
  3. 计算占空比 :通常红外载波的占空比是1/3或1/2,以降低平均功耗。例如周期26.3μs,占空比1/3,则高电平时间 ≈ 8.77μs,低电平时间 ≈ 17.53μs。这对应计数器计到约438(1315/3)时翻转。

注意 :务必根据你实际使用的IRM-3638型号的数据手册,确认其中心频率(Center Frequency)和对应的载波周期、占空比要求。这是硬件驱动的基础,绝不能想当然。

3. Verilog代码逐模块详解与实现

下面,结合我最终调试成功的代码,来详细分解每一个模块的设计思路和注意事项。我的系统时钟是50MHz。

module topirda(
    input clk,           // 50MHz 主时钟
    input rst_n,          // 低电平有效的复位信号
    input irda_receive,   // 来自IRM-3638的输出信号
    output irda_send,     // 驱动红外发射管的信号
    input sw1, sw2, sw3,  // 三个低电平有效的按键输入(假设按下为0)
    output led_d2, led_d3, led_d4 // 三个LED灯,低电平点亮
);

3.1 按键消抖与检测模块

机械按键在按下和弹起时,会产生持续数毫秒的抖动,会产生多个边沿,必须进行消抖处理。

// 20ms 消抖计数器
reg [19:0] cnt_20ms; // 50MHz时钟下,计满1,000,000次为20ms (1,000,000 * 20ns = 20ms)
reg key_value;       // 消抖后的稳定键值,0表示有键按下

always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        cnt_20ms <= 20'd0;
    else
        cnt_20ms <= cnt_20ms + 1'b1; // 循环计数
end

always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        key_value <= 1'b1; // 默认无按键按下
    else if (cnt_20ms == 20'hfffff) // 每20ms采样一次按键状态
        // 如果三个按键中任意一个为低电平(按下),则key_value为0
        key_value <= sw1 & sw2 & sw3;
end

设计要点

  • 采样周期选择 :20ms是经验值,能覆盖绝大多数机械按键的抖动时间。计数器 cnt_20ms 自由运行,每到计满值(这里用 20'hfffff 近似代表20ms点)时对按键状态进行一次采样。这种方法比检测到边沿后启动定时器更节省逻辑。
  • 按键逻辑 key_value <= sw1 & sw2 & sw3; 意味着只要 sw1 , sw2 , sw3 中有一个为0(按下), key_value 就为0。这实现了“任意键按下”的逻辑。
  • 寄存器输出 :将消抖后的信号 key_value 锁存在寄存器中,避免输出毛刺。

3.2 33kHz/38kHz载波生成模块

这是驱动红外LED的核心。我们需要产生一个占空比约为1/3的方波。

// 载波周期计数器:以26.3us (38kHz) 为例
parameter CARRIER_CNT_MAX = 11'd1315; // 26.3us / 20ns = 1315
parameter CARRIER_HIGH_CNT = 11'd438; // 1315 / 3 ≈ 438, 高电平时间约8.77us

reg [10:0] cnt_carrier; // 载波周期计数器
reg carrier_wave;       // 生成的载波信号

always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        cnt_carrier <= 11'd0;
    else if (cnt_carrier < CARRIER_CNT_MAX - 1)
        cnt_carrier <= cnt_carrier + 1'b1;
    else
        cnt_carrier <= 11'd0;
end

// 生成载波方波
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        carrier_wave <= 1'b0;
    else if (cnt_carrier < CARRIER_HIGH_CNT)
        carrier_wave <= 1'b1; // 计数小于438,输出高电平
    else
        carrier_wave <= 1'b0; // 计数大于等于438,输出低电平
end

设计要点

  • 参数化设计 :使用 parameter 定义关键参数,方便后期修改频率和占空比。例如,如果要改为33kHz(周期30.3us),只需修改 CARRIER_CNT_MAX = 30.3us / 20ns = 1515
  • 占空比精度 CARRIER_HIGH_CNT 的计算决定了占空比。1/3占空比是常见选择,但有些协议可能要求1/2。务必与接收头要求匹配。
  • 信号质量 carrier_wave 是一个纯净的、周期精确的方波,它将作为调制用的载波。

3.3 数据调制与发送模块

这部分将消抖后的按键信号( key_value )调制到载波上。逻辑是:有按键按下时,发送载波;无按键时,停止发送(输出恒低)。

reg irda_send_r; // 发送数据寄存器

always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        irda_send_r <= 1'b0;
    else if (!key_value) // 有键按下
        irda_send_r <= carrier_wave; // 输出载波
    else
        irda_send_r <= 1'b0; // 无键按下,输出恒定低电平
end

assign irda_send = irda_send_r; // 连接到输出端口

设计要点与常见误区

  • 调制逻辑 irda_send_r <= carrier_wave; 这行代码实现了ASK调制。当 key_value=0 (有按键),红外LED就会以38kHz的频率闪烁;当 key_value=1 (无按键),红外LED常灭。
  • 对应接收逻辑 :根据2.2节的解析,当IRM-3638持续收到这个38kHz的闪烁信号(即按键按下)时,其输出引脚 irda_receive 会变为 低电平 。当它收不到载波(按键松开)时, irda_receive 会变为 高电平
  • 一个关键检查点 :你的红外发射管驱动电路是否正确?FPGA的IO口驱动能力通常只有几十毫安,而红外LED需要更大的瞬间电流(可达100mA)才能有足够的发射距离。通常需要在FPGA输出引脚后接一个三极管(如NPN型的8050)或MOSFET来驱动红外LED,并串联一个限流电阻(如10-100Ω)。 直接连接FPGA引脚到LED,很可能导致亮度不足、距离极短,甚至损坏FPGA引脚。

3.4 接收解码与LED控制模块

接收端逻辑相对简单,因为IRM-3638已经完成了复杂的解调工作,输出了干净的数字电平。

// 将IRM-3638的输出直接分配给三个LED
assign {led_d2, led_d3, led_d4} = (irda_receive == 1'b0) ? 3'b000 : 3'b111;

设计要点

  • 逻辑电平匹配 :代码 (irda_receive == 1'b0) ? 3'b000 : 3'b111 意味着,当 irda_receive 为低电平时,三个LED输出低电平(假设LED是低电平点亮),灯亮。这正好对应了“按键按下 -> 发送载波 -> 接收头输出低电平 -> LED亮”的逻辑链。
  • 反逻辑处理 :如果你的LED电路是高电平点亮,或者你希望“无信号时灯亮,有信号时灯灭”,那么就需要将逻辑取反: assign {led_d2, led_d3, led_d4} = irda_receive ? 3'b111 : 3'b000; 。原项目代码中使用的就是这种反逻辑。 这完全取决于你的硬件电路设计,务必根据原理图调整。
  • 信号稳定性 :IRM-3638的输出在信号稳定时很干净,但在载波刚出现或消失的瞬间可能会有轻微抖动。对于LED控制这种应用无需处理,但如果用于数据通信,则必须进行边沿检测和同步处理。

4. 系统集成、调试与深度问题排查

将上述模块集成后,就构成了完整的 topirda 模块。然而,从代码到稳定工作的系统,还有很长的调试之路。以下是我在调试过程中遇到的核心问题及解决方法。

4.1 硬件连接检查清单

在烧录代码前,必须再三确认硬件连接,很多“不工作”都是硬件问题。

  1. 电源 :确保FPGA开发板、红外发射模块、红外接收模块供电正常且电压匹配(通常是3.3V或5V)。
  2. 接地 :确保FPGA、发射模块、接收模块共地。这是信号参考的基础,未共地会导致信号紊乱。
  3. 发射端
    • FPGA的 irda_send 引脚是否连接到了驱动三极管的基极(通过一个限流电阻,如1kΩ)?
    • 三极管的集电极是否通过一个限流电阻(如47Ω)连接到了红外LED的正极?
    • 红外LED的负极是否连接到三极管的发射极(并接地)?
    • 用手机摄像头初步测试 :肉眼看不见红外光,但手机摄像头能感应到。在暗处,用手机摄像头对准红外LED,按下按键,你应该能看到LED发出微弱的白光或紫光(这是手机CMOS对红外光的成像)。这是最快验证发射电路是否工作的办法。
  4. 接收端
    • IRM-3638的VCC、GND是否接对?
    • 信号输出引脚是否直接连接到FPGA的 irda_receive 输入引脚?通常不需要上拉电阻,因为芯片内部已有上拉。
    • 注意引脚顺序 :IRM-3638常见封装有三个引脚,顺序可能是(从弧形凹槽或正面看) 输出、地、电源 ,也可能是 地、输出、电源 。务必查阅数据手册或商家资料确认,接反会烧毁芯片!

4.2 软件仿真与调试技巧

即使硬件没问题,代码逻辑也可能有误。使用仿真工具(如ModelSim、Vivado Simulator)是必不可少的。

  1. 编写Testbench :模拟时钟、复位和按键输入。观察 irda_send 信号。
    • 在按键按下期间, irda_send 应该是完美的38kHz方波。
    • 检查方波的周期和占空比是否与设计一致(26.3μs周期,高电平8.77μs)。
  2. 检查计数器溢出 :原代码 if(cnt_1315 == 20'hfffff) 用于20ms采样,但 cnt_1315 是11位寄存器,最大值是2047, 20'hfffff 是20位的最大值,两者永远不相等!这会导致 key_value 永远不被更新。 这是一个致命的编码错误 。应该改为 if(cnt_20ms == 20'hfffff) 或者更合理的 if(&cnt_20ms) (检查所有位是否为1)或使用一个特定的计数值(如20‘d999_999,对应20ms)。
  3. 使用内部逻辑分析仪 :像SignalTap II (Intel) 或 ILA (Xilinx) 这样的工具,可以实时抓取FPGA内部信号。这是调试硬件-软件交互问题的神器。
    • 抓取 key_value , carrier_wave , irda_send_r , irda_receive 等信号。
    • 按下按键,观察 irda_send_r 是否出现载波,同时观察 irda_receive 是否在经过一个短暂延时后变低。这个延时就是IRM-3638的响应时间,通常在几百微秒量级。

4.3 常见故障现象与排查表

故障现象 可能原因 排查方法
LED毫无反应 1. 供电或接地问题。
2. 红外发射管未工作。
3. IRM-3638损坏或接线错误。
4. FPGA代码未正确运行或引脚分配错误。
1. 用万用表测量各点电压。
2. 用手机摄像头检查发射管。
3. 更换接收头,核对引脚顺序。
4. 检查FPGA配置是否成功,用逻辑分析仪看 irda_send 信号。
LED常亮或常灭,不受控制 1. irda_receive 引脚电平固定。可能是接收头损坏、电源问题,或FPGA引脚模式设置错误(应设为输入)。
2. 发送端载波频率严重偏离。
1. 测量接收头输出端电压,无信号时应为高电平(如3.3V),用金属物遮挡接收头,电压应有变化。
2. 用示波器测量 irda_send 引脚波形,核对频率和占空比。
控制不灵敏,距离很短 1. 发射管驱动电流不足。
2. 载波占空比不合适。
3. 环境红外干扰太强(如阳光直射)。
4. 发射管与接收头未对准。
1. 减小发射管限流电阻,增加驱动电流(注意不要超过器件最大值)。
2. 尝试调整载波占空比至1/2或1/3,看灵敏度是否有变化。
3. 在较暗环境下测试。
4. 确保发射管正对接收头。
按键松开后LED状态不稳定 1. 按键消抖逻辑有误, key_value 信号抖动。
2. IRM-3638输出在载波消失时有轻微抖动。
1. 用逻辑分析仪观察 key_value 信号,确保其干净稳定。
2. 对于LED应用可忽略;若需稳定,可在接收端对 irda_receive 信号也做一次20ms左右的滤波。

4.4 从演示到真正通信:协议化的思考

本项目只是一个简单的“通断”演示。真正的红外遥控(如NEC、RC5协议)要复杂得多,它们包含了 地址码、命令码、重复码、起始位和停止位 ,并且通过脉冲宽度(如560μs脉冲+560μs间隔代表‘0’,560μs脉冲+1690μs间隔代表‘1’)来编码数据。

如果你想在此基础上实现标准协议,需要:

  1. 发送端 :设计一个状态机,将按键值映射成特定的数据帧(如NEC协议的32位帧),然后用这个数据流去控制 irda_send_r 是否输出载波(即进行ASK调制)。逻辑‘0’和‘1’对应不同的载波发射时长。
  2. 接收端 :IRM-3638输出的是解调后的数据流(即基带信号)。你需要编写一个解码状态机,检测起始位(一个长低脉冲),然后测量后续高电平的持续时间来区分‘0’和‘1’,最后拼装出地址和命令码。
  3. 同步与容错 :需要考虑帧间间隔、重复码处理,以及一定的容错能力。

这个简单的IRM-3638驱动项目,正是理解这一切的基石。它让你搞清楚了最核心的部分:载波是如何生成、调制,并被接收头解调成数字信号的。掌握了这个,再去研究NEC等具体协议,就会豁然开朗。

调试电子系统,尤其是这种涉及数字和简单模拟混合的系统,切忌一头扎进代码里。我的教训就是一开始太“想当然”。正确的顺序永远是: 理解器件原理(数据手册)-> 设计硬件电路(原理图)-> 编写驱动逻辑(代码)-> 仿真验证 -> 硬件调试(仪器测量) 。每一步的扎实,才能换来最后“灯亮”那一刻的顺畅。红外通信虽然简单,但麻雀虽小五脏俱全,它涵盖了时钟分频、信号调制、按键处理、状态控制等多个FPGA基础知识点,是一个非常棒的练手项目。

Logo

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

更多推荐