Подключить тепловизор к микроконтроллеру? Без проблем! Особенно если это 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)



Информация для потенциальных покупателей
Поделиться с друзьями
-->

Комментарии (8)


  1. Seven-ov
    07.03.2017 10:56
    +2

    Было бы интересно приделать моторизированную платформу и сдвигом камеры делать снимки высокого разрешения статических объектов.

    PS 26 тысяч за эту камеру… Я понимаю, что они дорого стоят, но 26 тысяч!


    1. igor_kuznetsov
      07.03.2017 12:29
      +1

      Первый seek стоил 200$ ровно. Я очень доволен :)


  1. madf
    07.03.2017 13:23
    +1

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


  1. vandiemen
    07.03.2017 14:46

    Поправил


  1. pzhivulin
    07.03.2017 14:47

    Flir One пробовали?


    1. vandiemen
      07.03.2017 14:50

      Не пробовал. У FlirOne маленькое разрешение микроболометра (160х120). Лучше бы Flir убрали оптическую камеру и поставили более продвинутый сенсор, благо, компания специализируется на тепловизорах.


  1. AleksBal59
    07.03.2017 14:50
    +1

    Астрологи объявили неделю тепловизионного контроля. Количество статей по данной тематике увеличилось вдвое :)


    1. u010602
      07.03.2017 17:06
      +1

      Стоит признать что эта статья намного лучше очередного тепловизионного контроля кота и крана в ванной.