Best way to write modular 'C' code for microcontrollers - Page 1
Forum section: electronics / general

author: ZeroResistance
author_rank: Frequent Contributor
timestamp: November 17, 2016, 02:00:59 pm
content: I'm trying to write an interrupt based driver for the PS/2 keyboard using PIC24F.What I'm trying to understand is what is the best way to modularize this code.1. This piece of code will use some pins and resources and interrupts of the uC.2. Where do I define these I mean suppose I want to use a pin of PS2 clock and PS2 data, I see that the standard procedure is to make a '.h' file of the same name like 'ps2keyboard.h' and put these into that file.3. But what about fixed resources that the module uses like a fixed external interrupt pin like INT0 or INT1 how do I ensure that no one else uses that resource.4. Then comes interrupts again this would have its own interrupt handler where should the interrupt related code go?5. Then comes other peripherals like timers, uart, if this module needs to use those resources where would those be defined and how would those be used?I've just used the PS/2 keyboard as an example here but in general this question applies to the design of modular code and then reuse of that code across other projects.There may be other points which I have missed and I would like to hear from forum users what would the best way to approach this issue.TIAZR

author: obiwanjacobi
author_rank: Super Contributor
timestamp: November 17, 2016, 02:30:02 pm
content: Good question, I've always wondered that myself.If you said C++, I would have some kind of answer, but C....2) But then you would have to edit the ps2-file.h when pin assignments change. Now that is not very modular IMO.I gues you could take this as far as you like. You would probably take it further if you were writing a library, than just an 'app'. Most would assume you know everything that is going on in the app, so no need to make it all generic and modular.

author: JPortici
author_rank: Super Contributor
timestamp: November 17, 2016, 02:57:03 pm
content: Quote from: obiwanjacobi on November 17, 2016, 02:30:02 pmnothing that can't be done with defines.you want static pin assignment because the code gets optimized a lot , at least on lower end microsyour library header could contain something likeCode:#ifndef _ALTERNATE_PIN_ASSIGNMENT/* Default direction mask *//* Default pin assignment */#warning "Default Pin Assignment use. Refer to library documentation"#endifinterrupt are a bit more of my concern too. that means you have to restrain for using specific resources, which sucks and will make things harder for future projects with those libraries. I am doing it because i'm software emulating some peripherals and in that case a dedicated timer and interrupt has been a must

author: ZeroResistance
author_rank: Frequent Contributor
timestamp: November 17, 2016, 03:48:55 pm
content: Quote from: obiwanjacobi on November 17, 2016, 02:30:02 pmI don't mind you answering for C++ too however I wonder if the xc16 compiler supports C++.Yes to edit the ps2-file.h would be not sensible for every new project would there be some way to pass pins into this module, so that from main we can specify pin assignments.

author: JPortici
author_rank: Super Contributor
timestamp: November 17, 2016, 04:34:54 pm
content: yes. read my reply. that's how i do it. not sure if it's the best practice but it's still a better way than some digital write style crap

author: bktemp
author_rank: Super Contributor
timestamp: November 17, 2016, 04:40:10 pm
content: Quote from: ZeroResistance on November 17, 2016, 03:48:55 pmAssigning pins at run time works fine if the controller supports peripheral pin select. Otherwise I wouldn't do it, because it leads to inefficient code and is rarely necessary. Switching interrupt pins can be done by adding a HAL layer with callback functions that can be assigned during runtime. For small controllers this is useless, because it waste many kbytes of flash and it will also waste a lot of CPU time for small interrupts because of the increased overhead.If you don't like to edit the ps2 header file, add a ps2_config.h file where you can select the pin configuration. This is often used, because it generates the most optimized code.

author: nfmax
author_rank: Super Contributor
timestamp: November 17, 2016, 04:47:21 pm
content: Maybe you could use a pre-build script to process a pin defintions file (in a format of your choosing) and generate the actual .h files. This could be called from the makefile so the .h files only get regenerated when you change pin definitions.

author: rstofer
author_rank: Super Contributor
timestamp: November 17, 2016, 05:27:44 pm
content: I tend to write a .c & .h file for each peripheral. I don't have any clever way to preclude using a pin twice but it hasn't happened yet. I usually have a file, init.c, that calls all of the initialization functions in the proper sequence. If I put the interrupt handler in the peripherals code file, I need to expose the function so that I can program the NVIC. Not a big deal. If the program is fairly trivial, I might put all of the handlers in the interrupt file. I guess it depends on how I feel at the moment.I don't spend a lot of time worrying about porting the code to another device. If I am writing for an mbed, I'm not thinking about an Arduino or even an STM32F4. Maybe the high level code will port easily but it's just a given that every detail of the hardware dependent code will change. No point in worrying about it.

author: nctnico
author_rank: Super Contributor
timestamp: November 17, 2016, 05:31:14 pm
content: I usually have a hardware.h which holds all the pin GPIO pin definitions & control macros, system timer frequency, clock frequency. Then each hardware related software module has it's own initialisation & control routines for the peripherals it uses. I'd avoid making too generic layer when it comes to hardware because it is easy to create layers which don't add any real functionality.When it comes to handling a protocol it is a good idea to split it into a part which handles the physical layer (receiving & transmitting packets) and another part which deals with the higher level protocol part. This should make it easier to port the code.

author: John Coloccia
author_rank: Super Contributor
timestamp: November 17, 2016, 05:35:49 pm
content: It's easier if you have a bunch of processors in mind. It's hard to guess at what low level code on one type of processor will look like on another processor.My suggestion would be to write the code for the particular project that you have now. Take pains to make it neat without any bad hacks or things like that. When it's time to port it to some other processor or project, one of your tasks will be to go to this code and at that point get an idea for what the similarities and differences are. You'll be in much better position to write modular code once you have an intimate understanding of what can and can't be modular.What you don't want to do is spend gobs of time guessing at it and creating a system that is only of limited value when you have to move it. It's better to have a good solid idea of what you're really up against when you're talking about low level code.Sometimes, and this isn't what you expect to hear from a software engineer, the most practical way forward is to just not worry about it because the complexity and compromises of making something portable outweigh the benefit you gets. Sometimes, it's easier and more reliable to simply rewrite some simple bits of code than it is to maintain a more complicated library. You have to use your judgement here.

author: obiwanjacobi
author_rank: Super Contributor
timestamp: November 17, 2016, 08:46:56 pm
content: Besides the question if you should or should not, the question is how if you do.Using #defines as an API to a software component/module is very suboptimal IMO.For a hint on my C++ answer see my signature...My philosophy is that you try to create a library of generic and reusable components that are fully (unit) tested and known to be good. That means that you should not have to change a single character to use it in your project. After a while creating a new app is very easy because you have a lot of known good building blocks to pick from.You harvest generic code from existing projects (like John said in the previous post).

author: rstofer
author_rank: Super Contributor
timestamp: November 17, 2016, 09:12:03 pm
content: Quote from: John Coloccia on November 17, 2016, 05:35:49 pmThe best FORTRAN programs were written after the box of cards was dropped. It's amazing how much more clear the solution is when you can start fresh with prior experience.

author: hans
author_rank: Super Contributor
timestamp: November 17, 2016, 11:04:03 pm
content: You can get quite far with defines and preproccesors tricks in C, but creating a dependable and testable code base without reaching out to very messy hacks is near to impossible. You will quickly reach to const array's (that need to be optimized away - they should not appear in FLASH) or maybe even worse: macro tables.However I think that if a truly modular code base is a requirement, switch to C++ and ditch all MCU's that don't support it. It's unfortunate that XC16 does not have a C++ compiler, which I really don't understand from Microchip. AVR has it, MSP430 has it, and I would consider the PIC24 to be more than capable of it.After all, it's up to the programmer to use the provided language features wisely. Static classes andsometemplates can be extremely useful, as long as you're aware of what code all stuff translates to. For other chips, this site is quite good fun to get a live view of several GCC compilers:https://godbolt.org/I also saw this project the other day:https://github.com/fabio-d/xc16plusplusBut I don't know how up-to-date this is, if it works, or how stable/bugfree it is.Would like to try it out some day when I got the time.

author: John Coloccia
author_rank: Super Contributor
timestamp: November 17, 2016, 11:20:29 pm
content: FWIW, there's really no reason you can't write object oriented code using C....or assembly, for that matter. It's a pretty common design pattern to use structs with function pointers, bind the function pointers (and maybe even some data pointers to other structures) at runtime depending on the specific hardware, and voila....you've got something resembling objects in function, if not in appearance. Obviously you don't want to recreate some complex C++ style mechanisms using C code, but this is a nice, simple way of burying a lot of the messiness in one place...namely in your startup configuration code.But to do it sensibly, you still need to have a pretty good idea of what your hardware going to look like or you'll end up either building an abstraction that doesn't go far enough, or building an abstraction that is so complex it dwarfs the simple task of just writing the code from scratch, salvaging what you can from other projects.It really helps to get a few boards up and running, and to consider those "prototypes" in the sense that you intend to eventually scrap that code and replace it with something more generic. Once you get some experience with a platform, you can usually jump right in and create a usable library from day 1.When you've done this for a while, you eventually end up with a bunch of usable libraries and ideas that tend to follow you around like a loyal pet.

author: Howardlong
author_rank: Super Contributor
timestamp: November 18, 2016, 12:14:09 am
content: Quote from: John Coloccia on November 17, 2016, 05:35:49 pmI have to agree wholeheartedly with this approach. The difficulty in practice is knowing when to stop over-engineering for something that isn't in the plan, and instead concentrate on what you have planned. If you discover late on in a product design you have picked the wrong processor*, it is likely that you now have a lot more problems on your hands than a software port, for example your project may not now even make financial sense anymore, or there might not be the right skills available. This sort of thing should have been determined at a very early stage and that is what a POC is for.So yes, make the effort to modularise and layer your code, and use #defines for your hardware, but also remember that often you are resource limited so excessive abstraction is also to be avoided. I too am a bit of a fan of function pointers with structs where appropriate, and having a naming convention for namespace implementation, but none of this should be overbearing, leading to unnecessarily complicated code obfuscating its purpose.As with all engineering everything is a compromise, made up from several requirements of varying priority. Trying to develop generic interfaces for unknown and unplanned hardware scenarios is hard and excrutiatingly time consuming, and in many cases impossible unless your crystal ball skills are up to scratch.* By "picking the wrong processor" I'm talking about discovering that you need to change chip family or vendor, not that you might simply need a few more pins or a bit more memory within the the same device family which is usually simple from a software point of view, but it might need a new board spin, so usually nothing more than a minor irritation.

author: westfw
author_rank: Super Contributor
timestamp: November 18, 2016, 01:43:01 am
content: QuoteYou can only go so far to utilize or protect resources that are fundamentally un-sharable.QuoteYeah. That. You really don't want to implement a timer hw allocation mechanism that loops through all the timers available on the chip looking for one that isn't assigned to some other function yet, and figuring out how to use that timer with your "modular code" (keeping in mind that a given chip usually contains several different types of timers.The interrupts are actually a pretty good mechanism for defending resources. Put your ISRs (usually with fixed names) in your C code, and if anyone else uses the same ISR, you'll get error messages at link time.I'd have ps2k.c, ps2k-private.h, ps2k-api.h, psk2-config.h - stuff that you expect to be easily changeable (pin numbers) goes in the last one, and the others shouldn't have to change."Not going overboard on portability" isn't the same as ignoring portability entirely, of course. It'd be better to base your code on a timer callback service rather than a hardware timer, and better to use an interrupt mechanism that allows other pins in the same port to still get interrupts that are directed to other code (on an AVR, there are some pins that generate individually vectored interrupts, and some that only generate interrupts has part of a "pin change" interrupt shared with all the other bits on that port. It would be reasonable to at least consider both.)People have mentioned testability. Testing all cases of a generalized library is "hard." 2nd best is to DOCUMENT those cases you've tested and found working, those you've tested and found NOT working, and those you haven't tested but expect to fall in one side or the other.Spend a lot of time thinking about the API to the higher-level code. Chances are that being able to move the ps/2 keyboard to use a different type of interrupts may be less important (portability-wise) than being able to replace the ps/2 keyboard with a USB keyboard, so ... do you really want to use key codes that are specific to PS/2, or do you want something more generic (any codes permitted) or more specific (unified codes for the common keys)?QuoteA library should touch as few "other peripherals" as possible. There's not excuse for a PS/2 keyboard library to do anything with a UART.You CAN use "abstracted" features controlled by the config file. Fiddling with UART registers would be awful, but using "debug_print(xxx)" is fine...

author: obiwanjacobi
author_rank: Super Contributor
timestamp: November 18, 2016, 07:26:59 am
content: So we have two ways for modular design in C:1) use #defines2) use structs & ptrs#defines will work for smaller projects but will be a mess when scaled up. I like to use the modern IDE features that suggest values/operations and defines is not the best for that.Using structs and ptrs is less optimal than using C++ templates. Usually C++ templates are optimized very well - produces smaller code that uses less RAM. For large ARM processors this might not be an issue, but for small resource-limited MCU that is something you need to look out for.For interrupts I provide (inline) methods that can be either called from an ISR, but the choice to use an ISR is made at app level, not at module level. If the app decides to make it an ISR it just calls that specific method or a combination of methods provided by modules and others provided by the app itself.Part II of my C++ answer...

author: ZeroResistance
author_rank: Frequent Contributor
timestamp: November 18, 2016, 08:17:27 am
content: Quote from: obiwanjacobi on November 18, 2016, 07:26:59 amInteresting!In the Ps/2 keyboard example or for that matter any other library could you explain a bit of how to use these #defines and structs and pointers?

author: obiwanjacobi
author_rank: Super Contributor
timestamp: November 18, 2016, 09:38:52 am
content: Use defines to communicate app-variable but module-static data such as which pins to use etc.Use structs to group module instance data and have a common parameter for all module-related methods.<module>.hCode:#ifndef MAX_BUFFER_SIZE#define MAX_BUFFER_SIZE 16#endif#ifndef MODULE_SENSE_PIN#MODULE_SENSE_PIN 0#endiftypedef fnCallback ...struct Module{uint16_t SomeModuleVar;char ModuleBuffer;fnCallback OnNotify;};// stupid example methodvoid setNotifyCallback(struct Module* this, fnCallback handler){this->OnNotify = handler;}void Initialize(struct Module* this){// use masks and shifts as neededMCUREG = MODULE_SENSE_PIN;this->SomeModuleVar = 1; // set some state}

author: Lukas
author_rank: Frequent Contributor
timestamp: November 18, 2016, 01:03:00 pm
content: I consider the firmware for my DIY watch to be quite modular:https://github.com/carrotIndustries/pluto-fw/It clearly helped that the firmware has to be compiled to run on linux as well, so register accesses mangled with application code is a big no-noSome random hints:- Avoid global variables, make as many variables/functions static as possible- Use enums. Although enums aren't type safe in C, they still help to communicate the design intent- Follow a strict naming convention. Since C doesn't give you anything like namespaces and classes, it's up to you to name things sensible.

author: obiwanjacobi
author_rank: Super Contributor
timestamp: November 18, 2016, 01:36:57 pm
content: Quote from: Lukas on November 18, 2016, 01:03:00 pmYou can fix that.Usage:Code:BeginEnum(ControlState){Normal, // no other states activeHidden, // control is not displayedDisabled, // control is displayed as read-onlyFocused, // control is high-lightedSelected, // control is active/selected (entered)}EndEnum(ControlState)....inline bool getIsDisabled() const{return _state == ControlState::Disabled;}

author: Lukas
author_rank: Frequent Contributor
timestamp: November 18, 2016, 01:49:46 pm
content: Quote from: obiwanjacobi on November 18, 2016, 01:36:57 pmThat's C++.But why bother with that when C++ has type safe enums built in?http://en.cppreference.com/w/cpp/language/enum#Scoped_enumerations

author: obiwanjacobi
author_rank: Super Contributor
timestamp: November 18, 2016, 01:54:34 pm
content: Is it? Sorry, I always get them confused.The enum class didn't work on the compiler I wrote this for/with...

author: John Coloccia
author_rank: Super Contributor
timestamp: November 18, 2016, 02:11:23 pm
content: It all kind of comes full circle to another thread where we're talking about C and C++. C++ is fantastic and extremely useful if you consider it as an OO extension to C, even for embedded work. It's only bloated and funky if you decide to drag in STL and start using all of strange language features they keep adding to it over the years (though some new features aren't too bad).

author: mikeselectricstuff
author_rank: Super Contributor
timestamp: November 18, 2016, 02:24:56 pm
content: How likely is it that things will ever need to be changed ?Does it make sense to spend time making it super-generic and modular if the likelihood of actually needing that is low?Consider the time spent abstracting and #defining everything that might coneivably change compared with just getting on with it and commenting the things that need changing in the event anything changes.e.g.#if productvariant == version1<the code for that version>#endif#if productvariant == version2<the code for that version>#endifetc.Some things make sense to abstract into a header file - e.g. simple I/Os,#define led_out LATBbits.LATB1#define led_out_tris TRISBbits.TRISB1But things get more complicated once you start needing to use peripherals and map the peripheral pins.And even more so once you try to make it generic to use one of <n> peripherals.You end up with so many abstracted things it becomes hard to debug and follow exactly what's going on.And even harder if code density really becomes important e.g. merging pin direction defines over multiple ports into word-wide values.It will definitely take you time now to abstract everything as much as possibleIt might save some time at some unknown time in the future if you need to change things.You need to consider what is the best use of your time.There is no "right" or "wrong" answer, only more or less appropriate for your particular set of circumstances.

author: langwadt
author_rank: Super Contributor
timestamp: November 18, 2016, 02:57:07 pm
content: https://xkcd.com/1205/

author: mikeselectricstuff
author_rank: Super Contributor
timestamp: November 18, 2016, 03:09:03 pm
content: Quote from: ZeroResistance on November 17, 2016, 02:00:59 pmComments in the section of your code that does peripheral initialisation, ideally ordered by resource to reduce the chances of duplication.e.g.// timer 1 - used for comms timeoutT1CON=.....// timer 2 - used to send heartbeat messagesT2CON=.....//SPI1 - used for interface to turbo-encabulatorSPI1CON=.....

author: SHristov
author_rank: Newbie
timestamp: November 18, 2016, 03:29:18 pm
content: The best approach (at least for me) is to have couple layers of abstraction and separation in components.Each component could have header,source and configuration header (for example with defines).You could separate at-least 3 layers:- complex rivers - that are interacting directly with the HW (ex. SPIdrv, I2Cdrv, WDG and so on).- applications - running on the highest level (they don't care about the HW) - just process the input and writing on the output)-Run-time environment - through that componet are exchanging informationhttps://www.google.com/search?q=embedded+software+layers&newwindow=1&client=firefox-b&biw=1200&bih=1503&source=lnms&tbm=isch&sa=X&ved=0ahUKEwinlsCvzbLQAhWKIsAKHWmTDxAQ_AUIBigB#newwindow=1&tbm=isch&q=embedded+software+layers+complex+driver&imgrc=ZLmRXZ-D_a6WyM%3A

author: nctnico
author_rank: Super Contributor
timestamp: November 18, 2016, 04:13:50 pm
content: Quote from: ZeroResistance on November 17, 2016, 02:00:59 pmIMHO that is sorted by the hardware. An external interrupt pin will have a meaning to a certain part of the software only so it is unlikely to be shared.

author: obiwanjacobi
author_rank: Super Contributor
timestamp: November 18, 2016, 04:25:11 pm
content: If you have reusable (high-level) code (known good and unit tested) that uses fixed interfaces to access device-specific resources, the overhead is minimal and it all gets compiled into very efficient code (C++ templates). You only have to implement those fixed interface methods for the resources you actually use when porting to a new device.Your libraries are split into generic code (using fixed interfaces) and device specific reusable code (implementing these fixed interfaces) - per device. You harvest the generic code from existing projects and you write the device-specific code as needed - and/or reuse what you already have.I guess you could also make this work in C, just a little messier and less optimal IMO.

author: ZeroResistance
author_rank: Frequent Contributor
timestamp: November 18, 2016, 06:12:18 pm
content: Quote from: Lukas on November 18, 2016, 01:03:00 pm1. When you say make as may variables / functions static as possible is it similar in a way to private variable in oop languages only in this case its private to that file in which the variable is declared?2. Are there any drawbacks of static variables?3. I like your idea of enums never used them as much I should have...

author: obiwanjacobi
author_rank: Super Contributor
timestamp: November 18, 2016, 07:50:49 pm
content: 1) Yes in a way. Static vars are only accessible by the module - or function if you declare it inside the function.Other modules cannot see that var.2) Using statics makes your code harder/impossible to unit test because it will be harder to control (stub).3) Use a naming convention to make sure the options are unique across modules.