无刷电机学习
转眼间已经工作好几个月了。上了班后才知道学习时间只能抽空去搞。忙里偷闲学习下电机控制。学习参照的是正点原子的直流无刷电机驱动教程。
直流无刷电机学习
直流无刷电机(BLDC)是指无电刷和换向器的电机,又称无换向器电机。


有刷电机原理:
从上图中可以看到无刷电机和有刷电机的定子和转子是相反的。无刷电机的定子是线圈。转子是永磁体。有刷电机的话定子是永磁体,转子是线圈,这是其中一个区别。并且有刷电机还多了换向器和电刷这两个结构。具体作用如下:
换向器切换线圈中电流的流向,反转磁极的方向,使其始终向右旋转。电刷向与轴一同旋转的换向器供电。
可以详细看该图:

简化版可以看上图,上图的换向器可以看做铜片1和铜片2组成,刚开始电流方向是从铜片1->铜片2.因此根据右手定则,可以得到里面线圈的磁场方向。因此顺时针转动带动换向器转动。进而铜片2和铜片1交换方向。电流在线圈的流向发生改变。因此NS再次调转。从而可以持续不断地转动。
如果上述理解不清楚的话也可以用左手定则:

我们可以把有刷简化为下面这两幅图:

序号1为初始状态,左手定则可以看出左边受力向上,右边受力向下。因此顺时针。
旋转180度后,由于换向器的作用,D和C交换了位置。此时根据左手定则受到的力依旧没发生变化。因此继续顺时针旋转。这就是换向器的作用。想要反转,就改变电流方向。
无刷电机原理:
无刷电机分类:
分为方波驱动和正弦波驱动。方波驱动的又分为外转子和内转子BLDC。正弦波驱动的为永磁同步PMSM。前两幅图分别为内转子和外转子BLDC。第三个图为永磁同步电机。



BLDC和PMSM的不同点:

其中集中绕组和分布绕组具体如下图:



并且BLDC是绕组两两导通,PMSM是三组一起导通。
无刷电机的主要参数
极对数
定义:转子磁铁NS级的对数,它和电机旋转速度密切相关, 关系为:电角速度(电子速度 )= 电机实际机械速度 × 极对数。电角速度单位一般是弧度/秒,电机实际机械速度单位常用转/分钟(RPM)。
举例:假设有一台无刷直流电机,其极对数为3。如果电机实际转速是1000RPM,将其换算为弧度/秒(1RPM = 2π/60 弧度/秒) ,1000RPM对应的机械角速度是\(1000\times\frac{2\pi}{60}=\frac{100\pi}{3}\)弧度/秒 ,那么电角速度就是\(\frac{100\pi}{3}× 3 = 100\pi\) 弧度/秒。在交流电机的旋转磁场理论中,极对数决定了旋转磁场的转速,极对数越多,在相同电频率下,电机实际转速就越低。
KV值
定义:表示电机在单位电压下的空载转速,单位是RPM/V(转/分钟/伏特) ,电机转速 = KV值×工作电压。KV值越大,意味着在相同工作电压下,电机转速越大。
举例:有一款航模电机,它的KV值是2200。当给它提供11.1V的锂电池(常见航模电池电压)时,根据公式可算出电机的空载转速为 2200×11.1 = 24420RPM。也就是说,这款电机在11.1V电压下,每分钟可以空转24420圈 。如果换用7.4V的电池,那空载转速就是2200×7.4 = 16280RPM。
额定转速
定义:电机在额定电流下的空载转速,通常单位用RPM(转/分钟)表示。它是衡量电机在标准工作条件下的转动快慢的参数。额定电流是指电机长时间正常运行时,允许通过的最大电流值。
举例:一台小型直流电动机,其额定电流为2A,额定转速标注为3000RPM。这就意味着,当这台电机接入合适的电源,并且通过它的电流稳定在2A ,没有机械负载(空载)的情况下,它每分钟会转动3000圈。如果实际运行电流偏离额定电流,电机转速可能会发生变化,比如电流过大,电机可能发热严重甚至转速下降;电流过小,电机可能无法达到额定转速。
转矩
定义:电机中转子产生的可以带动机械负载的驱动力矩,通常单位为牛·米(N·m)。转矩反映了电机带动负载的能力,转矩越大,电机能带动的负载就越重。
举例:以电动螺丝刀为例,一款电动螺丝刀电机的转矩是5N·m 。在拧螺丝时,如果螺丝受到的阻力矩小于5N·m,电机就能轻松带动螺丝刀头将螺丝拧紧;但如果螺丝生锈等原因导致阻力矩增大到超过5N·m ,电机就可能带不动螺丝刀头,出现拧不动螺丝的情况。再比如电动汽车,电机转矩大,就能在起步和加速时提供更强的动力,快速提升车速 ,并且能适应爬坡等需要较大驱动力的工况。
用的是正点原子的无刷电机:
引脚定义如下:



无刷电机驱动原理
需要对无刷电机内部结构图进行简化:


因此从上图可以看到最终简化出来的模型:
我们可以看到UVW两两通电有这些组合。一共包含6种。

看到这里大家可能会有疑问:
为啥红色箭头指的方向就是电机转轴在的位置呢。下面我来画图分析一下:

可以看出来,在该位置达到稳定,因此电机转轴对应该位置。同理可以分析出来这6种组合的情况。
因此有以下六种情况:
- 给 U 接 24V、V 悬空、W 接 GND,此时电机的转轴对应上图(1)的转子位置。
- 在上一步的基础上修改接线方式,给 U 接 24V、V 接 GND、W 悬空,此时电机的转轴对
应上图(2)的转子位置,相较于(1)旋转了一个角度。 - 在 2.的基础上继续修改接线方式,U 悬空,V 接 GND,W 接 24V,此时对应(3)。
- 在 3.的基础上继续修改接线方式,U 接 GND,V 悬空,W 接 24V,此时对应(4)。
- 在 4.的基础上继续修改接线方式,U 接 GND,V 接 24V,W 悬空,此时对应(5)。
- 在 5.的基础上继续修改接线方式,U 悬空,V 接 24V,W 接 GND,此时对应(6)。
同样的,如果顺序相反则无刷电机会反向旋转。同步进电机类似,实际上步进电机也算
是无刷电机,所以无刷电机也会存在步进电机那样的问题,即丢步,原因也是定子产生的磁
场没有来得及改变转子的位置,详细原因如下:
- 指令太急:无刷电机靠控制器发信号来改变定子磁场,实现换相转动。要是给控制器的换相指令发得太快,就像一直催一个人做事,它处理不过来,没办法及时发出下一次换相指令,电机就没法按预想的换相。
- 手脚太慢:电机里的一些电子零件,比如MOSFET ,它们在接通和断开电流的时候需要一点时间。要是换相指令变得太快,这些零件还没准备好,就会影响定子磁场正常建立,导致磁场没法及时推动转子。
- 太胖刹不住:无刷电机的转子有一定重量,就像一个胖人跑起来后很难马上停下或者转向一样,转子也有惯性。当我们突然要求它改变转动方向或者速度,它没办法一下子就跟上定子磁场的变化。
- 背的东西太重:要是电机带动的东西太重了,超过了它的能力范围,电机就很难按我们希望的那样转动。比如电机拉一辆超载的车,在换相的时候,就没力气把车拉动到该去的位置。
- 电流拖后腿:无刷电机定子绕组有电感特性,就像水流过弯弯曲曲的管道会受到阻碍一样,电流变化也会被电感阻碍。这样一来,定子磁场想要改变强度和方向,就没办法一下子完成。在快速换相的时候,新磁场不能马上到位,也就没法好好推动转子。
- 磁性“迟钝”:电机里的磁性材料有点“迟钝”,也就是磁滞现象。当我们想改变定子绕组的电流来改变磁场时 ,磁性材料不能马上跟着变,实际产生的磁场和我们想要的不一样,就影响了对转子的驱动。
- 有人捣乱:无刷电机工作的环境里,可能有一些设备会产生电磁干扰,就像有人在旁边大声喧哗影响你学习一样,这些干扰会影响控制器正常工作,让控制信号出错或者延迟,定子磁场就没法准确控制转子。
- 自身不稳:要是无刷电机的控制系统设计得不好,比如调节速度或者位置的参数没设置对,系统就会像走路不稳的人一样,摇摇晃晃或者反应慢半拍,定子磁场和转子位置就没办法配合好,出现丢步情况。
无刷电机驱动电路
从无刷电机的驱动原理我们可以得到:U相 V相 W相在6种状态中有时候需要接正极,有时候需要接负极。因此需要能够简单方便控制三相极性的切换。因此使用三相逆变电路来实现。

所谓的三相逆变电路就是由三个半桥构成的电路,图 21.2.9 中的 A+与 A-为一个半桥,
B+与 B-以及 C+与 C-各自又为一个半桥,共三个半桥;这三个半桥各自控制对应的 A、B、
C 三相绕组;当控制 A 的上桥臂 A+导通时,此时 A 相绕组接到电源正,当控制 B 的下桥臂
B-导通时,此时 B 相绕组接到电源负,所以此时电流由 A 流向 B。
所以想要控制绕组的极性,只需要控制绕组对应半桥的“上桥臂导通”或者“下桥臂导
通”就可以实现控制该相连接至“正极”或者“负极”了,但是要注意不可以同侧半桥上下
桥臂同时导通,会短路,烧毁电机!因此为了实现6步控制,如下图所示:

但是上述这种方式相当于直接把电源电压加在了电机上,电机速度一下子飙升到最大。所以一般实际控制我们都是采用PWM去控制。因为PWM可以控制占空比的大小,占空比的大小决定进入电机实际能量的多少。因此可以通过改变占空比控制转子扭矩和转速。

并且我们看出来一个周期有六个状态,所以每个相位对应60电角度。这个是由于无刷电机的机械结构所决定的。

通常使用 PWM 控制直流无刷电机的常见方法有 5 种,分别是: PWM – ON、ON–PWM、H_ON – L_PWM 、H_PWM – L_ON、H_PWM – L_PWM。且均是电机处于 120°的运行方式下进行的。这里的120度指的是电角度。

不同的控制方式在性能上有不同的效果,针对实际的应用场合可以多尝试多种调制方式,然后选择最优调制方式,一般认为:单极性调制转矩波动更小,双极性调制转矩波动较大。我们例程依据我们的驱动硬件所使用的是 H_PWM – L_ON 的驱动方式。
霍尔传感器位置确认以及分析
从上图中我们可以看到有三个霍尔传感器确认当前电机的位置。霍尔期间输出高低电平。 具体对应电平对应位置如下:


首先分析转子受力转动需要了解一个公式:
公式 P = F·v·cosθ 是关键(这里可以理解为 “力矩效率” 的简化逻辑):
- F:电机产生的 “合成磁力”(可以想象成拉着转子转的 “看不见的绳子”)。
- v:转子转动的线速度(不重要,重点看后面)。
- θ:合成磁力(F)与转子磁场方向的夹角。
当 θ=90° 时,cos90°=1,此时 “拉力” 的效率最高 —— 就像拉车时,“横着拉”(垂直于车的运动方向的反方向)比 “斜着拉” 更省力、劲儿用得更足。对电机来说,就是合成磁力与转子磁场呈 90° 时,转动效率最高、力矩最稳。
我们可以发现三个霍尔传感器安装的时候会存在误差。比如现在在区域1,可能位于区域1的最左边,也可能位于区域1的最右边,因此存在0-60°的角度误差。因为 “看不准角度”,电机没法让合成磁力始终和转子保持 90°,只能 “凑活” 选一个固定方向:假设扇区 1 是 “0°~60°” 电角度(转子在这个范围内转动),理想的合成磁力方向应该 “跟着转子动”(比如转子在 0° 时磁力在 90°,转子在 30° 时磁力在 120°,始终保持 90° 夹角)。霍尔传感器只能识别 “在扇区 1”,所以电机只能固定一个方向 ——120° 方向。
-
当转子在扇区 1 的0° 时:合成磁力在 120°,夹角是 120°(cos120°=-0.5,效率低);
-
当转子在扇区 1 的30° 时:合成磁力在 120°,夹角是 90°(cos90°=1,效率最高);
-
当转子在扇区 1 的60° 时:合成磁力在 120°,夹角是 60°(cos60°=0.5,效率中等)。
在整个扇区 1 里,“拉力夹角” 在 60°~120° 之间来回变,导致力矩一会儿大、一会儿小—— 这就是 “力矩抖动” 的原因。
选取合成磁场方向与当前区域0度边界为120度是当前的最优解。在扇区 1(转子角度 0°~60°),电机受限于霍尔传感器只能提供 “扇区级” 的模糊位置信息(无法知道转子在扇区内的精确角度),所以只能选择一个固定的合成磁力方向。此时选择 120° 方向,主要是基于以下两点考虑:
- 最大化平均效率:这个方向能让扇区内 “拉力夹角” 的平均值更接近理想的 90°。对比你提到的另一个可选方向 90°(W+ V- U 悬空),120° 方向的合成磁力本身更强,即便夹角在 60°~120° 波动,其产生的平均力矩和效率也高于 90° 方向(后者磁力弱,即便夹角理想,实际出力也更小)。
- 避免 “反向出力”:如果选择的合成磁力方向太偏(比如小于 60°),当转子角度接近 60° 时,夹角可能小于 0°(即磁力方向与转子转动方向相反),会产生 “制动力”,这是绝对要避免的。120° 方向能确保整个扇区内夹角始终为正(60°~120°),出力方向始终与转子转动方向一致。
不同厂家生产的电机霍尔传感器位置可能不一样,因此需要厂家会给出真值表,根据真值表去控制UVW的开启和关闭即可。

上述真值表对应的控制电路图如下图所示:
所以读取霍尔传感器的值,给对应的 U V W 相导通即可。

由实际机械角度可以进一步引出电角度这个概念。这里我们需要进一步区分下这两个概念:
- 机械角度:指的是电机转子在空间上旋转的角度,以圆周为360° 来度量。比如,电机转子从初始位置转动了半圈,那机械角度就是180°。
- 电角度:是基于电机磁场变化周期来定义的角度。对于一个具有 ( p ) 对极的电机,当转子旋转一周(机械角度360° )时,电机内部的磁场会经历 ( p ) 个完整的变化周期。我们把磁场变化一个周期所对应的角度定义为360° 电角度。
补充,机械角度是指电机转子的旋转角度,电角度是指磁场的旋转角度,它们的关系式满足:电角度 = 机械角度 * 极对数。

换言之理解就是三相电机的结构120度排列,导致会有6个磁场状态。而一个完整的磁场变化就是360度电角度,因此每个磁场状态下对应的电角度为60电角度。而实际对应的机械角度跟磁对极有关系。所以一个状态到一个状态某个相导通时间一定为120电角度。
三相电机的定子上,A、B、C 三个绕组就像三个 “磁场发生器”,它们不是乱摆的,而是按 120° 的间隔均匀分布在圆周上,就像钟表上 12 点、4 点、8 点的位置那样错开。正是这种 “120° 排兵布阵” 的结构,为后续产生不同方向的磁场打下了基础。
要让电机转起来,得给这三个绕组轮流通电。最常见的通电方式是 “六步一循环”:先给 A 和 B 通,再给 B 和 C 通,接着给 C 和 A 通,然后反过来给 B 和 A 通、C 和 B 通、A 和 C 通,这六步下来,就能让定子产生的磁场完整转一圈 —— 而磁场转一圈,就是一个 “360° 电角度” 的完整变化。
既然 6 步对应 360° 电角度,那每一步(也就是每一个磁场状态)持续的电角度自然就是 360÷6=60°。
再看单个绕组的通电时间:比如 A 相,在这六步里只参与其中两步(A 和 B 通、A 和 C 通),每步 60°,加起来就是 120° 电角度 —— 所以说每个绕组一次导通的时间就是 120° 电角度。
至于 “机械角度”,就看电机转子上的磁极对数了。比如 2 极电机(1 对磁极),磁场转 1 圈(360° 电角度),转子也跟着转 1 圈(360° 机械角度);要是 4 极电机(2 对磁极),磁场转 1 圈,转子只转半圈(180° 机械角度)。简单说就是:电机磁极对数越多,同样的电角度变化,转子实际转的机械角度就越小。
举例说明:
- 假设有一台2极(也就是1对极, ( p = 1 ) )的电机,当转子旋转的机械角度 ( \theta_m = 90° ) 时,根据上述公式,电角度 ( \theta_e = 1×90° = 90° ) ,此时机械角度和电角度相等。
- 再看一台4极( ( p = 2 ) )的电机,当转子旋转的机械角度 ( \theta_m = 90° ) 时 ,电角度 ( \theta_e = 2×90° = 180° ) ,也就是说在这台电机里,转子机械角度旋转90° ,但磁场变化了半个周期,电角度是180° 。
这个参数在计算速度大有作用。至于如何计算后面再说。
因此有刷电机的整体框图为:

正点原子无刷电机驱动电路介绍

上述为无刷电机的基本参数。我们可以看出这是4磁极数的电机。
下面来看下正点原子的无刷电机驱动电路:

MOS管定义
首先需要了解MOS管的基本构造,下图为MOS管三极的具体定义。

驱动电路MOS导通原理
接下来需要了解下如何导通高端MOSFET和低端MOSFET的原理:

高端MOS管中充电电容存在的原理
并且我们可以发现高端Q1和低端Q1有一个充电电容的区别。对于下图中来说,C11就是该电容。
具体作用如下:C11 存在的意义,是给源极电位不固定(飘移) 的高端 MOSFET(Q1),提供一个 “随源极同步变化的栅极驱动电压”,确保 G-S 极始终有足够压差(>阈值电压)。具体原理为:
第一步:低端 MOSFET(Q2)导通 → 自举电容 C11 “充电”
当 IR2110 的 LO 引脚输出高电平,低端 MOSFET Q2 导通。此时,Q2 的漏极(D)和源极(S)导通,电流路径:VCC11 → D1(DSK110) → VB 引脚 → C11 正极 → C11 负极 → VS 引脚 → Q2 的 D 极 → Q2 的 S 极 → 地。这一路电流给 C11 充电,直到 C11 两端电压接近 VCC11(比如 VCC11 是 15V,C11 就充到~15V)。
第二步:低端 MOSFET(Q2)截止 → 高端 MOSFET(Q1)需要导通 → 自举电容 C11 “放电供电”
当 IR2110 的 HO 引脚输出高电平,需要导通高端 MOSFET Q1 时:Q2 先截止(LO 引脚变低),Q1 准备导通。此时,电机绕组(Motor U)的电位开始变化,但自举电容 C11 的正极(连 VB)通过 IR2110 内部电路,连接到 Q1 的栅极(G);C11 的负极(连 VS),则连接到 Q1 的源极(S)(因为 VS 引脚直接连 Q1 的 S 极)。由于 C11 之前充了电(比如 15V),此时它会 “放电”,在 Q1 的 G、S 极之间提供电压:C11 正极(高电位) → Q1 的 G 极 → Q1 的 S 极 → C11 负极(低电位)。这就相当于,给 Q1 的 G、S 极加了一个 “随源极电位飘移的压差”(因为 C11 的负极连 Q1 的 S 极,S 极电位怎么变,C11 的压差都能 “跟着补”)。等效并联到高端 MOSFET 的 G-S 极两端。这种连接让 C11 的电压差直接成为 G-S 驱动电压,再加上电容 “电压不能突变” 的特性,无论 S 极电位怎么飘,G-S 压差都能稳定保持,所以高端 MOSFET 总能导通。

有感方波驱动:
使用的是正点原子的驱动板。无刷电机接口1的原理图如下:

实现的功能:
- 按键Key0一次加速旋转,按一次key1减速旋转,按下key2则停止旋转。
- LED0作为程序运行状态指示灯,闪烁周期200ms。

我们例程依据我们的驱动硬件所使用的是 H_PWM – L_ON 的驱动方式。所以要有三路PWM信号去负责导通三个上桥臂:L-ON是一直导通,所以用普通IO口去控制就可以。所以用到的定时器资源有以下这些:
- TIM1_CH1:PA8
- TIM1_CH2:PA9
- TIM1_CH3:PA10
- 普通IO口:PB13\14\15
还需要使能引脚控制电机开启和关闭,选取的引脚的为:SHDN:PF10。
因此,涉及到的IO口如下图所示:

下面来看下实际的程序:
首先我们可以思考下程序组成的大概框架部分:
程序配置:
定时器配置、GPIO配置
首先第一部分是要输出3路PWM信号。因为要先初始化定时器1以及配置对应的PWM模式。
void Mytimx_init(uint16_t arr,uint16_t psc)
{
__HAL_RCC_TIM1_CLK_ENABLE();//定时器1时钟使能
g_atimx_handle.Instance = TIM1;
g_atimx_handle.Init.Prescaler = psc;
g_atimx_handle.Init.CounterMode = TIM_COUNTERMODE_UP;
g_atimx_handle.Init.Period = arr;
g_atimx_handle.Init.ClockDivision=TIM_CLOCKDIVISION_DIV1; /* 分频因子 */
g_atimx_handle.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;//若开启(ENABLE),修改Period的值后,需等待当前计数周期结束才会生效;关闭(DISABLE)时,修改后立即生效。PWM 模式下通常关闭,以便快速调整周期。
g_atimx_handle.Init.RepetitionCounter = 0;//重复计数器用于扩展 PWM 的周期。例如,RepetitionCounter=1时,定时器需要计数 2 个周期才会产生一次更新事件。设为 0 表示不扩展,每个计数周期都产生更新事件。
HAL_TIM_PWM_Init(&g_atimx_handle);
// 配置PWM输出模式为PWM模式1
// PWM模式1工作逻辑:计数器值 < Pulse时输出有效电平,计数器值 >= Pulse时输出无效电平
g_atimx_oc_chy_handle.OCMode = TIM_OCMODE_PWM1;
// 设置PWM比较寄存器初始值为0(占空比初始值为0)
// 占空比计算公式:(Pulse / Period) × 100%,初始为0表示默认输出低电平
g_atimx_oc_chy_handle.Pulse = 0;
// 设置正向输出通道的极性为高电平有效
// 即PWM的有效电平为高电平,与PWM模式1配合实现:计数器值 < Pulse时输出高电平
g_atimx_oc_chy_handle.OCPolarity = TIM_OCNPOLARITY_HIGH;
// 设置互补输出通道的极性为高电平有效(仅高级定时器TIM1/TIM8支持互补通道)
// 互补通道用于驱动H桥的上下桥臂,与正向通道配合实现电机绕组电流方向控制
g_atimx_oc_chy_handle.OCNPolarity = TIM_OCNPOLARITY_HIGH;
// 关闭快速输出模式
// 快速模式可减少延迟但关闭滤波,电机控制中优先保证信号稳定,故禁用
g_atimx_oc_chy_handle.OCFastMode = TIM_OCFAST_DISABLE;
// 设置正向通道在空闲状态(定时器停止工作时)的输出为复位状态(低电平)
// 防止定时器停止时电机误动作,确保MOS管处于截止状态
g_atimx_oc_chy_handle.OCIdleState = TIM_OCIDLESTATE_RESET;
// 设置互补通道在空闲状态的输出为复位状态(低电平)
// 与正向通道空闲状态配置一致,保证上下桥臂在空闲时均截止
g_atimx_oc_chy_handle.OCNIdleState = TIM_OCNIDLESTATE_RESET;
// 将上述配置应用到定时器1的通道1(对应电机U相PWM控制)
HAL_TIM_PWM_ConfigChannel(&g_atimx_handle, &g_atimx_oc_chy_handle, TIM_CHANNEL_1);
// 将上述配置应用到定时器1的通道2(对应电机V相PWM控制)
HAL_TIM_PWM_ConfigChannel(&g_atimx_handle, &g_atimx_oc_chy_handle, TIM_CHANNEL_2);
// 将上述配置应用到定时器1的通道3(对应电机W相PWM控制)
HAL_TIM_PWM_ConfigChannel(&g_atimx_handle, &g_atimx_oc_chy_handle, TIM_CHANNEL_3);
/* 开启定时器通道1输出PWM */
HAL_TIM_PWM_Start(&g_atimx_handle,TIM_CHANNEL_1);
/* 开启定时器通道2输出PWM */
HAL_TIM_PWM_Start(&g_atimx_handle,TIM_CHANNEL_2);
/* 开启定时器通道3输出PWM */
HAL_TIM_PWM_Start(&g_atimx_handle,TIM_CHANNEL_3);
}
初始化完了定时器后,还要配置对应的输出引脚PA8、PA9、PA10。其中这个函数的命名要注意下,因为会被HAL_TIM_PWM_Init这个函数中进一步初始化对应GPIO配置。调用的函数为:HAL_TIM_PWM_MspInit。
HAL_StatusTypeDef HAL_TIM_PWM_Init(TIM_HandleTypeDef *htim)
{
/* Check the TIM handle allocation */
if (htim == NULL)
{
return HAL_ERROR;
}
/* Check the parameters */
assert_param(IS_TIM_INSTANCE(htim->Instance));
assert_param(IS_TIM_COUNTER_MODE(htim->Init.CounterMode));
assert_param(IS_TIM_CLOCKDIVISION_DIV(htim->Init.ClockDivision));
assert_param(IS_TIM_AUTORELOAD_PRELOAD(htim->Init.AutoReloadPreload));
if (htim->State == HAL_TIM_STATE_RESET)
{
/* Allocate lock resource and initialize it */
htim->Lock = HAL_UNLOCKED;
#if (USE_HAL_TIM_REGISTER_CALLBACKS == 1)
/* Reset interrupt callbacks to legacy weak callbacks */
TIM_ResetCallback(htim);
if (htim->PWM_MspInitCallback == NULL)
{
htim->PWM_MspInitCallback = HAL_TIM_PWM_MspInit;
}
/* Init the low level hardware : GPIO, CLOCK, NVIC */
htim->PWM_MspInitCallback(htim);
#else
/* Init the low level hardware : GPIO, CLOCK, NVIC and DMA */
HAL_TIM_PWM_MspInit(htim);
#endif /* USE_HAL_TIM_REGISTER_CALLBACKS */
}
/* Set the TIM state */
htim->State = HAL_TIM_STATE_BUSY;
/* Init the base time for the PWM */
TIM_Base_SetConfig(htim->Instance, &htim->Init);
/* Initialize the DMA burst operation state */
htim->DMABurstState = HAL_DMA_BURST_STATE_READY;
/* Initialize the TIM channels state */
TIM_CHANNEL_STATE_SET_ALL(htim, HAL_TIM_CHANNEL_STATE_READY);
TIM_CHANNEL_N_STATE_SET_ALL(htim, HAL_TIM_CHANNEL_STATE_READY);
/* Initialize the TIM state*/
htim->State = HAL_TIM_STATE_READY;
return HAL_OK;
}
因此接下来初始化GPIO的函数名字为:HAL_TIM_PWM_MspInit
void HAL_TIM_PWM_MspInit(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM1)
{
// 定义GPIO初始化结构体(用于配置引脚参数)
GPIO_InitTypeDef MOTOR_GPIO_INIT;
// 使能定时器1时钟(用于生成PWM信号)
__HAL_RCC_TIM1_CLK_ENABLE();
// 使能GPIOA时钟(PA8、PA9、PA10为上桥臂PWM输出引脚)
__HAL_RCC_GPIOA_CLK_ENABLE();
// 使能GPIOB时钟(PB13、PB14、PB15为下桥臂控制引脚)
__HAL_RCC_GPIOB_CLK_ENABLE();
// 配置GPIOB的3个引脚:PB13、PB14、PB15(下桥臂控制引脚)
MOTOR_GPIO_INIT.Pin = GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15; // 组合多个引脚
MOTOR_GPIO_INIT.Pull = GPIO_NOPULL; // 无上拉下拉(驱动MOS管无需上下拉)
MOTOR_GPIO_INIT.Speed = GPIO_SPEED_HIGH; // 高速输出(快速响应控制信号)
MOTOR_GPIO_INIT.Mode = GPIO_MODE_OUTPUT_PP; // 推挽输出模式(可输出高低电平驱动MOS管)
HAL_GPIO_Init(GPIOB, &MOTOR_GPIO_INIT); // 初始化GPIOB的下桥臂引脚
// 重新配置GPIOA的3个引脚:PA8、PA9、PA10(上桥臂PWM输出引脚)
MOTOR_GPIO_INIT.Pin = GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10; // 组合多个引脚
MOTOR_GPIO_INIT.Mode = GPIO_MODE_AF_PP; // 复用推挽输出(用于定时器PWM输出)
MOTOR_GPIO_INIT.Pull = GPIO_NOPULL; // 无上拉下拉(PWM信号无需上下拉)
MOTOR_GPIO_INIT.Speed = GPIO_SPEED_FREQ_HIGH; // 高速输出(匹配PWM高频特性)
MOTOR_GPIO_INIT.Alternate = GPIO_AF1_TIM1; // 复用为TIM1功能(PA8/9/10对应TIM1_CH1/2/3)
HAL_GPIO_Init(GPIOA, &MOTOR_GPIO_INIT); // 初始化GPIOA的上桥臂PWM引脚
// 配置定时器1更新中断优先级(抢占优先级2,子优先级2)
HAL_NVIC_SetPriority(TIM1_UP_TIM10_IRQn, 2, 2);
// 使能定时器1更新中断
HAL_NVIC_EnableIRQ(TIM1_UP_TIM10_IRQn);
}
}
六步换向、霍尔状态读取功能、电机开启停止功能:
首先要想知道此时电机转子位于哪个位置,需要去读取霍尔传感器此时的状态,霍尔传感器U、V、W三相对应接入的引脚为:PH10、PH11、PH12。获得霍尔传感器的状态其实就是读取高低电平。


首先进行霍尔引脚的初始化,这里设置为输入模式,并且默认上拉,不然就是未知的电平状态。读取霍尔状态的话是把每个引脚的高低电平进行拼接。比如霍尔U对应引脚PH10,所以放在第一位也就是与0x01(对应二进制为:0000 0001)进行或运算。霍尔V对应引脚PH11,对应第二位,所以和0x02(对应二进制为:0000 0010)。霍尔W对应的引脚为PH12,对应第三位(对应二进制为:0000 0100),也就是对应0x04。这样的话就可以与表里的霍尔值直接对应上
void hall_gpio_init(void)
{
GPIO_InitTypeDef gpio_init_struct;
__HAL_RCC_GPIOH_CLK_ENABLE();
/* 霍尔通道 1 引脚初始化 */
gpio_init_struct.Pin = GPIO_PIN_10 | GPIO_PIN_11 | GPIO_PIN_12;
gpio_init_struct.Mode = GPIO_MODE_INPUT;
gpio_init_struct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(HALL1_TIM_CH1_GPIO, &gpio_init_struct);
}
/**
* @brief 获取霍尔传感器引脚状态
* @param motor_id : 电机接口号
* @retval 霍尔传感器引脚状态
*/
uint32_t hallsensor_get_state(uint8_t motor_id)
{
__IO static uint32_t state ;
state = 0;
if(HAL_GPIO_ReadPin(GPIOH,GPIO_PIN_10)!= GPIO_PIN_RESET)
{
state |= 0x01U;
}
if(HAL_GPIO_ReadPin(GPIOH,GPIO_PIN_11)!= GPIO_PIN_RESET)
{
state |= 0x02U;
}
if(HAL_GPIO_ReadPin(GPIOH,GPIO_PIN_12)!= GPIO_PIN_RESET)
{
state |= 0x04U;
}
return state;
}
有了霍尔状态后,我们就能够知道此时该如何给每相如何通电。接下来需要编写六步换相的实际代码:六步换相的代码可以参考表格通电顺序进行编写。我们采用的是H_PWM,L_ON。因此只需要上桥臂给占空比,这里的话也是CCR进行赋值。下桥臂是高低电平。因此给高电平打开就可以。
/**
* @brief U相上桥臂导通,V相下桥臂导通
* @param 无
* @retval 无
*/
void m1_uhvl(void)
{
g_atimx_handle.Instance->CCR1 = g_bldc_motor1.pwm_duty;
g_atimx_handle.Instance->CCR2 = 0;
g_atimx_handle.Instance->CCR3 = 0;
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_13,GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_14,GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_15,GPIO_PIN_RESET);
}
/**
* @brief U相上桥臂导通,W相下桥臂导通
* @param 无
* @retval 无
*/
void m1_uhwl(void)
{
g_atimx_handle.Instance->CCR1 = g_bldc_motor1.pwm_duty;
g_atimx_handle.Instance->CCR2 = 0;
g_atimx_handle.Instance->CCR3 = 0;
HAL_GPIO_WritePin(M1_LOW_SIDE_W_PORT,M1_LOW_SIDE_W_PIN,GPIO_PIN_SET);
HAL_GPIO_WritePin(M1_LOW_SIDE_U_PORT,M1_LOW_SIDE_U_PIN,GPIO_PIN_RESET);
HAL_GPIO_WritePin(M1_LOW_SIDE_V_PORT,M1_LOW_SIDE_V_PIN,GPIO_PIN_RESET);
}
/**
* @brief V相上桥臂导通,W相下桥臂导通
* @param 无
* @retval 无
*/
void m1_vhwl(void)
{
g_atimx_handle.Instance->CCR1 = 0;
g_atimx_handle.Instance->CCR2 = g_bldc_motor1.pwm_duty;
g_atimx_handle.Instance->CCR3 = 0;
HAL_GPIO_WritePin(M1_LOW_SIDE_W_PORT,M1_LOW_SIDE_W_PIN,GPIO_PIN_SET);
HAL_GPIO_WritePin(M1_LOW_SIDE_U_PORT,M1_LOW_SIDE_U_PIN,GPIO_PIN_RESET);
HAL_GPIO_WritePin(M1_LOW_SIDE_V_PORT,M1_LOW_SIDE_V_PIN,GPIO_PIN_RESET);
}
/**
* @brief V相上桥臂导通,u相下桥臂导通
* @param 无
* @retval 无
*/
void m1_vhul(void)
{
g_atimx_handle.Instance->CCR1 = 0;
g_atimx_handle.Instance->CCR2 = g_bldc_motor1.pwm_duty;
g_atimx_handle.Instance->CCR3 = 0;
HAL_GPIO_WritePin(M1_LOW_SIDE_U_PORT,M1_LOW_SIDE_U_PIN,GPIO_PIN_SET);
HAL_GPIO_WritePin(M1_LOW_SIDE_V_PORT,M1_LOW_SIDE_V_PIN,GPIO_PIN_RESET);
HAL_GPIO_WritePin(M1_LOW_SIDE_W_PORT,M1_LOW_SIDE_W_PIN,GPIO_PIN_RESET);
}
/**
* @brief W相上桥臂导通,U相下桥臂导通
* @param 无
* @retval 无
*/
void m1_whul(void)
{
g_atimx_handle.Instance->CCR1 = 0;
g_atimx_handle.Instance->CCR2 = 0;
g_atimx_handle.Instance->CCR3 = g_bldc_motor1.pwm_duty;
HAL_GPIO_WritePin(M1_LOW_SIDE_U_PORT,M1_LOW_SIDE_U_PIN,GPIO_PIN_SET);
HAL_GPIO_WritePin(M1_LOW_SIDE_V_PORT,M1_LOW_SIDE_V_PIN,GPIO_PIN_RESET);
HAL_GPIO_WritePin(M1_LOW_SIDE_W_PORT,M1_LOW_SIDE_W_PIN,GPIO_PIN_RESET);
}
/**
* @brief W相上桥臂导通,v相下桥臂导通
* @param 无
* @retval 无
*/
void m1_whvl(void)
{
g_atimx_handle.Instance->CCR1 = 0;
g_atimx_handle.Instance->CCR2 = 0;
g_atimx_handle.Instance->CCR3 = g_bldc_motor1.pwm_duty;
HAL_GPIO_WritePin(M1_LOW_SIDE_V_PORT,M1_LOW_SIDE_V_PIN,GPIO_PIN_SET);
HAL_GPIO_WritePin(M1_LOW_SIDE_U_PORT,M1_LOW_SIDE_U_PIN,GPIO_PIN_RESET);
HAL_GPIO_WritePin(M1_LOW_SIDE_W_PORT,M1_LOW_SIDE_W_PIN,GPIO_PIN_RESET);
}
下面还需要能够及时关闭和开启电机:
关闭的话我们驱动电路中涉及到了shutdown引脚,因此操作这个引脚的高低电平就可以及时关闭开启芯片的工作。
/**
* @brief 关闭电机运转
* @param 无
* @retval 无
*/
#define SHUTDOWN_EN HAL_GPIO_WritePin(SHUTDOWN_PIN_GPIO,SHUTDOWN_PIN,GPIO_PIN_SET); /* 使能半桥芯片的SD引脚 */
#define SHUTDOWN_OFF HAL_GPIO_WritePin(SHUTDOWN_PIN_GPIO,SHUTDOWN_PIN,GPIO_PIN_RESET); /* 失能半桥芯片的SD引脚 */
void stop_motor1(void)
{
/* 关闭半桥芯片输出 */
SHUTDOWN_OFF;
/* 关闭PWM输出 */
HAL_TIM_PWM_Stop(&g_atimx_handle,TIM_CHANNEL_1);
HAL_TIM_PWM_Stop(&g_atimx_handle,TIM_CHANNEL_2);
HAL_TIM_PWM_Stop(&g_atimx_handle,TIM_CHANNEL_3);
/* 上下桥臂全部关断 */
g_atimx_handle.Instance->CCR2 = 0;
g_atimx_handle.Instance->CCR1 = 0;
g_atimx_handle.Instance->CCR3 = 0;
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_13,GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_14,GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_15,GPIO_PIN_RESET);
}
/**
* @brief 开启电机运转
* @param 无
* @retval 无
*/
void start_motor1(void)
{
SHUTDOWN_EN;
/* 使能PWM输出 */
HAL_TIM_PWM_Start(&g_atimx_handle,TIM_CHANNEL_1);
HAL_TIM_PWM_Start(&g_atimx_handle,TIM_CHANNEL_2);
HAL_TIM_PWM_Start(&g_atimx_handle,TIM_CHANNEL_3);
}
定时器回调
根据霍尔的状态导通对应的相。这里定时器回调时间为55us。回调的时间我这里理解一定要小于每个状态持续的时间,不能这个状态持续30us,你55us回调这个时候状态已经捕捉不到了。

pctr pfunclist_m1[6]=
{
&m1_uhwl, &m1_vhul, &m1_vhwl,
&m1_whvl, &m1_uhvl, &m1_whul
};
/**
* @brief TIM1和TIM10的更新中断服务函数
* @note 当TIM1或TIM10发生更新事件时,CPU会自动调用此函数
* 中断服务函数是硬件中断响应的入口点,负责将中断转交给HAL库处理
*/
void TIM1_UP_TIM10_IRQHandler(void)
{
/*
* 调用HAL库的定时器中断通用处理函数
* 参数&g_atimx_handle:指向定时器1的句柄结构体(已在初始化时绑定TIM1)
*
* 内部处理流程:
* 1. 检查中断源(确认是TIM1的更新中断)
* 2. 清除中断标志位(防止中断被重复响应)
* 3. 调用对应的中断回调函数(如周期溢出回调HAL_TIM_PeriodElapsedCallback)
*
* 在电机控制中,该函数最终会触发定时器周期中断的回调,用于实现:
* - 电机六步换向的状态切换
* - 转速检测与闭环控制
* - 定时采样电机反馈信号等
*/
HAL_TIM_IRQHandler(&g_atimx_handle);
}
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)/* 55us */
{
if(htim->Instance == TIM1)
{
if(g_bldc_motor1.run_flag == RUN)
{
if(g_bldc_motor1.dir == CW)
{
g_bldc_motor1.step_sta = hallsensor_get_state(MOTOR_1);
}
else//反转
{
g_bldc_motor1.step_sta = 7- hallsensor_get_state(MOTOR_1);//具体原因可以看对应表格
}
if ((g_bldc_motor1.step_sta <=6)&&(g_bldc_motor1.step_sta >=1))
{
pfunclist_m1[g_bldc_motor1.step_sta - 1]();
}
else /* 霍尔传感器错误、接触不良、断开等情况 */
{
stop_motor1();
g_bldc_motor1.run_flag = STOP;
}
}
}
}
上述功能编写完成后,就要在主函数初始化霍尔传感器、shutdowm引脚GPIO配置。以及初始化定时器。并且要实现按键key1按下占空比增加,并且占空比大于0是正传,否则是反转。key2按下占空比减小。
key = key_scan(0);
if(key == KEY0_PRES) /* 按下KEY0设置比较值+500 */
{
pwm_duty_temp += 500;
if(pwm_duty_temp >= MAX_PWM_DUTY/2) /* 限速 */
pwm_duty_temp = MAX_PWM_DUTY/2;
if(pwm_duty_temp > 0) /* 通过判断正负号设置旋转方向 */
{
g_bldc_motor1.pwm_duty = pwm_duty_temp;
g_bldc_motor1.dir = CW;
}
else
{
g_bldc_motor1.pwm_duty = -pwm_duty_temp;
g_bldc_motor1.dir = CCW;
}
g_bldc_motor1.run_flag = RUN; /* 开启运行 */
start_motor1(); /* 开启运行 */
}
else if(key == KEY1_PRES) /* 按下KEY1设置比较值-500 */
{
pwm_duty_temp -= 500;
if(pwm_duty_temp <= -MAX_PWM_DUTY/2)
pwm_duty_temp = -MAX_PWM_DUTY/2;
if(pwm_duty_temp < 0) /* 通过判断正负号设置旋转方向 */
{
g_bldc_motor1.pwm_duty = -pwm_duty_temp;
g_bldc_motor1.dir = CCW;
}
else
{
g_bldc_motor1.pwm_duty = pwm_duty_temp;
g_bldc_motor1.dir = CW;
}
g_bldc_motor1.run_flag = RUN; /* 开启运行 */
start_motor1(); /* 运行电机 */
}
else if(key == KEY2_PRES) /* 按下KEY2关闭电机 */
{
stop_motor1(); /* 停机 */
g_bldc_motor1.run_flag = STOP; /* 标记停机 */
pwm_duty_temp = 0; /* 数据清0 */
g_bldc_motor1.pwm_duty = 0;
}
delay_ms(10);
无刷电机电压温度电流采集:
三相电路采集电路分析:

上图中为U相电流采集电路。图 23.1.1.1 为 U 相电流采集电路(U、V、W 三相同理,这里只以 U 相为例),其中 R17为采样电阻(20mR),当有电流 I 流过采样电阻时,采样电阻上就会产生一个电压,电压大小为: I*0.02Ω(20mR)。我们使用 I-V 表示电流 I 经过采样电阻后形成的电压,所以 U相的电压,我们使用 I-V_U 表示。
这里使用采样电阻较小的原因为:
在U相电流采集电路中,采样电阻R17选择很小(20mR),主要有以下几个原因:
-
减少对主电路的影响 - 影响电路正常工作:在电机驱动等电路中,采样电阻是串联在主电路中的。如果采样电阻阻值较大,会在电路中产生较大的压降, 这就会使电机等负载两端的实际电压低于电源电压,影响负载的正常工作,导致电机转速不稳定或者达不到额定转速等问题。 - 增加功率损耗:根据功率计算公式(P = I^{2}R) ,电阻越大,在相同电流下,采样电阻自身消耗的功率就越大,这不仅会造成能量浪费,还可能导致采样电阻发热严重,甚至烧毁,影响电路的可靠性和稳定性。
-
提高测量精度和线性度 - 降低非线性影响:在实际应用中,采样电阻的阻值可能会随着温度等因素发生变化。阻值较小的采样电阻,其阻值变化的绝对值相对较小,对测量结果的影响也就更小,从而能更好地保证测量的线性度。 - 适配测量电路:多数电流采集电路后续会连接运算放大器等信号处理电路。较小的采样电阻产生的电压信号较小,便于通过合适的放大倍数进行放大处理,从而可以更精确地设置放大电路参数,以适应ADC(模拟 - 数字转换器)等采集设备的输入范围,提高测量精度。
-
快速响应电流变化 - 降低时间常数:采样电阻与电路中的寄生电容等会构成一个RC电路,其时间常数
\[\tau = RC\ \]。较小的采样电阻可以使时间常数减小,从而使电路能够更快地响应电流的变化,及时采集到准确的电流信号,满足对快速变化电流信号的实时采集需求。
因为电压较小,所以需要电压放大电路后来进行AD电压的采集:

下面首先要计算上图的放大倍数:

因此根据上图推导可以得到放大倍数为:12k/(1K+1K) =6。因为AMP IU是接到ADC引脚的,所以该值是可以得到的。所以可以进一步计算出来U相电流的值。
电源电压采集电路分析:

上图为电压跟随电路。电压跟随电路的结构非常简单,仅需将运放的输出端直接反馈到反相输入端(-),输入信号接入同相输入端(+),无需额外的电阻网络(或仅需平衡电阻)。所以 1 脚输出电压 VBUS 和电阻分压的值相同,即 VBUS = POWER/(12K+12K+1K)*1K,当 ADC 采集到 VBUS 电压后就可以计算出 POWER 电压。
温度采集电路分析:

上图中也是电压跟随电路。NTC电阻与固定的4.7k电阻分压。然后接进B运放的同相输入端。所以VTEMP的电压等于VTMEP = 3.3V/(Rt+4.7K)*4.7K。通过这个电压就能反推出NTC电阻的数值大小。这里我们采用双B值法计算:
T1=1 / ( In (Rt/R0) / B + 1/T2 ),
这里的 T1 和 T2 指的是 K 度即开尔文温度,K 度 = 273.15(绝对温度) + 摄氏度;其中:
⚫ T2 :273.15 + 25(25 摄氏度下的 K 度值)
⚫ T1 :实际温度
⚫ Rt :热敏电阻在 T1 温度下的阻值;
⚫ R0 :热敏电阻在 T2 常温下的标称阻值;
⚫ B 值:热敏电阻的重要恒定参数;
其中 T1 是我们需要求解的温度值,Rt 的大小由前面的温度采集电路可以算出,其他的参数都是可以在数据手册中查找得出,代入温度求解公式即可算出T1温度值,注意该值为K度。
由于需要采集电源电压、温度因此首先需要两路ADC。然后U、V、W三相电压也需要三路ADC。所以一共需要5路ADC。

所以这一节继续在上一节的基础上去配置ADC采集的相关部分就可以了。

ADC、DMA相关知识:
下面将复习下ADC和DMA的相关知识:
ADC GPIO初始化:
首先要初始化ADC对应的引脚,其实就是将其配置为模拟输入即可。其中各个引脚对应的ADC通道可以查看对应芯片数据手册。
#define ADC_Power_Pin GPIO_PIN_1
#define ADC_Power_Port GPIOB
#define ADC_Power_CLK_Enable() do{__HAL_RCC_GPIOB_CLK_ENABLE();}while(0) //电源端口GPIO配置
#define ADC_Power_channel ADC_CHANNEL_9
#define ADC_Temp_Pin GPIO_PIN_0
#define ADC_Temp_Port GPIOA
#define ADC_Temp_CLK_Enable() do{__HAL_RCC_GPIOA_CLK_ENABLE();}while(0) //温度端口GPIO配置
#define ADC_Temp_channel ADC_CHANNEL_0
#define ADC_MotorU_Pin GPIO_PIN_0
#define ADC_MotorU_Port GPIOB
#define ADC_MotorU_CLK_Enable() do{__HAL_RCC_GPIOB_CLK_ENABLE();}while(0) //电机U相配置
#define ADC_MotorU_channel ADC_CHANNEL_8
#define ADC_MotorV_Pin GPIO_PIN_6
#define ADC_MotorV_Port GPIOA
#define ADC_MotorV_CLK_Enable() do{__HAL_RCC_GPIOA_CLK_ENABLE();}while(0)//电机V相GPIO配置
#define ADC_MotorV_channel ADC_CHANNEL_6
#define ADC_MotorW_Pin GPIO_PIN_3
#define ADC_MotorW_Port GPIOA
#define ADC_MotorW_CLK_Enable() do{__HAL_RCC_GPIOA_CLK_ENABLE();}while(0)//电机W相GPIO配置
#define ADC_MotorW_channel ADC_CHANNEL_3
// 定义GPIO初始化结构体,并初始化为0(避免未初始化的随机值影响配置)
GPIO_InitTypeDef GPIO_InitStruct = {0};
// 使能ADC1外设的时钟(STM32外设必须先开时钟才能配置)
__HAL_RCC_ADC1_CLK_ENABLE();
// 依次使能各个ADC通道对应的GPIO端口时钟(宏定义封装了具体GPIO的时钟使能)
ADC_Power_CLK_Enable(); // 使能"电源相关ADC通道"的GPIO时钟(如供电电压采集引脚)
ADC_Temp_CLK_Enable(); // 使能"温度相关ADC通道"的GPIO时钟(如温度传感器引脚)
ADC_MotorU_CLK_Enable(); // 使能"电机U相ADC通道"的GPIO时钟(如电机U相电流采集引脚)
ADC_MotorV_CLK_Enable(); // 使能"电机V相ADC通道"的GPIO时钟(如电机V相电流采集引脚)
ADC_MotorW_CLK_Enable(); // 使能"电机W相ADC通道"的GPIO时钟(如电机W相电流采集引脚)
/* 配置所有ADC通道的GPIO为模拟输入模式(ADC采集必须的模式) */
// 配置"电源相关ADC通道"的引脚
GPIO_InitStruct.Pin = ADC_Power_Pin; // 指定引脚(如PA0,由宏ADC_Power_Pin定义)
GPIO_InitStruct.Mode = GPIO_MODE_ANALOG; // 模式:模拟输入(ADC专用,断开数字电路)
GPIO_InitStruct.Pull = GPIO_NOPULL; // 上下拉:不使能(模拟输入无需上下拉,避免干扰)
HAL_GPIO_Init(ADC_Power_Port, &GPIO_InitStruct); // 应用配置到对应GPIO端口(如GPIOA)
// 配置"温度相关ADC通道"的引脚(复用结构体,仅修改Pin参数)
GPIO_InitStruct.Pin = ADC_Temp_Pin; // 切换到温度采集引脚(如PA1)
HAL_GPIO_Init(ADC_Temp_Port, &GPIO_InitStruct); // 应用到温度引脚的GPIO端口
// 配置"电机U相ADC通道"的引脚
GPIO_InitStruct.Pin = ADC_MotorU_Pin; // 切换到电机U相采集引脚(如PA2)
HAL_GPIO_Init(ADC_MotorU_Port, &GPIO_InitStruct); // 应用到U相引脚的GPIO端口
// 配置"电机V相ADC通道"的引脚
GPIO_InitStruct.Pin = ADC_MotorV_Pin; // 切换到电机V相采集引脚(如PA3)
HAL_GPIO_Init(ADC_MotorV_Port, &GPIO_InitStruct); // 应用到V相引脚的GPIO端口
// 配置"电机W相ADC通道"的引脚
GPIO_InitStruct.Pin = ADC_MotorW_Pin; // 切换到电机W相采集引脚(如PA4)
HAL_GPIO_Init(ADC_MotorW_Port, &GPIO_InitStruct); // 应用到W相引脚的GPIO端口
接下来配置ADC1的对应模块。ADC1是挂在APB2总线上的,
时钟分析:
APB2总线时钟频率可以从程序刚开始初始化的时钟看出来。PLL的核心作用:“分频→倍频”生成高频时钟,PLL的任务是将输入时钟(如外部晶振HSE)经过“分频→倍频”处理,生成远高于输入频率的时钟,供系统(SYSCLK)或外设使用。对于STM32F4系列,PLL的计算涉及4个关键参数(对应代码中的pllm、plln、pllp、pllq),其中前三者决定系统时钟(SYSCLK)的频率:
步骤1:计算PLL的“中间频率”(VCO输入频率)
PLL首先对输入时钟(HSE)进行分频,得到“压控振荡器(VCO)”的输入频率。
公式:
代入数值:
步骤2:计算PLL的“输出频率”(VCO输出频率)
VCO会将输入频率倍频,得到PLL的原始输出频率。
公式:
代入数值:
步骤3:计算系统时钟(SYSCLK)
PLL输出频率需再经过分频,得到最终的系统时钟(SYSCLK)。分频由PLLP(pllp)控制,取值为2、4、6、8,对应分频系数。
公式:
代入数值(pllp=2):
- 分频(PLLM):将输入时钟降到VCO的“最佳工作范围”(STM32F4要求VCO输入为
1~2MHz)。如果直接用8MHz输入VCO,倍频后可能超出VCO的稳定工作区间,导致时钟不稳定。 - 倍频(PLLN):通过大倍数倍频,生成远高于输入的高频时钟(如336MHz),为后续分频提供“足够的频率余量”。
- 再分频(PLLP):将PLL输出分频到系统需要的频率(如STM32F407的最高SYSCLK为
168MHz),同时保证时钟的稳定性和精度。
上述计算中:
- VCO输入为
1MHz(在1~2MHz范围内); - PLL输出为
336MHz(≤344MHz); - SYSCLK为
168MHz(符合最高规格)。

上图可以看出若APB2分频系数不为1的话就会乘以2,所以APB2的时钟为168MHZ。
sys_stm32_clock_init(336, 8, 2, 7); /* 设置时钟,168Mhz */
uint8_t sys_stm32_clock_init(uint32_t plln, uint32_t pllm, uint32_t pllp, uint32_t pllq)
{
HAL_StatusTypeDef ret = HAL_OK;
RCC_ClkInitTypeDef rcc_clk_init_handle;
RCC_OscInitTypeDef rcc_osc_init_handle;
__HAL_RCC_PWR_CLK_ENABLE(); /* 使能PWR时钟 */
/* 下面这个设置用来设置调压器输出电压级别,以便在器件以最大频率工作时使性能与功耗实现平衡 */
__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1); /* VOS = 1, Scale1, 1.2V内核电压,FLASH访问可以得到最高性能 */
/* 使能HSE,并选择HSE作为PLL时钟源,配置PLL1,开启USB时钟 */
rcc_osc_init_handle.OscillatorType = RCC_OSCILLATORTYPE_HSE; /* 时钟源为HSE */
rcc_osc_init_handle.HSEState = RCC_HSE_ON; /* 打开HSE */
rcc_osc_init_handle.PLL.PLLState = RCC_PLL_ON; /* 打开PLL */
rcc_osc_init_handle.PLL.PLLSource = RCC_PLLSOURCE_HSE; /* PLL时钟源选择HSE */
rcc_osc_init_handle.PLL.PLLN = plln;
rcc_osc_init_handle.PLL.PLLM = pllm;
rcc_osc_init_handle.PLL.PLLP = pllp;
rcc_osc_init_handle.PLL.PLLQ = pllq;
ret = HAL_RCC_OscConfig(&rcc_osc_init_handle); /* 初始化RCC */
if(ret != HAL_OK)
{
return 1; /* 时钟初始化失败,可以在这里加入自己的处理 */
}
/* 选中PLL作为系统时钟源并且配置HCLK,PCLK1和PCLK2*/
rcc_clk_init_handle.ClockType = ( RCC_CLOCKTYPE_SYSCLK \
| RCC_CLOCKTYPE_HCLK \
| RCC_CLOCKTYPE_PCLK1 \
| RCC_CLOCKTYPE_PCLK2);
rcc_clk_init_handle.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; /* 设置系统时钟时钟源为PLL */
rcc_clk_init_handle.AHBCLKDivider = RCC_SYSCLK_DIV1; /* AHB分频系数为1 */
rcc_clk_init_handle.APB1CLKDivider = RCC_HCLK_DIV4; /* APB1分频系数为4 */
rcc_clk_init_handle.APB2CLKDivider = RCC_HCLK_DIV2; /* APB2分频系数为2 */
ret = HAL_RCC_ClockConfig(&rcc_clk_init_handle, FLASH_LATENCY_5); /* 同时设置FLASH延时周期为5WS,也就是6个CPU周期 */
if(ret != HAL_OK)
{
return 1; /* 时钟初始化失败 */
}
/* STM32F405x/407x/415x/417x Z版本的器件支持预取功能 */
if (HAL_GetREVID() == 0x1001)
{
__HAL_FLASH_PREFETCH_BUFFER_ENABLE(); /* 使能flash预取 */
}
return 0;
}
ADC配置:
ADC的时钟由下列配置可以看出来为81/4=21MHZ.并且5个点为一轮。并且设置了扫描模式和连续转换模式。扫描模式决定了可以按照顺序扫描配置的5个引脚对应的通道。连续转换模式开启后可以5个通道采集完后继续开始下一轮。并且一个单次采集转换的为ADC_SAMPLETIME_3CYCLES。
单个数据转换时间 = (采样时间周期数 + 硬件固定周期数) × ADC时钟周期
ADC时钟周期 = 1 ÷ 21MHz ≈ 47.62 纳秒(ns)
总时钟周期数 = 采样时间周期数(3) + 硬件固定周期数(12.5) = 15.5 个ADC时钟周期
所以完成一个采样点为:47.62 纳秒(ns)*15.5 = 738.11纳秒约等于0.74us。完成一轮采样为:0.74us.转换的顺序为按照rank的顺序来的。
ADC_ChannelConfTypeDef sConfig = {0}; // 定义ADC通道配置结构体,初始化为0
// 配置ADC句柄(g_adc_nch_dma_handle)的全局参数
g_adc_nch_dma_handle.Instance = USE_ADC; // 指定使用的ADC外设(如ADC1、ADC2,由USE_ADC宏定义)
g_adc_nch_dma_handle.Init.ClockPrescaler = ADC_CLOCKPRESCALER_PCLK_DIV4; // ADC时钟分频:PCLK(外设总线时钟)÷4
g_adc_nch_dma_handle.Init.Resolution = ADC_RESOLUTION_12B; // ADC分辨率:12位(可采集0~4095的数值,对应0~参考电压)
g_adc_nch_dma_handle.Init.ScanConvMode = ENABLE; // 使能“扫描模式”:多通道采集时,按顺序扫描所有配置的通道
g_adc_nch_dma_handle.Init.ContinuousConvMode = ENABLE; // 使能“连续转换模式”:一次转换完成后,自动开始下一次转换
g_adc_nch_dma_handle.Init.DiscontinuousConvMode = DISABLE; // 禁用“不连续转换模式”(与连续模式互斥)
g_adc_nch_dma_handle.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE; // 禁用“外部触发沿”:不通过外部信号(如定时器)触发ADC
g_adc_nch_dma_handle.Init.ExternalTrigConv = ADC_SOFTWARE_START; // 触发方式:软件触发(通过代码主动启动ADC转换)
g_adc_nch_dma_handle.Init.DataAlign = ADC_DATAALIGN_RIGHT; // 数据对齐方式:右对齐(采集结果的低位在数据寄存器的低12位)
g_adc_nch_dma_handle.Init.NbrOfConversion = ADC_NUM; // 配置的“转换通道总数”(由ADC_NUM宏定义,此处为5个通道)
g_adc_nch_dma_handle.Init.DMAContinuousRequests = ENABLE; // 使能“DMA连续请求”:ADC转换结果自动通过DMA传输到内存,无需CPU干预
g_adc_nch_dma_handle.Init.EOCSelection = ADC_EOC_SEQ_CONV; // EOC(转换完成标志)选择:序列转换完成后触发EOC(适合多通道连续采集)
HAL_ADC_Init(&g_adc_nch_dma_handle); // 应用上述全局配置,初始化ADC外设
sConfig.Channel = ADC_Power_channel;
sConfig.Rank = 1;
sConfig.SamplingTime = ADC_SAMPLETIME_3CYCLES;
HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &sConfig);
sConfig.Channel = ADC_Temp_channel;
sConfig.Rank = 2;
sConfig.SamplingTime = ADC_SAMPLETIME_3CYCLES;
HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &sConfig);
sConfig.Channel = ADC_MotorU_channel;
sConfig.Rank = 3;
sConfig.SamplingTime = ADC_SAMPLETIME_3CYCLES;
HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &sConfig);
sConfig.Channel = ADC_MotorV_channel;
sConfig.Rank = 4;
sConfig.SamplingTime = ADC_SAMPLETIME_3CYCLES;
HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &sConfig);
sConfig.Channel = ADC_MotorW_channel;
sConfig.Rank = 5;
sConfig.SamplingTime = ADC_SAMPLETIME_3CYCLES;
HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &sConfig);
接下来还需要配置DMA:

可以看到ADC1对应的通道为通道0;
#define ADC_DMA DMA2_Stream4
#define ADC_DMA_chanel DMA_CHANNEL_0
#define ADC_DMA_IRQN DMA2_Stream4_IRQn
#define ADC_DMA_IRQHandler DMA2_Stream4_IRQHandler
一、DMA时钟使能
```c
__HAL_RCC_DMA2_CLK_ENABLE();
```
- **作用**:开启DMA2外设的时钟。
- **关键背景**:STM32的外设(包括DMA)默认处于时钟关闭状态(为降低功耗),**必须先使能时钟,后续的DMA配置才能生效**。
- 细节:ADC1/2通常与DMA2的特定流/通道绑定(如ADC1对应DMA2_Stream0),因此此处开启DMA2时钟(具体看芯片型号,部分低功耗型号用DMA1)。
二、DMA句柄核心参数配置
`g_dma_nch_adc_handle`是DMA的句柄结构体(`DMA_HandleTypeDef`类型),用于存储DMA的配置信息和运行状态,以下是结构体成员的详细配置:
1. 指定DMA外设实例
```c
g_dma_nch_adc_handle.Instance = ADC_DMA;
```
- **作用**:明确当前配置的是哪一个DMA流(Stream)。
- **细节**:`ADC_DMA`是宏定义(如`#define ADC_DMA DMA2_Stream0`),代表与ADC绑定的具体DMA流。STM32的DMA有多个“流”(Stream),每个流对应不同的外设请求,需根据芯片手册确认ADC对应的DMA流(如STM32F407的ADC1连续转换请求对应DMA2_Stream0)。
#### 2. 指定DMA通道
```c
g_dma_nch_adc_handle.Init.Channel = ADC_DMA_chanel;
```
- **作用**:选择当前DMA流对应的“通道”(Channel)。
- **关键背景**:每个DMA流可对应多个“通道”,每个通道绑定一个特定的外设请求(如ADC1、TIM1等)。
- 细节:`ADC_DMA_chanel`是宏定义(如`#define ADC_DMA_chanel DMA_CHANNEL_0`),需与ADC外设的DMA请求通道匹配(确保ADC的转换请求能正确触发该DMA通道)。
#### 3. 配置DMA传输方向
```c
g_dma_nch_adc_handle.Init.Direction = DMA_PERIPH_TO_MEMORY;
```
- **作用**:设置DMA数据传输的方向。
- **选项说明**:
- `DMA_PERIPH_TO_MEMORY`:外设到内存(此处“外设”是ADC的数据寄存器DR,“内存”是`g_adc_value`数组);
- 其他方向:`DMA_MEMORY_TO_PERIPH`(内存到外设,如UART发送)、`DMA_MEMORY_TO_MEMORY`(内存到内存,如数据拷贝)。
- 适配场景:ADC采集是“外设生成数据→传入内存存储”,因此必须选此方向。
#### 4. 配置外设地址增量模式
```c
g_dma_nch_adc_handle.Init.PeriphInc = DMA_PINC_DISABLE; /* 外设非增量模式 */
```
- **作用**:设置DMA传输时,外设地址是否自动递增。
- **逻辑解析**:
- ADC的数据寄存器(DR)是**固定地址的单寄存器**(无论多少通道,结果都写入同一个`ADC_DR`);
- 因此无需递增外设地址(每次都读取同一个`ADC_DR`),故设为`DMA_PINC_DISABLE`(禁用增量)。
#### 5. 配置内存地址增量模式
```c
g_dma_nch_adc_handle.Init.MemInc = DMA_MINC_ENABLE; /* 存储器增量模式 */
```
- **作用**:设置DMA传输时,内存地址是否自动递增。
- **逻辑解析**:
- 内存目标是`g_adc_value`数组(存储多个ADC转换结果);
- 需让DMA每次传输后,内存地址+1(依次写入`g_adc_value[0]`→`g_adc_value[1]`→...),故设为`DMA_MINC_ENABLE`(使能增量)。
#### 6. 配置外设数据宽度
```c
g_dma_nch_adc_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; /* 外设数据长度:16位 */
```
- **作用**:设置外设数据的位宽(与ADC数据寄存器匹配)。
- **细节**:
- ADC是12位分辨率,转换结果存储在16位的`ADC_DR`寄存器中(右对齐,高4位无效);
- `DMA_PDATAALIGN_HALFWORD`表示“半字”(16位),与`ADC_DR`的宽度完全匹配(若选8位会截断数据,选32位会浪费空间)。
#### 7. 配置内存数据宽度
```c
g_dma_nch_adc_handle.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; /* 存储器数据长度:16位 */
```
- **作用**:设置内存中存储数据的位宽(与目标数组类型匹配)。
- **逻辑解析**:
- 目标数组`g_adc_value`通常定义为`uint16_t`类型(16位),用于存储12位的ADC结果;
- 内存与外设数据宽度保持一致(均为16位),避免数据传输错位。
#### 8. 配置DMA工作模式
```c
g_dma_nch_adc_handle.Init.Mode = DMA_CIRCULAR; /* 外设流控模式 */
```
- **作用**:设置DMA的传输模式(循环/非循环)。
- **关键说明**:
- `DMA_CIRCULAR`:循环模式——当DMA传输完`ADC_NUM×ADC_COLL`个数据后,**自动从内存首地址重新开始覆盖存储**,实现“无间断连续采集”;
- 对比`DMA_NORMAL`(非循环模式):传输完指定数据后停止,需重新启动DMA才会继续采集。
- 适配场景:你的ADC配置了“连续转换模式”,因此DMA必须选循环模式,才能匹配ADC的持续输出。
#### 9. 配置DMA优先级
```c
g_dma_nch_adc_handle.Init.Priority = DMA_PRIORITY_MEDIUM; /* 中等优先级 */
```
- **作用**:设置DMA流的优先级(当多个DMA流同时请求时,决定谁先执行)。
- **优先级等级**:`DMA_PRIORITY_LOW`(低)、`DMA_PRIORITY_MEDIUM`(中)、`DMA_PRIORITY_HIGH`(高)、`DMA_PRIORITY_VERY_HIGH`(极高)。
- 配置逻辑:ADC采集(如电机电流)需要一定实时性,但通常不高于电机PWM控制的DMA,因此设为中等优先级(平衡实时性和系统资源分配)。
### 三、初始化DMA外设
```c
HAL_DMA_Init(&g_dma_nch_adc_handle);
```
- **作用**:将上述配置的`g_dma_nch_adc_handle.Init`参数写入DMA的硬件寄存器,完成DMA的底层初始化。
- 本质:HAL库封装的“配置生效函数”,调用后DMA硬件才会按配置工作。
### 四、关联ADC与DMA句柄
```c
__HAL_LINKDMA(&g_adc_nch_dma_handle, DMA_Handle, g_dma_nch_adc_handle);
```
- **作用**:将ADC句柄(`g_adc_nch_dma_handle`)与DMA句柄(`g_dma_nch_adc_handle`)绑定。
- **关键意义**:
- 建立关联后,ADC的转换请求能自动触发DMA传输(无需CPU手动衔接);
- ADC句柄中会保存DMA句柄的地址,后续`HAL_ADC_Start_DMA()`等函数可直接通过ADC句柄操作DMA。
- 语法解析:`__HAL_LINKDMA(adc_handle, dma_member, dma_handle)`——`dma_member`是ADC句柄中用于存储DMA句柄的成员(固定为`DMA_Handle`)。
### 五、配置DMA中断(可选但常用)
#### 1. 设置DMA中断优先级
```c
HAL_NVIC_SetPriority(ADC_DMA_IRQN, 2, 1);
```
- **作用**:配置DMA中断的“抢占优先级”和“子优先级”。
- **参数说明**:
- `ADC_DMA_IRQN`:DMA中断号(宏定义,如`DMA2_Stream0_IRQn`,对应配置的DMA流);
- `2`:抢占优先级(数值越小,优先级越高,需与系统其他中断协调,如电机控制中断);
- `1`:子优先级(抢占优先级相同时,子优先级小的先执行)。
#### 2. 使能DMA中断
```c
HAL_NVIC_EnableIRQ(ADC_DMA_IRQN);
```
- **作用**:在NVIC(嵌套向量中断控制器)中使能DMA中断。
- 触发场景:当DMA传输完成指定数据量(或发生传输错误)时,会触发中断,进入中断服务函数(如`DMA2_Stream0_IRQHandler`)。
- 适配你的场景:之前提到的“2.6ms进入中断”,就是通过DMA中断实现的(传输完`ADC_NUM×ADC_COLL`个数据后触发)。
### 六、启动ADC+DMA连续采集
```c
HAL_ADC_Start_DMA(&g_adc_nch_dma_handle, (uint32_t *)g_adc_value, ADC_NUM * ADC_COLL);
```
- **作用**:启动ADC连续转换,并开启DMA自动传输,是整个采集流程的“启动开关”。
- **参数详解**:
1. `&g_adc_nch_dma_handle`:已关联DMA的ADC句柄;
2. `(uint32_t *)g_adc_value`:DMA传输的目标内存地址(`g_adc_value`数组的首地址,强制转换为`uint32_t*`是因为HAL库参数要求);
3. `ADC_NUM * ADC_COLL`:DMA单次传输的总数据量(`ADC_NUM`=5个通道,`ADC_COLL`=每个通道采集点数,乘积为总点数)。
- **启动后的流程**:
1. 软件触发ADC开始连续转换(符合`ADC_SOFTWARE_START`配置);
2. ADC按扫描模式依次转换5个通道,每个通道转换完成后触发DMA传输;
3. DMA将`ADC_DR`的数据写入`g_adc_value`数组,地址自动递增;
4. 传输完`ADC_NUM×ADC_COLL`个数据后,若为循环模式,自动重启传输;若需处理数据,可在DMA中断中做批量处理。
### 整体逻辑总结
这段代码的核心是**搭建“ADC连续扫描→DMA自动搬数→内存存储”的硬件自动化流程**,具体链路如下:
```
使能DMA时钟 → 配置DMA传输规则(方向/地址/宽度/模式)→ 初始化DMA → 绑定ADC与DMA → 配置DMA中断 → 启动ADC+DMA → 自动采集+传输
```
其最大优势是**全程无CPU干预**:ADC生成数据后,DMA直接搬运到内存,CPU可专注于电机控制、数据处理等核心任务,完美适配高频、多通道的实时采集场景。
以上配置完成后,完成采集后会进入中断:
因为ADC是DMA的通道0,当ADC每个通道采集完成后会发送DMA请求,而对应的DMA请求是通道4。所以对应的中断为:DMA2_Stream4_IRQn,对应的处理函数为:DMA2_Stream4_IRQHandler。当DMA传输完成后,会进入对应的中断处理函数。
// 若注册了传输完成回调,调用(关键:连接DMA与ADC回调的桥梁)
if(hdma->XferCpltCallback != NULL)
{
hdma->XferCpltCallback(hdma);
}
这个函数的绑定回调是在HAL_StatusTypeDef HAL_ADC_Start_DMA(ADC_HandleTypeDef* hadc, uint32_t* pData, uint32_t Length)函数中的 
会进一步调用:

所以只需要重写ADC回调函数数据即可。
void ADC_DMA_IRQHandler()
{
HAL_DMA_IRQHandler(&g_dma_nch_adc_handle);
}
```c
// DMA中断处理函数:响应DMA各类中断事件(完成、半传输、错误等),并调用对应回调
void HAL_DMA_IRQHandler(DMA_HandleTypeDef *hdma)
{
uint32_t tmpisr; // 暂存DMA中断状态寄存器(ISR)的值
__IO uint32_t count = 0U; // 超时计数变量(__IO确保volatile,防止编译器优化)
uint32_t timeout = SystemCoreClock / 9600U; // 超时阈值(按系统主频计算,用于硬件操作超时保护)
/* 计算当前DMA流对应的寄存器基地址 */
DMA_Base_Registers *regs = (DMA_Base_Registers *)hdma->StreamBaseAddress;
tmpisr = regs->ISR; // 读取DMA中断状态寄存器,获取所有中断标志
/* 传输错误中断管理 ***************************************/
// 检查当前DMA流的"传输错误中断标志(TEIF)"是否置位
if ((tmpisr & (DMA_FLAG_TEIF0_4 << hdma->StreamIndex)) != RESET)
{
// 进一步确认是否使能了"传输错误中断"(避免误处理未使能的中断)
if(__HAL_DMA_GET_IT_SOURCE(hdma, DMA_IT_TE) != RESET)
{
hdma->Instance->CR &= ~(DMA_IT_TE); // 禁用传输错误中断,防止持续触发
regs->IFCR = DMA_FLAG_TEIF0_4 << hdma->StreamIndex; // 清除传输错误标志
hdma->ErrorCode |= HAL_DMA_ERROR_TE; // 更新错误码:标记发生传输错误
}
}
/* FIFO错误中断管理 ******************************************/
// 检查当前DMA流的"FIFO错误中断标志(FEIF)"是否置位
if ((tmpisr & (DMA_FLAG_FEIF0_4 << hdma->StreamIndex)) != RESET)
{
// 确认是否使能了"FIFO错误中断"
if(__HAL_DMA_GET_IT_SOURCE(hdma, DMA_IT_FE) != RESET)
{
regs->IFCR = DMA_FLAG_FEIF0_4 << hdma->StreamIndex; // 清除FIFO错误标志
hdma->ErrorCode |= HAL_DMA_ERROR_FE; // 更新错误码:标记发生FIFO错误
}
}
/* 直接模式错误中断管理 ***********************************/
// 检查当前DMA流的"直接模式错误中断标志(DMEIF)"是否置位
if ((tmpisr & (DMA_FLAG_DMEIF0_4 << hdma->StreamIndex)) != RESET)
{
// 确认是否使能了"直接模式错误中断"
if(__HAL_DMA_GET_IT_SOURCE(hdma, DMA_IT_DME) != RESET)
{
regs->IFCR = DMA_FLAG_DMEIF0_4 << hdma->StreamIndex; // 清除直接模式错误标志
hdma->ErrorCode |= HAL_DMA_ERROR_DME; // 更新错误码:标记发生直接模式错误
}
}
/* 半传输完成中断管理 ******************************/
// 检查当前DMA流的"半传输完成中断标志(HTIF)"是否置位
if ((tmpisr & (DMA_FLAG_HTIF0_4 << hdma->StreamIndex)) != RESET)
{
// 确认是否使能了"半传输完成中断"
if(__HAL_DMA_GET_IT_SOURCE(hdma, DMA_IT_HT) != RESET)
{
regs->IFCR = DMA_FLAG_HTIF0_4 << hdma->StreamIndex; // 清除半传输完成标志
// 判断是否使能了"多缓冲区模式(DBM)"
if(((hdma->Instance->CR) & (uint32_t)(DMA_SxCR_DBM)) != RESET)
{
// 检查当前使用的是"内存缓冲区0"(CT位为0)
if((hdma->Instance->CR & DMA_SxCR_CT) == RESET)
{
// 若注册了缓冲区0半传输回调,调用该回调
if(hdma->XferHalfCpltCallback != NULL)
{
hdma->XferHalfCpltCallback(hdma);
}
}
// 当前使用的是"内存缓冲区1"(CT位为1)
else
{
// 若注册了缓冲区1半传输回调,调用该回调
if(hdma->XferM1HalfCpltCallback != NULL)
{
hdma->XferM1HalfCpltCallback(hdma);
}
}
}
// 未使能多缓冲区模式(普通模式)
else
{
// 若DMA为"非循环模式",禁用半传输中断(仅触发一次)
if((hdma->Instance->CR & DMA_SxCR_CIRC) == RESET)
{
hdma->Instance->CR &= ~(DMA_IT_HT);
}
// 若注册了半传输回调,调用该回调
if(hdma->XferHalfCpltCallback != NULL)
{
hdma->XferHalfCpltCallback(hdma);
}
}
}
}
/* 传输完成中断管理 ***********************************/
// 检查当前DMA流的"传输完成中断标志(TCIF)"是否置位(核心:触发ADC回调的源头)
if ((tmpisr & (DMA_FLAG_TCIF0_4 << hdma->StreamIndex)) != RESET)
{
// 确认是否使能了"传输完成中断"
if(__HAL_DMA_GET_IT_SOURCE(hdma, DMA_IT_TC) != RESET)
{
regs->IFCR = DMA_FLAG_TCIF0_4 << hdma->StreamIndex; // 清除传输完成标志
// 若DMA当前处于"中止状态",处理中止逻辑
if(HAL_DMA_STATE_ABORT == hdma->State)
{
// 禁用所有传输相关中断(完成、错误、直接模式)
hdma->Instance->CR &= ~(DMA_IT_TC | DMA_IT_TE | DMA_IT_DME);
hdma->Instance->FCR &= ~(DMA_IT_FE); // 禁用FIFO错误中断
// 若注册了半传输回调,也禁用半传输中断
if((hdma->XferHalfCpltCallback != NULL) || (hdma->XferM1HalfCpltCallback != NULL))
{
hdma->Instance->CR &= ~(DMA_IT_HT);
}
regs->IFCR = 0x3FU << hdma->StreamIndex; // 清除当前流的所有中断标志
__HAL_UNLOCK(hdma); // 解锁DMA,允许后续配置
hdma->State = HAL_DMA_STATE_READY; // 更新DMA状态为"就绪"
// 若注册了中止回调,调用该回调
if(hdma->XferAbortCallback != NULL)
{
hdma->XferAbortCallback(hdma);
}
return; // 中止处理完成,退出函数
}
// 判断是否使能了"多缓冲区模式"
if(((hdma->Instance->CR) & (uint32_t)(DMA_SxCR_DBM)) != RESET)
{
// 当前使用的是"内存缓冲区0"
if((hdma->Instance->CR & DMA_SxCR_CT) == RESET)
{
// 调用缓冲区1的传输完成回调
if(hdma->XferM1CpltCallback != NULL)
{
hdma->XferM1CpltCallback(hdma);
}
}
// 当前使用的是"内存缓冲区1"
else
{
// 调用缓冲区0的传输完成回调
if(hdma->XferCpltCallback != NULL)
{
hdma->XferCpltCallback(hdma);
}
}
}
// 未使能多缓冲区模式(普通模式)
else
{
// 若DMA为"非循环模式",禁用传输完成中断(仅触发一次)
if((hdma->Instance->CR & DMA_SxCR_CIRC) == RESET)
{
hdma->Instance->CR &= ~(DMA_IT_TC); // 禁用传输完成中断
__HAL_UNLOCK(hdma); // 解锁DMA
hdma->State = HAL_DMA_STATE_READY; // 更新状态为"就绪"
}
// 若注册了传输完成回调,调用(关键:连接DMA与ADC回调的桥梁)
if(hdma->XferCpltCallback != NULL)
{
hdma->XferCpltCallback(hdma);
}
}
}
}
/* 错误情况统一处理 */
// 若DMA发生过错误(ErrorCode非"无错误")
if(hdma->ErrorCode != HAL_DMA_ERROR_NONE)
{
// 若错误类型包含"传输错误(TE)"
if((hdma->ErrorCode & HAL_DMA_ERROR_TE) != RESET)
{
hdma->State = HAL_DMA_STATE_ABORT; // 更新状态为"中止"
__HAL_DMA_DISABLE(hdma); // 禁用当前DMA流
// 等待DMA流真正禁用(超时保护,防止死循环)
do
{
if (++count > timeout) // 计数超过阈值,退出等待
{
break;
}
}
while((hdma->Instance->CR & DMA_SxCR_EN) != RESET); // 等待EN位清零(流禁用)
__HAL_UNLOCK(hdma); // 解锁DMA
hdma->State = HAL_DMA_STATE_READY; // 更新状态为"就绪"
}
// 若注册了错误回调,调用该回调(通知用户DMA出错)
if(hdma->XferErrorCallback != NULL)
{
hdma->XferErrorCallback(hdma);
}
}
}
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
if (hadc->Instance == ADC1) /* 大约2.6ms采集完成进入中断 */
{
HAL_ADC_Stop_DMA(&g_adc_nch_dma_handle); /* 关闭DMA转换 */
calc_adc_val(g_adc_val); /* ADC数值转换 */
HAL_ADC_Start_DMA(&g_adc_nch_dma_handle, (uint32_t *)&g_adc_value, (uint32_t)(ADC_SUM)); /* 再启动DMA转换*/
}
}
ADC回调函数中做的事情很简单,关闭DMA转换,对采集到的数据做平均值处理:
void calc_adc_val(uint16_t *p)
{
uint32_t temp[ADC_NUM] = {0,0,0};
for(int i = 0;i < ADC_COLL;i++)
{
for(int j = 0;j < ADC_NUM;j++)
{
temp[j] += g_adc_value[i*ADC_NUM+j];
}
}
for(int j = 0;j < ADC_NUM;j++)
{
temp[j] /= ADC_COLL;
p[j] = temp[j];
}
}
对以上过程进行总结。首先就是ADC GPIO端口配置的初始化,初始化后GPIO后就可以进一步配置ADC相关的选项。首先定义并初始化了ADC通道配置结构体sConfig(用于后续具体通道的引脚和采样时间等参数配置);接着通过ADC句柄g_adc_nch_dma_handle配置核心参数,包括指定使用由USE_ADC宏定义的具体ADC外设(如ADC1或ADC2),将ADC时钟设置为外设总线时钟(PCLK)的1/4以保证转换精度,采用12位分辨率使转换结果范围为04095(对应0参考电压),使能扫描模式以支持多通道按顺序采集,开启连续转换模式让ADC在一次转换完成后自动启动下一次转换(同时禁用互斥的不连续转换模式),设置为软件触发方式(禁用外部信号触发),转换数据采用右对齐便于处理,指定由ADC_NUM宏定义的转换通道总数(此处为5个),使能DMA连续请求功能让转换结果自动通过DMA传输到内存而无需CPU干预,
以及将EOC(转换完成标志)设置为序列转换完成时触发(适合多通道连续采集场景),
- 当 ADC 配置为多通道扫描模式时,会按顺序依次对每个通道进行转换(例如 5 个通道时,会按通道 1→2→3→4→5 的顺序完成一轮转换)。
- 若 EOC 设置为 "序列转换完成时触发",则只有当这 5 个通道全部转换完毕后,才会产生一次 EOC 标志。
- 这个标志会触发后续操作:如果开启了 DMA,则触发一次 DMA 传输,将这一轮所有通道的转换结果一次性传输到内存;如果使能了中断,则会触发 ADC 中断,通知 CPU 可以处理这一轮的完整数据。
这段代码是STM32中ADC配合DMA工作的初始化配置及启动过程,具体功能如下:
- 首先使能DMA2时钟(
__HAL_RCC_DMA2_CLK_ENABLE()),为DMA外设提供工作时钟。 - 配置DMA句柄(
g_dma_nch_adc_handle)参数: - 指定使用的DMA外设(Instance = ADC_DMA)和通道(Channel = ADC_DMA_chanel) - 设置数据传输方向为外设到存储器(Direction = DMA_PERIPH_TO_MEMORY),即ADC数据传输到内存 - 禁用外设地址增量(PeriphInc = DMA_PINC_DISABLE),ADC数据寄存器地址固定 - 使能存储器地址增量(MemInc = DMA_MINC_ENABLE),内存缓冲区地址自动递增 - 外设和存储器数据长度均为16位(HALFWORD),匹配ADC的12位转换结果 - 工作模式设为循环模式(Mode = DMA_CIRCULAR),缓冲区满后自动从头开始覆盖 - 设置中等优先级(Priority = DMA_PRIORITY_MEDIUM) - 初始化DMA(
HAL_DMA_Init)并将DMA与ADC句柄关联(__HAL_LINKDMA),建立两者的硬件连接。 - 配置DMA中断:设置中断优先级(
HAL_NVIC_SetPriority)并使能中断(HAL_NVIC_EnableIRQ),用于处理DMA传输完成等事件。 - 最后启动ADC的DMA传输(
HAL_ADC_Start_DMA),指定数据存储缓冲区(g_adc_value)和传输数据量(ADC_NUM * ADC_COLL),使ADC开始连续转换并通过DMA自动将结果传输到内存。
工作流程就是:
- ADC 按序列(
ADC_NUM个通道)连续转换,每完成一个序列触发一次 EOC,触发 DMA 传输该序列数据; - DMA 持续传输数据,直到累计传输完
ADC_SUM(ADC_NUM * ADC_COLL)个数据; - DMA 完成
ADC_SUM个数据传输后,触发 “DMA 传输完成中断”; - 中断服务函数
ADC_DMA_IRQHandler调用 HAL 层处理函数,最终触发HAL_ADC_ConvCpltCallback。
计算电源电压和温度:
由于电源电压是第一个采集的通道,因此根据电源电压采集电路分析可以得到转换关系:
sprintf(buf,"Powervalue:%.1f",(g_adc_val[0]*ADC_Power_Convert));/* 显示输入电压的变化 */
#define ADC_Power_Convert (float)(3.3f / 4096 * 25)
由于分辨率是12位,并且电压是3.3v。所以一位adc值代表的就是3.3f/2^12 ,再根据分压电路分析可得对应的电源电压。
NTC采集温度一样的道理,根据ADC采集到的电压可以得到对应的电阻值,再根据双B值可以计算出对应的温度。
float Get_temp(uint16_t *p)
{
float r_t = 3.3f /((p[1] * (3.3f / 4096.0f)) / 4700.0f) -4700.0f;
float tem = 1/(log(r_t/10000.0f)/3380.0f + 1/(273.15f + 25.0f));
return tem - 273.15f;
}
计算三相电流以及母线电流:
计算三相电流要计算电机不转时候的电流。在定时器 6 更新中断里所操作的内容:首先判断电机是否停止,是的话将此时的三相电流所对应的 ADC 通道采集的 ADC 值进行累加取平均,即软件滤波,此时滤波后的值才是电机未启动时的基准电压。
bldc_init(10000-1,0);16.8kHz PWM波 每经过一个 PWM 周期(1/16.8kHz ≈ 59.5μs),中断触发一次。
btim_timx_int_init(1000 - 1,84 - 1); //1ms
- 时机:电机还没转(比如上电后、待机时),此时理论上 “采样电阻没有电流流过,应该无电压”,是测基准的最佳时机。
- 判断状态:先确认电机确实没转(比如通过霍尔传感器或编码器判断转子静止);
- 多次采集:让 ADC 反复采集三相采样电阻的电压(比如采集 50 次);
- 软件滤波:把 50次的采集值加起来再除以 50(取平均)。这么做是为了消除偶然的电压波动(比如某次采集受干扰跳变),得到一个稳定的 “基准值”。
else if(htim->Instance == TIM6)
{
if(g_bldc_motor1.run_flag == STOP)
{
uint32_t avg[3] = {0,0,0};
adc_amp_offset[0][adc_amp_offset_p] = g_adc_val[2];
adc_amp_offset[1][adc_amp_offset_p] = g_adc_val[3];
adc_amp_offset[2][adc_amp_offset_p] = g_adc_val[4];
adc_amp_offset_p++;
if(adc_amp_offset_p > ADC_COLL)
{
adc_amp_offset_p = 0;
}
for(int j = 0;j < 3;j++)
{
for(int i = 0;i < ADC_COLL;i++)
{
avg[j] += adc_amp_offset[j][i];
}
avg[j] /= ADC_COLL;
adc_amp_offset[j][ADC_COLL] = avg[j];
}
}
}
有了基准值后,再在电机运行时候采集ADC值,减去基准值电流即可
#define ADC2CURT (float)(3.3f / 4.096f / 0.12f) //采集的电流大小为ma.
for(int i = 0;i < 3;i++)
{
adc_val_ml[i] = g_adc_val[i+2];
adc_amp[i] = adc_val_ml[i] - adc_amp_offset[i][ADC_COLL];
if(adc_amp[i] > 0)
{
adc_amp_un[i] = adc_amp[i];
}
}
/* 运算母线电流(母线电流为任意两个有开关动作的相电流之和) */
if(g_bldc_motor1.step_sta == 0x05)
{
adc_amp_bus= (adc_amp_un[0] + adc_amp_un[1])*ADC2CURT; /* UV */
}
else if(g_bldc_motor1.step_sta == 0x01)
{
adc_amp_bus= (adc_amp_un[0] + adc_amp_un[2])*ADC2CURT; /* UW */
}
else if(g_bldc_motor1.step_sta == 0x03)
{
adc_amp_bus= (adc_amp_un[1] + adc_amp_un[2])*ADC2CURT; /* VW */
}
else if(g_bldc_motor1.step_sta == 0x02)
{
adc_amp_bus= (adc_amp_un[0] + adc_amp_un[1])*ADC2CURT; /* UV */
}
else if(g_bldc_motor1.step_sta == 0x06)
{
adc_amp_bus= (adc_amp_un[0] + adc_amp_un[2])*ADC2CURT; /* WU */
}
else if(g_bldc_motor1.step_sta == 0x04)
{
adc_amp_bus= (adc_amp_un[2] + adc_amp_un[1])*ADC2CURT; /* WV */
}
本文来自博客园,作者:Bathwind_W,转载请注明原文链接:https://www.cnblogs.com/bathwind/p/19051821

浙公网安备 33010602011771号