2018년 8월 29일 수요일

[LINUX] GPIO control 방법

Rockchip 3308 chip 을 이용한 리눅스 포팅 프로젝트가 생겨 관련 제어 기술을 공부하면서 자료를 정리하기로 하였다
아직까지 리눅스를 심도있게 본적이 없어서 빠르게 아래 Peripheral Device 들을 테스트 해볼 예정으로 시작하였다

타이틀은 : 리눅스도 모르는 초심자가 시작하는 리눅스 드라이버 개발 이 되려나....

  1. GPIO (제어 및 Driver)
    • Simple Control 
    • Interrupt
  2. PWM
  3. System Timer
  4. SPI

GPIO 제어를 하기 우선 DTS Script 를 이용하여 GPIO 접근을 할 수 있는 노드 정의에 대해 알아야 했다

그래서 같이 공부한 내용중 DTS 챕터에 대한 것을 별도로 정리함 ([LINUX] DTS 기초 문법 정리 글을 참조하세요. ) 

pinctrl 그리고 <> 를 이용한 pin 을 정의하는 것에 대해서 내가 사용하려는 PIN 에 대해 정의를 해야 한다

Rockchip GPIO 0번 부터 4번까지 4개의 Block 을 가지고 있으며 각 블럭은 32개의 핀으로 정의되어 있다
32개는 각각 ABCD 형태로 8개씩 4개의 단위로 쪼개저 있다

A0 (0) ... A7(7)... B0(8)... B7(9)....


기본적으로 Rockchip 에서 제공해주는 DTSI 파일에는 기본 블럭들에 대한 정의가 되어 있으며 개별 GPIO 를 사용하고 싶을 때에는
자신의 dts 파일을 만들어서 노드를 생성해주어야 한다

기본 EVB Board 를 위해서 아래와 같이 include 되어 계층화 되어 있으며  dtsi 는 건들지 않으며 dts 파일에서 사용하지 않을 노드는
status = "disabled" 시키고 사용할 노드는 status = "okay" 하고 없으면 생성해야 한다

rk3308.dtsi 
    ---- rk3308-evb-v11.dts
         ---- rk3308-evb-pdm-v11.dts   


GPIO0 번의 B3번 포트를 이용하여 signal 설정, 그리고 Interrupt 처리를 위한 IRQ 등록과 처리에 대해서 하는 것이 이 장의 최종 목적이다.  

DTS 에서 GPIO 를 어떤 식으로 정의 해놓았는지 찾아 보았다

아래와 같은 pinctrl 을 찾을 수 있었다. (상위 DTSI 에서 pinctrl 의 블럭들이 정의 되어 있기 때문에 여기서는 pinctrl 을 사용하기 위한 
실제 pin 노드를 추가 정의해야 한다
좀더 설명하자면 pinctrl 에서는 실제 Pin 마다 지원하는 기능들을 MUX 기능을 이용해서 선택할 수 있는데
(예를들어 GPIO pin SPI_TX 기능을 지원한다면 사용자는 이핀을 GPIO 로 사용할지 SPI_TX 로 사용할지를 MUX 선택을 통해 선택해야 한다
이러한 Pin 선택 MUX 설정을 제조사가 제공하는 최상위 DTSI 파일에는 기본적으로 제공을 해준다.  
(더 깊이 들어가는 내용은 목적과 거리가 있어서 생략)

다시 아래와 같이 pinctrl 에 추가 내용을 선언하기 위해서는 Root Node 가 아닌 외부에 독립적으로 선언해야 된다
그리고 앞에 & 연산자 문자를 붙여야 한다

  1.  pinctrl 내부에 내가 사용할 노드 이름을 설정
  2.  이 노드의 사용하고자 하는 gpio 이름을 생성하고
  3.  gpio 이름 노드의 세부 pin 설정을 해야 한다

나는 정의되어 있는 다른 놈들을 참조해서 나만의 GPIO 를 위한 정의를 아래 붉은 색처럼 추가 하였다

&pinctrl {
    pinctrl-names = "default";
    pinctrl-0 = <&rtc_32k>;

...(생략)
    wireless-wlan {
        wifi_wake_host: wifi-wake-host {
            rockchip,pins = <0 RK_PA0 RK_FUNC_GPIO &pcfg_pull_up>;
        };
    };

    my_gpio_driver { // 내가 사용할 노드의 이름
        my_pin_gpio: my-pin-gpio { //핀 노드 이름
             rockchip,pins = <0 RK_PB3 RK_FUNC_GPIO &pcfg_pull_up>; // GPIO 핀의 세부 설정 
        };
    };
};


위와 같이 정의내용 중에 rockchip,pins 는 제조사마다 다를 수 있을 것 같다.  (저 이름은 사용자 마음대로 정의할 수 있다)
Rockchip 의 경우 저런 형식으로 GPIO 를 정의한 것을 실제 코드에서는 아래와 같이 핸들로 읽어 오기 때문이다
코드에서 DTS 의 리소스에 접근하는 방법은 of_XXX 함수들이 이용되며 이 내용은 이 후 다시 더 설명할 예정이다

GPIO MUX pin 제어를 이용하기 위해서는 rockchip 에서 제공하는 pinctrl-rockchip 드라이버를 이용해야 되기 때문에 
위와 같이 정의해야 된다고만 알아 두자. (즉 제조사에 따라 pinctrl 은 달라 질 수 있다는 것도 잊지 말자)

list = of_get_property(np, "rockchip,pins", &size);


<0 RK_PB3 RK_FUNC_GPIO &pcfg_pull_down> 정의 내용은 
GPIO 그룹, PIN 번호, Function, 초기화 신호 순으로 정의를 하게 되어 있다

위에서 설명한 것처럼 1 2 3번은 제조사에서 제공하는 상수로 이루어져 있으며 이것을 처리하는 pinctrl 드라이버에 따라서 달라 질 수 있다
Rockchip 에서 는 위와 같이 사용하며 맨마지막 pcfg_pull_down 또한 bias-pull-down 으로 정의되어 있으며
실제 드라이버에서는 문자열로 인식하여 이것을 PIN_CONFIG_BIAS_PULL_DOWN 상수로 대체되어 사용되게 된다

pinctrl 에서 정의된 gpio 노드를 어떻게 사용하는지 알기 위해 난 또 wifi_wake_host 가 어떻게 쓰이는지 검색을 해보았다

wireless-wlan {
     compatible = "wlan-platdata";     // -> 드라이버와 매핑하기 위한 Unique 한 아이디 
     rockchip,grf = <&grf>;                // -> 전원 관련 설정 ( wifi 모듈을 연동하는 설정이기 때문에 필요한 듯하다
     pinctrl-names = "default";           // -> pinctrl 이름들 여러게가 선언될 수 있으며 하나만 사용시에는 "default" 라고 정의한다
     pinctrl-0 = <&wifi_wake_host>; // -> "default" 를 위한 pin 리스트 여기서 위에 선언한 gpio & 문자와 함께 설정하였다
     wifi_chip_type = "ap6255";         // -> 장치 고유 값
     WIFI,host_wake_irq = <&gpio0 RK_PA0 GPIO_ACTIVE_LOW>; // -> IRQ 및 접근 할 GPIO 핸들을 가져오기 pin 설정
     status = "okay";
};

즉 엄청난 검색을 통해 나는 DTS 에서 저 노드들이 필수로 필요하다는 사실을 알아 냈다
pin mux 를 통한 설정을 하기위해서 pinctrl-names pinctrl-0 을 정의해서 내가 정의한 gpio pins GPIO Function Mux Selection 
이 되도록 사용해야 된다
(그 이상은 나도 잘 모르겠다. 내가 잘못 알고 있거나 추가해야 될 사항이 있다면 언제든지 메일을 주면 수정하도록 하겠다. ) 

compatible 은 내가 작성할 드라이버를 찾기 위한 Unique 한 문자열 이름이며

GPIO 를 제어 하기 위한 핸들을 가져오기 위해서 임의의 이름인 "WIFI,host_wake_irq" 를 정의하고 위와 같이 설정한다
이 노드는 Root Node 안에 정의가 되어야 하며 아래와 같이 나는 신규 DTS 장치 노드를 생성하였다

/ {
    ... (생략)

    my_gpio_driver {
        compatible = "test,my_gpio_drviver";
        pinctrl-names = "default";
        pinctrl-0 = <&my_pin_gpio>;
        TEST,my_pin_gpio_0_b3 = <&gpio0 RK_PB3 GPIO_ACTIVE_LOW>;
        status = "okay";
    };
};

내용 정리 
  •  pinctrl pin mux 설정을 하고, 실제 사용할 장치 노드와 pin 노드간의 연결 관리를 한다
  • 장치 노드에는 고유의 compatible 이름을 가지고 있으며 이 이름은 실제 드라이버를 검색할 때 사용된다
  • pinctrl-names pinctrl-0 MUX 설정이며 실제 GPIO 접근을 위해서는 별도의 pin 설정을 해야 한다
  • status okay 가 되어야 드라이버 활성화가 된다

오 정말 간단하지 않은가? 하지만 난 이것을 이해하기 위해서 3일을 소비했다. ........

  • 참조 문서 
    • DTS 문서
    • Kernel / Documentation 문서들. (pinctrl-bindings.txt / rockchip,pinctrl.txt) 문서 
    • Device Tree 상세분석 in Linux Kernel 4.0 (커널 연구회)
    • 여러 검색 신공..... 



이제 DTS 노드를 생성하였으니 이제 Kernel 소스에 my_gpio_device 를 제어 하기 위한 드라이버를 만들어보자

드라이버를 만들기 위해서 드라이버의 종류를 알아 보게 되었다......  SPI, I2C 이런 장치들이 아닌경우는 즉 사용자 입맛에 맛는 드라이버를
만들기 위해서는 Platform_device_driver 를 사용해야 한다는 정보 였다
그리고 이 드라이버의 뼈대 코드를 어떻게 만들어야 되는지 또 검색을 하며 아래와 같은 코드를 만들 수 있었다. ...

무식하면 손품을 팔아야 한다.... .

리눅스에서 Menuconfig 구성을 위한 Kconfig 그리고 빌드하기 위한 Makefile 그리고 원하는 소스코드 추가 위치는 Kernel 소스에 내가 원하는 소스 위치
이 잡다한 설정을 하는데 또 하루의 시간을 허비 했다

Rockchip 의 경우 Kernel / Driver / My_pin_gpio 디렉토리를 만들고
디렉토리에 아래 3개의 파일을 만들었다
    Kconfig
    Makefile
    my_pin_gpio_drv.c

그리고 상위 Kconfig My_pin_gpio 를 빌드 하기 위한 스크립트 추가... (Kconfig 내용도 여기서는 제외 함 이 내용도 나중에 다시 정리해야 할 듯 하다.)
Makefile 또한 제조사가 제공하는 BSP 에 따라 여러 매크로 들이 존재하기 때문에, Kernel / Driver 의 다른 Platform Driver 를 검색해서 참조 해서 우선 
작성하기를 권한다. (Platform Driver 를 아는 방법은   c 파일에 Platform_Device_Register 함수를 사용한다면 그것은 바로 Platform Driver !!!! 우훗!!) 

c 코드의 Platform Driver 구조는 아래 구성요소로 구성된다

  • platform_driver 구조체의 기본 설정
  • init, exit 함수 

우선 DRIVER 를 위해서 2개의 함수와 모듈 Description 을 추가 하였다

static int __init my_sensor_init(void)
{

}

static void __exit my_sensor_exit(void)
{

}

module_init(my_sensor_init);
module_exit(my_sensor_exit);

MODULE_DESCRIPTION("my sensor control v0.1");
MODULE_AUTHOR("moon10200@gmail.com");
MODULE_LICENSE("GPL");

my_sensor_init 은 모듈이 처음한번 로딩 될 때 호출되는 함수이며, exit 는 언로드 될 때 한번 호출되는 함수이다
여기에 우리는 Platform Driver 이기 때문에 로딩될 때 Platform driver 로 등록하고 언로드 될 때 등록해제를 하는 함수를 사용한다

static struct of_device_id my_sensor_of_match[] = {
    { .compatible = "my-gpio-driver" },
    {}
};
MODULE_DEVICE_TABLE(of, my_sensor_of_match);

static struct platform_driver my_sensor_driver = {
    .probe = my_probe,
    .remove = my_remove,
    .suspend = my_suspend,
    .resume = my_resume,
    .driver = {
        .name = "my-sensor-driver",
        .owner = THIS_MODULE,
        .of_match_table = of_match_ptr(my_sensor_of_match),
    },
};

static int __init my_sensor_init(void)
{
    return platform_driver_register(&my_sensor_driver);
}

static void __exit my_sensor_exit(void)
{
    platform_driver_unregister(&my_sensor_driver);
}

  1. DTS 에 정의한 compatible 이름과 매핑되기 위한 my_sensor_of_match 를 정의하고 이것을 my_sensor_driver 구조체에 넣어 준다
  2. platform driver 이름을 정의하고
  3. 로딩된후 호출되는 probe함수 제거될 때 호출되는 remove 함수 그리고 장치가 suspend, resume 될 때 호출될 함수 를 지정하고 함수로 만들어 준다

일단 여기까지했으면 기본 platform driver 의 뼈대가 만들어 진것이다

static int my_probe(struct platform_device *pdev)
{
    return 0;
}

static int my_remove(struct platform_device *pdev)
{
    return 0;
}

static int my_suspend(struct platform_device *pdev, pm_message_t state)
{
    return 0;
}

static int my_resume(struct platform_device *pdev)
{
    return 0;
}

즉 드라이버가 register 된 후 compatible 과 일치하는 장치를 DTS 가 로딩될 때 찾아서 드라이버를 로딩해주는 절차가 부팅할 때 
진행된다고 보면 된다. 좀더 깊은 내용은 커널 코드를 공부해야 한다..... 우리는 사용만 할 줄 알면되니까..일단 넘어가자

여기서 이제 처음 호출되는 probe 함수부터 구현을 시작해 보자

// probe 에서 들어오는 인자는 각 드라이버의 형태에 따라 해당 구조체가 생성되어 전달된다
// 우리는 platform driver 이므로 platform_device 객체가 생성되어 전달된다
static int my_probe(struct platform_device *pdev) 
{    
    //platform_device 의 멤버중에  커스텀 데이터를 등록하고 가져올 수 있는 함수인
    //platform_get_drv_data platform_set_drv_data 를 제공하기 때문에 이를 위한 
    //드라이버에서만 사용할 범용 구조체를 생성하여 등록할 수 있다
    // 이 구조체를 만들어아 햐는 이유는 우리가 사용할 GPIO 핸들을 할당하면 그것을 platform_device 에 등록
    // 한 후 함수가 호출될 때 마다 꺼내 써야 하기 때문이다.     

    return 0;
}


그래서 소스 코드 상단에 아래와 같이 선언한다

struct my_drv_data {
    struct platform_device *pdev;        //자신의 platform_device 개체
    int my_gpio;                                   // gpio 핸들 
    int my_irq;                                            // gpio interrupt 를 위한 irq 번호
}


probe 함수가 호출되면 my_drv_data 를 생성하고 DTS 에서 원하는 기능을 가져온 후 두고두고 사용하기 위해
platform_device 에 등록해주면 된다

probe 에 아래의 코드를 넣어준다


struct my_drv_data *pdata = NULL;
int ret = 0;    

if (!g_pdrv) {  // 나중에 file io 를 선언하고 file io 에서 접근하기 위해서 전역 포인터를 선언함
    enum of_gpio_flags flags;
    
    pdata = devm_kzalloc(&pdev->dev, sizeof(struct my_drv_data), GFP_KERNEL); //// my_drv_data 메모리 할당
    if (!pdata)    {            
        return -ENOMEM;
    }
    //of_node DTS 에 파싱된 정보를 가지고 있다 of_XXX 함수를 이용하여 핸들을 가져옴
    pdata->my_gpio = of_get_named_gpio_flags(pdev->dev.of_node, "TEST,my_pin_gpio_0_b3", 0, &flags); 

    if (gpio_is_valid(pdata->my_gpio)) {        //정상으로 가져오면                     
        ret = gpio_direction_output(pdata->my_gpio, 1); // GPIO Direction output 으로 설정 후 1 (high) 로 설정한다.        
    }
    else
        pdata->my_gpio = -1;
}
//자신의 platform_device 객체 저장
pdata->pdev = pdev;
g_pdrv = pdata; // FILE IO 에서 접근하기 위해 g_pdrv 에 저장
// platform_device 에 사용할 고유의 구조체를 설정한다.
platform_set_drvdata(pdev, pdata);     
return 0;


이후 파일 IO 를 통해서 테스트를 하기 위해 File IO 를 위한  misc 드라이버를 생성하자
리눅스는 여러 드라이버 종류가 있단다 (문자, 네트워크, 블록, 기타 등등
우리는 기타 드라이버이기 때문에 misc 드라이버를 사용한다


아래와 같이 misc 드라이버를 위한 장치 구조체와 장치를 위한 file operation 함수 그리고 함수 정의
init / exit 함수에 각각 


static const struct file_operations my_gpio_fops = {
       .owner = THIS_MODULE,      
       .read = my_gpio_read,
       .write = my_gpio_write,
       .unlocked_ioctl = my_gpio_ioctl,
};
static struct miscdevice my_gpio_drv_misc = {
       .minor = MISC_DYNAMIC_MINOR,
       .name = "my_gpio_drv",
       .fops = &my_gpio_fops,
};

init 함수에서 misc 드라이버 추가
misc_register(&my_gpio_drv_misc);

exit 함수에 misc 드라이버 해제 코드 추가 
misc_deregister(&my_gpio_drv_misc);

file operation 함수를 아래와 같이 구현하고 우리는 write 만 쓰기 때문에 아래와 같이 코드 추가 

static ssize_t my_gpio_write(struct file *fileconst char __user *buf, size_t n, loff_t *offset)
{             
       LOG("write called %s \n", buf);   
       if (g_pdrv)
       {
              gpio_set_value(g_pdrv->my_gpio, 1); // GPIO Signal HIGH 설정
              udelay(100);
              gpio_set_value(g_pdrv->my_gpio, 0); // GPIO Signal LOW 설정
       }
       
       return n; // 0으로 하면 무한 루프가 빠진다.
}
static ssize_t my_gpio_read(struct file *file, char *buf, size_t length, loff_t *ofs)
{      
       return 0;
}
static long my_gpio_ioctl(struct file *pfile, unsigned int cmd, unsigned long arg)
{       
       return 0;
}

이와 같이 하고 이미지를 빌드 후 업로드 하면 장치 shell 에서 아래와 같이 등록된 장치가 설치 된것을 확인할 수 잇습니다

# ls /dev

....
my_gpio_drv


# echo write  > /dev/my_gpio_drv


신호를 찍어 보면 High 신호가 떨어진 것을 확인할 수 있다

글이 너무 길어 져서 다음에 Interrupt 사용방법에 대해 정리 해보자~~


댓글 1개:

  1. 감사합니다.
    가뭄에 단비같은 글이었습니다. 덕분에 삽질할 시간을 많이 줄였습니다.

    답글삭제