오래간만에 새로운 Board를 하나 입수했다. 이름하여 BeaglePlay~ 앞으로 몇차례에 걸쳐 BeaglePlay 보드를 파헤쳐 보는 시간을 가져 보고자 한다(3번째 시간). 😎
The 3rd time
목차
6. Device Tree & Device Drivers
7. References
Device Tree는 이제는 완전 주류(시쳇말로 인싸^^)가 되었다. x86을 제외한 대부분의 CPU에서 사용하고 있고, 이제는 bootloader(u-boot), zephyr 같은 RTOS에서도 사용되고 있다. 소위 BSP(Board Support Package) 개발자에게는 대세 중의 대세인 Yocto project와 더불어 Device Tree를 접수(?)해야 하는 사명 과제가 남았다.ㅋ 너무 거창한가 ~ 😋
이번 posting은 초심자가 쉽게 따라할 수 있도록 Embedded Linux Programming의 기초 과정을 담고자 하였다. 이번 posting의 내용과 더불어 이전 posting 내용도 함께 참고하면 좋을 듯 싶다.
6. Device Tree & Device Drivers
이번 장에서는 BeaglePlay를 위한 Device Tree와 Device Driver에 관한 얘기를 해 보고자 한다.
6.1 BeaglePlay Device Tree
우선, 이전 blog post에서도 언급한 바와 같이, TI AM62x SoC은 아래와 같이 3개의 h/w domain으로 구성되어 있다는 사실을 다시한번 강조하고 싶다. 그 이유는 BeaglePlay 보드 회로도나 device tree 내용을 보다보면, 아래의 3개의 domain에 대한 내용이 자연스럽게 등장하기 때문이다.
<AM62x SoC의 3 h/w domains>
- Main domain - 4 Cortex-A53(64bit)이 cover하는 영역(대분의 주요 주변 장치 포함, linux kernel 동작)
- MCU domain - Cortex-M4F(32bit) processor가 cover하는 영역(booting process, power/resource manangement 담당)
- WKUP(wake-up) domain - Cortex-R5F(32bit) proessor가 cover하는 영역(deep sleep mode시 사용)
[그림 6.1] TI AM62x SoC Interconnect Overview [출처 - 참고문헌 4]
📌 CBASS(Common Bus Architecture SubSystem)는 통신하고자 하는 IP 간의 물리적인 연결을 제공하는 crossbar 모듈이다(쉽게 말해 bus interface를 말함).
아래 그림은 BeaglePlay의 전체 block diagram으로 중앙에 위치한 AM62x SoC와 좌측의 PMIC(DCDC, LDO 등 포함), 그리고 우측의 주요 device(peripheral) 등으로 구성되어 있다.
[그림 6.2] BeaglePlay 보드 Block Diagram [출처 - 참고문헌 1,2]
또한, 다음 그림은 Power intput 관련 구성도(앞선 그림의 좌측에 해당하는 내용)인데, USB-C(5V/3A)를 통해 들어온 전원이 PMIC를 거쳐 각종 controller 및 최종 device 까지 흘러가는 과정을 보여준다. 전원 입력 부분은 board를 제어할 때 매우 중요한 부분으로, 경우에 따라서는 driver code를 통해 regulator등을 제어해야 할 수도 있다.
[그림 6.3] BeaglePlay 보드 Power Tree [출처 - 참고문헌 1,2]
마지막으로, 아래 그림은 i2c controller와 이에 연결되어 있는 i2c device들 간의 관계를 보여준다. (그림에는 표시되지 않았지만) I2C0 ~ I2C3 즉 4개의 i2c controller가 main domain에 존재하고, MCU domain에는 I2C0 controller 하나가, WKUP domain에도 I2C0 controller 하나가 보인다. 우측에는 I2C controller에 연결되는 slave device들이 나열되어 있다. (i2c device tree & driver 구현을 위해) 아래 그림에서 I2C3 controller에 mikroBUS device(확장핀)가 연결되어 있다는 사실을 특별히 기억해 두기 바란다.
자, 그럼 본격적으로, BeaglePlay 보드의 device tree를 분석해 보자.
___________________________________________________________
k3-pinctrl.h (pinctrl bindings for TI's K3 Soc family)
k3-am62.dtsi, k3-am62-main.dtsi, k3-am62-mcu.dtsi, k3-am62-wakeup.dtsi
^
|
k3-am625.dtsi
^
|
k3-am625-beagleplay.dts
___________________________________________________________
[그림 6.5] BeaglePlay 보드 device tree 상속 관계도
먼저 맨 상단의 k3-am62.dtsi 파일이 am62x soc 모델, 즉 "Texas Instruments K3 AM625 SoC"의 출발점이다. 여기에서는 main bus(cbass)가 정의되어 있고, 아래 3개의 파일을 통해 3개의 h/w domain 각각의 bus segment에 연결된 주변 장치를 기술하고 있다.
#include "k3-am62-main.dtsi"
=> main cbass bus에 연결된 주변 장치를 기술한다.
#include "k3-am62-mcu.dtsi"
#include "k3-am62-mcu.dtsi"
=> mcu cbass bus에 연결된 주변 장치를 기술한다.
#include "k3-am62-wakeup.dtsi"
#include "k3-am62-wakeup.dtsi"
=> wakeup cbass bus에 연결된 주변 장치를 기술한다.
[그림 6.6] k3-am62.dtsi 내용 중에서 발췌
다음으로 k3-am625.dtsi에는 주로 A53 CPU에 관한 내용(이 외에도 opp table과 L2 cache 등도 기술)이 담겨 있다.
[그림 6.7] k3-am625.dtsi 내용 중에서 발췌
마지막으로, k3-am625-beagleplay.dts는 beagleplay용 dts 파일을 정의하고 있다. 이 파일에는 지금까지 상위 dtsi 파일에서 언급되지 않았던 memory 부분이 기술되고 있으며, 상위 dtsi 파일(특히 k3-am62-main.dtsi 파일)에 정의된 대부분의 peripheral controller에 대한 설정이 재정의(override)되어 있다.
<주요 주변 장치 controller에 대한 override 설정 예>
&main_pmx0 {
gpio0_pins_default: gpio0-default-pins {
pinctrl-single,pins = <
AM62X_IOPAD(0x0004, PIN_INPUT, 7) /* (G25) OSPI0_LBCLKO.GPIO0_1 */
AM62X_IOPAD(0x0008, PIN_INPUT, 7) /* (J24) OSPI0_DQS.GPIO0_2 */
AM62X_IOPAD(0x000c, PIN_INPUT, 7) /* (E25) OSPI0_D0.GPIO0_3 */
AM62X_IOPAD(0x0010, PIN_INPUT, 7) /* (G24) OSPI0_D1.GPIO0_4 */
AM62X_IOPAD(0x0014, PIN_INPUT, 7) /* (F25) OSPI0_D2.GPIO0_5 */
AM62X_IOPAD(0x0018, PIN_INPUT, 7) /* (F24) OSPI0_D3.GPIO0_6 */
AM62X_IOPAD(0x0024, PIN_INPUT, 7) /* (H25) OSPI0_D6.GPIO0_9 */
AM62X_IOPAD(0x0028, PIN_INPUT, 7) /* (J22) OSPI0_D7.GPIO0_10 */
AM62X_IOPAD(0x002c, PIN_INPUT, 7) /* (F23) OSPI0_CSn0.GPIO0_11 */
AM62X_IOPAD(0x0030, PIN_INPUT, 7) /* (G21) OSPI0_CSn1.GPIO0_12 */
AM62X_IOPAD(0x0034, PIN_INPUT, 7) /* (H21) OSPI0_CSn2.GPIO0_13 */
AM62X_IOPAD(0x0038, PIN_INPUT, 7) /* (E24) OSPI0_CSn3.GPIO0_14 */
AM62X_IOPAD(0x00a4, PIN_INPUT, 7) /* (M22) GPMC0_DIR.GPIO0_40 */
AM62X_IOPAD(0x00ac, PIN_INPUT, 7) /* (L21) GPMC0_CSn1.GPIO0_42 */
>;
};
gpio0_pins_default: gpio0-default-pins {
pinctrl-single,pins = <
AM62X_IOPAD(0x0004, PIN_INPUT, 7) /* (G25) OSPI0_LBCLKO.GPIO0_1 */
AM62X_IOPAD(0x0008, PIN_INPUT, 7) /* (J24) OSPI0_DQS.GPIO0_2 */
AM62X_IOPAD(0x000c, PIN_INPUT, 7) /* (E25) OSPI0_D0.GPIO0_3 */
AM62X_IOPAD(0x0010, PIN_INPUT, 7) /* (G24) OSPI0_D1.GPIO0_4 */
AM62X_IOPAD(0x0014, PIN_INPUT, 7) /* (F25) OSPI0_D2.GPIO0_5 */
AM62X_IOPAD(0x0018, PIN_INPUT, 7) /* (F24) OSPI0_D3.GPIO0_6 */
AM62X_IOPAD(0x0024, PIN_INPUT, 7) /* (H25) OSPI0_D6.GPIO0_9 */
AM62X_IOPAD(0x0028, PIN_INPUT, 7) /* (J22) OSPI0_D7.GPIO0_10 */
AM62X_IOPAD(0x002c, PIN_INPUT, 7) /* (F23) OSPI0_CSn0.GPIO0_11 */
AM62X_IOPAD(0x0030, PIN_INPUT, 7) /* (G21) OSPI0_CSn1.GPIO0_12 */
AM62X_IOPAD(0x0034, PIN_INPUT, 7) /* (H21) OSPI0_CSn2.GPIO0_13 */
AM62X_IOPAD(0x0038, PIN_INPUT, 7) /* (E24) OSPI0_CSn3.GPIO0_14 */
AM62X_IOPAD(0x00a4, PIN_INPUT, 7) /* (M22) GPMC0_DIR.GPIO0_40 */
AM62X_IOPAD(0x00ac, PIN_INPUT, 7) /* (L21) GPMC0_CSn1.GPIO0_42 */
>;
};
..
&main_i2c3 {
pinctrl-names = "default";
pinctrl-0 = <&mikrobus_i2c_pins_default>;
clock-frequency = <400000>;
status = "okay";
};
pinctrl-names = "default";
pinctrl-0 = <&mikrobus_i2c_pins_default>;
clock-frequency = <400000>;
status = "okay";
};
이 밖에도 beagleplay board에만 존재하는 LED, buttons, regulator, hdmi, sound, mdio(ethernet 관련) 등에 관한 내용이 보인다.
[그림 6.8] k3-am625-beagleplay.dts 내용 중에서 발췌
언제나 느끼는 거지만, device tree는 생각보다 어렵다. Device tree를 처음부터 만드는 사람들(chip vendor)은 (지극히 당연한 얘기지만) 해당 chip에 대해 심도있게 이해하고 있어야만 가능하다. 😓 그렇다고 쫄지는(?) 말자. 천리길도 한걸음 부터다~ 고민하는 만큼, 알게 된다. 🏃
Device Tree 관련해서는 최신의 spec 문서를 참조할 필요가 있어 보인다.
또한, device tree 관련하여 이전 blog post 내용도 어느정도는 도움일 될 것으로 믿는다(역시 오래되었지만 그런대로 쓸만하다).
아, 그리고 아래 Thomas Petazzoni의 문서도 필히 참조해야 한다.
이상으로 BeaglePlay board의 device tree를 개략적으로 확인해 보았다. 지금 부터는 몇가지 예제를 통해 device tree & device driver를 구현하는 방법을 소개해 보도록 하자.
6.2 User Button을 이용한 GPIO Interrupt Driver 구현하기
이번 절에서는 BeaglePlay의 3개 버튼 중 User button을 용도 변경하여, 버튼을 누를 때(interrupt 발생 시)마다, 특정 message를 출력하는 GPIO 기반의 interrupt driver를 작성하는 방법을 소개해 보도록 하자.
우선 [그림 6.2] 전체 block도의 좌측 하단을 보면, User Button이 GPIO pin을 사용하는 것으로 표시되어 있다. 따라서, 회로도 상으로 User Button이 몇번 GPIO를 사용하는지를 확인해 볼 필요가 있다. Hmm ... 그런데, 이상하게도 아무리 눈씻고(?) 찾아 보아도 관련 내용이 보이질 않는다. 😓
[그림 6.9] BeaglePlay User 버튼 관련 회로도 [출처 - 참고문헌 1,2]
그렇다면, dts 파일을 한번 들여다 보기로 하자. 휴~, 다행히도 dts 파일에는 아래와 같이 user button에 대한 내용이 보인다. 아래 내용을 보니, main_gpio0 bank의 18번 pin을 사용하는 것 같다.
gpio_keys: gpio-keys {
compatible = "gpio-keys";
autorepeat;
pinctrl-names = "default";
pinctrl-0 = <&usr_button_pins_default>;
usr: button-usr {
label = "User Key";
linux,code = <BTN_0>;
gpios = <&main_gpio0 18 GPIO_ACTIVE_LOW>; // <=== 여기
};
};
compatible = "gpio-keys";
autorepeat;
pinctrl-names = "default";
pinctrl-0 = <&usr_button_pins_default>;
usr: button-usr {
label = "User Key";
linux,code = <BTN_0>;
gpios = <&main_gpio0 18 GPIO_ACTIVE_LOW>; // <=== 여기
};
};
드라이버 작업을 해야 하니, 위의 코드를 막고, 아래와 같은 새로운 내용을 추가하도록 하자.
[그림 6.10] User button용 device tree 내용 추가
위의 수정 사항을 반영한 Device tree 파일을 build해 보자.
$ . /opt/beagleplay/environment-setup-aarch64-poky-linux
$ export ARCH=arm64
$ export CROSS_COMPILE=aarch64-poky-linux-
$ make dtbs
UPD include/config/kernel.release
DTC arch/arm64/boot/dts/ti/k3-am625-beagleplay.dtb
$ export ARCH=arm64
$ export CROSS_COMPILE=aarch64-poky-linux-
$ make dtbs
UPD include/config/kernel.release
DTC arch/arm64/boot/dts/ti/k3-am625-beagleplay.dtb
______________________________________________________________
<bp_gpiokey.c>
/*
* bp_gpiokey.c - GPIO interrupt using user button on BeaglePlay board
*/
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/interrupt.h>
#include <linux/gpio/consumer.h>
#include <linux/miscdevice.h>
#include <linux/of_device.h>
static char *_NAME = "User Key";
/* interrupt handler */
static irqreturn_t interrupt_isr(int irq, void *data)
{
struct device *dev = data;
dev_info(dev, "interrupt received. key: %s\n", _NAME);
return IRQ_HANDLED;
}
static struct miscdevice basic_miscdevice = {
.minor = MISC_DYNAMIC_MINOR,
.name = "mydev",
};
static int my_probe(struct platform_device *pdev)
{
int ret_val, irq;
struct gpio_desc *gpio;
struct device *dev = &pdev->dev;
printk("my_probe() function is called.\n");
/* First method to get the Linux IRQ number */
gpio = devm_gpiod_get_optional(dev, "mybtn", GPIOD_IN);
if (IS_ERR(gpio)) {
printk("devm_gpiod_get_optional() failed\n");
return PTR_ERR(gpio);
}
irq = gpiod_to_irq(gpio);
if (irq < 0)
return irq;
printk("The IRQ number is: %d\n", irq);
/* Second method to get the Linux IRQ number */
irq = platform_get_irq(pdev, 0);
if (irq < 0) {
printk("irq is not available\n");
return -EINVAL;
}
printk("platform_get_irq(): %d\n", irq);
/* Allocate the interrupt line */
ret_val = devm_request_irq(dev, irq, interrupt_isr, IRQF_TRIGGER_FALLING, _NAME, dev);
if (ret_val) {
printk("Failed to request interrupt %d, error %d\n", irq, ret_val);
return ret_val;
}
ret_val = misc_register(&basic_miscdevice);
if (ret_val != 0) {
printk("could not register the misc device mydev\n");
return ret_val;
}
dev_info(dev, "Successfully loading ISR handler.\n");
return 0;
}
static int my_remove(struct platform_device *pdev)
{
printk("my_remove() function is called.\n");
misc_deregister(&basic_miscdevice);
printk("my_remove() function is exited.\n");
return 0;
}
static const struct of_device_id my_of_ids[] = {
{ .compatible = "beagleplay,userbutton"},
{},
};
MODULE_DEVICE_TABLE(of, my_of_ids);
static struct platform_driver my_platform_driver = {
.probe = my_probe,
.remove = my_remove,
.driver = {
.name = "userbutton",
.of_match_table = my_of_ids,
.owner = THIS_MODULE,
}
};
module_platform_driver(my_platform_driver);
MODULE_AUTHOR("Chunghan Yi <chunghan.gi@gmail.com>");
MODULE_DESCRIPTION("user button platform driver for GPIO interrupt test");
MODULE_LICENSE("GPL v2");
* bp_gpiokey.c - GPIO interrupt using user button on BeaglePlay board
*/
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/interrupt.h>
#include <linux/gpio/consumer.h>
#include <linux/miscdevice.h>
#include <linux/of_device.h>
static char *_NAME = "User Key";
/* interrupt handler */
static irqreturn_t interrupt_isr(int irq, void *data)
{
struct device *dev = data;
dev_info(dev, "interrupt received. key: %s\n", _NAME);
return IRQ_HANDLED;
}
static struct miscdevice basic_miscdevice = {
.minor = MISC_DYNAMIC_MINOR,
.name = "mydev",
};
static int my_probe(struct platform_device *pdev)
{
int ret_val, irq;
struct gpio_desc *gpio;
struct device *dev = &pdev->dev;
printk("my_probe() function is called.\n");
/* First method to get the Linux IRQ number */
gpio = devm_gpiod_get_optional(dev, "mybtn", GPIOD_IN);
if (IS_ERR(gpio)) {
printk("devm_gpiod_get_optional() failed\n");
return PTR_ERR(gpio);
}
irq = gpiod_to_irq(gpio);
if (irq < 0)
return irq;
printk("The IRQ number is: %d\n", irq);
/* Second method to get the Linux IRQ number */
irq = platform_get_irq(pdev, 0);
if (irq < 0) {
printk("irq is not available\n");
return -EINVAL;
}
printk("platform_get_irq(): %d\n", irq);
/* Allocate the interrupt line */
ret_val = devm_request_irq(dev, irq, interrupt_isr, IRQF_TRIGGER_FALLING, _NAME, dev);
if (ret_val) {
printk("Failed to request interrupt %d, error %d\n", irq, ret_val);
return ret_val;
}
ret_val = misc_register(&basic_miscdevice);
if (ret_val != 0) {
printk("could not register the misc device mydev\n");
return ret_val;
}
dev_info(dev, "Successfully loading ISR handler.\n");
return 0;
}
static int my_remove(struct platform_device *pdev)
{
printk("my_remove() function is called.\n");
misc_deregister(&basic_miscdevice);
printk("my_remove() function is exited.\n");
return 0;
}
static const struct of_device_id my_of_ids[] = {
{ .compatible = "beagleplay,userbutton"},
{},
};
MODULE_DEVICE_TABLE(of, my_of_ids);
static struct platform_driver my_platform_driver = {
.probe = my_probe,
.remove = my_remove,
.driver = {
.name = "userbutton",
.of_match_table = my_of_ids,
.owner = THIS_MODULE,
}
};
module_platform_driver(my_platform_driver);
MODULE_AUTHOR("Chunghan Yi <chunghan.gi@gmail.com>");
MODULE_DESCRIPTION("user button platform driver for GPIO interrupt test");
MODULE_LICENSE("GPL v2");
<Makefile>
obj-m := bp_gpiokey.o
KERNEL_DIR ?= /mnt/hdd/workspace/bootlin/beagleplay/bootlin/embedded-linux-beagleplay-labs/linux
all:
make -C $(KERNEL_DIR) ARCH=arm64 CROSS_COMPILE=aarch64-poky-linux- M=$(PWD) modules
clean:
make -C $(KERNEL_DIR) ARCH=arm64 CROSS_COMPILE=aarch64-poky-linux- M=$(PWD) clean
KERNEL_DIR ?= /mnt/hdd/workspace/bootlin/beagleplay/bootlin/embedded-linux-beagleplay-labs/linux
all:
make -C $(KERNEL_DIR) ARCH=arm64 CROSS_COMPILE=aarch64-poky-linux- M=$(PWD) modules
clean:
make -C $(KERNEL_DIR) ARCH=arm64 CROSS_COMPILE=aarch64-poky-linux- M=$(PWD) clean
______________________________________________________________
위의 device driver(kernel module)를 build해 보자.
$ . /opt/beagleplay/environment-setup-aarch64-poky-linux
$ export ARCH=arm64
$ export CROSS_COMPILE=aarch64-poky-linux-
$ make clean; make
$ export ARCH=arm64
$ export CROSS_COMPILE=aarch64-poky-linux-
$ make clean; make
$ ls -l *.ko
-rw-rw-r-- 1 chyi chyi 10480 7월 25 17:16 bp_gpiokey.ko
-rw-rw-r-- 1 chyi chyi 10480 7월 25 17:16 bp_gpiokey.ko
자, 모든 준비(dtb 파일과 kernel module)가 끝났으니, target board에 올려서 동작을 확인해 볼 차례이다.
<Target Board>
=> run bootcmd2
📌 (별건 아니지만) run bootcmd2 명령과 관련해서는 이전 blog posting을 참고하기 바란다.
<Target Board>
$ insmod ./bp_gpiokey.ko
OK, 예상대로 User button을 누를 때 마다 interrupt가 걸리고, (interrupt handler 함수를 통해) message가 console에 출력되는 것이 보인다. 😎
[그림 6.12] User button interrupt driver 동작 모습
지금까지 GPIO를 이용한 interrupt driver 예제를 간단히 구현해 보았다. 이전에도 비슷한 시험을 여러번 해 본 적이 있으니, 결코 어렵지 않을 것이다. 😙
6.3 HTU21D 온습도 센서를 위한 I2C Driver 구현하기
이번 절에서는 약방의 감초인 i2c device driver 구현 예제를 소개해 보고자 한다. 이번에 사용할 i2c device는 HTU21D 온습도 센서이다.
사실, HTU21D 온습도 센서를 이용한 i2c device driver와 관련해서는 이전 posting에서 2차례나 설명한 바 있다. 따라서 이번 posting에서는 중복되는 설명은 모두 빼고, 골자(?)만 정리해 보기로 하겠다(배경 설명 등은 아래 posting을 참고하기 바란다).
먼저 회로도 상으로 보면, AM62x SoC의 I2C3 controller가 mikroBUS(확장 pin 개념)와 연결되어 있다.
[그림 6.13] BeaglePlay MikroBUS 회로도 [출처 - 참고문헌 1,2]
좀 더 부연 설명하면, mikroBUS의 I2C3_SCL pin은 AM62x SoC A15 pin에 연결되어 있고, I2C3_SDA는 B15 pin에 연결되어 있음을 알 수 있다.
[그림 6.14] I2C3 SCL, SDA pin 연결 [출처 - 참고문헌 1,2]
AM62x Sitara Processors datasheet(44 page)를 보면, A15, B15 pin의 용도(pinmux)에 대한 설명이 나와 있는데, i2c3용으로 사용하려면 mux mode 값을 2로 설정해 주어야만 한다.
[그림 6.15] A15, B15 pinmux 설정 정보 [출처 - 참고문헌 3]
&main_i2c3 {
pinctrl-names = "default";
pinctrl-0 = <&mikrobus_i2c_pins_default>;
clock-frequency = <400000>;
status = "okay";
};
mikrobus_i2c_pins_default: mikrobus-i2c-default-pins {
pinctrl-single,pins = <
AM62X_IOPAD(0x01d0, PIN_INPUT_PULLUP, 2) /* (A15) UART0_CTSn.I2C3_SCL */
AM62X_IOPAD(0x01d4, PIN_INPUT_PULLUP, 2) /* (B15) UART0_RTSn.I2C3_SDA */
>;
};
pinctrl-single,pins = <
AM62X_IOPAD(0x01d0, PIN_INPUT_PULLUP, 2) /* (A15) UART0_CTSn.I2C3_SCL */
AM62X_IOPAD(0x01d4, PIN_INPUT_PULLUP, 2) /* (B15) UART0_RTSn.I2C3_SDA */
>;
};
위의 내용 중, AM62X_IOPAD 매크로의 의미가 궁금한데, arch/arm64/boot/dts/ti/k3-pinctrl.h 파일을 보면, 아래와 같이 정의되어 있다.
#define AM62AX_IOPAD(pa, val, muxmode) (((pa) & 0x1fff)) ((val) | (muxmode))
여기서 pa는 실제로는 pad configuration register 주소(offset 번지)이고, val | muxmode는 실제 register 값을 의미한다. 앞서 말한대로 muxmode가 2로 설정되어 있으므로, i2c3용으로 사용하겠다는 뜻이 된다.
여기까지, i2c3 SCL/SDA pin 연결과 pin mux 설정에 관하여 확인해 보았다. 이제부터는 본론으로 들어가서, HTU21D i2c device를 i2c3 controller에 물리적으로 연결한 후, (s/w인) device tree와 device driver를 구현해 보도록 하자.
먼저, VCC(3.3V), Ground, SCA, SDA pin 배치를 잘 확인한 후, HTU21D 센서를 target board에 아래와 같이 연결하도록 한다.
<AM62x SoC> <mikroBUS> <HTU21D>
+3.3V ========== +3.3V =========== 3.3V
GND ========== GND2 =========== GND
A15 ========== SCL =========== SCL
B15 ========== SDA =========== SDA
다음으로, 아래와 같이 k3-am625-beagleplay.dts 파일을 수정하여, i2c3 controller에 연결되는 htu21 slave device를 추가하도록 하자.
[그림 6.18] htu21d 관련 device tree node 추가 모습
Device tree 파일을 build해 보자.
$ . /opt/beagleplay/environment-setup-aarch64-poky-linux
$ export ARCH=arm64
$ export CROSS_COMPILE=aarch64-poky-linux-
$ make dtbs
UPD include/config/kernel.release
DTC arch/arm64/boot/dts/ti/k3-am625-beagleplay.dtb
$ export ARCH=arm64
$ export CROSS_COMPILE=aarch64-poky-linux-
$ make dtbs
UPD include/config/kernel.release
DTC arch/arm64/boot/dts/ti/k3-am625-beagleplay.dtb
마지막으로 device driver를 구현해야 하는데, menuconfig를 통해 확인해 보니, 해당 driver가 이미 module 형태로 define되어 있다. 따라서 이미 build된 kernel module 파일을 찾아 target board로 복사해 주기만 하면 된다.
<device driver code>
linux/drivers/iio/humidity/htu21.c
linux/drivers/iio/common/ms_sensors/ms_sensors_i2c.c
Device Drivers --->
Industrial I/O support ---->
Humidity sensors
자, 지금까지 준비한 내용을 target board에서 돌려 보도록 하자.
<Target Board>
root@beagleplay:~/workspace# insmod ./ms_sensors_i2c.ko
root@beagleplay:~/workspace# insmod ./htu21.ko
[ 128.571070] htu21 3-0040: Serial number : 48540046d0d63211
일단 device driver는 정상적으로 구동된 것 같다. 이상태에서, i2cdetect 명령으로 htu21d sensor(i2c address : 0x40)가 제대로 인식되는지를 확인해 보자. 0x40 부분에 UU라고 표시된 내용이 하나 보인다. UU라고 표시된 것은 device driver가 해당 device을 열심히 사용 중이라는 의미이다.
HTU21D 센서가 제대로 연결된 것이 확인되었으니, 아래 위치에서 온도/습도 값을 확인해 보도록 하자.
이상으로 BeaglePlay 보드에 i2c device를 하나 장착하고, 정상 동작하는 것을 확인해 보았다.
6.4 ADXL345 3축 가속도 센서를 위한 SPI Driver 구현하기
ADXL345 3축 가속도 센서와 관련해서는 이전 posting에서 한번 다룬 적이 있다. 이번 절에서는 i2c가 아니라 SPI 드라이버를 구현하는 과정을 소개해 보고자 한다.
mikroBUS에는 i2c 말고도 SPI pin(CS, SCK, MISO, MOSI)도 나와 있다.
mikroBUS SPI pin은 AM62x SoC SPI2 controller의 E19(CS0), A20(CLK), B19(D0 - MISO), A19(D1 - MOSI) pin과 연결되어 있음을 알 수 있다.
한편, AM62x Sitara Processors datasheet(28, 29, 30 page)에는, E19, A20, B19, A19 pin의 용도(pinmux)에 대한 설명이 나와 있는데, SPI2 용으로 사용하려면 mux mode 값을 1로 설정해 주어야만 한다.
[그림 6.24] E19, A20, B19, A19 pinmux 설정 정보 [출처 - 참고문헌 3]
pinctrl-names = "default";
pinctrl-0 = <&mikrobus_spi_pins_default>;
status = "okay";
};
mikrobus_spi_pins_default: mikrobus-spi-default-pins {
pinctrl-single,pins = <
AM62X_IOPAD(0x01b0, PIN_INPUT, 1) /* (A20) MCASP0_ACLKR.SPI2_CLK */
AM62X_IOPAD(0x01ac, PIN_INPUT, 1) /* (E19) MCASP0_AFSR.SPI2_CS0 */
AM62X_IOPAD(0x0194, PIN_INPUT, 1) /* (B19) MCASP0_AXR3.SPI2_D0 */
AM62X_IOPAD(0x0198, PIN_INPUT, 1) /* (A19) MCASP0_AXR2.SPI2_D1 */
>;
};
여기까지, SPI2 pin 연결과 pin mux 설정에 관하여 확인해 보았다. 이제부터는 본론으로 들어가서, ADXL345 spi device를 spi controller에 물리적으로 연결한 후, (s/w인) device tree와 device driver를 구현해 볼 차례이다.
pinctrl-single,pins = <
AM62X_IOPAD(0x01b0, PIN_INPUT, 1) /* (A20) MCASP0_ACLKR.SPI2_CLK */
AM62X_IOPAD(0x01ac, PIN_INPUT, 1) /* (E19) MCASP0_AFSR.SPI2_CS0 */
AM62X_IOPAD(0x0194, PIN_INPUT, 1) /* (B19) MCASP0_AXR3.SPI2_D0 */
AM62X_IOPAD(0x0198, PIN_INPUT, 1) /* (A19) MCASP0_AXR2.SPI2_D1 */
>;
};
여기까지, SPI2 pin 연결과 pin mux 설정에 관하여 확인해 보았다. 이제부터는 본론으로 들어가서, ADXL345 spi device를 spi controller에 물리적으로 연결한 후, (s/w인) device tree와 device driver를 구현해 볼 차례이다.
먼저, VCC(3.3V), Ground, CS0, CLK, D0(MISO), D1(MOSI) pin 배치를 잘 확인한 후, 아래와 같이 ADXL345 센서를 target board에 연결하도록 한다.
<AM62x SoC> <mikroBUS> <ADXL345>
+3.3V ============ +3.3V =========== VCC
GND ============ GND2 =========== GND
E19 ============ CS(CS0) =========== CS
A20 ============ SCK(CLK) =========== SCL
B19 ============ MISO(DO) =========== SDO
A19 ============ MOSI(D1) =========== SDA
다음으로, 아래와 같이 k3-am625-beagleplay.dts 파일을 수정하여, spi2 controller에 연결되는 adxl345 slave device를 추가하도록 하자.
[그림 6.28] ADXL345 node 추가 모습
📌 일단은 interrupt 설정을 제외하고 기본적인 설정만 추가하였다.Device tree 파일을 build해 보자.
$ . /opt/beagleplay/environment-setup-aarch64-poky-linux
$ export ARCH=arm64
$ export CROSS_COMPILE=aarch64-poky-linux-
$ make dtbs
UPD include/config/kernel.release
DTC arch/arm64/boot/dts/ti/k3-am625-beagleplay.dtb
$ export ARCH=arm64
$ export CROSS_COMPILE=aarch64-poky-linux-
$ make dtbs
UPD include/config/kernel.release
DTC arch/arm64/boot/dts/ti/k3-am625-beagleplay.dtb
마지막으로 device driver를 구현해야 하는데, menuconfig를 통해 확인해 보니, 해당 driver가 이미 module 형태로 define되어 있다. 따라서 이미 build된 kernel module 파일을 찾아 target board로 복사해 주기만 하면 된다.
<device driver code>
drivers/iio/accel/adxl345_spi.c
drivers/iio/accel/adxl345_core.c
Device Drivers --->
Industrial I/O support ---->
Accelerometers
자, 모든 것이 준비되었의 지금까지 준비한 내용을 target board에서 돌려 보도록 하자.
<Target Board>
root@beagleplay:~/workspace# insmod ./adxl345_core.ko
root@beagleplay:~/workspace# insmod ./adxl345_spi.ko
root@beagleplay:~/workspace# lsmod
Not tainted
adxl345_spi 12288 0 - Live 0xffff800079a60000
adxl345_core 12288 1 adxl345_spi, Live 0xffff800079a74000
root@beagleplay:~/workspace# insmod ./adxl345_spi.ko
root@beagleplay:~/workspace# lsmod
Not tainted
adxl345_spi 12288 0 - Live 0xffff800079a60000
adxl345_core 12288 1 adxl345_spi, Live 0xffff800079a74000
Device driver가 정상적으로 구동된 듯 보이니, 아래 위치에서 adxl345 sensor(accelerometer)가 알려주는 정보(x, y, z 축 가속도 정보)가 존재하는지 확인해 보도록 하자.
root@beagleplay:/sys/bus/spi/devices# ls -la
drwxr-xr-x 2 root root 0 Jan 1 1970 .
drwxr-xr-x 4 root root 0 Jan 1 1970 ..
lrwxrwxrwx 1 root root 0 Jan 1 1970 spi0.0 -> ../../../devices/platform/bus@f0000/20120000.spi/spi0
drwxr-xr-x 2 root root 0 Jan 1 1970 .
drwxr-xr-x 4 root root 0 Jan 1 1970 ..
lrwxrwxrwx 1 root root 0 Jan 1 1970 spi0.0 -> ../../../devices/platform/bus@f0000/20120000.spi/spi0
# cd /sys/bus/spi/devices/spi0.0/iio:device0
[그림 6.30] adxl345 3축 가속도 sensor 동작 모습(1)
[그림 6.31] adxl345 3축 가속도 sensor 동작 모습(2)
마지막으로 adlx345 spi driver의 코드를 살펴 보아야 하는데, (내용이 길어지는 관계로) 이 부분(코드 확인)은 여러분의 몫으로 남긴다. 😋
6.5 libgpiod와 GPIO control
다른 subsystem과 마찬가지로 Linux의 GPIO subsystem도 계속해서 변화하고 있다. 최근에는 libgpiod라는 userspace tool이 개발되었고, 이를 통해 GPIO를 설정하는 추세이다.
먼저, 위의 코드를 내려 받아 target board에서 사용하도록 cross-compile해 보자.
<libgpiod cross-compile하기>
$ git clone https://github.com/brgl/libgpiod
$ cd libgpiod/
$ ./autogen.sh --enable-tools=yes --host=armv8 CC=aarch64-linux-gnu-gcc --prefix=${YOUR_PATH}/output
-> yocto toolchain 대신 gnu toolchain으로 build함.
$ make
$ make install
$ cd output/bin
$ ls -la
합계 1160
drwxrwxr-x 2 chyi chyi 4096 7월 28 17:52 .
drwxrwxr-x 5 chyi chyi 4096 7월 28 17:52 ..
-rwxr-xr-x 1 chyi chyi 181472 7월 28 17:52 gpiodetect
-rwxr-xr-x 1 chyi chyi 190312 7월 28 17:52 gpioget
-rwxr-xr-x 1 chyi chyi 187368 7월 28 17:52 gpioinfo
-rwxr-xr-x 1 chyi chyi 211080 7월 28 17:52 gpiomon
-rwxr-xr-x 1 chyi chyi 200608 7월 28 17:52 gpionotify
-rwxr-xr-x 1 chyi chyi 200384 7월 28 17:52 gpioset
합계 1160
drwxrwxr-x 2 chyi chyi 4096 7월 28 17:52 .
drwxrwxr-x 5 chyi chyi 4096 7월 28 17:52 ..
-rwxr-xr-x 1 chyi chyi 181472 7월 28 17:52 gpiodetect
-rwxr-xr-x 1 chyi chyi 190312 7월 28 17:52 gpioget
-rwxr-xr-x 1 chyi chyi 187368 7월 28 17:52 gpioinfo
-rwxr-xr-x 1 chyi chyi 211080 7월 28 17:52 gpiomon
-rwxr-xr-x 1 chyi chyi 200608 7월 28 17:52 gpionotify
-rwxr-xr-x 1 chyi chyi 200384 7월 28 17:52 gpioset
다음으로, 위의 실행 파일을 target board로 옮겨 실행해 보도록 하자.
먼저, gpiodetect 명령을 사용하면 전체 gpiochip(bank) 정보를 확인할 수 있다. 아래 결과를 보니, BeaglePlay 보드에는 4개의 gpiochip이 있고, 각각의 gpiochip은 서로 다른 갯수의 gpio pin(or line)을 가지고 있는 것으로 확인된다.
[그림 6.32] gpiodetect 명령 실행 모습
다음으로, gpioinfo 명령을 사용하면, 각각의 gpiochip에 연결된 실제 line의 의미(용도)를 확인할 수가 있다. 가령, mikroBUS의 INT pin은 회로도 상으로 GPIO1_9(SoC pin B18)에 연결되어 있는데, 이를 gpioinfo 명령으로 확인해 보니, gpiochip3의 52개 line 중 line 9에 해당함을 알 수 있다.
[그림 6.33] gpioinfo 명령 실행 모습
gpiochip3 9 line에 jumper cable을 하나 꽂은 후, 반대편을 GND에 연결했다가, 다시 +3.3V에 연결할 경우, gpioget 명령으로 확인해 보니, inactive(0) => active(1)로 값이 변경됨을 알 수가 있다.
[그림 6.34] gpioget 명령 실행 모습
먼저, 회로도 및 libgpiod 명령을 이용하여, LED가 실제로 어느 GPIO pin에 연결되어 있는지를 확인해 보도록 하자.
[그림 6.35] LED 회도로(1)
[그림 6.36] LED 회로도(2)- GPIO0_3,4,5,6, 9
여기까지의 정보를 토대로, gpio example 코드를 하나 테스트해 보자.
<libgpiod example code>
libgpiod/examples/toggle_line_value.c
_____________________________________________________________
int main(void)
{
/* Example configuration - customize to suit your situation. */
static const char *const chip_path = "/dev/gpiochip2"; //gpiochip2로 수정
static const unsigned int line_offset = 5; //3, 4, 5, 6, 9 번으로 수정 가능
enum gpiod_line_value value = GPIOD_LINE_VALUE_ACTIVE;
struct gpiod_line_request *request;
request = request_output_line(chip_path, line_offset, value,
"toggle-line-value");
if (!request) {
fprintf(stderr, "failed to request line: %s\n",
strerror(errno));
return EXIT_FAILURE;
}
for (;;) {
printf("%d=%s\n", line_offset, value_str(value));
sleep(1);
value = toggle_line_value(value);
gpiod_line_request_set_value(request, line_offset, value);
}
gpiod_line_request_release(request);
{
/* Example configuration - customize to suit your situation. */
static const char *const chip_path = "/dev/gpiochip2"; //gpiochip2로 수정
static const unsigned int line_offset = 5; //3, 4, 5, 6, 9 번으로 수정 가능
enum gpiod_line_value value = GPIOD_LINE_VALUE_ACTIVE;
struct gpiod_line_request *request;
request = request_output_line(chip_path, line_offset, value,
"toggle-line-value");
if (!request) {
fprintf(stderr, "failed to request line: %s\n",
strerror(errno));
return EXIT_FAILURE;
}
for (;;) {
printf("%d=%s\n", line_offset, value_str(value));
sleep(1);
value = toggle_line_value(value);
gpiod_line_request_set_value(request, line_offset, value);
}
gpiod_line_request_release(request);
return EXIT_SUCCESS;
}
}
_____________________________________________________________
위의 코드가 제대로 동작하기 위해서는 device tree의 leds 설정을 모두 막아야 한다.
[그림 6.38] LED 관련 device tree node 수정 모습
실행 결과, 예상한 대로 5번 line의 led가 1초 간격으로 blinking 되고 있다.
toggle_line_value.c 파일의 내용은 복잡하니 않으로, (구조 파악 차원에서) 각자 한번씩 읽어 보기 바란다.
_______________
이상으로 (아쉬움은 좀 남지만) 3번에 나누어 "BeaglePlay 보드를 이용한 Embedded Linux Programming"에 관한 내용을 소개하는 시간을 가져 보았다. 끝까지 읽어주셔서 감사드린다. 😊
7. References
[2] beagleplay.pdf, https://docs.beagleboard.org/
=> beagleplay & TI documents
[6] embedded-linux-beagleplay-labs.pdf, bootlin
[7] linux-kernel-beagleplay-labs.pdf, bootlin
[8] embedded-linux-slides.pdf, bootlin
[9] Device Tree for dummys, Thomas Petazzoni, bootin
[10] Device Tree 101, Petazzoni, bootin
=> bootlin documents, 요즘은 이분들이 대세다~
=> device tree specification v0.4
[12] And, Google~
Slowboot