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.
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.
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.
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.
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).
// 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:
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
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.
Pin Assignments & Wiring
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 |
|---|---|---|
D2 | Motor 1 direction | BTS7960 #1 RPWM |
D3~ | Motor 1 enable | BTS7960 #1 R_EN + L_EN (bridge) |
D9~ | Motor 1 PWM speed | BTS7960 #1 LPWM |
D4 | Motor 2 direction | BTS7960 #2 RPWM |
D5~ | Motor 2 enable | BTS7960 #2 R_EN + L_EN (bridge) |
D10~ | Motor 2 PWM speed | BTS7960 #2 LPWM |
D6~ | Motor 3 direction | BTS7960 #3 RPWM |
D7 | Motor 3 enable | BTS7960 #3 R_EN + L_EN (bridge) |
D11~ | Motor 3 PWM speed | BTS7960 #3 LPWM |
A0 | Motor 1 feedback | Potentiometer 1 wiper |
A1 | Motor 2 feedback | Potentiometer 2 wiper |
A2 | Motor 3 feedback | Potentiometer 3 wiper |
D0/D1 | USB Serial | PC 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.
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.
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
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.
Safety Systems
🔴 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.
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.
SMC3 Firmware (GitHub) · SMC3Utils (GitHub) · FlyPT Mover · SimTools · XSimulator Community