文章目录[隐藏]
1 源码
不藏着掖着,直接上代码
链接: https://gitee.com/H0x9DEFA478/ic_mifare1-mfrc522.git.
2 操作对象是什么?
“废话,当然是IC卡了!” ,话可不能怎么说,我们得深入了解,至少的知道操作的IC卡到底是个什么东东,要操作具体内容是什么。
2.1 对象
2.1.1 使用硬件
2.1.1.1 RC522
图是网上随便找的图,这是我使用的模块,硬件电路已经接成SPI模式了
2.1.1.2 主控
主控是STM32F103C8T6,为什么是它,我手里的板子只有它是有排座引出引脚的。
2.1.1.3 IC卡
一张S50白卡。
2.1.2 电路
这玩意还需要电路?直接硬件SPI怼起来。
这是我的硬件连接
VCC----------3.3V--------3.3V
PA9----------------------RST
PA8----------------------IRQ (本例不使用,可不接)
PB14---------------------MISO (注意 不要接错)
PB15---------------------MOSI (注意 不要接错)
PB13---------------------SCK
PB12---------------------SDA(CS)
除了这些,还需要PA11,PA12用作USB功能,单片机用USB虚拟串口与电脑通信
2.1.3 对象说明
2.1.3.1 IC卡S50
M1卡是常见的IC卡类型, 而S50是M1卡的其中一种。
其内部可以存储1KB的数据。显而易见,我们就是要操作这1KB的数据
这是S50卡手册中对这1KB的描述,分为16个扇区,每个扇区4个块,每个块16字节。
可以发现,开头(图片底部的扇区)的扇区比较特殊,根据描述这个块出厂是被固化的,不可修改,用于存放UID之类的。
每个扇区尾部(第四个块)都有一个特殊的块,用来存放密钥A
、密钥B
和控制字
,控制字
控制着它所在块的访问条件,这个里面的内容可以不先深究,暂且只需要知道它的默认值是FF 07 80 69
就行,对于这个控制字,代表这这个扇区仅需要验证密钥A
就能随便访问整个扇区(读,写,增值减值等),其他控制字类型可以通过S50的说明文档找到。 剩下的就是数据块了,可以被我们自由使用的块。
具体怎么读写这些数据放到后面说。
2.1.3.2 MFRC522
用于与IC卡通信,就是一堆寄存器操作。这个玩意的通信方式有SPI,IIC,UART三种(我的代码给出了SPI和IIC的接口,但IIC的没有测试)
一翻RC522的手册,一看寄存器直接蒙了。这么那么多寄存器,我参考了一些网上的代码,发现都是同源的,操作基本都一致。我多次实验过后发现对于操作M1卡的初始化,只需要初始化一个寄存器就行,没错!就一个。这个后面会提到。
2.1.3.3 STM32F103C8T6
我觉得这个已经不需要说明
3 如何通讯
感觉也是废话,当然是用SPI通讯啊。不过看代码时要分清楚哪些是控制IC卡的通讯,哪些仅仅只是MCU与RC522之间的通讯。
3.1 射频接口
这里并不解释射频的底层原理(而且我也不会),但有一些基本的还是要知道的。
3.1.1 IC卡的能量来源
当然是从射频载波得到,频率为13.56MHz。为什么提到这个?MFRC522是可以控制载波是否开启的,也就是说我可以顺带控制IC卡是否有电。
3.1.2 MFRC522如何与IC卡通信
它们两个之间的通信也是通过载波进行,通过控制载波幅度来通信。(至于怎么控制的,我也不知道)。只需要知道这两者之间的通信是类似于串口的通讯方式,只不过物理层面是通过射频。
3.2 SPI接口
???
4 软件结构
谁不喜欢一层一层的结构呢?
软件分为Mifare1层
和MFRC522层
,Mifare1层
负责将用户操作转为与IC卡操作的通讯流,在通过MFRC522层
传到RC522上,再由RC522与IC卡通讯。
上层:Mifare1层
底层:MFRC522层
4.1 MFRC522层
此层负责将底层(SPI)与IC卡通讯连接起来,向底层调用SPI读写函数,向顶层提供IC卡读写函数。
具体代码见源码。下面为使用时需要包含的头文件。
#ifndef __MFRC522_H_
#define __MFRC522_H_
#include "H_Type.h"
#define vMFRC522_ProtocolType_ISO14443_A 1
#define vMFRC522_LL_Function_Type_SPI 1
#define vMFRC522_LL_Function_Type_IIC 2
#define vMFRC522_MFAuthent_AuthentType_A 1
#define vMFRC522_MFAuthent_AuthentType_B 2
//初始化时参数TxSequenceBuffer的长度
#define vMFRC522_TxSequenceBufferLength 19
//初始化时参数RxSequenceBuffer的长度
#define vMFRC522_RxSequenceBufferLength vMFRC522_TxSequenceBufferLength
typedef struct{
int Type;//底层传输类型
void (*Delay)(int);//延时调用
void (*Reset)();//复位调用
union{
void (*Transfer_SPI)(void*,void*,int,int);//传输序列 (发送序列,接收序列,长度,传输速度(0:高速 其他:低速))
struct{
int (*Write)(Hbyte,Hbyte*,int);//底层iic写操作 返回 0:成功 其他:失败 (7位器件地址,发送数据,发送数据长度)
int (*Read)(Hbyte,Hbyte*,int,Hbyte*,int);//底层iic读操作 返回 0:成功 其他:失败 (7位器件地址,发送数据,发送数据长度,接收数据,接收数据长度)
Hbyte deviceAddr;//器件7位地址
}Transfer_IIC;
}TransferSequence;
}MFRC522_LL_Function;
typedef struct _MFRC522{
int ProtocolType;
void* TxSequenceBuffer;//发送缓存
void* RxSequenceBuffer;//接收缓存
struct{
Hbyte (*ReadReg)(struct _MFRC522*,Hbyte);
void (*ReReadRegToArray)(struct _MFRC522*,Hbyte,Hbyte*,int);
void (*ReadRegArray)(struct _MFRC522*,Hbyte,Hbyte*,int);
void (*WriteReg)(struct _MFRC522*,Hbyte,Hbyte);
void (*ReWriteRegFromArray)(struct _MFRC522*,Hbyte,Hbyte*,int);
void (*WriteRegArray)(struct _MFRC522*,Hbyte,Hbyte*,int);
}StaticFunction;
int LL_Speed;
MFRC522_LL_Function LL_Function;
int MFCrypto1On;
Hbyte Version;
}MFRC522;
//操作返回结果
#define vMFRC522_Result_RecvTimeOut 4 //读取超时
#define vMFRC522_Result_RecvTooLong 3 //接收数据过长
#define vMFRC522_Result_NoCard 2 //无响应 未发现卡
#define vMFRC522_Result_ParamError 1 //参数错误
#define vMFRC522_Result_Ok 0 //操作无错误
#define vMFRC522_Result_Error -1 //出现错误 此情况下IC通信已经无法继续下去且无法恢复,需要复位后再次尝试通信
//============================================================================================================================================
//
// 提供给外部的方法
//
//============================================================================================================================================
/**
* @brief 初始化RC522
* @param _this 未使用的句柄
* @param LL_Function 底层相关回调与信息
* @param TxSequenceBuffer 底层发送缓存 长度vMFRC522_TxSequenceBufferLength字节
* @param RxSequenceBuffer 底层接收缓存 长度vMFRC522_RxSequenceBufferLength字节
* @param Type 协议类型 vMFRC522_ProtocolType_ISO14443_A可选
* @return 返回执行结果
*/
int MFRC522_Init(MFRC522* _this,MFRC522_LL_Function* LL_Function,void* TxSequenceBuffer,void* RxSequenceBuffer,int ProtocolType);
/**
* @brief 复位RC522 在通信出现错误时可以使用 复位后开启载波
* @param _this RC522句柄
* @return 无
*/
void MFRC522_Reset(MFRC522* _this);
/**
* @brief 向卡发送数据
* @param _this RC522句柄
* @param TxData 发送的数据指针
* @param TxBitLength 发送的长度 单位Bit 低位在前
* @param WaitTime 最大等待时间
* @return 返回执行结果
*/
int MFRC522_Transmit(MFRC522* _this,Hbyte* TxData,Hbyte TxBitLength,int WaitTime);
/**
* @brief 接收卡发来的数据
* @param _this RC522句柄
* @param RxData 容纳接收数据的空间指针
* @param MaxRxBitLengthPtr 最大接收长度 单位Bit
* @param RxBitLengthPtr 用于返回接收到的长度 单位Bit 低位在前
* @param WaitTime 最大等待时间
* @return 返回执行结果
*/
int MFRC522_Receive(MFRC522* _this,Hbyte* RxData,Hbyte MaxRxBitLengthPtr,Hbyte* RxBitLengthPtr,int WaitTime);
/**
* @brief 发送后接收数据
* @param _this RC522句柄
* @param TxData 发送的数据指针 *这个空间可与RxData为同一个
* @param TxBitLength 发送的长度 单位Bit 低位在前
* @param RxData 容纳接收数据的空间指针 *这个空间可与RxData为同一个
* @param MaxRxBitLengthPtr 最大接收长度 单位Bit
* @param RxBitLengthPtr 用于返回接收到的长度 单位Bit 低位在前
* @param WaitTime 最大等待时间
* @return 返回执行结果
*/
int MFRC522_Transceive(MFRC522* _this,Hbyte* TxData,Hbyte TxBitLength,Hbyte* RxData,Hbyte MaxRxBitLengthPtr,Hbyte* RxBitLengthPtr,int WaitTime);
/**
* @brief 进行三轮认证
* @param _this RC522句柄
* @param SerialNumber 卡ID
* @param BlockAddr 要验证的块地址
* @param AuthentType 要验证的密码类型 vMFRC522_MFAuthent_AuthentType_A与vMFRC522_MFAuthent_AuthentType_B可选
* @param Password 密码序列
* @return 返回执行结果
*/
int MFRC522_MFAuthent(MFRC522* _this,Hbyte* SerialNumber,Hbyte BlockAddr,int AuthentType,Hbyte* Password);
/**
* @brief 清除MFCrypto1On位 对一张卡处理完成后调用一次(清除加密状态)
* @param _this RC522句柄
* @return 无
*/
void MFRC522_ClearMFCrypto1On(MFRC522* _this);
//============================================================================================================================================
//
// 提供给外部的方法(不一定是顶层必要的方法)
//
//============================================================================================================================================
/**
* @brief 获取RC522版本号 可使用这个方法判定RC522是否存在
* @param _this RC522句柄
* @return 版本号
*/
Hbyte MFRC522_Version(MFRC522* _this);
/**
* @brief 验证RC522是否存在
* @param _this RC522句柄
* @return 返回执行结果
*/
int MFRC522_IsExist(MFRC522* _this);
/**
* @brief 设置RC522载波状态 可在空闲时关闭载波 节省能源消耗
* @param _this RC522句柄
* @param IsEn 是否使能载波
* @return 无
*/
void MFRC522_SetTxStatus(MFRC522* _this,int IsEn);
#endif //__MFRC522_H_
MFRC522_Init(MFRC522* _this,MFRC522_LL_Function* LL_Function,void* TxSequenceBuffer,void* RxSequenceBuffer,int ProtocolType)
中,会将句柄内部的一些函数指针指向对应的函数,以适应SPI或IIC方式,所有的底层调用通过LL_Function传入,这样即使有多个RC522需要控制,只需要创建多个句柄,分别在初始化的时候传入不同的LL_Function就行了。
//初始化寄存器
static void RegInit(MFRC522* _this){
//ISO14443A
if(_this->ProtocolType==vMFRC522_ProtocolType_ISO14443_A){
RegModify(_this,vMFRC522_RFCfgReg,vMFRC522_RFCfgReg_RxGain_Msk,0x7U<<vMFRC522_RFCfgReg_RxGain_Pos);//接收48dB
//发送接收之间冷却延时设置
RegModify(_this,vMFRC522_RxSelReg,vMFRC522_RxSelReg_RxWait_Msk,0x01U<<vMFRC522_RxSelReg_RxWait_Pos);
//调制发送信号为100%ASK
_this->StaticFunction.WriteReg(_this,vMFRC522_TxASKReg,vMFRC522_TxASKReg_Force100ASK);
}
}
在初始化方法中,void RegInit(MFRC522* _this)
被调用。里面负责寄存器初始化。可以发现,里面有三个寄存器被修改。但只有一个寄存器是必须的,其他是可选的(一些无关紧要的设置),必须的寄存器是vMFRC522_TxASKReg
,要将它的bit6置为1。
这个底层文件提供了直接(对于高层来说)与IC卡通讯的函数,还要一个三轮认证密码的数
int MFRC522_MFAuthent(MFRC522* _this,Hbyte* SerialNumber,Hbyte BlockAddr,int AuthentType,Hbyte* Password)
,因为NXP官方并不公开三轮认证的加密算法(不然只需要与IC卡通讯的方法就足够了)。NXP只给射频芯片上加了个密码验证的功能,由RC522来完成认证过程,所以提供了这个函数来使用这个功能(不然这个功能应该是属于上层的)。认证完毕后,通信的数据也不需要我们自己加密,而是由RC522加密发送的数据,解密接收的数据。
4.2 Mifare1层
此层负责将用户想对卡的操作转换成通讯流通过底层的MFRC522层
传送到IC卡上,完成操作。
具体代码见源码。下面为使用时需要包含的头文件。
#ifndef __IC_Mifare1_H_
#define __IC_Mifare1_H_
#include "H_Type.h"
typedef struct{
Hbyte* SerialNumber;
int SerialNumberLength;
Hbyte SAK;
Huint16 ATQA;
}IC_Mifare1_CommunicationCallback_Param;
typedef struct{
void* v;//底层句柄
//必须实现
int (*Reset)(void*);//复位底层
int (*Transmit)(void*,Hbyte*,Hbyte,int);//发送数据 (底层句柄,发送数据,发送位长度,最大等待时间)
int (*Receive)(void*,Hbyte*,Hbyte,Hbyte*,int);//接收数据 (底层句柄,接收数据,最大接收位长度,接收位长度,最大等待时间)
int (*Transceive)(void*,Hbyte*,Hbyte,Hbyte*,Hbyte,Hbyte*,int);//发送接收数据 (底层句柄,发送数据,发送位长度,接收数据,最大接收位长度,接收位长度,最大等待时间)
int (*MFAuthent)(void*,Hbyte*,Hbyte,int,Hbyte*);//三轮认证 密钥认证 (底层句柄,卡ID,块地址,密钥)
//可选实现(如果不需要 固定返回vIC_Mifare1_Result_Ok)
int (*CallBeforeProbeCard)(void*);//在卡检测之前被调用
int (*CallAfterHaltCard)(void*);//在卡操作完成之后被调用
int (*BspIsValid)(void*);//底层有效性检查 返回vIC_Mifare1_Result_Ok表明底层硬件有效 否则底层硬件无效
}IC_Mifare1_LL_Function;
typedef struct _IC_Mifare1{
int Status;//状态
IC_Mifare1_CommunicationCallback_Param* Param;//只有在CommunicationCallback中有效
IC_Mifare1_LL_Function LL_Function;//底层方法
}IC_Mifare1;
#define vIC_Mifare1_Result_NoCard 1 //无响应 未发现卡
#define vIC_Mifare1_Result_Ok 0 //操作无错误
#define vIC_Mifare1_Result_Error -1 //出现错误 此情况下IC通信已经无法继续下去且无法恢复,需要复位后再次尝试通信
#define vIC_Mifare1_Result_StatusError -2 //状态错误 调用了在某些状态下不能调用的方法
#define vIC_Mifare1_Status_Select 1 //卡选中状态
#define vIC_Mifare1_Status_Ready 0 //卡已准备好识别
#define vIC_Mifare1_Status_CommunicationError -1 //通信错误
#define vIC_Mifare1_Status_BspInvaild -2 //底层无效
#define vIC_Mifare1_AuthentType_A 1
#define vIC_Mifare1_AuthentType_B 2
#define vIC_Mifare1_ValueBlockGetValue_Value_Msk 0x0FU
#define vIC_Mifare1_ValueBlockGetValue_Value_Ok 0x00U //数值完全正确
#define vIC_Mifare1_ValueBlockGetValue_Value_PartOk 0x01U //数值部分正确, 能解析出数值
#define vIC_Mifare1_ValueBlockGetValue_Value_Error 0x02U //数值错误过多, 没有解析出数值
#define vIC_Mifare1_ValueBlockGetValue_Addr_Msk 0xF0U
#define vIC_Mifare1_ValueBlockGetValue_Addr_Ok 0x00U //地址完全正确
#define vIC_Mifare1_ValueBlockGetValue_Addr_PartOk 0x10U //地址部分正确, 能解析出地址
#define vIC_Mifare1_ValueBlockGetValue_Addr_Error 0x20U //地址错误过多, 没有解析出地址
//============================================================================================================================================
//
// 提供给外部的方法
//
//============================================================================================================================================
/**
* @brief 初始化IC_Mifare1控制器
* @param _this 未使用的句柄
* @param LL_Function 底层方法集合
* @return 无
*/
void IC_Mifare1_Init(IC_Mifare1* _this,IC_Mifare1_LL_Function* LL_Function);
/**
* @brief 尝试进行一次卡操作 如果寻卡成功CommunicationCallback被调用 此后卡被halt
* @param _this IC_Mifare1句柄
* @param CommunicationCallback IC_Mifare1句柄
* @param IsAll 0:仅选中空闲的卡 其他:所有卡都在选中范围
* @return 0: CommunicationCallback成功被调用 其他:CommunicationCallback没有被调用
*/
int IC_Mifare1_Communication(IC_Mifare1* _this,void (*CommunicationCallback)(IC_Mifare1*,IC_Mifare1_CommunicationCallback_Param*),int IsAll);
//============================================================================================================================================
//
// 提供给外部的方法 在IC_Mifare1_Communication()传入的CommunicationCallback()中调用
//
//============================================================================================================================================
/**
* @brief 密钥验证
* @param _this IC_Mifare1句柄
* @param BlockAddr 要验证的块地址
* @param AuthentType 密码类型 要验证的密码类型 vIC_Mifare1_AuthentType_A与vIC_Mifare1_AuthentType_B可选
* @param Password 密钥
* @return 返回执行结果
*/
int IC_Mifare1_Authent(IC_Mifare1* _this,Hbyte BlockAddr,int AuthentType,Hbyte* Password);
/**
* @brief 读一个块
* @param _this IC_Mifare1句柄
* @param BlockAddr 块地址
* @param Dst 指向用于存放数据的内存 大小16字节
* @return 返回执行结果
*/
int IC_Mifare1_ReadBlock(IC_Mifare1* _this,Hbyte BlockAddr,Hbyte* Dst);
/**
* @brief 写一个块
* @param _this IC_Mifare1句柄
* @param BlockAddr 块地址
* @param SrcData 要写入的数据 大小16字节
* @return 返回执行结果
*/
int IC_Mifare1_WriteBlock(IC_Mifare1* _this,Hbyte BlockAddr,Hbyte* SrcData);
/**
* @brief 数值块操作 数值块内的值 增加/减小/不加不减 到IC卡的数值缓冲区
* @param _this IC_Mifare1句柄
* @param BlockAddr IC_Mifare1句柄
* @param IsDecrement 如果dValue不为0, 则该段有效: 0:加值 其他:减值
* @param dValue 变化的值 如果为0 则使用Restore指令, 值不做变化的转到IC卡的数值缓冲区
* @return 返回执行结果
*/
int IC_Mifare1_ValueOperation(IC_Mifare1* _this,Hbyte BlockAddr,int IsDecrement,Huint32 dValue);
/**
* @brief 数值块操作 将IC卡的数值缓冲区的数值存储到块中
* @param _this IC_Mifare1句柄
* @param BlockAddr 要存储到的块地址
* @return 返回执行结果
*/
int IC_Mifare1_ValueTransfer(IC_Mifare1* _this,Hbyte BlockAddr);
//============================================================================================================================================
//
// 附加给外部的方法 无调用条件 随便调用
//
//============================================================================================================================================
/**
* @brief 生成数组块数据
* @param BlockData 生成的数据存放位置
* @param BlockAddr 块地址
* @param Value 数值
* @return 无
*/
void IC_Mifare1_MakeValueBlock(Hbyte* BlockData,Hbyte BlockAddr,Huint32 Value);
/**
* @brief 简单解析并获取块的数值和地址域的地址 (有更好的恢复方法(只是恢复概率更高) 但这个函数没有使用 其实也没什么必要)
* @param BlockData 块数据
* @param BlockAddr 用于返回地址域的地址
* @param Value 用于返回数值
* @return 结果有效性
*/
Hbyte IC_Mifare1_ValueBlockGetValue(Hbyte* BlockData,Hbyte* BlockAddr,Huint32* Value);
#endif //__IC_Mifare1_H_
执行void IC_Mifare1_Init(IC_Mifare1* _this,IC_Mifare1_LL_Function* LL_Function)
时,LL_Function包含对MFRC522层
的操作函数指针。
Mifare1层
封装了对M1卡的操作,因为M1卡都是从寻卡开始,到Halt卡结束。所以这些过程被封装起来了,出了个
int IC_Mifare1_Communication(IC_Mifare1* _this,void (*CommunicationCallback)(IC_Mifare1*,IC_Mifare1_CommunicationCallback_Param*),int IsAll)
函数,寻卡操作与Halt卡操作自动完成(其中Halt操作要在所有操作都成功的情况下会被使用,如果有操作失败,则不会被使用)。如果上次的调用这个函数时出现通讯错误,则本次调用时内部会首先复位底层的RC522,然后才开始寻卡操作。如果切顺利,在自动选中卡之后,传入该函数的
void (*CommunicationCallback)(IC_Mifare1*,IC_Mifare1_CommunicationCallback_Param*)
被调用。这是用户传入的回调,在里面用户可以按照自己的需要调用验证密码,读、写、加值减值等操作。如果这个函数没有被调用(例如没有找到卡), 这个方法就返回非0值。
因为通信格式相同,
Increment
加值 \Decrement
减值 \Restore
不变, 这三个操作被int IC_Mifare1_ValueOperation(IC_Mifare1* _this,Hbyte BlockAddr,int IsDecrement,Huint32 dValue)
这个函数包揽了。根据值是否为0,是否减少 来判定使用那条指令。这个函数调用成功后,被操作的值存在IC卡的缓冲区中(IC卡块里的数据并没有变化)。如果要保存操作后的数据(在IC卡的缓存里),需要调用int IC_Mifare1_ValueTransfer(IC_Mifare1* _this,Hbyte BlockAddr);
来实现保存。该保存函数的成功意味着数据成功保存(啥,这都能失败那就是卡的问题了)。
*IC_Mifare1_ReadBlock()
和IC_Mifare1_WriteBlock()
直接读取/写入块,没有缓冲区的事。
5 源码实现
STM32作为虚拟串口,接收主机的串口助手的指令。默认上电后不进行任何卡操作(仅初始化RC522)。
5.1 串口助手发送的指令
通过串口助手发送字符串进行操作,源码使用UTF8编码,串口助手要注意要设置为正确的编码。
单字符指令(注意大小写)
字符串: i
获取一些运行信息
字符串: v
获取MFRC522版本寄存器的值
字符串: I
(i的大写)发送该指令后,进入初始化状态,进入范围的白卡都会被初始化,扇区1写入一些数据,并设置密钥
字符串: D
发送该指令后,进入反初始化状态,进入范围的卡都会被反初始化,密钥恢复为FFFFFFFFFFFF
(只有指令I初始化的卡才能被反初始化)
字符串: R
发送该指令后,进入读取状态,可以读取初始化好的卡的内部数据
多字符指令(注意大小写)
字符串: Cxxx
该命令以C开头,xxx
为10进制有符号整数,长度不限。对数值块进行加,减操作。成功操作后,自动进入读取状态。
例如: C-100
值减100
例如: C50
值加50
串口助手发送C命令不要选上发送回车(后果我也不知道)
5.2 IC块的使用
源码只使用了IC卡的第二个扇区,这个扇区的所有块都被使用了。该扇区通过I
指令
5.2.1 作用布局
块0: 存放用户数据(存了一个字符串)
块1: 数值块(备份)
块2: 数值块
块3:控制块(修改密码)
数值块通过写块操作生成,只需要按照特定格式写入。
数值为整型数,位宽4字节,在块中小端排列,重复拍3次,其中第二次位取反。最后4个字节存放地址,存放备份的地址,但实际上这个地址似乎可以随便选。
5.3 相关操作
5.3.1 初始化卡
5.3.2 尝试重复初始化
疯狂恢复操作失败,因为卡已经初始化了,密码已修改(如果操作失败,RC522重新初始化 IC卡被重新上点,导致重复检测)
5.3.3 读取卡
5.3.4 减值
5.3.5 加值
5.3.6 将卡反初为白卡(恢复默认密码)
版权声明:本文为CSDN博主「0x9DEFA478」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_42907191/article/details/120744485
暂无评论