1. 基础无线LED控制

基础无线LED控制 学习卡片

任务说明

使用一对NRF24L01无线模块和两块STM32F103C8T6最小系统板,实现按键无线控制远端LED亮灭。发送端读取按键状态,通过NRF24L01以2.4GHz射频发送;接收端收到数据后驱动LED。通过本案例掌握NRF24L01的SPI初始化、收发寄存器配置及基本数据交互流程。

硬件连接

STM32引脚NRF24L01引脚
PA5 (SPI1_SCK)SCK
PA6 (SPI1_MISO)MISO
PA7 (SPI1_MOSI)MOSI
PA4 (GPIO输出)CSN
PA3 (GPIO输出)CE
PA2 (GPIO输入)IRQ(本案例不使用中断)

发送端按键接PB0(内部上拉,按下为低电平);接收端LED正极经330Ω电阻接PB1,负极接地。两模块需共地(若分别供电)。

接线示意:
发送端:STM32_PA5→NRF_SCK,PA6→MISO,PA7→MOSI,PA4→CSN,PA3→CE,PB0→按键(另一端接GND)
接收端:STM32_PA5→NRF_SCK,PA6→MISO,PA7→MOSI,PA4→CSN,PA3→CE,PB1→LED(另一端经电阻接GND)
发送端 STM32 PA5 (SCK) PA6 (MISO) PA7 (MOSI) PA4 (CSN) PA3 (CE) PB0 (按键输入) 3.3V / GND NRF24L01 SCK MISO MOSI CSN CE 按键 2.4GHz NRF24L01 SCK MISO MOSI CSN CE 接收端 STM32 PA5 (SCK) PA6 (MISO) PA7 (MOSI) PA4 (CSN) PA3 (CE) PB1 (LED输出) 3.3V / GND LED

代码实现

以下代码基于STM32标准外设库,发送端与接收端共用同一SPI驱动和NRF24L01寄存器操作函数,仅主函数逻辑不同。两工程分别编译后烧录到对应板卡。


// ============ nrf24l01.h ============
#ifndef __NRF24L01_H
#define __NRF24L01_H
#include "stm32f10x.h"

#define TX_ADDR_WIDTH   5
#define RX_ADDR_WIDTH   5

// 寄存器地址
#define CONFIG      0x00
#define EN_AA       0x01
#define EN_RXADDR   0x02
#define SETUP_AW    0x03
#define SETUP_RETR  0x04
#define RF_CH       0x05
#define RF_SETUP    0x06
#define STATUS      0x07
#define RX_ADDR_P0  0x0A
#define TX_ADDR     0x10
#define RX_PW_P0    0x11
#define FIFO_STATUS 0x17

// 指令
#define R_REGISTER  0x00
#define W_REGISTER  0x20
#define R_RX_PAYLOAD  0x61
#define W_TX_PAYLOAD  0xA0
#define FLUSH_TX    0xE1
#define FLUSH_RX    0xE2

// 引脚定义
#define NRF_CSN_LOW()   GPIO_ResetBits(GPIOA, GPIO_Pin_4)
#define NRF_CSN_HIGH()  GPIO_SetBits(GPIOA, GPIO_Pin_4)
#define NRF_CE_LOW()    GPIO_ResetBits(GPIOA, GPIO_Pin_3)
#define NRF_CE_HIGH()   GPIO_SetBits(GPIOA, GPIO_Pin_3)

void NRF_GPIO_Init(void);
void NRF_SPI_Init(void);
uint8_t NRF_SPI_RW(uint8_t byte);
uint8_t NRF_ReadReg(uint8_t reg);
void NRF_WriteReg(uint8_t reg, uint8_t value);
void NRF_WriteBuf(uint8_t reg, uint8_t *buf, uint8_t len);
void NRF_ReadBuf(uint8_t reg, uint8_t *buf, uint8_t len);
void NRF_TX_Mode(uint8_t *tx_addr);
void NRF_RX_Mode(uint8_t *rx_addr);
uint8_t NRF_SendPacket(uint8_t *data, uint8_t len);
uint8_t NRF_ReceivePacket(uint8_t *data, uint8_t *len);
void NRF_Init(void);

#endif

// ============ nrf24l01.c ============
#include "nrf24l01.h"
#include <string.h>

void NRF_GPIO_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

    // CSN(PA4), CE(PA3) 推挽输出
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3 | GPIO_Pin_4;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    NRF_CE_LOW();
    NRF_CSN_HIGH();
}

void NRF_SPI_Init(void)
{
    SPI_InitTypeDef SPI_InitStructure;
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1 | RCC_APB2Periph_GPIOA, ENABLE);

    GPIO_InitTypeDef GPIO_InitStructure;
    // SCK=PA5, MOSI=PA7 推挽复用
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    // MISO=PA6 浮空输入
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
    SPI_InitStructure.SPI_Mode = SPI_Mode_Master;
    SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;
    SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;
    SPI_InitStructure.SPI_CPHA = SPI_CPHA_Edge;
    SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;
    SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_8;
    SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
    SPI_Init(SPI1, &SPI_InitStructure);
    SPI_Cmd(SPI1, ENABLE);
}

uint8_t NRF_SPI_RW(uint8_t byte)
{
    while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET);
    SPI_I2S_SendData(SPI1, byte);
    while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET);
    return SPI_I2S_ReceiveData(SPI1);
}

uint8_t NRF_ReadReg(uint8_t reg)
{
    uint8_t val;
    NRF_CSN_LOW();
    NRF_SPI_RW(R_REGISTER | reg);
    val = NRF_SPI_RW(0x00);
    NRF_CSN_HIGH();
    return val;
}

void NRF_WriteReg(uint8_t reg, uint8_t value)
{
    NRF_CSN_LOW();
    NRF_SPI_RW(W_REGISTER | reg);
    NRF_SPI_RW(value);
    NRF_CSN_HIGH();
}

void NRF_WriteBuf(uint8_t reg, uint8_t *buf, uint8_t len)
{
    uint8_t i;
    NRF_CSN_LOW();
    NRF_SPI_RW(W_REGISTER | reg);
    for (i = 0; i < len; i++)
        NRF_SPI_RW(buf[i]);
    NRF_CSN_HIGH();
}

void NRF_ReadBuf(uint8_t reg, uint8_t *buf, uint8_t len)
{
    uint8_t i;
    NRF_CSN_LOW();
    NRF_SPI_RW(R_REGISTER | reg);
    for (i = 0; i < len; i++)
        buf[i] = NRF_SPI_RW(0x00);
    NRF_CSN_HIGH();
}

void NRF_Init(void)
{
    NRF_CE_LOW();
    NRF_WriteReg(CONFIG, 0x0E);      // 使能CRC, 16位CRC, 上电
    NRF_WriteReg(EN_AA, 0x01);       // 使能自动应答通道0
    NRF_WriteReg(EN_RXADDR, 0x01);   // 使能接收通道0
    NRF_WriteReg(SETUP_AW, 0x03);    // 5字节地址
    NRF_WriteReg(SETUP_RETR, 0x1A);  // 自动重发500us, 10次
    NRF_WriteReg(RF_CH, 0x40);       // 信道 2.440GHz
    NRF_WriteReg(RF_SETUP, 0x07);    // 1Mbps, 0dBm
    NRF_WriteReg(RX_PW_P0, 0x01);    // 通道0接收1字节
    NRF_CE_HIGH();
}

void NRF_TX_Mode(uint8_t *tx_addr)
{
    NRF_CE_LOW();
    NRF_WriteReg(CONFIG, 0x0E);      // 上电, 发送模式(PRIM_RX=0)
    NRF_WriteBuf(TX_ADDR, tx_addr, TX_ADDR_WIDTH);
    NRF_WriteBuf(RX_ADDR_P0, tx_addr, RX_ADDR_WIDTH); // 自动应答需要
    NRF_CE_HIGH();
}

void NRF_RX_Mode(uint8_t *rx_addr)
{
    NRF_CE_LOW();
    NRF_WriteBuf(RX_ADDR_P0, rx_addr, RX_ADDR_WIDTH);
    NRF_WriteReg(CONFIG, 0x0F);      // 上电, 接收模式(PRIM_RX=1)
    NRF_CE_HIGH();
}

uint8_t NRF_SendPacket(uint8_t *data, uint8_t len)
{
    uint8_t status;
    NRF_CE_LOW();
    NRF_CSN_LOW();
    NRF_SPI_RW(FLUSH_TX);
    NRF_CSN_HIGH();
    NRF_CSN_LOW();
    NRF_SPI_RW(W_TX_PAYLOAD);
    for (uint8_t i = 0; i < len; i++)
        NRF_SPI_RW(data[i]);
    NRF_CSN_HIGH();
    NRF_CE_HIGH();  // 启动发送

    // 等待发送完成或超时(约10ms)
    uint32_t timeout = 10000;
    while (timeout--) {
        status = NRF_ReadReg(STATUS);
        if (status & 0x20) {  // TX_DS 发送成功
            NRF_WriteReg(STATUS, 0x20);  // 清除标志
            return 1;
        }
        if (status & 0x10) {  // MAX_RT 重发超限
            NRF_WriteReg(STATUS, 0x10);
            NRF_CSN_LOW();
            NRF_SPI_RW(FLUSH_TX);
            NRF_CSN_HIGH();
            return 0;
        }
    }
    return 0;
}

uint8_t NRF_ReceivePacket(uint8_t *data, uint8_t *len)
{
    uint8_t status = NRF_ReadReg(STATUS);
    if (status & 0x40) {  // RX_DR 收到数据
        NRF_CSN_LOW();
        NRF_SPI_RW(R_RX_PAYLOAD);
        *len = NRF_SPI_RW(0x00);
        data[0] = NRF_SPI_RW(0x00);
        NRF_CSN_HIGH();
        NRF_WriteReg(STATUS, 0x40);  // 清除RX_DR
        return 1;
    }
    return 0;
}

// ============ main.c (发送端) ============
#include "stm32f10x.h"
#include "nrf24l01.h"

uint8_t tx_addr[5] = {0xE7, 0xE7, 0xE7, 0xE7, 0xE7};

void Delay_ms(uint32_t ms)
{
    for (uint32_t i = 0; i < ms * 4000; i++)
        __NOP();
}

int main(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);

    // PB0 按键输入,内部上拉
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_Init(GPIOB, &GPIO_InitStructure);

    NRF_GPIO_Init();
    NRF_SPI_Init();
    NRF_Init();
    NRF_TX_Mode(tx_addr);

    uint8_t key_state = 1;  // 上一次按键状态(1=未按下)
    uint8_t led_cmd = 0;

    while (1) {
        uint8_t current = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0);
        if (key_state == 1 && current == 0) {
            // 检测到按下沿
            led_cmd = !led_cmd;  // 翻转命令
            NRF_SendPacket(&led_cmd, 1);
            Delay_ms(50);  // 消抖
        }
        key_state = current;
        Delay_ms(5);
    }
}

// ============ main.c (接收端) ============
#include "stm32f10x.h"
#include "nrf24l01.h"

uint8_t rx_addr[5] = {0xE7, 0xE7, 0xE7, 0xE7, 0xE7};

void Delay_ms(uint32_t ms)
{
    for (uint32_t i = 0; i < ms * 4000; i++)
        __NOP();
}

int main(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);

    // PB1 LED输出
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB, &GPIO_InitStructure);
    GPIO_ResetBits(GPIOB, GPIO_Pin_1);  // 初始熄灭

    NRF_GPIO_Init();
    NRF_SPI_Init();
    NRF_Init();
    NRF_RX_Mode(rx_addr);

    uint8_t rx_data = 0;
    uint8_t rx_len = 0;

    while (1) {
        if (NRF_ReceivePacket(&rx_data, &rx_len)) {
            if (rx_len == 1) {
                if (rx_data == 0)
                    GPIO_ResetBits(GPIOB, GPIO_Pin_1);  // 熄灭
                else
                    GPIO_SetBits(GPIOB, GPIO_Pin_1);    // 点亮
            }
        }
        Delay_ms(1);
    }
}

编译与烧录

使用Keil MDK分别创建两个工程(发送端和接收端),均添加nrf24l01.c和对应main.c。芯片选择STM32F103C8,勾选USE_STDPERIPH_DRIVER宏。编译无误后,用ST-Link将发送端程序烧录到按键侧板卡,接收端程序烧录到LED侧板卡。

验证方法

两板均以3.3V供电(注意NRF24L01不能接5V)。将两模块天线端朝向同一方向,距离1~5米内。按下发送端按键,观察接收端LED是否翻转状态。每按一次,LED应在亮/灭之间切换一次。

判定标准:连续按10次按键,LED状态跟随翻转10次且无丢失,说明收发链路正常。若LED无反应,依次检查:①两模块信道和地址是否一致(本代码已固定为0x40信道和0xE7地址);②SPI接线是否与代码一致;③接收端PB1是否输出正确电平(可用万用表测量);④供电是否稳定在3.3V。

预期输出

发送端每检测到一次按键按下沿,通过NRF24L01发送1字节数据(0x00或0x01交替)。接收端收到后,PB1引脚输出对应电平,LED亮灭状态翻转。由于代码中发送端仅在按键按下沿发送一次,长按不会重复触发。