s3c2440裸机编程-电阻触摸屏

1 电阻触摸屏原理#

触摸屏包含上下叠合的两个透明层,一般覆盖在lcd表面,两个透明层是由均匀的电阻介质组成,如下图:

当触摸屏表面受到的压力(如通过笔尖或手指进行按压)足够大时,顶层与底层之间的薄膜会产生接触,此时会形成x方向和y方向的坐标。那么x,y坐标的值是怎么得来的呢?本质上就是通过ADC转换得来的。

触摸屏的等效电路可以看成如下图:

计算触点的X,Y坐标分为如下两步:

1.1 计算Y坐标#

在Y+电极施加驱动电压Vdrive, Y-电极接地,由于上下两层膜形成触点,X+做为触点的引出端,测量得到接触点的电压,触点电压与Vdrive电压之比等于触点Y坐标与屏高度之比。如下图:

1.2 计算X坐标#

在X+电极施加驱动电压Vdrive, X-电极接地,由于上下两层膜形成触点,Y+做为触点的引出端,测量得到接触点的电压,Y+做为引出端测量得到接触点的电压,触点电压与Vdrive电压之比等于触点X坐标与屏宽度之比。如下图:

2 电阻触摸屏的几种模式#

2.1 等待中断模式#

平时的时候上下两层膜并不粘在一起,我们把这种状态称为“等待中断模式”, 等效电路如下图的右边那幅图:

s5、s4闭合,s1、s2、s3断开,这个时候Y_ADC/XP通过S5接上拉电阻,处于高电平状态,X_ADC/YP接地。没法读取x,y坐标。

2.2 读取x坐标模式#

给X方向通电,也就是让S1、S3开关闭合,s2、s4断开,那么当屏幕按下,触点YP的电平就对应x坐标。(XP到XM之间是均匀的电阻介质)

x_adc电压/vcc = x坐标/width, 所以x坐标= width * x_adc电压/vcc

2.3 读取y坐标模式#

给Y方向通电,也就是让S2、S4开关闭合,s1、s3断开,那么当屏幕按下,触点XP的电平就对应y坐标。(YP到YM之间是均匀的电阻介质)

y_adc电压/vcc = Y坐标/height, 所以y坐标= height * y_adc电压/vcc

2.4 TS中断流程#

总结一下单次触发TS中断,使用触摸屏的流程:

1
2
3
4
5
1. 按下触摸屏,产生TS中断
2. 启动ADC(目的是获取x,y方向上的坐标值)
3. ADC转换完成,产生adc中断(adc转换需要一定的时间)
4. ADC中断中来读取x y坐标
5. 松开,结束

我们知道,现在的手机都是支持屏幕滑动翻页和长按的功能。那么这些功能是如何做到的呢?

2.4.1 中断加入定时器#

如何让触摸屏支持长按或者滑动操作(多次触发TS中断)?

答案:定时器,当长按屏幕,会产生多次TS中断,因此我们需要用定时器来判断,当定时一段时间后,还有TS中断产生,那么我们认为是长按操作,进行中断响应。滑动也是类似的道理,当定时时间到后,如果还有TS中断产生,且坐标发生了改变,就认为是滑动操作。

<5> 启动定时器
<6> 一段时间后,定时器中断发生,判断触摸屏是否仍被按下(是否有定时器中断产生),如果有就循环上述过程<2><3><4><5>

可以用如下流程图概括TSC的整个SW flow.

2.4.2 带定时器的TS中断处理流程#

image

3 触摸屏接口模式#

3.1 Normal Conversion Mode#

正常转换模式,一般情况下可以配置ADCCON和ADCDAT0来读取数据。

3.2 Separate X/Y position conversion Mode#


x,y坐标分离转换格式,x坐标会写入ADCDAT0, y坐标会写入ADCDAT1,所以会产生2次中断开分开完成x,y的坐标转换。

3.3 Auto(Sequential) X/Y Position Conversion Mode#

自动转换模式,当触摸屏按下后,会一次性对x,y方向的坐标进行转换,x坐标会写入ADCDAT0, x坐标会写入ADCDAT1。会产生一次中断进行x,y坐标的自动转换。

3.4 Waiting for Interrupt Mode#

等待中断模式 。可以设置rADCTSC=0xd3;也就是对应下图寄存器 // XP_PU, XP_Dis, XM_Dis, YP_Dis, YM_En.当产生中断信号(INT_TC)后,等待中断模式必须清除.(即XY_PST sets to the No operation Mode).

4 触摸屏控制器#

4.1 TS控制寄存器#

电阻触摸屏的原理本质上就是ADC,ADC相关寄存器介绍详见s3c2440裸机-ADC编程或者s3c2440裸机编程-ADC | Hexo (fuzidage.github.io)
TSC相比ADC多了一个ADCTSC寄存器,如下图:
image
当bit[2]=0,normal mode时,那么bit[1:0]需要配置成01或者10进行手工测量x,y.
当bit[2]=1,auto mode时,那么bit[1:0]需要配置成0,进行自动测量。

4.2 DATA寄存器#

4.2.1 x坐标ADCDATA0#

image

4.2.2 y坐标ADCDATA1#

image

4.3 松开按下检测寄存器#

这个寄存器可以检测是否有触摸中断产生,是按下触摸屏了,还是松开触摸屏了。
image

5 触摸屏编程示例#

5.1 ADC中断产生#

img

5.1.1 中断源#

ADC和TSC共用一个中断源,如下:

img

SRCPND表示哪个中断源产生了中断请求。

img

img

5.1.2 中断模式#

img

img

5.1.3 中断屏蔽寄存器#

img

img

5.1.4 中断挂起寄存器#

用来显示当前优先级最高的、正在发生的中断, 需要清除对应位。

img

img

从SRCPND寄存器可以读到ADC和TSC复用的同一个中断源,那么如何区分呢?

可以从SUBSRCPND寄存器配置,如下:

5.1.4.1 SUBSRCPND寄存器#

img

img

当bit 9被置1时,表示TSC中断。那么我们需要打开subsrcmask寄存器:

5.1.4.2 INTSUBMSK寄存器#

img

所以TSC中断的产生流程如下:

img

5.2 TS触摸屏编程流程#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1. 初始化TSC,ADCTSC寄存器
2. 设定TSC处于“等待中断模式”
3. 使能TSC中断
      INTSUBMSK
      MSK/MODE
4. 按下,进入TSC中断
      进入自动采集转换模式
      启动ADC
5. ADC中断
      读数据
      再次进入”等待中断模式“
      启动定时器(为了处理长按或者滑动操作)
6. 定时器中断
      若松开,结束
      如任然按下,进入步骤4的启动ADC流程

img

5.2.1 初始化#

1
2
3
4
5
6
7
8
void touchscreen_init(void) {
/* 设置触摸屏接口:寄存器 */
adc_ts_reg_init();
/* 设置中断 */
adc_ts_int_init();
/* 让触摸屏控制器进入"等待中断模式" */
enter_wait_pen_down_mode();
}

5.2.1.1 ts寄存器初始化#

主要是设置预分频,产生ADC clk = 1MHz。

1
2
3
4
5
6
7
8
9
10
11
void adc_ts_reg_init(void) {
/* [15] : ECFLG, 1 = End of A/D conversion
* [14] : PRSCEN, 1 = A/D converter prescaler enable
* [13:6]: PRSCVL, adc clk = PCLK / (PRSCVL + 1)
* [5:3] : SEL_MUX, 000 = AIN 0
* [2] : STDBM
* [0] : 1 = A/D conversion starts and this bit is cleared after the startup.
*/
ADCCON = (1<<14) | (49<<6) | (0<<3);
ADCDLY = 0xff;
}

5.2.1.2 ts 中断初始化#

为了将中断源开启,这里设置SUBSRCPND 和INTSUBMSK让中断源开启。通过register_irq()注册中断号和中断服务程AdcTsIntHandle,查表得出中断号为31,这样当硬件产生中断后可以从INTOFFSET区分是哪个中断号。如下图:

img

1
2
3
4
5
6
7
8
void adc_ts_int_init(void) {
SUBSRCPND = (1<<TC_INT_BIT) | (1<<ADC_INT_BIT);/*清中断*/
/* 注册中断处理函数 */
register_irq(31, AdcTsIntHandle); /*31号中断*/
/* 使能中断 */
INTSUBMSK &= ~((1<<ADC_INT_BIT) | (1<<TC_INT_BIT));//防止屏蔽(SUBMSK)
//INTMSK &= ~(1<<INT_ADC_TC);//reg_irq已经使能了31中断号
}

5.2.1.3 进入”等待中断模式”#

img

img

img

进入等待中断模式,YM闭合, YP, XP, XM断开,需要pull up,WAIT_PEN_DOWN表示要等待的是按下中断,当触摸屏按下时就会产生一个TSC irq,反之WAIT_PEN_UP表示要等待的是松开中断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#define ADC_INT_BIT (10)
#define TC_INT_BIT (9)
#define INT_ADC_TC (31)
/* ADCTSC's bits */
#define WAIT_PEN_DOWN (0<<8) /*触摸笔按下*/
#define WAIT_PEN_UP (1<<8) /*触摸笔松开*/
#define YM_ENABLE (1<<7)
#define YM_DISABLE (0<<7)
#define YP_ENABLE (0<<6)
#define YP_DISABLE (1<<6)
#define XM_ENABLE (1<<5)
#define XM_DISABLE (0<<5)
#define XP_ENABLE (0<<4)
#define XP_DISABLE (1<<4)
#define PULLUP_ENABLE (0<<3)
#define PULLUP_DISABLE (1<<3)
#define AUTO_PST (1<<2) /*自动转换*/
#define WAIT_INT_MODE (3) /*等待中断模式*/
#define NO_OPR_MODE (0) /*禁止模式*/

void enter_wait_pen_down_mode(void)/*等待按下模式*/ {
ADCTSC = WAIT_PEN_DOWN | PULLUP_ENABLE | YM_ENABLE | YP_DISABLE | XP_DISABLE | XM_DISABLE | WAIT_INT_MODE;
}
void enter_wait_pen_up_mode(void)/*等待松开模式*/ {
ADCTSC = WAIT_PEN_UP | PULLUP_ENABLE | YM_ENABLE | YP_DISABLE | XP_DISABLE | XM_DISABLE | WAIT_INT_MODE;
}

5.2.2 ts中断服务程序#

SUBSRCPND的bit9, bit10可以区分是TC中断还是ADC中断。

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void Isr_Tc(void)/*触摸屏中断服务程序*/ {
   printf("ADCUPDN = 0x%x, ADCDAT0 = 0x%x, ADCDAT1 = 0x%x, ADCTSC = 0x%x\n\r", ADCUPDN, ADCDAT0, ADCDAT1, ADCTSC);
   if (ADCDAT0 & (1<<15)) { //dat寄存器的第15位判断按下还是松开
     printf("pen up\n\r");
     enter_wait_pen_down_mode();
   } else {
     printf("pen down\n\r");
     /* 进入"等待触摸笔松开的模式" */
     enter_wait_pen_up_mode();
   }
}
void AdcTsIntHandle(int irq) {
   if (SUBSRCPND & (1<<TC_INT_BIT)) /* 如果是触摸屏中断 */
     Isr_Tc();
   // if (SUBSRCPND & (1<<ADC_INT_BIT)) /* ADC中断 */
   // Isr_Adc();
   SUBSRCPND = (1<<TC_INT_BIT) | (1<<ADC_INT_BIT);/*清中断*/
   //SRCPND = 1<<31;/*在interrupt.c已经清中断了*/
}
1
2
AdcTsIntHandle函数: 这里先注解掉ADC中断,只检测单独的按下松开触摸屏操作。那当isr处理完后为了能够正常响应下一次中断,需要清中断,否则会一直触发interrupt。
Isr_Tc函数:ADCDAT0 寄存器的第15位判断按下还是松开。那么当按下后,要将控制器进入”等待松开模式“,当松开后,要将控制器配置进入”等待按下模式“。

img

5.2.2.1 获取触摸屏坐标#

5.2.2.1.1 进入自动测量模式#

Auto(Sequential) X/Y Position Conversion Mode。打开TS控制寄存器,也就是ADCTSC寄存器:

img

让bit[2] =1, bit[1:0]=00,则会进入auto measurement。如果bit[2]=0,则需配置bit[1::0]=01 or 10是手动测量x,y坐标。

1
2
3
4
5
6
#define AUTO_PST         (1<<2) /*自动转换*/
#define WAIT_INT_MODE (3) /*等待中断模式*/
#define NO_OPR_MODE (0) /*禁止模式*/
void enter_auto_measure_mode(void) {
  ADCTSC = AUTO_PST | NO_OPR_MODE;
}
5.2.2.1.2 启动ADC#

触摸屏坐标就是通过ADC获取的。

img

1
ADCCON |= (1<<0);

所以TSC isr程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
void Isr_Tc(void) {/*触摸屏中断服务程序*/
if (ADCDAT0 & (1<<15)) {
printf("pen up\n\r");
enter_wait_pen_down_mode();
} else {
printf("pen down\n\r");
/* 进入"自动测量"模式 */
enter_auto_measure_mode();
/* 启动ADC */
ADCCON |= (1<<0);
}
}

那么当检测到按下后,需要进入auto measure mode,启动adc,然后就会进行自动坐标转换,转换结束后又会触发ADC中断,再次进入AdcTsIntHandle函数,进而进入Isr_Adc,SUBSRCPND可以区分中断源 。如下:

1
2
3
4
5
6
7
void AdcTsIntHandle(int irq) {
  if (SUBSRCPND & (1<<TC_INT_BIT)) /* 如果是触摸屏中断 */
    Isr_Tc();
  if (SUBSRCPND & (1<<ADC_INT_BIT)) /* ADC中断 */
    Isr_Adc();
  SUBSRCPND = (1<<TC_INT_BIT) | (1<<ADC_INT_BIT);/*清中断*/
}

我们知道ADC进行坐标转换结束后,那么会产生ADC中断,在Isr_Adc中即可获取我们的x,y坐标数据。由于我们按下后是进入了 “自动测量” 模式,因此那当数据获取完后我们得进入 “等待松开” 模式。

1
2
3
4
5
6
7
8
9
10
void Isr_Adc(void) {
int x = ADCDAT0;
int y = ADCDAT1;
if (!(ADCDAT0 & (1<<15))) { /* 在isr_Tc按下后,如果仍然按下才打印 */
x &= 0x3ff;
y &= 0x3ff;
printf("x = %08d, y = %08d\n\r", x, y);
}
enter_wait_pen_up_mode();
}

有可能触摸屏的测量过程非常长,那当ADC转换结束后,它已经松开了,这时不应该进行打印出坐标,所以这里在isr_Tc按下后,如果仍然按下才打印。

5.2.2.2 ADCDLY寄存器#

由于触摸屏采样的转换速率问题,按下后需要过一段电压才能稳定下来,那么数据才能稳定可能需要一定的延迟,所以需要配置ADC delay,让ADC慢一点产生中断,也就是等坐标稳定后在通知用户。

img

ADCDLY就是用来延时ADC启动的时间,让数据稳定后再进行转换。

img

可以看到,进行auto or manual measure 坐标转换的时序要满足:A = Dx,D表示ADCDLY的值。 现在晶振的频率是12Mhz, 那么根据触摸屏规格书我们取A= 5ms,那么D= 0.005s *12*1000000 = 60000,所以ADCDLY配置成60000.

修改前面的adc_ts_reg_init函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void adc_ts_reg_init(void) {
/* [15] : ECFLG, 1 = End of A/D conversion
* [14] : PRSCEN, 1 = A/D converter prescaler enable
* [13:6]: PRSCVL, adc clk = PCLK / (PRSCVL + 1)
* [5:3] : SEL_MUX, 000 = AIN 0
* [2] : STDBM
* [0] : 1 = A/D conversion starts and this bit is cleared after the startup.
*/
ADCCON = (1<<14) | (49<<6) | (0<<3);
/* 按下触摸屏, 延时一会再发出TC中断
* 延时时间 = ADCDLY * 晶振周期 = ADCDLY * 1 / 12000000 = 5ms
*/
ADCDLY = 60000;
}

5.3 TS触摸屏测试#

从左往右依次点击触摸屏,可以看到x坐标没有明显变化,y坐标反而线性变大。

img

同理,从上往下依次按下触摸屏,可以看到y坐标没有明显变化,x坐标反而线性变大。

img

这里是由于硬件上xp与yp接反了,ym与xm接反了,如下图:但这里并不影响我们的时候,这里我们软件上可以进行x,y坐标的转换:

img

我们软件上可以对x,y轴进行flip, mirror, rotaion旋转等一系列操作即可。比如:

Case1:ts与lcd吻合

img

Case2:ts与lcd相反

img

5.4 利用定时器支持屏幕长按和滑动#

5.4.1 改进定时器#

前面s3c2440裸机-异常中断 | Hexo (fuzidage.github.io)有讲到在handle_irq_c()中去区分中断源,执行不同的isr

image-20240501173712329

那现在通过register_timer注册对应的定时器中断服务程序,timer_irq进行执行不同的定时器中断服务程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#define TIMER_NUM  32
#define NULL ((void *)0)
typedef void(*timer_func)(void);
typedef struct timer_desc {
  char *name;
  timer_func fp;
}timer_desc, *p_timer_desc;

timer_desc timer_array[TIMER_NUM];

int register_timer(char *name, timer_func fp) {
  int i;
  for (i = 0; i < TIMER_NUM; i++) {
    if (!timer_array[i].fp) {
      timer_array[i].name = name;
      timer_array[i].fp = fp;
      return 0;
    }
  }
  return -1;
}
void unregister_timer(char *name) {
  int i;
  for (i = 0; i < TIMER_NUM; i++) {
    if (!strcmp(timer_array[i].name, name)) {
      timer_array[i].name = NULL;
      timer_array[i].fp = NULL;
      return 0;
    }
  }
  return -1;
}
void timer_irq(void) {
  int i;
  for (i = 0; i < TIMER_NUM; i++) {
    if (timer_array[i].fp) {
      timer_array[i].fp();
    }
  }
}

我们想要用timer来进行进行流水灯实验,那么假如点灯函数为:
Isr_timer_led(){}

那么则只需要在led init的时候进行调用register_timer(“led”, Isr_timer_led), 那么当时间到后触发定时器中断,便会执行timer_irq.进入Isr_timer_led

5.4.2 初始化定时器#

前面s3c2440裸机-异常中断 | Hexo (fuzidage.github.io)有具体讲解,这里采用PWM定时器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void timer_init(void) {
  /* 设置TIMER0的时钟 */
  /* Timer clk = PCLK / {prescaler value+1} / {divider value}
= 50000000/(49+1)/16
= 62500
  */
  TCFG0 = 49; /* Prescaler 0 = 49, 用于timer0,1 */
  TCFG1 &= ~0xf;
  TCFG1 |= 3; /* MUX0 : 1/16 */
  /* 设置TIMER0的初值 */
  TCNTB0 = 625; /* 10Ms中断一次 */
  /* 加载初值, 启动timer0 */
  TCON |= (1<<1); /* Update from TCNTB0 & TCMPB0 */
  /* 设置为自动加载并启动 */
  TCON &= ~(1<<1);
  TCON |= (1<<0) | (1<<3); /* bit0: start, bit3: auto reload */
  /* 设置中断 */
  register_irq(10, timer_irq);
}

5.4.2.1 支持长按和滑动#

我们之前是2s timer触发一次中断,那如果是要支持触摸屏,我们必须让定时器10ms就触发一次中断。因此需要修改timer_init中的寄存器参数。

当按下触摸屏会产生TSC中断,然后启动ADC进而产生adc中断的时候,在Isr_Adc函数中进行定时器的设置,检测长按和滑动操作。

5.4.2.1.1 定义touchscreen_timer_irq#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
static volatile int g_ts_timer_enable = 0;
static void ts_timer_enable(void) {
  g_ts_timer_enable = 1;
}
static void ts_timer_disable(void) {
  g_ts_timer_enable = 0;
}
static int get_status_of_ts_timer(void) {
  return g_ts_timer_enable;
}
/* 每10ms该函数被调用一次
*/
void touchscreen_timer_irq(void) {
  if (get_status_of_ts_timer() == 0)
    return;

  if (ADCDAT0 & (1<<15)) { /* 如果松开 */
    ts_timer_disable();
    enter_wait_pen_down_mode();
    return;
  }
  /* 如果触摸屏仍被按下, 进入"自动测量模式", 启动ADC */
  else { /* 按下状态 */
    /* 进入"自动测量"模式 */
    enter_auto_measure_mode();
    /* 启动ADC */
    ADCCON |= (1<<0);
  }
}

image-20240501174908562

来分析一下这个程序的过程:

1
2
3
4
5
6
7
8
9
10
11
1. 在touchscreen_init的时候我们先注册了一个timer,然后修改了定时器的产生中断的时间间隔为10ms中断一次,所以touchscreen_timer_irq会每间隔10ms调用一次。没有按下,则touchscreen_timer_irq虽然也有走,但是就直接return.

2. 然后如果按下触摸屏,产生tsc中断,启动adc,产生adc中断。

如果产生了adc中断,但是读取状态发现已经松开了,则进入”等待按下状态“,并且让touchscreen_timer_irq失效。那么要是状态是被按下,则开启ts_timer_enable。

3. 当使能touchscreen_timer_irq这个定时器中断服务程序后,并且10ms到了touchscreen_timer_irq函数执行生效。

如果松开了,则进入”等待按下状态“,并且让touchscreen_timer_irq失效,表示没有长按或者滑动。

如果任然按下,输出长按或者滑动后的坐标结果。