• 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 ,看一下它的定义:

    /*!
     * rief Radio driver callback functions
     */
    typedef struct
    {
        /*!
         * rief  Tx Done callback prototype.
         */
        void    ( *TxDone )( void );
        /*!
         * rief  Tx Timeout callback prototype.
         */
        void    ( *TxTimeout )( void );
        /*!
         * rief 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 );
        /*!
         * rief  Rx Timeout callback prototype.
         */
        void    ( *RxTimeout )( void );
        /*!
         * rief Rx Error callback prototype.
         */
        void    ( *RxError )( void );
        /*!
         * rief  FHSS Change Channel callback prototype.
         *
         * param [IN] currentChannel   Index number of the current channel
         */
        void ( *FhssChangeChannel )( uint8_t currentChannel );
    
        /*!
         * rief 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 安全的修改。学到了学到了!

  • 相关阅读:
    理解Java虚拟机——Java内存模型管理
    Java 使用fastjson 将 json字符串写到文件中去
    java 如何调用 linux or mac 命令行
    MacOS 编译 openjdk8 并导入 Clion 调试
    linux ls 命令超级详解
    小 Q 与树 (点分治)
    mysql 索引策略
    java中serialVersionUID作用
    通过源码分析Spring Security用户认证流程
    使用PowerMockRunner和Mockito编写单元测试用例详解
  • 原文地址:https://www.cnblogs.com/Irvingcode/p/14721879.html
Copyright © 2020-2023  润新知