Top Silkscreen Layer


d6409383-ab7d-4768-bdde-3a6311d6f610 is this on the top or the bottom layer?
d6409383-ab7d-4768-bdde-3a6311d6f610 (“Credit - Name” / “Brooks Solveig”) is on the Top Silkscreen layer.

Schematic is Auto-placed

The schematic has been arranged automatically. Flux can redraw it to be more readable, with components placed in related groups and wires drawn more legibly. This takes about 10-60 mins.

Firmware Starter
Toolchain
This starter targets PlatformIO + Arduino on ESP32-S3 and uses native USB Serial/JTAG. Wi-Fi and Bluetooth LE remain disabled because neither radio stack is initialized.
platformio.ini

Ini


[env:fj-compass]
platform = espressif32
board = esp32-s3-devkitc-1
framework = arduino
monitor_speed = 115200
upload_protocol = esp-builtin
build_flags =
  -DARDUINO_USB_MODE=1
  -DARDUINO_USB_CDC_ON_BOOT=1
Final GPIO Mapping

Table


GPIONetFunction
4ENC_AEncoder A
5ENC_BEncoder B
6ENC_SWEncoder push switch
8I2C_SCLLIS2MDL SCL
9I2C_SDALIS2MDL SDA
10DISP_DATAAiP1629A DIO through U7
11DISP_CLKAiP1629A CLK through U7
12DISP_STBAiP1629A STB through U7
19USB_DMNative USB D−
20USB_DPNative USB D+
0ESP_BOOTBoot strap button
ENESP_ENHardware reset/enable
src/main.cpp

Cpp


#include <Arduino.h>
#include <Wire.h>
#include <Preferences.h>
#include <math.h>

namespace Pins {
constexpr uint8_t ENC_A = 4;
constexpr uint8_t ENC_B = 5;
constexpr uint8_t ENC_SW = 6;
constexpr uint8_t I2C_SCL = 8;
constexpr uint8_t I2C_SDA = 9;
constexpr uint8_t DISP_DATA = 10;
constexpr uint8_t DISP_CLK = 11;
constexpr uint8_t DISP_STB = 12;
}

namespace Lis2mdl {
constexpr uint8_t ADDRESS = 0x1E;
constexpr uint8_t WHO_AM_I = 0x4F;
constexpr uint8_t CFG_REG_A = 0x60;
constexpr uint8_t OUTX_L = 0x68;
constexpr uint8_t WHO_AM_I_VALUE = 0x40;
}

namespace Aip1629 {
constexpr uint8_t CMD_DATA_AUTO_WRITE = 0x40;
constexpr uint8_t CMD_ADDRESS_0 = 0xC0;
constexpr uint8_t CMD_DISPLAY_OFF = 0x80;
constexpr uint8_t CMD_DISPLAY_ON_BASE = 0x88;
constexpr uint8_t MAX_BRIGHTNESS_CODE = 5; // 0x8D = 12/16
}

Preferences preferences;
uint8_t displayRam[16] = {};
uint8_t brightnessCode = 2;
bool displayEnabled = true;
bool magnetometerPresent = false;

float xOffset = 0.0f;
float yOffset = 0.0f;
float xScale = 1.0f;
float yScale = 1.0f;

bool lastEncoderA = HIGH;
bool lastButton = HIGH;
uint32_t buttonPressedAt = 0;
uint32_t lastHeadingUpdate = 0;

constexpr uint16_t SEG_A1 = 1u << 0;
constexpr uint16_t SEG_A2 = 1u << 1;
constexpr uint16_t SEG_B  = 1u << 2;
constexpr uint16_t SEG_C  = 1u << 3;
constexpr uint16_t SEG_D1 = 1u << 4;
constexpr uint16_t SEG_D2 = 1u << 5;
constexpr uint16_t SEG_E  = 1u << 6;
constexpr uint16_t SEG_F  = 1u << 7;
constexpr uint16_t SEG_G1 = 1u << 8;
constexpr uint16_t SEG_G2 = 1u << 9;
constexpr uint16_t SEG_H  = 1u << 10;
constexpr uint16_t SEG_I  = 1u << 11;
constexpr uint16_t SEG_J  = 1u << 12;
constexpr uint16_t SEG_K  = 1u << 13;
constexpr uint16_t SEG_L  = 1u << 14;
constexpr uint16_t SEG_M  = 1u << 15;

uint16_t glyph(char c) {
  switch (c) {
    case 'A': return SEG_A1 | SEG_A2 | SEG_B | SEG_C | SEG_E | SEG_F | SEG_G1 | SEG_G2;
    case 'C': return SEG_A1 | SEG_A2 | SEG_D1 | SEG_D2 | SEG_E | SEG_F;
    case 'E': return SEG_A1 | SEG_A2 | SEG_D1 | SEG_D2 | SEG_E | SEG_F | SEG_G1 | SEG_G2;
    case 'L': return SEG_D1 | SEG_D2 | SEG_E | SEG_F;
    case 'N': return SEG_B | SEG_E | SEG_F | SEG_C | SEG_H | SEG_K;
    case 'S': return SEG_A1 | SEG_A2 | SEG_C | SEG_D1 | SEG_D2 | SEG_F | SEG_G1 | SEG_G2;
    case 'W': return SEG_B | SEG_C | SEG_E | SEG_F | SEG_J | SEG_M;
    default: return 0;
  }
}

void aipWriteByte(uint8_t value) {
  for (uint8_t bit = 0; bit < 8; ++bit) {
    digitalWrite(Pins::DISP_CLK, LOW);
    digitalWrite(Pins::DISP_DATA, (value >> bit) & 0x01u);
    delayMicroseconds(1);
    digitalWrite(Pins::DISP_CLK, HIGH);
    delayMicroseconds(1);
  }
  digitalWrite(Pins::DISP_CLK, LOW);
}

void aipCommand(uint8_t command) {
  digitalWrite(Pins::DISP_STB, LOW);
  delayMicroseconds(1);
  aipWriteByte(command);
  digitalWrite(Pins::DISP_STB, HIGH);
  delayMicroseconds(1);
}

void aipFlushRam() {
  aipCommand(Aip1629::CMD_DATA_AUTO_WRITE);
  digitalWrite(Pins::DISP_STB, LOW);
  delayMicroseconds(1);
  aipWriteByte(Aip1629::CMD_ADDRESS_0);
  for (uint8_t value : displayRam) aipWriteByte(value);
  digitalWrite(Pins::DISP_STB, HIGH);
  delayMicroseconds(1);
}

void aipSetEnabled(bool enabled) {
  displayEnabled = enabled;
  if (!enabled) {
    aipCommand(Aip1629::CMD_DISPLAY_OFF);
    return;
  }
  if (brightnessCode > Aip1629::MAX_BRIGHTNESS_CODE) {
    brightnessCode = Aip1629::MAX_BRIGHTNESS_CODE;
  }
  aipCommand(Aip1629::CMD_DISPLAY_ON_BASE | brightnessCode);
}

void showText(const char text[4]) {
  memset(displayRam, 0, sizeof(displayRam));
  for (uint8_t grid = 0; grid < 3; ++grid) {
    const uint16_t mask = glyph(text[grid]);
    displayRam[grid * 2] = static_cast<uint8_t>(mask & 0xFFu);
    displayRam[grid * 2 + 1] = static_cast<uint8_t>(mask >> 8);
  }
  aipFlushRam();
}

bool i2cWriteRegister(uint8_t reg, uint8_t value) {
  Wire.beginTransmission(Lis2mdl::ADDRESS);
  Wire.write(reg);
  Wire.write(value);
  return Wire.endTransmission() == 0;
}

bool i2cReadRegisters(uint8_t reg, uint8_t* data, size_t length) {
  Wire.beginTransmission(Lis2mdl::ADDRESS);
  Wire.write(reg);
  if (Wire.endTransmission(false) != 0) return false;
  if (Wire.requestFrom(static_cast<int>(Lis2mdl::ADDRESS), static_cast<int>(length), true) != length) return false;
  for (size_t i = 0; i < length; ++i) data[i] = Wire.read();
  return true;
}

bool lis2mdlBegin() {
  uint8_t id = 0;
  if (!i2cReadRegisters(Lis2mdl::WHO_AM_I, &id, 1)) return false;
  if (id != Lis2mdl::WHO_AM_I_VALUE) return false;
  return i2cWriteRegister(Lis2mdl::CFG_REG_A, 0x80); // temp compensation, 10 Hz, high-resolution continuous mode
}

bool lis2mdlRead(int16_t& x, int16_t& y, int16_t& z) {
  uint8_t raw[6];
  if (!i2cReadRegisters(Lis2mdl::OUTX_L, raw, sizeof(raw))) return false;
  x = static_cast<int16_t>((static_cast<uint16_t>(raw[1]) << 8) | raw[0]);
  y = static_cast<int16_t>((static_cast<uint16_t>(raw[3]) << 8) | raw[2]);
  z = static_cast<int16_t>((static_cast<uint16_t>(raw[5]) << 8) | raw[4]);
  return true;
}

void loadCalibration() {
  preferences.begin("compass", true);
  xOffset = preferences.getFloat("xOff", 0.0f);
  yOffset = preferences.getFloat("yOff", 0.0f);
  xScale = preferences.getFloat("xScale", 1.0f);
  yScale = preferences.getFloat("yScale", 1.0f);
  brightnessCode = preferences.getUChar("bright", 2);
  preferences.end();
  if (brightnessCode > Aip1629::MAX_BRIGHTNESS_CODE) {
    brightnessCode = Aip1629::MAX_BRIGHTNESS_CODE;
  }
}

void saveCalibration() {
  preferences.begin("compass", false);
  preferences.putFloat("xOff", xOffset);
  preferences.putFloat("yOff", yOffset);
  preferences.putFloat("xScale", xScale);
  preferences.putFloat("yScale", yScale);
  preferences.putUChar("bright", brightnessCode);
  preferences.end();
}

void calibrateMagnetometer() {
  if (!magnetometerPresent) return;
  showText("CAL");
  Serial.println("Calibration: rotate the assembly through every heading for 20 seconds.");

  int32_t minX = INT32_MAX, maxX = INT32_MIN;
  int32_t minY = INT32_MAX, maxY = INT32_MIN;
  const uint32_t start = millis();
  while (millis() - start < 20000) {
    int16_t x, y, z;
    if (lis2mdlRead(x, y, z)) {
      if (x < minX) minX = x;
      if (x > maxX) maxX = x;
      if (y < minY) minY = y;
      if (y > maxY) maxY = y;
    }
    delay(20);
  }

  const float spanX = static_cast<float>(maxX - minX) * 0.5f;
  const float spanY = static_cast<float>(maxY - minY) * 0.5f;
  if (spanX > 10.0f && spanY > 10.0f) {
    xOffset = static_cast<float>(maxX + minX) * 0.5f;
    yOffset = static_cast<float>(maxY + minY) * 0.5f;
    const float averageSpan = (spanX + spanY) * 0.5f;
    xScale = averageSpan / spanX;
    yScale = averageSpan / spanY;
    saveCalibration();
    Serial.println("Calibration saved.");
  } else {
    Serial.println("Calibration rejected: insufficient rotation.");
  }
}

const char* compassPoint(float degrees) {
  static const char labels[16][4] = {
    "N  ", "NNE", "NE ", "ENE", "E  ", "ESE", "SE ", "SSE",
    "S  ", "SSW", "SW ", "WSW", "W  ", "WNW", "NW ", "NNW"
  };
  int index = static_cast<int>(floorf((degrees + 11.25f) / 22.5f)) & 0x0F;
  return labels[index];
}

void updateHeading() {
  int16_t rawX, rawY, rawZ;
  if (!magnetometerPresent || !lis2mdlRead(rawX, rawY, rawZ)) {
    showText("---");
    return;
  }

  const float x = (static_cast<float>(rawX) - xOffset) * xScale;
  const float y = (static_cast<float>(rawY) - yOffset) * yScale;
  float heading = atan2f(y, x) * 180.0f / PI;
  if (heading < 0.0f) heading += 360.0f;

  if (displayEnabled) showText(compassPoint(heading));
  Serial.printf("heading=%.1f raw=(%d,%d,%d)\n", heading, rawX, rawY, rawZ);
}

void pollEncoder() {
  const bool a = digitalRead(Pins::ENC_A);
  const bool b = digitalRead(Pins::ENC_B);
  if (a != lastEncoderA && a == LOW) {
    if (b != a && brightnessCode < Aip1629::MAX_BRIGHTNESS_CODE) ++brightnessCode;
    if (b == a && brightnessCode > 0) --brightnessCode;
    aipSetEnabled(displayEnabled);
    saveCalibration();
  }
  lastEncoderA = a;

  const bool button = digitalRead(Pins::ENC_SW);
  if (lastButton == HIGH && button == LOW) buttonPressedAt = millis();
  if (lastButton == LOW && button == HIGH) {
    const uint32_t held = millis() - buttonPressedAt;
    if (held >= 1500) {
      calibrateMagnetometer();
    } else {
      aipSetEnabled(!displayEnabled);
      if (displayEnabled) updateHeading();
    }
  }
  lastButton = button;
}

void setup() {
  pinMode(Pins::DISP_DATA, OUTPUT);
  pinMode(Pins::DISP_CLK, OUTPUT);
  pinMode(Pins::DISP_STB, OUTPUT);
  digitalWrite(Pins::DISP_DATA, LOW);
  digitalWrite(Pins::DISP_CLK, LOW);
  digitalWrite(Pins::DISP_STB, HIGH);

  pinMode(Pins::ENC_A, INPUT);
  pinMode(Pins::ENC_B, INPUT);
  pinMode(Pins::ENC_SW, INPUT);
  lastEncoderA = digitalRead(Pins::ENC_A);
  lastButton = digitalRead(Pins::ENC_SW);

  Serial.begin(115200);
  const uint32_t serialWaitStart = millis();
  while (!Serial && millis() - serialWaitStart < 1500) delay(10);
  Serial.println("FJ Cruiser Dashboard Compass");

  loadCalibration();
  aipCommand(Aip1629::CMD_DISPLAY_OFF);
  memset(displayRam, 0, sizeof(displayRam));
  aipFlushRam();
  aipSetEnabled(true);

  Wire.begin(Pins::I2C_SDA, Pins::I2C_SCL, 400000);
  magnetometerPresent = lis2mdlBegin();
  Serial.printf("LIS2MDL: %s\n", magnetometerPresent ? "ok" : "not found");
  showText(magnetometerPresent ? "N  " : "---");
}

void loop() {
  pollEncoder();
  if (millis() - lastHeadingUpdate >= 100) {
    lastHeadingUpdate = millis();
    if (displayEnabled) updateHeading();
  }
  delay(1);
}
Startup Sequence
  1. Configure display pins and keep STB high.
  2. Start native USB serial.
  3. Load calibration/brightness from Preferences.
  4. Turn the AiP1629A display off, clear all 16 RAM bytes, then enable at a capped brightness.
  5. Start I2C on GPIO8/9 and verify LIS2MDL WHO_AM_I at 0x4F equals 0x40.
  6. Configure LIS2MDL CFG_REG_A = 0x80 for the datasheet startup mode: temperature compensation, 10 Hz, high-resolution continuous operation.
  7. Enter the heading/encoder loop.
Build and Upload

Bash


pio run
pio run -t upload
pio device monitor
If upload does not start, hold BOOT, tap RESET, release RESET, then release BOOT and retry.
Verification Notes
  • The code is written as a complete Arduino translation unit and uses only Arduino-core libraries (Wire, Preferences).
  • The AiP1629A command constants are included so the starter is runnable; immutable citation extraction for that vendor PDF was unavailable, so confirm command behavior during the mandatory RAM-clear, walking-grid, and walking-segment tests.
  • The glyph masks are intentionally easy to edit after the electrical walking-segment test.
  • Add magnetic declination and tilt compensation only after the flat two-axis bring-up passes.
  • Toolchain

  • `platformio.ini`

  • Final GPIO Mapping

  • `src/main.cpp`

  • Startup Sequence

  • Build and Upload

  • Verification Notes