Industrial Communication/CAN Bus: Protocol & Physical Layer

STM32 bxCAN 드라이버 구현 및 FreeRTOS, Linux SocketCAN 프로토콜 스택 아키텍처 분석

임베디드 친구 2025. 2. 23. 16:00
반응형

1. STM32 bxCAN 드라이버 및 임베디드 CAN 소프트웨어 스택 설계 배경

임베디드 시스템 및 전장 시스템 개발에서 CAN(Controller Area Network) 통신은 신뢰성 높은 차량용 및 산업용 데이터 전송을 위한 표준 프로토콜입니다. CAN 드라이버는 Microcontroller Unit(MCU)의 CAN 컨트롤러 하드웨어와 상위 레이어 애플리케이션 간 인터페이스를 제공하는 최하위 저수준(Low-Level) 소프트웨어 계층입니다.

실무 개발 환경에서 CAN 드라이버를 올바르게 구현하지 않으면 메시지 유실, 버스 오프(Bus-Off) 상태에 따른 시스템 정지, 인터럽트 교착 상태(Deadlock) 등 치명적인 결함이 발생할 수 있습니다. 본 문서에서는 ESP32 및 Zephyr 환경을 제외하고, 가장 광범위하게 쓰이는 STM32 bxCAN(HAL Library), FreeRTOS 태스크 기반 아키텍처, 그리고 Linux SocketCAN 스택 구조를 중심으로 계층별 아키텍처 분석 및 구현 가이드를 제공합니다.

2. STM32 HAL_CAN 및 Linux SocketCAN 핵심 결론 요약

  • STM32 bxCAN 구현: HAL_CAN_Init() 실행 후 HAL_CAN_ConfigFilter()를 통해 최소 1개 이상의 Rx 필터 은행(Filter Bank)을 활성화해야 HAL_CAN_RxFifo0MsgPendingCallback() 인터럽트가 동작합니다.
  • FreeRTOS 스택 구조: ISR 내에서는 xQueueSendFromISR()을 호출하여 수신 데이터를 버퍼링하고, 별도의 워커 태스크(Worker Task)에서 이를 수신 처리하도록 설계하여 ISR 지연을 최소화해야 합니다.
  • Linux SocketCAN: Socket API 기반(AF_CAN, SOCK_RAW)으로 인터페이스를 제어하며 candump, cansend 명령어를 통해 커널 수준의 네트워크 통신을 수행합니다.

3. STM32 bxCAN 드라이버 및 임베디드 CAN 소프트웨어 스택 상세 분석

3.1 임베디드 CAN 통신 소프트웨어 아키텍처 계층

CAN 통신 스택은 하드웨어 종속성을 분리하고 모듈성을 확보하기 위해 다중 레이어로 구조화됩니다.

계층 (Layer) 주요 역할 및 기능 구현 예시
Application Layer 사용자 로직, 진단 프로토콜(UDS, OBD-II) 처리 Custom App, CANopen App
Protocol Stack Layer 상위 통신 프로토콜 매핑 및 메시지 팩/언팩 CANopen, J1939, AUTOSAR CanIf
Driver / HAL Layer CAN 컨트롤러 레지스터 제어, TX/RX 버퍼 관리 STM32 HAL_CAN, Linux SocketCAN
Hardware Layer 온칩 CAN 컨트롤러 레지스터 및 외부 CAN Transceiver STM32 bxCAN, MCP2515, TJA1050

3.2 STM32 bxCAN HAL 드라이버 초기화 및 필터 설정 코드

STM32 bxCAN IP는 Power-On Reset 시 모든 필터 은행이 비활성화(Disabled) 상태로 설정됩니다. 명시적인 필터 설정 없이는 데이터를 수신할 수 없습니다.

#include "stm32f4xx_hal.h"

CAN_HandleTypeDef hcan1;

void STM32_CAN_Init(void) {
    // 1. CAN Hardware Peripheral Configuration
    hcan1.Instance = CAN1;
    hcan1.Init.Prescaler = 6;                  // APB1 Clock 42MHz -> Bit Rate 500Kbps
    hcan1.Init.Mode = CAN_MODE_NORMAL;
    hcan1.Init.SyncJumpWidth = CAN_SJW_1TQ;
    hcan1.Init.TimeSeg1 = CAN_BS1_11TQ;
    hcan1.Init.TimeSeg2 = CAN_BS2_2TQ;        // Sample Point = (1 + 11) / (1 + 11 + 2) = 85.7%
    hcan1.Init.TimeTriggeredMode = DISABLE;
    hcan1.Init.AutoBusOff = ENABLE;            // Automatic Bus-Off Management
    hcan1.Init.AutoWakeUp = DISABLE;
    hcan1.Init.AutoRetransmission = ENABLE;
    hcan1.Init.ReceiveFifoLocked = DISABLE;
    hcan1.Init.TransmitFifoPriority = DISABLE;

    if (HAL_CAN_Init(&hcan1) != HAL_OK) {
        /* CAN Initialization Error Handler */
        Error_Handler();
    }

    // 2. CAN Filter Configuration (Accept All Messages)
    CAN_FilterTypeDef sFilterConfig;
    sFilterConfig.FilterBank = 0;
    sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK;
    sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT;
    sFilterConfig.FilterIdHigh = 0x0000;
    sFilterConfig.FilterIdLow = 0x0000;
    sFilterConfig.FilterMaskIdHigh = 0x0000;
    sFilterConfig.FilterMaskIdLow = 0x0000;
    sFilterConfig.FilterFIFOAssignment = CAN_RX_FIFO0;
    sFilterConfig.FilterActivation = ENABLE;
    sFilterConfig.SlaveStartFilterBank = 14;

    if (HAL_CAN_ConfigFilter(&hcan1, &sFilterConfig) != HAL_OK) {
        /* Filter Configuration Error Handler */
        Error_Handler();
    }

    // 3. Start CAN Controller & Activate Interrupts
    if (HAL_CAN_Start(&hcan1) != HAL_OK) {
        /* Start Error Handler */
        Error_Handler();
    }

    if (HAL_CAN_ActivateNotification(&hcan1, CAN_IT_RX_FIFO0_MSG_PENDING) != HAL_OK) {
        /* Interrupt Activation Error Handler */
        Error_Handler();
    }
}

3.3 FreeRTOS 환경에서의 CAN Rx Queue 버퍼링 및 수신 처리

FreeRTOS 환경에서는 ISR(Interrupt Service Routine) 내에서 긴 처리 시간을 소모해서는 안 됩니다. ISR에서 Rx FIFO 프레임을 즉시 큐로 전달하고, 비동기 수신 태스크에서 데이터를 처리하도록 구현합니다.

#include "FreeRTOS.h"
#include "queue.h"
#include "task.h"
#include "stm32f4xx_hal.h"

extern CAN_HandleTypeDef hcan1;
QueueHandle_t xCANRxQueue = NULL;

typedef struct {
    uint32_t std_id;
    uint8_t dlc;
    uint8_t data[8];
} CAN_Message_t;

// FreeRTOS CAN Rx Worker Task
void vCANRxTask(void *pvParameters) {
    CAN_Message_t rx_msg;

    for (;;) {
        // Block until CAN message arrives in Queue
        if (xQueueReceive(xCANRxQueue, &rx_msg, portMAX_DELAY) == pdTRUE) {
            /* Process Received CAN Frame */
        }
    }
}

// STM32 CAN Rx FIFO0 Interrupt Callback
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) {
    CAN_RxHeaderTypeDef rx_header;
    CAN_Message_t rx_msg;
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;

    if (hcan->Instance == CAN1) {
        if (HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &rx_header, rx_msg.data) == HAL_OK) {
            rx_msg.std_id = rx_header.StdId;
            rx_msg.dlc = rx_header.DLC;

            // Send message to Queue from ISR
            xQueueSendFromISR(xCANRxQueue, &rx_msg, &xHigherPriorityTaskWoken);
            portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
        }
    }
}

3.4 Linux SocketCAN 네트워킹 드라이버 제어

Linux Kernel은 CAN 인터페이스를 네트워크 소켓 abstraction으로 다룹니다. SocketCAN 메커니즘을 통해 POSIX 소켓 API(socket(), bind(), write(), read())를 그대로 활용할 수 있습니다.

Linux SocketCAN CLI 명령어

# Set CAN Bitrate to 500Kbps and Bring Up Interface
sudo ip link set can0 type can bitrate 500000
sudo ip link set can0 up

# Monitor Incoming CAN Messages
candump can0

# Transmit Standard CAN Frame (ID: 0x123, Data: 11 22 33 44 55 66 77 88)
cansend can0 123#1122334455667788

Linux C SocketCAN Application Code

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <net/if.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <linux/can.h>
#include <linux/can/raw.h>

int open_socketcan(const char *ifname) {
    int s;
    struct sockaddr_can addr;
    struct ifreq ifr;

    if ((s = socket(PF_CAN, SOCK_RAW, CAN_RAW)) < 0) {
        perror("Socket creation failed");
        return -1;
    }

    strcpy(ifr.ifr_name, ifname);
    ioctl(s, SIOCGIFINDEX, &ifr);

    memset(&addr, 0, sizeof(addr));
    addr.can_family = AF_CAN;
    addr.can_ifindex = ifr.ifr_ifindex;

    if (bind(s, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
        perror("Bind failed");
        close(s);
        return -1;
    }

    return s;
}

4. STM32 및 Linux SocketCAN 디버깅 및 개발 팁

  • STM32 Auto-Recovery 설정: hcan.Init.AutoBusOff = ENABLE; 설정을 적용하면 노이즈나 버스 에러 카운터 누적으로 인해 Bus-Off 상태로 진입했을 때 MCU가 하드웨어적으로 버스 재접속을 자동 시도합니다.
  • Linux SocketCAN 가상 인터페이스(vcan) 활용: 실제 타깃 하드웨어 없이 개발 및 테스트를 수행하려면 Linux vcan(Virtual CAN) 커널 모듈을 로드하여 디버깅환경을 구축할 수 있습니다.
sudo modprobe vcan
sudo ip link add dev vcan0 type vcan
sudo ip link set up vcan0

5. CAN 드라이버 개발 시 흔히 하는 실수 및 예외 해결법

5.1 HAL_CAN_ConfigFilter() 미설정으로 인한 수신 콜백 미동작

  • 증상: CAN 하드웨어 버스 상에 물리적 신호가 존재하나 STM32 MCU에서 HAL_CAN_RxFifo0MsgPendingCallback() 함수가 호출되지 않음.
  • 원인: STM32 bxCAN은 Reset 시 모든 필터가 Disabled 상태가 되어 수신 프레임을 모두 차단합니다.
  • 해결책: HAL_CAN_ConfigFilter()를 호출하여 Filter Bank 0을 ENABLE로 설정해야 합니다.

5.2 Bit Timing 계산 오류로 인한 HAL_CAN_ERROR_BOF (Bus-Off) 진입

  • 증상: 프레임 전송 시 HAL_CAN_AddTxMessage()가 실패하고, TEC(Transmit Error Counter) 레지스터가 255를 초과하여 Bus-Off 상태 진입.
  • 원인: APB1 Clock 주파수 기반 Sample Point 계산 오류로 인한 비트 동기화 실패.
  • 해결책: ISO 11898 표준 가이드라인에 따라 Sample Point가 75% ~ 85% 범위 내에 오도록 Prescaler, BS1, BS2 레지스터 값을 재조정해야 합니다.

$$\text{Baud Rate} = \frac{\text{APB1 Clock}}{\text{Prescaler} \times (1 + \text{BS1} + \text{BS2})}$$

6. STM32 bxCAN 및 Linux SocketCAN 스택 마이그레이션 결론

본 문서에서는 STM32 bxCAN HAL 드라이버 구현 방식, FreeRTOS 기반 Queue 아키텍처, 그리고 Linux SocketCAN 활용 기법을 다루었습니다. MCU와 Linux OS 환경 간 시스템 구조 차이를 이해하고 적절한 드라이버 API와 버퍼링 구조를 설계하는 것이 안정적인 전장/임베디드 CAN 통신 스택 구축의 핵심입니다.


STM32도 함께 공부하세요.

반응형