1 SmartFOC-42步进

本项目基于 N32G452CCL7 微控制器开发了一款 smartFOC 电机控制系统。系统通过 SPI 接口接收磁编码器信号实现转速闭环控制,同时采用基于 Modbus 协议的 RS-485 总线与上位机通信,实现对二相混合式步进电机的磁场定向控制。系统还预留了 I2C 接口与传感器模块通信的能力,为姿态反馈扩展提供了可能。整体架构充分利用 N32G452CCL7 的硬件定时器资源,通过精准的 PWM 生成确保电机控制的动态性能,为高精度驱动场景提供了高效的解决方案。

system

1.1 控制原理架构

本42步进电机控制项目采用三环控制架构,从目标指令到电机驱动形成位置、速度、电流的闭环控制,确保运动精度与稳定性,具体逻辑如下:

  1. 指令输入与规划:以上位机输入的目标位置为参考值,通过S型规划算法对位置指令进行平滑处理,避免运动启停时的冲击。该算法通过规划加减速曲线,限制速度变化率,让电机运行更平稳。本项目也可速度环+电流环双环控制。

  2. 三环闭环控制

    • 位置环:将S型规划后的目标位置与编码器反馈的实际位置对比,输出作为速度环的目标速度,实现精准位置控制。

    • 速度环:接收位置环输出的目标速度,与编码器微分后的速度反馈比较,输出作为电流环的q轴目标电流,抑制速度波动。

    • 电流环:对q轴、d轴电流分别控制(d轴目标电流通常设为0,实现转矩最大化利用),输出经Park反变换生成静止坐标系下的电压指令,精准控制电机转矩输出。

  3. 驱动与反馈:电压指令输入SVPWM模块,生成最优开关序列驱动全桥电路,控制电机绕组电流。SVPWM通过合理分配电压矢量,提升直流电压利用率,让电机转矩更平滑。电机运行状态经编码器实时反馈,一方面用于位置、速度计算,另一方面为反park变换提供角度信息,形成完整闭环。

system

1.2 程序执行流程

项目程序初始化后进入主循环,执行中断程序中的控制算法、电流采样等重要程序,保障控制实时性与功能完整性:

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

主循环定时执行程序Modbus/CanOpen处理,定时解析总线指令,并反馈电机状态(实际位置、速度、故障码等),实现上位机或主控制器对步进电机的远程控制;故障检测,周期性检查过流、过压、编码器故障等异常,一旦触发,立即执行保护动作,保障系统安全;

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

2 硬件资源

2.1 42步进电机

  • 本系统采用的电机为24V的42步进电机,具有高精度、高扭矩输出等特点。该电机适用于需要精确位置控制和速度调节的应用场景,如自动化设备、机器人等。 system
  • 电机参数如下: system

2.2 MCU与主控芯片

本系统控制器采用自主研发的嵌入式主控单元,基于 N32G452CCL7 芯片设计。N32G452CCL7 是一款高性能的 32 位 ARM Cortex-M4 微控制器,主频可达 144MHz,具有丰富的外设接口和强大的处理能力,非常适合用于工业控制、机器人、智能装备等领域的应用。

  • 主控芯片N32G452CCL7(国民技术)

    • 主频144MHz

    • 内置512KB Flash和144KB SRAM

  • 工作电压24V DC(电源转换)

  • 主要通信接口
    • CAN总线 ×2(支持CAN 2.0A/B
    • RS485 ×2(隔离型)

      2.3 MT6835磁编码器

MT6835磁编码器基于各向异性磁阻技术和信号处理技术实现了0°~360°的绝对角度测量。该芯片还提供客户端自校准模式,用户只需匀速旋转电机,芯片就能自行进行校准,项目内上位机配备的校准模式就用于校准磁编码器。磁编码器通过磁场来确定电机方向,所以一般放置于电机驱动板中央。

system

2.4 24V直流电源

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

3 基础准备

3.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

3.2 软件安装

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

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

  • 安装Nationstech设备支持包:

    • 打开下载的资源包,找到并双击 N32G4xx_DFP.x.x.x.pack 自动安装,如下图所示。 pack
  • 安装ST-Link驱动:可参考下方CSDN博主的安装教程。

    stlink驱动教程: https://blog.csdn.net/m0_68987050/article/details/146936297?

4 理论基础

4.1 RS-485协议

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

system

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

4.1.1 RS485物理层

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

system

4.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 地线

4.1.3 连接方式

system

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

RS485应用实例如下:

system

4.2 Modbus通讯协议

4.2.1 Modbus简介

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

4.2.2 数据帧结构

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

system

地址码:这个字节写入从机的地址,每个从机都有唯一一个地址码,从机根据这个字节识别这条命令是否是发给自己的。当地址码位0时,为广播地址,所有从机均能识别。

功能码:这个字节告诉从机要进行什么操作,比如停机,自检,重启等。Modbus规定功能号为1-127。

数据:这段由起始地址和寄存器数量组成,意思是从哪个地址开始读多少个寄存器数量的数据。

校验位:根据某种算法生成的校验数据,常见的算法有奇偶校验,CRC校验。本项目采用的是CRC校验。这里附上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为校验位。

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

4.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实现两者之间的转换,这就是磁场定向控制的核心。

4.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变换。

4.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轴,二者结合可以显著提升电机的动态性能、效率及控制精度。

4.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=Ua/VDC*0.5 +0.5

Tb=Ub/VDC*0.5 +0.5

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

4.4 VF开环控制

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

system

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

4.5 故障检测

4.5.1 过速

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

4.5.2 过载

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

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

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

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

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

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

过载故障的简易判断:

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

4.5.3 堵转

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

4.5.4 编码器异常

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

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

system

5 软件代码教程

5.1 系统及外设初始化

system

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

5.1.1 系统时钟

system 时钟是系统的心脏,在系统中有着举足轻重的地位,本项目中通过定时器提供的中断为众多程序提供了执行周期,提高了系统执行效率与稳定性,在初始化的第一步,往往先初始化时钟,在需要高速处理速度的FOC中,我们采用N32G452CCL7的最大主频144MHZ system

我们根据时钟树,时钟源使用外部高速时钟,PPL MULFCT设置为x18,AHB使用系统时钟,APB2为AHB时钟的一半,ABP1为AHB时钟的四分之一。

void SystemClk_Init()
{
    ErrorStatus HSEStartUpStatus;

    //RCC system reset(for debug purpose)
    RCC_DeInit();

    //Enable HSE
    RCC_ConfigHse(RCC_HSE_ENABLE);//打开外部晶振
    //Wait till HSE is ready
    HSEStartUpStatus = RCC_WaitHseStable();//等待外部晶振就绪
    if(HSEStartUpStatus == SUCCESS)
    {
        //HCLK = SYSCLK
        RCC_ConfigHclk(RCC_SYSCLK_DIV1); //AHB使用系统时钟
        //PCLK2 = HCLK/2
        RCC_ConfigPclk2(RCC_HCLK_DIV2); //APB2(高速)为HCLK/2
        //PCLK1 = HCLK/4
        RCC_ConfigPclk1(RCC_HCLK_DIV4);// //APB1(低速)为HCLK/4

        //icache and prefetch configure
        FLASH_iCacheCmd(FLASH_iCache_EN);
        FLASH_PrefetchBufSet(FLASH_PrefetchBuf_DIS);
        //Flash wait state,4
        FLASH_SetLatency(FLASH_LATENCY_4); //flash操作的延时
        //PLLCLK = 8MHz * 18 = 144 MHz
        RCC_ConfigPll(RCC_PLL_SRC_HSE_DIV1, RCC_PLL_MUL_18);//PPL MULFCT设置为x18

        RCC_EnablePll(ENABLE);
        //Wait till PLL is ready
        while(RCC_GetFlagStatus(RCC_FLAG_PLLRD) == RESET){}
        RCC_ConfigSysclk(RCC_SYSCLK_SRC_PLLCLK);
        //Wait till PLL is used as system clock source
        while(RCC_GetSysclkSrc() != 0x08){}
    }

}

5.1.2 GPIO

GPIO(General-purpose input/output)初始化,也是极其重要的一环,这个环节关系到其他外设的初始化,代码中可以看到PWM,ADC,SPI,USART等外设接口的初始化。

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

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

void Gpio_Init(void)
{
    GPIO_InitType GPIO_InitStructure;

    //GPIO for LED
    GPIO_InitStructure.Pin = GPIO_PIN_13 | GPIO_PIN_14| GPIO_PIN_15;//外部晶振接入口
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitPeripheral(GPIOC, &GPIO_InitStructure);
    LED1_OFF;   //上电默认暗
    LED2_OFF;

    GPIO_InitStructure.Pin =  GPIO_PIN_12;                // ALL_L
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitPeripheral(GPIOA, &GPIO_InitStructure); 
    GPIO_WriteBit(GPIOA, GPIO_PIN_12, Bit_RESET);

    //FOC with TIM1 Channel 1, 1N, 2, 2N, 3, 3N and 4 Output
    GPIO_InitStruct(&GPIO_InitStructure);
    GPIO_InitStructure.Pin = GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10| GPIO_PIN_11;  //四个PWM口连接逆变电路
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitPeripheral(GPIOA, &GPIO_InitStructure);

    GPIO_InitStruct(&GPIO_InitStructure);
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
    GPIO_InitStructure.Pin = GPIO_PIN_5;//ADC电流采样串口
    GPIO_InitPeripheral(GPIOA, &GPIO_InitStructure);

    GPIO_InitStructure.Pin = GPIO_PIN_4;//母线电压采样串口
    GPIO_InitPeripheral(GPIOA, &GPIO_InitStructure);

    GPIO_InitStructure.Pin = GPIO_PIN_7;//ADC电流采样串口
    GPIO_InitPeripheral(GPIOA, &GPIO_InitStructure);

    //SPI==>MT6835
    GPIO_InitStructure.Pin = GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5;//SPI通信口分别为 SCK MISO MOSI
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitPeripheral(GPIOB, &GPIO_InitStructure);

    GPIO_InitStructure.Pin = GPIO_PIN_15; //SPI通信口 NSS
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitPeripheral(GPIOA, &GPIO_InitStructure);
    GPIO_SetBits(GPIOA, GPIO_PIN_15);

    GPIO_InitStructure.Pin = GPIO_PIN_12;     //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);        

    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);      //设置为接收模式,默认接收
}

5.1.3 USART

USART(Universal Synchronous Asynchronous Receiver Transmitter),通用同步异步收发器通常用于设备之间通信,在USART_InitType的结构体下有成员BaudRate(波特率),HardwareFlowControl(硬件流控制),Mode(模式),Parity(奇偶校验),StopBits(停止位),WordLength(字长)。

波特率,波特率表示单位时间内传送的码元符号的个数,这里的定义与比特率(单位时间内传送比特的个数)有明确的区别,读者注意不要搞混

硬件流控制,简单来说就是当我们的有一方的数据传输过快,另一方处理不过来,需要发送信号让发送方等一等,这时候就要启用硬件流控制,本项目并未启用硬件流控制。

奇偶校验,分为奇校验与偶校验,就是在数据的尾部舔一位使得1的总数为奇数或偶数,这一种简单的校验方法往往不启用,更常用的是CRC校验。

停止位,表示字符数据传输停止的位。

字长,用于确定字符数据的长度。

初始化流程与上文相似,创建一个USART_InitType下的结构体,给各个成员赋值后传入USART_Init中初始化USART


void Modbus_USART_Init(unsigned int baud)
{

    USART_InitType USART_InitStucture;
    NVIC_InitType NVIC_InitStucture;


    //USART结构体配置
    USART_DeInit(MODBUS_USART);
    USART_InitStucture.BaudRate = baud;
    USART_InitStucture.HardwareFlowControl = USART_HFCTRL_NONE;
    USART_InitStucture.Mode = USART_MODE_RX | USART_MODE_TX;
    USART_InitStucture.Parity = USART_PE_NO;
    USART_InitStucture.StopBits = USART_STPB_1;
    USART_InitStucture.WordLength = USART_WL_8B;

    USART_Init(MODBUS_USART, &USART_InitStucture);
    USART_EnableDMA(MODBUS_USART, USART_DMAREQ_RX | USART_DMAREQ_TX, ENABLE);

    USART_Enable(MODBUS_USART, ENABLE);
    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);
}

5.1.4 ADC

ADC(Analog-to-Digital Converter)数模转换器,能够将模拟量转化为数字量,常用于电压与电流的采样,本项目中ADC采样用于母线电压与绕组电流的采样,ADC不能直接采样电流,一般需要采样电阻与放大电路的配合来检测电流。

ADC_InitType下的成员包括WorkMode(工作模式),MultiChEn(多通道使能),ContinueConvEn(连续转换使能),ExtTrigSelect(外部触发选择),DatAlign(数据对齐),ChsNumber(通道数量)。

WorkMode(工作模式),配置 ADC 的基本工作模式。项目采用ADC_WORKMODE_INJ_SIMULT(同步注入模式)该模式能一次性转换四个通道,通常用于处理优先级高的任务,在FOC中我们需要保证电流环的实时性与AB相绕组电流的同步采样,使用同步注入模式是较好的选择。

MultiChEn(多通道使能),使能时允许 ADC 在规则组中扫描多个通道。项目中设置为使能,允许ADC2同时采样多个通道。

ContinueConvEn(连续转换使能),使能时ADC转换序列完成后自动重启转换新的序列,项目中设置为禁用每次转换使用tim1的触发源,保证了ADC采样频率的稳定性。

ExtTrigSelect(外部触发选择),选择启动 ADC 转换的外部触发源。项目使用的是TIM1的源,外部触发来源于 ADC1 的多路开关,ADC2 会被同步触发。

DatAlign(数据对齐),设置 ADC 转换结果在数据寄存器中的对齐方式。右对齐,数据从寄存器最低位开始存储(如 12 位结果存于 0x0FFF)。左对齐,数据从寄存器最高位开始存储(如 12 位结果存于 0xFFF0)。左对齐便于快速计算(无需移位),右对齐更符合常规数据处理逻辑。

ChsNumber(通道数量) 作用:指定规则组多通道扫描模式中转换的通道总数。仅当 MultiChEn = ENABLE 时有效。本项目使用注入组,故这里设置为0。



void Adc_Init(void)
{
    ADC_InitType ADC_InitStructure;
    NVIC_InitType NVIC_InitStructure;
    ADC_DeInit(ADC1);
    ADC_DeInit(ADC2);
    //ADC1 and ADC2 configuration
    ADC_InitStruct(&ADC_InitStructure);
    ADC_InitStructure.WorkMode = ADC_WORKMODE_INJ_SIMULT;
    ADC_InitStructure.MultiChEn = ENABLE;
    ADC_InitStructure.ContinueConvEn = DISABLE;
    ADC_InitStructure.ExtTrigSelect = ADC_EXT_TRIG_INJ_CONV_T1_TRGO;
    ADC_InitStructure.DatAlign = ADC_DAT_ALIGN_R;
    ADC_InitStructure.ChsNumber = 0;
    ADC_Init(ADC1, &ADC_InitStructure);
    ADC_Init(ADC2, &ADC_InitStructure);

    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的时钟设置了
    ADC_EnableExternalTrigInjectedConv(ADC2,ENABLE);

    //NVIC Initial
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    //Enable the ADC Interrupt 
    NVIC_InitStructure.NVIC_IRQChannel = ADC1_2_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);

  //Enable ADC
    ADC_Enable(ADC2, ENABLE);
    while(ADC_GetFlagStatusNew(ADC2,ADC_FLAG_RDY) == RESET);

    //Calibration
    ADC_StartCalibration(ADC2);
    while (ADC_GetCalibrationStatus(ADC2));

    ADC_ConfigInt(ADC2, ADC_INT_JENDC | ADC_INT_AWD, ENABLE);
}

5.1.5 DMA

本项目的DMA外设用于MODBUS通讯中的数据传输,初始化流程与其他外设相似,这里主要讲解DMA初始化结构体DMA_InitType下的各个成员的用法,其中包括PeriphAddr (外设地址)、MemAddr (内存地址)、Direction (数据传输方向)、BufSize (传输数据量)、PeriphInc (外设地址递增)、DMA_MemoryInc (内存地址递增)、PeriphDataSize (外设数据宽度)、MemDataSize (内存数据宽度)、CircularMode (循环模式)、Priority (通道优先级)、Mem2Mem (内存到内存模式)。

PeriphAddr (外设地址),用于指定外设数据寄存器的物理地址,(uint32_t) & (MODBUS_USART->DAT),这个寄存器是USART的数据寄存器。是用于存放收发数据的寄存器。DMA与外设的通讯无论收发都经过这个寄存器,所以无论是TX还RX的外设地址都写这个,读者需要注意到,这里因为是半双工通信所以只使用了一个收发数据寄存器,在全双工通信中可能会使用到两个。

MemAddr (内存地址),指定数据缓冲区的内存地址。

  • Tx配置使用了一个modbus_send_data的数组,这个数组名指向数组的首位,发送数据缓冲区起始地址,DMA从此处读取待发送数据。

  • Rx配置使用了一个modbus_recv_data数组,接收数据缓冲区起始地址,DMA将接收到的数据写入此处

Direction (数据传输方向),定义DMA数据传输方向。

  • TX配置:DMA_DIR_PERIPH_DST,内存到外设(数据从modbus_send_data复制到USART->DAT)

  • RX配置:DMA_DIR_PERIPH_SRC,外设到内存(数据从USART->DAT复制到modbus_recv_data)

BufSize (传输数据量),设置单次DMA传输的数据量(单位=数据宽度)

  • TX配置:0,初始化时设为0,实际发送前需动态设置有效数据长度

  • RX配置:MODBUS_RX_MAXBUFF,最大接收缓冲区长度,DMA自动传输该数量的字节

PeriphInc (外设地址递增),控制传输后外设地址是否自动递增

  • TX/RX配置:DMA_PERIPH_INC_DISABLE地址固定(USART数据寄存器地址固定不变)

DMA_MemoryInc(内存地址递增),控制传输后内存地址是否自动递增

  • TX/RX配置:DMA_MEM_INC_ENABLE地址自动递增(用于连续存储多字节数据)

PeriphDataSize (外设数据宽度)定义单次外设访问的数据大小

  • TX/RX配置:DMA_PERIPH_DATA_SIZE_BYTE,按字节访问(匹配USART的8位数据格式)

MemDataSize (内存数据宽度),定义单次内存访问的数据大小

  • TX/RX配置:DMA_MemoryDataSize_Byte,按字节访问(匹配缓冲区数据类型)

CircularMode (循环模式)使能缓冲区自动循环

  • TX/RX配置:DMA_MODE_NORMAL,单次传输模式(传输完成后停止,需重新使能)

Priority (通道优先级)设置DMA通道仲裁优先级

  • TX/RX配置:DMA_PRIORITY_LOW 低优先级(多个DMA通道竞争时使用)

Mem2Mem (内存到内存模式) 使能内存间直接传输(不经过外设)

  • TX/RX配置:DMA_M2M_DISABLE 禁用此模式(使用外设触发传输) ```C

void Modbus_DMA_Init(void) { DMA_InitType DMA_InitStruct; NVIC_InitType NVIC_InitStruct;

RCC_EnableAHBPeriphClk(MODBUS_DMA_CLK, ENABLE);

DMA_DeInit(MODBUS_DMA_TX_Channel);
DMA_DeInit(MODBUS_DMA_RX_Channel);
DMA_StructInit(&DMA_InitStruct);  //先默认初始化结构体

// 配置 DMA1 通道2, USART3_TX
DMA_InitStruct.PeriphAddr = (uint32_t) & (MODBUS_USART->DAT); // USART_DR 地址偏移:0x04
DMA_InitStruct.MemAddr = (uint32_t)modbus_send_data;  // 内存地址
DMA_InitStruct.Direction = DMA_DIR_PERIPH_DST;
DMA_InitStruct.BufSize = 0;             //寄存器的内容为0时,通道是否开启都不会发生数据传输
DMA_InitStruct.PeriphInc = DMA_PERIPH_INC_DISABLE;
DMA_InitStruct.DMA_MemoryInc = DMA_MEM_INC_ENABLE;
DMA_InitStruct.PeriphDataSize = DMA_PERIPH_DATA_SIZE_BYTE;
DMA_InitStruct.MemDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStruct.CircularMode = DMA_MODE_NORMAL;
DMA_InitStruct.Priority = DMA_PRIORITY_LOW;
DMA_InitStruct.Mem2Mem = DMA_M2M_DISABLE;
DMA_Init(MODBUS_DMA_TX_Channel, &DMA_InitStruct);
// 重映射
DMA_RequestRemap(MODBUS_DMA_TX_REMAP, MODBUS_DMA, MODBUS_DMA_TX_Channel, ENABLE);

// 配置 DMA1 通道3, USART3_RX
DMA_InitStruct.PeriphAddr = (uint32_t) & (MODBUS_USART->DAT); // (USART_DR) 地址偏移:0x04
DMA_InitStruct.MemAddr = (uint32_t)modbus_recv_data;  // 内存地址
DMA_InitStruct.Direction = DMA_DIR_PERIPH_SRC;       // 外设到内存
DMA_InitStruct.BufSize = MODBUS_RX_MAXBUFF;
DMA_InitStruct.PeriphInc = DMA_PERIPH_INC_DISABLE;
DMA_InitStruct.DMA_MemoryInc = DMA_MEM_INC_ENABLE;
DMA_InitStruct.PeriphDataSize = DMA_PERIPH_DATA_SIZE_BYTE;
DMA_InitStruct.MemDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStruct.CircularMode = DMA_MODE_NORMAL;
DMA_InitStruct.Priority = DMA_PRIORITY_LOW;
DMA_InitStruct.Mem2Mem = DMA_M2M_DISABLE;
DMA_Init(MODBUS_DMA_RX_Channel, &DMA_InitStruct);
// 重映射
DMA_RequestRemap(MODBUS_DMA_RX_REMAP, MODBUS_DMA, MODBUS_DMA_RX_Channel, ENABLE);

// 配置DMA发送完成中断
NVIC_InitStruct.NVIC_IRQChannel = MODBUS_DMA_TX_IRQn;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0;  // 抢占优先级
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;         // 子优先级
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);

}


<a id="section-5-1-6"></a>


### 5.1.6 PWM

PWM(Pulse Width Modulation,脉冲宽度调制)是一种通过改变脉冲的宽度来控制输出功率的技术,即调整方波占空比实现模拟电压控制。它在电机控制、LED调光等领域有广泛应用。本项目中使用PWM来控制步进电机的驱动。具体实现如下图所示:
<img src="assets\stepper_motor\PWM.png" alt="system" style="display: block; margin: 10px auto;zoom:50%" />
PWM这部分使用的结构体和成员较多,主要是TIM_TimeBaseInitType(时基配置结构体),OCInitType(输出比较配置结构体),TIM_BDTRInitType(刹车与死区配置结构体)

首先是**TIM_TimeBaseInitType**,这部分主要配置计时器的时钟。

**prescaler**(预分频)配置为0表示并不分频,时钟频率 = 定时器时钟 / (Prescaler+1)。

**cntmode**(计数模式)这里配置为中心对齐模式,从零计到最大值,再从最大计到最小往复循环。

**period**(周期)定时器计数上限,决定PWM周期(计算见下文)

**clkdiv**(时钟分频因子)控制定时器内部时钟分频(1分频表示不分频)

**repetcnt**(重复计数器)高级定时器特性,0表示每次更新事件立即生效


其次是**OCInitType**


**OcMode** (输出模式),PWM模式1(CNT < Pulse时输出有效电平)

**OutputState** (主输出使能),禁用通道主输出(后续需手动使能)

**OutputNState** (互补输出使能),禁用互补输出(用于带死区的电机控制)

**Pulse** (脉冲宽度),(TimerPeriod>>1) (周期值/2)    ,决定PWM占空比,此处初始50%对应0矢量,这里读者可以回顾之前4.3.1章节举的例子

**OcPolarity** (输出极性),有效电平为高/低(通道1/3高有效,通道2/4低有效)。

**OcNPolarity** (互补输出极性),互补通道有效电平为高,有效电平即导通,三极管门极要高电压才能导通。

**OcIdleState** (空闲状态主输出电平),定时器停止时输出复位电平(低),即关断,绕组上没有电压。

**OcNIdleState** (空闲状态互补输出电平),定时器停止时互补输出复位电平(低)

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




```c
void Pwm_Init(void)
{
    TIM_TimeBaseInitType TIM1_TimeBaseStructure;
    OCInitType TIM1_OCInitStructure;
    TIM_BDTRInitType TIM1_BDTRInitStructure;

    uint16_t TimerPeriod = 0;

    TimerPeriod = (MAIN_FREQUENCY / (PWM_FREQUENCY1*2)) - 1;  //4499

    //Time Base configuration
    TIM_DeInit(TIM1);
    TIM_InitTimBaseStruct(&TIM1_TimeBaseStructure);
    TIM1_TimeBaseStructure.Prescaler = 0;
    TIM1_TimeBaseStructure.CntMode = TIM_CNT_MODE_CENTER_ALIGN1;
    TIM1_TimeBaseStructure.Period = TimerPeriod;
    TIM1_TimeBaseStructure.ClkDiv = TIM_CLK_DIV1;
    TIM1_TimeBaseStructure.RepetCnt = 0;

    TIM_InitTimeBase(TIM1, &TIM1_TimeBaseStructure);
    //Channel 1, 2,3 in PWM mode
    TIM_InitOcStruct(&TIM1_OCInitStructure);
    TIM1_OCInitStructure.OcMode = TIM_OCMODE_PWM1;
    TIM1_OCInitStructure.OutputState = TIM_OUTPUT_STATE_DISABLE; 
    TIM1_OCInitStructure.OutputNState = TIM_OUTPUT_NSTATE_DISABLE;                  
    TIM1_OCInitStructure.Pulse = (TimerPeriod>>1);
    TIM1_OCInitStructure.OcPolarity = TIM_OC_POLARITY_HIGH;
    TIM1_OCInitStructure.OcNPolarity = TIM_OCN_POLARITY_HIGH; 
    TIM1_OCInitStructure.OcIdleState = TIM_OC_IDLE_STATE_RESET;
    TIM1_OCInitStructure.OcNIdleState = TIM_OC_IDLE_STATE_RESET;  
    TIM_InitOc1(TIM1, &TIM1_OCInitStructure); 
    TIM_InitOc3(TIM1, &TIM1_OCInitStructure);
    TIM1_OCInitStructure.OcPolarity = TIM_OC_POLARITY_LOW;  
    TIM_InitOc2(TIM1, &TIM1_OCInitStructure);
    TIM_InitOc4(TIM1, &TIM1_OCInitStructure);
    TIM_SelectOutputTrig(TIM1, TIM_TRGO_SRC_UPDATE);   //选择发出的触发信号
    //Enables the TIM1 Preload on CC1,CC2,CC3,CC4 Register
    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);



    //TIM1 counter enable
    TIM_Enable(TIM1, ENABLE);
    TIM_EnableCtrlPwmOutputs(TIM1,ENABLE);
}

5.1.7 SPI

SPI(Serial Peripheral Interface)串行外设接口,是一种同步串行通信协议,常用于微控制器与外设之间的高速数据传输。 SPI是一个同步数据总线,它使用主从架构,主设备控制通信时序,从设备响应主设备的命令。SPI通信通常包括四条线:SCK(时钟线)、MOSI(主设备输出从设备输入)、MISO(主设备输入从设备输出)和NSS(片选线)。精妙之处在于采用单独的数据线和单独的时钟信号来保证发送端和接收端的同步,避免了异步通信中可能出现的数据错位问题。 system 本项目SPI通信主要用于读取MT6835磁编码器的数据

DataDirection(数据传输模式):全双工模式,同时使用 MOSI 和 MISO 线进行双向数据传输。

SpiMode(主从模式),配置为 SPI 主设备(生成时钟信号,控制通信)。

DataLen(数据帧长度),每帧传输 16 位数据(2 字节)。

CLKPOL(时钟极性),空闲状态时 SCK 保持高电平。

CLKPHA(时钟相位),在时钟的第二个边沿采样数据(与极性组合决定具体模式)。

NSS (片选控制),软件控制 NSS 信号(需手动操作 GPIO 选择从设备)。

BaudRatePres(波特率预分频),时钟分频系数为 16,波特率 = 系统时钟 / 16。

FirstBit(传输顺序),从最高有效位(MSB)开始传输。

CRCPoly (多项式),设置 CRC 校验多项式为 x⁸ + x² + x + 1(值 7 的对应多项式系数)

void MT6835_Init(void)
{
    NVIC_InitType NVIC_InitStructure;
    /* Configure and enable SPI_MASTER interrupt -------------------------------*/
    NVIC_InitStructure.NVIC_IRQChannel                   = SPI3_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority        = 1;
    NVIC_InitStructure.NVIC_IRQChannelCmd                = ENABLE;
    NVIC_Init(&NVIC_InitStructure);

    SPI_InitType  SPI_InitStructure;

    SPI_InitStructure.DataDirection = SPI_DIR_DOUBLELINE_FULLDUPLEX;
    SPI_InitStructure.SpiMode = SPI_MODE_MASTER;
    SPI_InitStructure.DataLen = SPI_DATA_SIZE_16BITS;//gj点
    SPI_InitStructure.CLKPOL = SPI_CLKPOL_HIGH;
    SPI_InitStructure.CLKPHA = SPI_CLKPHA_SECOND_EDGE;
    SPI_InitStructure.NSS = SPI_NSS_SOFT;
    SPI_InitStructure.BaudRatePres = SPI_BR_PRESCALER_16;//SPI_BR_PRESCALER_256;
    SPI_InitStructure.FirstBit = SPI_FB_MSB;
    SPI_InitStructure.CRCPoly = 7;
    SPI_Init(SPI3, &SPI_InitStructure);

    SPI_I2S_EnableInt(SPI3, SPI_I2S_INT_RNE, ENABLE); //使能接收中断

    SPI_Enable(SPI3, ENABLE);
}

5.1.8 NVIC

system 本项目的定时中断的主要构成如上图所示,各个功能

NVIC,可以简单理解为中断配置,各个外设都有触发中断的方法,比如usart收到消息就中断,ADC转换完成就中断等等,所以NVIC的配置总是分散在各个外设配置中。

NVIC初始化结构体NVIC_InitType下的成员包括NVIC_IRQChannel,NVIC_IRQChannelPreemptionPriority,NVIC_IRQChannelSubPriority,NVIC_IRQChannelCmd。

NVIC_IRQChannel(中断通道),指定要配置的外设中断源(此处为 SPI3 的中断通道)

NVIC_IRQChannelPreemptionPriority(抢占优先级),设置中断的抢占优先级(0 为最高,数值越大优先级越低)

NVIC_IRQChannelSubPriority(子优先级),在相同抢占优先级的中断中,决定响应顺序(数值越小优先级越高)

NVIC_IRQChannelCmd ENABLE(中断使能),控制该中断通道的开关状态(ENABLE/DISABLE)

抢占优先级是一个大组,大组0总是比大组1的优先级高,在大组0内有很多中断源,要使你的中断在组内尽可能快就要配置子优先级,子优先级决定了同一组内的中断优先程度。

5.2 控制框架代码详解

system 本项目控制程序流程如上图所示,通过将定时器提供的10kHz中断分频后以1kHz运行上层算法,在上层算法中选择不同的模式运行。

5.2.1 FOC控制流程

system

5.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);
}

5.2.3 SVPWM

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

void PWM(SVPVM *v)
{
    short MPeriod;
    int Tmp;
    //v->MfuncC1 = v->Ta; // MfuncC1 is in Q15
    //v->MfuncC2 = v->Tb; // MfuncC2 is in Q15

    v->MfuncC1 = v->Ubeta;//Vb是short类型32767~-32768,乘以4499后写到int中,右移15位相当于将Vb(32767~-32768)归一化到0~1之间再乘以4499除以2再加上4499除以2,简单理解为Vb 32767对应24v,-32768对应-24v
    v->MfuncC2 = v->Ualpha;

    // Compute the timer period (Q0) from the period modulation input (Q15)
    Tmp = (int)v->PeriodMax * (int)v->MfuncPeriod;         // Q15 = Q0*Q15    
     //147418733=4499*32767
    MPeriod = (short)(Tmp >> 16) + (short)(v->PeriodMax >> 1); // Q0 = (Q15->Q0)/2 + (Q0/2)   
    //4498.5=2249+4499/2

    //Compute the compare A (Q0) from the EPWM1AO & EPWM1BO duty cycle ratio (Q15)
    Tmp = (int)MPeriod * (int)v->MfuncC1;                  // Q15 = Q0*Q15 //4499*ub
    //EPwm4Regs.CMPA.half.CMP_A= (int16)(Tmp>>16) + (int16)(MPeriod>>1);   // Q0 = (Q15->Q0)/2 + (Q0/2)
    v->Va = (short)(Tmp >> 16) + (short)(MPeriod >> 1); // Q0 = (Q15->Q0)/2 + (Q0/2)    //

    //Compute the compare B (Q0) from the EPWM2AO & EPWM2BO duty cycle ratio (Q15)
    Tmp = (int)MPeriod * (int)v->MfuncC2;                 // Q15 = Q0*Q15   //
    //EPwm5Regs.CMPA.half.CMP_A = (int16)(Tmp>>16) + (int16)(MPeriod>>1);  // Q0 = (Q15->Q0)/2 + (Q0/2)
    v->Vb = (short)(Tmp >> 16) + (short)(MPeriod >> 1); // Q0 = (Q15->Q0)/2 + (Q0/2)
}

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

system

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

5.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;
    }

}

5.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;
        //计算有匀速阶段的曲线数据
        float accel_aaccel = ptr->accel_max / ptr->a_accel;
        float vel_accel;

        if(ptr->accel_max < 1e-6f)
        {
            vel_accel = accel_aaccel;
        }
        else
        {
            vel_accel = fabsf((ptr->vel_tar - ptr->vel_init)) / ptr->accel_max;
        }

        float decel_adecel = ptr->decel_max / ptr->a_decel;
        float vel_decel;

        if(ptr->decel_max < 1e-6f)
        {
            vel_decel = decel_adecel;
        }
        else
        {
            vel_decel = ptr->vel_tar / ptr->decel_max;
        }

        float t2_t1 = vel_accel - accel_aaccel;
        float t6_t5 = vel_decel - decel_adecel;
        sp->t1 = accel_aaccel;
        sp->t2 = vel_accel;
        sp->t3 = vel_accel + accel_aaccel;
        sp->t4 = sp->t3 + (ptr->end_position - sp->s7) / ptr->vel_tar;
        sp->t5 = sp->t4 + decel_adecel;
        sp->t6 = sp->t4 + vel_decel;
        sp->t7 = sp->t6 + decel_adecel;

        sp->v1 = ptr->vel_init + 0.5f * sp->vel_flag * ptr->a_accel * powf(accel_aaccel, 2);
        sp->v2 = sp->v1 + ptr->accel_max * sp->vel_flag * t2_t1;
        sp->v3 = ptr->vel_tar;
        sp->v4 = sp->v3;
        sp->v5 = sp->v4 - 0.5f * ptr->a_decel * sp->vel_flag * powf(decel_adecel, 2);
        sp->v6 = sp->v5 - ptr->decel_max * sp->vel_flag * t6_t5;

        sp->s1 = ptr->start_position + ptr->vel_init * accel_aaccel + 1.0f / 6.0f * sp->vel_flag * ptr->a_accel * powf(accel_aaccel, 3);
        sp->s2 = sp->s1 + sp->v1 * t2_t1 + 0.5f * ptr->accel_max * sp->vel_flag * powf(t2_t1, 2);
        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);
        sp->s4 = sp->s3 + sp->v3 * (sp->t4 - sp->t3);
        sp->s5 = sp->s4 + sp->v4 * decel_adecel - 1.0 / 6 * ptr->a_decel * sp->vel_flag * powf(decel_adecel, 3);
        sp->s6 = sp->s5 + sp->v5 * t6_t5 - 0.5f * ptr->decel_max * sp->vel_flag * powf(t6_t5, 2);
        //float ss=sp->s6+sp->v6*decel_adecel-0.5*ptr->decel_max*sp->vel_flag*powf(decel_adecel,2)+1.0/6*ptr->a_decel*sp->vel_flag*powf(decel_adecel,3);
        sp->s7 = ptr->end_position;
    }

    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;
    }

}

5.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;

5.3 故障检测代码详解

5.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;// 时间窗口清零
    }
}

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

5.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,若大于则打上过载的标签。

5.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;
        }
    }
}

6 应用实例

6.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 |

6.2 上位机调试

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

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

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

system

步进电机有体积小、成本低、控制精度高,适合中小型模组的特点常用于中低端产品控制。本项目将其部署在直线模组上实现精准的位置控制(直线模组的应用场景如自动化设备、3D打印、精密搬运等)。

项目实现了直线模组的精准位置控制,定位误差≤0.1mm;支持速度可调(和正反转切换);具备限位保护(避免模组运行超程);系统长时间稳定运行。

7 结论

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

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

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

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

results matching ""

    No results matching ""