STM32学习之串口采用DMA收发数据:需要利用状态机加DMA加串口
写在前面
在学习这一节知识点的时候,真的是感觉太抽象了,没有一个合适的视频讲的我有那种豁然开朗的感觉,直到我看到了这篇文章,大家可以去看看,里面的描述特别形象。
链接:https://blog.csdn.net/gdjason/article/details/51019219
是什么
DMA —- Directional Memory Access,直接存储器存取用来提供在外设和存储器之间或者存储器和存储器之间的高速数据传输。无须CPU干预,数据可以通过DMA快速地移动,这就节省了CPU的资源来做其他操作。
其中最重要的就是节省了CPU资源。
串口直接收发和DMA结合串口收发比较
串口直接收发数据过程:
接受:
数据通过串口发送到CPU,CPU然后把数据存储到数组里面
发送:
数据存储到数组里面, CPU把数组里面的数据发送到串口
CPU在这一串操作的过程中是会被占用的,不能进行别的操作
DMA和串口结合起来收发数据过程:
发送:
内存中的数据,通过DMA发送到外设,发送完毕,DMA的发送完毕标志清除。
接受:
串口发送数据,DMA把数据取走,然后把数据存储到数组
在这一串操作的过程中,CPU都是空闲的,可以去干别的事情,可以不用管串口的收发
并且不需要担心串口的收发,串口接收DMA在初始化的时候就处于开启状态,然后需要配置一个空闲中断一直等待数据的到来,也就是只要串口接收数据完毕,就会有一个电平变化,然后就可以进入中断,然后我们不需要在软件上无需做任何事情,只要在初始化配置的时候设置好配置就可以了。 也就是只要你打开了DMA通道,探测到电平变化,进入空闲中断,就可以自动进行收发
没听懂?那用一种更清晰的方法来描述
(来自前方声明的链接博客的描述)
对于DMA,打个比方就很好理解:
角色预设: 淘宝店主 —- STM32 MCU
快递员 —- 外设(如UART,SPI)
发货室 —- DMA
1、首先你是一个淘宝店主,如果每次发货收货都要跟快递沟通交涉会很浪费时间和精力。
2、然后你就自己建了一个发货室,发货室里有好多个货柜箱子,每个箱子上都写着快递名字(如果申通快递,顺丰快递等)。
3、每次发什么快递,你就找到对应的货柜箱子,把货物放进去即可,然后跟快递通知一声。
4、快递取走快件。
5、如果是收货,快递直接把快件放到对应的柜子,然后通知你一下。
6、你过来提取货物。
通过上面的方式,你可以不需要直接跟快递打交道,就可以轻松发货成功,DMA处理方式跟上面例子是一样的。
如果下图:
那么DMA在STM32上是具体怎么实现的呢? 我们先了解一下STM32关于DMA的相关配置。
1、两个DMA控制器有12个通道(DMA1有7个通道,DMA2有5个通道)
ps:对应我们例子,就是有两个大的发货室,一个有7个货柜,另个有5个货柜。
2、在同一个DMA模块上,多个请求间的优先权可以通过软件编程设置(共有四级:很高、高、中等和低),优先权设置相等时由硬件决定(请求0优先于请求1,依此类推)
ps: 店主可以跟每个快递公司签订协议,可以在货柜前贴上加急(很高),很急(高),急(中),一般(低), 如果同时有几个快递员过来取货,优先根据上面的优先级先取件。
3、独立数据源和目标数据区的传输宽度(字节、半字、全字),模拟打包和拆包的过程。源和目标地址必须按数据传输宽度对齐。
ps: 指的是货件大小
4、支持循环的缓冲器管理(会把原来的数据覆盖)
5、每个通道都有3个事件标志(DMA半传输、DMA传输完成和DMA传输出错),这3个事件标志逻辑或成为一个单独的中断请求。
ps: 送快递出现的异常情况(送到了一半,送完,快递出错)
在配置之前
相信听了前面这么多的讲解,你一定懂了串口采用DMA的过程,如果没懂…那还可以去查查别的文章
一定要去了解一下空闲中断!!!
下面来讲一下关于寄存器的一些知识点
1.STM32最多有两个DMA控制器,大容量有两个,DMA1有7个通道,DMA2有5个通道。用通俗的话来讲,就是STM32最多有2个快递员,第一个快递员最多有7家人需要收发快递,第二个快递员最多有5家人需要收发快递,一个快递员不能同时收发两家人的快递。
2.因为有这么多家人,当两家同时喊一个快递员来收快递的时候,这时候,就需要有一个先后顺序,这就是DMA的顺序。共有四级:很高、高、中等和低,在相等优先权时由硬件决定(请求 0 优先于请求 1,依此类推) 。
3.每个通道都有 3 个事件标志(DMA 半传输,DMA 传输完成和 DMA 传输出错),这 3 个 事件标志逻辑或成为一个单独的中断请求。
4.可以实现存储器和存储器间的传输,外设和存储器,存储器和外设的传输
5.最多传输65536个字节
6.每个通道都有 3 个事件标志(DMA 半传输,DMA 传输完成和 DMA 传输出错),这 3 个 事件标志逻辑或成为一个单独的中断请求。也就是说传输一半数据或者传输完成亦或者是传输出错都可以有一个中断。
串口利用DMA配置过程
实战代码
这是本周学习STM32任务之一,任务中少了Send_data[4]和Send_data[5],是接收方的ID
/*------------------------------------------------------
下面的就是实现串口和DMA结合的程序
包含了利用DMA实现串口的收发
-------------------------------------------------------*/
#include "sys.h"
#include "usart.h"
#include "led.h"
#include "string.h"
#include <stdarg.h>
#include "crc.h"
//
//如果使用ucos,则包括下面的头文件即可.
#if SYSTEM_SUPPORT_OS
#include "includes.h" //ucos 使用
#endif//加入以下代码,支持printf函数,而不需要选择use MicroLIB
#if 1
#pragma import(__use_no_semihosting)
//标准库需要的支持函数
struct __FILE
{ int handle; }; FILE __stdout;
//定义_sys_exit()以避免使用半主机模式
_sys_exit(int x)
{ x = x;
}
//重定义fputc函数
int fputc(int ch, FILE *f)
{ while((USART1->SR&0X40)==0);//循环发送,直到发送完毕 USART1->DR = (u8) ch; return ch;
}
#endif /*------------------------------------------------------
下面的就是实现串口和DMA结合的程序
包含了利用DMA实现串口的收发
-------------------------------------------------------*//*--------------------数据包类型-----------------------*/
enum//枚举
{WAITING_FF1, //等待1(包头)WAITING_FF2, //等待2(包头)RECEIVE_ID, //接收方idRECEIVE_ExtraID, //接收方扩展IDRECEIVE_LEN, //数据长度RECEIVE_PACKAGE, //数据包RECEIVE_CHECK //接收校验位
}receive_state_;u8 DMA_Rece_Buf[DMA_Rec_Len]; //DMA接收串口数据缓冲区
u16 Usart1_Rec_Cnt=0; //本帧数据长度 u8 DMA_Tx_Buf[DMA_Tx_Len]; //DMA接收串口数据缓冲区
u16 Usart1_Tx_Cnt=0; //本帧数据长度 u8 DataBag[3],PWMValue;
u8 CRCCODE,CRCCODEdaima; //自己计算的CRC校验码const CRC_8 crc_8_MAXIM = {0x31,0x00,0x00,TRUE,TRUE};
//UART1的GPIO初始化
void UART1_GPIO_Init(void)
{//GPIO端口设置GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //使能GPIOA时钟//USART1_TX PA.9GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //PA.9GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出GPIO_Init(GPIOA, &GPIO_InitStructure); //初始化PA9//USART1_RX PA.10GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; //浮空输入GPIO_Init(GPIOA, &GPIO_InitStructure); //初始化PA10
}//UART1的中断优先级初始化
void UART1_NVIC_Init(void)
{NVIC_InitTypeDef NVIC_InitStructure;NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; //串口1接收中断NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3 ;//抢占优先级3NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; //子优先级3NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化VIC寄存器
}//UART1_RX的DMA初始化
void UART1_DMA_RX_Init(void)
{DMA_InitTypeDef DMA_UART1_RX;//相应的DMA配置DMA_DeInit(DMA1_Channel5); //将DMA的通道5寄存器重设为缺省值 串口1对应的是DMA通道5DMA_UART1_RX.DMA_PeripheralBaseAddr = (u32)&USART1->DR; //DMA外设ADC基地址DMA_UART1_RX.DMA_MemoryBaseAddr = (u32)DMA_Rece_Buf; //DMA内存基地址DMA_UART1_RX.DMA_DIR = DMA_DIR_PeripheralSRC; //数据传输方向,从外设读取发送到内存DMA_UART1_RX.DMA_BufferSize = DMA_Rec_Len; //DMA通道的DMA缓存的大小DMA_UART1_RX.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设地址寄存器不变DMA_UART1_RX.DMA_MemoryInc = DMA_MemoryInc_Enable; //内存地址寄存器递增DMA_UART1_RX.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //数据宽度为8位DMA_UART1_RX.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; //数据宽度为8位DMA_UART1_RX.DMA_Mode = DMA_Mode_Normal; //工作在正常缓存模式DMA_UART1_RX.DMA_Priority = DMA_Priority_Medium; //DMA通道 x拥有中优先级 DMA_UART1_RX.DMA_M2M = DMA_M2M_Disable; //DMA通道x没有设置为内存到内存传输DMA_Init(DMA1_Channel5, &DMA_UART1_RX); //根据DMA_InitStruct中指定的参数初始化DMA的通道USART1_Tx_DMA_Channel所标识的寄存器DMA_Cmd(DMA1_Channel5, ENABLE); //正式驱动DMA传输
}//UART1_TX的DMA初始化
void UART1_DMA_TX_Init(void)
{DMA_InitTypeDef DMA_UART1_TX;//相应的DMA配置DMA_DeInit(DMA1_Channel4); //将DMA的通道5寄存器重设为缺省值 串口1对应的是DMA通道5DMA_UART1_TX.DMA_PeripheralBaseAddr = (u32)&USART1->DR; //DMA外设ADC基地址DMA_UART1_TX.DMA_MemoryBaseAddr = (u32)DMA_Tx_Buf; //DMA内存基地址DMA_UART1_TX.DMA_DIR = DMA_DIR_PeripheralDST; //数据传输方向,从内存读取发送到外设DMA_UART1_TX.DMA_BufferSize = DMA_Tx_Len; //DMA通道的DMA缓存的大小DMA_UART1_TX.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设地址寄存器不变DMA_UART1_TX.DMA_MemoryInc = DMA_MemoryInc_Enable; //内存地址寄存器递增DMA_UART1_TX.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //数据宽度为8位DMA_UART1_TX.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; //数据宽度为8位DMA_UART1_TX.DMA_Mode = DMA_Mode_Normal; //工作在正常缓存模式DMA_UART1_TX.DMA_Priority = DMA_Priority_Medium; //DMA通道 x拥有中优先级 DMA_UART1_TX.DMA_M2M = DMA_M2M_Disable; //DMA通道x没有设置为内存到内存传输DMA_Init(DMA1_Channel4, &DMA_UART1_TX); //根据DMA_InitStruct中指定的参数初始化DMA的通道USART1_Tx_DMA_Channel所标识的寄存器DMA_Cmd(DMA1_Channel4, ENABLE); //正式驱动DMA传输
}//初始化IO 串口1
//bound:波特率
void uart_init(u32 bound)
{USART_InitTypeDef USART_InitStructure;RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); //使能USART1时钟RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //使能DMA传输//USART 初始化设置USART_InitStructure.USART_BaudRate = bound; //串口波特率USART_InitStructure.USART_WordLength = USART_WordLength_8b; //字长为8位数据格式USART_InitStructure.USART_StopBits = USART_StopBits_1; //一个停止位USART_InitStructure.USART_Parity = USART_Parity_No; //无奇偶校验位USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //无硬件数据流控制USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; //收发模式USART_Init(USART1, &USART_InitStructure); //初始化串口1UART1_GPIO_Init(); //UART1的GPIO初始化UART1_NVIC_Init(); //UART1的中断优先级初始化USART_ITConfig(USART1, USART_IT_IDLE, ENABLE); //开启空闲中断USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE); //使能串口1的DMA接收USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE); //使能串口1的DMA发送USART_Cmd(USART1, ENABLE); //使能串口 UART1_DMA_RX_Init(); //UART1_RX的EDMA功能初始化UART1_DMA_TX_Init(); //UART1_TX的EDMA功能初始化
}//重新使能UART1_RX的DMA功能
void UART1_RX_DMA_Enable(void)
{ DMA_Cmd(DMA1_Channel5, DISABLE ); //先停止DMA,暂停接收 DMA_SetCurrDataCounter(DMA1_Channel5, DMA_Rec_Len); //DMA通道的DMA缓存的大小DMA_Cmd(DMA1_Channel5, ENABLE); //使能USART1 TX DMA1 所指示的通道
} //重新使能UART1_TX的DMA功能
void UART1_TX_DMA_Enable(void)
{ DMA_Cmd(DMA1_Channel4, DISABLE ); //先停止DMADMA_SetCurrDataCounter(DMA1_Channel4, DMA_Tx_Len); //DMA通道的DMA缓存的大小DMA_Cmd(DMA1_Channel4, ENABLE); //使能USART1 TX DMA1 所指示的通道
} //发送len个字节.
//buf:发送区首地址
//len:发送的字节数
void Usart1_Send(u8 *buf,u8 len)
{u8 t;for(t=0;t<len;t++) //循环发送数据{ while(USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET); USART_SendData(USART1,buf[t]);} while(USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);
}
/*在空闲中断服务函数里面实现状态机的功能*/
void USART1_IRQHandler(void) //串口1中断服务程序
{u8 i = 0,Receive_OK = 0,a;
#if SYSTEM_SUPPORT_OS //如果SYSTEM_SUPPORT_OS为真,则需要支持OS.OSIntEnter();
#endif//串口1空闲中断if(USART_GetITStatus(USART1, USART_IT_IDLE) != RESET) {/* 1.清除标志 */USART_ClearITPendingBit(USART1, USART_IT_IDLE); //清除中断标志/* 2.读取DMA */USART_ReceiveData(USART1); //读取数据Usart1_Rec_Cnt = DMA_Rec_Len - DMA_GetCurrDataCounter(DMA1_Channel5); //接收个数等于接收缓冲区总大小减已经接收的个数//检验串口是否正常工作代码if(DMA_Rece_Buf[0] == 0X42)LED1 = !LED1;/* 3.根据读取的DMA数据来进行相应的操作*///状态机执行相应的命令,因为串口发送的数据是定好的16个,所以我的DMA_Rece_Buf定死了是16个数据//while循环是为了使得状态机能够验证,需要循环至少8次a = 16; //初始化a值,让循环循环16次while(a--){switch(receive_state_){case WAITING_FF1:if(DMA_Rece_Buf[0] == 0x41){receive_state_=WAITING_FF2;}break;case WAITING_FF2:if(DMA_Rece_Buf[1]==0x54)receive_state_=RECEIVE_ID;break;//以上代码判断包头case RECEIVE_ID://接收IDDMA_Rece_Buf[2] = DMA_Rece_Buf[2] >>1; //发送的ID应该是C4DMA_Rece_Buf[3] = DMA_Rece_Buf[3] >>1; //发送的ID应该是40// 如此才能匹配下方if(DMA_Rece_Buf[2] == 0x62 && DMA_Rece_Buf[3] == 0x20)//{receive_state_ = RECEIVE_ExtraID;//LED1 = !LED1; //验证数据是否发送成功//}break;case RECEIVE_ExtraID://接收扩展IDif(DMA_Rece_Buf[4] == 0x00 && DMA_Rece_Buf[5] == 0x00)receive_state_ = RECEIVE_LEN;//以上代码验证接收方IDcase RECEIVE_LEN:if(DMA_Rece_Buf[6] == 0x08)//{receive_state_=RECEIVE_PACKAGE;//LED1 = !LED1;//验证数据是否发送成功//}break;//以上代码验证接收的数据包长度//接收数据包case RECEIVE_PACKAGE:while(i < 3){DataBag[i] = DMA_Rece_Buf[7+i]; i++;}i = 0; receive_state_=RECEIVE_CHECK;break;//CRC校验case RECEIVE_CHECK:CRCCODE = DMA_Rece_Buf[15];CRCCODEdaima = crc8(DMA_Rece_Buf,15,crc_8_MAXIM);//我在网上找到的CRC代码,不知道为什么,和计算出来的值就是不匹配,导致了我后面无论如何都无法校验if(CRCCODE == 0x01){ Receive_OK = 1;receive_state_=WAITING_FF1;//LED1 = !LED1;//验证数据是否发送成功CRCCODE = 0;CRCCODEdaima = 0;}default:receive_state_=WAITING_FF1;}}/* 4. 分析数据包内容*/if(Receive_OK){if(DataBag[0] == 0x01) //判断是哪一个灯{if(DataBag[1] == 0x00) //保持当前状态LED0 = LED0;else if(DataBag[1] == 0x01) //开灯LED0 = 0;else if(DataBag[1] == 0x02) //关灯LED0 = 1;else if(DataBag[1] == 0x03) //状态取反LED0 = !LED0;}else if(DataBag[0] == 0x02) //判断是哪一个灯{ if(DataBag[1] == 0x00) //保持当前状态 LED1 = LED1;else if(DataBag[1] == 0x01) //开灯LED1 = 0;else if(DataBag[1] == 0x02) //关灯LED1 = 1;else if(DataBag[1] == 0x03) //状态取反LED1 = !LED1;}if(DataBag[2] <= 190) //判断舵机需要转过多少度{PWMValue = DataBag[2];}}/* 5.搬移数据进行其他处理 *///memcpy函数就是把Rece_Buf里面的Cnt个数据存储到Tx_Buf里面memcpy(DMA_Tx_Buf, DMA_Rece_Buf, Usart1_Rec_Cnt); //将接收转移通过串口1的DMA方式发送出去测试UART1_TX_DMA_Enable(); //开启一次DMA发送,实现转发/* 6.开启新的一次DMA接收 */UART1_RX_DMA_Enable(); //重新使能DMA,等待下一次的接收} #if SYSTEM_SUPPORT_OS //如果SYSTEM_SUPPORT_OS为真,则需要支持OS.OSIntExit();
#endif
}
#include "led.h"
#include "delay.h"
#include "sys.h"
#include "usart.h"
#include "timer.h"
#include "CRC.h"extern u8 PWMValue; //PWM方波值
int main(void)
{ u16 times=0;/* 系统初始化 */delay_init(); //延时函数初始化 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置NVIC中断分组2:2位抢占优先级,2位响应优先级/* 自定义外设初始化 */LED_Init(); //LED端口初始化uart_init(115200); //初始化串口(DMA发送和空闲中断接收模式)TIM3_PWM_Init(1999,719); //这里的arr的值和psc的值可以修改,只要在不分频情况下为72MHZ,719对应0.01ms,1999对应2000*0.01 = 20mswhile(1){delay_ms(10); TIM_SetCompare2(TIM3, 1750 + PWMValue/0.9);times++;//闪烁LED,提示系统正在运行
// if(times == 50)
// {// LED0 = !LED0;
// times = 0;
// }}
}
从正点原子和网上找到的代码修改
欢迎大家批评指正
STM32学习之串口采用DMA收发数据:需要利用状态机加DMA加串口相关推荐
- STM32f407与STM32F103 串口采用DMA收发数据配置方法的异同
最近有个项目需要用到STM32F407ZET6这款芯片,其中有一个串口收发数据的应用.因为之前有用过STMF32F103ZET6通过DMA收发数据的方案,所以我打算移植之前的代码实现这个功能,STM3 ...
- STM32F0系列串口DMA收发数据
关于STM32F0系列串口DMA收发数据详解 这里用的库函数版本,芯片型号为stm32f030c8t6.在用到串口DMA时,要按以下几个步骤进行. 1.确定使用的串口号,这里,我用的是usart2,对 ...
- python串口操作_Python Serial串口基本操作(收发数据)
1.需要模块以及测试工具 模块名:pyserial 使用命令下载:python -m pip install pyserial 串口调试工具:sscom5.13.1.exe 2.导入模块 import ...
- STM32F407ZG 实现DMA收发数据
正点原子的DMA测试实验,代码只包含了DMA发送数据,而没有接收数据,而我需要实现DMA收发数据,经过查找资料,以及验证,终于实现了DMA发送和接收数据. 本人将dma的驱动代码写入了dma.c和dm ...
- c# 使用线程对串口serialPort进行收发数据
c# 使用线程对串口serialPort进行收发数据 一共写了四种方法,窗体界面都是一样的: 方法一: using System; using System.Collections.Generic; ...
- STM8S UART串口使用中断收发数据
STM8S UART串口使用中断收发数据 原来调过STM8L的串口,逻辑简单,中断清晰,换成STM8S105K4后,虽然也是用STD库, 除去函数名.宏名等语言层面的差异以外,中断处理方面也有些不一样 ...
- stm32 学习--Stm32F407 SPI1 全双工DMA 收发数据
最近被 ESP8266 的设计整崩溃了, 一个半的 串口 真的 坑. 为了让 我毕设中的 ESP8266 和 Stm32F4 单片机进行数据传输,我需要找到一种除了 串口意外的 通讯方式(吐槽一下 ...
- 【华大测评】+串口DMA收发数据
串口传输用中断实现的话,要频繁的进入中断函数,这样无疑增加MCU的负担,干扰正常程序的运行,对于一些实时性强的应用,如数字显示应用中,液晶屏显示可能受影响而不能正常显示.用DMA实现串口收发数据,进行 ...
- 2020-03-05-stm32 学习--Stm32F407 SPI1 全双工DMA 收发数据
最近被 ESP8266 的设计整崩溃了, 一个半的 串口 真的 坑. 为了让 我毕设中的 ESP8266 和 Stm32F4 单片机进行数据传输,我需要找到一种除了 串口意外的 通讯方式(吐槽一下 ...
最新文章
- XmlReader and XmlWriter in .NET
- macOS解决sublime text3运行python3报:UnicodeEncodeError: 'ascii' codec can't encode characters in position
- 利用ListView实现新闻客户端的新闻内容图文混排
- openCV视频处理与图像转换
- MATLAB矩阵复制数据
- 【原创】软件测试工程师基础技能+
- R中根据匹配原则将一列拆分为几列的方法
- UI设计师一定要懂的专业名词
- spring加载jar包中多个配置文件
- Mac的触控板坏了怎么办?将键盘变成鼠标的方法
- RK3399 M0 调试-启动
- 能否在计算机界面保存快捷方式,详细教您清理1kb快捷方式病毒
- 《算法竞赛进阶指南》0x62 T4 黑暗城堡
- 发展你的GDS的应用技术
- java算法:1234组成无重复的三位数
- 通过企业微信或者微信公众号发送小程序消息推送功能
- [Algorithmic Toolbox学习笔记][week6]Placing Parentheses
- 从零开始学前端第十七讲--微信小程序开发入门
- Google Play支付 接入配置
- JAVA大华摄像头抓拍与API接口集成