Подключить тепловизор к микроконтроллеру? Без проблем! Особенно если это STM32 с интерфейсом USB Host и тепловизор Seek Thermal от Даджет!
Паяльник глазами тепловизора 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)
Автор: Даджет