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:
- Sprawdzimy programowanie i debugowanie przez ST-Link – bez dodawania żadnego własnego kodu.
- 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
- Build projektu.
- Flash przez ST-Link.
- Otwieramy terminal na komputerze.
- Wybieramy port szeregowy ST-Link.
- Ustawiamy:
115200 8N1 - 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:
- Podłącz USB ST-Link.
- Sprawdź
ls /dev/ttyACM*. - Podłącz drugie USB do USB-OTG FS na Nucleo.
- 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.
- Konfiguracja CAN1 w CubeMX.
- Konfiguracja CAN2.
- Ustawienie filtrów RX.
- Uruchomienie CAN.
- Połączenie dwóch SN65HVD230.
- Test
CAN1 → CAN2. - Test
CAN2 → CAN1. - Sprawdzenie ramek na Linuxie / logach.
- 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:
- uruchomić CAN1,
- skonfigurować filtr przepuszczający wszystkie standardowe ramki,
- uruchomić odbiór przez FIFO0,
- 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:
hcan1istnieje 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.
- 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
- 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;
}
- 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:
- CAN1 nadaje ramkę
0x123. - 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.