Scratch Build Namco Point Blank Arcade Cabinet Using Arcade Hardware and MiSTer FPGA

fiatbravodriver

Beginner
Joined
Jun 6, 2026
Messages
4
Reaction score
4
Location
United Kingdom
Scratch Build Namco Point Blank Arcade Cabinet Using Arcade Hardware and MiSTer FPGA

The Brief : Build an Arcade Cabinet that Plays Point Blank 1 , 2, 3 and Ghoul Panic using a CRT and Original Recoil Namco Guns


I have started on the Cabinet so If using a MiSTER wont work i'll just Save Up for a Point Blank 2 PCB :)
Here is a info dump of everything so far.

A Mister connects to an Arcade Monitor, an arduino is needed to fire the solenoids when trigger is pressed, a Playstation SNAC has to be used to connect the light guns to the mister, everything else is arcade hardware.

IMG_4343.jpeg


IMG_4321.JPG





MiSTerFPGA Core: Namco System 12 Core beta 4 by XelaNotPu
https://www.patreon.com/XelaNotPu/posts/namco-system-12-172302772


Parts Needed​
Qty​
Item Description​
Notes​
2​
Namco Original Arcade Guns with 6 pin plugs
Player 1 and 2​
1​
24v Power Supply
Mean Well​
1​
Arcade Cabinet with Mirror, coin door, Bezel glass, control panel, 2x gun holsters, 2x 51mm Arcade Buttons 1x Blue and 1x Red with LEDS
​
1​
Jamma Cable
To spice cables into rather than soldering to the Jamma Port​
1​
15khz Monitor 28”
Hantarax Polo​
1​
Heber Multisystem 2 arcade edition VGA out
£270​
1​
Mean Well 5v 4A Power Supply
To power the Mister​
1​
Heber SNAC Classic Cartridge or compatible PSX SNAC adapter with 2 ports and RF
To connect the light guns​
2​
PlayStation extension cables to butcher
​
1​
Molex 2139 4 way
Pre Wired connector to connect to L & R Speakers​
1​
Arduino AtMega32u4
Acts as a USB HID Device to the mister​
1​
Arduino Screw terminal expansion board
Makes connections easier​
1​
64 to 512GB Fast SD Card
Formatted to correct file type​
2​
Dual MOSFET / XY-MOS 5-36V 15A 400W
The Arduino sends a low power voltage to trigger the Solenoid on the Arcade Gun when the trigger is pressed. Features Screw terminals and circuit protection. 1 for Each Gun.​
1​
VGA to screw terminal adapter
To allow the signal from the gen sensor to connect to the C-Sync of the Monitor​
2​
1N4007 diodes
One across the output to protect circuit​
2​
Inline Fuse Holder
Protection​
2​
Fuse
Protection​
1​
Multisystem2.Ini file
To control the FPGA​


Namco Arcade Gun Wiring​
Pin ColourDescription Connects to
1WhiteOptical Sensor SignalPlayStation Controller Pin 9 (Player 1 Gun to Port 1 & Player 2 Gun to Port 2)
2Red+5VDC to power the optical sensor5V Pin on Arduino
3Yellow24v Recoil Constant +24VDCMOSFET / Relay OUT + Pin
4BlackGroundGND Pin on Arduino (Shared with VGA Video GND PIN 10)
5BlueTrigger SwitchDigital Pin 2 on Arduino
6OrangeGun Solenoid Negative switched groundMOSFET OUT - Pin

The Namco arcade gun also features a thicker green cable that is used to earth the Solenoid.

Additional Wiring​
Original Namco Pin​
Original Namco Wiring Colour​
Function​
Connects to​
1​
Red​
Unused​
​
2​
Red​
Unused​
​
3​
Orange​
Player 1 Gun Solenoid​
​
4​
Brown​
Player 1 Start Lamp (Red Arcade Button)​
Player 1 LED Lamp on Arcade Cabinet​
5​
White​
Player 1 Gun Sensor​
​
6​
Black​
Ground (Shared)​
​
7​
Pink​
Player 2 Gun Solenoid​
​
8​
Purple​
Player 2 Start Lamp (Blue Arcade Button)​
Player 2 Lamp on Arcade Cabinet​
9​
Grey​
Gun 2 Sensor​
​
10​
Blue​
Unused​
​

​
Arduino Wiring ​
Pin ColourFunction Connects to
D4BluePlayer 2 Gun Trigger PressPlayer 2 Trigger Microswitch
D2BluePlayer 1 Gun Trigger PressPlayer 1 Trigger Microswitch
D3WhitePlayer 1 MOSFET PCB OutputPlayer 1 MOSFET TRIG
GNDBlackArduino GNDPlayer 1 MOSFET GND
GNDBlackArduino GNDPlayer 2 MOSFET GND
D5WhitePlayer 2 MOSFET PCB OutputPlayer 2 MOSFET TRIG

Note: You must run a ground wire from the Arduino GND pin to the PSX SNAC ground (or the gun's black ground wire). This links the two systems together so the Arduino can see when the trigger line drops to 0V
​
MOSFET Trigger Switch​
PinWire ColourFunctionConnects to
VIN +YellowExternal +24v DC PSUPSU +24v DC
VIN -BlackExternal -24 DC PSU Negative GNDPSU GND
TRIG / PWMWhiteArduino Digital Pin 4Arduino Digital Pin D4
GNDBlackAny Arduino GND PinArduino Ground Pin
VCCNoneLeave Completely Disconnected
OUT +YellowLoad positive wire +Connect to the gun's solenoid positive lead (or bridge from VIN+)
OUT -OrangeLoad Negative -Orange Wire on Arcade Gun

Fit a 1N4007 diode with silver stripe facing OUT+ & Plain Side of the diode to OUT-

PlayStation Controller Port Player 1 & 2​
​
Pin​
Sony Wire Colour​
Function​
Connects to​
Arcade Gun Pin & Colour​
1​
Brown​
Data​
Not used​
NA​
2​
Orange​
Command​
Transmits Trigger Press​
​
3​
Grey or Violet/Purple​
Vibration motor 7.6vdc​
Not used​
NA​
4​
Black​
Common ground for optics and switch​
​
Pin 4 Black​
5​
Red​
+5VDC​
Red wire on Gun Sensor​
Pin 2 Red​
6​
Yellow​
Attention / Chip Select​
Not used​
NA​
7​
Blue​
Clock​
Not used​
NA​
8​
Not used​
Often omitted​
Not used​
NA​
9​
Green​
​
White wire on Gun Sensor (V-Sense Signal)​
Pin 1 White​

MiSTer Settings
The MiSTER needs to be told to output 15Khz for the Arcade monitor and to send the combined Sync and gun optical sensor.
You do not need to connect the monitor sync to the SNAC on the multisystem2

Ensure the mister.ini config is set to: composite_sync=1

When you connect the Arduino to the MiSTEr for the first time you need to connect a usb keyboard and press F12 then scroll down to Serial Mode / SNAC and change from None to Enabled. In the FPGA Core set the gun type mapping to GunCon.

Will this even work?
I welcome any advice :thumbup:
Any Questions just ask!
 
Last edited:
Some cabinet build update photos.


IMG_4447.jpg
IMG_4461.jpeg


Also I have this code that's to go on 2 Arduinos but still no MisTER to test so project is stalled :-(

// Namco arcade gun -> MiSTer, via ATmega32u4 (Leonardo / Pro Micro)
// ONE BOARD PER GUN: flash this same sketch on both boards.
// Needs the "Joystick" library by Matthew Heironimus (Joystick.h).
//
// Wiring per board (signals flow INTO the Arduino, except the recoil gate):
// csync (VGA HSync pin, MiSTer.ini csync=1) -> D3 (INT0) via ~330 ohm
// gun sensor output (clean 5V logic) -> D4 (ICP1)
// trigger switch to GND -> D8 (INPUT_PULLUP)
// D9 -> MOSFET gate (via 100 ohm, 10k pull-down to GND)
// MiSTer/CRT/24V PSU ground -> Arduino GND
//
// UTILITY BUTTONS (Wire switches directly between pin and Arduino GND):
// D5 -> Player Start
// D6 -> Arcade Test
// D7 -> Arcade Service
//
// Timer1 runs at 16 MHz (62.5 ns per tick). An NB-1 line is about 1016 ticks.

#include <Joystick.h>

Joystick_ Joystick(JOYSTICK_DEFAULT_REPORT_ID, JOYSTICK_TYPE_GAMEPAD,
4, 0, // 4 buttons (Trigger, Start, Test, Service), no hat
true, true, // X, Y
false, false, false, false, false, false, false, false, false);

// ---------- CONFIGURATION ----------
#define DEBUG_SERIAL 0 // 1 for diagnostic tracking; 0 for zero-latency gameplay

const uint16_t VSYNC_WIDTH = 480; // sync low pulse longer than this (~30 us) = vsync
const uint8_t MISS_FRAMES = 3; // frames with no pulse = gun off screen
const bool SENSOR_RISING = true; // false if your sensor pulse is active low

// Dynamic Calibration Limits (Maps full raw timer spectrum to MiSTer's internal engine)
const uint16_t H_MIN_LIMIT = 0;
const uint16_t H_MAX_LIMIT = 1500; // Over-scans well beyond the physical ~1016 horizontal line ticks
const uint16_t V_MIN_LIMIT = 0;
const uint16_t V_MAX_LIMIT = 320; // Over-scans standard vertical 240p/480i line bounds

const uint8_t DEBOUNCE_MS = 5;

const bool RECOIL_ENABLED = true;
const uint8_t RECOIL_PIN = 9; // PB5 (Port B, Pin 5)
const uint16_t RECOIL_ON_MS = 35; // solenoid on-time per shot: start low, raise slowly
const uint16_t RECOIL_COOLDOWN_MS = 120; // minimum time between shots (limits heat)
// -----------------------------------

volatile uint16_t lastFall = 0; // TCNT1 at start of last sync pulse
volatile uint16_t lineCount = 0; // lines since end of vsync
volatile uint16_t frameId = 0;
volatile bool inVsync = false;
volatile bool got = false; // first pulse of this frame captured
volatile uint16_t pulseH = 0, pulseV = 0;

ISR(INT0_vect) { // any edge on csync
uint16_t now = TCNT1;
if (!(PIND & _BV(PD0))) { // Falling edge: Sync pulse begins
lastFall = now;
} else { // Rising edge: Pulse ends
uint16_t width = now - lastFall;
if (width > VSYNC_WIDTH) {
inVsync = true;
} else {
if (inVsync) { // first normal hsync after vsync = new frame
inVsync = false;
lineCount = 0;
got = false;
frameId++;
} else {
lineCount++;
}
}
}
}

ISR(TIMER1_CAPT_vect) { // sensor pulse
if (!got && !inVsync) {
uint16_t captureTime = ICR1; // Cache 16-bit hardware register immediately
pulseH = captureTime - lastFall;
pulseV = lineCount;
got = true;
}
}

int16_t scaleAxis(int32_t v, int32_t lo, int32_t hi) {
int32_t out = (v - lo) * 254 / (hi - lo) - 127;
if (out < -127) out = -127;
if (out > 127) out = 127;
return (int16_t)out;
}

// ---- ULTRA-FAST DEBOUNCE BLOCK (DIRECT PORT READS) ----
struct Debouncer {
bool state;
bool lastRaw;
uint32_t lastChange;
};

// Index mappings for readability
const uint8_t IDX_TRG = 0;
const uint8_t IDX_ST = 1;
const uint8_t IDX_TST = 2;
const uint8_t IDX_SVC = 3;

// Layout array matching indices: 0=Trigger, 1=Start, 2=Test, 3=Service
Debouncer buttons[] = {
{ false, false, 0 }, // Index 0: Trigger
{ false, false, 0 }, // Index 1: Start
{ false, false, 0 }, // Index 2: Test
{ false, false, 0 } // Index 3: Service
};

bool updateButtonsFast() {
bool anyChanged = false;
uint32_t now = millis();

// Read target register states in 1 CPU cycle
// D8 = PINB Bit 4 | D5 = PINC Bit 6 | D6 = PIND Bit 7 | D7 = PINE Bit 6
bool rawPins[] = {
!(PINB & _BV(PB4)), // D8 (Trigger)
!(PINC & _BV(PC6)), // D5 (Start)
!(PIND & _BV(PD7)), // D6 (Test)
!(PINE & _BV(PE6)) // D7 (Service)
};

for (uint8_t i = 0; i < 4; i++) {
if (rawPins != buttons.lastRaw) {
buttons.lastRaw = rawPins;
buttons.lastChange = now;
}
if (rawPins != buttons.state && (now - buttons.lastChange) >= DEBOUNCE_MS) {
buttons.state = rawPins;
anyChanged = true;
}
}
return anyChanged;
}

// ---- OPTIMIZED RECOIL (non-blocking, one pulse per trigger pull) ----
bool recoilOn = false;
uint32_t recoilStart = 0;

void updateRecoil(bool fire) {
if (!RECOIL_ENABLED) return;
uint32_t now = millis();

if (recoilOn && (now - recoilStart) >= RECOIL_ON_MS) {
PORTB &= ~_BV(PB5); // Fast digital write LOW to D9
recoilOn = false;
}
if (fire && !recoilOn && (now - recoilStart) >= RECOIL_COOLDOWN_MS) {
PORTB |= _BV(PB5); // Fast digital write HIGH to D9
recoilOn = true;
recoilStart = now;
}
}

void setup() {
// Configure Recoil Pin D9 (PORTB, Pin 5) as Output, write LOW (first thing: gate low so solenoid is off)
DDRB |= _BV(PB5);
PORTB &= ~_BV(PB5);

// Set Pin Pullups via Direct Data Register manipulations
DDRB &= ~_BV(PB4); PORTB |= _BV(PB4); // D8 (Trigger) Input Pullup
DDRC &= ~_BV(PC6); PORTC |= _BV(PC6); // D5 (Start) Input Pullup
DDRD &= ~_BV(PD7); PORTD |= _BV(PD7); // D6 (Test) Input Pullup
DDRE &= ~_BV(PE6); PORTE |= _BV(PE6); // D7 (Service) Input Pullup

pinMode(3, INPUT); // csync
pinMode(4, INPUT); // sensor

Joystick.setXAxisRange(-127, 127);
Joystick.setYAxisRange(-127, 127);
Joystick.begin(false); // explicitly controlled state updates

#if DEBUG_SERIAL
Serial.begin(115200);
#endif

// Timer1: free running, prescaler 1, input capture with noise canceller
TCCR1A = 0;
TCCR1B = _BV(CS10) | _BV(ICNC1) | (SENSOR_RISING ? _BV(ICES1) : 0);
TIMSK1 = _BV(ICIE1);

// INT0: interrupt on any logical change
EICRA = (EICRA & ~(_BV(ISC01) | _BV(ISC00))) | _BV(ISC00);
EIFR = _BV(INTF0);
EIMSK |= _BV(INT0);
}

uint16_t lastFrame = 0;
uint8_t missCount = 0;
int16_t x = 127, y = 127; // start off screen
int16_t lastX = 999, lastY = 999; // Caches previous state values

void loop() {
bool oldTrigger = buttons[IDX_TRG].state;
bool changed = updateButtonsFast();
bool triggerPressedEdge = (buttons[IDX_TRG].state && !oldTrigger);

updateRecoil(triggerPressedEdge); // fire on the press edge only
bool needSend = changed; // trigger works even with no video sync

// Atomic capture block for critical multi-byte ISR variables
noInterrupts();
uint16_t f = frameId;
bool g = got;
uint16_t h = pulseH;
uint16_t v = pulseV;
interrupts();

if (f != lastFrame) { // a new frame has started
lastFrame = f;
if (g) {
missCount = 0;
x = scaleAxis(h, H_MIN_LIMIT, H_MAX_LIMIT);
y = scaleAxis(v, V_MIN_LIMIT, V_MAX_LIMIT);

#if DEBUG_SERIAL
Serial.print("h="); Serial.print(h);
Serial.print(" v="); Serial.println(v);
#endif
} else if (missCount < MISS_FRAMES) {
missCount++;
}

if (missCount >= MISS_FRAMES) {
x = 127;
y = 127; // off screen
}
}

// Only trigger a USB state send if physical positioning data or inputs change
if (x != lastX || y != lastY) {
lastX = x;
lastY = y;
needSend = true;
}

if (needSend) {
Joystick.setXAxis(x);
Joystick.setYAxis(y);
Joystick.setButton(0, buttons[IDX_TRG].state); // Trigger
Joystick.setButton(1, buttons[IDX_ST].state); // Start
Joystick.setButton(2, buttons[IDX_TST].state); // Test
Joystick.setButton(3, buttons[IDX_SVC].state); // Service
Joystick.sendState();
}
}
 
Back
Top