Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Version: w26.1.1

Latest updates made on 3/20/2026

AER Embedded Systems Documentation

Welcome to the software and systems doc page for Anteater Electric Racing. This site is the shared starting point for onboarding, architecture, and implementation details across the team.

Start Here:

Subteam Split

Data Acquisition (DAC)

The DAC side focuses on telemetry transport, storage, visualization, and simulation tooling.

  • CAN ISO-TP telemetry ingestion and decoding (CAN docs)
  • MQTT broker startup and configuration (MQTT docs)
  • Telemetry fan-out to MQTT and TDengine (Send docs)
  • Dashboard + Raspberry Pi telemetry services
  • Simulator efforts (Godot, inverter simulation, RL) (Simulator docs)

Firmware

Firmware owns embedded control and safety-critical behavior on vehicle modules.

  • CCM control firmware (sensor I/O, control logic, telemetry) (CCM Overview)
  • PCC firmware for precharge sequencing and HV/LV safety interactions (PCC Overview)
  • FreeRTOS task scheduling and CAN message flow (Tasks & Scheduling)
  • Vehicle state machine, fault handling, power limiting, and wheel-speed sensing

Current Focus Areas

  • Transitioning codebase to comp car with new Motor + Inverter
  • CAN interrupt robustness and queue overflow validation
  • Shock sensor integration and calibration for suspension telemetry
  • Launch control tuning and slip-ratio based torque shaping
  • Power limit behavior around rule thresholds and low-SOC derating
  • Continued simulator expansion and PCC subsystem documentation

Contributing to Docs

This site is built with mdBook and Mermaid support.

  • Add or update markdown pages under src/
  • Keep navigation in sync through src/SUMMARY.md
  • Run locally with mdbook serve

If you are unsure where new content belongs, start in the relevant onboarding page and link deeper implementation details from there.

Author: Alistair Keiller

Onboarding

Relevant coding background to understand DAC

Git: Video || Article Rust: Book && Rustlings Cargo: Guide (sections 1-2, optionally 3) Iced.rs: Short Guide Tokio.rs: Tutorial

Relevant hardware background to understand DAC

Raspberry Pi 5: Fun Video Teensy 4.1: Fun Video CAN protocol: Video

Codebase overview

Here is our codebase: https://github.com/Anteater-Electric-Racing/embedded

fsae-dashboard

dashboard

This dashboard displays current critical information to the driver.

fsae-raspi

Runs on the Raspberry Pi and handles all telemetry ingestion and storage. It polls ISO-TP CAN data from can0, decodes it into typed telemetry, and fans it out to an embedded rumqttd MQTT broker and a TDengine time-series database.

See the CAN docs, MQTT docs, and Send docs for details.

Author: Mikayla Lee

AER Telemetry over CAN ISO-TP

Overview

This system receives motor-controller telemetry over CAN ISO-TP, decodes it into strongly typed data, and forwards it for downstream processing. It also includes vCAN-based test utilities for CI.


Function: read_can_hardware

Purpose: Continuously reads and decodes telemetry packets from can0

Workflow:

  • Opens an ISO-TP socket on can0 (source ID 0x666, destination ID 0x777)
  • Retries socket creation on failure with a 1-second backoff
  • Reads incoming packets asynchronously, validates packet length, and parses raw bytes into TelemetryData using Deku
  • Captures a timestamp with now_ms() and forwards via send_message(data, ts)

Error Handling:

  • Logs socket creation failures and malformed packets
  • Logs packets with trailing bytes after decode
  • Re-enters socket setup if packet reads stop or fail

Function: read_can

Dispatches to hardware ISO-TP reception (can0) or synthetic mode when the synthetic feature flag is enabled.


Function: read_can_synthetic

Publishes default TelemetryData at a ~1 ms interval for testing and CI. Logs throughput approximately once per second.


Struct: TelemetryData

Represents a fully decoded MCU telemetry packet received over CAN ISO-TP. Fields are decoded little-endian from a fixed-layout binary frame.

FieldTypeNotes
apps_travelf32Normalized accelerator pedal travel
bse_frontf32Front brake sensor
bse_rearf32Rear brake sensor
imd_resistancef32Isolation monitoring measured resistance
imd_statusu32Raw IMD status bits — interpret against MCU spec
pack_voltagef32HV pack voltage
pack_currentf32HV pack current
socf32Battery state of charge
discharge_limitf32Current discharge limit
charge_limitf32Current charge limit
low_cell_voltf32Minimum cell voltage
high_cell_voltf32Maximum cell voltage
avg_cell_voltf32Average cell voltage
motor_speedf32Motor rotational speed
motor_torquef32Instantaneous output torque
max_motor_torquef32Configured torque limit
motor_directionMotorRotateDirection
motor_stateMotorState
mcu_main_stateMCUMainState
mcu_work_modeMCUWorkMode
mcu_voltagef32MCU DC bus voltage
mcu_phase_currentf32MCU phase current
mcu_currentf32MCU current draw
motor_tempi32Motor temperature
mcu_tempi32MCU temperature
mcu_warning_levelMCUWarningLevelAggregated MCU warning severity
shocktravel14f32Shock travel channels 1–4
dc_main_wire_over_volt_faultbool
motor_phase_curr_faultbool
mcu_over_hot_faultbool
resolver_faultbool
phase_curr_sensor_faultbool
motor_over_spd_faultbool
drv_motor_over_hot_faultbool
dc_main_wire_over_curr_faultbool
drv_motor_over_cool_faultbool
dc_low_volt_warningbool
mcu_12v_low_volt_warningbool
motor_stall_faultbool
motor_open_phase_faultbool
over_currentbool (1 bit)System-level protection flag
under_voltagebool (1 bit)Supply below valid range
over_temperaturebool (1 bit)Temperature exceeded safe limit
appsbool (1 bit)APPS fault flag
bsebool (1 bit)BSE fault flag
bppsbool (1 bit)BPPS fault flag
apps_brake_plausbool (1 bit)Accelerator/brake plausibility violation
low_battery_voltagebool (1 bit)Battery low warning; followed by 24-bit pad

Author: Lawrence Chan

MQTT Broker

Overview

mqttd() loads the embedded rumqttd.toml config, builds a rumqttd::Broker, and starts it.


Function: mqttd

Workflow:

  1. Reads ../rumqttd.toml via include_str!.
  2. Builds config::Config from TOML.
  3. Deserializes into broker config.
  4. Creates Broker::new(config) and calls broker.start().

Error Handling:

  • Logs and returns if config load fails.
  • Logs and returns if config deserialization fails.
  • Logs if broker startup fails.

Author: Alistair Keiller

Telemetry Fan-Out (MQTT + TDengine)

Overview

send_message() serializes a telemetry reading once, attaches a ts field, then fans out to both MQTT and TDengine.


Function: send_message

Workflow:

  1. Serialize message to JSON; insert ts with the provided timestamp.
  2. Publish JSON to MQTT topic T::topic() at QoS AtMostOnce.
  3. Convert to TDengine schemaless line protocol and enqueue for write.

Backpressure / error behavior:

  • Serialization failure → log, drop
  • MQTT queue full → warn, drop; publish error → log
  • Line protocol conversion failure → log, drop
  • TDengine queue full → warn, drop; channel error → log

Runtime Singletons

get_mqtt_client — lazily creates an AsyncClient (OnceCell), spawns the MQTT event loop. On poll error: logs and retries after 1 second.

get_tdengine_sender — lazily creates a channel sender (OnceCell), ensures the fsae database exists, and spawns 4 worker tasks sharing one receiver. Workers batch up to 5,000 records per write. After 10 consecutive write failures, drops and recreates the database.


Trait: Reading

Requires Serialize. Implementors define topic() -> &'static str to specify the MQTT/TDengine measurement name.


Function: now_ms

Returns current UNIX time as milliseconds (u64).

Virtual Simulator for Vehicle Testing

Author: -

Godot Game Engine Simulation

Author: Lawrence Chan

Overview

Virtual simulation utilizing Godot Game Engine for testing vehicle characteristics / autonomous driving.


Vehicle Controller Script (Godot – VehicleBody3D)

Overview

This script implements a basic vehicle controller for a Godot VehicleBody3D node.

It handles:

  • Acceleration and engine force
  • Braking
  • Steering control
  • Natural deceleration
  • Speed limiting
  • Debug speed output

The script reads player inputs and updates the physics behavior of the vehicle’s VehicleWheel3D nodes accordingly.


Node Requirements

The script must be attached to a VehicleBody3D node.

The vehicle should contain child nodes of type:

  • VehicleWheel3D with use_as_traction = true (drive wheels)
  • VehicleWheel3D with use_as_steering = true (steering wheels)

Input Controls

KeyAction
WAccelerate
ATurn left
SBrake
DTurn right
CReset steering to center

These must be mapped in Project Settings → Input Map:

  • accelerate
  • decelerate
  • steer_left
  • steer_right
  • steer_center

Configuration Constants

Debug

const DISPLAY_DEBUG_SPEED = true

Prints vehicle speed and steering data to the console.


Engine / Acceleration

ConstantDescription
MAX_POWERMaximum engine force
ACCELERATION_STEPIncrement applied to engine force when accelerating
USE_DYNAMIC_THROTTLEEnables dynamic throttle based on speed
MOTOR_RAMP_DOWN_STEPRate engine force decreases when not accelerating

Braking

ConstantDescription
BRAKING_STEPBrake force increment
BASE_DECELPassive braking when no throttle is applied
MAX_BRAKINGMaximum brake force

Steering

ConstantDescription
STEER_STEPSteering change per frame
MAX_STEERING_ANGLEMaximum wheel steering angle in degrees

Speed Limit

const MAX_VEHICLE_SPEED_MPH = 90.0

Maximum allowed vehicle speed.

Internally the script uses m/s and km/h, converting to mph only for debug display.


Core Variables

VariablePurpose
previous_displayed_speedPrevents duplicate debug prints
steer_normalizedNormalized steering value for debugging

Utility Functions

Degree / Radian Conversion

func deg_to_rad(deg):
    return deg * PI / 180

func rad_to_deg(rad):
    return rad * 180 / PI

Used for steering calculations.


Speed Conversion

func kph_to_mph(kph) -> float:
    return kph / 1.609

Converts kilometers per hour to miles per hour.


Throttle Function

func throttle_function(current_speed) -> float:
    # Connect engine power data here
    return 1000.0

Returns engine force when dynamic throttle is enabled.

This is currently a placeholder and can be replaced with a realistic engine torque curve.


Normalized Steering Angle

func norm_steering_angle(steer: float) -> float:
    return steer * (90/PI)

Converts steering radians into a normalized value for debugging.


Physics Update Loop

Main vehicle logic runs inside:

func _physics_process(delta: float) -> void:

This function performs:

  1. Input processing
  2. Debug speed calculation
  3. Wheel physics updates

Speed Calculation

Vehicle speed is calculated from the body’s velocity:

var velocity_mps = linear_velocity.length()

Conversions:

  • m/s → km/h: mps * 3.6
  • km/h → mph: kph / 1.609

Example debug output:

VEHICLE SPEED | 72 km/h [20 m/s] (45 mph) | STEER: 0.4

Wheel Processing

The script loops through all child nodes:

for object in get_children():

If the node is a VehicleWheel3D, it is processed depending on its role.


Traction Wheels

If:

wheel.use_as_traction

The script applies engine and braking forces.

Acceleration

Engine force increases while:

  • Below MAX_POWER
  • Below MAX_VEHICLE_SPEED

Braking

Brake force increases up to:

MAX_BRAKING

Engine force is set to zero when braking.

Natural Deceleration

When neither accelerating nor braking:

  • Engine force gradually decreases
  • Base braking (BASE_DECEL) is applied

Steering Wheels

If:

wheel.use_as_steering

The script adjusts the wheel steering angle.

Steering changes are converted from degrees to radians because Godot steering uses radians.


Steering Limits

Maximum steering range:

± MAX_STEERING_ANGLE

If exceeded, the value is clamped.


Steering Reset

Pressing C resets steering:

wheel.steering = 0.0

Vehicle Behavior Summary

BehaviorDescription
AccelerationApplies engine force to traction wheels
BrakingApplies brake force and disables engine force
SteeringRotates steering wheels within limits
Natural slowdownEngine force gradually decreases
Speed limitingPrevents vehicle exceeding maximum speed
Debug outputDisplays speed and steering data

Possible Improvements

  • Implement a realistic engine torque curve
  • Add gear ratios and transmission
  • Implement traction control
  • Add drift or slip simulation
  • Display speed using a UI element instead of console output

Example Node Structure

VehicleBody3D
│
├── VehicleWheel3D (FrontLeft)
├── VehicleWheel3D (FrontRight)
├── VehicleWheel3D (RearLeft)
└── VehicleWheel3D (RearRight)

Typical configuration:

WheelTractionSteering
Front LeftNoYes
Front RightNoYes
Rear LeftYesNo
Rear RightYesNo

Summary

This script provides a basic but functional vehicle physics controller using Godot’s built-in vehicle system.

It demonstrates:

  • Player input handling
  • Vehicle physics updates
  • Wheel-based acceleration and steering
  • Debug speed monitoring

The system is designed to be simple, modular, and easily extendable for more advanced driving simulations.

Inverter Simulation

Author: -

Reinforcement Learning

Godot RL Agents Integration for Autonomous Driving

Author: Mikayla Lee

Overview

The Godot RL Agents integration enables reinforcement learning agents to train autonomous driving policies on the AER simulator. This system bridges the Godot physics simulator with RL training libraries, allowing any standard AI RL agents to learn driving behaviors in a virtual environment.

The integration serves as the interface layer between:

  • Godot car physics simulator
  • Stable-Baselines3 PPO training algorithms
  • Future autonomous driving control systems

System Architecture

The RL training pipeline consists of:

  1. GodotEnv Wrapper - Connects RL agents to the Godot simulator
  2. Action Space - Defines steering and throttle controls (2D continuous space)
  3. Observation Space - Captures car telemetry (position, velocity, speed)
  4. PPO Training Model - Implements Proximal Policy Optimization for policy learning
  5. Reset/Step Functions - Handles episode management and action execution

The system uses the Godot RL Agents library to establish direct communication between the Python training environment and the Godot game engine, eliminating the need for manual WebSocket implementation.

Implementation Details

Environment Configuration

class GodotCarEnv(GodotEnv):
    def __init__(self, env_path=None, port=11008, show_window=True, seed=0):
        super().__init__(
            env_path=env_path,
            port=port,
            show_window=show_window,
            seed=seed
        )

The environment wrapper inherits from GodotEnv and manages:

  • Connection to Godot executable on port 11008
  • Visual rendering control for training monitoring

Action Space

Actions are represented as a 2D continuous vector [steering, throttle]:

ControlRangeMapping
Steering-1.0 to 1.0-20° to +20°
Throttle-1.0 to 1.0Full brake to full gas

Observation Space

The agent receives 7D telemetry data per timestep:

  • Position (x, y, z) - 3D coordinates in world space
  • Velocity (vx, vy, vz) - 3D velocity vector
  • Speed - Scalar magnitude of velocity

Training Pipeline

The system uses Proximal Policy Optimization (PPO) with the following arameters:

ParameterValuePurpose
Learning Rate3e-4Gradient descent step size
Steps per Update2048Experience collection before update
Batch Size64Mini-batch size for optimization
Epochs10Training iterations per update
Gamma0.99Discount factor for future rewards

Integration Workflow

The RL training loop operates as follows:

  1. Start:
    reset() resets car to starting position → returns initial observation

  2. Action Execution:
    Agent predicts action based on current observation → step(action) sends controls to Godot

  3. Simulation Update:
    Godot applies physics, updates car state → returns new observation and reward

  4. Learning:
    PPO updates policy based on collected experience → repeat until convergence

This cycle runs for 100,000 timesteps during initial training, with the trained model saved for deployment.

Current Status & Next Steps

Completed:

  • Researched and integrated Godot RL Agents library
  • Implemented GodotEnv wrapper with action/observation spaces
  • Configured PPO training pipeline with Stable-Baselines3
  • Established reset/step function architecture

In Progress:

  • Creating basic test simulator in Godot to validate integration
  • Working with other daq members to fully integrate RL wrapper with car physics simulation

Future Work:

  • Implement reward shaping for racing-specific behaviors (staying on track, lap time optimization)
  • Add termination conditions (crash detection, lap completion)

Dependencies

pip install godot-rl stable-baselines3

References

Onboarding

Author: Karan Thakkar

Relevant coding background to understand firmware

Git: Video || Article

C++ reference and language: cppreference.com PlatformIO: platformio.org — helpful for faster prototyping and device management.

FreeRTOS official site and API reference: freertos.org

Debugging & diagnostics

  • TBD (openCD?)

Relevant hardware background to understand firmware

Teensy 4.1: PJRC Teensy 4.1 - MCU board used in our stack

CAN protocol: Video

Custom PCBs

CCM

Controls all the sensor inputs + outputs and perform motor control calculations.

PCC

Automates the battery precharge sequence to mimize instaneous capacitor charging.

Codebase overview

Here is our codebase: https://github.com/Anteater-Electric-Racing/embedded

fsae-vehicle-fw

Contains all vehicle firmware for CCM (sensor interfacing + motor controls)

fsae-pcc

Contains all firmware for PCC sequence.

Author: Karan Thakkar

System Architecture

---
config:
  theme: 'base'
  themeVariables:
    primaryColor: '#ffffffff'
    primaryTextColor: '#000000ff'
    primaryBorderColor: '#17007cff'
    lineColor: '#000000ff'
    secondaryColor: '#f3f3f3ff'
    tertiaryColor: '#f3f3f3ff'
---
flowchart BT
    %% This is a comment for the entire diagram

     subgraph CAN[" "]

            BMS(Orion BMS 2)
            BC(Battery Charger)
            PCC(PCC - Teensy 4.0)
            INV(OMNI Intervter)
            RPI(Raspberry Pi)
            MOTOR(Motor) === INV
        end

        CCM(CCM - Teensy 4.1)



        %%CAN LOOP1
        BMS <--> |CAN1| BC <--> |CAN1| PCC <---> |CAN1| CCM

        %%CANLOOP2
        INV<-----> |CAN2|CCM<---> |CAN1 + CAN2|RPI



        subgraph Analog[Analog]
            direction LR
                B(Brake Sensors)
                A(Pedal Position Sensors)
                LPOT(Suspension Travel Sensors)
                TR(Thermistors)
        end

        subgraph PWM[PWM]
            direction LR
                W(WheelSpeed Sensors)
                FP(Fans & Pumps)
                SP(Speaker & Amp)
        end
        subgraph MISC[Digital]
            direction LR
                RTM(Ready To Move Button)
                BL(Brake Light)
        end

        Analog --> CCM
        PWM --> CCM
        MISC -->CCM
---
config:
  theme: 'base'
  themeVariables:
    primaryColor: '#ffffffff'
    primaryTextColor: '#000000ff'
    primaryBorderColor: '#17007cff'
    lineColor: '#000000ff'
    secondaryColor: '#f3f3f3ff'
    tertiaryColor: '#f3f3f3ff'
---
flowchart LR
PCC(Precharge Status) --> CCM
A(Analog Inputs) --> CCM
D(Digital Inputs) --> CCM
CCM(Teensy MCU) --> IC(Inverter Commands) --> Motor(Motor Control)
CCM --> Raspi(Raspi) --> Dashboard(Dashboard: Driver + Pit)
CCM --> PWM(PWM Output) --> CC(Cooling Control)

style CCM fill:#00FF00,stroke:#333,stroke-width:2px

Tasks and Threads

“ “

Author: Karan Thakkar

CCM: Central Computer Module

CCM is the main central vehicle board. Its main purpose is to acquire data from sensors across the car, use it in vehicle control commands and send over CAN to our Raspberry Pi.

CCM is powered by (1) Teensy 4.1 MCU which runs an ARM-Cortex-M7. We write our firmware in C++ and flash it to our board. The board communicates with various devices over analog, digital, CAN and i2C.

The full kicad files can be found here.

Current Version: 2.0

Schematic

“ “

  • The top left shows the MCU pinout
  • The top right shows the CAN Tranceiver setup
  • The bottom shows the I/O

PCB Layout

“ “

  • The Right side holds all the analog inputs + 12v Power in
  • The left side holds CAN1/CAN2 + PWM outputs from CCM as well as misc inputs.

Improvements

This board has planned improvments for a version (3.0) Some new features include

  • More sensors inputs (Tire Temp, Brake Temp, Steering Wheel Angle)
  • Input for Inverter Keyswitch (KL15)

Author: Karan Thakkar

Peripherals

Digital I/O

RTM (Ready To Move) Button Input

The RTM input is handled as a standard digital GPIO read. It functions as a momentary push-button with software debouncing to avoid false triggering from mechanical bounce. Each press toggles the latched internal state between 0 and 1, allowing the system to reliably detect transitions. This signal is used in the vehicle state machine to move the car from the IDLE state into the RUNNING or DRIVE state once all safety conditions are met.

Brake Light Output

The brake light is driven as a digital output that activates based on braking conditions. Depending on system rules and logic, this signal can be turned on when:

  • Brake pressure exceeds a threshold
  • Regenerative braking torque is applied above a defined amount
  • Other system-defined braking conditions are met The output is typically a high-side digital drive that activates the physical brake lamp in accordance with FSAE regulations.

WheelSpeed Sensors (Front) Input

Front wheel speed sensors are hall-effect pickups producing a square-wave digital signal proportional to wheel rotation. The frequency of the pulses corresponds to wheel speed, allowing RPM to be computed from:

  • The frequency of the input pulses
  • The known number of sensor teeth or magnets
  • Wheel circumference and drivetrain ratios

From this, speed and wheel rotational dynamics can be calculated for traction control, odometry, telemetry, and dynamic vehicle control.

Fans & Pumps Output

Two MOSFET-controlled outputs are available for driving the cooling fan and coolant pump. These are controlled through software feedback loops that monitor thermistor temperature readings. When temperature increases beyond a target setpoint, fan or pump duty cycle increases proportionally. When temperatures fall below defined limits, duty cycle is reduced. This provides automatic thermal regulation for the accumulator or power electronics cooling loop.

Speaker Output

Authors: Dylan Tran, Anushree Godbole

Audio signaling is performed using a PWM output connected to a low-pass filter and to an amplifier, which drives the speaker. This is used to produce a tone for the Ready to Drive Sound (RTDS).

The speaker system ensures the car meets FSAE safety requirements for audible signaling before the drivetrain engages.

The Ready to Drive Sound is a continuous beep lasting 1–3 seconds, triggered during the vehicle startup sequence.

Testing Results: Testing was performed at 12V with the potentiometer maxed out to ensure compliance with the 80dB @ 2m requirement (EV.9.7 and IN.10.3). Tests were conducted in a quiet, closed room.

DistanceRecorded LevelVoltageStatus
1 meter101 dB~8.9VPASS
2 meters94 dB~8.9VPASS

Note: A voltage drop to ~8.9V was observed during the 3-second pulse; however, the decibel output remained well above the required threshold.


Analog I/O

Brake System Encoder

A linear analog pressure transducer is used to measure hydraulic brake line pressure. The voltage is read via an ADC and mapped into engineering units using a calibration curve. Brake pressure is used to:

  • Smoothly blend mechanical and regenerative braking
  • Block acceleration torque while braking
  • Activate brake lights
  • Support telemetry and control algorithms

This sensor is part of multiple safety-critical paths and must be sampled and checked reliably.

Acceleration Pedal Position

The accelerator pedal position sensor outputs a variable voltage proportional to throttle demand. The reading is converted into a normalized torque request and fed into the torque controller. For safety, dual redundant channels are commonly monitored, and mismatches are checked to detect faults such as wiring damage or sensor failure. If readings disagree beyond limits, torque is disabled and a fault is raised.

Suspension Travel

Suspension position is measured by analog linear potentiometers These readings allow:

  • Logging for vehicle dynamics analysis
  • Dynamic control adjustments such as damping or traction control

Thermistors

Thermistors measure temperature in critical subsystems such as:

  • Motor controllers
  • Batteries and accumulator modules
  • Cooling loops
  • Power electronics

These readings are used in closed-loop thermal regulation. Raw voltage readings are converted using standard thermistor curve equations or lookup tables. If temperatures exceed defined safety thresholds, torque may be reduced, cooling activated, or the vehicle shut down depending on severity.


CAN

BMS

The Battery Management System communicates via CAN, sending essential data such as:

  • Cell voltages
  • Module temperatures
  • Current consumption
  • State-of-Charge and State-of-Health
  • Fault flags and system warnings

The control system must consume these messages to maintain safe vehicle operation. If BMS signals a critical fault, the drivetrain must disable torque output immediately.

Battery Charger

During charging mode, the onboard charger exchanges CAN messages with the BMS and vehicle controller. Messages include:

  • Charging current and voltage targets
  • Charge state progress
  • Fault and error reporting

The charger generally follows BMS instructions to ensure safe constant-current and constant-voltage charging curves.

PCC

The PCC (Pre Charge Circuit) communicates over CAN to exchange infor with the CCM. Data typically includes:

  • Precharge state
  • Precharge Progress
  • TS/Accum Voltage read over frequency to voltage circuits
  • Precharge time

The PCC and CCM coordinate to ensure safe charing of the inverter and HV side. Read more about this in the PCC section.

CCM

The CCM (Central Computer Module) acts as the main MCU for car.

  • Broadcast system heartbeat messages
  • Reads all sensor readings
  • Receive driver input and button states
  • Relay fault conditions
  • Coordinate operation between subsystems
  • Sends torque commands to the Inverter based on all readings
  • Relays info to telemetry side.

Purpose

Process two pedal sensors and output a safe, normalized throttle value.

  • Converts raw ADC → pedal % (0–1)
  • Ensures both sensors agree
  • Detects faults (electrical + plausibility)
  • Prevents brake + throttle simultaneously

High Level Flow

Raw ADC → Filter → % Mapping → Voltage Check → Faults → Output


Update Function

APPS_UpdateData(raw1, raw2)

  1. Filter Inputs
  • Apply low-pass filter (100 Hz)
  • Smooths noise and spikes
  • This helps keep the pedal signal stable instead of jumping around from tiny electrical fluctuations

  1. Convert to Pedal %
  • Uses calibrated raw values:
rest → 0%
full → 100%
apps1 = LINEAR_MAP(raw1, REST1, FULL1, 0 → 1)
apps2 = LINEAR_MAP(raw2, REST2, FULL2, 0 → 1)
  • This means the code is mainly built around real measured ADC values from the pedal, not just ideal voltages
  • That makes calibration easier because the mapping matches the actual installed hardware

  1. Clamp Values
  • Ensure:
0 ≤ APPS ≤ 1
  • If the sensor goes slightly below rest or above full travel, the final output is still kept in a safe range

  1. Convert to Voltage Used for fault detection only:
voltage = ADC_VALUE_TO_VOLTAGE(raw)
  • The code still converts raw readings into voltage so it can check for open-circuit or short-circuit behavior
  • So percentage is based mainly on raw ADC calibration, while voltage is used mainly for electrical safety checks

  1. Voltage Safety Check

APPS1 (3.3V):

0.33V → 2.97V (fault range)

APPS2 (5V):

0.5V → 4.5V (fault range)
  • If a sensor voltage goes outside these ranges for too long, the system raises an APPS fault
  • This helps catch wiring issues, sensor faults, or readings that are no longer physically believable

  1. Sensor Agreement Check
difference = |APPS1 - APPS2|

If:

difference > 0.1 (10%)

→ FAULT

  • Since the pedal uses two sensors, they should track each other closely
  • If they disagree too much, the system treats that as unsafe

  1. Brake Plausibility Check

If:

Throttle > 25%
AND
Brake > threshold

→ FAULT

Reset when:

Throttle < 5%
  • This prevents the car from accepting heavy throttle while strong braking is also being detected

Output

APPS = (APPS1 + APPS2) / 2
  • The final pedal command is the average of both sensors
  • This gives one clean throttle value to pass into the rest of the vehicle logic

ADS1115 Integration

The APPS code is fed by the ADS1115 external ADC.

In adc.cpp, the ADS reads the two pedal channels and passes them into:

APPS_UpdateData(raw1, raw2);

So the flow is:

Pedal sensor → ADS1115 → raw ADC counts → APPS_UpdateData()

Why use the ADS1115?

The ADS1115 gives 16-bit readings, which means it can measure the sensor signal with much finer resolution than a typical 12-bit onboard ADC.

That helps because:

  • higher resolution gives smaller voltage steps between readings
  • better precision makes pedal calibration cleaner
  • less quantization error means smoother percentage mapping
  • less noise sensitivity makes the signal easier to filter and trust

In simple terms, the ADS gives the APPS module a more detailed picture of what the pedal is doing.

Why that matters for APPS

Since APPS is safety-critical, better signal quality is valuable:

  • small pedal changes are easier to detect
  • the filtered signal is smoother
  • the two sensors can be compared more accurately
  • fault checks become more reliable

So while the APPS logic itself is mainly based on filtered raw counts and calibration, the ADS1115 improves the quality of those raw counts before APPS ever sees them.


Summary

APPS:

  • Converts pedal → throttle
  • Verifies sensor agreement
  • Blocks unsafe conditions

The ADS1115 helps by giving high-resolution 16-bit sensor readings with better accuracy and lower effective noise, making the APPS signal cleaner and more trustworthy before torque is commanded.

Author: Karan Thakkar

Vehicle Control

Motor/Inverter

The motor inverter in this system is responsible for controlling how power flows from the high-voltage battery to the traction motor, and its behavior is managed through a well-defined state machine aligned with Formula SAE Electric vehicle requirements.

The inverter is commanded through CAN messages that include torque requests, motor mode, relay control, and system readiness.

Depending on the operating state—such as OFF, STANDBY, PRECHARGING, IDLE, DRIVING, or FAULT—the inverter receives different command values that ensure the vehicle only delivers torque when it is electrically safe to do so. During driving, the requested torque is converted into a percentage value that the inverter understands, while additional parameters like regen mode, gear mode, and allowable currents from the BMS are also communicated. This helps maintain smooth torque delivery and ensures that high-voltage operation meets safety rules set by FSAE.

The transitions between inverter states are based on system conditions such as the Ready-To-Move button, precharge status, BMS confirmations, and overall system health.

For example, the inverter does not enter the DRIVING state until the precharge process has completed, battery relays are confirmed closed, and the driver actively requests propulsion. If communication issues or electrical faults occur, the inverter is immediately commanded into a FAULT state to disable torque and open relays where needed.

By structuring inverter behavior around explicit state transitions and safety checks, the system guarantees predictable and rules-compliant operation while providing clear separation between low-voltage logic and high-voltage power control, which is a core requirement for competition-legal and reliable Formula SAE EV design.

State Machine

“ “

Fault Handling

This fault map system uses a single 32-bit variable to keep track of all vehicle faults. Each fault type has its own bit in the bitmap, and when a fault is detected, its bit is turned on. When the fault clears, the bit is turned off. This makes fault storage very compact and fast to check, which is helpful on embedded systems where memory and CPU time matter. The system can quickly tell if any fault is active or look at specific bits to see which ones are triggered.

The handler function then looks at the bitmap and sets the motor into a fault state if any fault is active. Right now, every fault is treated the same, but the structure is flexible enough to customize actions later, such as limiting torque for minor issues or shutting down the drivetrain for serious ones. This design keeps the code organized, easy to maintain, and simple to expand as the system grows.

Author: Sebastian Ethan Basa

Traction Control Module Overview

This module implements a basic traction‑control strategy intended to limit excessive wheel slip during acceleration. The approach uses front‑wheel speeds as an estimate of vehicle speed and compares them to the driven rear‑wheel speeds to detect slip. When slip exceeds a defined threshold, the system reduces the requested torque using a PID controller.

Purpose

The goal of this traction‑control logic is to prevent rear‑wheel spin by dynamically adjusting torque output. By monitoring the difference between front and rear wheel speeds, the system attempts to maintain the driven wheels within an acceptable slip range, improving vehicle stability and acceleration performance.

Control Method

The algorithm calculates slip using the ratio:

slip = (rear wheel speed average - front wheel speed average)/(front wheel speed average)

A PID controller is configured to reduce torque when slip exceeds a predefined threshold. The PID output directly modifies the torque request sent to the powertrain, with proportional control currently being the primary active term.

System Behavior

  • Front wheel speeds serve as a reference for true vehicle speed.
  • Rear wheel speeds represent the driven wheels, where slip is expected to occur.
  • When slip rises above the threshold, the PID controller computes a corrective torque reduction.
  • The loop continuously monitors wheel speeds and updates torque in real time.

Current Limitations

This implementation is an early prototype and is not yet suitable for on‑vehicle use. Key limitations include:

  • The PID controller is re‑initialized on every loop iteration, preventing proper integral and derivative behavior.
  • The control loop runs indefinitely without timing control, blocking other system tasks and preventing consistent update rates.
  • The slip threshold is set unrealistically high for effective traction control.
  • Wheel‑speed inputs are unfiltered, making the system sensitive to noise.
  • No safety checks or torque‑limiting constraints are implemented beyond the raw PID output.

Purpose

Design a function that optimizes 0–60 mph acceleration by adjusting torque output based on slip ratio.

  • Optimal slip ratio ≈ 7% (commonly 5–10% depending on car).
  • If slip ratio exceeds optimal → decrease torque
  • If slip ratio falls below optimal → increase torque

Algorithm Overview

Initial Function

  • Validate and reset PID gains and internal PID state.
  • Obtain constants (max torque, optimal slip target).
  • Initialize launch control button to false (inactive until pressed).

Update Function

  1. Check button state

    • If not pressed → output driver torque normally.
  2. Wheel speeds

    • Controlled speed: rear wheel with higher speed.
    • Free rolling speed: front wheel with lower speed.
  3. Slip Ratio

    • slip = (controlledSpeed - freeRollingSpeed) / freeRollingSpeed
  4. Torque Logic

  • If slip < 0.05 → increase torque.
  • If slip > 0.10 → decrease torque.
  1. PID Correction
  • Run at 100 Hz (dt = 0.01).
  • Use PID::PID_Calculate() for correction.
  • Add correction to driver torque.
  1. Clamp Torque
  • If corrected torque > max → clamp to maxTorque.
  • If < 0 → clamp to 0 Nm.

Implementation Details

void launchControl_Init()

Initializes PID controller values for slip ratio correction.


void threadLaunchControl(void *pvParameters)

LAUNCH_STATE_ON

  1. Obtain slower front wheel speed.
  2. Convert rad/s → m/s using wheel radius.
  3. Motor/rear speed used as controlled speed.
  4. Compute slip.
  5. PID applies torque correction.
  6. Clamp torque within bounds.
  7. If brakes are pressed → exit to LAUNCH_STATE_OFF.

LAUNCH_STATE_OFF

  • Wait for:
  • Front brake fully pressed
  • Motor speed = 0
  • Then activate launch control (FSAE requires button activation — must update logic).

FAULT State

  • Triggered by irregular wheel speeds or invalid conditions.

TESTING PLAN

Method Signature:

threadLaunchControl(void *pvParameters)

Constants:

slipTarget = 0.7f

minTorque = 0.0f

maxTorque = 260.0f

wheelRadius = 0.35f

TestCase 1 (base case)

Brake = 60 psi

Motor speed = 0 rad/s

Motor state = MOTOR_STATE_DRIVING

threadLaunchControl()

launchControlState -> LAUNCH_STATE_ON

TestCase 2

wheelSpeedFL = 80 rad/s, wheelSPEEDFR = 82 rad/s, motorSpeed = 80 rad/s

realTorque = 150 Nm

LaunchState = on

Slip ratio = 0.0–0.02 -> small PID correction

torqueDemand = 150 Nm (unchanged to barely changed)

LaunchState = ON

TestCase 3

wheelSpeedFL = 40 rad/s, wheelSpeedFR = 42 rad/s, motorSpeed = 100 rad/s

realTorque = 200 Nm

Slip ratio = (100–41)/41 = 1.43 -> very high slip

PID correction negative -> torque reduced near 0 Nm

torqueDemand = near minTorque = around 0 Nm

TestCase 4

wheelSpeedFL = 100, wheelSpeedFR = 99, motorSpeed = 100

realTorque = 250 Nm, slip slightly below 0.07

PID positive correction increases torque above 260 -> clamped at 260 Nm

torqueDemand = 260 Nm

TestCase 5

LaunchState = ON, BSE_GetBSEReading() -> 80 PSI

APPS_GetAPPSReading() < 1.0

Expected behavior:

PID reset

torqueDemand = rawTorqueInput

launchControlState = LAUNCH_STATE_OFF

Author: Karan Thakkar

Tasks & Scheduling

“ “

The firmware is structured using FreeRTOS to split the vehicle control system into separate tasks, each handling its own area. ADC readings, motor control, telemetry, and high-level control all run in their own threads, so critical operations don’t get blocked by slower tasks. For example, threadADC continuously samples analog sensors, while threadMotor handles the motor state machine, updates torque, and communicates with the VCU and BMS over CAN. This keeps motor commands and sensor updates running on a predictable schedule, which is important for safe and reliable operation in the car.

The threadMain task is used for testing and user interaction. It reads input from the serial monitor to adjust torque, change vehicle states, and toggle regen, while also printing telemetry like motor speed, current, and temperatures. threadTelemetry collects and sends key vehicle data over CAN on a regular cycle. Separating everything into different threads makes the system easier to debug, maintain, and expand. FreeRTOS handles scheduling, so high-priority tasks like motor control always run reliably, while less critical tasks like user input don’t interfere.

Watchdog Timer

In Progess

Author: Karan Thakkar

Telemetry

Current Version: HIMaC testing:

telemetryData = {
            .APPS_Travel = APPS_GetAPPSReading(),

            .motorSpeed = MCU_GetMCU1Data()->motorSpeed,
            .motorTorque = MCU_GetMCU1Data()->motorTorque,
            .maxMotorTorque = MCU_GetMCU1Data()->maxMotorTorque,
            .motorState = Motor_GetState(),
            .mcuMainState = MCU_GetMCU1Data()->mcuMainState,
            .mcuWorkMode = MCU_GetMCU1Data()->mcuWorkMode,

            .mcuVoltage = MCU_GetMCU3Data()->mcuVoltage,
            .mcuCurrent = MCU_GetMCU3Data()->mcuCurrent,
            .motorTemp = MCU_GetMCU2Data()->motorTemp,
            .mcuTemp = MCU_GetMCU2Data()->mcuTemp,

            .dcMainWireOverVoltFault =
                MCU_GetMCU2Data()->dcMainWireOverVoltFault,
            .dcMainWireOverCurrFault =
                MCU_GetMCU2Data()->dcMainWireOverCurrFault,
            .motorOverSpdFault = MCU_GetMCU2Data()->motorOverSpdFault,
            .motorPhaseCurrFault = MCU_GetMCU2Data()->motorPhaseCurrFault,

            .motorStallFault = MCU_GetMCU2Data()->motorStallFault,
            .mcuWarningLevel = MCU_GetMCU2Data()->mcuWarningLevel,

        };

This telemetry section acts as a real-time summary of everything important happening in the drivetrain during HIMaC testing. It records the driver’s accelerator pedal position so we know how much torque is being requested at any given moment. Keeping this information in one place makes it easy to log, monitor, and analyze how the system responds under different conditions.

It also gathers detailed status information from the motor controller, including actual motor speed, delivered torque, voltage, current, and temperatures. These values help confirm that the system is performing as expected and staying within safe operating limits. Along with this, the telemetry captures the current operating modes and internal controller states, giving a clear picture of what the motor system is actively doing.

In addition, the telemetry includes a set of fault and warning indicators that report when something abnormal has occurred, such as overcurrent, overspeed, overheating, or voltage problems. These alerts allow the system to react quickly by limiting torque or switching into a safer mode. Overall, this telemetry structure provides a clean and organized snapshot of the drivetrain’s condition, making testing, troubleshooting, and performance analysis much easier.

All this data is sent over CAN to our Raspberry PI in 1 packet.

Purpose

Design a function that converts 4 shock sensor ADC readings into usable shock travel values in millimeters for telemetry and suspension analysis.

The shock sensors are linear potentiometers whose signal sits close to 5 V when the shock is fully extended. When the suspension compresses, the voltage only decreases slightly.

The total useful signal range is approximately:

5.00 V → 4.86 V

So the actual change is only about 0.14 V.

Because the change is very small relative to the 5 V baseline, the code treats the difference from 5 V as the meaningful signal and scales that difference to the full mechanical travel of the shock.


Algorithm Overview

Initial Function

  • Reset all stored shock sensor values
  • Clear:
    • raw ADC readings
    • converted voltages
    • shock travel values in mm

This ensures startup begins with known values instead of stale sensor data.


Update Function

  1. Read 4 ADC values

    • Inputs come in as rawReading1 through rawReading4
  2. Invert ADC readings

    • Each reading is converted using:
abs(4095 - rawReading)
  • The ADC is 12-bit, meaning values range from 0 to 4095.
  • Because the sensor voltage decreases during compression, the raw ADC value behaves opposite of the desired interpretation of travel.
  • Subtracting from 4095 flips the scale so that increasing compression produces increasing processed signal values.
  1. Convert ADC counts to voltage
ADC_VALUE_TO_VOLTAGE(...)

This converts the processed ADC value into volts.

  1. Use abs() on voltage
abs(ADC_VALUE_TO_VOLTAGE(...))

This ensures the voltage value is always non‑negative. It mainly acts as a safety guard against unexpected sign behavior.

  1. Map voltage difference to shock travel

The shock signal only changes by 0.14 V across its usable range:

5.00 V → 4.86 V

The code maps that small change to the full suspension travel using:

(voltage / 0.14f) * SHOCK_TRAVEL_MAX_MM

Conceptually this means:

VoltageShock Travel
5.00 V0 mm
4.93 V~50% travel
4.86 Vmax travel
  1. Clamp output

The computed travel is limited using:

constrain(value, 0.0f, SHOCK_TRAVEL_MAX_MM)

This prevents invalid sensor readings from producing impossible travel values.


Why abs() is used

1. Flipping the ADC scale

abs(4095 - rawReading)

The sensor begins near 5 V when fully extended, so the ADC reading starts close to the top of the ADC range.

As the shock compresses:

  • Voltage drops slightly
  • ADC value drops slightly

By subtracting from 4095, the scale is mirrored so the processed signal increases with compression.

The abs() ensures the result stays positive.

2. Protecting voltage conversion

abs(ADC_VALUE_TO_VOLTAGE(...))

This simply ensures the converted voltage value cannot become negative due to any intermediate math behavior.

The important calibration step is not abs() itself but recognizing that the signal range is only 0.14 V below 5 V.


Implementation Details

void Shock_Init()

Initializes all stored linear potentiometer data to zero.

Values cleared:

  • raw ADC readings
  • converted voltages
  • shock travel values

This function runs once during startup.


void ShockTravelUpdateData(...)

Processes all four shock sensor ADC readings.

Steps:

  1. Flip ADC readings using 4095 - rawReading
  2. Convert processed ADC values to voltages
  3. Interpret the small 0.14 V drop from 5 V as suspension compression
  4. Scale that voltage change into millimeters
  5. Clamp output values within the physical travel range

Linpot_GetData()

Returns a pointer to the shared linPots data structure.

Other modules can read:

  • raw processed sensor values
  • voltages
  • shock travel in millimeters

This allows telemetry and future control algorithms to access suspension data.


Example

Example input

If the sensor is fully extended:

rawReading ≈ 4095

After inversion:

4095 - 4095 = 0

Voltage becomes near 5 V baseline, so travel ≈ 0 mm.


If the voltage drops by 0.07 V:

5.00 V → 4.93 V

That represents roughly half of the total 0.14 V span, so:

travel ≈ 0.5 × SHOCK_TRAVEL_MAX_MM

TESTING PLAN

Method Signature:

ShockTravelUpdateData(uint16_t rawReading1,
                      uint16_t rawReading2,
                      uint16_t rawReading3,
                      uint16_t rawReading4)

Constants:

ADC max count = 4095
voltage span = 0.14 V
minTravel = 0
maxTravel = SHOCK_TRAVEL_MAX_MM

TestCase 1 (Initialization)

Call:

Shock_Init()

Expected behavior:

  • All voltages = 0
  • All raw readings = 0
  • All shock travel values = 0 mm

TestCase 2 (Fully Extended)

Inputs correspond to ~5 V:

rawReading ≈ 4095

Expected:

shockTravel = 0 mm

TestCase 3 (Mid Travel)

Voltage ≈ 4.93 V (half of 0.14 V span)

Expected:

shockTravel ≈ 0.5 × SHOCK_TRAVEL_MAX_MM

TestCase 4 (Full Compression)

Voltage ≈ 4.86 V

Expected:

shockTravel = SHOCK_TRAVEL_MAX_MM

TestCase 5 (Out of Range)

Voltage difference exceeds 0.14 V.

Expected:

constrain() clamps value to SHOCK_TRAVEL_MAX_MM

PowerLimit_Update()

Purpose

This function ensures the car does not request motor torque which exceeds two important torque threshold requirements. The two cases in which this function would intervene are when max requested power exceeds 80kW (EV.3.3.1) and when the accumulator SOC is < 20%, in which rules specify that we must linearly reduce allowed power down to 0 at 5%.

Structure

Given above is a block diagram of the PowerLimit_update() function. In our codebase, PowerLimit_update() is called inside motor.cpp, during Motor_UpdateMotor()’s MOTOR_STATE_DRIVING state. The variable torqueDemand is given as an argument to Motor_UpdateMotor(), and this is then passed into our function, and finally returned and assigned to motorData.desiredTorque. A function somewhere else in the codebase will then read this value and send it to the motors, creating the actual torque requested.

In addition to torqueDemand, PowerLimit_Update() requires accumulator voltage and current as well as accumulator SOC. Voltage and current are multiplied to find power.

Implementation

There are a few constants used in this method, all of which are defined inside /utils/utils.h. What these all represent and what they are used for are as follows:

SOC_START, SOC_END

Represents the percentage (as a decimal) at which SOC-based torque limiting (derating) begins and ends. As of 10/2025 they are set to .2 and .05, respectively. Derating will begin at SOC_START, and will linearly reduce until reaching SOC_END, when torqueDemand will be 0.

MOTOR_MAX_TORQUE

Represents the maximum torque that the car is designed to output. Measured in N*m. This is used to linearly scale the torqueLimit proportionally based on SOC.

POWER_THRESHOLD_kWh

Represents maximum power allowed in FSAE rulebook. When calculated power is over this amount, driverTorqueCmd gets scaled down by this threshold divided by the calculated power. The assumption behind this is that power and torque are proportional, meaning that when calculated power is 10% above the threshold, driverTorqueCmd gets scaled down accordingly by 10%.

SOC derating amount is calculated and then stored inside the local variable torqueLimit, which is assigned to driverTorqueCmd at the end. Power limiting doesn’t use torqueLimit, it just directly sets driverTorqueCmd.

Testing

Method signature:

PowerLimit_Update(float soc, float voltage, float current, float driverTorqueCmd)

Constants:

MOTOR_MAX_TORQUE = 260.0F

POWER_THRESHOLD_kWh = 90

SOC_START = 0.2

SOC_END = 0.05

Test case 1 (base case):

PowerLimit_Update(.25, 40, 2, 200);

Returns 200

Test case 2 (MOTOR_MAX_TORQUE condition checking):

PowerLimit_Update(.25, 40, 2, 270);

Returns 260

Test case 3 (POWER_THRESHOLD_kWh condition checking):

PowerLimit_Update(.25, 40, 3, 200);

Returns 150

Test case 4 (SOC derating checking):

PowerLimit_Update(.1, 40, 2, 200);

Returns 66.67

Test case 5 (SOC end derating checking):

PowerLimit_Update(.05, 40, 2, 200);

Returns 0

Author: Pranav Kocharlakota

Wheel Encoder

Purpose

The wheel encoder module measures how fast a wheel is spinning, reporting the result in RPM (revolutions per minute). It works by detecting a gear attached to the wheel — each time a tooth on that gear passes a sensor, the module takes note of the timing and uses that to figure out how fast the wheel is turning.

How It Keeps Track of Speed

The module is always listening in the background. Whenever a gear tooth passes the sensor, it quietly records the exact time of that event and compares it to the previous one. That time gap is the key piece of information used to calculate RPM.

This listening happens automatically and independently from everything else the system is doing, so it never misses a pulse even while the rest of the program is busy.

If no gear tooth passes the sensor for more than half a second, the module assumes the wheel has stopped and reports 0 RPM. This prevents the system from showing a stale, outdated speed reading when the wheel is no longer moving.

Calculating RPM

Once the time between two pulses is known, the speed is calculated by figuring out how long a full rotation would take at that rate — accounting for the number of teeth on the gear — and converting that into revolutions per minute.

RPM = 60,000,000 / (Δt × N)

Where:

  • Δt = time between the last two pulses (in microseconds)
  • N = number of teeth on the gear

A gear with more teeth gives more frequent pulses, which means better accuracy at low speeds. A gear with fewer teeth gives fewer pulses, which is fine at higher speeds but less precise when the wheel is moving slowly.

Setup and Ongoing Use

The module has two distinct phases:

1. Startup When the system first powers on, the module sets up the sensor pin and tells the hardware to start listening for gear tooth signals automatically. This only needs to happen once.

2. Running While the system is operating, the module should be checked regularly (many times per second is ideal). Each check looks at the most recent pulse timing, applies the RPM calculation, and updates the current speed reading. Anything in the system that needs the wheel speed can then simply ask for the latest value.

Summary of Constants

SettingWhat It Controls
Sensor pinWhich physical pin on the board the sensor is connected to
Gear teethHow many teeth are on the encoder gear
TimeoutHow long to wait without a pulse before reporting 0 RPM (default: 0.5 seconds)

Author: Pranav Kocharlakota

CAN Interrupt Handler

Overview

CAN_RxInterruptHandler replaces a constantly polling system — where the processor would repeatedly check if a message had arrived — with one that only runs when a message actually arrives. When triggered, it takes the message’s ID and data, bundles them into a CANMessage_t struct, and drops it into a queue. This frees the processor to do other work between messages rather than burning cycles checking for them.

The Queue

The queue holds up to 10 messages at a time. If a message arrives when the queue is full, it is dropped and the overflow counter increments. However, messages should be cleared from the queue faster than the queue can be populated so the overflow counter acts as a safety and testing mechanism. If a task was waiting on the queue, the handler wakes it up immediately after adding the message.

Why It’s Kept Short

The handler runs at a higher priority than everything else, blocking all other execution while it runs. Doing anything beyond packaging and queuing would stall the system. The actual processing of each message happens separately, at normal priority.

Testing Plan

1. Normal Operation

Goal: Confirm CAN messages are being received and processed correctly with no overflow.

  • Power on the system with all devices connected to the CAN bus.
  • Open the Serial Monitor and confirm that CAN messages are being printed — precharge data is readable by the CCM and inverter commands are being received.
  • Monitor the Serial Monitor for the duration of the test. If no overflow message appears, the handler is working correctly.
  • Cross-check the received values against known expected values (e.g., correct voltage, temperature, SOC) to confirm data is not being corrupted in transit.
  • If no overflow message is printed and values look correct, the code is functioning as expected. Stop here.

2. Overflow Debugging (Fallback)

Goal: Identify why overflow messages appeared in step 1.

  • Open CANalyzer and connect to the bus. Log all incoming messages and note the total message rate per second and which IDs are transmitting. Ensure that it is only essential devices and all are transmitting at normal rates.
  • The size of the queue may be too small increase it by 10 until overflow is gone. The ideal size of the queue is between 10 and 30.
  • Do not increase the queue by huge increments. This is because a large queue takes more memory and more importantly a large queue paints over possible issues with transmission rates of other devices.

Authors: Anushree Godbole, Kenneth Dao

Watchdog Timer (WDT) Implementation

Overview

The Watchdog Timer (WDT) subsystem is implemented to ensure the reliable operation of critical vehicle control software. Its primary function is to detect software stalls or timing violations in key tasks and enforce recovery through a system reset.

This implementation monitors the execution health of three safety-critical subsystems:

  • Brake System Encoder (BSE)
  • Accelerator Pedal Position Sensors (APPS)
  • Traction Control (planned, not yet implemented)

If any subsystem fails to update within a defined time threshold, the watchdog is no longer serviced, allowing the microcontroller’s hardware watchdog to reset the system.

System Architecture

The WDT system consists of:

  1. Hardware Watchdog Peripheral (via Watchdog_t4)
  2. FreeRTOS Monitoring Task (WDT_Update_Task)
  3. Timing Tracking Variables (*_last_run_tick)
  4. Bitmask-Based Fault Detection

Each monitored subsystem updates a shared timestamp (*_last_run_tick) whenever it executes successfully. The watchdog task periodically checks these timestamps to determine system health.

Fault Detection Strategy

The system uses a time-based fault detection approach:

  • Each subsystem must update within a defined fault threshold
  • The watchdog task runs periodically to evaluate subsystem health
  • A subsystem is considered faulty if its update interval exceeds the threshold

Fault states are internally represented using a bitmask, allowing multiple subsystem failures to be tracked simultaneously and enabling easy scalability for additional monitored components.

Fault Representation

Faults are encoded using a bitmask:

SubsystemBitValue
BSE00b01
APPS10b10
  • WDT_REQUIRED_MASK = 0b00 represents no flags and a fully healthy system
  • If any fault bit is set, the watchdog is not serviced, allowing timeout

Timing Relationship: Detection vs. Reset

The watchdog system uses two distinct timing parameters that serve different purposes:

  • 100 ms (Fault Threshold for APPS and BSE):
    Defines how frequently the system checks the health of the subsystems and detects timing violations

  • 1 second (Watchdog Timeout):
    Defines how long the system can go without being serviced before the hardware watchdog triggers a reset

A fault must persist across multiple monitoring cycles before resulting in a watchdog timeout, ensuring both responsiveness and stability.

Watchdog Behavior

The watchdog operates on a strict all-systems-healthy requirement:

  • Healthy system:
    All monitored subsystems update within their thresholds → watchdog is continuously serviced

  • Fault condition:
    One or more subsystems exceed their timing threshold → watchdog is not serviced

  • System response:
    If the watchdog is not serviced within its configured timeout (1 second), the hardware automatically resets the microcontroller

This ensures that any persistent software failure results in a full system restart.

Fault Handling & Diagnostics

If a fault is detected, the system logs the condition via serial output:

ConditionMessage
BSE overdue“WDT: BSE update overdue”
APPS overdue“WDT: APPS update overdue”
Both overdue“WDT: BSE and APPS updates overdue”

Authors: Adam Wu, Anna Lee

Pre-Charge Circuit


The pre-charge circuit is a subsystem that ensures that the accumulator (battery) will charge safely by limiting the inrush current to the car’s tractive system when the car is first powered on. The tractive system (TS) refers to the Inverter and Motor of the car as a unit. Limiting the current protects the rest of the system from potential damage. Accumulator-TS current doesn’t actually flow through the PCC , but rather the PCC will monitor their respective voltage levels and control the relays which connect Accumulator and TS. In the event of a shutdown, PCC is able to control relays and completely disconnect the accumulator from the TS. Watch this video for a great explanation on why we need such a system, ours is very similar: https://www.youtube.com/watch?v=6-RndXZ5mR4

Hardware

How It Works

Schematic diagram of PCC

The PCC is split into two halves, high voltage (HV) and low voltage (LV). The HV side deals with accumulator and tractive system voltages which routinely go up to 400 V, while the LV side does not exceed 12 V. The FSAE rulebook specifies that there must be separation between these two voltage levels. However, we still need to communicate betweeen the two sides, as the teensy needs to read the voltages of accumulator and TS, both of which are on the HV side. In our circuit, this is accomplished using two optocouplers. Optocouplers achieve voltage separation utilizing an LED on one side (in this case HV) which will internally light up when there is a signal, and a phototransistor on the other side which will react to that LED.

HV Side

Schematic diagram of PCC

Here is a schematic demonstrating our system. The PCC is constantly reading accumulator voltage and tractive system voltage which come from B+_in and TS+, respectively. These are the most important connections to the system, and are fed into the board with a 2x2 molex connector. To read the voltages, the circuit utilizes two voltage to frequency converters, with input voltages that are stepped down with a voltage divider resistor network to be 1/10 of the actual value.

There are two Accumulator Isolation Relays (AIRs) in the PCC: AIR+ and AIR-. These AIRs are open air contact relays, capable of isolating very high voltages. In addition, there is a shutdown relay, which functions as a two way switch. We will refer to the shutdown relay as closed when it’s in position 1, and open when it’s in position 2. The precharge sequence and the states of these 3 relays during them is as follows:

Precharge: AIR+ is open, AIR- and shutdown relay are closed, TS is slowly and safely charged through the resistor. End of precharge: When TS+ is 70% of B+_in, the onboard teensy will tell AIR+ to close, bypassing the resistor. The system is ready. Error state: When we receive a shutdown signal through Shutdown_in or from a too fast/too slow precharge (see video for explanation if confused), AIR+/- and shutdown relay open. This configuration disconnects the accumulator and safely discharges the tractive system.

LV Side

Most of the complexity of the circuit are for the V2F converters, and those largely don’t need to be worried about. What is important to understand here is that the V2F converters convert an analog voltage to a signal with a frequency proportional to that voltage. Meaning: the V2F output signal is oscillating between 1 and 0. When vin is high, that oscillation will be faster, and when it is low that oscillation will slow down.

Using the optocouplers to maintain separation, that signal is fed to the teensy which then samples that data, and is able to control the rest of the circuit. The detailed implementation of these LM331 V2F converters involve capacitors and resistors which should be tuned to special values, but for this discussion of the PCC we will not dive into that.

Additionally, information on TS voltage, accumulator voltage, and error data are being sent over CANBUS to the CCM. This requires two teensy pins and utilizes a CAN transciever module.

Connecting Everything and Testing

Setting up the PCC can be confusing, and since it deals directly with accumulator voltage levels, it is important to understand how the wiring and connections work. Each of the connectors and what each pin is for are as follows:

HV IN TS+/-: positive and negative terminals of the tractive system B+_in: Accumulator-side AIR+ pin GNDS: Accumulator-side AIR- pin

POWER IN Shutdown_in: active high shutdown signal that triggers safe discharging of tractive system GLV-: Ground for GLV 12V: Positive terminal of GLV, or a 12 V power supply

IR+ Relay and IR- Relay Shutdown_in: active high shutdown signal that triggers safe discharging of tractive system GLV-, IR+_GND: ground signals

CAN Bus CANH/CANL: used for communication with CCM and other modules

Setup To test or use PCC, in addition to the board itself, you will need:

  • two open air relays
  • one 12 V source
  • Accumulator or a 400 V power supply
  • Motor+Inverter or some capacitor to represent the load

To hook up the accumulator to the inverter, take the two open air relays and decide which of them be AIR+ and AIR-. Connect one pin from AIR+ to Accumulator+ and one pin from AIR- to Accumulator-. The other pin of AIR+ goes to the positive terminal of the actual tractive system, and the other pin of the AIR- goes to the negative terminal of the tractive system. Definitely triple check this configuration. Make sure everything is isolated, it’s a good idea to use electrical tape over the contactors to avoid accidental shorting.

Now that the high-voltage components are hooked up, we need to connect the wires that allow PCC to monitor voltage levels. Connect the TS AIR+ pin to the TS+ pin of the HV IN connector, and the TS AIR- pin to TS-. Connect the accumulator AIR+ pin to B+_in, and the accumulator AIR- pin to GNDS. You will need to crimp wires and prepare a 2x2 molex connector.

Finally, we need to connect the PCC to the relays, so it can control them and open them in the case of a fault. The IR+/- ports correspond to AIR+/-, respectively. Simply connect them to their respective relays with Shutdown_in as the high input and with GND as low. After connecting CAN, the PCC is properly set up.

Software

Important Modules

General Overview

As mentioned in HV, the PCC is constantly reading the accumulator voltage and tractive system voltage and printing them to the serial monitor. As it does so, it transitions between four possible states: STANDBY, PRECHARGE, ONLINE, and ERROR.

  1. STANDBY (void standby()): The initial/idle state of the PCC. Waits for a stable shutdown circuit (SDC). Opens accumulator isolation relays (AIR) and precharge relay. If the accumulator voltage is greater than or equal to the minimum voltage for the shutdown circuit (PCC_MIN_ACC_VOLTAGE), transitions into the PRECHARGE state.
  2. PRECHARGE (void precharge()): Closes AIR- and precharge relay. Monitors precharge progress, which is a function of the tractive system voltage and accumulator voltage. If the precharge progress is completed (prechargeProgress >= PCC_TARGET_PERCENT), checks if the target percentage was reached too quickly. If so, transitions into the ERROR state. Otherwise, transitions into the ONLINE state. If the precharge is too slow, it will also transition into the ERROR state.
  3. ONLINE (void running()): The status that indicates that precharge has safely and successfully completed. Closes AIR+ to connect accumulator (ACC) to tractive system (TS) and opens precharge relay.
  4. ERROR (void errorState()): The PCC error status. Opens AIRs and precharge relay.

epoch refers to system time (?)

main.cpp

void setup(): Initializes GPIO pins and the precharge system.

void threadMain(): Continuously reads accumulator voltage and tractive system voltage and switches between the four states based on corresponding conditions.

precharge.cpp

getFrequency(int pin): Gets frequency of a square wave signal from a GPIO pin.

getVoltage(int pin): Converts the frequency into voltage for accumulator or tractive system GPIO pins.

prechargeInit(): Initializes the mutex and precharge task.

prechargeTask(void *pvParameters): Handles the state machine and status updates.

void standby(), void precharge(), void running(), void errorState(): See General Overview.

float getTSVoltage(): Gets the tractive system voltage.

float getAccumulatorVoltage(): Gets the accumulator voltage.

PrechargeState getPrechargeState(): Returns the current precharge state (undefined, standby, precharge, online, or error).

getPrechargeError(): Returns current error information as an error code.

gpio.cpp

Initializes GPIO pins (shutdown control pin, accumulator pin, tractive system pin).

can.cpp

CAN communication.

Improvements + next steps

  • Clean up code (get rid of duplicate and unncessary comments)
  • Replace B+_in and TS+ wire housings which barely fit inside 2x2 molex connectors

Author: Sebastian Ethan Basa

ORION BMS (Battery Management System)

“ “

The purpose of the BMS is to monitor various aspects of the battery, protect its lifespan, and report any faults with the battery.

“ “

The BMS transmits data via CAN messages which have their own unique CAN IDs. Using C++ and FreeRTOS, we can retrieve the CAN data from the BMS in the form of data structs, which can be interpreted and used throughout the code to control the car. Some of the data values that the BMS can transmit include battery cell voltages, temperatures, battery health, and fault conditions.