Embedded Programming Tips for CCS C
Implementation of datatypes like char, int, long, short, etc. are compiler specific. For example, the keyword int may mean an 8-bit unsigned integer in one compiler and a 16-bit signed integer in another compiler. This will often cause problems when people try to read or reuse your code. Be as specific as possible.
good: int8 some_int; int32 some_32bit_int; unsigned char some_unsigned_8_bit_char; bad: char some_char; int some_int;
Writing Directly to a Memory Address
When configuring the peripherals of the PIC, one usually checks the PIC's datasheet and manually writes the bits corresponding to the desired settings to the special function registers (SFRs). The CCS library attempts to make things easier by wrapping up the SFRs into function calls such as setup_adc, etc. However, some functionality may be "lost in translation," or the documentation may be so vague that it's easier just to the write the correct bits directly to the SFRs. Unfortunately, the SFR names used in the datasheet are not defined in CCS's device .h files, so we'll have to define them ourselves.
As an example, the CCS library doesn't have a function that will write to PR5 (Timer5 period match register) for devices (such as the PIC18F4431) that have them. However, we can look in the datasheet in the "Special Function Registers" section to find memory address of the SFR we want.
Note: When writing to 16-bit SFRs with a high and low byte which may be changing (such as a timer register), it is important to write to the high byte first and write to the low byte second. When reading from the 16-bit register, read the low byte first and the high byte second. The high byte is actually a buffer; writing to the low byte will automatically load the high byte into the register, and reading from the low byte will automatically copy the current value into the high byte register, in a single operation. This prevents the data from changing or rolling over in the time between reading or writing the first and second registers.
For the CCS compiler, you can use the #byte preprocessor directive to tell it to place a variable at a specific memory address. Look up the correct memory address in the datasheet (the 0x denotes hexadecimal notation):
//put variables at memory location #byte PR5L=0xF90 //PR5 low byte #byte PR5H=0xF91 //PR6 high byte
Later in the code, when we want to set the values, we can set it like any other variable:
PR5H = 10000/256; //take only high bits PR5L = 10000%256; //take only low bits
Alternatively, we can also use pointers:
int8* PR5H; int8* PRHL; PR5H = 0xF91; PR5H = 0xF90; *PR5H = 10000/256; *PR5L = 10000%256;
Sometimes, you want to have different versions of your code. Although you can keep different copies of your code or comment and un-comment out parts of your code, this can be very inconvenient. A much better method is to use conditional compilation with the #if directive. For example:
#define DEBUG_MODE //since we haven't explicitly defined the macro, it is assigned the value 0. //#define TEST_MODE ... ... #if defined(DEBUG_MODE) //this can also be written as #if defined DEBUG_MODE ... //This code gets compiled if DEBUG_MODE is defined #elif defined(TEST_MODE) ... //This code gets compiled if TEST_MODE is defined #else ...//This code gets compiled if neither DEBUG_MODE or TEST_MODE is defined #endif
We could also use logic operators with the #if directive:
#if defined(MODE_1) || defined(MODE_2) ... #endif
#if MODE==1 ... #elif MODE==0 ... #elif MODE>1 ... #else ... #endif
We can also use the #ifndef directive, which means "if not defined".
The volatile keyword tells compiler not to optimize the variable because it can change unexpectedly (e.g. through hardware or an interrupt). For example, you may have a variable that is constantly being read in a loop and never written to in the loop, but is written to in an ISR. A compiler might decide that your value is constant and replace it with a constant if it doesn't see the change being made in the ISR.
When to use "volatile":
- global variables that can be changed by an ISR
- variables that can be changed by hardware (e.g. if you put your variable in the hardware register of a counter)
example: volatile unsigned int counter_val; //this will declare counter_val to be volatile