FPGA基础入门【13】开发板USB键盘控制,教你做硬核键盘侠

上一篇教程挖了个USB键盘的坑,这一篇来填它,教你做硬核键盘侠

FPGA基础入门【13】开发板USB键盘控制

  • 键盘控制信号
    • 键盘
  • 逻辑设计
  • 编译烧写测试
  • 总结

键盘控制信号

和USB接口有关的引脚连接和PS/2接口协议在上一篇教程介绍过,这里重复一次:
PS/2 Control

FPGA基础入门【13】开发板USB键盘控制,教你做硬核键盘侠_第1张图片
具体可以参考NEXYS 4开发板文档

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

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

FPGA基础入门【13】开发板USB键盘控制,教你做硬核键盘侠_第2张图片
从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信号控制权交还

键盘

对键盘来说,它用的是扫描码,每个按键对应一个代码,当一个按键被按下,每100ms会重复发送一次;当这个按键被松开,一个0xF0被发出,跟着是那个被松开的按键。那些可以被shift的按键,比如大小写字母和可以代表符号的数字键,它的扫描码后面会跟着shift码,FPGA需要根据这个来决定用哪个ASCII字符。有些按键,比如Ctrl和Alt被按下时,会在扫描码前先发一个E0,当它们被松开时,会发E0 F0,并跟随着相应按键的扫描码。

这部分比较复杂,需要写相应代码来确定使用哪个ASCII码,相应按键的扫描码如下:
FPGA基础入门【13】开发板USB键盘控制,教你做硬核键盘侠_第3张图片
有一个网站把这部分写的比较清楚:PS2 controller
其中就包含了一张键盘字符表,每个按键KEY都有对应按下(MAKE)的代码,和松开(BREAK)的代码:
FPGA基础入门【13】开发板USB键盘控制,教你做硬核键盘侠_第4张图片

键盘的一些其他特殊指令如下,我们可以用Echo回声测试指令,看键盘是否会返回相应数据
FPGA基础入门【13】开发板USB键盘控制,教你做硬核键盘侠_第5张图片

逻辑设计

这个逻辑结构计划基本照搬上一篇教程中用串口控制USB鼠标的逻辑,因为两者用的都是PS/2控制协议,只需要调用之前写的ps2_transmitter.v,并对顶层做少量的修改即可。

修改后的顶层代码usb_keyboard.v如下:
引脚基本不变,只是去掉了不必要的LED

module usb_keyboard(
    input             clk,
    input             rst,
    
    // 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;

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)
);

串口输出逻辑不变,一样是把PS/2收到的8位数转化成2个十六进制符号传回,并且把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

把用来控制鼠标的指令修改一下,0是复位指令0xFF,1是LED等配置0xED,用来控制键盘上大写锁定、滚动锁定、小键盘锁定LED,跟随着2的最低3位分别指定这三个LED都亮起,3是回声测试0xEE,4是重传指令0xFE

// Input from uart
(* dont_touch = "true" *)reg [7:0]  RXD_delay;
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 // Reset
            8'b10001100: begin PS2_data_out <= 8'hED; end // Set status LED
            8'b01001100: begin PS2_data_out <= 8'h07; end // LED byte
            8'b11001100: begin PS2_data_out <= 8'hEE; end // Echo
            8'b00101100: begin PS2_data_out <= 8'hFE; end // Resend
            default: begin PS2_data_out <= 8'hEE; end
            endcase
        end
        else begin
            PS2_enable <= 1'b0;
        end
    end
end

endmodule

编译烧写测试

由于代码是基于上一篇USB鼠标的教程,已经实际测试过,因此这里跳过仿真步骤,如果需要的话可以仿照前几篇教程中的仿真进行。

在Vivado中新建名为usb_keyboard的工程,选择NEXYS 4 DDR 作为配置,并加入修改好的顶层代码usb_keyboard.v和上一篇教程中写好的ps2_transmitter.v。

加入由官方约束文件NEXYS4.xdc修改成的引脚约束文件usb_keyboard.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]


##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,为开发板连上USB烧写线,并烧写进bitstream。复位后打开Putty串口控制窗口,配置方式如下:
FPGA基础入门【13】开发板USB键盘控制,教你做硬核键盘侠_第6张图片
FPGA基础入门【13】开发板USB键盘控制,教你做硬核键盘侠_第7张图片
为开发板上连上USB键盘,如果你只有一个键盘,可以在PC端开启一个虚拟键盘On-Screen Keyboard。先后输入几个指令,看到串口控制窗口如下:
FPGA基础入门【13】开发板USB键盘控制,教你做硬核键盘侠_第8张图片

  • AA是接上键盘后传回的自测试完成信号0xAA
  • 0FAAA,发出0复位指令0xFF,键盘传回0xFA确认Acknowledge,重新自测试并传回自测试完成信号0xAA
  • 1FA2,发出1配置LED指令0xED,键盘返回0xFA确认,然后在发出2具体LED信息0x07,最低3位都是1,将全部三个LED都亮起,如下面的图片所示,键盘上两盏LED都亮起,还有一个大写锁定在另一边没拍下来
  • 3EE,发出3回声测试0xEE,键盘返回0xEE
  • 58F058EBF02B3CF03C21F02142F042,“随便”在键盘上打了几个字母,比如0x58是大写锁定,0xF058是松开大写锁定,其他几个字母都可以通过查上方的字符表得到
  • 442,发出4重传指令0xFE,把键盘中最后发出的数据0x42重传了一遍

FPGA基础入门【13】开发板USB键盘控制,教你做硬核键盘侠_第9张图片

总结

硬核键盘侠教程结束,下一篇讲VGA视频接口控制,不过VGA是给以前的电子管电视机使用的,希望能在下一篇开始前找到一个VGA转HDMI的转接口,不然我手上还真没有能用的VGA显示器。。。

你可能感兴趣的:(FPGA)