Подключение тепловизора Seek Thermal к STM32

    Подключить тепловизор к микроконтроллеру? Без проблем! Особенно если это STM32 с интерфейсом USB Host и тепловизор Seek Thermal от Даджет!


    Паяльник глазами тепловизора SeekThermal

    Введение


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

    Сегодня речь пойдёт о подключении тепловизора Seek Thermal к микроконтроллеру STM32. А предоставила мне данное устройство компания Даджет. На просторах Geektimes данный тепловизор рассматривался не раз: освещалась, в основном, его работа с Android, а также проскакивала статья о подключении данного устройства к ПК. В своём обзоре я хочу рассказать о собственном опыте подключения тепловизора Seek Thermal к микроконтроллеру STM32 через USB хост.

    Аппаратные требования


    Не такие уж и специфические! Всё что должен иметь Ваш STM32 — это USB интерфейс, способный работать в режиме Host и какой-нибудь интерфейс для управления ЖК экраном. Самый очевидный выбор — это взять STM32F4 — Discovery. У меня под рукой оказалась плата STM32F746G-Discovery. Соответственно описание будет для этой платы, но! Т.к. код сгенерирован в среде CubeMX, возможно применить и другую EVM. Считаю применённую мной плату избыточной для данного проекта.

    Программная часть


    Данный тепловизор не реализует какой-либо класс при общении по USB. Всё взаимодействие осуществляется напрямую, bulk-запросами через эндпойнты. Отправляя команды (реквесты) на control эндпойнт, можно включить тепловизор, откалибровать его, и заставить передать кадр, или несколько кадров. Особенно подробно работа с Seek Thermal описана на данном форуме.

    Таким образом, для работы тепловизора с микроконтроллером STM32, нам необходимо:

    1) Взять любой пример USB Host для Вашей любимой платы (я взял STM32 USB Host CDC example из коллекции примеров STM32F7 CubeMX);
    2) Выкинуть оттуда процедуру инициализации класса устройства;
    3) Написать удобные обёртки для работы с функциями чтения/записи в управляющие эндпойнты и эндпойнты данных;
    4) Написать свою функцию по преобразованию сырых данных в нечто отображаемое;
    5) Задействовать LUT (color Look Up Table) для раскрашивания монохромной картинки в цветную. Эта фитча появилась в семействе микроконтроллеров STM32, которые могут самостоятельно управляться с ЖК экранами.

    Для начала сделаем что-то похожее на кусочек из libusb, который поможет нам связать HAL Library с последующим кодом:

    Код процедуры из libusb
    int libusb_control_transfer(libusb_device_handle* dev_handle,
                                uint8_t request_type, uint8_t bRequest, uint16_t wValue, uint16_t wIndex,
                                unsigned char* data, uint16_t wLength, unsigned int timeout) {
    
        hUSBHost.Control.setup.b.bmRequestType = request_type;
        hUSBHost.Control.setup.b.bRequest      = bRequest;
        hUSBHost.Control.setup.b.wValue.w      = wValue;
        hUSBHost.Control.setup.b.wIndex.w      = wIndex;
        hUSBHost.Control.setup.b.wLength.w     = wLength;
    
        int status;
        
        do {
            status = USBH_CtlReq(&hUSBHost, data, wLength);
        } while (status == USBH_BUSY);
    
        if (status != USBH_OK)  {
            hUSBHost.RequestState = CMD_SEND;
            return 0;
    
        } else {
            return wLength;
        }
    
    }
    


    Затем сходим сюда и подсмотрим процедуру vendor_transfer. Также, не помешает обратить внимание на список запросов struct Request.

    код процедуры vendor_transfer
    int vendor_transfer(bool direction, uint8_t req, uint16_t value, uint16_t index, uint8_t * data, uint8_t size, int timeout)
    {
    	int res;
    	uint8_t bmRequestType = (direction ? LIBUSB_ENDPOINT_IN : LIBUSB_ENDPOINT_OUT)
    	 | LIBUSB_REQUEST_TYPE_VENDOR | LIBUSB_RECIPIENT_INTERFACE;
    	uint8_t bRequest = req;
    	uint16_t wValue = value;
    	uint16_t wIndex = index;
    
    
    	uint8_t * aData = data;
    	uint16_t wLength = size;
    
    	if (!direction) {
    		// to device
    #ifdef LOG_DEBUG
    		USBH_UsrLog("ctrl_transfer(0x%x, 0x%x, 0x%x, 0x%x, %d)", bmRequestType, bRequest, wValue, wIndex, wLength);
    		printf(" [");
    		for (int i = 0; i < wLength; i++) {
    			printf(" %02x", data[i]);
    		}
    		printf(" ]\n");
    #endif
    		res = libusb_control_transfer(handle, bmRequestType, bRequest, wValue, wIndex, aData, wLength, timeout);
    #ifdef LOG_DEBUG
    		if (res != wLength) {
    							USBH_UsrLog("Bad returned length: %d\n", res);
    						}
    #endif
    	}
    	else {
    		// from device
    #ifdef LOG_DEBUG
    		USBH_UsrLog("ctrl_transfer(0x%x, 0x%x, 0x%x, 0x%x, %d)",
    				 bmRequestType, bRequest, wValue, wIndex, wLength);
    #endif
    				res = libusb_control_transfer(handle, bmRequestType, bRequest,
    				 wValue, wIndex, aData, wLength, timeout);
    #ifdef LOG_DEBUG
    				if (res != wLength) {
    					USBH_UsrLog("Bad returned length: %d\n", res);
    				}
    				printf(" -> [");
    				for (int i = 0; i < res; i++) {
    					printf(" %02x", data[i]);
    				}
    				printf(" ]\n");
    #endif
    	}
    	return res;
    }
    


    Далее, напишем процедуру приёма картинки. Тут особо комментировать нечего, подсмотрели в CDC Example.

    Процедура приёма данных по USB
    int CAM_ProcessReception(USBH_HandleTypeDef *phost)
    {
    USBH_URBStateTypeDef URB_Status = USBH_URB_IDLE;
    uint16_t length = 0;
    uint8_t data_rx_state = CDC_RECEIVE_DATA;
    
    size = FRAME_WIDTH * FRAME_HEIGHT;
    int bufsize = size * sizeof(uint16_t);
    
    int bsize = 0;
    while (data_rx_state != CDC_IDLE) {
      switch(data_rx_state)
      {
    
      case CDC_RECEIVE_DATA:
    
        USBH_BulkReceiveData (phost, &rawdata[bsize], 512, InPipe);
    
        data_rx_state = CDC_RECEIVE_DATA_WAIT;
    
        break;
    
      case CDC_RECEIVE_DATA_WAIT:
    
        URB_Status = USBH_LL_GetURBState(phost, InPipe);
    
        /*Check the status done for reception*/
        if(URB_Status == USBH_URB_DONE )
        {
          length = USBH_LL_GetLastXferSize(phost, InPipe);
    
          bsize+= length;
    
          if(((bufsize - length) > 0) && (bsize < bufsize)) //TODO
          {
    
            data_rx_state = CDC_RECEIVE_DATA;
          }
          else
          {
            data_rx_state = CDC_IDLE;
    
          }
    #if (USBH_USE_OS == 1)
          osMessagePut ( phost->os_event, USBH_CLASS_EVENT, 0);
    #endif
        }
        break;
    
      default:
    
        break;
      }
    }
      return data_rx_state;
    }
    


    Также, нам понадобится как-то рисовать полученные данные на экране. Замечу, что в 20-м байте данных, представляющих из себя 16-битный массив пикселов, хранится информация о типе кадра. Кадров бывает несколько типов. Нас интересует калибровочный кадр и рабочий кадр. Калибровочный кадр получается тогда, когда тепловизор закрывает шторку и делает снимок «темноты». При съёмке обычного кадра шторка открыта. Таким образом, при работе Вы всегда слышите как девайс щёлкает шторкой.

    Процедура отрисовки изображения на экране
    void BSP_LCD_DrawArray(uint32_t Xpos, uint32_t Ypos, uint32_t width, uint32_t height, uint8_t bit_pixel, uint8_t *pbmp)
    {
      uint32_t index = 0;
      uint32_t index2 = 0;
     // uint32_t address;
      //uint32_t input_color_mode = 0;
      //uint32_t Color;
      static int pixel;
      static int calib_pixel=0;
      uint8_t Component;
      static int v;
      uint8_t frame_type;
    
    frame_type = *(__IO uint8_t *) (pbmp + 20);
    
    	switch (frame_type) {
    case 6:
    	calib_pixel = (*(uint16_t*)pbmp);
    	minpixel = calib_pixel;
    	//calib_pixel = bswap_16(calib_pixel);
    	break;
    
    case 3:
    	  /* Convert picture to ARGB8888 pixel format */
    	  for(index=0; index < height; index++)
    	    {
    
    		  for(index2=0; index2 < width; index2++)
    		  {
    
    			  pixel = (*(uint16_t*)pbmp);
    
    			  //pixel = bswap_16(pixel);
    			  //v = pixel - calib_pixel;
    			  //v += 0x8000;
    
    			  if (maxpixel < pixel)
    				  maxpixel = pixel;
    
    			  if (minpixel > pixel)
    				  minpixel = pixel;
    
    			  if (pixel < 0) {
    				  pixel = 0;
    			  	}
    
    			  if (pixel > 0xFFFF) {
    			  	pixel = 0xFFFF;
    			  	}
    
    			  v = map(pixel, 6000, 13000, 0, 255);
    
    			 //v = (v - MAX) * 255 / (MIN - MAX);
    
    			 if (v < 0)
    				 v = 0;
    			 if (v > 255)
    				 v = 255;
    
    			  BSP_LCD_DrawPixel(index2+270, index+100, (0xFF << 24) | (uint8_t)v << 16 | (uint8_t)v << 8 | (uint8_t)v);
    			  pbmp += 2;
    		  }
    	    }
    
    
    	break;
    
    case 4:
    
    	break;
    
    						}
    
    }
    


    Наконец, главный цикл, из которого видно — где чего обрезали, где чего вставили.

    Главный цикл
    #define DELAY1 10
    #define USB_PIPE_NUMBER 0x81
    #define FRAME_WIDTH 208
    #define FRAME_HEIGHT 156
    
    uint8_t OutPipe, InPipe;
    uint8_t usb_device_state;
    uint8_t rawdata[FRAME_HEIGHT*FRAME_WIDTH*2];
    uint8_t data[64];
    USBH_StatusTypeDef status;
    uint8_t transf_size;
    int size;
    
    int main(void)
    {
      /* Enable the CPU Cache */
      CPU_CACHE_Enable();
    
      /* STM32F7xx HAL library initialization:
           - Configure the Flash ART accelerator on ITCM interface
           - Configure the Systick to generate an interrupt each 1 msec
           - Set NVIC Group Priority to 4
           - Low Level Initialization
         */
      HAL_Init();
    
      /* Configure the System clock to have a frequency of 200 MHz */
      SystemClock_Config();
    
      /* Init CDC Application */
      CDC_InitApplication();
    
      /* Init Host Library */
      USBH_Init(&hUSBHost, USBH_UserProcess, 0);
    
      /* Add Supported Class */
      //USBH_RegisterClass(&hUSBHost, USBH_CDC_CLASS);
    
      /* Start Host Process */
      USBH_Start(&hUSBHost);
    
      /* Run Application (Blocking mode) */
      while (1)
      {
        /* USB Host Background task */
        USBH_Process(&hUSBHost);
    
        if (hUSBHost.gState == HOST_CHECK_CLASS) {
    
        	switch (usb_device_state) {
        	case 1:
        		status = USBH_Get_StringDesc(&hUSBHost,hUSBHost.device.DevDesc.iManufacturer, data , 64);
        		if (status == USBH_OK) {
        			USBH_UsrLog("## Manufacturer : %s",  (char *)data);
        			HAL_Delay(1000);
        			usb_device_state = 1;
        		}
        		break;
        	case 2:
        		status = USBH_Get_StringDesc(&hUSBHost, hUSBHost.device.DevDesc.iProduct, data , 64);
    
        		if (status == USBH_OK) {
        		    			USBH_UsrLog("## Product : %s",  (char *)data);
        		    			HAL_Delay(1000);
        		    			usb_device_state = 2;
        		    		}
        		break;
        	case 0:
    
        		InPipe = USBH_AllocPipe(&hUSBHost, 0x81);
    
    
        		    	              status = USBH_OpenPipe(&hUSBHost,
        		    	                                          InPipe,
        		    	                                          0x81,
        		    	                                          hUSBHost.device.address,
        		    	                                          hUSBHost.device.speed,
        		    	                                          USB_EP_TYPE_BULK,
        		    	                                          USBH_MAX_DATA_BUFFER);
    
        		    	              if (status == USBH_OK)
        		    	            	  usb_device_state = 3;
    
        		break;
        	case 3:
        		HAL_Delay(1);
    
        		const uint8_t data0[2] = {0x00, 0x00};
        		vendor_transfer(0, SET_OPERATION_MODE, 0, 0, data0, 2);
        		vendor_transfer(0, SET_OPERATION_MODE, 0, 0, data0, 2);
        		vendor_transfer(0, SET_OPERATION_MODE, 0, 0, data0, 2);
    
        		data[0] = 0x01;
        		vendor_transfer(0, TARGET_PLATFORM, 0, 0, data, 1);
    
        		data[0] = 0x00;
        		data[1] = 0x00;
        		vendor_transfer(0, SET_OPERATION_MODE, 0, 0, data);
    
        		transf_size = vendor_transfer(1, GET_FIRMWARE_INFO, 0, 0, data, 4);
    
        		transf_size = vendor_transfer(1, READ_CHIP_ID, 0, 0, data, 12);
    
        		const uint8_t data1[6] = { 0x20, 0x00, 0x30, 0x00, 0x00, 0x00 };
        		vendor_transfer(0, SET_FACTORY_SETTINGS_FEATURES, 0, 0, data1, 6);
    
        		transf_size = vendor_transfer(1, GET_FACTORY_SETTINGS, 0, 0, data, 64);
    
        		const uint8_t data2[6] = { 0x20, 0x00, 0x50, 0x00, 0x00, 0x00 };
        		vendor_transfer(0, SET_FACTORY_SETTINGS_FEATURES, 0, 0, data2, 6);
    
        		transf_size = vendor_transfer(1, GET_FACTORY_SETTINGS, 0, 0, data, 64);
    
        		const uint8_t data3[6] = { 0x0c, 0x00, 0x70, 0x00, 0x00, 0x00 };
        		vendor_transfer(0, SET_FACTORY_SETTINGS_FEATURES, 0, 0, data3, 6);
    
        		transf_size = vendor_transfer(1, GET_FACTORY_SETTINGS, 0, 0, data, 24);
    
        		const uint8_t data4[6] = { 0x06, 0x00, 0x08, 0x00, 0x00, 0x00 };
        		vendor_transfer(0, SET_FACTORY_SETTINGS_FEATURES, 0, 0, data4, 6);
    
        		vendor_transfer(1, GET_FACTORY_SETTINGS, 0, 0, data, 12);
    
        		const uint8_t data5[2] = { 0x08, 0x00 };
        		vendor_transfer(0, SET_IMAGE_PROCESSING_MODE, 0, 0, data5, 2);
    
        		vendor_transfer(1, GET_OPERATION_MODE, 0, 0, data,2);
    
        		const uint8_t data6[2] = { 0x08, 0x00 };
        		vendor_transfer(0, SET_IMAGE_PROCESSING_MODE, 0, 0, data6, 2);
    
        		const uint8_t data7[2] = { 0x01, 0x00 };
        		vendor_transfer(0, SET_OPERATION_MODE, 0, 0, data7, 2);
    
        		vendor_transfer(1, GET_OPERATION_MODE, 0, 0, data, 2);
    
        		USBH_UsrLog("SeeK Thermal Init Done.\n");
    
        		size = FRAME_WIDTH * FRAME_HEIGHT;
    
        		int bufsize = size * sizeof(uint16_t);
        		status = CDC_IDLE;
        		usb_device_state = 4;
        		break;
        	case 4:
        		//while(1 ){
        		 // request a frame
        		data[0] = (uint8_t)(size & 0xff);
        		data[1] = (uint8_t)((size>>8)&0xff);
        		data[2] = 0;
        		data[3] = 0;
    
        		if (status == CDC_IDLE)
        		vendor_transfer(0, 0x53, 0, 0, data, 4);
        		
                    status = CAM_ProcessReception(&hUSBHost);
    
        		if (status == CDC_IDLE)
                    BSP_LCD_DrawArray(10, 10, FRAME_WIDTH, FRAME_HEIGHT, 16, rawdata);
    
        		usb_device_state = 4;
    
        		break;
    
        	}
        	          }
      }
    }
    


    Заключение


    Работа тепловизора с микроконтроллером выглядит намного шустрее, чем со смартфоном. Рекомендую данный даджет для оценки тепловой картины электронных устройств. Тепловизор имеет настраиваемое фокусное расстояние, что позволяет рассматривать даже отдельные электронные компоненты на плате! В заключение видеоролик, из которого можно оценить скорость работы тепловизора (где-то 8-9 fps)



    Информация для потенциальных покупателей
    Даджет 81,78
    Компания
    Поделиться публикацией
    Комментарии 8
    • +2
      Было бы интересно приделать моторизированную платформу и сдвигом камеры делать снимки высокого разрешения статических объектов.

      PS 26 тысяч за эту камеру… Я понимаю, что они дорого стоят, но 26 тысяч!
      • +1
        Первый seek стоил 200$ ровно. Я очень доволен :)
      • +1

        тормоза за такие деньги :D

        • 0
          Поправил
          • 0
            Flir One пробовали?
            • 0
              Не пробовал. У FlirOne маленькое разрешение микроболометра (160х120). Лучше бы Flir убрали оптическую камеру и поставили более продвинутый сенсор, благо, компания специализируется на тепловизорах.
            • +1
              Астрологи объявили неделю тепловизионного контроля. Количество статей по данной тематике увеличилось вдвое :)
              • +1
                Стоит признать что эта статья намного лучше очередного тепловизионного контроля кота и крана в ванной.

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

              Самое читаемое
              Интересные публикации