当前位置: 首页 > news >正文

【51单片机】6. 定时器、按键切换流水灯时钟Demo

1. 定时器

  • 定义:51单片机定时器属于单片机内部资源,电路的连接和运转均在单片机内部完成

  • 作用:

    • 用作计时系统,实现软件计时,或使程序隔固定时间完成一项操作

    • 替代长时间的Delay,提高CPU运行效率和处理速度

举例子,在写流水灯代码的时候,置状态→Delay→置状态→Delay→...,假如想要边流水灯,边使用按键控制流水灯就会很困难,因为Delay的时候会占用所有的CPU,导致无法同时进行其他操作。

  • 原理:

    • 定时器在单片机内部像一个小闹钟,根据时钟的输出信号,每隔“一秒”,计数单元数值增加一

    • 当计数单元数值增加到“设定的闹钟提醒时间”时,计数单元就会向中断系统发出中断申请,产生“响铃提醒”

    • 从而程序跳转到中断服务函数中执行

2. 常用的定时模式(16位定时器/计数器)

在这里插入图片描述

  • Sysclk:系统时钟,即晶振周期,使用这个就是定时器

    • Sysclk有÷12÷6,这个表示的是12分频或6分频。江科大视频里的单片机是12MHz的,如果选择÷12就得到输出的频率是1MHz,一个周期就表示1μs的时间,如果选择÷6就得到2MHz,则是2μs
  • T0 Pin:由单片机外部引脚提供时,单片机的定时器就是计数器

  • 其中, C / T ‾ C/\overline{T} C/T表示选下(计数器Counter)还是选上(定时器Timer):=0表示选择上面的定时器,=1表示选择下面的计数器

  • 下面向右旋转90°的表示非门,GATE输入为0,经非门为1

  • 最靠近非门的带有轻微弯曲的,还连接了 I N T 0 ‾ \overline{INT0} INT0的表示或门

  • 靠近或门的直的、还连接了 T R 0 TR_0 TR0的表示与门

  • TL0表示Time Low,TH0表示Time High,就是低位和高位的区别,TL0和TH0连起来就是16位的计数器,每次+1+1,作为定时器使用,到达上限65536后触发中断

3. 中断系统

这是计算机组成原理里边比较基础的一个内容,这里仅粘贴一些概念:

  • 中断系统是为使CPU具有对外界紧急事件的实时处理能力而设置的

  • CPU正处理某件事时外界发生了紧急事件请求,要求CPU暂停当前工作,转而去处理紧急事件,处理完后,再回到原来被中断的地方继续工作,则称为中断

  • 微型机中断系统允许多个中断源,多个中断源同时向CPU请求中断,要求为它服务时,CPU优先响应哪一个中断源请求,通常根据中断源轻重缓急优先处理最紧急的中断请求源。即每隔中断源有一个优先级别,CPU总是优先响应优先级别最高的中断请求

  • CPU在处理一个中断请求时,发生了另一个优先级更高的中断请求,CPU能够暂停对原来中断源的服务,转而去处理高优先级的中断请求源,处理完后再回到原低级中断服务程序,这样的过程称为中断嵌套,这样的中断系统称为多级中断系统。而没有中断嵌套功能的中断系统称为单级中断系统

在这里插入图片描述

4. 定时器、中断相关寄存器

单片机通过配置寄存器来控制内部线路的连接。

定时器/计数器与中断相关的寄存器如下所示:

在这里插入图片描述

只需要给这些寄存器赋对应的值,就可以控制电路进行工作。

以寄存器电路图为例:

在这里插入图片描述

这其中,根据单片机相关说明书:

  • EA:CPU总中断允许控制位,EA=1,CPU开放中断,EA=0,CPU屏蔽所有中断申请。EA的作用是使中断允许形成两级控制。即各中断源首先受EA控制;其次还受各中断源自己的中断允许控制位控制。

  • EX0/EX1,ET0/1:外部中断0/1中断允许位。为1时允许中断,为0禁止中断

  • PX0/1,PT0/1则是控制中断优先级的:为0低优先级,为1高优先级

5. 初始化定时器

配置可以参考STC89C52手册里面中断和定时器/计数器相关的内容。需要配置TCON和TMOD

定时器相关位如下:

在这里插入图片描述

M1和M0共同决定工作在那种模式:

  • M1 M0 模式

  • 0 0 13位定时器/计数器

  • 0 1 16位定时器/计数器(常用)

  • 1 0 8位自动重装载定时器

  • 1 1 定时器0此时作为双8位定时器/计数器

C / T ‾ C/\overline{T} C/T给0就是定时器模式,给1就是计数器模式

GATE门控端给0就是TR_0单独控制

TMOD = 0x01;

PS: 不可位寻址表示必须整体赋值,不能单个赋值;可位寻址就是可以整体赋值也可以单个赋值。

但上面这种配置会有一个问题,可能原本有其他程序在使用高4位的内容,这样一配置就会覆盖了高4位,所以换成如下方式:

TMOD &= 0xF0; // TMOD低四位清零,高四位不变
TMOD |= 0x01; // TMOD最低位置1,高位不变

在这里插入图片描述

TF0是定时器/计数器T0溢出中断标志。置1表示向CPU请求中断。

TF0 = 0;

TR0是定时器运行控制位,TR0=1时允许T0开始计数,TR0=0时禁止T0计数。

TR0 = 1;

IE0:IE0=1表示外部中断0向CPU请求中断,CPU响应外部中断时由硬件将IE0置为0。

IT0:外部中断0。

PS:IE0和IT0用于管GATE那一部分的,但是GATE已经置0了,此时不是从外面输入的无需理会。

6. 定时器进一步初始化

频率f为12MHz,定时器时钟为12T,根据公式 T = 1 f T=\frac{1}{f} T=f1,有 12 T = 1 12 × 10 6 H z 12T=\frac{1}{12×10^6Hz} 12T=12×106Hz1,即1μs计时一次

定时器每次+1表示1μs,作为16位定时器,总共能计65536μs,也就是65ms。

想要能够计到1s,唯一的办法是每计1ms就触发中断来重置定时器,重复1000次后就是1s。

为了实现 “每计1ms就触发中断来重置定时器”,我们需要给TL0和TH0置不同的数:

在这里插入图片描述

也就是从64536开始计数,我们要给TH0和TL0一共赋上64536的值。通过除法和求余可以取出高8位值和低8位值:

TH0 = 64536 / 256;
TL0 = 64536 % 256;

以上定时器的位置,也可以通过stc-isp这个软件来快速初始化:

在这里插入图片描述
上图选的有点问题,应该选16位而不是16位自动重载

7. 中断部分初始化

实际上就是将4中的中断通路给打通,等待中断到来时,发起中断,观察4中的图,实际上需要处理的寄存器包括:

  • ET0

  • EA

  • PT0(IP,中断优先级)

ET0=1;
EA=1;
PT0=0;	

8. 用定时器,让LED灯1s亮1次

#include <REGX52.H>void Timer0_Init(void)		//1毫秒@11.0592MHz
{// 定时器初始化TMOD &= 0xF0;		//设置定时器模式TMOD |= 0x01;		//设置定时器模式TL0 = 0x18;		//设置定时初值TH0 = 0xFC;		//设置定时初值TF0 = 0;		//清除TF0标志TR0 = 1;		//定时器0开始计时// 中断初始化ET0=1;EA=1;PT0=0; // 优先级设置
}unsigned int timeCount = 0;
void Timer0_Rountine() interrupt 1 //定时器0中断函数,定时器计了1000次1μs为1ms
{TL0 = 0x18;		//设置定时初值TH0 = 0xFC;		//设置定时初值timeCount++;   // 统计计数次数if (timeCount == 1000)  // 产生1000次中断,即timeCount计了1000次为1s{timeCount = 0;  // 重置计数P2_0 = ~P2_0;  // 亮灯取反}
}void main()
{Timer0_Init();while (1){}
}

实际上我的板子是11.0592MHz,这样速度可能会比1s快,推理如下:

选择12MHz & 12T → 1μs计时一次,1ms = 1000μs,计1000次得1ms,while循环反复1000得1s
选择11.0592MHz & 12T → 0.922μs计时一次,1ms = 1000μs,计1000次得0.922ms,while循环反复1000得0.92s

整体的思路就是:

  1. 上电时初始化定时器及中断的寄存器(Timer0_Init()函数)

  2. 写中断函数,处理自己想要的流程

9. 利用定时器实现按钮控制LED流水灯向左/向右闪

主函数:

#include <REGX52.H>
#include <INTRINS.H>
#include "Timer0.h"
#include "Key.h"unsigned char keyNum,LEDMode = 0; // 表示获取的keyNum,以及当前流水灯所处状态void main()
{P2 = 0xFE; // 流水灯初始状态,第1个灯亮Timer0_Init(); // 上电,定时器初始化while (1){keyNum = key(); // 获取keyNumif (keyNum) // 如果keyNum不等于0,判断是否需要转变模式{if (keyNum == 1) // 如果按下按键{LEDMode++; // LED模式改变if (LEDMode == 2) LEDMode = 0; // 达到2时变回1}}}
}unsigned int timeCount = 0;
void Timer0_Rountine() interrupt 1 //定时器0中断函数,定时器计了1000次1μs为1ms
{TL0 = 0x18;		//设置定时初值TH0 = 0xFC;		//设置定时初值timeCount++;   // 统计计数次数if (timeCount == 500)  // 产生1000次中断,即timeCount计了1000次为1s{timeCount = 0;  // 重置计数if (LEDMode == 0) P2 = _crol_(P2,1); // INTRINS.H封装的好用函数,左移1位,溢出会自动判断并回去else P2 = _cror_(P2,1); // INTRINS.H封装的好用函数,右移1位,溢出会自动判断并回去}
}

Timer0.c实际上就是封装了初始化定时器0的函数:

#include <REGX52.H>/*** @brief 定时器0初始化,1毫秒@11.0592MHz* @param 无* @retval 无*/
void Timer0_Init(void)		//1毫秒@11.0592MHz
{// 定时器初始化TMOD &= 0xF0;		//设置定时器模式TMOD |= 0x01;		//设置定时器模式TL0 = 0x18;		//设置定时初值TH0 = 0xFC;		//设置定时初值TF0 = 0;		//清除TF0标志TR0 = 1;		//定时器0开始计时// 中断初始化ET0=1;EA=1;PT0=0; // 优先级设置
}/* 定时器中断函数模板
void Timer0_Rountine() interrupt 1 //定时器0中断函数,定时器计了1000次1μs为1ms
{static unsigned int timeCount = 0;TL0 = 0x18;		//设置定时初值TH0 = 0xFC;		//设置定时初值timeCount++;   // 统计计数次数if (timeCount == 1000)  // 产生1000次中断,即timeCount计了1000次为1s{timeCount = 0;  // 重置计数}
}
*/

Timer0.h:

#ifndef __TIMER0_H__
#define __TIMER0_H__void Timer0_Init(void);#endif

Key.c就是检测按下的按键键值:

#include <REGX52.H>
#include "Delay.h"/*** @brief 获取独立按键键码* @param 无* @retval */
unsigned char key()
{unsigned char currKey = 0;if (P3_1 == 0) {Delay(20); while(P3_1 == 0); Delay(20); currKey = 1;}if (P3_0 == 0) {Delay(20); while(P3_0 == 0); Delay(20); currKey = 2;}if (P3_2 == 0) {Delay(20); while(P3_2 == 0); Delay(20); currKey = 3;}if (P3_3 == 0) {Delay(20); while(P3_3 == 0); Delay(20); currKey = 4;}return currKey;
}

Key.h:

#ifndef __KEY_H__
#define __KEY_H__unsigned char key();#endif 

Delay.c是很久之前写的延时:

#include <INTRINS.H>void Delay(unsigned int ms)		//@11.0592MHz
{unsigned char i, j;while (ms){_nop_();i = 2;j = 199;do{while (--j);} while (--i);ms--;}
}

Delay.h:

#ifndef __DELAY_H__
#define __DELAY_H__void Delay(unsigned int ms);#endif

当然也可以自己试着写一下左右移1位的函数:

#include <REGX52.H>
#include <INTRINS.H>
#include "Timer0.h"
#include "Key.h"unsigned char keyNum,LEDMode = 0; // 表示获取的keyNum,以及当前流水灯所处状态
unsigned char bitMoveL(unsigned char P);
unsigned char bitMoveR(unsigned char P);void main()
{P2 = 0xFE; // 流水灯初始状态,第1个灯亮Timer0_Init(); // 上电,定时器初始化while (1){keyNum = key(); // 获取keyNumif (keyNum) // 如果keyNum不等于0,判断是否需要转变模式{if (keyNum == 1) // 如果按下按键{LEDMode++; // LED模式改变if (LEDMode == 2) LEDMode = 0; // 达到2时变回1}}}
}unsigned int timeCount = 0;
void Timer0_Rountine() interrupt 1 //定时器0中断函数,定时器计了1000次1μs为1ms
{TL0 = 0x18;		//设置定时初值TH0 = 0xFC;		//设置定时初值timeCount++;   // 统计计数次数if (timeCount == 500)  // 产生1000次中断,即timeCount计了1000次为1s{timeCount = 0;  // 重置计数if (LEDMode == 0) P2 = bitMoveL(P2); // INTRINS.H封装的好用函数,左移1位,溢出会自动判断并回去else P2 = bitMoveR(P2); // INTRINS.H封装的好用函数,右移1位,溢出会自动判断并回去}
}unsigned char bitMoveL(unsigned char P)
{return P == 0x7F? 0xFE : ~((~P) << 1);
}unsigned char bitMoveR(unsigned char P)
{return P == 0xFE? 0x7F : ~((~P) >> 1);
}

注意:如果这里给的类型都是unsigned int的话,右移就会出现问题,因为unsigned int是16位的,右移可能会导致高8位部分移动过来出现一些不合理的情况

另附结果,注意:由于LED灯位置的设计,我们做左移操作时,板子上显示的是右移;我们做右移操作时,板子上显示的结果是左移。

按键切换流水灯模式

左移:
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
省去中间一部分…跳到D7
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

右移:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

10. 定时器显示时钟

main.c

#include <REGX52.H>
#include "Timer0.h"
#include "Delay.h"
#include "LCD1602.h"unsigned char hours = 0;
unsigned char minutes = 0;
unsigned char seconds = 0;void main()
{//上电初始化LCD_Init();Timer0_Init();LCD_ShowString(1,1,"Clock:");LCD_ShowNum(2,1,hours,2);LCD_ShowString(2,3,":");LCD_ShowNum(2,4,minutes,2);LCD_ShowString(2,6,":");LCD_ShowNum(2,7,seconds,2);while(1){}
}void Timer0_Rountine() interrupt 1 //定时器0中断函数,定时器计了1000次1μs为1ms
{static unsigned int timeCount = 0;TL0 = 0x18;		//设置定时初值TH0 = 0xFC;		//设置定时初值timeCount++;   // 统计计数次数if (timeCount == 1000)  // 产生1000次中断,即timeCount计了1000次为1s{timeCount = 0;  // 重置计数// 更新计时seconds++; // 每1s,秒针+1if (seconds == 60) // 当到60s时{minutes++; // 分针+1seconds = 0; // 重置秒针if (minutes == 60) // 当到60min时{hours++; // 时针+1minutes = 0; // 重置分针if (hours == 24) // 当到24h时{hours = 0; // 重置时针}LCD_ShowNum(2,1,hours,2); // 显示时针}LCD_ShowNum(2,4,minutes,2); // 显示分针}LCD_ShowNum(2,7,seconds,2); // 显示秒数}
}

Timer0.cTimer0.hDelay.cDelay.h的代码和9中的一样

LCD1602.c:

#include <REGX52.H>//引脚配置:
sbit LCD_RS=P2^6;
sbit LCD_RW=P2^5;
sbit LCD_EN=P2^7;
#define LCD_DataPort P0//函数定义:
/*** @brief  LCD1602延时函数,12MHz调用可延时1ms* @param  无* @retval 无*/
void LCD_Delay()
{unsigned char i, j;i = 2;j = 239;do{while (--j);} while (--i);
}/*** @brief  LCD1602写命令* @param  Command 要写入的命令* @retval 无*/
void LCD_WriteCommand(unsigned char Command)
{LCD_RS=0;LCD_RW=0;LCD_DataPort=Command;LCD_EN=1;LCD_Delay();LCD_EN=0;LCD_Delay();
}/*** @brief  LCD1602写数据* @param  Data 要写入的数据* @retval 无*/
void LCD_WriteData(unsigned char Data)
{LCD_RS=1;LCD_RW=0;LCD_DataPort=Data;LCD_EN=1;LCD_Delay();LCD_EN=0;LCD_Delay();
}/*** @brief  LCD1602设置光标位置* @param  Line 行位置,范围:1~2* @param  Column 列位置,范围:1~16* @retval 无*/
void LCD_SetCursor(unsigned char Line,unsigned char Column)
{if(Line==1){LCD_WriteCommand(0x80|(Column-1));}else if(Line==2){LCD_WriteCommand(0x80|(Column-1+0x40));}
}/*** @brief  LCD1602初始化函数* @param  无* @retval 无*/
void LCD_Init()
{LCD_WriteCommand(0x38);//八位数据接口,两行显示,5*7点阵LCD_WriteCommand(0x0c);//显示开,光标关,闪烁关LCD_WriteCommand(0x06);//数据读写操作后,光标自动加一,画面不动LCD_WriteCommand(0x01);//光标复位,清屏
}/*** @brief  在LCD1602指定位置上显示一个字符* @param  Line 行位置,范围:1~2* @param  Column 列位置,范围:1~16* @param  Char 要显示的字符* @retval 无*/
void LCD_ShowChar(unsigned char Line,unsigned char Column,char Char)
{LCD_SetCursor(Line,Column);LCD_WriteData(Char);
}/*** @brief  在LCD1602指定位置开始显示所给字符串* @param  Line 起始行位置,范围:1~2* @param  Column 起始列位置,范围:1~16* @param  String 要显示的字符串* @retval 无*/
void LCD_ShowString(unsigned char Line,unsigned char Column,char *String)
{unsigned char i;LCD_SetCursor(Line,Column);for(i=0;String[i]!='\0';i++){LCD_WriteData(String[i]);}
}/*** @brief  返回值=X的Y次方*/
int LCD_Pow(int X,int Y)
{unsigned char i;int Result=1;for(i=0;i<Y;i++){Result*=X;}return Result;
}/*** @brief  在LCD1602指定位置开始显示所给数字* @param  Line 起始行位置,范围:1~2* @param  Column 起始列位置,范围:1~16* @param  Number 要显示的数字,范围:0~65535* @param  Length 要显示数字的长度,范围:1~5* @retval 无*/
void LCD_ShowNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{unsigned char i;LCD_SetCursor(Line,Column);for(i=Length;i>0;i--){LCD_WriteData(Number/LCD_Pow(10,i-1)%10+'0');}
}/*** @brief  在LCD1602指定位置开始以有符号十进制显示所给数字* @param  Line 起始行位置,范围:1~2* @param  Column 起始列位置,范围:1~16* @param  Number 要显示的数字,范围:-32768~32767* @param  Length 要显示数字的长度,范围:1~5* @retval 无*/
void LCD_ShowSignedNum(unsigned char Line,unsigned char Column,int Number,unsigned char Length)
{unsigned char i;unsigned int Number1;LCD_SetCursor(Line,Column);if(Number>=0){LCD_WriteData('+');Number1=Number;}else{LCD_WriteData('-');Number1=-Number;}for(i=Length;i>0;i--){LCD_WriteData(Number1/LCD_Pow(10,i-1)%10+'0');}
}/*** @brief  在LCD1602指定位置开始以十六进制显示所给数字* @param  Line 起始行位置,范围:1~2* @param  Column 起始列位置,范围:1~16* @param  Number 要显示的数字,范围:0~0xFFFF* @param  Length 要显示数字的长度,范围:1~4* @retval 无*/
void LCD_ShowHexNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{unsigned char i,SingleNumber;LCD_SetCursor(Line,Column);for(i=Length;i>0;i--){SingleNumber=Number/LCD_Pow(16,i-1)%16;if(SingleNumber<10){LCD_WriteData(SingleNumber+'0');}else{LCD_WriteData(SingleNumber-10+'A');}}
}/*** @brief  在LCD1602指定位置开始以二进制显示所给数字* @param  Line 起始行位置,范围:1~2* @param  Column 起始列位置,范围:1~16* @param  Number 要显示的数字,范围:0~1111 1111 1111 1111* @param  Length 要显示数字的长度,范围:1~16* @retval 无*/
void LCD_ShowBinNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{unsigned char i;LCD_SetCursor(Line,Column);for(i=Length;i>0;i--){LCD_WriteData(Number/LCD_Pow(2,i-1)%2+'0');}
}

LCD1602.h:

#ifndef __LCD1602_H__
#define __LCD1602_H__//用户调用函数:
void LCD_Init();
void LCD_ShowChar(unsigned char Line,unsigned char Column,char Char);
void LCD_ShowString(unsigned char Line,unsigned char Column,char *String);
void LCD_ShowNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length);
void LCD_ShowSignedNum(unsigned char Line,unsigned char Column,int Number,unsigned char Length);
void LCD_ShowHexNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length);
void LCD_ShowBinNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length);#endif

效果如下:

定时器实现时钟

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

江科大的代码是将LCD_ShowNum的所有内容放在While循环里不断刷新:

#include <REGX52.H>
#include "Timer0.h"
#include "Delay.h"
#include "LCD1602.h"unsigned char hours = 23;
unsigned char minutes = 59;
unsigned char seconds = 55;void main()
{//上电初始化LCD_Init();Timer0_Init();LCD_ShowString(1,1,"Clock:");LCD_ShowNum(2,1,hours,2);LCD_ShowString(2,3,":");LCD_ShowNum(2,4,minutes,2);LCD_ShowString(2,6,":");LCD_ShowNum(2,7,seconds,2);while(1){LCD_ShowNum(2,1,hours,2); // 显示时针LCD_ShowNum(2,4,minutes,2); // 显示分针LCD_ShowNum(2,7,seconds,2); // 显示秒数}
}void Timer0_Rountine() interrupt 1 //定时器0中断函数,定时器计了1000次1μs为1ms
{static unsigned int timeCount = 0;TL0 = 0x18;		//设置定时初值TH0 = 0xFC;		//设置定时初值timeCount++;   // 统计计数次数if (timeCount == 1000)  // 产生1000次中断,即timeCount计了1000次为1s{timeCount = 0;  // 重置计数// 更新计时seconds++; // 每1s,秒针+1if (seconds == 60) // 当到60s时{minutes++; // 分针+1seconds = 0; // 重置秒针if (minutes == 60) // 当到60min时{hours++; // 时针+1minutes = 0; // 重置分针if (hours == 24) // 当到24h时{hours = 0; // 重置时针}}}}
}

我是把赋值语句全写在中断里面了, 我个人感觉应该是差不多的。但是,问题是:不知道赋值语句会不会耗时长从而导致时钟计数不准

  • 不会的话,我觉得我的方法更好一些,必要时候才赋值,不用一直刷新

  • 会的话,江科大的方法更好

相关文章:

  • Harbor 2.12.2 and 2.12.3 初始化密码错误
  • 风控系统中,要调用第三方服务获取信息,很慢,如何解决?
  • Pytorch中view函数详解和工程实战示例
  • Vue + element实现电子围栏功能, 根据省市区选择围栏, 自定义围栏 ,手动输入地名围栏, 保存围栏,清除围栏,加载围栏,批量检测标点是否在围栏内。
  • 杭州电商代运营公司排名前十
  • 网络的那些事——初级——路由策略
  • FastDFS 分布式存储
  • Vue 3.6前瞻:响应式性能革命与Vapor模式展望
  • codeforces 2057D. Gifts Order
  • springboot3+mybatisplus(5)-backend-mybaitsplus+frontend-router
  • 7.8 Evaluating the finetuned LLM
  • Linux下OLLAMA安装卡住怎么办?
  • uni-app项目怎么实现多服务环境切换
  • LangChain--(1)
  • 如何将一个url地址打包成一个windows桌面版本的应用程序
  • 质因数分解_java
  • Redis哨兵机制
  • 基于SpringAI实现专家系统
  • echarts中给饼图加圆点
  • 关于深度学习网络中的归一化BN
  • 做app网站有哪些/百度官方人工客服电话
  • 做家装的设计公司网站/高手优化网站
  • 有什么做兼职的好的网站/常用的seo工具推荐
  • 楚雄网站建设/搜索推广渠道有哪些
  • cms网站后台管理系统/外贸seo推广
  • 网站网页制作教程/企业品牌网站营销