Knowledge Base
DIY Motion Simulator

3-Axis Motion Sim
Software Stack

Complete implementation guide — from game telemetry to motor shaft. Arduino Uno + BTS7960 H-bridge + DC motors with PID position control, driving Assetto Corsa, ACC & DCS World.

Arduino Uno DC Motors × 3 BTS7960 H-Bridge SMC3 Firmware FlyPT Mover / SimTools PID @ 4,096 Hz
01

System Architecture

The full data pipeline from physics engine to motor shaft has four stages. Each is handled by a dedicated, well-tested component — no custom code required.

🎮
Game
Shared Memory / UDP
🖥️
Motion Cueing
FlyPT Mover / SimTools
📡
Serial USB
115200 baud · 3-byte packets
SMC3
Arduino PID → PWM
🔩
Motors
BTS7960 H-Bridge × 3
02

PC-Side Software

Two applications dominate the DIY motion sim space. Both read game telemetry, apply washout/filtering, and stream actuator position targets over serial USB to the Arduino. Both have native SMC3 output presets.

Free

FlyPT Mover

Most advanced motion cueing in the DIY space. Node-based architecture with 30+ filter types, 3D rig visualization, and real-time tuning. Steeper learning curve but superior output.

Games
30–40 native
Filters
30+ types
VR Support
OVRMC built-in
3D Preview
Yes

SimTools

Most mature option with the largest community on xsimulator.net. Plugin pipeline architecture with the widest game support. Built-in SMC3 serial preset—zero configuration needed.

Games
100+ plugins
Filters
Washout + Smooth
Community
Largest (XSim)
SMC3 Preset
One-click
💡
Recommendation: Start with FlyPT Mover. It's free, has a dedicated SMC3 output mode, and FlyPT can use SimTools as a source — giving indirect access to all 100+ SimTools game plugins if you ever need them.
03

SMC3 Arduino Firmware

The SMC3 (Simulator Motor Controller 3-axis) firmware is the de facto standard for DC motor motion sims. It runs closed-loop PID position control for 3 motors at ~4,096 updates/second with 10-bit resolution. Get it from GitHub, along with the SMC3Utils Windows tuning utility.

Serial Protocol

Fixed binary protocol at 115200 baud, 8N1. Each command is exactly 3 bytes: one ASCII letter + two bytes encoding a 10-bit value (0–1023).

serial_protocol.txt
// Position commands (3 bytes each):
[A][highByte][lowByte]  → Motor 1 target position
[B][highByte][lowByte]  → Motor 2 target position
[C][highByte][lowByte]  → Motor 3 target position

// PID tuning commands:
[D][value]  Kp Motor 1    [G][value]  Ki Motor 1    [J][value]  Kd Motor 1
[E][value]  Kp Motor 2    [H][value]  Ki Motor 2    [K][value]  Kd Motor 2
[F][value]  Kp Motor 3    [I][value]  Ki Motor 3    [L][value]  Kd Motor 3

PID Control Loop

Runs in a timer interrupt at ~4,096 Hz across all three motors. This is the core algorithm per motor:

pid_core.cpp
int error = target - feedback;                    // Both 0–1023 from pot ADC
long P_term = (long)Kp * error;                   // Proportional

integralSum = constrain(integralSum + error,      // Anti-windup clamp
                        -iLimit, iLimit);
long I_term = (long)Ki * integralSum;             // Integral
long D_term = (long)Kd * (feedback - prevFeedback); // Derivative on measurement

long output = (P_term + I_term - D_term) >> 7;   // Scale ÷128
int pwm = constrain(output, -pwmMax, pwmMax);

if (abs(error) <= deadzone)  pwm = 0;             // Dead band
if (pwm > 0 && pwm < pwmMin) pwm = pwmMin;        // Overcome stiction
🎛️
Starting PID values: Kp 300–500 · Ki 0 · Kd 50–200 · PWMmin 20–40 · Deadzone 1. Start with Ki=0, Kd=0, raise Kp until responsive, then add Kd to kill oscillation. Use SMC3Utils for real-time target vs. feedback traces.
04

Pin Assignments & Wiring

⚠️
Use BTS7960/IBT-2, not L298N. L298N is limited to ~2A with 1.8–3.2V voltage drop (BJT). Motion sim motors draw 5–20A under load. BTS7960 handles 43A peak with MOSFET topology and ~0.1V drop.

SMC3 compile-time Mode 2 (IBT-2/BTS7960). Separate motor PSU (12–24V, 20–30A). Motor PSU GND must tie to Arduino GND (common ground). Power Arduino via USB only.

Arduino Pin SMC3 Function Connects To
D2Motor 1 directionBTS7960 #1 RPWM
D3~Motor 1 enableBTS7960 #1 R_EN + L_EN (bridge)
D9~Motor 1 PWM speedBTS7960 #1 LPWM
D4Motor 2 directionBTS7960 #2 RPWM
D5~Motor 2 enableBTS7960 #2 R_EN + L_EN (bridge)
D10~Motor 2 PWM speedBTS7960 #2 LPWM
D6~Motor 3 directionBTS7960 #3 RPWM
D7Motor 3 enableBTS7960 #3 R_EN + L_EN (bridge)
D11~Motor 3 PWM speedBTS7960 #3 LPWM
A0Motor 1 feedbackPotentiometer 1 wiper
A1Motor 2 feedbackPotentiometer 2 wiper
A2Motor 3 feedbackPotentiometer 3 wiper
D0/D1USB SerialPC via USB cable

Potentiometer wiring: 10kΩ linear pots as voltage dividers — one leg to 5V, other to GND, wiper to A0/A1/A2. Add a 0.1µF ceramic cap between each wiper and GND, right at the Arduino pin. Route pot signal wires physically away from motor power cables.

Power filtering: 100µF electrolytic cap across each BTS7960 motor power input, plus a 0.1µF ceramic cap for high-frequency noise suppression.

05

Game Telemetry

Assetto Corsa / ACC Shared Memory

Both titles expose telemetry via Windows shared memory (memory-mapped files) at ~100 Hz. Three pages: acpmf_physics (G-forces, orientation, suspension), acpmf_graphics (session info), and acpmf_static (car/track metadata). No configuration needed — both SimTools and FlyPT read shared memory directly.

Key motion fields: accG[3] (lateral/vertical/longitudinal G), pitch, roll, heading (radians), localAngularVel[3] (roll/pitch/yaw rates), suspensionTravel[4] (road texture). ACC extends AC's structure but retains the same motion-critical fields — AC profiles are largely compatible.

DCS World Lua Export → UDP

DCS uses Lua export scripts that send telemetry over UDP. Functions: LoGetADIPitchBankYaw() (orientation in radians), LoGetAccelerationUnits() (G-forces as {x,y,z}), LoGetAngularVelocity(). Place the export script in %USERPROFILE%\Saved Games\DCS\Scripts\Export.lua.

Export.lua
package.path  = package.path..";.\\LuaSocket\\?.lua"
package.cpath = package.cpath..";.\\LuaSocket\\?.dll"
socket = require("socket")
local udp = nil

function LuaExportStart()
    udp = socket.udp()
    udp:settimeout(0)
end

function LuaExportActivityNextEvent(t)
    local pitch, bank, yaw = LoGetADIPitchBankYaw()
    local acc = LoGetAccelerationUnits()
    local angVel = LoGetAngularVelocity()
    if pitch and acc then
        local data = string.format(
            "%.4f;%.4f;%.4f;%.4f;%.4f;%.4f;%.4f;%.4f;%.4f",
            pitch, bank, yaw,
            acc.x, acc.y, acc.z,
            angVel.x, angVel.y, angVel.z)
        udp:sendto(data, "127.0.0.1", 4123)
    end
    return t + 0.02  -- 50 Hz
end

function LuaExportStop()
    if udp then udp:close() end
end
⚠️
DCS only loads one Export.lua. If you run SRS, TacView, or other tools with export scripts, use the Scripts\Hooks\ folder instead to avoid conflicts.

Custom / Generic Input UDP

For unsupported games: SimTools accepts UDP on port 4123 in semicolon-separated float format: pitch;roll;yaw;surge;sway;heave. Any app that emits this format becomes a telemetry source. FlyPT can also use SimTools itself as a source, giving indirect access to all 100+ game plugins.

06

Safety Systems

🛑
This system moves a human-occupied chair with significant force. Safety engineering is non-negotiable. Get the hardware E-stop wired and tested before the first motor ever moves.

🔴 Hardware E-Stop

Normally-closed mushroom button in series with AC mains feeding the motor PSU. Kills all motor power regardless of software state. Avoids the problem of switching high-current DC directly.

📐 SMC3 Clip Zones

Configurable band near each travel limit where SMC3 applies hard reverse braking. Max Limit zone beyond that shuts down all H-bridge drivers and requires manual reset.

⏱️ Serial Watchdog

Parks all motors to center if no valid serial command arrives within 500ms. Catches USB disconnections, game crashes, and PC freezes.

🔌 Inline Fuses

Automotive blade fuses on each motor's positive wire, rated slightly above normal operating current. Protects against stall current and wiring faults.

🧱 Mechanical Hard Stops

Rubber bumpers bolted at each axis travel limit. The ultimate physical safety layer — stops motion even if all electronics fail.

⚡ PWM Limiting

Keep PWMmax below 255 during initial tuning. Limits motor speed and force while you dial in PID parameters and verify feedback direction.

07

Quick Start Checklist

With hardware already built, the entire software stack can be operational in an afternoon. The bulk of time goes to PID tuning, not installation.

Wire the E-Stop

NC mushroom button in series with motor PSU AC mains. Test it. Test it again.

Flash SMC3 to Arduino

Download from GitHub. Select IBT-2/BTS7960 compile-time mode in the .ino before uploading.

Wire BTS7960 Modules

Follow the pin table above. Common ground between motor PSU and Arduino. Caps on power inputs and pot wipers.

Tune PID with SMC3Utils

Connect via COM port. Start Kp=400, Ki=0, Kd=0. Raise Kp until responsive, add Kd to kill oscillation. Watch the target (blue) vs feedback (green) traces.

Install FlyPT Mover (or SimTools)

Select SMC3 output preset → COM port → 115200 baud → 10-bit binary. Test with virtual output before enabling real motors.

Configure Game Telemetry

AC/ACC: zero config (shared memory auto-detected). DCS: place Export.lua in Saved Games folder. Select game source in motion software.

Tune Motion Cueing

Adjust gains, washout, smoothing per axis. Start conservative (low gains) and increase until it feels right. Use FlyPT's 3D preview to verify axis mapping before sitting in the chair.