본문 바로가기

Core BSP 분석/리눅스 커널 핵심 분석

GPIO - 리눅스 커널

이번에는 GPIO에 대해 이야기 좀 해보려 합니다.
 
한 10년 전인가요? 기억이 좀 가물 가물한데요. 대학원 면접을 봤었어요. 그 때 어느 교수님이 했던 질문이 생각납니다.
 
     "소프트웨어와 하드웨어의 경계가 어디냐"? 
 
그 당시 너무 얼어 있어 제대로 대답을 못했던 것 같습니다. 결국 대답을 못하고 말았습니다. 지금 생각해 보면 정말 쉬운 질문이었는데 참 안타깝죠.
 
만약 지금 그 질문을 받으면 전 이렇게 대답할 것입니다.
 
     "GPIO와 인터럽트입니다."
 
물론 하드웨어와 소프트웨어의 경계가 GPIO와 인터럽트만 있는 것은 아닙니다. 
메모리 맵드 아이오(memory mapped IO)도 있죠. 특정 메모리 공간에 어떤 값을 쓰면 이에 맞게 하드웨어 디바이스가 동작하는 것입니다.
 
이제 좀 GPIO에 대해서 조금 더 짚어보겠습니다.

보통 MCU에는 본연의 임무를 하는 pin들이 있습니다. 

     "Hardware적으로 정해져 있는 CS, WE라든가 하는 정해진 일을 하는 pin
    어떤 특별한 임무를 갖지 않고, 유저가 원하는 대로 I/O로 사용할 수 있는 pin" 

그런데, 재미있게도 GPIO란 핀은 여러가지 기능을 제공합니다. 다용도로 쓸 수 있다는 것입니다. 축구 선수를 스트라이커, 수비수, 골키퍼로 사용할 수 있는 것과 비슷한 것 같습니다.

좀 더 기술적으로 예를 들어볼까요? 
 
     "어떤 pin은 어떤 chip에 대한 CS로 사용할 수도 있지만, GPIO로도 쓸 수 있다."
 
 이것을 임베디드 용어로 Alternative Functionality라고 말합니다. 그런데 어떤 pin은 이런 Alternative Functionality를 갖는가 하면, GPIO 전용으로만 쓰이는 pin들이 있습니다. 이 정보를 제대로 알려면 MCU의 데이터 쉬트(Data Sheet)나 회로도를 꼭 읽어봐야 합니다.
 
이런 것을 GPIO라고 부르는데, GPIO라는 건 GP + I/O라는 뜻입니다. GPIO는 말 그대로 , IO니까, Input을 받거나(GPIO pin 상태를 읽고 싶거나), Output을 내놓거나(GPIO pin에 뭔가 쓰고 싶거나)하는 데 쓰입니다. 이런 GPIO는 MCU하나에 적으면 10개 많으면 200개까지 준비되어 있습니다.

ARM 기반 MCU입장에서 GPIO에 대해서 알아볼까요? 
 
     "GPIO는 AMBA bus에 연결된 SOC Peripheral이다." 
     "GPIO는 APB bus에 연결된 Slave이다. "
 
그래서 GPIO들도 Register를 가지고 있고, CPU입장에서는 Register를 통해서 control 가능합니다. 
 
그 제어(control)이라는 게 I/O로 쓸 수 있다고 했으니까, 어떤 때는 인풋(Input) 어떤 때는 아웃풋(Output)으로 써야 합니다. 이런 I/O는 레지스터(Register)를 통해서 Programmable할 수 있습니다. Input, Output으로 원하는 대로 사용 가능해야 하니까. 이런 레지스터의 구조는 물론 MCU마다 당연히 달라요. 퀄컴, 인텔, 엔비디아 서로 다른 구조로 설계를 했다는 것이지요. 그러니 data sheet를 잘 읽어야 합니다.

Register를 통해서 GPIO를 control해야 하는 것에는, 3가지가 있어요. 이 3가지가 모두 제대로 설정되어야 GPIO를 내 수족처럼 control할 수 있어요.
 
1) Pin의 Mode
   → GPIO로 쓸 꺼냐, Alternative Functionality로 쓸 꺼냐.
   → Alternative로 설정하게 되면 GPIO로는 못쓴다. 
   → 게임 오바 되면 Hardware적으로 정해진 임무를 수행. 신경 꺼도 됨

2) Pin의 상태 (Mode)를 활성화 하면서 Data Direction을 정할 수 있다.
   → 지금부터 Pin이 사용 가능하도록 Init 하겠다.
   → Input이냐, Output이냐를 결정하겠다.

3) 자~ 읽어 보자 라든가, 자~ 값을 써보자를 할 수 있다.

리눅스 커널 API

리눅스 드라이버 레벨에서 GPIO를 제어 하기 위해 리눅스 커널에서는 다음 API를 제공합니다.
필요한 헤더파일   #include <asm/gpio.h>
GPIO 입력설정     gpio_direction_input( gp_nr );
GPIO 출력설정     gpio_direction_ouput( gp_nr,  init_val );   // init_val 는 초기값이다.
GPIO 출력            gpio_set_value( gp_nr, val );                   // val 는 0, 1 값이다.
GPIO 입력            gpio_get_value( gp_nr );
GPIO 인터럽트 활성화   set_irq_type( irq_nr, irq_type );           
 
그런데, 이렇게 API에 대한 설명만 읽고는 실무에 활용할 수 없습니다. 상세한 시나리오를 만들어서 실제 API가 어디서 사용되는 지 알아야 머리에 오래 남고 다른 드라이버에 활용하기 쉽습니다.


GPIO 출력 모드

아래는 GPIO를 출력 모드로 설정해서 사용하고 싶을 때의 예시 코드입니다.
GPIO를 출력 모드로 사용한다는 것은 특정 상황에서 GPIO을 0 혹은 1로 바꾸고 싶을 경우에 주로 사용합니다.

(참고로 아래 udelay(1) 만큼 딜레이를 주는 코드가 칫셉 벤더에서 전달이 되면, 절대 바꾸면 안됩니다. 이게 다 데이터 시트에 있는 것을 구현하는 것이기 때문입니다.)
#define GPIO_NUMBER 11 
int gpio_nreset = GPIO_NUMBER;
 
static int cs4271_probe(struct snd_soc_codec *codec)
{
    struct cs4271_private *cs4271 = snd_soc_codec_get_drvdata(codec);
    struct cs4271_platform_data *cs4271plat = codec->dev->platform_data;
    int ret;
 
    //GPIO 핀을 초기화
        if (gpio_request(gpio_nreset, "CS4271 Reset"))
            gpio_nreset = -EINVAL;
 
    // GPIO를 출력(아웃풋)모드로 변경
        gpio_direction_output(gpio_nreset, 0);
 
    // 나노 초만큼 딜레이를 줌
        udelay(1);
 
    // GPIO 핀을 1로 올려 줌 
        gpio_set_value(gpio_nreset, 1);
        /* Give the codec time to wake up */
        udelay(1);
    }
 
    cs4271->gpio_nreset = gpio_nreset;
}
 
 
GPIO 입력 모드

아래는 GPIO를 입력 모드로 설정해서 사용하고 싶을 때의 예시 코드입니다. 특정 상황에서 GPIO PIN의 값을 읽고 싶을 경우 GPIO를 입력 모드로 쓰면 됩니다.
#define GPIO_NUMBER 11 
int gpio_nreset = GPIO_NUMBER;
static int cs4271_probe(struct snd_soc_codec *codec)
{
    struct cs4271_private *cs4271 = snd_soc_codec_get_drvdata(codec);
    struct cs4271_platform_data *cs4271plat = codec->dev->platform_data;
    int ret;
    int gpio_nreset = -EINVAL;

//GPIO 핀을 초기화
        if (gpio_request(gpio_nreset, "CS4271 Reset"))
            gpio_nreset = -EINVAL;
    if (gpio_nreset >= 0) {
        /* Reset codec */
    
// GPIO를 입력(input)모드로 변경
        gpio_direction_input(gpio_nreset);

// 나노 초만큼 딜레이를 줌
        udelay(1);

// GPIO 핀을 읽음 
        if(gpio_get_value(gpio_nreset) == 1) 
              printk(" Oh! GPIO PIN is one");
        /* Give the codec time to wake up */
        udelay(1);
    }
 
이번에 다른 예를 들어볼까요? 디바이스 드라이버 요구 사항을 먼저 알아볼까요?
 
     "만약에 리눅스 드라이버에서 커널 패닉이 발생한 경우 모뎀 프로세서에 GPIO로 이 사실을 알려주고
     (그래야 모뎀도 동작을 멈출 수 있겠죠.) 싶을 때는 GPIO를 어떻게 사용하면 될까?"
 
위 예시에서 언급한 모뎀은 AP 하드웨어와 떨어진 하드웨어를 의미합니다.
 
당연히 출력(output)모드로 사용하면 됩니다. 샘플 코드는 아래와 같아요.
(물론 위 시나리오에서 GPIO로 통신할 수 있는 구조로 회로가 설계되어 있다고 가정 하겠습니다.)

아래는 커널 패닉이 발생되었을 시에 모뎀 프로세서에 이를 알리는 부분인데, 어디선가 이 함수를 호출한다고 가정해요.
static int send_kernel_panic_msg_to_modem_processor(void)
{
    gpio_set_value(gpio_kernel_panic, 1);
}
 
아래는 GPIO를 초기화하는 코드에요.
#define GPIO_NUMBER_FOR_KERNEL_PANIC 12
int gpio_kernel_panic = GPIO_NUMBER_FOR_KERNEL_PANIC;
static int deliver_kernel_panic_accident_to_modem_probe(struct snd_soc_codec *codec)
{

    //GPIO 핀을 초기화
        if (gpio_request(gpio_kernel_panic, "Kernel Panic"))
            gpio_nreset = -EINVAL;

    // GPIO를 출력(아웃풋)모드로 변경하고 기본(default)로 0으로 설정, 
    // 그래야 나중에 1로 이 값을 올려야 정상동작이 가능함
        gpio_direction_output(gpio_kernel_panic, 0);
...
}
 
출력 모드/입력 모드 모두 사용하는 시나리오

두가지 GPIO모드를 사용하는 다른 시나리오를 만들어 볼까요? 유저 스페이스에서 debug fs로 어떤 값이 드라이버에 전달될 때, LCD를 제어하는 시나리오입니다.

     "/d/lcd_driver/val 노드에 1을 써주면 GPIO에 그대로 1이란 값을 써주자."
/d/lcd_driver/val 노드를 유저 스페이스에서 읽으려고 하면 GPIO 상태를 읽어 줘야하는 상황이에요.
#define GPIO_LCD_DRIVER_GET 15
#define GPIO_LCD_DRIVER_SET 16

int gpio_lcd_get = GPIO_LCD_DRIVER_GET;
int gpio_lcd_set = GPIO_LCD_DRIVER_SET; 

// /d/lcd_driver/val 노드를 유저 스페이스에서 읽으려고 하면 lcd_driver_get 함수가 호출되요.
// cat /d/lcd_driver/val
static int lcd_driver_get(void *data, u64 *val)
{
    int ret = 0;
    
    ret = gpio_get_value(gpio_lcd_get);    
    return ret;
}

// /d/lcd_driver/val 노드에 유저 스페이스에서 1로 설정을 하면 lcd_driver_set 함수가 호출이 됨
// echo /d/lcd_driver/val 1
static int lcd_driver_set(void *data, u64 val)
{
    if(val == 1) {
        gpio_set_value(gpio_lcd_set, 1);
    }
}
DEFINE_SIMPLE_ATTRIBUTE(lcd_driver_fops, lcd_driver_get, lcd_driver_set, "%llun");

static int lcd_driver_probe(struct platform_device *pdev)
{
    lcd_debug_debugfs_root = debugfs_create_dir("lcd_driver", NULL);
    debugfs_create_file("val", S_IRUGO, kernel_bsp_debug_debugfs_root, NULL, &lcd_driver_fops);

    //GPIO 핀을 초기화
    gpio_request(gpio_lcd_set, "lcd driver set");
    
    // GPIO를 출력(아웃풋)모드로 변경하고 기본(default)로 0으로 설정, 
    //그래야 나중에 1로 이 값을 올려야 정상동작이 가능함
    gpio_direction_output(gpio_lcd_set, 0);

    //GPIO 핀을 초기화
    gpio_request(gpio_lcd_get, "lcd driver set");

    // GPIO 핀을 입력 모드로 초기화    
    gpio_direction_input(gpio_lcd_get);
    return 0;
}
 
이 정도로 정리하면 잊어 먹지는 않을 것 같아요.
 
     "혹시 글을 읽고 궁금한 점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실하게 답변 올려 드리겠습니다!"
 
 
# Reference: For more information on 'Linux Kernel';
 
디버깅을 통해 배우는 리눅스 커널의 구조와 원리. 1
 
디버깅을 통해 배우는 리눅스 커널의 구조와 원리. 2