on 2024-05-21 08:00 AM
This article provides a step-by-step guide on how to use a FDCAN peripheral to establish communication between two NUCLEO-G0B1RE boards. Also, outlining the process from configuring the hardware connections to developing and testing the firmware, leading to successful data transmission between the boards.
The "Flexible Data-Rate Controller Area Network" (FDCAN) peripheral is a powerful communication interface that enables high-speed data exchange between two or more devices. It is a newer version of the classic "Controller Area Network" (CAN) protocol, providing improved data rates, higher bandwidth, and enhanced error detection capabilities. The peripheral is compatible with the CAN-FD protocol according to ISO 11898-2 and it is widely used in various industries, including automotive, industrial automation, and aerospace.
In this article, we explore how to use the FDCAN peripheral to communicate between two STM32 Nucleo boards. There are many different filter configurations available in the CAN protocol. This article covers the basic communication without any fancy filters and relying on a simple CAN 2.0 communication.
For this tutorial, we use two NUCLEO-G0B1RE boards and two HW-021 modules, both with TJA1050 CAN transceivers. The Nucleo board is equipped with the STM32G0B1RET6U microcontroller, and the steps shown here can be easily adapted to any other STM32 that has CAN or FDCAN peripheral. For firmware development, the STM32CubeIDE software is used.
To set up the hardware for communication, we need a transceiver that can convert the digital signals from the G0 MCU into analog signals that can be transmitted over the physical CAN bus. The transceiver also provides the impedance that defines the CAN_High bus and the CAN_Low bus. It also provides protection against voltage spikes and other electrical disturbances that can damage the microcontroller. Below is an image to clarify the connections between the Nucleo board and the transceiver's board:
The power should be connected via the 5 V available in the CN6 in the Nucleo board, as the TJA1050 requires a VCC = 5 V. Connect the FDCAN Tx and Rx pins from each board to the respective pins in the TJA1050 transceiver at connector P1. For this particular demo, the FDCAN1 TX/RX pins are the PC5 and PC4 pins, available in the connector CN9. Refer to the image below for clarification on the connections between the Nucleo and the transceiver's board:
A small note: The label marks on NUCLEO board indicates that these pins are related to the USART peripheral, but the FDCAN is also available in these pins.
This connection should be replicated for the other Nucleo and transceiver set. Do not share the ground between the two sets. The connection between the transceivers should be made using the CAN_High and the CAN_Low pins, available in the connector P2. This establishes a physical connection between the two as we can see in the following image. By following these steps, we can set up the hardware for reliable communication.
Let us start creating a project for the STM32G0B1RE in the STM32CubeIDE and set the following pinout configurations within the *.ioc file:
In the RCCs tab, enable the HSE in [BYPASS Clock Source]. This allows the application to use the onboard STLINK's 8MHz clock and change the clock settings in the [Clock Configuration] tab to make the HSE as the entry point for the PLL and the overall SYSCLK to 64 MHz
For this example, we transmit data on the FDCAN bus by polling and receive it through interrupts. While the peripheral can use up to 2 FIFOs to handle data reception, we just need the FIFO0. In the [Parameter Settings] tab, navigate to [NVIC Settings] and enable the option FDCAN1_IT0.
After completing all these steps, your microcontroller configuration should look like this:
Now, you can save and click the [Code generation] button. In the main.c file, we must include the stdio.h library to the project and add some global variables. Use the USER CODE sections to locate where you should insert the code snippets in your application:
/* USER CODE BEGIN Includes */
#include <stdio.h>
/* USER CODE END Includes */
/* USER CODE BEGIN PD */
#define KEY_PRESSED GPIO_PIN_RESET
#define KEY_NOT_PRESSED GPIO_PIN_SET
/* USER CODE END PD */
/* USER CODE BEGIN PV */
uint8_t ubKeyNumber = 0x0;
uint8_t ubKeyNumberValue = 0x0;
uint8_t ubLedBlinkTime = 0x0;
FDCAN_RxHeaderTypeDef RxHeader;
uint8_t RxData[8];
FDCAN_TxHeaderTypeDef TxHeader;
uint8_t TxData[8];
/* USER CODE END PV */
The variable ubKeyNumber tracks the current key number, which increments and is transmitted over the FDCAN bus each time the user button is pressed. ubKeyNumberValue sets the maximum key number and resets the current key number to zero when reached. ubLedBlinkTime sets the blink time of an LED for the LED_Display function. RxHeader and RxData receive data over FDCAN, with RxHeader containing message information and RxData is a buffer that contains the actual data. TxHeader and TxData transmit data over FDCAN, with TxHeader storing message information and TxData containing the actual data.
In this demo, the printf function is used to relay the messages via the terminal. So, we need to create the low level implementation for that and use the USART to stream the data.
/* USER CODE BEGIN 0 */
int __io_putchar(char ch)
{
HAL_UART_Transmit(&huart2, (uint8_t *)&ch, 1, 10);
return ch;
}
/* USER CODE END 0 */
Now, we need to declare two function prototypes we use for the application: FDCAN_Config and Led_Display. FDCAN_Config assists in easily configuring FDCAN filters and the peripheral operational mode. The Led_Display function helps us navigate through different blinking delays.
/* USER CODE BEGIN PFP */
static void FDCAN_Config(void);
static void LED_Display(uint8_t LedStatus);
/* USER CODE END PFP */
These functions are implemented in the USER CODE BEGIN 4 section, by the end of the main.c file.
This is a function that configures the FDCAN module for communication. The function starts by defining a variable of type FDCAN_FilterTypeDef, which is a structure that contains the configuration settings for the FDCAN filters. The function then sets the configuration settings for the filter.
In this case, the range filter is selected, the filter index is set to 0, and the FIFO0 is used to receive the data. Two IDs are provided to create the range. After configuring the filter, the function starts the FDCAN module by calling the HAL_FDCAN_Start function. It then activates the notification for new messages in RX FIFO 0 by calling the HAL_FDCAN_ActivateNotification function.
Finally, the function prepares the Tx header by setting the identifier, frame type, data length, error indicator, bit rate switch, and many other features. This header is used to configure the data transmission over the FDCAN bus.
/* USER CODE BEGIN 4 */
/**
* @brief Configures the FDCAN.
* None
* @retval None
*/
static void FDCAN_Config(void)
{
FDCAN_FilterTypeDef sFilterConfig;
/* Configure Rx filter */
sFilterConfig.IdType = FDCAN_STANDARD_ID;
sFilterConfig.FilterIndex = 0;
sFilterConfig.FilterType = FDCAN_FILTER_RANGE;
sFilterConfig.FilterConfig = FDCAN_FILTER_TO_RXFIFO0;
sFilterConfig.FilterID1 = 0x321;
sFilterConfig.FilterID2 = 0x7FF;
if (HAL_FDCAN_ConfigFilter(&hfdcan1, &sFilterConfig) != HAL_OK)
{
Error_Handler();
}
/* Start the FDCAN module */
if (HAL_FDCAN_Start(&hfdcan1) != HAL_OK)
{
Error_Handler();
}
if (HAL_FDCAN_ActivateNotification(&hfdcan1, FDCAN_IT_RX_FIFO0_NEW_MESSAGE, 0) != HAL_OK)
{
Error_Handler();
}
/* Prepare Tx Header */
TxHeader.Identifier = 0x321;
TxHeader.IdType = FDCAN_STANDARD_ID;
TxHeader.TxFrameType = FDCAN_DATA_FRAME;
TxHeader.DataLength = FDCAN_DLC_BYTES_2;
TxHeader.ErrorStateIndicator = FDCAN_ESI_PASSIVE;
TxHeader.BitRateSwitch = FDCAN_BRS_OFF;
TxHeader.FDFormat = FDCAN_CLASSIC_CAN;
TxHeader.TxEventFifoControl = FDCAN_NO_TX_EVENTS;
TxHeader.MessageMarker = 0;
}
Moving on to the LED_Display function implementation, this function is responsible for controlling the blinking of an LED based on the value of the LedStatus parameter. The function sets the blink time by assigning a value to the variable ubLedBlinkTime. However, this function requires the HAL_SYSTICK_Callback function because it is used to implement a delay and toggle the LED pin based on the value of ubLedBlinkTime.
The LED_Display function starts by declaring a static variable u16LedBlinkCounter that counts the number of system ticks that have occurred. The function then checks if the variable is greater than zero and decreases it if the conditional is true. This creates a delay based on the value of ubLedBlinkTime. If the variable value is zero, the function toggles the LED pin using the HAL_GPIO_TogglePin function and sets u16LedBlinkCounter to a value that creates a delay based on the value of ubLedBlinkTime. This creates the blinking effect of the LED. This should be placed in the USER CODE 4 section
void LED_Display(uint8_t LedStatus)
{
/* Turn OFF all LEDs */
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin,GPIO_PIN_RESET) ;
/* Blink LED every 200ms */
switch(LedStatus)
{
case (1):
printf("Blink LED every 100ms\r\n");
ubLedBlinkTime = 1;
break;
case (2):
printf("Blink LED every 200ms\r\n");
ubLedBlinkTime = 2;
break;
case (3):
printf("Blink LED every 400ms\r\n");
ubLedBlinkTime = 4;
break;
case (4):
printf("Blink LED every 800ms\r\n");
ubLedBlinkTime = 8;
break;
default:
break;
}
}
void HAL_SYSTICK_Callback(void){
static uint16_t u16LedBlinkCounter = 0;
if(u16LedBlinkCounter){
u16LedBlinkCounter--;
}
else{
u16LedBlinkCounter = 100*ubLedBlinkTime;//delay based in the received value
HAL_GPIO_TogglePin(LED_GPIO_Port,LED_Pin);
}
}
/* USER CODE END 4 */
It is important to note that for the LED to work properly, the developer must go to the stm32g0xx_it_c file and call the SysTick callback by adding HAL_SYSTICK_IRQHandler in the SysTick_Handler. This is necessary to ensure that the HAL_SYSTICK_Callback function is executed at the appropriate time during program execution. Failure to do so may result in unexpected behavior or errors in the program.
* @brief This function handles System tick timer.
*/
void SysTick_Handler(void)
{
/* USER CODE BEGIN SysTick_IRQn 0 */
/* USER CODE END SysTick_IRQn 0 */
HAL_IncTick();
/* USER CODE BEGIN SysTick_IRQn 1 */
HAL_SYSTICK_IRQHandler();
/* USER CODE END SysTick_IRQn 1 */
}
/* USER CODE BEGIN 2 */
FDCAN_Config();
ubKeyNumberValue = 0x4;
/* USER CODE END 2 */
The program then enters an infinite loop using the while(1) statement. Within this loop, the demo application creates another while loop that waits for the user button press. Once the button is pressed, the program increments the value of ubKeyNumber and then changes the blink LED frequency. The program then sets the data to be transmitted over the FDCAN bus and starts the transmission process.
If the transmission fails, the program calls the Error_Handler function. Next, the program waits for 10 milliseconds before checking if the button has been released. If the button has not been released, the program continues to wait or returns to the beginning of the loop and waits for the button to be pressed again. Refer to the code implementation below:
/* USER CODE BEGIN 3 */
while (HAL_GPIO_ReadPin(BTN_GPIO_Port, BTN_Pin) == KEY_PRESSED)
{
if (ubKeyNumber >= ubKeyNumberValue)
{
ubKeyNumber = 0x00;
}
else
{
LED_Display(++ubKeyNumber);
/* Set the data to be transmitted */
TxData[0] = ubKeyNumber;
TxData[1] = 0xAD;
/* Start the Transmission process */
if (HAL_FDCAN_AddMessageToTxFifoQ(&hfdcan1, &TxHeader, TxData) != HAL_OK)
{
/* Transmission request Error */
Error_Handler();
}
HAL_Delay(10);
while (HAL_GPIO_ReadPin(BTN_GPIO_Port, BTN_Pin) != KEY_NOT_PRESSED)
{
}
}
}
}
/* USER CODE END 3 */
Now that we have the transmission process done, we need to move to the reception handling using the HAL_FDCAN_RxFifo0Callback when a new message is received in RX FIFO 0.
The function starts by checking if the FDCAN_IT_RX_FIFO0_NEW_MESSAGE interrupt flag is set. If it is, the function retrieves the received message using the HAL_FDCAN_GetRxMessage function. If the message retrieval is successful, the function checks if the received message matches the expected identifier, ID type, and data length.
If it does, the function calls the LED_Display function to blink the LED based on the received data. The function also sets the ubKeyNumber variable to the received data and prints a message to the console indicating that the reception was successful. This ensures that both boards are synchronized and display the same LED status based on sharing the value of ubKeyNumber. Add this function as part of the USER CODE 4 section.
void HAL_FDCAN_RxFifo0Callback(FDCAN_HandleTypeDef *hfdcan, uint32_t RxFifo0ITs)
{
if((RxFifo0ITs & FDCAN_IT_RX_FIFO0_NEW_MESSAGE) != RESET)
{
/* Retrieve Rx messages from RX FIFO0 */
if (HAL_FDCAN_GetRxMessage(hfdcan, FDCAN_RX_FIFO0, &RxHeader, RxData) != HAL_OK)
{
Error_Handler();
}
/* Display LEDx */
if ((RxHeader.Identifier == 0x321) && (RxHeader.IdType == FDCAN_STANDARD_ID) && (RxHeader.DataLength == FDCAN_DLC_BYTES_2))
{
LED_Display(RxData[0]);
ubKeyNumber = RxData[0];
}
}
}
That concludes our example code. To run the application, connect the USB cable from your board's STLINK to your computer, start the debug session, and observe the application running. This process allows you to monitor the behavior of the program and identify any potential issues or errors.
Enter in a debug session or program the board and open a virtual COM port terminal on your computer. When the user presses the button on one board, the LED status changes. The new value is transmitted over the FDCAN bus to the other board, which receives the message and updates its ubKeyNumber variable to match the value received. This ensures that both boards are synchronized and display the same LED status based on sharing the value of ubKeyNumber.
To verify the success of the FDCAN communication between the two boards, we displayed messages in a virtual COM terminal as an interface. These messages indicated whether the transmission and reception of data were successful or not.
In this example, the user can press the button to alternate transmission and reception between the boards. The results of this process are shown in the images below:
This example code provides a simple set up for developers looking to implement FDCAN communication in their own projects. By following the steps outlined article, developers can successfully establish FDCAN communication between two STM32 microcontrollers and transmit/receive data with ease.
Best wishes for your developments and hope you enjoyed this material!
Here are some related links that contain the material that was used to create this article, and can be helpful in your developments.
TJA1050 High speed CAN transceiver (nxp.com)