; ; _printf: Basic layer for all printf type functions. ; ; Ullrich von Bassewitz, 2000-10-21 ; .include "zeropage.inc" .export __printf .import popax, pushax, pusheax, decsp6, push1, axlong, axulong .import _ltoa, _ultoa .import _strlower, _strlen .macpack generic ; ---------------------------------------------------------------------------- ; We will store variables into the register bank in the zeropage. Define ; equates for these variables. ArgList = regbank+0 ; Argument list pointer Format = regbank+2 ; Format string OutData = regbank+4 ; Function parameters ; ---------------------------------------------------------------------------- ; Other zero page cells Base = ptr1 FSave = ptr1 FCount = ptr2 .code ; ---------------------------------------------------------------------------- ; Get one character from the format string and increment the pointer. Will ; return zero in Y. GetFormatChar: ldy #0 lda (Format),y IncFormatPtr: inc Format bne @L1 inc Format+1 @L1: rts ; ---------------------------------------------------------------------------- ; Output a pad character: outfunc (d, &padchar, 1) OutputPadChar: lda PadChar ; ---------------------------------------------------------------------------- ; Call the output function with one character in A Output1: sta CharArg jsr PushOutData lda #CharArg jsr pushax jsr push1 jmp CallOutFunc ; fout (OutData, &CharArg, 1) ; ---------------------------------------------------------------------------- ; Decrement the argument list pointer by 2 DecArgList2: lda ArgList sub #2 sta ArgList bcs @L1 dec ArgList+1 @L1: rts ; ---------------------------------------------------------------------------- ; Get an unsigned int or long argument depending on the IsLong flag. GetUnsignedArg: lda IsLong ; Check flag bne GetLongArg ; Long sets all jsr GetIntArg ; Get an integer argument jmp axulong ; Convert to unsigned long ; ---------------------------------------------------------------------------- ; Get an signed int or long argument depending on the IsLong flag. GetSignedArg: lda IsLong ; Check flag bne GetLongArg ; Long sets all jsr GetIntArg ; Get an integer argument jmp axlong ; Convert to long ; ---------------------------------------------------------------------------- ; Get a long argument from the argument list. Returns 0 in Y. GetLongArg: jsr GetIntArg ; Get high word sta sreg stx sreg+1 ; Run into GetIntArg fetching the low word ; ---------------------------------------------------------------------------- ; Get an integer argument from the argument list. Returns 0 in Y. GetIntArg: jsr DecArgList2 ldy #1 lda (ArgList),y tax dey lda (ArgList),y rts ; ---------------------------------------------------------------------------- ; Read an integer from the format string. Will return zero in Y. ReadInt: ldy #0 sty ptr1 sty ptr1+1 ; Start with zero @Loop: lda (Format),y ; Get format string character sub #'0' ; Make number from ascii digit bcc @L9 ; Jump if done cmp #9+1 bcs @L9 ; Jump if done ; Skip the digit character jsr IncFormatPtr ; Add the digit to the value we have in ptr1 pha ; Save digit value lda ptr1 ldx ptr1+1 asl ptr1 rol ptr1+1 ; * 2 asl ptr1 rol ptr1+1 ; * 4, assume carry clear adc ptr1 sta ptr1 txa adc ptr1+1 sta ptr1+1 ; * 5 asl ptr1 rol ptr1+1 ; * 10, assume carry clear pla adc ptr1 ; Add digit value sta ptr1 bcc @Loop inc ptr1+1 bcs @Loop ; Branch always ; We're done converting @L9: lda ptr1 ldx ptr1+1 ; Load result rts ; ---------------------------------------------------------------------------- ; Put a character into the argument buffer and increment the buffer index PutBuf: ldy BufIdx inc BufIdx sta Buf,y rts ; ---------------------------------------------------------------------------- ; Get a pointer to the current buffer end and push it onto the stack PushBufPtr: lda #Buf add BufIdx bcc @L1 inx @L1: jmp pushax ; ---------------------------------------------------------------------------- ; Push OutData onto the software stack PushOutData: lda OutData ldx OutData+1 jmp pushax ; ---------------------------------------------------------------------------- ; Output Width pad characters ; PadLoop: jsr OutputPadChar OutputPadding: inc Width bne PadLoop inc Width+1 bne PadLoop rts ; ---------------------------------------------------------------------------- ; Output the argument itself: outfunc (d, str, arglen); ; OutputArg: jsr PushOutData lda Str ldx Str+1 jsr pushax lda ArgLen ldx ArgLen+1 jsr pushax jmp CallOutFunc ; ---------------------------------------------------------------------------- ; ltoa: Wrapper for _ltoa that pushes all arguments ltoa: sty Base ; Save base jsr pusheax ; Push value jsr PushBufPtr ; Push the buffer pointer... lda Base ; Restore base jmp _ltoa ; ultoa (l, s, base); ; ---------------------------------------------------------------------------- ; ultoa: Wrapper for _ultoa that pushes all arguments ultoa: sty Base ; Save base jsr pusheax ; Push value jsr PushBufPtr ; Push the buffer pointer... lda Base ; Restore base jmp _ultoa ; ultoa (l, s, base); ; ---------------------------------------------------------------------------- ; __printf: ; Save the register bank variables into the save area pha ; Save low byte of ap ldy #5 Save: lda regbank,y sta RegSave,y dey bpl Save ; Get the parameters from the stack pla ; Restore low byte of ap sta ArgList ; Argument list pointer stx ArgList+1 jsr popax ; Format string sta Format stx Format+1 jsr popax ; Output descriptor sta OutData stx OutData+1 ; Initialize the output counter in the output descriptor to zero lda #0 tay sta (OutData),y iny sta (OutData),y ; Get the output function from the output descriptor and remember it iny lda (OutData),y sta CallOutFunc+1 iny lda (OutData),y sta CallOutFunc+2 ; Start parsing the format string MainLoop: lda Format ; Remember current format pointer sta FSave lda Format+1 sta FSave+1 ldy #0 ; Index @L1: lda (Format),y ; Get next char beq @L2 ; Jump on end of string cmp #'%' ; Format spec? beq @L2 iny ; Bump pointer bne @L1 inc Format+1 ; Bump high byte of pointer bne @L1 ; Branch always ; Found a '%' character or end of string. Update the Format pointer so it is ; current (points to this character). @L2: tya ; Low byte of offset add Format sta Format bcc @L3 inc Format+1 ; Calculate, how many characters must be output. Beware: This number may ; be zero. A still contains the low byte of the pointer. @L3: sub FSave sta FCount lda Format+1 sbc FSave+1 sta FCount+1 ora FCount ; Is the result zero? beq @L4 ; Jump if yes ; Output the characters that we have until now. To make the call to out ; faster, build the stack frame by hand (don't use pushax) jsr decsp6 ; 3 args ldy #5 lda OutData+1 sta (sp),y dey lda OutData sta (sp),y dey lda FSave+1 sta (sp),y dey lda FSave sta (sp),y dey lda FCount+1 sta (sp),y dey lda FCount sta (sp),y jsr CallOutFunc ; Call the output function ; We're back from out(), or we didn't call it. Check for end of string. @L4: jsr GetFormatChar ; Get one char, zero in Y tax ; End of format string reached? bne NotDone ; End not reached ; End of format string reached. Restore the zeropage registers and return. ldx #5 Rest: lda RegSave,x sta regbank,x dex bpl Rest rts ; Still a valid format character. Check for '%' and a '%%' sequence. Output ; anything that is not a format specifier. On intro, Y is zero. NotDone: cmp #'%' bne @L1 lda (Format),y ; Check for "%%" cmp #'%' bne FormatSpec ; Jump if really a format specifier jsr IncFormatPtr ; Skip the second '%' @L1: jsr Output1 ; Output the character... jmp MainLoop ; ...and continue ; We have a real format specifier ; Format is: %[flags][width][.precision][mod]type ; Y is zero on entry. FormatSpec: ; Initialize the flags lda #0 ldx #FormatVarSize-1 @L1: sta FormatVars,x dex bpl @L1 ; Start with reading the flags if there are any. X is $FF which is used ; for "true" ReadFlags: lda (Format),y ; Get next char... cmp #'-' bne @L1 stx LeftJust beq @L4 @L1: cmp #'+' bne @L2 stx AddSign beq @L4 @L2: cmp #' ' bne @L3 stx AddBlank beq @L4 @L3: cmp #'#' bne ReadPadding stx AltForm @L4: jsr IncFormatPtr jmp ReadFlags ; ...and start over ; Done with flags, read the pad char. Y is still zero if we come here. ReadPadding: ldx #' ' ; PadChar cmp #'0' bne @L1 tax ; PadChar is '0' jsr IncFormatPtr lda (Format),y ; Read current for later @L1: stx PadChar ; Read the width. Even here, Y is still zero. A contains the current character ; from the format string ReadWidth: cmp #'*' bne @L1 jsr IncFormatPtr jsr GetIntArg ; Width is an additional argument jmp @L2 @L1: jsr ReadInt ; Read integer from format string... @L2: sta Width stx Width+1 ; ...and remember in Width ; Read the precision. Even here, Y is still zero. sty Prec ; Assume Precision is zero sty Prec+1 lda (Format),y ; Load next format string char cmp #'.' ; Precision given? bne ReadMod ; Branch if no precision given ReadPrec: jsr IncFormatPtr ; Skip the '.' lda (Format),y cmp #'*' ; Variable precision? bne @L1 jsr IncFormatPtr ; Skip the '*' jsr GetIntArg ; Get integer argument jmp @L2 @L1: jsr ReadInt ; Read integer from format string @L2: sta Prec stx Prec+1 ; Read the modifiers. Y is still zero. ReadMod: lda (Format),y cmp #'z' ; size_t - same as unsigned beq @L2 cmp #'h' ; short - same as int beq @L2 cmp #'t' ; ptrdiff_t - same as int beq @L2 cmp #'j' ; intmax_t/uintmax_t - same as long beq @L1 cmp #'L' ; long double beq @L1 cmp #'l' ; long int bne DoFormat @L1: lda #$FF sta IsLong @L2: jsr IncFormatPtr jmp ReadMod ; Initialize the argument buffer pointers. We use a static buffer (ArgBuf) to ; assemble strings. A zero page index (BufIdx) is used to keep the current ; write position. A pointer to the buffer (Str) is used to point to the the ; argument in case we will not use the buffer but a user supplied string. ; Y is zero when we come here. DoFormat: sty BufIdx ; Clear BufIdx ldx #Buf stx Str+1 ; Skip the current format character, then check it (current char in A) jsr IncFormatPtr ; Is it a character? cmp #'c' bne CheckInt ; It is a character jsr GetIntArg ; Get the argument (promoted to int) sta Buf ; Place it as zero terminated string... lda #0 sta Buf+1 ; ...into the buffer jmp HaveArg ; Done ; Is it an integer? CheckInt: cmp #'d' beq @L1 cmp #'i' bne CheckCount ; It is an integer @L1: ldx #0 lda AddBlank ; Add a blank for positives? beq @L2 ; Jump if no ldx #' ' @L2: lda AddSign ; Add a plus for positives (precedence)? beq @L3 ldx #'+' @L3: stx Leader ; Integer argument jsr GetSignedArg ; Get argument as a long ldy sreg+1 ; Check sign bmi @Int1 ldy Leader beq @Int1 sty Buf inc BufIdx @Int1: ldy #10 ; Base jsr ltoa ; Push arguments, call _ltoa jmp HaveArg ; Is it a count pseudo format? CheckCount: cmp #'n' bne CheckOctal ; It is a count pseudo argument jsr GetIntArg sta ptr1 stx ptr1+1 ; Get user supplied pointer ldy #0 lda (OutData),y ; Low byte of OutData->ccount sta (ptr1),y iny lda (OutData),y ; High byte of OutData->ccount sta (ptr1),y jmp MainLoop ; Done ; Check for an octal digit CheckOctal: cmp #'o' bne CheckPointer ; Integer in octal representation jsr GetSignedArg ; Get argument as a long ldy AltForm ; Alternative form? beq @Oct1 ; Jump if no pha ; Save low byte of value stx tmp1 ora tmp1 ora sreg ora sreg+1 ora Prec ora Prec+1 ; Check if value or Prec != 0 beq @Oct1 lda #'0' jsr PutBuf pla ; Restore low byte @Oct1: ldy #8 ; Load base jsr ltoa ; Push arguments, call _ltoa jmp HaveArg ; Check for a pointer specifier (%p) CheckPointer: cmp #'p' bne CheckString ; It's a pointer. Use %#x conversion ldx #0 stx IsLong ; IsLong = 0; inx stx AltForm ; AltForm = 1; lda #'x' bne IsHex ; Branch always ; Check for a string specifier (%s) CheckString: cmp #'s' bne CheckUnsigned ; It's a string jsr GetIntArg ; Get 16bit argument sta Str stx Str+1 jmp HaveArg ; Check for an unsigned integer (%u) CheckUnsigned: cmp #'u' bne CheckHex ; It's an unsigned integer jsr GetUnsignedArg ; Get argument as unsigned long ldy #10 ; Load base jsr ultoa ; Push arguments, call _ultoa jmp HaveArg ; Check for a hexadecimal integer (%x) CheckHex: cmp #'x' beq IsHex cmp #'X' bne UnknownFormat ; Hexadecimal integer IsHex: pha ; Save the format spec lda AltForm beq @L1 lda #'0' jsr PutBuf lda #'X' jsr PutBuf @L1: jsr GetUnsignedArg ; Get argument as an unsigned long ldy #16 ; Load base jsr ultoa ; Push arguments, call _ultoa pla ; Get the format spec cmp #'x' ; Lower case? bne @L2 lda Str ldx Str+1 jsr _strlower ; Make characters lower case @L2: jmp HaveArg ; Unknown format character, skip it UnknownFormat: jmp MainLoop ; We have the argument, do argument string formatting HaveArg: ; ArgLen = strlen (Str); lda Str ldx Str+1 jsr _strlen ; Get length of argument sta ArgLen stx ArgLen+1 ; if (Prec && Prec < ArgLen) ArgLen = Prec; lda Prec ora Prec+1 beq @L1 ldx Prec cpx ArgLen lda Prec+1 tay sbc ArgLen+1 bcs @L1 stx ArgLen sty ArgLen+1 ; if (Width > ArgLen) { ; Width -= ArgLen; /* padcount */ ; } else { ; Width = 0; ; } ; Since width is used as a counter below, calculate -(width+1) @L1: sec lda Width sbc ArgLen tax lda Width+1 sbc ArgLen+1 bcs @L2 lda #0 tax @L2: eor #$FF sta Width+1 txa eor #$FF sta Width ; /* Do padding on the left side if needed */ ; if (!leftjust) { ; /* argument right justified */ ; while (width) { ; fout (d, &padchar, 1); ; --width; ; } ; } lda LeftJust bne @L3 jsr OutputPadding ; Output the argument itself @L3: jsr OutputArg ; /* Output right padding bytes if needed */ ; if (leftjust) { ; /* argument left justified */ ; while (width) { ; fout (d, &padchar, 1); ; --width; ; } ; } lda LeftJust beq @L4 jsr OutputPadding ; Done, parse next chars from format string @L4: jmp MainLoop ; ---------------------------------------------------------------------------- ; Local data (all static) .bss ; Save area for the zero page registers RegSave: .res regbanksize ; One character argument for OutFunc CharArg: .byte 0 ; Format variables FormatVars: LeftJust: .byte 0 AddSign: .byte 0 AddBlank: .byte 0 AltForm: .byte 0 PadChar: .byte 0 Width: .word 0 Prec: .word 0 IsLong: .byte 0 Leader: .byte 0 BufIdx: .byte 0 ; Argument string pointer FormatVarSize = * - FormatVars ; Argument buffer and pointer Buf: .res 20 Str: .word 0 ArgLen: .res 2 .data ; Stuff from OutData. Is used as a vector and must be aligned CallOutFunc: jmp $0000