2026-04-29

Linux gpio-74x164 에 lines-initial-states 지원 추가 — 74HC595 chain 의 부팅 글리치 잡기 (개정판)

74HC595 / 74LVC594 같은 직렬-입력 병렬-출력 시프트 레지스터를 SPI 로 cascade 해서 GPIO 32개를 만들어 쓰는 보드를 다뤄본 적이 있으면 부팅 직후 짧게 깜빡이는 LED 한 줄을 본 기억이 있을 것이다. active-low 인디케이터를 한 칩에 모아둔 보드라면 더 또렷하다 — 부팅이 시작되자마자 모든 LED 가 한 번 전부 켜졌다가 user space 가 default-state 를 적용하면서 사라진다.

문제 — 74HC595 의 power-on 출력이 정의되어 있지 않다

74HC595 패밀리는 push-pull 출력 전용이고 read-back 경로가 없다. 그래서 Linux 의 gpio-74x164 드라이버는 chain 전체의 상태를 자기 메모리(chip->buffer) 로만 추적하고, 누가 한 비트를 set 하면 chain 전체를 한꺼번에 SPI 로 다시 쓴다 (__gen_74x164_write_config()). 그런데 chip->buffer 는 probe 시점에 devm_kzalloc() 에서 갓 나온 0x00 으로 시작한다. 결과적으로 chain 의 첫 번째 SPI write 는 항상 모든 출력 = 0 이다. active-low LED 보드에서는 그게 곧 "전부 켬" 이다.

이 글리치는 user space 의 gpio-leds default-state 가 적용되기 전, 즉 부팅 후 보통 수십~수백 ms 동안 보인다. 잠깐이라 무시할 만해 보이지만, 보드가 양산 운영 상태로 올라올 때마다 매번 그렇다는 게 문제다. CCTV 표시등이라면 짧은 점등이 사용자 신호로 잡힐 여지가 있다.

왜 gpio-hog 로는 안 되나

익숙한 해결책으로 DT 의 gpio-hog 가 떠오른다. 그런데 이 보드 / 이 드라이버 조합에는 세 가지 이유로 맞지 않는다.

  1. consumer 와 배타적이다. 이 보드는 chain 의 모든 라인을 gpio-leds 가 이미 claim 하고 있다. gpio-hog 는 hog 가 라인을 claim 하므로 leds 와 동시 사용이 불가능하다.
  2. 순차 적용으로는 글리치를 막을 수 없다. hog 는 라인별로 한 번씩 set 호출이 들어간다. __gen_74x164_write_config() 가 매번 chain 전체를 다시 쓰므로, hog 4개라면 full-chain SPI write 4회. 첫 hog 가 set 되기 전 0x00 한 번이 무조건 먼저 나간다 — 글리치 자체는 그대로 남는다.
  3. 의도와도 다르다. gpio-hog 의 binding 문서는 "driver 가 claim 하지 않는 line" 용도로 못 박혀 있다. 이 chain 의 라인은 driver(gpio-74x164) + consumer(gpio-leds) 가 모두 claim 한다.

그러므로 글리치를 진짜로 없애려면, driver 가 chain 의 첫 SPI write 전에 buffer 를 의도한 패턴으로 미리 채워둬야 한다. 그리고 그 패턴은 보드별로 다르므로 device tree 가 자연스러운 선언 위치다.

v1 — 새 property 'registers-default' 도입

처음에 보낸 v1 시리즈는 새 property registers-default (uint8-array, chip 별 한 바이트씩) 를 binding 에 추가하고 driver 가 이를 읽어 buffer 에 채우는 방식이었다.

gpio-leds-spi {
    compatible = "fairchild,74hc595";
    registers = <4>;
    registers-default = /bits/ 8 <0xff 0xff 0xff 0xff>;
    /* ... */
};

주말 동안 checkpatch.pl --strict, yamllint, make dt_binding_check, dt-validate 까지 모두 깨끗히 통과시키고 scripts/get_maintainer.pl 결과대로 GPIO/DT-bindings maintainer 들에게 송신했다.

v1 review — 두 가지 지적과 더 좋은 길

Krzysztof Kozlowski 가 두 가지를 지적했다.

  1. 왜 이 드라이버만 DT 에서 default 가 필요한가 — driver 에서 zero 안 쓰든지, 아니면 hog 를 제대로 처리하든지 둘 중 하나로 가는 게 맞지 않나.
  2. registers-default 는 vendor-prefix 가 없는 generic property 다 — vendor-specific binding 에 generic property 를 추가하는 건 can of worms 의 시작이다.

1번에는 위에서 정리한 74HC595 의 하드웨어 특성 + gpio-hog 가 맞지 않는 세 가지 이유로 답장을 했다. 그런데 답장 직후 Linus Walleij 가 다른 angle 의 제안을 줬다 — 이미 비슷한 binding 이 존재한다. nxp,pcf8575lines-initial-states property 가 정확히 같은 의도(부팅 시 라인별 초기값)로 쓰이고 있고, generic 한 위치에 documented 되어 있다는 것. 그러면 굳이 새 property 를 만들 필요 없이 그것을 재사용하면 된다.

이 한 마디가 v1 review 의 두 지적을 한 번에 풀어준다.

  • Krzysztof 의 vendor-prefix 우려 → 이미 nxp,pcf8575 binding 에 documented 되어 있는 property 라 새로 만드는 것이 아니다.
  • Krzysztof 의 "왜 이 driver 만 필요한가" 우려 → 이미 다른 driver(pcf8575) 도 같은 패턴을 쓰고 있어 이 driver 만 특별한 것이 아니게 된다.

review 가 더 좋은 방향을 짚어준 사례다. 코드를 더 짧고 더 일반적인 쪽으로 끌어줬다.

v2 — lines-initial-states 재사용

v2 는 새 property 도입을 전부 들어내고, lines-initial-states bitmask 를 재사용하는 방향으로 다시 짰다.

Binding YAML — property 한 줄 추가

properties:
  ...
  lines-initial-states:
    description: |
      Bitmask specifying the boot-time output state of each GPIO line.
      Bit N corresponds to GPIO line N. Since this device is output-only,
      bit=0 drives the line low and bit=1 drives it high. Lines beyond
      the bitmask come up low.
    $ref: /schemas/types.yaml#/definitions/uint32

examples:
  - |
    gpio-leds-spi {
        compatible = "fairchild,74hc595";
        registers = <4>;
        lines-initial-states = <0xff00ff00>;
        /* line 0..7 low, line 8..15 high, line 16..23 low, line 24..31 high */
    };

Driver — probe 에 if 블록 하나 추가

@@ -112,7 +112,7 @@ static int gen_74x164_probe(struct spi_device *spi)
 {
     struct device *dev = &spi->dev;
     struct gen_74x164_chip *chip;
-    u32 nregs;
+    u32 nregs, init_state;
     int ret;

@@ -134,6 +134,21 @@ static int gen_74x164_probe(struct spi_device *spi)

     chip->registers = nregs;

+    /*
+     * Optionally seed the chain with a board-specified pattern so the
+     * outputs come up in a known state on the first SPI write. The
+     * property follows the nxp,pcf8575 convention where bit N maps to
+     * GPIO line N. On this output-only device, bit=0 drives the line
+     * low and bit=1 drives it high. The bitmask covers up to 32 lines;
+     * any further outputs come up zeroed by devm_kzalloc().
+     */
+    if (!device_property_read_u32(dev, "lines-initial-states", &init_state)) {
+        unsigned int i;
+
+        for (i = 0; i < min(nregs, 4U); i++)
+            chip->buffer[nregs - 1 - i] = (init_state >> (i * 8)) & 0xff;
+    }
+
     chip->gpiod_oe = devm_gpiod_get_optional(dev, "enable", GPIOD_OUT_LOW);

이 driver 의 buffer layout 은 마지막 chip 이 인덱스 0 에 오므로, bit-N=line-N 매핑을 유지하기 위해 buffer 인덱스를 뒤집어 채운다. 32 라인 이하 (4-chip cascade 까지) 는 곧바로 들어가고, 그 이상은 어차피 devm_kzalloc 의 0 으로 남는다. 이 보드도 32 라인 이하라 충분하다.

변경 크기

 .../bindings/gpio/fairchild,74hc595.yaml | 13 +++++++++++++
 drivers/gpio/gpio-74x164.c               | 17 ++++++++++++++++-
 2 files changed, 29 insertions(+), 1 deletion(-)

v1 (binding 22+ / driver 28+) 보다 v2 가 절반 정도다. 새 property 를 만들지 않고 기존 컨벤션을 따르니 binding 도 driver 도 줄었다.

v2 송신과 thread 유지

v2 는 --in-reply-to=<v1 cover Message-Id> 로 보내서 lkml 의 같은 thread 아래에 쌓이도록 했다. Suggested-by: Linus Walleij <linus.walleij@linaro.org> trailer 도 driver 패치에 추가했다.

git format-patch -v2 --cover-letter -2     --in-reply-to='cover.1776872453.git.happycpu@gmail.com'     -o ../outgoing
git send-email     --to='linusw@kernel.org' --to='brgl@kernel.org' ...
    ../outgoing/

v2 cover letter 의 "Changes since v1" 섹션에 위에 적은 두 review 지적 → lines-initial-states 재사용으로 한 번에 해결, 의 흐름을 짧게 정리했다. lore 에 v2 가 v1 cover 의 답신으로 잡혀 있으면 reviewer 들이 thread 를 따라가기 편하다.

마무리

이 시리즈는 단순한 코드 변경 (driver 16+ 줄, binding 13+ 줄) 인데도 review 사이클을 한 번 돌면서 더 일반적이고 더 짧은 구현으로 정리됐다. v1 의 "vendor-prefix 없는 generic property 추가" 는 통과시켜도 동작은 했겠지만, 비슷한 시나리오가 있을 다른 driver 들 (pcf8575 외에도) 이 각자 다른 이름으로 같은 의미의 property 를 만들면서 기능이 흩어졌을 것이다. v2 는 "기존에 있는 컨벤션 (lines-initial-states) 을 다른 family 의 binding 에도 documented 하기" 로 바뀌었고, 결과적으로 mainline 유지 비용이 더 적은 형태가 됐다.

upstream 송신은 패치를 받게 만드는 것이 목적이 아니라 여러 사용 사례에 잘 맞도록 정제하는 과정이라는 것을, 짧은 시리즈 한 번에 다시 확인했다.

이 변경이 mainline 에 들어오면 이 보드의 다운스트림 패치 (0002-gpio-74x164-registers-default.patch) 도 자동으로 drop 가능하다. 이 보드 외에도 비슷한 cascade 보드가 이 패치를 활용할 수 있도록, mainline 단 한 줄짜리 documented 컨벤션으로 정리되는 편이 모두에게 가성비가 좋다.

Linux gpio-74x164 에 lines-initial-states 지원 추가 — 74HC595 chain 의 부팅 글리치 잡기

74HC595 / 74LVC594 같은 직렬-입력 병렬-출력 시프트 레지스터를 SPI 로 cascade 해서 GPIO 32개를 만들어 쓰는 보드를 다뤄본 적이 있으면 부팅 직후 짧게 깜빡이는 LED 한 줄을 본 기억이 있을 거다. active-low 인디케이터를 한 칩에 모아둔 보드라면 더 또렷하다 — 부팅 시작과 동시에 모든 LED 가 한 번 전부 켜졌다가 user space 가 default-state 를 적용하면서 사라진다.

문제 — 74HC595 의 power-on 출력이 정의 안 됨

74HC595 패밀리는 push-pull 출력 전용이고 read-back 경로가 없다. 그래서 Linux 의 gpio-74x164 드라이버는 chain 전체의 상태를 자기 메모리(chip->buffer) 로만 추적하고, 누가 한 비트를 set 하면 chain 전체를 한꺼번에 SPI 로 다시 쓴다 (__gen_74x164_write_config()). 그런데 chip->buffer 는 probe 시점에 devm_kzalloc() 에서 갓 나온 0x00 으로 시작한다. 결과적으로 chain 의 첫 번째 SPI write 는 항상 모든 출력 = 0 이다. active-low LED 보드에서는 그게 곧 "전부 켬" 이다.

이 글리치가 user space 의 gpio-leds default-state 가 적용되기 전, 즉 부팅 후 보통 수십~수백 ms 동안 보인다. 잠깐이라 무시할 만해 보이지만, 보드가 production 운영 상태로 들어갈 때마다 매번 그렇다는 게 문제다. CCTV 표시등이라면 짧게 점등되는 게 사용자 신호로 잡힐 수도 있다.

왜 gpio-hog 로 안 되나

익숙한 해결책으로 DT 의 gpio-hog 가 떠오른다. 그런데 본 보드 / 본 드라이버 조합에는 세 가지 이유로 안 맞는다.

  1. consumer 와 배타다. 본 보드는 chain 의 모든 라인을 gpio-leds 가 이미 claim 하고 있다. gpio-hog 는 hog 가 라인을 claim 하므로 leds 와 동시 사용이 불가능하다.
  2. 순차 적용으로 글리치를 막을 수 없다. hog 는 라인 별로 한 번씩 set 호출이 들어간다. __gen_74x164_write_config() 가 매번 chain 전체를 다시 쓰므로, hog 4개 = full-chain SPI write 4번. 첫 hog 가 set 되기 전 0x00 한 번이 무조건 먼저 나간다 — 글리치 자체는 그대로 남는다.
  3. 의도와도 다르다. gpio-hog 의 binding 문서는 "driver 가 claim 하지 않는 line" 용도로 못 박혀 있다. 본 chain 의 라인은 driver(gpio-74x164) + consumer(gpio-leds) 가 모두 claim 한다.

그러므로 글리치를 진짜로 없애려면, driver 가 chain 의 첫 SPI write 전에 buffer 를 의도한 패턴으로 미리 채워둬야 한다. 그리고 그 패턴은 보드별로 다르므로 device tree 가 자연스러운 선언 위치다.

v1 — 새 property 'registers-default' 도입

처음에 보낸 v1 시리즈는 새 property registers-default (uint8-array, chip 별 한 바이트씩) 를 binding 에 추가하고 driver 가 이를 읽어 buffer 에 채우는 방식이었다.

gpio-leds-spi {
    compatible = "fairchild,74hc595";
    registers = <4>;
    registers-default = /bits/ 8 <0xff 0xff 0xff 0xff>;
    /* ... */
};

주말 동안 checkpatch.pl --strict, yamllint, make dt_binding_check, dt-validate 까지 모두 깨끗히 통과시키고 scripts/get_maintainer.pl 결과대로 GPIO/DT-bindings maintainer 들에게 송신했다.

v1 review — 두 가지 지적과 더 좋은 길

Krzysztof Kozlowski 가 두 가지를 지적했다.

  1. 왜 이 드라이버만 DT 에서 default 가 필요하냐 — driver 에서 zero 안 쓰든지, 아니면 hog 를 제대로 처리하든지 둘 중 하나로 가는 게 맞지 않나.
  2. registers-default 는 vendor-prefix 가 없는 generic property 다 — vendor-specific binding 에 generic property 를 추가하는 건 can of worms 의 시작이다.

1번에는 위에서 정리한 74HC595 의 하드웨어 특성 + gpio-hog 가 안 되는 세 가지 이유로 답장을 했다. 그런데 답장 직후 Linus Walleij 가 다른 angle 의 제안을 줬다 — 이미 비슷한 binding 이 있다. nxp,pcf8575lines-initial-states property 가 정확히 같은 의도(부팅 시 라인별 초기값) 로 쓰이고 있고, generic 한 위치에 documented 되어 있다는 것. 그러면 굳이 새 property 를 만들 필요 없이 그걸 재사용하면 된다.

이게 v1 review 의 두 지적을 한 번에 풀어준다.

  • Krzysztof 의 vendor-prefix 우려 → 이미 nxp,pcf8575 binding 에 documented 되어 있는 property 라 새로 만드는 게 아님.
  • Krzysztof 의 "왜 이 driver 만 필요하냐" 우려 → 이미 다른 driver(pcf8575) 도 같은 패턴을 쓰고 있어 본 driver 만 특별한 게 아니게 됨.

리뷰가 코드를 더 짧고 더 일반적인 쪽으로 끌어준 셈이다.

v2 — lines-initial-states 재사용

v2 는 새 property 도입을 전부 들어내고, lines-initial-states bitmask 를 재사용하는 방향으로 다시 짰다.

Binding YAML — 한 property 추가

properties:
  ...
  lines-initial-states:
    description: |
      Bitmask specifying the boot-time output state of each GPIO line.
      Bit N corresponds to GPIO line N. Since this device is output-only,
      bit=0 drives the line low and bit=1 drives it high. Lines beyond
      the bitmask come up low.
    $ref: /schemas/types.yaml#/definitions/uint32

examples:
  - |
    gpio-leds-spi {
        compatible = "fairchild,74hc595";
        registers = <4>;
        lines-initial-states = <0xff00ff00>;
        /* line 0..7 low, line 8..15 high, line 16..23 low, line 24..31 high */
    };

Driver — probe 에 한 if 블록 추가

@@ -112,7 +112,7 @@ static int gen_74x164_probe(struct spi_device *spi)
 {
     struct device *dev = &spi->dev;
     struct gen_74x164_chip *chip;
-    u32 nregs;
+    u32 nregs, init_state;
     int ret;

@@ -134,6 +134,21 @@ static int gen_74x164_probe(struct spi_device *spi)

     chip->registers = nregs;

+    /*
+     * Optionally seed the chain with a board-specified pattern so the
+     * outputs come up in a known state on the first SPI write. The
+     * property follows the nxp,pcf8575 convention where bit N maps to
+     * GPIO line N. On this output-only device, bit=0 drives the line
+     * low and bit=1 drives it high. The bitmask covers up to 32 lines;
+     * any further outputs come up zeroed by devm_kzalloc().
+     */
+    if (!device_property_read_u32(dev, "lines-initial-states", &init_state)) {
+        unsigned int i;
+
+        for (i = 0; i < min(nregs, 4U); i++)
+            chip->buffer[nregs - 1 - i] = (init_state >> (i * 8)) & 0xff;
+    }
+
     chip->gpiod_oe = devm_gpiod_get_optional(dev, "enable", GPIOD_OUT_LOW);

본 driver 의 buffer layout 은 마지막 chip 이 인덱스 0 에 오므로, bit-N=line-N 매핑을 유지하기 위해 buffer 인덱스를 뒤집어 채운다. 32 라인 이하 (4-chip cascade 까지) 가 바로 들어가고, 그 이상은 어차피 devm_kzalloc 의 0 으로 남는다. 본 보드도 32 라인 이하라 충분하다.

변경 크기

 .../bindings/gpio/fairchild,74hc595.yaml | 13 +++++++++++++
 drivers/gpio/gpio-74x164.c               | 17 ++++++++++++++++-
 2 files changed, 29 insertions(+), 1 deletion(-)

v1 (binding 22+ / driver 28+) 보다 v2 가 절반 정도. 새 property 안 만들고 기존 컨벤션을 따르니 binding 도 driver 도 줄었다.

v2 송신과 thread 유지

v2 는 --in-reply-to=<v1 cover Message-Id> 로 보내서 lkml 의 같은 thread 아래에 쌓이도록 했다. Suggested-by: Linus Walleij <linus.walleij@linaro.org> trailer 도 driver 패치에 추가했다.

git format-patch -v2 --cover-letter -2     --in-reply-to='cover.1776872453.git.happycpu@gmail.com'     -o ../outgoing
git send-email     --to='linusw@kernel.org' --to='brgl@kernel.org' ...
    ../outgoing/

v2 cover letter 의 "Changes since v1" 섹션에 위에 적은 두 review 지적 → lines-initial-states 재사용으로 한 번에 해결, 의 흐름을 짧게 정리했다. lore 에 v2 가 v1 cover 의 답신으로 잡혀 있으면 reviewer 들이 thread 를 따라가기 편하다.

마무리

이 시리즈는 단순한 코드 변경 (driver 16+ 줄, binding 13+ 줄) 인데도 review 사이클을 한 번 돌면서 더 일반적이고 더 짧은 구현으로 정리됐다. v1 의 "vendor-prefix 없는 generic property 추가" 는 통과시켜도 동작은 했겠지만, 비슷한 시나리오 가 있을 다른 driver 들 (pcf8575 외에도) 이 각자 다른 이름으로 같은 의미의 property 를 만들면서 fragment 됐을 거다. v2 는 "기존에 있는 컨벤션 (lines-initial-states) 을 다른 family 의 binding 에 적용 documented 하기" 로 바뀌었고, 결과적으로 mainline 유지 비용이 더 적은 형태가 됐다.

upstream 송신은 패치를 받게 만드는 게 목적이 아니라 여러 사용 사례에 잘 맞도록 정제하는 과정이라는 것을, 짧은 시리즈 한 번 더 확인했다.

이 변경은 mainline 에 들어오면 본 보드의 다운스트림 패치 (0002-gpio-74x164-registers-default.patch) 도 자동으로 drop 가능하다. 본 보드 외에도 비슷한 cascade 보드가 본 패치를 활용할 수 있도록, mainline 단 한 줄짜리 documented 컨벤션으로 정리되는 편이 모두에게 가성비가 좋다.

TF-A 의 eMMC 초기화 실패 시 panic 대신 SoC 리셋으로 자가 복구하기

STM32MP153D 보드를 eMMC 부팅으로 양산하다 보면 가끔 부팅 중 멈춰 있는 보드를 만난다. 시리얼 콘솔에는 이게 마지막 메시지다.

SDMMC2 init failed
PANIC at PC : ...

그리고 그대로 영원히 정지. 외부 전원을 끊어 다시 넣어주면 멀쩡히 살아나는 경우가 대부분인데, 이건 일과성 실패 — 전원 레일 ramp 타이밍, eMMC fast-boot 모드 진입 직후의 버스 라인 노이즈, RCC 클록 도메인 안정화 jitter 같은, cold boot 한 번이면 사라지는 부류의 문제다. 그런데 TF-A BL2 는 그 한 번의 cold boot 기회를 스스로 만들지 못하고 panic() 에 갇혀 사람이 와서 전원을 뽑아주기를 기다린다. 양산 환경에서는 이게 사고가 된다.

왜 panic 보다 reset 인가

일과성 실패의 복구 경로는 결국 cold boot 한 번이다. 그러면 BL2 가 panic() 으로 SoC 를 정지시키는 대신 stm32mp_system_reset() 으로 SoC 자체를 reset 시켜버리는 편이 자연스럽다. RCC 시스템 리셋이 들어가면 BootROM 이 다시 실행되어 BL2 도 처음부터 다시 unpack/실행되니, 일시적 잡음으로 인한 init 실패는 다음 부팅 cycle 에서 자동으로 해소된다. 사람이 안 와도 된다.

패치 내용

변경 위치는 한 곳이다 — plat/st/common/bl2_io_storage.cboot_mmc(). stm32_sdmmc2_mmc_init() 가 실패한 직후 panic() 직전에 reset 호출 한 줄과 짧은 주석을 끼워 넣는다.

변경 전

params.device_info = &mmc_info;
if (stm32_sdmmc2_mmc_init(&params) != 0) {
    ERROR("SDMMC%u init failed\n", boot_interface_instance);
    panic();
}

변경 후

params.device_info = &mmc_info;
if (stm32_sdmmc2_mmc_init(&params) != 0) {
    ERROR("SDMMC%u init failed - resetting system\n",
          boot_interface_instance);
    /*
     * eMMC init failures here are usually transient (rail-ramp
     * timing, bus-line noise on fast-boot entry, RCC clock-domain
     * settling jitter). panic() leaves the SoC frozen and forces
     * an external power cycle; a system reset lets BootROM re-run
     * the entire boot path, which most transient failures survive.
     * stm32mp_system_reset() is __dead2, so panic() below is a
     * defensive fallback if the reset circuit is itself wedged,
     * and a no-return marker for analyzers.
     */
    stm32mp_system_reset();
    panic();
}

그리고 reset API 헤더 한 줄 추가.

 #include <drivers/st/stm32_sdmmc2.h>
+#include <drivers/st/stm32mp_reset.h>
 #include <drivers/usb_device.h>

전체 diff 는 한 파일에서 14줄 추가, 1줄 삭제. 정말 한 줄 변경이라고 봐도 무방하다.

panic() 도 그대로 두는 이유

stm32mp_system_reset() 은 헤더에 __dead2 (no-return) attribute 가 달려 있어, 호출 즉시 SoC 가 리셋되고 그 다음 줄의 panic() 에는 정상 동작에서는 절대 도달하지 않는다. 그런데 일부러 panic() 호출을 지우지 않고 그대로 둔 데는 두 가지 이유가 있다.

  1. 방어적 fallback. 리셋 회로나 전원 시퀀서에 결함이 있어 stm32mp_system_reset() 이 실제로는 리셋을 못 일으키는 만일의 상황을 가정해보자. 이때는 그래도 panic() 루프에 머물러 있는 편이, 실패 지점 너머로 실행을 계속하는 것보다 훨씬 안전하다. 절대 정지하면 안 되는 곳에서 정지하는 것보다, 절대 진행하면 안 되는 곳에서 진행하는 게 더 위험하다.
  2. 정적 분석기와 후속 수정자에 대한 명시적 표식. 이 분기는 "절대 그 이상 진행해서는 안 된다" 는 의미를 코드에 박아두는 것이다. 누군가 나중에 코드를 손보면서 reset 호출만 지우고 정책을 슬그머니 되돌리는 일을 막는다.

이런 방어선은 비용이 거의 없으면서 (panic() 한 줄) 코드 의도를 명확히 남긴다는 점에서 가성비가 좋다.

다운스트림 v2.4 → mainline v2.10 포팅 노트

본 변경은 사실 STM32MP153D 보드에서 TF-A v2.4 백포트로 1년 넘게 양산 운영해온 정책이다. v2.4 다운스트림 패치 (0005-mmc-init-reset.patch) 에는 함께 들어있는 두 가지가 더 있는데, mainline 송신본에서는 의도적으로 뺐다.

  • MMC_DEFAULT_MAX_RETRIES 를 5에서 50으로 늘리는 변경. 무조건 reset 으로 떨어지는 fallback 이 있는 이상 retry 횟수를 더 늘릴 동기가 약하고, 한 부팅 안에서 50번 재시도하느니 한 번 reset 하고 cold boot 부터 깔끔히 다시 가는 편이 결과적으로 빠르다.
  • "SDMMC init1 start / done" NOTICE 디버그 로그. production BL2 콘솔을 가능한 한 조용하게 유지하기 위해 뺐다. 디버깅이 필요하면 별도 NOTICE_LEVEL 로 켜는 편이 깔끔.

또 v2.4 에는 stm32image 포맷용 별도 파일 plat/st/common/bl2_stm32_io_storage.c 에도 동일한 panic() 위치가 있었는데, v2.10 에서는 이 파일이 bl2_io_storage.c 로 통합되어 패치 대상이 한 곳뿐이다. 통합된 김에 한 hunk 로 두 boot flow 를 모두 커버한다.

upstream 송신 결과

2026-04-29 03:21 UTC, TF-A 메일링 리스트로 송신했다.

  • Subject: [PATCH] fix(st): reset SoC instead of panic() on MMC init failure
  • Message-Id: <20260429032109.789181-1-happycpu@gmail.com>
  • To: tf-a@lists.trustedfirmware.org, yann.gautier@foss.st.com (ST plat maintainer)
  • Cc: madhukar.pappireddy@arm.com, manish.badarkhe@arm.com, olivier.deprez@arm.com (TF-A maintainers)
  • Archive: lists.trustedfirmware.org/archives/list/tf-a@lists.trustedfirmware.org/

TF-A 는 lore.kernel.org 가 아닌 hyperkitty 에 메일링 아카이브가 인덱싱되고, 실제 merge 채널은 메일링이 아니라 review.trustedfirmware.org Gerrit 이다. 메일링은 정책 RFC 토론 용도. ST plat maintainer ack 받으면 같은 commit 을 Gerrit 에 push 해서 정식 review 트랙으로 넘길 예정이다.

마무리

한 줄짜리 변경이지만 운영 입장에서 의미가 크다. "보드를 손으로 가서 전원을 뽑아야 살아나는 사고" 가 "다음 cold boot 한 번이면 자동 복구되는 일과성 잡음" 으로 바뀐다. 그동안 같은 경험을 모은 보드가 한두 대가 아니라면 이 변경은 사고 수를 통째로 한 자리수씩 깎아준다.

비슷한 위치에서 panic() 으로 정지하는 부트로더/펌웨어 코드는 가능한 한 reset 으로 떨어뜨려두는 정책이 양산 환경에 맞다. __dead2 함수 호출 + panic() fallback 의 두 줄짜리 패턴은 어디서 써도 가성비가 좋다.

Linux gpio-74x164 에 lines-initial-states 지원 추가 — 74HC595 chain 의 부팅 글리치 잡기 (개정판)

74HC595 / 74LVC594 같은 직렬-입력 병렬-출력 시프트 레지스터를 SPI 로 cascade 해서 GPIO 32개를 만들어 쓰는 보드를 다뤄본 적이 있으면 부팅 직후 짧게 깜빡이는 LED 한 줄을 본 기억이 있을 것이다. active-low...