厨房里的突发奇想
如果做饭时使用触控设备,您可能会注意到按照设备列出的食谱烹饪并非想象得那么简单。技术达人(例如鄙人)走进厨房时,喜欢看着平板电脑或智能手机上的菜谱做饭。您可能会说:“好吧,这有什么难度?”由于屏幕始终开启会消耗很大电量,通常手持装置在1、2分钟后没有操作时将自动进入休眠状态。那么,当您需要参照食谱时,设备已进入休眠状态。此事,您面临两个选择:要么强制屏幕保持永久开启;要么用沾满食物的手开启装置,而在屏幕上留下斑斑油渍。当然,您可以在每次查看时把手清洗干净,但不断重复洗手、擦干即繁琐,又费水。
我时常问自己:“怎样才能既不让屏幕始终开启,又不会弄脏装置?”实际上,有一种办法一举两得,即通过一个手势(不用接触屏幕)开启显示屏。听起来似乎很复杂,是吗?幸运的是,做起来可能比听起来容易一些。
接近检测传感器
许多触摸屏装置,尤其是智能手机,内部已经安装了红外(IR)接近检测传感器。这些传感器一般在通话期间自动打开/关闭屏幕,以避免意外操作手机的输入界面。这种传感器,加上精明的软件设计,就能实现利用一个手势唤醒装置的功能。
基本的设计思路是:设备进入休眠状态时,触摸屏关闭,应用处理器处于低功耗模式,依靠接近检测传感器“观察”背景的变化,当接收到的信号足够大时,做出适当反应。这与接近检测传感器在通话期间关闭屏幕的功能几乎完全相同。只是,我们的应用对数据有了不同的解释。
首先记录传感器在“正常”背景下的计数值,此时得到的数值可能为零,但实际设计中需要考虑系统失调(例如:散射或串扰)。然后将得到的数值设置为检测门限,当接收信号超过门限时触发中断或向应用处理器发送信号,以唤醒系统并打开屏幕。总体而言,这种方法非常简单、直观,可利用环境光检测器和IR接近检测传感器实现。
本文介绍的方案采用MAX44000,接近检测的数据读取时间间隔可以设置在1.56ms至100ms (与环境光检测传感器轮流读取数据)。假设最大检测距离为10cm,LED的辐射角为±15°,那么,可以覆盖的面积大约为22cm2或跨距大约为5.35cm,只有该区域内的移动目标才能捕捉到。由此,能够以最慢(即最低功耗)的采样速度可靠检测的最快手势动作大约为0.53mps。在此,我们还假设传感器只需要采集到一次高于门限的信号,即可识别经过覆盖区域的目标。
举手之劳...
理论上讲,该方案的实施非常简单。当装置进入休眠模式时,将接近检测传感器置为环境扫描模式,并在检测到目标时发出中断信号,指示捕捉到超过预设门限的信号。可通过I2C接口轮询传感器的状态。不幸的是,这种方式会消耗过大功率,超出了大多数用户的预期。
这也是接近检测传感器的设计重点,MAX44000传感器能够在许多方面摆脱应用处理器的干预,减轻处理器负荷(降低功耗)。
使能MAX44000的内部接近检测中断(寄存器0x01的第1位),可将唤醒门限写入内部寄存器(0x0B和0x0C)。当接近检测传感器的读数超过该门限时,触发中断标识置位,将MAX44000的/INT引脚置为低电平。当应用处理器检测到该引脚驱动为低电平时,可唤醒装置退出低功耗模式,并打开屏幕,或完成其它需要的动作。
…但不容忽视
实际应用往往不如理论那么容易,非接触唤醒的具体实施并非只是简单地检测高于门限的信号。实际上,具体的设计需要考虑诸多因素。
信号电平与电路布局
最关键的考虑应该是触发唤醒条件的信号电平,需要在系统响应灵敏度与误报概率之间进行权衡。如果门限过低,则很容易检测到输入(手势工作),但会增大瞬态噪声或突发条件产生误报的概率。反之,过高的检测门限能够把误报概率降至几乎为零,但却只能检测到非常接近的目标,甚至对任何输入(即使您疯狂晃动手臂)都反应迟钝。
解决这一问题的最佳方式是:首先降低系统噪声,可以通过光学方法或严谨的电路布局实现,降低的噪底有助于降低误报概率;其次,选择“平均”检测距离(例如:4cm至5cm)并利用参考目标测量信号,18%的灰板比较理想,但如果触摸屏上方安装了黑色玻璃,测量时也应该使用这样的玻璃,所测得的信号电平可以作为设置门限的最佳参考。通常可以遵循这样的原则:即将电平设置在满幅的8%至15%,即使电平发生变化。
可以按照上述经验数据设置MAX44000传感器的接近检测门限寄存器,图1所示为信号强度随距离变化的关系曲线,采用18%灰板,驱动电流为100mA,传感器上方没有玻璃罩。蓝线为可以选择的唤醒门限。
图1. MAX44000接近检测传感器信号强度随距离变化的关系曲线,采用18%灰板,100mA驱动电流,没有玻璃罩。
需要考虑噪声问题时,可利用低通滤波器处理信号;另外,MAX44000还有几个控制位可以用作触发中断标识之前的屏蔽,采用这种设置时,需要检测到一定数量超出门限的采样值时才会触发中断标示,能够在一定程度上降低噪声的影响。
一种稍微复杂的方法是将传感器的读数储存在数据队列中,然后利用定制的FIR软件对其进行滤波处理。但这种方法需要提高接近检测传感器的采样速率,否则则会降低能够捕捉到的传感器可视范围内的手势动作速率,特别是把采样速率设置在100ms时。利用器件的控制位屏蔽检测时,速率可最多降低16倍(通常选择4x屏蔽即可)。
手势速度
手势动作的快慢是我们需要考虑的另一因素。最大速度取决于:1. 传感器的可视范围;2. 手与传感器之间的距离;3. 采样率;4. 检测门限。前两项很容易确定:传感器的检测角度,结合传感器与目标之间的距离,利用基本的三角形即可计算出传感器可视范围内目标的移动距离。例如,如果传感器的视角为30度,最大有效检测距离10cm,那么,传感器可视范围内允许的目标移动距离为5.35cm,覆盖面积大约为78cm2。直线距离结合采样率,即可决定速度限值。 具体地说,如果采样率为T,那么目标跨越可视区域的时间不得小于T。例如,如果T为100ms (MAX44000的最低采样速率),那么按照上例,理论上最大允许的速率为1mps (这实际上已经相当快了)。您可能希望捕获到多个采样值来确认触发唤醒,这样的话,会降低允许的速率下限。
检测门限也影响最大允许速率。一般来说,门限越低,能够捕捉到的手势动作就越快。如上所述,应谨慎选择门限,以免产生误报。
人为因素
这种应用还会受到人手以及挥手动作等人为因素的影响。应通过一些案例确定一般大多数人的习惯,包括他们在屏幕前挥动手掌的速度以及与屏幕之间的距离,另外,是否戴手套也会产生一定的影响。不同的应用场合(不同装置)也会影响到设计需求,例如智能手机、平板电脑或汽车仪表盘,对存在具体的设计考虑。当然,设计过程中还应考虑用户界面和经验参数。
最后,还要对真假手势做出判断,即装置需要判断接收到的信号是来自于一个手势动作,还是简单的装置移动(例如:放置在外套、口袋或背包中,或者是屏幕朝下放置)。单纯依靠上述检测原理,很难做出正确的“真伪”鉴别,除非在装置内提供更多的背景信息。关于这一问题的讨论超出了本文范围。
设计中可以选择只有装置进入特定的应用程序时启动唤醒方案,也可以由用户手动操作使能。此外,许多此类装置都有一个加速度传感器,能够检测到屏幕是否背面朝下放置。如果用户手动将装置置于休眠模式,则可禁用该功能(例如关机状态)。
设计实例
为方便起见,本文附带了三段演示程序代码。第一段代码用于手动操作MAX44000的接近检测数据读取,概念上简单实现唤醒功能;第二段代码在第一段的基础上进行了扩展,增加了之前讨论的滤波功能;最后一段代码演示利用MAX44000中断唤醒触控装置。
示例代码1
__interrupt void TimedInterrupt( void )
{
uint8 proximity_counts;
....
....
if ( device_status == SLEEP_MODE )
{
// read one byte from register 0x16
proximity_counts = read_i2c_register(MAX44000_ADDR,0x16,1);
if (proximity_counts > WAKEUP_THRESHOLD)
{
device_status = WAKE_MODE;
...
}
else
{
// do whatever it is you need to in sleep mode
...
...
}
}
...
...
}
示例代码2
// example interrupt function where this might be implemented
__interrupt void TimedInterrupt( void )
{
uint8 proximity_counts;
uint8 filtered_counts;
....
....
if ( device_status == SLEEP_MODE )
{
// read one byte from register 0x16
proximity_counts = read_i2c_register(MAX44000_ADDR,0x16,1);
// weights[QUEUE_SIZE] contains the filter weights for the FIR filter
// data_queue[QUEUE_SIZE] is a FIFO queue meant to be the input to the filter
filtered_counts = fir_filter(proximity_counts,weights,data_queue);
if (filtered_counts > WAKEUP_THRESHOLD)
{
device_status = WAKE_MODE;
...
}
else
{
// do whatever it is you need to in sleep mode
...
...
}
}
...
...
}
/**
* fir_filter()
*
* Implements an FIR filter in the form
* y = w[0]*x[0] + w[1]*x[1] + ...+ w[QUEUE_SIZE]*x[QUEUE_SIZE]
*
* Arguments:
* uint8 input - newest datapoint taken (that is, x[0])
* uint8 *weights - w[0]...w[QUEUE_SIZE]
* uint8 *queue - the discrete sequence x[0]...x[QUEUE_SIZE]
*
* Returns:
* The FIR-filtered output, y
*/
uint8 fir_filter(uint8 input, uint8 *weights, uint8 *queue)
{
uint8 i;
int sum = 0;
// pop first entry in the queue, then
// push new data into the last position
push_into_queue(queue,input);
// input is now x[0]
for (i=0; i {
sum += weights[i]*queue[i];
}
return (sum/QUEUE_SIZE);
}
示例代码3
// this handles hardware-level interrupts on the micro
__interrupt void irq_handler( void )
{
...
// if the hardware interrupt came from the MAX44000 sensor
// pulling its \INT pin low
if ( irq_source == MAX44000 )
{
// if the device is in sleep mode
if (device_status == SLEEP_MODE)
{
device_status = WAKE_MODE; // wake up the device
...
// reconfigure whatever else you need here as the system wakes up
}
// otherwise, handle it however it is you wish
else
{
...
}
}
...
}
/**
* configure_max44000_for_sleep_mode()
*
* Sets up the MAX44000 to trigger a hardware interrupt when the proximity
* counts go above some set threshold.
*
* Arguments:
* uint8 upper_threshold - the set threshold (8-bit mode)
*
* Returns:
* n/a
*/
void configure_max44000_for_sleep_mode(uint8 upper_threshold)
{
uint8 max44000_thresh_registers[] = {0x0B,0x0C};
uint8 max44000_upper_thresh[] = {0x40,0};
max44000_upper_thresh[1] = upper_threshold;
// do a consecutive write of 0 followed by upper_threshold to
// registers 0xB and 0xC, respectively
// MAX44000_ADDR is usually 0x94
// interrupt will trigger only if proximity value is above the threshold
write_i2c_register(MAX44000_ADDR,max44000_thresh_registers,
max44000_upper_thresh,2);
// write to bits 2 and 3 of register 0x0A here if you wish to set the
// persist time to anything other than one sample
// writes to register 0x01 to enable interrupts on the MAX44000
max44000_enable_interrupt();
return;
}