Macros for Structured/HL assembly (ca65)

Discuss technical or other issues relating to programming the Nintendo Entertainment System, Famicom, or compatible systems. See the NESdev wiki for more information.

Moderator: Moderators

Post Reply
User avatar
Movax12
Posts: 541
Joined: Sun Jan 02, 2011 11:50 am

Macros for Structured/HL assembly (ca65)

Post by Movax12 »

While working on my personal projects, I decided that my macro code I wrote years ago (a few people may remember) to accomplish high-level like structures in ca65 needed some attention. It worked well, but the macro code itself was unorganized. I've rewritten it and pulled the needed files for this from my project and shared them on Gitlabs: https://gitlab.com/ca65/ca65hl
( There is also a Github, but I won't be updating it anymore: https://github.com/Movax12/ca65hl )

I would consider this beta, though I have tested it quite extensively. With this rewrite, structured code builds about twice as fast as with my old code, though modules would have to be quite large to notice a significant difference in build times vs plain assembly with light macro usage. There is quite a bit of macro code, but this is partly due to quite a bit of error checking: it tries to make sure that either appropriate code or an error message is generated.

Please read the manual.md in the repository for more information.
If anyone uses it, please let me know if you have questions or find any bugs!
Garth
Posts: 246
Joined: Wed Nov 30, 2016 4:45 pm
Location: Southern California
Contact:

Re: Macros for Structured/HL assembly (ca65)

Post by Garth »

This is great. Macros are often misused and misunderstood, or done without good use of conditional assembly to figure out the most efficient code to lay down; so it's always good to see good use of macros promoted to raise the level of the language where people think assembly is so cumbersome. I've gone to extents with nestable program flow control macros, as you can see in that part of my website, at http://wilsonminesco.com/StructureMacros/ . A couple of the many examples I have elsewhere on my site are:

Code: Select all

 ; For the RX_STATEs:
 ; When RX_STATE = 0, an $FF byte will be required to increment RX_STATE to 1.  Other bytes do nothing.  The $FF is discarded.
 ; When RX_STATE = 1, only a non-$FF byte will be put in READING_BUF and increment RX_STATE to 2.  More leading $FF's do nothing.
 ; When RX_STATE = 2, any value received will be put in READING, READING_BUF will be transferred to READING+1, and RX_STATE will
 ;                                                                                                               be returned to 0.


WATCH_ACIA:
      LDA  RX_STATE
      CASE  ACCUM

          CASE_OF  0                             ; In the case of RX_STATE being 0, an $FF is required to increment it to 1.
              IF_BIT   ACIA_STAT_REG, 3, IS_SET  ; If a byte came in (indicated by bit 3 of the status register),
                  LDA  ACIA_DATA_REG             ; get it (this clears the flag in the status register too),
                  INA                            ; and see if it's $FF.  (INA is 1 byte less than CMP #$FF, and I don't need A again.)
                  IF_EQ                          ; If it was $FF,
                      INC  RX_STATE              ; move on to watch for a valid (non-$FF) first byte.
                  END_IF                         ; The accumulator value gets discarded.
              END_IF                             ; If no byte came in, just exit.
          END_OF


          CASE_OF  1                             ; To get here, we've received the $FF marker and we're looking for a valid 1st byte.
              IF_BIT   ACIA_STAT_REG, 3, IS_SET  ; If a byte came in,
                  LDA  ACIA_DATA_REG             ; get it,
                  IF_PLUS                        ; see if it's valid as a high byte, ie, that the high bit is clear, and if so,
                      STA  READING_BUF           ; store it as valid in the buffer area meant to prevent wrong readings between bytes,
                      INC  RX_STATE              ; and move on to watch for the 2nd byte which could be anything.
                  END_IF                         ; If it's not a valid high byte, just discard it, and leave RX_STATE as is.
              END_IF                             ; If no byte came in, just exit.
          END_OF

                                                      ; In the case of RX_STATE being 2, we've gotten the high byte of READING,
          CASE_OF  2                                  ; and we're waiting for the second byte which could be anything.
              IF_BIT  ACIA_STAT_REG, 3, IS_SET        ; See if a byte came in.  If one did,
                  COPY  VIA_SR, TO, READING           ; just transfer it
                  COPY  READING_BUF, TO, READING+1    ; (including the high byte now, which we had saved to prevent glitches
                  STZ   RX_STATE                      ; in the two-byte READING value), and go back to state 0.
              END_IF                                  ; If no byte came in, just exit.
          END_OF


          STZ   RX_STATE                         ; If there's any chance for state to go invalid, just reset it and start over.
      END_CASE
      RTS
 ;-------------------
and

Code: Select all

SPI_Xceive:                             ; Start with data to send in A.
        STA  SPIOUT                     ; Store output data, and
        STZ  SPIIN                      ; initialize the input buffer.
        LDY  #8                         ; Set up bit counter.
        LDA  #SPI_MOSI                  ; Put MOSI bit mask in A for TRB/TSB.

        LDX   SPIMODE
        CASE  X_REG

           CASE_OF  mode_0              ; In the case of X saying mode 0,
              FOR_Y  Y_REG, DOWN_TO, 0  ; start into this loop.  Go thru 8x.
                 Shift_bit_onto_MOSI    ; (Shifts SPIOUT to start.)
                 INC  VIA1PA            ; Set SCLK high.
                 Rot_MISO_into_SPIIN    ; Move MISO into the receive buffer.
                 DEC  VIA1PA            ; Set SCLK low.
              NEXT_Y                    ; Decrement our bit counter.
           END_OF


           CASE_OF  mode_1              ; In the case of X saying mode 1:
              FOR_Y  Y_REG, DOWN_TO, 0
                 INC  VIA1PA            ; Set SCLK high.
                 Shift_bit_onto_MOSI    ; (Shifts SPIOUT to start.)
                 DEC  VIA1PA            ; Set SCLK low.
                 Rot_MISO_into_SPIIN    ; Move MISO into receive buffer.
              NEXT_Y
           END_OF


           CASE_OF  mode_2
              FOR_Y  Y_REG, DOWN_TO, 0
                 Shift_bit_onto_MOSI    ; (Shifts SPIOUT to start.)
                 DEC  VIA1PA            ; Set SCLK low.
                 Rot_MISO_into_SPIIN    ; Move MISO into receive buffer.
                 INC  VIA1PA            ; Set SCLK high.
              NEXT_Y
           END_OF


           CASE_OF  mode_3
              FOR_Y  Y_REG, DOWN_TO, 0
                 DEC  VIA1PA            ; Set SCLK low.
                 Shift_bit_onto_MOSI    ; (Shifts SPIOUT to start.)
                 INC  VIA1PA            ; Set SCLK high.
                 Rot_MISO_into_SPIIN    ; Move MISO into receive buffer.
              NEXT_Y
           END_OF
                                        ; You could handle invalid SPI modes
                                        ; here before END_CASE if desired.
        END_CASE

        LDA  SPIIN                      ; Returns the received data in Accum.
        RTS
 ;------------------
In most cases, the macros assemble exactly the same thing you would do by hand, meaning there's no performance or memory cost; it's just that you don't have to look at the ugly internal details every time. They make the code a lot more clear, concise, and maintainable, and lead to fewer bugs since you can see what you're doing better, and you become more productive. The program flow control structures can be nested with others of the same or different type; for example, one IF...ENDIF can be nested inside another IF...ENDIF, inside another, inside a FOR...NEXT, which is inside another FOR...NEXT, or other combinations.
http://WilsonMinesCo.com/ lots of 6502 resources
User avatar
Movax12
Posts: 541
Joined: Sun Jan 02, 2011 11:50 am

Re: Macros for Structured/HL assembly (ca65)

Post by Movax12 »

Garth wrote: Fri Mar 04, 2022 7:00 pm In most cases, the macros assemble exactly the same thing you would do by hand...
Yes, I can still control exactly what assembly language code is generated. I wouldn't recommend this for beginners just learning assembly though: It's probably better to learn the assembly first to understand how to use something like this properly. (Not that you couldn't use this - but I think it is important to understand how it is working.)
I haven't (re)implemented for loops or switch/case with this (yet?) as I haven't found that to be something I've used much. Usually just something like this works:

Code: Select all

; copy 16 bytes
ldx #$0F
repeat
    lda fromhere, x
    sta puthere, x
until (dex == negative) 
Almost a for loop:

Code: Select all

; loop from x = 1 to 15
ldx #$00
while (inx : x < #$10) do 
    lda fromhere, x
    sta puthere, x
endwhile
Garth
Posts: 246
Joined: Wed Nov 30, 2016 4:45 pm
Location: Southern California
Contact:

Re: Macros for Structured/HL assembly (ca65)

Post by Garth »

Movax12 wrote: Fri Mar 04, 2022 8:13 pmI wouldn't recommend this for beginners just learning assembly though: It's probably better to learn the assembly first to understand how to use something like this properly. (Not that you couldn't use this - but I think it is important to understand how it is working.)
I agree. It's like having kids master multiplying and dividing on paper before they're allowed to use calculators. First they need to understand what the process even is.
http://WilsonMinesCo.com/ lots of 6502 resources
Post Reply