Pinball controller based on TivaC eval. board compatible with the `Mission Pinball` API
Switch branches/tags
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Type Name Latest commit message Commit time
Failed to load latest commit information.
.gitignore Initial commit setting up CCS project Mar 28, 2015
LICENSE Create LICENSE Dec 9, 2018
Makefile Add HI cmd. OUT and LED tested ok Dec 2, 2018 Clean .ipynb, optimize isr priorities Dec 9, 2018
bit_rules.c Add HI cmd. OUT and LED tested ok Dec 2, 2018
fantastic.ld split i2c stuff, Linter Nov 23, 2018
i2c_inout.c Add HI cmd. OUT and LED tested ok Dec 2, 2018
i2c_inout.h Add HI cmd. OUT and LED tested ok Dec 2, 2018
main.c Clean .ipynb, optimize isr priorities Dec 9, 2018
main.h New I2C driver. Very promising. Ready for more debugging Nov 26, 2018
mySpi.c Add HI cmd. OUT and LED tested ok Dec 2, 2018
mySpi.h split i2c stuff, Linter Nov 23, 2018
myTasks.h Add HI cmd. OUT and LED tested ok Dec 2, 2018
startup_gcc.c Add HI cmd. OUT and LED tested ok Dec 2, 2018
switch_matrix.c New I2C driver. Very promising. Ready for more debugging Nov 26, 2018
switch_matrix.h Add HI cmd. OUT and LED tested ok Dec 2, 2018


Controller for Pinball machines based on an TM4C123G LaunchPad™ Evaluation Kit, compatible with the Mission Pinball API.

Design ideas

Hardware features

  • 8x8 Switch matrix inputs
  • 12 onboard drivers for solenoids, 4 of them can do hardware PWM (> 100 kHz)
  • 4 x I2C channels for extension boards (solenoid drivers, switch inputs, LED drivers, servo drivers, ...)
  • 3 x SPI channels for running WS2811 / WS2812 LED strings with up to 1024 LEDs per channel
  • In- / Outputs can be easily and cheaply added with PCF8574 I2C GPIO extenders (check eBay for cheap I/O modules)
  • Super fast USB virtual serial connection to host PC
  • KiCad PCB files available. Based on cheap and easily available components

Software features

  • Software can handle up to 320 channels which can be used as In- or Outputs
  • All In- / Output channels are identified by a 16 bit unique ID. No configuration necessary.
  • All Outputs support 3 bit PWM with > 125 Hz (using binary code modulation over I2C)
  • All inputs are read 1000 times per second and optionally debounced. A switch toggles after keeping its state for 4 ms.
  • The timing for the WS2811 LEDs is kept within spec by using the hardware SPI module and DMA transfers
  • Firmware is based on FreeRTOS
  • Compatible with the Mission Pinball API with this Hardware platform driver

Digital inputs / outputs

There are 4 I2C channels which can handle up to 8 x PCF8574 each. The addresses of the chips needs to be configured from 0x20 - 0x27.

To use an PCF8574 as an input, a 0xFF needs to be written to it (to prevent the open drain outputs pulling the pin low). This can be achieved with the HI command. Then the pin state can be acquired with a 1 byte read transaction over I2C.


The task_pcf_io() is running at 1000 Hz, reading all switches (matrix and I2C) into an array

  • Switch Matrix (SM) is read in foreground while I2C transactions happens in interrupt context
  • When both reads are finished, the Debouncing routine is called
  • More info about the I2C engine in this blog post
  • If a changed input bit was detected, it increments a 2 bit vertical counter. The ARM CPU can process 32 input bits in a single instruction, which speeds things up!
  • If the change persists for 4 read cycles, the vertical counter of that bit will overflow and trigger a definite change.
  • The current debounced state and any bits which toggled during the current iteration are stored in global variables.
  • The serial reporter function looks for changed bits, encodes them and reports them as Switch Events on the serial port
  • The quick-fire rule function checks a list of rules, which can be triggered by changed bits, which can lead to immediate actions, like coils firing

hwIndex identifies an input / output pin

It is a 16 bit number which uniquely identifies each input / output pin. Note that depending on the number of IO expanders usend in a setup, not all addresses will be valid.

     hwIndex for inputs
     0x000 - 0x03F --> Switch matrix inputs on mainboard
   [ 0x040 - 0x047 --> I2C Solenoid driver on mainboard  NOT AN INPUT! ]
     0x048 - 0x04F --> I2Cch. 0, I2Cadr. 0x21, bit 0-7  (First  external PCL GPIO extender on channel 0)
     0x050 - 0x057 --> I2Cch. 0, I2Cadr. 0x22, bit 0-7  (Second external PCL GPIO extender on channel 0)
     0x138 - 0x13F --> I2Cch. 3, I2Cadr. 0x27, bit 0-7  (7th    external PCL GPIO extender on channel 3)

Calculating the hwIndex for the Switch matrix

    hwIndex = SMrow * 8 + SMcol

where SMrow is the row wire number (from 0 - 7) and SMcol is the column wire number (from 0 - 7).

Calculating the hwIndex for I2C inputs

    hwIndex = 0x40 + I2Cchannel * 0x40 + (I2Cadr - 0x20) * 8 + PinIndex

where I2Cchannel is the output channel on the mainboard (from 0 - 3), I2Cadr is the configured I2C address set by dip switches on the port extender (0x20 - 0x27) and PinIndex is the output pin (0 - 7).

What about I2C outputs

Output extension boards (solenoid driver / digital outputs / etc.) also use the PCF8574 I2C chip and hence the same addresses. The chip automatically switches its pins to output mode once an I2C write operation has been carried out. So outputs do not need to be specially configured, just fire the OUT command with the known output address.

The PC8574 has open drain output drivers and can only sink current to ground. If the output driver is disabled (by writing the bit to 1), the pin can be used as an input. If the output driver is enabled (by writing the bit to 0) the pin cannot be used as an input as it is constantly pulled low.

!!! To avoid this, the user must take care to not send OUT commands on an hwIndex which is used as an input !!!

The mainboard features a built-in PCF8574 to drive 8 solenoids. use hwIndex 64 - 71 to address these channels.

What about the HW. PWM outputs

The mainboard features 4 high resolution PWM solenoid drivers running at 50 kHz. For these, the pwm hold power and pwm pulse power can be specified from 0 - 1500. The hwIndexes 0x3C - 0x3F are mapped in any OUT command to the HW. PWM channels. For any IN command, these addresses are mapped to the highest 4 Switch Matrix inputs.


Instructions for Ubuntu 18

Install gcc for arm

$ sudo apt-get install gcc-arm-none-eabi openocd build-essential make
# to get
$ pip install pyserial

Download TivaWare Full Release

From here: SW-TM4C- It's free but registration at is necessary.

$ echo "c74ef1da07246d4ad20a92521ba44a7d567f6bc62556cd1b76e7fd6a8a75bf8c  SW-TM4C-" | sha256sum -c -

$ unzip SW-TM4C-

Build and flash the project

Now edit Makefile and change the path ROOT = to the tivaware folder from above.

Plug in the debug connector and:

$ make
$ make flash
$ /dev/ttyACM0 115200

Serial command API

The Tiva board has two physical USB connectors. The DEBUG port is used to load and debug the firmware. It also provides a virtual serial port, which can be opened in a terminal to enter commands manually and to see status and debug messages from the Fan-Tas-Tic firmware.

The DEVICE port provides a virtual serial port which is meant to communicate with the (Python) host application. This port listens to the same commands but does not echo any input characters or status messages, which makes it easier to talk to programatically.

Note that the DEVICE port reports errors in the form of an ER:xxxx\n error code. They can be looked up in this (slightly out of date) table.

 Available commands   <required>  [optional]
?     : Display list of commands
*IDN? : Display ID and version info
IL    : I2C: List status of GPIO expanders
IR    : I2C: Reset I2C system
OL    : I2C: List output writers
HI    : <hwIndex> set all ports of PCF high (input mode)
SWE   : <OnOff> En./Dis. reporting of switch events
DEB   : <hwIndex> <OnOff> En./Dis. 12 ms debouncing
SW?   : Return the state of ALL switches (40 bytes)
SOE   : <OnOff> En./Dis. 24 V solenoid power (careful!)
OUT   : <hwIndex> <PWMlow> [tPulse] [PWMhigh]
RUL   : <ID> <IDin> <IDout> <trHoldOff>
        <tPulse> <pwmOn> <pwmOff> <bPosEdge>
RULE  : En./Dis a prev. def. rule: RULE <ID> <OnOff>
LEC   : <channel> <spiSpeed [Hz]> [frameFmt]
LED   : <channel> <nBytes>\n<binary blob of nBytes>
I2C   : <channel> <I2Caddr> [hexSendData] <nBytesRx>

IL I2C input list

Show status of the 4 I2C channels in 4 columns. The PCF8574 GPIO expanders have an I2C address between 0x20 and 0x27. Each address corresponds to one row. R indicates the chip is read (inputs), W indicates it is written (outputs). The [I2C_ADDRESS] follows. Then the last read value. Then the (I2C_ERROR_COUNT). The latter is incremented on every transaction without ACK.





W[20]:    (    0)   [20]:              [20]:              [20]:
R[21]: ff (    0)   [21]:              [21]:              [21]:
 [22]:              [22]:              [22]:              [22]:
 [23]:              [23]:              [23]:              [23]:
 [24]:              [24]:              [24]:              [24]:
 [25]:              [25]:              [25]:              [25]:
 [26]:              [26]:              [26]:              [26]:
 [27]:             R[27]:  0 (    0)   [27]:              [27]:

OL I2C output list

Show a list of active output channels. These channels are continously updated to achieve the bcm intensity modulation. Syntax is:

  • array index N:
  • Intensity values for each of the 8 bits of this PCF8574





N: [CH,I2C] PWM0 PWM1 ...
0: [0,20]    3    0    0    0    0    0    0    0

SWE enables the reporting of Switch events

When a switch input flips its state, its hwIndex and new state is immediately reported on the USB serial port. This feature is disabled by default and needs to be enabled with the SWE 1\n command.



    SWE 1\n

Afterwards, the input with hwIndex 0x0F8 changed to 1, 0x0FC changed to 0 and 0x0FE changed to 1


    SE:0f8=1 0fa=1 0fc=0 0fe=1\n

DEB disables the debouncing timer for certain inputs

By default, each input is buffered by a deboucning timer, which recognizes a change in input level only after it has been kept stable for 4 ms. This can be disabled to minimize input latency (for example for jet bumpers).



     DEB 0x0012 0\n

Disables debouncing for the input with hwIndex 0x0012.

SW? returns the state of all Switch inputs

Returns 40 bytes as 8 digit hex numbers. This encodes all 320 bits which can be addressed by a hwIndex.






SOE enable 24 V solenoid power

The Fan-Tas-Tic mainboard foresees a relay to disable the 24 V supply voltage to the solenoids. This is for safety reasons (in case of firmware hang-ups) -- but also to make sure the solenoid drivers do not have power until they are initialized. So the solenoids have to be manually activated by the user.



     SOE 1\n

Enables the 24 V supply to the solenoids (sets PE0 high).

OUT set a solenoid driver output

  • hwIndex
  • pwm hold power (0-7)
  • pulse time in ms (0 - 32767), optional
  • pwm pulse power (0-7), optional


Set output pin with hwIndex 0x100 to a pwm power level of 2 and keep it there.


    OUT 0x100 2\n

Pulse output with hwIndex 0x110 for 300 ms with a pwm power level of 7 and then keep it at a power level of 2.


    OUT 0x110 2 300 7\n

RUL setup and enable a quick-fire rule

  • quickRuleId (0-64)
  • input switch ID number
  • driver output ID number
  • post trigger hold-off time [ms]
  • pulse duration [ms]
  • pulse pwm (0-7)
  • hold pwm (0-7)
  • Enable trigger on pos edge?

Logic for each rule

    If a rule is enabled:
        If it is currently triggered:
            If holdOff time expired:
                set Rule to untriggered state
                decrement holdOff time
            Check if the input matches the trigger condition:
                Set Rule to triggered state
                switch output ON



    RUL 0 0x23 0x100 40 20 7 2 1\n

Setup ruleId 0. Input hwIndex is 0x23, output hwIndex is 0x100. After triggering, at least 40 ms need to ellapse before the trigger becomes armed again. Once triggered it pulses the output for 20 ms with pwmPower 7, then it holds the output with pwmPower 2. The trigger happens on a positive edge.

LED dump data to WS2811 RGB LED strings

There are 3 channels which can address up to 1024 LEDs each. First argument is the channel address (0-2). Second argument is the number of bytes which will be sent (nBytes) and must be an integer multiple of 3. Each LED needs to be set with 1 byte per color. For the WS2811 LED chip, the order of the bytes is RGB. For performance reasons, data must be sent as raw binary values (not ascii encoded!).



    LED 1 6\n

Set the first two LEDs on channel 1. The first LED will glow white at full power, the second red at half power.

Troubleshooting glitches

If you get glitches and artifacts on your LEDs, you can try the following:

  • Play with the SPI speed setting (LEC command). Some LED strings, especially the cheaper ones can significantly deviate from specifications
  • Try shorter cables to the first LED
  • If nothing else helps, get out the scope

LEC configure the WS2811 data rate

Set the output data-rate of the SPI module in bits / s. Note that 4 bits are needed to transmit 1 WS2811 baud. The WS2811 chip supports low speed (400 kBaud) and high speed (800 kBaud) mode. This setting is applied by the voltage level on a physical pin on the chip. The WS2812 LED only supports 800 kBaud mode.

 kBaud     spiSpeed [Hz]
   400   =       1600000
   800   =       3200000

This command allows the timing to be fine-tuned, to remove glitches.

The second argument frameFmt is optional and allows to experiment with the SPI frame format. Refer to the TM4C1294 datasheet for details.

 frameFmt    Description
     0x00    Moto fmt, polarity 0, phase 0
     0x02    Moto fmt, polarity 0, phase 1
     0x01    Moto fmt, polarity 1, phase 0
     0x03    Moto fmt, polarity 1, phase 1
     0x10    TI frame format
     0x20    National MicroWire frame format



    LEC 0 1700000\n

Set the SPI speed of the first LED channel to 1.7 Mbit/s. As 4 SPI bits encode 1 WS2811 clock period, the effective speed is 425 kBaud.

I2C do a custom I2C transaction

work in progress

This command does a send / receive transaction on one of the I2C busses. Use this to communicate with custom extension boards from python.



     I2C 3 0x20 ABCDEF 2\n



Do an I2C transaction on channel 3. The right shifted device address (without R/W bit) is 0x20. Send the 3 bytes of data 0xAB, 0xCD, 0xEF. Then read 2 bytes of data from the device, which are 0xE3 and 0xB4.