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.
platformio.iniIni
[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
Table
| GPIO | Net | Function |
|---|---|---|
| 4 | ENC_A | Encoder A |
| 5 | ENC_B | Encoder B |
| 6 | ENC_SW | Encoder push switch |
| 8 | I2C_SCL | LIS2MDL SCL |
| 9 | I2C_SDA | LIS2MDL SDA |
| 10 | DISP_DATA | AiP1629A DIO through U7 |
| 11 | DISP_CLK | AiP1629A CLK through U7 |
| 12 | DISP_STB | AiP1629A STB through U7 |
| 19 | USB_DM | Native USB D− |
| 20 | USB_DP | Native USB D+ |
| 0 | ESP_BOOT | Boot strap button |
| EN | ESP_EN | Hardware reset/enable |
src/main.cppCpp
#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); }
0x4F equals 0x40.CFG_REG_A = 0x80 for the datasheet startup mode: temperature compensation, 10 Hz, high-resolution continuous operation.Bash
pio run pio run -t upload pio device monitor
Wire, Preferences).Toolchain
`platformio.ini`
Final GPIO Mapping
`src/main.cpp`
Startup Sequence
Build and Upload
Verification Notes