Skip to content

ThundeRatz/STM32RF24

Repository files navigation

forthebadge forthebadge

All Contributors

πŸ“‘ STM32RF24

Para a versΓ£o em PT-BR πŸ‡§πŸ‡· desse documento, veja aqui.

πŸ“œ Index

πŸŽ‰ Intro

This repository contains a library to handle Nordic Semiconductor's radio frequency module nRF24L01, whose datasheet can be viewed here, when using the microcontrollers of the STM32 family.

This library was made to be used as a submodule in the STM32ProjectTemplate.

βž• Requirements

This library does not require any extra requirements to function, in addition to those already listed STM32ProjectTemplate requirements.

However, if you want to generate the documentation, as described in the section πŸ“ Documentation, it is necessary to install Doxygen. In Ubuntu, it is possible to install it with the following command:

sudo apt install doxygen

For other operating systems, you can see download options on the official Doxygen page.

Besides that, for formatting uncrustify is used, as described in the section 🎨 Formatting. To install it, on Ubuntu, run the following command on the terminal:

sudo apt install uncrustify

On Windows, download the .zip from SourceForge. Add the location of the executable in the PATH environment variable.

πŸ“¦ Git Submodules

As stated, this library functions as a submodule. In this way, it is easier to choose the version of the library that will be used in the project, also allowing its development in parallel.

πŸ’₯ Adding a submodule

Create a directory called lib, if it does not exist:

mkdir lib

And add the submodule by doing:

  • With HTTPS:
git submodule add --name STM32RF24 https://github.com/ThundeRatz/STM32RF24.git lib/STM32RF24
  • With SSH:
git submodule add --name STM32RF24 [email protected]:ThundeRatz/STM32RF24.git lib/STM32RF24

βœ… Initializing an existing submodule

When cloning a repository that already has submodules, it is necessary to clone the repositories of that submodule. This can be done in two ways, by cloning together with the project repository or after you have already cloned.

Example:

To clone together, run the following command, switching to the repository link of your project:

git clone --recurse-submodules [email protected]:ThundeRatz/STM32ProjectTemplate.git

To clone having already cloned the project repository, within it, you should do:

git submodule update --init

πŸ“ Repository Structure

The repository contains the following folders:

  • assets/ β†’ Images and .css files for documentation
  • docs/ β†’ Documentation files
  • inc/ β†’ Header files
  • src/ β†’ Source files

At the root of the repository, in addition to the files containing the code of conduct, contribution guidelines, README and license, there is the sources.mk file, which is responsible for making it possible for the library files to be found when compiling the code. There is also a Doxyfile to generate the documentation. Another relevant file is uncrustify.cfg which is used to format the files.

πŸ“ Documentation

This library is documented using Doxygen. To generate the documentation, run the following command at the root of your repository:

doxygen

The settings are in the file Doxyfile.

🎨 Formatting

Besides a good documentation, it is necessary that the code is always well formatted, which facilitates its understanding. For that, uncrustify was used. With uncrustify installed, to format a file that has changed, run the following command:

uncrustify -c uncrustify.cfg --replace --no-backup path_to_file/file_name

πŸ—οΈ Code Structure

The code is structured as follows:

  • nrf24l01_registers.h β†’ types and constants related to the module registers.
  • rf24_platform.c/.h β†’ lower-level types and functions that use HAL.
  • rf24.c/.h β†’ highest level types and functions for user use.
  • rf24_debug.c/.h β†’ useful functions to validate the module's operation.

πŸ”Œ Hardware Configuration

πŸ€” Getting to know the module

To configure your hardware, you must first analyze the pinout of the nRF24L01 module, as shown below:

nRF24L01 Module's Pinout

The module uses SPI (Serial Protocol Interface) to communicate with the microcontroller (to learn more about SPI, we recommend this article here from Sparkfun), so four pins are required for this communication, SCK, MISO, MOSI and CSN. The CSN is a GPIO pin, while the others are dedicated pins.

In addition, a GPIO pin connected to the CE (Chip Enable) is needed, which is used to control the module, enabling the transition between the states of the module's finite state machine.

The module also has an IRQ (Interruption Request) pin, allowing the module to function through interruptions. This pin must be connected to a pin that supports interruptions in the microcontroller if you want to use this feature, otherwise, you must connect it to the 3.3V, since the pin is active low.

πŸ”§ Configuring the microcontroller

To configure the microcontroller, the STM32CubeMX, one of the requirements of the STM32ProjectTemplate, will be used. Besides that, it will be necessary to have a project configured in Cube, if you don't have it, see the STM32ProjectTemplate README and for more details see the STM32Guide.

With the project open, go to Connectivity and then select an available SPI, as seen in the image below:

Configuring the SPI - 1

After doing this, a tab will open, where you can select the SPI mode, then select the Full-Duplex Master mode:

Configuring the SPI - 2

Than, as can be seen below in "1", some pins will be automatically set in some positions, but it is possible to move them to others if they are available. To see other available positions, hold the Ctrl button and click on the pin you want to move, if there is another pin that supports the function of the pin you want to move, the color of the alternative one will change.

A configuration screen will also appear, in which you will need to make some changes. As can be seen on page 45, item 8.1 of the datasheet, the module works with a 4-wire SPI serial interface from 0Mbps to 8Mbps and 8-bit commands. Therefore, as can be seen below in "2", the Data Size must be set to 8 bits and in "3" a value of Prescaler must be defined in order to obtain a Baud Rate of up to 8Mbps.

Configuring the SPI - 3

After that it will be necessary to configure the CSN pin, for that, click on the pin you want to use for this function and then select the GPIO_Output option, as seen in the image below (for this tutorial the CSN will be on the PC6 pin). The same should be done for the CE pin, since it is also a GPIO_Output (for this tutorial the CSN will be on the PC8 pin).

Configuring the CE

Finally, to configure the IRQ pin, click on the pin you want to use and select the GPIO_EXTIx option, where x depends on the chosen pin number. Below, the PC7 pin was used as an example, so having to choose GPIO_EXTI7:

Configuring the IRQ - 1

Since the IRQ pin is active low, it is necessary to configure it like this. For this, as seen below, go to System Core> GPIO, then in the GPIO configuration tab select your IRQ pin, than a pin configurations list will appear, in which, in GPIO Mode, select External mode must be chosen. Interrupt Mode with Falling edge trigger detection.

Configuring the IRQ - 2

After setting everything, save the project and close it. To generate the Cube files, follow the instructions in the STM32ProjectTemplate README.

πŸ“š Using the library

The library has different functions for configuring module parameters, receiving and transmitting in different ways. This section will show a basic way to initialize the module, use it as a receiver or as a transmitter.

The communication between two modules can be with or without acknowledgment (ACK). Using ACK helps to prevent the loss of packets sent. When ACK is enabled, your receiver, upon receiving a valid package, will send an ACK package to the transmitter, otherwise it will not send anything. On the other hand, the transmitter, after sending a packet, will be waiting to receive an ACK packet for a certain time, if the time runs out without receiving the ACK, it will send the packet it had sent again. Several different transaction diagrams can be seen starting on page 40, item 7.9 of the datasheet. This tutorial will show you how to communicate two modules with ACK, it will also be considered that there is only one transmitter and one receiver, but it is possible to have more modules.

In addition to what will be shown in the subsections below, for the library to function, it is necessary, in one of its .c files that includes the rf24.h file, to define the following function:

/**
 * @brief Library delay function.
 *
 * @note This function must be implemented by the user.
 *
 * @param ms Delay in milliseconds.
 *
 * @return @ref rf24_status.
 */
rf24_status_t rf24_delay(uint32_t ms);

It is a delay function used within the library, which receives a time in milliseconds. It can be defined in different ways, however, in general, it is possible to define it simply with the HAL_Delay(uint32_t Delay) function (it is necessary to include the main.h file generated by Cube for this):

rf24_status_t rf24_delay(uint32_t ms) {
    HAL_Delay(ms);

    return RF24_SUCCESS;
}

🏁 Initializing

Before starting the module itself, it is necessary to initialize the SPI that was configured in the Cube. The function name depends on which SPI was chosen, for the one chosen in the πŸ”Œ Hardware Configuration section above, it would be the following function:

MX_SPI2_Init(); /* The SPI2 was chosen in Cube */

To use the above function, it is necessary to include the file spi.h generated by Cube. In addition, it is recommended to put a delay of something around 100 ms after the SPI initialization.

Then, it is necessary to define in the code which pins and SPI instance were chosen, in addition to other configurations. For this, the pins chosen in the πŸ”Œ Hardware Configuration section will be considered and also that a 15-byte message will be sent, that is, payload size of 15.

First, you need to create a module instance and a pointer to it:

rf24_dev_t device; /* Module instance */
rf24_dev_t* p_dev = &device; /* Pointer to module instance */

Then, to configure the module, it can be done as follows:

/* Device config */

/* Get default configuration */
rf24_get_default_config(p_dev);

/* The SPI2 was chosen in Cube */
p_dev->platform_setup.hspi = &hspi2;

/* CSN on pin PC6 */
p_dev->platform_setup.csn_port = GPIOC;
p_dev->platform_setup.csn_pin = GPIO_PIN_6;

/* IRQ on pin PC7 */
p_dev->platform_setup.irq_port = GPIOC;
p_dev->platform_setup.irq_pin = GPIO_PIN_7;

/* CE on pin PC8 */
p_dev->platform_setup.ce_port = GPIOC;
p_dev->platform_setup.ce_pin = GPIO_PIN_8;

p_dev->payload_size = 15;

Finally, it is possible to initialize the module, passing the pointer of the module instance to the following function:

rf24_init(p_dev);

This function will return RF24_SUCCESS if initialization is successful and error values otherwise. For more details on the possible error values, see the code documentation.

πŸ“€ Using as a transmitter

To use a module as a transmitter it is necessary to know the address of the receiver to which the message will be sent, this information needs to be shared between the two, otherwise it is not possible to make the communication. In addition, as it will be shown here how to communicate with ACK, the transmitter will behave for a period as a receiver waiting for the ACK packet, so it is also necessary that it has a receiver address, this address must also be a information that two modules have.

For both the example of transmitter and receiver, the address vector below will be used, where the first is the address for the transmitter to receive the ACK packet and the second is the address of the receiver, to which the transmitter will send. Address sizes are configurable, but 5-byte addresses will be used.

uint8_t addresses[2][5] = {{0xE7, 0xE7, 0xE7, 0xE7, 0xE8}, {0xC2, 0xC2, 0xC2, 0xC2, 0xC1}};

For the configuration part of the transmitter it is also interesting to choose the output power of the module with the following function:

/**
 * @brief Set device output power.
 *
 * @param p_dev         Pointer to rf24 device.
 * @param output_power  Selected output power.
 *
 * @return @ref rf24_status.
 */
rf24_status_t rf24_set_output_power(rf24_dev_t* p_dev, rf24_output_power_t output_power);

Now, to receive and send according to the right addresses, it is necessary to open a writing pipe for the address addresses[1] and a reading pipe for addresses[0], which can be done as follows:

rf24_status_t device_status; /* Variable to receive the statuses returned by the functions */

device_status = rf24_open_writing_pipe(p_dev, addresses[1]);
device_status = rf24_open_reading_pipe(p_dev, 1, addresses[0]);

With that done, it's now possible to send messages! Say you want to send the following message stored in a vector:

uint8_t buffer[] = {'V', 'i', 'r', 't', 'u', 'a', 'l', ' ', 'h', 'u', 'g', 's', '!', '\r', '\n'};

To send it with ACK, it can be done as follows:

device_status = rf24_write(p_dev, buffer, 15, true);

This function will return RF24_SUCCESS if the transmitter was able to send the message and, as the communication is done with ACK, if the receiver has received the message.

πŸ“© Using as a receiver

As mentioned in transmitter's subsection, the address to which the transmitter will send the data must be the same as that registered in the receiver's code, as well as the address to which the receiver will send the ACK packet needs to be the same as the one on the transmitter, so the same addresses as the transmitter tutorial will be used:

uint8_t addresses[2][5] = {{0xE7, 0xE7, 0xE7, 0xE7, 0xE8}, {0xC2, 0xC2, 0xC2, 0xC2, 0xC1}};

In the case of the receiver, to receive and send according to the right addresses, it is necessary to open a writing pipe for the address addresses[0] and a reading pipe for addresses[1], as is done below:

rf24_status_t device_status; /* Variable to receive the statuses returned by the functions */

device_status = rf24_open_writing_pipe(p_dev, addresses[0]);
device_status = rf24_open_reading_pipe(p_dev, 1, addresses[1]);

Besides that, in order for the receiver to start receiving packets, it is necessary to call the following function:

device_status = rf24_start_listening(p_dev);

After that, it is already possible to receive packages! It is possible to check for a new package with the following function:

/**
 * @brief Checks if a new payload has arrived.
 *
 * @param p_dev         Pointer to rf24 device.
 * @param pipe_number   Pipe where the available data is.
 *
 * @note To don't ready a pipe, pass NULL as pipe_number argument.
 *
 * @return @ref rf24_status.
 */
rf24_status_t rf24_available(rf24_dev_t* p_dev, uint8_t* pipe_number);

And it is possible to read packages with the following function:

/**
 * @brief Reads the payload avaible in the receiver FIFO.
 *
 * @note Interruption flags related to the receiver are cleared.
 *
 * @param p_dev Pointer to rf24 device.
 * @param buff Pointer to a buffer where the data should be written
 * @param len Maximum number of bytes to read into the buffer
 *
 * @return @ref rf24_status.
 */
rf24_status_t rf24_read(rf24_dev_t* p_dev, uint8_t* buff, uint8_t len);

So, to check for packages in the queue and read the last package, you can do it as follows:

rf24_status_t device_status;
rf24_status_t read_status;

uint8_t buffer[15] = {0};

if ((device_status = rf24_available(p_dev, NULL)) == RF24_SUCCESS) {
    while ((device_status = rf24_available(p_dev, NULL)) == RF24_SUCCESS) {
        read_status = rf24_read(p_dev, buffer, p_dev->payload_size);
    }

    /* Do something with the read package */
}

πŸ› Debugging

To debug your code it is possible to use the functions of the file rf24_debug.c/.h, but for this it is also necessary to define a printf function. For ease of use, we recommend adding the SEGGER_RTT to the project. After adding it, having called the debugging functions in your code, to see what is being "printed" by the functions, run in the terminal, being at the root of your project:

make rtt

πŸ‘₯ Contributing

Any help in the development of robotics is welcome, we encourage you to contribute to the project! To learn how, see the contribution guidelines here.

✨ Contributors

Thanks goes to these wonderful people (emoji key):


Lucas Haug

πŸ’» πŸ“– ⚠️ 🌍

Lucas Schneider

πŸ’» πŸ“– ⚠️ 🌍 πŸ‘€

Daniel Nery

πŸ’» πŸ‘€

Felipe Gomes de Melo

πŸ€” πŸ‘€

Bernardo Coutinho

⚠️

This project follows the all-contributors specification. Contributions of any kind welcome!