A Special "Thanks" to Myke Predko for submitting the following project/article.
About the Author
Myke Predko is the author of "Programming and Customizing the PIC Microcontroller", the "Handbook of Microcontrollers" and "Programming and Customizing the 8051 Microcontroller" as well as the soon to be released "PC PhD" and "PC Interfacing Pocketbook" which are all published by McGraw-Hill.
As well as writing books on electronics and programming, Myke works for Celestica, Inc. in the area of New Products Test Engineering. His wife, Patience, and he have three children, Joel, Elliot and Marya.
Equates, Defines, Macros and Conditional Code
Lots of news before getting into this article on Macros and other ways to simplify your application development. The first is, I have finished going through the page proofs of "PC PhD" and it should be ready for going to the printers later this week. This book is about interfacing hardware to the PC along with a focus on MS-DOS software. Windows operation is also presented. The book contains four PICMicro projects that provide some basic interfaces for you to build from. This book is scheduled to be available in late August.
I am happy to announce that "Programming and Customizing the PIC Microcontroller" is finally available with a CD-ROM. The primary reason for going with a CD-ROM with this book is to avoid the problems that some people had with the diskette that came with it. The CD-ROM is identical to the diskette except that the YAP files have been updated with the latest level of code along with datasheets for the parts used in the book and an html interface. If you would like any of these additional files, please let me know and I will provide links to them for you.
The "El Cheapo" programmer has been generating a lot of interest from people and I have made a slight change to the software to better support more PCs. Check out article 4 for Version 2.1 and if you've built an El Cheapo - I'm interested in seeing any pictures of what you've come up with along with any comments.
For any applications that require PC code, I am thinking of only developing the applications from now on using Boland's "Turbo C". The reason for this is very simple, it can be downloaded free of charge from: http://community.borland.com/museum
I will be warming up my skills with "Turbo C" version 2.01. I have previously been using Microsoft "C" Version 3 which hasn't been available since 1988. When you download the code, note that you will have to load three diskettes with the Compiler, linker, include files and IDE before you can install them.
Lastly, I have a contest - check out the end of this article for your opportunity to win a copy of a special "Programming and Customizing the PIC Microcontroller".
I have received quite a few messages about my previous article about the bit-banging serial interface routines. These emails were questioning the macro code I was presenting and how advisable is using complex macros like these. The issues that were brought up were the difficulty in debugging the resulting code, understanding what it actually is doing and the loss of control over the application. These concerns are understandable and completely valid - but I think I have come up with a formula that avoids these problems.
This article is a primer on the various features of MPASM (and most other "macro assemblers") which can be used to make your application development simpler and easier to work through to understand what is happening. As I work through these features, I am presenting the knowledge toward creating macros that can greatly simplify your assembly language programming, both from the development point of view as well as from the debugging and understanding somebody else's application.
As I wrap up the article, I will go through the development of a "delay" macro that can be used with the Bit-Banging Serial Interface macros I've previously presented that takes advantage of all the features I'm going to talk about in this article and point toward a "standard" for developing interface macros which will simplify sharing code between users as well as make the development of PICMicro applications much more efficient.
There are six programming constructs are used prior the assembly or compilation of your application. This takes place in the "Macro Processor", which are also be known as the "Pre-Processor". This program's function is to read through the application source code, bring in all the "included" files and identify which blocks of code are to be ignored by the assembler and convert all the labels to numeric values to allow the assembler to convert the source code into a .hex file.
Normally, the macro processor converts all labels to constants except for addresses. The address conversion is carried out by the assembler as it works through the code.
In this article, while I am discussing how the six programming constructs work, what I am really doing is introducing you to a whole new way of programming that executes an application before your application is assembled into a file that the PICMicro can use.
The six constructs I am going to discuss in this article are:
ArbitraryPin EQU 4
will insert the numeric "4" for the string "ArbitraryPin", each time it is encountered.
I am stressing the idea that a numeric is evaluated and assigned to the label to help differentiate the "equate" from the "Define" or "Variable".
SecondPin EQU ArbitraryPin * 5
will pass the numeric value "20" instead of the string "ArbitraryPin * 5" or even "4 * 5".
When you look at the Microchip include (".inc") files for the different PICMicro devices, you'll see that all the hardware registers and their labeled bits are equates.
Variables are memory locations that can be defined within an application to help with the processing of operational parameters. For example, if you wanted to calculate the number of cycles between I/O pulses working at 9600 bps in a PICMicro running at 5 MHz, you could use the code:
IOCycles = (Frequency / 4) / Speed
to calculate the value "130".
The "variable" probably seems very similar to the "equate" as it saves a numeric value as well, but there is one important difference. "Variables" can be updated multiple times in the application, where as the "equate" can only be set to a value once.
Defines, on the other hand have the string assigned to them after instead of a numeric value. This is useful for setting up string data or multi-parameter data values.
The format for a "define" is:
#define ThirdPin ArbitraryPin * 10
In the case above, every time "ThirdPin" is encountered, the string "ThirdPin" will be replaced by "ArbitraryPin * 10". After the replacement is complete, the assembler will then look for any other labels and replace them with the appropriate equates, defines, variables or macros.
"multi-parameter data values" is the term that I use for Defines that have more than one piece of information. The classic example, for the PICMicro, is to use defines to specify complete bit information.
For example, the "GIE" bit of the "INTCON" register could be "defined" as:
GIE EQU 7
"GIE" in Intcon
which allows the GIE bit to be used without the programmer remembering which register the bit is in (and save a bit of typing).
To set the "GIE" bit, with this define, the following statement can be used:
bsf GIEBit ; Set the "GIE" Bit
which is easy to code and avoids much of the hassle of having to go back and reference the bit definition to understand what register it is associated with.
Just a quick point on this individual example. When you are passing a define like this to an instruction or a macro, note that you are actually specifying two parameters and not one. Because the define label is a single value, it is easy to think of what it represents as a single value. I have seen this mistake in quite a few beginners with them questioning why the assembler is stating that the number of parameters to a macro or instruction is different than the number expected. When you define a "Bit", always remember that the specification includes a register and a bit within it.
For all my applications, I now define my individual I/O pins using the format:
IO EQU #
; Define Bit Number
which really makes coding the application faster and easier. The reason why I use the separate equate is if I have to set or reset the bit. Using the "Bit Shift" operator ("<<" for left, ">>" for right), I can set the bit using the code:
iorlw 1 << IO ; Set the Specific Bit
or reset it (by getting the inverse of the value):
andlw 0x0FF ^ (1 << IO) ; Reset the Specific Bit
As I indicated above, Microchip has provided "include" files (ending with ".inc") which contains all the equates to the different registers and their bits in a specific PICMicro. The normal convention for the files is "P<PICPN>.inc" where "PICPN" is the PICMicro's part number (ie "16F84"). These files should be used exclusively in your application to define the registers and bits.
I emphasize this for two reasons. The first is that many people modify these files for specific applications by deleting unused registers or adding their own equates. This makes the include file specific to their applications which can be a problem when they want to change to a different PICMicro.
Recently, Microchip seems to have made the 16C61 obsolete, which I used for the YAP programmer. To fill new orders, we went with the 16C711.
Because I chose to use the Microchip include file without modification, to change to the new part number, all I did was change the PICMicro specified in the "LIST" statement and the include file to "P16C711.inc" and rebuild the application. This change was fast and the likely hood for problems was very small. If I had modified the Microchip 16C61 include file for my own purposes, then I would have to make the same changes to the 16C711 and retest the application to make sure I didn't miss anything or had made a mistake.
Include files themselves are inserted in line into the application. This means that they could be used as simple macros (the file's contents is put directly as source code into the application). The problem with this method is that parameters cannot be easily passed into the code that has been added.
While, equates, defines and variables could be used to pass parameters to the include files, the macro parameter interface is much easier and more intuitive. I put the "include" files right at the top of the application simply because this seems "neater" and more organized to me. By doing this, I am basically forcing myself to use macros to transfer code from the include files into the application.
One very nice features about Microchip's MPLAB is that execution single stepping jumps to an include file if the code is executing out of it. This is also true for macro execution. I should caution you not to place breakpoints in include files to avoid the problem that the code in them is invoked twice and the breakpoint occurs when you don't expect it.
Macros are similar to include files which have been placed at specific points in the source code. When a macro is "invoked" (ie its label found in the source), it is copied directly into the source. For example to add the "jz" macro (the "Jump to Address if Flag is Zero") to source code, it would be defined along with the code that uses it:
jz macro JumpLabel
; Jump to the Specified
The macro invocation is the "jz ThisLabel" statement in which the code between the "macro" and "endm" statements is inserted into the source. So, the code that is actually assembled is:
movf AnyReg, w
; Mainline Code
To indicate that a macro is being defined, the label you want to use along with "macro" directive is presented btween the label and the parameters. At the end of the Macro, the "endm" directive is used on a line all by itself.
The "parameters" are strings which replace the parameter string when the macro is inserted into the code. In the example above, the Macro parameter "JumpLabel" is replaced in the macro replacement code with "ThisLabel".
Macros may seem like they are like subroutines, but it is important to notice that they are different in two important respects. The first is the code is inserted in line. In the PICMicro, this can be important because four instruction cycles (two for the "call" statement and two for the "return") are saved.
The other difference is in how the parameters to the routine are used. In a subroutine, you will have to save the parameters in some manner. In the subroutine, if you change any of these saved parameter contents then there isn't a problem with the caller's version. If the parameter's contents in a macro is changed, then there may be a problem with the mainline's version being changed inadvertently. To be on the safe side, macro parameters should never be changed within a macro.
At the start of this article, I have put up a diagram which shows how the macro substitution works. In this diagram, I have shown a macro with "conditional code" which executes when a specific condition is met (if the "Define" label is present). If it is not (as in the example), the code is not inserted in the source code to be assembled.
Macros themselves come in two types. The first is what I call "Wrappers", simple code which eliminates extra typing or avoid having to work through different applications over and over again. The "jz" macro presented above is a good example of this.
These types of macros are actually more common than you may think with the enhanced MPASM functions (such as "sknc") designed to make the assembly language development simpler are actually "Wrapper" macros in disguise. The "sknc" macro (skip if the STATUS Carry flag is not set) could be written as:
; Skip if Carry Not Set
The second type of Macro, and the type that I want to discuss in this article is more complex with an eye toward providing sophisticated functions to the PICMicro without requiring significant effort on the part of the application designer. In my last article, I presented the Bit Banging serial interfaces and previous to that the button debounce interfaces which show off this philosophy. To create these sophisticated macros, while they follow the simple instructions presented here, they require all the macro processor ("pre-processor") functions that I am discussing here to provide the complete functions.
When you fire up "MPLAB", look under "Directives" for "MPASM", you will probably feel like you are being overwhelmed by the wide variety of different "Directives" that can be applied. Many of these directives are required for assembling the program and only a few are actually used in macros. These few are what I want to discuss.
A very common directive that is used for defining file registers to be used in an application is "CBLOCK". This directive is used to allow the application engineer to "reserve" file registers for the application. The format of the CBLOCK is:
CBLOCK <Memory Start>
; Define Application Variables
This method of declaring variables is quite good, but I have gone away from it in the past few weeks because macros cannot take advantage of it. Instead I have created the "vars.inc" include file which is used to declare the variables. Click Here to download vars.inc.
To define the memory block to access, the "VarStart" macro is used in the following format:
VarStart <Memory Start>, <Memory End>
To define a variable, I use the "VarAdd" macro:
VarAdd <Variable Name>, <Size in Bytes>
This method of defining variables is similar to "CBLOCK" except for two important differences. The first is, the end of the memory area to be defined can be specified. If more variables are required than are available, then an error will be invoked.
When I use this macro for applications which need buffers, I "End" VarStart at the start of the buffers and this way I can ensure that no variables encroach on the buffer space.
The second advantage of using this macro is that variables can be allocated from macros. This may not seem like a significant advantage because variables can be declared outside of the macro, but this workload is avoided and ensures that memory is not unexpectedly shared between different macros and sections of code.
In the complex macros that I am going to present, you will see that the "error" directive is used quite a bit. This directive is placed in line, in the format:
error "Message String"
Normally this macro is invoked from within "Conditional Assembly Code" statements and only be passed to the assembler under specific circumstances. The "Conditional Assembly Code" statements "if"/"if(n)def"/"else"/"endif" and "while"/"endw" execute in the macro processor and can be used to determine which statements are actually passed to the assembler.
For the "VarStart" example above, this macro should only execute once. To ensure that this is the case, I use the code:
In this code, the "ifdef" directive is "true" if the define "VarStartVar" is already declared. If it is already true, then I force an error in the assembly. If it is not already defined (which means it is the first time through), I then define the label to make sure that later incarnations do not successfully assemble.
"While" with its companion "endw" directives will repeat until the condition is no longer true. For example, clearing the variables defined in "VarAdd" could be accomplished using the code:
i = VarStartAddress ; Set
"i" to Starting Address of File Registers
If four variables, starting at address 0x00C are declared, these conditional statements will produce the code:
While this seems to make a lot of sense, it won't when you look at the listing file produced for the application. In this file, you will see the "while" statement invoked five times (once to fail) with the "clrf i" instruction placed after four of them. This can make single stepping through a macro sometimes very difficult from MPLAB.
With the conditional assembly statements and the other features of the MPASM assembler I have discussed in this article, I can now look at macros from a different perspective. Instead of them being sources of code to insert into an application, they are actually small programs which insert code appropriate to the current situation.
To show what I mean, I want to go through the development of the delay.inc file which provides the "dlay" macro which will give you a specific delay for a PICMicro running at a specific speed. This macro was written for an application I am writing at home to control an LED from a Serial Port and I used the "intser.inc" interrupt based "Bit Banging" serial interface routine.
Because I was starting to work on the macro article, I wanted to come up with a macro that would fit in with the philosophies that I am trying to come up with. In doing this, I wanted a macro that would insert the delay code after a user supplied label. The invocation would be:
My first pass at this was to use a pretty pedestrian sixteen bit delay based on traditional cycle counting delays that can be found on the Internet (including my web page). The macro I came up with looked like:
Dlay macroSpeed, Delay ;
Delay Set Number of usecs
This macro is pretty good for setting delays in non-interrupt based systems. But because I was using the interrupt based "Bit Banging" routines, I found that the actual delays were about twice what were specified to the macro. This was caused by the regular interrupt handler taking away cycles from the loop.
The response to this was to see if I could use the regular interrupt timing. This was first done by knowing that the variable "RTCActual" was produced by the interrupt delay set up code as the actual delay between interrupts in usecs. This meant that the specified delay could be calculated from the number of times the interrupt handler is invoked by the timer.
Note that in the macro above, I only define the file register (application) variable "_dlay" once by using the define and the conditional assembly code.
The code to do this check is:
Dlay macro Speed, Delay
; "Delay" in usecs
Note that within the interrupt timer ("RTC") delay code, that I check to see if the difference is greater than 256 and if it is, I compare to both RTC variables. This allows quite large delays as well as reasonably short ones.
If you are familiar with this method of timing delays, you will probably recognize that I have given up being able to specify precisely how many cycles are going to be delayed. As well, I err on the high side, adding two interrupt invocations to the expected final RTC value to ensure that the minimum timing delay specification is met.
For most applications (such as the LCD delay timing), this is acceptable because the minimum delay is the critical parameter. If precise timing is required, then the application should use a timer for the providing it instead of a polling loop with interrupts taking place in the background.
From the macros I've presented in this articles and the previous ones, I have tried to adhere to the following rules for defining and specifying the macros:
- Consistent across hardware (ie the Different Serial
Interfaces) and Processors
My ultimate goal is to create a standard library that application developers can use to develop their interface code for applications. There would be many advantages to this method. The biggest one would be the ease in which code could be shared. Rather than taking code from somebody else's source and trying to modify it to work in another one, the macros could be used directly.
In later articles, when demonstrating new interfaces, I will provide an include file with a macro interface in the format I've shown here to allow you to use the code in other applications.
As I have written this article, I was thinking a lot about creating structured language constructs (fancy words for "if"/"else"/"endif" and "while"/"wend") using macros. In fact, as I was writing this article I decided to create a little application to try out different strategies of implementing them.
Well, I figured out how to do them. I'm very pleased with what I've come up with (although they are quite complex). Before writing the next article, I will work at creating a full set of these macros and present them with any comments I get back on this one and my philosophies for using macros to make assembly language programming easier.
Copyright and Warranty Statement
This article is presented on an "AS IS" basis. I have tested the circuit and code that I have presented here and I am confident that it works on the hardware that I have used. Different hardware may result in different results.
The information contained here cannot be reproduced without the author's permission.
Program the PIC in simple BASIC using the PicBasic Compiler. Visit: http://www.rentron.com/PicBasic2.htm