这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » DIY与开源设计 » 电子DIY » 【let'sdo2026年第1期】静音步进电机控制实践过程贴-控制系统的搭建

共1条 1/1 1 跳转至

【let'sdo2026年第1期】静音步进电机控制实践过程贴-控制系统的搭建

高工
2026-06-25 21:50:29   被打赏 30 分(兑奖)     打赏

        前面我分别讲清楚了"电机要什么信号"和"H533 怎么配硬件"。但"配硬件"和"让电机转起来"之间,还差一座桥 ——应用软件。如果说 STEP/DIR/EN是MCU对电机的"语言",那shell就是我对MCU的语言。人通过 shell 说话,shell 把命令翻译成对步进电机驱动的调用,驱动去拨弄GPIO和TIM3,电机就动了。

stepper 驱动:把GPIO包装成"动作"

        我想要的不是一个"操作寄存器的工具集",而是一个有状态的对象。什么意思?就是说:

  • 你不用关心EN引脚现在是什么电平。

  • 你不用关心TIM3当前ARR是多少。

  • 你不用关心上次的命令和这次有没有冲突。

        你只管告诉驱动:"我要1kHz正转""停""反转",剩下的它自己处理。

        所以 stepper 模块对外暴露的不是十几个函数,而是几个语义清晰的动词

函数动作副作用
Stepper_Init()初始化EN=HIGH、PWM 停、DIR=FWD
Stepper_Start()启动EN=LOW、PWM 启动、状态=RUNNING
Stepper_Stop()停止PWM 停、EN=HIGH、状态=IDLE
Stepper_SetDirection(dir)换向DIR 引脚翻转到对应电平
Stepper_SetSpeed(hz)变速改 ARR、保持 50% duty

状态机

        模块内部维护一个 Stepper_t 结构体,记录三个核心状态:

typedef struct {
  uint32_t       step_freq_hz;  // 当前 STEP 频率
  StepperDir_t   dir;           // 当前方向
  StepperState_t state;         // IDLE 还是 RUNNING
} Stepper_t;

状态机只有两个状态:

Stepper_Start()        Stepper_Stop()
   ┌──────────────────────┐  ┌─────────────────────┐
   │                      ↓  ↓                     │
   │   IDLE  ────────────────→  RUNNING            │
   │   ↑  ←────────────────────   │                │
   │   └─────────────────────────┘                │
   └────────────────────────────────────────────────┘

        虽然简单,但状态机是嵌入式代码的核心武器。一旦把状态显式化,所有"是否要启动PWM"、"是否要拉高 EN"的判断都基于 state 字段。

三种"动作"的实现

        把 Stepper_Start、Stepper_Stop、Stepper_SetDirection 的实现放到一起对照看,差异一目了然:

void Stepper_Start(void)
{
  if (s_stepper.state == STEPPER_STATE_RUNNING) return;

  __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_4, s_pulse);
  HAL_GPIO_WritePin(EN_GPIO_Port, EN_Pin, STEPPER_EN_ENABLE);
  if (HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_4) != HAL_OK) {
    HAL_GPIO_WritePin(EN_GPIO_Port, EN_Pin, STEPPER_EN_DISABLE);
    return;
  }
  s_stepper.state = STEPPER_STATE_RUNNING;
}

void Stepper_Stop(void)
{
  if (s_stepper.state == STEPPER_STATE_IDLE) return;

  (void)HAL_TIM_PWM_Stop(&htim3, TIM_CHANNEL_4);
  HAL_TIM_Base_Stop(&htim3);
  __HAL_TIM_SET_COUNTER(&htim3, 0);

  HAL_GPIO_WritePin(EN_GPIO_Port, EN_Pin, STEPPER_EN_DISABLE);
  s_stepper.state = STEPPER_STATE_IDLE;
}

void Stepper_SetDirection(StepperDir_t dir)
{
  HAL_GPIO_WritePin(Dir_GPIO_Port, Dir_Pin,
                    (dir == STEPPER_DIR_FORWARD) ? STEPPER_DIR_FWD
                                                 : STEPPER_DIR_REV);
  s_stepper.dir = dir;
}

    Start 顺序:CCR → EN → PWM

        把 __HAL_TIM_SET_COMPARE 放在第一步,把 HAL_TIM_PWM_Start 放在最后一步。保证PWM启动后第一个周期宽度正确。EN后拉:TMC2209使能后立刻能看到STEP信号(从第二个周期开始)。如果反过来先使能再开PWM,会有一小段"EN 已生效但STEP还没出"的死区。

    Stop 顺序:PWM → Base → EN

        保证EN拉高的瞬间PB1已经是稳定的低电平,不会有"拉高 EN 时正好冒出一个脉冲"的边沿问题

        速度切换:ARR 是怎么"活"起来的

初始:PSC=95, ARR=999, CCR=500  →  1 kHz, 50% duty
       |
       |   调 Stepper_SetSpeed(500)
       ↓
最终:PSC=95, ARR=1999, CCR=1000  →  500 Hz, 50% duty

        PSC 不变(保持 1 MHz 定时器时钟),改 ARR。代码实现:

static void apply_timer_period(uint32_t freq_hz)
{
  uint16_t arr = compute_arr(freq_hz);
  s_pulse = (uint16_t)((arr + 1U) / 2U);   // 50% 占空比

  if (s_stepper.state == STEPPER_STATE_RUNNING) {
    (void)HAL_TIM_PWM_Stop(&htim3, TIM_CHANNEL_4);
    HAL_TIM_Base_Stop(&htim3);
  }

  __HAL_TIM_SET_AUTORELOAD(&htim3, arr);
  __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_4, s_pulse);
  __HAL_TIM_SET_COUNTER(&htim3, 0);

  s_stepper.step_freq_hz = STEPPER_TIMER_CLOCK_HZ / (uint32_t)(arr + 1U);

  if (s_stepper.state == STEPPER_STATE_RUNNING) {
    (void)HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_4);
  }
}

shell 系统:把UART变成"对话窗口"

        对 shell 的需求很简单:

            行式输入:用户敲一行、按回车,MCU 处理一行。

            回显:我打什么字符,终端上就显示什么。否则我根本不知道MCU收到了什么。

            基本命令集:start、stop、forward、reverse、speed1/2/3、status、help。

        最简的接收方式:每收到 1 字节就进一次中断

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
  if (huart->Instance != COM1_UART) return;
  
  char c = (char)s_rx_byte;
  // 处理 c ...
  
  // arm 下一字节
  HAL_UART_Receive_IT(huart, &s_rx_byte, 1);
}

字符处理的细节

┌─────────────────────┐
   初始 ──────→ │  等待下一个字符     │
                └─────────────────────┘
                       │
            ┌──────────┼──────────┬──────────────┐
            ↓          ↓          ↓              ↓
        可打印字符   \r 或 \n    \b 或 DEL      其他
            │          │          │              │
            ↓          ↓          ↓              ↓
       加入行缓冲   触发 dispatch  退格 1 字符    忽略
                       │
                       ↓
                  清空行缓冲
                  打印新提示符

行解析:轻量 tokenizer

        不用 strtok 之类的库函数,太重。需求是:

  • 把一行字符串按空白字符切分。

  • 第一个token是命令名。

static int shell_tokenize(char *line, char **argv, int max_args)
{
  int argc = 0;
  while (*line != '\0' && argc < max_args) {
    while (*line != '\0' && isspace((unsigned char)*line)) {
      *line++ = '\0';
    }
    if (*line == '\0') break;
    argv[argc++] = line;
    while (*line != '\0' && !isspace((unsigned char)*line)) {
      line++;
    }
  }
  return argc;
}

命令分发表

        shell 系统的核心是命令分发表

typedef struct {
  const char *name;                  // 命令名
  void      (*handler)(int, char**); // 处理函数
} Shell_Cmd_t;

static const Shell_Cmd_t s_cmds[] = {
  { "help",    cmd_help    },
  { "start",   cmd_start   },
  { "stop",    cmd_stop    },
  { "forward", cmd_forward },
  { "f",       cmd_forward },
  { "reverse", cmd_reverse },
  { "r",       cmd_reverse },
  { "speed1",  cmd_speed1  },
  { "s1",      cmd_speed1  },
  { "speed2",  cmd_speed2  },
  { "s2",      cmd_speed2  },
  { "speed3",  cmd_speed3  },
  { "s3",      cmd_speed3  },
  { "status",  cmd_status  },
};

        dispatch 逻辑:

for (size_t i = 0; i < sizeof(s_cmds)/sizeof(s_cmds[0]); i++) {
  if (ci_streq(argv[0], s_cmds[i].name)) {
    s_cmds[i].handler(argc, argv);
    return;
  }
}
shell_write("ERR: unknown command '");
shell_write(argv[0]);
shell_write_line("'. Type 'help'.");

各命令的实现

        每个命令就是一个"动作 + 反馈":

static void cmd_start(int argc, char **argv)
{
  (void)argc; (void)argv;
  Stepper_Start();
  shell_write_line("OK: motor started");
}

static void cmd_forward(int argc, char **argv)
{
  (void)argc; (void)argv;
  Stepper_SetDirection(STEPPER_DIR_FORWARD);
  shell_write_line("OK: direction = forward");
}

static void cmd_speed2(int argc, char **argv)
{
  (void)argc; (void)argv;
  Stepper_SetSpeed(STEPPER_SPEED2_HZ);
  shell_write_line("OK: speed = 500 Hz");
}

static void cmd_status(int argc, char **argv)
{
  (void)argc; (void)argv;
  const Stepper_t *info = Stepper_GetInfo();
  char buf[96];
  snprintf(buf, sizeof(buf),
           "state=%s dir=%s freq=%lu Hz EN=%s",
           info->state == STEPPER_STATE_RUNNING ? "running" : "idle",
           info->dir  == STEPPER_DIR_FORWARD  ? "forward" : "reverse",
           (unsigned long)info->step_freq_hz,
           HAL_GPIO_ReadPin(EN_GPIO_Port, EN_Pin) == STEPPER_EN_ENABLE
             ? "LOW (on)" : "HIGH (off)");
  shell_write_line(buf);
}





关键词: 步进     电机     控制     实践     控制系统    

共1条 1/1 1 跳转至

回复

匿名不能发帖!请先 [ 登陆 注册 ]