51单片机学习笔记

模块化编程

把各个模块的代码放在不同的c.文件里,再.h文件里提供外部可调用函数的声明,其他.c文件想使用其中的代码时,只需要#include "XXX.h"文件即可。使用模块化编程可极大的提高代码的可阅读性、可维护性、可移植性等。

个人理解:分开各个模块,使其作用专一,方便更改,或者复制到另一个项目,同时也简洁,更容易读懂代码。

格式:(以Delay函数举例)

  1. .h文件:可被外部调用的函数、变量的声明
//文件名一般大写
#ifndef _DELAY_H_//if not define如果没有定义,防止重复定义
#define _DELAY_H_

void Delay(unsigned char xms);//外部可调用的函数,注意:这里末尾要加分号

#endif
  1. .c文件:函数、变量的定义
//写Delay函数的定义
void Delay(unsigned int xms)
{
	unsigned char i, j;
	while(xms--)
	{
		i = 2;
		j = 239;
		do
		{
		while(--j);
		}while(--i);
	}
}

写好并添加进工程之后便可在main主函数中,添加头文件#include “Delay.h”(双引号表示自己写的),就可以使用Delay函数了。

注意

  • 任何自定义的变量、函数,在调用前必须有定义或者声明(同一个.c);
  • 使用到的自动化函数的.c文件必须添加到工程参与编译;
  • 使用到的.h为念必须要放到编译器可寻找的地方(工程文件夹根目录、安装目录、自定义)。

引申知识点
c预编译
:c语言的预编译以#开头,作用是在真正的编译开始之前,对代码进行一些处理(预编译)
在这里插入图片描述

此外还有#ifndef,#if,#else,#elif,#undef等。

LCD调试工具

  • 使用LCD1602液晶屏作为调试窗口、提供类似printf函数的功能,可实时观察单片机内部数据的变换情况,便于调试和演示。

这一节没什么好记的,因为用的都是他写好的函数,只要知道怎么用就好了。用法如下:
在这里插入图片描述
注意:LCD与数码管冲突。

矩阵键盘

介绍

  • 在键盘中案件数量较多时,为了减少I/O口的占用,通常将按键排列成矩阵形式
  • 采用逐行或逐列的“扫描”,就可以读出任何位置按键的状态(类似于数码管的扫描,只不过一个是输出,一个是输入,后面会谈到)

在这里插入图片描述
由图可知,P10至P13控制列,P14至P17控制行【可看作四排独立按键理解】

扫描的概念

对比

  • 数码管扫描(输出扫描)
    原理:显示第1位->显示第2位->显示第3位->…,快速循环,利用人眼视觉残留,实现所有数码管同时显示的效果。
  • 矩阵键盘扫描(输入扫描)
    原理:读取第1行(列)->读取第2行(列)->读取第3行(列)->…,快速循环这个过程,因为循环速度极快,最终实现所有按键同时检测的效果

以上两种扫描方式的共同特点:节省I/O口的数量

前提:因为矩阵键盘与步进电机(蜂鸣器)引脚冲突,为了避免蜂鸣器响,所以采取逐列扫描
难点:P1P2P3都是弱上拉工作模式,在上面的原理图中如果P1输出低电平,按键另一端连接VCC的话,检测时电流太大。因此另一端连接GND,检测P1口输入是否是低电平来判断按键是否被按下(个人理解)

根据上面的知识内容荣,就可以写出下面的矩阵键盘检测的
.h文件:

#ifndef __MATRIXKEY_H__
#define __MATRIXKEY_H__

unsigned char MatrixKey();

#endif

.c文件:

#include <REGX52.H>
#include "Delay.h"//定义中运用了Delay函数,因此需要加上头文件

/**
	*@brief 检测矩阵键盘按键键码
	*@param 无
	*@retval KeyNumber 键码
*/
unsigned char MatrixKey(){
	unsigned char KeyNumber = 0;
	P1 = 0xFF;//全置1
	P1_3 = 0;//第一列
	if(P1_7 == 0){Delay(20); while(P1_7 == 0); Delay(20); KeyNumber = 1;}
	if(P1_6 == 0){Delay(20); while(P1_6 == 0); Delay(20); KeyNumber = 5;}
	if(P1_5 == 0){Delay(20); while(P1_5 == 0); Delay(20); KeyNumber = 9;}
	if(P1_4 == 0){Delay(20); while(P1_4 == 0); Delay(20); KeyNumber = 13;}
	
	P1 = 0xFF;
	P1_2 = 0;//第二列
	if(P1_7 == 0){Delay(20); while(P1_7 == 0); Delay(20); KeyNumber = 2;}
	if(P1_6 == 0){Delay(20); while(P1_6 == 0); Delay(20); KeyNumber = 6;}
	if(P1_5 == 0){Delay(20); while(P1_5 == 0); Delay(20); KeyNumber = 10;}
	if(P1_4 == 0){Delay(20); while(P1_4 == 0); Delay(20); KeyNumber = 14;}
	
	P1 = 0xFF;
	P1_1 = 0;//第三列
	if(P1_7 == 0){Delay(20); while(P1_7 == 0); Delay(20); KeyNumber = 3;}
	if(P1_6 == 0){Delay(20); while(P1_6 == 0); Delay(20); KeyNumber = 7;}
	if(P1_5 == 0){Delay(20); while(P1_5 == 0); Delay(20); KeyNumber = 11;}
	if(P1_4 == 0){Delay(20); while(P1_4 == 0); Delay(20); KeyNumber = 15;}

	P1 = 0xFF;
	P1_0 = 0;//第四列
	if(P1_7 == 0){Delay(20); while(P1_7 == 0); Delay(20); KeyNumber = 4;}
	if(P1_6 == 0){Delay(20); while(P1_6 == 0); Delay(20); KeyNumber = 8;}
	if(P1_5 == 0){Delay(20); while(P1_5 == 0); Delay(20); KeyNumber = 12;}
	if(P1_4 == 0){Delay(20); while(P1_4 == 0); Delay(20); KeyNumber = 16;}

	return KeyNumber;//返回键码
}

【上面的写法简单且容易理解】

用LCD液晶屏显示矩阵键盘键码

#include <REGX52.H>
#include "MatrixKey.h"
#include "LCD1602.h"
#include "Delay.h"

void main(){
	LCD_Init();
	LCD_ShowString(1,1,"Hello");//第一行显示Hello
	while(1){
		unsigned char key = MatrixKey();//检测键码
		if(key){		//用if语句,防止key立马清零后显示0
			LCD_ShowNum(2,1,key,2);
		}
	}
}

并且,可以由此写出
矩阵键盘密码锁

#include <REGX52.H>
#include "MatrixKey.h"
#include "LCD1602.h"
#include "Delay.h"

unsigned char i = 0;//i表示位数
unsigned char a = 0;//a表示正确与否
unsigned char KeyNumber;
unsigned char password[4] = {1,3,4,1};//密码

void main(){
	LCD_Init();
	LCD_ShowString(1,1,"Password:");
	LCD_ShowString(2,1,"    ");
	
	while(1){
		KeyNumber = MatrixKey();//检测键码
		
		if(i < 4){
			if(KeyNumber>0 && KeyNumber<=10){	//当键码为0~10时,因为只显示一位数字,因此10即是0
				if(KeyNumber == password[i]&&a<4){	//对应密码正确的话,a增加1
					a++;
				}
				LCD_ShowNum(2,i+1,KeyNumber,1);
				i++;		//输入数字后,位数加1后移
			}
		}
		
		/*if(KeyNumber == 11 && i >= 0){
			i--;
			LCD_ShowString(2,i+1," ");
		}*/
		
		if(KeyNumber == 12){//键码为12,表示确认按键
			if(i == 4){//输入了4位
				if(a == 4){
					LCD_ShowString(1,11,"Right!");//当四位数字都正确时,输出Right
					Delay(1000);
					break;
				}
				else LCD_ShowString(1,11,"Error!");//否则,输出Error
				Delay(600);
				LCD_ShowString(1,11,"      ");
				LCD_ShowString(2,1,"    ");
			}
			else {			//数字不是四位时
				LCD_ShowString(1,11,"False!");		//输出错误False
				Delay(600);
				LCD_ShowString(1,11,"      ");		//清屏
				LCD_ShowString(2,1,"                ");
			}
			i = 0;			//数据重置
			a = 0;
		}
	}
}

定时器

介绍

  • 51单片机的定时器属于单片机的内部资源,其电路的连接和运转均在单片机内部完成
  • 作用:
    (1)用于计时系统,可实现软件计时,或者使程序每隔以固定时间完成一项操作
    (2)替代长时间的Delay,提高CPU的运行效率和处理速度

STC89C52定时器资源

  • 定时器个数:3个(T0,T1,T2),T0和T1与传统的51单片机兼容,T2是此型号单片机增加的资源。
  • 注意:定时器的资源和单片机的型号是关联在一起的,不同的型号可能会有不同的定时器个数和操作方式,但一般来说,T0和T1的操作方式是所有51单片机共有的。

定时器框图

  • 定时器在单片机内部就像一个小闹钟,根据时钟的输出信号,每隔“一秒”,计数单元加一,当到达所设置的数字时,计数单元就会向中断系统发出中断申请,使程序跳转到中断函数中执行。
  • 在这里插入图片描述

定时器的工作模式

模式0:13位定时器/计数器
​ 模式1:16位定时器/计数器
​ 模式2:8位自动重装模式
​ 模式3:两个8位计数器

工作模式1框图:
在这里插入图片描述
时钟
SYSclk:系统时钟,即晶振周期,本开发板上的晶振为11.0592MHz;
T0pin是外部引脚;
12T代表12分频,即12个周期一次输出;

C/T:如果是高电平1,那么就是计数器的功能,如果是低电平0,那就是计数器的功能。

计数单元
TL0和TH0为一个16位的计数器,有两个字节,高字节为TH,低字节为TL;
当TL0和TH0最大时,就会向中断系统申请中断。

中断系统
中断系统是为使CPU具有对外界紧急事件的实时处理能力而设置的。
如图所示过程:
在这里插入图片描述
当申请中断时,就会跳到中断程序,处理完后在返回主程序;
加粗样式存在多个中断源时,CPU会根据优先级进行处理,首先响应高优先级的中断请求。

STC89C52中断资源:

  • 中断源个数:8个(外部中断0、定时器0中断、外部中断1、定时器1中断、串口中断、定时器2中断、外部中断2、外部中断3)
  • 中断优先级个数:4个
  • 中断号
    在这里插入图片描述
  • 注意:中断的资源和单片机的型号是关联在一起的,不同的型号可能会有不同的中断资源,例如中断源个数不同、中断优先级个数不同等等

总的结构图:
在这里插入图片描述

寄存器

  • 寄存器是连接软硬件的媒介
  • 在单片机中寄存器就是一段特殊的RAM存储器,一方面,寄存器可以存储和读取数据,另一方面,每一个寄存器背后都连接了一根导线,控制着电路的连接方式
  • 寄存器相当于一个复杂机器的“操作按钮”
定时器/计时器0和1的相关寄存器

在这里插入图片描述
因此,需要设置TCON(Timer Control)和TMOD寄存器来设置定时器

在这里插入图片描述

  • TF1: 定时器/计数器T1溢出标志。T1被允许计数以后,从初值开始加1计数。当最高位产生溢出时由硬件置“1”TF1,向CPU请求中断,一直保持到CPU响应中断时,才由硬件清“0” TF1(TF1也可由程序查询清“0” )。
  • TR1: 定时器T1的运行控制位。该位由软件置位和清零。当GATE(TMOD.7) =0,TR1=1时就TR1:允许T1开始计数,TR1=0时禁止T1计数。当GATE(TMOD.7) =1,TR1=1且INT1输入高电平时,才允许T1计数。
  • TF0: 定时器/计数器TO溢出中断标志。T0被允许计数以后,从初值开始加1计数,当最高位产生溢出时,由硬件置“1”TFO,向CPU请求中断,一直保持CPU响应该中断时,才由硬件清“0”TFO ( TFO也可由程序查询清“0”)。[查询中断便是查询TF0是否为1]
  • TR0:定时器T0的运行控制位。该位由软件置位和清零。当GATE(TMOD.3) =0,TR0=1时就允许T0开始计数,TR0=0时禁止T0计数。当GATE(TMOD.3) =1,TR1=0且INTO输入高电平时,才允许T0计数。
  • IE1: 外部中断1请求源(INT1/P3.3) 标志。IE1=1,外部中断向CPU请求中断,当CPU响应该中断时由硬件清“0”IE1。
  • IT1: 外部中断1触发方式控制位。IT1=0时,外部中断1为低电平触发方式,当INT1 (P3.3)输入低电平时,置位IE1 。采用低电平触发方式时,外部中断源(输入到INT1) 必须保持低电平有效,直到该中断被CPU 响应,同时在该中断服务程序执行完之前,外部中断源必须被清除(P3.3要变高),否则将产生另一次中断。当IT1=1时,则外部中断1(INT1)端口由“1”-“0”下降沿跳变,激活中断请求标志位IE1 ,向主机请求中断处理。
  • IE0: 外部中断0请求源 (INTO/P3.2) 标志。IEO=1外部中断0向CPU请求中断,当CPU响应外部中断时,由硬件清“0”IEO(边沿触发方式)。
  • IT0: 外部中断0触发方式控制位。ITO=0时,外部中断0为低电平触发方式,当ITO(P3.2)IT0:输入低电平时,置位IEO。采用低电平触发方式时,外部中断源(输入到INTO) 必须保持低电平有效,直到该中断被CPU响应,同时在该中断服务程序执行完之前,外部中断源必须被清除(P3.2要变高),否则将产生另一次中断。当ITO=1时,则外部中断0(INTO)端口由“1”-“0”下降沿跳变,激活中断请求标志位IE1 ,向主机请求中断处理。
    【可位寻址:可以对寄存器中的每一位单独赋值】

2.在这里插入图片描述
M1为0,M0为1时,即为16位定时器,我们要用的模式;
C/T:控制定时器还是计数器功能;
GATE:门控端,控制是否由外部控制启动定时器

中断寄存器

在这里插入图片描述
结合下图看
在这里插入图片描述
由图可以看,启动中断
IE中需要ET0置1,EA置1;
IP为设置中断优先级,可以不考虑,我们暂且置0,低优先级。

定时器的初始化:

#include <REGX52.H>
/**
	*@brief 定时器0初始化
	*@param 无
	*@retval 无
*/
void Timer0Init(void)
{
	//由于TMOD不可寻址,因此需要用与&,或|来对单独一位设置
	TMOD &= 0xF0; //把TMOD的第四位清零而高四位不变
	TMOD |= 0x01;//把TMOD的最低位置一
	
	
	TL0 = 0x66;//低四位
	TH0 = 0xFC//高四位
	TF0 = 0;//清零
	TR0 = 1;

	//中断寄存器配置
	ET0 = 1;
	EA = 1;
	PT0 = 0;
}

一秒定时:

void Timer0_Routine() interrupt 1
{
	static unsigned int count = 0;//静态变量
	//重置
	TL0 = 0x66;
	TH0 = 0xFC;
	count++;//每次中断加一
	if(count >= 1000){
	
	}
}

定时器也可由软件直接生成
在这里插入图片描述
由此,我们就可以写出

按键控制LED流水灯方向

#include <REGX52.H>
#include "Timer0.h"
#include "Delay.h"

unsigned char mode = 0;
void main(){
	Timer0Init();
	P2 = 0xFE;
	while(1){//检测按键并改变模式
		if(P3_1 == 0){
			Delay(20);
			while(P3_1 == 0);
			Delay(20);
			mode = 1;
		}
		if(P3_0 == 0){
			Delay(20);
			while(P3_0 == 0);
			Delay(20);
			mode = 0;
		}
	}
}

void Timer0_Routine() interrupt 1
{
	static unsigned int count = 0;
	TL0 = 0x66;
	TH0 = 0xFC;
	count++;
	
	//流水灯代码
	if(count >= 1000 && mode == 0){
		P2 = (P2<<1) + 1;
		if(P2 == 0xFF){
			P2 = 0xFE;
		}
		count = 0;
	}
	if(count >= 1000 && mode == 1){
		P2 = (P2>>1) + 0x80;
		if(P2 == 0xFF){
			P2 = 0x7F;
		}
		count = 0;
	}
}

定时器计时

#include <REGX52.H>
#include "Timer0.h"
#include "LCD1602.h"
#include "Key.h"//独立按键检测模块

unsigned char hour=0,min=0,sec=0;//初始化
void main()
{
	//初始化
	LCD_Init();
	Timer0Init();
	LCD_ShowString(1,1,"Clock:");
	while(1)
	{
		//显示时间
		LCD_ShowNum(2,1,hour,2);
		LCD_ShowNum(2,4,min,2);
		LCD_ShowNum(2,7,sec,2);
		
		switch(Key())//检测按键并选择时间加一
		{
			case 1:
				hour++;
			break;
			case 2:
				min++;
				break;
			case 3:
				sec++;
				break;
			case 4://清零
				hour = 0;
				min = 0;
				sec = 0;
				break;
		}
	}
}

void Timer0_Routine() interrupt 1
{
	static unsigned int count = 0;
	TL0 = 0x66;
	TH0 = 0xFC;
	count++;
	
	//时钟
	if(count >= 1000){
		count = 0;
		sec++;
		if(sec >= 60)
		{
			sec = 0;
			min++;
			if(min >= 60)
			{
				min = 0;
				hour++;
				if(hour >= 24)
				{
					hour = 0;
				}
			}
		}
	}
}

串口通信

介绍

  • 串口是一种应用广泛的通讯接口,成本低、易使用,通信线路简单可用于实现两个设备之间的互相通信;
  • 单片机的串口可以使单片机与单片机,单片机与电脑,单片机与各式各样的模块互相通信,极大地拓展了单片机的应用范围,增强了其硬件实力;
  • 51单片机内部自带UART(Universal Asynchronous Receiver Transmitter,通用异步收发器),可实现单片机的串口通信。

硬件电路

在这里插入图片描述
TXD(Transmit Exchange Data)发送,RXD(Receive External Data)接收,因此需要两根通信线。

电平标准

电平标准是数据1和数据0的表达方式,是传输线缆中人为规定的电压与数据的对应关系,串口常用的电平标准有如下三种:

  • TTL电平:+5V表示1,0V表示0(单片机)
  • RS232电平:-3-15V表示1,+3+15V表示0(多用于电脑,稳定)
  • RS485电平:两线压差+2+6V表示1,-2-6V表示0(差分信号)

【TTL电平和 RS232电平传输距离有限,而RS485电平传输距离可以很远,千米级别】

接口及引脚定义

在这里插入图片描述

常见通信接口比较

在这里插入图片描述
此外还有:CAN、USB等

相关术语
在这里插入图片描述

单片机上的UART

  • STC89C52有1个UART

  • STC89C52的UART有四种工作模式:
    ​ 模式0:同步移位寄存器
    ​ 模式1:8位UART,波特率可变(常用)
    ​ 模式2:9位UART,波特率固定
    ​ 模式3:9位UART,波特率可变
    在这里插入图片描述

串口参数及时序图

  • 波特率:串口通信的速率(发送和接收各数据位的间隔时间)
  • 检验位:用于数据验证(减压数据传输是否正确)
  • 停止位:用于数据帧间隔

在这里插入图片描述

串口模式图

在这里插入图片描述
SBUF:串口数据缓存寄存器,物理上是两个独立的寄存器,但占用相同的地址。写操作时,写入的是发送寄存器(SBUF在等号左边),读操作时,读出的是接受寄存器(SBUF在等号右边)。

过程:
发送时,数据通过总线流入,通过定时器控制发送控制器来控制移位寄存器,将数据寄存到发送寄存器,同时触发中断,然后再通过总线发送出去,而速率通过定时器控制。接收类似。

在这里插入图片描述

串口相关寄存器

在这里插入图片描述
配置SCON和PCON,并打开中断IE就可以了。

在这里插入图片描述
在这里插入图片描述
SCON:
我们需要配置模式一:SM0为0,SM1为1;
不需要多机通信,因此SM2配为0;
REN:控制是否允许单片机接收数据,0为不允许;
TB8,RB8为9位数据,不是模式一的,因此不管,置0;
T1,R1标志位初始化给0;

PCON:
SMOD控制波特率是否加倍,不需要,因此给0;
SMOD0用于9位数据帧错误,模式一为8为数据,不需要,置0;

串口初始化

#include <REGX52.H>

void UART_Init(void)		//4800bps@11.0592MHz
{	
	SCON = 0x40;		//只发
	PCON |= 0x80;	

	//设置定时器模式8位自动重装
	TMOD &= 0x0F;
	TMOD |= 0x20;
	
	//设置定时初值
	TL1 = 0xF4;
	TH1 = 0xF4;	
	
	ET0 = 0;		//禁止定时器中断
	EA = 1;
	TR1 = 1;		//启动定时器1
}

也可通过软件配置串口
在这里插入图片描述

发送一个字节

void UART_SendByte(unsigned char Byte)
{
	SBUF = Byte;
	while(TI==0);//检测是否完成发送
	TI=0;//重置复位
}

数据显示模式

  • HEX模式/十六进制模式/二进制模式:以原始数据的形式显示
  • 文本模式/字符模式:以原始数据编码后的形式显示(ASCII码)

例子:电脑通过串口控制LED

#include <REGX52.H>
#include "Delay.h"
#include "UART.h"

unsigned char x;

void main()
{
	UART_Init();	//初始化
	while(1){
		UART_SendByte(x);
		Delay(1000);		//延时一秒发送数据x
	}
}

void UART_Routine()	interrupt 4		//串口中断为interrupt 4 
{
	if(RI == 1)		//如果是接收
	{
		x = SBUF;	//更新x的值
		P2 = ~x;	//改变LED
		RI = 0;		//复位
	}
	if(TI == 1)		//如果是发送,x每秒加一
	{
		P2 = ~x++;	//改变LED,并x加一
	}
}

LED点阵屏

介绍

LED点阵屏由若干个独立的LED组成,LED以矩阵的形式排列,以灯珠亮灭来显示文字,图片,视频等。LED点阵屏广泛应用于各种公共场合。

  • LED点阵屏分类
    按颜色:单色、双色、全彩
    按像素:8x8、16*16(大规模的LED点阵屏通常有很多个小点阵屏拼接而成)

显示原理

  • LED显示屏的连接方式类似于数码管,只不过是数码管把每一列的像素以“8”字型排列而已;
  • LED点阵屏与数码管一样,有共阴和共阳两种接法,不同的接法对应的电路结构不用;
  • LED点阵屏需要进行逐行或逐列扫描,才能使LED同时显示
    在这里插入图片描述
    点阵屏上LED的引脚有可能是按最近引脚引出,因此可能是乱序排列

原理图

在这里插入图片描述
在这里插入图片描述
OE:输出使能,低电平有效【交线帽操作】
SRCLR:串行清零端

74HC595介绍

74HC595是串行输入并行输出的移位寄存器,可用3根线输入串行数据,8根线输出并行数据,多片级联后,可输出16位,24位,32位等,常用于I/O口扩展。
在这里插入图片描述

  • SER输入串行数据
  • SERCLK:上升沿移位,即在上升沿(从低电平到高电平)时,会将SER的数据移进寄存器;
  • RCLK:上升沿锁存,即在上升沿(从低电平到高电平)时,会将寄存器里的数据锁存,并将这并行数据一同输出。

开发板引脚对应关系

在这里插入图片描述
列由P0控制,行由74HC595控制。

补充知识点:C51的sfr,sbit

  • sfr(special function register):特殊功能寄存器声明,例:sfr P0 = 0x80;
    声明P0口寄存器,物理地址为0x80
  • sbit(special bit):特殊位声明
    例:sbit P0_1 = 0x81; 或 sbit P0_1 = P0^1;
    声明P0寄存器的第1位
  • 可位寻址/不可位寻址:在单片机系统中,操作任意寄存器或者某一位的数据时,必须给出其物理地址,又因为一个寄存器里有8位,所以位的数量是寄存器数量的8倍,单片机无法对所有位进行编码,故每8个寄存器中,只有一个是可以位寻址的。对不可位寻址的寄存器,若要只操作其中一位而不影响其他位时,可用“&=”、“|=”、“^=”的方法进行位操作

控制74HC595(代码)

详情请看注释

#include <REGX52.H>

sbit RCK = P3^5;	//RCLK上升沿锁存
sbit SCK = P3^6;	//SERCLK上升沿移位
sbit SER = P3^4;	//串行数据

/**
	*@brief 74HC595写入一个字节
	*@param 要写入的字节
	*@retval 无
*/
void _74HC595_WriteByte(unsigned char Byte)
{
	unsigned char i;
	for(i = 0; i < 8; i++)		//循环8次,移入一个字节
	{
		SER = Byte&(0x80>>i); 	//SER先送高位数字,SER赋值的数非0即1
		SCK = 1;		//上升沿————移位
		SCK = 0;		//下降沿————清零重置
	}
	RCK = 1;		//上升沿————锁存
	RCK = 0;		//下降沿————清零重置
}

控制点阵屏显示(代码)

由上面引脚图可知,P0控制列,74HC595控制行。

因此:

#define	column	P0

/**
	*@brief 矩阵屏初始化
	*@param 无
	*@retval 无
*/
void MatrixLED_Init()
{
	SCK = 0;
	RCK = 0;
}

/**
	*@brief 矩阵屏显示一列数据
	*@param x列,y行
	*@retval 无
*/
void MatrixLED_Show(unsigned char x, y)	//x轴控制列,y轴控制行
{
	_74HC595_WriteByte(y);		//控制行,高位在上,低位在下,高电平1亮
	column = ~(0x80>>x);		//控制列,高位在左、低位在右,低电平0亮
	Delay(1);					//延时
	column = 0xFF;				//清零,消影
}

然后,我们就可以用上面的MatrixLED_Show函数显示图形了。

点阵屏显示动画

动画即是不断在动的画,因此将画连续显示,就成了动画。我们可以利用上面的点阵屏显示函数,写一个循环语句以达成该目的。

#include <REGX52.H>
#include "MatrixLED.h"

unsigned char code Hellocartoon[] = {
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0xFF,0x08,0x08,0x08,0x08,0xFF,0x00,0x00,0x0E,0x15,0x15,0x15,0x0D,0x00,0x00,0xFF,
0x01,0x02,0x00,0xFF,0x01,0x02,0x00,0x00,0x0E,0x11,0x11,0x0E,0x00,0x00,0xFD,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
};		//Hello动画数组存储数据,开头与末尾个八个0x00,用来循环时可无缝衔接

void main()
{
	unsigned char i, offset = 0, cnt = 0;
	MatrixLED_Init();		//初始化
	while(1)
	{
		for(i=0;i<8;i++)
		{
			MatrixLED_Show(i,Hellocartoon[i+offset]);		//显示一个画面
		}
		cnt++;			//cnt控制一张画面显示多久时间
		if(cnt == 10)
		{
			cnt = 0;
			offset++;		//cnt达到10时,画面切换成下一帧,因为只是要画面左移,所以offset加一即可
			if(offset > 40)
			{
				offset = 0;		//当offset加到40(最大数量48-8)时,重置
			}
		}
	}
}

动画数组数据因为运行时是不用改变的,放在RAM占用空间,因此可以加一个code(如代码所示),存到闪存flash

取字模软件获取动画参数

示例:
在这里插入图片描述

DS1302

介绍

  • DS1302是由美国DALLAS公司推出的具有涓细电流充电能力的低耗实时时钟芯片。他可以对年、月、日、周、时、分、秒进行计时,且具有闰年补偿等多种功能;
  • RTC(Real Time Clock):实时时钟,是一种集成电路,通常被称为时钟芯片。

定时器时钟由于精度不高,占用CPU,因此通常不选择,而选择实时时钟芯片。

引脚定义和应用电路

在这里插入图片描述

在这里插入图片描述
注意:VCC2才是主电源

32.768kHz晶振精确,可以给出1Hz的稳定脉冲,称作石英钟

这里的CE,IO,SCLK与74HC595里的RCLK,SER,SERCLK是一样的,都是P3_5,P3_4,P3_6。因此实时时钟会与点阵屏冲突。

开发板上的原理图
在这里插入图片描述
可以看到,并没有备用电源,因此断电后始终无法继续走。

内部结构框图

在这里插入图片描述
CE使能,

寄存器定义

在这里插入图片描述
Seconds秒,Minutes分,Hour时,Date日期,Month月份,Day星期,Year年份

第一行的CH置1的话,时钟停止
倒数第二行中有一个WP,控制写保护

Address/Command Byte:命令字
由命令字表可以看到
第6位表示控制RAM还是时钟
第0位表示是否开启写保护(上头有横杠表示低电平有效)

命令字可以在RTC第一二列查到;

时序定义

在这里插入图片描述

  • CE使能
  • R/W为1时读出read,为0时写入write
  • 在上升沿写入,在下降沿读出
  • 读出数据是由时钟控制的
  • 不管是读出还是写入,首先得明确位置,即应先输入命令字

DS1302读写代码

在这里插入图片描述
当电压为5V时,上升下降间隔时间最小为50ns,ns级别,单片机因此不用考虑

#include <REGX52.H>

sbit DS1302_SCLK = P3^6;
sbit DS1302_IO = P3^4;
sbit DS1302_CE = P3^5;

//写入
void DS1302_WriteByte(unsigned char Command, Date)
{
	unsigned char i;
	DS1302_CE = 1;	//开启使能
	for(i = 0; i < 8; i++)
	{
		DS1302_IO = Command&(0x01<<i);		//命令字,0x01不断左移,逐个数字写入
		DS1302_SCLK = 1;		//上升沿移位
		DS1302_SCLK = 0;		//清零
	}
	for(i = 0; i < 8; i++)
	{
		DS1302_IO = Date&(0x01<<i);			//要写入的数据也逐个写入
		DS1302_SCLK = 1;
		DS1302_SCLK = 0;
	}
	DS1302_CE = 0;			//关闭使能
}

//读出
unsigned char DS1302_ReadByte(unsigned char Command)
{
	unsigned char i, Date = 0x00;
	Command |= 0x01;		//读出的命令字最低位为1
	DS1302_CE = 1;			//开启使能
	for(i = 0; i < 8; i++)
	{
		DS1302_IO = Command&(0x01<<i);			//命令字
		DS1302_SCLK = 0;
		DS1302_SCLK = 1;
	}
	for(i = 0; i < 8; i++)
	{
		DS1302_SCLK = 1;
		DS1302_SCLK = 0;
		if(DS1302_IO){Date |= (0x01<<i);}		如果IO不为0,则存入1,以此办法读出数据
	}
	DS1302_IO = 0;
	DS1302_CE = 0;		//关闭使能
	return Date;
}

补充知识:BCD码

  • BCD码(Binary Coded Decimal),用4位二进制数来表示1位十进制数
  • 例:0001 0011表示13,1000 0101表示85,0001 1010不合法
  • 在十六进制中的体现:0x13表示13,0x85表示85,0x1A不合法
  • BCD码转十进制:DEC=BCD/16*10+BCD%16;(2位BCD)
  • 十进制转BCD码:BCD=DEC/10*16+DEC%10;(2位BCD)

时钟数据是以BCD码存储的
所以在显示时钟时需要将数据转化为十进制

显示时钟代码

因为上面的两个函数读出和写入时需要调用太多,太麻烦
因此,我们写一个DS1302_ReadTime函数直接读出时钟数据,一个DS1302_SetTime函数直接将时间写入时钟

#include <REGX52.H>

sbit DS1302_SCLK = P3^6;
sbit DS1302_IO = P3^4;
sbit DS1302_CE = P3^5;

//声明时间,方便后续书写
#define		year		0x8C
#define		day			0x8A
#define		month		0x88
#define		date		0x86
#define		hour		0x84
#define		minute		0x82
#define		second		0x80
#define		WP			0x8E

//初始时间
unsigned char Time[] = {23,12,13,12,0,0,3};


/**
	*@brief 时钟初始化
	*@param 无
	*@retval 无
*/
void DS1302_Init(void)
{
	DS1302_CE = 0;
	DS1302_SCLK = 0;
}


/**
	*@brief 写入对应时间
	*@param Command命令字——对应时间地址(year,month...)
	*@param Date数据——写入相应的时间数据
	*@retval 无
*/
void DS1302_WriteByte(unsigned char Command, Date)
{
	unsigned char i;
	DS1302_CE = 1;
	for(i = 0; i < 8; i++)
	{
		DS1302_IO = Command&(0x01<<i);
		DS1302_SCLK = 1;
		DS1302_SCLK = 0;
	}
	for(i = 0; i < 8; i++)
	{
		DS1302_IO = Date&(0x01<<i);
		DS1302_SCLK = 1;
		DS1302_SCLK = 0;
	}
	DS1302_CE = 0;
}


/**
	*@brief 读出对应时间
	*@param Command命令字——对应时间地址(year,month...)
	*@retval Date返回时间数据
*/
unsigned char DS1302_ReadByte(unsigned char Command)
{
	unsigned char i, Date = 0x00;
	Command |= 0x01;
	DS1302_CE = 1;
	for(i = 0; i < 8; i++)
	{
		DS1302_IO = Command&(0x01<<i);
		DS1302_SCLK = 0;
		DS1302_SCLK = 1;
	}
	for(i = 0; i < 8; i++)
	{
		DS1302_SCLK = 1;
		DS1302_SCLK = 0;
		if(DS1302_IO){Date |= (0x01<<i);}
	}
	DS1302_IO = 0;
	DS1302_CE = 0;
	return Date;
}


/**
	*@brief 将数组时间写入芯片
	*@param 无
	*@retval 无
*/
void DS1302_SetTime(void)
{
	DS1302_WriteByte(WP,0x00);	//关闭写保护
	
	//年、月、日、时、分、秒、星期,依次写入数组里的时间
	DS1302_WriteByte(year,Time[0]/10*16+Time[0]%10);
	DS1302_WriteByte(month,Time[1]/10*16+Time[1]%10);
	DS1302_WriteByte(date,Time[2]/10*16+Time[2]%10);
	DS1302_WriteByte(hour,Time[3]/10*16+Time[3]%10);
	DS1302_WriteByte(minute,Time[4]/10*16+Time[4]%10);
	DS1302_WriteByte(second,Time[5]/10*16+Time[5]%10);
	DS1302_WriteByte(day,Time[6]/10*16+Time[6]%10);
	DS1302_WriteByte(WP,0x80);	//打开写保护
}


/**
	*@brief 将当前时间赋值给初始时间数组
	*@param 无
	*@retval 无
*/
void DS1302_ReadTime(void)
{
	unsigned char temp;	//暂存数据
	依次将时钟时间赋给数组的时间
	temp = DS1302_ReadByte(year);
	Time[0] = temp/16*10+temp%16;
	temp = DS1302_ReadByte(month);
	Time[1] = temp/16*10+temp%16;
	temp = DS1302_ReadByte(date);
	Time[2] = temp/16*10+temp%16;
	temp = DS1302_ReadByte(hour);
	Time[3] = temp/16*10+temp%16;
	temp = DS1302_ReadByte(minute);
	Time[4] = temp/16*10+temp%16;
	temp = DS1302_ReadByte(second);
	Time[5] = temp/16*10+temp%16;
	temp = DS1302_ReadByte(day);
	Time[6] = temp/16*10+temp%16;
}

然后主函数

#include <REGX52.H>
#include "LCD1602.h"
#include "DS1302.h"
#include "Delay.h"


void main()
{
	LCD_Init();
	DS1302_Init();
	LCD_ShowString(1,1,"  -  -  ");
	LCD_ShowString(2,1,"  :  :  ");
	DS1302_SetTime();
	
	while(1)
	{
		DS1302_ReadTime();	//不断将时钟赋值给数组时间
		//显示时间
		LCD_ShowNum(1,1,Time[0],2);
		LCD_ShowNum(1,4,Time[1],2);
		LCD_ShowNum(1,7,Time[2],2);
		LCD_ShowNum(2,1,Time[3],2);
		LCD_ShowNum(2,4,Time[4],2);
		LCD_ShowNum(2,7,Time[5],2);
		LCD_ShowNum(2,13,Time[6],1);
	}
}

进阶,可调时钟

#include <REGX52.H>
#include "LCD1602.h"
#include "DS1302.h"
#include "Delay.h"
#include "Key.h"
#include "Timer0.h"

void TimeShow();					//时间展示模式
void TimeSet();						//时间设置模式
//TimeSet里按键三四判断该月最大天数。输入月份、年份,输出天数
unsigned char JudgeDate(unsigned char month, year);

unsigned char mode,keynum,number,x;

void main()
{
	//初始化
	LCD_Init();
	DS1302_Init();
	Timer0Init();
	
	//DS1302_SetTime();
	LCD_ShowString(1,1,"  -  -  ");
	LCD_ShowString(1,12,"Mode");
	LCD_ShowString(2,1,"  :  :  ");
	
	while(1)
	{
		keynum = Key();				//检测按键
		LCD_ShowNum(1,16,mode,1);	//显示模式
		
		//按键一转换模式
		if(keynum == 1)
		{
			if(mode == 0){mode = 1;}
			else if(mode == 1)
			{
				mode = 0;
				DS1302_SetTime();
			}
		}
		
		//模式启动
		if(mode == 0)
		{
			LCD_ShowString(2,10,"  ");		//清除序号
			TimeShow();
		}
		if(mode == 1){TimeSet();}
	}
}

//时间显示函数
void TimeShow()
{
	DS1302_ReadTime();
	LCD_ShowNum(1,1,Time[0],2);
	LCD_ShowNum(1,4,Time[1],2);
	LCD_ShowNum(1,7,Time[2],2);
	LCD_ShowNum(2,1,Time[3],2);
	LCD_ShowNum(2,4,Time[4],2);
	LCD_ShowNum(2,7,Time[5],2);
}

//调时间函数
void TimeSet()
{
	//按键二切换年月日时分秒
	if(keynum == 2)
	{
		//序号增加,越界清零
		number++;
		number%=6;
	}
	LCD_ShowNum(2,10,number+1,2);		//显示年月日等序号顺序
	
	//按键三增加
	if(keynum == 3)
	{
		Time[number]++;
		switch(number)
		{
			case 1:				//月
				if(Time[1]>12){Time[1]=1;}
				break;
			case 2:				//日
				if(Time[2]>JudgeDate(Time[1],Time[0]))
				{Time[2]=1;}
				break;
			case 3:				//时
				if(Time[3]>23){Time[3]=0;}
				break;
			case 4:				//分
				if(Time[4]>59){Time[4]=0;}
				break;
			case 5:				//秒
				if(Time[5]>59){Time[5]=0;}
				break;
		}
	}
	
	//按键四减少
	if(keynum == 4)
	{
		Time[number]--;
		switch(number)
		{
			case 1:				//月
				if(Time[1]== 0){Time[1]=12;}
				break;
			case 2:				//日
				if(Time[2]== 0)
				{Time[2]=JudgeDate(Time[1],Time[0]);}
				break;
			case 3:				//时
				if(Time[3]>23){Time[3]=23;}
				break;
			case 4:				//分
				if(Time[4]>59){Time[4]=59;}
				break;
			case 5:				//秒
				if(Time[5]>59){Time[5]=59;}
				break;
		}
	}
	//防止改变年月时日期超过最大天数
	if(Time[2]>JudgeDate(Time[1],Time[0]))
		{Time[2]=JudgeDate(Time[1],Time[0]);}
	//展示更改时间
	if(x == 0)
	{
	LCD_ShowNum(1,1,Time[0],2);
	LCD_ShowNum(1,4,Time[1],2);
	LCD_ShowNum(1,7,Time[2],2);
	LCD_ShowNum(2,1,Time[3],2);
	LCD_ShowNum(2,4,Time[4],2);
	LCD_ShowNum(2,7,Time[5],2);
	}
	else	//否则,对应位置清空,以达到闪烁效果
	{
		if(number < 3)
		{
			LCD_ShowString(1,number*3+1,"  ");
		}
		else LCD_ShowString(2,number*3-8,"  ");
	}
	
}

//判断最大日期
unsigned char JudgeDate(unsigned char month, year)
{
	unsigned char DateMax;
	if(month==1||month==3||month==5||month==7
		||month==8||month==10||month==12)
		{DateMax = 31;}		//31天月份
	if(month==4||month==6||month==9||month==11)
		{DateMax = 30;}		//30天月份
	if(month==2)		//先判断2月,在判断闰年
	{
		if(year%4==0){DateMax=29;}
		else DateMax=28;
	}
	return DateMax;	//返回最大日期
}

//中断,计时,用来闪烁的周期——1s
void Timer0_Routine() interrupt 1
{
	static unsigned int count = 0;
	TL0 = 0x66;
	TH0 = 0xFC;
	count++;
	if(count>=300)
	{
		count = 0;
		x = ~x;
	}
}

任务二代码(main函数):

#include <REGX52.H>
#include "LCD1602.h"
#include "DS1302.h"
#include "Delay.h"
#include "Key.h"
#include "Timer0.h"
#include "Nixie.h"
#include "MatrixKey.h"
#include "MatrixLED.h"

void mode0();
void mode1();
void mode2();
void mode3();
void clarm();
void showtime();//时分秒分开显示,后续模式二编写闪烁比较方便
void showhour();
void showminute();
void showsecond();

unsigned char keynum,key_last, mode, lastmode, x, y;
//按键keynum,模式mode,“-”闪烁判断x,时间数字闪烁判断y

unsigned char code clarm_p[] = {
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0xFB,0xFB,0x00,0x00,0x00,
0x00,0x00,0x00,0xFB,0xFB,0x00,0x00,0x00,//?????
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0xFB,0xFB,0x00,0x00,0x00,//?????
0x00,0x00,0x00,0xFB,0xFB,0x00,0x00,0x00,
0x00,0x00,0x00,0xFB,0xFB,0x00,0x00,0x00,
0x00,0x00,0x00,0x80,0x80,0x00,0x00,0x00,//????
0x00,0x00,0x00,0x80,0x80,0x40,0x20,0x00,
0x00,0x00,0x00,0x80,0x80,0x40,0x20,0x18,
0x00,0x00,0x00,0x80,0x80,0x42,0x24,0x18,
0x00,0x00,0x00,0x81,0x81,0x42,0x24,0x18,
0x00,0x04,0x02,0x81,0x81,0x42,0x24,0x18,
0x18,0x04,0x02,0x81,0x81,0x42,0x24,0x18,
0x18,0x24,0x42,0x81,0x81,0x42,0x24,0x18,
0x18,0x24,0x42,0x81,0x81,0x42,0x24,0x18,
0x18,0x24,0x42,0x81,0x81,0x42,0x24,0x18,//????
};

void main()
{
	DS1302_Init();//初始化
	Timer0Init();
	MatrixLED_Init();
	
	while(1)
	{
		keynum = MatrixKey();//读取按键
		
		if(keynum == 4 && key_last == 0)mode = 1;		//切换模式
		if(keynum == 8 && key_last == 0)
		{
			DS1302_SetTime();//将数组时间写入芯片,更新模式一所调时间
			mode = 2;
		}
		if(keynum == 9 && key_last == 0)mode = 3;
		
		switch(mode)//启动模式
		{
		case 0:
			mode0();//初始模式
			break;
		case 1:
			mode1();//调节模式
			break;
		case 2:
			mode2();//计时模式
			break;
		case 3:
			mode3();//设置闹钟模式
			DS1302_ReadTime();//不停更新数组时间,使得在调闹钟的时候,时钟依旧在走
			MatrixLED_Init();
			break;
		case 4:
			clarm();//闹钟
			DS1302_ReadTime();//不停更新数组时间,使得在调闹钟的时候,时钟依旧在走
			break;
		}
		key_last = keynum;
	}
}

void mode0()
{
	if(x == 1)
	{
		nixie(3,11);
		nixie(6,11);
	}
	nixie(1,1);nixie(2,2);nixie(4,0);nixie(5,0);nixie(7,0);nixie(8,0);
}

unsigned char temp;
void mode1()
{
	//加
	if(keynum==1 && key_last == 0)//加小时,越界清零
	{
		Time[3]++;
		if(Time[3]>23)
		{
			Time[3] = 0;
		}
	}
	if(keynum==2 && key_last == 0)//加分钟,越界清零
	{
		Time[4]++;
		if(Time[4]>59)
		{
			Time[4] = 0;
		}
	}
	if(keynum==3 && key_last == 0)//加秒,越界清零
	{
		Time[5]++;
		if(Time[5]>59)
		{
			Time[5] = 0;
		}
	}
	//减
	if(keynum==5 && key_last == 0)//减小时,越界清零
	{
		Time[3]--;
		if(Time[3]>23)
		{
			Time[3] = 23;
		}
	}
	if(keynum==6 && key_last == 0)//减分钟,越界清零
	{
		Time[4]--;
		if(Time[4]>59)
		{
			Time[4] = 59;
		}
	}
	if(keynum==7 && key_last == 0)//减秒,越界清零
	{
		Time[5]--;
		if(Time[5]>59)
		{
			Time[5] = 59;
		}
	}
	
	//显示横杠-
	nixie(3,11);nixie(6,11);
	//显示数字
	if(keynum != 0)//暂存按键数据
	{
		temp = keynum;
	}
	//闪烁
	if(y == 1)
	{
		showtime();
	}
	else
	{
		if(temp == 1||temp == 5)
		{
			showminute();
			showsecond();
		}
		else if(temp == 2||temp == 6)
		{
			showhour();
			showsecond();
		}
		else if(temp == 3||temp == 7)
		{
			showhour();
			showminute();
		}
		else showtime();
	}
}

void mode2()
{
	DS1302_ReadTime();//更新数组时间
	MatrixLED_Init();//因为加下来P0要置0,所以得先将点阵屏初始化
	if(x == 1)		//周期显示横杠-
	{
		nixie(3,11);
		nixie(6,11);
	}
	showtime();
}

unsigned char clarm_hour,clarm_minute,clarm_second;//闹钟时分秒
void mode3()
{
	//显示闹钟时间
	nixie(3,11);nixie(6,11);//显示横杠-
	nixie(1,clarm_hour/10);nixie(2,clarm_hour%10);//时
	nixie(4,clarm_minute/10);nixie(5,clarm_minute%10);//分
	nixie(7,clarm_second/10);nixie(8,clarm_second%10);//秒
	
	//加
	if(keynum==1 && key_last == 0)//加小时,越界清零
	{
		clarm_hour++;
		if(clarm_hour>23)
		{
			clarm_hour = 0;
		}
	}
	if(keynum==2 && key_last == 0)//加分钟,越界清零
	{
		clarm_minute++;
		if(clarm_minute>59)
		{
			clarm_minute = 0;
		}
	}
	if(keynum==3 && key_last == 0)//加秒,越界清零
	{
		clarm_second++;
		if(clarm_second>59)
		{
			clarm_second = 0;
		}
	}
	//减
	if(keynum==5 && key_last == 0)//减小时,越界清零
	{
		clarm_hour--;
		if(clarm_hour>23)
		{
			clarm_hour = 23;
		}
	}
	if(keynum==6 && key_last == 0)//减分钟,越界清零
	{
		clarm_minute--;
		if(clarm_minute>59)
		{
			clarm_minute = 59;
		}
	}
	if(keynum==7 && key_last == 0)//减秒,越界清零
	{
		clarm_second--;
		if(clarm_second>59)
		{
			clarm_second = 59;
		}
	}
}

unsigned char p_num, i, count3;//p_num图像帧数
void clarm()		//闹钟
{
	for(i = 0; i < 8; i++)
	{
		MatrixLED_Show(i,clarm_p[i+p_num]);//显示一帧
	}
	if(count3 > 200)
	{
		count3 = 0;
		p_num += 8;		//显示下一帧
		if(p_num > 136)p_num = 48;		//超出时重置
	}
}
	
void showtime()		//显示时间
{
	showhour();
	showminute();
	showsecond();
}

void showhour()
{
	nixie(1,Time[3]/10);nixie(2,Time[3]%10);//时
}

void showminute()
{
	nixie(4,Time[4]/10);nixie(5,Time[4]%10);//分
}

void showsecond()
{
	nixie(7,Time[5]/10);nixie(8,Time[5]%10);//秒
}

void Timer0_Routine() interrupt 1
{
	static unsigned int count1 = 0, count2 = 0;
	TL0 = 0x66;
	TH0 = 0xFC;
	count1++;
	count2++;
	if(count1 >= 500)
	{
		count1 = 0;
		x = !x;
	}
	if(count2 >= 250)
	{
		count2 = 0;
		y = !y;
	}
	if(Time[3]==clarm_hour&&
		Time[4]==clarm_minute&&
		 Time[5]==clarm_second)//判断是否达到预定时间,达到就切换成闹钟
	{
		mode = 4;
	}
	count3++;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值