New 65C02 SBC Project

Topics related to the SBC- series of printed circuit boards, designed by Daryl Rictor and popular with many 6502.org visitors.
twenglish1
Posts: 9
Joined: 27 Nov 2024

New 65C02 SBC Project

Post by twenglish1 »

So after seeing the Ben Eater 6502 series on youtube i was hooked, and interested to learn more, designed my own board, and i am now on my 4th revision of a 65C02 SBC, being proud of my achievement, figured this would be a good place to share. I have no formal background in electronics or programming, just a hobby of mine, previously had spent a lot of time programming AVRs and Arduinos for various projects. So this tends to be setup more in that aspect. Board currently has 3 65c22s with 20 of the IO pins broken out to headers, a 65c51, 4x5 keypad, labeled 0-F for hex input, and 4 unlabeled buttons at the top , 8-leds, a 16x2 character LCD, a MAX232 for RS-232 serial and a Microchip MCP2221 for USB to Serial. Currently running software i wrote that will accept up to 2kB of code through the hex keypad(for fun) or sent over serial(much more practical), storing it to RAM and executed through a button press. I am sure my code is a mess as i am new to this hardware and fairly new to assembly, but hey it works, and it was for sure a fun learning experience!
20241123_213518.jpg
User avatar
BigEd
Posts: 11463
Joined: 11 Dec 2008
Location: England
Contact:

Re: New 65C02 SBC Project

Post by BigEd »

Excellent! (And welcome!)
barnacle
Posts: 1831
Joined: 19 Jan 2004
Location: Potsdam, DE
Contact:

Re: New 65C02 SBC Project

Post by barnacle »

That does look a rather nice (and traditional) board - though I wonder about the UART pins: how are they separated from the signals from the RS232 converter (or are they just for monitoring?)

Neil
twenglish1
Posts: 9
Joined: 27 Nov 2024

Re: New 65C02 SBC Project

Post by twenglish1 »

So i realized i hadn't tested the RS-232 port at the time of posting, just did and it doesnt seem to work, it worked in the previous prototype before i added the USB UART chip, i seem to have overlooked isolating the signals, probably should have added header pins and jumpers to select between USB UART and RS-232, similar to what i did with all the IRQ pins
barnacle
Posts: 1831
Joined: 19 Jan 2004
Location: Potsdam, DE
Contact:

Re: New 65C02 SBC Project

Post by barnacle »

If those uart pins are just across the TX and RX _TTL_ side of the 232 chip, they won't affect anything if they're not attached to anything else.

And the 232 chip generates the positive and negative voltages required for RS232 (if you have the right capacitors) and the CTS and RTS are pass-through (after level conversion) so they shouldn't affect anything... Worth a check that your RS232 TX line (i.e. from the board) sits idle at -3v or greater, and +5 on the TTL side.

Neil
twenglish1
Posts: 9
Joined: 27 Nov 2024

Re: New 65C02 SBC Project

Post by twenglish1 »

I have -9v on the TX line, and 5v on the TTL side of the MAX232, however i think i found my issue, the -9v is on pin 2 of my DB9 connector, looks like i must have swapped the tx and rx lines at the connector. I think there was some confusion because with the USB UART the tx gets connected to RX, so i must have switched them at the DB9 connector as well

EDIT: Maybe not, looked at previous schematics and i changed nothing changed with the RS-232 circuitry, i will post schematics
twenglish1
Posts: 9
Joined: 27 Nov 2024

Re: New 65C02 SBC Project

Post by twenglish1 »

According to 232 pinouts pin 3 of DB9 is TX, confirmed data at this pin with the scope, has to be something obvious i missed

EDIT: Black and white schematics
Screenshot 2024-11-28 223244.png
Screenshot 2024-11-28 223244.png (6.67 KiB) Viewed 6358 times
Screenshot 2024-11-28 223225.png
Last edited by twenglish1 on Fri Nov 29, 2024 3:37 am, edited 1 time in total.
twenglish1
Posts: 9
Joined: 27 Nov 2024

Re: New 65C02 SBC Project

Post by twenglish1 »

So it looks like it is wired correctly, i am seeing data at the 65c51 RX pin when transferring with both rs-232 and USB UART. However i noticed that the amplitude of the signal is lower when sending data over the rs-232, not getting pulled as close to ground as with the USB UART, so maybe it is not pulling low enough to register the bits?
barnacle
Posts: 1831
Joined: 19 Jan 2004
Location: Potsdam, DE
Contact:

Re: New 65C02 SBC Project

Post by barnacle »

The world's best 'standard' and counting... I do not believe I have got this right first time even once in the last fifty years...

DB-9 connectors for both male (DTE) and female (DCE) both use pin 3 to transmit data and pin 2 to receive. So you need a crossover cable.

But your drawing is either (a) the wrong way around or (b) requires a straight-through connector.

Neil
barnacle
Posts: 1831
Joined: 19 Jan 2004
Location: Potsdam, DE
Contact:

Re: New 65C02 SBC Project

Post by barnacle »

Ah, do you have the TXD and RXD from both IC10 and IC11 connected together? You can't do that... (my original concern) try pulling the IC11 out of the board and see if it works through IC10, or vice versa.

Neil
twenglish1
Posts: 9
Joined: 27 Nov 2024

Re: New 65C02 SBC Project

Post by twenglish1 »

Got it, that makes sense, has to be the issue, i am certain that the RS-232 worked before adding the MCP2221 circuitry. I had questioned it numerous times but after i already had the board out to be made, i will test removing the MCP2221, and will most likely add pin headers/jumpers to switch them

UPDATE: Removed the MCP2221 (unfortunately was soldered in, didnt have the right socket for it when i assembled the board) and the RS-232 works correctly now. What would be the recommended way to have both of them available? The USB UART is much more convenient as it powers the board as well, something about the RS-232 port just feels official though, even though i am just using a USB RS-232 cable. If i have another board made, i might just add little dip switches, or pin headers to disconnect the MCP2221 from the UART circuit, in the mean time i think socketing the the MCP2221 (like i originally planned) and removing the chip when i want to use the RS-232 would be the easiest solution
User avatar
BigDumbDinosaur
Posts: 9425
Joined: 28 May 2009
Location: Midwestern USA (JB Pritzker’s dystopia)
Contact:

Re: New 65C02 SBC Project

Post by BigDumbDinosaur »

twenglish1 wrote:
Got it, that makes sense, has to be the issue, i am certain that the RS-232 worked before adding the MCP2221 circuitry. I had questioned it numerous times but after i already had the board out to be made, i will test removing the MCP2221, and will most likely add pin headers/jumpers to switch them

UPDATE: Removed the MCP2221 (unfortunately was soldered in, didnt have the right socket for it when i assembled the board) and the RS-232 works correctly now. What would be the recommended way to have both of them available? The USB UART is much more convenient as it powers the board as well, something about the RS-232 port just feels official though, even though i am just using a USB RS-232 cable. If i have another board made, i might just add little dip switches, or pin headers to disconnect the MCP2221 from the UART circuit, in the mean time i think socketing the the MCP2221 (like i originally planned) and removing the chip when i want to use the RS-232 would be the easiest solution

It might be useful in the future to use a jumper block with CTS, instead of hard-wiring it to ground.  You may wish you had that arrangement the moment you speed up the interface past 9600 bpS and RxD overruns start to become a nuisance.  “Soft” flow control above 9600 is usually unreliable, especially with a UART with no RxD FIFO.
x86?  We ain't got no x86.  We don't NEED no stinking x86!
twenglish1
Posts: 9
Joined: 27 Nov 2024

Re: New 65C02 SBC Project

Post by twenglish1 »

I had originally planned to have the hardware flow control pins wired up, at this point i am not entirely sure why i omitted them, i will surely be wiring them in if make a new version, so far i have been able to receive data at 19200 baud fairly reliably and write it to ram using the code below. I appreciate the pointers, this is fairly new to me, and i am sure this code is a disaster by most standards but it works, small portion of code checking the "page counter byte" and branching to the routine for each page of data received, up to 8 pages. I haven't tried to come up with a way to load the upper 8-bits of the ram address to be written to into the receive/write routine, beyond indexing the lower 8 bits using the x register. This would avoid having to use the extra jumps to get around the limitations of branch distance.

Code: Select all

loop:
	lda $505
	bit #$01						;%00000001
	bne page_1
	bit #$02						;%00000010
	bne page_2
	bit #$04						;%00000100
	bne page_3
	bit #$08						;%00001000
	bne page_4
	bit #$10						;%00010000
	bne page_5
	bit #$20						;%00100000
	bne page_6_jmp
	bit #$40						;%01000000
	bne page_7_jmp
	bit #$80						;%10000000
	bne page_8_jmp
	jmp col_1

page_6_jmp:
	jmp page_6
page_7_jmp:
	jmp page_7
page_8_jmp:
	jmp page_8

page_1:
	lda ACIA_STATUS					;Check For Serial Data
	and #$08
	beq col_1_jmp                                           ;Start Reading Keypad If No Serial Data
	lda ACIA_DATA					        ;Read Incoming Serial Data
	sta $1000,x						;Write data To Ram
	sta PORTB1
	inx								;Increment Index Counter
	bne page_1
	rol $505
	jmp loop
User avatar
BigDumbDinosaur
Posts: 9425
Joined: 28 May 2009
Location: Midwestern USA (JB Pritzker’s dystopia)
Contact:

Re: New 65C02 SBC Project

Post by BigDumbDinosaur »

twenglish1 wrote:
I appreciate the pointers, this is fairly new to me, and i am sure this code is a disaster by most standards but it works...

Ultimately, functioning code is the goal.  Better is fast, functioning code.  Even better is fast, functioning code that is bug-free.  It’s that last requirement that puts the challenge in programming, especially in assembly language.  :shock:

At the risk of being Captain Obvious, the first step is to understand the task to be carried out.  This is especially true when writing a device driver—a potentially-complex undertaking, which is what you are doing with the 6551.  In the case of a device driver, you should fully read and understand the data sheet (especially the “gotchas”), and perhaps post questions here if something in the data sheet isn’t clear—data sheets vary widely in lucidity and accuracy.  :?

Once you understand (or think you understand :D) what has to be done and also understand the device to be driven, the next step is not to write the code, but to write a narrative that explains how your code is going to work.  The narrative should be phrased as though you are explaining your program’s operation to someone who is not familiar with the task at hand.  As Garth has often noted, doing so helps you define the program flow, especially the difficult parts of an algorithm.  I always go through this process before I write the first line of code—it has never failed me.  In a few instances, I’ve drawn a flowchart to clarify my thoughts and help me understand the require workflow, e.g.:

26C92/28L92 TxD Interrupt Service Flow Chart
26C92/28L92 TxD Interrupt Service Flow Chart

I drew the above when I was developing the device driver for the NXP 26C92 dual UART (DUART) in my POC V1.1 unit.  The challenge was in writing code that could work with the device’s transmitter FIFOs so as to reduce the frequency of interrupts, while also using a minimum number of instructions to get the job done.  Also, being a dual UART, there were two channels to be serviced, which, of course, complicated things.

Working on the theory that one picture was worth a thousand words, flowcharting the process proved to be the quickest way to develop a workable algorithm.  The driver worked on the first try, which was a good thing—debugging a misbehaving interrupt handler can be quite a challenge.  :evil:

As well as defining the problem and how to solve it, I usually add a fairly extensive comment header to the top of the code in question, e.g.:

Code: Select all

;blkread: READ FROM SCSI BLOCK DEVICE
;
;	———————————————————————————————————————————————————————————————————————
;	Synopsis: This function reads one or more blocks from a SCSI block dev-
;	          ice, e.g., a disk, into a buffer.  The logical unit number is
;	          assumed to be zero.
;
;	          § All parameters are pointers to data.
;
;	          § The target device’s bus ID is a 16-bit quantity, with the
;	            MSB set to $00.
;
;	          § The logical block address (LBA) is a 32-bit quantity, even
;	            though the target device may not require or accept a 32-bit
;	            LBA.
;
;	          § The number of blocks to be accessed is a 16-bit quantity,
;	            with the MSB set to $00.  The maximum number of blocks that
;	            may be accessed is 127 ($007F).  If this field is $0000, no
;	            operation will occur & this function will immediately ret-
;	            urn without an error.
;
;	          § The buffer address is expressed as 32-bits, with the MSB of
;	            the MSW set to $00.  The buffer must be of sufficient size
;	            to hold the requested number of blocks multiplied by the
;	            device’s block size.
;
;	          § If the buffer pointer is null, the buffer pointer set by a
;	            previous call to the SSBUFS BIOS API is assumed to have
;	            been made.  It is recommended this sequence be used if rep-
;	            itive accesses are to be made to the same buffer.  Doing so
;	            will avoid the overhead of a BIOS API call on each access
;	            to set the buffer pointer.  Use this feature with caution!
;
;	          § The target SCSI device must have been enumerated during the
;	            system POST.
;	———————————————————————————————————————————————————————————————————————
;	Invocation example: pea #buf >> 16         ;buffer pointer MSW
;	                    pea #buf & $ffff       ;buffer pointer LSW
;	                    pea #nblk >> 16        ;block count pointer MSW
;	                    pea #nblk & $ffff      ;block count pointer LSW
;	                    pea #lba >> 16         ;LBA pointer MSW
;	                    pea #lba & $ffff       ;LBA pointer LSW
;	                    pea #scsi_id >> 16     ;device ID pointer MSW
;	                    pea #scsi_id & $ffff   ;device ID pointer LSW
;	                    .IF .DEF(_SCSI_)
;	                    JSL blkread
;	                    .ELSE
;	                    JSR blkread
;	                    .ENDIF
;	                    BCS ERROR
;
;	Exit registers: .A: entry value ¹
;	                .B: entry value ²
;	                .X: entry value
;	                .Y: entry value
;	                DB: entry value
;	                DP: entry value
;	                PB: entry value
;	                SR: nvmxdizc
;	                    ||||||||
;	                    |||||||+———> 0: okay
;	                    |||||||      1: error
;	                    +++++++————> entry value
;
;	Notes: 1) One of the following if an error:
;
;	            e_ptrnul: null pointer passed
;	            e_ssabt : transaction aborted
;	            e_ssblk : too many blocks
;	            e_sschk : check condition
;	            e_sscfe : controller fifo error
;	            e_sscge : controller general error
;	            e_sscmd : illegal controller command
;	            e_ssdne : device not enumerated
;	            e_ssdnp : device not responding
;	            e_sssnr : SCSI subsystem not ready
;	            e_sstid : device bus ID out of range
;	            e_ssubp : unsupported bus phase
;	            e_ssubr : unexpected bus reset
;	            e_ssucg : unsupported command group
;	            e_ssudf : unsupported driver function
;	            e_ssudt : unsupported device type
;
;	       2) $00 if an error.
;	———————————————————————————————————————————————————————————————————————

The above tells the reader what the blkread function does, what sort of data it needs from the caller to do its job, what data it returns to the caller, how it reports errors, etc..  The above isn’t just a reference; I was clarifying in my mind how this function was going to work by the simple expedient of writing down how it was going to work.  As my thoughts went to the metaphoric paper, I was also drawing out situations, such as null pointers, that could result in errors, in other words, engaging in defensive programming.

Equally important is adequately commenting your code.  A comment should be written to explain what is being accomplished in terms of the function’s overall purpose.  A comment should not explain what an instruction does; anyone who might read your code presumably knows the 6502 assembly language.  :D  An instruction that normally wouldn’t merit being commented should be adequately explained if used in an unusual way, for example:

Code: Select all

         tya                   ;recover length
         clc                   ;include checksum field size  <————
         sbc addrsize          ;load address field size
         sta datasize          ;expected data field size

In the above, the code sequence is computing the size of a data field in a Motorola S-record.  Usually carry is set before a subtraction, since carry acts as an inverted borrow.  By clearing carry, I avoid one extra step in the data field size computation, but it might not be obvious.  Hence clc, which usually needs no explanation when doing arithmetic, gets one.

Here’s an excerpt from the aforementioned blkread function illustrating how I generally comment things:

Code: Select all

         sec
         tsc
         sbc !#.s_wsf          ;reserve workspace
         tcs
         inc                   ;DP is the workspace...
         tcd                   ;stack frame pointer...
;
;			—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—
;			DP is also the absolute address
;			of the local CDB.
;			—-—-—-—-—-—-—-—-—-—-—-—-—-—-—-—
;
         sep #m_setx           ;8-bit index
         ldx #0
         phx                   ;workspace implicitly...
         plb                   ;in bank $00
         ldx #.s_wsf-s_word
;
.init    stz .wsf,x            ;clear local workspace
	.rept s_word
         dex
	.endr
         bpl .init

Comments are terse and unless the purpose of an instruction is non-obvious, it isn’t commented.

I can’t overemphasize the importance of commenting.  You might remember tomorrow what you wrote today...maybe you’ll remember it a week later.  A year or two later, and your memory might be completely out to lunch when you revisit what you wrote today.  So don’t be afraid to comment away, especially in sections that do unusual things, such as hardware stack manipulation (always a fertile ground for inadvertent bugs):

Code: Select all

;	———————————
;	COMMON EXIT
;	———————————
;
.done    rep #m_setr|sr_car
         tsc                   ;our stack location
         adc !#.s_wsf          ;dispose of...
         tcs                   ;local workspace
         adc !#.s_rsf          ;compute position of...
         tax                   ;register stack frame
         adc !#.s_psf          ;compute position of...
         tay                   ;parameter stack frame
         lda !#.s_rsf-1        ;overwrite parameters...
         mvp #0,#0             ;w/registers
         tyx                   ;point to the...
         txs                   ;register stack frame &...
         pld                   ;clean up our mess
         plb
         pla
         plx
         ply
         plp
	.if .def(_SCSI_)      ;if making “far” calls...
         rtl
	.else                 ;otherwise...
         rts
	.endif

The above stack pointer manipulations are non-obvious to anyone who isn’t well-acquainted with using the 65C816’s stack as a scratchpad, so commentary is de rigueur.

One other thing I recommend: don’t bury “magic numbers” in your code:

Code: Select all

   lda ACIA_STATUS               ;Check For Serial Data
   and #$08
   beq col_1_jmp                 ;Start Reading Keypad If No Serial 

In the above, it’s clear you are reading the UART’s status register—the operand says so.  Then you are testing it with a mask ($08) whose value tells the reader nothing about what it is you are testing.  I, for one, haven’t a clue what in the UART affects bit 3 in the status register; I haven’t used a 6551 in nearly 35 years.  Fortunately, I do have the WDC data sheet, which tells me that bit 3 indicates if the receiver has a datum waiting.  The data sheet even gives that bit a name: rdrf, meaning “receiver data register full.”

A better way to write the above might be:

Code: Select all

   lda ACIA_STATUS               ;get UART’s status
   bit #rdrf                     ;there a datum in RxD?
   beq col_1_jmp                 ;no, read keypad

Earlier in your program, you would declare...

Code: Select all

rdrf =%00001000     ;UART receiver status

...and then use rdrf anywhere in your code in which a reference to the UART’s receiver status must be made.  Note that I used bitwise notation to define rdrf to make sure I’ve chosen the correct bit (more defensive programming).  This is a recommended practice any time you are defining chip registers in which each bit means something or controls something.

The problems with burying “magic numbers” in code are several fold:

  • If a “magic number” has to be changed, you have to search for all instances in which it is used.  In a small project, that may not be a big deal.  In a large project with multiple source files, it is, as it is easy to overlook an instance where the “magic number” is used, which will introduce an insidious bug into your program.  On the other hand, if you define all your constants in a single file (an INCLUDE file) and refer to their symbols, all you have to do to change a constant in your program is edit that constant’s definition in your INCLUDE file and re-assemble your program.
     
  • Mistyping one instance of a “magic number” within code may go undetected until your program mysteriously malfunctions in seemingly random ways.  Such errors may be incredibly difficult to find (don’t ask me how I know this :oops:).  If, instead, that “magic number” is symbolically defined and you mistype the symbol’s name, the assembler will likely catch the error as an undefined symbol.
     
  • A “magic number” almost always tells the reader nothing about its purpose.  As I noted above, I had to look at the 65C51’s data sheet to determine what AND #$08 was testing.


Finally, once you get your program to where it will run and produce the right results, always keep it in a working state.  After you get it to run, you may want to optimize the code to make it run faster, or maybe take up less space—two sometimes-mutually-incompatible goals :D.  The safe approach is to either change one thing at a time, or make a backup copy before you go on an editing binge.  If you change only one thing and the program ceases to work, the fix is obvious: reverse that change.  If you go on an editing binge, the program stops working and there’s no backup copy, the fix isn’t obvious and you’ve got a problem.  :cry:
x86?  We ain't got no x86.  We don't NEED no stinking x86!
twenglish1
Posts: 9
Joined: 27 Nov 2024

Re: New 65C02 SBC Project

Post by twenglish1 »

Awesome thank you! Tons of good information, i will keep all that in mind for sure. I had slowly been going back though and trying to improve my code, i removed a ton of LDAs and ANDs and used the BIT instructions instead, and i definitely understand the importance of commenting everything, it would be very easy to not look at the code for days and then have to try and figure out what i was trying to do all over again!

EDIT: Some more details

Credit to this site for my address decoding, using the 74HC00 and 74HC02 method listed first:
https://trobertson.site/6502-memory-map ... s-decoder/

Thanks to GarthWilson's site for a ton of learning resources, i learned so much from reading though it!

Designed my board with KiCad and had them made by JLCPCB, i have done it in the past but i am not into hand wiring boards anymore haha
Post Reply