펌마타(Firmata)와 엑스박스 원 게임패드로 아두이노 로버 원활하게 제어하기
부품 및 소모품
| 마이크로소프트 엑스박스 컨트롤러 및 윈도우용 무선 어댑터 |
1개 |
| 스파크펀 블루투스 모뎀 - 블루스미르프 실버 (또는 HC-05, HC-06 동일) |
1개 |
| 아두이노 우노 (Arduino UNO) |
1개 |
앱 및 온라인 서비스
- 마이크로소프트 윈도우 10 IoT 코어 - 표준 펌마타 스케치와 엑스박스 원 컨트롤러 사이의 중개 역할
프로젝트 소개
몇 달 전 아주 저렴한 가격에 작은 로버 키트(아두이노 우노 제어)를 구했습니다. 키트가 매우 알찼습니다: 차체, 바퀴 2개, DC 기어 모터 2개, 우노 R3, L298N 듀얼 H-브리지 모터 컨트롤러 등 여러 부품 포함.
이 로버는 자율 주행용으로 프로그래밍하게 되어 있습니다. 초음파 센서와 서보도 키트에 포함되어 있고 아두이노 센서 실드 5도 들어 있습니다. 진짜 득템이었죠 ;-)
하지만 제 생각은 엑스박스 원 컨트롤러와 펌마타 프로토콜로 이 로버를 직접 조종하는 것이었습니다. 제 아들 중 한 명이 조종하게 하려고요. 아주 잘 작동합니다!
키트 조립
키트 조립은 어렵지 않습니다. 설명서가 중국어였지만 모두 논리적으로 보입니다. 베이스 플레이트가 하나라 일부 부품은 위쪽에, 일부는 아래쪽(이제 모터 컨트롤러)에 장착했습니다.
사진에서 두 개의 서로 다른 배터리 홀더를 볼 수 있습니다. 프로그래밍 중 모터를 돌리려면 별도 전원이 필요하다는 걸 알게 되었습니다.
모터 연결 - 컨트롤러 핀 레이아웃
이번 하드웨어의 핵심은 아두이노 우노가 아니라 모터 컨트롤러입니다.
"L298은 DC 브러시 모터와 스테퍼 모터용 듀얼 H-브리지 드라이버입니다. 넓은 동작 전압 범위를 지원하며 스루홀 패키지로 DIY 프로젝트에 접근하기 쉽게 2A/채널을 제공합니다."
이 컨트롤러는 아두이노에서 오는 신호로 두 모터의 속도와 방향을 제어합니다.
L298N 듀얼 H-브리지 모터 컨트롤러 핀 레이아웃:
- DC 모터 1 +
- DC 모터 1 -
- 전원 입력: 별도 배터리 홀더에서 9V 공급
- 공통 그라운드: 별도 배터리 홀더와 아두이노 모두 연결
- 전원 출력: 5V 출력 가능 (미사용. 아두이노는 다른 배터리 홀더로 전원 공급)
- ENA → 아두이노 D10 (PWM 가능, 흰색)
- IN1 → 아두이노 D9 (회색)
- IN2 → 아두이노 D8 (보라)
- IN3 → 아두이노 D7 (파랑)
- IN4 → 아두이노 D6 (초록)
- ENB → 아두이노 D5 (PWM 가능, 노랑)
- DC 모터 2 +
- DC 모터 2 -
- 2번과 3번 옆 점퍼는 제거하지 않음 (입력 전원 12V 초과 안 함, 최대 35V까지 가능) (사진에 점퍼 표시 안 됨)
참고: 컨트롤러에 자체 전원이 있고 아두이노에도 별도 전원이 있습니다. 공통 그라운드로 연결해야 합니다 (위 4번 항목 참조).
모터 연결 - 아두이노 핀 레이아웃
아두이노 연결은 직관적입니다. 전원 공급장치의 그라운드와 5V를 아두이노에 연결하고, 여섯 줄(ENA, IN1-4, ENB)을 D10~D5 핀에 연결합니다.
포트 7, 8 (다른 모터는 9, 10)은 일반 GPIO 포트입니다. 모터 방향 제어용입니다. 두 포트(예: 7, 8) 모두 LOW면 모터 정지. 하나는 HIGH, 다른 하나는 LOW면 한 방향으로 회전. 반대로 연결하면 반대 방향 회전.
하지만 그냥 이 핀들만 설정해선 아무 일도 안 일어납니다. 모터가 안 돌아요!
마법은 핀 5와 10에서 일어납니다. 이 '특별한' 핀들은 PWM 신호를 생성할 수 있습니다. 0~255 값을 설정하면 모터가 매우 느리게(정지)부터 고속까지 회전합니다.
참고: 아두이노 우노의 PWM 가능 포트에는 물결표(~)가 표시되어 있습니다.
블루투스 연결
사용한 블루투스 모듈은 RX, TX 포트만 연결하면 됩니다 (라인 크로스). 아두이노에서 5V 전원과 그라운드도 필요합니다.
아두이노에 펌마타 스케치 올리기
펌마타 스케치 "StandardFirmata"만 있으면 됩니다! 아두이노 IDE 예제에서 가져와 업로드하세요 (업로드 완료 위해 TX/RX 잠시 분리 필요할 수 있음).
참고: 블루투스 모듈 품질 문제로 스케치에 하드코딩된 보드레이트를 9600으로 낮췄습니다.
신사 여러분, 엔진 스타트
아니, 연결 테스트...
왜 펌마타를 쓰냐고요? 쉽기 때문입니다. 얼마나 쉽냐면요? 매우 쉽습니다. 프로그래밍 없이도 가능합니다. 윈도우 리모트 아두이노 익스피리언스 앱(스토어에서 다운로드, 윈도우 10 모바일에서도 작동)만 실행하면 됩니다.
이미 페어링된 HC-06 블루투스 모듈에 연결합니다.
PWM 페이지로 가서 디지털 핀 5를 활성화하고 값 128을 줍니다.
주의: 다음 단계에서 모터가 돕니다. 헬멧 쓰고 바퀴를 바닥에서 띄우세요. 왜인지 알게 될 겁니다.
디지털 페이지로 가서 디지털 핀 6 스위치를 켭니다.
모든 게 연결되고 작동하면 모터 하나가 돕니다!
그렇다면 다른 속도(PWM 값 조절)나 방향(디지털 핀 6을 0V, 디지털 핀 7을 5V로 설정)도 테스트해 보세요.
두 바퀴가 모두 돕니다. 이제 코딩 시작...
중매자 역할의 UWP 앱
엑스박스 원 컨트롤러와 로버 사이에 새로운 UWP 앱이 필요합니다.
이미 블로그에서 엑스박스 원 컨트롤러 사용법을 다뤘습니다. 이번엔 그 지식과 로버를 결합합니다.
UWP 앱 인터페이스를 봅시다: 버튼 두 개와 텍스트 블록으로 매우 심플합니다. 할 일은 첫째, 펌마타로 아두이노에 연결. 연결되면 컨트롤러 버튼으로 거대 루프를 시작해 컨트롤러 입력을 읽고 유용한 명령으로 변환.
클래식 탱크 컨트롤(두 개의 스틱)을 만들고 싶습니다. 엑스박스 원 컨트롤러의 두 썸스틱만 확인하면 됩니다. 앞으로/뒤로 밀면 해당 모터가 앞/뒤로 돕니다.
참고: 새 UWP 앱 시작 시 블루투스 기능 추가 잊지 마세요. 펌마타용 누겟 패키지 설치도 필요합니다.
먼저 XAML 스니펫(코드 섹션 참조)을 메인 폼 그리드에 추가해 버튼들을 만듭니다.
다음으로 메인 폼 코드 비하인드(코드 섹션 C# 소스 참조)를 추가합니다. 기본적으로 두 부분: 첫째, 블루투스로 펌마타 프로토콜 연결. 둘째, 엑스박스 원 컨트롤러 입력 리스닝 시작.
컨트롤 체크 루프 내에서 썸스틱이 앞/뒤/정지 범위 중 어디인지 판단하고, PWM 핀에 줄 값 결정.
PWM 값 쓰기가 펌마타에서 디지털 포트에 아날로그 값 쓰기로 표현되는 게 흥미롭습니다. 아두이노 클래스에 특별한 PWM 메서드는 없습니다.
"ArduinoDigitalWrite"와 "ArduinoAnalogWrite" 두 메서드를 추가해 같은 값을 아두이노에 반복 전송하지 않게 했습니다. 이렇게 안 하면 통신이 지저분해지고 아두이노 성능 저하됩니다. (더 풍부한 설계에서 부저 추가했는데, 중복 명령 무시 안 하면 부저 성능이 극도로 나빠지고 소리가 끔찍했습니다).
모터가 낮은 PWM 펄스를 받으면 바로 돌지 않고 독특한 소리로 휘파람을 붑니다. 이는 정상 동작입니다. 덕분에 전원 두 개가 필요하다는 걸 알았습니다. 처음엔 컨트롤러 고장인 줄 알았는데 한 모터 플러그 뽑으니 다른 게 휘파람 불더군요.
작동 원리:
각주: 깜빡이는 라이트는 드라마틱 효과뿐 아니라 안전용입니다. 창작물이 움직이기 시작하면 안전에 유의하세요. 장치는 멍청하지만 당신은 아닙니다! 그래서 처음에 위에 이 라이트를 달았습니다.
코드
통신 시작용 XAML 컨트롤
새 UWP 앱의 메인 폼에 붙여넣으세요
<StackPanel Orientation="Vertical">
<Button Name="btnStart" Click="btnStart_Click" FontSize="50" Width="400" Content="Start" />
<Button Name="btnController" Content="Controller" FontSize="50" Width="400" IsEnabled="False" Click="btnController_Click" />
<TextBlock Name="tbRead" FontSize="20" Foreground="Gray" />
</StackPanel>
UWP 앱 메인폼 코드 비하인드 (C#)
이 코드는 펌마타로 로버에 연결하고 엑스박스 원 컨트롤러 명령을 전달합니다
public sealed partial class MainPage : Page {
private BluetoothSerial _bluetooth;
private RemoteDevice _arduino;
private UwpFirmata _firmata = null;
private Gamepad _Gamepad = null;
private double _SpeedLimit = 0.75; // PWM 최대 255 * 0.75
private double _ActionPoint = 0.15; // 이 절대값 미만이면 로버 정지
private byte _LeftForward = 6;
private byte _LeftBackward = 7;
private byte _LeftValue = 5;
private byte _RightForward = 9;
private byte _RightBackward = 8;
private byte _RightValue = 10;
private Dictionary<byte, PinState> _CurrentPinStates = new Dictionary<byte, PinState>();
private Dictionary<byte, ushort> _CurrentSpeedValues = new Dictionary<byte, ushort>();
public MainPage() { this.InitializeComponent(); }
private void btnStart_Click(object sender, RoutedEventArgs e) {
btnStart.IsEnabled = false;
Gamepad.GamepadAdded += Gamepad_GamepadAdded;
Gamepad.GamepadRemoved += Gamepad_GamepadRemoved;
_bluetooth = new BluetoothSerial("HC-06");
_bluetooth.ConnectionLost += BluetoothConnectionLost;
_bluetooth.ConnectionFailed += BluetoothConnectionFailed;
_bluetooth.ConnectionEstablished += OnConnectionEstablished;
_firmata = new UwpFirmata();
_arduino = new RemoteDevice(_firmata);
_firmata.begin(_bluetooth);
_bluetooth.begin(9600, SerialConfig.SERIAL_8N1);
_arduino.DeviceReady += ArduinoDeviceReady;
}
// ... (이하 연결 처리, 컨트롤러 이벤트, 루프 로직 등 생략 - 원본 참조)
}