6 분 소요

디바이스 트리에서 세 핀을 인터럽트에서 “그냥 읽을 수 있는 입력”으로 바꿨다. 인터럽트까지는 아직 필요 없고, 유저 공간에서 gpioget으로 확인만 하면 되는 신호들이었다.

그러고 나서 이상한 걸 봤다.

root@myir:~# gpioget gpiochip7 12 14 15
0 0 0

값을 읽으면 보드의 LED가 꺼진다. 그리고 재부팅하면 다시 켜진다.

입력 핀이 LED를 켜고 끌 수는 없다. 뭔가 잘못됐다는 건 분명했다.


왜 급한 문제였나

그 핀들은 이런 신호였다.

이름 반대편
PH12 PM_INT 전력량계의 펄스 출력
PH14 EM_INT 옵토 → 74LV14 출력
PH15 RCD_INT 누전검출 → 74LV14 출력

전부 반대편이 출력이다. 만약 MPU 쪽도 출력으로 드라이브하고 있다면 출력끼리 맞붙는다. 한쪽은 HIGH를 밀고 한쪽은 LOW를 당기는 상태가 되고, 전류 제한이 없으면 둘 중 하나가 죽는다.

LED 하나 깜빡이는 문제가 아니라 소자 손상 문제였다.


첫 번째 판단은 틀렸다

확인부터 했다.

root@myir:~# gpioinfo gpiochip7 | grep -E "12|14|15"
        line  12:  "PM_INT"   unused  input  active-high
        line  14:  "EM_INT"   unused  input  active-high
        line  15: "RCD_INT"   unused  input  active-high

input. 그래서 “출력이 아니니 충돌은 없다”고 결론 냈다. 이게 틀렸다.

커널 소스를 보면 왜 틀렸는지 나온다.

/* drivers/pinctrl/stm32/pinctrl-stm32.c */
static int stm32_gpio_get_direction(struct gpio_chip *chip, unsigned int offset)
{
        stm32_pmx_get_mode(bank, pin, &mode, &alt);
        if ((alt == 0) && (mode == 0))
                ret = 1;          /* 입력 */
        else if ((alt == 0) && (mode == 1))
                ret = 0;          /* 출력 */
        else
                ret = -EINVAL;    /* AF 또는 analog */
}

그리고 gpiolib이 이 값을 받는 쪽.

/* drivers/gpio/gpiolib.c, gpiochip 등록 시점 */
if (!chip->get_direction(chip, i))
        set_bit(FLAG_IS_OUT, &desc->flags);
else
        clear_bit(FLAG_IS_OUT, &desc->flags);

!ret이 참이 되는 건 ret == 0, 즉 진짜 GPIO 출력일 때뿐이다. -EINVAL!(-22) == 0이라 거짓이 되어 입력으로 분류된다.

"input"  표시 →  진짜 입력  /  analog  /  AF
"output" 표시 →  진짜 GPIO 출력

AF 모드는 얼마든지 푸시풀 출력일 수 있다. 내가 배제한 건 “GPIO 출력” 하나뿐이었고, 정작 위험한 경우는 배제하지 못한 상태였다.

증거가 한쪽으로만 성립하는 경우가 있다. output이 뜨면 확실하지만, input이 떠도 아무것도 확정되지 않는다. 이걸 구분 안 하면 안전하다고 오판한다.


레지스터를 직접 읽으려다 실패

그럼 MODER을 직접 보면 되겠다 싶었다. devmem2는 이미지에 없고 busybox에도 applet이 없어서 파이썬으로 /dev/mem을 열었다.

GPIOH MODER   0x00004508
GPIOH OTYPER  0x00004508
GPIOH OSPEEDR 0x00004508
GPIOH PUPDR   0x00004508

네 레지스터가 전부 같은 값이 나왔다. 명백히 잘못된 읽기다. STM32는 GPIO 뱅크 클럭이 꺼져 있으면 레지스터 접근이 정상적으로 안 된다. 커널이 필요할 때만 클럭을 켜기 때문에, 유저 공간에서 /dev/mem으로 불쑥 읽으면 이런 값이 나온다.

(덧붙여 처음엔 베이스 주소도 틀렸다. GPIOH는 0x50009000이다. 0x50007000은 GPIOF다. stm32mp151.dtsi에서 뱅크 맵을 다시 확인했다.)


제대로 보는 방법

pinctrl 드라이버가 debugfs로 내보내는 게 있었다. 이건 내부에서 clk_enable()을 하고 읽기 때문에 신뢰할 수 있다.

grep -E "\(PH(4|5|6|12|13|14|15)\)" \
  /sys/kernel/debug/pinctrl/*50002000*/pinconf-pins

재부팅 직후, gpioget을 치기 전에 실행해야 한다. 한 번 읽으면 상태가 바뀌어 버리기 때문이다.

pin 116 (PH4):  input - low - floating
pin 117 (PH5):  input - low - floating
pin 118 (PH6):  input - low - floating
pin 124 (PH12): alternate 14 (LCD_R6) - push pull - floating - medium speed
pin 125 (PH13): input - low - floating
pin 126 (PH14): alternate 14 (LCD_G3) - push pull - floating - medium speed
pin 127 (PH15): alternate 14 (LCD_G4) - push pull - floating - medium speed

alternate 14, push pull. LCD 데이터 출력이었다.


LCD가 왜 거기서 나와

벤더 핀맵을 뒤지니 답이 있었다.

/* stm32mp157-ya157c-pinctrl.dtsi */
ltdc_pins_a: ltdc-a-0 {
        pins {
                pinmux = <STM32_PINMUX('H', 12, AF14)>, /* LCD_R6 */
                         <STM32_PINMUX('H', 13, AF14)>, /* LCD_G2 */
                         <STM32_PINMUX('H', 14, AF14)>, /* LCD_G3 */
                         <STM32_PINMUX('H', 15, AF14)>, /* LCD_G4 */
                         /* ... */
                bias-disable;
                drive-push-pull;
        };
};

우리 보드의 PM_INT / EM_INT / RCD_INT가 평가보드에서는 LCD 데이터 라인이다.

리눅스에서는 &ltdcdisabled로 껐으니 이 그룹이 적용될 일이 없다. 그런데 U-Boot은 별개의 바이너리다. 벤더 U-Boot이 스플래시 화면을 위해 LTDC를 초기화하면 이 핀들이 전부 푸시풀 출력으로 설정된다. 그리고 커널로 넘어온다.

핵심은 여기다.

리눅스는 아무도 선점하지 않은 핀의 상태를 바꾸지 않는다.

디바이스 트리에서 어떤 노드도 그 핀을 쓰지 않으면, pinctrl은 그 핀을 건드릴 이유가 없다. 부트로더가 남긴 설정이 그대로 살아 있다. 그래서 전력량계 출력에 LCD 픽셀 데이터가 맞붙고 있었다.

gpioget을 치면 꺼진 이유도 이걸로 설명된다. 그 순간 처음으로 MODER = 00이 되면서 드라이브가 멈춘 것이다. 그리고 재부팅하면 U-Boot이 다시 설정하니 원래대로 돌아간다.


그럼 옆 핀은 왜 멀쩡했나

같은 뱅크의 PH4, PH5, PH6, PH13은 input으로 나왔다. PH13은 LCD_G2인데도 그렇다.

차이는 하나뿐이었다. 그 네 핀에는 gpio-keys가 붙어 있었다.

din1 {
        label = "din1";
        interrupt-parent = <&gpioh>;
        interrupts = <13 IRQ_TYPE_EDGE_BOTH>;
};

gpios 없이 interrupts만 쓴 형태라 GPIO를 요청하지 않는데, STM32에서는 그래도 핀먹스가 설정된다.

/* drivers/pinctrl/stm32/pinctrl-stm32.c */
static int stm32_gpio_irq_request_resources(struct irq_data *irq_data)
{
        ret = stm32_gpio_direction_input(&bank->gpio_chip, irq_data->hwirq);
        if (ret)
                return ret;

        ret = gpiochip_lock_as_irq(&bank->gpio_chip, irq_data->hwirq);
        /* ... */
}

irqchip이 인터럽트를 잡을 때 핀을 디지털 입력으로 바꿔준다. 그래서 IRQ를 쓰는 핀은 자동으로 정리되고, 안 쓰는 핀만 부트로더 상태로 남았다.

내가 이 세 핀을 인터럽트에서 입력으로 바꾼 것이 원인이었다. 소비자를 떼어내자 아무도 안 잡는 핀이 되었고, U-Boot의 설정이 드러났다.


직관과 반대다

여기서 얻은 게 이번 작업의 핵심이다.

(X) 선점하지 않으면 건드리지 않으니 안전하다
(O) 선점해야 입력으로 확정된다

“쓰지 않는 핀이니 DT에 안 적어도 된다”는 생각은 쓰지 않는 핀에만 맞다. 회로에서 실제로 뭔가 연결된 핀이라면, DT에 적지 않는 것은 안전이 아니라 방치다. 부트로더가 무엇을 남겼는지에 운을 맡기는 것이다.


고친 방법

인터럽트 소비자로 되돌렸다.

int-keys {
        compatible = "gpio-keys";

        pm-int {
                label = "pm_int";               /* PH12 -> EXTI12 */
                linux,code = <KEY_F5>;
                interrupt-parent = <&gpioh>;
                interrupts = <12 IRQ_TYPE_EDGE_BOTH>;
        };
        em-int  { /* PH14 -> EXTI14 */ };
        rcd-int { /* PH15 -> EXTI15 */ };
};

걱정했던 건 읽기가 막히는 것이었다. STM32 pinctrl은 .strict = true라서, 어떤 디바이스가 먹스로 점유한 핀은 GPIO로 요청할 수 없다.

static const struct pinmux_ops stm32_pmx_ops = {
        /* ... */
        .strict                 = true,
};

그런데 IRQ 경로는 여기에 걸리지 않는다.

stm32_gpio_direction_input()
     pinctrl_gpio_direction_input()   /* 방향만 바꾼다 */
     ops->gpio_set_direction()        /* 먹스 소유권은 안 가져간다 */

gpiochip_lock_as_irq()
     FLAG_USED_AS_IRQ  설정
     FLAG_REQUESTED  건드리지 않는다

방향은 확정하면서 소유권은 안 잡는다. 그래서 gpiogetgpiomon이 그대로 동작한다. 인터럽트로 붙여서 잃는 게 없었고, 덤으로 evtest로도 볼 수 있게 됐다.

빌드는 경고 없이 통과했고, dtb를 역컴파일해서 값까지 확인했다.

$ dtc -I dtb -O dts stm32mp157a-sc6-v1.dtb | grep -A6 pm-int
        pm-int {
                label = "pm_int";
                linux,code = <0x3f>;
                interrupt-parent = <0x75>;
                interrupts = <0x0c 0x03>;
        };

0x0c = 라인 12, 0x03 = IRQ_TYPE_EDGE_BOTH. phandle 0x75gpio@50009000(GPIOH)인 것까지 확인했다. 인터럽트 부모를 잘못 적으면 조용히 엉뚱한 뱅크로 가기 때문에 이건 확인해두는 게 좋다.

검증은 재부팅 후 같은 pinconf-pins를 다시 보면 된다. 세 줄이 input으로 바뀌고, 부팅 직후부터 LED가 꺼져 있으면 된다.


아직 남은 것

같은 원인이 더 넓게 퍼져 있다. LTDC 핀 목록과 우리 보드 핀을 대조해보니 이렇다.

우리 핀 LTDC에서는 실제 연결
PG7 LCD_CLK PLC 모뎀 인터럽트
PG10 / PG12 LCD_B2 / B1 디지털 출력 1, 2
PI9 / PI1 LCD_VSYNC / G6 디지털 출력 3, 4
PI2 LCD_G7 외부 워치독 입력
PD9 LCD_B0 RS232 수신

디지털 출력 쪽은 리눅스가 gpio-leds로 잡으니 부팅 후에는 정상이다. 하지만 U-Boot 구간에서는 LCD 신호가 달링턴 드라이버 입력으로 들어간다. 부팅 중에 출력이 덜컥거릴 수 있다는 뜻이다.

한 줄로 전체 범위를 볼 수 있다.

grep "alternate 14" /sys/kernel/debug/pinctrl/*50002000*/pinconf-pins

근본 대책은 U-Boot에서 video를 끄는 것이다. 지금은 U-Boot 소스가 없어서 받아야 하고, 그 전까지는 회로에 연결된 핀은 DT에서 반드시 소비자를 붙인다가 유일한 방어선이다.


정리

이번에 배운 것을 순서대로 적으면 이렇다.

  1. gpioinfoinput은 판별 근거가 못 된다. AF도 analog도 input으로 표시된다
  2. /dev/mem으로 GPIO 레지스터를 읽으면 안 된다. 뱅크 클럭이 꺼져 있으면 쓰레기 값이 나온다
  3. pinconf-pins를 쓴다. 드라이버가 클럭을 켜고 읽어준다
  4. 선점하지 않은 핀은 부트로더 상태가 그대로 남는다. 안 적는 게 안전한 게 아니다
  5. IRQ를 붙이면 방향이 확정되고, 읽기도 계속 된다. STM32에서는 둘 다 된다

가장 오래 걸린 건 1번을 의심하기까지였다. 커널이 input이라고 말해주는데 그걸 안 믿기가 쉽지 않다. 도구가 보여주는 값이 무엇을 근거로 나온 값인지까지 확인해야 한다는 걸, 소스를 열고 나서야 알았다.

참고

1.AF = Alternate Function. STM32

댓글남기기