ASM has hit just about the right level of pain - what next? - Page 1
Forum section: electronics / general

author: paulca
author_rank: Super Contributor
timestamp: June 29, 2025, 11:44:34 am
content: So the retro Z80 DIY project is in full flight with GPIO, LCD screen etc. Another PCB is in the works with period PIO, CTC and DART.ASM coding is "ok" right now, I write some test code to get some hardware working and test a few different modes etc. Then I collect up all the scattered routines and put them into a "xx_driver.asm". As it progresses each new test application uses library routines used and tested by the last one. It's still "tiny" < 1000 lines of ASM.But it's getting to the point where I can't hide in "Level 1 ASM" code with registers and a little stack. I need to start with isolating functions, parameters, return values and stop relying on my register file as my main state. Its just too damn hard tracking register use, register mutation and register restore once you put mode 2 (or any!) interrupts into play. I still can't even decide if sub-routines should be responsible for restoring state or not. To me it seems as though sub-routines storing and restoring state "just in case" the parent was using it is wasteful ... is the parent wasn't. Should a "blocking delay" function which uses the accumulator "PUSH/POP AF" for example. I mean if the parent is using A or F and is relying on it, then sure. But is it not better that the caller assumes the callee will corrupt the register file and take its own measures?Load A, B, D, ECall somethingReload A, B, D, E againCall somethingReload A, B, D, E againetc.I need C. Badly. I know I am about to tie myself in knots in ASM, so I need to start prioritising getting a C compiler in the toolchain.I opened the can of worms with Chat GPT and regretted it. It's probably not going to help much. It offered me "sdcc". I checked it out, compiled hello-world.c and checked the files produced.To my horror it was dumping everying at 0x0000 and up, code, constants, everything. All over the page 0 RST/ISR blocks and all over my 0x100-0x2000 ROM code area.Actually that wasn't the horror, the horror was noting there were nearly 40 different segments declared in the map file. I tried the "easy options" like "--code-loc" and "--data-loc" but that only moved the "main" segments to those addresses, the other 32 of them remained at 0x0000 and up.So like all these toolkits and retro application suites, the probably started out to be what I wanted, but have had so many features and so much opinion added to them over the years that they are no longer "DIY Homebrew hardware" compatible. They are far more suited to "Insert Retro Kit Product w/ full CP/M support" build and run projects.I need something far more low level. Far simpler. I need a compiler that doesn't have 35 different ways to store variables but has just 3 or 4. The "classics", .bss, .text etc. Although it is my choice if I choose that nomeclature or not.I thought I was on to a winner when I noticed the "ASM" output it produced was "tamable" by changing just the labels that contained anything in the symbol file it might produce working code if I just lifted the 16 bytes of ASM in question, the addresses might just line up.My assembler doesn't like that format though. So I need to find, setup and use an ASM that does, then use a linker to merge the two objects into a ROM image with "dd".I am using sjasmplus for Z80 ASM. It's not "GNU ASM" compatible. That's fine. I looked at the GNU route and recoiled away. If you want opinionated ... GNU. Nothing is simple. If you want to go the "GNU Stack" route, it involves the full GNU devel stack, configure, automake, autoconf, gcc, ld and so on and so forth. It will produce "elf" outputs and your "boot.asm" will need to OBEY all that that entails if you want "ELF" binaries from a GNU C compiler to run. Including any parts of the memory mapping which it dictates and are too annoying to change. This "IS" the stack used by many MCUs today. Just a bit too complex from where I am right now.I don't think I want to go that far. Not with this project. It's perfume on a pig.I want to explore how it may have been done in the early days. Before you even got to the likes of CPM and BASIC. I realise both where mostly written in ASM, but C was around. I want the simple view from necessity to solve the problems I presently face. The next step not the 1990s solutions, the 1970s solutions.I envision a blunt simple C compiler than turns C code into Z80 ASM. When I declare a "const" it can go in "rom_data". If I declare a variable it can put it in "ram_data". If it has an initial value a two stage will be required. Someway to load "ram_data" from "rom_data" initial values at "user land boot".I don't need 48 different segments and full C99 compliance. I just want to make writing some code easier. Just simple things. A managed call structure which handles pass by register, pass by stack or pass by memory pointer. Maintains CPU state between nested calls. Auto 16bit indirect load and store so I don't have to write the ASM for them. Just the basicsWhat are my best options?Right now I am exploring z88dk which is another huge z80 dev kit project, but it attracted me as it has a "Homebrew hardware how-to" near the top of it's docs, which on a quick scan suggests it supports much simpler memory models and configurations like mine. Down side is... after an hour, I still hadn't been able to download it, install all the tools required to build it.... solutions which are larger than the project its self are hard to swallow.

author: paulca
author_rank: Super Contributor
timestamp: June 29, 2025, 11:51:09 am
content: A complication presented by "my model" is that I am targetting the MCU model. In circuit writable NOR Flash ROM.So for example:.org 0x2000user_main:Is in ROM.This alone makes my memory map incompatible with most Z80 systems where ROM boots the board, user loads program to RAM, ROM jumps to user code.The output of sdcc makes sense as 0000 will be mapped by the boot ROM via vectors to where it actually got loaded, at 0x8000 maybe.EDIT: While I ponder this I'm off to see if I can get the GPIO driver to "bit bang SPI". Maybe I can get it to talk to the outside world before I make room on the breadboard for the DART and UART. Actually bit banging UART might be easier if it wasn't for the tight timing (I have no timer support yet).

author: paulca
author_rank: Super Contributor
timestamp: June 29, 2025, 12:00:38 pm
content: As for where I am right now... this is my last test application. It tests displaying 8 and 16 bit values on the LCD.Code:user_main:; Init PIO Port B as output (Mode 0)LD A, PIO_COM_MODE_0 ; Control word: Mode 0, all bits outputOUT (PIO_COM_PORTB), A ; Device 1001 - Command + Port BLD A, 0x55OUT (PIO_DATA_PORTB), A; Point the correct ISR vector to our ISRLD A,PIO_PORTA_ISR_VCTLD DE, port_isrCALL InstallISRVectorIM 2EI; Setup port A for interruptsCALL init_port_aCALL lcd_initLD HL,str_titleCALL lcd_print_stringCALL lcd_line_twoLD HL,str_authorCALL lcd_print_stringLD BC,0x8FFFCALL delay_blockingLD D, 0x00 ; ValueLD B, 0xFF ; counter.count:CALL lcd_clear_screen_and_homeLD HL,str_t8bCALL lcd_print_stringCALL lcd_print_hex_prefixLD A, DCALL lcd_print_hex8LD A, D ; shouldnt need toCALL lcd_line_twoPUSH BCCALL lcd_print_hex_prefixLD C, DLD B, DCALL lcd_print_hex16LD BC,0x3FFFCALL delay_blockingPOP BC; DJNZ .countJR .countport_isr:IN A,(PIO_DATA_PORTA)AND 1CP 1JR NZ,.decrINC DJR .end.decr:IN A,(PIO_DATA_PORTA)AND 2CP 2JR NZ,.endDEC D.end:EIRETITwo buttons on port A. Press one it increments, press the other it decrements. Press both and it probably crashes.

author: tggzzz
author_rank: Super Contributor
timestamp: June 29, 2025, 12:16:33 pm
content: Quote from: paulca on June 29, 2025, 11:44:34 amWriting C for the Z80 is possible; I was doing it c1984.You should look at Forth; it is a halfway house between machine code and C, and is efficient w.r.t. space and time. It has an inbuilt REPL, so the "compiler" is on the board and you just use your PC to feed source code to it.

author: bingo600
author_rank: Super Contributor
timestamp: June 29, 2025, 12:44:57 pm
content: Have a look hereMaybe mostly CPM based but ...http://www.z80.eu/c-compiler.htmlI did some Z80 CPM w. "The Amsterdam Compiler Kit" , some 30+ yers agohttps://tack.sourceforge.net/Else i's expect sdcc is up for the task (embedded)https://sdcc.sourceforge.net/I have no idea about this one, but seems embedded'able too.https://github.com/z88dk/z88dkhttps://stackoverflow.com/questions/78253893/compiler-for-z80-cpu

author: brucehoult
author_rank: Super Contributor
timestamp: June 29, 2025, 12:48:02 pm
content: I think SDCC is your best bet.You can tell it where to put the code and data with a linker script or just on the command line:Code:sdcc -mz80 --no-std-crt0 -Wl-b_CODE=0x0000 -Wl-b_DATA=0x8000 -Wl-b_BSS=0x9000 -o output.ihx main.c crt0.relAs for calling conventions ... these early 8 bit micros -- and the 8086 for that matter -- didn't have any defined calling convention, it was every man for himself.People tended to just make up whatever seemed to work best for each individual function, and then documented that:memcpy(dst, src, size): DE=dst, HL=src, BC=size, clobbers AWith so few registers probably the only sensible thing, if you want a standard ABI, is to assume that everything not explicitly used for args/return is clobbered. Especially on 8080/z80 where it takes so few bytes of code to push/pop register pairs that you want to preserve just before/after calling a function, with args in registers, but accessing things in the middle of the stack is quite costly (e.g. ld hl,0xnnnn;add hl.sp;ld a,(hl))

author: iMo
author_rank: Super Contributor
timestamp: June 29, 2025, 03:47:43 pm
content: I do understand the magic temptation of many today's (younger) makers and hackers to play with the old tech like 8080/8085/8051/8088/Z80/6502/6809/68k etc. (like let us do the "retro stuff" on it) because I worked with all those chip in 80ties, BUT on the other hand I also can clearly see that the life is too short..I had a period several years back when I participated on a retroBSD projects, or when I built myself the PDP8 and PDP11 on an FPGA, but it was always about latest MIPS32 and FPGA, so at least the time spent with playing with some new technology gadgets.I burned a lot of time with Forth as well during last 15y - but always running on the "new technology" - like pic18/24/dspic33/pic32/atmegaXX/stm32/FPGA..Fazit - I would never ever consider that old tech today. Nothing new could be expected out of it - no new products could be made out of it, nothing new to learn when working with it, zero added value, just "the right level of pain"..

author: nctnico
author_rank: Super Contributor
timestamp: June 29, 2025, 03:54:04 pm
content: I'd go for SDCC when I would need to target an 8 bit like the Z80. IIRC I used SDCC for the 68HC11 or 8051 about a quarter of a century ago.

author: paulca
author_rank: Super Contributor
timestamp: June 29, 2025, 04:00:50 pm
content: I ain't young. in fact I hold a similar vintage to the Z80 itself.Back in 1982 when I insisted on reading the book on Z80 programming my Uncle left lying around, he bought me "Assembly Language for beginners".On a retro computing YouTube channel, someone surfaced with that book. All kinds of neurons spanning decades fired."You think you know how computers work? Prove it. Start back there, make one, they way they did."It is scoped though. "An arduino like MCU device". I evened started calling it "Z80-MCU-3232"So CPU, RAM, Incircuit flashable ROM, GPIO with interrupts, Timers, 2x(U)ARTs, possibly an SIO for SPI/I2C in hardware. Maybe a period 8bit resistor ladder ADC and PWM from the SIO?Anyway on that front I'm 50% there.

author: paulca
author_rank: Super Contributor
timestamp: June 29, 2025, 04:05:02 pm
content: Quote from: nctnico on June 29, 2025, 03:54:04 pmSeems it gets the recommendations.I just need to tame it then and work out how it's initialisers work etc.If I can get it part the way there I don't mind hacking binaries together with DD.The only reason I haven't properly partitioned the flash rom is that I'm lazy.

author: tggzzz
author_rank: Super Contributor
timestamp: June 29, 2025, 04:10:57 pm
content: Quote from: paulca on June 29, 2025, 04:00:50 pmThat is young.Understand the simplicity of Forth implementations, and you will see Forth is closer to assembler than C, is very simple to understand and implement, and memory efficient, and faster than BASIC.IMHO it is ideal for noodling around, peeking and poking, building software from the bottom up. If you are building a Z80 system from scratch, that is probably what you are intending to do.https://hackaday.com/2017/01/27/forth-the-hackers-language/

author: paulca
author_rank: Super Contributor
timestamp: June 29, 2025, 05:06:51 pm
content: BTW:I did manage to get SPI output today. Only 'H' ... I can work on the other characters later.Apparently 6.7kHz clock speed with a 49.2% duty cycle.That's not bad. Depending on what is receiving and how it feels, I can be more slopy with the duty cycle and double that speed maybe. (Master clock is 1.843Mhz)The target for SPI is A DS3231 RTC chip so it can be a clockEDIT:Assembler is funYou know that was almost easier than setting up an STM32 to do the same!

author: paulca
author_rank: Super Contributor
timestamp: June 29, 2025, 05:58:25 pm
content: Quote from: tggzzz on June 29, 2025, 04:10:57 pmThey had me with this....QuoteSpeaking of python, I tried to make my ROM flasher faster by sending it blocks of 2048 hex characters instead of 2 at a time.While I realise without concurrency (double buffers/ping pong etc.) it only scrubs the surface of the overheads.What I didn't expect was, as I made the blocks larger and larger, it slowed the overall process down.My little "thread heart beat" LED told me all I needed to know.Python was taking longer to generate a block of HEX than the Flash ROM was taking to program it.

author: tggzzz
author_rank: Super Contributor
timestamp: June 29, 2025, 08:37:16 pm
content: Quote from: paulca on June 29, 2025, 05:58:25 pmThat's an amusing snark, but unfair on Forth. It was designed for (by today's standards) miniscule systems. Hence it has to be simple - much simpler than an assembler or compilerI'm not a big fan of python, but will use it for small simple applications iff useful libraries are available.Forth is better close to the silicon; theres bugger all underneath it. At runtime it is simply an array of function pointers called one after another. Any function you define is itself just a list of function pointers.LISP is arguably simpler, but only if you ignore the heap and GC!

author: Analog Kid
author_rank: Super Contributor
timestamp: June 29, 2025, 09:01:12 pm
content: Just a comment from the peanut gallery here:I don't think you need to jump to C.You can do anything you need to do in assembly language: you're just running into its common limitations, primarily the small set of registers.My suggestion: come up with a scheme for parameter passing and variable storage using your registers, one that works for the specific app you're working on, and stick to it. That way you'll have an orthogonal method that you don't have to scratch your head and second-guess every time you need to call a subroutine or handle an interrupt.I've never programmed the Z80 but I do have an itch to do so before departing this life: it just seems like such a nice little machine, an unsung hero of the microprocessor world. Especially that interrupt-handling mechanism where all those registers get swapped: quite kewl.

author: nctnico
author_rank: Super Contributor
timestamp: June 29, 2025, 09:18:58 pm
content: Well, having done quite a bit of Z80 assembly back in the day (I even build a frequency counter driven by a self designed Z80 processor board at some point), one of my regrets is not to have switched to C back then. C makes life so much easier. Who cares about the registers. Let the compiler deal with it. Especially with decent and free compilers (like SDCC) available nowadays.

author: tggzzz
author_rank: Super Contributor
timestamp: June 29, 2025, 09:28:03 pm
content: The Z80 is not a nice machine; it is vile compared to a 6809 or similar, for many reasons.To give you an idea, look at the instruction map for a 680x: orthogonal, complete (mostly!). Now look at the Z80 equivalent: incomplete, sparse, messy.Try, for example to use a Z80 index register to point to a strict representative noting a node in a linked list, where the link to the next node starts at addresses 0x2 in the struct. Now try to move the index register to point to the next node; it is surprisingly messy and slow. Much better to forget the IX register and just use the 8080 registers!Try to do i/o to an i/o address/port defined at runtime.The Z80 looked good to naive hardware engineers who thought, for example, it was neat to be able to print a string with a single instruction!

author: Analog Kid
author_rank: Super Contributor
timestamp: June 29, 2025, 09:32:31 pm
content: So then I wonder why the OP chose the Z80 to program if it's as bad as you say it is.Apparently they weren't forced into that decision at gunpoint.I still wanna program it ... all of those deficiencies you point out are just challenges to be overcome. That's the fun of it.

author: nctnico
author_rank: Super Contributor
timestamp: June 29, 2025, 09:33:14 pm
content: Quote from: tggzzz on June 29, 2025, 09:28:03 pmI think you got the wrong processor in mind. Z80 allows to load input/output from the I/O space using the C register as the index.And getting the address of the next element of a linked list pointed to by IX into IX is not difficult. Something like:LD B, (IX+2)LD C, (IX+3)PUSH BCPOP IXI don't see how this is messy. Remember is it an 8 bit machine. PS: I didn't check big / little endian address order.

author: tggzzz
author_rank: Super Contributor
timestamp: June 29, 2025, 09:48:15 pm
content: Quote from: nctnico on June 29, 2025, 09:33:14 pmJust looked at my Mostek Z80 manual, which I haven't used in 40 years, and it seems you are... right.So what happened? Not sure, but possibly the Whitesmiths compiler wasn't up to that or was using an 8080 subset. I definitely remember seeing the in/out instruction being constructed on the stack, and then executed. Oh well.

author: nctnico
author_rank: Super Contributor
timestamp: June 29, 2025, 10:01:55 pm
content: Some of the older compilers where really crappy. A long time ago I spend money on a 8051 C compiler (IIRC sold / made by Dave Dunfield) which was utter crap. Even back then SDCC produced much better code.

author: tggzzz
author_rank: Super Contributor
timestamp: June 29, 2025, 10:10:55 pm
content: One of the attractions of the Whitesmiths compiler to more experienced people in my company was that it targetted many processors and could run on a PDP11. I suspect there weren't many of those c1983.Your index register linked list chaining looks about right.Fiveinstructions including surplus moves through the stack. Given everyone was.raving a about the improvements in the Z80, I was horrified when I first worked that one out.The earlier 6800 did it inoneinstruction: LDX loaded both bytes from successive memory locations, and it could use indexed mode with an offset. The mnemonic would have been something likeLDX 2,XBit nicer in all respects than the Z80LD B, (IX+2)LD C, (IX+3)PUSH BCPOP IXAnd the 6809 was even nicer

author: brucehoult
author_rank: Super Contributor
timestamp: June 29, 2025, 10:51:10 pm
content: Quote from: Analog Kid on June 29, 2025, 09:01:12 pmThe problem is there is no way to know which register set is active. And while some systems may reserve one set for interrupt handlers, you can equally well find examples of application code using both sets -- the fastest 32 bit mul/div and soft float routines use both sets, as do many graphics routines. You can't do that and use them for fast interrupts too -- and interrupts are usually so infrequent that it's much more important to make normal application code as fast as possible. Adding a buffer/queue to fast I/O devices is usually the better way to go.Also of courseex af,af',ex de,hl, andexx(swaps bc, de, hl with their shadows) don't actually copy any data, they just toggle flip-flops on one or more inputs of XORs on different register address lines.

author: Analog Kid
author_rank: Super Contributor
timestamp: June 29, 2025, 11:11:24 pm
content: Yes, I'm sure I'd have a pretty steep learning curve to start programming that chip.So is there really no way to know which set of shadow registers is active? No tricky way of reading/writing those registers to determine which? That would certainly be a problem ...

author: Benta
author_rank: Super Contributor
timestamp: June 29, 2025, 11:19:12 pm
content: @paulca, if you want to go really retro in this, you need to port CP/M as operating system, and use PL/M-80 as programming language. A crosscomplier is available: PLMX.Cheers.

author: brucehoult
author_rank: Super Contributor
timestamp: June 30, 2025, 12:02:36 am
content: Quote from: tggzzz on June 29, 2025, 10:10:55 pmYes, the 6800 is much better with pointer-chasing code. But it falls down badly with anything that needs multiple pointers, especially pointers that need updating, such as a memcpy(). You end up with code like...Code:LDX srcLDA 0,XINXSTX srcLDX dstSTA 0,XINXSTX dstDEC szBNE loop19 bytes, 46 cycles per loopSticking to 8080 instructions, the straightforward way on Z80 (there are faster ways) is...Code:LD A,(HL)LD (DE),AINC HLINC DEDEC BCLD A,BOR CJP NZ,loop9 bytes, 50 cycles per loopThe z80 code is less than half the size 9 bytes vs 19. 6800 can save 2 bytes for copies of less than 256 bytes by using "DEC B" for the count ... but Z80 also saves 2 bytes (and 2 instructions) by using DEC B.6502 more or less splits the difference.Load link pointer at offset 2:Code:LDY #2LDA (ptr),YTAXINYLDA (ptr),YSTX ptrSTA ptr+1Quite a few more bytes of code than Z80 but omg the Z80 takes 63 cycles for 4 instructions while the 6502 is 22 cycles for 7 instructions, fitting the usual pattern of Z80 using 3x more cycles than 6502. 6800 of course wins here with 6 cycles.For 6502 memcpy:Code:LDA (src),YSTA (dst),YDEYBNE loop7 bytes, 16 cycles. That's three times faster than either 6800 (46 cycles) or Z80 (50 cycles).It does however only handle maximum 256 bytes copied. For larger copies you need another loop around this one, which brings the code size to larger than 6800, but doesn't affect the speed (much). In fact usually two loops are used, one for blocks of 256 bytes then another for the final bytes.The 6809 of course solves all problems, with four 16 bit index registers (two of which also support push/pop) and a 16 bit D accumulator.Code:LDA ,X+STA ,Y+LEAU -1,UBNE loop8 bytes and I think 20 cycles per loop. Actually slightly bigger and slower than the 6502 inner loop, but handles bigger than 256 bytes.You can also do 2 bytes at a time with...Code:LDD ,X++STD ,Y++LEAU -2,UBNE loop... if you deal with a possible odd byte first.

author: brucehoult
author_rank: Super Contributor
timestamp: June 30, 2025, 12:06:33 am
content: Quote from: Analog Kid on June 29, 2025, 11:11:24 pmAbsolutely no way. I mean ... only wasting one of the six 8 bit registers to hold a magic value e.g. make B=0 and B'=1 and test B and swap if you have the wrong one. But no one ever does that. You juust have to keep track mentally, the same way you make sure malloc() and free() are properly paired in C.

author: Analog Kid
author_rank: Super Contributor
timestamp: June 30, 2025, 12:18:29 am
content: Quote from: brucehoult on June 30, 2025, 12:06:33 amSo a simple 1-byte flag variable (or even just a bit in a byte) would take care of tracking that. Doesn't sound too bad.

author: brucehoult
author_rank: Super Contributor
timestamp: June 30, 2025, 12:37:09 am
content: Quote from: Analog Kid on June 30, 2025, 12:18:29 amWell if you're ok with going from 6 bytes of B,C,D,E,H,L registers to 10 instead of 12. Or, going from 3 register pairs to 4.If you really wanted to go that route you'd be better off using a RAM byte as the flag.

author: Analog Kid
author_rank: Super Contributor
timestamp: June 30, 2025, 12:57:25 am
content: Quote from: brucehoult on June 30, 2025, 12:37:09 amThat's what I meant. Should have made that clear.

author: SiliconWizard
author_rank: Super Contributor
timestamp: June 30, 2025, 01:39:30 am
content: The Z80 has LDIR for memcpy. A single instruction, but 21 cycles per copied byte. Still usually faster than with a manual loop, unless you optimize the heck out of it (there were ways to copy data faster than LDIR, but that wasn't immediately obvious).

author: brucehoult
author_rank: Super Contributor
timestamp: June 30, 2025, 03:02:18 am
content: Quote from: SiliconWizard on June 30, 2025, 01:39:30 amIt does, but it's ONLY for memcpy. Sure, memcpy is important, but there are any number of similar operations that have code very little different to that shown above when done on 8080, 6800, 6502, 6809, but have no LDIR equivalent e.g. the rest of the <string.h> functions. Also things likefor( ... ) a= (b& m) | (c& n)which you can take quite complex on Z80 using IX and IY and the alternate BC, DE, HL and can take almost arbitrarily complex on 6502 with 128 ZP pointers available.6502 can get under 10 cycles per byte on 256*N byte blocks using something like ...Code:outer:ldy #63loop:lda src,ysta dst,ylda src+64,ysta dst+64,ylda src+128,ysta dst+128,ylda src+192,ysta dst+192,ydeybpl loopdexbeq doneinc loop+2 // 5 cycles each in ZP, 6 absinc loop+5inc loop+8inc loop+11inc loop+14inc loop+17inc loop+20inc loop+23jmp outerdone:... plus of course initial setup of the 4 src and 4 dst pointers in the self-modifying code.In case of misalignment, note that the loads take an extra cycle if a page boundary is crossed by the indexing, but stores don't, so make sure src is at least 64 byte aligned using an initial byte by byte loop.The outer loop adds less than 0.2 cycles/byte overhead to the 10.25 cycles/byte of the inner loop, total about 10.45. Copying 8 bytes per inner loop lowers that to about 9.98 cycles/byte and 16 bytes per inner loop is about the same.

author: tggzzz
author_rank: Super Contributor
timestamp: June 30, 2025, 06:59:23 am
content: bruceholt brings up a number of useful points, which I'll coalesce here.Back then different architectures are indeed better for different programming styles. IIRC the GEC4080 family was quite good for "Fortran style" coding based on indexing into arrays, but suboptimal fo "C style" pointer chasing. My coding style, even in the mid-late 70s before I knew C existed, seemed to have more pointer chasing and no array copying.Nowadays architectures are much better at the instruction level, with the pain points moving to other parts of the system such as comms, scheduling and caches.I've always taken the "number of cycles" metric with a very big pinch of salt, since different processors had a very different number of clock cycles in an instruction. From memory 6800 was a 1MHz clock and the Z80 was a 4MHz clock, so if the Z80 took four times as many clocks then it was an irrelevant difference. (IIRC the 1802 was even more extreme in that respect)The 6802's "zero page is special" and "short addressing modes" always put me off it. I'm told that one attitude is to regard zero page as being registers, but I've never had the reason to test that.The 6809 is indeedveryclean; I've always mildly regretted not having a reason to use it.The "LDIR for memcpy but only memcpy" fits in with my attitude to the Z80. It looks good on paper, but when you try to use it in your system the special case never seems to quite fit what you need to do. Consequently you are forced back on the core 8080 "RISC" instructions.

author: paulca
author_rank: Super Contributor
timestamp: June 30, 2025, 08:09:19 am
content: Quote from: nctnico on June 29, 2025, 09:33:14 pmAh, ha! So when you are looking through the 16bit load group and you can't find a 16bit register swap instructions... just use the stack.I never thought you can pop registers into other registers. Dumb.

author: brucehoult
author_rank: Super Contributor
timestamp: June 30, 2025, 08:44:40 am
content: Quote from: paulca on June 30, 2025, 08:09:19 amIt looks convenient, and only two bytes of code but 25 clock cycles.For 4 bytes of code but only 16 clock cycles you can do:Code:LD IXH,BLD IXL,CPick your poison.QuoteBytes are just bytes.

author: Analog Kid
author_rank: Super Contributor
timestamp: June 30, 2025, 08:49:10 am
content: So OP: will it be asm or C?

author: paulca
author_rank: Super Contributor
timestamp: June 30, 2025, 08:57:39 am
content: Quote from: Analog Kid on June 29, 2025, 09:01:12 pmYea. I mean there is mileage. I just hit a steep slope when I started to run out of registers. The first "pass" you can track a couple in your head easily enough. When you run out of the ones you can track easily things start to get much more "bug prone". Long 15-20min debug cycles almost entirely down to "Ok,who modified what?"An example:Code:LD A,DCALL lcd_print_hex8INC A ;; ERRRRR!!! Nope! A is corrupted.LD D, AThat one took me far too long to spot. I was dilligently putting A into D which was safe, but then school boyed it. Even though the function comment header of that routine (I wrote), clearly says, "Mutates A!"I ended up locking it down at both ends. I restore A in the sub-routine AND I don't rely on it in the callee. I had to point out to myself this is Z80 ASM, not Enterprise Java. Do one and remember, not both so you can forget.The thing is, I have a whole new level to just step up to. Variables! Remember those? LOLI just need to start putting labels down andLD A,(VAR_ITS_NAME)so stuffLD (VAR_ITS_NAME), ADefinitely slower than a register swap, but I don't even think it's longer than a PUSH POP.

author: paulca
author_rank: Super Contributor
timestamp: June 30, 2025, 09:05:30 am
content: Quote from: Analog Kid on June 30, 2025, 08:49:10 amBoth. What I am going to do is go and look at what the C compiler does or rather look for sub-set of techniques.Where I expect that to go and just like my prior post, is more verbose code, more diligent code, slower code. Just like what the C compiler would produce.If I formulate (or borrow from a C compiler) techniques and even though they may not always be the most efficient, if I stick to them, then i know what to expect, at least from my own code.I will be using sdcc and trying to tame it into my memory model.Thinking now, you know, I'm probably lucky I didn't go "Harvard" now.EDIT: ON sdcc. I had a late night, nearly asleep epiphany.All I need to move is BASE and BSS (or whichever symbol is the floor of writable space).It's the base that has been the issue. It starts at 0000 in ROM page 0. Forbidden.If I just set that to 0x2000 it will land above my "System ROM" and has "user_land" all the way to the very top of RAM where it will run into the stack long before it ends up in the Interrupt Vector tables.I just need to convince it to stay above 0x2000.The next challenge then will be linking symbols between the sjasmplus system ROM and sdcc produced "user_land" code. I could possibly do it maunally, It's not like there will be more than 100 rom routines.EDIT: I could port the sjasmplus to whatever asm sdcc prefers.... if I have to. It might make things easier, while it's small. Python can help.

author: tggzzz
author_rank: Super Contributor
timestamp: June 30, 2025, 09:16:25 am
content: Quote from: paulca on June 30, 2025, 08:57:39 amLargely disappears if you use Forth: all variables are on the top of the Forth stack. You only use the processor registers to execute the Forth program words, which is much simpler.Of course, every solution has its own downsides

author: brucehoult
author_rank: Super Contributor
timestamp: June 30, 2025, 10:51:24 am
content: Quote from: tggzzz on June 30, 2025, 09:16:25 amYou don't actually have to have a Forth on 8080/z80, you can just program in that style by yourself, using the native instructions.For example, keeping ToS in HL and dealing only with 16 bit values:<num> PUSH HL; LD HL,num1+ INC HL1- DEC HL+ EX DE,HL; POP HL; ADD HL,DE- EX DE,HL; POP HL; XOR A; SUB HL,DEDROP POP HLDUP PUSH HLSWAP EX (SP),HL@ LD A,(HL); INC HL; LD H,(HL); LD L,A! EX DE,HL; POP HL; LD (HL),E; INC HL; LD (HL),D; POP HLThe only thing, really, is that your return stack is mixed in with your evaluation stack.

author: tggzzz
author_rank: Super Contributor
timestamp: June 30, 2025, 11:10:41 am
content: Quote from: brucehoult on June 30, 2025, 10:51:24 amVery true. The only reason for not mentioning it was that the OP is interested in escaping from ASM into a higher level language.QuoteTraditionally Forth has two independent stacks: the Data stack and the Return stack.Without bothering to think about it, I would hope that those two stack pointers could be kept in HL and DE or IX and IY. That would leave the traditional 8080 SP stack for traditional uses.(Aside: one reason I chose not to make an 1802 computer was that too many separate instructions were required for traditional subroutine JSR/RET calls. Algol60 spoiled me, and I didn't appreciate the thought of alternative execution strategies)

author: guenthert
author_rank: Frequent Contributor
timestamp: July 01, 2025, 03:03:04 pm
content: So how retro does it need to be? Using tools which were available at that time or more recent ones? Z80 is an awkward architecture for C. sdcc might be your best bet, if 'modern' tools are allowed -- I have no experience with it and fortunately no need to make such. SmallC was available in the 80s, but that one is best to be avoided. I always had a weak spot for FORTH, but arguably the best programming environment then for the Z80 was Turbo Pascal.If I were to program 8bit computers again, I might be tempted to try Cowgol:https://cowlark.com/cowgol/

author: Benta
author_rank: Super Contributor
timestamp: July 01, 2025, 08:52:42 pm
content: Quote from: guenthert on July 01, 2025, 03:03:04 pmWow! A walk down memory lane.I'd forgotten about the Hejlsberg compilers, but used them all back then:ComPas, PolyPascal and TurboPascal. Fastest compilers and executables I've ever worked with on CP/M and early MS-DOS machines.But IIRC, they only run as native compilers, so you need a running CP/M-80 machine to generate code.

author: nfmax
author_rank: Super Contributor
timestamp: July 01, 2025, 09:19:24 pm
content: You can run CP/M 80 software on a modern computer and operating system using tnylpo -https://gitlab.com/gbrein/tnylpo

author: gcewing
author_rank: Regular Contributor
timestamp: July 05, 2025, 11:45:41 pm
content: Quote from: paulca on June 29, 2025, 11:44:34 amYou're overthinking this. Back when I was doing extensive Z80 programming, I adopted a very simple convention: callee preserves anything it uses. Anything else would have been a threat to my sanity.QuoteIMO, no. Either the caller assumes that the callee destroys everything, which could be just as wasteful, or implementation details of the callee become part of its contract with the caller, so that if the callee is changed in such a way that it uses more registers, you have to track down all the callers and update them. It could also result in larger code, because saving and restoring is being done at every call site instead of once in the callee.Better, I think, to settle on the simplest, least error-prone convention for most of your code, and only do that kind of micro-optimisation for the small proportion of code that really needs it.

author: paulca
author_rank: Super Contributor
timestamp: July 10, 2025, 12:44:47 pm
content: So for now I have migrated to a different style.Just use memory. It might not be a beautiful example of optimisation, but I just want working code with less bugs, I can optimise later once I know the concept works.The code isn't tested but it's the end of quite a long state machine for a "RAM Loader" via the PIO port (for now).The concept was working in that I could read and display a counter sent by the MCU, but the full state machine is still to test. Part of me is not looking forward to it and part of me is considering revisiting the slow clock and bus monitor apparatus.

author: Doctorandus_P
author_rank: Super Contributor
timestamp: July 23, 2025, 07:08:47 am
content: Quote from: tggzzz on June 29, 2025, 12:16:33 pmThat may be so, but OP is trying to move away from ASM.I do like my HP RPN calculator, but find something written in RPN extremely difficult to comprehend afterward.C is pretty much the standard for microcontroller programming, and I've always liked the language, but it did take a few weeks to get the hang of the syntax. Up to now I only write software for which GCC is available (CLANG is apparently another good C / C++ compiler, but I just stuck with GCC). If those compilers are not available for your target, then SDCC would also be my next choice. From what I've read it's a well regarded compiler and has grown quite mature over the years. I once used it myself to compile the code for the ubiquitous EUR10 Logic analyzer (8051 based controller, code part of Sigrok / Pulseview) but I did not write code in it myself.

author: paulca
author_rank: Super Contributor
timestamp: July 23, 2025, 01:02:28 pm
content: From further reflection. The problem with the C compiler is to do it's job it has to know a lot about your architecture.By default it seems to lean towards a flat RAM only memory model by default. Stuff starts at 0x0000 and only goes elsewhere if you explicitly map it there.The next level of config is not in mapping everything in or out of ROM/RAM areas, it's mapping it all into a "relocatable" or "fixed" offset using:LOAD ""(or a more raw variant loader).ROM boots. User "loads application code to vector, jumps to vector".This is the next most common architecture of the day.For these two models sdcc might be far more "tameable" when I get back to it.Originally I was jumping straight in with both feed to what happens in a modern MCU build chain with split memory segmentation models of SRAM, FlashROM etc. Ran head first into a wall instead of staying crawling and sneak under it.

author: paulca
author_rank: Super Contributor
timestamp: July 23, 2025, 01:08:15 pm
content: On a different note, without leaving ASM, I just got more confident and more adventurous.When it came to parsings command-line I didn't let myself think, "Oh shit, string utils!", I just started writing them. Turns out you can get quite "nifty" in ASM with strings... and even some high-level architecture sneaking in from my roots by making my str_util routines chainable. I use the flag register to signal sucess failure and so receive in A and return in A. The pointers passed in HL get returned in HL correct advanced and the length in BC gets returned in BC set to the remaining length.The most surprising thing is they work.The more annoying and harder to fix issue is UART overruns. Oh I hear you fancy modern day MCU devs nodding, but serious, you have DMA and a FIFO you have NO EXCUSE. I have one byte, If I dont read it before the next frame starts I loose one.The irony ... if I don't look at the OVRN flag I only drop 1 byte out of 10,000s. If I check the OVRN flag I drop 1 in a 100.

author: Peabody
author_rank: Super Contributor
timestamp: July 23, 2025, 01:54:36 pm
content: Quote from: paulca on July 23, 2025, 01:08:15 pmCan you generate an interrupt when a byte has been received? Then the ISR could read in the byte and store it in a circular buffer.

author: paulca
author_rank: Super Contributor
timestamp: July 23, 2025, 05:59:52 pm
content: Quote from: Peabody on July 23, 2025, 01:54:36 pmAt 9600 baud and 1.8Mhz I doubt it. That's the catch. You would think, lets use a FIFO, but the time and instructions pushing and popping the FIFO are in the critical path as well.What I did was shorten the protocol I was using to 3 byte bursts. I hot loop the CPU on "char ready", consume 3 then go and "parse them", then I send "ok" and let the other end send 3 more. So in the critical path are basically a memory write and a pointer advance, check if newline.... and it will still drop the odd byte.If you went down to a single byte you probably wouldn't lose any.

author: Analog Kid
author_rank: Super Contributor
timestamp: July 23, 2025, 06:22:10 pm
content: Quote from: paulca on July 23, 2025, 01:08:15 pmYes you can. Welcome to the wide, wild world of ASM.Suggestion: To parse text, rather than using a bunch of string-decomposition functions (instr(), substr()and friends), consider doing what I do:Implement a FSA (finite state automaton). Actually very easy to do in any assembly language. Create a state table with pointers to stub routines. Process the text to be parsed character by character. At each row of the table, you jump to the routine corresponding to the character just seen.*(You can even jump based on classes of characters, like numeric digits.)Works like a charm, is generally smaller than iterative code, and is very easy to tweak because it's data-driven.The first requirement here is to draw out the parsing (actually tokenizing) sequenceon paper. The code & data then follow directly from that diagram. It's like magic.*This part of the scheme is helped immensely by the use of theREP SCASBx86 instruction, which you lack in your instruction set. You'll have to code this up discretely, but that shouldn't be too hard: just construct a list of characters to be used for comparison (all your tokenizing chars.) and loop over that list until you find a match to the current character (or don't).

author: paulca
author_rank: Super Contributor
timestamp: July 23, 2025, 07:32:56 pm
content: I only wrote what I needed. Keyword and hex. In pairs, str_scan_hex (and it's aggregates, hex8, hex16) with str_print_hex. No "greedy" operators, even str_cmp takes a length.The Z80 may have served it's purpose, a stepping stone, I have a few more things with it, then I might move onward to the 68000.Thinking about the UART rate. To get 9600 baud I divide the clock by 192. So in theory I should have something like 1920 clock cycles per byte/frame.The Z80 averages something like 4-8 clocks per instruction with some taking way longer, but "in theory" you should be able to shift bytes easily fast enough with a ~240 instruction budget.So maybe my byte loss cause is else where.When I upload a program of maybe 300bytes in the format>AF\n<ok\n>A9\n<ok\nIt will occasionally miss the \n and stall the state machine. It's easy to reset with a further character which will reset the state machine, but it happens maybe 1 in ten timesI suppose with old NMOS chips it could be noise or anything tripping it out.1 in ten times sounds awfully familar to loading stuff on tape back in the day.

author: Peabody
author_rank: Super Contributor
timestamp: July 23, 2025, 09:42:03 pm
content: Sorry, I haven't been following closely enough. Are you bit-banging, or do you have an actual UART chip? Which chip?How about a a lower baud rate - 4800, or even 2400?

author: woofy
author_rank: Frequent Contributor
timestamp: July 24, 2025, 08:11:25 am
content: Quote from: paulca on July 23, 2025, 07:32:56 pmI felt the same a while back, so I built this:https://hackaday.io/project/177988-68k-mbc-a-3-ics-68008-homebrew-computerHowever it was "just another processor" without the magic that came with the Z80. My first ever computer was a Nascom1 built from a kit. I did nothing useful with it but had a huge amount of fun doing so. I programmed it in assembler (hand assembly on paper to start with) and grew to love the Z80. Nothing since has come close to the awe and wonder of those days.I guess nostalgia isn't what it used to be.

author: paulca
author_rank: Super Contributor
timestamp: July 24, 2025, 08:19:10 am
content: Quote from: Peabody on July 23, 2025, 09:42:03 pmIt's a period Z80 DART IC.It's not a big deal a byte dropping here and there. It will just incentivise me to write a timeout.

author: paulca
author_rank: Super Contributor
timestamp: July 24, 2025, 08:50:57 am
content: Quote from: woofy on July 24, 2025, 08:11:25 amIt's not nostalgia I was after really. It just so happens the two CPUs I used as a kid where the Z80 and the 68000.The last thing on the plan for the Z80 is a 3v3 IO Playground. Bringing the board stack to:Core CPU + IOToyBoard (period), 3v3IOPlayground (modern).The 3v3IOPlayground is looking like a first version with just level shifters and a socket for an Ice40 FPGA "Upduino", itself with 8 IO. The FPGA is there to open the door in the floor to go "down" if I wish. Starting with replicating the period Mode 2 interface and maybe a custom UART/SPI/I2C affair, or if I get a bit bogged down I can switch to the IP Blocks for the same in the FPGA.There is temptation to respin both of the first two boards with upgrades and move the FPGA and it's shifters to the CPU core board so I can do "MAXED out Z80" with stupid memory paging etc.It shouldn't be wasted, because I think the 68000 is probably capable of where I want to play next. Fully abstracting the memory away from the CPU, via a bus master controller which owns the memory and the CPU becomes a client.The Z80 playground with the FPGA or re-doing the Z80 CPU core board to do simple memory paging would be a brilliant pre-cursor learning venture before tackling some more involved with TLBs and page faults.I'm tempted to ask about the PIC, but then I decided.... I don't want to know. No spoilers!

author: Analog Kid
author_rank: Super Contributor
timestamp: July 24, 2025, 08:55:43 pm
content: Quote from: woofy on July 24, 2025, 08:11:25 amTook a look at that. Very interesting.But weird: why a PIC in addition to the 68000?Couldn't the guy figure out how to make a computer with just the 68K and some glue logic?Or would the extra chips have been too much of a hassle?

author: brucehoult
author_rank: Super Contributor
timestamp: July 25, 2025, 01:26:40 am
content: Quote from: paulca on July 23, 2025, 05:59:52 pmReally?I guess the interrupt routine will be something like:Code:PUSH AF ; 11 T-statesPUSH HL ; 11 T-statesIN A, (UART_STATUS) ; 11 T-statesBIT 0, A ; 8 T-statesJR Z, INT_EXIT ; 7 T-states (not taken, data present)IN A, (UART_DATA) ; 11 T-statesLD HL, (BUFFER_PTR) ; 16 T-statesLD (HL), A ; 7 T-statesINC HL ; 6 T-statesLD (BUFFER_PTR), HL ; 16 T-statesPOP HL ; 10 T-statesPOP AF ; 10 T-statesEI ; 4 T-statesRETI ; 14 T-statesThat's 125 clocks. Plus 13 to enter the interrupt handler (mode 0 or 1) and 4-23 to wait for the previous instruction to finish. So 161 total.But that's not wrapping the buffer pointer. Ok, Make the buffer 256 bytes, aligned. So just use INC L instead of INC HL. It's even 2 cycles quicker. LD (BUFFER_PTR), L would knock 6 more cycles. So 153 total. 38.25µs at 4 MHz.At 9600 BPS the bit time is 104µs and the byte time (8N1) is 1042µs.The thing is more than fast enough to use a simple GPIO and interrupt on every bit!! Let alone using a UART that buffers a character while it's receiving the next character. Even a '595 shift register can do that if someone tells it when to clock the just-received character into the output latches.In fast it looks fast enough (just!) to do the 230.4 kbit/s speed Apple used with the Zilog SCC, purely interrupt-driven.Oh wait, you said 1.8 MHz not 4 MHz. So the 38.25µS becomes 85µs. It's still fine.

author: paulca
author_rank: Super Contributor
timestamp: July 25, 2025, 08:44:51 am
content: Code:; Expects str_buffer write pointer in HL; Expect max len in BC; Returns HL = next free; BC = buffer remaining; On error or empty read:; HL = unchanged; BC = ignoredart_a_read_str:PUSH HL ; save the buffer start addr.loop:; char = getChar();CALL dart_a_rx_byte_blocking; immediately check error flags; CALL dart_a_rx_error_flags; JR NZ, .bad_entry; *write_ptr = char;LD (HL),A; if ( char=='\n' || char =='\r' ){ end_entry(); }CP 0x0D ; carriage returnJR Z, .end_entryCP 0x0A ; carriage new lineJR Z, .end_entry; save ALD D,A; if length2go == 0DEC BCLD A,BOR CJR Z, .bad_entry; next char, restore A (the input), echo; This is down here so we don't echo the CR/LF nor increment past itINC HLLD A,DCALL dart_a_tx_byte_blockingJR .loop ; while(true)And the inner wait loop:Code:dart_a_rx_byte_blocking:.wait_rx_ready:IN A, (DART_COMM_CHANNELA) ; status registerBIT DART_REG_RD0_RXCHAR_AVAILABLE_BIT, AJR Z, .wait_rx_readyIN A, (DART_DATA_CHANNELA)RETI expect it might be more to do with signal integ maybe. While everything else is clean the UART lines have a wide blur of noise onthem on the scope.The other part which isn't uS long isSTOP->STARTWith a 2020 FTDI Quad UART blasting at it, it will not pause a microsecond between frames. The DART chip is not expecting that.So I might try adding a second stop bit.https://gitlab.com/paulcz80/z80_asm/-/blob/master/os_design/dart_driver.asm?ref_type=headsEDIT:I see it.CALL dart_a_tx_byte_blockingTx and Rx run at the same baud clock. So the fact I enter into a "TX Wait loop" for the echo is spending ALL my budget.This could possible by converted from:CALL dart_a_tx_byte_blockingToCALL dart_a_tx_byte_unblockingWhere the byte to Tx is placed somewhere, interrupts turn on and an ISR actually sends it when it can.Further edit: There was a "I'll do that later" with this code, "ECHO" is meant to be configurable and DISABLED during a data transfer.If I had implemented that, there wouldn't have been an issue. With echo still enabled, it causes a lot of waste with TX wait loops and the other end (Python) has to keep discarding the echos.

author: paulca
author_rank: Super Contributor
timestamp: July 25, 2025, 09:20:32 am
content: As always though. The "gotcha" with ISRs. I have been afraid to do this for example:isr:IN A,(SOME_REGISTER); some more processingI have written code like that, but it has never survived the first debugging pass of non-working code before being the first to get removed and split between ISR and main thread via signalling.It is especially dangerous as while you are in the ISR and until you call "RETI" the mode 2 peripherals will still consider themselves "in interrupt service". I expect they are not fully functional in that state and reading writing registers on them may be unwelcome. You are also holding the daisy chain state for other peripherals to interrupt... and if they are up the queue they are permitted to nest.To split Rx and Tx into interrupts that means the main thread has to fork based on the interrupt signal.; consider DJNZ for timeout in BC.loop:; check Rx ready ISR signalJP .NZ rx_routine; check Tx ready ISR signalJP .NZ tx_routineJR .loop

author: DC1MC
author_rank: Super Contributor
timestamp: July 25, 2025, 09:25:23 am
content: I'll just leave this stuff here:"UZIX is a UNIX Implementation for MSX machines, based on UZI written by Douglas Braun. It is a multiuser, multitask operating system, implementing almost all 7th Edition AT&T UNIX kernel functionality."https://sourceforge.net/projects/uzix/https://www.hartetechnologies.com/manuals/Cromemco/Cromemco%20Cromix%20System%20Administrators%20Manual.pdfhttps://github.com/udo-munk/z80pack/blob/master/cromemcosim/cromixhttps://manx-docs.org/mirror/harte/Cromemco/Cheers,DC1MC

author: brucehoult
author_rank: Super Contributor
timestamp: July 25, 2025, 10:26:15 am
content: Quote from: paulca on July 25, 2025, 09:20:32 amThis is not interrupts, it is polling. That is how you lose input characters if the main loop does any significant additional work.

author: paulca
author_rank: Super Contributor
timestamp: July 25, 2025, 11:18:54 am
content: Quote from: brucehoult on July 25, 2025, 10:26:15 amThe main loop is doing nothing. That IS the main loop.It could be rewritten without interrupts as:READ status registerIF rx_ready:do_rxIF tx_ready:do_txCALL do_something_elseLOOPdo_something_else:// There is little to no point in being here, we have sweat FA time to do anything useful.Right or wrong, I fail to see the point of interrupts for tightly coupled high flow streams. There just isn't enough time to do anything useful other than track the flow state and handle errors and terminators.Interrupts are for when waiting would be stupid. Like waiting on human input, if you have something else to do, be it flash the cursor.Or updating the SYS_TICK if you want that luxury, then it makes sense to be "interrupted" as you can't tell when it will be only the timer knows.If you look at similar routines in ZXSpectrum land or in CPM. The most you will get is fancy borders updated or maybe a prompt flashing after you execute a large IO op. eg LOAD "" Your interaction options are "BREAK" or wait.

author: tggzzz
author_rank: Super Contributor
timestamp: July 25, 2025, 11:32:30 am
content: Quote from: brucehoult on July 25, 2025, 10:26:15 amPrecisely.As you say, the sane way to do it is the ISR puts the character into a FIFO (e.g. implemented as a circular buffer), and the main loop reads the bufferwhen it gets around to it- and then processes the character.That works well forburstsof characters. Naturally if theaveragetime to process a character is longer than the inter-character arrival time, you are screwed and there is nothing you can do about it.For transmitting characters, that ISR tells the main loop another character can be sent either via a different FIFO or by putting a magic flag into the FIFO. Alternatively the main loop simply polls the UART to see if its tx buffer is empty.

author: paulca
author_rank: Super Contributor
timestamp: July 25, 2025, 11:44:08 am
content: In my case. I receive 3 bytes. Parse them, receive 3 more.If the other end is not waiting, I will miss because it takes me longer to parse 2 bytes of ASCII into hex than it does to receive a character, apparently.I will try the FIFO. However, I am 100% sure I cannot parse ASCII Hex as fast as an FTDI can send it.EDIT:It does raise the question, why not just send raw bytes. The only answer I have is because it's fairly standard to send ASCII hex as it's "safe" and you can intervene or manually type it.If I made the data transfer more specific, or used the other UART channel dedicated to it, that might help me remove that need to parse entirely and I may possibly be able to keep up with the FIFO.

author: paulca
author_rank: Super Contributor
timestamp: July 25, 2025, 11:50:15 am
content: To spice it up even more. I don't think I even want the UART loader. I already have a functioning (or had until I dismantle the breadboard) "parallel IO" loaded with an STM32, using the PIO dedicated signallying READY/STROBE.It was significantly faster and easier.

author: tggzzz
author_rank: Super Contributor
timestamp: July 25, 2025, 01:18:22 pm
content: Quote from: paulca on July 25, 2025, 11:44:08 amAverage or peak?A FIFO can help with one but not the other.

author: paulca
author_rank: Super Contributor
timestamp: July 25, 2025, 02:11:50 pm
content: Quote from: tggzzz on July 25, 2025, 01:18:22 pmAverage. It's a loader, which is why, if you think about it, a FIFO or any buffer isn't really going to help, except with the parsing. It's job is to copy things to memory anyway.A FIFO into the parsing routine of a fixed length.Upstream can then send "FIFO.length / 2" burst.Interrupt clocks bytes into FIFO.When FIFO.not_empty it can signal the parsing routine to begin.It can also tell the up steam to send another burst.Then we ping pong around the half FIFO, consume half, ask for another burst until done.

author: paulca
author_rank: Super Contributor
timestamp: July 25, 2025, 02:20:12 pm
content: Even if I don't use the UART for "loading programs", it's other task I had planned was to integration with an ESP32 Comms module in default "AT command mode".If I ask it to make a REST or Web request for me, it's going to dump whatever comes back over UART. I don't believe it has a back signal. It will send 2048 byte "lines" of content until it's done.So not large requests from the Z80. Even at that, it's going to arrive in a burst. I need the code to be able to handle a sensible burst size that isn't 3 bytes.

author: tggzzz
author_rank: Super Contributor
timestamp: July 25, 2025, 03:10:10 pm
content: Quote from: paulca on July 25, 2025, 02:11:50 pmThen you have to reduce the amount of data transmitted, or to implement flow control either with hardware (UARTs CTS RTS) or software (XON XOFF)

author: Peabody
author_rank: Super Contributor
timestamp: July 25, 2025, 03:15:32 pm
content: Quote from: paulca on July 25, 2025, 11:44:08 amASCII hex is safer only if it doesn't lead to dropping bytes. A while back I did an SD card bootloader for MSP430, and ended up reading in binary from the card, which had been prepared from hex input by a Windows console program. It saved a lot of resources on the MCU to be able to just receive a byte and write it to flash untouched. Checksum at the end, of course. But that's SPI, with a CLK line you can control if necessary. Does your system need to use UART? If it does, then +1 for flow control, slower baud rate, two stop bits, etc.By the way, the Z80 DART datasheet brags that the input is "quadruple buffered". What's that all about?And on ASCII conversion, would a couple 16-byte lookup tables speed things up?

author: paulca
author_rank: Super Contributor
timestamp: July 25, 2025, 03:40:38 pm
content: I have been collecting ways to program it since I started. I expect I'll keep doing it LOLI can pull the flash chip and program it with a custom STM32 programmer.I can use an STM32 and level shifters to use traditional parallel handshaked IOI can use UART.I even want to stop having to remove the ROM and relocate one of the above to RAM and allow it to write the flash.All are possible. But all are in some varied state of working/complete/not yet.The PIO loader is fast and accurate. The worst bug it had was a startup deadlock that was fixed with a retry and just needs both ends to force the other to reset.However it requires a breadboard with level shifters.I think this is why I wrote the UART loader. It is a direct connect to the USB UART at 5V.... but it's slow and the more reliable seems to make it slower.Too many options, first world problems. Nothing wrong.

author: paulca
author_rank: Super Contributor
timestamp: July 25, 2025, 03:44:47 pm
content: Quote from: Peabody on July 25, 2025, 03:15:32 pmI'm not sure I remember that, but it is, by any modern standards a pile of shit. I thought I had a bug or a broken chip the first time I powered it up. It took a bit of research to find that ... no... thats a period NMOS DART at it's finest.2mA drive current at 2.4V output. And it considers that a "valiant high". Even without any load on it's output, save a 1M scope input, I get 4.1V and I feel lucky.And with such little output drive for something like a UART you would assume it's low power, right?40-50mA IDLE. Over 100mA (apparently up to 200mA) flat out.It does get warm lets say.I may however play this to my advantage. A small series resistor to load the PIO (of similar output abilities) just enough and I will get 3.3V interface voltages without level shifters.

author: tggzzz
author_rank: Super Contributor
timestamp: July 25, 2025, 03:51:39 pm
content: Quote from: paulca on July 25, 2025, 03:44:47 pmThat voltage is more than sufficient. Provided it is above 2V, a TTL input will consider it a high.The principal value to a higher drive current is to get the output from 0.8V to 2.0V faster.

author: paulca
author_rank: Super Contributor
timestamp: July 25, 2025, 04:44:23 pm
content: There are so many ways to move data into it that I have considered. From the PIO with an USB MCU enslaved to grabbing the datasheet for a period Floopy disk and making an STM32 be one of them.There is a practical driver though.I am getting tired of pulling the ROM chip. I need to stop flitting around trying this and that and pick one, make it work. Make it so that I can build and upload an application from an IDE.The UART wins. Justification:I have completed, working, on PCB hardware that does 2 channels of UART and a 5V fine USB FTDI quad UART free ports.Because I have 2 channels and my primary interface is itself UART. The data channel, when being used can go onto channel 2. It can therefore be raw binary as I do not need to see it, have it echo or anything. This prevents the current fiasco of disconnecting "screen" so I can connect the loader and faffing around.With a simple way to say:make all uploadI can use that to write the "Flash programmer" Z80 side.The worst that happens is I corrupt the ROM and have to take it out anyway.

author: paulca
author_rank: Super Contributor
timestamp: July 26, 2025, 02:07:05 pm
content: Code:Z80 MCU3232 V1Paul Campbell 2025>LOAD 9000 10ERROR>LOAD 9000 014ELoading... 0x9000 0x014EokComplete...Press ENTER to continue...Hello World!Z80 MCU3232 V1Paul Campbell 2025>RUN 9000Press ENTER to continue...Hello World!Z80 MCU3232 V1Paul Campbell 2025>Works a charm. The user app deliberately jumps back to the ROM entry.Also:Code:paul@linux-dev-vm:/mnt/homes/paul/devel/Z80/z80_asm$ python3 uart_bin_loader.py upload.bin 9000File opened okSerial opened okWrote 334 bytes in 0.35 seconds = 0.97 kBytes per second.Not that slow either.Just needs to be made a bit more resilient. Nice things like timeouts or error handling.The python loader is now redudnant. The Z80 is keeping up with raw 9600 baud. So this now works too:cat upload.bin > /dev/ttyUSB1EDIT:The python loader is BACK!LOAD HHHH HHHHIs tedious.Why can't I just do LOADIf you don't provide an address it will go to 0x8000 and if you do it will go where you ask.The loader, can send a header and a length and maybe a checksum if I get that fancy.This helps with the other issue... if you end up with something in a buffer after a failed load it's a bugger to get it to "realign" again. If the loader can be trusted to send a header, the CPU side can spin through garbage till it hits the header and then read and validate the length before proceeding. This may require a sync point.> <garbage dont care>LOADER_HEADER<2 byte LENGTH>< Some appropriate response for "ok or piss off"> <payload bytes full rate>

author: paulca
author_rank: Super Contributor
timestamp: July 27, 2025, 10:23:40 am
content: I got the protocol implemented. Pretty much as described, but no need for an "ok". It's fast enough to process the header/length/payload is one shot.I'm not posting code, it's already 250 lines. It's all muddled up in the same file as the command-line loop. The next task here is to refactor that out of the "test" scripts context and put what I want to keep into the ROM space. Both the CLI and the loader.Also made the start on a "ROM/MON" thing, out of necessity. For a while the application was uploading and immediately locking the board. So I could reboot it and go and look at what was in the memory.When you are already standing on top of "Drivers" and "Str_Utils" with a working TextUI, writing a memory monitor is not that difficult. Though it does feel good to reep the rewards of pushing everything "Up" into ROM and building on it.The "tying together" point though. The ROM/Mon was the first application written that didn't involving pulling a ROM chip out. It was also the first example of multi-programming, as I could load, run several programmes without rebooting.

author: brucehoult
author_rank: Super Contributor
timestamp: July 27, 2025, 11:04:13 am
content: Quote from: paulca on July 26, 2025, 02:07:05 pmI completely don't understand why this would be difficult on a 1.8 MHz Z80. 187.5 clock cycles per BIT. That's easy with bit-banging a GPIO. With a UART to gather 10 bits into a byte? 1875 clock cycles per byte.It *should* be able to handle 57600 or maybe even 115200 if, as you said before, you're not doing anything else at the same time, just decoding hex and shovelling it into RAM.

author: paulca
author_rank: Super Contributor
timestamp: July 28, 2025, 09:24:15 am
content: I tried 115200.115200 works for console, overruns for data, at least once and that's all it takes.57600 worked for both with an undetected byte error problem, spotted by eye.I pondered CRC/Chksum, but tried 2880028800 Didn't work.14400 Didn't work.57600 Didn't work again.Nothing, but 9600 worked.Note, that 57600 did work, but then didn't. When the only value I was changing was the timer "Time constant". This tells me there is something else at play.When "not working", the 'ART' frame was suspect, the scope was having trouble decoding it the frame size seemed to be varying. It's either miss reading or miss writing frames or both. The CTC baud clock looked perfect. At 14400 it was check to be 14.39998kHz with barely a jitter in the last digit. Strong 1 master clock pulse at 4.2V."screen" produced random garbage from the echo back as well as the "sent from Z80"..I think the DART has an '87 date code and the CTC an '86. They are both NMOS.I'll try again, after work, but I expect the same behaviour... sort of works, but gives up the ghost and needs to cool back down again. Although maybe I shouldn't whip crack 40 year old chips and expect magic that isn't smoke formed.The pair, DART and CTC, IDLE about 20mA each. Running at 9600 that doubles. I didn't check what they pull with dual channel 115200, but I expect that might be the problem.

author: brucehoult
author_rank: Super Contributor
timestamp: July 28, 2025, 09:44:04 am
content: Quote from: paulca on July 28, 2025, 09:24:15 amYes, the uart chip not being able to handle the speed or overheating is likely to be the problem. The Z80 not being able to process the data quickly enough is not -- assuming it is efficiently programmed.

author: Analog Kid
author_rank: Super Contributor
timestamp: July 29, 2025, 04:11:52 am
content: So, are you having fun yet?

author: tggzzz
author_rank: Super Contributor
timestamp: July 29, 2025, 07:48:22 am
content: Quote from: paulca on July 28, 2025, 09:24:15 amDatasheethttps://datasheet4u.com/datasheet/Zilog/Z8470-656613indicates clock should be >Vcc-0.6, i.e. >4.4V.That is sufficient to account for flaky operation.It also indicates Icc 100mA max, so your 40mA(?) would be well within spec.

author: paulca
author_rank: Super Contributor
timestamp: July 29, 2025, 08:29:54 am
content: Quote from: tggzzz on July 29, 2025, 07:48:22 amThe CTC VOH is 2.4V and 1.8mA.The DART output VoH is 2.4V @ 2mA.So I don't know where in that day and age is't getting >4.4V, it's not from it's own family.I set it back to 9600 and went on with things rather than "Min/Max" which was never the intent. It's not exactly 100% reliable even at 9600.I did have to fix an issue. While simple things like OVRRUN the DART clears the error as soon as you read the data, other errors upset it more "sticky" and you have to go and reset it by setting 3 different bits. So I added that which at least meant I didn't need to reset the board every 10th upload.I also have custom made UART leads, which are not looking the best. Those crimp strips were never made for humans. Some leads I have made are still working fine. Others come apart or never fit the JST header properly. At the moment I need to lift the IO board to get to the ROM chip ZIP socket underneath. This means the UART leads are constantly being disturbed and those homemade leads do not like it. "Why you not working now?", "Oh, the lead fell out."I got the bare minimum "monitor" app written. Just simple things like "read some memory", "write a memory location". Print 1kb of strings from address. Write to raw IO port (yes, you can reconfigure the DART running your console and ... end it), Read from a raw IO port. The IO read/writes are for the FPGA integration I have upcoming. Should make it a lot easier to test that without having to write ASM and upload every change.Maybe the weekend I want to remove the last need to use the ZIF socket and write a flash rom programmer. I already have the protocol "for ease" worked out. I use the existing UART loader to upload the sector to be written to RAM. Then load the flash programmer into RAM as well and run it. If done right the Z80 can be kept out of ROM addresses long enough to allow the flash to erase and then write the sector. Without relocating my ROM routines (no thanks), I will be mostly "bare metal again!"I might do something fun instead, I haven't played with sending terminal control characters yet and doing goofy stuff in the console.

author: tggzzz
author_rank: Super Contributor
timestamp: July 29, 2025, 08:55:52 am
content: Quote from: paulca on July 29, 2025, 08:29:54 amCompletelyirrelevant.The relevant spec is VIHC. Note the bold characters.

author: paulca
author_rank: Super Contributor
timestamp: July 29, 2025, 09:14:56 am
content: Quote from: brucehoult on July 28, 2025, 09:44:04 amI think it's old and past it's best. So if it were a new 16550A or similar I might call it broken. I just think it has character. There is also the point the board is 100% custom design, so I could have screwed up somewhere dramatically simply and ruin it. Like putting 100R series resistors on the UART Tx and Rx which didn't seem to do much except for pick up noise. Then again that's unfair, I'm comparing what the scope looks like on a board "pin" versus what it looks like on a UART flying lead. Obviously the later is an antenna and highly likely to be noisier. The noise is proportionally small to the signal which is important. There is nothing even approaching the TTL triggers.The hour or so I spent with the scope while I had it faster and failing the frames where mangled. It looks like it tried to draw the "START bit down and gave up. There is about 0.5V dip where the start bit should be, then a 0.5V overshoot, then it goes back to clocking bits, having completely missed the start bit.So while I was spamming the "space" key, the scope was seeing 0xF0 or something weird or under sized frame. Then it would send frames with a start bit that were about 1 bit too long. OVERLENIt does suggest an edge triggering problem, but not one way, it seems to miss pulses, but double as well.I don't mind if it says,"Loading failed, retry..." once in a while, but I do need the checksum. I have seen garbled ASCII in uploads and while that is amusing a bit flip in ASM code probably won't be.

author: paulca
author_rank: Super Contributor
timestamp: July 29, 2025, 09:16:51 am
content: Quote from: tggzzz on July 29, 2025, 08:55:52 amI don't think that means the baud trigger clock, I think that means the "Master Z80 clock" on the CLK pin? Which is a modern oscillator running on 5V in my case.The DART was fully intended to run with the CTC generating it's baud clock and the CTC is not capable of 5V output. It makes no claims over 2.4V.EDIT:Although. That said. It has been a whlie since I actually checked the master clock. Not since I asked it to drive 3 more NMOS ICs....

author: paulca
author_rank: Super Contributor
timestamp: July 29, 2025, 09:29:04 am
content: The DART (and CTC/SIO as well), the CMOS versions are like hens teeth, I don't I have seen one. On discord and vintage computing haunts I see people "walking on past" the NMOS variants as there is a lore they are unreliable, broken or from questionable, random "old stock" sources.

author: tggzzz
author_rank: Super Contributor
timestamp: July 29, 2025, 10:38:08 am
content: Quote from: paulca on July 29, 2025, 09:16:51 amYou were talking about the master z80 clock being only 4.2V; see above.The "modern oscillator's" Vcc being 5V is irrelevant. The only thing that matters in this context is VIHC>4.4V.Have you verified there is no noise on the DART's Vcc and Gnd lines. Signal integrity problems there could lead to flaky operation such as you describe.

author: paulca
author_rank: Super Contributor
timestamp: July 29, 2025, 12:52:15 pm
content: I see the confusion.The CTC goes high for a very short pulse as the timer zeros. I believe it is high for just 1 master clock duration.A DART byte looks like:The CTC baud clock looks like:The master clock looks like:(im just holding the probe against a pin)

author: tggzzz
author_rank: Super Contributor
timestamp: July 29, 2025, 01:13:12 pm
content: There's no point in checking an analogue output if the PSU rails are incorrect; no surprises there!Similarly there's no point in checking logic operation until PSU and input signal integrity has been verified.That means:checking PSU rails at the chip for correct voltage and absence of impulse noise (typically caused by transitions)checking inputs to see they voltages are valid w.r.t. VIHand VIL, and that they don't go outside the PSU railschecking input timing, paying particular attention to Tsetupand Tholdchecking clock signals are monotonic, with no excessive overshoot nor undershootAll those measurements must be done with the scope shield connected to the IC's GND pin by a short wire. Even a 6" lead can cause problems:https://entertaininghacks.wordpress.com/2015/04/23/scope-probe-accessory-improves-signal-fidelity/andhttps://entertaininghacks.wordpress.com/2016/09/17/scope-probe-accessory-higher-frequency-results/Be aware the clock frequency is completely irrelevant; the only important parameter is the rise/fall time. You can get just as many problems with a 1Hz clock as a 10MHz clock. Anthropomorphic explanation: the circuit neither knows nor cares when the next transition might occur.

author: paulca
author_rank: Super Contributor
timestamp: July 29, 2025, 02:56:16 pm
content: I'm going to put a checksum on it.It's bad, I'll fix it. If it's not, I'll live with it.I grew up rewinding game tapes 14 times just to get that one clean load and be able to play it. I may have low expectations of what Im doing here.The analogy though might be .... cleaning and realigning the tape heads would make an improvement, especially if you aligned them to that tape specifically, by ear.That would be me cleaning up my UART leads, properly terminating them on my end, rather than a tossed in 100R because it sounded right and ChatGPT said it was "common".

author: paulca
author_rank: Super Contributor
timestamp: July 29, 2025, 03:47:05 pm
content: So I went and checked the PSU rails as you suggested.... all full of confidence they were clean.I found 500mV peak to peak "pulse" noise. Traced it to the FTDI Quad UART and as it's not connected to VCC.... it's ground... USB ground.Having never had one work before I thought it was a waste of time and put a USB isolater on it. It worked and the 500mV noise is gone. Left with barely triggerably 35mVpp noise on VCC.

author: tggzzz
author_rank: Super Contributor
timestamp: July 29, 2025, 06:01:59 pm
content: Quote from: paulca on July 29, 2025, 03:47:05 pmI would expect you could easily trigger on 35mV of noise; certainly I can on every scope I own.You are using AC coupling when looking at the noise, aren't you? At, say 5mV/div, 35mV would be 7 divisions high.

author: paulca
author_rank: Super Contributor
timestamp: July 29, 2025, 06:20:43 pm
content: Quote from: tggzzz on July 29, 2025, 06:01:59 pmIt triggers, but just shows a persistence blur. One shooting it and I see these lovely little dragons.More has been added to the system though, I just plugged a whole breadboard into the bus with an FPGA. So the situation may have got a little noisier than this avo.I also had to remove the cheap Rudeng SWPSU and use the Tenma linear to get it that clean.Now you have me trying to justify a Rigol DP832 to myself.

author: tggzzz
author_rank: Super Contributor
timestamp: July 29, 2025, 07:39:16 pm
content: Use infinite persistence, and see the worst case values. That's one advantage of a digitizing scope over an analogue scope.Get rid of the 20MHz BW limit, and see the higher frequencies. That's imperative for signal integrity assessment.