Запускаем модуль flexible data rate can на STM32H743 на регистрах без HAL и cubemx.

Вступление.

Новый стандарт FDCAN был разработан компанией BOSCH в 2012 году. Стандарт обеспечивает больший объём передаваемых данных и скорости больше 1 мбит (для поля данных), скорость которого может достигать 12 мбит. Размер поля данных был увеличен до 64 байт. Что касается совместимости can и fdcan, то приёмники нового стандарта понимают старый вариант протокола, а старые приёмники не совместимы с новым стандартом. Углубляться в особенности протокола не будем, т.к. этой информации достаточно в интернете, а сразу перейдём к реализации fdcan от stm.

Инициализация модуля FDCAN.

Основное отличие FDCAN от BxCAN в микроконтроллерах stm32 заключается в реализации выделенной памяти для фильтров и буферов. В мк stm32h743 для fdcan выделено ровно 10 кбайт общей памяти (для fdcan1 и fdcan2). Эта область памяти гибко настраивается под любые нужны.

Адресация этой памяти идёт по 4 байта (слово): младшие два бита адреса не используются и адрес битовых полей xxSA (стартовый адрес каждого раздела) сдвинут на два бита вправо. Начальный стартовый адрес этой области памяти в мк stm32h743 = 0x4000AC00.

Далее последовательность настройки:

1) Выбираем источник тактирования модулей fdcan:

  • 00: hse_ck clock is selected as FDCAN kernel clock (default after reset)

  • 01: pll1_q_ck clock is selected as FDCAN kernel clock

  • 10: pll2_q_ck clock is selected as FDCAN

RCC->D2CCIP1R |= RCC_D2CCIP1R_FDCANSEL_1;

2) Настраиваем пины GPIO на нужную нам функцию:

Если используем стандартный CAN, то ищем пины FDCANxTX / FDCANxRX. Если используем FDCAN, то пины соответственно FDCANx_TXFD_MODE / FDCANxRXFDMODE. В моём примере я использовал обычный CAN на пинах PA11 (RX) и PA12 (TX), альтернативная функция #9. Использую свою инициализацию. Библиотека есть на github`e.

gpio_init (PORT_A, 11, MODE_ALT_F, TYPE_PUSH_PULL, SPEED_MAX, PULL_NO, ALTF_9); // RX
gpio_init (PORT_A, 12, MODE_ALT_F, TYPE_PUSH_PULL, SPEED_MAX, PULL_NO, ALTF_9); // TX

3) Включаем FDCAN модули:

RCC->APB1HENR |= RCC_APB1HENR_FDCANEN;

4) Входим в процесс инициализации:

FDCAN1->CCCR |= FDCAN_CCCR_INIT; 
while ((FDCAN1->CCCR & FDCAN_CCCR_INIT) == 0) {};

5) Разрешаем запись в регистры настройки:

FDCAN1->CCCR |= FDCAN_CCCR_CCE;

6) Включаем классический CAN и выключаем автоматическую ретрансляцию:

FDCAN1->CCCR &= ~(FDCAN_CCCR_FDOE);          
FDCAN1->CCCR |= FDCAN_CCCR_DAR; // RETR OFF

7) При необходимости можно включить Loopback режим для отладки:

FDCAN1->CCCR |= FDCAN_CCCR_TEST;
FDCAN1->TEST |= FDCAN_TEST_LBCK;

8) Очищаем область памяти:

#define FDCAN_MEM_START_ADDR          0x4000AC00UL
#define FDCAN_MEM_END_ADDR            0x4000D3FFUL

unsigned long *i;
for (i = (unsigned long*)FDCAN_MEM_START_ADDR; i < (unsigned long*)FDCAN_MEM_END_ADDR; i++) *i = 0;

9) Настраиваем номинальные time quanta сегменты и при необходимости time quanta сегменты для переменного битрейта :

#define CAN_PRESCALER   6 
#define CAN_SYNC_JW     2 
#define CAN_SYNC_SEG    1
#define CAN_PHASE_SEG1 11 
#define CAN_PHASE_SEG2  4

// nominal time quanta segments
FDCAN1->NBTP  = (CAN_SYNC_JW    - 1) << FDCAN_NBTP_NSJW_Pos;
FDCAN1->NBTP |= (CAN_PRESCALER  - 1) << FDCAN_NBTP_NBRP_Pos;
FDCAN1->NBTP |= (CAN_PHASE_SEG1 - 1) << FDCAN_NBTP_NTSEG1_Pos;
FDCAN1->NBTP |= (CAN_PHASE_SEG2 - 1) << FDCAN_NBTP_NTSEG2_Pos;

// flexible bitrate time quanta segments
FDCAN1->DBTP  = (CAN_SYNC_JW    - 1) << FDCAN_DBTP_DSJW_Pos;
FDCAN1->DBTP |= (CAN_PRESCALER  - 1) << FDCAN_DBTP_DBRP_Pos;
FDCAN1->DBTP |= (CAN_PHASE_SEG1 - 1) << FDCAN_DBTP_DTSEG1_Pos;
FDCAN1->DBTP |= (CAN_PHASE_SEG2 - 1) << FDCAN_DBTP_DTSEG2_Pos;

В итоге, при частоте fdcan kernel clock равной 48 МГц, битрейт получается 500 КГц.

Один бит протокола can состоит из четырёх временных сегментов:

  • Сегмент синхронизации (SYNC_SEG)

  • Cегмент распространения (PROP_SEG)

  • Фазовый сегмент 1 (PHASE_SEG1)

  • Фазовый сегмент 2 (PHASE_SEG2)

В реализации stm используются три сегмента:

  • Сегмент синхронизации (SYNC_SEG):

  • Битовый сегмент 1 (включая PROP_SEG и PHASE_SEG1)

  • Битовый сегмент 2 (PHASE_SEG2)

Сегменты одинарного бита протокола CAN
Сегменты одинарного бита протокола CAN

Time quanta - минимальная неделимая единица времени в протоколе CAN. Её получаем разделив единицу на fdcan kernel clock. Получаем Tq = 2.08333333e-8. Далее вступает в действие прескалер частоты. Мы умножаем нашу Tq на значение CAN_PRESCALER и получаем Tq = 0,000000125 сек.

Чтобы получить конечное время одного бита, умножаем сумму наших сегментов (1 + 11 + 4) на Tq и получаем 0,000002 сек.

В последнем действии берём единицу и делим на наше время одного бита. 1 / 0,000002 = 500 000 Гц.

10) Выключаем приём не нужных пакетов:

FDCAN1->GFC |= FDCAN_GFC_ANFS; // Reject non-matching frames standard
FDCAN1->GFC |= FDCAN_GFC_ANFE; // Reject non-matching frames extended
FDCAN1->GFC |= FDCAN_GFC_RRFS; // Reject all remote frames with 11-bit standard ID
FDCAN1->GFC |= FDCAN_GFC_RRFE; // Reject all remote frames with 29-bit standard ID

11) Настраиваем количество фильтров и их начальные адреса в области выделенной памяти:

// 11-bit filters
#define FDCAN_11B_FILTER_EL_CNT       0UL
#define FDCAN_11B_FILTER_EL_SIZE      4UL
#define FDCAN_11B_FILTER_EL_W_SIZE    (FDCAN_11B_FILTER_EL_SIZE / 4)
#define FCCAN_11B_FILTER_START_ADDR   (FDCAN_MEM_START_ADDR)
#define FDCAN_11B_FILTER_OFFSET       0UL

// 29-bit filters
#define FDCAN_29B_FILTER_EL_CNT       4UL
#define FDCAN_29B_FILTER_EL_SIZE      8UL 
#define FDCAN_29B_FILTER_EL_W_SIZE    (FDCAN_29B_FILTER_EL_SIZE / 4) 
#define FCCAN_29B_FILTER_START_ADDR   (FCCAN_11B_FILTER_START_ADDR + FDCAN_11B_FILTER_EL_CNT * FDCAN_11B_FILTER_EL_SIZE)
#define FDCAN_29B_FILTER_OFFSET       (FDCAN_11B_FILTER_OFFSET     + FDCAN_11B_FILTER_EL_CNT * FDCAN_11B_FILTER_EL_W_SIZE)

FDCAN1->SIDFC = (FDCAN_11B_FILTER_EL_CNT << FDCAN_SIDFC_LSS_Pos); // standart filter count
FDCAN1->XIDFC = (FDCAN_29B_FILTER_EL_CNT << FDCAN_XIDFC_LSE_Pos); // extended filter count
	
FDCAN1->SIDFC |= (FDCAN_11B_FILTER_OFFSET << FDCAN_SIDFC_FLSSA_Pos); // standard filter start address
FDCAN1->XIDFC |= (FDCAN_29B_FILTER_OFFSET << FDCAN_XIDFC_FLESA_Pos); // extended filter start address	

12) Далее настраиваем сами фильтры, о чём подробнее:

Структура стандартного фильтра (размер = 32 бита):

Структура элемента стандартного фильтра
Структура элемента стандартного фильтра

SFT

00: Диапазон значений идентификаторов от SFID1 до SFID2

01: Идентификатор SFID1 + идентификатор SFID2

10: Классический фильтр: SFID1 = фильтр, SFID2 = маска

11: Фильтр выключен.

SFEC

000: Фильтр выключен.

001: Сохранять валидные пакеты в FIFO 0

010: Сохранять валидные пакеты в FIFO 1

011: Отклонять пакеты валидные пакеты

100: Сделать приоритетным валидный пакет

101: Сделать приоритетным валидный пакет и поместить в буфер FIFO 0

110: Сделать приоритетным валидный пакет и поместить в буфер FIFO 1

111: Сохранить в Rx буфер или как отладочное сообщение, конфигурация FDCAN_SFT[1:0] игнорируется.

SFID1

Стандартный идентификатор 1

SFID2

Стандартный идентификатор 2 / маска

Расширенные фильтры занимают уже два слова в выделенной памяти. Структура расширенного фильтра (размер = 64 бита):

Структура элемента расширенного фильтра
Структура элемента расширенного фильтра

EFEC

000: Фильтр выключен.

001: Сохранять валидные пакеты в FIFO 0

010: Сохранять валидные пакеты в FIFO 1

011: Отклонять пакеты валидные пакеты

100: Сделать приоритетным валидный пакет

101: Сделать приоритетным валидный пакет и поместить в буфер FIFO 0

110: Сделать приоритетным валидный пакет и поместить в буфер FIFO 1

111: Сохранить в Rx буфер, конфигурация FDCAN_SFT[1:0] игнорируется.

EFTI

00: Диапазон значений идентификаторов от EFID1 до EFID2

01: Идентификатор EFID1 + идентификатор EFID2

10: Классический фильтр: EFID1 = фильтр, EFID2 = маска

11: Диапазон значений идентификаторов от EFID1 до EFID2. FDCAN_XIDAM маска не применяется.

EFID1

Расширенный идентификатор 1

EFID2

Расширенный идентификатор 2

Для обработки расширенных кадров в регистре XIDAM можно задать глобальную маску, которая после перезагрузки МК имеет значение 0x1FFF FFFF, т.е. заставляет fdcan модуль проверять все биты идентификаторов. Если выставить этот регистр в ноль, то модуль будет принимать абсолютно все пакеты. В битовом поле EFTI расширенных фильтров можно отключить использование этой маски.

Настраиваем сами фильтры:

#define CAN_TEST_ID_0 0x01
#define CAN_TEST_ID_1 0x02

// FILTER TYPE
#define FILTER_TYPE_RANGE       0UL
#define FILTER_TYPE_DUAL        1UL
#define FILTER_TYPE_CLASSIC     2UL
#define FILTER_TYPE_DISABLE     3UL

/* FILTER CONFIG */
#define FILTER_CFG_DISABLED     0UL
#define FILTER_CFG_STORE_FIFO_0 1UL
#define FILTER_CFG_STORE_FIFO_1 2UL
#define FILTER_CFG_REJECT       3UL

unsigned long *ptr = (unsigned long*)FCCAN_29B_FILTER_START_ADDR;

*ptr++ = (FILTER_CFG_STORE_FIFO_0 << 29) | (CAN_TEST_ID_0);
*ptr++ = (FILTER_TYPE_DUAL        << 30) | (CAN_TEST_ID_1);

// И так далее по списку фильтров.

В данном примере настроен один элемент расширенных фильтров в двойном режиме.

13) Настраиваем приёмный буфер FIFO 0. Смещение относительно начала выделенной памяти и количество элементов:

// Rx FIFO 0
#define FDCAN_RX_FIFO_0_EL_CNT        10
#define FDCAN_RX_FIFO_0_HEAD_SIZE     8UL
#define FDCAN_RX_FIFO_0_DATA_SIZE     8UL
#define FDCAN_RX_FIFO_0_EL_SIZE       (FDCAN_RX_FIFO_0_HEAD_SIZE   + FDCAN_RX_FIFO_0_DATA_SIZE)
#define FDCAN_RX_FIFO_0_EL_W_SIZE     (FDCAN_RX_FIFO_0_EL_SIZE / 4)
#define FDCAN_RX_FIFO_0_START_ADDR    (FCCAN_29B_FILTER_START_ADDR + FDCAN_29B_FILTER_EL_CNT * FDCAN_29B_FILTER_EL_SIZE)
#define FDCAN_RX_FIFO_0_OFFSET        (FDCAN_29B_FILTER_OFFSET     + FDCAN_29B_FILTER_EL_CNT * FDCAN_29B_FILTER_EL_W_SIZE)

FDCAN1->RXF0C  = FDCAN_RX_FIFO_0_OFFSET << FDCAN_RXF0C_F0SA_Pos;
FDCAN1->RXF0C |= FDCAN_RX_FIFO_0_EL_CNT << FDCAN_RXF0C_F0S_Pos;

14) Настраиваем буфер отправки сообщений. Режим буфера = FIFO. Если режим указан, как очередь, то сообщения отправляются согласно идентификаторам (больше идентификатор - быстрее отправится).

// TX buffers (FIFO)
#define FDCAN_TX_FIFO_EL_CNT          10UL
#define FDCAN_TX_FIFO_HEAD_SIZE       8UL
#define FDCAN_TX_FIFO_DATA_SIZE       8UL
#define FDCAN_TX_FIFO_EL_SIZE         (FDCAN_TX_FIFO_HEAD_SIZE + FDCAN_TX_FIFO_DATA_SIZE)
#define FDCAN_TX_FIFO_EL_W_SIZE       (FDCAN_TX_FIFO_EL_SIZE / 4)
#define FDCAN_TX_FIFO_START_ADDR      (FDCAN_TX_EVENT_START_ADDR + FDCAN_TX_EVENT_FIFO_EL_CNT * FDCAN_TX_EVENT_FIFO_EL_SIZE)
#define FDCAN_TX_FIFO_OFFSET          (FDCAN_TX_EVENT_OFFSET     + FDCAN_TX_EVENT_FIFO_EL_CNT * FDCAN_TX_EVENT_FIFO_EL_W_SIZE)

FDCAN1->TXBC &= ~(FDCAN_TXBC_TFQM); // FIFO operation
FDCAN1->TXBC |= FDCAN_TX_FIFO_EL_CNT << FDCAN_TXBC_TFQS_Pos;
FDCAN1->TXBC |= FDCAN_TX_FIFO_OFFSET << FDCAN_TXBC_TBSA_Pos;

15) Включаем прерывание модуля fdcan по приёму пакета:

FDCAN1->IE |= FDCAN_IE_RF0NE;
FDCAN1->ILE |= FDCAN_ILE_EINT0;

16) Выходим из режима инициализации и включаем прерывание в NVIC.

FDCAN1->CCCR &= ~(FDCAN_CCCR_INIT);
while ((FDCAN1->CCCR & FDCAN_CCCR_INIT) == 1) {};       
NVIC_EnableIRQ(FDCAN1_IT0_IRQn);
__enable_irq();

На этом инициализация закончена.

Чтение сообщения из выделенной памяти.

Для удобства создадим структуру нашего can пакета и структуру для удобного доступа к элементам выделенной памяти.

struct can_message
{
	unsigned char error;
	unsigned long id;
	unsigned char data[8];
	unsigned char length;
	unsigned char format;
	unsigned char type;
};

struct can_fifo_element
{
	unsigned long word0;
	unsigned long word1;
	unsigned long word2;
	unsigned long word3;
};

Функция чтение пакета (кадра) из выделенной памяти:

#define CAN_STANDARD_FORMAT     0UL
#define CAN_EXTENDED_FORMAT     1UL

#define DATA_FRAME              0UL
#define REMOTE_FRAME            1UL

struct can_message can_rx_message;
struct can_message can_tx_message;

void FDCAN1_read_msg (struct can_message* msg, unsigned char idx)
{
	struct can_fifo_element *fifo;

  // присваиваем адрес нашей структуре
	fifo = (struct can_fifo_element*)(FDCAN_RX_FIFO_0_START_ADDR + idx * FDCAN_RX_FIFO_0_EL_SIZE);

  // парсим поля
	msg->error   = (unsigned char)((fifo->word0 >> 31) & 0x01);
	msg->format  = (unsigned char)((fifo->word0 >> 30) & 0x01);
	msg->type    = (unsigned char)((fifo->word0 >> 29) & 0x01);
	
  // тип идентификатора
	if (msg->format == CAN_EXTENDED_FORMAT)
	{
		msg->id = fifo->word0 & 0x1FFFFFFF;
	}
	else
	{
		msg->id = (fifo->word0 >> 18) & 0x7FF;
	}
	
  // длина данных
	msg->length  = (unsigned char)((fifo->word1 >> 16) & 0x0F);
	
  // данные
	msg->data[0] = (unsigned char)((fifo->word2 >>  0) & 0xFF);
	msg->data[1] = (unsigned char)((fifo->word2 >>  8) & 0xFF);
	msg->data[2] = (unsigned char)((fifo->word2 >> 16) & 0xFF);
	msg->data[3] = (unsigned char)((fifo->word2 >> 24) & 0xFF);
	
	msg->data[4] = (unsigned char)((fifo->word3 >>  0) & 0xFF);
	msg->data[5] = (unsigned char)((fifo->word3 >>  8) & 0xFF);
	msg->data[6] = (unsigned char)((fifo->word3 >> 16) & 0xFF);
	msg->data[7] = (unsigned char)((fifo->word3 >> 24) & 0xFF);
}

Для чтения принятого пакета мы подставляем в функцию FDCAN1_read_msg наш can_rx_message и номер в буфере приёма.

Саму же функцию мы вызываем в прерывании. По хорошему так делать нельзя и лучше в прерывании возводить флаг нового сообщения, а потом в основном цикле (или задаче RTOS) считывать сам пакет, но для простого тестирования будем делать именно так.

Обработчик прерывания от модуля fdcan:

void FDCAN1_IT0_IRQHandler(void)
{
	unsigned char rx_fifo_get_index;
   
	// новое сообщение получено
	if((FDCAN1->IR & FDCAN_IR_RF0N) != 0)
	{
    // очищаем флаг
		FDCAN1->IR = FDCAN_IR_RF0N; 
    
    // берём индекс нового пакета в буфере
		rx_fifo_get_index = (unsigned char)((FDCAN1->RXF0S >> 8) & 0x3F);
    
    // читаем пакет
		FDCAN1_read_msg (&can_rx_message, rx_fifo_get_index);
    
    // обновляем индекс прочитанных пакетов
		FDCAN1->RXF0A = rx_fifo_get_index;
	};
	
	// сообщение потеряно в случае переполнения
	if((FDCAN1->IR & FDCAN_IR_RF0L) != 0)
	{
    // очищаем флаг
		FDCAN1->IR = FDCAN_IR_RF0L; 
	};
	
	// буфер RX FIFO переполнен
	if((FDCAN1->IR & FDCAN_IR_RF0F) != 0)
  {
    // do something
  };
}

После того, как мы прочитали новый пакет, нужно обновить индекс (указатель) подтверждения считывания пакета RXF0A.

Буфер приёма сообщений RX FIFO

Максимальная длина элемента RX FIFO при использовании нового fdcan равна 18 слов (4 байта * 18 = 72 байта). Это 2 слова на заголовок и 16 слов на данные, что даёт 64 байта данных. При использовании стандартного CAN (8 байт данных) максимальная длина элемента равна 4 слова (4 байта * 4 = 16 байт).

Структура элемента буфера приёма
Структура элемента буфера приёма

ESI (R0 bit 31)

Индикатор ошибки узла

0: Transmitting node is error active

1: Transmitting node is error passive

XTD (R0 bit 30)

0: 11-bit стандартный идентификатор.

1: 29-bit расширенный идентификатор.

RTR (R0 bit 29)

0: Получен пакет с данными.

1: Получен пакет удалённого запроса (remote frame).

ID (R0 bits 28:0)

Идентификатор. В случаем стандартного, идентификатор начинается с 18-го бита [28:18].

ANMF (R1 bit 31)

0: Получен валидный пакет прошедший фильтрацию. Один из фильтров совпал.

1: Получен пакет не прошедший фильтрацию.

FIDX (R1 bits 30:24)

Индекс фильтра пропустившего пакет.

FDF (R1 bit 21)

0: Стандартный формат пакета.

1: Новый формат (fdcan) пакета. Новая кодировка DLC и CRC.

BRS (R1 bit 20)

0: Пакет без смены битрейта фазы данных.

1: Пакет со сменой битрейта фазы данных.

DLC (R1 bits 19:16)

0-8: Классический CAN + CAN FD: пакет содержит от 0 до 8 байт.

9-15: Классический CAN: пакет содержит 8 байт.

9-15: CAN FD: полученный пакет содержит 12/16/20/24/32/48/64 байт данных.

RXTS (R1 bits 15:0)

Счётчик метки времени.

DBx

Байты данных.

Немного подробнее о кодировании поля DLC:

Кодирование поля DLC
Кодирование поля DLC

Думаю, что тут всё понятно и без объяснений.

Буфер отправки сообщений TX FIFO

Максимальная длина элемента TX FIFO при использовании нового fdcan равна 18 слов (4 байта * 18 = 72 байта). Это 2 слова на заголовок и 16 слов на данные, что даёт 64 байта данных. При использовании стандартного CAN (8 байт данных) максимальная длина элемента равна 4 слова (4 байта * 4 = 16 байт).

Структура элемента буфера отправки
Структура элемента буфера отправки

ESI (T0 bit 31)

Флаг ошибки:

0: ESI бит CAN FD зависит от ошибки (error passive flag)

1: ESI бит CAN FD передаётся рецессивным.

XTD (T0 bit 30)

0: 11-bit стандартный идентификатор.

1: 29-bit расширенный идентификатор.

RTR (T0 bit 29)

0: Пакет с данными.

1: Пакет удалённого запроса (remote frame).

ID (T0 bits 28:0)

Идентификатор. В случаем стандартного, идентификатор начинается с 18-го бита [28:18].

MM (T1 bits 31:24)

Маркер сообщения.

EFC (T1 bit 23)

0: Не сохранять событие в TX event fifo.

1: Сохранить событие в TX event fifo.

FDF (T1 bit 21)

0: Стандартный формат пакета.

1: Новый формат (fdcan) пакета. Новая кодировка DLC и CRC.

BRS (T1 bit 20)

0: Пакет без смены битрейта фазы данных.

1: Пакет со сменой битрейта фазы данных.

DLC (T1 bits 19:16)

0-8: Классический CAN + CAN FD: пакет содержит от 0 до 8 байт.

9-15: Классический CAN: пакет содержит 8 байт.

9-15: CAN FD: полученный пакет содержит 12/16/20/24/32/48/64 байт данных.

DBx

Байты данных.

Отправка сообщений

Заполняем структуру нашего пакета, конкретно в нашем случаем мы отправляем пакет обычного can (не fdcan) с 8-мю байтами данных, и скармливаем его функции FDCAN1_send_msg. Функция заполняет нужные поля элемента TX FIFO и отправляет пакет.

#define CAN_STANDARD_FORMAT     0UL
#define CAN_EXTENDED_FORMAT     1UL

#define DATA_FRAME              0UL
#define REMOTE_FRAME            1UL

// Заполняем поля пакета
can_tx_message.id      = 1;
can_tx_message.format  = CAN_EXTENDED_FORMAT;
can_tx_message.type    = DATA_FRAME;
can_tx_message.length  = 10;
can_tx_message.data[0] = 20;
can_tx_message.data[1] = 30;
can_tx_message.data[2] = 40;
can_tx_message.data[3] = 50;
can_tx_message.data[4] = 60;
can_tx_message.data[5] = 70;
can_tx_message.data[6] = 80;
can_tx_message.data[7] = 90;

void FDCAN1_send_msg (struct can_message *msg)
{
	struct can_fifo_element *fifo;
	unsigned char tx_index;
	
  // проверка буфера
	if ((FDCAN1->TXFQS & FDCAN_TXFQS_TFQF) != 0)
	{
		// буфер переполнен
	}
	
  // берём следующий свободный индекс
	tx_index = (FDCAN1->TXFQS >> 16) & 0xF;

  // присваиваем нашей fifo структуре адрес в буфере
	fifo = (struct can_fifo_element *)(FDCAN_TX_FIFO_START_ADDR + tx_index * FDCAN_TX_FIFO_EL_SIZE);
	
  //идентификатор пакета STD или EXT
	if (msg->format == CAN_STANDARD_FORMAT)
	{
		fifo->word0 = (msg->id << 18);
	}
	else
	{
		fifo->word0 = msg->id;
		fifo->word0 |= 1UL << 30; // extended flag
	}
	
  // кадр удалённого запроса
	if (msg->type == REMOTE_FRAME) fifo->word0 |= 1UL << 29;
	
  // количество байт данных
	fifo->word1 = (8UL << 16);  //Data size
  
  // данные
	fifo->word2 = (msg->data[3] << 24)|(msg->data[2] << 16)|(msg->data[1] << 8)|msg->data[0];
	fifo->word3 = (msg->data[7] << 24)|(msg->data[6] << 16)|(msg->data[5] << 8)|msg->data[4];

  // увеличиваем индекс и запускаем передачу пакета
	FDCAN1->TXBAR |= (1UL << tx_index);   
}

Заключение

В целом, по сравнению с BxCAN, ничего сложного нету. Можно разобраться и без cubemx за пару вечеров. Если у вас что-то не заработало, то в первую очередь проверьте тактирование модуля. При неправильной настройке у вас не будет срабатывать выход из режима инициализации. Также можно включить режим loopback, чтобы убедиться, что ваш код полностью рабочий, а проблема возможно дальше - на самой линии can.

Полный код на гитхабе