안녕하세요! 오늘은 작지만 강력한 ESP32-C3 Mini 보드를 사용해서 스텝모터를 돌려보겠습니다. 일반 아두이노보다 성능이 좋고 크기가 작아 실제 전시물 제작에 아주 유리한 보드죠.
최근 제가 제작하고 있는 ‘음악 나오는 전시용 회전판(턴테이블)’ 프로젝트에서도 이 스텝모터가 핵심적인 역할을 담당하고 있는데요. 왜 일반 모터가 아닌 스텝모터를 사용했는지, 그 이유와 특징을 정리해 드립니다.
1. 스텝모터란?
일반적인 DC 모터는 전원을 연결하면 뱅글뱅글 계속 회전하지만, 스텝모터는 이름처럼 ‘스텝(Step, 단계)’을 밟듯이 딱 정해진 각도만큼만 움직이는 모터입니다.
예를 들어, “90도만 움직여!” 혹은 “한 바퀴를 200번에 나눠서 움직여!”라는 명령을 아주 정확하게 수행할 수 있습니다.
2. 스텝모터의 최대 장점
- 정밀한 제어: 각도를 아주 미세하게 조절할 수 있어 로봇 팔, 3D 프린터, 그리고 제가 만든 회전판처럼 정확한 위치에 멈춰야 하는 장치에 필수입니다.
- 강력한 홀딩 토크(Holding Torque): 특정 각도에서 멈춰 있을 때, 외부 힘에 의해 돌아가지 않도록 꽉 붙잡고 있는 힘이 강합니다.
- 반복 정밀도: 같은 명령을 내리면 언제나 똑같은 위치로 돌아옵니다.
3. 주요 활용 사례
- 3D 프린터: 노즐을 0.1mm 단위로 움직일 때 사용됩니다.
- CCTV 카메라: 원하는 방향으로 렌즈를 정확히 돌릴 때 쓰입니다.
- 전시용 턴테이블: 일정한 속도로 회전하거나, 특정 제품 위치에서 잠시 멈추는 연출을 할 때 최적입니다.
4. 사용법
준비물
- ESP32-C3 SuperMini (또는 Mini 보드)
- 28BYJ-48 스텝모터
- ULN2003 모터 드라이버
- 점퍼 와이어
28BYJ-48 스텝모터스텝모터를 제어할 때 ULN2003 드라이버를 사용하는데
- 전류 증폭 (강력한 근육 역할) ESP32-C3의 제어 신호는 너무 약해서 모터를 직접 돌릴 힘이 없습니다. 드라이버는 이 약한 신호를 받아 모터를 구동할 수 있는 큰 전류로 바꿔줍니다.
- 보드 보호 (방패 역할) 모터가 멈출 때 발생하는 고전압(역기전력)이 거꾸로 흘러 들어와 비싼 메인 보드가 타버리는 것을 방지합니다.
- 효율적인 전원 관리 보드는 3.3V 전압으로 신호만 보내고, 모터는 5V 이상의 전원을 따로 쓸 수 있게 해줌으로써 시스템의 안정성을 높여줍니다.
ESP32-C3 SuperMini 보드의 GPIO 1~4번 핀을 사용합니다.
| ULN2003 드라이버 | ESP32-C3 GPIO |
| IN1 | Pin 1 |
| IN2 | Pin 2 |
| IN3 | Pin 3 |
| IN4 | Pin 4 |
| VCC / GND | 5V / GND |

5. 사용 코드
step.py set_run(speed) 함수
# esp32-c3 1~4핀과 uln2003의 in1~in4 간에 연결
import machine
import time
import _thread
# 핀 설정 (IN1, IN2, IN3, IN4)
pins = [machine.Pin(i, machine.Pin.OUT) for i in [1, 2, 3, 4]]
# 8단계 Half-step 시퀀스
sequence = [
[1, 0, 0, 0], [1, 1, 0, 0], [0, 1, 0, 0], [0, 1, 1, 0],
[0, 0, 1, 0], [0, 0, 1, 1], [0, 0, 0, 1], [1, 0, 0, 1]
]
# 전역 변수: 현재 속도 저장 (0이면 정지)
current_speed = 0
stop = 0 # 1이 되면 모든 동작 중지
def motor_thread():
""" 백그라운드에서 실제로 모터를 돌리는 쓰레드 함수 """
global current_speed
step_idx = 0
while True:
if current_speed == 0:
# 정지 상태일 때 전원 차단 및 대기
for pin in pins:
pin.value(0)
time.sleep(0.1)
continue
# 방향 및 속도 계산
direction = 1 if current_speed > 0 else -1
delay = (11 - abs(current_speed)) * 0.002
# 스텝 이동
step_idx = (step_idx + direction) % 8
for i in range(4):
pins[i].value(sequence[step_idx][i])
time.sleep(delay)
def step_run(speed):
""" 한번 호출하면 전역 변수 값을 바꿔서 쓰레드가 계속 돌게 함 """
global current_speed
# 범위 제한 (-10 ~ 10)
if speed > 10: speed = 10
if speed < -10: speed = -10
current_speed = speed
print(f"모터 속도가 {current_speed}(으)로 설정되었습니다.")
def test():
""" 30초 정회전, 30초 역회전 무한 반복 """
global stop
stop = 0
print("전시용 반복 테스트 시작 (stop = 1 시 중단)")
# (속도, 유지시간) 쌍으로 리스트 구성
scenarios = [(8, 30), (-8, 30)]
try:
while not stop: # stop이 0인 동안만 작동
for speed, duration in scenarios:
if stop: break
print(f"{'정회전' if speed > 0 else '역회전'} 중 (Speed {speed})...")
step_run(speed)
# 1초마다 stop 여부를 체크하며 대기
for _ in range(duration):
if stop: break
time.sleep(1)
# 루프 탈출 시 정지
step_run(0)
print("테스트 모드 종료")
except KeyboardInterrupt:
stop = 1
step_run(0)
# 모터 쓰레드 시작 (프로그램 시작 시 한 번만 실행)
_thread.start_new_thread(motor_thread, ())
step_run(0) # 정지
if __name__ == "__main__" :
test()
# pcf8575 로 28byj-48 모터 4개를 제어 하려고 한다.
# 0~3, 4~7, 8~11, 12~15 씩 스텝모터 4개 핀에 각각 연결해놨다.
# scl 1번핀, sda 2번핀
# 스텝모터 구동하는 함수 motor(sp1, sp2,sp3,sp4) 로 스텝모터 4개의 속도를 지정
import machine
import time
# I2C 설정
i2c = machine.I2C(0, scl=machine.Pin(1), sda=machine.Pin(2), freq=1000000)
print(i2c.scan())
PCF8575_ADDR = 0x20
# 8스텝 패턴
STEP_SEQ = [0b1000, 0b1100, 0b0100, 0b0110, 0b0010, 0b0011, 0b0001, 0b1001]
#STEP_SEQ = [0b1000, 0b0100, 0b0010, 0b0001]
# 각 모터의 상태 저장
current_indices = [0, 0, 0, 0]
last_step_time = [0, 0, 0, 0]
def write_pcf8575(data16):
buf = bytearray(2)
buf[0] = data16 & 0xFF
buf[1] = (data16 >> 8) & 0xFF
i2c.writeto(PCF8575_ADDR, buf)
def stop_all():
"""모든 모터의 전류를 차단하여 정지 및 발열 방지"""
# PCF8575의 16개 핀(P0~P17)을 모두 0으로 설정
write_pcf8575(0x0000)
print("All motors stopped and powered down.")
def motor(sp1, sp2, sp3, sp4, duration_ms=30000):
"""
sp값이 클수록 빠름 (예: 1~100)
0이면 정지, 음수면 역방향
"""
speeds = [sp1, sp2, sp3, sp4]
# 속도를 주기(ms)로 변환 (속도 5는 속도 1보다 5배 짧은 대기시간)
# 0으로 나누기 방지를 위해 아주 작은 값 처리
intervals = []
for s in speeds:
if s == 0:
intervals.append(0)
else:
intervals.append(1000 // abs(s)) # 속도에 반비례하는 대기 시간(ms)
start_time = time.ticks_ms()
try:
# 테스트를 위해 2초간 구동하는 루프
while time.ticks_diff(time.ticks_ms(), start_time) < duration_ms:
now = time.ticks_ms()
changed = False
out_bits = 0
for i in range(4):
if speeds[i] != 0:
# 마지막 스텝 이동 후 지정된 인터벌이 지났는지 확인
if time.ticks_diff(now, last_step_time[i]) >= intervals[i]:
# 스텝 인덱스 업데이트
if speeds[i] > 0:
current_indices[i] = (current_indices[i] + 1) % 8
else:
current_indices[i] = (current_indices[i] - 1) % 8
last_step_time[i] = now
changed = True
# 현재 인덱스의 비트 패턴을 합침 (정지 상태면 마지막 패턴 유지 혹은 0)
if speeds[i] != 0:
out_bits |= (STEP_SEQ[current_indices[i]] << (i * 4))
# 상태가 변했을 때만 I2C 전송 (통신 부하 감소)
if changed:
write_pcf8575(out_bits)
# CPU 과열 방지 및 I2C 안정성을 위한 미세 딜레이
#time.sleep_us(1)
finally:
# 테스트 종료 후 또는 에러 발생 시 반드시 모든 모터 정지
stop_all()
# 실행 예시:
motor(-10, 0, 100, 100)



micro2iot에 답글 남기기 응답 취소