mirror of
https://github.com/MiSTer-devel/VT52_MiSTer.git
synced 2026-04-19 03:05:47 +00:00
443 lines
19 KiB
Verilog
443 lines
19 KiB
Verilog
|
|
/*
|
|
|
|
The command_handler module is a complex Verilog module designed to handle
|
|
terminal-like commands and manage a text display. Here's a breakdown of
|
|
its key features and functionality:
|
|
|
|
1. Module Parameters:
|
|
- ROWS, COLS: Define the display dimensions (24 rows, 80 columns by default).
|
|
- LAST_ROW, ONE_PAST_LAST_ROW: Calculated based on ROWS and COLS.
|
|
- ROW_BITS, COL_BITS, ADDR_BITS: Bit widths for addressing rows, columns, and memory.
|
|
|
|
2. Inputs and Outputs:
|
|
- Inputs: clk, reset, data (8-bit), valid
|
|
- Outputs: ready, new_first_char, new_first_char_wen, new_char, new_char_address, new_char_wen, new_cursor_x, new_cursor_y, new_cursor_wen
|
|
|
|
3. State Machine:
|
|
The module uses a one-hot encoded state machine with the following states:
|
|
- state_char: Normal character processing
|
|
- state_esc: Escape sequence processing
|
|
- state_row, state_col, state_addr, state_cursor: Handling cursor movement commands
|
|
- state_erase: Erasing characters on the display
|
|
|
|
4. Character Processing:
|
|
- Handles printable characters (ASCII 32-126, excluding 127)
|
|
- Manages cursor movement within the display bounds
|
|
- Implements backspace, tab, linefeed, and carriage return functionality
|
|
|
|
5. Escape Sequence Handling:
|
|
Processes VT52-like escape sequences for:
|
|
- Cursor movement (up, down, left, right, home)
|
|
- Advanced cursor positioning (Esc-Y sequence)
|
|
- Screen and line erasure
|
|
|
|
6. Display Management:
|
|
- Implements a circular buffer for the display, allowing scrolling
|
|
- Manages the first character position (new_first_char) for scrolling
|
|
- Handles line wrapping and scrolling when reaching display boundaries
|
|
|
|
7. Memory Interaction:
|
|
- Generates write enables and addresses for updating characters in memory
|
|
- Manages cursor position updates
|
|
|
|
8. Special Features:
|
|
- Implements a tab-stop system (every 8 columns)
|
|
- Handles scrolling by updating the first character position and erasing the new line
|
|
|
|
9. Error Handling:
|
|
- Gracefully handles out-of-bounds cursor movements
|
|
- Ignores unrecognized escape sequences
|
|
|
|
This module appears to be designed for a terminal emulator or similar text-based
|
|
display system, possibly for a retro-computing project or a specialized interface.
|
|
It handles a subset of VT52-like terminal commands, managing both text input and
|
|
cursor control in a memory-mapped display buffer.
|
|
|
|
The design uses a state machine to process incoming characters and commands,
|
|
updating the display and cursor position accordingly. It's optimized for a
|
|
fixed-size display but includes logic to handle wraparound and scrolling,
|
|
making it suitable for continuous text input and display.
|
|
*/
|
|
|
|
|
|
// TODO define constants for the control chars
|
|
module command_handler
|
|
#(parameter ROWS = 24,
|
|
parameter COLS = 80,
|
|
parameter LAST_ROW = (ROWS-1) * COLS,
|
|
parameter ONE_PAST_LAST_ROW = ROWS * COLS,
|
|
parameter ROW_BITS = 5,
|
|
parameter COL_BITS = 7,
|
|
parameter ADDR_BITS = 11)
|
|
(
|
|
input clk,
|
|
input reset,
|
|
input [7:0] data,
|
|
input valid,
|
|
output ready,
|
|
output [ADDR_BITS-1:0] new_first_char,
|
|
output new_first_char_wen,
|
|
output [7:0] new_char,
|
|
output [ADDR_BITS-1:0] new_char_address,
|
|
output new_char_wen,
|
|
output [COL_BITS-1:0] new_cursor_x,
|
|
output [ROW_BITS-1:0] new_cursor_y,
|
|
output new_cursor_wen
|
|
);
|
|
|
|
// XXX maybe ready should be registered? reg ready_q;
|
|
reg [7:0] new_char_q;
|
|
reg [ADDR_BITS-1:0] new_char_address_q;
|
|
reg new_char_wen_q;
|
|
reg [COL_BITS-1:0] new_cursor_x_q;
|
|
reg [ROW_BITS-1:0] new_cursor_y_q;
|
|
reg new_cursor_wen_q;
|
|
reg [ADDR_BITS-1:0] new_first_char_q;
|
|
reg new_first_char_wen_q;
|
|
|
|
reg [ROW_BITS-1:0] new_row;
|
|
reg [COL_BITS-1:0] new_col;
|
|
// This may temporarily hold a value that's twice the size of a regular
|
|
// address (we adjust it at a later state)
|
|
// so we DON'T substract 1 from ADDR_BITS
|
|
reg [ADDR_BITS:0] new_addr;
|
|
reg [ADDR_BITS-1:0] last_char_to_erase;
|
|
|
|
reg [ADDR_BITS-1:0] current_row_addr;
|
|
reg [ADDR_BITS-1:0] current_char_addr;
|
|
|
|
// state: one hot encoding
|
|
// char is the normal state
|
|
// row, col, addr & cursor form a pipeline for Esc-Y
|
|
// no input accepted on addr & cursor
|
|
// erase is for the erase loop, no input is accepted on this state
|
|
localparam state_char = 8'b00000001;
|
|
localparam state_esc = 8'b00000010;
|
|
localparam state_row = 8'b00000100;
|
|
localparam state_col = 8'b00001000;
|
|
localparam state_addr = 8'b00010000;
|
|
localparam state_cursor = 8'b00100000;
|
|
localparam state_erase = 8'b01000000;
|
|
|
|
reg [7:0] state;
|
|
|
|
// if we are erasing part of the screen or moving the cursor
|
|
// we can't receive new commands
|
|
assign ready = (state & (state_erase | state_cursor | state_addr)) == 0;
|
|
assign new_char = new_char_q;
|
|
assign new_char_address = new_char_address_q;
|
|
assign new_char_wen = new_char_wen_q;
|
|
assign new_cursor_x = new_cursor_x_q;
|
|
assign new_cursor_y = new_cursor_y_q;
|
|
assign new_cursor_wen = new_cursor_wen_q;
|
|
assign new_first_char = new_first_char_q;
|
|
assign new_first_char_wen = new_first_char_wen_q;
|
|
|
|
always @(posedge clk) begin
|
|
if (reset) begin
|
|
new_char_q <= 0;
|
|
new_char_address_q <= 0;
|
|
new_char_wen_q <= 0;
|
|
|
|
new_cursor_x_q <= 0;
|
|
new_cursor_y_q <= 0;
|
|
new_cursor_wen_q <= 0;
|
|
|
|
current_row_addr <= 0;
|
|
current_char_addr <= 0;
|
|
|
|
new_first_char_q <= 0;
|
|
new_first_char_wen_q <= 0;
|
|
|
|
state <= state_char;
|
|
new_row <= 0;
|
|
new_col <= 0;
|
|
new_addr <= 0;
|
|
last_char_to_erase <= 0;
|
|
end
|
|
else begin
|
|
// after one clock cycle we should turn these off
|
|
if (new_char_wen_q) new_char_wen_q <= 0;
|
|
if (new_cursor_wen_q) new_cursor_wen_q <= 0;
|
|
if (new_first_char_wen_q) new_first_char_wen_q <= 0;
|
|
// first the states that consume input
|
|
if (ready && valid) begin
|
|
case (state)
|
|
state_char: begin
|
|
// new char arrived
|
|
if (data >= 8'h20 && data != 8'h7f) begin
|
|
// printable char, easy
|
|
new_char_q <= data;
|
|
new_char_address_q <= current_char_addr;
|
|
new_char_wen_q <= 1;
|
|
// no auto linefeed
|
|
if (new_cursor_x_q != (COLS-1)) begin
|
|
new_cursor_x_q <= new_cursor_x_q + 1;
|
|
current_char_addr <= current_char_addr + 1;
|
|
new_cursor_wen_q <= 1;
|
|
end
|
|
end
|
|
else begin
|
|
case (data)
|
|
// backspace
|
|
8'h08: begin
|
|
if (new_cursor_x_q != 0) begin
|
|
new_cursor_x_q <= new_cursor_x_q - 1;
|
|
current_char_addr <= current_char_addr - 1;
|
|
new_cursor_wen_q <= 1;
|
|
end
|
|
end
|
|
// tab
|
|
8'h09: begin
|
|
// go until the last tab stop by 8 spaces, then 1 by 1
|
|
if (new_cursor_x_q < (COLS-9)) begin
|
|
new_cursor_x_q <= {(new_cursor_x_q[COL_BITS-1:3]+1), 3'b000};
|
|
current_char_addr <= {(current_char_addr[ADDR_BITS-1:3]+1), 3'b000};
|
|
new_cursor_wen_q <= 1;
|
|
end
|
|
else if (new_cursor_x_q != (COLS-1)) begin
|
|
new_cursor_x_q <= new_cursor_x_q + 1;
|
|
current_char_addr <= current_char_addr + 1;
|
|
new_cursor_wen_q <= 1;
|
|
end
|
|
end // case: 8'h09
|
|
// linefeed
|
|
8'h0a: begin
|
|
if (new_cursor_y_q == (ROWS-1)) begin
|
|
new_first_char_q <= new_first_char_q == LAST_ROW?
|
|
0 : new_first_char_q + COLS;
|
|
|
|
if (current_row_addr == LAST_ROW) begin
|
|
current_row_addr <= 0;
|
|
current_char_addr <= new_cursor_x_q;
|
|
end
|
|
else begin
|
|
current_row_addr <= current_row_addr + COLS;
|
|
current_char_addr <= current_char_addr + COLS;
|
|
end
|
|
new_first_char_wen_q <= 1;
|
|
// characters to erase last line
|
|
new_char_q <= " ";
|
|
new_char_address_q <= new_first_char_q;
|
|
new_char_wen_q <= 1;
|
|
last_char_to_erase <= new_first_char_q + (COLS-1);
|
|
state <= state_erase;
|
|
end
|
|
else begin
|
|
new_cursor_y_q <= new_cursor_y_q + 1;
|
|
new_cursor_wen_q <= 1;
|
|
if (current_row_addr == LAST_ROW) begin
|
|
current_row_addr <= 0;
|
|
current_char_addr <= new_cursor_x_q;
|
|
end
|
|
else begin
|
|
current_row_addr <= current_row_addr + COLS;
|
|
current_char_addr <= current_char_addr + COLS;
|
|
end
|
|
end
|
|
end
|
|
// carriage return
|
|
8'h0d: begin
|
|
if (new_cursor_x != 0) begin
|
|
new_cursor_x_q <= 0;
|
|
new_cursor_wen_q <= 1;
|
|
current_char_addr <= current_row_addr;
|
|
end
|
|
end
|
|
// escape
|
|
8'h1b: begin
|
|
state <= state_esc;
|
|
end
|
|
endcase // case (data)
|
|
end // else: !if(data >= 8'h20 && data <= 8'h7e)
|
|
end // case: state_char
|
|
state_esc: begin
|
|
case (data)
|
|
// Basic cursor movement
|
|
// Esc-only, so no BS, LF & SPACE (covered before)
|
|
"B": begin
|
|
if (new_cursor_y_q != (ROWS-1)) begin
|
|
new_cursor_y_q <= new_cursor_y_q + 1;
|
|
new_cursor_wen_q <= 1;
|
|
if (current_row_addr == LAST_ROW) begin
|
|
current_row_addr <= 0;
|
|
current_char_addr <= new_cursor_x_q;
|
|
end
|
|
else begin
|
|
current_row_addr <= current_row_addr + COLS;
|
|
current_char_addr <= current_char_addr + COLS;
|
|
end
|
|
end
|
|
state <= state_char;
|
|
end
|
|
"I": begin
|
|
if (new_cursor_y_q == 0) begin
|
|
if (new_first_char_q == 0) begin
|
|
new_first_char_q <= LAST_ROW;
|
|
current_row_addr <= LAST_ROW;
|
|
current_char_addr <= LAST_ROW + new_cursor_x_q;
|
|
// characters to erase (whole line)
|
|
new_char_address_q <= LAST_ROW;
|
|
last_char_to_erase <= LAST_ROW+(COLS-1);
|
|
end
|
|
else begin
|
|
new_first_char_q <= new_first_char_q - COLS;
|
|
current_row_addr <= current_row_addr - COLS;
|
|
current_char_addr <= current_char_addr - COLS;
|
|
// characters to erase (whole line)
|
|
new_char_address_q <= new_first_char_q - COLS;
|
|
last_char_to_erase <= new_first_char_q - 1;
|
|
end
|
|
new_first_char_wen_q <= 1;
|
|
// character to erase last line
|
|
new_char_q <= " ";
|
|
new_char_wen_q <= 1;
|
|
state <= state_erase;
|
|
end
|
|
else begin
|
|
new_cursor_y_q <= new_cursor_y_q - 1;
|
|
new_cursor_wen_q <= 1;
|
|
if (current_row_addr == 0) begin
|
|
current_row_addr <= LAST_ROW;
|
|
current_char_addr <= LAST_ROW + new_cursor_x_q;
|
|
end
|
|
else begin
|
|
current_row_addr <= current_row_addr - COLS;
|
|
current_char_addr <= current_char_addr - COLS;
|
|
end
|
|
state <= state_char;
|
|
end
|
|
end
|
|
"A": begin
|
|
if (new_cursor_y_q != 0) begin
|
|
new_cursor_y_q <= new_cursor_y_q - 1;
|
|
new_cursor_wen_q <= 1;
|
|
if (current_row_addr == 0) begin
|
|
current_row_addr <= LAST_ROW;
|
|
current_char_addr <= LAST_ROW + new_cursor_x_q;
|
|
end
|
|
else begin
|
|
current_row_addr <= current_row_addr - COLS;
|
|
current_char_addr <= current_char_addr - COLS;
|
|
end
|
|
end
|
|
state <= state_char;
|
|
end
|
|
"C": begin
|
|
if (new_cursor_x_q != (COLS-1)) begin
|
|
new_cursor_x_q <= new_cursor_x_q + 1;
|
|
new_cursor_wen_q <= 1;
|
|
current_char_addr <= current_char_addr+1;
|
|
end
|
|
state <= state_char;
|
|
end
|
|
"D": begin
|
|
if (new_cursor_x_q != 0) begin
|
|
new_cursor_x_q <= new_cursor_x_q - 1;
|
|
new_cursor_wen_q <= 1;
|
|
current_char_addr <= current_char_addr-1;
|
|
end
|
|
state <= state_char;
|
|
end
|
|
// Advanced cursor movement
|
|
// Esc-only, so no CR & TAB (covered before)
|
|
"H": begin
|
|
new_cursor_x_q <= 0;
|
|
new_cursor_y_q <= 0;
|
|
new_cursor_wen_q <= 1;
|
|
current_row_addr <= new_first_char_q;
|
|
current_char_addr <= new_first_char_q;
|
|
state <= state_char;
|
|
end
|
|
"Y": begin
|
|
// "Y" received, expecting row & col
|
|
state <= state_row;
|
|
end
|
|
// Screen erasure
|
|
"K": begin
|
|
// erase to end of line
|
|
new_char_q <= " ";
|
|
new_char_address_q <= current_char_addr;
|
|
new_char_wen_q <= 1;
|
|
last_char_to_erase <= current_row_addr + (COLS-1);
|
|
state <= state_erase;
|
|
end
|
|
"J": begin
|
|
// erase to end of screen
|
|
new_char_q <= " ";
|
|
new_char_address_q <= current_char_addr;
|
|
new_char_wen_q <= 1;
|
|
last_char_to_erase <= new_first_char_q == 0?
|
|
LAST_ROW+(COLS-1): new_first_char_q-1;
|
|
state <= state_erase;
|
|
end
|
|
// escape
|
|
8'h1b: begin
|
|
// on VT52 two escapes don't cancel each other
|
|
// do nothing
|
|
end
|
|
default: begin
|
|
// unrecognized escape sequence, back to normal
|
|
state <= state_char;
|
|
end
|
|
endcase // case (data)
|
|
end // case: state_esc
|
|
state_row: begin
|
|
// row received, now we need col
|
|
new_row <= (data >= 8'h20 && data < (8'h20 + ROWS))?
|
|
data - 8'h20 : new_cursor_y;
|
|
state <= state_col;
|
|
end
|
|
state_col: begin
|
|
// row & col received, now we need to calculate the new row address
|
|
// XXX I'm not sure what happens if data < 8'h20, this is a guess
|
|
new_col <= (data >= 8'h20 && data < (8'h20 + COLS))?
|
|
data - 8'h20 : (COLS-1);
|
|
// this may need substracting if it's more than LAST_ROW
|
|
// but we'll do it in the next state
|
|
// new_addr has an extra bit to avoid overflows
|
|
new_addr <= new_row * 80 + new_first_char_q;
|
|
state <= state_addr;
|
|
end
|
|
// states erase, addr & cursor aren't here as they don't consume input
|
|
endcase // case (state)
|
|
end // if (ready && valid)
|
|
// now we can handle the states that don't consume input
|
|
else begin
|
|
case (state)
|
|
state_erase: begin
|
|
if (new_char_address_q == last_char_to_erase) begin
|
|
// all chars erased, resume normal operation
|
|
state <= state_char;
|
|
end
|
|
else begin
|
|
// keep erasing, but be careful if reaching the end of the buffer
|
|
new_char_address_q = new_char_address_q == LAST_ROW + (COLS+1)?
|
|
0 : new_char_address_q + 1;
|
|
new_char_wen_q <= 1;
|
|
end
|
|
end
|
|
state_addr: begin
|
|
// after possibly adjusting the address we are ready to
|
|
// move the cursor
|
|
new_addr <= new_addr > LAST_ROW? new_addr - ONE_PAST_LAST_ROW : new_addr;
|
|
state <= state_cursor;
|
|
end
|
|
state_cursor: begin
|
|
// move cursor and go back to idle
|
|
new_cursor_x_q <= new_col;
|
|
new_cursor_y_q <= new_row;
|
|
new_cursor_wen_q <= 1;
|
|
// once adjusted, we can ignore the higher bit
|
|
current_row_addr <= new_addr[ADDR_BITS-1:0];
|
|
current_char_addr <= new_addr + new_col;
|
|
|
|
state <= state_char;
|
|
end // if (state == state_col)
|
|
endcase // case (state)
|
|
end // else: !if(ready && valid)
|
|
end // else: !if(reset)
|
|
end // always @ (posedge clk)
|
|
endmodule
|