Etap 1 – Uruchomienie platformy

Etap 1 – Uruchomienie platformy

1. Utworzenie repozytorium i struktury katalogów

Najpierw przygotujemy czystą strukturę projektu, zanim wygenerujemy projekt STM32CubeMX.

Proponuję:

OpenDiag/
├── Firmware/
├── Hardware/
├── PC/
├── Tools/
├── Tests/
└── Docs/

Firmware/

Cały kod mikrokontrolera:

Firmware/
├── Core/
├── BSP/
├── Drivers/
├── Interfaces/
├── Middleware/
├── Protocols/
├── Services/
├── Applications/
└── ThirdParty/

Hardware/

Dokumentacja sprzętu:

Hardware/
├── Schematics/
├── PCB/
├── Datasheets/
└── Pinouts/

Na początku będzie tu głównie dokumentacja Nucleo i SN65HVD230.

PC/

Przyszłe aplikacje:

PC/
├── Linux/
└── Windows/

Na razie puste.

Tools/

Narzędzia pomocnicze, np.:

Tools/
├── CAN/
├── USB/
└── Scripts/

Tests/

Testy projektu:

Tests/
├── Firmware/
├── CAN/
├── ELM327/
└── Integration/

Docs/

Dokumentacja, którą właśnie tworzymy:

Docs/
├── Specification/
├── Architecture/
├── Protocols/
└── Tests/

I tutaj możemy umieścić naszą specyfikację:

Docs/Specification/
├── 01_Assumptions.md
├── 02_Hardware_Architecture.md
├── 03_Software_Architecture.md
├── 04_STM32_Configuration.md
├── 05_Interfaces.md
├── 06_Protocols.md
├── 07_Tests.md
└── 08_Roadmap.md

Git

Repozytorium utworzyłbym od razu:

mkdir OpenDiag
cd OpenDiag

git init

Następnie:

git branch -M main

Dodajemy .gitignore dla STM32CubeIDE:

# STM32CubeIDE
Debug/
Release/
.settings/
.metadata/
*.launch

# Build
build/

# OS
.DS_Store
Thumbs.db

# Editor
.vscode/

Uwaga: nie ignorujemy całego .project, .cproject itd., jeśli będziemy korzystać ze STM32CubeIDE. Projekt IDE powinien być zapisany w repozytorium.


Pierwszy commit

Po utworzeniu katalogów:

git add .
git commit -m "Initial project structure"

I na tym kończymy zadanie 1.

Nie tworzymy jeszcze projektu CubeMX.

Zadanie 2 będzie dopiero:

Utworzenie projektu STM32CubeMX dla Nucleo-F767ZI.

Wtedy ustalimy już konkretnie MCU, toolchain, zegary, USB, UART ST-Link, CAN1/CAN2 itd.


2. Utworzenie projektu STM32CubeMX dla Nucleo-F767ZI

Tutaj proponuję zrobić to wspólnie krok po kroku, zamiast od razu wrzucać całą konfigurację.

Na początek utworzymy projekt dla:

Board: NUCLEO-F767ZI

i ustawimy podstawowe parametry projektu:

  • STM32CubeIDE jako toolchain,
  • język C,
  • nazwa projektu OpenDiag,
  • lokalizacja projektu w naszym OpenDiag/Firmware/,
  • generator kodu.

Nie konfigurujemy jeszcze CAN, USB ani UART. Najpierw upewnijmy się, że CubeMX wygenerował czysty projekt i że możemy go skompilować.

Potem będziemy dodawać peryferia pojedynczo. To ułatwi znalezienie ewentualnego problemu.

Jeżeli masz już otwarte STM32CubeIDE, możemy zacząć od utworzenia projektu.


2.1 Utworzenie projektu

W STM32CubeIDE wybierz:

File → New → STM32 Project Powinno pojawić się okno Target Selection.

Wybór płytki

Przejdź do zakładki Board Selector.

Wyszukaj:

NUCLEO-F767ZI

NUCLEO-F767ZI – STMicroelectronics

i kliknij Next.


2.2 Nazwa projektu

W następnym oknie ustaw:

Project Name:
OpenDiag

Jeżeli IDE pyta o lokalizację, nie twórz drugiego repozytorium.

Chcemy, żeby projekt znalazł się w naszym:

OpenDiag/
└── Firmware/
    └── OpenDiag/

Czyli docelowo repozytorium może wyglądać:

OpenDiag/
├── Firmware/
│   └── OpenDiag/
├── Hardware/
├── PC/
├── Tools/
├── Tests/
└── Docs/

Ważne

Jeżeli masz już repozytorium Git utworzone w katalogu OpenDiag, nie wykonuj git init ponownie w Firmware/OpenDiag.


2.3 Ustawienia projektu

Jeżeli pojawi się wybór:

Targeted Language

wybierz:

C

Targeted Toolchain

STM32CubeIDE

Pozostałe opcje zostawmy na razie domyślne.

Kliknij Finish.

CubeIDE może zapytać:

Initialize all peripherals with their default Mode?

Na tym etapie możesz wybrać Yes.


I teraz stop. 🛑

Nie konfiguruj jeszcze żadnych peryferiów.

Po wygenerowaniu projektu sprawdźmy najpierw, czy mamy poprawną strukturę:

OpenDiag/
└── Firmware/
    └── OpenDiag/
        ├── Core/
        ├── Drivers/
        ├── ...
        ├── OpenDiag.ioc
        └── ...

oraz czy projekt buduje się bez błędów.

Daj znać, gdy projekt OpenDiag będzie utworzony. Wtedy zrobimy 2.4 – pierwszy Build bez żadnych zmian w konfiguracji.


3.1. Uruchomienie debuggera i logów przez ST-Link.

I tutaj zrobimy to w dwóch krokach:

  1. Sprawdzimy programowanie i debugowanie przez ST-Link – bez dodawania żadnego własnego kodu.
  2. Skonfigurujemy UART połączony ze ST-Linkiem i zrobimy pierwszy komunikat:
OpenDiag
Firmware starting...

na terminalu PC.

Dopiero gdy będziemy mieli działający kanał:

STM32 → UART → ST-Link → USB → PC

przejdziemy do USB CDC.

To da nam od razu kanał diagnostyczny niezależny od właściwego interfejsu OpenDiag.


3.2 – UART przez ST-Link

Tutaj najpierw musimy ustalić który UART w F767ZI jest fizycznie połączony ze ST-Linkiem.

W Nucleo-F767ZI jest to:

USART3

Połączenie na płytce Nucleo:

STM32F767ZI                 ST-Link
─────────────               ───────
USART3_TX  ───────────────► VCP_RX
USART3_RX  ◄─────────────── VCP_TX

Czyli:

STM32
 USART3
   │
   ├── TX/RX
   │
 ST-Link
   │
 USB
   │
 komputer

W CubeMX

Otwórz:

OpenDiag.ioc

Następnie:

Pinout & Configuration → Connectivity → USART3

Ustaw:

Mode:
Asynchronous

CubeMX powinien automatycznie przypisać odpowiednie piny dla połączenia z ST-Link.

Parametry na początek:

Baud Rate:     115200
Word Length:   8 Bits
Parity:        None
Stop Bits:     1
Data Direction: Receive and Transmit
Hardware Flow Control: None

Czyli klasyczne:

115200 8N1

Ważne

Na razie nie włączamy przerwań ani DMA dla USART3.

Chcemy najpierw najprostszą możliwą transmisję:

HAL_UART_Transmit()

Dzięki temu, jeżeli coś nie zadziała, będziemy mieli bardzo mało elementów do sprawdzenia.


Po konfiguracji:

Ctrl+S → Generate Code

i sprawdź, czy projekt nadal się buduje.

Na tym etapie jeszcze nie piszemy kodu wysyłającego tekst. Najpierw sprawdzimy samą konfigurację USART3.

3.3 Pierwszy komunikat OpenDiag

Dodamy minimalny kod do main.c, który po uruchomieniu STM32 wyśle przez USART3:

OpenDiag
Firmware starting...

Wykorzystamy wygenerowany przez CubeMX uchwyt:

huart3

i funkcję HAL:

HAL_UART_Transmit()

W main.c, po:

MX_USART3_UART_Init();

możemy dodać:

const char *msg = "OpenDiag\r\nFirmware starting...\r\n";
HAL_UART_Transmit(&huart3, (uint8_t *)msg, strlen(msg), HAL_MAX_DELAY);

Trzeba tylko pamiętać o:

#include <string.h>

Najlepiej jednak umieścić kod w odpowiednim bloku USER CODE, żeby regenerowanie projektu przez CubeMX go nie usunęło.

Następnie

  1. Build projektu.
  2. Flash przez ST-Link.
  3. Otwieramy terminal na komputerze.
  4. Wybieramy port szeregowy ST-Link.
  5. Ustawiamy:
    115200
    8N1
  6. Resetujemy STM32.

Powinniśmy zobaczyć:

OpenDiag
Firmware starting...

Jeżeli to zadziała, będziemy mieli potwierdzone całe połączenie STM32 → USART3 → ST-Link → USB → PC.


4. USB CDC

Teraz przechodzimy do właściwego interfejsu komunikacyjnego OpenDiag.

Chcemy uzyskać:

.            PC
              │
          USB cable
              │
        ┌─────┴─────┐
        │ STM32 USB │
        │    CDC    │
        └─────┬─────┘
              │
           Firmware

I ważne rozróżnienie:

ST-Link USB ── USART3 ──► DEBUG / LOG

USB użytkownika ── USB CDC ──► OpenDiag / FORScan

Nie będziemy mieszać tych dwóch kanałów.

Pierwszy cel USB CDC

Na tym etapie nie robimy jeszcze parsera ani ELM327.

Chcemy tylko uzyskać:

PC → USB CDC → STM32
STM32 → USB CDC → PC

czyli najprostsze echo.

Jeżeli wpiszesz:

Hello OpenDiag

STM32 powinien odesłać:

Hello OpenDiag

Dopiero po tym uznamy USB CDC za działające i przejdziemy dalej.

W CubeMX możemy teraz włączyć USB_OTG_FS → Device_Only oraz Middleware → USB_DEVICE → Communication Device Class (CDC).


4.1 Pierwsza komunikacja USB CDC

Po włączeniu CDC CubeMX wygenerował nam m.in.:

USB_DEVICE/
└── App/
    ├── usbd_cdc_if.c
    └── usbd_cdc_if.h

W usbd_cdc_if.c znajdziemy funkcję:

CDC_Receive_FS()

To jest miejsce, w którym USB CDC przekazuje firmware odebrane dane. Cel testu Chcemy uzyskać:

PC
 │
 │ USB
 ▼
STM32 USB CDC
 │
 │ echo
 ▼
PC

Czyli np.:

PC → "Hello\r\n"
STM32 → "Hello\r\n"

Jedna ważna rzecz

Nie chcę teraz wrzucać HAL_UART... ani żadnego dodatkowego kodu do main.c.

USB CDC działa asynchronicznie, więc późniejszy OpenDiag będzie oparty na:

USB RX
  ↓
bufor
  ↓
parser

Dlatego już teraz zrobimy test w miejscu, które później będzie naturalnym punktem wejścia danych.


Zanim zmienimy kod

Podłącz drugie USB Nucleo — to USB użytkownika, czyli złącze po stronie STM32, nie USB ST-Link.

Po podłączeniu sprawdź w systemie, czy pojawiło się nowe urządzenie szeregowe.

Na Linuxie:

ls /dev/ttyACM*

Powinniśmy zobaczyć coś w rodzaju:

/dev/ttyACM0
/dev/ttyACM1

Jeden będzie ST-Link VCP, drugi USB CDC.

Nie zgadujmy który jest który. Możemy to łatwo rozróżnić po identyfikacji urządzeń.

Najpierw więc:

  1. Podłącz USB ST-Link.
  2. Sprawdź ls /dev/ttyACM*.
  3. Podłącz drugie USB do USB-OTG FS na Nucleo.
  4. Ponownie wykonaj ls /dev/ttyACM*.

Podeślij mi wynik obu poleceń. Wtedy jednoznacznie ustalimy, który port jest ST-Link UART, a który OpenDiag USB CDC, zanim zaczniemy pisać echo.

Teraz zróbmy minimalny test transmisji.

W usbd_cdc_if.c mamy funkcję:

static int8_t CDC_Receive_FS(uint8_t* Buf, uint32_t *Len)

Na początek możemy zrobić najprostsze echo:

CDC_Transmit_FS(Buf, *Len);

czyli:

PC
 │
 │ "ABC"
 ▼
CDC_Receive_FS()
 │
 └──► CDC_Transmit_FS()
          │
          ▼
         PC

Ale jest jeden szczegół

Po odebraniu danych musimy ponownie uzbroić odbiór:

USBD_CDC_ReceivePacket(&hUsbDeviceFS);

Czyli testowa funkcja będzie wyglądała:

static int8_t CDC_Receive_FS(uint8_t* Buf, uint32_t *Len)
{
    CDC_Transmit_FS(Buf, *Len);

    USBD_CDC_ReceivePacket(&hUsbDeviceFS);

    return (USBD_OK);
}

To jest wyłącznie test transportu. Nie traktujemy tego jako docelowej implementacji USB.

Później CDC_Receive_FS() będzie tylko wrzucać dane do naszego bufora/kolejki:

USB
 ↓
CDC_Receive_FS()
 ↓
USB RX buffer
 ↓
Parser

a nie wykonywać całej logiki w callbacku.

Test

Po kompilacji i wgraniu firmware otwórz port USB CDC, np.:

picocom /dev/ttyACM1 -b 115200

Prędkość 115200 w przypadku USB CDC nie ma technicznego znaczenia, ale terminal jej wymaga.

Wpisz:

Hello OpenDiag

Jeżeli zobaczysz z powrotem:

Hello OpenDiag

mamy USB CDC → STM32 → USB CDC działające.

Wtedy zamkniemy Zadanie 4 i przejdziemy do CAN1/CAN2.


5. CAN1 i CAN2

Teraz dochodzimy do właściwego interfejsu samochodowego.

Najpierw skonfigurujemy oba kontrolery CAN w STM32, ale jeszcze bez podłączania samochodu.

Docelowo:

.                    STM32F767ZI
                         │
             ┌───────────┴───────────┐
             │                       │
           CAN1                    CAN2
             │                       │
        SN65HVD230             SN65HVD230
             │                       │
          CAN-H/L                  CAN-H/L
             │                       │
         HS-CAN                  MS-CAN

Na pierwszy test możemy użyć dwóch transceiverów SN65HVD230 i połączyć je ze sobą lokalnie, tworząc małą magistralę CAN. Dzięki temu sprawdzimy nadawanie i odbiór bez ryzyka ingerowania w instalację S-MAX-a.

  1. Konfiguracja CAN1 w CubeMX.
  2. Konfiguracja CAN2.
  3. Ustawienie filtrów RX.
  4. Uruchomienie CAN.
  5. Połączenie dwóch SN65HVD230.
  6. Test CAN1 → CAN2.
  7. Test CAN2 → CAN1.
  8. Sprawdzenie ramek na Linuxie / logach.
  9. Dopiero potem podłączenie do OBD S-MAX.

I tutaj warto zrobić to dokładnie, bo mamy bxCAN, a nie FDCAN jak w H723/H562. Dzięki temu późniejsza warstwa CAN API będzie musiała ukryć różnice sprzętowe.

5.1: Otwieramy OpenDiag.ioc i konfigurujemy CAN1.
5.1.1 CAN1 w CubeMX

Otwórz:

OpenDiag.ioc

Przejdź do:

Pinout & Configuration → Connectivity → CAN1

Ustaw:

Mode: Normal

CubeMX powinien automatycznie przypisać piny CAN1 dla Nucleo-F767ZI.

Parametry CAN

Na pierwszy test ustawmy:

CAN1 clock       = 54 MHz
Prescaler        = 6
BS1              = 10
BS2              = 7
SJW              = 1
TQ               = 18
Baudrate         = 500 kbit/s

To daje:

500 kbit/s

Przy zegarze CAN wynoszącym 108 MHz:

108 MHz / (18 × 12) = 500 kbit/s

Czyli:

CAN clock = 108 MHz
Prescaler = 18
TQ        = 12
           ├── Sync      = 1
           ├── Seg1      = 9? 
           └── Seg2      = 2

Tu jednak zatrzymajmy się przed wpisywaniem wartości.

W F767 konfiguracja czasu bitu zależy od tego, jaki dokładnie zegar CubeMX przydzielił do CAN1 w Twoim aktualnym projekcie. Nie chcę, żebyśmy wpisali wartości "z tabelki" i potem mieli różnicę w bitrate.

Zrób teraz tylko to:

W OpenDiag.ioc wejdź w:

CAN1 → Parameter Settings

i podaj mi, co CubeMX pokazuje przy:

Prescaler
Time Quanta / Time Seg1 / Time Seg2

oraz jaki jest:

CAN peripheral clock

5.1.2 Uruchomienie CAN1

Po wygenerowaniu kodu CubeMX będziemy mieli:

CAN_HandleTypeDef hcan1;

oraz:

MX_CAN1_Init();

Ale samo MX_CAN1_Init() nie uruchamia kontrolera CAN.

W naszym kodzie będziemy musieli wykonać:

HAL_CAN_Start(&hcan1);

a następnie skonfigurować filtr programowo.

Na pierwszy test chcemy:

przyjmować wszystkie standardowe ramki CAN.

Konfigurację zrobimy już w naszym własnym module, a nie w main.c. To będzie pierwszy rzeczywisty krok w kierunku architektury z Rozdziału 3.

Proponuję więc teraz nie dodawać jeszcze kodu do main.c, tylko wygenerować projekt po konfiguracji CAN1 i sprawdzić, czy nadal się kompiluje. Potem utworzymy:

Firmware/
└── OpenDiag/
    └── Core/
        └── can/
            ├── can.c
            └── can.h

i tam umieścimy pierwszą własną warstwę CAN.

Utwórzmy:

Firmware/OpenDiag/Core/can/
├── can.c
└── can.h

Na tym etapie moduł będzie bardzo prosty. Jego zadaniem będzie:

  1. uruchomić CAN1,
  2. skonfigurować filtr przepuszczający wszystkie standardowe ramki,
  3. uruchomić odbiór przez FIFO0,
  4. udostępnić później funkcje can_send() / can_receive().

can.h

Na początek:

#ifndef CAN_H
#define CAN_H

#include <stdint.h>

HAL_StatusTypeDef can_init(void);

#endif

Ale tutaj pojawia się ważna kwestia architektoniczna: nie chcemy, żeby can.h znał HAL. Zgodnie z Rozdziałem 5 wyższe warstwy mają być niezależne od STM32 HAL.

Dlatego zamiast HAL_StatusTypeDef zdefiniujemy własny typ wyniku.

Proponuję najpierw utworzyć:

Firmware/OpenDiag/Core/
└── common/
    ├── status.h
    └── types.h

status.h

#ifndef STATUS_H
#define STATUS_H

typedef enum
{
    STATUS_OK = 0,
    STATUS_ERROR,
    STATUS_TIMEOUT,
    STATUS_BUSY,
    STATUS_INVALID_PARAMETER

} Status_t;

#endif

I dopiero:

#ifndef CAN_H
#define CAN_H

#include "status.h"

Status_t can_init(void);

#endif

Dlaczego robimy to teraz?

Bo właśnie wchodzimy w rzeczywistą implementację architektury, którą ustaliliśmy wcześniej.

Chcemy:

Application
     │
     ▼
  can_init()
     │
     ▼
   can.c
     │
     ▼
 STM32 HAL

a nie:

Application
     │
     ▼
HAL_CAN_Start()

To będzie bardzo istotne później, kiedy będziemy obsługiwać CAN1, CAN2 i CAN3, a następnie ewentualnie przenosić kod na H723/H562.

Na razie jednak STOP

Nie pisz jeszcze kodu can.c.

Najpierw utwórzmy te dwa katalogi/pliki:

Core/
├── can/
│   ├── can.c
│   └── can.h
│
└── common/
    ├── status.h
    └── types.h

Następny krok 5.1.3 zrobimy już razem i wtedy podłączymy can.c do wygenerowanego hcan1.

I tu jedna korekta względem poprzedniego kroku: na razie nie potrzebujemy types.h. Nie twórzmy abstrakcji na zapas. status.h wystarczy.


5.1.3 can_init()

Chcemy uzyskać:

main()
  │
  ├── MX_CAN1_Init()
  │
  └── can_init()
        │
        ├── konfiguracja filtra
        ├── HAL_CAN_Start()
        └── aktywacja RX FIFO0

can.h Zostaw:

#ifndef CAN_H
#define CAN_H

#include "status.h"

Status_t can_init(void);

#endif

status.h

#ifndef STATUS_H
#define STATUS_H

typedef enum
{
    STATUS_OK = 0,
    STATUS_ERROR,
    STATUS_TIMEOUT,
    STATUS_BUSY,
    STATUS_INVALID_PARAMETER

} Status_t;

#endif

can.c Tutaj możemy już użyć HAL, bo can.c jest implementacją sterownika:

#include "can.h"
#include "main.h"

extern CAN_HandleTypeDef hcan1;

Status_t can_init(void)
{
    CAN_FilterTypeDef filter = {0};

    filter.FilterBank = 0;
    filter.FilterMode = CAN_FILTERMODE_IDMASK;
    filter.FilterScale = CAN_FILTERSCALE_32BIT;

    filter.FilterIdHigh = 0x0000;
    filter.FilterIdLow = 0x0000;

    filter.FilterMaskIdHigh = 0x0000;
    filter.FilterMaskIdLow = 0x0000;

    filter.FilterFIFOAssignment = CAN_FILTER_FIFO0;
    filter.FilterActivation = ENABLE;

    if (HAL_CAN_ConfigFilter(&hcan1, &filter) != HAL_OK)
    {
        return STATUS_ERROR;
    }

    if (HAL_CAN_Start(&hcan1) != HAL_OK)
    {
        return STATUS_ERROR;
    }

    if (HAL_CAN_ActivateNotification(
            &hcan1,
            CAN_IT_RX_FIFO0_MSG_PENDING) != HAL_OK)
    {
        return STATUS_ERROR;
    }

    return STATUS_OK;
}

Ważne: extern

Nie tworzymy drugiego:

CAN_HandleTypeDef hcan1;

CubeMX już utworzył go w main.c.

Dlatego:

extern CAN_HandleTypeDef hcan1;

oznacza:

hcan1 istnieje gdzie indziej, użyj istniejącego obiektu.


Podłączenie do main.c

Teraz w main.c dodajemy:

/* USER CODE BEGIN Includes */
#include "can.h"
/* USER CODE END Includes */

A po:

MX_CAN1_Init();

dodajemy:

if (can_init() != STATUS_OK)
{
    Error_Handler();
}

Czyli fragment będzie mniej więcej:

MX_GPIO_Init();
MX_CAN1_Init();
MX_USART3_UART_Init();
MX_USB_DEVICE_Init();

if (can_init() != STATUS_OK)
{
    Error_Handler();
}

Kolejność ma znaczenie

MX_CAN1_Init() musi wykonać się przed:

can_init();

bo can_init() korzysta z przygotowanego przez CubeMX hcan1.


Co osiągniemy?

Po uruchomieniu:

CubeMX
   │
   ▼
MX_CAN1_Init()
   │
   ▼
can_init()
   │
   ├── CAN filter
   ├── CAN Start
   └── RX interrupt

CAN1 będzie pracował z:

 500 kbit/s

i będzie przyjmował wszystkie standardowe ID.

Jeszcze niczego nie wysyłamy.

Najpierw sprawdzimy, czy firmware uruchamia się bez błędu po HAL_CAN_Start().

Jeżeli się uruchomi i nadal zobaczymy przez ST-Link:

OpenDiag
Firmware starting...

to mamy CAN1 uruchomiony.

Potem dodamy funkcję odbioru i dopiero wtedy podłączymy pierwszy SN65HVD230.


5.1.4 – odbiór pierwszej ramki CAN

Teraz potrzebujemy obsłużyć callback:

HAL_CAN_RxFifo0MsgPendingCallback()

Na razie zrobimy to bardzo prosto:

CAN1
  ↓
FIFO0
  ↓
HAL callback
  ↓
odczyt ramki
  ↓
Serial ST-Link

I wyświetlimy np.:

CAN RX: ID=123 DLC=8 DATA=11 22 33 44 55 66 77 88

Nie podłączamy jeszcze S-MAX-a. Do testu wykorzystamy dwa SN65HVD230 i lokalną magistralę CAN. W ten sposób najpierw potwierdzimy, że STM32 + transceiver + CAN rzeczywiście działają.

Następny krok zrobimy więc w dwóch częściach: 5.1.4a – fizyczne podłączenie SN65HVD230 do CAN1

i dopiero:

5.1.4b – callback odbioru CAN.


5.1.4a – SN65HVD230 + CAN1

Dla pierwszego testu mamy:

STM32F767ZI
      │
      │ CAN1 TX/RX
      ▼
SN65HVD230
      │
      ├── CANH
      └── CANL

Podłączenie

SN65HVD230:

SN65HVD230 Nucleo F767ZI
VCC 3.3 V
GND GND
TXD CAN1_RX
RXD CAN1_TX
CANH CANH
CANL CANL

Uwaga: TXD/RXD są patrzone od strony transceivera:

STM32 CAN_TX ───► RXD
STM32 CAN_RX ◄─── TXD

czyli:

STM32                 SN65HVD230
─────                 ──────────

CAN_TX ──────────────► RXD
CAN_RX ◄────────────── TXD

                     CANH ────── CANH
                     CANL ────── CANL

STB

Jeżeli korzystamy z trybu normalnego:

STB → GND

czyli transceiver nie jest w standby.


Ale jest jeszcze jedna ważna rzecz

Do testu CAN potrzebujemy drugiego węzła CAN.

Jeden SN65HVD230 podłączony do STM32 nie wystarczy do pełnego testu transmisji.

Najprościej zrobimy:

             CAN BUS
       ┌─────────────────┐
       │                 │
       ▼                 ▼
 STM32 + CAN1        Drugi CAN node
       │                 │
 SN65HVD230          SN65HVD230

Na drugim końcu magistrali potrzebujemy też terminacji:

CANH ───┬───────────────┬── CANH
        │               │
       120Ω            120Ω
        │               │
CANL ───┴───────────────┴── CANL

Jeżeli masz dwa SN65HVD230, możemy wykorzystać np. drugi jako drugi węzeł.

Jeżeli natomiast masz tylko jeden, możemy na chwilę zrobić prostszy test nadajnika, ale nie będzie to pełny test komunikacji CAN.

Najpierw więc sprawdźmy jedną rzecz:

Masz drugi SN65HVD230?

Jeżeli tak, zrobimy lokalną magistralę CAN1 ↔ drugi CAN i nie będziemy jeszcze dotykać samochodu.


5.1.4b – lokalna magistrala CAN

Mamy:

.                CAN BUS
       CANH ───────────────── CANH
         │                      │
         │                      │
    SN65HVD230             SN65HVD230
         │                      │
      CAN1 STM32             CAN2 STM32

czyli:

STM32 CAN1 ── SN65 #1 ───────── SN65 #2 ── STM32 CAN2
                 │                  │
                CANH──────────────CANH
                CANL──────────────CANL

Terminacja

Na obu końcach magistrali:

CANH ──┬── 120 Ω ──┬── CANL
       │            │
       └────────────┘

Czyli łącznie 2 × 120 Ω.

Jeżeli masz już rezystory terminujące na modułach SN65HVD230, sprawdź, czy faktycznie są aktywne. Przy dwóch węzłach powinny być dokładnie dwa terminatory.


Teraz ważna rzecz

Mamy CAN1 skonfigurowany, ale CAN2 jeszcze nie jest skonfigurowany.

Dlatego proponuję nie pisać jeszcze callbacku CAN1.

Najpierw: 5.1.5 – konfiguracja CAN2

W OpenDiag.ioc włączymy:

CAN2 → Normal

i ustawimy dokładnie ten sam bitrate:

500 kbit/s

CAN1:

54 MHz
÷ 6
÷ 18 TQ
= 500 kbit/s

CAN2 musi mieć taki sam nominal bitrate.

Następnie wygenerujemy kod i sprawdzimy Build.


Potem zrobimy test
CAN1
 │
 │ 0x123 / 8 bytes
 ▼
SN65 #1
 │
 ╞════════ CAN BUS ════════╡
 │
 ▼
SN65 #2
 │
 ▼
CAN2

i odwrotnie:

CAN2 → CAN1

Dopiero wtedy będziemy mieli rzeczywiste potwierdzenie działania kontrolerów CAN + transceiverów + magistrali.

Następny krok: konfigurujemy CAN2 w CubeMX.

Teraz zrobimy 5.5 – pierwszy rzeczywisty test CAN.

Ale zanim napiszemy obsługę CAN, proponuję zrobić to trochę inaczej niż wcześniej: najpierw przygotujemy prosty, tymczasowy test w main.c, żeby zweryfikować sam HAL i sprzęt.

Nie chcę jeszcze budować docelowego CAN API.


5.5 Pierwszy rzeczywisty test CAN.
5.5.1 CAN1 → CAN2

Na początek CAN1 będzie wysyłał jedną ramkę:

ID   = 0x123
DLC  = 8
DATA = 11 22 33 44 55 66 77 88

Schemat:

STM32 CAN1
    │
    ▼
SN65 #1
    │
    ╞══════════════════════╡
             CAN BUS
    ╞══════════════════════╡
    │
    ▼
SN65 #2
    │
    ▼
STM32 CAN2

Ale najpierw jedna rzecz

W main.c mamy już:

MX_CAN1_Init();
MX_CAN2_Init();

i nasze:

can_init();

obecnie dotyczy tylko CAN1.

Zanim zaczniemy wysyłać, musimy zdecydować, jak chcemy zorganizować kod dla dwóch kontrolerów.

I tutaj proponuję od razu zrobić to poprawnie:

can.c
can.h

nie powinny być "CAN1.c".

Warstwa powinna obsługiwać:

CAN1
CAN2
CAN3 (później)

przez jeden interfejs:

can_init(CAN_BUS_1);
can_init(CAN_BUS_2);

can_send(CAN_BUS_1, ...);
can_send(CAN_BUS_2, ...);

Dlatego proponuję teraz zatrzymać się przed pisaniem testu i poprawić can.c tak, aby od początku obsługiwał wiele magistral.

To będzie pierwszy element naszej właściwej warstwy sprzętowej CAN HAL.

Zróbmy wersję pod CAN1 + CAN2, ale już przygotowaną na ewentualny CAN3.

  1. can.h

Zmieńmy interfejs na:

#ifndef CAN_H
#define CAN_H

#include "status.h"

typedef enum
{
    CAN_BUS_1 = 0,
    CAN_BUS_2,
    CAN_BUS_3

} CanBus_t;

Status_t can_init(CanBus_t bus);

#endif

  1. can.c

Na razie obsłużmy CAN1 i CAN2:

#include "can.h"
#include "main.h"

extern CAN_HandleTypeDef hcan1;
extern CAN_HandleTypeDef hcan2;

static CAN_HandleTypeDef *can_get_handle(CanBus_t bus)
{
    switch (bus)
    {
        case CAN_BUS_1:
            return &hcan1;

        case CAN_BUS_2:
            return &hcan2;

        default:
            return NULL;
    }
}

Status_t can_init(CanBus_t bus)
{
    CAN_HandleTypeDef *hcan = can_get_handle(bus);

    if (hcan == NULL)
    {
        return STATUS_INVALID_PARAMETER;
    }

    CAN_FilterTypeDef filter = {0};

    /*
     * CAN1 i CAN2 współdzielą banki filtrów.
     * Na początek przepuszczamy wszystkie standardowe
     * i rozszerzone identyfikatory.
     */

    filter.FilterBank = (bus == CAN_BUS_1) ? 0 : 14;

    filter.FilterMode = CAN_FILTERMODE_IDMASK;
    filter.FilterScale = CAN_FILTERSCALE_32BIT;

    filter.FilterIdHigh = 0x0000;
    filter.FilterIdLow = 0x0000;

    filter.FilterMaskIdHigh = 0x0000;
    filter.FilterMaskIdLow = 0x0000;

    filter.FilterFIFOAssignment = CAN_FILTER_FIFO0;
    filter.FilterActivation = ENABLE;

    filter.SlaveStartFilterBank = 14;

    if (HAL_CAN_ConfigFilter(hcan, &filter) != HAL_OK)
    {
        return STATUS_ERROR;
    }

    if (HAL_CAN_Start(hcan) != HAL_OK)
    {
        return STATUS_ERROR;
    }

    if (HAL_CAN_ActivateNotification(
            hcan,
            CAN_IT_RX_FIFO0_MSG_PENDING) != HAL_OK)
    {
        return STATUS_ERROR;
    }

    return STATUS_OK;
}
  1. main.c Zamiast dotychczasowego:
    can_init();

    dajemy:

    
    if (can_init(CAN_BUS_1) != STATUS_OK)
    {
    Error_Handler();
    }

if (can_init(CAN_BUS_2) != STATUS_OK) { Error_Handler(); }


----------------------

#### Dlaczego `14`?

W F767 filtrami CAN zarządza wspólny blok filtrów.

Przyjmujemy:

Banki filtrów 0 ... 13 → CAN1 14 ... → CAN2

Dlatego:
```c
filter.SlaveStartFilterBank = 14;

oznacza podział:

CAN1                         CAN2
│                            │
Bank 0–13                    Bank 14–27

Na pierwszy test wystarczy nam po jednym banku:

CAN1 → bank 0
CAN2 → bank 14

Jedna uwaga

CAN_BUS_3 na razie istnieje w API, ale:

case CAN_BUS_3:
    return NULL;

Nucleo-F767ZI ma trzy kontrolery CAN, ale najpierw uruchamiamy dwa. Później dodanie trzeciego będzie rozszerzeniem tego samego sterownika, a nie zmianą architektury.


Po tej zmianie:

git diff

sprawdźmy, co dokładnie zmieniliśmy, a następnie Build.

Jeśli kompilacja przejdzie, mamy gotowe:

can_init(CAN_BUS_1)
can_init(CAN_BUS_2)

i możemy przejść do 5.5 – pierwszy test CAN1 → CAN2.


5.6 – pierwszy test CAN1 → CAN2

Potrzebujemy dwóch rzeczy:

  1. CAN1 nadaje ramkę 0x123.
  2. CAN2 odbiera ją i wypisuje przez USART3.

Testowa ramka:

ID   = 0x123
DLC  = 8
DATA = 11 22 33 44 55 66 77 88

Schemat:

STM32 CAN1
    │
SN65HVD230 #1
    │
    ├──────── CANH ────────┐
    └──────── CANL ────────┤
                           │
                    SN65HVD230 #2
                           │
                       STM32 CAN2

Chcę zrobić ten test dwukierunkowo na poziomie sprzętu, ale na początek tylko:

CAN1 TX → CAN2 RX

Do odbioru wykorzystamy FIFO0 i callback HAL.

W kolejnym kroku dodamy do naszego can.c dwie rzeczy:

can_send(...)

oraz

can_receive(...)

ale nie będziemy jeszcze budować pełnego parsera ani kolejki aplikacyjnej.

Najpierw sprawdzimy podstawową funkcję:

can_send(CAN_BUS_1, ...)
              ↓
          CAN1 TX
              ↓
          CAN BUS
              ↓
          CAN2 RX
              ↓
       HAL callback
              ↓
          USART3
              ↓
         ST-Link VCP

To będzie pierwszy test, który rzeczywiście potwierdzi:

STM32 → bxCAN → SN65HVD230 → magistrala → SN65HVD230 → bxCAN.

Następny krok: dodajemy can_send() i odbiór CAN2.


Tak. Zróbmy teraz minimalny test sprzętowy CAN1 → CAN2, bez rozbudowywania jeszcze API. Dzięki temu jeśli coś nie zadziała, łatwo ustalimy gdzie jest problem.

5.6.1 Dodajemy can_send()

Najpierw rozszerz can.h:

typedef struct
{
    uint32_t id;
    uint8_t  dlc;
    uint8_t  data[8];

} CanFrame_t;

Status_t can_send(CanBus_t bus, const CanFrame_t *frame);

W can.c:

Status_t can_send(CanBus_t bus, const CanFrame_t *frame)
{
    CAN_HandleTypeDef *hcan = can_get_handle(bus);

    if (hcan == NULL || frame == NULL || frame->dlc > 8)
    {
        return STATUS_INVALID_PARAMETER;
    }

    CAN_TxHeaderTypeDef tx_header = {0};
    uint32_t tx_mailbox;

    tx_header.StdId = frame->id;
    tx_header.ExtId = 0;
    tx_header.IDE = CAN_ID_STD;
    tx_header.RTR = CAN_RTR_DATA;
    tx_header.DLC = frame->dlc;
    tx_header.TransmitGlobalTime = DISABLE;

    if (HAL_CAN_AddTxMessage(
            hcan,
            &tx_header,
            frame->data,
            &tx_mailbox) != HAL_OK)
    {
        return STATUS_ERROR;
    }

    return STATUS_OK;
}

5.6.2 Odbiór CAN2

Teraz dodajmy funkcję pomocniczą w can.c:

static void can_process_rx(CAN_HandleTypeDef *hcan)
{
    CAN_RxHeaderTypeDef rx_header;
    uint8_t data[8];

    if (HAL_CAN_GetRxMessage(
            hcan,
            CAN_RX_FIFO0,
            &rx_header,
            data) != HAL_OK)
    {
        return;
    }

    printf("CAN RX ID=0x%03lX DLC=%lu\r\n",
           rx_header.StdId,
           rx_header.DLC);

    printf("DATA:");

    for (uint8_t i = 0; i < rx_header.DLC; i++)
    {
        printf(" %02X", data[i]);
    }

    printf("\r\n");
}

Callback HAL

W can.c:

void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
    if (hcan == &hcan2)
    {
        can_process_rx(hcan);
    }
}

Uwaga: printf() musi być przekierowane na USART3, jeśli nie masz już tego zrobionego. Jeśli nie masz, zamiast printf() na chwilę możemy użyć naszej wcześniejszej funkcji HAL_UART_Transmit().

Nie chcę teraz wprowadzać dodatkowej konfiguracji printf, więc jeśli jeszcze nie masz retargetingu, powiedz — zrobimy prosty logger przez USART3.

5.6.2 – prosty logger przez USART3 (bez retargetingu)

W main.c dodajmy funkcję:

static void debug_print(const char *text)
{
    HAL_UART_Transmit(
        &huart3,
        (uint8_t *)text,
        strlen(text),
        HAL_MAX_DELAY
    );
}

Potrzebujesz:

#include <string.h>

w USER CODE BEGIN Includes.


Odbiór CAN

Na razie również nie wrzucajmy printf() do can.c.

Dodaj w main.c:

void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
    CAN_RxHeaderTypeDef rx_header;
    uint8_t data[8];

    if (hcan != &hcan2)
    {
        return;
    }

    if (HAL_CAN_GetRxMessage(
            hcan,
            CAN_RX_FIFO0,
            &rx_header,
            data) != HAL_OK)
    {
        debug_print("CAN RX ERROR\r\n");
        return;
    }

    char msg[128];

    int len = snprintf(
        msg,
        sizeof(msg),
        "CAN RX ID=0x%03lX DLC=%lu\r\n",
        rx_header.StdId,
        rx_header.DLC
    );

    HAL_UART_Transmit(
        &huart3,
        (uint8_t *)msg,
        len,
        HAL_MAX_DELAY
    );

    debug_print("DATA:");

    for (uint8_t i = 0; i < rx_header.DLC; i++)
    {
        char byte[4];

        int len = snprintf(
            byte,
            sizeof(byte),
            " %02X",
            data[i]
        );

        HAL_UART_Transmit(
            &huart3,
            (uint8_t *)byte,
            len,
            HAL_MAX_DELAY
        );
    }

    debug_print("\r\n");
}

Potrzebujemy jeszcze:

#include <stdio.h>
#include <string.h>

5.6.3 Nadajemy ramkę

W main.c, po inicjalizacji CAN:

if (can_init(CAN_BUS_1) != STATUS_OK)
{
    Error_Handler();
}

if (can_init(CAN_BUS_2) != STATUS_OK)
{
    Error_Handler();
}

dodaj tymczasowo:

CanFrame_t test_frame =
{
    .id = 0x123,
    .dlc = 8,
    .data = {
        0x11, 0x22, 0x33, 0x44,
        0x55, 0x66, 0x77, 0x88
    }
};

A w while():

if (can_send(CAN_BUS_1, &test_frame) != STATUS_OK)
{
    debug_print("CAN TX ERROR\r\n");
}

HAL_Delay(1000);

1. W can.c dodajmy funkcję obsługującą odebraną ramkę

Załóżmy, że mamy już:

typedef struct
{
    uint32_t id;
    uint8_t dlc;
    uint8_t data[8];
} CanFrame_t;

Dodajmy prostą funkcję:

void can_rx_callback(CanBus_t bus, const CanFrame_t *frame)
{
    // Na razie tylko test.
    // Później tutaj trafi kolejka RX.
}

Ale nie chcę jeszcze dodawać pustej funkcji, bo możemy od razu zrobić sensowny fundament.

2. Callback HAL

W main.c:

void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
    CAN_RxHeaderTypeDef rx_header;
    uint8_t data[8];

    CanBus_t bus;

    if (hcan == &hcan1)
    {
        bus = CAN_BUS_1;
    }
    else if (hcan == &hcan2)
    {
        bus = CAN_BUS_2;
    }
    else
    {
        return;
    }

    if (HAL_CAN_GetRxMessage(
            hcan,
            CAN_RX_FIFO0,
            &rx_header,
            data) != HAL_OK)
    {
        return;
    }

    CanFrame_t frame = {0};

    frame.id = rx_header.StdId;
    frame.dlc = rx_header.DLC;

    memcpy(frame.data, data, frame.dlc);

    can_rx_callback(bus, &frame);
}

I potrzebujemy:

#include <string.h>
3. can_rx_callback() na razie tylko testowo

W can.c:


void can_rx_callback(CanBus_t bus, const CanFrame_t *frame)
{
    (void)bus;
    (void)frame;
}

To może wyglądać jak nic, ale to jest celowe.

Nie chcemy jeszcze:


debug_print()

w przerwaniu.


4. Ale chcę zrobić jeszcze jeden krok inaczej

Zamiast:

HAL callback
    ↓
can_rx_callback()

docelowo chcemy:

HAL callback
    ↓
can.c
    ↓
RX queue
    ↓
while()
    ↓
parser

Dlatego na tym etapie nie róbmy jeszcze can_rx_callback() jako API aplikacyjnego.

Lepiej przygotować od razu:

CanFrame_t can_rx_frame;
volatile bool can_rx_ready;

dla każdego CAN.

Ale zanim wybierzemy między pojedynczym buforem a ring bufferem, chciałbym zachować zgodność z naszym wcześniejszym planem architektury.

Proponuję więc następny krok:

can.c
can.h

CanFrame_t
CanBus_t

can_init()
can_send()
can_rx_process()

a callback HAL będzie tylko adapterem:

HAL callback
     ↓
can_rx_process()

I dopiero while() będzie mógł bezpiecznie robić debug_print().

Jeżeli chcesz, w następnym kroku możemy od razu przygotować konkretny can.h + can.c po tej refaktoryzacji, żebyśmy nie budowali tego kawałkami.

Poniżej kompletne wersje obu plików. can.h

#ifndef INC_CAN_H_
#define INC_CAN_H_

#include "main.h"
#include <stdint.h>
#include <stdbool.h>

typedef enum
{
    CAN_BUS_1 = 0,
    CAN_BUS_2

} CanBus_t;

typedef enum
{
    STATUS_OK = 0,
    STATUS_ERROR,
    STATUS_INVALID_PARAMETER

} Status_t;

typedef struct
{
    uint32_t id;
    uint8_t  dlc;
    uint8_t  data[8];

} CanFrame_t;

/* Inicjalizacja CAN */
Status_t can_init(CanBus_t bus);

/* Nadawanie ramki */
Status_t can_send(CanBus_t bus, const CanFrame_t *frame);

/* Sprawdzenie, czy odebrano nową ramkę */
bool can_rx_available(CanBus_t bus);

/* Pobranie ostatniej odebranej ramki */
Status_t can_receive(CanBus_t bus, CanFrame_t *frame);

/* Callback używany przez HAL CAN */
void can_rx_callback(CanBus_t bus,
                     const CanFrame_t *frame);

#endif /* INC_CAN_H_ */

can.c

#include "can.h"
#include <string.h>

/*
 * Uchwyty CAN wygenerowane przez CubeMX
 */
extern CAN_HandleTypeDef hcan1;
extern CAN_HandleTypeDef hcan2;

/*
 * Bufory RX
 *
 * Na tym etapie przechowujemy tylko
 * ostatnią odebraną ramkę.
 */
static CanFrame_t rx_frame_can1;
static CanFrame_t rx_frame_can2;

static volatile bool rx_ready_can1 = false;
static volatile bool rx_ready_can2 = false;

/*
 * Zwraca uchwyt HAL dla danego CAN.
 */
static CAN_HandleTypeDef *can_get_handle(CanBus_t bus)
{
    switch (bus)
    {
        case CAN_BUS_1:
            return &hcan1;

        case CAN_BUS_2:
            return &hcan2;

        default:
            return NULL;
    }
}

/*
 * Inicjalizacja CAN.
 */
Status_t can_init(CanBus_t bus)
{
    CAN_HandleTypeDef *hcan = can_get_handle(bus);

    if (hcan == NULL)
    {
        return STATUS_INVALID_PARAMETER;
    }

    CAN_FilterTypeDef filter = {0};

    /*
     * CAN1 i CAN2 współdzielą banki filtrów.
     *
     * CAN1:
     *   banki 0..13
     *
     * CAN2:
     *   banki 14..27
     */

    filter.FilterBank =
        (bus == CAN_BUS_1) ? 0 : 14;

    filter.SlaveStartFilterBank = 14;

    /*
     * Przepuszczamy wszystkie ID.
     */

    filter.FilterMode  = CAN_FILTERMODE_IDMASK;
    filter.FilterScale = CAN_FILTERSCALE_32BIT;

    filter.FilterIdHigh = 0x0000;
    filter.FilterIdLow  = 0x0000;

    filter.FilterMaskIdHigh = 0x0000;
    filter.FilterMaskIdLow  = 0x0000;

    filter.FilterFIFOAssignment = CAN_FILTER_FIFO0;
    filter.FilterActivation     = ENABLE;

    if (HAL_CAN_ConfigFilter(hcan, &filter) != HAL_OK)
    {
        return STATUS_ERROR;
    }

    /*
     * Uruchomienie CAN.
     */

    if (HAL_CAN_Start(hcan) != HAL_OK)
    {
        return STATUS_ERROR;
    }

    /*
     * Włączamy przerwanie:
     *
     * FIFO0 -> nowa ramka
     */

    if (HAL_CAN_ActivateNotification(
            hcan,
            CAN_IT_RX_FIFO0_MSG_PENDING) != HAL_OK)
    {
        return STATUS_ERROR;
    }

    return STATUS_OK;
}

/*
 * Nadawanie ramki CAN.
 */
Status_t can_send(
    CanBus_t bus,
    const CanFrame_t *frame)
{
    CAN_HandleTypeDef *hcan =
        can_get_handle(bus);

    if (hcan == NULL ||
        frame == NULL ||
        frame->dlc > 8)
    {
        return STATUS_INVALID_PARAMETER;
    }

    CAN_TxHeaderTypeDef tx_header = {0};

    uint32_t tx_mailbox;

    tx_header.StdId =
        frame->id;

    tx_header.ExtId = 0;

    tx_header.IDE =
        CAN_ID_STD;

    tx_header.RTR =
        CAN_RTR_DATA;

    tx_header.DLC =
        frame->dlc;

    tx_header.TransmitGlobalTime =
        DISABLE;

    if (HAL_CAN_AddTxMessage(
            hcan,
            &tx_header,
            frame->data,
            &tx_mailbox) != HAL_OK)
    {
        return STATUS_ERROR;
    }

    return STATUS_OK;
}

/*
 * Callback RX.
 *
 * Wywoływany przez HAL po odebraniu
 * ramki w FIFO0.
 *
 * UWAGA:
 * Nie wykonujemy tutaj żadnego UART.
 */
void HAL_CAN_RxFifo0MsgPendingCallback(
    CAN_HandleTypeDef *hcan)
{
    CAN_RxHeaderTypeDef rx_header;

    uint8_t data[8];

    CanBus_t bus;

    /*
     * Określenie, który CAN wygenerował IRQ.
     */

    if (hcan == &hcan1)
    {
        bus = CAN_BUS_1;
    }
    else if (hcan == &hcan2)
    {
        bus = CAN_BUS_2;
    }
    else
    {
        return;
    }

    /*
     * Odczyt FIFO.
     */

    if (HAL_CAN_GetRxMessage(
            hcan,
            CAN_RX_FIFO0,
            &rx_header,
            data) != HAL_OK)
    {
        return;
    }

    /*
     * Utworzenie naszej abstrakcyjnej
     * ramki CAN.
     */

    CanFrame_t frame = {0};

    frame.id  = rx_header.StdId;
    frame.dlc = rx_header.DLC;

    memcpy(
        frame.data,
        data,
        frame.dlc
    );

    /*
     * Przekazanie do wspólnego callbacka.
     */

    can_rx_callback(
        bus,
        &frame
    );
}

/*
 * Wspólna obsługa odebranej ramki.
 *
 * Na razie przechowujemy tylko
 * ostatnią odebraną ramkę.
 */
void can_rx_callback(
    CanBus_t bus,
    const CanFrame_t *frame)
{
    if (frame == NULL)
    {
        return;
    }

    if (bus == CAN_BUS_1)
    {
        rx_frame_can1 = *frame;

        rx_ready_can1 = true;
    }
    else if (bus == CAN_BUS_2)
    {
        rx_frame_can2 = *frame;

        rx_ready_can2 = true;
    }
}

/*
 * Czy CAN ma odebraną ramkę?
 */
bool can_rx_available(CanBus_t bus)
{
    switch (bus)
    {
        case CAN_BUS_1:
            return rx_ready_can1;

        case CAN_BUS_2:
            return rx_ready_can2;

        default:
            return false;
    }
}

/*
 * Pobranie ostatniej odebranej ramki.
 */
Status_t can_receive(
    CanBus_t bus,
    CanFrame_t *frame)
{
    if (frame == NULL)
    {
        return STATUS_INVALID_PARAMETER;
    }

    if (!can_rx_available(bus))
    {
        return STATUS_ERROR;
    }

    if (bus == CAN_BUS_1)
    {
        *frame = rx_frame_can1;

        rx_ready_can1 = false;
    }
    else if (bus == CAN_BUS_2)
    {
        *frame = rx_frame_can2;

        rx_ready_can2 = false;
    }
    else
    {
        return STATUS_INVALID_PARAMETER;
    }

    return STATUS_OK;
}

Ważna zmiana

Teraz HAL_CAN_RxFifo0MsgPendingCallback() nie robi już absolutnie żadnego debug_print() ani UART:

CAN IRQ
   ↓
HAL_CAN_GetRxMessage()
   ↓
CanFrame_t
   ↓
can_rx_callback()
   ↓
bufor CAN1/CAN2
   ↓
return z IRQ

A w while( ) możemy zrobić:

CanFrame_t frame;

if (can_rx_available(CAN_BUS_2))
{
    if (can_receive(CAN_BUS_2, &frame) == STATUS_OK)
    {
        debug_print("CAN2 RX\r\n");
        // tutaj wypisanie ID/DLC/DATA
    }
}

To jest już znacznie bezpieczniejsze.

Jedna uwaga

Ten wariant przechowuje tylko ostatnią ramkę. Jeśli w czasie, gdy while( ) jej nie odbierze, przyjdą trzy ramki:

CAN RX:  0x100
CAN RX:  0x101
CAN RX:  0x102

to bufor może ostatecznie zawierać tylko 0x102.

Na tym etapie jest to celowe. Najpierw sprawdźmy, czy ta architektura działa. Potem w następnym kroku zamienimy ten pojedynczy bufor na ring buffer, co będzie już odpowiednie dla rzeczywistego ruchu CAN w S-Maxie.

I dopiero na takim fundamencie będziemy budować ISO-TP / ELM327 / ForScan.