Non-Modular Code Problem
Simple projects don't typically share resources between different libraries but this problem is very common in more complex applications that use multiple libraries in the same system. If those libraries are not modular, they usually directly access resources that are shared with other libraries. When this happens, they may interfere with each other and cause the system to stop working. This problem may be intermittent and hard to find. If you’re lucky, the libraries may define identical symbols and not even build if you try to use them together in the same project. At least you’ll know right away you have a problem.
One way to resolve this problem is to add code to every library to remap or exclude shared resources or to communicate with each other and hand-off the resource. However, this is a losing proposition as it results in complex interdependencies between libraries that get exponentially harder to use and maintain as the number of libraries increases. A far better solution is to make libraries modular and let each library manage its own resources and not directly access the resources of other modules.
Modular Code Benefits
Modularity is vitally important to creating code that can easily be used and re-used. It allows library modules to be used as building blocks so you can divide and conquer a design problem. This allows you to easily add or remove functionality and build complex systems very quickly and easily.
Modular Code Defined
A modular library will have an interface that consists of functions and not globally accessible variables or registers. A function has input and output and may have some side effects. In most cases, the side effect is why we actually call the function, especially for modules that manage hardware (set a pin on a GPIO port for example).
The functions in a library module’s interface must be treated as black boxes. Any code that calls the function must not access or make any assumptions about what is inside the box. A module’s interface will consist of one or more closely related functions that work with a set of resources owned by that module. Having a set of functions for the interface to a module is necessary because a function can protect shared resources and prevent conflicts that might occur if those resources were accessed directly.
A good, modular library has a well-abstracted and clearly documented interface. It is made up of the smallest set of functions needed for a peripheral to perform its job. The functions should all relate to the same job (the job of that module) and the same resources. Each module must manage and protect the resources it owns and it must not directly access any resources it does not own. This is the key concept for modular libraries.
For example, a USB module should not manage a timer or provide functions for timer features. Instead, it should call the timer module’s interface functions to use the timer resource. The USB module should use as few of the timer module’s interface functions as it needs to effectively do what it has to do. It shouldn’t need to manage details like the clock source or rate used by the timer, all it needs is a callback in 50 ms. This is what is meant by being loosely coupled. The details of how the timer operates and what it needs are best managed by the timer module, not the USB module. A set of well-designed modules gives you the ability to more easily divide and conquer the problem you’re trying to solve instead of managing the details that each module should manage for you.
If any module needs to use the resources of another module, it must do so only by calling that module’s interface functions. So, for example, if our TCP/IP, USB, and Graphics modules all want to use a timer to let them know when a specific amount of time has passed, they would all call a common “system” timer module function to make that request.
Using Callback Functions to Prevent Conflicts
- Callback Function
- A callback function is a function found in a lower layer (helper) module, that lets a higher level module know when some event has occurred (event examples: the period of time has elapsed, the string has been transmitted, etc). In other words, a higher level module requests a lower level module to call it back when an event occurs.
How does a module prevent conflicts? In this example, the system timer function allows client modules to register a callback within a given period of time. The timer module must be intelligent enough to keep these requests from conflicting with each other.
This one keeps a queue of requests and checks to see if the new request will fit in the queue. If it does not, it returns an invalid handle to indicate that it is busy and cannot accept the new request. If it can accept the request, it puts the new request in the queue, along with some information it needs to manage the request and returns a valid handle to the item in the queue. Elsewhere in the module (not shown), it responds to timer interrupts at a rate that it manages and calls back clients in the queue when their time is up.
If the calling client receives an invalid handle, it must do something appropriate (retry the request at a later time or set an error flag).
Although this example shows a queue, it is not necessarily needed. If the timer service did not queue up requests, it would simply return that it was busy (return an invalid handle) any time a current request was being processed.