LoRa网关项目——SX1278开发(二)

#前言

​ 最近在做一个LoRa物联网网关的项目,网关的作用主要是管理连接的LoRa传感器终端,将传感数据通过协议转换向上转发到Internet,当然,也要处理下行的数据。

​ 使用到的LoRa射频芯片是SX1278,MCU为STM32F103RCT6,连接Internet用的是ESP8266+AT,且移植了FreeRTOS(单纯是为了学习),开发环境是STM32CubeMX+Keil 5。由于之前没负责过整个系统的开发,所以开此贴记录一下开发过程,由于本人上学以来语文一直不好,所以文笔正在努力进步中,如果此文章有您觉得我说的不明白的地方,可以发送邮件到wanglu082@yeah.net,或者在文章下方评论,我看到会尽快回复您,多谢谅解!


LoRa网关项目——SX1278开发(二)

​ 上一章介绍了整个工程的架构和选用的LoRa模块,本章自然就来到了SX1278的初始化环节。

一. SX1278初始化

​ 遇到有多个复杂的结构体的项目时,我个人不喜欢先去看一堆结构体的定义,而是先看代码的逻辑,遇到一个不会的再去到Defination去研究,所以咱们直接看代码。

1.1 注册相关回调函数

/* 注册相关的回调函数 */
RadioEvents.TxDone = OnTxDone;
RadioEvents.RxDone = OnRxDone;
RadioEvents.TxTimeout = OnTxTimeout;
RadioEvents.RxTimeout = OnRxTimeout;
RadioEvents.RxError = OnRxError;

​ 什么?第一步居然不是 xxxInit ,这是因为下一步对SX1278初始化函数要传入 RadioEvents ,所以要先对这个 RadioEvents 进行初始化,去到它的 Definantion 发现它的类型是 RadioEvents_t ,看一下它的定义:

/*!
 * \brief Radio driver callback functions
 */
typedef struct
{
    /*!
     * \brief  Tx Done callback prototype.
     */
    void    ( *TxDone )( void );
    /*!
     * \brief  Tx Timeout callback prototype.
     */
    void    ( *TxTimeout )( void );
    /*!
     * \brief Rx Done callback prototype.
     *
     * \param [IN] payload Received buffer pointer
     * \param [IN] size    Received buffer size
     * \param [IN] rssi    RSSI value computed while receiving the frame [dBm]
     * \param [IN] snr     Raw SNR value given by the radio hardware
     *                     FSK : N/A ( set to 0 )
     *                     LoRa: SNR value in dB
     */
    void    ( *RxDone )( uint8_t *payload, uint16_t size, int16_t rssi, int8_t snr );
    /*!
     * \brief  Rx Timeout callback prototype.
     */
    void    ( *RxTimeout )( void );
    /*!
     * \brief Rx Error callback prototype.
     */
    void    ( *RxError )( void );
    /*!
     * \brief  FHSS Change Channel callback prototype.
     *
     * \param [IN] currentChannel   Index number of the current channel
     */
    void ( *FhssChangeChannel )( uint8_t currentChannel );

    /*!
     * \brief CAD Done callback prototype.
     *
     * \param [IN] channelDetected    Channel Activity detected during the CAD
     */
    void ( *CadDone ) ( bool channelActivityDetected );
}RadioEvents_t;

​ RadioEvents_t的参数全部是函数指针,使用函数指针我认为是C语言中进阶高手的必经之路。到这可以推断出SX1278的驱动是基于事件触发回调函数的方式设计的,它的各类事件是通过DIO引脚通知的(后面会细说),在引脚的中断函数里调用这些函数指针指向的回调函数,最终做出相应的操作,这种用硬件通知事件的方式还是第一次见到,学习了。

​ 回调函数的实现后面再说,先对整个驱动做框架上的认知,这才是重点。

1.2 硬件初始化

​ 其实,我将SX1278的初始化分为硬件初始化和软件的初始化。硬件初始化就是有关GPIO引脚、SPI的初始化过程,说白了就是MCU能与SX1278进行通讯前期做的所有准备工作;而软件的初始化就要建立能与SX1278通过SPI进行通讯的基础上,它的任务是对其内部寄存器进行读写,使芯片处于默认的状态。

​ 软硬件初始化过程都位于下面一行代码中:

Radio.Init( &RadioEvents );

​ 不要小看这区区一行代码,里边其实有非常复杂的实现过程,只是因为漂亮的封装让我们觉得它很简单而已。

​ 首先就是这个 Radio ,这个东西是个全局变量,在 sx1276-board.c 中定义,它本身是 Radio_s 类型的,与上面的 RadioEvents_t 类似,它也是一个成员全是函数指针的结构体,这两个结构体都是在radio.h 中定义的,由于篇幅原因我把一些参数的注释给删了:

struct Radio_s
{
    void    ( *Init )( RadioEvents_t *events );
    RadioState_t ( *GetStatus )( void );
    void    ( *SetModem )( RadioModems_t modem );
    void    ( *SetChannel )( uint32_t freq );
    bool    ( *IsChannelFree )( RadioModems_t modem, uint32_t freq, int16_t rssiThresh );
    uint32_t ( *Random )( void );
    void    ( *SetRxConfig )( RadioModems_t modem, uint32_t bandwidth,
                              uint32_t datarate, uint8_t coderate,
                              uint32_t bandwidthAfc, uint16_t preambleLen,
                              uint16_t symbTimeout, bool fixLen,
                              uint8_t payloadLen,
                              bool crcOn, bool FreqHopOn, uint8_t HopPeriod,
                              bool iqInverted, bool rxContinuous );
    void    ( *SetTxConfig )( RadioModems_t modem, int8_t power, uint32_t fdev, 
                              uint32_t bandwidth, uint32_t datarate,
                              uint8_t coderate, uint16_t preambleLen,
                              bool fixLen, bool crcOn, bool FreqHopOn,
                              uint8_t HopPeriod, bool iqInverted, uint32_t timeout );
    bool    ( *CheckRfFrequency )( uint32_t frequency );
    uint32_t  ( *TimeOnAir )( RadioModems_t modem, uint8_t pktLen );
    void    ( *Send )( uint8_t *buffer, uint8_t size );
    void    ( *Sleep )( void );
    void    ( *Standby )( void );
    void    ( *Rx )( uint32_t timeout );
    void    ( *StartCad )( void );
    int16_t ( *Rssi )( RadioModems_t modem );
    void    ( *Write )( uint8_t addr, uint8_t data );
    uint8_t ( *Read )( uint8_t addr );
    void    ( *WriteBuffer )( uint8_t addr, uint8_t *buffer, uint8_t size );
    void    ( *ReadBuffer )( uint8_t addr, uint8_t *buffer, uint8_t size );
    void ( *SetMaxPayloadLength )( RadioModems_t modem, uint8_t max );

};

​ 这个结构体其实就是将一些中间件层的函数封装到一个结构体中,更加直观和方便调用。上面说了,Radio 中函数指针的赋值在 sx1276-board.c 中实现:

const struct Radio_s Radio =
{
    SX1276Init,
    SX1276GetStatus,
    SX1276SetModem,
    SX1276SetChannel,
    SX1276IsChannelFree,
    SX1276Random,
    SX1276SetRxConfig,
    SX1276SetTxConfig,
    SX1276CheckRfFrequency,
    SX1276GetTimeOnAir,
    SX1276Send,
    SX1276SetSleep,
    SX1276SetStby, 
    SX1276SetRx,
    SX1276StartCad,
    SX1276ReadRssi,
    SX1276Write,
    SX1276Read,
    SX1276WriteBuffer,
    SX1276ReadBuffer,
    SX1276SetMaxPayloadLength
};

​ 这里其实我觉得与我的想法产生分歧,sx1276-board 本身是板级支持包的文件,而这些函数都是中间件层的函数,这样就需要再引入 sx1276.h ,那么为什么不把 Radio 的定义放在sx1276.c 中呢?

​ 抛去这些,反正这下知道了,我们当前调用的 Radio.Init( &RadioEvents ) 其实最终是调用的 SX1276Init( &RadioEvents )

void SX1276Init( RadioEvents_t *events )
{
    uint8_t i;

    RadioEvents = events;

	SX1276TimerInit();         /* 初始化定时器 */
	SX1276IoInit();            /* 初始化GPIO, SPI */
    SX1276Reset();             /* 初始化Reset引脚且进行硬件复位*/
											         
    RxChainCalibration( );     /* 执行LF和HF波段的接收链校准 */

    SX1276SetOpMode( RF_OPMODE_SLEEP );

    SX1276IoIrqInit( DioIrq ); /* 初始化使用到的DIO引脚 */

		/* 给必要的寄存器赋初值 */
    for( i = 0; i < sizeof( RadioRegsInit ) / sizeof( RadioRegisters_t ); i++ )
    {
        SX1276SetModem( RadioRegsInit[i].Modem );
        SX1276Write( RadioRegsInit[i].Addr, RadioRegsInit[i].Value );
    }

    SX1276SetModem( MODEM_FSK );        /* 初始先是FSK模式, 后面会修改为LoRa模式 */

    SX1276.Settings.State = RF_IDLE;	/* 设置状态为空闲态 */
}

​ 这里面既包括硬件部分的初始化也包括软件部分的初始化,下面会介绍软件相关的,这里先说硬件。

  1. SX1276TimerInit(); 首先是Timer的初始化,sx1278的发送和接收可以设置一个超时时间,超过这个时间需要产生相应发送/接收超时的中断,这就需要定时器的参与,这里的定时器可以用硬件也可以用软件,我最终是采用了FreeRTOS提供的软件定时器,所以这部分在后面也会说。

  2. SX1276IoInit(); 初始化片选引脚、SPI等对应的GPIO,配置SPI的模式,都很常规,没什么好说的。

  3. SX1276IoIrqInit( DioIrq ); 这里可以说一下,这个函数实现了将MCU与sx1278的DIO0~DIO5连接的引脚初始化。这些引脚被配置成输入+外部中断模式,当对应的事件发生时,就会向通过对应DIOx向MCU报告,MCU检测到中断信号就会执行外部中断的中断服务函数。一个DIO可以被配置成不同的事件,这个可以在sx1278的官方手册中找到:
    image-20210429212523550

    DIO的映射关系可以通过配置 RegDioMapping1 和 RegDioMapping2 寄存器来实现,后续的配置Rx和Tx的代码中也会提到,到时候再说。

    这个函数的传入参数也有值得我学习的地方

    DioIrqHandler *DioIrq[] = { SX1276OnDio0Irq, SX1276OnDio1Irq, SX1276OnDio2Irq, SX1276OnDio3Irq, SX1276OnDio4Irq, NULL };

    DioIrq 是一个指向 void 类型的指针数组( DioIrqHandle 就是 void ),数组的内容是一些中断服务函数的函数指针,这样就可以通过 DioIrq[0]( ) 来访问 SX1276OnDio0Irq( ) 了,在MCU对应DIOx引脚的中断服务函数中可以看到这种访问方式,不可不谓精彩!


1.3软件初始化

​ 说完了硬件,软件部分其实就非常简单了,我们只需要看懂官方给的驱动文件是在干嘛就行了,有时候它为什么要这么做我也不明白,手册里写的也不清楚。

  1. RxChainCalibration( ); 摊牌了,完全不懂,但是也不用修改什么。
  2. SX1276SetOpMode( RF_OPMODE_SLEEP ); 设置芯片为休眠模式,只有休眠模式才能对寄存器进行配置。
  3. SX1276SetModem( MODEM_FSK ); 配置调制方式,这里驱动先使用FSK模式,后面会修改为LoRa。
  4. SX1276.Settings.State = RF_IDLE; State 也是一个重要的参数,这个驱动也引入了状态机的思想,共有4种状态:RF_IDLE, RF_RX_RUNNING, RF_TX_RUNNING, RF_CAD

关于对寄存器配置的操作中,我也学习到了新的东西,就拿改变opMode为例,通过查手册很容易知道是通过修改 RegOpMode 的 2-0 bit 来实现:

image-20210429222019245

在驱动中是这样实现的:

SX1276Write( REG_OPMODE, ( SX1276Read( REG_OPMODE ) & RF_OPMODE_MASK ) | opMode );

他首先读出了这个寄存器的原始内容,然后将它与上 RF_OPMODE_MASK ,RF_OPMODE_MASK 为 0xF8,这样在与操作之后该寄存器的2-0bit全为0了,最终在或上要修改的opMode就能实现 2-0bit 安全的修改。学到了学到了!

posted @ 2021-04-30 14:27  cnwanglu  阅读(3934)  评论(1编辑  收藏  举报