SX programmers are missing a bet: They don't use macros enough. And they don't use them because they are not expained well.
Well, enough! Prepare to become a MACRO MASTER! Here we go...
Macros are code that is executed by the compiler rather than by the processor. E.g. by SASM rather than by the SX. Macros are programs that generate code for the processor just like you generate code for the processor. You write the MACRO program so that it will write code for you instead of you haveing to write the code each time yourself. Macros are like a "mini-me" that I have taught how to write code. The programming language that you write macros in, are the compiler directives like IFDEF, REPT, EQU, and so on. If you don't understand at least a little about those, please read the SASM documentation before you try for this tutorial.
All Symbols are nothing more than a name for a number. In SASM, there are no string variables (except some macro parameters) so everything boils down to a 32 bit number. You can see them in the listing on the left side just right of the listing line number. The following source lines:
;Pretend to be for an SX28 CPU_Pins = 28
(which happen to be at lines 29 and 30 in the source file) turn into this in the listing file:
29 ;Pretend to be for an SX28 30 =0000001C CPU_Pins = 28
Notice that 1C is hex for 28 and that 8 hex digits are 32 bits total (4 bits per digit). Here are some more examples:
1 =00000005 what = RA 2 =00000004 what = FSR 3 =00000002 what = PC 4 =00000002 what = what
I've set a variable called "what" to different values so that the result will appear after the line number in the listing file. I think of it as asking "what is x" but it is actually assigning the value of x to the variable what and showing that value in the listing.
Here are some ways to define a symbol (called sym here) and set it to a number:
sym ;defines sym as a label to the current address. sym EQU value ;permanently sets sym to value. sym can't be changed. sym SET value ;initially sets sym to value. sym can be changed. sym = value ;initially sets sym to value in plain english. sym can be changed.
Since the sym = value form reads more like English, and does just what people expect it to do, this is the preferred style. Just to be clear on one point, if the above code were actually compiled, the EQU line would cause an error because it would be trying to change sym from being a label for a program address to being a general symbol. And if it got past that, the SET line would cause an error because the EQU line tells SASM not to let anything change sym. That can be... well once it a while someone, somewhere might find a need for that. In general just remember:
sym = value ;GOOD CODE!
Easy and simple. Just like most every other programming language. But don't try to make value be "ABC" it MUST be 123 (or something that ends up being 123 like 100 + 20 + 3). One apparent exception is single character "strings" in single quotes. These actually evaluate to their ASCII value. So what = 'A' sets it to 41. And if there are more than one character, only the first is used. what = 'ABC' still results in a 41 in what.
Now, Labels are special symbols. Labels are created whenever something, that isn't recognized as something else, is put in the far left column. e.g. no tab or space before it and it isn't a reserved word or already defined. They can have a colon after them, or not. Colons on the end don't matter. Ah! But colons on the beginning, that is a very interesting trick! And critical to the effective use of Macros.
If there is no colon on the beginning, the label is global It must be unique and will be recognized through out the file. With a colon on the beginning, the label becomes local.
One nice thing about local labels is that you can re-use the same local name over and over in your program. The only issue is that before you re-use a local label, there has to be a new global label. For example:
Starting_Label ...code... :A_Local_Label ...code... Middle_Label ...code... :A_Local_Label
"Starting_Label" and "Middle_Label" are both global labels. The first time ":A_Local_Label" is declared, the assembler actually creates the internal name "Starting_Label:A_Local_Label". The next time it is declared, the assembler creates the internal name "Middle_Label:A_Local_Label". If there wasn't another global label declared before declaring the same local label, the assembler would have generated the same internal name twice, resulting an error.
However, you can also manually refer to a <specific> local label by putting the same internal name SASM generates in your code. If you wrote the following:
<anywhere> in your code, the assembler would know which version of "A_Local_Label" to jump to because of the use of "Starting_Label:" in front.
Check out the following little test file:
org 5 ;move to the 5th program address. sym: ;define a label called sym. This is a global label org 10 ;move to the 10th program address. :sym ;define a label called :sym. This is local to sym: what = sym ;do some assignments to a symbol called what what = :sym ;so that we can find out what values these labels have. what = sym:sym ;There shouldn't be any mystery at this point. org 15 ;move up to word 15. lab ;define another global label called lab org 20 ;move to 20 :sym ;define another local label called :sym what = lab ;and again, see what values these labels have. what = :sym ;What will this one be? what = sym:sym
Here is the result in the list file. Notice the HEX values on the left. Don't you wish I had specified the org's in hex? Or used 1,2,3,4? Just trying to point out that these can be confusing. Look at the result:
1 =00000005 org 5 2 =00000005 sym: 3 4 =0000000A org 10 ;10 decimal is 0A hex 5 =0000000A :sym 6 7 =00000005 what = sym 8 =0000000A what = :sym 9 =0000000A what = sym:sym 10 11 =0000000F org 15 ;15 decimal is 0F hex 12 =0000000F lab 13 14 =00000014 org 20 ;20 decimal is 14 hex 15 16 =00000014 :sym 17 18 =0000000F what = lab 19 =00000014 what = :sym 20 =0000000A what = sym:sym
Also notice that I indented what. Had I not, it would have redefined the current global label. Line 8 would fail because it would be an attempt to re-define the label what. Also, in line 8, :sym would have been undefined. Do you see why?
The subtle differences between labels and regular symbols, and between global, local, equates, sets, and assigns are critical in SASM macro programming.
|If you make a symbol with a:||value?
Can it be
start a new
|1||text in the left column, no colon||Address||NO||YES||YES|
|2||colon then text in the left column||Address||NO||LOCAL||NO|
|3||text EQU value||Value||NO||YES||YES|
|4||text EQU value||Value||NO||YES||NO|
|5||text = value||Value||YES||YES||YES|
|6||text = value||Value||YES||YES||NO|
|7||:text = value||Value||YES||LOCAL||NO|
Be sure to catch the difference between 3 and 4 and between 5 and 6. That ONE LITTLE SPACE changes a lot. Be clear in your files, use a tab rather than a space.
|Rather than this:||I recommend you do this:|
There is another sort of a local label when it comes to macros, and ONLY in macros, which do a tricky, but needed, thing inside the compiler. These must be declared after the keyword LOCAL at the start of the macro, right after the name MACRO parmsline. Each local label is assigned a value that contains a number which is incremented each time the macro is invoked. An example may explain it better, but please don't get hung up on trying to understand everything this macro is doing, just look at the macro and then look at the result and see how the value of the lable "loc" is assigned:
tst MACRO LOCAL loc ;loc will only be "known" within the macro jmp @loc ;generate a processor instruction to long jump to whatever loc turns out to be IF ($ & 1 == 0) ;$ is the PC address where the next instruction will be. We are testing if it is even or odd. nop ;generate a processor instruction to do nothing. ENDIF ;but only if it would be placed on an even address in program memory. loc ;loc will now have the value of this address nop ; where we will put another nop. ENDM ;and that is the end of the macro org 25 ;start at program address 25 tst a ;invode the tst macro with a parameter of 'a' ; please note that the parameters are ignored by the macro tst b ; I just added them to allow us to differentiate between the ; three different invocations of the tst macro. tst c
And here is the result of that little test:
15 =00000019 org 25 16 17 tst a 18 0019 0A1B m jmp ??0000 19 m IF ($ & 1 == 0) 20 001A 0000 m nop 21 m ENDIF 22 =0000001B m ??0000 23 001B 0000 m nop 25 26 tst b 27 001C 0A1D m jmp ??0001 28 m IF ($ & 1 == 0) 29 m nop 30 m ENDIF 31 =0000001D m ??0001 32 001D 0000 m nop 34 35 tst c 36 001E 0A1F m jmp ??0002 37 m IF ($ & 1 == 0) 38 m nop 39 m ENDIF 40 =0000001F m ??0002 41 001F 0000 m nop 43
First, notice that we don't really know how much space there will be between the jmp and the target of the jump (the label defined by loc) each time the macro is used. tst a has a nop between the jmp and loc, but tst b doesn't. No matter, SASM still figures out where to go. Sigh, I guess people wont be impressed by that unless they have seen error messages about "undefined forward references" before.
Notice also that in the list file, you can't find the word loc anywhere. It was replaced by ??0000 in tst a, by ??0001 in tst b and by ??0002 in tst c. Those ?? things ARE valid labels, just like loop, main, or any other lable you might put in your code. They just don't exist until the macro gets invoked.
Parameters are passed to Macros in SASM by name unless they are preceded by a "?" and then they are passed by value. What the heck difference does it make? Well, not much usually, but it does allow for some cool tricks and it is as close as you get to strings in SASM.
The next little macro has been written, in various forms, by many different SASM users over time. It doesn't work as expected, but what they are trying to do is only include the page instruction when a jump will cross a page. After we look at it, we can discuss why it isn't that useful but more importantly, show the trap it leads people into and how to avoid it.
_jmp MACRO address noexpand ;don't put all this decision making stuff in the list file IF address/$100 == $/$100 ;decide if the address is over a page from where we are now ($) expand ;do show the result of our decision: jmp address ;We don't need to page (Actually not a valid assumption) noexpand ;more decision makeing behind the scenes ELSE ;the other possibility is that: expand ;and we should show this: page address ;we really do need to page (Probably the best assumption) jmp address ; before we jump noexpand ;hide the closing ENDIF ;done making decisions ENDM
This macro assumes that the page bits are all set for the address you are currently at, and then decides if you need to page or not page depending on if the address is in this page or another. I don't use this because in practice, it is just about useless. You can't be sure where your code will end up and if it rolled over the top of one page into the next just before you call this macro, the page bits didn't get changed, aren't set to the local page, and you are not going to reset them when you needed to. Anyway, I'm using it to illustrate another issue.
What happens if you call it with $ or $ plus or minus some value as the address? Lets say that your trying to make a jump to an address that is just in the next page and you know it will be 10 instructions ahead of where you are... So you do a _jmp $+10. The macro sees that the address is in the next page, compiles the page $+10 and then compiles the jmp $+10. The problem is that $ changed between the page and the jmp. And it changed because address is still "$+10" and NOT the numerical value of the target. You need to call it with _jmp ?($+10) because that causes the target to be evaluated at the beginning. Actually, no; what you really need is to add a line that goes myAddress = address as the first line of the macro and then use myAddress in the rest of the macro. Why? Because then the parameter gets evaluated before it gets used anywhere in the macro. Also, because myAddress is just a local variable, you can change it if you need to pretend that your macro was called with a different value... ok, its a small point, but it comes in handy some times. The corrected macro looks like this, and it doesn't care what you call it with, it will always work correctly.
And this is how you should handle parameters: Name them after the MACRO (for more than one, separate with commas) then assign them to a local variable so you know they have been evaluated, and so you can change them if needed. Our previous __jmp macro would have been better like this:
_jmp MACRO address myAddress = address ;GOOD CODE! noexpand IF myAddress/$100 == $/$100 expand jmp myAddress noexpand ELSE expand jmp @myAddress ;GOOD CODE! noexpand ENDIF ENDM
Keep in mind, this macro is probably useless. Also, note that I changed it from a separate page and jmp to jmp @myAddress. This is a very good idea for SASM code in general, but I wanted to illustrate another reason to use this layout: You can cause the macro parameter to be passed by value by calling the macro with _jmp ?$ as we know, but if the macro compiles the jump with jmp @\1 or jmp address it will cause an "Symbol <#> is not defined" error; instead, you must compile the jump with jmp @$??\1 (see the SASM manual section 4.4.2 "Token Pasting" and more below).
Keep it simple right? Just define a local var in the macro, set the var to your parameter name (or \1, etc..) before any code is compiled and then using the var rather than the parm when the jump is compiled.
Conditionals are a way of makeing SASM into a sort of an expert system: They allow you to put your knowledge into the compiler for your own use in the future. Why? Because no matter how smart you are, you just can't, and shouldn't remember all this junk. Once you read up on how many ports each SX has, and how that changes the first available global file register, you should never have to remember that in your code. You can just set a compiler variable with the number of pins that CPU has (or some other way of identifying the chip) and then let the compiler do all the work of placing your file register variables.
IF CpuPins > 18 IF CpuPins > 28 GPRegOrg = $0A ;$0A to $0F - limited to 6 bytes - global ELSE GPRegOrg = 8 ;$08 to $0F - limited to 8 bytes - global ENDIF ELSE GPRegOrg = 7 ;$07 to $0F - limited to 9 bytes - global ENDIF ;GLOBAL VARIABLES --------------------------------------------- org GPRegOrg Temp ds 1 flags ds 1 ;general flag register RS232Rx_flag = flags.0 RS232RxFrameErr = flags.1 TimerFlag = flags.2 ;timer rollover flag Timers = $ ;timer TimerAccL ds 1 ;timer accumulator low TimerAccH ds 1 ;timer accumulator high TimerAccT ds 1 ;timer accumulator top watch TimerFlag, 1, ubin watch TimerAccL, 24, uhex StackPtr ds 1 ;Stack watch StackPtr,8,UHEX Count ds 1 IF $ > $10 ERROR 'out of gobal variable space' ENDIF
Notice that the above code will compile just fine on an SX18 or 28, but will generate a very nice error message when you try to compile it on an SX48 or 52. You don't have to remember that, and you don't have to track down why sometimes count is getting incremented and other times it isn't.
One of the best uses for conditional compilation that I personally have used is to develop a set of macros using conditions to automatically compile the correct code to compare registers with literals, registers with registers, W with literals, etc... so that I don't have to remember how all that works. AND they manage bank selects, special cases and paging. Nothing to forget. I'll talk about that more latter on.
Token pasting allows you to combine macro parameter values into new and exciting strings. This is the only string processing in SASM.
The SASM manual is a total waste on this one. First, the C<token??token> form is not correct. The actual form is just token??token or token??text or even this??token??that where token was one of the formal parameters of the macro. text, this, and that are just text.
Token pasting only pastes tokens to text. You can't paste local lables, variables, or even the VALUE of tokens. Just the tokens themselves. In this case, tokens really means the value of the parameters. So for example, if we want to paste an array name and index together to store values in calculated postions, we could try
ary MACRO name, ptr, val myName = name myPtr = ptr myVal = val myName??_??myPtr = myVal ENDM
but calling this with, for example, ary op, 1, 2 will get us myName_myPtr = myVal rather than the desired op_1 = 2. We could try:
ary MACRO name, ptr, val name??_??ptr = val ENDM
and if we called that with ary op, 1, 2 it would, indeed, compile op_1 = 2. But if we try to calculate the index via ptr = 1, and call it with ary op, ptr, 2 it will make op_ptr = 2 rather than op_1 = 2. The best we can manage in this situation is to use the standard old \# notation and add some logic to allow us to also retrieve values:
2 ary MACRO 3 NOEXPAND 9 IF \0 == 3 ;if they give us three parameters 10 EXPAND ;then they must want to assign a value to an index of the array 11 \1??_??\2 = \3 12 NOEXPAND 13 ELSE ;otherwise, get an index of the array back out. 14 EXPAND 15 \1 = ??\1??_??\2 16 NOEXPAND 17 ENDIF 18 ENDM 19 20 ary op, 1, 10 ;literal index 1 of the "op" array is set to 10 (0A hex) 29 =0000000A m op_1 = 10 37 38 =00000002 ptr = 2 ;computed index 2 39 ary op, ptr, 11 ; of the op array is set to 11? OOPS! 48 =0000000B m op_ptr = 11 56 ary op, ?(ptr), 11 ;but if we convert the name "ptr" to the value of ptr 65 =0000000B m op_2 = 11 73 =00000001 ptr = ptr - 1 ;and we really can compute indexes. Lets go back... 74 ary op, ?(ptr) ;...and get that index 1 value back 87 =0000000A m op = op_1 91 =0000000A what = op ;and there it is, safe and sound.
Here is the actual finished macro with a test to verify that the index is being sent as a number.
ary MACRO LOCAL tst NOEXPAND tst = ?\2? IF tst > '9' OR tst < '0' ERROR "USAGE: ary name, pointer, value WHERE: pointer is a number (use ary name, ?(ptr) ) and value is an optional value" ENDIF IF \0 == 3 EXPAND \1??_??\2 = \3 NOEXPAND ELSE EXPAND \1 = ??\1??_??\2 NOEXPAND ENDIF ENDM
The "callable" macro takes care of those pesky "Address xxx is not within lower half of memory page" errors. If you haven't seen that yet, you will. Try this:
org $300 cantcallme nop ret org 0 call @cantcallme
Welcome to the club! Since the call instuction only has 8 bits available for the target addres (the address of the subroutine you are trying to call) and there are only 3 page bits, the 12 bit wide PC needs another bit from somewhere. The designers of the 16C5x processors, many years ago, solved the problem by filling in bit 8 of the PC (bit number 9) with a zero when you do a call. As a result, if a subroutine is in the upper half of a 512 word page, where bit 8 of the address is 1, you can't call it. Actually, the same thing applys to
LowHalfPage = $ org $100 HighHalfPage = $ callable MACRO Name LOCAL NOEXPAND IF ($ & $100) != 0 IFNDEF LowHalfPage ERROR "Please add 'LowHalfPage=$, org $100, HighHalfPage=$' at the start of the program." ENDIF IFNDEF HighHalfPage ERROR "Please set HighHalfPage to the end of the first LowHalfPage area" ENDIF IF LowHalfPage >= HighHalfPage error "Out of LowHalfPage space" ENDIF EXPAND ;Our current location cant be called, we need to compile a jmp to here ;from a location that CAN be called Name??:code = $ ;don't forget where we came from org LowHalfPage ;move to a location we saved in low half page space Name ;set a global label for the new routine here jmp @Name??:code ;to the actual code of the (now) callable routine LowHalfPage = $ ;point to the next space in the table for next time org Name??:code ;move back to where we started ;ready to compile the routines code NOEXPAND ELSE EXPAND ;Our current location is callable, no need to do anything special Name ;set a global label for the new routine here NOEXPAND ENDIF ENDM ;lets try it! org $200 ;the lower half of the second page, usually no problem callable callmeup nop ;just a dummy subroutine, put whatever here ret org $300 ;the UPPER half of the second page, ALWAYS a problem callable callmetoo nop ret org $400 call @callmeup call @callmetoo
Here is part of the listing file we get from this:
60 ;lets try it! 61 =00000200 org $200 ;the lower half of the second page, usually no problem 62 callable callmeup 87 m ;Our current location is callable, no need to do anything special 88 =00000200 m callmeup ;set a global label for the new routine here 92 0200 0000 nop ;just a dummy subroutine, put whatever here 93 0201 000C ret 94 95 =00000300 org $300 ;the UPPER half of the second page, ALWAYS a problem 96 callable callmetoo 109 m ;Our current location cant be called, we need to compile a jmp to here 110 m ;from a location that CAN be called 111 =00000300 m callmetoo:code = $ ;don't forget where we came from 112 =00000000 m org LowHalfPage ;move to a location we saved in low half page space 113 =00000000 m callmetoo ;set a global label for the new routine here 114 0000 0011 m jmp @callmetoo:code ;to the actual code of the (now) callable routine 0001 0B00 115 =00000002 m LowHalfPage = $ ;point to the next space in the table for next time 116 =00000300 m org callmetoo:code ;move back to where we started 117 m ;ready to compile the routines code 126 0300 0000 nop 127 0301 000C ret 128 129 =00000400 org $400 130 0400 0011 call @callmeup 0401 0900 131 0402 0010 call @callmetoo 0403 0900
This next macro is nothing more than a container for a routine. It just contains a subroutine (could be any subroutine) such as a 16bit add or subtract, or a delay routine for example. The interesting thing is that it is designed to be placed in an include file and forgotten. If you call the macro in the main file, the first time, it will compile in a copy of the subroutine right at that point, with a jump around the subroutine for your main code, and then a call to the subroutine afterwards. Why would you want to do that? Because the second and following time, it only compiles in a call to the subroutine that was placed in the code the first time.
So why not just put the subroutine in like normal? Before the main code? Because with this macro, if you never use it, it takes up no space. If you use it once, it takes up only a few more words of code space than it would if it were hand coded, and then each additional use takes up the standard subroutine call. Now, you can define this macro, put it in an include file, and forget about it. It will not contribute to code bloat in your programs unless it is needed. As you continue to define more and more routines with this method, SASM grows from an assembler to a full featured language with your own library of keywords.
invc:n = 0 invc MACRO LOCAL around NOEXPAND invc:n = invc:n + 1 IF invc:n == 1 EXPAND jmp @around callable invc:code ENDIF EXPAND ;actual code goes here ret around NOEXPAND ENDIF EXPAND call @invc:code NOEXPAND ENDM org $300 invc invc
Earlier we talked about conditional compilation. The IF, IFDEF, IFNDEF, ELSE, ENDIF commands control SASM and allow us to get different bits of code out the door depending on our current situation. As I said, its a way of makeing SASM into a sort of an expert system and puts your knowledge into the compiler for your own use in the future. MiniMe compiles my code.
But what about putting decision making ability like that into the SX itself?
Doing accruate and reliable conditionals in assembly language is one of the
hardest tasts to master. If you haven't messed with comparison in assembly,
take an asprin before you start, then read
http://www.sxlist.com/techref/ubicom/lib/flow/compcon_sx.htm Or better yet, just use my macros from
|file: /Techref/new/letter/news0403.htm, 32KB, , updated: 2012/10/23 16:55, local time: 2018/4/25 15:28,
|©2018 These pages are served without commercial sponsorship. (No popup ads, etc...).Bandwidth abuse increases hosting cost forcing sponsorship or shutdown. This server aggressively defends against automated copying for any reason including offline viewing, duplication, etc... Please respect this requirement and DO NOT RIP THIS SITE. Questions?|
<A HREF="http://www.piclist.com/techref/new/letter/news0403.htm"> MassMind.org Newsletter 2004/03</A>
|Did you find what you needed?|
PICList 2018 contributors:
o List host: MIT, Site host massmind.org, Top posters @20180425 RussellMc, Van Horn, David, Sean Breheny, Isaac M. Bavaresco, Bob Blick, Neil, David C Brown, alan.b.pearce, Brent Brown, John Gardner,
* Page Editors: James Newton, David Cary, and YOU!
* Roman Black of Black Robotics donates from sales of Linistep stepper controller kits.
* Ashley Roll of Digital Nemesis donates from sales of RCL-1 RS232 to TTL converters.
* Monthly Subscribers: Gregg Rew. on-going support is MOST appreciated!
* Contributors: Richard Seriani, Sr.
Welcome to www.piclist.com!