STM32移植lorawan协议栈连接腾讯云物联网开发平台(IoT Explorer)

前言

在移植协议之前,先给大家科普一下Lora 和 lorawan 的区别。

LoRa 是LPWAN通信技术中的一种,是美国Semtech公司采用和推广的一种基于扩频技术的超远距离无线传输方案。这一方案改变了以往关于传输距离与功耗的折衷考虑方式为用户提供一种简单的能实现远距离、长电池寿命、大容量的系统,进而扩展传感网络。目前,LoRa 主要在全球免费频段运行,各个国家和地区不一样,中国区运行在470MHZ和779MHZ。

LoRaWAN是一个开放标准,它定义了基于LoRa芯片的LPWAN技术的通信协议。 LoRaWAN在数据链路层定义媒体访问控制(MAC),专为具有单一运营商的大型公共网络而设计,具体而言,每个节点将数据传输到网关或多个网关。然后网关将数据转发到网络服务器,在网络服务器上执行冗余检测,安全检查和消息调度,LoRaWAN现在由LoRa联盟维护(link)。

总体而言,LoRa仅包含链路层协议,并且非常适用于节点间的P2P通信;同时,LoRa模块(立创商城上20块左右)也比LoRaWAN(某宝30到40块)便宜一点;
LoRaWAN包含网络层,因此可以将信息发送到任何已连接到云平台的基站。只需将正确的天线连接到其插座,LoRaWAN模块就可以以不同的频率工作。

在这里插入图片描述
one picture wins thoustands words(如图所示)
LoRaWAN = MAC Layer
LoRa = PHY Layer
LoRa + LoRaWAN = LPWAN

正是因为lorawan,成千上万个节点的连网变得可能,本次移植的STM32 节点所连接的网关(SX1301)理论上能连接62500个节点。

开发环境的准备

Nucleo-F746ZG Board and ST Nucleo LoRa GW Module 如下图
在这里插入图片描述
Nucleo-L073R8 Board and ST Nucleo LoRa Sensor V2
在这里插入图片描述
因为 ST Nucleo LoRa Sensor V2 上的RHF0M003 模块已经集成了lorawan 协议了,只需MCU通过UART发送AT指令就能实现lorawan通讯(上文所说较贵的一类模块)

安信可ra-02(sx1278)
在这里插入图片描述
模块通过杜邦线连接开发板的SPI1 GPIO(PA0-reset脚, PA10-中断脚) ,VCC和地,连接好如下图所示:
在这里插入图片描述
PC通过串口连接网关,通过AT指令连接腾讯云物联网开发平台,指令如下
AT+PKTFWD=loragw.things.qcloud.com,1700,1700
AT+CH=0,486.3,A
AT+CH=1,486.5,A
AT+CH=2,486.7,A
AT+CH=3,486.9,A
AT+CH=4,487.1,B
AT+CH=5,487.3,B
AT+CH=6,487.5,B
AT+CH=7,487.7,B
AT+CH=8,OFF
AT+CH=9,OFF
AT+log=on (此条很重要,可以看到网关与云平台,与节点的通讯情况)
AT+Reset 复位网关,则开始服务器连接了。
以上就是节点移植前准备工作了

正文

初始化

void LORA_Init (LoRaMainCallback_t *callbacks, LoRaParam_t* LoRaParam )
{
 uint8_t devEui[] = LORAWAN_DEVICE_EUI;
  uint8_t joinEui[] = LORAWAN_JOIN_EUI;    //连接腾讯云平台用不到这个参数
  
  /* init the Tx Duty Cycle*/
  LoRaParamInit = LoRaParam;
  
  /* init the main call backs*/
  LoRaMainCallbacks = callbacks;
  
#if (STATIC_DEVICE_EUI != 1)
  LoRaMainCallbacks->BoardGetUniqueId( devEui );  
#endif
  
#if( OVER_THE_AIR_ACTIVATION != 0 )

  PPRINTF( "OTAA\n\r"); 
  PPRINTF( "DevEui= %02X-%02X-%02X-%02X-%02X-%02X-%02X-%02X\n\r", HEX8(devEui));
  PPRINTF( "AppEui= %02X-%02X-%02X-%02X-%02X-%02X-%02X-%02X\n\r", HEX8(joinEui));
  PPRINTF( "AppKey= %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X\n\r", HEX16(AppKey));
#else

#if (STATIC_DEVICE_ADDRESS != 1)
  // Random seed initialization
  srand1( LoRaMainCallbacks->BoardGetRandomSeed( ) );
  // Choose a random device address
  DevAddr = randr( 0, 0x01FFFFFF );
#endif
  PPRINTF( "ABP\n\r"); 
  PPRINTF( "DevEui= %02X-%02X-%02X-%02X-%02X-%02X-%02X-%02X\n\r", HEX8(devEui));
  PPRINTF( "DevAdd=  %08X\n\r", DevAddr) ;
  PPRINTF( "NwkSKey= %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X\n\r", HEX16(NwkSEncKey));
  PPRINTF( "AppSKey= %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X\n\r", HEX16(AppSKey));
#endif
.
.
.
#elif defined( REGION_CN470 )
  LoRaMacInitialization( &LoRaMacPrimitives, &LoRaMacCallbacks, LORAMAC_REGION_CN470 );
 .
 .
 .
  mibReq.Param.DevEui = devEui;
  mibReq.Param.AppKey = AppKey;
  mibReq.Param.NwkKey = NwkKey; //这几个参数很重要一定要设对,我就  
  mibReq.Param.Class= CLASS_A;  // 因为没设Nwkkey 导致入不网,
  //Lorawan 1.0.x 也要设置,具体原因在下文会详细分析
  .
  .
  .
  LoRaMacStart( );
}

可以看出,初始化就是根据我们设置的一些宏 如入网方式,使用地区等等进行初始化。

入网

void LORA_Join( void)
{
    MlmeReq_t mlmeReq;
  
    mlmeReq.Type = MLME_JOIN;
    mlmeReq.Req.Join.Datarate = LoRaParamInit->TxDatarate;
  
    JoinParameters = mlmeReq.Req.Join;

#if( OVER_THE_AIR_ACTIVATION != 0 )
    LoRaMacMlmeRequest( &mlmeReq );    //腾讯云物联网平台要求空中入网的方式,所以定义了这个宏为1,于是调用了这个函数;                                                                       
#else
.
.
.
#endif
}

LoRaMacStatus_t LoRaMacMlmeRequest( MlmeReq_t* mlmeRequest )
{
    LoRaMacStatus_t status = LORAMAC_STATUS_SERVICE_UNKNOWN;
    MlmeConfirmQueue_t queueElement;
    uint8_t macCmdPayload[2] = { 0x00, 0x00 };

    if( mlmeRequest == NULL )
    {
        return LORAMAC_STATUS_PARAMETER_INVALID;
    }
    if( LoRaMacIsBusy( ) == true )
    {
        return LORAMAC_STATUS_BUSY;
    }
    if( LoRaMacConfirmQueueIsFull( ) == true )
    {
        return LORAMAC_STATUS_BUSY;
    }
    .
    .
    .
    switch( mlmeRequest->Type )   //通过入参来判断我们这是是入网请求MLME_JOIN
    {
        case MLME_JOIN:
        {
            if( ( MacCtx.MacState & LORAMAC_TX_DELAYED ) == LORAMAC_TX_DELAYED )
            {
                return LORAMAC_STATUS_BUSY;
            }

            ResetMacParameters( );

            MacCtx.NvmCtx->MacParams.ChannelsDatarate = RegionAlternateDr( MacCtx.NvmCtx->Region, mlmeRequest->Req.Join.Datarate, ALTERNATE_DR );

            queueElement.Status = LORAMAC_EVENT_INFO_STATUS_JOIN_FAIL;

            status = SendReJoinReq( JOIN_REQ );     //我们再进去看这个函数

            if( status != LORAMAC_STATUS_OK )    
            {
					PPRINTF( "joinreq ok\n\r");
                // Revert back the previous datarate ( mainly used for US915 like regions )
                MacCtx.NvmCtx->MacParams.ChannelsDatarate = RegionAlternateDr( MacCtx.NvmCtx->Region, mlmeRequest->Req.Join.Datarate, ALTERNATE_DR_RESTORE );
            }
						else
						{
							PPRINTF( "joinreq not ok\n\r");
						}
            break;
        }
        .
        .
        .
        return status;
}

LoRaMacStatus_t SendReJoinReq( JoinReqIdentifier_t joinReqType )
{
    LoRaMacStatus_t status = LORAMAC_STATUS_OK;
    LoRaMacHeader_t macHdr;
    macHdr.Value = 0;
    bool allowDelayedTx = true;

    // Setup join/rejoin message
    switch( joinReqType )
    {
        case JOIN_REQ:
        {
         .
         .
         .
        }
    }

    // Schedule frame
    status = ScheduleTx( allowDelayedTx );  //再看这个函数
    return status;
}
//谜底快解开了....
static LoRaMacStatus_t ScheduleTx( bool allowDelayedTx )
{
   LoRaMacStatus_t status = LORAMAC_STATUS_PARAMETER_INVALID;
    TimerTime_t dutyCycleTimeOff = 0;
    NextChanParams_t nextChan;
    size_t macCmdsSize = 0;

    // Update back-off
    CalculateBackOff( MacCtx.NvmCtx->LastTxChannel );
    .
    .
    .
     if( MacCtx.NvmCtx->NetworkActivation == ACTIVATION_TYPE_NONE )
    {
        MacCtx.RxWindow1Delay = MacCtx.NvmCtx->MacParams.JoinAcceptDelay1 +   MacCtx.RxWindow1Config.WindowOffset;
        MacCtx.RxWindow2Delay = MacCtx.NvmCtx->MacParams.JoinAcceptDelay2 + MacCtx.RxWindow2Config.WindowOffset;
	    PPRINTF( "MacCtx.RxWindow1Delay is %d\n\r",MacCtx.RxWindow1Delay);
    }
     //这里也重点说一下,很多人入不了网的原因是因为接收窗口的时间不对,从发出入 
    //请求到从网关接收入网应答这个时间间隔是5秒,这个和腾讯云物联网平台的工程师确
    //确认过
 .
 .
 .
     // Secure frame
     //谜底就在这个函数里
    LoRaMacStatus_t retval = SecureFrame( MacCtx.NvmCtx->MacParams.ChannelsDatarate, MacCtx.Channel ); 
    if( retval != LORAMAC_STATUS_OK )
    {
        return retval;
    }

    // Try to send now
    return SendFrameOnChannel( MacCtx.Channel );  
}

== 之前一直入网不成功,联系腾讯云的夏云飞老师,得到的回复是MIC错误,夏老师说MIC错误只有两个原因,一是key错了,二是算法错了。 ==

我确信算法不会错,因为我没有改过源码,所以我再次确认了AppKey 和 devEui,没错。经过了一段时间的折腾和腾讯云的两位大神夏云飞老师和twowinter(真名不知道啊,哈哈)的指导和提示,再次去看代码,答案如下

static LoRaMacStatus_t SecureFrame( uint8_t txDr, uint8_t txCh )
{
    LoRaMacCryptoStatus_t macCryptoStatus = LORAMAC_CRYPTO_ERROR;
    uint32_t fCntUp = 0;

    switch( MacCtx.TxMsg.Type )
    {
        case LORAMAC_MSG_TYPE_JOIN_REQUEST:  
           //我们来看看下面的函数 LoRaMacCryptoPrepareJoinRequest
            macCryptoStatus = LoRaMacCryptoPrepareJoinRequest( &MacCtx.TxMsg.Message.JoinReq );
            if( LORAMAC_CRYPTO_SUCCESS != macCryptoStatus )
            {
                return LORAMAC_STATUS_CRYPTO_ERROR;
            }
            MacCtx.PktBufferLen = MacCtx.TxMsg.Message.JoinReq.BufSize;
            break;
      .
      .
      .
    return LORAMAC_STATUS_OK;
}

LoRaMacCryptoStatus_t LoRaMacCryptoPrepareJoinRequest( LoRaMacMessageJoinRequest_t* macMsg )     
{
    if( macMsg == 0 )
    {
        return LORAMAC_CRYPTO_ERROR_NPE;
    }
    //这里加密用的是NWK_KEY,  但是我没有设置,所以加密错误,这就是原因,我也打印再次确认过,就是nwk_key。 破案了
    KeyIdentifier_t micComputationKeyID = NWK_KEY;  

    // Add device nonce
#if ( USE_RANDOM_DEV_NONCE == 1 )
    uint32_t devNonce = 0;
    SecureElementRandomNumber( &devNonce );
    CryptoCtx.NvmCtx->DevNonce = devNonce;
#else
    CryptoCtx.NvmCtx->DevNonce++;
#endif
    CryptoCtx.EventCryptoNvmCtxChanged( );
    macMsg->DevNonce = CryptoCtx.NvmCtx->DevNonce;

#if( USE_LRWAN_1_1_X_CRYPTO == 1 )   //这里是USE_LRWAN_1_1_X 的宏,但是我的是1_0_X, 所以为零
    // Derive lifetime session keys
    if( DeriveLifeTimeSessionKey( J_S_INT_KEY, macMsg->DevEUI ) != LORAMAC_CRYPTO_SUCCESS )
    {
        return LORAMAC_CRYPTO_ERROR;
    }
    if( DeriveLifeTimeSessionKey( J_S_ENC_KEY, macMsg->DevEUI ) != LORAMAC_CRYPTO_SUCCESS )
    {
        return LORAMAC_CRYPTO_ERROR;
    }
#endif

    // Serialize message
    if( LoRaMacSerializerJoinRequest( macMsg ) != LORAMAC_SERIALIZER_SUCCESS )
    {
        return LORAMAC_CRYPTO_ERROR_SERIALIZER;
    }

    // Compute mic   这里计算用到了上面的nwk_key,破案了
    if( SecureElementComputeAesCmac( NULL, macMsg->Buffer, ( LORAMAC_JOIN_REQ_MSG_SIZE - LORAMAC_MIC_FIELD_SIZE ), micComputationKeyID, &macMsg->MIC ) != SECURE_ELEMENT_SUCCESS )
    {
        return LORAMAC_CRYPTO_ERROR_SECURE_ELEMENT_FUNC;
    }

    // Reserialize message to add the MIC
    if( LoRaMacSerializerJoinRequest( macMsg ) != LORAMAC_SERIALIZER_SUCCESS )
    {
        return LORAMAC_CRYPTO_ERROR_SERIALIZER;
    }

    return LORAMAC_CRYPTO_SUCCESS;
}

发送与接收

发送和接收因为没遇到什么困难,直接调用发送函数就行,因为是class A 设备,会在发送后,打开接收窗口接收;贴一个发送函数吧

bool LORA_send(lora_AppData_t* AppData, LoraConfirm_t IsTxConfirmed)
{
    McpsReq_t mcpsReq;
    LoRaMacTxInfo_t txInfo;
  
    /*if certification test are on going, application data is not sent*/
    if (certif_running() == true)
    {
			PPRINTF("certif_run\r\n");
      return false;
    }
    
    if( LoRaMacQueryTxPossible( AppData->BuffSize, &txInfo ) != LORAMAC_STATUS_OK )
    {
        // Send empty frame in order to flush MAC commands
        mcpsReq.Type = MCPS_UNCONFIRMED;
        mcpsReq.Req.Unconfirmed.fBuffer = NULL;
        mcpsReq.Req.Unconfirmed.fBufferSize = 0;
        mcpsReq.Req.Unconfirmed.Datarate = LoRaParamInit->TxDatarate;
    }
    else
    {
        if( IsTxConfirmed == LORAWAN_UNCONFIRMED_MSG )
        {
            mcpsReq.Type = MCPS_UNCONFIRMED;
            mcpsReq.Req.Unconfirmed.fPort = AppData->Port;
            mcpsReq.Req.Unconfirmed.fBufferSize = AppData->BuffSize;
            mcpsReq.Req.Unconfirmed.fBuffer = AppData->Buff;
            mcpsReq.Req.Unconfirmed.Datarate = LoRaParamInit->TxDatarate;
        }
        else
        {
            mcpsReq.Type = MCPS_CONFIRMED;
            mcpsReq.Req.Confirmed.fPort = AppData->Port;
            mcpsReq.Req.Confirmed.fBufferSize = AppData->BuffSize;
            mcpsReq.Req.Confirmed.fBuffer = AppData->Buff;
            mcpsReq.Req.Confirmed.NbTrials = 8;
            mcpsReq.Req.Confirmed.Datarate = LoRaParamInit->TxDatarate;
        }
    }
    if( LoRaMacMcpsRequest( &mcpsReq ) == LORAMAC_STATUS_OK )
    {
        return false;
    }
    return true;
}

还是贴个图吧
在这里插入图片描述
以上就是全部内容。
最后再次感谢腾讯云的夏云飞老师和twowinter。
大家如果对Lora有兴趣可以看看twowinter 的文章,会很有帮助的,链接如下
link
http://blog.csdn.net/iotisan/

代码下载链接
link
https://download.csdn.net/download/lintonxie/12265246

更多推荐