From 761dbed67af5ed0c751b0787476c3c9417b4ff7e Mon Sep 17 00:00:00 2001 From: Zeal8bit Date: Tue, 28 Nov 2023 20:14:37 +0800 Subject: [PATCH] video: add a feature to upgrade the video board firmware via Recovery Fix a bug in the keyboard driver that resulted in corrupted NOR flash. On boot, if the video board is in recovery mode, then the bootloader will automatically run the flasher on the UART. Add a new entry in the menu to flash the video board firmware. The firmware needs to be sent via UART, just like NOR flash programming. --- Makefile | 4 +- include/config.asm | 7 +- include/stdout_h.asm | 29 ++- src/boot.asm | 4 +- src/keyboard.asm | 112 +++++++---- src/menu.asm | 172 +++++++++++----- src/pio.asm | 10 +- src/rst_vectors.asm | 2 +- src/systems.asm | 4 +- src/uart.asm | 7 +- src/video.asm | 65 ++++-- src/video_board.asm | 469 +++++++++++++++++++++++++++++++++++++++++++ 12 files changed, 753 insertions(+), 132 deletions(-) create mode 100644 src/video_board.asm diff --git a/Makefile b/Makefile index ea964ce..ea16e92 100644 --- a/Makefile +++ b/Makefile @@ -21,8 +21,8 @@ ifeq ($(CONFIG_ENABLE_TESTER),1) FILES += tester.asm endif -ifneq ($(CONFIG_UART_AS_STDOUT),1) - FILES += video.asm keyboard.asm +ifeq ($(CONFIG_ENABLE_VIDEO_BOARD),1) + FILES += video.asm video_board.asm keyboard.asm stdout.asm endif define FIND_ADDRESS = diff --git a/include/config.asm b/include/config.asm index a25ab1b..88f6fe2 100644 --- a/include/config.asm +++ b/include/config.asm @@ -5,9 +5,10 @@ IFNDEF CONFIG_H DEFINE CONFIG_H -; When set, the UART will be used as the standard output. When not set or set to 0, the video driver will -; be used. The UART driver will still be used to receive files. -DEFC CONFIG_UART_AS_STDOUT = 1 +; When set, the video board will be used as the default standard output. In that case, +; the UART will still be used for receiving files and sending some info messages. +; If not set, the UART driver will the standard output. +DEFC CONFIG_ENABLE_VIDEO_BOARD = 1 ; When set, the Zeal logo will be shown on screen, this takes more RAM at runtime but won't affect the ; OS or program running after the bootloader as the modified tiles are saved and restored. diff --git a/include/stdout_h.asm b/include/stdout_h.asm index 07cb928..62c7b03 100644 --- a/include/stdout_h.asm +++ b/include/stdout_h.asm @@ -8,7 +8,7 @@ DEFINE STDOUT_H ; It is possible to choose either the UART or video board as the standard output - IF CONFIG_UART_AS_STDOUT + IF !CONFIG_ENABLE_VIDEO_BOARD INCLUDE "uart_h.asm" DEFC stdout_initialize = uart_initialize @@ -35,21 +35,32 @@ DEFM 0x1b, "[0m" ENDM - ELSE ; !CONFIG_UART_AS_STDOUT + ELSE ; CONFIG_ENABLE_VIDEO_BOARD + ; In case we have the video board, it may be possible to switch back to + ; to the UART as standard output if the video board fails to boot. INCLUDE "video_h.asm" INCLUDE "keyboard_h.asm" + INCLUDE "uart_h.asm" - DEFC stdout_initialize = video_initialize + DEFC STDOUT_DRIVER_UART = 0 + DEFC STDOUT_DRIVER_VIDEO = 1 + + ; When the video board is enabled, we can fallback the standard input to UART + ; Parameters: + ; A - New driver to select, STDOUT_DRIVER_UART OR STDOUT_DRIVER_VIDEO + EXTERN std_set_driver + EXTERN stdout_initialize + EXTERN stdout_write + EXTERN stdout_put_char + EXTERN stdout_newline DEFC stdout_autoboot = video_autoboot - DEFC stdout_write = video_write - DEFC stdout_put_char = video_put_char - DEFC stdout_newline = video_newline - DEFC stdin_get_char = keyboard_next_char - DEFC stdin_has_char = keyboard_has_char - DEFC stdin_set_synchronous = keyboard_set_synchronous DEFC stdout_prepare_menu = video_clear_screen + EXTERN stdin_get_char + EXTERN stdin_has_char + EXTERN stdin_set_synchronous + ; Use a prefix for the colors MACRO YELLOW_COLOR _ DEFM 0xFE diff --git a/src/boot.asm b/src/boot.asm index 4e59857..38b72f8 100644 --- a/src/boot.asm +++ b/src/boot.asm @@ -36,10 +36,12 @@ bootloader_entry: ldir call sys_table_init - call stdout_initialize ; UART will be used to receive files in all cases call uart_initialize call pio_initialize + IF CONFIG_ENABLE_VIDEO_BOARD + call stdout_initialize + ENDIF ; CONFIG_ENABLE_VIDEO_BOARD ; Ready to send and receive data over UART, show welcome message ld hl, start_message diff --git a/src/keyboard.asm b/src/keyboard.asm index 84aa15b..7ba7c0d 100644 --- a/src/keyboard.asm +++ b/src/keyboard.asm @@ -9,68 +9,99 @@ SECTION BOOTLOADER - ; Receive a character from the keyboard (synchronous, blocking) - ; Parameters: - ; None + ; Receive a character from the keyboard, non-blocking version ; Returns: - ; A - ASCII byte received + ; A - 0 if no valid character was received, character value else ; Alters: - ; A, B, DE - PUBLIC keyboard_next_char -keyboard_next_char: - xor a - ld (received), a - - PUBLIC keyboard_get_char -keyboard_get_char: - ld de, received - -_keyboard_get_char_loop: - ld a, (de) + ; A, B, HL, DE + PUBLIC keyboard_get_char_nonblocking +keyboard_get_char_nonblocking: + ld hl, received + ld b, 0 + ; Atomic test and set + di + ld a, (hl) + ld (hl), b + ei or a - jr z, _keyboard_get_char_loop - ; Check if it's release key + ret z + ; Make HL point to the 'flag' variable + inc hl + ; Set a flag if release key cp KB_RELEASE_SCAN - jr z, _release_scan - ; Character is not a "release command" - ; Check if the character is a printable char - push hl + jr z, _release_scan_received + ; Check if we received a release scan previously + ld b, (hl) + ; Check if flag is 0 + inc b + dec b + jr nz, _flag_not_zero + ; Flag is not 0, we just received a real character, parse it + ; Check if it is a printable character cp KB_PRINTABLE_CNT - 1 jp nc, _special_code ; jp nc <=> A >= KB_PRINTABLE_CNT - 1 ; Save the scan size in B ld hl, base_scan ld b, base_scan_end - base_scan - jr get_from_scan + jr _get_from_scan _special_code: ; Special character is still in A - cp KB_EXTENDED_SCAN - jr z, _pop_ret - add -KB_SPECIAL_START + sub KB_EXTENDED_SCAN + ; Ignore extended scan characters for now, return 0 + ret z + ; We just subtracted KB_EXTENDED_SCAN, add it back + add KB_EXTENDED_SCAN-KB_SPECIAL_START ld hl, special_scan ld b, special_scan_end - special_scan -get_from_scan: +_get_from_scan: ; Check if there would be an overflow cp b - ; If there no carry, A is equal or bigger than the length - jr nc, _overflow_pop_continue + ; If there no carry, A is equal or bigger than the length, consider this + ; an invalid character, return 0 + jr nc, _overflow_ret_zero + ; Get the value of A from the scan table pointed by HL add l ld l, a adc h sub l ld h, a ld a, (hl) -_pop_ret: - pop hl ret -_overflow_pop_continue: - pop hl - jr _keyboard_get_char_loop +_overflow_ret_zero: + xor a + ret +_flag_not_zero: + ; Clear the flag and ignore the current character, return 0 + xor a + ; Fall-through +_release_scan_received: + ; Set the flag to a non-zero value + ld (hl), a + ; Return not-valid character + xor a + ret + -_release_scan: - ; Ignore the next character - call keyboard_next_char - ; Return the next character - jp keyboard_next_char + ; Receive a character from the keyboard (synchronous, blocking) + ; Parameters: + ; None + ; Returns: + ; A - ASCII byte received + ; Alters: + ; A, B, DE + PUBLIC keyboard_next_char +keyboard_next_char: + ; Clear the previously received keys + xor a + ld (received), a + push hl +_keyboard_next_char_loop: + call keyboard_get_char_nonblocking + or a + ; If character is invalid, continue the loop + jr z, _keyboard_next_char_loop + pop hl + ret base_scan: DEFB 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, '\t', '`', 0 @@ -120,4 +151,5 @@ keyboard_int_handler: SECTION BSS -received: DEFS 1 \ No newline at end of file +received: DEFS 1 +flag: DEFS 1 \ No newline at end of file diff --git a/src/menu.asm b/src/menu.asm index 80ca8f3..f5894ae 100644 --- a/src/menu.asm +++ b/src/menu.asm @@ -13,6 +13,8 @@ EXTERN newline EXTERN test_hardware EXTERN format_eeprom + EXTERN video_board_flash + EXTERN video_wait_recovery SECTION BOOTLOADER @@ -161,9 +163,9 @@ print_bc_hex: ld a, c jp print_a_hex - ; Print in hex the 8-bit value from A on the UART + ; Print, in hex, the 8-bit value in register A ; Parameters: - ; A - Value to print on the UART + ; A - Value to print on the standard output ; Returns: ; None ; Alters: @@ -219,10 +221,12 @@ process_menu_choice: jp z, menu_delete_entry cp 's' jp z, menu_save_new_entry - IF CONFIG_UART_AS_STDOUT cp 'b' jp z, menu_change_baudrate - ENDIF + IF CONFIG_ENABLE_VIDEO_BOARD + cp 'v' + jp z, menu_flash_video_board + ENDIF ; CONFIG_ENABLE_VIDEO_BOARD cp 'f' jp z, menu_flash_rom IF CONFIG_ENABLE_TESTER @@ -292,42 +296,11 @@ _menu_flash_rom_addr: ld e, d ld d, c push de -_menu_flash_ask_size: - ; Ask for the size to load now - PRINT_STR(binary_size_str) - ld hl, buffer - call menu_read_input - ld hl, buffer - call parse_hex +_menu_flash_ask_binary: + ; Receive the binary file (size will be asked) in RAM, at 0x80000 + call receive_binary or a - jr nz, _menu_flash_ask_size - ; Check that the size is not 0! - ld a, c - or d - or e - jr z, _menu_flash_ask_size - ; As we have 512KB of RAM, but 16KB are mapped currently for the bootloader, - ; make sure the given size doesn't exceed 512 - 16 = 496KB <=> CD <= 0x07c0 - ; Even though the flash is 256KB big, the hardware supports flash up to 512KB, - ; so keep the code like this for any NOR flash. - ld h, c - ld l, d - push de - ld de, 0x7c1 - xor a - sbc hl, de - pop de - jp nc, _menu_flash_ask_size - ; Print a message to send a file - push bc - push de - PRINT_STR(binary_send_str) - pop de - pop bc - ; Receive a file in RAM, at physical address 0x80000. - ; Parameters: - ; CDE - Size of the file to receive - call uart_receive_big_file + jr nz, _menu_flash_ask_binary ; Print a message saying flash is in progress push bc push de @@ -365,7 +338,115 @@ _menu_flash_rom_zero: ld d, a ld e, a push de - jr _menu_flash_ask_size + jr _menu_flash_ask_binary + + + IF CONFIG_ENABLE_VIDEO_BOARD + + ; Routine to flash the video board with a binary file. + ; This routine: + ; - Asks for a binary size on the standard input + ; - Asks for the recovery mode to be switched on + ; - Flashes the binary + ; - Waits for a hard reboot/power off + ; Routine called when "Flash the video board" is selected from the menu. + ; Parameters: + ; Z flag - This function is called from the menu, all the messages will be outputed. + ; NZ flag - This function is called from recovery, some messages won't be shown. + PUBLIC menu_flash_video_board +menu_flash_video_board: + push af + ; If called from recovery, do not show the "hexadecimal" warning notice + jr nz, _menu_flash_video_board_binary + PRINT_STR(new_entry_notice_str) + ; Ask for the size of the binary +_menu_flash_video_board_binary: + call receive_binary + or a + jr nz, _menu_flash_video_board_binary + ; Get back the flags from the stack but wait before testing them + pop af + push bc + push de + ; If we are already in the recovery, do not ask to switch + jr nz, _menu_flash_video_board_recovery + PRINT_STR(video_board_switch) + call video_wait_recovery +_menu_flash_video_board_recovery: + PRINT_STR_UART(flashing_flash_str) + pop de + pop bc + ; Flash the received file to the video board + ; Parameters: + ; CDE - Size of the data to flash + call video_board_flash + or a + ; Error if A is not 0 + jr z, _menu_flash_video_board_reboot + PRINT_STR_UART(flash_erase_error_str) +_menu_flash_video_board_reboot: + PRINT_STR_UART(video_board_power_off) +_menu_wait_reboot: + jr _menu_wait_reboot + + PUBLIC video_board_switch + PUBLIC video_board_switch_end +video_board_switch: DEFM "Press the recovery button on the video board to start flashing\r\n" +video_board_switch_end: +video_board_power_off: DEFM "Please power off the board\r\n" +video_board_power_off_end: + + ENDIF + + ; Helper routine to ask for a size between 1 and 496KB, print a message and receive a binary + ; Parameters: + ; None + ; Returns: + ; CDE - 24-bit parsed size + ; A - 0 on success, non-zero on failure + ; Alters: + ; A, BC, HL, DE, `buffer` +receive_binary: + PRINT_STR(binary_size_str) + ld hl, buffer + call menu_read_input + ld hl, buffer + call parse_hex + or a + ret nz + ; Check that the size is not 0! + ld a, c + or d + or e + jr z, _receive_binary_error + ; As we have 512KB of RAM, but 16KB are mapped currently for the bootloader, + ; make sure the given size doesn't exceed 512 - 16 = 496KB <=> CD <= 0x07c0 + ; Even though the flash is 256KB big, the hardware supports flash up to 512KB, + ; so keep the code like this for any NOR flash. + ld h, c + ld l, d + push de + ld de, 0x7c1 + xor a + sbc hl, de + pop de + jr nc, _receive_binary_error + ; Success, print a message and return 0 + ; Print a message to send a file + push bc + push de + PRINT_STR(binary_send_str) + pop de + pop bc + ; Receive a file in RAM, at physical address 0x80000. + ; Parameters: + ; CDE - Size of the file to receive + ; Returns: + ; A - 0 (always) + jp uart_receive_big_file +_receive_binary_error: + inc a + ret ; Routine called when "Change baudrate" is selected. This is useful even if the @@ -816,7 +897,7 @@ flash_erase_success_str: flash_erase_success_str_end: flash_erase_error_str: RED_COLOR() - DEFM "Error while writing to flash" + DEFM "Error while flashing binary" END_COLOR() flash_erase_error_str_end: system_table_full_str: @@ -878,12 +959,11 @@ advanced_msg: DEFM "a - Add a new entry\r\n" DEFM "d - Delete an existing entry\r\n" DEFM "s - Save configuration to flash\r\n" - IF CONFIG_UART_AS_STDOUT - DEFM "b - Change baudrate\r\n" - ELSE ; VIDEO as stdout - DEFM "b - Change UART baudrate (for sending files)\r\n" - ENDIF ; CONFIG_UART_AS_STDOUT + DEFM "b - Change UART baudrate\r\n" DEFM "f - Flash/Program the ROM\r\n" + IF CONFIG_ENABLE_VIDEO_BOARD + DEFM "v - Flash the video board\r\n" + ENDIF DEFM "q - Quick format I2C EEPROM (ZealFS)\r\n" IF CONFIG_ENABLE_TESTER DEFM "t - Test hardware\r\n" diff --git a/src/pio.asm b/src/pio.asm index 7c78823..e7b5b07 100644 --- a/src/pio.asm +++ b/src/pio.asm @@ -5,13 +5,13 @@ INCLUDE "config.asm" INCLUDE "pio_h.asm" - IF CONFIG_UART_AS_STDOUT - DEFC IO_PIO_SYSTEM_INT_MASK = ~(1 << IO_UART_RX_PIN) & 0xff - DEFC int_handler = uart_int_handler - ELSE ; !CONFIG_UART_AS_STDOUT + IF CONFIG_ENABLE_VIDEO_BOARD DEFC IO_PIO_SYSTEM_INT_MASK = ~(1 << IO_KEYBOARD_PIN) & 0xff DEFC int_handler = keyboard_int_handler - ENDIF ; CONFIG_UART_AS_STDOUT + ELSE ; !CONFIG_ENABLE_VIDEO_BOARD + DEFC IO_PIO_SYSTEM_INT_MASK = ~(1 << IO_UART_RX_PIN) & 0xff + DEFC int_handler = uart_int_handler + ENDIF ; CONFIG_ENABLE_VIDEO_BOARD EXTERN int_handlers_table EXTERN uart_int_handler diff --git a/src/rst_vectors.asm b/src/rst_vectors.asm index 993e690..4c80cb9 100644 --- a/src/rst_vectors.asm +++ b/src/rst_vectors.asm @@ -93,7 +93,7 @@ int_handlers_table: ; Section containing the list of systems registered that we can boot ; It must be aligned on 4KB as the SST39SF0x0 NOR Flashes can only erase ; sectors of 4KB. - ; Thus the sections above (code) must not be bigger than 4KB. If more space + ; Thus the sections above (code) must not be bigger than 8KB. If more space ; is needed, move this section further and adapt the code. SECTION SYS_TABLE ORG 0x2000 \ No newline at end of file diff --git a/src/systems.asm b/src/systems.asm index 13b301d..71e09cd 100644 --- a/src/systems.asm +++ b/src/systems.asm @@ -71,7 +71,7 @@ _sys_table_get_first_loop: ; No return PUBLIC sys_table_boot_entry sys_table_boot_entry: - IF !CONFIG_UART_AS_STDOUT + IF CONFIG_ENABLE_VIDEO_BOARD call video_unload_assets ENDIF ; Jump to the physical address directly @@ -231,7 +231,7 @@ _sys_table_get_first_empty_found: ; - PUBLIC sys_boot_from_ram sys_boot_from_ram: - IF !CONFIG_UART_AS_STDOUT + IF CONFIG_ENABLE_VIDEO_BOARD call video_unload_assets ENDIF ; The user's program is in RAM page 0 (0x80000) diff --git a/src/uart.asm b/src/uart.asm index c55714e..d19d2ce 100644 --- a/src/uart.asm +++ b/src/uart.asm @@ -34,8 +34,6 @@ uart_initialize: ret - IF CONFIG_UART_AS_STDOUT - ; Function called everytime the menu is about to be shown, do nothing in the case of the UART. PUBLIC uart_clear_screen uart_clear_screen: @@ -216,8 +214,6 @@ _uart_dequeue_available: pop hl ret - ENDIF ; CONFIG_UART_AS_STDOUT - PUBLIC uart_disable_fifo uart_disable_fifo: @@ -374,7 +370,7 @@ _uart_receive_next_byte: ; Parameters: ; CDE - Size of the file to receive (maximum 496KB) ; Returns: - ; None + ; A - 0 ; Alters: ; A PUBLIC uart_receive_big_file @@ -436,6 +432,7 @@ uart_receive_big_file_end: pop de pop bc pop hl + xor a ret diff --git a/src/video.asm b/src/video.asm index 324949b..6c2c114 100644 --- a/src/video.asm +++ b/src/video.asm @@ -4,15 +4,17 @@ INCLUDE "config.asm" INCLUDE "mmu_h.asm" - INCLUDE "uart_h.asm" + INCLUDE "stdout_h.asm" INCLUDE "pio_h.asm" INCLUDE "video_h.asm" INCLUDE "keyboard_h.asm" SECTION BOOTLOADER - EXTERN keyboard_has_char - EXTERN keyboard_get_char + EXTERN keyboard_get_char_nonblocking + EXTERN pio_disable_interrupt + EXTERN video_is_in_recovery + EXTERN menu_flash_video_board DEFC DEFAULT_VIDEO_MODE = VID_MODE_TEXT_640 DEFC DEFAULT_CURSOR_BLINK = 30 @@ -24,7 +26,6 @@ ENDM - ; Initialize the video card ; Parameters: ; None @@ -34,6 +35,10 @@ ; A PUBLIC video_initialize video_initialize: + ; If the video board is in recovery mode do not initialize anything + call video_is_in_recovery + jr z, _video_initialize_switch + ; Map the text controller to the banked I/O xor a out (IO_MAPPER_BANK), a @@ -69,6 +74,13 @@ video_initialize: out (IO_TEXT_CTRL_REG), a ret + ; Recovery mode, switch to UART mode +_video_initialize_switch: + ; Disable interrupts on the PIO + call pio_disable_interrupt + ld a, STDOUT_DRIVER_UART + jp std_set_driver + ; Print the given bytes on screen ; Parameters: @@ -101,32 +113,34 @@ _video_write_loop: ; Routine showing the autoboot message and waiting for a keypress. + ; If the video board is in recovery mode, propose to flash it, this function won't + ; return in that case. ; Returns: ; A - 0 if autoboot, 1 if key pressed PUBLIC video_autoboot video_autoboot: - ; Print the autoboot message + ; Check if the video board is in recovery mode + call video_is_in_recovery + jr z, video_recovery_flash + ; Not in recovery mode, continue booting normally, print the autoboot message ld hl, boot_message ld bc, boot_message_end - boot_message call video_write - ASSERT(CONFIG_AUTOBOOT_DELAY_SECONDS * 60 < 0x10000) + ASSERT(CONFIG_AUTOBOOT_DELAY_SECONDS * 59 < 0x10000) ; Wait CONFIG_AUTOBOOT_DELAY_SECONDS seconds while checking keyboard input - ; We will wait (CONFIG_AUTOBOOT_DELAY_SECONDS * 60) v-blanks since the refresh rate is 60Hz - ; TODO: Use the V-blank interrupts - ld hl, CONFIG_AUTOBOOT_DELAY_SECONDS * 60 + ; We will wait (CONFIG_AUTOBOOT_DELAY_SECONDS * 59) v-blanks since the refresh rate is 60Hz. + ; Keep one less v-blank to compensate the instructions we are executing. + ld hl, CONFIG_AUTOBOOT_DELAY_SECONDS * 59 ; Save the previous control status register in C ; This will let us check the edge and not the level of the v-blank xor a _video_autoboot_loop: ld c, a ; previous ctrl status in C - ; Check if any character is ready - ; Preserves HL and C - call keyboard_has_char - or a - jr z, _video_autoboot_loop_continue - ; A character was received on the keyboard, check if it's the ESC key - ; Preserves HL and C - call keyboard_get_char + ; Preserves C + push hl + call keyboard_get_char_nonblocking + pop hl + ; If A was not valid, it won't be equal to KB_ESC, so no need to check validity sub KB_ESC ; If it's not the ESC key, ignore it and continue the loop jr nz, _video_autoboot_loop_continue @@ -157,6 +171,22 @@ _video_autoboot_loop_continue: boot_message: DEFM "Booting...\n\nPress ESC key to enter menu" boot_message_end: + ; Function called when the video mode is in recovery mode, the user + ; will be asked to send a binary to flash. + ; Never returns +video_recovery_flash: + ; The UART interrupts are already disabled because...never initialized + PRINT_STR_UART(recovery_detected) + ; Clear the Z flag before jumping to this routine to mark that we are already in + ; recovery mode + or 1 + jp menu_flash_video_board + + +recovery_detected: + DEFM "Recovery mode detected on the video board, switched to UART mode\r\n" + DEFM 0x1b, "[1;33mTo flash the video board firmware, please specify the size in hexadecimal:", 0x1b, "[0m\r\n" +recovery_detected_end: ; Print a single byte on the screen ; Parameters: @@ -294,7 +324,6 @@ video_clear_screen: ld a, DEFAULT_CURSOR_BLINK out (IO_TEXT_CURS_TIME), a - ; Save the current mapping MMU_GET_PAGE_NUMBER(MMU_PAGE_1) push af diff --git a/src/video_board.asm b/src/video_board.asm new file mode 100644 index 0000000..cd3f044 --- /dev/null +++ b/src/video_board.asm @@ -0,0 +1,469 @@ + +; SPDX-FileCopyrightText: 2023 Zeal 8-bit Computer +; +; SPDX-License-Identifier: Apache-2.0 + INCLUDE "mmu_h.asm" + INCLUDE "uart_h.asm" + INCLUDE "stdout_h.asm" + + SECTION BOOTLOADER + + DEFC CPU_FREQ = 10000000 + + ; SPI Flash protocol related constants + DEFC SPI_READ_STA_CMD = 0x05 + DEFC SPI_WRITE_STA_CMD = 0x01 + + DEFC SPI_PAGE_PROG_CMD = 0x02 + + DEFC SPI_WRITE_ENA_CMD = 0x06 + DEFC SPI_WRITE_DIS_CMD = 0x04 + + DEFC SPI_PWR_DOWN_CMD = 0xb9 + DEFC SPI_ERASE_64KB_CMD = 0xd8 + + ; Version register + DEFC VID_VER_REV = 0x80 + DEFC VID_VER_MIN = 0x81 + DEFC VID_VER_MAJ = 0x82 + + ; Features registers + DEFC VID_MAP_DEVICE = 0x8e + DEFC VID_SPI_DEVICE = 0x1 + ; LED control register + DEFC LED_CTRL_REG = 0x8d + DEFC LED_CTRL_OFF = 0 + DEFC LED_CTRL_BLK_SYNC = 1 + DEFC LED_CTRL_BLK_ASYN = 2 + DEFC LED_CTRL_ON = 3 + + + ; SPI controller-related contants + DEFC SPI_REG_BASE = 0xa0 + DEFC REG_VERSION = (SPI_REG_BASE + 0) + DEFC REG_CTRL = (SPI_REG_BASE + 1) + DEFC REG_CTRL_START = 1 << 7 ; Start SPI transaction + DEFC REG_CTRL_RESET = 1 << 6 ; Reset the SPI controller + DEFC REG_CTRL_CS_START = 1 << 5 ; Assert chip select (low) + DEFC REG_CTRL_CS_END = 1 << 4 ; De-assert chip select signal (high) + DEFC REG_CTRL_CS_SEL = 1 << 3 ; Select among two chip selects (0 and 1) + DEFC REG_CTRL_RSV2 = 1 << 2 + DEFC REG_CTRL_RSV1 = 1 << 1 + DEFC REG_CTRL_STATE = 1 << 0 ; SPI controller in IDLE state (no transaction) + DEFC REG_CLK_DIV = (SPI_REG_BASE + 2) + DEFC REG_RAM_LEN = (SPI_REG_BASE + 3) + DEFC REG_CHECKSUM = (SPI_REG_BASE + 4) + ; ... ; + DEFC REG_RAM_FIFO = (SPI_REG_BASE + 7) + DEFC REG_RAM_FROM = (SPI_REG_BASE + 8) + DEFC REG_RAM_TO = (SPI_REG_BASE + 15) + + ; Calculate the contanst to start the transfer and assert Chip-Select + DEFC SPI_START_TRANSFER = REG_CTRL_START | REG_CTRL_CS_START | REG_CTRL_CS_SEL + DEFC SPI_END_TRANSFER = REG_CTRL_CS_END | REG_CTRL_CS_SEL + + + EXTERN video_board_switch + EXTERN video_board_switch_end + + ; Check if the video board is in recovery mode + ; Parameters: + ; None + ; Returns: + ; A - 0 if in recovery mode, positive value else + ; Alters: + ; A + PUBLIC video_is_in_recovery +video_is_in_recovery: + in a, (VID_VER_REV) + sub 0xde + ret nz + in a, (VID_VER_MIN) + cp 0xad + ret nz + in a, (VID_VER_MAJ) + cp 0x23 + ret + + + ; Wait for the recovery mode to be entered by the video board + ; Parameters: + ; None + PUBLIC video_wait_recovery +video_wait_recovery: + ; TODO: refactor to make this cleaner instead of referencing labels from a + ; different file + PRINT_STR_UART(video_board_switch) + ; Wait for 500 milliseconds + ld de, 500 + call sleep_ms + ; Check if we are in the recovery mode + call video_is_in_recovery + ret z + jr video_wait_recovery + + ; Flash the NOR Flash located on the video board through SPI. + ; Parameters: + ; RAM[0x80000]: Data to flash at offset 0 of the flash + ; CDE: 24-bit size of the data + ; Returns: + ; A - 0 on success, error code else + PUBLIC video_board_flash +video_board_flash: + ; To signify a flash is in progress, make the LEDs blink at the same time + ld a, LED_CTRL_BLK_SYNC + out (LED_CTRL_REG), a + ; Enable the SPI component + ld a, VID_SPI_DEVICE + out (VID_MAP_DEVICE), a + ; Configure the SPI clock to 25MHz + ld a, 2 + out (REG_CLK_DIV), a + ; Even though the NOR Flash may support erasing smaller sectors than 64KB ones, + ; let's keep it like this to support as many flashes as possible. + ; Calculate the number of sectors to erase: round_up(CDE/64KB) + ; This is equivalent o C + ((D | E) ? 1 : 0) + ld a, d + or e + ; Pre-load A since it doesn't alter the flags + ld a, c + jr z, _no_inc + inc a +_no_inc: + ; A contains the number of sectors to erase, erase them + call video_board_erase_A_sectors + ; We have to program the data as pages, each page is 256 bytes, so we have + ; CD pages in total. Let's forget about the "incomplete" page for now + push de + ; Store number of pages in BC + ld b, c + ld c, d + ; Store in H the virtual address highest byte + ld h, 0x40 + ; Store in DE the upper 16-bit of physical address + ld de, 0 + ; Map the pages 1 and 2 to physical address 0x80000 + ld a, 0x80000 >> 14 + out (MMU_PAGE_1), a + inc a + out (MMU_PAGE_2), a + inc a + ; Store the next pages to map in L + ld l, a + ; Check if we have any 256-byte page to program + ld a, b + or c + jr z, _no_full_pages +_program_page: + ; Iterate over BC pages + push hl + push de + push bc + call video_board_program_page + pop bc + pop de + pop hl + ; Increment the virtual page, if it reaches (0xC), we have already sent 32KB + ; Go back to 0x40 + inc h + ld a, 0xc0 + cp h + jr nz, _program_page_no_overflow + ; Map the next pages too! + ld a, l + out (MMU_PAGE_1), a + inc a + out (MMU_PAGE_2), a + inc a + ; Store the next pages in L again + ld l, a + ld h, 0x40 +_program_page_no_overflow: + ; In all cases, increment the physical address by 256 + inc de + ; Decrement the total number of pages to write + dec bc + ld a, b + or c + jr nz, _program_page +_no_full_pages: + ; Program the remaining bytes + pop bc + ; A is zero, test C directly + or c + jr z, _video_board_flash_disable_write + ; Program remaining bytes (less than 256), H contains the virtual address MSB + ; A contains the number of bytes to send + dec a ; Decrement as requested by the routine below + call video_board_program_A_bytes +_video_board_flash_disable_write: + ; Issue a WRITE-DISABLE command to the SPI flash + ld a, SPI_WRITE_DIS_CMD + call spi_start_single_byte_command + ; Save returned value in B + ld b, a + ; Notify that the flash is finished by turning LEDs on + ld a, LED_CTRL_ON + out (LED_CTRL_REG), a + ; Restore returned value + ld a, b + ret + + + ; Erase A sectors starting at 0x000000 on the flash + ; Parameters: + ; A - Number of sectors to erase (guaranteed not 0) + ; Returns: + ; None + ; Alters: + ; A +video_board_erase_A_sectors: + push de + push bc + ; Iterate over all the sectors + ld b, a + ; Prepare the hardware buffer with the lowest 16-bit bytes of sector address + xor a + ; Second and third address bytes (always 0) + out (REG_RAM_FROM + 2), a + out (REG_RAM_FROM + 3), a +_erase_sector: + call spi_start_write_enable_cmd + ; Erase command + ld a, SPI_ERASE_64KB_CMD + out (REG_RAM_FROM), a + ; Sector address highest byte (count - 1) + ld a, b + dec a + out (REG_RAM_FROM + 1), a + ; Set the HW buffer length: command + 3 bytes = 4 bytes + ld a, 4 + call spi_start_transfer + ; Save BC since B contains the erase sector count + push bc +_erase_sector_wait_status: + ; The erase command has been started, it is going to take a while, not less than + ; 75ms. Let's not flood the SPI bus with STATUS commands, and stall for a while + ld de, 75 + call sleep_ms + ; Wait for completion + call spi_get_status + rrca + jr c, _erase_sector_wait_status + ; Sector is erased, continue! + pop bc + djnz _erase_sector + ; Restore original BC and DE and return + pop bc + pop de + ret + + + ; Program BC pages into the video board flash. + ; Parameters: + ; H - Upper 8-bit of the virtual address + ; DE - Upper 16-bit of the physical address to write + ; Returns: + ; None + ; Alters: + ; A, BC, DE, HL +video_board_program_page: + ld a, 0xff + ; Fall-through + + ; Program the last page, which is strictly smaller than 256 bytes + ; Parameters: + ; H - Upper 8-bit of the virtual address + ; DE - Upper 16-bit of the physical address to write + ; A - Number of bytes to write MINUS 1! + ; 0x8000-0xC000 - mapped to the actual data to write + ; Returns: + ; A - 0 on success, postiive value else +video_board_program_A_bytes: + ; Save the number of bytes to flash in register B + ld b, a + ; Issue a write-enable command + call spi_start_write_enable_cmd + call spi_start_write_page_command + ; Send the 256 bytes, use HL as the virtual address + ld l, 0 + ; We can now re-use DE registers + ; Store (number_of_bytes % 8) in D + ld a, b + inc a + and 0x7 ; <=> (B+1) % 8 + ld d, a + ; Let E store the number of loops to perform by dividing number_of_bytes by 8 + ld a, b + add 1 ; cannot use `inc a` since we need the carry in case of overflow! + rra + rra + rra + ; Remove the uppest two bits, since we counted CY as the 8th bit + and 0x3f + ; If A is 0, we have less than 8 bytes to write + jr z, _program_less_than_8 + ld e, a + call spi_write_E_pages_8_bytes +_program_less_than_8: + ; Number of bytes to transfer is in D (less than 8) + ; if it is 0, we can return already + ld a, d + or a + jr z, spi_end_transfer_wait_write + ; We still have some bytes to write, let's do this, reset the FIFO + or 0x80 ; clear pseudo-FIFO indexes + out (REG_RAM_LEN), a + ; Store number of bytes to transfer in B, as required by otir instruction + ld b, d + ld c, REG_RAM_FIFO + ; Load the bytes in the hardware RAM thanks to the pseudo-FIFO register + otir + ; Start the transfer + ld a, SPI_START_TRANSFER + out (REG_CTRL), a + ; Wait for the SPI Idle state, the SPI flash has NOT started writing, so no + ; need to wait for it for now + call spi_wait_idle + jp spi_end_transfer_wait_write + + + ; Start an SPI command by asserting the CS low and send the physical page address + ; Parameters: + ; DE - Physical address to write +spi_start_write_page_command: + ; Send the command and the address to write + ld a, SPI_PAGE_PROG_CMD + out (REG_RAM_FROM + 0), a + ; Highest bytes + ld a, d + out (REG_RAM_FROM + 1), a + ld a, e + out (REG_RAM_FROM + 2), a + ; 0 as the lowest byte + xor a + out (REG_RAM_FROM + 3), a + ; Set the RAM length to 4 + ld a, 4 + out (REG_RAM_LEN), a + ; Start the SPI transfer + ld a, SPI_START_TRANSFER + out (REG_CTRL), a + ret + + + ; Start an SPI transfer that writes E pages of 8 bytes + ; Alters: + ; A, BC, E, HL +spi_write_E_pages_8_bytes: + ; RAM Length is never modified by the hardware, so it is persistent. + ; Let's use the pseudo-FIFO mode, reset the indexes by setting the highest bit. + ld a, 0x88 + out (REG_RAM_LEN), a + ; C will contain the address of the register + ld c, REG_RAM_FIFO +_spi_write_E_pages_8_bytes_next_batch: + ; B must contain the number of bytes to transfer (8) + ld b, 8 + ; Load the bytes in the hardware RAM thanks to the pseudo-FIFO register + otir + ld a, SPI_START_TRANSFER + out (REG_CTRL), a + ; Wait for the SPI Idle state, the SPI flash has NOT started writing, so no + ; need to wait for it for now + call spi_wait_idle + ; Continue the loop to transfer the rest of the bytes + dec e + jr nz, _spi_write_E_pages_8_bytes_next_batch + ret + + + ; Wait for the SPI controller to go to Idle state + ; Alters: + ; A +spi_wait_idle: + in a, (REG_CTRL) + rrca ; Check bit 0, must be 0 too + ret nc + jr spi_wait_idle + + + ; End the SPI (write) transfer and wait for the transaction to terminate on the flash end + ; Returns: + ; A - 0 (Success) +spi_end_transfer_wait_write: + ld a, SPI_END_TRANSFER + out (REG_CTRL), a +_spi_end_transfer_wait_write_loop: + call spi_get_status + rrca + ret nc + jr _spi_end_transfer_wait_write_loop + + ; Get the current status of the SPI flash +spi_get_status: + ; Issue a status command + ld a, SPI_READ_STA_CMD + out (REG_RAM_FROM), a + ; Provide a 2-byte size since we need to read a byte (the second byte will be discarded) + ld a, 2 + call spi_start_transfer + ; The transaction is finished since `spi_start_transfer` waits for idle state + ; Read the status reply from the flash + in a, (REG_RAM_FROM + 1) + ; Lowest bit is 1 if write is in progress, 0 else + ret + + + ; Issue a write-enable command +spi_start_write_enable_cmd: + ld a, SPI_WRITE_ENA_CMD + ; Fall-through + + ; Start a sungle byte command + ; Parameters: + ; A - Command to send +spi_start_single_byte_command: + out (REG_RAM_FROM), a + ld a, 1 + ; Fall-through + + ; Start an SPI transfer by setting the buffer length and asserting the chip select + ; Parameters: + ; A - Length of the HW buffer + ; Returns: + ; A - 0 (Success) +spi_start_transfer: + out (REG_RAM_LEN), a + ; Start the command and assert chip select at the same time + ld a, SPI_START_TRANSFER + out (REG_CTRL), a + ; Wait for the transaction to be finished + call spi_wait_idle + ; De-assert the chip-select + ld a, SPI_END_TRANSFER + out (REG_CTRL), a + ; Success, return 0 + xor a + ret + + + ; Sleep for a given amount of milliseconds + ; Parameters: + ; DE - Number of milliseconds to sleep for + ; Returns: + ; None +sleep_ms: + ld bc, CPU_FREQ / 1000 / 24 +_sleep_ms_waste: + ; 24 T-states for the following, until 'jp nz, _sleep_ms_waste' + dec bc + ld a, b + or c + jp nz, _sleep_ms_waste + ; If we are here, a milliseconds has elapsed + dec de + ld a, d + or e + jp nz, sleep_ms + ret