官方的GD32F527I_EVL的开发板是带有LCD屏的,屏上的触摸屏的驱动芯片为XPT2046,采用SPI接口。本次将分享如何驱动他,并讯取坐标。
【电阻触摸屏原理】
电阻式触摸屏主要是利用压力感应进行控制。它的构成是显示屏及一块与显示屏紧密贴合的电阻薄膜屏。这个电阻薄膜屏通常分为两层,一层是由玻璃或有机玻璃构成的基层,其表面涂有透明的导电层;基层外面压着我们平时直接接触的经过硬化及防刮处理的塑料层,塑料层内部同样有一层导电层,两个导电层之间是分离的。当我们用手指或其他物体触摸屏幕的时候,两个导电层发生接触,电阻产生变化,控制器则根据电阻的具体变化来判断接触点的坐标并进行相应的操作。
其原理就是X方向和Y方向的电阻分压,点击不同的点产生的电压不同,由此判断触碰点。
两个ITO涂层的两端分别引出X-、X+、Y-、Y+四个电极,见图 XY的ITO层结构 , 这是电阻屏最常见的四线结构,通过这些电极,外部电路向这两个涂层可以施加匀强电场或检测电压。
XPT2046是专用在四线电阻屏的触摸屏控制器,GD32可通过SPI接口向它写入控制字, 由它测得X、Y方向的触点电压返回给GD32。
【连接原理图】
官方的开发板说明中有LCD屏的接口原理图:
【读写时序】
在XPT2046中官方给出的时序图如下:
由时序图,我们可以得出,先发送转换电压的指令,然后再读取12个bit就得到对应线路的电压转换值。
【代码】
首先先定义宏,即IO,以及高低电平的转换:
// 引脚定义 #define XPT2046_PENIRQ_PORT GPIOI #define XPT2046_PENIRQ_PIN GPIO_PIN_3 #define XPT2046_MOSI_PORT GPIOF #define XPT2046_MOSI_PIN GPIO_PIN_9 #define XPT2046_MISO_PORT GPIOH #define XPT2046_MISO_PIN GPIO_PIN_7 #define XPT2046_SCK_PORT GPIOH #define XPT2046_SCK_PIN GPIO_PIN_6 #define XPT2046_NSS_PORT GPIOF #define XPT2046_NSS_PIN GPIO_PIN_6 #define XPT2046_BUSY_PORT GPIOG #define XPT2046_BUSY_PIN GPIO_PIN_3 #define XPT2046_CS_ENABLE() gpio_bit_reset(XPT2046_NSS_PORT, XPT2046_NSS_PIN) #define XPT2046_CS_DISABLE() gpio_bit_set(XPT2046_NSS_PORT, XPT2046_NSS_PIN) #define XPT2046_PENIRQ_READ() gpio_input_bit_get(XPT2046_PENIRQ_PORT, XPT2046_PENIRQ_PIN) #define XPT2046_BUSY_READ() gpio_input_bit_get(XPT2046_BUSY_PORT, XPT2046_BUSY_PIN) #define XPT2046_MOSI_HIGH() gpio_bit_set(XPT2046_MOSI_PORT, XPT2046_MOSI_PIN) #define XPT2046_MOSI_LOW() gpio_bit_reset(XPT2046_MOSI_PORT, XPT2046_MOSI_PIN) #define XPT2046_SCK_HIGH() gpio_bit_set(XPT2046_SCK_PORT, XPT2046_SCK_PIN) #define XPT2046_SCK_LOW() gpio_bit_reset(XPT2046_SCK_PORT, XPT2046_SCK_PIN) #define XPT2046_MISO_READ() gpio_input_bit_get(XPT2046_MISO_PORT, XPT2046_MISO_PIN) // SPI定义 #define TOUCH_SPI SPI4 /* 坐标转换参数 */ #define TOUCH_X_MIN 221 // 触摸屏X轴最小值 #define TOUCH_X_MAX 3823 // 触摸屏X轴最大值 #define TOUCH_Y_MIN 228 // 触摸屏Y轴最小值 #define TOUCH_Y_MAX 3743 // 触摸屏Y轴最大值 #define SCREEN_WIDTH 480 // 屏幕宽度 #define SCREEN_HEIGHT 272 // 屏幕高度
2、编写初始化IO的代码
其中MOSI/SCK/CS初始化为输出模式,PENIRQ、MISO初始化为输入模式。通过观察BUSY的时序图,发现其输出与官方的函数不对,通过延时一秒来跳过。
/* 初化触摸屏的IO */ void XPT2046_gpio_init(void) { //使能GPIO时钟 rcu_periph_clock_enable(RCU_GPIOF); rcu_periph_clock_enable(RCU_GPIOH); rcu_periph_clock_enable(RCU_GPIOI); rcu_periph_clock_enable(RCU_GPIOG); //初始化MOSI SCK NSS引脚 gpio_mode_set(XPT2046_MOSI_PORT, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, XPT2046_MOSI_PIN); gpio_output_options_set(XPT2046_MOSI_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, XPT2046_MOSI_PIN); gpio_mode_set(XPT2046_SCK_PORT, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, XPT2046_SCK_PIN); gpio_output_options_set(XPT2046_SCK_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, XPT2046_SCK_PIN); gpio_mode_set(XPT2046_NSS_PORT, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, XPT2046_NSS_PIN); gpio_output_options_set(XPT2046_NSS_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, XPT2046_NSS_PIN); //初始化MISO PENIRQ BUSY引脚 gpio_mode_set(XPT2046_MISO_PORT, GPIO_MODE_INPUT, GPIO_PUPD_NONE, XPT2046_MISO_PIN); gpio_mode_set(XPT2046_PENIRQ_PORT, GPIO_MODE_INPUT, GPIO_PUPD_NONE, XPT2046_PENIRQ_PIN); gpio_mode_set(XPT2046_BUSY_PORT, GPIO_MODE_INPUT, GPIO_PUPD_PULLDOWN, XPT2046_BUSY_PIN); //CS拉高 XPT2046_CS_DISABLE(); //SCK拉低 XPT2046_SCK_LOW(); }
3、编写发送转换指令的函数:
/* 向XPT2046发送一个字节 */ void XPT2046_send_byte(uint8_t byte) { XPT2046_SCK_LOW(); delay_5us(); for(uint8_t i=0; i<8; i++) { delay_5us(); if(byte & (0x80 >> i)) { XPT2046_MOSI_HIGH(); } else { XPT2046_MOSI_LOW(); } XPT2046_SCK_HIGH(); delay_5us(); XPT2046_SCK_LOW(); } }
4、为了获取适当的延时,编写了一个延时函数:
void delay_5us(void) { for(uint8_t i=0; i<10; i++); }
这个函数,可以通过各自的芯片来微调延时,达到能正确的获取到数据即可。
5、写命令函数:
/* 向XPT2046发送一个字节 */ void XPT2046_send_byte(uint8_t byte) { XPT2046_SCK_LOW(); delay_5us(); for(uint8_t i=0; i<8; i++) { delay_5us(); if(byte & (0x80 >> i)) { XPT2046_MOSI_HIGH(); } else { XPT2046_MOSI_LOW(); } XPT2046_SCK_HIGH(); delay_5us(); XPT2046_SCK_LOW(); } }
6、从XPT2046接收12位数据
uint16_t XPT2046_recv_byte(void) { uint16_t data = 0; XPT2046_SCK_LOW(); delay_5us(); for(uint8_t i=0; i<12; i++) { XPT2046_SCK_HIGH(); delay_5us(); XPT2046_SCK_LOW(); data <<= 1; if(XPT2046_MISO_READ()) { data |= 0x01; } delay_5us(); } return data; }
7、读取指定坐标函数,传入参数为指令,返回为读取ADC的值:
static uint16_t XPT2046_readXY(uint8_t cmd) { XPT2046_CS_ENABLE(); uint16_t data = 0; XPT2046_send_byte(cmd); delay_1ms(1); data = XPT2046_recv_byte(); XPT2046_CS_DISABLE(); return data; }
8、读取坐标,并返回XY的坐标植
/** * @brief 将触摸屏原始坐标转换为屏幕坐标 * @param touch_x: 触摸屏原始X坐标 * @param touch_y: 触摸屏原始Y坐标 * @param screen_x: 转换后的屏幕X坐标指针 * @param screen_y: 转换后的屏幕Y坐标指针 */ void XPT2046_convert_cood(uint16_t touch_x, uint16_t touch_y, uint16_t *screen_x, uint16_t *screen_y) { // 限制输入坐标在有效范围内 if(touch_x < TOUCH_X_MIN) touch_x = TOUCH_X_MIN; if(touch_x > TOUCH_X_MAX) touch_x = TOUCH_X_MAX; if(touch_y < TOUCH_Y_MIN) touch_y = TOUCH_Y_MIN; if(touch_y > TOUCH_Y_MAX) touch_y = TOUCH_Y_MAX; // X轴转换 (反向) *screen_x = ((TOUCH_X_MAX - touch_x) * (SCREEN_WIDTH - 1)) / (TOUCH_X_MAX - TOUCH_X_MIN); // Y轴转换 (反向) *screen_y = ((TOUCH_Y_MAX - touch_y) * (SCREEN_HEIGHT - 1)) / (TOUCH_Y_MAX - TOUCH_Y_MIN); // 确保输出坐标不会超出屏幕范围 if(*screen_x >= SCREEN_WIDTH) *screen_x = SCREEN_WIDTH - 1; if(*screen_y >= SCREEN_HEIGHT) *screen_y = SCREEN_HEIGHT - 1; } /* 读取触摸屏坐标 */ void XPT2046_read_cood(uint16_t *x, uint16_t *y) { uint16_t raw_x, raw_y; // 读取原始坐标 raw_x = XPT2046_readXY(0x90); raw_y = XPT2046_readXY(0xD0); /* 打印坐标 */ // printf("x = %d, y = %d\r\n", *x, *y); // 转换为屏幕坐标 XPT2046_convert_cood(raw_x, raw_y, x, y); /* 打印坐标 */ // printf("Raw: X=%d, Y=%d -> Screen: X=%d, Y=%d\r\n", raw_x, raw_y, *x, *y); }
9、通过PEN_IRQ来获取是否有触摸发生:
/* 检测触摸屏是否按下 */ uint8_t XPT2046_touch_press(void) { if(gpio_input_bit_get(XPT2046_PENIRQ_PORT, XPT2046_PENIRQ_PIN) == RESET) { return 1; } else { return 0; } }
到此代码编写就完成了。
【驱动效果检测】
烧写代码到开发板后,打开串口助手,发现可以正常的获取坐标值 :
【经验心得】
此次驱动,花费了不少时间,一来是作者对这个芯片的驱动不是很熟悉,二来是现在触摸大都用的是电容触摸了,接触得比较少。
XPT相比于IIC接口的如何GT911的驱动要相对复杂一些。特别是需要比较精确的获取坐标值时,需要编写复杂的较准。我此次由于驱动对定位不是非常高所以没有编写较准函数。如果需要比较精准的定位,需要细心的进行较准、滤波等处理。