This is the manual for FreeWPC, a free platform for developing replacement game ROMs for Bally/Williams pinball machines based on the WPC (Williams Pinball Controller) chip. FreeWPC provides the foundation for building new ROMs with custom game rules. This document describes version 1.10-dev of the software.
FreeWPC is developed by Brian Dominy (brian@oddchange.com) and is licensed under the GNU General Public License version 2 or later. The latest version of the software can be obtained at http://www.oddchange.com/freewpc. Developers should be familiar with basic embedded systems programming concepts and the C programming language. Familiarity with 6809 assembler is recommended but not necessary. Also, knowledge of WPC pinball machines is very helpful.
FreeWPC is a toolkit for building brand new software for pinball machines. It primarily targets the Williams WPC family of games, which was used in all of that company's games from 1990-1999, starting with ‘Funhouse’ and ending with ‘Cactus Canyon’.
This document provides an overview of the FreeWPC architecture for developers who wish to understand how the system works. FreeWPC is constantly evolving and the information here is subject to change.
FreeWPC only replaces your game ROM (the U6 chip on the CPU board), which contains all of the game code and dot matrix text/graphics. In particular, sounds and music are NOT included in this device, so FreeWPC games use the same sounds as the real machines.
FreeWPC is 100% Williams-free code, written from scratch. It is not a hack on existing ROMs and does not contain any software extracted from real games. Most of the code is written mostly in the C programming language. (Actual WPC games were programmed in assembly language). FreeWPC uses the freely available GCC6809 C compiler. There are some assembly language routines for really low-level, or high performance parts of the program.
All generations of WPC are supported. FreeWPC is also architected into a number of distinct layers which makes it highly portable to other types of pinball machines than just WPC games, including new custom hardware. The platform files describe the circuit boards. The CPU files are specific to the microprocessor, and can be shared between platforms (WPC and WhiteStar both use a 6809). The machine files are unique to a particular game.
Development requires a UNIX-like environment, such as Linux or Cygwin for Microsoft Windows users. Many common UNIX tools like bc and perl are required.
FreeWPC has become more portable over the years, so that it can be used to develop pinball software for platforms other than WPC. This support is still evolving rapidly. In addition to building ROMs, the toolkit supports native mode compilation, which builds an executable that can be run directly on your development machine. This lets you test and debug the software without requiring a real machine or even an emulator. Basically, native mode replaces the lowest level of hardware access with builtin emulation. In place of actual I/O being controlled, it displays the results of the simulation on your screen, using either the console, ncurses, or gtk.
This chapter explains how to install the software.
FreeWPC is mainly a toolkit, and thus is provided in source code format. However, periodically binary ROMs are compiled and published as well. If you are not a developer, but would like to experiment with FreeWPC, you can just download the binary packages and install them in your machine or in your emulator.
If you are a developer and want to write your own game code, for one of the existing games that FreeWPC has been ported to or for a brand new game, you need a source code package.
If you just want to browse the source code, you can view it online at the GitHub repository.
Binary packages are available as .zip files, which contain .rom files. These ROMs replace the game ROM only (the U6 chip on the CPU board). If you want to test in a real machine, you need to burn a real EPROM from this file.
If you want to test under PinMAME, you need to copy the ROM file into your PinMAME roms directory; where this is depends on how you installed PinMAME. These directories typically contain .zip files of both the game ROM and sound ROMs together. You'll need to unzip the original file, replace the game ROM, then zip it back. Make sure to save your original game ROM file for when you want to revert back to the Williams code.
The following is a partial list of other programs which are used during the build process: gcc6809, dd, bc, zip, unzip, bash, Perl.
The FreeWPC source code tree is organized into the following directories:
On hardware which requires bank switching (like WPC), these functions are kept
in the fixed region of ROM that can be called at any time.
They are mostly software-related APIs for common features that pinball machines have,
like replay, match, and dot-matrix effects.
The native directory here is used in simulation, to obtain support
that is based on portable POSIX support.
FreeWPC can be compiled in many different ways with lots of optional features. You cannot just type make at the top of the directory tree without first specifying what you want to build. This is done by creating a file named .config. The file config.example is provided as an example of the kinds of things that can be tuned.
.config is written in standard Makefile syntax. (The top-level Makefile includes this file.) You set various options by writing variable assignments, such as:
MACHINE := wcs
You must specify at least a MACHINE. All other configuration variables are optional; suitable defaults will be used.
Here is a list of the most important build options.
MACHINEMAKEFLAGSFREEWPC_DEBUGGERy if you want to include debug messages in the build.
CONFIG_BPTy if you want to enable the breakpoint module.
GCC_VERSIONEXTRA_CFLAGS#ifdef, you can enable those flags
by including them in EXTRA_CFLAGS.
DEBUG_COMPILERy if you are debugging the 6809 compiler. This causes
many temporary files to be saved for later analysis.
SAVE_ASMy if you just want to see the assembly language code that
GCC6809 generates. This is similar to DEBUG_COMPILER but produces
much less GCC internals. If you understand 6809 assembly language, this
can be useful.
NATIVEy to enable native mode, or simulation.
CONFIG_UIconsole for a much more raw, printf
style output. gtk support is in progress.
PINMAMEEXTRA_PINMAME_FLAGSTARGET_ROMPATHAfter configuring, type make. This will build a ROM and optionally install it into your PinMAME ROM directory if you have said where that is.
The exact commands are not printed, but you can force the details to be shown by setting the environment variable Q to the empty string, like this:
make Q=""
The build procedure is complicated, but it can be broken down into the following steps:
If the size of the ROM is larger than the number of pages that the compiler generates, then the final ROM image must be padded with blank sections. The dd command is used to generate a file named blankxxx.bin, where xxx is the size of the file in kilobytes (KB). This file is later concatenated with the linker output to produce a final ROM of the required size.
The linker is invoked several times, once per page or bank of ROM. Different options are passed each time to place the correct object files into that section, and to resolve references correctly. All of these options are written to linker command files, which have the .lnk extension and are placed in the build directory. They are lists of command-line options to the linker, but placed into a file because of the huge number of them.
The symbol link mach is set to point to the correct machine code directory, based on the value of the MACHINE make variable. Likewise, include/mach is pointed to the machine includes.
Some #defines are generated automatically by scanning the code for their uses. These include files begin with the prefix gendefine and are created by a script also named gendefine in the tools directory.
At present this is only used to autogenerate the task group IDs.
Callsets are a mechanism for implementing a simple event subscription/invocation mechanism that is fully described at compile-time. Event handling code is emitted in a C file named callset.c.
Source code is compiled using the GCC6809 compiler. The compiler generates assembler code with the .s extension. These files are then assembled using the asxxxx asembler tools into object files with the .o extension.
Because the linker requires each section to contain at least one object file, a dummy file is assembly per section to ensure that this doesn't happen. The page header is 1 byte long and contains the page number.
The aslink utility is used to create one S-record file, with the .s19 extension, for each page of ROM.
The tools/srec2bin utility included with FreeWPC is run to convert the S-records into raw binary files.
The binary files are concatenated, along with any blank files, to form the final ROM image.
If successful, build/var.rom will contain the ready-to-burn ROM image. var is a string composed of the machine's short name and the version number.
Warnings and errors during the build are display on the console as they occur; they are also log to a file named err.
In native mode, instead of a ROM, a file named freewpc_var is generated.
FreeWPC provides a standalone operating system environment when compiling to run on real hardware. It does not require the use of any existing OS and has its own multitasker and memory manager. This chapter describes the key features of the FreeWPC programming environment.
FreeWPC implements a runtime, round-robin, non-preemptive task scheduler. The system manages multiple tasks, each of which has its own call stack. You can create a new task when you want to run some code in parallel with the current thread of execution. The current task has complete control of the CPU and will continue to run until it explicit exits or sleeps (waiting for a certain amount of time). The minimum sleep time is defined by IRQS_PER_TICK and is currently 16ms.
(Contrast this with desktop operating systems, which use timeslicing and preemption to allow multiple threads to run in parallel. There, the OS switches between the tasks. In FreeWPC task switches only happen explicitly; this provides for more deterministic behavior, it requires less overhead, and it eliminates the need for mutexes (locks) between two tasks.
If a task does not give up control, either by sleeping, exiting, or yielding
after a certain amount of time, the fatal error ERR_FCFS_LOCKUP will be asserted.
When a new task is created, it does not begin running immediately; control always continues with the task that started it. This allows you to configure the new task before it can run.
Each task is identified by a process ID, or pid. The PID for a task is assigned by the system when it is created. A task also has a group ID, or gid. The GID is assigned by the programmer, and multiple tasks can share the same GID. GIDs allow you to control a group of related tasks, or to refer to a task using a compile-time ID as opposed to a run-time ID.
In native mode, the multitasking APIs are implemented using the GNU Pth library. A good description of this library can be found at http://www.gnu.org/software/pth/. Note this is not the same as pthreads, which is the predominant preemptive threading library.
For Windows programmers, this threading model is very similar to what Win32 calls fibers.
After all tasks have been given a chance to run in a 16ms timeslice, the system runs the periodic functions. These have equal priority to tasks, but they do not have a stack and they cannot sleep. In the code, these are also referred to as idle functions, although that term is slightly incorrect (they are guaranteed to be called even on a fully busy system).
You can register a periodic function at various rates: either every 16ms, 100ms, 1 second, or 10 seconds. Handlers may not be called at the exact rate, but it will be as close as possible. Slippage occurs when it takes longer than 16ms for all tasks to be given a chance to run. If you need exact timing, use a realtime function.
For example, assume that a 1 second handler does not get called until after 1.5 seconds from the previous call, because the system is busy running other tasks. Then the next time it will be called just 500ms later; the scheduler will realize that it is behind and try to catch up.
You cannot dynamically register or deregister periodic handlers at runtime.
task_create_gidtask_set_rom_page immediately after this call to say where
it was placed.
task_create_gid1task_recreate_gidtask_sleepTIME defines which is given in units of approximately 16ms. Because it is 8-bit, this limits it
to about 4 seconds.
If you want to sleep for the absolute minimum, instead of task_sleep (0) it is recommended
to use task_yield().
task_sleep_sectask_exittask_find_gidtask_kill_pidtask_kill_gidtask_set_argtask_get_argThe multitasking APIs are flexible but do not guarantee realtime response, since any task may run for a long time. Processing that needs to occur at a strict interval is done by real time tasks, sometimes abbreviated in the code as RTTs.
Optimizing the realtime tasks is key to an efficient system. To date, about 35-40% of the 6809 is spent in realtime tasks; the actual value depends on the machine and the number of device drivers needed.
Real-time tasks are scheduled at compile-time. When you write an RTT, you must also add an entry to the schedule file, either kernel/system.sched for the core parts of the system, or machine/machine/machine.sched for the game-specific RTTs. Each entry in the schedule file gives the rate at which you want your function to be run, for example, once every 8ms. You can schedule to run as frequently as once every 1ms, but you should only schedule as often as you really need it, otherwise the CPU will have little time to do anything else!
Real-time functions run in interrupt context. The periodic interrupt will preempt anything else that is running at non-real-time priority. Realtime functions cannot sleep.
A scheduler, gensched, processes the schedule files at compile-time and generates the interrupt handler code which calls all of the RTTs at the correct frequency. The output can be seen in build/sched_irq.c.
Each line in the schedule file is formatted as follows:
function frequency duration
function identifies the name of the function that you want called in realtime context. If you prefix the name with an exclamation point '!', the function is assumed to be an inline function; otherwise, it is an ordinary function. You can append a conditional as a suffix, e.g. my_rtt?CONFIG_FOO will call my_rtt, but only if the conditional CONFIG_FOO is set.
frequency says how often the function should be called, in multiples of the system IRQ tick, which is 1ms on WPC. Presently, this value can range from 1 to 2048 ms. It must also be a power of 2.
duration says how long this function takes to run, in the worst case. The schedule tries to reserve enough CPU cycles for all RTTs and to arrange the order in which they are called so that tasks are balanced. The duration is also given in milliseconds and should be less than 1ms. It can also be specified in CPU cycles by appending the 'c' suffix. It is not critical if the value is not exact, but a reasonable approximation should be listed.
Blank lines and lines beginning with the '#' sign are treated as comments.
By default, the scheduler will unroll the interrupt handler into 8 smaller handlers. That is, over a period of 8ms, the system will cycle through 8 different interrupt handlers. The reason is that this cuts down on the number of checks which have to be performed on every IRQ, since infrequent (greater than 8ms) actions do not need to be tested every time.
If a function is scheduled every 4ms, then it will be called from half of those interrupt handlers (every other one, either the even-numbered ones or the odd- numbered ones). Likewise for functions scheduled every 2ms. Functions scheduled every 1ms are called every time. For functions scheduled 8ms or more, those are all executed from the last of the 8 handlers, and 'if' statements wrap the blocks of code that need to run less often.
Sometimes you may want to unroll a function manually. An example of where this is used is with the switch detection logic. All switches must be polled every 2ms. However, it takes a long time to poll every switch. Instead, we poll half of the switches during 1ms, and the other half on the next 1ms. This spreads out the processing and keeps the length of any one IRQ call from being too long.
To tell the scheduler to do this, first write two separate rtt functions. By convention these are numbered with a suffix, e.g. switch_rtt_0 and switch_rtt_1. Then in the schedule file:
switch_rtt/2 1 280c
The "/2" says that the function was manually unrolled into two separate RTTs. The frequency is now 1ms, because one or the other will be scheduled twice as often now. The duration is the length of time it takes for one of them to run, and is currently assumed to be the same for both.
FreeWPC does not use dynamic (heap) memory allocation, except in some very rare circumstances. All variables should be declared global, or put on the stack.
Global variables cannot be statically initialized. Variables should be initialized explicitly inside a C function.
Global variables can be divided into a number of categories, depending on their usage. Normal globals are always in scope and behave as you would expect. Additionally, you can tag a global with one of the following attributes:
__fastram__Do not use fastram unless you are working in the core.
__permanent__factory_reset event, because factory
reset should restore everything to a sane state. GCC6809 places these variables
at an address that is not automatically cleared by the startup code.
The init or init_complete
__nvram__pinio_nvram_unlock(), change
it, and then call pinio_nvram_lock(). This helps to ensure that
certain critical variables are not accidentally corrupted.
Do not sleep while inside the critical section. Do not nest calls to these functions either.
This feature makes use of special hardware in the WPC ASIC.
Adjustments and audits kept in NVRAM are managed via special APIs which do the locking/unlocking for you.
This feature works even in native mode; the __nvram__ variables are saved
to a file when you exit the program.
__local__start_player event.
There is a limit to how many locals can be declared; if you exceed this, you will get a linker error.
Although ordinary variables work well, sometimes you only need to store a single bit. For this, instead of declaring a byte variable and wasting 7 of the 8 bits, you can use a flag. Flags are packed efficiently in the memory to avoid wasting space. This can be used helpful on small memory platforms.
The bit APIs are the most generic. You provide a bit number and a pointer to a bit buffer where the bits are packed.
The flag APIs operate on a per-player flag buffer. They are also automatically cleared at player start. You only provide a flag number.
The global flag APIs are similar, but those flags are global and do not change across players, nor across games.
Flag and global flag IDs are distinct. Be careful not to pass a global flag ID to a flag API, or vice-versa.
bit_on, flag_on, global_flag_onbit_off, flag_off, global_flag_offbit_toggle, flag_toggle, global_flag_togglebit_test, flag_test, global_flag_testFreeWPC is mostly an event-driven system, which only acts when there are new inputs to the system. This section describes the basic interface to the application layer software, and then explains how that is implemented internally.
When any module wants to be notified when a particular event occurs, it declares an event catcher function, using the CALLSET_ENTRY macro. Here is an example:
CALLSET_ENTRY (strobe_multiball, sw_forcefield_target)
{
...
}
This declares a new catcher for the module named strobe_multiball,
which is to be called whenever the event sw_forcefield_target occurs.
The name of the module is arbitrary, but the convention is to use the name of the source code file or something similar. If you need to write two different handlers in the same source file for the same event, they need to have different module names.
The event name must exactly match the name used by the generator of that event.
There is a list of predefined system events which are often caught (see System Events).
Switch names can also be used as events (e.g. sw_free_kick_target). Normally a switch
event is generated on an inactive-to-active transition; you can cause an event
on any transition by declaring the switch as ‘edge’ in the machine configuration file.
Ball devices also generate events for several reasons (see Ball Tracking), for example,
dev_lock_enter.
If there are multiple catchers for the same event, then all of them will be invoked, in some random order. For example, another mode might also make use of the same target shown above:
CALLSET_ENTRY (mode_start, sw_forcefield_target)
{
...
}
Then both of these handlers would be called when the switch is closed.
To generate an event, use the callset_invoke() API, passing
it the name of the event. You can create your own events for any
purpose; event names do not need to be declared. Such events are
as full-featured as system-defined events. When defining custom events
in a game, it is customary to prefix the event name with the short
name of the game (e.g. ‘tz’).
Event catchers are allowed to throw new events. This will result in nested function calls.
Most event handlers do not return a value. However, in some cases it is desirable to stop calling event handlers once the event has been claimed. You can do this with boolean events.
To throw a boolean event, use callset_invoke_boolean(). This
returns TRUE if all of the event handlers return TRUE; else it
returns FALSE.
The event handlers are declared using CALLSET_BOOL_ENTRY instead
of CALLSET_ENTRY, and they must return either TRUE or FALSE.
The invocation of boolean handlers will immediately stop if one of them returns FALSE (so-called short-circuit evaluation).
The runtime code generated by gencallset will call the macro callset_debug
before invoking each of the handlers. This macro is passed one argument, which is a 16-bit
integer that is unique to every handler. The default implementation is to save this in
persistent memory; if a crash occurs, after rebooting the value can be examined to help
determine the last function that caused the problem.
Previously, debugging required a compile-time flag set in CALLSET_FLAGS, but this is no longer needed.
For more thorough debugging, you can rewrite the implementation of callset_debug
to do something else with those IDs, such as print them or set a breakpoint.
When you write a CALLSET_ENTRY, the module name and event name are
concatenated to form the name of a function, separated by an underscore.
When you write a callset_invoke, it calls a function named callset_event.
gencallset scans all of the source code and creates these functions; you can
see the output in build/callset.c. So there is no queueing, memory allocation, or
anything complicated — these are just ordinary function calls. The trick is to do all
of the work at compile-time.
Because event handlers are just function calls, they can sometimes become deeply
nested. For example, a start button press can cause many other events to be
thrown. On the 6809 hardware, the stack size is limited and a stack overflow can
occur if functions nest too deeply, AND a call to task_sleep is made. As long
as you don't sleep, there is no problem; the limitation is in the size of the per-task
stack area. start_game and test_start are particularly problematic
sometimes. Avoid sleeping in such handlers to be safe; if necessary, use a periodic
function instead.
A template is a crude implementation of a reusable code block, similar in purpose to C++ templates but much more limited. Were FreeWPC written in C++, regular templates would have been used here.
Templates end in .ct (for C template) and are always kept in the drivers/ directory. A template file is instantiated via commands in the machine description. The script ctemp converts a template to one or more .c or .h files, which are then compiled normally. Thus, templates can contain RTTs or event handlers just like any other file.
A template contains ordinary C code, plus special template directives. These always begin with two at-signs (@@). The list of valid directives, in the order that they are normally used, is listed below:
@@class@@parameter name@@file@@Within the C code, you use single-at (@) references to substitute the values of template variables into the text. For example,
@@class xyz
int @class_variable
would be translated into the following plain C code:
int xyz_variable
Notice that template variable names cannot contain underscores.
You define your own template variables with the @@parameter directive. Some variables are predefined by the code generator:
@class@self@instanceVariable substitutions can occur just about anywhere. For example, you can use a variable as the argument to the @@file directive. In fact, this is generally how you would use it. Consider the following:
@@class widget
@@file @class.h
@@file @self.h
@@file @class.c
@@file @self.c
Here, the template named widget is defined. It generates four
different source files: two based on the name of the class (widget.c
and widget.h), and two based on the name of each instance.
If there were 2 widgets instantiated by the machine, say a left widget and
a right widget, then we would have left_widget.c, left_widget.h,
right_widget.c, and right_widget.h.
The use case for templates is to define reusable device drivers. From game to game, there is commonality on how certain devices should be treated, although the parameters are different.
Typically, a device requires a number of different functions to control it. Most devices need a realtime function if hardware accesses must be frequent. A periodic function can handle less critical actions. APIs are defined for the rest of the logic to access it. All of this can be put into a single template, then instantiated by each machine with values for the machine-specific parameters.
Currently there are device drivers for all of the following:
spsol.ct)
These are switch-activated coils, like slingshots, jet bumpers, or kickbacks. When the switch activates, the associated coil is fired. The driver allows you to enable/disable the automatic action, as well as defining the length of the coil pulse. The switch is also briefly ignored afterwards to prevent rapid-fire action.
spinner.ct)
The driver is useful because it handles the fact that spinner switches activate very fast. It polls the spinner switch at a fast rate and keeps a count of the number of activations, which are then processing by a slower callback at normal task time.
duty.ct)
This is typically for a diverter that needs to be held on for a long period of time, like TZ's left ramp. Enabling the coil begins by applying a high power pulse, then dropping to a lower power duty cycle (typically 25% but it is configurable.) It is emulating what the Fliptronic flippers do with a power and hold coil, except that there is only one physical coil.
bivar.ct)
Used by the Twilight Zone clock and the World Cup Soccer ball. For when there are two inputs to the motor to control direction.
drop.ct)
A generic, single drop target driver. Handles software knockdown when capable, and retry functions.
Fatal errors when detected will cause the system to reboot. The system
API fatal() can be used to cause a fatal error at any time.
A list of the error codes is kept in include/sys/errno.h.
There is also the nonfatal() API
for logging errors which will not cause a reset. These are audited
and when DEBUGGER is defined, it will display a brief message on the DMD.
The core operating system code throws fatal errors in several key circumstances:
Because tasks continue to run until they give up control, a "hung" task will hang the entire system. At realtime level, this can be detected by seeing that the current task has not changed for a certain period of time. This works because realtime uses interrupts.
The number of tasks is bounded, to prevent runaway task creation/ memory allocation. Many other subsystems do the same whenever a limit is reached.
As a result, you never need to check the return value of a task_create
call, because if it fails, then control is never returned to you.
This is almost
always because someone called pinio_nvram_unlock() and forgot
to call pinio_nvram_lock() afterwards, or executed a task_sleep
in between.
There are a number of ways to implement timers. There are slight differences between them all.
The first way is just to use task_sleep from within task context.
A task can sleep in between actions to enforce a particular delay.
Note that this gives a minimum delay; the actual delay may be longer.
If you are not in a task context that can sleep, you can create a new task which is allowed to sleep. Starting and stopping such tasks is equivalent to starting and stopping a timer. Querying the task by its GID is equivalent to checking if the timer is still running.
If you want to use this model, consider using the timer_xxx APIs,
which do this underneath a standard API that clearly shows what you are trying
to accomplish.
Because these timers are based on tasks, they do not need to be statically declared in advance. They are limited in number only by the maximum number of tasks.
Sometimes you may want a timer, but without the overhead of a separate task.
Then use the free_timer_xxx functions. Free timers
are small counters that are updated in realtime context. They do not
support pausing (hence, the name) but can be started/stopped. The set
of all free timers must be declared in advance in the machine
configuration file.
Free timers are useful for the case when you need to run a timer, and no code needs to be executed when the timer expires. You can always write code to query the timer value, but if it expires to zero, then nothing special happens. One common use is for detecting certain playfield shots when a series of switches needs to activate within a certain amount of time, say 1 second apart.
The 6809 processor does not allow all of the ROM image to be accessible at once. 32KB of the ROM (the uppermost portion) is fixed and can be accessed at all times. The remaining ROM is divided into "banks" or "pages" of 16KB each. Access to these areas is a little more complicated.
You can use the page_push and page_pop functions to change the
current banked area. You must only call these from code running in the fixed
area, otherwise the current function becomes inaccessible! Many kernel functions
use these to retrieve data that is placed into specific, named banks (e.g.
fonts and DMD transition functions).
The GCC6809 compiler supports function calls across all of the banks without much effort from the programmer. Each C file that is compiled must have all of its code and constant data placed into the same bank. You specify which bank via the Makefiles. For example, suppose you add a new file to the common directory, named newfile.c. In common/Makefile, the line:
COMMON_BASIC_OBJS += common/newfile.o
adds this module into the COMMON_OBJS list; all of these objects are part of the COMMON_PAGE bank. In the top-level Makefile, you can see that COMMON_PAGE is mapped to bank 56.
There are a number of predefined sections, like COMMON, EFFECT, TEST, and TEST2. More can be added by modifying the topmost Makefile. Each of these is then mapped to a physical bank of ROM; it is possible for multiple sections to map to the same physical page.
The Makefiles only control the placement of code and data. To make function calls between sections work correctly, you also need to modify the C prototypes.
When the compiler encounters an ordinary prototype, such as:
void foo (void);
then a call to foo() is an ordinary call with no consideration for banking.
However,
__attribute__((page ("56")) void foo (void);
says that foo() will be physically placed in page 56, and a far call might be
needed. If the calling function is not in page 56, then the compiler will emit some
special code to switch the bank, make the call, and restore the bank. This even
works when the caller is in a different bank and not in the fixed region. When
each file is compiled, it is told which page it is in using the -mfar-code-page option.
Because this mechanism is used so often, there are macros to make it easier. The above prototype could be rewritten as:
__common__ void foo (void);
See include/env.h for a list of the macros that can be used. In general, every section has an attribute that can be used like this.
Because placement is a somewhat manual procedure, it is possible for a bank to become full. When that happens, code often must be moved or new sections added. This is rare but does happen from time to time.
The core APIs provide a common set of hardware access functions that can be used by game designers. The interfaces are designed to be portable to all pinball hardware platforms and all machines. However, the underlying implementation may vary.
These functions do not access the hardware directly, but instead, they call into a device driver which may be platform-specific. See the section on Platform APIs for information on drivers perform the actual I/O.
These APIs are intended to be called from the application layer (what other operating systems might refer to as "user space"). For example, there are core APIs to poll the state of switches (active, or inactive?). Under the hood, the switch driver must deal with the fact that switches are arranged in rows and columns, and that some switches are optos: this is all transparent to the user.
Core APIs are all defined in the kernel directory. Any functions
which are not static may be called from game developers from within
normal task context at any time.
Switches are present in all pinball games to receive input from the ball and the player.
Switches are numbered from 0 to NUM_SWITCHES-1.
How the switch numbers are mapped to actual inputs is platform and
machine dependent. The machine description names the switches and defines
their physical characteristics.
Each switch entry can declare the following flags, which alter the way that the switching code will manage it. The capitalized name is used in the C code; the lowercase name is the spelling used in the machine config file.
Says that this is an optical switch, which is active low rather than active high.
Says that the event handler (CALLSET_ENTRY) should be thrown in both directions:
when it becomes inactive or active.
Handlers for edge switches generally
need to check which type of transition just happened, by calling
switch_poll_logical, and then act accordingly.
All counting switches in a ball container must be edge switches, so that the device count can be updated correctly. The shooter switch is also usually declared edge.
Says that this switch should only be serviced during a game.
Says that this switch is on the playfield, and can be activated by a ball,
as opposed to cabinet buttons, motor optos, etc. A playfield switch
event will normally set the valid playfield flag. It will also cause the
any_pf_switch event to be thrown, which is normally used to score
during "frenzy" modes.
This is the default; use the noplay tag to indicate that a switch
is not playfield scoring.
Set for a SW_PLAYFIELD switch for which the valid playfield flag should not automatically be set. This is used for switches that might unreasonably activate not due to a ball, such as a misaligned jet bumper that continually fires.
These are the switches that you can trigger at ball start and drain, and still get the ball back.
Says that this switch handler should also be called in test mode.
Normally in test mode handlers are not called, but important switches
like cabinet buttons require this. If this flag is set, handlers will
need to test the in_test variable to see if we are in test mode,
and act accordingly.
In addition to polling their levels, the switch driver will detect when switches have changed state, and invoke their event handlers. The switch entry in the config file names a function to be called when these changes occur.
The driver performs debouncing, so that rapid open and close
are not considered. By default, a switch is processed if it remains
active for only 4ms. You can declare a larger debounce time using the
debounce tag. A transition that lasts for less time is ignored.
Switch entries can also declare an associated playfield lamp; when this is done, valid activations of the switch will cause a brief flicker of the lamp.
switch_pollSwitch readings are polled by the switch driver continuously; this
API just returns the results of the last time the switch was
examined. How often switches are polled is platform-dependent;
on WPC, it is every 2ms.
switch_poll_logicalswitch_poll
but it inverts its result for optos.
rt_switch_pollswitch_poll, but only for use inside realtime tasks.
It is slightly more efficient. It always returns a physical level;
drivers must invert for optos.
Controlled lamps are used to highlight shots on the playfield and to keep track of the progress of scoring features. Unlike general illumination, each lamp is individually controllable by the CPU.
Lamps are numbered from 0 to NUM_LAMPS-1. For each lamp, there
are several flags that can be set to determine its physical state.
The job of the lamp driver is to combine all of these flags into a
single output value, and send that to the underlying hardware.
Every lamp has a basic state, which is either on, off, or flashing. The basic state of a lamp is a per-player value and is automatically saved and restored as players change in a multiplayer game. They are controlled by the following APIs:
lamp_onlamp_offlamp_testlamp_togglelamp_flash_onlamp_flash_offlamp_flash_togglelamp_tristate_onlamp_tristate_offlamp_tristate_flashThe basic state may be temporarily overriden via a lamp effect. An effect can allocate a lamp, thereby overriding its basic value. When the effect finishes, the lamp shows its basic state again. See Lamp Effects.
In the machine description, you can attach a color and coordinates to each lamp. These are not used by the core OS, but they can be used by lamp effects that manipulate the lamps in certain patterns.
Solenoid outputs are used for many purposes. We use the term 'solenoid' or 'coil' generically to refer to any writable, high power output. They could be connected to coils, flashers, or motors. Like the lamps, they are individually controllable.
Unlike the controlled lamps, the core API does not permit you to turn on a solenoid permanently. Instead, you either use a pulsing API, which turns it on for a short period of time (and the system turns it back off for you), or you use a custom driver to control it.
Pulsing is used for solenoids that are operated infrequently, and only for short durations at a time. These are normally used for kickers, poppers, etc. The system queues the requests and executes them one at a time, never in parallel.
The default pulse settings are controlled by the entry
in the machine description, in the "drives" section. The time
parameter sets the total length of time for the pulse. It can be an integer
value in milliseconds, or one of the TIME_xxx values which are in
multiples of 16ms. The duty
parameter controls the duty cycle; use one of the SOL_DUTY_xxx defines.
SOL_DUTY_100 keeps the driver on for the entire interval; SOL_DUTY_50 keeps
it on half the time, delivering half the power; and so on. The minimum duty
cycle is 1/8th full power.
If you do not specify either of these values, you will get a reasonable, but conservative default. Solenoids and flashers may have different default values. A large portion of getting a new game to work correctly is determining the correct values to use here.
sol_requestsol_request_asyncsol_req_start_specificThe flashlamp APIs are similar to the solenoid pulse APIs. A flashlamp pulse is always with a 100% duty cycle, but for a much shorter period of time.
Note: it is currently possible to use the solenoid pulse APIs on flashers, but this should not be done. This will likely be enforced in the future.
flasher_pulseflasher_pulse_shortflasher_startCustom drivers can run for longer periods of time. They are allowed to run in parallel with anything else. They will frequently use duty cycling to avoid keeping the coil 100% active for too long, and thus they often require realtime processing to switch reliably.
These types of drivers are all implemented using templates.
Dot matrix APIs should only be called from display effects.
There are two key objects you should understand: pages and windows.
A display page is special RAM which holds the frame buffer that determines which pixels are turned on and off. Display memory cannot be accessed like ordinary memory, though; it does not have a fixed address.
A display window is an address that the CPU can use to read/write display memory – that is, it sees one of the display pages.
There can be more display pages than windows, so not all pages are accessible at all times. Through mapping calls, you decide which pages are mapped to which windows.
You must go through several steps for each frame you want to write. For each step, there are several APIs to choose from.
dmd_alloc_mapdmd_alloc_low_highDo not assume that the low and high windows are contiguous in memory.
dmd_dup_mappeddmd_clean_page_mapdmd_copy_pagedmd_flip_low_high to swap
them temporarily.
font_render_stringfont_render_string_centerfont_render_string_rightbmp_drawframe_drawdmd_text_outlineThis API should be used when rendering text onto a color image. Each '1' bit
in the mask corresponds to a pixel that should be made black in the final image,
so that the text stands out from the background image. See dmd_overlay_outline
below for a method to do this.
dmd_overlaydmd_overlay_colordmd_overlay_onto_colordmd_overlay_outlinedmd_text_outline.
This is a fairly slow call, as it is requires several AND and OR operations.
dmd_and_pagedmd_or_pagedmd_xor_pagedmd_show_mapdmd_show_otherdmd_show2Prior to showing the image, you can schedule a transition. You call
dmd_sched_transition just before showing to say which transition should
be used.
Other APIs are listed below.
dmd_flip_low_highdmd_rough_copydmd_rough_eraseAt present, sound support is very WPC-specific. The sound board is an external device that is controlled by sending commands over a serial-like interface. Two flavors of sound board are supported: WPC, and DCS. The same APIs are used in either case.
sound_sendSee also the section on Sound Effects for other APIs that can be used.
General illumination allows for additional lighting that is managed in groups of lamps, which are all enabled and disabled at the same time. These are often referred to as GI "strings". FreeWPC supports a maximum of 8 strings.
On the WPC platform, GI can be dimmed, because the controls for these strings are governed through triacs. (On WPC-95, some of the GI are dimmable and some are not.)
gi_enablegi_disablegi_dimgi_enable, but it takes
an extra parameter, which specifies the intensity of the lamps.
Not all hardware supports dimming.
If dimming is not supported, then this API is unavailable and
will cause a compiler error.
On machines with a realtime clock, these APIs let you set and get the current time of day. At present, only WPC is supported.
The chip provides three registers for counting minutes, hours, and days. FreeWPC keeps the calendar time in persistent memory. About once per minute, it polls the RTC and updates the calendar time to reflect any changes.
rtc_readrtc_writeEffect processing is a layer above the hardware which provides controlled access to the lamps, display, flashers, and general illumination. It resolves contention when multiple tasks try to access the same object at the same time through the use of priorities.
The system manages three distinct types of effects: display, lamps, and music. They are fully independent of each other. It is not possible to request that all three types do something in sync with one another. There are similarities between the way each type is handled, but all three have fundamental differences.
Lamp effects manage the controlled lamps, plus the GI and flashers.
These principles apply to all types of effects.
At any time, there is at most 1 effect that is running of each
type. The
running effect is the one which has been started with the
highest priority. Priorities are simple integer values:
the higher the number, the more important the effect is. A list
of predefined priority values is defined in priority.h;
these files begin with PRI and have been carefully defined relative
to one another. Use one of these values when possible.
It is possible for no effect to be running if none has been started. In that case, the current priority is considered to be zero. Actual effects always have a priority that is non-zero.
The running effect is executed in a separate task context, except for
music, which is executed on a separate CPU. The
currently running display effect always has the group ID
GID_DEFF; likewise GID_LEFF for the lamps. Effects
that are started but not running do not have a task assigned to them,
and are simply marked as started in memory somehow. Because they do
not have enough priority to run, there is nothing for them to do.
Effect tasks can be preempted, if a higher priority effect is started later. In that case, the old task is killed, and a new task is started. When the task is killed, there is no guarantee as to what was doing previously, and there is no opportunity for it to do cleanup on its own.
Effect functions must be declared in the machine configuration file. Each entry names the function to be called to run the effect, along with its priority and other optional properties.
The APIs and implementation for display/lamp effects are mostly the same, but there are differences due to the following:
The display and music should always be doing something, even when nothing really important is happening. Thus, these effect modules implement background effects, a way to determine what to do in these circumstances. Lamp effects do not support a background mode; it is OK for no effect to be running, as the lamps have a default state that shows through.
The strategy for handling background effects is as follows: When
there is no explicitly started effect, an event is generated:
either display_update or music_refresh. Any module that can
provide a background effect should implement these handlers.
The handler can decide which effect, if any, it wants to provide by
calling either deff_start_bg or music_request. (If the
conditions are not correct, it can simply return without doing anything,
as with a mode that is not running.) The handler request includes
the ID for the deff or music to be started, plus a priority value. After all of the
event handlers have been invoked, the one with the highest priority
wins and will be started.
The common code provides many of the implementations to deal with the usual cases. For example, while a game is running, the default effect is to show the game scores. Default music codes can be defined for different circumstances: normal game play, ball at plunger, during bonus.
Because handlers can use the values of other variables in the
determination (are we in game, for example?), the calculations need to
be redone periodically, in case the conditions change. To force a refresh
of both the display and music,
call effect_update_request. The update will take place within the next
100ms; the delay is intended to smooth out multiple such requests within
a short time, and the delay is not really noticeable.
When a foreground display or music effect exits, an update will take place immediately. In this case, if display update is needed, then music is not updated, and vice-versa.
If an update is not requested, the display and music will still be refreshed
periodically by the system, about twice per second, as the conditions checked
within the handlers may have changed. Because of this, effect_update_request
is really an optional call, and only forces the update to happen faster.
Display effects are defined in the [deffs] section of the machine
configuration file. Both foreground and background deffs are defined here.
Each deff has an implementation function, a priority, and one or more of
the following properties:
Says that if the effect cannot be started now, to queue it and keep retrying. This flag should be set on higher priority messages that really ought to be seen by the player.
Only applies to D_QUEUED deffs. Says that they should timeout eventually if they cannot be started.
Says that pressing both flipper buttons will abort this effect.
Says that while this effect is running, ball kickouts are paused. This flag is typically set on important instructions or on big scoring effects to allow the player to pause and celebrate.
Says that this is a score award effect; the value of the last switch
score is made available to the effect via the score_deff_get() API.
Says that if the effect is started when it is already running, that it will be restarted (normally, the second call would do nothing). This should generally be set on D_SCORE effects at least.
deff_startdeff_stopdeff_exitdeff_nicedeff_updateLamp effects are defined in the [leffs] section of the machine
configuration file. Each lamp effect defines a function that executes
when the effect is running, plus it says which of the lamps, flashers,
and G.I. strings it is able to modify. Attempts to modify unallocated
objects have no effect.
The objects allocated to a lamp effect are guaranteed to be turned off when the function starts, and are restored to their previous states when the function ends.
A lamp effect can also be declared as shared. A shared leff can run in parallel with the current running leff, and with other shared leffs. Shared leffs have a lower priority than normal leffs; the effect can be masked. Also, shared leffs run to completion and are never preempted.
Shared leffs are usually for indicating low-priority game state that requires constant updating, and where it is not appropriate to use the default lamp state. Normal leffs are intended for light shows.
Lamp effects cannot be aborted by the flippers like display effects.
If a normal leff cannot be started, it is forgotten and never queued, like display effects can be.
leff_startleff_stopleff_restartleff_on, leff_off, leff_toggle, leff_testgi_leff_alloc, gi_leff_enable, gi_leff_disable, gi_leff_freeleff_exitThe main difference with sound and music is that the main CPU does not implement the effect; they are generated by a separate board. The main CPU sends commands to the secondary board to request that a particular sound be played. Because of this, no running task is required for the effect. (Platforms other than WPC may differ in this regard.)
Sounds and music may also overlap, depending on the capabilities of the sound board. Each platform defines a set of independent channels. When a sound needs to be played, you can specify a subset of channels that it can be played on. If one of them is free, the sound can be played. The system tracks which channels are free or in use.
There is no easy way to know when the sound has finished playing. The sound board is capable of sending responses back to the CPU board, but this has not been explored thoroughly yet. For now, sound calls need to specify how long the sound will take to complete, so that the channels can be deallocated. Exact precision is not really necessary here, unless trying to string together a sequence of speech calls.
The music channel is treated special, because it is the one used for the running background track. All other sounds are transient, whereas the music plays in a constant loop.
Some sound effects need to preempt the background music, so that both are not heard simultaneously. When a sound effect allocates the music channel, that kills the background music first; when the temporary sound finishes, the background is automatically restarted.
music_updatesound_startsound_send,
which is the low-level API to send a command to the sound board.
This API handles priority, channel allocation, and tracks the duration
of effects.
sample_startspeech_startmusic_effect_startmusic_disablemusic_enablemusic_timed_disableTypically, groups of lamps are often used for a similar purpose.
A lamplist is an ordered array of lamps that are often controlled together.
Each lamplist has a unique ID, and is internally represented by an array of
the lamp values, terminated by a special lamp code named LAMP_END.
The lamplist APIs provide convenience whenever many lamps need to be updated at once.
lamplist_indexlamplist_first_entrylamplist_last_entrylamplist_previous_entrylamplist_next_entrylamplist_applylamplist_apply_nomacrolamplist_test_alllamplist_test_anylamplist_apply_leff_alternatinglamplist_step_incrementlamplist_step_decrementlamplist_build_incrementlamplist_build_decrementlamplist_rotate_nextlamplist_rotate_previousA transition is an interim effect that runs between the end of one display effect and the beginning of another. By default, a new effect will simply kill the old one and overwrite it with a new image.
Transitions are scheduled – a handler is installed that runs the next time that a new image is shown. Instead of showing the image right away, the transition handler kicks in and displays some combination of the present view and the new one. When the transition finishes, the display should consist entirely of the new image.
Currently, all DMD transitions are contained in common/dmdtrans.c and segment transitions are in kernel/segment.c.
In addition to the display and music effect updates, there are a couple of other things that need to be refreshed periodically. The default states of the lamps, outside of any running effect, sometimes need to change depending on a complex combination of factors.
For example, consider a feature that needs to be disabled temporarily during any multiball. Instead of using the lamp bit to track whether the feature is enabled, we should use a separate bit flag, and then draw the lamp if the flag is set and the feature is not masked by a multiball.
The lamp_update event is generated periodically by the system
to give modules a chance to recalculate the states of the lamps.
Currently, it is generated about once per 500ms. Code can request
a faster update by calling effect_update_request as before.
Likewise, sometimes solenoid-driven devices require a periodic update
as well. Ball scoops that raise from the playfield, like the trap door
on Funhouse, can either be up or down, and the desired state depends on
a number of things. A separate device_update event is thrown for
the purpose of updating these. It works identically to the lamp_update
function; however, these events are generated more frequently, as they are
more important to be right in a timely manner.
Note that device update is disabled during ball search, so that search handlers can complete control of the devices to try to find the missing pinball.
The ball tracking module manages the locations of all of the pinballs in the machine. It is implemented in common/device.c and uses container declarations in the machine description.
A device is defined to be something which can hold and release pinballs. (Earlier versions of the software used the term container.) A device is comprised of one or more switches which count the number of balls in the device, plus a release solenoid that can be pulsed to force one ball to be removed.
Ball troughs, kickouts, poppers, saucers, and locks are all just different shapes that a device can take. Some containers only hold one ball, while others can hold many.
For each device, the ball tracker maintains the following state information:
The desired count is crucial to understanding the module's behavior. Whenever the device has more balls than desired, the ball tracker will initiate one or more releases to try to bring the count down to the desired number. Otherwise, any balls that enter the device are kept there; this is used to implement ball locks.
Software requests to kick a ball are always done by manipulating the desired count. The device module has complete control over the actual release solenoid.
The trough device is the only device which normally desires to keep any balls. Playfield devices generally desire zero balls unless a lock has been enabled.
Device switches must be declared as edge switches, which invoke the event handler on both open and close transitions. The tracker simply updates the current count on either type of transition.
The ball tracker also maintains a count of the number of balls in play, which is the total number of balls installed, minus the number of balls that are seen in devices, minus the number of balls declared "missing". Missing balls are flagged when a game is started but the balls are not all seen.
The ball tracker also generates new per-device events. The name of
the event includes the name of the device: for example,
dev_trough_enter.
The enter event is generated when the current count goes up by 1. Game code should normally always register handlers for the enter events rather than individual switches. Note that a switch closure does not always indicate enter: consider an eject that is trying to kick out but the kicker is weak, so that the ball falls back in immediately. Also, when a ball trough releases a ball, a whole series of switch closures occur rapidly, but eventually it stabilizes. The ball tracker waits a little while after switch change before it begins to take action.
When a release is necessary, the module first generates a kick_request event. This is a boolean event; handlers can return FALSE if the request should be postponed for some reason. In that case, the module will retry again later. Once the request is allowed, a kick_attempt event is generated just before the solenoid is pulsed. This is typically caught in order to play a sound effect. If the pulse fails, the attempt event may be thrown multiple times.
The kick_success event is generated when the release is deemed successful: when the device count goes down by 1. The kick_failure event is instead thrown when an attempts fails repeatedly and the device is abandoned.
The trough device is special; every game must have one. The system completely handles the device events generated for the ball trough.
The function device_add_live is called whenever a new ball
is successfully served. The live count of the machine is the
number of balls in play for the purposes of game code (i.e. not counting
missing pinballs or balls locked on the playfield from previous games).
The single_ball_play event is generated when the live count
is reduced to 1. Multiball modes use
this to stop properly. There is also a ball_count_change event
invoked on every change of the number of balls in play; some games use
this to update playfield multipliers.
Likewise, device_remove_live is called whenever a ball drains.
It ultimately is the one that generates the end_ball event
when all balls have drained.
Live count is not necessarily the actual number of balls in play. If a missing pinball suddenly reappears, the live count is not increased.
The device system generally assumes that there is one ball-sensing switch for every ball that might be in the device. Thus, a 4-ball trough needs 4 switches. The tracker recounts balls periodically and does this just by polling each switch to see if a ball is present or not.
For some devices, this is not possible. Consider the Twilight Zone gumball machine, which can hold 3 or 4 balls but there are no switches inside to verify this. To handle this case, virtual balls are supported. This is a per-device counter that is completely software controlled. APIs exist to add/remove virtual balls that cannot be seen by switches. Machine-specific code must be written to manage this.
When a device driver knows for sure that a ball has entered the device,
it calls device_add_virtual. The extra ball will be tracked and
counted even though there is no switch to see it anymore. When virtual
balls are added, the device enter event is thrown as usual, as
if a physical ball had entered.
When it knows that a ball has exited the device, it calls
device_remove_virtual. This decrements the virtual ball counter.
This behaves exactly like when a real ball is kicked out, so the
kick_success event is thrown and the live ball count goes up.
The FreeWPC test mode works similarly to the original WPC menu system. However, there are extra options and a few important differences. For testing core changes to FreeWPC, it is often more convenient to use test mode than to play a game.
You can navigate the menus with the flipper buttons in addition to the coin door buttons. Use Start to enter, and double flip to exit.
When compiling with the MACHINE_TEST_ONLY option, the game
will boot directly into test mode, and there is no provision to start a game.
All of the standard adjustments are supported. There are also a few new adjustments.
Timed Game allows you to switch between normal game play and a timed game. Machines can implement different rules for timed play or not. They can also disable it entirely.
Payment Type configures whether the display asks you to insert coins, bills, or swipe a card.
All of the standard audits are supported.
Presently, audits are kept as 16-bit integers, so they max out at 65535. This will probably change at some point.
The test menu contains roughly the same tests as in the actual WPC games, with some differences.
The solenoid test lets you control the duration and duty cycle of each coil pulse by using the flipper buttons.
The development menu contains a series of additional tests that are probably only relevant to developers and not to end users. Current tests include:
Cycles through all of the available fonts. Use the flipper buttons to scroll through the alphabet. The machine will try to display as much as possible at once, depending on the font size.
Cycles through all display effects. Press ENTER to activate a deff,
as if deff_start were called. For long-running effects, press
ENTER again to stop it via deff_stop.
Cycles through all lamp effects. Press ENTER to activate a leff,
as if leff_start were called. Press ENTER again to stop it.
Remember that multiple lamp effects can be active simultaneously if they are declared as shared and they do not overlap in the lamps that they use.
Cycles through the lamplists. The flipper buttons cycle through the different ways that the lamplist can be controlled. The default is to turn on all of the lamps. You can also strobe them, flash them, etc.
Cycles through each of the ball devices, telling you its current status (how many balls it sees, kickouts pending, errors). The flipper buttons will scroll through a set of actions (empty, kick 1, etc.); press ENTER to perform the action displayed.
This test also continuously shows the global device status, such as how many total balls are detected and whether any balls are missing.
Tests the random number generator.
Cycles through all of the dot-matrix or alphanumeric transition effects.
Cycles through all of the display frames.
Entering this test forces a fatal error, which should restart the system.
Starts randomly starting and stopping display effects until exiting the test.
Cycles through the symbol bitmap, a collection of arrows, boxes, etc.
Advance through the various combinations of players per game and player-up, to show how the score screen would look.
Displays vital data about the PIC.
Lets you interactively scroll through the CPU's memory and alter values.
The files in the common directory provide a library of generic pinball functions that can be reused from game to game. Some of the more important ones are described in detail in this chapter.
FreeWPC implements a weak form of coin switch handling. It is not very robust and does not time the coins as it should.
If FREE_ONLY is defined, it will build a ROM that doesn't require
coins.
This module tracks lit and collected extra balls. Lit extra balls can be easy (lit until end of game) or hard (lit only until end of ball).
In the machine config file, you should tag the lamp that indicates
an extra ball is lit as extra-ball, and the shoot again lamp
as shoot-again. Then the lamps will automatically update for you
when these APIs are called.
light_easy_extra_balllight_hard_extra_balleb_light_adjustablecollect_extra_ballcan_award_extra_ballThere is also an API special_award to award a special, but
it does not manage any lamps automatically. It just obeys the adjustment
to award whatever has been configured for special, and fires the knocker.
FreeWPC implements a tournament mode module. Note this is what newer Stern games refer to as competition mode.
Tournament mode can be enabled globally in the adjustments menu, or it can be enabled by holding down the left flipper button briefly before starting the game. A message will indicate that tournament mode has been enabled. It affects all players in the game.
Software should check the tournament_mode_enabled boolean variable
to determine if tournament is in effect.
Machines don't need to do much to produce a working ball search function.
The common code knows how to pulse all of the regular solenoids.
It also knows which not to fire anything marked as a motor
or flasher.
You can mark a solenoid in the machine description with nosearch
to ignore other solenoids.
Machines that need to handle a particular power driver in a non-standard
way (by using a template driver) should implement a ball_search
event handler, and mark the
associated solenoid as nosearch.
The ball_search handlers will be called along with the automatic
pulsing.
For example, on Funhouse, the Rudy saucer eject needs to make sure that the mouth is open before kicking.
Solenoids associated with ball devices are also skipped unless they are empty.
Game code can call ball_search_timeout_set to set the amount of
idle time that must expire before a ball search will occur. The default
is 15 seconds. The timer resets anytime a playfield switch (one marked
with the SW_PLAYFIELD flag) triggers. You can also manually reset it
manually using ball_search_timer_reset.
Call knocker_fire to fire the knocker. If a coin meter is attached
to the knocker coil, it will not be pulsed. If the machine defines a sound
effect for knock, that will be played instead of pulsing a solenoid.
Adjustments are 8-bit variables kept in persistent storage. Each group of related adjustments is checksummed to verify integrity. Adjustments can be checked by just reading the variable; there is no special API to do so.
Audits are 16-bit variables kept in the non-volatile area of memory. They
are generally incremented via the audit_increment API, which adds 1.
They can also be incremented by an arbitrary value, via audit_add; or
they can be assigned via audit_assign.
A global playfield multiplier is supported; use score_multiplier_set to
change it. It is automatically set to 1 at the beginning of each ball.
Scores can be stated in two ways: as a 5-byte, binary-coded decimal value, or as an 8-bit "score code". The long values allow for arbitrary values up to one billion points. The short values are more compact and index a table of common score values, which are listed in the machine config.
The first set of APIs operate on arbitary BCD score buffers:
score_zeroscore_copyscore_addscore_subscore_mulscore_compareThe second group of APIs increment the current player's score by a fixed value.
scorescore_longscore_multiplescore(), but also takes a multiplier argument. This multiplier
and the global score multiplier are taken into account.
score_long_multiplescore_long and score_multiple. This is the most
low-level API; all others ultimately call it.
score_long_unmultipliedThe third group of APIs, called the ladder APIs, add score according to some rule.
A lamp timer is a countdown timer which is tied to a playfield lamp. The amount of time remaining controls how fast the lamp flashes. When the timer reaches zero, the effect stops.
You declare a structure of type struct lamptimer_args, which
names the lamp and the initial timer value, in seconds.
The lamp is modified as part of an internally generated lamp effect; thus, the basic state of the lamp is retained. While the lamp timer runs, the basic state is overriden by the effect.
lamp_timer_startlamp_timer_findlamp_timer_stopThe score rank module is optional. When enabled for a game, the system will
monitor the relative changes in player's scores over the course of a multi-
player game, and throw a rank_change event whenever the current player
moves into a new place. It is up to each game to decide how to handle it.
To write a timed mode, you need to do two things:
First, create a structure of type struct timed_mode_ops and fill out
all of the required information:
GID.
When the mode is started, a task will be created with this group ID.
system_timer_pause when the mode should pause for the "usual cases" only.
Use null_false_function if the mode should not pause at all. You can
supply your own function if you need custom behavior.
Second, handle several system events which affect the mode's operation:
timed_mode_music_refresh.music_refresh handler, passing it a pointer to the mode ops struct.
timed_mode_display_update.display_update handler, passing it a pointer to the mode ops struct.
From outside the mode itself, other modules call these APIs to interact with your mode. The mode ops struct is part of the public interface.
timed_mode_begintimed_mode_endtimed_mode_running_pThe ball serve module is the preferred API for adding balls to play. It uses the ball tracking APIs to program the ball trough, but adds support for autoplunging and multiball logic.
The system supports games with autoplungers or manual plungers. Auto launch support requires that the machine defines three things: the launch button switch, the launch solenoid, and a shooter switch.
The base API serve_ball simply kicks a ball out of the trough.
It also resets the valid playfield flag and refreshes background effects.
This is the same call made by the game state machine during start ball.
Use serve_ball_auto instead if you want to ball to be autolaunched
as soon as it is served successful. Otherwise, it is identical. If
a ball is served without autolaunch, it can be launched later by calling
launch_ball. This happens automatically when the launch button is
pressed.
You normally do not need to call either of those APIs, except in some rare cases.
The preferred way to start multiballs is to use set_ball_count,
which sets the number of balls in play, or add_ball_count if you
want to say how many balls to be added. These use serve_ball_auto
to do the work. They work on manual plunger games too. Only one ball
will ever be placed in the shooter at a time.
serve_ballserve_ball_autolaunch_ballset_ball_countadd_ball_countFreeWPC implements the Mute and Pause feature that was included in the Twilight Zone home ROM. It is optional at compile-time. The machine must have an extra-ball buyin button for it to work.
When compiled in, and the adjustment "MUTE/PAUSE" is set to YES, then pressing buytin during a game will hold the flippers, disable ball kickouts, and turn off the background music. To continue, press the button again. It will also timeout automatically after 15 minutes.
This chapter explains in detail how FreeWPC fonts and graphics work.
FreeWPC uses simple bitmaps to represent fonts, icons, and full-screen graphics. A bitmap is an array of rectangular pixel data. The data bits are prefixed by a short header which contains the width and height information. The image is stored one row at a time, starting from the top. Within a row, the bits are stored in little-endian format, with the least significant bit representing the leftmost pixel.
A font is a set of bitmaps mapped to a contiguous sequence of characters. The font header identifies the ASCII code for the first bitmap that is represented; this saves space by not needing to encode the low-valued ASCII control characters. The nominal height of the characters is also defined, which allows for descenders.
A frame is a full-screen bitmap, 128x32. Since these are fairly common, special APIs are used to draw them that are more optimal, and the width/height is implied and not actually stored in the bitmap. A frame can have 4 colors per pixel (3 shades plus black) so the frame is actually stored as two consecutive bit planes.
Each bit plane of a frame can optionally be compressed using run-length
encoding (RLE) or zero suppression to save space. An uncompressed
frame requires 1KB, which limits the total number of frames in a ROM
significantly. Every frame header includes
a flags field, which indicates which if any compression method was used.
The frame decoders are written in assembly language in platform/wpc/dmd.s.
Compression is a balance between ROM size and processor speed. The best compression methods would take far too long for the 6809 to decode. The methods used strike a compromise. In particular, bitwise operations are avoided in the decoders because of the 6809's inability to do bitlevel operations quickly.
Useful for images that contain long sequences of the same byte value. Each long run is replaced by a 3-byte code: the first byte is the code 0xA8, which signals an escape sequence, the second byte is the number of bytes divided by 2 (so the length must be even), and the third byte is the repeated value. A special two-byte code also indicate end of frame, so that the decoder does not need to count the number of decoded bytes.
The sparse format encodes images that contain mostly zeroes, or which have large transparent sections that do not require writing to the display. The image is encoded as a series of (length, data, move) triples, where the move value says how many bytes forward to move the cursor.
The script fontgen2 converts TrueType font files (.ttf files) into WPC font files (.fon).
The font_render_string family of APIs is used to draw text to
the display. The arguments are a font object, the x and y coordinates
where the string should be placed, and a string. The string can be
a constant string literal or the global format buffer (see below).
There are three variants which justify the text differently: centered, left-justified, and right-justified. Centering is done both vertically and horizontally; the others only justify left-to-right, and the y coordinate always specifies the top of the print area.
Text printing is CPU intensive. Display effects should take care not to print text more than needed. It is often more efficient to print strings to an overlay buffer, and then copy them to the main display page, if the same text needs to be printed over and over again.
FreeWPC contains a printf-like function for formatting text
strings with variable data, however, it is not quite the same.
The sprintf() function formats a string into a unique,
global buffer named sprintf_buffer. It is like the
actual C function of the same name, but the first argument is implied.
The format specifiers are also slightly different. Here is a list of the valid ones:
%b%8b would print a 4-byte BCD
string containing 8 digits. Also, this format will insert commas
(or periods) between digits as necessary.
%c%dU8).
%E%ldU16).
%lxU16).
%p%s%wU32).
%xU8).
Like in C, you can insert a number in front of the format letter to limit the output to a particular width. If the length begins with '0', then it will be padded with leading zeroes if necessary.
The formatter does not support signed numbers, and will print them as if they were declared unsigned.
The formatter is not particularly efficient for printing large decimal values, as the 6809 is not very good at long division.
The list of all frames compiled in the ROM is defined in an image map. This is a machine-specific file that says which images to copy into the final ROM image.
There can be multiple frame list files. The common code provides a frame list of standard images, like the FreeWPC logo, which go into every build.
Each entry in the image map gives an image to be imported, such as a PGM graphics file, plus an optional frame ID. The frame ID becomes a C #define that refers to the image from the source code. The image linker writes a file build/imagemap.h which contains a list of all the frame IDs. Frame IDs are optional for the internal frames in a sequence (a for loop would only need to name the starting and ending frame).
It is the job of the image linker to decide what compression techniques to perform. The linker is told the maximum amount of space that can be used for images, which is the total size of the ROM minus any sections reserved for source code. If all images fit without compression, then all is well.
Otherwise, the linker will perform as much as compression as necessary. At present, images are compressed in the order that they were declared. A future enhancement would be to start with those images that can be compressed the best without requiring much more CPU power to decode them.
In some rare cases, trying to compress an image fails to produce a smaller buffer. The linker notices this and leaves such images uncompressed.
Game developers do not generally need to understand all the details of initialization. However, this may be of interest to system developers.
Here is a rough outline of the steps taken to initialize the FreeWPC environment:
main function is called (on real hardware,
this is called from the RESET handler; in simulation, this is called
by the OS like any other executable). main is a platform-dependent
function. It can be thought of as the continuation of low-level
initialization, except written in C.
main calls into freewpc_init, where the common
initialization begins. This is defined in kernel/init.c.
init event is thrown
here, so any new subsystems added just need to catch this event to initialize
themselves. It is important that modules do not interact with each other during
this phase, because the order of init calls is not guaranteed.
init_ok event is thrown to allow modules to perform a final
initialization, which may require interaction with each other. Modules can
return FALSE if a factory reset is required. In that case, the system will
reboot after performing the factory reset.
init_complete event is thrown, to signal that all modules are
ready.
The platform APIs define how the hardware is accessed on a particular platform. They only need to be written once, and then all games on that hardware architecture can be supported.
If you are developing code for a WPC machine, you do not need to understand the platform APIs in detail, because they are already written for you. If you plan to port FreeWPC to a different hardware architecture, then you will need to write these functions first before you can create any game code.
Every platform must define a header file in
include/platform/platform.h.
This file defines a number of inline functions, all of which begin with
pinio. It also defines a handful of constants, which begin
with PINIO.
Platform APIs are the functions that perform the physical I/O. On WPC,
these functions read and write the WPC ASIC registers. All reads and writes
to I/O registers should use the readb and writeb macros.
How these work depends on the platform. For most platforms, these turn into
memory-mapped operations. However, they could also use a parallel port or
USB.
Platform APIs are not suitable for game code to call directly, as they are too low-level. Instead, game code uses the core APIs to access a device driver; the device driver then uses the platform APIs.
pinio_write_switch_columnpinio_read_switch_rowspinio_read_dedicated_switchespinio_read_flipperspinio_read_flipper_buttonspinio_read_flipper_eospinio_read_localepinio_write_lamp_strobepinio_write_lamp_datapinio_write_solenoid_set (bank, value)pinio_read_ac_zerocross (CONFIG_AC)pinio_reset_soundpinio_write_soundpinio_sound_ready_ppinio_read_soundpinio_write_gi (CONFIG_GI)pinio_read_triac (CONFIG_TRIAC)pinio_write_triac (CONFIG_TRIAC)pinio_watchdog_reset (CONFIG_WATCHDOG)pinio_active_led_toggle (CONFIG_DIAG_LED)pinio_dmd_window_set (CONFIG_DMD)pinio_dmd_window_get (CONFIG_DMD)pinio_dmd_window_ptr (CONFIG_DMD)pinio_dmd_set_visible (CONFIG_DMD)pinio_dmd_request_interrupt (CONFIG_DMD)pinio_parport_write (CONFIG_PARALLEL_PORT)pinio_set_bank (CONFIG_BANK_SWITCH)pinio_get_bank (CONFIG_BANK_SWITCH)pinio_read_timer (CONFIG_HIGHRES_TIMER)
pinio_write_timer (CONFIG_HIGHRES_TIMER)Native mode lets you run FreeWPC directly on your host build machine. It is a form of emulation, like PinMAME, except the emulator is "built-in" to the game program itself. It works on both Linux and Cygwin systems.
There are two major differences in a simulation build. First, certain low-level OS and CPU specific features must be replaced by new components that work under your native operating system environment. In particular, 6809 assembly languages are not used at all in this mode. We use either portable C versions of these functions, or comparable functions that are provided by the host OS.
Second, I/O reads and writes do not directly control real hardware, but
are instead emulated internally by the program itself. All I/O is
initiated by the platform APIs and uses the readb and writeb
functions to address the hardware. In native mode, these functions define
a separation between the "program" and the "simulation".
Native mode is most useful for debugging and testing the higher layers of FreeWPC, which aren't as hardware or timing dependent. This is only possible because the majority of the code is written in C.
To compile in native mode, just add NATIVE=y to your .config.
Instead of a ROM, you'll get an executable that can be run directly.
Native mode programs are no longer realtime. There are two main concerns related to scheduling.
First, normal multitasking (the task_xxx APIs) is accomplished using the pth library, an open source, nonpreemptive thread library. A thin wrapper is provided which has the same APIs as in 6809 mode, but which calls pth functions for the implementation.
Periodic functions must be called occasionally from a special thread, since FreeWPC no longer provides the scheduler.
Interrupts are also simulated from an ordinary thread. The native operating system probably does not allow threads to sleep for as little as 1ms; on Linux the minimum sleep time is on the order of 10-20ms instead, and under Cygwin, it may be even longer. When the interrupt thread is woken up, it checks the system clock to see how much time has actually elapsed, and invokes the interrupt handler multiple times. The net effect is that interrupt handlers are invoked the same number of times as they would be on real hardware, but not the same way: they are called in batches, rather than being equally spread out.
By default, the simulation runs at the same speed as the native system clock. It is possible to speed up the simulation by a constant multiplier, which is sometimes helpful for rapid testing.
Permanent and non-volatile variables are supported; their values are saved in files across program runs. Non-volatile variables are not actually write-protected though. This may be implemented in the future.
The native program includes a simple, curses-based user interface to show what it is doing. Switches, lamps, solenoids, and GI states are redrawn in the window as necessary. Also, the DMD or alphanumeric display is emulated.
The UI support is separated into a separate module from the core emulation, and could be replaced by something more sophiscated in the future, for example, using a window library like Qt or GTK.
Switches can be simulated by pressing keys. The ESC key terminates the simulation. Many of the key mappings are the same as what is offered in PinMAME.
Advanced simulation techniques, such as ball movements, are only partially implemented at the moment.
You can use GDB to debug the native mode program.
Because the program uses curses to draw, it is easiest to run gdb from a separate terminal and attach to the running program. See the gdb manual for more information on how this works. Basically, you only need to know its process ID.
If you are in the FreeWPC source tree, you can run make attach to do this
automatically. If it finds a running program, it will attach, halt the program
(you'll see everything freeze), then give you a debugger prompt.
From here, you can set breakpoints, step through the code, etc. If the program
crashes, you can use the bt command to get a backtrace and see exactly
where it failed.
Since interrupt handlers are really just ordinary functions in native mode,
they are much easier to debug. You can use printf and other functions that
would normally be off-limits. The interrupt_dbprintf function can
be used; in 6809 mode, this is a macro which does nothing, while in native
mode it equates to a regular dbprintf.
Much like FreeWPC itself is portable, the builtin simulator is also designed to be portable. Its architecture is structured similarly in layers that isolate the platform and machine specific parts.
The interface between the simulator and the target system are the
readb and writeb functions. I/O initiated by the target
is intercepted by the simulator. The simulator itself catches these calls.
Platforms register callbacks for addresses that can be simulated.
The simulator allows you to control it via a command language. While the program is running, you can enter commands via the user-interface. You can also load scripts of commands from files.
Some files are checked automatically at startup and these commands are always run. These are kept in the conf subdirectory of the source tree. First, it will load conf/freewpc.conf. Then it will load a machine-specific file, conf/machine.conf.
In the curses-based simulator, press the colon key (like in vi) to open a command prompt. Other UIs may be different.
The basic commands are listed below. See other sections for more commands that are more specific.
Because commands can be placed in script files, shell-style comments and blank lines are accepted and ignored.
The simulator uses variables in certain places to allow you to change its
behavior at runtime. The set and print commands work on these
variables.
All variables are fundamentally of type int. There is no support for strings.
Simulator modules declare variables in the usual way, then export these to the
variable tracker using conf_add. This associates a public name for the variable
that the command language sees. When variables are modified in this way, the module's
int variables are then modified directly.
dbprintfYou can use the dbprintf() function to print debug
messages.
dbprintf uses the builtin sprintf function to format
the message, and then calls db_puts to print it.
How these messages are printed depends on which version of
pinmame you are using. At present, there is no way
to see these messages when you are running on real hardware.
If you are running Linux, and you obtain the patches to xpinmame, each character is written to a new hardware register, the debugger port. It acts like a serial port and can be used to communicate between the game program and a separate debug console. The program wpcdebug is the console. Run this at the same time as pinmame and you will see the debug messages printed out. The two programs use a local socket to send data back and forth.
The console also accepts keyboard input and passes it back to the running program. See common/db.c for more information.
Under an unpatched emulator, such as on Windows, debug messages are
sent to the WPC parallel port. pinmame
writes the printer output to a file in the memcard directory.
Under Windows, these files cannot be read while the program is
still running.
In either case, the code to drive the serial/parallel port is only enough to keep the emulator happy; none of it would work on real hardware.
In native mode only, you can use gdb as you would on any other program. Because native mode uses ncurses, however, it is best to run the debugger in a separate window from the program itself.
Start FreeWPC as usual, then from another window, run make attach. (See the Makefile to see what this does; you really don't need to be in the source tree to do it.) gdb starts, searches for the PID of the program, and attaches to it. The program will stop, and you can then control it from the gdb prompt.
There are some special gdb macros provided in gdbmacros. All of the usual breakpoints, stepping, and variable evaluation commands work as you would expect.
exec09 is the 6809 emulator that is provided with GCC6809. exec09 supports a subset of the WPC architecture and can be used as a replacement for PinMAME. It is mostly good for debugging straight CPU code that does not access hardware. It provides a command-line monitor with breakpoints, single-step, and symbolic debugging which is sometimes more useful than PinMAME or gdb.
exec09 can measure the length of time a function takes, including interrupt handlers, which is good for determining performance.
FreeWPC now supports breaking into the game program when the compile-time
flag CONFIG_BPT is turned on. This works both in emulators and on
real hardware. When enabled, this feature overrides the normal use of the
Escape coin door button. Pressing Escape now halts the system; press Escape
again to resume.
While halted, you can use the Up and Down buttons to view memory.
If a fatal error occurs when the debugger is compiled in, the machine will fall into the debugger instead of rebooting. This will show you what error occurred and which task was last running.
When halted, only normal task scheduling and periodic functions cease to run. Interrupt handlers/realtime functions cannot be halted, and thus, if they fail, the system will behave erratically. (Smoke alert.)
You can also dynamically enable and disable breakpoints at various places within the code, so that the debugger will start when the program hits that location, without having to press Escape.
The switch stress test is used to test switch handling over a long period of time. When enabled, this adjustment causes the game to pretend that switch closures are occurring randomly. No balls are actually put into play, but the machine is fooled into thinking so.
Stress test also exercises ball devices correctly, and simulates 'enter' events instead of individual device switches. Ball locks, trough serves, and multiballs can all be tested.
You can force end-of-ball by pressing the Start Button several seconds after the start of the ball (the delay allows you to still add players to test multiplayer games). If in multiball play, pressing Start simulates the drain of exactly one ball at a time.
Closures are simulated randomly at a rate of about 10 per second. This test has been used to uncover some hard to find bugs in the game logic after extended play.
Naturally, this feature is disabled by default.
This appendix provides an overview of the WPC hardware, in terms of its capabilities and how software is able to control everything in the machine.
A much more thorough guide to the WPC and other pinball architectures can be found at http://www.pinrepair.com/wpc/index.htm.
WPC pinball machines contain a number of circuit boards, some that are intelligent with microprocessors, and others that are passive. One of these, the CPU board, is the master and houses the main microprocessor, a Motorola 6809 running at 2MHz. All other boards are connected to this one via ribbon cables. The functionality is spread across multiple boards to make maintenance easier and to isolate faults. These data cables carry 5V, generally active low signals. Inputs read as high (inactive) when the cables are disconnected.
From 1990-1999, six different generations of machines were produced, which differed only slightly. In order of appearance:
The initial generation, which used 2 16-character alphanumeric displays, Each character is comprised of 14 line segments, a comma, and a period.
In addition to the CPU board, there is a power driver board which handles all of the high voltage I/O, and an alphanumeric controller board, which translates CPU commands into the proper signals that the displays need. There is also a separate sound board.
In the second generation, the alphanumerics are replaced by a dot matrix controller/display (DMD), which has 128x32 pixels. The display expects a serial bitstream and must be continously refreshed. The controller board stores up to 16 frames in its own RAM and handles the refresh. It connects to the main CPU board which writes the data. The display refreshes at 122Mhz.
Here, the flipper switches and drive transistors are moved from the regular switch matrix and power driver board onto a separate board, called the Fliptronic Board. There are Fliptronic I and Fliptronic II boards but they appear the same to software.
This frees up several of the switch matrix entries previously used for flipper buttons for other purposes. The flipper-related circuitry on the power driver board goes unused.
With the DCS generation, the sound board is radically improved, replacing the synthesizer chip and low-quality analog circuits with a DSP supporting high fidelity, stereo, multi-channel sound.
A security PIC chip is added between the ASIC and the switch matrix inputs. The CPU no longer reads the switch data directly; it sends commands to the PIC which then reads the data. The PIC requires some special cryptic codes to be sent otherwise it will not return valid switch data, making the game unplayable.
The functions of the circuit boards are combined into fewer boards, using FPGAs to replace many of the discrete TTL components. Functionally it is not much different, although the CPU addresses for many of the functions changed. The WPC ASIC remained the same, but its outputs are connected to the other circuit boards in slightly different ways.
The CPU board contains the main processor: a Motorola 68B09E, running at 2Mhz. It is an 8-bit/16-bit CPU with a 64KB address space. Bank switching is required to address more than 64KB. On reset, location 0xFFFE is read to determine the address of the first instruction.
8KB of RAM is located at physical address 0x0000. When power is turned off, three AA batteries on the CPU board maintain the state of the RAM.
The game ROM size varies from 128KB to 1MB, depending on the game. The hardware supports a maximum of 1MB; this upper limit was used in all of the later models to accommodate more and more graphics. The uppermost 32KB is permanently mapped into the 64KB address space at address 0x8000 and contains the core operating system functions. The remaining parts of the ROM must be bank switched in, 16KB at a time; only one 16KB bank at a time is visible at address 0x4000.
The WPC ASIC is the heart of the system and is essentially a giant address decoder. It combines a lot of the functionality that was performed by PIAs and TTL logic in earlier solid state games. All read/write requests from the CPU are first seen by the ASIC, which can then either respond to it directly if it is an internal function, or forward the request to another device. RAM and ROM requests cause those devices enables to be asserted. For I/O, it may be more complicated and cause I/O to other circuit boards.
The system blanking circuit protects the circuit boards during system initialization. The ASIC generates the BLANKING signal which is carried to all of the other boards. When asserted, BLANKING means to disable all output circuits.
Blanking is initially asserted by the ASIC at powerup, so that no lamps or solenoids can be turned on, regardless of the states of the outputs from the CPU board. Once the CPU has initialized, it writes to the ASIC to disable blanking; then the other register outputs take effect.
LED 3 is controlled by writing to bit 7 of the WPC_LEDS register; this register can also be read to get the current LED state. The OS toggles this bit periodically to indicate that the system is alive. It is also used during early fatal errors as a primitive way of providing an error message.
The ASIC has a builtin watchdog timer which will reset the CPU board if software does not restart it periodically. The expiration seems to be on the order of 1 or 2ms.
FreeWPC restarts the watchdog on every periodic interrupt, every 1ms. During initialization, when interrupts are disabled, it also restarts it occasionally.
The ASIC has a bit shifter which offloads the CPU from having to calculate shifts using multiple CPU instructions. The 6809 does not have a native shift instruction for more than 1 bit at a time.
The bit shifter is used to optimize bit-level operations (set, clear, test, and toggle). This is particularly useful on WPC because RAM is scarce; many variables are stored as bits to save space.
As an address decoder, the WPC ASIC is capable of restricting access to any part of the address space. The memory protection circuit lets you lock a portion of the system RAM as read-only, so that writes to those addresses are effectively no-ops.
The feature can be enabled/disabled, and the size of the protected region can also be specified. The region must reside in the uppermost part of RAM and must be a power of 2 in size.
FreeWPC uses memory protection to guard adjustments, audits, and some other vital data.
The time-of-day device maintains the current calendar time. It has two read/write registers, which stores hours and minutes. The clock continues to count even when power is turned off.
Software maintains the calendar time in nonvolatile memory.
The high resolution timer is not currently used by FreeWPC. It allows for sub-millisecond accuracy, and was only used in alphanumeric games to do display dimming.
The bank switch register allows you to select which portion of the ROM is mapped to the banked region of the physical address space, at 0x4000. Only the lower 6-bits of the register are used, allowing for up to 64 pages. These bits become the uppermost address lines sent to the ROM device.
All of the switch inputs terminate directly on the CPU board. This includes up to 64 playfield switches, arranged in an 8x8 matrix; 8 direct inputs used for service buttons and coin switches; and several jumpers/DIP switches used for configuring some factory defaults.
All of the other I/O is located on other boards, which are connected to the CPU board via ribbon cables. Generally on these cables, one side of the pins are connected to ground, while the others contain actual inputs/outputs. All logic values measure 0V for a '0', and +5V for a '1'. Inputs have pullup resistors so that reading the signal while the cable is not connected will return '1'. All generations of WPC use the same 17x2 pin connector to the power driver board, which controls lamps and general solenoids. Display, sound, and flipper interfaces differ somewhat between generations.
Special I/O boards exist to drive other miscellaneous devices like ticket dispensers or a printer. These were addons that are not typically installed in all machines.
The ASIC generates the reset, IRQ, and FIRQ signals which are sent to the CPU.
The 6809 instructions to generate soft interrupts — swi, swi2, and swi3, are not used. The handlers for those vectors all throw fatal errors.
The ASIC drives the CPU reset signal. This signal is active low; normally the reset line is high. The ASIC will pull reset low when the watchdog timer expires.
IRQ is generated 976 times per second, about once every 1.02ms. An oscillator on the CPU board generates the frequency. The periodic interrupt can be disabled/enabled by writing to the ASIC's WPC_ZEROCROSS_IRQ_CLEAR register. Separately, IRQ can be masked/unmasked by writing to the 6809's condition-code (CC) register.
FIRQ can be generated in two ways: from the dot matrix controller after a certain scanline is redrawn, or from the high-performance timer. When an FIRQ is received, the CPU has to determine which of these occurred to determine how to process it.
The DMD controller can interrupt via FIRQ when a particular scanline of the display has just been sent to the display. This can be used to tell the CPU when to display a new frame.
The high precision timer can interrupt when its value reaches zero.
Either of these can be enabled/disabled individually, in addition to masking the interrupt at the processor.
The CPU's NMI input is not used and is connected to Vcc.
The power driver board contains all of the high power circuitry. It has no intelligent parts and is controlled completely by the CPU board over a short ribbon cable. It houses the drive transistors which switch current to the high power devices, along with fuses and other power-related circuitry.
WPC supports up to 64 individually controllable lamps. The lamps are arranged in an 8x8 matrix. At any given instant, only 8 of the lamps can receive power. A column strobe is written to determine which set of 8 lamps is being addressed. A row output is written to specify the on/off states of that set of lamps. Software must repeatedly strobe each of the lamp columns in order to update all 64 lamps.
The filaments in incandescent lamps operate at around 6V. The lamp circuitry switches a much higher 18V, but strobing only enables that voltage for 1/8 of the time, and filaments do not instantly turn off when power is removed, so no flicker is perceived. LEDs do not have this property and this explains the flicker that is often seen when people replace normal bulbs with LEDs.
All versions of the power driver board support 28 controlled outputs for solenoids, motors, etc. These are divided into four groups: 8 high power drivers, 8 low power drivers, 8 flashlamp drivers, and 4 general purpose drivers. Each bank operates at a different voltage, somewhere between 20V and 50V.
The CPU board enables/disable a driver by writing a command to the power driver board. All values are latched on the driver board and thus retain their states until the CPU changes them. The latches are not readable, so software must maintain the last value written in RAM. A CPU board reset will assert a blanking signal to reset the latches; this helps in the event of a software crash.
On WPC-95, 4 additional low voltage outputs running at 5V are added to the general purpose group, which can be used for miscellaneous I/O like small DC motors.
The GI circuitry allows for 5 strings of up to 18 lamps each. These are separate from the controlled lamps described previously; individual lamps are not controllable, only entire strings of lamps. Each string can independently be turned on and off by a triac. The lamps all run off 6.3V AC.
Starting with WPC-95, two of the GI strings are always on and not under software control, saving the cost of the triacs, but eliminating the ability to dim them.
When enabled, the triacs illuminate the lamps until the AC voltage crosses zero. They must be constantly re-enabled to keep the lamps on.
The zero-cross circuit tells the CPU when the AC voltage is near zero.
Alternating current (AC) rapidly changes between a positive and negative voltage many times per second. In the US, the frequency is 60Hz, which means that the voltage is near zero about once every 8.33ms. Outside the US, line voltage is 50Hz and so zero crossing occurs every 10ms.
The zero cross data is used to dim the general illumination.
Both the WPC and DCS sound boards are connected via a ribbon cable to the CPU board, which sends it both read and write commands. The actual values vary greatly depending on the board type. These boards are intelligent and have processors running their own operating system dedicated to sound tasks.
1-byte or 2-byte commands are written to request particular sounds to be played. The sound board stores all of the sound data on additional EPROMs, and has its own CPU that decodes the data and writes it to various audio devices.
The sound board can also send data back to the CPU, notably to tell it when a sound has finished playing, allowing the CPU to synchronize its actions to the sounds and music.
On all but the earliest games, flipper control is located on a separate board, called the Fliptronic Board. It has 8 drivers for up to 4 flippers. Half of the drivers switch high power 50V, used when initially turning on a flipper; the other half switch a lower 'holding' power that is adequate for keeping the flipper in the raised position for a long period of time. The board also supports 8 switches that can be read by the CPU board: 4 for the flipper buttons and 4 end-of-stroke (EOS) switches. The button switches are wired to the cabinet buttons. The EOS switches are mounted on the flipper and used to determine when the flippers are in the 'up' position, indicating that the lower power voltage can be used.
On pre-Fliptronic games, the flippers are hardwired to the cabinet buttons, so flipping occurs without any CPU involvement. The CPU can control a relay on the power driver board to cut flipper power, though.
On WPC-95, the Fliptronic Board was eliminated and its components moved elsewhere: switch inputs moved to the CPU board, and flipper outputs moved to the driver board.
Some games use an auxiliary driver board which allows up to 8 additional flashlamps/low power devices to be controlled. These machines must have the following line in the machine config:
define MACHINE_SOL_EXTBOARD1
The auxiliary board plugs into the slot formerly used by the alphanumeric display board; thus, alphanumeric games cannot use this. This port was later removed on WPC-95.
The dot matrix display is 128 columns x 32 rows. The display itself is very dumb and expects a serial bitstream of pixels to be clocked in. The controller board has the display RAM and the serialization logic.
The controller fetches 1 byte (8 pixels) every 32 CPU cycles (16 microseconds). At this rate, it takes 256 microseconds per row and a little more than 8 milliseconds per complete frame. Thus, the refresh rate is about 122MHz.
The display RAM holds 8KB. A full DMD bit plane requires 128x32 pixels, that is, 4096 bits, or 512 bytes. Thus, there is enough RAM to hold a total of 16 planes. At any time, at most two planes can be addressed by the CPU (changed to six in WPC-95).
The SRAM is designed to allow both the 6809 and the serial decoder to access it simultaneously, at nonoverlapping phases of the system clocks.
Bits are encoded within a byte such that the leftmost pixel is in the least significant bit. This is backwards from how binary numbers are normally visualized.
The DMD controller exposes the following registers:
Says which of the 16 pages is currently accessible at CPU address 0x3800.
Says which of the 16 pages is currently accessible at CPU address 0x3A00.
The visible page register, WPC_DMD_ACTIVE_PAGE, holds the page number of the frame that should be clocked to the display. Writing to this register does not take effect immediately but instead at the beginning of the next vertical retrace.
The controller keeps track of which row is currently being refreshed, and makes this accessible to the CPU by reading the WPC_DMD_SCANLINE register. Values range from 0 to 31.
The CPU can also write to WPC_DMD_SCANLINE to request an FIRQ interrupt to be generated when the current scanline reaches a certain value. This is used to implement shading: the active page buffer is rapidly changed between different bit planes at different frequencies to simulate color. Because there is latency between the time that FIRQ is generated and the CPU can respond to it, this writable register can compensate for that delay and help to ensure that flipping occurs as fast as possible.
This section describes the WPC memory map in detail. In particular, it gives the function of each of the WPC ASIC registers, along with the names given to them by FreeWPC.
Machines define most of their hardware configuration in the form of an "md" file. Long ago, this was done by directly writing a .h file with the required information. The .h file approach was more cumbersome and somewhat error- prone.
The md file is "compiled" by the script genmachine, which writes out a .h file and also some .c files that get linked in to the final program. genmachine will do some consistency checking on the input to make sure it is sane.
It will enforce naming consistency between identifiers, declarations, and strings. For example, the start button gets a define for its switch number (SW_START_BUTTON), an identifier for callback events (sw_start_button), and a string in the menus ("START BUTTON"). gendefine can produce all of these automatically from a single "Start Button:" declaration in the .md file.
Though the syntax is fairly strict, there is a bit of magic in how different categories need to be written. The easiest way to write a config for a new game is to copy from another one. The Twilight Zone and World Cup Soccer configs are the most complete. There is more documentation in those files on how things should be set up.
The "md" file is just a text file. Blank lines and lines beginning with a '#' are ignored, like in a shell script.
Long lines can be broken up like in C, with a backslash at the end of each line. A comma also acts as a line continuation character, which is useful when defining long lists.
The file is divided into sections, which begin with a section header in square brackets. For example:
global-statements
[section1]
section1-statements
[section2]
section2-statements
Only section names that are known to genmachine are permitted. The list of
permitted sections is listed near the top of the genmachine script, and
includes: switches, lamp, drives, gi, lamplists, containers, etc.
Notice that there is a global section that is in effect at the top of a file, before you declare any sections.
Within a section, you declare items that fit that category. All declarations take this form:
key: property1 [, property2...]
A declaration begins with a key, followed by a colon, followed by a comma- separated list of properties. The key and/or properties can contain any characters, including spaces, but not commas or colons.
There are two types of declarations: fixed objects and dynamic objects. They are written similarly but not exactly the same.
In one style, used mostly for hardware layout, the key refers to the object's physical identification, such as the switch, lamp, or solenoid number. Here the key is generally numeric, although for solenoids the syntax is slightly different to specify the solenoid bank. The first property of these declarations should always be the human readable name of the object. Depending on the type of declaration, genmachine can validate that the key name is valid.
An example:
[switches]
14: Tilt, tilt, ingame, noplay
The key, 14, identifies which switch is being described. For switches and lamps, the key is given as a pair of column/row digits, as it would be listed in the game manual. The first property, Tilt, gives it a name. This is used to generate the defines and strings related to the switch.
Everything else identifies the properties of the switch. Different types of objects will have different properties. Here, we say three things: (1) it is an instance of a well-known switch class, called 'tilt'; this ties it directly to system code for processing tilts automatically. (2) The tilt switch should only be serviced during a game. (3) A closure does not mark valid playfield.
In the second type of object definition, there is no physical identifier, and so the name before the colon is the human readable name. These are generally used for software constructs. For these, genmachine automatically assigns a number based on the order of the declarations. There can be an unlimited number of objects of these types, unlike those tied to hardware where there is a physical limit.
For example:
[deffs]
Multiball Start: page(MACHINE_PAGE), PRI_GAME_QUICK6
This defines a display effect for multiball start. A #define is generated, DEFF_MULTIBALL_START, which is a numeric ID used to refer to the effect. The IDs for all deffs are assigned sequentially, and do not need to be specified as with the switches above. Everything following the colon is treat as a property just as above.
This example also shows a variation in the property syntax. Above, we saw properties 'ingame' and 'noplay', which are binary properties: just stating them causes them to be turned on. Binary properties can be listed in any order; genmachine knows what all of the allowable binary properties are and will handle them correctly. In the deff declaration above, there is a 'page' property, which is not binary – it has a value, MACHINE_PAGE. For these valued properties, the syntax is always ‘variable(value)’.
The 'include' directive can be used to include config syntax from another file, much like a C '#include'. This is used to bring in common definitions for the platform, that can be shared across games.
For example, no game defines a switch entry for the "ALWAYS CLOSED" switch, which is the same in every WPC game. This can be put into a file shared by all machines.
By convention, the machine-specific file includes the platform-specific file, which may itself include other files. The WPC platform provides different files for the different hardware generations, one per variation and one that is common for all.
All includes files are read and parsed before any of the output is generated. It is thus possible to override definitions that were seen in an earlier include. The default WPC md file provides names for all of the switches and lamps, so if you omit one from the machine file, you get a default definition. The tester ROM uses this facility.
The 'define' directive is used for miscellaneous settings. It gets translated to a C '#define' in the output file mach-config.h. For example,
define MACHINE_NUMBER 531
is converted to:
#define MACHINE_NUMBER 531
Note that the pound sign is not included in the mdfile, as it would be treated as a comment.
Certain things need to be defined in the global sections, using Key: Value syntax. The human readable name of the machine, the system type, and a few other things can be given here. These do not appear directly in the mach-config.h, but are used by genmachine to guide the compilation. Again, see any existing config file for an example.
Here is a list of the sections that can appear.
switchesThe possible attributes for switches are:
optoedgenoplayingameintestbuttonnoscoreAdditionally, these attributes are used to tag special switches. There should only be one switch of each type.
outholeshootertiltslam-tiltbuyin-buttonlaunch-buttonstart-buttontrough-stacklampsThese attributes are used to tag special lamps:
startbuyinextra-ballshoot-againdrivesflash attribute to say
which ones are flashers. Use motor to say which ones are motors.
These attributes are used to tag special drives:
ballserveknockerlaunchgilamplists[lamps] section;
a range of lamps in the form lamp_start..lamp_end; or another
lamplist name, allowing for nested declarations.
containersThe trough container should be marked with the attribute trough.
templatestestsdeffsleffsadjustmentsauditssystem_sounds
system_music
highscoresflagsglobalflagsscoresfontstimerstemplatesgenmachine is a Perl script. It parses all of the md commands and builds a giant hash with all of the data. genmachine is invoked multiple times with different options, requesting that different output files be generated. All of the output files are C or H files put into the 'build' subdirectory, which are then compiled normally.
For the Perl programmer, each object declaration is itself a Perl anonymous hash, where each property of the object is one of the hash entries. Using the variable(value) syntax, it is possible to put anything into the object definition. However, only certain keys are recognized by the output functions. Adding new properties generally doesn't require a parser change, but only a change to the output routines. Binary properties and well-known object classes do need to be stated – there are constant tables at the top of the script that declare these.
Here is a list of all of the standard events that the system generates.
add_creditsadd_partial_creditsadd_playeramode_pageamode_startamode_stopany_device_enterany_kick_attemptany_pf_switchball_count_changeboolean ball_drainThe standard ballsaver module takes care of this, but you can hook this if
you write an "unlimited ball" mode, like Lost in the Zone.
ball_searchbonusdev_foo_enterdev_foo_kick_attemptdev_foo_kick_failuredev_foo_kick_requestdev_foo_kick_successdev_foo_surprise_releasedevice_updatediagnostic_checkdisplay_updateempty_balls_testboolean empty_trough_kickend_ballend_gameend_playerextra_ball_awardfactory_resetflipper_abortflipper_code_enteredidleidle_every_100msidle_every_secondidle_every_ten_secondsinitinit_completeinit_ok (boolean)lamp_updatematch_awardedminute_elapsedmissing_ball_foundmusic_refreshreplayscore_deff_startscore_overlayserve_ballshoot_againsingle_ball_playslam_tiltspecial_awardstart_ballstart_gamestart_playerstart_without_creditsstatus_reportstop_gamesw_footest_starttilttilt_warningtimed_drain_penaltytimed_game_tickvalid_playfieldThe checksum tool works on FreeWPC ROMs as well as the original Williams factory ROMs. It can verify and update the checksum field located just above the interrupt vector table.
The checksum is a 16-bit value that resides at logical address 0xFFEE (18 bytes from the top of ROM). This value should equal the sum of all the 8-bit byte values in the ROM, modulo 65536. The checksum word is itself included when calculating and verifying it.
csum also uses the word at address 0xFFEC as a fixup word, which can be set to any value to help make the checksum match.
The gcc6809 linker produces S-record files by default. This tool converts S-records to raw binary format.
Each bank of the ROM is linked individually; each produces an S-record file. These files contain nearly everything to generate a binary file, except for the size and the value to place at holes. We use 0xFF for all holes as this is more friendly to EPROMs.
The scheduler reads in a list of schedule files, which by convention end with the extension .sched. Each entry gives a function and a frequency at which it should be called. It generates a C source file that contains a top-level function which then calls all of those functions at the right rate. It does this by keeping a counter of the number of times that the top function was called.
The scheduler supports loop unrolling, where the top function is actually decomposed into a small number of functions. This reduces the number of if-statements needed overall; it does require an extra indirection via function pointer. This can cause code duplication for the sake of speed. The unroller tries to balance all of the functions to be scheduled using performance data in the schedule file.
This is a short Perl script which can transform a TrueType font file (.ttf) into FreeWPC source code. It requires the convert program from the ImageMagick tool suite.
On older machines without a dot matrix, driving segmented score displays was a relatively low horsepower task. Up until the late 1980's, 7-segment displays were used that could only display numbers (and a few letters). Including a comma, there are only 8 bits per segment. Assuming 4 players per game and 8-digit scoring, that's still only 256 bits – or 32 bytes of data – that has to be managed by the CPU.
Later, 16-bit segments were used that could display full text, which doubles the data size, but still the quantity of data to be manipulated was low.
The dot matrix display changed that. The DMD is 128 pixels wide by 32 pixels high, for a total of 4096 pixels, or 512 bytes, per frame. Let's look at some of the challenges that are created.
Achieving a realistic looking animation means redrawing the display frequently enough so that movements appear smooth. See this article about flicker fusion threshold for more information.
A 20Hz frame rate is decent looking. That requires updating the display 20 times per second, or once every 50ms. For better quality, 25Hz or 30Hz is desirable. However, the 2MHz processor, saturated with other things besides DMD updating, is not well-suited to a high frame rate.
We'll assume a 20Hz frame rate throughout this article, and see how difficult even that is to achieve. We'll find that in some cases, we have to settle for even less than that.
Another way of looking at it is that every 50ms, or every 97600 CPU cycles, we need to draw and commit another display frame. Later, we'll talk about the CPU instruction set and how efficiently it can do some common operations.
The display itself has no notion of color. Each pixel is either off, or glows orange. WPC games simulated different shades of orange by using a page flipping technique. By rapidly switching between two pages, up to 4 different colors (3 oranges plus black) are perceived.
The key is that the two pages are not shown equally. One of them is displayed 1/3 of the time; the other 2/3 of the time. If a pixel is off in both pages, it appears black. If a pixel is on in both pages, it appears to be on all the time, at 100% intensity. If a pixel is on in one page but not the other, then it appears either 1/3 or 2/3 of the time, producing either a dark brown or medium orange color.
The controller itself does not implement the page flipping; the software does. The controller can interrupt the CPU (on the FIRQ line) whenever the display has just been refreshed. Software can reprogram the controller to display a different page of data at this time. Doing it at any other time will produce tearing, where parts of two different pages are temporarily displayed at once.
The display refreshes at 122MHz (ref); therefore, the FIRQ occurs about once every 8ms (or 16000 CPU cycles). The FIRQ handler needs to be fast since it is so frequent. All it needs to do though, is to set the active page register in the controller to a new value. (FreeWPC does this in less than 40 cycles.)
The same technique, with more pages, could be used to simulate more colors. For example, 3 pages could produce 8 colors, and 4 pages could produce 16 colors. However, note that as more pages are flipped, the effective refresh rate drops. This means that the page flipping begins to be perceived as movement and not as color change. Showing more colors requires other techniques that are not discussed further.
2 pages = effective frame every 3 redraws = 40Hz frame rate 3 pages = effective frame every 7 redraws = 17Hz frame rate 4 pages = effective frame every 15 redraws = 8Hz frame rate
The 6809 does not provide much support for bulk movement of data, which is what is mainly needed to update the display. Assume for now that a single 512-byte, monochrome frame of data is in the game EPROM, which needs to be copied into display memory, i.e.
memcpy (display_memory, image_src, 512);
How long does this simplest of operations take? It all depends on the implementation of the copy. Let's look at some approaches.
A simple way is to copy byte-by-byte. Assume that the source and destination addresses are already in registers, then we can do something like this:
loop:
ldb ,x+
stb ,u+
dec count
bne loop
We use the X and U registers, because instructions using Y are one byte longer, and also take one cycle longer.
This actually doesn't work, because count is a byte variable, and we need to copy 512 bytes. Also, it is inefficient because many cycles are spent in the 'dec' and 'bne'. Loop unrolling can help us eliminate overhead:
lda #64
sta count
loop:
ldb ,x+
stb ,u+
ldb ,x+
stb ,u+
ldb ,x+
stb ,u+
ldb ,x+
stb ,u+
dec count
bne loop
As each iteration now copies 4 bytes at a time, we can do this in 64 loop iterations.
Obviously, we could unroll some more and it would run even faster, but with diminishing returns and at the expense of a larger program. Code space is not the primary concern here, but it does play a role.
The above example can be improved further by realizing that the 6809 can operate on 16-bit words. Loading/storing a word at a time helps a lot. The number of iterations can be cut in half, too, which reduces loop overhead more.
lda #32
sta count
loop:
ldd ,x++
std ,u++
ldd ,x++
std ,u++
ldd ,x++
std ,u++
ldd ,x++
std ,u++
dec count
bne loop
The 'ldd' and 'std' instructions, in the indexed modes used above, take 7 cycles each. dec takes 5 cycles – or 4 if we ensure that it is in the direct page. (footnote here) bne only takes 2 cycles. So one iteration of this loop, which copies 8 bytes, takes 63 cycles. Multiply by 32 and we get 2016 cycles, or a little more than 1ms, just to do a straightforward page copy.
Now consider the following:
It turns out that our copying loop is NOT the most efficient that it can be. Remember, the goal is to do bulk copying of data. There actually are instructions that can copy more than 16-bits of data at a time: the stack push/pull instructions!
The PSHS (push) instruction can copy the contents of the A, B, X, Y, U, S, PC, DP, and CC registers onto the stack. Likewise PULS does the opposite, loading from memory into registers. These instructions don't quite work though, because we need a working stack. However, there is another variant: PSHU and PULU. These were intended to be used for a "user stack", but for our purposes, U is just another pointer register. If we point U at our display page, we can do bulk copies using PSHU, and get more than 16-bits done at a time. [Note that this only works for either the source or the destination, but not both.]
These instructions are not without some cost, though. What they improve is the overhead of instruction fetching. Each of them takes 5 cycles plus 1 cycle for each byte transferred. Recall that ldd/std took 7 cycles. So if we use the stack instructions for only 16-bits at a time, the cost is the same. But any additional bytes copied come for only 1 cycle per byte.
We want to push as many values in registers as possible, but we can't use them all. U is already being used as our pointer, so we can't use it for the display data, too. PC is obviously not a good candidate. DP and CC affect other things, so we don't want to trash them. That leaves A, B, X, Y, and S.
FreeWPC chooses not to use S as a data register, although it could if the copy code was protected against interrupts and no stack variables were needed during the copy. In some applications, this might be possible. Because of the large amount of time that a copy takes, I felt that keeping interrupts disabled for a long time was a bad idea. So I only use A, B, and X, and Y: 6 bytes copied at a time. [ Note: I'm not using Y. ]
And that, as far as I can tell, is THE fastest way to copy bytes on the 6809.
For full 128x32 frames, a bytewise or wordwise copy doesn't need to concern itself with the individual bits in the image. However, when copying smaller bitmaps to an arbitrary DMD location – for example, when drawing font glyphs – bit aligment becomes a problem.
First, some background. A single byte of data in the controller's memory addresses 8 consecutive pixels within a single row. The least-significant bit of the data corresponds to the leftmost pixel. That is, the value 0x01 draws one pixel aligned to the left; 0x80 is aligned to the right. If you're used to picturing a byte of data in its binary form, this is completely backwards from that, which can cause some confusion when working with these algorithms.
Now say we have an 8x1 bitmap (8 pixels wide, 1 pixel high) stored in one of these bytes, and we want to place it anywhere on the display. If the leftmost pixel location is byte-aligned, this is easy; we just write the byte to that location.
If the leftmost pixel location is not byte-aligned, you can see that this will take at least two instructions, because two different bytes of the display data are modified. Suppose we want to put this bitmap at a row where x=4 (four pixels in from the left edge of the display). How would we do it?
First, we're going to have to modify two different bytes of data.
Second, note that to be correct, we cannot just overwrite these areas, because that would trash some of the bits that we are not concerned with. The pixels from x=0 to x=3 and from x=12 to x=15 should not be altered. This requires reading those locations, modifying only the appropriate pixels, then writing back the results.
So already this is not trivial. This is an important problem to solve because this is how all fonts get drawn.
The 6809 does not support shifting more than 1 bit in a single instruction.
All this said, there are a number of efficient solutions to the problem, but they all take a lot of CPU cycles. FreeWPC's approach is to examine the size of the source bitmap and the target bit alignment, to determine the most efficient method to use. Three different situations are handled:
1. the source width plus the bit alignment are less than 8 bits. This means no more than 1 byte per row need to be emitted. This is very efficient for small fonts in some cases. [ This is not right. ]
2. the source width is 8 bits or less, and the width plus alignment is less than 16 bits. This is almost as good; we can read 1 byte per row and write 2 bytes per row.
3. Everything else. The generic method always works, but is inefficient for all but the largest of bitmaps.
FreeWPC has not experimented much with various compression algorithms for full-screen images. Compression has not been implemented at all for font data and other small bitmaps, simply because the space savings would not be worth all of the extra computation.
The goal is not perfect compression, but rather to be good enough. The best compressor takes time – and thereby requires time to decompress. We need a fast decompression algorithm that the 6809 can do well. That limits the scope to the simplest of algorithms. Really good compressors work at the bit-level, but because of the 6809's inability to do bit-level manipulation fast, FreeWPC has only considered techniques that work a byte or a word at a time.
There are a number of basic approaches to compression. The first is to use a form of run length encoding (RLE), where a long string of consecutive byte or word values is compressed into a value+count pair. Not all images will have long strings like this, but many do, especially runs of 0x00 and 0xFF.
The second is to use a form of delta encoding, where an image is encoded as the set of differences from a previous image. This is useful when doing animations, in which consecutive images don't differ too much.
More complicated techniques than these are likely to be a burden for the 6809.
FreeWPC was started in 2005 and was written entirely in assembly language, before the availability of the gcc6809 C compiler. An initial attempt to ease programming involved the use of some complicated macros, written in the m4 programming language. Work was halted during the development of gcc6809, then the system was gradually rewritten in C. The early source code repository actually contained the compiler changes as well.
The first time that a FreeWPC ROM was placed into a game was in May, 2008. It did not run OK due to some problems with the watchdog circuit and with the PIC initialization. Those problems were resolved within only a few hours.
How to Program a New Game
While the rest of this manual serves as a reference to the FreeWPC system, this chapter takes a different approach and explains systematically how to go about writing up the rules for a new game from the beginning. Details are intentionally skipped here, with pointers back to the relevant sections of the manual if you want to know more.
Like any good software program, a game should be broken down into logically separate entities as much as possible. An object-oriented approach can still be taken even when coding in C. I prefer to think of a game as consisting of a set of mostly independent modules, with each module represented by a separate C file.
Each module defines a set of functions that are private to it: the implementation. In C, these are usually coded as "static" functions to prevent accidental use from other files. Other functions are public by default. In FreeWPC, make sure that you have a prototype for all such functions that includes one of the ROM bank modifiers (__common__ or __machine__, for example), otherwise they may not be called correctly. Always check for warnings in the error file when you build a ROM; this type of problem is always pointed out there.
For the most part, the game program is event-driven, meaning that functions are called only when some external event occurs. The OS throws events for just about every event that you will care about, but your code can also create new events too. There are basically three types of events that you will care about: switch events (some playfield switch just activated), timer events (some amount of time has expired), and milestone events (notable points in time like the start of a game).
Use the callset mechanism to write callback handlers to handle all of the events you need to monitor. Nearly every module will require this. These are the main entry points to the module.
Modules can serve different purposes. I like to classify modules into three classes: drivers, shot detectors, and rules. Each of these serves a different purpose, and keeping them in separate modules is good programming design.
Drivers deal with the physical nature of the specific machine. They should do nothing with game rules. Drivers manage the hardware devices and define an API that lets higher layer game code interact with it. The hardware aspect can be the most difficult part of programming a pinball game, as it needs to handle many types of fault conditions.
Shot detection can be viewed as an extension of the switch events. Sometimes, a single switch activation can be considered a shot, but not always. Consider on Twilight Zone the detection of left versus right loops; the same switches are involved, but they are triggered in a different order. In Attack From Mars, the right hole awards a "front shot" or "back shot" differently and used other recent shots to determine which to award. In the shot modules, you write logic that defines what makes a valid shot. You monitor the switches and timers that contribute to the shot, and then you create new events for the shots that you define. By doing this in one module, the rest of the code can just listen for a "right hole front shot" event.
Shot detection can be overdone if not careful. It is best to keep it as simple as possible. Compensation for bad switches can be done here, too.
The game rules are at the top of the module stack. Every timed mode, multiball, or other game feature should generally have its own file. They use the shot detection logic to listen for valid shot events and have their own logic for defining how to award points and what effects to trigger.
Note that drivers and shot detection should be rules-agnostic; thus, once they are coded correctly, it should be possible to rewrite the game rules without changing them.
When you start coding a brand new machine, you must write a machine description file. This defines the physical switches, lamps, and solenoids for that game. Doing this is enough to make all of the low-level APIs work. These binary APIs allow you to read switches one by one, turn on/off lamps, and pulse solenoids and flashers.
However, you are not allowed to turn on a solenoid permanently. A generic API to do so would be dangerous. For flashers and ball kicking devices, the pulsing APIs, which ensure that the device is turned off eventually, are enough. But for some devices, you need more control. To do that, you need to write a device driver.
Here are some examples of drivers:
- Diverters which must be held on for a long period of time. - Solenoids that are tied to switches, like slingshots and jets, which need fast response time. Also motor banks which have home switches. - Devices where there are multiple outputs working together. For example, the clock on TZ uses two motor control lines, one to spin forward and another to spin backward.
Ball kicking devices are automatically handled by the ball device module. You write definitions in the [containers] section of the md file to define these. Other device drivers should be written using a template driver.
Most modes are OK to overlap and run in parallel; you must explicitly define how to handle conflicts if this is not wanted. Rules will listen for shot events, update state, add to the score, and start display, lamp, and sound effects.
Rules are also the only modules that will need to keep state per-player. Use the __local__ modifier to create a per-player variable. Define flags (not globalflags) in the machine description to create 1-bit flags. For trivial rules you can sometimes keep state directly in the lamp matrix.
Types of rules: static rules, timed modes, multiball modes
Every rule should define the following types of APIs: - start_game (physical devices only) - start_player - start_ball - available? - qualified? (running? in grace period?)
- plus other APIs as pertinent to that type of rule shot->rule mapping
For timed/multiball modes there are predefined APIs...
Test modules. These are extensions to the test mode menus and are generally tied to a particular device driver, but not all drivers will need a test option.