热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

STM32IO口模拟串口通讯

转自:http:ziye334.blog.163.comblogstatic224306191201452833850647前阵子,调项目时需要用到低波

转自:http://ziye334.blog.163.com/blog/static/224306191201452833850647

 

前阵子,调项目时需要用到低波特率串口通讯(300的波特率),才发下发现在正常情况下(PCLK1时钟频率为72M,PCLK2时钟频率为36M):STM32的USART0的最低波特率只能设置到1200,;而USART1最低波特率只能设置到600。怎么设置STM32的600或以下的波特率呢?有两种方法:一种是改变外设时钟频率,而另一种方法就是使用IO口模拟串口通讯。今天就来讲讲,用IO口模拟串口通信!

1、串口传输协议

首先,必须要知道串口通讯时数据是怎样传输的?这里以异步传输字符为例子,如下图所示:
 一般字符传输都采用:1位起始位,8位数据位,1位停止位,没有校验位 的形式传输,其他形式的这里不讲。串口异步传输在空闲状态时都必须是高电平。第一位传输的是起始位,起始位会将原来空闲时的高电平拉成低电平,起始位用来来标识数据开始传输,提示接收方准备开始接收数据;当接收方第一次检测到一个下降沿时,就表示接收到了起始位。起始位后就是8位的数据位,接收方在接收每一位数据的时候会采集几十次,如果结果都是低电平,则接收到的数据位0,如果结果都是高电平,则棘手到的数据位是1。1位停止位会将电平拉成高电平,以为接收下一个数据做准备。


2、IO模拟串口发送程序

IO口模拟串口发送数据,必须严格按照上面的异步传输协议。我们用伪代码实现这一过程:

void VirtualCOM_ByteSend(u8 val)

{

u8 i;

IO_LOW(); //起始位,拉低电平

Delay(sometime);

for(i &#61; 0; i <8; i&#43;&#43;) //8位数据位

{

if(val & 0x01)

IO_HIGH();

else

IO_LOW();

Dealy(sometime);

val >>&#61; 1;

}

IO_HIGH(); //停止位&#xff0c;拉高电平

Delay(sometime);

}


代码很简单&#xff0c;思路也很清晰&#xff0c;完全是按照异步传输的过程写的。这里最重要的是Delay(sometime)&#xff0c;sometime的延时时间就决定了传输的速度&#xff0c;sometime去取某些值才可以设置程序标准的串口波特率(1200、2400、9600、38400、115200等等)。

下面&#xff0c;我采用STM32开发板实现IO模拟串口发送。

&#xff08;1&#xff09;选择IO引脚设置为虚拟串口TX引脚

我选择PA4引脚来模拟串口的TX引脚&#xff0c;所以需要配置下PA4这个引脚为推挽输出&#xff1a;

#define COM_TX_PORT GPIOA

#define COM_TX_PIN GPIO_Pin_4

void VirtualCOM_TX_GPIOConfig(void)

{

GPIO_InitTypeDef GPIO_InitStructure;

/* PA4最为数据输出口&#xff0c;模拟TX */

GPIO_InitStructure.GPIO_Pin &#61; COM_TX_PIN;
GPIO_InitStructure.GPIO_Mode &#61; GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed &#61; GPIO_Speed_50MHz;
GPIO_Init(COM_TX_PORT, &GPIO_InitStructure);
GPIO_SetBits(COM_TX_PORT, COM_TX_PIN);

}

这里需要说明的是&#xff0c;在配置完引脚后&#xff0c;需要将PA4引脚拉高&#xff0c;这样做是为了防止在发送数据起始位时&#xff0c;由于原来引脚是低电平而导致没有产生一个下降沿信号。

&#xff08;2&#xff09;IO模拟串口发送一个字节

遵循串口异步传输协议&#xff0c;编写了STM32上面的相应代码&#xff1a;

#define COM_TX_PORT GPIOA

#define COM_TX_PIN GPIO_Pin_4

#define COM_DATA_HIGH() GPIO_SetBits(COM_TX_PORT, COM_TX_PIN) //高电平
#define COM_DATA_LOW() GPIO_ResetBits(COM_TX_PORT, COM_TX_PIN) //低电平

u32 delayTime;

void VirtualCOM_ByteSend(u8 val)
{
u8 i &#61; 0;
COM_DATA_LOW(); //起始位
Delay_us(delayTime);
for(i &#61; 0; i < 8; i&#43;&#43;) //8位数据位
{
if(val & 0x01)
COM_DATA_HIGH();
else
COM_DATA_LOW();
Delay_us(delayTime);
val >>&#61; 1;
}
COM_DATA_HIGH(); //停止位
Delay_us(delayTime);
}

&#xff08;3&#xff09;IO模拟串口发送字符串

既然发送一个字节的函数已将实现了&#xff0c;那么发送字符串函数就简单了&#xff1a;

void VirtualCOM_StringSend(u8 *str)
{
while(*str !&#61; 0)
{
VirtualCOM_ByteSend(*str);
str&#43;&#43;;
}
}

 
3、IO模拟接收程序
接收的代码比发送的代码复杂些。先讲讲怎么IO口接收数据的思路。为了接收数据&#xff0c;IO引脚必须可以检测到传输数据的起始位&#xff0c;检测起始位其实相当于与要检测一个下降沿信号&#xff0c;那么引脚只要配置成外部中断模式就可以检测到这个起始信号。然后根据传输速率配置一个相应时间定时的定时器。当检测到起始信号后&#xff0c;打开该定时器&#xff0c;每隔一定时间就会进入定时器中断&#xff0c;检测当前的IO引脚高低电平&#xff0c;从而决定接收到的数据是‘1’还是‘0’。当第九次进入定时器中断服务程序时&#xff0c;说明已经收到了一个字节的数据&#xff0c;此时关闭定时器。
下面在讲讲怎么在STM32开发板上实现这一过程。
&#xff08;1&#xff09;选择IO引脚模拟串口接收引脚RX
我选择PA5来模拟串口的接收引脚RX&#xff0c;所以需要配置PA5为输入模式&#xff0c;同时打开它的外部中断。

#define COM_RX_PORT GPIOA

#define COM_RX_PIN GPIO_Pin_5

void VirtualCOM_RX_GPIOConfig(void)

{

GPIO_InitTypeDef GPIO_InitStructure;

EXTI_InitTypeDef EXTI_InitStruct;

NVIC_InitTypeDef NVIC_InitStructure;

 

/* PA5最为数据输入&#xff0c;模拟RX */
GPIO_InitStructure.GPIO_Pin &#61; COM_RX_PIN;
GPIO_InitStructure.GPIO_Mode &#61; GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Speed &#61; GPIO_Speed_50MHz;
GPIO_Init(COM_RX_PORT, &GPIO_InitStructure);

 

EXTI_InitStruct.EXTI_Line&#61;EXTI_Line5;
EXTI_InitStruct.EXTI_Mode&#61;EXTI_Mode_Interrupt;
EXTI_InitStruct.EXTI_Trigger&#61;EXTI_Trigger_Falling;//下降沿都中断
EXTI_InitStruct.EXTI_LineCmd&#61;ENABLE;
EXTI_Init(&EXTI_InitStruct);

 

NVIC_InitStructure.NVIC_IRQChannel&#61;EXTI9_5_IRQn; //外部中断&#xff0c;边沿触发
NVIC_InitStructure.NVIC_IRQChannelSubPriority &#61; 2;
NVIC_InitStructure.NVIC_IRQChannelCmd&#61;ENABLE;
NVIC_Init(&NVIC_InitStructure);

}

&#xff08;2&#xff09;配置一个定时器用来定时接收数据
我配置TIM2定时器为一定的定时周期&#xff0c;在它的中断服务程序中读取串口发送过来数据。定时器配置代码如下&#xff1a;

void TIM2_Configuration(u16 period)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;

NVIC_InitTypeDef NVIC_InitStructure;

RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);//使能TIM2的时钟
TIM_DeInit(TIM2); //复位TIM2定时器
TIM_InternalClockConfig(TIM2); //采用内部时钟给TIM2提供时钟源

TIM_TimeBaseStructure.TIM_Prescaler &#61; 72 - 1; //预分频系数为72&#xff0c;这样计数器时钟为72MHz/72 &#61; 1MHz
TIM_TimeBaseStructure.TIM_ClockDivision &#61; 0; //设置时钟分频
TIM_TimeBaseStructure.TIM_CounterMode &#61; TIM_CounterMode_Up;//设置计数器模式为向上计数模式
TIM_TimeBaseStructure.TIM_Period &#61; period - 1; //设置计数溢出大小&#xff0c;每计period个数就产生一个更新事件
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseStructure); //将配置应用到TIM2中

TIM_ClearFlag(TIM2, TIM_FLAG_Update); //清除溢出中断标志
TIM_ITConfig(TIM2,TIM_IT_Update,ENABLE); //开启TIM2的中断
TIM_Cmd(TIM2,DISABLE); //关闭定时器TIM2

 

NVIC_InitStructure.NVIC_IRQChannel &#61; TIM2_IRQn; //通道设置为TIM2中断
NVIC_InitStructure.NVIC_IRQChannelSubPriority &#61; 1;//响应式中断优先级0
NVIC_InitStructure.NVIC_IRQChannelCmd &#61; ENABLE; //打开中断
NVIC_Init(&NVIC_InitStructure);
}

&#xff08;3&#xff09;IO口模拟串口接收功能的实现
IO口接收串口数据的功能是通过PA5引脚的外部中断服务程序与定时器的中断服务程序相互配合实现的。首先需要为数据定一些状态机&#xff0c;方便标识接收到数据的状态&#xff1a;

enum{
COM_START_BIT, //停止位
COM_D0_BIT, //bit0
COM_D1_BIT, //bit1
COM_D2_BIT, //bit2
COM_D3_BIT, //bit3
COM_D4_BIT, //bit4
COM_D5_BIT, //bit5
COM_D6_BIT, //bit6
COM_D7_BIT, //bit7
COM_STOP_BIT, //bit8
};

定义好了状态机&#xff0c;还需要一个变量&#xff0c;来保存这些状态机的变化&#xff0c;并定义它的初始状态为COM_STOP_BIT&#xff1a;

u8 recvStat &#61; COM_STOP_BIT; //定义状态机

下面是PA5的外部中断服务程序&#xff0c;它的主要任务是检测起始位&#xff0c;当他第一次检测到下降沿&#xff0c;则说明数据即将到来&#xff0c;这时只要打开定时器就可以了&#xff1a;

#define COM_RX_STAT GPIO_ReadInputDataBit(COM_RX_PORT, COM_RX_PIN)

void EXTI9_5_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line5)!&#61;RESET)
{
if(!COM_RX_STAT) //检测引脚高低电平&#xff0c;如果是低电平&#xff0c;则说明检测到下升沿
{
if(recvStat &#61;&#61; COM_STOP_BIT) //状态为停止位
{
recvStat &#61; COM_START_BIT; //接收到开始位
Delay(1000); //延时一定时间
TIM_Cmd(TIM2, ENABLE); //打开定时器&#xff0c;接收数据
}
}
EXTI_ClearITPendingBit(EXTI_Line5); //清除EXTI_Line1中断挂起标志位
}
}

上面代码中&#xff0c;检测到下降沿并设置了状态之后&#xff0c;延时了一定的时候&#xff0c;才打开定时器&#xff0c;这样做的原因是让定时器每次在信号的中间检测&#xff0c;而不要在信号边沿检测。正如下面图所示&#xff1a;
 下面就是定时器的中断服务程序&#xff0c;它主要是接收串口发送过来的数据&#xff0c;在它之前我们需要线定义一个变量用来保存接收到的数据&#xff1a;

u8 recvData;

然后&#xff0c;定时器中断中&#xff0c;每收到1位数据就改变下状态机并同时写入这个recvData对应的数据位中&#xff0c;当收到8为数据后&#xff0c;然后关闭定时器定时&#xff0c;以等待新的数据到来&#xff1a;

void TIM2_IRQHandler(void)
{
if(TIM_GetITStatus(TIM2, TIM_IT_Update) !&#61; RESET) //检测是否发生溢出更新事件
{
TIM_ClearITPendingBit(TIM2 , TIM_FLAG_Update);//清除中断标志
recvStat&#43;&#43;; //改变状态机
if(recvStat &#61;&#61; COM_STOP_BIT) //收到停止位
{
TIM_Cmd(TIM2, DISABLE); //关闭定时器
return; //并返回
}
if(COM_RX_STAT) //&#39;1&#39;
{
recvData |&#61; (1 << (recvStat - 1));
}
else //&#39;0&#39;
{
recvData &&#61; ~(1 <<(recvStat - 1));
}
}
}

4、一些精确延时与不精确延时的实现
上面代码中&#xff0c;需要定义一个不精确定时与两个精确定时&#xff0c;分别用在检测到下降沿后延时一段时间在打开定时器和控制传输速率中&#xff1a;

void Delay(u32 t)
{
while(t--);
}

void Delay_us(u32 nus)
{
SysTick->LOAD&#61;nus*9; //时间加载
SysTick->CTRL|&#61;0x01; //开始倒数
while(!(SysTick->CTRL&(1<<16)));//等待时间到达
SysTick->CTRL&#61;0X00000000; //关闭计数器
SysTick->VAL&#61;0X00000000; //清空计数器
}

void Delay_ms(u16 nms)
{
SysTick->LOAD&#61;(u32)nms*9000; //给重装载寄存器赋值&#xff0c;9000时&#xff0c;将产生1ms的时基
SysTick->CTRL|&#61;0x01; //开始倒数
while(!(SysTick->CTRL&(1<<16))); //等待时间到达
SysTick->CTRL&#61;0X00000000; //关闭计数器
SysTick->VAL&#61;0X00000000; //清空计数器
}

5、编写一个配置波特率的函数

这里能配置的只有300、600、1200三种波特率&#xff0c;其他的波特率我不想弄&#xff0c;也没有必要弄。下面编写一个初始化IO模拟的串口&#xff0c;包括引脚配置、波特率设置、定时时间设置等&#xff1a;

#define _300BuadRate 3150
#define _600BuadRate 1700
#define _1200BuadRate 800

void VirtualCOM_Config(u16 baudRate)

{
u32 period;
VirtualCOM_TX_GPIOConfig();

VirtualCOM_RX_GPIOConfig();
if(baudRate &#61;&#61; _300BuadRate) //波特率300
period &#61; _300BuadRate &#43; 250;
else if (baudRate &#61;&#61; _600BuadRate) //波特率600
period &#61; _600BuadRate &#43; 50;
else if (baudRate &#61;&#61; _1200BuadRate) //波特率1200
period &#61; _1200BuadRate &#43; 50;
TIM2_Configuration(period); //设置对应模特率的定时器的定时时间
delayTime &#61; baudRate; //设置IO串口发送的速率
}

要问上面的那些数字是怎么得来的&#xff0c;实话说&#xff1a;我是试出来&#xff0c;但是是有根据地试出来的。我以波特率为1200为例&#xff1a;IO串口发送函数VirtualCOM_ByteSend()中&#xff0c;我们用Delay_us(delayTime)来控制传输的速率&#xff0c;如果波特率设为1200&#xff0c;则1/1200&#61;830us相当于没830us传输1bit数据&#xff0c;所以在delayTime理论上应该设为830才能保证以波特率1200的速率发送数据&#xff0c;但是由于发送是由代码实现&#xff0c;有一定的延时&#xff0c;而不像真正串口通过移位寄存器发送那样快速&#xff0c;所以需要将delayTime的值在830附近调整&#xff0c;最后我试出来delayTime&#61;800时&#xff0c;正好实现了波特率为1200的速率发送。
同样的&#xff0c;在IO串口接收时&#xff0c;需要设定定时周期&#xff0c;这个周期也是试出来的&#xff0c;但是也是有依据的。还是以1200波特率接收为例&#xff1a;理论上应该设置定时时间为1/1200&#61;830us&#xff0c;则需要的定时值为72000000/(1/830us)&#61;72*830&#xff0c;这里设置定时器的预分频为72&#xff0c;则周期值应该为830&#xff0c;所以上面代码中period的理论上应该等于830&#xff0c;但是接收是由代码写成的&#xff0c;有一定的延时&#xff0c;而不像真正串口一样全部有硬件完成那样快速&#xff0c;所以需要将period的值在830附近调整&#xff0c;最后试出来period&#61;850时&#xff0c;可以正常接收串口发送过来的数据。
6、其他函数的编写
首先需要编写的BSP_Init()函数&#xff0c;来初始化板子的其他一些外设的的初始化&#xff1a;

void BSP_Init(void)

{

static volatile ErrorStatus HSEStartUpStatus &#61; SUCCESS;

RCC_DeInit(); //默认配置SYSCLK, HCLK, PCLK2, PCLK1, 复位后就是该配置

RCC_HSEConfig(RCC_HSE_ON); //使能外部高速晶振
HSEStartUpStatus &#61; RCC_WaitForHSEStartUp();//等待外部高速稳定

if(HSEStartUpStatus &#61;&#61; SUCCESS)
{
FLASH_PrefetchBufferCmd(FLASH_PrefetchBuffer_Enable);//使能flash预读取缓冲区
FLASH_SetLatency(FLASH_Latency_2); //令Flash处于等待状态&#xff0c;2是针对高频时钟的
RCC_HCLKConfig(RCC_SYSCLK_Div1); //HCLK &#61; SYSCLK 设置高速总线时钟&#61;系统时钟
RCC_PCLK2Config(RCC_HCLK_Div1); //PCLK2 &#61; HCLK 设置低速总线2时钟&#61;高速总线时钟
RCC_PCLK1Config(RCC_HCLK_Div2); //PCLK1 &#61; HCLK/2 设置低速总线1的时钟&#61;高速时钟的二分频
RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_9); //PLLCLK &#61; 8MHz * 9 &#61; 72 MHz 利用锁相环讲外部8Mhz晶振9倍频到72Mhz
RCC_PLLCmd(ENABLE); //使能PLL锁相环
while(RCC_GetFlagStatus(RCC_FLAG_PLLRDY) &#61;&#61; RESET){} //等待锁相环输出稳定
RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK);//将锁相环输出设置为系统时钟
while(RCC_GetSYSCLKSource() !&#61; 0x08){} //等待校验成功
} //使能GPIO口所使用的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOB|RCC_APB2Periph_GPIOC|RCC_APB2Periph_GPIOD|RCC_APB2Periph_GPIOE|RCC_APB2Periph_GPIOF|RCC_APB2Periph_GPIOG, ENABLE);
最后是main函数&#xff0c;main函数很简单&#xff0c;只要调用配置IO串口的配置函数就可以了&#xff1a;

extern u8 recvData;

int main(void)

{

BSP_Init();

VirtualCOM_Config(_600BuadRate); //配置IO模拟串口的波特率为600

VirtualCOM_StringSend("HelloWorld!\r\n"); //发送“HelloWorld!”字符串

while(1)

{

VirtualCOM_ByteSend(recvData);

Delay(5000000);

}

}

7、现象

首先。我们需要用TTL转USB的串口线&#xff0c;连接到电脑&#xff0c;打开串口调试工具&#xff0c;设置波特率为600&#xff0c;1位停止位&#xff0c;然后就可以收到IO模拟串口发过来的"HelloWorld"&#xff0c;然后&#xff0c;我们发送一个字符&#39;a&#39;过去&#xff0c;然后就会每间隔一段时间打印出该字符。如下图所示&#xff1a;



推荐阅读
  • 本文介绍了Oracle数据库中tnsnames.ora文件的作用和配置方法。tnsnames.ora文件在数据库启动过程中会被读取,用于解析LOCAL_LISTENER,并且与侦听无关。文章还提供了配置LOCAL_LISTENER和1522端口的示例,并展示了listener.ora文件的内容。 ... [详细]
  • 如何自行分析定位SAP BSP错误
    The“BSPtag”Imentionedintheblogtitlemeansforexamplethetagchtmlb:configCelleratorbelowwhichi ... [详细]
  • IhaveconfiguredanactionforaremotenotificationwhenitarrivestomyiOsapp.Iwanttwodiff ... [详细]
  • 本文分享了一个关于在C#中使用异步代码的问题,作者在控制台中运行时代码正常工作,但在Windows窗体中却无法正常工作。作者尝试搜索局域网上的主机,但在窗体中计数器没有减少。文章提供了相关的代码和解决思路。 ... [详细]
  • CF:3D City Model(小思维)问题解析和代码实现
    本文通过解析CF:3D City Model问题,介绍了问题的背景和要求,并给出了相应的代码实现。该问题涉及到在一个矩形的网格上建造城市的情景,每个网格单元可以作为建筑的基础,建筑由多个立方体叠加而成。文章详细讲解了问题的解决思路,并给出了相应的代码实现供读者参考。 ... [详细]
  • 本文讨论了在openwrt-17.01版本中,mt7628设备上初始化启动时eth0的mac地址总是随机生成的问题。每次随机生成的eth0的mac地址都会写到/sys/class/net/eth0/address目录下,而openwrt-17.01原版的SDK会根据随机生成的eth0的mac地址再生成eth0.1、eth0.2等,生成后的mac地址会保存在/etc/config/network下。 ... [详细]
  • Imtryingtofigureoutawaytogeneratetorrentfilesfromabucket,usingtheAWSSDKforGo.我正 ... [详细]
  • 实现一个通讯录系统,可添加、删除、修改、查找、显示、清空、排序通讯录信息
    本文介绍了如何实现一个通讯录系统,该系统可以实现添加、删除、修改、查找、显示、清空、排序通讯录信息的功能。通过定义结构体LINK和PEOPLE来存储通讯录信息,使用相关函数来实现各项功能。详细介绍了每个功能的实现方法。 ... [详细]
  • Python操作MySQL(pymysql模块)详解及示例代码
    本文介绍了使用Python操作MySQL数据库的方法,详细讲解了pymysql模块的安装和连接MySQL数据库的步骤,并提供了示例代码。内容涵盖了创建表、插入数据、查询数据等操作,帮助读者快速掌握Python操作MySQL的技巧。 ... [详细]
  • 李逍遥寻找仙药的迷阵之旅
    本文讲述了少年李逍遥为了救治婶婶的病情,前往仙灵岛寻找仙药的故事。他需要穿越一个由M×N个方格组成的迷阵,有些方格内有怪物,有些方格是安全的。李逍遥需要避开有怪物的方格,并经过最少的方格,找到仙药。在寻找的过程中,他还会遇到神秘人物。本文提供了一个迷阵样例及李逍遥找到仙药的路线。 ... [详细]
  • 先看官方文档TheJavaTutorialshavebeenwrittenforJDK8.Examplesandpracticesdescribedinthispagedontta ... [详细]
  • 本文介绍了Codeforces Round #321 (Div. 2)比赛中的问题Kefa and Dishes,通过状压和spfa算法解决了这个问题。给定一个有向图,求在不超过m步的情况下,能获得的最大权值和。点不能重复走。文章详细介绍了问题的题意、解题思路和代码实现。 ... [详细]
  • 设计模式——模板方法模式的应用和优缺点
    本文介绍了设计模式中的模板方法模式,包括其定义、应用、优点、缺点和使用场景。模板方法模式是一种基于继承的代码复用技术,通过将复杂流程的实现步骤封装在基本方法中,并在抽象父类中定义模板方法的执行次序,子类可以覆盖某些步骤,实现相同的算法框架的不同功能。该模式在软件开发中具有广泛的应用价值。 ... [详细]
  • Gitlab接入公司内部单点登录的安装和配置教程
    本文介绍了如何将公司内部的Gitlab系统接入单点登录服务,并提供了安装和配置的详细教程。通过使用oauth2协议,将原有的各子系统的独立登录统一迁移至单点登录。文章包括Gitlab的安装环境、版本号、编辑配置文件的步骤,并解决了在迁移过程中可能遇到的问题。 ... [详细]
  • 深入解析Linux下的I/O多路转接epoll技术
    本文深入解析了Linux下的I/O多路转接epoll技术,介绍了select和poll函数的问题,以及epoll函数的设计和优点。同时讲解了epoll函数的使用方法,包括epoll_create和epoll_ctl两个系统调用。 ... [详细]
author-avatar
捕鱼达人2602929461
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有