查看: 2709|回复: 0

零死角玩转stm32-高级篇之液晶触摸画板

[复制链接]
  • TA的每日心情
    慵懒
    2015-5-29 12:01
  • 签到天数: 11 天

    连续签到: 1 天

    [LV.3]偶尔看看II

    发表于 2013-8-20 17:05:49 | 显示全部楼层 |阅读模式
    分享到:
        4、液晶触摸画板

       4.1实验简介
    本实验向大家介绍如何使用STM32的FSMC接口驱动LCD屏,及使用触摸屏控制器检测触点坐标。
    4.2 LCD控制器简介
    LCD,即液晶显示器,因为其功耗低、体积小,承载的信息量大,因而被广泛用于信息输出、与用户进行交互,目前仍是各种电子显示设备的主流。
    因为STM32内部没有集成专用的液晶屏和触摸屏的控制接口,所以在显示面板中应自带含有这些驱动芯片的驱动电路(液晶屏和触摸屏的驱动电路是独立的),STM32芯片通过驱动芯片来控制液晶屏和触摸屏。以野火3.2寸液晶屏(240*320)为例,它使用ILI9341芯片控制液晶屏,通过TSC2046芯片控制触摸屏。
    4.2.1 ILI9341控制器结构
    液晶屏的控制芯片内部结构非常复杂。最主要的是位于中间GRAM(Graphics RAM),可以理解为显存。GRAM中每个存储单元都对应着液晶面板的一个像素点。它右侧的各种模块共同作用把GRAM存储单元的数据转化成液晶面板的控制信号,使像素点呈现特定的颜色,而像素点组合起来则成为一幅完整的图像。
    框图的左上角为ILI9341的主要控制信号线和配置引脚,根据其不同状态设置可以使芯片工作在不同的模式,如每个像素点的位数是6、16还是18位;使用SPI接口还是8080接口与MCU进行通讯;使用8080接口的哪种模式。MUC通过SPI或8080接口与ILI9341进行通讯,从而访问它的控制寄存器(CR)、地址计数器(AC)、及GRAM。
    在GRAM的左侧还有一个LED控制器(LED Controller)。LCD为非发光性的显示装置,它需要借助背光源才能达到显示功能,LED控制器就是用来控制液晶屏中的LED背光源。

    图 01 ILI9341控制器内部框图

    4.2.2 像素点的数据格式
    图像数据的像素点由红(R)、绿(G)、蓝(B)三原色组成,三原色根据其深浅程度被分为0~255个级别,它们按不同比例的混合可以得出各种色彩。如R:255,G255,B255混合后为白色。根据描述像素点数据的长度,主要分为8、16、24及32位。如以8位来描述的像素点可表示28=256色,16位描述的为216=65536色,称为真彩色,也称为64K色。实际上受人眼对颜色的识别能力的限制, 16位色与12位色已经难以分辨了。
    ILI9341最高能够控制18位的LCD,但为了数据传输简便,我们采用它的16位控制模式,以16位描述的像素点。按照标准格式,16位的像素点的三原色描述的位数为R:G:B =5:6:5,描述绿色的位数较多是因为人眼对绿色更为敏感。16位的像素点格式见图 02。

    图 02 16位像素点格式

    图中的是默认18条数据线时,像素点三原色的分配状况,D1~D5为蓝色,D6~D11为绿色,D13~D17为红色。这样分配有D0和D12位是无效的。若使用16根数据线传送像素点的数据,则D0~D4为蓝色,D5~D10为绿色,D11~D15为红色,使得刚好使用完整的16位。
    RGB比例为5:6:5是一个十分通用的颜色标准,在GRAM相应的地址中填入该颜色的编码,即可控制LCD输出该颜色的像素点。如黑色的编码为0x0000,白色的编码为0xffff,红色为0xf800。
    4.2.3 ILI9341的通讯时序
    目前,大多数的液晶控制器都使用8080或6800接口与MCU进行通讯,它们的时序十分相似,野火以ILI9341使用的8080通讯时序进行分析,实际上ILI9341也可以使用SPI接口来控制。
    ILI9341的8080接口有5条基本的控制信号线:
    用于片选的CSX信号线;
    用于写使能的WRX信号线;
    用于读使能的RDX信号线;
    用于区分数据和命令的D/CX信号线;
    用于复位的RESX信号线。
    其中带X的表示低电平有效。除了控制信号,还有数据信号线,它的数目不定,可根据ILI9341框图中的IM[3:0]来设定,这部分一般由制作液晶屏的厂家完成。为便于传输像素点数据,野火使用的液晶屏设定为16条数据线D[15:0]。使用8080接口的写命令时序图见错误!未找到引用源。。

    图 03 使用18条数据线的8080接口写命令时序

    由图可知,写命令时序由CSX信号线拉低开始,D/CX信号线也置低电平表示写入的是命令地址(可理解为命令编码,如软件复位命令:0x01),以WRX信号线为低,RDX信号为高表示数据传输方向为写入,同时,在数据线[17:0]输出命令地址,在第地二个传输阶段传送的为命令的参数,所以D/CX要置高电平,表示写入的是命令数据
    当我们需要向GRAM写入数据的时候,把CSX信号线拉低后,把D/CX信号线置为高电平,这时由D[17:0]传输的数据则会被ILI9341保存至它的GRAM中。
    4.3 用STM32驱动LCD
    ILI9341的8080通讯接口时序可以由STM32使用普通I/O接口进行模拟,但这样效率较低,它提供了一种特别的控制方法——使用FSMC接口。
    FSMC简介
    FSMC(flexible static memory controller),译为静态存储控制器。可用于STM32芯片控制NOR FLASH、PSRAM、和NAND FLASH存储芯片。其结构见图 04。

    图 04 FSMC结构图

    我们是使用FSMC的NORPSRAM模式控制LCD,所以我们重点分析框图中NOR FLASH控制信号线部分。控制NOR FLASH主要使用到如下信号线:

    图 05 FSMC控制NOR FLASH的信号线

    根据STM32对寻址空间的地址映射,见前面的错误!未找到引用源。,地址0x6000 0000 ~0x9FFF FFFF是映射到外部存储器的,而其中的0x6000 0000 ~0x6FFF FFFF则是分配给NOR FLASH、PSRAM这类可直接寻址的器件。当FSMC外设被配置为正常工作,并且外部接了NOR FLASH,这时若向0x60000000地址写入数据0xffff,FSMC会自动在各信号线上产生相应的电平信号,写入数据。该过程的时序图见图 06。

    图 06 FSMC写NOR时序图

    它会控制片选信号NE[X]选择相应的某块NOR 芯片,然后使用地址线A[25:0]输出0x60000000,在NEW写使能信号线上发出写使能信号,而要写入的数据信号0xffff则从数据线D[15:0]输出,然后数据就被保存到NOR FLASH中了。
    用FSMC模拟8080时序
    在图 06的时序图中NADV信号是在地址、信号线复用时作为锁存信号的,在此,我们略它。然后读者会发现,这个FSMC写NOR时序是跟8080接口的时序(见图 03)是十分相似的,对它们的信号线对比如下:
               8080信号线            功能    FSMC-NOR信号线        功能
    CSX片选信号NEx片选
    WRX写使能NWR写使能
    RDX读使能NOE读使能
    D[15:0]数据信号D[15:0]数据信号
    DCX数据/命令选择A[25:0]地址信号
    前四种信号线都是完全一样的,仅在8080的数据命令选择线与FSMC的地址信号线有区别。为了模拟出8080时序,我们把FSMC的A0地址线(也可以使用其它地址线)连接8080的DCX,即A0为高电平时,数据线D[15:0]的信号会被理解ILI9341为数值,若A0为低电平时,传输的信号则会被理解为命令
    也就是说,当向地址为0x6xxx xxx1、0x6xxx xxx3、0x6xxx xxx5…这些奇数地址写入数据时,地址线A0(D/CX)会为高电平,这个数据被理解为数值;若向0x6xxx xxx0 、0x6xxx xxx2、0x6xxx xxx4…这些偶数地址写入数据时,地址线A0(D/CX)会为低电平,这个数据会被理解为命令
    有了这个基础,只要我们在代码中利用指针变量,向不同的地址单元写入数据,就能够由FSMC模拟出的8080接口向ILI9341写入控制命令或GRAM的数据了。
    4.3.1 触摸屏感应原理
    触摸屏常与液晶屏配套使用,组合成为一个可交互的输入输出系统。除了熟悉的电阻、电容屏外,触摸屏的种类还有超声波屏、红外屏。由于电阻屏的控制系统简单、成本低,且能适应各种上恶劣环境,被广泛采用。
    电阻触摸屏的基本原理为分压,它由一层或两层阻性材料组成,在检测坐标时,在阻性材料的一端接参考电压Vref,另一端接地,形成一个沿坐标方向的均匀电场。当触摸屏受到挤压时,阻性材料与下层电极接触,阻性材料被分为两部分,因而在触摸点的电压,反映了触摸点与阻性材料的Vref端的距离,而且为线性关系,而该触点的电压可由ADC测得。更改电场方向,以同样的方法,可测得另一方向的坐标。
    4.3.2 TSC2046触摸屏控制器
    TSC2046是专用在四线电阻屏的触摸屏控制器,MCU可通过SPI接口向它写入控制字,由它测得X、Y方向的触点电压返回给MCU。见图 07。

    图 07 TSC2046与电阻屏的连接图

    图中,电阻屏两层阻性材料的两端分别接入到TSC2046的X+、X-和Y+、Y-。当要测量X坐标时,MCU通过SPI接口写命令到TSC2046,使它通过内部的模拟开关使X+、X-接通电源,于是在电阻屏的X方向上产生一个匀强电场;把Y+、Y-连接到TSC2046的ADC。当电阻屏被触摸时,上、下两层的阻性材料接触,在PENIRQ引脚产生一个中断信号,通知MCU。该触点的电压由Y+或Y-(此时的Y+Y-电阻很小,可忽略)引入到ADC进行测量,MCU读取该电压,进行软件转换,就可以测得触点X方向的坐标。同理可以测得Y方向的坐标。
    4.4 实验讲解
    4.4.1 实验描述及工程文件清单
    实验描述 :野火STM32开发板驱动配套的3.2寸液晶、触摸屏,使用FSMC接口控制该屏幕自带的液晶控制器ILI9341,使用SPI接口与触摸屏控制器TSC2046通讯。驱动成功后可在屏幕上使用基本的触摸绘图功能。
    硬件连接: TFT 数据线
    PD14-FSMC-D0 ----LCD-DB0
    PD15-FSMC-D1 ----LCD-DB1
    PD0-FSMC-D2 ----LCD-DB2
    PD1-FSMC-D3 ----LCD-DB3
    PE7-FSMC-D4 ----LCD-DB4
    PE8-FSMC-D5 ----LCD-DB5
    PE9-FSMC-D6 ----LCD-DB6
    PE10-FSMC-D7 ----LCD-DB7
    PE11-FSMC-D8 ----LCD-DB8
    PE12-FSMC-D9 ----LCD-DB9
    PE13-FSMC-D10 ----LCD-DB10
    PE14-FSMC-D11 ----LCD-DB11
    PE15-FSMC-D12 ----LCD-DB12
    PD8-FSMC-D13 ----LCD-DB13
    PD9-FSMC-D14 ----LCD-DB14
    PD10-FSMC-D15 ----LCD-DB15
    TFT 控制信号线
    PD4-FSMC-NOE ----LCD-RD
    PD5-FSMC-NEW ----LCD-WR
    PD7-FSMC-NE1 ----LCD-CS
    PD11-FSMC-A16 ----LCD-DC
    PE1-FSMC-NBL1 ----LCD-RESET
    PD13-FSMC-A18 ----LCD-BLACK-LIGHT
    触摸屏TSC2046控制线
    PA5-SPI1-SCK ----TSC2046-SPI -SCK
    PA7-SPI1-MOSI ----TSC2046-SPI - MOSI
    PA6-SPI1-MISO ----TSC2046-SPI – MISO
    PB7-I2C1-SDA ----TSC2046-SPI-CS
    PB6-I2C1-SCL ----TSC2046- INT_IRQ
    用到的库文件: startup/start_stm32f10x_hd.c
    CMSIS/core_cm3.c
    CMSIS/system_stm32f10x.c
    FWlib/misc.c
    FWlib/stm32f10x_spi.c
    FWlib/stm32f10x_rcc.c
    FWlib/stm32f10x_exti.c
    FWlib/stm32f10x_gpio.c
    FWlib/stm32f10x_fsmc.c
    用户编写的文件: USER/main.c
    USER/stm32f10x_it.c
    USER/lcd.c
    USER/SysTick.c
    USER/lcd_botton.c
    USER/Touch.c
    野火STM32开发板3.2寸LCD硬件连接图(兼容V1版本的2.4寸屏)

    4.4.2配置工程环境
    本LCD触摸屏画板实验中我们用到了GPIO、RCC、SPI、EXTI、FSMC外设,所以我们先要把以下库文件添加到工程:stm32f10x_gpio.c 、stm32f10x_rcc.c、stm32f10x_spi.c、stm32f10x_exti.c、stm32f10x_fsmc.c。由于在TSC2046的触摸检测中使用了中断,所以还要把misc.c文件添加进工程。
    本工程使用了旧的用户文件SysTick.c,用作定时,把它添加到新工程之中,并新建 lcd_botton.c、lcd.c、Touch.c及相应的头文件。其中lcd_botton.c文件定义了最底层的LCD控制函数,LCD上层的函数如画点、显示字符等位于lcd.c文件中。
    最后在 stm32f10x_conf.h 中把使用到的ST库的头文件注释去掉。
    /**
    **********************************************************
    * @file    Project/STM32F10x_StdPeriph_Template/stm32f10x_conf.h
    * @author  MCD Application Team
    * @version V3.5.0
    * @date    08-April-2011
    * @brief   Library configuration file.
    ******************************************************/
    #include "stm32f10x_exti.h"
    #include "stm32f10x_fsmc.h"
    #include "stm32f10x_gpio.h"
    #include "stm32f10x_rcc.h"
    #include "stm32f10x_spi.h"
    #include "misc.h"
    4.4.3 main文件
    从本工程的main文件分析代码的执行流程:
    /*
    * 函数名:main
    * 描述  :主函数
    * 输入  :无
    * 输出  :无
    */
    int main(void)
    {
    SysTick_Init();                     /*systick 初始化*/
    LCD_Init();                         /*LCD初始化*/
    Touch_init();                       /*触摸初始化*/
    while(Touchl_Calibrate() !=0);      /*等待触摸屏校准完毕*/
    Init_Palette();                     /*画板初始化*/
    while (1)
    {
    if(touch_flag == 1)         /*如果触笔按下了*/
    {
    /*获取点的坐标*/
    if(Get_touch_point(&display, Read_2046_2(), &touch_para ) !=DISABLE)
    {
    /*画点*/
    Palette_draw_point(display.x,display.y);                                                /*画点*/
    }
    }
    }
    }
    其执行流程如下:
    1、调用SysTick_Init()、LCD_Init()、Touch_init()初始化了STM32的Systick、FSMC、SPI外设,并用FSMC和SPI接口初始化了ILI9341和TSC2046控制器。
    2、调用Touch1_Calibrate()函数进行触摸屏校准,使得触摸屏与液晶屏的坐标匹配。
    3、调用Init_Palette()函数初始化触摸画板的应用程序,使得在LCD上显示画板界面,并能够正常响应触摸屏的信号。
    4、第16~27行的while循环,通过不断检测触笔按下标志touch_flag,判断触摸屏是否被触笔按下。触摸屏控制器TSC2046检测到触笔信号时,由它的PENIRQ引脚触发STM32的中断,在中断服务函数中对touch_flag标志置1。
    5、第21行,若检测到触笔按下,调用Get_touch_point()函数读取TSC2046的寄存器,获得与触点的X、Y坐标相关的电压信号,转化成LCD的X、Y坐标。
    6、获取了触点坐标后,使LCD液晶屏在该坐标点显示为对应的颜色。
    7、循环触点捕捉、画点过程,就实现了触摸画板的功能。
    4.4.5 初始化FSMC模式
    4.4.5.1初始化液晶屏流程
    在main函数中调用的LCD_Init()函数,它对液晶控制器ILI9341用到的GPIO、FSMC接口进行了初始化,并且向该控制器写入了命令参数,配置好了LCD液晶屏的基本功能。其函数定义位于lcd_botton.c文件,如下:
    /*****************************************
    * 函数名:LCD_Init
    * 描述  :LCD 控制 I/O 初始化
    *         LCD FSMC 初始化
    *         LCD 控制器 HX8347 初始化
    * 输入  : 无
    * 输出  :无
    * 举例  :无
    * 注意  :无
    ******************************************/
    void LCD_Init(void)
    {
    unsigned long i;
    LCD_GPIO_Config();      //初始化使用到的GPIO
    LCD_FSMC_Config();      //初始化FSMC模式
    LCD_Rst();              //复位LCD液晶屏
    Lcd_init_conf();        //写入命令参数,对液晶屏进行基本的初始化配置
    Lcd_data_start();       //发送写GRAM命令
    for(i=0; i<(320*240); i++)
    {
    LCD_WR_Data(GBLUE); //发送颜色数据,初始化屏幕为GBLUE颜色
    }
    }
    LCD_Init()函数执行后,最直观的结果是使LCD整个屏幕显示编码为0X07FF的GBLUE颜色。
    函数中调用的LCD_GPIO_Config()主要工作是把液晶屏(不包括触摸屏)中使用到的GPIO引脚和使能外设时钟,除了背光、复位用的PD13PD1设置为通用推挽输出外,其它的与FSMC接口相关的地址信号、数据信号、控制信号的端口均设置为复用推挽输出
    4.4.5.2初始化FSMC模式
    接下来LCD_Init()函数调用LCD_FSMC_Config()设置FSMC的模式,我们的目的是使用它的NOR FLASH模式模拟出8080接口,在LCD接口中我们使用的是FSMC地址线A16作为8080的D/CX命令选择信号的。LCD_FSMC_Config()具体代码如下:
    /*******************************************
    * 函数名:LCD_FSMC_Config
    * 描述  :LCD  FSMC 模式配置
    * 输入  : 无
    * 输出  :无
    * 举例  :无
    * 注意  :无
    *********************************************/
    void LCD_FSMC_Config(void)
    {
    FSMC_NORSRAMInitTypeDef  FSMC_NORSRAMInitStructure;
    FSMC_NORSRAMTimingInitTypeDef  p;
    p.FSMC_AddressSetupTime = 0x02;  //地址建立时间
    p.FSMC_AddressHoldTime = 0x00;   //地址保持时间
    p.FSMC_DataSetupTime = 0x05;         //数据建立时间
    p.FSMC_BusTurnAroundDuration = 0x00;   //总线恢复时间
    p.FSMC_CLKDivision = 0x00;            //时钟分频
    p.FSMC_DataLatency = 0x00;            //数据保持时间
    p.FSMC_AccessMode = FSMC_AccessMode_B;   //在地址数据线不复用的情况下,ABCD模式的区别不大
    //本成员配置只有使用扩展模式才有效
    FSMC_NORSRAMInitStructure.FSMC_Bank = FSMC_Bank1_NORSRAM1;           //NOR FLASH的BANK1
    FSMC_NORSRAMInitStructure.FSMC_DataAddressMux = FSMC_DataAddressMux_Disable;      //数据线与地址线不复用
    FSMC_NORSRAMInitStructure.FSMC_MemoryType = FSMC_MemoryType_NOR;                  //存储器类型NOR FLASH
    FSMC_NORSRAMInitStructure.FSMC_MemoryDataWidth = FSMC_MemoryDataWidth_16b;        //数据宽度为16位
    FSMC_NORSRAMInitStructure.FSMC_BurstAccessMode = FSMC_BurstAccessMode_Disable;    //使用异步写模式,禁止突发模式
    FSMC_NORSRAMInitStructure.FSMC_WaitSignalPolarity = FSMC_WaitSignalPolarity_Low;  //本成员的配置只在突发模式下有效,等待信号极性为低
    FSMC_NORSRAMInitStructure.FSMC_WrapMode = FSMC_WrapMode_Disable;                 //禁止非对齐突发模式
    FSMC_NORSRAMInitStructure.FSMC_WaitSignalActive = FSMC_WaitSignalActive_BeforeWaitState;   //本成员配置仅在突发模式下有效。NWAIT信号在什么时期产生
    FSMC_NORSRAMInitStructure.FSMC_WaitSignal = FSMC_WaitSignal_Disable;              //本成员的配置只在突发模式下有效,禁用NWAIT信号
    FSMC_NORSRAMInitStructure.FSMC_WriteBurst = FSMC_WriteBurst_Disable;              //禁止突发写操作
    FSMC_NORSRAMInitStructure.FSMC_WriteOperation = FSMC_WriteOperation_Enable;        //写使能
    FSMC_NORSRAMInitStructure.FSMC_ExtendedMode = FSMC_ExtendedMode_Disable;          //禁止扩展模式,扩展模式可以使用独立的读、写模式
    FSMC_NORSRAMInitStructure.FSMC_ReadWriteTimingStruct = &p;                         //配置读写时序
    FSMC_NORSRAMInitStructure.FSMC_WriteTimingStruct = &p;                             //配置写时序
    FSMC_NORSRAMInit(&FSMC_NORSRAMInitStructure);
    /* 使能 FSMC Bank1_SRAM Bank */
    FSMC_NORSRAMCmd(FSMC_Bank1_NORSRAM1, ENABLE);
    }
    本函数主要使用了两种类型的结构体对FSMC进行配置,第一种为 FSMC_NORSRAMInitTypeDef类型的结构体主要用于NOR FLASH的模式配置,包括存储器类型、数据宽度等。另一种的类型为FSMC_NORSRAMTimingInitTypeDef,这是用于配置FSMC的 NOR FLASH模式下读写时序中的地址建立时间、地址保持时间等,代码中用它定义了结构体p,这个第二种类型的结构体在前一种结构体中被指针调用。
    下面先来分析FSMC_NORSRAMInitTypeDef的结构体成员:
    1、FSMC_Bank
    用于选择外接存储器的区域(或地址),见图 08 FSMC存储块,STM32的存储器映射中,把0x6000 0000~0x9fff ffff的地址都映射到被FSMC控制的外存储器中,其中属于NOR FLASH的为0x6000 0000~0x6fff ffff。而属于NOR FLASH的这部分地址空间又被分为4份,每份大小为64MB,编号为BANK1 ~BANK4。分BANK是由FSMC寻址范围决定的,该接口的地址线最多为26条,即最大寻址空间为226 =64MB。为了扩展寻址空间,可把地址与数据线与多片NOR FLASH并联,由不同的片选信号NE[3:0]区分不同的BANK。
    在本实验中,我们使用的是FSMC的信号线NE1作为控制8080的CSX片选信号,所以我们把本成配置为FSMC_Bank1_NORSRAM1 (NE1片选BANK1)。

    图 08 FSMC存储块

    2、FSMC_DataAddressMux
    本成员用于配置FSMC的数据线与地址线是否复用。FSMC支持数据与地址线复用或非复用两种模式。在非复用模式下16位数据线及26位地址线分开始用;复用模式则低16位数据/地址线复用。在复用模式下,推荐使用地址锁存器以区分数据与地址。当NADV信号线为低时,复用信号线ADx(x=0…15)上出现地址信号Ax,当NADV变高时,ADx上出现数据信号Dx。
    本实验中用FSMC模拟8080接口,地址线A16提供8080的D/CX信号,实际上就只使用了这一条地址线,IO资源并不紧张,所以把本成员配置为FSMC_DataAddressMux_Disable (非复用模式)。
    3、FSMC_MemoryType
    本成员用于配置FSMC外接的存储器的类型,可被配置为NOR FLASH模式、PSARM模式及SRAM模式。
    在本实验的应用中,由于NOR FLASH模式的时序与8080更接近,所以本结构体被配置为FSMC_MemoryType_NOR(NOR FLASH模式)。
    4、FSMC_MemoryDataWidth
    本成员用于设置FSMC接口的数据宽度,可被设置为8Bit或16bit。对于16位宽度的外部存储器。在STM32地址映射到FSMC接口的结构中,HADDR信号线是需要转换到外部存储器的内部AHB地址线,是字节地址
    若存储器的数据线宽为8Bit,FSMC的26条地址信号线FSMC_A[25:0]直接可以引入到与AHB相连的HADDR[25:0],26条字节地址信号线最大寻址空间为64MB。见图 09。

    图 09 外部存储器地址

    若存储器的数据线宽16Bit,则存储器的地址信号线是半字地址(16Bit) 。为了使HADDR的字节地址信号线与存储器匹配,FSMC的25条地址信号线FSMC_A[24:0]与HADDR[25:1]相连,由于变成了半字地址(16Bit),仅需要25半字字地址信号线就达到最大寻址空间64MB。正因地址线的不对称相连,16bit数据线宽下,实际的访问地址为右移一位之后的地址。
    本实验中8080接口采用16bit模式,所以我们把本成员配置为FSMC_MemoryDataWidth_16b,由于地址线不对称相连,这会影响到我们用地址信号线A16控制的8080接口的D/CX信号
    5、FSMC_BurstAccessMode
    本成员用于配置访问模式。FSMC对存储器的访问分为异步模式突发模式(同步模式)。在异步模式下,每次传送数据都需要产生一个确定的地址,而突发模式可以在开始始提供一个地址之后,把数据成组地连续写入。
    本实验中使用FSMC模拟8080端口,更适合使用异步模式,因而向本成员赋值为FSMC_WriteBurst_Disable。
    6、突发模式参数配置
    代码中的30~34行,都是关于使用突发模式时的一些参数配置,这些成员为: FSMC_WaitSignalPolarity(配置等待信号极性)、FSMC_WrapMode(配置是否使用非对齐方式)、FSMC_WaitSignalActive(配置等待信号什么时期产生)、FSMC_WaitSignal(配置是否使用等待信号)、FSMC_WriteBurst(配置是否允许突发写操作),这些成员均需要在突发模式开启后配置才有效。
    这些成员在开启突发模式时才有效,本实验使用的是异步模式,所以这些成员的参数没有意义。
    7、FSMC_WriteOperation
    本成员用于配置写操作使能,如果禁止了写操作,FSMC不会产生写时序,但仍可从存储器中读出数据。
    本实验需要写时序,所以向本成员赋值为FSMC_WriteOperation_Enable(写使能)
    8、FSMC_ExtendedMode
    本成员用于配置是否使用扩展模式,在扩展模式下,读时序和写时序可以使用独立时序模式。如读时序使用模式A,写时序使用模式B,这些A、B、C、D模式实际上差别不大,主要是在使用数据/地址线复用的情况下,NADV信号产生的时序不一样,具体的时序图可查阅《STM32参考手册》。
    本实验中数据/地址线不复用,所以读写时序中不同的NADV信号并没影响,禁止使用扩展模式SMC_ExtendedMode_Disable。
    9、FSMC_ReadWriteTimingStruct及FSMC_WriteTimingStruct
    这两个参数分别用来设置FSMC的读时序及写时序的时间参数。若使用了扩展模式,则前者配置的是读时序,后者为写时序;若禁止了扩展模式,则读写时序都使用FSMC_ReadWriteTimingStruct结构体中的参数。
    在配置这两个参数时,使用的是类型FSMC_NORSRAMTimingInitTypeDef时序初始化结构体,对这种类型结构体的成员进行赋值。它的成员分别有:FSMC_AddressSetupTime(地址建立时间)、FSMC_AddressHoldTime(地址保持时间)、FSMC_DataSetupTime(数据建立时间)、FSMC_DataLatency(数据保持时间) 、FSMC_BusTurnAroundDuration(总线恢复时间)、FSMC_CLKDivision(时钟分频)、FSMC_AccessMode(访问模式)。对以上各个时间成员赋的数值X表示X个时钟周期,它的时钟是由HCLK经过成员时钟分频得来的,该分频值在成员FSMC_CLKDivision(时钟分频)中设置。其中FSMC_AccessMode(访问模式)成员的设置只在开启了扩展模式才有效,而且开启了扩展模式后,读时序和写时序的设置可以是独立的。
    本实验中的时序设置是根据ILI9341的datasheet设置的,调试的时候可以先把这些值设置得大一些,然后慢慢靠近datasheet要求的最小值,这样会取得比较好的效果。时序的参数设置对LCD的显示效果有一定的影响。
    配置完初始化结构体后,要调用库函数FSMC_NORSRAMInit()把这些配置参数写到控制寄存器,还要调用 FSMC_NORSRAMCmd()使能BANK1。如果是使用FSMC配置其它存储器如NAND FLASH,要使用其它的库函数及初始化结构体。
    4.4.6 FSMC模拟8080读写参数、命令
    回到LCD_Init()函数的执行流程,初始化完成FSMC接口后,就可以使用它控制ILI9341了。在LCD_Init()中调用了Lcd_init_conf()函数向ILI9341写入了一系列的控制参数:
    /**********************************
    * 函数名:Lcd_init_conf
    * 描述  :ILI9341 LCD寄存器初始配置
    * 输入  : 无
    * 输出  :无
    * 举例  :无
    * 注意  :无
    *************************************/
    void Lcd_init_conf(void)
    {
    DEBUG_DELAY();
    LCD_ILI9341_CMD(0xCF);
    LCD_ILI9341_Parameter(0x00);
    LCD_ILI9341_Parameter(0x81);
    LCD_ILI9341_Parameter(0x30);
    DEBUG_DELAY();
    LCD_ILI9341_CMD(0xED);
    LCD_ILI9341_Parameter(0x64);
    LCD_ILI9341_Parameter(0x03);
    LCD_ILI9341_Parameter(0x12);
    LCD_ILI9341_Parameter(0x81);
    DEBUG_DELAY();
    LCD_ILI9341_CMD(0xE8);
    LCD_ILI9341_Parameter(0x85);
    LCD_ILI9341_Parameter(0x10);
    LCD_ILI9341_Parameter(0x78);
    // ………………此处省略几十行…………
    本函数十分长,由于篇幅问题,以上只是该函数其中的一部分,省略部分的代码也是这样的模板,只是写入的命令和参数不一样而已,这些命令和参数设置了像素点颜色格式、屏幕扫描方式、横屏竖屏等初始化配置,这些命令的意义从ILI9341的datasheet命令列表中可以查到。该函数通过调用LCD_ILI9341_CMD()写入命令,用LCD_ILI9341_Parameter()写入参数。它们实质是两个宏:
    /*******************************************************/
    #define LCD_ILI9341_CMD(index)       LCD_WR_REG(index)
    #define LCD_ILI9341_Parameter(val)   LCD_WR_Data(val)
    /****为了移植方便,上面的宏只是封装,以下才是最底层的宏**********
    * 选择BANK1-BORSRAM1 连接 TFT,地址范围为0X60000000~0X63FFFFFF
    * FSMC_A16 接LCD的DC(寄存器/数据选择)脚
    * 16 bit => FSMC[24:0]对应HADDR[25:1]
    * 寄存器基地址 = 0X60000000
    * RAM基地址 = 0X60020000 = 0X60000000+2^16*2 = 0X60000000 + 0X20000 = 0X60020000
    * 当选择不同的地址线时,地址要重新计算。
    */
    #define Bank1_LCD_D    ((u32)0x60020000)       //Disp Data ADDR
    #define Bank1_LCD_C    ((u32)0x60000000)       //Disp Reg ADDR
    /*选定LCD指定寄存器(命令编码)*/
    #define LCD_WR_REG(index)    ((*(__IO u16 *) (Bank1_LCD_C)) = ((u16)index))
    /*往LCD 写入数据*/
    #define LCD_WR_Data(val)       ((*(__IO u16 *) (Bank1_LCD_D)) = ((u16)(val)))
    这部分是FSMC模拟8080接口的精髓。
    读写参数、命令
    先来看第21行的宏,LCD_WR_Data(val),这是一个带参宏,用于向LCD控制器写入参数,参数为val。它的宏展开为:
    ((*(__IO u16 *) (Bank1_LCD_D)) = ((u16)(val)))
    宏展开中的(Bank1_LCD_D)是一个在第14行定义的宏,它的值为0x6002 0000,实质是一个地址,这个地址的计算在后面介绍。
    (__IO u16 *) (Bank1_LCD_D)表示把(Bank1_LCD_D)强制转换成一个16位的地址。((*(__IO u16 *) (Bank1_LCD_D))表示再对这个地址作” * ”指针运算,取该指针对象的内容,并把它的内容赋值为 = ((u16)(val)))。所以整个宏的操作就是:把参数val写入到地址为0x6002 0000的地址空间。
    由于这个地址被STM32映射到外存储器,所以会由FSMC外设以访问NOR FLASH的形式、时序,在地址线上发出0x6002 0000地址信号,在数据线上发出val数据信号,写入参数到外存储器中。而FSMC接口又被我们模拟成了8080接口,最终val被8080接口理解为参数,传输到ILI9341控制器中。
    计算地址
    见图 010。计算地址前,再明确一下在本实验中,使用的是FSMC_NE1作为8080_CS片选信号,以FSMC_A16作为8080_D/CX 数据/命令信号(图中为RS,意义相同)。

    图 010 FSMC与8080端口连接简图

    按这种连接时,FSMC_NE1为低电平、FSMC_A16为高电平,表示通过D[15:0]发送接收的数据被8080接口解释为参数(数值),当我们访问0x6002 0000这个地址的时候,正好符合这个条件。该地址的计算过程如下:
    由于选择的是使用FSMC_NE1片选信号线,片选的为BANK1,所以基地址为0x6000 0000。要把地址线FSMC_A16置为高电平,可以采用下列算式:
    0x6000 0000 |= 1<<16;  //结果 = 0x6001 0000
    但是,这样计算出来的地址只是数据线为8Bit模式下的字节地址。由于我们采用的是16Bit数据线,FSMC[24:0]与HADDR[25:1]对齐,HADDR地址要左移一位才是FSMC的访问地址。因此为了把FSMC中的FSMC_A16 (D/CX线)置1,实际上要对应到HADDR地址(AHB地址)的HADDR_A17,即正确的计算地址公式应为:
    0x6000 0000 |= 1<<(16+1);  //此为正确结果 = 0x6002 0000
    对于16位数据线模式,能使FSMC_NEX为低电平,FSMC_A16为高电平的地址,并不只有0x6002 0000一个,只要是属于0X60000000~0X63FFFFFF范围内(BANK1地址范围),HADDR_A17 位为高电平的地址均可,这是因为我们只采用了FSMC_A16用于8080的D/CX信号,所以地址线的电平状态并无影响。如0x6002 0001地址,在本实验中是与0x6002 0000等价的。
    若修改这个地址,使FSMC_NEX为低电平,FSMC_A16为低电平,即D/CX被置0,8080会把由数据线传输的信号理解为命令。以同样的方式计算,符合这样要求的其中一个地址为0x6000 0000 ,向这个地址空间赋值,这个值最终会被8080接口解释为命令。宏LCD_WR_REG(index) 就是这样实现的。
    给整个屏幕上色
    再次回到LCD_Init()函数,它调用完Lcd_init_conf()初始化了液晶屏后,向使用Lcd_data_start()函数发送了一个命令——写GRAM内容,即后面发送的数据都被解析为显示到屏幕像素点的数据。代码中使用for循环把语句LCD_WR_Data(GBLUE)执行了320*240次,即把所有像素点都显示为GBLUE颜色。
    4.4.6.1液晶屏画点函数
    初始化了液晶屏后,就可以控制液晶上每个像素点的颜色了。如果能够实现一个画点函数,在指定的(x,y)坐标像素点上显示指定的颜色,那么就能够实现一切液晶屏最复杂的显示功能,如在液晶屏指定位置显示形状、文字、图像,都可以通过调用画点函数或以类似的方式控制液晶的像素点。本实验中的画点函数LCD_Point()lcd.c文件中定义如下:
    /******************************************************
    * 函数名:LCD_Point
    * 描述  :在指定坐标处显示一个点
    * 输入  : -x 横向显示位置 0~319
    -y 纵向显示位置 0~239
    * 输出  :无
    * 举例  :    LCD_Point(100,200);
    LCD_Point(10,200);
    LCD_Point(300,220);
    * 注意  :    (0,0)位置为液晶屏左上角 已测试
    *********************************************************/
    void LCD_Point(u16 x,u16 y)
    {
    LCD_open_windows(x,y,1,1);
    LCD_WR_Data(POINT_COLOR);
    }
    本函数首先调用了一个液晶显示窗口函数LCD_open_windows()用于开辟液晶屏上的显示区域,后面写入的颜色数据将被显示到该区域中。示窗函数按(x,y,1,1)的参数调用时,该函数开辟了一个坐标为(x,y)的像素点。接着调用LCD_WR_Data()向ILI9341写入颜色数据,像素的坐标即为示窗函数开辟的显示坐标。于是LCD_Point()函数就实现了控制特定坐标的像素点。
    接下来分析LCD_open_windows()函数是如何开辟显示区域的:
    /**********************************
    * 函数名:LCD_open_windows
    * 描述  :开窗(以x,y为坐标起点,长为len,高为wid)
    * 输入  : -x    窗户起点
    -y      窗户起点
    -len  窗户长
    -wid 窗户宽
    * 输出  :无
    * 举例  :无
    * 注意  :无
    *************************************/
    void LCD_open_windows(u16 x,u16 y,u16 len,u16 wid)
    {
    if(display_direction == 0)      /*如果是横屏选项*/
    {
    LCD_ILI9341_CMD(0X2A);
    LCD_ILI9341_Parameter(x>>8);    //start 起始位置的高8位
    LCD_ILI9341_Parameter(x-((x>>8)<<8));  //起始位置的低8位
    LCD_ILI9341_Parameter((x+len-1)>>8);    //end 结束位置的高8位
    LCD_ILI9341_Parameter((x+len-1)-(((x+len-1)>>8)<<8));  //结束位置的低8位
    LCD_ILI9341_CMD(0X2B);
    LCD_ILI9341_Parameter(y>>8);   //start
    LCD_ILI9341_Parameter(y-((y>>8)<<8));
    LCD_ILI9341_Parameter((y+wid-1)>>8);   //end
    LCD_ILI9341_Parameter((y+wid-1)-(((y+wid-1)>>8)<<8));
    }
    else
    {
    LCD_ILI9341_CMD(0X2B);
    LCD_ILI9341_Parameter(x>>8);
    LCD_ILI9341_Parameter(x-((x>>8)<<8));
    LCD_ILI9341_Parameter((x+len-1)>>8);
    LCD_ILI9341_Parameter((x+len-1)-(((x+len-1)>>8)<<8));
    LCD_ILI9341_CMD(0X2A);
    LCD_ILI9341_Parameter(y>>8);
    LCD_ILI9341_Parameter(y-((y>>8)<<8));
    LCD_ILI9341_Parameter((y+wid-1)>>8);
    LCD_ILI9341_Parameter((y+wid-1)-(((y+wid-1)>>8)<<8));
    }
    LCD_ILI9341_CMD(0x2c);
    }
    本函数主要使用了三个ILI9341的控制命令:
    1、0x2A列地址控制命令
    用于设置显示区域的列像素区域。它有四个参数,分别为SC的高8位、SC的低8位及EC的高8位、EC的低8位。SC和EC的意义见图 011。SC表示要控制的显示区域的列起始坐标EC表示显示区域的列结束坐标

    图 011 列控制命令SC和EC

    2、0x2B页地址控制命令
    用于设置显示区域的 页(行)像素区域。与上一命令类似,也有四个参数,分别为SP的高8位、SP的低8位及EP的高8位、EP的低8位。SP和EP的意义见错误!未找到引用源。。SP表示要控制的显示区域的行起始坐标,EP表示显示区域的行结束坐标。

    3、0x2C写RAM命令
    本命令用于表示开始写入像素显示数据,紧跟着本命令后面的即为写入到GRAM的RGB 5:6:5的颜色数据。在初始化液晶屏函数中也调用到本命令。使用本命令,后面的颜色数据按照一个接着一个预设的扫描方式(扫描方式决定了是横屏和坚屏)写入到由0x2A和0x2B设置的显示区域中。横屏扫描方式为从左到右,扫描完一行(页)像素点再扫描下一行(页)。竖屏扫描为从上到下,扫描完一列再扫描下一列。
    了解这三个命令后,就知道示窗函数的执行流程了。

    调用本示窗函数后,就可以开辟一个起始坐标为(x,y),长为len宽为wid的矩形显示窗口了。特别地,当len=wid=1时, 开辟的为一个坐标为(x,y)的像素点。
    4.4.6.2触摸屏校正
    在main函数初始化完成LCD之后,调用了Touchl_Calibrate()函数进行触摸屏校正。
    /******************************************************
    * 函数名:Touchl_Calibrate
    * 描述  :触摸屏校正函数
    * 输入  : 无
    * 输出  :0    --- 校正成功
    1   --- 校正失败
    * 举例  :无
    * 注意  :无
    *********************************************************/
    int Touchl_Calibrate(void)
    {
    uint8_t i;
    u16 test_x=0, test_y=0;
    u16 gap_x=0, gap_y=0;
    Coordinate * Ptr;
    // delay_init();
    Set_direction(0);   //设置为横屏
    for(i=0;i<4;i++)
    {
    LCD_Rectangle(0,0,320,240,CAL_BACKGROUND_COLOR);  //使整个屏幕显示背景颜色
    LCD_Str_6x12_O(10, 10,"Touch Calibrate", 0);  //显示提示信息
    LCD_Num_6x12_O(10,25, i+1,  0);               //显示触点次数
    delay_ms(500);
    DrawCross(DisplaySample.x,DisplaySample.y);  //显示校正用的“十”字
    do
    {
    Ptr=Read_2046();  //读取TSC2046数据到变量ptr
    }
    while( Ptr == (void*)0 );  //当ptr为0时表示没有触点被按下
    ScreenSample.x= Ptr->x;    //把读取的原始数据存放到ScreenSample结构体
    ScreenSample.y= Ptr->y;
    }
    /* 用原始参数计算出 原始参数与坐标的转换系数。 */
    Cal_touch_para( &DisplaySample[0],&ScreenSample[0],&touch_para ) ;
    /*计算X值*/
    test_x = ( (touch_para.An * ScreenSample[3].x) +
    (touch_para.Bn * ScreenSample[3].y) +
    touch_para.Cn
    ) / touch_para.Divider ;
    /*计算Y值*/
    test_y = ( (touch_para.Dn * ScreenSample[3].x) +
    (touch_para.En * ScreenSample[3].y) +
    touch_para.Fn
    ) / touch_para.Divider ;
    gap_x = (test_x > DisplaySample[3].x)?(test_x - DisplaySample[3].x)DisplaySample[3].x - test_x);
    gap_x = (test_y > DisplaySample[3].y)?(test_y - DisplaySample[3].y)DisplaySample[3].y - test_y);
    LCD_Rectangle(0,0,320,240,CAL_BACKGROUND_COLOR);
    if((gap_x>11)||(gap_y>11))
    {
    LCD_Str_6x12_O(100, 100,"Calibrate fail", 0);
    LCD_Str_6x12_O(100, 120,"  try again   ", 0);
    delay_ms(2000);
    return 1;
    }
    aa1 = (touch_para.An*1.0)/touch_para.Divider;
    bb1 = (touch_para.Bn*1.0)/touch_para.Divider;
    cc1 = (touch_para.Cn*1.0)/touch_para.Divider;
    aa2 = (touch_para.Dn*1.0)/touch_para.Divider;
    bb2 = (touch_para.En*1.0)/touch_para.Divider;
    cc2 = (touch_para.Fn*1.0)/touch_para.Divider;
    LCD_Str_6x12_O(100, 100,"Calibrate Success", 0);
    delay_ms(1000);
    return 0;
    }
    本函数的主要作用是在指定的几个液晶屏坐标(逻辑坐标)显示”十”字交叉点,由用户使用触笔点击触摸屏交叉点,读取由TSC2046测得的触点电压(物理坐标)。采集4个不同位置的触点电压(物理坐标),然后根据触摸校准算法把逻辑坐标与物理坐标转换公式的系数A、B、C、D、E、F计算出来。
    若使用此函数校准成功后,用户再点击触摸屏时,可把测量出的触点电压(物理坐标)代入已知系数的转换公式,计算出对应的液晶屏坐标(逻辑坐标)。
    转换公式的系数为以上代码66~72行中的aa1、bb1、cc1、aa2、bb2、cc3这几个全局变量,如果把这几个数据保存在非易失性存储器(SD卡、EEPROM等)中,上电后向这几个变量赋值,就不需要每次上电都进行一次触屏校准了。
    本函数中大部分都是关于触摸屏校准算法的数学运算,有兴趣的读者可查阅其它相关资料来理解。在代码中的18~30行,与触摸屏的触点电压获取有关,分析如下:
    1、第20~25行,调用LCD_Rectangle()、LCD_Str_6x12_O()、LCD_Num_6x12_O()、DrawCross()由液晶屏显示背景、提示信息及校准用的“十”字。这些函数都与液晶的画点函数原理类似,关于字符显示的在下一个章节进行说明。
    2、第28行,调用Read_2046()函数获取触点的电压,该函数通过向TSC2046控制器发送控制命令:若触笔点击触摸屏时采集触点的电压,采集10个电压取平均值,结果返回给变量Ptr;若没有触点,则Ptr的值为0,由do-while循环等待至采集到数据为止。Ptr中保存的电压数据在后面被用于校准算法计算。
    Read_2046()函数定义如下:
    /******************************************************
    * 函数名:Read_2046
    * 描述  :得到滤波之后的X Y
    * 输入  : 无
    * 输出  :Coordinate结构体地址
    * 举例  :无
    * 注意  :速度相对比较慢
    *********************************************************/
    Coordinate *Read_2046(void)
    {
    static Coordinate  screen;
    int m0,m1,m2,TP_X[1],TP_Y[1],temp[3];
    uint8_t count=0;
    /* 坐标X和Y进行9次采样*/
    int buffer[2][9]={{0},{0}};
    do
    {
    Touch_GetAdXY(TP_X,TP_Y);
    buffer[0][count]=TP_X[0];
    buffer[1][count]=TP_Y[0];
    count++;
    }   /*用户点击触摸屏时即TP_INT_IN信号为低 并且 count<9*/
    while(!INT_IN_2046&& count<9);
    //由于篇幅问题,此处省略很多行,省略部分主要为计算10个采样电压的平均值
    ………………………………
    Read_2046()函数中,调用了Touch_GetAdXY(),它用于获取一次触点(x,y)电压。实际上,驱动TSC2046最底层的是命令WR_CMD(CHX)WR_CMD(CHY),发送了这两个命令后,TSC2046开始采集相应的触点电压,通过SPI传送触点电压数据到STM32。
    命令语句中的CHX宏展开为0xd0,CHY为0x90,它们是根据TSC2046的命令格式设定的。驱动TSC2046的命令控制字格式。

    其中S为数据传输起始标志位该位必为1,A2~A0进行通道选择MOD用于转换 精度选择,1为8位精度,0为12位精度。

    所谓通道选择即为检测哪一个通道的坐标。如A2~A0为 001时,即命令控制字为0x90,根据表格知,芯片会给触摸屏的Y阻性材料层的两端提供Y+、Y-的电压,若有触笔点击,则Y触点电压可经过X+利用ADC读取得。同理命令控制字为0xd0时,A2~A0为101,即给X阻性材料层提供电压,触点电压经过Y+由ADC读取得。这就是TSC2046采集触点电压的原理。
    4.4.6.3检测触点、画点
    回到main函数。触摸屏也校准好后,剩下的就是应用程序代码了,调用Init_Palette()使液晶屏显示出画板的界面, (由于印刷原因,无法分辨具体颜色)

    该画板的界面左侧为各种颜色方块,右侧为提供给用户进行绘画的空间。显示完该界面后,循环检测touch_flag标志,它在中stm32f10x_it.c文件中的中断服务函数被赋值。当触摸屏被按下时会进入该函数,对touch_flag赋值,如下:
    void EXTI9_5_IRQHandler(void)
    {
    if(EXTI_GetITStatus(EXTI_Line6) != RESET)
    {
    GPIO_ResetBits(GPIOB, GPIO_Pin_5);
    touch_flag=1;
    EXTI_ClearITPendingBit(EXTI_Line6);
    }
    }
    若touch_flag标志为1,即触摸屏被按下,main中调用函数Get_touch_point(),该函数通过Read_2046_2()获取触点电压,根据公式把电压转换为液晶坐标,并保存到display结构体中。得到液晶坐标后,main调用Palette_draw_point()对相应的坐标点进行处理,若触点位于画板界面的颜色方块中,则使画笔变为该颜色,其后在空白界面的触点将显示该颜色的笔迹。
    4.5实验现象
    将野火STM32开发板供电(DC5V),插上JLINK,插上串口线(两头都是母的交叉线),接上液晶屏,将编译好的程序下载到开发板。运行后,可在LCD屏幕看到提示信息“Touch Calibrate” 和触摸屏校正用的“十”字。点击“十”字的中间(共四个),若校正成功后会出现画板界面,在画板界面的右侧可进行绘画,点击左侧的颜色块可选择笔迹的颜色。

    <a href="https://www.eeboard.com/wp-content/uploads/2013/08/STM32-18.png">
    回复

    使用道具 举报

    您需要登录后才可以回帖 注册/登录

    本版积分规则

    关闭

    站长推荐上一条 /2 下一条

    手机版|小黑屋|与非网

    GMT+8, 2024-3-29 05:03 , Processed in 0.126946 second(s), 18 queries , MemCache On.

    ICP经营许可证 苏B2-20140176  苏ICP备14012660号-2   苏州灵动帧格网络科技有限公司 版权所有.

    苏公网安备 32059002001037号

    Powered by Discuz! X3.4

    Copyright © 2001-2020, Tencent Cloud.