【嵌入式电机控制#4】有刷电机软硬件结合部分(二)(高难度,重理解)

发布于:2025-07-01 ⋅ 阅读:(22) ⋅ 点赞:(0)

一、代码实现流程

        1. 定时器初始化:配置IO、配置定时器、初始化PWM互补输出、死区控制等

        2. 电机初始化:默认拉低SD引脚

        3. 电机启动:打开计数功能、拉高SD引脚

        4. 电机停止:关闭计数功能,拉低SD引脚

        5. 电机正反转:先关闭两个PWM的输出,正转开始主通道,反转开启互补通道

二、 CubeMX初始化

        这里不跟随某原子的课程方式,花费大量时间去学寄存器确实是繁琐的事情,我们这里用CubeMX对程序进行初始化配置;

        这里用的主控是F103ZET6,首先我们定义PWM的引脚。

                                                                

        (1)引脚复用

                因为我们板子的连接采用了引脚复用的方式,这里可以先点击对应引脚,选择TIM1的主通道和互补输出。然后再进入TIM1设置界面,选择内部时钟,然后设置PWM通道1的互补输出。这样我们就把两个引脚PA8和PB13复用到了TIM1的通道1PWM。

                其次,因为我们的CubeMX跟寄存器配置有区别,这里不能用默认的定时器开启,否则我们在开启PWM输出的时候波形可能是中间一截出来的,会影响系统性能。

                这里我们有一个技巧,可以把从模式改成Reset Mode,表示定时器计数默认情况下是静能状态。然后配置触发源,随便选一个硬件触发ITRx,但是我们不使用,只是借用。目的是绕过CubeMX的自启动配置。

                如果需要开启/关闭定时器,那么我们直接用base_start/stop来开启计数就可以。

                注意:base_start/stop不是开启关闭定时器,而是开启关闭它的计数功能。

                

                在实际项目中,如果对芯片复用不熟悉,可以查询数据手册和硬件原理图,在Cube的引脚选项中也会给你标出来。

        (2)GPIO参数设置(很容易忘记的)

                我们接着不往下面翻,直接跳转到GPIO参数界面,对两个IO口进行最高输出速率调整,一般调到High就够用了。其他不动。

                

        (3)定时器参数设置

                这里需要注意一点,对于直流电机,PWM本身的频率在100Hz-10kHz比较合适,可以有效的减少开关损耗,但是过低会引入噪声。

                我们这里选取5kHz,

                

          (4)比较输出寄存器配置

                这里的Pulse是PWM高低电平切换的阈值,在mode1(?CNT<Pulse)且极性polarity为low的情况下,输出结果与CNT和Pulse的比较结果相反,当小于Pulse时,比较结果为1,极性反相,输出为0。

                当mode2 (? CNT>Pulse)且极性为high的情况下,输出结果与CNT和Pulse比较结果相同,当小于Pulse时,比较结果为0,输出结果为1.

                所以说这里mode和polarity有两种配置方式,一种是1和low,一种是2和high。

                为什么这里的配置与我们在TB6612下做的不一样呢?因为这个驱动芯片的原理是利用IH和IO的高低差来实现电机转动,当我们默认把一个Input置0的时候,另一个必须为1才能起到作用。与TB芯片的工作逻辑正好是相反的。

                接下来设置空闲状态,标准做法是拉低。但是这个地方有个疑问,拉低了的话是否会与OSSR存在冲突?(疑问)

                但是根据实际情况来看,真正的互补输出时的电平特征还是根据OSSR寄存器配置来的(下面一段有解释)

                此外快速模式需要打开;

                

           (5)刹车与死区配置

                其实死区是用于避免无刷电机H桥烧掉的,而且经过查资料,它这个半桥芯片好像就是自带死区的,但我们有刷下设定死区影响也不是很大。

                按照课程板子的特性,我们的死区时间配置为0x0F(15D)

                这里,刹车的意思指的是我们的PWM突然停掉了,控制器应该如何应对。

                OSSR 位决定了当定时器处于运行模式(非复位状态)也处于计数状态,但没有使能输出的情况下,互补 PWM 输出通道(如 TIM1 的 CH1 和 CH1N、CH2 和 CH2N 等)的最终状态(高电平、低电平或高阻态)。        

                这里一定要搞清楚,定时器还在计数状态,只是某个(些)互补通道没有输出。

                为了使我们的定时器停止计数后不会误触发功率器件,这里需要启动OSSR,让它在所有PWM输出关闭时,所有PWM引脚都输出高阻态。

                这里提一下OSSR、OSSI 和 IDLE state的区别:

                1. IDLE State:主要用于定义当定时器的主输出使能位 MOE 为 0 时,定时器输出引脚的状态。可以通过相关配置设置为高电平或低电平。

                在hal库中,我们可以对已经开始计时的TIM使用pwm_stop函数让它进入空闲状态。

                也就是说,我们采用idle state是面向单个通道下的空闲管理。 而cube中引入互补通道各自的idle state是为了更精细的控制。

                2. OSSR:定时器主输出使能开启的情况下,针对具有互补输出的通道起作用。当其中一个互补输出通道未被使能时,OSSR 用于决定该未使能通道的输出状态。

                若互补通道都使能(CCxE=CCxNE=1),此时很容易理解,两个端口按照配置正常输出PWM波。

                若互补通道都禁用(CCxE=CCxNE=0),此时存疑,可以认为两个端口都处于高阻状态。
                若互补通道有一个使能,一个禁用。(也就是正反转时的状态)则使能的端口正常输出PWM波,禁用的端口的状态与OSSR相关。
                若OSSR=0,则禁用端口输出高阻(存疑),与CCxP或者CCxNP无关。
                若OSSR=1,则禁用端口输出无效电平。(也就是1,与驱动控制需求对应)

                假设OC1N为被禁用,即(CC1NE=0),则此时OC1N的输出为无效电平,即CC1NP为0时,高电平有效,输出电平为0,CC1NP为1是,低电平有效,输出电平为1。

                所以根据官方手册的说法,启用了OSSR能直接把独立idle覆盖掉的。

                3. OSSI:OSSI与OSSR的作用很类似,其只有在MOE=0(主输出静能)时起作用,即PWM处于高阻状态。我们假定互补的两个通道都使能,下面分条陈述:    

                当OSSI=1时:

                若OISx与OSIxN不同,有且只有一个电平为高电平,则通道输出的电平对应空闲状态。即OCxN=OSIxN,OCx=OISx。
                若OISx=OSIxN=1时,只有CCxP=CCxNP=0时,OCx=OCxN=0;其余时候OCx=OCxN=1。
                若OISx=OSIxN=0时,与前面相似,只有CCxP=CCxNP=1时,OCx=OCxN=1,其余时候OCx=OCxN=0。

                若OSSI=0,此时输出高阻。

                它显然不是我们控制逻辑中需要的,所以选择disable。

                这里还有个lock配置,其实它的意思就是机器上电后能是否只能写一次配置,很显然我们是必须关闭的。

                所以最终刹车死区配置如下:

                

                可能有人会担心,如果我们的CEN=0(base_stop)之后,PWM的输出会如何,其实在咋们的代码中,这只是一瞬间的事情。

                具体讲,它跟MOE和idle都有关系,但是几乎不会影响我们的停止。因为计数关闭后下一个时刻,我们可以迅速拉低SD引脚来禁用驱动芯片。

            (6)驱动芯片使能引脚配置

                注意,接下来的内容完全面向Cube的HAL库,与寄存器初始化配置后的流程有着非常明显的区别,请仔细甄别。

                我们还需要一个控制SD的引脚,开始时默认把它拉低(禁用驱动)。

                

三、 MDK程序设计

        (1)电机启动

        建议这里不采用正点原子的写法,因为在Cube配置中如果我们使用了高级定时器或者PWM功能,那么主函数会生成一个TIMx_init函数,自动帮我们完成初始化配置并启动定时器。

        所以,这里不需要再用basestart;

        然后我们此时才能拉高SD,开启驱动芯片。

        注意,此时电机还未开始转动,只是完成了启动的必要条件。

void DC_motor_start(){
        HAL_TIM_BASE_Start(&htim1);
        /*这个时候才开始拉高SD使能驱动芯片*/
		HAL_GPIO_TogglePin(SD_GPIO_Port,SD_Pin);
}

        (2)电机停止

        停止的话这里关掉PWM,尽量不要关掉定时器,因为定时器中可能还有其他内容在运作着,然后拉低SD。

void DC_motor_stop(){
	    HAL_TIM_Base_Stop(&htim1);
        //关闭PWM计数功能
		HAL_GPIO_TogglePin(SD_GPIO_Port,SD_Pin);
        //拉低SD,使驱动芯片关闭
}

        (3)电机正反转

        我们需要可以先停掉所有PWM再经过1,0的状态机判断实现。这里不需要担心死区的问题,因为初始化配置时就帮你安排好了,只要GPIO出现边沿,HAL库就在帮你做死区时间。

        我们可以先停掉所有的PWM,然后写一个状态机逻辑,进行前后独立通道开启。也不用担心另外一个通道是什么情况,因为我们配置时,用了OSSR,互补PWM空闲状态下是高。

void DC_motor_dir(uint8_t dir){
		HAL_TIM_PWM_Stop(&htim1,TIM_CHANNEL_1);
		HAL_TIMEx_PWMN_Stop(&htim1,TIM_CHANNEL_1);
		if (dir == FORW){
				HAL_TIMEx_PWMN_Start(&htim1,TIM_CHANNEL_1);
		}
		else if (dir == BACK){
				HAL_TIM_PWM_Start(&htim1,TIM_CHANNEL_1);
		}
}

 完整测试代码

main.c

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "DC-motor.h"
/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */

/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
#define FORW 1
#define BACK 0
#define TICKMINUS 40
/* USER CODE END PD */


/* USER CODE BEGIN 4 */
uint32_t fallingedge=0;
uint32_t risingedge=0;
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin){
		if(GPIO_Pin==KEY1_Pin){
				if(HAL_GPIO_ReadPin(KEY1_GPIO_Port,KEY1_Pin)==RESET){
						risingedge=HAL_GetTick();
				}
				else if(HAL_GPIO_ReadPin(KEY1_GPIO_Port,KEY1_Pin)==SET&&!fallingedge){
						fallingedge=HAL_GetTick();
						if(risingedge-fallingedge>TICKMINUS){
								if(dir == FORW){
										DC_motor_start();
										DC_motor_dir(BACK);
										dir = BACK;
								}
								else if(dir == BACK){
										DC_motor_start();
										DC_motor_dir(FORW);
										dir = FORW;
								}
						}
						fallingedge=risingedge=0;
					}
		}
		
		if(GPIO_Pin==KEY0_Pin){
				if(HAL_GPIO_ReadPin(KEY1_GPIO_Port,KEY0_Pin)==RESET){
						risingedge=HAL_GetTick();
				}
				else if(HAL_GPIO_ReadPin(KEY1_GPIO_Port,KEY0_Pin)==SET&&!fallingedge){
						fallingedge=HAL_GetTick();
						if(risingedge-fallingedge>TICKMINUS){
								DC_motor_stop();
						}
						fallingedge=risingedge=0;
					}
		}
		
}
/* USER CODE END 4 */

DC-motor.h

#include "stm32f1xx_hal.h"

#ifndef FORW
#define FORW 1
#endif
#ifndef BACK
#define BACK 0
#endif

extern uint8_t dir;
void DC_motor_start(void);
void DC_motor_stop(void);
void DC_motor_dir(uint8_t dir);

DC-motor.c

#include "DC-motor.h"
#include "tim.h"

void DC_motor_start(){
		HAL_TIM_PWM_Start(&htim1,TIM_CHANNEL_1);
		HAL_TIMEx_PWMN_Start(&htim1, TIM_CHANNEL_1);
		HAL_GPIO_WritePin(SD_GPIO_Port,SD_Pin,1);
		// motor started
}

void DC_motor_stop(){
		HAL_TIM_PWM_Stop(&htim1,TIM_CHANNEL_1);
	  HAL_TIMEx_PWMN_Stop(&htim1,TIM_CHANNEL_1);
		HAL_GPIO_WritePin(SD_GPIO_Port,SD_Pin,0);
		//motor stopped
}

void DC_motor_dir(uint8_t dir){
		HAL_TIM_PWM_Stop(&htim1,TIM_CHANNEL_1);
		HAL_TIMEx_PWMN_Stop(&htim1,TIM_CHANNEL_1);
		if (dir == FORW){
				HAL_TIMEx_PWMN_Start(&htim1,TIM_CHANNEL_1);
		}
		else if (dir == BACK){
				HAL_TIM_PWM_Start(&htim1,TIM_CHANNEL_1);
		}
}