Распознавание жестов с помощью APDS-9960

    image

    Читая комментарии к моей предыдущей статье про APDS-9960, где речь шла про распознавание цвета и уровня освещенности для меня стали очевидными две вещи: 1) тема распознавания жестов интересна и 2) тема эта не раскрыта.

    Действительно, если уж взялся за описание APDS-9960, то без рассмотрения жестов описание это выглядит несколько незавершенным. Поэтому я нашел свободное время, чтобы исследовать и эту тему тоже.

    В данной статье я предлагаю Вашему вниманию обзор возможностей для распознавания жестов которые предоставляет сенсор APDS-9960.

    В статье будет рассмотрен механизм настройки сенсора, сбор данных, их обработка и представление. Вы сами сможете убедиться в том насколько это просто — работать с жестами с помощью APDS-9960.

    Как и в прошлый раз, статья будет сопровождаться кодом, все происходящее в котором будет подробно описано. Полная версия кода доступна в конце статьи.

    Сразу небольшая ремарка: встроенного автоматического механизма определения жестов у APDS-9960 не предусмотрено; то есть такого, чтобы прям вот, прочитал, значит, регистр, а там уже и жест обработанный лежит — такого в APDS-9960 нет; а это означает, что придется писать свой алгоритм интерпретации жестов, чем впоследствии и займемся.

    Вообще, это одновременно и хорошо и не очень. Не очень — потому что может усложнить исследование данного сенсора для начинающего, а хорошо, потому что, вкупе с данными о приближении, можно, изощряясь, вообще напридумывать собственных жестов различных каких угодно разнообразных и всяких.

    Но, поскольку данная статья несет лишь обзорную функцию, мы ограничимся только базовыми UP-DOWN-LEFT-RIGHT жестами.

    Ну что же, приступим.

    Теория


    Позволю себе чуточку матчасти.

    Для получения необходимой информации о движении и направлении движения в APDS-9960 используются ИК светодиод и четыре фотодиода, которые, как наглядно проиллюстрировано на рисунке ниже, регистрируют сигналы в диапазоне ближнего ИК (NIR).

    image

    ИК светодиод (LED) несет функцию подсветки, а фотодиоды (UDLR) регистрируют отраженный от «препятствия» свет.

    Фотодиоды расположены на сенсоре таким образом, что в зависимости от направления движения «препятствия», соответствующий фотодиод получит большую часть отраженного ИК-сигнала на входе и меньшую часть на выходе. В то же время документация на APDS-9960 недвусмысленно подсказывает нам, что интерпретировать направление движения можно измеряя и сравнивая амплитуду и разность фаз сигналов с фотодиодов UDLR.

    image

    Практика


    Для работы с APDS-9960, как и в прошлой раз, будем использовать STM32VLDISCOVERY. Подключение также не поменялось.

    Настройка APDS-9960

    Производим первоначальную настройку сенсора.

    Вот так вот:

    APDS9960_init
    void APDS9960_init(void) {
    
    	i2c1_write(APDS9960_CONTROL, DEFAULT_PGAIN);
    	i2c1_write(APDS9960_GPENTH, DEFAULT_GPENTH);
    	i2c1_write(APDS9960_GEXTH, DEFAULT_GEXTH);
    	i2c1_write(APDS9960_GCONF2, DEFAULT_GGAIN);
    	i2c1_write(APDS9960_GPULSE, DEFAULT_PULSE_LENGTH);
    	i2c1_write(APDS9960_PPULSE, DEFAULT_PULSE_LENGTH);
    
    }
    

    Что же здесь происходит? Давайте разбираться.

    i2c1_write(APDS9960_CONTROL, DEFAULT_PGAIN);

    PGAIN (Proximity Gain Control) — это параметр который управляет коэффициентом усиления чувствительности приближения. Присвоим ему значение 2, что соответствует усилению в четыре раза.

    i2c1_write(APDS9960_GPENTH, DEFAULT_GPENTH);
    i2c1_write(APDS9960_GEXTH, DEFAULT_GEXTH); 

    GPENTH (Gesture Proximity Enter Threshold Register) — этот параметр устанавливает пороговое значение близости для определения начала распознавания жеста.

    GEXTH (Gesture Exit Threshold Register), соответственно, устанавливает пороговое значение для определения окончания распознавания жеста.

    i2c1_write(APDS9960_GCONF2, DEFAULT_GGAIN); 

    В регистре GCONF2 (Gesture configuration two) мы явно устанавливаем только параметр GGAIN (Gesture Gain Control) в значение усиления в четыре раза.

    i2c1_write(APDS9960_GPULSE, DEFAULT_PULSE_LENGTH);
    i2c1_write(APDS9960_PPULSE, DEFAULT_PULSE_LENGTH); 

    Подсветка. По умолчанию значение для источника тока ИК светодиода подсветки установлено в 0, что соответствует току в 100 мА, нас это вполне устроит — менять не будем.

    ИК подсветка в APDS-9960 представляет собой последовательность импульсов и характеризуется соответствующими параметрами регистров для жестов GPULSE (Gesture pulse count and length): GPLEN (Gesture Pulse Length) и GPULSE (Number of Gesture Pulses), а также приближения PPULSE (Proximity Pulse Count Register): PPLEN (Proximity Pulse Length) и PPULSE (Proximity Pulse Count) задающими количество импульсов и период каждого отдельного импульса.

    Определим, что GPLEN и PPLEN примут значение 2 равное 16 мкс, а GPULSE и PPULSE значение 9, которое соответствует 10 импульсам.

    Как видите, настройка оказалась ненамного сложнее аналогичной для распознавания цветов и освещения из предыдущего обзора APDS-9960.

    Чтение данных

    Теперь переместимся в основной цикл программы, в котором начнем то и дело регистрировать и интерпретировать данные с фотодиодов, а также научимся находить отличия одного жеста от другого.

    Перво-наперво, стартуем APDS-9960 с функциями работы с жестами и приближением.

    GesturesSet(GESTURES_START); 

    И сразу же начинаем отслеживать параметр GVALID. GVALID (Gesture FIFO Data) — это параметр регистра GSTATUS (Gesture Status Register), который, находясь в отличном от нуля состоянии, сообщает нам о том, что у сенсора имеются пригодные для использования данные о жестах.

    Документация учит нас, что информация о жестах находится в буфере, в области оперативной памяти, которая в общем случае имеет размер 32 x 4 байт.

    На практике, фактический размер этого буфера можно узнать прочитав значение регистра GFLVL (Gesture FIFO level), т.е. по моим сугубо эмпирическим экспериментальным наблюдениям, получается GFLVL*4. Как-то так:

    image

    Ну и как следует из названия буфера, данные в нем располагаются в порядке First In — First Out. То есть, грубо говоря, чем «раньше» поступил сигнал с каждого из фотодиодов тем «выше» в GFLVL он располагается.

    Данные с фотодиодов (UDLR) можно прочитать из соответствующих регистров Gesture FIFO Register:

    — GFIFO_U (Gesture FIFO Data, UP)
    — GFIFO_D (Gesture FIFO Data, DOWN)
    — GFIFO_L (Gesture FIFO Data, LEFT)
    — GFIFO_R (Gesture FIFO Data, RIGHT)

    После каждого чтения значений из этих регистров, GFLVL декрементируется; таким образом, по хорошему, необходимо произвести чтение полностью всего буфера до момента пока GFLVL не достигнет нуля.

    Для определения жестов нам понадобятся только первые четыре байта этого буфера, не больше. Поэтому и прочитаем мы только их.

    GestureUp = i2c1_read(APDS9960_GFIFO_U);
    GestureDown = i2c1_read(APDS9960_GFIFO_D);
    GestureLeft = i2c1_read(APDS9960_GFIFO_L);
    GestureRight = i2c1_read(APDS9960_GFIFO_R);
    

    Распознавание жестов

    Чтобы интерпретировать какой же именно жест произошел, произведем нехитрые вычисления:

    GestUpDown = GestureUp-GestureDown;
    GestLeftRight = GestureLeft-GestureRight; 

    Для определения того какой из жестов в данный момент случился нам важны не сами значения GestUpDown и GestLeftRight, а только лишь знак, так сказать, вещественного числа.

    То есть, иными словами, принимая на вход отрицательные и положительные значения переменных GestUpDown и GestLeftRight определяем какой жест совершен.

    Таблица истинности для переменных GestUpDown и GestLeftRight представлена на рисунке ниже

    image

    Теперь обнулим GFLVL:

    GesturesSet(GESTURES_STOP); 

    … и вернемся в начало основного цикла программы.

    А теперь весь код целиком:

    main.c
    
    #include "stm32f10x.h"
    
    #define APDS9960_I2C_ADDR       0x39
    #define APDS9960_ENABLE         0x80
    #define APDS9960_GSTATUS        0xAF
    #define APDS9960_GFLVL          0xAE
    
    //Gesture FIFO Register (0xFC – 0xFF):
    #define APDS9960_GFIFO_U        0xFC
    #define APDS9960_GFIFO_D        0xFD
    #define APDS9960_GFIFO_L        0xFE
    #define APDS9960_GFIFO_R        0xFF
    
    
    #define APDS9960_CONTROL        0x8F
    #define APDS9960_GPENTH         0xA0
    #define APDS9960_GEXTH          0xA1
    #define APDS9960_GCONF2         0xA3
    #define APDS9960_GPULSE         0xA6
    #define APDS9960_PPULSE         0x8E
    
    #define GESTURES_START          0x01
    #define GESTURES_STOP           0x02
    
    #define DEFAULT_GPENTH          40      // Threshold for entering gesture mode
    #define DEFAULT_GEXTH           30      // Threshold for exiting gesture mode    
    #define DEFAULT_PGAIN           8 			// Proximity Gain Control: 4X 
    #define DEFAULT_GGAIN           0x40		// Gesture Gain Control: 4X
    
    #define DEFAULT_PULSE_LENGTH    0x89    // 16us, 10 pulses
    
    /* Bit fields */
    #define APDS9960_PON            0x01
    #define APDS9960_AEN            0x02
    #define APDS9960_PEN            0x04
    #define APDS9960_WEN            0x08
    #define APSD9960_AIEN           0x10
    #define APDS9960_PIEN           0x20
    #define APDS9960_GEN            0x40
    #define APDS9960_GVALID         0x01
    
    int GestUpDown = 0;
    int GestLeftRight = 0;
    
    //-----------------------------------------------------------------------
    
    uint8_t i2c1_read(uint8_t addr);
    void i2c1_write(uint8_t addr, uint8_t data);
    
    void I2C1_init(void)
    {
    	
    	I2C_InitTypeDef I2C_InitStructure;
      GPIO_InitTypeDef  GPIO_InitStructure;
    
      RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);
      RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB| RCC_APB2Periph_AFIO , ENABLE);
    
    	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;
    	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;
    	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
    	GPIO_Init(GPIOB, &GPIO_InitStructure);
    	
    	I2C_StructInit(&I2C_InitStructure);
    	I2C_InitStructure.I2C_ClockSpeed = 100000;
    	I2C_InitStructure.I2C_OwnAddress1 = 0x01;
    	I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;
    	I2C_Init(I2C1, &I2C_InitStructure);
    	I2C_Cmd(I2C1, ENABLE);
    
    }
    
    //-----------------------------------------------------------------------
    
    void APDS9960_init(void) {
    
    	i2c1_write(APDS9960_CONTROL, DEFAULT_PGAIN);		
    	i2c1_write(APDS9960_GPENTH, DEFAULT_GPENTH);
    	i2c1_write(APDS9960_GEXTH, DEFAULT_GEXTH);
    	i2c1_write(APDS9960_GCONF2, DEFAULT_GGAIN);
    	i2c1_write(APDS9960_GPULSE, DEFAULT_PULSE_LENGTH);
    	i2c1_write(APDS9960_PPULSE, DEFAULT_PULSE_LENGTH);
    	
    }
    
    //-----------------------------------------------------------------------
    
    uint8_t i2c1_read(uint8_t addr)
    {
    	uint8_t data;
    	while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY));
    	I2C_GenerateSTART(I2C1, ENABLE);
    	while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
    	I2C_Send7bitAddress(I2C1, APDS9960_I2C_ADDR<<1, I2C_Direction_Transmitter);
    	while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
    	I2C_SendData(I2C1, addr);
    	while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
    	I2C_GenerateSTART(I2C1, ENABLE);
    	while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
    	I2C_Send7bitAddress(I2C1, APDS9960_I2C_ADDR<<1, I2C_Direction_Receiver);
    	while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_RECEIVED));
    	data = I2C_ReceiveData(I2C1);
    	while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED));
    	I2C_AcknowledgeConfig(I2C1, DISABLE);
    	I2C_GenerateSTOP(I2C1, ENABLE);
    	while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY));
    	return data;
    }
    
    //-----------------------------------------------------------------------
    
    void i2c1_write(uint8_t addr, uint8_t data)
    {
    	I2C_GenerateSTART(I2C1, ENABLE);
    	while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
    	I2C_Send7bitAddress(I2C1, APDS9960_I2C_ADDR<<1, I2C_Direction_Transmitter);
    	while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
    	I2C_SendData(I2C1, addr);
    	while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
    	I2C_SendData(I2C1, data);
    	while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
    	I2C_GenerateSTOP(I2C1, ENABLE);
    	while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY)) {};
    
    }
    
    //-----------------------------------------------------------------------
    
    void GesturesSet(uint8_t GestSel) {
    	
    	switch (GestSel)  
    	{  
    		case GESTURES_START:
    			i2c1_write(APDS9960_ENABLE, APDS9960_GEN | APDS9960_PEN | APDS9960_PON);
    			break;
    		case GESTURES_STOP:
    			i2c1_write(APDS9960_ENABLE, APDS9960_PEN | APDS9960_PON);
    			break;  
    		default:
    			i2c1_write(APDS9960_ENABLE, APDS9960_GEN | APDS9960_PEN | APDS9960_PON);			
    	}
    	
    }
    
    //-----------------------------------------------------------------------
    
    int main()
    {
    	
    	uint8_t GFLVL_buf = 0;
    	
    	uint8_t GSTATUS_buf = 0;
    
    	uint8_t GestureUp = 0;
    	uint8_t GestureDown = 0;
    	uint8_t GestureLeft = 0;
    	uint8_t GestureRight = 0;
    	
    	I2C1_init();
    	
    	APDS9960_init();
    
      while (1)
      {
    
    		GFLVL_buf = 0;
    		
    		GSTATUS_buf = 0;
    
    		GestureUp = 0;
    		GestureDown = 0;
    		GestureLeft = 0;
    		GestureRight = 0;
    		
    		GestUpDown = 0;
    		GestLeftRight = 0;
    		
    		GesturesSet(GESTURES_START);
    
    		GSTATUS_buf = i2c1_read(APDS9960_GSTATUS);
    
    		if(GSTATUS_buf & APDS9960_GVALID) {
    		
    			GFLVL_buf = i2c1_read(APDS9960_GFLVL);
    
    			if(GFLVL_buf) {
    				
    				GestureUp = i2c1_read(APDS9960_GFIFO_U);
    				GestureDown = i2c1_read(APDS9960_GFIFO_D);
    				GestureLeft = i2c1_read(APDS9960_GFIFO_L);
    				GestureRight = i2c1_read(APDS9960_GFIFO_R);				
    
    				//Truth table:
    				//UP: 	 GestUpDown(+) | GestLeftRight(+)
    				//DOWN:  GestUpDown(-) | GestLeftRight(-)
    				//LEFT:  GestUpDown(+) | GestLeftRight(-)
    				//RIGHT: GestUpDown(-) | GestLeftRight(+)
    				
    				GestUpDown = GestureUp-GestureDown;
    				GestLeftRight = GestureLeft-GestureRight;
    						
    				GesturesSet(GESTURES_STOP);
    
    			}
    			
    		}
    			
    	}
    
    }
    
     


    Хочу отметить, что механизм жестов у APDS-9960 работает очень даже неплохо. Распознавание стабильное, хорошо работают встроенные в APDS-9960 UV and IR фильтры.

    Надеюсь, данный материал кому-нибудь окажется полезен. Спасибо за внимание.
    • +12
    • 6,1k
    • 4
    Поделиться публикацией

    Комментарии 4

      0

      Тоже игрался с этим датчиком в своей домашней поделке.
      Правда, глубоко вникать в даташиты не пришлось, т.к. нашел готовую библиотеку: https://learn.sparkfun.com/tutorials/apds-9960-rgb-and-gesture-sensor-hookup-guide#arduino-library-installation
      Адаптировал я её к STM32CubeMX, заодно отрефакторив код так, что он стал сильно короче оригинала.
      Там был один затык. Покупал я не оригинальный спаркфановский датчик, а дешевый аналог с ebay. И никак не хотело работать распознавание жестов. Оказалось, что константы инициализации для Sparkfun отличаются от тех, с которыми прекрасно заработал мой экземпляр.

        0
        Спасибо за комментарий. Вы молодец, что интересуетесь; однако «глубоко вникать в даташиты не пришлось...» — вот это не очень хорошо. Сейчас большинство начинающих игнорируют спеки. Лично я считаю такую практику порочной, поэтому свой обзор на APDS-9960 я специально задумывал таким, чтобы был больший уклон в детальное описание внутреннего устройства сенсора, принципов его работы и вдумчивое взаимодействие с ним с помощью МК. Кстати, для обзора в качестве МК выбран STM32 тоже не случайно; такой выбор был обусловлен изрядной популярностью сего МК на просторах СНГ.
        У меня APDS-9960 модуль тоже не от SparkFun, а производства фирмы RobotDyn, Вы просто, видимо, мою первую статью не читали, вот: habr.com/post/423847.
        По поводу констант для жестов — константы для порога начала и окончания распознавания жеста от SparkFun я считаю наиболее удачными, поэтому и использовал их без изменения — как есть. Проверьте внимательнее — все ли Вы делаете верно? Комментарии — это хорошо, но еще лучше — публикуйте свою статью, было бы интересно. Сенсор APDS-9960 я нахожу весьма занятным, однако статей, с детальным его описанием, в рунете почти нет. Желаю Вам успехов!
          0
          Спасибо за статью. Может, подскажете, как поведёт себя датчик, если его накрыть, например, стеклом? Чем непрозрачным (для видимого света) вообще можно закрывать этот датчик (в эстетических целях), чтобы он не глючил?
            0
            Пожалуйста. Извините, не подскажу; потому как таких экспериментов не экспериментировал.

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое