Stm32mp157_핀리스트에서dtb까지_ai
STM32MP157 · Linux 5.4.31 · MYiR YA157C
핀리스트에서 DTB까지
커스텀 캐리어보드를 올릴 때 디바이스 트리를 어떻게 읽고, 고치고, 확인하는가. 개념부터 make dtbs와 debugfs 검증까지 실제 SC6_0 프로젝트 기준으로 정리한 작업 문서.
커널 ~/work/myir-st-linux 빌드 ~/work/build DTS arch/arm/boot/dts/
Contents
/concept
디바이스 트리는 무엇을 해결하는가
커널은 드라이버를 갖고 있고, 디바이스 트리는 이 보드에 무엇이 어디에 붙어 있는지를 갖고 있습니다. 둘을 분리한 것이 전부입니다.
x86은 PCI·ACPI로 하드웨어가 스스로 자기를 소개합니다. ARM SoC는 그런 게 없습니다. I2C 버스 2번에 주소 0x38로 터치 컨트롤러가 붙어 있다는 사실을 아무도 스스로 말해주지 않으니, 예전에는 보드마다 C 파일(board-*.c)을 하나씩 만들어 커널에 박아 넣었습니다. 보드가 늘어날수록 커널 트리가 감당이 안 됐고, 그래서 이 정보를 커널 밖으로 빼낸 것이 디바이스 트리입니다.
따라서 DTS 작업의 성격은 이렇습니다. 드라이버를 새로 짜는 일이 아니라, 이미 있는 드라이버에게 배선 정보를 알려주는 일입니다. 대부분의 작업은 “이 드라이버를 켜고, 이 핀을 쓰고, 이 GPIO에 리셋이 걸려 있다”고 선언하는 것으로 끝납니다.
부팅 경로에서의 위치
// 빌드 타임 .dts + .dtsi ──[ cpp ]──▶ 전처리된 .dts ──[ dtc ]──▶ .dtb // 런타임 U-Boot ──▶ .dtb를 메모리에 적재 ──▶ 커널에 주소 전달 │ ▼ 커널이 노드를 순회하며 compatible 문자열로 맞는 드라이버를 찾아 probe() 호출
dts → dtb → probe
여기서 중요한 결과가 하나 나옵니다. dtb는 커널 이미지와 별개 파일입니다. 커널을 다시 빌드하지 않아도 dtb만 교체하면 하드웨어 구성이 바뀌고, 반대로 커널만 새로 올리고 dtb를 안 바꾸면 변경사항이 전혀 반영되지 않습니다. 실무에서 “분명히 고쳤는데 안 되는데요”의 절반은 여기서 나옵니다.
노드와 드라이버를 연결하는 열쇠는 compatible 문자열입니다. DTS에 compatible = "edt,edt-ft5x06"이라고 쓰면, 커널은 자기가 가진 드라이버 중 같은 문자열을 등록한 것을 찾아 붙입니다. 문자열이 틀리면 조용히 아무 일도 일어나지 않습니다.
/files
파일 계층: dtsi와 dts
확장자 두 개의 차이는 단순합니다.
.dtsi— i = include. 여러 보드가 공유하는 조각. 혼자서는 컴파일되지 않습니다..dts— 최종 보드 하나. .dts 파일 하나가 .dtb 하나가 됩니다.
아래로 갈수록(나중에 include될수록) 우선순위가 높습니다. SoC 공통 정의를 맨 위에 깔고, 모듈이 덮고, 보드가 최종적으로 덮는 구조입니다.
stm32mp157c-ya157c-lcd-v2.dts ← 최종 보드 = dtb 1개 └─ stm32mp157c-ya157c-v2.dts 캐리어보드 (레귤레이터, SD/eMMC) ├─ stm32mp157.dtsi SoC 계열 ├─ stm32mp15xa.dtsi 파트번호별 차이 (A/C/D/F) ├─ stm32mp157-ya157c-pinctrl.dtsi MYiR 핀맵 그룹 ├─ stm32mp15xxac-pinctrl.dtsi 패키지별 핀 정의 └─ stm32mp15xx-ya157c.dtsi 모듈 + 캐리어 혼재 ├─ stm32mp157-m4-srm.dtsi └─ stm32mp157-m4-srm-pinctrl.dtsi
YA157C BSP의 실제 include 체인
벤더 BSP 주의
이름만 보면 stm32mp15xx-ya157c.dtsi가 SoM(모듈) 전용처럼 보이지만, 실제로는 모듈 레벨(DDR·NAND·eMMC·Ethernet PHY)과 캐리어보드 레벨(터치스크린·Type-C·LED·백라이트·UART 핀맵)이 한 파일에 섞여 있습니다. “모듈 파일은 안 건드린다”는 원칙이 이 BSP에서는 성립하지 않습니다. 새 보드를 만들 때는 이 파일도 복사해서 손봐야 합니다.
새 보드를 추가하는 표준 구성
기존 파일을 직접 수정하기보다 복사해서 새 이름으로 시작하는 편이 안전합니다. 벤더가 BSP를 업데이트했을 때 충돌이 나지 않고, 원본과 diff로 비교할 수 있습니다.
stm32mp157-sc6-pinctrl.dtsi // 우리 보드 핀 그룹
stm32mp15xx-sc6.dtsi // ya157c.dtsi 복사 → 캐리어 부분 수정
stm32mp157a-sc6-v1.dts // 최종 보드
/syntax
문법: 실무에 필요한 만큼만
DT 문법은 넓지만, 보드 브링업에서 실제로 쓰는 것은 여섯 가지 정도입니다.
노드와 프로퍼티
label: node-name@40013000 {
compatible = "st,stm32-i2c"; // 문자열
reg = <0x40013000 0x400>; // 32비트 정수 (셀)
interrupts = <31 0>;
status = "okay";
gpio-controller; // 값 없는 불리언 = 존재하면 true
};
노드 이름 뒤의 @40013000은 reg의 첫 값과 일치해야 하는 unit address입니다. 주소가 없는 노드(가상 노드)에는 붙이지 않습니다. 값 표기는 <>가 32비트 정수 배열, []가 바이트 배열, ""가 문자열입니다.
label과 phandle — 작업의 90%
노드 앞에 붙인 label:은 다른 곳에서 &label로 참조하는 이름표입니다. 이게 두 가지 방식으로 쓰입니다.
프로퍼티 안 → 참조(phandle)
“이 GPIO 컨트롤러의 8번 핀”처럼 다른 노드를 가리킬 때.
reset-gpios = <&gpiob 10 GPIO_ACTIVE_LOW>;
vmmc-supply = <&v3v3>;
최상위 → 덮어쓰기(override)
이미 정의된 노드에 프로퍼티를 추가·교체. 대부분의 작업이 이 형태.
&i2c1 {
status = "okay";
clock-frequency = <400000>;
};
덮어쓰기는 병합입니다. 위 예시는 i2c1 노드를 새로 만드는 게 아니라, SoC dtsi에 이미 있는 노드에 두 프로퍼티를 얹는 것입니다. 같은 프로퍼티가 이미 있으면 나중 값이 이깁니다.
status — 가장 많이 건드리는 프로퍼티
| 값 | 의미 |
|---|---|
"okay" |
사용. 드라이버 probe 대상. |
"disabled" |
미사용. 노드는 존재하지만 probe되지 않음. |
SoC dtsi는 거의 모든 페리페럴을 disabled로 정의해 둡니다. 보드 dts에서 쓰는 것만 okay로 켜는 것이 기본 흐름입니다.
부모가 이깁니다
부모 노드가 disabled면 자식이 okay여도 동작하지 않습니다. MYiR BSP의 타이머가 정확히 이 형태입니다.
&timers3 {
status = "disabled"; // ← 부모가 꺼져 있어서
pwm {
status = "okay"; // ← 이게 무의미
};
};
PWM을 쓰려면 부모 &timers3부터 "okay"로 바꿔야 합니다.
삭제 지시자
상위 파일이 정의한 것을 없애야 할 때 씁니다. 예를 들어 DMA 채널을 다른 용도로 돌리려고 UART에서 떼는 경우입니다.
&uart4 {
/delete-property/ dmas;
/delete-property/ dma-names;
};
&some_node {
/delete-node/ child-node;
};
alias와 chosen
aliases {
ethernet0 = ðernet0;
serial0 = &uart4; // → /dev/ttySTM0
serial3 = &uart5; // → /dev/ttySTM3
};
chosen {
stdout-path = "serial0:115200n8"; // 콘솔
};
serialN 별칭 번호가 /dev/ttySTMN 번호를 결정합니다. 유저 공간 코드가 포트 이름에 의존한다면 이 번호를 함부로 바꾸면 안 됩니다.
/pinctrl
핀 멀티플렉싱 — STM32 작업의 핵심
물리적 핀 하나가 최대 16가지 기능 중 하나로 동작합니다. 어느 것을 고를지 지정하는 것이 pinctrl이고, 커스텀 보드 작업의 대부분이 여기에 있습니다.
PB13 하나만 봐도 TIM1_CH1N, SPI2_SCK, USART3_CTS, FDCAN2_TX, ETH1_TXD1, UART5_TX 중 무엇이든 될 수 있습니다. 핀리스트 엑셀의 AF0~AF14 열이 바로 이 선택지 표입니다.
STM32_PINMUX 해부
STM32_PINMUX('B', 13, AF14)
│ │ │
│ │ └─ 기능 선택: AF0~AF15 | GPIO | ANALOG | RSVD
│ └─────── 핀 번호 (0~15)
└───────────── 포트 (A~K, Z)
매크로는 include/dt-bindings/pinctrl/stm32-pinfunc.h에 정의돼 있습니다. AF 외의 특수 값 세 개를 알아둘 필요가 있습니다.
| 값 | 용도 |
|---|---|
GPIO |
범용 입출력. 단, GPIO로 쓸 핀은 보통 pinctrl 그룹에 넣지 않고 gpios = <&gpiob 10 ...>로 직접 참조합니다. |
ANALOG |
아날로그 입력, 또는 절전 상태에서 핀을 하이임피던스로 놓을 때. sleep 그룹이 전부 ANALOG인 이유입니다. |
RSVD |
이 핀을 Cortex-M4 코어에 양보. A7 리눅스는 손대지 않습니다. |
핀리스트 엑셀 → 매크로
변환은 기계적입니다. 엑셀 AF 열의 번호가 곧 매크로의 AF 번호입니다.
| 핀 | 엑셀에서 찾은 열 | 매크로 |
|---|---|---|
| PB13 | AF14 열 = UART5_TX |
STM32_PINMUX('B', 13, AF14) |
| PB6 | AF4 열 = I2C1_SCL |
STM32_PINMUX('B', 6, AF4) |
| PC7 | AF2 열 = TIM3_CH2 |
STM32_PINMUX('C', 7, AF2) |
그룹 정의와 연결
핀은 그룹 단위로 정의해서 노드에서 참조합니다. 두 곳을 같이 봐야 합니다.
① pinctrl dtsi — 그룹 정의
&pinctrl {
uart5_pins_sc6: uart5-sc6-0 {
pins1 {
pinmux = <STM32_PINMUX('B', 13, AF14)>; /* TX */
bias-disable;
drive-push-pull;
slew-rate = <0>;
};
pins2 {
pinmux = <STM32_PINMUX('B', 12, AF14)>; /* RX */
bias-disable;
};
};
};
출력 핀(pins1)과 입력 핀(pins2)을 나누는 이유는 전기적 속성이 다르기 때문입니다. 출력에는 드라이브 방식과 슬루레이트를 주고, 입력에는 풀업/풀다운만 지정합니다. 자주 쓰는 속성은 bias-disable / bias-pull-up / bias-pull-down, drive-push-pull / drive-open-drain, slew-rate = <0~3>입니다. I2C는 반드시 drive-open-drain입니다.
② 보드 dts — 노드에서 참조
&uart5 {
pinctrl-names = "default", "sleep";
pinctrl-0 = <&uart5_pins_sc6>; // "default"에 대응
pinctrl-1 = <&uart5_sleep_pins_sc6>; // "sleep"에 대응
status = "okay";
};
pinctrl-names의 순서가 pinctrl-0, pinctrl-1, pinctrl-2 …와 인덱스로 짝지어집니다. 이름은 관례적으로 default(동작), sleep(절전), idle(런타임 PM)을 씁니다. default는 probe 시점에 커널이 자동으로 적용합니다.
STM32MP1 함정: PZ 포트
GPIOA~GPIOK는 &pinctrl(pin-controller@50002000)에 있지만, GPIOZ는 &pinctrl_z(pin-controller-z@54004000)라는 별도 컨트롤러입니다. PZ 핀 그룹을 &pinctrl 안에 넣으면 컴파일은 통과해도 동작하지 않습니다. SPI1이나 I2C4/I2C6처럼 PZ를 쓰는 버스를 만질 때 반드시 확인하세요.
/workflow
핀리스트에서 DTS까지, 5단계
실제 순서입니다. SC6_0에서 UART5를 PB6/PB5(벤더 기본)에서 PB13/PB12(우리 배선)로 옮기는 작업을 예로 따라갑니다.
-
신호와 핀을 확정한다
핀리스트에서 해당 신호가 어느 핀에 배선됐는지 읽습니다. 회로도(schematic)와 반드시 교차 확인하세요. 핀리스트는 의도이고 회로도가 사실입니다.
핀 19 PB13(U5_TX) 핀 21 PB12(U5_RX) -
AF 번호를 찾는다
같은 행에서 원하는 기능이 몇 번째 AF 열에 있는지 봅니다. 이때 그 기능이 그 핀에 존재하는지도 함께 검증됩니다. 없으면 배선이 잘못된 것입니다.
PB13 행 → AF14 열 = UART5_TX ✓ PB12 행 → AF14 열 = UART5_RX ✓ -
기존 그룹을 확인한다 — 재사용할지, 새로 만들지
이름이 맞다고 내용까지 맞는 게 아닙니다. 반드시 열어서 실제 핀을 확인하세요.
$ grep -A6 "uart5_pins_a:" arch/arm/boot/dts/stm32mp157-ya157c-pinctrl.dtsi uart5_pins_a: uart5-0 { pins1 { pinmux = <STM32_PINMUX('B', 6, AF12)>; /* 우리 보드는 PB6이 I2C1_SCL */벤더 그룹은 PB6/PB5였고 우리 보드에서 그 두 핀은 각각 I2C1_SCL과 LCD 리셋입니다. 그대로 쓰면 LCD가 죽습니다. 새 그룹이 필요합니다.
-
그룹을 정의하고 노드에 연결한다
default와 sleep을 짝으로 만듭니다. sleep은 같은 핀을
ANALOG로 놓는 것이 관례입니다./* stm32mp157-sc6-pinctrl.dtsi */ &pinctrl { uart5_pins_sc6: uart5-sc6-0 { pins1 { pinmux = <STM32_PINMUX('B', 13, AF14)>; bias-disable; drive-push-pull; slew-rate = <0>; }; pins2 { pinmux = <STM32_PINMUX('B', 12, AF14)>; bias-disable; }; }; uart5_sleep_pins_sc6: uart5-sleep-sc6-0 { pins { pinmux = <STM32_PINMUX('B', 13, ANALOG)>, <STM32_PINMUX('B', 12, ANALOG)>; }; }; }; /* 보드 dts */ &uart5 { pinctrl-names = "default", "sleep"; pinctrl-0 = <&uart5_pins_sc6>; pinctrl-1 = <&uart5_sleep_pins_sc6>; status = "okay"; }; -
충돌을 검사한다
가장 많이 건너뛰고 가장 많이 물리는 단계입니다. 세 가지를 봅니다.
- 핀 중복 — 활성화된 두 그룹이 같은 핀을 잡으면 나중 드라이버가
-EBUSY로 probe 실패합니다. - EXTI 라인 중복 — 아래 별도 설명.
- 버려진 그룹 — 옛 그룹을 참조하는 노드가 남아 있지 않은지.
EXTI 라인 규칙
STM32의 외부 인터럽트는 핀 번호마다 포트를 하나만 고를 수 있습니다. EXTI3에는 PA3·PB3·PC3… 중 딱 하나만 연결됩니다. 포트가 달라도 핀 번호가 같으면 경쟁합니다.
EXTI 후보 핀 상태 3 PG3 USB_IRQ · PI3 BH_WAKE · PA3 SD_DET 충돌 7 PB7 LINT · PG7 Q_INT 충돌 9 PB9 TINT 단독 인터럽트가 아니라 폴링이나 단순 GPIO 입력으로 쓰는 핀은 EXTI를 점유하지 않으므로 계산에서 빼도 됩니다. 예를 들어 SD 카드 검출은
broken-cd(폴링)로 돌리면 EXTI3에서 빠집니다. 그래도 남는 충돌은 DTS로 해결할 수 없고 하드웨어를 바꿔야 합니다. - 핀 중복 — 활성화된 두 그룹이 같은 핀을 잡으면 나중 드라이버가
/tooling
이걸 다 손으로 해야 하나 — CubeMX의 자리
STM32CubeMX는 MPU 모드에서 디바이스 트리를 생성합니다. 다만 MCU에서처럼 “Generate 누르면 끝”은 되지 않고, 채워주는 영역이 분명하게 제한됩니다.
이 BSP에도 흔적이 있습니다
MYiR 핀맵 파일의 그룹 이름 접미사를 세어보면 세 계보가 섞여 있습니다. _mx가 CubeMX의 생성 규칙입니다.
| 접미사 | 개수 | 출처 |
|---|---|---|
_mx |
7 | CubeMX 생성 — uart7_pins_mx, uart8_pins_mx, tim4_pwm_pins_mx … |
_myir |
5 | MYiR 수작업 |
_a 등 |
111 | ST 레퍼런스에서 복사·수정 |
전체 123개 그룹 중 CubeMX 생성분은 7개입니다. 벤더조차 일부만 도구로 뽑고 나머지는 손으로 썼다는 뜻이고, 이 비율이 MPU 개발의 현실을 그대로 보여줍니다.
이 프로젝트에는 .ioc가 없습니다
커널 트리에도 작업 폴더에도 CubeMX 프로젝트 파일(.ioc)이 없습니다. 생성물만 남고 원본이 사라진 상태라 재생성이 불가능합니다. 도구를 쓰려면 CubeMX 프로젝트를 새로 만들어야 하고, 만든 뒤에는 .ioc를 반드시 형상관리에 넣으세요.
경계선
CubeMX가 해주는 것
- 핀아웃 GUI — 페리페럴을 고르면 가능한 핀만 보여주고 AF 번호를 자동 계산
- 핀 중복 검출 — 같은 핀을 두 기능에 배정하면 즉시 막음
- 클럭 트리 설정 — 손으로 하기 가장 어려운 부분
- TF-A / U-Boot / 커널 3종 DT 스켈레톤 (
*-mx.dtsi) - Cortex-M4 펌웨어 프로젝트 동시 생성
CubeMX가 못 해주는 것
- 보드 레벨 노드 전부 — I2C 주소
0x38의 터치 칩, 패널compatible과 타이밍,gpio-leds,mmc-pwrseq, 레귤레이터 트리 - SoM 지식 — 모듈이 DDR·NAND·PHY에 어떤 핀을 고정으로 쓰는지 모름
- 벤더 BSP 병합 — ST 레퍼런스 기준으로 새로 뱉으므로 결국 수작업 머지
핵심은 첫 항목입니다. CubeMX는 “핀에 I2C1을 배정했다”까지만 압니다. 그 버스 끝에 무엇이 붙어 있는지는 모릅니다. 그런데 DTS 작업량의 대부분이 바로 그 부분입니다.
왜 MCU만큼 자동화되지 않는가
STM32F4·H7 같은 MCU에서는 CubeMX가 초기화 코드를 통째로 생성하고 그게 표준 워크플로입니다. MPU + 리눅스는 구조가 다릅니다.
| STM32 MCU (F4/H7) | STM32MP1 + Linux | |
|---|---|---|
| 생성기 출력 | 초기화 C 코드 전체 | pinmux · 클럭 DT 조각 |
| 초기화 주체 | 생성된 코드 | 커널 드라이버 (이미 존재) |
| 산출물의 성격 | “이렇게 초기화하라” | “이 보드엔 뭐가 붙어있다” |
| GUI 표현 가능 범위 | 대부분 | pinmux · 클럭까지만 |
DT가 담는 정보 대부분(칩 종류, 슬레이브 주소, 리셋 순서, 패널 타이밍)은 회로도를 읽어야 나오는 서술이라 GUI로 입력받을 형식이 마땅치 않습니다. 임베디드 리눅스 쪽에 아직도 쓸 만한 DTS 전용 GUI 에디터가 없는 것도 같은 이유입니다.
실속 있는 사용법: 생성기가 아니라 검증기로
전면 도입보다 핀 배치 검증 도구로 쓰는 편이 투자 대비 효과가 큽니다.
- 보드의 핀 할당을 CubeMX에 입력 → 핀 중복이 자동으로 걸러집니다
- EXTI는 GPIO 설정 화면에서 라인 번호별로 묶여 보이므로, 앞서 본 PG3/PI3/PA3 같은 충돌을 육안으로 빨리 찾습니다
- AF 번호를 GUI가 채워주니 데이터시트 열을 세다 생기는 실수가 없어집니다
- 생성된
*-mx.dtsi에서 pinctrl 그룹만 발췌해 가져오고, 디바이스 노드는 손으로
버전 스큐 주의
이 BSP는 커널 5.4.31 / OpenSTLinux 구버전 기준인데, 최신 CubeMX는 커널 6.x 세대 ecosystem을 타겟으로 생성합니다. 생성물을 통째로 붙이면 바인딩이 맞지 않을 수 있습니다. pinctrl 그룹 정도만 발췌하는 것이 안전하고, 그마저도 STM32_PINMUX 값을 눈으로 확인하고 넣으세요.
/patterns
자주 쓰는 노드 패턴
브링업에서 반복해서 쓰는 형태들입니다. GPIO 극성 상수는 <dt-bindings/gpio/gpio.h>, 인터럽트 타입은 <dt-bindings/interrupt-controller/irq.h>에서 옵니다.
GPIO 출력 — LED
led {
compatible = "gpio-leds";
run {
label = "run";
gpios = <&gpioa 14 GPIO_ACTIVE_HIGH>;
linux,default-trigger = "heartbeat";
default-state = "off";
};
};
→ /sys/class/leds/run/brightness로 제어됩니다.
GPIO 입력 + 인터럽트 — 버튼·INT 신호
keys {
compatible = "gpio-keys";
din1 {
label = "din1";
linux,code = <KEY_F1>;
interrupt-parent = <&gpioh>;
interrupts = <6 IRQ_TYPE_EDGE_BOTH>; // PH6 → EXTI6
};
};
interrupt-parent가 포트, interrupts의 첫 값이 핀 번호입니다. 이 번호가 곧 EXTI 라인 번호이므로 충돌 검사 대상입니다.
I2C 디바이스
&i2c1 {
pinctrl-names = "default", "sleep";
pinctrl-0 = <&i2c1_pins_sc6>;
pinctrl-1 = <&i2c1_sleep_pins_sc6>;
clock-frequency = <400000>;
status = "okay";
/delete-property/ dmas;
/delete-property/ dma-names;
touchscreen@38 {
compatible = "edt,edt-ft5x06";
reg = <0x38>; // I2C 슬레이브 주소
interrupt-parent = <&gpiob>;
interrupts = <9 IRQ_TYPE_EDGE_FALLING>; // PB9 = TINT
reset-gpios = <&gpiob 10 GPIO_ACTIVE_LOW>; // PB10 = TRST
touchscreen-size-x = <800>;
touchscreen-size-y = <480>;
status = "okay";
};
};
SPI 디바이스 — 유저 공간에서 접근
&spi1 {
pinctrl-names = "default", "sleep";
pinctrl-0 = <&spi1_pins_a>;
pinctrl-1 = <&spi1_sleep_pins_a>;
cs-gpios = <&gpioz 3 0>; // PZ3 = CS, GPIO로 제어
status = "okay";
spidev@0 {
compatible = "rohm,dh2228fv"; // spidev 바인딩
reg = <0>; // cs-gpios 배열의 인덱스
spi-max-frequency = <20000000>;
};
};
spidev compatible
최신 커널은 compatible = "spidev"를 거부하고 경고를 냅니다(“buggy DT”). 실제 디바이스 바인딩을 쓰거나, 드라이버가 없으면 rohm,dh2228fv 같은 등록된 문자열을 관례적으로 씁니다.
PWM 백라이트
부모 타이머를 켜는 것을 잊지 마세요.
&timers3 {
status = "okay"; // ← 부모부터
pwm3: pwm {
pinctrl-names = "default", "sleep";
pinctrl-0 = <&pwm3_pins_a>; // PC7 AF2 = TIM3_CH2
pinctrl-1 = <&pwm3_sleep_pins_a>;
status = "okay";
};
};
backlight: backlight {
compatible = "pwm-backlight";
pwms = <&pwm3 1 5000000>; // 채널 인덱스, 주기(ns)
brightness-levels = <0 8 16 32 64 128 255>;
default-brightness-level = <5>;
enable-gpios = <&gpioc 6 GPIO_ACTIVE_HIGH>; // PC6 = BL_EN
};
pwms의 첫 인자는 0부터 세는 채널 인덱스입니다. TIM3_CH2는 인덱스 1입니다. 여기서 한 칸 어긋나는 실수가 흔합니다.
고정 레귤레이터
vcc_2v5: regulator-2v5 {
compatible = "regulator-fixed";
regulator-name = "vcc_2v5";
regulator-min-microvolt = <2500000>;
regulator-max-microvolt = <2500000>;
regulator-always-on;
};
전원 공급이 필요한 노드에서 vmmc-supply = <&vcc_2v5> 형태로 참조합니다. 드라이버가 *-supply를 요구하는데 없으면 probe가 지연되거나 실패합니다.
ADC 채널
&adc {
vdd-supply = <&vdd>;
vdda-supply = <&vdd>;
vref-supply = <&vrefbuf>; // 기준전압원 ← 반드시 실제 회로와 일치
status = "okay";
adc1: adc@0 {
st,adc-channels = <0 1>; // ANA0, ANA1
st,min-sample-time-nsecs = <10000>;
status = "okay";
};
};
ANA0/ANA1은 전용 아날로그 핀이라 pinmux가 필요 없습니다. 일반 GPIO를 ADC로 쓸 때만 ANALOG 그룹을 만듭니다. vref-supply는 뒤의 함정 항목에서 다시 다룹니다.
/build
빌드와 보드 반영
먼저: 이 환경은 그대로 빌드하면 실패합니다
SDK 환경 스크립트를 그대로 source하면 커널 호스트 툴 컴파일 단계에서 죽습니다. 원인과 해결은 바로 아래 0단계에 있습니다. 실측으로 확인한 문제입니다.
0. 환경 충돌 해결 — SDK와 호스트의 세대 차이
| 버전 | 시기 | |
|---|---|---|
| 호스트 | Ubuntu 24.04.4 · glibc 2.39 · gcc 13.3 | 2024 |
| SDK | OpenSTLinux 3.1-snapshot · gcc 9.3 | 2020 |
SDK 환경 스크립트는 PATH 맨 앞에 자기 호스트 툴 디렉터리를 넣습니다. 그러면 커널이 scripts/basic/fixdep 같은 호스트 툴을 빌드할 때, 시스템 gcc 13이 SDK의 2020년산 ld를 호출하게 됩니다. gcc 13의 LTO 플러그인은 GLIBC_2.33 이상을 요구하는데 SDK가 들고 있는 libc는 그보다 낮아서 이렇게 끝납니다.
ld: liblto_plugin.so: error loading plugin:
libc.so.6: version `GLIBC_2.33' not found
collect2: error: ld returned 1 exit status
make[3]: *** [scripts/Makefile.host:107: scripts/basic/fixdep] Error 1
동시에 반대 방향 문제도 있습니다. flex와 bison이 시스템에는 없고 SDK에만 있습니다. 그래서 SDK를 아예 안 쓰는 것도 답이 아닙니다.
해결은 한 줄입니다. SDK를 source한 다음에 시스템 경로를 앞으로 되돌립니다.
. /opt/st/myir/3.1-snapshot/environment-setup-cortexa7t2hf-neon-vfpv4-ostl-linux-gnueabi
unset LDFLAGS CFLAGS CPPFLAGS CXXFLAGS
export PATH=/usr/local/bin:/usr/bin:/bin:$PATH
이러면 역할이 정확히 갈립니다.
| 도구 | 어디 것을 쓰나 | 이유 |
|---|---|---|
ld, gcc (호스트) |
시스템 /usr/bin |
호스트 툴 빌드용. 최신이어야 함 |
flex, bison |
SDK | 시스템에 미설치 |
arm-ostl-linux-gnueabi-* |
SDK | 크로스 컴파일러. 이게 본체 |
더 깔끔한 대안
sudo apt install flex bison으로 시스템에 설치하면 PATH 재정렬 없이도 됩니다. 다만 위 방법은 시스템을 건드리지 않으므로 먼저 시도해볼 만합니다.
1. 빌드 스크립트로 고정
매번 손으로 치면 실수합니다. ~/work/kbuild.sh로 저장해두고 인자만 바꿔 쓰세요.
#!/bin/bash
set -e
. /opt/st/myir/3.1-snapshot/environment-setup-cortexa7t2hf-neon-vfpv4-ostl-linux-gnueabi
unset LDFLAGS CFLAGS CPPFLAGS CXXFLAGS
export PATH=/usr/local/bin:/usr/bin:/bin:$PATH # 호스트 툴 우선
cd ~/work/myir-st-linux
make ARCH=arm CROSS_COMPILE=arm-ostl-linux-gnueabi- \
O=$HOME/work/build "$@"
chmod +x ~/work/kbuild.sh
2. Makefile에 dtb 타겟 등록
새 .dts는 arch/arm/boot/dts/Makefile에 넣어야 빌드됩니다. dtb-$(CONFIG_ARCH_STM32) += \ 목록의 기존 ya157c 항목 근처(1011행 부근)에 추가합니다.
stm32mp157c-ya157c-v2.dtb \
stm32mp157c-ya157c-hdmi-v2.dtb \
stm32mp157c-ya157c-lcd-v2.dtb \
stm32mp157a-sc6-v1.dtb \
백슬래시 이어쓰기라 목록 마지막 줄에는 \를 붙이지 않습니다. 중간에 넣는 것이 안전합니다.
3. dtb 빌드
커널 전체를 다시 빌드할 필요 없습니다. dtbs 타겟만 돌리면 됩니다.
~/work/kbuild.sh dtbs -j$(nproc)
결과물은 ~/work/build/arch/arm/boot/dts/에 떨어집니다.
stm32mp157c-ya157c-v2.dtb 69,993 B
stm32mp157c-ya157c-lcd-v2.dtb 70,402 B
stm32mp157c-ya157c-hdmi-v2.dtb 70,477 B
stm32mp15xc-ya157c-t.dtb 70,318 B
...
한 파일만 빠르게
문법 에러만 잡을 때는 대상 dtb 하나만 지정하면 됩니다.
~/work/kbuild.sh stm32mp157a-sc6-v1.dtb
4. 커널·모듈이 필요할 때
DTS만 고쳤다면 dtbs로 충분합니다. 드라이버를 켜고 끄는 등 .config를 건드렸다면 그때 전체를 빌드합니다.
# 설정 변경
~/work/kbuild.sh menuconfig
# 커널 이미지 + dtb + 모듈
~/work/kbuild.sh zImage dtbs modules -j$(nproc)
# 모듈을 한곳에 모으기
~/work/kbuild.sh INSTALL_MOD_PATH=$HOME/work/mod_out modules_install
보드 defconfig가 필요하면 myc-ya157c_defconfig가 arch/arm/configs/에 있습니다. 다만 ~/work/build/.config가 이미 있다면 덮어쓰지 마세요 — 기존 설정이 날아갑니다.
5. 보드에 반영
부트로더 구성에 따라 위치가 다릅니다. 보드에서 현재 쓰는 dtb 이름을 먼저 확인하고 같은 이름으로 덮어씁니다.
# 보드에서 현재 로드된 dtb 확인
cat /proc/device-tree/model
# 보드로 복사 (부팅 파티션 마운트 지점은 환경에 따라 다름)
scp stm32mp157a-sc6-v1.dtb root@<board-ip>:/boot/
sync && reboot
U-Boot 환경변수(fdtfile 등)가 dtb 파일명을 지정하는 경우, 이름을 바꿨다면 부트로더 쪽도 함께 수정해야 합니다.
/debug
확인과 디버깅
“DTS를 고쳤는데 안 된다”의 원인은 대개 셋 중 하나입니다. dtb가 안 바뀌었거나, 노드가 disabled거나, 핀이 다른 드라이버에 잡혀 있거나.
실제 적용된 트리 읽기
/proc/device-tree는 커널이 실제로 받은 DT를 디렉터리로 노출합니다. 소스가 아니라 결과를 보는 것이므로 가장 신뢰할 수 있습니다.
cat /proc/device-tree/model
# 특정 노드의 status 확인
cat /proc/device-tree/soc/i2c@40013000/status
# 전체 구조 훑기
find /proc/device-tree -maxdepth 2 -type d
dtb 역컴파일 — 병합 결과 전체 보기
include가 여러 겹이라 최종 값이 무엇인지 소스만 봐서는 알기 어렵습니다. dtb를 dts로 되돌리면 병합된 최종 형태가 나옵니다. 덮어쓰기가 의도대로 됐는지 확인하는 가장 확실한 방법입니다.
# 커널이 빌드한 dtc를 쓰는 편이 버전이 맞습니다
DTC=~/work/build/scripts/dtc/dtc
$DTC -I dtb -O dts -o /tmp/final.dts \
~/work/build/arch/arm/boot/dts/stm32mp157a-sc6-v1.dtb
# 보드에서 실행 중인 트리를 그대로 덤프
dtc -I fs -O dts /proc/device-tree > /tmp/running.dts
역컴파일 시 unit_address_vs_reg 같은 경고가 여럿 나오는 것은 정상입니다. 원본 DTS에 이미 있던 스타일 경고이고 동작과는 무관합니다.
핀이 실제로 누구에게 잡혔는지
pinctrl debugfs가 이 질문에 직접 답합니다. 핀 충돌을 추적할 때 가장 먼저 보는 곳입니다.
mount -t debugfs none /sys/kernel/debug # 안 되어 있으면
ls /sys/kernel/debug/pinctrl/
# soc:pin-controller@50002000 ← GPIOA~K
# soc:pin-controller-z@54004000 ← GPIOZ
# 핀별 소유 드라이버
cat /sys/kernel/debug/pinctrl/soc:pin-controller@50002000/pinmux-pins
# 그룹 정의 목록
cat /sys/kernel/debug/pinctrl/soc:pin-controller@50002000/pingroups
출력에서 pin 29 (PB13): 40011000.serial (GPIO UNCLAIMED) function uart5 group uart5-sc6-0 같은 줄을 찾으면 됩니다. 기대한 드라이버 이름이 아니면 다른 노드가 먼저 가져간 것입니다.
GPIO 상태
cat /sys/kernel/debug/gpio
probe 실패 추적
dmesg | grep -iE "probe|fail|error|-EBUSY|deferred"
# 드라이버가 디바이스를 잡았는지 (심볼릭 링크 존재 여부)
ls -l /sys/bus/i2c/devices/
ls -l /sys/bus/platform/drivers/stm32-usart/
-EPROBE_DEFER는 실패가 아니라 “의존 리소스가 아직 준비 안 됨”입니다. 보통 레귤레이터나 GPIO 컨트롤러를 기다리는 중이고, 끝까지 해소되지 않으면 *-supply 참조가 잘못된 경우가 많습니다.
버스별 확인
i2cdetect -y -r 0 # i2c-0의 슬레이브 스캔
ls /dev/spidev*
ls /dev/ttySTM*
cat /sys/bus/iio/devices/iio:device0/in_voltage0_raw # ADC ch0
/pitfalls
흔한 함정
아래 표에서 무증상은 컴파일도 부팅도 정상이라 로그로 잡히지 않는 부류입니다. 가장 오래 잡아먹습니다.
| 증상 | 원인 | 확인법 |
|---|---|---|
| 고쳤는데 아무 변화 없음 | dtb를 안 바꿨거나 U-Boot가 다른 dtb를 로드 | /proc/device-tree/model 확인 |
| 드라이버가 안 뜸 | 부모 노드가 disabled (타이머·MFD에서 빈발) | 역컴파일한 dts에서 부모 status |
-EBUSY로 probe 실패 |
다른 활성 노드가 같은 핀을 선점 | pinmux-pins |
| 인터럽트 하나만 동작 | EXTI 라인 번호 중복 (포트 달라도 충돌) | 핀 번호끼리 비교, /proc/interrupts |
| probe가 무한 지연 | -EPROBE_DEFER — *-supply 참조 오류 |
dmesg \| grep defer |
| PZ 핀만 동작 안 함 | 그룹을 &pinctrl_z가 아닌 &pinctrl에 정의 |
pinctrl 인스턴스 두 개 비교 |
| 무증상 ADC 값이 일정 비율로 틀림 | vref-supply가 실제 기준전압과 불일치 |
회로도 대조 — 로그로는 못 잡음 |
| 무증상 엉뚱한 핀이 토글됨 | 그룹 이름만 보고 재사용, 내용 미확인 | 그룹 정의를 직접 열어보기 |
| 무증상 시리얼 포트 번호가 바뀜 | aliases의 serialN 변경 |
ls /dev/ttySTM* |
실제 사례 — 로그 없이 32% 틀리는 ADC
YA157C BSP에서 &vrefbuf는 이미 2.5V로 설정되어 status = "okay"인데, &adc의 vref-supply는 3.3V 고정 레귤레이터 &vddref를 가리키고 있습니다. 보드가 실제로 2.5V 기준전압을 쓴다면 ADC는 에러 하나 없이 모든 값을 3.3/2.5 = 1.32배로 계산합니다.
수정은 한 줄이지만, 이런 종류는 회로도와 대조하지 않으면 영원히 발견되지 않습니다.
- vref-supply = <&vddref>;
+ vref-supply = <&vrefbuf>;
벤더 BSP를 신뢰하지 말 것
평가보드 DTS는 평가보드 배선입니다. 그룹 이름이 uart5_pins_a라고 해서 그 안이 우리 보드의 UART5 핀일 이유가 없습니다. 재사용하기 전에 반드시 grep으로 열어서 실제 STM32_PINMUX 값을 확인하세요. 이름은 관례일 뿐 계약이 아닙니다.
/checklist
보드 브링업 체크리스트
빨간 표시는 무증상 오류로 이어지는 항목입니다.
- 핀리스트와 회로도를 교차 확인했다 — 핀리스트는 의도, 회로도가 사실
- 재사용하는 pinctrl 그룹을 전부 열어서 실제
STM32_PINMUX값을 확인했다 - PZ 포트 그룹은
&pinctrl_z아래에 정의했다 - 인터럽트로 쓰는 핀들의 번호가 서로 겹치지 않는다 (EXTI)
- 핀 배치를 CubeMX에 넣어 중복·EXTI 충돌을 교차 검증했다 —
.ioc는 형상관리에 - MFD·타이머는 부모 노드부터
status = "okay" *-supply참조가 실제 전원 회로와 일치한다 (특히vref-supply)- 안 쓰는 벤더 노드를
disabled로 껐다 — 핀 선점 방지 aliases의serialN번호가 유저 공간 기대와 맞는다Makefile에 dtb 타겟을 등록했다- 역컴파일(
dtc -I dtb -O dts)로 병합 결과를 눈으로 확인했다 - 보드에서
pinmux-pins로 핀 소유자를 확인했다 dmesg에defer가 남아 있지 않다
SC6_0 / MYC-YA157C-V2 Linux 5.4.31 · arch/arm STM32MP157AAC
댓글남기기