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 가 떠오른다. 그런데 이 보드 / 이 드라이버 조합에는 세 가지 이유로 맞지 않는다.
- consumer 와 배타적이다. 이 보드는 chain 의 모든 라인을
gpio-leds가 이미 claim 하고 있다.gpio-hog는 hog 가 라인을 claim 하므로 leds 와 동시 사용이 불가능하다. - 순차 적용으로는 글리치를 막을 수 없다. hog 는 라인별로 한 번씩 set 호출이 들어간다.
__gen_74x164_write_config()가 매번 chain 전체를 다시 쓰므로, hog 4개라면 full-chain SPI write 4회. 첫 hog 가 set 되기 전 0x00 한 번이 무조건 먼저 나간다 — 글리치 자체는 그대로 남는다. - 의도와도 다르다.
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 가 두 가지를 지적했다.
- 왜 이 드라이버만 DT 에서 default 가 필요한가 — driver 에서 zero 안 쓰든지, 아니면 hog 를 제대로 처리하든지 둘 중 하나로 가는 게 맞지 않나.
registers-default는 vendor-prefix 가 없는 generic property 다 — vendor-specific binding 에 generic property 를 추가하는 건 can of worms 의 시작이다.
1번에는 위에서 정리한 74HC595 의 하드웨어 특성 + gpio-hog 가 맞지 않는 세 가지 이유로 답장을 했다. 그런데 답장 직후 Linus Walleij 가 다른 angle 의 제안을 줬다 — 이미 비슷한 binding 이 존재한다. nxp,pcf8575 의 lines-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 컨벤션으로 정리되는 편이 모두에게 가성비가 좋다.