前面我分别讲清楚了"电机要什么信号"和"H533 怎么配硬件"。但"配硬件"和"让电机转起来"之间,还差一座桥 ——应用软件。如果说 STEP/DIR/EN是MCU对电机的"语言",那shell就是我对MCU的语言。人通过 shell 说话,shell 把命令翻译成对步进电机驱动的调用,驱动去拨弄GPIO和TIM3,电机就动了。
stepper 驱动:把GPIO包装成"动作"
我想要的不是一个"操作寄存器的工具集",而是一个有状态的对象。什么意思?就是说:
你不用关心EN引脚现在是什么电平。
你不用关心TIM3当前ARR是多少。
你不用关心上次的命令和这次有没有冲突。
你只管告诉驱动:"我要1kHz正转"、"停"、"反转",剩下的它自己处理。
所以 stepper 模块对外暴露的不是十几个函数,而是几个语义清晰的动词:
状态机
模块内部维护一个 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);
}
我要赚赏金
