This is the second installment on a series of post regarding software programming on PIC18F cpu family. You can find the first here.
You can (and you really should for a non-trivial project if you care about your mental sanity) program the 18F using the C language.
Basically there are two options – the first is the MCC18 compiler from Microchip and the other is the HiTech C. MCC18 is cheap and crappy, HiTech C is expensive and more optimizing (I cannot say whether is crappy or not since I never used it).
MCC18 is not fully C89 standard, on the other hand you need some extension to get your work done on this little devil. HiTech could be more ISO/ANSI compliant (I don’t know), but it is not compatible with MCC18 (is something they are planning to add in next releases, anyway I wouldn’t hold my breath). For this reason you’d better chose early which compiler you want to go with since they are not compatible. Probably you can manage to write portable code, but be prepared to write a lot of wrapper layers. Nonetheless you have to sort this out before starting coding.
Just to give you a hint about the compatibility problem I am talking about, apart from the way the two compilers provide access to the hardware registers, the HiTech uses the “const” attribute to chose the storage for variables, while MCC18 relies on non-standard storage qualifier keywords rom and (optionally) ram.
When I say that MCC18 is crappy, I have a number of arguments to support my point. Each point cost me at least a couple of hours to discover and work around, but sometimes I needed to spend days.
ISO/ANSI compliance is lacking from the preprocessor to the compiler. Not only preprocessor fails to properly expand nested macros, but it also messes up line numbering when a function-like macro is invoked on multiple lines.
For the first problem I haven’t found any workaround but to hand-code a part of the preprocessor work. For line numbers I use the backslash to foul the preprocessor in believing it is just a long line
#define A(B,C,D) /* macro definition */ A( longParameterB, longParameterC, longParameterD );
Compiler warnings are inadequate at best. For example you don’t get any message if a function that return a non-void type has no return statement. On the contrary when you compare an unsigned int to 0 (and not 0u) you get a warning. And you get warning for correct code, for example you can’t pass a T* to a const void* parameter without getting the warning, event if the two pointers have the same size and the same internal representation.
This behavior makes your life hard if your programming guidelines require maximum warning level and no warnings and doesn’t help you with real problems in your code. I use PC-Lint to spot real problems, but a run of gcc with some #define to handle non-standard constructs will spot most of them.
About warnings I had to fight back my loathing of useless casts and add them just for shutting up the compiler.
Given the poor state of the lad, I hadn’t been able to write a static assertion macro. Usually you write such a macro by turning a boolean condition in a compile-time expression that can be either valid or invalid (e.g. declaring an array with -1 or 0 elements, declaring an enum and assigning the first value to 1/0 or 1/1…). I haven’t found any way to get the compiler refuse any of these constructs.
One of the worst part of the toolchain is that it can produce code that breaks some hardware limitation without a warning. For example the compiler relies on a global temporary area for computing numerical expressions (array access is a case). The code generated expects that the temporary area is entirely contained in a data memory bank. The compiler nor the linker are able to detect when this area falls across a data memory bank boundary and alert the programmer. This is nasty because you can get subtle problems or having a failing program just after a recompilation.
Similarly the C startup code relies on a similar constraint for a group of variables, should they not fit in the same data memory bank, the initialization silently fails.
It took me few minutes to rewrite the startup code initialization routine and can’t see any noticeable slowdown.
I would advise to:
- rewrite the C startup code, keep in mind the limitations of the compiler (breaks on objects laid across page boundary, breaks on accessing struct larger than 127 bytes, break on accessing automatic variables if the space taken is some tens of bytes);
- use another tool (gcc/Pc-lint) to parse the source code and get meaningful warnings (missing returns, == instead of =, unused variables, uninitialized variables and so on);
- enforce data structure invariants and consistency by use of assertion;
- if you find a way to implement static assertion, let me know.
Next time I’ll write about linker and assembler.