From dd4e970bd9db1a9d29ac947bcb4bdce0a5ca25fb Mon Sep 17 00:00:00 2001 From: rweather Date: Tue, 8 Aug 2023 01:47:13 +1000 Subject: [PATCH] Port to Ben Eater's 6502 Breadboard Computer (#149) * Port to Ben Eater's 6502 Breadboard Computer * Make "eater" complete and hosted * Get the examples to compile and run. * Define missing abort() function. * Move delay() from libcrt0 to libc. * Define getchar() and __putchar() in C. * Convert CR into LF in getchar(). * Rename putchar_raw() to __chrout() for consistency with other platforms. * Rename getchar_raw() to __chrin(). * Improvements to the "eater" target * Add include guards to chrin.h and chrout.h * Add -mlto-zp option to clang.cfg * Remove unnecessary .data_initializers section from link.ld --- README.md | 2 + mos-platform/CMakeLists.txt | 1 + mos-platform/eater/CMakeLists.txt | 39 ++++++++++ mos-platform/eater/README.md | 83 +++++++++++++++++++++ mos-platform/eater/abort.c | 7 ++ mos-platform/eater/chrin.h | 18 +++++ mos-platform/eater/chrout.h | 14 ++++ mos-platform/eater/clang.cfg | 3 + mos-platform/eater/crt0/reset.S | 38 ++++++++++ mos-platform/eater/crt0/serial.S | 115 ++++++++++++++++++++++++++++++ mos-platform/eater/crt0/systick.S | 74 +++++++++++++++++++ mos-platform/eater/delay.c | 18 +++++ mos-platform/eater/eater.h | 18 +++++ mos-platform/eater/getchar.c | 9 +++ mos-platform/eater/link.ld | 47 ++++++++++++ mos-platform/eater/putchar.c | 8 +++ 16 files changed, 494 insertions(+) create mode 100644 mos-platform/eater/CMakeLists.txt create mode 100644 mos-platform/eater/README.md create mode 100644 mos-platform/eater/abort.c create mode 100644 mos-platform/eater/chrin.h create mode 100644 mos-platform/eater/chrout.h create mode 100644 mos-platform/eater/clang.cfg create mode 100644 mos-platform/eater/crt0/reset.S create mode 100644 mos-platform/eater/crt0/serial.S create mode 100644 mos-platform/eater/crt0/systick.S create mode 100644 mos-platform/eater/delay.c create mode 100644 mos-platform/eater/eater.h create mode 100644 mos-platform/eater/getchar.c create mode 100644 mos-platform/eater/link.ld create mode 100644 mos-platform/eater/putchar.c diff --git a/README.md b/README.md index 4501838f7..fd9dbe50e 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ The LLVM-MOS compiler toolchain and platform libraries. - Atari 8-bit - XEX file - 8-KiB or 16-KiB standard cartridge +- [Ben Eater's Breadboard 6502 Computer](https://eater.net/6502) - [Commander X16](https://www.commanderx16.com/) - Commodore 64 - Commodore PET @@ -123,6 +124,7 @@ executables and libraries for that target. | -------------------------------- | --------------------- | | Atari 8-bit (.XEX) | `mos-atari8-clang` | | Atari 8-bit (Standard cartridge) | `mos-atari8-stdcart` | +| Ben Eater's 6502 Breadboard Kit | `mos-eater-clang` | | Commander X16 | `mos-cx16-clang` | | Commodore 64 | `mos-c64-clang` | | Commodore PET | `mos-pet-clang` | diff --git a/mos-platform/CMakeLists.txt b/mos-platform/CMakeLists.txt index 54ddfeaeb..4742675a0 100644 --- a/mos-platform/CMakeLists.txt +++ b/mos-platform/CMakeLists.txt @@ -52,6 +52,7 @@ add_subdirectory(commodore) add_subdirectory(c64) add_subdirectory(cpm65) add_subdirectory(cx16) +add_subdirectory(eater) add_subdirectory(mega65) add_subdirectory(sim) add_subdirectory(nes) diff --git a/mos-platform/eater/CMakeLists.txt b/mos-platform/eater/CMakeLists.txt new file mode 100644 index 000000000..313efe443 --- /dev/null +++ b/mos-platform/eater/CMakeLists.txt @@ -0,0 +1,39 @@ +platform(eater COMPLETE HOSTED PARENT common) + +if(NOT CMAKE_CROSSCOMPILING) + return() +endif() + +install(FILES + chrin.h + chrout.h + eater.h +TYPE INCLUDE) +install(FILES link.ld TYPE LIB) + +add_platform_library(eater-crt0 + crt0/reset.S + crt0/serial.S + crt0/systick.S +) +merge_libraries(eater-crt0 + common-crt0 + common-init-stack + common-copy-zp-data + common-zero-bss + common-exit-loop +) + +add_platform_library(eater-c + abort.c + delay.c + getchar.c + putchar.c +) + +target_compile_options(eater-crt0 PUBLIC -mcpu=mosw65c02) +target_link_libraries(eater-crt0 PRIVATE common-asminc) + +target_include_directories(eater-c BEFORE PUBLIC .) +target_compile_options(eater-c PUBLIC -mcpu=mosw65c02) +target_link_libraries(eater-c PRIVATE common-asminc) diff --git a/mos-platform/eater/README.md b/mos-platform/eater/README.md new file mode 100644 index 000000000..d3614264b --- /dev/null +++ b/mos-platform/eater/README.md @@ -0,0 +1,83 @@ + +This directory contains support for [Ben Eater's Breadboard 6502](https://eater.net/6502) project. + +Features of the port +-------------------- + +This port has the following features: + +* .text and .rodata segments are placed into ROM, between $8000 and $FFFF. +* .data and .bss segments are placed into RAM, between $0200 and $3FFF. +* The C stack grows downwards from $4000. +* LLVM registers are stored between $E0 and $FF in the zero page. +* Support for serial input and output via the W65C51N Asynchronous +Communications Interface Adapter (ACIA). +* System millisecond tick counter based on the T2 timer in the +W65C22 Versatile Interface Adapter (VIA). + +Serial console +-------------- + +The W65C51N Asynchronous Communications Interface Adapter (ACIA) +at address $5000 in the breadboard's memory map is used to implement the +\_\_putchar() and getchar() functions for the C library. + +The ACIA is configured for 19200 bps N-8-1 communications and RTS +handshaking. The handshaking lines on the W65C51N should be hooked +up as follows: + +* Connect pin 8 (RTS) of the W65C51N to pin 10 (T2IN) of the MAX232. +* Connect pin 7 (T2OUT) of the MAX232 to pin 8 (CTS) on the DB9 connector. +* Connect pins 9 (CTS), 16 (DCD), and 17 (DSR) of the W65C51N to ground. +* Everything else should be connected up the same way as in +[Ben Eater's standard schematic](https://eater.net/schematics/6502-serial.png), +including the diodes D1 and D2 on the IRQ lines for the chips. + +Serial output uses busy-waiting after transmitting each character, +due to bugs in the W65C51N chip. + +Interrupts and RTS handshaking are used to reduce the chance of missing +incoming characters. A 256-byte buffer is used to hold incoming data +until it can be read. If the buffer fills up, then RTS will be disabled +and communications with the host will stop until the program starts +reading bytes again with getchar(). + +System tick counter +------------------- + +The system tick counter can be read by program code with the millis() +function: + + unsigned long millis(void); + +The counter starts at zero at startup time and increments every millisecond. +The value will wrap around after 49.7 days. + +The millis() function is intended for implementing timeouts and delays without +using hard-coded delay loops. The built-in delay() function makes it easy +to introduce a simple delay up to 65535 milliseconds into your program: + + void delay(unsigned ms); + +Interrupt handling +------------------ + +The crt0 code takes care of serial and system tick interrupts automatically. +After the standard interrupt sources have been handled, the crt0 code +will call the irq() function, which must be declared as follows: + + __attribute__((interrupt)) void irq(void) + { + ... + } + +Non-maskable interrupts can also be handled if the program declares the +nmi() function as follows: + + __attribute__((interrupt)) void nmi(void) + { + ... + } + +The linker script provides stub definitions for irq() and nmi() in case +you don't need them. diff --git a/mos-platform/eater/abort.c b/mos-platform/eater/abort.c new file mode 100644 index 000000000..78df86c2e --- /dev/null +++ b/mos-platform/eater/abort.c @@ -0,0 +1,7 @@ +#include +#include + +void abort(void) { + puts("Aborted"); + _exit(134); // 128 + SIGABRT +} diff --git a/mos-platform/eater/chrin.h b/mos-platform/eater/chrin.h new file mode 100644 index 000000000..9c7182b72 --- /dev/null +++ b/mos-platform/eater/chrin.h @@ -0,0 +1,18 @@ +#ifndef _CHRIN_H_ +#define _CHRIN_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +// Get a character from the serial console. +int __chrin(void); + +// Get a character from the serial console, returns -1 if none available. +int __chrin_no_wait(void); + +#ifdef __cplusplus +} +#endif + +#endif // not _CHRIN_H_ diff --git a/mos-platform/eater/chrout.h b/mos-platform/eater/chrout.h new file mode 100644 index 000000000..5770ae55c --- /dev/null +++ b/mos-platform/eater/chrout.h @@ -0,0 +1,14 @@ +#ifndef _CHROUT_H_ +#define _CHROUT_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +void __chrout(char c); + +#ifdef __cplusplus +} +#endif + +#endif // not _CHROUT_H_ diff --git a/mos-platform/eater/clang.cfg b/mos-platform/eater/clang.cfg new file mode 100644 index 000000000..a1ae98f7b --- /dev/null +++ b/mos-platform/eater/clang.cfg @@ -0,0 +1,3 @@ +-D__eater__ +-mcpu=mosw65c02 +-mlto-zp=218 diff --git a/mos-platform/eater/crt0/reset.S b/mos-platform/eater/crt0/reset.S new file mode 100644 index 000000000..4dcfa0537 --- /dev/null +++ b/mos-platform/eater/crt0/reset.S @@ -0,0 +1,38 @@ +; Copyright (c) 2023 Rhys Weatherley +; +; Licensed under the Apache License, Version 2.0 with LLVM Exceptions, +; See https://github.com/llvm-mos/llvm-mos-sdk/blob/main/LICENSE for license +; information. + +.include "imag.inc" + +; Initialize the system from the reset vector. +.global __do_reset +.section .init.50,"axR",@progbits +__do_reset: +; Fix the D and I flags. + cld + cli +; Set up the initial 6502 stack pointer. + ldx #$ff + txs + +.text +; IRQBRK handler. +.global _irqbrk +.section .text._irqbrk,"axR",@progbits +_irqbrk: + pha ; __systick_isr and __serial_isr use A and X. + phx + cld ; Just in case. + jsr __systick_isr ; Handle the system millisecond tick timer interrupt. + jsr __serial_isr ; Handle serial receive interrupts. + plx + pla + jmp irq ; Jump to the user-supplied IRQ handler. + +; Default IRQ and NMI handler if the user's program hasn't defined one. +.global _irq_default +.section .text._irq_default,"axR",@progbits +_irq_default: + rti diff --git a/mos-platform/eater/crt0/serial.S b/mos-platform/eater/crt0/serial.S new file mode 100644 index 000000000..e21a45c08 --- /dev/null +++ b/mos-platform/eater/crt0/serial.S @@ -0,0 +1,115 @@ +; Copyright (c) 2023 Rhys Weatherley +; +; Licensed under the Apache License, Version 2.0 with LLVM Exceptions, +; See https://github.com/llvm-mos/llvm-mos-sdk/blob/main/LICENSE for license +; information. + +#define ACIA_DATA 0x5000 +#define ACIA_STATUS 0x5001 +#define ACIA_CMD 0x5002 +#define ACIA_CTRL 0x5003 + +#define RX_HIGH 240 ; Buffer high water mark. +#define RX_LOW 224 ; Buffer low water mark. + +; Initialize the serial port. +.global __do_init_serial +.section .init.270,"axR",@progbits +__do_init_serial: + lda ACIA_STATUS ; Clear any reported errors. + lda ACIA_DATA ; Empty the receive buffer if something is in it. + stz ACIA_STATUS ; Force the ACIA to reset itself. + lda #$1f ; 19200-N-8-1 + sta ACIA_CTRL + lda #$09 ; ACIA_TIC1 | ACIA_DTR + sta ACIA_CMD + +; Handle interrupts for serial receive. +.text +.global __serial_isr +.section .text.__serial_isr,"axR",@progbits +__serial_isr: + lda ACIA_STATUS ; Is there an ACIA interrupt pending? + bpl __serial_isr_end + and #$08 ; Is RDRF set, indicating a received byte? + beq __serial_isr_end + lda ACIA_DATA ; Retrieve the character that just arrived. + ldx __serial_rx_in ; Add it to the serial receive buffer. + sta __serial_rx_buffer,x + inc __serial_rx_in ; Advance the buffer pointer. + lda __serial_rx_in ; Is the buffer above the high water mark? + sec + sbc __serial_rx_out + cmp #RX_HIGH + bcc __serial_isr_end + lda #$01 ; Disable receive interrupts. + sta ACIA_CMD +__serial_isr_end: + rts + +; Get a character from the serial receive buffer. +.global __chrin +.section .text.__chrin,"ax",@progbits +__chrin: + ldx __serial_rx_out ; Do we have a character? + cpx __serial_rx_in + beq __chrin ; No, then go back and wait again. + inc __serial_rx_out ; Increment the buffer's output pointer. + lda __serial_rx_in ; Are we now below the low water mark? + sec + sbc __serial_rx_out + cmp #RX_LOW + bcs __chrin_have_char + lda #$09 ; Turn receive interrupts back on. + sta ACIA_CMD +__chrin_have_char: + lda __serial_rx_buffer,x + ldx #0 + rts + +; Get a character from the serial receive buffer, without waiting. +.global __chrin_no_wait +.section .text.__chrin_no_wait,"ax",@progbits +__chrin_no_wait: + ldx __serial_rx_out ; Do we have a character? + cpx __serial_rx_in + beq __chrin_no_char + inc __serial_rx_out ; Increment the buffer's output pointer. + lda __serial_rx_in ; Are we now below the low water mark? + sec + sbc __serial_rx_out + cmp #RX_LOW + bcs __chrin_no_wait_have_char + lda #$09 ; Turn receive interrupts back on. + sta ACIA_CMD +__chrin_no_wait_have_char: + lda __serial_rx_buffer,x + ldx #0 + rts +__chrin_no_char: + lda #$ff ; No character available, return -1. + tax + rts + +; Put a character to the serial port. +.global __chrout +.section .text.__chrout,"ax",@progbits +__chrout: + sta ACIA_DATA ; Transmit A using the ACIA. + ldx #$70 ; Wait ~560us for the byte to be transmitted. +__chrout_wait_for_tx: + dex + bne __chrout_wait_for_tx + rts + +; Control variables for the serial input buffer in the zero page. +.section .zp.bss,"zaw",@nobits +__serial_rx_in: + .fill 1 +__serial_rx_out: + .fill 1 + +; Location of the serial input buffer in noinit RAM. +.section .noinit,"aw",@nobits +__serial_rx_buffer: + .fill 256 diff --git a/mos-platform/eater/crt0/systick.S b/mos-platform/eater/crt0/systick.S new file mode 100644 index 000000000..6db8112f8 --- /dev/null +++ b/mos-platform/eater/crt0/systick.S @@ -0,0 +1,74 @@ +; Copyright (c) 2023 Rhys Weatherley +; +; Licensed under the Apache License, Version 2.0 with LLVM Exceptions, +; See https://github.com/llvm-mos/llvm-mos-sdk/blob/main/LICENSE for license +; information. + +.include "imag.inc" + +#define VIA_T2CL 0x6008 +#define VIA_T2CH 0x6009 +#define VIA_ACR 0x600b +#define VIA_IFR 0x600d +#define VIA_IER 0x600e + +#define T2COUNT 1000 + +; Initialize the system millisecond tick timer. +.global __do_init_systick +.section .init.250,"axR",@progbits +__do_init_systick: + lda #$a0 ; Enable T2 interrupts. + sta VIA_IER + lda #$00 ; T2 in one-shot mode. + lda #mos16lo(T2COUNT) ; T2 interrupt every 1000 clock ticks. + sta VIA_T2CL + lda #mos16hi(T2COUNT) + sta VIA_T2CH + +; Handle interrupts for the system millisecond tick timer. +.text +.global __systick_isr +.section .text.__systick_isr,"axR",@progbits +__systick_isr: + lda VIA_IFR ; Has the T2 timer expired? + and #$20 + beq __systick_isr_end + inc __systick_value ; Increment the system tick counter. + bne __systick_isr_restart + inc __systick_value+1 + bne __systick_isr_restart + inc __systick_value+2 + bne __systick_isr_restart + inc __systick_value+3 +__systick_isr_restart: + ldx VIA_T2CH ; Restart the one-shot T2 timer. + lda VIA_T2CL + cpx VIA_T2CH ; Has the high byte changed? + bne __systick_isr_restart ; If yes, we need to read T2CL/T2CH again. + clc + adc #mos16lo(T2COUNT - 24) ; Adjust the T2 deadline for the elapsed ticks. + sta VIA_T2CL + txa + adc #mos16hi(T2COUNT - 24) + sta VIA_T2CH +__systick_isr_end: + rts + +; Get the value of the system millisecond tick timer. +.global millis +.section .text.millis,"ax",@progbits +millis: + sei + lda __systick_value+2 + sta __rc2 + lda __systick_value+3 + sta __rc3 + lda __systick_value + ldx __systick_value+1 + cli + rts + +.section .zp.bss,"zaw",@nobits +__systick_value: + .fill 4 diff --git a/mos-platform/eater/delay.c b/mos-platform/eater/delay.c new file mode 100644 index 000000000..01de050d8 --- /dev/null +++ b/mos-platform/eater/delay.c @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2023 Rhys Weatherley + * + * Licensed under the Apache License, Version 2.0 with LLVM Exceptions, + * See https://github.com/llvm-mos/llvm-mos-sdk/blob/main/LICENSE for license + * information. + */ + +extern unsigned long millis(void); + +void delay(unsigned ms) +{ + if (ms) { + unsigned long start = millis(); + while ((millis() - start) < ms) + ; /* do nothing */ + } +} diff --git a/mos-platform/eater/eater.h b/mos-platform/eater/eater.h new file mode 100644 index 000000000..8cc53aa73 --- /dev/null +++ b/mos-platform/eater/eater.h @@ -0,0 +1,18 @@ +#ifndef _EATER_H_ +#define _EATER_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +// Get the value of the system millisecond tick counter. +unsigned long millis(void); + +// Wait for a specific number of milliseconds. +void delay(unsigned ms); + +#ifdef __cplusplus +} +#endif + +#endif // not _EATER_H_ diff --git a/mos-platform/eater/getchar.c b/mos-platform/eater/getchar.c new file mode 100644 index 000000000..852acb48b --- /dev/null +++ b/mos-platform/eater/getchar.c @@ -0,0 +1,9 @@ +#include +#include + +int getchar(void) { + int c = __chrin(); + if (c == '\r') + c = '\n'; + return c; +} diff --git a/mos-platform/eater/link.ld b/mos-platform/eater/link.ld new file mode 100644 index 000000000..1bd8a75b0 --- /dev/null +++ b/mos-platform/eater/link.ld @@ -0,0 +1,47 @@ +/* Linker script for Ben Eater's 6502 breadboard computer. */ + +/* Provide imaginary (zero page) registers. */ +__rc0 = 0xe0; +INCLUDE imag-regs.ld +ASSERT(__rc31 == 0xff, "Inconsistent zero page map.") + +MEMORY { + zp : ORIGIN = 0x00, LENGTH = 0xe0 + ram : ORIGIN = 0x200, LENGTH = 0x4000 - 0x200 + rom : ORIGIN = 0x8000, LENGTH = 0x7ffa +} + +/* Provide default IRQ and NMI handlers if the program doesn't have them. */ +PROVIDE(irq = _irq_default); +PROVIDE(nmi = _irq_default); + +SECTIONS { + .text : { + INCLUDE text-sections.ld + } >rom AT>rom + .rodata : { INCLUDE rodata-sections.ld } >rom AT>rom + .data : { INCLUDE data-sections.ld } >ram AT>rom + __data_load_start = LOADADDR(.data) - LOADADDR(.rodata) + ADDR(.rodata); + __data_size = SIZEOF(.data); + .zp.data : { INCLUDE zp-data-sections.ld } >zp AT>rom + __zp_data_load_start = LOADADDR(.zp.data) - LOADADDR(.rodata) + ADDR(.rodata); + __zp_data_size = SIZEOF(.zp.data); + .zp.bss (NOLOAD) : { + INCLUDE zp-bss-sections.ld + } >zp + INCLUDE zp-bss-symbols.ld + INCLUDE zp-noinit.ld + .bss (NOLOAD) : { INCLUDE bss-sections.ld } >ram AT>ram + INCLUDE bss-symbols.ld + .noinit (NOLOAD) : { INCLUDE noinit-sections.ld } >ram AT>ram +} + +/* Set initial soft stack address to just above last ram address. (It grows down.) */ +__stack = ORIGIN(ram) + LENGTH(ram); + +OUTPUT_FORMAT { + FULL(rom) + SHORT(nmi) + SHORT(_start) + SHORT(_irqbrk) +} diff --git a/mos-platform/eater/putchar.c b/mos-platform/eater/putchar.c new file mode 100644 index 000000000..911ad81b6 --- /dev/null +++ b/mos-platform/eater/putchar.c @@ -0,0 +1,8 @@ +#include +#include + +void __putchar(char c) { + if (__builtin_expect(c == '\n', 0)) + __chrout('\r'); + __chrout(c); +}