CMP EMBEDDED.COM

Login | Register     Welcome Guest  
HOME DESIGN PRODUCTS COLUMNS E-LEARNING CONFERENCES CODE FORUMS/BLOGS NEWSLETTERS CONTACT FEATURES RSS RSS


PIC Programming in C Using Hand Compilation

by John Hilton

Most very small programs are written in assembly language because either no C compiler is available or the inefficiencies of the compiled code are unacceptable for the project. This article shows how to use the benefits of C and of high-end C compiler tools to develop assembly programs for any microcontroller.

Developing programs in C is faster and easier than in assembly language, yet most very small processor programs are written in assembly language. The reasons are either the lack of a C compiler or because the inefficiencies of the compiled code are unacceptable for the project. The techniques I present here show how to exploit C and high-end C compiler tools to essentially develop assembly programs for any microcontroller. You could say that these techniques show how to comment assembly programs in C, gaining syntax checking, source code browser capability, and function testing in the process. Having the code in C also provides greater portability, especially if it is to be ported onto a microcontroller with an adequate C compiler.

Although the techniques I present here focus on Microchip’s PIC microcontroller and Microsoft’s C/C++ programming environment, they are generally applicable. A number of the techniques apply to assembly programs in general, and solve difficult issues such as code and RAM bank management and the elimination of run-time stack pointers.

Microchip’s MPASM assembler had no linker when this work began. A linker provides highly desirable features such as the ability to easily rearrange code and variables. Using MPASM’s macro capability, many linker functions are implemented in a semi-automatic fashion by the techniques presented here. Even compared with the current linker, the hand compilation techniques avoid certain restrictions and complexities.

Target audience
I’ve written this article for programmers with a working knowledge of C and some knowledge of PIC assembly language. A reader without PIC assembler knowledge will have some difficulty understanding certain details but should gain an overall appreciation of the concepts.

Background
Hand compilation is the process of manually converting high-level language source code into assembler code. Hand compilation typically produces efficient code but can prove tedious and error-prone in some areas. In the past, coding a project in assembler was faster than coding it in C and hand compiling it. Modern C development environments, though, provide highly desirable tools that speed up the development cycle of C programs. A few small software tools can aid the hand compilation process dramatically, relieving or eliminating the tedious aspects.

The inefficiency problem occurred for me on a project in the late ’80s, in which the C compiler for a 6502 microcontroller made heavy use of a run-time stack and used a large set of working RAM “registers.” Driven by a desire to write the project in C, I developed a set of techniques to simplify hand compilation. I remembered having viewed a combined source/assembly listing from Microsoft’s early C compiler. You could see how the C program was actually implemented in 8088 assembler. To solve the 6502 problem, I realized that Microsoft’s C compiler could be used to generate a source/assembly listing. This code would then be hand modified, replacing the 8088 instructions with 6502 instructions. The 8088 C compiler generated labels and the associated jump instructions, alleviating some of the work. This approach worked very well in the late ’80s.

More recently I encountered a need to port C code to a PIC16C63 microcontroller. At the time the PIC C compiler was awkward (having no linker) and inefficient. The code would ideally be developed in Microsoft’s C environment and debugged in the PIC’s development environment, MPLAB. The tool set I present here achieved this goal. Many linker characteristics were implemented using assembler macros and variables. Various scanning utilities were written to aid the process and, in particular, stack optimization. The resulting toolset provided an excellent PIC development environment.

Conventions
The following typedefs are used in sample code:

typedef unsigned	char uint8;
typedef	char int8;
typedef unsigned	short uint16;
typedef	short int16;
Overview
To begin programming for a target microcontroller, you must implement the target’s programming environment. This involves writing two files, <target>.c and <target>.h, to define the target microcontroller’s special function registers (SFRs) and special instructions. The program is then developed as a normal C program in the host programming environment using all of the environment’s bells and whistles. The resulting host executable is nonfunctional because the registers are only variables. The program is then hand compiled by adding assembly code as special C comments. The program’s source files can then be converted into assembly files using a C2ASM program. The assembly files have the C code as assembler comments. Finally, the assembly files are assembled into the target’s executable. Assembly errors are corrected in the host programming environment and the C2ASM program rerun. Once the assembly code is free of assembly errors, the target debugging environment is used to debug the program. Using Microsoft Developer Studio and MPLAB, for example, the debug cycle is:

  • Find the bugs in MPLAB
  • Fix the bugs in the C source files in Developer Studio
  • Recompile as a C program in Developer Studio
  • Run a makefile to convert the C source into assembly source and assemble it into the target’s executable
  • Test the result in MPLAB
On Windows, it’s easy to switch between Microsoft C/C++, MPLAB, and the DOS makefile window during the debug cycle. Experimentation was also simplified because the MPLAB source files are different from the “real” source files. Temporary changes could be made in the MPLAB source files. These changes would be overwritten on the next run of the C2ASM program.

The essence of the technique can be shown with an example of a function in a file called sample.c that starts out as a normal C function:

// Sample.c
uint8 HexDigit( uint8 val )
{
val &= 0x0f;
val += ‘0’;
if (val >= ‘0’+10)
val += (‘A’-10) - ‘0’;
return val;
}
This example has the following hand compilation lines manually added to it as C comments:
// Sample.c
uint8 HexDigit( uint8 val )
{
//// _FUNCTION 0,HexDigit,0,0
////#DEFINE val Gen+0
val &= 0x0f;
val += ‘0’;
//// andlw 0x0f
////
addlw ‘0’
//// fmovwf val
if (val >= ‘0’+10)
//// movlw ‘0’+10
//// fsubwf val,w ;val-(‘0’+10)
//// fbnc LSR314
val += (‘A’-10) - ‘0’;
//// movlw (‘A’-10)-’0’
//// faddwf val,f
return val;
////LSR314:	crpor val
////	fmovfw val
////	freturn _HexDigit
////#UNDEFINE val
}
Sample.c is converted into sample.asm using a program that strips “////” from lines beginning with “////” and places “;” in front of other lines:
; // Sample.c
; uint8 HexDigit( uint8 val )
; {
_FUNCTION 0,HexDigit,0,0
#DEFINE val Gen+0
; val &= 0x0f;
; val += ‘0’;
andlw 0x0f
addlw ‘0’
fmovwf val
; if (val >= ‘0’+10)
movlw ‘0’+10
fsubwf val,w ;val-(‘0’+10)
fbnc LSR314
; val += (‘A’-10) - ‘0’;
movlw (‘A’-10)-’0’
faddwf val,f
; return val;
LSR314:	crpor val
fmovfw val
freturn _HexDigit
#UNDEFINE val
; }

Sample.c is the C code with assembler as comments. Sample.asm is the assembler code with C as comments. The assembly code incorporates many macros to simplify hand compilation.

This code is then assembled to produce an initial listing file. The macros expand to produce a voluminous listing that proves cumbersome for debugging. So this first listing is stripped clean by a utility program to leave the original C code comments and the clean explicit assembly instructions. This clean assembly source file is then assembled to produce a clean listing file useful for debugging in MPLAB. As you can see in Listing 1, the assembly code is quite efficient.

Code and RAM page issues
Every CPU has addressable ranges for code and RAM. On older or smaller CPUs, the addressable ranges were sometimes smaller than the available ranges. Various hardware techniques were developed to provide access to the available ranges by setting up some form of index to banks or segments of RAM and/or ROM. The x86 architecture is one of the most well known with its use of segment registers to access more than 64K. On the PIC, a PCLATH register is used to provide this access for the code page (PIC-speak for a bank) and the RP0 and RP1 bits in the STATUS register for RAM (register file in PIC-speak).

Managing pages can be a real headache for the programmer be-cause of the tedious and error-prone nature of manually handling page bits. Changing a function or a variable from one page to another can require intense scrutiny of code that calls the function or accesses the variable. The following techniques, although very involved to initially create, are easy to use. The benefits are:

  • Efficient code
  • Semi-automatic code bank and RAM bank handling
  • Easy switching of functions and variables to optimize bank usage and/or code speed

Although the techniques presented here specifically address a two-page implementation, the approach can be extended for more than two pages.

Programming with the PIC hand compilation toolset

Mnemonic macros and functions . One focus of the toolset is to be able to write functions with minimal concern for code and RAM page management. This feature has the added benefit of allowing functions and variables to be easily moved between pages, enabling the toolset to generate page-setting instructions only as required.

All the opcode mnemonics that are affected by either the code or RAM page selectors have an equivalent macro defined with a preceding “f” (for “far,” as per x86 style). The mnemonics affected by the code bank setting also have a macro with a “p” suffix (for “page”), except for fcall. For example:

fcall	_MyFunction
fgoto	LABEL0
fgotop	0,LABEL0
fbbc	BIT(MyBitVariable),LABEL1
fbbcp	BIT(MyBitVariable),1,LABEL1
fmovwf	MyVariable
The p suffix indicates the target address is preceded by the code page, 0 or 1. Non-p suffix macros simply implement the p suffix macro using the current code page value.

These macros track the code and RAM page bits and generate page-setting instructions as needed. They require some assistance from source code scanning utility programs to perform their task, as well as the assistance of the programmer to specify, at each jump target, a list of variables that could have affected the RAM page bit. The macros make use of assembly-time variables to store status information. Assembly-time variables do not exist at run time.

A function is assumed to exist within a page. 1 The _FUNCTION macro begins a function where the first parameter is the code page. This macro changes the assembly location, if necessary, using the ORG directive and sets an assembly-time variable, Cpage, to the selected code page. For example:

FUNCTION 0,MyFunction,1,0
selects code page 0 due to the 0 before MyFunction. The trailing numbers define the number of bytes required on the byte stack and the number of bits required on the bit stack. These stacks are assembly-time stacks and have no run-time code associated with managing them. More on assembly-time stacks later.

To call a function, use the function name with an underscore (_) prefix. A scanning utility creates a file called fndefs.inc with definitions for the functions that include their code page. For example, _MyFunction and the byte and bit stack bases are defined in fndefs.inc as:

#DEFINE _MyFunction 0,MyFunction
MyFunction0_SP	EQU 7
MyFunction0_BSP	EQU 4

So:
fcall _MyFunction
expands to:
fcall 0,MyFunction
The fcall macro first checks the current status of RP0, generating:
bcf STATUS,RP0
if necessary, then checks the status of PCLATH,3 generating:
bcf PCLATH,3
if necessary. Finally, fcall generates:
call MyFunction
Behind the scenes, fcall is also managing the assembly-time variables that track the status of RP0 and PCLATH,3.

Sometimes, setting the page selectors explicitly is desirable, which must only be done using:

 bsf STATUS,RP0
rpage SET RP_ONE

The assembly-time variable rpage tracks the status of RP0 and is either RP_NONE, RP_ZERO, RP_ONE, or RP_BOTH. Variable rpage is RP_NONE only after a non-skipped return or goto. The code page selector can be explicitly set using:
 bcf PCLATH,3
cpage SET CP_ZERO

Sometimes you’ll want to generate a page setting instruction earlier than, say, entry into a loop. This is best accomplished using:

prepcp 0,LABEL

for the code page or:

preprp MyVariable
for the RAM page.

The prepcp and preprp macros generate appropriate code and RAM page bit-setting instructions only when the current page selector doesn’t match that required for the argument. They also update the page status variables.

Don’t use skip instructions before a mnemonic macro. The macro may or may not generate a selector setting instruction, so a skip instruction before a macro is incorrect.

Calling conventions
There are four general-purpose working bytes: Gen+0, Gen+1, Gen+2, and Gen+3. Parameters are passed “little-endian” (low byte first), beginning with the W register, then the working bytes starting with Gen+1, then the routine’s stack. Listing 2 shows a few examples.

You can pass more than five bytes by allocating space on what is called a link time stack .

Segments
A linker loads object files into an executable or loadable image. This process involves placing similar items into contiguous blocks or segments. As items are placed in segments, their location in memory is determined. Code that accesses an item needs the item’s memory address to be put into the code’s instructions. The linker does this, thereby linking modules together.

The tool set has six segments:

  • BitSeg—contains all global and automatic bool variables
  • Ram0Seg—contains all non-bit variables in RAM page 0
  • Ram1Seg—contains all non-bit variables in RAM page 1
  • Code0Seg—contains all functions in code page 0
  • Code1Seg—contains all functions in code page 1
  • ConstSeg—contains all constant arrays and strings
By convention, BitSeg is placed in RAM page 0 and ConstSeg is placed in code page 1.

Variables
Defining variables . RAM variables are defined by appending them to the end of the required segment, as shown in Listing 3.

Note how the assembler variables Ram0SegEnd, Ram1SegEnd, and BitSegEnd are used to append variables to the end of the respective segments. This approach allows variables to be easily added and removed during code development. Different program configurations can be easily defined using #IF assembler directives, the allocation of space for the variables being automatic. This functionality is just what a linker normally does.

Accessing variables and STATUS.RP0 management . The mnemonic macros generate a RAM page-setting instruction if the top bit of the address differs from the current RAM page setting. For example, if rpage is RP_ZERO and TxLastTime is on page one, then:

faddwf TxLastTime,w

will generate:

bcf STATUS,RP0
addwf TxLastTime & 0x7f,w

Because page bit instructions are generated automatically, variables can be repositioned at will by modifying their definition.

To minimize the number of page bit setting instructions, you should place arrays (which are indexed through INDF—i.e., without using the page bit) into page 1, along with rarely used variables or with variables that are used within a module. Commonly used variables and the stack are placed in page 0. The calling convention implemented by the toolset ensures that page 0 is selected before calling any function and before any return. 2 Similarly, the toolset ensures the code page selector, PCLATH,3, is returned unchanged from a function.

Defining and using bits . A bit is accessed using its address and index. Using an eight-bit address plus the three-bit index means 11 bits are used to address a bit. PIC assembly requires bits to be defined using a “byte_address, bit_index” format. Hence the toolset implements four macros:

#DEFINE BIT_ADDRESS(ra,i) (((ra)<<3)|(i&7))
#DEFINE BIT(b) ((b)>>3),((b)&7)
#DEFINE BIT_INDEX(b) ((b)&7)
#DEFINE BIT_RADDR(b)
((b)>>3)

Bits are allocated together in the bit segment, BitSeg. Bit declaration and allocation are as follows:

extern bool
CommsPrintable,
AddLf,
PackingBits;
////CommsPrintable EQU BitSegEnd+0
////AddLf EQU BitSegEnd+1
////PackingBits EQU BitSegEnd+2
////BitSegEnd SET BitSegEnd+3
These declarations assign 11-bit addresses to the bit names. Examples of using bits are as follows:
 if (!CommsPrintable)
//// fbbs BIT(CommsPrintable),LCO531
PackingBits = TRUE;
//// fbsf BIT(PackingBits)
////LC0531: crpor BIT_RADDR(CommsPrintable)
if (AddLf)
PutChar( LF );
//// movlw LF
//// fcallbs BIT(AddLf),_PutChar
Arrays and constants . The PIC16Cxx architecture stores constants as arrays of RETLW instructions. To read a value at index i, the address of Array+i is loaded into PCLATH and PCL. Execution jumps to the RETLW instruction, which returns with the literal value loaded in the W register. Unfortunately, if an interrupt occurs just after loading PCL, the instruction following the desired RETLW instruction will be executed. 3 Microchip indicates that interrupts should be disabled to prevent this fault from occurring. Disabling interrupts is also an awkward issue because the occurrence of an interrupt just after BCF INTCON,GIE will have the effect of not changing GIE. Microchip recommends a loop to check that GIE was cleared. 4 These awkward issues trap many novice PIC programmers.

To address these issues, a lookup function is used to detect an interrupt and retry the RETLW instruction if one occurred, as shown in Listing 4.

The interrupt function sets InterruptOccurred. Now table lookups are very efficient. For example:

 x = array[4];
//// fmovlf HIGH (array+4),Gen+1
//// fmovlw LOW (array+4)
//// fcall __TableLookup_
//// fmovwf x
This table lookup technique has the added benefit that interrupts are not disabled.

Variable Initialization . Global variables are not initialized at hand-compile time. It would be possible to create initialized segments and load them from ROM before calling main, but the added complexity is probably not worth the effort. By convention, variables are explicitly initialized in a function called Initialize(), which is called by main().

Replicating the target environment
MPASM provides a number of predefined macros, variables, and constants in include files that define the target processor. To define the target environment likewise in C, two files exist. For the PIC16C63, they are P16C63.H and P16C63.C. These files are not used during assembly.

The target environment can be characterized in C by defining the special function registers, defining special processor instructions as macros, and implementing a bit type.

Defining SFRs . SFRs are accessed as either bytes or bits. It is desirable to imitate the assembler usage where, for example, NAME references the byte and NAME.BIT references the bit. C doesn’t provide for this directly but #defining _NAME for byte access is the best we can do.

To support this in C, use the following format in, say, P16C63.H.

extern union {
int8 _byte;
struct { B0:1, B1:1, B2:1, B3:1, B4:1, B5:1, B6:1, B7:1 } ;
struct {
RCD8:1, OERR:1, FERR:1, :1,
CREN:1, SREN:1, RC89:1, SPEN:1
} ;
} RCSTA;
#define _RCSTA	RCSTA._byte

In P16C63.C implement:
#define extern
#include “P16C63.h”
To access, for instance, CREN, use RCSTA.CREN. To directly access bits by their index, use, for example, RCSTA.B3. SFRs with elements consisting of two or more bits can be defined using, for example, “:2”. Note how the SFR is left capitalized as a style issue to match the assembler style. It does not denote the classic C constant or macro.

One benefit of this approach is that the compiler generates an error if an SFR is used with the wrong bit name. Assemblers typically do not detect this type of error.

On the PIC, the W register is defined similarly as:

extern union wreg_u {
uint8 _byte;
struct { B0:1, B1:1, B2:1, B3:1, B4:1, B5:1, B6:1, B7:1 } ;
} wreg;
#define _wreg wreg._byte

The W register is rarely accessed directly in C so instead of using just w, the name wreg is used to allow w to be used as a normal C variable.

Defining special processor instructions . Special processor instructions for the PIC that have no C functionality are implemented as intrinsic functions:

#define _nop_()
#define _clrwdt_()
The C compiler ignores the special instructions but hand compilation implements them. Intrinsic functions that may be required to test other functions on the PC in a test program can be implemented in C, but not hand compiled:
uint8 _swap_( uint8 val )
{
// C code implementation
return ((val<<4) & 0xf0) | ((val>>4) & 0x0f);
}
Defining a bit type . RAM is limited on tiny processors and the use of bit flags rather than byte flags saves not only precious RAM but permits the use of efficient bit instructions. Since RAM usage isn’t an issue in the C environment, a byte flag is used. The C++ bool name is used as a matter of style:
typedef uint8 bool;
The call, byte, and bit stacks
Programming languages typically implement stacks to store return addresses, parameters, and automatic variables used within a function. A single stack is commonly used for all three purposes and a stack pointer is used to keep track of the stack. The PIC16Cxx architecture implements a special eight-level call stack, but no access is provided to the stack pointer or the return addresses. No stack pointer or stack support is implemented for parameters and automatic variables. This design is very appropriate for a small RISC processor.

Let’s define a run-time stack as the common stack that has a stack pointer in which values are dynamically pushed and popped at run time. Reentrant functions (functions that may be called before a previous call to the same function has completed) can have multiple copies of their automatic variables at any one time. They require a dynamic run-time stack to operate. Non-reentrant functions only use one copy of their automatic variables. This single copy could be assigned an absolute RAM location and the code made more efficient by directly addressing the automatic variables rather than indirect addressing through a stack pointer. Before the non-reentrant function is called and after it returns, the RAM can be used by other functions.

Firmware programs for small processors usually don’t have reentrant functions. A number of C compilers take advantage of this fact to implement a static link time stack. The linker constructs the complete call tree and then allocates specific storage locations for the stack values according to worst case usage. For example:

main()
{
int16 a;
...
FunctionA();
FunctionB();
}
void FunctionA()
{
int8 array[4];
...
FunctionB();
}
void FunctionB()
{
int16 x;
...
}

The linker will assign two bytes for main(), four bytes for FunctionA(), and then two bytes for FunctionB(). When main() calls FunctionB(), the four FunctionA() bytes are not used and so a gap exists in the link time stack. Because the automatic variables now have absolute addresses, the compiler can create more efficient code that directly addresses RAM. The stack pointer is also eliminated.

Link time stacks are highly desirable for small processors because of the efficiency of the code. Since the direct addressing of automatic variables eliminates a stack pointer, RAM is efficiently reused.

I recommend implementing Boo-lean flags as single bits for small processors. Combining bits and bytes on a single stack isn’t feasible, so the hand compilation toolset implements separate bit and byte stacks. These are in BitStackSeg and StackSeg, respectively.

Stack management . The call, byte, and bit stacks are managed by the _FUNCTION macro, a scanning utility program (CREF.AWK), and a call tree analysis program (CREF.EXE). “CREF” stands for cross reference.

The _FUNCTION macro has the format of:

_FUNCTION CodePage,Name,ByteUsage,BitUsage
where CodePage is the code page, 0 or 1; Name is the function name; ByteUsage is the byte stack usage; and BitUsage is the bit stack usage. For example:
Reorient( int16 vec[3] )
{
//// _FUNCTION 0,Reorient,4,1
////#DEFINE vec SP+0
bool Negate;
int16 tmp;
uint8 i;
////#DEFINE Negate BSP+0
////#DEFINE tmp
SP+1
////#DEFINE i SP+3
//// fmovwf vec ;vec is ;passed in W
...
}
////#UNDEFINE vec
////#UNDEFINE Negate
////#UNDEFINE tmp
////#UNDEFINE i
//// freturn _Reorient

Here the function called Reorient is to be placed in code page 0, four bytes are used (Vec, tmp, and i) on the byte stack and one bit is used (Negate) on the bit stack. The _FUNCTION macro uses these values to ensure the code will assemble into Code0Seg, SP will be set to Reorient0_SP, and BSP will be set to Reorient0_BSP. These variables are defined in fndefs.inc, which is created by CREF.EXE.

CREF.AWK scans the listing file and extracts all the _FUNCTION lines, all the call and goto lines, and any quoted text following the text _CREF.AWK_. The resulting file, CREF.DAT, is used by CREF.EXE to build the call tree for the program and analyze the worst case call, byte, and bit stack usage. CREF.EXE creates the files FNDEFS.INC and CREF.NEW. FNDEFS.INC provides the “_” name and the “_SP” byte and “_BSP” bit stack definitions for the assembler. CREF.NEW gives each function along with the worst case call, byte, and bit stack usage, followed by a list of all the functions called by this function. CREF.EXE also prints the complete call, byte, and bit stack sizes, which can be used to refine SIZES.INC.

Beyond the eight-level call limit . The PIC16Cxx has an eight-level hardware call stack that is used by call instructions and an interrupt. Allowing, for instance, one level for the interrupt handler, leaves seven call levels. This limit may suffice for many projects, but it can present real problems if it is exceeded. Two degrees of workarounds are available.

First, you must determine the critical depth call path. If a function on this call path is only called from one location, the call and return instructions can be replaced by gotos. You must exercise care to maintain the call convention of the RAM page selector being set to page 0. For example:

 Single( ‘1’ );
////
movlw ‘1’
//// preprp 0x00
//// fgotop _Single
////SingleReturn:
//// crpor 0x00
============================
void Single( uint8 c )
//// _FUNCTION 0,Single,1,0
{
...
}
//// preprp 0x00
//// fgotop 0,SingleReturn
Note how the return page, 0, for SingleReturn is explicitly written.

If all of the critical depth call path functions are called from more than one location, the return address is given to the function. For instance:

 Single( ‘1’ );
////
fmovlf LOW RTN012,SingleSP+0
//// fmovlf HIGH RTN012,SingleSP+1
//// movlw ‘1’
//// preprp 0x00
//// fgoto _Single
////RTN012: crpor 0x00
//// nop ;required in case of
;an interrupt
============================
void Single( uint8 c )
//// _FUNCTION 0,Single,1,0
{
...
}
//// fmovff SingleSP+1,PCLATH
//// fmovfw SingleSP+0
//// preprp 0x00
//// movwf PCL
Notice the NOP following the return address to deal with the situation where an interrupt occurs just as PCL is loaded, causing the processor to return from the interrupt to the instruction after the target—in this case, the NOP.

Host testing and debugging of functionality
Code that doesn’t require access to the target processor’s environment can be tested in an isolated test program on the PC. For instance, a computation that converts raw readings into “cooked” values can be executed independently of the target processor. Intrinsic functions like _swap_() and functions that implement, for instance, an 8x8 multiply, can be coded in C for the test program. You can write a test wrapper that inputs test values and outputs the results, either interactively or to/from test files. The pure algorithmic C code can then be debugged using the full debugging capabilities of, for instance, Microsoft C/C++. This is especially helpful for fixed-point mathematics, in which overflow and under flow conditions can be tedious and difficult to detect.

Host debugging can speed up code development by eliminating algorithmic bugs far more quickly than using the PIC debugging environment.

Useful coding conventions
Labels . Branch target labels are given the form:

LMMNNN

where MM is a two-letter abbreviation for the source file and NNN is a random three-digit number.

C startup . C programs begin with a main() function. Prior to executing that main() function, you should clear RAM. Variables often need to be initialized to zero, and random RAM values can create unusual side effects on code operation. Clearing RAM, though, resolves both of these potential concerns.

Interrupt handling . Locate the interrupt handler, Interrupt(), at the interrupt address rather than trying to save the code page selector status and jumping to the interrupt function. Assembling the interrupt function first in the assembly process permits a valid setting for Code0SegEnd following Interrupt().

Reset handling . There are four instructions at the reset address, 0x000, before the interrupt address, 0x004. They are best used as:

bcf INTCON,GIE ;ensure ;interrupts are disabled

bcf STATUS,RP0 ;ensure RAM ;page 0 is selected

bcf PCLATH,3 ;ensure code ;page 0 is selected

goto Reset2 ;jump to the ;reset code

Refer to Reset() in the Hello World sample code, available on the Web at www.embedded.com/code.htm, to see how this is implemented using the page management macros.

Accessing bank-independent SFRs . The following SFRs on the PIC16C63 are bank-independent: INDF, PCL, STATUS, FSR, PCLATH and INTCON. Accessing these SFRs with an f macro isn’t necessary, as the standard assembler instruction will suffice.

“Self-healing” code . The real-world possibility of an electrostatic discharge or electromagnetic interference randomly changing bits can cause valid firmware to fail in varying degrees. Product specifications often include statistically acceptable failure levels for products. Testing would involve subjecting a product to repeated events, such as static discharge, at different levels and then measuring the number and degree of failures. The firmware programmer can have a significant impact on the results of these tests. If the firmware is written to be self-healing, the test results will be better than firmware that isn’t written to be self-healing.

Special function registers are best handled by writing a SetSfrs() function that is called regularly to put the SFRs in their correct state. Some SFRs can just be written with a single value. Other SFRs may have different values depending on some state or condition. Some SFRs may have some bits that should be set, others that should be cleared, and others that should not be touched. Some SFRs should not be written to unless they are obviously incorrect. In this way, the SFRs are continually kept valid.

Loops should be written to ensure that an invalid value will not cause an infinite loop to occur. Indices can be checked for validity and corrected. Programming with the potential for failure in mind can significantly improve the robustness of the product. You can go overboard to the extent that you compromise the cost or functionality of the product.

Code that deals with conditions external to the microcontroller is beyond the scope of this discussion.

Optimizing call/return sequences . If the last statement in a function is a call of another function, the normal code is:

MyFunction()
{
...
FunctionX();
//// fcall _FunctionX
}
//// freturn _MyFunction
It is possible to save an instruction by just jumping to the function:
MyFunction()
{
...
FunctionX();
//// preprp 0
//// fgoto _FunctionX
}

This approach can cause a code page problem if FunctionX() is on a different page from MyFunction(). You can address this problem by overriding the code page selector status, CpStatus, after the calls to MyFunction(). Alternatively, assembler directives could automatically select the call/return or the goto alternative based on the pages of MyFunction() and FunctionX(). Unless code space is at a premium, implementing the call/return sequence is much less error-prone.

Optimizing lone function calls . If a function is called from only one place, the call and return code can be eliminated by folding the function’s code into the place of its call. This process is similar to an inline function.

File organization. Writing a program as a set of modules that contain related functions and variables is desirable. Standard programming practice encourages module-specific variables and functions to be hidden from foreign modules. Programs for small processors gain little benefit from variable hiding. In fact, declaring all variables and functions in one file is often more convenient. If separate include files are used, then including all of them into one public.h include file is good practice. Public.h is then included in each C file. All the variables can then be defined by creating a public.c file, which is:

#define extern
#include “public.h”
void public() {}
Main.c should contain the startup and main functions along with, perhaps, error handling, intrinsic functions, and miscellaneous functions that do not belong in any other module. The interrupt handler is placed in interrupt.c.

Hello World sample program files . I’ve provided a sample program online that sends the message “Hello World\r” every second. This program can be used as a starting point for other projects. It provides a fair range of hand compilation techniques, along with general microcontroller techniques, and is worth studying in detail. Table 1 describes the files.

Further tool refinement
The next evolution of the toolset is to implement elements of the hand compilation toolset in an MPASM successor. Implementing the various segments and making them adjust their starting addresses automatically would not be too difficult. A call and jump tree could be created and RAM and code page selector instructions generated automatically. Messages would be generated for loop conditions, requiring the programmer to specify how to resolve them. Pseudo commands can be added to provide additional control over the operation as necessary.

The firmware programmer could now program with little concern for page bits. The page location of variables and functions could be easily adjusted to optimize the program.

Further suggestions
First, it appears to be possible to develop a scanning program that detects possible page violations and extraneous page bit setting instructions from the executable binary image itself. This program would be a useful PIC tool. Most likely the programmer would need to provide a list of jump targets for direct writing of PCL.

Secondly, C++ could be used, allowing for some nice enhancements. For instance, operators like multiply and add could be overloaded to implement specific fixed-point behavior of arguments. In general, though, the object-oriented techniques of C++ add too much complexity to the hand compilation process.

Benefitting the programmer
Hand compilation is a viable technique to implement portable, modular, and efficient programs for very small processors. The learning curve for these techniques is several days, but the payoff in overall project time is well worth the effort.

Caution is still required to correctly hand compile the intent of a C expression. Subtle differences between what the C expression defines and what the hand compilation executes can cause confusion. But this is the same issue as differences between assembly comments and the associated instructions.

The semi-automatic handling of code and RAM pages is a tremendous benefit for the programmer. Of particular value is the ability to move variables and functions easily between banks so as to optimize the program.

Projects with a C compiler can also benefit from hand compilation techniques because the key functions can be hand compiled into more efficient code.

John Hilton is currently VP, chief technology scientist at Spacetec IMC Corp. He has three degrees, a BS in computer science and physics, a BE in mechanical engineering, and an ME (Research), also in mechanical engineering. Hilton’s master’s thesis topic led to the invention of a product called a Spaceball, which is used today in the CAD and gaming industries.


Figure 1

Notes and References

  1. The programmer can, with care, implement a function that crosses page boundaries.
  2. This technique can introduce inefficiencies but, by keeping arrays in bank 1 and common variables in bank 0, they are minimal. Further evolution of the implementation could permit RAM bank setting to be a per function characteristic. Special cases can also be handled manually.
  3. Refer to Microchip’s Application Note AN556, “Implementing a Table Read,” under the heading “Interrupts,” www.microchip.com.
  4. Refer to Microchip’s Application Note AN576, “Techniques to Disable Global Interrupts,” www.microchip.com.
Table 1 Hello World sample program files

File name Description
makefile Project makefile for nmake.
hello.asm Project assembly file given to MPASM. It includes PIC16C63.inc, prepub.inc, public.inc, postpub.inc, fndefs.inc, intrpt.asm, and the project’s modules.
PIC16C63.inc* Standard MPASM include file defining the PIC16C63.
P16C63.h* C include file defining the PIC16C63 (somewhat similar to Microchip’s standard PIC16C63.inc).
prepub.inc* Contains macros and assembler variables required before assembling public.inc.
postpub.inc* Contains macros and assembler variables.
public.h Either declares all variables and function prototypes or includes all module’s include files. Included in all module’s “.c” files.
public.inc Assembler version of public.h created by C2ASM.AWK.
fndefs.inc Contains all function definitions and stack assignments. Created by CREF.EXE.
intrpt.c “Interrupt” module containing the interrupt handler, Interrupt().
main.c “Main” module source file containing main().
hellowld.c “Hello World” module that repetitively sends a message.
command.c “Command” module that processes commands received on the serial port.
serial.c “Serial” module for managing RS-232C communications.
buttons.c “Buttons” module for managing the buttons.
timer.c “Timer” module for managing the timer.
version.b Contains the version string and a generic date string that is automatically converted to the date of assembly.
version.c Automatically generated by the makefile from version.b.
*.asm Assembler version of their respective “.c” files. Automatically generated using the awk program C2ASM.AWK.

Embedded.com Career Center
Looking for a new job?
SEARCH JOBS

Browse all jobs

SPONSOR
RECENT JOB POSTINGS





 :