image



Не так давно, была опубликована статья «Пастильда — открытый аппаратный менеджер паролей». Так как данный проект является открытым, то мы решили, что будет интересно, если мы будем писать небольшие заметки о процессе проектирования, о задачах, которые перед нами стоят и о трудностях, с которыми мы сталкиваемся.

Основная суть Пастильды заключается в том, что она является своеобразным переходником между клавиатурой и ПК. Таким образом, она должна уметь:
  • быть USB хостом для клавиатуры, которая к ней подключается,
  • быть клавиатурой для ПК, чтобы либо перенаправлять сообщения от реальной клавиатуры, либо самой быть клавиатурой,
  • быть дисковым накопителем, чтобы можно было редактировать базу данных паролей в удобном для человека виде.

Данный функционал является скелетом нашего проекта, поэтому первая заметка будет посвящена именно ему.


Реализация USB хоста

Итак, во-первых мне нужно было реализовать на устройстве USB хост, чтобы оно могло распознавать и общаться с подключенной к нему клавиатурой. Так как в работе я использую связку Eclipse + GNU ARM Eclipse + libopencm3, то очень хотелось найти уже что-то готовое и желательно написанное с использованием библиотеки libopencm3. Желание мое было очень жирным, до последнего момента не верила, что мои поиски увенчаются успехом. Однако под конец рабочего дня, проскролив интернет до самого дна, я вдруг наткнулась вот на это. libusbhost? Серьезно? И это был не просто написанный на основе libopencm3 usb хост, он еще и был написан под STM32F4, под тот самый, который мы решили использовать в проекте. В общем, звезды сошлись и радости моей не было предела. Кстати, оказалось, что этот проект создавался как часть libopencm3, однако его так и не добавили в библиотеку.

Как библиотеку, libusbhost я не собирала, просто взяла необходимые мне исходники, написала драйвер для клавиатуры и, в общем-то все, погнали! Но обо всем по-порядку.

Из libusbhost я взяла следующие файлы:
  • usbh_device_driver.h
  • usbh_config.h
  • usbh_hubbed.[ch]
  • usbh_lld_stm32f4.[ch]

Там был еще файл usart_helpers.[ch], с его помощью можно было по UART передавать в терминал все сообщения, приходящие от устройства в хост и еще много различной отладочной информации. Я с этим функционалом поигралась, но из проекта его убрала.

По аналогии с usbh_driver_hid_mouse.[ch], я написала драйвер для клавиатуры (usbh_driver_hid_kbd.[ch]).

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

USB Host Class
constexpr uint8_t  USB_HOST_TIMER_NUMBER     = 6;
constexpr uint16_t USB_HOST_TIMER_PRESCALER  = (8400 - 1);
constexpr uint16_t USB_HOST_TIMER_PERIOD     = (65535);

typedef void (*redirect)(uint8_t *data, uint8_t len);
typedef void (*control_interception)();

static redirect redirect_callback;
static control_interception control_interception_callback;

class USB_host
{
public:
	USB_host(redirect redirect_callback, control_interception control_interception_callback);
	void poll();

	static void kbd_in_message_handler(uint8_t data_len, const uint8_t *data);

	static constexpr hid_kbd_config_t kbd_config = { &kbd_in_message_handler };
	static constexpr usbh_dev_driver_t *device_drivers[] =
	{
		(usbh_dev_driver_t *)&usbh_hid_kbd_driver
	};

private:
	TIMER_ext *_timer;
	void timer_setup();
	uint32_t get_time_us();
	void oth_hs_setup();
};


Здесь все прозрачно. Устройство должно слушать клавиатуру и ждать набора специальной комбинации клавиш, для перехода в режим выбора логина и пароля. Это происходит в обработчике прерывания от клавиатуры kbd_in_message_handler(uint8_t data_len, const uint8_t *data). Тут есть два варианта развития событий:
  • Если комбинации нет, то нам нужно пропустить сообщение от клавиатуры дальше в ПК. Для обработки данного события, в конструктор передана функция _redirect_callback.
  • Если комбинация нажата, то нам нужно оповестить систему о том, что мы перешли в режим выбора логина и пароля, следовательно, мы больше не транслируем сообщения от клавиатуры в ПК. Теперь само устройство является клавиатурой, а сообщения от настоящей клавиатуры теперь интерпретируются как команды устройству. Для обработки такого события, в конструктор передана функция _control_interception_callback.


Реализация составного USB устройства

Далее мне нужно было сделать так, чтобы наше устройство отображалось в диспетчере устройств и как клавиатура, и как дисковый накопитель. Тут вся магия в дескрипторах=) В этом документе, в главе 9, подробно описан USB Device Framework. Эту главу нужно очень внимательно прочитать и в соответствии с ней описать дескрипторы устройства. В моем случае получилось следующее:

Composite USB Descriptors

	static constexpr uint8_t keyboard_report_descriptor[]  =
	{
			0x05, 0x01, 0x09, 0x06, 0xA1, 0x01, 0x05, 0x07, 0x19, 0xE0, 0x29, 0xE7, 0x15, 0x00, 0x25, 0x01,
			0x75, 0x01, 0x95, 0x08, 0x81, 0x02, 0x95, 0x01, 0x75, 0x08, 0x81, 0x01, 0x95, 0x03, 0x75, 0x01,
			0x05, 0x08, 0x19, 0x01, 0x29, 0x03, 0x91, 0x02, 0x95, 0x05, 0x75, 0x01, 0x91, 0x01, 0x95, 0x06,
			0x75, 0x08, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x05, 0x07, 0x19, 0x00, 0x2A, 0xFF, 0x00, 0x81, 0x00,
			0xC0
	};

	static constexpr char  usb_strings[][30] =
	{
			"Third Pin",
			"Composite Device",
			"Pastilda"
	};

	static constexpr struct usb_device_descriptor dev =
	{
			USB_DT_DEVICE_SIZE, //bLength
			USB_DT_DEVICE,      //bDescriptorType
			0x0110,             //bcdUSB
			0x0,                //bDeviceClass
			0x00,               //bDeviceSubClass
			0x00,               //bDeviceProtocol
			64,                 //bMaxPacketSize0
			0x0483,             //idVendor
			0x5741,             //idProduct
			0x0200,             //bcdDevice
			1,                  //iManufacturer
			2,                  //iProduct
			3,                  //iSerialNumber
			1                   //bNumConfigurations
	};

	typedef struct __attribute__((packed))
	{
		struct usb_hid_descriptor hid_descriptor;
		struct
		{
			uint8_t bReportDescriptorType;
			uint16_t wDescriptorLength;
		} __attribute__((packed)) hid_report;
	} type_hid_function;

	static constexpr type_hid_function  keyboard_hid_function =
	{
		{
			9,          //bLength
			USB_DT_HID, //bDescriptorType
			0x0111,     //bcdHID
			0,          //bCountryCode
			1           //bNumDescriptors
		},

		{
			USB_DT_REPORT,
			sizeof(keyboard_report_descriptor)
		}
	};

	static constexpr struct usb_endpoint_descriptor hid_endpoint =
	{
		USB_DT_ENDPOINT_SIZE,        //bLength
		USB_DT_ENDPOINT,             //bDescriptorType
		Endpoint::E_KEYBOARD,        //bEndpointAddress
		USB_ENDPOINT_ATTR_INTERRUPT, //bmAttributes
		64,                          //wMaxPacketSize
		0x20                         //bInterval
	};

	static constexpr struct usb_endpoint_descriptor msc_endpoint[] =
	{
		{
			USB_DT_ENDPOINT_SIZE,        //bLength
			USB_DT_ENDPOINT,             //bDescriptorType
			Endpoint::E_MASS_STORAGE_IN, //bEndpointAddress
			USB_ENDPOINT_ATTR_BULK,      //bmAttributes
			64,                          //wMaxPacketSize
			0                            //bInterval
		},

		{
			USB_DT_ENDPOINT_SIZE,         //bLength
			USB_DT_ENDPOINT,              //bDescriptorType
			Endpoint::E_MASS_STORAGE_OUT, //bEndpointAddress
			USB_ENDPOINT_ATTR_BULK,       //bmAttributes
			64,                           //wMaxPacketSize
			0                             //bInterval
		}
	};

	static constexpr struct usb_interface_descriptor iface[] =
	{
		{
			USB_DT_INTERFACE_SIZE,   //bLength
			USB_DT_INTERFACE,        //bDescriptorType
			Interface::I_KEYBOARD,   //bInterfaceNumber
			0,                       //bAlternateSetting
			1,                       //bNumEndpoints
			USB_CLASS_HID,           //bInterfaceClass
			1,                       //bInterfaceSubClass
			1,                       //bInterfaceProtocol
			0,                       //iInterface
			&hid_endpoint, &keyboard_hid_function,
			sizeof(keyboard_hid_function)
		},

		{
			USB_DT_INTERFACE_SIZE,     //bLength
			USB_DT_INTERFACE,          //bDescriptorType
			Interface::I_MASS_STORAGE, //bInterfaceNumber
			0,                         //bAlternateSetting
			2,                         //bNumEndpoints
			USB_CLASS_MSC,             //bInterfaceClass
			USB_MSC_SUBCLASS_SCSI,     //bInterfaceSubClass
			USB_MSC_PROTOCOL_BBB,      //bInterfaceProtocol
			0x00,                      //iInterface
			msc_endpoint, 0, 0
		},
	};

	static constexpr struct usb_config_descriptor::usb_interface ifaces[]
	{
		{
			(uint8_t *)0,                   //cur_altsetting
			1,                              //num_altsetting
			(usb_iface_assoc_descriptor*)0, //iface_assoc
			&iface[Interface::I_KEYBOARD]   //altsetting
		},

		{
			(uint8_t *)0,                     //cur_altsetting
			1,                                //num_altsetting
			(usb_iface_assoc_descriptor*)0,   //iface_assoc
			&iface[Interface::I_MASS_STORAGE] //altsetting
		},
	};

	static constexpr struct usb_config_descriptor config_descr =
	{
		USB_DT_CONFIGURATION_SIZE, //bLength
		USB_DT_CONFIGURATION,      //bDescriptorType
		0,                         //wTotalLength
		2,                         //bNumInterfaces
		1,                         //bConfigurationValue
		0,                         //iConfiguration
		0x80,                      //bmAttributes
		0x50,                      //bMaxPower
		ifaces
	};



keyboard_report_descriptor был взят из документа Device Class Definition for Human Interface Devices (HID) , Appendix E.6 Report Descriptor (Keyboard). Честно, сильно не разбиралась со структурой отчета, поверила документу) В целом, вот пара моментов, на которые нужно обратить особое внимание:
  • usb_config_descriptor: поле bNumInterfaces должно отражать столько интерфейсов, сколько реально реализовано. В нашем случае два: HID и MSD
  • usb_interface_descriptor: поле bInterfaceNumber обозначает номер интерфейса, но отсчет начинается с нуля, следовательно, номер первого интерфейса — 0.

Вот, с описательной точки зрения, наверно, и все. Не могу не отметить, как грамотно в библиотеке описаны дескрипторы (их описание находится в файле usbstd.h). Все четко по документации. Полагаю, это значительно упростило мне задачу, так как не возникало вопросов «Как же мне описать составное устройство?». Все сразу было понятно.

Для работы с составным устройством был написан класс USB_composite, представленный ниже.

Composite USB Class
extern "C" void USB_OTG_IRQ();

int USB_control_callback(usbd_device *usbd_dev, struct usb_setup_data *req,
		                 uint8_t **buf, uint16_t *len, usbd_control_complete_callback *complete);

void USB_set_config_callback(usbd_device *usbd_dev, uint16_t wValue);

static uint8_t keyboard_protocol = 1;
static uint8_t keyboard_idle = 0;
static uint8_t keyboard_leds = 0;

class USB_composite
{
public:
	uint8_t usbd_control_buffer[500];
	UsbCompositeDescriptors *descriptors;
	uint8_t usb_ready = 0;
	usbd_device *my_usb_device;

	USB_composite(const uint32_t block_count,
	 	 	 	  int (*read_block)(uint32_t lba, uint8_t *copy_to),
			          int (*write_block)(uint32_t lba, const uint8_t *copy_from));

	void usb_send_packet(const void *buf, int len);

	int hid_control_request(usbd_device *usbd_dev, struct usb_setup_data *req, uint8_t **buf, uint16_t *len,
			                void (**complete)(usbd_device *usbd_dev, struct usb_setup_data *req));

	void hid_set_config(usbd_device *usbd_dev, uint16_t wValue);
};


Ключевыми в этом классе являются две функции:
  • Функция hid_control_request нужна для общения Пастильды как клавиатуры с хостом (в данном случае, хост — это ПК). Вне класса данная функция вызывается через USB_control_callback.
  • Функция hid_set_config нужна для того, чтобы настроить конечные точки (endpoints) и зарегистрировать USB_control_callback, описанный в предыдущем пункте. Вне класса данная функция вызывается через USB_set_config_callback.

Ниже представлен вариант их реализации:

Callbacks
int USB_composite::hid_control_request(usbd_device *usbd_dev, struct usb_setup_data *req, uint8_t **buf, uint16_t *len,
					            void (**complete)(usbd_device *usbd_dev, struct usb_setup_data *req))
{
	(void)complete;
	(void)usbd_dev;

	if ((req->bmRequestType & USB_REQ_TYPE_DIRECTION) == USB_REQ_TYPE_IN)
	{
		if ((req->bmRequestType & USB_REQ_TYPE_TYPE) == USB_REQ_TYPE_STANDARD)
		{
			if (req->bRequest == USB_REQ_GET_DESCRIPTOR)
			{
				if (req->wValue == 0x2200)
				{
					*buf = (uint8_t *)descriptors->keyboard_report_descriptor;
					*len = sizeof(descriptors->keyboard_report_descriptor);
					return (USBD_REQ_HANDLED);
				}
				else if (req->wValue == 0x2100)
				{
					*buf = (uint8_t *)&descriptors->keyboard_hid_function;
					*len = sizeof(descriptors->keyboard_hid_function);
					return (USBD_REQ_HANDLED);
				}
				return (USBD_REQ_NOTSUPP);
			}
		}
		else if ((req->bmRequestType & USB_REQ_TYPE_TYPE) == USB_REQ_TYPE_CLASS)
		{
			if (req->bRequest == HidRequest::GET_REPORT)
			{
				*buf = (uint8_t*)&boot_key_report;
				*len = sizeof(boot_key_report);
				return (USBD_REQ_HANDLED);
			}
			else if (req->bRequest == HidRequest::GET_IDLE)
			{
				*buf = &keyboard_idle;
				*len = sizeof(keyboard_idle);
				return (USBD_REQ_HANDLED);
			}
			else if (req->bRequest == HidRequest::GET_PROTOCOL)
			{
				*buf = &keyboard_protocol;
				*len = sizeof(keyboard_protocol);
				return (USBD_REQ_HANDLED);
			}
			return (USBD_REQ_NOTSUPP);
		}
	}

	else
	{
		if ((req->bmRequestType & USB_REQ_TYPE_TYPE) == USB_REQ_TYPE_CLASS)
		{
			if (req->bRequest == HidRequest::SET_REPORT)
			{
				if (*len == 1)
				{
					keyboard_leds = (*buf)[0];
				}
				return (USBD_REQ_HANDLED);
			}
			else if (req->bRequest == HidRequest::SET_IDLE)
			{
				keyboard_idle = req->wValue >> 8;
				return (USBD_REQ_HANDLED);
			}
			else if (req->bRequest == HidRequest::SET_PROTOCOL)
			{
				keyboard_protocol = req->wValue;
				return (USBD_REQ_HANDLED);
			}
		}
		return (USBD_REQ_NOTSUPP);
	}

	return (USBD_REQ_NEXT_CALLBACK);
}

int USB_control_callback(usbd_device *usbd_dev,
		struct usb_setup_data *req, uint8_t **buf, uint16_t *len,
		usbd_control_complete_callback *complete)
{
	return(usb_pointer->hid_control_request(usbd_dev, req, buf, len, complete));
}

void USB_composite::hid_set_config(usbd_device *usbd_dev, uint16_t wValue)
{
	(void)wValue;
	(void)usbd_dev;

	usbd_ep_setup(usbd_dev, Endpoint::E_KEYBOARD, USB_ENDPOINT_ATTR_INTERRUPT, 8, 0);
	usbd_register_control_callback(usbd_dev, USB_REQ_TYPE_INTERFACE, USB_REQ_TYPE_RECIPIENT, USB_control_callback );
}

void USB_set_config_callback(usbd_device *usbd_dev, uint16_t wValue)
{
	usb_pointer->hid_set_config(usbd_dev, wValue) ;
}


Как правило, функции control_request и set_config должны быть явно описаны для каждого устройства. Однако из этого правила есть исключение: Mass Storage Device. Итак, разберемся с конструктором класса USB_Composite.

Во-первых, мы инициализируем ноги USB OTG FS:

	GPIO_ext uf_p(PA11);
	GPIO_ext uf_m(PA12);

	uf_p.mode_setup(Mode::ALTERNATE_FUNCTION, PullMode::NO_PULL);
	uf_m.mode_setup(Mode::ALTERNATE_FUNCTION, PullMode::NO_PULL);

	uf_p.set_af(AF_Number::AF10);
	uf_m.set_af(AF_Number::AF10);

Во-вторых, нам нужно проинициализировать наше составное устройство, зарегистрировать USB_set_config_callback, о котором шла речь выше, и разрешить прерывание:
	my_usb_device = usbd_init(&otgfs_usb_driver, &(UsbCompositeDescriptors::dev),
			&(UsbCompositeDescriptors::config_descr), (const char**)UsbCompositeDescriptors::usb_strings, 3,
			  usbd_control_buffer, sizeof(usbd_control_buffer));

	usbd_register_set_config_callback(my_usb_device, USB_set_config_callback);
	nvic_enable_irq(NVIC_OTG_FS_IRQ);

Этого достаточно для того, чтобы в диспетчере устройств наше устройство распознавалось:
  • Во вкладке «Контроллеры USB»: как составное устройство,
  • В этой же вкладке, как «Запоминающее устройство для USB»,
  • Во вкладке «Клавиатуры», как «Клавиатура HID».

Однако «Запоминающее устройство для USB» будет помечено предупреждением о том, что устройство работает неправильно. Все дело в том, что в отличие от других USB устройств, Mass Storage инициализируется немного иначе, через функцию usb_msc_init, описанную в файле usb_msc.c библиотеки libopencm3. Выше я уже упоминала о том, что для MSD нет необходимости явно описывать функции control_request и set_config. Это потому, что функция usb_msc_init все сделает за нас: и конечные точки настроит, и все колбэки зарегистрирует. Таким образом, нам нужно дополнить конструктор еще одной строчкой:
	usb_msc_init(my_usb_device, Endpoint::E_MASS_STORAGE_IN, 64, Endpoint::E_MASS_STORAGE_OUT, 64,
			"ThirdPin", "Pastilda", "0.00", block_count, read_block, write_block);

Тут можно заметить, что при инициализации MSD, нам нужно передать ему минимальное API для работы с памятью:
  • block_count: количество секторов памяти,
  • read_block: функция для чтения сектора,
  • write_block: функция для записи сектора.

В Пастильде мы используем внешний флеш SST25VF064C. Драйвер для этой микросхемы можно посмотреть здесь. В дальнейшем, на основе этого драйвера, во флеше будет реализована файловая система. Скорее всего, об этом как-нибудь подробно напишет мой коллега. Но так как я хотела поскорее протестировать работу MSD, я написала зародыш файловой системы=) Над ним можно поплакать здесь.

Так вот. Теперь, когда конструктор класса USB_Composite дописан, можно собрать проект, прошить устройство и увидеть, что «Запоминающее устройство для USB» больше не помечено предупреждением, а во вкладке «Дисковые устройства» можно обнаружить «ThirdPin Pastilda USB Device». И, казалось бы, все хорошо. Но нет=) Проблем стало больше:

1. Зайти на диск невозможно. При попытке сделать это все виснет, умирает, компьютеру очень плохо.
2. Распознавание устройства как дискового занимает более 2-х минут.

Об этих проблемах и о том, как их решить без вреда для здоровья написано здесь: USB mass storage device и libopencm3.

И, о, чудо! Никаких пятен=) Теперь все работает. У нас есть USB хост и составное USB устройство. Осталось только объединить их работу.

Объединение хоста и составного устройства

Наша цель:
  • Транслировать сообщения от клавиатуры в ПК до тех пор, пока не нажата комбинация Ctrl + Shift + ~.
  • После нажатия комбинации Ctrl + Shift + ~, Пастильда должна перехватить управление и отправить сообщение в ПК как клавиатура, после чего мы возвращаемся в режим трансляции и снова ожидаем комбинацию.


Код, реализующий все это, простой как палка:

App.cpp
App *app_pointer;

App::App()
{
	app_pointer = this;

	clock_setup();
	systick_init();

	_leds_api = new LEDS_api();
	_flash = new FlashMemory();
	usb_host = new USB_host(redirect, control_interception);
	usb_composite = new USB_composite(_flash->flash_blocks(), _flash->flash_read, _flash->flash_write);
}
void App::process()
{
	_leds_api->toggle();
	usb_host->poll();
}

void App::redirect(uint8_t *data, uint8_t len)
{
	app_pointer->usb_composite->usb_send_packet(data, len);
}

void App::control_interception()
{
	memset(app_pointer->key, 0, 8);
	app_pointer->key[2] = KEY_W;
	app_pointer->key[3] = KEY_O;
	app_pointer->key[4] = KEY_N;
	app_pointer->key[5] = KEY_D;
	app_pointer->key[6] = KEY_E;
	app_pointer->key[7] = KEY_R;
	app_pointer->usb_composite->usb_send_packet(app_pointer->key, 8);

	app_pointer->key[2] = 0;
	app_pointer->key[3] = 0;
	app_pointer->key[4] = 0;
	app_pointer->key[5] = 0;
	app_pointer->key[6] = 0;
	app_pointer->key[7] = 0;
	app_pointer->usb_composite->usb_send_packet(app_pointer->key, 8);

	app_pointer->key[2] = KEY_SPACEBAR;
	app_pointer->key[3] = KEY_W;
	app_pointer->key[4] = KEY_O;
	app_pointer->key[5] = KEY_M;
	app_pointer->key[6] = KEY_A;
	app_pointer->key[7] = KEY_N;
	app_pointer->usb_composite->usb_send_packet(app_pointer->key, 8);

	app_pointer->key[2] = 0;
	app_pointer->key[3] = 0;
	app_pointer->key[4] = 0;
	app_pointer->key[5] = 0;
	app_pointer->key[6] = 0;
	app_pointer->key[7] = 0;
	app_pointer->usb_composite->usb_send_packet(app_pointer->key, 8);
}


В конструкторе мы инициализируем все, что необходимо:
  1. Светодиоды, чтобы моргали;
  2. Флеш, чтобы можно было файлы на диске создавать / удалять;
  3. Хост, передав ему при этом функцию redirect (что делать, если комбинации нет) и control_interception (что делать, если комбинация нажата);
  4. Составное устройство, передав ему функции чтения / записи памяти;

И вот, собственно, и все. Начало положено, скелет нашего устройства создан. Совсем скоро будет доработана файловая система, по нажатию комбинации Ctrl + Shift + ~, мы будем попадать в однострочное меню, а во флеше будет храниться наша зашифрованная база данных паролей.

Буду рада любым комментариям и пожеланиям.

И, конечно же, ссылка на github.
Поделиться с друзьями
-->

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


  1. gbg
    20.07.2016 17:48
    +4

    Ехали антипаттерны через антипаттерны:
    memset(app_pointer->key, 0, 8);
    app_pointer->key[2] = KEY_W;
    app_pointer->key[3] = KEY_O;
    app_pointer->key[4] = KEY_N;
    app_pointer->key[5] = KEY_D;
    app_pointer->key[6] = KEY_E;
    app_pointer->key[7] = KEY_R;
    app_pointer->usb_composite->usb_send_packet(app_pointer->key, 8);

    app_pointer->key[2] = 0;
    app_pointer->key[3] = 0;
    app_pointer->key[4] = 0;
    app_pointer->key[5] = 0;
    app_pointer->key[6] = 0;
    app_pointer->key[7] = 0;

    1. Магическая константа 8. Стоит поменять размер массива — имеем жуткий головняк с отловом константы.
    2. Как следствие первого — первый и второй аргументы memset между собой не связаны. Компилятор никак не сможет предупредить нас, если что-то пошло не так. Напоминаю, что у вас C++, а значит есть шаблоны. Можно сделать все проверки очень красиво и в compile time.
    3. Обнуление — классный кусок «китайского кода». Вам работу построчно оплачивают? Это раздутый кирпич кода, весь семантический смысл которого — memset. Если вы думаете, что цикл будет работать дольше — заставьте компилятор его раскрутить.


    1. ana_lazareva
      20.07.2016 18:31

      На этом месте в дальнейшем будет меню, здесь нужно было просто вывести что-нибудь, что бы понять, работает USB HID или нет.


      1. gbg
        20.07.2016 21:17
        +1

        Для начала, предлагаю вам воспользоваться простейшим шаблонным трюком-оберткой для memset:

        template<typename T> void ZeroIt(T& value)
        {
        	memset(&value,0,sizeof(value));
        }
        

        Тест на IDEONE
        Мой pull-request


        1. ana_lazareva
          21.07.2016 12:36

          Спасибо, я услышала вас. Давно собиралась почитать книжку Real-Time C++ Efficient Object-Oriented and Template Microcontroller Programming, — Kormanyos C.M, а вы напомнили мне о ней)


        1. BigW
          25.07.2016 10:37

          Мммм… пример я посмотрел, но не совсем понимаю в ZeroIt передается массив целиком или же указатель на него? Если последнее — то при размере массива более 4 (на 32 битной архитектуре) вы получите ой… А если массив целиком — вы можете получить срыв стека… может все же определить размер массива константой и передавать её?


          1. gbg
            25.07.2016 11:54

            Массив передается по ссылке. Раз по ссылке, то sizeof(value) = sizeof(массива). В моем примере, это будет 12 байт.
            В большинстве случаев, компилятор и вовсе выбросит отдельный вызов функции и впишет на это место memset с правильными параметрами.


    1. Jef239
      22.07.2016 13:19

      Про константу 8 — вы правы. Про обнуление — не совсем. Для ВАС — memset читабельней, для Анны — читабельней присвоение нулями. Embeded проекты — трудно отлаживать, их проще сразу писать в максимально простом и надежном варианте.

      И ещё. Вы не сумели разобраться, что делает этот код. А это — ОДНОРАЗОВЫЙ тест, он посылает сообщение «WONDER WOMEN». Живет такой тест — полчаса. И чем тупее в нем код — тем лучше. Тест должен тестировать остальные части программы, а не быть источником ошибок.


      1. gbg
        22.07.2016 13:37

        Данный тест наверняка сделан при помощи copy-paste, а не набран полностью вручную. Статистика говорит о том, что copy-paste — это очень продуктивный источник ошибок. Даже Кармак в Quake III учудил.

        Этот код не помещается на один экран стандартного монитора. Следовательно, нарушает эргономику — для его обозрения нужно дергать свою кратковременную память и скроллить туда-сюда.

        Этот код у неподготовленного читателя вызывает необходимость сверки индексов — а вдруг, там не все и не подряд? И там ведь действительно — не все, а с двойки.

        И про «тестовость» и «временность». Такие чудесные времянки потом, в условиях цейтнота, перерастают в Главное Техническое Решение Продукта и живут потом годами.


        1. Jef239
          22.07.2016 18:40
          +2

          Нет, это именно ТЕСТ. Проверка, что мы можем что-то передать, притворившись клавиатурой. И он реально одноразовый.Он не предназначен для ВАШЕГО монитора. А, например, на мой — он влезает целиком.

          Да, согласен, иногда времянки живут долго. Но это именно ИНОГДА. А в данном случае — ЗАВЕДОМО одноразовый тест.

          У вас мышление преподавателя. а не ИНЖЕНЕРА. У инженера к разным частям кода — разные требования. К одним — скорость, к другим — читаемость, к третьим — быстрота написания, к четвертым — отсутствие ошибок. А преподаватель считает, что есть две точки зрения — одна ЕГО, а другая неправильная.

          Я уже видел, как отличный ученик таких вот преподавателей стал УЖАСНЫМ инженером. его код был красив, параметризован, обвешан кучей темплейтов и классов. Он даже работал — но только с данными от сферической лошади в вакууме. Чуть что реальное — и все, случайно залетевший дятел рушил всю цивилизацию.

          В инженерных понятиях — код должен эффективно выполнять СВОЮ задачу. Причем для каждой задачи — понятие эффективности разное. Но практически никогда таким критерием не является КРАСОТА кода. Да, да для некоторого кода ПОЛЕЗНА сопровождаемость. Но не тогда, когда она идет в ущерб главной задаче.

          А когда второстепенные критерии становятся на первое место — это не инженер. Это вечный студентик, работающий ради отметки.


  1. kekekeks
    20.07.2016 18:24
    +3

    Что поражает в «экосистеме» разработки под микроконтроллеры — жуткий зоопарк не совместимых друг с другом и с немного отличающися железом, нет, не библиотек, а огрызков исходного кода. Каких-либо вменяемых абстракций просто нет, народ из своей бизнес-логики пишет напрямую в регистры и занимается прочим непотребством. В ChibiOS хотя бы более-менее работающий HAL сделали и понятия блочных устройств и потоков ввели, но это не сильно спасает, когда в руки попадает какой-нибудь экранчик, пример к которому написан ещё до появления STMCube и использует старый и вручную поправленый cmsis, а иной документации просто нет.


    1. segment
      20.07.2016 18:31
      +2

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


      1. sam_satan
        20.07.2016 19:33
        +4

        Можно обернуть всю параметризацию, которая известна compile time (а такой 99%) в шаблоны, тем самым без потери производительности мы получим гибкую абстракцию


        1. segment
          20.07.2016 19:38
          -1

          Если код пишется для любительских поделок, то такой вариант подойдет. Как говориться на вкус и цвет. Но, в более ответственных применениях, такой «сахар» приведет к увеличению сложности верификации проекта и стоимости его поддержки.


          1. sam_satan
            20.07.2016 21:11
            +2

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


            1. BigW
              21.07.2016 10:46

              Мне кажется — сильно зависит от назначения, если нужен жесткий реалтайм — привет asm регистры и пр… Если не очень жесткий — разного рода rtos, если время реакции некритично (в пределах 1/10 — 1/20 сек) и вычислительных и иных мощностей чипа хватает — можно городить все что угодно и во главу поставить, например, как вы верно указали удобство поддержки и независимость от платформы…


              1. sam_satan
                21.07.2016 11:25
                +1

                Всегда на утверждения о корреляции асма и производительности скидываю эту статью.
                http://easyelectronics.ru/rabota-s-portami-vvoda-vyvoda-mikrokontrollerov-na-si.html

                Если вы используете раннее связывание, то оверхеда перед прибиванием гвоздями нет совершенно никакого. При этом код типа

                typedef Led<PortA,4> led1;
                typedef Led<PortA,5> led2;
                led1::init();
                led2::init();
                led1::toggle();
                led2::toggle();
                

                приятнее читать и удобнее сопровождать

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


            1. Jef239
              22.07.2016 18:20
              -1

              Хотелось бы узнать побольше о вашем опыте. О том, как вы переносите ваши шаблоны на GCC 2.95 (привет МСВС), как он работают на компиляторах, отличных от GCC (привет разным российскими процессорам), как вы переносите ваш код на системы, где С++ просто нету, а есть только Си?

              Или вы ради вашей красоты просто отказываетесь от проектов, где заказчиком жестко задан процессор?

              Что касается сложности верификации — ну вы же профи, вы алгоритмы работы своих устройств выучили на зубок. А для обычного человека намного проще, когда код работы с устройством идет в тех же понятиях, что и в описании устройства. А любая абстракция — мешает верифицировать код по документации, Потому что сегодня одно устройство, завтра другое, послезавтра третье. И на каждое — 50-100 страниц доки.

              я не профи во встраиваемых системах. Полдюжины embeded проектов, где работает мой код — это мало. Потому часть, связанную с железом мне проще писать ровно по доке. Без выкрутасов крутых ембедчиков, которые эти доки помнят наизусть.

              Зато мой код — РАБОТАЕТ. А коллеге, любившему темплейты — оказалось проще уволиться, чем отладить свой код. У него вся энергия ушла в многоуровневые навороты. В итоге навороты красивые, но функции своей не выполняют. Хотя если бы обошелся без красивостей и код был бы меньше раза в 3-4 и работал бы намного правильней. УВЫ, темплейты и классы — отличный способ спустить пар в свисток.


              1. sam_satan
                22.07.2016 19:43

                Вы не поверите, но GCC 2.95 поддерживает шаблоны.

                На компиляторах от Keil и Iar все работает прекрасно.

                Миландр (привет разным российскими процессорам) тоже поддерживает C++ и шаблоны. Любые армы поддерживают C++ и шаблоны, микроконтроллеры авр поддерживают C++ и шаблоны. Пики и 8051 только остались за бортом, но их никто не использует.

                Так что похоже вы сами себе придумали проблему и сами поставили ее мне в укор.
                Раз пошло время придуманых ситуаций, я тоже задам вам вопрос.
                Что вы будете делать если микроконтроллер не поддерживает ни C++ ни С а только ADA? Или вы ради вашей красоты просто отказываетесь от проектов, где заказчиком жестко задан процессор?

                Вы опять же не поверите, но параметризацию устройства можно тоже вынести в отдельный класс и получить великолепную абстракцию, причем человеку который пишет не надо знать что и как там инкапсулированно, настроил phy микросхему на 10 Мбит, ок, на 100 надо? Не будем перелапачивать весь код, а просто добавим реализацию конфигурирования на 100мБит.

                Зато мой код — РАБОТАЕТ. А коллеге, любившему темплейты — оказалось проще уволиться, чем отладить свой код.
                Вот это характеристика только ваша и коллеги. МОЙ КОД ТОЖЕ РАБОТАЕТ!!!!!!!!!!!..

                Это сродни истории
                «К нам в бригаду строителей пришел странный какой то, все молотками забивают гвозди, а он пневматический принес, поковырялся там, гвоздей не забил ни одного на компрессор пожаловался и уволился.»

                Это же не пневматического молотка проблема, это человека. А вы экстраполировали своего коллегу на все сообщество C++ программистов.


                1. Jef239
                  23.07.2016 02:22

                  ЖАЛЬ, что вы проблему не понимаете. Ещё больше ЖАЛЬ, что вы свое непонимание проблемы передаете студентам.

                  Первичное — код должен РАБОТАТЬ. ПРАВИЛЬНО работать. Выполнять нужные функции. Вторичное — это НАДЕЖНОСТЬ кода. Работоспособность кода при ошибках в данных, при изменении условий, На третьем месте — СКОРОСТЬ и затраты памяти. И только на четвертом — СОПРОВОЖДАЕМОСТЬ. Потому что в большинстве случаев сопровождение надежно работающего кода — не нужно.

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

                  А вот на ПЕРВОМ месте сопровождаемость лишь тогда, когда код НЕ РАБОТАЕТ Не выполняет своих функций. Вот тогда 99% сил будут потрачены на сопровождение и поиск ошибок.

                  Ну и сдача экзаменов — на экзаменах, конечно СТИЛЬ написания намного важнее, работает программа или нет.

                  В итоге после ВУЗа мы получаем специалистов с отличным стилем и неработоспобными программами. И это ОБЩАЯ ситуация.

                  Поймите, вы ставите телегу впереди лошади. То есть стиль впереди работоспособности. Мне ЖАЛЬ, что вы не понимаете, насколько это вредно.

                  Из более 30 лет своей программистом карьеры я лет 20 занимался именно сопровождением чужого кода.И код, в котором на первом месте сопровождаемость — исправить невозможно. Его можно только написать заново. А если код рабоспособен и надежен — проблем с ним немного. Если он плохо написан — его несложно отрефакторить.

                  Теперь ответы на ваши вопросы.
                  1) Код без темплейтов и С++сных наворотов я перепишу на что угодно. Не только на АДА, но даже на РАЯ (он же Ершол). Даром что ли я на полусотне языков читаю. А вот ваши красивости на АДА просто так не перенесешь.
                  2) имелся ввиду 1879ВЯ1Я от модуля. Для DSP там российский компилятор.
                  3) GCC 2.95 — да, шаблоны поддерживает. Но далеко не все, что умеет GCC 5.0
                  4) Кого выносить в отдельный модуль — решение архитектурное, а не стилевое. Давайте не путать предмет спора. Хорошая архитектура — действительно полезна для проекта. Но если вы не понимаете отличие архитектуры от стиля — значит вы не готовы к серьезному обсуждению.

                  ИТОГ. О ваших проектах вы не рассказали ничего. Есть чего рассказывать или все под NDA как незабвенная Bolgeos?


                  1. sam_satan
                    23.07.2016 08:40
                    +1

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

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


                    1. Jef239
                      23.07.2016 15:09

                      Вселенское зло — это ошибка ПРИОРИТЕТОВ. Перфекционизм во второстепенном.Замена цели средством.

                      Я правильно понимаю, что вы разрабатываете измеритель для одного спутника, потом немного дописываете — и получаете измеритель для второго, потом — для третьего? То есть в ВАШЕЙ задаче сопровождаемость действительно может стоять даже на первом месте. То есть вам может быть важнее выдать 10 похожих приборов, чем работоспособность отдельного прибора. И уж точнее важнее время разработки всей серии, чем время разработки отдельного прибора.

                      Осталось понять, что это ВАШ частный случай. Который не имеет отношения к той же пастильде.

                      Шаблоны и разные С++ные хитрости — это СРЕДСТВА. На своем месте — они хороши. А когда их написание идет в ущерб работоспособности кода — они плохи.

                      Прошу прощения, спойлеры мне недоступны.

                      http://bash.im/quote/420672
                      Вася и Петя одновременно начали писать один и тот же продукт.
                      Вася был «ориентирован на результат» и начал сразу писать говнокод не продумав толком архитектуру.
                      А Петя месяц разрабатывал архитектуру, месяц делал удобный интуитивный интерфейс, которому позавидывал бы Джони Айв, потом месяц писал тесты, потом два месяца писал сам код и получил идеальное стабильное приложение.
                      Но Вася выпустил уже через месяц первую версию программы, пусть и не идеальную, пусть с багами, но рабочую, и начал её продавать. Ещё через месяц выпустил вторую версию исправляющие баги первой и добавляющие новые баги. Ещё через месяц на доходы от продаж нанял двух толковых программеров, которые за два месяца перелопатили весь код, согласно пожеланиям пользователей допилили интерфейс и выпустили третью версию программы.
                      Итого, через пять месяцев у Васи было два работника, куча клиентов и сносно работающее приложение отвечающее желаниям клиентов.
                      У Пети было вылизанное никому не известное приложение, минус на банковском счёте и ни одного клиента.
                      В завершение этого выдуманного примера можно сказать, что через полгода Вася купил все наработки Пети, Петю взял в штат тестировщиком, а сам по пьяни разбился на своём новеньком Туареге


                    1. Jef239
                      23.07.2016 21:33

                      Да, чтобы вы понимали. Мои требования к компиляции на любом компилятор, начиная с borland C++ 3.1 для MS-DOS 1990ого года выпуска — они точно так же специфичны, как и ваши.

                      Судя по минусам — тут много народу, кто путает средства с целями. Ну что же, мне жаль. :-(


      1. maxzhurkin
        20.07.2016 21:02

        Абстракция, в общем-то, может и не выходить за пределы текста кода, хороший (или ужасный) пример такого кода — Метод Даффа


    1. ana_lazareva
      20.07.2016 18:49

      Да, согласна с вами. О применении ChibiOS в проектах думаю уже довольно давно. Слышала, что HAL там действительно самый хороший из всего того, что на данный момент представлено. Но на внедрение использования OS (на корпоративном уровне) нужно время, которого к сожалению всегда не хватает. Однако, полагаю, что скоро жизнь заставит и переход на ChibiOS все-таки будет осуществлен.


    1. safari2012
      21.07.2016 16:42

      а что ещё остается делать при размере ОЗУ 2кбайта.


      1. kekekeks
        21.07.2016 21:31

        На описываемом в статье USB-брелке используется чип STM32F405 ($4.39 за штуку на digikey), у которого на борту полмегабайта флэша и 192 килобайт ОЗУ.


  1. spmbt
    20.07.2016 18:34
    +4

    В этом плане с вашим проектом хорошо пересекается ещё одна интересная и потенциально нужная как открытый проект задача — индивидуальная раскладка клавиатуры, носимая с собой, не зависящая от ОС. От такой «раскладки» требуется не только перекодировка комбинаций с обычной клавиатуры через USB-переходник, но и кодирование произвольных длинных слов (тех же паролей, но и не только) из других слов или сочетаний клавиш. И там тоже стоит вопрос об аппаратной реализации хаба USB. Так что можно брать вашу реализацию за основу и тащить туда.

    (Куда? Не задавался давно этим вопросом; может, кто-то подскажет? Нашёл только похожую функцию в одном гаджете для планшетов — клавиатура через USB)


    1. mkarev
      20.07.2016 20:37

      Интересно. Эдакий аппаратный Karabiner для мака. И наоборот.
      После долгой работы на маке, переходя на PC, начинаешь на автомате использовать command+c вместо ctr+c и т.п. А переопределить такие штуки глобальным хуком (на Win32) не всегда можно. Аппаратный транслятор в этом смысле имел бы преимущество.


  1. x893
    20.07.2016 19:32
    +1

    Было бы интересно посмотреть на отчет из PVS-Studio по этому проекту.
    Уверен много интересного напишет.


    1. x893
      20.07.2016 23:10

      А если бы еще был проект под Keil — радости не было бы границ!


    1. ana_lazareva
      21.07.2016 12:26

      Разве PVS-Studio не заточен под VS? Может быть вы можете порекомендовать еще какие-нибудь анализаторы?


      1. gbg
        21.07.2016 12:54

        У PVS есть бинарник, который можно вызывать для каждого файла проекта по аналогии с компилятором. FAQ. То есть, к IDE он жестко не привязан.

        Рекомендовать могу анализатор из состава Clang. Он интересен тем, что бесплатен и открыт. Опенсорс к опенсорсу.


        1. ana_lazareva
          22.07.2016 16:12

          Спасибо.


  1. zhigalin
    20.07.2016 21:02
    -7

    Девушка — программист микроконтроллеров!


    1. x893
      20.07.2016 21:07
      +2

      А такого удивительного? Что девушки — не программисты?


      1. Wesha
        20.07.2016 22:03
        -12

        В этом месте не будет комментария про морскую свинку.


        1. unalacuna
          21.07.2016 10:02

          А у тебя не будет минусов, потому что никто не догадается. Удивительно, правда?


          1. Wesha
            21.07.2016 18:00
            -4

            Извините покорно, но Вы не подскажете, когда конкретно я с Вами на брудершафт пил?


            1. unalacuna
              21.07.2016 22:36
              +1

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


              1. Wesha
                21.07.2016 23:16
                -1

                А что невежливого в сообщении, что чего-то где-то не будет?

                (А что Вы, прочитав это сообщение, подумали — это уже претензия не ко мне: «каждый воспринимает увиденное в меру своей распущенности» ©.)


  1. Konachan700
    21.07.2016 09:30
    +1

    Не сказано о главном — как (будет) реализована поддержка клав, у которых не один HID, а несколько. Их ведь большинство сейчас, и сегодняшняя версия устройства работать с многими клавами будет глючно.


    1. ana_lazareva
      21.07.2016 12:14
      +2

      Реализация 0.1 является тестовой, типо proof of concept и все такое. В рамках данной реализации, мы пока не используем media клавиатуры, клавиатуры с USB хабами и т. д. Мы обязательно займемся этим на следующем этапе развития проекта, но на данной стадии это избыточно. Тут перед нами стоит другая задача: проверить жизнеспособность идеи. Например, на внешней памяти предполагается организация файловой системы и нужно оценить скорость записи файлов на диск через USB, скорость работы самого контроллера с этой памятью как с файловой системой. Еще нам нужно портировать keepass и тут тоже много вопросов: сколько времени будет занимать шифрование / дешифрование базы данных, а подумали ли мы о том, где хранить расшифрованную базу данных и др. Таким образом, пока что нам нужно сделать прототип устройства, чтобы понять:

      • подходящий ли контроллер мы выбрали?
      • а память?
      • нам нужно где-то хранить расшифрованную БД, так может рядом с флешом поставить еще и энергозависимую память?
      • а что со скоростью работы устройства? не пора ли нам использовать RTOS?

      и многое другое.


      1. kekekeks
        21.07.2016 21:45

        У вас на MCU есть ноги для SDIO. Подключите к ним microSD флешку и не мучайтесь c внешним контроллером памяти. Для ChibiOS есть готовая реализация USB Mass Storage поверх SD-карты, кстати.
        К бд имеет смысл стучаться, используя шифрование/дешифрование «на лету».


        1. ana_lazareva
          22.07.2016 09:56

          Так да) Мы уже задались вопросом почему мы SD-то не поставили, это действительно было бы проще. С контроллером тоже косяк вышел, мы поставили STM32F40x, а нужно было STM32F41x, ибо в там есть крипто процессор. «На лету» вы имеете ввиду расшифровывать только необходимые в данный момент записи?


          1. kekekeks
            22.07.2016 12:44

            Вам весь файл в памяти не нужен, его вполне можно читать в поточном режиме при поиске нужного пароля.


      1. KonstantinSpb
        22.07.2016 02:07

        Да, РТОС вскоре может пригодится, чтобы велосипед не изобретать, вроде неплохая РТОС — RIOT
        http://www.riot-os.org/#features


  1. hdg_79
    21.07.2016 10:46

    Хе… а в устройстве не предусмотрен недокументированный режим аппаратного кейлогера? Реализация прям напрашивается…


    1. KonstantinSpb
      21.07.2016 11:23

      Я думаю, если будет возможность перепрошивать контроллер собранной у себя из исходников прошивкой, то кейлоггер маловероятен.


  1. zolkko
    21.07.2016 11:48

    Интересно, можно ли в принципе на stm32f4 одновременно завести hid и uvc? Использую stm32 usb device library. М.б. есть какие-либо примеры?


    1. ana_lazareva
      22.07.2016 10:16
      +2

      Вы имеете ввиду USB video device? Почему нет? Для того, чтобы устройство распознавалось как video device, необходимо лишь правильно описать дескрипторы, тут найти примеры не составит труда. Для работы устройства нужно понимать протокол взаимодействия хоста с данным видом устройств. Существует документ Universal Serial Bus Device Class Definition for Video Devices там все подробно описано (глава 3 поможет с дескрипторами, а глава 4 с протоколом взаимодействия). Изучите его, поймите, что требуется, и это, я полагаю, значительно облегчит ваши поиски! Удачи!


  1. KonstantinSpb
    22.07.2016 13:14

    Будет ли реализована возможность заливать собранную самому прошивку и загрузчик?