项目介绍
利用iCE40核心板和底板等,实现可定时的音乐始终。任务要求:
(1)使用扩展板上的12颗彩灯对应于12个小时
(2)核心板上的FPGA产生时钟,在OLED显示评上通过模拟或者数字的方式显示当前的时间 - 小时、分、秒
(3)将“小时”的信息通过12颗彩灯来显示,效果自行设计
(4)具有定时的功能,通过扩展板上的按键设置时间,到该时间点即(彩灯闪烁 + 音频播放),持续5秒钟时间
(5)音频播放通过扩展板上的蜂鸣器来实现
硬件介绍
iCE40UP5K是莱迪思半导体推出的一款高性能、低功耗的现场可编程门阵列(FPGA)。其基本架构包括逻辑单元、可编程互连资源、输入输出模块。逻辑单元采用查找表(LUT)结构,每个LUT可以存储多个逻辑函数的真值表,通过组合这些LUT可以实现复杂的逻辑功能。可编程互连资源包括一系列的可编程开关和连接矩阵,用于连接不同的逻辑单元和其他资源。用户可以根据需要灵活地配置这些连接,以实现特定的电路功能。输入输出模块负责与外部电路进行数据交互,提供多种电气标准和接口类型,以便与其他芯片或设备进行连接。
方案框图和项目设计思路
方案框图
项目设计思路
项目设计思路是利用iCE40核心板生成时钟信号,通过OLED屏展示当前时间,同时将小时信息通过扩展板上的12颗彩灯直观显示,用户可通过按键设置定时,当到达设定时间时,对应的彩灯会闪烁且蜂鸣器发出音频提示,持续5秒,以此实现时间的可视化展示与定时提醒功能。该项目可以拆分为时间显示、时间设置、音乐报时三个小项目。因此设计了时间控制、OLED控制、按键控制、时间差计算、蜂鸣器控制等模块来实现。按键控制模块设置当前时间和定时时间,在设计这个模块时我也设计了按键消抖模块,防止在按键按下或释放时产生的抖动影响信号的稳定性。时间控制模块用于计算当前时间并按照1s时间间隔自增。时间差计算模块用于定时项目,便于观察当前时间距离定时时间的间隔。蜂鸣器控制模块负责控制蜂鸣器的启动、停止、切换和使能。同时添加beeper 模块,通过生成不同音符和节拍的控制信号来控制蜂鸣器的发声,同时通过按键K2控制歌曲的切换,并通过LED显示。OLED控制模块为与OLED显示屏进行交互的模块,输入信号包括当前时间、下一时间、时间差等,模块将这些信息显示在OLED屏幕上
软件流程图和关键代码介绍
软件流程图
关键代码介绍
按键消抖模块
(1)按键信号延迟:
always @ (posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n) begin
key_d0 <= 1'b1;
key_d1 <= 1'b1;
end
else begin
key_d0 <= key;
key_d1 <= key_d0;
end
end
利用key_d0和key_d1将输入的按键信号延迟两个时钟周期。通过这种方式,能够将当前的按键状态与之前的状态进行比较,帮助判断按键是否有稳定变化。
(2)消抖计数器:
always @ (posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)
cnt <= 20'd0;
else begin
if(key_d1 != key_d0) //检测到按键状态发生变化
cnt <= CNT_MAX; //则将计数器置为20'd100_0000,
//即延时100_0000 * 20ns(1s/50MHz) = 20ms
else begin //如果当前按键值和前一个按键值一样,即按键没有发生变化
if(cnt > 20'd0) //则计数器递减到0
cnt <= cnt - 1'b1;
else
cnt <= 20'd0;
end
end
end
设计了一个20位计数器cnt,用于消除按键抖动。计数器初始值为0,如果检测到按键状态发生变化(key_d1和key_d0不同),则计数器会被重新加载到 CNT_MAX(20ms的时间延迟)。如果按键状态没有变化,计数器将递减,直到为 0。当计数器为0时,表示按键的状态已经稳定。
(3)按键稳定信号输出:
always @ (posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)
key_filter <= 1'b1;
//在计数器递减到1时送出按键值
else if(cnt == 20'd1)
key_filter <= key_d1;
else
key_filter <= key_filter;
end
always @ (posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)
key_filter_d1 <= 1'b1;
//在计数器递减到1时送出按键值
else
key_filter_d1 <= key_filter;
end
当计数器值递减到1时,key_filter被更新为key_d1,即按键的稳定状态。这个信号key_filter会持续直到计数器完成下一轮的计数。
key_filter_d1是key_filter 的延迟版,帮助确保信号稳定后才能最终生成一个脉冲输出。
(4)按键脉冲输出:
always @ (posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)
key_pulse <= 1'b0;
//在计数器递减到1时送出按键值
else
key_pulse <= !key_filter&key_filter_d1;
end
最终的消抖信号 key_pulse由key_filter和key_filter_d1比较得到。key_filter表示的即为按键按下这一动作,输出一个脉冲信号,表示按键的稳定变化。
按键控制模块
module key1_decode
#(
parameter OLED_MODE_MAX = 4'd8
)
(
input clk,
input rst,
input key1_pulse,
output reg [3:0] oled_mode
);
always @(posedge clk or negedge rst)begin
if (!rst)
oled_mode <= 4'd0;
else if (key1_pulse)
if(oled_mode == OLED_MODE_MAX - 1'b1)
oled_mode <= 1'b0;
else
oled_mode <= oled_mode + 1'b1;
else
oled_mode <= oled_mode;
end
endmodule
这个key1_decode
模块的作用是对按键key1_pulse
进行解码,并用于OLED显示模式切换。开发板上的K1是模式控制按键,一共控制8种模式:
Mode0:显示模式,只有在此模式下才会触发定时功能,即触发一圈灯闪烁效果及蜂鸣器效果,其他模式都是设置模式;
Mode1:设置当前时间的小时选项;
Mode2:设置当前时间的分钟选项;
Mode3:设置当前时间的秒选项;
Mode4:设置定时功能时间的小时选项;
Mode5:设置定时功能时间的分钟选项;
Mode6:设置定时功能时间的秒选项;
Mode7:设置蜂鸣器播放的音乐。
Mode1~Mode7这些模式下进行相应的设置操作都需要按K2。
按键 key1_pulse 每次触发,OLED 模式递增 1,直到最大模式后循环回 0。
时间控制模块
//数字钟自增与按键叠加
always @(posedge clk or negedge rst) begin
if (!rst) begin
counter <= 0; sec <= 27; min <= 54; hour <= 1;
curr_hour0 <= 0; curr_hour1 <= 0; curr_min0 <= 0; curr_min1 <= 0;
curr_sec0 <= 0; curr_sec1 <= 0;
end else begin
if (counter == MAX_1s - 1) begin counter <= 0;
if(key2_pulse)begin
case(oled_mode)
3:begin//按键调节秒
if (sec == 58) begin sec <= 0;
if (min == 59) begin min <= 0;
hour <= (hour == 11) ? 0 : hour + 1; // 12小时制
end else min <= min + 1; // 分钟+1
end else sec <= sec + 2; // 秒+1
end
2:begin//按键调节分
if (sec == 59) begin sec <= 0;
if (min == 58) begin min <= 0;
hour <= (hour == 11) ? 0 : hour + 1; // 12小时制
end else min <= min + 1; // 分钟+1
end else begin
sec <= sec + 1; // 秒+1
min <= min + 1;
end
end
1:begin//按键调节小时
if (sec == 59) begin sec <= 0;
if (min == 59) begin min <= 0;
hour <= (hour == 10) ? 0 : hour + 1; // 12小时制
end else min <= min + 1; // 分钟+1
end else begin
sec <= sec + 1; // 秒+1
hour <= hour + 1;
end
end
模块使用 sec(秒)、min(分钟)和 hour(小时)变量来表示当前时间。时间是基于一个外部时钟信号 clk 进行更新的。通过 counter 计数器控制每秒的递增,MAX_1s 为计数器的最大值。每当 counter 达到 MAX_1s 时,计时器更新sec、min 和 hour 的值。模块支持多种不同的时间计数模式,通过输入的 key2_pulse 触发,在不同的 oled_mode 下执行不同的时间更新逻辑。oled_mode 可能包括:只增加秒、增加分钟、增加小时,或者在不同的模式下根据按键输入递增对应的时间单位。
时间差计算模块
// 计算next时间和当前时间的差值
always @(posedge clk or negedge rst) begin
if (!rst) begin
diff_hour <= 1;diff_min <= 2;diff_sec <= 3;
borrow_min <= 0;borrow_hour <= 0;
end else begin
// 秒差的计算
if (next_sec < sec) begin
diff_sec <= 60 + next_sec - sec; // 借位
borrow_min <= 1; // 需要从分钟借位
end else begin
diff_sec <= next_sec - sec;
borrow_min <= 0; // 不需要借位
end
// 分钟差的计算
if (next_min < min) begin//一定需要借位
if(borrow_min)begin
diff_min <= 59 + next_min - min; // 借位
borrow_hour <= 1; // 需要从小时借位
end else begin
diff_min <= 60 + next_min - min; // 借位
borrow_hour <= 1; // 需要从小时借位
end
end else if(next_min == min)begin//可能需要借位
if(borrow_min)begin
diff_min <= 59; // 借位
borrow_hour <= 1; // 需要从小时借位
end else begin
diff_min <= 0; // 借位
borrow_hour <= 0; // 不需要从小时借位
end
end else begin//本身next_min>min,不需要借位
if(borrow_min)begin
diff_min <= next_min - min - 1;
borrow_hour <= 0; // 不需要借位
end else begin
diff_min <= next_min - min;
borrow_hour <= 0; // 不需要借位
end
end
// 小时差的计算
if (next_hour < hour) begin//一定需要借位
if(borrow_hour)begin
diff_hour <= 11 + next_hour - hour; // 借位
end else begin
diff_hour <= 12 + next_hour - hour; // 借位
end
end else if(next_hour == hour)begin//可能需要借位
if(borrow_hour)begin
diff_hour <= 11; // 借位
end else begin
diff_hour <= 0; // 借位
end
end else begin//本身next_hour>hour,不需要借位
if(borrow_hour)begin
diff_hour <= next_hour - hour - 1;
end else begin
diff_hour <= next_hour - hour;
end
end
end
end
该模块接收当前时间(sec, min, hour)和目标时间(next_sec, next_min, next_hour)。计算秒差、分钟差和小时差,特别地,对于每个时间单位(秒、分钟、小时),如果目标时间小于当前时间,模块会进行借位(从上一级时间单位借位)计算出的小时、分钟和秒的差值将被格式化成两位数的显示形式,分别存储在 diff_hour1, diff_hour0(小时的高位和低位)、diff_min1, diff_min0(分钟的高位和低位)、diff_sec1, diff_sec0(秒的高位和低位)中,方便后续显示
蜂鸣器使能控制模块
(1)蜂鸣器启动 (beeper_start):
always@(posedge clk or negedge rst)
if(!rst)
beeper_start <= 1'b0;
else if(oled_mode == 0 && diff_hour == 0 && diff_min == 0 && diff_sec == 0)
beeper_start <= 1'b1;
else
beeper_start <= 1'b0;
当oled_mode==0且diff_hour == 0, diff_min == 0, diff_sec == 0(即目标时间与当前时间的差值为零),表示目标时间已到,蜂鸣器应当启动。
在这种情况下,beeper_start 被设置为 1,表示蜂鸣器开始响铃。
(2)蜂鸣器停止(beeper_stop):
always@(posedge clk or negedge rst)
if(!rst)
beeper_stop <= 1'b0;
else if(oled_mode == 0)
beeper_stop <= key2_pulse;
else
beeper_stop <= 1'b0;
当 oled_mode == 0(表示某个模式下),key2_pulse(按键脉冲)为有效信号时,蜂鸣器停止响铃。
只要按键触发,beeper_stop 会被设置为 1,停止蜂鸣器。
(3)蜂鸣器切换 (beeper_switch):
always@(posedge clk or negedge rst)
if(!rst)
beeper_switch <= 1'b0;
else if(oled_mode == 7)
beeper_switch <= key2_pulse;
else
beeper_switch <= 1'b0;
当 oled_mode == 7 时,key2_pulse 触发时,蜂鸣器的切换功能被启用。此时,beeper_switch 会根据按键状态切换蜂鸣器的工作状态(例如启用或禁用)。
(4)蜂鸣器使能(beeper_en):
always@(posedge clk or negedge rst)
if(!rst)
beeper_en <= 1'b0;
else if(beeper_start)
beeper_en <= 1'b1;
else if(beeper_stop)
beeper_en <= 1'b0;
else
beeper_en <= beeper_en;
beeper_en 控制蜂鸣器是否开启。它的状态取决于 beeper_start 和 beeper_stop:
当 beeper_start 为 1(蜂鸣器应启动时),beeper_en 被设为 1,启用蜂鸣器。
当 beeper_stop 为 1(蜂鸣器应停止时),beeper_en 被设为 0,禁用蜂鸣器。
蜂鸣器模块
beeper 模块用于通过生成不同音符和节拍的控制信号来控制蜂鸣器的发声,同时通过按键K2控制歌曲的切换,并通过LED显示蜂鸣器的状态用于上板debug。
//当蜂鸣器使能时,计数器按照计数终值(分频系数)计数
always@(posedge clk_in or negedge rst_n_in) begin
if(rst_n_in == 1'b0) begin
time_cnt <= 1'b0;
end else if(tone_en == 1'b0) begin
time_cnt <= 1'b0;
end else if(time_cnt>=time_end) begin //计数清零
time_cnt <= 1'b0;
end else begin
time_cnt <= time_cnt + 1'b1;
end
end
//根据计数器的周期,翻转蜂鸣器控制信号
always@(posedge clk_in or negedge rst_n_in) begin
if(rst_n_in == 1'b0) begin
piano_out <= 1'b0;
end else if(tone==5'd0) begin //没有音节时,不让蜂鸣器出声
piano_out <= 1'b0;
end else if(time_cnt==time_end) begin
piano_out <= ~piano_out; //蜂鸣器控制输出翻转,两次翻转为1Hz
end else begin
piano_out <= piano_out;
end
end
time_cnt 计数器用来控制蜂鸣器的发声周期。time_end 控制蜂鸣器的频率,根据 tone 来设置 time_end 的值,从而控制蜂鸣器的频率。根据 time_cnt 和 time_end,当计数器达到设定的 time_end 值时,翻转 piano_out 信号,从而控制蜂鸣器的开关,产生音频输出。
OLED控制模块
此模块为与OLED显示屏进行交互的模块,输入信号包括当前时间、下一时间、时间差等,模块将这些信息显示在OLED屏幕上。同时,代码还根据oled_mode
输入信号显示不同的模式,并当显示为音乐模式Mode7时根据key2_mode7
信号的输入变化文本内容。
代码中包含一个状态机(state),有多个状态如IDLE、MAIN、INIT、SCAN、WRITE、DELAY、CHINESE、CHINESE_FAN,用于控制显示屏的显示行为:
(1)INIT(初始化OLED显示屏)
作用:此状态用于初始化OLED显示屏,确保显示屏处于正确的工作状态。
具体操作:
在此状态下,首先进行OLED复位操作,关闭显示屏,使其进入初始化阶段。
然后通过spi总线发送控制指令进行一些硬件配置,例如设置电源、时钟等,确保显示屏能够正常工作。
在初始化过程中,程序会等待一定的延时(DELAY状态),以确保每个初始化步骤完成后,显示屏能够正常响应。
初始化完成后,切换到下一个状态(通常是MAIN状态)。
(2)MAIN(显示不同模式下oled屏幕每一行的内容)
作用:在此状态下,OLED显示屏用于显示各种信息,包括当前时间、下一时间、时间差和显示模式。
具体操作:
该状态根据不同的时间和模式信息来更新显示内容。例如,显示当前时间、下一时间、时间差以及OLED的工作模式。cnt_main计数器在此状态下循环,每次刷新显示屏,更新显示的内容。例如,可以显示格式化的时间字符串(如"Curr Time: 12:30:45"),并显示模式信息(如"Mode: 0")。调用SCAN状态以扫描并更新显示内容。
(3)SCAN(扫描显示内容,循环调用WRITE状态往oled屏幕中写入一行)
作用:扫描并更新显示内容,确保显示内容准确无误。
具体操作:
在此状态下,程序会扫描并根据需要更新每个位置的字符。根据当前的cnt_scan值,OLED的不同位置会显示不同的字符或信息。此状态通常用于控制字符显示的顺序。例如,在一次扫描过程中,可能需要逐个字符地将内容从内存中读取并显示到屏幕上。一旦扫描完成,系统返回到MAIN状态或继续执行下一个操作。
(4)WRITE(写入数据到OLED屏幕,每次写入一个字节)
作用:将字符数据写入OLED屏幕,以便显示信息。
具体操作:
通过oled_dcn和oled_dat信号控制数据和命令的传输。每个字符的8位数据会逐一通过oled_dat发送到OLED显示器,oled_clk控制数据的时序。该状态在SCAN状态中被调用,用于根据字符数据写入显示内容。
(5)DELAY(设置延迟,确保显示内容正确刷新)
作用:提供适当的延迟,以确保OLED屏幕正确刷新并显示稳定内容。
具体操作:
在某些操作(如初始化或数据写入后)完成后,系统会进入DELAY状态。在此状态下,系统会根据设定的延迟时间(例如num_delay)暂停一段时间,确保显示的内容能够正确地稳定在屏幕上。一旦延迟时间结束,系统会返回到先前的状态(通常是MAIN或SCAN状态),继续进行下一步操作。
(6)CHINESE(从内存中读取中文字符进行显示,循环调用WRITE状态往oled屏幕中写入一行)
作用:该状态用于从内存中读取中文字符并显示在OLED屏幕上。
具体操作:
在此状态下,系统从mem_chinese内存中读取中文字符数据(每个汉字占用多个字节)。对应的字节数据被传送到显示屏,显示对应的汉字字符。每个汉字需要多次传输数据,因此需要逐字节、逐行地读取并显示每个汉字。一旦显示完成,系统通常返回到MAIN状态,继续显示其他内容。
(7)CHINESE_FAN(字符反向显示表示选中此行,循环调用WRITE状态往oled屏幕中写入一行)
作用:与CHINESE状态类似,但在此状态下,字符可能以反向或者其他特定的方式显示。
具体操作:
在此状态下,中文字符仍然从mem_chinese内存中读取,但字符的显示方式可能会有所不同。比如,字符的颜色可能会反转,或者按某种特殊方式进行显示(例如反向显示,背景和字符颜色互换)。类似于CHINESE状态,每个汉字都需要按字节逐一显示,但此状态可能包括额外的显示效果(如反向显示)。
一圈灯显示模块
该模块通过状态机控制LED灯的显示和时间控制
(1)模块常量和参数
时序参数:T0H, T0L, T1H, T1L分别定义了LED数据编码中0和1的高电平和低电平的时序。这些是基于WS2812协议的规范,用于控制LED颜色。
计时器:cnt_time_max定义了计时器的最大值,用于控制LED灯的转动周期。
复位时间:RST定义了LED灯循环复位的时间。
LED颜色:LED_1到LED_8定义了8个LED的颜色数据,通过25位二进制表示每个LED的RGB值。
状态:通过16个状态(IDLE, LED_one, LED_two, ... , RST_FSM)控制LED的状态和状态转移。
(2)LED灯圈旋转
always@(posedge clk or negedge rst ) begin
if(!rst)begin
cnt_time<=24'b0;
ledcolor1<=LED_1;
ledcolor2<=LED_2;
ledcolor3<=LED_3;
ledcolor4<=LED_4;
ledcolor5<=LED_5;
ledcolor6<=LED_6;
end
else if(cnt_time == cnt_time_max - 1'b1)begin
cnt_time<=24'b0;
ledcolor1<=ledcolor6;
ledcolor2<=ledcolor1;
ledcolor3<=ledcolor2;
ledcolor4<=ledcolor3;
ledcolor5<=ledcolor4;
ledcolor6<=ledcolor5;
end
else
cnt_time<=cnt_time+1'b1;
end
cnt_time:计时器,控制LED灯的轮转,24次计时后,LED的颜色会向前转移一位。
ledcolor1到ledcolor6:保存每个LED的颜色值,每次计时器溢出时,LED灯的颜色发生转移,实现灯光的循环旋转效果。
(3)PWM信号生成
always @(posedge clk or negedge rst)
begin
if(!rst)
cycle_cnt <= 7'd0;
else if(cycle_cnt == (T0H + T0L - 6'd1))
cycle_cnt <= 7'd0;
else if(state != RST_FSM)//涓€杞紶杈撲笅鏉ワ紝澶嶄綅鍚庡榻��
cycle_cnt <= cycle_cnt + 1'b1;
else
cycle_cnt <= 7'd0;
end
cycle_cnt:周期计数器,用于计时LED控制信号的高电平和低电平持续时间。
根据cycle_cnt的值和LED的数据编码规则,控制led_pwm信号,输出PWM信号来调节LED灯的亮度。
(4)状态机设计
FSM状态机:状态机根据state值控制LED灯的工作状态,包括从一个LED到下一个LED的状态转移(LED_one到LED_twe)。
state_tran:当完成一个LED的PWM信号传输后,状态机将进入下一个LED的显示,并完成一次完整的LED灯显示。
RST_FSM:在复位状态下,所有的LED灯将被初始化,状态机会等待复位计时器达到设定的复位时间(RST)后,返回到IDLE状态,开始下一轮灯光显示。
(5)LED控制信号的移位
shift:通过移位操作,逐位控制LED的颜色。bit_cnt计数器用于逐位移位,生成LED的显示信号。
具体的颜色数据通过ledcolor1, ledcolor2, ...来传递,通过bit_cnt逐位输出相应的RGB数据。
(6)开始/停止控制
flicker:根据start和stop信号,控制是否开始或停止LED灯的显示。当start信号为高时,flicker设置为1,开始LED的显示;当stop信号为高时,flicker设置为0,停止LED的显示。
(7)状态转移
在每个LED的显示周期结束后,状态机会根据state_tran的标志转移到下一个LED的显示状态。
当复位计时器rst_cnt到达复位时间时,state_tran_rst信号会被激活,重新开始新一轮的LED灯周期。
功能展示
FPGA资源占用
心得体会
这是我第一次使用ice板卡进行实验,对板子和编译环境都比较陌生。在写代码的过程中,思路还是比较清晰的,但真正落实的时候还是会出现各种bug。在整个实验过程中,我遇到了一系列的问题,从硬件连接错误到软件配置问题,再到设计逻辑上的缺陷。但是通过不断地查阅资料、向他人请教和自己的思考实践,我逐渐学会了如何分析问题、定位问题的根源并找到有效的解决方案。通过这次实验,我对FPGA技术有了更深入的理解,以后的学习也有了方向。
最后非常感谢电子森林举办寒假练活动,给予我不断学习、进取的机会,也要感谢各位老师的悉心指导。