简介:面向STM32与Proteus开发者的直流无刷电机联合仿真资源,工程在Proteus 8.7中搭建,主控为STM32F103R6,移植μC/OS-II操作系统,通过MOS管驱动BLDC_STAR电机,并驱动AMPIRE128X64液晶屏显示,字库为自制,覆盖电机控制、任务调度与图形显示三大主题。压缩包共204个文件,包括48个C源码、38个头文件、Proteus工程文件、Keil工程配置、编译中间文件以及hex/axf烧录文件,整体仅4.9MB;其中C源码与头文件涉及系统移植、电机驱动和LCD显示,Proteus工程可直接打开运行,hex/axf文件便于对照验证,目录划分清楚。已有4679人学习下载,适合正在学习μC/OS-II、BLDC控制或Proteus仿真的读者。使用者可获得完整可打开的仿真工程与Keil代码,既能复现BLDC换相与LCD刷屏效果,也可参考其自制字库与模块化结构进行二次开发,是入门电机控制与嵌入式实时操作系统的实用资料,适合课程设计、毕业设计或竞赛实训参考。 入坑嵌入式之后,我一直想找一个能同时验证“电机控制 + 人机交互 + 实时操作系统”的工程,但网上的例子通常各管各的:Proteus BLDC仿真的文章只讲换相逻辑,uC/OS-II教程只讲任务调度和信号量,LCD显示又多停留在点亮屏幕。这次我干脆把它们合在同一个Proteus工程里:用STM32F103C8做控制核心,驱动BLDC无刷电机,霍尔传感器负责六步换相,PWM调速,一块12864液晶屏实时显示转速和运行状态,顶层用uC/OS-II把电机控制、按键扫描、屏幕刷新切成三个任务。这篇文章就是整个项目的完整记录,从搭建元件、连接线路、写换相逻辑、移植uC/OS-II,到踩坑排查,一并整理出来。
这个Demo适合两类读者:一类是想入门BLDC控制,但暂时没有硬件条件,想在仿真里先把逻辑跑通的人;另一类是刚接触RTOS,想找个真实工程搞明白任务怎么划分、优先级怎么配的人。即使你之前没碰过Proteus,照着下文也能把工程搭起来。
1. 一个仿真文件同时塞下BLDC、LCD和uC/OS-II,到底图什么
先说动机。很多人学BLDC控制的时候,直接在裸机主循环里写一个大while(1),里面轮流处理霍尔换相、按键读取和LCD刷新。这么写在小项目里没问题,但一旦代码变复杂,问题就来了:LCD刷新是毫秒到百毫秒级别的操作,按键需要消抖轮询,而BLDC换相在霍尔信号边沿到来后必须尽快完成,否则转矩就会抖动,严重时直接失步。把这些速度差异巨大的事情塞进同一个循环,调度顺序稍有变动,电机表现就会变差。
uC/OS-II解决的就是“谁先跑、谁后跑、跑多久”的问题。我把整个系统拆成三个任务:电机控制任务负责速度设定和占空比调整,按键扫描任务负责读取按键和消抖,LCD刷新任务负责把转速、状态信息渲染到12864上。实时性要求最高的霍尔换相动作放在中断里做,不依赖RTOS调度。
这里有一个很多人会误解的点:用了RTOS不代表所有事情都能往任务里扔。BLDC的六步换相本质上是微秒级事件,而uC/OS-II是抢占式调度,任务切换需要保存现场、恢复上下文,耗时不确定。如果换相动作放在任务里,当霍尔边沿到来时任务不一定立即执行,电机就很容易“顿挫”。所以我的方案是:换相逻辑放在霍尔外部中断里,RTOS负责管理“模式切换、速度计算、按键响应、屏幕刷新”这些低实时性但逻辑复杂的部分。
这套设计在仿真里的价值很明显:Proteus可以随时暂停、单步、加探针看波形,不需要真实电机和驱动板。比如我想观察霍尔信号和PWM输出的时序关系,只需要放一个虚拟示波器,在线看PWM波形,配合逻辑探针数电平变化,比接真实示波器还方便。从学习角度看,先用仿真把“RTOS + BLDC + LCD”这套架构吃透,再上硬件,能少走很多弯路。
2. Proteus侧元件选型与接线:从驱动桥到12864串行接口
Proteus里的仿真工程,元件选型和接线决定成败。BLDC电机模型不像普通直流电机那样两根线一接就转,它需要三相驱动桥、霍尔反馈、电源、还有驱动信号。下面是我实际用的元件清单和接线方式,照着搭基本不会错。
2.1 元件清单与电源设置
表格里的元件都可以在Proteus元件库里搜到:
| 元件 | 搜索名称 | 数量 | 用途 |
|---|---|---|---|
| 主控 | STM32F103C8 | 1 | 控制核心,跑uC/OS-II |
| 无刷电机 | BLDC Motor | 1 | 三相无刷直流电机,带霍尔输出 |
| MOSFET | NMOS,比如IRF540 | 6 | 组成三相全桥驱动 |
| 液晶屏 | 12864 LCD(ST7920方案) | 1 | 显示转速、方向、状态 |
| 按键 | BUTTON | 3 | 加速、减速、启停 |
| 电阻 | RES | 若干 | 栅极串阻、上拉电阻 |
| 电位器 | POT | 1 | 调节LCD对比度 |
| 直流电源 | VCC/引脚电源 | 2组 | 12V电机母线、5V逻辑 |
电源设置非常关键。Proteus默认用VCC/GND符号给电路供电,STM32F103C8内部自带了供电网络,不需要额外接电源符号,但这不代表你能忽略电机侧。我习惯把母线电压设置成12V,在原理图上放置一个12V的端子或电源元件,给三相桥的VM端供电。电位器用来调节LCD对比度,一般接在VLCD引脚和地之间,仿真时对比度调太高会导致屏幕显示发黑。
2.2 三相驱动桥和电机的连接
驱动级是6个N沟道MOSFET组成的三相全桥。上桥三个管子的漏极统一接12V母线,源极分别接三个输出点;下桥三个管子的源极统一接地,漏极分别接到对应的输出点。每个输出点就是BLDC电机一相的驱动端,接电机的PHA、PHB、PHC引脚。
STM32的TIM1高级定时器可以产生三对互补PWM输出,正好对应三相桥臂的上下管。我分配的引脚是:PA8输出CH1(A相上桥),PB13输出CH1N(A相下桥),PA9/PA10输出CH2/CH3上桥,PB14/PB15输出CH2N/CH3N下桥。这样六路PWM信号一一对应,逻辑清晰。
BLDC电机模型上的霍尔输出HA、HB、HC,不能直接接到MCU引脚了事,三个霍尔信号需要接上拉电阻到逻辑电源,然后接MCU的PB0、PB1、PB2。Proteus的BLDC模型内部已经做了部分简化,但霍尔输出仍然是真实电平,读取后走外部中断,每个霍尔边沿都会触发中断。
2.3 LCD12864走SPI模式,省出一堆IO
ST7920控制器方案的12864液晶支持并行和串行两种接口。并行接口要接DB0-DB7八根数据线,加上RS、RW、E使能信号,占用的引脚太多。为了让工程显得更简洁,我把PSB引脚接低电平,让屏幕进入串行模式,这样只需要三根控制线:CS片选、SID串行数据、SCLK串行时钟。
Proteus里不同版本的12864模型,引脚名称和数量会有细微差异。如果你用的版本里找不到ST7920串行模型的准确对应关系,有个替代办法:改成并行接口连接,把DB0-DB7接到GPIO,运行时用并行初始化时序,其余逻辑完全不变。我在工程里用串行模式,是因为逻辑上更贴近真实产品里省IO的做法。
2.4 接线最容易错的地方
我一开始把BLDC电机的PHA/PHB/PHC直接接到了STM32引脚上,想着单片机输出换相信号驱动电机,结果电机纹丝不动。后来才意识到,BLDC电机是功率件,MCU引脚根本不能提供相电流,必须经过三相桥。正确的接法是三相输出端接电机的三相输入,中间必须经过MOSFET组成的功率级,MCU的PWM只控制MOSFET栅极,不直接驱动电机。
另外,霍尔信号线和PWM信号线在Proteus里相邻摆放,不会像真实电路那样产生干扰,但要注意命名清晰。我用网络标签区分:HALL_A/HALL_B/HALL_C对应霍尔信号,DRV_AH/DRV_AL等对应每一路PWM,这样后期排错时看网络标签比看线更直观。
3. 六步换相与测速:让RTOS做调度,让霍尔中断做换相
BLDC控制的核心是六步换相。六步换相的意思是,一个电周期内,三相定子绕组按照霍尔状态被顺序导通,每次只有两相导通,第三相悬空,每60度电角度换一次相。六个霍尔状态组合恰好对应六个导通阶段,形成一个周期。如果你对这六个状态不敏感,可以想象成三个开关配合“旋转磁场”:每次换相都让磁场方向提前一点,转子就被一直“推”着往前走。
3.1 换相是微秒级事件,不能交给任务调度
前面提到,RTOS任务调度有延迟,霍尔边沿触发中断后,如果此时CPU正在执行低优先级任务,uC/OS-II需要先保存当前任务现场,再切换任务,这个过程的开销在几百个周期。对于BLDC换相来说,这个时间虽然很短,但放在高性能控制场景下会引入相位延迟,转矩脉动立刻变大。
所以我的设计把换相放到了霍尔外部中断服务函数里。中断一进来就读取三个霍尔引脚的状态,查换相表,直接写三个TIM1比较寄存器,整个动作只有几十条指令,不涉及RTOS调度。RTOS任务只负责“下一拍该给多少占空比”,以及“速度目标变了要不要重新计算”,类似老板管方向和预算,真正跑腿的是中断。
3.2 TIM1输出六路PWM的初始化细节
STM32的TIM1高级定时器可以输出互补PWM并带死区,非常适合驱动三相桥。初始化时要开启TIM1时钟、配置GPIO为复用推挽输出、选择PWM模式1,然后使能CCER里的CCxE和CCxNE。这里有个容易漏的坑:六路PWM要输出,必须在CCER中对应位都置1,只开上桥不开下桥,电机照样不转。
简化后的初始化思路是:
TIM_TimeBaseInitStructure.TIM_Prescaler = 71; // 72MHz / 72 = 1MHz TIM_TimeBaseInitStructure.TIM_Period = 49; // 1MHz / 50 = 20kHz TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_OutputNState = TIM_OutputNState_Enable; TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OCInitStructure.TIM_OCNPolarity = TIM_OCNPolarity_High; TIM_OCInitStructure.TIM_DeadTime = 0x05; // 死区时间,防止上下管直通注意最后必须调用TIM_CtrlPWMOutputs(TIM1, ENABLE),这是TIM1和TIM8作为高级定时器独有的主输出使能位。很多人在仿真里找不到PWM输出,就是因为漏了这一句,或者只开了CCER的通道使能,没有开主输出。
3.3 霍尔换相代码与测速换算
换相表可以预先算好:读三个霍尔引脚,得到一个0-7之间的数值,其中0和7是非法状态,1-6对应六个换相阶段。每次进入中断,就把当前占空比值写入三个比较寄存器,实现一步换相。
const uint8_t StepTable[8] = {0, 2, 4, 1, 3, 5, 0, 0}; void HALL_IRQHandler(void) { uint8_t hall = read_hall_state(); // 读PB0/PB1/PB2 uint8_t step = StepTable[hall] & 3; TIM1->CCR1 = step_setting[step][0]; TIM1->CCR2 = step_setting[step][1]; TIM1->CCR3 = step_setting[step][2]; }当然实际项目里还要处理霍尔边沿抖动、低速时的信号死区等问题,但在Proteus仿真阶段,这个简化逻辑足够跑通。
测速我采用霍尔信号捕获法:因为CLK频率高、电周期相对较长,用定时器输入捕获测HA引脚相邻两个上升沿之间的时间,换算成转速。假设极对数为1,那么电周期T等于机械周期,转速公式是RPM = 60 / T。如果BLDC模型默认极对数不是1,就把60 / (T * pole_pairs)算进去。实际测速任务在uC/OS-II里每200ms跑一次,把捕获到的周期值换算成整数转速,更新到共享变量。
4. uC/OS-II移植与三个任务的优先级博弈
uC/OS-II是经典的教学级RTOS,源码清晰,特别适合第一次做系统移植的人。把它跑在STM32F103C8上,本质上是四个文件的配合:os_cpu.h、os_cpu_a.asm、os_cpu_c.c和os_cfg.h。在Keil工程里把这些文件加入组别,再把PendSV_Handler和SysTick_Handler的中断向量指向uC/OS-II的实现,系统就能跑起来。
4.1 把uC/OS-II挂到STM32F103C8上的三个关键文件
移植常见的问题是启动文件里已经定义了PendSV_Handler和SysTick_Handler,而uC/OS-II移植文件里也定义了同名函数,链接时报重复定义错误。解决办法是把启动文件里的PendSV_Handler和SysTick_Handler改成空定义或直接注释掉,让uC/OS-II的版本接管。SysTick中断里需要调用OSTimeTick(),PendSV负责上下文切换,这两个入口必须指向RTOS,否则任务调度不会生效。
另一个容易踩的坑是中断优先级分组。Cortex-M3的NVIC响应规则是:优先级数值越低,实际优先级越高。uC/OS-II要求PendSV和SysTick都设置成最低优先级(数值最大),并且要配置为可屏蔽中断,这样才能保证在任务切换时不会被普通外设中断插队。我的配置是:PendSV优先级15,SysTick优先级15,霍尔外部中断优先级0,这样霍尔信号能立刻抢到CPU。
4.2 任务表与调度模型
整个系统的任务划分如下表:
| 任务名 | 优先级 | 执行周期 | 主要工作 |
|---|---|---|---|
| MotorControlTask | 3 | 100ms | 读取目标转速、计算占空比、更新PWM配置 |
| KeyScanTask | 5 | 50ms | 扫描按键、消抖、发送控制消息 |
| LcdRefreshTask | 7 | 200ms | 读取共享速度变量、格式化字符串、刷新12864 |
最高优先级给了电机控制任务,因为速度环和占空比调整需要相对及时;按键扫描是慢事件,50ms完全够;LCD刷新在RTOS里属于“最不重要但最耗时”的事,所以优先级最低。这样配置的理由很简单:让最不需要实时性的任务等待,把CPU时间让给重要的事。
任务之间通过全局变量传递数据,但这在RTOS里是个隐患。一个任务写数据,另一个任务读数据,可能读到一半的数据。uC/OS-II提供了信号量、邮箱和消息队列,这里最合适的做法是:电机控制任务用二进制信号量告知按键任务“目标转速已更新”,或者干脆用OSQPost发送结构体指针,简单又不容易出错。
4.3 优先级设错会让仿真看起来很怪
如果你把LCD刷新任务优先级设得比电机控制任务高,那么屏幕上内容倒是很流畅,但电机转速调节会出现明显卡顿。更隐蔽的问题是:任务优先级相同的情况下,uC/OS-II采用时间片轮转,但Proteus仿真速度本来就偏慢,时间片切换开销会放大,整个系统像“慢动作”。我的经验是,在同一优先级的任务只有一个,避免时间片轮转带来的不确定性。
另外,OS_TICKS_PER_SEC这个配置项决定了时钟节拍的频率。真实硬件上很多人设1000,系统时基更细腻,但Proteus仿真中节拍太密会显著拖慢速度。我实际用100,任务调度的体验足够,仿真流畅度也好很多。
5. 仿真路上最容易翻车的四个点与完整排查记录
仿真跑了几天,遇到的问题比预想的多。很多坑不是说直接看数据手册就能想到的,必须非常耐心地逐层排查。下面四个问题最具代表性,我把完整的排查链路列出来,方便你复现。
5.1 电机不转但PWM照常输出
现象是:示波器看PA8引脚,PWM波形正常,但BLDC电机纹丝不动。我第一反应是霍尔信号没接对,于是用逻辑探针看PB0-PB2,发现三个霍尔状态正常,旋动电机模型时电平会变化。接着怀疑MOSFET桥,但Proteus模型里MOSFET开关特性是理想的,不太可能烧坏。
排查到后来,我把注意力放回TIM1配置上。问题出在低级得不能再低级的地方:我配置了CH1-CH3和CH1N-CH3N的PWM输出,但六步换相在每个阶段只导通两相,第三相需要完全关闭。我的换相表里把不导通相的CCR值设置成0,但由于互补输出是打开的,CCR为0并不代表输出恒低,有些模式下仍然会输出有效电平,导致电机三相同时被“锁死”,自然不转。
解决办法是在换相表里显式关闭不导通相的PWM输出,把对应通道的CCER位清零。仿真里看似PWM正常,实际上三个桥臂的状态组合不对,这正是BLDC控制比普通直流电机复杂的地方:每一相的PWM输出不是一个独立的正弦波,而是要跟霍尔状态严格配合。
5.2 12864白屏:不是初始化代码的问题
第一次点亮12864时,屏幕一直白屏,数据看起来初始化函数也调了,延时也加了,就是不显示。我怀疑是串行时序的SCLK频率太快,Proteus里会有些许延迟,但实际情况不是这个。
后来我在关键初始化步骤之间插入变量标志,用Proteus的调试窗口观察程序执行到哪一步停止。发现程序停在LCD初始化里等待一个状态位,而那个状态位永远等不到。原因是我的初始化代码放在了一个任务里,这个任务被更高优先级的电机控制任务抢占,SPI发送时序被拉长,导致状态位判断失败。
解决方式是:LCD的初始化逻辑放在uC/OS-II启动多任务之前完成,不让它被任务调度打断;显示内容时才放到LcdRefreshTask里。另外,12864的PSB引脚一定要接低电平,很多原理图里把它悬空,悬空会被内部上拉识别成并行模式,串行初始化时序自然失效。
5.3 uC/OS-II启动即HardFault
移植uC/OS-II后第一次运行,程序启动就进入HardFault。我初步判断是任务栈溢出,或者栈指针配置问题。用Keil的调用栈窗口看,发现PC指针乱跳,明显是在上下文切换时出了问题。
逐层排查下去,发现是任务栈大小不够:每个任务栈我一开始只分配128字节,但任务里用了printf类格式化函数,栈瞬间就被撑爆。uC/OS-II任务栈是独立的,不在系统栈里,你需要在创建任务时给足空间。我最后把三个任务栈都配到512字节以上,HardFault消失。
接着又出现一个奇怪现象:任务A正常,任务B跑了几个周期后卡死。最后定位到是共享变量没有用volatile声明。电机控制任务更新转速值,LCD任务读取,编译器优化后LCD任务读的是寄存器里的旧副本。这个在Proteus仿真里也会发生,加了volatile之后问题消失。
5.4 Proteus鼠标都挪不动:仿真性能调优
仿真跑通之后,画面卡到鼠标都拖不动。查了一圈,问题主要出在三个地方:一是12864刷新太频繁,我最初设的100ms刷新一次,每帧都要传大量数据;二是虚拟示波器上挂了太多探针,每个探针都在做实时采样;三是uC/OS-II的时钟节拍设到了1000Hz,每秒上万次任务调度,Proteus的仿真内核忙不过来。
优化方案很有效:LCD刷新周期改到300ms,示波器探针只保留PWM和霍尔两个关键通道,OS_TICKS_PER_SEC降到100。这么改完,Proteus仿真速度接近真实硬件的感觉,整个系统运行也稳定了。仿真速度是很多人容易忽视的问题,原因不是电脑性能差,而是你给仿真内核加的“工作量”太大了。
6. 仿真跑通之后,还能往这个Demo里加什么
仿真稳定后,我做了一些扩展,感觉这个Demo的可玩空间还很大。
6.1 从开环定占空比到PID闭环调速
最初电机转速是开环的:占空比固定,电机在负载变化时转速会掉。进一步做闭环只需把测得的转速作为反馈,目标转速和实际转速的误差进PID控制器,输出作为占空比调节量。在Proteus里验证PID参数非常方便:给BLDC模型加负载变化,观察转速有没有回到目标值,省去真实电机平台上的反复烧录调试。
6.2 在Proteus里观察相电流和反电动势
Proteus的BLDC模型支持观察反电动势波形。把电流探针接到电机相线上,就能看到每个换相阶段电流的变化,验证六步换相相序是否正确。这个在真实硬件上要用电流钳,很麻烦,仿真里用探针就解决了。通过观察反电动势过零点的位置,你还能为无感BLDC控制做铺垫,把霍尔传感器换成反电动势检测,整套逻辑直接变成无感方案。
6.3 仿真和真板的几个差异
最后提醒一点:仿真跑通不代表直接上板没问题。Proteus里的MOSFET是理想器件,没有导通电阻、开关延时和热损耗,仿真环境下你甚至可以忽略死区时间,但真实驱动板必须设置足够大的死区,否则上下管直通瞬间烧管子。12864在真实硬件上还涉及电平匹配、背光功耗、走线干扰,这些在Proteus里都体现不出来。
我在这个工程跑通后最大的体会是:uC/OS-II真正的价值不是让你觉得“我用了操作系统”,而是逼你把系统拆成边界清晰的任务,再把共享数据和实时性要求一条条梳理清楚。BLDC部分也一样,仿真里的每一个“不转”“卡死”都在训练你像单片机一样思考:先看信号,再看时序,最后才怀疑代码。希望你也能用这套流程把BLDC、LCD和uC/OS-II一次玩明白。
本文还有配套的精品资源,点击获取