AT24C02存储器
# AT24C02(I2C总线)
# 一、存储器介绍
今天介绍一下AT24C02 (opens new window),首先呢,它是一种可以实现掉电不丢失的存储器,可用于保存单片机运行时想要永久保存的数据信息,在介绍AT24C02之前,我们先来介绍一下存储器!
先来简单介绍一下RAM(随机存储器)以及ROM(只读存储器)的优缺点吧!
优点 | 缺点 | |
---|---|---|
RAM | 储存速度快 | 掉电丢失 |
ROM | 存储速度慢 | 掉电不丢失 |
# 1.1 RAM
- RAM主要分为SRAM(静态RAM)和DRAM(动态RAM),SRAM主要用于电脑CPU以及我们的单片机CPU;而DRAM主要用在电脑内存条以及手机的运行内存,因为电容器会掉电,所以需要不断进行扫描。
优点 | 缺点 | |
---|---|---|
RAM | 储存速度快 | 掉电丢失 |
ROM | 存储速度慢 | 掉电不丢失 |
# 1.2 ROM
- ROM主要分为Mask ROM(掩膜ROM),PROM(可编程ROM),EPROM(可擦除可编程ROM ),E2PROM (电可擦除可编程ROM ),这四个是一家的,还有Flash(闪存),硬盘、软盘、光盘等,其中Flash目前使用十分广泛,基本上打败了ROM一家。
特点 | |
---|---|
Mask ROM | 只能读 |
PROM | 可以写,但只能一次 |
EPROM | 可以写多次,但要紫外线照射30分钟 |
E2PROM | 可以写多次,并且只要几毫秒即可 |
Flash | 与E2PROM类似,但集成度更高 |
硬盘、软盘、光盘等 | 软盘和光盘目前见的比较少了 |
# 1.3 存储器简化模型
- 这里的左边是地址总线,下面是数据总线,两种线之间互相不连接,可看做是数据总线放在了地址总线上面(立体的看!)
- 使用的原理:首先我们选择地址总线,比如像赋值10000000,相当于打开了第一行,之后选择连接的结点(之前都没有连接上),将其连上,(像上图,我们给地址总线1000 0000 然后连接(3个红点),那么数据总线就为1011 0000)
- Mask ROM使用的方法是一个二极管(这么做的原因是防止电流经过上面的节点导致数据混乱),
- PROM使用了两个二极管(一个二极管和保险丝),但是其中一个二极管(保险丝)比较容易击穿,当给高电压的时候,蓝色电容(保险丝熔断)击穿,实现数据写入。
# 二、AT24C02
- AT24C02是一种可以实现掉电不丢失的存储器,可用于保存单片机运行时想要永久保存的数据信息
- 存储介质:E2PROM
- 通讯接口:I2C总线
- 容量:256字节
# 2.1 引脚及应用电路
# 2.2 内部结构框图
- 接下来我们来简单的介绍一下内部结构,我们从每个部分进行讲解!
- 第一个就是我们刚刚介绍的存储器简化模型那样,网状结构
- 第二个是一个译码器,用于输入地址
- 第三个是输入输出端,通过Y DEC将数据输出
- 第四个也是译码器,用来帮助MUX输出数据,然后就直接输出数据
- 第五个是用来擦除数据用的
- 第六个是用来设置地址的,里面有个寄存器是用来存储地址的,每写入和读出寄存器自动加一,读出不指定地址,默认拿出寄存器的地址
- 第七个是开始结束逻辑
- 第八个是一个地址比较器
- 第九个是一个控制串行逻辑
# 三、I2C总线
- I2C总线(Inter IC BUS)是由Philips公司开发的一种通用数据总线
- 两根通信线:SCL(Serial Clock)、SDA(Serial Data)
- 同步、半双工,带数据应答
- 通用的I2C总线,可以使各种设备的通信标准统一,对于厂家来说,使用成熟的方案可以缩短芯片设计周期、提高稳定性,对于应用者来说,使用通用的通信协议可以避免学习各种各样的自定义协议,降低了学习和应用的难度
4分钟看懂!I2C通讯协议 最简单的总线通讯!_哔哩哔哩_bilibili (opens new window)
# 3.1 I2C电路规范
- 所有I2C设备的SCL连在一起,SDA连在一起
- 设备的SCL和SDA均要配置成开漏输出模式
- SCL和SDA各添加一个上拉电阻,阻值一般为4.7KΩ左右
- 开漏输出和上拉电阻的共同作用实现了“线与”的功能,此设计主要是为了解决多机通信互相干扰的问题
- 上拉电阻是将总线给拉成高电平,当连接在总线上的任意一个设备输出低电平时,总线被拉低就是输出了低电平
- “线与”的意思就是连接在总线上的设备只要有一个输出低电平(0),总线就为低电平(0),只有全部设备都为高阻态时总线才是高电平
(这里因为讲到了开漏输出,所以也说一下单片机的弱上拉模式)
# 3.1.1 弱上拉:
- 单片机的IO口是一种弱上拉模式,默认是1
- 如果输入了0,那么开关口闭合,输出口拉到GND上,就输出了0
- 如果输入了1,那么开关口断开,就输出了1,所以高电平驱动能力是弱的,低电平驱动能力比较强(单片机本来能接受的电压就不高,遇到GND就被拉到0了,所以高电平驱动能力弱)
# 3.1.2 I2C
- 因为SCL和SDA都是开漏输出模式,所以如果我们想要CPU和其中一个IC通信,那只要让它为0,其他为1(为一开漏输出开关断开,出现浮空,所以就不会有影响)
- 相当于CPU想发0的时候,就拉到0,想发1时就松手,然后上拉电阻拉到1
# 3.1.3 IC的内部结构
# 3.1.4 抽象理解I2C的通信方式
- 通信规则:
- 1、杠子在上方代表1,下方代表0
- 2、每个人只能拉杆子或者松开手
- 3、每个人需要地址进行通信
- 先发送地址,确定要通信的从机,再发送信息
# 3.2 I2C的时序结构
- 接下来我们来介绍一下六个时序结构,只要集齐了这六个时序结构,就可以召唤数据帧了!😍😍😍
# 3.2.1 起始条件
- 起始条件:SCL高电平期间,SDA从高电平切换到低电平(相当于告诉大家我要发送信息了)
void I2C_Start(void)
{
// 可以理解为初始化,确保SDA一定为高电平
I2C_SDA=1;
I2C_SCL=1;
// 按照时序图可得,先SDA为0,再SCL为0
I2C_SDA=0;
I2C_SCL=0;//防止产生多余时序
}
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
# 3.2.2 终止条件
- 终止条件:SCL高电平期间,SDA从低电平切换到高电平(相当于告诉大家我要停止了)
void I2C_Stop(void)
{
I2C_SDA=0;//先写这个,初始化,确保是低电平
I2C_SCL=1;
I2C_SDA=1;
}
1
2
3
4
5
6
2
3
4
5
6
- 终止条件是唯一一个最后不用把SCL置为低电平的时序结构
# 3.2.3 发送一个字节
- 发送一个字节:SCL低电平期间,主机将数据位依次放到SDA线上(高位在前),然后拉高SCL,从机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可发送一个字节
- 这里SDA画两条线是因为我们不确定SDA具体的电平
void I2C_SendByte(unsigned char Byte)
{
unsigned char i;
for(i=0;i<8;i++)
{
//SCL低电平期间,主机将数据位依次放到SDA线上
//之所以不用先将SCL置0,是因为,除了结束条件,其他时序结构最后都会把SCL置0
I2C_SDA=(Byte&0x80>>i); //老师想说的话一个一个往外蹦,话都传到教室(SDA)里
//拉高SCL,从机读取数据位
I2C_SCL=1; //同学开始听教室里老师的话
//拉低,以备进入下次循环,重新拉高SCL,让从机读取下一个数据位
I2C_SCL=0; //同学听完一个
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- 像上面这种立马置高又置低的情况,是要考虑是否加延迟的,但因为我们单片机比较慢,而转换的速度又很快,所以我们可以不加延迟,但是在后面字节写的时候,因为字节写需要时间,所以执行字节写后需要给延时,让AT24C02内部执行一些操作(视频12-2,12:09)
# 3.2.4 接收一个字节
- 接收一个字节:SCL低电平期间,从机将数据位依次放到SDA线上(高位在前),然后拉高SCL,主机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可接收一个字节(主机在接收之前,需要释放SDA,即将SDA置1)
unsigned char I2C_ReceiveByte(void)
{
unsigned char i,Byte = 0x00;
//主机将SDA释放,从机使用SDA,将数据依次放到SDA线上
I2C_SDA=1; //空气里没有老师的话了,同学抓住机会发言,话都传在教室里😭
for(i=0;i<8;i++)
{
//主机要读取数据需要先将SCL置为高电平
I2C_SCL=1; //老师开始听教室里同学的发言
//主机读取数据位
if(I2C_SDA){Byte|=(0x80>>i)} //老师听到一个字就存进脑子里一个字😕
I2C_SCL=0; //老师听完了一个字
}
//读完了8个位,得到数据
return Byte; //老师接收到了同学完整的发言
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 3.2.5 发送应答和接收应答
- 发送应答(SA):在接收完一个字节之后,主机在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答(相当于一个回应)
- 接收应答(RA):在发送完一个字节之后,主机在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接收之前,需要释放SDA)
发送应答:如果给应答,SDA就给0,不给应答就给1
void I2C_SendAck(unsigned char AckBit) { I2C_SDA=AckBit; //老师给出回应,声音在教室里(也有可能不回应🤐) I2C_SCL=1; //同学开始听 I2C_SCL=0; //同学结束听 } unsigned char I2C_ReceiveAck(void) { unsigned char AckBit; I2C_SDA=1; //老师不说了,教室里安静了,同学可以开始回应 I2C_SCL=1;//老师开始听到同学回应 AckBit=I2C_SDA; //老师听到了同学的回应 I2C_SCL=0;//老师听完了同学的回应 return AckBit; //0就是有听到回应,1就是没有回应 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 3.3 I2C 数据帧
- I2C数据帧其实就是上面六个部分拼合在一起,把数据帧拆分开来看,就比较好理解了。
# 3.3.1 发送一帧数据
- 相当于老师(主机)再讲课,学生(从机)给老师回复
# 3.3.2 接收一帧数据
- 相当于老师(主机)叫学生(从机)回答问题
# 3.3.3 复合格式
先发送再接收数据帧
像是一个完整的回答过程,老师(主机)提出问题,学生(从机)回答
# 3.4 AT24C02数据帧
- AT24C02数据帧,其实不止这几个,但我们在这里就简单介绍一下这两种!
- AT24C02的固定地址为1010,可配置地址本开发板上为000,所以SLAVE ADDRESS+W为0xA0,SLAVE ADDRESS+R为0xA1
# 3.4.1 字节写
- 字节写:在WORD ADDRESS处写入数据DATA
# 3.4.2 随机读
- 随机读:读出在WORD ADDRESS处的数据DATA
#include <REGX52.H>
#include "I2C.h"
#define AT24C02_ADDRESS_READ 0xA1
#define AT24C02_ADDRESS_WRITE 0xA0
/**
* @brief AT24C02写入一个字节
* @param WordAddress 要写入字节的地址
* @param Data 要写入的数据
* @retval 无
*/
void AT24C02_WriteByte(unsigned char WordAddress,Date)
{
I2C_Start();
I2C_SendByte(AT24C02_ADDRESS_WRITE);
I2C_ReceiveAck();
I2C_SendByte(WordAddress);
I2C_ReceiveAck();
I2C_SendByte(Date);
I2C_ReceiveAck();
I2C_Stop();
}
/**
* @brief AT24C02读取一个字节
* @param WordAddress 要读出字节的地址
* @retval 读出的数据
*/
unsigned char AT24C02_ReadByte(unsigned char WordAddress)
{
unsigned char Date;
I2C_Start();
I2C_SendByte(AT24C02_ADDRESS_WRITE);
I2C_ReceiveAck();
I2C_SendByte(WordAddress);
I2C_ReceiveAck();
I2C_Start();
I2C_SendByte(AT24C02_ADDRESS_READ);
I2C_ReceiveAck();
Date=I2C_ReceiveByte();
I2C_SendAck(1);
I2C_Stop();
return Date;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# 3.5 AT24C02数据存储主函数
#include <REGX52.H>
#include "Delay.h"
#include "Key.h"
#include "AT24C02.h"
#include "LCD1602.h"
#include "I2C.h"
unsigned char KeyNum;
unsigned int Num;
void main()
{
LCD_Init();
LCD_ShowNum(1,1,Num,5);
while(1)
{
KeyNum=Key();
if(KeyNum==1) //K1按键,Num自增
{
Num++;
LCD_ShowNum(1,1,Num,5);
}
if(KeyNum==2) //K2按键,Num自减
{
Num--;
LCD_ShowNum(1,1,Num,5);
}
if(KeyNum==3) //K3按键,向AT24C02写入数据
{
//Num被定义为int,而我们C51的int 是16位,即两个字节
//所以这里我们需要把Num拆分成高低两位,去存入
//写入的地址可以是0~255中的任意一个(因为我们定义的是unsigned char)
AT24C02_WriteByte(0,Num%256);
//每次写入后必须给5ms的延时,让AT24C02内部执行一些操作
Delay(5);
AT24C02_WriteByte(1,Num/256);
Delay(5);
LCD_ShowString(2,1,"Write OK");
Delay(1000);
LCD_ShowString(2,1," ");
}
if(KeyNum==4) //K4按键,从AT24C02读取数据
{
//读取高低位,整合到一起
Num=AT24C02_ReadByte(0);
Num|=AT24C02_ReadByte(1)<<8;
LCD_ShowNum(1,1,Num,5);
LCD_ShowString(2,1,"Read OK ");
Delay(1000);
LCD_ShowString(2,1," ");
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# 3.6 秒表(定时器扫描按键数码管)
接下来,我们将会改进之前动态数码管的实现,使用定时器来扫描,然后实现一个具有记忆功能的秒表。
我们使用定时器来扫描按键以及数码管,所以按键以及数码管都需要用到定时器的功能,具体内容如下所示:
- 但我们只有一个中断函数,这样很容易出错,而且不能达到目的,并且代码耦合性过高,所以我们采用另一种方式,如下所示。就是将定时函数放到主函数里面去,再每隔一段时间调用各个部分的函数以达到目的,好了,接下来我们看看代码是如何实现的吧!
总体的思路就是,通过定时器和定时中断函数来判断独立按键是否按下,同时不断扫描数码管显示数据,
key.c和key.h
#include <REGX52.H>
unsigned char Key_KeyNumber;
/**
* @brief 获取按键键码
* @param 无
* @retval 按下按键的键码,范围:0,1~4,0表示无按键按下
*/
unsigned char Key(void)
{
unsigned char Temp=0;
Temp = Key_KeyNumber;
// 将Key_KeyNumber置0,因为Key_KeyNumber不会刷新
// 不清零可能导致后面使用这key的时候没有按键按下但是返回上一次记录的KeyNum,覆盖之前不会变,导致误判按下(实则没有)
Key_KeyNumber=0;
return Temp;
}
/**
* @brief 获取当前按键的状态,无消抖及松手检测
* @param 无
* @retval 按下按键的键码,范围:0,1~4,0表示无按键按下
*/
unsigned char Key_GetState()
{
unsigned char KeyNumber=0;
if(P3_1==0){KeyNumber=1;}
if(P3_0==0){KeyNumber=2;}
if(P3_2==0){KeyNumber=3;}
if(P3_3==0){KeyNumber=4;}
return KeyNumber;
}
/**
* @brief 按键驱动函数,在中断中调用
* @param 无
* @retval 无
*/
void Key_Loop(void)
{
static unsigned char NowState,LastState;
//抖动的延时为20ms
LastState=NowState;
NowState=Key_GetState();
//上一次定时中断获取到了keyNumber,新一次为0,说明现在正处于按键松开的抖动阶段
if(LastState==1&&NowState==0)
{
Key_KeyNumber=1;
}
if(LastState==2&&NowState==0)
{
Key_KeyNumber=2;
}
if(LastState==3&&NowState==0)
{
Key_KeyNumber=3;
}
if(LastState==4&&NowState==0)
{
Key_KeyNumber=4;
}
}
------------------------------------------
Key.h
------------------------------------------
#ifndef __KEY_H__
#define __KEY_H__
unsigned char Key(void);
void Key_Loop(void);
#endif
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
- Nixie.c和Nixie.h
#include <REGX52.H>
#include "Delay.h"
//数码管显示缓存区
unsigned char Nixie_Buf[9]={0,10,10,10,10,10,10,10,10};
//数码管段码表
unsigned char NixieTable[]={0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F,0x00,0x40};
//0,1,2,3,4,5,6,7,8,9, ,-
/**
* @brief 设置显示缓存区
* @param Location 要设置的位置,范围:1~8
* @param Number 要设置的数字,范围:段码表索引范围
* @retval 无
*/
void Nixie_SetBuf(unsigned char Location,Number)
{
Nixie_Buf[Location]=Number;
}
/**
* @brief 数码管扫描显示
* @param Location 要显示的位置,范围:1~8
* @param Number 要显示的数字,范围:段码表索引范围
* @retval 无
*/
void Nixie_Scan(unsigned char Location,Number)
{
P0=0x00; //段码清0,消影
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]; //段码输出
}
/**
* @brief 数码管驱动函数,在中断中调用
* @param 无
* @retval 无
*/
void Nixie_Loop(void)
{ //在数码管显示
static unsigned char i=1;
Nixie_Scan(i,Nixie_Buf[i]);
i++;
if(i>=9){i=1;}
}
______________________________________________
Nixie.h
______________________________________________
#ifndef __NIXIE_H__
#define __NIXIE_H__
void Nixie_SetBuf(unsigned char Location,Number);
void Nixie_Scan(unsigned char Location,Number);
void Nixie_Loop(void);
#endif
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
- main.c
#include <REGX52.H>
#include "Delay.h"
#include "Key.h"
#include "AT24C02.h"
#include "Timer0.h"
#include "Nixie.h"
unsigned char KeyNum; //获取独立按键键码
unsigned char Min,Sec,MiniSec; //秒表时间
unsigned char RunFlag; //秒表开关运动暂停标识位
void main()
{
Timer0_Init(); //初始化定时器
while(1)
{
KeyNum=Key();
if(KeyNum==1) //K1按键按下
{
RunFlag=!RunFlag; //启动标志位翻转
}
if(KeyNum==2) //K2按键按下
{
Min=0; //时间清0
Sec=0;
MiniSec=0;
}
if(KeyNum==3) //K3按键按下
{
AT24C02_WriteByte(0,Min); //将分秒写入AT24C02
Delay(5);
AT24C02_WriteByte(1,Sec);
Delay(5);
AT24C02_WriteByte(2,MiniSec);
Delay(5);
}
if(KeyNum==4) //K4按键按下
{
Min=AT24C02_ReadByte(0); //读出AT24C02数据
Sec=AT24C02_ReadByte(1);
MiniSec=AT24C02_ReadByte(2);
//因为如果我们秒表没有暂停按下独立按键4,会读出我们存储的数据,但是会在这个数据的基础上继续运行,所以下面这个语句,是为了在出现这种情况时,停止
if(RunFlag){RunFlag=0;}
}
//数码管是一位一位显示的,所以要将数字拆成高低两位
Nixie_SetBuf(1,Min/10); //设置显示缓存,显示数据
Nixie_SetBuf(2,Min%10);
Nixie_SetBuf(3,11);
Nixie_SetBuf(4,Sec/10);
Nixie_SetBuf(5,Sec%10);
Nixie_SetBuf(6,11);
Nixie_SetBuf(7,MiniSec/10);
Nixie_SetBuf(8,MiniSec%10);
}
}
/**
* @brief 秒表驱动函数,在中断中调用
* @param 无
* @retval 无
*/
void Sec_Loop(void)
{
if(RunFlag)
{
MiniSec++;
if(MiniSec>=100)
{
MiniSec=0;
Sec++;
if(Sec>=60)
{
Sec=0;
Min++;
if(Min>=60)
{
Min=0;
}
}
}
}
}
void Timer0_Routine() interrupt 1
{
static unsigned int T0Count1,T0Count2,T0Count3;
TL0 = 0x18; //设置定时初值
TH0 = 0xFC; //设置定时初值
T0Count1++;
if(T0Count1>=20) //20是因为抖动大概20ms
{
T0Count1=0;
Key_Loop(); //20ms调用一次按键驱动函数
}
T0Count2++;
if(T0Count2>2) //给的越大,闪的越明显
{
T0Count2=0;
Nixie_Loop();//2ms调用一次数码管驱动函数
}
T0Count3++;
if(T0Count3>=10) //1s等于1000ms
{
T0Count3=0;
Sec_Loop(); //10ms调用一次数秒表驱动函数
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
编辑 (opens new window)
上次更新: 2023/09/13, 12:29:52