上一篇教程介绍了NEXYS4 开发板中UART串口通信的使用方式,这一篇介绍USB接口接收鼠标和键盘信号

FPGA基础入门【12】开发板USB鼠标控制

  • 开发板USB芯片
    • 信号时序图
    • 鼠标初始化
  • 逻辑设计
    • PS2接口控制模块
    • 串口控制
  • 编译烧写
    • 脚本定义
    • 观察结果
  • 逻辑升级
    • 改进代码
  • 仿真编译烧写
  • 总结

开发板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发送指令到鼠标的步骤如下

  1. 一般时钟Clock信号都是由鼠标驱动的,只有Host发送指令出去的时候要拉低一次,就是标为1的位置,Host驱动时钟成低电平,并保持大约60us
  2. 在时钟拉低时,也就是标为2的位置,把数据Data信号也拉低
  3. 60us结束后把时钟Clock信号的控制权交回,开启侦测时钟,这时鼠标侦测到标为3的时钟上升沿,数据Data位0,准备接受指令
  4. 鼠标开始驱动时钟Clock,每个上升沿读取一位数据,Host可以在侦测到时钟下降沿时把数据位准备好,数据包顺序和读取时一样,1位起始低电位,8位LSB到MSB数据,1位奇偶校验,1位结尾高电位
  5. 把数据Data信号控制权交还

鼠标初始化

刚刚连上鼠标时,鼠标不会立刻开始传送数据到Host,具体流程如下:

  1. USB接口连上,鼠标开始自测试,自测试完成发送0xAA给Host
  2. 紧接着0xAA,鼠标自动接上固定的设备ID号0x00
  3. Host发送鼠标数据流使能指令0xF4
  4. 等待鼠标传回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 datainput            clock_in,           // connected to usb clock input signalinput            serial_data_in,     // connected to usb data input signaloutput reg [7:0] parallel_data_in,   // 8-bit input data buffer, from the USB interfaceoutput reg       parallel_data_valid,// indicate the input data is ready or notoutput reg       data_in_error,      // reading error when the odd parity is not matched// ports for output dateoutput reg       clock_out,           // connected to usb clock output signaloutput reg       serial_data_out,     // connected to usb data output signalinput      [7:0] parallel_data_out,   // 8-bit output data buffer, to the USB interfaceinput            parallel_data_enable,// control signal to start a writing processoutput reg       data_out_complete,output reg       busy,                // indicate the transmitter is busyoutput reg       clock_output_oe,     // clock output enableoutput 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) beginclock_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) beginif(rst) beginstate <= IDLE;endelse beginstate <= next_state;end
endalways @(posedge clk) begincase(state)IDLE: beginnext_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 stateWAIT_IO: beginif(clock_in_negedge) begin  // input data detected, and the start bit is ignorednext_state <= DATA_IN;busy <= 1'b1;data_count <= 4'd0;endelse if(parallel_data_enable) begin // output data enable detected, and send out the start bit right herenext_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 outputdata_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;endend

读取数据状态,每个时钟下降沿就向移位寄存器中塞一位,塞到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 dataDATA_IN: beginif(clock_in_negedge && (data_count < 4'd10)) begindata_in_buf <= {data_in_buf[9:0], serial_data_in};data_count <= data_count + 4'd1;endelse if(data_count == 4'd10) beginnext_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])) begindata_in_error <= 1'b1;endendend

在发送数据状态前,把时钟和数据输出信号拉低60个微秒,然后放开时钟的输出控制,控制权交还给鼠标

    // Before sending, need to drive the clock and data low for about 60us, clock will go back to high after 60usINITIALIZE : beginif(clock_count < 16'd6000) beginclock_count <= clock_count + 16'd1;clock_output_oe <= 1'b1;clock_out <= 1'b0;endelse beginnext_state <= DATA_OUT;clock_output_oe <= 1'b0;clock_out <= 1'b1;endend

鼠标开始接收指令,并驱动时钟发送剩下的10位数

    // Mouse will drive the clock again, wait and detect 10 falling edge clock to send out the reset dataDATA_OUT : beginif(clock_in_negedge) beginif(data_count < 4'd10) begindata_count <= data_count + 4'd1;serial_data_out <= data_out_buf[10];data_out_buf <= {data_out_buf[9:0], 1'b0};endelse if(data_count == 4'd10) begindata_out_complete <= 1'b1;next_state <= IDLE;busy <= 1'b0;endendendendcase
endendmodule

这个控制模块可以保存着,之后做键盘控制应该可以用

串口控制

串口控制的逻辑设计如下:

  • 如果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 portinout             USB_CLOCK,inout             USB_DATA,// UART portinput             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) beginif(rst) begintx_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 registerelse if(PS2_valid) begincase(PS2_data_in[3:0])4'h0: begin tx_shift[9:0] <= 10'b0000011001; end4'h1: begin tx_shift[9:0] <= 10'b0100011001; end4'h2: begin tx_shift[9:0] <= 10'b0010011001; end4'h3: begin tx_shift[9:0] <= 10'b0110011001; end4'h4: begin tx_shift[9:0] <= 10'b0001011001; end4'h5: begin tx_shift[9:0] <= 10'b0101011001; end4'h6: begin tx_shift[9:0] <= 10'b0011011001; end4'h7: begin tx_shift[9:0] <= 10'b0111011001; end4'h8: begin tx_shift[9:0] <= 10'b0000111001; end4'h9: begin tx_shift[9:0] <= 10'b0100111001; end4'hA: begin tx_shift[9:0] <= 10'b0100000101; end4'hB: begin tx_shift[9:0] <= 10'b0010000101; end4'hC: begin tx_shift[9:0] <= 10'b0110000101; end4'hD: begin tx_shift[9:0] <= 10'b0001000101; end4'hE: begin tx_shift[9:0] <= 10'b0101000101; end4'hF: begin tx_shift[9:0] <= 10'b0011000101; endendcasecase(PS2_data_in[7:4])4'h0: begin tx_shift[19:10] <= 10'b0000011001; end4'h1: begin tx_shift[19:10] <= 10'b0100011001; end4'h2: begin tx_shift[19:10] <= 10'b0010011001; end4'h3: begin tx_shift[19:10] <= 10'b0110011001; end4'h4: begin tx_shift[19:10] <= 10'b0001011001; end4'h5: begin tx_shift[19:10] <= 10'b0101011001; end4'h6: begin tx_shift[19:10] <= 10'b0011011001; end4'h7: begin tx_shift[19:10] <= 10'b0111011001; end4'h8: begin tx_shift[19:10] <= 10'b0000111001; end4'h9: begin tx_shift[19:10] <= 10'b0100111001; end4'hA: begin tx_shift[19:10] <= 10'b0100000101; end4'hB: begin tx_shift[19:10] <= 10'b0010000101; end4'hC: begin tx_shift[19:10] <= 10'b0110000101; end4'hD: begin tx_shift[19:10] <= 10'b0001000101; end4'hE: begin tx_shift[19:10] <= 10'b0101000101; end4'hF: begin tx_shift[19:10] <= 10'b0011000101; endendcaseCTS_delay <= 20'h00000;end

当FPGA有收到有效信号时,原样返回给PC端,让PC端的控制窗口能显示相应字符。rx_start是之后的接收部分放出的接收信号

    // When receiving data, output the same thing in the meantimeelse if((~RXD) || rx_start) beginTXD <= RXD;CTS <= 1'b0;end

如果buffer里有有效数据就顺序输出

    // Shift out the received dataelse beginif(tx_count < 16'd867) begintx_count <= tx_count + 16'd1;endelse begintx_count <= 16'd0;endif(tx_count == 16'd0) beginTXD <= tx_shift[19];tx_shift <= {tx_shift[18:0], 1'b1};CTS <= CTS_delay[19];CTS_delay <= {CTS_delay[18:0], 1'b1};endend
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) beginif(rst) beginRXD_delay <= 8'h00;rx_count <= 16'd0;rx_bit_count <= 4'd0;PS2_enable <= 1'b0;rx_start <= 1'b0;endelse if(~RTS) beginif(rx_count < 16'd867) beginrx_count <= rx_count + 16'd1;endelse beginrx_count <= 16'd0;endif( (rx_count == 16'd0) && (~RXD) && (~rx_start) ) beginRXD_delay <= 8'h00;rx_bit_count <= 4'd0;rx_start <= 1'b1;endelse if( (rx_count == 16'd0) && rx_start && (rx_bit_count != 4'd8)) beginrx_bit_count <= rx_bit_count + 4'd1;RXD_delay <= {RXD_delay[6:0], RXD};endelse if( (rx_count == 16'd0) && rx_start) beginrx_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; end8'b10001100: begin PS2_data_out <= 8'hF6; end8'b01001100: begin PS2_data_out <= 8'hFE; end8'b11001100: begin PS2_data_out <= 8'hF4; end8'b00101100: begin PS2_data_out <= 8'hF5; enddefault: begin PS2_data_out <= 8'hEA; endendcaseendelse beginPS2_enable <= 1'b0;endend
endendmodule

编译烧写

先把串口控制的第一步完成了,以便改进成状态机模式。仿真步骤就跳过了,大多数的逻辑都在上一个教程里使用过

脚本定义

新建一个名为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]##Switchesset_property -dict {PACKAGE_PIN J15 IOSTANDARD LVCMOS33} [get_ports rst]## LEDsset_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 Interfaceset_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 portinout             USB_CLOCK,inout             USB_DATA,// UART portinput             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) beginif(rst) beginUSB_CLOCK_d <= 1'b0;USB_DATA_d  <= 1'b0;endelse beginUSB_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 LEDsif(rst) beginled_selection_left <= 16'h0100;led_selection_right <= 16'h0080;end// Loop shift leftelse if( |led_selection_move_left ) beginled_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 rightelse if( |led_selection_move_right ) beginled_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
generatefor(i=0; i<16; i=i+1) begin : LED_control// brightness controlalways @(posedge clk or posedge rst) begin// Initialif(rst) beginled_bright[i] <= (i==7) ? 6'h1F : ( (i==8) ? 6'h20 : 6'h00);endelse if(led_move_left) begin// When move_left signal asserted, the selected right LED decrease brightnessif(led_selection_right[i]) beginled_bright[i] <= led_bright[i] - 6'd1;end// And the left LED increase brightnesselse if(led_selection_left[i]) beginled_bright[i] <= led_bright[i] + 6'd1;end// Unselected LED keep offelse beginled_bright[i] <= 6'd0;end// If the right LED is about to reach minimum, indicate selection register to shift leftif(led_selection_right[i] & led_bright[i] == 6'h01) beginled_selection_move_left[i] <= 1'b1;endelse beginled_selection_move_left[i] <= 1'b0;endendelse if(led_move_right) begin// When move_right signal asserted, the selected right LED increase brightnessif(led_selection_right[i]) beginled_bright[i] <= led_bright[i] + 6'd1;end// And the left LED decrease brightnesselse if(led_selection_left[i]) beginled_bright[i] <= led_bright[i] - 6'd1;end// Unselected LED keep offelse beginled_bright[i] <= 6'd0;end// If the left LED is about to reach minimum, indicate selection register to shift rightif(led_selection_left[i] & led_bright[i] == 6'h01) beginled_selection_move_right[i] <= 1'b1;endelse beginled_selection_move_right[i] <= 1'b0;endendelse beginled_selection_move_left[i] <= 1'b0;led_selection_move_right[i] <= 1'b0;endend// Use pwm wave to control the brightness of LEDsalways @(posedge clk or posedge rst) beginif(rst) beginled_pwm_count[i] <= 6'd0;led[i] <= 1'b0;endelse if(led_pwm_count[i] < led_bright[i])beginled_pwm_count[i] <= led_pwm_count[i] + 6'd1;led[i] <= 1'b1;endelse beginled_pwm_count[i] <= led_pwm_count[i] + 6'd1;led[i] <= 1'b0;endendend
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) beginif(rst) beginstate <= IDLE;endelse beginstate <= next_state;end
endalways @(posedge clk) begincase(state)IDLE: beginnext_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 beginningSEND_RESET: beginif(~PS2_busy && PS2_complete) beginnext_state <= WAIT_ACKNOWLEDGE1;PS2_enable <= 1'b0;endelse beginnext_state <= SEND_RESET;PS2_enable <= 1'b1;PS2_data_out <= 8'hFF;endend// Wait for the first acknowledge signal 0xFAWAIT_ACKNOWLEDGE1: beginif(PS2_valid && (PS2_data_in == 8'hFA)) begin   // acknowledgednext_state <= WAIT_SELF_TEST;endelse beginnext_state <= WAIT_ACKNOWLEDGE1;endend// The mouse will send back self-test pass signal 0xAA back firstWAIT_SELF_TEST: beginif(PS2_valid && (PS2_data_in == 8'hAA)) begin   // self-test passednext_state <= WAIT_MOUSE_ID;endelse beginnext_state <= WAIT_SELF_TEST;endend// Then followed by the ID 0x00WAIT_MOUSE_ID: beginif(PS2_valid && (PS2_data_in == 8'h00)) begin   // mouse IDnext_state <= ENABLE_DATA_REPORT;endelse beginnext_state <= WAIT_MOUSE_ID;endend// Enable data report mode 0xF4ENABLE_DATA_REPORT: beginif(~PS2_busy && PS2_complete) beginnext_state <= WAIT_ACKNOWLEDGE2;PS2_enable <= 1'b0;endelse beginnext_state <= ENABLE_DATA_REPORT;PS2_enable <= 1'b1;PS2_data_out <= 8'hF4;endend// Wait for the second acknowledge signal 0xFAWAIT_ACKNOWLEDGE2: beginif(PS2_valid && (PS2_data_in == 8'hFA)) begin   // acknowledgednext_state <= GET_DATA1;endelse beginnext_state <= WAIT_ACKNOWLEDGE2;endend// 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 simplicityGET_DATA1: beginif(PS2_valid) beginled_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;endelse beginled_move_left <= 1'b0;led_move_right <= 1'b0;next_state <= GET_DATA1;endend// Second byte, X distanceGET_DATA2: beginif(PS2_valid) beginnext_state <= GET_DATA3;endelse beginled_move_left <= 1'b0;led_move_right <= 1'b0;next_state <= GET_DATA2;endend// Third byte, Y distance, loop back to wait for next data packetGET_DATA3: beginif(PS2_valid) beginnext_state <= GET_DATA1;endelse beginnext_state <= GET_DATA3;endendendcase
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) beginif(rst) begintx_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 registerelse if(PS2_valid) begincase(PS2_data_in[3:0])4'h0: begin tx_shift[9:0] <= 10'b0000011001; end4'h1: begin tx_shift[9:0] <= 10'b0100011001; end4'h2: begin tx_shift[9:0] <= 10'b0010011001; end4'h3: begin tx_shift[9:0] <= 10'b0110011001; end4'h4: begin tx_shift[9:0] <= 10'b0001011001; end4'h5: begin tx_shift[9:0] <= 10'b0101011001; end4'h6: begin tx_shift[9:0] <= 10'b0011011001; end4'h7: begin tx_shift[9:0] <= 10'b0111011001; end4'h8: begin tx_shift[9:0] <= 10'b0000111001; end4'h9: begin tx_shift[9:0] <= 10'b0100111001; end4'hA: begin tx_shift[9:0] <= 10'b0100000101; end4'hB: begin tx_shift[9:0] <= 10'b0010000101; end4'hC: begin tx_shift[9:0] <= 10'b0110000101; end4'hD: begin tx_shift[9:0] <= 10'b0001000101; end4'hE: begin tx_shift[9:0] <= 10'b0101000101; end4'hF: begin tx_shift[9:0] <= 10'b0011000101; endendcasecase(PS2_data_in[7:4])4'h0: begin tx_shift[19:10] <= 10'b0000011001; end4'h1: begin tx_shift[19:10] <= 10'b0100011001; end4'h2: begin tx_shift[19:10] <= 10'b0010011001; end4'h3: begin tx_shift[19:10] <= 10'b0110011001; end4'h4: begin tx_shift[19:10] <= 10'b0001011001; end4'h5: begin tx_shift[19:10] <= 10'b0101011001; end4'h6: begin tx_shift[19:10] <= 10'b0011011001; end4'h7: begin tx_shift[19:10] <= 10'b0111011001; end4'h8: begin tx_shift[19:10] <= 10'b0000111001; end4'h9: begin tx_shift[19:10] <= 10'b0100111001; end4'hA: begin tx_shift[19:10] <= 10'b0100000101; end4'hB: begin tx_shift[19:10] <= 10'b0010000101; end4'hC: begin tx_shift[19:10] <= 10'b0110000101; end4'hD: begin tx_shift[19:10] <= 10'b0001000101; end4'hE: begin tx_shift[19:10] <= 10'b0101000101; end4'hF: begin tx_shift[19:10] <= 10'b0011000101; endendcaseCTS_delay <= 20'h00000;end// When receiving data, output the same thing in the meantimeelse if((~RXD) || rx_start) beginTXD <= RXD;CTS <= 1'b0;end// Shift out the received dataelse beginif(tx_count < 16'd867) begintx_count <= tx_count + 16'd1;endelse begintx_count <= 16'd0;endif(tx_count == 16'd0) beginTXD <= tx_shift[19];tx_shift <= {tx_shift[18:0], 1'b1};CTS <= CTS_delay[19];CTS_delay <= {CTS_delay[18:0], 1'b1};endend
endendmodule

仿真编译烧写

我没有怎么做仿真,最多就看了一下是否有语法错误,如果需要仿真的代码,可以下载附带的源代码,或者留言

工程还是用前面新建好的,代码名字不变,重新编译并烧写即可,不需要配置ChipScope。

复位后,可以打开串口端口,看到鼠标初始化返回的FAAA00FA,以及之后的数据流,这里就不展示了

观察LED阵列,可以看到它们在跟着鼠标的左右移动而动。不过它们没有跟随鼠标移动的速度,那是因为我们只读取了鼠标移动的X方向,而没有用上移动距离,目的是简化代码

无法上传视频,就看移动鼠标的前后变化:

总结

这一篇教程比较长,有两个顶层代码,不过鼠标控制LED的结果看起来还是很舒服的。下一步填上USB键盘的坑

FPGA基础入门【12】开发板USB鼠标控制相关推荐

  1. FPGA基础入门【8】开发板外部存储器SPI flash访问

    前两篇教程利用数码管project介绍了chipscope和各种烧写开发板的方式,这篇开始继续探索开发板,这次关注外置存储器的控制,外置指的是芯片外部,不是开发板外部.板子上的外置存储器有DDR2和S ...

  2. Linux基础入门--驱动开发--USB

    Linux基础入门--驱动开发--USB 1.基本概念 2.组成结构 2.1 设备描述符 2.2 配置描述符 2.3 接口描述符 2.4 端点描述符 2.5 字符串描述符 3.管道 4.端点分类 4. ...

  3. 一期完结《一篇文章让你从HTML零基础入门前端开发》12.28

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VRcXH9FM-1672214813897)(./assets/%E7%8E%84%E5%AD%90Shaer-%E4% ...

  4. FPGA基础入门【1】Vivado官方免费版安装

    本人自本科大二开始接触FPGA相关知识,现已将近五年,从这篇开始将从比较基础的角度讲述如何一步步了解FPGA.我相信动手一步步做下去是从零开始学习知识的最快方法,因此不会从最基础开始讲,而是在碰到相应 ...

  5. FPGA基础入门【3】Blink逻辑及仿真

    从这一篇开始正式介绍FPGA中的硬件逻辑,第一个目标就是从零开始在NEXYS 4开发板上实现闪烁LED. 软件编程中hello world是初学语言中实现的第一个功能,而硬件编程中blink是同等的地 ...

  6. FPGA基础入门【6】ChipScope的使用

    当FPGA设计中复杂度慢慢变高的时候,仿真的手段也要增加,目前我们仿真的手段都是在ModelSim中配置相应的testbench,给模块发送需要的信号.这种软件仿真的方式有几个缺点: 一个是软件仿真速 ...

  7. Tang Nano FPGA(35元开发板).初探

    ​Lichee Tang Nano 基于高云小蜜蜂系列GW1N-1 FPGA的简约型开发板.该芯片搭载了1K LUT4的逻辑资源,1 PLL和4 Block RAM,开发板引出了所有I/O接口,适用于 ...

  8. 【视频回放与课件】零基础入门AI开发

    今天上午,受广州图书馆邀请,在第一讲<零代码上手人工智能>的基础上,以<零基础入门AI开发>为主题,分四步解锁人工智能学习的概念与开发工具,让您在一小时内轻松掌握人工智能开发要 ...

  9. FPGA基础入门篇(四) 边沿检测电路

    FPGA基础入门篇(四)--边沿检测电路 一.边沿检测 边沿检测,就是检测输入信号,或者FPGA内部逻辑信号的跳变,即上升沿或者下降沿的检测.在检测到所需要的边沿后产生一个高电平的脉冲.这在FPGA电 ...

最新文章

  1. 高性能网络编程1----accept建立连接
  2. fdisk 磁盘分区命令
  3. 树的遍历 | 翻转二叉树
  4. php对mysql进行增删改查,php对mysql进行增删改查
  5. [论文阅读] Cost-Effective REgion-based Active Learning for Semantic Segmentation
  6. LC 231 power of 2
  7. 恋恋风辰 对于redis底层框架的理解(一)
  8. idea zip怎么安装_Mybatis源码分析(一): 下载Mybatis源码安装并导入IDEA
  9. 揭秘网易视频云在数据传输方面的优化实战
  10. Adobe Photoshop 2021 22.4.2 绿色精简版
  11. [通信技术]Iub接口协议——专用传输信道(DCH)的用户平面协议
  12. Android检测手机是否安装app
  13. 视频播放器(AVPlayer)
  14. 【征文大赛】TiDB 社区第二届征文大赛,一次性带走社区全部新周边,还有bose 降噪耳机、倍轻松按摩仪等你拿!
  15. daimayuan每日一题#849 国家铁路
  16. PCIEBXMCx4板卡
  17. 【C语言】【unix c】信号量集(system v ipc)
  18. 全国计算机等级考试Java上机真题
  19. Linux 内存性能检测工具
  20. 实现类似涂鸦跳跃的游戏(对象池的使用和背景固定)

热门文章

  1. linux 可变 大小 磁盘6,Linux下调整磁盘大小后的基于LVM的磁盘扩容
  2. workbench设置单元坐标系_节点坐标系:很多Workbench结构用户不知道的重要概念
  3. 【keepass】利用keepassxc-browser浏览器扩展和keepassnatmsg插件实现密码自动填充(KeePassHttp-Connector/KeePassHttp)
  4. PS2019仿制图章工具、图案图章工具
  5. oracle汉字变成方框,电脑操作系统所有的汉字都变成方框了,是什么原因,怎么办?...
  6. K8S Yaml 详细说明及简单样例
  7. JavaGUI——Java图形用户界面
  8. 06.破解Windows7密码
  9. POJ-2325解题报告
  10. 三坐标检测基础知识之坐标系2021