Приветствую всех самодельщиков!
Позвольте представить вам для обсуждения/повторения проект беспроводного, мобильного руля-штурвала. Название проекта - LeanDrive. Контроллер не привязан к столу, удерживается руками в воздухе. Для управления используются наклоны/повороты руля, две аналоговых педали (газ-тормоз или что хотите) и 6 кнопок. Руль определяется системой как геймпад и работает с подавляющим большинством игр. Если игра хочет проприетарный контроллер (типа XBox-овского), то можно использовать программы "переходники" типа x360ce.
Руль собран на отладочной плате ESP32 WROOM. Наклон контролируется гироскопом-акселерометром MPU-6050/ Корпус напечатан на 3D-принтере. Печатные платы делали сами на небольшом фрезере с ЧПУ (CNC1610). Это уже вторая версия. Первый прототип был более простой и проводный на Atmega 32U4, как и руль автора сайта.
Презентационное видео, в котором описываются особенности устройства, проводятся тесты и кратко описываются технические детали. В комментариях к видео есть ссылки на все модели, детали, схемы, код. Ниже я их продублирую.
3D модели размещены на сайте 3dtoday.
LeanDrive - беспроводный руль для компьютерных гонок
Электронная часть. К сожалению, платы проектировались отдельно на SprintLayout (EasyEDA освоил чуть позже). Они приаттачены к проекту в соответствующем формате. Могу выложить гербер или картинки для ЛУТ. Там же есть и код.
Проект на EasyEDA
Код написан на ArduinoIDE с установленным драйвером для ESP32.
Изображения
Буду признателен за любые комментарии, замечания, пожелания!
Спасибо Александру за ценнейший ресурс!
P.S. 30.03.2021 Версия 1.1: добавления, исправления
Позвольте представить вам для обсуждения/повторения проект беспроводного, мобильного руля-штурвала. Название проекта - LeanDrive. Контроллер не привязан к столу, удерживается руками в воздухе. Для управления используются наклоны/повороты руля, две аналоговых педали (газ-тормоз или что хотите) и 6 кнопок. Руль определяется системой как геймпад и работает с подавляющим большинством игр. Если игра хочет проприетарный контроллер (типа XBox-овского), то можно использовать программы "переходники" типа x360ce.
Руль собран на отладочной плате ESP32 WROOM. Наклон контролируется гироскопом-акселерометром MPU-6050/ Корпус напечатан на 3D-принтере. Печатные платы делали сами на небольшом фрезере с ЧПУ (CNC1610). Это уже вторая версия. Первый прототип был более простой и проводный на Atmega 32U4, как и руль автора сайта.
Презентационное видео, в котором описываются особенности устройства, проводятся тесты и кратко описываются технические детали. В комментариях к видео есть ссылки на все модели, детали, схемы, код. Ниже я их продублирую.
3D модели размещены на сайте 3dtoday.
LeanDrive - беспроводный руль для компьютерных гонок
Электронная часть. К сожалению, платы проектировались отдельно на SprintLayout (EasyEDA освоил чуть позже). Они приаттачены к проекту в соответствующем формате. Могу выложить гербер или картинки для ЛУТ. Там же есть и код.
Проект на EasyEDA
Код написан на ArduinoIDE с установленным драйвером для ESP32.
Проект для Arduino IDE:
/*
* LeanDrive, Bluetooth руль для компьютерных игр
* https://www.youtube.com/watch?v=BMJRcHLejD8&t
* TERRABYTE channel
*
* v. 1.1 от 30.03.2021
* Добавлено:
* - автоматическая калибровка педалей.
* Исправлено:
* - Некорректное отображение (озвучивание) уровней заряда аккумулятора.
*
* v. 1.0 от 22.03.2021
* Базовая версия
*/
#include <BleGamepad.h>
#include <WiFi.h>
#include "Wire.h"
// Назначение пинов контроллера
#define ACCELERATOR_PIN 27 // Педаль газа
#define BREAK_PIN 14 // Педаль тормоза
#define BTN_LEFT_CASE_PIN 33 // Пин левой кнопки на корпусе (нумерация на плате)
#define BTN_RIGHT_CASE_PIN 32 // Пин правой кнопки на корпусе
#define BTN_LEFT_TOP_HANDLE_PIN 4 // Пин левой верхней кнопки на руле
#define BTN_LEFT_BOTTOM_HANDLE_PIN 5 // Пин левой нижней кнопки на руле
#define BTN_RIGHT_TOP_HANDLE_PIN 13 // Пин правой верхней кнопки на руле
#define BTN_RIGHT_BOTTOM_HANDLE_PIN 12 // Пин правой нижней кнопки на руле
#define ACCUM_PIN 15 // Уровень заряда аккумулятора
#define SOUND_PIN 26 // Пин динамика
#define MPU_ADDR 0x68 // Адрес гироскопа
#define SOUND_FREQ 2000 // Частота звука динамика по умолчанию
#define SOUND_DUTY 128 // Скважность импульсов тона 0...255 (128 - 50%)
#define SOUND_RESOLUTION 8 // Разрешение по скважности в битах
#define SOUND_CHANNEL 0 // Канал таймера ШИМ
#define SOUND_INDICATOR_PERIOD 60000 // Период опроса датчика заряда аккумулятора
#define SOUND_PULSE_TIME 200 // Длительность звукового импульса
#define SOUND_PULCES 5 // Количество импульсов в звуке "аккумулятор менее 20 процентов"
#define ACCUM_LOW_ADC 1770 // Нижний уровень заряда аккумулятора в уровне АЦП. Зависит от делителя напряжения.
#define ACCUM_HI_ADC 2370 // Верхний уровень заряда аккумулятора в уровне АЦП
#define TIME_ROUND 20 // период опроса кнопок руля (мс)
#define MODE_SWITCH_TIME 3000 // Время зажатия кнопок для переключения режима (секунд);
enum drive_mode_t {CAR_MODE, FLY_MODE}; // Режим работы руля: автомобиль или самолет (влияет на интерпретацию значения педалей - как разные оси или как одна ось)
uint32_t last_time = 0; // Время последнего опроса
uint32_t last_accum_time = 0; // Время последнего опроса аккумулятора
uint32_t pulse_time = 0; // Время импульса звука или паузы
uint32_t mode_time = 0; // Время зажатия клавиш переключения режима руля
// Ускорения, угловые скорости, температура
int16_t ax, ay, az, gx, gy, gz, temperature;
int32_t accelerator_value = 0; // Значение с АЦП педали газа
int32_t break_value = 0; // Значение с АЦП педали тормоза
int32_t accelerator_low_level = 1250; // Нижний порог педали ускорения
int32_t break_low_level = 1450; // нижний порог педали тормоза
//int16_t accelerator = 0; // Промежуточная переменная для приведения данных
int16_t wheel_angle = 0; // Поворот руля
int16_t wheel_tilt = 0; // Наклон руля (вперед, назад)
int32_t accum = 0; // Заряд аккумулятора в %
int32_t accum_past = 0; // Прошлое значение фильтрованных данных с АЦП (для фильтра)
uint16_t pulse_count = 0; // Переменная обратного отсчета оставщихся импульсов (точнее смен состояния)
drive_mode_t drive_mode = CAR_MODE; // По-умолчанию режим автомобиля
BleGamepad bleGamepad("LeanDrive-BT", "Terrabyte", 50);
void setup() {
// Выключаем WiFi
WiFi.mode(WIFI_OFF);
// Назначение кнопок
pinMode(BTN_LEFT_CASE_PIN, INPUT_PULLUP);
pinMode(BTN_RIGHT_CASE_PIN, INPUT_PULLUP);
pinMode(BTN_LEFT_TOP_HANDLE_PIN, INPUT_PULLUP);
pinMode(BTN_LEFT_BOTTOM_HANDLE_PIN, INPUT_PULLUP);
pinMode(BTN_RIGHT_TOP_HANDLE_PIN, INPUT_PULLUP);
pinMode(BTN_RIGHT_BOTTOM_HANDLE_PIN, INPUT_PULLUP);
// Инициализация гироскопа
Wire.begin();
Wire.beginTransmission(MPU_ADDR);
Wire.write(0x6B); // PWR_MGMT_1 register
Wire.write(0); // set to zero (wakes up the MPU-6050)
Wire.endTransmission(true);
bleGamepad.begin(); // Инициализация Bluetooth
bleGamepad.setAutoReport(false); // Устанавливаем ручное подтверждаение транзакции при помощи bleGamepad.sendReport();
// Инициализируем ШИМ для генерации звуков
ledcSetup(SOUND_CHANNEL, SOUND_FREQ, SOUND_RESOLUTION);
ledcAttachPin(SOUND_PIN, SOUND_CHANNEL);
// Измеряем напряжение аккумулятора путем усреднения по 20 отсчетам для снижения колебаний
// Проводим калибровку уровней покоя педалей
accum = 0;
for (int i = 1; i <= 20; i++) {
accum = accum + analogRead(ACCUM_PIN);
accelerator_low_level = accelerator_low_level + analogRead(ACCELERATOR_PIN);
break_low_level = break_low_level + analogRead(BREAK_PIN);
delay(20); // Задержка для устранения переходных процессов
}
accum = accum / 20;
accelerator_low_level = accelerator_low_level / 20 + 100;
break_low_level = break_low_level / 20 + 100;
accum_past = accum; // Установка начального значения заряда
accum = constrain(accum, ACCUM_LOW_ADC, ACCUM_HI_ADC);
accum = map(accum, ACCUM_LOW_ADC, ACCUM_HI_ADC, 1, 99);
// Вычисляем, сколько раз пропикать
pulse_count = (accum / 10) * 2; // Например, 65% - 6 раз (умножение на два - количество переходов в импульсах: один писк - два перехода: вкл и выкл)
// Serial.begin(115200);
// Serial.println("Starting...");
}
void loop() {
// Проверка клавиш переключения режима (если нажаты все 4 кнопки на баранке в течение 3 сек, то переводим режим автомобиль - самолет)
if (!digitalRead(BTN_LEFT_TOP_HANDLE_PIN) && !digitalRead(BTN_LEFT_BOTTOM_HANDLE_PIN) && !digitalRead(BTN_RIGHT_TOP_HANDLE_PIN) && !digitalRead(BTN_RIGHT_BOTTOM_HANDLE_PIN)) {
if (mode_time == 0) mode_time = millis(); // Если до этого не нажаты, то фиксируем время нажатия
if ((millis() - mode_time) >= MODE_SWITCH_TIME) {
// Переключаем режим
if (drive_mode == CAR_MODE) {
drive_mode = FLY_MODE;
pulse_count = 4; // Пищим 2 раза
} else {
drive_mode = CAR_MODE;
pulse_count = 2; // Пропищим разочек
}
// Сбросим время нажатия
mode_time = 0;
}
} else {
mode_time = 0; // Если хотя бы одна кнопка отжата, то сбрасываем время нажатия
}
// Проверка уровня заряда
if ((millis() - last_accum_time) >= SOUND_INDICATOR_PERIOD) {
last_accum_time = millis();
if (accum < 20) pulse_count = SOUND_PULCES * 2; // Количество смен состояний в 2 раза больше количества импульсов
}
// Обработка звука
if (pulse_count > 0) {
// Пора ли менять состояние звука
if ((millis() - pulse_time) >= SOUND_PULSE_TIME) {
pulse_time = millis();
if ((pulse_count & 1) == 0) ledcWrite(SOUND_CHANNEL, SOUND_DUTY); // Включаем звук если число четное
else ledcWrite(SOUND_CHANNEL, 0);
pulse_count = pulse_count - 1;
}
}
// Пора ли опрашивать датчики
if ((millis() - last_time) >= TIME_ROUND)
{
last_time = millis();
// Чтение данных с гироскопа
mpu_read();
// Вычисляем поворот руля
wheel_angle = -constrain(ay, -16380, 16380);
// Serial.print(ay);
// Serial.print(" ");
// Serial.print(wheel_angle);
wheel_angle = map(wheel_angle, -16380, 16380, -32760, 32760);
if (wheel_angle >= 32766) wheel_angle = 32760;
if (wheel_angle <= -32766) wheel_angle = -32760;
// Serial.print(" ");
// Serial.println(wheel_angle);
// Вычисляем наклон руля
wheel_tilt = -constrain(ax, -16380, 16380);
wheel_tilt = map(wheel_tilt, -16380, 16380, -32760, 32760);
if (wheel_tilt >= 32766) wheel_tilt = 32760;
if (wheel_tilt <= -32766) wheel_tilt = -32760;
// Вычисление педали газа
accelerator_value = analogRead(ACCELERATOR_PIN);
accelerator_value = constrain(accelerator_value, accelerator_low_level, 4095);
accelerator_value = map(accelerator_value, accelerator_low_level, 4095, 1, 65530); // Диапазон для триггеров
if (accelerator_value < 1 ) accelerator_value = 1;
if (accelerator_value > 65530 ) accelerator_value = 65530;
// Вычисление педали тормоза
break_value = analogRead(BREAK_PIN);
break_value = constrain(break_value, break_low_level, 4095);
break_value = map(break_value, break_low_level, 4095, 1, 65530); // Диапазон для триггеров
if (break_value < 1 ) break_value = 1;
if (break_value > 65530 ) break_value = 65530;
// Получение уровня заряда аккумулятора
accum = analogRead(ACCUM_PIN);
if (accum_past == 0) accum_past = accum; // Для первого раза
accum = (accum_past * 9 + accum) / 10; // IIR фильтр (Сглаживает колебания по питанию). y(i) = y(i-1) * 0.9 + x(i) * 0.1, где y - выходной сигнал, x - входной
accum_past = accum;
accum = constrain(accum, ACCUM_LOW_ADC, ACCUM_HI_ADC);
accum = map(accum, ACCUM_LOW_ADC, ACCUM_HI_ADC, 1, 99);
// Если руль подключен к компьютеру, передаем данные
if(bleGamepad.isConnected())
{
// Установка наклона руля (левый стик)
bleGamepad.setLeftThumb(wheel_angle, wheel_tilt);
// Установка акселератора и тормоза в зависимости от режима
if (drive_mode == CAR_MODE) {
// В режиме автомобиля управление через триггеры
bleGamepad.setRightTrigger(accelerator_value);
bleGamepad.setLeftTrigger(break_value);
} else {
// В режиме самолета управление через одну ось правого стика
accelerator_value = (accelerator_value - break_value) / 2;
// Serial.print(accelerator_value);
// Serial.print(" ");
bleGamepad.setRightThumb(accelerator_value, 0);
}
// bleGamepad.setRightThumb(accelerator_value, break_value); // Управление газом-тормозом через стики
// bleGamepad.setBatteryLevel(accum); // С этим проблемы. Геймпад видно, но реакции на кнопки нет (joy.cpl не видит). Надо разбираться.
// Реакция на кнопки
if (!digitalRead(BTN_LEFT_CASE_PIN)) bleGamepad.press(BUTTON_1);
else bleGamepad.release(BUTTON_1);
if (!digitalRead(BTN_RIGHT_CASE_PIN)) bleGamepad.press(BUTTON_2);
else bleGamepad.release(BUTTON_2);
if (!digitalRead(BTN_LEFT_TOP_HANDLE_PIN)) bleGamepad.press(BUTTON_3);
else bleGamepad.release(BUTTON_3);
if (!digitalRead(BTN_LEFT_BOTTOM_HANDLE_PIN)) bleGamepad.press(BUTTON_4);
else bleGamepad.release(BUTTON_4);
if (!digitalRead(BTN_RIGHT_TOP_HANDLE_PIN)) bleGamepad.press(BUTTON_5);
else bleGamepad.release(BUTTON_5);
if (!digitalRead(BTN_RIGHT_BOTTOM_HANDLE_PIN)) bleGamepad.press(BUTTON_6);
else bleGamepad.release(BUTTON_6);
// Передаем данные
bleGamepad.sendReport();
}
}
}
// Чтение данных с гироскопа
void mpu_read(){
Wire.beginTransmission(MPU_ADDR);
Wire.write(0x3B); // начинаем с регистра 0x3B (ACCEL_XOUT_H)
Wire.endTransmission(false);
Wire.requestFrom(MPU_ADDR,14,true); // запросить всего 14 регистров
ax=Wire.read()<<8|Wire.read(); // 0x3B (ACCEL_XOUT_H) & 0x3C (ACCEL_XOUT_L)
ay=Wire.read()<<8|Wire.read(); // 0x3D (ACCEL_YOUT_H) & 0x3E (ACCEL_YOUT_L)
az=Wire.read()<<8|Wire.read(); // 0x3F (ACCEL_ZOUT_H) & 0x40 (ACCEL_ZOUT_L)
temperature=Wire.read()<<8|Wire.read(); // 0x41 (TEMP_OUT_H) & 0x42 (TEMP_OUT_L)
gx=Wire.read()<<8|Wire.read(); // 0x43 (GYRO_XOUT_H) & 0x44 (GYRO_XOUT_L)
gy=Wire.read()<<8|Wire.read(); // 0x45 (GYRO_YOUT_H) & 0x46 (GYRO_YOUT_L)
gz=Wire.read()<<8|Wire.read(); // 0x47 (GYRO_ZOUT_H) & 0x48 (GYRO_ZOUT_L)
}
Буду признателен за любые комментарии, замечания, пожелания!
Спасибо Александру за ценнейший ресурс!
P.S. 30.03.2021 Версия 1.1: добавления, исправления
Изменено: