File STM32Board.cpp
File List > Firmware > Libraries > STM32Board > STM32Board.cpp
Go to the documentation of this file
#ifdef ARDUINO_ARCH_STM32
#include "STM32Board.h"
#include <CANProtocol.h> // for CanStatus enum values
// ── Private types ─────────────────────────────────────────────────────────────
// Under STM32BOARD_TEST this enum lives in the header (so currentState() can return it);
// keep the two definitions identical.
#ifndef STM32BOARD_TEST
enum class LedState {
OFF,
BOOTING,
NORMAL,
CONNECTED,
CAN_ERROR,
BUS_OFF,
WARNING,
};
#endif
// ── Private state ─────────────────────────────────────────────────────────────
static LedState _state = LedState::OFF;
static bool _blinkPhase = false;
static uint32_t _ledLastMs = 0;
static bool _debugOn = false;
static HardwareSerial _diag(PA10, PA9); // USART1: RX=PA10, TX=PA9
static CAN_HandleTypeDef _hcan;
// Derived-state inputs — arbitrated by _recompute() into the effective _state.
// Storing inputs (not the final state) lets CONNECTED survive a transient CAN fault and
// re-engage on recovery, and lets WARNING be cleared independently.
static CanStatus _canStatus = CanStatus::STARTING;
static bool _linkActive = false;
static uint32_t _linkLastMs = 0;
static bool _warning = false;
static bool _begun = false;
static bool _clockFault = false;
static constexpr uint32_t LINK_DECAY_MS = 500;
// ── Private helpers ───────────────────────────────────────────────────────────
static uint16_t _blinkPeriodFor(LedState s) {
switch (s) {
case LedState::BOOTING: return 1000;
case LedState::NORMAL: return 1000;
case LedState::CAN_ERROR: return 250;
case LedState::WARNING: return 500;
default: return 0; // solid or off — no timer needed
}
}
static void _applyLed() {
switch (_state) {
case LedState::OFF:
digitalWrite(STM32Board::PIN_LED_RED, LOW);
digitalWrite(STM32Board::PIN_LED_GREEN, LOW);
break;
case LedState::BOOTING:
case LedState::CAN_ERROR:
digitalWrite(STM32Board::PIN_LED_RED, _blinkPhase ? HIGH : LOW);
digitalWrite(STM32Board::PIN_LED_GREEN, LOW);
break;
case LedState::NORMAL:
digitalWrite(STM32Board::PIN_LED_RED, LOW);
digitalWrite(STM32Board::PIN_LED_GREEN, _blinkPhase ? HIGH : LOW);
break;
case LedState::CONNECTED:
digitalWrite(STM32Board::PIN_LED_RED, LOW);
digitalWrite(STM32Board::PIN_LED_GREEN, HIGH);
break;
case LedState::BUS_OFF:
digitalWrite(STM32Board::PIN_LED_RED, HIGH);
digitalWrite(STM32Board::PIN_LED_GREEN, LOW);
break;
case LedState::WARNING:
digitalWrite(STM32Board::PIN_LED_RED, _blinkPhase ? HIGH : LOW);
digitalWrite(STM32Board::PIN_LED_GREEN, _blinkPhase ? LOW : HIGH);
break;
}
}
// Arbitrate the inputs into the effective LED state by fixed precedence (highest first).
// Sole writer of _state. Resets the blink phase and repaints only on an actual transition,
// preserving the invariant that the phase resets only when the state genuinely changes.
//
// not begun → OFF
// clock fault → WARNING (amber alt) ← root HW fault: outranks the CAN
// errors it causes, so a bad clock is
// not misdiagnosed as a bus problem (#245)
// CAN BUS_OFF → BUS_OFF (solid red) } CAN faults outrank an app-layer
// CAN TX_ERROR → CAN_ERROR (fast red) } warning — a dead/erroring bus must
// CAN STARTING → BOOTING (slow red) } not be masked by setWarning().
// warning → WARNING (amber alt)
// NORMAL + linkActive → CONNECTED (solid green) ← only reachable when CAN is NORMAL
// NORMAL → NORMAL (slow green)
static void _recompute() {
LedState next;
if (!_begun) next = LedState::OFF;
else if (_clockFault) next = LedState::WARNING;
else if (_canStatus == CanStatus::BUS_OFF) next = LedState::BUS_OFF;
else if (_canStatus == CanStatus::TX_ERROR) next = LedState::CAN_ERROR;
else if (_canStatus == CanStatus::STARTING) next = LedState::BOOTING;
else if (_warning) next = LedState::WARNING;
else if (_linkActive) next = LedState::CONNECTED;
else next = LedState::NORMAL;
if (next != _state) {
_state = next;
_blinkPhase = false;
_ledLastMs = millis();
_applyLed();
}
}
// HAL weak-symbol override — defines CAN GPIO once for all OpenSkyhawk STM32 boards.
extern "C" void HAL_CAN_MspInit(CAN_HandleTypeDef* hcan_p) {
if (hcan_p->Instance != CAN1) return;
__HAL_RCC_CAN1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitTypeDef gpio = {};
gpio.Pin = GPIO_PIN_12;
gpio.Mode = GPIO_MODE_AF_PP;
gpio.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &gpio);
gpio.Pin = GPIO_PIN_11;
gpio.Mode = GPIO_MODE_INPUT;
gpio.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOA, &gpio);
}
// PCLK2 / 6 → ADCCLK, called on BOTH exit paths of SystemClock_Config below.
//
// ADCPRE resets to /2, which off the 72 MHz PCLK2 is 36 MHz — 2.6x the F103's 14 MHz maximum.
// /4 = 18 MHz is still over, so /6 = 12 MHz is the fastest legal divider. (On the fault path
// PCLK2 is 8 MHz, so /6 gives 1.33 MHz — above the 0.6 MHz minimum.)
//
// Nobody else was setting it: on F1 the Arduino core defers the prescaler to SystemClock_Config
// (analog.cpp guards its __HAL_RCC_ADC_CONFIG with !defined(STM32F1xx)) and variant_generic.cpp
// never set it, so this strong override is the only owner. Running here means it lands before
// setup(), so the core's own analogRead() ADC init inherits it. Nothing but the ADCs consumes
// ADCCLK, so the #245 clock work below is untouched.
//
// This mirrors variant_PILL_F103Cx.cpp, which does the same via HAL; we omit its
// RCC_PERIPHCLK_USB branch deliberately — every OpenSkyhawk STM32 env builds -DUSB_NONE.
//
// NOTE: nothing observable was fixed by this. Axis noise, PA2 in production, and die-temperature
// telemetry were each predicted to improve and each measured unchanged on PanelGroup Rev 1. The
// justification is that the part was running out of spec, plus high-impedance sources in general
// (cockpit pots are ~2.5 kΩ at mid-travel) — which has NOT been measured on an actual pot. (#263)
static void _configAdcClock(void) {
RCC_PeriphCLKInitTypeDef adc = {};
adc.PeriphClockSelection = RCC_PERIPHCLK_ADC;
adc.AdcClockSelection = RCC_ADCPCLK2_DIV6;
// On F1 the ADC branch is assert_param + a CFGR field write — it cannot fail. The cast
// says ignoring the status is deliberate, not an oversight.
(void)HAL_RCCEx_PeriphCLKConfig(&adc);
}
// Strong override of the core's __weak SystemClock_Config. The genericSTM32F103C8/CB
// variant defaults SYSCLK to HSI-PLL 64 MHz → APB1 32 MHz → CAN 444 kbps (spec 500).
// -DHSE_VALUE only tells HAL the crystal frequency; nothing selects HSE. Every
// OpenSkyhawk STM32 node links STM32Board, so selecting HSE here fixes the fleet in
// one place: HSE 8 MHz × 9 = 72 MHz → APB1 36 MHz → CAN 500 kbps, matching begin()'s
// bit timing. If HSE fails to start (dead crystal / cold joint → HSERDY timeout) or the
// configured tree is self-inconsistent, _clockFault latches and begin() raises a visible
// WARNING rather than silently running the wrong rate. NOTE: this does NOT detect a
// wrong-VALUE crystal — HAL_RCC_Get*Freq() derive from the compile-time HSE_VALUE, not a
// measurement, so a 12 MHz part would compute (and pass) as 72 MHz while really running
// 108 MHz. Correct crystal value is a BOM/build guarantee, not runtime-detectable here.
// See issue #245. -DFORCE_CLOCK_FALLBACK exercises the fault path without a dead crystal.
// Displacing the variant also made this the owner of the ADC prescaler — see _configAdcClock
// above, called on both exit paths (#263).
extern "C" void SystemClock_Config(void) {
#ifndef FORCE_CLOCK_FALLBACK
RCC_OscInitTypeDef osc = {};
osc.OscillatorType = RCC_OSCILLATORTYPE_HSE;
osc.HSEState = RCC_HSE_ON;
osc.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
osc.PLL.PLLState = RCC_PLL_ON;
osc.PLL.PLLSource = RCC_PLLSOURCE_HSE;
osc.PLL.PLLMUL = RCC_PLL_MUL9; // 8 MHz × 9 = 72 MHz
if (HAL_RCC_OscConfig(&osc) == HAL_OK) {
RCC_ClkInitTypeDef clk = {};
clk.ClockType = RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_HCLK |
RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
clk.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
clk.AHBCLKDivider = RCC_SYSCLK_DIV1; // 72 MHz
clk.APB1CLKDivider = RCC_HCLK_DIV2; // 36 MHz — CAN, USART2/3
clk.APB2CLKDivider = RCC_HCLK_DIV1; // 72 MHz — USART1 diag
if (HAL_RCC_ClockConfig(&clk, FLASH_LATENCY_2) == HAL_OK) {
// Verify the configured tree is self-consistent (guards a bad edit to the
// dividers/mult above). These freqs are computed from the RCC config × the
// compile-time HSE_VALUE, NOT measured — so this cannot catch a wrong-value
// crystal (see header note); it only confirms the config we just applied.
if (HAL_RCC_GetSysClockFreq() == 72000000UL &&
HAL_RCC_GetPCLK1Freq() == 36000000UL) {
_configAdcClock(); // PCLK2 72 MHz → ADCCLK 12 MHz (#263)
return; // 72 MHz locked and verified
}
}
}
#endif
// HSE failed / forced / wrong frequency → do NOT silently run at the wrong CAN rate.
// Latch the fault (begin() → WARNING) and limp on internal RC just enough to signal.
_clockFault = true;
RCC_OscInitTypeDef h = {};
h.OscillatorType = RCC_OSCILLATORTYPE_HSI;
h.HSIState = RCC_HSI_ON;
h.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT;
h.PLL.PLLState = RCC_PLL_NONE;
HAL_RCC_OscConfig(&h);
RCC_ClkInitTypeDef c = {};
c.ClockType = RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_HCLK |
RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
c.SYSCLKSource = RCC_SYSCLKSOURCE_HSI; // 8 MHz — visibly wrong, safe boot
c.AHBCLKDivider = RCC_SYSCLK_DIV1;
c.APB1CLKDivider = RCC_HCLK_DIV1;
c.APB2CLKDivider = RCC_HCLK_DIV1;
HAL_RCC_ClockConfig(&c, FLASH_LATENCY_0);
_configAdcClock(); // PCLK2 8 MHz → ADCCLK 1.33 MHz (#263)
}
// ── Public API ────────────────────────────────────────────────────────────────
namespace STM32Board {
void begin() {
pinMode(PIN_LED_RED, OUTPUT);
pinMode(PIN_LED_GREEN, OUTPUT);
_begun = true;
_canStatus = CanStatus::STARTING;
_recompute(); // → BOOTING, or WARNING if _clockFault latched in SystemClock_Config (#245)
_diag.begin(115200);
analogReadResolution(16); // STM32duino defaults to 10-bit; 16-bit fills uint16_t directly for PinRef
// Report the resulting clock + derived CAN bitrate so a wrong-speed board is
// self-evident on the diag console, not just via the WARNING LED (issue #245).
// ADCCLK is read back the same way, so an unset prescaler is visible without a
// bench probe (#263). Nothing parses this line — it is diagnostic, not a contract.
if (_debugOn) {
uint32_t pclk1 = HAL_RCC_GetPCLK1Freq();
_diag.print(_clockFault ? F("CLOCK FAULT: ") : F("CLOCK OK: "));
_diag.print(F("SYSCLK=")); _diag.print(HAL_RCC_GetSysClockFreq() / 1000000UL); _diag.print(F("MHz "));
_diag.print(F("PCLK1=")); _diag.print(pclk1 / 1000000UL); _diag.print(F("MHz "));
_diag.print(F("CAN=")); _diag.print(pclk1 / (4UL * 18UL)); _diag.print(F("bps "));
_diag.print(F("ADC=")); _diag.print(HAL_RCCEx_GetPeriphCLKFreq(RCC_PERIPHCLK_ADC) / 1000UL);
_diag.println(F("kHz"));
}
// 500 kbps on APB1 @ 36 MHz: prescaler=4, BS1=13TQ, BS2=4TQ → 18TQ total.
// SJW=4TQ required for Blue Pill clone crystal tolerance (validated Experiment B).
_hcan.Instance = CAN1;
_hcan.Init.Prescaler = 4;
_hcan.Init.Mode = CAN_MODE_NORMAL;
_hcan.Init.SyncJumpWidth = CAN_SJW_4TQ;
_hcan.Init.TimeSeg1 = CAN_BS1_13TQ;
_hcan.Init.TimeSeg2 = CAN_BS2_4TQ;
_hcan.Init.TimeTriggeredMode = DISABLE;
_hcan.Init.AutoBusOff = ENABLE; // hardware recovery ~3 ms, no firmware action needed
_hcan.Init.AutoWakeUp = DISABLE;
_hcan.Init.AutoRetransmission = DISABLE; // prevents runaway bus-off on unACKed frames
_hcan.Init.ReceiveFifoLocked = DISABLE;
_hcan.Init.TransmitFifoPriority = DISABLE;
HAL_CAN_Init(&_hcan);
}
void setDebug(bool on) { _debugOn = on; }
bool isDebug() { return _debugOn; }
bool clockFault() { return _clockFault; }
void tick() {
uint32_t now = millis();
// Decay the data-flowing link → drop CONNECTED back to NORMAL after a quiet gap.
if (_linkActive && (now - _linkLastMs) >= LINK_DECAY_MS) {
_linkActive = false;
_recompute();
}
uint16_t period = _blinkPeriodFor(_state);
if (period > 0 && (now - _ledLastMs) >= (uint32_t)(period / 2)) {
_blinkPhase = !_blinkPhase;
_ledLastMs = now;
_applyLed();
}
}
void onCanStatus(CanStatus status) {
_canStatus = status;
_recompute();
}
void setWarning(bool on) {
_warning = on;
_recompute();
}
void setLinkActive(bool active) {
if (active) {
_linkActive = true;
_linkLastMs = millis();
} else {
_linkActive = false;
}
_recompute();
}
void log(const char* msg) {
if (_debugOn) _diag.println(msg);
}
HardwareSerial& diagSerial() { return _diag; }
CAN_HandleTypeDef* canHandle() { return &_hcan; }
// ── Internal die-temperature telemetry ──────────────────────────────────────────
// Free node health from the F103's built-in sensor (ADC ch16) + Vrefint (ch17); no
// external parts. STM32duino's analogRead(ATEMP)/analogRead(AVREF) enable TSVREFE and
// pick the internal channels. Conversion uses F103 datasheet typicals (uncalibrated —
// no factory trim), referencing Vsense to the Vdd derived from Vrefint.
#if defined(ATEMP) && defined(AVREF)
static constexpr uint32_t ADC_FULL = 1UL << 16; // full-scale count at analogReadResolution(16)
static constexpr uint32_t VREFINT_MV = 1200; // F103 Vrefint typical (no VREFINT_CAL on F103)
static constexpr int32_t V25_MV = 1430; // temp-sensor output at 25 °C, typical
static constexpr int32_t AVG_SLOPE_10 = 43; // Avg_Slope 4.3 mV/°C, ×10 for integer math
#endif
uint16_t readVddMv() {
#if defined(ATEMP) && defined(AVREF)
uint32_t vref = analogRead(AVREF);
if (vref == 0) return 0;
return (uint16_t)(VREFINT_MV * ADC_FULL / vref);
#else
return 0;
#endif
}
int8_t readDieTempC() {
#if defined(ATEMP) && defined(AVREF)
uint16_t vdd = readVddMv();
if (vdd == 0) return INT8_MIN; // internal reference unreadable → unavailable
uint32_t traw = analogRead(ATEMP);
int32_t vsense_mv = (int32_t)((uint64_t)traw * vdd / ADC_FULL);
int32_t tempC = ((V25_MV - vsense_mv) * 10) / AVG_SLOPE_10 + 25;
if (tempC <= INT8_MIN) tempC = INT8_MIN + 1; // reserve INT8_MIN as the "unavailable" sentinel
if (tempC > INT8_MAX) tempC = INT8_MAX;
return (int8_t)tempC;
#else
return INT8_MIN;
#endif
}
void logNodeFaultEdge(const char* tag, NodeFaultCode fault, const char* detail) {
static NodeFaultCode _prevFault = NodeFaultCode::NONE;
if (fault != _prevFault && _debugOn) {
auto& d = diagSerial();
if (fault != NodeFaultCode::NONE) {
d.print('['); d.print(tag); d.print(F("] degraded: ")); d.print(detail);
d.print(F(" (fault ")); d.print((int)(uint8_t)fault); d.println(')');
} else {
d.print('['); d.print(tag); d.println(F("] recovered"));
}
}
_prevFault = fault;
}
#ifdef STM32BOARD_TEST
LedState currentState() { return _state; }
#endif
} // namespace STM32Board
#endif // ARDUINO_ARCH_STM32