Módulo de movimiento lento para el belén con motor paso a paso en C++ 
Es Navidad y toca montaje electrónico para el Belén. Este año trataremos de hacer un generador de movimiento lento que permita simular el paso de los tres reyes magos y hacer que estos transiten desde un extremo del belén hasta el pesebre moviéndose de forma lenta y autónoma a lo largo de los días.

Objetivo

La idea es que los tres reyes magos (tres figuras que van a lomos de sus respectivos camellos) se vayan desplazando lentamente desde un extremo del montaje del belén hasta el otro extremo (donde está situado el pesebre) a lo largo de los días y de forma imperceptible. De esta forma cada día estarán más cerca de su "destino".



Descripción funcional

Se plantea un montaje basado en un microcontrolador (en este caso un RISC-V), tres botones ("modo", "avance" y "retroceso"), un display de 7 segmentos y un motor paso a paso de 4 fases con reductora (4076 pasos por vuelta). El módulo funcionará en 3 modos entre los que se alternará pulsando uno de los botones:

- Modo de movimiento libre del motor paso a paso: en este modo se muestra la letra "c" en el display de 7 segmentos y usando los dos botones restantes se podrá mover rápidamente (aprox cinco vueltas por minuto) el motor paso a paso.

- Modo de configuración de días: En este modo se configurará la cantidad de días (en el display de 7 segmentos) que se desea que dure el movimiento completo del motor paso a paso.

- Modo de reposo (display de 7 segmentos apagado): Al entrar en este modo, el microcontrolador calcula, a partir de los pasos avanzados (o retrocedidos) en el modo 1 y a partir de los días configurados en el modo 2, la velocidad (lenta) a la que tendrá que moverse el motor paso a paso para que en el transcurso de esos días el motor vuelva a la posición de encendido. El montaje se quedaría en este modo hasta que el motor paso a paso llegue a su posición inicial (la que tenía cuando se encendió).

Cuando el montaje se enciende, el modo en el que arranca es el modo 1.

Un caso de uso típico sería el siguiente. El motor paso a paso se alojará en el pesebre, un hilo tendrá un extremo conectado al eje del motor (en el que se enrollará como si fuese el carrete de una caña de pescar) y el otro extremo de ese hilo estará pegado a una cartulina donde estarán alojados los reyes magos:

1. Encendemos el montaje (modo 1) y hacemos girar el motor paso a paso de tal forma que la cartulina con los reyes magos quede en lo que será el final de su recorrido (pegados al pesebre y con el hilo lo más enrollado posible al eje del motor).

2. Apagamos el módulo.

3. Volvemos a encender el módulo para que quede registrada la posición actual del motor como posición "reposo" o de "destino".

4. Con el módulo recién encendido (de nuevo modo 1), volvemos a mover el motor de forma rápida pero en el sentido contrario al dado en el paso 1, para que vaya soltando hilo mientras vamos tirando de los reyes magos (para mantener el hilo algo tenso) hasta que hayamos colocado los reyes magos en su posición inicial. Al estar en modo 1, el microcontrolador habrá estado contabilizando la cantidad de pasos que hemos tenido que hacer mover al motor para que los reyes magos lleguen a su posición inicial.

5. Pulsamos el botón de configuración para pasar al modo 2 y usamos los otros dos botones para definir la cantidad de días que queremos que tarden los reyes magos en llegar al pesebre.

6. Pulsamos de nuevo el botón de configuración para pasar el modo 3 (se apagará el display de 7 segmentos, aunque el led azul sigue parpadeando para que sepamos que el montaje sigue funcionando) y lo dejamos encendido así durante todos los días que dure el montaje. En este modo el motor paso a paso se irá moviendo poco a poco de tal manera que cuando haya pasado la cantidad de días configurada en el modo 2, los reyes magos hayan llegado al pesebre.

7. No se debe apagar el montaje ya que no se almacena el estado en ningún tipo de memoria no volátil. Si hay un corte de corriente en el montaje, hay que volver a configurarlo empezando por el paso 1.

Circuito



Como se puede ver se trata de un montaje muy sencillo que hace uso sólo de las características GPIO del microcontrolador y, por tanto, aunque está hecho con un RISC-V, el montaje es fácilmente portable a cualquier otro microcontrolador con la suficiente cantidad de pines GPIO: 3 de entrada y 11 de salida (7 para el display y 4 para el motor paso a paso).

- El teclado consta de 3 botones ("modo", "-" y "+") con circuito antirrebote básico.

- El display de 7 segmentos es uno sencillo de cátodo común.

- El motor paso a paso es un 28BYJ-48 de 4 fases, a 5 voltios y controlado por un chip ULN2003. El motor tiene una reductora que hace que requiera 4076 pasos para completar una vuelta.

Diseño del software

El software está desarrollado en C++ y el diagrama de clases es el siguiente:



Las clases están diseñadas para ser lo más independientes posible del hardware final en el que se ejecuten. De cara a la implementación específica para el GD32VF103, se definieron las siguientes clases:



"Heartbeat" es una clase que se encarga de hacer perpadear el led azul de la placa del GD32VF103 mientras que las clases My... son las encargadas de configurar y leer o escribir en los GPIO correspondientes a cada funcionalidad (teclado, 7 segmentos y motor paso a paso, respectivamente).

Por ejemplo, la clase "Stepper" es la encargada de mover el motor paso a paso. La velocidad está dada por el valor de la variable miembro "ticksPerStep", que indica la cantidad de veces que debe ejecutarse "run()" para avanzar en un paso el motor. Teniendo el cuenta que cada "run()" se ejecutan cada 10 milisegundos (100 veces por segundo), se puede calcular el valor de "ticksPerStep" a partir de la velocidad que queramos imprimirle al motor:

#include "Stepper.H"

using namespace avelino;
using namespace std;

void Stepper::init() {
    this->gpioConfigure();
    this->gpioWrite(0);
}

void Stepper::start() {
    this->doStart = true;
}

void Stepper::stop() {
    this->doStart = false;
}

void Stepper::run() {
    Status localStatus = this->status;
    do {
        this->status = localStatus;
        if (localStatus == Status::STOPPED) {
            if (this->doStart) {
                this->gpioWrite(this->lastMask);
                this->timer = this->ticksPerStep;
                //cout << "Stepper::run: STOPPED --> RUNNING_1, timer=" << this->timer << endl;
                localStatus = Status::RUNNING;
            }
        }
        else if (localStatus == Status::RUNNING) {
            this->timer--;
            int32_t threshold = (int32_t) this->ticksPerStep - (int32_t) this->getTicksPerPulse();
            if (this->timer < threshold)
                this->gpioWrite(0);
            if (this->timer <= 0) {
                this->lastMask = this->getNextMask(this->lastMask, this->wise);
                this->gpioWrite(this->lastMask);
                if (this->listener != NULL)
                    this->listener->stepDone(*this);
                this->timer = this->ticksPerStep;
                //cout << "Stepper::run: RUNNING_1 --> RUNNING_2, timer=" << this->timer << endl;
            }
            if (!this->doStart) {
                this->gpioWrite(0);
                //cout << "Stepper::run: RUNNING_1 --> STOPPED" << endl;
                localStatus = Status::STOPPED;
            }
        }
    } while (localStatus != this->status);
}

Como se puede ver, la clase "Stepper" se encarga de la gestión de los avances en los pasos del motor, mientras que la clase "MyStepper" se encarga de implementar las operaciones de bajo nivel relacionadas con el motor:

#include "MyStepper.H"

using namespace avelino;
using namespace std;

#define  RCU_APB2EN   *((uint32_t *) 0x40021018)
#define  GPIOA_CTL0   *((uint32_t *) 0x40010800)
#define  GPIOA_OCTL   *((uint32_t *) 0x4001080C)
#define  GPIOB_CTL0   *((uint32_t *) 0x40010C00)
#define  GPIOB_OCTL   *((uint32_t *) 0x40010C0C)

void MyStepper::gpioConfigure() const {
    // enable clock on ports A and B
    RCU_APB2EN = RCU_APB2EN | (((uint32_t) 3) << 2);
    // configure A5, A6 and A7 as push/pull output
    GPIOA_CTL0 = (GPIOA_CTL0 & 0x000FFFFF) | 0x22200000;
    // configure B0 as push/pull output
    GPIOB_CTL0 = (GPIOB_CTL0 & 0xFFFFFFF0) | 0x00000002;
}

void MyStepper::gpioWrite(uint32_t mask) const {
    if (mask & 1)
        GPIOA_OCTL |= (((uint32_t) 1) << 5);
    else
        GPIOA_OCTL &= ~(((uint32_t) 1) << 5);
    if (mask & 2)
        GPIOA_OCTL |= (((uint32_t) 1) << 6);
    else
        GPIOA_OCTL &= ~(((uint32_t) 1) << 6);
    if (mask & 4)
        GPIOA_OCTL |= (((uint32_t) 1) << 7);
    else
        GPIOA_OCTL &= ~(((uint32_t) 1) << 7);
    if (mask & 8)
        GPIOB_OCTL |= ((uint32_t) 1);
    else
        GPIOB_OCTL &= ~((uint32_t) 1);
}

uint32_t MyStepper::getNextMask(uint32_t prevMask, Wise wise) const {
    if (wise == Stepper::Wise::CLOCK)
        return ((prevMask << 1) | ((prevMask >> 3) & 1)) & 0x0F;
    else
        return ((prevMask >> 1) | ((prevMask & 1) << 3)) & 0x0F;
}

const uint32_t MyStepper::getStepsPerRevolution() const {
    return 4076;
}

const uint32_t MyStepper::getTicksPerPulse() const {
    return 20;   // 20 * 10 = 200 ms
}

Las tareas de MyStepper se limitan a:

- Calcular una máscara de paso a partir de la anterior.

- Configurar las salidas GPIO.

- Emitir una máscara a las salidas GPIO.

De esta manera se mantiene separado el código C++ independiente del hardware del código C++ dependiente del hardware. De hecho la subcarpeta "linux" implementa versiones simuladas de Stepper, Keypad y SevenSegments, que permiten depurar en un terminal de Linux el funcionamiento del módulo antes de tostarlo en el microcontrolador.

Los tres diferentes modos de funcionamiento del módulo están representados por clases que heredan de la clase "State": una clase por cada modo. La clase "ConfigureStepsState" es la clase cuyo "run()" se ejecuta mientras estamos en el modo 1, la clase "ConfigureDaysState" es la clase cuyo "run()" se ejecuta mientras estamos en el modo 2, mientras que la clase "IdleState" es la clase cuyo "run()" se ejecuta mientras estamos en el modo 3 o modo de reposo. La clase "StateManager" se encarga de gestionar la "salida" de un modo y la "entrada" en el siguiente modo.

#ifndef  __STATE_H__
#define  __STATE_H__

#include "Task.H"

extern "C++" {
    namespace avelino {
        using namespace std;

        template <typename T>
        class State : public Task {
            protected:
                T *data;
            public:
                virtual void onLoad(T &data) = 0;
                virtual void run() = 0;
                virtual State<T> &getNextState() = 0;
                virtual T &onUnload() = 0;
        };

        template <typename T>
        class StateManager : public Task {
            protected:
                State<T> *currentState;
            public:
                StateManager(State<T> &initialState, T &initialData) : currentState(&initialState) {
                    this->currentState->onLoad(initialData);
                };
                virtual void run() {
                    if (this->currentState != NULL) {
                        this->currentState->run();
                        State<T> &nextState = this->currentState->getNextState();
                        if (this->currentState != &nextState) {
                            T &data = this->currentState->onUnload();
                            this->currentState = &nextState;
                            this->currentState->onLoad(data);
                        }
                    }
                };
        };
    }
}

#endif  // __STATE_H__

Al final, lo que se hace en "main.cc" es configurar el CLIC (Core Local Interrupt Controller) del RISC-V para que genere una interrupción de timer cada 10 milisegundos y en cada callback del timer se ejecuta el run de los objetos:

- keypad (teclado)

- sevenSegments (7 segmentos)

- stepper (motor paso a paso)

- heartbeat (el led que parpadea)

- stateManager (encargado, a su vez de ejecutar el "run()" del "ConfigureStepsState", del "ConfigureDaysState" o del "IdleState", en función del modo en el que nos encontremos).

#include "Task.H"
#include "Timer.H"
#include "MyStepper.H"
#include "MyKeypad.H"
#include "MySevenSegments.H"
#include "SharedData.H"
#include "State.H"
#include "MyIdleState.H"
#include "MyConfigureStepsState.H"
#include "ConfigureDaysState.H"
#include "interrupt.H"
#include "Heartbeat.H"

using namespace avelino;
using namespace std;

class MyTimerListener : public TimerListener {
    public:
        Task **tasks;
        uint16_t numTasks;
        MyTimerListener(Task **tasks, uint16_t numTasks) : tasks(tasks), numTasks(numTasks) { };
        virtual void timerExpired();
};

void MyTimerListener::timerExpired() {
    Task **t = this->tasks;
    for (uint16_t i = 0; i < this->numTasks; i++, t++)
        (*t)->run();
}

int main() {
    interruptInit();
    MyKeypad keypad;
    keypad.init();
    MySevenSegments sevenSegments;
    sevenSegments.init();
    MyStepper stepper;
    stepper.init();
    Heartbeat heartbeat;
    // states
    SharedData data;
    MyConfigureStepsState configureStepsState(keypad, sevenSegments, stepper);
    ConfigureDaysState configureDaysState(keypad, sevenSegments, stepper);
    MyIdleState idleState(keypad, sevenSegments, stepper);
    // link states
    configureStepsState.setNextState(configureDaysState);
    configureDaysState.setNextState(idleState);
    idleState.setNextState(configureStepsState);
    StateManager<SharedData> stateManager(configureStepsState, data);
    // round robin tasks
    const int NUM_TASKS = 5;
    Task *tasks[NUM_TASKS] = { &keypad, &sevenSegments, &stepper, &heartbeat, &stateManager };
    MyTimerListener myTimerListener(tasks, NUM_TASKS);
    Timer timer;
    timer.init(myTimerListener, 10_msForTimer);
    while (true)
        asm volatile ("wfi");
    return 0;
}

Inicializamos todos los objetos y creamos un sencillo gestor de tareas de tipo round-robin para invocar las funciones miembro "run()" de los objetos de tipo "Task".

Todo el código puede descargarse de la sección soft.







¡Feliz Navidad! :-)

Administrator (Avelino Herrera Morales) 
Jajaja, gracias por leer, crack. Siempre haces la misma pregunta y ya sabes la respuesta: En un .tar.gz en /soft como los desarrolladores "de verdad" xD

Andrés  
Muy buen artículo!!

Tienes el código colgado en GitHub o en otro sitio?

Un saludo =)

Comentarios 
Lo sentimos. No se permiten nuevos comentarios después de 90 días.