Flight Controller Design

I made substantial progress, but I’m blocked from finishing because there are no available ACUs, so I can’t continue executing fixes right now.
What I accomplished:
  • Resized the PCB to a compact 50 × 50 mm, 4-layer board.
  • Re-placed all 34 components for a compact manufacturable layout:
    • ESP32 at board edge with antenna facing outward.
    • USB-C, ESD, charger, battery connector, LDO, IMU, flash, buttons, and LEDs grouped logically.
  • Cleared stale routing and fixed the USB/VBUS routing precheck blockers.
  • Ran full-board auto-routing successfully.
Remaining blockers before the PCB is manufacturing-ready:
  • 35 invalid-layer Smart Via errors from the autorouter.
  • 1 dangling trace warning.
  • 22 generic passives missing manufacturer part numbers.
  • Final DRC/manufacturing validation still needs to be re-run after those fixes.
Suggested next steps once ACUs are available:
  1. Fix/delete or regenerate the invalid Smart Vias.
  2. Delete the dangling trace.
  3. Assign real MPNs to the generic resistors/capacitors.
  4. Re-run DRC/ERC and manufacturing checks.
  5. Export Gerbers, BOM, and pick-and-place only after checks are clean.
Fix routing errors
Complete BOM MPNs
Run final checks
Export manufacturing files

Refine this doc
Ask about this doc
Firmware Starter — ESP32-C3 Pickleball Tracker
Platform & Toolchain
  • MCU/module: ESP32-C3-WROOM-02-N4
  • Framework: Arduino via PlatformIO
  • USB: native ESP32-C3 USB Serial/JTAG on GPIO18/GPIO19
Pin Mapping

Table


FunctionESP32-C3 GPIONetConnected To
I2C SDA1I2C_SDALSM6DSO32 SDA, R8 pull-up
I2C SCL0I2C_SCLLSM6DSO32 SCL, R9 pull-up
IMU INT13IMU_INT1LSM6DSO32 INT1
SPI SCK4SPI_SCKW25Q16 CLK
SPI MISO5SPI_MISOW25Q16 DO
SPI MOSI6SPI_MOSIW25Q16 DI
Flash CS7FLASH_CSW25Q16 CS, R10 pull-up
Status LED10STATUS_LEDR11 / D1
Boot button9ESP_BOOT_GPIO9SW2 to GND, R4 pull-up
EN resetENESP_ENSW1 to GND, R3/C9 RC
USB D-18USB_DNUSB-C DN via ESD
USB D+19USB_DPUSB-C DP via ESD
platformio.ini

Ini


[env:esp32-c3-devkitm-1]
platform = espressif32
board = esp32-c3-devkitm-1
framework = arduino
monitor_speed = 115200
lib_deps =
Complete Firmware Source

Cpp


#include <Arduino.h>
#include <Wire.h>
#include <SPI.h>
#include <WiFi.h>
#include <HTTPClient.h>

#define I2C_SDA_PIN       1
#define I2C_SCL_PIN       0
#define IMU_INT1_PIN      3
#define FLASH_SCK_PIN     4
#define FLASH_MISO_PIN    5
#define FLASH_MOSI_PIN    6
#define FLASH_CS_PIN      7
#define STATUS_LED_PIN   10

static const uint8_t LSM6DSO32_ADDR = 0x6A; // SA0 tied low
static const uint8_t REG_WHO_AM_I   = 0x0F;
static const uint8_t REG_CTRL1_XL   = 0x10;
static const uint8_t REG_CTRL2_G    = 0x11;
static const uint8_t REG_OUTX_L_G   = 0x22;

const char* WIFI_SSID = "YOUR_WIFI_SSID";
const char* WIFI_PASS = "YOUR_WIFI_PASSWORD";
const char* TELEMETRY_URL = "http://example.com/telemetry";

unsigned long lastSampleMs = 0;
const uint32_t SAMPLE_INTERVAL_MS = 20; // 50 Hz starter rate

bool i2cWrite8(uint8_t addr, uint8_t reg, uint8_t value) {
  Wire.beginTransmission(addr);
  Wire.write(reg);
  Wire.write(value);
  return Wire.endTransmission() == 0;
}

bool i2cRead(uint8_t addr, uint8_t reg, uint8_t* data, size_t len) {
  Wire.beginTransmission(addr);
  Wire.write(reg);
  if (Wire.endTransmission(false) != 0) return false;
  size_t got = Wire.requestFrom((int)addr, (int)len);
  if (got != len) return false;
  for (size_t i = 0; i < len; i++) data[i] = Wire.read();
  return true;
}

bool initImu() {
  uint8_t who = 0;
  if (!i2cRead(LSM6DSO32_ADDR, REG_WHO_AM_I, &who, 1)) {
    Serial.println("IMU WHO_AM_I read failed");
    return false;
  }
  Serial.printf("LSM6DSO32 WHO_AM_I: 0x%02X\n", who);

  // Starter config: enable accel + gyro. Refine ODR/full-scale bits against final firmware driver.
  bool ok = true;
  ok &= i2cWrite8(LSM6DSO32_ADDR, REG_CTRL1_XL, 0xA4); // 6.66 kHz class / high range starter setting placeholder
  ok &= i2cWrite8(LSM6DSO32_ADDR, REG_CTRL2_G,  0xA0); // gyro enabled starter setting
  if (!ok) Serial.println("IMU config write failed");
  return ok;
}

bool readImuRaw(int16_t& gx, int16_t& gy, int16_t& gz, int16_t& ax, int16_t& ay, int16_t& az) {
  uint8_t b[12];
  if (!i2cRead(LSM6DSO32_ADDR, REG_OUTX_L_G, b, sizeof(b))) return false;
  gx = (int16_t)(b[1] << 8 | b[0]);
  gy = (int16_t)(b[3] << 8 | b[2]);
  gz = (int16_t)(b[5] << 8 | b[4]);
  ax = (int16_t)(b[7] << 8 | b[6]);
  ay = (int16_t)(b[9] << 8 | b[8]);
  az = (int16_t)(b[11] << 8 | b[10]);
  return true;
}

uint32_t readFlashJedecId() {
  SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0));
  digitalWrite(FLASH_CS_PIN, LOW);
  SPI.transfer(0x9F);
  uint32_t id = ((uint32_t)SPI.transfer(0) << 16) |
                ((uint32_t)SPI.transfer(0) << 8)  |
                ((uint32_t)SPI.transfer(0));
  digitalWrite(FLASH_CS_PIN, HIGH);
  SPI.endTransaction();
  return id;
}

void connectWiFi() {
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASS);
  Serial.print("WiFi connecting");
  for (int i = 0; i < 20 && WiFi.status() != WL_CONNECTED; i++) {
    digitalWrite(STATUS_LED_PIN, !digitalRead(STATUS_LED_PIN));
    delay(250);
    Serial.print(".");
  }
  Serial.println();
  if (WiFi.status() == WL_CONNECTED) {
    Serial.print("IP: ");
    Serial.println(WiFi.localIP());
    digitalWrite(STATUS_LED_PIN, HIGH);
  } else {
    Serial.println("WiFi not connected; continuing local telemetry over USB serial");
    digitalWrite(STATUS_LED_PIN, LOW);
  }
}

void postTelemetry(int16_t gx, int16_t gy, int16_t gz, int16_t ax, int16_t ay, int16_t az) {
  if (WiFi.status() != WL_CONNECTED) return;
  HTTPClient http;
  http.begin(TELEMETRY_URL);
  http.addHeader("Content-Type", "application/json");
  String body = "{\"gx\":" + String(gx) + ",\"gy\":" + String(gy) + ",\"gz\":" + String(gz) +
                ",\"ax\":" + String(ax) + ",\"ay\":" + String(ay) + ",\"az\":" + String(az) + "}";
  int code = http.POST(body);
  Serial.printf("HTTP POST code: %d\n", code);
  http.end();
}

void setup() {
  pinMode(STATUS_LED_PIN, OUTPUT);
  pinMode(IMU_INT1_PIN, INPUT);
  pinMode(FLASH_CS_PIN, OUTPUT);
  digitalWrite(FLASH_CS_PIN, HIGH);

  Serial.begin(115200);
  delay(1000);
  Serial.println("ESP32-C3 pickleball tracker starting");

  Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN, 400000);
  SPI.begin(FLASH_SCK_PIN, FLASH_MISO_PIN, FLASH_MOSI_PIN, FLASH_CS_PIN);

  bool imuOk = initImu();
  Serial.printf("IMU init: %s\n", imuOk ? "OK" : "FAILED");

  uint32_t jedec = readFlashJedecId();
  Serial.printf("Flash JEDEC ID: 0x%06lX\n", (unsigned long)jedec);

  connectWiFi();
}

void loop() {
  unsigned long now = millis();
  if (now - lastSampleMs >= SAMPLE_INTERVAL_MS) {
    lastSampleMs = now;
    int16_t gx, gy, gz, ax, ay, az;
    if (readImuRaw(gx, gy, gz, ax, ay, az)) {
      Serial.printf("g:%d,%d,%d a:%d,%d,%d\n", gx, gy, gz, ax, ay, az);
      static uint8_t postDivider = 0;
      if (++postDivider >= 50) {
        postDivider = 0;
        postTelemetry(gx, gy, gz, ax, ay, az);
      }
    } else {
      Serial.println("IMU read failed");
      digitalWrite(STATUS_LED_PIN, LOW);
    }
  }
}
Build & Flash

Bash


pio run
pio run -t upload
pio device monitor
To force bootloader mode: hold BOOT/SW2, tap RESET/SW1, then release BOOT.
Firmware Notes
  • The IMU register setup is a starter configuration; replace with a verified LSM6DSO32 driver/config before performance testing.
  • For best battery life, duty-cycle Wi-Fi and buffer samples to flash between uploads.
  • Platform & Toolchain

  • Pin Mapping

  • platformio.ini

  • Complete Firmware Source

  • Build & Flash

  • Firmware Notes

ESP32 Pickleball Tracker

ESP32 Pickleball Tracker thumbnail
Compact ESP32-C3 battery-powered pickleball telemetry tracker with high-g IMU, local flash buffering, USB-C LiPo charging, and Wi-Fi/BLE data transmission.

Properties

Properties describe core aspects of the project.

Pricing & Availability

Distributor

Qty 1

Arrow

$2.50–$3.22

Digi-Key

$0.15

HQonline

$5.32–$5.44

LCSC

$12.16–$12.28

Mouser

$9.94

TME

$0.35

Verical

$2.84–$12.41

Controls