树莓派复制MFRC522 门禁IC卡(支持block0写入,亲测可用)
1. 基本原理
友情提示:本篇文章只用于学习交流使用,请不要用于其它用途
我们总共需要三个组件:树莓派,MFRC522模块,IC卡
树莓派与MFRC522模块的交互通过SPI方式:树莓派首先通过"sudo raspi-config"命令打开SPI口,然后根据协议向模块特定寄存器读写数据,以此完成数据收发操作
MFRC522模块与IC卡是通过电磁感应无接触交互的:MFRC522模块内部会产生感应电磁场,IC卡内部线圈通过电磁感应获取电能,然后解析数据,最终完成IC卡内部存储器的数据读写操作
整个系统流程就是首先树莓派通过SPI接口向MFRC522模块发送数据和命令,MFRC522模块收到数据和命令后,产生电磁场,然后IC卡通过内部感应电圈获得电能和数据,解析数据后读写存储器,最终完成IC卡数据的读写
参考资料:https://download.csdn.net/download/chenwh_cn/12746663,附件中包含了IC卡调试工具,以及IC卡和MFRC522模块的数据手册,介绍了基本组成原理和内部存储器的功能,以及各种命令交互的数据协议格式,
2. 代码实现
a. SPI 依赖库的安装
下载源代码 https://github.com/lthiery/SPI-Py.git
然后进入SPI-Py目录执行:sudo python setup.py install
该库提供了3个API接口函数用以实现SPI通信:
- openSPI,用于打开SPI口,同时可以设置mode, speed, bits, delay等
- transfer,用于收发数据
- closeSPI,用于关闭SPI口
b. 驱动接口实现 MFRC522.py
"__"前缀为私有函数,"MFRC522_"前缀为公有函数,开头注释描述了树莓派与MFRC522模块的连线方式
# RC522-pin Pi-pin
# 3.3V 1(3.3V)
# RST 22(GPIO25)
# GND 6(GND)
# IRQ none
# MISO 21(GPIO9)
# MOSI 19(GPIO10)
# SCK 23(GPIO11)
# SDA 24(GPIO8)import RPi.GPIO as GPIO
import spi
import signal
import timeclass MFRC522:PCD_IDLE = 0x00 # cancel current commandPCD_AUTHENT = 0x0E # authentPCD_RECEIVE = 0x08 # receive dataPCD_TRANSMIT = 0x04 # send dataPCD_TRANSCEIVE = 0x0C # send & receive dataPCD_RESETPHASE = 0x0F # resetPCD_CALCCRC = 0x03 # CRC calculatePICC_REQIDL = 0x26 # detect cards, not sleppPICC_REQALL = 0x52 # detect cards, all PICC_ANTICOLL = 0x93 # anti collisionPICC_SElECTTAG = 0x93 # select cardPICC_AUTHENT1A = 0x60 # authent APICC_AUTHENT1B = 0x61 # authent BPICC_READ = 0x30 # read blockPICC_WRITE = 0xA0 # write blockPICC_DECREMENT = 0xC0 # dec moneyPICC_INCREMENT = 0xC1 # inc moneyPICC_RESTORE = 0xC2 # send data to bufPICC_TRANSFER = 0xB0 # save date from bufPICC_HALT = 0x50 # idle statusReserved00 = 0x00 # registerCommandReg = 0x01CommIEnReg = 0x02DivlEnReg = 0x03CommIrqReg = 0x04DivIrqReg = 0x05ErrorReg = 0x06Status1Reg = 0x07Status2Reg = 0x08FIFODataReg = 0x09FIFOLevelReg = 0x0AWaterLevelReg = 0x0BControlReg = 0x0CBitFramingReg = 0x0DCollReg = 0x0EReserved01 = 0x0FReserved10 = 0x10ModeReg = 0x11TxModeReg = 0x12RxModeReg = 0x13TxControlReg = 0x14TxAutoReg = 0x15TxSelReg = 0x16RxSelReg = 0x17RxThresholdReg = 0x18DemodReg = 0x19Reserved11 = 0x1AReserved12 = 0x1BMifareReg = 0x1CReserved13 = 0x1DReserved14 = 0x1ESerialSpeedReg = 0x1FReserved20 = 0x20CRCResultRegM = 0x21CRCResultRegL = 0x22Reserved21 = 0x23ModWidthReg = 0x24Reserved22 = 0x25RFCfgReg = 0x26GsNReg = 0x27CWGsPReg = 0x28ModGsPReg = 0x29TModeReg = 0x2ATPrescalerReg = 0x2BTReloadRegH = 0x2CTReloadRegL = 0x2DTCounterValueRegH = 0x2ETCounterValueRegL = 0x2FReserved30 = 0x30TestSel1Reg = 0x31TestSel2Reg = 0x32TestPinEnReg = 0x33TestPinValueReg = 0x34TestBusReg = 0x35AutoTestReg = 0x36VersionReg = 0x37AnalogTestReg = 0x38TestDAC1Reg = 0x39TestDAC2Reg = 0x3ATestADCReg = 0x3BReserved31 = 0x3CReserved32 = 0x3DReserved33 = 0x3EReserved34 = 0x3FNRSTPD = 22MAX_LEN = 18MI_OK = 0MI_NOTAGERR = 1MI_ERR = 2MI_TIMEOUT = 3def __init__(self, dev='/dev/spidev0.0', spd=1000000):self.dev0 = spi.openSPI(device=dev, speed=spd)GPIO.setmode(GPIO.BOARD)GPIO.setup(self.NRSTPD, GPIO.OUT)GPIO.output(self.NRSTPD, 1)self.MFRC522_Init()def __WriteReg(self, addr, val):spi.transfer(self.dev0, ((addr<<1)&0x7E, val))def __ReadReg(self, addr):val = spi.transfer(self.dev0, (((addr<<1)&0x7E) | 0x80, 0))return val[1]def __SetRegBitMask(self, reg, mask):tmp = self.__ReadReg(reg)self.__WriteReg(reg, tmp | mask)def __ClearRegBitMask(self, reg, mask):tmp = self.__ReadReg(reg)self.__WriteReg(reg, tmp & (~mask))def __ToCard(self, command, sendData):retStatus = self.MI_OKbackData = []backLen = 0irqEn = 0x00waitIRq = 0x00lastBits = Nonen = 0i = 0if command == self.PCD_AUTHENT:irqEn = 0x12waitIRq = 0x10if command == self.PCD_TRANSCEIVE:irqEn = 0x77waitIRq = 0x30self.__WriteReg(self.CommIEnReg, irqEn|0x80) # enable interuptself.__ClearRegBitMask(self.CommIrqReg, 0x80) # clear interupt flagsself.__SetRegBitMask(self.FIFOLevelReg, 0x80) # init FIFOself.__WriteReg(self.CommandReg, self.PCD_IDLE) # cancel current commandfor i in range(len(sendData)):self.__WriteReg(self.FIFODataReg, sendData[i])self.__WriteReg(self.CommandReg, command)if command == self.PCD_TRANSCEIVE:self.__SetRegBitMask(self.BitFramingReg, 0x80)i = 2000while True:n = self.__ReadReg(self.CommIrqReg)i -= 1if ~((i!=0) and ~(n&0x01) and ~(n&waitIRq)):breakself.__ClearRegBitMask(self.BitFramingReg, 0x80)if i != 0:if (self.__ReadReg(self.ErrorReg) & 0x1B) == 0x00:if n & irqEn & 0x01:retStatus = self.MI_NOTAGERRif command == self.PCD_TRANSCEIVE:n = self.__ReadReg(self.FIFOLevelReg)lastBits = self.__ReadReg(self.ControlReg) & 0x07if lastBits != 0:backLen = (n-1)*8 + lastBitselse:backLen = n*8if n == 0:n = 1if n > self.MAX_LEN:n = self.MAX_LENfor i in range(n):backData.append(self.__ReadReg(self.FIFODataReg))else:retStatus = self.MI_ERRelse:retStatus = self.MI_TIMEOUTreturn (retStatus, backData, backLen)def __CalulateCRC(self, indata):self.__ClearRegBitMask(self.DivIrqReg, 0x04)self.__SetRegBitMask(self.FIFOLevelReg, 0x80)for i in range(len(indata)):self.__WriteReg(self.FIFODataReg, indata[i])self.__WriteReg(self.CommandReg, self.PCD_CALCCRC)i = 255while True:n = self.__ReadReg(self.DivIrqReg)i -= 1if ~((i != 0) and ~(n&0x04)):breakcrc = []crc.append(self.__ReadReg(self.CRCResultRegL))crc.append(self.__ReadReg(self.CRCResultRegM))return crcdef MFRC522_Init(self):self.MFRC522_Reset()self.__WriteReg(self.TModeReg, 0x8D)self.__WriteReg(self.TPrescalerReg, 0x3E)self.__WriteReg(self.TReloadRegL, 30)self.__WriteReg(self.TReloadRegH, 0)self.__WriteReg(self.TxAutoReg, 0x40)self.__WriteReg(self.ModeReg, 0x3D)self.MFRC522_AntennaOn()def MFRC522_Reset(self):# reg: 0x01# buf: 0x0Fself.__WriteReg(self.CommandReg, self.PCD_RESETPHASE)def MFRC522_AntennaOn(self):# reg: 0x14# buf: 0bxxxxxx11temp = self.__ReadReg(self.TxControlReg)if not(temp & 0x03):self.__SetRegBitMask(self.TxControlReg, 0x03)def MFRC522_AntennaOff(self):# reg: 0x14# buf: 0bxxxxxx00self.__ClearRegBitMask(self.TxControlReg, 0x03)# function : read block# parameter : blockAddr(0~63)# return : retStatus# backData[16]def MFRC522_ReadBolock(self, blockAddr):retStatus = self.MI_OK# cmd: 0x0c# buf: 0x30 blockAddr crc[2]buf = []buf.append(self.PICC_READ)buf.append(blockAddr)crc = self.__CalulateCRC(buf)buf.append(crc[0])buf.append(crc[1])(status, backData, backLen) = self.__ToCard(self.PCD_TRANSCEIVE, buf)if (status != self.MI_OK) or (backLen != 8*self.MAX_LEN):retStatus = self.MI_ERRreturn (retStatus, backData)# function : write block# parameter : blockAddr(0~63)# writeData[16]# return : retStatusdef MFRC522_WriteBlock(self, blockAddr, writeData):retStatus = self.MI_OK# cmd: 0x0c# buf: 0xA0 blockAddr crc[2]buf = []buf.append(self.PICC_WRITE)buf.append(blockAddr)crc = self.__CalulateCRC(buf)buf.append(crc[0])buf.append(crc[1])(status, backData, backLen) = self.__ToCard(self.PCD_TRANSCEIVE, buf)if (status != self.MI_OK) or ((backData[0]&0x0F) != 0x0A) or (backLen != 4):retStatus = self.MI_ERRif status == self.MI_OK:# cmd: 0x0c# buf: writeData[16] crc[2]buf2 = []for i in range(16):buf2.append(writeData[i])crc = self.__CalulateCRC(buf2)buf2.append(crc[0])buf2.append(crc[1])(status, backData, backLen) = self.__ToCard(self.PCD_TRANSCEIVE, buf2)if (status != self.MI_OK) or ((backData[0]&0x0F) != 0x0A) or (backLen != 4):retStatus = self.MI_ERRreturn retStatus# function : detect card# parameter : reqMode: detect mode# 0x52 = detect all cards # 0x26 = detect not sleep cards# return : retStatus# backData: card type(2 bytes)# 0x4400 = Mifare_UltraLight# 0x0400 = Mifare_One(S50)# 0x0200 = Mifare_One(S70)# 0x0800 = Mifare_Pro(X)# 0x4403 = Mifare_DESFiredef MFRC522_Request(self, reqMode): retStatus = self.MI_OKself.__WriteReg(self.BitFramingReg, 0x07)# cmd: 0x0c# buf: 0x26/0x52buf = []buf.append(reqMode)(status, backData, backLen) = self.__ToCard(self.PCD_TRANSCEIVE, buf)if (status != self.MI_OK) or (backLen != 0x10):retStatus = self.MI_ERRreturn (retStatus, backData)# function : anticoll# parameter :# return : retStatus# backData(Uid, 4 bytes)def MFRC522_Anticoll(self):retStatus = self.MI_OKserNumCheck = 0self.__WriteReg(self.BitFramingReg, 0x00)# cmd: 0x0c# buf: 0x93 0x20buf = []buf.append(self.PICC_ANTICOLL)buf.append(0x20)(status, backData, backLen) = self.__ToCard(self.PCD_TRANSCEIVE, buf)if (status==self.MI_OK) and (len(backData)==5):for i in range(4):serNumCheck ^= backData[i]if serNumCheck != backData[4]:retStatus = self.MI_ERRelse:retStatus = self.MI_ERRreturn (retStatus, backData)# function : select card# parameter : Uid# return : retStatusdef MFRC522_Select(self, Uid):retStatus = self.MI_OKserNumCheck = 0# cmd: 0x0c# buf: 0x93 0x70 Uid[4] check crc[2]buf = []buf.append(self.PICC_SElECTTAG)buf.append(0x70)for i in range(4):buf.append(Uid[i])serNumCheck ^= Uid[i]buf.append(serNumCheck)crc = self.__CalulateCRC(buf)buf.append(crc[0])buf.append(crc[1])(status, backData, backLen) = self.__ToCard(self.PCD_TRANSCEIVE, buf)if (status != self.MI_OK) or (backLen != 0x18):retStatus = self.MI_ERRreturn retStatus# function : auth sectorkey# parameter : authMode(PICC_AUTHENT1A/PICC_AUTHENT1B)# blockAddr# sectorkey(6 bytes)# Uid(4 bytes)# return : retStatusdef MFRC522_Auth(self, authMode, blockAddr, sectorkey, Uid):retStatus = self.MI_OK# cmd: 0x0e# buf: authMode blockAddr sectorkey[6] Uid[4]buf = []buf.append(authMode)buf.append(blockAddr)for i in range(6):buf.append(sectorkey[i])for i in range(4):buf.append(Uid[i])(status, backData, backLen) = self.__ToCard(self.PCD_AUTHENT, buf)if (status != self.MI_OK) or not(self.__ReadReg(self.Status2Reg)&0x08):retStatus = self.MI_ERRreturn retStatus# function : idle# parameter :# return : retStatusdef MFRC522_Halt(self):retStatus = self.MI_OK# cmd: 0x0c# buf: 0x50 0x00 crc[2]buf = []buf.append(self.PICC_HALT)buf.append(0)crc = self.__CalulateCRC(buf)buf.append(crc[0])buf.append(crc[1])(status, backData, backLen) = self.__ToCard(self.PCD_TRANSCEIVE, buf)retStatus = statusreturn retStatusdef MFRC522_WriteCmd40(self):retStatus = self.MI_OKself.__WriteReg(self.BitFramingReg, 0x07)# cmd: 0x0c# buf: 0x40buf = []buf.append(0x40)(status, backData, backLen) = self.__ToCard(self.PCD_TRANSCEIVE, buf)if (status != self.MI_OK) or (backData[0] != 0x0a):retStatus = statusreturn retStatusdef MFRC522_WriteCmd43(self):retStatus = self.MI_OKself.__WriteReg(self.BitFramingReg, 0x00)# cmd: 0x0c# buf: 0x43buf = []buf.append(0x43)(status, backData, backLen) = self.__ToCard(self.PCD_TRANSCEIVE, buf)if (status != self.MI_OK) or (backData[0] != 0x0a):retStatus = statusreturn retStatusdef MFRC522_StopCrypto1(self):self.__ClearRegBitMask(self.Status2Reg, 0x08)def MFRC522_CloseSPI(self):spi.closeSPI(self.dev0)
c. 复制IC卡数据实现 main.py
通过request, anticoll, select, auth, halt 等命令,首先从源卡中读取block数据,然后将数据写入到新卡对应的block中
注意:block0的数据写入与其它block的写入不同,还需要写入两个特殊的命令0x40和0x43, 参考:https://blog.csdn.net/baidu_34570497/article/details/79689778
#!/usr/bin/env python
# -*- coding: utf8 -*-import MFRC522rc = MFRC522.MFRC522()
# save src card data, then write the saved date to des card
dataBlock0 = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15]
sectorkey = [0xff, 0xff, 0xff, 0xff, 0xff, 0xff]def read_block0():(status, tagType) = rc.MFRC522_Request(rc.PICC_REQIDL)if status != rc.MI_OK:print("MFRC522_Request error")returnprint("MFRC522_Request success, tagType: %#x %#x" %(tagType[0], tagType[1]))(status, Uid) = rc.MFRC522_Anticoll()if status != rc.MI_OK:print("MFRC522_Anticoll error")returnprint("MFRC522_Anticoll success, Uid: %#x %#x %#x %#x" %(Uid[0], Uid[1], Uid[2], Uid[3]))status = rc.MFRC522_Select(Uid)if status != rc.MI_OK:print("MFRC522_Select error")returnprint("MFRC522_Select success")status = rc.MFRC522_Auth(rc.PICC_AUTHENT1A, 1, sectorkey, Uid)if status != rc.MI_OK:print("MFRC522_Auth error")returnprint("MFRC522_Auth success")(status, dataBlock0) = rc.MFRC522_ReadBolock(0)if status != rc.MI_OK:print("MFRC522_ReadBolock error")returnprint("MFRC522_ReadBolock success, block0: %#x %#x %#x %#x %#x %#x %#x %#x %#x %#x %#x %#x %#x %#x %#x %#x" %(dataBlock0[0], dataBlock0[1], dataBlock0[2], dataBlock0[3], dataBlock0[4], dataBlock0[5], dataBlock0[6], dataBlock0[7], dataBlock0[8], dataBlock0[9], dataBlock0[10], dataBlock0[11], dataBlock0[12], dataBlock0[13], dataBlock0[14], dataBlock0[15]))rc.MFRC522_StopCrypto1()def write_block0():(status, tagType) = rc.MFRC522_Request(rc.PICC_REQIDL)if status != rc.MI_OK:print("MFRC522_Request error")returnprint("MFRC522_Request success, tagType: %#x %#x" %(tagType[0], tagType[1]))(status, Uid) = rc.MFRC522_Anticoll()if status != rc.MI_OK:print("MFRC522_Anticoll error")returnprint("MFRC522_Anticoll success, Uid: %#x %#x %#x %#x" %(Uid[0], Uid[1], Uid[2], Uid[3]))status = rc.MFRC522_Select(Uid)if status != rc.MI_OK:print("MFRC522_Select error")returnprint("MFRC522_Select success")status = rc.MFRC522_Halt()print("MFRC522_Halt %d" %status)status = rc.MFRC522_WriteCmd40()if (status != rc.MI_OK):print("MFRC522_ToCard 0x40 error, status:%d" %status)returnprint("MFRC522_ToCard 0x40 success")status = rc.MFRC522_WriteCmd43()if (status != rc.MI_OK):print("MFRC522_ToCard 0x43 error, status:%d" %status)returnprint("MFRC522_ToCard 0x43 success")status = rc.MFRC522_WriteBlock(0, dataBlock0)if status != rc.MI_OK:print("MFRC522_WriteBlock error")returnprint("MFRC522_WriteBlock success")rc.MFRC522_StopCrypto1() def closeSPI():rc.MFRC522_CloseSPI()if __name__ == '__main__':while True:action = input("Enter action r/w: ")if action == 'r':read_block0()elif action == 'w':write_block0()elif action == 'q':closeSPI()breakprint("exit procedure")
d. 运行结果
- 将原卡放到模块的感应区域,然后输入“r”,程序读取原卡block0数据
- 将新卡放到模块的感应区域,然后输入“w”,程序将读出的block0数据写入到新卡的block0
- 测试新卡门禁是否可用
注意:
调试过程中很可能会出现0x40和0x43写入失败的问题,定位发现是 MFRC522_Halt() 命令执行失败,排除掉其它因素后极有可能是使用的新卡不支持block0写入的,可以换个卡再试试
通常普通门禁只是读取IC卡的Uid进行校验,即block0的前4个字节,如果block0成功写入还是不能打开门禁,可能就是门禁系统还使用了其它数据进行校验,这时可以将原卡的所有block数据都读出来然后写入新卡
树莓派复制MFRC522 门禁IC卡(支持block0写入,亲测可用)相关推荐
- Proxmark3教程1:小白如何用PM3破解复制M1全加密门禁IC卡
IC卡已经在我们的生活中无处不在了,门禁,电梯,吃饭,洗车,可以说与我们的生活息息相关了. 但是如果有一天,你的门禁卡丢了,怎么配呢?跟配钥匙一样的,必须现有原钥匙才可以. 那我们今天就看看,如何用P ...
- 【毕业设计】 树莓派寝室宿舍门禁刷卡系统 - 物联网 单片机 嵌入式
文章目录 0 前言 1 前言 2 主要器件 3 实物效果 4 树莓派读取 RC522 RFID 标签 5 mg90s 控制原理 6 最后 0 前言
- 华为手机怎样复制加密门禁卡_手机NFC复制小区用的门禁卡
我们说一说目前的门禁卡到底有哪些类型呢?门禁卡一般分为:ID卡和IC卡.而EM卡.M1卡.CPU卡等等,都是这两种卡的细分.它们能够开门,都是基于RFID感应的原理来实现开门的. 我们看一张图,在这 ...
- 小区门禁卡可以复制到手机上吗_如何用手机复制小区门禁卡,一招就可以解决!...
首先要确认你手机有没有NFC功能 NFC,即近场通讯,是一种非接触式通讯技术,相比蓝牙等方式,连接速度快,不需要电源.NFC有三种工作模式,有卡模式.点对点模式.读卡器模式.NFC在门禁.公交.手机支 ...
- 华为复制加密门禁卡_MIUI12轻体验:关于模拟门禁卡,你想知道的都在这里
当MIUI12正式发布的时候,点燃了亓纪想要体验新系统的决心.刚好手边有一台小米9,于是便申请了内测权限,体验到了全新的MIUI12.今天想和大家分享的是升级后感知最强的功能:模拟加密卡. 原因 作为 ...
- 为什么复制的门禁卡只能用一次_手机NFC为什么可以复制小区用的门禁卡?
展开全部 NFC,即近场通讯,是一种非接触式通讯技术,相比62616964757a686964616fe4b893e5b19e31333431363534蓝牙等方式,连接速度快,不需要电源.NFC有三 ...
- showmodaldialog 为什么不能复制_防复制的门禁读头可以防止UID和FUID读卡器
目前,有很多智慧小区门禁采用防复制门禁读头可以防止UID和FUID门禁读卡器-防复制的门禁读头哪家好?防复制的门禁读头可以防止UID和FUID读卡器 1.防复制的门禁读头可以防止那些复制的卡? 回答: ...
- python人脸识别门禁系统毕设_树莓派人脸识别门禁系统代码以及代码分析——opencv拍照调用FACE++处理...
树莓派人脸识别门禁系统的总体设计 硬件包括:树莓派3B.电源模块.转5V降压模块.触摸感应传感器模块.声光报警模块和驱动模块. 由此设计出的人脸识别门禁系统总体结构框图如下: 总体结构框图 程序运行时 ...
- 人脸识别门禁_小区人脸识别门禁或取代传统门禁刷卡方式
可以说,社区是一个人口聚集的小型社会.首先社区的人流量很大,进出是十分频繁.其次,社区的大门以及各处进出口是业主与访客.外来人员等进进出出最关键的通道.因此,每个社区都会有一套成型的门禁系统,常见的是 ...
最新文章
- Jrebel 热部署插件的使用和破解
- easyui datagrid 表格动态隐藏部分列的展示
- 百度程序员哀叹:回老家发现村里很多人开保时捷,道奇等豪车,比程序员有钱多了!...
- 京东方拟收购法国零售物联网领域公司50.1%以上股份
- 数据挖掘之人工神经网络BP算法
- python内置方法怎么使用_python中的常用内置方法
- Netty工作笔记0013---Channel应用案例4Copy图片
- android SDK安装以及环境变量配置
- 析构函数无法命中断点. 当前无法命中断点: 没有与此行关联的调试程序的目标代码类型的可执行代码. 可能的原因包括;条件编译, 编译器优化或者......
- Matlab箱线图Boxplot横坐标x轴设置
- Python策略模式实现源码分享
- 这项技术曾应用于无人驾驶,荣耀10将其移植到手机上这样操作!
- 剑指Offer题目汇总(持续更新中...)
- 2022-2027年中国沙漠旅游行业市场全景评估及发展战略规划报告
- WinCap数据包显示
- 基于C++的钻石金字塔问题算法设计
- [论文阅读] EIE: Efficient Inference Engine on Compressed Neural Network
- 微信支付签名验证失败的问题
- 神经性脚臭案例整理(二)
- 解决[WinError 87] pip自动安装不成功问题