I would like to do conditional compiling and include different C files based on the hardware being used. For example:
#ifdef HARDWARE_MODULE_1
#include "hardware_module_1.c"
#endif
#ifdef HARDWARE_MODULE_2
#include "hardware_module_2.c"
...
Everything works fine this way only debugging becomes a problem. Breakpoints don't work correctly when placed in the C file that's included that way. The program ends up breaking somewhere else random in the code (different C file altogether) When I manually add file to the project everything seems to be working fine. I'm using uVision V4.60.6.10
Still not clear though why would breakpoints not work in the approach described above breakpoints in includes have never worked.
BUT #ifdef mode1 module 1 stuff #elseif mod2 module 2 stuff .... will work
that can also be done this way file 1 #ifdef mod1
file2 #ifdet mod2
Presumably, because the name of the #included file is lost - the compiler only "sees" the name of the "outer" file which did the #include ?
As the others have said, the way to do this is by having different targets.
Or just have the entire content of the file within a #if ...
All right. If I go the route of multiple targets how do I handle includes from those hardware specific header files. For example, each hardware file defines SYSCLK value which is used in other header files. However, now I can't include the header file since I don't know which file is being used
Note that you can have different project settings for the different targets.
random_source_file.c:
#include "config.h" do_stuff ...
config.h:
#if defined(HARDWARE_1) #include "config_hw_1.h" #elif defined(HARDWARE_2) #include "config_hw_2.h" #else #error "Undefined hardware." #endif ...
main.c:
hw_init(); // will call the hw_init() for the target-specific hardware. ... for (;;) { ... hw_process(); ... }
So you have to define HARDWARE_x symbol in the options for each target. I don't really see any advantages to this approach then. It already works like that. I define the type of hardware I use in the options. Based on that all that files get included automatically. I don't really have any other target specific settings which would have to be changed besides that. The only problem is that you can't really debug those files which seems to be a uVision bug.
"It already works like that" Hrm. I would say your current solution works rather badly. And that's a big reason why you came here and started this thread.
The difference with targets is that you can do:
hw_init();
and the hw_init() from the specific target_xx.c will be called. So you get very clean code with few #ifdef section.
If you do not use targets, then you either need to do:
#if defined(HW_1) hw1_init(); #elif defined(HW_2) hw2_init(); #elif ... #endif
Or you need to either:
#include "config.h" #if defined(HW_1) #include "hardware_1.h" #elif defined(HW_2) #include "hardware_2.h" ...
or:
#include "config.h" #include "hardware_1.h" #include "hardware_2.h" #include "hardware_3.h" ...
and then in each hardware include file do:
// hardware_x.h #if defined(HW_x) && !defined(HW_x_H) #define HW_x_H ... #endif // defined(HW_x) && defined(HW_x_H)
and in every hardware_x.c do:
#include "config.h" #if defined(HW_x) #include "hw_x.h" ... #endif defined(HW_x)
In short - you get much fewer #if conditions in your source code if you are using targets, since the only #if you need is to decide which hw_x.h to include. Possibly, you might need:
#if HW_HAVE_FEATURE_X hw_init_feature_x(); #endif #if HW_HAVE_FEATURE_Y hw_init_feature_y(); #endif
if there are significant differences in capabilities between different hardwares.
Another advantage with targets, is that you can request Keil to build all targets. So a single build command can give you: firmware_hw_1.axf firmware_hw_2.axf firmware_hw_3.axf
Then all you need to do is copy the resulting output files into "v3.2.1/".
And you can manage that without changing any source files. So your source code management tool will not complain that needs to be checked in.
And if you then use a build machine, then it's common practice to only allow builds from the source code repository. Which mean you aren't allowed to modify any file to change the define from HW_1 to HW_2.
What you should also notice, is that C header files are not intended for the actual implementation of the code. The main exception is when you have small functions that you need to use in multiple source files and want them inlined. But that requires that the C compiler supports the concept of inline functions, allowing you to write:
__inline char intrusion_detected(void) { return (FIO1PIN & (1u << P1_INTRUSION)) != 0; }
"The only problem is that you can't really debug those files which seems to be a uVision bug."
You should probably think twice about the use of the word bug. I would say, it's a question of you having made an incorrect assumption after having designing your code in a way that is quite far from normal best practices.
Don't assume that a debugger will allow you to place breakpoints in a header file, since C header files aren't intended to store the implementation of functions. The debug information is likely to point symbols into a line number for an object file. And the object file is created from a *.c file. So don't expect that a debugger will be able to find magical translation information that will tell that the source line number in the object file isn't pointing to a line in the corresponding *.c file, but is actually a line number in a *.h file. And where should then the debug information store the name of this *.h file, so the debugger knows to open the correct header file?
Incorrect assumptions are really a very large source of errors in this world.
"The difference with targets is that you can do:
and the hw_init() from the specific target_xx.c will be called. So you get very clean code with few #ifdef section"
Like I said before it is EXACTLY how my code works. The only difference is that you add your target_xx.c file directly to your project and I include it in preprocessor:
#if defined(HARDWARE_1) #include "config_hw_1.c" #elif defined(HARDWARE_2) #include "config_hw_2.c" #else #error "Undefined hardware." #endif
random_source_file.h:
#if defined(HARDWARE_1) #include "config_hw_1.h" #elif defined(HARDWARE_2) #include "config_hw_2.h" #else #error "Undefined hardware." #endif
I don't see an overabundance of #if statements here and the code is pretty straightforward.
As far as a bug goes - I never said that I included code in the header files! Where are you getting it from? In the example above let's say HARDWARE_1 is defined. I compile code and put a valid breakepoint in the code in config_hw_1.c. Don't see anything "quite far from normal best practices" so far. I run the the program and it does break however not in the place of the breakpoint and not even at the same file. It places the yellow marker someplace else and not even on the code line but at some comments.
I thought about it twice and I'm calling it a bug.
I've thought about what you've written and responded with more than twice now. Better I don't say what conclusion I've come up with!
"As far as a bug goes - I never said that I included code in the header files! "
The big issue here is the following sequence, where you pretend that "hardware_module_1.c" is a header file and decides to #include it.
#ifdef HARDWARE_MODULE_1 #include "hardware_module_1.c" #endif
It doesn't matter if you include a *.c file from a *.h file or from a *.c file. The thing is that you should _not_ include a *.c file. If you want breakpoints, you should keep the code in the c file, and have the compiler directly process the c file.
The reason the C language got the concept of header files, is that the *.c (header files) are intended to be #include'd. And the *.c files are intended to be sent to the compiler to produce object files that can then be linked. So each object file is the result of one (1) *.c file.
You made the assumption that you should get a good result by including function implementations. Your assumption was wrong. And to my knowledge, Keil hasn't promised that they will have explicit support for breakpoints in code that has been #include'd.
"I don't see an overabundance of #if statements here and the code is pretty straightforward."
What you fail to notice here, is that while there exists two different alternatives that gives the same number of #ifdef needed, one alternative only includes function prototypes and one includes the actual implementation. So one alternative does what #include is intended to be used for, while the other alternative does not. One alternative works, and one alternative breaks your debugging.
So once more - #include is intended for header files. *.c are not header files. Whenever you decides to #include a *.c file - or place the actual implementation of functions in a *.h file, you'll have to accept the problems that follows. It is possible to drive a car to/from work in the reverse gear. But you can't call it a bug if the car is hard to control, gives you a pained neck, or if the police stops you because the break lights are on the wrong side of the vehicle in relation to your travel direction. The way you use #include is contrary to best practices - and you get to suffer the problems that follows from selecting a bad solution.
It's up to you to decide if you want to continue to complain about Keil, or if you decide to change your design and code as the language creators intended the language to be used. But no one will care about your complaints as long as you continue to do your transports with the car in reverse gear.
"I thought about it twice and I'm calling it a bug."
We can agree on that. Just that it isn't a bug in the Keil tools, but in code far closer to you.
"And to my knowledge, Keil hasn't promised that they will have explicit support for breakpoints in code that has been #include'd"
Neither does it make the opposite statement. Besides, Including C file into another C file is legal from the language standpoint.
" And the *.c files are intended to be sent to the compiler to produce object files that can then be linked. So each object file is the result of one (1) *.c file"
There is nothing wrong with producing a single compilation unit from several source files.
"You made the assumption that you should get a good result by including function implementations. Your assumption was wrong."
Why? For all your pontification and colorful albeit misapplied metaphors you failed to give a factual reason for what exactly goes wrong when you include .c file.
There is also another advantage in my approach. Other tools (or IDE's) will compile the project correctly.
One more thing. C language (and compiler) doesn't have a concept of source files and header files. It is just merely a convention. So preprocessor just copies and pastes them before compilation begins.
"Neither does it make the opposite statement."
Irrelevant. A bug is when the software functionality breaches the contract. What isn't part of the contract can't be a bug - but is quite likely a bad assumption.
If the car manufacturer promises the car can do 200 km/h, then it must be able to do that. But it doesn't tell if the car can do 220 km/h or not.
"Besides, Including C file into another C file is legal from the language standpoint."
The world is full of constructs that are legal from the language standpoint. The language allows you to write code that does division by zero - but the outcome of running the code will most probably not be a big success.
In your specific case, the compiler do produce a working program. It's just that you assume that you should also be able to have breakpoints in the included files. And a breakpoint in "hardware_1.c" would normally have the debug information in "hardware_1.o", but you never produced any "hardware_1.o"...
"There is nothing wrong with producing a single compilation unit from several source files."
That depends on your assumptions of the outcome, outside of the contract of the compiler being able to produce working programs.
The preprocessor will merge all #include into a single file that the compiler will process. But the line numbers in this merged file can't be used while debugging, since the debugger will not be able to open such a merged file.
So the preprocessor might then decide to not just insert line numbers, but also file names in the merged data to allow the debugger to not just map to different lines in the source file, but to map to lines in the different included files. But the thing here is that you need to separate what is required by the language, and what is implementation decisions when implementing the compiler, the debug format and the debugger.
"For all your pontification and colorful albeit misapplied metaphors you failed to give a factual reason for what exactly goes wrong when you include .c file."
Not actually true - you just decided to brush aside what I wrote.
Just as you can get a car with 2 doors or 5 doors, different compilers will have different functionality - there are a lot of things that aren't locked down by the language standard. Compilers that also supports C++ are much more likely to support breakpoints in header files since it's part of the C++ concept to have implementation code in header files.
Here is, by the way, a link to an older version of gcc that couldn't handle breakpoints in header files because the debug information could not handle more than one source file name per object file: www.delorie.com/.../faq12_6.html
That just about "all" C++ compilers supports breakpoints in header files doesn't mean you can expect all C compilers to do it. Since a C++ compiler needs to handle code in header files, a C++ compiler would use a debug format that supports it. And would use the same debug format when compiling C code.
The problem here is that most compilers you ever see something written about on the net are C/C++ compilers, i.e. capable of supporting both languages. So people makes assumptions based on only having seen luxury implementations. For many years, a huge number of people constantly reported bugs all over the net whenever a C compiler couldn't handle // end-of-line comments. Just because they assumed it was part of the C language standard since a very large number of compilers did add that functionality as a non-standard extension when compiling C.
"There is also another advantage in my approach. Other tools (or IDE's) will compile the project correctly."
The same thing would be true if each individual C file is separately compiled but with all code inside an #if/#endif block, and either assumes that you either have a global define in the project file/Makefile or a single header file that specifies the defined symbol. And then you would get debugging to work with all compilers that supports the generation of debug information.
"So preprocessor just copies and pastes them before compilation begins."
But that is a huge simplification that isn't really fully applicable when you start to thin about debugging.
What if I do this?
#define PREFIX "com1_" #define RXSIZE 32 #define TXSIZE 32 #include "uart.c" #undef PREFIX #undef RXSIZE #undef TXSIZE #define PREFIX "com2_" #define RXSIZE 128 #define TXSIZE 1024 #include "uart.c" ... com1_init(9600); com2_init(115200);
and then in "uart.c" have
uint8_t _(rxbuf)[RXSIZE]; uint8_t _(txbuf)[TXSIZE]; void _(init)(void) { ... }
The above can be correct C code. And the compiler can manage to produce a valid program. But every line in "uart.c" is used to produce two separate serial port implementations. So creative use of the preprocessor resulted in "template" support in C.
But a breakpoint at uart.c:152 would be the same source line that is used in both com1_init() and com2_init(). How would the IDE and the debugger then know how to translate from a source line into an offset into the binary when you try to set a breakpoint? Or should the IDE+debugger recognize the above and set a breakpoint both in com1_init() and in com2_init()? But if you then wait until the debugger breaks on com1_init() and you then press a key to remove a breakpoint - should both breakpoints be removed? Or just the com1_init() breakpoint?
C++ compiler vendors have to invest a huge amount of time to try to see how far they can get their debuggers to handle included code, because of class method code, template code, inlined code, ...
So "valid code" does not mean you have the right to demand that the debugger - any debugger - will be able to reverse-engineer what to actually do in all situations.
In the end, each and every developer will still have to stand for the consequences if they make an incorrect assumption.