Editor's note: In Part1 the authordescribed some of the conventional solutions to multi-threadingproblems in embedded systems including: (1) A/D conversion, (2)trigger-and-react-in-ISR, (3) round-robin-tasking, and (4) mailbox orevent driven.
Next I'll outline the publisher-subscriber-approach, a data-centricmodel that overcomes the various limitations discussed earlier, and inwhich:
1. An initial AD-conversionis triggered.
2. The ADC ISR (interruptlevel below high data priority) picks up the value from theAD-converter and calls SetDataObject onobIdAdValue (reference to a data object) when conversion is complete.
3 . The calculation modulehas registered a high priority data-notification-callback. Thiscallback is called and a safe data pointer gets passed into it. Thisdata contains the new AD-value used in the calculation for method A orB. Again SetDataObject is called, this time on obIdOutput .Another conversion is triggered.
4 . The output-module hasregistered a high priority data-notification callback for obId-Outpu twhich gets called now. It sends the value to the output hardware.
5. The display-moduledepends on both the ADC-module and the Output-module as it hasregistered notification callbacks for both obIdAdValue and obIdOutput onthe low priority notification level. These get called by the DCOSframework. They receive processor time when no ISR at a higher priorityis running (From Start-Of-Conversion to End-Of-Conversion in thisexample).
Again, this approach performs in a similar way as the last two (asmany conversions as possible, predictable reaction). The overhead isgreater than with the trigger-and-react-in-ISR approach but less thanwith the RTOS-mailbox approach.
This time the reusability is maximized, too. The display andcalculation modules stay completely independent of each other as thereis no need to actively pass the data down to the display code module.This is because of the 1-to-many relation between the publisher and thesubscribers. As a result the dependencies between modules truly rejectthe necessary use-relations. All modules can easily be reused orsubstituted by another implementation.
Dependencies, use-relations anddata-flow configuration
As to the relations it's easy to realize any type of relations. “1 to1” is standard. “1 to many” can easily be realized by letting eachconsumer module register its callback for the ADC value.
A “many to 1 relation” sometimes comes in handy, too. Imagine theADC only sets the data objects if the value has significantly changed.Really, there is no point in recalculating the same output using thesame input. Now the calculations are not being done again until thevalue significantly changes. But you should also recalculate the outputwhen you change the calculation method or change a factor thatinfluences the output, too.
This is easy now. Create an obIdConfig andan obId-Factor somewhere and register the calculation callback with these (at the samepriority), too. Then any change of AD-value OR factor OR config willtrigger a calculation and cause a new output to be realized.
You can now configure the data-flow into the system rather thanimplement it in more static code. The calculation subsystem in thisexample takes 3 inputs and produces one output. So there will becallbacks registered for those 3 inputs that each call the samefunction producing the single output.
If the one function ” calc() in Listing 5 to the left ” gets calledfrom 3 other functions we have to think about re-entrancy andsynchronization for the access to the global variables config, factor and adVal .
Well, we don't! The 3 callbacks will run on the same interrupt-levelthat DataPrioHigh is assigned to, consequently only one of these can execute at any time.
Normally I would have implemented this in code like the following:
if ((AdValue != previousAdValue)
|| (config != lastConfig)
|| (factor != lastFactor))
As the if-condition gets more complex it may well consume moreprocessor time by checking it every cycle than it frees up by avoidingneedles calculations. For this example and other more complex cases thedata-centric approach will very often show its advantages over moretraditional approaches.
There are two other important changes. By letting the ADC set thedata-object only when the value has changed we switched from a cyclicmodel to an event-driven model and thereby saved a lot of performance.This is additional time available for lower priority tasks and on thewhole a lot more background work can be done by the processor.
The second change concerns the data-flow. The data centric approachallows you to link data directly, relying on the events to propagateany changes through the system. It has now become easier to design asystem as being made up of small reusable building blocks. It's nowvery easy to attach some a monitoring or bus-communica-tion module. Yousimply let it be notified by the data objects it needs and link thatmodule into the project. The rest of the project remains unaffected ofthat change.
Again, I'll relate the DCOS to SW-Design by using a final example. Theproject to be realized is defined such that an AD-converter shallprovide AD-conversion values in a cyclic manner. Depending on thecurrent user-configuration, this value shall be used in one of threeways:
1. To determine the dutyfraction of a pulse-width-modulated signal. The user can set the PWMcycle time.
2. Multiplied with a userdefinable factor. The result is applied to an analog output. 3. Usedto set the volume of speech replay. The user may also control speechreplay using commands for start, stop, play, next and prev. The usershall be kept informed about the system status. The user interface isrealized using serial communication.
When designing software I usually get pencil and paper first, drawrectangles of modules, think about groups of functionality and possiblemodule interfaces (the header files). The reusability check comes next.I often made the experience that an easily reusable module is usually agood module.
The reusability check consists of thinking up possible scenarios ofwhere and how each of the modules could be reused for another ” moregeneral ” project. It soon highlights which features of a modulesinterface need more generalization; and, of course, which modules areso project specific that reuse doesn't make sense.
There is a modular programming rule that I break: “No globalvariables in the interface (replace them by access functions)”. I doput many data-object-IDs in header files because they are part of theinterface. They are not used as normal global variables, as they getinitialized once during Initialization and then stay fixed. Otherglobal variables are still banned in my code, though.
Figure 2 above shows agraphical representation of the modular layout of the finaldemo application. It consists of the follow elements:
1) A user interface:command input and system status output via RS232. Picking up inputcharacters runs at a higher priority to ensure that no bytes get lost.Command interpretation is carried out on a medium interrupt priority.For a list of available commands just connect a terminal emulationprogram at 115200bps 8N1 and is turned off in this mode.) various ISRs.type”help”.
2) ADC-module:AD-conversions are being triggered by a timer callback in a cyclicmanner. The only output is a dataobject obIdAdcVal .
3) A PWM module (softPWM ).This module realizes a Pulse-Width-Modulated output using all LEDs onthe evaluation-board. The cycle period can be set usingobIdPWMdutyFraction (in milliseconds via command spc). If the modeAD-to-PWM is active (command “adpwm “) Theduty-fraction is determined by the converted AD-values. (Otherdiagnostic functionality of the LEDs is turned off in this mode.)
3) A DAC module is realizedusing the LED outputs, too. You should imagine an R2R ladder beingconnected to the LEDs for this mode. The output of that R2R-ladder willbe an analog output. (Other diagnostic functionality of the LEDs isturned off in this mode)
4) A speech module receivesthe commands play, next, prev, pause and stop. The volume and theprogress are continuously put out to the display.
5) Three Calculation &Scaling modules for pwm duty cycle, DAC scaling and volume convert theAD-value to the correct range applicable to the respective outputmodule. The DACscaling also has a factor that can be manipulated bycommands. This module contains the function OnAdToDACValOrFactor-Changed() demonstrating the many-to-one data-relation mentioned earlier.
6) The central CmdExec moduleswitches the data-flow at run-time in response to user commands. The ADvalue either goes to the PWM, to the DAC or to the volume of thereplayed speech. When the AD-value goes to the volume the otherwiseunused LEDs are used for displaying activities of various ISRs.
Due to size limitations of Keil's Evaluation Version I was not ableto fit in an AD-Value-Oupu tnor a PWM-Duty-Cycle-Output to the display.(In debugging sessions the codesize limit is used up to the last fewbytes.) You may try it as an exercise to scrap a module and implementthese notifications instead.
All general modules have been tested and stressed separately insmaller test-projects and a versioning system has been used to keep thesources in sync. These modules are ADC, softPWM, DAC, Speech and, ofcourse, all modules in the library (LED-access, serial communication,data object handling including notification and timers).
Apart from testing, these smaller projects also make error tracingmuch easier. Once you located where the problem lies you can provokeerrors in the test-project without having the rest of a possiblycomplex system interfering with your debug-session. The versioningensures that when you fixed a bug in one module you have automaticallyfixed it in all projects that use that module.
Furthermore, this strictly modular approach enables you to veryquickly create other products. Think about turning this example projectinto a handheld music-player. What you need is a real display. So youwould scrap the UI-part and replace it by another one. Then you maywant to use the potentiometer not only to control the volume but alsoto navigate between sound-files.
Using one of the external interrupts and a 360° potentiometeryou can even implement a click & scroll user input device tonavigate menus etc. You can look at the modules in layers, too. Modulesthat use others depend on them. From the layered perspective they are alayer above as shown in Figure 3 below .
It's important to draw a clear line between Use-Relations andData-Flow-Direction. The module “Calc PWM ” in theexample module diagram above uses both ADC and PWM. Yet it's importantto note that both ADC and PWM are on the same level, i.e. they areequally independent of the rest of the application.
“CalcPWM “is using both of them, taking the output of ADC and setting the inputof the PWM. Every now and then I check my use-relations to see if thereality still matches my model of the application. You can do thiseasily by checking which includes are contained in the *.c source file.
I often delete unused #include 's,clean up the code again or revise the model on this occasion. This alsois a good measure against the temptation to clutter your sources withglobal variables and include everymodule from every other one.
This cmdExec-module in this project sets and clears data connections atrun-time. As shown in Listing 6below, left , it calls volScalingActivate() by which the volScaling-module sets or clears its subscriptions (registers/un-registers itsdata-notification callback) to obIdAdValue .
The effect is that the flexibility is established once. If a variablewas used to decide which way the data flows it would have to be checkedevery cycle which would put a burden on the performance again.
Being able to add flexibility without impact on performance is veryhandy when your customers or your boss are hard to satisfy. In myexperience they tend to demand some addition and extra gimmick, somespecial operating mode, every 25th cycle done something else, etc.(ignoring any specification, of course). By the time the customer (oryour boss) is prepared to consider your software just acceptable yourcode might well employ quite a few “config “variablesand probably gota bit cluttered and difficult to maintain.
At that stage the processor also spends a considerable amount oftime with checking these added operating modes and submodes again andagain. Luckily, being able to simply reconfigure the data-flow we cannow get along without added if's and switches.
I have presented the idea of a data centric operating system.Starting with the techniques it's built upon – SW-interrupt, Callbacks,Using the Interrupt Controller to control Task-Priorities andpre-emption – I compared its performance to that of commercial RTOSes.Idiscussed different approaches and solutions to common problems inmicro-controller applications and sketched out how the proposedapproach fits in with modern SW-design, layered models,code-reusability and -maintenance.
Having created those highly independent code-modules again I hopethat assembling them together to do projects becomes a bit more likeplaying with building blocks.
To read Part 1 in thisseries, go to Themultithreadsynchronization problem: conventionalsolutions.
Dirk Braun (email@example.com)studied mechanical engineering at King's College London. He has beenworking as a self-employed programmer for industrial applications,databases, PCs and microcontrollers and held various courses onprogramming in C, C++ and Java. Since 2003 he has been as a seniorsoftware developer at BSTInternational which produces web guiding and web control systemsfor the paper, labelling, packaging, printing and tire industries.