(六)ESP32-S3之IIC_EXIO
XL9555 介绍
XL9555 是一款 24 引脚的 CMOS 器件,支持 IIC 总线或 SMBus 接口进行驱动。XL9555 器件是一个 16 位通用并行输入/输出(GPIO)扩展器,可用其 GPIO 连接按键、LED、传感器等,解决需要额外的 I/O 的需求。XL9555 有如下特性:
⚫ IIC 总线至 16 位 GPIO 扩展器
⚫ 工作电源电压范围为 2.3 V 至 5.5 V
⚫ 低待机电流消耗
⚫ 5 V 容错 I/O 端口
⚫ 400 kHz 快速模式 IIC 总线时钟频率
⚫ SCL/SDA 输入上的噪声滤波器
⚫ 内部通电复位
⚫ 器件地址由 3 个硬件地址引脚决定,最多可在总线上挂载 8 个器件
⚫ 中断脚为开漏输出模式(低电平有效)
⚫ 16 个 I/O 引脚,默认为 16 个输入
简单概括一下,XL9555 可使用 400kHz 速率的 IIC 通信接口与微控制器进行连接,也就是用 2 根通信线可扩展使用 16 个 IO。XL9555 器件地址会由三个硬件地址引脚决定,理论上在这个 IIC 总线上可挂载 8 个 XL9555 器件,足以满足 IO 引脚需求。XL9555 上电进行复位,16 个I/O 口默认为输入模式,当输入模式的 IO 口状态发生变化时,即发生从高电平变低电平或者从低电平变高电平,中断脚会拉低。当中断有效后,必须对 XL9555 进行一次读取/写入操作,复位中断,才可以输出下一次中断,否则中断将一直保持。
XL9555 引脚图如下图所示:
XL9555 器件总共有 24 个管脚,分别为电源线 VCC、地线 GND、GPIO 口、通信线、地址线,上图用不同底色标注出来了。16 个 IO 分为了 2 组,一组是 8 个,分为是 P0x 和 P1x,这些IO 都可通过器件寄存器进行配置作为输出或者输出使用。通信线就是 SDA 和 SCL,中断线 INT也划分过来通信线。而地址线就是 A0、A1 和 A2,用来决定器件地址。
XL9555 寻址
要进行 IIC 通信,首先得知道器件地址,XL9555 器件地址是 7 位的,具体格式如下图。
从上图可以知道,XL9555 器件地址由两部分组成,一部分就是“Fixed bits”即固定的 4 位“0100”;另一部分就是“Programmable bits”即可编程的 3 位“A2 A1 A0”,在硬件上,都把这三个引脚接地处理,所以这三位为“000”。最终可得到,XL9555 器件地址为“0100000”即
0x20。读操作地址就为 0x41,即 0100 0001;写操作地址就为 0x40,即 0100 0000。
XL9555 寄存器
接下来,介绍一下 XL9555 器件的八个寄存器,如下图所示
由于在 IIC通信中,数据都是以字节作为单位,表示寄存器地址的数据也是 1个字节。由于XL9555器件只有八个寄存器,所以这里 1个字节用 3个位表示,即 Table 5中的 B2、B1和 B0。这 8个寄存器都是 XL9555器件的 16个 GPIO 进行配置,其实分为 4 种:输入查询、输出设置、极性翻转和端口配置,每种都有两个寄存器对应的就是 P0 端口和 P1 端口。地址 0x00 和 0x01 的寄存器是“Input Port0”和“Input Port1”寄存器,主要用于获取 P0 和P1 的 IO 输入状态。寄存器如下图所示
该寄存器只反应引脚输入逻辑电平情况,不管 IO 是设置成输入还是输出模式。打个比方,从 0x00 地址处(Input Port 0 Register)读出的数据是 0x55,以二进制展开为 01010101,从高位到低位对应的就是 P07~P00 的 IO 状态,P00 的输入电平状态就为高电平。
地址 0x02 和 0x03 的寄存器是“Output Port0”和“Output Port1”寄存器,主要用于设置 P0和 P1 的 IO 输出电平。寄存器如下图所示。
该寄存器设置的是已经配置成输出模式的 IO 口的 IO 输出状态,1 代表的是高电平,0 代表的都是低电平,配置 IO 为输出模式的寄存器为 Configuration Port 寄存器。寄存器的一些位值对已经设置成输入模式的 IO 口是没有影响的。该寄存器还支持读取,读取到的值只是设置值,并不是实际引脚电平值,实际电平值通过 Input Port 寄存器查询即可。
地址 0x04 和 0x05 的寄存器是“Polarity Inversion Port0”和“Polarity Inversion Port1”寄存器,用于对端口 0 和端口 1 进行极性翻转。该寄存器值默认为 0,所以对 IO 电平翻转功能并没有启用,且在本实验也没有用到,所以不做讲解,详细说明可看《XL9555 数据手册》P13。地址 0x06 和 0x07 的寄存器是“Configuration Port1”和“Configuration Port0”寄存器,用于配置 P0 和 P1 的 IO 输入/输出模式。寄存器如下图所示。
该寄存器某一个位设置成 1 即作为输入模式,设置成 0 即作为输出模式。打个比方,要向0x06 地址处(Configuration Port 0 Register)写入的数据是 0x55,以二进制展开为 01010101,从高位到低位对应的就是 P07~P00 的 IO 配置模式,P00、P02、P04、P06 这四个 IO 口即配置为输入模式,而 P01、P03、P05、P07 这四个 IO 口配置为输出模式。XL9555 上电复位后,所有 IO
口默认都是输入状态,即上图这两个寄存器读出来的值都是 0xFF。
XL9555 硬件原理图
IIC_EXIO 函数解析
在 IDF 版的 09_iic_exio 例程中,作者在 09_iic_exio \components\BSP 路径下新增了一个 IIC文件夹和一个 XL9555 文件夹,分别用于存放 iic.c、iic.h 和 xl9555.c 以及 xl9555.h 这四个文件。其中,iic.h 和 xl9555.h 文件负责声明 IIC 以及 XL9555 相关的函数和变量,而 iic.c 和 xl9555.c 文件则实现了 IIC 以及 XL9555 的驱动代码。下面,我们将详细解析这四个文件的实现内容
iic.h
/* IIC 控制块 */
typedef struct _i2c_obj_t {
i2c_port_t port;
gpio_num_t scl;
gpio_num_t sda;
esp_err_t init_flag;
} i2c_obj_t;
/* 读写数据结构体 */
typedef struct _i2c_buf_t {
size_t len;
uint8_t *buf;
} i2c_buf_t;
extern i2c_obj_t iic_master[I2C_NUM_MAX];
/* 读写标志位 */
#define I2C_FLAG_READ (0x01) /* 读标志 */
#define I2C_FLAG_STOP (0x02) /* 停止标志 */
#define I2C_FLAG_WRITE (0x04) /* 写标志 */
/* 引脚与相关参数定义 */
#define IIC0_SDA_GPIO_PIN GPIO_NUM_41 /* IIC0_SDA 引脚 */
#define IIC0_SCL_GPIO_PIN GPIO_NUM_42 /* IIC0_SCL 引脚 */
#define IIC1_SDA_GPIO_PIN GPIO_NUM_5 /* IIC1_SDA 引脚 */
#define IIC1_SCL_GPIO_PIN GPIO_NUM_4 /* IIC1_SCL 引脚 */
#define IIC_FREQ 400000 /* IIC 通信频率 */
#define I2C_MASTER_TX_BUF_DISABLE 0 /* I2C 主机不需要缓冲区 */
#define I2C_MASTER_RX_BUF_DISABLE 0 /* I2C 主机不需要缓冲区 */
#define ACK_CHECK_EN 0x1 /* I2C master 将从 slave 检查 ACK
**iic.c **
i2c_obj_t iic_master[I2C_NUM_MAX]; /* 为 IIC0 和 IIC1 分别定义 IIC 控制块结构体 */
/**
* @brief 初始化 IIC
* @param iic_port:I2C 编号(I2C_NUM_0 / I2C_NUM_1)
* @retval IIC 控制块 0 / IIC 控制块 1
*/
i2c_obj_t iic_init(uint8_t iic_port)
{
uint8_t i;
i2c_config_t iic_config_struct = {0};
if (iic_port == I2C_NUM_0)
{
i = 0;
}
else
{
i = 1;
}
iic_master[i].port = iic_port;
iic_master[i].init_flag = ESP_FAIL;
if (iic_master[i].port == I2C_NUM_0)
{
iic_master[i].scl = IIC0_SCL_GPIO_PIN;
iic_master[i].sda = IIC0_SDA_GPIO_PIN;
}
else
{
iic_master[i].scl = IIC1_SCL_GPIO_PIN;
iic_master[i].sda = IIC1_SDA_GPIO_PIN;
}
iic_config_struct.mode = I2C_MODE_MASTER; /* 设置 IIC 模式-主机模式 */
iic_config_struct.sda_io_num = iic_master[i].sda; /* 设置 IIC_SDA 引脚 */
iic_config_struct.scl_io_num = iic_master[i].scl; /* 设置 IIC_SCL 引脚 */
/* 配置 IIC_SDA 引脚上拉使能 */
iic_config_struct.sda_pullup_en = GPIO_PULLUP_ENABLE;
/* 配置 IIC_SCL 引脚上拉使能 */
iic_config_struct.scl_pullup_en = GPIO_PULLUP_ENABLE;
iic_config_struct.master.clk_speed = IIC_FREQ; /* 设置 IIC 通信速率 */
/* 设置 IIC 初始化参数 */
i2c_param_config(iic_master[i].port, &iic_config_struct);
/* 激活 I2C 控制器的驱动 */
iic_master[i].init_flag = i2c_driver_install(iic_master[i].port,/* 端口号 */
/* 主机模式 */
iic_config_struct.mode,
/* 从机模式下接收缓存大小(主机模式不使用) */
I2C_MASTER_RX_BUF_DISABLE,
/* 从机模式下发送缓存大小(主机模式不使用) */
I2C_MASTER_TX_BUF_DISABLE,
0);/* 用于分配中断的标志(通常从机模式使用)*/
if (iic_master[i].init_flag != ESP_OK)
{
while(1)
{
printf("%s , ret: %d", __func__, iic_master[i].init_flag);
vTaskDelay(1000);
}
}
return iic_master[i];
}
/**
* @brief IIC 读写数据
* @param self:设备控制块
* @param addr:设备地址
* @param n :数据大小
* @param bufs:要发送的数据或者是读取的存储区
* @param flags:读写标志位
* @retval 无
*/
esp_err_t i2c_transfer(i2c_obj_t *self, uint16_t addr, size_t n,
i2c_buf_t *bufs, unsigned int flags)
{
int data_len = 0;
esp_err_t ret = ESP_FAIL;
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
if (flags & I2C_FLAG_WRITE)
{
i2c_master_start(cmd); /* 启动位 */
i2c_master_write_byte(cmd, addr<<1, ACK_CHECK_EN);/* 从机地址 + 写操作位 */
i2c_master_write(cmd, bufs->buf, bufs->len, ACK_CHECK_EN);/* len 个数据 */
data_len += bufs->len;
--n;
++bufs;
}
i2c_master_start(cmd); /* 启动位 */
i2c_master_write_byte(cmd, addr << 1 |
(flags & I2C_FLAG_READ),
ACK_CHECK_EN); /* 从机地址 + 读/写操作位 */
for (; n--; ++bufs)
{
if (flags & I2C_FLAG_READ)
{
i2c_master_read(cmd, /* 读取数据 */
bufs->buf,
bufs->len,
n == 0 ? I2C_MASTER_LAST_NACK : I2C_MASTER_ACK);
}
else
{
if (bufs->len != 0)
{
i2c_master_write(cmd,
bufs->buf,
bufs->len,
ACK_CHECK_EN); /* len 个数据 */
}
}
data_len += bufs->len;
}
if (flags & I2C_FLAG_STOP)
{
i2c_master_stop(cmd); /* 停止位 */
}
ret = i2c_master_cmd_begin(self->port,
cmd,
100 * (1 + data_len) / portTICK_PERIOD_MS);
i2c_cmd_link_delete(cmd); /* 释放命令链接使用的资源 */
return ret;
}
与 STM32 不同,ESP32 在 IIC 的配置上提供了两个 IIC 端口,但是在开发过程中会用到使用不同 IIC 端口的外设,为了保持代码的最大兼容性与减少两个 IIC 端口在使用过程中的冲突,我们在 iic.h 文件里定义了包含 IIC 端口的结构体参数,同时在 IIC 初始化函数中实现对这两个IIC 端口的判断,再激活 IIC 控制器驱动函数完成对 IIC 的初始化。在i2c_transfer()函数中,我们创建一个命令链接,将一系列待发送给从机的数据填充命令链接,接下来器件通信时序去决定 flags 参数,进而选择代码不同的执行情况。在最后释放命令链接使用资源前触发 I2C 控制器执行命令链接,并赋值于 ret 用作函数返回值。IIC 驱动中对 IIC 的各种操作,例如产生 IIC 起始信号、产生 IIC 停止信号等,请读者结合IIC 的时序规定查看本实验的配套实验源码
xl9555.h
通过前面的介绍可知,XL9555 器件有 8 个寄存器,而且 16 个 IO 口在寄存器的位置都是固定的,基于单个 IO 操作单位的考虑,故这里我们也定义了对应的宏,如下所示。
/* 引脚与相关参数定义 */
#define XL9555_INT_IO GPIO_NUM_40 /* XL9555_INT 引脚 */
#define XL9555_INT gpio_get_level(XL9555_INT_IO) /* 读取 XL9555_INT 的电平 */
#define EXIO_ADDR 0x20 /* 7 位器件地址 */
/* 器件寄存器 */
#define XL9555_INPUT_PORT0_REG 0 /* 输入 P0 寄存器用于读取 P0 端口的输入值 */
#define XL9555_INPUT_PORT1_REG 1 /* 输入 P1 寄存器用于读取 P1 端口的输入值 */
#define XL9555_OUTPUT_PORT0_REG 2 /* 输出 P0 寄存器用于设置 P0 端口的输出值 */
#define XL9555_OUTPUT_PORT1_REG 3 /* 输出 P1 寄存器用于设置 P1 端口的输出值 */
#define XL9555_INVERSION_PORT0_REG 4
/* 极性反转 P0 寄存器用于当 P0 端口做为输入时,对输入的电平进行反转处理,即管脚为高电平时,设置这
个寄存器中相应的位为 1 时,读取到的输入寄存器 0,1 的值就是低电平 0 */
#define XL9555_INVERSION_PORT1_REG 5
/* 极性反转 P1 寄存器用于当 P1 端口做为输入时,对输入的电平进行反转处理,即管脚为高电平时,设置这
个寄存器中相应的位为 1 时,读取到的输入寄存器 0,1 的值就是低电平 0 */
#define XL9555_CONFIG_PORT0_REG 6
/* 配置 P0 寄存器用于配置 P0 端口的做为输入(1)或是输出(0) */
#define XL9555_CONFIG_PORT1_REG 7
/* 配置 P1 寄存器用于配置 P1 端口的做为输入(1)或是输出(0) */
/* XL9555 各个 IO 的功能 */
#define AP_INT_IO 0x0001 /* AP3216C 中断引脚 P00 */
#define QMA_INT_IO 0x0002 /* QMA6100P 中断引脚 P01 */
#define SPK_EN_IO 0x0004 /* 功放使能引脚 P02 */
#define BEEP_IO 0x0008 /* 蜂鸣器控制引脚 P03 */
#define OV_PWDN_IO 0x0010 /* 摄像头待机引脚 P04 */
#define OV_RESET_IO 0x0020 /* 摄像头复位引脚 P05 */
#define GBC_LED_IO 0x0040 /* ATK_MODULE 接口 LED 引脚 P06 */
#define GBC_KEY_IO 0x0080 /* ATK_MODULE 接口 KEY 引脚 P07 */
#define LCD_BL_IO 0x0100 /* RGB 屏背光控制引脚 P10 */
#define CT_RST_IO 0x0200 /* 触摸屏中断引脚 P11 */
#define SLCD_RST_IO 0x0400 /* SPI_LCD 复位引脚 P12 */
#define SLCD_PWR_IO 0x0800 /* SPI_LCD 控制背光引脚 P13 */
#define KEY3_IO 0x1000 /* 按键 3 引脚 P14 */
#define KEY2_IO 0x2000 /* 按键 2 引脚 P15 */
#define KEY1_IO 0x4000 /* 按键 1 引脚 P16 */
#define KEY0_IO 0x8000 /* 按键 0 引脚 P17 */
#define KEY0 xl9555_pin_read(KEY0_IO) /* 读取 KEY0 引脚 */
#define KEY1 xl9555_pin_read(KEY1_IO) /* 读取 KEY1 引脚 */
#define KEY2 xl9555_pin_read(KEY2_IO) /* 读取 KEY2 引脚 */
#define KEY3 xl9555_pin_read(KEY3_IO) /* 读取 KEY3 引脚 */
#define KEY0_PRES 1 /* KEY0 按下 */
#define KEY1_PRES 2 /* KEY1 按下 */
#define KEY2_PRES 3 /* KEY1 按下 */
#define KEY3_PRES 4 /* KEY1 按下 */
xl9555.c
/**
* @brief 初始化 XL9555
* @param 无
* @retval 无
*/
void xl9555_init(i2c_obj_t self)
{
uint8_t r_data[2];
if (self.init_flag == ESP_FAIL)
{
iic_init(I2C_NUM_0); /* 初始化 IIC */
}
xl9555_i2c_master = self;
gpio_config_t gpio_init_struct = {0};
gpio_init_struct.intr_type = GPIO_INTR_DISABLE;
gpio_init_struct.mode = GPIO_MODE_INPUT;
gpio_init_struct.pin_bit_mask = (1ull << XL9555_INT_IO);
gpio_init_struct.pull_down_en = GPIO_PULLDOWN_DISABLE;
gpio_init_struct.pull_up_en = GPIO_PULLUP_ENABLE;
gpio_config(&gpio_init_struct); /* 配置 XL_INT 引脚 */
/* 上电先读取一次清除中断标志 */
xl9555_read_byte(r_data, 2);
xl9555_ioconfig(0xF003);
xl9555_pin_write(BEEP_IO, 1);
xl9555_pin_write(SPK_EN_IO, 1);
}
我们在上文提到,在 XL9555 初始化函数中,首先对 IIC 初始化进行一个判断,这是因为ESP32-S3 芯片对第二次初始化的函数会返回配置错误的警告,从而导致芯片一直在复位,所以为了避免重复初始化某个函数我们一般会在相应的驱动初始化代码前对其进行判断,若在此前的步骤中没有初始化某个函数那么在这个判断语句中也可以对某函数进行初始化。此举有效避免了重复初始化以及遗漏初始化的问题。然后就是对 XL9555 引脚的配置了。这里需要注意的是我们在对某个引脚采取读写操作前会先读取一次清除中断表示的操作。
接下来,看一下如何向 XL9555 寄存器写入一个字节数据的函数 xl9555_write_byte,代码如下:
/**
* @brief 向 XL9555 写入 16 位 IO 值
* @param reg:寄存器地址
* @param data:要写入的数据
* @param len:要写入数据的大小
* @retval ESP_OK:读取成功;其他:读取失败
*/
esp_err_t xl9555_write_byte(uint8_t reg, uint8_t *data, size_t len)
{
i2c_buf_t bufs[2] = {
{.len = 1, .buf = ®},
{.len = len, .buf = data},
};
return i2c_transfer(&xl9555_i2c_master,
XL9555_ADDR,
2,
bufs,
I2C_FLAG_STOP);
}
这里的读操作流程跟 XL9555 写数据函数的过程是一致的,这里不再赘述。基于写数据和读数据的函数接口,我们就可以为输出功能、输入功能以及端口配置功能封装成对应函数接口,方便调用,函数代码如下。
/**
* @brief 获取某个 IO 状态
* @param pin : 要获取状态的 IO
* @retval 此 IO 口的值(状态, 0/1)
*/
int xl9555_pin_read(uint16_t pin)
{
uint16_t ret;
uint8_t r_data[2];
xl9555_read_byte(r_data, 2);
ret = r_data[1] << 8 | r_data[0];
return (ret & pin) ? 1 : 0;
}
/**
* @brief XL9555 的 IO 配置
* @param config_value:IO 配置输入或者输出
* @retval 返回设置的数值
*/
uint16_t xl9555_ioconfig(uint16_t config_value)
{
/* 从机地址 + CMD + data1(P0) + data2(P1) */
/* P00、P01、P14、P15、P16、P17 为输入,
其他引脚为输出 -->1111 0000 0000 0011
注意:0 为输出,1 为输入*/
uint8_t data[2];
esp_err_t err;
int retry = 3;
data[0] = (uint8_t)(0xFF & config_value);
data[1] = (uint8_t)(0xFF & (config_value >> 8));
do
{
err = xl9555_write_byte(XL9555_CONFIG_PORT0_REG, data, 2);
if (err != ESP_OK)
{
retry--;
vTaskDelay(100);
ESP_LOGE("IIC", "%s configure %X failed, ret: %d", __func__,
config_value, err);
xl9555_failed = 1;
if ((retry <= 0) && xl9555_failed)
{
vTaskDelay(5000);
esp_restart();
}
}
else
{
xl9555_failed = 0;
break;
}
} while (retry);
return config_value;
}
xl9555_ioconfig 函数主要就是设置 XL9555 某个 IO 的模式,可设置成输出,也可设置为输入。内部实现逻辑比较简单,在XL9555芯片中除去 IIC通信引脚以及电源引脚还有 16个引脚,其中每个引脚对应不同寄存器,所以每一个引脚都有一个与之对应的 16 进制寄存器地址。假如我们要设置 IO0_0 与 IO0_1 以及 IO1_1 为输入模式,其它引脚皆为输出模式,那么只需将这三个引脚配置为“1”,其它引脚配置为“0”,对应 16 位的二进制数为:“0000 0010 0000 0011”,将该二进制数转换为十六进制也就是:“0x203”我们将得到的数值作为形参传进该函数中,即可完成对这三个引脚的输入模式配置。
xl9555_pin_read ()函 数 主 要 就 是 获 取 XL9555 某 个 IO 的 电 平 状 态 , 通 过 调用 xl9555_read_byte()函数去查询输入端口寄存器的值,然后与该位进行比较,最终知道该位的电平状态。
**CMakeLists.txt **
set(src_dirs
LED
IIC
XL9555)
set(include_dirs
LED
IIC
XL9555)
set(requires
driver)
idf_component_register(SRC_DIRS ${src_dirs}
INCLUDE_DIRS ${include_dirs} REQUIRES ${requires})
component_compile_options(-ffast-math -O3 -Wno-error=format=-Wno-format)
main.c
i2c_obj_t i2c0_master;
/**
* @brief 显示实验信息
* @param 无
* @retval 无
*/void show_mesg(void)
{
/* 串口输出实验信息 */
printf("\n");
printf("********************************\n");
printf("ESP32-S3\n");
printf("EXIO TEST\n");
printf("ATOM@ALIENTEK\n");
printf("KEY0:Beep On, KEY1:Beep Off\n");
printf("KEY2:LED On, KEY3:LED Off\n");
printf("********************************\n");
printf("\n");
}
/**
* @brief 程序入口
* @param 无
* @retval 无
*/
void app_main(void)
{
uint8_t key;
esp_err_t ret;
ret = nvs_flash_init(); /* 初始化 NVS */
if (ret == ESP_ERR_NVS_NO_FREE_PAGES ||
ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
{
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
led_init(); /* 初始化 LED */
i2c0_master = iic_init(I2C_NUM_0); /* 初始化 IIC0 */
xl9555_init(i2c0_master); /* 初始化 XL9555 */
show_mesg(); /* 显示实验信息 */
while(1)
{
key = xl9555_key_scan(0);
switch (key)
{
case KEY0_PRES:
{
printf("KEY0 has been pressed \n");
xl9555_pin_write(BEEP_IO, 0);
break;
}
case KEY1_PRES:
{
printf("KEY1 has been pressed \n");
xl9555_pin_write(BEEP_IO, 1);
break;
}
case KEY2_PRES:
{
printf("KEY2 has been pressed \n");
LED(0);
break;
}
case KEY3_PRES:
{
printf("KEY3 has been pressed \n");
LED(1);
break;
}
default:
{break;
}
}
if (XL9555_INT == 0)
{
printf("XL9555_INT success!!! \n");
}
vTaskDelay(200);
}
}
下载代码完成后,按键被按下时,程序会执行特定的操作。例如,如果按下 KEY0,程序会启动蜂鸣器;如果按下 KEY1,程序会关闭蜂鸣器;如果按下 KEY2,程序会关闭 LED 灯;如果按下 KEY3,程序会打开 LED 灯。
更多推荐
所有评论(0)