Embedded System/Embedded Linux Build System Yocto

Yocto Project 기반 Embedded Linux Peripheral Control & Web Application 개발 가이드

임베디드 친구 2025. 5. 23. 23:34
반응형

1. Yocto Project 기반 임베디드 애플리케이션 개발 배경 및 문제 정의

Yocto Project 기반으로 임베디드 리눅스 시스템을 구축할 때 주변 장치(Peripherals) 제어 인터페이스 구현 과정에서 다양한 디버깅 및 안정성 문제가 발생할 수 있습니다.

기존 레거시 방식인 sysfs GPIO 제어 방식은 버그 및 리소스 누수(Resource Leak) 위험이 높으며, 최신 리눅스 커널(v4.8 이상 및 v5.x/v6.x) 환경에서는 Deprecated 상태입니다.

기존 소스코드의 sizeof(value) 오류(포인터 크기 반환 문제) , termios 플래그 미설정으로 인한 UART 통신 블로킹 이슈 , I2C ioctl 예외 처리 미흡 문제를 교정하고 libgpiod API 기반 최신 제어 표준 기술을 반영하여 안정성을 확보하는 가이드를 제공합니다.

2. Yocto 애플리케이션 핵심 해결 요약

  • libgpiod 최신 API 적용: 레거시 sysfs 대신 libgpiod API를 사용하여 칩셋 레이어의 직접 제어 및 리소스 누수 방지.
  • UART termios 완전 초기화: Raw 모드 설정(cfmakeraw)을 통해 이스케이프 문자로 인한 통신 제어 데이터 왜곡 차단.
  • sysfs sizeof() 버그 수정: C언어 문자열 전송 시 strlen()을 적용하여 파일 디스크립터 시스템 콜 쓰기 버그 해결.

3. Peripheral Control (GPIO, UART, I2C) 및 RESTful API 구현 분석

3.1 Linux GPIO Control: sysfs 버그 수정 및 libgpiod 전환

기존 C 언어 코드의 sizeof(value) 구문은 Char 포인터의 크기(64-bit architecture 기준 8 Bytes)를 반환하므로 sysfs 커널 노드 버퍼에 메모리 쓰기 오류를 일으킵니다. 리눅스 커널 4.8 이상에서는 gpiod (/dev/gpiochipX) 인터페이스 사용이 권장됩니다.

C 언어 libgpiod 기반 GPIO Control

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <gpiod.h>

#define GPIO_CHIP "gpiochip0"
#define GPIO_OFFSET 23

int main(void) {
    struct gpiod_chip *chip;
    struct gpiod_line *line;
    int ret;

    // Open GPIO chip device
    chip = gpiod_chip_open_by_name(GPIO_CHIP);
    if (!chip) {
        perror("gpiod_chip_open_by_name failed");
        return EXIT_FAILURE;
    }

    // Get GPIO line
    line = gpiod_chip_get_line(chip, GPIO_OFFSET);
    if (!line) {
        perror("gpiod_chip_get_line failed");
        gpiod_chip_close(chip);
        return EXIT_FAILURE;
    }

    // Request output direction
    ret = gpiod_line_request_output(line, "led_control", 0);
    if (ret < 0) {
        perror("gpiod_line_request_output failed");
        gpiod_chip_close(chip);
        return EXIT_FAILURE;
    }

    // LED ON
    gpiod_line_set_value(line, 1);
    printf("LED ON\n");
    sleep(2);

    // LED OFF
    gpiod_line_set_value(line, 0);
    printf("LED OFF\n");

    // Release resources
    gpiod_line_release(line);
    gpiod_chip_close(chip);
    return EXIT_SUCCESS;
}

Python gpiod 기반 GPIO Control

import time
import gpiod

GPIO_CHIP = "gpiochip0"
GPIO_OFFSET = 23

# Open chip line request
chip = gpiod.Chip(GPIO_CHIP)
line = chip.get_line(GPIO_OFFSET)

try:
    line.request(consumer="led_control", type=gpiod.LINE_REQ_DIR_OUT)
    
    # LED ON
    line.set_value(1)
    print("LED ON")
    time.sleep(2)

    # LED OFF
    line.set_value(0)
    print("LED OFF")
finally:
    line.release()
    chip.close()

3.2 Linux UART Communication: termios Raw Mode 설정 및 비동기 처리

UART 통신 시 단순 Baud rate 설정만 수행할 경우, parity, stop bit, echo mode 문제로 인한 데이터 트래픽 단절 현상이 발생할 수 있습니다. cfmakeraw() 함수를 통해 터미널 속성을 Raw 데이터 수신 모드로 초기화합니다.

C 언어 termios 기반 Raw UART 통신

#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <termios.h>
#include <unistd.h>

int main(void) {
    int uart_fd = open("/dev/ttyS1", O_RDWR | O_NOCTTY | O_NDELAY);
    if (uart_fd < 0) {
        perror("Failed to open /dev/ttyS1");
        return -1;
    }

    struct termios options;
    tcgetattr(uart_fd, &options);

    // Set baud rate to 115200
    cfsetispeed(&options, B115200);
    cfsetospeed(&options, B115200);

    // Set Raw Mode (Disable canonical mode, echo, signals)
    cfmakeraw(&options);
    options.c_cflag |= (CLOCAL | CREAD);
    
    // Apply changes immediately
    tcsetattr(uart_fd, TCSANOW, &options);

    const char *send_buf = "Hello UART!\n";
    write(uart_fd, send_buf, strlen(send_buf));

    sleep(1);

    char recv_buf[100];
    memset(recv_buf, 0, sizeof(recv_buf));
    int n = read(uart_fd, recv_buf, sizeof(recv_buf) - 1);
    
    if (n > 0) {
        printf("Received (%d bytes): %s\n", n, recv_buf);
    }

    close(uart_fd);
    return 0;
}

Python pyserial 기반 UART 통신

import serial

try:
    # Initialize serial port with 115200 baud rate
    ser = serial.Serial(
        port="/dev/ttyS1",
        baudrate=115200,
        bytesize=serial.EIGHTBITS,
        parity=serial.PARITY_NONE,
        stopbits=serial.STOPBITS_ONE,
        timeout=1
    )

    ser.write(b"Hello UART!\n")
    response = ser.readline().decode('utf-8')
    print(f"Received: {response}")

except serial.SerialException as e:
    print(f"UART Serial Exception: {e}")
finally:
    if 'ser' in locals() and ser.is_open:
        ser.close()

3.3 Linux I2C Bus Communication: ioctl 및 SMBus 프로토콜 비교

I2C 제어 시 /dev/i2c-X 캐릭터 디바이스 기반 File I/O 방식과 Linux Kernel SMBus API 활용 방식의 명확한 구조 비교입니다.

항목 (Properties) I2C Raw File I/O (read/write) Linux SMBus API (i2c_smbus_*)
안정성 (Reliability) 단순 바이트 읽기/쓰기 시 NACK 리셋 제어 미흡 커널 레벨 패킷 에러 검증(PEC) 지원으로 높은 안정성
I2C Protocol 규격 커스텀 프로토콜 프레임 조합 가능 SMBus Standard Protocol 표준 준수
코드 복잡도 레지스터 주소 지정 후 반복 처리 필요

i2c_smbus_read_byte_data 등 단일 함수로 처리 가능

C 언어 i2c-dev API 기반 I2C Read

#include <stdio.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <linux/i2c-dev.h>
#include <i2c/smbus.h>

#define I2C_DEV_PATH "/dev/i2c-1"
#define SLAVE_ADDR 0x48

int main(void) {
    int file = open(I2C_DEV_PATH, O_RDWR);
    if (file < 0) {
        perror("Failed to open I2C bus");
        return -1;
    }

    if (ioctl(file, I2C_SLAVE, SLAVE_ADDR) < 0) {
        perror("Failed to acquire bus access/talk to slave");
        close(file);
        return -1;
    }

    // Read byte from register 0x00 via SMBus API
    __s32 res = i2c_smbus_read_byte_data(file, 0x00);
    if (res < 0) {
        perror("I2C Read Failed");
        close(file);
        return -1;
    }

    printf("Received data: 0x%02X\n", res);

    close(file);
    return 0;
}

Python smbus2 기반 I2C Read

from smbus2 import SMBus

I2C_BUS = 1
SLAVE_ADDR = 0x48
REGISTER = 0x00

try:
    with SMBus(I2C_BUS) as bus:
        # Read a single byte from address 0x48, register 0x00
        data = bus.read_byte_data(SLAVE_ADDR, REGISTER)
        print(f"Received data: {hex(data)}")
except IOError as e:
    print(f"I2C I/O Error: {e}")

3.4 Python 기반 Flask REST API & Sensor Data Logging Service

Yocto 환경에서 WSGI 및 Gunicorn 구동을 고려한 Flask API 서비스 및 센서 데이터 로깅 아키텍처입니다.

Flask RESTful API 서비스

from flask import Flask, jsonify

app = Flask(__name__)

@app.route("/api/v1/status", methods=["GET"])
def get_status():
    return jsonify({
        "status": "success",
        "system": "Yocto Embedded Linux"
    }), 200

if __name__ == "__main__":
    # Do not use debug=True in production environment
    app.run(host="0.0.0.0", port=5000)

Non-blocking Threaded Data Logger

import time
import os

LOG_FILE_PATH = "/var/log/sensor_data.log"

def append_sensor_log(data_val):
    try:
        # Ensure log file directory exists
        os.makedirs(os.path.dirname(LOG_FILE_PATH), exist_ok=True)
        with open(LOG_FILE_PATH, "a", encoding="utf-8") as f:
            timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
            f.write(f"[{timestamp}] Sensor Data: {data_val}\n")
    except IOError as e:
        print(f"Failed to write log: {e}")

if __name__ == "__main__":
    while True:
        append_sensor_log(123)
        time.sleep(10)

4. Embedded Linux 개발 및 디버깅 팁

  1. gpiodetect & gpioinfo 명령어 활용: Yocto 이미지 빌드 시 libgpiod-tools 패키지를 구함하면 터미널에서 핀 상태를 동적으로 확인 가능합니다.
    # Check available GPIO chips
    gpiodetect
    
    # Check pin offset and current reservation state
    gpioinfo gpiochip0
    
  2. I2C Device Scanning: i2c-tools 패키지의 i2cdetect를 활용해 I2C 버스상의 슬레이브 장치 응답을 사전 점검합니다.
    i2cdetect -y -r 1
    
  3. dmesg 및 ftrace를 이용한 커널 로그 분석: 디바이스 트리(Device Tree, DTS) 매핑 상태를 확인하려면 dmesg 명령을 점검합니다.
    dmesg | grep -i "uart\|i2c\|gpio"
    

5. 자주 발생하는 실수 및 예외 처리 방법

5.1 sysfs 포인터 크기 버그 (sizeof 오류)

  • 문제 원인: C 언어에서 write(fd, value, sizeof(value)) 구문을 실행할 경우, valueconst char* 형태라면 데이터 전체의 길이가 아닌 포인터 변수 크기(8바이트)가 전달됩니다.
  • 해결 방법: string.hstrlen(value)를 전달하거나 libgpiod API로 전환합니다.

5.2 /dev/ttyS* 권한 문제 (Permission Denied)

  • 문제 원인: 비-Root 계정 실행 시 디바이스 노드 접근 권한 에러 발생.
    usermod -aG dialout,i2c username
    
  • 해결 방법: udev 규칙을 추가하거나 계정을 관련 그룹에 등록합니다.

5.3 I2C Bus Lockup (SCL Low Holding) 현상

  • 문제 원인: 비정상 프로세스 종료로 인해 슬레이브 장치가 SCL/SDA 라인을 점유한 채 해제되지 않는 현상.
  • 해결 방법: DTS 설정 내 i2c-recovery pinctrl 모드를 추가하거나 , GPIO 모드로 재설정 후 9개의 클럭 맥동(Clock pulse)을 인가하는 리셋 함수를 구현합니다.

6. 결론

Yocto Project 기반 임베디드 리눅스 개발 환경에서는 최신 커널 표준인 libgpiodSMBus API를 도입하는 것이 핵심입니다. 또한 UART 및 I2C 디바이스 제어 시 Raw 모드 미설정 및 포인터 연산 오류를 선제적으로 방지하여 시스템 안정성을 극대화해야 합니다.

반응형