Link here

A Turnkeyed C Application Program

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

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

The example application reads a voltage using the onboard analog to digital (ATD) converter and outputs two pulse width modulated (PWM) signals. Each of these PWM outputs, when time-averaged using a simple resistor and capacitor low-pass filter, tracks the input voltage. One of the outputs is generated by the PWM subsystem on PORTP of the Freescale HCS12 (9S12) microcontroller. The PWM subsystem creates a pulse train without the need for interrupts or cycle-by-cycle intervention by the processor, as described in the “Pulse-Width Modulated I/O” chapter of this document. The other pulse output is created by the HCS12’s ECT (Enhanced Capture/Timer) subsystem on PORTT. This output is fashioned by an output compare interrupt that generates each signal transition as described in the “Timer Controlled I/O” chapter. In addition, the program calculates and displays the mean and standard deviation of the input signal at the terminal. This application 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 onboard ATD converter
  • Use floating point mathematics to calculate means and standard deviations
  • Use the PWM subsystem to generate a pulse train output
  • Write and install an output compare interrupt routine that generates a PWM output
  • 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 and it can be found in "Turnkey Demo" project.

Open a new "Turnkey Demo" project for a downloadable version of this code.

Look for this icon under Project→ New Project:
industrial automation control demo (Link to sbc-single-board-computers:freescale-hcs12-9s12-c-language:instrument-control:pdq_turnkey.png using /embedded-systems/_media/sbc-single-board-computers/freescale-hcs12-9s12-c-language/instrument-control/pdq_turnkey.png)
Turnkey Demo

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 10-bit ATD converter, converts the reading to its equivalent voltage, and saves it in a floating point variable called input_voltage.
  • The second “PWM output” task is responsible for the duty cycle of two pulse trains, each having an average value that matches the latest input_voltage measured by the data gathering task. This task calculates the required duty cycle, high_time and low_time of a pulse width modulated (PWM) output signal. It updates the duty count of the 16-bit resolution PWM01 output under the control of the HCS12’s PWM subsystem on PORTP pin PP1 (pin 15 of the PDQ Digital Field Header). Once configured, this creates a PWM output under hardware control with no intervention from the CPU (Central Processing Unit) aside from updates to the PWM duty register.
  • An interrupt attached to the output compare associated with the Enhanced Capture/Timer channel 0 (ECT0) controls the pulse output signal appearing at PORTT pin PT0 (pin 24 of the PDQ Digital 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 serial port for display on the terminal. 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 the QED-Forth operating system and executes main(). In the default 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, and allows the statistics task to use the Serial1 port to send out the data summaries to the terminal. By adding the UseSerial2() command to the statistics task and commenting out the "STATUS = ASLEEP;" line, you can use the interactive QED-Forth task during development and debugging, enabling you 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 PDQ Board or PDQScreen, two resistors, two capacitors, a 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 potentiometer is connected to place a controllable voltage on AN0, which is channel 0 of the ATD converter on the HCS12. The potentiometer may be from 1 Kohm to 100 Kohm in value. The potentiometer is connected between +5VAN and AGND analog ground (pins 2 and 1, respectively, of the Analog Field Header), with the potentiometer’s wiper connected to AN0 (pin 24 of the PDQ Analog Field Header).

PWM outputs are independently generated on two pins, one on PORTP generated by the PWM subsystem, and one on PORTT generated by the ECT subsystem. One pulse train appears on PP1 (that is, pin 1 of PORTP) at pin 15 of the PDQ Digital Field Header. The other pulse train appears on PT0 at pin 24 of the PDQ Digital Field Header. On each of these output pins, a series resistor followed by a capacitor to ground 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 terminal via one of the serial ports (serial1 is the default, but this can be changed to serial2 as described below).

 

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 the heap which holds FORTH_ARRAYs in paged memory. Fortunately, the Mosaic IDE Plus automatically sets up a versatile default memory map. This is discussed in detail in the chapter titled Making Effective Use of Memory. For reference, here is a brief summary of the main memory areas allocated by GNU C:

  • 0x8000-BFFF (16K per page) in pages 0x00-0x13 holds up to 320Kbytes of compiled application code (on pages 00-0x0F) and debugging headers (starting at page 0x10). Pages 0x00-0x17 can be backed up to shadow flash and restored at each COLD restart using the SAVE.ALL interactive command. Pages 00-0x13 can also be write protected using the interactive WP.ALL command.
  • 0x8000-BFFF (16K per page) in pages 0x14-0x1C is paged RAM. Pages 0x18-1C is a heap area that holds array data. The DEFAULT_HEAPSTART constant equals the heap start xaddress (0x188000), and the DEFAULT_HEAPEND constant equals the end xaddress of this region (0x1CBFFF), as declared in the C:\MosaicPlus\c\libraries\include\mosaic\HEAP.h file.
  • 0x0800-0FFF and 0x2000-7FFF is 26K available common RAM which hold C variables, C arrays, as well as the pfa’s (parameter field areas) of FORTH_ARRAYs.
  • 0x0680-07FF is available EEPROM referenced by the eeprom section {EEPROM at 0x0400-067F is reserved for startup utilities and interrupt revectoring}. To locate a variable in eeprom, append the _eeprom tag, as in:
    int testEEVar  _eeprom;

  • The operating system occupies onchip flash at 0x8000-BFFF on pages 0x38-3F, common flash at 0xC000-FFFF, and reserves 0x8000-BFFF on pages 0x1D-1F for system RAM and memory-mapped device addressing.

The object code ends up in RAM that is backed up to and restored from the shadow flash. The variable area is in common RAM that is accessible regardless of the page, and the heap is located in paged 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 paged memory starting at page 0x10 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 it does not have to be included in the final downloaded production image. 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 should be included in the final turnkeyed system.

 

ATD data gathering task

The first section of the example application code uses the HCS12’s analog to digital (ATD) converter to convert the voltage input derived from the external potentiometer to a digital value. As explained in the Data Acquisition Using Analog to Digital Conversion chapter of this document, each conversion result is returned as a 10-bit field left justified in a 16-bit integer result, and can be interpreted as a 16-bit unsigned positive result between 0 and 65535. The task converts the ATD 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 ATD, and the number of counts (2^16 = 65536 counts). The CountToVolts() function converts the ATD 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 ATDOn() to power up the ATD, and enters an infinite loop that acquires an ATD sample, converts it to a voltage, and stores it in the variable input_voltage which other tasks can read.

Special storage operators named PeekFloatUninterrupted() and PokeFloatUninterrupted() are declared using the lock() and unlock() keywords. As explained in the How to temporarily disable interrupts section, these functions temporarily disable interrupts and restore the global interrupt enable flag (the I bit in the Condition Code Register) to its prior condition. The uninterruptable peek and poke routines are 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 millisecond.

 

Pulse Width Modulation (PWM) task

The goal is to create two PWM outputs, each of whose average voltage is equal to the input voltage read by the data gathering task. The period of each PWM signal is arbitrarily chosen to be 100 milliseconds (ms). 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 signals. One of the signals is generated by the PWM subsystem on PORTP; after configuration, a simple write to the duty register of this output is sufficient to change the PWM duty cycle to reflect the latest value of the analog input. The other pulse output on PORTT is generated with the help of the ECT0_ID (Enchanced Capture/Timer channel 0) output compare interrupt. This interrupt routine (described in the next section) controls the PORTT output signal subject to the high_time and low_time 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 low and high voltage levels on pins PP1 and PT0 and set these constants accordingly.

The default period of the TCNT free-running timer which drives the ECT system is 1.6 microseconds (us). The period of each PWM output is chosen to be 100 ms, which corresponds to 62,500 TCNT counts. For simplicity and symmetry, we also configure the clock source for the PWM01 output to have a 1.6us period. This means that the high time for each of the two PWM outputs should be equal. The high time of the PWM01 output on PORTP pin PP1 is simply the value written to its duty register, so the PWM task performs this write as:

PWMDutyWrite( high_time, PWM01 );  // write high_time to channel PWM01 duty register 

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 1000. That is, we only require 10 bits of resolution from our PWM; after all, the output is meant to mimic an analog input that is converted with 10 bits of resolution (1 part in 1024). The benefit of imposing a minimum high or low time is that by doing so we can ensure that the minimum time between ECT output compare interrupts is more than adequate to allow the interrupt 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.

 

ECT Output Compare interrupt code

This code defines an interrupt service routine and an installation routine for the ECT0_ID (Enchanced Capture/Timer channel 0) output compare interrupt which controls the PORTT output signal on pin PT0. The relevant HCS12 hardware register names are defined in the hcs12regs.h file in the C:\MosaicPlus\c\libraries\include\mosaic directory.

ECTPulseMaker() is the interrupt service routine that controls the state of the PT0 output bit. It relies on the ability of the output compare (OC) function to automatically change the state of an associated PORTT output bit at a specified time. Specifically, the ECT0 output compare can be configured to automatically change the state of the associated output pin PT0 when the count in the TC0 register matches the contents of the free-running counter (TCNT) register; this is called a “successful output compare”. Invoking the OCAction() function with the OC_SET_ACTION parameter causes the output to go high on the next successful compare, and invoking OCAction() with the OC_CLEAR_ACTION parameter causes the output to go low on the next successful compare. The ECTPulseMaker() routine simply reverses the target pin state and adds the appropriate high_time or low_time increment to the TC0 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. In fact, most of the execution time of the ECTPulseMaker() routine is consumed calling the OCAction() function. Calling an operating system function from a C program requires about 3 us (microseconds) of overhead, plus about 2 us per parameter passed to the function. To rigorously optimize the speed of this interrupt service routine, you could eliminate the call to OCAction() and instead write directly to the ECT0 mode and level bits that control the pin action. This would involve studying the Motorola ECT Block User Guide document, defining the appropriate bit masks, and coding the instructions to manipulate them. The service routine as shown is reasonably fast, and uses the built-in driver function to yield simple and maintainable code.

InstallPulseMaker() calls OutputCompare() and OCAction() to enable direct hardware control of PORTT pin PT0 by the ECT0 channel. It calls ATTACH() to post ECTPulseMaker() as the ECT0_ID interrupt handler routine, sets the “fast clear mode” so that the service routine does not have to explicitly clear the interrupt flag bit, and enables the ECT0 interrupt. The SetPulseParameters() PWM task activation routine calls the InstallPulseMaker() initialization routine when the task starts up. Note that MAKE_ISR() must be called with ECTPulseMaker as an argument for ATTACH() to execute properly.

 

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 a 1-line summary of the mean and standard deviation of this data to the terminal 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.

CalcStats() calculates the latest calculated mean and standard deviation values, and ShowStats() writes them to the serial port. ShowStats() uses printf() to print the floating point numbers. 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 and enters an infinite loop that logs the data and displays the statistics. If you want to simultaneously debug this program using the serial1 port while seeing the statistics printout on the serial2 port, insert the UseSerial2() command before the infinite loop in the Statistics() routine. The default baud rate for both the serial1 and serial2 ports is 115200 baud; this can easily be changed by using the Baud routine in either C or interactive Forth. Pause() is included in the infinite loop so that both cooperative and timesliced task switching are used.

This task calls the PauseOnKey() function. When you type a Carriage Return (labeled "ENTER" on many keyboards), PauseOnKey() calls Abort() to end the program and enter the Forth monitor. This lets you regain control of the processor.

If you have used PRIORITY.AUTOSTART:
to set up this program to autostart, typing a carriage return followed by NO.AUTOSTART will remove the autostart vector so that other programs may be loaded into the controller.
 

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.

The command

SERIAL_ACCESS = RELEASE_ALWAYS;

is inserted to make sure that all tasks release the serial port after each character is sent or received. In applications in which more than one task tries to access the serial port, this command avoids serial port contention that can effectively “silence” the program.

Note that 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 C:\MosaicPlus\c\libraries\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 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, 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 may be commented out during program development so that the awake QED-Forth task can be used to aid in debugging; in this case, the UseSerial2() command should be inserted in the Statistics() task activation routine as discussed above.

The main() function then RELEASEs the FORTH_TASK’s control of the serial line. This is necessary whenever another task requires access to the serial port. 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() ensures 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:

Open a new "Turnkey Demo" project for a downloadable version of this code.

Look for this icon under Project→ New Project:
factory automation (Link to sbc-single-board-computers:freescale-hcs12-9s12-c-language:instrument-control:pdq_turnkey.png using /embedded-systems/_media/sbc-single-board-computers/freescale-hcs12-9s12-c-language/instrument-control/pdq_turnkey.png)
Turnkey Demo

Once the demo is open, click Build→ Build. 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 PDQ Board is turned on and is communicating with the Mosaic Terminal. Then download the program to the PDQ Board by using the terminal’s “Send File” menu item to send the TURNKEY.dlf file.

 

Using SAVE, RESTORE and write-protection during debugging

Note that the download file contains the SAVE.ALL directive, which automatically backs up the code image to the shadow flash, and causes it to be automatically reloaded upon each COLD restart. You can optionally type at the terminal:

WP.ALL↓

to write protect all the protectable pages; to undo this directive, type

WE.ALL↓

to write enable all of the pages. The chapters titled Your First C Program and Writing and Compiling Programs explain these concepts in detail.

 

Going into production

To load a pristine board with the application, simply download the TURNKEY.DLF file, and execute the

PRIORITY.AUTOSTART: MAIN

command to install the autostart vector. Power cycle the Board and it will automatically run the application!

The downloaded program file typically executes the SAVE or SAVE.ALL command which store 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 here. 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.

 

Configure the board to autostart the program

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

PRIORITY.AUTOSTART: main↓

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 the top of page 0x0F RAM and page 0x0F shadow flash, ensuring that the vector will be present whenever code is restored from the shadow flash.

Then upon the next restart the main() routine will automatically execute. Note that the QED-Forth V6.xx greeting is suppressed when an autostart routine is installed; if desired, you could easily print your own greeting by modifying the main() function.

To erase the autostart vector and return to the QED-Forth prompt, hit the “Enter” key on your terminal to cause the program to return to the Forth monitory, then type at the terminal:

NO.AUTOSTART↓

If the program does not respond to the terminal, activate the Special Cleanup Mode by installing the jumper labeled “Clean” next to the reset button, and then 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 115200, and sets the default serial port as serial1. If you need to change these defaults, invoke the interactive BAUD and/or USE.SERIAL2 commands.

The PRIORITY.AUTOSTART: command is used to configure systems that will go into production. For one-of-a-kind prototypes, another operating system command called simply AUTOSTART: is available that installs the autostart pattern in the on-chip flash that resides in the HCS12 proceessor chip. The pattern installed by AUTOSTART: is in the processor chip and not in the paged RAM along with the program code. Both autostart vectors are useful, but the priority autostart is often preferred because it keeps the autostart vector in the same region of memory as the program code.

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

 

Turnkeyed application code listing

Listing 14-1



See also → A Brief Introduction to Forth Programming

 
 
 
Navigation
 
Registration on or use of this site constitutes acceptance of our User Agreement and Privacy Policy. Purchase of Mosaic's products constitutes acceptance of the End User License Agreement, Sales Terms and Conditions, and Life Support policy. Mosaic’s products are not authorized for use as components in life support or medical devices. The material on this site may not be reproduced, distributed, transmitted, cached or otherwise used, except with the prior written permission of Mosaic Industries, Inc. Mosaic and other product names are trademarked and should be capitalized when reproduced. You are encouraged to link to pages of this site.

© Mosaic Industries, Inc. All rights reserved.