├── CMakeLists.txt ├── LICENSE ├── Makefile ├── README.md ├── char.rom ├── eia.h ├── eia_linux.c ├── eia_pico.c ├── error.c ├── error.h ├── main_pico.c ├── main_sdl.c ├── palvideo.c ├── palvideo.h ├── palvideo.pio ├── ps2kbd.c ├── ps2kbd.h ├── ps2kbd.pio ├── sdlgui.c ├── sdlgui.h ├── terminal.c └── terminal.h /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.12) 2 | 3 | include($ENV{PICO_SDK_PATH}/external/pico_sdk_import.cmake) 4 | 5 | project(test_project C CXX ASM) 6 | set(CMAKE_C_STANDARD 11) 7 | set(CMAKE_CXX_STANDARD 17) 8 | pico_sdk_init() 9 | 10 | add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/char.arm.o 11 | MAIN_DEPENDENCY char.rom 12 | WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} 13 | COMMAND ${CMAKE_OBJCOPY} 14 | ARGS -I binary -O elf32-littlearm -B armv6s-m char.rom ${CMAKE_CURRENT_BINARY_DIR}/char.arm.o ) 15 | 16 | add_executable(terminominal char.arm.o) 17 | 18 | pico_generate_pio_header(terminominal ${CMAKE_CURRENT_LIST_DIR}/palvideo.pio) 19 | pico_generate_pio_header(terminominal ${CMAKE_CURRENT_LIST_DIR}/ps2kbd.pio) 20 | 21 | target_sources(terminominal PRIVATE 22 | main_pico.c 23 | palvideo.c 24 | ps2kbd.c 25 | eia_pico.c 26 | terminal.c 27 | error.c 28 | ) 29 | 30 | target_link_libraries(terminominal PRIVATE 31 | pico_stdlib 32 | pico_multicore 33 | hardware_pio 34 | hardware_dma 35 | ) 36 | 37 | pico_add_extra_outputs(terminominal) 38 | 39 | target_compile_definitions(terminominal PRIVATE -DKEYBOARD_NORWEGIAN) 40 | 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Kjetil Erga 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CFLAGS=-Wall -Wextra -lSDL2 -lpthread 2 | 3 | all: terminominal 4 | 5 | terminominal: main_sdl.o sdlgui.o terminal.o eia_linux.o error.o char.o 6 | gcc ${CFLAGS} $^ -o $@ 7 | 8 | main_sdl.o: main_sdl.c 9 | gcc ${CFLAGS} -c $^ -o $@ 10 | 11 | sdlgui.o: sdlgui.c 12 | gcc ${CFLAGS} -c $^ -o $@ 13 | 14 | terminal.o: terminal.c 15 | gcc ${CFLAGS} -c $^ -o $@ 16 | 17 | eia_linux.o: eia_linux.c 18 | gcc ${CFLAGS} -c $^ -o $@ 19 | 20 | error.o: error.c 21 | gcc ${CFLAGS} -c $^ -o $@ 22 | 23 | char.o: char.rom 24 | objcopy -I binary -O elf64-x86-64 -B i386 $^ $@ 25 | 26 | .PHONY: clean 27 | clean: 28 | rm -f *.o terminominal 29 | 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Terminominal 2 | VT100 terminal emulator for Raspberry Pi Pico. 3 | 4 | Features: 5 | * Composite video (CVBS) PAL video output. 6 | * PS/2 protocol input, with Norwegian or US keyboard layout. 7 | * Visible picture of 880 dots and 240 scanlines, framed by border. 8 | * Custom 11x10 pixel font, ISO-8859-1 (latin-1) compatible. 9 | * UART baud rate up to 115200 supported. 10 | * Passes some [vttest](https://invisible-island.net/vttest/) cases at least. 11 | * SDL-based Linux version available for test purposes. 12 | * Blinking cursor! 13 | 14 | ## GPIO Connections 15 | ``` 16 | |--------|-----------|------------|-------------------------| 17 | | Pin No | Pin Name | Function | Connected To | 18 | |--------|-----------|------------|-------------------------| 19 | | 1 | GP0 | UART TX | | 20 | | 2 | GP1 | UART RX | | 21 | | 6 | GP4 | PS/2 Data | 3.3V<->5V Level Shifter | 22 | | 7 | GP5 | PS/2 Clock | 3.3V<->5V Level Shifter | 23 | | 21 | GP16 | CVBS DAC | 680 Ohm Resistor | 24 | | 22 | GP17 | CVBS DAC | 220 Ohm Resistor | 25 | | 36 | 3V3 (OUT) | +3.3V | 3.3V<->5V Level Shifter | 26 | | 40 | VBUS | +5V | 3.3V<->5V Level Shifter | 27 | |--------|-----------|------------|-------------------------| 28 | ``` 29 | 30 | ## Compiling 31 | Requirements: 32 | * CMake 33 | * ARM GCC toolchain 34 | * [Pico SDK](https://github.com/raspberrypi/pico-sdk) 35 | 36 | Create a build folder and call cmake pointing to the source directory containing the CMakeLists.txt file: 37 | ``` 38 | mkdir build 39 | cd build 40 | PICO_SDK_PATH=/path/to/pico-sdk cmake /path/to/terminominal/ 41 | make 42 | ``` 43 | 44 | Flash the resulting "terminominal.elf" file with SWD or transfer the "terminominal.uf2" file through USB in BOOTSEL mode. 45 | 46 | ## Further Reading 47 | Information on my blog: 48 | * [VT100 Terminal Emulator on Raspberry Pi Pico](https://kobolt.github.io/article-198.html) 49 | * [Monospaced 11x10 Font](https://kobolt.github.io/article-202.html) 50 | 51 | YouTube video: 52 | * [Terminominal running cmatrix](https://www.youtube.com/watch?v=movhRMJprEs) 53 | 54 | -------------------------------------------------------------------------------- /char.rom: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kobolt/terminominal/62ebf8caca418d8b70024586425b08339324b160/char.rom -------------------------------------------------------------------------------- /eia.h: -------------------------------------------------------------------------------- 1 | #ifndef _EIA_H 2 | #define _EIA_H 3 | 4 | #include 5 | 6 | void eia_init(void); 7 | void eia_send(uint8_t c); 8 | void eia_update(void); 9 | 10 | #endif /* _EIA_H */ 11 | -------------------------------------------------------------------------------- /eia_linux.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include "terminal.h" 13 | 14 | #define TTY_DEVICE "/dev/ttyS2" 15 | #define TTY_SPEED 115200 16 | 17 | 18 | 19 | static int tty_fd; 20 | 21 | 22 | 23 | static void exit_handler(void) 24 | { 25 | close(tty_fd); 26 | } 27 | 28 | 29 | 30 | void eia_init(void) 31 | { 32 | int result; 33 | struct termios tio; 34 | 35 | tty_fd = open(TTY_DEVICE, O_RDWR | O_NOCTTY); 36 | if (tty_fd == -1) { 37 | fprintf(stderr, "open() failed with errno: %d\n", errno); 38 | exit(1); 39 | } 40 | 41 | atexit(exit_handler); 42 | 43 | cfmakeraw(&tio); 44 | cfsetospeed(&tio, B115200); 45 | 46 | result = ioctl(tty_fd, TCSETS, &tio); 47 | if (result == -1) { 48 | fprintf(stderr, "ioctl() failed with errno: %d\n", errno); 49 | exit(1); 50 | } 51 | } 52 | 53 | 54 | 55 | void eia_send(uint8_t c) 56 | { 57 | fprintf(stderr, ">>> 0x%02x %c\n", c, isprint(c) ? c : ' '); 58 | write(tty_fd, &c, 1); 59 | } 60 | 61 | 62 | 63 | void eia_update(void) 64 | { 65 | uint8_t c; 66 | int result; 67 | result = read(tty_fd, &c, 1); 68 | if (result == 1) { 69 | fprintf(stderr, "< 0x%02x %c\n", c, isprint(c) ? c : ' '); 70 | terminal_handle_byte(c); 71 | } 72 | } 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /eia_pico.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "pico/stdlib.h" 3 | #include "hardware/uart.h" 4 | #include "hardware/irq.h" 5 | #include "pico/util/queue.h" 6 | #include "terminal.h" 7 | 8 | 9 | 10 | void eia_init(void) 11 | { 12 | gpio_set_function(0, GPIO_FUNC_UART); 13 | gpio_set_function(1, GPIO_FUNC_UART); 14 | 15 | uart_init(uart0, 115200); 16 | 17 | uart_set_format(uart0, 8, 1, UART_PARITY_NONE); /* 8n1 */ 18 | uart_set_fifo_enabled(uart0, true); 19 | } 20 | 21 | 22 | 23 | int eia_send(uint8_t c) 24 | { 25 | uart_putc(uart0, c); 26 | } 27 | 28 | 29 | 30 | void eia_update(void) 31 | { 32 | if (uart_is_readable(uart0)) { 33 | terminal_handle_byte(uart_getc(uart0)); 34 | } 35 | } 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /error.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "error.h" 4 | 5 | int error_ps2_parity = 0; 6 | 7 | void error_log(const char *format, ...) 8 | { 9 | va_list args; 10 | 11 | va_start(args, format); 12 | vfprintf(stderr, format, args); 13 | va_end(args); 14 | } 15 | 16 | -------------------------------------------------------------------------------- /error.h: -------------------------------------------------------------------------------- 1 | #ifndef _ERROR_H 2 | #define _ERROR_H 3 | 4 | #include 5 | 6 | extern int error_ps2_parity; 7 | 8 | void error_log(const char *format, ...); 9 | 10 | #endif /* _ERROR_H */ 11 | -------------------------------------------------------------------------------- /main_pico.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "palvideo.h" 3 | #include "ps2kbd.h" 4 | #include "terminal.h" 5 | #include "eia.h" 6 | #include "pico/stdlib.h" 7 | #include "pico/multicore.h" 8 | 9 | static void main_core1(void) 10 | { 11 | while (1) { 12 | palvideo_update(); 13 | } 14 | } 15 | 16 | int main(void) 17 | { 18 | eia_init(); 19 | terminal_init(); 20 | palvideo_init(); 21 | ps2kbd_init(); 22 | 23 | multicore_reset_core1(); 24 | multicore_launch_core1(main_core1); 25 | 26 | while (1) { 27 | eia_update(); 28 | } 29 | 30 | return 0; 31 | } 32 | 33 | -------------------------------------------------------------------------------- /main_sdl.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "sdlgui.h" 4 | #include "terminal.h" 5 | #include "eia.h" 6 | 7 | void *main_two(void *argp) 8 | { 9 | (void)argp; 10 | while (1) { 11 | sdlgui_update(); 12 | } 13 | return NULL; 14 | } 15 | 16 | int main(void) 17 | { 18 | pthread_t tid; 19 | 20 | eia_init(); 21 | terminal_init(); 22 | sdlgui_init(); 23 | 24 | pthread_create(&tid, NULL, main_two, NULL); 25 | while (1) { 26 | eia_update(); 27 | } 28 | pthread_join(tid, NULL); 29 | 30 | return 0; 31 | } 32 | 33 | -------------------------------------------------------------------------------- /palvideo.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "pico/stdlib.h" 4 | #include "hardware/pio.h" 5 | #include "hardware/clocks.h" 6 | #include "hardware/dma.h" 7 | #include "palvideo.pio.h" 8 | #include "terminal.h" 9 | 10 | #define ROW_MAX 24 11 | #define COL_MAX 80 12 | #define CHAR_WIDTH 11 13 | #define CHAR_HEIGHT 10 14 | #define FRAME_SCANLINES 625 15 | #define FRAME_SECTIONS 80 16 | 17 | extern uint8_t _binary_char_rom_start[]; 18 | 19 | 20 | 21 | static PIO palvideo_pio; 22 | static uint palvideo_sm; 23 | static uint32_t palvideo_frame[FRAME_SCANLINES][FRAME_SECTIONS]; 24 | static uint32_t *palvideo_ap = &palvideo_frame[0][0]; 25 | 26 | 27 | 28 | static void palvideo_section_prime(uint32_t data) 29 | { 30 | static int scanline = 0; 31 | static int section = 0; 32 | 33 | palvideo_frame[scanline][section] = data; 34 | 35 | section++; 36 | if (section >= FRAME_SECTIONS) { 37 | section = 0; 38 | scanline++; 39 | if (scanline >= FRAME_SCANLINES) { 40 | scanline = 0; 41 | } 42 | } 43 | } 44 | 45 | 46 | 47 | static void palvideo_vsync_pulse_prime(void) 48 | { 49 | /* 27.3us / 0.05us = 546 */ 50 | for (int i = 0; i < 34; i++) { 51 | palvideo_section_prime(0x00000000); 52 | } 53 | 54 | /* 4.7us / 0.05us = 94 */ 55 | palvideo_section_prime(0x05555555); /* Minus 0.1us (2) */ 56 | palvideo_section_prime(0x55555555); 57 | palvideo_section_prime(0x55555555); 58 | palvideo_section_prime(0x55555555); 59 | palvideo_section_prime(0x55555555); 60 | palvideo_section_prime(0x55555555); 61 | } 62 | 63 | 64 | 65 | static void palvideo_equal_pulse_prime(void) 66 | { 67 | /* 2.3us / 0.05us = 46 */ 68 | palvideo_section_prime(0x00000000); 69 | palvideo_section_prime(0x00000000); 70 | palvideo_section_prime(0x00000005); /* Plus 0.1us (2) */ 71 | 72 | /* 29.7us / 0.05us = 594 */ 73 | for (int i = 0; i < 37; i++) { 74 | palvideo_section_prime(0x55555555); 75 | } 76 | } 77 | 78 | 79 | 80 | static void palvideo_scanline_prime(int scanline) 81 | { 82 | /* Line Sync - 4.7us / 0.05us = 94 */ 83 | palvideo_section_prime(0x00000000); 84 | palvideo_section_prime(0x00000000); 85 | palvideo_section_prime(0x00000000); 86 | palvideo_section_prime(0x00000000); 87 | palvideo_section_prime(0x00000000); 88 | 89 | /* Back Porch - 5.6us / 0.05us = 112 */ 90 | palvideo_section_prime(0x00000005); 91 | palvideo_section_prime(0x55555555); 92 | palvideo_section_prime(0x55555555); 93 | palvideo_section_prime(0x55555555); 94 | palvideo_section_prime(0x55555555); 95 | palvideo_section_prime(0x55555555); 96 | palvideo_section_prime(0x55555555); 97 | palvideo_section_prime(0x5555555A); /* Plus 0.1us (2) */ 98 | 99 | /* Left Border */ 100 | palvideo_section_prime(0xAAAAAAAA); 101 | palvideo_section_prime(0xAAAAAAAA); 102 | palvideo_section_prime(0xAAAAAAAA); 103 | palvideo_section_prime(0xAAAAAAAA); 104 | palvideo_section_prime(0xAAAAAAAA); 105 | 106 | if (scanline >= 42 && scanline < 282) { 107 | /* Screen Area */ 108 | for (int i = 0; i < 55; i++) { 109 | palvideo_section_prime(0x55555555); 110 | } 111 | } else { 112 | /* Top and Bottom Border */ 113 | for (int i = 0; i < 55; i++) { 114 | palvideo_section_prime(0xAAAAAAAA); 115 | } 116 | } 117 | 118 | /* Right Border */ 119 | palvideo_section_prime(0xAAAAAAAA); 120 | palvideo_section_prime(0xAAAAAAAA); 121 | palvideo_section_prime(0xAAAAAAAA); 122 | palvideo_section_prime(0xAAAAAAAA); 123 | 124 | /* Front Porch - 1.65us / 0.05 = 33 */ 125 | palvideo_section_prime(0xAAAAAAA9); /* Plus 0.75us (15) */ 126 | palvideo_section_prime(0x55555555); 127 | palvideo_section_prime(0x55555555); 128 | } 129 | 130 | 131 | 132 | static void palvideo_frame_prime() 133 | { 134 | /* 1 -> 5 */ 135 | palvideo_vsync_pulse_prime(); 136 | palvideo_vsync_pulse_prime(); 137 | palvideo_vsync_pulse_prime(); 138 | palvideo_vsync_pulse_prime(); 139 | palvideo_vsync_pulse_prime(); 140 | palvideo_equal_pulse_prime(); 141 | palvideo_equal_pulse_prime(); 142 | palvideo_equal_pulse_prime(); 143 | palvideo_equal_pulse_prime(); 144 | palvideo_equal_pulse_prime(); 145 | 146 | /* 6 -> 310 */ 147 | for (int i = 6; i <= 310; i++) { 148 | palvideo_scanline_prime(i - 6); 149 | } 150 | 151 | /* 311 -> 317 */ 152 | palvideo_equal_pulse_prime(); 153 | palvideo_equal_pulse_prime(); 154 | palvideo_equal_pulse_prime(); 155 | palvideo_equal_pulse_prime(); 156 | palvideo_equal_pulse_prime(); 157 | palvideo_vsync_pulse_prime(); 158 | palvideo_vsync_pulse_prime(); 159 | palvideo_vsync_pulse_prime(); 160 | palvideo_vsync_pulse_prime(); 161 | palvideo_vsync_pulse_prime(); 162 | palvideo_equal_pulse_prime(); 163 | palvideo_equal_pulse_prime(); 164 | palvideo_equal_pulse_prime(); 165 | palvideo_equal_pulse_prime(); 166 | 167 | /* 318 -> 622 */ 168 | for (int i = 318; i <= 622; i++) { 169 | palvideo_scanline_prime(i - 318); 170 | } 171 | 172 | /* 623 -> 625 */ 173 | palvideo_equal_pulse_prime(); 174 | palvideo_equal_pulse_prime(); 175 | palvideo_equal_pulse_prime(); 176 | palvideo_equal_pulse_prime(); 177 | palvideo_equal_pulse_prime(); 178 | palvideo_equal_pulse_prime(); 179 | } 180 | 181 | 182 | 183 | void palvideo_init(void) 184 | { 185 | uint offset; 186 | 187 | palvideo_frame_prime(); 188 | 189 | palvideo_pio = pio0; 190 | palvideo_sm = pio_claim_unused_sm(palvideo_pio, true); 191 | offset = pio_add_program(palvideo_pio, &palvideo_program); 192 | palvideo_program_init(palvideo_pio, palvideo_sm, offset); 193 | 194 | dma_channel_config c0 = dma_channel_get_default_config(0); 195 | channel_config_set_transfer_data_size(&c0, DMA_SIZE_32); 196 | channel_config_set_read_increment(&c0, true); 197 | channel_config_set_write_increment(&c0, false); 198 | channel_config_set_dreq(&c0, DREQ_PIO0_TX0); 199 | channel_config_set_chain_to(&c0, 1); 200 | 201 | dma_channel_configure(0, &c0, 202 | &palvideo_pio->txf[palvideo_sm], &palvideo_frame, 203 | FRAME_SCANLINES * FRAME_SECTIONS, false); 204 | 205 | dma_channel_config c1 = dma_channel_get_default_config(1); 206 | channel_config_set_transfer_data_size(&c1, DMA_SIZE_32); 207 | channel_config_set_read_increment(&c1, false); 208 | channel_config_set_write_increment(&c1, false); 209 | channel_config_set_chain_to(&c1, 0); 210 | 211 | dma_channel_configure(1, &c1, 212 | &dma_hw->ch[0].read_addr, &palvideo_ap, 1, false); 213 | 214 | dma_channel_start(0); 215 | } 216 | 217 | 218 | 219 | static inline int palvideo_shade(bool on, terminal_char_t c) 220 | { 221 | if (on ^ ((c.attribute >> TERMINAL_ATTRIBUTE_REVERSE) & 0x1)) { 222 | if (((c.attribute >> TERMINAL_ATTRIBUTE_BLINK) & 0x1) && 223 | ((time_us_32() % 1000000) > 500000)) { 224 | return 0b01; 225 | } else { 226 | if ((c.attribute >> TERMINAL_ATTRIBUTE_BOLD) & 0x1) { 227 | return 0b10; 228 | } else { 229 | return 0b11; 230 | } 231 | } 232 | } else { 233 | if (((c.attribute >> TERMINAL_ATTRIBUTE_REVERSE) & 0x1) && 234 | (((c.attribute >> TERMINAL_ATTRIBUTE_BLINK) & 0x1) && 235 | ((time_us_32() % 1000000) > 500000))) { 236 | if ((c.attribute >> TERMINAL_ATTRIBUTE_BOLD) & 0x1) { 237 | return 0b10; 238 | } else { 239 | return 0b11; 240 | } 241 | } else { 242 | return 0b01; 243 | } 244 | } 245 | } 246 | 247 | 248 | 249 | static inline void palvideo_set_pixel(uint16_t row, uint16_t col, 250 | int y, int x, int shade) 251 | { 252 | uint32_t mask; 253 | int shift, scanline, section; 254 | 255 | scanline = ((row * 10) + y) + 5 + 42; /* First interlace. */ 256 | section = (((col * 11) + x) / 16) + 18; 257 | 258 | shift = 30 - ((((col * 11) + x) % 16) * 2); 259 | mask = 0b11 << shift; 260 | palvideo_frame[scanline][section] = 261 | (palvideo_frame[scanline][section] & ~mask) | ((shade << shift) & mask); 262 | 263 | scanline = ((row * 10) + y) + 317 + 42; /* Second interlace. */ 264 | palvideo_frame[scanline][section] = 265 | (palvideo_frame[scanline][section] & ~mask) | ((shade << shift) & mask); 266 | } 267 | 268 | 269 | 270 | static inline void palvideo_char(uint8_t row, uint8_t col, terminal_char_t c) 271 | { 272 | int y, x; 273 | uint8_t char_data; 274 | int offset; 275 | bool on; 276 | 277 | for (y = 0; y < CHAR_HEIGHT; y++) { 278 | offset = (c.byte * CHAR_HEIGHT * 2) + (y * 2); 279 | char_data = _binary_char_rom_start[offset]; 280 | for (x = 0; x < 8; x++) { 281 | if (((c.attribute >> TERMINAL_ATTRIBUTE_UNDERLINE) & 0x1) 282 | && y == (CHAR_HEIGHT - 1)) { 283 | on = true; 284 | } else { 285 | on = (char_data >> x) & 0x1; 286 | } 287 | palvideo_set_pixel(row, col, y, (7 - x), 288 | palvideo_shade(on, c)); 289 | } 290 | 291 | char_data = _binary_char_rom_start[offset + 1]; 292 | for (x = 0; x < 3; x++) { 293 | if (((c.attribute >> TERMINAL_ATTRIBUTE_UNDERLINE) & 0x1) 294 | && y == (CHAR_HEIGHT - 1)) { 295 | on = true; 296 | } else { 297 | on = (char_data >> x) & 0x1; 298 | } 299 | palvideo_set_pixel(row, col, y, (2 - x) + 8, 300 | palvideo_shade(on, c)); 301 | } 302 | } 303 | } 304 | 305 | 306 | 307 | void palvideo_update(void) 308 | { 309 | int row, col; 310 | 311 | for (row = 0; row < ROW_MAX; row++) { 312 | for (col = 0; col < COL_MAX; col++) { 313 | if (terminal_char_changed(row, col)) { 314 | palvideo_char(row, col, terminal_char_get(row, col)); 315 | } 316 | } 317 | } 318 | } 319 | 320 | 321 | 322 | -------------------------------------------------------------------------------- /palvideo.h: -------------------------------------------------------------------------------- 1 | #ifndef _PALVIDEO_H 2 | #define _PALVIDEO_H 3 | 4 | int palvideo_init(void); 5 | void palvideo_update(void); 6 | 7 | #endif /* _PALVIDEO_H */ 8 | -------------------------------------------------------------------------------- /palvideo.pio: -------------------------------------------------------------------------------- 1 | 2 | .program palvideo 3 | 4 | .wrap_target 5 | pull block 6 | again: 7 | out pins, 2 [0] 8 | jmp !osre, again [0] 9 | .wrap 10 | 11 | % c-sdk { 12 | static inline void palvideo_program_init(PIO pio, uint sm, uint offset) 13 | { 14 | pio_sm_config c = palvideo_program_get_default_config(offset); 15 | sm_config_set_out_shift(&c, false, true, 32); 16 | sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_TX); 17 | 18 | /* Configure pins 16 and 17 as output. */ 19 | sm_config_set_out_pins(&c, 16, 2); 20 | pio_gpio_init(pio, 16); 21 | pio_gpio_init(pio, 17); 22 | pio_sm_set_consecutive_pindirs(pio, sm, 16, 2, true); 23 | 24 | /* Configure to run at 40MHz. */ 25 | float div = clock_get_hz(clk_sys) / 40000000.0; 26 | sm_config_set_clkdiv(&c, div); 27 | 28 | pio_sm_init(pio, sm, offset, &c); 29 | pio_sm_set_enabled(pio, sm, true); 30 | } 31 | %} 32 | 33 | -------------------------------------------------------------------------------- /ps2kbd.c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kobolt/terminominal/62ebf8caca418d8b70024586425b08339324b160/ps2kbd.c -------------------------------------------------------------------------------- /ps2kbd.h: -------------------------------------------------------------------------------- 1 | #ifndef _PS2KBD_H 2 | #define _PS2KBD_H 3 | 4 | int ps2kbd_init(void); 5 | 6 | #endif /* _PS2KBD_H */ 7 | -------------------------------------------------------------------------------- /ps2kbd.pio: -------------------------------------------------------------------------------- 1 | 2 | .program ps2kbd 3 | 4 | .wrap_target 5 | wait 0 pin 1 ; Start Bit 6 | wait 1 pin 1 7 | set x, 8 8 | again: 9 | wait 0 pin 1 ; Data Bits + Parity Bit 10 | in pins, 1 11 | wait 1 pin 1 12 | jmp x-- again 13 | wait 0 pin 1 ; Stop Bit 14 | wait 1 pin 1 15 | irq wait 0 16 | .wrap 17 | 18 | % c-sdk { 19 | static inline void ps2kbd_program_init(PIO pio, uint sm, uint offset) 20 | { 21 | pio_sm_config c = ps2kbd_program_get_default_config(offset); 22 | sm_config_set_in_shift(&c, true, true, 9); /* 8 Data Bits + Parity Bit */ 23 | sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_RX); 24 | 25 | /* Configure pins 4 (Data) and 5 (Clock) as input. */ 26 | sm_config_set_in_pins(&c, 4); 27 | pio_gpio_init(pio, 4); 28 | pio_gpio_init(pio, 5); 29 | pio_sm_set_consecutive_pindirs(pio, sm, 4, 2, false); 30 | 31 | gpio_pull_up(4); 32 | gpio_pull_up(5); 33 | 34 | pio_sm_init(pio, sm, offset, &c); 35 | pio_sm_set_enabled(pio, sm, true); 36 | } 37 | %} 38 | 39 | -------------------------------------------------------------------------------- /sdlgui.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "terminal.h" 7 | #include "eia.h" 8 | 9 | #ifdef COL_132 10 | #define SDLGUI_WIDTH 1452 11 | #else 12 | #define SDLGUI_WIDTH 880 13 | #endif 14 | #define SDLGUI_HEIGHT 240 15 | 16 | #define CHAR_WIDTH 11 17 | #define CHAR_HEIGHT 10 18 | 19 | extern uint8_t _binary_char_rom_start[]; 20 | 21 | 22 | 23 | static SDL_Window *sdlgui_window = NULL; 24 | static SDL_Renderer *sdlgui_renderer = NULL; 25 | static SDL_Texture *sdlgui_texture = NULL; 26 | static SDL_PixelFormat *sdlgui_pixel_format = NULL; 27 | static Uint32 *sdlgui_pixels = NULL; 28 | static int sdlgui_pixel_pitch = 0; 29 | static Uint32 sdlgui_ticks = 0; 30 | 31 | 32 | 33 | static void sdlgui_exit_handler(void) 34 | { 35 | if (sdlgui_pixel_format != NULL) { 36 | SDL_FreeFormat(sdlgui_pixel_format); 37 | } 38 | if (sdlgui_texture != NULL) { 39 | SDL_UnlockTexture(sdlgui_texture); 40 | SDL_DestroyTexture(sdlgui_texture); 41 | } 42 | if (sdlgui_renderer != NULL) { 43 | SDL_DestroyRenderer(sdlgui_renderer); 44 | } 45 | if (sdlgui_window != NULL) { 46 | SDL_DestroyWindow(sdlgui_window); 47 | } 48 | SDL_Quit(); 49 | } 50 | 51 | 52 | 53 | int sdlgui_init(void) 54 | { 55 | if (SDL_Init(SDL_INIT_VIDEO) != 0) { 56 | fprintf(stderr, "Unable to initalize SDL: %s\n", SDL_GetError()); 57 | return -1; 58 | } 59 | atexit(sdlgui_exit_handler); 60 | 61 | if ((sdlgui_window = SDL_CreateWindow("Terminominal", 62 | SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 63 | SDLGUI_WIDTH, SDLGUI_HEIGHT, 0)) == NULL) { 64 | fprintf(stderr, "Unable to set video mode: %s\n", SDL_GetError()); 65 | return -1; 66 | } 67 | 68 | if ((sdlgui_renderer = SDL_CreateRenderer(sdlgui_window, -1, 0)) == NULL) { 69 | fprintf(stderr, "Unable to create renderer: %s\n", SDL_GetError()); 70 | return -1; 71 | } 72 | 73 | if ((sdlgui_texture = SDL_CreateTexture(sdlgui_renderer, 74 | SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, 75 | SDLGUI_WIDTH, SDLGUI_HEIGHT)) == NULL) { 76 | fprintf(stderr, "Unable to create texture: %s\n", SDL_GetError()); 77 | return -1; 78 | } 79 | 80 | if (SDL_LockTexture(sdlgui_texture, NULL, 81 | (void **)&sdlgui_pixels, &sdlgui_pixel_pitch) != 0) { 82 | fprintf(stderr, "Unable to lock texture: %s\n", SDL_GetError()); 83 | return -1; 84 | } 85 | 86 | if ((sdlgui_pixel_format = SDL_AllocFormat(SDL_PIXELFORMAT_ARGB8888)) 87 | == NULL) { 88 | fprintf(stderr, "Unable to create pixel format: %s\n", SDL_GetError()); 89 | return -1; 90 | } 91 | 92 | return 0; 93 | } 94 | 95 | 96 | 97 | static inline void sdlgui_set_pixel(uint16_t y, uint16_t x, uint8_t shade) 98 | { 99 | sdlgui_pixels[(y * SDLGUI_WIDTH) + x] = 100 | SDL_MapRGB(sdlgui_pixel_format, shade, shade, shade); 101 | } 102 | 103 | 104 | 105 | static inline uint8_t sdlgui_shade(bool on, terminal_char_t c) 106 | { 107 | if (on ^ ((c.attribute >> TERMINAL_ATTRIBUTE_REVERSE) & 0x1)) { 108 | if (((c.attribute >> TERMINAL_ATTRIBUTE_BLINK) & 0x1) && 109 | ((sdlgui_ticks % 1000) > 500)) { 110 | return 0x0; 111 | } else { 112 | if ((c.attribute >> TERMINAL_ATTRIBUTE_BOLD) & 0x1) { 113 | return 0x7F; 114 | } else { 115 | return 0xFF; 116 | } 117 | } 118 | } else { 119 | if (((c.attribute >> TERMINAL_ATTRIBUTE_REVERSE) & 0x1) && 120 | (((c.attribute >> TERMINAL_ATTRIBUTE_BLINK) & 0x1) && 121 | ((sdlgui_ticks % 1000) > 500))) { 122 | if ((c.attribute >> TERMINAL_ATTRIBUTE_BOLD) & 0x1) { 123 | return 0x7F; 124 | } else { 125 | return 0xFF; 126 | } 127 | } else { 128 | return 0x0; 129 | } 130 | } 131 | } 132 | 133 | 134 | 135 | static inline void sdlgui_char(uint8_t row, uint8_t col, terminal_char_t c) 136 | { 137 | int y, x; 138 | uint8_t char_data; 139 | int offset; 140 | bool on; 141 | 142 | for (y = 0; y < CHAR_HEIGHT; y++) { 143 | offset = (c.byte * CHAR_HEIGHT * 2) + (y * 2); 144 | 145 | char_data = _binary_char_rom_start[offset]; 146 | for (x = 0; x < 8; x++) { 147 | if (((c.attribute >> TERMINAL_ATTRIBUTE_UNDERLINE) & 0x1) 148 | && y == (CHAR_HEIGHT - 1)) { 149 | on = true; 150 | } else { 151 | on = (char_data >> x) & 0x1; 152 | } 153 | sdlgui_set_pixel((row * CHAR_HEIGHT) + y, 154 | (col * CHAR_WIDTH) + (7 - x), 155 | sdlgui_shade(on, c)); 156 | } 157 | 158 | char_data = _binary_char_rom_start[offset + 1]; 159 | for (x = 0; x < 3; x++) { 160 | if (((c.attribute >> TERMINAL_ATTRIBUTE_UNDERLINE) & 0x1) 161 | && y == (CHAR_HEIGHT - 1)) { 162 | on = true; 163 | } else { 164 | on = (char_data >> x) & 0x1; 165 | } 166 | sdlgui_set_pixel((row * CHAR_HEIGHT) + y, 167 | (col * CHAR_WIDTH) + (2 - x) + 8, 168 | sdlgui_shade(on, c)); 169 | } 170 | } 171 | } 172 | 173 | 174 | 175 | void sdlgui_update(void) 176 | { 177 | int row, col; 178 | SDL_Event event; 179 | 180 | while (SDL_PollEvent(&event) == 1) { 181 | switch (event.type) { 182 | case SDL_QUIT: 183 | exit(0); 184 | break; 185 | 186 | case SDL_TEXTINPUT: 187 | eia_send(event.text.text[0] & 0xFF); 188 | break; 189 | 190 | case SDL_KEYDOWN: 191 | switch (event.key.keysym.sym) { 192 | case SDLK_RETURN: 193 | eia_send('\n'); 194 | break; 195 | 196 | case SDLK_ESCAPE: 197 | eia_send(0x1B); 198 | break; 199 | 200 | case SDLK_BACKSPACE: 201 | eia_send(0x08); 202 | break; 203 | 204 | case SDLK_TAB: 205 | eia_send(0x09); 206 | break; 207 | 208 | case SDLK_UP: 209 | eia_send(0x1B); 210 | if (terminal_cursor_key_code() != 0) { 211 | eia_send(terminal_cursor_key_code()); 212 | } 213 | eia_send('A'); 214 | break; 215 | 216 | case SDLK_LEFT: 217 | eia_send(0x1B); 218 | if (terminal_cursor_key_code() != 0) { 219 | eia_send(terminal_cursor_key_code()); 220 | } 221 | eia_send('D'); 222 | break; 223 | 224 | case SDLK_DOWN: 225 | eia_send(0x1B); 226 | if (terminal_cursor_key_code() != 0) { 227 | eia_send(terminal_cursor_key_code()); 228 | } 229 | eia_send('B'); 230 | break; 231 | 232 | case SDLK_RIGHT: 233 | eia_send(0x1B); 234 | if (terminal_cursor_key_code() != 0) { 235 | eia_send(terminal_cursor_key_code()); 236 | } 237 | eia_send('C'); 238 | break; 239 | } 240 | break; 241 | } 242 | } 243 | 244 | if (sdlgui_renderer != NULL) { 245 | SDL_UnlockTexture(sdlgui_texture); 246 | 247 | SDL_RenderCopy(sdlgui_renderer, sdlgui_texture, NULL, NULL); 248 | 249 | if (SDL_LockTexture(sdlgui_texture, NULL, 250 | (void **)&sdlgui_pixels, &sdlgui_pixel_pitch) != 0) { 251 | fprintf(stderr, "Unable to lock texture: %s\n", SDL_GetError()); 252 | exit(0); 253 | } 254 | } 255 | 256 | /* Force 60 Hz (NTSC) */ 257 | while ((SDL_GetTicks() - sdlgui_ticks) < 16) { 258 | SDL_Delay(1); 259 | } 260 | 261 | for (row = 0; row < (SDLGUI_HEIGHT / CHAR_HEIGHT); row++) { 262 | for (col = 0; col < (SDLGUI_WIDTH / CHAR_WIDTH); col++) { 263 | if (terminal_char_changed(row, col)) { 264 | sdlgui_char(row, col, terminal_char_get(row, col)); 265 | } 266 | } 267 | } 268 | 269 | if (sdlgui_renderer != NULL) { 270 | SDL_RenderPresent(sdlgui_renderer); 271 | } 272 | 273 | sdlgui_ticks = SDL_GetTicks(); 274 | } 275 | 276 | 277 | 278 | -------------------------------------------------------------------------------- /sdlgui.h: -------------------------------------------------------------------------------- 1 | #ifndef _SDLGUI_H 2 | #define _SDLGUI_H 3 | 4 | int sdlgui_init(void); 5 | void sdlgui_update(void); 6 | 7 | #endif /* _SDLGUI_H */ 8 | -------------------------------------------------------------------------------- /terminal.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "terminal.h" 5 | #include "eia.h" 6 | #include "error.h" 7 | 8 | #define PARAM_MAX 8 9 | #define PARAM_LEN 12 10 | 11 | #define ROW_MAX_HARD 24 12 | #define COL_MAX_HARD 132 13 | 14 | typedef enum { 15 | ESCAPE_NONE = 0, 16 | ESCAPE_START = 1, 17 | ESCAPE_CSI = 2, 18 | ESCAPE_HASH = 3, 19 | ESCAPE_G0_SET = 4, 20 | ESCAPE_G1_SET = 5, 21 | } escape_t; 22 | 23 | 24 | 25 | static terminal_char_t screen[ROW_MAX_HARD][COL_MAX_HARD]; 26 | static bool screen_changed[ROW_MAX_HARD][COL_MAX_HARD]; 27 | static bool tab_stop[COL_MAX_HARD]; 28 | 29 | static int cursor_row; 30 | static int cursor_col; 31 | static int margin_top; 32 | static int margin_bottom; 33 | static uint8_t cursor_print_attribute; 34 | static bool cursor_outside_scroll; 35 | 36 | static uint8_t current_g0_set; 37 | static uint8_t current_g1_set; 38 | 39 | static int saved_row; 40 | static int saved_col; 41 | static uint8_t saved_print_attribute; 42 | 43 | static escape_t escape; 44 | static char param[PARAM_MAX][PARAM_LEN]; 45 | static int param_index; 46 | static bool param_used; 47 | 48 | static bool mode_cursor_key_app = false; 49 | static bool mode_ansi = true; 50 | static bool mode_column_132 = false; 51 | static bool mode_scrolling_smooth = false; 52 | static bool mode_screen_reverse = false; 53 | static bool mode_origin_relative = false; 54 | static bool mode_wraparound = false; 55 | static bool mode_auto_repeat = false; 56 | static bool mode_interlace = false; 57 | static bool mode_keypad_app = false; 58 | static bool mode_line_feed = false; 59 | 60 | 61 | 62 | static inline int col_max(void) 63 | { 64 | return (mode_column_132) ? 131 : 79; /* 0-Indexed */ 65 | } 66 | 67 | static inline int row_max(void) 68 | { 69 | return ROW_MAX_HARD - 1; 70 | } 71 | 72 | 73 | 74 | static inline void param_reset(void) 75 | { 76 | param_index = 0; 77 | param_used = false; 78 | for (int i = 0; i < PARAM_MAX; i++) { 79 | param[i][0] = '\0'; 80 | } 81 | } 82 | 83 | static inline void param_append(char c) 84 | { 85 | int len = 0; 86 | while (param[param_index][len] != '\0') { 87 | len++; 88 | } 89 | if (len >= (PARAM_LEN - 1)) { 90 | error_log("Overflow on parameter length!\n"); 91 | return; 92 | } 93 | param[param_index][len] = c; 94 | param[param_index][len+1] = '\0'; 95 | } 96 | 97 | 98 | 99 | static inline void tab_stop_clear(int col) 100 | { 101 | /* Clear All */ 102 | if (col == -1) { 103 | for (int i = 0; i < COL_MAX_HARD; i++) { 104 | tab_stop[i] = false; 105 | } 106 | return; 107 | } 108 | 109 | /* Clear Single */ 110 | if (col > col_max()) { 111 | return; 112 | } 113 | tab_stop[col] = false; 114 | } 115 | 116 | static inline void tab_stop_set(int col) 117 | { 118 | if (col > col_max()) { 119 | return; 120 | } 121 | tab_stop[col] = true; 122 | } 123 | 124 | static inline void tab_stop_default(void) 125 | { 126 | for (int i = 8; i < COL_MAX_HARD; i += 8) { 127 | tab_stop[i] = true; 128 | } 129 | } 130 | 131 | 132 | 133 | static inline void screen_set(int row, int col, terminal_char_t c) 134 | { 135 | if (screen[row][col].byte != c.byte || 136 | screen[row][col].attribute != c.attribute) { 137 | screen[row][col] = c; 138 | screen_changed[row][col] = true; 139 | } 140 | } 141 | 142 | 143 | 144 | static inline terminal_char_t screen_get(int row, int col) 145 | { 146 | return screen[row][col]; 147 | } 148 | 149 | 150 | 151 | static inline void erase_in_char(uint8_t row, uint8_t col) 152 | { 153 | terminal_char_t c; 154 | 155 | if (col > col_max()) { 156 | return; 157 | } 158 | if (row > row_max()) { 159 | return; 160 | } 161 | 162 | c.byte = ' '; 163 | c.attribute = 0; 164 | 165 | screen_set(row, col, c); 166 | } 167 | 168 | 169 | 170 | static void erase_in_line(int p) 171 | { 172 | int col; 173 | 174 | if (p == 0) { 175 | /* Erase from the active position to the end of the line, inclusive. */ 176 | for (col = cursor_col; col <= col_max(); col++) { 177 | erase_in_char(cursor_row, col); 178 | } 179 | 180 | } else if (p == 1) { 181 | /* Erase from the start of the line to the active position, inclusive. */ 182 | for (col = 0; col <= cursor_col; col++) { 183 | erase_in_char(cursor_row, col); 184 | } 185 | 186 | } else if (p == 2) { 187 | /* Erase all of the line, inclusive. */ 188 | for (col = 0; col <= col_max(); col++) { 189 | erase_in_char(cursor_row, col); 190 | } 191 | } 192 | } 193 | 194 | 195 | 196 | static void erase_in_display(int p) 197 | { 198 | int row, col; 199 | 200 | if (p == 0) { 201 | /* Erase from the active position to the end of the screen, inclusive. */ 202 | for (row = cursor_row + 1; row <= row_max(); row++) { 203 | for (col = 0; col <= col_max(); col++) { 204 | erase_in_char(row, col); 205 | } 206 | } 207 | erase_in_line(0); 208 | 209 | } else if (p == 1) { 210 | /* Erase from start of the screen to the active position, inclusive. */ 211 | for (row = 0; row < cursor_row; row++) { 212 | for (col = 0; col <= col_max(); col++) { 213 | erase_in_char(row, col); 214 | } 215 | } 216 | erase_in_line(1); 217 | 218 | } else if (p == 2) { 219 | /* Erase all of the display. */ 220 | for (row = 0; row <= row_max(); row++) { 221 | for (col = 0; col <= col_max(); col++) { 222 | erase_in_char(row, col); 223 | } 224 | } 225 | } 226 | } 227 | 228 | 229 | 230 | static void scroll_up(void) 231 | { 232 | int row, col; 233 | for (row = margin_top + 1; row < (margin_bottom + 1); row++) { 234 | for (col = 0; col <= col_max(); col++) { 235 | screen_set(row - 1, col, screen_get(row, col)); 236 | } 237 | } 238 | cursor_row = margin_bottom; 239 | erase_in_line(2); 240 | } 241 | 242 | static void scroll_down(void) 243 | { 244 | int row, col; 245 | for (row = margin_bottom; row > margin_top; row--) { 246 | for (col = 0; col <= col_max(); col++) { 247 | screen_set(row, col, screen_get(row - 1, col)); 248 | } 249 | } 250 | cursor_row = margin_top; 251 | erase_in_line(2); 252 | } 253 | 254 | 255 | 256 | static inline void print_char(uint8_t byte) 257 | { 258 | terminal_char_t c; 259 | 260 | /* Handle cursor wrapping. */ 261 | if (cursor_col > col_max()) { 262 | if (mode_wraparound) { 263 | cursor_col = 0; 264 | cursor_row++; 265 | if (cursor_row > row_max()) { 266 | cursor_row = row_max(); 267 | } 268 | } else { 269 | cursor_col = col_max(); 270 | } 271 | } 272 | 273 | c.byte = byte; 274 | c.attribute = cursor_print_attribute; 275 | 276 | screen_set(cursor_row, cursor_col, c); 277 | if (byte == ' ' && cursor_col == col_max()) { 278 | /* Don't move cursor if printing a space at the right margin. */ 279 | } else { 280 | cursor_col++; 281 | } 282 | } 283 | 284 | 285 | 286 | static inline void screen_alignment_display(void) 287 | { 288 | terminal_char_t c; 289 | int row, col; 290 | 291 | c.byte = 'E'; 292 | c.attribute = 0; 293 | 294 | for (row = 0; row <= row_max(); row++) { 295 | for (col = 0; col <= col_max(); col++) { 296 | screen_set(row, col, c); 297 | } 298 | } 299 | } 300 | 301 | 302 | 303 | static inline void cursor_activate(void) 304 | { 305 | terminal_char_t c; 306 | 307 | if (cursor_col > col_max()) { 308 | return; 309 | } 310 | if (cursor_row > row_max()) { 311 | return; 312 | } 313 | 314 | c = screen_get(cursor_row, cursor_col); 315 | c.attribute |= (0x1 << TERMINAL_ATTRIBUTE_REVERSE); 316 | c.attribute |= (0x1 << TERMINAL_ATTRIBUTE_BLINK); 317 | screen_set(cursor_row, cursor_col, c); 318 | } 319 | 320 | 321 | 322 | static inline void cursor_deactivate(void) 323 | { 324 | terminal_char_t c; 325 | 326 | if (cursor_col > col_max()) { 327 | return; 328 | } 329 | if (cursor_row > row_max()) { 330 | return; 331 | } 332 | 333 | c = screen_get(cursor_row, cursor_col); 334 | c.attribute &= ~(0x1 << TERMINAL_ATTRIBUTE_REVERSE); 335 | c.attribute &= ~(0x1 << TERMINAL_ATTRIBUTE_BLINK); 336 | screen_set(cursor_row, cursor_col, c); 337 | } 338 | 339 | 340 | 341 | void terminal_init(void) 342 | { 343 | cursor_row = 0; 344 | cursor_col = 0; 345 | cursor_print_attribute = 0; 346 | cursor_outside_scroll = false; 347 | 348 | current_g0_set = 0; 349 | current_g1_set = 0; 350 | 351 | saved_row = 0; 352 | saved_col = 0; 353 | saved_print_attribute = 0; 354 | 355 | escape = ESCAPE_NONE; 356 | 357 | margin_top = 0; 358 | margin_bottom = row_max(); 359 | 360 | for (int row = 0; row < ROW_MAX_HARD; row++) { 361 | for (int col = 0; col < COL_MAX_HARD; col++) { 362 | screen[row][col].byte = ' '; 363 | screen[row][col].attribute = 0; 364 | screen_changed[row][col] = true; 365 | } 366 | } 367 | 368 | tab_stop_default(); 369 | 370 | cursor_activate(); 371 | } 372 | 373 | 374 | 375 | void terminal_handle_escape_csi(uint8_t byte) 376 | { 377 | int param_int, i; 378 | 379 | switch (byte) { 380 | case 'A': /* CUU - Cursor Up */ 381 | param_int = (param_used) ? atoi(param[0]) : 1; 382 | param_int = (param_int == 0) ? 1 : param_int; /* Convert zero to one. */ 383 | if (param_int > (margin_top + cursor_row)) { 384 | cursor_row = margin_top; 385 | } else { 386 | cursor_row -= param_int; 387 | } 388 | escape = ESCAPE_NONE; 389 | break; 390 | 391 | case 'B': /* CUD - Cursor Down */ 392 | param_int = (param_used) ? atoi(param[0]) : 1; 393 | param_int = (param_int == 0) ? 1 : param_int; /* Convert zero to one. */ 394 | if (param_int > (margin_bottom - cursor_row)) { 395 | cursor_row = margin_bottom; 396 | } else { 397 | cursor_row += param_int; 398 | } 399 | escape = ESCAPE_NONE; 400 | break; 401 | 402 | case 'C': /* CUF - Cursor Forward */ 403 | param_int = (param_used) ? atoi(param[0]) : 1; 404 | param_int = (param_int == 0) ? 1 : param_int; /* Convert zero to one. */ 405 | if (param_int > (col_max() - cursor_col)) { 406 | cursor_col = col_max(); 407 | } else { 408 | cursor_col += param_int; 409 | } 410 | escape = ESCAPE_NONE; 411 | break; 412 | 413 | case 'D': /* CUB - Cursor Backward */ 414 | param_int = (param_used) ? atoi(param[0]) : 1; 415 | param_int = (param_int == 0) ? 1 : param_int; /* Convert zero to one. */ 416 | if (param_int > cursor_col) { 417 | cursor_col = 0; 418 | } else { 419 | cursor_col -= param_int; 420 | } 421 | escape = ESCAPE_NONE; 422 | break; 423 | 424 | case 'c': /* DA - Device Attributes */ 425 | eia_send(0x1B); 426 | eia_send('['); 427 | eia_send('?'); 428 | eia_send('1'); 429 | eia_send(';'); 430 | eia_send('0'); /* No options */ 431 | eia_send('c'); 432 | escape = ESCAPE_NONE; 433 | break; 434 | 435 | case 'g': /* TBC - Tabulation Clear */ 436 | param_int = (param_used) ? atoi(param[0]) : 0; 437 | if (param_int == 0) { 438 | tab_stop_clear(cursor_col); 439 | } else if (param_int == 3) { 440 | tab_stop_clear(-1); 441 | } 442 | escape = ESCAPE_NONE; 443 | break; 444 | 445 | case 'h': /* SM - Set Mode */ 446 | for (i = 0; i <= param_index; i++) { 447 | if ((param[i][0] == '?') && (param[i][1] == '1')) { 448 | mode_cursor_key_app = true; 449 | 450 | } else if ((param[i][0] == '?') && (param[i][1] == '3')) { 451 | mode_column_132 = true; 452 | erase_in_display(2); 453 | cursor_row = margin_top; 454 | cursor_col = 0; 455 | 456 | } else if ((param[i][0] == '?') && (param[i][1] == '4')) { 457 | mode_scrolling_smooth = true; 458 | 459 | } else if ((param[i][0] == '?') && (param[i][1] == '5')) { 460 | mode_screen_reverse = true; 461 | 462 | } else if ((param[i][0] == '?') && (param[i][1] == '6')) { 463 | mode_origin_relative = true; 464 | cursor_row = margin_top; 465 | cursor_col = 0; 466 | 467 | } else if ((param[i][0] == '?') && (param[i][1] == '7')) { 468 | mode_wraparound = true; 469 | 470 | } else if ((param[i][0] == '?') && (param[i][1] == '8')) { 471 | mode_auto_repeat = true; 472 | 473 | } else if ((param[i][0] == '?') && (param[i][1] == '9')) { 474 | mode_interlace = true; 475 | 476 | } else if ((param[i][0] == '2') && (param[i][1] == '0')) { 477 | mode_line_feed = true; 478 | } 479 | } 480 | escape = ESCAPE_NONE; 481 | break; 482 | 483 | case 'l': /* RM - Reset Mode */ 484 | for (i = 0; i <= param_index; i++) { 485 | if ((param[i][0] == '?') && (param[i][1] == '1')) { 486 | mode_cursor_key_app = false; 487 | 488 | } else if ((param[i][0] == '?') && (param[i][1] == '2')) { 489 | mode_ansi = false; 490 | 491 | } else if ((param[i][0] == '?') && (param[i][1] == '3')) { 492 | mode_column_132 = false; 493 | erase_in_display(2); 494 | cursor_row = margin_top; 495 | cursor_col = 0; 496 | 497 | } else if ((param[i][0] == '?') && (param[i][1] == '4')) { 498 | mode_scrolling_smooth = false; 499 | 500 | } else if ((param[i][0] == '?') && (param[i][1] == '5')) { 501 | mode_screen_reverse = false; 502 | 503 | } else if ((param[i][0] == '?') && (param[i][1] == '6')) { 504 | mode_origin_relative = false; 505 | cursor_row = margin_top; 506 | cursor_col = 0; 507 | 508 | } else if ((param[i][0] == '?') && (param[i][1] == '7')) { 509 | mode_wraparound = false; 510 | 511 | } else if ((param[i][0] == '?') && (param[i][1] == '8')) { 512 | mode_auto_repeat = false; 513 | 514 | } else if ((param[i][0] == '?') && (param[i][1] == '9')) { 515 | mode_interlace = false; 516 | 517 | } else if ((param[i][0] == '2') && (param[i][1] == '0')) { 518 | mode_line_feed = false; 519 | } 520 | } 521 | escape = ESCAPE_NONE; 522 | break; 523 | 524 | case 'm': /* SGR - Select Graphic Rendition */ 525 | for (i = 0; i <= param_index; i++) { 526 | if (param[i][0] == '0') { 527 | cursor_print_attribute = 0; 528 | } else if (param[i][0] == 0x00) { 529 | cursor_print_attribute = 0; 530 | } else if (param[i][0] == '1') { 531 | cursor_print_attribute |= (0x1 << TERMINAL_ATTRIBUTE_BOLD); 532 | } else if (param[i][0] == '4') { 533 | cursor_print_attribute |= (0x1 << TERMINAL_ATTRIBUTE_UNDERLINE); 534 | } else if (param[i][0] == '5') { 535 | cursor_print_attribute |= (0x1 << TERMINAL_ATTRIBUTE_BLINK); 536 | } else if (param[i][0] == '7') { 537 | cursor_print_attribute |= (0x1 << TERMINAL_ATTRIBUTE_REVERSE); 538 | } 539 | } 540 | escape = ESCAPE_NONE; 541 | break; 542 | 543 | case 'r': /* DECSTBM - Set Top and Bottom Margins */ 544 | margin_top = ((param_used) ? atoi(param[0]) : 1) - 1; 545 | margin_bottom = ((param_index > 0) ? atoi(param[1]) : row_max() + 1) - 1; 546 | margin_top = (margin_top < 0) ? 0 : margin_top; 547 | margin_bottom = (margin_bottom < 0) ? 0 : margin_bottom; 548 | cursor_row = margin_top; 549 | cursor_col = 0; 550 | escape = ESCAPE_NONE; 551 | break; 552 | 553 | case 'f': /* HVP - Horizontal and Vertical Position */ 554 | case 'H': /* CUP - Cursor Position */ 555 | cursor_row = ((param_used) ? atoi(param[0]) : 1) - 1; 556 | cursor_col = ((param_index > 0) ? atoi(param[1]) : 1) - 1; 557 | cursor_row = (cursor_row < 0) ? 0 : cursor_row; /* Negative to zero. */ 558 | cursor_col = (cursor_col < 0) ? 0 : cursor_col; /* Negative to zero. */ 559 | 560 | if (mode_origin_relative) { 561 | cursor_row += margin_top; /* Compensate for different origin. */ 562 | if (cursor_row < margin_top) { 563 | cursor_row = margin_top; 564 | } else if (cursor_row > margin_bottom) { 565 | cursor_row = margin_bottom; 566 | } 567 | } else { 568 | if (cursor_row < margin_top) { 569 | cursor_outside_scroll = true; 570 | } else if (cursor_row > margin_bottom) { 571 | cursor_outside_scroll = true; 572 | } 573 | } 574 | 575 | if (cursor_row > row_max()) { 576 | cursor_row = row_max(); 577 | } else if (cursor_row < 0) { 578 | cursor_row = 0; 579 | } 580 | if (cursor_col > col_max()) { 581 | cursor_col = col_max(); 582 | } else if (cursor_col < 0) { 583 | cursor_col = 0; 584 | } 585 | escape = ESCAPE_NONE; 586 | break; 587 | 588 | case 'J': /* ED - Erase In Display */ 589 | param_int = (param_used) ? atoi(param[0]) : 0; 590 | erase_in_display(param_int); 591 | escape = ESCAPE_NONE; 592 | break; 593 | 594 | case 'K': /* EL - Erase In Line */ 595 | param_int = (param_used) ? atoi(param[0]) : 0; 596 | erase_in_line(param_int); 597 | escape = ESCAPE_NONE; 598 | break; 599 | 600 | case ';': 601 | param_index++; 602 | if (param_index >= PARAM_MAX) { 603 | error_log("Overflow on parameter!\n"); 604 | param_index--; 605 | } 606 | break; 607 | 608 | /* Parameter */ 609 | case '0': 610 | case '1': 611 | case '2': 612 | case '3': 613 | case '4': 614 | case '5': 615 | case '6': 616 | case '7': 617 | case '8': 618 | case '9': 619 | case '?': 620 | param_append((char)byte); 621 | param_used = true; 622 | break; 623 | 624 | case 0x08: /* BS inside CSI sequence. */ 625 | if (cursor_col > 0) { 626 | cursor_col--; 627 | } 628 | break; 629 | 630 | case 0x0B: /* VT inside CSI sequence. */ 631 | if (cursor_row < row_max()) { 632 | cursor_row++; 633 | } 634 | break; 635 | 636 | case 0x0D: /* CR inside CSI sequence. */ 637 | cursor_col = 0; 638 | break; 639 | 640 | default: 641 | error_log("Unhandled CSI escape code: 0x%02x\n", byte); 642 | escape = ESCAPE_NONE; 643 | break; 644 | } 645 | } 646 | 647 | 648 | 649 | void terminal_handle_escape_hash(uint8_t byte) 650 | { 651 | switch (byte) { 652 | case '8': /* DECALN - Screen Alignment Display */ 653 | screen_alignment_display(); 654 | escape = ESCAPE_NONE; 655 | break; 656 | 657 | default: 658 | error_log("Unhandled hash escape code: 0x%02x\n", byte); 659 | escape = ESCAPE_NONE; 660 | break; 661 | } 662 | } 663 | 664 | 665 | 666 | void terminal_handle_escape(uint8_t byte) 667 | { 668 | if (escape == ESCAPE_CSI) { 669 | terminal_handle_escape_csi(byte); 670 | 671 | } else if (escape == ESCAPE_HASH) { 672 | terminal_handle_escape_hash(byte); 673 | 674 | } else if (escape == ESCAPE_G0_SET) { 675 | current_g0_set = byte; 676 | escape = ESCAPE_NONE; 677 | 678 | } else if (escape == ESCAPE_G1_SET) { 679 | current_g1_set = byte; 680 | escape = ESCAPE_NONE; 681 | 682 | } else { 683 | switch (byte) { 684 | case '[': 685 | escape = ESCAPE_CSI; 686 | param_reset(); 687 | break; 688 | 689 | case '#': 690 | escape = ESCAPE_HASH; 691 | param_reset(); 692 | break; 693 | 694 | case '(': 695 | escape = ESCAPE_G0_SET; 696 | break; 697 | 698 | case ')': 699 | escape = ESCAPE_G1_SET; 700 | break; 701 | 702 | case '=': /* DECKPAM - Keypad Application Mode */ 703 | mode_keypad_app = true; 704 | escape = ESCAPE_NONE; 705 | break; 706 | 707 | case '>': /* DECKPNM - Keypad Numeric Mode */ 708 | mode_keypad_app = false; 709 | escape = ESCAPE_NONE; 710 | break; 711 | 712 | case '<': /* VT52 - Enter ANSI Mode */ 713 | mode_ansi = true; 714 | escape = ESCAPE_NONE; 715 | break; 716 | 717 | case '7': /* DECSC - Save Cursor */ 718 | cursor_row = saved_row; 719 | cursor_col = saved_col; 720 | cursor_print_attribute = saved_print_attribute; 721 | escape = ESCAPE_NONE; 722 | break; 723 | 724 | case '8': /* DECRC - Restore Cursor */ 725 | saved_row = cursor_row; 726 | saved_col = cursor_col; 727 | saved_print_attribute = cursor_print_attribute; 728 | escape = ESCAPE_NONE; 729 | break; 730 | 731 | case 'D': /* IND - Index */ 732 | cursor_row++; 733 | escape = ESCAPE_NONE; 734 | break; 735 | 736 | case 'E': /* NEL - Next Line */ 737 | cursor_row++; 738 | cursor_col = 0; 739 | escape = ESCAPE_NONE; 740 | break; 741 | 742 | case 'H': /* HTS - Horizontal Tabulation Set */ 743 | tab_stop_set(cursor_col); 744 | escape = ESCAPE_NONE; 745 | break; 746 | 747 | case 'M': /* RI - Reverse Index */ 748 | cursor_row--; 749 | escape = ESCAPE_NONE; 750 | break; 751 | 752 | case 'c': /* RIS - Reset To Initial State */ 753 | terminal_init(); 754 | escape = ESCAPE_NONE; 755 | break; 756 | 757 | default: 758 | error_log("Unhandled escape code: 0x%02x\n", byte); 759 | escape = ESCAPE_NONE; 760 | break; 761 | } 762 | } 763 | } 764 | 765 | 766 | 767 | void terminal_handle_byte(uint8_t byte) 768 | { 769 | cursor_deactivate(); 770 | 771 | if (escape != ESCAPE_NONE) { 772 | terminal_handle_escape(byte); 773 | 774 | } else { 775 | switch (byte) { 776 | case 0x1B: /* ESC */ 777 | escape = ESCAPE_START; 778 | break; 779 | 780 | case 0x07: /* BEL */ 781 | /* Ringing the bell is not implemented. */ 782 | break; 783 | 784 | case 0x08: /* BS */ 785 | if (cursor_col > 0) { 786 | cursor_col--; 787 | } 788 | break; 789 | 790 | case 0x09: /* HT */ 791 | while (! tab_stop[cursor_col]) { 792 | cursor_col++; 793 | if (cursor_col > col_max()) { 794 | cursor_col = col_max(); 795 | break; 796 | } 797 | } 798 | break; 799 | 800 | case 0x0A: /* LF */ 801 | case 0x0B: /* VT */ 802 | case 0x0C: /* FF */ 803 | cursor_row++; 804 | if (mode_line_feed) { 805 | cursor_col = 0; 806 | } 807 | break; 808 | 809 | case 0x0D: /* CR */ 810 | cursor_col = 0; 811 | break; 812 | 813 | case 0x0E: /* SO */ 814 | /* Select G1 character set is not implemented. */ 815 | break; 816 | 817 | case 0x0F: /* SI */ 818 | /* Select G0 character set is not implemented. */ 819 | break; 820 | 821 | case 0x7F: /* DEL */ 822 | /* Ignored. */ 823 | break; 824 | 825 | default: 826 | print_char(byte); 827 | break; 828 | } 829 | } 830 | 831 | /* Handle scrolling. */ 832 | if (cursor_outside_scroll) { 833 | if (cursor_row >= margin_top && cursor_row <= margin_bottom) { 834 | cursor_outside_scroll = false; 835 | } 836 | } 837 | if (! cursor_outside_scroll) { 838 | if (cursor_row > margin_bottom) { 839 | scroll_up(); 840 | } else if (cursor_row < margin_top) { 841 | scroll_down(); 842 | } 843 | } 844 | 845 | cursor_activate(); 846 | } 847 | 848 | 849 | 850 | bool terminal_char_changed(uint8_t row, uint8_t col) 851 | { 852 | if ((screen[row][col].attribute >> TERMINAL_ATTRIBUTE_BLINK) & 0x1) { 853 | return true; 854 | } 855 | return screen_changed[row][col]; 856 | } 857 | 858 | 859 | 860 | terminal_char_t terminal_char_get(uint8_t row, uint8_t col) 861 | { 862 | terminal_char_t c; 863 | c.byte = '.'; 864 | c.attribute = 0; 865 | 866 | if (row > row_max()) { 867 | return c; 868 | } else if (col > col_max()) { 869 | return c; 870 | } else { 871 | screen_changed[row][col] = false; 872 | return screen_get(row, col); 873 | } 874 | } 875 | 876 | 877 | 878 | uint8_t terminal_cursor_key_code(void) 879 | { 880 | if (mode_ansi) { 881 | if (mode_cursor_key_app) { 882 | return 'O'; 883 | } else { 884 | return '['; 885 | } 886 | } else { 887 | return 0; 888 | } 889 | } 890 | 891 | 892 | 893 | bool terminal_send_crlf(void) 894 | { 895 | if (mode_line_feed) { 896 | return true; 897 | } else { 898 | return false; 899 | } 900 | } 901 | 902 | 903 | 904 | -------------------------------------------------------------------------------- /terminal.h: -------------------------------------------------------------------------------- 1 | #ifndef _TERMINAL_H 2 | #define _TERMINAL_H 3 | 4 | #include 5 | #include 6 | 7 | #define TERMINAL_ATTRIBUTE_BOLD 1 8 | #define TERMINAL_ATTRIBUTE_UNDERLINE 2 9 | #define TERMINAL_ATTRIBUTE_BLINK 3 10 | #define TERMINAL_ATTRIBUTE_REVERSE 4 11 | 12 | typedef struct terminal_char_s { 13 | uint8_t byte; 14 | uint8_t attribute; 15 | } terminal_char_t; 16 | 17 | void terminal_init(void); 18 | void terminal_handle_byte(uint8_t byte); 19 | terminal_char_t terminal_char_get(uint8_t row, uint8_t col); 20 | bool terminal_char_changed(uint8_t row, uint8_t col); 21 | uint8_t terminal_cursor_key_code(void); 22 | bool terminal_send_crlf(void); 23 | 24 | #endif /* _TERMINAL_H */ 25 | --------------------------------------------------------------------------------