Chapter 17 - Putting It All Together: A Turnkeyed Application Program
This chapter presents an application program that integrates a range of hardware and software features of the QED Board. The application is "turnkeyed", meaning that it can be burned into PROM and configured to autostart each time the board is powered up. This chapter explains all of the elements of the program. The program listing is presented at the end of this chapter.
This program can run on a QED Board with a minimum memory configuration comprising the 64K QED Development ROM in socket#1, a 32K RAM in socket#2, and a 32K PROM in socket#3. Each kilobyte (K) represents 1024 bytes of memory. Of course, the additional battery-backed RAM available in the developer package or Product Design Kit makes programming and debugging easier. The code assumes that a 4 line by 20 character liquid crystal display is connected to the board. The 12 bit analog to digital converter and 8 bit digital to analog converter options are not required.
The example application reads a voltage using the 8 bit analog to digital (A/D) converter and outputs a pulse width modulated (PWM) signal that, when averaged using a simple resistor and capacitor, tracks the input voltage. In addition, the program calculates and displays the mean and standard deviation of the input signal. This application demonstrates how to:
- Set up the memory map to be compatible with a PROMmed application
- Use the 8 bit A/D converter
- Use floating point mathematics
- Write and install an interrupt routine that generates a PWM output
- Assembly code an interrupt routine for increased performance
- Use the matrix math toolkit to calculate means and standard deviations
- Write to the liquid crystal display (LCD)
- Write the application using a modular multitasking approach
- Configure the application to automatically start upon power-up or reset
The 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 task gathers data from channel 0 (or any other channel that you designate) of the 8 bit A/D converter (Port E on the 68HC11), converts the 8 bit reading to its equivalent voltage, and saves it in a floating point variable called INPUT.VOLTAGE.
- The second task calculates the duty cycle, high-time and low-time of a pulse width modulated (PWM) output signal so that its average value matches the latest INPUT.VOLTAGE measured by Task 1. An interrupt attached to output compare 3 (OC3) controls the PWM output signal appearing at pin 5 of PORTA (PA5) of the 68HC11. The interrupt service routine updates the OC3 timer registers based on the high-time and low-time calculated by Task 2.
- Once each second for ten seconds, the third task samples the input voltage calculated by Task 1 and stores it in a matrix called LAST.10.VOLTAGES. When 10 seconds have elapsed, this task calculates the mean and standard deviation of the 10 data points and writes them to the LCD display. It then starts filling the matrix with new data, repeating the entire process.
There is one additional task that is always present in this application: the default task that runs QED-Forth. In the final ROMmed 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. Of course, the QED-Forth task is important during development and debugging; it allows us to execute commands and monitor the performance of the routines that are being tested. It may also be very handy for a service person to awaken and regain access to the QED-Forth task in the field to perform diagnostics or to upgrade the instrument. For this reason, the code includes a "back door" interrupt whose only action is to awaken the Forth task. This interrupt is activated by the active-low IRQ input pin on the I/O and Control bus connector.
Hardware Required for the Example Application
This sample application requires a QED Board, a 4 line by 20 character liquid crystal display with cable (available from Mosaic Industries), a resistor, capacitor, potentiometer, and a voltmeter to verify proper operation. An optional operational amplifier would allow the output signal to drive loads other than a high impedance voltmeter or oscilloscope. Please be careful not to short pins together when working with the on-board connectors.
Figure 17.1 shows a functional schematic of the circuitry required, and points out the relevant pin numbers on the 40 pin digital and the 40 pin analog I/O connectors. A 10 Kohm (or any other reasonably valued) potentiometer is connected to place a controllable voltage on AN0, which is channel 0 of the 8 bit A/D converter on the HC11. The potentiometer is connected between +5VAN (the analog +5 Volt supply) and AGND (analog ground), with the potentiometer's wiper connected to AN0 (pin 10 of the analog I/O connector).
Figure 17.1. Functional schematic of circuitry needed to verify proper operation of the example turnkeyed application program.
The PWM output appears on PA5 (that is, pin 5 of PORTA). A resistor and capacitor are connected as shown in Figure 17.1 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. The optional amplifier shown in the figure is needed if the output must drive lower impedance loads.
The statistical report generated by Task 3 is sent to the 4 line by 20 character LCD display. The display interface and device drivers are on the QED Board, and a simple cable directly connects the display to the QED Board. The "Getting Started With the QED Board" booklet describes how to connect the display.
Design the Memory Map
The Four Main Memory Areas
The first step in programming an application is assigning the memory areas that will be occupied by the definitions area, name area, variable area, and heap. See Chapter 3 for a complete discussion of this topic; this section provides a brief summary.
The definitions area contains the "object code" of the application, including the definitions of all functions as well as the values assigned to constants. The definitions area must be in PROM (or some type of write-protected nonvolatile memory) for the application to run properly.
The name area contains the linked list of assigned names for the definitions in the application. The name area must be present during development and debugging. If names are required in the final application (for example, if you want to give the end user the ability to execute commands from a terminal), then the name area should be included in PROM or nonvolatile memory in the final turnkeyed system. The name area can be left out of the final turnkeyed application if the end user need not execute Forth commands to use the final turnkeyed instrument. Removing the names saves PROM space in the production system, and can improve the "security" of the code by preventing a would-be pirate from benefiting from informative names while trying to decipher the code.
While the definitions and name areas should end up as PROM in the final system, the variable area and heap must always be located in RAM. The variable area includes the areas where variable values are stored, and includes the parameter fields that hold addressing and dimensioning information associated with heap items. The heap holds arrays, matrices, and other data structures. Both variables and the contents of the heap must be subject to change while the application is running, so they can never be ROMmed.
Other Memory Areas
Simple non-multitasking applications can typically leave the data and return stacks, user area, and buffers such as the PAD (scratchpad), TIB (terminal input buffer) and POCKET (used by the interpreter) in their default locations in common RAM. Appendix A details the default positions of these areas.
Multitasking applications must allocate memory for task areas. Each task typically requires a 1K task area that includes the task's stacks, user area, PAD, TIB, and POCKET. You can use the BUILD.STANDARD.TASK routine to set up a task area for each task in common RAM. The 6K of common RAM starting at 9600H and labeled as "6K available RAM" in Appendix A is a convenient place to allocate task areas for up to 6 standard tasks.
While compiling an application, all four main memory areas (definitions, names, variables, and heap) are located in RAM. During debugging, the definitions and name areas may be optionally write-protected. As described in Chapter 3, proper use of write-protection can save you from having to re-download all of your code after a crash or mistaken command.
Available PROM and RAM on the QED Board
The QED Board has 3 memory sockets labeled S1, S2, and S3. Each can accommodate a memory device ranging from 32 Kbytes to 128 Kbytes. Appendix A of the Software Manual specifies the memory map in detail, and a brief summary is presented here.
Socket S1 accommodates the QED-Forth ROM which holds the development system as well as the runtime libraries. This ROM must be present for the QED Board to function.
Socket S2 holds a 32K or 128K RAM, and at least 32K of RAM must be present in this socket for the QED Board to operate. This memory cannot be write-protected. If a 32K RAM is installed, the onboard logic places 20K of it on page fifteen (0FH) at addresses 3000H to 7FFFH and the remainder is common RAM. Some common RAM is reserved by QED-Forth, and some is overmapped by the 68HC11's EEPROM. 11.5K of common RAM remains available to the programmer at addresses 8400H-ADFFH and B000H-B3FFH. If a 128K RAM is installed in socket S2, the three 32K pages numbered 1, 2, and 3 are also available.
Socket S3 is called the "RAM/ROM socket". It accommodates either RAM (typically during program development) or PROM (typically in your final product). During program development and debugging, a 128K battery-backed RAM is typically installed in this socket. The memory is addressed at pages 4, 5, 6, and 7, and each page contains 32 Kbytes. Turning DIP switch #1 ON write-protects pages 4 and 5, and turning DIP switch #2 ON write-protects pages 6 and 7. This socket is an ideal location for the memory areas such as the definitions and name areas that will end up as PROM in the final system. During program development, code can be compiled into the RAM in this socket, and the memory can be write-protected to emulate a PROM. When the application is completely debugged, the contents can be burned into a PROM, and the PROM can be plugged into the socket. Turning dip switch #3 ON configures the socket to accommodate a PROM. The PROM size can be 32K (page 4 only), 64K (pages 4 and 5), or 128K (pages 4, 5, 6, and 7).
Setting Up the Memory Map
To set up a memory map tailored to a specific application, the following factors should be considered:
- The amount of memory required for each of the four main memory areas (definitions, names, variables, and heap).
- Whether or not the name area must be present in the final application.
- Memory requirements for task areas in common RAM.
- The amount and location of available PROM on the final production board which must accommodate the definitions and name areas (if present).
- The amount and location of available RAM on the final production board which must accommodate the variable area, heap area, task areas, stacks and buffers.
For our short example application, a 20K definitions area and 12K name area in PROM and a 4K variable area and 16K heap in RAM are more than adequate. These can be accommodated by the minimum onboard memory configuration which is the QED-Forth ROM in socket S1, a 32K RAM in S2, and a 32K PROM in S3. We set up the memory map so that the name area can be present in the final system. There are three tasks in this application, and the three task areas are allocated in the available common RAM starting at address 9600H. The default task's user area and stacks (that is, the user area and stacks that are used by QED-Forth during program development) and the PAD, POCKET, and TIB buffers are left in their default positions in common RAM (see Appendix A for details).
In the application code we set the 20K definitions area to start at the beginning of page 4 (denoted as address 0000H\4) by storing to the definitions pointer DP. We set the 12K name area to start at address 5000H\4 by setting the name pointer NP. Both of these reside on page 4 which will be PROM in the final system, as desired.
The variable area is initialized to start at 8E00H\0H by storing to the variable pointer VP. This area resides in common RAM. The heap is initialized with the IS.HEAP command to occupy 16K starting at 4000H\FH. This area resides in page 0FH which is always RAM. We also define some constants specifying the start of the variable area and the start and end of the heap; these will be used by the startup routine to zero the variable area and initialize the heap.
It turns out that the command
4 USE.PAGE
sets up the exact same memory map that we have chosen here. In the sample application we spell out the individual commands so that it will be easier for the reader to generalize to other memory map configurations.
Notice that we execute the ANEW command after we have set DP, NP, and VP but before we have defined the first entry in the dictionary. The ANEW command is a handy dictionary management device that automatically FORGETs prior versions of the application code when a new version is sent over to the QED Board. It is explained in detail in Chapter 3.
We set WIDTH to 8 (up from its default value of 4). This instructs QED-Forth to save the first 8 letters of each routine's name in the name area, thus reducing the likelihood of non-unique names.
A/D Data Gathering Task
The next section of the example application code uses the 8 bit analog to digital (A/D) converter to convert the voltage input derived from the external potentiometer (see Figure 17.1) to a digital value between 0 and 255. The task converts the 8 bit A/D reading into its equivalent floating point voltage which spans 0 to 5 Volts, and stores it in a variable called INPUT.VOLTAGE.
We define some simple constants that specify the high and low reference voltages of the A/D, and the number of counts (28 = 256 counts). The routine → VOLTAGE converts the 8 bit A/D reading into its equivalent floating point voltage (a number between 0 and 5 Volts). GATHER.DATA is the activation word for this task. It calls A/D8.ON to power up the 8 bit A/D, and enters an infinite loop that acquires an A/D sample, converts it to a voltage, and stores it in the variable INPUT.VOLTAGE which other tasks can read. An uninterruptable |F!| store operator is used because this variable holds data that is accessed by more than one task. The uninterruptable operators ensure that 32 bit data is not misread because of intervening interrupts or task switches. Consult the "Multitasking" Chapter for a discussion of uninterruptable memory operators.
PAUSE task-switch commands are not included in the infinite loop of GATHER.DATA; we rely only on the timeslicer to switch tasks. If we wanted to reduce the proportion of time that the processor spent servicing this task, we could place a PAUSE command in the loop.
Pulse Width Modulation Task
The goal is to create a PWM output whose average voltage is equal to the input voltage read by the data gathering task. This code specifies the activity of the task that calculates the high-time and low-time parameters needed to generate the pulse width modulated output signal. An interrupt routine (described in the next section) controls the output signal subject to the high- and low-times calculated by this task.
We could perform all of the duty cycle computations in the interrupt service routine that controls the PWM output, but this is not a good practice. Long interrupt service routines can delay the processor's ability to respond to other interrupts in a timely manner. The best approach is to perform the more time-consuming computational functions in "foreground" tasks so that the associated "background" interrupt service routines execute very rapidly.
We define 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 are defined to specify the voltage levels corresponding to logic level 0 and logic level 1; for maximum accuracy you could measure the voltage levels on PA5 (pin 5 of PORTA on the 68HC11) and set these constants accordingly.
The period of the PWM output is chosen to be 130 msec, which corresponds to 65,000 counts on the free-running timer. The QED-Forth operating system automatically configures the free-running counter to increment every 2 microseconds (see "The 16-Bit Free-Running Counter" section in the "Programmable Timer" chapter of the QED Hardware Manual).
To reduce the chance of "missing" an interrupt, the minimum time between interrupts should be greater than the time required to service the interrupt. The high level interrupt service routine explained in the next section executes in about 400 microseconds (µsec), so we specify a minimum time between interrupts as 500 µsec. The constant MINIMUM.HIGH.OR.LOW.TIME is used to enforce this 250 count minimum. The minimum count introduces an error in the average of the PWM signal when the output voltage is nearly 0 or nearly 5 Volts. The maximum magnitude of the error is 500 µsec out of 130 msec, which is one part in 260. This very closely approximates the 8 bit resolution (1 part out of 256) with which we measure the input voltage, so we have not unduly compromised the system's performance by imposing this constraint.
We next define a routine that calculates the PWM duty cycle such that the average output signal matches the latest measured input voltage. DUTY.CYCLE→ HIGH&LOW.TIMES converts the calculated duty cycle into the parameters needed by the interrupt service routine, and SET.PWM.PARAMETERS is the infinite loop that serves as the activation word for the PWM task. Note once again that we have not put PAUSE in the loop because we have decided to rely solely on timesliced multitasking in this simple application.
Output Compare 3 Interrupt Code
This code defines an interrupt service routine and an installation routine for the OC3 interrupt which controls the output signal. We first define names for all of the relevant registers, and constants to name the relevant bit masks (For another example that uses the OC3 interrupt, consult the section titled "A One-Shot Output Pulse Generator" in the "Programmable Timer" chapter of the QED Hardware Manual).
OC3.SERVICE is the interrupt service routine that controls the state of the PA5 output bit. It relies on the ability of the output compare (OC) function to automatically change the state of an associated PORTA output bit at a specified time. Specifically, OC3 can be configured to automatically change the state of the output PA5 when the count in the TOC3 register matches the contents of the free-running counter (TCNT) register. Setting the "mode bit" specified by the constant OC3.MODE.MASK in the timer control 1 (TCTL1) register enables the automatic output control function. The OC3 "level bit" specifies whether the output bit will be set high or low upon a successful compare. The OC3.SERVICE routine simply reverses the state of the OC3 level bit (specified by the constant OC3.LEVEL.MASK) and adds the appropriate HIGH.TIME or LOW.TIME increment to the OC3 register to specify when the next interrupt will occur. Recall that HIGH.TIME and LOW.TIME are calculated by the foreground PWM task, so the interrupt has very little to do and can execute rapidly. The high level Forth version of the service routine executes in about 400 µsec.
FASTER.OC3.SERVICE is an assembly coded version of the interrupt service routine. It executes in less than one quarter of the time required by the high level version. The assembly commands can very quickly access addresses in the common memory that do not require a page change. Note that fetches from LOW.TIME and HIGH.TIME still rely on CALLs to high level Forth commands because page changes are involved. To further optimize the speed of this routine, we could use a slightly different memory map to place the variable area in common memory. Then LOW.TIME and HIGH.TIME could be accessed by fast assembly commands.
INSTALL.OC3 enables direct hardware control of PA5 by setting the OC3 mode bit, ATTACHes OC3.SERVICE or FASTER.OC3.SERVICE to the OC3 interrupt, initializes PA5 by forcing an output compare, and enables the OC3 interrupt.
Note that we have simply defined the service and installation routines; we have not yet executed them. Thus the interrupt is not yet active. The autostart routine defined at the end of the code example executes the initialization routine each time the processor restarts.
Statistics Task
The code in this section continuously loads a matrix with one input voltage acquired in each of the last 10 seconds, and writes the mean and standard deviation of this data to the LCD display every 10 seconds. We define a row matrix called LAST.10.VOLTAGES to hold the data; this matrix resides in the heap associated with the statistics task. Two self-fetching variables keep track of the current and prior matrix indices; these aid in managing storage of data into the matrix.
SETUP.DISPLAY writes the headings to the LCD display. The $>DISPLAY routine makes it easy to write to a portion of the DISPLAY.BUFFER in system RAM, and the UPDATE.DISPLAY command writes the contents of the buffer to the LCD display. CALC.STATS calculates the latest calculated mean and standard deviation values, and SHOW.STATS writes them to the display. SHOW.STATS uses F>FIXED$ to convert the floating point numbers on the stack to ascii strings that can be written to the display buffer. The FIXED format is used so that each floating point number will occupy the same number of spaces each time it is displayed. LOG.DATA&SHOW.STATS fills the LAST.10.VOLTAGES matrix 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 matrix, sets the floating point format to fix the width of displayed numbers, and enters an infinite loop that logs the data and displays the statistics. PAUSE is not included in the infinite loop because we rely on the timeslicer to perform all task switching.
Build and Activate the Tasks
Now that we have defined the activation routines for the tasks, it is time to allocate the 1K task areas in common RAM and set up the tasks. The three tasks are named and assigned a task area with the TASK: statements. Each standard task requires one kilobyte of common RAM. Consulting the memory map in Appendix A of the Software Manual, we find that 6K of memory from 9600H through ADFFH is available. We use a total of 3 kilobytes starting at 9600H for the three task areas.
The routine BUILD.TASKS initializes the user area and return 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 QED-Forth task) the only task in the round-robin loop. This sets a known startup condition from which the task loop may be constructed.
Note that the input and PWM tasks are built with null specifications of heap and variable areas because they do not access any heap items and do not define any new variables during operation. The statistics task, however, does access a heap item. It requires a heap specified by BOTTOM.OF.HEAP and TOP.OF.HEAP which are defined at the beginning of the application code. Note that we also use the same heap specification for the default QED-Forth task so that we can examine the contents of the statistics task's data matrix while debugging. The QED-Forth task is not active in the final application, so the sharing of this heap space does cause a conflict during operation.
ACTIVATE.TASKS 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.
Set Up the "Back Door"
The designer of any application must decide to what extent the end user will have access to the QED-Forth language and compiler. At one extreme, the application can be set up so that QED-Forth is not running in the final application. This can be accomplished by putting the QED-Forth task ASLEEP or by using the default task to run a non-Forth task. In this case the name area need not be present in the final application; this increases the security of the application and makes reverse engineering of the code more difficult. At the other extreme, the end user can be granted complete access via the serial line to all QED-Forth functions and to the application vocabulary. In between these extremes there are a range of possibilities. The QED-Forth task can be kept ASLEEP, with a "back door" awakening capability known only by authorized personnel such as field servicemen. Another possibility is that a limited "sealed" application vocabulary is present in the final application. This allows the user to execute a controlled set of functions related to the application, but prevents the user from running the full set of QED-Forth functions.
In this application we choose to keep the name area intact and available in the application PROM, but we keep the QED-Forth task ASLEEP. We also define a "back door" capability that allows field service personnel to gain access to the QED-Forth development environment. The back door is activated by momentarily grounding the IRQ input pin (pin 20 on the I/O and Control Bus) which calls the IRQ service routine. The BACKDOOR routine simply stores the constant AWAKE into the STATUS user variable of the default task so that QED-Forth can be accessed via the serial port. Any reset or restart will call the autostart routine which returns QED-Forth to its sleeping status.
Define the Autostart Routine
ZERO.VARIABLE.AREA erases the variables, self-fetching variables, and parameter fields used by the application. It is good programming practice to initialize these each time the application starts up.
The word START.UP is the top level routine in the application. After execution of the PRIORITY.AUTOSTART command, it will be called each time the board is powered up or reset. The operating system always wakes up and enters the default task whose user area starts at 8400H\0 in common memory upon every restart, so the default QED-Forth task is always the task that executes the START.UP routine. START.UP first clears the variable area. It initializes the VFORTH user variable to point to the header for START.UP. This initialization ensures that if the default QED-Forth task is awakened, the programmer will have access to all of the defined names in the application up to and including START.UP. The routine then initializes the heap of the default QED-Forth task, initializes the elapsed time clock to zero, installs and initializes the OC3 (PWM) and IRQ (back door) interrupt service routines, and builds and activates the tasks.
The next command in START.UP 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 the system to FORGET all of the defined words in the application.
Note that the COLD restart at each startup sets up the default memory map in common RAM (as detailed in the memory map in Appendix A). This places the definitions and name areas of the default task in common RAM which makes it possible to define new routines from the QED-Forth task. START.UP initializes the heap to coincide with the heap of the statistics task. Thus if the back door is used to wake up the QED-Forth task in the final system, the field service person could examine the contents of the statistics heap and even define some new routines to diagnose any problems that may have arisen.
START.UP then puts the default QED-Forth task ASLEEP by executing
ASLEEP STATUS !
This takes effect once multitasking commences, and does not prevent execution of the remainder of START.UP. This command should be commented out during program development so that the awake QED-Forth task can be used to aid in debugging the START.UP routine. It may also be commented out if the programmer wants QED-Forth to remain awake and available while the final application is running.
The START.UP routine then RELEASEs QED-Forth's control of the serial line. This is not required in this simple application, but it is necessary if another task requires access to the serial port in the final application. For example, the RELEASE statement would be required if the statistics task printed to the terminal instead of to the LCD display.
The final commands start the timeslicer (which also starts the elapsed time clock and globally enables interrupts) and PAUSE to immediately transfer control to the next task in the round-robin loop. The final PAUSE is not essential in this simple application, but it does ensure smooth operation in applications where tasks other than QED-Forth require access to the serial port.
We execute SAVE which stores relevant memory map pointers into reserved locations in EEPROM. Executing RESTORE brings back access to all of the defined words even if the processor has crashed because of a programming bug. If page 4 is write-protected after downloading the application code, then a bug-induced crash cannot corrupt the definitions or name areas. The use of write-protected code and the RESTORE feature can greatly reduce the number of times that you have to re-download your code during debugging.
The final command is
CFA.FOR START.UP PRIORITY.AUTOSTART
which installs the top-level word START.UP as a routine that is automatically executed upon each restart or reset. Note that the "QED-Forth V2.0" greeting is suppressed when an autostart routine is installed (you could easily print your own greeting by modifying the START.UP routine). PRIORITY.AUTOSTART installs an autostart pattern in the top 6 bytes of page 4 which is in PROM in the final system. The autostart pattern tells the operating system to automatically call the START.UP routine. Thus simply plugging an appropriately programmed PROM into socket S3 configures any QED Board to run this application upon every reset or restart.
The PRIORITY.AUTOSTART function is required for systems that will be PROMmed and go into production. For one-of-a-kind prototypes, another function (called simply AUTOSTART) is available that installs the autostart pattern in EEPROM which resides in the 68HC11 chip itself. Because the pattern installed by AUTOSTART is in the processor chip and not in PROM, it is not automatically transferred to a new board when the application PROM is plugged in. In summary, the AUTOSTART function is convenient while debugging a prototype, but the PRIORITY.AUTOSTART function must be used when generating a PROM for a production system.
After executing the PRIORITY.AUTOSTART command, the START.UP routine can be invoked by resetting the QED Board, thus entering the application. If you need to remove the autostart vector, un-write-protect page 4 by turning DIP switch #1 OFF, and enter the special cleanup mode by turning DIP switch #5 ON and resetting the QED Board. Then return DIP switch #5 to its normal OFF position and you can continue debugging.
You can monitor the operation of the application by connecting a voltmeter or oscilloscope across the output capacitor C1 as shown in Figure 17.1, and by watching the update of statistics every 10 seconds on your LCD display. Adjusting the input potentiometer should result in an output voltage that tracks the input.
Generating a PROM for the Target System
Now that the application code has been debugged and turned into a completed "turnkeyed" application, generating your production system involves programming a single PROM.
PROM burners accept the "image" that is to be burned into the PROM in either Intel Hex or Motorola Hex (S1 or S2) formats. These formats allow the PROM contents to be saved and transferred as an ascii text file with numbers represented in hexadecimal base. Each line of a hex file contains numbers specifying the address and contents of a specified number of bytes (typically 32 per line).
The QED Board generates these ascii hex files using the built-in routines DUMP.INTEL, DUMP.S1, and DUMP.S2. The popular Intel Hex format uses 16 bit addresses so a single file can be used to program PROMs up to 64 Kbytes long. The Motorola Hex format (also called "S-record format") is also widely supported and is more flexible. The Motorola S1 format uses 16 bit addresses. The S2 format uses 24 bit addresses, and so can be used to program larger PROMs.
For example, to create an Intel Hex format file representing all of page 4, set your terminal program to capture incoming text into a file, and execute
HEX
0000 04 0000 8000 DUMP.INTEL
where 0000\04 is the starting address on the QED Board, 0000 is the 16 bit starting address in the PROM, and 8000H is the number of bytes in the 32 kilobyte page. In response to this command QED-Forth outputs the formatted information. The resulting file can then be sent to the PROM burner to create the application in a 32K PROM (Part No. 27C256, 150 nanoseconds or faster).
To burn the PROM in less time, we can dump out only the bytes that are occupied by the definitions area, name area, and autostart region of the application. This is accomplished by setting the terminal program to capture incoming text and executing:
HEX DEFINITIONS.START XDUP DROP HERE DEFINITIONS.START |X1-X2|>U DUMP.INTEL \ definitions area NAME.AREA.START XDUP DROP NHERE NAME.AREA.START |X1-X2|>U DUMP.INTEL \ name area 7FE0 4 7FE0 20 DUMP.INTEL \ autostart region
The definitions area extends from DEFINITIONS.START to HERE, and the name area extends from NAME.AREA.START to NHERE. The autostart specification occupies the last 6 bytes on page 4, but we dump the last 32 bytes because each line of the hex dump reports 32 bytes. These three separate Intel Hex dumps can be concatenated by removing the terminating line (:000000001FF) in each of the first two dumps. Then the single file can be sent to the PROM burner to make the image of the application. The unprogrammed bytes in the PROM will retain values of FFH.
We would take a different approach if we needed to burn a PROM containing a full 128K of application code. For example, suppose that our application resides in 128 K spanning pages 4, 5, 6, and 7. We could generate a Motorola S2 format file by recording the ascii dump generated by the commands
HEX 0 4 DIN 000000 8000 DUMP.S2 \ dump page 4 0 5 DIN 008000 8000 DUMP.S2 \ dump page 5 0 6 DIN 010000 8000 DUMP.S2 \ dump page 6 0 7 DIN 018000 8000 DUMP.S2 \ dump page 7
Each line dumps a 32K page starting at QED address 0 in pages 4, 5, 6, and 7 respectively. The double number following DIN specifies the 24 bit start address of each page in the PROM; note that each start address is 8000H greater than the preceding start address. 8000H is the number of bytes in each 32 K page. These four S2 dumps can be sent to the PROM burner sequentially, or they can be concatenated by removing the intermediate header records (which start with "S1") and the termination records (which start with "S9").
Now to run your application, simply remove the RAM from socket S3, plug the newly burned PROM into S3, and flip the DIP switch #3 (the S3 RAM/ROM switch) to the ON position. Power up the QED Board and it will automatically run your application. Replicate your PROMs and you're in production!
Turnkeyed Application Code Listing
\ May 1992 Copyright Mosaic Industries, Inc. \ \ ************************ Turnkeying a QED Application ******************** \ \ This is the code for an example application. The accompanying chapter presents a detailed \ explanation of this code. The code can run on a QED board with the minimum memory configuration \ which includes the QED-Forth development ROM in socket#1, a 32K RAM in socket#2, and a 32K ROM \ in socket#3. To allow display of the statistical results calculated by the program, a 4X20 LCD display \ should be attached to the QED Board. The optional 12 bit A/D and 8 bit D/A are not needed. \ \ Description of the application: \ This program reads an input voltage, creates a pulse width modulated (PWM) signal whose average \ value tracks the input voltage, and reports the mean and standard deviation of the input voltage. The \ following 3 tasks do the work: \ Task 1: Reads 8 bit A/D, measuring the voltage on a potentiometer connected to a PortE pin. \ Task 2: Outputs on Port A bit 5 a PWM signal whose average equals the A/D input. \ Task 3: Every 1 second puts A/D value into a matrix, and every 10 seconds displays the mean and \ standard deviation of the last 10 seconds worth of data. \ \ The following programming issues are addressed: \ \ Setting up the memory map \ Using ANEW \ Proper initialization of tasks at startup \ Autostarting \ Interrupt service routines and interrupt attaching \ Optional assembly coding to speed up an interrupt service routine \ Heap initialization \ Multitasking \ SAVE, RESTORE \ Do.cold.on.reset for secure restart \ Floating point calculations \ Using local and self-fetching variables \ Using the display \ Optional "back door" access to QED-Forth to allow field service and diagnostics \ Generation of a PROM image of application \ ************************ Memory Map Initialization ************************ \ Memory map summary of ROM system, assuming a minimum memory configuration \ with development ROM in socket #1, 32K RAM in socket#2 and 32K ROM in socket#3: \ 0-7FFF (32K) in page 4 is ROMmed user application code \ 3000-7FFF (20K) in page 0F is RAM \ 8400-ADFF is available common RAM, as is 1K 68HC11 on-chip RAM at B000-B3FF \ (See Appendix A in software manual for common RAM details) \ AEC0-AFFF is available EEPROM {EEPROM at AE00-AEBF is reserved for QED-Forth} \ QED-Forth ROM occupies 0-7FFF on page 0, 0-2FFF on page F, and B400-FFFF in common ROM \ Before compiling any code we first setup the memory map for compilation: HEX \ use hexadecimal base during compilation of the program 0000 04 DP X! \ put definitions (up to 20K) on page 4 5000 04 NP X! \ 12K name area on page 4 8E00 0 VP X! \ Variable area in common RAM ANEW TURNKEYED.APPLICATION \ now that we've set memory map we can execute ANEW 8 WIDTH ! \ save up to 8 letters in name field of each word defined \ We define some memory map constants that will be useful later: 0000 04 XCONSTANT DEFINITIONS.START 5000 04 XCONSTANT NAME.AREA.START 8E00 0 XCONSTANT VARIABLE.AREA.START 4000 0F XCONSTANT BOTTOM.OF.HEAP \ 16K heap in page F RAM 7FFF 0F XCONSTANT TOP.OF.HEAP BOTTOM.OF.HEAP TOP.OF.HEAP IS.HEAP \ initialize heap now for compilation \ Note that the specified memory map could also have been set by executing \ 4 USE.PAGE \ However, for the sake of clarity and generality we have shown the commands \ that set each memory map pointer. \ *********************** 8 BIT A/D Data Gathering Task ********************* \ This task gathers data from the 8 bit A/D and places it in a variable easily accessed by other tasks. \ Define the control registers and some useful constants: 0 CONSTANT A/D8.INPUT.CHANNEL \ You can use any channel you want and place the input potentiometer on the corresponding A/D8 input \ pin. For example, if 0 is your input channel, place the input signal on the pin labeled AN0 (pin 10 of \ the analog connector). FVARIABLE INPUT.VOLTAGE \ Holds voltage corresponding to latest result of A/D input Because the contents of this \ variable are shared among several tasks, we use uninterruptable |F!| and |F@| operations \ to access it. \ Now we convert a measured count from the A/D that ranges from 0 to 255 into a voltage that ranges \ from 0.0 Volts (the low reference voltage) to nearly 5.0 Volts (the high reference voltage). \ This involves solving the equation: \ Voltage = [ (high.ref.voltage - low.ref.voltage) * measured.count / 256 ] + low.ref.voltage \ First let's define some constants: DECIMAL 256. FCONSTANT #FULL.SCALE.COUNTS \ 256 counts in an 8 bit converter 0.0 FCONSTANT LOW.REF.VOLTAGE 5.0 FCONSTANT HIGH.REF.VOLTAGE \ NOTE: For maximum accuracy, measure +5VAN with a voltmeter and \ set HIGH.REF.VOLTAGE equal to the result. : ->VOLTAGE ( n -- r ) \ This routine converts 8 bit A/D measured count n { 0 <= n <= 255 } \ to the corresponding floating point voltage r {typically, 0.0 <= r <= 5.0 } \ by solving the equation: \ r = [ (high.ref - low.ref) * count / 256 ] + low.ref FLOT \ convert input to floating point HIGH.REF.VOLTAGE LOW.REF.VOLTAGE F- F* #FULL.SCALE.COUNTS F/ LOW.REF.VOLTAGE F+ ( -- r ) \ this is the answer ; : GATHER.DATA ( -- ) \ This is the activation routine for the data gathering task. \ It continually acquires readings from the A/D, converts the readings to voltages, \ and updates the INPUT.VOLTAGE variable that is accessed by other tasks. A/D8.ON \ make sure A/D is powered up BEGIN \ start infinite loop A/D8.INPUT.CHANNEL A/D8.SAMPLE ( -- n ) \ n is 8bit A/D result ->VOLTAGE ( -- r ) \ r is fp representation of voltage INPUT.VOLTAGE |F!| ( -- ) \ save with uninterruptable store AGAIN ; \ ********************* Pulse Width Modulation (PWM) Task ******************** \ This task calculates the high time and low time of the PWM output based on \ the value of the INPUT.VOLTAGE which is updated by the data gathering task. \ The goal is to set the duty cycle of the PWM output so that the average of \ the PWM signal equals the INPUT.VOLTAGE. This is achieved by solving the \ equation: \ DutyCycle = (INPUT.VOLTAGE - low.output) / (high.output - low.output) \ in which low.output and high.output are the voltage levels that appear \ on the PWM output pin in the low and high states, respectively. \ \ Given the duty cycle, and given our choice of a PWM period of 130 msec, we \ calculate the high.time and low.time of the PWM signal in terms of the timer \ counts; each timer count equals 2 microseconds. High.time is the number of \ timer counts that the signal is in the high state and Low.time is the number \ of timer counts that the signal is in the low state during each 130 msec \ period. High.time and Low.time are calculated as: \ High.time = Duty.cycle * TIMER.COUNTS/PERIOD \ Low.time = TIMER.COUNTS/PERIOD - High.time \ where TIMER.COUNTS/PERIOD = 130 msec/2 microsec = 65,000. \ \ We also "clamp" High.time and Low.time to a minimum value to prevent a \ situation where interrupts are requested so rapidly that the processor one. \ \ The OC3 (output compare 3) interrupt code in the subsequent section uses these \ calculated values to pulse width modulate the PORTA output port pin PA5. DECIMAL \ We define some variables and constants: \ VARIABLE HIGH.TIME \ holds the number of timer counts that PWM output is high VARIABLE LOW.TIME \ holds the number of timer counts that PWM output is low 0.0 FCONSTANT LOW.OUTPUT.VOLTAGE \ equals the voltage on PA5 when it is low 5.0 FCONSTANT HIGH.OUTPUT.VOLTAGE \ equals the voltage on PA5 when it is high \ NOTE: for maximum accuracy, the voltage output of pin PA5 should be measured in \ the low and high states and LOW.OUTPUT.VOLTAGE and HIGH.OUTPUT.VOLTAGE set \ accordingly. 130. FCONSTANT MS/PERIOD \ use a 130 msec period for PWM output \ NOTE: the timer "rolls over" every 131 msec 65000 CONSTANT TIMER.COUNTS/PERIOD \ 130 milliseconds corresponds to 65,000 counts of 2 microseconds (usec) each \ on the free-running timer. QED-Forth sets the contents of TMSK2 so that \ each timer count represents 2 usec. This is true whether the crystal speed \ is 8 MHz or 16MHz. We assume here that the programmer has not installed a \ different value in TMSK2 using INSTALL.REGISTER.INITS. 250 CONSTANT MINIMUM.HIGH.OR.LOW.TIME \ corresponds to 500usec \ we choose a minimum high or low time approximately equal to the maximum \ number of timer counts divided by 256. That is, we only require 8 bits of \ resolution from our PWM. The benefit of imposing a minimum high or low \ time is that by doing so we can ensure that the minimum time between \ PWM interrupts is 500 usec. This is more than sufficient time to allow the \ service routine to execute, even if it is written in high level code. : CALCULATE.DUTY.CYCLE ( -- r | r = duty cycle; 0 <= r <= 1.0) \ Implements the equation: \ DutyCycle = (Input.voltage - low.output) / (high.output - low.output) INPUT.VOLTAGE |F@| ( -- r1 | r1 = latest input voltage from potentiometer ) LOW.OUTPUT.VOLTAGE F- HIGH.OUTPUT.VOLTAGE LOW.OUTPUT.VOLTAGE F- F/ ZERO FMAX ONE FMIN \ clamp the result to 0 <= duty.cycle <= 1.0 ; : DUTY.CYCLE->HIGH&LOW.TIMES ( r -- | r = duty cycle ) \ saves high and low times as 16bit integer counts in the variables HIGH.TIME \ and LOW.TIME using the equations: \ HIGH.TIME = Duty.cycle * TIMER.COUNTS/PERIOD \ LOW.TIME = TIMER.COUNTS/PERIOD - HIGH.TIME \ Both HIGH.TIME and LOW.TIME are clamped to a minimum so that timer interrupts \ don't occur more frequently than they can be reliably serviced. TIMER.COUNTS/PERIOD UFLOT F* \ calc duty.cycle*{counts/period} UFIXX \ convert it to an unsigned integer MINIMUM.HIGH.OR.LOW.TIME UMAX ( -- high.time ) \ enforce minimum value HIGH.TIME ! ( -- ) \ save result TIMER.COUNTS/PERIOD HIGH.TIME @ - ( -- unclamped.low.time ) DUP MINIMUM.HIGH.OR.LOW.TIME U< IF DROP MINIMUM.HIGH.OR.LOW.TIME ( -- clamped.low.time ) TIMER.COUNTS/PERIOD MINIMUM.HIGH.OR.LOW.TIME - HIGH.TIME ! \ adjust high.time THEN LOW.TIME ! ( -- ) \ set low.time ; : SET.PWM.PARAMETERS ( -- ) \ This is the activation roution for the PWM task. It updates the \ values in HI.TIME and LOW.TIME for use by the OC3 interrupt which \ generates the PWM output waveform on PORTA pin PA5. BEGIN CALCULATE.DUTY.CYCLE DUTY.CYCLE->HIGH&LOW.TIMES AGAIN ; \ ********************* OC3 Interrupt Code ********************** \ This interrupt routine generates a pulse width modulated output on Port A bit 5 \ based on the duty cycle calculated by the PWM task. For more detail, see two \ chapters in the QED Hardware Manual. The chapter titled "A Hardware Perspective \ on 68HC11 Interrupts" describes how to create interrupt service routines, and, \ The "Programmable Timer" Chapter discusses output compares and generation of \ pulse signals. One example uses the OC3 interrupt that is used here. \ Briefly, there are 4 steps involved in coding an interrupt service routine: \ 1. Name all required hardware registers using the REGISTER: command, and \ name all required bit masks with appropriate mnemonics. \ 2. Use QED-Forth or assembly code to define an interrupt handler \ which must clear the interrupt request flag and perform required service actions. \ 3. Install the interrupt handler using the ATTACH routine. \ 4. Write routines to enable and disable the interrupt. \ (We combine steps 3 and 4 in a routine that ATTACHes and enables the interrupt). HEX \ define the names of required registers: 800E REGISTER: TCNT \ free-running counter register 801A REGISTER: TOC3 \ count register for output compare 3 {OC3} 8020 REGISTER: TCTL1 \ timer control register #1 8022 REGISTER: TMSK1 \ timer interrupt mask register #1 8023 REGISTER: TFLG1 \ timer interrupt flag register #1 800B REGISTER: CFORC \ allows us to force an output compare \ Define output mode configuration flags and masks that specify action \ to be performed when a successful output compare occurs: 20 CONSTANT PA5.MASK \ mask for setting/resetting PA5 20 CONSTANT OC3.MASK \ to set/clr OC3 interrupt flag & mask 10 CONSTANT OC3.LEVEL.MASK \ mask in TCTL1; controls level of PA5 20 CONSTANT OC3.MODE.MASK \ mask in TCTL1; enables pin control of PA5 \ Summary of the functions of these registers: \ OC3 (output compare 3) is associated with pin PA5 on PORTA. We can write a 16 \ bit count into the TOC3 register, and when the count in TOC3 matches the count \ in the main counter TCNT, an interrupt request can occur. The request only occurs \ if interrupts are globally enabled and if the OC3 interrupt is locally enabled. \ The OC3 interrupt is locally enabled by setting the bit specified by the OC3.MASK \ in the TMSK1 register. When the interrupt request occurs, the 68HC11 automatically \ sets the bit specified by OC3.MASK in the TFLG1 (timer flag #1) register. Our \ interrupt service routine must clear this bit (oddly enough, interrupt request \ bits are cleared by writing a 1 to the bit position!) The register TCTL1 \ controls whether the state of the PA5 bit is automatically changed when TOC3 \ matches TCNT. In this application we enable this automatic "pin control" action \ by setting the bit specified by OC3.MODE.MASK in TCTL1. The bit specified by \ OC3.LEVEL.MASK in TCTL1 then controls the level (high or low) to which PA5 is \ set upon a successful output compare. DECIMAL 500. FCONSTANT COUNTS/MS \ number of TCNT counts per msec : OC3.SERVICE OC3.MASK TFLG1 C! \ Reset the OC3 interrupt flag so that new OC3 interrupts will be \ recognized. Because the flag is cleared by writing a one to it we can \ use a C! command without affecting the other bits. TCTL1 C@ OC3.LEVEL.MASK AND \ Look at the OC3/PA5 pin output level IF \ If the output level just toggled high we'll OC3.LEVEL.MASK TCTL1 CLEAR.BITS \ set the mode/level bit so the next output \ compare forces the pin low after the HIGH.TIME HIGH.TIME @ \ Leave the HIGH.TIME on the stack ELSE \ If the output level just toggled low we'll OC3.LEVEL.MASK TCTL1 SET.BITS \ set the mode/level bit so the next output \ compare forces the pin high after the LOW.TIME LOW.TIME @ \ Leave the LOW.TIME on the stack ENDIF TOC3 +! \ Add the HIGH.TIME or LOW.TIME to TOC3 to set \ the time at which the next interrupt occurrs. ; CODE FASTER.OC3.SERVICE ( -- ) \ This interrupt service routine performs the same functions as the high level \ service routine named OC3.SERVICE. This alternative interrupt service routine \ is assembly coded for improved performance; it executes in under 90 usec. \ Note that high level routines are still invoked to fetch from HIGH.TIME \ and LOW.TIME because these fetches involve page changes that are best handled \ by the pre-coded high level routines. TCTL1 DROP EXT LDAB OC3.LEVEL.MASK IMM BITB \ Look at the OC3/PA5 pin output level NE IF, \ If the output level just toggled high we'll OC3.LEVEL.MASK COMPLEMENT IMM ANDB \ clear the level bit in TCTL1 so TCTL1 DROP EXT STAB \ the next OC3 clears PA5 after the HIGH.TIME CALL HIGH.TIME CALL @ \ fetch the HIGH.TIME to the data stack ELSE, \ If the output level just toggled low we OC3.LEVEL.MASK IMM ORAB \ set the level bit in TCTL1 so that the TCTL1 DROP EXT STAB \ the next OC3 clears PA5 after the LOW.TIME CALL LOW.TIME CALL @ \ fetch the LOW.TIME to the data stack ENDIF, 0 IND,Y LDD \ move the HIGH.TIME or LOW.TIME from the TOC3 DROP EXT ADDD \ stack into D and use it to TOC3 DROP EXT STD \ update the OC count for the next output compare INY INY \ clear data stack OC3.MASK IMM LDAA \ Reset the OC3 interrupt flag so that new TFLG1 DROP EXT STAA \ OC3 interrupts will be recognized. The flag is \ cleared by writing a one to it. This must be done \ so that an interrupt is invoked on a successful \ compare, and this flag must be cleared before \ the compare happens. RTS \ return from the interrupt service routine END.CODE : INSTALL.OC3 ( -- ) \ This installation routine enables the "pin control" function, ATTACHES the \ service routine, configures PortA pin 5 as an output, initializes the output \ compare register TOC3, clears the OC3 interrupt request flag (thus ignoring \ prior interrupt requests), and locally enables the OC3 interrupt. \ PWM can begin when interrupts are globally enabled. OC3.MASK TMSK1 CLEAR.BITS \ First we disable OC3 interrupts. OC3.MODE.MASK TCTL1 SET.BITS \ Set the OC3 mode bit so that an output \ compare automatically sets or clears \ the PA5 output pin depending on the \ state of the level bit. That bit is \ toggled by the interrupt routine. \ CFA.FOR OC3.SERVICE OC3.ID ATTACH \ optional high level service routine CFA.FOR FASTER.OC3.SERVICE OC3.ID ATTACH \ use the high performance version OC3.LEVEL.MASK TCTL1 CLEAR.BITS \ Set to low before pulses start. OC3.LEVEL.MASK CFORC SET.BITS \ and initialize by forcing a compare. TCNT @ TIMER.COUNTS/PERIOD + TOC3 ! \ start after a minimum 130ms delay OC3.MASK TFLG1 C! \ clear interrupt flag OC3F OC3.MASK TMSK1 SET.BITS \ set OC3I to locally enable interrupts ; \ ************************** Statistics Task ************************** \ This task samples the analog input voltage (see task 1) once per second, stores it \ in a matrix, and calculates and prints the mean and standard deviation \ of the data every 10 seconds. DECIMAL MATRIX: LAST.10.VOLTAGES \ row matrix; holds 1 voltage per second for past 10 seconds \ these index variables keep track of where data is placed in the LAST.10.VOLTAGES matrix: INTEGER: MATRIX.INDEX INTEGER: LAST.INDEX : CALC.STATS ( -- r1\r2 | r1 = mean, r2 = standard deviation ) \ This routine calculates mean and standard deviation of the LAST.10.VOLTAGES matrix \ using routines from the built-in matrix math library. 0 -1 ' LAST.10.VOLTAGES ROW/COL.CENTERED ( -- mean ) ' LAST.10.VOLTAGES MATRIX.VARIANCE FSQRT ( -- mean\standard.deviation ) ; \ These routines manage the LCD display. \ The message on the display will look like this: \ Mean = \ x.xxx Volts \ Standard Deviation = \ x.xxx Volts 4 CONSTANT FP.OFFSET \ character offset to start of mean or std.deviation in display line 14 CONSTANT VOLTS.OFFSET \ character offset to start of "Volts" label in display line : SETUP.DISPLAY ( -- ) \ writes headings to display; leaves numbers blank. DISPLAY.BUFFER 80 BLANK \ blank display buffer " Mean = " 0 0 $>DISPLAY \ line 0 " Volts" 1 VOLTS.OFFSET $>DISPLAY \ line 1 " Standard Deviation =" 2 0 $>DISPLAY \ line 2 " Volts" 3 VOLTS.OFFSET $>DISPLAY \ line 3 UPDATE.DISPLAY \ write buffer contents to display ; : SHOW.STATS ( r1\r2 -- | r1 = mean, r2 = standard deviation ) \ displays mean and standard deviation of LAST.10.VOLTAGES on LCD display F>FIXED$ DROP 3 FP.OFFSET $>DISPLAY \ write standard deviation to display buffer F>FIXED$ DROP 1 FP.OFFSET $>DISPLAY \ write mean to display buffer UPDATE.DISPLAY \ write to display ; : LOG.DATA&SHOW.STATS ( -- ) \ increments MATRIX.INDEX every second, \ loads INPUT.VOLTAGE data collected by task 1 into LAST.10.VOLTAGES matrix, \ and displays statistics on LCD display every 10 seconds READ.ELAPSED.SECONDS ( -- u\ud | #msec\d.#sec ) ROT 2DROP ( -- #sec{ls.word} ) 10 UMOD TO MATRIX.INDEX ( -- ) \ set 0 <= MATRIX.INDEX <= 9 MATRIX.INDEX LAST.INDEX <> IF \ if a second has elapsed... INPUT.VOLTAGE |F@| 0 MATRIX.INDEX LAST.10.VOLTAGES F! \ store reading in matrix MATRIX.INDEX TO LAST.INDEX \ update LAST.INDEX MATRIX.INDEX 9 = IF \ if 10 seconds have elapsed... CALC.STATS \ calculate new statistics SHOW.STATS \ update display ENDIF ENDIF ; : STATISTICS ( -- ) \ this is the activation routine for the statistics task; \ it calculates and displays the mean and standard deviation of the data. 1 10 ' LAST.10.VOLTAGES DIMMED \ dimension... ' LAST.10.VOLTAGES ZERO.MATRIX \ ... and initialize matrix -1 TO LAST.INDEX \ initialize LAST.INDEX 2 LEFT.PLACES ! 3 RIGHT.PLACES ! \ set floating point display format SETUP.DISPLAY \ write headings to display BEGIN LOG.DATA&SHOW.STATS \ calculate and display the statistics AGAIN ; \ ********************* BUILD TASKS **************************** \ Note: we'll keep the Forth interpreter task's user area at address 8400\0 \ during development/debugging, and put it ASLEEP in the final turnkeyed version \ First name the tasks and assign their base addresses: HEX 8400 0 TASK: FORTH.TASK \ default task running QED-Forth; \ this task is automatically built and started upon each reset/restart; \ in the autostart routine FORTH.TASK puts itself ASLEEP so the end \ user can't run Forth. A service person, however, can bring the IRQ \ input pin low to invoke the "secret back door" that awakens QED-Forth. 9600 0 TASK: READ.INPUT \ data gathering task 9A00 0 TASK: CONTROL.OUTPUT \ PWM task 9E00 0 TASK: REPORT.STATISTICS \ statistics reporting task : BUILD.TASKS ( -- ) \ Empties the round robin task loop and then \ carefully builds the tasks every time we start up. \ Note that only the statistics task has access to the heap. (STATUS) NEXT.TASK ! \ must be done! this effectively kills \ other tasks by emptying the round robin task loop. 0\0 0\0 0\0 READ.INPUT BUILD.STANDARD.TASK 0\0 0\0 0\0 CONTROL.OUTPUT BUILD.STANDARD.TASK BOTTOM.OF.HEAP TOP.OF.HEAP 0\0 REPORT.STATISTICS BUILD.STANDARD.TASK ; : ACTIVATE.TASKS ( -- ) \ associate activation routines with each of the tasks. CFA.FOR GATHER.DATA READ.INPUT ACTIVATE CFA.FOR SET.PWM.PARAMETERS CONTROL.OUTPUT ACTIVATE CFA.FOR STATISTICS REPORT.STATISTICS ACTIVATE ; \ ********************* SET UP BACK DOOR ********************* \ the "back door" is simply an interrupt routine invoked by temporarily \ grounding the IRQ input. It awakens the QED-Forth task which is typically \ asleep in the final production system. The back door can be invoked \ by field service personnel to aid in field diagnosis of the instrument. : BACKDOOR ( -- ) \ called by the IRQ interrupt, simply awakens the default FORTH.TASK \ by storing AWAKE into its STATUS user variable. \ this feature allows a service person to gain access to the \ QED-Forth interactive environment to diagnose a problem, update code, etc. \ A switch to ground from the IRQ input {pin 20 of the I/O and control bus} \ could be used to invoke the back door access to QED-Forth. AWAKE STATUS FORTH.TASK TASK'S.USER.VAR ! ; : INSTALL.BACKDOOR ( -- ) \ installs the interrupt service routine for the active.low IRQ input \ which is accessible at pin 20 of the digital I/O and control bus. \ note that there is no local interrupt mask for the IRQ input; \ it is enabled whenever interrupts are globally enabled. \ so all we need to do is attach the service routine. CFA.FOR BACKDOOR IRQ.ID ATTACH ; \ ********************* SET UP AUTOSTART ROUTINE ********************* \ We'll designate the top level word START.UP as the PRIORITY.AUTOSTART routine. \ Every time the QED-Board is powered up or reset, the START.UP routine will automatically \ be executed by the default FORTH task. \ START.UP zeros the variable area. \ It re-initializes the VFORTH user variable which points to the top of the \ names list so that all of the defined names are accessible. \ It also initializes the heap. \ Thus the FORTH task which runs START.UP has access to all defined names and to the heap, \ and this is very useful while debugging and performing diagnostics. \ START.UP initializes the elapsed time clock, \ installs the OC3 and the "back door" interrupt service routines, \ and builds and activates the tasks. \ It releases control of the serial line, starts the timeslicer, and PAUSEs \ to begin execution of the application. \ After debugging is complete, the optional commands which \ specify a COLD restart and which put the FORTH task ASLEEP can be inserted; \ these commands are "commented out" in the code shown here. VHERE VARIABLE.AREA.START |X1-X2|>U CONSTANT #VARIABLE.BYTES \ calculate and save as a constant the number of variable bytes \ that we have used; we'll erase the bytes every time we start up. : ZERO.VARIABLE.AREA ( -- ) \ It is good practice to initialize all variables upon startup! VARIABLE.AREA.START #VARIABLE.BYTES ( start.xaddr\#bytes.allocated -- ) ERASE ( -- ) \ zero the variables ; : START.UP ( -- ) \ this is the highest level routine in the turnkeyed application. ZERO.VARIABLE.AREA \ init variables [ LATEST ] 2LITERAL VFORTH X! \ this preserves access to the name list BOTTOM.OF.HEAP TOP.OF.HEAP IS.HEAP \ it's important to init heap at startup INIT.ELAPSED.TIME \ initialize QED-Forth elapsed time clock INSTALL.OC3 \ install service routine for PWM generation INSTALL.BACKDOOR \ install service routine for IRQ input BUILD.TASKS \ initialize user areas of the tasks ACTIVATE.TASKS \ associate action routine with each task \ COLD.ON.RESET \ ensures full initialization upon each reset \ ASLEEP STATUS ! \ puts Forth task asleep; \ COLD.ON.RESET and ASLEEP commands are commented out during debugging \ but are present in the final version SERIAL RELEASE \ in case another task needs the serial port START.TIMESLICER \ starts elapsed time clock and globally enables interrupts PAUSE \ start next task immediately ; SAVE \ if we happen to crash, RESTORE recovers our definitions and pointers. \ Now execute: CFA.FOR START.UP PRIORITY.AUTOSTART \ which initializes the priority.autostart vector \ at locations 7FFA-7FFF on page 4 which will be in PROM. \ For now, we can write protect the battery backed memory in page 4 \ by turning DIP switch #1 ON. \ This emulates the behavior of a PROM so that we can make sure that everything \ will work properly in the final PROMmed application. \ Then upon the next restart the START.UP routine will automatically execute. \ NOTE: To erase the autostart vector and return to QED-Forth, \ un-write-protect page 4 by turning DIP switch#1 OFF, \ and activate the Special Cleanup Mode by toggling DIP switch #5 and \ resetting the board; then restore switch #5 to its original position. \ The Special Cleanup Mode erases the autostart vector and restores the \ board to a "pristine" condition. \ To generate a PROM that contains this application, make an image of page 4 \ which holds the definitions area, name area and autostart vector. \ Set your terminal to capture incoming text to a disk file, and execute: \ HEX 0 4 0 8000 DUMP.INTEL \ Then send the resulting file to a PROM burner and \ burn a 32K PROM (part No. 27C256, 150 nsec or faster). \ Turn off your board and remove the RAM from socket S3. \ Turn DIP switch #3 ON to configure socket S3 to accept a PROM, and \ plug the PROM into socket S3. \ Power up the QED Board and it will automatically run the application!