So... the UART on my SBC works beautifully.
Except for one teeny tiny problem.
It won't change baud rate. For the longest time I thought it was just plain busted, but I finally hooked up an oscilloscope to it and discovered that its running at a baud rate of 14375. As soon as I switched the terminal emulator on the PC to that baud rate, works perfectly.
I cannot for the life of me figure out why I can't set the baud rate. As best as I (and one my lecturers) can tell I'm following the datasheet to the letter, but regardless of what divisor I write into the divisor latches, nothing changes. Every other aspect of the UART is working perfectly as best as I can tell, so I don't think I managed to zap it with ESD as I wouldn't expect ESD problems to manifest so specifically.
The UART I'm using is an NXP SC16C550BIB48 (
datasheet). You can see how its wired in using the schematic etc
Here.
I'm copying in the complete source code below just in case the problem is in a far off region I hadn't considered, but the directly relevant setup code is the block underneath the "
; Initial setup of the UART" comment. Please excuse my sawdust, this is literally the in-development-code I have right now exactly as it is.
uarttest.asmCode:
; FPGA-6502 Test Computer Test BIOS#2
; UART Test.
; Copyright (C) Nicholas Parks Young, 2017. All Rights Reserved.
; Version 20170601
;
INCLUDE "definitions.asm"
INCLUDE "wozprint.asm"
CODE
RstHandler:
; Just arrived here from a RESET condition. The following is known:
; Status Register P: NF=?, VF=?, 5F=1, BF=1, DF=0, IF=1, ZF=?, CF=?
; Program Counter PC: RSTHandler
; All other flags and registers are undefined. We must define them.
; Define all the undefined things.
STZ biosStatus ; Reset BIOS status bits - in particular bit 0, which indicates successful initialisation.
SEI ; Disable Interrupts -> IF=1
CLD ; Disable Decimal mode -> DF=0
LDX #$FF ; Set X=FF
TXS ; Copy X to S -> S=FF
INX ; Increment X -> X=0
TXA ; Copy X to A -> A=0
TAY ; Copy A to Y -> Y=0, NF=0, ZF=1
CLC ; Clear Carry flag -> CF=0
CLV ; Clear Overflow flag -> VF=0
; Processor is now in the following known state:
; P: NF=0, VF=0, 5F=1, BF=1, DF=0, IF=1, ZF=1, CF=0
; A=0
; S=FF
; X=0
; Y=0
; Initial setup of the VIA.
LDA #$FF ; Ensure all interrupt flags are cleared.
STA VIA_IFR
LDA #IER_CLEAR|INT_T2|INT_CB1|INT_CB2|INT_CA1|INT_CA2|INT_SR
STA VIA_IER ; Ensure all interrupts we're not currently using are disabled.
LDA #IER_SET|INT_T1
STA VIA_IER ; Ensure the interrupts we are using are enabled.
STZ VIA_PCR ; Set all Cxx pins to input/negative-edge.
LDA #%10111111 ; Setup port B - everything is an output except SPIMISO...
STA VIA_DDRB
LDA #%00011100 ; Set initial output states for Port B. ~SPICSx lines are inactive(high)
STA VIA_ORB ; For SERDir (bit7), LOW is output, HIGH is input.
LDA #$FF ; Set port A direction register to all outputs
STA VIA_DDRA
STZ VIA_ORA ; Set port A outputs to all low
LDA #T1_CONTI|T2_TIMED|SR_DISABLED|NOLATCH_PB_CB1|NOLATCH_PA_CA1
STA VIA_ACR ; Set Timer 1 to generate interrupts as specified by T1 Latches.
LDA #$40 ; 0x9C40 = 40000. Note little-endian as timing starts on write to VIA_T1CH.
STA VIA_T1CL ; T1 is set to generate interrupts at PHI2/Latches intervals. Effectively
LDA #$9C ; I'll get a T1 interrupt every 40/20ms (for 1MHz/2MHz respectively)
STA VIA_T1CH
; Initial setup of the UART
STZ biosCharsRcvd ; Clear the characters received counter.
STZ biosCharsRcvd+1
LDA #$83 ; Enable access to DLL/DLM, set parity to off, set word length to 8 bits, 1 stop bit.
STA UART_LCR
LDA #12 ; Set DLL/DLM to 12(decimal). This divisor gives 9600 baud with 1.8432MHz crystal.
STA UART_DLL
STZ UART_DLM
LDA #$03 ; Disable access to DLL/DLM, set parity to off, set word length to 8 bits, 1 stop bit.
STA UART_LCR
LDA #$23 ; Set Auto-RTS/CTS to On, DTR = active.
STA UART_MCR
LDA #$01 ; Enable FIFOs. Must be done before setting other FCR bits.
STA UART_FCR
LDA #$C7 ; FIFO Trigger level = 14 bytes, DMA Mode = off. Reset FIFOs.
STA UART_FCR
LDA #$09 ; Enable interrupt on FIFO trigger level and MSR change.
STA UART_IER
SMB0 <biosStatus ; We've finished setting up, so we can set the "setup" status flag
CLI ; Enable interrupts.
?BusyWait:
WAI ; Wait for an interrupt to happen
JMP ?BusyWait ; After interrupt has been serviced, go back to waiting.
NmiHandler:
; If the "setup" status bit is clear, skip NMI routine.
BBR0 <biosStatus, ?HandlerEnd
; If the DCD/DSR active bit is clear, skip NMI routine.
;BBR1 <biosStatus, ?HandlerEnd
; Wait until the UART TX FIFO is completely cleared, just in case.
; To do this, check LSR[6] -> V Flag -> 0 = TX FIFO Not Empty.
?BusyWait:
;BIT UART_LSR ; Copy LSR[6] into V Flag
;BVC ?BusyWait ; If V Flag is clear (which is true if the TX FIFO is non-empty), go to ?BusyWait
PHA ; Preserve A
; Print number of chars received to the UART.
LDA biosCharsRcvd+1 ; Load High Byte First (little-endian)
JSR PrintHex ; Print hex bytes
LDA biosCharsRcvd ; Load Low Byte Second
JSR PrintHex ; Print hex bytes
PLA ; Restore A
?HandlerEnd:
RTI
IrqHandler:
PHA ; Save accumulator
PHX ; Save X
; Check if interrupt came from VIA T1.
LDA VIA_IFR ; Get the Interrupt Flag Register from the VIA.
BIT #IFR_IRQACTIVE|INT_T1 ; Mask the IFR so that ZF=0 if Timer1 triggered the interrupt.
BEQ ?UartIrq ; If ZF=1 its not the VIA's T1, so next we'll check the UART.
; Interrupt came from VIA T1.
?ViaIrqTimer1:
LDA VIA_ORA ; Get current state of output port A
EOR #%00000001 ; Flip bit 0
STA VIA_ORA ; Put it back.
LDA #$FF ; Clear the interrupt from the VIA's IFR register.
STA VIA_IFR ; I'm clearing all the flags even though it should only be T1, just in case.
JMP ?IrqHandlerEnd ; Job done.
; Interrupt came from the UART (probably)
?UartIrq:
; Jump to the appropriate handler given the UART's ISR status.
LDA UART_ISR
AND #$0F ; Mask out the four high bits which we're not interested in.
ASL A ; Shift one left because addresses are 2 bytes, not one.
TAX
JMP (?UartIrqJumpTable, X)
KDATA
?UartIrqJumpTable:
dw ?UartMsrChangeHandler ; ISR[3:0] = 0000 = MSR has changed
dw ?IrqHandlerEnd ; ISR[3:0] = 0001 = No UART Interrupt
dw ?UartTransmitterEmptyHandler ; ISR[3:0] = 0010 = Transmitter FIFO is Empty
dw ?IrqHandlerEnd ; ISR[3:0] = 0011 = No UART Interrupt
dw ?UartDataFullHandler ; ISR[3:0] = 0100 = Receiver FIFO is Full.
dw ?IrqHandlerEnd ; ISR[3:0] = 0101 = No UART Interrupt
dw ?UartDataReceivedHandler ; ISR[3:0] = 0110 = Some data has been received
dw ?IrqHandlerEnd ; ISR[3:0] = 0111 = No UART Interrupt
dw ?IrqHandlerEnd ; ISR[3:0] = 1000 = Invalid
dw ?IrqHandlerEnd ; ISR[3:0] = 1001 = No UART Interrupt
dw ?IrqHandlerEnd ; ISR[3:0] = 1010 = Invalid
dw ?IrqHandlerEnd ; ISR[3:0] = 1011 = No UART Interrupt
dw ?UartDataTimeoutHandler ; ISR[3:0] = 1100 = Data in Receiver FIFO and timeout period elapsed
dw ?IrqHandlerEnd ; ISR[3:0] = 1101 = No UART Interrupt
dw ?IrqHandlerEnd ; ISR[3:0] = 1110 = Invalid
dw ?IrqHandlerEnd ; ISR[3:0] = 1111 = No UART Interrupt
ENDS
; MSR status has changed. Investigate DSR & DCD state.
?UartMsrChangeHandler:
LDA #$A0
CMP UART_MSR ; ZF=1 if both DSR and DCD are "low".
BNE ?SkipBitSet ; If ZF=0 (DSR or DCD inactive) branch then skip the bit set
SMB1 <biosStatus ; If ZF=1 then set bit 1 of biosStatus to indicate DSR&DCD are active.
?SkipBitSet:
BEQ ?SkipBitReset ; If ZF=1 branch skip the bit clear
RMB1 <biosStatus ; If ZF=0 then clear bit 1 of biosStatus to indicate DSR&DCD are inactive.
?SkipBitReset:
JMP ?UartIrq ; Keep repeating until all UART interrupts are serviced.
; Received some data that is to be processed.
?UartDataFullHandler:
?UartDataTimeoutHandler:
LDA #$01 ; Set ZF=0 if LSR[0] = 1; ZF=1 if LSR[0] = 0.
BIT UART_LSR
BEQ ?IrqHandlerEnd ; If ZF=1 then LSR=0 so no data -> skip to end.
CLC ; Add 1 to character counter (16-bit little-endian)
LDA #$01
ADC biosCharsRcvd ; Low byte has one added
STA biosCharsRcvd ; Store result back
LDA #$00 ; Add the carry result flag to the high byte
ADC biosCharsRcvd+1
STA biosCharsRcvd+1 ; Store the resulting high byte back
; Echo the char
LDA UART_RHR ; Copy received byte from RX FIFO into RegA.
; If DSR/DCD are inactive, skip to end dropping the received char.
; BBR1 <biosStatus, ?IrqHandlerEnd
STA UART_THR ; Echo byte back by writing to TX FIFO
JMP ?UartIrq ; Keep repeating until all UART interrupts are serviced.
?UartTransmitterEmptyHandler: ; Not used, interrupt should be disabled. Fall through to end.
?UartDataReceivedHandler: ; Not used, interrupt should be disabled. Fall through to end.
?IrqHandlerEnd:
PLX ; Restore X
PLA ; restore accumulator
RTI ; return from interrupt
ENDS
BIOSVECT SECTION OFFSET BIOSVCT_BASE
dw NmiHandler
dw RstHandler
dw IrqHandler
ENDS
END
definitions.asmCode:
; FPGA-6502 Test Computer Test BIOS#2
; Common Definitions & Setup
; Copyright (C) Nicholas Parks Young, 2017. All Rights Reserved.
; Version 20170531
SPACES ON ; Fix stupid comments behaviour
CHIP W65C02S ; This is designed for the WDC W65C02S
LONGA OFF ; As we're using the W65C02S, Long Accumulator is not possible
LONGI OFF ; As we're using the W65C02S, Long Immediates are not possible
CASE ON ; Make sure everything is case sensitive
CHKIMMED OFF ; Allow immediate values to be truncated
EXTERNS OFF ; Make sure that we need to explicitly declare XREFs
RADIX 10 ; Use Decimal by default
BIT7 OFF ; Do not set the 7th bit for ASCII bytes.
SQUOTE OFF ; Require a closing quote for string literals.
TWOCHAR OFF ; Do not convert two letter short codes into ASCII bytes. Counter-intuitive.
ARGCHK ON ; Verify number of arguments to macro calls.
MACFIRST OFF ; Don't allow macro names to override opcode, directive or section names.
PL 61 ; Page length for Listing is 61 lines
PW 132 ; Page width for Listing is 132 characters
TOP 0 ; Insert no extra blank lines on each page of a listing.
ASCLIST ON ; Make sure complete data is shown in listing.
; Define the various regions of the address for the FPGA-6502 Test Computer.
; Use these to define sections, etc.
PAGEZERO_BASE equ $0 ; Page zero is 0 to 0xFF (256 bytes)
PAGEZERO_SIZE equ 256
HWSTACK_BASE equ $100 ; Hardware Stack is 0x100 to 0x1FF (256 bytes)
HWSTACK_SIZE equ 256
SWSTACK_BASE equ $200 ; Software Stack is 0x200 to 0x2FF (256 bytes)
SWSTACK_SIZE equ 256
BIOSRAM_BASE equ $300 ; RAM Space reserved for BIOS (768 bytes)
BIOSRAM_SIZE equ 768
RAM_BASE equ $600 ; General Purpose RAM is 0x600 to 0x3FFF (14.5KiB)
RAM_SIZE equ 14848
CARTROM_BASE equ $4000 ; Cartridge ROM is 0x4000 to 0xBFFF (32KiB)
CARTROM_SIZE equ 32768
VIAREG_BASE equ $C000 ; VIA I/O device registers 0xC000 to 0xC00F
VIAREG_SIZE equ 16
UARTREG_BASE equ $C400 ; UART I/O device registers 0xC400 to 0xC407
UARTREG_SIZE equ 8
BIOSROM_BASE equ $E000 ; BIOS ROM is 0xE000 to 0xFEFF
BIOSROM_SIZE equ 8186
BIOSVCT_BASE equ $FFFA ; BIOS Routine Vectors. Note: Interrupt Vectors are at 0xFFFA to 0xFFFF
BIOSVCT_SIZE equ 6 ; Note: this is physically a part of the BIOS ROM
; VIA registers
; see datasheet for W65C22S:
; http://www.westerndesigncenter.com/wdc/documentation/w65c22.pdf
VIAREG SECTION REF_ONLY
ORG VIAREG_BASE
VIA_ORB ; Output Register for Port B (write)
VIA_IRB db ; Input Register for Port B (read)
VIA_ORA ; Output Register for Port A (write)
VIA_IRA db ; Input Register for Port A (read)
VIA_DDRB db ; Data Direction Register for Port B
VIA_DDRA db ; Data Direction Register for Port A
VIA_T1CL db ; Timer 1 Low-Order Latches (write); Timer 1 Low-Order Counter (read)
VIA_T1CH db ; Timer 1 High-Order Counter
VIA_T1LL db ; Timer 1 Low-Order Latches
VIA_T1LH db ; Timer 1 High-Order Latches
VIA_T2CL db ; Timer 2 Low-Order Latches (write); Timer 2 Low-Order Counter (read)
VIA_T2CH db ; Timer 2 High-Order Counter
VIA_SR db ; Shift Register
VIA_ACR db ; Auxiliary Control Register
VIA_PCR db ; Peripheral Control Register
VIA_IFR db ; Interrupt Flag Register
VIA_IER db ; Interrupt Enable Register
VIA_ORA_NH ; Output Register for Port A (write), No Handshake
VIA_IRA_NH db ; Input Register for Port A (read), No Handshake
ENDS
; VIA Constants
CB2_IN_NEG equ %000<<5 ; PCR - Input-negative active edge on CB2
CB2_IN_NEG_IND equ %001<<5 ; PCR - Independent interrupt input-negative edge on CB2
CB2_IN_POS equ %010<<5 ; PCR - Input-positive active edge on CB2
CB2_IN_POS_IND equ %011<<5 ; PCR - Independent interrupt input-positive edge on CB2
CB2_OUT_HANDSHAKE equ %100<<5 ; PCR - Handshake Output on CB2
CB2_OUT_PULSE equ %101<<5 ; PCR - Pulse output on CB2
CB2_OUT_LOW equ %110<<5 ; PCR - Low output on CB2
CB2_OUT_HIGH equ %111<<5 ; PCR - High output on CB2
CB1_NEG equ %0<<4 ; PCR - Negative active edge on CB1
CB1_POS equ %1<<4 ; PCR - Positive active edge on CB1
CA2_IN_NEG equ %000<<1 ; PCR - Input-negative active edge on CA2
CA2_IN_NEG_IND equ %001<<1 ; PCR - Independent interrupt input-negative edge on CA2
CA2_IN_POS equ %010<<1 ; PCR - Input-positive active edge on CA2
CA2_IN_POS_IND equ %011<<1 ; PCR - Independent interrupt input-positive edge on CA2
CA2_OUT_HANDSHAKE equ %100<<1 ; PCR - Handshake Output on CA2
CA2_OUT_PULSE equ %101<<1 ; PCR - Pulse output on CA2
CA2_OUT_LOW equ %110<<1 ; PCR - Low output on CA2
CA2_OUT_HIGH equ %111<<1 ; PCR - High output on CA2
CA1_NEG equ %0 ; PCR - Negative active edge on CA1
CA1_POS equ %1 ; PCR - Positive active edge on CA1
T1_TIMED equ %00<<6 ; ACR - Timed interrupt each time T1 is loaded.
T1_CONTI equ %01<<6 ; ACR - Continuous interrupts on T1.
T1_TIMED_ONESHOT equ %10<<6 ; ACR - Timed interrupt each time T1 is loaded, with one shot output on PB7
T1_CONTI_SQUAREW equ %11<<6 ; ACR - Continuous interrupts on T1, with square wave output on PB7
T2_TIMED equ %0<<5 ; ACR - Timed interrupt on T2
T2_COUNT_PB6 equ %1<<5 ; ACR - Decrement T2 on negative pulse on PB6
SR_DISABLED equ %000<<2 ; ACR - Shift Register disabled
SR_IN_T2 equ %001<<2 ; ACR - Shift in under control of T2
SR_IN_PHI2 equ %010<<2 ; ACR - Shift in under control of System Clock
SR_IN_CB1 equ %011<<2 ; ACR - Shift in under control of External Clock on CB1
SR_OUT_T2_FREE equ %100<<2 ; ACR - Shift out free running at T2 rate
SR_OUT_T2_CTRL equ %101<<2 ; ACR - Shift out under control of T2
SR_OUT_PHI2 equ %110<<2 ; ACR - Shift out under control of System Clock
SR_OUT_CB1 equ %111<<2 ; ACR - Shift out under control of External Clock on CB1
LATCH_PA_CA1 equ %1 ; ACR - Enable latching input data on Port A on CA1 active transition
LATCH_PB_CB1 equ %1<<1 ; ACR - Enable latching input data on Port B on CB1 active transition
NOLATCH_PA_CA1 equ %0 ; ACR - Disable latching input data on Port A on CA1 active transition
NOLATCH_PB_CB1 equ %0<<1 ; ACR - Disable latching input data on Port B on CB1 active transition
IER_CLEAR equ %0<<7 ; IER - Disable corresponding interrupts:
; e.g. IER_CLEAR|INT_T1|INT_T2 will disable T1 and T2 interrupts.
IER_SET equ %1<<7 ; IER - Enable corresponding interrupts:
; e.g. IER_SET|INT_T1|INT_T2 will disable T1 and T2 interrupts.
IFR_IRQACTIVE equ %1<<7 ; IFR - Bit is set if any triggered but uncleared interrupts in VIA
INT_T1 equ %1<<6 ; IFR/IER - Bit corresponding to Timer 1 interrupt
INT_T2 equ %1<<5 ; IFR/IER - Bit corresponding to Timer 2 interrupt
INT_CB1 equ %1<<4 ; IFR/IER - Bit corresponding to CB1 interrupt
INT_CB2 equ %1<<3 ; IFR/IER - Bit corresponding to CB2 interrupt
INT_SR equ %1<<2 ; IFR/IER - Bit corresponding to Shift Register interrupt
INT_CA1 equ %1<<1 ; IFR/IER - Bit corresponding to CA1 interrupt
INT_CA2 equ %1 ; IFR/IER - Bit corresponding to CA2 interrupt
; UART registers
UARTREG SECTION REF_ONLY
ORG UARTREG_BASE
UART_DLL ; Baud Rate Divisor Latch, LSB (only when LCR[7]=1)
UART_RHR ; Receive Holding Register (read)
UART_THR db ; Transmit Holding Register (write)
UART_DLM ; Baud Rate Divisor Latch, MSB (only when LCR[7]=1)
UART_IER db ; Interrupt Enable Register
UART_ISR ; Interrupt Status Register (read)
UART_FCR db ; FIFO Control Register (write)
UART_LCR db ; Line Control Register
UART_MCR db ; Modem Control Register
UART_LSR db ; Line Status Register (read)
UART_MSR db ; Modem Status Register (read)
UART_SPR db ; Scratchpad Register
ENDS
; UART Constants
; To Do.
; SPI Registers
; To Do
; SPI Constants
; To Do
; SER Registers
; To Do
; SER Constants
SER_READ equ %1<<7
SER_WRITE equ %0<<7
; To Do.
; Non-hardware numbers.
CART_SIG_VAL equ $55AA
CART_SIG_LOC equ CARTROM_BASE
CART_SIG_SIZ equ 2
PAGE0
biosCharsRcvd dw ; 16-bit counter for number of received characters.
biosStatus db ; Bitfield for internal status flags
; bit 0: 1 = Setup complete
; bit 1: 1 = DSR/DCD active
ENDS
wozprint.asmCode:
; Adapted from code in the Apple 1 WOZ Monitor source:
; http://www.sbprojects.com/projects/apple1/wozmon.txt
;-------------------------------------------------------------------------
; Subroutine to print a byte in A in hex form (destructive)
;-------------------------------------------------------------------------
PrintHex: PHA ; Save A for LSD
LSR
LSR
LSR ; MSD to LSD position
LSR
JSR ?PRHEX ; Output hex digit
PLA ; Restore A
; Fall through to print hex routine
;-------------------------------------------------------------------------
; Subroutine to print a hexadecimal digit
;-------------------------------------------------------------------------
?PRHEX: AND #%00001111 ; Mask LSD for hex print
ORA #"0" ; Add "0"
CMP #"9"+1 ; Is it a decimal digit?
BCC ?ECHO ; Yes! output it
ADC #6 ; Add offset for letter A-F
; Fall through to print routine
;-------------------------------------------------------------------------
; Subroutine to print a character to the terminal
;-------------------------------------------------------------------------
?ECHO STA UART_THR ; Output character.
RTS
_________________
Want to design a PCB for your project? I strongly recommend
KiCad. Its free, its multiplatform, and its easy to learn!
Also, I maintain KiCad libraries of
Retro Computing and
Arduino components you might find useful.