nrf51822是Nordic的一款BLE芯片产品,该芯片工作稳定,功耗低,性能适中,兼容性好,芯片外设还是比较丰富的,但没有提供触摸按键控制器,故而如果有这类需求即需要用户自行实现,Nordic为这类需求提供了几类参考,经过实际使用,采用人体电容感应原理的设计表现还是比较优秀的。
电容触摸按键的基本原理都是一样的,都是利用人体电容与板载电容并联后带来的电路上的变化来确定是否有按键动作,一般是通过计算电容充电时间来检测电容变化。
RC充放电的过程一般如下图所示:
充电时间除了与充电开始和结束的电压有关外,还和系统中的容值(即C)有关系,而在触摸按键设计上,人体靠近按键区域时,在此电路中的电容值就变成为人体电容加上板载电容,那么到达某个电压门限值的充电时间就会延长,只要能够设定好V1,V0和Vt,并且能足够精确地计算充电时间加上一定的平滑算法,则可以确定是否人体接触。
该方案的参考资料1。
按照上一章节的描述,要达成计算充电时间的目的,需要包括如下要素:
- 电容,触摸按键本身,也可以再并联一个电容以稳定系统;
- 充电源,使用一个GPIO为系统中的电容充电;
- 电阻,从充电GPIO接出;
- 电压检测,使用nrf51822自带的LPCOMP加上一个模拟输入脚,该比较器功耗低,检测精度不高,但是在本方案中已经足够,设置其参考电压至合适值;
- 充电时间记录,使用nrf51822自带的高精度定时器,最高频率为16MHz;
- 采样定时器,使用nrf51822的SWI来触发定期的采样。
每次采样的流程:
启动LPCOMP;
启动高精度定时器;
充电GPIO拉高,等待LPCOMP或者高精度定时器中断;
LPCOMP中断,则停止定时器,充电GPIO拉低,获取定时器采样值,进行时间统计,确认是否有人体接触;
高精度定时器中断,则LPCOMP任务,充电GPIO拉低,获取定时器采样值,进行时间统计。
static void ppi_init(void)
{
uint32_t err_code = NRF_SUCCESS;
err_code = nrf_drv_ppi_init();
APP_ERROR_CHECK(err_code);
err_code = nrf_drv_ppi_channel_alloc(&ppi_channel1);
APP_ERROR_CHECK(err_code);
err_code = nrf_drv_ppi_channel_assign(ppi_channel1,
&NRF_LPCOMP->EVENTS_UP,
&CAPSENSE_TIMER->TASKS_CAPTURE[0]);
APP_ERROR_CHECK(err_code);
err_code = nrf_drv_ppi_channel_alloc(&ppi_channel2);
APP_ERROR_CHECK(err_code);
err_code = nrf_drv_ppi_channel_assign(ppi_channel2,
&NRF_LPCOMP->EVENTS_UP,
&CAPSENSE_TIMER->TASKS_STOP);
APP_ERROR_CHECK(err_code);
// Enable both configured PPI channels
err_code = nrf_drv_ppi_channel_enable(ppi_channel1);
APP_ERROR_CHECK(err_code);
err_code = nrf_drv_ppi_channel_enable(ppi_channel2);
APP_ERROR_CHECK(err_code);
}
void nrf_capsense_init(capsense_channel_t *channel_array, capsense_config_t *config_array, uint32_t channel_num)
{
CAPSENSE_TIMER->PRESCALER = 4;
CAPSENSE_TIMER->BITMODE = TIMER_BITMODE_BITMODE_16Bit << TIMER_BITMODE_BITMODE_Pos;
CAPSENSE_TIMER->CC[1] = 1000;
CAPSENSE_TIMER->SHORTS = TIMER_SHORTS_COMPARE1_CLEAR_Msk | TIMER_SHORTS_COMPARE1_STOP_Msk;
CAPSENSE_TIMER->INTENSET = TIMER_INTENSET_COMPARE1_Msk;
CAPSENSE_TIMER->TASKS_CLEAR = 1;
NRF_LPCOMP->REFSEL = LPCOMP_REFSEL_REFSEL_SupplyFourEighthsPrescaling << LPCOMP_REFSEL_REFSEL_Pos;
NRF_LPCOMP->ANADETECT = LPCOMP_ANADETECT_ANADETECT_Up << LPCOMP_ANADETECT_ANADETECT_Pos;
NRF_LPCOMP->SHORTS = LPCOMP_SHORTS_UP_STOP_Msk;
NRF_LPCOMP->INTENSET = LPCOMP_INTENSET_UP_Msk;
NRF_LPCOMP->ENABLE = LPCOMP_ENABLE_ENABLE_Enabled << LPCOMP_ENABLE_ENABLE_Pos;
m_capsense_channel_list = channel_array;
m_channel_num = channel_num;
for(uint32_t ch = 0; ch < channel_num; ch++)
{
nrf_gpio_cfg_output(config_array[ch].output);
nrf_gpio_cfg_input(analog_ch_input_no[config_array[ch].input], NRF_GPIO_PIN_NOPULL);
channel_array[ch].ana_pin = config_array[ch].input;
channel_array[ch].out_pin = config_array[ch].output;
channel_array[ch].rolling_average = 400 * ROLLING_AVG_FACTOR;
channel_array[ch].average = 0xFFFF;
channel_array[ch].average_counter = channel_array[ch].average_int = 0;
channel_array[ch].value_debouncemask = 0;
channel_array[ch].val_max = 0;
channel_array[ch].val_min = 0xFFFF;
channel_array[ch].ch_num = ch;
}
ppi_init();
NVIC_SetPriority(CAPSENSE_TIMER_IRQ, 3);
NVIC_EnableIRQ(CAPSENSE_TIMER_IRQ);
NVIC_SetPriority(LPCOMP_IRQn, 3);
NVIC_EnableIRQ(LPCOMP_IRQn);
}
该驱动中实际使用的是1M的定时器,该定时器功耗比较低,并且1M的频率在本场景中可以满足要求。
LPCOMP的门限电压设置为1/2的系统电压,电压上穿门限时触发中断。
为了能够及时的停止定时器以及获取采样值,使用了PPI,避免系统调度带来采样精度问题。
该源码可以于softdevice并存。
void LPCOMP_IRQHandler(void)
{
if(NRF_LPCOMP->EVENTS_UP)
{
NRF_LPCOMP->EVENTS_UP = 0;
CAPSENSE_TIMER->EVENTS_COMPARE[1] = 0;
sampling_finalize(CAPSENSE_TIMER->CC[0]);
}
}
void CAPSENSE_TIMER_IRQHandler(void)
{
if(CAPSENSE_TIMER->EVENTS_COMPARE[1])
{
CAPSENSE_TIMER->EVENTS_COMPARE[1] = 0;
NRF_LPCOMP->EVENTS_UP = 0;
sampling_finalize(CAPSENSE_TIMER->CC[1]);
}
}
LPCOMP中断到达后,如果是UP事件(即充电至门限值),那么获取定时器采样值,开始对结果进行分析。
TIMER中断达到后,表示未达到门限值,使用最大值作为结果进行分析。
static void analyze_sample(capsense_channel_t *capsense_channel, uint32_t value)
{
static bool value_above_threshold = false;
capsense_channel->value = value;
if(capsense_channel->value > capsense_channel->val_max)
{
capsense_channel->val_max = capsense_channel->value;
}
if(capsense_channel->value < capsense_channel->val_min)
{
capsense_channel->val_min = capsense_channel->value;
}
if(capsense_channel->average_counter < INITIAL_CALIBRATION_TIME)
value_above_threshold = false;
else
value_above_threshold = (capsense_channel->value > (capsense_channel->average + HIGH_AVG_THRESHOLD));
capsense_channel->rolling_average = (capsense_channel->rolling_average * (ROLLING_AVG_FACTOR-1) + (capsense_channel->value*ROLLING_AVG_FACTOR))/ROLLING_AVG_FACTOR;
capsense_channel->average_counter++;
capsense_channel->average_int += capsense_channel->value;
capsense_channel->average = capsense_channel->average_int / capsense_channel->average_counter;
if(capsense_channel->average_counter > 1000)
{
capsense_channel->average_counter /= 2;
capsense_channel->average_int /= 2;
}
capsense_channel->value_debouncemask = (capsense_channel->value_debouncemask << 1) | (value_above_threshold ? 0x01 : 0);
if(capsense_channel->pressed && ((capsense_channel->value_debouncemask & DEBOUNCE_ITER_MASK) == 0))
{
capsense_channel->pressed = false;
}
if(!capsense_channel->pressed && ((capsense_channel->value_debouncemask & DEBOUNCE_ITER_MASK) == DEBOUNCE_ITER_MASK))
{
capsense_channel->pressed = true;
}
}
为了减少误判的概率,系统对获取的结果进行统计分析,获取一段时间的充电时间均值,在使用多次采样结果进行对比,经过实际测试,误判的概率还是很低的。
static void sampling_initiate()
{
NRF_LPCOMP->PSEL = m_capsense_channel_list[channel_current].ana_pin << LPCOMP_PSEL_PSEL_Pos;
NRF_LPCOMP->TASKS_START = 1;
CAPSENSE_TIMER->TASKS_CLEAR = 1;
CAPSENSE_TIMER->TASKS_START = 1;
nrf_gpio_pin_set(m_capsense_channel_list[channel_current].out_pin);
}
static void sampling_finalize(uint32_t new_sample)
{
nrf_gpio_pin_clear(m_capsense_channel_list[channel_current].out_pin);
if(channel_current < (m_channel_num-1))
{
channel_current++;
sampling_initiate();
analyze_sample(&m_capsense_channel_list[channel_current-1], new_sample);
}
else
{
analyze_sample(&m_capsense_channel_list[channel_current], new_sample);
if(m_callback)
{
m_pressed_mask_last = m_pressed_mask;
m_pressed_mask = 0;
for(int i = 0; i < m_channel_num; i++)
{
if(m_capsense_channel_list[i].pressed)
{
m_pressed_mask |= 1 << i;
}
}
m_callback(m_pressed_mask, m_pressed_mask ^ m_pressed_mask_last);
}
}
}
每次采样开始都是启动LPCOMP和TIMER,然后拉高GPIO开始充电,结束的时候则分析采样结果,并且将结果通过回调通知给应用。
static void print_channel(capsense_channel_t *cap_ch)
{
printf("\r%.1f \t%.1f \t%.1f \t%.1f ", (float)cap_ch->value , (float)(cap_ch->average) , (float)cap_ch->val_min , (float)cap_ch->val_max );
}
static void capsence_sample_timer_handler(void * p_context)
{
nrf_capsense_sample();
for(int i = 0; i < CAPS_NUM_CHANNELS; i++)
{
printf("\n");
print_channel(&m_capsense_array[i]);
}
}
static void capsense_event(uint32_t pin_mask, uint32_t pin_changed_mask)
{
nrf_gpio_pin_write(LED_1, ((pin_mask & 0x01) ? 0 : 1));
}
static void capsense_init(void)
{
uint32_t err_code;
capsense_config_t capsense_config[] = {{ANA_AIN2_P01, 12}};
nrf_capsense_init(m_capsense_array, capsense_config, CAPS_NUM_CHANNELS);
nrf_capsense_set_callback(capsense_event);
err_code= app_timer_create(&m_capsense_timer_id,
APP_TIMER_MODE_REPEATED,
capsence_sample_timer_handler);
err_code= app_timer_start(m_capsense_timer_id, CAPSENSE_SAMPLE_DELAY, NULL);
}
int main(void)
{
capsense_init();
}
除了将相关资源定义完成并且传给capsense,还定义了app_timer,采样定时器并不需要太高的精度,使用系统自带的app_timer足够了。
上述方案是可行的,并且效果良好,但是采用该方案后系统功耗有所提升,且无法进入SYSTEM_OFF模式,nrf51822的功耗优势难于体现,如果能够有另外一种方法,使得系统在进行off模式的时候触摸按键也能够工作(哪怕抗干扰性能差一点也可以),那么与该方案配合将成为一个比较完善的触摸按键方案。