StepperCAN

StepperCAN es una librería portable en C para microcontroladores de 32 bits que permite controlar de forma independiente hasta 5 motores de pasos, mediante señales STEP/DIR en modo software. Se puede controlar a través de comandos CAN/UART o llamando directamente a las funciones públicas disponibles. Es posible integrarla en entornos de desarrollo como STM32cubeIDE, MPLAB, MCUXpresso IDE, Keil, entre otros.

Su implementación es ideal en proyectos donde la personalización a bajo nivel es importante y el uso de comandos de trayectoria por G-code es inviable o innecesario. Esta librería surgió de la necesidad de mover simultáneamente varios motores en un mecanismo articulado y como ha funcionado correctamente he decidido compartirla.

Características generales:

  • Hasta 5 motores (ampliable)
  • Modos de funcionamiento independientes por motor:
    • Velocidad constante
    • Posición con perfil trapezoidal
    • Posición con perfil trapezoidal durante tiempo (punto a punto únicamente)
    • Posición sincronizada para dos o más motores (punto a punto únicamente)
  • Aceleración y desaceleración independientes
  • Protocolo de comandos por CAN/UART con respuestas y telemetría en tiempo real por CAN.
  • Comunicación UART unidireccional para configuración básica de drivers TMC2209
  • Portable, compacto, no usa punto flotante

La generación de las señales STEP está basada en software, empleando una ISR maestro de período fijo con la técnica de acumulación de fase. Se puede integrar en cualquier microcontrolador de 32 bits y asignar cualquier GPIO como señal.

Es importante tener en cuenta que este método tiene importantes limitaciones, tales como:

  • No permite sincronización exacta entre múltiples motores
  • Genera cierto grado de jitter debido al acumulador de fase
  • Limitado a frecuencias medias, por lo general no más de 100KHz en el pin STEP en un ARM M0.
  • Ocasiona una carga de CPU elevada por cada motor adicional

Como ejemplo, se incluye un FW desarrollado en STMCubeMX y STMCubeIDE para una placa de impresora 3D modelo FLY-D5 (comprada en AliExpress), con
STM32F072 de 128KB a 48Mhz y conexión CAN/UART.

FLY-D5

Controlador de impresora 3D FLY-D5

FLY-D5

Pinout proporcionado por el fabricante


Cómo usar la librería en un proyecto nuevo:

Como ejemplo usaré STM32CubeMX y STM32CubeIDE. Para otros fabricantes distintos a STM32 se deben usar sus herramientas de desarrollo respectivas.
La siguiente explicación es muy genérica y sin entrar en detalles, de lo contrario sería demasiado extensa. En caso de dudas por favor consulta con cualquier LLM.

Paso general 1: Crear y configurar un proyecto nuevo en STM32CubeMX

FLY-D5

STM32cubeMX con la mayoría de pines configurados para la placa FLY-D5

Pasos específicos:

  • Asignar los pines DIR, STEP, UART, CAN, ADCs, etc, según el HW y los requerimientos del proyecto
  • Configurar el reloj del sistema y periféricos necesarios
  • Activar y configurar el primer timer para que genere una interrupción de alta prioridad en intervalos muy cortos (10us para 2 motores o 20us para 4 motores). Esta será la ISR que genera los pulsos STEP (ejemplo TIM2).
  • Activar y configurar el segundo timer para que genere una interrupción de prioridad menor, cada 1ms. Esta será la ISR que controla la velocidad en tiempo real (ejemplo TIM7).
  • Activar y configurar un tercer timer a 1Mhz de reloj. Se usará para crear retardos en us para la emulación UART, necesaria para la configuración de los drivers. (Se puede implementar de otras maneras igualmente)
  • Guardar y generar el proyecto para STM32CubeIDE. Usar LL (low level) y evitar HAL para las ISR de los timers y acceso a GPIO

Paso general 2: Abrir el proyecto con STM32CubeIDE y realizar la integración:

FLY-D5

STM32cubeIDE con el proyecto cargado y compilación inicial

Pasos específicos:

  • El proyecto debería compilar sin tocar nada. Esto garantiza que ha sido generado correctamente por cubeMX
  • Agregar al proyecto las carpetas del repositorio: Inc, Src y tms_drivers. Las carpetas Inc se deben agregar en los includes de compilación.
  • En la carpeta Inc, modifica el archivo board_config.h según los requrimientos del HW y proyecto
  • En la carpeta Inc, modifica el archivo port.h para indicar las funciones de acceso los GPIO, la estructura del puerto, delay en ms y get_ticks de tu sistema.
  • En la carpeta Src, modifica el archivo port.c para indicar las funciones de escritura en CAN, UART, callbacks de recepción de mensajes,
    activación de los timers, entre otros, de tu sistema.
  • En el archivo main.c (lo ha generado cubeMX y está en Core/Src), agregar al principio:
#include "scheduler.h";

Dentro de main() y antes del bucle principal, ejecutar la inicialización de la librería:

motor_control_init();

Dentro del bucle principal ejecutar la función de planificación de tareas (no agregar delays dentro del bucle):

do_motor_control_tasks();

Debería quedar aproximadamente así (main.c):

#include "scheduler.h";

int main(){
	//...
	//...
	motor_control_init();
	while(1){
		//...
		do_motor_control_tasks();
	}
}

En el archivo stm32f0xx_it.c agregar los callbacks de los timers TIM2 y TIM7 (pueden ser otros según lo que se haya configurado en cubeMX).
En el caso de la interrupción que genera las señales STEP, debería quedar de la siguiente forma:

/**
  * @brief This function handles TIM2 global interrupt.
  */
void TIM2_IRQHandler(void)
{
  /* USER CODE BEGIN TIM2_IRQn 0 */
	if (LL_TIM_IsActiveFlag_UPDATE(TIM2))
	{
		LL_TIM_ClearFlag_UPDATE(TIM2);
		process_step_isr();
	}
  /* USER CODE END TIM2_IRQn 0 */
  /* USER CODE BEGIN TIM2_IRQn 1 */

  /* USER CODE END TIM2_IRQn 1 */
}

Para la interrupción de control de velocidad, el código debería quedar de la siguiente forma:

/**
  * @brief This function handles TIM7 global interrupt.
  */
void TIM7_IRQHandler(void)
{
  /* USER CODE BEGIN TIM7_IRQn 0 */
	if (LL_TIM_IsActiveFlag_UPDATE(TIM7))
	{
		LL_TIM_ClearFlag_UPDATE(TIM7);
		update_motor_data_isr();
	}
  /* USER CODE END TIM7_IRQn 0 */
  /* USER CODE BEGIN TIM7_IRQn 1 */

  /* USER CODE END TIM7_IRQn 1 */
}

En este punto debería compilar sin errores ni warnings. Ajustar el nivel de optimización para debug y release en caso de ser necesario (recomendado optimize for debug y optimize for speed respectivamente).

Funciones públicas

A continuación, se muestran algunas de las funciones que se pueden ejecutar para controlar los motores. Las unidades de distancia son en steps, de velocidad en steps/s y de aceleración en steps/s2. En el caso de las funciones que tienen tiempo en alguno de los parámetros, la unidad es en ms.

volatile MotorData_t *get_motor_data_struct(uint8_t motor);

ret_status enable_all_motors();
ret_status disable_all_motors();
ret_status enable_single_motor(const uint8_t motor);
ret_status disable_single_motor(const uint8_t motor);

ret_status set_accel_decel_param(volatile MotorData_t *m, const int32_t value, const uint8_t type);
ret_status set_synchronized_position(volatile MotorData_t *motor, int32_t position);
ret_status set_motor_position(volatile MotorData_t *motor, int32_t target_position, int32_t speed);
ret_status set_motor_position_time(volatile MotorData_t *motor, int32_t new_position, int32_t time);
ret_status set_motor_velocity_according_time_ramp(volatile MotorData_t *motor, int32_t velocity, uint32_t ramp);
ret_status set_motor_velocity_according_accel_decel(volatile MotorData_t *motor, int32_t velocity);

int32_t get_motor0_steps();
int32_t get_motor1_steps();
int32_t get_motor2_steps();
int32_t get_motor3_steps();
int32_t get_motor4_steps();

int32_t get_motor0_current_velocity();
int32_t get_motor1_current_velocity();
int32_t get_motor2_current_velocity();
int32_t get_motor3_current_velocity();
int32_t get_motor4_current_velocity();

MotorData_t es una estructura donde están contenidos los parámetros y variables de los motores. En el código están definidas 5 instancias estáticas:

volatile MotorData_t motor0;
volatile MotorData_t motor1;
volatile MotorData_t motor2;
volatile MotorData_t motor3;
volatile MotorData_t motor4;

Cada función devuelve un código de error indicado en la siguiente enumeración:

typedef enum {
    STATUS_OK = 0,
	STATUS_NO_OP,
	STATUS_BUSY,
    STATUS_INVALID_ARG,
	STATUS_INVALID_MOTOR,
	STATUS_OUT_OF_RANGE,
	STATUS_UNKNOWN_ERROR,
	STATUS_FW_FAULT,
} ret_status;

Ejemplos de uso:

Encender motor 2 y 3:

ret_status res;
res = enable_single_motor(2);
res = enable_single_motor(3);

Establecer velocidad constante a 6400 steps/s en 1s de rampa en motor 2. Velocidad 1200 steps/s en 500ms de rampa en motor 3:

ret_status res;
volatile MotorData_t *m = NULL;

m = get_motor_data_struct(2);
res = set_motor_velocity_according_time_ramp(m, 6400, 1000);

m = get_motor_data_struct(3);
res = set_motor_velocity_according_time_ramp(m, 1200, 500);

//También se puede hacer directamente usando la dirección de la estructura correspondiente:

set_motor_velocity_according_time_ramp(&motor2, 6400, 1000);
set_motor_velocity_according_time_ramp(&motor3, 1200, 500);

Establecer posición 5000 a velocidad 3200 steps/s en motor 2. Posición -25000 a velocidad 30000 steps/s en motor 3:

//Posición absoluta por defecto

ret_status res;
volatile MotorData_t *m = NULL;

m = get_motor_data_struct(2);
res = set_motor_position(m, 5000, 3200);

m = get_motor_data_struct(3);
res = set_motor_position(m, -25000, 30000);

Establecer aceleración 5000 steps/s2 y desaceleración 2000 steps/s2 para los motores 2 y 3:

ret_status res;
volatile MotorData_t *m = NULL;

m = get_motor_data_struct(2);
res = set_accel_decel_param(m, 5000, SET_ACCEL);
res = set_accel_decel_param(m, 2000, SET_DECEL);

m = get_motor_data_struct(3);
res = set_accel_decel_param(m, 5000, SET_ACCEL);
res = set_accel_decel_param(m, 2000, SET_DECEL);

Establecer posición sincronizada de los motores 0, 1, y 2. Motor 0 a la posición 1000, motor 1 a la posición 5000 y motor 2 a la posición 500. A una velocidad de 3200 steps/s:

ret_status res;
volatile MotorData_t *m = NULL;

m = get_motor_data_struct(0);
res = set_synchronized_position(m, 1000);

m = get_motor_data_struct(1);
res = set_synchronized_position(m, 500);

m = get_motor_data_struct(2);
res = set_synchronized_position(m, 500);

res = set_vmax_synchronized_position(3200);
res = start_synchronized_position();

Lista de comandos CAN/UART

En caso de controlar los motores vía remota por CAN o UART, se indica a continuación la estructura de la trama de comandos:

byte0: comando, del 1 al 18
byte1: motor, del 0 al 4
byte2 al 7 -> datos (según comando)

Comandos disponibles:

Comando Valor decimal
ENABLE_DISABLE_MOTOR 1
SET_MOTOR_VELOCITY_TIME_RAMP 2
SET_MOTOR_VELOCITY_ACCEL_RAMP 3
SET_MOTOR_POSITION 4
SET_SPEED_POSITION_MODE 6
SET_ACCEL 7
SET_DECEL 8
SET_EMERGENCY_DECEL 9
SET_POSITION_MODE 11
CLEAR_POSITION_COUNTER 12
COMMAND_FLOW_CONTROL 13
SET_SYNCHRONIZED_POSITION 14
SET_SYNCHRONIZED_SPEED 15
START_SYNCHRONIZED_POSITION 16
SET_MOTOR_POSITION_TIME 17
GET_POSITION_AND_SPEED 18

Ejemplo de uso desde un cliente:

uint8_t data[8] = {0};
data[0] = ENABLE_DISABLE_MOTOR;		//Comando
data[1] = 2;						//Motor 2
data[2] = 1; 						//Valor 1: encender, 0: apagar
Send_frame(Id, data, 3);			//Función genérica para enviar el mensaje. 

data[0] = ENABLE_DISABLE_MOTOR;		//Comando
data[1] = 3;						//Motor 3
data[2] = 1; 						//Valor 1: encender, 0: apagar
Send_frame(Id, data, 3);			//Función genérica para enviar el mensaje. Longitud de datos 3

Establecer velocidad constante a 5000 steps/s con rampa de aceleración por tiempo de 2s para los motores 2 y 3:

uint8_t  data[8] = {0};
int32_t  velocity = 5000;	//steps/s
uint16_t time_ms = 2000;	//ms

data[0] = SET_MOTOR_VELOCITY_TIME_RAMP;		//Comando
data[1] = 2;								//Motor 2
data[2] = (uint8_t)velocity;				//Little-endian
data[3] = (uint8_t)(velocity >> 8);				
data[4] = (uint8_t)(velocity >> 16);				
data[5] = (uint8_t)(velocity >> 24);				
data[6] = (uint8_t)time_ms;					//Little-endian
data[7] = (uint8_t)(time_ms >> 8);				

Send_frame(Id, data, 8);					//Función genérica para enviar el mensaje. Longitud de datos 8
data[1] = 3;								//Motor 3
Send_frame(Id, data, 8);					//Función genérica para enviar el mensaje. Longitud de datos 8
Enlace a la librería en GitHub

Descargo de responsabilidad

Esta librería está en fase inicial (versión 0.1.1) y seguramente contiene errores, comportamientos no previstos y tiene documentación limitada. Se ofrece tal cual, sin garantías de funcionamiento ni de ausencia de bugs.