STM32F103与M5311NBiot模组的调试笔记
前言:
不知道为什么,现在物联网这个东西越来火了,GPRS等无线通讯模组,似乎正在被NBiot慢慢替代,伴随着我亲爱的室友(儿子)遇到了NBiot的问题。我被尝试着调试了一下这个所谓的"很好调"的模组。仅以此文作为学习笔记,向其中借鉴到思路的大佬表示崇高敬意,同时,还是存在着非常多缺点与不完善之处,望见谅。就当个笑话看过就好。
1.硬件选型
1.1 NBiot模组选型:
从标题就可以看出,所选择的NBiot模组是以M5311为主控的芯片,现在市面上有各种各样的NBiot模组,选择这个无非就是便宜,加之之前在B站上看到过某个大佬的简易调试过程,因此选择了M5311,TB购买链接在下面:
链接: M5311
1.2 MCU选型:
带串口通讯的MCU就行,这里选择了stm32F103C8T6是因为不仅便宜,而且在处理这种AT指令通信过程中,返回各种不定长数据的情况下可以通过使用串口空闲中断+DMA自动搬运数据,真的不要太香!
2.通讯协议方面
M5311连接onenet平台使用的LWM2M协议,似乎并不需要理解具体协议的含义是什么,因为我们在跟模组通讯的过程中无非使用的就是AT指令,我们只需要知道该使用哪些AT指令,以及这些AT指令的意思是什么就可以了。
简单的举个例子:
软件复位终端可以看到他的固定返回值有OK(成功)与ERROR(失败),因此在发送出该命令后,我们需要检测模组返回的值是ERROR还是OK。
同理可以得到
AT+CGPADDR=1
AT+MIPLCREATE
AT+MIPLADDOBJ
AT+MIPLDISCOVERRSP
AT+MIPLOPEN
AT+MIPLNOTIFY等等AT指令的具体作用。
3 stm32与NBiot模组的通信方式
先来讲讲为什么要用串口空闲中断,而不是场规的串口接受非空中断,理由很简单:当处理的数据为固定长度的时候,确定数据接受完毕确实很简单,只需要在串口中断中计数即可,但是当发送回MCU的数据为不定字长,帧头帧尾都是无规则的数据时,要确定数据何时接受结束就会变得非常困难,我也是最近才知道有串口空闲中断这种东西,讲的简单点就是当串口接受一帧数据完成后,后续无数据再发送了,MCU会自动将串口空闲中断标志位置位(而不是字面意思上理解的串口空闲着就会进入中断)
那为什么要用DMA呢?
其实原因也很简单,我们可以把DMA理解成一个“舔狗”,当你指定了DMA的搬运方向并将它使能了之后,一旦当串口接受了数据,DMA就会屁颠屁颠的跑过去帮串口把东西搬到她想要的地方去,且不需要CPU的参与。因此我们可以定义一个数组Receive_Buffer[],让DMA把串口的USARTX->DR寄存器中的数据搬运到数组中去。这在调试的时候非常有用。
具体串口空闲中断结合DMA搬运的讲解可以看这篇文章,讲得很好:
链接: 串口空闲中断以及DMA搬运.
4 硬件连线
M5311 STM32
PWR---------PB1(通用推挽输出)
RX------------PB10(USART_3_TX)
TX-------------PB10(USART_3_RX)
VCC---------你懂的
GND---------你懂的
这里解释一下为什么选择了串口3,因为我的最小系统板子上,只有USART1连接了ch340能与电脑通讯,因此将串口1作为打印串口3与M5311通讯记录的工具(老工具人了)。也可以选择不接PWR,使用M5311核心板子上按键长按来使其工作。
5 程序思路以及代码实现
串口1配置:就进行正常的配置就可以了不需要开启任何中断
串口3、DMA配置、串口3中断配置:
Receive_Buffer[]:DMA搬运数据终点,调试信息汇总处
static void DMA_Config(void)
{
DMA_InitTypeDef DMA_Initstructure;
/*开启DMA时钟*/
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
DMA_DeInit(DMA1_Channel3);
/*DMA配置*/
DMA_Initstructure.DMA_PeripheralBaseAddr = (uint32_t)(&USART3->DR);
DMA_Initstructure.DMA_MemoryBaseAddr = (uint32_t)Receive_Buffer;
DMA_Initstructure.DMA_DIR = DMA_DIR_PeripheralSRC;
DMA_Initstructure.DMA_BufferSize =DATA_LENGTH;
DMA_Initstructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_Initstructure.DMA_MemoryInc =DMA_MemoryInc_Enable;
DMA_Initstructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_Initstructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_Initstructure.DMA_Mode = DMA_Mode_Normal;
DMA_Initstructure.DMA_Priority = DMA_Priority_High;
DMA_Initstructure.DMA_M2M = DMA_M2M_Disable;
DMA_Init(DMA1_Channel3,&DMA_Initstructure);
DMA_Cmd(DMA1_Channel3,ENABLE);
}
void MY_USART3_CONFIG(uint32_t baud)//串口初始化函数
{
DMA_Config();
//初始化时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3, ENABLE);
//串口复位
USART_DeInit(USART3);
//使用串口3进行通信
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
//TXD PB10配置
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_10;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStructure);
//RXD PB11配置
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IN_FLOATING;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_11;
GPIO_Init(GPIOB,&GPIO_InitStructure);
//串口的通讯协议设置
USART_InitStructure.USART_BaudRate=baud;//波特率
USART_InitStructure.USART_HardwareFlowControl=USART_HardwareFlowControl_None;//有无硬件流
USART_InitStructure.USART_Mode=USART_Mode_Rx|USART_Mode_Tx;//收发模式全开
USART_InitStructure.USART_Parity=USART_Parity_No;//无校验
USART_InitStructure.USART_StopBits=USART_StopBits_1;//一停止位
USART_InitStructure.USART_WordLength=USART_WordLength_8b;//发送字节长度
USART_Init(USART3,&USART_InitStructure);
//开启串口空闲接收中断
USART_ITConfig(USART3,USART_IT_IDLE,ENABLE);
//开启串口DMA接收
USART_DMACmd(USART3,USART_DMAReq_Rx,ENABLE);
//开启串口时钟
USART_Cmd(USART3,ENABLE);
}
void USART3_IRQHandler(void)
{
uint8_t length=0;
if(USART_GetITStatus(USART3,USART_IT_IDLE)==SET)
{
//清除空闲中断标志(软件序列清除)
length=USART3->SR;
length=USART3->DR;
//当DMA通道配置为非循环模式时,传输结束后,为了开始新的DMA传输命令,得把DMA失能,并重新设置接受数据长度
DMA_Cmd(DMA1_Channel3,DISABLE);
length=DATA_LENGTH-DMA_GetCurrDataCounter(DMA1_Channel3);
Receive_Buffer[length]='\0';
DMA1_Channel3->CNDTR=DATA_LENGTH;
DMA_Cmd(DMA1_Channel3,ENABLE);
//通过串口1打印M5311发送指令
if(observe_flag)
printf("%s\r\n",Receive_Buffer);
receive_flag=1;
}
}
发送字符与要求返回字符的比较函数,这里参考了B站阿正哥哥的代码,具体思路就是将发送字符串后返回的字符串与希望返回的字符串比较(函数strstr的作用),加了个延时判断,防止过早退出。
若接受数据正确则返回1
否则返回0,再必要的AT指令设置过程中可以配合while(true);
使其初始化到实现为止。
//Receive_Buffer[];DMA搬运终点
uint8_t NBiot_SendCmd(char *cmd,char *reply,int wait)
{
uint32_t i;
//memset(Receive_Buffer,0,sizeof(Receive_Buffer));//先清空接受缓存区
printf("[NBiot_SendCmd] %s", cmd);//串口打印发送命令
delay_ms(100);//适当延时
COM_Sendstr(USART3,cmd,strlen(cmd));
//延时等待函数
if (wait >= 1000)
{
for (i = 0; i < (wait / 1000); i++)
delay_ms(1000);
}
else
delay_ms(wait);
if (strcmp(reply, "") == 0) //返回值为空
return 0;
else
{
if (strstr((char*)Receive_Buffer, reply)) //是否包含数据
{
printf("respond Successfully\r\n");
receive_flag=0;
return 1;
}
else if (strstr((char*)Receive_Buffer, "ERROR"))
{
printf("ERROR\r\n");
receive_flag=0;
return 0;
}
else
{
printf("Other Error:%s\r\n",Receive_Buffer);
receive_flag=0;
return 0;
}
}
}
模块初始化用到的AT指令,以及代码实现:
AT+CMRB:复位指令
while(!NBiot_SendCmd("AT+CMRB\r\n", "ATREADY", 2000)); //软复位
AT+CGPADDR=1:据说是联网测试
while(NBiot_SendCmd("AT+CGPADDR=1\r\n","+CGPADDR: 1",1000));
AT+MIPLCREATE=注册码 :这里的注册码可以重复使用
const char *Onenet_Key="AT+MIPLCREATE=56,130038F10003F2002A04001100000000000010196E62696F7462742E6865636C6F7564732E636F6D3A35363833000131F300080000000000,0,56,0\r\n";
while(NBiot_SendCmd((char *)Onenet_Key,"OK",20000)==0);//连接平台时间较长延时时间也就较长
AT+MIPLADDOBJ=0,3300,1,“1”,0,1 创建实例格式参数见注释
//创建object
/*<ref>,<objid>,<inscount>,<bitmap>,<atts>,<acts>*/
char *Create_OBJ="AT+MIPLADDOBJ=0,3300,1,\"1\",0,1\r\n";
while(NBiot_SendCmd((char *)Create_OBJ,"OK",1000)==0);
AT+MIPLDISCOVERRSP=0,3300,1,4,“5750” 订阅资源格式如注释
//订阅资源
/*<ref>,<objid>,<result>,<length>,<data>*/
char *Create_Resource="AT+MIPLDISCOVERRSP=0,3300,1,4,\"5750\"\r\n";
while(NBiot_SendCmd((char *)Create_Resource,"OK",1000)==0);
AT+MIPLOPEN=0,3000,30 连接onenet
//尝试连接Onenet
/*<ref>,<lifetime>[,<timeout>]*/
char *Onenet_Connect="AT+MIPLOPEN=0,3000,30\r\n";
while(NBiot_SendCmd((char *)Onenet_Connect,"OK",1000)==0);
AT+MIPLNOTIFY=0,0,3300,0,5750,1,18,“songjunchuanshizhu” 上报数据
可以将songjunchuanshizhu替换成你想要上发的数据,18的位置代表字符串长度
//上报数据
/*<ref>,<mid>,<objid>,<insid>,<resid>,<type>,<len>,<value>,<index>,<flag>[,<ackid>]*/
char *Notify_Data="AT+MIPLNOTIFY=0,0,3300,0,5750,1,18,\"songjunchuanshizhu\",0,0\r\n";
while(NBiot_SendCmd((char *)Notify_Data,"OK",1000)==0);
运行结果
连线上电复位后等待M5311初始化,在配置好的onenet平台可以看到上报的数据,可以将notify指令中数据换成想要上报的如温湿度之类的数据,创建OBJ以及订阅资源那一块搞得还不是很懂。就当个笔记看看吧。
总结
全文写的比较糙,当作笔记用吧,希望本来就会的大佬勿喷。