Здравствуйте, дорогие читатели!

Пришла мне тут одна идейка, а не собрать ли пульт управления космическим кораблем. На USB. С нативной поддержкой драйверов. Custom HID. Чтобы воткнул и всё работает, без всяких танцев и бубнов. В итоге, получился некий монструозный «геймпад» для космических симуляторов. В общем, судите сами.

Поначалу, я мало представлял, что будет в итоге. Хотелось два основных джойстика, как на Союзе-МС, немного переключателей, кнопок и несколько дисплеев.

Прикинув рабочую поверхность моего стола, выбрал размеры пульта по ширине и глубине 500*300 мм. А пошарив по строительным складам и магазинам в поисках стройматериалов, выбрал высоту 125мм. В итоге приобрел лист 4 мм фанеры, рейки 20*12 мм и доску 120*20 мм.

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



Но пока оставим малярные работы в стороне и я расскажу про электронную начинку.

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


Остальная мелочь проблем не вызвала. Контроллер выбрал STM32. В качестве АЦП для джойстиков 16-битные ADS1118. Также был куплен блок питания на 12 В. Собственно такое напряжение объясняется тем, что мне в руки попал указатель уровня топлива от «шахи», который я тоже хотел сюда же пристроить.


На фото блок питания, стабилизаторы на 5 и 3.3 В, STM32, MCP23017, ADS1118

Контроллер 100-выводный STM32F407VET6, к нему подключается:

2 селектора на 4 положения
1 переменный резистор
2 переключателя осей
4 основных оси
2 вспомогательных оси
2 регулирующих оси
4 клавишных переключателя по 2 кнопки каждый
20 кнопок со светодиодами
4 основных выключателя со светодиодами
2 кнопки-грибка со светодиодами
2 кнопки таймеров
3 выключателей со светодиодами
13 переключателей
2 ADS1118 (АЦП)
4 MAX7219 (8-знаковые LED-дисплеи)
2 TM1637 (дисплей-часы)
1 PCF8574 (расширитель I/O, воткнут в знакосинтезирующий дисплей)


Получилась такая структурка

Чего-то многовато будет для сотни ног МК, решил я, и добавил сюда же расширители входов-выходов: четыре штуки MCP23017, на 16 входов или выходов каждый. Забегая вперед, скажу, что задержка опроса входов у расширителя получилась около 0.13 мс на одну микросхему, при скорости шины I2C 400кГц. То есть это с запасом перекрывает минимальное время опроса USB в 1 мс.

Чтобы не гонять шину I2C бесполезными запросами, у MCP23017 есть выхода-прерывания, которые устанавливаются при изменении состояния входов. Их я тоже применил в своем проекте. Как оказалось далее, из-за дребезга контактов эти прерывания оказались бесполезными.

АЦП ADS1118 несколько не успевает за скоростью USB, заявленная производительность у него составляет максимально 820 отсчетов в секунду, что равно 1.2 мс, при этом, он имеет несколько входов, которые внутри через мультиплексор уже подключены к АЦП. Я использовал 2 входа на одну микросхему, поэтому время обновления значений составляет 2.4 мс. Плоховато, но что поделаешь? К сожалению на али других 16-битных быстрых АЦП нет.


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

Программа CPU написана в стиле программы ПЛК. Никаких блокирующих запросов. Ядро периферию не ждет, не успела и хрен с ним, на следующем цикле опросит. Никаких RTOS в проекте тоже нет, попробовал, уперся в минимальное время ожидания задачи 1 мс — получается медленно, если нам надо отправлять данные по USB с частотой 1 мс. В итоге понял, что буду использовать ОС без osDelay(), а тогда зачем RTOS? Просто, как в ПЛК, располагать инструкции программы один за другим внутри бесконечного цикла вполне достаточно.

Использовал, конечно же CubeMX и библиотеки HAL. Кстати, на HAL недавно перешел и удивился удобности. Не знаю, почему до сих пор не очень популярен, там главное разобраться поначалу, а потом пойдет очень просто. Такое чувство, что программируешь ардуину.

Девайс у нас будет USB custom HID. HID есть mouse, keyboard, gamepad, joystick, какие-то еще. А есть custom. Всё это не требует драйверов от операционной системы. Точнее, они уже написаны разработчиком. Кастомный девайс хорош тем, что мы сами комбинируем возможности всех вышеназванных устройств по своему усмотрению.

Вообще, USB штука очень сложная, имеет мануал почти в тысячу страниц и с наскока её не взять. Кто не хочет читать тяжелые мануалы, есть великолепная статья USB in a NutShell, погуглите. Также у неё есть перевод. Всё же попытаюсь некоторые моменты объяснить «на пальцах».

USB — пакетированная передача данных с кучей уровней и абстракций. Девайс у нас — никаких данных запрашивать не может, всю передачу инициализирует хост. Хост пишет и запрашивает данные в так называемые конечные точки, физически это некоторые буферы в памяти МК. Чтобы хост понимал по каким конечным точкам можно писать, а какие конечные точки читать и какие данные он может интерпретировать как кнопки и оси нашего устройства и, вообще, что это тут у нас за устройство, в начале коннекта он запрашивает дескрипторы девайса. Этих дескриптеров много и составлять их сложно и можно как угодно, и ошибиться тоже, где угодно. Физически они представляют собой массив байт.

На самом деле, CubeMX сгенерирует код инициализации Custom HID лучше нас.





Прошу обратить внимание на последней картинке под цифрой 3. Это размер дескриптора в байтах, который и определяет какие оси и кнопки есть на нашем девайсе. Генерируется этот дескриптор в программе HID Descriptor Tool. Там есть несколько примеров для самостоятельного изучения. Вообще, вот мой дескриптор. Там пока отсутствуют данные для дисплеев, для простоты понимания, но присутствуют все кнопки и оси джойстиков. Его нужно поместит в файл usbd_custom_hid_if.c. По-умолчанию, куб этот дескриптор делает пустым.

HID Descriptor (размер 104 байта)
__ALIGN_BEGIN static uint8_t CUSTOM_HID_ReportDesc_FS[USBD_CUSTOM_HID_REPORT_DESC_SIZE] __ALIGN_END =
{
  /* USER CODE BEGIN 0 */
	0x05, 0x01,                    // USAGE_PAGE (Generic Desktop)
	0x15, 0x00,                    // LOGICAL_MINIMUM (0)
	0x09, 0x04,                    // USAGE (Joystick)
	0xa1, 0x01,                    // COLLECTION (Application)
	0x05, 0x02,                    //   USAGE_PAGE (Simulation Controls)
	0x09, 0xbb,                    //   USAGE (Throttle)
	0x15, 0x00,                    //   LOGICAL_MINIMUM (0)
	0x27, 0xff, 0xff, 0x00, 0x00,  //   LOGICAL_MAXIMUM (65535)
	0x75, 0x10,                    //   REPORT_SIZE (16)
	0x95, 0x01,                    //   REPORT_COUNT (1)
	0x81, 0x02,                    //   INPUT (Data,Var,Abs)
	0x05, 0x01,                    //   USAGE_PAGE (Generic Desktop)
	0x09, 0x01,                    //   USAGE (Pointer)
	0xa1, 0x00,                    //   COLLECTION (Physical)
	0x09, 0x30,                    //     USAGE (X)
	0x09, 0x31,                    //     USAGE (Y)
	0x95, 0x02,                    //     REPORT_COUNT (2)
	0x81, 0x02,                    //     INPUT (Data,Var,Abs)
	0xc0,                          //   END_COLLECTION
	0x05, 0x01,                    //   USAGE_PAGE (Generic Desktop)
	0x09, 0x32,                    //   USAGE (Z)
	0x95, 0x01,                    //   REPORT_COUNT (1)
	0x81, 0x02,                    //   INPUT (Data,Var,Abs)
	0x09, 0x33,                    //   USAGE (Rx)
	0x81, 0x02,                    //   INPUT (Data,Var,Abs)
	0x09, 0x34,                    //   USAGE (Ry)
	0x81, 0x02,                    //   INPUT (Data,Var,Abs)
	0x09, 0x35,                    //   USAGE (Rz)
	0x81, 0x02,                    //   INPUT (Data,Var,Abs)
	0x09, 0x36,                    //   USAGE (Slider)
	0x81, 0x02,                    //   INPUT (Data,Var,Abs)
	0x09, 0x39,                    //   USAGE (Hat switch)
	0x15, 0x01,                    //   LOGICAL_MINIMUM (1)
	0x25, 0x08,                    //   LOGICAL_MAXIMUM (8)
	0x35, 0x00,                    //   PHYSICAL_MINIMUM (0)
	0x46, 0x0e, 0x01,              //   PHYSICAL_MAXIMUM (270)
	0x65, 0x14,                    //   UNIT (Eng Rot:Angular Pos)
	0x75, 0x08,                    //   REPORT_SIZE (8)
	0x95, 0x01,                    //   REPORT_COUNT (1)
	0x81, 0x02,                    //   INPUT (Data,Var,Abs)
	0x05, 0x09,                    //   USAGE_PAGE (Button)
	0x19, 0x01,                    //   USAGE_MINIMUM (Button 1)
	0x29, 0x40,                    //   USAGE_MAXIMUM (Button 64)
	0x15, 0x00,                    //   LOGICAL_MINIMUM (0)
	0x25, 0x01,                    //   LOGICAL_MAXIMUM (1)
	0x75, 0x01,                    //   REPORT_SIZE (1)
	0x95, 0x40,                    //   REPORT_COUNT (64)
	0x55, 0x00,                    //   UNIT_EXPONENT (0)
	0x65, 0x00,                    //   UNIT (None)
	0x81, 0x02,                    //   INPUT (Data,Var,Abs)
  /* USER CODE END 0 */
  0xC0    /*     END_COLLECTION	             */
};


По сути, его можно составлять как угодно, сначала задаются параметры USAGE PAGE и необходимый USAGE, например ось USAGE (Throttle), а затем после слова INPUT (Data,Var,Abs) система будет считать, что у нас есть ось «Газ». Размерность переменной оси и их кол-во задается параметрами LOGICAL_MAXIMUM, MINIMUM, REPORT_SIZE, REPORT_COUNT, которые должны стоять перед INPUT.

Более подробно про эти параметры, а также, что такое (Data,Var,Abs), можно прочесть в Device Class Definition for Human Interface Devices (HID) v1.11.

Ниже приведен пример инициализации оси Throttle из моего дескриптора. В данном примере Throttle имеет диапазон значений 0-65535, что соответствует одной переменной uint16_t.

	0x05, 0x02,                    //   USAGE_PAGE (Simulation Controls)
	0x09, 0xbb,                    //   USAGE (Throttle)
	0x15, 0x00,                    //   LOGICAL_MINIMUM (0)
	0x27, 0xff, 0xff, 0x00, 0x00,  //   LOGICAL_MAXIMUM (65535)
	0x75, 0x10,                    //   REPORT_SIZE (16)
	0x95, 0x01,                    //   REPORT_COUNT (1)
	0x81, 0x02,                    //   INPUT (Data,Var,Abs)

А да, еще, допустим, можно не писать LOGICAL_MAXIMUM, MINIMUM, REPORT_SIZE, REPORT_COUNT каждый раз, хост будет определять это значение по предыдущему параметру. Это иллюстрируют оси, которые идут один за другим, без указания размера и кол-ва:

	0x09, 0x32,                    //   USAGE (Z)
	0x95, 0x01,                    //   REPORT_COUNT (1)
	0x81, 0x02,                    //   INPUT (Data,Var,Abs)
	0x09, 0x33,                    //   USAGE (Rx)
	0x81, 0x02,                    //   INPUT (Data,Var,Abs)
	0x09, 0x34,                    //   USAGE (Ry)
	0x81, 0x02,                    //   INPUT (Data,Var,Abs)
	0x09, 0x35,                    //   USAGE (Rz)
	0x81, 0x02,                    //   INPUT (Data,Var,Abs)
	0x09, 0x36,                    //   USAGE (Slider)

Всему этому дескриптору, который выше под спойлером, соответствует следующая структура. Она, по сути, уже не обязательна, просто так удобнее вести запись на основе указателей.

#pragma pack(push, 1)
typedef struct _myReportStruct
{
  uint16_t Throttle;
  uint16_t X;
  uint16_t Y;
  uint16_t Z;
  uint16_t Rx;
  uint16_t Ry;
  uint16_t Rz;
  uint16_t Slider;
  uint8_t Hat; // 0 - none, 1 - up, 2 - up-right, 3 - right, 4 - down-right...
  uint32_t Buttons1; // 32 buttons of 1 bit each
  uint32_t Buttons2; // 32 buttons of 1 bit each
}myReportStruct;
#pragma pack(pop)

volatile myReportStruct Desk;

Эту структуру можно посылать хосту функцией

USBD_CUSTOM_HID_SendReport(&hUsbDeviceFS, (uint8_t *) &Desk, sizeof(Desk));

Первый параметр — хендл USB, он у нас уже создан кубом. Возможно, понадобится подключить include-ом необходимый файл, где впервые инициализируется этот хендл и прописать extern USBD_HandleTypeDef hUsbDeviceFS;, чтобы с ним можно было работать. Второй параметр — указатель на нашу структуру и третий — размер структуры в байтах.

После заливки и прошивки контроллера можно заметить, что чего-то USB медленно шевелится. Данные с нашей панели обновляются не быстро. Чтобы было быстро, в файлах usbd_customhid.h нужно поменять #define CUSTOM_HID_EPIN_SIZE на максимальное значение 0x40, #define CUSTOM_HID_EPOUT_SIZE тоже поставить 0x40. В файле usbd_customhid.c найти комментарии в дескрипторе эндпойнтов "/* bInterval: Polling Interval (20 ms) */" и поменять байт дескриптора на 0x01 для каждого эндпойнта, всего два раза. Что будет соответствовать 1 мс обмена данными.


Должно получиться нечто подобное. Стандартное устройство без установки каких-либо драйверов

В общем-то, с функцией управления немного разобрались. Её сделать довольно легко и все кнопки и оси уже работают. Осталось сделать работу дисплеев. Делал я её делал, полгода примерно, и уже полгода панель пылится в долгом ящике. Нет времени. Поэтому решил выложить статью именно в таком виде, а то она рискует вообще не выйти.

С дисплеями всё тоже самое, что и с осями. Под них нужно дополнить наш дескриптор HID девайса, только указать что это дисплеи и вместо принятия данных Input, хост будет посылать данные Output.

Дескриптор HID девайса значительно разросся. Здесь я уже применил параметры Report ID, чтобы не забивать буфер приема/передачи и эндпойнты полными данными и различать, что за телеграмма нам пришла. Report ID представляет собой байт uint8_t со значением, который идет вначале телеграммы. Значением мы сами задаем в дескрипторе HID девайса.

CUSTOM_HID_ReportDesc_FS
//AXIS
	0x05, 0x01,                    // USAGE_PAGE (Generic Desktop)
	0x09, 0x04,                    // USAGE (Joystick)
	0xa1, 0x01,                    // COLLECTION (Application)28
	0x05, 0x02,                    //   USAGE_PAGE (Simulation Controls)
	0x09, 0xbb,                    //   USAGE (Throttle)
	0x15, 0x00,                    //   LOGICAL_MINIMUM (0)
	0x27, 0xff, 0xff, 0x00, 0x00,  //   LOGICAL_MAXIMUM (65535)
	0x75, 0x10,                    //   REPORT_SIZE (16)
	0x95, 0x01,                    //   REPORT_COUNT (1)
	0x85, 0x01,					   //   REPORT_ID (1)
	0x81, 0x02,                    //   INPUT (Data,Var,Abs)
	0x05, 0x01,                    //   USAGE_PAGE (Generic Desktop)
	0x09, 0x01,                    //   USAGE (Pointer)
	0xa1, 0x00,                    //   COLLECTION (Physical)
	0x09, 0x30,                    //     USAGE (X)
	0x09, 0x31,                    //     USAGE (Y)
	0x95, 0x02,                    //     REPORT_COUNT (2)
	0x81, 0x02,                    //     INPUT (Data,Var,Abs)
	0xc0,                          //   END_COLLECTION
	0x05, 0x01,                    //   USAGE_PAGE (Generic Desktop)
	0x09, 0x32,                    //   USAGE (Z)
	0x95, 0x01,                    //   REPORT_COUNT (1)
	0x81, 0x02,                    //   INPUT (Data,Var,Abs)
	0x09, 0x33,                    //   USAGE (Rx)
	0x81, 0x02,                    //   INPUT (Data,Var,Abs)
	0x09, 0x34,                    //   USAGE (Ry)
	0x81, 0x02,                    //   INPUT (Data,Var,Abs)
	0x09, 0x35,                    //   USAGE (Rz)
	0x81, 0x02,                    //   INPUT (Data,Var,Abs)
	0x09, 0x36,                    //   USAGE (Slider)
	0x81, 0x02,                    //   INPUT (Data,Var,Abs)

	//HAT
	0x09, 0x39,                    //   USAGE (Hat switch)
	0x15, 0x01,                    //   LOGICAL_MINIMUM (1)
	0x25, 0x08,                    //   LOGICAL_MAXIMUM (8)
	0x35, 0x00,                    //   PHYSICAL_MINIMUM (0)
	0x46, 0x0e, 0x01,              //   PHYSICAL_MAXIMUM (270)
	0x65, 0x14,                    //   UNIT (Eng Rot:Angular Pos)
	0x75, 0x08,                    //   REPORT_SIZE (8)
	0x95, 0x01,                    //   REPORT_COUNT (1)
	0x81, 0x02,                    //   INPUT (Data,Var,Abs)

	//Buttons
	0x05, 0x09,                    //   USAGE_PAGE (Button)
	0x19, 0x01,                    //   USAGE_MINIMUM (Button 1)
	0x29, 0x40,                    //   USAGE_MAXIMUM (Button 64)
	0x15, 0x00,                    //   LOGICAL_MINIMUM (0)
	0x25, 0x01,                    //   LOGICAL_MAXIMUM (1)
	0x75, 0x01,                    //   REPORT_SIZE (1)
	0x95, 0x40,                    //   REPORT_COUNT (64)
	0x55, 0x00,                    //   UNIT_EXPONENT (0)
	0x65, 0x00,                    //   UNIT (None)
	0x81, 0x02,                    //   INPUT (Data,Var,Abs)

	//LEDs
	0x85, 0x02,					   // REPORT_ID (2)
	0x05, 0x08,                    // USAGE_PAGE (LEDs)
	0x09, 0x4B,                    // USAGE (Generic Indicator)
	0x95, 0x40,                    // REPORT_COUNT (16)
	0x91, 0x02,                    // OUTPUT (Data,Var,Abs)
	0xc0,                          // END_COLLECTION

	//LCD Displays
        0x05, 0x14,                    // USAGE_PAGE (Alphnumeric Display)
	0x09, 0x01,                    // USAGE (Alphanumeric Display)
	0x15, 0x00,                    // LOGICAL_MINIMUM (0)
	0xa1, 0x02,                    // COLLECTION (Logical)

	0x09, 0x32,                    //   USAGE (Cursor Position Report)
	0xa1, 0x02,                    //   COLLECTION (Logical)
	0x85, 0x04,                    //     REPORT_ID (4)
	0x75, 0x08,                    //     REPORT_SIZE (8)
	0x95, 0x01,                    //     REPORT_COUNT (1)
	0x25, 0x13,                    //     LOGICAL_MAXIMUM (19)
	0x09, 0x34,                    //     USAGE (Column)
	0xb1, 0x22,                    //     FEATURE (Data,Var,Abs,NPrf)
	0x25, 0x03,                    //     LOGICAL_MAXIMUM (3)
	0x09, 0x33,                    //     USAGE (Row)
	0x91, 0x22,                    //     OUTPUT (Data,Var,Abs,NPrf)
	0xc0,                          //   END_COLLECTION

	0x09, 0x2b,                    //   USAGE (Character Report)
	0xa1, 0x02,                    //   COLLECTION (Logical)
	0x85, 0x05,                    //     REPORT_ID (5)
	0x95, 0x14,                    //     REPORT_COUNT (20)
	0x26, 0xFF, 0x00,              //     LOGICAL_MAXIMUM (255)
	0x09, 0x2c,                    //     USAGE (Display Data)
	0x92, 0x02, 0x01,              //     OUTPUT (Data,Var,Abs,Buf)
	0xc0,                          //   END_COLLECTION


    0x09, 0x24,                    // USAGE (Display Control Report)
    0x85, 0x06,                    // REPORT_ID (6)
    0x95, 0x01,                    // REPORT_COUNT (1)
    0x91, 0x22,                    // OUTPUT (Data,Var,Abs,NPrf)
	0xc0,                          // END_COLLECTION

	//LED Displays
        0x05, 0x14,                    // USAGE_PAGE (Alphnumeric Display)
	0x09, 0x01,                    // USAGE (Alphanumeric Display)
	0x15, 0x00,                    // LOGICAL_MINIMUM (0)
	0xa1, 0x02,                    // COLLECTION (Logical)

	0x09, 0x2b,                    //   USAGE (Character Report)
	0xa1, 0x02,                    //   COLLECTION (Logical)
	0x85, 0x07,                    //     REPORT_ID (7)
	0x75, 0x08,                    //     REPORT_SIZE (8)
	0x95, 0x28,                    //     REPORT_COUNT (40)
	0x26, 0xFF, 0x00,              //     LOGICAL_MAXIMUM (255)
	0x09, 0x2c,                    //     USAGE (Display Data)
	0x92, 0x02, 0x01,              //     OUTPUT (Data,Var,Abs,Buf)
	0xc0,                          //   END_COLLECTION

	//Other DATA
    0x06, 0x00, 0xff,              // USAGE_PAGE (Generic Desktop)
    0x09, 0x01,                    // USAGE (Vendor Usage 1)
    0xa1, 0x01,                    // COLLECTION (Application)
    0x85, 0x08,                    //   REPORT_ID (8)
    0x09, 0x01,                    //   USAGE (Vendor Usage 1)
    0x15, 0x00,                    //   LOGICAL_MINIMUM (0)
    0x27, 0xff, 0xff, 0x00, 0x00,  //   LOGICAL_MAXIMUM (65535)
    0x75, 0x10,                    //   REPORT_SIZE (16)
    0x95, 0x0A,                    //   REPORT_COUNT (10)
    0x91, 0x82,                    //   OUTPUT (Data,Var,Abs,Vol)

Обработка Output происходит в функции static int8_t CUSTOM_HID_OutEvent_FS(uint8_t event_idx, uint8_t state), которая, по-умолчанию, находится в usbd_custom_hid_if.c.

static int8_t CUSTOM_HID_OutEvent_FS()
static int8_t CUSTOM_HID_OutEvent_FS(uint8_t event_idx, uint8_t state)
{
  /* USER CODE BEGIN 6 */
  uint8_t dataReceiveArray[USBD_CUSTOMHID_OUTREPORT_BUF_SIZE];
  USBD_CUSTOM_HID_HandleTypeDef     *hhid = (USBD_CUSTOM_HID_HandleTypeDef*)hUsbDeviceFS.pClassData;

  for (uint8_t i = 0; i < USBD_CUSTOMHID_OUTREPORT_BUF_SIZE; i++)
  {
	dataReceiveArray[i] = hhid->Report_buf[i];
  }

  if (dataReceiveArray[0] == 2) //report ID 2 leds
  {
       // если Report id == 2, то делаем что-то на основе данных в dataReceiveArray[1 + N], например, зажигаем LED 
  }

  if (dataReceiveArray[0] == 4) //report ID 4 cursor position
  {
       // если Report id == 4, то делаем что-то, например устанавливаем курсор на LCD 
  }

  if (dataReceiveArray[0] == 5) //report ID 5 display data
  {
     // если Report id == 5, то делаем что-то, например выводим данные с USB на LCD 
  }

  // и так далее, смотря сколько ID у нас в дескрипторе

  return (USBD_OK);
  /* USER CODE END 6 */
}


Осталось только написать программу на ПК, которая отправляет нужные репорты, чтобы рулить дисплеями. Впрочем, для проверки кода МК подойдет великолепная программа от ST: USB HID Demonstrator. Она позволяет слать репорты с ПК с каким угодно содержимым.


Тест LED дисплеев

На этом этапе я пока закончил. И не известно, начну ли снова.

Играется в симуляторы интереснее, чем с клавиатурой. Но не настолько, чтобы прямо был вау-эффект. Клавиатура, она тоже похожа на пульт управления. Но управлять осями-джойстиками, как минимум, необычно. Чувствуешь себя космонавтом. Правда, для полного погружения необходим скафандр.

Надеюсь, вам было интересно. Опечатки, неточности и бред присутствует. Желающие поковыряться в коде могут посмотреть здесь.

С уважением.

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


  1. Anton23
    19.04.2019 12:15
    +5

    Спасибо, интересно. Но не хватает видео с игрой на этом ПУ.


    1. TyVik
      19.04.2019 13:16

      Очень крутая идея! Даёшь симуляцию стыковки или посадки на Луну.


      1. 8street Автор
        19.04.2019 14:08

        Постараюсь сделать на выходных.


        1. 8street Автор
          20.04.2019 09:24
          -1


          1. chapter_one
            20.04.2019 23:46

            Очень здорово! Монитор может поставить повыше?


  1. norguhtar
    19.04.2019 12:41

    А для какого сима это все запиливалось?


    1. 8street Автор
      19.04.2019 14:08
      +1

      Подойдет для любого сима, к которому можно подключить джойстик или геймпад. Я чаще всего играю в KSP.


      1. MuKPo6
        19.04.2019 19:35

        Попробуйте «Reentry — An Orbital Simulator»


        1. chapter_one
          19.04.2019 20:48
          +1

          Ну тогда сразу Orbiter. Куда уж хардкорнее?


  1. chapter_one
    19.04.2019 13:48
    +3

    Автор, остановись! Это начинается все невинно: там дощечка, тут фанерка, топливомер от «шахи», а через пару лет ты покупаешь гараж, потому что в квартиру это уже не помещается.

    www.creativesimulations.com/Cockpit%20Shell.htm


    1. LynXzp
      19.04.2019 17:07
      +1

      А потом участок


    1. 0xd34df00d
      19.04.2019 18:40

      Блин, круто! Надо такой кокпит для F/A-18 сделать.



      1. chapter_one
        19.04.2019 19:27
        +1

        Что-то вроде такого?
        image


        1. 0xd34df00d
          19.04.2019 19:31

          Офигенно. Это под какой сим? С DCS заведётся? (Edit: увидел домен. Заведётся, конечно)

          Я не представляю, например, как сделать отображение чего надо на MFCD и, тем более, например, на UFC или панели двигателя и топлива.


          1. chapter_one
            19.04.2019 19:41
            +1

            Я когда начал задумываться о постройке своего «гнезда симмера» и пошел гуглить информацию просто офигел, насколько обширный рынок этого дела существует в США (в первую очередь) и в Европе. Куча контор, которые выпускают панельки, приборы, экраны, органы управления, и прочее для нужд замороченных симмеров, даже готовые киты есть из которых можно собрать полную кабину 737, только каркас нужно самому сделать. Крайне недешевое удовольствие, но качество изготовления попадается просто потрясающее, а сборка и подключение не представляет никаких сложностей. Пока остановился на сайтековских панельках для Цессны, вот таких saitek.pro/flight-simulation/pro-flight-panels которые просто собраны под мониторами в самопальный корпус. Не то, конечно, но уже интереснее.


  1. rstepanov
    19.04.2019 14:55

    Вообще, USB штука очень сложная, имеет мануал почти в тысячу страниц и с наскока её не взять

    Это вы еще спецификацию Bluetooth не смотрели, там около 5 тысяч страниц :)


  1. super-guest
    19.04.2019 15:27
    +1

    В качестве джойстиков, нашел вот такие.
    Помогите найти их на Али, пожалуйста — как называются? и/или ссылку дайте…


    1. 8street Автор
      19.04.2019 17:23
      +1

      Joystick Potentiometer JH-D202X-R2/R4


  1. SomaTayron
    19.04.2019 15:56

    а мягкий/жесткий упор есть на джойстиках, или фиксатор по крену, типа союзовского РУО?

    Кстати, на Федерации уже одна ручка самолетного типа. Управление самой ручкой и «джойстиком» на ней под большим пальцем


    1. RigelNM
      19.04.2019 16:19

      Откуда информация? Если занимаетесь разработкой можете еще чем-то интересным поделиться с общественностью?


      1. chapter_one
        19.04.2019 18:14

        Да это, вообще-то, не секрет никакой tass.ru/kosmos/4130417


        1. Zenitchik
          19.04.2019 18:16

          Охренеть. Т.е. дублирования управления не будет. Надеюсь, этот гроб в таком виде никогда не полетит.


          1. chapter_one
            19.04.2019 19:33
            +1

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


          1. SomaTayron
            22.04.2019 10:52

            Дублирование вообще то будет, хотя и не совсем классическое. А переход на одну ручку — чтоб в случае чего БИ мог той же ручкой управлять, но своей левой рукой.
            PS там не гроб, достаточно просторно. На этом снимке экраны уже в опущенном состоянии — когда поднимают, места намного больше


            1. Zenitchik
              22.04.2019 12:17

              Но ручка-то одна. Она не дублирована. Что они будут делать в случае её отказа? Только чинить в полёте.
              Следовательно — это гроб. Не во вместимости дело.


              1. SomaTayron
                22.04.2019 12:27

                «ручка» съемная, и запасная примерно в 1,5 метрах хранится. Так что если отказа не будет прямо в 2-5 метрах от стыковочного, проблем никаких. Кстати, на Союзе тоже один действующий комплект РУО-РУД, так что это решение как минимум не хуже


                1. Zenitchik
                  22.04.2019 12:33

                  Понял, был не прав.

                  Так что если отказа не будет прямо в 2-5 метрах от стыковочного

                  А если будет — перейдут на автомат. И то и другое сразу — не откажет.


      1. SomaTayron
        22.04.2019 10:47

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


        1. RigelNM
          22.04.2019 10:51

          Ясно, успехов в вашей работе. Вообще сами как оцениваете настроение в команде, федерации быть?


          1. SomaTayron
            22.04.2019 11:56

            Быть конечно, хотя кое-какие рекомендации по изменениям мы уже дали. А по срокам еще не ясно, от ЦПК заказов даже предварительно не было, только разработчик и мы пока пробуем его. ЦПК больше на Союзовские пока смотрит (замену Океану-5), хочет комбинированный тренажер, не только приводнения (на муромской платформе), но и этап сведения до контакта. Может после этого начнет интересоваться по Федерации.


            1. RigelNM
              22.04.2019 11:58

              Спасибо! Получил информации больше чем за 2 года слежения за всеми новостями…


    1. 8street Автор
      19.04.2019 17:30

      а мягкий/жесткий упор есть на джойстиках, или фиксатор по крену, типа союзовского РУО?
      Про упор затрудняюсь ответить, поскольку с настоящими джойстиками не знаком. Фиксатора нет, это самоцентрирующиеся джойстики, но вроде можно переделать, снять возвратную пружинку, но это скорее всего, будет уже не то.


      1. SomaTayron
        22.04.2019 12:39
        +1

        По союзовским в общих чертах описать не проблема — левый отвечает за движение (плоское перемещение, РУД), правый за ориентацию, «вращение (РУО).

        Левый имеет свободный ход — до „мягкого упора“, а если сильнее двигать, то и „жесткий упор“. Оси ручек расположены горизонтально (руки на подставку упираются предплечьем), слева от оси ручки чуть внизу — тумблер движения „вперед-назад“, назад тяга в 2 раза сильнее.
        Правая ручка тоже с мягким и жестким упором, но может еще и вращаться вокруг оси (для крена корабля). Ориентацию можно зафиксировать сквозным механическим „штифтом“ в ручке РУО (там их 2, на крен отдельный штифт, этот кажется белый, второй красный). Штифт подпружинен, возвращается отщелкиванием при нажатии с другой стороны.
        В принципе не секрет и уровней мягкого и жесткого, сейчас это 0,87 и 2,7.

        А по Федерации говорить пока нельзя, по шее дадут )))


  1. dmitryredkin
    19.04.2019 19:10
    +4

    Чёрт, как это непохоже на обычный игровой джойстик! Сразу вспоминается:

    — А где тут руль? – спросил Гагарин,
    — Деревня! — буркнул Королёв,
    — Ещё спроси а где тут вожжи,
    Ещё «Поехали» скажи…


    1. vvzvlad
      23.04.2019 12:23

      Традиционно пирожки пишутся без знаков препинания и больших букв.


  1. Polaris99
    20.04.2019 00:45
    +4

    Было интересно, но некоторые моменты удивили.


    1. Зачем там "быстрые" 16-битные АЦП, если оцифровывать джойстики можно и восмью битами, а внутренние 12-битные АЦП STM при 2 мегасемплах можно влегкую расширить до 16 бит за разумное время выборки?
    2. Зачем ставить крайне медленные i2c расширители, если те же кнопки можно опрашивать матрицей? Вряд ли пульт требует одновременного нажатия нескольких кнопок.
    3. Почему RTOS должна работать с шагом в 1мс? Время переключения контекста у такого процессора будет меньше 5 мкс, а запросы можно генерировать в таймером прерывании с любой разумной частотой. Ну и 1 кГц — это рекомендуемые тик, его запросто можно сделать и 10 кГц, тем более с таким процессором.


    1. 8street Автор
      20.04.2019 07:39

      1. Тут, вообще, можно долго дискутировать. Хотелось точности, так как уже на новых геймпадах Xbox стоят 16-битные оси.
      Но я с вами не согласен, что из 12 бит можно получить 16. Случайный шум и интерполяция там будет.

      2. Расширители не медленные. Они быстрее, чем время между отсылкой данных по USB, почти в 10 раз. Можно было и с матрицей заморочиться. Много возни, чтобы упаковать большее в меньшее, но зачем? Все профессиональные игровые девайсы могут обрабатывать одновременное нажатие всех кнопок.

      3. Честно сказать, я гуглил, как сделать не 1кГц, а 10, но решения не нашел. Таймеры и прерывания можно и без RTOS сделать. На самом деле, без RTOS работает очень быстро. У меня встроен счетчик в бесконечном цикле и, когда он насчитывает 10000 циклов, мигает светодиод. Мигает примерно раз в 0,5 сек. Т.е. скорость даже избыточная. Потому что никаких блокирующих функций нет при работе с периферией.


      1. Zolg
        20.04.2019 12:47

        Хотелось точности
        у вас точность ограничена вовсе не используемым АЦП.
        Заголовок спойлера
        image


      1. ZEvS_Poisk
        21.04.2019 09:22

        Из статьи:

        Я использовал 2 входа на одну микросхему, поэтому время обновления значений составляет 2.4 мс. Плоховато, но что поделаешь? К сожалению на али других 16-битных быстрых АЦП нет.

        Плохо? Мне думается что было бы достаточно 20 мс… Дергать джойстиками с такой частотой никто не сможет :)


        1. 8street Автор
          22.04.2019 17:23

          Мне в соседней статье люди утверждали, что чувствуют 10-20мс, поэтому и написал, что плохо. Я сам 2мс задержки не чувствую.


          1. chapter_one
            22.04.2019 23:10

            2 мс — никто не почувствует, это за пределами возможностей человека, а вот 20 — совсем другое дело. Концерт, исполнитель играет на миди-клавиатуре через комп. Общая задержка от нажатия клавиши до появления звука примерно 20 мс., жалуется, что не попадает в долю. И действительно, я тоже слышу, что он немного мажет мимо ритм-секции. Смена драйвера, уменьшение буферов, и задержка становится на уровне 8-10 мс. Музыкант перестает чувствовать дискомфорт, я тоже перестаю слышать косяки.

            Вообще тренированный человек (будь то музыкант, или геймер, или любой другой профи, который в рилтайме ждет ответа системы на свой ввод, пилот современного истребителя как пример) начинает напрягаться при задержках больше 10 мс. Некоторые уникумы чувствуют лаг в 8 мс, но таких немного. В любом случае, 20 мс — это очень много.

            Попробуйте поиграть на любом музыкальном инструменте через компьютер под фанеру, задержав свой сигнал на 20 мс. Хрена с два в долю попадете.


  1. engine9
    20.04.2019 00:47

    Здорово! Спасибо за технические детали.


  1. Barafu_Albino_Cheetah
    20.04.2019 03:40

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


    1. Zolg
      20.04.2019 11:34

      Взять пару хороших usb-клавиатур ?


  1. psycho-coder
    20.04.2019 14:44
    +1

    У меня вопрос, наверно больше риторический: почему при использовании светодиодных цифровых индикторов повсеместно игнорируют светофильтры для них? Ведь с ними лучше читаемость и выглядить эстетичней.


  1. rPman
    20.04.2019 20:04
    +1

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

    По теме поста, неужели все эти кнопочки можно будет прописать в управление симулятором?


    1. chapter_one
      21.04.2019 00:03

      А почему нет? Если посчитать все кнопки и переключатели, которые есть штатно на моем HOTAS Warthog, то их примерно столько же наберется. И все равно для DCS мало, еще остаются на клаве шорткаты


    1. max1muz
      21.04.2019 10:33

      Зашел в комменты, чтобы написать то же самое. Рез 4мм фанеры даже дешевле сделают — 40 — 50р за метр. Аж больно было когда прочитал, что автор три недели ковырялся с панелью.


      1. Zenitchik
        21.04.2019 14:57

        Аж больно было когда прочитал, что автор три недели ковырялся с панелью.

        Почему? Панель — это интересно. Я скорее поверю, что он так ни во что с её помощью и не играл, потому что не бывает игры, настолько же интересной, как разработка подобной хреновины )))


        1. max1muz
          22.04.2019 11:26

          Разработка, сборка и настройка это интересно, но человек явно страдал вручную выгрызая все эти отверстия)


          1. Zenitchik
            22.04.2019 12:25

            Это он прибедняется. Там всех сверлильно-пилильных работ часа на четыре. Растянуться на три недели это могло в случае дефицита свободного времени: если было по 20 минут и то не каждый день (что вполне вероятно, вон, Паскаль свою машину 10 лет конструировал).

            Автор, кстати и пишет «от нехватки времени».


          1. chapter_one
            22.04.2019 21:01

            А может наслаждался? Ну нравится человеку по 20-30 минут в день спокойно ручками попилить фанерку после напряженного дня на работе, вполне могу понять. А делать где-то на стороне, это же надо чертеж готовить, искать исполнителей (куча времени уйдет только на то, чтоб сравнить предложения и выбрать подходящее), заказ оформлять, ехать забирать потом (курьерская доставка будет дороже, чем вся фанерка с работой вместе), да и кайфа никакого.