简介:STM32F103模拟IIC协议访问PCA9555 IO扩展芯片的完整工程源码与设计笔记,附带标准外设库与Keil工程配置,面向有基础嵌入式开发经验的读者。资源通过GPIO推挽输出模拟SCL/SDA时序,对照硬件手册讲解7位地址配置、读写寄存器操作及应答位处理,并给出HAL库函数调用与实测验证过程,可帮助快速移植到实际项目。压缩包共127个文件、约2MB,以32个c源文件和33个头文件为主体,含uvprojx工程文件、hex烧录文件和驱动代码,形成可直接编译调试的闭环。已有4837人下载学习,适合需要扩展GPIO数量或理解软件模拟IIC时序的STM32开发者参考。亲测有效,代码具备实际参考价值,可在此基础上根据寄存器映射灵活调整输入输出控制逻辑,并适配不同MCU平台复用。
1. 为什么用模拟IIC去驱动PCA9555:从一次GPIO不够用说起
做嵌入式项目最怕的一件事就是:板子都画好了,功能逻辑也调通了,结果临到量产前发现GPIO不够用。某次给一个基于STM32F103的设备加按键矩阵和LED指示,满打满算差4个引脚,换大封装芯片不现实,改板子代价又太高,最后盯上了板子上空闲的IIC总线——挂一颗PCA9555,用两个引脚换16个GPIO,这笔账怎么算都划算。
但真正动手才发现,STM32F103的硬件IIC外设在工程圈里口碑一直很微妙,尤其是和多路从设备混用、或者线长一点的时候,总线状态机卡死在BUSY标志上是家常便饭。很多人因此直接把硬件IIC扔到一边,改用GPIO模拟时序。这篇笔记记录的就是这样一套完整可跑的方案:GPIO模拟IIC主模式,访问PCA9555完成16路IO的输入输出控制,代码在F103上实测通过,直接复制引脚定义就能用。
这套代码不只解决“读PCA9555”这一个点,它把IIC协议里最关键的起始条件、停止条件、应答检测、多字节连续读写全部用GPIO翻转实现了。理解它之后,你去驱动AT24C02、MPU6050这些IIC设备,思路完全一致。适合正在做驱动移植、或者被硬件IIC折腾到头大的开发者,也适合想搞明白IIC时序到底怎么回事的新手。
2. 技术选型:为什么弃用硬件IIC,以及模拟IIC的整体架构
2.1 硬件IIC的坑:BUSY标志和总线锁死
STM32F103的硬件IIC模块在ST官方论坛和各大嵌入式社区里被讨论了很多年,问题集中在几个固定场景。第一是总线繁忙标志偶发置位,明明总线上没有设备在通信,I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY)却一直返回SET,主模式根本发不出起始信号。第二是出错后恢复困难,从设备NACK或者总线干扰导致时序错乱,硬件状态机可能停在某个中间态,必须彻底复位外设甚至重新初始化GPIO才能恢复。第三是中断和DMA配合的复杂性,用库函数版本开发时,事件中断和错误中断混在一起,排查起来很费劲。
模拟IIC本质上是把协议层完全握在自己手里。SCL和SDA两根线用普通GPIO推挽输出,时序通过延时和电平翻转精确控制,总线出问题随时可以把两根线拉高复位,不存在外设状态机一说。代价是CPU占用略高,但对于PCA9555这种低速IO扩展场景,100kHz以内的通信速率完全够用,CPU开销可以忽略。对于几十毫秒级别的按键扫描和LED刷新任务,模拟IIC反而更稳。
2.2 整体模块划分和文件结构
这套驱动按照“协议层—设备层—应用层”三层切分,不搞一坨代码塞一个文件的做法。拿到手的源码包结构如下:
Project ├── Hardware │ ├── iic.h │ ├── iic.c // IIC协议层:起始、停止、发送、接收、ACK │ ├── pca9555.h │ └── pca9555.c // 设备层:寄存器地址映射、IO读写封装 ├── App │ ├── main.c // 应用层示例:按键扫描和LED控制 │ └── bsp_io_exp.c // 板级适配:引脚定义和业务逻辑接线 └── System ├── stm32f10x_it.c └── delay.c // 延时函数,用于时序控制iic.c只负责最基础的协议原语,不关心挂在总线上的设备是谁。pca9555.c基于协议层提供寄存器级读写接口,对外暴露PCA9555_WritePin和PCA9555_ReadPin这类函数。bsp_io_exp.c才是具体项目和芯片对接的地方,里面配置引脚映射关系。要换引脚或者接别的IIC设备,只改最上层即可,协议层不用动。
3. 核心代码拆解:模拟IIC协议层的每个时序细节
3.1 宏定义和引脚初始化
模拟IIC的第一步是定义SCL和SDA对应的GPIO引脚,这里直接使用寄存器操作,保证时序翻转的精确性。宏定义放在iic.h里,方便不同板子切换引脚。
#define IIC_SCL_GPIO_PORT GPIOB #define IIC_SCL_GPIO_PIN GPIO_Pin_6 #define IIC_SCL_GPIO_CLK RCC_APB2Periph_GPIOB #define IIC_SDA_GPIO_PORT GPIOB #define IIC_SDA_GPIO_PIN GPIO_Pin_7 #define IIC_SDA_GPIO_CLK RCC_APB2Periph_GPIOB // 电平控制宏 #define SCL_1() GPIO_SetBits(IIC_SCL_GPIO_PORT, IIC_SCL_GPIO_PIN) #define SCL_0() GPIO_ResetBits(IIC_SCL_GPIO_PORT, IIC_SCL_GPIO_PIN) #define SDA_1() GPIO_SetBits(IIC_SDA_GPIO_PORT, IIC_SDA_GPIO_PIN) #define SDA_0() GPIO_ResetBits(IIC_SDA_GPIO_PORT, IIC_SDA_GPIO_PIN) // SDA输入读取宏 #define SDA_READ() GPIO_ReadInputDataBit(IIC_SDA_GPIO_PORT, IIC_SDA_GPIO_PIN)引脚初始化函数里有个关键点:SDA引脚需要在输出和输入之间切换,所以初始化时把SDA配置为开漏输出,这样读引脚的时候不需要重新改GPIO模式。开漏输出配合外部上拉电阻,天然支持线与特性,这也是IIC协议要求的电气特性。
void IIC_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(IIC_SCL_GPIO_CLK | RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin = IIC_SCL_GPIO_PIN; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // SCL推挽输出 GPIO_Init(IIC_SCL_GPIO_PORT, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = IIC_SDA_GPIO_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; // SDA开漏输出 GPIO_Init(IIC_SDA_GPIO_PORT, &GPIO_InitStructure); SCL_1(); SDA_1(); }这里SCL用推挽输出、SDA用开漏输出,是模拟IIC最常见的搭配。SCL是主机独占总线,推挽输出翻转更快、波形更陡,能支持更高频率。SDA必须开漏,因为在应答阶段需要释放总线读取从设备的ACK信号,如果SDA是推挽输出,强行读输入反而可能烧引脚。上拉电阻一般选4.7kΩ,如果总线设备多或者线长,可以换成2.2kΩ增强驱动能力。
3.2 起始、停止、应答三大时序
IIC通信的底层时序可以用一句话概括:SCL高电平期间,SDA产生下降沿就是起始条件,SDA产生上升沿就是停止条件;数据传输过程中,SDA必须在SCL低电平期间变化,SCL高电平期间保持稳定。代码如下:
// 起始条件:SCL高时,SDA从高拉低 void IIC_Start(void) { SDA_1(); SCL_1(); delay_us(5); SDA_0(); delay_us(5); SCL_0(); // 拉低SCL,准备传输数据 } // 停止条件:SCL高时,SDA从低拉高 void IIC_Stop(void) { SDA_0(); SCL_1(); delay_us(5); SDA_1(); delay_us(5); } // 发送一位数据,bit_val为0或1 void IIC_SendBit(uint8_t bit_val) { if (bit_val) { SDA_1(); } else { SDA_0(); } delay_us(3); SCL_1(); // SCL拉高,从机采样SDA delay_us(5); SCL_0(); // SCL拉低,允许SDA变化 delay_us(3); }delay_us是阻塞延时,STM32F103主频72MHz下,一个空循环大约几个时钟周期。延时数值决定了IIC通信速率,上面参数实测大约100kHz。如果想要加快速度,可以减小延时,最低不要低于2微秒,否则SDA数据还没稳定SCL就拉高了,从机可能采样到错误电平。每次SCL_0()之后的延时同样不能省,这段低电平时间是给从机处理数据用的。
应答检测是IIC调试里最容易翻车的地方。主机发送完一个字节后,第9个时钟周期要释放SDA总线,然后读取SDA电平:低电平表示从机应答ACK,高电平表示从机无应答NACK。
// 等待从机应答,返回0表示ACK,返回1表示NACK uint8_t IIC_WaitAck(void) { uint8_t ack = 0; SDA_1(); // 释放SDA总线 delay_us(3); SCL_1(); delay_us(5); if (SDA_READ()) { ack = 1; // SDA为高,说明从机没应答 } SCL_0(); delay_us(3); return ack; } // 主机发送应答,ack为0表示应答,为1表示非应答 void IIC_SendAck(uint8_t ack) { if (ack) { SDA_1(); // 非应答 } else { SDA_0(); // 应答 } delay_us(3); SCL_1(); delay_us(5); SCL_0(); delay_us(3); }IIC_WaitAck返回1的时候,绝大多数情况是设备地址写错了,或者芯片没有正常上电。排查方法很简单:用示波器抓SDA在第9个时钟的高电平状态,如果是高,总线上确实没有设备响应。还有一种冷门情况是PCA9555的A0/A1/A2地址引脚悬空,导致芯片实际地址和代码里不一致。
3.3 完整字节收发:高位先出的实现
IIC协议规定数据字节高位先出,也就是先发bit7再发bit0。收发函数的实现如下:
// 发送一个字节,高位先出 void IIC_SendByte(uint8_t data) { uint8_t i; for (i = 0; i < 8; i++) { if (data & 0x80) { SDA_1(); } else { SDA_0(); } data <<= 1; delay_us(3); SCL_1(); delay_us(5); SCL_0(); delay_us(3); } } // 接收一个字节,高位先出 uint8_t IIC_ReadByte(void) { uint8_t i, data = 0; SDA_1(); // 释放SDA,交给从机驱动 for (i = 0; i < 8; i++) { data <<= 1; SCL_1(); delay_us(5); if (SDA_READ()) { data |= 0x01; } SCL_0(); delay_us(3); } return data; }接收字节函数里有个细节:data <<= 1和data |= 0x01的顺序不能反过来。如果先按位或再移位,读到的第1位会跑到bit1的位置,最后整个字节的位序就是反的。我见过有人在调PCA9555输入状态时读到的值总是刚好是实际值的左右镜像,就是这里写反了。另外SDA_1()必须在读取数据前执行,这样才能释放总线控制权,否则SDA一直输出低电平,读到的永远是0。
4. PCA9555驱动实现:从寄存器映射到16路IO读写
4.1 PCA9555的寄存器地址映射规则
PCA9555是16位IIC GPIO扩展芯片,内部有6个8位寄存器,按命令字节寻址。命令字节的bit7到bit4固定为0,bit3到bit0决定寄存器类型和端口选择。端口0对应IO0.0到IO0.7,端口1对应IO1.0到IO1.7,映射规则如下表:
| 命令字节 | 寄存器名 | 功能 |
|---|---|---|
| 0x00 | Input Port 0 | 读端口0输入状态 |
| 0x01 | Input Port 1 | 读端口1输入状态 |
| 0x02 | Output Port 0 | 写端口0输出状态 |
| 0x03 | Output Port 1 | 写端口1输出状态 |
| 0x04 | Polarity Inversion 0 | 端口0极性反转设置 |
| 0x05 | Polarity Inversion 1 | 端口1极性反转设置 |
| 0x06 | Configuration 0 | 端口0方向配置,0输出1输入 |
| 0x07 | Configuration 1 | 端口1方向配置,0输出1输入 |
设备地址是另一个容易混淆的点。PCA9555的7位地址由硬件引脚A0/A1/A2决定,固定部分是0100,所以地址范围是0x20到0x27。实际IIC通信时要左移一位拼上读写位,写地址是(0x20 << 1) | 0,读地址是(0x20 << 1) | 1。很多人调不通第一反应是代码问题,结果发现是原理图上A0到A2接到了VCC或GND,导致地址和程序不匹配。
4.2 初始化和方向配置
PCA9555上电后默认所有IO为输入状态,所以如果直接把某个引脚当输出用而不做配置,电平根本拉不起来。初始化代码要做两件事:配置方向、设置默认输出电平。先配置方向再写输出,可以避免IO在切换瞬间输出不确定电平,把外部连接的继电器或者MOS管误触发。
#define PCA9555_DEV_ADDR_W 0x40 // 0x20左移一位 + 写位 #define PCA9555_DEV_ADDR_R 0x41 // 0x20左移一位 + 读位 // 初始化PCA9555 // dev_addr为7位设备地址,如0x20 // dir0/dir1为方向配置,0表示输出,1表示输入 // out0/out1为输出默认值 void PCA9555_Init(uint8_t dev_addr, uint8_t dir0, uint8_t dir1, uint8_t out0, uint8_t out1) { uint8_t addr_w = (dev_addr << 1) | 0; // 配置端口0方向 IIC_Start(); IIC_SendByte(addr_w); IIC_WaitAck(); IIC_SendByte(0x06); // Configuration 0寄存器 IIC_WaitAck(); IIC_SendByte(dir0); // 写入方向配置 IIC_WaitAck(); IIC_Stop(); // 配置端口1方向 IIC_Start(); IIC_SendByte(addr_w); IIC_WaitAck(); IIC_SendByte(0x07); // Configuration 1寄存器 IIC_WaitAck(); IIC_SendByte(dir1); IIC_WaitAck(); IIC_Stop(); // 写入默认输出电平 PCA9555_WritePort(dev_addr, 0, out0); PCA9555_WritePort(dev_addr, 1, out1); }这里分三次IIC通信是保守写法,实际也可以合并成一次通信,连续写四个寄存器。但分开写清晰,而且即使某一次通信失败,也不会影响其他寄存器的状态。dev_addr参数和前述0x20的关系需要理解清楚:函数内部接收的是7位原始地址,到IIC_SendByte时才左移拼上读写位。这样设计的好处是,调用方不用记IIC地址要左移的规则,直接填数据手册上的地址就行。
4.3 单引脚读写封装:屏蔽位操作的细节
PCA9555是按8位一组操作的,但应用层通常只关心某个引脚。封装单引脚读写接口时,需要先读出当前端口寄存器值,再修改对应位,最后写回。这里要注意读操作会改变IIC总线的传输方向,期间要发一次重复起始条件。
// 写单个引脚输出电平 // port为0或1,pin为0到7,level为0或1 void PCA9555_WritePin(uint8_t dev_addr, uint8_t port, uint8_t pin, uint8_t level) { uint8_t temp; uint8_t reg_addr = (port == 0) ? 0x02 : 0x03; // 选择输出寄存器 temp = PCA9555_ReadReg(dev_addr, reg_addr); // 先读回当前输出值 if (level) { temp |= (1 << pin); } else { temp &= ~(1 << pin); } PCA9555_WriteReg(dev_addr, reg_addr, temp); // 写回修改后的值 } // 读单个引脚输入状态,返回0或1 uint8_t PCA9555_ReadPin(uint8_t dev_addr, uint8_t port, uint8_t pin) { uint8_t temp; uint8_t reg_addr = (port == 0) ? 0x00 : 0x01; // 选择输入寄存器 temp = PCA9555_ReadReg(dev_addr, reg_addr); if (temp & (1 << pin)) { return 1; } else { return 0; } }读改写的过程存在一个理论上的风险:如果两个任务同时调用PCA9555_ReadPin和PCA9555_WritePin,可能读到旧值,覆盖掉另一个任务刚写的新值。在裸机单线程场景下这个问题不存在,但如果上了RTOS,建议在PCA9555_WritePin外面加一个互斥信号量,或者把整个读改写过程放在临界区里。
4.4 连续读多个IO:模拟按键矩阵扫描
PCA9555最常见的应用场景之一就是扩展按键输入。16个IO如果全部用于按键,一次性读取端口0和端口1的输入值,然后逐位判断哪个按键按下,比逐个引脚读快得多。下面是一个完整的扫描实现:
// 读取PCA9555两个端口的全部输入状态 // 返回16位数据,低8位是端口0,高8位是端口1 uint16_t PCA9555_ReadAllPins(uint8_t dev_addr) { uint8_t data_l, data_h; uint8_t addr_w = (dev_addr << 1) | 0; uint8_t addr_r = (dev_addr << 1) | 1; // 发命令字节,指定要读取的寄存器起始地址 IIC_Start(); IIC_SendByte(addr_w); IIC_WaitAck(); IIC_SendByte(0x00); // 从Input Port 0开始读 IIC_WaitAck(); // 重复起始条件,切换为读模式 IIC_Start(); IIC_SendByte(addr_r); IIC_WaitAck(); data_l = IIC_ReadByte(); // 读端口0 IIC_SendAck(0); // 发送应答,继续读下一个字节 data_h = IIC_ReadByte(); // 读端口1 IIC_SendAck(1); // 最后一个字节发非应答 IIC_Stop(); return (uint16_t)((data_h << 8) | data_l); }这个函数里重复起始条件是一个容易忽略的关键点。读多字节时,不能在读完第一个字节后直接发停止再重新开始,因为那样会丢失总线所有权,从机认为通信已经结束,后续读不到数据。IIC协议规定这种情况下要在总线上发一个重复起始条件,然后跟设备地址加读位。PCA9555支持寄存器地址自动递增,所以从0x00开始读,第二个字节自动就是端口1的数据。
5. 避坑指南:PCA9555调试中常见的5个问题
5.1 设备无应答:地址匹配不过去
现象:IIC_WaitAck一直返回1,读出来的数据全是0xFF或者0x00。
原因分两类。第一类是地址写错,PCA9555的7位地址0x20到0x27不是直接发给总线的,需要左移一位,很多人在这一步搞混。第二类是A0到A2引脚悬空,芯片内部没有上拉也没有下拉,读到的地址不确定,可能落在0x20到0x27之外的某个值。
解决:先核对原理图上A0/A1/A2接的到底是VCC还是GND,对照数据手册算出实际地址。然后用逻辑分析仪或者示波器抓起始条件后的第一个字节,直接看地址线上的实际值,对比代码里发送的地址,一抓一个准。
5.2 IO输出电平不对:方向寄存器被覆盖
现象:初始化时明明配置了某个引脚为输出,也写了输出电平,但实测引脚电压不对,有时候是0V拉不上去,有时候是高电平但带不动负载。
原因:配置顺序出了问题。如果先写输出寄存器再配置方向,PCA9555内部逻辑会在方向切换的瞬间把输出寄存器重置,导致之前写的电平丢失。另外,方向寄存器默认值全为1,即所有IO都是输入模式,如果配置代码没执行到,引脚就是高阻态,万用表量到的电压是浮空的,不是真正的高电平。
解决:严格按“先配置方向,再写输出电平”的顺序初始化。如果代码在跑飞或者死机后IO状态不确定,可以在初始化函数末尾加一个读回校验,把方向寄存器的值读出来,确认配置确实写进去了。
5.3 SDA一直为低:总线被从机拉死
现象:程序运行一段时间后,IIC通信突然全部超时,用示波器看SDA波形,发现一直处于低电平,SCL还在正常翻转。
原因:通信中途发生异常,比如主机发送过程中从机掉电,或者代码在IIC_Stop之前进入了死循环,从机正处于等待数据的状态,把SDA拉低不放。IIC协议规定从机在没有收到停止条件前,有权利持续拉低SDA表示忙碌。
解决:在IIC通信的每个步骤之间加超时判断,不要死等。我一般的做法是在IIC_WaitAck里加一个计数器,超过比如200次循环就强制发停止条件,然后拉高SCL和SDA各9个时钟,给总线上的设备做一次软件复位,之后再重新发起通信。
5.4 模拟IIC读到错位数据:延时太短
现象:初始化配置读回来的数据偶尔正确偶尔错误,用示波器看波形发现SDA数据线的建立时间不够,SCL上升沿附近SDA还在跳变。
原因:SCL_1()之后到SDA_READ()之间延时太短。SCL拉高后,从机需要一点时间把数据稳定到SDA线上,这个时间在PCA9555数据手册里是几百纳秒,但考虑到GPIO翻转本身有压摆率限制,实际留3到5微秒是安全的。
解决:把IIC_ReadByte中SCL_1()和SDA_READ()之间的延时从2微秒加到5微秒。如果项目对速度有要求,可以优化为先读GPIO输入寄存器再延时,用空间换时间,但不要在时序上省这几微秒,否则后患无穷。
5.5 按键扫描抖动:缺少去抖逻辑
现象:用PCA9555读按键状态,明明是按了一次按键,逻辑里却触发了三四次。
原因:PCA9555本身是纯硬件IO扩展,没有按键去抖功能。机械按键在按下和释放的瞬间,簧片接触会产生几十毫秒的抖动,引脚电平在这个窗口内会反复跳变,直接读取必然产生多次触发。
解决:在PCA9555_ReadAllPins返回值的基础上做软件去抖。常见做法是连续读两次,间隔10到20毫秒,两次读到的状态一致才认为按键有效。更稳妥的做法是维护一个状态机,边沿检测加定时确认,消除抖动的同时还能区分长按和短按。
6. 二次开发指南:把PCA9555驱动改造成你自己的模块
拿到这套代码后,最常见的改造需求有三个:换GPIO引脚、接多个PCA9555、把驱动移植到其他STM32型号。前两个需求半小时就能搞定,第三个需要一点耐心。
换引脚的改造点在iic.h的宏定义上。把IIC_SCL_GPIO_PORT和IIC_SDA_GPIO_PORT改成目标引脚对应的端口和PIN号,再把IIC_SCL_GPIO_CLK和RCC_APB2Periph_GPIOB的时钟宏一起改掉。这里有个隐藏坑:如果SCL和SDA在不同端口,比如SCL在GPIOA而SDA在GPIOB,那RCC_APB2PeriphClockCmd里要同时使能两个端口时钟,我见过有人只使能了一个端口时钟,结果另一个引脚完全不动作,排查了半天。
接多个PCA9555的改造更简单。每个芯片的A0/A1/A2引脚可以独立设置,把三个地址引脚排列组合出8个不同地址,然后封装一个设备表:
// 多设备管理结构体 typedef struct { uint8_t dev_addr; // PCA9555的7位地址 uint8_t port_dir[2]; // 端口方向配置 uint16_t output; // 当前输出状态缓存 } PCA9555_Dev_t; // 初始化两个PCA9555 PCA9555_Dev_t g_pca_dev[2]; void PCA9555_MultiInit(void) { g_pca_dev[0].dev_addr = 0x20; // A0=A1=A2=0 g_pca_dev[1].dev_addr = 0x21; // A0=1, A1=A2=0 PCA9555_Init(g_pca_dev[0].dev_addr, 0x00, 0xFF, 0x00, 0x00); PCA9555_Init(g_pca_dev[1].dev_addr, 0x00, 0x00, 0xFF, 0xFF); }移植到其他STM32型号时,iic.c里的GPIO_SetBits和GPIO_ResetBits是STM32标准库的通用函数,只要你的工程里包含对应型号的标准库,这些代码可以直接编译。但F1系列和F4系列的时钟树不同,RCC_APB2Periph_GPIOB这类宏在F4上可能是RCC_AHB1Periph_GPIOB,而且GPIO初始化结构体里少了GPIO_Speed字段,需要按型号调整。如果是用HAL库,直接换成HAL_GPIO_WritePin和HAL_GPIO_ReadPin即可,时序逻辑完全不用动。
另外补充一个我在实际项目里的血泪教训:模拟IIC和硬件IIC不要同时挂在同一条总线上。有一次项目里PCA9555用的是模拟IIC,板上另一个传感器用了硬件IIC,两条总线共用同一组引脚,结果硬件IIC的初始化把模拟IIC的GPIO模式改掉了,两边的通信全乱套。从那以后我每次做板级适配都强制走一遍引脚复用检查,确认每个引脚只被一个外设初始化,希望这篇笔记能帮你在自己的项目里少踩这些坑。
本文还有配套的精品资源,点击获取