/* --- Peripheral handles --- */
static TIM_HandleTypeDef htim2; /* Step pulse timer */
static TIM_HandleTypeDef htim3; /* Encoder timer */
static UART_HandleTypeDef huart1;
/* --- Stepper configuration --- */
#define STEPS_PER_REV 1600 /* 200 * 8 (1/8 microstepping) */
#define TIMER_FREQ 1000000 /* 1 MHz (prescaler = 71) */
#define MAX_SPEED 800 /* steps/sec */
#define MIN_SPEED 100 /* steps/sec (start/stop speed) */
#define ACCELERATION 2000 /* steps/sec^2 */
#define HOMING_SPEED 200 /* steps/sec */
#define HOMING_BACKOFF 50 /* steps/sec (slow approach) */
#define MAX_TRAVEL_STEPS 16000 /* Software travel limit */
#define ENCODER_INIT_POS 32768
#define ENCODER_SCALE 10 /* 10 steps per encoder count */
/* --- Motion planner state --- */
static volatile int32_t current_pos = 0; /* Current position in steps */
static volatile int32_t target_pos = 0; /* Target position in steps */
static volatile int32_t accel_steps = 0; /* Steps taken during acceleration */
static volatile int32_t decel_start = 0; /* Step count at which deceleration begins */
static volatile int32_t total_steps = 0; /* Total steps for current move */
static volatile int32_t step_count = 0; /* Steps taken in current move */
static volatile uint32_t current_period = 0; /* Current timer period (ticks) */
static volatile uint32_t cruise_period = 0; /* Period at cruise speed */
static volatile float current_speed = 0; /* Current speed in steps/sec */
static volatile MotionState_t motion_state = MOTION_IDLE;
static volatile SystemState_t sys_state = SYS_HOMING;
static volatile uint8_t step_pin_state = 0;
static volatile uint8_t homed = 0;
static volatile int8_t homing_next_phase = -1; /* -1 = no pending, 0/1/2 = start phase */
/* UART transmit buffer */
static char uart_buf[128];
/* --- GPIO helpers --- */
static void Stepper_SetDir(uint8_t forward)
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1,
forward ? GPIO_PIN_SET : GPIO_PIN_RESET);
static void Stepper_Enable(uint8_t en)
/* ENABLE is active low */
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2,
en ? GPIO_PIN_RESET : GPIO_PIN_SET);
static void Stepper_SetMicrostepping(uint8_t ms1, uint8_t ms2, uint8_t ms3)
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_3, ms1 ? GPIO_PIN_SET : GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_4, ms2 ? GPIO_PIN_SET : GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_5, ms3 ? GPIO_PIN_SET : GPIO_PIN_RESET);
static uint8_t LimitHome_Read(void)
/* NC switch to GND with pull-up: HIGH = triggered (open), LOW = normal (closed) */
return HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_14) == GPIO_PIN_SET;
static uint8_t LimitEnd_Read(void)
return HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_15) == GPIO_PIN_SET;
static uint8_t EncoderButton_Read(void)
return HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_10) == GPIO_PIN_RESET; /* Active low */
static int32_t Encoder_GetDelta(void)
int32_t count = (int32_t)__HAL_TIM_GET_COUNTER(&htim3);
int32_t delta = count - (int32_t)ENCODER_INIT_POS;
__HAL_TIM_SET_COUNTER(&htim3, ENCODER_INIT_POS); /* Reset to midpoint */
static void LED_Set(uint8_t on)
/* PC13 is active low on Blue Pill */
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, on ? GPIO_PIN_RESET : GPIO_PIN_SET);
static void UART_Print(const char *msg)
HAL_UART_Transmit(&huart1, (uint8_t *)msg, strlen(msg), 100);
/* --- Motion planner --- */
static uint32_t Speed_To_Period(float speed)
if (speed < 1.0f) return TIMER_FREQ; /* Extremely slow, cap the period */
return (uint32_t)((float)TIMER_FREQ / speed);
static void Motion_Start(int32_t steps_to_move)
if (steps_to_move == 0) return;
Stepper_SetDir(steps_to_move > 0 ? 1 : 0);
total_steps = (steps_to_move > 0) ? steps_to_move : -steps_to_move;
current_speed = (float)MIN_SPEED;
current_period = Speed_To_Period(current_speed);
cruise_period = Speed_To_Period((float)MAX_SPEED);
/* Calculate acceleration and deceleration distances */
/* Steps to accelerate from MIN_SPEED to MAX_SPEED:
v^2 = v0^2 + 2*a*s => s = (v^2 - v0^2) / (2*a) */
float accel_dist = ((float)MAX_SPEED * (float)MAX_SPEED
- (float)MIN_SPEED * (float)MIN_SPEED)
/ (2.0f * (float)ACCELERATION);
accel_steps = (int32_t)accel_dist;
/* If total distance is too short for full accel+decel, use triangular profile */
if (2 * accel_steps >= total_steps) {
accel_steps = total_steps / 2;
decel_start = total_steps - accel_steps;
motion_state = MOTION_ACCEL;
__HAL_TIM_SET_AUTORELOAD(&htim2, current_period);
__HAL_TIM_SET_COUNTER(&htim2, 0);
HAL_TIM_Base_Start_IT(&htim2);
static void Motion_Stop(void)
HAL_TIM_Base_Stop_IT(&htim2);
motion_state = MOTION_IDLE;
static void Motion_StartHoming(uint8_t phase)
/* Phase 1: seek toward home switch */
Stepper_SetDir(0); /* Move toward home (negative direction) */
period = Speed_To_Period((float)HOMING_SPEED);
motion_state = MOTION_HOMING_SEEK;
/* Phase 2: back off from switch */
period = Speed_To_Period((float)HOMING_BACKOFF);
motion_state = MOTION_HOMING_BACKOFF;
/* Phase 3: fine approach */
period = Speed_To_Period((float)HOMING_BACKOFF);
motion_state = MOTION_HOMING_FINE;
__HAL_TIM_SET_AUTORELOAD(&htim2, period);
__HAL_TIM_SET_COUNTER(&htim2, 0);
HAL_TIM_Base_Start_IT(&htim2);
/* Timer interrupt handler: generate one step pulse and update speed */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
if (htim->Instance != TIM2) return;
/* Generate step pulse (toggle) */
step_pin_state = !step_pin_state;
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0,
step_pin_state ? GPIO_PIN_SET : GPIO_PIN_RESET);
/* Only count on rising edge */
if (!step_pin_state) return;
/* --- Homing modes --- */
* Homing phase transitions set a flag; the main loop picks up
* the flag and calls Motion_StartHoming() outside the ISR.
* Never call HAL_Delay or blocking functions inside an ISR.
if (motion_state == MOTION_HOMING_SEEK) {
homing_next_phase = 1; /* Main loop starts backoff */
if (motion_state == MOTION_HOMING_BACKOFF) {
homing_next_phase = 2; /* Main loop starts fine approach */
if (motion_state == MOTION_HOMING_FINE) {
/* --- Normal motion --- */
if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_SET) {
/* Check limit switches */
if (LimitHome_Read() || LimitEnd_Read()) {
sys_state = SYS_LIMIT_HIT;
/* Check if move complete */
if (step_count >= total_steps) {
/* Update speed based on profile phase */
if (step_count < accel_steps && motion_state != MOTION_DECEL) {
motion_state = MOTION_ACCEL;
current_speed += (float)ACCELERATION / current_speed;
if (current_speed >= (float)MAX_SPEED) {
current_speed = (float)MAX_SPEED;
motion_state = MOTION_CRUISE;
} else if (step_count >= decel_start) {
motion_state = MOTION_DECEL;
current_speed -= (float)ACCELERATION / current_speed;
if (current_speed < (float)MIN_SPEED) {
current_speed = (float)MIN_SPEED;
motion_state = MOTION_CRUISE;
current_period = Speed_To_Period(current_speed);
__HAL_TIM_SET_AUTORELOAD(&htim2, current_period);
/* --- System clock and peripheral init --- */
static void SystemClock_Config(void)
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
HAL_RCC_OscConfig(&RCC_OscInitStruct);
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK
| RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2);
static void MX_GPIO_Init(void)
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
__HAL_RCC_GPIOC_CLK_ENABLE();
/* PA0: STEP (output), PA1: DIR (output), PA2: ENABLE (output) */
GPIO_InitStruct.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
/* PB3: MS1, PB4: MS2, PB5: MS3 (outputs) */
GPIO_InitStruct.Pin = GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
/* PB10: Encoder button (input, pull-up) */
GPIO_InitStruct.Pin = GPIO_PIN_10;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
/* PB14: Home limit, PB15: End limit (inputs, pull-up) */
GPIO_InitStruct.Pin = GPIO_PIN_14 | GPIO_PIN_15;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
/* PC13: Onboard LED (output) */
GPIO_InitStruct.Pin = GPIO_PIN_13;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET); /* LED off */
/* Initial stepper state: disabled */
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_SET); /* ENABLE high = disabled */
static void MX_TIM2_Init(void)
__HAL_RCC_TIM2_CLK_ENABLE();
htim2.Init.Prescaler = 71; /* 72 MHz / 72 = 1 MHz */
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
htim2.Init.Period = 10000; /* Initial period (overwritten before use) */
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
HAL_TIM_Base_Init(&htim2);
HAL_NVIC_SetPriority(TIM2_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(TIM2_IRQn);
static void MX_TIM3_Encoder_Init(void)
__HAL_RCC_TIM3_CLK_ENABLE();
TIM_Encoder_InitTypeDef sEncoder = {0};
TIM_MasterConfigTypeDef sMaster = {0};
/* PA6 and PA7 for encoder channels */
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_6 | GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
htim3.Init.Prescaler = 0;
htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
htim3.Init.Period = 65535;
htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
HAL_TIM_Base_Init(&htim3);
sEncoder.EncoderMode = TIM_ENCODERMODE_TI12;
sEncoder.IC1Polarity = TIM_ICPOLARITY_RISING;
sEncoder.IC1Selection = TIM_ICSELECTION_DIRECTTI;
sEncoder.IC1Prescaler = TIM_ICPSC_DIV1;
sEncoder.IC1Filter = 0x0F;
sEncoder.IC2Polarity = TIM_ICPOLARITY_RISING;
sEncoder.IC2Selection = TIM_ICSELECTION_DIRECTTI;
sEncoder.IC2Prescaler = TIM_ICPSC_DIV1;
sEncoder.IC2Filter = 0x0F;
HAL_TIM_Encoder_Init(&htim3, &sEncoder);
sMaster.MasterOutputTrigger = TIM_TRGO_RESET;
sMaster.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
HAL_TIMEx_MasterConfigSynchronization(&htim3, &sMaster);
__HAL_TIM_SET_COUNTER(&htim3, ENCODER_INIT_POS);
HAL_TIM_Encoder_Start(&htim3, TIM_CHANNEL_ALL);
static void MX_USART1_Init(void)
__HAL_RCC_USART1_CLK_ENABLE();
GPIO_InitTypeDef GPIO_InitStruct = {0};
/* PA9: TX (AF push-pull) */
GPIO_InitStruct.Pin = GPIO_PIN_9;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
/* PA10: RX (input floating) */
GPIO_InitStruct.Pin = GPIO_PIN_10;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
/* --- LED blink patterns --- */
static uint32_t led_last_toggle = 0;
static void LED_Update(void)
uint32_t now = HAL_GetTick();
if (now - led_last_toggle >= 250) {
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
if (now - led_last_toggle >= 100) {
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
/* --- Main application --- */
/* Set 1/8 microstepping: MS1=HIGH, MS2=HIGH, MS3=LOW */
Stepper_SetMicrostepping(1, 1, 0);
UART_Print("Stepper Positioning Stage\r\n");
UART_Print("Homing...\r\n");
/* Start homing sequence */
int32_t encoder_target = 0;
/* Handle homing phase transitions (deferred from ISR) */
if (sys_state == SYS_HOMING) {
if (homing_next_phase >= 0) {
int8_t phase = homing_next_phase;
HAL_Delay(50); /* Settling delay (safe here in main loop) */
Motion_StartHoming(phase);
UART_Print("Homed. Position: 0\r\n");
UART_Print("Turn encoder to set target, press to move.\r\n");
/* Handle limit hit state: require encoder button press to acknowledge */
if (sys_state == SYS_LIMIT_HIT) {
if (EncoderButton_Read() && !button_prev) {
UART_Print("Limit cleared. Re-home recommended.\r\n");
button_prev = EncoderButton_Read();
/* Read encoder for target adjustment (only when idle) */
if (sys_state == SYS_IDLE) {
int32_t delta = Encoder_GetDelta();
encoder_target += delta * ENCODER_SCALE;
/* Clamp to travel range */
if (encoder_target < 0) encoder_target = 0;
if (encoder_target > MAX_TRAVEL_STEPS) encoder_target = MAX_TRAVEL_STEPS;
snprintf(uart_buf, sizeof(uart_buf),
"Target: %ld steps (%ld.%01ld rev)\r\n",
(long)(encoder_target / STEPS_PER_REV),
(long)((encoder_target % STEPS_PER_REV) * 10 / STEPS_PER_REV));
/* Button press: execute move */
uint8_t button = EncoderButton_Read();
if (button && !button_prev) {
int32_t move = encoder_target - current_pos;
snprintf(uart_buf, sizeof(uart_buf),
"Moving: %ld -> %ld (%ld steps)\r\n",
(long)current_pos, (long)encoder_target, (long)move);
target_pos = encoder_target;
/* Check if move completed */
if (sys_state == SYS_MOVING && motion_state == MOTION_IDLE) {
snprintf(uart_buf, sizeof(uart_buf),
"Arrived: %ld steps\r\n", (long)current_pos);
/* Periodic position report */
if (HAL_GetTick() - last_print >= 500) {
last_print = HAL_GetTick();
if (sys_state == SYS_MOVING) {
snprintf(uart_buf, sizeof(uart_buf),
"Pos: %ld Speed: %d sps State: %s\r\n",
(long)current_pos, (int)current_speed,
motion_state == MOTION_ACCEL ? "ACCEL" :
motion_state == MOTION_CRUISE ? "CRUISE" :
motion_state == MOTION_DECEL ? "DECEL" : "IDLE");
/* --- IRQ Handlers --- */
void TIM2_IRQHandler(void)
HAL_TIM_IRQHandler(&htim2);
void SysTick_Handler(void)
Comments