2)摘自《开拓者FPGA开发指南》关注官方微信号公众号,获取更多资料:正点原子
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-13912-1-1.html
第二十四章 红外遥控实验
红外遥控是一种无线、非接触控制技术,具有抗干扰能力强、信息传输可靠、功耗低、易
实现等显著特点,被诸多
电子设备特别是家用电器广泛采用,并越来越多的应用到计算机系统
中。本章我们将使用开拓者FPGA开发板接收红外遥控器发出的红外信号,并将数据显示在数码
管上,如果监测到重复码,则通过LED灯闪烁指示。
本章分为以下几个章节:
24.1 红外遥控简介
24.2 实验任务
24.3 硬件设计
24.4 程序设计
24.5 下载验证
红外遥控简介
红外遥控是一种无线、非接触控制技术,由于它不具有像无线电遥控那样可以穿过障碍物
去控制被控对象的能力,所以同类产品的红外遥控器,可以有相同的遥控频率或编码,而不会
隔墙控制或干扰邻居的家用电器,这对于大批量生产以及在家用电器上普及红外遥控器提供了
极大的方便。红外遥控器发射出的实际上是一种红外光(红外线),其波长范围在1mm到760nm
之间,而人眼可见光的波长范围一般在400nm到760nm之间,所以我们并不能看到红外遥控器发
出的红外光,因此对环境的影响很小,也不会影响临近的无线电设备。
红外遥控器的编码目前广泛使用的是:NEC协议和Philips RC-5协议。开拓者FPGA开发板
配套的遥控器使用的是NEC协议,其逻辑电平编码格式如图 24.1.1所示。
图 24.1.1 NEC协议逻辑电平编码格式
NEC协议采用PPM调制(Pulse Posi
tion Modulation,脉冲位置调制)的形式进行编码,
数据的每一位(Bit)脉冲长度为560us,由38KHz的载波脉冲(carrier burst)进行调制,推
荐的载波占空比为1/3至1/4。由上图可知,有载波脉冲的地方,其宽度都为560us,而载波脉
冲的间隔时间是不同的。逻辑“1”的载波脉冲+载波脉冲间隔时间为2.25ms;逻辑“0”的载
波脉冲+载波脉冲间隔时间为逻辑“1”的一半,也就是1.125ms。
图 24.1.2 NEC协议的数据传输格式
图 24.1.2为NEC协议的数据传输格式。由图可知,传输数据时低位在前,图中的地址码
(Address)为0x59,控制码(Command)为0x16。一个信息的发送由9ms的AGC(自动增益控制)
载波脉冲开始,用于在早期的IR红外接收器中设置增益;紧接着是4.5ms的空闲信号;随后是地址码和控制码。地址码和控制码分别传输了两次,第二次传输的地址码和控制码都是反码,
用于对地址码和控制码做校验,当然,也可以直接忽略地址码反码和控制码反码。每次信息都
是按照同步码(9ms载波脉冲+4.5ms空闲信号)、地址码、地址反码、控制码和控制反码的格
式进行传输,因此,单次信息传输的时间是固定不变的。
当红外遥控器上的按键被一直按下时,红外遥控器只会发送一次完整的信息,其后会每隔
110ms发送一次重复码(也叫连发码)。重复码的数据格式比较简单,同样是由9ms的AGC(自
动增益控制)载波脉冲开始,紧接着是2.25ms的空闲信号,随后是560us的载波脉冲,重复码
的数据格式如图 24.1.3和图 24.1.4所示。
图 24.1.3 重复码的数据格式
图 24.1.4 一直发送重复码
以上部分是对NEC协议的介绍,也就是红外遥控器发送数据时所遵循的协议规范,接下来
我们了解下开发板板载的红外接收头,其型号为HS0038B,实物图和结构框图如图 24.1.5和图
24.1.6所示。
图 24.1.5 HS0038B实物图
图 24.1.6 HS0038B结构框图
红外接收头通常被厂家集成在一个
元件中,成为一体化红外接收头。内部集成了红外监测
二极管、自动增益放大器(AGC)、带通滤波器(Band Pass)、解调器(Demodulator)等电
路。红外遥控器发出的信息经38KHz的载频进行二级调制以提高发射效率,达到降低
电源功率
的目的,然后再经过红外发射二极管产生红外线向空间中发射。红外接收头通过红外监测二极
管,将光信号转换成电信号,经过
电路调制之后,最终输出可以被FPGA采集的TTL电平信号。
这里要注意的一点是,红外接收头内部的三极管电路具有信号反向的功能,也就是将1变为0,
0变为1,那么上面的整个协议则电平反过来接收。9ms本来是高电平,那么将变为低电平,以
此类推如图 24.1.7所示,接收解码对应的波形是FPGA最终接收到的红外信号。
图 24.1.7 红外接收解码接收图
下图为红外解码接收到的完整波形。
图 24.1.8 红外解码接收到的完整波形
从图 24.1.8可以看到,地址码为0,控制码为0x15。在一段时间之后,我们还可以收到几
个脉冲,这就是NEC协议规定的重复码(连发码),如果一帧数据发送完毕之后,按键仍然没
有放开,则发射重复码,可以通过统计重复码来标记按键按下的长短/次数。
实验任务
本节实验任务是使用开拓者FPGA开发板接收红外遥控器发出的红外信号,并将数据显示在
数码管上;如果监测到重复码,则通过LED灯闪烁指示。
硬件设计
HS0038电路原理图如图 24.3.1所示,图中REMOTE_IN信号为红外接收头的电平输出端。
图 24.3.1 HS0038B电路原理图
本实验的管脚分配如下表所示
表 24.3.1 红外遥控数码管显示实验管脚分配
程序设计
根据实验任务可大致规划出控制流程,红外驱动模块解析红外数据,将控制码输出至数码
管驱动模块,重复码有效信号输出至LED控制模块。数码管驱动模块将对应的位选和段选信号
发送至数码管,使相应的数字显示在数码管上,LED控制模块根据重复码信号控制LED灯的亮灭。
系统框图如下所示。
图 24.4.1 红外遥控实验系统框图
顶层模块原理图如下所示
图 24.4.2 顶层模块原理图
FPGA顶层(top_remote_rcv)例化了以下两个模块:红外驱动模块(remote_rcv)和数码
管动态显示模块(seg_led),实现各模块间信号的交互。
顶层模块代码如下:
1
module top_remote_rcv
(
2
input sys_clk
, //系统时钟
3
input sys_rst_n
, //系统复位信号,低电平有效
4
input remote_in
, //红外接收信号
5
output [5
:0
] sel
, //数码管位选信号
6
output [7
:0
] seg_led
, //数码管段选信号
7
output led //led灯
8
);
9
10 //wire define
11
wire [7
:0
] data
;
12
wire repeat_en
;
13
14 //*****************************************************
15 //** main code
16 //*****************************************************
17
18 //数码管显示模块
19 seg_led u_seg_led
(
20
.clk
(sys_clk
),
21
.rst_n
(sys_rst_n
),
22
.sel
(sel
),
23
.seg_led
(seg_led
),
24
.data
(data
), //红外数据
25
.point
(6'd0
), //无小数点
26
.en
(1'b1
), //使能数码管
27
.sign
(1'b0
) //无符号显示
28
);
29
30 //HS0038B驱动模块
31 remote_rcv u_remote_rcv
(
32
.sys_clk
(sys_clk
),
33
.sys_rst_n
(sys_rst_n
),
34
.remote_in
(remote_in
),
35
.repeat_en
(repeat_en
),
36
.data_en
(),
37
.data
(data
)
38
);
39
40 led_ctrl u_led_ctrl
(
41
.sys_clk
(sys_clk
),
42
.sys_rst_n
(sys_rst_n
),
43
.repeat_en
(repeat_en
),
44
.led
(led
)
45
);
46
47
endmodule
顶层模块完成对其他模块的例化,红外驱动模块输出的控制码(data)连接至数码管显示
模块,输出的repeat_en(重复码有效信号)连接至LED控制模块。
由本章简介部分介绍的红外传输时序可以发现,红外传输时序非常适合使用状态机来编写。
红外驱动模块状态跳转图如下图所示。
图 24.4.3 红外驱动模块状态跳转图
红外驱动模块使用三段式状态机来解析红外遥控信号,从上图可以比较直观的看到每个状
态实现的功能以及跳转都下一个状态的条件。由于一次完整的红外信息和重复码都是以同步码
(9ms的低电平)开始,其空闲信号高电平的时间是不一样的,一次完整的红外信息空闲信号
高电平时间是4.5ms,而重复码的空闲信号高电平时间是2.25ms。所以我们在st_start_judge
状态判断空闲信号高电平的时间,如果时间是4.5ms,则跳转到st_rec_data状态;如果时间是
2.25ms,则跳转到st_repeat状态。
红外驱动模块部分代码如下:
1
module remote_rcv
(
2
input sys_clk
, //系统时钟
3
input sys_rst_n
, //系统复位信号,低电平有效
4
5
input remote_in
, //红外接收信号
6
output reg repeat_en
, //重复码有效信号
7
output reg data_en
, //数据有效信号
8
output reg [7
:0
] data //红外控制码
9
);
10
11 //parameter define
12
parameter st_idle
= 5'b0_0001
; //空闲状态
13
parameter st_start_low_9ms
= 5'b0_0010
; //监测同步码低电平
14
parameter st_start_judge
= 5'b0_0100
; //判断重复码和同步码高电平(空闲信号)
15
parameter st_rec_data
= 5'b0_1000
; //接收数据
16
parameter st_repeat_code
= 5'b1_0000
; //重复码
17
18 //reg define
19
reg [4
:0
] cur_state
;
20
reg [4
:0
] next_state
;
21
22
reg [11
:0
] div_cnt
; //分频计数器
23
reg div_clk
; //分频时钟
24
reg remote_in_d0
; //对输入的红外信号延时打拍
25
reg remote_in_d1
;
26
reg [7
:0
] time_cnt
; //对红外的各个状态进行计数
27
28
reg time_cnt_clr
; //计数器清零信号
29
reg time_done
; //计时完成信号
30
reg error_en
; //错误信号
31
reg judge_flag
; //检测出的标志信号 0:同步码高电平(空闲信号) 1:重复码
32
reg [15
:0
] data_temp
; //暂存收到的控制码和控制反码
33
reg [5
:0
] data_cnt
; //对接收的数据进行计数
34
35 //wire define
36
wire pos_remote_in
; //输入红外信号的上升沿
37
wire neg_remote_in
; //输入红外信号的下降沿
38
39 //*****************************************************
40 //** main code
41 //*****************************************************
42
43
assign pos_remote_in
= (~remote_in_d1
) & remote_in_d0
;
44
assign neg_remote_in
= remote_in_d1
& (~remote_in_d0
);
45
46 //时钟分频,50Mhz/(2*(3124+1))=8khz,T=0.125ms
47
always @(posedge sys_clk
or negedge sys_rst_n
) begin
48
if (!sys_rst_n
) begin
49 div_cnt
<= 12'd0
;
50 div_clk
<= 1'b0
;
51
end
52
else if(div_cnt
== 12'd3124
) begin
53 div_cnt
<= 12'd0
;
54 div_clk
<= ~div_clk
;
55
end
56
else
57 div_cnt
= div_cnt
+ 12'b1
;
58
end
59
60 //对红外的各个状态进行计数
61
always @(posedge div_clk
or negedge sys_rst_n
) begin
62
if(!sys_rst_n
)
63 time_cnt
<= 8'b0
;
64
else if(time_cnt_clr
)
65 time_cnt
<= 8'b0
;
66
else
67 time_cnt
<= time_cnt
+ 8'b1
;
68
end
69
70 //对输入的remote_in信号延时打拍
71
always @(posedge div_clk
or negedge sys_rst_n
) begin
72
if(!sys_rst_n
) begin
73 remote_in_d0
<= 1'b0
;
74 remote_in_d1
<= 1'b0
;
75
end
76
else begin
77 remote_in_d0
<= remote_in
;
78 remote_in_d1
<= remote_in_d0
;
79
end
80
end
81
82 //状态机
83
always @ (posedge div_clk
or negedge sys_rst_n
) begin
84
if(!sys_rst_n
)
85 cur_state
<= st_idle
;
86
else
87 cur_state
<= next_state
;
88
end
89
90
always @(*) begin
91 next_state
= st_idle
;
92
case(cur_state
)
93 st_idle
: begin //空闲状态
94
if(remote_in_d0
== 1'b0
)
95 next_state
= st_start_low_9ms
;
96
else
97 next_state
= st_idle
;
98
end
99 st_start_low_9ms
: begin //监测同步码低电平
100
if(time_done
)
101 next_state
= st_start_judge
;
102
else if(error_en
)
103 next_state
= st_idle
;
104
else
105 next_state
= st_start_low_9ms
;
106
end
107 st_start_judge
: begin //判断重复码和同步码高电平(空闲信号)
108
if(time_done
) begin
109
if(judge_flag
== 1'b0
)
110 next_state
= st_rec_data
;
111
else
112 next_state
= st_repeat_code
;
113
end
114
else if(error_en
)
115 next_state
= st_idle
;
116
else
117 next_state
= st_start_judge
;
118
end
119 st_rec_data
: begin //接收数据
120
if(pos_remote_in
&& data_cnt
== 6'd32
)
121 next_state
= st_idle
;
122
else
123 next_state
= st_rec_data
;
124
end
125 st_repeat_code
: begin //重复码
126
if(pos_remote_in
)
127 next_state
= st_idle
;
128
else
129 next_state
= st_repeat_code
;
130
end
131
default : next_state
= st_idle
;
132
endcase
133
end
134
135
always @(posedge div_clk
or negedge sys_rst_n
) begin
136
if (!sys_rst_n
) begin
137 time_cnt_clr
<= 1'b0
;
138 time_done
<= 1'b0
;
139 error_en
<= 1'b0
;
140 judge_flag
<= 1'b0
;
141 data_en
<= 1'b0
;
142 data
<= 8'd0
;
143 repeat_en
<= 1'b0
;
144 data_cnt
<= 6'd0
;
145 data_temp
<= 32'd0
;
146
end
147
else begin
148 time_cnt_clr
<= 1'b0
;
149 time_done
<= 1'b0
;
150 error_en
<= 1'b0
;
151 repeat_en
<= 1'b0
;
152 data_en
<= 1'b0
;
153
case(cur_state
)
154 st_idle
: begin
155 time_cnt_clr
<= 1'b1
;
156
if(remote_in_d0
== 1'b0
)
157 time_cnt_clr
<= 1'b0
;
158
end
159 st_start_low_9ms
: begin //9ms/0.125ms = 72
160
if(pos_remote_in
) begin
161 time_cnt_clr
<= 1'b1
;
162
if(time_cnt
>= 69
&& time_cnt
<= 75
)
163 time_done
<= 1'b1
;
164
else
165 error_en
<= 1'b1
;
166
end
167
end
168 st_start_judge
: begin
169
if(neg_remote_in
) begin
170 time_cnt_clr
<= 1'b1
;
171 //重复码高电平2.25ms 2.25/0.125 = 18
172
if(time_cnt
>= 15
&& time_cnt
<= 20
) begin
173 time_done
<= 1'b1
;
174 judge_flag
<= 1'b1
;
175
end
176 //同步码高电平4.5ms 4.5/0.125 = 36
177
else if(time_cnt
>= 33
&& time_cnt
<= 38
) begin
178 time_done
<= 1'b1
;
179 judge_flag
<= 1'b0
;
180
end
181
else
182 error_en
<= 1'b1
;
183
end
184
end
185 st_rec_data
: begin
186
if(pos_remote_in
) begin
187 time_cnt_clr
<= 1'b1
;
188
if(data_cnt
== 6'd32
) begin
189 data_en
<= 1'b1
;
190 data_cnt
<= 6'd0
;
191 data_temp
<= 16'd0
;
192
if(data_temp
[7
:0
] == ~data_temp
[15
:8
]) //校验控制码和控制反码
193 data
<= data_temp
[7
:0
];
194
end
195
end
196
else if(neg_remote_in
) begin
197 time_cnt_clr
<= 1'b1
;
198 data_cnt
<= data_cnt
+ 1'b1
;
199 //解析控制码和控制反码
200
if(data_cnt
>= 6'd16
&& data_cnt
<= 6'd31
) begin
201
if(time_cnt
>= 2
&& time_cnt
<= 6
) begin //0.565/0.125 = 4.52
202 data_temp
<= {1'b0
,data_temp
[15
:1
]}; //逻辑“0”
203
end
204
else if(time_cnt
>= 10
&& time_cnt
<= 15
) //1.69/0.125 = 13.52
205 data_temp
<= {1'b1
,data_temp
[15
:1
]}; //逻辑“1”
206
end
207
end
208
end
209 st_repeat_code
: begin
210
if(pos_remote_in
) begin
211 time_cnt_clr
<= 1'b1
;
212 repeat_en
<= 1'b1
;
213
end
214
end
215
default : ;
216
endcase
217
end
218
end
219
220
endmodule
在代码第47行开始的always语句块中,我们对输入的50MHz的时钟进行分频,得到一个周
期为0.125ms(8KHz)的时钟,即以8Khz的时钟对红外信号进行采样。这里之所以对时钟进行
分频,是因为红外信号接收的过程用时较长,如果使用50Mhz的时钟采样,内部定义的计数器
位宽会比较大,所以我们对输入的时钟做了分频的处理,当然分频得到其它频率的时钟也是可
以的。
代码中使用三段式状态机对红外信号进行解析。状态机默认是在st_idle(空闲)状态,
并且此时time_cnt_clr的值为1,即time_cnt计数器停止计时;当监测到remote_in_d0为低电
平 之 后 , time_cnt_clr 的 值 为 0 , time_cnt 计 数 器 开 始 计 时 , 此 时 状 态 机 跳 转 到
st_start_low_9ms状态,在这里主要向大家介绍下程序是如何对9ms低电平的同步码进行计数
的。在代码的第160行,当检测到pos_remote_in(红外信号上升沿)为高电平时,说明此时红
外信号拉高,即同步码低电平结束,此时判断time_cnt的值是否接近9ms,如果接近9ms,此时
开始跳转到st_start_judge状态,否则跳转到空闲状态。程序后面对空闲信号、重复码以及数
据的检测方法类似,在此不再赘述。
图 24.4.4为SignalTap抓取的波形图,从图中可以清晰的看到红外驱动模块各个状态跳转
的波形图。可以观察到空闲状态时总线为高电平,按下遥控器按键后,发出9ms低电平的同步
码和4.5ms高电平的空闲信号,然后发出00000000的地址码和11111111的地址反码;接下来发
送10100010的控制码和01011101的控制反码。需要注意的是,红外遥控先发送的是数据的低位,
所以控制码为8’b01000101(8’d69),和图中的data(控制码)保持一致。在波形图的最后,
接收到了红外遥控器发出的重复码,当程序检测到重复码之后,repeat_en发出一次脉冲信号。
图 24.4.4 SignalTap抓取的波形图
LED控制模块代码如下:
1
module led_ctrl
(
2
input sys_clk
, //系统时钟
3
input sys_rst_n
, //系统复位信号,低电平有效
4
5
input repeat_en
, //重复码触发信号
6
output reg led //LED灯
7
);
8
9 //reg define
10
reg repeat_en_d0
; //repeat_en信号打拍采沿
11
reg repeat_en_d1
;
12
reg [22
:0
] led_cnt
; //LED灯计数器,用于控制LED灯亮灭
13
14 //wire define
15
wire pos_repeat_en
;
16
17 //*****************************************************
18 //** main code
19 //*****************************************************
20
21
assign pos_repeat_en
= ~repeat_en_d1
& repeat_en_d0
;
22
23 ////repeat_en信号打拍采沿
24
always @(posedge sys_clk
or negedge sys_rst_n
) begin
25
if(!sys_rst_n
) begin
26 repeat_en_d0
<= 1'b0
;
27 repeat_en_d1
<= 1'b0
;
28
end
29
else begin
30 repeat_en_d0
<= repeat_en
;
31 repeat_en_d1
<= repeat_en_d0
;
32
end
33
end
34
35
always @(posedge sys_clk
or negedge sys_rst_n
) begin
36
if(!sys_rst_n
) begin
37 led_cnt
<= 23'd0
;
38 led
<= 1'b0
;
39
end
40
else begin
41
if(pos_repeat_en
) begin
42 led_cnt
<= 23'd5_000_000
; //单次重复码:亮80ms 灭20ms
43 led
<= 1'b1
; //led亮的时间:4_000_000*20ns=80ms
44
end
45
else if(led_cnt
!= 23'd0
) begin
46 led_cnt
<= led_cnt
- 23'd1
;
47
if(led_cnt
< 23'd1_000_000
) //led灭的时间:1_000_000*20ns=20ms
48 led
<= 1'b0
;
49
end
50
end
51
end
52
53
endmodule
LED控制模块代码比较简单,首先检测repeat_en信号的上升沿(如代码的第24行开始的
always所示),pos_repeat_en拉高之后,计数器赋值为5_000_000,随后计数器每个周期开始
递减1,直到计数到0;在计数器在1_000_000~5_000_000范围内,点亮LED灯,其它情况熄灭LED
灯,从而指示红外遥控模块是否检测到重复码。
下载验证
首先我们打开红外遥控工程,在工程所在的路径下打开top_remote_rcv/par文件夹,在里
面找到“top_remote_rcv.qpf”并双击打开。注意工程所在的路径名只能由字母、数字以及下
划线组成,不能出现中文、空格以及特殊字符等。工程打开后如图 24.5.1所示。
图 24.5.1 打开工程
工程打开后通过点击工具栏中的“Programmer”图标(图中红框位置)打开下载界面,通过
“Add File” 按 钮 选择串 口工程 中 top_remote_rcv/par/output_files 目 录 下
“top_remote_rcv.sof”文件,下载界面如图 24.5.2所示(图中已下载完毕)。
图 24.5.2 下载界面
将下载器一端连电脑,另一端与开发板上对应端口连接,然后连接电源线并打开电源开关。
开发板电源打开后,在程序下载界面点击“Hardware Setup”, 在弹出的对话框中选择
当前的硬件连接为“USB-Blaster”。然后点击“Start”将工程编译完成后得到的sof文件下
载到开发板中。
下载完成后,按下遥控器上任意按键,就可以观察数码管上显示的数据了;长按按键的话,
可以观察到LED在不停地闪烁。需要注意的是,使用遥控器之前需要先将遥控器后部的塑料绝
缘片拔出,否则遥控器无法正常使用。