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

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

开发板USB芯片

NEXYS 4 开发板上有一个辅助微控制器PIC24FJ128,用来提供USB嵌入式host,上电后这个微控制器处于配置模式,要么把bitstrem下载到FPGA,要么被配置成使用其他资源。Bitstream被下载到FPGA后,这个微控制器被切换成USB host功能,不过只支持鼠标和键盘(不知道支不支持鼠标和键盘一起连接的那种)。这个微控制器还兼职控制micro SD卡,这部分之后再讲

看一下USB与FPGA相关的连接:
connection
可以看到只有两根线,没有配置相关的引脚,不在我们的控制下(毕竟烧写都是用这块芯片控制的,FPGA才是它的slave),因此我们不深入了解这块PIC怎么用的,只考虑怎么使用USB信号

信号时序图

鼠标和键盘都是用11位信号,包括1位的开始码,小头优先的8位数据,1位奇偶校验位,和1位终结码,只是8位数据不同。这种控制协议实际上是PS/2接口的信号直接转到了USB接口上。时序图如下:
PS2
PS/2接口长这个样,在老电脑上出现,现在基本上看不到了
PS/2
这个时序图画的不是很清楚,看另外一份文档能更清楚的看到细节:PS/2 Mouse Control

时钟信号和数据信号都是双向的,没人去驱动它们的时候就被电阻拉高成高电平。

接收鼠标传来的数据相对简单,host保持侦测时钟和数据,当时钟Clock的下降沿时,侦测到数据Data也拉低,代表一个数据包传送出来,之后的10个时钟下降沿,分别收到从最低位LSB到MSB的八位数据,1位的奇偶校验(1表示八位数据中1的位数为偶数,0是奇数),最后1位高电平表示数据包结束。

PS2 detail
从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个字节,其形式如下,注意传送数据是低位优先的
mouse data

  • 第一个字节[7:6] YY和XY,分别表示纵向和横向移动速度是否溢出,1表示溢出,这时移动的量以最大值计算
  • [5:4] YS和XS,分别表示纵向和横向移动方向,1表示向左或者向下,0表示向右或者向上
  • [1:0] R和L,分别表示右键和左键,1表示按下
  • 第二个字节[7:0] 表示横向移动的量
  • 第三个字节[7:0] 表示纵向移动的量

从Host传到鼠标的指令有如下这些,这里我们只用到带自测试的复位和数据流使能:
mouse command

逻辑设计

从上面时序图中的时钟参数可以看出,鼠标的时钟频率大约是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,对应数据流使能指令,可以看到如下结果
serial0
0FAAA003FA的意思是,0是输入指令,FA是鼠标返回的acknowledge确认信号,AA00是鼠标复位后自测试返回的信号,加上鼠标自己的ID,3是第二个指令,FA是鼠标再次返回的确认信号。

这时候鼠标就已经配置完成了,移动鼠标,或者点击左右键,可以看到立刻返回了一堆数据
serial 1
可以看到鼠标返回的数据是6个字符为单位,也就是3个字节。第一个返回的是18FF00,回到前面的PS2接口分析,可以看出是向左移动距离FF

逻辑升级

从上面的串口控制可以摸清楚鼠标初始化的流程,因此设计状态机如下:
FSM
得到鼠标的数据流后,计划只使用第一个字节中的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亮度越高:
PWM wave
做法是用一个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方向,而没有用上移动距离,目的是简化代码

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

总结

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

  • 17
    点赞
  • 50
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值