목차
1. STM32MP157C Discovery Kit 환경 설정
2. GPIO Interrupt Platform Driver
- Device tree and platform driver, GPIO Interrupt(top half)
3. Bottom Halves and Deferring Work
- Kernel timer
- Tasklet
- Workqueue
- Threaded Interrupt(IRQ Thread)
- Kernel Thread
4. References
1. STM32MP157C Discovery Kit 환경 설정
DK2 보드와 관련해서는 이미 이전 blog post를 통해 여러 차례 소개한 바가 있다. DK2 보드가 생소한 분들은 먼저 아래 blog post의 내용을 참조하기 바란다.
지금부터는 아래 ST wiki page의 내용을 기초로 하여 (이번 blog post에서 사용할) ARM cross toolchain(정확하게는 Yocto SDK) 및 linux kernel 등을 설치해 보도록 하겠다.
<Yocto SDK 설치>
$ ./st-image-weston-openstlinux-weston-stm32mp1-x86_64-toolchain-3.1-openstlinux-5.4-dunfell-mp1-20-06-24.sh
ST OpenSTLinux - Weston - (A Yocto Project Based Distro) SDK installer version 3.1-openstlinux-5.4-dunfell-mp1-20-06-24
=======================================================================================================================
Enter target directory for SDK (default: /opt/st/stm32mp1/3.1-openstlinux-5.4-dunfell-mp1-20-06-24):
You are about to install the SDK to "/opt/st/stm32mp1/3.1-openstlinux-5.4-dunfell-mp1-20-06-24". Proceed [Y/n]? y
[sudo] chyi의 암호:
Extracting SDK................................................................................................................................................................................................................done
Setting it up...done
SDK has been successfully set up and is ready to be used.
Each time you wish to use the SDK in a new shell session, you need to source the environment setup script e.g.
[Tip] (설치 후 확인해 보니) ARM용 gcc compiler는 아래 디렉토리에 존재한다.
/opt/st/stm32mp1/3.1-openstlinux-5.4-dunfell-mp1-20-06-24/sysroots/x86_64-ostl_sdk-linux/usr/bin/arm-ostl-linux-gnueabi
$ . /opt/st/stm32mp1/3.1-openstlinux-5.4-dunfell-mp1-20-06-24/environment-setup-cortexa7t2hf-neon-vfpv4-ostl-linux-gnueabi
-> 이 파일에는 cross-compile과 관련한 모든 환경 변수(SYSROOT, compile path 등)가 담겨 있다. 이 명령을 매번 실행하는 것 보다는 ~/.bashrc에 넣어 두면 편리하다.
-> 설치된 gcc(arm-ostl-linux-gnueabi-gcc) version이 9.3.0임을 알 수 있다.
<kernel download & build>
$ tar xvf en.SOURCES-kernel-stm32mp1-openstlinux-20-02-19.tar.xz
$ cd stm32mp1-openstlinux-20-02-19/sources/arm-ostl-linux-gnueabi/linux-stm32mp-4.19-r0
$ tar xvf linux-4.19.94.tar.xz
$ cd linux-4.19.94
$ make menuconfig
$ make uImage vmlinux dtbs LOADADDR=0xC2000040 -j8
$ make modules
$ mkdir -p $PWD/install_artifact/
$ make INSTALL_MOD_PATH="$PWD/install_artifact" modules_install
이 정도로 해서 간단하게 device driver(or kernel programming) 개발 환경 설정 작업을 마무리하도록 하겠다. 보다 자세한 사항은 참고 문헌 [1]을 참조하기 바란다.
2. GPIO Interrupt Platform Driver
필자는 이미 Linux kernel programming(or device driver programming)에 관하여 (3년 전에) 상세히 정리한 바가 있다.
and
하지만 Linux kernel은 나날이 발전(오늘 현재 5.7.9가 release되어 있음)하고 있어서, 위의 내용으로는 충분치 못하다는 생각이 들었다. 따라서 이번 posting에서는 여러 주제 중 interrupt 관련 부분만을 뽑아 최신 kernel (linux 4.19.94) 에 맞게 재 정리해 보고자 한다.
2.1) DK2 보드의 User Button
DK2 보드에는 사용자용 버튼(User1, User2)이 두개 있다. 이 두개의 버튼은 본래 u-boot에서 firmware writing mode(User1: USB programming mode, User2: Android fastboot mode)로 진입하기 위해 만들어졌지만, 필자는 이를 gpio interrupt 처리용으로 잠시 용도변경해 볼 생각이다.
[그림 2.1] DK2 보드의 사용자 버튼(User1, User2)
[Tip] 위의 그림에서 User2 버튼이 B2로 표기된 부분은 오타인 듯 보인다. 즉, B2 -> B4로 표기되는 것이 맞을 것 같다.
[그림 2.2] DK2 보드의 사용자 버튼(User1, User2)의 pinmap
[그림 2.3] DK2 보드의 block도 중 User button(우측) 발췌
2.2) Device Tree 내용 추가
앞서 언급한 User1 버튼은 아래 그림과 같이 PA14 pin(GPIO A bank 14번째 pin)에 연결되어 있다.
[그림 2.4] User1 버튼에 대한 회로도 - push-pull 회로
DK2 보드용 device tree 파일 내용을 살펴보면, STMP157C 칩(SoC)에는 대략 10개(gpio A ~ I, Z) 정도의 GPIO bank가 존재하는 것 같다. 또한 하나의 GPIO bank에는 16개의 GPIO pin이 포함되어 있음을 알 수 있다.
[그림 2.5] STMP157C의 GPIO controller에 대한 device tree 표현 - stm32mp157cac-pinctrl.dtsi 파일에서 발췌
[그림 2.6] kernel booting message
<여기서 잠깐 !>
-> DK2 보드의 device tree에 관하여
1장에서 사용한 kernel source를 기준으로 device tree 파일(linux-4.19.94/arch/arm/boot/dts/stmp32mp*)의 상관 관계를 파악해 보면 대략 다음과 같다.
stm32mp157c.dtsi
stm32mp157cac-pinctrl.dtsi -> stm32mp157-pinctrl.dtsi
stm32mp157c-m4-srm.dtsi
^
|
stm32mp157a-dk1.dts
^
|
stm32mp157c-dk2.dts
[그림 2.7] DK2 보드의 device tree 상관 관계도
그런데, 이 내용은 이전 blog post에서 분석한 내용(buildroot 기반)과는 사뭇 차이가 난다. 왜 그럴까 ?
[그림 2.8] DK2 보드의 device tree 상관 관계도 - buildroot 기준
이는 아마도, Yocto 환경(linux-4.19.94)과 buildroot 환경(linux-5.7.1)에서 오는 차이인 듯하다. 아니, kernel 버젼이 다른 것이 더 큰 이유가 될 것 같다.
어찌됐든 우리는 이 중에서 User1 button을 활용한 GPIO interrupt 부분에 관심이 있으니, 이와 관련해서 좀 더 분석해 보기로 하자.
<gpio controller & device 상호 관계>
intc(GIC) <---- gpioa controller <--- User1 button
(interrupt-parent) (interrupt device)
<gpioa controller node>
[그림 2.9] gpioa bank(gpio a controller) - stm32mp157-pinctrl.dtsi
[그림 2.10] gpioa bank(gpio a controller) - stm32mp157cac-pinctrl.dtsi
GPIO 속성 설정과 관련해서는 아래 문서를 참조할 필요가 있다.
Documentation/devicetree/bindings/gpio/gpio.txt
User1 button 용 device tree node는 대략 다음과 같다(예상해 볼 수 있다).
<User1 button - gpio device node>
user1_button {
compatible = "blahblahblah";
user1-gpios = <&gpioa 14 GPIO_ACTIVE_LOW>;
interrupt-parent = <&gpioa>;
interrupts = <14 IRQ_TYPE_EDGE_FALLING>;
};
<속성 정보 설명>
[<name>-]gpios 속성 지정자
=> bank(phandle), pin 번호, active-high or active-low value
[참고] <name>-gpios 형태로 사용해야 함(new style). <name>- 없이 gpios, gpio도 사용 가능(하위 버젼 호환성 차원에서 지원)
interrupt-parent 속성
=> interrupt controller(여기서는 gpioa)에 대한 phandle 값
interrupts 속성
=> interrupt signal을 내보내는 device(gpio pin/pad)의 pin 값(여기서는 14)과, IRQ type(include/dt-bindings/interrupt-controller/irq.h 내용 참조) 지정
-- -- -- -- --
GPIO pin을 제대로 활용하기 위해서는 pin control 설정에 대해서도 정확히 알고 있어야 한다. 이와 관련해서는 아래 문서를 참조하도록 하자.
Documentation/devicetree/bindings/pinctrl/st,stm32-pinctrl.txt
[그림 2.11] Documentation/devicetree/bindings/pinctrl/st,stm32-pinctrl.txt
---------------
이상의 내용을 기초로 device tree 코드를 구현해 보도록 하자.
<stm32mp157-pinctrl.dtsi에 추가할 내용>
pinctrl_user2_button_a: user2-pb-0 {
pins {
pinmux = <STM32_PINMUX('A', 14, GPIO)>;
drive-push-pull;
};
};
[Tip] STM32_PINMUX macro는 include/dt-bindings/pinctrl/stm32-pinfunc.h 파일에 정의되어 있다.
[Tip] 위에서 drive-push-pull을 지정한 이유는 그림 2.4의 회로도를 유심히 살펴보면 알 수가 있다. Push-pull 출력은 2개의 TR(transistor)로 구성되어 있는데, 아래 스위치(TR)를 누르면 출력 핀(PA14)이 GND와 연결되어 LOW가 출력(전류 싱크)되고, 위쪽 스위치를 누르면 출력 핀은 VCC(3.3V)와 연결되어 HIGH가 출력(전류 소스)된다. Push-pull과 대비되는 개념으로 open-drain/collector가 있는데, 얘는 전류 싱크로만 동작 가능하다. 즉, LOW 상태와 회로가 open된 상태인 하이 임피던스만 존재한다.
<stm32mp157a-dk1.dts에 추가할 내용>
my_button_interrupt {
compatible = "eagle,int-button";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_user2_button_a>; /* stm32mp157-pinctrl.dtsi에서 정의 */
mybtn-gpios = <&gpioa 14 GPIO_ACTIVE_LOW>; /* new style gpio specifier */
interrupt-parent = <&gpioa>;
interrupts = <14 IRQ_TYPE_EDGE_FALLING>;
};
Device tree 작업을 마쳤으니, 관련 내용을 target board에 반영시켜 볼 차례이다^^.
$ cd linux-4.19.94
$ make dtbs
-> 지금까지 작업한 내용을 compile해 보자.
[그림 2.13] compile 후, dtb 파일 생성
다음으로, compile 결과 파일(dtb 파일)을 target board에 올려 보도록 하자.
<Target board>
root@stm32mp1:~# ifconfig eth0 192.168.1.100 netmask 255.255.255.0 up
<Desktop PC>
$ cd arch/arm/boot
$ scp ./uImage root@192.168.1.100:~/workspace/boot
-> device tree 파일만 수정했으나, 현재 동작중인 kernel과 새로 build한 kernel 간에 차이가 있을 수 있으므로, kernel image(uImage)도 함께 교체하도록 한다.
$ cd dts
$ scp ./stm32mp157c-dk2.dtb root@192.168.1.100:~/workspace/boot
$ scp ./stm32mp157c-dk2-a7-examples.dtb root@192.168.1.100:~/workspace/boot
$ scp ./stm32mp157c-dk2-m4-examples.dtb root@192.168.1.100:~/workspace/boot
[Tip] DK2 보드는 현재 microSD로 부팅한 상태이며, /boot 디렉토리에 kernel과 dtb 파일 등이 위치하고 있다.
[그림 2.14] target board /boot 디렉토리
<Target board>
root@stm32mp1:~# cd ~/workspace/boot
root@stm32mp1:~# cp ./uImage /boot
root@stm32mp1:~# cp ./*.dtb /boot
root@stm32mp1:~# sync; sync; reboot
-> 파일 교체 후에는 반드시 부팅을 하도록 한다.
2.3) Platform driver 구현
Device tree 작업이 끝났으니, 이제 부터는 이를 사용하는 platform driver(gpio interrupt driver)를 구현해 볼 차례이다.
원래는 driver 작성 과정을 상세히 설명할 계획이었으나, 시간 관계상 source code의 위치를 소개하는 것으로 이를 대신하고자 한다.
[그림 2.15] GPIO interrupt - platform driver probe 함수 소개
그럼, driver가 제대로 동작하는지 확인해 보도록 하자.
<Desktop PC>
$ git clone https://github.com/ChunghanYi/linux_kernel_hacks
-> 위의 코드는 필자의 github에 올려 두었으니, 이를 먼저 내려 받도록 한다.
$ cd linux_kernel_hacks/writing_device_drivers_by_coopj/s_08
$ ../genmake
-> Makefile을 자동으로 생성해준다.
$ make clean
$ make
$ file lab3_interrupt.ko
-> cross-compile이 제대로 되었는지 확인해 본다.
$ scp ./lab3_interrupt.ko root@192.168.1.100:~/workspace
-> scp 명령을 사용하여 target board로 kernel module을 올린다.
<Target board>
root@stm32mp1:~/workspace# insmod ./lab3_interrupt.ko
-> insmod 명령을 사용하여 kernel module을 구동시킨다.
[ 3769.676181] int-button my_button_interrupt: my_probe() function is called.
[ 3769.681772] stm32mp157-pinctrl soc:pin-controller@50002000: pin PA14 already requested by my_button_interrupt; cannot claim for GPIOA:14
[ 3769.694580] stm32mp157-pinctrl soc:pin-controller@50002000: pin-14 (GPIOA:14) status -22
[ 3769.701946] int-button my_button_interrupt: gpio get failed
[ 3769.707667] int-button: probe of my_button_interrupt failed with error -22
어랍쇼~ 에러가 발생한다. 어디가 문제일까 ? "pin-controller ... pin PA14 already requested"라는 부분이 보이는데 ?!
이상하다. GPIOA 14 pin에 대한 설정을 다른 곳에서 이미 하고 있나 ? 그렇다면, 일단 pinctrl 설정 부분을 막고 테스트해 보자.
[그림 2.16] device tree 수정 버젼(stm32mp157a-dk1.dts) - pinctrl 부분 제거
<Target board>
root@stm32mp1:~/workspace# insmod ./lab3_interrupt.ko
-> 수정 후, 다시 테스트(반복되는 과정은 생략)해 보니, 정상 동작한다.
-> User1 버튼을 누르면, 한줄씩 메시지가 출력된다.
[그림 2.17] lab3_interrupt.ko 모듈 정상 동작 모습
[Tip] 왜 PA14 already requested 메시지가 출력되는지를 알기 위해서는 전체 device tree 부분을 면밀히 검토해 보아야 할 듯하다.
<여기서 잠깐 !>
-> devm_request_irq( ) 함수에 대해...
-------------------------------------------------------------------------------------------------------------------
int devm_request_irq(struct device *dev,
unsigned int irq,
irq_handler_t handler,
unsigned long irq_flags,
const char *devname,
void *dev_id);
unsigned int irq,
irq_handler_t handler,
unsigned long irq_flags,
const char *devname,
void *dev_id);
a) interrupt handler를 등록한다.
b)기존에 많이 사용하던 request_irq( ) 함수와 비교해, 첫번째 argument가 struct device pointer라는 점에서 차이가 있는데, 이런 함수를 일컬어 managed resource API(workqueue로 유명한 허태준씨가 2007년 kernel 2.6.21부터 제안함)라고 부른다.
c) request_irq()는 사용 후, free_irq()를 반드시 해 주어야 하나, devm_request_irq()는 driver 제거 or probe 실패 시 자동으로 memory를 해제시켜 주는 특징이 있다.
d) 자세한 사항은 참고문헌 [4]를 참고하도록 하자.
- dev: device나 module이 해제될 때에 자동으로 제거되는 장치에의 pointer
- irq: IRQ number. platform device의 경우는 platform_get_irq() 함수를 통해 얻은 값을 사용하면 됨.
- handler: IRQ handler 함수 pointer
- irq_flags: IRQ flag 값(아래 값을 사용할 수 있음)
#define IRQF_TRIGGER_NONE 0x00000000
#define IRQF_TRIGGER_RISING 0x00000001
#define IRQF_TRIGGER_FALLING 0x00000002
#define IRQF_TRIGGER_HIGH 0x00000004
#define IRQF_TRIGGER_LOW 0x00000008
#define IRQF_TRIGGER_MASK (IRQF_TRIGGER_HIGH | IRQF_TRIGGER_LOW | \
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING)
#define IRQF_TRIGGER_PROBE 0x00000010
- devname: interrupt를 위해 사용하는 이름(/proc/interrupts로 확인 가능)
- dev_id: device별 data structure를 가리키는 pointer를 전달하기 위해서 사용함.
-------------------------------------------------------------------------------------------------------------------
d) 자세한 사항은 참고문헌 [4]를 참고하도록 하자.
- dev: device나 module이 해제될 때에 자동으로 제거되는 장치에의 pointer
- irq: IRQ number. platform device의 경우는 platform_get_irq() 함수를 통해 얻은 값을 사용하면 됨.
- handler: IRQ handler 함수 pointer
- irq_flags: IRQ flag 값(아래 값을 사용할 수 있음)
#define IRQF_TRIGGER_NONE 0x00000000
#define IRQF_TRIGGER_RISING 0x00000001
#define IRQF_TRIGGER_FALLING 0x00000002
#define IRQF_TRIGGER_HIGH 0x00000004
#define IRQF_TRIGGER_LOW 0x00000008
#define IRQF_TRIGGER_MASK (IRQF_TRIGGER_HIGH | IRQF_TRIGGER_LOW | \
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING)
#define IRQF_TRIGGER_PROBE 0x00000010
- devname: interrupt를 위해 사용하는 이름(/proc/interrupts로 확인 가능)
- dev_id: device별 data structure를 가리키는 pointer를 전달하기 위해서 사용함.
-------------------------------------------------------------------------------------------------------------------
이상으로 기본적인 GPIO interrupt driver(platform driver)와 관련 device tree 작업을 진행해 보았다.
3. Bottom Halves and Deferring Work
이제부터는 2장에서 작성한 platform driver의 내용을 조금씩 변경해 가면서, kernel timer, tasklet, threaded interrupt, work queue, kernel thread 등에 관하여 살펴 보도록 하자. Kernel이 upgrade되면서 많은 부분이 달라졌다. 따라서 예전에 정리해 둔 내용으로는 문제가 있다(하지만, 그림은 나름 쓸만하여 copy & paste해 보았다 😅).
[그림 3.1] Linux interrupt(top half)와 Bottom halves 개념도
[그림 3.2] Interrupt와 interrupt handler 개요도
본격적인 내용 설명에 앞서 (좀 오래된 문서이기는 하지만) 필자가 오래 전에 작성해 둔 참고 문헌 [3]을 반드시 훑어 볼 것을 권한다. 지면 & 시간 관계상 모든 것을 다시 설명을 할 수는 없으니 말이다. 😂
3.1) Kernel Timer
<개요>
[그림 3.3] kernel timer 개요도
[Tip] kernel timer API는 예전 버젼(3.x 이하, 시점은 정확하지 않음)과 많은 차이를 보인다. 특히 초기화 함수인 init_timer() 대신 timer_setup()를 사용해야 한다. 그 밖에도 많은 부분에 변화가 있다. 아래 예제 코드를 통해 직접 확인해 보기 바란다.
<Example code>
[그림 3.4] kernel timer 예제 코드
<실행 모습>
root@stm32mp1:~/workspace# insmod ./lab4_periodic_timers_alt.ko
-> 구동해 보니, 두개의 kernel timer가 서로 다른 주기로 message를 출력함을 알 수 있다.
[그림 3.5] kernel timer 예제 실행 모습
[Tip] 편의 상, kernel timer example은 2장에서 소개한 platform driver랑은 무관하게 작성하였다.
3.2) Tasklet
<개요>
[그림 3.6] tasklet 개요도
<Example code>
[그림 3.7] tasklet 예제 코드
<실행 모습>
root@stm32mp1:~/workspace# insmod ./lab_platform_interrupt_tasklet.ko
-> 구동 후, User1 button을 누르면 tasklet code(아래 그림 하단의 message 출력)가 정상 동작한다.
[그림 3.8] tasklet 예제 실행 모습
3.3) Workqueue
<개요>
[그림 3.9] workqueue 개요도
<Example code>
[그림 3.10] workqueue 예제 코드
<실행 모습>
root@stm32mp1:~/workspace# insmod ./lab_platform_interrupt_workqueue.ko
-> 구동 후, User1 button을 누르면 workqueue code(아래 그림 하단의 message)가 정상 동작한다.
3.4) Threaded Interrupt
<개요>
[그림 3.12] threaded interrupt(threaded irq) 개요도
<Example code>
[그림 3.13] threaded interrupt 예제 코드
<실행 모습>
root@stm32mp1:~/workspace# insmod ./lab_platform_interrupt_threaded_irq.ko
-> 구동 후, User1 button을 누르면 irq thread routine(아래 그림 하단의 message)이 정상 동작한다.
[그림 3.14] threaded interrupt 예제 실행 모습
3.5) Kernel Thread
<개요>
[그림 3.15] kernel thread 개요도
<Example code>
[그림 3.16] kernel thread 예제 코드
<실행 모습>
root@stm32mp1:~/workspace# insmod ./lab_platform_interrupt_thread.ko
[24771.478353] int-button my_button_interrupt: The irq number is 68.
[24771.483115] int-button my_button_interrupt: platform_get_irq(): 68
[24771.489646] int-button my_button_interrupt: Successfully loading ISR handler.
-> 구동 후, User1 button을 누르면 thread routine(아래 그림 하단의 message)이 정상 동작한다.
[그림 3.17] kernel thread 예제 실행 모습
이상으로 kernel 4.19.94 환경에서 GPIO interrupt & bottom halves 관련 platform driver & device tree 작성에 관하여 소개해 보았다. 대부분 간단한(?) 수준의 코드(하지만, 누군가에게는 꼭 필요한 코드)이므로 이해하는데는 크게 무리가 없었을 것으로 믿는다. 😋
May the source be with you~
4. References
[2] Linux Device Drivers Development, John Madieu, Packt
[Tip] 책 내용 중에 몇가지 오류가 있기는 하나, 나름 최신 kernel 내용을 언급하고 있는 (아주) 좋은 책이다. 근데, 왜 Amazon은 Packt(늘 느끼는 거지만 출판 상태가 별로 좋지 못함) 책을 열심히 밀고 있는 걸까 ?