资讯详情

基于DE2-115的FPGA红外解码实战:从NEC协议到状态机实现

📅 2026/10/8 20:24:01 | 华诺云谱 👁 阅读
基于DE2-115的FPGA红外解码实战:从NEC协议到状态机实现
DE2-115这块板子我前前后后折腾过不少项目但要说哪个实验最“回头看收获最大”基于DE2-115的红外解码绝对排得上号。很多人一听“红外解码”第一反应是拿个接收头、接个单片机、开个定时器就能搞定确实51单片机或者是STM32都能做但用FPGA做一遍是完全不同的体验——你会被迫把NEC协议里的每一个时序细节抠明白而不是调个库函数就完事。这篇博文就从头到尾拆一下这个实验怎么做从协议时序到Verilog状态机从Modelsim仿真到DE2-115板级验证顺带把我在实际调试中踩过的坑都列出来给准备做数电课设、FPGA入门或者想搞懂红外协议底层的朋友一个完整参考。1. 先想清楚这个红外解码实验到底要解什么1.1 核心需求与难点在哪红外遥控解码本质上是把一个遥控器发出的、经过调制的红外光信号恢复成一个个二进制数据位。遥控器上的按键按下以后内部芯片会生成一串脉冲序列驱动红外LED以某个频率闪烁发光接收端用一个一体化红外接收头把光信号转换成电信号再输出一串TTL电平。而FPGA或者单片机要做的就是根据这串电平的高低变化时间还原出“0”和“1”最后拼成按键码。说起来一句话但实际动手会碰到几个坎一是红外信号的时序非常短几百微秒级别得用合适的时钟去测量二是信号是异步进来的FPGA里最忌讳直接拿异步信号做判断必须做同步处理三是接收头输出的电平极性和你直觉里“有信号就高电平”完全相反没搞清楚之前会白调半天四是要处理引导码、数据位、重复码各种状态单纯用一堆if-else很容易写成一团乱麻。这些难点凑在一起正好适合用状态机来解而状态机恰恰是FPGA设计的核心基本功。1.2 为什么用DE2-115而不是51单片机网上搜“红外遥控解码仿真图”跳出来的大都是基于51单片机的红外遥控解码仿真用Proteus画个电路图单片机跑个C代码仿真波形一拉能出结果就算完事。这种方案胜在门槛低C语言写起来快而且有现成的库函数和例程可以参考。但问题是单片机的定时器中断处理方式特别容易把注意力带偏——你可能会花大量时间去调中断优先级、关中断开中断、算定时器初值反而忽略了红外协议本身的时序逻辑。用DE2-115的Cyclone IV FPGA来做思路完全不一样。FPGA是并行硬件逻辑可以把“计时”、“边沿检测”、“状态机判定”、“数据移位”这些任务拆成多个独立的逻辑块同时运行每个环节都看得见摸得着。再者DE2-115板载50MHz时钟逻辑资源对于红外解码这种小设计来说绰绰有余哪怕你把计数器、状态机、显示模块全部加上资源占用也就几个百分点。还有一点很实际很多学校数电课设要求用FPGA平台DE2-115又是非常普及的教学板用这块板子做实验资料多、参考多遇到问题容易找到人讨论。1.3 接收头选择与硬件前置知识硬件部分其实很简单核心器件就是一个一体化红外接收头常见的有1838B、TSOP348、HS0038B等。我这次用的是1838B便宜、好买、到处都有。接收头一般有三个引脚不同厂家的引脚定义和排列顺序不太一样用之前一定要查对应的数据手册。我手上这颗1838B从接收面看过去引脚从左到右依次是OUT、GND、VCC但同型号不同批次也有可能相反这个细节我身边同事就踩过坑接反了之后输出端一直是高电平怎么按遥控器都没反应。接线的时候VCC接DE2-115板上的5V或者3.3V都可以1838B工作电压范围大概在2.7V到5.5V我用的是板上的5VGND和板子共地OUT输出引脚接到DE2-115的GPIO口上建议挑一个空闲的GPIO比如GPIO[0]然后用Pin Planner指定引脚。需要提醒一句DE2-115的GPIO接口电平是3.3V而1838B输出高电平大约是供电电压减去一点点压降所以接5V供电时输出高电平会在4V多直连到3.3V的FPGA引脚会有风险最好用5V转3.3V的电阻分压或者电平转换为省事也可以直接把接收头供电改成3.3V实验完全能正常工作。我后来一直用3.3V供电稳得很。2. NEC协议时序红外解码的核心细节2.1 帧格式引导码、数据位和重复码市面上绝大多数家电遥控器用的都是NEC协议它最典型的特点就是部分脉宽调制每个数据位由一段相同宽度的低电平加上不同宽度的高电平组成。先看一帧数据的结构开头必须有一个引导码由9ms左右的低电平和4.5ms左右的高电平组成相当于告诉接收端“注意后面要发数据了”。然后是4个字节的地址和命令先低位后高位顺序是8位地址码、8位地址反码、8位命令码、8位命令反码。反码的作用是校验比如地址码是0x00地址反码就是0xFF接收端把两个字节对不上就直接判定这帧数据无效。数据位的定义也要背熟逻辑“0”是560微秒低电平加560微秒高电平整体长度大约1.12ms逻辑“1”是560微秒低电平加1.69ms高电平整体长度大约2.25ms。也就是说区分0和1看的是同一个bit里高电平部分有多长而不是总长度。最后还有重复码如果遥控器按键一直按住不松手每隔110ms左右会发送一次重复码重复码的结构是9ms低电平和2.25ms高电平和引导码的区别就在高电平宽度上。明白了这几个时间参数解码方案基本就在脑子里成型了测低电平时间辨别引导码/数据位起始测高电平时间辨别数据位的值是0还是1。2.2 38kHz载波与接收头的解调逻辑很多人刚开始学红外解码会有个疑惑红外接收头输出的波形为什么和我用逻辑分析仪抓到的发射端信号不太一样原因是接收头内部做了解调和整形。遥控器发射时并不是直接发出高电平拉低电平的方波而是把信号调制在38kHz的载波上逻辑低电平对应的是“发射38kHz载波”逻辑高电平对应“不发射”。这个38kHz的载波频率也需要注意不同品牌可能会用36kHz、40kHz但接收头一般对38kHz附近的信号最敏感所以绝大多数方案都用38kHz。一体化红外接收头内部集成了光电二极管、自动增益控制、带通滤波器和解调输出级。它能识别出38kHz左右的载波信号然后输出一个经过整形的、反向的TTL电平信号。也就是说空闲状态下没有红外光接收头输出的是高电平一旦接收到38kHz载波输出立刻变成低电平载波消失以后又恢复高电平。所以你在FPGA引脚上看到的是引导码低电平9ms、高电平4.5ms逻辑1是低电平560us、高电平1.69ms。这一点必须牢记很多人第一次写代码时想当然地认为收到高电平代表有信号结果怎么调都不对。2.3 定时窗口的宽容度把握协议里给的9ms、4.5ms、560us这些数字是理想值实际遥控器发射出来的波形会有±10%甚至更大的偏差而且接收头本身也会引入一定的时延。所以解码过程中判定某个电平宽度属于哪一类时一定要用宽窗口而不是死等精确值。比如引导码低电平我一般是设置7000us到11000us都算合法引导码高电平则是3500us到5500us算合法重复码的高电平窗口设置在1500us到3000us。数据位的判定上低电平窗口设在300us到900us高电平小于900us判为0大于1200us判为1。这些窗口如果设得太窄遥控器距离远一点、电量低一点就会出现大量丢帧设得太宽又容易把噪声误判成有效数据。实际调试时可以用逻辑分析仪抓一段真实波形测量一下自己遥控器的时间参数再针对性微调窗口比直接照抄网上别人给的数值靠谱得多。3. FPGA端解码逻辑状态机、计时器和移位寄存器3.1 模块划分与信号设计FPGA解码的核心思路是用一个高频时钟去测量输入电平的持续时间。DE2-115板载50MHz时钟周期是20ns直接用50MHz时钟做计时的话9ms对应的计数值是450000需要用20位计数器。为了计算方便我习惯先在模块内部产生一个1MHz的节拍信号也就是每计数50个时钟周期产生一个tick_1us脉冲这样每个计数单位对应1us9ms就是900013bit计数器就够了仿真和调试时看数值清晰很多。顶层模块的输入输出信号按这样设计clk接50MHzrst_n接板上的按键复位用下降沿复位方便一点ir_in接红外接收头输出输出端用led[7:0]直接显示命令码外加一个vld信号每成功解码一帧数据就给一个高电平脉冲。信号定义如下ir_sync[1:0]输入信号两级同步寄存器消除亚稳态ir_rise和ir_fall同步后的上升沿、下降沿脉冲cnt_us周期计数器产生1us节拍time_cnt电平宽度计数器单位是usstate主状态机状态寄存器shift_reg、bit_cnt移位寄存器和已接收位数计数器addr_data、cmd_data存储地址码和命令码。3.2 边沿检测与计时器实现先写输入同步和边沿检测。这一步看似简单但在FPGA设计里是不能少的因为ir_in是外部异步信号直接拿它做时钟或复位会引入亚稳态轻则偶尔解错一个位重则整个状态机跑飞。标准做法是打两拍reg [1:0] ir_sync; always (posedge clk or negedge rst_n) begin if (!rst_n) begin ir_sync 2b11; end else begin ir_sync {ir_sync[0], ir_in}; end end wire ir_rise ir_sync[1] ~ir_sync[0]; // 上升沿 wire ir_fall ~ir_sync[1] ir_sync[0]; // 下降沿初始化时ir_sync设为2b11对应空闲高电平。打两拍之后用相邻两位的异或关系产生边沿脉冲在50MHz时钟下这个脉冲宽度是一个时钟周期20ns足够用来触发状态机了。接着是1us节拍和计时器。cnt_us从0数到49第49拍把tick_1us拉高一个时钟同时cnt_us清零time_cnt在tick_1us有效时递增在每次检测到边沿时清零这样它的值就代表当前这段电平持续的微秒数reg [13:0] time_cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) time_cnt 14d0; else if (ir_rise | ir_fall) time_cnt 14d0; else if (tick_1us) time_cnt time_cnt 14d1; end3.3 状态机ASM设计详解状态机是整个解码模块的灵魂我按NEC协议的自然流程把状态分成IDLE、GUIDE_LOW、GUIDE_HIGH、BIT_LOW、BIT_HIGH、FRAME_DONE。下面逐个展开。IDLE态等待下降沿一旦检测到ir_fall说明遥控器开始发射信号立刻进入GUIDE_LOW态时间戳清零。GUIDE_LOW态监测引导码的低电平宽度如果遇到上升沿就判断低电平时间是否落在7000us到11000us之间是则进入GUIDE_HIGH否则回到IDLE继续等待如果计时超过11000us还没有上升沿说明是噪声也要回IDLE。GUIDE_HIGH态监测引导码的高电平宽度遇到下降沿时做两个判断如果高电平时间在3500us到5500us之间说明是正常引导码然后马上进入BIT_LOW态开始接收数据位如果高电平时间在1500us到3000us之间说明这是重复码可以选择在输出端拉一个repeat脉冲然后回IDLE如果两个窗口都不满足也回IDLE。BIT_LOW态和BIT_HIGH态是一对负责接收32个数据位。NEC协议每一位都从低电平开始所以BIT_LOW态里先等上升沿同时持续计时如果低电平时间大于1000us说明这不是标准数据位整个帧数据无效回IDLE。BIT_LOW态遇到上升沿后低电平时间在300us到900us之间算通过进入BIT_HIGH态。BIT_HIGH态里等下降沿下降沿到来时根据高电平时间判定当前位小于900us判为“0”大于1200us判为“1”超出这两个范围则解码失败回IDLE。每解出一个位就把值移入shift_reg同时bit_cnt加1。当bit_cnt数到32时说明一整帧数据接收完毕自动进入FRAME_DONE做一次反码校验校验通过就把命令码送到led端口并拉高vld一帧脉冲然后回IDLE等待下一帧。看起来状态挺多但每个状态做的事情很明确归根结底就是“等边沿、读时间、做比较”。我在调试中发现最容易出问题的是BIT_LOW到BIT_HIGH的切换时机。NEC协议中每一位开头都有一个560us的低电平但这个低电平和上一位的高电平是连续在一起的所以状态机在上一位BIT_HIGH检测到下降沿时就已经顺理成章地把当前位切到了BIT_LOW态此时计时器清零等上升沿正好是在测当前位的低电平宽度。这样串起来以后每一位的处理流程就是完整且不重叠的。3.4 数据移位、反码校验与输出控制移位接收数据时我用的是按位赋值的写法这样逻辑更直观第0个bit存到shift_reg[0]第1个bit存到shift_reg[1]依次类推。32位全部接收完毕以后shift_reg内部的排列是地址码在[7:0]地址反码在[15:8]命令码在[23:16]命令反码在[31:24]。为什么要这么排因为NEC协议是LSB先发最先收到的是地址码的最低位所以第几个收到的bit就放到第几个位置组合起来正好是这个顺序。反码校验逻辑用一条组合判断即可wire frame_ok (shift_reg[15:8] ~shift_reg[7:0]) (shift_reg[31:24] ~shift_reg[23:16]);校验通过时把shift_reg[23:16]赋给命令码输出同时产生一个高电平脉冲。这里我踩过一个坑frame_ok是组合逻辑直接用它的结果去驱动vld会因为数据变化和状态切换不同步导致vld出现毛刺。后来改成在时序逻辑里用frame_ok去锁存vld输出每个解码完成周期产生一个确定宽度的脉冲才彻底解决。如果你的数据显示模块对毛刺敏感记得也做这一步同步。4. 从Modelsim仿真到DE2-115板级联调全流程跑通4.1 编写NEC协议Testbench模拟真实遥控器波形拿到代码第一步不是上板而是写Testbench仿真。红外解码这种带严格时序的模块如果在板子上出了问题调试效率很低但在仿真环境里你可以完全控制输入波形的每一段宽度能用最短的时间验证状态机的每个分支。Testbench的核心任务是模拟一个遥控器让它按照NEC协议输出一帧完整数据喂给待测模块。我习惯用Verilog的task来封装“发送一个字节”的逻辑。参考代码如下reg ir_sig; task send_byte; input [7:0] byte_data; integer i; begin for (i 0; i 8; i i 1) begin ir_sig 0; #560; ir_sig 1; if (byte_data[i] 1b1) #1690; else #560; end end endtask先发引导码再连续发4个字节最后留一段时间观察输出initial begin ir_sig 1; // 引导码 ir_sig 0; #9000; ir_sig 1; #4500; // 地址码 0x00, 地址反码 0xFF, 命令码 0x45, 命令反码 0xBA send_byte(8h00); send_byte(8hFF); send_byte(8h45); send_byte(8hBA); #5000; $finish; end这个Testbench里有几个细节值得注意一是初始化时ir_sig必须是1因为接收头空闲输出是高电平二是引导码先置低再延时模拟的是发射载波导致接收头输出拉低三是数据位低电平时长固定560us高电平时长按数据位决定。这样跑完仿真待测模块的led输出应该变成0x45vld拉出一个高脉冲。4.2 仿真波形怎么看常见仿真问题分析Modelsim里跑完仿真后我一般先不看数据而是先看状态跳转是否符合预期。把state信号加进波形窗口用十六进制显示然后顺着时间轴找IDLE态检测到下降沿后跳GUIDE_LOW上升沿后跳GUIDE_HIGH引导码高电平结束后的下降沿跳BIT_LOW然后BIT_LOW和BIT_HIGH交替出现32次最后进入FRAME_DONE整个过程应该像一条流水线一样顺畅。如果状态机卡在某个状态不动十有八九是电平宽度比较的窗口设得不对。比如引导码低电平实际持续时间是9000us但仿真波形上计数器显示的是8950或者9050如果你的窗口上界设成8800引导码就会被判定失败状态机会在IDLE和GUIDE_LOW之间反复横跳。这类问题通过观察time_cnt在边沿时刻的数值就能快速定位。仿真还有一个高频问题Testbench里用#560这种延时默认单位是仿真器的timescale如果忘了写timescale 1ns/1ps仿真时间会变得非常慢或者完全乱掉。我在Testbench第一行务必加上timescale 1ns/1ps同时在模块内部用50MHz时钟去计时仿真时间单位微秒级别跑完整帧数据只需要几毫秒的仿真时间Modelsim不会卡。4.3 上板验证GPIO接线与板级现象对照仿真通过以后才轮到上板环节。DE2-115的GPIO接口有很多引脚具体用哪一个要开Quartus的Pin Planner查看原理图对应的FPGA引脚号。比如GPIO[0]在原理图上连接到FPGA的PIN_AE23实际以板子原理图为准这里只是举例说明方式在QSF文件里写一行引脚约束就行。千万不要在代码里硬编码引脚不同批次、不同版本的DE2-115 GPIO引脚分配可能会有差异最稳妥的方式是每次新建工程时从官方原理图重新查一遍。引脚约束好、编译下载之后用遥控器对准接收头按一下按键观察DE2-115上的LEDR灯。如果解码成功8个红色LED应当显示出命令码的二进制结果比如命令码0x45显示成01000101。按下不同按键LED的变化应当和遥控器键值对应上。如果按了没反应别急着怀疑FPGA逻辑先用万用表或者示波器量一下接收头OUT引脚的静态电平正常应该接近3.3V如果量到0V要么是接线问题要么是接收头已经烧了。确认静态电平正常后再按遥控器示波器上应当看到类似方波的脉冲簇只有当这一级信号正常时FPGA代码才有意义。5. 常见问题与排查技巧实录5.1 现象、原因、解决方案速查表这个实验我在不同板子上反复做过也帮不少同学排查过问题把高频故障整理成一张速查表建议收藏备用故障现象可能原因排查与解决方案静态电平为0V按遥控器无反应接收头VCC/GND接反或供电电压异常对照数据手册检查引脚定义重新接线静态电平正常但解码永远不触发接收头输出没有接到指定GPIO或Pin Planner引脚约束错误用示波器确认OUT引脚波形在QSF里核对引脚号解码偶尔成功但键值乱码数据位宽度窗口过宽或过窄造成误判用逻辑分析仪抓真实波形微调0/1判决窗口命令码正确但地址码总是不对反码校验逻辑写反或者LSB/MSB顺序理解错用0x00/0xFF这类特征码验证移位顺序按住按键时每隔一段时间重复输出这是重复码在触发解码完成在GUIDE_HIGH态增加重复码分支单独处理不再进入数据位仿真正常上板后频繁误触发红外输入未做同步打拍亚稳态导致状态机跑飞检查ir_sync两级寄存器确保所有判断都基于同步后的信号一上电还没按遥控器LED就乱跳环境红外噪声被当成有效信号拉高所有窗口阈值在IDLE态增加确认逻辑5.2 几个容易被忽视的“小坑”按实际经验最坑的不是状态机逻辑本身而是三个看起来跟解码“没关系”的细节。第一个是复位电路。DE2-115板上的按键复位默认是高电平有效还是低电平有效一定要对照原理图确认。我最初的代码里写的下降沿复位结果板子上的USER_RESET按键是低电平有效导致复位一直处于无效状态整块FPGA逻辑看起来像死了。后来改成用KEY[0]的按下沿触发复位逻辑才恢复正常。第二个是接收头的距离和角度。红外接收头有一定的接收角度正对时效果最好倾斜超过45度就容易丢帧。调试的时候不要把手放在接收头前面也不要让日光灯或者阳光直射接收头强光里的红外成分会干扰38kHz解调导致误码。我刚开始上板验证时一直在窗边调老是有奇怪的乱码后来拉到室内暗处测试问题立刻消失。第三个是延时不要抠太死。用FPGA做红外解码最大的优势是计时精确但并不意味着要把窗口设置成精确到1us。我见过有同学把引导码低电平窗口设成9000到9010理论上一帧正确的数据都通不过实际上遥控器电池电压变化、个体差异都会影响脉冲宽度。我的经验值是所有窗口至少给到理想值上下30%的余量引导码这种标志性强的信号甚至可以放到40%以上可靠性反而更高。5.3 从解码成功到功能扩展一帧数据解码成功只是起点这个模块可以很容易扩展出很多有意思的功能。比如在DE2-115的6位数码管上显示键值或者把不同按键映射成板载外设的控制指令按“1”点亮LEDR按“2”驱动蜂鸣器响两声按“3”让SDRAM读写测试跑起来。另一个方向是学习型遥控器增加一个按键进入“学习模式”把接收到的NEC波形各段宽度保存到RAM里发射时再用38kHz PWM模块把这些宽度重新产生出来就能实现一个能复制任何遥控器的设备。我实际做过的扩展是把解码模块和UART串口连在一起红外按下的键值通过DE2-115的UART口发到PC串口助手相当于做了一个无线输入设备。这个扩展并不复杂——FPGA这边加一个简单的UART发送模块PC端开个串口工具就能看到键值调试起来非常直观。如果你做课设还差一点“工作量”这个方向比重复造轮子有意义得多。结语与个人体会这个实验做完我对“时序逻辑到底是什么”的理解上了一个台阶。用51单片机解码时你只需要在中断里翻转一个标志位很难真正感受到“宽度测量”的意义但在FPGA里你手动设计状态机、手动处理异步信号、手动写Testbench仿真每一步都在逼自己把协议吃透。个人经验是先从仿真把路径跑通再上板调硬件遇到问题先用示波器确定信号源是否正常再回头看RTL逻辑这个顺序能省下大量无头绪的debug时间。DE2-115板载资源对红外解码来说绰绰有余万一你做复杂场景遇到瓶颈也尽可放心把模块丢到更大的系统里复用——只要状态机写得清晰、时序约束合理后续移植几乎零成本。希望这篇记录能帮你少踩几个坑顺利完成你的红外解码实验。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑