아두이노 나노와 SG90 서보로 만드는 DIY 2축 모션 레이싱 시뮬레이터
프로젝트 개요
SimTools 모션 시뮬레이터 소프트웨어와 아두이노 나노, 저가 SG90 마이크로 서보 2개를 조합하여 2축(롤/피치) 모션 플랫폼을 구현합니다. 게임에서 전송되는 텔레메트리 데이터(가속도, 각도 등)를 SimTools가 시리얼로 아두이노에 전송하면, 아두이노가 서보를 구동해 플랫폼을 기울이는 방식입니다. 소규모 프로토타입이며, 실사용 시 대형 서보/모터 드라이버로 확장 가능합니다.
하드웨어 구성
| 부품 | 수량 | 비고 |
| 아두이노 나노 R3 | 1 | 시리얼 수신 및 서보 PWM 제어 |
| SG90 마이크로 서보 모터 | 2 | 롤(좌우), 피치(전후) 축 구동 |
| 납땜 도구, 브레드보드/퍼프보드, 점퍼 와이어 | - | 회로 구성용 |
소프트웨어 구성
- SimTools: 모션 시뮬레이터 미들웨어. 게임 플러그인(Live for Speed, Assetto Corsa, iRacing 등) 지원. 데모 모드에서 LFS 플러그인 완전 동작
- 아두이노 펌웨어: SimTools 전용 프로토콜 파싱 및 서보 구동
SimTools 설정 요약
- SimTools 다운로드: xsimulator.net
- Live for Speed 데모 다운로드: lfs.net (SimTools 데모 모드에서 테스트 가능)
- SimTools에서 아두이노 시리얼 포트(COMx, 9600bps) 설정, 액추에이터 2개(R, L) 매핑
- 사용자 매뉴얼: PDF 다운로드
아두이노 펌웨어 전체 코드
//********************************************************************************************
// RC Model Servo for SimTools Motion Software
// Original by EAOROBBIE, Modified by aarondc
//********************************************************************************************
#include
// #define DEBUG 1 // 디버그 출력 활성화 시 주석 해제
const int kActuatorCount = 2;
const char kActuatorName[kActuatorCount] = { 'R', 'L' }; // SimTools가 보내는 액추에이터 식별 문자
const int kPins[kActuatorCount] = { 4, 5 }; // 서보 연결 핀
const int kActuatorScale[kActuatorCount][2] = {
{ 0, 179 }, // Right 액추에이터 스케일링 (입력 0~255 → 서보 0~179)
{ 179, 0 } // Left 액추에이터 스케일링 (반전)
};
const char kEOL = '~'; // 값 구분자 (틸드)
const int kMaxCharCount = 3; // 최대 3자리 숫자(0~255)
Servo actuatorSet[kActuatorCount];
int actuatorPosition[kActuatorCount] = { 90, 90 };
int currentActuator = 0;
int valueCharCount = 0;
enum TPortState { psReadActuator, psReadValue };
TPortState currentState = psReadActuator;
void setup() {
for (int i = 0; i < kActuatorCount; i++) {
actuatorSet[i].attach(kPins[i]);
updateActuator(i);
}
Serial.begin(9600);
}
void loop() { } // 모든 처리는 serialEvent()에서
void serialEvent() {
char tmpChar;
int tmpValue;
while (Serial.available()) {
// 1단계: 액추에이터 이름 문자 읽기
if (currentState == psReadActuator) {
tmpChar = Serial.read();
#ifdef DEBUG
Serial.print("read in "); Serial.println(tmpChar);
#endif
for (int i = 0; i < kActuatorCount; i++) {
if (tmpChar == kActuatorName[i]) {
currentActuator = i;
currentState = psReadValue;
actuatorPosition[currentActuator] = 0;
valueCharCount = 0;
#ifdef DEBUG
Serial.print("which is actuator "); Serial.println(i);
#endif
break;
}
}
}
// 2단계: 숫자 값 읽기 (최대 3자리, '~'로 종료)
if (currentState == psReadValue) {
while (valueCharCount < kMaxCharCount && Serial.available()) {
tmpValue = Serial.read();
if (tmpValue != kEOL) {
tmpValue -= 48; // ASCII '0' 보정
if (tmpValue < 0 || tmpValue > 9) tmpValue = 0;
actuatorPosition[currentActuator] = actuatorPosition[currentActuator] * 10 + tmpValue;
valueCharCount++;
} else break;
}
// 값 수신 완료 시 서보 업데이트
if (tmpValue == kEOL || valueCharCount == kMaxCharCount) {
#ifdef DEBUG
Serial.print("read in "); Serial.println(actuatorPosition[currentActuator]);
#endif
// 0~255 입력 범위를 서보 각도 범위로 매핑
actuatorPosition[currentActuator] = map(actuatorPosition[currentActuator], 0, 255,
kActuatorScale[currentActuator][0],
kActuatorScale[currentActuator][1]);
#ifdef DEBUG
Serial.print("scaled to "); Serial.println(actuatorPosition[currentActuator]);
#endif
updateActuator(currentActuator);
currentState = psReadActuator;
}
}
}
}
void updateActuator(int thisActuator) {
actuatorSet[thisActuator].write(actuatorPosition[thisActuator]);
}
회로도

제작 팁 및 확장
- 서보 진동/떨림 발생 시 코드 내
delay 조정 또는 디지털 서보용 라이브러리(300Hz PWM) 사용 고려
- 전원 공급: 서보 2개 동시 구동 시 아두이노 5V 레귤레이터 과부하 우려 → 외부 5V/2A 이상 전원 권장
- 실제 시뮬레이터 제작 시: 대형 서보(예: Dynamixel, 웜기어 모터) + 모터 드라이버 + 강성 프레임 필요
- SimTools 플러그인 개발 SDK 활용하여 미지원 게임 연동 가능
- 3축(요우 추가) 또는 6축(헥사포드) 확장은 액추에이터 수 및 프로토콜 확장만으로 가능
참고 커뮤니티