
1. 项目概述与核心思路最近在捣鼓一个基于FPGA的小玩意儿想用红外遥控来控制几个LED灯。手头正好有IRM-3638这个红外接收头想着用Verilog写个驱动应该不难结果一脚踩进了坑里折腾了好几天。这事儿让我深刻体会到硬件通信协议这东西真不能想当然尤其是红外这种“眼见为实”的传输方式你以为的“1”和“0”跟芯片理解的“1”和“0”可能完全是两码事。我的核心需求很简单用FPGA模拟一个红外发射端当按下开发板上的某个按键时通过红外发射管发送编码信号对面的IRM-3638接收头收到后解码并点亮对应的LED灯。听起来就是个“按键-编码-发射-接收-解码-点灯”的链条但魔鬼全在细节里。IRM-3638是一个很常见的红外接收解码芯片内部集成了PIN光电二极管、前置放大器、限幅器、带通滤波器、解调器和输出整形电路。它的工作流程是接收到被38kHz常见载频我用的模块是33kHz载波调制的红外光信号后经过内部处理最终输出解调后的数字信号。很多初学者包括之前的我最容易产生的误解是发射端发送一个持续的高电平逻辑1接收端就输出高电平发射端不发光逻辑0接收端就输出低电平。如果你也这么想那恭喜你即将和我一样开始一段“怀疑人生”的调试之旅。实际上对于像IRM-3638这类采用“电平反转”逻辑的接收头其真实行为是当持续接收到对应频率的载波红外信号时其输出引脚为低电平逻辑0当没有接收到有效载波信号时其输出引脚为高电平逻辑1。这个“反直觉”的逻辑是理解整个通信过程的关键也是我最初代码跑不通的根本原因。所以这个项目的Verilog实现核心就围绕两点第一在发射端如何正确地生成被33kHz载波调制的数字信号第二在接收端如何正确解读IRM-3638输出的“反逻辑”信号并将其转化为我们期望的控制动作。下面我就把踩坑后理顺的思路和最终可用的代码详细拆解一遍。2. 红外通信原理与IRM-3638工作机制深度解析2.1 为何需要载波调制红外通信如电视遥控器很少直接使用原始的数字电平亮/灭来传输数据。主要原因有二抗干扰和节能。环境中有大量的红外噪声源如太阳光、白炽灯等它们都包含红外光谱。如果直接控制红外LED的亮灭这些噪声很容易被接收器误判为有效信号。其次让红外LED持续高亮发送数据功耗也很大。因此通用的做法是采用幅移键控ASK调制也称为载波调制。具体来说我们会用一个频率固定的高频方波通常是38kHz本例中是33kHz作为载波。要发送逻辑“0”时就让这个载波信号通过红外LED发射出去LED高速闪烁要发送逻辑“1”时就让红外LED保持熄灭。这样接收端IRM-3638内部有一个带通滤波器其中心频率就设计在33kHz。只有频率接近33kHz的闪烁红外光才能被有效接收和解调环境中的直流或低频红外噪声就被过滤掉了极大地提高了抗干扰能力。同时由于LED是间歇性闪烁而非常亮也降低了平均功耗。2.2 IRM-3638的“反逻辑”输出详解理解了载波调制就能看懂IRM-3638的数据手册了。它的输出逻辑可以这样描述输出低电平0当IRM-3638检测到持续存在的、频率在其中心频率附近如33kHz的调制红外信号时其输出引脚会变为低电平。输出高电平1当IRM-3638没有检测到有效的调制红外信号时其输出引脚会保持高电平通常内部有上拉。这就解释了之前的误区。发射端“发送1”意指我们希望传输的逻辑1实际上是让LED不发光那么接收端IRM-3638检测不到载波因此输出高电平1。发射端“发送0”我们希望传输的逻辑0实际上是让LED以33kHz闪烁那么接收端检测到载波因此输出低电平0。这里存在一个“逻辑层”的转换应用层逻辑我们想要的1 - LED灭 0 - LED闪烁。物理层信号IRM-3638输出的LED灭 - 输出1 LED闪烁 - 输出0。所以从接收端IRM-3638输出的信号到我们最终想得到的控制逻辑需要一次取反操作。这个关系必须捋清否则代码怎么写都是错的。2.3 通信协议框架本例中我们实现的是一个非常简单的“即时命令”传输没有复杂的地址码、数据码和反码。其协议可以定义为空闲状态发射端持续不发光应用层逻辑1。接收端IRM-3638持续输出高电平物理层信号1。按键按下发射端开始持续发送33kHz载波应用层逻辑0。接收端IRM-3638输出变为低电平物理层信号0。按键释放发射端恢复空闲状态不发光。接收端IRM-3638输出恢复高电平。这个协议只传输一个“有/无”按键事件没有区分是哪个按键。在后面的扩展部分我们会讨论如何加入简单的编码来区分多个按键。3. 系统设计与模块划分基于以上分析我们可以将整个FPGA系统划分为几个功能明确的模块。虽然原文代码写在了同一个topirda模块里但从设计和复用的角度拆分开更清晰。3.1 顶层模块Top负责实例化所有子模块连接外部引脚时钟、复位、按键、红外收发、LED和内部信号。它就像项目的总接线图。3.2 按键消抖模块Key_Debounce机械按键在按下和释放时会产生持续数毫秒的抖动电平快速跳变。如果不处理一次按键会被误判为多次。消抖模块的功能就是滤除这些抖动输出一个稳定、干净的按键状态信号。通常采用延时采样法检测到按键状态变化后等待一段时间如20ms如果状态保持稳定则确认按键事件。3.3 红外载波生成模块IR_Carrier_Gen这个模块是发射端的核心。它的任务是产生一个占空比约为1/3高电平8.77us低电平16.53us、频率为33kHz的方波信号作为调制载波。我们需要根据系统主时钟如50MHz来精确分频得到这个信号。3.4 红外信号调制与发送模块IR_Transmitter该模块根据按键状态来自消抖模块决定是否将载波信号输出到红外发射管。逻辑是当有按键按下时允许载波信号通过即输出给发射管当无按键时输出持续低电平让LED熄灭。3.5 红外信号接收与处理模块IR_Receiver这个模块处理来自IRM-3638接收头的信号。如前所述IRM-3638的输出是“反逻辑”的。因此这个模块主要做两件事第一可能需要对输入信号进行同步化处理避免亚稳态第二进行逻辑反转将物理层信号0有载波1无载波转换回应用层逻辑1有按键0无按键。转换后的信号直接用于控制LED。4. Verilog代码实现与逐行解析接下来我们按照模块划分重写并详细解释每一段代码。我将使用系统时钟clk50MHz周期T20ns。4.1 顶层模块top_irdamodule top_irda ( input wire clk, // 50MHz 系统时钟 input wire rst_n, // 低电平有效的全局复位信号 input wire irda_rx, // 来自 IRM-3638 的接收信号 output wire irda_tx, // 连接到红外发射管的发送信号 input wire [2:0] key_in, // 三个按键输入key_in[0]sw1, [1]sw2, [2]sw3 output wire [2:0] led_out // 三个LED输出led_out[0]led_d2, [1]led_d3, [2]led_d4 ); // 内部连线声明 wire key_pressed; // 消抖后的按键状态1表示有按键按下 wire carrier_wave; // 33kHz 载波信号 wire tx_enable; // 发送使能信号由按键状态控制 //--- 实例化按键消抖模块 --- key_debounce u_key_debounce ( .clk (clk), .rst_n (rst_n), .key_raw (|key_in), // 将三个按键“或”起来任一按下即有效 .key_stable (key_pressed) ); //--- 实例化载波生成模块 --- ir_carrier_gen u_ir_carrier_gen ( .clk (clk), .rst_n (rst_n), .carrier (carrier_wave) ); // 发送使能逻辑有按键按下时使能发送 assign tx_enable key_pressed; //--- 实例化发送模块本质是一个与门 --- assign irda_tx tx_enable ? carrier_wave : 1b0; //--- 接收端处理逻辑 --- // IRM-3638输出0表示收到载波有按键1表示空闲无按键 // 我们需要将其反相来控制LED亮1灭0 assign led_out (irda_rx 1b0) ? 3b111 : 3b000; endmodule代码解析顶层模块非常简洁只做连线和简单的逻辑组合。|key_in是按位或操作意思是三个按键中任何一个为低电平按下结果就为1。这样我们将三个按键合并为一个控制信号。发送端irda_tx key_pressed carrier_wave。当按键按下时输出载波否则输出0LED灭。接收端led_out ~irda_rx。因为irda_rx为0代表收到信号按键按下所以我们取反后得到1点亮LED。4.2 按键消抖模块key_debouncemodule key_debounce ( input wire clk, // 50MHz input wire rst_n, input wire key_raw, // 原始的按键输入0表示按下 output reg key_stable // 消抖后的稳定输出1表示有按键按下 ); // 参数定义20ms消抖时间50MHz时钟下需要计数 20ms / 20ns 1,000,000 个周期 parameter DEBOUNCE_TIME 1_000_000; // 20ms 50MHz reg [19:0] counter; // 需要20位计数器 (2^201,048,576 1,000,000) reg key_sync0, key_sync1; // 两级同步寄存器用于消除亚稳态 wire key_negedge; // 按键下降沿检测按下瞬间 //--- 同步化处理将异步的按键信号同步到时钟域 --- always (posedge clk or negedge rst_n) begin if (!rst_n) begin key_sync0 1b1; // 默认按键未按下高电平 key_sync1 1b1; end else begin key_sync0 key_raw; key_sync1 key_sync0; end end // 检测下降沿同步后信号为高上一拍为低表示按键被按下 assign key_negedge (~key_sync1) key_sync0; //--- 消抖计数器逻辑 --- always (posedge clk or negedge rst_n) begin if (!rst_n) begin counter 20d0; key_stable 1b0; end else begin if (key_negedge) begin // 检测到下降沿开始计数 counter 20d0; key_stable 1b0; // 计数期间输出保持为0 end else if (counter DEBOUNCE_TIME) begin // 计数未满继续累加 counter counter 1b1; key_stable 1b0; end else begin // 计数满20ms认为按键已稳定按下输出1 key_stable 1b1; // 计数器可以保持也可以清零这里选择保持最大值直到按键释放 counter DEBOUNCE_TIME; end // 当按键释放同步后信号变高时立即复位输出和计数器 if (key_sync1) begin key_stable 1b0; counter 20d0; end end end endmodule代码解析与注意事项亚稳态处理key_raw是来自外部按键的异步信号直接用来控制计数器非常危险可能引发亚稳态。通过两级D触发器key_sync0,key_sync1进行同步是FPGA设计中的标准做法。边沿检测key_negedge信号只在检测到按键从高到低变化的那个时钟周期为高。这标志着一次按键动作的开始我们用它来启动消抖计数器。计数器设计计数器在检测到下降沿后从0开始计数。在计满20ms之前key_stable输出为0屏蔽了抖动期的波动。计满后才输出1表示确认按键稳定按下。释放处理一旦检测到按键同步信号变高释放立即将key_stable拉低并清零计数器响应迅速。注意这是一个简化版的消抖逻辑适用于本例中“按住即持续发送”的需求。对于需要检测“按下”和“释放”两个事件的场景逻辑会更复杂一些。4.3 红外载波生成模块ir_carrier_genmodule ir_carrier_gen ( input wire clk, // 50MHz input wire rst_n, output reg carrier // 33kHz 载波输出 ); // 参数计算 // 载波频率33kHz - 周期 T_carrier 1 / 33k ≈ 30.303us // 系统时钟周期T_clk 20ns (50MHz) // 一个载波周期需要的时钟数N_total T_carrier / T_clk 30.303us / 20ns ≈ 1515 parameter TOTAL_CYCLES 1515; // 载波总周期计数 // 占空比设定高电平约8.77us低电平约16.53us (来自原文占空比~1/3) // 高电平时钟数N_high 8.77us / 20ns 438.5 ≈ 438 parameter HIGH_CYCLES 438; reg [10:0] counter; // 需要11位计数器 (2^112048 1515) always (posedge clk or negedge rst_n) begin if (!rst_n) begin counter 11d0; carrier 1b0; end else begin if (counter TOTAL_CYCLES - 1) begin counter counter 1b1; end else begin counter 11d0; // 计数到1514后归零 end // 生成载波计数小于HIGH_CYCLES时输出高电平否则输出低电平 if (counter HIGH_CYCLES) begin carrier 1b1; end else begin carrier 1b0; end end end endmodule代码解析与参数计算 这是整个发射端最需要精确计算的部分。任何频率或占空比的偏差都可能导致IRM-3638无法正确解调。总周期数计算50MHz / 33kHz ≈ 1515。这意味着我们需要一个计数器从0数到1514然后归零如此循环就能得到一个周期为1515 * 20ns 30.3us频率为1 / 30.3us ≈ 33kHz的方波。占空比控制原文提到高电平持续约8.77us。8.77us / 20ns 438.5取整为438。因此我们让计数器在0-437期间输出高电平在438-1514期间输出低电平即可得到近似1:2的占空比438:1077≈1:2.46接近1/3。精度问题由于时钟分辨率的限制我们无法生成完美的33kHz和精确的8.77us。1515 * 20ns 30.30us对应频率33.003kHz误差极小完全可接受。占空比的微小偏差通常也不会影响接收。4.4 系统集成与测试要点将上述模块例化到顶层后就完成了整个系统。在硬件测试时需要关注以下几点硬件连接发射管注意红外发射LED的限流电阻。通常5V电源下串联一个100Ω左右的电阻电流约几十mA确保有足够的发射强度。irda_tx引脚直接或通过三极管驱动LED阴极。接收头IRM-3638通常有三个引脚VCC接3.3V或5V、GND、OUT信号输出。OUT引脚需要连接一个上拉电阻通常10kΩ到VCC以确保空闲时输出高电平。本例中FPGA的irda_rx引脚应配置为输入并启用内部弱上拉如果支持。调试方法首先测试发射端用示波器或逻辑分析仪探头测量irda_tx引脚和红外发射管两端。按下按键时应能看到频率为33kHz的方波。注意观察方波的频率和占空比是否与设计相符。然后测试接收端在发射端正常工作时用示波器测量IRM-3638的OUT引脚。按下按键时应看到该引脚从高电平变为低电平释放按键时恢复高电平。这验证了“反逻辑”关系。最后联调确认上述两步后LED应该能随按键正确点亮和熄灭。如果LED行为相反按下灭释放亮说明接收端的逻辑反相处理错了检查assign led_out (irda_rx 1b0) ? 3‘b111 : 3’b000;这行代码。5. 从简单到复杂协议扩展与抗干扰增强上面的实现是一个最基础的“开关”式通信。在实际应用中我们通常需要传输具体的按键值如SW1、SW2、SW3分别控制不同的灯。这就需要定义一套简单的红外编码协议。5.1 定义NEC-like简易协议我们可以借鉴常见的NEC协议格式做一个简化版。一个数据帧包含引导码一个9ms的载波脉冲逻辑0后跟一个4.5ms的空闲逻辑1。用于通知接收端一帧数据开始同时让接收端的自动增益控制电路稳定。用户码可选8位地址用于区分不同设备。本例中可以固定为一个值。数据码8位数据用来表示哪个按键被按下。例如8‘h01代表SW18’h02代表SW28‘h03代表SW3。数据反码8位数据的逐位取反用于校验。结束位一个560us的载波脉冲逻辑0后停止发送。逻辑“0”和“1”采用脉冲位置调制逻辑“0”560us的载波脉冲 560us的空闲。逻辑“1”560us的载波脉冲 1690us的空闲。这样不同的按键就会对应一串不同时间间隔的脉冲序列接收端通过测量脉冲之间的间隔来解码出是“0”还是“1”进而得到按键值。5.2 发送端编码器实现思路需要新增一个ir_encoder模块。其输入是消抖后的按键编号输出是一个tx_data_valid信号和tx_data信号。当检测到按键事件时ir_encoder启动一帧数据的发送。内部需要一个状态机依次产生引导码、用户码、数据码、数据反码和结束位的波形。波形生成依赖于一个精确的定时器基于系统时钟计数来控制irda_tx输出载波逻辑0或空闲逻辑1的持续时间。发送过程中tx_data_valid拉高ir_transmitter模块根据tx_data的值0或1来决定输出载波的时间长度。5.3 接收端解码器实现思路需要新增一个ir_decoder模块。其输入是来自IRM-3638的irda_rx信号输出是解码有效的data_valid信号和key_code数据。该模块持续监测irda_rx的下跳沿表示收到载波脉冲开始。检测到下降沿后启动一个高精度定时器测量从本次下降沿到下一个下降沿之间的时间即脉冲之间的空闲时间。根据测量到的时间判断是引导码、逻辑“0”、逻辑“1”还是结束位。如果是引导码则开始接收后续的32位数据用户码16位数据码8位数据反码8位。接收完成后校验数据反码是否正确。如果正确则输出data_valid和key_code。顶层模块根据key_code的值分别控制不同的LED。5.4 增加抗干扰措施重复码处理许多红外协议在按键持续按住时不会重复发送完整数据帧而是发送一个简短的“重复码”。解码器需要能识别并处理这种重复码以实现长按功能。接收超时在解码状态机中如果在一定时间内如几十毫秒没有收到预期的边沿应复位状态机避免因干扰脉冲导致解码器“卡死”。信号滤波在irda_rx信号进入解码器之前可以增加一个数字滤波器例如连续采样多次只有多次采样值一致才采纳以滤除窄毛刺干扰。6. 常见问题、调试技巧与避坑指南6.1 问题1LED状态与按键动作相反现象按下按键LED熄灭松开按键LED点亮。原因没有正确处理IRM-3638的“反逻辑”输出。你可能直接用了irda_rx去控制LED。解决确保接收端逻辑是led ~irda_rx或led (irda_rx 0) ? 1 : 0。6.2 问题2按键控制不灵敏有时无反应现象快速点按偶尔失效或者需要按得很用力、很准。原因消抖时间不合适。20ms是通用值但你的按键特性可能不同。红外发射管或接收头指向不对或距离太远、有遮挡。环境光干扰太强如强烈的日光或白炽灯直射接收头。解决调整消抖时间参数DEBOUNCE_TIME尝试15ms或25ms。确保发射管和接收头正对距离在几厘米到几米内视器件功率而定。避免强光直射接收头或给接收头加上遮光罩。用示波器看接收头输出信号确认在按下按键时输出是干净的低电平没有杂波。6.3 问题3通信距离非常短现象只有把发射管和接收头几乎贴在一起才能工作。原因发射管驱动电流不足。限流电阻太小会烧坏LED太大会导致光强不足。载波频率偏差太大。IRM-3638的带通滤波器有一定带宽如±2kHz如果你的载波是30kHz或36kHz可能落在滤波器边缘导致灵敏度下降。发射管或接收头本身质量或型号问题。解决计算驱动电流假设红外LED正向压降Vf1.2V电源Vcc3.3V期望电流If20mA。则限流电阻 R (Vcc - Vf) / If (3.3-1.2)/0.02 105Ω。可以选用100Ω电阻。务必确认LED的参数精确测量载波用示波器测量irda_tx引脚的实际频率和占空比调整ir_carrier_gen模块中的TOTAL_CYCLES和HIGH_CYCLES参数使其尽可能接近33kHz和1/3占空比。尝试更换不同品牌或批次的接收头。6.4 问题4加入编码协议后解码不稳定现象能识别到按键但解码出的键值经常错误。原因定时器精度不够。用50MHz时钟测量ms/us级时间计数器的位宽要足够并且比较值要计算准确。解码状态机的容错窗口设置得太窄。由于晶振误差和传输延迟测量到的时间会有微小偏差。没有处理连发或干扰脉冲。解决放宽时间容限不要用绝对相等来判断时间而要用一个范围 T_min T_max。例如判断560us的逻辑“0”空闲时间可以设定范围在400us到700us之间都认为是有效的。增加校验使用数据反码进行校验只有校验通过的数据帧才被采纳。实现超时复位在等待脉冲边沿的状态中加入超时机制例如等待超过5ms还没来自动复位到初始状态准备接收下一帧。6.5 调试必备工具与技巧示波器/逻辑分析仪这是调试硬件通信的“眼睛”。一定要用它来看irda_tx和irda_rx引脚上的真实波形。看载波频率对不对看接收头的输出是否干净看编码波形的时间间隔是否准确。FPGA在线调试工具如Xilinx的ILA集成逻辑分析仪或Intel的SignalTap。可以将内部的关键信号如状态机状态、计数器值、解码后的数据引出来观察这对于调试复杂的解码状态机至关重要。分段调试法不要一次性写完所有代码。先确保载波生成正确再测试发送端能否受按键控制接着测试接收端能否正确反相控制LED。最后再叠加复杂的编码解码逻辑。每完成一步用工具验证一步。模拟接收在编写解码器时可以先用一个Verilog testbench模拟一个发送端产生标准的编码波形送给解码器模块进行仿真确保解码逻辑在理想环境下是正确的再下载到板卡进行硬件调试。红外通信的Verilog实现是一个非常好的数字逻辑设计练习项目它涵盖了时钟分频、状态机、边沿检测、抗干扰设计、协议解析等多个知识点。从最基础的“点对点开关”到完整的“编解码通信系统”每一步的深入都能带来新的收获。最关键的就是一开始要理解那个“反逻辑”关系这是打开红外通信大门的钥匙。希望这篇详细的拆解能帮你少走弯路一次成功。