IoT 프로젝트를 하면서 스텝모터를 제어해보다 보니 생각보다 복잡했습니다. 처음에는 다른 모듈처럼 라이브러리 선택하고 코드짜는 게 문제일 줄 알았는데, 정작 배선할 때 보니 드라이버도 있고, 라이브러리도 여러 가지였습니다.
그래서 이 글에서는 제가 알아본 두 가지 라이브러리의 차이점과, 왜 AccelStepper를 선택하게 됐는지 정리했습니다.
잠깐, 라이브러리 얘기 전에 드라이버부터 짚고 넘어가야 할 것 같습니다.
스텝모터는 정해진 방향으로 무한히 도는 모터입니다. 원리 자체가 강한 전자석 자기장을 만들어 그거에 맞춰서 회전하는 거죠. 그러다 보니 강한 전류가 필요합니다.
아두이노 MCU에서 드라이버 없이 직접 제어하려고 하면 당연히 전류 부족으로 모터가 제대로 안 돌거나, 최악의 경우 MCU에 손상을 입힙니다.
드라이버는 이런 문제들을 해결해줍니다:
이제 드라이버가 준비됐다면, 이 모터를 제어할 라이브러리를 골라야 합니다. 라이브러리를 사용할 때 핀을 설정해 줘야 하는데, 핀 정보는 드라이버와 연결되어 있는 핀이어야 합니다.
"아두이노 스텝모터"를 검색하면 가장 먼저 나오는 게 기본 Stepper 라이브러리입니다. 간단하고 바로 쓸 수 있어서 좋지만... 실제 프로젝트에 사용해보면 한계가 보입니다.
| 항목 | Stepper | AccelStepper |
|---|---|---|
| 동작 방식 | 블로킹(Blocking) | 논블로킹(Non-blocking) |
| 속도 제어 | 고정 속도만 가능 | 최대속도, 가속/감속 지원 |
| 지원 모터 | 주로 4선 스텝모터 | 2/3/4선 다양한 인터페이스 |
| 모션 품질 | 급격한 시작/정지 | 부드러운 가속/감속 |
이것이 두 라이브러리의 가장 큰 차이점인데, 실제로 코드를 짜보면 정말 중요합니다. 특히 여러 모듈이 상호작용하는 프로젝트일 수록, loop 주기에 딜레이가 최소화되어야 합니다.
블로킹 방식 (Stepper):
모터 제어 함수 호출
↓
함수가 끝날 때까지 기다림
↓
다른 코드 실행 가능
Stepper 라이브러리의 step() 함수를 호출하면, 정해진 스텝만큼 이동할 때까지 다른 코드를 실행할 수 없습니다. 만약 100스텝을 이동하는 데 1초가 걸린다면, 그 1초 동안 센서를 읽을 수도 없고, 다른 모터를 제어할 수도 없습니다. 시스템의 주기성이 깨지기 쉽습니다.
논블로킹 방식 (AccelStepper):
제어 함수 호출
↓
함수 즉시 반환
↓
loop()에서 계속 run() 호출하면서 배경에서 동작
AccelStepper는 moveTo()로 목표를 지정하고, loop()에서 계속 run()을 호출하기만 하면 됩니다. 그 사이에 센서도 읽고, LED도 깜빡이고, 다른 모터도 제어할 수 있습니다.
실제 차이를 느낀 순간:
처음에 Stepper로 모터를 제어하다가, 동시에 다른 센서를 읽어야 하는 상황이 생겼습니다. 그때 모터가 움직이는 동안 센서 데이터가 밀렸고, 시스템이 예측 불가능하게 동작했습니다. AccelStepper로 바꾸니까 깔끔하게 해결됐습니다.
AccelStepper를 선택했다면, 이제 어떻게 써야 할지 알아야 합니다.
// 28BYJ-48 스텝모터를 사용하는 경우
const int STEP_INT1 = 2;
const int STEP_INT2 = 3;
const int STEP_INT3 = 5;
const int STEP_INT4 = 6;
AccelStepper stepper(AccelStepper::FULL4WIRE, STEP_INT1, STEP_INT3, STEP_INT2, STEP_INT4);
void setup() {
stepper.setMaxSpeed(1000); // 최대 1000 step/s
stepper.setAcceleration(500); // 가속도 500 step/s²
stepper.setCurrentPosition(0); // 현재 위치를 0으로 초기화
}
interface 파라미터가 중요한데, 모터와 드라이버 방식에 따라 달라집니다.
| 타입 | 숫자 | 모터/드라이버 유형 | 언제 쓸까? |
|---|---|---|---|
DRIVER | 1 | STEP/DIR 드라이버 (A4988, DRV8825 등) | 스텝/방향 신호로 제어하는 드라이버 |
FULL4WIRE | 4 | 4선 풀스텝 모터 | 강하고 빠른 회전이 필요할 때 |
HALF4WIRE | 8 | 4선 하프스텝 (28BYJ-48 등) | 정밀 제어가 필요할 때, 더 부드러운 움직임 |
추가 인터페이스:
FULL2WIRE (2): 2선 풀스텝 모터FULL3WIRE (3): 3선 풀스텝 (HDD 스핀들 등)HALF3WIRE (6): 3선 하프스텝대부분은 DRIVER 또는 HALF4WIRE를 쓰는데, 반드시 모터 데이터시트를 확인해야 합니다. 핀 개수나 순서가 맞지 않으면 모터가 경련하거나 역방향으로 돕니다.
// 절대 위치로 이동 (가장 자주 쓰는 함수)
stepper.moveTo(2048); // 2048 스텝 위치로 이동
// 상대적으로 이동
stepper.move(100); // 현재 위치에서 100스텝만큼 더 이동
// loop()에서 주기적으로 호출 (핵심!)
stepper.run(); // 한 스텝 실행, 가속 프로파일 자동 적용
// 목표 위치에 도착했는지 확인
if (stepper.distanceToGo() == 0) {
Serial.println("도착!");
stepper.moveTo(0); // 다시 처음 위치로 이동
}
// 현재 위치 확인
long pos = stepper.currentPosition();
// 현재 동작 중인지 확인
if (stepper.isRunning()) {
Serial.println("움직이는 중...");
}
전 28BYJ-48 모터를 풀스텝으로 사용했습니다.
const int STEP_INT1 = 2;
const int STEP_INT2 = 3;
const int STEP_INT3 = 5;
const int STEP_INT4 = 6;
AccelStepper stepper(AccelStepper::FULL4WIRE, STEP_INT1, STEP_INT3, STEP_INT2, STEP_INT4);
void setup() {
Serial.begin(9600);
stepper.setMaxSpeed(1000); // 최대 속도
stepper.setAcceleration(500); // 가속도
stepper.setCurrentPosition(0); // 처음 위치 설정
}
void loop() {
// 도착하면 다음 목표 지정
if (stepper.distanceToGo() == 0) {
static int target = 2048;
stepper.moveTo(target);
target = (target == 2048) ? 0 : 2048; // 왕복
}
// 핵심: loop()에서 계속 호출
stepper.run();
// 모터가 움직이면서 시리얼 출력
Serial.println(stepper.currentPosition());
delay(10);
}
반드시 run()을 loop()에서 호출하세요
run()을 계속 호출해야 움직입니다.데이터시트 확인은 필수입니다
runToPosition()은 블로킹입니다
moveTo() + run()을 써야 논블로킹 방식으로 동작합니다.가속/감속 값 조절
Stepper 라이브러리는 간단한 프로젝트에는 충분하지만, 프로젝트가 조금만 복잡해지면 한계가 보입니다. AccelStepper는 처음엔 조금 더 복잡해 보이지만, 일단 익숙해지면 훨씬 더 강력하고 안정적입니다.
혹시 라이브러리 선택으로 고민하고 있다면, 그냥 AccelStepper로 시작하는 것을 추천합니다. Stepper 라이브러리와 크게 다르지도 않으니까요!