普冉PY32系列(八) GPIO模拟和硬件SPI方式驱动无线收发芯片XN297LBW

目录

XN297LBW

XN297LBW 是一个SOP8封装的2.4GHz频段无线收发芯片, 价格在1元左右, 适用于低成本应用. 虽然磐启已经发布了 XN297L 的下一代产品 PAN1026, 但是市面上基本上见不到后者的身影, 零售能买到的还是 XN297L.

生产商是上海磐启, 产品页地址: https://wiki.panchip.com/ble-lite/2-4g-t-rx/xn297l_series/

磐启对 XN297L 的产品介绍: "工作在 2.400~2.483GHz 世界通用 ISM 频段的单片无线收发芯片, XN297L采用嵌入式基带协议引擎, 适用于超低功耗无线应用. 采用 GFSK 调制, 可配置频率信道, 输出功率和接口数据速率等射频参数. XN297L 支持 2Mbps, 1Mbps 和 250Kbps 的数据速率. 对于长距离应用, 输出功率可以调节高达 11dBm, 对于短距离和超低功率应用, 输出功率可以低至-23dBm."

XN297LBW 主要特性

  • 无线
    • 通信频段:2.400GHz~2.483GHz
    • 数据速率:2Mbps,1Mbps,250Kbps
    • 调制方式:GFSK
  • 发射器
    • 输出功率:11, 9, 5, -1, -10 or -23dBm
    • 18mA@2dBm
    • 30mA@9dBm
  • 接收器
    • -83dBm@2Mbps
    • -87dBm@1Mbps
    • -91dBm@250Kbps
  • 协议引擎
    • 支持1到32字节或64字节数据长度
    • 支持自动应答及自动重传
    • 6个接收数据通道构成1:6的星状网络
  • 电源管理
    • 工作电压:2.3~3.3V
    • 2uA断电模式
    • 30uA待机-Ⅰ模式
  • 主机接口
    • 支持3引脚SPI, 4Mbps SPI接口速率
    • 支持两个独立的32字节TX和RX FIFOs
    • 支持一个64字节的TX和RX FIFOs
  • 封装
    • SOP8

这里要注意的几点:

  1. 工作电压是3.3V, 不要错接5V.
  2. SPI速率为4MHz, 实测上限不会比4MHz高多少, 在6MHz频率时大概率SPI通信错误导致不能工作.
  3. TX FIFO 与NRF24L01相比只有两个32字节, 而NRF24L01是3个32字节. 性能相对缩水.

PIN脚定义和应用电路

PIN脚定义

  • VDD 和 VSS 分别接 VCC 和 GND
  • XC1 和 XC2 接晶振
  • ANT 接天线
  • 用于MCU接口通信的只有 CSN, SCK 和 DATA 这三个PIN

应用电路

模块实物

嘉立创打样的测试模块 (项目地址 https://oshwhub.com/iosetting/xn297lbw-xl2400-evb)

使用PY32F0驱动XN297LBW

XN297L最新的SDK可以从磐启的论坛下载 论坛›BLE-Lite系列2.4GHz TRX›XN297L›XN297L_SDK. 因为面向的主要是低成本应用, 大多数搭配的MCU为廉价的8位8051, 不一定有硬件SPI, 为了保证兼容在SDK中使用的都是GPIO模拟SPI方式进行驱动. 但是实际上是可以通过硬件SPI方式进行驱动的.

以下分别对GPIO模拟和硬件SPI方式的驱动进行介绍.

硬件准备

  • XN297LBW模块
  • PY32F002A/PY32F003/PY32F030 系列MCU的开发板, 建议在验证阶段使用 20PIN 及以上封装的型号, 避免PIN脚复用引起的干扰. 跑通后再迁移到低PIN型号
  • USB2TTL模块, 用于观察输出
  • 以上硬件需要两套, 测试中分别用于接收和发送

下面以PY32F002A为例. 代码不需调整可以直接运行于 PY32F003x 和 PY32F030x 系列的其它型号.

GPIO模拟方式

接线

注意电源使用3.3V

复制代码
PY32          XN297LBW SOP8
PA1   ------> CLK/SCK
PA6   ------> CSN/NSS
PA7   ------> DATA/MOSI

              USB2TTL
PA2(TX) ----> RX
PA3(RX) ----> TX

代码说明

SDK代码中使用的MCU是STM8L, 需要迁移到 PY32F002A.

将 xn297l.h 中的 GPIO 设置换为PY32F002A的PIN脚

c 复制代码
#define XN297L_DATA_OUT()        LL_GPIO_SetPinMode(GPIOA, LL_GPIO_PIN_7, LL_GPIO_MODE_OUTPUT)
#define XN297L_DATA_IN()         LL_GPIO_SetPinMode(GPIOA, LL_GPIO_PIN_7, LL_GPIO_MODE_INPUT)
#define XN297L_DATA_LOW()        LL_GPIO_ResetOutputPin(GPIOA, LL_GPIO_PIN_7)
#define XN297L_DATA_HIGH()       LL_GPIO_SetOutputPin(GPIOA, LL_GPIO_PIN_7)
#define XN297L_DATA_READ()       LL_GPIO_IsInputPinSet(GPIOA, LL_GPIO_PIN_7)

#define XN297L_SCK_LOW()         LL_GPIO_ResetOutputPin(GPIOA, LL_GPIO_PIN_1)
#define XN297L_SCK_HIGH()        LL_GPIO_SetOutputPin(GPIOA, LL_GPIO_PIN_1)

#define XN297L_CSN_LOW()         LL_GPIO_ResetOutputPin(GPIOA, LL_GPIO_PIN_6)
#define XN297L_CSN_HIGH()        LL_GPIO_SetOutputPin(GPIOA, LL_GPIO_PIN_6)

#define XN297L_CE_LOW()          XN297L_WriteReg(XN297L_CMD_CE_FSPI_OFF, 0)
#define XN297L_CE_HIGH()         XN297L_WriteReg(XN297L_CMD_CE_FSPI_ON, 0)

在 main.c 中增加GPIO初始化

c 复制代码
static void APP_GPIOConfig(void)
{
  LL_GPIO_InitTypeDef GPIO_InitStruct;

  /* PA1 CLK */
  GPIO_InitStruct.Pin = LL_GPIO_PIN_1;
  GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
  GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
  GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
  LL_GPIO_Init(GPIOA, &GPIO_InitStruct);
  /* PA6 CSN */
  GPIO_InitStruct.Pin = LL_GPIO_PIN_6;
  LL_GPIO_Init(GPIOA, &GPIO_InitStruct);
  /* PA7 DATA */
  GPIO_InitStruct.Pin = LL_GPIO_PIN_7;
  GPIO_InitStruct.Mode = LL_GPIO_MODE_INPUT;
  LL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}

使用GPIO模拟SPI的字节写

c 复制代码
/**
 * Emulate SPI Write on GPIO pins
 */
void XN297L_WriteByte(uint8_t value)
{
    uint8_t i = 0;
    XN297L_SCK_LOW();
    XN297L_DATA_OUT();
    for (i = 0; i < 8; i++)
    {
        XN297L_SCK_LOW();
        if (value & 0x80)
        {
            XN297L_DATA_HIGH();
        }
        else
        {
            XN297L_DATA_LOW();
        }
        XN297L_SCK_HIGH();
        value = value << 1;
    }
    XN297L_SCK_LOW();
}

模拟字节读. 这里有个细节, 在XN297L_SCK_HIGH();之后加一个__NOP();, 如果没有这个NOP(), PY32F0在低频率(8MHz和24MHz)的时候容易产生读取错误.

c 复制代码
/**
 * Emulate SPI Read on GPIO pins
 */
uint8_t XN297L_ReadByte(void)
{
    uint8_t i = 0, RxData = 0;

    XN297L_DATA_IN();
    for (i = 0; i < 8; i++)
    {
        RxData = RxData << 1;
        XN297L_SCK_HIGH();
        __NOP();
        if (XN297L_DATA_READ())
        {
            RxData |= 0x01;
        }
        else
        {
            RxData &= 0xfe;
        }
        XN297L_SCK_LOW();
    }
    return RxData;
}

XN297L 的初始化. 这部分是相对固定的流程, 可以根据自己的需要进行调整, 但是在测试阶段务必保持接收端和发送端的配置一致. 这里在SDK的代码上做了一些修改, 开启了发送的重试和ACK.

c 复制代码
// 这部分来自于手册 "XN297L 软件设计和调试参考"
const uint8_t 
    BB_cal_data[]    = {0x12,0xED,0x67,0x9C,0x46},
    RF_cal_data[]    = {0xF6,0x3F,0x5D},
    RF_cal2_data[]   = {0x45,0x21,0xEF,0x2C,0x5A,0x42},
    Dem_cal_data[]   = {0x01},
    Dem_cal2_data[]  = {0x0B,0xDF,0x02};

void XN297L_Init(void)
{
    XN297L_WriteReg(XN297L_CMD_RST_FSPI, 0x5A); // Soft reset
    XN297L_WriteReg(XN297L_CMD_RST_FSPI, 0XA5);

    XN297L_WriteReg(XN297L_CMD_FLUSH_TX, 0);
    XN297L_WriteReg(XN297L_CMD_FLUSH_RX, 0);
    XN297L_WriteReg(XN297L_CMD_W_REGISTER | XN297L_REG_STATUS, 0x70);       // Clear status flags
    XN297L_WriteReg(XN297L_CMD_W_REGISTER | XN297L_REG_EN_AA, 0x3F);        // AutoAck on all pipes
    XN297L_WriteReg(XN297L_CMD_W_REGISTER | XN297L_REG_EN_RXADDR, 0x3F);    // Enable all pipes (P0 ~ P5, bit0 ~ bit5)
    XN297L_WriteReg(XN297L_CMD_W_REGISTER | XN297L_REG_SETUP_AW, XN297L_SETUP_AW_5BYTE); // Address width
    XN297L_WriteReg(XN297L_CMD_W_REGISTER | XN297L_REG_RF_CH, 78);          // Channel 78, 2478M HZ
    XN297L_WriteReg(XN297L_CMD_W_REGISTER | XN297L_REG_RX_PW_P0, XN297L_PLOAD_WIDTH ); // Payload width of P0
    XN297L_WriteReg(XN297L_CMD_W_REGISTER | XN297L_REG_RX_PW_P1, XN297L_PLOAD_WIDTH ); // Payload width of P1
    XN297L_WriteReg(XN297L_CMD_W_REGISTER | XN297L_REG_RX_PW_P2, XN297L_PLOAD_WIDTH ); // Payload width of P2
    XN297L_WriteReg(XN297L_CMD_W_REGISTER | XN297L_REG_RX_PW_P3, XN297L_PLOAD_WIDTH ); // Payload width of P3
    XN297L_WriteReg(XN297L_CMD_W_REGISTER | XN297L_REG_RX_PW_P4, XN297L_PLOAD_WIDTH ); // Payload width of P4
    XN297L_WriteReg(XN297L_CMD_W_REGISTER | XN297L_REG_RX_PW_P5, XN297L_PLOAD_WIDTH ); // Payload width of P5

    XN297L_WriteFromBuf(XN297L_CMD_W_REGISTER | XN297L_REG_BB_CAL,    BB_cal_data,  sizeof(BB_cal_data));
    XN297L_WriteFromBuf(XN297L_CMD_W_REGISTER | XN297L_REG_RF_CAL2,   RF_cal2_data, sizeof(RF_cal2_data));
    XN297L_WriteFromBuf(XN297L_CMD_W_REGISTER | XN297L_REG_DEM_CAL,   Dem_cal_data, sizeof(Dem_cal_data));
    XN297L_WriteFromBuf(XN297L_CMD_W_REGISTER | XN297L_REG_RF_CAL,    RF_cal_data,  sizeof(RF_cal_data));
    XN297L_WriteFromBuf(XN297L_CMD_W_REGISTER | XN297L_REG_DEM_CAL2,  Dem_cal2_data,sizeof(Dem_cal2_data));
    XN297L_WriteReg(XN297L_CMD_W_REGISTER | XN297L_REG_DYNPD, 0x00); // Dynamic payload width: off
    XN297L_WriteReg(XN297L_CMD_W_REGISTER | XN297L_REG_RF_SETUP,  XN297L_RF_POWER_P_9|XN297L_RF_DR_1M); // 9dbm 1Mbps
    XN297L_WriteReg(XN297L_CMD_ACTIVATE, 0x73);

    XN297L_WriteReg(XN297L_CMD_W_REGISTER | XN297L_REG_SETUP_RETR, 0x10|0x05); // Retry interval 500µs, 5 times

    if(XN297L_PLOAD_WIDTH >32)
    {
        XN297L_WriteReg(XN297L_CMD_W_REGISTER | XN297L_REG_FEATURE, XN297L_FEATURE_BIT5_CE_SOFT|XN297L_FEATURE_BIT43_DATA_64BYTE);
    }
    else
    {
        XN297L_WriteReg(XN297L_CMD_W_REGISTER | XN297L_REG_FEATURE, XN297L_FEATURE_BIT5_CE_SOFT);
    }
}

数据发送函数. 因为前面开启了重试和ACK, 这里做了一个等待发送结果的轮询和超时判断

c 复制代码
uint8_t XN297L_TxData(uint8_t *ucPayload, uint8_t length)
{
    uint8_t y = 100, status = 0;
    XN297L_CE_HIGH();
    __NOP();
    XN297L_WriteFromBuf(XN297L_CMD_W_TX_PAYLOAD, ucPayload, length);
    // Retry until timeout
    while (y--)
    {
        LL_mDelay(1);
        status = XN297L_ReadStatus();
        // If TX successful or retry timeout, exit
        if ((status & (XN297L_FLAG_MAX_RT | XN297L_FLAG_TX_DS)) != 0)
        {
            //printf(" %d %02x\r\n", y, status);
            break;
        }
    }
    XN297L_WriteReg(XN297L_CMD_FLUSH_TX, 0);
    XN297L_WriteReg(XN297L_CMD_W_REGISTER | XN297L_REG_STATUS, 0x70);
    XN297L_CE_LOW();
    return status;
}

数据接收. 因为接收使用的是轮询, 所以这里只是简单地判断了接收状态, 在收到数据时读取数据.

c 复制代码
uint8_t XN297L_DumpRxData(void)
{
    uint8_t status, rxplWidth;
    status = XN297L_ReadStatus();
    if (status & XN297L_FLAG_RX_DR)
    {
        XN297L_WriteReg(XN297L_CMD_W_REGISTER | XN297L_REG_STATUS, status);
        rxplWidth = XN297L_ReadReg(XN297L_CMD_R_RX_PL_WID);
        XN297L_ReadToBuf(XN297L_CMD_R_RX_PAYLOAD, xbuf, rxplWidth);
    }
    return status;
}

完整代码

XN297L 示例代码的 GitHub 仓库地址: https://github.com/IOsetting/py32f0-template/tree/main/Examples/PY32F0xx/LL/GPIO/XN297LBW_Wireless

运行测试

修改 main.c 中的模式设置, 0为接收, 1为发送, 分别写入至两个PY32F002A开发板, 观察UART的输出.

c 复制代码
// 0:RX, 1:TX
#define XN297L_MODE 0

接收端在每次接收到数据时, 输出第1,2,31个字节的值; 发送端每发送255组数据(每组32字节)后, 会显示发送成功的个数(十六进制), 这个输出可以用于计算发送成功率, 以及发送速度.

硬件SPI方式

接线

接线方式使用4线制全双工, PY32的MOSI和MISO都接到XN297LBW的DATA, 但是在MOSI(PA7)上串一个1K的电阻. 对于使用SPI协议的三线连接, 如果半双工SPI有问题, 都可以用这种接线试试全双工方式通信. 从实际测试看, XN297LBW 支持这种接线方式.

复制代码
PY32                XN297LBW SOP8
PA0   ------------> DATA/MOSI
PA7   ---> 1KR ---> DATA/MOSI
PA1   ------------> CLK/SCK
PA6   ------------> CSN/NSS

                    USB2TTL
PA2(TX) ----------> RX
PA3(RX) ----------> TX

代码说明

SPI接口的初始化. 注意SPI的时钟频率不要超过4MHz

c 复制代码
/**
 * SPI1 Alternative Function Pins
 * SPI1_SCK:  PA1_AF0, PA2_AF10, PA5_AF0, PA9_AF10, PB3_AF0
 * SPI1_MISO: PA0_AF10, PA6_AF0, PA7_AF10, PA11_AF0, PA13_AF10, PB4_AF0
 * SPI1_MOSI: PA1_AF10, PA2_AF0, PA3_AF10, PA7_AF0, PA8_AF10, PA12_AF0, PB5_AF0
 * SPI1_NSS:  PA4_AF0, PA10_AF10, PA15_AF0, PB0_AF0, PF1_AF10, PF3_AF10
*/
static void APP_SPI_Config(void)
{
  LL_SPI_InitTypeDef SPI_InitStruct = {0};
  LL_GPIO_InitTypeDef GPIO_InitStruct = {0};

  LL_APB1_GRP2_EnableClock(LL_APB1_GRP2_PERIPH_SPI1);

  // PA1 SCK
  GPIO_InitStruct.Pin = LL_GPIO_PIN_1;
  GPIO_InitStruct.Mode = LL_GPIO_MODE_ALTERNATE;
  GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_HIGH;
  GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
  GPIO_InitStruct.Pull = LL_GPIO_PULL_UP;
  GPIO_InitStruct.Alternate = LL_GPIO_AF_0;
  LL_GPIO_Init(GPIOA, &GPIO_InitStruct);
  // PA0 MISO
  GPIO_InitStruct.Pin = LL_GPIO_PIN_0;
  GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
  GPIO_InitStruct.Alternate = LL_GPIO_AF_10;
  LL_GPIO_Init(GPIOA, &GPIO_InitStruct);
  // PA7 MOSI
  GPIO_InitStruct.Pin = LL_GPIO_PIN_7;
  GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
  GPIO_InitStruct.Alternate = LL_GPIO_AF_0;
  LL_GPIO_Init(GPIOA, &GPIO_InitStruct);
  /*
   * Full duplex mode, MOSI and MISO both connect to DATA,
   * Add one 1KR between MOSI and DATA
  */
  SPI_InitStruct.TransferDirection = LL_SPI_FULL_DUPLEX;
  SPI_InitStruct.Mode = LL_SPI_MODE_MASTER;
  SPI_InitStruct.DataWidth = LL_SPI_DATAWIDTH_8BIT;
  SPI_InitStruct.ClockPolarity = LL_SPI_POLARITY_LOW;
  SPI_InitStruct.ClockPhase = LL_SPI_PHASE_1EDGE;
  SPI_InitStruct.NSS = LL_SPI_NSS_SOFT;
  // SPI的时钟频率不要超过4MHz
  SPI_InitStruct.BaudRate = LL_SPI_BAUDRATEPRESCALER_DIV16;
  SPI_InitStruct.BitOrder = LL_SPI_MSB_FIRST;
  LL_SPI_Init(SPI1, &SPI_InitStruct);
  LL_SPI_Enable(SPI1);
}

硬件SPI方式的字节读写

c 复制代码
uint8_t SPI_TxRxByte(uint8_t data)
{
  uint8_t SPITimeout = 0xFF;
  /* Check the status of Transmit buffer Empty flag */
  while (READ_BIT(SPI1->SR, SPI_SR_TXE) == RESET)
  {
    if (SPITimeout-- == 0)
      return 0;
  }
  LL_SPI_TransmitData8(SPI1, data);
  SPITimeout = 0xFF;
  while (READ_BIT(SPI1->SR, SPI_SR_RXNE) == RESET)
  {
    if (SPITimeout-- == 0)
      return 0;
  }
  // Read from RX buffer
  return LL_SPI_ReceiveData8(SPI1);
}

对应XN297L的命令读写改造为调用硬件SPI读写函数

c 复制代码
uint8_t XN297L_WriteReg(uint8_t reg, uint8_t value)
{
    uint8_t reg_val;
    XN297L_CSN_LOW();
    SPI_TxRxByte(reg);
    reg_val = SPI_TxRxByte(value);
    XN297L_CSN_HIGH();
    return reg_val;
}

uint8_t XN297L_ReadReg(uint8_t reg)
{
    uint8_t reg_val;
    XN297L_CSN_LOW();
    SPI_TxRxByte(reg);
    reg_val = SPI_TxRxByte(XN297L_CMD_NOP);
    XN297L_CSN_HIGH();
    return reg_val;
}

完整代码

XN297L 示例代码的 GitHub 仓库地址: https://github.com/IOsetting/py32f0-template/tree/main/Examples/PY32F0xx/LL/SPI/XN297L_Wireless

运行测试

和GPIO模拟方式的一样, 修改 main.c 中的模式设置, 0为接收, 1为发送, 分别写入至两个PY32F002A开发板, 观察UART的输出.

c 复制代码
// 0:RX, 1:TX
#define XN297L_MODE 0

利用FIFO队列提升发送速度

在 NRF24L01 的使用中, 可以通过 "直接写入TX FIFO -> 通过 FLAG 观察 TX FIFO 是否写满判断是继续写入还是阻塞等待" 的方式提升发送速度. XN297L 的 TX FIFO 队列包含两组 32 个字节, 也可以通过这种方式进行加速.

相关的函数

c 复制代码
ErrorStatus XN297L_TxFast(const uint8_t *ucPayload, uint8_t length)
{
    //Blocking only if FIFO is full. This will loop and block until TX is successful or fail
    while ((XN297L_ReadStatus() & XN297L_FLAG_TX_FULL)) {
        if (xn297l_state & XN297L_FLAG_MAX_RT) {
            return ERROR;
        }
    }
    XN297L_WriteFromBuf(XN297L_CMD_W_TX_PAYLOAD, ucPayload, length);
    XN297L_CE_HIGH();
    return SUCCESS;
}

// 用于 MAX_RT 状态清除标志位
void XN297L_ReuseTX(void)
{
    XN297L_WriteReg(XN297L_CMD_W_REGISTER | XN297L_REG_STATUS, XN297L_FLAG_MAX_RT); //Clear max retry flag
    XN297L_CE_LOW();
    XN297L_CE_HIGH();
}

使用方式: 在发送循环中调用 XN297L_TxFast() 进行发送, 在遇到错误时, 用 XN297L_ReuseTX() 重置状态

c 复制代码
if (XN297L_TxFast(tmp, XN297L_PLOAD_WIDTH) == SUCCESS)
{
  j++;
}
else
{
  XN297L_ReuseTX();
}

从实际测试结果看, 用 XN297L_TxFast() 发送相比普通发送方式有10%的性能提升.

相关推荐
泛凡(Linyongui)16 天前
一款基于LP4057芯片的单节 4.2V 锂电池线性充电管理电路,带电源路径自动切换功能
arm开发·单片机·py32
Megahertz662 个月前
记一次失败的 FreeRTOS 移植
mcu·py32·py32f003
Milton4 个月前
不到80元的E88无刷电机无人机拆解
xn297lbw·py32f002b·tp4054·pfs122
Milton2 年前
普冉PY32系列(十五) PY32F0系列的低功耗模式
py32·py32f002b
Milton2 年前
普冉PY32系列(十四) 从XL2400迁移到XL2400P
py32f002a·xl2400·xl2400p
Milton2 年前
普冉PY32系列(十三) SPI驱动WS2812全彩LED
py32·ws2812·py32f072
Milton2 年前
普冉PY32系列(十二) 基于PY32F002A的6+1通道遥控小车III - 驱动篇
py32f002a·74hc595·yx-1818
Milton2 年前
普冉PY32系列(十) 基于PY32F002A的6+1通道遥控小车I - 综述篇
py32f002a·xl2400·74hc595·74hc165·yx-1818
Milton2 年前
普冉PY32系列(九) GPIO模拟和硬件SPI方式驱动无线收发芯片XL2400
py32·py32f002a·xn297lbw·xl2400
IOsetting2 年前
普冉PY32系列(八) GPIO模拟和硬件SPI方式驱动无线收发芯片XN297LBW
py32f002a·xn297l·xn297lbw·py32f003