上一篇教程挖了个USB键盘的坑,这一篇来填它,教你做硬核键盘侠
和USB接口有关的引脚连接和PS/2接口协议在上一篇教程介绍过,这里重复一次:
PS/2 Control
时钟信号和数据信号都是双向的,没人去驱动它们的时候就被电阻拉高成高电平。
接收键盘传来的数据相对简单,host保持侦测时钟和数据,当时钟Clock的下降沿时,侦测到数据Data也拉低,代表一个数据包传送出来,之后的10个时钟下降沿,分别收到从最低位LSB到MSB的八位数据,1位的奇偶校验(1表示八位数据中1的位数为偶数,0是奇数),最后1位高电平表示数据包结束。
对键盘来说,它用的是扫描码,每个按键对应一个代码,当一个按键被按下,每100ms会重复发送一次;当这个按键被松开,一个0xF0被发出,跟着是那个被松开的按键。那些可以被shift的按键,比如大小写字母和可以代表符号的数字键,它的扫描码后面会跟着shift码,FPGA需要根据这个来决定用哪个ASCII字符。有些按键,比如Ctrl和Alt被按下时,会在扫描码前先发一个E0,当它们被松开时,会发E0 F0,并跟随着相应按键的扫描码。
这部分比较复杂,需要写相应代码来确定使用哪个ASCII码,相应按键的扫描码如下:
有一个网站把这部分写的比较清楚:PS2 controller
其中就包含了一张键盘字符表,每个按键KEY都有对应按下(MAKE)的代码,和松开(BREAK)的代码:
键盘的一些其他特殊指令如下,我们可以用Echo回声测试指令,看键盘是否会返回相应数据
这个逻辑结构计划基本照搬上一篇教程中用串口控制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串口控制窗口,配置方式如下:
为开发板上连上USB键盘,如果你只有一个键盘,可以在PC端开启一个虚拟键盘On-Screen Keyboard。先后输入几个指令,看到串口控制窗口如下:
硬核键盘侠教程结束,下一篇讲VGA视频接口控制,不过VGA是给以前的电子管电视机使用的,希望能在下一篇开始前找到一个VGA转HDMI的转接口,不然我手上还真没有能用的VGA显示器。。。