Link here

A Turnkeyed C Application Program

Presents and describes a C-language multitasking example program that performs data acquisition and instrument control functions

This chapter presents an application program that integrates a range of hardware and software features of the QCard Controller Single Board Computer (SBC). The application is "turnkeyed", meaning that it can be placed in Flash and configured to autostart each time the board is powered up. This chapter explains all of the elements of the program and provides an example of C coding for the QCard Controller.

The example application reads a voltage using the 8 bit analog to digital (A/D) converter and outputs a pulse width modulated (PWM) signal that, when averaged using a simple resistor and capacitor, tracks the input voltage. In addition, the program calculates and displays the mean and standard deviation of the input signal. This example C application program demonstrates how to:

  • Use the pre-coded library routines
  • Take advantage of the "Make" utility which automatically sets up a memory map that is compatible with a turnkeyed application in Flash
  • Use the 8 bit A/D converter
  • Use floating point mathematics to calculate means and standard deviations
  • Write and install an interrupt routine that generates a PWM output
  • Assembly-code an interrupt routine for increased performance
  • Write to the liquid crystal display (LCD)
  • Write the application using a modular multitasking approach
  • Configure the application to automatically start upon power-up or reset

The program is called TURNKEY.C in the \MOSAIC\DEMOS_AND_DRIVERS\MISC\C EXAMPLES directory. The commented application code is presented at the end of this chapter. The text of this chapter will frequently refer to the code, offering background and explanation. Some of the section titles in this chapter are keyed to the titled sections in the code which are set off with asterisks.


Overview of the application

The main activities of this application are performed by 3 modular tasks and one interrupt routine:

  • The first "data gathering" task collects data from channel 0 (or any other channel that you designate) of the 8 bit A/D converter (PORTE on the 68HC11 microcontroller chip), converts the 8 bit reading to its equivalent voltage, and saves it in a floating point variable called input_voltage.
  • The second "output control" task calculates the duty cycle, high-time and low-time of a pulse width modulated (PWM) output signal so that its average value matches the latest input_voltage measured by the data gathering task.
  • An interrupt attached to output compare 3 (OC3) controls the PWM output signal appearing at PORTA (PA5), pin 11 of the Field Header. The interrupt service routine updates the OC3 timer registers based on the high-time and low-time calculated by the output control task.
  • Once each second for ten seconds, the third "statistics" task samples the input voltage calculated by Task 1 and stores it as a floating point number in a FORTH_ARRAY called Last10Voltages. When 10 seconds have elapsed, this task calculates the mean and standard deviation of the 10 data points and writes them to the LCD display. It then starts filling the array with new data, repeating the entire process.

There is one additional task that is always present in this application: the default task named FORTH_TASK that runs QED-Forth and executes main(). In the final version of this application we put the QED-Forth task ASLEEP. This prevents the end user of the system from gaining access to the Forth interpreter. We use the interactive QED-Forth task during development and debugging; it allows us to execute commands and monitor the performance of the routines that are being tested.


Hardware required for the example application

This sample application requires a QCard Controller, Keypad/Display Wildcard, a resistor, capacitor, potentiometer, and a voltmeter to verify proper operation. An optional operational amplifier would allow the output signal to drive loads other than a high impedance voltmeter or oscilloscope. Please be careful not to short pins together when working with the on-board connectors.

A 10 Kohm (or any other reasonably valued) potentiometer is connected to place a controllable voltage on PE0, which is channel 0 of the 8 bit A/D converter on the HC11. The potentiometer is connected between +5V and AGND (analog ground), with the potentiometer’s wiper connected to PE0 (pin 24 of the field header).

The PWM output appears on PA5 (that is, pin 5 of PORTA). A resistor and capacitor are connected to integrate the square-wave output signal to a steady average voltage across the capacitor. The capacitor voltage can be measured with a high impedance voltmeter or oscilloscope. An optional amplifier can be used if the output must drive lower impedance loads.

The statistical report generated by Task 3 is sent to the LCD display.


The memory map

The first step in programming an application is assigning the memory areas that will be occupied by the object code, variable area, and also the heap which holds FORTH_ARRAYs in paged memory. Fortunately, the Mosaic IDE Make Tool automatically sets up a very versatile memory map. For reference, here is a brief summary of the main memory areas allocated by the Make Tool:

  • 0x0000-0x7FFF (32K) in page 4 is the program’s object code which will eventually be in Flash.
  • 0x4600-0x7FFF (14.5K) in page 0x0F is the RAM heap area that holds FORTH_ARRAY data in paged memory.
  • 0x3000-0x45FF (5.5K) in page 0F is a reserved heap for the graphics display buffer.
  • 0x8E00-0xADFF (8K) is available common RAM (the C .data and .init sections) which hold C variables, C arrays, TASK areas, and pfa’s (parameter field areas) of FORTH_ARRAYs.
  • 0xB000-0xB3FF (1K) is 68HC11 on-chip RAM (the C .onchipRAM section); the top 48 bytes at 0xB3D0-0xB3FF are reserved.
  • 0xAEC0-0xAFFF (320 bytes) is available EEPROM (the C .eeprom section); EEPROM at 0xAE00-0xAEBF is reserved for startup utilities and interrupt revectoring.
  • 0x4000-0x7FFF (16K) on page 0x05 is used for QED-Forth debugging definitions that are present in the .DLF download file
  • QED-Forth ROM occupies 0x0000-0x7FFF on page 0, 0x0000-0x2FFF on page 0x0F, and 0xB400-0xFFFF in common ROM.

While the object code ends up in Flash memory, the variable area and heap must always be located in RAM. The variable area includes the areas where variable values are stored, where TASK areas (the tasks’ stacks, buffers, pointers, etc.) are allocated, and includes the parameter fields that hold addressing and dimensioning information associated with heap items. The heap in paged memory holds FORTH_ARRAYS and other data structures. Both variables and the contents of the heap must be subject to rapid change while the application is running, so they can never be placed in Flash.


Other memory areas

The upper half of page 5 is used to hold the QED-Forth definitions (names and object code) that facilitate interactive debugging. This region is typically not needed after debugging is done, and so is typically not included in the final Flash. If interactive function calling capability is required in the final application (for example, if you want to give the end user or a customer service person the ability to execute interactive commands from a terminal), then this area can be included in nonvolatile Flash memory in the final turnkeyed system.

While downloading an application, the object code area must be RAM so that the specified bytes can be stored in memory. As described earlier[pkc3], proper use of SAVE and RESTORE utilities can save you from having to re-download all of your code after a crash or mistaken command.


8 bit A/D data gathering task

The first section of the example application code uses the 8 bit analog to digital (A/D) converter to convert the voltage input derived from the external potentiometer to a digital value between 0 and 255. The task converts the 8 bit A/D reading into its equivalent floating point voltage which spans 0.0 to 5.0 Volts, and stores it in a variable called input_voltage.

We define some simple constants that specify the high and low reference voltages of the A/D, and the number of counts (2^8 = 256 counts). The CountToVolts() function converts the 8 bit A/D reading into its equivalent floating point voltage (a number between 0.0 and 5.0 Volts). GatherData() is the activation routine for this task. It calls AD8On() to power up the 8 bit A/D, and enters an infinite loop that acquires an A/D sample, converts it to a voltage, and stores it in the variable input_voltage which other tasks can read.

A special storage operator named PokeFloatUninterrupted() is declared using the _protect keyword, which causes the compiler to disable interrupts before calling the function, and restore the global interrupt enable flag (the I bit in the Condition Code Register) to its prior condition after the function returns. This uninterruptable function is used because the input_voltage variable holds data that is accessed by more than one task. The uninterruptable store and fetch operators ensure that 32 bit data is not misread because of intervening interrupts or task switches.

The Pause() cooperative task-switch command forces at least one task switch on each pass through the infinite loop of GatherData(); we also rely on the timeslicer to switch tasks every 5 ms.


Pulse Width Modulation (PWM) task

The goal is to create a PWM output whose average voltage is equal to the input voltage read by the data gathering task. This code specifies the activity of the task that calculates the high-time and low-time parameters needed to generate the pulse width modulated output signal. An interrupt routine (described in the next section) controls the output signal subject to the high- and low-times calculated by this task.

We could perform all of the duty cycle computations in the interrupt service routine that controls the PWM output, but this is not a good practice. Long interrupt service routines can delay the processor’s ability to respond to other interrupts in a timely manner. The best approach is to perform the more time-consuming computational functions in foreground tasks so that the associated background interrupt service routines execute very rapidly.

We define static integer variables to hold the high-time and low-time which, as their names imply, hold the number of timer counts that the PWM signal is high and low. Floating point constants LOW_OUTPUT_VOLTAGE and HIGH_OUTPUT_VOLTAGE are defined to specify the voltage levels corresponding to logic level 0 and logic level 1; for maximum accuracy you could measure the voltage levels on PA5 (pin 5 of PORTA on the 68HC11 microcontroller chip) and set these constants accordingly.

The period of the PWM output is chosen to be 130 msec, which corresponds to 65,000 counts on the free-running timer. The resident QED operating system automatically configures the free-running counter to increment every 2 microseconds.

We enforce a minimum time between interrupts via the constant MINIMUM_HIGH_OR_LOW_TIME which is approximately equal to the maximum number of timer counts divided by 256. That is, we only require 8 bits of resolution from our PWM. The benefit of imposing a minimum high or low time is that by doing so we can ensure that the minimum time between PWM interrupts is more than adequate to allow the service routine to execute.

We next define a routine that calculates the PWM duty cycle such that the average output signal matches the latest measured input_voltage. The HighAndLowTimes() function converts the calculated duty cycle into the parameters needed by the interrupt service routine, and SetPulseParameters() is the infinite loop that serves as the activation word for the PWM task. Note once again that we have put Pause() in the loop so that both cooperative and timesliced multitasking are used.


Output compare interrupt code

This code defines an interrupt service routine (ISR) and an installation routine for the OC3 (Output Compare 3) interrupt which controls the output signal. We first define constants to name the relevant bit masks. The relevant 68HC11 hardware register names are defined in the QEDREGS.H file in the \MOSAIC\FABIUS\INCLUDE\MOSAIC directory.

OC3Service() is the interrupt service routine that controls the state of the PA5 output bit. It relies on the ability of the output compare (OC) function to automatically change the state of an associated PORTA output bit at a specified time. Specifically, OC3 can be configured to automatically change the state of the output PA5 when the count in the TOC3 register matches the contents of the free-running counter (TCNT) register. Setting the "mode bit" specified by the constant OC3_MODE_MASK in the timer control 1 (TCTL1) register enables the automatic output control function. The OC3 "level bit" specifies whether the output bit will be set high or low upon a successful compare. The OC3Service() routine simply reverses the state of the OC3 level bit (specified by the constant OC3_LEVEL_MASK) and adds the appropriate high_time or low_time increment to the OC3 register to specify when the next interrupt will occur. Recall that high_time and low_time are calculated by the foreground PWM task, so the interrupt has very little to do and can execute rapidly (typically in under 50 µsec).

InstallOC3() enables direct hardware control of PA5 by setting the OC3 mode bit, calls ATTACH() to post OC3Service() as the OC3 interrupt handler routine, initializes PA5 by forcing an output compare, and enables the OC3 interrupt. The main() routine defined at the end of the code example calls the InstallOC3() initialization routine each time the processor restarts.


Assembly coding a function definition from C

AssembledOC3Service() is an assembly coded version of the interrupt service routine. While it is only slightly faster than the high level version, it is included to illustrate how assembly coding is performed within this programming environment.

Assembly mnemonics use the standard Motorola format as described in the Motorola 68HC11 book. After the function is declared and the opening { is entered, the #asm preprocessor directive indicates that the following code is to be passed straight through to the assembler, bypassing the C compiler. The #endasm preprocessor directive returns control to the compiler. These preprocessor directives must appear alone on a line. Each opcode instruction must occupy a separate line that starts with at least one tab or space. Labels (such as branch destinations) should appear alone on a line, with no leading spaces. The * is the comment character; text after the first * on a line is ignored.

Assembly constants are defined using EQU (equate) statements. Note that just above the function definition of AssembledOC3Service(), the required constants are defined as EQU statements within the delimiting #asm ... #endasm preprocessor directives.


Statistics task

The code in this section continuously loads a matrix with one input voltage acquired in each of the last 10 seconds, and writes the mean and standard deviation of this data to the liquid crystal display every 10 seconds. We define a single-row FORTH_ARRAY called Last10Voltages to hold the data; this array resides in the heap associated with the statistics task. Two variables keep track of the current and prior matrix indices; these aid in managing storage of data into Last10Voltages.

The SetupDisplay() function writes the headings to the LCD display. The STRING_TO_DISPLAY macro (defined in the INTRFACE.H file) makes it easy to write a text string to a portion of the display buffer in system RAM, and the UpdateDisplay() command writes the contents of the buffer to the LCD display. CalcStats() calculates the latest calculated mean and standard deviation values, and ShowStats() writes them to the display. ShowStats() uses sprintf() to convert the floating point numbers to ASCII strings that can be written to the display buffer. The format is specified so that each floating point number will occupy the same number of spaces each time it is displayed. LogDataAndShowStats() fills the Last10Voltages array with measured voltages and calls the subsidiary functions to display the statistical results every 10 seconds.

Statistics() is the activation routine for the task; it dimensions and initializes the data array, sets up the display, and enters an infinite loop that logs the data and displays the statistics. Pause() is included in the infinite loop so that both cooperative and timesliced task switching are used.


Build and activate the tasks

Now that we have defined the activation routines for the tasks, it is time to execute the TASK statement to name the tasks, allocate their 1K task areas in common RAM and set up the tasks.

The routine BuildTasks() initializes the user area and stack frame of each task, and links the tasks into a round-robin loop. The first statement of this routine is very important:


When executed, this command makes the currently operating task (which will always be the default FORTH_TASK) the only task in the round-robin loop. This sets a known startup condition from which the task loop may be constructed.

Note that the ReadInputTask and ControlOutputTask are built with a null heap specification because they do not access any heap items. The StatisticsTask, however, does access a heap item. It requires a heap specified by DEFAULT_HEAPSTART and DEFAULT_HEAPEND which are defined in the HEAP.H header file in the \MOSAIC\FABIUS\INCLUDE\MOSAIC directory. Note that we also use the same heap specification for the default FORTH_TASK; see the INIT_DEFAULT_HEAP() routine. The FORTH_TASK is not active in the final application, so the sharing of this heap space does not cause a conflict during operation.

ActivateTasks() simply activates each of the three tasks with the appropriate action routine. Each action routine is an infinite loop that performs the desired activity of the task.


Define the main routine

The main() function is the top level routine in the application. After execution of the PRIORITY.AUTOSTART command as explained below, it will be called each time the board is powered up or reset. The operating system always wakes up and enters the default FORTH_TASK whose task area starts at 0x8400 in common memory upon every restart, so the default QED-Forth task is always the task that calls main().

It is good programming practice to initialize all variables each time the application starts up; this is done by the InitVariables() function. After initializing the variables, main() calls INIT_DEFAULT_HEAP() to initialize the heap of the FORTH_TASK, calls InitElapsedTime() to initialize the elapsed time clock to zero, installs and initializes the OC3 (PWM) interrupt service routine, and builds and activates the tasks.

The next command in main() is


which forces a COLD restart every time the machine is reset. This enhances the operating security of a turnkeyed application by ensuring that every user variable and many hardware registers are initialized after every restart and reset. This command may be commented out during debugging so that restarts do not cause QED-Forth to FORGET all of the defined functions in the application program.

The main() function then puts the default FORTH_TASK asleep by executing:


This takes effect once multitasking commences, and does not prevent execution of the remainder of main(). This command should be commented out during program development so that the awake QED-Forth task can be used to aid in debugging.

The main() function then RELEASEs the FORTH_TASK’s control of the serial line. This is not required in this simple application, but it would be necessary if another task required access to the serial port in the final application. For example, the RELEASE() statement would be required if the statistics task printed to the terminal instead of to the LCD display.

The final commands start the timeslicer (which also starts the elapsed time clock and globally enables interrupts) and Pause() to immediately transfer control to the next task in the round-robin loop. The final Pause() is not essential in this simple application, but it does ensure smooth operation in applications where tasks other than QED-Forth require access to the serial port.


Compile the program

To compile, assemble and link the program and create the download file, simply use your editor to open the TURNKEY.C file in the \MOSAIC\DEMOS_AND_DRIVERS\MISC\C EXAMPLES directory, and then click on the Make Tool. When the compilation is complete, you can view the warning messages and highlight the associated source code lines. (None of the warnings adversely affect the operation of this program.)

Make sure that your QCard Controller is turned on and is communicating with the Mosaic Terminal. Then download the program to the QCard Controller by using the terminal’s "Send File" menu item to send the TURNKEY.DLF file.


Using SAVE, RESTORE and write-protection during debugging

After downloading the program, you can interactively type SAVE from the terminal; this stores relevant memory map pointers into reserved locations in EEPROM. You can proceed to interactively test each function in the program one at a time as described earlier[pkc7]. If a crash occurs, simply type RESTORE to bring back access to all of the interactively callable function names. The use of SAVE and RESTORE can greatly reduce the number of times that you have to re-download your code during debugging.


Going into production: Autostart the program

After debugging is completed interactively type the QED-Forth command:


which installs main() as a routine that is automatically executed upon each restart or reset. Note that the QED-Forth V4.4x greeting is suppressed when an autostart routine is installed (you could easily print your own greeting by modifying the main() function). PRIORITY.AUTOSTART installs an autostart pattern in the top 6 bytes of page 4 [pkc8]which is in Flash in the final system. The autostart pattern tells the operating system to automatically call main().

The PRIORITY.AUTOSTART command is used to configure systems that will go into production. For one-of-a-kind prototypes, another QED-Forth command (called simply AUTOSTART) is available that installs the autostart pattern in EEPROM which resides in the 68HC11 chip itself. Because the pattern installed by AUTOSTART is in the processor chip and not in Flash, it is not automatically transferred to a new board when the production Flash is plugged in. In summary, the AUTOSTART command is convenient while debugging a prototype, but the PRIORITY.AUTOSTART command must be used when generating Flash for a production system[pkc9].

After executing the PRIORITY.AUTOSTART command, the main() routine can be invoked by resetting the QCard Controller, thus starting the program. If you need to remove the autostart vector (and the QED-Forth monitor is still active and responding to your commands, that is, if you didn’t put it ASLEEP), you can simply type:


to remove any PRIORITY.AUTOSTART or AUTOSTART vectors. If QED-Forth is not awake (and so does not respond to the terminal), you can remove the autostart vector by entering the special cleanup mode. Note that the special cleanup mode configures the system to expect a baud rate of 19200 baud, so if you are using a different baud rate, you’ll need to interactively execute the command BAUD1.AT.STARTUP to re-establish the desired configuration.

You can monitor the operation of the turnkeyed program by connecting a voltmeter or oscilloscope across the output capacitor C1, and by watching the update of statistics every 10 seconds on your display. Adjusting the input potentiometer should result in an output voltage that tracks the input.


Turnkeyed application code listing

Listing 12-1 Turnkeyed Application

This page is about: Writing Multitasking Instrument Control Program, Using Tasks for Analog to Digital (ATD) Conversion, Pulse Width Modulation (PWM), Signal Processing & Reporting – Presents a C-language multitasking example program for data acquisition and control of electronic instruments, using ATD, PWM, array data storage, and statistical calculations. Discusses production code for embedded systems. autostart, save, restore