草庐IT

一文搞懂如何使用STM32驱动直流电机(普通PWM输出和L298N、高级定时器输出带死区双通道互补PWM和IR2110S及自举电路、H桥电路和电机正反转)

圆竹 2023-07-12 原文

本文将用最通俗易懂的语言讲解怎么使用STM32驱动直流电机,以及在使用过程中容易遇到的问题和解决办法。本文将介绍两种驱动方式:普通PWM驱动L298N驱动直流电机;互补PWM驱动IR2110S驱动直流电机。笔者将文章分为两部分:不懂原理直接使用部分和一定要懂原理再用(仅IR2110S)部分。看完后,你会说:圆哥NB,原来驱动电机如此简单。


文章目录


前言

笔者从开始接触嵌入式单片机开始,就和驱动电机相伴而走。从最开始的直接买L298N驱动模块直接驱动直流电机,到现在自己设计PCB电路板驱动直流电机,可以说是和电机驱动共同成长了。现在笔者将这一过程的收获记录下来,希望对大家有所帮助。


一、不懂原理直接使用

0.了解L298N和IR2110S怎么驱动直流电机的

不用详细了解L298N芯片和IR2001S的芯片引脚,但是要知道这两个芯片是干嘛的。这两个芯片的相同功能就是把单片机输出的3.3VPWM波变成你给这两个芯片供电的电压PWM波。比如你给芯片供电12V,那么输出的就是12V的PWM波,占空比和单片机输出的PWM波占空比一致。
改变电机的电压可以改变电机的转速,改变电机的正负极可以改变电机的转向。改变电机的驱动电压通过改变PWM波的占空比实现,比如给芯片供电12V,PWM的占空比是60%,那么就相当于给电机供电12V*60% = 7.2V。供电电压乘以占空比就是电机的供电电压。需要注意的是,这里的占空比指的是高电平时间和周期的比值。
改变电机的转向(改变电机的正负极),这个操作很简单,会在下面介绍。

1.普通PWM和L298N

STM32输出普通PWM笔者这里不再赘述,这个驱动过程十分简单,网上随便一搜索就有一大堆的例子。
L298N可以买到别人设计好的模块,在淘宝等店铺直接搜索L298N电机驱动模块就可以看到各种各样的产品。这些产品的驱动方式大同小异,笔者下面将选取一种介绍。
如上图所示,怎么使用这个这个模块呢?听笔者一一介绍。首先,这个模块需要供电,从图中可以看到供电部分由12V供电、供电GND、5V供电、和板载5V使能的那个跳线帽组成。如果板载5V使能那个跳线帽安上,那么就不需要给驱动模块单独5V供电,只需要12V供电就行了。一般来说会使用这种方式。另一种方式就是12V、5V都给供电,然后把板载5V使能的那个跳线帽去掉。
然后看通道A使能、逻辑输入、通道B使能的那一部分。左边的3排控制左边的输出A通道,右边的3排控制右边的输出B通道。笔者以左边三排为例进行讲解,右边三排和左边是对称的。先看通道A使能的那个跳线帽,使用时将那个跳线帽拔掉,然后把单片机的PWM输出引脚接到靠近板边缘的排针上面。需要注意的是,最好通过光耦或者其他方式将单片机引脚和使能排针做一下隔离。如果不做隔离,尽量不要使用外力让电机转动,不然电机会发电,有可能烧坏单片机等电路。逻辑输入靠近通道A使能的那两个排针是用来控制通道A的电压正负的,给这两个排针1 0,通道A会输出一个电压,给这两个排针0 1,通道A会输出和1 0 相反的电压,从而让电机反转。这两个逻辑控制可以直接接到单片机的IO口,用IO口控制。当然,如果不需要正反转,可以直接给固定的电压就好。
L298N到这里介绍结束,总结一下。给供12V的电压,把单片机的PWM输出引脚接上,再把逻辑输入接上,就可以控制电机的转速和正反转了。


2.互补PWM和IR2110S

笔者使用的单片机型号是STM32F103C8T6,下面将介绍基于这款单片机高级定时器一的带死区的互补PWM怎么配置的。事实上,由于CMSIS协议的存在,这个配置只需做少量修改或者无需修改就可以使用在Cotex-M内核的其他单片机上。

代码如下:

void TIM1_PWM_DeadtimeInit(u16 arr,u16 psc,u16 deadtime)
{
    GPIO_InitTypeDef GPIO_InitSturcture;
    TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
    TIM_OCInitTypeDef TIM_OCInitStructure;
    TIM_BDTRInitTypeDef TIM_BDTRInitStructure;
    NVIC_InitTypeDef NVIC_InitStructure;
 //时钟配置
    RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE );
    RCC_APB2PeriphClockCmd( RCC_APB2Periph_TIM1, ENABLE );
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);

    //TIM1 PWM CH1->PA8  CH2->PA9  CH1N->PB13  CH2N->PB14,PWM管脚映射
    //与PWM管脚初始化
    GPIO_InitSturcture.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9;
    GPIO_InitSturcture.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitSturcture.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init( GPIOA, &GPIO_InitSturcture );

    GPIO_InitSturcture.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_14;
    GPIO_InitSturcture.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitSturcture.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init( GPIOB, &GPIO_InitSturcture );
	
	  GPIO_SetBits(GPIOA, GPIO_Pin_8 | GPIO_Pin_9);
    GPIO_SetBits(GPIOB, GPIO_Pin_13 | GPIO_Pin_14);
	//关于定时器中断的配置,有的资料上需要配置,有的资料上不需要
	//配置,笔者配置和不配置的都试了,都可以使用,这里还是配置上	
	NVIC_InitStructure.NVIC_IRQChannel =  TIM1_UP_IRQn;    
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;       
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;  
    NVIC_Init(&NVIC_InitStructure);
    //关于定时器重装载值和分频系数的配置,决定了PWM的频率
    //timer base set
    TIM_TimeBaseInitStructure.TIM_Period = arr;
    TIM_TimeBaseInitStructure.TIM_Prescaler = psc;
    TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseInitStructure.TIM_ClockDivision = 0x00;
    TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0x00;
    TIM_TimeBaseInit( TIM1, &TIM_TimeBaseInitStructure );
	TIM_ARRPreloadConfig(TIM1, ENABLE);
    //OC1 Mode set 高级定时器互补输出的设置
    TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;		
    TIM_OCInitStructure.TIM_Pulse = 50;							
    TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;		
    TIM_OCInitStructure.TIM_OCNPolarity = TIM_OCPolarity_High;
    TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; 	
    TIM_OCInitStructure.TIM_OutputNState = TIM_OutputNState_Enable;
    TIM_OCInitStructure.TIM_OCIdleState = TIM_OCIdleState_Reset;
    TIM_OCInitStructure.TIM_OCNIdleState = TIM_OCNIdleState_Reset;  
    TIM_OC1Init( TIM1, &TIM_OCInitStructure );
	TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable);
    //OC2 Mode set 
    TIM_OCInitStructure.TIM_Pulse = 0;						
    TIM_OC2Init( TIM1, &TIM_OCInitStructure );
	TIM_OC2PreloadConfig(TIM1, TIM_OCPreload_Enable);
		
	//DeadTime Set 死区时间的设置
    TIM_BDTRInitStructure.TIM_OSSRState = TIM_OSSRState_Enable; 
    TIM_BDTRInitStructure.TIM_OSSIState = TIM_OSSIState_Enable; 
    TIM_BDTRInitStructure.TIM_LOCKLevel = TIM_LOCKLevel_2; 			
    TIM_BDTRInitStructure.TIM_DeadTime = deadtime;					
    TIM_BDTRInitStructure.TIM_Break = TIM_Break_Disable;		
    TIM_BDTRInitStructure.TIM_BreakPolarity = TIM_BreakPolarity_High; 
    TIM_BDTRInitStructure.TIM_AutomaticOutput = TIM_AutomaticOutput_Enable; 
    TIM_BDTRConfig( TIM1, &TIM_BDTRInitStructure );
 
    TIM_CtrlPWMOutputs( TIM1, ENABLE );
		

    TIM_Cmd( TIM1, ENABLE );
}

代码如上图所示,和常规固件库编程思路一样:对时钟进行配置、对管脚进行初始化、对定时器进行初始化、对PWM进行初始化、对死区进行配置。
这里有几点需要注意。首先是高级互补输出的设置的第一行,那里有几种不同的模式,笔者选择的是PWM模式一。如果感兴趣,你可以试一试其他的模式。
第二点是死区时间的设置。死区时间的计算比较复杂,感兴趣的请移步以下链接,是CSDN的另一个博主写的。
链接: STM32F1死区时间配置详解
第三点是这个函数怎么用。需要首先在主函数里进行初始化。
TIM1_PWM_DeadtimeInit(36000-1,0,0XAC);
这是笔者使用的。函数第一个形参是PWM重装载值,第二个形参是PWM分频系数,第三个是死区时间的设置。0XAC表示死区时间是3us。笔者使用的配置的意思是:不分频、计时器到(36000-1)会重新开始计数、由分频系数和重装载值可算出PWM频率是2kHz。
第四点是如何更改PWM的占空比,和普通PWM更改的方式一样。使用
void TIM_SetCompare1(TIM_TypeDef* TIMx, uint16_t Compare1);函数更改比较值就可以了。

由于该互补PWM是用来驱动IR2110S的,笔者已经在IR2110S上进行了实验,死区时间和频率都没有任何问题。

接下来是IR2110S如何使用的问题了。IR2110S目前集成好的模块产品并不是很多,笔者所知野火有一款直流无刷电机驱动板,里面的H桥电路是用IR2110S驱动的。一般来说,使用IR2110S都需要自己设计PCB。当然,不需要知道原理的情况下IR2110S的使用十分简单。

如上图所示,这是使用IR2100S的一种情况。从左侧看,直接将单片机的互补PWM引脚接到V+和V-,然后给芯片供电,按照电路连接就可以驱动芯片在V_OUT端输出高电压(POWER)的PWM波形。
需要注意三点。第一点是SD端口是控制芯片使能的,悬空或者接0电位芯片正常工作,接3.3V或5V电平芯片停止工作。所以在使用中一般直接悬空就可以。第二点是图上的12V电压,可以根据需要选择,只要比场效应管的导通电压Vgs高一些就可以。比如我选择的场效应管的导通电压是10V,那我就选择了12V的电压。
第三点是在使用IR2110S驱动电机的时候,如果想要输出POWER峰值的PWM波,必须要给V+和V-这两个引脚输出互补带死区的PWM波,频率还要合适。这是由于自举电路的存在,如果频率不对或者不是互补的PWM波,都会导致自举电路变成不举电路。举不起来也就无法完成驱动。

那么IR2110S怎么控制电机的正反转呢。其实只需要两个这样的电路,分别把输出接口接到电机的两端就可以了。驱动其中一个电路,让另一个电路和地连接的场效应管一直导通,是一个方向的电压。反过来驱动另外一个电路是反方向的电压。这样就实现了H桥电路。


二、我要懂原理

1.IR2110S的优势

IR2110S具有集成度高,响应速度快等诸多优势。最重要的是,驱动N多个芯片只需要一路电源,大大的减少了设计的难度。这一切都要归功于自举电路。下面将介绍自举电路的原理。

2.IR2110S自举电路


如上图,V-为高点平,V+为低电平,此时Q4导通,Q3截止。
Q4导通后,请看我右边画的线,此时电容会通过该回路充电。经过一小段时间后,电容会充电到12V。


如上图,在电容充满12V电后,紧接着V+导通,V-截止。
在变换的瞬间,Q4截止,此时Q3导通。当POWER电压到达V_OUT的一刻,由于POWER一般大于12V,所以Q3马上截止。
请注意,精彩的部分来了。由于Q4截止后,电容两端电压不能突变,所以电容C11上方还保留着12V的电压。由于POWER到达了V_OUT一瞬间,所以在这一瞬间,C11上的电压是POWER+12V。此时这个POWER+12V在IR2110S内部电路(就是两个场效应管)作用下会给到HO端,此时Q3在这一瞬间又被导通,直到C11放电完成。
这就是一次循环过程,接下来的充电放电就是重复上述的两个过程。所以这就是为什么要互补的PWM波,就是为了给自举电路的电容充电和放电的!!!


总结

请多多关注,接下来笔者可能更新一波如何使用PID算法做简单的自动驾驶。
笔者已经尽可能的理清逻辑,但是难免会有些地方不容易理解,所以欢迎大家多多留言提问和指教呀。技术问题,有问必回!!!

有关一文搞懂如何使用STM32驱动直流电机(普通PWM输出和L298N、高级定时器输出带死区双通道互补PWM和IR2110S及自举电路、H桥电路和电机正反转)的更多相关文章

  1. ruby - 如何使用 Nokogiri 的 xpath 和 at_xpath 方法 - 2

    我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div

  2. ruby - 如何从 ruby​​ 中的字符串运行任意对象方法? - 2

    总的来说,我对ruby​​还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用

  3. ruby - 使用 RubyZip 生成 ZIP 文件时设置压缩级别 - 2

    我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看ruby​​zip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d

  4. ruby - 为什么我可以在 Ruby 中使用 Object#send 访问私有(private)/ protected 方法? - 2

    类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc

  5. ruby-on-rails - 使用 Ruby on Rails 进行自动化测试 - 最佳实践 - 2

    很好奇,就使用ruby​​onrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提

  6. ruby - 在 Ruby 中使用匿名模块 - 2

    假设我做了一个模块如下:m=Module.newdoclassCendend三个问题:除了对m的引用之外,还有什么方法可以访问C和m中的其他内容?我可以在创建匿名模块后为其命名吗(就像我输入“module...”一样)?如何在使用完匿名模块后将其删除,使其定义的常量不再存在? 最佳答案 三个答案:是的,使用ObjectSpace.此代码使c引用你的类(class)C不引用m:c=nilObjectSpace.each_object{|obj|c=objif(Class===objandobj.name=~/::C$/)}当然这取决于

  7. ruby - 使用 ruby​​ 和 savon 的 SOAP 服务 - 2

    我正在尝试使用ruby​​和Savon来使用网络服务。测试服务为http://www.webservicex.net/WS/WSDetails.aspx?WSID=9&CATID=2require'rubygems'require'savon'client=Savon::Client.new"http://www.webservicex.net/stockquote.asmx?WSDL"client.get_quotedo|soap|soap.body={:symbol=>"AAPL"}end返回SOAP异常。检查soap信封,在我看来soap请求没有正确的命名空间。任何人都可以建议我

  8. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

    关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

  9. ruby-on-rails - 如何验证 update_all 是否实际在 Rails 中更新 - 2

    给定这段代码defcreate@upgrades=User.update_all(["role=?","upgraded"],:id=>params[:upgrade])redirect_toadmin_upgrades_path,:notice=>"Successfullyupgradeduser."end我如何在该操作中实际验证它们是否已保存或未重定向到适当的页面和消息? 最佳答案 在Rails3中,update_all不返回任何有意义的信息,除了已更新的记录数(这可能取决于您的DBMS是否返回该信息)。http://ar.ru

  10. ruby-on-rails - 'compass watch' 是如何工作的/它是如何与 rails 一起使用的 - 2

    我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t

随机推荐