OpenWrt에 새 기기 지원 추가하기

※ 2021-09-05: v21.02.0 기준으로 업데이트

들어가기 전에

글의 목적

뭔가 그럴듯한 인트로를 적어보려고 했는데 적당한 표현이 떠오르지 않아 포기하기로 하고, 핵심만 말하자면 국내에 OpenWrt 개발자 공개 커뮤니티라고 할 만한 데가 없어(비공개 네이버 카페는 딥웹이라 보아야 한다고 생각합니다) 관련 정보를 찾기가 쉽지 않은 상황이 아쉬워서, 이런 튜토리얼 비스무리한 거라도 올려 경력있는 신입과 재야에 숨은 고수의 유입을 유도함으로써 저도 편하게 버스타고 가고 싶다는 사적인 소망을 이루고자 이 글을 썼다는 얘기입니다.

왜 OpenWrt?

OpenWrt는 유무선공유기, NAS, SBC와 같이 자원이 제약된 임베디드 시스템을 위한 리눅스 배포판입니다. 사용자마다 각각의 원하는 점이 있겠지만(무선 성능 개선, 원하는 패키지 설치, 보안 향상 등등) 개인적으로는 잘 정돈된 코드베이스와 개발 환경이라는 데에 큰 점수를 주고 싶습니다. 메인라인에 가까운 최신 버전 커널과 툴체인은 칩셋 제조사 SDK나 그에 의존하는 여타 커펌 프로젝트에선 기대하기 어려운 일이지요. 다른 오픈소스 프로젝트와 비교해봤을 때도 기능성과 복잡도 면에서 Buildroot와 Yocto 사이에 끼어있기에 적당한 타협안이 된다고 생각합니다.

또한 스마트폰, MP3, 키보드 등과 마찬가지로, 제조사 펌웨어에 발묶여있는 공유기에 오픈소스 커펌을 포팅하는 작업은 코더의 소일거리로도 적당하다고 봅니다.

글의 방향

disclaimer: 케이스를 열거나 커펌을 설치하면 제품 보증이 무효가 되고 자칫하면 기기가 손상될 수 있습니다.
이 글의 내용을 따라함으로써 (또는 잘못된 방식으로 활용함으로써) 생길 수 있는 결과에 대한 책임은 사용자 스스로에게 있습니다.

이 글에선 OpenWrt에 새 기기 지원을 추가하는 전반적인 과정(하드웨어 정보를 얻는 것부터 시작해서 업스트림에 패치를 보내는 데까지)을 다루고자 합니다.

이 글은 읽는 사람이 이미 리눅스에 익숙하고 기본적인 하드웨어 지식이 있다는 가정 하에 작성되었습니다.

이 글에서 설명하는 방법과 절차는 최선이 아닐 수 있습니다. 처음에야 일단 따라해보면서 익혀야겠지만, 어느 정도 익숙해지면 여기선 왜 이렇게 했을까, 더 좋은 방법은 없을까, 하고 고민하면서 스스로 개선점을 찾아보길 바랍니다.

예시로 사용할 기기는 제 첫 메인라이닝 모델이면서 하드웨어와 패치가 단순한 ipTIME A604M으로 선택했습니다.
OpenWrt 버전은 2021-09-04 시점에서 최신 안정 버전인 v21.02.0을 사용하겠습니다. 21.02 이후로 master 브랜치에서 많은 변화가 있었기 때문에 (DSA, nvmem-cells 등) 추후에 기기 지원을 넣으실 분들은 이 글은 참고만 하고 최신 버전에 맞게 작업하셔야 합니다.

사전조사

어떤 기기에 OpenWrt를 포팅해보자고 마음을 먹은 다음에 가장 먼저 해야할 일이 뭘까요? 그 기기에 OpenWrt를 올리는 것이 가능할지, 올린 다음에 실사용이 가능할지, 이미 포팅을 시도한 사례가 있는지 등을 먼저 확인하는 과정이 필요합니다. 이미 가지고 있는 기기라면 상관없지만, 덥썩 구매해놓고 나중 가서 올릴 수 없다는 걸 알게 되면 실망이 크잖아요.

일단 A604M의 하드웨어 스펙을 조사해봅시다.

  • SoC: MediaTek MT7628A (580MHz)
  • RAM: DDR2 64MB
  • 플래시: 8MB
  • 이더넷 포트: 100Mbps WAN x1, 100Mbps LAN x4
  • WiFi: 2.4GHz 2x2, 5GHz 2x2

제품소개에선 WiFi 칩셋으로 뭘 쓰는지까진 안 나오는데, 사실 이 정도만 해도 감지덕지고, 제조사에서 어떤 CPU를 쓰는지조차 알려주지 않는 경우도 흔합니다.

만약 기기의 내부 사진을 얻을 수 있다면 정보 수집에 큰 도움이 됩니다. 공유기 리뷰어 분들은 겉모습만 찍고 넘기지 말고 분해도 해서 내부 칩이 보이는 사진도 올려주시면 좋겠습니다…

해외/국제 모델의 경우엔 WikiDevifccid.io의 힘을 빌리는 것이 가능합니다. WikiDevi에는 공유기 디바이스 및 칩셋 정보가 모여있고, fccid.io에서는 FCC ID가 있는 제품의 내부 사진을 열람할 수 있습니다.

또한 펌웨어 바이너리소스코드도 좋은 정보원이 됩니다.

다시 본론으로 돌아가면, MT7628A는 OpenWrt에서 지원하는 SoC이고 ramips/mt76x8 타겟에 해당합니다.
현재 OpenWrt가 지원하는 플랫폼 중 인기 있는 몇 개를 살펴보자면 다음과 같습니다.

  • ar71xx, ath79: (MIPS 기반) Atheros SoC를 사용합니다. 전자는 mach.c를 사용하고 후자는 DTS를 사용합니다.[1]
  • ipq40xx, ipq806x: (ARM 기반) Qualcomm Atheros SoC를 사용합니다.
  • bcm47xx, bcm53xx: Broadcom SoC를 사용합니다. 전자는 MIPS 아키텍처고 후자는 ARM 아키텍처입니다.
  • ramips, mediatek: Ralink/MediaTek SoC를 사용합니다. 전자는 MIPS 아키텍처고 후자는 ARM 아키텍처입니다.
  • kirkwood, mvebu: Marvell SoC를 사용합니다. 이 계열은 공유기뿐만 아니라 NAS 기기에도 많이 사용됩니다.

Lexra 기반의 Realtek SoC는 지원되지 않습니다. 다만 MIPS 기반 RTL838x 계열은 초기 지원이 추가된 상태입니다.

이 중 ramips 타겟은 지원이 잘 되고 있는 상태고, 지원하는 기종도 많은 걸 봤을 때, A604M 포팅에 SoC가 걸림돌이 되진 않을 것으로 보입니다. 또한 OpenWrt의 최소 요구 사양으로 여겨지는 8/64 조건(플래시 8MB 이상, RAM 64MB 이상)도 만족하고 있습니다.

위에 링크를 달아놓은 내부 사진을 잘 살펴보면 보드에 UART로 추측되는 4핀 핀헤더가 달려있는 게 보입니다. 시리얼 콘솔은 디바이스 정보를 얻고 디버깅을 하는 데 필수적이고, 이게 없으면 이후 진행이 매우 골치아파지기 때문에, UART 핀의 존재 여부 정도는 미리 확인해두는 게 좋습니다.

하드웨어 및 펌웨어 조사

사전조사에서 OpenWrt를 올릴 수 있을 것 같다는 희망이 보였다면 이제 본격적인 조사 작업에 들어가봅시다.

기기 아랫면의 고무 패킹을 빼고 나사를 풀어줍니다.

제가 분해에 능하지 못해 그러는 걸진 모르겠지만, 남들은 잘만 분해된다는데 나는 아무리 힘을 줘도 뚜껑이 안 열리면 울고 싶어집니다. 노하우를 안다고 해서 분해가 반드시 쉬워지는 건 아니지만, 적어도 걸쇠의 위치라도 안다면 공략이 한결 수월해진다고 생각합니다.

다행히 이 케이스는 별로 걸리적거릴 게 없어서 쉽게쉽게 열립니다.

분해 후 내부 칩의 부품번호를 확인합니다.

  • SoC: MediaTek MT7628AN (MIPS 24KEc 580MHz, bgn 2x2)
  • RAM: ESMT M14D5121632A (DDR2 64MB)
  • 플래시: cFeon QH64-104HIP (SPI NOR 8MB, Eon EN25QH64와 동일한 모델로 보임)
  • WiFi 5GHz: MediaTek MT7612EN (abgn+ac 2x2)

각 부품의 데이터시트도 (찾을 수 있으면) 구해둡니다.

이제 시리얼 포트에 접속해봅니다. 원랜 멀티미터로 전압과 핀아웃을 먼저 확인하는 것이 정석이지만, 프로브 대고 있는 사진을 찍기 귀찮아서 생략하겠습니다. 다행히 A604M은 포트 옆에 핀아웃이 친절하게 적혀있어서 따로 찾아볼 필요가 없겠네요.

잠시 보충설명을 하자면,

  • 3~5핀 포트가 있으면 십중팔구 UART이고, 10~20핀 포트가 있으면 JTAG일 가능성이 높습니다.
  • 대부분의 공유기 SoC는 TTL 전압이 3.3V입니다. 하지만 IPQ806X 같은 예외도 있습니다.
  • GND, RXD, TXD 핀만 연결하면 됩니다. VCC는 (보드에 이미 전원을 넣고 있다면) 불필요할뿐더러 괜한 전압 충돌만 생길 수 있으니 꽂지 마세요.
  • 경험상 Atheros SoC를 사용하는 공유기에서 UART 연결문제가 발생할 때가 많았습니다.(시리얼 어댑터를 연결하고 전원을 넣으면 부팅이 안 된다든가, RX는 되는데 TX가 안 된다든가) 뭔가 잘 안 되면 USB-UART 어댑터를 다른 걸로 바꿔보세요.

UART 보율은 어떻게 알아낼까요? 이것도 무작정 시도해보는 방법도 있지만, 보통 SoC별로 많이 쓰는 값이 있기 때문에(일반적으로 115200, ramips는 57600, realtek은 38400) 그것부터 시도해보면 좋습니다. A604M은 57600에 걸렸습니다.

전원 넣고서 부팅이 완료될 때까지의 전체 로그를 한 번은 수집해야 합니다.

ipTIME 공유기는 부팅이 끝난 다음에 별다른 쉘을 띄워주지 않습니다.

부팅 로그 첫 부분을 보면 부트로더에서 선택지가 뜨는 것을 볼 수 있습니다.

WeVO나 netis 공유기에선 부트로더 쉘도 열 수 있는데 ipTIME ramips 기기에선 TFTP 복구모드밖에 쓸 수 없습니다.

이 복구모드는 원래 펌웨어 업데이트에 실패했을 때 부트로더에서 자동으로 진입하는 모드입니다. 딱히 숨겨진 기능이 아니기 때문에 ipTIME 홈페이지에 사용법 설명글도 올라와 있습니다.

t를 눌러 복구 모드에 들어가면 공유기는 TFTP 서버를 열어줍니다. 컴퓨터에서 고정 IP로 이더넷 연결 후(LAN 포트에 꽂아야 합니다) TFTP 클라이언트를 실행합니다.

일단 기능 테스트를 위해 정펌을 올려봤는데 잘 되네요.

처음 써보는 기기라면 관리자 웹페이지도 둘러보고 LED는 언제 불이 들어오는지도 살펴보고 하면서 기능을 더 탐색해야겠지만, ipTIME 공유기 기능은 다들 아시리라 믿고 넘어가겠습니다.

빌드 및 부팅

이제 OpenWrt 펌웨어를 빌드해봅시다. v21.02.0엔 제가 이미 메인라이닝한 A604M 지원 패치가 들어가 있는데, 이 글에선 없다고 가정하고 진행하겠습니다.

빌드 환경으론 Ubuntu 20.04를 사용하겠습니다. RAM은 4GB, 여유 저장공간은 10GB 이상 있어야 빌드가 수월할 겁니다.

일단 OpenWrt에서 이미 지원하는 mt76x8 디바이스 중 A604M과 사양이 대략 비슷해보이는 Cudy WR1000의 펌웨어 이미지를 빌드하겠습니다. 이는 디바이스에서 OpenWrt 이미지를 정상적으로 부팅하는 것이 가능한지 테스트해보려는 목적이고, 나중에 제대로 전용 이미지를 만들 겁니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
## 패키지 설치 및 저장소 클론
sudo apt-get install build-essential libncurses-dev gawk unzip wget python3 git file rsync
git clone https://github.com/openwrt/openwrt.git
cd openwrt
## 기존 패치 삭제
git switch -c dev-a604m v21.02.0
wget https://mans0n.github.io/2020/08/22/openwrt-add-new-device/v21.02.0-pre-a604m.patch
git am v21.02.0-pre-a604m.patch

## 외부 패키지 가져오기, 한 번만 하면 됨
./scripts/feeds update -a
./scripts/feeds install -a
## 빌드 타겟 설정
make menuconfig
## "Target System" -> "MediaTek Ralink MIPS"
## "Subtarget" -> "MT76x8 based boards"
## "Target Profile" -> "Cudy WR1000"
## 빌드 중간에 실패할 때를 대비해 CONFIG_BUILD_LOG도 활성화시켜두면 좋음
## "Advanced configuration options (for developers)" -> "Enable log files during build process"
make defconfig
# ./scripts/diffconfig.sh

## 소스파일 다운로드, 한 번만 하면 됨
make download -j3
## 빌드 시작, (CPU 스레드 수 + 1)개의 프로세스로 병렬처리
make -j$((`nproc` + 1))

처음 빌드할 땐 툴체인까지 빌드해야 하기 때문에 시간이 좀 많이 걸립니다. 컴퓨터 성능이 좋다면 30분만에 끝날 수도 있고, 별로라면 한두 시간 걸릴 수도 있습니다.

참고문서:

  • [OpenWrt wiki] Quick Image Building Guide
  • [OpenWrt wiki] Build system usage

빌드 후 새로 생기는 디렉토리 몇 개를 살펴보면 다음과 같습니다.

  • bin: 빌드 결과물인 펌웨어 이미지와 패키지가 저장되는 장소
  • build_dir: 소스코드가 빌드되는 장소 (만약 저장공간이 부족해진다면 build_dir/toolchain-*부터 삭제하세요)
  • dl: 다운로드 받은 소스 압축 파일이 저장되는 장소
  • staging_dir: 빌드한 툴체인 바이너리나 라이브러리 헤더 등이 저장되는 장소

설치에 사용할 이미지는 bin/targets/ramips/mt76x8/ 아래에 있습니다.

참고로 initramfs와 squashfs 이미지의 차이점을 알아보자면,

  • initramfs: 커널과 rootfs가 모두 포함된 이미지입니다. rootfs는 RAM에만 존재하고, 모든 파일 변경사항은 시스템 종료시 사라집니다. U-Boot의 tftpboot 명령어를 쓰면 RAM에 이미지를 바로 올리고 부팅하는 것이 가능합니다. 보통 디버깅용으로 많이 쓰지만, 원한다면 실사용 목적으로 쓸 수도 있습니다.
  • squashfs: 커널과 읽기전용 rootfs(squashfs)와 읽고쓰기 가능한 추가 파일시스템(jffs2를 overlayfs로 사용)이 담긴 이미지입니다.[2] 모든 파일 변경사항은 이 overlayfs에 저장되어 플래시에 기록됩니다. 만약 리셋을 한다면 오버레이만 날려버려서 초기상태로 되돌아갑니다.

이제부터 하려는 건 정펌 상태에서 커펌을 올릴 수 있는 길을 찾는 것입니다. 가장 확실한 방법은 플래시를 보드에서 떼내서 커펌을 기록하고 다시 보드에 붙이는 방법이지만, 보통은 그렇게까지 하고 싶진 않죠. 통상적으론 다음의 셋 중에 하나를 얻으면 됩니다.

  • 루트 권한의 유닉스 쉘
  • 부트로더 쉘
  • 원하는 코드를 실행시킬 수 있는 하드웨어 인터페이스 (JTAG 등)

A604M은 이 중에 해당하는 항목이 없지만, 대신 부트로더에서 진입할 수 있는 복구모드가 있었죠.

복구모드도 바보는 아닐 테니 아무 이미지나 막 받아들이진 않을 거고, OpenWrt 이미지를 정펌 포맷에 맞춰줄 필요가 있을 겁니다.

우선 정펌 포맷을 살펴보겠습니다.

uImage 헤더가 앞에 달려있고, 커널과 rootfs가 뒤따라오는 구조입니다. hexdump로 살펴봐도 제조사 헤더나 꼬리가 붙어있는 것 같진 않네요.

이걸 아까 빌드한 openwrt-ramips-mt76x8-cudy_wr1000-initramfs-kernel.bin 이미지 포맷과 비교해봅니다.

똑같이 uImage 헤더로 시작하고, 뒤에는 커널만 따라옵니다. squashfs가 빠지긴 했지만 구조는 대충 비슷하니 이 정도면 복구모드도 만족해하지 않을까요.

이 initramfs 이미지를 TFTP로 올려보겠습니다.

이미지를 거부하네요? 정펌 이미지엔 a604m이라 적혀있는 부분이 initramfs 이미지엔 MIPS OpenWrt Linux-5.4.143이라 적혀있어서 안 된다는 것 같습니다. 아까 binwalk 결과에서 uImage 헤더의 이미지 이름란에 이런 문자열이 들어가 있었죠.

다행히 이건 이미지 Makefile에 한 줄만 추가해주면 해결됩니다.

다시 이미지를 빌드하고 TFTP로 올리면 이번엔 이미지를 정상적으로 접수합니다.

부팅이 끝나고 쉘이 뜨면 성공한 겁니다.

Q: 근데 아직 플래시 덤프도 안 했는데 너무 무작정 설치해버린 거 아닌가요?
A: 네 맞습니다… 경험상 ipTIME ramips 기기에서 부트로더 복구모드를 믿어도 괜찮고, 덤프는 나중에 남은 부분만 해도 충분하다는 걸 알기 때문에 이렇게 간 거고, 다른 디바이스 작업할 땐 꼭 덤프부터 하세요.[3]

기능 살리기

OpenWrt 부팅에 성공했으니, 이제 본격적으로 디바이스 지원 패치를 작성해봅시다.

일단 A604M 이미지를 만드는 것부터 시작하겠습니다. WR1000 지원 패치를 복사해 A604M으로 이름만 바꿔줍니다. wr1000로 검색하면 금방 찾을 수 있습니다.

대충 이 정도? 별로 안 많죠? dts는 파일을 새로 만들어야 합니다.

이 다음에 make menuconfig를 다시 하면 모델 목록에 A604M이 뜹니다. 새로 빌드한 openwrt-ramips-mt76x8-iptime_a604m-initramfs-kernel.bin 이미지로 재설치하면 이전처럼 잘 부팅되어야 합니다.

다음으로 넘어가기 전에 잠깐 DTS가 무엇인지에 대해 짧게 설명하겠습니다.

예전엔 (그리고 지금도 일부는) 각 기기 모델마다 존재하는 mach-*.c 또는 board-*.c에서 주변장치 초기화 및 설정 작업을 진행했지만, 현재는 이 목적을 위해 DTS(Device Tree Source)라는 별개 형식의 파일을 사용하는 추세입니다. dtc라는 별도의 컴파일러를 써서 DTS 소스파일을 DTB 바이너리로 만들고, 이걸 커널 이미지 옆에 덧붙이거나 저장장치 어딘가에 저장해놨다가 부팅 시 불러와서 사용합니다. DT는 실행 코드가 아니라 단순히 하드웨어를 기술하는 역할만 하고, 드라이버는 DTB를 읽어들여 기기에 어떤 장치가 달려있고 각각을 어떻게 사용해야 하는지 파악합니다.

참고로 모든 기기가 디바이스 트리를 쓰는 건 아니고, 이외에도 여러 하드웨어 정보 관리 방법이 있습니다. 브로드컴 공유기는 nvram을 사용하기도 하고, x86 컴퓨터에선 ACPI(특히 DSDT)를 사용합니다.

ramips는 아직 커널 메인라이닝이 덜 됐기 때문에 아직도 많은 부분이 OpenWrt 다운스트림 패치로 남아있고, dts도 커널 소스트리에 들어간 걸 쓰지 않고 target/linux/ramips/dts/에 있는 로컬 파일을 사용합니다. 내용을 보면 SoC의 전반적인 기능을 dtsi 파일에서 정의하고, 이걸 디바이스 dts에서 불러와 필요한 기능만 활성화시켜 사용하는 구조로 되어있습니다.

참고문서:

  • Device Tree for Dummies
  • Device Tree 문법

이제부터 할 일은 A604M의 기능을 하나씩 살려나가는 것입니다. 확인해야 할 항목을 꼽아보자면 이더넷, 플래시, WiFi, LED, 버튼 정도가 되겠네요.

뭘 어떻게 해야 하는 건지 막막할 땐 다른 디바이스 지원 패치에선 어떻게 했는지 소스코드나 커밋 로그를 찾아보세요. DT 속성의 의미나 사용방법이 궁금할 땐 커널 Documentation/devicetree/bindings/ 경로 아래 있는 문서를 읽어보면 도움이 됩니다.

참고문서:

  • [OpenWrt wiki] Adding a new device

이더넷

일단 이더넷이 돼야 이후 작업이 편해집니다. 이더넷 설정은 dts와 02_network에 담겨있습니다.

&esw 노드는 그냥 날려버립니다. mediatek,portmap 속성은 스위치 LAN/WAN 설정하는 역할인데, 이건 잠시 후에 02_network에서 할 거라 여기선 빼도 됩니다. &ethernet 노드는 지금은 그대로 둡니다.

LAN/WAN 설정을 하기 위해선 각 포트가 MT7628A에 내장된 이더넷 스위치의 몇 번 포트에 해당하는지를 알아야 합니다. 이건 swconfig를 쓰면 쉽게 알아낼 수 있습니다.

처음에 외부 포트에 아무 선도 꽂지 않으면 6번 포트만 연결된 것으로 나옵니다. 6번 포트는 스위치와 CPU를 연결하는 내부포트이겠지요. 그리고 WAN 포트에 선을 꽂으니 0번 포트가 연결된 것으로 뜹니다.

이런 식으로 각 외부포트와 스위치 포트번호를 짝지으면 이렇게 나옵니다.

  • 0번 포트 - WAN
  • 1번 포트 - LAN 4
  • 2번 포트 - LAN 3
  • 3번 포트 - LAN 2
  • 4번 포트 - LAN 1
  • 6번 포트 - CPU

이걸 02_network에 적을 겁니다.

2:lan:3의 의미는 스위치 2번 포트가 LAN 3 포트와 연결되어 있다는 뜻입니다.

이제 이더넷 LAN/WAN이 모두 잘 될 겁니다. 컴퓨터와 외부 인터넷 선을 꽂아서 확인해보세요.

플래시

A604M은 저장장치로 SPI NOR 플래시를 사용합니다. 리눅스에서는 이러한 FTL이 없는 플래시 칩을 MTD 장치로 나타냅니다. MTD 저장공간을 파티션으로 분할하려면 하드 디스크처럼 파티션 테이블을 쓰는 게 아니라, 파티션 정보를 커널 또는 dts에 직접 적어줘야 합니다.

정펌 부팅 로그를 보면 mtd 파티션을 어떻게 나눠놨는지가 뜹니다.

대충 정리해보면 이렇습니다.

파티션 시작 위치 파티션 크기 파티션 이름
0x000000 0x020000 bootloader
0x020000 0x010000 config
0x030000 0x010000 factory
0x040000 0x7c0000 firmware

bootloader 파티션엔 U-Boot 부트로더가 있을 거고, firmware 파티션엔 정펌 이미지가 들어가 있겠죠.
factory 파티션은 보통 무선 칩셋이 사용하는 캘리브레이션 데이터와 MAC 주소를 담는 장소고, config 파티션엔 보통 부트로더 환경변수나 기기 설정값(WiFi SSID와 비번 등)이 담겨있습니다.

이걸 dts에 옮겨 적습니다.

  • "firmware"는 OpenWrt에서 특별한 파티션 이름입니다. 커널은 이 이름의 파티션을 살펴보고 kernel, rootfs, rootfs_data 파티션으로 분할합니다.
  • compatible = "denx,uimage"는 펌웨어 이미지가 uImage 포맷임을 알리는 속성입니다.
  • firmware 파티션 이외엔 손대서 좋을 게 없으니 읽기전용으로 강제합니다. 만약 읽기전용 파티션을 수정해야 할 일이 생긴다면 kmod-mtd-rw를 쓰세요.
  • spi-max-frequency는 플래시 I/O 속도를 결정합니다. 이 값이 너무 작으면 읽기쓰기 속도가 느리고, 너무 크면 CPU가 플래시를 인식하지 못합니다. 이 값을 제대로 정하는 과정은 좀 복잡한데, 일단 플래시 데이터시트에 따르면 50MHz 이하로 두는 게 안전할 것 같습니다.

아직 플래시 덤프를 못 했죠? 이제 할 수 있습니다.

개인적으론 플래시 전체 크기의 파티션을 따로 만들어서 덤프하는 방법을 선호합니다.

NOR는 대체로 살리기도 쉽고 덤프하기도 쉬운데, NAND는 ECC나 BBT 등 신경써줘야 할 게 많아서 더 복잡합니다.

참고문서:

  • Linux MTD FAQ
  • [OpenWrt wiki] The OpenWrt Flash Layout

WiFi

보드에 WiFi 칩셋이 두 개 있었죠. MT7628A는 2.4GHz 대역을 담당하고, PCIe로 연결된 MT7612E는 5GHz 대역을 담당합니다. 각각은 factory 파티션에 저장된 캘값을 필요로 하고, dts에는 이 캘값의 위치를 적어주어야 합니다.

factory 파티션 내용을 쭉 훓어보면 알겠지만, 대부분의 공간은 텅 비어있고 0x0 부분과 0x8000 부분에 약간의 데이터가 저장되어 있습니다.

이 중에 어떤 게 누구 건지 어떻게 알 수 있을까요? 가장 간단한 방법은 둘 다 시도해보는 겁니다. 캘값엔 칩셋 정보도 들어가있기 때문에 잘못 넣으면 못 받아먹고 토해냅니다.[4]

또 다른 방법은 MAC 주소를 비교해보는 겁니다. 보통 WiFi 칩셋마다 서로 다른 MAC 주소를 할당하기 때문에, 정펌 상태에서 어떤 주소를 쓰는지 확인하고 캘값에 적힌 주소와 비교해보면 되겠습니다. WiFi AP의 MAC 주소는 관리자 웹페이지에서 확인할 수도 있고, WiFi 스캔 앱이나 iw 명령어로 확인할 수도 있습니다.

제 기기의 MAC 주소는 이렇습니다.

  • 2.4GHz: 88:36:6c:xx:xx:dc
  • 5GHz: 88:36:6c:xx:xx:de

보통 마지막 바이트만 살짝 다르게 나옵니다. 이 주소를 factory 파티션에서 검색해봅니다.

0x0이 2.4GHz, 0x8000이 5GHz네요. 이걸 dts에 적어줍니다.

  • &wmac은 MT7628A의 내장 WiFi 모듈입니다. 0x0은 SoC dtsi에 이미 들어가있으니 다시 안 써줘도 됩니다.
  • MT7628A는 PCIe 포트가 하나밖에 없어서 별로 상관없지만, 포트가 여러 개인 기기에서 작업할 땐 lspci -nn으로 어떤 게 어디에 물려있는지 확인해야 합니다.
  • ieee80211-freq-limit는 WiFi 칩셋이 사용할 주파수 대역을 제한하기 위해 사용합니다. 원래 MT7612E는 2.4GHz와 5GHz를 둘 다 지원하는데, A604M에선 5GHz 안테나 선만 연결해서 사용하고 있습니다. 하지만 커널은 그걸 모르니 수동으로 알려줘야 합니다.
  • wifi@0,0 안에 led 노드는 MT7612E의 LED 핀을 위한 설정인데, 뒤에서 보겠지만 A604M에선 사용되지 않으므로 지웁니다.

WiFi 커널 모듈도 추가해줘야 하는데, kmod-mt7603kmod-mt76x2가 이미 이미지에 포함되어 있어서 더 건들 게 없습니다.

WiFi 작동확인을 하려면 /etc/config/wireless에서 장치를 켜면 됩니다. 자세한 방법은 위키 문서를 참조하세요.

이더넷 MAC 주소

이더넷 MAC 주소도 정펌과 같게 해주는 게 좋습니다. 이것도 관리자 웹페이지에서 확인하거나 arp-scan 명령어로 직접 확인할 수 있습니다.

또는 정펌 부팅 로그에서 읽어올 수도 있고요.

이 MAC 주소는 이상하게 부트로더 파티션에 있네요.

LAN 주소는 dts에, WAN 주소는 02_network에 적어줍니다. 둘 다 02_network에 넣어도 되긴 하지만, 커널한테도 귀띔해줘서 나쁠 거 없으니까요.

이제 MAC 주소가 다 맞게 되었네요.

  • 만약 플래시에 정확하게 일치하는 MAC 주소가 없으면 비슷한 주소에다 차이값을 더해서 쓸 수 있습니다.
  • 기기 어딘가에 대표 MAC 주소가 적혀 있으면 label-mac에 넣을 수 있지만, ipTIME은 더 이상 보드에 MAC 주소 스티커를 붙이지 않는 모양입니다.

참고문서:

  • [OpenWrt wiki] Device Support: MAC address setup

USB

A604M엔 외부 USB 포트가 없습니다. 보드에 USB 포트 흔적이 남아있긴 한데, 굳이 납땜해서 달고 싶은 마음은 안 드네요.

왠진 모르겠지만 SoC dtsi에서 USB를 기본적으로 활성화해둔 상태이므로 dts에서 비활성화해둡니다.

  • ohci는 USB 1.1, ehci는 USB 2.0, xhci는 USB 3.0에 해당합니다.
  • USB 포트가 여러 개인 경우 lsusb -t 명령어를 쓰면 어떤 게 어느 건지 구별할 때 도움이 됩니다.
  • USB 포트의 전원핀(VBUS)을 GPIO로 제어하는 경우도 있습니다. 전원 LED가 달려있는 USB 장치가 있으면 드라이버 문제인지 VBUS 문제인지 판별할 수 있겠죠.

LED와 버튼

A604M에 달린 LED와 버튼 목록은 다음과 같습니다.

  • LED: 전원, CPU, WiFi 2.4G, WiFi 5G, LAN 1~4, WAN
  • 버튼: WPS, 리셋

LED 중에서 소프트웨어로 제어해야 하는 건 CPU와 WiFi 정도네요. 전원 LED는 VCC에 직결되어 있고, LAN/WAN LED는 스위치에서 자동제어 해주는데 굳이 건들 필요 없겠죠.

보통 이런 LED는 SoC의 GPIO 핀에 연결되어 있습니다. 어떤 LED가 몇 번 핀에 달려있는지 확인하기 위해선 GPIO 핀을 차례로 올렸다 내렸다 하면서 반응하는 LED를 찾는게 일반적인 방법이지만, 지금 보니 커널 버전이 5.4로 올라가면서 gpio 드라이버가 바뀌고 핀 순서가 뒤죽박죽 되어버렸네요. 에휴… 그냥 바로 지름길로 가겠습니다.

정펌 소스를 보면 LED/버튼의 핀 번호가 나와있습니다.[5] 예를 들어 WiFi 5G LED는 5번 핀이라고 하네요.

  • CPU LED: GPIO #11
  • WiFi 2.4G LED: GPIO #46
  • WiFi 5G LED: GPIO #5
  • 리셋 버튼: GPIO #38
  • WPS 버튼: GPIO #45

MT7628 데이터시트를 보면 각 GPIO 핀이 SoC에서 어떤 다른 역할을 할 수 있는지 적혀있습니다.

그러니까 GPIO 5번 핀은 I2C SDA 핀으로도 쓰일 수 있다는 겁니다. 이 핀을 I2C가 아니라 GPIO로 쓰고 싶다면 dts pinctrl 노드에 이를 명시해주어야 합니다.

콘솔에서 GPIO 제어 한번 안 해보고 이대로 지나가버리는 건 좀 섭섭하니 핀 테스트도 해봅시다.

5번 핀은 리눅스에서 gpio485에 해당합니다. 이미 export된 gpio는 아직 삭제 안한 WR1000의 LED와 버튼입니다.
value에 0을 넣으면 LED가 켜지고, 1을 넣으면 LED가 꺼질 겁니다. LED가 켜질 때 또는 버튼이 눌릴 때(active) GPIO가 1(high)이면 active high, GPIO가 0(low)이면 active low라고 합니다.

이 정보를 바탕으로 dts 노드를 작성합니다.

  • linux,code는 해당 버튼이 눌릴 때 입력될 신호를 지정합니다.
  • linux,default-trigger는 해당 LED가 사용할 트리거를 지정합니다. phy0tpt는 WiFi phy0 장치에 트래픽이 발생할 때 깜박이는 트리거입니다.
  • dts에서 지정하지 못하는 트리거는 01_leds에서 처리하는데, A604M은 dts에서 모든 LED 설정을 마쳤으므로 01_leds 설정은 빼버립니다.
  • led-boot alias는 OpenWrt가 부팅하는 동안 깜박일 상태 LED를 지정합니다.
  • SoC GPIO가 아닌 외부 WiFi 칩셋이나 GPIO expander 등에 LED가 연결된 경우도 있습니다. 이런 LED를 찾아서 제어하려면 먼저 해당 칩셋 드라이버를 켜고 dts에서 추가 작업을 해야 합니다.

이제 LED와 버튼이 모두 작동할 것입니다. 참고로 OpenWrt에서는 버튼이 눌리면 /etc/rc.button/에 있는 스크립트가 실행됩니다.

참고문서:

  • [OpenWrt wiki] GPIO

Failsafe

OpenWrt를 쓰다가 뭔가 설정을 잘못해서 더 이상 원격 접속이 안 되는 상황이 생길 수 있습니다. 이럴 때를 대비해 OpenWrt에도 안전 모드 같은 게 있는데, 이게 바로 failsafe 모드입니다. 부팅 중에 상태 LED가 빠르게 깜박일 때 콘솔에서 f를 누르거나 보드에 달린 아무 버튼을 누르면 failsafe 모드로 진입합니다. 공유기는 192.168.1.1 고정 IP 상태가 되고, 외부에서 고정 IP로 유선 연결해서 ssh 접속을 할 수 있게 됩니다.

이걸 왜 여기서 다루냐면, 디바이스 지원 패치를 작성하면서 failsafe 모드가 정상 작동하는지도 확인할 필요가 있기 때문입니다. 갑자기 큰일이 생겨서 failsafe 모드에 의존할 일이 생겼는데, 이더넷 설정에 문제가 있어서 ssh 접속이 안 되면 눈물이 나겠죠. 대부분의 경우엔 별다른 추가 설정 필요없이 잘 될 겁니다.

부트로더

정펌 부트로더에는 이런저런 기능적 제약이 걸려있는 경우가 많기 때문에 U-Boot 부트로더를 새로 빌드해 갈아치우는 경우도 꽤 있습니다. 이 경우 U-Boot 지원 패치도 따로 만들어서 넣어야 합니다.

하지만 A604M의 경우엔 굳이 바꾸지 않아도 잘 돌아가고, ramips 타겟에선 부트로더는 그대로 두는 경우가 많아서 여기서도 따로 건들지 않겠습니다.

설치법

이제 기기의 웬만한 기능은 다 살려놨습니다. 마지막으로 설치 이미지를 만들어봅시다.

OpenWrt에선 squashfs 설치 이미지를 크게 두 가지로 분류합니다.

  • factory: 정펌에서 OpenWrt를 설치할 때 사용하는 이미지입니다. 제조사의 펌웨어 검증 절차를 통과하기 위해 이상한 게 덕지덕지 붙어있는 경우가 많습니다.
  • sysupgrade: 이미 OpenWrt가 설치된 기기에서 다시 OpenWrt를 설치할 때(보통 새 버전으로 업그레이드 할 때) 사용하는 이미지입니다.

factory 이미지가 반드시 따로 존재할 필요는 없습니다. 만약 정펌에서 sysupgrade 이미지를 바로 설치하는 게 가능하면 sysupgrade가 factory 역할도 하게 되는 겁니다.

으… 매번 "이미지"를 반복하는 것도 슬슬 귀찮아지니 앞으로 initramfs, factory, sysupgrade 이미지를 각각 I펌, F펌, S펌으로 줄여서 부르겠습니다.

일단 S펌부터 살펴보겠습니다. 따로 이미지 생성방법을 지정하지 않으면 기본 레시피에 따라 S펌이 빌드됩니다.

빌드 이미지 디렉토리에 있는 S펌 포맷을 확인합니다.

uImage 헤더로 시작하고, 커널과 rootfs가 뒤따라오는 건 정펌과 매우 유사해 보입니다. 오…? 그러면 복구모드에서 S펌을 받아들일지도 모르겠네요.

해보니 잘 됩니다. S펌 첫 부팅은 jffs2 초기화 때문에 시간이 더 걸립니다.

OpenWrt를 재설치할 땐 sysupgrade 명령어를 쓰고, 모델별 재설치 절차는 platform.sh에서 지정합니다.

ramips 타겟의 기본 재설치 절차는 default_do_upgrade인데, 이건 NOR 플래시의 firmware 파티션에 이미지를 설치하는 일반적인 방법입니다.

OpenWrt를 재설치하면 기존에 설치해둔 패키지 등은 사라지지만, /etc/config/에 있는 설정값은 보존됩니다. 한번 테스트 해보겠습니다.

hostname을 "a604m"으로 바꾸고 재설치를 했으니 다음에 부팅하면 hostname이 a604m으로 떠야합니다.

그리고 실제로 그렇게 됐네요. 이로써 재설치 기능도 검증이 된 셈입니다.

개인이 혼자 사용할 거라면 이걸로 포팅이 끝났다고 볼 수 있지만, 만약 지원 패치를 업스트림에 머지시킬 생각이라면 보다 손쉬운 설치법을 찾는 게 좋을 겁니다. 모든 사람이 시리얼 어댑터를 가지고 있는 건 아니니까요.

가장 좋은 방법은 정펌 관리자 웹페이지의 펌업 메뉴를 이용하는 것입니다. 이것만큼 쉬운 방법도 없겠죠.

S펌을 업로드하고 플래싱하고 OpenWrt로 부팅하는 데까진 잘 되는데, 중간에 커널 패닉이 나면서 부팅에 실패합니다.

요지는 rootfs를 못 찾겠다는 얘깁니다. 대체 왜? tftpboot를 쓸 수 있다면 플래시 덤프라도 해보겠지만, 방법이 없으니 펌업 로그라도 읽어봐야죠.

정펌에서 플래싱을 할 때 2000536 바이트만큼 기록하겠다고 하네요. 1.9MB는 너무 적은데요. 이 값은 어디서 온 걸까요?

아까 S펌 binwalk 결과에서 uImage 헤더의 이미지 크기란에 2000472 바이트라고 적혀있었습니다. 2000472는 커널만 포함하는 크기고, rootfs는 딱 2000536(= 2000472 + 64) 바이트 지점에서 시작합니다. 이 말인즉슨, 정펌은 헤더의 이미지 크기란을 기준으로 플래싱을 하고, S펌의 uImage는 딱 커널까지만 포함하기 때문에, 그 이후 부분이 플래싱이 되지 않아 커널이 rootfs를 못 찾는 겁니다.

두 이미지의 포맷 차이를 그림으로 대충 그려보면 이렇습니다.

1
2
3
4
5
6
7
8
9
10
11
# 정펌
┌─────uImage─────┐
┌────┬────┬─────┐
│ header │ kernel │ squashfs │
└────┴────┴─────┘

# S펌
┌──uImage──┐
┌────┬────┬─────┬───────┬─────┐
│ header │ kernel │ squashfs │ jffs2 marker │ metadata │
└────┴────┴─────┴───────┴─────┘

만약 uImage가 커널 이후 범위도 포함하게 한다면 어떨까요? 제가 알아본 바로는, OpenWrt의 mtdsplit 코드는 uImage가 커널만 포함하는 걸 전제로 짜여있기 때문에, 헤더를 수정하면 아마 커널이 rootfs를 못 찾을 겁니다.

그러면 정펌 관리자 웹페이지를 통해선 OpenWrt 설치가 불가능한 걸까요? 다행히도 방법이 남아있습니다. I펌은 이미 정펌 포맷에 들어맞기 때문에 웹페이지에서도 펌업이 잘 됩니다. 그럼 I펌을 F펌으로 쓰면 되겠죠. 다만 위에서 말했듯 I펌은 파일 변경사항이 저장이 안 되기 때문에 다시 S펌으로 재설치할 필요는 있을 겁니다.

여기서 스샷을 따로 첨부하진 않겠지만 I펌→S펌, S펌→정펌 설치도 잘 됩니다. 모두 sysupgrade 명령어로 처리 가능합니다.(다만 후자의 경우 정펌엔 OpenWrt 메타데이터가 없기 때문에 -F 옵션을 추가해주어야 합니다.) OpenWrt에선 config 파티션에 손대지 않기 때문에 정펌으로 돌아가도 기존 설정은 그대로 유지될 것입니다.

아쉽게도 A604M은 리셋 버튼을 누른 상태에서 전원을 넣어도 TFTP 복구모드에 들어가지 않습니다. 강제로 복구모드에 진입하기 위해 정펌 업데이트 중간에 전원을 끄는 방법도 있긴 하지만 이건 남한테 추천은 못 하겠네요.

마지막으로 이미지 Makefile을 한번 보고 가도록 하겠습니다. dts가 하드웨어 정보를 모아놓은 곳이라면, image/Makefile은 이미지 생성방법이 적힌 곳입니다.

  • IMAGE_SIZE는 펌웨어의 최대 크기를 지정합니다. 그냥 firmware 파티션 크기를 적어주면 됩니다.
  • DEVICE_PACKAGES는 이미지에 포함될 기본 패키지를 지정합니다. 플래시 공간을 절약하기 위해선 단순히 있으면 좋은 정도가 아니라 기본 작동에 필수적인 것들만 포함시켜야 합니다.
  • 이미지 종류나 생성방법을 바꾸려면 IMAGESIMAGE/<image>를 설정하면 되는데, A604M의 경우엔 기본 I펌과 S펌만으로도 충분하므로 factory.bin 관련 설정은 지웁니다.
  • SUPPORTED_DEVICES는 특수한 사정이 없으면 기본값을 쓰면 되므로 뺍니다.

메인라이닝

여태까지의 코드를 하나의 패치로 만들면 이렇게 됩니다. v21.02.0-a604m.diff

이 패치를 혼자 사용하거나 개인 리포지토리에 올리고 끝내는 것도 방법이지만, 만약 OpenWrt 공식 저장소에 넣을 수 있다면 더 많은 사람들이 지원 패치의 혜택을 볼 수 있겠죠.

업스트림 저장소에 패치를 머지했을 때 좋은 점을 몇 가지 들어보자면,

  • 공식 빌드봇이 이미지를 자동으로 빌드해주므로 직접 빌드할 필요가 없습니다.
  • 만약 직접 빌드한 이미지를 쓴다면, 일반 패키지는 opkg로 다운로드받아 설치할 수 있지만, 커널 모듈만큼은 다운로드받아 쓸 수 없고 다시 빌드해 넣어야합니다. 이는 커널 빌드 옵션이 달라서 생기는 일으로, 공식 빌드봇과 같은 옵션을 줘서 빌드하면 해결되긴 하지만 이는 시간이 꽤 오래 걸리는 일입니다. 만약 패치에 커널 옵션 변경사항이 있다면 그마저도 불가능하고요.
  • 공식 저장소에 머지되고 나면 로컬에서 업스트림 버전 업데이트를 따라갈 필요가 없다는 건 당연하겠죠.

다만 패치가 "hack"에 가깝거나, 다른 모델에 부작용을 끼치거나, 코드 관리 부담이 큰 경우엔 머지가 거부될 수 있습니다.

현재 OpenWrt는 패치를 메일링 리스트GitHub에서 받고 있습니다.[6] OpenWrt 메인테이너 중에는 커널 개발자도 많아서 패치 처리 절차도 비슷하게 따라가는 것 같습니다.

이 글에서 GitHub PR 여는 법을 설명하진 않겠지만, 대신 몇 가지 주의사항을 언급하자면,

  • 많은 오픈소스 프로젝트에서 패치를 어떻게 작성해야 하는지 가이드라인을 제시합니다. OpenWrt의 경우에도 마찬가지로 공식 가이드에 따라 패치의 형식을 맞춰주어야 합니다.
  • 다른 리뷰어의 질문과 코멘트에 적절히 응답해야 합니다. 나는 작동하는 패치를 줬으니 너희들이 알아서 고쳐서 넣어라, 식으로 나오는 사람이 가끔 있는데 이러면 머지될 확률이 낮아집니다.
  • 대부분의 오픈소스 프로젝트가 그렇듯이, PR은 관리자의 선의로 머지됩니다. 특히 기기 지원 패치는 머지되기까지 몇 주 걸리는 게 보통입니다. PR에 아무런 반응이 달리지 않고 시간만 지나가는 데에 실망감이 드는 건 전혀 이상한 일이 아닙니다. 그렇지만 인내심을 갖고 기다리면서 가끔씩 관련자를 호출하는 것 외에 달리 할 수 있는 게 있는 것도 아닙니다.

당연하다면 당연한 얘기지만 이것도 지키지 못해 거부되는 PR을 몇 번 봤기 때문에 노파심에 언급해둡니다…

운이 좋아 PR이 머지가 된다면 축하드립니다! 하루이틀 지나면 공식 다운로드 서버OFS에서 이미지를 다운받을 수 있을 겁니다.

이 또한 의무는 아니지만(패치는 기여자의 선의로 작성됩니다), 머지 이후에도 당분간 기기를 처분하지 말고 혹시 들어올지 모르는 버그 제보에 대비하면 더욱 좋을 것 같습니다.

참고문서:

  • [OpenWrt wiki] Submitting patches
  • [OpenWrt wiki] Device support policies / best practices
  • [OpenWrt wiki] Frequent PR mistakes or “How to prevent my PR from getting delayed for sure”

  1. 이제 ar71xx 타겟은 master 브랜치에서 제거되었기 때문에 앞으론 ath79 타겟을 써야합니다. ↩︎

  2. NOR 플래시 이미지에 해당하는 얘기고, NAND 플래시나 SD카드 이미지는 구조가 조금 다릅니다. ↩︎

  3. A604M에서 SOIC 클립으로 온보드 플래시 덤프가 가능한 걸 확인했습니다. 여기까지 시도하실 분이라면 제가 뭐라 덧붙이지 않아도 어련히 잘 하시겠죠. ↩︎

  4. 그럼 만약 동일한 칩셋이 두 개라면요? 그런 경우라도 보통 2.4GHz용 하나, 5GHz용 하나, 이런 식이기 때문에 캘값을 바꿔 넣으면 일단 인식은 되는데 정작 WiFi 신호가 안 잡힐 겁니다. 그럼 세 개라면요? …그만합시다. ↩︎

  5. 그 외에도 메모리 크기, 주변장치 종류, 스위치 포트 순서도 알려주는 아주 좋은 자료입니다. ↩︎

  6. 나중에 자체운영 GitLab으로 넘어갈 거라곤 하는데 아직은 별다른 소식이 없네요. ↩︎