[카테고리:] 펌웨어

  • 무선조종기 개발의 추억

    무선조종기 개발의 추억

    2022년 가을에 시작해 2025년 겨울까지 손에서 놓지 못한 물건이 있다. AGV 무선 조종기다. 펌웨어부터 회로, 케이스, 양산, 그리고 현장 A/S 까지 혼자 끌고 갔다. 잘 만든 이야기는 아니다. 다만 오래 붙들었던 이야기다.

    완성된 AGV 무선 조종기
    <그림 1> 최종 형태. 조이스틱, 펑션키 8개, OLED, 비상정지

    무엇을 만들려 했나

    구조는 단순하다. 세 덩어리가 줄지어 선다.

    구분내용
    조종기ATmega328P + 블루투스 모듈 + OLED + 리튬전지 3.7 V / 2,000 mA
    수신기STM32F429 + 이더넷 + 와이어 엔코더(I2C) + RS232 · CAN
    상위B&R PLC 가 TCP/IP 로 수신기에 커맨드를 내린다

    B&R 이 커맨드 10 을 내리면 조종기 데이터가, 11 을 내리면 와이어 엔코더의 길이와 각도가 올라온다. 사람이 조이스틱을 밀면 그 값이 블루투스를 타고 수신기로, 이더넷을 타고 PLC 로, 그리고 바퀴로 간다. 글로 쓰면 세 줄이다.

    클럭 하나에 사흘

    첫 벽은 부트로더였다. 외부 클럭을 8 MHz 로 쓰기로 해 놓고 퓨즈 비트를 맞추지 않으면 다운로드가 아예 안 된다. 어렵게 올렸더니 이번엔 시리얼 글자가 깨졌다.

    원인은 같은 자리였다. 보율(BPS)은 클럭을 기준으로 계산된다. MCU 는 멀쩡히 돌고 다운로드도 되는데 통신만 깨지니, 통신 코드만 붙들고 한참을 헤맸다. 퓨즈 비트는 잘못 건드리면 MCU 가 그대로 죽는다. 죽인 칩이 몇 개인지는 세지 않았다.

    방향

    PCB 가 처음 조립되어 왔다. 전원을 넣었는데 MCU 가 아예 동작하지 않았다. 하루를 꼬박 회로도와 대조한 끝에 찾은 원인은 IC 를 반대로 꽂은 것이었다.

    조종기 PCB
    <그림 2> 조종기 기판. 아래쪽이 펑션키 8개

    며칠 뒤에는 펑션키 여덟 개 중 일곱 개가 먹지 않았다. 디코더를 의심하며 코드를 뒤졌는데, 스위치를 거꾸로 조립한 것이었다. 그리고 그 디코더는 결국 내 설계 실수로 드러났다. 경우의 수를 잘못 세어 스위치 하나를 영영 쓸 수 없게 만들어 놓았다.

    그해 가을에 배운 것은 이것이다.

    코드를 의심하기 전에 방향을 보라.

    보이지 않는 것들

    조종기가 슬립 모드에 들어갔는데도 배터리가 줄었다. MCU 는 자고 있었지만 5 V 부스터와 3.3 V 레귤레이터가 깨어 있었다. 자는 척만 하고 있었던 셈이다. 전원단을 통째로 끊는 오토 파워 오프 회로를 새로 그렸다.

    배터리 잔량은 정밀 ADC 를 쓰지 않고 다섯 칸으로만 나눴다. 하드웨어를 늘리는 대신 쓰는 사람이 알아볼 정도면 충분하다고 봤다.

    수신기에서는 이더넷 클럭이 발진하지 않았다. 25 MHz 크리스털을 몇 번이나 바꿔 끼웠다. 원인은 LAN8742A 바닥면의 그라운드 처리를 빠뜨린 것이었다. 보이지 않는 면이었다. 같은 시기에 MAX3232 의 차지펌프 캐패시터도 하나 누락되어 있었다.

    LAN8742A 회로도
    <그림 3> 발목을 잡았던 이더넷 PHY 회로

    스위치를 지우다

    초기 설계에는 페어링할 AGV 주소를 고르는 로터리 딥 스위치가 있었다. 그런데 1번부터 9번까지밖에 못 고른다. 대수를 늘리려면 하드웨어를 늘려야 했다.

    결국 스위치를 지우고 화면으로 옮겼다. 오른쪽 버튼 두 개를 3초 누르면 주소 선택 화면이 뜨고, 조이스틱을 위아래로 움직여 고른 뒤 다시 두 버튼을 누르면 끝난다. 부품은 하나 줄었고 고를 수 있는 대수는 늘었다.

    비슷한 판단이 하나 더 있었다. MCU 를 32 KB 짜리에서 128 KB 짜리로 올렸다. 값 차이는 천 원이었는데, 메모리가 빠듯해 한글을 화면에 띄우기조차 어려웠기 때문이다. 양산용 부품을 고를 때는 가격과 스펙만 보지 않았다. 국내에 물량이 있는가, 몇 년 뒤에도 구할 수 있는가가 더 중요했다. 성능이 가장 좋은 안을 두고 수급이 안정된 안을 택했다.

    하드웨어로 풀던 것을 펌웨어로 옮기면 대개 이런 일이 생긴다. 원가는 줄고 유연해진다. 대신 코드가 늘고, 늘어난 코드는 언젠가 나에게 돌아온다.

    수신기가 죽었다

    2023년 4월, 현장에서 수신기 CPU 가 죽어서 돌아왔다. 왜 죽었는지 알 수 없었다. 회로도를 들고 전자과 교수님을 찾아갔다.

    답은 전원 입력단에 있었다. 24 V 에서 5 V, 다시 3.3 V 로 내려가는 구간의 전압 마진이 너무 얇아 역으로 전류가 흐를 수 있다는 것이었다. 입력단에 전류 제한 코일을 넣으라는 조언을 받았다. 전류의 변화량이 클 때 그 변화를 눌러 주는 역할이다.

    그 나이에 회로도를 들고 학교를 찾아가는 일이 부끄럽지 않았다고는 못 하겠다. 다만 모르는 걸 모른다고 말하지 않으면, 죽은 CPU 는 계속 돌아온다.

    6천 원

    원가를 줄이려고 블루투스 안테나를 딥 타입(14,000원)에서 SMD 타입(6,000원)으로 바꿨다. 2호기의 신호가 유난히 약했다. 스마트폰 앱으로 재 보니 1호기는 −50 dB, 2호기는 그에 한참 못 미쳤다.

    제조사에 물으니 답이 한 줄이었다. 안테나 반경 5 mm 안에는 어떤 부품도 있으면 안 된다. 이미 배치를 끝낸 기판이었다. 8,000원을 아끼려다 결국 딥 타입으로 되돌렸다.

    조종기 OLED 표시
    <그림 4> 페어링된 상태

    양산, 그리고 그 뒤

    2023년 8월에 50대를 양산했다. 그런데 송신기에서 또 블루투스가 말썽이었고, 원인은 다시 클럭 설정이었다. 1년 전에 배운 것을 1년 뒤에 다시 배웠다.

    그해 12월에는 군산에 내려가 7대를 백업했다. 이듬해 5월과 6월에는 수신기 소자가 연달아 나갔다. 7월에는 부품을 교체해 입고된 27대 중 8대가 불량이었다. 2025년 11월에도 나는 수신기 CAN 포트를 확인하고 있었다.

    끝난 적이 없는 물건이었다. 3년 동안 이 조종기는 내게 계속 돌아왔고, 돌아올 때마다 내가 놓친 것을 하나씩 들고 왔다. IC 방향, 그라운드 한 면, 캐패시터 하나, 안테나 옆 5 mm.

    그래도 지금 어딘가의 공장에서는 누군가 이 조종기를 쥐고 AGV 를 세우고 있을 것이다. 펑션키 여덟 개 중 하나는 내 계산 실수로 비어 있는 채로.

    그 비어 있는 자리가, 이상하게도 가장 오래 기억에 남는다.

  • Teensy 4.1 로 WS2812 LED Strip 구동하기

    Teensy 4.1 로 WS2812 LED Strip 구동하기

    Teensy 4.1 로 WS2812(네오픽셀) LED 링을 구동하는 기본기를 정리한다. 개발환경 설정부터 R·G·B 패턴 코드까지 훑는다.

    1. Teensy 4.1 개발환경 설정

    Arduino IDE 대신 VSCode + PlatformIO 조합을 쓴다. 자동완성과 헤더 추적이 제대로 되고, 프로젝트마다 라이브러리 버전이 독립적으로 관리된다.

    VSCode 를 설치한 뒤 확장 세 가지를 넣는다.

    • PlatformIO IDE — 빌드·업로드·라이브러리 관리를 전담한다
    • C/C++ — 코드 탐색과 자동완성
    • Arduino — 아두이노 문법 보조
    VSCode 확장 마켓플레이스에서 PlatformIO IDE 설치
    <그림 1> 확장 탭에서 PlatformIO IDE 를 검색해 설치한다

    프로젝트 루트의 platformio.ini 가 설정의 전부다.

    [env:teensy41]
    platform      = teensy
    board         = teensy41
    framework     = arduino
    monitor_speed = 115200
    

    화면 아래 상태 표시줄의 체크 표시가 빌드, 화살표가 업로드다. Teensy 는 업로드 시 Teensy Loader 가 자동으로 뜬다.

    ★주) 시리얼 모니터에 글자가 깨져 나오면 platformio.ini 의 monitor_speed 와 코드의 Serial.begin() 속도가 어긋난 것이다. J-Tag 없이 디버깅할 때는 결국 Serial.print() 가 주력이므로 이 둘을 반드시 맞춰 둔다.

    Teensy 4.1 보드
    <그림 2> Teensy 4.1. NXP i.MX RT1062(600 MHz)에 SD 슬롯을 갖췄다

    2. LED Strip 기본 개념

    흔히 네오픽셀이라 부르는 WS2812B 는 LED 안에 제어 IC 가 들어 있는 소자다. 그래서 선이 세 가닥뿐이다 — 5 V, GND, DATA.

    12개 WS2812 LED 링
    <그림 3> 12구 링타입 LED. 빨강 5 V, 검정 GND, 노랑 DATA 세 가닥이 전부다

    데이터는 한 줄로 흘러간다. 첫 픽셀이 앞의 24 비트(GRB 각 8 비트)를 먹고, 나머지를 다음 픽셀로 흘려보낸다. 그래서 픽셀 수만큼 연달아 보내면 각자 제 몫을 가져간다. 주의할 점은 순서가 RGB 가 아니라 GRB 라는 것이다.

    <표 1> 배선 시 확인할 것

    항목내용
    전류픽셀당 최대 약 60 mA(백색 최대 밝기). 12구면 0.7 A 남짓이므로 USB 로 가능하지만, 수십 개를 넘으면 별도 5 V 전원을 쓴다
    신호 레벨Teensy 출력은 3.3 V, WS2812 는 5 V 기준 약 3.5 V 이상을 요구한다. 대개는 동작하지만 경계선이므로 레벨 시프터를 넣는 편이 안전하다
    보호 소자DATA 선에 300~500 Ω 직렬 저항, 전원단에 1000 µF 커패시터
    접지별도 전원을 쓸 때 GND 를 반드시 공통으로 묶는다

    3. Teensy 4.1 네오픽셀 라이브러리

    WS2812 는 타이밍이 빡빡해서 보통 라이브러리는 전송 중 인터럽트를 막는다. 모터 제어나 통신을 함께 하면 그 순간 놓치는 것이 생긴다.

    Teensy 에는 이를 피하는 WS2812Serial 이 있다. 하드웨어 시리얼과 DMA 로 파형을 내보내므로 CPU 를 붙잡지 않는다. 대신 시리얼 TX 로 쓸 수 있는 핀에서만 동작하니, 라이브러리 예제의 핀 목록을 확인하고 배선한다.

    라이브러리특징
    WS2812SerialTeensy 전용. DMA 비차단. 핀 제약 있음 — 권장
    Adafruit_NeoPixel어느 보드에서나 동작. 전송 중 인터럽트 차단
    FastLED색 보정·패턴 함수가 풍부. Teensy 에서 WS2812Serial 을 백엔드로 쓸 수 있다

    4. R·G·B 패턴 코드

    12구 링을 빨강 → 초록 → 파랑 순으로 한 칸씩 채우는 예제다.

    #include <WS2812Serial.h>
    
    const int numled = 12;
    const int pin    = 1;          // 시리얼 TX 로 쓸 수 있는 핀
    
    byte drawingMemory[numled * 3];         // 작업용
    DMAMEM byte displayMemory[numled * 12]; // DMA 전송용
    
    WS2812Serial leds(numled, displayMemory, drawingMemory, pin, WS2812_GRB);
    
    #define RED   0xFF0000
    #define GREEN 0x00FF00
    #define BLUE  0x0000FF
    #define OFF   0x000000
    
    void setup() {
      leds.begin();
      leds.setBrightness(60);      // 0~255. 눈이 부시고 전류도 크므로 낮춰 둔다
    }
    
    // 한 칸씩 차오르게 칠한다
    void colorWipe(int color, int wait_ms) {
      for (int i = 0; i < leds.numPixels(); i++) {
        leds.setPixel(i, color);
        leds.show();               // 여기서 DMA 전송이 시작된다
        delay(wait_ms);
      }
    }
    
    void loop() {
      colorWipe(RED,   60);
      colorWipe(GREEN, 60);
      colorWipe(BLUE,  60);
      colorWipe(OFF,   60);
    }
    

    setPixel() 은 버퍼에 쓰기만 하고 실제 출력은 show() 에서 일어난다. 여러 픽셀을 바꿀 때는 전부 써 넣은 뒤 show() 를 한 번만 부르는 것이 맞다. 위 예제는 차오르는 모습을 보이려 일부러 매번 호출했다.

    밝기는 setBrightness() 로 낮춰 쓴다. 12구를 백색 최대로 켜면 0.7 A 가 흐르고 눈으로도 지나치게 밝다. 실제 장비에서는 60 안팎이 적당했다.

    다음에는 링을 회전시키는 패턴과, 상위 제어기에서 패턴 번호를 받아 바꾸는 구조를 다룬다.

어제 방문자 : 3 명오늘 방문자 : 1 명전체 방문자 : 584 명