SmartFOC-for-servo-motor

1 项目背景

目前,市面上一直缺少一款真正适合自学和科研的开源级闭环伺服电机控制平台。虽然也有一些开源项目,比如 ODrive、SimpleFOC等,但在实际使用中总觉得“离谱又不够用”——要么太复杂,要么太简单,常常不太适合真正想系统学习 FOC 控制的人。

ODrive 工具链繁琐,配置麻烦。各种脚本、各种语言混在一起,看得眼花缭乱,刚想动手就已经被劝退。SimpleFOC 体积小巧,但功能上限太低,其控制结构大多停留在原理层面,缺少真正贴合工业场景的控制逻辑。这些开源项目缺少各种电机的保护机制,比如缺相保护、过流保护、短路检测……这些关键功能往往缺失或根本没有考虑。并且,这些项目大多配套的是低功率小电机,通常只配霍尔传感器,供电低、精度低,要想将这些开源项目的算法真正用于机器人或工业控制就力不从心了。也正因为这些原因,搭建一个“能学、能用、能上手还靠谱”的伺服驱动开源平台就显得尤为重要。

SmartFOC 就是为了解决这些问题而生。本项目主要包括一个上位机,电机驱动板,以及电机本体,上位机运行在PC上通过RS485转USB与电机驱动板连接,驱动板固定在电机上并接入48V直流电源,通过内部的逆变电路驱动电机旋转。

system

整套系统围绕400W低压永磁同步无刷直流电机设计,搭配工业级17位磁编码器与高可靠性的伺服驱动电路。控制核心基于磁场定向控制(FOC)三环结构,配备速度环、位置环、电流环,完整支持 SVPWM、S型速度规划、实时过流保护、缺相检测等常见机制。同时集成 Modbus 与 CANopen 工业通讯协议,可直接对接工业设备,真正做到从学习平台平滑过渡到工程实用。

更重要的是,SmartFOC 没有复杂的脚本堆叠和混乱的配置流程,软件结构简洁清晰,代码易于理解,适合本科阶段自学入门,也适合研究生深入研究高阶控制算法,甚至可以直接用于工业级机器人控制和伺服系统实验,是真正打通“教学—科研—工程”的闭环控制平台。

简单来说,就是既能用来学,也能拿来真做事,不再停留在“讲原理”的阶段,而是带着完整的系统和工程视角,把 FOC 控制玩明白、用起来。这即是本项目的目的。

2 资源清单

工欲善其事,必先利其器。刚接触FOC和伺服电机,不知如何下手时,先把准备工作做好是非常重要的。以下是本项目所需的主要资源清单:

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

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

3 硬件资源

3.1 永磁同步电机(PMSM)

本项目采用 48V 永磁同步电机(PMSM)。这款电机本身就带着高力矩密度的优势,调速范围也很宽,能在 0 到 3000rpm 之间灵活调整,满足于不同场景下的运动需求。而且,此电机是机器人领域、工业领域常用的 400W 低压无刷永磁同步直流电机,加上工业级 17 位磁编码和工业级驱动电路,搭配磁场定向控制算法,能实现优异的控制效果。​ system

  • 电机参数如下:

system

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

参数名称 数值 物理意义 工程意义
额定电压 48V 电机稳定运行的最佳电压,超出可能损坏绝缘或磁路 电源选型依据,需匹配驱动器输出电压
额定电流 10A 额定负载下的稳态电流 可用于设计驱动器电流容量(如选配15A以上MOSFET)
线间电阻 0.19Ω 绕组电气特性 可以用于计算损耗,建立数学模型
线间电感 0.46mH 绕组电气特性 可以作为设计驱动器PWM频率的依据
转矩系数 0.127Nm/A 电流→扭矩的转换效率(T = Kt × I) 闭环控制时,通过电流环精确控制输出扭矩
额定转矩 1.27 N·m 电机在额定工况下的持续输出扭矩 可用于计算加速度,进而设计s型规划曲线最大加速度
转子惯量 0.58×10⁻⁴ kg·m² 转子自身转动惯量 计算加速时间 t = (JΔω)/T,影响动态响应
反电动势常数 6.8 V/Krpm 转速与感应电压之间的比例 可以用于估计最大转速,也可用于实现无传感器控制

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 48V直流电源

  • 48V 直流电源与电机参数适配 —— 电机额定电压为 48V、额定电流为 10A,此电源能为其提供持续稳定的工作电压,确保电机高效运转。

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 软件安装

  • 安装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?

5 理论基础

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协议了

PLANRobot_Servo_48V线束如下图:

system

PLANRobot_Servo_48V线束定义:

颜色 定义 颜色 定义 颜色 定义
红色 BAT+ 黑色 BAT- 玫红 485A
黄色 X1+ 绿色 SGND 浅绿 485B
白色 X2+ 棕色 SGND 红白 CANH
蓝色 X3+ 灰色 SGND 黑白 CANL
橙色 Y1+ 紫色 Y2+ 蓝白 SGND
绿白 SGND 粗紫 48V+ 粗蓝 48V-

RS485应用实例如下:

system

5.2 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

帧结构 功能
地址码 这个字节写入从机的地址,每个从机都有唯一一个地址码,从机根据这个字节识别这条命令是否是发给自己的。当地址码位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为校验位。

5.3 CAN总线及CANopen通信协议

为了理解通信协议,我们通常参考一个理论模型(OSI 7层模型),但这里我们用更实用的简化版,主要关注这三层:

system

物理层(Physical Layer)可以理解为马路和车辆。它规定了信号的实际传输方式(怎么传),电压多少伏,用几根线,电缆是什么类型,传输速度有多快;任务单一,仅负责将 “0” 和 “1” 的电信号从一端传输至另一端,不涉及对信号含义的解读。例如前文提及的 RS-485 通信,便规定双绞线上的电压差在 + 2V~+6V 范围时表示 1,在 - 2V~-6V 范围时表示 0

数据链路层(Data Link Layer)相当于交通规则和车牌。它负责给数据的传输定规矩,确保数据在物理线路上能够可靠传输,具体涵盖以下内容:

功能分类 具体说明
帧结构 明确数据应被打包成何种 “包裹”(帧)、帧多大,以及包含什么信息(如地址、数据、校验码)
寻址机制 用于识别数据的发给谁,通过给每个设备分配唯一地址实现
错误检测功能 帧在传输过程中有没有被破环,借助校验码对传输过程中可能出现损坏的数据包进行检查
仲裁机制 当多个设备同时需要发送数据时,确定发送顺序,避免冲突

应用层(Application Layer)类似于包裹内的货物清单和操作指令。它定义了数据的具体含义以及设备间交互的逻辑规则。例如,收到 “0x1234” 这一数值所代表的是温度、速度还是开关状态;要发送一条指令,怎么发,对方收到后怎么回复;设备参数的怎么配置以及设备故障的怎么识别等,这些均属于应用层的管理范畴

5.3.1 CAN 总线 (Controller Area Network)

CAN 总线主要定义了物理层数据链路层的标准。它是一条共享的通信高速公路,让很多设备可以挂在这条线上互相说话。

5.3.1.1 CAN 总线的物理层

CAN总线有较长的历史,经过多年的发展,CAN总线的物理层已经发展出了很多种不同的标准,但都与RS-485一样通常使用差分信号 (CAN_H 和 CAN_L) 通过双绞线传输,这种设计抗干扰能力极强,适合嘈杂的工业或汽车环境。常见的物理层标准有ISO 11898 (高速), ISO 11519-2 (低速)等。不同的物理层协议,有不同的逻辑状态定义,以ISO11898(高速CAN)为例:

信号类型 工作状态 电压范围(V)
CAN_H 隐性(逻辑 1) 最小值: 2.00,正常值: 2.50,最大值: 3.00
CAN_H 显性(逻辑 0) 最小值: 2.75,正常值: 3.50,最大值: 4.50
CAN_L 隐性(逻辑 1) 最小值: 2.00,正常值: 2.50,最大值: 3.00
CAN_L 显性(逻辑 0) 最小值: 0.50,正常值: 1.50,最大值: 2.25
电位差(H-L) 隐性(逻辑 1) 最小值: -0.5,正常值: 0,最大值: 0.05
电位差(H-L) 显性(逻辑 0) 最小值: 1.5,正常值: 2.0,最大值: 3.0

在 CAN 总线中,必须使它处于隐性电平(逻辑 1)或显性电平(逻辑 0)中的其中一个状态。假如有两个 CAN 通讯节点,在同一时间,一个输出隐性电平,另一个输出显性电平,CAN总线的“线与”特性将使它处于显性电平状态,显性电平的名字就是这样来的,即可以认为显性具有优先的意味。 由于 CAN 总线协议的物理层只有 1 对差分线,在一个时刻只能表示一个信号,所以对通讯节点来说,CAN 通讯是半双工的,收发数据需要分时进行。在 CAN 的通讯网络中,因为共用总线,在整个网络中同一时刻只能有一个通讯节点发送信号,其余的节点在该时刻都只能接收。

5.3.1.2 CAN 总线的数据链路层

CAN总线不同于RS-485,CAN总线不仅定义了物理层还定义了数据链路层,主要包括帧结构寻址仲裁错误检测与处理

先看最重要的帧结构 (Frame Structure), 帧结构定义了数据怎么打包。具体结构如下:

system

帧结构 功能
起始段 主要用于通知总线上的其他设备做好接收的准备,用一个显性位(逻辑 0)宣告一帧数据传输开始。
仲裁段 包含 11 位标识符(ID) + 1 位远程发送请求位(RTR),共 12bit ,用来区分帧的优先级。数值越小优先级越高,当多个设备同时发送数据时,优先级高的帧能优先占用总线,避免数据冲突,保障关键数据优先传输。
数据域 实际要传输的数据就放在这里。
控制域 包含一些控制信息,比如数据域长度 (DLC)
CRC 校验 用于检查数据在传输过程中是否出错
ACK 接收节点用来确认是否成功收到帧

寻址 (Addressing),CAN 使用的是 基于标识符的广播/内容寻址。节点发送帧时带上一个 ID,总线上所有节点都能收到这个帧。每个节点根据自己需要,决定是否处理这个 ID 的数据(过滤)。没有传统意义上的“源地址”和“目标地址”字段,例如modbus中的地址码。这个ID放在仲裁段的前11位中。

仲裁 (Arbitration), 这是 CAN 的核心优势,当多个节点同时想发送数据时,它们在发送 ID 的同时也在监听总线电平。如果某个节点发了一个“显性位”(0),而它监听到的是“隐性位”(1),它就立刻停止发送(知道自己优先级低)。这个过程在 ID 发送过程中就完成了,优先级最高(ID 值最小)的帧会赢得仲裁,继续发送,其他帧自动退避稍后重试。没有任何数据冲突或丢失,效率很高。

错误检测与处理, 除了 CRC,CAN 协议还有多种错误检测机制(位错误、填充错误、格式错误、ACK 错误)。检测到错误的节点会发送“错误帧”通知大家,出错的帧会被自动重传。非常可靠。

一言以蔽之,CAN 总线是一条可靠、抗干扰、支持多主机、带智能仲裁机制的高速公路(物理层+数据链路层),规定了车辆(电信号)怎么跑、包裹(帧)长什么样、怎么避免撞车(仲裁)、怎么检查包裹损坏(校验)。但它不管包裹里装的东西(数据)具体代表什么。

5.3.2 CANopen 协议

CANopen 是建立在CAN总线之上的一个应用层协议。它给 CAN 这条高速公路定义了货物标准、物流规则和仓库管理手册。CANopen 完全依赖 CAN 总线提供物理层和数据链路层服务,使用标准的 CAN 帧来传输信息。总的来说CANopen协议主要干了以下几件事:

给每个设备发工牌,每个连在CAN总线上的设备都有一个唯一的数字编号,就像工号一样(节点ID)。这样信息发出来就知道是谁说的,发给谁。统一各个设备之间的语言,它定义了一个巨大的“字典”(对象字典Object Dictionary - OD)。这个字典里,每个设备内部的重要信息(比如电机的目标转速、当前温度、故障代码)都被分配了一个标准的编号和格式。无论设备是A厂还是B厂的,只要说“字典里第XX号信息是多少”,对方就一定能听懂。例如:

0x6040:00 可能代表 “控制字” (Control Word)

0x6064:00 可能代表 “位置实际值” (Actual Position)

0x1000:00 可能代表 “设备类型” (Device Type)

0x1017:00 可能代表 “生产者心跳时间” (Producer Heartbeat Time)

CANopen 还定义了不同类型的“标准包裹”(通信对象 Communication Objects)用于特定目的:

通信对象 功能
PDO(Process Data Object)过程数据对象 “实时广播”,用于传输需要快速、周期性更新的实时数据。一个 PDO 可以打包传输多个 OD 对象的值。比如电机当前的实时位置。就像电机在车间里喊一声“我走到5号位了!”,所有关心这个信息的设备都能立刻听到。通常是事件触发或周期性触发。也可以是收到 SYNC 报文后同步发送。速度快、效率高,但不保证每次必达(没有应用层确认机制),适合对实时性要求高的过程数据。
SDO (Service Data Object)服务数据对象 “一对一通话”。用于访问对象字典中的任何条目,配置参数、读取诊断信息、传输大数据块等。发送方会等待接收方的明确确认回复。可靠、保证送达(有确认机制),但速度相对 PDO 慢。适合配置和非实时数据交换。就像一对一打电话,确保信息准确无误地送达,但速度比喊话慢一点。
NMT (Network Management)网络管理 “指挥官指令”。主节点(NMT Master)用来管理整个网络,比如让所有节点启动、停止 、复位。也用于监控节点状态。
EMCY (Emergency)紧急报文 “警报”。设备发生严重故障时(如过温、过流),立即主动发送此报文通知网络上的其他节点(特别是主节点)。
SYNC (Synchronization)同步报文 “发令枪”。通常由主节点或专用同步节点周期性发送。收到 SYNC 的节点,如果配置为同步生产者,会同时发送它们的 PDO 数据。用于实现网络中多个设备的精确时间同步操作。
TIME (Time Stamp)时间报文 用于在网络上分发统一的时间戳信息。

CANopen是运行在 CAN 高速公路上的高级物流管理系统(应用层)。它通过“对象字典”统一了所有设备的“货物编码手册”,定义了“实时广播”(PDO)、“一对一通话”(SDO)、“指挥官指令”(NMT)、“警报”(EMCY)、“发令枪”(SYNC) 等标准化的包裹类型和交互规则,确保挂接在 CAN 总线上的各种设备,即使来自不同厂家,也能像一个团队一样高效、可靠、有序地协同工作

5.3.3 典型工作流程

system

(1)网络初始化,系统上电后,所有节点进入 "初始化" 状态,等待 NMT 主节点发送启动指令。

(2)节点启动,NMT 主节点发送 "0X01" 指令(NMT 消息),指定节点 ID 的设备进入 "运行" 状态。

(3)参数配置,通过 SDO 通讯,主节点读写从节点的对象字典,配置设备参数,定义PDO 映射哪些数据。

(4)实时数据传输,主节点发送 SYNC 同步帧,触发从节点同步动作。从节点通过 TPDO 发送实时数据到主节点,主节点通过 RPDO 接收。主节点通过 RPDO 发送控制指令,从节点通过 TPDO 接收并执行。

system

(1)节点发生故障时,通过 EMCY 对象发送错误信息,主节点接收后进行诊断和处理。

(2)主节点通过 NMT 指令控制节点状态(如停止、复位),实现网络维护。

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

5.4.1 永磁同步电机

在正式进入foc理论讲解之前我们先了解一下我们所使用的电机,PMSM(Permanent Magnet Synchronous Motor)即永磁同步电机,永磁是指转子是永磁体,区别于其他使用绕组作为转子的电机,使用永磁体作为转子有结构简单,无需励磁电流,损耗小等优点。同步,即电机绕组产生的旋转磁场的转速与电机转子转速一致。异步则反之。异步电机由于其特殊的结构需要转子与磁场有不同的速度才能持续产生驱动力。更进一步,永磁同步电机分为表贴式,与内置式,本项目使用的电机是表贴式永磁同步电机,表贴式永磁同步电机的永磁体贴在转子表面,故称表贴式,内置式永磁同步电机的永磁体内嵌在转子中,故称内置式。两种永磁体安装方式各有优缺点。表贴式电机,制造简单,成本低转动惯量小。内置式电机,机械强度高,功率密度高,易于弱磁扩速。在数学模型上我们需要知道的是,表贴式电机为隐极电机(Ld=Lq),内置式电机为凸极电机(Ld≠Lq)。

5.4.2 永磁同步电机数学模型及FOC理论

在进一步了解永磁同步电机之前,我们需要先了解PMSM中使用到的各种坐标系,α-β坐标系,d-q坐标系,三相坐标系。

system

如图,d-q坐标系建立于转子之上随转子一起旋转,d轴与磁场方向平行,q轴与磁场方向垂直,α-β建立于定子之上,不随转子旋转,α轴与A相绕组重合,三相坐标系建立于三相绕组之上,与α-β轴一样不随转子旋转,所以α-β坐标系与三相坐标系之间的转换关系是固定的,不会随着转子转动而改变。三相坐标系向α-β坐标系的转换就是Clark变换,反之则是反Clark变换。α-β坐标系与d-q坐标系之间的变换则与电角度相关,转换系数会随着转子的运动而改变。α-β坐标系向d-q坐标系的转换成为park变换,反之就是反park变换。FOC就是通过Clark变换与park变换将三相电流解耦成d-q坐标系下的电流来降低控制难度,提升控制精度。以下的数学模型会给出更具体的说明

这里给出旋转坐标系(d-q坐标系)下的永磁同步电机数学模型,以下分别是电压方程,电磁转矩方程,机械运动方程,以及电角度与机械角度的关系方程。

{ud=Rid+LdddtidωeLqiquq=Riq+Lqddtiq+ωe(Ldid+ψm) \begin{cases} u_{\text{d}} = Ri_{d}+L_{d}\frac{d}{dt}i_{\text{d}} - \omega_{e}L_{q}i_{q} \\ u_{\text{q}} = Ri_{q}+L_{q}\frac{d}{dt}i_{\text{q}} + \omega_{e}(L_{d}i_{d}+\psi_m) \end{cases}

Te=32pniq[id(LdLq)+ψm] T_{\text{e}} = \frac{3}{2}p_{n}i_{q}[i_{d}(L_{d}-L_{q})+\psi_m]

Jdωmdt=TeTLBωm J\frac{d\omega_m}{dt} = T_{e}-T_{L}-B\omega_m

ωe=pnωm \omega_e = p_{n}\omega_m

其中 ud,uq分别是定子电压d-q轴分量;id、iq分别是定子电流的d-q轴分量;R是定子电阻;ψd、ψq为定子的磁轴的d-q轴分量;ωe是电角速度;Ld、Lq分别是d-q轴电感分量;ψm为转子磁链,Pn为极对数,TL为负载转矩,Te为电磁转矩,ωm为机械角速度,B为阻滞系数。这里的电压与电流,电阻指的是相电压,相电流,相电阻。

其中,磁链是线圈匝数与该线圈各匝平均磁通的乘积(ψ=NΦ),这个概念的引入是为了简化磁通量的计算,读者可以想象,线圈中的每一匝与磁极的距离并不相同,故每一匝的磁通量并不相同,这样会使总磁通的计算式变得非常冗长,这里引入磁链的概念就能很好的解决这个问题。

ϕ1+ϕ2+ϕ3+ϕ4...=Nϕ0=ψ \phi_1+\phi_2+\phi_3+\phi_4...= N\phi_0=\psi

其中,Φ1,Φ2等表示各匝的磁通量,Φ0表示各匝的平均磁通量。磁链常用的符号还有λ。在PMSM中磁链可以分为转子磁链与定子磁链,定子磁链指定子电流生成磁通与定子绕组交链形成的磁链。转子磁链指转子永磁体生成磁通与定子绕组交链形成的磁链。除了转子磁链和定子磁链还有一种比较常见的概念是交轴磁链与直轴磁链,这里要先引入交轴与直轴的概念,直轴即d轴,与转子磁场方向一致。交轴即q轴,与转子磁场方向垂直。交轴磁链与直轴磁链表达式如下:

{ψd=Ldid+ψmψq=Lqiq \begin{cases} \psi_{\text{d}} = L_{d}i_{d}+\psi_m \\ \psi_{\text{q}} = L_{q}i_{q} \end{cases}

其中ψd为d轴磁链即直轴磁链,ψq为q轴磁链即交轴磁链,需要注意其中的Ld与id,Lq与iq的乘积也为磁链。交直轴磁链可以理解为三相的绕组经过Clark变换 Park变换后等效出了两相的绕组,且绑定在永磁体上,一相绕组与d轴平行,一相绕组与q轴平行。由于d轴绕组与永磁体磁场平行,q轴绕组与永磁体磁场垂直,所以永磁体对d轴绕组的磁链分量有贡献,而对q轴绕组的磁链分量没有贡献。所以在d轴磁链方程中会比q轴磁链方程多一项转子磁链。需要注意的是Ld和Lq都是等效值,指的是交直轴电枢反应电感,在磁路饱和时二者都会减小,特别是q轴。若忽略磁饱和,则Ld和Lq为恒定值,此时对于特定的电机,q轴的磁链仅与Iq的电流分量有关,d轴则与Id,ψm有关。

还有一个需要注意的概念是电角度,电角度是机械角度乘以极对数(p)。当p=1时,电角度等于机械角度;当转子是一个只有一对磁极的永磁体,手动将转子转过一圈时,在绕组上感应的电动势会是一个正弦波周期。当p=2时,将测得两个正弦波周期。换句话说,当转子旋转一周(360°机械角度)时,电角度变化360°×p,绕组中的电流或电压信号会变化p个周期。因此,极对数p大于1的电机旋转一周时,绕组经历的N-S极转换p次。在坐标转换中使用电角度,将参考系对齐转子位置,从而简化控制方程。

本项目使用的是表贴式永磁同步电机(Ld=Lq)故电磁转矩方程可以简化如下

Te=32pnψfiq T_{\text{e}} = \frac{3}{2}p_{n}\psi_f i_{q}

可以看到电磁转矩直接与Iq相关,在本项目中,对电机的控制可以简化为对Iq的控制,控制了电磁转矩就控制了加速度,并以此在速度控制,位置控制中得到更高的精度,关于Iq更详细的说明请参考SmartFOC-42步进文档,不同于步进电机,α-β轴没有办法直接与三相绕组相对应,需要额外的clark转换把三相的各相相差120°的电压矢量转换到α-β轴上,相应的SVPWM的流程也更加复杂。

5.4.3 控制框架

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

system

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

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

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

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

5.4.4 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.4.5 Clark变换与反Clark变换

5.4.5.1 Clark变换公式推导

FOC算法中,Clarke变换即可将电机静止的三相相电流Ia,Ib,Ic转化为静止的alpha—beta两相直角坐标系中。

system

如图可得:

{Iα=IaIbcosπ3Iccosπ3Iβ=Ibcosπ6Iccosπ6 \begin{cases} I_\alpha = I_{\text{a}} - I_{\text{b}} \cos \dfrac{\pi}{3} - I_{\text{c}} \cos \dfrac{\pi}{3} \\ I_\beta = I_{\text{b}} \cos \dfrac{\pi}{6} - I_{\text{c}} \cos \dfrac{\pi}{6} \end{cases}

将上式写成矩阵的形式,即:

[IαIβ]=ka[1121203232][IaIbIc] \begin{bmatrix} I_\alpha \\ I_\beta \end{bmatrix} = k_{\text{a}} \begin{bmatrix} 1 & -\dfrac{1}{2} & -\dfrac{1}{2} \\ 0 & \dfrac{\sqrt{3}}{2} & -\dfrac{\sqrt{3}}{2} \end{bmatrix} \begin{bmatrix} I_a \\ I_b \\ I_c \end{bmatrix}

其中 Ka 为等幅值变换系数,值为 2/3 。这里简要说明一下 2/3 从何而来。对于标准的三相电压,各相电压空间上互差 120° 并且有公式如下:

{Va=Vdccos(ωt)Vb=Vdccos(ωt2π3)ej2π3Vc=Vdccos(ωt+2π3)ej4π3 \begin{cases} \vec{V_{a}} = {V_{dc}}\cos(\omega t) \\ \vec{V_{b}}= {V_{dc}}\cos(\omega t-\frac{2\pi}{3})e^{j\frac{2\pi}{3}} \\ \vec{V_{c}} = {V_{dc}}\cos(\omega t+\frac{2\pi}{3})e^{j\frac{4\pi}{3}} \end{cases}

Va+Va+Va=32Vdcej2π3 \vec{V_{a}} +\vec{V_{a}} +\vec{V_{a}}=\frac{3}{2} {V_{dc}}e^{j\frac{2\pi}{3}}

有此式可以得出三相电压合成后的矢量的幅值会被放大 3/2 倍,所以在clark变换时乘了一个 2/3 的系数以保持幅值不变。若无特殊说明本文默认采样幅值不变系数。

在实际的控制系统中clark变换往往只输入两相电流,剩下一相通过基尔霍夫电流定律(Ia+Ib+Ic=0)得出,这里将Ic简化后得出以下公式:

{Iα=IaIβ=13×(2Ib+Ia) \begin{cases} I_\alpha = I_a \\ I_\beta = \frac{1}{\sqrt{3}} \times (2I_b + I_a) \end{cases}

5.4.5.2 Clark逆变换公式推导

在 FOC 控制算法中,Clarke 逆变换最终用于将电压矢量转换为 SVPWM 的输入。基于上文对 Clarke 变换的推导,下面来反推 Clarke 逆变换。 由上一节可得:

{Iα=IaIβ=13×(2Ib+Ia) \begin{cases} I_\alpha = I_a \\ I_\beta = \frac{1}{\sqrt{3}} \times (2I_b + I_a) \end{cases}

因此:

Iβ=13(2Ib+Iα) I_\beta = \frac{1}{\sqrt{3}} \left( 2I_b + I_\alpha \right)

3Iβ=2Ib+Iα \sqrt{3}I_\beta = 2I_b + I_\alpha

2Ib=3IβIα 2I_b = \sqrt{3}I_\beta - I_\alpha

Ib=3IβIα2 I_b = \frac{\sqrt{3}I_\beta - I_\alpha}{2}

再次根据基尔霍夫定律反推Ic后,Clarke逆变换最终可总结为:

{Ia=IαIb=3IβIα2Ic=Iα3Iβ2 \begin{cases} \mathbf{I}_a = I_\alpha \\ \mathbf{I}_b = \dfrac{\sqrt{3}\,I_\beta - I_\alpha}{2} \\ \mathbf{I}_c = \dfrac{-I_\alpha - \sqrt{3}\,I_\beta}{2} \end{cases}

5.4.6 SVPWM

本节主要讨论在三相电压源逆变器中的svpwm技术。

5.4.6.1 SVPWM合成原理

三相电压源逆变电路拓扑结构如下图所示:

system

首先定义上管为Sa、Sb、Sc。且1表示开通,0表示关断。上管开通时对应的下管关断,上管关断时对应的下管开通,故不另外定义下管状态。三个管对应八个状态。分别为000,001,010,011,100,101,110,111。各个状态对应的相电压 VAN ,VBN ,VCN 以及合成电压 Vout 如下表所示。

开关状态 VAN VBN VCN Vout
000 0 0 0 0
001 -Vdc/3 -Vdc/3 2Vdc/3 2Vdc/3e^(j4π/3)
010 -Vdc/3 2Vdc/3 -Vdc/3 2Vdc/3e^(j2π/3)
011 -2Vdc/3 Vdc/3 Vdc/3 2Vdc/3e^(jπ)
100 2Vdc/3 -Vdc/3 -Vdc/3 2Vdc/3
101 Vdc/3 -2Vdc/3 Vdc/3 2Vdc/3e^(j5π/3)
110 Vdc/3 Vdc/3 -2Vdc/3 2Vdc/3e^(jπ/3)
111 0 0 0 0

我们 Vout 根据可以得到6个非0矢量,与两个0矢量。并总结出以下公式:

{VAN=Vdc3(2sasbsc)VBN=Vdc3(2sbsasc)VCN=Vdc3(2scsasb) \begin{cases} V_{AN} = \frac{V_{dc}}{3}(2s_{a}-s_{b}-s_{c}) \\ V_{BN} = \frac{V_{dc}}{3}(2s_{b}-s_{a}-s_{c}) \\ V_{CN} = \frac{V_{dc}}{3}(2s_{c}-s_{a}-s_{b}) \end{cases}

将6个非零矢量整合在一起后我们可以得到下图

system

为了方便表达,对由六个电压矢量划分出来的扇区分别进行标号,序号为1~6。其中,正六边形为电压矢量的最大幅值边界,我们合成出来的电压矢量不能超过这个边界,六边形的内切圆及其内部为线性调制区,在这个区域内我们可以合成不失真的正弦波波形。注意其中

V1=V2=V3=V4=V5=V6=23Vdc |\vec{V_{1}}|=|\vec{V_{2}}|=|\vec{V_{3}}|=|\vec{V_{4}}|=|\vec{V_{5}}|=|\vec{V_{6}}|= \frac{2}{3}V_{dc}

SVPVM的理论基础建立在平均值等效原理之上,即在一个开关周期 TS 内,通过不同的矢量组合使其平均值与目标矢量平均值相等。

system

例如,在上图中目标矢量位于第一扇区,我们就可以选取相邻的矢量 V2 与 V1 进行合成,设V1 作用的时间为T1, V2 作用的时间为T2,0矢量作用时间为T0,则我们可以根据该原理得到下式:

TS=T1+T2+T0 T_{S} = T_{1}+T_{2}+T_{0}

TSVout=V1outT1+V2outT2+T0(V0orV7) T_{S}\vec{V_{out}} =\vec{V_{1out}} T_{1} +\vec{V_{2out}}T_{2}+T_{0}(\vec{V_{0}}or\vec{V_{7}})

{V1out=T1TSV1V2out=T2TSV2 \begin{cases} \vec{V_{1out}} = \frac{T_{1}}{T_{S}}\vec{V_{1}}\\ \vec{V_{2out}} = \frac{T_{2}}{T_{S}}\vec{V_{2}} \end{cases}

由图进一步推导得到:

{V1out=Voutsin(60θ)cos30V2out=Voutsinθcos30 \begin{cases} |\vec{V_{1out}} |= \frac{|\vec{V_{out}}|\sin(60^\circ-\theta)}{cos30^\circ}\\ |\vec{V_{2out}} |= \frac{|\vec{V_{out}}|\sin\theta}{cos30^\circ} \end{cases}

将以上公式联立可得:

{T1=3VmVdcTssin(60θ)T2=3VmVdcTssinθT0=T7=12(TsT1T2) \begin{cases} T_{1} = \sqrt{3}\frac{V_{m}}{V_{dc}}T_{s}\sin(60^\circ-\theta)\\ T_{2} = \sqrt{3}\frac{V_{m}}{V_{dc}}T_{s}\sin\theta\\ T_{0} = T_{7} =\frac{1}{2}(T_{s}-T_{1}-T_{2}) \end{cases}

这里引出svpwm中的调制比公式:

M=3VoutVdc M= \sqrt{3}\frac{V_{out}}{V_{dc}}

由以上公式可知 Vout≤2Vdc/3 若 Vout=2Vdc/3 则此时调制比为1.1547此时 Vout 位于六边形顶点上,若 Vout 位于内切圆上则调制比恰好为1。

5.4.6.2 SVPWM算法实现

在SVPWM算法的实现中我们需要注意的是此时 Vout是不存在的,我们在程序中直接接收到的是 Valpha Vbeta,所以我们无需像上文一样从 Vout 出发进行推导。第一步首先需要通过 Valpha Vbeta 来判断扇区,这里给出扇区判断公式:

{U1=uβU2=32uα12uβU3=32uα12uβ \begin{cases} U_{1} ={u_{\beta}}\\ U_{2} = \frac{\sqrt{3}}{2}{u_{\alpha}}-\frac{1}{2}u_{\beta}\\ U_{3} = -\frac{\sqrt{3}}{2}{u_{\alpha}}-\frac{1}{2}u_{\beta} \end{cases}

再定义变量A,B,C

若 U1>0 则A=1,否则A=0

若 U2>0 则B=1,否则B=0

若 U3>0 则C=1,否则C=0

令N=4C+2B+A,则有以下对应表:

扇区序号 1 2 3 4 5 6
N 3 1 5 4 6 2

接下来可以推导各个扇区的公式,这里给出三个扇区的推导过程,定义 矢量 Vx 的作用时间为Tx(x=0,1,2,3,4,5,6,7) 。

当目标矢量在扇区一时:

{Vα=T1TsV1T12TsV2Vβ=3T22TsV2 \begin{cases} V_{\alpha} = \frac{T_{1}}{T_{s}}{|\vec{V_{1}}|}-\frac{T_{1}}{2T_{s}}{|\vec{V_{2}}|} \\ V_{\beta} = \frac{\sqrt{3}T_{2}}{2T_{s}}{|\vec{V_{2}}|} \end{cases}

推导后可得:

{T1=3Ts2Vdc(3VαVβ)T2=3TsVdcVβ \begin{cases} T_{1} = \frac{\sqrt{3}T_{s}}{2V_{dc}}(\sqrt{3}V_{\alpha}-V_{\beta})\\ T_{2} = \frac{\sqrt{3}T_{s}}{V_{dc}}V_{\beta} \end{cases}

当目标矢量在扇区二时:

{Vα=T22TsV2T32TsV3Vβ=3T22TsV2+3T32TsV3 \begin{cases} V_{\alpha} = \frac{T_{2}}{2T_{s}}{|\vec{V_{2}}|}-\frac{T_{3}}{2T_{s}}{|\vec{V_{3}}|} \\ V_{\beta} = \frac{\sqrt{3}T_{2}}{2T_{s}}{|\vec{V_{2}}|}+\frac{\sqrt{3}T_{3}}{2T_{s}}{|\vec{V_{3}}|} \end{cases}

推导后可得:

{T2=3Ts2Vdc(3VαVβ)T3=3Ts2Vdc(3Vα+Vβ) \begin{cases} T_{2} = \frac{\sqrt{3}T_{s}}{2V_{dc}}(\sqrt{3}V_{\alpha}-V_{\beta})\\ T_{3} = \frac{\sqrt{3}T_{s}}{2V_{dc}}(-\sqrt{3}V_{\alpha}+V_{\beta}) \end{cases}

当目标矢量在扇区三时:

{Vα=T42TsV4T32TsV3Vβ=3T32TsV3 \begin{cases} V_{\alpha} = -\frac{T_{4}}{2T_{s}}{|\vec{V_{4}}|}-\frac{T_{3}}{2T_{s}}{|\vec{V_{3}}|} \\ V_{\beta} = \frac{\sqrt{3}T_{3}}{2T_{s}}{|\vec{V_{3}}|} \end{cases}

推导后可得:

{T3=3TsVdcVβT4=3Ts2Vdc(3Vα+Vβ) \begin{cases} T_{3} = \frac{\sqrt{3}T_{s}}{V_{dc}}V_{\beta}\\ T_{4} = \frac{\sqrt{3}T_{s}}{2V_{dc}}(-\sqrt{3}V_{\alpha}+V_{\beta}) \end{cases}

将所有扇区推导完成后,可以将作用时间的计算结果总结如下:

{X=3TsVdcVβY=3Ts2Vdc(3Vα+Vβ)Z=3Ts2Vdc(3Vα+Vβ) \begin{cases} X = \frac{\sqrt{3}T_{s}}{V_{dc}}V_{\beta}\\ Y= \frac{\sqrt{3}T_{s}}{2V_{dc}}(\sqrt{3}V_{\alpha}+V_{\beta})\\ Z= \frac{\sqrt{3}T_{s}}{2V_{dc}}(-\sqrt{3}V_{\alpha}+V_{\beta}) \end{cases}

扇区序号 1 2 3 4 5 6
N 3 1 5 4 6 2
x=1 y=2 x=2 y=3 x=3 y=4 x=4 y=5 x=5 y=6 x=6 y=1
Tx -Z Z X -X -Y Y
Ty X Y -Y Z -Z -X

T0=T7=(Ts-Tx-Ty)/2

若 Tx+Ty>Ts 则:

Tx=TxTx+TyTs T_{x}= \frac{T_{x}}{T_{x}+T_{y}}T_{s} Ty=TyTx+TyTs T_{y}= \frac{T_{y}}{T_{x}+T_{y}}T_{s}

至此我们已经计算出来各个目标矢量需要的基础矢量的作用时间,接下来我们需要安排各个基础矢量的作用顺序,其中零矢量的安排最为自由,适当安排零矢量有利于减少开关次数,降低开关损耗,本项目采样七段式SVPWM生成,七段式SVPWM中零矢量分配规律如下:每次开关只开关一相,零矢量对称分配,保证pwm中央对称,降低谐波,本项目的零矢量安排如下图所示

Vout所在扇区序号 N 三相波形图
1 3 system
2 1 system
3 5 system
4 4 system
5 6 system
6 2 system

表中可以看到零矢量一半分给了T7,一半分给了T0。我们将T7放置于中央,T0放置于边缘,这么做是有好处的,以扇区1为例,读者可以想象如果将零矢量全部分给T0那么波形图中,将总是有一相全部位于低电平,相电压将更集中于波形中央如图(b),而将T7放置于中央时则将电压分散开来如图中的(a),两种情况下电压的持续时间是相等的,但是图(a)的频率明显大于图(b),这有利于减小电流纹波,抑制转矩脉动,电机将运行的更加平稳。

system

开关时间总结如下:

定义

{Ta=TsTxTy4Tb=Ta+Tx4Tc=Tb+Ty4 \begin{cases} T_{a}= \frac{T_{s}-T_{x}-T_{y}}{4}\\ T_{b}= T_{a}+\frac{T_{x}}{4}\\ T_{c}= T_{b}+\frac{T_{y}}{4} \end{cases}

则各扇区对应的三相电压开关切换时间点,Ton1, Ton2 , Ton3 如 下表所示

扇区序号 1 2 3 4 5 6
N 3 1 5 4 6 2
Ton1 Ta Tb Tc Tc Tb Ta
Ton2 Tb Ta Ta Tb Tc Tc
Ton3 Tc Tc Tb Ta Ta Tb

Ton1,Ton2,Ton3 计算结果如下图所示,此处 Ts=4500

system

Ton1,Ton2,Ton3 计算出来后,在就可以送入微控制器中央对其模式下的pwm寄存器进行发波了,详细操作可以参见下文的代码讲解。

除了SVPWM的调制方法外,常用的还有spwm,以及spwm+三次谐波等,其中spwm+三次谐波与svpwm性能相近,且计算量更小,有兴趣的读者可以深入了解,这里不做详细介绍。

5.5 VF开环控制

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

5.6 故障检测

5.6.1 过速

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

5.6.2 过载

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

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

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

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

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

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

过载故障的简易判断:

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

5.6.3 堵转

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

5.6.4 编码器异常

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

system

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

5.6.5 缺相检测

缺相检测是指在电机运行过程中,某一相绕组由于断路或接触不良等原因导致无法正常工作,从而影响电机的正常运行。缺相会导致电机转矩下降、振动增大、温度升高等问题,严重时可能损坏电机或驱动器。因此,电机缺相检查必不可少。

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) --------------------------
    //GPIO for LED
    GPIO_InitStructure.Pin = GPIO_PIN_13 | GPIO_PIN_14;
    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 for input IO
    GPIO_InitStructure.Pin = GPIO_PIN_15;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_InitPeripheral(GPIOC, &GPIO_InitStructure);
    GPIO_InitStructure.Pin = GPIO_PIN_2;
    GPIO_InitPeripheral(GPIOA, &GPIO_InitStructure);
    GPIO_InitStructure.Pin = GPIO_PIN_5;
    GPIO_InitPeripheral(GPIOA, &GPIO_InitStructure);

    //GPIO for output IO
    GPIO_InitStructure.Pin = GPIO_PIN_6 | GPIO_PIN_7;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitPeripheral(GPIOA, &GPIO_InitStructure);

  // -------------------------- 2. PWM输出(连接逆变电路,GPIOA) --------------------------
    //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_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitPeripheral(GPIOA, &GPIO_InitStructure);
    GPIO_InitStructure.Pin =  GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15;
    GPIO_InitPeripheral(GPIOB, &GPIO_InitStructure);

   // -------------------------- 3. ADC采样(模拟信号输入,GPIOA) --------------------------
    //ADC1_IN1 VBUS PA0,ADC2_IN6 SPEED PC0
    GPIO_InitStruct(&GPIO_InitStructure);
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;

    GPIO_InitStructure.Pin = GPIO_PIN_0;
    GPIO_InitPeripheral(GPIOA, &GPIO_InitStructure);
    GPIO_InitStructure.Pin = GPIO_PIN_1;
    GPIO_InitPeripheral(GPIOA, &GPIO_InitStructure);
    GPIO_InitStructure.Pin = GPIO_PIN_3;
    GPIO_InitPeripheral(GPIOA, &GPIO_InitStructure);
    GPIO_InitStructure.Pin = GPIO_PIN_4;             //Vdc
    GPIO_InitPeripheral(GPIOA, &GPIO_InitStructure);
    GPIO_InitStructure.Pin = GPIO_PIN_1;
    GPIO_InitPeripheral(GPIOB, &GPIO_InitStructure);


    // -------------------------- 4. SPI通信(连接MT6835磁编码器,GPIOB)--------------------------
    // PB3(SCK)、PB4(MISO)、PB5(MOSI):SPI通信引脚
    //SPI==>MT6835
    GPIO_InitStructure.Pin = GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5;
    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;
    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_9;     //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_9);

    // -------------------------- 5. Modbus-RS485通信(USART + 控制引脚)-----------------------


    GPIO_InitType GPIO_InitStucture;
    USART_InitType USART_InitStucture;
    NVIC_InitType NVIC_InitStucture;

    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;
    GPIO_InitPeripheral(MODBUS_USART_GPIO, &GPIO_InitStucture);
    //GPIO接收端采用浮空输入;
    GPIO_InitStucture.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_InitStucture.Pin = MODBUS_USART_RxPin;
    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;  // 中断配置结构体

    // 复位ADC1和ADC2寄存器到默认值
    ADC_DeInit(ADC1);
    ADC_DeInit(ADC2);

    // 配置ADC1和ADC2的基本参数
    ADC_InitStruct(&ADC_InitStructure);  // 初始化结构体为默认值

    // 设置ADC工作模式:同步注入模式(ADC1和ADC2同时转换)
    ADC_InitStructure.WorkMode = ADC_WORKMODE_INJ_SIMULT;
    ADC_InitStructure.MultiChEn = ENABLE;            // 启用多通道扫描
    ADC_InitStructure.ContinueConvEn = DISABLE;      // 禁用连续转换模式
    // 设置外部触发源:TIM1捕获比较4事件触发注入转换
    ADC_InitStructure.ExtTrigSelect = ADC_EXT_TRIG_INJ_CONV_T1_CC4;
    ADC_InitStructure.DatAlign = ADC_DAT_ALIGN_R;    // 数据右对齐
    ADC_InitStructure.ChsNumber = 0;                 // 规则通道数设为0(仅使用注入通道)

    // 应用配置到ADC1和ADC2
    ADC_Init(ADC1, &ADC_InitStructure);
    ADC_Init(ADC2, &ADC_InitStructure);

    /* 配置注入通道序列和采样时间 */
    // ADC1配置3个注入通道
    ADC_ConfigInjectedSequencerLength(ADC1, 3);  // 设置注入序列长度为3
    // 通道1(位置1):Vbus电压检测,28.5周期采样时间
    ADC_ConfigInjectedChannel(ADC1, ADC_CH_1, 1, ADC_SAMP_TIME_28CYCLES5);
    // 通道2(位置2):W相电流,28.5周期采样时间
    ADC_ConfigInjectedChannel(ADC1, ADC_CH_2, 2, ADC_SAMP_TIME_28CYCLES5);
    // 通道4(位置3):V相电流,28.5周期采样时间
    ADC_ConfigInjectedChannel(ADC1, ADC_CH_4, 3, ADC_SAMP_TIME_28CYCLES5);

    // ADC2配置2个注入通道
    ADC_ConfigInjectedSequencerLength(ADC2, 2);  // 设置注入序列长度为2
    // 通道1(位置1):直流母线电压,28.5周期采样时间
    ADC_ConfigInjectedChannel(ADC2, ADC_CH_1, 1, ADC_SAMP_TIME_28CYCLES5);
    // 通道3(位置2):温度检测,28.5周期采样时间
    ADC_ConfigInjectedChannel(ADC2, ADC_CH_3, 2, ADC_SAMP_TIME_28CYCLES5);

    // 启用ADC1和ADC2的外部触发注入转换功能
    ADC_EnableExternalTrigInjectedConv(ADC1, ENABLE);
    ADC_EnableExternalTrigInjectedConv(ADC2, ENABLE);

    /* 配置中断控制器 */
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);  // 设置优先级分组(2位抢占优先级)
    NVIC_InitStructure.NVIC_IRQChannel = ADC1_2_IRQn;         // 选择ADC1&2中断通道
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; // 抢占优先级2
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;        // 子优先级1
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;           // 启用中断通道
    NVIC_Init(&NVIC_InitStructure);

    /* 启用ADC并等待准备就绪 */
    ADC_Enable(ADC1, ENABLE);
    while(ADC_GetFlagStatusNew(ADC1, ADC_FLAG_RDY) == RESET); // 等待ADC1就绪
    ADC_Enable(ADC2, ENABLE);
    while(ADC_GetFlagStatusNew(ADC2, ADC_FLAG_RDY) == RESET); // 等待ADC2就绪

    /* ADC校准流程 */
    ADC_StartCalibration(ADC1);  // 启动ADC1校准
    while(ADC_GetCalibrationStatus(ADC1)); // 等待校准完成
    ADC_StartCalibration(ADC2);  // 启动ADC2校准
    while(ADC_GetCalibrationStatus(ADC2)); // 等待校准完成

    /* 启用中断事件 */
    // 使能注入转换结束中断和模拟看门狗中断
    ADC_ConfigInt(ADC1, ADC_INT_JENDC | ADC_INT_AWD, ENABLE);
    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 初始化代码:



//modbus DMA配置
#define MODBUS_DMA                    DMA1
#define MODBUS_DMA_RX_Channel       DMA1_CH3
#define MODBUS_DMA_TX_Channel       DMA1_CH2
#define MODBUS_DMA_TX_REMAP         DMA1_REMAP_USART3_TX
#define MODBUS_DMA_RX_REMAP         DMA1_REMAP_USART3_RX
#define MODBUS_DMA_CLK                 RCC_AHB_PERIPH_DMA1

//DMA发送完成中断配置
#define MODBUS_DMA_TX_IRQn             DMA1_Channel2_IRQn
#define MODBUS_DMA_TX_INT_TXC         DMA1_INT_TXC2
#define MODBUS_DMA_TX_IRQHandler    DMA1_Channel2_IRQHandler


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

    // 使能DMA时钟(MODBUS_DMA_CLK是宏定义的DMA时钟,RCC_AHB_PERIPH_DMA1)
    RCC_EnableAHBPeriphClk(MODBUS_DMA_CLK, ENABLE);

    // 复位DMA通道到默认状态
    DMA_DeInit(MODBUS_DMA_TX_Channel);  // 复位发送通道DMA1_CH3
    DMA_DeInit(MODBUS_DMA_RX_Channel);  // 复位接收通道DMA1_CH2

    // 初始化DMA结构体为默认值
    DMA_StructInit(&DMA_InitStruct);

    /******************** 配置USART发送DMA通道 ********************/
    // 目标外设地址:USART数据寄存器地址
    DMA_InitStruct.PeriphAddr = (uint32_t)&(MODBUS_USART->DAT);  
    // 内存源地址:发送数据缓冲区
    DMA_InitStruct.MemAddr = (uint32_t)modbus_send_data;  
    // 传输方向:内存到外设(发送方向)
    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位)
    DMA_InitStruct.PeriphDataSize = DMA_PERIPH_DATA_SIZE_BYTE;  
    // 内存数据宽度:字节(8位)
    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请求重映射(将USART发送请求映射到指定DMA通道DMA1_REMAP_USART3_TX)
    DMA_RequestRemap(MODBUS_DMA_TX_REMAP, MODBUS_DMA, MODBUS_DMA_TX_Channel, ENABLE);

    /******************** 配置USART接收DMA通道 ********************/
    // 源外设地址:USART数据寄存器地址
    DMA_InitStruct.PeriphAddr = (uint32_t)&(MODBUS_USART->DAT);  
    // 内存目标地址:接收数据缓冲区
    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请求重映射(将USART接收请求映射到指定DMA通道DMA1_REMAP_USART3_RX)
    DMA_RequestRemap(MODBUS_DMA_RX_REMAP, MODBUS_DMA, MODBUS_DMA_RX_Channel, ENABLE);

    /******************** 配置DMA发送完成中断 ********************/
    // 设置NVIC中断通道
    NVIC_InitStruct.NVIC_IRQChannel = MODBUS_DMA_TX_IRQn;  
    // 使能中断通道
    NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;  
    // 抢占优先级0(最高)
    NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0;  
    // 子优先级0
    NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;  
    // 应用中断配置
    NVIC_Init(&NVIC_InitStruct);

    // 使能DMA发送完成中断
    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,脉冲宽度调制)是三相伺服电机矢量控制系统的执行中枢,通过精确控制逆变器桥臂开关管的导通 / 关断时间,调整方波占空比将直流母线电压等效转换为三相正弦交流电压,驱动电机产生旋转磁场。其核心价值在于:用数字电路实现模拟电压调节,配合 FOC 算法中的坐标变换,实现对电机磁链和转矩的毫秒级精准控制。其工作原理如下图所示:

system

三相伺服电机驱动需 6 路 PWM 信号(每相上下桥臂各 1 路),对应定时器的 3 组互补通道(如 TIM1_CH1/CH1N、CH2/CH2N、CH3/CH3N)。PWM的核心配置依赖三类结构体,分别控制时基、输出比较及刹车 / 死区参数。下面进行一一解析:

(1)TIM_TimeBaseInitType结构体

配置定时器的时基参数,是PWM波形生成的“时间基准”。它决定了PWM的频率、计数模式(对称/非对称)、时钟精度等基础特性,直接影响PWM波形的周期和对称性,是整个PWM系统的节拍器。

参数成员 作用说明 本项目配置及原因
Prescaler 预分频器,降低定时器时钟频率(时钟频率 = 定时器时钟 / (Prescaler + 1)) 0(不分频):保留高频时钟输入(如144MHz),确保PWM频率≥10kHz,减少电机运行噪声。
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结构体

功能:配置输出比较参数,控制PWM输出的波形。它决定了PWM的占空比、输出极性(高/低有效)、互补通道逻辑等关键特性,直接定义PWM波形的具体形态,是将电压指令转换为脉冲信号的核心。

参数成员 作用说明 本项目配置及原因
OcMode PWM输出模式(定义计数器与比较值的关系) TIM_OCMODE_PWM1:计数器值 < 比较值(Pulse)时输出有效电平,符合常规控制逻辑。
OutputState 主通道PWM输出使能控制 初始TIM_OUTPUT_STATE_DISABLE(禁用):避免初始化时误输出PWM导致电机意外转动,后续通过TIM_EnableCtrlPwmOutputs统一使能。
OutputNState 互补通道PWM输出使能控制(对应半桥上下管) 初始TIM_OUTPUT_NSTATE_DISABLE(禁用):同主通道,统一控制使能时机。
Pulse 比较值,决定PWM占空比(占空比 = Pulse / Period × 100%) TimerPeriod >> 1(周期的1/2):初始占空比50%,对应0电压矢量(电机无转矩输出)。
OcPolarity 主通道有效电平(高/低电平为有效) TIM_OC_POLARITY_HIGH(高有效):主通道高电平时开关管导通,匹配功率器件驱动逻辑。
OcNPolarity 互补通道有效电平 TIM_OCN_POLARITY_HIGH(高有效):与主通道互补,确保上下桥臂不同时导通。
OcIdleState 定时器停止时主通道输出电平 TIM_OC_IDLE_STATE_RESET(低电平):停止时关断主通道,避免电机绕组持续通电。
OcNIdleState 定时器停止时互补通道输出电平 TIM_OC_IDLE_STATE_RESET(低电平):停止时关断互补通道,保护功率器件。

3. TIM_BDTRInitType结构体

功能:配置刹车和死区参数,维持PWM系统的安全。它通过死区时间防止上下桥臂直通短路,通过刹车功能在过流等故障时紧急关断PWM,为功率器件和电机提供硬件级保护,是系统可靠性的核心保障。

参数成员 作用说明 本项目配置及原因
OssrState 运行模式下关闭状态使能(稳定未使用通道的输出) TIM_OSSR_STATE_ENABLE:使能OSSR,避免未用PWM通道输出异常电平,提升电路稳定性。
OssiState 空闲模式下关闭状态使能(锁定定时器停止时的输出电平) TIM_OSSI_STATE_ENABLE:使能OSSI,定时器停止时PWM通道锁定为低电平,确保安全。
LockLevel 参数锁定级别(防止意外修改PWM配置) TIM_LOCK_LEVEL_OFF(无锁定):允许动态修改PWM参数(如FOC实时调整占空比),适配控制需求。
DeadTime 死区时间(上下桥臂切换时的安全间隔,防止同时导通) 0x72(约2μs):根据功率器件开关速度配置,确保上管关断后下管再导通(或反之),避免桥臂直通短路。
Break 刹车输入使能(外部保护信号触发PWM紧急关断) TIM_BREAK_IN_ENABLE(使能):允许外部电路(如电流传感器)触发刹车,紧急关断PWM,保护电机和逆变器。
BreakPolarity 刹车信号极性(高/低电平有效) TIM_BREAK_POLARITY_HIGH(高有效):兼容常用过流保护芯片的输出逻辑(过流时输出高电平)。
AutomaticOutput 自动输出使能(定时器启动后是否自动输出PWM) TIM_AUTO_OUTPUT_DISABLE(禁用):需通过TIM_EnableCtrlPwmOutputs手动使能,增强启动阶段安全性。
IomBreakEn I/O主模块刹车使能(是否允许I/O模块触发刹车) false(禁用):仅依赖外部硬件保护信号,简化逻辑。

学习了PWM工作原理与基础配置,下面进入实际运用。

本项目基于高级定时器 TIM1 实现三相互补 PWM 输出,集成死区时间控制(防止上下桥臂直通短路)、硬件刹车保护(过流时紧急关断)等安全机制,完美适配三相全桥逆变电路,满足伺服电机高精度、高可靠性的控制需求。具体代码如下:

void Pwm_Init(void)
{
    TIM_TimeBaseInitType TIM1_TimeBaseStructure;   // 定时器时基配置结构体
    OCInitType TIM1_OCInitStructure;               // 输出比较配置结构体
    TIM_BDTRInitType TIM1_BDTRInitStructure;       // 刹车和死区配置结构体

    // 计算定时器周期值(PWM频率 = 主频 / (2 * (TimerPeriod + 1))
    uint16_t TimerPeriod = (MAIN_FREQUENCY / (PWM_FREQUENCY1 * 2)) - 1;

    /******************** 定时器时基配置 ********************/
    TIM_DeInit(TIM1);  // 复位TIM1寄存器到默认值
    TIM_InitTimBaseStruct(&TIM1_TimeBaseStructure);  // 初始化时基结构体

    // 配置时基参数
    TIM1_TimeBaseStructure.Prescaler = 0;          // 预分频器设为0(不分频)
    TIM1_TimeBaseStructure.CntMode = TIM_CNT_MODE_CENTER_ALIGN1;  // 中心对齐模式1(先递增后递减)
    TIM1_TimeBaseStructure.Period = TimerPeriod;   // 自动重装载值(决定PWM频率)
    TIM1_TimeBaseStructure.ClkDiv = TIM_CLK_DIV1;  // 时钟分频(不分频)
    TIM1_TimeBaseStructure.RepetCnt = 0;           // 重复计数器(不使用)

    TIM_InitTimeBase(TIM1, &TIM1_TimeBaseStructure);  // 应用时基配置

    /******************** PWM通道配置(通道1-3) ********************/
    TIM_InitOcStruct(&TIM1_OCInitStructure);  // 初始化输出比较结构体

    // 配置PWM模式参数
    TIM1_OCInitStructure.OcMode = TIM_OCMODE_PWM1;  // PWM模式1(CNT < CCR时有效)
    TIM1_OCInitStructure.OutputState = TIM_OUTPUT_STATE_DISABLE;    // 主输出禁用(暂时)
    TIM1_OCInitStructure.OutputNState = TIM_OUTPUT_NSTATE_DISABLE; // 互补输出禁用(暂时)
    TIM1_OCInitStructure.Pulse = (TimerPeriod >> 1);  // 初始占空比50%(CCR值)
    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; // 互补空闲状态低电平

    // 应用配置到通道1-3(通常用于三相电机控制)
    TIM_InitOc1(TIM1, &TIM1_OCInitStructure);  // 通道1配置
    TIM_InitOc2(TIM1, &TIM1_OCInitStructure);  // 通道2配置
    TIM_InitOc3(TIM1, &TIM1_OCInitStructure);  // 通道3配置

    /******************** 通道4配置(用于触发ADC采样) ********************/
    TIM1_OCInitStructure.OutputState = TIM_OUTPUT_STATE_ENABLE;  // 启用通道4输出
    TIM1_OCInitStructure.Pulse = TimerPeriod - 200;  // 设置比较值(决定ADC触发位置)
    TIM_InitOc4(TIM1, &TIM1_OCInitStructure);  // 应用配置到通道4

    /******************** 启用预加载寄存器 ********************/
    // 使能自动重装载预加载(避免更新时产生毛刺)
    TIM_ConfigOc1Preload(TIM1, TIM_OC_PRE_LOAD_ENABLE);
    TIM_ConfigOc2Preload(TIM1, TIM_OC_PRE_LOAD_ENABLE);
    TIM_ConfigOc3Preload(TIM1, TIM_OC_PRE_LOAD_ENABLE);

    /******************** 刹车和死区配置(电机控制关键) ********************/
    TIM1_BDTRInitStructure.OssrState = TIM_OSSR_STATE_ENABLE;         // 运行模式下关闭状态启用
    TIM1_BDTRInitStructure.OssiState = TIM_OSSI_STATE_ENABLE;         // 空闲模式下关闭状态启用
    TIM1_BDTRInitStructure.LockLevel = TIM_LOCK_LEVEL_OFF;            // 无锁定(可配置)
    TIM1_BDTRInitStructure.DeadTime = 0x72;                           // 死区时间(防止上下管直通)
    TIM1_BDTRInitStructure.Break = TIM_BREAK_IN_ENABLE;               // 使能刹车输入
    TIM1_BDTRInitStructure.BreakPolarity = TIM_BREAK_POLARITY_HIGH;   // 刹车高电平有效
    TIM1_BDTRInitStructure.AutomaticOutput = TIM_AUTO_OUTPUT_DISABLE; // 禁用自动输出
    TIM1_BDTRInitStructure.IomBreakEn = false;                        // 禁用I/O主刹车

    TIM_ConfigBkdt(TIM1, &TIM1_BDTRInitStructure);  // 应用刹车和死区配置

    /******************** 启动定时器和PWM输出 ********************/
    TIM_Enable(TIM1, ENABLE);  // 使能TIM1计数器
    TIM_EnableCtrlPwmOutputs(TIM1, ENABLE);  // 使能主输出(PWM信号实际输出)
}

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;  // 中断配置结构体

    /* 配置并启用SPI3中断 */
    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_InitType SPI_InitStructure;  // SPI配置结构体

    /* SPI3主控制器初始化配置 */
    SPI_InitStructure.DataDirection = SPI_DIR_DOUBLELINE_FULLDUPLEX;  // 全双工模式
    SPI_InitStructure.SpiMode = SPI_MODE_MASTER;          // 主机模式
    SPI_InitStructure.DataLen = SPI_DATA_SIZE_16BITS;     // 16位数据长度(MT6835要求)
    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; // 波特率预分频(系统时钟/16)
    SPI_InitStructure.FirstBit = SPI_FB_MSB;              // 高位先传输(MSB first)
    SPI_InitStructure.CRCPoly = 7;                        // CRC多项式(未使用)

    // 应用SPI配置
    SPI_Init(SPI3, &SPI_InitStructure);

    /* 启用接收中断 */
    // 使能RX非空中断(接收寄存器有数据时触发)
    SPI_I2S_EnableInt(SPI3, SPI_I2S_INT_RNE, ENABLE);

    /* 启用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、ic;
  • ② 对ia、ib、ic进行 Clark 变换,得到 αβ 坐标系下的电流分量:iα、iβ;
  • ③ 对 iα、iβ 进行 Park 变换,得到 dq 坐标系下的电流分量:id、iq;
  • ④ 计算 id、iq 与设定目标值id_ref、iq_ref的误差;
  • ⑤ 将误差输入 PI 控制器计算出 id、iq 的控制输出:ud、uq;
  • ⑥ 对 ud、uq 进行反 Park 变换,得到 αβ 坐标系下的电压分量:uα、uβ;
  • ⑦ 对 uα、uβ 进行反 Clark 变换,得到三相电压分量:ua、ub、uc;
  • ⑧ 将 ua、ub、uc 输入 SVPWM 模块进行调制,合成电压空间矢量,输出该时刻三个半桥的开关状态进而控制电机旋转;
  • ⑨ 更新电机转子位置角度,重复上述流程。

6.2.2 Clark变换与Park变换

在交流电机控制领域,坐标变换是简化控制算法的核心技术。其中 Clark 变换与 Park 变换常被联合使用,实现从三相静止坐标系到两相旋转坐标系的转换,为电机的矢量控制奠定基础。

6.2.2.1 Clark变换

Clark 变换(克拉克变换)的核心功能是将三相静止坐标系(abc)下的电流(或电压)转换为两相静止坐标系(αβ)下的分量,通过基尔霍夫电流定律(Ic+Ia+Ib=0)简化公式。其数学表达式如下:

{Iα=IaIβ=13×(2Ib+Ia) \begin{cases} I_\alpha = I_a \\ I_\beta = \frac{1}{\sqrt{3}} \times (2I_b + I_a) \end{cases}

式中,Ia、Ib为三相电流中的 A 相和 B 相分量),Iα、Iβ为转换后 αβ 坐标系下的电流分量。

6.2.2.2 Park变换

Park 变换(派克变换)用于将两相静止坐标系(αβ)下的分量进一步转换为跟随转子旋转的 dq 坐标系下的分量,从而将交流量转化为直流量,便于采用直流电机的控制思路进行调节。其数学表达式如下:

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

式中,θe为电机转子的电角度,id、iq分别为 dq 旋转坐标系下的直轴和交轴电流分量。

在实际工程实现中,通常将 Clark 变换与 Park 变换串联执行,直接从三相电流得到 dq 坐标系下的分量。对应的代码实现如下:

void clarke(SVPVM *v)
{
    short Cosine, Sine;
    short tmpAngle;
    short tmp1 = 0;
    short tmp2 = 0;
    tmpAngle = v->Angle2;
    Sine   = _IQ15sinPU(tmpAngle); //0--32767
    Cosine = _IQ15cosPU(tmpAngle);
    //clark变换:abc→αβ
    // 计算α分量(直接等于A相)
    tmp1 = v->As;    
    // 计算β分量:Beta = (2*Bs + As) / √3,1/sqrt(3) = 0.57735026918963
    tmp2 = _IQ15mpy((v->As + _IQ15mpy(_IQ15(2), v->Bs)), _IQ15(0.57735026918963)); 

    //park变换:αβ→dq
    v->IDs = _IQ15mpy(tmp1, Cosine) + _IQ15mpy(tmp2, Sine); // 计算直轴电流i_d
    v->IQs = _IQ15mpy(tmp2, Cosine) - _IQ15mpy(tmp1, Sine); // 计算交轴电流i_q
}

6.2.3 反PARK变换与反CLARK变换

在电机矢量控制系统中,反 Park 变换与反 Clark 变换是坐标变换的逆过程,用于将旋转坐标系下的控制量转换回三相静止坐标系,最终实现对电机的精确控制。

6.2.3.1 反PARK变换

反 Park 变换(Inverse Park Transform)是 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}

式中,​id、iq为 dq 坐标系下的直轴和交轴分量,θe为电机转子电角度,iα、iβ为转换后 αβ 坐标系下的分量。 在工程实现中,反 Park 变换常需结合电压幅值限制(防止功率器件过调制),代码实现如下:

short tmpDs, tmpQs;  // 临时变量:限幅后的dq轴电压
void ipark(SVPVM *v)
{
    short Cosine, Sine;
    short tmpAngle;

    // 1. 获取电角度并计算三角函数值
    tmpAngle = v->Angle; //0--32767
    Sine = _IQ15sinPU(tmpAngle);
    Cosine = _IQ15cosPU(tmpAngle);

    // 2. 直流侧电压系数校正
    tmpQs = _IQ12mpy(v->UQs, v->DcCoeff);
    tmpDs = _IQ12mpy(v->UDs, v->DcCoeff);

    // 3. 电压限幅:防止功率器件过调制
    if(tmpQs > 30500)// _IQ15(0.9))
    {
        tmpQs = 30500;//935
    }
    else if(tmpQs < -30500)// _IQ15(0.9))
    {
        tmpQs = -30500;//935
    }
    if(tmpDs > 6500)
    {
        tmpDs = 6500;
    }
    else if(tmpDs < -6500)
    {
        tmpDs = -6500;
    }
    // 4. 执行反Park变换
    v->Ualpha = _IQ15mpy(tmpDs, Cosine) - _IQ15mpy(tmpQs, Sine);
    v->Ubeta  = _IQ15mpy(tmpQs, Cosine) + _IQ15mpy(tmpDs, Sine);
}

6.2.3.2 反Clark变换

反 Clark 变换(Inverse Clark Transform)是 Clark 变换的逆过程,主要用于将 αβ 静止坐标系下的分量转换为 abc 三相静止坐标系下的分量,是空间矢量脉宽调制(SVPWM)的关键步骤。其数学表达式如下:

{ia=Iαib=3IβIα2ic=Iα3Iβ2 \begin{cases} \mathbf{i}_a = I_\alpha \\ \mathbf{i}_b = \dfrac{\sqrt{3}\,I_\beta - I_\alpha}{2} \\ \mathbf{i}_c = \dfrac{-I_\alpha - \sqrt{3}\,I_\beta}{2} \end{cases}

式中,Iα、Iβ为 αβ 坐标系下的分量,ia、ib、ic为转换后三相坐标系下的分量。

反 Clark 变换在 SVPWM 中的代码实现如下:

void svgendq(SVPVM *v)
{
    short Va, Vb, Vc, t1, t2;   // 三相电压输出
    short Sector = 0;  // Sector is treated as Q0 - independently with global Q

    // 1. 反Clark变换:将αβ电压转换为三相电压
    Va = v->Ubeta;
    Vb = _IQ15mpy(_IQ15(-0.5), v->Ubeta) + _IQ15mpy(_IQ15(0.8660254), v->Ualpha); // 0.8660254 = sqrt(3)/2
    Vc = _IQ15mpy(_IQ15(-0.5), v->Ubeta) - _IQ15mpy(_IQ15(0.8660254), v->Ualpha); // 0.8660254 = sqrt(3)/2

}
    // 2. SVPWM扇区判断与矢量作用时间计算,此处省略

6.2.4 SVPWM

以下是三相表贴式永磁同步电机的svpwm代码,计算公式可以参考上文的理论推导部分的计算公式。

void svgendq(SVPVM *v)
{
    short Va, Vb, Vc, t1, t2;      // 中间变量:变换后的电压分量和占空比分量
    short Sector = 0;               // 扇区号(0-6)

    /* 步骤1:计算三个参考电压分量(用于扇区判断)*/
    Va = v->Ubeta;  // β分量直接作为Va
    // Vb = -0.5*Ubeta + (√3/2)*Ualpha
    Vb = _IQ15mpy(_IQ15(-0.5), v->Ubeta) + _IQ15mpy(_IQ15(0.8660254), v->Ualpha);
    // Vc = -0.5*Ubeta - (√3/2)*Ualpha
    Vc = _IQ15mpy(_IQ15(-0.5), v->Ubeta) - _IQ15mpy(_IQ15(0.8660254), v->Ualpha);

    /* 步骤2:确定60度扇区(基于参考电压的符号)*/
    if(Va > _IQ15(0)) Sector |= 1;  // Va>0 -> 位0置1
    if(Vb > _IQ15(0)) Sector |= 2;  // Vb>0 -> 位1置1
    if(Vc > _IQ15(0)) Sector |= 4;  // Vc>0 -> 位2置1
    // 扇区号 = 位2|位1|位0 (范围1-6,0表示零矢量)

    /* 步骤3:重新计算用于占空比生成的电压分量 */
    Va = v->Ubeta;  // X分量 = Ubeta
    // Y分量 = 0.5*Ubeta + (√3/2)*Ualpha
    Vb = _IQ15mpy(_IQ15(0.5), v->Ubeta) + _IQ15mpy(_IQ15(0.8660254), v->Ualpha);
    // Z分量 = 0.5*Ubeta - (√3/2)*Ualpha
    Vc = _IQ15mpy(_IQ15(0.5), v->Ubeta) - _IQ15mpy(_IQ15(0.8660254), v->Ualpha);

    /* 步骤4:根据扇区计算基本矢量作用时间 */
    if(Sector == 0) {
        // 零矢量情况:所有相50%占空比
        v->Ta = _IQ15(0.5);
        v->Tb = _IQ15(0.5);
        v->Tc = _IQ15(0.5);
    }
    else if(Sector == 1) {  // 扇区1: 0-60度
        t1 = Vc;           // 基本矢量1作用时间
        t2 = Vb;           // 基本矢量2作用时间
        v->Tb = _IQ15mpy(_IQ15(0.5), (_IQ15(1) - t1 - t2));  // Tb = (1-t1-t2)/2
        v->Ta = v->Tb + t1;  // Ta = Tb + t1
        v->Tc = v->Ta + t2;  // Tc = Ta + t2
    }
    else if(Sector == 2) {  // 扇区2: 60-120度
        t1 = Vb;
        t2 = -Va;
        v->Ta = _IQ15mpy(_IQ15(0.5), (_IQ15(1) - t1 - t2));
        v->Tc = v->Ta + t1;
        v->Tb = v->Tc + t2;
    }
    else if(Sector == 3) {  // 扇区3: 120-180度
        t1 = -Vc;
        t2 = Va;
        v->Ta = _IQ15mpy(_IQ15(0.5), (_IQ15(1) - t1 - t2));
        v->Tb = v->Ta + t1;
        v->Tc = v->Tb + t2;
    }
    else if(Sector == 4) {  // 扇区4: 180-240度
        t1 = -Va;
        t2 = Vc;
        v->Tc = _IQ15mpy(_IQ15(0.5), (_IQ15(1) - t1 - t2));
        v->Tb = v->Tc + t1;
        v->Ta = v->Tb + t2;
    }
    else if(Sector == 5) {  // 扇区5: 240-300度
        t1 = Va;
        t2 = -Vb;
        v->Tb = _IQ15mpy(_IQ15(0.5), (_IQ15(1) - t1 - t2));
        v->Tc = v->Tb + t1;
        v->Ta = v->Tc + t2;
    }
    else if(Sector == 6) {  // 扇区6: 300-360度
        t1 = -Vb;
        t2 = -Vc;
        v->Tc = _IQ15mpy(_IQ15(0.5), (_IQ15(1) - t1 - t2));
        v->Ta = v->Tc + t1;
        v->Tb = v->Ta + t2;
    }

    /* 步骤5:将占空比从[0,1]转换为[-1,1]范围 */
    // 转换公式:Duty = 2*(Duty - 0.5)
    v->Ta = _IQ15mpy(_IQ15(2.0), (v->Ta - _IQ15(0.5)));
    v->Tb = _IQ15mpy(_IQ15(2.0), (v->Tb - _IQ15(0.5)));
    v->Tc = _IQ15mpy(_IQ15(2.0), (v->Tc - _IQ15(0.5)));
}



void PWM(SVPVM *v)//该函数的功能是将SVPWM模块输出的三相占空比(Ta, Tb, Tc)转换为实际的PWM比较寄存器值。代码中使用了Q15格式的定点数运算。
{
    short MPeriod;  // 计算得到的实际PWM周期值(Q0格式)
    int Tmp;        // 临时变量用于中间计算(32位防止溢出)

    /* 步骤1:传递SVPWM计算得到的三相占空比 */
    // 将SVPWM输出的Ta,Tb,Tc(Q15格式,范围[-1,1])赋值给Mfunc变量
    v->MfuncC1 = v->Ta;  // 相位A占空比指令
    v->MfuncC2 = v->Tb;  // 相位B占空比指令
    v->MfuncC3 = v->Tc;  // 相位C占空比指令

    /* 步骤2:计算实际PWM周期值(中心对齐PWM需要)*/
    // 计算:MPeriod = PeriodMax * MfuncPeriod + PeriodMax/2
    Tmp = (int)v->PeriodMax * (int)v->MfuncPeriod;  // 32位乘法(Q0 * Q15 → 32位Q15)
    // 转换为Q0格式:右移16位取高16位 + 周期最大值的一半(中心对齐偏移)
    MPeriod = (short)(Tmp >> 16) + (short)(v->PeriodMax >> 1); 

    /* 步骤3:计算相位A的比较寄存器值 */
    // 计算:Va = MPeriod * MfuncC1 + MPeriod/2
    Tmp = (int)MPeriod * (int)v->MfuncC1;  // PWM周期 × 占空比系数(Q0 * Q15)
    // 转换为Q0格式:右移16位取高16位 + 周期值的一半(中心点基准)
    v->Va = (short)(Tmp >> 16) + (short)(MPeriod >> 1); 

    /* 步骤4:计算相位B的比较寄存器值 */
    Tmp = (int)MPeriod * (int)v->MfuncC2;  // 同样计算过程
    v->Vb = (short)(Tmp >> 16) + (short)(MPeriod >> 1);

    /* 步骤5:计算相位C的比较寄存器值 */
    Tmp = (int)MPeriod * (int)v->MfuncC3;  // 同样计算过程
    v->Vc = (short)(Tmp >> 16) + (short)(MPeriod >> 1);

}
    //计算后的数值直接写入寄存器
    TIM1->CCDAT1 = svpwm.Va; // U相占空比
    TIM1->CCDAT2 = svpwm.Vb; // V相占空比
    TIM1->CCDAT3 = svpwm.Vc; // W相占空比

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

system

所以在pwm生成中往往会加入死区时间,我们可以使用N32G452CCL7提供的死区生成结构体来实现这个死区时间 system

6.2.5 PI控制器

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

void pidIqs_calc(PIDIqs *v)
{
    // 计算当前误差 = 参考值 - 反馈值
    v->Err = v->Ref - v->Fdb;

    // 计算比例项输出:Up = Kp * Err
    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; 
    }

    /******************** 防止方向反转的特殊处理 ********************/
    // 情况1:期望正转但实际反转(堵转或异常情况)
    if((pidpv.Ref >= 0) && (MotorControler.SpeedFdbp < 0))
    {
        // 若积分项为负(反转方向),清零积分
        if(temp_Ui < 0)
        {
            temp_Ui = 0;
        }
        // 若比例项为负(反转方向),清零比例
        if(v->Up < 0)
        {
            v->Up = 0;
        }
    }
    // 情况2:期望反转但实际正转
    else if((pidpv.Ref < 0) && (MotorControler.SpeedFdbp > 0))
    {
        // 若积分项为正(正转方向),清零积分
        if(temp_Ui > 0)
        {
            temp_Ui = 0;
        }
        // 若比例项为正(正转方向),清零比例
        if(v->Up > 0)
        {
            v->Up = 0;
        }
    }

    // 将积分项转换为Q15格式(右移15位)
    v->Ui = ((temp_Ui) >> 15);

    // 合成PID输出:比例项 + 积分项(Ki已包含在积分系数中)
    v->OutPreSat = v->Up + v->Ki * v->Ui;  // 注意:Ki通常较小以限制积分影响

    // 输出限幅处理
    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 位置环速度规划

位置环主要由四个部分组成:曲线规划、期望参数的计算、位置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.7 开环VF控制器

通过5.5开环控制器的学习,其实开环 VF 控制(电压 - 频率比控制)的核心思想是保持输出电压与频率的比例恒定(V/F = 常数),使电机气隙磁通基本不变,实现平稳调速。下面进入项目实际运用。

本项目的开环 VF 控制器核心控制逻辑就三步:

(1)步长调节:根据目标速度,动态调整 “电角度步长”(VF_ElectricalAngleStep),控制电机加速 / 减速 / 停止;

(2)电角度更新:以步长为增量,实时计算电机转子电角度(VF_ElectricalAngle),模拟旋转磁场;

(3)电压计算:基于 “步长 - 电压” 经验公式,输出与频率匹配的电压(VF_Voltage),保持 V/F 比基本恒定。

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


case 1 :         // 开环VF运行模式
{
    svpwm.SvpwmControlState = 1;  // 标记SVPWM进入VF控制模式

    // 步长更新计数器:累计到100时才调整步长(分频控制,避免步长频繁波动)
    SystemVar.VF_ElectricalAngleStepCount++;

    // 每累计100次更新一次步长(控制步长变化频率,如10kHz中断下,100次对应1ms更新一次)
    if(SystemVar.VF_ElectricalAngleStepCount >= 100)
    {
        SystemVar.VF_ElectricalAngleStepCount = 0;  // 重置计数器

        // 1. 加速逻辑:步长小于最大值,且未设置减速(min=0),则步长+1(增速)
        if((SystemVar.VF_ElectricalAngleStep < SystemVar.VF_ElectricalAngleStepMax) && (SystemVar.VF_ElectricalAngleStepMin == 0))
        {
            SystemVar.VF_ElectricalAngleStep++;  // 步长增加(速度提高)
            VF_Dir = 1;  // 标记正转方向
        }

        // 2. 减速逻辑:步长大于最小值,且未设置加速(max=0),则步长-1(减速)
        if((SystemVar.VF_ElectricalAngleStep > SystemVar.VF_ElectricalAngleStepMin) && (SystemVar.VF_ElectricalAngleStepMax == 0))
        {
            SystemVar.VF_ElectricalAngleStep--;  // 步长减小(速度降低)
            VF_Dir = -1;  // 标记反转方向
        }

        // 3. 停止逻辑1:同时设置了最大和最小步长(可能是急停指令),步长归0
        if((SystemVar.VF_ElectricalAngleStepMax != 0) && (SystemVar.VF_ElectricalAngleStepMin != 0))
        {
            SystemVar.VF_ElectricalAngleStep = 0;  // 步长为0(停止)
            SystemVar.VF_ElectricalAngle = 0;  // 电角度复位
            VF_Dir = 0;  // 无方向
            SystemVar.VF_Voltage = 0;  // 电压归0(停止输出)
        }

        // 4. 停止逻辑2:未设置最大和最小步长(默认停止状态),参数归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;
        }
        // 注:此处通过限幅逻辑避免步长或电压过大,保护电机和驱动电路
    }

    // 5. 电角度更新:根据步长计算实时电角度(模拟转子旋转)
    // 27.31f:步长到电角度的转换系数(步长对应速度,转换为每周期的角度增量)
    // & 0x7FFF:将角度限制在0~32767(对应0~360°电角度,15位无符号范围)
    SystemVar.VF_ElectricalAngle = (SystemVar.VF_ElectricalAngle + ((short)((((float)SystemVar.VF_ElectricalAngleStep)) * 27.31f))) & 0x7FFF;

    // 6. 电压计算:基于经验公式,保持V/F比基本恒定,同时补偿方向
    // (步长>>1):步长分频(降低电压随步长的变化率)
    // SystemVar.VF_Coefficient:比例系数(控制V/F比)
    // SystemVar.VF_Coefficient_B:偏移补偿(低速时提高电压,避免转矩不足)
    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;
        }
    }
}

6.3.4 缺相检查

本控制系统采用的缺相检查逻辑为:

  • 计算三相电流(ImeasA、ImeasB、ImeasC)减去各自偏移量(ImeasAOffset等)后的绝对值,通过右移 6 位(等效于除以 64,做数值缩放)判断是否接近 0(即三相电流均异常偏小);
  • 若满足上述条件,进一步判断:电机速度反馈为 0(SpeedFdbp == 0)、非特定通信模式零点辨识(UartMode.Mode != 5)、SVPWM 非位置保持状态(svpwm.SvpwmControlState != 4),则启动延迟计数器(ImeasCheckDelay);
  • 当延迟计数器达到设定阈值(SystemError.LowSpeedTimer)时,判定为缺相,设置系统错误码为M_SYSERR_LACKPHASE;若不满足上述附加条件,则重置延迟计数器,避免误判,继续运行。

具体执行代码如下:

//缺相检查
void LostPhase(void)
{
    // 检测缺相故障(电机运行时三相电流均为0)
    if(SystemError.MotorRunFlag)  // 电机正在运行
    {
        // 检测三相电流是否都接近零(考虑偏移和噪声)
        // 右移6位相当于除以64,用于创建检测阈值(约1.5%满量程)
        if((((Abs(SystemError.ImeasA - SystemError.ImeasAOffset)) >> 6) == 0) && 
           (((Abs(SystemError.ImeasB - SystemError.ImeasBOffset)) >> 6) == 0) && 
           (((Abs(SystemError.ImeasC - SystemError.ImeasCOffset)) >> 6) == 0))
        {
            // 额外条件:电机实际速度为0,且不在特定模式(如调试模式)
            if((MotorControler.SpeedFdbp == 0) && 
               (UartMode.Mode != 5) &&          // 排除零点模式
               (svpwm.SvpwmControlState != 4))  // 排除使能状态
            {
                // 增加缺相检测计时器
                SystemError.ImeasCheckDelay++;

                // 当计时超过低速检测时间阈值
                if(SystemError.ImeasCheckDelay >= SystemError.LowSpeedTimer)
                {
                    // 设置系统错误标志:缺相故障
                    SystemError.SysErr = M_SYSERR_LACKPHASE;
                }
            }
            else
            {
                // 条件不满足时重置计时器
                SystemError.ImeasCheckDelay = 0;
            }
        }
    }
    else
    {
        // 电机未运行时重置检测计时器
        SystemError.ImeasCheckDelay = 0;
    }

    /******************** 电流传感器故障检测 ********************/
    // 检测A相电流传感器偏移是否异常(130对应约3.2A@16位ADC)
    if(Abs(SystemError.ImeasAOffset) > 130)
    {
        SystemError.SysErr = M_SYSERR_CURRENTSENSOR;
    }

    // 检测B相电流传感器偏移是否异常
    if(Abs(SystemError.ImeasBOffset) > 130)
    {
        SystemError.SysErr = M_SYSERR_CURRENTSENSOR;
    }

    // 检测C相电流传感器偏移是否异常
    if(Abs(SystemError.ImeasCOffset) > 130)
    {
        SystemError.SysErr = M_SYSERR_CURRENTSENSOR;
    }
}

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
  • 注意事项: (1)模式之间切换需要先下使能,之后再上使能,才能切换成功。 (2)电机运行不准时,可以使用校准模式校准磁编码器,校准完成后需要下使能,之后再上使能才能生效。 system (3)零点辨识功能可以用于电机的零点校准,校准完成后需要下使能,之后再上使能才能生效。

8 结论

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

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

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

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

results matching ""

    No results matching ""