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_ARRAY
s 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_ARRAY
s.
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:
NEXT_TASK = TASKBASE;
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
ColdOnReset()
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:
STATUS = ASLEEP;
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 RELEASE
s 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:
CFA.FOR main PRIORITY.AUTOSTART↓
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:
NO.AUTOSTART
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
// Turnkeyed Application Code Listing, C Language version.
// This is a complete real-time application program, with instructions on
// how to set up the program to AUTOSTART each time the processor starts up.
// Copyright 2004 Mosaic Industries, Inc. All Rights Reserved.
// Disclaimer: THIS SOFTWARE IS PROVIDED ON AN "AS IS/" BASIS, WITHOUT ANY
// WARRANTIES OR REPRESENTATIONS EXPRESS OR IMPLIED, INCLUDING, BUT NOT
// LIMITED TO, ANY IMPLIED WARRANTIES OF MERCHANTABILITY OR FITNESS
// FOR A PARTICULAR PURPOSE.
// ************************ Turnkeying a QED Application ********************
//
// This is the code for an example application. The accompanying chapter
// presents a detailed explanation of this code. The code can run on a QCard
// with the minimum memory configuration. To allow display of the statistical
// results calculated by the program, a Keypad/Display Wildcard should be
// attached to the QCard.
// Description of the application:
// This program reads an input voltage, creates a pulse width modulated (PWM)
// signal whose average value tracks the input voltage, and reports the
// mean and standard deviation of the input voltage. The following 3 tasks do
// the work:
// Task 1: Reads 8 bit A/D input AN0, measuring the voltage on a potentiometer
// connected to PortE pin PE0/AN0.
// Task 2: Outputs on Port A bit 5 a PWM signal whose average equals the A/D
// input.
// Task 3: Every 1 second puts A/D value into a matrix, and every 10 seconds
// displays the mean and standard deviation of the last 10 seconds
// worth of data.
//
// The following issues are addressed:
//
// Using the default C memory map as set up by the MAKE utility
// Proper initialization of tasks at startup
// Autostarting
// Interrupt service routines and interrupt attaching
// In-line assembly coding
// Initialization of the heap memory manager
// Timesliced multitasking
// ColdOnReset() for secure restart
// Floating point calculations
// Using the display
// Going into production
// ************************ Default Memory Map ************************
// 0-7FFF (32K) in page 4 is user application code (eventually will be ROMmed)
// 4600-7FFF (14.5K) in page 0F is RAM Heap area that holds banked array data.
// 3000-45FF (5.5K) in page 0F is a heap for the graphics display buffer.
// 8E00-ADFF is available common RAM (the C .data and .init sections)
// which hold C variables, C arrays, and pfa’s (parameter field areas)
// of FORTH_ARRAYs.
// B000-B3FF is 1K 68HC11 on-chip RAM; the top 48 bytes
// at B3D0-B3FF are reserved.
// AEC0-AFFF is available EEPROM referenced by the .eeprom section
// {EEPROM at AE00-AEBF is reserved for startup utilities
// and interrupt revectoring}
// QED-Forth ROM occupies 0-7FFF on page 0, 0-2FFF on page F,
// and B400-FFFF in common ROM.
#include <\mosaic\allqed.h> // include all of the qed and C utilities
// *********************** 8 BIT A/D Data Gathering Task *********************
// This task gathers data from the 8 bit A/D and places it in a variable
// easily accessed by other tasks.
// Define the control registers and some useful constants:
#define AD8_INPUT_CHANNEL ((char) 0)
// You can use any channel you want and place the input potentiometer on
// the corresponding A/D8 input pin. For example, if 0 is your input
// channel, place the input signal on the pin labeled AN0 (pin 10 of
// the analog connector).
static float input_voltage;
// Holds voltage corresponding to latest result of A/D input. Because the
// contents of this variable are shared among several tasks, we use
// uninterruptable PokeFloatUninterrupted and PeekFloatUninterrupted
// to access it.
// Now we convert a measured count from the A/D that ranges from 0 to 255
// into a voltage that ranges from 0.0 Volts (the low reference voltage)
// to nearly 5.0 Volts (the high reference voltage).
// This involves solving the equation:
// Voltage=[(high.ref.voltage -
low.ref.voltage)*measured.count/256]+low.ref.voltage
// First let’s define some constants:
#define FULL_SCALE_COUNTS 256 // 256 counts in an 8 bit converter
#define LOW_VREF 0.0
#define HIGH_VREF 5.0
// NOTE: For maximum accuracy, measure +5VAN with a voltmeter and
// set HIGH.REF.VOLTAGE equal to the result.
_Q float CountToVolts(int count)
// This routine converts 8 bit A/D measured count n { 0 <= n <= 255 }
// to the corresponding floating point voltage r
// {typically, 0.0 <= r <= 5.0 } by solving the equation:
// r = [ (high.ref - low.ref) * count / 256 ] + low.ref
{ return( ((HIGH_VREF - LOW_VREF) * count / FULL_SCALE_COUNTS) + LOW_VREF );
}
_Q _protect void PokeFloatUninterrupted(float value, float* destination )
// stores value into the address specified by destination;
// the _protect keyword guarantees that
// interrupts will be disabled while this function executes;
// this prevents corruption of data when two tasks are accessing the same
// 4-byte variables
{ *destination = value;
}
_Q _protect float PeekFloatUninterrupted(float * source )
// fetches and returns the contents from the address specified by source;
// the _protect keyword guarantees that
// interrupts will be disabled while this function executes;
// this prevents corruption of data when two tasks are accessing the same
// 4-byte variables
{ return *source;
}
_Q void GatherData(void)
// This is the activation routine for the data gathering task.
// It continually acquires readings from the A/D, converts the readings
// to voltages, and updates the input_voltage variable that is accessed
// by other tasks.
{ uchar sample;
float sample_voltage;
AD8On(); // make sure A/D is powered up
while(1) // start infinite loop
{ sample = AD8Sample(AD8_INPUT_CHANNEL);
sample_voltage = CountToVolts(sample);
PokeFloatUninterrupted(sample_voltage, &input_voltage);
Pause();
}
}
// ********************* Pulse Width Modulation (PWM) Task ********************
// This task calculates the high time and low time of the PWM output based on
// the value of the input_voltage which is updated by the data gathering task.
// The goal is to set the duty cycle of the PWM output so that the average of
// the PWM signal equals the input_voltage. This is achieved by solving the
// equation:
// DutyCycle = (input_voltage - low.output) / (high.output - low.output)
// in which low.output and high.output are the voltage levels that appear
// on the PWM output pin in the low and high states, respectively.
// Given the duty cycle, and given our choice of a PWM period of 130 msec, we
// calculate the high.time and low.time of the PWM signal in terms of the timer
// counts; each timer count equals 2 microseconds. high_time is the number of
// timer counts that the signal is in the high state and low_time is the number
// of timer counts that the signal is in the low state during each 130 msec
// period. high_time and low_time are calculated as:
// high_time = Duty.cycle * TIMER_COUNTS_PER_PERIOD
// low_time = TIMER_COUNTS_PER_PERIOD - high_time
// where TIMER_COUNTS_PER_PERIOD = 130 msec/2 microsec = 65,000.
// We also "clamp/" high_time and low_time to a minimum value to prevent a
// situation where interrupts are requested so rapidly that the processor
// can’t service them.
// The OC3 (output compare 3) interrupt code in the subsequent section uses
// these calculated values to pulse width modulate the PORTA output port pin
// PA5.
// We define some variables and constants:
static uint high_time; // the number of timer counts that PWM output is high
static uint low_time; // the number of timer counts that PWM output is low
static float duty_cycle;
#define LOW_OUTPUT_VOLTAGE 0.0 // the voltage on PA5 when it is low
#define HIGH_OUTPUT_VOLTAGE 5.0 // the voltage on PA5 when it is high
// NOTE: for maximum accuracy, the voltage output of pin PA5 could be measured
// in the low and high states and LOW_OUTPUT_VOLTAGE and HIGH_OUTPUT_VOLTAGE
// set accordingly.
#define MS_PER_PERIOD 130 // use a 130 msec period for PWM output
// NOTE: the timer "rolls over/" every 131 msec
#define TIMER_COUNTS_PER_PERIOD 65000
// 130 milliseconds corresponds to 65,000 counts of 2 microseconds (usec)
// each on the free-running timer. QED-Forth sets the contents of TMSK2 so
// that each timer count represents 2 usec. This is true whether the crystal
// speed is 8 MHz or 16MHz. We assume here that the programmer has not
// installed a different value in TMSK2 using InstallRegisterInits().
#define MINIMUM_HIGH_OR_LOW_TIME ((int) 250) // corresponds to 500usec
// we choose a minimum high or low time 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 bey doing so we can ensure that the minimum time between PWM
// interrupts is 500 usec. This is more than sufficient time to allow the
// service routine to execute.
_Q void CalculateDutyCycle(void)
// implements the equation:
// duty_cycle = (input_voltage-low.output)/(high_output-low_output)
// duty_cycle is in the range 0.0 to 1.0
{ duty_cycle = (input_voltage - LOW_OUTPUT_VOLTAGE)
/ (HIGH_OUTPUT_VOLTAGE - LOW_OUTPUT_VOLTAGE);
duty_cycle = MIN((float) 1.0, duty_cycle) ;
duty_cycle = MAX((float) 0.0, duty_cycle) ; // clamp to 0<=duty_cycle<=1.0
}
_Q void HighAndLowTimes(void)
// saves high and low times as 16bit integer counts in the variables
// high_time and low_time using the equations:
// high_time = duty_cycle * TIMER_COUNTS_PER_PERIOD
// low_time = TIMER_COUNTS_PER_PERIOD - high_time
// both high_time and low_time are clamped to a minimum so that timer interrupts
// don’t occur more frequently than they can be reliably serviced.
{
high_time = MAX((TIMER_COUNTS_PER_PERIOD *
duty_cycle),MINIMUM_HIGH_OR_LOW_TIME);
low_time = MAX((TIMER_COUNTS_PER_PERIOD - high_time),MINIMUM_HIGH_OR_LOW_TIME);
high_time = TIMER_COUNTS_PER_PERIOD - low_time;
// make sure that high_time + low_time is exactly one period despite clamping
}
_Q void SetPulseParameters( void )
// this is the activation roution for the pwm task. It updates the
// values in high_time and low_time for use by the oc3 interrupt which
// generates the pwm output waveform on PORTA pin PA5.
{ while(1)
{ CalculateDutyCycle();
HighAndLowTimes();
Pause();
}
}
// ********************* OC3 Interrupt Code **********************
// This interrupt routine generates a pulse width modulated output on Port A
// bit 5 based on the duty cycle calculated by the PWM task.
// There are 4 steps involved in coding an interrupt service routine:
// 1. Name all required hardware registers (see the QEDREGS.H file in the
// \MOSAIC\FABIUS\INCLUDE\MOSAIC directory),
// and name all required bit masks with appropriate mnemonics.
// 2. Use C or assembly code to define an interrupt handler which
// must clear the interrupt request flag and perform required
// service actions.
// 3. Install the interrupt handler using the Attach() function.
// 4. Write routines to enable and disable the interrupt. (We combine
// steps 3 and 4 in a routine that ATTACHes and enables the interrupt).
// Define output mode configuration flags and masks that specify action
// to be performed when a successful output compare occurs:
#define PA5_MASK 0x20 // mask for setting/resetting PA5
#define OC3_MASK 0x20 // to set/clr OC3 interrupt flag & mask
#define OC3_MODE_MASK 0x20 // mask in TCTL1; enables PA5 pin control
#define OC3_LEVEL_MASK 0x10 // mask in TCTL1; controls level of PA5
// Summary of the functions of these registers: OC3 (output compare 3) is
// associated with pin PA5 on PORTA. We can write a 16 bit count into the TOC3
// register, and when the count in TOC3 matches the count in the main counter
// TCNT, an interrupt request can occur. The request only occurs if interrupts
// are globally enabled and if the OC3 interrupt is locally enabled. The OC3
// interrupt is locally enabled by setting the bit specified by the OC3_MASK in
// the TMSK1 register. When the interrupt request occurs, the 68HC11
// automatically sets the bit specified by OC3_MASK in the TFLG1 (timer flag
// #1) register. Our interrupt service routine must clear this bit (oddly
// enough, interrupt request bits are cleared by writing a 1 to the bit
// position!) The register TCTL1 controls whether the state of the PA5 bit is
// automatically changed when TOC3 matches TCNT. In this application we enable
// this automatic "pin control/" action by setting the bit specified by
// OC3_MODE_MASK in TCTL1. The bit specified by OC3_LEVEL_MASK in TCTL1 then
// controls the level (high or low) to which PA5 is set upon a successful
// output compare.
_Q void OC3Service(void)
{ char temp = OC3_LEVEL_MASK;
TFLG1 = OC3_MASK; // reset the oc3 interrupt flag so that new oc3
// interrupts will be recognized. because the flag
// is cleared by writing a one to it we can use a
// assignment command without affecting other bits.
if( temp &= TCTL1 ) // look at the oc3/pa5 pin output level
{ TCTL1 &= ~OC3_LEVEL_MASK; // AND with complement of mask
TOC3 += high_time;
} // if the output level just toggled high we’ll
// set the mode/level bit so the next output
// compare forces the pin low after the high_time
else // else if the output level just toggled low...
{ TCTL1 |= OC3_LEVEL_MASK;
TOC3 += low_time; // set the mode/level bit so the next output
} // compare forces the pin high after low_time
}
// Pass the required constants through to the assembler:
// Don’t insert any leading spaces on a line that contains an EQU directive.
#asm
TOC3 EQU $801A
TCTL1 EQU $8020
TFLG1 EQU $8023
PA5_MASK EQU $205
OC3_MASK EQU $20
OC3_MODE_MASK EQU $20
OC3_LEVEL_MASK EQU $10
NOT_OC3_LEVEL_MASK EQU $EF
#endasm
_Q void AssembledOC3Service( void )
// This interrupt service routine performs the same functions as the high level
// service routine named OC3Service. This alternative interrupt service routine
// is assembly coded to illustrate the principles of assembly coding with
// this environment. This routine it executes in under 45 usec
// including the call and return overhead. The high level version
// is nearly as fast.
// NOTE: #asm and #endasm in-line assembly directives
// must be the first on a line,
// AND: each line of assembly code MUST START WITH A TAB OR SPACE,
// BUT: labels must NOT be preceeded by a tab or space.
// The comment character for assembly code is the * as shown below.
{
#asm
ldab TCTL1
bitb #OC3_LEVEL_MASK * Look at the OC3/PA5 pin output level
beq is_low
* If output just went high, clear the level bit in TCTL1: next OC3 clrs PA5
andb #NOT_OC3_LEVEL_MASK
stab TCTL1
ldd TOC3
addd high_time
bra finished
is_low
* If output just went low, set the level bit in TCTL1: next OC3 sets PA5
orab #OC3_LEVEL_MASK
stab TCTL1
ldd TOC3
addd low_time
finished
std TOC3 * update the OC count for the next output compare
ldaa #OC3_MASK * Reset the OC3 interrupt flag by writing a 1
staa TFLG1 * so that new OC3 interrupts will be recognized.
#endasm
}
_Q void InstallOC3( void )
// This installation routine enables the "pin control/" function, ATTACHES the
// service routine, configures PortA pin 5 as an output, initializes the output
// compare register TOC3, clears the OC3 interrupt request flag (thus ignoring
// prior interrupt requests), and locally enables the OC3 interrupt.
// PWM can begin when interrupts are globally enabled.
{
TMSK1 &= ~OC3_MASK; // Disable OC3 interrupts.
TCTL1 |= OC3_MODE_MASK;
// Set the OC3 mode bit so that an output compare sets or clears PA5 pin
// depending on the level bit which is toggled by the interrupt routine.
// ATTACH(AssembledOC3Service, OC3_ID); // optional assembly service routine
ATTACH(OC3Service, OC3_ID); // use the C-coded version
TCTL1 &= ~OC3_LEVEL_MASK; // Set to low before pulses start.
CFORC |= OC3_MASK; // and init by forcing a compare.
TOC3 = TCNT + TIMER_COUNTS_PER_PERIOD; // start after a min 130ms delay
TFLG1 = OC3_MASK; // clear interrupt flag OC3F
TMSK1 |= OC3_MASK; // set OC3I to locally enable OC3
}
// ************************** Statistics Task **************************
// This task samples the analog input voltage (see task 1) once per second,
// stores it in a matrix, and calculates and prints the mean and standard
// deviation of the data every 10 seconds.
FORTH_ARRAY Last10Voltages;
// row matrix; holds 1 voltage per second for past 10 seconds
// these index variables keep track of where data is placed in Last10Voltages:
static int matrix_index, last_index;
// these hold the derived statistics:
static float mean, standard_deviation;
_Q float RowMean(int rownum, FORTH_ARRAY * array_ptr)
// sums the elements in the specified row of the specified matrix and
// divides by the number of elements in the row to calculate the mean.
{ float accumulator = 0.0;
int i, numcolumns = NUMCOLUMNS(array_ptr); // see \Mosaic\ARRAY.H file
for(i=0; i < numcolumns; i++)
accumulator += FARRAYFETCH(float, rownum, i, array_ptr);
return (accumulator / numcolumns);
}
_Q float RowStandardDeviation(int rownum, FORTH_ARRAY* array_ptr, float row_mean)
// subtracts the row mean from each element and sums the squares of all
// resulting differences in the specified row of the specified matrix
// and takes the square root of the sum to calculate the standard deviation.
{ float accumulator = 0.0, temp;
int i, numcolumns = NUMCOLUMNS(array_ptr); // see \Mosaic\ARRAY.H file
for(i=0; i < numcolumns; i++)
{ temp = FARRAYFETCH(float, rownum, i, array_ptr) - row_mean;
accumulator += (temp * temp);
}
return sqrt(accumulator);
}
_Q void InitFPArray(float value, FORTH_ARRAY * array_ptr)
// stores specified floating point value in each element of the array
// do not use this function to init arrays that hold char, int or long data
{ int r, c,
numcolumns = NUMCOLUMNS(array_ptr), // see \Mosaic\ARRAY.H file
numrows = NUMROWS(array_ptr);
for(c=0; c< numcolumns; c++)
for(r=0; r < numrows; r++)
FARRAYSTORE(value, r, c, array_ptr);
}
_Q void CalcStats( void )
// This routine calculates mean and standard deviation of the
// Last10Voltages matrix and stores them in variables.
{
mean = RowMean(0, &Last10Voltages); // row# = 0
standard_deviation = RowStandardDeviation(0, &Last10Voltages, mean);
}
// These routines manage the LCD display.
// The message on the display will look like this:
// Mean =
// x.xxx Volts
// Standard Deviation =
// x.xxx Volts
#define FP_OFFSET 4 // character offset to start of mean or
// std.deviation in display line
#define VOLTS_OFFSET 14 // character offset to start of "Volts/" label
// in display line
#define MAX_CHARS_PER_DISPLAY_LINE 40
// large enough for 4x20 character display as well as 16 x 40 graphics display
#define SPACE 0x20 // ascii space {blank}
#define DISPLAY_CONFIG_BYTE ((uchar*) 0xAE1E) // see grphext.c file
#define TOSHIBA_VS_HITACHI_MASK 0x40 // 1 = toshiba; 0 = hitachi
char linebuffer[MAX_CHARS_PER_DISPLAY_LINE+1];
// single line buffer to assist in writing to display
// NOTE: it’s all right if this buffer is longer than the display line;
// StrToDisplay() will ignore the extra characters.
_Q uint Toshiba(void)
// returns flag, = true if toshiba display; false if hitachi
{
return (((*DISPLAY_CONFIG_BYTE) & TOSHIBA_VS_HITACHI_MASK) ? 1 : 0);
}
_Q void BlankDisplayString(char* string_base, int numelements)
// writes "blanks/" into the specified string, taking into account that
// Toshiba graphics displays in text mode use 0x00 as the code for BL
// numelements includes the terminating null character
{ int i;
char blankchar = (Toshiba() ? 0 : SPACE);
for( i=0; i < (numelements-1); i++)
string_base[i] = blankchar;
string_base[numelements-1] = 0; // terminating null
}
_Q void SetupDisplay( void )
// writes headings to display; leaves numbers blank.
{ xaddr display_buffer_base = DisplayBuffer();
int numlines = LinesPerDisplay();
int chars_per_line = CharsPerDisplayLine();
char blankchar = (Toshiba() ? 0 : SPACE);
FillMany(display_buffer_base, (numlines * chars_per_line), blankchar);
// FillMany() blanks the display buffer // blank display buffer
STRING_TO_DISPLAY(“mean = “, 0, 0 ); // line 0
STRING_TO_DISPLAY(“volts”, 1, VOLTS_OFFSET); // line 1
STRING_TO_DISPLAY(“standard deviation =“, 2, 0 ); // line 2
STRING_TO_DISPLAY(“volts”, 3, VOLTS_OFFSET); // line 3
UpdateDisplay(); // write buffer contents to display
}
// sprintf(char* s, const char *format, ... ) does formatted write to specified
// string. Returns #chars assigned,
// not including the terminating null that it writes after the last char.
// Returns a negative number if an error occurs.
_Q void ShowStats( void )
// displays mean and standard deviation of Last10Voltages on LCD display
{ int num_chars_placed;
BlankDisplayString(linebuffer, MAX_CHARS_PER_DISPLAY_LINE);
num_chars_placed = sprintf(linebuffer, "%7.3f", mean);
// use field width = 7 chars; 3 digits to right of the decimal point
if(num_chars_placed > 0)
STRING_TO_DISPLAY(linebuffer, 1, FP_OFFSET); // line 1
BlankDisplayString(linebuffer, MAX_CHARS_PER_DISPLAY_LINE);
num_chars_placed = sprintf(linebuffer, "%7.3f", standard_deviation);
if(num_chars_placed > 0)
STRING_TO_DISPLAY(linebuffer, 3, FP_OFFSET); // line 3
UpdateDisplay(); // write to display
}
_Q void LogDataAndShowStats( void )
// increments matrix_index every second,
// loads input_voltage data collected by task 1 into Last10Voltages array,
// and displays statistics on LCD display every 10 seconds
{ float latest_input;
// we need this temporary because we must do an uninterrupted fetch
// from input_voltage, then pass the result to FARRAYSTORE
matrix_index = (ReadElapsedSeconds()) % 10; // 0<= matrix_index <= 9
if(matrix_index != last_index) // if a second has elapsed...
{ latest_input = PeekFloatUninterrupted(&input_voltage);
FARRAYSTORE(latest_input, 0, matrix_index, &Last10Voltages);
// store reading in matrix
last_index = matrix_index; // update last_index
if(matrix_index == 9) // if 10 seconds have elapsed...
{ CalcStats(); // calculate new statistics
ShowStats(); // update display
}
}
}
_Q void Statistics( void )
// this is the activation routine for the statistics task;
// it calculates and displays the mean and standard deviation of the data.
{
DIM(float, 1, 10, &Last10Voltages); // dimension as a 1-row array
InitFPArray(0.0, &Last10Voltages); // initialize array
last_index = -1; // initialize last_index
SetupDisplay(); // write headings to display
while(1)
{ LogDataAndShowStats(); // calc and display statistics
Pause();
}
}
// ********************* BUILD TASKS ****************************
// Note: we’ll keep the Forth interpreter task active during
// development/debugging, and put it ASLEEP in the final turnkeyed version.
// First declare the tasks and allocate their 1K task areas:
// FORTH_TASK (see MTASKER.H) is the default task running QED-Forth;
// this task is automatically built and started upon each reset/restart;
// in the autostart routine FORTH_TASK puts itself ASLEEP so the end
// user can’t run Forth.
TASK ReadInputTask; // data gathering task base xaddr
TASK ControlOutputTask; // PWM task base xaddr
TASK StatisticsTask; // statistics reporting task
// BUILD_C_TASK(HeapStart,HeapEnd,Base) and
// ACTIVATE(function_ptr, task_base_addr)
// macros are defined in \mosaic\mtasker.h file.
_Q void BuildTasks( void )
// Empties the round robin task loop and then
// carefully builds the tasks every time we start up.
// Note that only the statistics task has access to the heap.
{
NEXT_TASK = TASKBASE; // important! empties task loop before building
SERIAL_ACCESS = RELEASE_ALWAYS; // allow sharing of serial ports
BUILD_C_TASK(0,0,&ReadInputTask);
BUILD_C_TASK(0,0,&ControlOutputTask);
BUILD_C_TASK(DEFAULT_HEAPSTART,DEFAULT_HEAPEND,&StatisticsTask);
}
_Q void ActivateTasks( void )
// associate activation routines with each of the tasks.
{
ACTIVATE(GatherData, &ReadInputTask);
ACTIVATE(SetPulseParameters, &ControlOutputTask);
ACTIVATE(Statistics, &StatisticsTask);
}
// ********************* SET UP AUTOSTART ROUTINE *********************
// We’ll designate the top level word main as the PRIORITY.AUTOSTART routine.
// Every time the QED-Board is powered up or reset, the main routine will
// automatically be executed by the default FORTH_TASK
// main() zeros the variable area and initializes the heap.
// During debugging, the FORTH task which runs main() has access to
// all defined function names declared using the _Q keyword;
// the names are sent to the QCard via the .DLF download file.
// This helps to debug the program.
// main() initializes the elapsed time clock, installs the
// OC3 interrupt service routines, and builds and activates the tasks.
// It releases control of the serial line, starts the timeslicer, and PAUSEs
// to begin execution of the application.
// After debugging is complete, the optional commands which
// specify a COLD restart and which put the FORTH_TASK ASLEEP can be inserted;
// these commands are "commented out/" in the code shown here.
_Q void InitVariables(void)
{ // init static variables & strings at runtime, and delete array
// to clean up the heap before allocating heap items
input_voltage = 0;
high_time = low_time = 0;
DELETED(&Last10Voltages);
matrix_index = last_index = 0;
mean = standard_deviation = 0.0;
BlankDisplayString(linebuffer, MAX_CHARS_PER_DISPLAY_LINE);
}
void main(void)
// this is the highest level routine in the turnkeyed application.
{
InitVariables(); // init variables, delete arrays
INIT_DEFAULT_HEAP(); // it’s important to init heap at startup
InitElapsedTime(); // initialize qed elapsed time clock
InstallOC3(); // install service routine for pwm generation
BuildTasks(); // initialize user areas of the tasks
ActivateTasks(); // associate action routine with each task
// the following 2 commands are removed during debugging;present in final version
// ColdOnReset(); // ensures full initialization upon each reset
// STATUS = ASLEEP; // puts forth task asleep;
RELEASE(SERIAL); // in case another tasks needs to use serial
StartTimeslicer(); // starts elapsed time clock, enables interrupts
Pause(); // start next task immediately
}
// Now from your terminal, type:
// CFA.FOR main PRIORITY.AUTOSTART
// this will install main() as the routine that is automatically executed
// each time the board is reset or powered on.
// The PRIORITY.AUTOSTART routine initializes the priority.autostart vector
// at locations 7FFA-7FFF on page 4 which will be in Flash.
// Then upon the next restart the main() routine will automatically execute.
// NOTE: To erase the autostart vector and return to QED-Forth,
// type at the terminal:
// NO.AUTOSTART
// If you have put the FORTH_TASK asleep so that it does not
// respond to the terminal,
// activate the Special Cleanup Mode by installing jumper J1 and
// pressing the reset button.
// The Special Cleanup Mode erases the autostart vector and restores the
// board to a "pristine/" condition (it also sets the default
// baud rate to 19200, and the LCD display type to 4x20 character
// display; use Baud1AtStartup() and IsDisplay() if you need to
// change these defaults).
//
// ********* BURNING THE FINAL APPLICATION ***********
// To generate a Flash chip that contains this application,
// download the TURNKEY.DLF file, and execute the
// CFA.FOR MAIN PRIORITY.AUTOSTART
// command to install the autostart vector.
// Before resetting the board or typing main (which could put FORTH asleep
// and make communications difficult), we’ll make an image of page 4
// which holds the object code and autostart vector:
// To make a Motorola S2-record file to send to the Flash programmer,
// set your terminal to capture incoming text to a disk file using the
// "Save File/" menu item, and interactively type from the terminal:
// HEX 0 4 DIN 0 8000 DUMP.S2
// Close the file when the dump terminates, and use Textpad to remove
// any extraneous file contents such as the DUMP.S2 command and Forth’s
// "ok/" prompts. Then send the resulting file to a Flash Programmer and
// burn a 512k Flash chip.
// Power up the QCard and it will automatically run the 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