Files
stepper_base/components/stepper_motor/stepper_motor.c
Cedarium Zhang 404f75cef5 Initial commit
2026-01-30 12:10:24 +08:00

694 lines
28 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "esp_log.h"
#include "esp_rom_sys.h"
#include "esp_random.h"
#include "stepper_motor.h"
static const char *TAG = "Stepper Motor";
/* ========== Step Sequence Definitions ========== */
/**
* @brief 8-beat half-step sequence - Clockwise (forward rotation)
* @note Alternates single-phase and dual-phase excitation for smoother motion and higher precision
*/
static const int step_sequence_cw[8][4] = {
{1, 0, 0, 0}, // Step 1: IN1 (single phase)
{1, 1, 0, 0}, // Step 2: IN1+IN2 (dual phase)
{0, 1, 0, 0}, // Step 3: IN2 (single phase)
{0, 1, 1, 0}, // Step 4: IN2+IN3 (dual phase)
{0, 0, 1, 0}, // Step 5: IN3 (single phase)
{0, 0, 1, 1}, // Step 6: IN3+IN4 (dual phase)
{0, 0, 0, 1}, // Step 7: IN4 (single phase)
{1, 0, 0, 1} // Step 8: IN4+IN1 (dual phase)
};
/**
* @brief 8-beat half-step sequence - Counterclockwise (reverse rotation)
* @note Alternates single-phase and dual-phase excitation for smoother motion and higher precision
*/
static const int step_sequence_ccw[8][4] = {
{1, 0, 0, 1}, // Step 1: IN4+IN1 (dual phase)
{0, 0, 0, 1}, // Step 2: IN4 (single phase)
{0, 0, 1, 1}, // Step 3: IN3+IN4 (dual phase)
{0, 0, 1, 0}, // Step 4: IN3 (single phase)
{0, 1, 1, 0}, // Step 5: IN2+IN3 (dual phase)
{0, 1, 0, 0}, // Step 6: IN2 (single phase)
{1, 1, 0, 0}, // Step 7: IN1+IN2 (dual phase)
{1, 0, 0, 0} // Step 8: IN1 (single phase)
};
/* ========== Private Functions ========== */
/**
* @brief Set stepper motor pin states
*
* @param in1 IN1 pin state (0 or 1)
* @param in2 IN2 pin state (0 or 1)
* @param in3 IN3 pin state (0 or 1)
* @param in4 IN4 pin state (0 or 1)
*/
static void set_motor_pins(int in1, int in2, int in3, int in4)
{
gpio_set_level(IN1_PIN, in1);
gpio_set_level(IN2_PIN, in2);
gpio_set_level(IN3_PIN, in3);
gpio_set_level(IN4_PIN, in4);
}
/**
* @brief Stepper motor clockwise one step
*
* @param step Current step number (will be auto modulo 8)
*/
static void stepper_step_cw(int step)
{
step = step % 8; // Ensure step is in range 0-7 (8 steps in half-step mode)
set_motor_pins(step_sequence_cw[step][0],
step_sequence_cw[step][1],
step_sequence_cw[step][2],
step_sequence_cw[step][3]);
}
/**
* @brief Stepper motor counterclockwise one step
*
* @param step Current step number (will be auto modulo 8)
*/
static void stepper_step_ccw(int step)
{
step = step % 8; // Ensure step is in range 0-7 (8 steps in half-step mode)
set_motor_pins(step_sequence_ccw[step][0],
step_sequence_ccw[step][1],
step_sequence_ccw[step][2],
step_sequence_ccw[step][3]);
}
/**
* @brief Microsecond precision delay function
*
* @param delay_us Delay time (microseconds)
*/
static inline void precise_delay_us(int delay_us)
{
if (delay_us > 0) {
esp_rom_delay_us(delay_us);
}
}
/* ========== Public Function Implementations ========== */
/**
* @brief Clockwise rotation with acceleration/deceleration
*
* @param steps Number of rotation steps
* @param target_delay_us Target speed delay (microseconds)
*
* @note Uses linear acceleration/deceleration algorithm, divided into acceleration, constant speed, and deceleration phases
*/
static void stepper_rotate_cw_with_accel(int steps, int target_delay_us)
{
int start_delay_us = STEPPER_START_DELAY_US;
int accel_steps = STEPPER_ACCEL_STEPS;
int decel_steps = STEPPER_DECEL_STEPS;
// If total steps too few, adjust acceleration/deceleration steps
if (steps < (accel_steps + decel_steps)) {
accel_steps = steps / 3;
decel_steps = steps / 3;
}
int constant_steps = steps - accel_steps - decel_steps;
int current_delay_us;
ESP_LOGD(TAG, "Accel-Decel profile: accel=%d, constant=%d, decel=%d steps",
accel_steps, constant_steps, decel_steps);
// Acceleration phase
for (int i = 0; i < accel_steps; i++) {
// Linear interpolation: gradually decrease from start_delay to target_delay
current_delay_us = start_delay_us - (start_delay_us - target_delay_us) * i / accel_steps;
stepper_step_cw(i);
precise_delay_us(current_delay_us);
}
// Constant speed phase
for (int i = accel_steps; i < accel_steps + constant_steps; i++) {
stepper_step_cw(i);
precise_delay_us(target_delay_us);
}
// Deceleration phase
for (int i = accel_steps + constant_steps; i < steps; i++) {
// Linear interpolation: gradually increase from target_delay to start_delay
int decel_progress = i - (accel_steps + constant_steps);
current_delay_us = target_delay_us + (start_delay_us - target_delay_us) * decel_progress / decel_steps;
stepper_step_cw(i);
precise_delay_us(current_delay_us);
}
}
/**
* @brief Counterclockwise rotation with acceleration/deceleration
*
* @param steps Number of rotation steps
* @param target_delay_us Target speed delay (microseconds)
*
* @note Uses linear acceleration/deceleration algorithm, divided into acceleration, constant speed, and deceleration phases
*/
static void stepper_rotate_ccw_with_accel(int steps, int target_delay_us)
{
int start_delay_us = STEPPER_START_DELAY_US;
int accel_steps = STEPPER_ACCEL_STEPS;
int decel_steps = STEPPER_DECEL_STEPS;
// If total steps too few, adjust acceleration/deceleration steps
if (steps < (accel_steps + decel_steps)) {
accel_steps = steps / 3;
decel_steps = steps / 3;
}
int constant_steps = steps - accel_steps - decel_steps;
int current_delay_us;
ESP_LOGD(TAG, "Accel-Decel profile: accel=%d, constant=%d, decel=%d steps",
accel_steps, constant_steps, decel_steps);
// Acceleration phase
for (int i = 0; i < accel_steps; i++) {
current_delay_us = start_delay_us - (start_delay_us - target_delay_us) * i / accel_steps;
stepper_step_ccw(i);
precise_delay_us(current_delay_us);
}
// Constant speed phase
for (int i = accel_steps; i < accel_steps + constant_steps; i++) {
stepper_step_ccw(i);
precise_delay_us(target_delay_us);
}
// Deceleration phase
for (int i = accel_steps + constant_steps; i < steps; i++) {
int decel_progress = i - (accel_steps + constant_steps);
current_delay_us = target_delay_us + (start_delay_us - target_delay_us) * decel_progress / decel_steps;
stepper_step_ccw(i);
precise_delay_us(current_delay_us);
}
}
/**
* @brief Rotate by specified angle (with acceleration/deceleration)
*
* @param angle Rotation angle, positive for right (clockwise), negative for left (counterclockwise)
* @param target_delay_us Target speed delay (microseconds), will automatically accelerate from slow speed to this speed at start
*
* @note Half-step mode: 4128 steps = 360 degrees
* @note Uses linear acceleration/deceleration algorithm to ensure smooth start and stop
*/
void stepper_rotate_angle_with_accel(float angle, int target_delay_us)
{
// Half-step mode: 4128 steps = 360 degrees
int steps = (int)(angle * 4128.0 / 360.0 + 0.5);
if (steps == 0) {
ESP_LOGI(TAG, "Angle too small, no rotation needed");
return;
}
if (steps > 0) {
ESP_LOGD(TAG, "Rotating %.1f° CW with acceleration (%d steps, target %dus/step)",
angle, steps, target_delay_us);
stepper_rotate_cw_with_accel(steps, target_delay_us);
} else {
ESP_LOGD(TAG, "Rotating %.1f° CCW with acceleration (%d steps, target %dus/step)",
angle, -steps, target_delay_us);
stepper_rotate_ccw_with_accel(-steps, target_delay_us);
}
}
/**
* @brief Shake head action function
*
* @param amplitude Shake amplitude (one-sided angle), e.g., 30 means shake 30 degrees left and right
* @param cycles Number of shake cycles, one complete left-right shake counts as 1
* @param speed_us Shake speed (microsecond delay), recommend using STEPPER_SPEED_xxx macros
*
* Motion sequence (example with amplitude=30, cycles=2):
* 1. Turn left to -30° → pause
* 2. Turn right to +30° → pause → turn left to -30° → pause (cycle 1)
* 3. Turn right to +30° → pause → turn left to -30° → pause (cycle 2)
* 4. Return to center 0°
*/
void stepper_shake_head(float amplitude, int cycles, int speed_us)
{
if (amplitude <= 0 || cycles <= 0) {
ESP_LOGW(TAG, "Invalid shake head parameters: amplitude=%.1f, cycles=%d", amplitude, cycles);
return;
}
ESP_LOGD(TAG, "Shake head started: amplitude=%.1f°, cycles=%d, speed=%dus/step",
amplitude, cycles, speed_us);
// Step 1: Turn left from center to start position
ESP_LOGD(TAG, "Moving to left start position (%.1f°)", -amplitude);
stepper_rotate_angle_with_accel(-amplitude, speed_us);
vTaskDelay(pdMS_TO_TICKS(10)); // Brief pause
// Shake cycles
for (int i = 0; i < cycles; i++) {
ESP_LOGD(TAG, "Shake cycle %d/%d", i + 1, cycles);
// Turn right (from left to right, rotate 2*amplitude degrees)
stepper_rotate_angle_with_accel(2 * amplitude, speed_us);
vTaskDelay(pdMS_TO_TICKS(10)); // Brief pause
// Turn left (from right to left, rotate 2*amplitude degrees)
stepper_rotate_angle_with_accel(-2 * amplitude, speed_us);
vTaskDelay(pdMS_TO_TICKS(10)); // Brief pause
}
// Last step: Return from left to center position
ESP_LOGD(TAG, "Returning to center position");
stepper_rotate_angle_with_accel(amplitude, speed_us);
}
/**
* @brief Gradually decaying shake head action function
*
* @param initial_amplitude Initial shake amplitude (one-sided angle), e.g., 30 means initially shake 30 degrees left and right
* @param decay_rate Decay rate
* - Range 0.0~1.0: Percentage decay, e.g., 0.8 means amplitude becomes 80% each time
* - >1.0: Fixed angle decay, e.g., 5.0 means decrease by 5 degrees each time
* @param speed_us Shake speed (microsecond delay), recommend using STEPPER_SPEED_xxx macros
*
* @note Shake amplitude gradually decreases until below minimum threshold (5 degrees) then stops
* @note Speed automatically decreases when amplitude is small to avoid jitter
*/
void stepper_shake_head_decay(float initial_amplitude, float decay_rate, int speed_us)
{
if (initial_amplitude <= 0) {
ESP_LOGW(TAG, "Invalid amplitude: %.1f", initial_amplitude);
return;
}
if (decay_rate <= 0) {
ESP_LOGW(TAG, "Invalid decay rate: %.3f", decay_rate);
return;
}
ESP_LOGD(TAG, "Decay shake head started: initial_amplitude=%.1f°, decay_rate=%.3f, speed=%dus/step",
initial_amplitude, decay_rate, speed_us);
float current_amplitude = initial_amplitude;
float min_amplitude = 5.0; // Minimum amplitude threshold (degrees), stop shaking below this to avoid small angle jitter
float smooth_threshold = 8.0; // Smooth threshold (degrees), reduce speed below this
int cycle_count = 0;
bool use_percentage_decay = (decay_rate > 0.0 && decay_rate < 1.0); // Determine if percentage decay or fixed value decay
float current_position = 0.0; // Track current position relative to center
// Step 1: Turn left from center to start position
ESP_LOGD(TAG, "Moving to left start position (%.1f°)", -current_amplitude);
stepper_rotate_angle_with_accel(-current_amplitude, speed_us);
current_position = -current_amplitude;
vTaskDelay(pdMS_TO_TICKS(10)); // Brief pause
// Shake loop, until amplitude below threshold
while (current_amplitude >= min_amplitude) {
cycle_count++;
// Dynamically adjust speed based on current amplitude, slower speed for smaller amplitude to avoid jitter
int current_speed_us = speed_us;
if (current_amplitude < smooth_threshold) {
// Linear interpolation: speed factor increases from 1.0 to 1.3 (max 30% slowdown) as amplitude decreases from smooth_threshold to min_amplitude
float speed_factor = 1.0 + 0.3 * (smooth_threshold - current_amplitude) / (smooth_threshold - min_amplitude);
current_speed_us = (int)(speed_us * speed_factor);
ESP_LOGD(TAG, "Decay shake cycle %d: amplitude=%.1f°, speed adjusted to %dus/step (factor=%.2f)",
cycle_count, current_amplitude, current_speed_us, speed_factor);
} else {
ESP_LOGD(TAG, "Decay shake cycle %d: amplitude=%.1f°", cycle_count, current_amplitude);
}
// Turn right (from left to right, rotate 2*current_amplitude degrees)
stepper_rotate_angle_with_accel(2 * current_amplitude, current_speed_us);
current_position += 2 * current_amplitude;
vTaskDelay(pdMS_TO_TICKS(15)); // Brief pause, longer when amplitude is small
// Calculate next amplitude
float next_amplitude;
if (use_percentage_decay) {
// Percentage decay mode: amplitude = current amplitude × decay rate
next_amplitude = current_amplitude * decay_rate;
ESP_LOGD(TAG, "Percentage decay: next amplitude=%.1f° (%.1f%%)",
next_amplitude, decay_rate * 100);
} else {
// Fixed value decay mode: amplitude = current amplitude - decay value
next_amplitude = current_amplitude - decay_rate;
ESP_LOGD(TAG, "Fixed decay: next amplitude=%.1f° (-%.1f°)",
next_amplitude, decay_rate);
}
// Check if should continue shaking
if (next_amplitude < min_amplitude) {
ESP_LOGD(TAG, "Next amplitude too small (%.1f° < %.1f°), stopping at position %.1f°",
next_amplitude, min_amplitude, current_position);
break;
}
current_amplitude = next_amplitude;
// Dynamically adjust speed based on current amplitude
current_speed_us = speed_us;
if (current_amplitude < smooth_threshold) {
float speed_factor = 1.0 + 0.3 * (smooth_threshold - current_amplitude) / (smooth_threshold - min_amplitude);
current_speed_us = (int)(speed_us * speed_factor);
}
// Turn left (from right to left, rotate 2*current_amplitude degrees)
stepper_rotate_angle_with_accel(-2 * current_amplitude, current_speed_us);
current_position -= 2 * current_amplitude;
vTaskDelay(pdMS_TO_TICKS(15)); // Brief pause
// Calculate next amplitude again (symmetric decay)
if (use_percentage_decay) {
next_amplitude = current_amplitude * decay_rate;
} else {
next_amplitude = current_amplitude - decay_rate;
}
// Check next amplitude
if (next_amplitude < min_amplitude) {
ESP_LOGD(TAG, "Next amplitude too small (%.1f° < %.1f°), stopping at position %.1f°",
next_amplitude, min_amplitude, current_position);
break;
}
current_amplitude = next_amplitude;
}
// Last step: Return to center position
ESP_LOGD(TAG, "Returning to center from position %.1f°", current_position);
// Use slightly slower speed when returning to center to ensure smoothness
float abs_position = (current_position > 0) ? current_position : -current_position;
int return_speed_us = speed_us;
if (abs_position < smooth_threshold) {
// If remaining angle is small, slightly reduce return speed (max 20% slowdown)
float speed_factor = 1.2;
return_speed_us = (int)(speed_us * speed_factor);
ESP_LOGD(TAG, "Using slower speed for return: %dus/step", return_speed_us);
}
stepper_rotate_angle_with_accel(-current_position, return_speed_us);
ESP_LOGD(TAG, "Decay shake head completed: total cycles=%d", cycle_count);
}
/**
* @brief Generate random angle offset (within ±max_offset range)
*
* @param max_offset Maximum offset (degrees)
* @return float Random offset angle, range [-max_offset, +max_offset]
*
* @note Used to add randomness to motions for more natural movement
*/
static float get_random_angle_offset(float max_offset)
{
if (max_offset <= 0) {
return 0;
}
// Generate random number between 0 and 1
uint32_t random_value = esp_random();
float normalized = (float)(random_value % 10000) / 10000.0; // 0.0 ~ 1.0
// Convert to range -max_offset to +max_offset
float offset = (normalized * 2.0 - 1.0) * max_offset;
return offset;
}
/**
* @brief Look around action function (with small amplitude scanning + random offset)
*
* @param left_angle Main angle to turn left (positive value), e.g., 45 means turn left 45 degrees
* @param right_angle Main angle to turn right (positive value), e.g., 45 means turn right 45 degrees
* @param scan_angle Small amplitude scanning angle at left and right sides (positive value), e.g., 10 means scan 10 degrees left and right
* @param pause_ms Pause time after each movement (milliseconds), simulating "observation" motion
* @param large_speed_us Large movement speed (microsecond delay), used for main position rotation
* @param small_speed_us Small scanning speed (microsecond delay), used for small range observation, recommend slower than large_speed_us
*
* @note Random offset is added to each rotation for more natural motion
* @note Large movement: random offset ±10°
* @note Small scanning: random offset ±10°
*
* Motion sequence (example with left_angle=45, scan_angle=10, actual values will have random offset):
* 1. Turn left to -45°±10° (large speed) → pause
* 2. Scan left to -55°±10° (small speed) → pause
* 3. Scan right to -35°±10° (small speed) → pause
* 4. Turn right to +45°±10° (large speed) → pause
* 5. Scan right to +55°±10° (small speed) → pause
* 6. Scan left to +35°±10° (small speed) → pause
* 7. Return to center 0° (large speed)
*/
void stepper_look_around(float left_angle, float right_angle, float scan_angle, int pause_ms, int large_speed_us, int small_speed_us)
{
if (left_angle < 0 || right_angle < 0 || scan_angle < 0) {
ESP_LOGW(TAG, "Invalid angles: left=%.1f, right=%.1f, scan=%.1f (should be positive)",
left_angle, right_angle, scan_angle);
return;
}
if (pause_ms < 0) {
pause_ms = 0;
}
ESP_LOGD(TAG, "Look around started: left=%.1f°, right=%.1f°, scan=%.1f°, pause=%dms, large_speed=%dus, small_speed=%dus",
left_angle, right_angle, scan_angle, pause_ms, large_speed_us, small_speed_us);
// Accumulated position tracking (relative to starting center position)
float accumulated_position = 0.0;
// ========== Phase 1: Look Left ==========
if (left_angle > 0) {
// 1. Turn left to main position (large amplitude, fast) + random offset
float left_offset = get_random_angle_offset(10.0); // ±10 degrees random
float actual_left_angle = left_angle + left_offset;
stepper_rotate_angle_with_accel(-actual_left_angle, large_speed_us);
accumulated_position -= actual_left_angle; // Track accumulated position
vTaskDelay(pdMS_TO_TICKS(pause_ms));
// 2. Scan left with small amplitude (slow speed) + random offset
if (scan_angle > 0) {
float scan_left_offset = get_random_angle_offset(10.0); // ±10 degrees random
float actual_scan_left = scan_angle + scan_left_offset;
// Ensure small amplitude rotation is not less than 5 degrees
if (actual_scan_left < 5.0) {
actual_scan_left = 5.0;
scan_left_offset = actual_scan_left - scan_angle;
}
stepper_rotate_angle_with_accel(-actual_scan_left, small_speed_us);
accumulated_position -= actual_scan_left; // Track accumulated position
vTaskDelay(pdMS_TO_TICKS(pause_ms));
// 3. Scan right directly with small amplitude (slow speed, don't return to main position) + random offset
float scan_right_offset = get_random_angle_offset(10.0); // ±10 degrees random
float actual_scan_range = 2 * scan_angle + scan_left_offset - scan_right_offset;
// Ensure small amplitude rotation is not less than 5 degrees
if (actual_scan_range < 5.0) {
actual_scan_range = 5.0;
}
stepper_rotate_angle_with_accel(actual_scan_range, small_speed_us);
accumulated_position += actual_scan_range; // Track accumulated position
vTaskDelay(pdMS_TO_TICKS(pause_ms));
}
}
// ========== Phase 2: Look Right ==========
if (right_angle > 0) {
// 4. Turn from current position to right main position (fast) + random offset
float right_offset = get_random_angle_offset(10.0); // ±10 degrees random
float actual_right_angle = right_angle + right_offset;
// Calculate rotation angle: turn from current accumulated position to right side
float turn_angle = actual_right_angle - accumulated_position;
stepper_rotate_angle_with_accel(turn_angle, large_speed_us);
accumulated_position += turn_angle; // Add actual rotated angle to avoid precision errors
vTaskDelay(pdMS_TO_TICKS(pause_ms));
// 5. Scan right with small amplitude (slow speed) + random offset
if (scan_angle > 0) {
float scan_right2_offset = get_random_angle_offset(10.0); // ±10 degrees random
float actual_scan_right = scan_angle + scan_right2_offset;
// Ensure small amplitude rotation is not less than 5 degrees
if (actual_scan_right < 5.0) {
actual_scan_right = 5.0;
scan_right2_offset = actual_scan_right - scan_angle;
}
stepper_rotate_angle_with_accel(actual_scan_right, small_speed_us);
accumulated_position += actual_scan_right; // Track accumulated position
vTaskDelay(pdMS_TO_TICKS(pause_ms));
// 6. Scan left directly with small amplitude (slow speed, don't return to main position) + random offset
float scan_left2_offset = get_random_angle_offset(10.0); // ±10 degrees random
float actual_scan_range2 = 2 * scan_angle + scan_right2_offset - scan_left2_offset;
// Ensure small amplitude rotation is not less than 5 degrees
if (actual_scan_range2 < 5.0) {
actual_scan_range2 = 5.0;
}
stepper_rotate_angle_with_accel(-actual_scan_range2, small_speed_us);
accumulated_position -= actual_scan_range2; // Track accumulated position
vTaskDelay(pdMS_TO_TICKS(pause_ms));
}
}
// ========== Phase 3: Return to Center ==========
// Use accumulated position to return directly to center (tolerance: 0.5 degrees)
float abs_position = (accumulated_position > 0) ? accumulated_position : -accumulated_position;
if (abs_position > 0.5) {
stepper_rotate_angle_with_accel(-accumulated_position, large_speed_us);
} else {
ESP_LOGD(TAG, "Already near center (offset=%.2f°)", accumulated_position);
}
ESP_LOGD(TAG, "Look around completed");
}
/**
* @brief Follow drum beat swing function
*
* @param angle Swing angle for each beat (positive value), e.g., 10 means swing 10 degrees left and right
* @param speed_us Rotation speed (microsecond delay)
*
* @note Each call to this function automatically switches direction (using static variable to track state)
* @note Call sequence: left → right → left → right ...
*
* Typical usage: Call this function when music beat is detected, motor will swing left and right following the beat
*/
void stepper_beat_swing(float angle, int speed_us)
{
// Static variable to track current swing direction, false=left, true=right
static bool swing_direction = false;
if (angle <= 0) {
ESP_LOGW(TAG, "Invalid angle: %.1f (should be positive)", angle);
return;
}
if (swing_direction) {
// Turn right
stepper_rotate_angle_with_accel(angle, speed_us);
} else {
// Turn left
stepper_rotate_angle_with_accel(-angle, speed_us);
}
// Switch direction
swing_direction = !swing_direction;
}
/**
* @brief Cat nuzzling action function (gently turn left and return to center, repeat several times)
*
* @param angle Angle to turn left (positive value), e.g., 20 means turn left 20 degrees
* @param cycles Number of nuzzles, each includes a complete "turn-return to center" motion
* @param speed_us Rotation speed (microsecond delay), recommend using slower speed like STEPPER_SPEED_SLOW for gentle feel
*
* Motion sequence (example with angle=20, cycles=3):
* 1. Turn left to -20° (slow) → pause 100ms → return to center 0° (slow) → pause 50ms
* 2. Turn left to -20° (slow) → pause 100ms → return to center 0° (slow) → pause 50ms
* 3. Turn left to -20° (slow) → pause 100ms → return to center 0° (slow)
*
* @note Uses slow smooth acceleration/deceleration throughout for gentle feel
* @note Brief pause after each turn to simulate cat nuzzling contact feel
*/
void stepper_cat_nuzzle(float angle, int cycles, int speed_us)
{
if (angle <= 0) {
ESP_LOGW(TAG, "Invalid angle: %.1f (should be positive)", angle);
return;
}
if (cycles <= 0) {
ESP_LOGW(TAG, "Invalid cycles: %d (should be positive)", cycles);
return;
}
ESP_LOGD(TAG, "Cat nuzzle started: angle=%.1f°, cycles=%d, speed=%dus/step",
angle, cycles, speed_us);
// Execute nuzzle cycles
for (int i = 0; i < cycles; i++) {
ESP_LOGD(TAG, "Nuzzle cycle %d/%d", i + 1, cycles);
// Slowly turn left
stepper_rotate_angle_with_accel(-angle, speed_us);
vTaskDelay(pdMS_TO_TICKS(100)); // Pause 100ms to simulate nuzzling contact feel
// Slowly return to center
stepper_rotate_angle_with_accel(angle, speed_us);
// Add brief pause between cycles (not after last cycle)
if (i < cycles - 1) {
vTaskDelay(pdMS_TO_TICKS(50)); // Brief pause after each nuzzle, prepare for next one
}
}
ESP_LOGD(TAG, "Cat nuzzle completed");
}
/**
* @brief Turn off all stepper motor coils (power off)
*
* @note After calling this function, motor will no longer hold position and can be rotated by external force
* @note Saves power consumption and avoids motor heating from prolonged energization
*/
void stepper_motor_power_off(void)
{
set_motor_pins(0, 0, 0, 0);
}
/**
* @brief Initialize stepper motor GPIO pins
*
* @note Configures IN1-IN4 pins as output mode, initial state is low level
*/
void stepper_motor_gpio_init(void)
{
gpio_config_t io_conf = {
.pin_bit_mask = (1ULL << IN1_PIN) | (1ULL << IN2_PIN) | (1ULL << IN3_PIN) | (1ULL << IN4_PIN),
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE
};
gpio_config(&io_conf);
// Set all pins to low level
gpio_set_level(IN1_PIN, 0);
gpio_set_level(IN2_PIN, 0);
gpio_set_level(IN3_PIN, 0);
gpio_set_level(IN4_PIN, 0);
ESP_LOGI(TAG, "GPIO initialization completed, IN1-IN4 pins set as output mode with default low level");
}