FPGA实现红外遥控解码:从NEC协议到数码管显示的完整工程实践
2026/6/14 2:24:52 网站建设 项目流程

1. 项目概述:从遥控器到数码管的红外解码之旅

搞硬件的朋友,手边或多或少都有几块FPGA开发板。学完了点灯、按键消抖、数码管显示这些基础操作后,总想找个有点意思又不太复杂的项目练练手,把之前零散的知识点串起来。红外遥控解码,就是一个绝佳的选择。它几乎是我们身边最常见的无线通信方式之一,从电视、空调到各种智能家电,无处不在。自己动手用FPGA实现一个红外接收解码器,不仅能深入理解这种通信协议的时序精髓,还能顺带巩固状态机设计、时钟分频、边沿检测等核心的FPGA开发技能。

这个项目的目标很明确:使用一个普通的红外接收头(比如常见的HS0038),接收来自遥控器发射的红外编码信号,通过FPGA内部的逻辑进行解码,最终将解码得到的32位原始码值,实时显示在四位数码管上。整个过程,从红外光的调制与解调,到数字信号的捕获与解析,再到最终的人机界面显示,形成了一个完整的信号链。对于初学者而言,成功看到数码管上随着遥控器按键而变化的十六进制数字时,那种成就感是单纯仿真通过无法比拟的。它让你真切地感受到,自己写的代码正在物理世界里与真实的电子元件进行交互。

2. 红外遥控协议基础与解码核心思路

在动手写代码之前,我们必须先搞清楚“敌人”的通信规则。市面上绝大多数消费电子产品的红外遥控,都采用一种被称为“NEC协议”或其变体的编码格式。虽然不同厂商可能有细微调整,但基本框架大同小异。理解这个协议,是解码成功的前提。

2.1 NEC协议帧结构解析

一次完整的按键按下,遥控器发出的不是一个简单的电平信号,而是一帧精心设计的数据包。这帧数据主要包含以下几个部分:

  1. 引导码:这是一帧数据的“开场白”,用于唤醒接收端,并提供一个时间基准。标准NEC协议的引导码由一个持续9ms的38kHz载波脉冲(对应接收头输出低电平),紧跟一个持续4.5ms的空闲期(接收头输出高电平)组成。这个独特的9ms+4.5ms组合,在数据流中非常醒目,是帧起始的可靠标志。
  2. 用户码:通常为16位,可以理解为遥控器的“身份证号”,用于区分不同厂家的设备。比如,电视遥控器的用户码和空调遥控器的就不同,防止误操作。
  3. 用户反码:紧接在用户码之后,是用户码的逐位取反。这提供了一种简单的校验机制,接收端可以通过对比用户码和其反码,初步判断数据在传输过程中是否出现严重错误。
  4. 数据码:8位,代表具体的按键值。例如,音量加、电源键等都有其唯一的数据码。
  5. 数据反码:8位,是数据码的逐位取反,作用与用户反码类似,用于校验数据码。

因此,一帧完整的数据通常是:引导码(9ms低+4.5ms高)+16位用户码+16位用户反码+8位数据码+8位数据反码,总计约67.5ms(引导码13.5ms + 32位数据 * 每位约2.25ms)。需要注意的是,数据发送时是低位(LSB)在前,高位(MSB)在后

2.2 逻辑“0”与“1”的时空定义

解码的关键,在于正确识别每一个数据位是“0”还是“1”。NEC协议规定,每一位数据都以一个持续0.56ms的38kHz载波脉冲(低电平)开始,这个脉冲被称为“起始位”。区别在于紧随其后的高电平持续时间:

  • 逻辑“0”:起始位后,保持高电平0.56ms。
  • 逻辑“1”:起始位后,保持高电平1.68ms。

所以,每一位的周期是固定的:对于“0”,是0.56ms(低)+ 0.56ms(高)= 1.12ms;对于“1”,是0.56ms(低)+ 1.68ms(高)= 2.24ms。可以看到,“1”的周期是“0”的两倍。解码时,我们就是在测量起始低电平过后,高电平的持续时间。如果高电平持续约0.56ms后变低,则该位是“0”;如果持续约1.68ms后变低,则该位是“1”。

2.3 FPGA解码的核心挑战与策略

FPGA是并行执行的硬件,而红外信号是串行的时序流。我们的核心任务是将这个串行流,准确地“翻译”成并行的数据字。这里有几个关键点:

  1. 亚稳态处理:红外接收头输出的信号是异步于FPGA系统时钟的。直接用系统时钟去采样这个信号,极易产生亚稳态,导致误判。必须采用同步器(两级或更多级寄存器链)进行同步处理。
  2. 精确计时:我们需要测量引导码和数据位的高低电平宽度,精度通常在微秒(μs)级别。FPGA的主时钟频率很高(如50MHz,周期20ns),直接用它来计数会得到非常大的计数值。通常的做法是进行合理分频,产生一个适合测量红外信号宽度的“采样时钟”。
  3. 状态机设计:解码过程天然是一个顺序流程:等待引导码 -> 验证引导码 -> 接收32位数据 -> 输出显示。使用有限状态机来清晰地描述这个过程,是最规范、最可靠的设计方法。
  4. 边沿检测:我们关心信号何时从高变低(下降沿)或从低变高(上升沿),因为电平的跳变点标志着一位数据的开始或结束。通过寄存信号的历史状态,可以精确地检测到这些边沿。

基于以上分析,我们的解码方案可以规划为:首先对红外输入信号进行同步化和边沿检测;然后利用一个分频计数器来量化电平持续时间;最后,用一个状态机控制整个解码流程,在适当的边沿时刻,根据计数值判断当前是引导码还是数据位,并逐位拼接出完整数据。

3. 硬件平台搭建与关键模块设计

理论清晰后,我们开始着手实现。假设我们使用一块常见的FPGA开发板(如Altera Cyclone IV或Xilinx Spartan-6),核心时钟50MHz,板上带有四位数码管。

3.1 硬件连接与接口定义

硬件连接非常简单:

  • 红外接收头(HS0038)
    • VCC-> 开发板3.3V5V(注意查看接收头规格)
    • GND-> 开发板GND
    • OUT-> FPGA某个通用IO引脚(例如PIN_E1),在代码中定义为IR输入信号。
  • 四位数码管:通常是共阳或共阴极,通过段选(led_db[7:0],控制a-g段和dp点)和位选(led_cs[3:0],控制哪一位亮)信号驱动。需要根据原理图确认是共阳还是共阴,以及段码表。

我们的Verilog模块接口可以定义如下:

module IR_Receiver ( input wire clk, // 系统时钟,如50MHz input wire rst_n, // 低电平有效的全局复位 input wire IR, // 红外接收头信号输入 output reg [3:0] led_cs, // 数码管位选信号 output reg [7:0] led_db // 数码管段选信号 );

注意IR信号在空闲时为高电平,当接收到38kHz载波时输出低电平。这一点非常重要,是后续逻辑判断的基础。

3.2 信号同步与边沿检测模块

这是确保系统稳定性的第一道关卡。我们使用两级寄存器进行同步,再用一级寄存器锁存前一状态,用于边沿检测。

reg IR_sync0, IR_sync1, IR_sync2; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin IR_sync0 <= 1'b1; // 空闲时为高 IR_sync1 <= 1'b1; IR_sync2 <= 1'b1; end else begin IR_sync0 <= IR; // 第一级同步 IR_sync1 <= IR_sync0; // 第二级同步,此信号可用于稳定判断 IR_sync2 <= IR_sync1; // 锁存前一状态 end end // 边沿检测信号 wire IR_falling_edge = (IR_sync2 == 1'b1) && (IR_sync1 == 1'b0); // 下降沿:高 -> 低 wire IR_rising_edge = (IR_sync2 == 1'b0) && (IR_sync1 == 1'b1); // 上升沿:低 -> 高 wire IR_change = IR_falling_edge | IR_rising_edge; // 任何跳变

这里,IR_sync1代表了同步化、稳定后的当前红外信号状态。IR_sync2IR_sync1上一个时钟周期的状态。通过比较它们,就能在信号发生变化的那个时钟周期,产生一个周期宽度的脉冲信号(IR_falling_edgeIR_rising_edge)。这个脉冲是我们后续所有计时和状态转换的触发器。

3.3 时钟分频与脉宽测量模块

我们需要一个“尺子”来测量高、低电平的持续时间。直接使用50MHz(周期20ns)计数,数值会很大(9ms需要计数450,000次)。为了简化逻辑和节省资源,我们可以先进行一次分频。

根据NEC协议,最小的计时单位是0.56ms(560μs)。为了保证测量精度,我们希望在0.56ms内至少采样16次。那么采样周期应为 560μs / 16 = 35μs。我们的系统时钟周期是20ns,所以分频系数为 35μs / 20ns = 1750。

我们设计两个计数器:

  • div_counter:计数1750个系统时钟,产生一个35μs的周期信号(div_clk_en)。
  • width_counter:在div_clk_en有效时计数,其值代表了当前电平持续了多少个“35μs”单位。
reg [10:0] div_counter; // 计数0-1749,需11位 (2048 > 1750) reg div_clk_en; // 分频使能信号,高电平一个系统时钟周期 reg [8:0] width_counter; // 脉宽计数器,最大计数值需能覆盖1.68ms (1.68ms/35us=48) always @(posedge clk or negedge rst_n) begin if (!rst_n) begin div_counter <= 11'd0; div_clk_en <= 1'b0; end else begin if (IR_change) begin // 关键!信号跳变时,清零分频计数器,重新开始测量新电平 div_counter <= 11'd0; div_clk_en <= 1'b0; end else begin if (div_counter == 11'd1749) begin div_counter <= 11'd0; div_clk_en <= 1'b1; // 每1750个时钟周期,产生一个使能脉冲 end else begin div_counter <= div_counter + 1'b1; div_clk_en <= 1'b0; end end end end always @(posedge clk or negedge rst_n) begin if (!rst_n) begin width_counter <= 9'd0; end else begin if (IR_change) begin // 信号跳变,清零脉宽计数器 width_counter <= 9'd0; end else if (div_clk_en) begin // 每35us,脉宽计数器加1 width_counter <= width_counter + 1'b1; end end end

现在,width_counter的值就代表了当前电平持续的“35μs”的个数。例如,当width_counter计数到大约257时(257 * 35μs ≈ 9ms),我们就认为检测到了引导码的9ms低电平。

实操心得:为什么要在IR_change时清零计数器?这是解码准确性的生命线。红外信号每次跳变,都意味着前一个电平的结束和新电平的开始。此时必须将测量“尺子”归零,才能开始测量新电平的宽度。如果忘记清零,width_counter会一直累加,导致所有测量结果都是错的。

3.4 解码状态机设计

这是整个解码器的“大脑”。我们设计一个四状态的状态机:

  1. IDLE:空闲状态,等待引导码开始(检测到持续的低电平)。
  2. LEADER_LOW:已进入引导码低电平阶段,等待上升沿,并验证低电平宽度是否为9ms左右。
  3. LEADER_HIGH:已进入引导码高电平阶段,等待下降沿,并验证高电平宽度是否为4.5ms左右。
  4. RECEIVE_DATA:引导码验证通过,开始接收32位数据。在此状态下,根据上升沿和下降沿,以及width_counter的值,判断每一位是“0”还是“1”。

状态转移图(文字描述):

  • IDLE->LEADER_LOW: 当IR_sync1为低电平时(检测到低电平开始)。
  • LEADER_LOW->LEADER_HIGH: 当检测到IR_rising_edge,且此时width_counter值在9ms对应范围(如240-270)内。
  • LEADER_LOW->IDLE: 如果IR_rising_edgewidth_counter不在9ms范围内,说明不是合法引导码,回到空闲。
  • LEADER_HIGH->RECEIVE_DATA: 当检测到IR_falling_edge,且此时width_counter值在4.5ms对应范围(如120-140)内。
  • LEADER_HIGH->IDLE: 如果IR_falling_edgewidth_counter不在4.5ms范围内,回到空闲。
  • RECEIVE_DATA->IDLE: 成功接收完32位数据,或者接收过程中出现超时、电平宽度错误等异常。
  • RECEIVE_DATA状态内循环:处理每一位数据。
localparam [1:0] IDLE = 2'b00, LEADER_LOW = 2'b01, LEADER_HIGH = 2'b10, RECEIVE_DATA = 2'b11; reg [1:0] current_state, next_state; reg [5:0] bit_cnt; // 数据位计数器,0-31 reg [31:0] data_shift_reg; // 32位移位寄存器,用于拼接数据 reg data_valid; // 数据接收完成有效标志 // 状态寄存器 always @(posedge clk or negedge rst_n) begin if (!rst_n) current_state <= IDLE; else current_state <= next_state; end // 下一状态逻辑 always @(*) begin next_state = current_state; // 默认保持当前状态 case (current_state) IDLE: begin if (IR_sync1 == 1'b0) next_state = LEADER_LOW; // 检测到低电平开始 end LEADER_LOW: begin if (IR_rising_edge) begin if ((width_counter > 240) && (width_counter < 270)) // 约9ms范围 next_state = LEADER_HIGH; else next_state = IDLE; // 引导码错误 end end LEADER_HIGH: begin if (IR_falling_edge) begin if ((width_counter > 120) && (width_counter < 140)) // 约4.5ms范围 next_state = RECEIVE_DATA; else next_state = IDLE; // 引导码错误 end end RECEIVE_DATA: begin if (bit_cnt == 6'd32) begin // 收到32位 next_state = IDLE; end else if (/* 超时或错误判断 */) begin next_state = IDLE; // 接收错误 end end default: next_state = IDLE; endcase end

3.5 数据位接收与拼接逻辑

RECEIVE_DATA状态下,我们需要处理每一位数据。每一位都以一个0.56ms的低电平(起始位)开始,以高电平结束。因此:

  1. 每次IR_rising_edge(低电平结束,高电平开始),我们应检查刚结束的低电平宽度是否约为0.56ms(对应width_counter约16)。如果不是,则说明数据位格式错误,可置错误标志。
  2. 每次IR_falling_edge(高电平结束,下一位低电平开始),我们根据刚结束的高电平宽度来判断该数据位是“0”还是“1”:
    • 如果width_counter值在“0”的范围内(如12-20),则该位为0。
    • 如果width_counter值在“1”的范围内(如40-56),则该位为1。
    • 如果都不在,则数据错误。

由于数据是低位在前,我们需要将接收到的位从右向左(或从左向右移位后反向)拼接到一个32位寄存器中。

// 在RECEIVE_DATA状态下的输出逻辑 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin bit_cnt <= 6'd0; data_shift_reg <= 32'd0; data_valid <= 1'b0; end else begin data_valid <= 1'b0; // 默认清零 case (current_state) IDLE, LEADER_LOW, LEADER_HIGH: begin bit_cnt <= 6'd0; data_shift_reg <= 32'd0; end RECEIVE_DATA: begin if (IR_rising_edge) begin // 检查起始低电平宽度是否合法(约0.56ms) if (!((width_counter > 12) && (width_counter < 20))) begin // 错误处理,可跳回IDLE或记录错误 bit_cnt <= 6'd0; data_shift_reg <= 32'd0; end end else if (IR_falling_edge) begin // 根据刚结束的高电平宽度判断数据位 if ((width_counter > 12) && (width_counter < 20)) begin // 判断为“0” data_shift_reg <= {1'b0, data_shift_reg[31:1]}; // 右移,新位在LSB?注意顺序! bit_cnt <= bit_cnt + 1'b1; end else if ((width_counter > 40) && (width_counter < 56)) begin // 判断为“1” data_shift_reg <= {1'b1, data_shift_reg[31:1]}; // 右移 bit_cnt <= bit_cnt + 1'b1; end else begin // 宽度不符,数据错误 bit_cnt <= 6'd0; data_shift_reg <= 32'd0; end // 检查是否收满32位 if (bit_cnt == 6'd31) begin // 注意,这里是在收到第32位后,bit_cnt会变成32 data_valid <= 1'b1; // 产生一个时钟周期的有效脉冲 end end end endcase end end

重要纠偏与技巧:上面代码中data_shift_reg的拼接方式{1‘bX, data_shift_reg[31:1]}错误的。这实现了右移,但新位进入了最高位(MSB),而协议是低位在前。正确的低位在前接收方式有两种:

  1. 左移,新位放在最低位data_shift_reg <= {data_shift_reg[30:0], new_bit};这样,第一个收到的位最终会在data_shift_reg[0],符合直观。但显示时可能需要根据你的理解调整顺序。
  2. 右移,新位放在最高位,最后整体反转data_shift_reg <= {new_bit, data_shift_reg[31:1]};接收完后,data_shift_reg[0]是最后收到的位。为了得到正确的顺序,可以最后执行一次data_shift_reg <= reverse_bits(data_shift_reg);。第一种左移方式更直观,推荐使用。

3.6 数码管动态扫描显示模块

解码得到的32位数据(data_shift_reg)需要显示。我们将其分成4个8位字节,分别对应用户反码用户码数据反码数据码(根据协议顺序调整)。然后驱动四位数码管进行动态扫描显示。

动态扫描的原理是利用人眼视觉暂留,快速轮流点亮每一个数码管。只要扫描频率足够快(>60Hz),看起来就像是同时点亮。

reg [15:0] display_data; // 假设我们只显示用户码和数据码(共16位,4个十六进制数) reg [3:0] digit_sel; // 位选寄存器 reg [19:0] scan_cnt; // 扫描计数器 reg [7:0] seg_data; // 当前要显示的段码 // 将32位数据中需要显示的部分提取出来,例如显示用户码(16-31位)和数据码(0-15位)的低8位 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin display_data <= 16'h0000; end else if (data_valid) begin // 当解码完成时锁存显示数据 display_data <= {data_shift_reg[23:16], data_shift_reg[7:0]}; // 示例:显示用户码低8位和数据码 end end // 扫描计数,控制切换频率 always @(posedge clk or negedge rst_n) begin if (!rst_n) scan_cnt <= 20'd0; else scan_cnt <= scan_cnt + 1'b1; end // 位选信号生成(循环左移或右移) always @(posedge clk or negedge rst_n) begin if (!rst_n) digit_sel <= 4'b0001; // 从第一位开始 else if (scan_cnt == 20'hFFFFF) begin // 约每10ms切换一次(根据时钟频率调整) digit_sel <= {digit_sel[2:0], digit_sel[3]}; // 循环左移 end end // 根据位选信号,选择当前要显示的4位二进制数,并转换为段码 always @(*) begin case (digit_sel) 4'b0001: seg_data = hex_to_seg(display_data[3:0]); // 显示最低位 4'b0010: seg_data = hex_to_seg(display_data[7:4]); 4'b0100: seg_data = hex_to_seg(display_data[11:8]); 4'b1000: seg_data = hex_to_seg(display_data[15:12]); // 显示最高位 default: seg_data = 8'hFF; // 全灭 endcase end // 段码输出 assign led_db = seg_data; // 位码输出(注意共阳/共阴) assign led_cs = digit_sel; // 假设共阴数码管,低电平选中 // 十六进制到7段数码管译码函数(共阴为例) function [7:0] hex_to_seg; input [3:0] hex; begin case (hex) 4'h0: hex_to_seg = 8'h3F; // 0 4'h1: hex_to_seg = 8'h06; // 1 4'h2: hex_to_seg = 8'h5B; // 2 4'h3: hex_to_seg = 8'h4F; // 3 4'h4: hex_to_seg = 8'h66; // 4 4'h5: hex_to_seg = 8'h6D; // 5 4'h6: hex_to_seg = 8'h7D; // 6 4'h7: hex_to_seg = 8'h07; // 7 4'h8: hex_to_seg = 8'h7F; // 8 4'h9: hex_to_seg = 8'h6F; // 9 4'hA: hex_to_seg = 8'h77; // A 4'hB: hex_to_seg = 8'h7C; // b 4'hC: hex_to_seg = 8'h39; // C 4'hD: hex_to_seg = 8'h5E; // d 4'hE: hex_to_seg = 8'h79; // E 4'hF: hex_to_seg = 8'h71; // F default: hex_to_seg = 8'h00; endcase end endfunction

4. 系统集成、调试与深度优化

将上述所有模块整合到顶层模块中,并进行引脚分配、编译综合,就可以下载到FPGA进行测试了。但第一次往往不会那么顺利,调试是必不可少的环节。

4.1 系统集成与引脚分配

顶层模块就是例化各个子模块并连接信号。在Quartus II或Vivado中,需要为clkrst_nIRled_csled_db分配实际物理引脚。clk连接板载晶振引脚,rst_n连接按键,IR连接接收头输出脚,led_csled_db连接数码管的位选和段选引脚。务必查阅开发板原理图进行正确分配。

4.2 调试技巧与常见问题排查

  1. 问题:数码管无任何显示。

    • 排查:首先检查硬件连接,特别是红外接收头的VCC和GND是否接反。用示波器或逻辑分析仪探测IR引脚,按下遥控器时,应该能看到一串明显的脉冲波形。如果没有,可能是接收头损坏或供电问题。如果有波形但解码失败,进入下一步。
  2. 问题:按下遥控器,数码管显示乱码或固定值不变。

    • 排查:这通常是解码逻辑或时序问题。
      • 检查同步和边沿检测:可以通过SignalTap II或ChipScope等嵌入式逻辑分析仪,抓取IR_sync1IR_falling_edgeIR_rising_edge信号。确保边沿检测脉冲在信号跳变时正确产生,且只有一个时钟周期宽度。
      • 检查脉宽计数器:抓取width_counterIR_change时是否被清零,在电平持续期间是否正常递增。在引导码低电平结束时,它的值是否在预期的9ms范围内(例如,对于35μs单位,9ms对应257左右)。
      • 检查状态机:抓取current_state信号,观察其是否按照IDLE -> LEADER_LOW -> LEADER_HIGH -> RECEIVE_DATA -> IDLE的路径正确跳转。如果卡在某个状态,检查该状态的转移条件。
      • 检查数据拼接:在RECEIVE_DATA状态,抓取bit_cntdata_shift_reg。观察bit_cnt是否从0递增到31,以及data_shift_reg是否随着每个IR_falling_edge正确移位。特别注意低位在前的拼接顺序是否正确。
  3. 问题:同一个按键,每次显示的码值不稳定(最后几位跳动)。

    • 排查:这是最常见的时序容错问题。我们的width_counter判断条件(如(width_counter > 240) && (width_counter < 270))范围设置得太窄。由于遥控器晶振误差、接收头响应偏差以及我们分频计数的误差,实际脉宽会有波动。需要适当放宽判断范围。例如,9ms的判断可以放宽到200~300,0.56ms的判断可以放宽到10~25,1.68ms的判断可以放宽到35~60。具体范围需要通过实验,观察正常信号下width_counter的实际分布来确定。
  4. 问题:遥控器必须离得很近才有效,或者反应迟钝。

    • 排查:检查红外接收头是否被环境光干扰(尤其是日光灯和太阳光)。可以尝试给接收头加一个简单的遮光罩。另外,确保程序中没有因为错误标志导致频繁复位解码过程。

4.3 深度优化与功能扩展

基础功能实现后,可以考虑以下优化和扩展,让项目更完善:

  1. 增加连发码处理:很多遥控器按住按键不放时,会先发送一帧完整数据,之后每隔约110ms发送一个简短的重复码(通常为9ms低电平 + 2.25ms高电平 + 0.56ms低电平)。可以在状态机中增加一个REPEAT状态来识别和处理这种重复码,实现按住连续响应的效果。

  2. 添加校验与错误恢复:目前代码在遇到宽度不符时会直接复位。可以增加更健壮的机制,比如连续错误计数,超过阈值才复位。或者利用用户码/数据码的反码进行校验,只有校验通过的数据才更新显示。

  3. 显示格式优化:将32位数据显示为8个十六进制数,或者将用户码和数据码分开显示,并增加一些标识符(如小数点)来区分。

  4. 串口输出:除了数码管显示,还可以将解码出的数据通过UART发送到电脑串口助手,方便记录和分析不同遥控器的编码。

  5. 学习与存储功能(向“红外学习机”迈进):将解码得到的用户码和数据码存储到FPGA内部的ROM或外部的EEPROM中。然后设计一个发射电路(用一个IO口驱动红外发射管),将存储的编码按照NEC协议格式重新发射出去,这样就可以模拟原遥控器,实现万能遥控或智能联动的功能。这需要另一个状态机来精确生成38kHz载波和调制信号。

4.4 参数计算与容差设计回顾

最后,我们系统性地回顾一下核心参数的计算与选择思路,这是硬件调试的基石:

  • 系统时钟T_clk = 20ns (50MHz)

  • 最小测量单位T_unit = 0.56ms / 16 = 35μs。选择16次采样是经验值,在分辨率和计数器位宽间取得平衡。

  • 分频系数N_div = T_unit / T_clk = 35μs / 20ns = 1750

  • 脉宽计数器理论值

    • 引导码低电平(9ms):9ms / 35μs ≈ 257
    • 引导码高电平(4.5ms):4.5ms / 35μs ≈ 129
    • 数据位起始低电平(0.56ms):0.56ms / 35μs ≈ 16
    • 数据位“0”高电平(0.56ms):16
    • 数据位“1”高电平(1.68ms):1.68ms / 35μs ≈ 48
  • 容差范围设计:考虑到各方误差,实际判断时不能使用精确值,必须设置一个范围。例如:

    • check_9ms: (240 < width_counter < 270)// ±10%
    • check_4ms: (115 < width_counter < 145)// ±10%
    • check_low: (12 < width_counter < 20)// ±25%
    • check_high: (40 < width_counter < 56)// ±15%

范围的设计需要保守一点,特别是对于引导码的判断可以严格一些(范围窄),对于数据位的判断可以宽松一些(范围宽),因为数据位更容易受到干扰。最佳的容差范围需要通过实际测试,采集大量样本的width_counter值,观察其分布情况后最终确定。

红外解码是一个经典的时序数字电路设计案例,它完美结合了同步设计、状态机、计数器、边沿检测等基础知识点。通过这个项目,你不仅能收获一个看得见摸得着的成果,更能深刻理解如何用硬件描述语言去“对话”真实的物理信号。当数码管上终于稳定地显示出遥控器按键对应的码值时,之前调试的所有焦虑都会瞬间化为进阶的底气。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询