A Turnkeyed Forth Application Program
This chapter presents and explains an example program that illustrates how to implement data acquisition and automation functions in a multitasking environment using the Forth programming language. The program integrates a range of hardware and software features available on 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. For a C-language version, see A Turnkeyed C Application Program.
Overview of the application
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:
- Set up a memory map using the DEFAULT.MAP function
- Declare an APPLICATION segment header
- Take advantage of pre-compiled device driver LIBRARY segments using the REQUIRES.FIXED directive
- 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 PDQ_TURNKEY.4th in the C:\MosaicPlus\forth\demos 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. Wherever possible, the function and variable names were kept the same as in the C turnkey application program presented in an earlier chapter to simplify comparison between the C and Forth versions of the program.
Structure 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 matrix 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 matrix with new data, repeating the entire process. Recall that a matrix is simply a 2-dimensional array containing floating point numbers.
There is one additional task that is always present in this application: the default task that runs the QED-Forth operating system and executes the top level function called DoTurnkey. 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 ASLEEP command, 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 also the heap which holds arrays and matrices in paged memory. Fortunately, the DEFAULT.MAP function automatically sets up a versatile default memory map. This is discussed in detail in the chapter titled Using Paged Memory. For reference, here is a brief summary of the main memory areas allocated by the PDQ Board:
- 0x8000-BFFF (16K per page) in pages 0x00-0x13 holds up to 320Kbytes of compiled application code (on pages 00-0x0F) and 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. DEFAULT.MAP initializes the DP (Dictionary Pointer) to 0x8000 on page 0x00, and initializes the NP (Names Pointer) to 0x8000 on page 0x10.
- 0x8000-BFFF (16K per page) in pages 0x14-0x1C is paged RAM. Pages 0x18-0x1C are allocated as a heap area that holds array data. The DEFAULT_HEAPSTART constant equals the heap start xaddress (DIN 0x188000), and the DEFAULT_HEAPEND constant equals the end xaddress of this region (DIN 0x1CBFFF), as defined in the pdq_turnkey.4th source file. These constants are passed to the IS.HEAP function to initialize the heap. Make sure to include an IS.HEAP command in the startup routine of every application program that you write.
- 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 arrays and matrices. The VP (Variable Pointer) is initialized to 0x2000 in common RAM.
- 0x0680-07FF is available EEPROM (EEPROM at 0x0400-067F is reserved for startup utilities and interrupt revectoring}. DEFAULT.MAP initializes the EEP (EEPROM Pointer) to 0x0680.
- 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 arrays, matrices 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 may not be 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 ATD.On
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 |F@| and |F!| are used to access the input_voltage variable. These functions temporarily disable interrupts while performing the memory access. The uninterruptable save and store 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:
high_time @ PWM01 PWM.Duty.Write \ 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.
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 OC.Action
function with the OC.SET.ACTION parameter causes the output to go high on the next successful compare, and invoking OC.Action
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. The service routine as shown is reasonably fast, and uses the built-in driver function to yield simple and maintainable code.
InstallPulseMaker calls Output.Compare
and OC.Action
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.
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 matrix called Last10Voltages to hold the data; this matrix resides in the heap associated with the statistics task. The last_index variable keeps track of the prior matrix index to manage the 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 .” and F. to print the floating point statistics summary. The FILL.FIELD ON 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. PAUSE is included in the infinite loop so that both cooperative and timesliced task switching are used.
This task calls the PAUSE.ON.KEY function. When you type a Carriage Return (labeled "ENTER" on many keyboards), PAUSE.ON.KEY calls ABORT to end the program and enter the Forth monitor. This lets you regain control of the processor.
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 ALLOCATE.TASK: statement to name the tasks and allocate their 1K task areas in common RAM. 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:
(STATUS) NEXT.TASK !
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
RELEASE.ALWAYS SERIAL.ACCESS !
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 at the top of the file. Note that we also use the same heap specification for the default Forth task. 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 top level function
The DoTurnkey 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 DoTurnkey.
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, DoTurnkey executes:
DEFAULT_HEAPSTART DEFAULT_HEAPEND IS.HEAP
to initialize the heap of the Forth task, calls INIT.ELAPSED.TIME to initialize the elapsed time clock to zero, and builds and activates the tasks.
The next command in main is
COLD.ON.RESET
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 DoTurnkey function then puts the default Forth task asleep by executing:
ASLEEP STATUS !
This takes effect once multitasking commences, and does not prevent execution of the remainder of DoTurnkey. 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 DoTurnkey 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 the program, make sure that your PDQ Board is turned on and is communicating with the Mosaic Terminal. Then simply download the pdq_turnkey.4th source code file to the PDQ Board by using the terminal’s “Send File” menu item.
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 Program” and “Writing, Compiling, Downloading and Debugging Programs” explain these concepts in detail.
Going into production
To load a pristine board with the application, simply download the PDQ_TURNKEY.4th file, and execute the
PRIORITY.AUTOSTART: DoTurnkey
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. 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.
To speed up the loading of programs into boards that are being produced in volume, you can create a pre-compiled version of the application program that can be more quickly downloaded into production systems. This program was declared as an application segment using the command
APPLICATION TURNKEY_APP
in the source code file. By executing
IN.PLACE FALSE COMPOSE.FORTH.INSTALLER.FOR TURNKEY_APP " turnkey.fin"
after initially compiling this program, the board will dump back to the terminal a Forth INstaller file named turnkey.fin. When this file is later downloaded to a pristine board, the application is installed. Of course, if there are any REQUIRED
device driver libraries (such as the LIBNAME example described in the “Memory Map and Required Segments” section of the source code file, their *.fin files should be loaded before loading the turnkey.fin file. This technique provides a way of speeding the loading of Forth programs into controller boards for volume production. See the glossary entry of COMPOSE.FORTH.INSTALLER.FOR in the Forth glossary for more details. The Segment Management chapter also describes how to use this function.
Configure the board to autostart the program
After debugging is completed you can interactively type the QED-Forth command:
PRIORITY.AUTOSTART: DoTurnkey↓
This will install DoTurnkey 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 DoTurnkey 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 DoTurnkey 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
Turnkeyed Application source code file TURNKEY.4th
1: \ Turnkeyed Application Code Listing, Forth Language version. 2: \ This is a complete real-time application program, with instructions on 3: \ how to set up the program to AUTOSTART each time the processor starts up. 4: 5: \ Copyright 2009 Mosaic Industries, Inc. All Rights Reserved. 6: \ Disclaimer: THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT ANY 7: \ WARRANTIES OR REPRESENTATIONS EXPRESS OR IMPLIED, INCLUDING, BUT NOT 8: \ LIMITED TO, ANY IMPLIED WARRANTIES OF MERCHANTABILITY OR FITNESS 9: \ FOR A PARTICULAR PURPOSE. 10: 11: \ ************************ Turnkeying an Application ******************** 12: \ 13: \ This is the code for an example application. The accompanying chapter 14: \ presents a detailed explanation of this code. 15: \ The code can run on a PDQ Board or PDQScreen. 16: 17: \ Description of the application: 18: \ This program reads an input voltage, creates two pulse width modulated (PWM) 19: \ signals whose average values track the input voltage, and reports the 20: \ mean and standard deviation of the input voltage. 21: \ One of the PWM signals is created using the HCS12 PWM subsystem on PORTP, 22: \ and the other PWM output is created using the HCS12 ECT (Enhanced Capture/Timer) 23: \ subsystem on PORTT. 24: \ The following 3 tasks do the work: 25: 26: \ Task 1: Reads 10-bit Analog To Digital (ATD) input AN0, 27: \ measuring the voltage on a potentiometer 28: \ connected to AN0 (pin 24 of the Analog Field Header). 29: 30: \ Task 2: 31: \ 1. This task outputs on PORTP bit 1 (PP1 is pin 15 of the 32: \ Digital Field Header) a PWM created without interrupts by the 33: \ 16-bit concatenated PWM01 output operated by the 34: \ PWM subsystem of the HCS12 processor on PORTP. 35: \ 2. This task outputs on PORTT bit 0 (PT0 is pin 24 of the Digital Field 36: \ Header) a PWM signal whose average equals the ATD input. This PWM is 37: \ generated using an Output Compare associated with the ECT0_ID interrupt. 38: 39: \ Task 3: Every 1 second puts ATD voltage into a matrix, and every 10 seconds 40: \ displays the mean and standard deviation of the last 10 seconds 41: \ worth of data. 42: \ 43: \ The following issues are addressed: 44: \ 45: \ Using the recommended DEFAULT.MAP memory map 46: \ How to insert REQUIRES commands if pre-compiled drivers are used 47: \ Proper initialization of tasks at startup 48: \ Autostarting 49: \ Interrupt service routines and interrupt attaching 50: \ Initialization of the heap memory manager 51: \ Timesliced multitasking 52: \ ColdOnReset() for secure restart 53: \ Floating point calculations 54: \ Going into production 55: 56: 57: 58: \ ************************ Default Memory Map ************************ 59: 60: \ The "Making Effective Use of Memory" chapter in the User Guide 61: \ provides a complete description of the PDQ memory map. This is a brief summary: 62: 63: \ 0x8000-BFFF (16K per page) in pages 0x00-0x13 holds up to 320Kbytes of 64: \ compiled application code. Pages 0x00-0x17 can be backed up to shadow flash 65: \ and restored at each COLD restart using the SAVE.ALL interactive command. 66: \ Pages 00-0x13 can also be write protected using WP.ALL. 67: 68: \ 0x8000-BFFF (16K per page) in pages 0x14-0x1C is paged RAM. 69: \ Pages 0x18-1C is allocated as a heap area that holds array data. 70: \ To assist in explicitly initializing the heap at startup, 71: \ we define the DEFAULT_HEAPSTART constant to equal the heap start xaddress 72: \ (DIN 0x188000), and the DEFAULT_HEAPEND constant to equal 73: \ the end xaddress of this region (DIN 0x1CBFFF). 74: 75: \ 0x0800-0FFF and 0x2000-7FFF is 26K available common RAM. 76: \ VP starts at 0x2000 to hold variables, pfa’s (parameter field areas) 77: \ of arrays and matrices, etc. 78: 79: \ 0x0680-07FF is available EEPROM referenced by the eeprom section; 80: \ EEP starts at 0x680. 81: \ {EEPROM at 0x0400-067F is reserved for startup utilities 82: \ and interrupt revectoring}. 83: 84: \ The operating system occupies onchip flash at 0x8000-BFFF on pages 0x38-3F, 85: \ common flash at 0xC000-FFFF, and reserves 0x8000-BFFF 86: \ on pages 0x1D-1F for system RAM and memory-mapped device addressing. 87: 88: 89: \ *********************** MEMORY MAP AND REQUIRED SEGMENTS ************ 90: 91: \ If your application requires any pre-compiled LIBRARY segments 92: \ supplied by Mosaic, you can place a #include statement 93: \ of the *.fin (Forth INstaller) file in your source code to load the file. 94: \ See the segment management chapter in the User Guide for more information, 95: \ including the use of the *.qfin (Quick Forth INstaller) file. 96: \ The command looks like this (it may include an optional filepath between the " "): 97: \ #include "LIBNAME.fin" 98: 99: DECIMAL \ compile this program using decimal number base 100: \ note that hex numbers can be entered using a leading 0x 101: DEFAULT.MAP \ establish the default memory map described above 102: 103: APPLICATION TURNKEY_APP 104: \ define the program as an application segment. The matching END.SEGMENT 105: \ command is at the end of the program. 106: \ Declaring the application segment has two advantages: 107: \ 1. It acts as an ANEW statement to simplify reloading of code during debugging. 108: \ 2. It allows you to reference functions defined in pre-compiled 109: \ device drivers called LIBRARY segments. 110: \ Each LIBRARY is referenced by a #include statement as discussed above, 111: \ and has an associated REQUIRES.FIXED or REQUIRES.RELATIVE statement. 112: \ For example, to declare a library named LIBNAME, type: 113: \ REQUIRES.FIXED LIBNAME 114: \ If this statement is commented in, the functions defined in LIBNAME can be 115: \ called by this program. These pre-compiled device driver libraries are 116: \ available for the Wilcards and the Graphical User Interface (GUI) Toolkit. 117: \ Note: if TURNKEY_APP is not going to be relocated to a different address, 118: \ the REQUIRES.FIXED and REQUIRES.RELATIVE declarations are identical. 119: \ 3. It allows you to create a pre-compiled version of the application program 120: \ that can be quickly loaded into production systems. By executing 121: \ IN.PLACE FALSE COMPOSE.FORTH.INSTALLER.FOR TURNKEY_APP " turnkey.fin" 122: \ the board will dump back to the terminal a Forth INstaller file 123: \ named turnkey.fin. When this file is later downloaded to a pristine 124: \ board, the application is installed. 125: \ Of course, if there are any required device driver libraries 126: \ (such as the LIBNAME example described above), their *.fin files 127: \ should be loaded before loading the turnkey.fin file. 128: \ This technique provides a way of speeding the loading of Forth programs 129: \ into controller boards for volume production. 130: \ See the glossary entry of COMPOSE.FORTH.INSTALLER.FOR in the 131: \ Forth glossary for more details. The Segment Management chapter also 132: \ describes how to use this function. 133: 134: 135: DIN 0x188000 XCONSTANT DEFAULT_HEAPSTART 136: DIN 0x1CBFFF XCONSTANT DEFAULT_HEAPEND 137: \ These constants are used by the build task routine 138: \ to explicitly initialize the heap at startup. 139: 140: 141: \ *********************** ATD Data Gathering Task ********************* 142: 143: \ This task gathers data from the Analog To Digital (ATD) converter 144: \ and places it in a variable that is easily accessed by other tasks. 145: \ See the functions listed in the ATD Drivers section of the 146: \ Forth Glossary's Categorized Word List. 147: 148: \ Define the control registers and some useful constants: 149: 150: 0 CONSTANT ATD_INPUT_CHANNEL 151: \ You can use any channel you want and place the input potentiometer on 152: \ the corresponding ATD input pin. For example, if 0 is your input 153: \ channel, place the input signal on the pin labeled AN0 (pin24 of 154: \ the Analog Field Header). 155: 156: 157: FVARIABLE input_voltage 158: \ Holds voltage corresponding to latest result of ATD input. Because the 159: \ contents of this variable are shared among several tasks, we use 160: \ uninterruptable |F!| and |F@| to access it. 161: 162: \ The ATD reading is a 10-bit field left-justified in a 16-bit number. 163: \ As explained in detail in the "Data Acquisition" chapter of the User Guide, 164: \ this ATD value can be treated as a 16-bit value that ranges from 0 to 65535. 165: \ We want to convert this into a voltage that ranges from 0.0 Volts 166: \ (the low reference voltage) to nearly 5.0 Volts (the high reference voltage). 167: \ This involves solving the equation: 168: \ Voltage=[(high.ref.voltage - low.ref.voltage)*measured.count/65536]+low.ref.voltage 169: \ See the "Data Acquisition" chapter in the User Manual for a 170: \ discussion of this equation. 171: 172: 173: \ First let’s define some constants: 174: 65536.0 FCONSTANT FULL_SCALE_COUNTS \ left-justified value is treated as 16 bits 175: 0.0 FCONSTANT LOW_VREF 176: 5.0 FCONSTANT HIGH_VREF 177: HIGH_VREF LOW_VREF F- 178: FCONSTANT HIGH_VREF-LOW_VREF \ pre-calculated difference for fast execution 179: \ NOTE: For maximum accuracy, place a voltmeter between AGND and +5VAN 180: \ (pins 1 and 2, respectively, on the PDQ Analog Field Header) 181: \ to measure +5VAN, and set HIGH_VREF equal to the result. 182: \ Or, if you want to use a signal other than the AGND analog ground 183: \ as your voltage ground, measure both signals with respect to your 184: \ chosen ground, and set the LOW_REF and HIGH_REF constants accordingly. 185: 186: 187: : CountToVolts ( count -- r | r = fp_volts ) 188: \ This routine converts a 16-bit ATD measured count n { 0 <= n <= 65535 } 189: \ to the corresponding floating point voltage r 190: \ {typically, 0.0 <= r <= 5.0 } by solving the equation: 191: \ r = [ (high.ref - low.ref) * count / 65536 ] + low.ref 192: UFLOT ( fp_count -- ) 193: HIGH_VREF-LOW_VREF F* ( count*scalerange -- ) 194: FULL_SCALE_COUNTS F/ LOW_VREF F+ ( fp_volts -- ) 195: ; 196: 197: 198: : GatherData ( -- ) 199: \ This is the activation routine for the data gathering task. 200: \ It continually acquires readings from the ATD, converts the readings 201: \ to voltages, and updates the input_voltage variable that is accessed 202: \ by other tasks. 203: ATD_INPUT_CHANNEL ATD.ON \ turn on ATD containing the input channel 204: BEGIN \ start infinite loop 205: ATD_INPUT_CHANNEL ATD.Single ( result--) \ get 1 sample 206: CountToVolts ( fp_volts--) \ convert to volts 207: input_voltage |F!| ( -- ) \ store voltage 208: PAUSE \ let other tasks run 209: AGAIN 210: ; 211: 212: 213: \ ********************* Pulse Width Modulation (PWM) Task ******************** 214: 215: \ This task oversees the generation of two pulse-width modulated (PWM) outputs: 216: \ 1. One of the outputs is generated by the hardware PWM subsystem on the HCS12 217: \ processor which drives the PORTP pins. This concatenated 16-bit-resolution 218: \ PWM01 output appears at PORTP pin PP1 (pin 15 of the PDQ Digital Field Header). 219: \ See the "PWM" chapter in the User Manual and 220: \ see the functions listed in the PWM Drivers section of the 221: \ Forth Glossary's Categorized Word List. 222: \ 2. The other pulse output is generated by the Enhanced Capture/Timer (ECT) 223: \ subsystem on the HCS12 processor. An Output Compare channel on ECT0 generates 224: \ the pulse train on PORTT pin PT0 (pin 24 of the PDQ Digital Field Header). 225: \ An interrupt service routine responds to the ECT0_ID interrupt to form 226: \ each edge of this signal. 227: \ See the "Timer Controlled I/O" chapter in the User Manual, and 228: \ see the functions listed in the Timer-Controlled I/O Drivers section of the 229: \ Forth Glossary's Categorized Word List. 230: 231: \ This PWM task's activation routine calculates the high time and low time 232: \ of the PWM outputs based on the value of the input_voltage which is updated 233: \ by the data gathering task. A 100 millisecond (ms) period is used 234: \ for both of the PWM outputs; consequently, we want their duty cycles to be equal. 235: \ The goal is to set the duty cycle of each PWM output so that the time-average of 236: \ the PWM signal equals the input_voltage (the analog voltage at the ATD input pin). 237: \ This is achieved by solving the equation: 238: \ DutyCycle = (input_voltage - low_output) / (high_output - low_output) 239: \ in which low_output and high_output are the logic-low and logic-high 240: \ voltage levels that appear on the digital PWM output pins (PP1 and PT0) 241: \ in the low and high states, respectively. 242: 243: 244: \ Given the duty cycle, and given our choice of a PWM period of 100 msec, we 245: \ calculate the high_time and low_time of the PWM signal in terms of the timer 246: \ counts; each timer count equals 1.6 microseconds, which is the default 247: \ ECT timebase set by the operating system. We assume here that the programmer 248: \ has not installed a different value using ECT.Prescaler 249: \ high_time is the number of timer counts that the signal is in the high state 250: \ and low_time is the number of timer counts that the signal is in the low state 251: \ during each 100 msec period. high_time and low_time are calculated as: 252: \ high_time = duty_cycle * TIMER_COUNTS_PER_PERIOD 253: \ low_time = TIMER_COUNTS_PER_PERIOD - high_time 254: \ where TIMER_COUNTS_PER_PERIOD = 100 msec/1.6 microsec = 62,500. 255: 256: \ We also "clamp" high_time and low_time to a minimum value to match the 257: \ PWM resolution to that of the incoming ATD signal (which has 10 bit resolution) 258: \ and to prevent a situation where ECT interrupts are requested so rapidly 259: \ that the processor can’t service one before the next interrupt request occurs. 260: 261: \ The ECT0 (output compare channel 0 of the ECT Enhanced Capture/Timer system) 262: \ interrupt code in the subsequent section uses these calculated values 263: \ to pulse width modulate the PORTT output port pin PT0. 264: 265: \ We define some variables and constants: 266: VARIABLE high_time \ the number of timer counts that PWM output is high 267: VARIABLE low_time \ the number of timer counts that PWM output is low 268: FVARIABLE duty_cycle 269: \ the fractional portion of the full scale voltage represented by the analog input 270: 271: 0.0 FCONSTANT LOW_OUTPUT_VOLTAGE \ = voltage on the PT output when it is low 272: 5.0 FCONSTANT HIGH_OUTPUT_VOLTAGE \ = voltage on the PT output when it is high 273: HIGH_OUTPUT_VOLTAGE LOW_OUTPUT_VOLTAGE F- 274: FCONSTANT HIGH_OUTPUT-LOW_OUTPUT_VOLTAGE \ pre-calculated for fast execution 275: \ NOTE: for maximum accuracy, the voltage outputs of pins PT0 and PP1 could be 276: \ measured in the low and high states and LOW_OUTPUT_VOLTAGE and 277: \ HIGH_OUTPUT_VOLTAGE could be set accordingly. 278: 279: 100 CONSTANT MS_PER_PERIOD \ use a 100 msec period for PWM output 280: \ NOTE: the timer "rolls over" every 104.86 msec 281: 282: 62500 CONSTANT TIMER_COUNTS_PER_PERIOD 283: TIMER_COUNTS_PER_PERIOD UFLOT 284: FCONSTANT FP_TIMER_COUNTS_PER_PERIOD 285: \ 100 milliseconds corresponds to 62500 counts of 1.6 microseconds (usec) 286: \ each on the free-running timer. The operating system sets the default 287: \ TCNT period to 1.6 usec. We assume here that the programmer has not 288: \ installed a different value using ECTPrescaler(). 289: \ For symmetry and simplicity, we also set the clock source period of the 290: \ PWM01 channel to equal 1.6us. 291: 292: 293: 60 CONSTANT MINIMUM_HIGH_OR_LOW_TIME \ under 100usec 294: \ we choose a minimum high or low time approximately equal to the maximum 295: \ number of timer counts divided by 1000. That is, we only require 10 bits of 296: \ resolution from our PWM; after all, this is the resolution of the ATD reading 297: \ that we are attempting to mimic. 298: 299: 300: : CalcDutyCycle ( -- ) 301: \ implements the equation: 302: \ duty_cycle = (input_voltage-low.output)/(high_output-low_output) 303: \ duty_cycle is in the range 0.0 to 1.0 304: input_voltage |F@| ( fp_latest_atd_voltage -- ) 305: LOW_OUTPUT_VOLTAGE F- 306: HIGH_OUTPUT-LOW_OUTPUT_VOLTAGE F/ ( fp_duty_cycle -- ) 307: ONE FMIN ZERO FMAX ( fp_duty_cycle -- ) \ clamp to 0<=duty_cycle<=1.0 308: duty_cycle F! ( -- ) \ save duty cycle in fvariable 309: ; 310: 311: 312: : CalcHighAndLowTimes ( -- ) 313: \ saves high and low times as 16bit integer counts in the variables 314: \ high_time and low_time using the equations: 315: \ high_time = duty_cycle * TIMER_COUNTS_PER_PERIOD 316: \ low_time = TIMER_COUNTS_PER_PERIOD - high_time 317: \ both high_time and low_time are clamped to a minimum so that timer interrupts 318: \ don’t occur more frequently than they can be reliably serviced. 319: duty_cycle F@ FP_TIMER_COUNTS_PER_PERIOD F* UFIXX ( hightime -- ) 320: MINIMUM_HIGH_OR_LOW_TIME UMAX ( estimated_hightime-- ) \ clamp hightime 321: TIMER_COUNTS_PER_PERIOD SWAP - ( lowtime -- ) \ lowtime = period - hightime 322: MINIMUM_HIGH_OR_LOW_TIME UMAX ( lowtime -- ) \ clamp lowtime 323: DUP low_time ! ( lowtime -- ) \ store in low_time variable 324: TIMER_COUNTS_PER_PERIOD SWAP - ( hightime -- ) 325: high_time ! ( -- ) \ store in high_time variable 326: \ ensures that high_time + low_time is exactly one period despite clamping 327: ; 328: 329: 330: \ ************* PWM Output Using HCS12's PWM Subsystem ********* 331: 332: \ PWM01 is a 16-bit concatenated PWM output comprising 8-bit channels 0&1. 333: \ Its output appears on pwm1 pin = PP1 (pin 15 of the PDQ Digital Field Header). 334: \ The driving clock period is chosen to be 1.6us to equal the ECT driving clock. 335: \ PWM01 is declared as an active-high, non-center-aligned output. 336: \ This means that the "duty" value written to the PWM01 channel 337: \ should equal the high_time value calculated above. 338: \ Reasoning: high_time is the 16-bit high time of a PWM signal with 100msec period 339: \ and 1.6us driving clock. 340: \ This is the definition of the "duty" value of an active-high 16-bit PWM signal. 341: \ Thus, we can use the PWM.Duty.Write function to update the duty cycle 342: \ as shown in the SetPulseParameters function below. 343: 344: 32 CONSTANT CLOCKA_1_6USEC 345: \ prescaler for clockA period = 32 * 0.05us Eclk.period = 1.6us 346: 347: 348: : Setup16BitPWM ( -- ) 349: \ initialize PWM01 output on pin PP1 for 1.6us driving clock period, 350: \ 16-bit concatenated channel, starting at 50% duty cycle. 351: PWM.CLOCKA CLOCKA_1_6USEC PWM.Prescaler \ clockA period = 1.6us 352: PWM01 PWM.Unscaled.Clock \ PWM01 uses unscaled clockA @ 1.6us 353: \ now call PWM.Setup for PWM01, the next comment line recaps the prototype: 354: \ ( active_high\scaled_clock\centered\period\duty\channel_id--) PWM.Setup 355: TRUE FALSE FALSE TIMER_COUNTS_PER_PERIOD TIMER_COUNTS_PER_PERIOD 2/ PWM01 356: PWM.Setup ( -- ) 357: PWM01 PWM.Enable ( -- ) \ enable PWM01, starts at 50% duty cycle 358: ; 359: 360: \ ********************* ECT Output Compare Interrupt Code ********************** 361: 362: \ This interrupt routine generates a pulse width modulated output on PortT 363: \ pin PT0 based on the duty cycle calculated by the PWM task. 364: \ We use Forth to define an interrupt handler. 365: \ Unless the "Fast Clear Mode" (available only for ATD and ECT systems), 366: \ the handler must clear the interrupt request flag by writing a 1 to it. 367: \ In this case, we use the fast clear mode. 368: \ We install the interrupt handler using the Attach() function, 369: \ and locally enable the interrupt. 370: 371: 0 CONSTANT PT0_ID \ channel_id for PT0 372: VARIABLE current_state \ used by ISR to track state, initialized by InstallPulseMaker 373: 374: : ECTPulseMaker ( -- ) \ interrupt service routine for output compare 0 375: current_state @ 376: IF \ if the output level just toggled low: 377: OC.SET.ACTION PT0_ID OC.Action \ go high upon next successful compare 378: low_time @ ( lowtime--) \ compare forces the pin high after low_time 379: ELSE \ else if output was low, and just toggled high: 380: OC.CLEAR.ACTION PT0_ID OC.Action \ go low upon next successful compare 381: high_time @ ( hightime--) \ compare forces the pin low after high_time 382: ENDIF ( high_or_low_time -- ) 383: PT0_ID OC.IC.Reg.Read \ this access to TC0 auto-clears the flag bit 384: + PT0_ID OC.Reg.Write ( -- ) \ compare forces the pin after specified time 385: current_state @ NOT current_state ! \ toggle the state variable 386: ; 387: 388: : InstallPulseMaker ( -- ) \ installs PWM output compare on PT0 389: \ NOTE: the calling routine must ensure that interrupts are globally enabled! 390: PT0_ID Output.Compare \ make ECT0/PT0 an output compare 391: TCNT.Read 1- PT0_ID ( TCNT_contents-1\channel_id -- ) 392: OC.Reg.Write \ wait 1 rollover period til start 393: current_state OFF \ declare current state as low, and... 394: OC.SET.ACTION PT0_ID OC.Action \ ...go active high upon successful compare 395: CFA.FOR ECTPulseMaker ECT0.ID ATTACH \ post the interrupt handler 396: ECT.Fast.Clear \ fast clear mode simplifies the ISR 397: PT0_ID ECT.Interrupt.Enable \ enable output compare 0 interrupt 398: ; 399: 400: 401: 402: \ ************************* PWM Task Activation Routine *********************** 403: 404: : SetPulseParameters ( -- ) 405: \ this is the activation roution for the pwm task. It updates the 406: \ values in high_time and low_time for use by the ECT0.ID output compare interrupt 407: \ which generates the pwm output waveform on pin PT0. 408: \ It also updates the PWM01 channel's duty parameter to control the 409: \ output waveform on pin PP1. 410: \ PWM01 is a 16-bit concatenated PWM output comprising 8-bit channels 0&1. 411: \ Its driving clock period is chosen to be 1.6us to equal the ECT driving clock. 412: \ PWM01 is declared as an active-high, non-center-aligned output by Setup16BitPWM(). 413: \ This means that the "duty" value written to the PWM01 channel 414: \ should equal the high_time value calculated above. 415: \ Reasoning: high_time is the 16-bit high time of a PWM signal with 100msec period 416: \ and 1.6us driving clock. 417: \ This is the definition of the "duty" value of an active-high 16-bit PWM signal. 418: \ Thus, we use the PWM.Duty.Write function to update the duty cycle. 419: InstallPulseMaker \ install ECT0 interrupt handler for pin PT0 420: Setup16BitPWM \ install PWM01 on pin PP1 421: BEGIN 422: CalcDutyCycle 423: CalcHighAndLowTimes 424: high_time @ PWM01 PWM.Duty.Write \ write high_time to duty, explained above. 425: PAUSE \ let other tasks run 426: AGAIN 427: ; 428: 429: \ ************************** Statistics Task ************************** 430: 431: \ This task samples the analog input voltage collected by the GatherData task 432: \ once per second, stores it in a matrix, and calculates and prints to the 433: \ terminal the mean and standard deviation of the data every 10 seconds. 434: 435: MATRIX: Last10Voltages 436: \ row matrix; holds 1 voltage per second for past 10 seconds 437: 438: 439: VARIABLE last_index \ keeps track of where data is placed in Last10Voltages 440: 441: \ these hold the derived statistics: 442: FVARIABLE mean 443: FVARIABLE standard_deviation 444: 445: : RowMean ( rownum\matrix_xpfa -- fp_mean ) 446: \ sums the elements in the specified row of the specified matrix and 447: \ divides by the number of elements in the row to calculate the mean. 448: XDUP ?DIM.MATRIX ( rownum\matrix.xpfa\#rows\#cols ) 449: LOCALS{ &cols &rows x&pfa &rownum } 450: ZERO ( fp_accumulator -- ) 451: &cols 0 452: DO &rownum I x&pfa [] F@ F+ ( fp_accumulator -- ) \ add to accumulator 453: LOOP 454: &cols FLOT F/ ( fp_mean -- ) \ mean = accumulator / #cols 455: ; 456: 457: : RowStandardDeviation ( rownum\matrix_xpfa\fp_row_mean -- fp_row_std_dev ) 458: \ subtracts the row mean from each element and sums the squares of all 459: \ resulting differences in the specified row of the specified matrix 460: \ and takes the square root of the sum to calculate the standard deviation. 461: 2SWAP ( rownum\fp_row_mean\matrix_xpfa -- ) 462: XDUP ?DIM.MATRIX ( rownum\fp_row_mean\matrix.xpfa\#rows\#cols ) 463: LOCALS{ &cols &rows x&pfa f&row_mean &rownum } 464: ZERO ( fp_accumulator -- ) 465: &cols 0 466: DO &rownum I x&pfa [] F@ 467: f&row_mean F- ( fp_accum\fp_centered_el -- ) 468: FDUP F* ( fp_accum\fp_centered_el^2 -- ) 469: F+ ( fp_accum -- ) \ add to accumulator 470: LOOP 471: FSQRT ( fp_row_std_dev -- ) 472: ; 473: 474: : InitFPArray ( fp_value\matrix_xpfa -- ) 475: \ stores specified floating point value in each element of the matrix. 476: \ xpfa is a matrix with 2 dimensions and 4 bytes per element (fp contents). 477: \ Note that ZERO.MATRIX does this if the desired fp_value equals zero. 478: \ This routine is presented for reference in cases where other values are desired. 479: XDUP ?DIM.MATRIX ( fp_value\matrix.xpfa\#rows\#cols ) 480: LOCALS{ &cols &rows x&pfa f&value } 481: &rows 0 482: DO &cols 0 483: DO f&value J I x&pfa [] F! 484: LOOP 485: LOOP 486: ; 487: 488: : CalcStats ( -- ) 489: \ This routine calculates mean and standard deviation of the 490: \ Last10Voltages row matrix and stores them in variables. 491: \ The matrix has only one row whose row index = 0. 492: 0 ' Last10Voltages ( rownum\matrix_xpfa -- ) \ row# = 0 493: 3DUP RowMean ( rownum\matrix_xpfa\fp_row_mean -- ) 494: FDUP mean F! ( rownum\matrix_xpfa\fp_row_mean -- ) \ save in variable 495: RowStandardDeviation ( fp_row_std_dev -- ) 496: standard_deviation F! ( -- ) \ save in variable 497: ; 498: 499: \ The message on the terminal screen will look like this: 500: \ Mean = x.xxxx Volts Standard Deviation = x.xxxx Volts 501: 502: : ShowStats ( -- ) 503: \ writes a 1-line summary to terminal. 504: CR \ start a new line 505: ." Mean = " mean F@ F. ." Volts Standard Deviation = " standard_deviation F@ F. 506: ; 507: 508: : LogDataAndShowStats ( -- ) 509: \ increments matrix_index variable every second, 510: \ loads input_voltage fp data collected by ATD task into Last10Voltages array, 511: \ and displays statistics to terminal every 10 seconds 512: READ.ELAPSED.SECONDS ( -- u\ud | #msec\d.#sec ) 513: ROT 2DROP ( -- #sec{ls.word} ) 514: 10 UMOD ( matrix_index -- ) \ set 0 <= matrix_indx <= 9 515: DUP LOCALS{ &matrix_index } ( matrix_index -- ) \ use local for speed 516: last_index @ <> ( second.elapsed? -- ) 517: IF \ if a second has elapsed... 518: input_voltage |F@| ( fp_voltage--) \ use uninterruptable access 519: 0 &matrix_index Last10Voltages F! ( -- ) \ store voltage in matrix 520: &matrix_index DUP last_index ! ( matrix_index-- ) \ update last_index 521: 9 = ( matrix_index=9? -- ) 522: IF \ if 10 seconds have elapsed... 523: CalcStats \ calculate new statistics 524: ShowStats \ update display 525: ENDIF 526: ENDIF 527: ; 528: 529: : Statistics ( -- ) 530: \ this is the activation routine for the statistics task; 531: \ it calculates and displays the mean and standard deviation of the data. 532: \ NOTE: if you want to interactively debug on the serial1 port 533: \ while seeing the statistics printout on the serial2 port, 534: \ comment in the UseSerial2(); statement. The default serial2 baudrate = 57600. 535: \ This task calls PAUSE.ON.KEY. When you type a Carriage Return ("ENTER"), 536: \ PAUSE.ON.KEY calls ABORT to end the program and enter the Forth monitor 537: \ This lets you regain control of the processor. 538: 1 10 ' Last10Voltages DIMMED \ dimension... 539: ' Last10Voltages ZERO.MATRIX \ ... and initialize matrix 540: -1 last_index ! \ initialize last_index 541: 2 LEFT.PLACES ! 3 RIGHT.PLACES ! FILL.FIELD ON \ set fp display format 542: CR ." Starting Statistics task, will print once every 10 seconds..." 543: CR ." Type a carriage return to abort the program" CR 544: BEGIN 545: LogDataAndShowStats \ calculate and display the statistics 546: PAUSE \ let other tasks run 547: PAUSE.ON.KEY \ to abort the program, type a CR at terminal 548: AGAIN 549: ; 550: 551: \ ********************* BUILD TASKS **************************** 552: 553: \ First declare the tasks and allocate their 1K task areas: 554: \ FORTH_TASK (see MTASKER.H) is the default task running QED-Forth; 555: \ this task is automatically built and started upon each reset/restart; 556: \ in the autostart routine FORTH_TASK puts itself ASLEEP so the end 557: \ user can’t run Forth. 558: 559: ALLOCATE.TASK: ReadInputTask \ data gathering task base xaddr 560: 561: ALLOCATE.TASK: ControlOutputTask \ PWM task base xaddr 562: 563: ALLOCATE.TASK: StatisticsTask \ statistics reporting task 564: 565: : BuildTasks ( -- ) 566: \ Empties the round robin task loop and then 567: \ carefully builds the tasks every time we start up. 568: \ Note that only the statistics task has access to the heap. 569: (STATUS) NEXT.TASK ! \ must be done! this effectively kills 570: \ other tasks by emptying the round robin task loop. 571: RELEASE.ALWAYS SERIAL.ACCESS ! \ avoid serial port contention 572: 0\0 0\0 0\0 ReadInputTask BUILD.STANDARD.TASK 573: 0\0 0\0 0\0 ControlOutputTask BUILD.STANDARD.TASK 574: DEFAULT_HEAPSTART DEFAULT_HEAPEND 0\0 StatisticsTask BUILD.STANDARD.TASK 575: ; 576: 577: 578: : ActivateTasks ( -- ) 579: \ associate activation routines with each of the tasks. 580: CFA.FOR GatherData ReadInputTask ACTIVATE 581: CFA.FOR SetPulseParameters ControlOutputTask ACTIVATE 582: CFA.FOR Statistics StatisticsTask ACTIVATE 583: ; 584: 585: 586: \ ********************* SET UP AUTOSTART ROUTINE ********************* 587: 588: \ We’ll designate the top level word main as the PRIORITY.AUTOSTART: routine. 589: \ Every time the PDQ Board is powered up or reset, the main routine will 590: \ automatically be executed. 591: \ main() zeros the variable area and initializes the heap. 592: \ During debugging, you can comment out the ASLEEP command in main and comment in 593: \ the UseSerial2() command in Statistics to allow Serial1 to be used for 594: \ debugging via the terminal. The FORTH task which runs main() has access to 595: \ all defined function names. 596: \ DoTurnkey initializes the variables and elapsed time clock, 597: \ and builds and activates the tasks. 598: \ It releases control of the serial line, starts the timeslicer, and PAUSEs 599: \ to begin execution of the application. 600: \ After debugging is complete, the optional command which 601: \ specifies a COLD restart can be inserted; 602: \ this command is "commented out" in the code shown here. 603: 604: 605: : InitVariables ( -- ) 606: \ init static variables at runtime, and 607: \ it's a good idea to erase the parameter field struct of a Forth array at startup. 608: ZERO input_voltage F! 609: current_state OFF 610: high_time OFF low_time OFF 611: ZERO duty_cycle F! 612: ' Last10Voltages ARRAY.PF MATRIX.PF ERASE \ erase the matrix parameter field 613: last_index OFF 614: ZERO mean F! ZERO standard_deviation F! 615: ; 616: 617: 618: : DoTurnkey ( -- ) 619: \ this is the highest level routine in the turnkeyed application. 620: InitVariables \ init variables, delete arrays 621: DEFAULT_HEAPSTART DEFAULT_HEAPEND 622: IS.HEAP \ it’s important to init heap at startup 623: INIT.ELAPSED.TIME \ initialize qed elapsed time clock 624: BuildTasks \ initialize user areas of the tasks 625: ActivateTasks \ associate action routine with each task 626: \ the following command is removed during debugging;present in final version 627: \ COLD.ON.RESET \ ensures full initialization upon each reset 628: \ the following ASLEEP command can be removed during debugging 629: \ if the UseSerial2() command is inserted in the Statistics() function: 630: ASLEEP STATUS ! \ puts forth task asleep so statistics can print 631: SERIAL1.RESOURCE RELEASE \ in case another tasks needs to use serial 632: START.TIMESLICER \ starts elapsed time clock, enables interrupts 633: PAUSE \ start next task immediately 634: ; 635: 636: 637: END.SEGMENT \ end the TURNKEY_APP segment 638: ( this delays while flash is written. should print -1 for success: ) 639: SAVE.ALL . 640: \ this automatically backs up the code image to the shadow flash, 641: \ and causes it to be automatically reloaded upon each COLD restart. 642: 643: \ You can optionally write protect the code by typing WP.ALL at the terminal: 644: \ WP.ALL \ comment this in to write protect all pages. 645: \ to undo, type WE.ALL to write-enable all pages 646: 647: 648: \ ************************************* 649: 650: \ To compile this file, simply use the Mosaic Terminal 651: \ to download this source code file into the controller. 652: \ Note that the download file contains the 653: \ SAVE.ALL 654: \ directive, which automatically backs up the code image to the shadow flash, 655: \ and causes it to be automatically reloaded upon each COLD restart. 656: \ You can optionally type at the terminal: 657: \ WP.ALL 658: \ to write protect all the protectable pages; to undo this directive, type 659: \ WE.ALL 660: \ to write enable all of the pages. The User Guide document explains it all. 661: 662: \ Now from your terminal, type: 663: \ PRIORITY.AUTOSTART: DoTurnkey 664: 665: \ this will install main() as the routine that is automatically executed 666: \ each time the board is reset or powered on. 667: \ The PRIORITY.AUTOSTART: routine initializes the priority.autostart vector 668: \ at the top of page 0x0F shadow flash AND page 0x0F SRAM. 669: 670: \ Then upon the next restart the DoTurnkey routine will automatically execute. 671: \ NOTE: To erase the autostart vector and return to the QED-Forth prompt, 672: \ type a CR ("ENTER") to abort the program, then type at the terminal: 673: \ NO.AUTOSTART 674: \ If you have put the FORTH_TASK asleep so that it does not 675: \ respond to the terminal, 676: \ activate the Special Cleanup Mode by installing the jumper 677: \ labeled "Clean" near the reset button, and pressing the reset button. 678: \ The Special Cleanup Mode erases the autostart vector and restores the 679: \ board to a "pristine" condition (it also sets the default 680: \ baud rate to 57600, default serial = serial1 port; 681: \ invoke BAUD and/or USE.SERIAL2 if you need to change the defaults). 682: \ 683: 684: 685: \ ************** GOING INTO PRODUCTION *************** 686: 687: \ To load a pristine board with the application, simply 688: \ download the this file, and execute the 689: \ PRIORITY.AUTOSTART: DoTurnkey 690: \ command to install the autostart vector. 691: \ Power cycle the Board and it will automatically run the application! 692: \ 693: \ To speed up the loading of programs into boards that are being produced in 694: \ volume, you can create a pre-compiled version of the application program 695: \ that can be more quickly downloaded into production systems. By executing 696: \ IN.PLACE FALSE COMPOSE.FORTH.INSTALLER.FOR TURNKEY_APP " turnkey.fin" 697: \ after initially compiling this program, 698: \ the board will dump back to the terminal a Forth INstaller file 699: \ named turnkey.fin. When this file is later downloaded to a pristine 700: \ board, the application is installed. 701: \ Of course, if there are any required device driver libraries 702: \ (such as the LIBNAME example described earlier in this file), their *.fin files 703: \ should be loaded before loading the turnkey.fin file. 704: \ This technique provides a way of speeding the loading of Forth programs 705: \ into controller boards for volume production. 706: \ See the glossary entry of COMPOSE.FORTH.INSTALLER.FOR in the 707: \ Forth glossary for more details. The Segment Management chapter also 708: \ describes how to use this function.
Simple Forth example application showing memory management
1: 1 WRITE.ENABLE \ Needed to assure that we can write to the code area on page 0x00 2: 2 WRITE.ENABLE \ Needed to assure that we can write to the names area on page 0x10 3: DEFAULT.MAP \ Places the dictionary (code) and names on pages 00 and 10 respectively 4: 5: ANEW ExampleProgram \ an ANEW marker for easy forgetting during development 6: 7: \ Define variables and constants here. 8: 9: \ If you use lookup tables, you can use constant matrices to contain them, like this: 10: 8 1 DIM.CONSTANT.MATRIX: mLOOKUP 11: \ Yoiu can populate the lookup table at compile time like this: 12: MATRIX mLOOKUP = 0.5 0.5 0.5 0.2 0.0 0.0 0.0 0.0 13: \ or you can populate it programatically, like this: 14: : Init_mLOOKUP ( -- ) 15: \ ... 16: ; 17: Init_mLOOKUP \ Compile time initialization of the mLOOKUP table 18: 19: \ If you need an on-the-fly data structure you can use a matrix like this: 20: MATRIX: DATA 21: \ Be sure to recover its heap space whenever the program is reloaded, like this: 22: unique.msg @ unique.msg OFF 23: : ON.FORGET ( -- ) 24: ' DATA DELETED 25: ; 26: unique.msg ! 27: 28: \ You place your code for your application here: 29: \ ... 30: \ ... 31: \ ... 32: \ ... 33: \ ... 34: \ ... 35: \ 36: 37: : FinalWord 38: \ First, initialize variables. 39: \ ... 40: ; 41: 42: : AutoApplication \ This is the program that is AUTOSTARTed 43: RESTORE \ When autostarted, optionally restore the pointers, in particular the names 44: \ pointer so that after this application is finished executing the user can call other 45: \ words or set variable values. 46: \ Initialize particular variables here that must be initialized for the first running 47: \ of FinalWord, allowing the user to change variable values for subsequent runnings. 48: FinalWord \ The word that implements the application. 49: ; 50: 51: 52: \ Assuming our code went onto page 0x00 and the names went onto page 0x10 we store them 53: \ into flash and retrieve them on startup with the following directives: 54: 0 1 STORE.PAGES DROP \ store code page 0x00 in shadow flash 55: 0x10 1 STORE.PAGES DROP \ store names page 0x10 in shadow flash 56: 0x00 1 1 LOAD.PAGES.AT.STARTUP \ load code page 0x00 to RAM at startup 57: 0x10 1 2 LOAD.PAGES.AT.STARTUP \ load names page 0x10 to RAM at startup 58: SAVE \ Save pointers so that they can be restored later. 59: \ Note that we could have used SAVE.ALL to accomplish the above. It stores pages 0x00 60: \ through 0x17, taking 11 seconds to do so; it sets the same pages to load at starup; 61: \ and it executes SAVE. We do the above because it saves time. 62: 63: 1 WRITE.PROTECT \ protect pages 0-0xF 64: 2 WRITE.PROTECT \ protect pages 0x10-0x13 65: 66: AUTOSTART: AutoApplication \ Autostarts the application on COLD or powerup.
See also → A Turnkeyed C Application Program
For Experts: Compilation and Segment Management