Jak przesłać dane między CAN1, a CAN2 w STM32 bez użycia transceivera?
Na rozgrzewkę:
We wpisie przedstawię implementację protokołu CAN na procesorze STM32G4, uwzględniając i tłumacząc filtry programowe, pokażę jak przesłać informację pomiędzy CAN1 i CAN2 używając jednego procesora, a to wszystko bez użycia transceivera. Czasem chcemy przetestować działanie magistrali CAN lub filtrów programowych bez konieczności użycia dwóch osobnych płytek lub dodatkowych transceiverów. Taki testowy setup pozwala zweryfikować poprawność konfiguracji CAN w STM32 oraz logikę aplikacji przed wdrożeniem sprzętowym.
Przedstawienie schematu podłączenia:

Połączony układ wygląda następująco:

Działanie układu:
Konfiguracja pinów:
CAN1_Tx oraz CAN2_Tx są wyjściami typu push-pull, dlatego potrzebne są diody zabezpieczające.
CAN1_Rx oraz CAN2_Rx skonfigurowane są jako wejścia AF (Alternate Function) bez pull-up oraz bez pull-down.
Zasada działania:
CAN na warstwie fizycznej rozróżnia stan dominujący (logiczne 0) i recesywny (logiczna 1). W normalnej magistrali robi to transceiver różnicowy (CAN_H/CAN_L). Tutaj transceiver jest pominięty, za to uzyskujemy zachowanie podobne do magistrali open-drain:
– Diody separujące zabezpieczają wyjścia przed sytuacją, gdzie na jednym z CAN_Tx byłby stan niski, a na drugim stan wysoki. Taka sytuacja doprowadziłaby do uszkodzenia wyjść CAN_Tx mikrokotrolera.
– Rezystor podciągający ustala pasywnie stan recesywny, poprzez podciągnięcie wspólnej linii do 3.3V.
– Kiedy któryś CAN_Tx chce nadać bit dominujący, jego wyjście ciągnie linię do zera, przez diodę przepływa prąd do masy i wspólny punkt zostaje sprowadzony do niskiego poziomu.
Dlaczego RX odbiera poprawnie?
Peryferium CAN w STM32 oczekuje sygnału z transceivera w postaci jednokanałowego poziomu logicznego: niski – dominujący, wysoki – recesywny. W tym układzie wspólny węzeł daje taki właśnie poziom (podciągnięty = 1, ściągnięty do 0 = 0), więc kanał Rx widzi poprawne stany i dekoduje ramki poprawnie.
Analiza przebiegu:
Na poniższym przebiegu przedstawiłem na kanale 0 sygnał na katodzie diody CAN1_Tx, zaś na kanale 1 przedstawiłem sygnał na liniach Rx. Jak widać na przebiegu sygnał CAN dekodowany jest poprawnie.

Konfiguracja procesora w środowisku CubeIDE:
Na początku zacznę od przedstawienia konfiguracji procesora jakiej użyję we wpisie w środowisku CubeIDE:

Konfiguracja CAN1:

Konfiguracja przerwania CAN1:

Konfiguracja CAN2:

Włączenie przerwania CAN2:

Filtry programowe CAN:
Protokół CAN jest komunikacją rozproszeniową, czyli wszystkie urządzenie podpięte do jednej magistrali otrzymują dane. W takim razie w jaki sposób urządzenia wiedzą, że informacje są przekazywane właśnie do nich? Z pomocą przychodzą filtry programowe zaimplementowane w programie. Peryferium CAN reaguje na ramkę o określonym ID, które to ID jest zgodne z ustawionymi filtrami.
Filtry jakie można wybrać podczas konfiguracji CAN w FilterType:
- RANGE FILTER – czyli ramki z zakresu od ID1 do ID2 będą uwzględniane. Służy do testów lub prostych przypadków,
- MASK FILTER – ID1 to wzorzec ID, ID2 to maska bitowa. Jeśli jest bit 1 w masce, wtedy dany bit jest porównywany, jeśli w masce jest bit 0 wtedy bit jest ignorowany. Przepuszczane są ramki które na konkretnych maski jest 1 logiczna, a ramka przychodząca ma tą samą wartość jak wzorzec ID1. Najlepszy filtr, gdy chcemy odbierać grupy ramek o wspólnym prefiksie ID,
- DUAL FILTER – dla ramek o dwóch ID równych ID1 lub ID2. Najlepszy, gdy chcemy odebrać konkretne dwa ID bez pisania osobnych filtrów,
- RANGE FILTER NO EIDM – działa tak samo jak range filter, lecz ignoruje bit (IDE) określający czy ramka jest standardowej długości czy rozszerzonej. Ten filtr jest przydatny, gdy system pracuje z mieszanymi ramkami: standardowymi i rozszerzonymi, a użytkownik chce filtrować ramki tylko po numerze ID.
FDCAN_FilterTypeDef fdcan1filterconfig;
fdcan1filterconfig.FilterConfig = FDCAN_FILTER_TO_RXFIFO1;
fdcan1filterconfig.FilterID1 = 0x11;
fdcan1filterconfig.FilterID2 = 0x11;
fdcan1filterconfig.FilterIndex = 0;
fdcan1filterconfig.FilterType = FDCAN_FILTER_MASK;
fdcan1filterconfig.IdType = FDCAN_STANDARD_ID;
if (HAL_FDCAN_ConfigFilter(&hfdcan2, &fdcan1filterconfig) != HAL_OK)
{
Error_Handler();
}
Kod:
Callback od CAN1:
//CAN1
void HAL_FDCAN_RxFifo0Callback(FDCAN_HandleTypeDef *hfdcan, uint32_t RxFifo0ITs)
{
if(RxFifo0ITs && FDCAN_IT_RX_FIFO0_NEW_MESSAGE != RESET)
{
if(HAL_FDCAN_GetRxMessage(hfdcan, FDCAN_RX_FIFO0, &RxHeader1, RxData1)!= HAL_OK)
{
Error_Handler();
}
if (HAL_FDCAN_ActivateNotification(hfdcan, FDCAN_IT_RX_FIFO0_NEW_MESSAGE, 0))
{
Error_Handler();
}
}
}
Callback od CAN2:
//CAN2
void HAL_FDCAN_RxFifo1Callback(FDCAN_HandleTypeDef *hfdcan, uint32_t RxFifo1ITs)
{
if(RxFifo1ITs && FDCAN_IT_RX_FIFO1_NEW_MESSAGE != RESET)
{
if(HAL_FDCAN_GetRxMessage(hfdcan, FDCAN_RX_FIFO1, &RxHeader2, RxData2)!= HAL_OK)
{
Error_Handler();
}
if (HAL_FDCAN_ActivateNotification(hfdcan, FDCAN_IT_RX_FIFO1_NEW_MESSAGE, 0))
{
Error_Handler();
}
if(RxHeader2.DataLength==2)
{
datacheck=1;
}
}
}
Nagłówek ramki CAN1 oraz CAN2:
//Header CAN1
TxHeader1.Identifier = 0x11; // Standard ID (Matches Filter)
TxHeader1.IdType = FDCAN_STANDARD_ID; // 11-bit Identifier
TxHeader1.TxFrameType = FDCAN_DATA_FRAME; // Data Frame (not remote)
TxHeader1.DataLength = FDCAN_DLC_BYTES_2; // 8-byte data payload
TxHeader1.ErrorStateIndicator = FDCAN_ESI_ACTIVE; // Normal Error State
TxHeader1.BitRateSwitch = FDCAN_BRS_OFF; // No Bit Rate Switching (for CAN FD)
TxHeader1.FDFormat = FDCAN_CLASSIC_CAN;
TxHeader1.TxEventFifoControl = FDCAN_NO_TX_EVENTS;
TxHeader1.MessageMarker = 0;
//Header CAN2
TxHeader2.Identifier = 0x22; // Standard ID (Matches Filter)
TxHeader2.IdType = FDCAN_STANDARD_ID; // 11-bit Identifier
TxHeader2.TxFrameType = FDCAN_DATA_FRAME; // Data Frame (not remote)
TxHeader2.DataLength = FDCAN_DLC_BYTES_2; // 8-byte data payload
TxHeader2.ErrorStateIndicator = FDCAN_ESI_ACTIVE; // Normal Error State
TxHeader2.BitRateSwitch = FDCAN_BRS_OFF; // No Bit Rate Switching (for CAN FD)
TxHeader2.FDFormat = FDCAN_CLASSIC_CAN;
TxHeader2.TxEventFifoControl = FDCAN_NO_TX_EVENTS;
TxHeader2.MessageMarker = 0;
Główna pętla programu:
while (1)
{
if(HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13) == GPIO_PIN_SET)
{
TxData1[0]=200;
TxData1[1]=20;
if(HAL_FDCAN_AddMessageToTxFifoQ(&hfdcan1, &TxHeader1, TxData1) != HAL_OK)
{
Error_Handler();
}
HAL_Delay(1000);
}
if(datacheck==1)
{
for(int i=0;i<RxData2[1];i++)
{
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
HAL_Delay(RxData2[0]);
}
datacheck=0;
}
}
Prezentacja funkcjonowania układu:
CAN1 wysyła informacje na temat częstotliwości i ilości mrugnięć diodą do CAN2 po naciśnięciu przycisku. CAN2 odbiera dane o częstotliwości i ilości mrugnięć diodą. Dalej następują mrugnięcia diodą z określoną częstotliwością i określoną ilością razy.
Uwagi praktyczne:
Na co uważać przy testach bez transceivera?
- Linie CAN_TX i CAN_RX w STM32 są CMOS, więc nie mają odporności na zakłócenia ani zabezpieczeń ESD.
- Działa tylko w krótkim połączeniu (na tej samej płytce).
- W realnym zastosowaniu zawsze wymagany jest transceiver (np. TJA1051, MCP2551, SN65HVD230).
Podsumowanie:
We wpisie przedstawiłem prosty, testowy sposób na implementację protokołu CAN, kiedy pod ręką nie mamy transceivera oraz dysponujemy tylko jednym procesorem. W kolejnych wpisach pokażę, jak dołączyć zewnętrzny transceiver i komunikować się między dwoma różnymi mikrokontrolerami STM32 po magistrali CAN.
Ten wpis powstał jako część naszych wewnętrznych testów i eksperymentów z komunikacją CAN w STM32.
Potrzebujesz pomocy przy projektowaniu elektroniki, programowaniu mikrokontrolerów? Skontaktuj się z nami.
[1] Dostęp w Internecie: https://www.keil.com/appnotes/files/apnt_236_v2.9.pdf