SmartFOC-for-stepper-motor

1 项目背景

目前,市面上缺乏适合自学和科研的开源级 42 步进电机闭环控制平台,现有如 ODrive、SimpleFOC 等项目存在明显不足:ODrive 工具链繁琐、配置复杂,容易劝退新手;SimpleFOC 功能上限低,控制结构停留在原理层面,缺少贴合 42 步进电机实际应用的逻辑,像卡尔曼滤波、前馈控制等实用算法,以及过流、过载、超速、缺相保护等关键机制基本没有,且配套的 42 步进电机多为简化版,难以满足深入学习和实际应用需求。​

为此,SmartFOC 应运而生。它围绕常见的 42 步进电机设计,简单易上手,能系统学习和运用 FOC,控制核心采用适配的闭环控制架构,配备电流环、速度环、位置环,支持 PWM 控制、速度规划及多种保护机制,可直接对接相关工业设备,实现从学习到工程实用的平滑过渡。同时,其软件结构简洁清晰,代码易于理解实现,适合本科生入门、研究生深入研究高阶算法,还能直接用于小型设备控制和实验,真正打通 “教学 — 科研 — 工程” 一体的 42 步进电机闭环控制平台,让用户既能学习 42 步进电机控制,又能实际应用,基于易上手的步进电机把 FOC 控制学透用好。

2 资源清单

工欲善其事,必先利其器。刚接触FOC和步进电机,不知如何下手时,先把准备工作做好是非常重要的。

本项目主要包括一个上位机,电机驱动板,以及电机本体,上位机运行在PC上通过RS485转USB与电机驱动板连接,驱动板固定在电机上并接入24V直流电源,通过内部的逆变电路驱动电机旋转。

以下是本项目所需的主要资源清单:

类型 具体内容
硬件设备 核心部件:42步进电机、电机驱动板(内置N32G452CCL7主控芯片、MT6835磁编码器)、24V直流电源
连接类:连接线(杜邦线、RS485转USB线、ST-Link调试线)
辅助工具:电脑、螺丝刀(固定硬件)
软件工具 开发环境:Keil MDK(μVision5,版本≥5.38)、N32G4xx_DFP芯片支持包、N32G45x标准库
调试工具:ST-Link V2调试器及驱动(版本≥V2.38.26)、配套上位机软件(磁编校准/参数调试)
辅助工具:串口助手(SSCOM/XCOM,调试通信)

看完清单,接下来将挑选重要的部分进行详细介绍,帮助初学者快速上手本项目。

3 硬件资源

3.1 42步进电机

本项目采用额定电压为 24 V的 42 步进电机。这款电机体型小巧,结构紧凑,在小型设备和精密传动场景中很常见,能灵活适应有限的安装空间。它的步距角精准,运行稳定,非常适合作为入门学习和小型设备驱动的核心部件。​作为市面上应用广泛的基础款电机,42 步进电机操作简单易上手,再结合矢量控制算法,能实现更精准的闭环控制。无论是学生学习 FOC 控制原理,还是进行小型设备的驱动实验,都能通过它快速掌握核心技术,同时为后续接触更复杂的工业级电机控制或是更深层次的控制算法研究打下扎实基础。

system

电机相关参数以及尺寸如下图所示:

system

通过上图,初学者可能不知道重点需要关注哪些参数。接下来将介绍一些关键参数的含义,帮助初学者更好地理解电机性能。

参数名称 数值 物理原理 工程意义
步距角 1.8° 电机每接收1个脉冲信号,转子转过的机械角度(2相步进电机典型值) 定义开环控制精度上限(1.8°对应200步/转),步进角越小,开环控制越精准
相电流 2.5A 电机每相绕组持续运行时的安全电流 决定功率器件选型,比如MOSFET和芯片等器件就得能承受大于3A的电流(安全裕量); 电流大,发热多,驱动板也要考虑散热设计
保持转矩 50N·cm 绕组通额定电流时,转子能承受的最大静负载力矩,超过则电机丢步 直观判断带载能力,负载力矩>50N·cm时,开环必丢步,需FOC闭环补偿
相电阻 0.9Ω 步进电机绕组的直流电阻,反映电机的铜损特性 电流流过电阻,就会有功率损耗以及发热,这是最直观的;同时电阻也会分走电压,影响实际工作电流
相电感 1.6mH 绕组的等效电感,反映电流变化的“阻碍特性”,( V=L\frac{di}{dt} ), 决定电流响应速度,电感会阻碍电流变化,电感越大,相同电压下电流爬升越慢,动态性能越差

至于图中机械尺寸,简单看看即可。

3.2 主控芯片

本系统控制器采用自主研发的嵌入式主控单元,基于国民技术的 N32G452CCL7 芯片设计。这款芯片是高性能的 32 位 ARM Cortex-M4 微控制器,主频高达 144MHz,外设接口丰富,处理能力强劲,而且性价比很高。它在工业控制、机器人、智能装备等领域应用广泛,是行业里常用的高性能选择,也适合初学者自学使用。

芯片手册链接:https://www.nationstech.com/uploads/%E9%80%9A%E7%94%A8MCU/N32G452/%E8%8A%AF%E7%89%87%E6%96%87%E6%A1%A3/%E6%95%B0%E6%8D%AE%E6%89%8B%E5%86%8C/CN_DS_N32G452_Series_Datasheet.pdf

3.3 MT6835磁编码器

MT6835磁编码器基于各向异性磁阻技术(AMR)和信号处理技术实现了0°~360°的绝对角度测量。初学者可能不太理解这个有什么用。简单来说,步进电机本属于开环控制系统,无法实时获取转子位置,容易出现丢步、失步等问题。而磁编码器可以实时反馈电机转子位置,提供高精度的闭环控制能力。它通过检测转子上的磁场变化来获取角度信息,具有高分辨率和低噪声特性,非常适合用于步进电机的闭环控制。

磁编码器的工作原理就是当转子转动时,磁场方向相对编码器的 AMR 传感器发生变化 → 传感器电阻随磁场方向改变 → 经芯片内部电路转换为 0°~360° 的绝对角度值,电机上电即可直接获得电机位置,这一特性为 FOC 控制提供了实时、无累积误差的转子位置反馈,是实现 “磁场定向”(Park 变换)和 “精准调速” 的核心前提。有关FOC的具体内容后面会继续深入讲解。

此外,该芯片还提供客户端自校准模式,用户只需匀速旋转电机,芯片就能自行进行校准,项目内上位机配备的校准模式就用于校准磁编码器。磁编码器通过磁场来确定电机方向,所以一般放置于电机驱动板中央,如下图所示,驱动板中间即MT6835磁编码器。。

system

3.4 24V直流电源

24V直流电源即可为电机提供稳定的工作电压,同时也为主控芯片和其他外设供电。接电时区分电机正负极即可。

4 软件资源

4.1 开发资源

组件 版本要求 获取方式 备注
Keil MDK (μVision5) 5.38+ Keil官网 需注册Nationstech设备支持
N32G4xx_DFP 2.1.0+ 国民技术官网 芯片支持包
开发库 - N32G45x标准库 标准库 (N32 Standard Peripheral Library)
ST-Link V2驱动 V2.38.26+ ST中文官网 下载STSW_LINK009

4.2 软件安装

(1)安装Keil MDK核心包:可参考下方CSDN博主的安装教程。

  [keil5——安装教程附资源包](https://blog.csdn.net/weixin_48100941/article/details/126192218?): `https://blog.csdn.net/weixin_48100941/article/details/126192218?`

(2)安装Nationstech设备支持包:

打开下载的资源包,找到并双击 `N32G4xx_DFP.x.x.x.pack` 自动安装,如下图所示。

pack

(3)安装ST-Link驱动:可参考下方CSDN博主的安装教程。

[stlink驱动教程](https://blog.csdn.net/m0_68987050/article/details/146936297?): `https://blog.csdn.net/m0_68987050/article/details/146936297?`

5 理论基础

做完上面的准备工作,接下来就可以开始学习本项目的理论基础了。本项目的理论基础主要从通信"RS485+Modbus"到电机控制理论搭建电机应用的知识框架,包括RS-485通信协议、Modbus协议以及磁场定向控制理论(FOC)。这些理论是实现步进电机闭环控制的核心内容,理解它们有助于更好地掌握本项目的实现原理和控制方法。

5.1 RS-485协议

RS-485是一种在工业、自动化、汽车领域常用的串行通信标准,传输距离可长达1200m,速率可达10Mbit/s,RS-485使用的是差分传输,即通过两根线的电压差来决定信号是0还是1。所以RS-485传输线使用的是一对双绞线,一个定义为A,一个定义为B

system

还有一点要注意的是,RS485标准只规定了物理层协议,并未规定数据层协议。简单来说就是RS485只规定了什么是0和1,并未规定这位0和1具体是停止位、数据位还是起始位等。所以在一般使用中还需结合另一数据层协议使用,较为常见的是modbus协议。

5.1.1 RS485物理层

RS-485标准规定了+2V~+6V为1,-2V~-6V为0。这里的电压就是A线与B线之间的电压差(A-B)。这种差分信号能有效减少长距离信号传输的信号衰减与噪声干扰,A线的干扰与B线的干扰会被相减的操作抵消,这种能力称为共模抑制。

system

5.1.2 物理接口

现在RS-485接口大多兼容RS-422,很多转换器上标的都是T+/A,T-/B。这里的A,B就是RS-485的AB线。T+,T-则是RS-422标准使用的

system

针对DB9针形的母头,RS485有如下定义,其中pin6-pin9不接

system

DB9 输出信号 RS485半双工接线
1 T/R+ A
2 T/R- B
3 RXD+
4 RXD-
5 GND 地线

5.1.3 连接方式

system

RS-485标准可以全双工也可以半双工(同一时间只能收或发),但是实际使用时四线制的全双工的接线更复杂,所以用的比较少。RS-485更多使用的时双线的总线,并在总线上挂载多个从机,同一总线上最多可挂载32个从机,支持多从机模式,但不支持多主机模式,简单来说就是,发命令的只能有一个,听命令的能有很多个。至于这个主机命令具体发给谁,各个从机又是如何识别的,那就要引出下文的Modbus协议了

RS485应用实例如下:

system

5.2 Modbus通讯协议

通过学习上面的RS485协议知道,RS485只规定了物理层的通信方式,即什么是0和1,并未规定数据层的协议,比如怎么发送、发送内容、怎么解析。因此我们需要结合modbus协议来实现完整的通讯功能,下面我们进入modbus协议的学习。

5.2.1 Modbus简介

Modbus协议是一种数据传输格式,由起始帧、数据帧、校验帧等组成,Modbus协议是一个大类,里面有很多Modbus协议的变种,比较常见的有Modbus-RTU、Modbus-ASCll、Modbus-TCP。其中最常见的是作为Modbus默认协议的Modbus-RTU,本项目采用的也是Modbus-RTU,所以下文只介绍Modbus-RTU协议,其他变种与Modbus-RTU类似。

5.2.2 数据帧结构

Modbus-RTU协议中的命令由地址码(一个字节)、功能码(一个字节),数据(N个字节),校验码(两个字节),4个部分组成

system

字段名称 作用 具体说明
地址码 标识命令的目标从机 1个字节,这个字节写入从机的地址,每个从机都有唯一一个地址码,从机根据这个字节识别这条命令是否是发给自己的。当地址码位0时,为广播地址,所有从机均能识别。
功能码 指定从机需执行的操作 1个字节, 这个字节告诉从机要进行什么操作,比如停机,自检,重启等。Modbus规定功能号为1-127。
数据 明确操作的具体对象和范围 这段由起始地址和寄存器数量组成,意思是从哪个地址开始读多少个寄存器数量的数据。
校验位 验证数据传输的准确性 2个字节,本项目采用CRC校验(可通过工具网站 http://www.ip33.com/crc.html 计算),接收方需重新计算并与发送方校验位比对,不一致则判定传输错误

具体实例

主机请求:0xD2 + 0x01 + 0xA5 + 0x04+ CRC

从机响应:0xD2 +0x01 + 0x04 + 0x11 + 0x00 + 0x11 + 0x00 + 0x00 + CRC

主机D2为地址码,01为功能码,A5为寄存器起始地址,04为读取四个寄存器,CRC为校验位。

从机D2为地址码,01为功能码,04指示了接下来数据段的长度,CRC为校验位。

5.3 磁场定向控制理论(FOC)

5.3.1 FOC理论基础

我们首先简要了解一下实际的二相步进电机的与其简化模型。 system

在本项目所使用的实际的二相混合式42步进电机中有8个绕组,极对数为50,图中的四根先分别控制两相绕组(一相有四个绕组),接下来的理论推导全部基于步进电机的简化模型,所以实际电机结构在这里不做过多介绍。

在了解电机之前,我们需要知道通电导线与磁场的关系,通电导线与磁场之间遵从右手定则,右手握住通电导线,大拇指指向电流方向,其余四指指向磁场方向。

system system

当我们把通电导线在铁芯上绕成绕组时,我们就可以得到一个电磁铁,通电的方向决定了磁场的方向

system

在二相步进电机的简化模型中(如图)绕组简化为两个绕组a与绕组b,转子也简化为一个永磁体,给绕组a自左向右通电时,转子(永磁体)的N极会指向绕组a,即下图中的状态7,当我们给绕组a通自右向左的电流时,转子的S极会指向绕组a,即状态3,绕组b同理,当我们依次给绕组通上方向正确的电流时,磁铁就会被不断吸引,这样我们就实现了最简单的步进电机,步进角为90度。当我们给ab两个绕组同时通电时,转子会指向ab两个绕组之间,夹角为45度,根据两个绕组的磁场方向的不同,步进电机可以实现下图中2,4,6,8的状态,再加上其余单个绕组通电的状态,我们就实现了步进角为45度的步进电机,当我们按顺序给ab绕组通电时,转子会被绕组产生的磁场不断吸引,最终电机就旋转了起来。 system

绕组a与绕组b在同一平面内成直角关系放置,我们可以通过在一段极短时间内,通过给ab绕组分配不同的电压来生成任意方向的磁场,例如,在0.1ms内的前0.05ms时绕组a生成向左的磁场,后0.05ms生成向右的磁场,就可以认为在这段时间内绕组a产生的磁场分量为0。 既然可以生成任意方向的磁场,那么那个方向的磁场能更好的吸引转子旋转呢?显然,我们需要生成垂直于转子方向的磁场。这个方向的磁场强度直接与Q轴电流Iq,电磁转矩Te相关,在这里给出步进电机的电磁转矩方程加以理解:

Te=Nr(LdLq)idiq+NrImMsriq T_{\text{e}} = N_{\text{r}}(L_\text{d}-L_{\text{q}})i_{\text{d}}i_{\text{q}}+N_{\text{r}}I_\text{m}M_{\text{sr}}i_{\text{q}}

其中,Te为电磁转矩,Nr为转子齿数,Ld为d轴电感,Lq为q轴电感,id为d轴电流,iq为q轴电流,Im为永磁体的等效转子电流,Msr是转子和定子的互感系数。为了获得最大转矩,本文的控制方法是设定直轴分量id=0,以实现最大转矩电流比控制。因此,步进电机的转矩方程可以从包含d轴和q轴分量的完整形式简化为仅与q轴电流相关的形式:

Te=NrImMsriq T_{\text{e}} =N_{\text{r}}I_\text{m}M_{\text{sr}}i_{\text{q}}

可以看出,电磁转矩仅由q轴电流iq决定。因此,电机的控制可简化为调节iq的大小和方向。

为了计算我们该用多大绕组a磁场分量与绕组b磁场分量生成这个最优的磁场,我们对固定的绕组建立αβ坐标系,对旋转的永磁体建立dq坐标系,其中q轴垂直于转子,d轴与转子平行。并通过park变换与反park实现两者之间的转换,这就是磁场定向控制的核心。

5.3.2 控制框架

该项目中的42步进电机控制采用位置-速度-电流三环的控制框架,总体控制流程如下:

system

位置环输入期望的位置,通过与MT6835磁编码器获取的实际的位置信息相减后,得到误差并输入PI控制器。

位置环PI控制器输出的控制量输入到速度环中,速度环的输入与实际速度比较后得到误差,速度误差输入速度环PI控制器。

速度环控制器输出的输出量输入到电流环的Q轴电流环作为期望值,其中Q轴电流环的反馈量是经过ab相电流采样与park变换后得到的。输入量与反馈量的差输入到电流环PI控制器中,DQ电流环控制器输出量,经过反PARK变换后得到ab相的电压控制量,该控制量输入到SVPWM中计算占空比,最后写入到芯片寄存器中进行PWM发波。

D轴电流环期望值这里可以直接设置为0,在一些基于FOC的其他控制策略中D轴电流期望值不一定为0,这里不做更多讨论。

需要注意的是本项目的采用的是混合式二相步进电机,与更一般的三相电机不同,二相电机的FOC框架中没有Clark变换。

5.3.3 Park变换与反park变换

system

在二相混合式步进电机的Park变换中,将α轴与β轴固定在定子绕组上,d轴q轴则固定于转子上随转子一起旋转,通过三角函数与电角度θ将旋转的坐标系映射到静止的坐标系上。推导后可以给出以下公式:

{id=iαcosθe+iβsinθeiq=iαsinθe+iβcosθe \begin{cases} i_{\text{d}} = i_{\alpha}\cos\theta_{\text{e}} + i_{\beta}\sin\theta_{\text{e}} \\ i_{\text{q}} = -i_{\alpha}\sin\theta_{\text{e}} + i_{\beta}\cos\theta_{\text{e}} \end{cases}

反park变换是park变换的逆操作,通过三角函数与电角度θ将静止的αβ坐标系投影到旋转的dq坐标系。推导后可以给出以下公式:

{iα=idcosθeiqsinθeiβ=idsinθe+iqcosθe \begin{cases} i_{\alpha} = i_d\cos\theta_{\text{e}} - i_q\sin\theta_{\text{e}} \\ i_{\beta} = i_d\sin\theta_{\text{e}} + i_q\cos\theta_{\text{e}} \end{cases} 其中,θe为电角度,Id为d轴电流,Iq为q轴电流,Iβ为β轴电流,Iα为α轴电流。

PARK与反PARK变换实现了旋转坐标系与静止坐标系之间的转换,将复杂的交流量解耦为直流量,使得系统可以独立控制d轴与q轴,二者结合可以显著提升电机的动态性能、效率及控制精度。

5.3.4 SVPWM

SVPWM解决的问题是,在经过电流环输出以及反park变换,我们拥有αβ轴的期望电压后,该如何给各相绕组分配电压或是占空比?这个问题听起来有些奇怪,αβ轴的电压不是直接对应ab绕组的电压吗?为什么不根据αβ轴的电压直接输出,为什么要大费周章多一个SVPWM?

这是因为SVPWM算法在最开始是针对三相电机设计的,三相电机在得到αβ轴电压后,还需要SVPWM在恰当的分配后,将三相绕组上的电压计算出来。但二相电机没有这么多绕组,所以在二相电机中的SVPWM会被简化很多。即使如此SVPWM仍有减少电压谐波,减少开关管开关次数,防止过调制等重要用途。

要明白SVPWM在干什么,我们需要先了解逆变电路。本项目的电机所使用的电源是24V直流电源,需要逆变电路将直流电转换为交流电来驱动电机,逆变电路的拓扑结构如下:

system

四个mos管控制一个绕组(简化模型,实际的电机控制的不止一个),A相绕组,B相绕组独立控制。上a管与下b管打开时绕组上产生正向电压,下a管与上b管打开时绕组上产生负向电压,AB相绕组mos管不断的开关就能实现在αβ轴上产生期望的电压。控制mos管开关的就需要用到芯片上的PWM外设了。注意在上a管与下b管打开时,下a管与上b管必须关闭,也就是说上a管,下b管需要与下a管,上b管相反的PWM波形。

知道了绕组电压产生的基本原理,我们还需要知道我们需要产生怎样的电压。在d轴期望值为0的磁场定向控制中,当q轴电流大小固定并随着转子旋转起来时,在绕组a与绕组b上的电流分量呈正弦波波动,相位相差90度,这俩个特征在电机调试中是很重要的,如果有电流波形异常就可能导致电机出现各种各样的故障。SVPWM的目标就是产生平滑的成正弦波波形的绕组电流。

接下来我们进入具体的SVPWM的操作流程,首先我们根据二相电机的两相绕组(简化模型)划分出4个扇区,3相的电机可能是6个扇区。为了方便理解,左图中的x轴与y轴用的是αβ轴,轴上的最大幅值为母线电压Vdc(24v)U1,U3,U2,U4则是通过αβ轴合成出来的电压矢量,最大幅值为根号2倍Udc,在其他资料中更多的是将该图旋转45度后以u1,u2,u3,u4划分扇区的图像,即右图。

system system

在扇区图中,我们首先要了解的图其中的虚线部分,虚线是电压矢量的最大幅值边界,这个边界是由Udc确定的,若期望合成的矢量超出这个部分,就需要用到过调制技术(实际无法超出这个边界),图中这个圆是最大不失真电压矢量圆,这个圆全部处于边界之内,Udc有能力去合成这个圆上每一个方向的电压矢量,若是圆的半径继续增加的话,就会出现在最大幅值边界外的点(Udc附近),此时就会出现失真,波形上的表现则是正弦波波峰处被削平了一点。

我们可以根据,反PARK变换后得到的α轴电压与β轴电压来判断期望的电压处于哪个扇区,以此确定电压的正负,之后根据以下公式确定占空比。

{Ta=Ua2VDC+12Tb=Ub2VDC+12 \begin{cases} T_{a} = \frac{U_{a}}{2V_{DC}} +\frac{1}{2} \\ T_{b} = \frac{U_{b}}{2V_{DC}} +\frac{1}{2} \end{cases}

其中Vdc为母线电压(24v),Ta为绕组a的占空比,Tb为绕组b的占空比,Ua为α轴的电压,Ub为β轴的电压,沿轴正方向的电压为正,沿轴负方向的电压为负。这里需要注意的是当占空比为0.5时,绕组上产生的电压分量可以视为0,读者可以回顾在4.3.1 中的简易模型部分举的例子加以理解。 PWM采用中央对称模式,发出一对相位相反的PWM波与逆变电路的开关管连接。

5.4 VF开环控制

开环控制框架: system 本项目的电机配备VF开环运行模式,开环模式可以在不依赖位置信息的情况下使电机旋转起来,开环运行的电机与foc模式下的电机有较大的不同,在开环模式中由于没有转子的位置信息,我们没有办法再建立绑定于转子的dq轴坐标系,合成的矢量与转子之间的角度也不再确定,即使如此我们仍可以通过在程序里设定一个合成矢量的角度值,并在循环中不断自增而形成一个旋转的磁场。转子自动会随着这个旋转的磁场旋转。

system

需要注意的是在开环运行模式中,速度不再与转矩相关而是与角度的自增速度相关。

5.5 故障检测

5.5.1 过速

电机过速度异常‌是指电机在运行过程中转速超过其设计或允许的最大速度,这可能导致电机性能下降、设备损坏甚至安全事故。过速度异常的检测相对简单,一般采用转速检测法,在过速度时,电机转速会急剧上升。通过光电编码器或霍尔传感器实时监控电机转速,将实时转速与预设的最高转速进行对比,如果转速高于设定值且持续一定时间,则判定为过速度,立即停止电机运行。

5.5.2 过载

驱动器或电机电流高于其额定电流运行的工况为过载工况。

过载分为驱动器过载和电机过载,逻辑一样。过载导致的驱动器损坏或电机损坏都是因为过热。

对于驱动器来说,逆变电桥的发热跟电流相关,跟电压几乎无关。 因为这个原因,很多驱动器使用输出电流标识容量,而非功率。

对于电机来说,发热主要来自于线圈、铁损和机械摩擦,机械摩擦主要跟转速相关,线圈发热跟电流相关。因为相比较摩擦损耗线圈发热占比较大,铁损不便计算,用电流判断过载较为合理。

过载与否基于额定电流判断,电流高于额定电流越多,则允许的过载运行时间越短。过载故障被触发后可以减速停机。

过载故障的严谨判断:根据过载表格设计,如1.2倍过载允许工作10分钟,1.5倍过载允许工作3分钟等。但这种算法计算量大,需要的前置测试繁多。

过载故障的简易判断:

  1. 过载电流 = 当前电流 - 额定电流;
  2. 过载程度 += 过载电流 × 过载电流过载判断运行周期;
  3. 如果 过载程度 < 0 则 过载程度 = 0;如果 过载程度 > 过载阈值 则 减速停机

5.5.3 堵转

堵转,电机因为负载过大转不动了,这时候的电机往往有电流过大和转速过低这两个特征,与过载的相同之处在电流过大,不同之处在转速。所以我们判定堵转往往采用转速检测法,在堵转时,电机转速会急剧下降。通过传感器实时监控电机转速,将实时转速与预设的最低速进行对比,如果转速低于设定值且持续一定时间,则判定为堵转。

5.5.4 编码器异常

编码器作为其闭环控制的关键部件,为电机控制提供精确反馈。然而,复杂的工业环境使编码器易受电磁干扰、机械振动等影响,出现信号丢失、数据跳变等异常,导致电机转矩波动、失步,严重影响设备运行效率与安全。因此代码通常要包括编码器异常检测(信号丢失、数据跳变)和故障报警提示。

编码器异常包括电压过低,磁场太弱,速度过快,这部分的检测可以通过读取MT6835磁编码器的状态寄存器来确定

system

6 软件代码教程

通过前面的介绍,我们已经了解了控制原理及通讯协议等基础理论知识,接下来我们将介绍本项目的软件代码实现。 system

本项目的程序流程如上图所示,项目程序初始化后进入主循环,中断程序中会执行控制算法、电流采样等重要程序,保障控制的实时性和功能完整性。

(1)上电初始化阶段,会完成定时器、ADC、GPIO 等外设的初始化,为控制算法运行奠定硬件基础;同时初始化 Modbus、CanOpen 等通信协议栈,配置从站参数、波特率等,确保电机控制指令可通过总线传输;此阶段还会预加载控制算法参数(如 PID 参数、S 型规划参数)和电机参数(如极对数、额定电流),使系统进入待运行状态。

(2)主循环会定时执行程序,包括处理 Modbus/CanOpen,定时解析总线指令,反馈电机的实际位置、速度、故障码等状态,实现上位机或主控制器对步进电机的远程控制;另外还有故障检测,会定期检查缺相、过流、过压、编码器故障等异常,一旦出现问题,立即执行保护动作,保障系统安全。

(3)中断处理阶段,ADC 中断会高频触发,实时采样电机绕组电流、母线电压等信号,为电流环控制提供反馈,保证电流调节的实时性;定时器中断按控制周期触发,执行位置环、速度环计算和 S 型规划的进度更新,保障运动控制的时序精度;SPI 中断用于读取编码器数据,快速同步电机实际位置信息,为位置环、速度环提供精准反馈。

代码流程看上去有些复杂,但实际上每个模块的功能都很清晰,下面我们将逐一介绍各个模块的实现细节。

6.1 系统及外设初始化

system

本项目的初始化流程如上图所示,包括时钟,PWM,Modbus,SPI等,时钟采用的144Mhz系统时钟,设置10Khz的中断支持定时中断与控制算法运行,ADC用于电流采样,SPI用于读取磁编码器的数据,CANopen与Modbus用于上位机通信,FLASH读写用于参数初始化及参数的储存,其中涉及到的GPIO,PWM,SPI,USART,ADC外设配置将在下文详细介绍。

6.1.1 系统时钟

system

时钟是系统的心脏,所有程序的执行节奏都由它决定,在系统中有着举足轻重的地位。本项目中通过定时器提供的中断为众多程序提供了执行周期,提高了系统执行效率与稳定性,在初始化的第一步,往往先初始化时钟,在需要高速处理速度的FOC中,我们采用N32G452CCL7的最大主频144MHZ就是为了复杂的控制算法快速跑完,下面进行具体的时钟树配置。 system

我们根据时钟树,时钟源使用外部高速时钟,PPL MULFCT设置为8MHz*18,得到144MHz(芯片支持的最大频率)。下面进行分频分配:

(1)AHB总线(高速外设如GPIO、SPI)直接使用系统时钟144MHz;

(2) APB2总线(更高速外设)为AHB时钟的一半72MHz;

(3)ABP1总线为AHB时钟的四分之一36MHz。

这样既保证了 FOC 算法需要的高速计算能力,又让不同外设工作在合适的频率(避免超频损坏)。下面是具体的代码实现,注意这里的时钟配置函数是芯片库提供的,初学者可以根据芯片手册自行查询相关函数。

void SystemClk_Init()
{
    ErrorStatus HSEStartUpStatus;

    //复位RCC寄存器到默认值
    RCC_DeInit();

    //打开外部晶振
    RCC_ConfigHse(RCC_HSE_ENABLE);
    //等待外部晶振就绪
    HSEStartUpStatus = RCC_WaitHseStable();
    // HSE启动成功,配置PLL和分频
    if(HSEStartUpStatus == SUCCESS)
    {
        //AHB使用系统时钟
        RCC_ConfigHclk(RCC_SYSCLK_DIV1); 
        //APB2(高速)为HCLK/2
        RCC_ConfigPclk2(RCC_HCLK_DIV2);  
        //APB1(低速)为HCLK/4
        RCC_ConfigPclk1(RCC_HCLK_DIV4);  

        //使能指令缓存(iCache),提高Flash读取速度(144MHz下很重要)
        FLASH_iCacheCmd(FLASH_iCache_EN);
        FLASH_PrefetchBufSet(FLASH_PrefetchBuf_DIS);

        //设置Flash操作延时为4个周期
        //因为主频到144MHz后,Flash读写速度跟不上,需要加延时等待
        FLASH_SetLatency(FLASH_LATENCY_4); 

        //配置PLL:以HSE为源(不分频,直接用8MHz),倍频18倍 → 8×18=144MHz
        RCC_ConfigPll(RCC_PLL_SRC_HSE_DIV1, RCC_PLL_MUL_18);//PPL MULFCT设置为x18

        //使能PLL(让刚才的配置生效)
        RCC_EnablePll(ENABLE);
         // 等待PLL稳定
        while(RCC_GetFlagStatus(RCC_FLAG_PLLRD) == RESET){}
        //配置系统时钟源为PLL
        RCC_ConfigSysclk(RCC_SYSCLK_SRC_PLLCLK);
        // 等待系统时钟切换完成
        while(RCC_GetSysclkSrc() != 0x08){}
    }

}

6.1.2 GPIO

GPIO(General-purpose input/output)即通用输入输出口,是芯片与外部设备沟通的 “桥梁”——LED 的亮灭、PWM 波的输出、传感器的数据读取,都要通过 GPIO 实现。 下面进入GPIO初始化讲解,这个环节关系到其他外设的初始化,代码中可以看到PWM,ADC,SPI,USART等外设接口的初始化。

无论什么外设,GPIO的初始化都遵循“三步法”。如下图所示: system

即使用GPIO_InitType定义一个结构体,这个结构体下Pin,GPIO_Speed,GPIO_Mode这三个成员,分别指定引脚号,引脚速度,与引脚模式(上拉,推挽,下拉等),注意,各个结构体下的成员根据芯片的不同型号有较大差异。读者需要根据芯片的数据手册自行查询。

在指定好这三个成员后就可以将结构体传入 GPIO_InitPeripheral进行初始化了。

void Gpio_Init(void)
{
    GPIO_InitType GPIO_InitStructure;

    // -------------------------- 1. LED指示灯(GPIOC) --------------------------
    // 配置引脚:PC13、PC14、PC15
    GPIO_InitStructure.Pin = GPIO_PIN_13 | GPIO_PIN_14| GPIO_PIN_15;

    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // LED对速度要求低,50MHz足够
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;  // 推挽输出:可直接输出高低电平,驱动LED
    GPIO_InitPeripheral(GPIOC, &GPIO_InitStructure);  // 应用到GPIOC端口
    LED1_OFF;   //上电默认暗
    LED2_OFF;   //上电默认暗

    // -------------------------- 2. 总使能控制(GPIOA) --------------------------
    GPIO_InitStructure.Pin =  GPIO_PIN_12;                // PA12:总使能引脚(ALL_L)
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;      // 推挽输出:直接控制使能信号
    GPIO_InitPeripheral(GPIOA, &GPIO_InitStructure); 
    GPIO_WriteBit(GPIOA, GPIO_PIN_12, Bit_RESET);         // 初始输出低电平(默认使能)


     // -------------------------- 3. PWM输出(连接逆变电路,GPIOA) --------------------------
    GPIO_InitStruct(&GPIO_InitStructure);
    // PA8、PA9、PA10、PA11:定时器1的PWM通道,控制逆变电路的MOS管
    GPIO_InitStructure.Pin = GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10| GPIO_PIN_11;  
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;     // 复用推挽输出:由定时器外设控制,而非CPU直接写
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;   // PWM需要快速切换,50MHz满足需求
    GPIO_InitPeripheral(GPIOA, &GPIO_InitStructure);

     // -------------------------- 4. ADC采样(模拟信号输入,GPIOA) --------------------------
     // PA5:电流采样引脚(模拟信号输入到ADC)
    GPIO_InitStruct(&GPIO_InitStructure);
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;   // 模拟输入:直接连接ADC模块,不经过数字电路
    GPIO_InitStructure.Pin = GPIO_PIN_5;
    GPIO_InitPeripheral(GPIOA, &GPIO_InitStructure);

    // PA4:母线电压采样引脚(同上,模拟输入)
    GPIO_InitStructure.Pin = GPIO_PIN_4;
    GPIO_InitPeripheral(GPIOA, &GPIO_InitStructure);

    // PA7:另一路电流采样引脚(同上,模拟输入)
    GPIO_InitStructure.Pin = GPIO_PIN_7;
    GPIO_InitPeripheral(GPIOA, &GPIO_InitStructure);

    // -------------------------- 5. SPI通信(连接MT6835磁编码器,GPIOB) --------------------------
    // PB3(SCK)、PB4(MISO)、PB5(MOSI):SPI通信引脚
    GPIO_InitStructure.Pin = GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5;  //SPI通信口分别为 SCK MISO MOSI
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;               // SPI通信需要较高速度
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;                 // 复用推挽:由SPI外设控制时序
    GPIO_InitPeripheral(GPIOB, &GPIO_InitStructure);            

    // PA15:SPI片选(NSS)引脚(控制哪个设备通信)
    GPIO_InitStructure.Pin = GPIO_PIN_15;               //SPI通信口 NSS
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;    // 推挽输出:CPU直接控制片选(高低电平切换)
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitPeripheral(GPIOA, &GPIO_InitStructure);
    GPIO_SetBits(GPIOA, GPIO_PIN_15);

    // -------------------------- 6. 校准控制(GPIOB) --------------------------
    GPIO_InitStructure.Pin = GPIO_PIN_12;     // PB12:校准控制引脚(CAL)
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitPeripheral(GPIOB, &GPIO_InitStructure);
    GPIO_ResetBits(GPIOB, GPIO_PIN_12);        

    // -------------------------- 7. Modbus-RS485通信(USART + 控制引脚) --------------------------
    // 使能USART和GPIO的时钟(必须先开时钟,外设才能工作)
    RCC_EnableAPB2PeriphClk(MODBUS_USART_GPIO_CLK, ENABLE);
    MODBUS_USART_APBxClkCmd(MODBUS_USART_CLK, ENABLE);
    GPIO_InitStruct(&GPIO_InitStucture);

    //GPIO发送端采用复用推挽输出;
    GPIO_InitStucture.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStucture.Pin = MODBUS_USART_TxPin;//modbus初始化
    GPIO_InitPeripheral(MODBUS_USART_GPIO, &GPIO_InitStucture);

    //GPIO接收端采用浮空输入;
    GPIO_InitStucture.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_InitStucture.Pin = MODBUS_USART_RxPin;//modbus初始化
    GPIO_InitPeripheral(MODBUS_USART_GPIO, &GPIO_InitStucture);

    //485发送控制引脚
    GPIO_InitStucture.Pin = RS485_DE_Pin;
    GPIO_InitStucture.GPIO_Mode = GPIO_Mode_Out_PP;   //推挽输出
    GPIO_InitStucture.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitPeripheral(RS485_DE_GPIO, &GPIO_InitStucture);
    GPIO_ResetBits(RS485_DE_GPIO, RS485_DE_Pin);      //设置为接收模式,默认接收
}

6.1.3 USART

USART(Universal Synchronous Asynchronous Receiver Transmitter)即通用同步异步收发器,是芯片与外部设备传输数据的常用工具,既能同步通信(双方按同一时钟节奏传数据),也能异步通信(靠约定的规则同步)。在项目中,USART 配合 RS-485 硬件,实现 Modbus 协议的数据传输 ,就像一个快递员,按规矩把数据打包、传送、签收。

USART 通信需要双方约定参数,这些参数通过USART_InitType结构体配置,核心成员如下:

参数成员 作用说明 本项目配置及原因
BaudRate(波特率) 单位时间内传送的码元符号的个数 由输入参数baud指定;项目中Modbus采用115200,兼顾远距离传输稳定性
HardwareFlowControl 硬件流控制,通过RTS/CTS引脚,实现“数据过多时收件方喊停”的功能(此项目未使用) USART_HFCTRL_NONE(不启用);项目数据量小,软件控制即可满足需求
Mode 通信模式:可单独配置接收(RX)、发送(TX)或双向通信 USART_MODE_RX \ USART_MODE_TX(双向);Modbus需主机发命令、从机回响应,需双向通信
Parity(奇偶校验) 在数据尾部加1位,使“1的总数”为奇/偶数,不常用 USART_PE_NO(不启用);Modbus采用更可靠的CRC校验,替代奇偶校验
StopBits(停止位) 数据结束的标记,表示字符数据传输停止的位 USART_STPB_1(1个停止位);最通用配置,平衡传输效率与可靠性
WordLength(字长) 单个字符的数据位数,确定字符数据长度 USART_WL_8B(8位);Modbus协议中数据以8位为基本单位,可覆盖0~255数值范围

下面是 Modbus 通信专用的 USART 初始化代码,结合 Modbus 场景理解更清晰:


void Modbus_USART_Init(unsigned int baud)
{

    USART_InitType USART_InitStucture;  // USART配置结构体
    NVIC_InitType NVIC_InitStucture;    // 中断优先级配置结构体


    // -------------------------- 1. 复位USART外设  --------------------------
    USART_DeInit(MODBUS_USART);

    // -------------------------- 2. 配置USART核心参数  --------------------------
    USART_InitStucture.BaudRate = baud;                            // 波特率:与从机保持一致(115200)
    USART_InitStucture.HardwareFlowControl = USART_HFCTRL_NONE;    // 未启用硬件流控制
    USART_InitStucture.Mode = USART_MODE_RX | USART_MODE_TX;       // 允许接收和发送(双向通信)
    USART_InitStucture.Parity = USART_PE_NO;                       // 不启用奇偶校验(依赖Modbus的CRC校验)
    USART_InitStucture.StopBits = USART_STPB_1;                    // 1个停止位(Modbus标准)
    USART_InitStucture.WordLength = USART_WL_8B;                   // 8位数据位(Modbus数据格式)

    // -------------------------- 3. 应用配置到USART外设 --------------------------
    USART_Init(MODBUS_USART, &USART_InitStucture);

    // --------------4. 启用DMA收发(提高效率:数据直接在USART和内存间传输,不占用CPU) ---------------
    USART_EnableDMA(MODBUS_USART, USART_DMAREQ_RX | USART_DMAREQ_TX, ENABLE);

    // --------------------------  5. 使能USART外设 --------------------------
    USART_Enable(MODBUS_USART, ENABLE);
    // -------------------------- 6. 开启空闲中断   --------------------------
    USART_ConfigInt(MODBUS_USART, USART_INT_IDLEF, ENABLE); //开启空闲中断

    //NVIC寄存器配置:中断请求通道设置为USART3,启动使能,抢占优先级为1,子(响应)优先级为1
    NVIC_InitStucture.NVIC_IRQChannel = MODBUS_USART_IRQn;
    NVIC_InitStucture.NVIC_IRQChannelCmd = ENABLE;
    NVIC_InitStucture.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_InitStucture.NVIC_IRQChannelSubPriority = 1;
    NVIC_Init(&NVIC_InitStucture);
}

6.1.4 ADC

ADC(Analog-to-Digital Converter),中文名模数转换器,是将连续变化的模拟信号(如电压、电流)转换成数字信号。在电机控制中,我们需要实时知道母线电压(是否稳定)和绕组电流(电机输出是否正常),这些都是模拟量,必须通过 ADC 转换成数字量,才能被 MCU 处理。

举个例子:绕组电流本身是电流信号,ADC 不能直接读取电流,所以需要先通过采样电阻(电流流过产生电压)和放大电路(把微弱电压放大到 ADC 能识别的范围),把电流信号变成电压信号,再由 ADC 转换为数字值 —— 这就像用温度计测体温,先把温度转换成水银高度,再读数。

和前面的USART一样,ADC的初始化也需要配置一些参数,这些参数通过ADC_InitType结构体配置,核心成员如下:

参数成员 作用说明 本项目配置及原因
WorkMode(工作模式) 决定ADC的采样方式(如独立采样、同步采样等),影响多通道采样的时间一致性。 项目采用同步注入模式:支持多通道同时采样,保证A相电流、B相电流、母线电压的采样无时差,满足FOC电流环对数据同步性的要求。
MultiChEn(多通道使能) 控制是否允许ADC一次扫描多个通道(单通道/多通道切换)。 ENABLE(使能):需同时采样绕组电流(Iu、Iv)和母线电压(Vdc)多个信号,单通道模式无法满足需求。
ContinueConvEn(连续转换使能) 控制ADC是否自动循环采样(连续/单次转换切换)。 DISABLE(禁用):由TIM1(PWM定时器)外部触发采样,确保采样频率与电流环周期(如10kHz)严格同步,避免数据采集节奏混乱。
ExtTrigSelect(外部触发选择) 选择启动ADC采样的触发源(如定时器、外部引脚),决定采样时机。 ADC_EXT_TRIG_INJ_CONV_T1_TRGO(TIM1触发):使采样与PWM输出同步,避开MOS管开关噪声干扰,同时保证采样频率稳定。
DatAlign(数据对齐) 定义ADC转换结果在寄存器中的存储方式(左对齐/右对齐),影响数据处理便捷性。 ADC_DAT_ALIGN_R(右对齐):转换结果从寄存器最低位开始存储(如12位结果存为0x0FFF),符合常规数据处理逻辑,无需额外移位计算。
ChsNumber(通道数量) 指定规则组多通道扫描的通道总数(仅多通道使能时有效)。 0:本项目采用“注入组”采样(优先级更高,适合电流、电压等实时信号),规则组不使用,故设为0。

了解了基本原理和核心参数,下面是 ADC 的初始化代码:



void Adc_Init(void)
{
    ADC_InitType ADC_InitStructure;     // ADC配置结构体
    NVIC_InitType NVIC_InitStructure;   // 中断优先级配置结构体
    // -------------------------- 1. 复位ADC外设 --------------------------
    ADC_DeInit(ADC1);
    ADC_DeInit(ADC2);

    // -------------------------- 2. 配置ADC核心参数 --------------------------
    ADC_InitStruct(&ADC_InitStructure);
    // 工作模式:同步注入模式(ADC1和ADC2同步采样,保证多通道数据同时性)
    ADC_InitStructure.WorkMode = ADC_WORKMODE_INJ_SIMULT;
    // 多通道使能:允许一次采样多个通道(电流、电压需要同时测)
    ADC_InitStructure.MultiChEn = ENABLE;
    // 连续转换禁用:不自动循环采样,由外部触发(TIM1)控制时机
    ADC_InitStructure.ContinueConvEn = DISABLE;
    // 外部触发源:用TIM1的触发信号启动采样(和PWM同步,减少噪声)
    ADC_InitStructure.ExtTrigSelect = ADC_EXT_TRIG_INJ_CONV_T1_TRGO;
    // 数据对齐:右对齐(12位结果从寄存器低位开始存储,便于处理)
    ADC_InitStructure.DatAlign = ADC_DAT_ALIGN_R;
    // 通道数量:规则组不使用,注入组采样,所以设置为0
    ADC_InitStructure.ChsNumber = 0;

    // -------------------------- 3. 应用配置到ADC外设 --------------------------
    ADC_Init(ADC1, &ADC_InitStructure);
    ADC_Init(ADC2, &ADC_InitStructure);

    // -------------------------- 4. 配置ADC通道 --------------------------
    // ADC1配置:注入组采样3个通道
    ADC_ConfigInjectedSequencerLength(ADC2,3);//3个通道
    ADC_ConfigInjectedChannel(ADC2, ADC_CH_4, 1, ADC_SAMP_TIME_28CYCLES5);//Iv
    ADC_ConfigInjectedChannel(ADC2, ADC_CH_2, 2, ADC_SAMP_TIME_28CYCLES5);//Iu
    ADC_ConfigInjectedChannel(ADC2, ADC_CH_1, 3, ADC_SAMP_TIME_28CYCLES5);//vdc

    //此处的ADC_SAMP_TIME_28CYCLES5表示采样28.5个周期,计算周期时要算上系统自带的1.5个周期
    //一个周期多长时间就要看ADCCLK的时钟设置了

    // -------------------------- 5. 使能外部触发注入转换 --------------------------
    ADC_EnableExternalTrigInjectedConv(ADC2,ENABLE);


    // --------------------------6. 配置中断优先级 --------------------------
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);           // 中断分组:2位抢占优先级,2位响应优先级
    NVIC_InitStructure.NVIC_IRQChannel = ADC1_2_IRQn;         // ADC1和ADC2的中断通道
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; // 抢占优先级2(较高,保证实时性
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;        // 响应优先级1
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;           // 使能中断
    NVIC_Init(&NVIC_InitStructure);

    // -------------------------- 7. 使能ADC外设 --------------------------
    ADC_Enable(ADC2, ENABLE);
    while(ADC_GetFlagStatusNew(ADC2,ADC_FLAG_RDY) == RESET);

    // -------------------------- 8. 启动ADC校准 --------------------------
    ADC_StartCalibration(ADC2);
    while (ADC_GetCalibrationStatus(ADC2));

    // -------------------------- 9. 使能ADC2中断:注入转换完成(采样结束)和模拟看门狗(电压超限时报警) --------------------------
    ADC_ConfigInt(ADC2, ADC_INT_JENDC | ADC_INT_AWD, ENABLE);
}

6.1.5 DMA

DMA(直接存储器访问)是一种不打扰CPU的数据传输方式 —— 它能让外设(如 USART)和内存之间直接交换数据(如下图),不用 CPU 全程参与,就像给数据开了直通车,CPU专注于 FOC 算法等核心任务,大大提高系统效率。在本项目中,DMA 被用来处理 Modbus 通信的数据收发,让 CPU 能专注于电机控制算法( FOC),避免被频繁的串口数据传输打断。

system

DMA 的工作方式由DMA_InitType结构体的参数决定,这些参数定义了 “数据从哪来、到哪去、一次传多少、速度优先级” 等规则。结合本项目Modbus通信的需求,关键参数解析如下:

参数成员 作用说明 本项目配置(TX发送/RX接收)
PeriphAddr(外设地址) 指定外设数据寄存器的物理地址 均为(uint32_t)&(MODBUS_USART->DAT):USART的数据寄存器(DAT)是收发数据的共用端点(半双工通信特点)
MemAddr(内存地址) 指定内存中数据缓冲区的起始地址 - TX:(uint32_t)modbus_send_data(发送缓冲区,DMA从中取数据);
- RX:(uint32_t)modbus_recv_data(接收缓冲区,DMA将数据存入此处)
Direction(传输方向) 定义数据流动方向,如:外设→内存 - TX:DMA_DIR_PERIPH_DST(内存→外设,发送数据);
- RX:DMA_DIR_PERIPH_SRC(外设→内存,接收数据)
BufSize(传输数据量) 单次DMA传输的字节数 - TX:0(初始化不指定,发送前动态设置实际长度);
- RX:MODBUS_RX_MAXBUFF(最大接收长度,防溢出)
PeriphInc(外设地址递增) 传输后是否自动增加外设地址,适合多寄存器连续传输 均为DMA_PERIPH_INC_DISABLE(禁用):USART数据寄存器地址固定,无需递增
DMA_MemoryInc(内存地址递增) 传输后是否自动增加内存地址,适合连续存储多字节 均为DMA_MEM_INC_ENABLE(使能):内存缓冲区为数组,需依次存储下一字节(如buf[0]→buf[1]
PeriphDataSize(外设数据宽度) 外设一次传输的数据大小(字节/半字/字) 均为DMA_PERIPH_DATA_SIZE_BYTE(字节):匹配USART的8位数据格式(Modbus协议要求)
MemDataSize(内存数据宽度) 内存一次传输的数据大小 均为DMA_MemoryDataSize_Byte(字节):与外设宽度一致,避免数据格式错误
CircularMode(循环模式) 传输完成后是否自动重启(循环/单次传输) 均为DMA_MODE_NORMAL(正常模式):Modbus数据按帧传输,一帧完成后停止,需手动重启下一次
Priority(通道优先级) 多DMA通道竞争时的仲裁优先级(高/中/低)。 均为DMA_PRIORITY_LOW(低优先级):Modbus通信优先级低于电机控制(如FOC电流环)
Mem2Mem(内存到内存模式) 是否允许内存间直接传输(不经过外设)。 均为DMA_M2M_DISABLE(禁用):本项目为外设(USART)与内存间传输,无需内存直传

了解了DMA工作方式及核心参数,下面是 Modbus 通信专用的 DMA 初始化代码:


void Modbus_DMA_Init(void)
{
    DMA_InitType DMA_InitStruct;     // DMA配置结构体
    NVIC_InitType NVIC_InitStruct;   // 中断优先级配置结构体

    // -------------------------- 1. 复位DMA外设 --------------------------
    RCC_EnableAHBPeriphClk(MODBUS_DMA_CLK, ENABLE);


    // -------------------------- 2. 复位TX和RX通道 --------------------------
    DMA_DeInit(MODBUS_DMA_TX_Channel);
    DMA_DeInit(MODBUS_DMA_RX_Channel);
    DMA_StructInit(&DMA_InitStruct);  //先默认初始化结构体

    // -------------------------- 3. 配置TX通道(发送数据:内存→USART) --------------------------
    // 外设地址:USART的数据寄存器(DAT),所有收发都经过这里
    DMA_InitStruct.PeriphAddr = (uint32_t) & (MODBUS_USART->DAT); // USART_DR 地址偏移:0x04
    // 内存地址:发送缓冲区(modbus_send_data数组的首地址)
    DMA_InitStruct.MemAddr = (uint32_t)modbus_send_data;  
    // 传输方向:内存→外设(数据从缓冲区发给USART)
    DMA_InitStruct.Direction = DMA_DIR_PERIPH_DST;
     // 传输数据量:初始化为0(发送前根据实际数据长度设置,避免无效传输)
    DMA_InitStruct.BufSize = 0;             
    // 外设地址不递增:USART寄存器地址固定
    DMA_InitStruct.PeriphInc = DMA_PERIPH_INC_DISABLE;
    // 内存地址递增:发送缓冲区是数组,数据逐字节发送
    DMA_InitStruct.DMA_MemoryInc = DMA_MEM_INC_ENABLE;
    // 外设数据宽度:8位(Modbus协议要求)
    DMA_InitStruct.PeriphDataSize = DMA_PERIPH_DATA_SIZE_BYTE;
    // 内存数据宽度:8位(与外设一致)
    DMA_InitStruct.MemDataSize = DMA_MemoryDataSize_Byte;
    // 循环模式:禁用(发送一帧数据后停止)
    DMA_InitStruct.CircularMode = DMA_MODE_NORMAL;
    // 优先级:低(Modbus通信优先级低于FOC控制)
    DMA_InitStruct.Priority = DMA_PRIORITY_LOW;
     // 内存到内存模式:禁用,通过USART外设传输
    DMA_InitStruct.Mem2Mem = DMA_M2M_DISABLE;
    // 应用配置到DMA通道
    DMA_Init(MODBUS_DMA_TX_Channel, &DMA_InitStruct);
    // 将DMA通道3重映射到USART3_TX(TX发送)
    DMA_RequestRemap(MODBUS_DMA_TX_REMAP, MODBUS_DMA, MODBUS_DMA_TX_Channel, ENABLE);

    // -------------------------- 4。 配置RX通道(接收数据:USART→内存) --------------------------
    // 外设地址:USART的数据寄存器(DAT),所有收发都经过这里
    DMA_InitStruct.PeriphAddr = (uint32_t) & (MODBUS_USART->DAT); 
    // 内存地址:接收缓冲区(modbus_recv_data数组的首地址)
    DMA_InitStruct.MemAddr = (uint32_t)modbus_recv_data;  
    // 传输方向:外设→内存(数据从USART接收并存入缓冲区)
    DMA_InitStruct.Direction = DMA_DIR_PERIPH_SRC;      
    // 传输数据量:最大接收长度(防止溢出,Modbus协议规定最大256字节)
    // 注意:实际接收长度由Modbus协议控制,DMA只负责存储
    DMA_InitStruct.BufSize = MODBUS_RX_MAXBUFF;
    // 外设地址不递增:USART寄存器地址固定
    DMA_InitStruct.PeriphInc = DMA_PERIPH_INC_DISABLE;
    // 内存地址递增:接收缓冲区是数组,数据逐字节存储
    DMA_InitStruct.DMA_MemoryInc = DMA_MEM_INC_ENABLE;
    // 外设数据宽度:8位(Modbus协议要求)
    DMA_InitStruct.PeriphDataSize = DMA_PERIPH_DATA_SIZE_BYTE;
    // 内存数据宽度:8位(与外设一致)
    DMA_InitStruct.MemDataSize = DMA_MemoryDataSize_Byte;
    // 循环模式:禁用(接收一帧数据后停止)
    DMA_InitStruct.CircularMode = DMA_MODE_NORMAL;
    // 优先级:低(Modbus通信优先级低于FOC控制)
    DMA_InitStruct.Priority = DMA_PRIORITY_LOW;
    // 内存到内存模式:禁用,通过USART外设传输
    DMA_InitStruct.Mem2Mem = DMA_M2M_DISABLE;
    // 应用配置到DMA通道
    DMA_Init(MODBUS_DMA_RX_Channel, &DMA_InitStruct);
    // 将DMA通道2重映射到USART3_RX(RX接收)
    DMA_RequestRemap(MODBUS_DMA_RX_REMAP, MODBUS_DMA, MODBUS_DMA_RX_Channel, ENABLE);


    // -------------------------- 5. 配置DMA发送完成中断 --------------------------
    // 中断通道:TX通道的中断
    NVIC_InitStruct.NVIC_IRQChannel = MODBUS_DMA_TX_IRQn;
    // 启用中断:使能DMA通道的传输完成中断
    NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
    // 抢占优先级0(较高,确保发送完成后及时处理)
    NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0;  
    // 子优先级0(响应优先级,确保中断处理及时)
    NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;         
    // 应用配置到NVIC
    NVIC_Init(&NVIC_InitStruct);
    // 配置MODBUS_DMA_TX_Channel传输完成中断
    DMA_ConfigInt(MODBUS_DMA_TX_Channel, DMA_INT_TXC, ENABLE);

    //禁止发送,使能接收
    DMA_EnableChannel(MODBUS_DMA_TX_Channel, DISABLE);
    DMA_EnableChannel(MODBUS_DMA_RX_Channel, ENABLE);
}

6.1.6 PWM

PWM(Pulse Width Modulation,脉冲宽度调制)是一种通过改变脉冲的宽度来控制输出功率的技术,即调整方波占空比实现模拟电压控制。它在电机控制、LED调光等领域有广泛应用。电机直流电源(24V),PWM 就像快速开关—— 通过高频切换通断,让绕组平均感受到不同的电压(占空比 50% 相当于 12V,30% 相当于 7.2V),最终实现电流的精准控制,PWM原理如下图所示: system

PWM这部分使用的结构体和成员较多,主要是TIM_TimeBaseInitType(时基配置结构体),OCInitType(输出比较配置结构体),TIM_BDTRInitType(刹车与死区配置结构体),具体如下:

(1)TIM_TimeBaseInitType(时基配置结构体)

参数成员 作用说明 本项目配置及原因
Prescaler 预分频器,降低定时器时钟频率(时钟频率 = 定时器时钟 / (Prescaler + 1)) 0(不分频):保持高频时钟输入,确保PWM频率足够高(减少电机运行噪声)
CntMode 计数器计数模式(递增/递减/中心对齐) TIM_CNT_MODE_CENTER_ALIGN1(中心对齐1):生成对称PWM波,减少电流谐波,使电机运行更平稳
Period 计数器最大值,决定PWM周期(周期 = (Period + 1) × 2 × 时钟周期,中心对齐模式) (MAIN_FREQUENCY / (PWM_FREQUENCY1*2)) - 1计算(如4499):匹配FOC电流环需求(通常10kHz以上)
ClkDiv 定时器内部时钟分频(影响计数精度) TIM_CLK_DIV1(1分频):不分频,保证计数精度,避免PWM频率偏移
RepetCnt 重复计数器(高级定时器特性,控制更新事件间隔) 0:每个计数周期均触发更新事件,保证PWM实时性

(2)OCInitType(输出比较配置结构体)

参数成员 作用说明 本项目配置及原因
OcMode PWM输出模式(定义计数器与脉冲宽度的比较逻辑) TIM_OCMODE_PWM1:计数器值 < Pulse时输出有效电平,符合常规PWM控制逻辑
OutputState 主通道PWM输出使能控制 初始TIM_OUTPUT_STATE_DISABLE(禁用):避免初始化时误触发,后续统一使能
OutputNState 互补通道PWM输出使能控制(用于半桥上下管) TIM_OUTPUT_NSTATE_DISABLE(禁用):本项目通过软件模拟互补输出,不依赖硬件互补通道
Pulse 脉冲宽度,决定PWM占空比(占空比 = Pulse / Period × 100%) (TimerPeriod >> 1)(周期的1/2):初始占空比50%(对应0电压矢量,避免电机意外转动)
OcPolarity 主通道有效电平(高/低电平为有效) 通道1/3:TIM_OC_POLARITY_HIGH(高有效);通道2/4:TIM_OC_POLARITY_LOW(低有效):模拟半桥上下管互补开关
OcNPolarity 互补通道有效电平 TIM_OCN_POLARITY_HIGH(高有效):与主通道配合,确保上下管开关逻辑正确
OcIdleState 定时器停止时主通道输出电平 TIM_OC_IDLE_STATE_RESET(低电平):停止时关断输出,保护电机和驱动板
OcNIdleState 定时器停止时互补通道输出电平 TIM_OC_IDLE_STATE_RESET(低电平):停止时关断互补输出,避免短路

(3)TIM_BDTRInitType(刹车与死区配置结构体)(注:本项目中未配置)

参数成员 作用说明 本项目配置及原因
DeadTime 死区时间(防止半桥上下管同时导通的安全间隔) 未配置(禁用):本项目通过软件逻辑实现死区控制,替代硬件死区生成
LockLevel 锁定级别(保护PWM配置不被意外修改) 未配置(禁用):简化初始化,适合教学场景
OssrState 运行模式下的空闲状态输出控制 未配置(禁用):本项目无需特殊空闲状态控制
OssiState 停止模式下的空闲状态输出控制 未配置(禁用):停止时通过OcIdleState统一控制输出
BreakState 刹车功能使能(外部信号触发紧急停转) 未配置(禁用):教学项目暂不涉及紧急刹车功能
BreakPolarity 刹车信号极性(高/低电平触发刹车) 未配置(禁用):同上

注:二相混合式步进电机需要两相pwm,每相有两路互补的pwm,共四个pwm通道,双H桥逆变电路中同一桥臂上的开关管为互补的,读者可以结合上面双h桥逆变电路理解。本项目因为串口占用并未使用互补的通道,而是使用了(CH1 CH2 CH3 CH4)四个独立的通道,将其中两个通道反转后模拟互补的pwm输出,所以未使用死区生成的结构体。硬件生成死区用不了,也有软件的死区生成,详情请见SVPWM的章节

下面是 PWM 初始化代码,结合电机控制需求详细注释,理解每个配置如何影响电机运行:

void Pwm_Init(void)
{
    TIM_TimeBaseInitType TIM1_TimeBaseStructure;  // 时基配置结构体
    OCInitType TIM1_OCInitStructure;              // 输出比较配置结构体
    TIM_BDTRInitType TIM1_BDTRInitStructure;      // 刹车与死区配置结构体(本项目未使用)

    uint16_t TimerPeriod = 0;  // 定时器周期值(计数器最大值)

    // --------------------------  1. 计算定时器周期(决定PWM频率) --------------------------
    // 公式:TimerPeriod = (系统时钟频率 / (目标PWM频率 × 2)) - 1
    // 乘2是因为中心对齐模式下,一个PWM周期包含"递增+递减"两个阶段
    TimerPeriod = (MAIN_FREQUENCY / (PWM_FREQUENCY1*2)) - 1;  //4499

    // --------------------------  2. 初始化TIM1定时器 --------------------------
    TIM_DeInit(TIM1);
    // 初始化时基结构体(填充默认值
    TIM_InitTimBaseStruct(&TIM1_TimeBaseStructure);

    // --------------------------  3. 配置时基参数 --------------------------
    TIM1_TimeBaseStructure.Prescaler = 0;                          // 预分频0:定时器时钟 = 系统时钟(不分频)
    TIM1_TimeBaseStructure.CntMode = TIM_CNT_MODE_CENTER_ALIGN1;   // 中心对齐模式1:对称PWM
    TIM1_TimeBaseStructure.Period = TimerPeriod;                   // 周期值:决定PWM频率
    TIM1_TimeBaseStructure.ClkDiv = TIM_CLK_DIV1;                  // 时钟分频1:不分频
    TIM1_TimeBaseStructure.RepetCnt = 0;                           // 重复计数0:每个周期都更新PWM
    // 应用时基配置到TIM1
    TIM_InitTimeBase(TIM1, &TIM1_TimeBaseStructure);

    // --------------------------  4. 配置输出比较(PWM模式) --------------------------
    // 初始化输出比较结构体(填充默认值)
    TIM_InitOcStruct(&TIM1_OCInitStructure);
    TIM1_OCInitStructure.OcMode = TIM_OCMODE_PWM1;                 // PWM模式1:CNT < Pulse时输出有效电平
    TIM1_OCInitStructure.OutputState = TIM_OUTPUT_STATE_DISABLE;   // 先禁用主输出(后续统一使能)
    TIM1_OCInitStructure.OutputNState = TIM_OUTPUT_NSTATE_DISABLE; // 禁用互补输出                 
    TIM1_OCInitStructure.Pulse = (TimerPeriod>>1);                 // 初始Pulse = 周期/2 → 占空比50%(0电压矢量)
    TIM1_OCInitStructure.OcPolarity = TIM_OC_POLARITY_HIGH;        // 通道1/3:高电平有效
    TIM1_OCInitStructure.OcNPolarity = TIM_OCN_POLARITY_HIGH;      // 互补通道:高电平有效
    TIM1_OCInitStructure.OcIdleState = TIM_OC_IDLE_STATE_RESET;    // 空闲时输出低电平(关断)
    TIM1_OCInitStructure.OcNIdleState = TIM_OC_IDLE_STATE_RESET;   // 互补通道空闲时输出低电平

    // 配置通道1、3(高极性)
    TIM_InitOc1(TIM1, &TIM1_OCInitStructure);   // 应用到通道1
    TIM_InitOc3(TIM1, &TIM1_OCInitStructure);   // 应用到通道3

    // 调整通道2、4为低极性(与1、3互补,模拟半桥上下管)
    TIM1_OCInitStructure.OcPolarity = TIM_OC_POLARITY_LOW;    // 通道2/4:低电平有效
    TIM_InitOc2(TIM1, &TIM1_OCInitStructure);   // 应用到通道2
    TIM_InitOc4(TIM1, &TIM1_OCInitStructure);   // 应用到通道4

    // --------------------------  5.选择触发信号(更新事件作为触发源,用于同步ADC采样) --------------------------
    TIM_SelectOutputTrig(TIM1, TIM_TRGO_SRC_UPDATE);  

    // --------------------------  6.使能通道预加载 --------------------------
    TIM_ConfigOc1Preload(TIM1, TIM_OC_PRE_LOAD_ENABLE);
    TIM_ConfigOc2Preload(TIM1, TIM_OC_PRE_LOAD_ENABLE);
    TIM_ConfigOc3Preload(TIM1, TIM_OC_PRE_LOAD_ENABLE);
    TIM_ConfigOc4Preload(TIM1, TIM_OC_PRE_LOAD_ENABLE);

    // --------------------------  7. 启动定时器并使能PWM输出 --------------------------
    TIM_Enable(TIM1, ENABLE);
    TIM_EnableCtrlPwmOutputs(TIM1,ENABLE);
}

6.1.7 SPI

SPI(Serial Peripheral Interface)串行外设接口,是一种同步串行通信协议,常用于微控制器与外设之间的高速数据传输,通过单独的时钟线(SCK)实现主从设备的严格同步,适合高速数据传输(比 UART 快)。

SPI是一个同步数据总线,它使用主从架构,主设备控制通信时序,从设备响应主设备的命令。SPI通信通常包括四条线(如下图所示):

(1)SCK:Serial Clock,时钟线,由主机发送给从机,用于同步数据传输;

(2)MOSI:Maser Output Slave Input,主设备输出从设备输入(数据来自主机);

(3)MISO:Maser Input Slave Output,主设备输入从设备输出(数据来自从机);

(4)NSS:片选线,主机发出片选信号,选择特定的从设备进行通信(通常是低电平有效)。

system

精妙之处在于采用单独的数据线和单独的时钟信号来保证发送端和接收端的同步,避免了异步通信中可能出现的数据错位问题。本项目SPI通信主要用于读取MT6835磁编码器的数据。

同前面的初始化一样,SPI的初始化也是通过结构体来配置的,SPI_InitType结构体包含了SPI通信的所有参数设置,每个参数都决定了数据传输的时序、格式和速度。。下面是本项目中 SPI 的初始化的关键参数解析:

参数成员 作用说明 本项目配置及原因
DataDirection 定义数据传输方向(单工/半双工/全双工),决定是否同时使用MOSI和MISO线。 SPI_DIR_DOUBLELINE_FULLDUPLEX(双线全双工):需同时向MT6835发送命令和接收角度数据,双向通信满足需求。
SpiMode 配置主从模式(主设备生成时钟/从设备跟随时钟),决定通信的控制方。 SPI_MODE_MASTER(主设备):MCU需主动发起通信,控制磁编码器的数据读取节奏,符合“主控-被控”逻辑。
DataLen 设定每帧数据的位数(8位/16位等),需与从设备数据格式匹配。 SPI_DATA_SIZE_16BITS(16位):MT6835的角度数据帧为16位,匹配编码器的通信格式,避免数据截断。
CLKPOL 时钟极性:定义空闲状态时SCK线的电平(高/低),影响数据采样的基准。 SPI_CLKPOL_HIGH(高极性):空闲时SCK为高电平,与MT6835的时钟极性要求一致,确保采样时机正确。
CLKPHA 时钟相位:定义数据在时钟的第1/2个边沿被采样,决定信号读取的时刻。 SPI_CLKPHA_SECOND_EDGE(第二个边沿采样):匹配编码器的时序特性,在SCK第二个跳变沿采样数据,避免错位。
NSS 片选控制方式(硬件自动/软件手动),决定如何选中目标从设备。 SPI_NSS_SOFT(软件控制):通过GPIO手动拉低/拉高NSS线,灵活控制与编码器的通信启停(通信时拉低,结束时拉高)。
BaudRatePres 波特率预分频系数:决定SPI通信速度(波特率=系统时钟/分频系数)。 SPI_BR_PRESCALER_16(16分频):波特率=144MHz/16=9MHz,兼顾速度(满足FOC实时性)和稳定性(编码器支持该速率)。
FirstBit 数据传输顺序(高位MSB/低位LSB先传),需与从设备传输习惯一致。 SPI_FB_MSB(高位先传):符合MT6835的传输规范,多数外设默认高位先传,避免数据位序错误。
CRCPoly CRC校验多项式:用于数据传输的校验(可选),提高通信可靠性。 7(对应多项式x⁸+x²+x+1):启用CRC校验,减少角度数据传输错误(角度数据对FOC控制至关重要,需保证准确性)。

了解原理与核心参数配置后,下面进入SPI初始化代码,结合上面参数解析,理解每个配置如何影响通信:

void MT6835_Init(void)
{

    NVIC_InitType NVIC_InitStructure;// 中断优先级配置结构体

    // -------------------------- 1. 配置SPI中断优先级(确保数据接收能及时处理) --------------------------
    NVIC_InitStructure.NVIC_IRQChannel                   = SPI3_IRQn;     // SPI3的中断通道
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;             // 抢占优先级2(较高,保证实时性)
    NVIC_InitStructure.NVIC_IRQChannelSubPriority        = 1;             // 响应优先级1
    NVIC_InitStructure.NVIC_IRQChannelCmd                = ENABLE;        // 使能中断
    NVIC_Init(&NVIC_InitStructure);

    // SPI配置结构体
    SPI_InitType  SPI_InitStructure;

    // -------------------------- 2.  配置SPI核心参数(与MT6835的通信规则匹配)--------------------------
     // 数据方向:全双工(同时收发)
    SPI_InitStructure.DataDirection = SPI_DIR_DOUBLELINE_FULLDUPLEX;
    // 模式:主设备(MCU控制时钟)
    SPI_InitStructure.SpiMode = SPI_MODE_MASTER;
     // 数据长度:16位(编码器数据帧为16位)
    SPI_InitStructure.DataLen = SPI_DATA_SIZE_16BITS;
    // 时钟极性:高极性(空闲时SCK为高电平)
    SPI_InitStructure.CLKPOL = SPI_CLKPOL_HIGH;
    // 时钟相位:第二个边沿采样(MT6835要求)
    SPI_InitStructure.CLKPHA = SPI_CLKPHA_SECOND_EDGE;
    // 片选控制:软件控制(通过GPIO手动拉低/拉高NSS)
    SPI_InitStructure.NSS = SPI_NSS_SOFT;
    // 波特率预分频:16分频(波特率=144MHz/16=9MHz,满足MT6835要求)
    SPI_InitStructure.BaudRatePres = SPI_BR_PRESCALER_16;//SPI_BR_PRESCALER_256;
    // 数据传输顺序:高位先传(符合MT6835规范)
    SPI_InitStructure.FirstBit = SPI_FB_MSB;
    // CRC校验多项式:7(启用CRC,减少传输错误)
    SPI_InitStructure.CRCPoly = 7;

    // -------------------------- 3. 配置SPI GPIO引脚(SCK、MOSI、MISO、NSS) --------------------------
    SPI_Init(SPI3, &SPI_InitStructure);

    // --------------------------  4. 使能SPI接收中断(收到编码器数据后触发中断处理) --------------------------
    SPI_I2S_EnableInt(SPI3, SPI_I2S_INT_RNE, ENABLE); //使能接收中断

    // --------------------------  5. 使能SPI3外设(启动通信功能) --------------------------
    SPI_Enable(SPI3, ENABLE);
}

6.1.8 NVIC

NVIC(嵌套向量中断控制器)是芯片内部管理所有中断的指挥中心。在本项目中,外设(如 SPI、USART、ADC)会随时产生中断请求(比如 “收到数据了”“采样完成了”),NVIC 的作用就是决定哪个中断先处理、哪个后处理,避免多个中断争夺, 导致系统混乱。

简单说,NVIC 就是给不同的中断分配 “优先级”,优先级高的先通行,优先级低的排队等,确保系统高效响应关键任务(比如 FOC 控制中的电流环中断,必须比通信中断先处理)。前面的初始化代码中大家可能已经看到了 NVIC 的配置,有两个概念非常重要:抢占优先级和子优先级。

(1)抢占优先级:相当于病情紧急程度(如 “抢救”>“普通外伤”)。优先级高的中断可以打断正在处理的低优先级中断(比如电流环中断来了,能暂停通信中断的处理)。

(2)子优先级:相当于同紧急程度下的排队顺序(如两个 “普通外伤” 病人,先到先处理)。子优先级高的不能打断同级抢占优先级的中断,只能等前一个处理完再上。

学习完这些,下面进入项目中的实际应用,看看 NVIC 如何管理这些中断请求,确保系统高效运行。如下图所示:

system

下面我们来详细了解一下 NVIC 的配置方法。NVIC的初始化结构体 NVIC_InitType 包含了中断配置的所有参数,和前面的DMA,SPI等等是一样的道理,下面是本项目中 NVIC 的配置参数解析:

参数成员 作用说明 本项目配置及原因
NVIC_IRQChannel 指定要配置的中断源(对应哪个外设的中断,如SPI、ADC、USART等),是中断的身份标识。 例如SPI3_IRQn(SPI3中断)、ADC1_2_IRQn(ADC1/2中断):精准定位需要管理的中断,确保配置只作用于目标外设。
NVIC_IRQChannelPreemptionPriority 抢占优先级(“大组”优先级):数值越小,优先级越高,可打断正在执行的低抢占优先级中断。 例如SPI3中断设为2:高于Modbus通信中断,保证编码器角度数据优先处理;低于电流环定时器中断,确保控制逻辑不被干扰。
NVIC_IRQChannelSubPriority 子优先级(“小组”优先级):抢占优先级相同时,数值越小越先响应(不能打断,仅决定排队顺序)。 例如SPI3中断设为1:在抢占优先级为2的中断中(如ADC采样中断),比子优先级为2的中断先处理,保证数据同步性。
NVIC_IRQChannelCmd 控制中断通道的开关(ENABLE开启/Disable关闭),决定是否响应该中断请求。 均为ENABLE(使能):需要处理的中断(如编码器数据接收、电流采样完成)必须开启,否则外设请求会被忽略。

代码在这里就不过多赘述了,前面的ADC、SPI、DMA等初始化都配置了对应的NVIC,可以参考前面的代码。

6.2 控制框架代码详解

system

本项目控制程序流程如上图所示,通过将定时器提供的10kHz中断分频后以1kHz运行上层算法,在上层算法中选择不同的模式运行,最终输出电压空间矢量,控制电机运行。

6.2.1 FOC控制流程

  • ① 对电机两相电流进行采样得到:ia、ib;
  • ② 对 iα、iβ 进行 Park 变换,得到 dq 坐标系下的电流分量:id、iq;
  • ③ 计算 id、iq 与设定目标值id_ref、iq_ref的误差;
  • ④ 将误差输入 PI 控制器计算出 id、iq 的控制输出:ud、uq;
  • ⑤ 对 ud、uq 进行反 Park 变换,得到 αβ 坐标系下的电压分量:uα、uβ;
  • ⑥ 将 uα、uβ 输入 SVPWM 模块进行调制,合成电压空间矢量,输出PWM信号进而控制电机旋转;
  • ⑦ 更新电机转子位置角度,重复上述流程。

6.2.2 PARK变换与反PARK变换

park变换与反park变换程序与公式基本一致,只是有些提高计算效率和计算精度的小技巧,程序中先把cos,sin值计算出来,避免重复计算,并且使用Q15格式计算,提高计算精度和计算效率。

Q15,可以简单理解为将浮点数乘以2^15转换为定点数计算。

void park(SVPVM *v) 
{
    short cos_theta = _IQ15cosPU(v->Angle);  // 获取转子角度的余弦值(Q15格式)
    short sin_theta = _IQ15sinPU(v->Angle);  // 获取转子角度的正弦值(Q15格式)

    // 计算dq轴电流(使用IQ乘法并累加)
    v->IDs = _IQ15mpy(v->As, cos_theta) + _IQ15mpy(v->Bs, sin_theta); // id
    v->IQs = _IQ15mpy(v->Bs, cos_theta) - _IQ15mpy(v->As, sin_theta); // iq
}
void ipark(SVPVM *v)            //反park变换
{
    short Cosine, Sine;
    short tmpAngle;

    tmpAngle = v->Angle; //0--32767   --- >> 65536 代表着两圈   65536/2  =32768 

    // 获取转子角度的正弦、余弦值
    Sine     = _IQ15sinPU(tmpAngle);    
    Cosine     = _IQ15cosPU(tmpAngle);    

    //执行反park变换
    v->Ualpha = _IQ15mpy(tmpDs, Cosine) - _IQ15mpy(tmpQs, Sine);
    v->Ubeta  = _IQ15mpy(tmpQs, Cosine) + _IQ15mpy(tmpDs, Sine);
}

6.2.3 SVPWM

二相混合式步进电机svpwm这部分是非常简单的,计算公式可以参考上文的理论推导部分的计算公式,主要要注意的部分是数据格式,详见代码注释

void PWM(SVPVM *v)
{
    short MPeriod;  // 实际使用的定时器周期(转换后的Q0格式)
    int Tmp;        // 临时变量,用于中间计算(防止溢出)

    // 将α-β坐标系下的电压分量赋值给中间变量,作为PWM计算的输入
    // Ubeta和Ualpha为short类型(范围-32768~32767),对应归一化的电压值
    v->MfuncC1 = v->Ubeta;  // MfuncC1用于计算V相PWM比较值,对应Ubeta电压分量
    v->MfuncC2 = v->Ualpha; // MfuncC2用于计算U相PWM比较值,对应Ualpha电压分量


    // 计算定时器周期(MPeriod):将调制周期从Q15格式转换为实际周期值(Q0格式)
    // v->PeriodMax:定时器最大周期(如4499,Q0格式)
    // v->MfuncPeriod:周期调制系数(Q15格式,范围-1~1,此处用于动态调整周期)
    Tmp = (int)v->PeriodMax * (int)v->MfuncPeriod;  // 乘法:Q0 * Q15 = Q15(结果为放大后的周期值)
    // 转换为Q0格式:右移16位(Q15转Q0的量化处理)+ 周期最大值的一半(偏移量,确保中心对齐)
    MPeriod = (short)(Tmp >> 16) + (short)(v->PeriodMax >> 1);  // 最终周期值(如4499的中心对齐周期)


    // 计算U相PWM比较值(Va):根据Ubeta电压分量生成
    // MPeriod:当前定时器周期(Q0格式)
    // v->MfuncC1(Ubeta):Q15格式电压分量(-32768~32767对应-24V~24V)
    Tmp = (int)MPeriod * (int)v->MfuncC1;  // 乘法:Q0 * Q15 = Q15(电压分量与周期的乘积)

    // 转换为PWM比较寄存器值:右移16位(Q15转Q0)+ 周期的一半(中心对齐偏移)
    // 结果对应PWM占空比,实现Ubeta电压分量的输出
    v->Va = (short)(Tmp >> 16) + (short)(MPeriod >> 1);  


    // 计算V相PWM比较值(Vb):根据Ualpha电压分量生成
    // 原理同U相,对应Ualpha电压分量的转换
    Tmp = (int)MPeriod * (int)v->MfuncC2;  // 乘法:Q0 * Q15 = Q15(电压分量与周期的乘积)
    v->Vb = (short)(Tmp >> 16) + (short)(MPeriod >> 1);   // 转换为PWM比较值,实现Ualpha电压分量的输出

}

在理想的SVPWM中互补的PWM完全对称,一路高电平另一路立刻进入低电平,但是在实际的pwm中图中红框部分肯出现高电平的重叠,也就是H桥的四个开关全部导通,这种重叠现象可能导致电机驱动器中的晶体管(例如功率开关)损坏,同时也会降低电机的效率。

system

所以在pwm生成中往往会加入死区时间,我们可以通过在软件上调整占空比来实现这个死区时间 system

6.2.4 PI控制器

各部分的PI控制器大致一样这里只写出q轴的PI控制器。更详细的解释在注释中

void pidIqs_calc(PIDIqs *v)
{
    //计算电流误差和比例项的输出值
    v->Err = v->Ref - v->Fdb;
    v->Up = v->Kp * v->Err;
    //积分项累加,输出饱和时积分项不再累加
        if((v->OutPreSat > v->OutMax&&v->Err>0)||(v->OutPreSat < v->OutMin&&v->Err<0))
    { }else
    {
        temp_Ui += v->Err ; 
    }
    //防止反转
    if((pidpv.Ref >= 0)&&MotorControler.SpeedFdbp < 0 )  //当期望转速为正,实际转速为负值时清零输出
    {
      //  v->Ref = v->CurrentRef;
        if(temp_Ui < 0)
        {
            temp_Ui = 0;
        }
        if(v->Up < 0)
        {
           v->Up = 0;
        }
    }
    else if((pidpv.Ref < 0)&&MotorControler.SpeedFdbp > 0)  // 反向
    {
   //    v->Ref = 0 - v->CurrentRef;

        if(temp_Ui > 0)
        {
            temp_Ui = 0;
        }

        if(v->Up > 0)
        {
            v->Up = 0;
        }
    }
        v->Ui=((temp_Ui) >> 15);

    //合成比例项与积分项
    v->OutPreSat = v->Up +v->Ki * v->Ui;  //缩小积分项影响

    //积分限幅
    if(v->OutPreSat > v->OutMax)
    {
        v->Out =  v->OutMax;
    }
    else if(v->OutPreSat < v->OutMin)
    {
        v->Out =  v->OutMin;
    }
    else
    {
        v->Out = v->OutPreSat;
    }

}

6.2.5 位置环速度规划

位置环主要由四个部分组成:曲线规划、期望参数的计算、位置PI控制器、SVPWM。

system

本项目的位置环的规划采用s型曲线规划,该规划有七个阶段加加速阶段,匀加速阶段,减加速阶段,匀速阶段,减减速阶段,匀减速阶段,加减速阶段

system

s型曲线规划由以下函数完成,输入期望位置,加加速度,最大加速度与匀速阶段的速度,输出曲线上各个阶段的时间,位置与速度。

enum SP_Error Set_SpeedPlant_Para(Set_SP_Para *ptr)
{
    //检查输入数据合法性
    enum SP_Error sp_error = none_err;

    if(ptr->accel_max < 0)
    {
        sp_error = accel_max_neg_err;
    }

    if(ptr->decel_max < 0)
    {
        sp_error = decel_max_neg_err;
    }

    if(ptr->a_accel < 0)
    {
        sp_error = a_accel_neg_err;
    }

    if(ptr->a_decel < 0)
    {
        sp_error = a_decel_neg_err;
    }

    if(fabsf(ptr->vel_tar) < 1.0f) //定点
    {
        sp_error = vel_tar_zero_err;
    }

    //定义指针,避免从ptr开始寻址,简化
    SpeedPlant *sp = ptr->sp;

    //倒转判断
    if(ptr->end_position < ptr->start_position)
    {
        float tem_value = 0;
        sp->forward_flag = -1;
        tem_value = ptr->end_position;
        ptr->end_position = ptr->start_position;
        ptr->start_position = tem_value;
        ptr->vel_init = -ptr->vel_init;
        ptr->vel_tar = -fabsf(ptr->vel_tar); //目标转动方向一定要跟目标速度同号
    }
    else
    {
        sp->forward_flag = 1;
        ptr->vel_tar = fabsf(ptr->vel_tar);
    }

    //速度方向判断
    sp->vel_flag = 1;
    if((ptr->vel_tar >= 0) && (ptr->vel_init >= ptr->vel_tar)) //Vo>=Vm>=0
    {
        sp->vel_flag = -1;
        ptr->a_accel = ptr->a_decel; //此时需要减速,采用减速部分的参数
        ptr->accel_max = ptr->decel_max;
    }
    else if(ptr->vel_tar < 0) //Vm<0
    {
        if((-1.0f * ptr->vel_init) <= ptr->vel_tar) //Vo<Vm<0
        {
            sp->vel_flag = -1;
            ptr->a_decel = ptr->a_accel; //此时需要加速,采用加速部分的参数
            ptr->decel_max = ptr->accel_max;
        }
        ptr->vel_tar = -ptr->vel_tar; //取反,倒转时所有速度取反,起始和结束位置对调
    }

    float accel_max_tem = ptr->accel_max; //存储最大加速度,用于后面最大加速度判断

    if(sp_error == none_err)
    {
        //这样可以将情况整合在一起,所有情况都能到达最高加速度,只是这种情况下的t2=t1
        if(fabsf(ptr->vel_tar - ptr->vel_init) <= (powf(accel_max_tem, 2) / ptr->a_accel))
        {
            //设置的最大速度较小。很快到达,前半段没有匀加速度阶段,将最大加速度重置为临界加速度
            ptr->accel_max = sqrtf(fabsf((ptr->vel_tar - ptr->vel_init)) * ptr->a_accel);
        }

        if(fabsf(ptr->vel_tar) <= (powf(ptr->decel_max, 2) / ptr->a_decel)) //与上面同理
        {
            ptr->decel_max = sqrtf(fabsf(ptr->vel_tar) * ptr->a_decel);
        }

        //sp->s3=(ptr->vel_tar+ptr->vel_init)*(ptr->accel_max/ptr->a_accel+fabsf(ptr->vel_tar-ptr->vel_init)/ptr->accel_max)/2;
        sp->s7 = (ptr->vel_tar + ptr->vel_init) * (ptr->accel_max / ptr->a_accel + fabsf(ptr->vel_tar - ptr->vel_init) / ptr->accel_max) / 2 \
                 +ptr->vel_tar * (ptr->decel_max / ptr->a_decel + ptr->vel_tar / ptr->decel_max) / 2;

        //目标位置不足以走完没有匀速阶段的全程
        while((ptr->end_position - ptr->start_position) < sp->s7)
        {
            //如果目标速度与初始速度相同时仍不能实现曲线,则降低目标速度也不能实现,提示出错
            if(fabsf(ptr->vel_tar - ptr->vel_init) < 1e-6f)
            {
                sp_error = distance_too_small_err;
                break;
            }

            ptr->vel_tar = ptr->vel_tar * 0.95f; //降低目标速度

            //如果目标速度过低,认为曲线无法实现
            if(ptr->vel_tar < 1.0f)  //由于速度是定点,至少为1
            {
                sp_error = distance_too_small_err;
                break;
            }

            //如果目标速度降低后由大于初始速度变成小于初始速度,需要将速度标志设为-1
            if((ptr->vel_init > ptr->vel_tar) && (sp->vel_flag == 1))
            {
                sp->vel_flag = -1;
            }

            //修改最大加速度和最大减速度
            if(fabsf(ptr->vel_tar - ptr->vel_init) <= (powf(accel_max_tem, 2) / ptr->a_accel))
            {
                ptr->accel_max = sqrtf(fabsf((ptr->vel_tar - ptr->vel_init)) * ptr->a_accel);
            }

            if(fabsf(ptr->vel_tar) <= (powf(ptr->decel_max, 2) / ptr->a_decel))
            {
                ptr->decel_max = sqrtf(fabsf(ptr->vel_tar) * ptr->a_decel);
            }

            //重新计算总位移
            sp->s7 = (ptr->vel_tar + ptr->vel_init) * (ptr->accel_max / ptr->a_accel + fabsf(ptr->vel_tar - ptr->vel_init) / ptr->accel_max) / 2 \
                     +ptr->vel_tar * (ptr->decel_max / ptr->a_decel + ptr->vel_tar / ptr->decel_max) / 2;
        }

        sp->s7 = sp->s7 + ptr->start_position;

       // ------------------------- 1. 计算加速阶段的时间参数 -------------------------
       // accel_aaccel:加加速时间(加速度从0→max_accel的时间,由加加速度a_accel决定:a_accel = Δa/Δt → Δt = max_accel/a_accel)
        float accel_aaccel = ptr->accel_max / ptr->a_accel;
        float vel_accel;
        if(ptr->accel_max < 1e-6f)
        {  // 若最大加速度趋近于0,加速阶段无匀加速,总时间等于加加速时间
            vel_accel = accel_aaccel;
        }
        else
        {   // 否则,匀加速时间 = 速度变化量 / 最大加速度(速度从init_vel→tar_vel)
            vel_accel = fabsf((ptr->vel_tar - ptr->vel_init)) / ptr->accel_max;
        }

        // ------------------------- 2. 计算减速阶段的时间参数 -------------------------
        // decel_adecel:减减速时间(减速度从max_decel→0的时间,由减减速度a_decel决定:a_decel = Δa/Δt → Δt = max_decel/a_decel)
        float decel_adecel = ptr->decel_max / ptr->a_decel;
        float vel_decel;

        if(ptr->decel_max < 1e-6f)
        {   // 若最大减速度趋近于0,减速阶段无匀减速,总时间等于减减速时间
            vel_decel = decel_adecel;
        }
        else
        {   // 否则,匀减速时间 = 匀速速度 / 最大减速度(速度从tar_vel→0,假设最终速度为0)
            vel_decel = ptr->vel_tar / ptr->decel_max;
        }

         // ------------------------- 3. 计算阶段内的时间差 -------------------------
        float t2_t1 = vel_accel - accel_aaccel;   // 匀加速阶段持续时间(t2 - t1)
        float t6_t5 = vel_decel - decel_adecel;   // 匀减速阶段持续时间(t6 - t5)

        // ------------------------- 4. 计算各阶段的结束时间点(t1~t7) -------------------------
        sp->t1 = accel_aaccel;                                            // t1:加加速阶段结束(加速度↑阶段)
        sp->t2 = vel_accel;                                               // t2:匀加速阶段结束(加速度恒定阶段)
        sp->t3 = vel_accel + accel_aaccel;                                // t3:加减速阶段结束(加速度↓阶段,进入匀速)
        sp->t4 = sp->t3 + (ptr->end_position - sp->s7) / ptr->vel_tar;    // t4:匀速阶段结束时间 = t3 + 匀速持续时间(匀速位移 = 总位移 - 加速位移 - 减速位移
        sp->t5 = sp->t4 + decel_adecel;                                   // t5:减加速阶段结束(减速度↑阶段,速度开始↓)
        sp->t6 = sp->t4 + vel_decel;                                      // t6:匀减速阶段结束(减速度恒定阶段)
        sp->t7 = sp->t6 + decel_adecel;                                   // t7:减减速阶段结束(减速度↓阶段,运动停止)

        // ------------------------- 5. 计算各阶段的结束速度(v1~v6) -------------------------
        // v1:加加速阶段末速度(加速度a(t)=a_accel*t,速度是加速度的积分:∫0~t1 a(t)dt = 0.5*a_accel*t1²,叠加初始速度)
        sp->v1 = ptr->vel_init + 0.5f * sp->vel_flag * ptr->a_accel * powf(accel_aaccel, 2);  
        // v2:匀加速阶段末速度(v1 + 匀加速阶段速度变化:max_accel * t2_t1)
        sp->v2 = sp->v1 + ptr->accel_max * sp->vel_flag * t2_t1;  
        sp->v3 = ptr->vel_tar;                     // v3:匀速阶段速度(恒定)
        sp->v4 = sp->v3;                           // v4:同v3(匀速阶段速度不变)
        // v5:减加速阶段末速度(v4 - 减加速阶段速度变化:0.5*a_decel*t5²,负号表示减速)
        sp->v5 = sp->v4 - 0.5f * ptr->a_decel * sp->vel_flag * powf(decel_adecel, 2);  
        // v6:匀减速阶段末速度(v5 - 匀减速阶段速度变化:max_decel * t6_t5)
        sp->v6 = sp->v5 - ptr->decel_max * sp->vel_flag * t6_t5;  


       // ------------------------- 6. 计算各阶段的结束位置(s1~s7) -------------------------
       // s1:加加速阶段末位置(初始位置 + 初始速度*t1 + 加加速度的位移积分:∫0~t1 ∫0~t v(t)dt = (1/6)*a_accel*t1³)
        sp->s1 = ptr->start_position + ptr->vel_init * accel_aaccel + 1.0f / 6.0f * sp->vel_flag * ptr->a_accel * powf(accel_aaccel, 3);  
       // s2:匀加速阶段末位置(s1 + v1*t2_t1 + 匀加速位移:0.5*max_accel*t2_t1²)
       sp->s2 = sp->s1 + sp->v1 * t2_t1 + 0.5f * ptr->accel_max * sp->vel_flag * powf(t2_t1, 2);  
       // s3:加减速阶段末位置(s2 + v2*accel_aaccel + 加减速位移:0.5*max_accel*accel_aaccel² - (1/6)*a_accel*accel_aaccel³,加速度从max_accel降到0)
       sp->s3 = sp->s2 + sp->v2 * accel_aaccel + 0.5f * ptr->accel_max * sp->vel_flag * powf(accel_aaccel, 2) - 1.0f / 6.0f * ptr->a_accel * sp->vel_flag * powf(accel_aaccel, 3);  
       // s4:匀速阶段末位置(s3 + v3*(t4 - t3),匀速位移=速度×时间差)
       sp->s4 = sp->s3 + sp->v3 * (sp->t4 - sp->t3);  
       // s5:减加速阶段末位置(s4 + v4*decel_adecel - 减加速位移:(1/6)*a_decel*decel_adecel³,负号表示减速)
       sp->s5 = sp->s4 + sp->v4 * decel_adecel - 1.0f / 6.0f * ptr->a_decel * sp->vel_flag * powf(decel_adecel, 3);  
       // s6:匀减速阶段末位置(s5 + v5*t6_t5 - 匀减速位移:0.5*max_decel*t6_t5²,负号表示减速)
       sp->s6 = sp->s5 + sp->v5 * t6_t5 - 0.5f * ptr->decel_max * sp->vel_flag * powf(t6_t5, 2);  
       sp->s7 = ptr->end_position;                // s7:最终位置(减速阶段结束,到达目标位置)
    }

    return sp_error;
}

以下函数负责在各个阶段运行中输出期望位置与速度,这两个数据会传入到位置环的PID控制器

Current_value SpeedPlant_positionControl(Set_SP_Para* ptr, float t)
{
    Current_value current_value;
    SpeedPlant* sp = ptr->sp;

    if(t <= 0)//运行之前
    {
        current_value.accel = 0;
        current_value.vel = ptr->vel_init;
        current_value.position = ptr->start_position;
    }
    else if(t <= sp->t1)//0~t1的阶段时  加加速度阶段
    {
        float t_pow = powf(t, 2);
        current_value.accel = sp->vel_flag * ptr->a_accel * t;//当前加速度=方向*加加速度*时间 
        current_value.vel = ptr->vel_init + 0.5 * sp->vel_flag * ptr->a_accel * t_pow;//当前速度=初速度+0.5速度方向*加速度*时间平方 
        current_value.position = ptr->start_position + 1.0 / 6 * sp->vel_flag * ptr->a_accel * t_pow * t + ptr->vel_init * t;//当前位置=初始位置+1/6*速度方向*加加速度*时间3次方+初速度*时间
    }
    else if(t <= sp->t2)//t1~t2阶段时  匀加速度阶段
    {
        t = t - sp->t1;//从t1开始重新计时
        current_value.accel = sp->vel_flag * ptr->accel_max;//当前加速度=速度方向*最大加速度
        current_value.vel = sp->v1 + current_value.accel * t;//当前速度=速度+加速度*时间
        current_value.position = sp->s1 + sp->v1 * t + 0.5 * sp->vel_flag * ptr->accel_max * powf(t, 2);//位置+速度*时间+0.5*速度方向*最大加速度*时间平方
    }
    else if(t <= sp->t3)//t2~t3阶段时  减加速度阶段
    {
        t = t - sp->t2;//从t2开始重新计时
        float t_pow = powf(t, 2);
        current_value.accel = sp->vel_flag * (ptr->accel_max - ptr->a_accel * t);//当前加速度=速度方向*(最大加速度-加加速度*时间)
        current_value.vel = sp->v3 - 0.5f * ptr->a_accel * sp->vel_flag * powf(sp->t1 - t, 2);//当前速度=初速度-0.5*速度方向*加加速度(负数)*(t1-t)的平方,t1~0与t2~t3长度相同,t到达t3时,加速度归零
        current_value.position = sp->s2 + sp->v2 * t + 0.5f * ptr->accel_max * sp->vel_flag * t_pow - 1.0f / 6.0f * ptr->a_accel * sp->vel_flag * t_pow * t;//当前位置+初速度*时间+0.5*最大加速度*方向*时间的平方-1/6*加加速度*方向*t的三次方
    }
    else if(t <= sp->t4)//匀速阶段
    {
        current_value.accel = 0;
        current_value.vel = sp->v3;
        current_value.position = sp->s3 + sp->v3 * (t - sp->t3);
    }
    else if(t <= sp->t5)//减减速阶段
    {
        t = t - sp->t4;
        float t_pow = powf(t, 2);
        current_value.accel = -ptr->a_decel * t;
        current_value.vel = sp->v4 - 0.5f * ptr->a_decel * t_pow;
        current_value.position = sp->s4 + sp->v4 * t - 1.0f / 6.0f * ptr->a_decel * t_pow * t;
    }
    else if(t <= sp->t6)//匀减速阶段
    {
        t = t - sp->t5;
        current_value.accel = -ptr->decel_max;
        current_value.vel = sp->v5 - ptr->decel_max * t;
        current_value.position = sp->s5 + sp->v5 * t - 0.5f * ptr->decel_max * powf(t, 2);
    }
    else if(t <= sp->t7)//加减速阶段
    {
        t = t - sp->t6;
        float t_pow = powf(t, 2);
        current_value.accel = ptr->a_decel * t - ptr->decel_max;
        current_value.vel = sp->v6 - ptr->decel_max * t + 0.5f * ptr->a_decel * t_pow;
        current_value.position = sp->s6 + sp->v6 * t - 0.5f * ptr->decel_max * t_pow + 1.0f / 6.0f * ptr->a_decel * t_pow * t;
    }
    else if(t > sp->t7)//停止阶段
    {
        current_value.accel = 0;
        current_value.vel = 0;
        current_value.position = ptr->end_position;
    }

    if(sp->forward_flag == -1)
    {
        current_value.accel = -current_value.accel;
        current_value.vel = -current_value.vel;
        current_value.position = ptr->end_position + ptr->start_position - current_value.position;
    }

    return current_value;
}

以下为位置环控制器主要接收位置数据与速度数据,位置数据计算主要输出,速度数据作为辅助。控制器输出的q轴电压与角度结合经过反park变换后,输入SVPWM里输出PWM波


void pidposition_calc(PIDpos *v)
{
    v->Err = v->Ref - v->Fdb;
    v->Up = ((int)v->Kp * ((int)v->Err) >> 4);

    if(v->Up > 2500)  //防止抖动时,PD 产生极大电压,造成MOS 损坏。
    {
        v->Up = 2500 ;
    }
    else if(v->Up < -2500)
    {
        v->Up = -2500;
    }

    v->UiMax = 3000 * 32768;
    v->UiMin = -3000 * 32768;

    v->Ui =  v->Ui + (v->Ki * ((v->Err) >> 4));

    if(v->Ui > v->UiMax)
    {
        v->Ui = v->UiMax;
    }

    if(v->Ui < v->UiMin)
    {
        v->Ui = v->UiMin;
    }

    if(v->Speedref > 0)
        tempspeeda = 750;

    else if(v->Speedref < 0)
        tempspeeda = -750;

    else tempspeeda = 0;

    //6.5+665
    //7.5 +750
    tempspeedb = ((v->Speedref * 15) >> 1) + tempspeeda;

    v->OutPreSat = v->Up + ((v->Ui) >> 15) + tempspeedb;

    if(v->OutPreSat > v->OutMax)
    {
        v->Out =  v->OutMax;
    }
    else if(v->OutPreSat < v->OutMin)
    {
        v->Out =  v->OutMin;
    }
    else
    {
        v->Out = v->OutPreSat;
    }

}

6.2.6 开环VF控制器

开环控制器代码如下,VF_ElectricalAngleStep即每次自增的角度值,该值由期望速度给出,电压值与速度值的关系根据经验公式确定。 其中,SystemVar.VF_Coefficient = 13,SystemVar.VF_Coefficient_B = 1350。

 case 1 :         //开环运行
 {
    svpwm.SvpwmControlState = 1;

    SystemVar.VF_ElectricalAngleStepCount++;

if(SystemVar.VF_ElectricalAngleStepCount >= 100)
{
    SystemVar.VF_ElectricalAngleStepCount = 0;
//判断运行方向
    if((SystemVar.VF_ElectricalAngleStep < SystemVar.VF_ElectricalAngleStepMax) && (SystemVar.VF_ElectricalAngleStepMin == 0))
        {
            SystemVar.VF_ElectricalAngleStep = SystemVar.VF_ElectricalAngleStep + 1;
            VF_Dir = 1;
        }

    if((SystemVar.VF_ElectricalAngleStep > SystemVar.VF_ElectricalAngleStepMin) && (SystemVar.VF_ElectricalAngleStepMax == 0))
        {
            SystemVar.VF_ElectricalAngleStep = SystemVar.VF_ElectricalAngleStep - 1;
            VF_Dir = -1;
        }
 //停止
    if((SystemVar.VF_ElectricalAngleStepMax != 0) && (SystemVar.VF_ElectricalAngleStepMin != 0))
        {
            SystemVar.VF_ElectricalAngleStep = 0;
            SystemVar.VF_ElectricalAngle = 0;
            VF_Dir = 0;
            SystemVar.VF_Voltage = 0;
        }

    if((SystemVar.VF_ElectricalAngleStepMax == 0) && (SystemVar.VF_ElectricalAngleStepMin == 0))
        {
            SystemVar.VF_ElectricalAngleStep = 0;
            SystemVar.VF_ElectricalAngle = 0;
            VF_Dir = 0;
            SystemVar.VF_Voltage = 0;
        }
        //根据实际效果,增加限幅工作。
}
    //电角度自增
    SystemVar.VF_ElectricalAngle = (SystemVar.VF_ElectricalAngle + ((short)((((float)SystemVar.VF_ElectricalAngleStep)) * 27.31f))) & 0x7FFF;
    //根据经验公式给出电压
    SystemVar.VF_Voltage = (SystemVar.VF_ElectricalAngleStep >> 1) * (SystemVar.VF_Coefficient * 10) + ((SystemVar.VF_Coefficient_B - 500) * VF_Dir);
}
break;

6.3 故障检测代码详解

6.3.1 过速

过速检测流程图如下: system

完整代码实现:

void SpeedAnalyse(void)
{
    if(Abs(MotorControler.SpeedFdbp) > MotorControler.MaxSpeed)
    {   //反馈速度超过设定的最大速度
        SystemError.OverSpeedTimer++;//时间窗口累加
        if(SystemError.OverSpeedTimer >= (SystemError.OverSpeed))
        {  //时间窗口大于等于允许过速度的时间
            SystemError.OverSpeedTimer = SystemError.OverSpeed ;
            //时间窗口锁定,防止溢出。
            SystemError.SysErr = M_SYSERR_OVER_SPEED; //过速度标志
        }
    }
    else
    {
        SystemError.OverSpeedTimer = 0;// 时间窗口清零
    }
}

上述代码通过检测转速和电流,实时监控电机的运行状态,避免过速度引发的系统故障,保障电机和驱动电路的安全与可靠

6.3.2 过载与堵转

过载与堵转分析流程框图如下: system

过载与堵转分析代码如下:

 void TorsionAnalyse(void)
{
    // 检查当前扭矩是否超过额定值
    if(Abs(svpwm.IQs) > MotorControler.RatedTorque) 
    {
        // 扭矩超限时累计过载计时器
        SystemError.OverLoadTimer++;
       // 转速低于阈值(3RPM)
        if(Abs(MotorControler.SpeedFdbp) < 3) // 堵转条件
        {
            // 堵转超时保护
            if(SystemError.OverLoadTimer >= (SystemError.IqsMaxOverTime * 10))
            {
                SystemError.OverLoadTimer = SystemError.IqsMaxOverTime * 10; // 防溢出处理
                SystemError.SysErr = M_SYSERR_ROTOR_LOCKED ; // 设置堵转故障码
            }
        }
        else // 过载状态
        {
            // 过载超时保护
            if(SystemError.OverLoadTimer >= (SystemError.IqsMaxOverTime * 20))
            {
                SystemError.OverLoadTimer = SystemError.IqsMaxOverTime * 20; // 防溢出处理
                SystemError.SysErr = M_SYSERR_OVER_LOAD; // 设置过载故障码
            }
        }
    }
    else 
    {
        // 扭矩恢复正常时重置计时器
        SystemError.OverLoadTimer = 0; 
    }
}

通过比较q轴电流绝对值与额定转矩的大小,从而判断出电流是否超出额定值,若 Abs(svpwm.IQs) > MotorControler.RatedTorque则说明电流超出额定值。

if(Abs(svpwm.IQs) > MotorControler.RatedTorque) ,此时使SystemError.OverLoadTimer++,计算电流超限的时间,当电流没有超限时将其清零。

此代码通过Abs(MotorControler.SpeedFdbp) < 3,比较转速绝对值和3的大小,再结合电流超限从而可以判定为堵转的情形。

若此时为堵转,检查SystemError.OverLoadTimer是否大于10倍SystemError.IqsMaxOverTime,若大于则标志出堵转的标签。

若为过载的情况,则检查SystemError.OverLoadTimer是否大于20倍SystemError.IqsMaxOverTime,若大于则打上过载的标签。

6.3.3 编码器异常

编码器检查主要依靠MT6835磁编码器的状态寄存器数据,如果MT6835编码器有报错则不读取角度值,角度值保持初始值-19999,循环内保持初始值一段时间后就认为编码器异常。


void LostCoder(void)
{
 static int count = 0;
    if(SystemVar.AngleFromMT6835Offset1 == 0) 
    {
        SystemError.SysErr = M_SYSERR_CODER;
    }
    if(SystemVar.AngleFromMT6835 == -19999)    
    {
        count++;        //编码器异常计数
        if(count >= 80)
        {
            SystemError.SysErr = M_SYSERR_CODERNESS;     //编码器异常,报警
            count = 0;  //重置计数器
        }
    }
}

7 应用实例

7.1 串口调试

首先介绍一种较为简单的调试方法,串口调试。

读者需要下载串口通信软件,常用的有XCOM,SSCOM,串口调试助手等,首先通过rs485转usb转接口与电机连接,并给电机通电,具体的接线方式可以参照上文RS485章节的接线图。接线完成后,打开电脑的设备管理器确认电脑是否识别到转接设备。若未识别则要检查驱动是否安装设备是否损坏。

system

下一步打开任意串口调试软件,这里以SSCOM为例,端口号选择设备管理器识别到的端口号,波特率选择115200,点击打开串口,发送选择hex发送(发送16进制)其他具体设置如下图。 system

读者按照30rpm,速度模式选择,使能,开始的顺序发送命令,电机就能旋转起来。若电机旋转速度误差过大可以使用校准模式校准磁编码器,若电机已经在运行需要先下使能,之后按照校准模式,使能,开始的顺序发送命令就可启动校准模式。

串口通信命令表 | 指令功能 | 指令数据(十六进制) | | :------: | :--------------------------------------------------: | | 30rpm | 01 10 01 32 00 02 04 00 00 75 30 5A 76 | | 速度模式选择 | 01 10 01 07 00 01 02 00 02 36 E6 | | 校准模式 | 01 10 01 07 00 01 02 00 05 77 24 | | 零点辨识 | 01 10 01 07 00 01 02 00 04 B6 E4 | | 使能 | 01 10 01 38 00 01 02 00 01 73 E8 | | 开始 | 01 10 01 3B 00 01 02 00 01 73 DB | | 下使能 | 01 10 01 38 00 01 02 00 02 33 E9 |

7.2 上位机调试

上位机软件使用方法与串口调试类似,读者需要下载上位机软件,打开后选择对应的串口号,波特率选择115200,点击连接按钮连接电机。连接成功后可以在上位机软件的界面上看到电机的实时状态数据。

  • 第一步:打开上位机 system
  • 第二步:选择modbus通信协议,选择正确的串口号,波特率选择115200(会有记忆功能),点击连接串口按钮 system
  • 第三步:连接成功后可以看到电机的实时状态数据,母线电压与电流,随后选择需要运行的模式,设置参数,使能,启动。 system
  • 第四步:可以在上位机软件的界面上看到电机的实时状态数据,母线电压与电流,转速,位置等数据,可以通过信号示波采集需要的数据进行分析。 system
  • 注意事项:
    • 模式之间切换需要先下使能,之后再上使能,才能切换成功。
    • 电机运行不准时,可以使用校准模式校准磁编码器,校准完成后需要下使能,之后再上使能才能生效。 system
    • 零点辨识功能可以用于电机的零点校准,校准完成后需要下使能,之后再上使能才能生效。

7.3 基于42步进电机的直线模组控制

直线模组控制,简单说就是对直线模组的运动状态进行精准调控,让它能在特定轨迹上平稳、准确地移动。​

system

如上图所示,本项目采用的直线模组由电机、导轨、滑块、滚珠丝杠和刻度尺等部件组成。它的核心工作原理是通过电机驱动传动机构,把电机的旋转运动转化成滑块的直线运动。整个系统连接上位机后,可对电机的位置进行调节,通过对比电机给定的移动位置和滑块实际停止的位置,就能检测出电机位置控制的精度。​

在项目中,将 42 步进电机固定在直线模组上,通过联轴器与滚珠丝杆连接。电机位置转动具体的对应关系要说明:电机位置变化 65536,对应电机转动两圈。滚珠丝杆转动一周,滑块移动的距离是 2mm。工作时,通过上位机发送指令,控制电机转动,滑块就能准确移动到指定位置。​

经过实际检测,这个项目实现了直线模组的精准位置控制,定位误差≤0.1mm;支持速度调节和正反转切换;还具备限位保护功能,能避免模组运行时出现超程的情况;同时,系统可以长时间稳定运行,满足学习和实验的需求。

8 结论

本文介绍了以二相混合式步进电机为例的FOC、SVPWM理论推导及实现,控制框架的搭建,PI控制器的编写,相关外设配置。从keil的安装配置开始,为后续开发奠定了坚实基础。

通过foc基础理论的学习,掌握了park变换与反park变换等核心概念,并通过实际的代码加以实践。在外设配置方面学习了一系列单片机的基础外设,包括PWM.SPI.DMA等。

在实际调试中,利用串口调试工具与项目上位机成功使电机旋转,并成功运行开环模式,闭环模式,零点辨识,校准模式等。

整个系统实现了foc框架下的步进电机精确控制,以及开环模式下的稳定运行与编码器自校准。

results matching ""

    No results matching ""