51单片机学习历程

本文介绍了51单片机在实际应用中的几个关键功能,包括定时器的配置和中断处理,串口通信与DS1302时钟芯片的交互,以及AT24C02存储器的I2C通信。还涉及了LED显示、矩阵键盘和矩阵LED的控制方法,以及数码管的显示和消抖技术。
摘要由CSDN通过智能技术生成

发光二极管(LED)

标注:

  1. 大旗子表示负极,小旗子表示正极
  2. 长的一端为正极,短的为负极
  3. 正极内电极较小,负极内电极较大

电阻上的数字如何看?

​ 102 是分两部分看 10 2 --> 1K 1000

	473						  47    3     --> 47K        47000

​ 1001 100 1 --> 1K 1000

单片机工作原理:

​ CPU --> 寄存器 --> 驱动器 --> IO口

二进制转十六进制:

	1. 四位一段
	2. 8421码

点亮第1个LED

  1. 需要控制P2口
  2. 给P2^0 低电平 其他 1 2 3 4 5 6 7 为高电平
#include <REGX52.H>

void main()
{
	//P2 = 0xFE;// 1111 1110
	P2 = 0x55;// 表示 0101 0101 LED间隔点亮
}

LED闪烁

  1. while死循环,使得程序不断执行。
  2. 需要延时函数,使得P2^0口的高低电平切换有时间差
#include <reg52.h>
#include <intrins.h> // 声明_nop_函数
void Delay500ms()		//@12.000MHz
{
	unsigned char i, j, k;
	_nop_(); // 执行空语句
	i = 4;
	j = 205;
	k = 187;
	do
	{
		do
		{
			while (--k);
		} while (--j);
	} while (--i);
}
void main()
{
	while(1)
	{
		P2 = 0xFE; // 1111 1110
		Delay500ms();
		P2 = 0xFF;    //1111 1111
		Delay500ms();
	}
}

LED流水灯

  1. 流水的需要依次点亮每个led,也就是一个挨一个的高低电平的切换
  2. 同时需要延时函数
#include <regx52.h>
#include <intrins.h>
void Delay500ms()		//@12.000MHz
{
	unsigned char i, j, k;
	_nop_();
	i = 4;
	j = 205;
	k = 187;
	do
	{
		do
		{
			while (--k);
		} while (--j);
	} while (--i);
}
void main()
{
	P2 = 0xFE;// 1111 1110
	Delay500ms();
	P2 = 0xFD;// 1111 1101
	Delay500ms();
	P2 = 0xFB;// 1111 1011
	Delay500ms();
	P2 = 0xF7;// 1111 0111
	Delay500ms();
	P2 = 0xEF;// 1110 1111
	Delay500ms();
	P2 = 0xDF;// 1101 1111
	Delay500ms();
	P2 = 0xBF;// 1011 1111
	Delay500ms();
	P2 = 0x7F;// 0111 1111
	Delay500ms();
}

流水灯plus

数据类型:

  1. 存数据的小盒子

    unsigned int 16位(单片机) 0~65535

    int 16位(单片机) -32768~32767

#include <regx52.h>

void Delay1ms(unsigned int xms)		//@12.000MHz
{
	unsigned char i, j; 
	while(xms)
	{
        i = 2;  
        j = 239;
        do
		{
			while (--j);
		}while (--i);
		xms--;
	}

}

void main()
{
	P2 = 0xFE;	// 1111 1110
	Delay1ms(100);
	P2 = 0xFD;	// 1111 1101
	Delay1ms(100);
	P2 = 0xFB;	// 1111 1011
	Delay1ms(100);
	P2 = 0xF7;	// 1111 0111
	Delay1ms(100);
	P2 = 0xEF;	// 1110 1111
	Delay1ms(100);
	P2 = 0xDF;	// 1101 1111
	Delay1ms(100);
	P2 = 0xBF;	// 1011 1111
	Delay1ms(100);
	P2 = 0x7F;	// 0111 1111
	Delay1ms(100);
		
}

延时程序的函数

void Delay1ms(unsigned int xms)		//@12.000MHz
{
	unsigned char i, j; 
	while(xms)
	{
        i = 2;  
        j = 239;
        do
		{
			while (--j);
		}while (--i);
		xms--;
	}
}

其实最佳写法为把8个数据放到数组之中,省去这样的大篇幅

独立按键

轻触按键:相当于一种电子开关,按下接通,松开断开,实现原理是通过按键内部的金属弹片受力弹动来实现接通和断开

声明单独io

#include <reg52.h>
sbit P2_0 = P2^0;

引用<regx52.h>,就不需要声明

#include <regx52.h>
P2_0 = 0;

独立按键控制led亮灭

  1. 查电路引脚图可知,控制第一个独立按键的为P3.1IO口,引入regx52.h文件后,可以不需要声明,直接使用P3_1
  2. 独立按键在按下是,贴片接通,为P3_1为低电平,松开时为高电平
  3. if else判断语句,对P3_1进行判断
#include <regx52.h>

void main()
{
	while(1)
	{
		if(P3_1 == 0)
		{
			P2_0 = 0;
		}
		else
			P2_0 = 1;
	}
}

移位运算法 << 左移运算符,高位移出,低位补0
>> 右移运算符,低位移出,高位补0
0011 0010>>1 --> 0001 1001
0011 0010<<2 --> 1100 1000

按位与

0001 1000 & 0010 1010 --> 0001 1000

按位或

0001 1000 | 0010 1010 --> 0011 1010

按位异或(0与0 为0 1与0为1,1与1为0)

0001 1000 ^ 0010 1010 --> 0011 0010

按位取反

~0001 1000 --> 1110 0111

按键的抖动

对于机械开关,当机械触点断开、闭合时,由于机械触点的弹性作用,一个开关在闭合时不会马上稳定的接通,在断开时也不会一下子断开,所以在开关闭合以及断开时的瞬间会伴随一连串的抖动。人话来讲就是,高电平与低电平切换时,不会立即进入低电平,而是在波折

如何消除抖动?

  1. 硬件消抖,加元器件,对抖动进行过滤
  2. 软件消抖,按下按键后加延时函数

独立按键控制led状态

通过延时函数与while循环来模拟抖动的过程,以消除抖动。

取反运算符,使得P2_0口按键改变高低电平

#include <regx52.h>

void Delay(unsigned int xms)	
{
	unsigned char i, j;
	while(xms--)
	{
		i = 2;
		j = 239;
		do
		{
		while (--j);
		} while (--i);
	}
}
void main()
{
	while(1)
	{
		if(P3_1==0)
		{
			Delay(20);
			while(P3_1==0);
			Delay(20);
			P2_0 = ~P2_0;
		}
	}
}

独立按键控制led显示二进制

1 0000 0001 1111 1110

2 0000 0010 1111 1101

3 0000 0011 1111 1100

4 0000 0100 1111 1011

5 0000 0101 1111 1010

6 0000 0110 1111 1001

7 0000 0111 1111 1000

通过按键使得num从1~n,

二进制取反后,赋值给P2整个io口,实现二进制

#include <regx52.h>

void Delay(unsigned int xms)	
{
	unsigned char i, j;
	while(xms--)
	{
			i = 2;
			j = 239;
			do
			{
			while (--j);
		} while (--i);
	}
	
}
void main()
{
	unsigned char LEDnum;
	while(1)
	{
		if(P3_1 == 0)
		{
			Delay(20);
			while(P3_1==0);
			Delay(20);
			LEDnum++;
			P2= ~LEDnum;
		}
	}
}

独立按键控制led移位

使用移位运算符实现下面的情形

0000 0001 0x01<<0

0000 0010 0x01<<1

0000 0100 0x01<<2

0000 1000 0x01<<3

0001 0000 0x01<<4

0010 0000 0x01<<5

0100 0000 0x01<<6

1000 0000 0x01<<7

#include <regx52.h>
void Delay(unsigned int xms)	
{
	unsigned char i, j;
	while(xms--)
	{
			i = 2;
			j = 239;
			do
			{
			while (--j);
		} while (--i);
	}
	
}
void main()
{
	unsigned char num=0;
	P2 = ~(0x01);
	while(1)
	{
		
		if(P3_1==0)
		{
			Delay(20);
			while(P3_1 ==0);
			Delay(20);
			if(num==7)
			{
				num =0;
				P2 = ~(0x01<<num);
			}
			else
			{
				num++;
				P2 = ~(0x01<<num);
			}

		}
		if(P3_0==0)
		{
			Delay(20);
			while(P3_0 ==0);
			Delay(20);
			if(num==0)
			{
				num =7;
				P2 = ~(0x01<<num);
			}
			else
			{
				num--;
				P2 = ~(0x01<<num);
			}
		}
	}

}

数码管

led数码管 :通常是一种简单、廉价的显示器,是由多个发光二极管封装在一起组成"8"字型的器件

一位数码管:

​ 共阴极连接、共阳极连接 两种连接方式

******A******	分布图		10 9 8 7 6  引脚图	
*			*
F			B
*			*	
******G******
*			*
E			C
*			*	·DP
******D******			   1 2 3 4 5 
A --> 7
B --> 6
C --> 4
D --> 2
E --> 1
F --> 9
G --> 10
DP --> 5

共阴极

数字6的段码 为 1011 1110

共阳极

数字6的段码 为0100 0001

四位数码管:

12 11 10 9 8 7		引脚图





1 2 3 4 5 6

动态数码管显示:

​ 利用人眼的分辨率小,不断进行数码管扫描

138译码器

p2_4p2_3p2_2CBA三个对应io口

CBAY
000Y0 = 0 其他为1 LED1
001Y1=0 其他为1 LED2
010Y2 = 0 其他为1 LED3
011Y3 = 0其他为1 LED4
100Y4=0 其他为1 LED5
101Y5=0 其他为1 LED6
110Y6=0 其他为1 LED7
111Y7=0 其他为1 LED8

CBA二进制转为十进制后对应Y后面的数字

74HC245 --> 双向数据缓冲器

高位对端口的高位

io口0端口gio口7端口a,也就是abcdefgdp部分的数据,在传输的时候要反着读

静态数码管显示

// 显示6
#include <regx52.h>

void main()
{
	P2_4 = 1;P2_3 =0;P2_2 =1;
	P0 = 0x7D;
	while(1)
	{
		
	}
}
动态显示需要消影
同时需要在段选与下一次位选之间,加一个清零
位选 段选 清零 位选 段选 位选 段选
#include<regx52.h>
unsigned char NixieTable[]={0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F};


void Delay(unsigned int xms)	
{
	unsigned char i, j;
	while(xms--)
	{
			i = 2;
			j = 239;
			do
			{
			while (--j);
		} while (--i);
	}
	
}
void Nixie(unsigned char Location,Number)
{
	switch(Location)
	{
		case 1:P2_4 = 1;P2_3 =1;P2_2 =1;break;
		case 2:P2_4 = 1;P2_3 =1;P2_2 =0;break;
		case 3:P2_4 = 1;P2_3 =0;P2_2 =1;break;
		case 4:P2_4 = 1;P2_3 =0;P2_2 =0;break;
		case 5:P2_4 = 0;P2_3 =1;P2_2 =1;break;
		case 6:P2_4 = 0;P2_3 =1;P2_2 =0;break;
		case 7:P2_4 = 0;P2_3 =0;P2_2 =1;break;
		case 8:P2_4 = 0;P2_3 =0;P2_2 =0;break;
	}
	P0  = NixieTable[Number];
	Delay(1);
	P0 = 0x00;

}
void main()
{
	
	while(1)
	{
		Nixie(1,1);
		Nixie(2,2);
		Nixie(3,3);
	}
		
}

数字0-9的字段:

0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F

A-F的字段:

0x77,0x7C,0x39,0x5E,0x79,0x71

数码管驱动方式:

  1. 单片机直接扫描:硬件设备简单,但会耗费大量的单片机CPU时间
  2. 专用芯片扫描:内部自带显存、扫描电路、单片机质询告诉它显示什么即可

模块化编程

// 在头文件中的声明
// .h
#ifndef __DELAY_H__
#define __DELAY_H__

void Delay(unsigned int xms);

#endif

// 在模块函数文件中的定义
// .c
void Delay(unsigned int xms)
{
    unsigned char i, j;
    while(xms--)
    {
        i = 2;
        j = 239;
        do
        {
            while (--j);
        } while (--i);
    }
}

// 在main函数中的引用
#include "Delay.h"


void main()
{
    Delay(10);
}

// 常用的预编译命令
#include <REGX52.H>			把REGX52.H文件的内容搬到此处
#define PI 3.14				定义PI,将PI替换为3.14
#define ABC 				定义ABC
#ifndef __XX_H__			如果没有定义__XX_H__
#endif						与#ifndef,#if匹配,组成“括号”
							此外还有#ifdef,#if,#else,#elif,#undef等

LCD模块化代码调用

LCD_Init();							初始化
LCD_ShowChar(1,1,'A');				显示一个字符
LCD_ShowString(1,3,"Hello");		显示字符串
LCD_ShowNum(1,9,123,3);				显示十进制数字
LCD_ShowSignedNum(1,13,-66,2);		显示有符号十进制数字
LCD_ShowHexNum(2,1,0xA8,2);			显示十六进制数字
LCD_ShowBinNum(2,4,0xAA,8);			显示二进制数字

在本地中有关于LCD1602的.c和.h文件,需要时引用即可,在主函数中直接调用以上函数,达到实现LCD屏的作用

矩阵键盘

  1. 在键盘中按键较多时,为了减少I/O口的占用,通常将键盘排列成矩阵形式
  2. 采取逐行或者逐列的扫描,就可以读出任何设置的按键状态

我们这里矩阵键盘采取逐列扫描

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rEC4LPLq-1681634737177)(E:\单片机笔记\assets\1678604686710.png)]

首先 矩阵键盘受P1 i/o口控制,给整个io口高电平 0-3,给低电平,进行列的测试,当矩阵键盘按下时,4-7 会出现变为低电平
本函数模块的逻辑基于此
另外矩阵键盘与独立按键一样需要消抖
// MatrixKey.c
#include <regx52.h>
#include "Delay.h"
unsigned char MatrixKey()
{
	unsigned char KeyNumber=0;
	P1 = 0xFF;
	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;
}
// main.c
#include<regx52.h>
#include"MatrixKey.h"
#include"LCD1602.h"
void main()
{	
	LCD_Init();
	LCD_ShowString(1,1,"hello world");
	while(1)
	{
		unsigned KeyNumber = 0;
		KeyNumber = MatrixKey();
		if(KeyNumber)
		{
			LCD_ShowNum(2,1,KeyNumber,2);
		}
	}
}

矩阵键盘密码锁

s1到s9为1-9 – > 这里可以直接采用按键的返回值,s1 - s9 本身按键返回值就是1-9

s10为0 – > 这里可以采取取模的方法,使得10%10 = 0

s11为确认键 – > 密码正确反馈 ok 密码错误显示 err

s12为返回键 --> 返回设置为清零

s13位退出键 – > 退出设定为初始化

// main.c
#include<regx52.h>
#include"MatrixKey.h"
#include"LCD1602.h"
unsigned int Password;
int count = 0;

void main()
{	
	LCD_Init();
	LCD_ShowString(1,1,"PASSWORD:");
	while(1)
	{
		unsigned KeyNum = 0;
		KeyNum = MatrixKey();
        if(KeyNum)
        {
            if(KeyNum <= 10)
            {
                if(count<4)
                {
                    Password *= 10;  // 密码左移一位
                    Password += KeyNum % 10;   // 加上一位密码
                    count++; // 计数加1次
                }
                LCD_ShowNum(2,1,Password,4);
            }
            if(KeyNum ==11)
            {
                if(Password==2345)
                {
                    LCD_ShowString(1,14,"OK ");
                    Password = 0;
                    count = 0;
                    LCD_ShowNum(2,1,Password,4);
                }
                else
                {
                    LCD_ShowString(1,14,"ERR");
                    Password = 0;
                    count = 0;
                    LCD_ShowNum(2,1,Password,4);
                }
            }
            if(KeyNum == 12)
            {
                Password = 0;
                count = 0;
                LCD_ShowNum(2,1,Password,4);
            }
            if(KeyNum == 13)
            {
                LCD_Init();
                LCD_ShowString(1,1,"PASSWORD:");
            }
        }
	}

}

定时器

介绍:51单片机的定时器属于单片机内部资源,其电路的连接和运转,都在单片机内部完成

定时器的作用:

  1. 用于计时系统,可实现软件的计时,或者使程序每隔一段时间完成一项操作
  2. 代替长时间的Delay,提高CPU运行效率和处理速度

定时器的个数:3个(T0,T1,T2),T0,T1与传统51单片机兼容

注意:定时器的资源和单片机的型号是关联在一起的,不同的型号可能会有不同的定时器个数和操作方式,但一般来说,T0和T1的操作方式是所有51单片机所共有的

框图:

定时器在单片机内部就像一个小闹钟一样,根据时钟的输出信号,每隔“一秒”,计数单元的数值就增加一,当计数单元数值增加到“设定的闹钟提醒时间”时,计数单元就会向中断系统发出中断申请,产生“响铃提醒”,使程序跳转到中断服务函数中执行

1.时钟 --> 2.计数单元 --> 3.中断系统

1.提供计数单元的时钟脉冲

2.时钟计数

3.产生中断执行定时的任务

工作模式

​ 模式0:13位定时器/计数器

**模式1:16位定时器/计数器(常用)**

模式2:8位自动重装模式

 模式3:两个8位计数器

中断程序的流程:

​ 主程序 --> 中断请求 继续执行主程序

​ ↓ ↑

​ 中断响应 --> 执行中断处理程序 --> 中断返回

定时器需要配置初始信息

//此函数为定时器与中断初始化函数
void Timer0Init()		//1毫秒@12.000MHz
{
	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
{
    static unsigned int T0cnt;
    TL0 = 0x18;		//设置定时初值
	TH0 = 0xFC;		//设置定时初值
    T0cnt++;
    if(T0cnt>=1000)
    {
        T0cnt = 0;
    }
}

定时器控制流水灯代码

// main.c
#include <REGX52.H>
#include <intrins.h>
#include "Timer0.h"
#include "Key.h"

unsigned char KeyNumber;
unsigned char LEDmode;
void main()
{
    P2 = 0xFE;
    Timer0Init();
    while(1)
    {
        KeyNumber = Key();
        if(KeyNumber)
        {
            if(KeyNumber==1)
            {
                LEDmode++;
                if(LEDmode>=2)
                    LEDmode=0;
            }
        }
    }
}

void Timer0_Rountine()      interrupt 1
{
    static unsigned int T0cnt;
    TL0 = 0x18;		//设置定时初值
	TH0 = 0xFC;		//设置定时初值
    T0cnt++;
    if(T0cnt>=500)
    {
        T0cnt=0;
        if(LEDmode==0)
            P2 =_crol_(P2,1);
        if(LEDmode==1) 
            P2 =_cror_(P2,1);
    }
}  

定时器闹钟代码

// main.c
#include <REGX52.H>
#include "LCD1602.h"
#include "Timer0.h"
unsigned int Sec=55,Min=59,Hour=23;

void main()
{
    Timer0Init();
    LCD_Init();
    LCD_ShowString(1,1,"Clock:");
    LCD_ShowString(2,1,"  :  :");
    while(1)
    {
        LCD_ShowNum(2,1,Hour,2);
        LCD_ShowNum(2,4,Min,2);
        LCD_ShowNum(2,7,Sec,2);
    }

}
void Timer0_Routine()   interrupt 1
{
    static unsigned int Tcnt;
	TL0 = 0x18;		//设置定时初值
	TH0 = 0xFC;		//设置定时初值
    Tcnt++;
    if(Tcnt>=1000)
    {
        Tcnt=0;
        Sec++;
        if(Sec>=60)
        {
            Sec=0;
            Min++;
            if(Min>=60)
            {
                Min=0;
                Hour++;
                if(Hour>=24)
                {
                    Sec=0;
                    Min=0;
                    Hour=0;
                }
            }
        }
    }
}

串口

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gnIn1Ju9-1681634737178)(E:\单片机笔记\assets\1678243236002.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BSAXlhy6-1681634737178)(E:\单片机笔记\assets\1678243260581.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TfZVzrLB-1681634737178)(E:\单片机笔记\assets\1678243283848.png)]

串口配置初始化函数

/**
  * @brief  串口初始化函数
  * @param  无
  * @retval 无
  */
void UART_Init()		//4800bps@12.000MHz
{
	PCON |= 0x80;		//使能波特率倍速位SMOD
	SCON = 0x50;		//8位数据,可变波特率
	TMOD &= 0x0F;		//清除定时器1模式位
	TMOD |= 0x20;		//设定定时器1为8位自动重装方式
	TL1 = 0xF3;		//设定定时初值
	TH1 = 0xF3;		//设定定时器重装值
	ET1 = 0;		//禁止定时器1中断
	TR1 = 1;		//启动定时器1
    EA = 1;         // 配置中断
    ES = 1;         // 配置中断
    
}

串口发送字节信息函数

/**
  * @brief  串口发送字节信息函数
  * @param  Byte 发送的信息
  * @retval 无
  */
void UART_SendByte(unsigned char Byte)
{
    SBUF = Byte;
}

串口中断函数

/* 串口中断程序模板*/
void UART_Routine() interrupt 4
{
    if(RI==1)
    {
        P2 = SBUF;
        UART_SendByte(SBUF);
        RI = 0;
    }
}

串口向电脑发送数据程序

// main.c
#include <REGX52.H>
#include "Delay.h"
#include "UART.h"
unsigned char Sec;

void main()
{
    UART_Init();
    while(1)
    {
        UART_SendByte(Sec);
        Sec++;
        Delay(1000);
    }
}

电脑向串口发送数据控制LED

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

void main()
{
    UART_Init();
    while(1)
    {

    }
}
void UART_Routine() interrupt 4
{
    if(RI==1)
    {
        P2 = SBUF;
        UART_SendByte(SBUF);
        RI = 0;
    }
}

LED矩阵

原理与矩阵键盘一样

74HC595芯片工作原理

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gFtLmxUG-1681634737179)(E:\单片机笔记\assets\1678242178331.png)]

RCLK --> RCK 上升沿锁存,一旦给上升沿会把下图中左边的值移到右边,要初始化为0

SERCLK --> SCK 上升沿移位,一旦是上升沿就会向下移位储存SER 中的数值,要初始化为0

SER --> SER 用来接收高低电平的数据Data

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q29Mtck1-1681634737179)(E:\单片机笔记\assets\1678242260199.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C5NldOV6-1681634737179)(E:\单片机笔记\assets\1678243206725.png)]

// 定义引脚
sbit RCK = P3^5; // RCLK
sbit SCK = P3^6; // SERCLK
sbit SER = P3^4; // SER

// 初始化函数
void Init()
{
    SCK = 0;
    RCK = 0;
}
// 控制74HC595写数据
void _74HC595_Write_Byte(unsigned char Byte)
{
    unsigned char i;
    for (i = 0; i < 8; i++)
    {
        SER = Byte&(0x80>>i); // Byte与上0x80 会得到第一位的数据,每次移位就可以得到Byte的每一位
        // SER 是一位寄存器 只会储存一个值,所以 = 后面有值就为1,无值就为0
        SCK = 1;
        SCK = 0;
    }
    RCK = 1;
    RCK = 0;
  
}

// 矩阵书写行和列的函数
void MatrixLED_ShowColumn(unsigned char Column,unsigned char Data)
{
    _74HC595_Write_Byte(Data);
    P0 = ~(0x80 >> Column);// 确定哪一列显示
    Delay(1); //
    P0 = 0xFF;// 这两句是为了消影
}
// main
#include <REGX52.H>
#include "MatrixLED.h"
// 取模软件生成的矩阵编号
unsigned char code Anmiation[] = {
    0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
    0x7F,0x08,0x08,0x08,0x7F,0x00,0x0E,0x15,
    0x15,0x0D,0x00,0x7E,0x01,0x02,0x00,0x7E,
    0x01,0x02,0x00,0x06,0x09,0x09,0x06,0x00,
    0x00,0x7D,0x00,0x00,0x00,0x00,0x00,0x00,
    0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
};
// 这是流动矩阵的main函数
void main()
{
    //Offset是偏移量,Cnt是计数器
    unsigned char i,Offset=0,Cnt=0;
    Init();
    while(1)
        
    {
        for(i = 0; i < 8; i++)
        {
  			 MatrixLED_ShowColumn(i,Anmiation[i+Offset]);
        }
        Cnt++;
        if(Cnt>10)
        {
            Cnt=0;
            Offset++;
            if(Offset>40)
            {
                Offset = 0;
            }
        }
        
    }
}

DS1302时钟

DS1302时钟芯片模式与年月日时分秒星期的接口

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wQuX5lbJ-1681634737179)(E:\单片机笔记\assets\1678605621225.png)]DS1302的引脚图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NasfizS8-1681634737179)(E:\单片机笔记\assets\1678605738882.png)]

时序定义图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VwKKIz1f-1681634737180)(E:\单片机笔记\assets\1678606299234.png)]

显示时间程序

// DS1302.h
#ifndef __DS1302_H__
#define __DS1302_H__

extern unsigned int DS1302_time[];
void DS1302_Settime(void);
void DS1302_Readtime(void);
void DS1302_WriteByte(unsigned char Command,unsigned char Data);

unsigned char DS1302_ReadByte(unsigned char Command);


#endif
// DS1302.c
#include <REGX52.H>

// 宏定义时钟控制的年月日时分秒星期的各位置的数据写入接口
#define DS1302_SECOND   0x80
#define DS1302_MINUTE   0x82
#define DS1302_HOUR     0x84
#define DS1302_DATE     0x86
#define DS1302_MONTH    0x88
#define DS1302_DAY      0x8a
#define DS1302_YEAR     0x8c
#define DS1302_WP       0x8e
// 定义引脚 DS1302_SCLK 是上升与下降沿  DS1302_IO 读取数据的io口  DS1302_CE 使能
sbit DS1302_SCLK = P3^6;
sbit DS1302_IO = P3^4;
sbit DS1302_CE = P3^5;
// 外部可引用数组,用来存时间的各个量
unsigned int DS1302_time[]={23,3,10,20,13,55,5};
/**
* @brief  DS1302初始化函数 DS1302_SCLK 上电后为0  DS1302_CE 使能上电后也要为0
  * @param  无
  * @retval 无
  */
void DS1302_Init(void)
{
    DS1302_SCLK = 0;
    DS1302_CE = 0;

}
/**
  * @brief  DS1302 写命令与传数据的函数
  * @param  Command 传入要进行的命令
  * @param  Data 传入要写入的数据
  * @retval 无
  */
void DS1302_WriteByte(unsigned char Command,Data)
{
    unsigned char i;
	
	DS1302_CE=1; // 使能给高电平
	for(i=0;i<8;i++)
	{
		DS1302_IO=Command&(0x01<<i); // 每次写入要进行的命令  把最低位放在前面
		DS1302_SCLK=1;  // 给上升沿,读入数据
		DS1302_SCLK=0;  // 恢复为0
	}
	for(i=0;i<8;i++)
	{
		DS1302_IO=Data&(0x01<<i); // 写入时间数据  低位在前
		DS1302_SCLK=1;   // 给上升沿读入数据
		DS1302_SCLK=0;   // 恢复为0
	}
	DS1302_CE=0;  // 使能恢复为低电平
}
/**
  * @brief  DS1302读数据的函数
  * @param  Command 读数据的命令
  * @retval 返回读出来的数据
  */
unsigned char DS1302_ReadByte(unsigned char Command)
{
    unsigned char i,Data = 0x00;
    DS1302_CE = 1;      // 使能给高电平
    Command |= 0x01;    // 为了减少引脚的定义,所以 将写的命令或上1 就会把最低位置1,变为读的模式
    for (i = 0;i < 8;i++)
    {
        DS1302_IO = Command&(0X01<<i);  // 每次写入要进行的命令  与1是清零
        DS1302_SCLK = 0;     // 这里这样写,可以保证最后时序没有超过读的时序
        DS1302_SCLK = 1;     // 
    }
    for (i = 0;i < 8;i++)
    {
        DS1302_SCLK = 1;  // 在这里继续给sclk置1,是因为 读数据时少了一个上升沿,让它停留一会
        DS1302_SCLK = 0;  // 给下降沿 读数据
        if (DS1302_IO){Data |= (0x01<<i);}  // 如果io口上有数据1,Data与上0x01 会变为1
    }
    DS1302_CE = 0;  // 使能恢复低电平
    DS1302_IO = 0;  // 读完数据后,io口清零,否则读取数据可能有错误
    return Data;

}
/**
  * @brief  DS1302设置时间函数
  * @param  无
  * @retval 无
  */
void DS1302_Settime(void)
{

    // 这里调用写入数据函数,先传入命令吗,在传入要写的时间数据 // 十进制转为BCD码
    DS1302_WriteByte(DS1302_WP,0X00);
    DS1302_WriteByte(DS1302_YEAR,DS1302_time[0]/10*16+DS1302_time[0]%10);
    DS1302_WriteByte(DS1302_MONTH,DS1302_time[1]/10*16+DS1302_time[1]%10);
    DS1302_WriteByte(DS1302_DATE,DS1302_time[2]/10*16+DS1302_time[2]%10);
    DS1302_WriteByte(DS1302_HOUR,DS1302_time[3]/10*16+DS1302_time[3]%10);
    DS1302_WriteByte(DS1302_MINUTE,DS1302_time[4]/10*16+DS1302_time[4]%10);
    DS1302_WriteByte(DS1302_SECOND,DS1302_time[5]/10*16+DS1302_time[5]%10);
    DS1302_WriteByte(DS1302_DAY,DS1302_time[6]/10*16+DS1302_time[6]%10);
    DS1302_WriteByte(DS1302_WP,0X80);

}
/**
  * @brief  读时间数据的函数
  * @param  无
  * @retval 无
  */
void DS1302_Readtime(void)
{
    // 这里调用读数据函数,直接传入读的命令,然后BCD码转为10进制
    unsigned char TEMP = 0;
    TEMP = DS1302_ReadByte(DS1302_YEAR);
    DS1302_time[0] = TEMP/16*10+TEMP%16;
    TEMP = DS1302_ReadByte(DS1302_MONTH);
    DS1302_time[1] = TEMP/16*10+TEMP%16;
    TEMP = DS1302_ReadByte(DS1302_DATE);
    DS1302_time[2] = TEMP/16*10+TEMP%16;
    TEMP = DS1302_ReadByte(DS1302_HOUR);
    DS1302_time[3] = TEMP/16*10+TEMP%16;
    TEMP = DS1302_ReadByte(DS1302_MINUTE);
    DS1302_time[4] = TEMP/16*10+TEMP%16;
    TEMP = DS1302_ReadByte(DS1302_SECOND);
    DS1302_time[5] = TEMP/16*10+TEMP%16;
    TEMP = DS1302_ReadByte(DS1302_DAY);
    DS1302_time[6] = TEMP/16*10+TEMP%16;
}
#include <REGX52.H>
#include "DS1302.h"
#include "LCD1602.h"

unsigned char Second;

void main()
{
    LCD_Init();
    DS1302_Init();
    DS1302_Settime();
    LCD_ShowString(1,1,"  -  -");
    LCD_ShowString(2,1,"  :  :");
    while(1)
    {
        DS1302_Readtime();
        LCD_ShowNum(1,1,DS1302_time[0],2);
        LCD_ShowNum(1,4,DS1302_time[1],2);
        LCD_ShowNum(1,7,DS1302_time[2],2);
        LCD_ShowNum(2,1,DS1302_time[3],2);
        LCD_ShowNum(2,4,DS1302_time[4],2);
        LCD_ShowNum(2,7,DS1302_time[5],2);
        LCD_ShowNum(2,9,DS1302_time[6],2);
    
    }

}

可调时钟程序

// main.c

#include <REGX52.H>
#include "DS1302.h"
#include "LCD1602.h"
#include "Timer0.h"
#include "Key.h"
unsigned char KeyNumber,MODE,TimeSetSelect,TimeSetFlashFlag;

void TimeShow(void)
{
    DS1302_ReadTime();
    LCD_ShowNum(1,1,DS1302_Time[0],2);
    LCD_ShowNum(1,4,DS1302_Time[1],2);
    LCD_ShowNum(1,7,DS1302_Time[2],2);
    LCD_ShowNum(2,1,DS1302_Time[3],2);
    LCD_ShowNum(2,4,DS1302_Time[4],2);
    LCD_ShowNum(2,7,DS1302_Time[5],2);
   
}
void TimeSet(void)//时间设置功能
{
	if(KeyNumber==2)//按键2按下
	{
		TimeSetSelect++;//设置选择位加1
		TimeSetSelect%=6;//越界清零
	}
	if(KeyNumber==3)//按键3按下
	{
		DS1302_Time[TimeSetSelect]++;//时间设置位数值加1
		if(DS1302_Time[0]>99){DS1302_Time[0]=0;}//年越界判断
		if(DS1302_Time[1]>12){DS1302_Time[1]=1;}//月越界判断
		if( DS1302_Time[1]==1 || DS1302_Time[1]==3 || DS1302_Time[1]==5 || DS1302_Time[1]==7 || 
			DS1302_Time[1]==8 || DS1302_Time[1]==10 || DS1302_Time[1]==12)//日越界判断
		{
			if(DS1302_Time[2]>31){DS1302_Time[2]=1;}//大月
		}
		else if(DS1302_Time[1]==4 || DS1302_Time[1]==6 || DS1302_Time[1]==9 || DS1302_Time[1]==11)
		{
			if(DS1302_Time[2]>30){DS1302_Time[2]=1;}//小月
		}
		else if(DS1302_Time[1]==2)
		{
			if(DS1302_Time[0]%4==0)
			{
				if(DS1302_Time[2]>29){DS1302_Time[2]=1;}//闰年2月
			}
			else
			{
				if(DS1302_Time[2]>28){DS1302_Time[2]=1;}//平年2月
			}
		}
		if(DS1302_Time[3]>23){DS1302_Time[3]=0;}//时越界判断
		if(DS1302_Time[4]>59){DS1302_Time[4]=0;}//分越界判断
		if(DS1302_Time[5]>59){DS1302_Time[5]=0;}//秒越界判断
	}
	if(KeyNumber==4)//按键3按下
	{
		DS1302_Time[TimeSetSelect]--;//时间设置位数值减1
		if(DS1302_Time[0]<0){DS1302_Time[0]=99;}//年越界判断
		if(DS1302_Time[1]<1){DS1302_Time[1]=12;}//月越界判断
		if( DS1302_Time[1]==1 || DS1302_Time[1]==3 || DS1302_Time[1]==5 || DS1302_Time[1]==7 || 
			DS1302_Time[1]==8 || DS1302_Time[1]==10 || DS1302_Time[1]==12)//日越界判断
		{
			if(DS1302_Time[2]<1){DS1302_Time[2]=31;}//大月
			if(DS1302_Time[2]>31){DS1302_Time[2]=1;}
		}
		else if(DS1302_Time[1]==4 || DS1302_Time[1]==6 || DS1302_Time[1]==9 || DS1302_Time[1]==11)
		{
			if(DS1302_Time[2]<1){DS1302_Time[2]=30;}//小月
			if(DS1302_Time[2]>30){DS1302_Time[2]=1;}
		}
		else if(DS1302_Time[1]==2)
		{
			if(DS1302_Time[0]%4==0)
			{
				if(DS1302_Time[2]<1){DS1302_Time[2]=29;}//闰年2月
				if(DS1302_Time[2]>29){DS1302_Time[2]=1;}
			}
			else
			{
				if(DS1302_Time[2]<1){DS1302_Time[2]=28;}//平年2月
				if(DS1302_Time[2]>28){DS1302_Time[2]=1;}
			}
		}
		if(DS1302_Time[3]<0){DS1302_Time[3]=23;}//时越界判断
		if(DS1302_Time[4]<0){DS1302_Time[4]=59;}//分越界判断
		if(DS1302_Time[5]<0){DS1302_Time[5]=59;}//秒越界判断
	}
	//更新显示,根据TimeSetSelect和TimeSetFlashFlag判断可完成闪烁功能
	if(TimeSetSelect==0 && TimeSetFlashFlag==1){LCD_ShowString(1,1,"  ");}
	else {LCD_ShowNum(1,1,DS1302_Time[0],2);}
	if(TimeSetSelect==1 && TimeSetFlashFlag==1){LCD_ShowString(1,4,"  ");}
	else {LCD_ShowNum(1,4,DS1302_Time[1],2);}
	if(TimeSetSelect==2 && TimeSetFlashFlag==1){LCD_ShowString(1,7,"  ");}
	else {LCD_ShowNum(1,7,DS1302_Time[2],2);}
	if(TimeSetSelect==3 && TimeSetFlashFlag==1){LCD_ShowString(2,1,"  ");}
	else {LCD_ShowNum(2,1,DS1302_Time[3],2);}
	if(TimeSetSelect==4 && TimeSetFlashFlag==1){LCD_ShowString(2,4,"  ");}
	else {LCD_ShowNum(2,4,DS1302_Time[4],2);}
	if(TimeSetSelect==5 && TimeSetFlashFlag==1){LCD_ShowString(2,7,"  ");}
	else {LCD_ShowNum(2,7,DS1302_Time[5],2);}
}
void main()
{
	LCD_Init();
	DS1302_Init();
	Timer0Init();
	LCD_ShowString(1,1,"  -  -  ");//静态字符初始化显示
	LCD_ShowString(2,1,"  :  :  ");
	
	DS1302_SetTime();//设置时间
	
	while(1)
	{
		KeyNumber=Key();//读取键码
		if(KeyNumber==1)//按键1按下
		{
			if(MODE==0){MODE=1;TimeSetSelect=0;}//功能切换
			else if(MODE==1){MODE=0;DS1302_SetTime();}
		}
		switch(MODE)//根据不同的功能执行不同的函数
		{
			case 0:TimeShow();break;
			case 1:TimeSet();break;
		}
	}
}
void Timer0_Routine() interrupt 1
{
	static unsigned int T0Count;
	TL0 = 0x18;		//设置定时初值
	TH0 = 0xFC;		//设置定时初值
	T0Count++;
	if(T0Count>=500)//每500ms进入一次
	{
		T0Count=0;
		TimeSetFlashFlag=!TimeSetFlashFlag;//闪烁标志位取反
	}
}

AT24C02与I2C总线

I2C的时序结构

起始与终止条件:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eCSBSZYt-1681634737180)(E:\单片机笔记\assets\1679188178440.png)]

/**
  * @brief  I2C总线起始函数
  * @param  无
  * @retval 无
  */
void I2C_Start()
{
    I2C_SCL=1;  // 时钟线置1
    I2C_SDA=1;  // 数据线置1
    I2C_SDA=0;  // 数据线产生下降沿
    I2C_SCL=0;  // 时钟线恢复0
}
/**
  * @brief  I2C总线终止函数
  * @param  无
  * @retval 无
  */
void I2C_Stop()
{
    I2C_SDA=0;  // 数据线置0
    I2C_SCL=1;  // 时钟线置1
    I2C_SDA=1;  // 数据线产生上升沿
}

I2C发送数据函数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rF0WZDBQ-1681634737180)(E:\单片机笔记\assets\1679188770934.png)]

/**
  * @brief  I2C总线发送数据函数
  * @param  要发送的数据
  * @retval 无
  */
void I2C_SendByte(unsigned char Byte)
{
    unsigned char i;
    for(i=0;i<8;i++)
    {
        I2C_SDA=Byte&(0x80>>i);  //从高位发送 
        I2C_SCL=1;
        I2C_SCL=0;
    }
    
}

接收数据函数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-t7PI60u8-1681634737180)(E:\单片机笔记\assets\1679188806721.png)]

/**
  * @brief  I2C接收数据函数
  * @param  无
  * @retval 返回接收到的数据
  */
unsigned char I2C_ReceiveByte(void)
{
    unsigned char i,Byte=0x00;
    I2C_SDA=1;
    for(i=0;i<8;i++)
    {
        I2C_SCL=1;
        if(I2C_SDA)
        {Byte|=(0x80>>i);}
        I2C_SCL=0;
    }
    return Byte;
}

发送与接收应答函数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AbQSBNbw-1681634737180)(E:\单片机笔记\assets\1679188977713.png)]

/**
  * @brief  I2C发送应答函数
  * @param  要发送的应答指令
  * @retval 无
  */
void I2C_SendAck(unsigned char AckBit)
{
    I2C_SDA=AckBit;
    I2C_SCL=1;
    I2C_SCL=0;
}

/**
  * @brief  I2C接收应答函数
  * @param  无
  * @retval 返回接收到的应答 0为应答,1为非应答
  */
unsigned char I2C_ReceiveAck(void)
{
	unsigned char AckBit;
	I2C_SDA=1;
	I2C_SCL=1;
	AckBit=I2C_SDA;
	I2C_SCL=0;
	return AckBit;
}

AT24C02发送数据逻辑

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xeyUTaJr-1681634737181)(E:\单片机笔记\assets\1679189161124.png)]

注意:AT24C02的固定地址为1010,可配置地址本开发板上为000

所以SLAVE ADDRESS+W为0xA0,SLAVE ADDRESS+R为0xA1

/**
  * @brief  AT24C02写入数据函数
  * @param  写入地址    写入数据
  * @retval 无
  */
void AT24C02_WriteByte(unsigned char WordAddress,Data)
{
    I2C_Start();
    I2C_SendByte(AT24C02_ADDRESS);
    I2C_ReceiveAck();
    I2C_SendByte(WordAddress);
    I2C_ReceiveAck();
    I2C_SendByte(Data);
    I2C_ReceiveAck();
    I2C_Stop();
}
/**
  * @brief  AT24C02读出数据函数
  * @param  要读取数据的地址
  * @retval 读取到的数据
  */
unsigned char AT24C02_ReadByte(unsigned char WordAddress)
{
	unsigned char Data;
	I2C_Start();
	I2C_SendByte(AT24C02_ADDRESS);
	I2C_ReceiveAck();
	I2C_SendByte(WordAddress);
	I2C_ReceiveAck();
	I2C_Start();
	I2C_SendByte(AT24C02_ADDRESS|0x01);
	I2C_ReceiveAck();
	Data=I2C_ReceiveByte();
	I2C_SendAck(1);
	I2C_Stop();
	return Data;
}

注意:51是存在写周期的,所以瞬间写入,瞬间读出,会导致数据存储失败

考虑延时函数,延时5us

rief I2C接收数据函数

  • @param 无
  • @retval 返回接收到的数据
    */
    unsigned char I2C_ReceiveByte(void)
    {
    unsigned char i,Byte=0x00;
    I2C_SDA=1;
    for(i=0;i<8;i++)
    {
    I2C_SCL=1;
    if(I2C_SDA)
    {Byte|=(0x80>>i);}
    I2C_SCL=0;
    }
    return Byte;
    }

发送与接收应答函数

[外链图片转存中...(img-AbQSBNbw-1681634737180)]

```c
/**
  * @brief  I2C发送应答函数
  * @param  要发送的应答指令
  * @retval 无
  */
void I2C_SendAck(unsigned char AckBit)
{
    I2C_SDA=AckBit;
    I2C_SCL=1;
    I2C_SCL=0;
}

/**
  * @brief  I2C接收应答函数
  * @param  无
  * @retval 返回接收到的应答 0为应答,1为非应答
  */
unsigned char I2C_ReceiveAck(void)
{
	unsigned char AckBit;
	I2C_SDA=1;
	I2C_SCL=1;
	AckBit=I2C_SDA;
	I2C_SCL=0;
	return AckBit;
}

AT24C02发送数据逻辑

[外链图片转存中…(img-xeyUTaJr-1681634737181)]

注意:AT24C02的固定地址为1010,可配置地址本开发板上为000

所以SLAVE ADDRESS+W为0xA0,SLAVE ADDRESS+R为0xA1

/**
  * @brief  AT24C02写入数据函数
  * @param  写入地址    写入数据
  * @retval 无
  */
void AT24C02_WriteByte(unsigned char WordAddress,Data)
{
    I2C_Start();
    I2C_SendByte(AT24C02_ADDRESS);
    I2C_ReceiveAck();
    I2C_SendByte(WordAddress);
    I2C_ReceiveAck();
    I2C_SendByte(Data);
    I2C_ReceiveAck();
    I2C_Stop();
}
/**
  * @brief  AT24C02读出数据函数
  * @param  要读取数据的地址
  * @retval 读取到的数据
  */
unsigned char AT24C02_ReadByte(unsigned char WordAddress)
{
	unsigned char Data;
	I2C_Start();
	I2C_SendByte(AT24C02_ADDRESS);
	I2C_ReceiveAck();
	I2C_SendByte(WordAddress);
	I2C_ReceiveAck();
	I2C_Start();
	I2C_SendByte(AT24C02_ADDRESS|0x01);
	I2C_ReceiveAck();
	Data=I2C_ReceiveByte();
	I2C_SendAck(1);
	I2C_Stop();
	return Data;
}

注意:51是存在写周期的,所以瞬间写入,瞬间读出,会导致数据存储失败

考虑延时函数,延时5us

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值