4. 开发环境搭建

各位同学,欢迎来到第四讲。前面我们聊了STM32的时钟树和启动流程,今天要讲的是真正让电机“动起来”的核心——GPIO和定时器。这两个外设,说白了就是MCU的“手脚”和“心跳”。

我刚开始做运动控制时,总觉得GPIO不就是个高低电平嘛,有什么好研究的?直到有一次,一个步进电机驱动器死活不响应脉冲,查了两天发现是GPIO开漏输出没加上拉电阻……嗯,从那以后我再也不敢小看GPIO的配置了。

4.1 GPIO模式配置:推挽与开漏

GPIO的模式,我习惯把它分成两大类:推挽输出和开漏输出。你想想看,一个引脚要输出高电平或低电平,内部电路是怎么实现的?

4.1.1 推挽输出(Push-Pull)

推挽输出,内部有两个MOS管:一个负责“推”(输出高电平),一个负责“挽”(输出低电平)。

  • 输出高电平时:上管导通,下管截止,引脚直接连到VDD。
  • 输出低电平时:上管截止,下管导通,引脚直接连到GND。

这种模式的好处是驱动能力强,电平切换速度快。我在驱动LED指示灯或直接控制继电器时,首选推挽输出。

适用场景:普通数字信号输出、LED控制、蜂鸣器驱动、直接连接数字输入引脚。

4.1.2 开漏输出(Open-Drain)

开漏输出就只有一个下管。输出低电平时,下管导通,引脚接地。输出高电平时呢?下管截止,引脚处于高阻态——说白了,它自己拉不上去,必须靠外部上拉电阻。

为什么要用这么麻烦的方式?我举个例子:I2C总线。多个设备挂在同一条线上,如果都用推挽输出,一个设备输出高电平,另一个输出低电平,那就短路了。开漏输出配合上拉电阻,实现了“线与”逻辑——任何一个设备拉低,总线就是低电平。

我的习惯:开漏输出一般配上4.7kΩ或10kΩ的上拉电阻。频率高时用4.7kΩ,频率低时10kΩ就够了。我曾经在I2C上用过100kΩ上拉,结果波形圆得像正弦波……

4.1.3 模式对比

特性 推挽输出 开漏输出
输出高电平 直接VDD 需外部上拉
输出低电平 直接GND 直接GND
驱动能力 强 弱(取决于上拉)
多设备共用 不适用 适用(线与)
典型应用 LED、普通信号 I2C、电平转换

4.2 定时器工作原理

定时器是运动控制的灵魂。你要产生PWM波驱动电机,要测量编码器的脉冲宽度,都离不开它。STM32的定时器家族很庞大,但核心原理就一个:计数器 + 比较器。

4.2.1 基本定时器结构

一个定时器内部,大致有这几个部分:

  • 预分频器(PSC):把时钟源分频,决定计数步长。
  • 计数器(CNT):向上、向下或双向计数。
  • 自动重装载寄存器(ARR):计到多少值就溢出或重置。
  • 捕获/比较寄存器(CCR):用来和CNT比较,产生事件。

举个例子:时钟是72MHz,预分频器设为72-1,那么计数器每1微秒加1。ARR设为999,就是每1000微秒(1毫秒)溢出一次。这就是一个1ms的定时器。

注意:预分频器和ARR的值都要减1!因为计数器是从0开始计数的。我见过很多新手直接写72和1000,结果定时时间完全不对。

4.2.2 PWM输出模式

PWM,就是脉冲宽度调制。说白了,就是固定频率下,改变高电平的占空比。电机调速、舵机控制,全靠它。

配置PWM时,ARR决定周期,CCR决定占空比。比如ARR=999,CCR=500,那么占空比就是50%。

// 以TIM2为例,配置PWM输出
TIM_HandleTypeDef htim2;
htim2.Instance = TIM2;
htim2.Init.Prescaler = 71;      // 72MHz / (71+1) = 1MHz
htim2.Init.Period = 999;        // 1MHz / (999+1) = 1kHz
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
HAL_TIM_PWM_Init(&htim2);

TIM_OC_InitTypeDef sConfigOC;
sConfigOC.OCMode = TIM_OCMODE_PWM1;
sConfigOC.Pulse = 500;          // 占空比 50%
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_1);
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);

这段代码配置了1kHz的PWM,占空比50%。你改Pulse的值,就能实时调整电机速度。

4.2.3 输入捕获模式

输入捕获,是用来测量外部信号的。比如你要测一个脉冲的宽度,或者编码器输出的频率。

工作原理很简单:当引脚检测到指定的边沿(上升沿或下降沿)时,定时器会把当前CNT的值“捕获”到CCR寄存器里。两次捕获的差值,就是信号的周期或脉宽。

实战经验:我在做无刷电机FOC控制时,用输入捕获来读取霍尔传感器的换相信号。注意要配置好滤波器和预分频,不然电机高速转动时,捕获到的值会跳变。

4.3 中断优先级管理

中断,是实时系统的命脉。STM32使用NVIC(嵌套向量中断控制器)来管理中断优先级。这里有两个关键概念:抢占优先级和子优先级。

  • 抢占优先级:数字越小,优先级越高。高抢占优先级可以打断低抢占优先级的中断。
  • 子优先级:当抢占优先级相同时,子优先级决定谁先执行。但子优先级不能互相打断。

我一般这样分配:

中断类型 抢占优先级 子优先级 说明
电机过流保护 0 0 最高优先级,必须立即响应
定时器PWM更新 1 0 控制周期中断
编码器输入捕获 1 1 与PWM同级,但子优先级低
串口接收 2 0 通信中断,可被电机中断打断

避坑指南:我曾经把串口中断优先级设得比定时器还高,结果电机控制周期被频繁打断,导致电机抖动。记住:运动控制的中断优先级一定要高于通信中断。

4.4 SysTick定时器应用

SysTick是Cortex-M内核自带的24位递减计数器。它不依赖外设定时器,每个Cortex-M芯片都有。HAL库的HAL_Delay()就是基于SysTick实现的。

SysTick的典型用途:

  • 提供系统心跳:一般配置为1ms中断一次,用于任务调度。
  • 精确延时:替代软件延时,不阻塞CPU。
  • 时间基准:为其他定时器提供同步时钟。

配置SysTick很简单:

// 系统时钟72MHz,SysTick每1ms中断一次
HAL_SYSTICK_Config(SystemCoreClock / 1000);
HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);
HAL_NVIC_SetPriority(SysTick_IRQn, 15, 0);  // 最低优先级

注意,SysTick的优先级我一般设成最低(15)。为什么?因为SysTick中断只是用来维护系统时钟,不应该打断更重要的电机控制中断。

我的习惯:在电机控制主循环里,用SysTick做超时判断。比如等待编码器归零信号,如果超过100ms没收到,就报错停机。这样既不会死等,又能保证实时性。

4.5 本章知识结构

下面这张图,把本章的核心知识点串起来了。你可以把它当作一个快速索引。

第四章:GPIO与定时器基础 GPIO模式配置 推挽输出 开漏输出 定时器工作原理 PWM输出 输入捕获 中断优先级管理 抢占优先级 子优先级 SysTick定时器应用 系统心跳 精确延时 时间基准 核心要点 • GPIO推挽输出驱动强,开漏输出适合多设备共用总线 • 定时器核心:PSC分频 + CNT计数 + ARR周期 + CCR比较 • 中断优先级:抢占优先级决定能否打断,子优先级决定同级的顺序 • SysTick是内核定时器,优先级设最低,用于系统心跳和延时 • 运动控制中,电机中断优先级 > 通信中断优先级