Паяльник глазами тепловизора SeekThermal
Введение
Думаю, что все сталкивались с такими гаджетами как тепловизор, ну хотя бы читали о них. И среди этих устройств есть целый подкласс гаджетов, которые не являются самостоятельным устройством, а служат чем-то вроде приставки с компьютеру или смартфону.
Сегодня речь пойдёт о подключении тепловизора Seek Thermal к микроконтроллеру STM32. А предоставила мне данное устройство компания Даджет. На просторах Geektimes данный тепловизор рассматривался не раз: освещалась, в основном, его работа с Андройд, а также проскакивала статья о подключении данного устройства к ПК. В своём обзоре я хочу рассказать о собственном опыте подключения тепловизора Seek Thermal к микроконтроллеру STM32 через USB хост.
Аппаратные требования
Не такие уж и специфические! Всё что должен иметь Ваш STM32 — это USB интерфейс, способный работать в режиме Host и какой-нибудь интерфейс для управления ЖК экраном. Самый очевидный выбор — это взять STM32F4 — Discovery. У меня под рукой оказалась плата STM32F746G-Discovery. Соответственно описание будет для этой платы, но! Т.к. код сгенерирован в среде CubeMX, возможно применить и другую EVM. Считаю прменённую мной плату избыточной для данного проекта.
Программная часть
Данный тепловизор не реализует какой-либо класс при общении по USB. Всё взаимодействие реализовано напрямую bulk-запросами через эндпойнты. Отправляя данные на определённый эндпойнт, можно включить тепловизор, откалибровать его, и заставить передать кадр, или несколько кадров. Особенно подробно работа с 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 с последующим кодом:
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.
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: %dn", 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: %dn", res); } printf(" -> ["); for (int i = 0; i < res; i++) { printf(" %02x", data[i]); } printf(" ]n"); #endif } return res; }
Далее, напишем процедуру приёма картинки. Тут особо комментировать нечего, подсмотрели в CDC Example.
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)
Источник