4.在esp32上使用tftLCD屏幕(st7789驱动)

前言

写在开头

        在前言里面我想告诉你一些在对一款新的处理器的学习中,我们会遇到的那些错误,并且我相信这绝对不会是我一个人会出现的问题,出现这种问题首先原因是处理器的教程不够全面,没有像stm32一样一套保姆级别的教程能够一点一点引导着大家去学习,第二个原因就是esp32更新版本很快,导致一些教程会因为大版本的更新而不能使用。这里我就想站在初学者的角度来进行一番探索,也希望大家看完我出错的的过程后能够有所启发。

初次尝试

        在学习一款处理器的过程中,如何连接外设,配置外设接口并且成功驱动外设是非常重要的,经过前面的学习,我们结合其他处理器的学习经验,例如stm32,再结合乐鑫官方的教程和示例工程,其实是很容易就能控制一些常规的GPIO口,USART,I2C接口的外设或者通信。

        但是在我学习到LCD屏幕时,却遇到了很大的阻碍,和往常一样,在我准备配置这块1.3寸的驱动为st7789的屏幕时,我照例打开乐鑫官方的编程指南ESP-IDF 编程指南 - ESP32 - — ESP-IDF 编程指南 latest 文档 (espressif.com)。找到API参考,外设API,进而找到LCD这个目录,然后我就发现没有中文,这种情况我已经不是第一次遇到了,所以我很熟练的进行手册的版本切换,从原来的master版切换到v5.1.3版本,切换完成后我就看到它上面提示到最新稳定版是v5.2.1,所以我就又点击它切换到5.2.1版本。但是令人失望的是依旧没有出现我们想要的中文。那么我就明白了这一个说明大概率是没有中文的,但是没关系,英文版本我们一样看得懂,毕竟现在互联网这么发达,找到一个翻译实在是太容易了。

        首先映入眼帘的就是功能概述了,我从中提取到三个关键信息

  1. 乐鑫提供了开箱即用的LCD驱动程序
  2. 有安装驱动程序的步骤
  3. 有控制LCD IO的API

        我这一看这不是妥了吗,基本上和之前GPIO,USART,I2C一样,按着官方的教程一步一步来,应该很轻松就能把这个屏幕初始化完成。殊不知这才是噩梦的开始。

并没有找到驱动

        首先是即使乐鑫官方明说了有st7789的驱动程序,我也没有在乐鑫组件注册表中找到它,上面所提到的两款驱动,st7789ssd1306,我只能搜索到ssd1306的驱动组件。但是这也无伤大雅,驱动组件中无非是定义着一些有关驱动的参数罢了,相关参数我们完全可以按照stm32开发时的流程,直接在st7789驱动的数据手册里面或者是商家给的驱动说明里面提取即可。

初次尝试安装SPI LCD IO 驱动程序

        然后就是去点击SPI Interfaced LCD链接进入安装SPI LCD IO 驱动程序然后获取面板句柄的步骤的页面,实际上这些内容就在下面,直接向下滚动就能够看到。

        我这一看,嚯,一二三步骤都标好了,这不就是我最需要的吗,当时我心里还在想,乐鑫官方是真的贴心啊,学习成本大大降低了,之后就是我按照上面的三个步骤,进行我的SPI配置,先创建SPI总线,然后从 SPI 总线分配 LCD IO 设备句柄,最后就是安装液晶屏控制器驱动程序,一切都是那么的水到渠成。

初次使用控制LCD IO的API

        依旧是点击LCD 面板 IO 操作链接,转到下图界面

        然后我选择了这三个看起来有用的初始化函数,进行一一调用。但是当我调用完成以后,我迷茫了,是的跟着手册一路配置加初始化到这里我不知道我该干什么了,按理说我们现在已经初始化完成了SPI和LCD的相关配置,接着就是收发数据了,但是我完全不知道该如何向LCD屏中发送数据,而手册中唯一和发送数据相关的就是esp_lcd_panel_draw_bitmap()函数,这个函数是将用户提供的颜色缓冲区绘制到LCD屏幕上。

        当时我没有感觉到又哪里不对,心里想的是这不就是个画点函数,有了画点函数我不随便编写其他的绘画函数。所以当时我选择了先编译一下程序看看,一方面是想看初始化完成后到底是个什么情况,另一方面也是要确定我的程序确实没有问题能够运行起来。

        很成功,在解决了编译时出现的小问题后,程序编译成功并且也烧录成功了,但是屏幕这个时候并没有点亮。在了解到初始化完成后屏幕也不会点亮,必须手动填充色块的相关情况后,我决定使用esp_lcd_panel_draw_bitmap()这个函数来试着填充一下屏幕区域,但是这时候我发现了另外的问题,虽然这个函数的参数和画点函数比较相似,都是传递起始坐标和结束坐标,另外就是传递一个颜色指针。但是我如果想填满整个屏幕,我就需要定义一个240*240大小的数组,并且我还要给他填充数据,这也太傻鸟了吧,我在心里忍不住吐槽。于是我就想着只填充几行看看效果就行,定义了一个20*20的数组,将数组的地址传递进去之后,按照它这函数的作用,应该是会显示一个小方块,不过经过我重新烧录验证,屏幕依旧是是没有任何反应。

        到这里我就开始怀疑我的程序到底是不是有问题了,于是我不再局限于官方的编程手册,转而到网络上搜索相关的教程,事实证明我这一步决策是十分正确的,网上的相关教程确实补充了我之前根据官方编程手册的一些不足之处(实际上也是官方的另外的教程罢了)。

二次尝试

        由于大部分人使用esp32做项目都是用Arduino这个开发环境,所以我在搜索教程时也遇到了一定困难,中间具体的搜索过程我就不细说了,直接讲最后的搜索结果,我在bilibili上找到了一个用esp-idf移植驱动一块触摸评的教程,起初一段我觉得这个教程讲的十分的详细,所以第二次我就完全按照这个教程来进行学习ESP32-S3 开发 SPI 屏【DIY 智能手表】_哔哩哔哩_bilibili

        依照这个教程,我们再次找到了另外的LCD官方教程SPI LCD 详解 - - — ESP-IoT-Solution latest 文档 (espressif.com),这次的教程相比之前详细了不少,经过阅读这篇教程(视频前面上其实也是参考了这个教程),我发现我在第一次尝试的时候没有初始化SPI总线 ,也没有配置LCD 驱动 IC 的初始化命令及参数。

        所以根据视频上的流程,我对之前的代码进行了更改,最后呈现出一下状态,由于我没有使用官方的驱动配置头文件,没有想视频中一样修改一下其他同类型的驱动头文件,所以关于驱动头文件里面声明的结构体我直接放在了我自己的头文件当中:

#include "bsp_spi_lcd.h"
// static const char BACK_COLOR[240][240] ;

static const char *TAG = "st7789-example";
static const st7789_lcd_init_cmd_t lcd_init_cmds [] ={
    /* {cmd, { data }, data_size, delay_ms} "*/
    // {0x36, (uint8_t []){0x00}, 1, 0},
    // {0x3A, (uint8_t []){0x05}, 1, 0},
    {0xB2, (uint8_t []){0x0C,0x0C,0x00,0x33,0x33}, 5, 0},
    {0xB7, (uint8_t []){0x35}, 1, 0},
    {0xBB, (uint8_t []){0x19}, 1, 0},
    {0xC0, (uint8_t []){0x2C}, 1, 0},
    {0xC2, (uint8_t []){0x01}, 1, 0},
    {0xC3, (uint8_t []){0x12}, 1, 0},
    {0xC4, (uint8_t []){0x20}, 1, 0},
    {0xC6, (uint8_t []){0x0F}, 1, 0},
    {0xD0, (uint8_t []){0xA4,0xA1}, 2, 0},
    {0xE0, (uint8_t []){0xD0,0x04,0x0D,0x11,0x13,0x2B,0x3F,0x54,0x4C,0x18,0x0D,0x0B,0x1F,0x23}, 14, 0},
    {0xE1, (uint8_t []){0xD0,0x04,0x0C,0x11,0x13,0x2C,0x3F,0x44,0x51,0x2F,0x1F,0x1F,0x20,0x23}, 14, 0},

    {0x21, (uint8_t []){0x00}, 1, 0},
    {0x11, (uint8_t []){0x00}, 1, 120},
    {0x29, (uint8_t []){0x00}, 1, 0},
    {0x2A, (uint8_t []){0x00}, 1, 0},
    {0x2B, (uint8_t []){0x00}, 1, 0},
    {0x2C, (uint8_t []){0x00}, 1, 0},

};

//初始化接口设备
void Lcd_Init(void)
{
    static esp_lcd_panel_io_handle_t io_handle = NULL;
    static esp_lcd_panel_handle_t panel_handle = NULL;
    // static lv_disp_draw_buf_t disp_buf;
    // static lv_disp_drv_t disp_drv;

     /***************************************** 1.初始化总线 **************************************************/
    ESP_LOGI(TAG, "Initialize SPI bus");
    spi_bus_config_t buscfg = {
        .sclk_io_num = PIN_NUM_CLK,   // 连接 LCD SCK(SCL) 信号的 IO 编号
        .mosi_io_num = PIN_NUM_MOSI,  // 连接 LCD MOSI(SDO、SDA) 信号的 IO 编号
        .miso_io_num = -1,  // 连接 LCD MISO(SDI) 信号的 IO 编号,如果不需要从 LCD 读取数据,可以设为 `-1`
        .quadwp_io_num = -1,          // 必须设置且为 `-1`
        .quadhd_io_num = -1,          // 必须设置且为 `-1`
        .max_transfer_sz = LCD_H_RES * LCD_W_RES * sizeof(uint16_t), // 表示 SPI 单次传输允许的最大字节数上限,通常设为全屏大小即可
    };
    ESP_ERROR_CHECK(spi_bus_initialize(SPI3_HOST, &buscfg, SPI_DMA_CH_AUTO));
                                          // 第 1 个参数表示使用的 SPI 主机 ID,和后续创建接口设备时保持一致
                                          // 第 3 个参数表示使用的 DMA 通道号,默认设置为 `SPI_DMA_CH_AUTO` 即可

    /****************************************** 2.创建接口设备 **************************************************/
    ESP_LOGI(TAG, "Install panel IO");
    esp_lcd_panel_io_spi_config_t io_config = {
        .dc_gpio_num = PIN_NUM_DC,
        .cs_gpio_num = -1,       // 连接 LCD CS 信号的 IO 编号,可以设为 `-1` 表示不使用
        .pclk_hz = LCD_PIXEL_CLOCK_HZ,           //Clock out at 20 MHz
        .lcd_cmd_bits = LCD_CMD_BITS,       // 单位 LCD 命令的比特数,应为 8 的整数倍
        .lcd_param_bits = LCD_PARAM_BITS,   // 单位 LCD 参数的比特数,应为 8 的整数倍
        .spi_mode = 3,         // SPI 模式(0-3),需根据 LCD 驱动 IC 的数据手册以及硬件的配置确定(如 IM[3:0])
        .trans_queue_depth = 10,        // SPI 设备传输数据的队列深度,一般设为 10 即可
        // .on_color_trans_done = example_notify_lvgl_flush_ready,   // 单次调用 `esp_lcd_panel_draw_bitmap()` 传输完成后的回调函数
        // .user_ctx = &disp_drv,
        .flags = {            // 以下为 SPI 时序的相关参数,需根据 LCD 驱动 IC 的数据手册以及硬件的配置确定
            .sio_mode = 0,    // 通过一根数据线(MOSI)读写数据,0: Interface I 型,1: Interface II 型
    },
    };
    // Attach the LCD to the SPI bus
    ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)SPI3_HOST, &io_config, &io_handle));

    /***************************************** 3.创建 LCD 设备 ***************************************************/
    ESP_LOGI(TAG, "Install ST7789 panel driver");
    const st7789_vendor_config_t vendor_config = {  // 用于替换驱动组件中的初始化命令及参数
        .init_cmds = lcd_init_cmds,
        .init_cmds_size = sizeof(lcd_init_cmds) / sizeof(st7789_lcd_init_cmd_t),
    };
    esp_lcd_panel_dev_config_t panel_config = {
        .reset_gpio_num = PIN_NUM_RES,    // 连接 LCD 复位信号的 IO 编号,可以设为 `-1` 表示不使用
        .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB,   // 像素色彩的元素顺序(RGB/BGR),
                                                    //  一般通过命令 `LCD_CMD_MADCTL(36h)` 控制
        .bits_per_pixel = 16,  // 色彩格式的位数(RGB565:16,RGB666:18),
                                                    // 一般通过命令 `LCD_CMD_COLMOD(3Ah)` 控制
        .vendor_config = &vendor_config,           // 用于替换驱动组件中的初始化命令及参数
    };
    ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(io_handle, &panel_config, &panel_handle));

    /****************************************** 4.初始化 LCD 设备 *************************************************/
    ESP_ERROR_CHECK(esp_lcd_panel_reset(panel_handle));
    ESP_ERROR_CHECK(esp_lcd_panel_init(panel_handle));
    ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel_handle, true));
    // esp_lcd_panel_draw_bitmap(panel_handle,0,0,240,240,BACK_COLOR);

        当我完成上述代码后,由于视频中是触摸屏,并且还要移植LVGL图形界面,但是我现在还不想一步到位,我只想先实现屏幕能够点亮,能够显示静态的文字跟图片,所以视频后面关于配置触屏I2C以及LVGL的移植代码我就并没有添加。

        到了这一步应该是配置已经事无巨细了,可实际当我满怀信息的去烧录完成等着屏幕点亮,却让我异常失望,屏幕并没有亮,这不禁让我怀疑我的屏是不是有问题。这个想法一下子就让我闻到了成功的气息,而问题的转折点也就出现在这里。

最终尝试

        为了测试我的屏幕是否有问题,我使用了商家给的代码,并且把代码移植到我自己的F103C8T6程序上,结果是能够使用商家的程序成功点亮这块屏幕,看着keil中熟悉的代码,然后我就有了新的想法,我们不要使用乐鑫教程中普遍使用的官方的配置函数,而是自己通过SPI总线向stm32中一样配置发送命令,发送数据的函数,然后所有的流程都严格依照32中的流程进行,这样不就能够可行了吗。

        所以我最后抛弃掉了所有的教程,开始了自己对32控制lcd屏幕的流程的完全移植,到了最后我重要是功夫不负有心人,在第一次尝试后的第二天完成了屏幕点亮。


正片

配置SPI控制器

        由于屏幕使用了SPI协议进行通信,那么我们点亮屏幕的第一步就是配置SPI,这一段在我们之前尝试的时候都有提到,我把代码放在这里,相信大家看到代码后就能理解这一环节到底在做些什么:

#define PIN_NUM_MISO -1
#define PIN_NUM_MOSI 23
#define PIN_NUM_CLK  18
#define PIN_NUM_CS   -1

void vspi_init(void);

static spi_device_handle_t spi;    //创建spi

static const spi_bus_config_t buscfg={   //spi定义
    .miso_io_num=PIN_NUM_MISO, 
    .mosi_io_num=PIN_NUM_MOSI,
    .sclk_io_num=PIN_NUM_CLK,
    .quadwp_io_num=-1,
    .quadhd_io_num=-1,
    .max_transfer_sz=4092
};

static const spi_device_interface_config_t devcfg = {
    .clock_speed_hz = SPI_MASTER_FREQ_40M,
    .mode = 3,
    .spics_io_num   = PIN_NUM_CS,
    .queue_size = 7
};

void vspi_init(void)
{
    spi_bus_initialize(SPI3_HOST,&buscfg,SPI_DMA_CH_AUTO);
    spi_bus_add_device(SPI3_HOST, &devcfg, &spi);
}

        我稍微解释一下,大家可以看到我PIN_NUM_CSPIN_NUM_MISO都定义为了-1,这是因为我们这块屏幕并没有使用这两根线,所以给-1表示不使用此功能。

编写LCD屏幕初始化函数

LCD_Init

/*st7789*/
void LCD_Init(void)
{
    esp_rom_gpio_pad_select_gpio(LCD_RES);   //gpio_pad_select_gpio(LCD_RES)之前
    esp_rom_gpio_pad_select_gpio(LCD_DC);   //gpio_pad_select_gpio(LCD_DC)
    gpio_set_direction(LCD_RES,GPIO_MODE_OUTPUT);    //设置io22为输出模式
    gpio_set_direction(LCD_DC,GPIO_MODE_OUTPUT);    //设置io21为输出模式

	vspi_init();
	LCD_RES_CLR();
	DELAY(20);
	LCD_RES_SET();
	DELAY(20);
	//BLK_OFF();
	// DELAY(100);

	// LCD_WR_REG(0x11); 
	// DELAY(120);

	LCD_WR_REG(0x36);
	LCD_WR_DATA8(0xc0);
	LCD_WR_REG(0x3A); 
	LCD_WR_DATA8(0x05);

	LCD_WR_REG(0xB2);
	LCD_WR_DATA8(0x0C);
	LCD_WR_DATA8(0x0C);
	LCD_WR_DATA8(0x00);
	LCD_WR_DATA8(0x33);
	LCD_WR_DATA8(0x33); 

	LCD_WR_REG(0xB7); 
	LCD_WR_DATA8(0x35);  

	LCD_WR_REG(0xBB);
	LCD_WR_DATA8(0x19);

	LCD_WR_REG(0xC0);
	LCD_WR_DATA8(0x2C);

	LCD_WR_REG(0xC2);
	LCD_WR_DATA8(0x01);

	LCD_WR_REG(0xC3);
	LCD_WR_DATA8(0x12);   

	LCD_WR_REG(0xC4);
	LCD_WR_DATA8(0x20);  

	LCD_WR_REG(0xC6); 
	LCD_WR_DATA8(0x0F);    

	LCD_WR_REG(0xD0); 
	LCD_WR_DATA8(0xA4);
	LCD_WR_DATA8(0xA1);

	LCD_WR_REG(0xE0);
	LCD_WR_DATA8(0xD0);
	LCD_WR_DATA8(0x04);
	LCD_WR_DATA8(0x0D);
	LCD_WR_DATA8(0x11);
	LCD_WR_DATA8(0x13);
	LCD_WR_DATA8(0x2B);
	LCD_WR_DATA8(0x3F);
	LCD_WR_DATA8(0x54);
	LCD_WR_DATA8(0x4C);
	LCD_WR_DATA8(0x18);
	LCD_WR_DATA8(0x0D);
	LCD_WR_DATA8(0x0B);
	LCD_WR_DATA8(0x1F);
	LCD_WR_DATA8(0x23);

	LCD_WR_REG(0xE1);
	LCD_WR_DATA8(0xD0);
	LCD_WR_DATA8(0x04);
	LCD_WR_DATA8(0x0C);
	LCD_WR_DATA8(0x11);
	LCD_WR_DATA8(0x13);
	LCD_WR_DATA8(0x2C);
	LCD_WR_DATA8(0x3F);
	LCD_WR_DATA8(0x44);
	LCD_WR_DATA8(0x51);
	LCD_WR_DATA8(0x2F);
	LCD_WR_DATA8(0x1F);
	LCD_WR_DATA8(0x1F);
	LCD_WR_DATA8(0x20);
	LCD_WR_DATA8(0x23);

	LCD_WR_REG(0x21); 
	LCD_WR_REG(0x11);
	LCD_WR_REG(0x29);

	LCD_Clear(0xFFFF);

}

        这一步就是初始化LCD的最关键的步骤,就是发送命令配置屏幕的参数,这一部分基本上就是参考购买屏幕的商家给的示例程序来进行配置

        可以看到我们在初始化的时候用到了发送命令和发送8位数据的函数,这需要我们自己编写,我这里的所有步骤都是基于对stm32的完全模仿,而且我人物这也是我们初学esp32的最好方式,不要一上来就想着使用官方所提供好的功能函数,因为很可能你根本就搞不明白这些功能函数的到底有什么用处。

LCD_WR_GER

void LCD_WR_REG(uint8_t dat)
{
	LCD_DC_CLR();//写命令
	lcd_cmd(dat);
}

LCD_WR_DATA8

void LCD_WR_DATA8(uint8_t dat)
{
	LCD_DC_SET();//写数据
	lcd_cmd(dat);
}

lcd_cmd

void lcd_cmd(const uint8_t cmd)
{
    esp_err_t ret;
    spi_transaction_t t;
    memset(&t, 0, sizeof(t));       //Zero out the transaction
    t.length=8;                     //Command is 8 bits
    t.tx_buffer=&cmd;               //The data is the cmd itself
    t.user=(void*)0;                //D/C needs to be set to 0
    ret=spi_device_polling_transmit(spi, &t);  //Transmit!
    assert(ret==ESP_OK);            //Should have had no issues.
}

        下面我们就是要解决最后第一个问题了,可以看到的是,我在LCD_Init中最后使用了一个清屏函数,只要我们想办法编写这个刷白函数,就能点亮屏幕,实现全屏颜色显示,而且在上面我们已经编写过传输8位指令或者数据的一个函数lcd_cmd,我们要使用这个函数的话就要循环调用240*240*2次lcd_cmd函数,这样做可以是可以但是有点拖拉,所以在这个函数的基础上,我们可以编写一个一次能够传送指定数据大小的函数:

LCD_data_x

void LCD_data_x(uint16_t *dat,uint32_t len)
{
    spi_transaction_t t;
    memset(&t, 0, sizeof(t));       //Zero out the transaction
    t.length=len;                     //Command is 8 bits
    t.tx_buffer=dat;               //The data is the cmd itself
    t.user=(void*)0;                //D/C needs to be set to 0
    esp_err_t ret=spi_device_polling_transmit(spi, &t);  //Transmit!
    assert(ret==ESP_OK);            //Should have had no issues.
}

        有了这个函数,我们在编写清屏函数时就不用调用如此之多的lcd_cmd了,下面就是我对清屏函数的实现:

void LCD_Clear(uint16_t Color)
{
	LCD_Address_Set(0,0,LCD_W-1,LCD_H-1);
	LCD_DC_SET();//写数据
	uint16_t color_temp[240*2];
	memset(color_temp, Color, sizeof(color_temp));
    for(uint16_t i=0;i<LCD_W/2;i++){
	 	LCD_data_x(color_temp,240*16*2);
	}
}

        可以发现在清屏函数中我一次传输了两行的颜色数据,所以for循环中我只需要循环二分之一的屏幕宽度次就可以了,这样的操作相比于一次传输半个颜色数据在输出效率上会高一些,因为我们使用的16位颜色,但是之前的函数一次才传递8位,意味着我们一个颜色数据就要传输两次才能传输成功,现在这样就显然合理了很多。

        上面的函数里面我们用到了许多宏,这些我都把它放在了头文件中,在下面放出供大家参考使用:

LCD_Init.h

#define LCD_W 240
#define LCD_H 240

#define LCD_RES GPIO_NUM_22
#define LCD_DC  GPIO_NUM_21
#define LCD_BLK GPIO_NUM_12
#define BLK_ON()    gpio_set_level(LCD_BLK,1);
#define BLK_OFF()   gpio_set_level(LCD_BLK,0);

#define LCD_SCL_SET() gpio_set_level(LCD_SCL,1);
#define LCD_SCL_CLR() gpio_set_level(LCD_SCL,0);
#define LCD_SDA_SET() gpio_set_level(LCD_SDA,1);
#define LCD_SDA_CLR() gpio_set_level(LCD_SDA,0);
#define LCD_CS_SET() gpio_set_level(LCD_CS,1);
#define LCD_CS_CLR() gpio_set_level(LCD_CS,0);
#define LCD_RES_SET() gpio_set_level(LCD_RES,1);
#define LCD_RES_CLR() gpio_set_level(LCD_RES,0);

#define LCD_DC_SET() gpio_set_level(LCD_DC,1);
#define LCD_DC_CLR() gpio_set_level(LCD_DC,0);

        至此,大家可以在主函数中尽情的调用LCD_Init函数来对屏幕进行初始化了,相信初始化完成后,你们也能成功点亮自己LCD屏。

  • 32
    点赞
  • 57
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 14
    评论
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

慵懒之龟

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值