4축 로봇 팔 제작 보고서
1. 제작 동기
로봇과 자동화 기계 시스템에 관심이 많아, 4축 움직임이 가능한 로봇 팔을 직접 제작해 보고 싶었다. 다양한 모터를 활용해 각 축의 움직임을 구현하고, 서보모터와 DC모터의 제어 방식을 비교하며 구조와 동작 원리를 실습해보기 위해 이 프로젝트를 진행하게 되었다.
2. 준비물
•
DC모터 1개 (회전축용)
•
서보모터 3개 (SG90)
◦
집게용 1개
◦
전후 움직임용 1개
◦
상하 움직임용 1개
•
나무막대 (아이스크림 스틱)
•
폼보드
•
볼트와 너트
•
플라스틱 기어
•
글루건
•
점퍼선
•
아두이노 UNO (또는 호환 보드)
•
모터 드라이버 (L298N 등)
3. 제작 과정
① 설계
4축 움직임을 구현하기 위해 아래와 같은 구조로 설계하였다
구조 특징
모터종류 | 역할 | 위치 |
Dc모터 | 회전축(좌우회전) | 베이스(하단) |
서보모터 | 집게 열고 닫기 | 집게 하단부 |
서보모터 | 상하 관절 움직이기 | 링크 센터 |
서보모터 | 전후 관절 움직이기 | 링크센터 |
•
DC모터는 모터 드라이버를 통해 회전방향 제어 가능
•
서보모터는 각도 지정으로 정밀한 위치 이동
•
레버형 링크 구조로 서보모터와 팔을 연결해 자연스러운 움직임 구현
•
집게(그리퍼)는 기어 구조와 서보모터로 열고 닫기 가능
② 조립
•
폼보드에 회전축 DC모터 고정
•
나무막대에 구멍을 뚫어 볼트-너트로 링크 연결
•
서보모터 3개를 각각 전후, 상하, 집게 위치에 고정
•
플라스틱 기어를 이용해 집게와 레버형 링크 구조 완성
③ 회로 연결
•
서보모터 3개 → 아두이노 D핀
•
DC모터 1개 → 모터 드라이버(L298N) → 아두이노
•
전원 및 GND 연결
4. 결과
•
4축 모두 정상 작동
◦
DC모터를 이용해 베이스가 좌우 회전
◦
서보모터로 팔의 전후, 상하 이동 가능
◦
서보모터로 집게를 열고 닫아 물체를 잡고 놓을 수 있음
•
물체를 집고 회전시켜 원하는 위치로 옮길 수 있음
•
링크 구조 덕분에 움직임이 자연스럽고 부드러움
5. 느낀점
이번 제작을 통해 서보모터와 DC모터의 제어 방식 차이와 다축 링크 구조의 움직임 원리를 직접 체험할 수 있어 유익했다. 특히 DC모터의 속도 제어와 서보모터의 각도 제어 방식을 활용해 다양한 움직임을 구현할 수 있었던 점이 재미있었다. 다음에는 센서를 활용해 자동 동작이 가능한 시스템으로 업그레이드하거나 서보모터의 개수를 늘려 6축 이상의 로봇 팔을 설계해보고 싶다.
# 사용자 설정 영역
BT_NAME = "mb" # 블루투스 출력 이름 바꿔서 쓰길 권장, 영문 8글자 이내
PIN_SERVO1 = 16 # 360° 서보 #1 (GP16)
PIN_SERVO2 = 17 # 360° 서보 #2 (GP17)
PIN_SERVO3 = 14 # 180° 서보 #3 (GP14)
PIN_SERVO4 = 15 # 180° 서보 #4 (GP15)
PIN_EXT_LED = 1 # 외부 LED 핀
PIN_RGB_RED = 19 # RGB LED Red 핀
PIN_RGB_GREEN = 20 # RGB LED Green 핀
PIN_RGB_BLUE = 21 # RGB LED Blue 핀
from machine import Pin, PWM
from micropython import const
import bluetooth
import struct
import time
# ========== BLE Advertising 관련 코드 ==========
_ADV_TYPE_FLAGS = const(0x01)
_ADV_TYPE_NAME = const(0x09)
_ADV_TYPE_UUID16_COMPLETE = const(0x3)
_ADV_TYPE_UUID32_COMPLETE = const(0x5)
_ADV_TYPE_UUID128_COMPLETE = const(0x7)
_ADV_TYPE_UUID16_MORE = const(0x2)
_ADV_TYPE_UUID32_MORE = const(0x4)
_ADV_TYPE_UUID128_MORE = const(0x6)
_ADV_TYPE_APPEARANCE = const(0x19)
def advertising_payload(limited_disc=False, br_edr=False, name=None, services=None, appearance=0):
payload = bytearray()
def _append(adv_type, value):
nonlocal payload
payload += struct.pack("BB", len(value) + 1, adv_type) + value
_append(_ADV_TYPE_FLAGS,
struct.pack("B", (0x01 if limited_disc else 0x02) + (0x18 if br_edr else 0x04)))
if name:
_append(_ADV_TYPE_NAME, name)
if services:
for uuid in services:
b = bytes(uuid)
if len(b) == 2:
_append(_ADV_TYPE_UUID16_COMPLETE, b)
elif len(b) == 4:
_append(_ADV_TYPE_UUID32_COMPLETE, b)
elif len(b) == 16:
_append(_ADV_TYPE_UUID128_COMPLETE, b)
if appearance:
_append(_ADV_TYPE_APPEARANCE, struct.pack("<h", appearance))
return payload
def decode_field(payload, adv_type):
i, result = 0, []
while i + 1 < len(payload):
if payload[i + 1] == adv_type:
result.append(payload[i + 2 : i + payload[i] + 1])
i += 1 + payload[i]
return result
def decode_name(payload):
n = decode_field(payload, _ADV_TYPE_NAME)
return str(n[0], "utf-8") if n else ""
def decode_services(payload):
services = []
for u in decode_field(payload, _ADV_TYPE_UUID16_COMPLETE):
services.append(bluetooth.UUID(struct.unpack("<h", u)[0]))
for u in decode_field(payload, _ADV_TYPE_UUID32_COMPLETE):
services.append(bluetooth.UUID(struct.unpack("<d", u)[0]))
for u in decode_field(payload, _ADV_TYPE_UUID128_COMPLETE):
services.append(bluetooth.UUID(u))
return services
# ========== BLE Simple Peripheral 관련 코드 ==========
_IRQ_CENTRAL_CONNECT = const(1)
_IRQ_CENTRAL_DISCONNECT = const(2)
_IRQ_GATTS_WRITE = const(3)
_FLAG_READ = const(0x0002)
_FLAG_WRITE_NO_RESPONSE = const(0x0004)
_FLAG_WRITE = const(0x0008)
_FLAG_NOTIFY = const(0x0010)
_UART_UUID = bluetooth.UUID("6E400001-B5A3-F393-E0A9-E50E24DCCA9E")
_UART_TX = (bluetooth.UUID("6E400003-B5A3-F393-E0A9-E50E24DCCA9E"),
_FLAG_READ | _FLAG_NOTIFY)
_UART_RX = (bluetooth.UUID("6E400002-B5A3-F393-E0A9-E50E24DCCA9E"),
_FLAG_WRITE | _FLAG_WRITE_NO_RESPONSE)
_UART_SERVICE = (_UART_UUID, (_UART_TX, _UART_RX),)
class BLESimplePeripheral:
def __init__(self, ble, name=BT_NAME):
self._ble = ble
self._ble.active(True)
self._ble.irq(self._irq)
((self._handle_tx, self._handle_rx),) = self._ble.gatts_register_services((_UART_SERVICE,))
self._connections = set()
self._write_callback = None
self._payload = advertising_payload(name=name, services=[_UART_UUID])
self._advertise()
def _irq(self, event, data):
if event == _IRQ_CENTRAL_CONNECT:
conn, _, _ = data
print("New connection", conn)
self._connections.add(conn)
elif event == _IRQ_CENTRAL_DISCONNECT:
conn, _, _ = data
print("Disconnected", conn)
self._connections.remove(conn)
self._advertise()
elif event == _IRQ_GATTS_WRITE:
_, value_handle = data
value = self._ble.gatts_read(value_handle)
if value_handle == self._handle_rx and self._write_callback:
self._write_callback(value)
def send(self, data):
for h in self._connections:
self._ble.gatts_notify(h, self._handle_tx, data)
def is_connected(self):
return bool(self._connections)
def _advertise(self, interval_us=500000):
print("Starting advertising")
self._ble.gap_advertise(interval_us, adv_data=self._payload)
def on_write(self, callback):
self._write_callback = callback
# ========== 간단한 큐 클래스 ==========
class SimpleQueue:
def __init__(self, maxsize):
self.queue = []
self.maxsize = maxsize
def put(self, item):
if len(self.queue) < self.maxsize:
self.queue.append(item)
else:
raise OverflowError("Queue is full")
def get(self):
if self.queue:
return self.queue.pop(0)
raise IndexError("Queue is empty")
def empty(self):
return not self.queue
def full(self):
return len(self.queue) >= self.maxsize
# ========== 서보 설정 및 변수 ==========
servo1 = PWM(Pin(PIN_SERVO1), freq=50)
servo2 = PWM(Pin(PIN_SERVO2), freq=50)
min_duty = 1638 # CCW 최대
mid_duty = 4915 # 정지
max_duty = 8192 # CW 최대
speed = 0.5 # 0.0~1.0
state = '1' # '1'=정지, '2'=CW, '3'=CCW
def apply_servo():
if state == '2':
d = int(mid_duty + speed * (max_duty - mid_duty))
elif state == '3':
d = int(mid_duty - speed * (mid_duty - min_duty))
else:
d = mid_duty
servo1.duty_u16(d)
servo2.duty_u16(d)
servo3 = PWM(Pin(PIN_SERVO3), freq=50)
servo4 = PWM(Pin(PIN_SERVO4), freq=50)
servo3_angle = 90
servo4_angle = 90
def angle_to_duty(angle):
# 0°≈1ms(3277), 180°≈2ms(6553)
return int(3277 + (angle / 180) * (6553 - 3277))
def handle_servo3_left():
global servo3_angle
servo3_angle = max(0, servo3_angle - 10)
servo3.duty_u16(angle_to_duty(servo3_angle))
print(f"Servo3 ← {servo3_angle}°")
def handle_servo3_right():
global servo3_angle
servo3_angle = min(180, servo3_angle + 10)
servo3.duty_u16(angle_to_duty(servo3_angle))
print(f"Servo3 → {servo3_angle}°")
def handle_servo4_up():
global servo4_angle
servo4_angle = min(180, servo4_angle + 10)
servo4.duty_u16(angle_to_duty(servo4_angle))
print(f"Servo4 ↑ {servo4_angle}°")
def handle_servo4_down():
global servo4_angle
servo4_angle = max(0, servo4_angle - 10)
servo4.duty_u16(angle_to_duty(servo4_angle))
print(f"Servo4 ↓ {servo4_angle}°")
# ========== LED 설정 ==========
onboard_led = Pin("LED", Pin.OUT)
external_led = Pin(PIN_EXT_LED, Pin.OUT)
rgb_red = PWM(Pin(PIN_RGB_RED), freq=1000)
rgb_green = PWM(Pin(PIN_RGB_GREEN), freq=1000)
rgb_blue = PWM(Pin(PIN_RGB_BLUE), freq=1000)
led_blink_active = False
led_blink_state = False
last_blink_time = 0
# ========== 키 핸들러(1~9, a/d/w/s, x) ==========
def handle_key_1():
global state
state = '1'; apply_servo(); print("→ 360° 서보 정지")
def handle_key_2():
global state
state = '2'; apply_servo(); print(f"→ 360° 서보 CW ({speed:.1f})")
def handle_key_3():
global state
state = '3'; apply_servo(); print(f"→ 360° 서보 CCW ({speed:.1f})")
def handle_key_4():
global speed
old = speed; speed = max(0.0, speed - 0.1)
print(f"→ 속도 느리게: {old:.1f}→{speed:.1f}"); apply_servo()
def handle_key_5():
global speed
old = speed; speed = min(1.0, speed + 0.1)
print(f"→ 속도 빠르게: {old:.1f}→{speed:.1f}"); apply_servo()
def handle_key_6():
global led_blink_active
led_blink_active = not led_blink_active
if not led_blink_active: onboard_led.off()
print(f"내부 LED 깜빡이기: {'ON' if led_blink_active else 'OFF'}")
def handle_key_7():
rgb_red.duty_u16(65535); rgb_green.duty_u16(0); rgb_blue.duty_u16(0)
print("RGB LED: Red")
def handle_key_8():
rgb_red.duty_u16(0); rgb_green.duty_u16(65535); rgb_blue.duty_u16(0)
print("RGB LED: Green")
def handle_key_9():
rgb_red.duty_u16(0); rgb_green.duty_u16(0); rgb_blue.duty_u16(65535)
print("RGB LED: Blue")
def handle_key_a(): handle_servo3_left()
def handle_key_d(): handle_servo3_right()
def handle_key_w(): handle_servo4_up()
def handle_key_s(): handle_servo4_down()
def handle_reset_all():
global state, speed, led_blink_active, led_blink_state
state, speed = '1', 0.5
apply_servo()
led_blink_active = False
led_blink_state = False
onboard_led.off(); external_led.off()
rgb_red.duty_u16(0); rgb_green.duty_u16(0); rgb_blue.duty_u16(0)
global servo3_angle, servo4_angle
servo3_angle = servo4_angle = 90
servo3.duty_u16(angle_to_duty(servo3_angle))
servo4.duty_u16(angle_to_duty(servo4_angle))
print("All systems reset")
# ========== 주기적 작업 처리 ==========
def handle_periodic_tasks():
global last_blink_time, led_blink_state
now = time.ticks_ms()
if led_blink_active and time.ticks_diff(now, last_blink_time) > 500:
led_blink_state = not led_blink_state
onboard_led.value(led_blink_state)
last_blink_time = now
# ========== 데이터 처리 & BLE 수신 핸들러 ==========
data_queue = SimpleQueue(10)
def process_data():
while not data_queue.empty():
data = data_queue.get()
cmd = chr(data[0])
if cmd == '1': handle_key_1()
elif cmd == '2': handle_key_2()
elif cmd == '3': handle_key_3()
elif cmd == '4': handle_key_4()
elif cmd == '5': handle_key_5()
elif cmd == '6': handle_key_6()
elif cmd == '7': handle_key_7()
elif cmd == '8': handle_key_8()
elif cmd == '9': handle_key_9()
elif cmd == 'a': handle_key_a()
elif cmd == 'd': handle_key_d()
elif cmd == 'w': handle_key_w()
elif cmd == 's': handle_key_s()
elif cmd == 'x': handle_reset_all()
else: print("Unknown cmd:", cmd)
def on_rx(data):
print("Received:", data)
if not data_queue.full():
data_queue.put(data)
# ========== 메인 ==========
ble = bluetooth.BLE()
sp = BLESimplePeripheral(ble)
handle_reset_all()
print("""
=== Control Keys ===
360° 서보: 1=Stop 2=CW 3=CCW 4=Slow↓ 5=Fast↑
내부 LED Blink=6 RGB Red/Green/Blue=7/8/9
180° 서보: a=Servo3← d=Servo3→ w=Servo4↑ s=Servo4↓
x=Reset All
Waiting for connections...
""")
while True:
if sp.is_connected():
sp.on_write(on_rx)
process_data()
handle_periodic_tasks()
else:
time.sleep(0.1)
JavaScript
복사