[EN] State-Machines applied to DC Motor Calibration

This document is an intro + guide to the project of the DC Motor Calibration Automated Test Setup.

The project automates a speed–voltage calibration for a small DC propulsion unit. An Arduino generates a configurable PWM (=pulse width modulation) ramp TTL signal that drives an H-bridge motor driver, which powers a 3.7 V drone DC motor with a two-blade propeller. The motor sits in a simple rig with an optical slot sensor acting as an incremental encoder: each blade crossing produces a pulse used to infer rotational speed RPM (=rotations per minute). Test parameters—ramp maximum, duration, step/cadence—are set in software so the same bench hardware can run many repeatable sweeps.

During a run, the PWM ramp and the RPM measurements are synchronized: PWM updates define clean timing windows over which pulses are counted and converted to RPM (or, alternatively, a fixed sampling period can be used). After each measurement, the system streams tab-separated values (TSV) over the USB serial link—time, commanded waveform value, applied PWM, and RPM—so the user can copy-paste straight into Excel (or any spreadsheet) for plotting and further analysis.

The hardware part is simple. A PC connects over USB to an Arduino board (e.g., Arduino Uno). One PWM-capable digital pin on the Arduino feeds the H-bridge DC motor driver’s enable/PWM input.

The H-bridge board is powered by a bench DC supply (set to the motor’s rated voltage/current) and drives a 3.7 V mini drone DC motor fitted with a two-blade propeller. Verify that voltage–current operating points during the calibration ramp are not limited by the DC supply (current limit/sag), especially at high speed/high load.

An optical slot sensor (IR LED + phototransistor) is mounted in a small mechanical setup so each blade interrupts the beam once per revolution half-turn, producing two clean pulses per revolution. Use the slot sensor board’s LED indicators to align the blades for a clean RPM signal, and prefer the sensor’s digital output over the analog output. The sensor output connects to an Arduino interrupt pin (e.g., D2) for accurate pulse counting.

Logic grounds are commoned: Arduino GND ↔ driver logic GND ↔ sensor GND. The motor power supply is separated from the logic power supply, which stays tied at the driver. Power the motor from the bench supply and the Arduino from USB to reduce coupling; the H-bridge’s internal diodes handle flyback.

For real time observation, use two scope test points: the PWM signal (Arduino → driver enable) and the RPM pulse signal (sensor → Arduino interrupt). View duty and pulse timing on an oscilloscope while the Arduino simultaneously uses them (drive and measure).

For user input, a pushbutton, a potentiometer (knob), and the USB serial interface can be considered for future use. Also, for user output, three RGB LEDs for visual feedback and a piezo buzzer for acoustic signalling/sound feedback can be used. These are included for completeness here; however, their use in the current project is optional, but they can be handy to implement simple user interfaces here or in other projects.

The software part is organized into six small independent tasks, through modules that are coded as finite state machines (FSMs).

This system is an experimental test ground for academic training. While the automated DC motor calibration could be implemented more simply, the goal is to introduce and consolidate a reusable, modular FSM framework that is scalable and easy to adapt to future projects.

It’s meant to teach students how FSMs work, how to implement and test them, to understand why we use FSMs, and show that the code is reusable and scalablable, that it stays clear and tidy as the system grows (no spaghetti!), and how the modules are integrated into a system, and how they can interact via a small set of shared globals.


Why State Machines?

The problem: embedded systems juggle multiple tasks—waveform generation, PWM output, RPM measurement, logging, user I/O—often with different cadences and hardware constraints. Naive loops with delays quickly become unmanageable.

State machines solve this by:

  • Modularity: each feature lives in its own file/function with its own states, timers, and config; no spaghetti delays.

  • Testability: each FSM can be exercised in isolation (e.g., force states or inject inputs) and verified.

  • Determinism: all FSMs are non-blocking and run every loop; timing is explicit (period checks), not implicit (delay()).

  • Extensibility: add a state or transition without rewriting unrelated parts.

  • Low cross coupling: modules “talk” only through a few shared global signals (bytes, floats, counters), not through deep call chains.

  • Usability: the behavior of each module is easy to explain with a simple UML state chart.


Software layout

The software layout follows a modular state-machine architecture: every essential task runs non-blocking in its own finite state machine (FSM), coded in a separate, reusable file. Each FSM keeps its own local configuration (as const inside the function), uses internal state to handle timing (no delay()), and communicates only through a few shared global signals. The Arduino controller file in the same folder configures the interrupt(s), declares the shared globals, and assembles the system by calling all FSMs in loop().

Controller file

MotorCalib.ino                  // controller, globals, interrupt config, empty setup(), loop() calling all FSMs

FSM Modules

WaveformFSM.ino                 // WaveformFSM_update()
PWMOutputFSM.ino                // PWMOutputFSM_update()
RPM_FSM.ino                     // RPM_FSM_update()
DataLogger_FSM.ino           // UnifiedLogger_FSM_update(...)
UserInput_FSM.ino               // UserInput_FSM_update()
UserOutput_FSM.ino              // UserOutput_FSM_update()
  • Waveform FSM: Generates a sample stream (sine / square / sawtooth / single ramp) at a configured sample period and modulation period; writes sig_waveform_value.

  • PWM Output FSM: Periodically applies sig_waveform_value to the PWM pin with clamp, optional minimum, and slew limiter; writes sig_pwm_applied and increments sig_pwm_update_counter each update.

  • RPM FSM: Counts optical encoder pulses (from ISR) and computes RPM either on a fixed period or synchronized to PWM windows using sig_pwm_update_counter; writes sig_rpm.

  • Data Logger FSM: One logger with two modes—free-run (fixed cadence) or PWM-synchronized—printing selected columns (time, waveform, PWM, RPM) as TSV.

  • User Input FSM: Debounces a pushbutton, samples a potentiometer, and parses simple serial key=value commands; writes sig_input_*.

  • User Output FSM: Drives RGB LEDs and a piezo via tone() (continuous or timed) or digital toggling; consumes sig_led_* and sig_buzzer_*.

How do the parts come together?

  1. Waveform → PWM: The Waveform FSM produces a desired duty (sig_waveform_value).

  2. PWM applies (with safety): PWM FSM clamps, optionally lifts small values, and slew-limits before writing the pin. It also increments sig_pwm_update_counter on each update.

  3. RPM sampling: RPM FSM measures pulses either on a fixed period or in sync with PWM windows (edge-to-edge), then writes sig_rpm.

  4. Logging: Data Logger prints TSV lines either periodically or per PWM window, with selectable columns for time / waveform / PWM / RPM.

  5. User input/output: The User Input FSM updates sig_input_* from the button, pot, and serial. sig_led_* and the buzzer signals are also globally declared, and are driven by User Output FSM. Any state machine can access them.

  6. MotorCalib.ino is the “composer”: it declares the shared global variables, configures and attaches the encoder ISR, leaves setup() empty (FSMs self-init on first call), and in loop() calls the FSMs in order, e.g. Waveform PWM Output RPM Datalogger User Input User Output.

  7. Each FSM is non-blocking and advances via timers and switch(state); they read/write only the documented globals, so files remain independent and reusable across projects.

Tips for Labs & Testing

  • Single-module tests: Comment out others and run one FSM at a time; use the Logger in free-run mode to watch behavior.

  • Traceability: Each FSM’s header in code documents details task, inputs (signals), outputs (signals), usage, tests, and how to extend— explore and study the information in each module.

  • Implement the use case: Set Waveform to SINGLE_RAMP (no repeat) to capture a full PWM→RPM calibration sweep.


Annex 1: Full reference code base

The full reference code base is available for download for course participants on the courses moodle platform, pls check.

Annex 2: Calibration Controller

The calibration controller Arduino file motorCalib.ino is the project’s orchestration point. It owns the shared signals (simple global variables), defines the encoder ISR, and runs all finite-state machines (FSMs) once per loop in a fixed, non-blocking order. Each FSM keeps its own local configuration and internal timing; the controller only wires them together through a few globals.

Tasks

  • Shared globals: declare the small set of cross-module variables (e.g., sig_waveform_value, sig_pwm_applied, sig_pwm_update_counter, sig_encoder_pulses, sig_rpm, user I/O and output intents).

  • Interrupt ownership: define the ISR (increments volatile unsigned long sig_encoder_pulses) and the encoder pin. The attach/detach is performed by the RPM FSM during its INIT state (single source of truth for mode/edge), while the controller remains the canonical place for the ISR symbol and counter.

  • Execution model: cooperative, non-blocking scheduling—every FSM is called once per loop(). Each FSM advances via timers/guards (no delay()), so tasks interleave predictably.

  • Call order (establishes data flow): Waveform PWM Output RPM Datalogger User Input User Output. This ordering ensures:

    • PWM reads the latest waveform sample.

    • RPM can couple its measurement window to PWM updates via sig_pwm_update_counter.

    • The Datalogger prints values computed this cycle.

    • User input/output are applied every cycle without blocking others.

Timing & determinism

  • All timing is explicit (period checks inside each FSM). Loop frequency can vary; behavior does not.

  • The only asynchronous source is the encoder ISR; access its counter with volatile and brief critical sections (already handled inside the RPM FSM).

Code: motorCalib.ino

Annex 3: Global signals

These are the only cross-module variables. Each FSM reads and/or writes them.

Signal

Type

Producer(s)

Consumer(s)

Meaning

sig_waveform_value

byte

Waveform FSM

PWM Output, Logger

Desired duty sample (0..255)

sig_pwm_applied

byte

PWM Output

Logger

Actually written PWM (0..255)

sig_pwm_update_counter

unsigned long

PWM Output

RPM FSM, Logger

Increments each time PWM is updated

sig_encoder_pulses

volatile unsigned long

ISR

RPM FSM

Rising-edge pulse counter from slot sensor

sig_rpm

float

RPM FSM

Logger

Latest RPM estimate

sig_input_button

byte

User Input

Any

Debounced button (0/1)

sig_input_pot

int

User Input

Any

Potentiometer ADC (0..1023)

sig_input_key

char

User Input

Any

Last serial key

sig_input_value

long

User Input

Any

Last serial value

sig_input_has_cmd

bool

User Input

Any

Latched “new command available”

sig_led_r/g/b

byte

Any

User Output

RGB LED on/off commands

sig_buzzer_tone_enable

bool

Any

User Output

Use tone() if true

sig_buzzer_tone_freq_hz

unsigned int

Any

User Output

Tone frequency

sig_buzzer_tone_ms

unsigned int

Any

User Output

0=continuous, else one-shot duration

sig_buzzer_toggle_enable

bool

Any

User Output

Use digital toggling if true and tone disabled

sig_buzzer_toggle_period_ms

unsigned int

Any

User Output

Half-period for toggling


Annex 4: FSM modules explained

Below is a UML (unified model language) diagram by FSM (states and transitions). Note the strict correspondence between the diagram and the code.

The list is started with a generic, educative example of a reference FSM, and then followed by the detailed description of the FSMs used.

Reference FSM (generic example)

Task: este exemplo genérico de máquina de estados com três estados — INIT, STATE1, STATE2 — ilustra um ciclo típico: inicializar, operar num primeiro modo, alternar para um segundo modo mediante um evento/condição, e regressar.

        stateDiagram-v2
  [*] --> INIT
  INIT --> STATE1 : event/condition_1 (p.ex., "inicialização concluída")
  STATE1 --> STATE2 : event/condition_2 (p.ex., "temporizador1 expirou" OU "botão premido")
  STATE1 --> STATE1 : else (permanece até ocorrer event/condition_2)
  STATE2 --> STATE1 : event/condition_3 (p.ex., "temporizador2 expirou" OU "valor abaixo do limiar")
  STATE2 --> STATE2 : else (permanece até ocorrer event/condition_3)
    

Events/transition conditions:

  • event/condition_1: sistema pronto (ex.: configuração feita).

  • event/condition_2: condição para mudar de STATE1 para STATE2 (ex.: temporizador1, entrada externa, limiar atingido).

  • event/condition_3: condição para regressar de STATE2 para STATE1 (ex.: temporizador2, entrada externa, limiar desfeito).

Activities:

  • INIT: preparar variáveis/recursos; marcar sistema como pronto.

  • STATE1: executar atividade A (ex.: atualizar saída/monitorizar entrada com período T1).

  • STATE2: executar atividade B (ex.: outra estratégia/saída com período T2).

Code (Arduino/C++; não bloqueante, educativo)

// ---------------- Optional shared signal for demos ----------------
byte sig_ref_state = 0;  // 0=INIT, 1=STATE1, 2=STATE2

// ---------------- Three-state reference FSM ----------------------
void ReferenceFSM_update() {
  // -------- Local configuration constants (tunable) --------------
  const unsigned int  PERIOD_STATE1_MS = 250;   // drives event/condition_2 (example)
  const unsigned int  PERIOD_STATE2_MS = 500;   // drives event/condition_3 (example)

  // -------- Local state constants (UPPERCASE) --------------------
  const byte INIT   = 0;
  const byte STATE1 = 1;
  const byte STATE2 = 2;

  // -------- Local static variables -------------------------------
  static byte          state = INIT;
  static unsigned long t1_ms = 0;   // timer for STATE1
  static unsigned long t2_ms = 0;   // timer for STATE2
  static bool          ready = false; // models event/condition_1

  // -------- Time base --------------------------------------------
  unsigned long now = millis();

  // -------- FSM ---------------------------------------------------
  switch (state) {

    case INIT: {
      // Activities: initialize resources/vars
      ready = true;                  // event/condition_1 satisfied
      sig_ref_state = 0;

      // Transition: INIT -> STATE1 on event/condition_1
      if (ready) {
        t1_ms = now;                 // reset STATE1 timer
        state = STATE1;
      }
      break;
    }

    case STATE1: {
      // Activities: do-A (periodic action at PERIOD_STATE1_MS)
      sig_ref_state = 1;

      // Example periodic activity (replace with your own):
      if ((unsigned long)(now - t1_ms) >= PERIOD_STATE1_MS) {
        // --- event/condition_2 occurs here (e.g., timer1 expired) ---
        t2_ms = now;                 // prepare STATE2 timer
        state = STATE2;              // Transition: STATE1 -> STATE2
      }
      // else: remain in STATE1
      break;
    }

    case STATE2: {
      // Activities: do-B (periodic action at PERIOD_STATE2_MS)
      sig_ref_state = 2;

      // Example periodic activity (replace with your own):
      if ((unsigned long)(now - t2_ms) >= PERIOD_STATE2_MS) {
        // --- event/condition_3 occurs here (e.g., timer2 expired) ---
        t1_ms = now;                 // prepare STATE1 timer
        state = STATE1;              // Transition: STATE2 -> STATE1
      }
      // else: remain in STATE2
      break;
    }

    default: {
      // Safety net
      state = INIT;
      break;
    }
  } // switch
}

Uso: chame ReferenceFSM_update() em cada iteração de loop(). Adaptação: substitua os temporizadores por entradas reais (botões/sensores) para materializar event/condition_2 e event/condition_3.

Waveform Generator FSM

Task: “Tick at a steady pace and compute the next point of the chosen wave.”

        stateDiagram-v2
  [*] --> INIT
  INIT --> WAIT_TICK : after setup
  WAIT_TICK --> COMPUTE : every sample period
  COMPUTE --> WAIT_TICK : periodic waves (sine/square/saw) or ramp still running
  COMPUTE --> HOLD : single ramp finished AND not repeating
  HOLD --> WAIT_TICK : if restarted


    

Events: timer elapsed. Activities: compute waveform from phase = (now - t0) % MOD_PERIOD.

Code:

void WaveformFSM_update()


PWM Output FSM

Task: “At a fixed pace, update PWM for the motor; and limit how fast it changes.”

        stateDiagram-v2
  [*] --> INIT
  INIT --> WAIT_TICK : after setup
  WAIT_TICK --> UPDATE : every PWM update period
  UPDATE --> WAIT_TICK : after writing PWM (with clamps & slew)

    

Events: timer elapsed. Activities: enforce MAX_DUTY, optional min, slew limit in logical domain; write PWM & signals.

Code:

PWMOutputFSM_update()


RPM FSM

Task: “Open a measurement window, count pulses, compute RPM; windows can be timed or aligned with PWM updates.”

        stateDiagram-v2
  [*] --> INIT
  INIT --> IDLE : after setup
  IDLE --> START_WINDOW : MODE=coupled AND PWM updated
  IDLE --> START_WINDOW : MODE=free-run AND timer elapsed
  START_WINDOW --> END_WINDOW : window started
  END_WINDOW --> COMPUTE : MODE=coupled AND next PWM update
  END_WINDOW --> COMPUTE : MODE=free-run AND window time elapsed
  COMPUTE --> IDLE : after RPM computed

    

Events: PWM edge via sig_pwm_update_counter (coupled) or periodic timer (free-run). Activities: snapshot or reset pulse counter; compute RPM (guard for very short dt).

Code:

void RPM_FSM_update()


Data Logger FSM

Task: “Either log on a timer, or once per PWM window—only the columns you enabled.”

        stateDiagram-v2
  [*] --> INIT
  INIT --> IDLE : after setup
  IDLE --> LOG : MODE=coupled AND PWM updated
  IDLE --> LOG : MODE=free-run AND log period elapsed
  LOG --> IDLE : after one line printed

    

Events: PWM edge or fixed timer; Activities: print selected fields; reprint header if layout changes.

Code: void DataLogger_FSM_update()


User Input FSM

Task: “Continuously read button and knob; when a full serial line arrives, parse key/value.”

        stateDiagram-v2
  [*] --> INIT
  INIT --> WAIT_TICK : after setup
  WAIT_TICK --> SCAN_INPUTS : serial data available
  SCAN_INPUTS --> PARSE_LINE : end-of-line reached
  SCAN_INPUTS --> WAIT_TICK : no complete line yet
  PARSE_LINE --> WAIT_TICK : after key/value latched

    

Events: debounce timeout, sample timer, serial EOL. Activities: normalize active-low button to 0/1; capture ADC; parse char=value.

Code:

void UserInput_FSM_update()


User Output FSM

Task: “At a steady pace, drive LEDs; make sound via tone() or simple on/off toggling.”

        stateDiagram-v2
  [*] --> INIT
  INIT --> WAIT_TICK : after setup
  WAIT_TICK --> APPLY : every output update period
  APPLY --> WAIT_TICK : after LEDs/buzzer refreshed

    

Events: timer elapsed. Activities: tone() continuous/one-shot, or software toggle; active-high/low LED handling.

Code:

void UserOutput_FSM_update()