Gebo你是誰?

USB 方向盤控制遙控車

你是誰?
進行中發布 2026-09-29・最後更新 2026-09-30
怎麼用方向盤開遙控車?

用 ESP32-S3 讀取 USB 方向盤,再透過 ELRS 高頻頭送出 CRSF 訊號,就能用方向盤開遙控車。下面分三步說明。

STEP 1

ESP32 硬體連接

STEP 2

ESP32-S3 燒入程式碼

#include <Arduino.h>
#include "driver/gptimer.h"

// ==========================================
// 【硬體腳位定義:ESP32-S3 雙線雙震動完整版】
// ==========================================
const int PEDAL_PIN = 1;      // 油門/煞車踏板輸入 (GPIO 1)
const int STEERING_PIN = 2;   // 方向盤轉向輸入 (GPIO 2)
#define PPM_PIN 18            // PPM 訊號輸出腳 (GPIO 18)

// 左震動馬達 (A側)
const byte LEFT_PWM = 13;     // ENA -> 控制左馬達強弱
const byte LEFT_IN = 16;      // IN2 -> 數位輸出 (建立電位差)

// 右震動馬達 (B側) 
const byte RIGHT_PWM = 4;     // ENB -> 控制右馬達強弱
const byte RIGHT_IN = 5;      // IN4 -> 數位輸出 (建立電位差)

// ==========================================
// 【操控參數與死區校正】
// ==========================================
const int PEDAL_MIN = 0;       
const int PEDAL_CENTER = 1870; // 油門實測中立點
const int PEDAL_MAX = 4095;    
const int PEDAL_DEADZONE = 40; 
const int STEERING_MIN = 0;    
const int STEERING_MAX = 4095; 
const int STEERING_DEADZONE_LOW = 1930;  // 方向盤中立點下限
const int STEERING_DEADZONE_HIGH = 1970; // 方向盤中立點上限

// ==========================================
// 【PPM 參數與極性設定】
// ==========================================
#define PPM_POLARITY_NEGATIVE
#ifdef PPM_POLARITY_NEGATIVE
  #define SIGNAL_ON  LOW
  #define SIGNAL_OFF HIGH
#else
  #define SIGNAL_ON  HIGH
  #define SIGNAL_OFF LOW
#endif

#define NUM_CHANNELS 8
#define PPM_FRAME_LEN 22500 
#define PPM_PULSE_LEN 300
#define PPM_MIN 1000
#define PPM_MID 1500
#define PPM_MAX 2000

volatile uint16_t ppm_channels[NUM_CHANNELS];
gptimer_handle_t ppm_timer = NULL;
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;

// ==========================================
// 【gptimer 硬體中斷函數】
// ==========================================
static bool IRAM_ATTR on_timer_alarm(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *user_ctx) {
  static bool is_pulse = true;
  static uint8_t current_channel = 0;
  static uint32_t total_frame_time = 0;

  portENTER_CRITICAL_ISR(&timerMux);
  uint32_t next_alarm_val = 0;

  if (is_pulse) {
    digitalWrite(PPM_PIN, SIGNAL_ON);
    next_alarm_val = PPM_PULSE_LEN;
    total_frame_time += PPM_PULSE_LEN;
    is_pulse = false;
  } else {
    digitalWrite(PPM_PIN, SIGNAL_OFF);
    
    if (current_channel < NUM_CHANNELS) {
      uint32_t data_len = ppm_channels[current_channel] - PPM_PULSE_LEN;
      next_alarm_val = data_len;
      total_frame_time += data_len;
      current_channel++;
      is_pulse = true;
    } else {
      uint32_t sync_len = PPM_FRAME_LEN - total_frame_time;
      if (sync_len < 4000) {
        sync_len = 4000;
      }
      next_alarm_val = sync_len;
      current_channel = 0;
      total_frame_time = 0;
      is_pulse = true;
    }
  }

  gptimer_alarm_config_t alarm_config = {
    .alarm_count = next_alarm_val,
    .reload_count = 0,
    .flags = { .auto_reload_on_alarm = true }
  };
  gptimer_set_alarm_action(timer, &alarm_config);
  portEXIT_CRITICAL_ISR(&timerMux);
  return true;
}

// ==========================================
// 【初始化設定】
// ==========================================
void setup() {
  Serial.begin(115200);
  analogReadResolution(12);
  analogSetAttenuation(ADC_11db);

  pinMode(PPM_PIN, OUTPUT);
  digitalWrite(PPM_PIN, SIGNAL_OFF); 

  // 初始化震動馬達腳位
  pinMode(LEFT_PWM, OUTPUT);
  pinMode(LEFT_IN, OUTPUT);
  pinMode(RIGHT_PWM, OUTPUT);
  pinMode(RIGHT_IN, OUTPUT);

  digitalWrite(LEFT_IN, LOW);
  digitalWrite(RIGHT_IN, LOW);

  for (int i = 0; i < NUM_CHANNELS; i++) {
    ppm_channels[i] = PPM_MID;
  }

  gptimer_config_t timer_config = {
    .clk_src = GPTIMER_CLK_SRC_DEFAULT,
    .direction = GPTIMER_COUNT_UP,
    .resolution_hz = 1000000, 
    .intr_priority = 0,
    .flags = { .intr_shared = false }
  };
  gptimer_new_timer(&timer_config, &ppm_timer);

  gptimer_event_callbacks_t cbs = { .on_alarm = on_timer_alarm };
  gptimer_register_event_callbacks(ppm_timer, &cbs, NULL);

  gptimer_alarm_config_t alarm_config = {
    .alarm_count = PPM_PULSE_LEN,
    .reload_count = 0,
    .flags = { .auto_reload_on_alarm = true }
  };
  gptimer_set_alarm_action(ppm_timer, &alarm_config);

  gptimer_enable(ppm_timer);
  gptimer_start(ppm_timer);

  Serial.println("==================================================");
  Serial.println(" ESP32-S3 觸覺回饋完美對稱版:立體震動系統啟動! ");
  Serial.println("==================================================");
}

// ==========================================
// 【主程式循環】
// ==========================================
void loop() {
  int raw_pedal = analogRead(PEDAL_PIN);
  int raw_steering = analogRead(STEERING_PIN);

  // 油門/煞車踏板映射
  int pedal_ppm = PPM_MID; 
  if (raw_pedal > (PEDAL_CENTER + PEDAL_DEADZONE)) {
    ppm_channels[1] = map(raw_pedal, PEDAL_CENTER, PEDAL_MAX, PPM_MID, PPM_MAX);
    pedal_ppm = ppm_channels[1];
  } else if (raw_pedal < (PEDAL_CENTER - PEDAL_DEADZONE)) {
    ppm_channels[1] = map(raw_pedal, PEDAL_MIN, PEDAL_CENTER, PPM_MIN, PPM_MID);
    pedal_ppm = ppm_channels[1];
  } else {
    ppm_channels[1] = PPM_MID;
    pedal_ppm = PPM_MID;
  }

  // 方向盤轉向映射
  int steer_ppm = PPM_MID; 
  if (raw_steering >= STEERING_DEADZONE_LOW && raw_steering <= STEERING_DEADZONE_HIGH) {
    steer_ppm = PPM_MID;
  } else if (raw_steering > STEERING_DEADZONE_HIGH) {
    steer_ppm = map(raw_steering, STEERING_DEADZONE_HIGH, STEERING_MAX, PPM_MID, PPM_MIN);
  } else if (raw_steering < STEERING_DEADZONE_LOW) {
    steer_ppm = map(raw_steering, STEERING_DEADZONE_LOW, STEERING_MIN, PPM_MID, PPM_MAX);
  }
  steer_ppm = constrain(steer_ppm, PPM_MIN, PPM_MAX);

  // 計算雙側震動強度
  int left_out_pwm = 0;
  int right_out_pwm = 0;

  if (pedal_ppm >= PPM_MID) {
    int base_vib = map(pedal_ppm, PPM_MID, PPM_MAX, 80, 255);
    digitalWrite(LEFT_IN, HIGH);
    digitalWrite(RIGHT_IN, HIGH);

    if (steer_ppm < PPM_MID) { 
      // 左轉增強
      int bias = map(steer_ppm, PPM_MIN, PPM_MID, 50, 0);
      left_out_pwm = constrain(base_vib + bias, 80, 255);
      right_out_pwm = constrain(base_vib - bias, 0, 255);
    } else if (steer_ppm > PPM_MID) { 
      // 右轉增強
      int bias = map(steer_ppm, PPM_MID, PPM_MAX, 0, 50);
      left_out_pwm = constrain(base_vib - bias, 0, 255);
      right_out_pwm = constrain(base_vib + bias, 80, 255);
    } else { 
      left_out_pwm = base_vib;
      right_out_pwm = base_vib;
    }
  } else {
    left_out_pwm = 0;
    right_out_pwm = 0;
    digitalWrite(LEFT_IN, LOW);
    digitalWrite(RIGHT_IN, LOW);
  }

  analogWrite(LEFT_PWM, left_out_pwm);
  analogWrite(RIGHT_PWM, right_out_pwm);

  // 安全寫入中斷共享變數
  portENTER_CRITICAL(&timerMux);
  ppm_channels[0] = steer_ppm;  // CH1 轉向
  ppm_channels[1] = pedal_ppm;  // CH2 油門
  ppm_channels[2] = PPM_MID;    // CH3
  ppm_channels[3] = PPM_MID;    // CH4
  portEXIT_CRITICAL(&timerMux);

  Serial.printf("[監控] 1.油門(PPM): %4dus | 2.轉向(PPM): %4dus | 3.左震動(PWM): %3d | 4.右震動(PWM): %3d\n", 
                pedal_ppm, steer_ppm, left_out_pwm, right_out_pwm);
                
  delay(10); 
}

準備清單

硬體

  • ESP32-S3
  • ELRS 高頻頭
  • 遙控車
  • 電調
  • ELRS 轉 PWM 接收器
  • USB 方向盤

程式庫

  • AlPlC_PCM

開發版驅動

  • Arduino ESP32 Board
請問要如何操作以上步驟

先將step1接線圖如上接線,接下來將程式代碼貼上Arduino燒錄器,點擊上傳便可

相關文章

沒有直接連結的,依相同標籤補上。

虛線框是尚未整理成文章的原始筆記,點進去可以看到完整的思路脈絡。