STM32电机控制例程分享 第八期:USART HMI 串口屏控制步进电机运转
在一台小型CNC雕刻机的调试现场,工程师正对着笔记本电脑运行串口助手,手动输入十六进制指令来调整主轴位置。旁边的学生看得直皱眉:“就不能做个界面点一下就动吗?”——这其实正是许多嵌入式开发者的真实写照:功能早已实现,却因缺乏直观操作方式而显得“不够产品化”。
今天我们要解决的就是这个问题。用一块几十元的串口屏 + 一颗STM32F103,搭建一个真正意义上的“人机交互”系统,实现通过触摸屏幕直接控制步进电机启停、调速、换向,并实时反馈运行状态。整个过程不依赖PC,无需复杂的GUI框架,开发周期可以压缩到一天以内。
从通信链路讲起:为什么选USART而不是SPI或I²C?
很多人第一反应是:“显示屏不是应该用并口或者RGB接口吗?”但对资源有限的小型控制系统来说, 串行通信才是性价比之王 。尤其是基于USART的HMI方案,只需要两根线(TX/RX)就能完成双向数据交换,极大简化布线和PCB设计。
以常见的Nextion或禾力恒通系列串口屏为例,它们内部集成了独立的显示处理器和固件,你不需要操心像素绘制、触摸扫描这些底层逻辑。你要做的只是发送一串ASCII文本指令,比如:
SendToHMI("t0.txt=\"Speed: 200 PPS\"");
屏幕就会自动把ID为
t0
的文本框内容更新为“Speed: 200 PPS”。反过来,当你点击界面上的按钮,它会主动回传一个3字节的数据包,如
0x65 0x01 0x00
,告诉你“第1页的第0号控件被按下”。
这种“主控发命令,屏幕做执行;屏幕报事件,主控做响应”的模式,完美契合STM32这类MCU的能力边界——既能处理实时控制任务,又不至于被图形渲染拖垮。
当然,前提是通信要稳。我曾经遇到过因波特率偏差导致指令错乱的问题:明明发的是
page 1
,结果屏幕跳到了不存在的页面。后来发现是串口屏默认使用内部RC振荡器,误差高达±3%,而STM32接了外部晶振,两者稍有不匹配就会丢帧。解决方案很简单:
统一设置为115200bps,并确保双方都启用硬件流控或至少加入超时重试机制
。
下面是经过实战验证的基础通信封装:
// usart.c
void SendToHMI(const char* cmd) {
HAL_UART_Transmit(&huart1, (uint8_t*)cmd, strlen(cmd), HAL_MAX_DELAY);
uint8_t end[] = {0xFF, 0xFF, 0xFF};
HAL_UART_Transmit(&huart1, end, 3, HAL_MAX_DELAY); // 必须加这三个字节!
}
别小看最后那三个
0xFF
,这是绝大多数串口屏识别指令结束的关键标志。漏掉它,轻则刷新失败,重则累积缓存造成后续指令解析错位。
至于接收端,建议采用中断+状态机的方式处理返回数据,避免阻塞主循环。以下是一个适用于Nextion屏的事件解析片段:
uint8_t rx_buffer[10];
uint8_t rx_index = 0;
void UART_RxCallback(UART_HandleTypeDef *huart) {
if (huart->Instance == USART1) {
uint8_t data;
HAL_UART_Receive(&huart1, &data, 1, 1);
if (rx_index == 0 && data != 0x65) return; // 等待起始字节
rx_buffer[rx_index++] = data;
if (rx_index >= 3) {
handle_touch_event(rx_buffer[1], rx_buffer[2]);
rx_index = 0;
}
}
}
void handle_touch_event(uint8_t page_id, uint8_t comp_id) {
switch(page_id) {
case 0:
if (comp_id == 1) motor_start(get_speed_from_slider());
if (comp_id == 2) motor_stop();
break;
}
}
这里的关键在于不要盲目轮询。串口屏支持中断上报模式(即“有触控才发”),合理利用这一特性可显著降低CPU负载。
屏幕不只是显示器:如何让它真正“参与”控制流程?
很多初学者把串口屏当成纯输出设备,只用来显示参数。但实际上,它的潜力远不止于此。
想象这样一个场景:你在调试一台自动送料机构,需要精确设定每步前进的脉冲数。传统做法是改代码、重新烧录、观察效果,反复迭代。而现在,你可以直接在屏幕上放一个数值输入框:
n0.val=1000
用户输入后,STM32读取这个值并用于计算运动距离。更进一步,添加一个“测试运行”按钮,点击后电机按设定脉冲数走一步,完成后自动停止并提示“已完成1000步”。
这才是现代HMI应有的交互体验。
为了实现这一点,你需要让STM32具备“查询屏幕当前值”的能力。虽然协议上不支持直接GET操作,但我们可以通过“事件触发+主动请求”的组合拳达成目的。例如:
-
用户修改滑块 → 屏幕发送
0x65 xx yy -
MCU收到后,立即发送
get n0.val指令 -
屏幕回复
0x71 0x?? ...带回实际数值
虽然多了一次往返,但在人机交互的节奏下完全可接受。而且这样做还有一个好处: 形成闭环校验 ,防止因通信异常导致本地变量与界面显示不同步。
另外值得一提的是背光控制。有些项目要求夜间自动调暗屏幕亮度,这完全可以由STM32根据定时器或光敏传感器动态调节:
void set_brightness(uint8_t level) {
char cmd[20];
sprintf(cmd, "dim=%d", level);
SendToHMI(cmd);
}
一条指令搞定,连I²C都没用上。
步进电机怎么“听话”?PWM之外你还得懂时序约束
说到驱动步进电机,很多人第一反应是:“不就是输出PWM吗?”确实,STM32的定时器能轻松产生几十kHz的方波信号送到STEP引脚。但真正难点不在“发出脉冲”,而在“何时开始、如何加速、怎样停止”。
我曾在一个客户项目中看到这样的现象:电机低速运行平稳,一旦提速到一定频率就突然卡住不动,甚至发出啸叫。检查接线无误、电源充足,问题出在哪?答案是: 启动频率过高导致失步 。
步进电机有个基本物理特性:转子有惯性,绕组有电感。刚启动时如果直接给高频脉冲,磁场变化太快,转子跟不上,就会“丢步”。正确的做法是采用 梯形或S型加减速曲线 ,逐步提升频率。
最简单的实现方式是在启动阶段先以较低频率运行几百个脉冲,再线性增加至目标速度。停止时反向操作。虽然我们这次例程暂未引入复杂算法,但至少要做到:
- 启动时从100~200PPS起步;
- 最高速度不超过驱动模块允许的最大响应频率(A4988一般为20kHz);
- 加入软件限位,通过累计脉冲数判断是否到达行程终点。
下面是基于TIM2的PWM输出配置示例:
void SetMotorFrequency(uint32_t freq) {
if (freq == 0) {
HAL_TIM_PWM_Stop(&htim2, TIM_CHANNEL_1);
return;
}
uint32_t timer_clock = 72000000;
uint32_t prescaler = 72 - 1; // 1MHz计数频率
uint32_t period = (1000000 / freq) - 1;
__HAL_TIM_SET_PRESCALER(&htim2, prescaler);
__HAL_TIM_SET_AUTORELOAD(&htim2, period);
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, period / 2); // 50%占空比
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);
}
注意这里将定时器时钟预分频为1MHz,使得ARR寄存器的值直接对应微秒级周期,便于计算。同时保持50%占空比,确保每个脉冲宽度足够长(>1μs),满足A4988等芯片的最小脉宽要求。
DIR方向信号则通过普通GPIO控制:
HAL_GPIO_WritePin(DIR_GPIO_Port, DIR_Pin, direction ? GPIO_PIN_SET : GPIO_PIN_RESET);
简单有效。
实际系统集成中的那些“坑”,你知道几个?
1. CPU负载冲突:通信和控制谁优先?
当UART接收和PWM输出同时发生时,若全部放在主循环中轮询,很容易出现响应延迟。我的建议是:
- 高优先级:PWM输出走定时器硬件通道 ,不受软件影响;
- 中优先级:UART接收使用中断+DMA ,避免频繁进入ISR;
- 低优先级:界面刷新、日志打印等非实时任务放入main loop 。
这样即使串口屏短时间内大量刷新,也不会干扰电机运行。
2. 掉电记忆怎么办?
很多应用场景希望下次上电时恢复上次使用的参数(如默认速度)。由于串口屏本身不具备存储能力,这部分工作必须由STM32完成。
最简单的方案是利用STM32F1的后备寄存器(Backup Registers)或模拟EEPROM(通过Flash扇区模拟)。每次参数变更时保存一次:
#define DEFAULT_SPEED_ADDR 0x0800FC00
uint32_t speed = 200;
// 修改后保存
write_flash(DEFAULT_SPEED_ADDR, speed);
// 上电读取
speed = read_flash(DEFAULT_SPEED_ADDR);
虽然Flash擦写寿命有限(约1万次),但对于参数存储已绰绰有余。
3. 紧急停止必须可靠
在机电系统中,“急停”按钮的安全等级最高。理想情况下应使用外部中断引脚连接物理按键,一旦触发立即关闭PWM输出:
void EXTI0_IRQHandler(void) {
HAL_TIM_PWM_Stop(&htim2, TIM_CHANNEL_1);
motor_state = STOPPED;
SendToHMI("t_status.txt=\"EMERGENCY STOP\"");
}
必要时还可联动串口屏弹出警告画面,形成多重保障。
这种架构适合哪些项目?有没有替代方案?
这套“USART HMI + STM32 + 步进驱动”的组合拳,在教育实验平台、DIY设备、小型自动化装置中表现出色。它的最大优势不是性能有多强,而是 开发效率极高 。
举个例子:你要做一个智能窗帘控制器。传统方案可能需要Linux主板+Qt界面+网络服务,成本高、功耗大、启动慢。而用STM32+F103+串口屏,两天就能做出原型:屏幕显示开关按钮、光线强度、当前开合度,通过PWM控制电机正反转,还能设定定时任务。
当然,它也有局限。如果你要做的是工业级多轴联动CNC,那就需要更强大的处理器、EtherCAT总线、高级运动规划算法。这时候FreeRTOS+CANopen+编码器闭环才是正解。
但对于大多数中小型项目而言,过度设计反而会拖慢进度。 能用简单方法解决的问题,就不该引入复杂架构 。
未来扩展方面,可以在现有基础上轻松叠加功能:
- 加Wi-Fi模块实现手机远程监控;
- 引入Modbus协议接入PLC系统;
- 使用SD卡记录运行日志;
- 保留升级空间,后期替换为LVGL+RTOS方案应对更复杂UI需求。
这种高度集成的设计思路,正在悄然改变嵌入式产品的开发范式。不再需要庞大的团队和漫长的周期,一个人、一块板、几天时间,就能做出具备完整人机交互能力的智能设备。而这,正是开源硬件与模块化设计的魅力所在。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
488

被折叠的 条评论
为什么被折叠?



