1、blaster的安装 通过quartus中的driver
2、quartus的安装
代码编写流程
- 工程创建
- verilog编写
- multisim仿真
- 上版验证
- 引脚接入
流水灯
module flow_led(
input sys_clk , //系统时钟
input sys_rst_n, //系统复位,低电平有效
output reg [3:0] led //4个LED灯
);
//reg define
reg [23:0] counter;
//*****************************************************
//** main code
//*****************************************************
//计数器对系统时钟计数,计时0.2秒
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n)
counter <= 24'd0;
else if (counter < 24'd1000_0000)
counter <= counter + 1'b1;
else
counter <= 24'd0;
end
//通过移位寄存器控制IO口的高低电平,从而改变LED的显示状态
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n)
led <= 4'b0001;
else if(counter == 24'd1000_0000)
led[3:0] <= {led[2:0],led[3]};
else
led <= led;
end
endmodule
// 由于此时频率的时钟为50Mhz 所以 这里10M时进行一次跳变,那么此时的时间为0.2s
按键控制蜂鸣器
按键消抖
![[Pasted image 20230503191838.png]]
![[截图_20230503191911.png]]
上述图片表示按键消抖后的作用
如何实现按键消抖:当按键发生跳动开始 维持0.02s表示该跳变有效
top_key_beep.v
module top_key_beep(
input sys_clk, //时钟信号50Mhz
input sys_rst_n, //复位信号
input key, //按键信号
output beep //蜂鸣器控制信号
);
//wire define
wire key_value;
wire key_flag;
//*****************************************************
//** main code
//*****************************************************
//例化按键消抖模块
key_debounce u_key_debounce(
.sys_clk (sys_clk),
.sys_rst_n (sys_rst_n),
.key (key),
.key_flag (key_flag),
.key_value (key_value)
);
//例化蜂鸣器控制模块
beep_control u_beep_control(
.sys_clk (sys_clk),
.sys_rst_n (sys_rst_n),
.key_flag (key_flag),
.key_value (key_value),
.beep (beep)
);
endmodule
按键消抖模块 key_debounce.v
module key_debounce(
input sys_clk, //外部50M时钟
input sys_rst_n, //外部复位信号,低有效
input key, //外部按键输入
output reg key_flag, //按键数据有效信号
output reg key_value //按键消抖后的数据
);
//reg define
reg [31:0] delay_cnt;
reg key_reg;
//*****************************************************
//** main code
//*****************************************************
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n) begin
key_reg <= 1'b1;
delay_cnt <= 32'd0;
end
else begin
key_reg <= key;
if(key_reg != key) //一旦检测到按键状态发生变化(有按键被按下或释放)
delay_cnt <= 32'd1000000; //给延时计数器重新装载初始值(计数时间为20ms)
else if(key_reg == key) begin //在按键状态稳定时,计数器递减,开始20ms倒计时
if(delay_cnt > 32'd0)
delay_cnt <= delay_cnt - 1'b1;
else
delay_cnt <= delay_cnt;
end
end
end
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n) begin
key_flag <= 1'b0;
key_value <= 1'b1;
end
else begin
if(delay_cnt == 32'd1) begin //当计数器递减到1时,说明按键稳定状态维持了20ms
key_flag <= 1'b1; //此时消抖过程结束,给出一个时钟周期的标志信号
key_value <= key; //并寄存此时按键的值
end
else begin
key_flag <= 1'b0;
key_value <= key_value;
end
end
end
endmodule
蜂鸣器控制模块 beep_control.v
module beep_control(
//input
input sys_clk, //系统时钟
input sys_rst_n, //复位信号,低电平有效
input key_flag, //按键有效信号
input key_value, //消抖后的按键信号
output reg beep //蜂鸣器控制信号
);
//*****************************************************
//** main code
//*****************************************************
always @ (posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)
beep <= 1'b1;
else if(key_flag && (~key_value)) //判断按键是否有效按下
beep <= ~beep;
end
endmodule
触摸按键
- 原理分析:任何两个导电的物体之间都存在着感应电容,一个按键即一个焊盘与大地也可构成一个感应电容,在周围环境不变的情况下,该感应电容值是固定不变的微小值。当有人体手指靠近触摸按键时,人体手指与大地构成的感应电容并联焊盘与大地构成的感应电容,会使总感应电容值增加。电容式触摸按键C在检测到某个按键的感应电容值发生改变后,将输出某个按键被按下的确定信号。电容式触摸按键因为没有机械构造,所有的检测都是电量的微小变化,所以对各种干扰会更加敏感,因此触摸按键设计、触摸面板的设计以及触摸C的选择都十分关键
相对于传统按键,抗干扰性弱
![[截图_20230503193756.png]]
TPAD当手指按下时,触控电容发生变化,从而影响值的变化
抓取上升沿
![[截图_20230503194702.png]]
利用信号的跳变进行上升沿的抓取,本质上就是信号什么时候发生变化
我们可以用两个信号,一个信号比另一个信号快一个周期,那么在时钟发生变化的
- 当快一个周期的信号为1,慢一个周期的信号为0,就可以实现上升沿的抓取
module touch_led(
//input
input sys_clk, //时钟信号50Mhz
input sys_rst_n, //复位信号
input touch_key, //触摸按键
//output
output reg led //LED灯
);
//reg define
reg touch_key_d0;
reg touch_key_d1;
//wire define
wire touch_en;
//*****************************************************
//** main code
//*****************************************************
//捕获触摸按键端口的上升沿,得到一个时钟周期的脉冲信号
assign touch_en = (~touch_key_d1) & touch_key_d0;
//对触摸按键端口的数据延迟两个时钟周期
always @ (posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n) begin
touch_key_d0 <= 1'b0;
touch_key_d1 <= 1'b0;
end
else begin
touch_key_d0 <= touch_key;
touch_key_d1 <= touch_key_d0;
end
end
//根据触摸按键上升沿的脉冲信号切换led状态
always @ (posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n)
led <= 1'b1; //默认状态下,点亮LED
else begin
if (touch_en)
led <= ~led;
end
end
endmodule
呼吸灯
FPGA不像单片机能够实现DAC,实现给定不同的IO不同的电压,可以通过PWM波实现
本质上就是调节PWM波的占空比
![[截图_20230503195328.png]]
通过设定一个值作为比较,例如 一个时钟周期为1ms,在这一ms过程中,计数器从1-50000,给一个比较值,这个比较值从0-50000来回变化,给一个标志位进行实现是如何变化方向的即可
module breath_led(
input sys_clk , //时钟信号50Mhz
input sys_rst_n , //复位信号
output led //LED
);
//reg define
reg [15:0] period_cnt ; //周期计数器频率:1khz 周期:1ms 计数值:1ms/20ns=50000
reg [15:0] duty_cycle ; //占空比数值
reg inc_dec_flag ; //0 递增 1 递减
//*****************************************************
//** main code
//*****************************************************
//根据占空比和计数值之间的大小关系来输出LED
assign led = (period_cnt >= duty_cycle) ? 1'b1 : 1'b0;
//周期计数器
always @(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)
period_cnt <= 16'd0;
else if(period_cnt == 16'd50000)
period_cnt <= 16'd0;
else
period_cnt <= period_cnt + 1'b1;
end
//在周期计数器的节拍下递增或递减占空比
always @(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n) begin
duty_cycle <= 16'd0;
inc_dec_flag <= 1'b0;
end
else begin
if(period_cnt == 16'd50000) begin //计满1ms
if(inc_dec_flag == 1'b0) begin //占空比递增状态
if(duty_cycle == 16'd50000) //如果占空比已递增至最大
inc_dec_flag <= 1'b1; //则占空比开始递减
else //否则占空比以25为单位递增
duty_cycle <= duty_cycle + 16'd25;
end
else begin //占空比递减状态
if(duty_cycle == 16'd0) //如果占空比已递减至0
inc_dec_flag <= 1'b0; //则占空比开始递增
else //否则占空比以25为单位递减
duty_cycle <= duty_cycle - 16'd25;
end
end
end
end
endmodule
静态数码管
![[截图_20230503201758.png]]
通过片选的方式减少引脚的使用
代码实现思路:
- 通过一个选通信号将六个数码管全选通
- 利用一个always模块对数字进行加,实现数字循环,
- 通过case对输出的数码管段选信号进行选择
seg_led_static_top.v
module seg_led_static_top (
input sys_clk , // 系统时钟
input sys_rst_n, // 系统复位信号(低有效)
output [5:0] seg_sel , // 数码管位选
output [7:0] seg_led // 数码管段选
);
//parameter define
parameter MAX_NUM = 25000_000; // 数码管变化的时间间隔0.5s
//wire define
wire add_flag; // 数码管变化的通知信号
//*****************************************************
//** main code
//*****************************************************
//每隔0.5s产生一个时钟周期的脉冲信号
time_count #(
.MAX_NUM (MAX_NUM)
) u_time_count(
.clk (sys_clk ),
.rst_n (sys_rst_n),
.add_flag (add_flag )
);
//每当脉冲信号到达时,使数码管显示的数值加1
seg_led_static u_seg_led_static (
.clk (sys_clk ),
.rst_n (sys_rst_n),
.add_flag (add_flag ),
.seg_sel (seg_sel ),
.seg_led (seg_led )
);
endmodule
time_count.v 每0.5s产生一个脉冲信号
module time_count(
input clk , // 时钟信号
input rst_n , // 复位信号
output reg add_flag // 一个时钟周期的脉冲信号
);
//parameter define
parameter MAX_NUM = 25000_000; // 计数器最大计数值
//reg define
reg [24:0] cnt; // 时钟分频计数器
//*****************************************************
//** main code
//*****************************************************
//计数器对时钟计数,每计时到0.5s,输出一个时钟周期的脉冲信号
always @ (posedge clk or negedge rst_n) begin
if (!rst_n) begin
add_flag <= 1'b0;
cnt <= 1'b0;
end
else if(cnt < MAX_NUM - 1'b1) begin
cnt <= cnt +1'b1;
add_flag <= 1'b0;
end
else begin
cnt <= 1'b0;
add_flag <= 1'b1;
end
end
endmodule
module seg_led_static (
input clk , // 时钟信号
input rst_n , // 复位信号(低有效)
input add_flag, // 数码管变化的通知信号
output reg [5:0] seg_sel , // 数码管位选
output reg [7:0] seg_led // 数码管段选
);
//reg define
reg [3:0] num; // 数码管显示的十六进制数
//*****************************************************
//** main code
//*****************************************************
//控制数码管位选信号(低电平有效),选中所有的数码管
always @ (posedge clk or negedge rst_n) begin
if (!rst_n)
seg_sel <= 6'b111111;
else
seg_sel <= 6'b000000;
end
//每次通知信号到达时,数码管显示的十六进制数值加1
always @ (posedge clk or negedge rst_n) begin
if (!rst_n)
num <= 4'h0;
else if(add_flag) begin
if (num < 4'hf)
num <= num + 1'b1;
else
num <= 4'h0;
end
else
num <= num;
end
//根据数码管显示的数值,控制段选信号
always @ (posedge clk or negedge rst_n) begin
if (!rst_n)
seg_led <= 8'b0;
else begin
case (num)
4'h0 : seg_led <= 8'b1100_0000;
4'h1 : seg_led <= 8'b1111_1001;
4'h2 : seg_led <= 8'b1010_0100;
4'h3 : seg_led <= 8'b1011_0000;
4'h4 : seg_led <= 8'b1001_1001;
4'h5 : seg_led <= 8'b1001_0010;
4'h6 : seg_led <= 8'b1000_0010;
4'h7 : seg_led <= 8'b1111_1000;
4'h8 : seg_led <= 8'b1000_0000;
4'h9 : seg_led <= 8'b1001_0000;
4'ha : seg_led <= 8'b1000_1000;
4'hb : seg_led <= 8'b1000_0011;
4'hc : seg_led <= 8'b1100_0110;
4'hd : seg_led <= 8'b1010_0001;
4'he : seg_led <= 8'b1000_0110;
4'hf : seg_led <= 8'b1000_1110;
default : seg_led <= 8'b1100_0000;
endcase
end
end
endmodule
PLL IP核调用
PLL:Phase Locked Loop(锁相环)
PLL是一种反馈控制电路,其特点是利用外部输入的参考信号控制环路内部震荡信号的频率和相位。
![[截图_20230503210622.png]]
Quartus Il软件提供了锁相环PLL的IP核,对时钟网络进行系统级的时钟管理和偏移控制,具有时钟倍频、分频、相位偏移和可编程占空比的功能。
本节的实验任务是将ALTPLL IP核产生的四个时钟,输出到FPGA的扩展口I1O上,时钟分别是100Mhz,100Mhz(相位偏移180废)、50Mhz和25Mhz。
如何创建IP核
![[截图_20230504090849.png]]
ctclone4 中文手册中讲述了
源同步模式
无补偿模式
正常模式
零延迟缓冲模式
![[截图_20230504091836.png]]
设置系统时钟的频率,并且设置PLL的模式
![[截图_20230504092000.png]]
1、是否创建一个复位对PLL进行异步复位(什么是异步什么是同步)
异步复位:它是指无论时钟沿是否到来,只要复位信号有效,就对系统进行复位。
同步复位:是复位信号和时钟同步,当时钟上升沿检测到复位信号,执行复位操作。
2、是否创建一个相位使能?
3、是否创建一个locked(指示信号生成已经处于稳定状态)
![[截图_20230504092328.png]]
高级功能,暂不使用
![[截图_20230504092410.png]]
选择时钟的切换,由于开发板只有一个晶振,所以不用设置
![[截图_20230504092455.png]]
动态配置,动态调整信号
![[截图_20230504092518.png]]
输出时钟信号的设置
![[截图_20230504092806.png]]
设置完成之后
![[截图_20230504093037.png]]
需要对IP核进行仿真需要调用IP仿真库
如何使用IP核
1、选择魔法棒,编辑一个已经存在的IP核 可以重新对时钟进行配置
此时只有 pll_ckj.qip 文件,包含了PLL的IP核的文件
顶层文件
module ip_pll(
input sys_clk , //系统时钟
input sys_rst_n , //系统复位,低电平有效
//输出时钟
output clk_100m , //100Mhz时钟频率
output clk_100m_180deg, //100Mhz时钟频率,相位偏移180度
output clk_50m , //50Mhz时钟频率
output clk_25m //25Mhz时钟频率
);
//wire define
wire rst_n ; //复位信号
wire locked ; //locked信号拉高,锁相环开始稳定输出时钟
//*****************************************************
//** main code
//*****************************************************
//系统复位与锁相环locked相与,作为其它模块的复位信号
assign rst_n = sys_rst_n & locked;
//锁相环
pll_clk u_pll_clk(
.areset (~sys_rst_n ), //锁相环高电平复位,所以复位信号取反
.inclk0 (sys_clk ),
.c0 (clk_100m ),
.c1 (clk_100m_180deg),
.c2 (clk_50m ),
.c3 (clk_25m ),
.locked (locked )
);
endmodule
进行时序分析
![[截图_20230504094032.png]]
创建网表
![[截图_20230504094105.png]]
![[截图_20230504094224.png]]
创建系统时钟
![[截图_20230504094301.png]]
derivepll的clock,并将SDC文件添加到工程
RAM IP核的使用
参考Cyclone 4 中文手册
![[截图_20230504130925.png]]
- data[] 写入数据
- address[] 写入地址
- wren 写使能
- byteena[] 字节使能信号
- addressstall 地址使能时钟 拉高时,地址不改变,就是stop
- inclock 输入时钟
- inclocken
- rden 读使能信号
- aclr
- q[] 输出读数据
- outclock 读时钟
- outclocken 读时钟写使能
字节使能支持
Cyclone IV器件 M9K存储器模块支持字节使能,该功能屏蔽了输入数据,这样仅写入 数据中的指定字节。未被写入的字节保留之前写入的值。wren信号以及字节使能 (byteena) 信号一起控制 RAM 模块的写操作。byteena 信号默认为高电平 ( 使能的 ), 这种情况下写操作仅由wren信号来控制。byteena寄存器的清零端口是不存在的。当 写端口具有 ×16, ×18, ×32, 或者 ×36 位数据位宽时,M9K 模块将支持字节使能。
![[Pasted image 20230504131908.png]]
时序图
![[截图_20230504131917.png]]
数据读写
- 时序图:读取RAM数据
![[截图_20230504131957.png]] - 时序图写入数据 ![[截图_20230504132005.png
在时钟触发时的边界情况总结(基于RAM实验)
顶层模块
module ip_1port_ram(
input sys_clk , //系统时钟
input sys_rst_n //系统复位,低电平有效
);
//wire define
wire ram_wr_en ; //ram写使能
wire ram_rd_en ; //ram读使能
wire [4:0] ram_addr ; //ram读写地址
wire [7:0] ram_wr_data ; //ram写数据
wire [7:0] ram_rd_data ; //ram读数据
//*****************************************************
//** main code
//*****************************************************
//ram读写模块
ram_rw u_ram_rw(
.clk (sys_clk),
.rst_n (sys_rst_n),
.ram_wr_en (ram_wr_en ),
.ram_rd_en (ram_rd_en ),
.ram_addr (ram_addr ),
.ram_wr_data (ram_wr_data),
.ram_rd_data (ram_rd_data)
);
//ram ip核
ram_1port u_ram_1port(
.address (ram_addr),
.clock (sys_clk),
.data (ram_wr_data),
.rden (ram_rd_en),
.wren (ram_wr_en),
.q (ram_rd_data)
);
endmodule
ram 读写模块逻辑
module ram_rw(
input clk , //时钟信号
input rst_n , //复位信号,低电平有效
output ram_wr_en , //ram写使能
output ram_rd_en , //ram读使能
output reg [4:0] ram_addr , //ram读写地址
output reg [7:0] ram_wr_data, //ram写数据
input [7:0] ram_rd_data //ram读数据
);
//reg define
reg [5:0] rw_cnt ; //读写控制计数器
//*****************************************************
//** main code
//*****************************************************
//rw_cnt计数范围在0~31,ram_wr_en为高电平;32~63时,ram_wr_en为低电平
assign ram_wr_en = ((rw_cnt >= 6'd0) && (rw_cnt <= 6'd31) && rst_n) ? 1'b1 : 1'b0;
//rw_cnt计数范围在32~63,ram_rd_en为高电平;0~31时,ram_rd_en为低电平
assign ram_rd_en = ((rw_cnt >= 6'd32) && (rw_cnt <= 6'd63)) ? 1'b1 : 1'b0;
//读写控制计数器,计数器范围0~63
always @(posedge clk or negedge rst_n) begin
if(rst_n == 1'b0)
rw_cnt <= 6'd0;
else if(rw_cnt == 6'd63)
rw_cnt <= 6'd0;
else
rw_cnt <= rw_cnt + 6'd1;
end
//读写控制器计数范围:0~31 产生ram写使能信号和写数据信号
always @(posedge clk or negedge rst_n) begin
if(rst_n == 1'b0)
ram_wr_data <= 8'd0;
else if(rw_cnt >= 6'd0 && rw_cnt <= 6'd31)
ram_wr_data <= ram_wr_data + 8'd1;
else
ram_wr_data <= 8'd0;
end
//读写地址信号 范围:0~31
always @(posedge clk or negedge rst_n) begin
if(rst_n == 1'b0)
ram_addr <= 5'd0;
else if(ram_addr == 5'd31)
ram_addr <= 5'd0;
else
ram_addr <= ram_addr + 1'b1;
end
endmodule
上述读写模块的逻辑
1、生成一个0-63的计数器,当复位信号到来时计数器置0
2、写控制器:当计数为0-31时,向RAM中写入数据
3、读控制器:当计数为32-63时,向RAM中读出数据
![[截图_20230504150713.png]]
![[截图_20230504151816.png]]
此时为读操作,读操作为什么得到的数据会延迟一个周期呢?
总结:当进行数据读时,得到的数据有一个周期的延迟,因为上升沿进行读取的时候 是在下一个上升沿到来时才读取到信号
双端口存储器
- 利用PLL IP核生成两个时钟
- 利用双端口RAM
![[截图_20230504153342.png]]
- data[]:写入数据
- wraddress[] 写入地址
- wren 写使能
- bytenna[] 字节控制
- wr_addressstall 写地址stop
- wrclock 写时钟
- wrclocken 写时钟使能
- aclr 异步复位信号
- rdaddress 读地址
- rden 读使能
- q[] 读输出
- rd_addressstall 读地址stop
- rdclock 读时钟
- rdclocken 读时钟写使能
IP核 FIFO
FIFO一般指的是对数据的存储具有先进先出特性的一个缓存器被用于数据的缓存,或者高速异步数据的交互也即所谓的跨时钟域信号传递。
RAM通过地址来寻址
顺序存储数据利用FIFO
用于高速异步交互,跨时钟域传递。
例子:当两个设备的时钟不同,通过中间连一个FIFO实现数据传输
FIFO的分类:
同步FIFO : 指读时钟和写时钟为同一个时钟
异步FIFO :指读写时钟不一致,读写时是互相独的
异步混合位宽FIFO:指读写时钟不一致,且读写数据位宽也可以不一致
SCFIFO 同步
![[截图_20230504162219.png]]
- data[] 写入数据
- wrreq 写入请求
- rdreq 读入请求
- clock 时钟信号
- sclr 同步复位
- aclr 异步复位
- q[] 输出状态
- full FIFO写入满
- almost_full 几乎满了
- empty FIFO空
- almost_empty 几乎空了
![[截图_20230504163001.png]]
FIFO 没有地址
串行通信基础
并行通信与串行通信比较
并行通信
![[截图_20230504181445.png]]
优点:传输速度快 缺点:占用引脚多
串行通信
![[截图_20230504181601.png]]
优点:通信线路简单、占用引脚资源少缺点:传输速度慢
同步通信与异步通信比较
同步通信:带时钟同步信号的数据传输;发送方和接收方在同一时钟的控制下,同步传输数据。
![[截图_20230504181745.png]]
由一个时钟对信号进行控制
异步通信:没有时钟线约束,通过波特率进行约束实现数据传输,例如约束的波特率为9600,接受与发送接收9600,就可以实现数据传输不发生错误
单工 半双工 全双工
单工:数据只能沿一个方向传输
半双工:数据传输可以沿两个方向,但需要分时进行
全双工:数据可以同时进行双向传输
![[截图_20230504182110.png]]
常见的串行通信接口 UART 单总线 SPI I2C
![[截图_20230504182245.png]]
UART
它在发送数据时将并行数据转换成串行数据来传输,在接收数据时将接收到的串行数据转换成并行数据,UART串口通信需要两根信号线来实现,一根用于串口发送,另外一根负责串口接收
![[截图_20230504182806.png]]
校验位:通过是奇校验 与 偶校验的形式 调整校验位
起始位: 为0
停止位:1
例如此时的波特率为115200,传输数据无论如何就是为1s发送115200bit数据
在verilog代码中,如何对比波特率呢?
接口标准:
![[截图_20230504183359.png]]
RS232
![[截图_20230504184131.png]]
CH340C 实现USB转串口
程序设计
![[截图_20230504184907.png]]
434个时钟周期发送一个数据,1s发送115200波特率
50M/115200 = 434(clk/s)
sys_clk : 系统时钟
uart_rxd : 数据发送过程
start_flag 开始位
rx_flag:接受标志位
clk_cnt:计数时钟
rx_cnt: 计数发送数据数量
uart_done: 停止位到达时为0
框架:
![[截图_20230504185557.png]]
在uart_recv中编码思路:
通过双寄存器抓取下降沿,实现start_flag的实现