STM32 CAN 통신 예제: CubeMX와 HAL로 송수신 구현하기
13 분
- 0. 시작하기 전에
- 1. STM32 CAN 통신 예제의 기본 원리
- 2. Classic CAN과 CAN FD의 차이
- 3. Classic CAN 송수신 실습
- 4. CAN FD 송수신 실습
- 5. 정리
- 6. 참고 자료
핵심 요약
- Classic CAN과 CAN FD의 프레임 및 비트 타이밍 차이를 설명합니다.
- NUCLEO-G491RE와 Jetson TX2를 CAN 트랜시버로 연결합니다.
- CubeMX 필터·인터럽트 설정과 HAL 기반 송수신 코드를 구현합니다.
- 종단저항, CANH·CANL, candump와 오류 응답을 이용한 점검 방법을 다룹니다.
Nucleo-G491RE 보드와 Jetson 보드를 이용한 CAN 버스 통신
이 글에서는 STM32 CAN 통신 예제로 NUCLEO-G491RE와 Jetson TX2를 연결하고, Classic CAN과 CAN FD의 비트 타이밍·필터·수신 인터럽트·응답 송신을 단계별로 구현합니다.
브레드보드에서 트랜시버와 종단저항, CANH·CANL 연결을 검증한 뒤 커넥터와 테스트 포인트를 작은 CAN 인터페이스 PCB로 정리하면 반복 시험과 배선 점검이 쉬워집니다. PCB 제작시Gerber 파일은 JLCPCB PCB 서비스에 업로드하고 온라인 견적에서 제작 가능 여부와 비용을 확인할 수 있습니다.
이 실습에서 배울 수 있는 것들
- Classic CAN 통신
- CAN-FD 통신
- 통신을 이용한 GPIO 제어
0. 시작하기 전에
NUCLEO-F446RE의 bxCAN은 Classic CAN 프레임을 지원합니다. Classic CAN의 데이터 필드는 최대 8바이트이고 명목 비트레이트는 일반적으로 최대 1 Mbit/s이므로, 더 큰 페이로드나 빠른 데이터 구간이 필요하면 CAN FD 컨트롤러가 있는 MCU를 사용합니다.
NUCLEO-G491RE의 FDCAN은 Classic CAN과 CAN FD를 모두 지원합니다. CAN 버스는 여러 제어 보드가 같은 차동 배선을 공유하는 시스템에서 널리 사용됩니다.
이를 위해 메인으로 Jetson 보드를 사용하고 (실습에서는 Jetson TX2를 사용합니다) 서브로 Nucleo-G491RE보드를 사용합니다. 메인으로 사용할 수 있는 SBC(Single Board)보드들은 많지만 CAN 컨트롤러를 내장하고 있는 보드들은 그렇게 많지 않습니다.
임베디드에서 많이 사용되고 있는 라즈베리 파이 보드들도 이 CAN 컨트롤러를 내장하고 있지 않습니다. 물론 CAN 컨트롤러를 따로 구입해서 장착해도 되겠지만 번거로움을 피하고자 Jetson 보드를 사용합니다.
Jetson TX2와 일부 Jetson 플랫폼은 CAN 컨트롤러를 제공하지만, 보드마다 핀 mux, 트랜시버, 인터페이스 이름과 커널 설정이 다를 수 있습니다. 다른 Jetson 보드를 사용할 때는 해당 보드 문서를 먼저 확인합니다.
1. STM32 CAN 통신 예제의 기본 원리
Classic CAN은 여러 장치가 두 가닥의 차동 버스(CANH, CANL)를 공유하면서, 메시지 ID를 기준으로 우선순위를 결정하여 통신하는 방식입니다. 이는 Classic CAN과 CAN-FD가 동일합니다.
보드에 따라 다르지만 CAN 컨트롤러가 내장되어 있다고 가정하더라도 이 컨트롤러의 출력을 그대로 통신에 사용할 수 없습니다. 이 신호를 차동 버스에 맞게 변환하는 작업이 필요한데 CAN 트랜시버(Transceiver)가 변환을 합니다.
트랜시버는 다양한 제품들이 있는데 Classic CAN만을 지원하는 제품도 있고 CAN-FD까지 지원하는 제품들도 있습니다. 일반적으로 FD까지 지원하는 제품들의 가격이 높습니다.
이번 실습에서는 Adafruit CAN Pal CAN Bus Transceiver(TJA1051T/3) 모듈을 사용합니다. 이 모듈은 3.3~5 V 로직과 Classic CAN/CAN FD 버스를 연결할 수 있습니다.
아래쪽에 보드에 연결될 핀들(VCC, GND, RX, TX)가 있고 윗쪽 터미널에 CANL, GND, CANH 터미널이 있습니다.

그림 1. CAN Transceiver
UART처럼 TX와 RX를 교차 연결하지 않습니다. MCU의 FDCAN_TX는 트랜시버 TXD에, FDCAN_RX는 RXD에 연결합니다.
그리고 두대의 트랜시버를 서로 연결해야 합니다. 여기에서는 TX, RX가 따로 없습니다. 대신에 차동 신호인 CANH, CANL 과 GND가 있는데 CANH는 CANH끼리, CANL은 CANL끼리 그리고 GND는 GND끼리 연결을 하면 됩니다. 오히려 UART보다 연결이 쉽습니다.

그림 2. CAN Bus연결
CAN 통신은 메시지 ID 기반의 통신을 합니다. 각 메시지 마다 ID를 가지고 있습니다.
만일 여러대의 디바이스가 버스에 연결되어 있다면 모든 노드가 메시지를 수신하고 각 노드(디바이스)는 필터를 사용해서 원하는 ID에 있는 데이터만 전달을 하게 됩니다.
이 ID는 노드를 구분하는 것 이외에 우선순위 같은 의미를 가질 수 있습니다. 한 디바이스에서 우선순위가 높은 메시지와 낮은 메시지를 구분해서 높은 메시지를 먼저 처리하는 기능을 만들 수 있습니다. (RTOS에서 Message Queue를 이용해서 메시지를 처리할 때 많이 사용하는 방법입니다)
메시지 ID는 Standard ID가 있고 Extended ID가 있습니다.
Standard ID는 11비트의 값을 사용하고 0x000 ~ 0x7FF값까지 사용할 수 있습니다.
Extended ID는 29비트의 값을 사용하고 범위는 0x00000000 ~ 0x1FFFFFFF 입니다.
CAN 버스의 물리적 양 끝에는 각각 120 Ω 종단저항을 둡니다. 중간 노드에는 종단저항을 추가하지 않습니다. 그림 1의 모듈은 스위치로 온보드 종단저항을 선택할 수 있으므로 버스에서 노드가 차지하는 위치에 맞게 설정합니다.
CANH와 CANL에는 꼬임선을 권장합니다. 두 선에 비슷하게 유입되는 공통 모드 잡음을 차동 수신기가 억제하므로 노이즈 내성이 좋아지지만, 배선·접지·종단 조건이 잘못되면 오류는 여전히 발생할 수 있습니다.
CAN 컨트롤러는 비트, 스터핑, CRC, 형식과 ACK 오류를 감지하고 송수신 오류 카운터를 관리합니다.
| 오류 | 의미 |
|---|---|
| Bit Error | 송신값과 실제 버스값이 다름 |
| Stuff Error | Bit Stuffing 규칙 위반 |
| CRC Error | CRC 불일치 |
| Form Error | 고정 형식 비트 오류 |
| ACK Error | 정상 수신 노드에서 ACK가 없음 |
중요한 것은 오류 종류뿐 아니라 Error Warning, Error Passive, Bus-Off 상태를 어떻게 기록하고 복구할지 정하는 것입니다.
예를 들어 Bus-Off 이후 자동 재시작 시간을 둘 수 있지만, 반복 오류의 원인을 확인하지 않고 MCU를 무조건 리셋하는 방식은 피합니다.
2. Classic CAN과 CAN FD의 차이
(1) 최대 데이터
Classic CAN은 최대 8바이트, CAN FD는 최대 64바이트의 데이터를 전송합니다. CAN FD의 DLC가 표현하는 길이는 0, 1, 2, 3, 4, 5, 6, 7, 8, 12, 16, 20, 24, 32, 48, 64바이트입니다.
(2) 비트 레이트
Classic CAN은 프레임 전체가 하나의 비트레이트를 사용합니다. CAN FD는 BRS(Bit Rate Switch)를 켜면 중재 구간의 명목 비트레이트와 데이터 구간의 비트레이트를 다르게 설정할 수 있습니다.
(3) 속도
Classic CAN의 명목 비트레이트는 최대 1 Mbit/s입니다. CAN FD의 실제 데이터 비트레이트 상한은 MCU, 트랜시버, 배선 길이와 신호 품질에 따라 달라지며, 이 실습은 5 Mbit/s를 사용합니다.
(4) CRC
에러를 찾아내는데 사용되는 CRC에서도 Classic CAN에서는 15비트 CRC를, CAN-FD에서는 17/21비트의 CRC를 지원합니다.
3. Classic CAN 송수신 실습
실습할 기능은 다음과 같습니다.
- Jetson 에서 CAN 패킷 (ID + Data) 전송
- 데이터를 받아 다른 ID로 Jetson에 전송
- 데이터 값이 00 이면 내장 LED OFF, 값이 01 이면 내장 LED ON
- 전송속도: 500Kbps

그림 3. CAN 실습을 위한 Jetson과 G491RE 보드의 연결
3.1 STM32CubeMX 핀과 비트 타이밍 설정

그림 4. CAN 버스 핀 설정

그림 5. CAN 파라미터 설정
중요 설정
Frame Format: Classic mode
Mode: Normal mode
Nominal Sync Jump Width: 8
Data와 관련된 값: 모두 1
- BRS를 사용하지 않는 Classic CAN이라서 Data rate를 사용하지 않음
Nominal Prescaler: 10
- 입력 주파수 170MHz를 나누는 역할을 함
Nominal Time Seq1: 25
Nominal Time Seq2: 8
비트레이트 계산을 위해 TQ(Time Quantum)계산이 필요한데 이 TQ는 CAN에서 한 비트의 시간을 나누는 가장 작은 시간 단위입니다. 비트레이트 계산은 다음과 같습니다.
Sync Segment: 1 TQ
Nominal Time Seq1: 25 TQ
Nominal Time Seq2: 8 TQ
Total 34 TQ

CAN-FD에서도 비슷한 방법으로 계산을 할것입니다.
PA5는 내장 LED에 연결이 되어 있습니다. PA5를 GPIO_OUTPUT을 세팅하면 됩니다.

그림 6. 내장 LED (GPIO 세팅)
3.2 Classic CAN HAL 코드
VCP 코드입니다. NUCLEO-G491RE에서는 LPUART1 핸들 hlpuart1을 사용합니다. (코드 일부)
/* USER CODE BEGIN 0 */
int __io_putchar(int ch)
{
HAL_UART_Transmit(&hlpuart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
return ch;
}
/* USER CODE END 0 */
/* USER CODE BEGIN 0 */
int __io_putchar(int ch)
{
HAL_UART_Transmit(&hlpuart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
return ch;
}
/* USER CODE END 0 */
헤더, 명령값, 수신·송신 구조체와 버퍼를 선언합니다. (코드 일부)
/* USER CODE BEGIN Includes */
#include <stdio.h>
#include <string.h>
/* USER CODE END Includes */
/* USER CODE BEGIN PD */
#define LED_OFF_COMMAND 0x00U
#define LED_ON_COMMAND 0x01U
/* USER CODE END PD */
/* USER CODE BEGIN PV */
FDCAN_RxHeaderTypeDef RxHeader;
FDCAN_TxHeaderTypeDef TxHeader;
uint8_t RxData[8];
volatile uint8_t canRxReady = 0U;
/* USER CODE END PV */
/* USER CODE BEGIN Includes */
#include <stdio.h>
#include <string.h>
/* USER CODE END Includes */
/* USER CODE BEGIN PD */
#define LED_OFF_COMMAND 0x00U
#define LED_ON_COMMAND 0x01U
/* USER CODE END PD */
/* USER CODE BEGIN PV */
FDCAN_RxHeaderTypeDef RxHeader;
FDCAN_TxHeaderTypeDef TxHeader;
uint8_t RxData[8];
volatile uint8_t canRxReady = 0U;
/* USER CODE END PV */
Classic CAN에서 수신되는 데이터의 크기는 8바이트 입니다. 버퍼의 크기역시 8바이트로 설정 합니다.
CAN 필터, 송신 헤더, HAL_FDCAN_Start()와 최초 수신 알림을 설정합니다. (코드 일부)
CAN 필터와 송신 헤더를 설정합니다.
/* USER CODE BEGIN 0 */
/* __io_putchar() 함수 아래에 추가합니다 */
static void CAN_Config(void)
{
FDCAN_FilterTypeDef sFilterConfig = {0};
sFilterConfig.IdType = FDCAN_STANDARD_ID;
sFilterConfig.FilterIndex = 0;
sFilterConfig.FilterType = FDCAN_FILTER_MASK;
sFilterConfig.FilterConfig = FDCAN_FILTER_TO_RXFIFO0;
sFilterConfig.FilterID1 = 0x123U;
sFilterConfig.FilterID2 = 0x7FFU;
if (HAL_FDCAN_ConfigFilter(&hfdcan1, &sFilterConfig) != HAL_OK)
{
Error_Handler();
}
if (HAL_FDCAN_ConfigGlobalFilter(
&hfdcan1,
FDCAN_REJECT,
FDCAN_REJECT,
FDCAN_REJECT_REMOTE,
FDCAN_REJECT_REMOTE) != HAL_OK)
{
Error_Handler();
}
TxHeader.Identifier = 0x321U;
TxHeader.IdType = FDCAN_STANDARD_ID;
TxHeader.TxFrameType = FDCAN_DATA_FRAME;
TxHeader.DataLength = FDCAN_DLC_BYTES_1;
TxHeader.ErrorStateIndicator = FDCAN_ESI_ACTIVE;
TxHeader.BitRateSwitch = FDCAN_BRS_OFF;
TxHeader.FDFormat = FDCAN_CLASSIC_CAN;
TxHeader.TxEventFifoControl = FDCAN_NO_TX_EVENTS;
TxHeader.MessageMarker = 0U;
if (HAL_FDCAN_Start(&hfdcan1) != HAL_OK)
{
Error_Handler();
}
if (HAL_FDCAN_ActivateNotification(&hfdcan1,
FDCAN_IT_RX_FIFO0_NEW_MESSAGE, 0U) != HAL_OK)
{
Error_Handler();
}
}
/* USER CODE BEGIN 0 */
/* __io_putchar() 함수 아래에 추가합니다 */
static void CAN_Config(void)
{
FDCAN_FilterTypeDef sFilterConfig = {0};
sFilterConfig.IdType = FDCAN_STANDARD_ID;
sFilterConfig.FilterIndex = 0;
sFilterConfig.FilterType = FDCAN_FILTER_MASK;
sFilterConfig.FilterConfig = FDCAN_FILTER_TO_RXFIFO0;
sFilterConfig.FilterID1 = 0x123U;
sFilterConfig.FilterID2 = 0x7FFU;
if (HAL_FDCAN_ConfigFilter(&hfdcan1, &sFilterConfig) != HAL_OK)
{
Error_Handler();
}
if (HAL_FDCAN_ConfigGlobalFilter(
&hfdcan1,
FDCAN_REJECT,
FDCAN_REJECT,
FDCAN_REJECT_REMOTE,
FDCAN_REJECT_REMOTE) != HAL_OK)
{
Error_Handler();
}
TxHeader.Identifier = 0x321U;
TxHeader.IdType = FDCAN_STANDARD_ID;
TxHeader.TxFrameType = FDCAN_DATA_FRAME;
TxHeader.DataLength = FDCAN_DLC_BYTES_1;
TxHeader.ErrorStateIndicator = FDCAN_ESI_ACTIVE;
TxHeader.BitRateSwitch = FDCAN_BRS_OFF;
TxHeader.FDFormat = FDCAN_CLASSIC_CAN;
TxHeader.TxEventFifoControl = FDCAN_NO_TX_EVENTS;
TxHeader.MessageMarker = 0U;
if (HAL_FDCAN_Start(&hfdcan1) != HAL_OK)
{
Error_Handler();
}
if (HAL_FDCAN_ActivateNotification(&hfdcan1,
FDCAN_IT_RX_FIFO0_NEW_MESSAGE, 0U) != HAL_OK)
{
Error_Handler();
}
}
RX FIFO0 새 메시지 알림에서 HAL_FDCAN_GetRxMessage()를 호출하는 콜백입니다. (코드 일부)
/* USER CODE BEGIN 4 */
void HAL_FDCAN_RxFifo0Callback(FDCAN_HandleTypeDef *hfdcan,
uint32_t RxFifo0ITs)
{
if (hfdcan->Instance != FDCAN1)
{
return;
}
if ((RxFifo0ITs & FDCAN_IT_RX_FIFO0_NEW_MESSAGE) != 0U)
{
if (HAL_FDCAN_GetRxMessage(hfdcan, FDCAN_RX_FIFO0,
&RxHeader, RxData) == HAL_OK)
{
canRxReady = 1;
}
}
}
/* USER CODE BEGIN 4 */
void HAL_FDCAN_RxFifo0Callback(FDCAN_HandleTypeDef *hfdcan,
uint32_t RxFifo0ITs)
{
if (hfdcan->Instance != FDCAN1)
{
return;
}
if ((RxFifo0ITs & FDCAN_IT_RX_FIFO0_NEW_MESSAGE) != 0U)
{
if (HAL_FDCAN_GetRxMessage(hfdcan, FDCAN_RX_FIFO0,
&RxHeader, RxData) == HAL_OK)
{
canRxReady = 1;
}
}
}
메시지를 받으면 이벤트가 발생해서 콜백함수가 실행됩니다.
HAL_FDCAN_GetRxMessage 함수를 사용하면 RxHeader (세번째 인자)에 ID를 포함해서 모든 데이터가 구조체에 들어가게 됩니다.
MX_FDCAN1_Init() 뒤 USER CODE BEGIN 2 영역에서 CAN_Config()를 한 번 호출합니다. (코드 일부)
/* USER CODE BEGIN 2 */
CAN_Config();
printf("\r\n");
printf("G491RE Classic CAN Test\r\n");
printf("RX ID: 0x123, TX ID: 0x321\r\n");
printf("Bitrate: 500 kbit/s\r\n");
/* USER CODE END 2 */
/* USER CODE BEGIN 2 */
CAN_Config();
printf("\r\n");
printf("G491RE Classic CAN Test\r\n");
printf("RX ID: 0x123, TX ID: 0x321\r\n");
printf("Bitrate: 500 kbit/s\r\n");
/* USER CODE END 2 */
메인 루프에서 수신 ID와 DLC를 확인하고 LED를 제어한 뒤 ID 0x321로 응답합니다. (코드 일부)
while (1)
{
if (canRxReady != 0U)
{
uint8_t command;
uint32_t identifier;
uint32_t dataLength;
__disable_irq();
command = RxData[0];
identifier = RxHeader.Identifier;
dataLength = RxHeader.DataLength;
canRxReady = 0U;
__enable_irq();
printf("RX ID=0x%03lX Data=%02X\r\n",(unsigned long)identifier,
command);
if ((identifier == 0x123U) &&
(dataLength == FDCAN_DLC_BYTES_1))
{
if (command == LED_ON_COMMAND)
{
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);
printf("LED ON\r\n");
}
else if (command == LED_OFF_COMMAND)
{
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET);
printf("LED OFF\r\n");
}
uint8_t TxData[1] = {command};
if (HAL_FDCAN_GetTxFifoFreeLevel(&hfdcan1) > 0U)
{
if (HAL_FDCAN_AddMessageToTxFifoQ(&hfdcan1,
&TxHeader, TxData) == HAL_OK)
{
printf("TX ID=0x321 OK\r\n");
}
else
{
printf("TX failed\r\n");
}
}
}
}
}
while (1)
{
if (canRxReady != 0U)
{
uint8_t command;
uint32_t identifier;
uint32_t dataLength;
__disable_irq();
command = RxData[0];
identifier = RxHeader.Identifier;
dataLength = RxHeader.DataLength;
canRxReady = 0U;
__enable_irq();
printf("RX ID=0x%03lX Data=%02X\r\n",(unsigned long)identifier,
command);
if ((identifier == 0x123U) &&
(dataLength == FDCAN_DLC_BYTES_1))
{
if (command == LED_ON_COMMAND)
{
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);
printf("LED ON\r\n");
}
else if (command == LED_OFF_COMMAND)
{
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET);
printf("LED OFF\r\n");
}
uint8_t TxData[1] = {command};
if (HAL_FDCAN_GetTxFifoFreeLevel(&hfdcan1) > 0U)
{
if (HAL_FDCAN_AddMessageToTxFifoQ(&hfdcan1,
&TxHeader, TxData) == HAL_OK)
{
printf("TX ID=0x321 OK\r\n");
}
else
{
printf("TX failed\r\n");
}
}
}
}
}
콜백 함수에서 이미 RxHeader라는 구조체에 모든 데이터를 넣었기 때문에 그 구조체에 있는 데이터를 읽어서 비교만 하면 됩니다. 데이터가 00인지 01 인지.
데이터를 보낼때는 HAL_FDCAN_GetTxFifoFreeLevel() 함수를 써서 송신버퍼(FIFO)에 메시지를 넣을 빈 공간이 하나 이상 있는지를 확인한 후에 빈 공간이 있다면 HAL_FDCAN_AddMessageToTxFifoQ() 함수를 사용해서 메시지를 넣습니다.
테스트를 위해서 Jetson쪽 명령도 필요합니다. (코드 일부)
(1) CAN 커널 모듈 로드 (커맨드 라인에서)
sudo modprobe can
sudo modprobe can_raw
sudo modprobe can_dev
sudo modprobe mttcan
sudo modprobe can
sudo modprobe can_raw
sudo modprobe can_dev
sudo modprobe mttcan
(2) CAN 500Kbps 설정 (코드 일부)
sudo ip link set can0 down
sudo ip link set can0 type can \
bitrate 500000 \
berr-reporting on \
restart-ms 100
sudo ip link set can0 up
sudo ip link set can0 down
sudo ip link set can0 type can \
bitrate 500000 \
berr-reporting on \
restart-ms 100
sudo ip link set can0 up
(3) CAN 명령 보내기 (LED 끄기 / LED 켜기) (코드 일부)
cansend can0 123#00
cansend can0 123#01
cansend can0 123#00
cansend can0 123#01
(4) CAN명령이 잘 전송되는지 확인 (다른 터미널을 열어야 함) (코드 일부)
candump can0
candump can0
4. CAN FD 송수신 실습
Classic CAN과 같은 핀을 사용합니다. Connectivity 에서 FDCAN1을 선택하세요.

그림 7. CAN-FD 파라미터 설정
중요 설정
Frame Format: FD mode with BitRate Switching (BRS)
- 가장 중요합니다
Mode: Normal mode
1Mbps 세팅
Nominal Prescaler: 5
Nominal Time Seq1: 25
Nominal Time Seq2: 8
Nominal Sync Jump Width: 8
5Mbps 세팅
Data Prescaler: 1
Data Sync Jump Width: 8
Data Time Seq1: 25
Data Time Seq2: 8
명목 구간은 170 MHz/(5×(1+25+8))=1 Mbit/s입니다.
데이터 구간은 170 MHz/(1×(1+25+8))=5 Mbit/s입니다. SJW는 Time Seg2보다 크지 않게 8로 설정합니다.
4.1 CAN FD HAL 코드
/* USER CODE BEGIN PD */
#define LED_OFF_COMMAND 0x00
#define LED_ON_COMMAND 0x01
/* USER CODE END PD */
/* USER CODE BEGIN PV */
FDCAN_RxHeaderTypeDef RxHeader;
FDCAN_TxHeaderTypeDef TxHeader;
uint8_t RxData[64];
volatile uint8_t canRxReady = 0;
/* USER CODE END PV */
/* USER CODE BEGIN PD */
#define LED_OFF_COMMAND 0x00
#define LED_ON_COMMAND 0x01
/* USER CODE END PD */
/* USER CODE BEGIN PV */
FDCAN_RxHeaderTypeDef RxHeader;
FDCAN_TxHeaderTypeDef TxHeader;
uint8_t RxData[64];
volatile uint8_t canRxReady = 0;
/* USER CODE END PV */
CAN FD의 최대 데이터 필드에 맞춰 수신 버퍼를 64바이트로 설정합니다.
/* USER CODE BEGIN 0 */
/* __io_putchar() 함수 아래에 추가합니다 */
static void CAN_Config(void)
{
FDCAN_FilterTypeDef sFilterConfig = {0};
sFilterConfig.IdType = FDCAN_STANDARD_ID;
sFilterConfig.FilterIndex = 0;
sFilterConfig.FilterType = FDCAN_FILTER_MASK;
sFilterConfig.FilterConfig = FDCAN_FILTER_TO_RXFIFO0;
sFilterConfig.FilterID1 = 0x123U;
sFilterConfig.FilterID2 = 0x7FFU;
if (HAL_FDCAN_ConfigFilter(&hfdcan1, &sFilterConfig) != HAL_OK)
{
Error_Handler();
}
if (HAL_FDCAN_ConfigGlobalFilter(
&hfdcan1,
FDCAN_REJECT,
FDCAN_REJECT,
FDCAN_REJECT_REMOTE,
FDCAN_REJECT_REMOTE) != HAL_OK)
{
Error_Handler();
}
TxHeader.Identifier = 0x321U;
TxHeader.IdType = FDCAN_STANDARD_ID;
TxHeader.TxFrameType = FDCAN_DATA_FRAME;
TxHeader.DataLength = FDCAN_DLC_BYTES_1;
TxHeader.ErrorStateIndicator = FDCAN_ESI_ACTIVE;
TxHeader.BitRateSwitch = FDCAN_BRS_ON;
TxHeader.FDFormat = FDCAN_FD_CAN;
TxHeader.TxEventFifoControl = FDCAN_NO_TX_EVENTS;
TxHeader.MessageMarker = 0U;
if (HAL_FDCAN_Start(&hfdcan1) != HAL_OK)
{
Error_Handler();
}
if (HAL_FDCAN_ActivateNotification(&hfdcan1,
FDCAN_IT_RX_FIFO0_NEW_MESSAGE, 0U) != HAL_OK)
{
Error_Handler();
}
}
/* USER CODE BEGIN 0 */
/* __io_putchar() 함수 아래에 추가합니다 */
static void CAN_Config(void)
{
FDCAN_FilterTypeDef sFilterConfig = {0};
sFilterConfig.IdType = FDCAN_STANDARD_ID;
sFilterConfig.FilterIndex = 0;
sFilterConfig.FilterType = FDCAN_FILTER_MASK;
sFilterConfig.FilterConfig = FDCAN_FILTER_TO_RXFIFO0;
sFilterConfig.FilterID1 = 0x123U;
sFilterConfig.FilterID2 = 0x7FFU;
if (HAL_FDCAN_ConfigFilter(&hfdcan1, &sFilterConfig) != HAL_OK)
{
Error_Handler();
}
if (HAL_FDCAN_ConfigGlobalFilter(
&hfdcan1,
FDCAN_REJECT,
FDCAN_REJECT,
FDCAN_REJECT_REMOTE,
FDCAN_REJECT_REMOTE) != HAL_OK)
{
Error_Handler();
}
TxHeader.Identifier = 0x321U;
TxHeader.IdType = FDCAN_STANDARD_ID;
TxHeader.TxFrameType = FDCAN_DATA_FRAME;
TxHeader.DataLength = FDCAN_DLC_BYTES_1;
TxHeader.ErrorStateIndicator = FDCAN_ESI_ACTIVE;
TxHeader.BitRateSwitch = FDCAN_BRS_ON;
TxHeader.FDFormat = FDCAN_FD_CAN;
TxHeader.TxEventFifoControl = FDCAN_NO_TX_EVENTS;
TxHeader.MessageMarker = 0U;
if (HAL_FDCAN_Start(&hfdcan1) != HAL_OK)
{
Error_Handler();
}
if (HAL_FDCAN_ActivateNotification(&hfdcan1,
FDCAN_IT_RX_FIFO0_NEW_MESSAGE, 0U) != HAL_OK)
{
Error_Handler();
}
}
Classic CAN 코드와 비교해 보면 두개의 설정만 다릅니다.
Classic CAN:
TxHeader.BitRateSwitch = FDCAN_BRS_OFF;
TxHeader.FDFormat = FDCAN_CLASSIC_CAN;
CAN-FD:
TxHeader.BitRateSwitch = FDCAN_BRS_ON;
TxHeader.FDFormat = FDCAN_FD_CAN;
콜백 함수는 동일합니다.
현재 STM32G4 HAL의 RxHeader.DataLength는 하위 4비트에 DLC 코드(0~15)를 저장합니다. 실제 바이트 수는 하위 니블을 마스크한 뒤 변환표로 구합니다. USER CODE 0에 추가합니다. (코드 일부)
static uint8_t FDCAN_DlcToBytes(uint32_t dlc)
{
static const uint8_t dlcToBytes[16] =
{
0, 1, 2, 3, 4, 5, 6, 7, 8, 12, 16, 20, 24, 32, 48, 64
};
return dlcToBytes[dlc & 0x0FU];
}
static uint8_t FDCAN_DlcToBytes(uint32_t dlc)
{
static const uint8_t dlcToBytes[16] =
{
0, 1, 2, 3, 4, 5, 6, 7, 8, 12, 16, 20, 24, 32, 48, 64
};
return dlcToBytes[dlc & 0x0FU];
}
초기화와 수신 데이터 복사·출력 부분입니다. 표시 비트레이트는 CubeMX 설정과 동일한 명목 1 Mbit/s, 데이터 5 Mbit/s입니다. (코드 일부)
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_LPUART1_UART_Init();
MX_FDCAN1_Init();
CAN_Config();
printf("\r\n");
printf("G491RE CAN FD Test\r\n");
printf("RX ID: 0x123, TX ID: 0x321\r\n");
printf("Nominal bitrate: 1 Mbit/s\r\n");
printf("Data bitrate: 5 Mbit/s\r\n");
while (1)
{
if (canRxReady)
{
FDCAN_RxHeaderTypeDef localHeader;
uint8_t localData[64];
uint8_t length;
__disable_irq();
localHeader = RxHeader;
memcpy(localData, RxData, sizeof(localData));
canRxReady = 0;
__enable_irq();
length = FDCAN_DlcToBytes(localHeader.DataLength);
printf("RX ID=0x%03lX Length=%u Data=",
(unsigned long)localHeader.Identifier, length);
for (uint8_t i = 0; i < length; i++)
{
printf("%02X ", localData[i]);
}
printf("\r\n");
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_LPUART1_UART_Init();
MX_FDCAN1_Init();
CAN_Config();
printf("\r\n");
printf("G491RE CAN FD Test\r\n");
printf("RX ID: 0x123, TX ID: 0x321\r\n");
printf("Nominal bitrate: 1 Mbit/s\r\n");
printf("Data bitrate: 5 Mbit/s\r\n");
while (1)
{
if (canRxReady)
{
FDCAN_RxHeaderTypeDef localHeader;
uint8_t localData[64];
uint8_t length;
__disable_irq();
localHeader = RxHeader;
memcpy(localData, RxData, sizeof(localData));
canRxReady = 0;
__enable_irq();
length = FDCAN_DlcToBytes(localHeader.DataLength);
printf("RX ID=0x%03lX Length=%u Data=",
(unsigned long)localHeader.Identifier, length);
for (uint8_t i = 0; i < length; i++)
{
printf("%02X ", localData[i]);
}
printf("\r\n");
수신 프레임의 첫 바이트로 LED를 제어하고 같은 DLC로 ID 0x321 응답을 전송합니다. (코드 일부)
if ((localHeader.Identifier == 0x123) && (length >= 1))
{
if (localData[0] == LED_ON_COMMAND)
{
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);
printf("LED ON\r\n");
}
else if (localData[0] == LED_OFF_COMMAND)
{
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET);
printf("LED OFF\r\n");
}
}
TxHeader.DataLength = localHeader.DataLength;
if (HAL_FDCAN_AddMessageToTxFifoQ(&hfdcan1,
&TxHeader, localData) == HAL_OK)
{
printf("CAN FD TX ID=0x321 OK\r\n");
}
else
{
printf("CAN FD TX failed\r\n");
}
}
}
}
if ((localHeader.Identifier == 0x123) && (length >= 1))
{
if (localData[0] == LED_ON_COMMAND)
{
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);
printf("LED ON\r\n");
}
else if (localData[0] == LED_OFF_COMMAND)
{
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET);
printf("LED OFF\r\n");
}
}
TxHeader.DataLength = localHeader.DataLength;
if (HAL_FDCAN_AddMessageToTxFifoQ(&hfdcan1,
&TxHeader, localData) == HAL_OK)
{
printf("CAN FD TX ID=0x321 OK\r\n");
}
else
{
printf("CAN FD TX failed\r\n");
}
}
}
}
뒷부분은 이전 Classic과 동일합니다.
4.2 Jetson CAN FD 설정과 송수신 테스트
STM32CubeMX 값과 같은 명목 1 Mbit/s·데이터 5 Mbit/s로 can0를 설정하고 BRS 플래그가 있는 CAN FD 프레임을 보냅니다. 보드에 따라 mttcan 로드와 핀 mux 설정이 추가로 필요합니다. (코드 일부)
sudo ip link set can0 down
sudo ip link set can0 type can bitrate 1000000 dbitrate 5000000 fd on \
berr-reporting on restart-ms 100
sudo ip link set can0 up
cansend can0 123##10102030405060708090A0B0C
candump can0

그림 8. VCP 출력값

그림 9. candump 출력

그림 10. 입력 데이터의 길이가 조건을 만족하지 못하면 발생하는 에러
5. 정리
이상으로 Classic CAN과 CAN FD의 물리 연결, 비트 타이밍, 필터, 수신 인터럽트와 응답 송신을 확인했습니다. 실제 장치 수와 버스 길이가 늘어나면 종단, 샘플 포인트, 오류 상태와 재전송 정책을 함께 검증해야 합니다.
6. 참고 자료
STMicroelectronics RM0440 Reference Manual: 공식 문서 열기
STMicroelectronics UM2505 NUCLEO-G491RE 보드 매뉴얼: 공식 문서 열기
Adafruit CAN Pal TJA1051T/3 제품 정보: 공식 문서 열기
Linux Kernel CAN 문서: 공식 문서 열기
지속적인 성장
STM32 CAN 통신 예제: CubeMX와 HAL로 송수신 구현하기
핵심 요약 Classic CAN과 CAN FD의 프레임 및 비트 타이밍 차이를 설명합니다. NUCLEO-G491RE와 Jetson TX2를 CAN 트랜시버로 연결합니다. CubeMX 필터·인터럽트 설정과 HAL 기반 송수신 코드를 구현합니다. 종단저항, CANH·CANL, candump와 오류 응답을 이용한 점검 방법을 다룹니다. Nucleo-G491RE 보드와 Jetson 보드를 이용한 CAN 버스 통신 이 글에서는 STM32 CAN 통신 예제로 NUCLEO-G491RE와 Jetson TX2를 연결하고, Classic CAN과 CAN FD의 비트 타이밍·필터·수신 인터럽트·응답 송신을 단계별로 구현합니다. 브레드보드에서 트랜시버와 종단저항, CANH·CANL 연결을 검증한 뒤 커넥터와 테스트 포인트를 작은 CAN 인터페이스 PCB로 정리하면 반복 시험과 배선 점검이 쉬워집니다. PCB 제작시Gerber 파일은 JLCPCB PCB 서비스에 업로드하고 온라인 견적에서 제작 가능 여부와 비용을 확......
STM32 PWM 제어 가이드: 주파수·듀티비 설정과 모터 제어 예제
핵심 요약 타이머 Prescaler와 ARR을 이용한 PWM 주파수·듀티비 계산 방법을 정리합니다. NUCLEO-G491RE에서 서보모터와 DC 모터를 PWM으로 제어합니다. 단일 채널 및 A/B상 인코더를 읽어 회전 방향과 RPM을 계산합니다. CubeMX 설정, HAL 코드와 VCP 출력 결과를 실제 실습 이미지로 확인합니다. Nucleo-G491RE 보드와 DC 모터/인코더를 이용한 실습 이 글에서는 STM32 PWM의 주파수·듀티비 계산 원리를 익히고, NUCLEO-G491RE의 타이머와 카운터를 이용해 서보모터·DC 모터를 구동하며 단일 채널 및 A/B상 인코더를 읽습니다. 브레드보드에서 모터 드라이버와 인코더 동작을 확인한 뒤 전원·신호 커넥터와 테스트 포인트를 작은 인터페이스 PCB로 정리하면 배선 오류를 줄이고 재현성을 높일 수 있습니다. PCB 제작시Gerber 파일은 JLCPCB PCB 서비스에 업로드하고 온라인 견적에서 제작 가능 여부와 비용을 확인할 수 있습니다. 타이머/......
STM32 ADC 사용법: 전압 측정, 인터럽트·DMA와 DAC 피드백
핵심 요약 STM32 ADC의 해상도, 샘플링과 디지털 값을 전압으로 환산하는 원리를 설명합니다. NUCLEO-F446RE에서 Polling, Interrupt, DMA 방식으로 ADC 데이터를 수집합니다. TEMT6000 조도 센서와 다중 채널 ADC 설정 및 Circular DMA 처리를 구현합니다. DAC 출력과 LED 조명 피드백을 연결해 간단한 실시간 제어를 확인합니다. 이 글에서는 STM32 ADC의 기본 원리와 전압 계산 방법을 살펴보고, NUCLEO-F446RE에서 TEMT6000의 밝기 값을 Polling·인터럽트·DMA로 읽어 DAC 기반 LED 조명 피드백까지 구현합니다. 브레드보드에서 센서와 LED 드라이버의 동작을 확인한 뒤 커넥터와 테스트 포인트를 작은 인터페이스 PCB로 정리하면 배선 오류를 줄이고 같은 실험 환경을 반복하기 쉽습니다. PCB 제작시에Gerber 파일은 JLCPCB PCB 서비스에 업로드하고 온라인 견적에서 제작 가능 여부와 비용을 확인할 수 있습니다......
STM32 UART 통신 예제: 송수신, 인터럽트 및 DMA 구현 가이드
핵심 요약 Polling, Interrupt, DMA 수신 방식의 차이와 CPU·버퍼의 역할을 비교합니다. NUCLEO-G491RE, NUCLEO-F446RE와 BH1750을 연결해 실제 센서 데이터를 전송합니다. 115200 bps, 8-N-1, TX/RX 교차 연결과 공통 GND 설정을 단계별로 확인합니다. DMA Normal 및 Circular 모드와 Receive-to-Idle 처리 방법을 예제로 구현합니다. Virtual COM, 인터럽트 및 DMA로 센서 데이터 송수신 하기 NUCLEO-G491RE, NUCLEO-F446RE, BH1750 1. 이번 글에서 만들 시스템 STM32 UART 통신 예제를 처음 배울 때는 문자열 하나를 보내는 예제부터 시작하는 경우가 많습니다. 문자열이 터미널에 출력되면 통신이 된 것처럼 보입니다. 하지만 실제 임베디드 시스템에서는 센서가 계속 데이터를 만들고, 다른 MCU가 그 데이터를 놓치지 않고 받아 처리해야 합니다. 이번 글에서는 Nucleo-G4......
EasyEDA에서 커스텀 PCB 풋프린트 만드는 방법
필요한 PCB 풋프린트가 EasyEDA 부품 라이브러리에 없다면 직접 만들 수 있습니다. 풋프린트 라이브러리 편집기는 PCB 편집기와 유사한 도구를 사용하지만 일부 불필요한 기능은 표시되지 않을 수 있습니다. 풋프린트는 부품 본체의 크기, 패키지 형식, 패드 크기, 핀 길이와 폭, 핀 간격 등 실제 실장에 필요한 정보를 도형으로 표현한 PCB 설계 데이터입니다. 기호 라이브러리 작성과 기본 흐름이 비슷하며, 이 글에서는 EasyEDA에서 커스텀 풋프린트를 만드는 과정을 단계별로 설명합니다. 회로도에 관한 내용은 전기 회로도 설계 가이드를 참고하십시오. PCB 패드와 풋프린트 설계 PCB 설계에서는 부품을 선택하고 보드 위에 배치한 뒤 각 부품을 전기적으로 연결합니다. 부품마다 핀 수, 핀 배열과 패키지 형식이 다릅니다. PCB 패드는 부품 리드나 단자를 납땜하는 노출 금속 영역이며, 패드를 정확한 위치에 배열하고 외곽선과 조립 정보를 더한 것이 풋프린트입니다. 부품 라이브러리 작성은 크게 두 ......
PCB 레이어 수 선택 방법: 단면·양면·다층 PCB 설계 가이드
PCB 설계를 시작할 때 가장 중요한 결정 중 하나는 동박 레이어 수를 정하는 것입니다. 레이어 수는 배선 공간, 신호 무결성, 전원 분배, 방열, 기판 크기와 제조 비용에 영향을 줍니다. 이 글에서는 프로젝트에 적합한 PCB 레이어 수를 판단하는 기준과 단면·양면·다층 PCB의 설계 지침을 설명합니다. PCB 레이어 수를 결정하는 기준 회로 복잡도 분석 부품 수가 많고 연결 관계가 복잡할수록 더 많은 배선 공간이 필요합니다. 부품 밀도가 높거나 BGA와 같이 핀 수가 많은 부품을 사용하면 추가 신호 레이어가 필요할 수 있습니다. 여러 고속 신호나 민감한 아날로그 신호가 포함된 설계에서는 전원 및 그라운드 플레인을 별도로 배치할 수 있는 추가 레이어가 신호 무결성 향상에 도움이 됩니다. 연속적인 리턴 경로를 확보하면 노이즈, 신호 간섭과 크로스토크를 줄일 수 있습니다. 기판 크기와 공간 제약 고려 PCB가 소형 전자기기 내부에 들어가야 한다면 사용할 수 있는 기판 면적이 제한됩니다. 이 경우 ......