上一篇教程介绍了NEXYS4 开发板中UART串口通信的使用方式,这一篇介绍USB接口接收鼠标和键盘信号
开发板USB芯片
NEXYS 4 开发板上有一个辅助微控制器PIC24FJ128,用来提供USB嵌入式host,上电后这个微控制器处于配置模式,要么把bitstrem下载到FPGA,要么被配置成使用其他资源。Bitstream被下载到FPGA后,这个微控制器被切换成USB host功能,不过只支持鼠标和键盘(不知道支不支持鼠标和键盘一起连接的那种)。这个微控制器还兼职控制micro SD卡,这部分之后再讲
看一下USB与FPGA相关的连接:
可以看到只有两根线,没有配置相关的引脚,不在我们的控制下(毕竟烧写都是用这块芯片控制的,FPGA才是它的slave),因此我们不深入了解这块PIC怎么用的,只考虑怎么使用USB信号
信号时序图
鼠标和键盘都是用11位信号,包括1位的开始码,小头优先的8位数据,1位奇偶校验位,和1位终结码,只是8位数据不同。这种控制协议实际上是PS/2接口的信号直接转到了USB接口上。时序图如下:
PS/2接口长这个样,在老电脑上出现,现在基本上看不到了
这个时序图画的不是很清楚,看另外一份文档能更清楚的看到细节:PS/2 Mouse Control
时钟信号和数据信号都是双向的,没人去驱动它们的时候就被电阻拉高成高电平。
接收鼠标传来的数据相对简单,host保持侦测时钟和数据,当时钟Clock的下降沿时,侦测到数据Data也拉低,代表一个数据包传送出来,之后的10个时钟下降沿,分别收到从最低位LSB到MSB的八位数据,1位的奇偶校验(1表示八位数据中1的位数为偶数,0是奇数),最后1位高电平表示数据包结束。
从Host发送指令到鼠标的步骤如下
- 一般时钟Clock信号都是由鼠标驱动的,只有Host发送指令出去的时候要拉低一次,就是标为1的位置,Host驱动时钟成低电平,并保持大约60us
- 在时钟拉低时,也就是标为2的位置,把数据Data信号也拉低
- 60us结束后把时钟Clock信号的控制权交回,开启侦测时钟,这时鼠标侦测到标为3的时钟上升沿,数据Data位0,准备接受指令
- 鼠标开始驱动时钟Clock,每个上升沿读取一位数据,Host可以在侦测到时钟下降沿时把数据位准备好,数据包顺序和读取时一样,1位起始低电位,8位LSB到MSB数据,1位奇偶校验,1位结尾高电位
- 把数据Data信号控制权交还
鼠标初始化
刚刚连上鼠标时,鼠标不会立刻开始传送数据到Host,具体流程如下:
- USB接口连上,鼠标开始自测试,自测试完成发送0xAA给Host
- 紧接着0xAA,鼠标自动接上固定的设备ID号0x00
- Host发送鼠标数据流使能指令0xF4
- 等待鼠标传回3个字节为单位的数据流
每当鼠标有移动或者点下之类的操作时,就会送出三个11位数,有效数是24位即3个字节,其形式如下,注意传送数据是低位优先的
- 第一个字节[7:6] YY和XY,分别表示纵向和横向移动速度是否溢出,1表示溢出,这时移动的量以最大值计算
- [5:4] YS和XS,分别表示纵向和横向移动方向,1表示向左或者向下,0表示向右或者向上
- [1:0] R和L,分别表示右键和左键,1表示按下
- 第二个字节[7:0] 表示横向移动的量
- 第三个字节[7:0] 表示纵向移动的量
从Host传到鼠标的指令有如下这些,这里我们只用到带自测试的复位和数据流使能:
逻辑设计
从上面时序图中的时钟参数可以看出,鼠标的时钟频率大约是10kHz-16kHz左右,跟NEXYS 4开发板上的系统时钟100MHz相差很大,有6000倍左右,如果使用ChipScope,至少要6000*11=66000个系统时钟周期才能采集完,而ChipScope可以配置的最大长度才131072,只能采集两个字节。
若是使用串口,可以更方便的观察慢速数据,还可以传送指令到FPGA中。因此这次我们分成两个阶段来设计,第一个阶段用串口传递指令,第二个阶段用状态机自动配置鼠标初始化
PS2接口控制模块
在开始顶层设计之前,我们来写一个PS2接口的控制模块ps2_transmitter.v
大概设计要求是:
- 可收可发,收到的串行数据转成8位并行数据,发送出去的8位并行数据也可以转为串行数据
- 有一个enable信号控制发送开始,有一个valid信号表示数据接收完成,都是单时钟脉冲
- 自动根据发送和接收控制USB接口两根线的使用权限
- 由于PS2数据包有奇偶校验,需要一位错误信号位
由这些设计要求,加上前面对PS2接口的分析,写出如下模块:
接口定义,在顶层需要提前把inout形式接口拆分成input,output,和oe(输出使能output enable)
module ps2_transmitter(
input clk,
input rst,
// ports for input data
input clock_in, // connected to usb clock input signal
input serial_data_in, // connected to usb data input signal
output reg [7:0] parallel_data_in, // 8-bit input data buffer, from the USB interface
output reg parallel_data_valid,// indicate the input data is ready or not
output reg data_in_error, // reading error when the odd parity is not matched
// ports for output date
output reg clock_out, // connected to usb clock output signal
output reg serial_data_out, // connected to usb data output signal
input [7:0] parallel_data_out, // 8-bit output data buffer, to the USB interface
input parallel_data_enable,// control signal to start a writing process
output reg data_out_complete,
output reg busy, // indicate the transmitter is busy
output reg clock_output_oe, // clock output enable
output reg data_output_oe // data output enable
);
资源定义
// State machine
parameter [3:0] IDLE = 4'd0;
parameter [3:0] WAIT_IO = 4'd1;
parameter [3:0] DATA_IN = 4'd2;
parameter [3:0] DATA_OUT = 4'd3;
parameter [3:0] INITIALIZE = 4'd4;
reg [3:0] state;
reg [3:0] next_state;
// Parallel data buffer
reg [10:0] data_out_buf;
reg [10:0] data_in_buf;
reg [3:0] data_count;
// Counter for clock and data output
reg [15:0] clock_count;
时钟下降沿探测
// Used to detect the falling edge of clock_in, to see if there is anything coming in
// If data coming in, then we cannot start writing data out
reg [1:0] clock_in_delay;
wire clock_in_negedge;
always @(posedge clk) begin
clock_in_delay <= {clock_in_delay[0], clock_in};
end
assign clock_in_negedge = (clock_in_delay == 2'b10) ? 1'b1 : 1'b0;
状态机部分
always @(posedge clk or posedge rst) begin
if(rst) begin
state <= IDLE;
end
else begin
state <= next_state;
end
end
always @(posedge clk) begin
case(state)
IDLE: begin
next_state <= WAIT_IO;
clock_output_oe <= 1'b0;
data_output_oe <= 1'b0;
data_in_error <= 1'b0;
data_count <= 4'd0;
busy <= 1'b0;
parallel_data_valid <= 1'b0;
clock_count <= 16'd0;
data_in_buf <= 11'h0;
data_out_buf <= 11'h0;
clock_out <= 1'b1;
serial_data_out <= 1'b1;
data_out_complete <= 1'b0;
parallel_data_in <= 8'h00;
end
等待状态,当探测到时钟下降沿时,开启读取状态;当有送数据请求时,缓存好并行数据以及其奇偶校验位、结束位,开启送数据初始化状态。需要注意的是,在这一步中,读取或者送出初始低电平都直接完成了
// If the clock is driven low by mouse, then start reading
// If need to send data, and not in data reading mode, then start sending
// Indicate busy when leaving this state
WAIT_IO: begin
if(clock_in_negedge) begin // input data detected, and the start bit is ignored
next_state <= DATA_IN;
busy <= 1'b1;
data_count <= 4'd0;
end
else if(parallel_data_enable) begin // output data enable detected, and send out the start bit right here
next_state <= INITIALIZE;
busy <= 1'b1;
data_count <= 4'd0;
clock_output_oe <= 1'b1;
clock_out <= 1'b0; // drive low for about 60us to initialize output
data_out_buf <= {parallel_data_out[0],parallel_data_out[1],parallel_data_out[2],parallel_data_out[3],
parallel_data_out[4],parallel_data_out[5],parallel_data_out[6],parallel_data_out[7],
~^(parallel_data_out), 2'b11};
data_output_oe <= 1'b1;
serial_data_out <= 1'b0;
end
end
读取数据状态,每个时钟下降沿就向移位寄存器中塞一位,塞到10位,就可以逆序输出并行数据了,顺便检查一下奇偶校验位
// After the start bit, detect 10 falling edge on clock pin, and shift record the data
// When finish, invert the byte and send out parallel data
DATA_IN: begin
if(clock_in_negedge && (data_count < 4'd10)) begin
data_in_buf <= {data_in_buf[9:0], serial_data_in};
data_count <= data_count + 4'd1;
end
else if(data_count == 4'd10) begin
next_state <= IDLE;
data_count <= 4'd0;
busy <= 1'b0;
parallel_data_valid <= 1'b1;
parallel_data_in <= {data_in_buf[2],data_in_buf[3],data_in_buf[4],data_in_buf[5],
data_in_buf[6],data_in_buf[7],data_in_buf[8],data_in_buf[9]};
if(data_in_buf[1] == ^(data_in_buf[9:2])) begin
data_in_error <= 1'b1;
end
end
end
在发送数据状态前,把时钟和数据输出信号拉低60个微秒,然后放开时钟的输出控制,控制权交还给鼠标
// Before sending, need to drive the clock and data low for about 60us, clock will go back to high after 60us
INITIALIZE : begin
if(clock_count < 16'd6000) begin
clock_count <= clock_count + 16'd1;
clock_output_oe <= 1'b1;
clock_out <= 1'b0;
end
else begin
next_state <= DATA_OUT;
clock_output_oe <= 1'b0;
clock_out <= 1'b1;
end
end
鼠标开始接收指令,并驱动时钟发送剩下的10位数
// Mouse will drive the clock again, wait and detect 10 falling edge clock to send out the reset data
DATA_OUT : begin
if(clock_in_negedge) begin
if(data_count < 4'd10) begin
data_count <= data_count + 4'd1;
serial_data_out <= data_out_buf[10];
data_out_buf <= {data_out_buf[9:0], 1'b0};
end
else if(data_count == 4'd10) begin
data_out_complete <= 1'b1;
next_state <= IDLE;
busy <= 1'b0;
end
end
end
endcase
end
endmodule
这个控制模块可以保存着,之后做键盘控制应该可以用
串口控制
串口控制的逻辑设计如下:
- 如果PS2控制模块有收到信号,就把1个字节以16进制输出到PC
- 如果有收到PC传送过来的字符,立刻回传,让串口显示屏中可以看到自己写入了什么
- 接收到的字符数字0-4分别对应5个指令,0对应复位,1对应无自测试的复位,2对应重传上一个字节,3对应数据流使能,4对应数据流终止
具体代码如下:
引脚定义,加入时钟、复位、串口、USB。在此LED悬空,暂时不做打算
module usb_mouse(
input clk,
input rst,
output reg [15:0] led,
// UART port
inout USB_CLOCK,
inout USB_DATA,
// UART port
input RXD,
output reg TXD,
output reg CTS,
input RTS
);
USB引脚控制,以及资源定义
// USB ports control
wire USB_CLOCK_OE;
wire USB_DATA_OE;
wire USB_CLOCK_out;
wire USB_CLOCK_in;
wire USB_DATA_out;
wire USB_DATA_in;
assign USB_CLOCK = (USB_CLOCK_OE) ? USB_CLOCK_out : 1'bz;
assign USB_DATA = (USB_DATA_OE) ? USB_DATA_out : 1'bz;
assign USB_CLOCK_in = USB_CLOCK;
assign USB_DATA_in = USB_DATA;
wire PS2_valid;
wire [7:0] PS2_data_in;
wire PS2_busy;
wire PS2_error;
wire PS2_complete;
reg PS2_enable;
(* dont_touch = "true" *)reg [7:0] PS2_data_out;
把上面PS2接口控制模块接上
// Controller for the PS2 port
// Transfer parallel 8-bit data into serial, or receive serial to parallel
ps2_transmitter ps2_transmitter(
.clk(clk),
.rst(rst),
.clock_in(USB_CLOCK_in),
.serial_data_in(USB_DATA_in),
.parallel_data_in(PS2_data_in),
.parallel_data_valid(PS2_valid),
.busy(PS2_busy),
.data_in_error(PS2_error),
.clock_out(USB_CLOCK_out),
.serial_data_out(USB_DATA_out),
.parallel_data_out(PS2_data_out),
.parallel_data_enable(PS2_enable),
.data_out_complete(PS2_complete),
.clock_output_oe(USB_CLOCK_OE),
.data_output_oe(USB_DATA_OE)
);
串口从FPGA到PC输出部分
// Output the data to uart
reg [15:0] tx_count;
reg [19:0] tx_shift;
reg [19:0] CTS_delay;
always @(posedge clk or posedge rst) begin
if(rst) begin
tx_count <= 16'd0;
TXD <= 1'b1;
tx_shift <= 20'd0;
CTS <= 1'b1;
CTS_delay <= 20'hFFFFF;
end
当PS2控制模块准备好接收的信号时,把8位16进制数转换成2个字符,也就是十六进制的样子。使用查表转码
// When get data from PS2, transfer and buffer it into register
else if(PS2_valid) begin
case(PS2_data_in[3:0])
4'h0: begin tx_shift[9:0] <= 10'b0000011001; end
4'h1: begin tx_shift[9:0] <= 10'b0100011001; end
4'h2: begin tx_shift[9:0] <= 10'b0010011001; end
4'h3: begin tx_shift[9:0] <= 10'b0110011001; end
4'h4: begin tx_shift[9:0] <= 10'b0001011001; end
4'h5: begin tx_shift[9:0] <= 10'b0101011001; end
4'h6: begin tx_shift[9:0] <= 10'b0011011001; end
4'h7: begin tx_shift[9:0] <= 10'b0111011001; end
4'h8: begin tx_shift[9:0] <= 10'b0000111001; end
4'h9: begin tx_shift[9:0] <= 10'b0100111001; end
4'hA: begin tx_shift[9:0] <= 10'b0100000101; end
4'hB: begin tx_shift[9:0] <= 10'b0010000101; end
4'hC: begin tx_shift[9:0] <= 10'b0110000101; end
4'hD: begin tx_shift[9:0] <= 10'b0001000101; end
4'hE: begin tx_shift[9:0] <= 10'b0101000101; end
4'hF: begin tx_shift[9:0] <= 10'b0011000101; end
endcase
case(PS2_data_in[7:4])
4'h0: begin tx_shift[19:10] <= 10'b0000011001; end
4'h1: begin tx_shift[19:10] <= 10'b0100011001; end
4'h2: begin tx_shift[19:10] <= 10'b0010011001; end
4'h3: begin tx_shift[19:10] <= 10'b0110011001; end
4'h4: begin tx_shift[19:10] <= 10'b0001011001; end
4'h5: begin tx_shift[19:10] <= 10'b0101011001; end
4'h6: begin tx_shift[19:10] <= 10'b0011011001; end
4'h7: begin tx_shift[19:10] <= 10'b0111011001; end
4'h8: begin tx_shift[19:10] <= 10'b0000111001; end
4'h9: begin tx_shift[19:10] <= 10'b0100111001; end
4'hA: begin tx_shift[19:10] <= 10'b0100000101; end
4'hB: begin tx_shift[19:10] <= 10'b0010000101; end
4'hC: begin tx_shift[19:10] <= 10'b0110000101; end
4'hD: begin tx_shift[19:10] <= 10'b0001000101; end
4'hE: begin tx_shift[19:10] <= 10'b0101000101; end
4'hF: begin tx_shift[19:10] <= 10'b0011000101; end
endcase
CTS_delay <= 20'h00000;
end
当FPGA有收到有效信号时,原样返回给PC端,让PC端的控制窗口能显示相应字符。rx_start是之后的接收部分放出的接收信号
// When receiving data, output the same thing in the meantime
else if((~RXD) || rx_start) begin
TXD <= RXD;
CTS <= 1'b0;
end
如果buffer里有有效数据就顺序输出
// Shift out the received data
else begin
if(tx_count < 16'd867) begin
tx_count <= tx_count + 16'd1;
end
else begin
tx_count <= 16'd0;
end
if(tx_count == 16'd0) begin
TXD <= tx_shift[19];
tx_shift <= {tx_shift[18:0], 1'b1};
CTS <= CTS_delay[19];
CTS_delay <= {CTS_delay[18:0], 1'b1};
end
end
end
从PC输入到FPGA部分,为了简单起见,只识别数字0-4对应五个基础指令,传送给PS2控制模块
// Input from uart
(* dont_touch = "true" *)reg [7:0] RXD_delay;
// 0: 8'hFF Reset
// 1: 8'hF6 Reset without self-test
// 2: 8'hFE Resend the last byte
// 3: 8'hF4 Enable data reporting
// 4: 8'hF5 Disable data reporting
// else: 8'hEA Set stream mode
reg [15:0] rx_count;
(* dont_touch = "true" *)reg [3:0] rx_bit_count;
reg rx_start;
always @(posedge clk or posedge rst) begin
if(rst) begin
RXD_delay <= 8'h00;
rx_count <= 16'd0;
rx_bit_count <= 4'd0;
PS2_enable <= 1'b0;
rx_start <= 1'b0;
end
else if(~RTS) begin
if(rx_count < 16'd867) begin
rx_count <= rx_count + 16'd1;
end
else begin
rx_count <= 16'd0;
end
if( (rx_count == 16'd0) && (~RXD) && (~rx_start) ) begin
RXD_delay <= 8'h00;
rx_bit_count <= 4'd0;
rx_start <= 1'b1;
end
else if( (rx_count == 16'd0) && rx_start && (rx_bit_count != 4'd8)) begin
rx_bit_count <= rx_bit_count + 4'd1;
RXD_delay <= {RXD_delay[6:0], RXD};
end
else if( (rx_count == 16'd0) && rx_start) begin
rx_start <= 1'b0;
rx_bit_count <= 4'd0;
PS2_enable <= 1'b1;
case(RXD_delay[7:0])
8'b00001100: begin PS2_data_out <= 8'hFF; end
8'b10001100: begin PS2_data_out <= 8'hF6; end
8'b01001100: begin PS2_data_out <= 8'hFE; end
8'b11001100: begin PS2_data_out <= 8'hF4; end
8'b00101100: begin PS2_data_out <= 8'hF5; end
default: begin PS2_data_out <= 8'hEA; end
endcase
end
else begin
PS2_enable <= 1'b0;
end
end
end
endmodule
编译烧写
先把串口控制的第一步完成了,以便改进成状态机模式。仿真步骤就跳过了,大多数的逻辑都在上一个教程里使用过
脚本定义
新建一个名为usb_mouse的工程,选择开发板NEXYS 4 DDR的配置,添加上面的代码usb_mouse.v和ps2_transmitter.v
加入约束constraint文件usb_mouse.xdc,同样这是用标准模板取自己需要部分修改出来的(NEXYS 4 DDR Master XDC):
## This file is a general .xdc for the Nexys4 DDR Rev. C
## To use it in a project:
## - uncomment the lines corresponding to used pins
## - rename the used ports (in each line, after get_ports) according to the top level signal names in the project
## Clock signal
set_property -dict {PACKAGE_PIN E3 IOSTANDARD LVCMOS33} [get_ports clk]
create_clock -period 10.000 -name sys_clk_pin -waveform {0.000 5.000} -add [get_ports clk]
##Switches
set_property -dict {PACKAGE_PIN J15 IOSTANDARD LVCMOS33} [get_ports rst]
## LEDs
set_property -dict {PACKAGE_PIN H17 IOSTANDARD LVCMOS33} [get_ports {led[0]}]
set_property -dict {PACKAGE_PIN K15 IOSTANDARD LVCMOS33} [get_ports {led[1]}]
set_property -dict {PACKAGE_PIN J13 IOSTANDARD LVCMOS33} [get_ports {led[2]}]
set_property -dict {PACKAGE_PIN N14 IOSTANDARD LVCMOS33} [get_ports {led[3]}]
set_property -dict {PACKAGE_PIN R18 IOSTANDARD LVCMOS33} [get_ports {led[4]}]
set_property -dict {PACKAGE_PIN V17 IOSTANDARD LVCMOS33} [get_ports {led[5]}]
set_property -dict {PACKAGE_PIN U17 IOSTANDARD LVCMOS33} [get_ports {led[6]}]
set_property -dict {PACKAGE_PIN U16 IOSTANDARD LVCMOS33} [get_ports {led[7]}]
set_property -dict {PACKAGE_PIN V16 IOSTANDARD LVCMOS33} [get_ports {led[8]}]
set_property -dict {PACKAGE_PIN T15 IOSTANDARD LVCMOS33} [get_ports {led[9]}]
set_property -dict {PACKAGE_PIN U14 IOSTANDARD LVCMOS33} [get_ports {led[10]}]
set_property -dict {PACKAGE_PIN T16 IOSTANDARD LVCMOS33} [get_ports {led[11]}]
set_property -dict {PACKAGE_PIN V15 IOSTANDARD LVCMOS33} [get_ports {led[12]}]
set_property -dict {PACKAGE_PIN V14 IOSTANDARD LVCMOS33} [get_ports {led[13]}]
set_property -dict {PACKAGE_PIN V12 IOSTANDARD LVCMOS33} [get_ports {led[14]}]
set_property -dict {PACKAGE_PIN V11 IOSTANDARD LVCMOS33} [get_ports {led[15]}]
##USB HID (PS/2)
set_property -dict {PACKAGE_PIN F4 IOSTANDARD LVCMOS33} [get_ports USB_CLOCK]
set_property -dict {PACKAGE_PIN B2 IOSTANDARD LVCMOS33} [get_ports USB_DATA]
##USB-RS232 Interface
set_property -dict {PACKAGE_PIN C4 IOSTANDARD LVCMOS33} [get_ports RXD]
set_property -dict {PACKAGE_PIN D4 IOSTANDARD LVCMOS33} [get_ports TXD]
set_property -dict {PACKAGE_PIN D3 IOSTANDARD LVCMOS33} [get_ports CTS]
set_property -dict {PACKAGE_PIN E5 IOSTANDARD LVCMOS33} [get_ports RTS]
观察结果
编译生成bitstream,打开hardware manager将bitstream烧写进去后,和上一篇教程一样打开Putty串口端口,具体配置也可以参考上一篇教程。
先把鼠标连接到开发板上的USB接口,再复位FPGA,在串口端口中打入0,对应鼠标复位指令,再打入3,对应数据流使能指令,可以看到如下结果
0FAAA003FA的意思是,0是输入指令,FA是鼠标返回的acknowledge确认信号,AA00是鼠标复位后自测试返回的信号,加上鼠标自己的ID,3是第二个指令,FA是鼠标再次返回的确认信号。
这时候鼠标就已经配置完成了,移动鼠标,或者点击左右键,可以看到立刻返回了一堆数据
可以看到鼠标返回的数据是6个字符为单位,也就是3个字节。第一个返回的是18FF00,回到前面的PS2接口分析,可以看出是向左移动距离FF
逻辑升级
从上面的串口控制可以摸清楚鼠标初始化的流程,因此设计状态机如下:
得到鼠标的数据流后,计划只使用第一个字节中的X方向来控制LED阵列,跟着鼠标来动
改进代码
在串口控制观察清楚了鼠标控制的模式后,可以设计一个规范化的顶层模块,再设计一个LED阵列控制器。保留串口控制中的输出部分,取消输入。代码如下:
这部分和前面一样
module usb_mouse(
input clk,
input rst,
output reg [15:0] led,
// UART port
inout USB_CLOCK,
inout USB_DATA,
// UART port
input RXD,
output reg TXD,
output reg CTS,
input RTS
);
// USB ports control
wire USB_CLOCK_OE;
wire USB_DATA_OE;
wire USB_CLOCK_out;
wire USB_CLOCK_in;
wire USB_DATA_out;
wire USB_DATA_in;
assign USB_CLOCK = (USB_CLOCK_OE) ? USB_CLOCK_out : 1'bz;
assign USB_DATA = (USB_DATA_OE) ? USB_DATA_out : 1'bz;
assign USB_CLOCK_in = USB_CLOCK;
assign USB_DATA_in = USB_DATA;
wire PS2_valid;
wire [7:0] PS2_data_in;
wire PS2_busy;
wire PS2_error;
wire PS2_complete;
reg PS2_enable;
(* dont_touch = "true" *)reg [7:0] PS2_data_out;
// Used for chipscope
(* dont_touch = "true" *)reg USB_CLOCK_d;
(* dont_touch = "true" *)reg USB_DATA_d;
always @(posedge clk or posedge rst) begin
if(rst) begin
USB_CLOCK_d <= 1'b0;
USB_DATA_d <= 1'b0;
end
else begin
USB_CLOCK_d <= USB_CLOCK_in;
USB_DATA_d <= USB_DATA_in;
end
end
// Controller for the PS2 port
// Transfer parallel 8-bit data into serial, or receive serial to parallel
ps2_transmitter ps2_transmitter(
.clk(clk),
.rst(rst),
.clock_in(USB_CLOCK_in),
.serial_data_in(USB_DATA_in),
.parallel_data_in(PS2_data_in),
.parallel_data_valid(PS2_valid),
.busy(PS2_busy),
.data_in_error(PS2_error),
.clock_out(USB_CLOCK_out),
.serial_data_out(USB_DATA_out),
.parallel_data_out(PS2_data_out),
.parallel_data_enable(PS2_enable),
.data_out_complete(PS2_complete),
.clock_output_oe(USB_CLOCK_OE),
.data_output_oe(USB_DATA_OE)
);
LED阵列控制,使用PWM波来控制LED的亮度,由于眼睛的视觉重叠效果只能识别24Hz以下的亮度变化,高电平占一个周期的比例越高,LED亮度越高:
做法是用一个6位的持续计数器,以及一个可控的阈值,当计数器高于它时LED出低电平,否则高电平,这时阈值的大小就代表LED亮度。每一个LED都用一个独立的驱动,以及一个独立的阈值
加上一个LED选择寄存器,一开始选择最中间两个LED,向左移动时,左LED变亮,右LED变暗;向右移动时,左LED变暗,右LED变亮。当有LED阈值减到0时就预示着LED选择寄存器要移动了
语言方面,这里第一次使用了generate语法,可以快速生成多个并行的类似的结构
// Control of the movement of LED
genvar i;
// Threshold of pwm wave, the higher, the brighter
reg [5:0] led_bright[15:0];
// Counter for pwm wave
reg [5:0] led_pwm_count[15:0];
// signal from USB, gathered in state machine
reg led_move_left, led_move_right;
// LED selection, only light up the selected one
reg [15:0] led_selection_left, led_selection_right;
// When one LED reach the minimum or maximum, it will indicate to move selection register
reg [15:0] led_selection_move_left, led_selection_move_right;
always @(posedge clk or posedge rst) begin
// Original selection is the middle two LEDs
if(rst) begin
led_selection_left <= 16'h0100;
led_selection_right <= 16'h0080;
end
// Loop shift left
else if( |led_selection_move_left ) begin
led_selection_left <= {led_selection_left[14:0], led_selection_left[15]};
led_selection_right <= {led_selection_right[14:0], led_selection_right[15]};
end
// Loop shift right
else if( |led_selection_move_right ) begin
led_selection_left <= {led_selection_left[0], led_selection_left[15:1]};
led_selection_right <= {led_selection_right[0], led_selection_right[15:1]};
end
end
// Every one LED has a separate controller logic
generate
for(i=0; i<16; i=i+1) begin : LED_control
// brightness control
always @(posedge clk or posedge rst) begin
// Initial
if(rst) begin
led_bright[i] <= (i==7) ? 6'h1F : ( (i==8) ? 6'h20 : 6'h00);
end
else if(led_move_left) begin
// When move_left signal asserted, the selected right LED decrease brightness
if(led_selection_right[i]) begin
led_bright[i] <= led_bright[i] - 6'd1;
end
// And the left LED increase brightness
else if(led_selection_left[i]) begin
led_bright[i] <= led_bright[i] + 6'd1;
end
// Unselected LED keep off
else begin
led_bright[i] <= 6'd0;
end
// If the right LED is about to reach minimum, indicate selection register to shift left
if(led_selection_right[i] & led_bright[i] == 6'h01) begin
led_selection_move_left[i] <= 1'b1;
end
else begin
led_selection_move_left[i] <= 1'b0;
end
end
else if(led_move_right) begin
// When move_right signal asserted, the selected right LED increase brightness
if(led_selection_right[i]) begin
led_bright[i] <= led_bright[i] + 6'd1;
end
// And the left LED decrease brightness
else if(led_selection_left[i]) begin
led_bright[i] <= led_bright[i] - 6'd1;
end
// Unselected LED keep off
else begin
led_bright[i] <= 6'd0;
end
// If the left LED is about to reach minimum, indicate selection register to shift right
if(led_selection_left[i] & led_bright[i] == 6'h01) begin
led_selection_move_right[i] <= 1'b1;
end
else begin
led_selection_move_right[i] <= 1'b0;
end
end
else begin
led_selection_move_left[i] <= 1'b0;
led_selection_move_right[i] <= 1'b0;
end
end
// Use pwm wave to control the brightness of LEDs
always @(posedge clk or posedge rst) begin
if(rst) begin
led_pwm_count[i] <= 6'd0;
led[i] <= 1'b0;
end
else if(led_pwm_count[i] < led_bright[i])begin
led_pwm_count[i] <= led_pwm_count[i] + 6'd1;
led[i] <= 1'b1;
end
else begin
led_pwm_count[i] <= led_pwm_count[i] + 6'd1;
led[i] <= 1'b0;
end
end
end
endgenerate
状态机部分,对照前面的状态机设计图
// State machine definition
parameter [3:0] IDLE = 4'd0;
parameter [3:0] SEND_RESET = 4'd1;
parameter [3:0] WAIT_ACKNOWLEDGE1 = 4'd2;
parameter [3:0] WAIT_SELF_TEST = 4'd3;
parameter [3:0] WAIT_MOUSE_ID = 4'd4;
parameter [3:0] ENABLE_DATA_REPORT = 4'd5;
parameter [3:0] WAIT_ACKNOWLEDGE2 = 4'd6;
parameter [3:0] GET_DATA1 = 4'd7;
parameter [3:0] GET_DATA2 = 4'd8;
parameter [3:0] GET_DATA3 = 4'd9;
(* dont_touch = "true" *)reg [3:0] state;
(* dont_touch = "true" *)reg [3:0] next_state;
always @(posedge clk or posedge rst) begin
if(rst) begin
state <= IDLE;
end
else begin
state <= next_state;
end
end
always @(posedge clk) begin
case(state)
IDLE: begin
next_state <= SEND_RESET;
PS2_enable <= 1'b0;
PS2_data_out <= 8'h00;
end
// First send out a reset, in case the mouse is attached in the beginning
SEND_RESET: begin
if(~PS2_busy && PS2_complete) begin
next_state <= WAIT_ACKNOWLEDGE1;
PS2_enable <= 1'b0;
end
else begin
next_state <= SEND_RESET;
PS2_enable <= 1'b1;
PS2_data_out <= 8'hFF;
end
end
// Wait for the first acknowledge signal 0xFA
WAIT_ACKNOWLEDGE1: begin
if(PS2_valid && (PS2_data_in == 8'hFA)) begin // acknowledged
next_state <= WAIT_SELF_TEST;
end
else begin
next_state <= WAIT_ACKNOWLEDGE1;
end
end
// The mouse will send back self-test pass signal 0xAA back first
WAIT_SELF_TEST: begin
if(PS2_valid && (PS2_data_in == 8'hAA)) begin // self-test passed
next_state <= WAIT_MOUSE_ID;
end
else begin
next_state <= WAIT_SELF_TEST;
end
end
// Then followed by the ID 0x00
WAIT_MOUSE_ID: begin
if(PS2_valid && (PS2_data_in == 8'h00)) begin // mouse ID
next_state <= ENABLE_DATA_REPORT;
end
else begin
next_state <= WAIT_MOUSE_ID;
end
end
// Enable data report mode 0xF4
ENABLE_DATA_REPORT: begin
if(~PS2_busy && PS2_complete) begin
next_state <= WAIT_ACKNOWLEDGE2;
PS2_enable <= 1'b0;
end
else begin
next_state <= ENABLE_DATA_REPORT;
PS2_enable <= 1'b1;
PS2_data_out <= 8'hF4;
end
end
// Wait for the second acknowledge signal 0xFA
WAIT_ACKNOWLEDGE2: begin
if(PS2_valid && (PS2_data_in == 8'hFA)) begin // acknowledged
next_state <= GET_DATA1;
end
else begin
next_state <= WAIT_ACKNOWLEDGE2;
end
end
// Get first byte from mouse, find if it's moving left or right, and if left clicked and right clicked
// [4] is the XS bit, 1 means left, 0 means right
// [1] is right click, [0] is left click, 1 means clicked
// We don't get the distance here, for simplicity
GET_DATA1: begin
if(PS2_valid) begin
led_move_left <= ((PS2_data_in[4]) | PS2_data_in[0]) ? 1'b1 : 1'b0;
led_move_right <= ((~PS2_data_in[4]) | PS2_data_in[1]) ? 1'b1 : 1'b0;
next_state <= GET_DATA2;
end
else begin
led_move_left <= 1'b0;
led_move_right <= 1'b0;
next_state <= GET_DATA1;
end
end
// Second byte, X distance
GET_DATA2: begin
if(PS2_valid) begin
next_state <= GET_DATA3;
end
else begin
led_move_left <= 1'b0;
led_move_right <= 1'b0;
next_state <= GET_DATA2;
end
end
// Third byte, Y distance, loop back to wait for next data packet
GET_DATA3: begin
if(PS2_valid) begin
next_state <= GET_DATA1;
end
else begin
next_state <= GET_DATA3;
end
end
endcase
end
保留串口输出到PC的部分
// Output the data to uart
reg [15:0] tx_count;
reg [19:0] tx_shift;
reg [19:0] CTS_delay;
always @(posedge clk or posedge rst) begin
if(rst) begin
tx_count <= 16'd0;
TXD <= 1'b1;
tx_shift <= 20'd0;
CTS <= 1'b1;
CTS_delay <= 20'hFFFFF;
end
// When get data from PS2, transfer and buffer it into register
else if(PS2_valid) begin
case(PS2_data_in[3:0])
4'h0: begin tx_shift[9:0] <= 10'b0000011001; end
4'h1: begin tx_shift[9:0] <= 10'b0100011001; end
4'h2: begin tx_shift[9:0] <= 10'b0010011001; end
4'h3: begin tx_shift[9:0] <= 10'b0110011001; end
4'h4: begin tx_shift[9:0] <= 10'b0001011001; end
4'h5: begin tx_shift[9:0] <= 10'b0101011001; end
4'h6: begin tx_shift[9:0] <= 10'b0011011001; end
4'h7: begin tx_shift[9:0] <= 10'b0111011001; end
4'h8: begin tx_shift[9:0] <= 10'b0000111001; end
4'h9: begin tx_shift[9:0] <= 10'b0100111001; end
4'hA: begin tx_shift[9:0] <= 10'b0100000101; end
4'hB: begin tx_shift[9:0] <= 10'b0010000101; end
4'hC: begin tx_shift[9:0] <= 10'b0110000101; end
4'hD: begin tx_shift[9:0] <= 10'b0001000101; end
4'hE: begin tx_shift[9:0] <= 10'b0101000101; end
4'hF: begin tx_shift[9:0] <= 10'b0011000101; end
endcase
case(PS2_data_in[7:4])
4'h0: begin tx_shift[19:10] <= 10'b0000011001; end
4'h1: begin tx_shift[19:10] <= 10'b0100011001; end
4'h2: begin tx_shift[19:10] <= 10'b0010011001; end
4'h3: begin tx_shift[19:10] <= 10'b0110011001; end
4'h4: begin tx_shift[19:10] <= 10'b0001011001; end
4'h5: begin tx_shift[19:10] <= 10'b0101011001; end
4'h6: begin tx_shift[19:10] <= 10'b0011011001; end
4'h7: begin tx_shift[19:10] <= 10'b0111011001; end
4'h8: begin tx_shift[19:10] <= 10'b0000111001; end
4'h9: begin tx_shift[19:10] <= 10'b0100111001; end
4'hA: begin tx_shift[19:10] <= 10'b0100000101; end
4'hB: begin tx_shift[19:10] <= 10'b0010000101; end
4'hC: begin tx_shift[19:10] <= 10'b0110000101; end
4'hD: begin tx_shift[19:10] <= 10'b0001000101; end
4'hE: begin tx_shift[19:10] <= 10'b0101000101; end
4'hF: begin tx_shift[19:10] <= 10'b0011000101; end
endcase
CTS_delay <= 20'h00000;
end
// When receiving data, output the same thing in the meantime
else if((~RXD) || rx_start) begin
TXD <= RXD;
CTS <= 1'b0;
end
// Shift out the received data
else begin
if(tx_count < 16'd867) begin
tx_count <= tx_count + 16'd1;
end
else begin
tx_count <= 16'd0;
end
if(tx_count == 16'd0) begin
TXD <= tx_shift[19];
tx_shift <= {tx_shift[18:0], 1'b1};
CTS <= CTS_delay[19];
CTS_delay <= {CTS_delay[18:0], 1'b1};
end
end
end
endmodule
仿真编译烧写
我没有怎么做仿真,最多就看了一下是否有语法错误,如果需要仿真的代码,可以下载附带的源代码,或者留言
工程还是用前面新建好的,代码名字不变,重新编译并烧写即可,不需要配置ChipScope。
复位后,可以打开串口端口,看到鼠标初始化返回的FAAA00FA,以及之后的数据流,这里就不展示了
观察LED阵列,可以看到它们在跟着鼠标的左右移动而动。不过它们没有跟随鼠标移动的速度,那是因为我们只读取了鼠标移动的X方向,而没有用上移动距离,目的是简化代码
无法上传视频,就看移动鼠标的前后变化:
总结
这一篇教程比较长,有两个顶层代码,不过鼠标控制LED的结果看起来还是很舒服的。下一步填上USB键盘的坑