易智联LM401模块,采用STM32WLE5CBU6单片机,采用ARM Cortex-M4内核,自带128K flash,48K SRAM,单片机内嵌Lora通信模块,通信核心为SX1262。
该模块的调制方式可以设置为Lora(默认)和FSK两种,其最大发射功率为22dBm。LM401模块的额定工作电压为1.8V至3.6V,内置32.768KHz晶振,采用30PinSMT封装,并且留有UASRT、IIC、SPI、ADC等接口,引脚说明如下:
二、硬件设计知识
由于模块内置32.768K和32M两颗晶振,所以电路设计时最小系统为电源、复位和射频天线、SWD四大部分。以下是本人设计的一个测试版的电路图和PCB
软件结构(以PINGPONG范例程序为例)
在上述例程中,最重要的就是Lora应用程序,"app_subghz_phy.c"文件的功能为Lora的初始化,"subghz_phy_app.c"的功能是Lora的通信设置及其信息处理函数。最下面的LM401的两个C文件是为了适配其配套的开发板对应的LED和按键功能。
main.c文件里面就只有2个函数,分别是“int main(void)”和”SystemClock_Config()”,第二个函数的功能是系统时钟配置,在main函数中被调用。main函数及其功能如下:
int main(void)
{
HAL_Init();//HAL库初始化
SystemClock_Config();//系统时钟设置
MX_GPIO_Init();//GPIO初始化
MX_SubGHz_Phy_Init();//Lora初始化,这个函数在"app_subghz_phy.c"文件中。
while (1)
{
MX_SubGHz_Phy_Process();//系统进程文件,在Lora应用程序中跑了一个小型的操作系统,通过不断获取系统的状态标志位来触发不同的功能函数。这个函数在"app_subghz_phy.c"文件中
}
}
在main函数中,通常在MX_SubGHz_Phy_Init();函数后添加个人应用函数。如果有需要不断循环执行的函数如串口接收解析函数则添加在MX_SubGHz_Phy_Process();函数后。
这个文件内只有2个函数,它们分别是“void MX_SubGHz_Phy_Init(void)”和“void MX_SubGHz_Phy_Process(void)”。
void MX_SubGHz_Phy_Init(void)函数的功能是:调用“SystemApp_Init();”函数和“SubghzApp_Init();”函数进行系统初始化和Lora功能初始化。“SystemApp_Init();”函数在“sys_app.c”文件中,“SubghzApp_Init();”函数在“subghz_phy_app.c”文件中。
void MX_SubGHz_Phy_Process(void)函数的功能是调用“UTIL_SEQ_Run (UTIL_SEQ_DEFAULT)”函数不断获取Lora应用的状态进而执行不同的功能函数。
这个文件是Lora应用函数,其包括了Lora通信的属性设置、应用函数。因为该文件比较中交所以我会讲解其H文件。
3.3.1subghz_phy_app.h文件讲解
#define USE_MODEM_LORA 1 //这两行是对调制方式的宏定义
#define USE_MODEM_FSK 0
#define REGION_CN470 //声明当前模块的通信规格
#if defined( REGION_AS923 ) //依据不同的通信规格设置系统通信频率
#define RF_FREQUENCY 923000000 /* Hz */
#elif defined( REGION_AU915 )
#define RF_FREQUENCY 915000000 /* Hz */
#elif defined( REGION_CN470 )
#define RF_FREQUENCY 490000000 /* Hz */
#elif defined( REGION_CN779 )
#define RF_FREQUENCY 779000000 /* Hz */
#elif defined( REGION_EU433 )
#define RF_FREQUENCY 433000000 /* Hz */
#elif defined( REGION_EU868 )
#define RF_FREQUENCY 868000000 /* Hz */
#elif defined( REGION_KR920 )
#define RF_FREQUENCY 920000000 /* Hz */
#elif defined( REGION_IN865 )
#define RF_FREQUENCY 865000000 /* Hz */
#elif defined( REGION_US915 )
#define RF_FREQUENCY 915000000 /* Hz */
#elif defined( REGION_RU864 )
#define RF_FREQUENCY 864000000 /* Hz */
#else
#error "Please define a frequency band in the compiler options."
#endif
#define TX_OUTPUT_POWER 22 /* 设置发射功率,单位为dBm */
/*以下语句的功能为根据系统的解调方式设置对应的通信属性,包括了带宽、扩频因子、编码率、数据长度等属性。*/
#if (( USE_MODEM_LORA == 1 ) && ( USE_MODEM_FSK == 0 ))//Lora属性设置。
#define LORA_BANDWIDTH 0 /* [0: 125 kHz,1:250 kHz, 2:500 kHz,3:Reserved]*/
#define LORA_SPREADING_FACTOR 10 /* [SF7..SF12] */
#define LORA_CODINGRATE 1 /* [1: 4/5, 2: 4/6, 3: 4/7, 4: 4/8] */
#define LORA_PREAMBLE_LENGTH 8 /* Same for Tx and Rx */
#define LORA_SYMBOL_TIMEOUT 5 /* Symbols */
#define LORA_FIX_LENGTH_PAYLOAD_ON false
#define LORA_IQ_INVERSION_ON false
#elif (( USE_MODEM_LORA == 0 ) && ( USE_MODEM_FSK == 1 ))//FSK属性设置
#define FSK_FDEV 25000 /* Hz */
#define FSK_DATARATE 50000 /* bps */
#define FSK_BANDWIDTH 50000 /* Hz */
#define FSK_PREAMBLE_LENGTH 5 /* Same for Tx and Rx */
#define FSK_FIX_LENGTH_PAYLOAD_ON false
#else
#error "Please define a modem in the compiler subghz_phy_app.h."
#endif
#define PAYLOAD_LEN 32 //设置用户数据长度
void SubghzApp_Init(void); //Lora模块初始化函数声明
以上就是subghz_phy_app.h文件的所有内容及其功能。
3.3.2 subghz_phy_app.c文件讲解
typedef enum
{
RX,
RX_TIMEOUT,
RX_ERROR,
TX,
TX_TIMEOUT,
} States_t;//设置系统状态结构体
#define RX_TIMEOUT_VALUE 5000 //设置接收超时时间
#define TX_TIMEOUT_VALUE 3000 //设置发送超时时间
#define PING "PING" //定义发送字符串
#define PONG "PONG" //定义发送字符串
#define MAX_APP_BUFFER_SIZE 255 //设置最大数据长度
#if (PAYLOAD_LEN > MAX_APP_BUFFER_SIZE) //判断最大数据长度是否大于用户数据长度
#error PAYLOAD_LEN must be less or equal than MAX_APP_BUFFER_SIZE
#endif
#define RX_TIME_MARGIN 200 //定义发送前等待时间
#define FSK_AFC_BANDWIDTH 83333 定义AFC带宽
#define LED_PERIOD_MS 200 //定义LED翻转时间
static RadioEvents_t RadioEvents; //静态定义状态事件RadioEvents_t类型结构体
static States_t State = RX; //声明系统初始状态为接收完成
static uint8_t BufferRx[MAX_APP_BUFFER_SIZE]; //定义接收数组
static uint8_t BufferTx[MAX_APP_BUFFER_SIZE]; //定义发送数组
uint16_t RxBufferSize = 0; //定义接收数据长度变量
int8_t RssiValue = 0; //定义最后接收的信号强度变量
int8_t SnrValue = 0; //定义最后接收的信噪比变量
static UTIL_TIMER_Object_t timerLed; //声明定时器设置结构体
bool isMaster = true;//声明设备标识(发送端则为true,接收端为false)
static int32_t random_delay;//随机延时变量,确保将2个设备同步及减少同步时间
/* 以下几行是将后面定义的函数提前声明以便文件内随时调用 */
static void OnRxDone(uint8_t *payload, uint16_t size, int16_t rssi, int8_t LoraSnr_FskCfo);
static void OnTxDone(void);
static void OnTxTimeout(void);
static void OnRxTimeout(void);
static void OnRxError(void);
static void OnledEvent(void *context);
static void PingPong_Process(void);
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)//按键IO外部中断回调函数
{
switch (GPIO_Pin)
{
case BUTTON_SW1_PIN:
BSP_LED_Toggle(LED_BLUE) ;//LED状态取反,在LM401驱动文件内定义
HAL_Delay(20); //HAL库自带延时函数,单位为ms
BSP_LED_Toggle(LED_BLUE) ;
APP_PRINTF("BUTTON SW1\r\n");//日志发送函数,通过LP_USART输出
break;
case BUTTON_SW2_PIN:
BSP_LED_Toggle(LED_GREEN) ;
HAL_Delay(20);
BSP_LED_Toggle(LED_GREEN) ;
APP_PRINTF("BUTTON SW2\r\n");
break;
case BUTTON_SW3_PIN:
BSP_LED_Toggle(LED_RED) ;
HAL_Delay(20);
BSP_LED_Toggle(LED_RED) ;
APP_PRINTF("BUTTON SW3\r\n");
break;
default:
APP_PRINTF("Unkonw Button\r\n");
break;
}
}
void SubghzApp_Init(void) //Lora模块初始化函数
{
//创建一个定时器,第一个参数为结构体,第二个参数为执行周期,第3个参数为定时器属性(一次性的还是永久的),第4个参数为定时器溢出回调函数,第五个不知
UTIL_TIMER_Create(&timerLed, 0xFFFFFFFFU, UTIL_TIMER_ONESHOT, OnledEvent, NULL);
UTIL_TIMER_SetPeriod(&timerLed, LED_PERIOD_MS);//设置定时器初值,第二个参数为定时器溢出时间
UTIL_TIMER_Start(&timerLed);//定时器启动
/* 声明Lora通信事件回调函数 */
RadioEvents.TxDone = OnTxDone;//发送完成回调函数
RadioEvents.RxDone = OnRxDone;//接收完成回调函数
RadioEvents.TxTimeout = OnTxTimeout;//发送超时回调函数
RadioEvents.RxTimeout = OnRxTimeout;//接收超时回调函数
RadioEvents.RxError = OnRxError;//接受错误回调函数
Radio.Init(&RadioEvents);//Lora事件初始化函数
Radio.SetChannel(RF_FREQUENCY);//设置Lora通信频率
//设置发送参数
Radio.SetTxConfig(MODEM_LORA, TX_OUTPUT_POWER, 0, LORA_BANDWIDTH,// 模式、发送功率、0、信号带宽
LORA_SPREADING_FACTOR, LORA_CODINGRATE,//扩频因子、编码率
LORA_PREAMBLE_LENGTH, LORA_FIX_LENGTH_PAYLOAD_ON,//前导长度、负载长度
true, 0, 0, LORA_IQ_INVERSION_ON, TX_TIMEOUT_VALUE);//true, 0, 0,IQ 采样反转,发送超时时间
//设置接收参数
Radio.SetRxConfig(MODEM_LORA, LORA_BANDWIDTH, LORA_SPREADING_FACTOR,// 模式、信号带宽、扩频因子
LORA_CODINGRATE, 0, LORA_PREAMBLE_LENGTH,//编码率、0、前导长度
LORA_SYMBOL_TIMEOUT, LORA_FIX_LENGTH_PAYLOAD_ON,//超时符号数、负载长度
0, true, 0, 0, LORA_IQ_INVERSION_ON, true);//0, true, 0, 0,IQ 采样反转,true
Radio.SetMaxPayloadLength(MODEM_LORA, MAX_APP_BUFFER_SIZE);//设置最大负载长度
/* LED初始化*/
BSP_LED_Init(LED_GREEN);
BSP_LED_Init(LED_RED);
BSP_LED_Init(LED_BLUE);
/* 按键初始化,入口参数为按键编号、按键工作模式(1表示会触发外部中断)*/
BSP_PB_Init(BUTTON_SW1, BUTTON_MODE_EXTI);
BSP_PB_Init(BUTTON_SW2, BUTTON_MODE_EXTI);
BSP_PB_Init(BUTTON_SW3, BUTTON_MODE_EXTI);
random_delay = (Radio.Random()) >> 22; //计算同步的随机延时
memset(BufferTx, 0x0, MAX_APP_BUFFER_SIZE);//清空接收数组
Radio.Rx(RX_TIMEOUT_VALUE + random_delay);//将 Lora 设置为接收等待模式
UTIL_SEQ_RegTask((1 << CFG_SEQ_Task_SubGHz_Phy_App_Process), UTIL_SEQ_RFU, PingPong_Process);//任务进程注册
}
static void OnTxDone(void)//发送完成回调函数
{
State = TX;//系统状态置位
UTIL_SEQ_SetTask((1 << CFG_SEQ_Task_SubGHz_Phy_App_Process), CFG_SEQ_Prio_0);
//任务进程注册
}
/* 接收完成回调函数*/
static void OnRxDone(uint8_t *payload, uint16_t size, int16_t rssi, int8_t LoraSnr_FskCfo)
{
/
SnrValue = LoraSnr_FskCfo;//记录有效载荷Lora的信噪比
State = RX; //系统状态置位
memset(BufferRx, 0, MAX_APP_BUFFER_SIZE);//清零接收数组
RxBufferSize = size;//获取接收内容的长度,储存在RxBufferSize中
if (RxBufferSize <= MAX_APP_BUFFER_SIZE)//判断接受数据长度是否超长
{
memcpy(BufferRx, payload, RxBufferSize);//将Lora接受的数据从payload存在数组BufferRx中
}
RssiValue = rssi;//记录接收信号强度
for (int i = 0; i < PAYLOAD_LEN; i++)//打印输出接收内容
{
APP_LOG(TS_OFF, VLEVEL_H, "%02X", BufferRx[i]); if (i % 16 == 15)
{
APP_LOG(TS_OFF, VLEVEL_H, "\n\r");//输出2位16禁止的内容,右对齐
}
}
APP_LOG(TS_OFF, VLEVEL_H, "\n\r");//输出换行
UTIL_SEQ_SetTask((1 << CFG_SEQ_Task_SubGHz_Phy_App_Process), CFG_SEQ_Prio_0);
//任务进程注册
}
static void OnTxTimeout(void)//发送超时回调函数
{
State = TX_TIMEOUT;//系统状态置位
UTIL_SEQ_SetTask((1 << CFG_SEQ_Task_SubGHz_Phy_App_Process), CFG_SEQ_Prio_0);
//任务进程注册
}
static void OnRxTimeout(void)//接收超时回调函数
{
State = RX_TIMEOUT;//系统状态置位
UTIL_SEQ_SetTask((1 << CFG_SEQ_Task_SubGHz_Phy_App_Process), CFG_SEQ_Prio_0);
//任务进程注册
}
static void OnRxError(void)//接收错误回调函数
{
State = RX_ERROR;//系统状态置位
UTIL_SEQ_SetTask((1 << CFG_SEQ_Task_SubGHz_Phy_App_Process), CFG_SEQ_Prio_0);
//系统状态置位
}
static void PingPong_Process(void)//进程处理函数
{
Radio.Sleep(); //将Lora模块设置为待机状态
switch (State)
{
case RX://如果接收完成
if (isMaster == true)//如果设备是发送端
{
if (RxBufferSize > 0)//接收到内容
{
//strncmp(s1,s2,n),比较两个数组的前n个字符,返回一个个大于、等于或小于零的整数,等于0时表示相等
//如果接收到的数据是"PONG"
if (strncmp((const char *)BufferRx, "PONG", sizeof(PONG) - 1) == 0)
{
UTIL_TIMER_Stop(&timerLed);//停止并删除计时器
BSP_LED_Off(LED_GREEN);//关闭绿灯
BSP_LED_Toggle(LED_RED);//红灯状态翻转
/* 延时一段时间,长度为随机延时+系统唤醒延时*/
HAL_Delay(Radio.GetWakeupTime() + RX_TIME_MARGIN);
memcpy(BufferTx, PING, sizeof(PING) - 1);//将发送数据写入数组
Radio.Send(BufferTx, PAYLOAD_LEN);//发送BufferTx数组中的数据
}
//如果接收到的数据是"PING"
else if (strncmp((const char *)BufferRx, "PING",sizeof(PING)- 1) == 0)
{
isMaster = false;//将设备状态改为接收端
Radio.Rx(RX_TIMEOUT_VALUE);//将 Lora 设置为接收等待模式
}
else //如果接收的数据不是PING或PONG
{
isMaster = true;//将设备状态改为发送端
Radio.Rx(RX_TIMEOUT_VALUE);//将 Lora 设置为接收等待模式
}
}
}
else//如果设备是接收端
{
if (RxBufferSize > 0)//如果接收到数据
{
//如果接收到的数据是"PING"
if (strncmp((const char *)BufferRx, PING, sizeof(PING) - 1) == 0)
{
UTIL_TIMER_Stop(&timerLed);//停止并删除计时器
BSP_LED_Off(LED_RED);//关闭红灯
BSP_LED_Toggle(LED_GREEN);//绿灯状态翻转
/* 延时一段时间,长度为随机延时+系统唤醒延时*/
HAL_Delay(Radio.GetWakeupTime() + RX_TIME_MARGIN);
memcpy(BufferTx, PONG, sizeof(PONG) - 1);//将发送数据写入数组
Radio.Send(BufferTx, PAYLOAD_LEN);//发送BufferTx数组中的数据
}
else //如果接收的不是"PING"
{
isMaster = true;//将设备状态改为发送端
Radio.Rx(RX_TIMEOUT_VALUE);//将 Lora 设置为接收等待模式
}
}
}
break;
case TX://如果发送完成
Radio.Rx(RX_TIMEOUT_VALUE);//将 Lora 设置为接收等待模式
break;
case RX_TIMEOUT://如果接收超时
//LOW_POWER_DISABLE==1表示没有打开
#if defined (LOW_POWER_DISABLE) && (LOW_POWER_DISABLE == 0)//如果开启了低功耗
UTIL_TIMER_Stop(&timerLed);
BSP_LED_Off(LED_RED) ;
BSP_LED_Off(LED_GREEN) ;
Radio.Sleep();//将模块进入休眠状态
break;
#endif
case RX_ERROR://如果接收错误
if (isMaster == true)//如果是发送端
{
/* 延时一段时间,长度为随机延时+系统唤醒延时*/
HAL_Delay(Radio.GetWakeupTime() + RX_TIME_MARGIN + random_delay);
//将要发送的数据从字符串"PING"储存在数组BufferTx中
memcpy(BufferTx, PING, sizeof(PING) - 1);//将发送数据写入数组
Radio.Send(BufferTx, PAYLOAD_LEN); //发送BufferTx数组中的数据
}
else//如果是接收端
{
Radio.Rx(RX_TIMEOUT_VALUE);//将 Lora 设置为接收等待模式
}
break;
case TX_TIMEOUT://如果发送超时
Radio.Rx(RX_TIMEOUT_VALUE);将 Lora 设置为接收等待模式
break;
default://如果是其他状态就直接退出
break;
}
}
static void OnledEvent(void *context)//定时器溢出回调函数
{
BSP_LED_Toggle(LED_GREEN);//LED状态翻转
BSP_LED_Toggle(LED_RED);
UTIL_TIMER_Start(&timerLed);//更新定时器初值并重新启动定时器
}
本程序的主要功能是主机通过按键触发Lora通信功能向从机发送指定的字符串,发送完成后LED2状态切换,在接收到从机发送的内容后再切换LED3的状态。从机在接收到相应内容后LED3状态切换,之后再将接收到的内容发送给主机,发送完成之后LED2状态切换。
本程序是在PINGPONG范例程序的基础上修改而来并做了如下修改:
(1)通过USART1进行通信,并进行了重定向,使printf函数可以使用。
(2)按键功能修改为触发Lora发送。
(3)修改发送和接收回调函数及进程处理函数定义。
4.1、main.c文件讲解
main.c文件在PINGPONG范例程序的基础上在main函数中添加了串口接收数据处理内容。当接收到符合要求的数据时会使LED3的状态翻转。以下是main函数内容:
int main(void)
{
int len=0;//串口接收数据长度
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_SubGHz_Phy_Init();//Lora初始化
MX_USART1_UART_Init();//串口1初始化函数
while (1)
{
MX_SubGHz_Phy_Process();
if(USART_RX_STA&0x8000)//如果USART_RX_STA的最高位为1表示接收到了数据
{
len=USART_RX_STA&0x3fff;//USART_RX_STA的低14位为数据长度
USART_RX_STA=0;//接收完成标志位清零
//以下是串口通信协议解析,接收数据的最后2个字节是否都为0x01
if((USART_RX_BUF[len-2]==0x01)&&(USART_RX_BUF[len-1]==0x01))
{
BSP_LED_Toggle(LED_RED);//如果符合通信协议就翻转红色LED
}
}
}
}
4.2、usart.c文件讲解
在串口初始化文件中新增了串口1的设置程序,并且增加了串口1的中断服务函数。在该函数中定义的串口接收标志函数、串口接收数组以及串口1初始化结构体句柄都应该在usart.h中进行外部声明允许在外部被调用。因为串口1的初始化函数与串口2基本无差别,所以就不再赘述
以下是串口1重定向部分,如果在各个C文件中有使用到printf函数,则该文件一定要引用“stdio”这个头文件。特别注意,重定向中的寄存器名称会根据芯片不同而不同:
int fputc(int ch, FILE *f)
{
while((USART1->ISR&0X40)==0)
{}//循环发送,直到发送完毕
USART1->TDR = (unsigned char) ch;
return ch;
}
以下是串口1的中断服务函数:
void USART1_IRQHandler(void)
{
unsigned char Res;
if((__HAL_UART_GET_FLAG(&huart1,UART_FLAG_RXNE)!=RESET)) //接收中断
{
Res=USART1->RDR; //读取接收数据
if((USART_RX_STA&0x8000)==0)//最高位为0,接收未完成
{
if(USART_RX_STA&0x4000)//次高位为1,接收到了0x0d
{
if(Res!=0x0a)USART_RX_STA=0;//如果加收的不是0x0a,接收错误
else USART_RX_STA|=0x8000; //接收完成了
}
else //次高位为0,还没收到0X0D
{
if(Res==0x0d)USART_RX_STA|=0x4000;//如果接收到0x0D,次高位置1
else
{
USART_RX_BUF[USART_RX_STA&0X3FFF]=Res ;//接收数据赋值给数组
USART_RX_STA++;//数据长度加1
if(USART_RX_STA>(USART_REC_LEN-1))USART_RX_STA=0;//长度过长
}
}
}
}
HAL_UART_IRQHandler(&huart1);
}
subghz_phy_app.c文件在PINGPONG范例程序的基础上进行了修改。主要修改有以下几个地方:
1、按键触发外部中断添加了主从机判断,只有主机模式才有效;
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)//按键IO外部中断回调函数
{
if (isMaster == true)//主从机判断
{
switch (GPIO_Pin)
{
case BUTTON_SW1_PIN:
memcpy(BufferTx, "TEST BUTTON\r\n", sizeof("TEST BUTTON\r\n")); Radio.Send(BufferTx,sizeof("TEST BUTTON\r\n")-1);
break;
case BUTTON_SW2_PIN:
memcpy(BufferTx, "TEST BUTTON\r\n", sizeof("TEST BUTTON\r\n")); Radio.Send(BufferTx, sizeof("TEST BUTTON\r\n")-1);
break;
case BUTTON_SW3_PIN:
memcpy(BufferTx,"TEST BUTTON\r\n", sizeof("TEST BUTTON\r\n"));
Radio.Send(BufferTx, sizeof("TEST BUTTON\r\n")-1);
break;
default:break;
}
}
}
2、进程管理函数中修改了各状态下系统的操作内容。尤其是在接收完成时需要对设备的属性进行判断进而执行不同的操作。代码如下:
static void PingPong_Process(void)
{
switch (State)//判断系统状态
{
case RX://如果接收完成
if (isMaster == true)//判断设备属性,如果设备是发送端
{
if (RxBufferSize > 0)//接收到内容
{
if(strncmp((const char *)BufferRx,"TEST BUTTON\r\n",sizeof("TEST BUTTON\r\n")-1)==0)
{
BSP_LED_Toggle(LED_RED);
printf("%s",BufferRx);
Radio.Rx(RX_TIMEOUT_VALUE);//将 Lora 设置为接收等待模式
}
}
}
else//如果设备是接收端
{
BSP_LED_Toggle(LED_RED);
printf("%s",BufferRx);
memcpy(BufferTx, BufferRx, sizeof(BufferRx));//发送数据写入数组
Radio.Send(BufferTx, sizeof(BufferRx));//发送数据
}
break;
case TX://如果发送完成
BSP_LED_Toggle(LED_GREEN);
Radio.Rx(RX_TIMEOUT_VALUE);//将 Lora 设置为接收等待模式
break;
case RX_TIMEOUT://如果接收超时
Radio.Rx(RX_TIMEOUT_VALUE);将 Lora 设置为接收等待模式
break;
case RX_ERROR://如果接收错误
Radio.Rx(RX_TIMEOUT_VALUE);将 Lora 设置为接收等待模式
break;
default://如果是其他状态就直接退出
break;
}
}
本程序的主要功能就是在组网通信的状态下可以将发送的指令让指定设备执行,即点对点通信。每一个设备都有自己的设备ID(LocalID),在接收到信息时通过判断信息格式以及接收ID是否匹配来实现点对点通信,如果数据帧符合要求并且接收ID和自己的设备ID相匹配才对信息进行解析。
同时还将设备的ID信息写在了flash内部,可以保证在系统断电后修改以后的设备ID不会丢失。设备的通信属性信息可以通过串口依据指定的协议帧进行修改,修改后会更新flash内的内容。
本程序中usart.c文件的内容与主从通信中完全一致,所以这里将不再继续讲解。接下来将着重讲解应用中点对点通信程序的实现和flash读写程序的实现以及点对点通信协议帧的构成、串口修改设备属性协议帧的构成。
int main(void)
{
int len=0;//串口接收数据长度
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_SubGHz_Phy_Init();//Lora初始化
MX_USART1_UART_Init();
FLASH_Init();//设备属性初始化,从flash读取信息
SET_Communication_Parameters();//Lora通信信息初始化
while (1)
{
MX_SubGHz_Phy_Process();
if(USART_RX_STA&0x8000)//串口接收标志位判断
{
len=USART_RX_STA&0x3fff;
USART_RX_STA=0;//清除标志位
if(len==USART_RX_BUF[1])
{
if((USART_RX_BUF[0]==Began)&&(USART_RX_BUF[len-1]==Endding))
{
switch(USART_RX_BUF[2])//依据串口接收的信息修改属性
{
case 0x01:LocalID=USART_RX_BUF[3];break;
case 0x02:SendID=USART_RX_BUF[3];break;
}
//将设置的属性信息写入flash
Flash_Write((uint64_t)LocalID,(uint64_t)SendID);
SET_Communication_Parameters();//Lora通信信息初始化
}
}
}
}
}
以上便是主函数的代码,while不断更新状态同时检查串口接收是否完成。
串口通信协议如下:帧头 数据长度 修改参数 修改值 帧尾,最后必须以0x0D 0x0A结尾。其中整帧数据采用定长格式为5个字节(除去0x0D 0x0A),帧头固定为0xF1,帧尾为0xFF,数据长度为0x05,修改参数目前暂定2个有效值,分别为0x01和0x02,他们分别对应着设备ID和接收设备ID,即当修改参数为0x01时修改值将会被赋值给设备ID,设备ID将被修改然后写入flash,为0x02时方法一致,修改的是接收设备ID。
在本应用中增加了一个Lora通信信息初始化函数[void SET_Communication _Parameters(void)],该函数会依据Lora通信协议将发送的数据储存在数组Send_Buffer中。除此之外便是修改了按键的功能,将其设置为按下一次发送一次数据。除此之外便是进程处理函数接收完成状态处理函数增加了数据判断和解析函数。只有符合Lora通信协议并且接收方是自己才会进行解析。以下是代码讲解:
unsigned char LocalID;//以下几行定义Lora通信协议组成变量
unsigned char SendID;
unsigned char Send_Length=5;
unsigned char Began=0xF1;
unsigned char Endding=0xFF;
unsigned char DATA=0xBB;
unsigned char Send_Buffer[5];
void SET_Communication_Parameters(void)//设置通信参数
{
Send_Buffer[0]=Began;
Send_Buffer[1]=SendID;
Send_Buffer[2]=Send_Length;
Send_Buffer[3]=DATA;
Send_Buffer[4]=Endding;
}
进程处理函数中接收完成部分:
case RX://如果接收完成
if (RxBufferSize > 0)//接收到内容
{
if(BufferRx[1]==LocalID)//判断信息接收方是否是自己
{
if(( BufferRx[2] == Send_Length ) && ( BufferRx[0] == Began ) && ( BufferRx[Send_Length -1]==Endding ))//判断是否符合通信协议要求
{
BSP_LED_Toggle(LED_RED);
Radio.Rx(RX_TIMEOUT_VALUE);//将 Lora 设置为接收等待模式
}
}
else//如果接收ID不是自己
{
Radio.Rx(RX_TIMEOUT_VALUE);//将 Lora 设置为接收等待模式
}
}
break;
进行点对点通信实现的方式就是通信协议。在本应用中,我设置的通信协议为数据帧长度为5个uint8_t类型的数据,分别是:帧头、接收设备ID(SendID),数据帧长度、数据内容、帧尾。其中帧头、数据长度、数据内容和帧尾固定,分别是0xF1,0x05,0xBB,0xFF,唯一变化的就是接收设备ID,其余信息可依据个人需要修改。
在Lora通信成功接收到信息后会进入接收完成回调函数,在该函数中会将接收的信息转存在接收数组上,随后将系统状态设置为接收完成。最后在进程处理函数中对数据帧进行判断和解析。
在本文件中主要具有2个函数,一个是读取指定位置指定长度的信息的读取函数,另一个是在指定位置写入指定长度数据的函数,通过这两个函数来保证配置后的通信属性掉电不会丢失。同时还有一个flash初始化函数,该函数的作用是在系统上电初始化时读取flash内的信息并将其赋值给指定的变量。以下是代码部分:
uint64_t STMFLASH_ReadHalfWord(u32 faddr)//flash读取函数,地址为32位长度
{
return *(vu64*)faddr;
}
void Flash_Write(uint64_t DATA1,uint64_t DATA2)//flash写函数
{
FLASH_EraseInitTypeDef FlashEraseInit;
unsigned int SectorError=0;
HAL_FLASH_Unlock(); //解锁
//配置要擦除的扇区
FlashEraseInit.TypeErase=FLASH_TYPEERASE_PAGES; //擦除类型,页面擦除
FlashEraseInit.Page=63; //页面地址
FlashEraseInit.NbPages=1; //要擦除的页面数
HAL_FLASH_Unlock();
HAL_FLASHEx_Erase(&FlashEraseInit,&SectorError);
while(HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD,LocalID_Address,DATA1)!=HAL_OK);//等待写入完成
while(HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD,SendID_Address,DATA2)!=HAL_OK);//等待写入完成
HAL_FLASH_Lock();//锁定
}
void FLASH_Init(void)//flash初始化函数
{
localid_read=STMFLASH_ReadHalfWord(LocalID_Address);//读取指定位置信息
sendid_read=STMFLASH_ReadHalfWord(SendID_Address);//读取指定位置信息
if(localid_read==0xFFFFFFFFFFFFFFFF){LocalID=0x02;}//未写入时默认为0x01
else {LocalID=localid_read&0xff;}
if(sendid_read==0xFFFFFFFFFFFFFFFF) {SendID=0x01;}//未写入时默认为0x02
else {SendID=sendid_read&0xff;}
}
注:在flash读信息时数据为64位长,将其与一个8位的数据进行与运算其结果也是8位;在flash写时,如果数据长度小于写入长度(程序中写入长度为64为,写入数据为8为)剩余部分会被写入0。
STM32WL系列单片机flash擦除只有2个选项,一个是页面擦除,另一个是扇区擦除。在该系列单片机中一个页面的大小为2个字节,并且要保存的信息不多,所以程序中将通信属性信息写在了最后一页中,同时擦除选项选择了页面擦除。除此之外HAL库的flash写入函数中写入类型有两个选项是,一个是写入一个字即32位(FLASH_TYPEPROGRAM_FAST),但实测不成功。后选择了写入长度双字即64位(FLASH_TYPEPROGRAM_DOUBLEWORD),这也是为什么LocalID_Address(0x0801F800)和SendID_Address(0x0801F808)的地址相差了8个16进制数。
还有就是读取函数中vu64的宏定义为typedef __IO uint64_t vu64;