Completed part 3 of project 3
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
@startuml
|
||||
|
||||
[*] --> Initialization
|
||||
|
||||
state Initialization
|
||||
state Operational
|
||||
|
||||
Initialization : entry / initialize parameters
|
||||
Initialization : entry / send Boot-up
|
||||
Initialization : do / wait for command
|
||||
|
||||
Operational : entry / enable motor control
|
||||
Operational : entry / LED ON
|
||||
Operational : do / update speed controller at 250 Hz
|
||||
Operational : exit / stop motor control
|
||||
Operational : exit / LED OFF
|
||||
|
||||
Initialization --> Operational : set operational / 'o'
|
||||
Operational --> Initialization : reset / 'r'
|
||||
|
||||
@enduml
|
||||
@@ -0,0 +1,30 @@
|
||||
@startuml
|
||||
|
||||
[*] --> Initialization
|
||||
|
||||
state Initialization
|
||||
state Operational
|
||||
state Stopped
|
||||
|
||||
Initialization : entry / initialize parameters
|
||||
Initialization : entry / send Boot-up
|
||||
Initialization : do / wait for command
|
||||
|
||||
Operational : entry / enable motor control
|
||||
Operational : entry / LED ON
|
||||
Operational : do / update speed controller at 250 Hz
|
||||
Operational : exit / stop motor control
|
||||
Operational : exit / LED OFF
|
||||
|
||||
Stopped : entry / brake motor
|
||||
Stopped : entry / disable normal controller activity
|
||||
Stopped : do / LED blink at 2 Hz
|
||||
Stopped : exit / LED OFF
|
||||
|
||||
Initialization --> Operational : set operational / 'o'
|
||||
Operational --> Initialization : reset / 'r'
|
||||
|
||||
Operational --> Stopped : emergency stop / fault
|
||||
Stopped --> Initialization : reset / 'r'
|
||||
|
||||
@enduml
|
||||
@@ -0,0 +1,23 @@
|
||||
# Part 3 - PI Controller TDD
|
||||
|
||||
## Normal flows
|
||||
|
||||
1. Zero error produces zero output.
|
||||
2. Positive error produces positive control output.
|
||||
3. Negative error produces negative control output.
|
||||
4. Constant error accumulates integral action.
|
||||
5. The PI controller operates through the common Controller interface.
|
||||
|
||||
## Alternate flows
|
||||
|
||||
1. Positive output is limited to +255.
|
||||
2. Negative output is limited to -255.
|
||||
3. Positive saturation does not cause integral windup.
|
||||
4. Negative saturation does not cause integral windup.
|
||||
5. The controller recovers correctly after saturation.
|
||||
6. Reset clears the accumulated integral.
|
||||
|
||||
The tests were written and executed before the PI control law was
|
||||
implemented. The initial run produced the expected failing tests.
|
||||
After implementing the PI control law and anti-windup behavior,
|
||||
the same test suite passed all 11 tests.
|
||||
@@ -0,0 +1,37 @@
|
||||
|
||||
This directory is intended for project header files.
|
||||
|
||||
A header file is a file containing C declarations and macro definitions
|
||||
to be shared between several project source files. You request the use of a
|
||||
header file in your project source file (C, C++, etc) located in `src` folder
|
||||
by including it, with the C preprocessing directive `#include'.
|
||||
|
||||
```src/main.c
|
||||
|
||||
#include "header.h"
|
||||
|
||||
int main (void)
|
||||
{
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Including a header file produces the same results as copying the header file
|
||||
into each source file that needs it. Such copying would be time-consuming
|
||||
and error-prone. With a header file, the related declarations appear
|
||||
in only one place. If they need to be changed, they can be changed in one
|
||||
place, and programs that include the header file will automatically use the
|
||||
new version when next recompiled. The header file eliminates the labor of
|
||||
finding and changing all the copies as well as the risk that a failure to
|
||||
find one copy will result in inconsistencies within a program.
|
||||
|
||||
In C, the convention is to give header files names that end with `.h'.
|
||||
|
||||
Read more about using header files in official GCC documentation:
|
||||
|
||||
* Include Syntax
|
||||
* Include Operation
|
||||
* Once-Only Headers
|
||||
* Computed Includes
|
||||
|
||||
https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html
|
||||
@@ -0,0 +1,26 @@
|
||||
#ifndef CONTEXT_H
|
||||
#define CONTEXT_H
|
||||
|
||||
#include <state.h>
|
||||
|
||||
class Context
|
||||
{
|
||||
public:
|
||||
Context(State& initialState);
|
||||
|
||||
void transition_to(State& newState);
|
||||
|
||||
void do_work();
|
||||
|
||||
void reset();
|
||||
void set_operational();
|
||||
void set_pre_operational();
|
||||
void fault();
|
||||
|
||||
const char* current_state_name() const;
|
||||
|
||||
private:
|
||||
State* state;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,17 @@
|
||||
#ifndef CONTROL_TIMER_H
|
||||
#define CONTROL_TIMER_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
class Control_timer
|
||||
{
|
||||
public:
|
||||
void init();
|
||||
|
||||
// Returns true once for every 4 ms control tick.
|
||||
bool take_tick();
|
||||
|
||||
uint32_t tick_count() const;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,19 @@
|
||||
#ifndef CONTROLLER_H
|
||||
#define CONTROLLER_H
|
||||
|
||||
class Controller
|
||||
{
|
||||
public:
|
||||
virtual ~Controller() = default;
|
||||
|
||||
virtual double update(
|
||||
double reference,
|
||||
double actual
|
||||
) = 0;
|
||||
|
||||
virtual void reset() = 0;
|
||||
|
||||
virtual const char* name() const = 0;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,22 @@
|
||||
#ifndef DIGITAL_IN_H
|
||||
#define DIGITAL_IN_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
class Digital_in
|
||||
{
|
||||
public:
|
||||
// pin is the PORTB bit number:
|
||||
// PB0 = D8, PB1 = D9, ..., PB5 = D13
|
||||
Digital_in(uint8_t pin);
|
||||
|
||||
void init();
|
||||
|
||||
bool is_hi() const;
|
||||
bool is_lo() const;
|
||||
|
||||
private:
|
||||
uint8_t pinMask;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,23 @@
|
||||
#ifndef DIGITAL_OUT_H
|
||||
#define DIGITAL_OUT_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
class Digital_out
|
||||
{
|
||||
public:
|
||||
// pin is the PORTB bit number:
|
||||
// PB0 = D8, ..., PB5 = D13
|
||||
Digital_out(uint8_t pin);
|
||||
|
||||
void init();
|
||||
|
||||
void set_hi();
|
||||
void set_lo();
|
||||
void toggle();
|
||||
|
||||
private:
|
||||
uint8_t pinMask;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,28 @@
|
||||
#ifndef ENCODER_H
|
||||
#define ENCODER_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <digital_in.h>
|
||||
|
||||
class Encoder
|
||||
{
|
||||
public:
|
||||
Encoder(uint8_t c1Pin, uint8_t c2Pin);
|
||||
|
||||
void init();
|
||||
void update();
|
||||
|
||||
int32_t position() const;
|
||||
|
||||
private:
|
||||
Digital_in c1;
|
||||
Digital_in c2;
|
||||
|
||||
uint8_t c1Pin;
|
||||
|
||||
bool previousC1;
|
||||
|
||||
volatile int32_t count;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,35 @@
|
||||
#ifndef INITIALIZATION_H
|
||||
#define INITIALIZATION_H
|
||||
|
||||
#include <state.h>
|
||||
#include <speed_controller.h>
|
||||
#include <digital_out.h>
|
||||
|
||||
class Initialization : public State
|
||||
{
|
||||
public:
|
||||
Initialization(
|
||||
Speed_controller& speedController,
|
||||
Digital_out& statusLED
|
||||
);
|
||||
|
||||
const char* name() const override;
|
||||
|
||||
void on_entry() override;
|
||||
void on_do() override;
|
||||
void on_exit() override;
|
||||
|
||||
void set_operational() override;
|
||||
|
||||
void set_operational_state(
|
||||
State& operationalState
|
||||
);
|
||||
|
||||
private:
|
||||
Speed_controller& speedController;
|
||||
Digital_out& statusLED;
|
||||
|
||||
State* operationalState;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,26 @@
|
||||
#ifndef MOTOR_DRIVER_H
|
||||
#define MOTOR_DRIVER_H
|
||||
|
||||
class Motor_driver
|
||||
{
|
||||
public:
|
||||
void init();
|
||||
|
||||
// Range: -255 ... +255
|
||||
void set_command(double command);
|
||||
|
||||
void coast();
|
||||
|
||||
void brake();
|
||||
|
||||
double command() const;
|
||||
|
||||
private:
|
||||
double currentCommand;
|
||||
|
||||
void enable_pwm_outputs();
|
||||
|
||||
unsigned char magnitude(double value) const;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,45 @@
|
||||
#ifndef OPERATIONAL_H
|
||||
#define OPERATIONAL_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <state.h>
|
||||
#include <speed_controller.h>
|
||||
#include <digital_out.h>
|
||||
|
||||
class Operational : public State
|
||||
{
|
||||
public:
|
||||
Operational(
|
||||
Speed_controller& speedController,
|
||||
Digital_out& statusLED
|
||||
);
|
||||
|
||||
const char* name() const override;
|
||||
|
||||
void on_entry() override;
|
||||
void on_do() override;
|
||||
void on_exit() override;
|
||||
|
||||
void reset() override;
|
||||
void fault() override;
|
||||
|
||||
void set_initialization_state(
|
||||
State& initializationState
|
||||
);
|
||||
|
||||
void set_stopped_state(
|
||||
State& stoppedState
|
||||
);
|
||||
|
||||
private:
|
||||
Speed_controller& speedController;
|
||||
Digital_out& statusLED;
|
||||
|
||||
State* initializationState;
|
||||
State* stoppedState;
|
||||
|
||||
uint8_t logCounter;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,29 @@
|
||||
#ifndef P_CONTROLLER_H
|
||||
#define P_CONTROLLER_H
|
||||
|
||||
#include <controller.h>
|
||||
|
||||
class P_controller : public Controller
|
||||
{
|
||||
public:
|
||||
P_controller(double kp);
|
||||
|
||||
double update(
|
||||
double reference,
|
||||
double actual
|
||||
) override;
|
||||
|
||||
void reset() override;
|
||||
|
||||
const char* name() const override;
|
||||
|
||||
void set_kp(double kp);
|
||||
double get_kp() const;
|
||||
|
||||
private:
|
||||
double kp;
|
||||
|
||||
double limit(double value) const;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,42 @@
|
||||
#ifndef PI_CONTROLLER_H
|
||||
#define PI_CONTROLLER_H
|
||||
|
||||
#include <controller.h>
|
||||
|
||||
class PI_controller : public Controller
|
||||
{
|
||||
public:
|
||||
PI_controller(
|
||||
double kp,
|
||||
double ti,
|
||||
double timestep
|
||||
);
|
||||
|
||||
double update(
|
||||
double reference,
|
||||
double actual
|
||||
) override;
|
||||
|
||||
void reset() override;
|
||||
|
||||
const char* name() const override;
|
||||
|
||||
void set_kp(double kp);
|
||||
double get_kp() const;
|
||||
|
||||
void set_ti(double ti);
|
||||
double get_ti() const;
|
||||
|
||||
double get_timestep() const;
|
||||
|
||||
private:
|
||||
double kp;
|
||||
double ti;
|
||||
double timestep;
|
||||
|
||||
double integral;
|
||||
|
||||
double limit(double value) const;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,47 @@
|
||||
#ifndef SPEED_CONTROLLER_H
|
||||
#define SPEED_CONTROLLER_H
|
||||
|
||||
#include <encoder.h>
|
||||
#include <speed_estimator.h>
|
||||
#include <motor_driver.h>
|
||||
#include <controller.h>
|
||||
|
||||
class Speed_controller
|
||||
{
|
||||
public:
|
||||
Speed_controller(
|
||||
Encoder& encoder,
|
||||
Speed_estimator& estimator,
|
||||
Motor_driver& motor,
|
||||
Controller& controller
|
||||
);
|
||||
|
||||
void start();
|
||||
void stop();
|
||||
void brake();
|
||||
|
||||
void update();
|
||||
|
||||
void set_reference(double rpm);
|
||||
double reference() const;
|
||||
|
||||
void set_controller(Controller& controller);
|
||||
|
||||
double actual_rpm() const;
|
||||
double control_output() const;
|
||||
|
||||
private:
|
||||
Encoder& encoder;
|
||||
Speed_estimator& estimator;
|
||||
Motor_driver& motor;
|
||||
|
||||
Controller* controller;
|
||||
|
||||
bool running;
|
||||
|
||||
double referenceRPM;
|
||||
double actualRPM;
|
||||
double controlOutput;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,37 @@
|
||||
#ifndef SPEED_ESTIMATOR_H
|
||||
#define SPEED_ESTIMATOR_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
class Speed_estimator
|
||||
{
|
||||
public:
|
||||
Speed_estimator(
|
||||
double countsPerRevolution,
|
||||
double sampleTimeSeconds
|
||||
);
|
||||
|
||||
void reset(int32_t initialPosition);
|
||||
|
||||
double update(int32_t currentPosition);
|
||||
|
||||
double rpm() const;
|
||||
|
||||
private:
|
||||
static const uint8_t WINDOW_SIZE = 10;
|
||||
|
||||
double countsPerRevolution;
|
||||
double sampleTime;
|
||||
|
||||
int32_t previousPosition;
|
||||
|
||||
int32_t countHistory[WINDOW_SIZE];
|
||||
int32_t rollingCounts;
|
||||
|
||||
uint8_t historyIndex;
|
||||
uint8_t samplesFilled;
|
||||
|
||||
double currentRPM;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,43 @@
|
||||
#ifndef STATE_H
|
||||
#define STATE_H
|
||||
|
||||
class Context;
|
||||
|
||||
class State
|
||||
{
|
||||
protected:
|
||||
Context* context_ = nullptr;
|
||||
|
||||
public:
|
||||
virtual ~State() = default;
|
||||
|
||||
void set_context(Context* context)
|
||||
{
|
||||
context_ = context;
|
||||
}
|
||||
|
||||
virtual const char* name() const = 0;
|
||||
|
||||
virtual void on_entry() = 0;
|
||||
virtual void on_do() = 0;
|
||||
virtual void on_exit() = 0;
|
||||
|
||||
// Events
|
||||
virtual void reset()
|
||||
{
|
||||
}
|
||||
|
||||
virtual void set_operational()
|
||||
{
|
||||
}
|
||||
|
||||
virtual void set_pre_operational()
|
||||
{
|
||||
}
|
||||
|
||||
virtual void fault()
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,39 @@
|
||||
#ifndef STOPPED_H
|
||||
#define STOPPED_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <state.h>
|
||||
#include <speed_controller.h>
|
||||
#include <digital_out.h>
|
||||
|
||||
class Stopped : public State
|
||||
{
|
||||
public:
|
||||
Stopped(
|
||||
Speed_controller& speedController,
|
||||
Digital_out& statusLED
|
||||
);
|
||||
|
||||
const char* name() const override;
|
||||
|
||||
void on_entry() override;
|
||||
void on_do() override;
|
||||
void on_exit() override;
|
||||
|
||||
void reset() override;
|
||||
|
||||
void set_initialization_state(
|
||||
State& initializationState
|
||||
);
|
||||
|
||||
private:
|
||||
Speed_controller& speedController;
|
||||
Digital_out& statusLED;
|
||||
|
||||
State* initializationState;
|
||||
|
||||
uint8_t ledCounter;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,46 @@
|
||||
|
||||
This directory is intended for project specific (private) libraries.
|
||||
PlatformIO will compile them to static libraries and link into the executable file.
|
||||
|
||||
The source code of each library should be placed in a separate directory
|
||||
("lib/your_library_name/[Code]").
|
||||
|
||||
For example, see the structure of the following example libraries `Foo` and `Bar`:
|
||||
|
||||
|--lib
|
||||
| |
|
||||
| |--Bar
|
||||
| | |--docs
|
||||
| | |--examples
|
||||
| | |--src
|
||||
| | |- Bar.c
|
||||
| | |- Bar.h
|
||||
| | |- library.json (optional. for custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
|
||||
| |
|
||||
| |--Foo
|
||||
| | |- Foo.c
|
||||
| | |- Foo.h
|
||||
| |
|
||||
| |- README --> THIS FILE
|
||||
|
|
||||
|- platformio.ini
|
||||
|--src
|
||||
|- main.c
|
||||
|
||||
Example contents of `src/main.c` using Foo and Bar:
|
||||
```
|
||||
#include <Foo.h>
|
||||
#include <Bar.h>
|
||||
|
||||
int main (void)
|
||||
{
|
||||
...
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
The PlatformIO Library Dependency Finder will find automatically dependent
|
||||
libraries by scanning project source files.
|
||||
|
||||
More information about PlatformIO Library Dependency Finder
|
||||
- https://docs.platformio.org/page/librarymanager/ldf.html
|
||||
@@ -13,3 +13,16 @@ platform = atmelavr
|
||||
board = nanoatmega328new
|
||||
framework = arduino
|
||||
monitor_speed = 115200
|
||||
|
||||
|
||||
[env:native]
|
||||
platform = native
|
||||
test_framework = unity
|
||||
test_build_src = yes
|
||||
|
||||
build_flags =
|
||||
-D UNITY_INCLUDE_DOUBLE
|
||||
|
||||
build_src_filter =
|
||||
-<*>
|
||||
+<pi_controller.cpp>
|
||||
@@ -0,0 +1,80 @@
|
||||
#include <Arduino.h>
|
||||
|
||||
#include <context.h>
|
||||
|
||||
Context::Context(State& initialState)
|
||||
: state(nullptr)
|
||||
{
|
||||
transition_to(initialState);
|
||||
}
|
||||
|
||||
void Context::transition_to(
|
||||
State& newState
|
||||
)
|
||||
{
|
||||
if (state != nullptr)
|
||||
{
|
||||
state->on_exit();
|
||||
|
||||
Serial.print("Transition: ");
|
||||
Serial.print(state->name());
|
||||
Serial.print(" -> ");
|
||||
Serial.println(newState.name());
|
||||
}
|
||||
|
||||
state = &newState;
|
||||
|
||||
state->set_context(this);
|
||||
|
||||
state->on_entry();
|
||||
}
|
||||
|
||||
void Context::do_work()
|
||||
{
|
||||
if (state != nullptr)
|
||||
{
|
||||
state->on_do();
|
||||
}
|
||||
}
|
||||
|
||||
void Context::reset()
|
||||
{
|
||||
if (state != nullptr)
|
||||
{
|
||||
state->reset();
|
||||
}
|
||||
}
|
||||
|
||||
void Context::set_operational()
|
||||
{
|
||||
if (state != nullptr)
|
||||
{
|
||||
state->set_operational();
|
||||
}
|
||||
}
|
||||
|
||||
void Context::set_pre_operational()
|
||||
{
|
||||
if (state != nullptr)
|
||||
{
|
||||
state->set_pre_operational();
|
||||
}
|
||||
}
|
||||
|
||||
void Context::fault()
|
||||
{
|
||||
if (state != nullptr)
|
||||
{
|
||||
state->fault();
|
||||
}
|
||||
}
|
||||
|
||||
const char* Context::current_state_name() const
|
||||
{
|
||||
if (state == nullptr)
|
||||
{
|
||||
return "None";
|
||||
}
|
||||
|
||||
return state->name();
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
#include <control_timer.h>
|
||||
|
||||
#include <avr/io.h>
|
||||
#include <avr/interrupt.h>
|
||||
#include <util/atomic.h>
|
||||
|
||||
static volatile uint8_t pendingTicks = 0;
|
||||
static volatile uint32_t totalTicks = 0;
|
||||
|
||||
ISR(TIMER1_COMPA_vect)
|
||||
{
|
||||
totalTicks++;
|
||||
|
||||
if (pendingTicks < 255)
|
||||
{
|
||||
pendingTicks++;
|
||||
}
|
||||
}
|
||||
|
||||
void Control_timer::init()
|
||||
{
|
||||
pendingTicks = 0;
|
||||
totalTicks = 0;
|
||||
|
||||
// Timer1 CTC mode, 250 Hz
|
||||
// 16 MHz / 64 / (999 + 1) = 250 Hz
|
||||
|
||||
TCCR1A = 0;
|
||||
TCCR1B = 0;
|
||||
TCNT1 = 0;
|
||||
|
||||
OCR1A = 999;
|
||||
|
||||
TCCR1B |= (1 << WGM12);
|
||||
|
||||
// Prescaler = 64
|
||||
TCCR1B |=
|
||||
(1 << CS11) |
|
||||
(1 << CS10);
|
||||
|
||||
TIMSK1 |= (1 << OCIE1A);
|
||||
}
|
||||
|
||||
bool Control_timer::take_tick()
|
||||
{
|
||||
bool tickAvailable = false;
|
||||
|
||||
ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
|
||||
{
|
||||
if (pendingTicks > 0)
|
||||
{
|
||||
pendingTicks--;
|
||||
tickAvailable = true;
|
||||
}
|
||||
}
|
||||
|
||||
return tickAvailable;
|
||||
}
|
||||
|
||||
uint32_t Control_timer::tick_count() const
|
||||
{
|
||||
uint32_t ticks;
|
||||
|
||||
ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
|
||||
{
|
||||
ticks = totalTicks;
|
||||
}
|
||||
|
||||
return ticks;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
#include <digital_in.h>
|
||||
#include <avr/io.h>
|
||||
|
||||
Digital_in::Digital_in(uint8_t pin)
|
||||
{
|
||||
pinMask = static_cast<uint8_t>(1U << pin);
|
||||
}
|
||||
|
||||
void Digital_in::init()
|
||||
{
|
||||
// Input
|
||||
DDRB &= static_cast<uint8_t>(~pinMask);
|
||||
|
||||
// Internal pull-up
|
||||
PORTB |= pinMask;
|
||||
}
|
||||
|
||||
bool Digital_in::is_hi() const
|
||||
{
|
||||
return (PINB & pinMask) != 0;
|
||||
}
|
||||
|
||||
bool Digital_in::is_lo() const
|
||||
{
|
||||
return (PINB & pinMask) == 0;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
#include <digital_out.h>
|
||||
#include <avr/io.h>
|
||||
|
||||
Digital_out::Digital_out(uint8_t pin)
|
||||
{
|
||||
pinMask = static_cast<uint8_t>(1U << pin);
|
||||
}
|
||||
|
||||
void Digital_out::init()
|
||||
{
|
||||
DDRB |= pinMask;
|
||||
}
|
||||
|
||||
void Digital_out::set_hi()
|
||||
{
|
||||
PORTB |= pinMask;
|
||||
}
|
||||
|
||||
void Digital_out::set_lo()
|
||||
{
|
||||
PORTB &= static_cast<uint8_t>(~pinMask);
|
||||
}
|
||||
|
||||
void Digital_out::toggle()
|
||||
{
|
||||
PORTB ^= pinMask;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
#include <encoder.h>
|
||||
|
||||
#include <avr/io.h>
|
||||
#include <avr/interrupt.h>
|
||||
|
||||
static Encoder* activeEncoder = nullptr;
|
||||
|
||||
ISR(PCINT0_vect)
|
||||
{
|
||||
if (activeEncoder != nullptr)
|
||||
{
|
||||
activeEncoder->update();
|
||||
}
|
||||
}
|
||||
|
||||
Encoder::Encoder(uint8_t c1Pin, uint8_t c2Pin)
|
||||
: c1(c1Pin),
|
||||
c2(c2Pin),
|
||||
c1Pin(c1Pin),
|
||||
previousC1(false),
|
||||
count(0)
|
||||
{
|
||||
}
|
||||
|
||||
void Encoder::init()
|
||||
{
|
||||
c1.init();
|
||||
c2.init();
|
||||
|
||||
activeEncoder = this;
|
||||
|
||||
previousC1 = c1.is_hi();
|
||||
count = 0;
|
||||
|
||||
// Clear pending PORTB pin-change interrupt
|
||||
PCIFR |= (1 << PCIF0);
|
||||
|
||||
// Enable PORTB pin-change interrupts
|
||||
PCICR |= (1 << PCIE0);
|
||||
|
||||
// Enable interrupt on C1
|
||||
PCMSK0 |= (1 << c1Pin);
|
||||
}
|
||||
|
||||
void Encoder::update()
|
||||
{
|
||||
bool currentC1 = c1.is_hi();
|
||||
|
||||
if (currentC1 != previousC1)
|
||||
{
|
||||
bool currentC2 = c2.is_hi();
|
||||
|
||||
if (currentC1 == currentC2)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
else
|
||||
{
|
||||
count--;
|
||||
}
|
||||
|
||||
previousC1 = currentC1;
|
||||
}
|
||||
}
|
||||
|
||||
int32_t Encoder::position() const
|
||||
{
|
||||
uint8_t oldSREG = SREG;
|
||||
|
||||
cli();
|
||||
int32_t currentCount = count;
|
||||
SREG = oldSREG;
|
||||
|
||||
return currentCount;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
#include <Arduino.h>
|
||||
|
||||
#include <initialization.h>
|
||||
#include <context.h>
|
||||
|
||||
Initialization::Initialization(
|
||||
Speed_controller& speedController,
|
||||
Digital_out& statusLED
|
||||
)
|
||||
: speedController(speedController),
|
||||
statusLED(statusLED),
|
||||
operationalState(nullptr)
|
||||
{
|
||||
}
|
||||
|
||||
const char* Initialization::name() const
|
||||
{
|
||||
return "Initialization";
|
||||
}
|
||||
|
||||
void Initialization::on_entry()
|
||||
{
|
||||
speedController.stop();
|
||||
|
||||
statusLED.set_lo();
|
||||
|
||||
Serial.println();
|
||||
Serial.println("Initialization");
|
||||
Serial.println("Boot-up");
|
||||
}
|
||||
|
||||
void Initialization::on_do()
|
||||
{
|
||||
// No continuous activity required
|
||||
// in Part 1 Initialization.
|
||||
}
|
||||
|
||||
void Initialization::on_exit()
|
||||
{
|
||||
}
|
||||
|
||||
void Initialization::set_operational()
|
||||
{
|
||||
if (operationalState != nullptr)
|
||||
{
|
||||
context_->transition_to(
|
||||
*operationalState
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void Initialization::set_operational_state(
|
||||
State& state
|
||||
)
|
||||
{
|
||||
operationalState = &state;
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
#include <Arduino.h>
|
||||
|
||||
#include <digital_in.h>
|
||||
#include <digital_out.h>
|
||||
#include <encoder.h>
|
||||
|
||||
#include <control_timer.h>
|
||||
#include <speed_estimator.h>
|
||||
|
||||
#include <motor_driver.h>
|
||||
|
||||
#include <p_controller.h>
|
||||
#include <speed_controller.h>
|
||||
|
||||
#include <context.h>
|
||||
#include <initialization.h>
|
||||
#include <operational.h>
|
||||
#include <stopped.h>
|
||||
|
||||
// --------------------------------------------------
|
||||
// Hardware / drivers
|
||||
// --------------------------------------------------
|
||||
|
||||
// Encoder:
|
||||
// PB0 = D8
|
||||
// PB1 = D9
|
||||
Encoder encoder(0, 1);
|
||||
|
||||
// Emergency-stop:
|
||||
// PB2 = D10
|
||||
// Internal pull-up enabled.
|
||||
// LOW = emergency stop pressed.
|
||||
Digital_in emergencyStop(2);
|
||||
|
||||
// Status LED:
|
||||
// PB5 = D13
|
||||
Digital_out statusLED(5);
|
||||
|
||||
Control_timer controlTimer;
|
||||
|
||||
Motor_driver motorDriver;
|
||||
|
||||
// 2100 counts/revolution,
|
||||
// 4 ms fixed controller period
|
||||
Speed_estimator speedEstimator(
|
||||
2100.0,
|
||||
0.004
|
||||
);
|
||||
|
||||
P_controller pController(40.0);
|
||||
|
||||
Speed_controller speedController(
|
||||
encoder,
|
||||
speedEstimator,
|
||||
motorDriver,
|
||||
pController
|
||||
);
|
||||
|
||||
// --------------------------------------------------
|
||||
// State machine
|
||||
// --------------------------------------------------
|
||||
|
||||
Initialization initialization(
|
||||
speedController,
|
||||
statusLED
|
||||
);
|
||||
|
||||
Operational operational(
|
||||
speedController,
|
||||
statusLED
|
||||
);
|
||||
|
||||
Stopped stopped(
|
||||
speedController,
|
||||
statusLED
|
||||
);
|
||||
|
||||
Context* stateMachine = nullptr;
|
||||
|
||||
void setup()
|
||||
{
|
||||
Serial.begin(115200);
|
||||
|
||||
statusLED.init();
|
||||
|
||||
emergencyStop.init();
|
||||
|
||||
encoder.init();
|
||||
|
||||
motorDriver.init();
|
||||
|
||||
speedController.set_reference(
|
||||
50.0
|
||||
);
|
||||
|
||||
// --------------------------------------------------
|
||||
// State transitions
|
||||
// --------------------------------------------------
|
||||
|
||||
initialization.set_operational_state(
|
||||
operational
|
||||
);
|
||||
|
||||
operational.set_initialization_state(
|
||||
initialization
|
||||
);
|
||||
|
||||
operational.set_stopped_state(
|
||||
stopped
|
||||
);
|
||||
|
||||
stopped.set_initialization_state(
|
||||
initialization
|
||||
);
|
||||
|
||||
static Context context(
|
||||
initialization
|
||||
);
|
||||
|
||||
stateMachine = &context;
|
||||
|
||||
controlTimer.init();
|
||||
|
||||
Serial.println();
|
||||
Serial.println("Project 3 - Part 2");
|
||||
Serial.println("Commands:");
|
||||
Serial.println("o = set operational");
|
||||
Serial.println("r = reset");
|
||||
Serial.println("D10 LOW = emergency stop");
|
||||
}
|
||||
|
||||
void loop()
|
||||
{
|
||||
// --------------------------------------------------
|
||||
// Emergency-stop fault
|
||||
// --------------------------------------------------
|
||||
|
||||
if (emergencyStop.is_lo())
|
||||
{
|
||||
stateMachine->fault();
|
||||
}
|
||||
|
||||
// --------------------------------------------------
|
||||
// Serial commands
|
||||
// --------------------------------------------------
|
||||
|
||||
if (Serial.available() > 0)
|
||||
{
|
||||
char command =
|
||||
Serial.read();
|
||||
|
||||
if (command == 'o')
|
||||
{
|
||||
stateMachine->
|
||||
set_operational();
|
||||
}
|
||||
else if (command == 'r')
|
||||
{
|
||||
stateMachine->
|
||||
reset();
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------
|
||||
// Stable 250 Hz state activity
|
||||
// --------------------------------------------------
|
||||
|
||||
if (controlTimer.take_tick())
|
||||
{
|
||||
stateMachine->do_work();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
#include <motor_driver.h>
|
||||
|
||||
#include <avr/io.h>
|
||||
|
||||
void Motor_driver::init()
|
||||
{
|
||||
currentCommand = 0.0;
|
||||
|
||||
// D11 / PB3 / OC2A -> AIN1
|
||||
DDRB |= (1 << PB3);
|
||||
|
||||
// D3 / PD3 / OC2B -> AIN2
|
||||
DDRD |= (1 << PD3);
|
||||
|
||||
PORTB &= ~(1 << PB3);
|
||||
PORTD &= ~(1 << PD3);
|
||||
|
||||
// Timer2 Fast PWM
|
||||
TCCR2A = 0;
|
||||
TCCR2B = 0;
|
||||
TCNT2 = 0;
|
||||
|
||||
OCR2A = 0;
|
||||
OCR2B = 0;
|
||||
|
||||
TCCR2A =
|
||||
(1 << COM2A1) |
|
||||
(1 << COM2B1) |
|
||||
(1 << WGM21) |
|
||||
(1 << WGM20);
|
||||
|
||||
// Prescaler = 8
|
||||
// PWM frequency ~= 7.8 kHz
|
||||
TCCR2B =
|
||||
(1 << CS21);
|
||||
}
|
||||
|
||||
void Motor_driver::enable_pwm_outputs()
|
||||
{
|
||||
// Non-inverting PWM on OC2A and OC2B
|
||||
TCCR2A |=
|
||||
(1 << COM2A1) |
|
||||
(1 << COM2B1);
|
||||
|
||||
TCCR2A &=
|
||||
~((1 << COM2A0) |
|
||||
(1 << COM2B0));
|
||||
}
|
||||
|
||||
unsigned char Motor_driver::magnitude(
|
||||
double value
|
||||
) const
|
||||
{
|
||||
if (value < 0.0)
|
||||
{
|
||||
value = -value;
|
||||
}
|
||||
|
||||
if (value > 255.0)
|
||||
{
|
||||
value = 255.0;
|
||||
}
|
||||
|
||||
return static_cast<unsigned char>(value);
|
||||
}
|
||||
|
||||
void Motor_driver::set_command(double command)
|
||||
{
|
||||
if (command > 255.0)
|
||||
{
|
||||
command = 255.0;
|
||||
}
|
||||
|
||||
if (command < -255.0)
|
||||
{
|
||||
command = -255.0;
|
||||
}
|
||||
|
||||
currentCommand = command;
|
||||
|
||||
enable_pwm_outputs();
|
||||
|
||||
unsigned char pwm =
|
||||
magnitude(command);
|
||||
|
||||
if (command > 0.0)
|
||||
{
|
||||
// Same direction that previously gave positive RPM:
|
||||
// AIN1 = LOW
|
||||
// AIN2 = PWM
|
||||
OCR2A = 0;
|
||||
OCR2B = pwm;
|
||||
}
|
||||
else if (command < 0.0)
|
||||
{
|
||||
OCR2A = pwm;
|
||||
OCR2B = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
OCR2A = 0;
|
||||
OCR2B = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void Motor_driver::coast()
|
||||
{
|
||||
currentCommand = 0.0;
|
||||
|
||||
enable_pwm_outputs();
|
||||
|
||||
OCR2A = 0;
|
||||
OCR2B = 0;
|
||||
}
|
||||
|
||||
void Motor_driver::brake()
|
||||
{
|
||||
currentCommand = 0.0;
|
||||
|
||||
// Disconnect Timer2 PWM from the pins.
|
||||
TCCR2A &=
|
||||
~((1 << COM2A1) |
|
||||
(1 << COM2A0) |
|
||||
(1 << COM2B1) |
|
||||
(1 << COM2B0));
|
||||
|
||||
// DRV8833: both inputs HIGH gives braking.
|
||||
PORTB |= (1 << PB3);
|
||||
PORTD |= (1 << PD3);
|
||||
}
|
||||
|
||||
double Motor_driver::command() const
|
||||
{
|
||||
return currentCommand;
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
#include <Arduino.h>
|
||||
|
||||
#include <operational.h>
|
||||
#include <context.h>
|
||||
|
||||
Operational::Operational(
|
||||
Speed_controller& speedController,
|
||||
Digital_out& statusLED
|
||||
)
|
||||
: speedController(speedController),
|
||||
statusLED(statusLED),
|
||||
initializationState(nullptr),
|
||||
stoppedState(nullptr),
|
||||
logCounter(0)
|
||||
{
|
||||
}
|
||||
|
||||
const char* Operational::name() const
|
||||
{
|
||||
return "Operational";
|
||||
}
|
||||
|
||||
void Operational::on_entry()
|
||||
{
|
||||
Serial.println("Operational");
|
||||
|
||||
statusLED.set_hi();
|
||||
|
||||
logCounter = 0;
|
||||
|
||||
speedController.start();
|
||||
}
|
||||
|
||||
void Operational::on_do()
|
||||
{
|
||||
speedController.update();
|
||||
|
||||
logCounter++;
|
||||
|
||||
// Print every 40 ms
|
||||
if (logCounter >= 10)
|
||||
{
|
||||
logCounter = 0;
|
||||
|
||||
Serial.print("Reference: ");
|
||||
Serial.print(
|
||||
speedController.reference()
|
||||
);
|
||||
|
||||
Serial.print(" RPM, Actual: ");
|
||||
Serial.print(
|
||||
speedController.actual_rpm()
|
||||
);
|
||||
|
||||
Serial.print(" RPM, Control: ");
|
||||
Serial.println(
|
||||
speedController.control_output()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void Operational::on_exit()
|
||||
{
|
||||
speedController.stop();
|
||||
|
||||
statusLED.set_lo();
|
||||
}
|
||||
|
||||
void Operational::reset()
|
||||
{
|
||||
if (initializationState != nullptr)
|
||||
{
|
||||
context_->transition_to(
|
||||
*initializationState
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void Operational::fault()
|
||||
{
|
||||
if (stoppedState != nullptr)
|
||||
{
|
||||
Serial.println("FAULT detected");
|
||||
|
||||
context_->transition_to(
|
||||
*stoppedState
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void Operational::set_initialization_state(
|
||||
State& state
|
||||
)
|
||||
{
|
||||
initializationState = &state;
|
||||
}
|
||||
|
||||
void Operational::set_stopped_state(
|
||||
State& state
|
||||
)
|
||||
{
|
||||
stoppedState = &state;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
#include <p_controller.h>
|
||||
|
||||
P_controller::P_controller(double kp)
|
||||
: kp(kp)
|
||||
{
|
||||
}
|
||||
|
||||
double P_controller::update(
|
||||
double reference,
|
||||
double actual
|
||||
)
|
||||
{
|
||||
double error =
|
||||
reference - actual;
|
||||
|
||||
double output =
|
||||
kp * error;
|
||||
|
||||
return limit(output);
|
||||
}
|
||||
|
||||
void P_controller::reset()
|
||||
{
|
||||
// P controller has no stored dynamic state.
|
||||
}
|
||||
|
||||
const char* P_controller::name() const
|
||||
{
|
||||
return "P";
|
||||
}
|
||||
|
||||
void P_controller::set_kp(double newKp)
|
||||
{
|
||||
kp = newKp;
|
||||
}
|
||||
|
||||
double P_controller::get_kp() const
|
||||
{
|
||||
return kp;
|
||||
}
|
||||
|
||||
double P_controller::limit(double value) const
|
||||
{
|
||||
if (value > 255.0)
|
||||
{
|
||||
return 255.0;
|
||||
}
|
||||
|
||||
if (value < 0.0)
|
||||
{
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
#include <pi_controller.h>
|
||||
|
||||
PI_controller::PI_controller(
|
||||
double kp,
|
||||
double ti,
|
||||
double timestep
|
||||
)
|
||||
: kp(kp),
|
||||
ti(ti),
|
||||
timestep(timestep),
|
||||
integral(0.0)
|
||||
{
|
||||
}
|
||||
|
||||
double PI_controller::update(
|
||||
double reference,
|
||||
double actual
|
||||
)
|
||||
{
|
||||
const double error =
|
||||
reference - actual;
|
||||
|
||||
// Candidate integral:
|
||||
// E(k) = E(k-1) + e(k)T
|
||||
const double candidateIntegral =
|
||||
integral + error * timestep;
|
||||
|
||||
// Candidate PI output:
|
||||
// u = Kp * (e + E/Ti)
|
||||
const double candidateOutput =
|
||||
kp *
|
||||
(
|
||||
error +
|
||||
candidateIntegral / ti
|
||||
);
|
||||
|
||||
// -----------------------------------------
|
||||
// Conditional integration / anti-windup
|
||||
// -----------------------------------------
|
||||
|
||||
// Integrate normally if the candidate
|
||||
// controller output is not saturated.
|
||||
if (
|
||||
candidateOutput <= 255.0 &&
|
||||
candidateOutput >= -255.0
|
||||
)
|
||||
{
|
||||
integral =
|
||||
candidateIntegral;
|
||||
}
|
||||
|
||||
// If positively saturated, only integrate
|
||||
// if the error is driving the output back down.
|
||||
else if (
|
||||
candidateOutput > 255.0 &&
|
||||
error < 0.0
|
||||
)
|
||||
{
|
||||
integral =
|
||||
candidateIntegral;
|
||||
}
|
||||
|
||||
// If negatively saturated, only integrate
|
||||
// if the error is driving the output back up.
|
||||
else if (
|
||||
candidateOutput < -255.0 &&
|
||||
error > 0.0
|
||||
)
|
||||
{
|
||||
integral =
|
||||
candidateIntegral;
|
||||
}
|
||||
|
||||
const double output =
|
||||
kp *
|
||||
(
|
||||
error +
|
||||
integral / ti
|
||||
);
|
||||
|
||||
return limit(output);
|
||||
}
|
||||
|
||||
void PI_controller::reset()
|
||||
{
|
||||
integral = 0.0;
|
||||
}
|
||||
|
||||
const char* PI_controller::name() const
|
||||
{
|
||||
return "PI";
|
||||
}
|
||||
|
||||
void PI_controller::set_kp(
|
||||
double value
|
||||
)
|
||||
{
|
||||
kp = value;
|
||||
}
|
||||
|
||||
double PI_controller::get_kp() const
|
||||
{
|
||||
return kp;
|
||||
}
|
||||
|
||||
void PI_controller::set_ti(
|
||||
double value
|
||||
)
|
||||
{
|
||||
if (value > 0.0)
|
||||
{
|
||||
ti = value;
|
||||
}
|
||||
}
|
||||
|
||||
double PI_controller::get_ti() const
|
||||
{
|
||||
return ti;
|
||||
}
|
||||
|
||||
double PI_controller::get_timestep() const
|
||||
{
|
||||
return timestep;
|
||||
}
|
||||
|
||||
double PI_controller::limit(
|
||||
double value
|
||||
) const
|
||||
{
|
||||
if (value > 255.0)
|
||||
{
|
||||
return 255.0;
|
||||
}
|
||||
|
||||
if (value < -255.0)
|
||||
{
|
||||
return -255.0;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
#include <speed_controller.h>
|
||||
|
||||
Speed_controller::Speed_controller(
|
||||
Encoder& encoder,
|
||||
Speed_estimator& estimator,
|
||||
Motor_driver& motor,
|
||||
Controller& controller
|
||||
)
|
||||
: encoder(encoder),
|
||||
estimator(estimator),
|
||||
motor(motor),
|
||||
controller(&controller),
|
||||
running(false),
|
||||
referenceRPM(0.0),
|
||||
actualRPM(0.0),
|
||||
controlOutput(0.0)
|
||||
{
|
||||
}
|
||||
|
||||
void Speed_controller::start()
|
||||
{
|
||||
estimator.reset(
|
||||
encoder.position()
|
||||
);
|
||||
|
||||
controller->reset();
|
||||
|
||||
actualRPM = 0.0;
|
||||
controlOutput = 0.0;
|
||||
|
||||
running = true;
|
||||
|
||||
motor.coast();
|
||||
}
|
||||
|
||||
void Speed_controller::stop()
|
||||
{
|
||||
running = false;
|
||||
|
||||
controlOutput = 0.0;
|
||||
|
||||
motor.coast();
|
||||
}
|
||||
|
||||
void Speed_controller::brake()
|
||||
{
|
||||
running = false;
|
||||
|
||||
controlOutput = 0.0;
|
||||
|
||||
motor.brake();
|
||||
}
|
||||
|
||||
void Speed_controller::update()
|
||||
{
|
||||
if (!running)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
actualRPM =
|
||||
estimator.update(
|
||||
encoder.position()
|
||||
);
|
||||
|
||||
controlOutput =
|
||||
controller->update(
|
||||
referenceRPM,
|
||||
actualRPM
|
||||
);
|
||||
|
||||
motor.set_command(
|
||||
controlOutput
|
||||
);
|
||||
}
|
||||
|
||||
void Speed_controller::set_reference(
|
||||
double rpm
|
||||
)
|
||||
{
|
||||
referenceRPM = rpm;
|
||||
}
|
||||
|
||||
double Speed_controller::reference() const
|
||||
{
|
||||
return referenceRPM;
|
||||
}
|
||||
|
||||
void Speed_controller::set_controller(
|
||||
Controller& newController
|
||||
)
|
||||
{
|
||||
controller = &newController;
|
||||
|
||||
controller->reset();
|
||||
}
|
||||
|
||||
double Speed_controller::actual_rpm() const
|
||||
{
|
||||
return actualRPM;
|
||||
}
|
||||
|
||||
double Speed_controller::control_output() const
|
||||
{
|
||||
return controlOutput;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
#include <speed_estimator.h>
|
||||
|
||||
Speed_estimator::Speed_estimator(
|
||||
double countsPerRevolution,
|
||||
double sampleTimeSeconds
|
||||
)
|
||||
: countsPerRevolution(countsPerRevolution),
|
||||
sampleTime(sampleTimeSeconds),
|
||||
previousPosition(0),
|
||||
rollingCounts(0),
|
||||
historyIndex(0),
|
||||
samplesFilled(0),
|
||||
currentRPM(0.0)
|
||||
{
|
||||
for (uint8_t i = 0; i < WINDOW_SIZE; i++)
|
||||
{
|
||||
countHistory[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void Speed_estimator::reset(int32_t initialPosition)
|
||||
{
|
||||
previousPosition = initialPosition;
|
||||
|
||||
rollingCounts = 0;
|
||||
historyIndex = 0;
|
||||
samplesFilled = 0;
|
||||
currentRPM = 0.0;
|
||||
|
||||
for (uint8_t i = 0; i < WINDOW_SIZE; i++)
|
||||
{
|
||||
countHistory[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
double Speed_estimator::update(int32_t currentPosition)
|
||||
{
|
||||
int32_t deltaCount =
|
||||
currentPosition - previousPosition;
|
||||
|
||||
previousPosition = currentPosition;
|
||||
|
||||
rollingCounts -=
|
||||
countHistory[historyIndex];
|
||||
|
||||
countHistory[historyIndex] =
|
||||
deltaCount;
|
||||
|
||||
rollingCounts += deltaCount;
|
||||
|
||||
historyIndex++;
|
||||
|
||||
if (historyIndex >= WINDOW_SIZE)
|
||||
{
|
||||
historyIndex = 0;
|
||||
}
|
||||
|
||||
if (samplesFilled < WINDOW_SIZE)
|
||||
{
|
||||
samplesFilled++;
|
||||
}
|
||||
|
||||
double windowTime =
|
||||
samplesFilled * sampleTime;
|
||||
|
||||
currentRPM =
|
||||
(rollingCounts * 60.0) /
|
||||
(countsPerRevolution * windowTime);
|
||||
|
||||
return currentRPM;
|
||||
}
|
||||
|
||||
double Speed_estimator::rpm() const
|
||||
{
|
||||
return currentRPM;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
#include <Arduino.h>
|
||||
|
||||
#include <stopped.h>
|
||||
#include <context.h>
|
||||
|
||||
Stopped::Stopped(
|
||||
Speed_controller& speedController,
|
||||
Digital_out& statusLED
|
||||
)
|
||||
: speedController(speedController),
|
||||
statusLED(statusLED),
|
||||
initializationState(nullptr),
|
||||
ledCounter(0)
|
||||
{
|
||||
}
|
||||
|
||||
const char* Stopped::name() const
|
||||
{
|
||||
return "Stopped";
|
||||
}
|
||||
|
||||
void Stopped::on_entry()
|
||||
{
|
||||
Serial.println("Stopped");
|
||||
Serial.println("Emergency stop - motor brake active");
|
||||
|
||||
// Immediately stop normal controller operation
|
||||
// and actively brake the motor.
|
||||
speedController.brake();
|
||||
|
||||
statusLED.set_hi();
|
||||
|
||||
ledCounter = 0;
|
||||
}
|
||||
|
||||
void Stopped::on_do()
|
||||
{
|
||||
// State activity runs every 4 ms.
|
||||
//
|
||||
// 2 Hz LED:
|
||||
// full period = 500 ms
|
||||
// toggle approximately every 250 ms
|
||||
//
|
||||
// 250 ms / 4 ms = 62.5 ticks
|
||||
|
||||
ledCounter++;
|
||||
|
||||
if (ledCounter >= 63)
|
||||
{
|
||||
ledCounter = 0;
|
||||
statusLED.toggle();
|
||||
}
|
||||
}
|
||||
|
||||
void Stopped::on_exit()
|
||||
{
|
||||
statusLED.set_lo();
|
||||
}
|
||||
|
||||
void Stopped::reset()
|
||||
{
|
||||
if (initializationState != nullptr)
|
||||
{
|
||||
context_->transition_to(
|
||||
*initializationState
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void Stopped::set_initialization_state(
|
||||
State& state
|
||||
)
|
||||
{
|
||||
initializationState = &state;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
|
||||
This directory is intended for PlatformIO Test Runner and project tests.
|
||||
|
||||
Unit Testing is a software testing method by which individual units of
|
||||
source code, sets of one or more MCU program modules together with associated
|
||||
control data, usage procedures, and operating procedures, are tested to
|
||||
determine whether they are fit for use. Unit testing finds problems early
|
||||
in the development cycle.
|
||||
|
||||
More information about PlatformIO Unit Testing:
|
||||
- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html
|
||||
+438
@@ -0,0 +1,438 @@
|
||||
#include <unity.h>
|
||||
|
||||
#include <controller.h>
|
||||
#include <pi_controller.h>
|
||||
|
||||
// --------------------------------------------------
|
||||
// Unity setup
|
||||
// --------------------------------------------------
|
||||
|
||||
void setUp()
|
||||
{
|
||||
}
|
||||
|
||||
void tearDown()
|
||||
{
|
||||
}
|
||||
|
||||
// --------------------------------------------------
|
||||
// Normal-flow tests
|
||||
// --------------------------------------------------
|
||||
|
||||
void test_zero_error_gives_zero_output()
|
||||
{
|
||||
PI_controller controller(
|
||||
10.0,
|
||||
1.0,
|
||||
0.1
|
||||
);
|
||||
|
||||
double output =
|
||||
controller.update(
|
||||
50.0,
|
||||
50.0
|
||||
);
|
||||
|
||||
TEST_ASSERT_DOUBLE_WITHIN(
|
||||
0.001,
|
||||
0.0,
|
||||
output
|
||||
);
|
||||
}
|
||||
|
||||
void test_positive_error_gives_positive_output()
|
||||
{
|
||||
PI_controller controller(
|
||||
10.0,
|
||||
1.0,
|
||||
0.1
|
||||
);
|
||||
|
||||
// error = 2
|
||||
// E = 2 * 0.1 = 0.2
|
||||
// u = 10 * (2 + 0.2 / 1)
|
||||
// = 22
|
||||
|
||||
double output =
|
||||
controller.update(
|
||||
52.0,
|
||||
50.0
|
||||
);
|
||||
|
||||
TEST_ASSERT_DOUBLE_WITHIN(
|
||||
0.001,
|
||||
22.0,
|
||||
output
|
||||
);
|
||||
}
|
||||
|
||||
void test_negative_error_gives_negative_output()
|
||||
{
|
||||
PI_controller controller(
|
||||
10.0,
|
||||
1.0,
|
||||
0.1
|
||||
);
|
||||
|
||||
// error = -2
|
||||
// E = -2 * 0.1 = -0.2
|
||||
// u = 10 * (-2 - 0.2)
|
||||
// = -22
|
||||
|
||||
double output =
|
||||
controller.update(
|
||||
48.0,
|
||||
50.0
|
||||
);
|
||||
|
||||
TEST_ASSERT_DOUBLE_WITHIN(
|
||||
0.001,
|
||||
-22.0,
|
||||
output
|
||||
);
|
||||
}
|
||||
|
||||
void test_integral_accumulates_over_time()
|
||||
{
|
||||
PI_controller controller(
|
||||
10.0,
|
||||
1.0,
|
||||
0.1
|
||||
);
|
||||
|
||||
double first =
|
||||
controller.update(
|
||||
51.0,
|
||||
50.0
|
||||
);
|
||||
|
||||
double second =
|
||||
controller.update(
|
||||
51.0,
|
||||
50.0
|
||||
);
|
||||
|
||||
double third =
|
||||
controller.update(
|
||||
51.0,
|
||||
50.0
|
||||
);
|
||||
|
||||
// After first update:
|
||||
// E = 0.1
|
||||
// u = 10 * (1 + 0.1) = 11
|
||||
|
||||
TEST_ASSERT_DOUBLE_WITHIN(
|
||||
0.001,
|
||||
11.0,
|
||||
first
|
||||
);
|
||||
|
||||
// After second update:
|
||||
// E = 0.2
|
||||
// u = 12
|
||||
|
||||
TEST_ASSERT_DOUBLE_WITHIN(
|
||||
0.001,
|
||||
12.0,
|
||||
second
|
||||
);
|
||||
|
||||
// After third update:
|
||||
// E = 0.3
|
||||
// u = 13
|
||||
|
||||
TEST_ASSERT_DOUBLE_WITHIN(
|
||||
0.001,
|
||||
13.0,
|
||||
third
|
||||
);
|
||||
}
|
||||
|
||||
// --------------------------------------------------
|
||||
// Saturation tests
|
||||
// --------------------------------------------------
|
||||
|
||||
void test_positive_output_is_limited()
|
||||
{
|
||||
PI_controller controller(
|
||||
100.0,
|
||||
1.0,
|
||||
0.1
|
||||
);
|
||||
|
||||
double output =
|
||||
controller.update(
|
||||
60.0,
|
||||
50.0
|
||||
);
|
||||
|
||||
TEST_ASSERT_DOUBLE_WITHIN(
|
||||
0.001,
|
||||
255.0,
|
||||
output
|
||||
);
|
||||
}
|
||||
|
||||
void test_negative_output_is_limited()
|
||||
{
|
||||
PI_controller controller(
|
||||
100.0,
|
||||
1.0,
|
||||
0.1
|
||||
);
|
||||
|
||||
double output =
|
||||
controller.update(
|
||||
40.0,
|
||||
50.0
|
||||
);
|
||||
|
||||
TEST_ASSERT_DOUBLE_WITHIN(
|
||||
0.001,
|
||||
-255.0,
|
||||
output
|
||||
);
|
||||
}
|
||||
|
||||
// --------------------------------------------------
|
||||
// Anti-windup tests
|
||||
// --------------------------------------------------
|
||||
|
||||
void test_positive_saturation_does_not_wind_up()
|
||||
{
|
||||
PI_controller controller(
|
||||
100.0,
|
||||
1.0,
|
||||
0.1
|
||||
);
|
||||
|
||||
// Repeatedly demand an unreachable positive output.
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
controller.update(
|
||||
60.0,
|
||||
50.0
|
||||
);
|
||||
}
|
||||
|
||||
// With anti-windup the large saturated error
|
||||
// should not have accumulated.
|
||||
//
|
||||
// New error = +1
|
||||
// E = 1 * 0.1 = 0.1
|
||||
// u = 100 * (1 + 0.1) = 110
|
||||
|
||||
double output =
|
||||
controller.update(
|
||||
51.0,
|
||||
50.0
|
||||
);
|
||||
|
||||
TEST_ASSERT_DOUBLE_WITHIN(
|
||||
0.001,
|
||||
110.0,
|
||||
output
|
||||
);
|
||||
}
|
||||
|
||||
void test_negative_saturation_does_not_wind_up()
|
||||
{
|
||||
PI_controller controller(
|
||||
100.0,
|
||||
1.0,
|
||||
0.1
|
||||
);
|
||||
|
||||
// Repeatedly demand an unreachable negative output.
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
controller.update(
|
||||
40.0,
|
||||
50.0
|
||||
);
|
||||
}
|
||||
|
||||
// New error = -1
|
||||
// E = -0.1
|
||||
// u = 100 * (-1 - 0.1) = -110
|
||||
|
||||
double output =
|
||||
controller.update(
|
||||
49.0,
|
||||
50.0
|
||||
);
|
||||
|
||||
TEST_ASSERT_DOUBLE_WITHIN(
|
||||
0.001,
|
||||
-110.0,
|
||||
output
|
||||
);
|
||||
}
|
||||
|
||||
void test_controller_recovers_after_saturation()
|
||||
{
|
||||
PI_controller controller(
|
||||
100.0,
|
||||
1.0,
|
||||
0.1
|
||||
);
|
||||
|
||||
// Cause positive saturation.
|
||||
controller.update(
|
||||
60.0,
|
||||
50.0
|
||||
);
|
||||
|
||||
// Return to a small error.
|
||||
// Anti-windup should allow immediate recovery.
|
||||
double output =
|
||||
controller.update(
|
||||
51.0,
|
||||
50.0
|
||||
);
|
||||
|
||||
TEST_ASSERT_DOUBLE_WITHIN(
|
||||
0.001,
|
||||
110.0,
|
||||
output
|
||||
);
|
||||
}
|
||||
|
||||
// --------------------------------------------------
|
||||
// Reset test
|
||||
// --------------------------------------------------
|
||||
|
||||
void test_reset_clears_integral()
|
||||
{
|
||||
PI_controller controller(
|
||||
10.0,
|
||||
1.0,
|
||||
0.1
|
||||
);
|
||||
|
||||
// Build up integral contribution.
|
||||
controller.update(
|
||||
51.0,
|
||||
50.0
|
||||
);
|
||||
|
||||
controller.update(
|
||||
51.0,
|
||||
50.0
|
||||
);
|
||||
|
||||
// Clear stored integral.
|
||||
controller.reset();
|
||||
|
||||
// This should now behave exactly like
|
||||
// the first +1 error sample again.
|
||||
//
|
||||
// E = 0.1
|
||||
// u = 10 * (1 + 0.1) = 11
|
||||
|
||||
double output =
|
||||
controller.update(
|
||||
51.0,
|
||||
50.0
|
||||
);
|
||||
|
||||
TEST_ASSERT_DOUBLE_WITHIN(
|
||||
0.001,
|
||||
11.0,
|
||||
output
|
||||
);
|
||||
}
|
||||
|
||||
// --------------------------------------------------
|
||||
// Polymorphism test
|
||||
// --------------------------------------------------
|
||||
|
||||
void test_pi_controller_works_through_controller_interface()
|
||||
{
|
||||
PI_controller pi(
|
||||
10.0,
|
||||
1.0,
|
||||
0.1
|
||||
);
|
||||
|
||||
// Access PI controller only through
|
||||
// the common Controller interface.
|
||||
Controller* controller =
|
||||
π
|
||||
|
||||
double output =
|
||||
controller->update(
|
||||
52.0,
|
||||
50.0
|
||||
);
|
||||
|
||||
TEST_ASSERT_DOUBLE_WITHIN(
|
||||
0.001,
|
||||
22.0,
|
||||
output
|
||||
);
|
||||
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"PI",
|
||||
controller->name()
|
||||
);
|
||||
}
|
||||
|
||||
// --------------------------------------------------
|
||||
// Test runner
|
||||
// --------------------------------------------------
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
|
||||
UNITY_BEGIN();
|
||||
|
||||
RUN_TEST(
|
||||
test_zero_error_gives_zero_output
|
||||
);
|
||||
|
||||
RUN_TEST(
|
||||
test_positive_error_gives_positive_output
|
||||
);
|
||||
|
||||
RUN_TEST(
|
||||
test_negative_error_gives_negative_output
|
||||
);
|
||||
|
||||
RUN_TEST(
|
||||
test_integral_accumulates_over_time
|
||||
);
|
||||
|
||||
RUN_TEST(
|
||||
test_positive_output_is_limited
|
||||
);
|
||||
|
||||
RUN_TEST(
|
||||
test_negative_output_is_limited
|
||||
);
|
||||
|
||||
RUN_TEST(
|
||||
test_positive_saturation_does_not_wind_up
|
||||
);
|
||||
|
||||
RUN_TEST(
|
||||
test_negative_saturation_does_not_wind_up
|
||||
);
|
||||
|
||||
RUN_TEST(
|
||||
test_controller_recovers_after_saturation
|
||||
);
|
||||
|
||||
RUN_TEST(
|
||||
test_reset_clears_integral
|
||||
);
|
||||
|
||||
RUN_TEST(
|
||||
test_pi_controller_works_through_controller_interface
|
||||
);
|
||||
|
||||
return UNITY_END();
|
||||
}
|
||||
Reference in New Issue
Block a user