最近在学习lvgl,这是个开源的嵌入式图像显示框架,足够支撑一些资源匮乏的单片机,来显示一些看起来比较专业的界面,例如下面这种
还有这种
是不是很酷
放下Lvgl姑且不表。因为是用户操作的界面,现在的大部分屏幕都可以配备触摸操作,不再需要额外的键盘鼠标,所以,今天就来学习一下配置我手中这块支持触摸的屏幕。
不像我们的手机,嵌入式设备大部分配备的是电阻屏,因为精准度高,并且廉价。
最常见的就是我这种4线电阻屏。就是4根线,X+,X-,Y+,Y-。
原理如下:
四线主要是由镀有ITO镀层的两层薄膜所组成。其中的一层在屏幕的左右边缘,各有一条垂直的总线,另外一层是在屏幕的顶部和底部都各有一条水平的总线。如果你在一层的薄膜两条总线上施加以电压,那么在ITO的镀层之上就会形成一个均匀的电场。当其使用者触击到触摸屏时,触击点的两层薄膜就会发生接触,在另外一层的薄膜之上就可以测量到接触点的电压值了。
那么4根线如何测量呢
首先第一步:在X-上施加于0V电压,X+上施加于VCC,然后测量Y-(或者Y+)电极上的电压值VPX,然后计算出接触点P的X座标。
第二步:在Y-上施加于0V的电压,在Y+上施加VCC,测量出X-(或X+)电极上的电压值VPY。然后计算出接触点P的Y座标。
以上的两个步骤可以组成一个测量周期,可以得到一组(X,Y)的座标。
原理上的方法,我暂且称之为ADC测量法,因为是通过ADC测量电压得到的。既然原理清晰了,那么就可以开始解决了,利用4个GPIO,连接屏幕的4线,按照测量方法,进行编程。这叫需求转化代码。
关键代码
//获取x y 的值 x,y 为指针,指向存储的变量
void get_map_x_y(int* x,int* y)
{
GPIO_InitTypeDef GPIO_InitStruct;
//读 X 值
//配置 X+为高 推挽 X-为低 推挽
GPIO_InitStruct.Pin = P_TOUCH_X_JIA;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(P_TOUCH_PORT, &GPIO_InitStruct);
HAL_GPIO_WritePin(P_TOUCH_PORT, P_TOUCH_X_JIA, GPIO_PIN_SET);
GPIO_InitStruct.Pin = P_TOUCH_X_JIAN;
HAL_GPIO_Init(P_TOUCH_PORT, &GPIO_InitStruct);
HAL_GPIO_WritePin(P_TOUCH_PORT, P_TOUCH_X_JIAN, GPIO_PIN_RESET);
//配置 Y+为adc模式 Y-为浮空输入
hadcy.Instance = ADC;
hadcy.Init.channel = P_TOUCH_Y_CH;
hadcy.Init.freq = 1000;
if (HAL_ADC_Init(&hadcy) != HAL_OK)
{
Error_Handler();
}
GPIO_InitStruct.Pin = P_TOUCH_Y_JIAN;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
HAL_GPIO_Init(P_TOUCH_PORT, &GPIO_InitStruct);
*x=HAL_ADC_GET_INPUT_VOLTAGE(&hadcy);
HAL_GPIO_WritePin(P_TOUCH_PORT, P_TOUCH_X_JIA, GPIO_PIN_RESET);
HAL_GPIO_WritePin(P_TOUCH_PORT, P_TOUCH_X_JIAN, GPIO_PIN_RESET);
//读 Y 值
//配置 y+为高 推挽 y-为低 推挽
GPIO_InitStruct.Pin = P_TOUCH_Y_JIA;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(P_TOUCH_PORT, &GPIO_InitStruct);
HAL_GPIO_WritePin(P_TOUCH_PORT, P_TOUCH_Y_JIA, GPIO_PIN_SET);
GPIO_InitStruct.Pin = P_TOUCH_Y_JIAN;
HAL_GPIO_Init(P_TOUCH_PORT, &GPIO_InitStruct);
HAL_GPIO_WritePin(P_TOUCH_PORT, P_TOUCH_Y_JIAN, GPIO_PIN_RESET);
//配置 x+为adc模式 x-为浮空输入
hadcx.Instance = ADC;
hadcx.Init.channel = P_TOUCH_X_CH;
hadcx.Init.freq = 1000;
if (HAL_ADC_Init(&hadcx) != HAL_OK)
{
Error_Handler();
}
GPIO_InitStruct.Pin = P_TOUCH_X_JIAN;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
HAL_GPIO_Init(P_TOUCH_PORT, &GPIO_InitStruct);
*y=HAL_ADC_GET_INPUT_VOLTAGE(&hadcx);
HAL_GPIO_WritePin(P_TOUCH_PORT, P_TOUCH_Y_JIA, GPIO_PIN_RESET);
HAL_GPIO_WritePin(P_TOUCH_PORT, P_TOUCH_Y_JIAN, GPIO_PIN_RESET);
}
完全就是按照原理图,翻译出来的操作,这里我们需要注意一下啊,测量点X+与Y+要在ADC模式和输出模式下转换,所以,我们要选择既支持GPIO,又支持ADC转化的引脚。否则就无法测量出来值。
不过这种方式我研究了5个小时,然后就发现,X坐标测量出来的电压,全屏幕都在变化,然后我就细心的发现了,板子上自带了一个ADC芯片,并且已经接好了线。所以这种电压的变化,我怀疑和外部接了芯片有关系,遂放弃了这种做法。
不过这种做法完全是可行的。并且还不需要额外的芯片,换句话说,这种做法,免费。
板子上自带的芯片,就是ads7846,这是一款非常主流的电阻屏驱动芯片,它将4线电阻的测量,转化为SPI总线的数据读取,并且提供了中断,对接单片机,那叫一个6。
提供了6根线
引脚 | 说明 |
---|---|
CLK | SPI时钟 |
CS | 片选 |
MOSI | 主机输出从机输入 |
MISO | 主机输入从机输出 |
BUSY | 忙信号 |
PEN | 中断信号 |
这里使用了一下GPIO模拟的方式。
GPIO初始化
static void TOUCH_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
__HAL_RCC_GPIO_CLK_ENABLE();
GPIO_InitStruct.Pin = TDIN;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(P_TOUCH_PORT, &GPIO_InitStruct);
HAL_GPIO_WritePin(P_TOUCH_PORT, TDIN, GPIO_PIN_RESET);
GPIO_InitStruct.Pin = TCLK;
HAL_GPIO_Init(P_TOUCH_PORT, &GPIO_InitStruct);
HAL_GPIO_WritePin(P_TOUCH_PORT, TCLK, GPIO_PIN_RESET);
GPIO_InitStruct.Pin = TCS;
HAL_GPIO_Init(P_TOUCH_PORT, &GPIO_InitStruct);
HAL_GPIO_WritePin(P_TOUCH_PORT, TCS, GPIO_PIN_RESET);
GPIO_InitStruct.Pin = DOUT;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(P_TOUCH_PORT, &GPIO_InitStruct);
GPIO_InitStruct.Pin = PEN;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(P_TOUCH_PORT, &GPIO_InitStruct);
HAL_NVIC_SetPriority(GPIOA_IRQn, 0);
HAL_NVIC_EnableIRQ(GPIOA_IRQn);
}
这里注意一下,要打开中断,这个PEN引脚,在有触摸的时候,是低电平,当手拿开的时候,就是高电平了。所以如果要持续读取,要不断的判断PEN引脚的电瓶。
BUSY信号也可以用上,初始化为读入方式就可以。
核心函数,读取坐标
#define CMD_RDX 0X90 //0B10010000即用差分方式读X坐标
#define CMD_RDY 0XD0 //0B11010000即用差分方式读Y坐标
//读取一次X,Y值
//读到的X,Y坐标值必须都大于100
//成功返回1,不成功返回0
//读数限制在100~3800之间.
const float y_sin=0.1348;
const float x_sin=0.1739;
int get_map_x_y(int* X,int* Y)
{
int px=0,py=0;
start_spi();//启动SPI
WriteByteADS(CMD_RDX);
//ADS7846的转换时间最长为6us
TCLK_SET(1);
delay_us(3);
TCLK_SET(0);
delay_us(3);
px=ReadWordADS();
WriteByteADS(CMD_RDY);
//ADS7846的转换时间最长为6us
TCLK_SET(1);
delay_us(3);
TCLK_SET(0);
delay_us(3);
py=ReadWordADS();//读Y轴坐标
TCS_SET(1);
if((px>100)&&(py>100)&&(px<3800)&&(py<3800))
{
int abs_px,abs_py;
abs_px=px-100;
abs_py=py-100;
*X=x_sin*abs_px;
*Y=y_sin*abs_py;
return 1;//读数成功(范围限制)
}
else
{
return 0; //读数失败
}
}
我先大概试了一下4个角的坐标,然后估算出了x轴和y轴的单位,即读取到的坐标1个单位表示多少个像素。得到了下面两个值。
const float y_sin=0.1348;
const float x_sin=0.1739;
然后就可以按照读取到的值去掉起始值,再乘以刚才的系数,就能够得到我这个320*240屏幕的具体坐标了。
abs_px=px-100;
abs_py=py-100;
X=x_sinabs_px;
Y=y_sinabs_py;
在前面一篇文章中,显示屏16线的驱动已经完成,《单片机—HLK-W801并口驱动ST7789》
那么结合起来今天的触摸屏,就可以做一个绘图板了。
中断判断,这里只是置一个标志位,有触摸操作会触发。
void HAL_GPIO_EXTI_Callback(GPIO_TypeDef *GPIOx, uint32_t GPIO_Pin)
{
if ((GPIOx == P_TOUCH_PORT) && (GPIO_Pin == PEN))
{
key_flag = 1;
}
}
然后主函数中,进行持续读点画点就可以了。
while (1)
{
if (key_flag == 1)
{
HAL_Delay(20);
while(HAL_GPIO_ReadPin(P_TOUCH_PORT, PEN) == GPIO_PIN_RESET)
{
int pos_x=0, pos_y=0;
get_map_x_y(&pos_x,&pos_y);
LCD_DrawPoint(320-pos_x,pos_y, 0xf000);
HAL_Delay(5);
}
key_flag = 0;
}
}
在HAL_Delay不同的情况下,有不同的绘制效果。
HAL_Delay(20)
HAL_Delay(5)
去掉延迟
可以看出,有延迟的时候,坐标比较干净,没有延迟的时候,在落笔和起笔的时候,会有很多漂移的点,这个漂移的效果,倒像是沙画的效果,撒出来的。
这篇文章只是为了学习lvgl做了一个技术储备,用来作为lvgl的输入方式。顺带也了解了一下电阻屏的驱动方式,并且掌握了两种测量方法。
昨天是妇女节,姑娘都嫌弃这个名字,显得土,后来就叫女神节。其实当初这个节日是女工为了争取平等的福利而创立的,全称是“联合国妇女权益和国际和平日”,同事说,其实也应该给已婚妇女的老公也放半天,这样能更好的给女性放个假,我觉得说的有道理啊。