The DSTRO DDS-based stroboscope is a BOBZ project built with a MINT processor and several other inexpesive components such as a Direct Digital Synthesis module, an LCD display and an incremental encoder. The stroboscope provides high resolution and accuracy and several other useful features such as an adjustable duty cycle.
A typical use for the stroboscope is to determine the rotational rate of a motor or to perform "stop motion" display of other periodic events.
Page iiiPRODUCT INDEX
The DSTRO DDS-based digital stroboscope provides a high accuracy stroboscope that can be be built with a BOBZ MINT processor board and several other inexpensive parts.
The MINT processor board uses an inexpensive Silicon Laboratories C8051F850 processor that controls system components such as a 2X16 LCD display, an incremental encoder, a Direct Digital Synthesis (DDS) module and a strobe LED.
The top line of the LCD displays the strobe's output frequency, which can vary between 0.01 Hertz and 500 Hertz in 0.01 to 100 Hertz steps. The frequency units (Hertz or rpm) are also displayed on the top line next to the numeric readout.
The second line of the LCD shows the currently encoder rate: 0.01 to 100 Hertz or 0.6 to 6000 rpm. Also displayed on line 2, next to the rate, is the currently-selected LED duty cycle (in percent).
The incremental encoder is an inexpensive mechanical encoder with an integrated pushbutton switch. The encoder allows very fine frequency or rpm adjustments. The switch is used to select menu options. Menu options include Rate, Duty Cycle Units (Hertz or rpm).
Whenever a menu item is selected, it is saved to the processor's flash program memory so that it can be restored when the strobe is next turned on. Items saved in flash include the current frequency, last-used menu and last-set menu choice.
The strobe project was originally done to measure the rotation rate of a rotating platform used for a color display. This project also illustrates the kind of project that can be built with the MINT processor chip and some commonly-available and inexpensive components.
The primary purpose of this manual is to describe how the stroboscope was built, showing how the MINT processor can be used to build a useful project. Another objective is to describe some of the MyForth software used to implement the stroboscope. Particularly emphasized are software modules used to interface with the major project components such as the incremental encoder, LCD and DDS. Also described are MyForth routines for multiplication and menuing.
Because the stroboscope is simply a low-frequency variable frequency oscillator (VFO) with a display, control knob and menu, the project can serve as a template for other VFO applications.
A stroboscope can produce an intense low frequency flashing or pulsing light that can trigger harmful physical effects such as epileptic siezures. This is particularly true for individuals with photosensitive epilepsy. Before using the strobe, please ensure that you and all observers are not susceptible to these effects.
Also, the light output, even with one white LED, may be intense enough to potentially cause temporary vision impairment or permanent eye damage when viewed directly at close distances, for long time periods or in conjunction with lenses. Users are cautioned that risks may increase when using higher output light sources, such as LED arrays, for strobe illumination.
Some types of LEDs and light sources, such as infrared and ultraviolet LEDs or laser modules are particularly dangerous and should not be used with the DSTRO output driver.
The DDS-based stroboscope offers the following features:
The construction of the DSTRO prototype is outlined in this document, including an enclosure that is inexpensive and easy to build. A printed circuit board (PCB) is now being designed that implements the wiring between the LCD and the MINT board. The PCB is the same length as the LCD and provides edge connector pads for the encoder, its switch, the DDS module and the strobe LED.
The stroboscope assembly is built on a prototyping board measuring 2 by 2 3/4 inches (50 by 70 mm). This board provides a mounting base for the 1 by 16 LCD socket (along the top edge) and the 2 by 12 MINT processor socket. The LCD covers most of the top side of the assembly and the MINT processor board mounts on the bottom side of the assembly.
The DDS module mounts on the back side of the proto board in a 2 by 10 dual inline pin format.
The proto board also provides mounting for various components and connectors, including the LCD contrast potentiometer, the strobe LED driver and connectors for the power input, encoder and strobe LED.
The board hosts all of the components and modules shown in Schematic 1.
Photo 1 shows the board assembly mounted in a case along with the incremental encoder (with a spinner knob).
Photo 2 shows the front of the board assembly with the LCD module dominating. The LCD contrast control is shown mounted on the proto board near the top right of the photo. Near the contrast potentiometer is PL7, the 6-pin connector that brings out all of the DDS module's output signals (only one is used). Other than some wire wrap wiring, other parts of the assembly are masked by the LCD or are mounted on the back of the proto board.
Toward the bottom of Photo 2 are the right-angle stake pin connectors for the incremental encoder/switch (PL6) and the strobe LED (PL5). Note the mounting of the MINT module on the back side of the proto board, which is barely observable in the lower left of Photo 2.
Photo 3 shows an oblique view of the back side of the protoboard assembly. This photo mainly features the MINT processor and the DDS module. The connector for the LCD shows along the bottom edge.
As shown, the MINT processor board stands vertically at the bottom left of the proto board.
The DDS module mounts along the top of the board. Note that the 16 Megahertz (MHz) crystal oscillator on the DDS module is mounted on a machined pin socket. This oscillator replaces the normal 125 MHz crystal oscillator furnished with the module. The lower-frequency crystal oscillator increases the strobe's low-frequency resolution. Pin 1 of the oscillator is marked with a small dab of white paint.
A 5 Volt power connector mounts on the bottom right of the board. The row of stake pins near the power connector are ground connections used during development. Connector P7 is visible at the top right of the board. The eight-pin socket was originally intended for the crystal oscillator and is not used.
Before proceeding further, we recommend downloading a high resolution DSTRO schematic. A printed copy of this is is easier to read than Schematic 1 which has been reduced to fit on an 8 1/2" X 11" page. Because the schematic is often cited, it is more convenient to reference the printed schematic than to frequently page back to it.
The following are the basic DSTRO design considerations:
For the following, refer to Photo 1, which shows a typical Frequency display.
At startup, the current DSTRO version is briefly displayed on the top line of the LCD. The version consists of the date of the last firmware download. For example, the display might read: DSTRO 18Mar14 . The strobe LED also remains lit for the duration of the startup display.
The Frequency display appears after the Startup display. The top line of the Frequency display shows the current flash frequency. The default units for the Frequency display is Hertz. However, if the units were previously set in revolutions per minute (rpm), the displayed frequency is in rpm. The frequency is changed by rotating the incremental encoder knob.
As shown in Photo 1, the knob used for this project has a smaller "high rate" handle on its outer radius that allows spinning the dial to rapidly move between frequency settings. The outer diameter of the knob is sufficient to allow adjustment to the finest rate increments (e.g., 0.01 Hertz) with small finger movements.
The second line of the Frequency display, shows the duty cycle followed by the resolution. For example, a duty cycle of 90% and a resolution of 0.6 rpm would display as: d=90% r=0.6 . Note that the resolution is shown in the same units that were selected for the frequency (e.g., r=0.6 for rpm units).
Operating parameters may be changed by pushing the incremental encoder knob to bring up the main menu display, the Option menu. This menu offers three secondary menus for Rate, Units and Duty Cycle. Each option is displayed with a ">" in front of it (e.g., >rate). Rotating the encoder knob sequentially displays the options. The options appear centered on the second line of the Option menu.
Rotating the encoder knob in a clockwise direction brings up each option in normal order (e.g., >rate, >units, >duty). Rotating the encoder knob counterclockwise displays the options in reverse sequence.
The last menu option is "return to the frequency display." This option is indicated by "<---" to indicate movement back to the previous menu. In the Option Menu, selecting this option returns to the Frequency display. The back arrow "return" indication is also displayed on each of the option menus to allow return to the Option menu.
Whenever an option is selected on any menu, the selected option will appear first whenever the menu is reselected. For example, if the rate option is selected, the next time the Option Menu is displayed, the rate option will appear first.
Because option selections are saved to flash memory, the menu ordering is also preserved at power up.
Whenever an option is selected on any menu, the current frequency is also saved to flash memory for restoration at startup. This includes selection of the return option.
Thus, to save the current frequency without altering any options, select the return option on the Option Menu. Because saving the current frequency may be a frequently-selected option, the return option is given preference for redisplay on the Option Menu.
Select the Rate menu by pressing the encoder knob when the ">rate" option is displayed on the Option menu. This brings up the Rate Menu with "Rate Menu" displayed at the center of line 1 of the LCD. One of the rate options is displayed at the center of line 2 (e.g., >0.1 Hertz). The displayed rate is the last-selected rate. As with other menus, rotating the encoder knob displays the available options.
The rate options are:
Note that rate selection is always in "Hertz" units. Indication based on the current units selection is a future (and desirable) enhancement. The rpm rate increment is always the selected Hertz increment multiplied by 60. For example, selecting a rate of 0.01 Hertz corresponds to selecting an increment of 0.6 rpm.
When a rate is selected, a "selected" message appears on line 2. For example, when a rate of 1 Hertz is selected, line 2 will briefly display the message: 1 Hertz set! .
Immediately after a rate is set, the user is returned to the Frequency display. This eliminates the need for returning to the Option Menu and selecting the return option.
The Units menu operates in much the same way as the Rate menu. When initially selected, "Units Menu" displays at the center of line 1 and one of the units options appears centered on line 2. Rotating the encoder knob cycles between the units options.
There are only two units options: rpm and Hertz . When an option is selected, a message such as "rpm set!" is briefly displayed and the user is returned to the Frequency display.
The Duty Cycle menu operates in much the same way as the Units and Rate menus. When initially selected, "Duty Cycle Menu" displays at the center of line 1 and a duty cycle option displays at the center of line 2. Rotating the encoder knob cycles between the duty cycle options.
The duty cycle options are:
Because of the way that the duty cycle is set, the above rates should be accurate to approximately 1 percent if the DDS module's output is close to 1 Volt peak to peak (measurements on several modules of different manufacture indicates that this generally true).
The construction of the main stroboscope assembly is described in the Physical section above. This section describes a simple enclosure made for this assembly. The enclosure also allows for mounting of the incremental encoder and its knob.
The enclosure is built in two parts: the main enclosure case (case) and the mounting bracket (bracket) for the strobe assembly. These are described in the following sections.
The incremental encoder and knob mount in the hole on the top plate of the enclosure. The strobe assembly mounts on the enclosure bracket. The bracket and its strobe assembly slide into the case and is fastened to the case back with screws. The following sections describe the mounting in more detail.
Photo 4 shows the DSTRO case. The bottom, sides and front of the case are built with 1/8" thick plywood veneer (labeled "Luan finish plywood sheet" at my local Home Depot). This material is very inexpensive and easy to cut. It can be stained to provide an attractive wood-finish case.
The top cover of the case consists of a 4 1/4 inch by 5 1/4 inch (108 by 134 mm) piece of clear acrylic plastic that is 1/8 inch thick. The hole for the encoder was centered on the cover and drilled about 1 1/2 inches (38 mm) up from its bottom edge.
The top cover was left clear to show the placement of the strobe assembly and encoder cabling in the enclosure. The enclosure could be dressed up a bit by putting a paper under on the back side of the top cover with cutouts for the LCD and the encoder shaft.
A suggested underlayment material is heavy matte finish photo print stock that can be pre-printed with project labels. Front Panel Express allows accurate layout of the underlayment, including precise positioning of the LCD cutout and hole for the encoder shaft. The fonts, sizes and colors are sufficient for professional-looking labels.
The bottom of the case is 4 inches (102 mm) wide and 5 inches (127 mm) long. The two sides are 5 inches (127 mm) long. The back edge of each side is 2 5/8 inches (67 mm) high and the front edge is 7/8 inches (23 mm) high. A small front piece matches the height of the front edges and spans the entire front to hide the bottom and front edges. On the case shown in the photos, this piece measures approximately 4 7/16 inches (113 mm) but should be cut to size so that it just covers the three front edges of the case. This front piece also helps keep the sides at right angles to the bottom piece.
The bottom and side pieces were glued together with E6000 adhesive using corner clamps to maintain the pieces at right angles during gluing. The E6000 is more "forgiving" than wood glue in that mis-glued sides (don't ask) can be taken apart, cleaned up with gentle rubbing and re-glued. Also, the sides may be bent slightly so to ensure that they are at precise right angles. The front piece can be glued on after the sides are adjusted to precise right angles with wooden spacers (e.g., wooden craft sticks).
After the sides, bottom and front are glued together, the bottom part of the case should be quite rigid and durable.
Before glueing, the plastic front was temporarily taped in place with blue painters' masking tape. This keeps the front accurately and securely positioned during gluing and allows glueing along the front panel edges in several stages.
The plastic front was fitted to the bottom of the case such that it does not extend fully to the side and front pieces. Because of the thickness of the wood sheet that was used for this particular case, the front did not have to be trimmed. This may not always be the case -- it is suggested that the plastic front be cut after the bottom is finished.
This method of front panel placement allows the panel edges to be glued with a bead of E6000, avoiding the problems associated with applying adhesive to the case bottom and then positioning it and securing it. That method is feasible, but somewhat difficult to do without smearing glue on the bottom of the acrylic.
Glue on the inside of the case is very difficult to remove because of the relatively small dimensions of the inside. Thus, the front panel glue-up is perhaps best performed by first taping the panel to the case and then running glue beads on the outside in several stages. Initially, several small dots of glue were used in the corners to hold the panel in place without tape. Then, after the glue dots were dry, the glue beads were applied.
Any unwanted glue adhering to the top or sides can be easily be removed after drying by gentle rubbing (e.g., with your fingers or a soft cotton cloth).
Note that, while the resulting case is relatively attractive and easy to build, it provides no shielding for the high frequency components such as the DDS module and the MINT processor. If noise from the processor and DDS is a problem, a metal case is advised.
The bracket assembly evolved from the DSTRO prototype that was mounted on a base consisting of a scrap piece of Luan veneer. For the prototype, the strobe assembly was mounted at an angle on the base with unequal-height standoffs screwed on the proto board edges. The standoffs were glued to the base with E6000. The encoder mounted separately on a scrap piece of perforated board mounted to the strobe proto board.
This mounting technique for the strobe assembly was used when designing the "production" enclosure. For the finished strobe, the base plate was attached to a back plate that mounts to the back part of the case. This provides a complete enclosure and a way of securely mounting the strobe assembly to the case. Thus, the base plate and back plate, glued together, form what is called the "bracket" in the following text.
The base plate for the bracket consists of a piece of scrap fiberglass circuit board 4 inches (102 mm) wide by 3 inches (76 mm long). It is glued at right angles to a Luan backplate measuring approxmiately 4 inches (102 mm) by 2 3/8 inches (60 mm). Using a scrap piece of Luan plywood for the base would be more consistent with the construction of the rest of the case. However, the height of the case may have to be extended a bit for best fit with a thicker base.
The back plate is cut so that it fits exactly inside the back of the case. Thus, the base plate surface is glued flush with the bottom edge of the back plate.
Before glueing the base plate to the back plate, it is best to drill all of the holes for the back plate. It needs holes for mounting screws and the connector mounting plate.
First, drill the mounting screw holes. The back plate fastens to the enclosure with two screws placed about half way up the back side. This is shown in Photo 6. The screws fasten to standoffs glued to the sides of the case. In the photos, the standoffs are shown offset from the sides of the case with two wooden shims (broken pieces of wooden craft sticks).
This mounting method can be improved by cutting two pieces of the Luan stock so that they fit on the side pieces. This not only provides a cleaner appearance but is probably easier to implement. It also makes it much easier to determine the exact vertical placement of the standoffs and the matching back plate screw holes.
After cutting the shims, mark the vertical placement of the threaded standoffs. Glue the shims, followed by the standoffs. The standoffs should be positioned with the vertical placement marks and adjusted so that they are exactly perpendicular to the back of the bracket. This can be done easily with a small piece of Luan stock with exact corners (e.g., cut with a chop saw and checked with a square).
When gluing a standoff, place the case on its side and glue the standoff in place with a very small spot of glue. The purpose of this is to place the standoff, not glue it permanently in place. After the glue spot has set up, verify the standoff placement in relation to its twin and to its desired vertical placement. After verifying the standoff placements, glue them permanently in place with a liberal glue bead extending down both sides of the standoff.
The standoffs used in the enclosure shown were 1/2 inch (12 mm) long but longer standoffs are even better in providing a strong mounting. When doing the final glue-up on the standoffs, be careful not to get any glue on the standoff ends: this may interfere with the mounting screws. A small piece of paper can be glued on the back end of the standoff to keep glue out while allowing the back end to be used for glueing. Again, E6000 is very forgiving and re-glueing is not a problem.
Once the standoffs are permanently in place, mark the back for the holes in the back plate that pass the mounting screws. Depending on how tightly the back plate fits into the case, horizontal mounting screw placement can be tricky. A tight fit allows the most accurate placement (e.g., with calipers or a paper template). If the back plate fit is looser, the edge play must be accounted for. This is not an onerous task and mistakes can often be corrected by enlarging the mounting holes. The back plate can also be easily re-cut at this stage.
After drilling the mounting screw holes, drill the holes for the connector plate. The connector plate mounts with standoffs on the face of the back board (see Photo 6). The connector plate provides mounting for a polarized connector for the 5 Volt power input. It also provides mounting for a two-terminal screw-down terminal block for the wires connecting to the strobe LED (or head).
The plate measures about 1 inch (126 mm) by 1 3/4 inch (145 mm). It is cut from a scrap piece of perf board and corner mounting holes are drilled using the tenth-inch spaced solder pads as a drill guide. The connector plate mounts approximately 1/2 inch (13 mm) in from the edge of the back plate and centered vertically but placement is not critical.
Four holes in the corners of the perf board are required for screws and standoffs mounting the perf board to the back. A 1/4 inch (6 mm) hole drilled under the connector board passes connector wires from the strobe assembly to the connector board.
The next stage in preparing the bracket is to glue the back and bottom pieces together.
Because there is no corner support for the bottom and back pieces of the bracket, the bracket was "stiffened up" with small metal right angle brackets glued in the corners. In retrospect, it would probably be best to glue the back and bottom together with a small length of aluminum angle stock. This would make glue-up easy and would also improve the strength and rigidity of the bracket assembly.
The bottom and back pieces were glued at right angles using corner clamps (picture framing clamps) and E6000, just like the case sides.
Once the back and sides are glued together, it is time to place and mount the main strobe assembly on the base of the bracket.
The main strobe assembly is attached to the bracket base plate by gluing metal standoffs to the base plate at an angle. This angle is roughly the same angle as the case face plate. The standoffs are mounted on the corners of the strobe assembly's proto board. An edge of the MINT processor board holds up the back of the assembly and establishes the slope of the mounting.
Once the angle of the assembly is established as approximately the same as that of the case faceplate, the standoffs are glued in place with E6000. Note that the angle of the LCD and that of the faceplate do not have to match exactly. The LCD is held in place only by the socket along one edge. Otherwise, it is free to move slightly up and down in relation to the face of the proto board. When the bracket assembly is pushed into the case, the LCD will adjust so that it is flat against the faceplate, exactly matching the faceplate angle.
The exact placement of the strobe assembly on the bracket bottom requires some trial and error. The glue points for the standoffs must be visually determined by pushing the bracket assembly into the case, while the standoffs are loose. After the LCD is exactly in place with the back plate fitting flush, the glue points for the standoffs can be observed.
This process can be made more precise by taping a small piece of grid paper on the base plate before a trial fitting. This allows positioning of the standoff glue points in relation to known grid points.
After removing the bracket assembly, the standoffs can be glued down. Because E6000 can be removed easily, a mistake in fitting can be easily rectified.
The last mounting stage is to hook up the wires coming from the main strobe assembly. These consist of the wires going to the back connector plate and the wires going to the encoder/switch assembly.
The DSTRO prototype used on on-board polarized power connector. Connecting with this was not feasible because of the size of the mating power connector. To reduce the connection footprint, relatively heavy stranded wires were soldered directly to the main strobe board and routed out to the back panel through the wiring holes. These wires were soldered directly to a polarized connector soldered to the connector board.
On the main strobe assembly, the strobe LED connector consists of two right-angle stake pins. These connect to the strobe LED wires with a two-conductor molex socket. The strobe LED wires are routed to the connector board in the same way as the power wiring.
The wires are soldered to a two-position terminal block soldered to the connector board. The LED polarity was marked with a felt-tip marker on one of the terminals. The use of the terminal block connection allows the strobe head wiring to be as long as needed. To reduce stress on the strobe LED wires, they are tied to one of the connector board's standoffs with a tie wrap.
For the DSTRO prototype, the encoder and switch connections to the main strobe assembly were implemented with a 6-pin molex socket and right angle stake pins.
The prototype's connection cable was not long enough to allow connecting to the strobe assembly with the bracket assembly removed from the case. Also, the molex connector interfers somewhat with the standoffs used to mount the back plate to the case.
To extend the encoder/switch connection and lower the connector footprint, wire wrap wires were wrapped directly to the board connector. On the other end, the wires are soldered to a row of six stake pins that fit into the molex connector. A better implementation of this connection would be to wire wrap to the board connector and directly solder the ends to the encoder and switch.
With this approach, the encoder/switch assembly is permanently attached to the strobe assembly. This shouldn't be much of a problem except during switch mounting. To avoid problems during mounting, make the wires to the encoder/switch assembly long enough to allow holding the encoder/switch assembly in place (e.g., by hand) without interfering with the main strobe assembly. A length of 6 inches (152 mm) should be sufficient.
One of the project objectives is to provide a template for projects using the C8051F850 chip, an LCD, an incremental encoder and a DDS chip. This covers a number of possible projects, but the most common application is for projects requiring an accurate and wide-range variable frequency oscillator.
For MyForth users, the stroboscope project illustrates the use of various code modules used for the main components, including a menuing system to integrate component functionality.
The following describes some of the features of code modules that can serve as a starting point for future projects. It also provides some additional coding examples for new MyForth users, thus providing additional examples beyond those given in the MyForth manual.
The following sections are not presented in any particular logical order, but do generally correspond to the ordering in the application load file, job.fs . We recommend that you download the DSTRO application software and print out (or view) the examples as they are discussed.
############ # Sample 1 # ############ \ job.fs \ false constant tethered true constant turnkeyed false constant standalone ############ # Sample 2 # ############ \ \ -----[ include interpreter(s) ] tethered [if] include mide-tether.fs [then] standalone [if] include dacs-standalone.fs [then] ############ # Sample 3 # ############ \ -----[ patch the reset vector ] turnkeyed [if] \ Turnkeyed with go. start interrupt : cold stacks go ; \ patch reset to go [else] \ Testing with quit. start interrupt : cold stacks quit ; [then] \ \ -----[ select interactive or standalone ] tethered [if] \ interactive number entry :m # tnumber emit-s m; :m ## [ dup 8 rshift $ff and swap $ff and ] # # m; [then] \ standalone [if] \ standalone - patch dictionary pointer \ Note: see config.fs -- headers are tacked on (moved) to the end of code by "headers" : r cold ; headers ] here [ dict org heads ##p! org ] [ .( heads start at: ) heads . .( size: ) ] here [ heads - . .( bytes) ] here ( *) patch-cold org cold ( *) org \ patch cold (after end of heads) [then]
Figure 1. Configuration Options
The file job.fs is the application load file. It performs basic chip configuration and loads the application software files. Figure 1 lists this file.
As shown in Figure 1, initial code in the job.fs file establishes compile options such as tethered, turnkeyed or standalone. In this case, the application is turnkeyed so that the application automatically comes up at reboot. The turnkey Word, "go", is defined in main.fs . Normally, initialization is consolidated in the "go" Word so that everything is configured correctly at startup (or reset).
Because this project uses an LCD menuing system, there is no need for a standalone interpreter (and serial port). The tethering option is useful during development (i.e., using the C2 interposer), but is not used for the final application.
Also shown in Figure 1 are several code snippets, each with a "Sample" heading. These show the control of job loading, conditional compiling and configuration. Where several code snippets are included in a single figure, they are each introduced with a sequential Sample label.
The source code file is named at the top of the figure. Closely related code snippets from diffent files may be presented in the same figure. In this case, the name of the file is given just below the "Sample" label.
Samples 2 and 3 show how the load options are used to conditionally compile related code.
Sample 2 shows how the tethered and standalone interpreters are compiled. The names of the interpreter files, mide-tether.fs and dacs-standalone, are (in this case) the latest versions from other applications. When used, the file names typically change if there are significant modifications for the current application.
Sample 3 shows how the configuration options are used to do post-compilation operations such as patching the reset vector so that it calls the "go" Word for turnkeyed applications.
For applications using the standalone interpreter, the headers for each definition must be moved to the end of the application code in flash memory. Normally, the header information is created as the application is compiled in a separate memory space; when the application is finished, the heads are moved to a location in flash memory.
\ job.fs \ \ -----[ configure for 850 Dev ] $200 constant start \ Reset vector. Bootloader at $0000. $20B constant TIM0 \ timer 0 interrupt $21B constant TIM1 \ timer 1 overflow interrupt $223 constant RI \ serial receive interrupt $22B constant TIM2 \ timer 2 overflow interrupt $233 constant rom-start \ start of code ($22b + $08) $1fff constant target-size \ 8K flash \ true constant cs? \ produce compilation summary? \ \ -----[ load MyForth system ] include mide-loader.fs \ \ -----[ define SFRs, generate bootloader image ] include sfr85x_140422.fs include bootloader850_140422.fs
Figure 2. Chip Configuration
Figure 2 mostly shows the chip configuration code. The top of the figure shows the definition of interrupt vectors and compilation constants for the 850 chip. The first part defines the re-mapped location of interrupt vectors to accommodate the MyForth bootloader (not discussed here). The "rom-start" definition shows the start address of compiled application code. The "target-size" defines the size of flash for the 850 chip (8K).
A constant, "cs?" controls the appearance of the compilation statistics. If this is true, the total memory usage for each file is displayed when the application is compiled.
The statement "include mide-loader.fs" loads the MyForth system, including the assembler, MyForth, the disassembler and (if used) serial port definitions. The "mide" prefix just indicates that it was copied from a previous application (in this case, the MIDE C2 compiler). Note that the loader file includes several other files such as misc8051.fs (the assembler and Forth system) and dis5x.fs (the disassembler).
The next statement, "include sfr85x.fs", loads the special function register definitions for the 850 chip. These are derived from the C source file distributed by Silicon Laboratories, Inc. The following statement loads the MyForth bootloader modified for the 850 chip. The bootloader is discussed more fully in the MyForth reference manual.
\ job.fs include 25_delays.fs \ 25 MHz delays include init.fs \ chip initialization, I/O primitives include utils.fs \ misc. utils -- load before application \ \ -----[ include interpreter(s) ] tethered [if] include mide-tether.fs [then] standalone [if] include dacs-standalone.fs [then] \ \ -----[ load the application ] \ \ strobe.txt \ text link to doc file \ [ cr ] \ whitespace for compilation summary include xors.fs \ xor macros (e.g., for case statements) include lcd16.fs include memory.fs \ direct cell, flash & xram memory access include regs.fs include xvars.fs \ variables in xram with flash save/restore include ie.fs include timer0.fs include rates.fs \ rate tables include duty.fs \ duty cycle table include multiply.fs \ multiple byte multiply by a constant include qdigits.fs include freqs.fs \ calculate dds load values include 9850.fs \ 9850 DDS driver include intervals.fs \ encoder menu intervals include updates.fs \ frequency, LCD updates include menu.fs \ lcd menuing system include main.fs \ initialization, go definition \ include interactive.fs \ Should come _after_ application for efficiency. [ cr cr ]
Figure 3. Application Loading
Figure 3 shows how the application is loaded by "including" various code modules. Some of these are discussed in more detail in later sections.
Note that the applicaion includes drivers for the major stroboscope components such as the incremental encoder, the DDS module and LCD. Also included are modules for the menuing system, digit display (in qdigits.fs) and saving parameters to flash memory (in memory.fs).
\ 25_delays.fs \ :m delay765 ( n -) \ ~8.1 ms with n=1 and 24.5 MHz clock 7 #for 0 # 6 #for 0 # delay5 6 #next 7 #next m; \ :m |us ( n - ) 7 #for 6 # 6 #for 6 #next 7 #next m; \ 100 # us -> 90 usec ( adding nop in inner loop -> 114 usec) : us ( n - ) [ 7 push 6 push ] |us [ 6 pop 7 pop ] ; :m |ms ( n - ) 7 #for 100 # 6 #for 81 # 5 #for 5 #next 6 #next 7 #next m; : ms ( n - ) [ 7 push 6 push 5 push ] |ms [ 5 pop 6 pop 7 pop ] ; \ 0 [if] \ -------------------[ double number raw delay ]------------------------ 1. Double number delay (100 ## ##delay =~ 229 usec) 2. Note that an interrupt service delay may interfere significantly with this delay routine at low microsecond values. [then] \ ---------------------------------------------------------------------- :m ##delay ( d - ) \ double number delay 0 # begin drop -1 ## |d+ |over |over ior 0=until drop drop drop m;
Figure 4. Delays
At the first part of the application load file is 25_delays.fs which defines various delays based on a the 850 chip's internal 25 MHz clock. This file is routinely loaded for any application using the internal clock and requiring delays expressed in microseconds and milliseconds. The code uses straightford application of delay loops or nested delay loops to provide a variety of ways to express delays. Figure 4 shows typical definitions.
Note that most of the delays are defined as macros. Macros use memory only when they are invoked within a Word. Some macro definitions are preceded with "|" to indicate that the definition is a macro. In the case of the definition of "ms", the "|ms" macro is used within a definition, along with code that preserves various direct cells. Thus, "ms" is the safe definition and the "|ms" definition can be used to avoid some memory usage and execution overhead where there is no register conflict.
The source file for these delays contains additional notes on timing and compilation.
\ init.fs 0 [if] ---------------------------[ Notes ]------------------------------------ 1. Configures 850 chip on MINT board. 2. To be called as part of cold so that after a reset the hardware returns to these settings 3. If a bit needs changing in the application, "and/orl" it. 4. 850 I/O assignments for MINT Board: MINT Port Pin Type Function ---- --- ---- -------------------------------------------------------------- P0.0 7 PP DDS wclk P0.1 5 PP DDS frud (FQ_UD) P0.2 3 PP DDS data P0.3 4 PP DDS mrst P0.4 6 PP cpt0 output -- drives strobe LED (disconnect on-board LED1) P0.5 8 AI cpt0 negative input (normally reserved for serial input) P0.6 10 AI comparator positive input P0.7 12 PP PWM output from PCA0 P1.0 14 PP LCD DB4 P1.1 16 PP LCD DB5 P1.2 18 PP LCD DB6 P1.3 20 PP LCD DB7 P1.4 22 DI IE B P1.5 21 PP IE A P1.6 19 PP LCD E P1.7 17 PP LCD RS P2.0 15 DI incremental encoder switch P2.1 13 PP unassigned 2,9,24 GND 1 VDD (5 Volts) 13 RST/C2CK (to prog debug adapter) \ [then] \ ----------------------------------------------------------------------
Figure 5. I/O Documentation
Figure 5 lists the part of the init.fs file that documents the chip I/O. This provides a handy reference for the I/O coding that takes place later in the file.
Note that the entire documentation block is enclosed by a starting with "0 [if]" and and ending with "[then]" to comment out the block. This combination is a feature of GForth and is frequently used to document code within a file, patch out test code or conditionally compile a code block (e.g., "1 [if] ... [then]" unconditionally compiles the enclosed code).
\ init.fs \ 1 [if] \ --------------------------[ IE ]-------------------------------------- \ :m ie-switch [ 0 .P2 ] m; \ encoder switch :m iea [ 5 .P1 ] m; \ encoder phase a :m ieb [ 4 .P1 ] m; \ \ switch reading, debounce, etc. \ \ :m 80ms-delay $0a # delay765 m; \ debounce delay -: 80ms-delay 80 # ms ; \ : test-80ms begin 80ms-delay ~cbit again \ \ --- continue on switch up and down -: ?ie-up begin 80ms-delay ie-switch until. ; \ continue if ie-switch is up -: ?ie-down begin 80ms-delay ie-switch 0=until. ; \ continue if ie-switch is down -: ?switch-up ?ie-up 80ms-delay ; \ with debounce \ [then] \ ----------------------------------------------------------------------
Figure 6. I/O Configuration
Figure 6 lists part of the initialization file containing definitions for configuring chip I/O. This includes mnemonic naming for signals relating to particular resources (e.g., "ie-switch" for the incremental encoder's switch input). Later sections discuss the incremental encoder in more detail.
\ init.fs :m //pca $08 # PCA0MD #! \ PCA0 clock source is SYSCLK $00 # PCA0CN #! \ set bit 6 to run PCA0 (init to stop) \ $c2 # PCA0CPM0 #! \ PWM enabled (bit 1), cmp0 function enable (ECOM bit 6), \ 16 bit PWM (bit 7) $42 # PCA0CPM0 #! \ no 16-bit PWM \ $00 # PCA0PWM #! \ default -- 8 bits if no 16 bit PWM \ 8-11th bit overflow int. disabled \ $00 # PCA0POL #! \ default (normal polarity) \ $00 # PCA0CLR #! \ default m; \ :m +pca0 [ 6 .PCA0CN set ] m; :m -pca0 [ 6 .PCA0CN clr ] m; :m normal-polarity $00 # PCA0POL #! m; :m invert-polarity $01 # PCA0POL #! m; \ \ ----- 16-bit \ -: !pwm ( d -) push PCA0CPL0 #! pop PCA0CPH0 #! ; \ -: /pwm $8fff ## !pwm +pca0 ; \ \ ----- 8-bit -: !pwm ( n -) PCA0CPH0 #! ; -: /pwm $b0 # !pwm ( invert-polarity) +pca0 ;
Figure 7. PWM Using PCA0
Figure 7 shows the implemention of an 8-bit Pulse Width Modulator (PWM) using Programmable Counter Array 0 (PCA0). The PWM output is applied to pin P0.7 and filtered by R8 and C1. This produces a DC voltage, as shown in Schematic 1. The DC voltage from the PWM filtering network is applied to the negative input of Comparator 0 (CMP0) to set the duty cycle. This is explained in more detail in later sections.
For now, it is only necessary to know how the PWM is set up. Most of the PCA initialization is defined in the macro named "//PCA", given in Figure 7. The action of the values loaded into each of the PCA registers is given in the comments. Note that some statements are commented out with a "\" command. Code from the comment symbol to the end of the line is ignored.
Code for a 16-bit PWM is shown. Although this has been verified, the 8-bit PWM provides adequate voltage resolution and the higher output frequency is easier to filter.
Using the 25 Megahertz system clock (SYSCLK) as the source for the PCA results in a PWM signal of about 96 kHz. The output frequency for the 16-bit PWM is about 400 Hertz. The init.fs file contains a table of output voltages versus PWM values over the range from 0 to 255. The file also has an example showing a simple test routine that uses the incremental encoder to change the PWM value while displaying the result on the LCD.
Following the PCA initialization routine are several definitions to allow control of the PWM. The macros "+pca" and "-pca" turn the PWM on and off, respectively. The macros "normal-polarity" and "reverse-polarity" change the PWM polarity. The Word "/pwm" initializes the PWM -- it is used in "/chip" which is the Word that initializes the chip.
\ init.fs 0 [if] \ -------------------[ I/O initialization ]----------------------------- 1. Steps for port I/O initialization, per datasheet: Step 1 - Select input mode (analog or digital) for all port pins using PnMDIN. If the pin is in analog mode, a "1" must also be written to the corresponding Port Latch. Default for PnMDIN is $ff (pins not configured as analog inputs). Step 2 - Select the output mode using PnMDOUT. Output Modes - 0=open drain, 1=push pull Step 3 - Assign port pins to desired peripherals using XBRn registers. Step 4 - Enable the crossbar (XBARE="1") 2. Pins used as comparator or ADC inputs should be configured as analog inputs. 3. Analog input pins should be skipped by the crossbar. 4. All analog pins must have a "1" set in the corresponding Port Latch reg. [then] \ ---------------------------------------------------------------------- -: /chip $00 # XBR2 #! \ disable xbr \ \ -----[ configure I/O ] \ \ ----- port 0 $6f # P0SKIP #! \ skip P0[0-3], P0[5,6] \ cmp0- on P0.5 cmp0+ on P0.6, cmp0 out on P0.4 $56 # CPT0MX #! \ mux: cmp0= to P0.5, cmp0+ to P0.6 $9f # P0MDIN #! \ P0[5,6] analog input $9f # P0MDOUT #! \ P0[0,1,2,3,4] PP, P0[5,6] DI, P0.7 PP \ $8f # P0MDOUT #! \ P0[0-3] and P0.7 are outputs $80 # CPT0CN #! \ enable cmp0, no hysteresis \ read bit 6 to get output state $00 # CPT0MD #! \ cpt0 fast, high pwr (100 ns response) \ \ ----- port 1 $cf # P1MDOUT #! \ P1[0,1,2,3,6,7] PP, P1[4,5] DI \ \ ----- port2 $02 # P2MDOUT #! \ P2.1 is PP for strobe LED, P2.0 is switch input $00 # CLKSEL #! \ 24.5 MHz internal clock \ $07 # PRTDRV #! \ enable all as high drive (default) //pca \ \ ----- crossbars \ $08 # XBR0 #! \ enable cmp0 output, on P0.4 $01 # XBR1 #! \ CEX0 routed to port pin (for PWM), on P0.7 $40 # XBR2 #! \ enable XBR, weak pullups enabled \ \ -----[ init application I/O ] \ [ ( IE) iea set ieb set ie-switch set mint_led set ( off) ( DDS) mrst clr wclk clr frud clr ] ;
Figure 8. Chip Initialization
Figure 8 shows the main chip initialization. The comments should explain most of the code.
The statement "$6f # P0SKIP #!" sets the crossbar skip register to skip over port zero pins 0 to 3 (this is P0[0-3] in my notation). This is needed to reserve these pins to drive the DDS module. It results in the output of the first comparator (CMP0) appearing at P0.4 . This output drives the FET (Q1) controlling the strobe LED (LED3 on the schematic).
Also skipped are pins 5 and 6 of port 0. This allows the comparator inputs to be assigned to these pins. Note that the positive comparator input is connected to the DDS output. This output is a one-volt peak to peak sine wave that varies from a few millivolts to 1 Volt positive. It is connected to P0.6, the positive input of the comparator, by jumpering pins 2 and 3 of PL8 (see Schematic 1).
With the given skip configuration, the output of the comparator appears at P0.7 (the input to the filter formed by R8 and C1). The comparator inputs are configured as analogs, per the datasheet. The DDS drive signals, the strobe LED drive and the comparator output are configured as outputs (e.g., with statements like "$9f # P0MDOUT #!").
Port 1 is mostly configured as for output, except for pins 4 and 5 which are digital inputs for the incremental encoder phases. Port 2, pin 0 (P2.0) is configured as a digital input to sense the switch on the incremental encoder.
Bits are set in crossbar registers XBR0 and XBR1 to enable the comparator and PWM outputs on port pins (i.e., P0.4 and P0.7). Weak pullups are enabled on XBR2 to provide a weak pullup for pins that need it (e.g., P1.4, P1.5 and P2.1).
The final code section, enclosed in brackets, invokes the assembler (via GForth statements) to set the I/O to a known state at startup. Also, inputs must be initially set for them to work. This is done with statements like "ie-a set", "ie-b set" and "ie-switch set" to ensure that the incremental encoder inputs work correctly.
\ lcd16.fs -- 16-pin lcd driver for 850 -- 15Feb14 rjn \ \ -----[ LCD I/O ] :m pins P1 m; :m dirs P1MDOUT m; :m .e [ 6 .P1 ] m; :m .rs [ 7 .P1 ] m; -: instruction [ .rs clr ] ; -: data [ .rs set ] ; \ \ 160 us delay may be ok at half the value given -: strobe [ .e set ] 100 # us [ .e clr ] 160 # us 160 # us ; \ \ note: $c4 swaps nibbles in the accumulator in one cycle \ -: lcd ( c - ) $b0 # pins and! \ output high nibble first dup $f3 # and [ $c4 ] , pins ior! strobe $b0 # pins and! \ now output do low nibble $3f # and pins ior! strobe ; \ -: /lcd \ instruction $30 # pins #! \ RS=0, instruction mode, 8-bit, 1/8 duty, 5X7 $cf # dirs ior! \ configure pins as outputs 30 # ms \ power on delay $33 # pins #! \ initialization pattern strobe 10 # ms strobe strobe $32 # pins #! strobe 10 # ms $28 # lcd 10 # ms $0e # lcd $01 # lcd 10 # ms $02 # lcd data 10 # ms ;
Figure 9. LCD Initialization
Figure 9 shows LCD initialization. This is performed with the Word "/lcd" (most initialization Words begin with "/" or "//"). The initialization sequence was copied from an LCD datasheet and has been in use for many years. The code is relatively straightforward. The "strobe" Word pulses the "E" pin on the LCD. The "lcd" Word sends a character to the LCD data pins one nibble at a time. Note that the LCD I/O is set up in the initalization module (init.fs).
\ lcd16.fs \ \ -: lcd-command lcd data 2 # ms ; \ -: 0lcd instruction 1 # lcd-command ; \ reset lcd -: lcd-home instruction 2 # lcd-command ; \ home cursor -: lcd-off instruction 8 # lcd-command ; -: lcd-blank instruction $0B # lcd-command ; -: lcd-coff instruction $0C # lcd-command ; \ display on, cursor off -: lcd-don lcd-coff ; \ display on -: lcd-blink instruction $0D # lcd-command ; \ blinking char at cursor -: lcd-con instruction $0E # lcd-command ; \ cursor on -: lcd-dblink instruction $0F # lcd-command ; \ display, cursor, blink on -: lcd-con instruction $0E # lcd-command ; \ cursor on -: lcd-cleft instruction $10 # lcd-command ; \ move cursor left -: lcd-cright instruction $14 # lcd-command ; \ move cursor right -: lcd-left instruction $18 # lcd-command ; \ scroll display one pos left -: lcd-right instruction $1C # lcd-command ; \ scroll display one pos right -: line1 instruction $80 # lcd-command ; -: line2 instruction $C0 # lcd-command ; \ -: home&blink lcd-home lcd-blink ; \ \ --- send blank(s) to display \ : _lcd string " " -: _lcd $20 # lcd ; -: n_lcd ( n -) [ 4 push ] 4 #for _lcd 4 #next [ 4 pop ] ; \ n blanks \ -: /line1 line1 16 # n_lcd line1 ; -: /line2 line2 16 # n_lcd line2 ;
Figure 10. LCD Functions
Figure 10 presents the Words used to control data output to the LCD. Most of these Words control the cursor position and/or type. The control Words put the LCD in the instruction mode and send the instruction byte using the "lcd-command" Word.
The Word "_lcd" sends a blank to the display. The Word "n_lcd" sends multiple blanks to the display, with the number to send is on the stack. The Words "/line1" and "/line2" clear (initialize) line1 and line2 by positioning the cursor and sending 16 blanks.
\ lcd16.fs :m lcd-type ( da) p! |@p+ begin |@p+ lcd 1- 0=until drop m; \ def. below may also be in serial-string.fs or utils.fs :m " 34 parse here there place here there [ c@ 1 + ] allot m; -: lstring pop pop swap lcd-type ; \ -: "zero lstring " 0" \ --- sample string \ : greet lstring " Hello Handsome!" \ Example: /chip /lcd greet \ \ -----[ HEX Display ] \ : (digit) -10 # + -if -39 # + then 97 # + ; \ in debug.fs -: lcd.digit (digit) lcd ; -: lcd-sign -if negate 45 # lcd ; then ; \ : u/mod |u/mod ; \ Avoid leading zeroes in (u.). \ : three digit \ : two digit digit ; \ : (u.) 10 # u/mod 10 # u/mod if three ; then drop if two ; then drop \ digit ; -: u.lcd 10 # u/mod 10 # u/mod lcd.digit lcd.digit lcd.digit ; -: h.lcd 16 # u/mod lcd.digit lcd.digit ; -: (lcd.cells) [ 4 push ] 4 #for @+ h.lcd 4 #next [ 4 pop ] ; -: lcd.4h 4 # (lcd.cells) ; -: lcd.cells ( adr n -) swap a! (lcd.cells) ; \ defined in send-utils.fs \ : 10000s 0 # begin push -10000 ## d+ -if 10000 ## d+ pop ; then pop 1+ again \ : 1000s 0 # begin push -1000 ## d+ -if 1000 ## d+ pop ; then pop 1+ again \ : 100s 0 # begin push -100 ## d+ -if 100 ## d+ pop ; then pop 1+ again -: ud.lcd 10000s lcd.digit 1000s lcd.digit 100s lcd.digit drop 10 # u/mod lcd.digit lcd.digit ; 1 [if] \ ------------------[ display stack on LCD ]---------------------------- \ -: l'empty lstring " empty!" -: .hexes 4 #for @+ _lcd h.lcd 4 #next ; -: .hexes1 S #@ 1 # + a! .hexes ; -: .hexes2 S #@ 5 # + a! .hexes ; -: (do-line) depth 5 #
] # lcd ; -: s.lcd 0lcd s.hdr depth 0=if drop l'empty ; then drop do-line ?line2 ; \ must precede with line1 or line2 for placement -: ss.lcd s.hdr depth 0=if drop l'empty ; then drop do-line ; \ [then] \ ----------------------------------------------------------------------
Figure 11. LCD Utilities
Figure 11 shows the definition of various LCD utilities, such as string and numeric output. Also shown are utilities to display the stack, which is useful during debugging. The stack display is enclosed within a conditional comment field so that it can be removed for the final application.
The Word """ (quote) parses a text string, defined at compile time, and places it in flash. The location in flash is just after the definition of the string Word (e.g., "greet" in the example). At execution time the address of the string Word, a two-byte double number, is on the processor's stack (not the MyForth stack). The Word "lstring" extracts this address from the stack (with pop pop) and swaps the bytes to get them in the proper order. The Word "lcd-type" loads this address into the data pointer (with p!) so that the bytes can be sequentially accessed and output to the LCD. The "greet" example shows typical string usage.
Numeric display can be performed either in hex or decimal. Numbers can be single bytes or double numbers (16 bits). Decimal output is based on "(digit)" a utility Word usually defined in the debug.fs file. The Word "lcd-digit" outputs a single decimal digit to the display at the current cursor location. The Word "u.lcd" displays a digit as an unsigned number. The Word "h.lcd" displays a byte as a two-digit hexadecimal number.
The "lcd.cells" Word allows any number of direct cells to be displayed. This is useful when displaying quad (four byte) or double quad (eight byte) numbers stored in direct cells.
The "ud.lcd" Word displays an unsigned double number (16 bits).
The "s.lcd" Word displays the contents of the stack on two lines. The display starts with the number of stack items followed by up to the first nine items. Stack items are displayed with the top of stack first (adjacent to the number of items).
Often, only the first few stack items are of interest. Also, it is often useful to display application output on one of the LCD lines while displaying the stack contents on another line. The Word "ss.lcd" displays a shortened stack display (four items) on either one of the LCD lines. To use this Word, the cursor must first be positioned to the start of the line selected for stack display.
\ ie.fs \ \ ----- [ allocate ie accumulators, variables ] \ cpuHERE constant ie-acc 4 cpuALLOT \ ie-acc is MSB, ie-acc3 is LSB :m ie-acc1 [ ie-acc 1 + ] m; :m ie-acc2 [ ie-acc 2 + ] m; :m ie-acc3 [ ie-acc 3 + ] m; :m |0ie-acc ie-acc # zero4 m; -: 0ie-acc |0ie-acc ; \ use these in main loop -: @ie-acc ( - q) -int ie-acc # @quad< +int ; -: !ie-acc ( q -) -int ie-acc # !quad +int ; :m |option ie-acc3 m; :m |option1 ie-acc2 m; -: @option ( - n) |option #@ ; -: !option ( n -) |option #! ; -: @option1 ( - n) |option1 #@ ; -: !option1 ( n -) |option1 #! ; -: @doption @option @option1 ; \ get last two bytes of ie accumulator -: !doption ( lsb msb - ) [ ie-acc 2 + ] #! |option #! ; \ 1 [if] \ ----------------------[ menu utilities ]------------------------------ \ -: ?activity \ exits if switch or ie activity @option dup begin drop @option |over xor cmd? ior until drop drop ; \ -: ?changed \ see if anything has changed ?activity \ wait for switch or option change cmd? if drop ; then drop ; \ exit if change is switch \ [then] \ ----------------------------------------------------------------------
Figure 12. Incremental Encoder Accumulator
Figure 12 shows the definition of the 32-bit accumulator for the incremental encoder. The statement "cpuHERE constant ie-acc 4 cpuALLOT" allocates four direct cells from CPU RAM. Following macros such as ":m ie-acc1 [ ie-acc 1 + ] m;" name each of the cells so that they can be accessed individually.
The Word "0ie-acc" zeroes the cells (i.e., for initialization and clipping). Note the Big Endian convention -- the most significant byte of a four byte value is at the lowest memory location in flash. This follows the storage of multi-byte numbers on the stack where the most significant byte is on the top of stack and thus the first to be accessed with stack operations.
The Words "!ie-acc" and "@ie-acc" allow quad (four byte) values to be stored into and fetched from the accumulator (after turning off interrupts to avoid changes during the operations).
Some accumulator cells are named according to their typical usage. For example, the least significant byte of the accumulator is often used to establish menu options. Thus, the least significant byte of the accumulator, "ie-acc3", is fetched and stored with "@option" and "!option", respectively.
This is syntactical sugar so that the name corresponds with the function being performed. The second byte of the accumulator is also available as option1. Both bytes may be treated as a double number and can be read and written with "@doption" and "!doption", respectively.
The Words "?activity" and "?changed" are also defined for menu operations. They detect whether or not there is any encoder or encoder switch activity. This avoids unnecessary checking of an idle accumulator request for a menu change in a loop.
\ ie.fs \ cpuHERE constant ie-step 4 cpuALLOT \ ie-step is MSB, ie-step 3 + is LSB :m ie-step3 [ ie-step 3 + ] m; :m ie-step2 [ ie-step 2 + ] m; :m ie-step1 [ ie-step 1 + ] m; -: 0ie-step ie-step # zero4 ; -: /ie-step 0ie-step 1 # ie-step3 #! ; -: /ie-step10 0ie-step 10 # ie-step3 #! ; -: /ie-step100 0ie-step 100 # ie-step3 #! ; \ \ -----[ Increment, Decrement encoder accumulators ] \ :m |ie-increment [ t push clrc ie-acc3 t mov ie-step3 addc t ie-acc3 mov ie-acc2 t mov ie-step2 addc t ie-acc2 mov ie-acc1 t mov ie-step1 addc t ie-acc1 mov ie-acc t mov ie-step addc t ie-acc mov t pop ] m; \ [ : subb dup 8 < if $98 + ] , [ exit then $95 ] , , [ ; ] \ now in misc8051.fs :m |ie-decrement [ t push clrc ie-acc3 t mov ie-step3 subb t ie-acc3 mov ie-acc2 t mov ie-step2 subb t ie-acc2 mov ie-acc1 t mov ie-step1 subb t ie-acc1 mov ie-acc t mov ie-step subb t ie-acc mov t pop ] m; \ \ -----[ ie accumulator and "changed" cells ] \ cpuHERE constant ie-old 1 cpuALLOT \ holds old ie bits \ \ -----[ encoder read and update ] \ -: @ie-old ( - n) ie-old #@ ; :m |!ie-old ( n - n) [ t ie-old mov ] m; -: !ie-old |!ie-old ; :m |@ie ( - n) 0 # [ iea movbc rlc ieb movbc rlc ] m; -: @ie |@ie ; :m ie-change? ( - ?) |@ie @ie-old xor m; :m |ie-changed? ( - ?) |@ie @ie-old xor |@ie |!ie-old drop m; -: ie-changed? |ie-changed? ; \ \ -----[ ie updates ] \ :m |?ie-bump |@ie [ rlc ] ie-old #@ xor 1 .t if. |ie-increment drop ; then |ie-decrement drop m; -: ?ie-bump |?ie-bump ; :m |ie-transfer |@ie |!ie-old drop m;
Figure 13. Accumulator Management
Figure 13 shows the implementation of the encoder update algorithm. Updates occur in increments stored in the 32-bit "ie-step" accumulator. Words such as "/ie-step" and "/ie-step100" initialize the "ie-step" accumulator with step values.
For example, "/ie-step" initializes the step interval to one and "/ie-step100" sets the step interval to 100. The "|ie-increment" and "|ie-decrement" macros add or subtract the increment to/from the accumulator. The names are prefixed with a "|" to emphasize that they are macros.
The macros are straightforward assembler definitions. Before execution, the top of stack, t, is saved and "clrc" clears the carry bit to set up for addition and subtraction with multiple bytes. Results are saved back to the encoder's accumulator. Speed is not much of a consideration: these operations take a few microseconds while the encoder update interval is one millisecond.
The heavy lifting of the incremental encoder code is performed by "?ie-bump", a MyForth Word that calls the "|?ie-bump" macro. In "|?ie-bump", the encoder bits, "iea" and "ieb", are read into the carry bit from I/O pins and left-shifted into the lower bits of a byte on the top of the stack. This encoder reading is compared with the previous read, contained in "ie-old", using an "exclusive or" operation. If bit 1 of the result is set, the accumulator is incremented by the current contents of the "ie-step" register. Otherwise, the value is decremented.
The Word "ie-changed?" compares the current and old values of the encoder to see if they have changed. If not, execution of "?ie-bump" is not necessary.
Of course, all of this assumes that the encoder has not changed multiple times since it was last read. This is ensured by setting the interrupt service routine that services the encoder to a suitably fast interval. One millisecond has proven adequate to keep up with even the fastest update rates (e.g., an encoder with a weighted spinner knob that is rapidly spun). Applications with the encoder not driven by hand may require faster updates and more processor resources.
The macro "|ie-transfer" transfers the current encoder reading to the "ie-old" direct cell.
\ ie.fs \ \ -------------------------[ band limit checking ]----------------------------- \ -: ie>gp0 @ie-acc gp0 # !quad ; -: ie>gp4 @ie-acc gp4 # !quad ; -: gp0>ie gp0 # @quad< !ie-acc ; here constant band-table here ( *) \ \ msb ------------- lsb band freq limit \ ---- -------------- \ $00 , $00 , $c3 , $50 , \ 0u 500 Hertz (50,000) $00 , $00 , $00 , $01 , \ 0l 0.01 Hertz \ \ calculate actual number of items in band limit table (e.g., for clip, display) here ( *) [ swap - 2/ 2/ ] constant #btable \ -: @band-table ( index - q) \ get quad from band limit table push band-table ## pop ai>qadr @qflash< ; \ \ cpuHERE constant upper0 1 cpuALLOT \ msB of upper limit cpuHERE constant upper1 1 cpuALLOT \ cpuHERE constant upper2 1 cpuALLOT \ cpuHERE constant upper3 1 cpuALLOT \ lsB of upper limit \ cpuHERE constant lower0 1 cpuALLOT \ msB of lower limit cpuHERE constant lower1 1 cpuALLOT \ cpuHERE constant lower2 1 cpuALLOT \ cpuHERE constant lower3 1 cpuALLOT \ lsB of lower limit :m |upper-gp0 \ gp0 is negative if gp0 is larger than upper0 \ quad result in gp0 [ t push clrc upper3 t mov gp3 subb t gp3 mov upper2 t mov gp2 subb t gp2 mov upper1 t mov gp1 subb t gp1 mov upper0 t mov gp0 subb t gp0 mov t pop ] m; :m |gp0-lower \ gp0 is negative if gp0 is larger than lower0 \ quad result in gp0 [ t push clrc gp3 t mov lower3 subb t gp3 mov gp2 t mov lower2 subb t gp2 mov gp1 t mov lower1 subb t gp1 mov gp0 t mov lower0 subb t gp0 mov t pop ] m;
Figure 14. Limit Checks
Figure 14 shows how the accumulator is updated, including how accumulator clipping is performed.
Normally, accumulator values are not valid over its entire 32-bit range. Additionally, decrementing the accumulator when it contains zero will result in a negative value. This is seldom desirable.
Thus, the encoder's accumulator may need to be limited to a value of zero at the low end of the range and at a maximum positive value at the high end of the range. There are a number of places that this can be done in a typical application, but it is best to do it during the encoder update in the encoder's interrupt service routine.
For the stroboscope application, the encoder is clipped at 0.01 Hertz and 500 Hertz. These values are hardwired in flash in "band-table." The Word "@band-table" takes an index and returns a quad number on the MyForth stack.
An index of 0 returns the four-byte lower band limit. This limit is 1 because frequency values are scaled by 100. Similarly, an index of 1 returns the high limit as 50000 (500 Hertz scaled by 100). These limit values, after being fetched from the table, are loaded into direct cells "upper-limit" and "lower-limit ."
The macro "|upper-gp0" subtracts the contents of the general purpose quad register zero, "gp0" from the upper limit. Before this calculation, "gp0" is loaded with the current contents of the incremental encoder.
When the result of the "|upper-gp0" calculation is negative, the upper limit has been exceeded. Similarly, "|gp0-lower" is negative when the lower limit is negative. The "/clip" Word loads the upper and lower band limits from flash to the direct cells used to store the limits.
The code is designed to accomodate any number of band limit pairs. For the stroboscope, there is only one range and only a single band limit pair. But, the the table-driven structure was adapted from the SUDDS application that implemented a multi-range variable frequency oscillator.
\ ie.fs ############ # Sample 1 # ############ -: ?uclip \ ~19 usec ie>gp0 |upper-gp0 gp0 #@ -if drop upper0 # @quad< ie-acc # !quad ; then drop ; -: ?lclip \ ~19 usec ie>gp0 |gp0-lower gp0 #@ -if drop lower0 # @quad< ie-acc # !quad ; then drop ; -: /clip \ pre-load limits for speed 0 # @band-table upper0 # !quad 1 # @band-table lower0 # !quad ; :m |?ie-update ie-change? if ?ie-bump ?uclip ?lclip then drop |ie-transfer m; -: /ie |ie-transfer 0ie-acc /ie-step /clip ; / timer0.fs ############ # Sample 2 # ############ TIM0 interrupt : timer0-isr |disable-interrupts |stop-T0 \ [ 4 .P0 set ] \ for timing save-state \ ~960 ns |ticks0- \ update 1 ms tick timer |?ie-update \ update encoder restore-state \ ~960 ns $F7 # TH0 #! $FF # TL0 #! \ reload timer, ~ 1 ms \ $FF # TH0 #! $40 # TL0 #! \ reload timer, ~ 100 us \ [ 4 .P0 clr ] \ for timing |start-T0 |enable-interrupts reti \ -: /timer0 -int |start-T0 +int ;
Figure 15. Encoder Updates
Figure 15 shows how the limit checks and the accumulator increment/decrement routine are used to update the encoder. It also shows how the update routine is used in the Timer 0 interrupt service routine (ISR).
As shown in Sample 1, the upper and lower clipping checks are performed by "?uclip" and "?lclip", respectively. These Words load the "gp0" cells with the current incremental encoder contents and perform a subtractive limit check.
To detect limit violations, "gp0" is checked after the subtractive check to see if it is negative. This is done by the MyForth "-if"conditional which checks the most significant bit of the byte on the stack and conditionally branches based on its value.
If gp0 is negative, the upper or lower limit (depending on the type of check) has been crossed and the limit is written to the incremental encoder's accumulator. As noted, the limit checks take about 20 microseconds each.
Sample 1 also shows how the Word "|?ie-update" is defined and executed in the ISR to update the encoder's accumulator. The update Word is executed once every millisecond in the Timer 0 interrupt. Note that the code for the Timer 0 ISR executes the encoder update and also manages a millisecond "tick" signal.
In "|?ie-update", the macro "|ie-change?" checks to see if the encoder bits have changed since the last update by comparing the current reading with the last reading (contained in "ie-old"). If there is a change, the encoder's accumulator is updated (bumped up or down by the current "ie-step" value) and the result is checked for limit clipping. Note that the update must be performed before performing the limit checks.
The "|ie-transfer" macro is always executed to save the current encoder to the "old" value of the encoder (ie-old) for the next time around. This is also done at initialization, as given in "/ie" encoder initialization Word. This initialization Word also zeroes out the accumulator and initializes the limit variables from the flash table.
The "/ie" Word also sets the inital encoder increment to one. This may not be necessary depending on how the initial increment is set. Typically, it is set from a value saved in flash memory (e.g., from a previous session). In this case, a single byte may be used as an input to a case statement that sets the encoder increment size; typically, this byte would be stored in flash as the "rate" option, as with DSTRO.
\ 9850.fs -- 9850 DDS Driver for single DDS -- 20Feb14 rjn \ 0 [if] \ ------------------------[ DDS I/O ]----------------------------------- \ \ these are defined in init.fs, shown here for code readability \ :m wclk [ 0 .P0 ] m; :m frud [ 1 .P0 ] m; :m dds-data [ 2 .P0 ] m; :m mrst [ 3 .P0 ] m; \ [then] \ ---------------------------------------------------------------------- \ -: fload [ frud set ] nop [ frud clr ] ; \ assumes FRUD is low :m clock [ wclk set ] nop [ wclk clr ] m; \ 9850 clocks low to high \ 0 [if] \ --------------------[ chip initialization ]--------------------------- \ 1. Reset must remain high for at least 5 sysclk (500 us for 10 kHz sysclk) 2. Minimum reset recovery is 2 sysclk 3. Reset output latency is 13 clock cycles from start of reset 4. After reset, chip is in parallel mode. Serial mode must be set after a reset. \ [then] \ ---------------------------------------------------------------------- \ -: 1us 1 # us ; \ reset DDS -- good down to at least ~17 MHz \ a 5 sysclk reset width for 17 MHz is (5)(60ns)=300ns \ for convenience, use 1us which is lower than 17 MHz -: /dds nop nop [ mrst set ] 1us [ mrst clr ] 1us ; \ \ -----[ init serial mode ] \ per figs 10 & 11 on rev H datasheet \ assumes D0 high, D1 high, D2 low -: /smode /dds clock fload ; \ \ -----[ send a byte to DDS A ] -: sbyte ( n -) \ send least significant bit first [ wclk clr ] ( wclk should be clear, but ...) 8 # 7 #for [ rrc dds-data movcb ] \ send lsb from carry to dds-data nop clock \ 9850 clocks low to high 7 #next drop ; \
Figure 16. DDS Primitives
Figure 16 shows the primitives used to initialize and to send a 32-bit frequency value to a 9850 DDS.
The Word "/smode" initializes the DDS (with "/dds") and applies clock and load signal changes. These operations are per datasheet specifications.
The Word "sbyte" sends a byte to the DDS, least significant bit first. The "for ... next" loop is iterated 8 times, once for each data bit. The data bits, being sent lsb first, are shifted right into carry (i.e., by the "rrc" instruction). This bit in carry is sent to the dds-data input with the "movcb" (move carry into bit) instruction. The following "nop" instruction provides a small delay to allow the data to settle before it is clocked into the chip with the "clock" Word.
\ 9850.fs \ [ \ --- gforth definitions! \ 0 [if] \ -------------[ frequency calculation notes ]-------------------------- \ 1. For a 100 MHz crystal: (2^32)/100,000,000 = 42.94967295 counts/Hertz = $19999999 counts/Hertz 2. For a 125 MHz crystal: (2^32)/125,000,000 = 34.35973836 counts/Hertz = $cccccccc counts/Hertz 3. For a 16 MHz crystal: (2^32)/(16,000,000) = 268.435456 counts/Hertz = $02af31dc counts/Hertz [then] \ ---------------------------------------------------------------------- \ 4294967295 constant 2^32 16000000 constant freq \ crystal frequency \ 16000000/4294967295 = 0.00372529030 Hz/count : U*/MOD >R UM* R> UM/MOD ; : U*/ U*/MOD NIP ; : (hz) ( n1 - n2) 2^32 freq U*/ ; : hz ( freq -) (hz) hex . decimal ; \ \ convert a gforth number (32 bits) to two 16-bit "doubles" for MyForth : >doubles ( n - leastn mostn) dup $ffff0000 and 16 rshift swap $0000ffff and swap ; : n>bytes ( n -- n1 n2) dup $ff00 and 8 rshift swap $00ff and ; : >singles ( n - n1 n2 n3 n4) \ convert to four MyForth singles (bytes) >doubles swap n>bytes rot n>bytes ; \ usage: 843 (hz) >singles : tbytes ( n - n1 n2 n3 n4) (hz) >singles ; \ tone bytes : hertz ( n - n1 n2) (hz) >doubles ; \ ] \ --- back to MyForth -: ~phase ( n - n') $1f # and 2* 2* 2* ; \ shift phase bytes to upper -: sbytes ( msb .. lsb) sbyte sbyte sbyte sbyte ; \ lsb sent first \ -: 0sbyte 0 # sbyte ; -: (send) ( msb n2 n3 lsb -) sbytes 0sbyte ; -: send (send) fload ; \ \ -----[ Set DDS from afreq quad register ] \ -: !abytes afreq # @quad send ; -: (zz) -int !abytes +int ; :m (A) a-fload (zz) m; 0 [if] \ -------------------[ test ]------------------------------------------- \ : 1000hz /smode [ 1000 hertz ] ## swap ## swap ( msb .. lsb) send ; : 2000hz /smode \ 125 MHz clock $6f # sbyte $0c # sbyte $01 # sbyte $00 # sbyte 0sbyte fload ; \ [then] \ ----------------------------------------------------------------------
Figure 17. DDS Output
Figure 17 shows how DDS frequencies are calculated and sent. Calculations are performed by GForth using its 32-bit integer math. GForth is invoked with "[" which sets MyForth to search GForth definitions first.
The most important calculation is for counts per Hertz. This constant is used to convert a frequency to a DDS load value. It is calculated by divideng the maximum 32-bit load value for the DDS by the clock frequency (the "clock" constant). The maximum number of counts is two to the 32nd power (shown as 2^32 in the code). Several sample calculations are shown, including the calculation for the stroboscope clock frequency of 16 Megahertz for a resolution of approximately 0.004 Hertz.
This fractional value is set up for a scaled integer multiplication (see multiply.fs) by first scaling the count per Hertz value by a power of two. This integer scaling preserves a large number of the significant digits in a fractional value. The results (after de-scaling) are accurate to 9 or 10 digits.
The scaled fractional value is multiplied by the integer frequency contained in the incremental encoder's accumulator. The 64-bit result is de-scaled to yield the 32-bit value to be loaded into the DDS. The calculations can be easily performed faster than the LCD display can display a readable value.
The GForth calculations also include a number of utility Words to generate the 32-bit DDS load value using GForth's integer math. The Word "U/*" is a reasonably standard Word to perform unsigned integer scaling with a double number (64 bit) intermediate product. This Word is not in GForth but is easily derived with existing GForth Words.
The Word "hz", given the frequency in Hertz, displays the hexadecimal value to load into the DDS to yield that frequency output. This is useful in defining tables, but dynamic calculations for LCD display are performed by multiplying the frequency by the scaled count/Hertz constant.
The Word "sbytes" sends the 32-bit frequency Word to the DDS, least significant byte first. For a single DDS, the fifth phase byte is set to zero (i.e., with "0sbyte"), but a phase Word, "~phase" (change phase), is available for applications using two DDS modules. Some initialization considerations for this kind of operation are contained in the DCON manual (it isn't as straightforward as one might think).
The Word "send" sends all of the bytes to the DDS and then loads them to produce a changed output. The macro (A) loads the calculated frequency value into a quad register, "afreq", and then sends this stored value to the DDS (after disabling interrupts).
The test section shows several ways to calculate and send a test value to the DDS.
\ multiply.fs ############ # Sample 1 # ############ \ 0 [if] \ ---------------[ Russian Peasant Multiplication ]--------------------- \ 1. Multiplies a double quad number by a 4-byte constant using the Russian Peasant algorithm (shifts and adds). 2. Uses the freq register for the larger factor and the scale register for the smaller factor. The resulting product is in the prod register. 3. Note: "dq" means "double quad", a 64-bit register. \ [then] \ ---------------------------------------------------------------------- \ \ -----[ quad register, scale ] \ cpuHERE constant scale 4 cpuALLOT \ scale is MSB, scale 3 + is LSB :m scale1 [ scale 1 + ] m; :m scale2 [ scale 2 + ] m; :m scale3 [ scale 3 + ] m; \ -: 0scale 0 ## 0 ## scale # !quad ; \ \ -----[ prod -- double quad product register ] \ cpuHERE constant prod 8 cpuALLOT \ prod is MSB, prod 7 + is LSB \ :m prod1 [ prod 1 + ] m; :m prod2 [ prod 2 + ] m; :m prod3 [ prod 3 + ] m; :m prod4 [ prod 4 + ] m; :m prod5 [ prod 5 + ] m; :m prod6 [ prod 6 + ] m; :m prod7 [ prod 7 + ] m; \ -: 0prod 0 ## 0 ## 0 ## 0 ## prod # !dq ; \ -: @prod< prod # @quad< ; \ \ load prod (test) \ : lprod $78 # $56 # $34 # $02 # $67 # $45 # $23 # $01 # !prod ; \ freqs.fs ############ # Sample 2 # ############ \ \ ----- 16 MHz clock = (2^32)/(16,000,000) = 268.435456 counts/Hertz here constant 16MHz-scale \ counts/Hertz is compiled here (in flash), lsb first $dc , $31 , $af , $02 , \ 2^24 scaling, ~10 digit accuracy, 0.01 Hertz -: read-d16 ( - q) 16MHz-scale ## @qflash ;
Figure 18. Scale and Product Setup
Sample 1 in Figure 18 shows the definition of two of the registers used in multiplication, the "scale" register and the "prod" (product) register.
The "scale" register contains a 32-bit scale factor used in the multiplication. For DDS frequency calculations, this register contains the scaled value for counts per Hertz. Sample 2 shows the storage and retrieval of the scaled counts/Hertz value for a clock frequency of 16 Megahertz.
The four bytes of the scaled value are placed in flash at the location named "16MHz-scale" using the "," Word which places a byte in the compiled image. The Word "read-d16" places the double number flash address on the processor's stack and calls the utility Word "@qflash" to fetch a quad value from flash.
The definition of the eight-byte (double quad) product register is straightforward, but note that the bytes are stored in Big Endian format with the most significant byte in the first cell (named "prod"). The Word "@prod<" fetches the double quad product starting at the least significant byte (hence the "<" in the name) so that the value is put on the stack with the most significant byte on the top of the stack (normal Forth convention).
\ multiply.fs \ \ -----[ freq -- double quad factor "a" register ] \ \ multiply result ends up here \ cpuHERE constant freq 8 cpuALLOT \ freq is MSB, freq 7 + is LSB \ :m freq1 [ freq 1 + ] m; :m freq2 [ freq 2 + ] m; :m freq3 [ freq 3 + ] m; :m freq4 [ freq 4 + ] m; :m freq5 [ freq 5 + ] m; :m freq6 [ freq 6 + ] m; :m freq7 [ freq 7 + ] m; \ -: 0freq 0 ## 0 ## 0 ## 0 ## freq # !dq ; :m prod>freq prod # 8 # @cells< freq # !dq m; \ \ : lfreq $78 # $56 # $34 # $02 # $67 # $45 # $23 # $01 # !freq ; \ test \ -: freq2*' \ multiply contents of freq register by 2, keep carry result clrc freq7 #@ 2*' freq7 #! freq6 #@ 2*' freq6 #! freq5 #@ 2*' freq5 #! freq4 #@ 2*' freq4 #! freq3 #@ 2*' freq3 #! freq2 #@ 2*' freq2 #! freq1 #@ 2*' freq1 #! freq #@ 2*' freq #! ;
Figure 19. Frequency Register
Figure 19 shows the definition of the remaining multiplication register. This register, "freq", is an 8-byte register that holds the double quad frequency to be multiplied by the quad scaling constant in the "scale" register. This yields a double quad product in the "prod" register.
The Word "freq2*'" multiplies "freq" by two, retaining the final carry result. Note: in MyForth, the "'" (tick) character often denotes an operation where the carry bit is significant.
\ multiply.fs \ \ -----[ Add, Subtract freq to/from prod ] \ :m |prod+freq [ t push clrc prod7 t mov freq7 addc t prod7 mov prod6 t mov freq6 addc t prod6 mov prod5 t mov freq5 addc t prod5 mov prod4 t mov freq4 addc t prod4 mov prod3 t mov freq3 addc t prod3 mov prod2 t mov freq2 addc t prod2 mov prod1 t mov freq1 addc t prod1 mov prod t mov freq addc t prod mov t pop ] m; \ -: prod+freq |prod+freq ; \ results in prod
Figure 20. Register Addition
Figure 20 shows the double quad addition that is performed on the "prod" and "freq" registers for the multiplication algorithm. This operation is relatively straightforwarward, but note that the "t" or top of stack register is used for intermediate results. This is done so that the value contained in "freq" is not affected by the addition. This leaves the result in "freq" ready for the next iteration. The "t push" and "t pop" operations in "prod+freq" preserve the current top of stack.
Because the multi-byte arithmetic uses the carry bit, carry is cleared at the start of the definition. The definition uses the MyForth assembler and thus the code is contained within left and right bracket pairs to explicitly show the entry to the GForth context (with "[") and the exit back to the MyForth context (with "]"). Both Forth and macro versions are defined for the operation.
The macro is prefixed with a "|" (bar) to help identify it in a definition. Although not all macro definitions are named with a "|" prefix, this convention helps identify whether or not a macro or a Word is being used. Mixed use of macros and Forth Words allows a tradeoff between speed and compilation size. Although the macro version is not directly used in the algorithm, it is left in the source code because it does not consume memory until it is used in a definition.
\ multiply.fs \ \ : cs$ string " cs! " \ debug \ -: scale2/' \ divide scale register by 2, accumulate remainder on carry result clrc scale #@ 2/' scale #! scale1 #@ 2/' scale1 #! scale2 #@ 2/' scale2 #! scale3 #@ 2/' scale3 #! if' prod+freq ( can't be a macro!) ( cr cs$) ; then ; -: scale2*' \ multiply scale register by 2 clrc scale3 #@ 2*' scale3 #! scale2 #@ 2*' scale2 #! scale1 #@ 2*' scale1 #! scale #@ 2*' scale #! ; \ \ -----[ load scale ] \ -: (msteps) ( - n) \ calculate multiply steps by looking for the last "1" 32 # 32 # 4 #for scale2*' if' ; then -1 # + 4 #next ; \ assumes scale loaded, calculates number of multiply steps, preserves scale -: msteps ( - n) scale # 4 # @cells< (msteps) push scale # 4 # !cells pop ; \ note: 31 bytes, this is the last step: remainders are accumulated X 2 -: prod/2' \ divide contents of prod register by 2, keep carry info clrc prod #@ 2/' prod #! prod1 #@ 2/' prod1 #! prod2 #@ 2/' prod2 #! prod3 #@ 2/' prod3 #! prod4 #@ 2/' prod4 #! prod5 #@ 2/' prod5 #! prod6 #@ 2/' prod6 #! prod7 #@ 2/' prod7 #! ; \ -: multiply \ multiply freq by scale, results in prod 0prod msteps 4 #for freq2*' scale2/' 4 #next prod/2' ; \ \ load multiplicand in freq, results in prod :m 10>scale 0scale $0a # scale3 #! m; -: freq*10 10>scale multiply prod>freq ; \ 0 [if] \ ----------------------------- test ----------------------------------- \ -: s$ string " start: " -: f$ string " finish: " -: .minfo .scale .freq .prod ; \ display mult. regs -: d$ string " -----------" \ \ load scale and freq first -: multiply \ multiply scale and freq, results in prod 0prod cr s$ .minfo msteps 4 #for cr d$ freq2*' scale2/' .minfo 4 #next prod/2' cr cr f$ .minfo ; \ Example for 50 MHz frequency, 125 MHz xtal, scale scaled by 2**22: \ ($2FAF080)($89705F4)=$199999995FBA00 \ dividing by 2**22=$66666665=1,717,986,917 (vs. 1,717,986,918) [then] \ ----------------------------------------------------------------------
Figure 21. Multiply
Figure 21 shows the implementation of the multiplication algorithm given the register definitions and arithmetic operations given in Figures 18 to 20.
The multiplication is performed using the commonly-used method of shifting and adding. For those not familiar with this method, is simply consists of multiplying one factor by two (left shift) while dividing the other factor by two (right shift). Thus, at each stage, the product is the same. When one factor is unity, the other factor is the product (almost).
There is a problem with this method: when dividing a number by two does not produce an exact result, a remainder must be added to the product. This happens when the least significant bit of the right-shifted factor is one (it is odd). For odd numbers, the result is in error by the size of the other factor in the multiplication.
To compensate for this error, the other factor is added to the final result whenever an odd number is divided by two. The number of corrective additions depends on the number of "ones" in the factor used as a multiplier (in our case, the "scale" factor). This is convenient because the scale factor is known and always the same. This allows the number of ones (and hence the number of algorithm steps) to be pre-calculated.
The Word "multiply" first zeroes out the "prod" register so that any residual results from previous multiplications do not affect the results. As noted above, the "scale" register contains the value to be divided by two in each iteration. The multiplication is finished when the most significant bit that is set is reached (i.e., the "scale" register contains a one).
The Word "msteps" pre-calculates the number of multiply steps by shifting "scale" left until the result is negative (the most significant set bit is reached). Pre-calculating the number of shifts allows the use of a specific loop index. Calculating algorithm termination on the fly is possible but less efficient and more difficult to implement than performing a specific number of iterations.
The "msteps" calculation returns with the number of iterations on the stack. The code "freq2*' scale2/'" performs the multiplication of the "freq" register and the division of the "scale" register in a loop with its index equal to the value computed by the "msteps" Word. After the loop exits, the result in "prod" is divided by two because of the extra operation introduced by the "msteps" calculation (see below).
Most of the work of the "msteps" calculation is performed by "(msteps)" which starts with a value of 32 which is decremented each time "scale" is divided by two. The "scale" register is left-shifted into carry. When the carry is set, the most significant digit is one and "(msteps)" exits with the number of shifts plus one on the stack. The "plus one" is because the termination is determined by the value of carry, not the most significant bit of the "scale" register. This code could probably be simplified by directly detecting the last bit instead of checking for carry being set. This "off by one" index is the reason for the extra divide by two in "msteps."
The Word "scale2/'" divides "scale" by two and performs a check for the division of an odd number (i.e., with "if' prod+freq" test). Because the divide by two consists of a "rotate right through carry" operation, carry is set whenever an odd number in "scale" is divided by two. If this is the case, "prod+freq" is executed to add the current value in the "freq" register to the "prod" register.
Note that, when "scale2/'" is executed, the "freq" register has not yet been multiplied by two. Thus, the ordering of the division and multiplication of the two factors is important. Also note that the value in the "freq" register is not affected by the "prod+freq" addition because the results are incrementally accumulated in the top of stack byte (the MyForth "t" register).
The Words below "multiply" in Figure 21 illustrate the use of multiply. The Word "freq*10" shows how to multiply a frequency in the "freq" register by 10, returning the result to the "freq" register. The multiply example in the test section displays multiplication results using the serial port and a standalone interpreter (not used in DSTRO). Also provided in the test section is a sample multiplication.
\ menu.fs \ \ -----[ main menu loop ] \ -: xmain @1of4 0=if drop >rate ; then |1xor 0=if drop >units ; then |1xor |2xor 0=if drop >duty ; then |2xor |3xor 0=if drop xback ; then drop ; -: save-main-choice ( - n) @option !bx0 ; -: restore-main-choice ( n -) @bx0 !option ; -: 'main-menu lstring " Option Menu" ; -: .main-menu /line1 lcd-coff 2 # n_lcd 'main-menu ; -: ?select ie-switch 0=if. ?switch-up \ s.lcd \ debug save-main-choice xmain restore-main-choice .main-menu /last \ s.lcd \ debug then ; -: /main-menu restore-main-choice /last /back /ie-step ; -: main-options line2 @1of4 ?shown 0=if drop .rate-choice ; then |1xor 0=if drop .units-choice ; then |1xor |2xor 0=if drop .duty-choice ; then |2xor |3xor 0=if drop .back ; then drop ; -: main-menu /main-menu .main-menu 0 # begin drop main-options ?activity ?select back? until drop ?.sopt ?rate erase-config save-xtable ?switch-up ;
Figure 22. Main Menu
Figure 22 shows the code for the DSTRO Main option menu. This code provides an LCD menuing system to allow options such as frequency rate, units and duty cycle to be selected. This is typical of an LCD menu that is commonly used in BOBZ applications. The "main-menu" code is discussed first because most of the details for Words used in the Main menu would not make much sense outside the context of the menu scheme.
Starting at the bottom of the figure is the definition of "main-menu" that provides the menu interface for DSTRO options. Ignoring the initialization and cleanup Words, the menu Word consists of a "begin ... until" loop that terminates when the user selects the "<--" option that is on each menu. Selection of this option terminates the current menu and executes the Word "back?" to terminate the loop. When the loop terminates, some cleanup is performed before returning to the main application loop (in the definition of "go").
The cleanup required on menu exit varies from application to application. In this case, one of the cleanup actions is to save the last-selected option in flash so that a restarted application uses the last-selected menu options. Saving the options to flash is performed by the "erase-config" and "save-xtable" Words. These erase the flash configuration block and save the options contained in an XRAM table to it. The term XRAM is used to designate extra RAM that is contained in the processor but accessed as external RAM. The 850 chip has 256 bytes of XRAM.
The "?switch-up" Word ensures that user has released the incremental encoder push switch, continuing to the next Word only if the switch is not pressed. This ensures that the option menu is not immediately re-entered from the "go" loop.
Before entering the "main-menu" loop, a zero is put on the stack. This value is immediately dropped on loop entry. This is needed because "back?" always returns a flag byte. If the "back" option is selected, the flag is non-zero, causing the loop to terminate. Otherwise, the loop repeats.
The "drop" at the start of the loop gets rid of the zero flag introduced by the "back?" Word during each iteration of the loop. The "drop" after the "until" part of the loop drops the termination flag. In MyForth, flags and conditional values are normally left on the stack for possible use in following Words. When not used, these must be explicitly dropped. This is a significant difference from most Forth systems.
Inside the "main-menu" loop, "main-options" is executed to display the option choices. This is a "display only" Word that just cycles through the option choices depending on the current value of the incremental encoder.
Thus, rotating the incremental encoder knob displays the menu items one at a time on the second line of the LCD display. Turning the knob counter-clockwise results in a sequential display of >Rate, >Units, >Duty and "<--", in the order given, in the "main-options" case statement. Rotating the encoder counter clockwise displays the options in reverse order.
On entry to "main-options", the LCD cursor is positioned to line2 by the "line2" Word. Next, the Word "@1of4" translates the least significant byte of the incremental encoder register into one of four different values between 0 and 3. These values wrap so that the next value after 3 is 0 and the next value before 0 is 3.
These wrapping values, based on incremental encoder position, are processed by a case statement. Thus, depending on the encoder value, one of four text strings are displayed on line 2 of the LCD.
A file named intervals.fs provides a number of definitions to convert the encoder position to one of several values, ranging from 1 of 2 to 1 of 13. These interval definitions are generally implemented by performing a "mod" operation on the 256 possible encoder values. Some intervals are computed specially to provide a more even spacing of values.
The Word "?shown" prevents repetetive display of the same value by checking the current value against the last value used to display a menu item.
The case statement is typical for MyForth, with each possible value compared with a constant by xoring it with the current case number. When a value matches, the case is executed to display the option text (e.g., with ".rate-choice"). After the choice is displayed, "main-options" is exited (with ";", which compiles a "ret" instruction. If the compared value does not match, it is restored with an "exclusive or" operation and checks continue until no more checks can be performed.
Assuming that "main-options" has displayed an option and exited, the "main-menu" loop next executes "?activity" to check if there is any incremental encoder activity. If no activity, the main loop does not continue to execute.
When "?activity" detects activity in the form of an encoder change or an encoder switch press, "?change" executes to process the change. If the change is due to a switch press, the menu option is processed. Otherwise, the change is displayed by re-executing "main-options", as previously described.
When "?select" executes, it checks for a switch press. If the encoder switch has been pressed, "?switch-up" executes to ensure that the switch has been released before further processing. Before processing the selected option, the current main option choice is saved so it can be restored when option processing is complete (i.e., with "restore-main-choice").
The processing of the selected menu option is handled by "xmain" (execute main choice). The "xmain" Word is implemented using another four-item case statement. The selected item using the current encoder position (which shouldn't have changed after selection).
It is possible for the encoder position to change slightly when pushing the encoder switch down. This is not normally a problem because each option is represented by a range of values and each selection is typically selected near the middle of a range.
When an option is just between two ranges, the selection can change, causing the selection of the next option. This has not proved to be a significant problem in normal usage because of the large span between selection ranges when ther are a small number of options.
This problem can rarely be observed when there are a large number of options and the range for each is limited. This is rarely the case but, if this is a problem for a particular application, it can be easily fixed by saving the selected option number. This solution was not implemented because it consumes a direct cell and "option jumping" is rarely a problem.
\ menu.fs \ \ -----[ units menu ] \ \ --- unit strings \ -: save-units-choice @option !bx2 ; -: restore-units-choice @bx2 !option ; -: 'units-menu lstring " Units Menu" -: 'units lstring " >units" -: .units-choice /line2 4 # n_lcd 'units ; -: .units-menu /line1 2 # n_lcd 'units-menu ; \ -: 'units=hertz lstring " >Hertz" \ -: 'hertz-set lstring " Hertz set!" -: .units=hertz /line2 4 # n_lcd 'units=hertz ; \ use bxtable3 for Hertz/rpm value: 0=Hertz, 1=rpm -: >units=hertz /line2 0 # !bx3 ?.units 'set persist ; -: 'units=rpm lstring " >rpm" \ -: 'rpm lstring " rpm" -: .units=rpm /line2 4 # n_lcd 'units=rpm ; \ -: 'rpm-set lstring " rpm set!" -: >units=rpm /line2 1 # !bx3 ?.units 'set persist ; \ :m ?xunits ( n - n) |2xor 0=if drop xback ; then |2xor save-units-choice m; -: xunits @1of3 ?xunits 0=if drop >units=hertz xback ; then |1xor 0=if drop >units=rpm xback ; then drop ; -: units-menu @1of3 ?shown 0=if drop .units=hertz ; then |1xor 0=if drop .units=rpm ; then |1xor |2xor 0=if drop .back ; then drop ; -: do-units begin units-menu ?activity ie-switch 0=until. ?switch-up xunits ; \ -: >units /back /last .units-menu restore-units-choice 0 # begin drop do-units back? until drop ;
Figure 23. Option Execution
Figure 23 shows how a selected option is processed. Assuming that the "Units" option has been selected, it is processed with ">units", which is implemented with another "begin ... until" loop.
The Words "/back" and "/last" initialize the "return to previous selection" and "last selected" code. The Word ".units-menu" displays the string "Units Menu" on line 1 of the LCD to show that the user has entered a new menu. Like the Main menu, option selections appear on line 2. Techniques similar to those used for the Main menu are used to select options from the Units menu.
For example, the Units menu uses a "begin ... again" loop to cycle through and display a list of Units options. This is done with "do-units", another "begin ... until" loop for displaying menu options. The Word "units-menu" displays one of the two Units option strings or the "<--" option string that signals a return to the main application loop (through the main-menu loop).
The "units-menu" Word uses "@1of3" to derive values between 0 and 2 from the encoder position. This value is used display one of three option strings on line 2. The options rotate back and forward with incremental encoder position, as previously described for the Main menu.
This option display continues until the encoder switch is depressed. As before, "?activity" checks to see if there has been a change to avoid loop (and display) thrashing.
When the encoder switch is depressed, the option display exits. Note that the "do-units" loop terminates on the value of an input bit assigned to the encoder switch. Thus there are no stack flags to manage. Also note that, after exiting the loop, "?switch-up" executes to ensure that the user has released the encoder switch.
The Word "xunits" uses the current encoder position (corresponding to the selected option) to select the option action to perform. This is similar to the technique used for the Main menu.
In "xunits", the Word "?xunits" executes before the case statement selects one of the Units options. The "?xunits" Word checks to see if the user has selected the "back" option. If so, it sets a variable "~back?" to signal that a return was selected. More on this later.
Assuming that Hertz was seleted as the units to use, "xunits" executes ">units=hertz" which simply sets a variable and displays a "set" message. The variable is used in the main application loop to display either a "Hertz" or "rpm" string following the current flash rate.
Now note that, after executing ">units=hertz", the Word "xback" executes before "xunits" exits (i.e., with ";"). When "xback" sets the flag "~back?" true (a value of $ff), the menuing system exits the "main-menu" loop and retursn to the application loop defined in the "go" (turnkey) Word. This exit occurs after executing the cleanup code following the "main-menu" loop.
The Word "back?" just returns the value of the "~back?" flag. Note that "back?" causes the loop in ">units" to exit back to "main-menu" through the Word "xmain", as shown in Figure 22. Also note that "back?" also causes an exit from the "main-menu" loop.
Thus, when the user selects an option and exits from the Units menu, the menu system returns back to the main application loop in the "go" turnkey Word. This behavior was desirable in the DSTRO application, but such a "multi-level" return may not be best for other applications. In this case, the "back?" Word can be selectively applied so that loops exit under different conditions.
Typically, menuing system is customized for each application. The menuing application for DSTRO provides a useful template for other menuing systems.
Most projects using an LCD can benefit from a menuing system. Examine other MyForth applications for other menuing systems. One caution: menuing code has evolved as applications have been implemented over several years. The code presented here is the most recent (and hopefully the best) code for menuing.
Production release to local PC.
Created rough template.