This project demonstrates how to write basic firmware for the CC1101 RF transceiver with the ESP32 using the ESP-IDF framework. In this demo, we will be accessing a register inside the CC1101 (and performing a few other operations). A successful read of this register will confirm we have set up our devices to communicate properly. The relevant code can be found in main/main.cpp.
Note: We use ESP-IDF here for full control and learning purposes, but Arduino can be a simpler option for long term development.
Writing this firmware can be accomplished in five steps:
- Acquiring prerequisites (hardware, software)
- Wiring the appropriate pins from the CC1101 to the ESP32
- Initializing an SPI bus on the ESP32
- Adding the CC1101 as a device on that bus
- Transmitting and receiving data between the devices
This README will reference the ESP32 documentation and the official TI CC1101 transceiver datasheet. Basic programming experience, familiarity with the SPI interface, and development board knowledge (Raspberry Pi, Arduino, ESP32) will be helpful to follow along.
- Prerequisites
- Wiring
- Initialize an SPI Bus
- Add a Device
- Register Access in the CC1101
- Interact with the Device
- Further Reading
-
CC1101 transceiver: The CC1101 is a low cost, low power sub-1 GHz RF transceiver designed for wireless applications in the 300-348 MHz, 387-464 MHz, and 779-928 MHz ISM/SRD bands. Commonly used with microcontrollers like Arduino, ESP8266, and the Flipper Zero for sub-GHz communication.
-
ESP32: The ESP32 is a microcontroller with integrated Wi-Fi and Bluetooth, manufactured by Espressif Systems. It is widely used in IoT (Internet of Things) projects due to its powerful 32-bit dual-core processor, high performance, and versatility in smart home and wearable devices.
-
Breadboard wires: Breadboard wires (or jumper wires) are flexible or solid core wires with pins on the ends used to create temporary, solderless connections on a breadboard for prototyping circuits.
This hardware is available for purchase on many online platforms such as AliExpress and Amazon.
-
Visual Studio Code: A lightweight, extensible source code editor used to write and manage ESP32 projects.
-
ESP-IDF (Espressif IoT Development Framework): The official development framework for ESP32, providing the toolchain, build system, drivers, and APIs.
-
Espressif IDF Extension for VS Code: An extension that integrates ESP-IDF into VS Code, enabling build, flash, monitor, and project management features.
-
Python 3.x: Ensure your Python version is compatible with the latest version of ESP-IDF.
Note: The pinout can change based on the type of ESP32 and CC1101 module you own. Verify your board's pinout before wiring.
| CC1101 Pin | ESP32 Pin |
|---|---|
VCC |
3.3V |
GND |
GND |
CSn |
GPIO 5 |
MOSI |
GPIO 23 |
MISO |
GPIO 19 |
SCK |
GPIO 18 |
GDO0 |
GPIO 4 |
GDO2 |
Optional / Not Used |
Warning
Ensure VCC on the CC1101 is connected to 3.3V only. Applying 5V can damage the chip.
Once everything is wired up and the prerequisites are complete, we can begin writing firmware using ESP-IDF. If you're new to ESP-IDF, there are several videos online that can assist with setting up a project from scratch. I recommend watching this one and reading the official documentation for getting started with ESP-IDF. After your environment is ready, open main.cpp and we will begin implementing the SPI configuration.
The protocol the CC1101 uses to physically communicate with other devices is called SPI, or Serial Peripheral Interface. It is one of three main protocols that embedded devices use to transmit data; the other two are known as I2C and UART. The purpose of each wire in the SPI protocol is shown below.
| Pin | Full Name | Purpose |
|---|---|---|
MOSI |
Master Out, Slave In | Sends data to the CC1101 |
MISO |
Master In, Slave Out | Receives data from the CC1101 |
SCK |
Serial Clock | Synchronizes data transfer |
CSn |
Chip Select | Selects the CC1101 for communication |
An important concept in the SPI protocol is known as the SPI bus, where the SPI bus is essentially a group of shared wires that transfer data. These wires map to the four pins of the SPI protocol: the MOSI, MISO, SCK, and CSn pins. spi_bus_initialize tells the ESP32 to configure the SPI bus according to our specifications including which SPI controller to select and which pins we are using for this bus. We initialize the SPI bus using:
This function requires:
extern "C" void app_main(void) {
spi_bus_config_t busConfig = {};
busConfig.MOSI_io_num = GPIO_NUM_23;
busConfig.MISO_io_num = GPIO_NUM_19;
busConfig.sclk_io_num = GPIO_NUM_18;
busConfig.quadwp_io_num = -1;
busConfig.quadhd_io_num = -1;
spi_bus_initialize(SPI3_HOST, &busConfig, SPI_DMA_DISABLED);
...
}- SPI3_HOST
- The SPI controller you are selecting. There are four SPI controllers on the classic ESP32. Two are tied to internal ESP32 operations, while
SPI2_HOSTandSPI3_HOSTare available for public interfacing.
- The SPI controller you are selecting. There are four SPI controllers on the classic ESP32. Two are tied to internal ESP32 operations, while
- busConfig
- MOSI_io_num: The GPIO pin that connects the
MOSIpin. - MISO_io_num: The GPIO pin that connects the
MISOpin. - sclk_io_num: The GPIO pin that connects the SCLK pin.
- quadwp & quadhd: These pins don't exist on the CC1101, so they are set to -1 indicating the bus doesn't need to consider them.
- MOSI_io_num: The GPIO pin that connects the
- SPI_DMA_DISABLED
- Controls whether the SPI driver uses Direct Memory Access for transfers. DMA can be disabled for small and simple transfers.
An SPI bus can have multiple devices using it. All devices would share the MOSI, MISO, and SCK lines, while each device would have its own CSn line that determines which device the master (ESP32) would listen to. This method will let the ESP32 know how to interact with our CC1101 by specifying which host it belongs to, what clock speed to use, etc. We add the CC1101 using:
This function requires:
extern "C" void app_main(void) {
...
spi_device_interface_config_t deviceConfig = {};
spi_device_handle_t cc1101;
deviceConfig.command_bits = 0;
deviceConfig.address_bits = 0;
deviceConfig.dummy_bits = 0;
deviceConfig.clock_speed_hz = 1000000;
deviceConfig.spics_io_num = GPIO_NUM_5;
deviceConfig.queue_size = 1;
deviceConfig.mode = 0;
spi_bus_add_device(SPI3_HOST, &deviceConfig, &cc1101);
...
}- SPI3_HOST
- Use the same host that you specified in the bus configuration step.
- deviceConfig
- command, address, & dummy bits: The CC1101 does not have any phases specified in a transfer (see section 10: 4-wire Serial Configuration and Data Interface in the CC1101 datasheet).
- clock_speed_hz: Table 22 in the CC1101 datasheet specifies the max frequency as 6-10 MHz depending on the action. This value should be lower than that.
- spics_io_num: The GPIO pin we wired
CSnto. Setting this allows the ESP32 to automatically know when to start listening to the CC1101. Use -1 if you want to control the chip select manually. If you are to control it manually, read section 10 of the CC1101 datasheet where it specifies theCSnpin values. - queue_size: Set to 1 as our program is only using synchronous methods (such as
spi_device_polling_transmit()). - mode: The SPI mode is determined by a combination of the Clock Polarity (CPOL) and the Clock Phase (CPHA). From the diagram (figure 15 in the CC1101 datasheet), we can see the SCLK line starts and idles low. So the CPOL is zero. We can also see that the lines indicate data is sampled on the rising edge of the SCLK signal, meaning the CPHA is zero. A combination of CPOL = 0 and CPHA = 0 means the SPI mode is 0.

- cc1101
- An arbitrary name that should correspond to the device you are using. We will reference this in our transactions.
The CC1101 has three main types of addresses: configuration registers, status registers, and command strobes.
-
Configuration registers are read/write and control radio parameters like frequency, modulation, and packet behavior.
-
Status registers are read only and report internal state information such as
PARTNUM,VERSION,RSSI, andFIFOstatus. -
Command strobes are single byte instructions that immediately trigger actions inside the radio such as system reset (
SRES), enter receiver mode (SRX), or enter transmit mode (STX).
The CC1101 does not have separate phases for sending bytes (no separate command phase, address phase, etc). It starts every SPI transaction with a single header byte that follows this format:
| Bit Position | Field Name | Width | Description | Values |
|---|---|---|---|---|
| 7 | R/W | 1 bit | Determines if operation is read or write | 0 = Write1 = Read |
| 6 | Burst | 1 bit | Determines single or multi-byte access | 0 = Single access1 = Burst access |
| 5–0 | Address | 6 bits | Register address or command strobe | 0x00 – 0x3F |
- Bit position 7 tells the CC1101 if we are reading an address or writing to an address.
- Bit position 6 specifies if we are using single or multi-byte access. We will not be implementing multi-byte access in this guide.
- Bit position 5-0 is the address that we want to interact with.
One important thing to know is that there is a special interaction between status registers and command strobes: they can share the same address. This is known as 'overloading' a register. The way we differentiate between them at the same address is with the burst and R/W bits.
For example, the address 0x30 contains the command strobe SRES AND the PARTNUM status register. The difference is that when we construct our header byte, we must set the burst and R/W bits to 1 if we want to access the status register at this address and leave them as 0 if we want to send the command strobe. The table below shows how we form the different bytes for each case:
| Name | Address (Hex) | Header Byte (Hex) | Address (Binary) | Header Byte (Binary) |
|---|---|---|---|---|
SRES |
0x30 |
0x30 |
0011 0000 |
0011 0000 |
PARTNUM |
0x30 |
0xF0 |
0011 0000 |
1111 0000 |
Below are some relevant addresses with different command strobes (Table 42) and status register values (Table 44). Note that the parenthesis in table 44 contain the header byte you would send to access the registers.
The first thing returned by the CC1101 is the chip status byte that provides a summary regarding the internal state of the device. We can ignore the specifics of this byte for now, but just take note of how this byte fits into an SPI transaction. In a transaction, the actual requested data (if any) is always returned after the chip status byte.
Important
The number of bytes sent in a transaction is always equal to the number of bytes received. This is due to the nature of the SPI protocol; it is a full duplex, so the slave and the master can only transmit data at the same time. They cannot transfer data independently of each other. Further reading about the SPI protocol is recommended if a full-duplex is unfamiliar.
Example: To read the value in the PARTNUM register, we would send the value 1111 0000 (0xF0). The first two bits are the R/W and burst bits set to 1, while the last 6 are the address where PARTNUM lives at (11 0000).
Since data can only be received while the master is transmitting, we must send two bytes: 0xF0 0x00. In return, we receive two bytes corresponding to the chip status byte and the actual register value. Sending only the byte 0xF0 would return the chip status byte, but not the actual register value. 0x00 functions as a dummy byte meant to give time (clock cycles) for the slave to send back the requested data.
This starts a transaction between the CC1101 and the ESP32. This will start clocking bits out of MOSI from a transmit buffer and collecting data into a receive buffer on MISO simultaneously. We perform transactions using:
This function requires:
extern "C" void app_main(void) {
...
spi_transaction_t version_register = {};
uint8_t tx_v[2] = {0xF1, 0x00};
uint8_t rx_v[2] = {0x00, 0x00};
version_register.tx_buffer = tx_v;
version_register.rx_buffer = rx_v;
version_register.length = 16;
spi_device_polling_transmit(cc1101, &version_register);
...
}Note: This functionality has been refactored into a helper function
transmit_datainmain.cpp.
- cc1101
- The device name we created earlier in our process.
- version_register
- tx_v: The Bytes we want to send to the CC1101. tx_v[0] will always be our header byte, which is
0xF1. This corresponds to theVERSIONregister in the CC1101 (see Table 44 above or in the datasheet). The second byte is a dummy byte used to clock out the register value from the slave. This needs to be included, as every status register read will return two bytes: a chip status byte and the register value byte. In order to receive 2 bytes, we must send 2 bytes as well (due to the nature of the SPI protocol being a full-duplex). - rx_v: This buffer will be filled with the response of the slave. Again, we include two bytes in the buffer because that is what we expect to receive when we send two bytes. rx_v[0] will always be the chip status byte when the CC1101 fills the buffer.
- length: We are sending two bytes, so that equals 16 bits.
- tx_v: The Bytes we want to send to the CC1101. tx_v[0] will always be our header byte, which is
After calling this method, simply logging out the version_register receive buffer will show us the value contained inside the VERSION register. As stated before, the first byte is a chip status byte. So we will receive a chip status byte located in rx_v[0] and the actual register value in rx_v[1]. The expected value in the VERSION register will be 0x14.
Section 19.1 of the datasheet specifies the required sequence for powering up the CC1101. The system must be reset every time you turn on the power supply.
The government approved method of accomplishing this is as follows:
- Pull
CSnLOW, then drive it HIGH again - Wait for
MISOto go LOW - Send
SRES
This would require you to set spics_io_num to -1 when adding a device to the bus and controlling the CSn pin manually.
Alternatively, you can send the command strobes SRES, SIDLE, and SFTX in that order. After this sequence, your device should be ready to use. See strobe_reset in main.cpp.
The next guide in this series focuses on transmitting a signal with the CC1101 and interpreting datasheets. It is noticeably more complex, but a great way to gain a deeper understanding about interacting with embedded devices!




