STM32G474RE FDCAN Bus-Off with MS72450 motor controller — signals visible on scope but no ACK
Hi,
I am trying to establish CAN communication between a
NUCLEO-G474RE and a YMMOTOR MS72450 motor controller
using the FDCAN1 peripheral.
HARDWARE SETUP:
- STM32 NUCLEO-G474RE (STM32G474RETx)
- Transceiver: WCMCU-230 (SN65HVD230), 3.3V, connected
to PA11 (FDCAN1_RX) and PA12 (FDCAN1_TX) via AF9
- Motor controller: YMMOTOR MS72450 (CAN 2.0B, 500kbps,
standard frame, Motorola byte order)
- Termination: 120 ohm on motor controller side (factory
default built-in). 120 ohm on transceiver side.
- Common GND connected between STM32 and motor controller
- Motor controller ACC pin (pin 10, 48-85V) is powered
CLOCK CONFIGURATION:
- HSI 16MHz -> PLL -> 170MHz system clock
- FDCAN kernel clock = PLLQ = 170MHz
- NominalPrescaler=17, TimeSeg1=15, TimeSeg2=4, SJW=1
- Calculated bitrate = 170MHz / (17 x 20TQ) = 500kbps
SYMPTOM:
- Green LED blinks initially = TX frames being queued
successfully (HAL_FDCAN_AddMessageToTxFifoQ returns HAL_OK)
- Oscilloscope on CANH/CANL shows clear CAN signals from
the motor controller side
- But STM32 always goes into Bus-Off after a few seconds
- busOffCount variable keeps incrementing
- rxCounter stays at 0 — no frames received from motor
controller
- Motor controller is broadcasting 0x411/0x412 every 50ms
(confirmed on scope)
WHAT I THINK IS HAPPENING:
Motor controller receives our 0x415 frame but does not
ACK it, possibly due to bitrate mismatch from HSI
oscillator tolerance (+/-1%). STM32 error counter climbs
to Bus-Off.
QUESTION:
1. Is HSI clock tolerance causing the bitrate mismatch
that prevents ACK?
2. Should I switch to HSE (ST-Link MCO 8MHz bypass) for
more accurate timing?
3. Is there anything wrong with my FDCAN configuration?
MY FDCAN INIT CODE:
hfdcan1.Init.ClockDivider = FDCAN_CLOCK_DIV1;
hfdcan1.Init.FrameFormat = FDCAN_FRAME_CLASSIC;
hfdcan1.Init.Mode = FDCAN_MODE_NORMAL;
hfdcan1.Init.AutoRetransmission = DISABLE;
hfdcan1.Init.NominalPrescaler = 17;
hfdcan1.Init.NominalSyncJumpWidth = 1;
hfdcan1.Init.NominalTimeSeg1 = 15;
hfdcan1.Init.NominalTimeSeg2 = 4;
hfdcan1.Init.StdFiltersNbr = 1;
MY SYSTEM CLOCK CODE:
HSI, PLLM=DIV4, PLLN=85, PLLQ=DIV2
-> FDCAN kernel clock = 170MHz
-> 170MHz / (17 x 20TQ) = 500,000 bps
OSCILLOSCOPE:
[ATTACH YOUR SCOPE PHOTO HERE]
Time div: 20us/div
CH1 (CANH): 2V/div
CH2 (CANL): 2V/div
Clear CAN frame activity visible from motor controller.
Thank you
/* USER CODE BEGIN Header */
/**
******************************************************************************
* @file : main.c
* @brief : Main program body - CAN Motor Controller Test
******************************************************************************
*/
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
/* Private variables ---------------------------------------------------------*/
COM_InitTypeDef BspCOMInit;
FDCAN_HandleTypeDef hfdcan1;
/* USER CODE BEGIN PV */
volatile uint32_t rxCounter = 0;
volatile uint8_t lastRxData[8];
volatile uint32_t lastRxID = 0;
/* USER CODE END PV */
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_FDCAN1_Init(void);
/**
* @brief The application entry point.
*/
int main(void)
{
/* MCU Configuration */
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_FDCAN1_Init();
/* Initialize LED */
BSP_LED_Init(LED_GREEN);
/* Initialize USER push-button */
BSP_PB_Init(BUTTON_USER, BUTTON_MODE_EXTI);
/* Initialize COM1 port (115200, 8 bits, no parity) */
BspCOMInit.BaudRate = 115200;
BspCOMInit.WordLength = COM_WORDLENGTH_8B;
BspCOMInit.StopBits = COM_STOPBITS_1;
BspCOMInit.Parity = COM_PARITY_NONE;
BspCOMInit.HwFlowCtl = COM_HWCONTROL_NONE;
if (BSP_COM_Init(COM1, &BspCOMInit) != BSP_ERROR_NONE)
{
Error_Handler();
}
/* Infinite loop */
while (1)
{
FDCAN_TxHeaderTypeDef TxHeader;
uint8_t TxData[8];
// CAN ID 0x415 = VCU -> Motor Controller command frame
TxHeader.Identifier = 0x415;
TxHeader.IdType = FDCAN_STANDARD_ID;
TxHeader.TxFrameType = FDCAN_DATA_FRAME;
TxHeader.DataLength = FDCAN_DLC_BYTES_8;
TxHeader.ErrorStateIndicator = FDCAN_ESI_ACTIVE;
TxHeader.BitRateSwitch = FDCAN_BRS_OFF;
TxHeader.FDFormat = FDCAN_CLASSIC_CAN;
TxHeader.TxEventFifoControl = FDCAN_NO_TX_EVENTS;
TxHeader.MessageMarker = 0;
// Byte 0: Throttle percentage (0x00 = 0%, safe start)
// Change to 0x64 (=100 decimal = 100%) to command full throttle later
TxData[0] = 0x00;
// Byte 1: Gear + CAN control bits
// 0x08 = binary 00001000
// bit[7:5] = 000 -> P gear (safe, motor won't move)
// bit[4] = 0 -> no reverse
// bit[3] = 1 -> gear/throttle is CAN-controlled (not hardwire)
// bit[2:0] = 000 -> reserved
// Change to 0x68 (= 01101000) for 1st gear + CAN controlled once comms confirmed
TxData[1] = 0x08;
// Byte 2: Function switch (all off for now)
TxData[2] = 0x00;
// Bytes 3-7: unused, set to 0
TxData[3] = 0x00;
TxData[4] = 0x00;
TxData[5] = 0x00;
TxData[6] = 0x00;
TxData[7] = 0x00;
// Send the frame - LED blinks on every successful TX
if (HAL_FDCAN_AddMessageToTxFifoQ(&hfdcan1, &TxHeader, TxData) == HAL_OK)
{
BSP_LED_Toggle(LED_GREEN);
}
// Motor controller expects 0x415 every 50ms per the CjPower spec
HAL_Delay(50);
}
}
/**
* @brief System Clock Configuration
* HSI -> PLL -> 170MHz system clock
* FDCAN kernel clock = PLLQ = 170MHz
* Bit timing: 170MHz / (Prescaler17 x 20TQ) = 500kbps
*/
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1_BOOST);
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI;
RCC_OscInitStruct.HSIState = RCC_HSI_ON;
RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSI;
RCC_OscInitStruct.PLL.PLLM = RCC_PLLM_DIV4;
RCC_OscInitStruct.PLL.PLLN = 85;
RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2;
RCC_OscInitStruct.PLL.PLLQ = RCC_PLLQ_DIV2;
RCC_OscInitStruct.PLL.PLLR = RCC_PLLR_DIV2;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK
| RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4) != HAL_OK)
{
Error_Handler();
}
}
/**
* @brief FDCAN1 Initialization
* Classic CAN, Normal mode, 500kbps
*/
static void MX_FDCAN1_Init(void)
{
hfdcan1.Instance = FDCAN1;
hfdcan1.Init.ClockDivider = FDCAN_CLOCK_DIV1;
hfdcan1.Init.FrameFormat = FDCAN_FRAME_CLASSIC;
hfdcan1.Init.Mode = FDCAN_MODE_NORMAL;
hfdcan1.Init.AutoRetransmission = DISABLE;
hfdcan1.Init.TransmitPause = DISABLE;
hfdcan1.Init.ProtocolException = DISABLE;
// Bit timing for 500kbps @ 170MHz FDCAN kernel clock
// 170MHz / (17 x 20TQ) = 500,000 bps
hfdcan1.Init.NominalPrescaler = 17;
hfdcan1.Init.NominalSyncJumpWidth = 1;
hfdcan1.Init.NominalTimeSeg1 = 15;
hfdcan1.Init.NominalTimeSeg2 = 4;
// Data phase (unused in Classic CAN, kept for HAL compatibility)
hfdcan1.Init.DataPrescaler = 17;
hfdcan1.Init.DataSyncJumpWidth = 4;
hfdcan1.Init.DataTimeSeg1 = 15;
hfdcan1.Init.DataTimeSeg2 = 4;
hfdcan1.Init.StdFiltersNbr = 1;
hfdcan1.Init.ExtFiltersNbr = 0;
hfdcan1.Init.TxFifoQueueMode = FDCAN_TX_FIFO_OPERATION;
if (HAL_FDCAN_Init(&hfdcan1) != HAL_OK)
{
Error_Handler();
}
// Filter: accept ALL standard IDs (catch-all for debugging)
// Once comms confirmed, narrow to 0x411/0x412 only
FDCAN_FilterTypeDef sFilterConfig;
sFilterConfig.IdType = FDCAN_STANDARD_ID;
sFilterConfig.FilterIndex = 0;
sFilterConfig.FilterType = FDCAN_FILTER_MASK;
sFilterConfig.FilterConfig = FDCAN_FILTER_TO_RXFIFO0;
sFilterConfig.FilterID1 = 0x000; // ID
sFilterConfig.FilterID2 = 0x000; // Mask 0x000 = accept everything
if (HAL_FDCAN_ConfigFilter(&hfdcan1, &sFilterConfig) != HAL_OK)
{
Error_Handler();
}
// Accept non-matching frames into FIFO0 as well (belt and suspenders)
if (HAL_FDCAN_ConfigGlobalFilter(&hfdcan1,
FDCAN_ACCEPT_IN_RX_FIFO0,
FDCAN_ACCEPT_IN_RX_FIFO0,
FDCAN_FILTER_REMOTE,
FDCAN_FILTER_REMOTE) != HAL_OK)
{
Error_Handler();
}
// Enable FDCAN interrupt in NVIC so RxFifo0Callback actually fires
HAL_NVIC_SetPriority(FDCAN1_IT0_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(FDCAN1_IT0_IRQn);
// Start peripheral FIRST
if (HAL_FDCAN_Start(&hfdcan1) != HAL_OK)
{
Error_Handler();
}
// THEN activate RX interrupt notification
if (HAL_FDCAN_ActivateNotification(&hfdcan1,
FDCAN_IT_RX_FIFO0_NEW_MESSAGE, 0) != HAL_OK)
{
Error_Handler();
}
}
/**
* @brief GPIO Initialization
* Configures PA11/PA12 as FDCAN1 RX/TX alternate function
*/
static void MX_GPIO_Init(void)
{
// Enable GPIO port clocks
__HAL_RCC_GPIOC_CLK_ENABLE();
__HAL_RCC_GPIOF_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
// Configure PA11 (FDCAN1_RX) and PA12 (FDCAN1_TX)
// as Alternate Function - connects pins to FDCAN1 peripheral internally
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_11 | GPIO_PIN_12;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF9_FDCAN1;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}
/**
* @brief RX Callback - fires automatically when a CAN frame arrives
* Updates rxCounter, lastRxID, lastRxData for Live Expressions monitoring
*/
void HAL_FDCAN_RxFifo0Callback(FDCAN_HandleTypeDef *hfdcan, uint32_t RxFifo0ITs)
{
FDCAN_RxHeaderTypeDef RxHeader;
uint8_t RxData[8];
if (HAL_FDCAN_GetRxMessage(hfdcan, FDCAN_RX_FIFO0, &RxHeader, RxData) == HAL_OK)
{
rxCounter++;
lastRxID = RxHeader.Identifier;
for (int i = 0; i < 8; i++)
{
lastRxData[i] = RxData[i];
}
}
}
/**
* @brief Error Handler
*/
void Error_Handler(void)
{
__disable_irq();
while (1) { }
}
#ifdef USE_FULL_ASSERT
void assert_failed(uint8_t *file, uint32_t line) { }
#endif
