Skip to content

Commit

Permalink
Port to Ben Eater's 6502 Breadboard Computer (#149)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
rweather authored Aug 7, 2023
1 parent 541a8ab commit dd4e970
Show file tree
Hide file tree
Showing 16 changed files with 494 additions and 0 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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` |
Expand Down
1 change: 1 addition & 0 deletions mos-platform/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
39 changes: 39 additions & 0 deletions mos-platform/eater/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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)
83 changes: 83 additions & 0 deletions mos-platform/eater/README.md
Original file line number Diff line number Diff line change
@@ -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.
7 changes: 7 additions & 0 deletions mos-platform/eater/abort.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#include <stdio.h>
#include <stdlib.h>

void abort(void) {
puts("Aborted");
_exit(134); // 128 + SIGABRT
}
18 changes: 18 additions & 0 deletions mos-platform/eater/chrin.h
Original file line number Diff line number Diff line change
@@ -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_
14 changes: 14 additions & 0 deletions mos-platform/eater/chrout.h
Original file line number Diff line number Diff line change
@@ -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_
3 changes: 3 additions & 0 deletions mos-platform/eater/clang.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-D__eater__
-mcpu=mosw65c02
-mlto-zp=218
38 changes: 38 additions & 0 deletions mos-platform/eater/crt0/reset.S
Original file line number Diff line number Diff line change
@@ -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
115 changes: 115 additions & 0 deletions mos-platform/eater/crt0/serial.S
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit dd4e970

Please sign in to comment.