基于STM32F103C8T6使用Arduino IDE编程闭环控制4个带编码器的有刷直流电机

这篇具有很好参考价值的文章主要介绍了基于STM32F103C8T6使用Arduino IDE编程闭环控制4个带编码器的有刷直流电机。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

题记:标题有点长了,纯粹为了方便被检索到~~~本贴主要用于支持南方科技大学SDIM学院工业设计专业大三综合项目移动底盘学习,也是我自己按照费曼学习方法的一次尝试,用从底层搭建一个机器人底盘来复习自动控制原理。

        由于工业设计专业没有开设嵌入式课程,多数同学不具备使用Keil或STM32CubeIDE的基础。鉴于Arduino开发的友好性(主要是参考资料多),特使用支持Arduino环境的STM32F103C8T6作为底盘控制核心。已经会使用stm32单片机的同学推荐直接使用官方推荐的编程方式,Arduino的性能和资源丰富性确实不如CubeIDE。

      言归正转,以下是一些项目设计解读:

1、底盘长什么样子?

arduino开发stm32f103c8t6,stm32,嵌入式硬件,单片机,ide

arduino开发stm32f103c8t6,stm32,嵌入式硬件,单片机,ide

有人问模型能给吗?当然可以,上图的模型必须给到位:

链接:https://pan.baidu.com/s/1zzqOA6rm-wdzWZ0YmT_UCA?pwd=obkx 
提取码:obkx 

————其他所有的电子资料会及时更新在这个位置。

2、为何要采用四轮独立电机驱动?三轮、阿克曼、麦克纳姆底盘行不?

       四轮独立驱动的好处是底盘驱动能力相比三轮、阿克曼都要强,承载能力比麦克纳姆强,还可以实现原地转向,车辆通过性也优于其他三种底盘。缺点是轮胎损耗高,效率不如三轮和阿克曼,同时控制4个电机对硬件资源要求高。今年的大三项目中,机器人需要在毛坯建筑中移动,相比而言四轮独立驱动的底盘比较合适。

3、电机如何选型?

       商用机器人四轮移动底盘一般选用无刷轮毂电机,但是价格通常要超过1500元单只,性能对该项目来说也有点过剩。淘宝的科研教学用途底盘通常使用有刷电机,成本优势明显,加上也能匹配SDIM的控制课程实验要求(直流有刷电机模型比无刷电机要简单),项目就直接确实使用直流有刷电机。

        淘宝常见的机器人底盘喜欢使用TT电机或36直流有刷减速电机,通常电机尾端自带光电、霍尔或GMR编码器,方便速度闭环控制。然而这2类电机的承载能力实在无法满足项目要求(负载10公斤),必须寻找承载能力足够的电机才行。

        感谢万能的淘宝,我找到了2款大功率的带减速器的直流电机。此类电机主要使用GMR编码器,分辨率达500线/转,还配套了联轴器、独立悬挂、轮胎等组件,单电机承载能力在5kg以上,完全满足项目要求。

        采购链接如下:底盘轮组模块平行四边形独立悬挂避震器光电编码器行星减速电机-淘宝网 (taobao.com)https://item.taobao.com/item.htm?spm=a1z0k.7628869.0.0.39eb37delSzkhs&id=618634863947&_u=t2dmg8j26111

      两款电机均满足项目要求,本贴是基于MD60和8寸充气轮胎,装配起来发现站个人上去都没有问题,考虑到成本,建议选择MD36的电机。另外MD36的编码器还有霍尔类型的,这种分辨率虽然差一点,但也能满足速度闭环的要求,使用Arduino的Mega2560板子也能处理(霍尔一圈只有13个脉冲,相比而言GMR有500个脉冲,由于脉冲需要触发中断,Mega2560无法胜任GMR的高速驱动要求,霍尔就没问题)。下面是MD36电机的一些性能参数和选型参考:

arduino开发stm32f103c8t6,stm32,嵌入式硬件,单片机,ide

        需要额外指出的是,本帖使用的STM32F107板子在标准72MHz主频下可以胜任4个GMR编码器电机的反馈要求,但是其引脚数量有限,增加巡线传感器后就必须额外增加一个上位主控,从节约成本和满足要求的角度综合考虑,选用霍尔编码器的MD36电机最适合本项目。

         如在推荐链接中采购MD36,需要选择减速比为1:51、霍尔编码器、独立悬挂,对应的轮毂法兰内孔要与电机出轴一致,轮胎尺寸最大可选择8.5寸的。

4、电机驱动器选型

      2款直流电机的需求最大电流不同,淘宝商家分别给出了1款驱动器可覆盖2种电机的需求,每个可驱动2个电机,链接如下:

D50A大功率MOS双路直流有刷电机驱动模块12A大电流24V驱动器-淘宝网 (taobao.com)https://item.taobao.com/item.htm?spm=a1z10.3-c-s.w4002-15726392041.19.4768143evpiwvb&id=569591305325

arduino开发stm32f103c8t6,stm32,嵌入式硬件,单片机,ide

5、底盘如何设计?

      加上几根型材、拧几个螺丝即可。: )SW的模型已经提供在百度网盘文件包里了,如选用36的电机可以自行根据商家图纸简易绘制。

      外壳在周鼎老师的课上按要求进行即可,注意轮子的尺寸和轮距、轴距可能影响外观。

6、STM32F103控制板长什么样子?

图上的板子是常见的样子,价格含运费不高于15元,比UNO还要便宜!!本项目中建议买不焊接排针的C8T6版本,参考链接如下:

arduino开发stm32f103c8t6,stm32,嵌入式硬件,单片机,ide

STM32F103C8T6最小系统板C6T6STM32单片机开发板核心板板江协科技-tmall.com天猫https://detail.tmall.com/item.htm?abbucket=6&id=739127102803&ns=1&skuId=5097382116547&spm=a21n57.1.0.0.5583523cqTJBZI板子接口图如下,要是能找到更清楚的再更新:

arduino开发stm32f103c8t6,stm32,嵌入式硬件,单片机,ide

下面这张图感觉更清楚,但引脚定义有点随意。

arduino开发stm32f103c8t6,stm32,嵌入式硬件,单片机,ide     

 图中可以看到引脚存在复用情况,要参考以下链接根据物理引脚的实际需求选用合适的引脚,比如PWM引脚只有10-13、16~19、29~32、42~46可以,因为这4组引脚背后对应的是4个硬件定时器。再比如,STM32单片机的核心电压为3.3V,部分引脚不可以输入5V电压,能输入5V电压的引脚,做输出引脚使用时也无法直接推挽输出5V,要匹配5V的模块时需留意使用外接上拉电阻的开漏输出才行。

有关引脚的详细功能定义,可参考如下链接(虽然链接中的Maple板子跟我们的不太一样,芯片也有一点型号上的差异,但是不妨碍参考,有关软硬件的内容仍然具有可读性):

docs.leaflabs.com/docs.leaflabs.com/index.htmlhttp://docs.leaflabs.com/docs.leaflabs.com/index.html放一点参考链接的截图:

arduino开发stm32f103c8t6,stm32,嵌入式硬件,单片机,ide

arduino开发stm32f103c8t6,stm32,嵌入式硬件,单片机,ide

 正点原子有款STM32F103RCT6的板子,比上面的C8T6板子接口要丰富,价格也能接受,也可以选择这个,下载程序时记得把芯片型号换一换。使用方法见下面的链接:

【精选】STM32如何使用arduino_ide进行开发_stm32调用 arduino 读外部脉冲长度_正点原子的博客-CSDN博客arduino开发stm32f103c8t6,stm32,嵌入式硬件,单片机,ide【精选】STM32如何使用arduino_ide进行开发_stm32调用 arduino 读外部脉冲长度_正点原子的博客-CSDN博客

7、底盘怎么接线?

所用电机驱动板是兼容3.3V驱动的,随便接合适的引脚即可。用于电机转角反馈的GMR编码器理论上也是兼容的5V/3.3V。不过考虑信号线挺长的,还是给编码器用了5V电源,因此A/B相的引脚就只能选择5V兼容引脚。此外还要预留出串口通讯和USB下载引脚,PC13用于LED指示也需要预留。

借用上面的接口图修改的图如下,其中LF指左前方电机、LR指左后方电机、RF指有前方电机、RR指右后方电机。

驱动板和电机之间接线参考下图(11月16日更新,为了腾出3号串口(PB10、PB11),更换了LR电机的A、B相接线位置至PB8、PB9,程序中也做了更新)。注意图中的A、B相可能与代码不太一样,可能会接反,如果发现闭环控制速度为0时,轮子稍作转动就无法停止,就需要将AB相反转一下。

arduino开发stm32f103c8t6,stm32,嵌入式硬件,单片机,ide

arduino开发stm32f103c8t6,stm32,嵌入式硬件,单片机,ide

8、电源怎么设计?

由于电机工作在24V下,建议采用DJI的 TB48S/TB47S航模电池(主要是有现成的)。

arduino开发stm32f103c8t6,stm32,嵌入式硬件,单片机,ide

9、如何使用Arduino IDE给STM32F103开发板下载代码?

         有3种方式可以给开发板下载代码,分别是通过USB接口(PA11、PA12)、UART1接口(PA9、PA10)和默认的SWD接口。

 A、通过USB接口下载

可参考以下链接配置,优点是不用管BOOT跳线位置,使用体验约等于UNO,也不需要额外的硬件(其实下载bin文件还是需要USB转串口模块)。此种方法只能使用Arduino1.8的版本,新版本会报错。另外一般最高频率只能到72MHz,在本项目中足够使用。

STM32F103C8T6在Arduino IDE里编程_stm32f103c8t6 烧录米思齐bin文件-CSDN博客https://blog.csdn.net/bobo184/article/details/84349184

B、通过UART1接口下载(本帖推荐)

这是网上最常用的方法,优点是在安装上面链接的开发板库后,用串口下载可将103芯片超频到128MHz使用(USB就不可以),Arduino IDE最新版也可以使用。缺点嘛,就是每次下载前后要对BOOT进行跳线,这个有点烦,而且还需要准备一个USB转UART的转换器。参考链接如下:

STM32F103C8T6使用aduino环境编程_interface serial_w32: 115200 8e1-CSDN博客https://blog.csdn.net/qq_38288618/article/details/90553252?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522169684745916800215067511%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=169684745916800215067511&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduend~default-2-90553252-null-null.142%5Ev95%5Einsert_down28v1&utm_term=STM32F103C8T6%20Arduino&spm=1018.2226.3001.4187

 C、通过SWD接口下载

首先你得有个ST-link,如果有这东西的话大概率你已经会用stm自己的IDE了。参考链接如下:Getting Started · stm32duino/Arduino_Core_STM32 Wiki · GitHubhttps://github.com/stm32duino/Arduino_Core_STM32/wiki/Getting-Started上面的链接是一个官方支持STM32duino的网页,几乎支持所有stm32的芯片,需要中文支持的话可以参考以下2个链接:

Arduino借助STM32Duino开发STM32教程-(2023年8月)-CSDN博客https://blog.csdn.net/m0_46236949/article/details/132381810?ops_request_misc=&request_id=&biz_id=102&utm_term=stm32duino%20arduino&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduweb~default-0-132381810.142%5Ev96%5Epc_search_result_base7&spm=1018.2226.3001.4187ArduinoIDE + STM32Link烧录调试_arduino烧录stm32_BobBobBao的博客-CSDN博客STM32,Arduino烧录调试总结_arduino烧录stm32https://blog.csdn.net/sinat_22081411/article/details/125206320?ops_request_misc=&request_id=&biz_id=102&utm_term=STM32F103C8T6%20Arduino%20SWD&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduweb~default-9-125206320.142%5Ev95%5Einsert_down28v1&spm=1018.2226.3001.4187使用ST-link下载可能会报错“STM32CubeProgrammer not found (STM32_Programmer_CLI.exe).”,解决办法如下:

使用官方Arduino板支持包开发全系列STM32_stm32官方支持arduino_笑春风oO的博客-CSDN博客https://blog.csdn.net/qcmyqcmy/article/details/128278285

10、有速度闭环控制的参考代码吗?

我使用的控制器是STM32F103C8T6,接线图参考上面第7节。

以下的代码使用串口控制电机的速度,速度为闭环控制。此代码不能用于系统参数辨识,辨识的代码会在后面提供。另外需要注意:如果迁移到Mega2560,要注意PWM相关代码要做修改,原因是stm32的PWM范围是0~65535,Mega2560是0~255,此外引脚定义也有差异,具体的参考以下代码的注释。

//PID速度闭环控制4个电机

#define LED PC13                    //调试用的LED

//以下为左前方电机引脚定义
#define LF_Motor_IN1 PA5            //LF电机使能引脚1
#define LF_Motor_IN2 PA4            //LF电机使能引脚2
#define LF_PWM PA0                  //LF电机调速引脚
#define LF_ENCODER_A PB7            //LF编码器A相引脚
#define LF_ENCODER_B PB6            //LF编码器B相引脚

//以下为右前方电机引脚定义
#define RF_Motor_IN1 PB0            //RF电机使能引脚1
#define RF_Motor_IN2 PB1            //RF电机使能引脚2
#define RF_PWM PA2                  //RF电机调速引脚
#define RF_ENCODER_A PB12           //RF编码器A相引脚
#define RF_ENCODER_B PB13           //RF编码器B相引脚

//以下为左后方电机引脚定义
#define LR_Motor_IN1 PA7            //LR电机使能引脚1
#define LR_Motor_IN2 PA6            //LR电机使能引脚2
#define LR_PWM PA1                  //LR电机调速引脚
#define LR_ENCODER_A PB8           //LR编码器A相引脚
#define LR_ENCODER_B PB9           //LR编码器B相引脚

//以下为右后电机引脚定义
#define RR_Motor_IN1 PC14           //RR电机使能引脚1
#define RR_Motor_IN2 PC15           //RR电机使能引脚2
#define RR_PWM PA3                  //RR电机调速引脚
#define RR_ENCODER_A PB14           //RR编码器A相引脚
#define RR_ENCODER_B PB15           //RR编码器B相引脚

volatile int i = 0;                //调试用的公共变量

volatile int LF_Velocity = 0, LF_Count = 0;     //左前方电机编码器,Count计数变量 Velocity存储设定时间内A相下降沿的个数,与实际转速正相关
volatile int RF_Velocity = 0, RF_Count = 0;     //左后方电机编码器,Count计数变量 Velocity存储设定时间内A相下降沿的个数,与实际转速正相关
volatile int LR_Velocity = 0, LR_Count = 0;     //右前方电机编码器,Count计数变量 Velocity存储设定时间内A相下降沿的个数,与实际转速正相关
volatile int RR_Velocity = 0, RR_Count = 0;     //右后方电机编码器,Count计数变量 Velocity存储设定时间内A相下降沿的个数,与实际转速正相关

String Target_Value;             //串口获取的速度字符串变量
volatile int LF_value,RF_value,LR_value,RR_value;   //用于存储通过PI控制器计算得到的用于调整电机转速的PWM值,最大值65535
float KP = 200, KI = 20; //PI参数,此处调整会影响启动电流,低速时可能引起震荡
volatile float LF_Target=0,RF_Target=0,LR_Target=0,RR_Target=0; //电机转速目标值,5ms定时器最大可用范围±280,2ms定时器,最大可用范围±120

///*********** 限幅************
//  以下两个参数让输出的PWM在一个合理区间
//  当输出的PWM小于1500时电机不转 所以要设置一个启始PWM
//  STM32单片机的PWM不能超过65535 所以 PWM_Restrict 起到限制上限的作用
//*****************************/
int startPWM = 1500;               //克服死区的启始PWM
int PWM_Restrict = 64000;          //startPW+PWM_Restric=65500<65535

/**********外部中断触发计数器函数(4个电机需要独立的外部中断处理函数)************
  根据转速的方向不同我们将计数器累计为正值或者负值(计数器累计为正值为负值为计数器方向)
  只有方向累计正确了才可以实现正确的调整,否则会出现逆方向满速旋转

  ※※※※※※超级重点※※※※※※

  所谓累计在正确的方向即
  (1)计数器方向
  (2)电机输出方向(控制电机转速方向的接线是正着接还是反着接)
  (3)PI 控制器 里面的误差(Basi)运算是目标值减当前值(Target-Encoder),还是当前值减目标值(Encoder-Target)
  三个方向只有对应上才会有效果否则你接上就是使劲的朝着一个方向(一般来说是反方向)满速旋转,出现这种问题,需要将AB相的线调过来,或改下引脚定义
  我例子里是我自己对应好的,如果其他驱动单片机在自己尝试的时候出现满速旋转就是三个方向没对应上
  下列函数中由于在A相上升沿触发时,B相是低电平,和A相下降沿触发时B是高电平是一个方向,在这种触发方式下,我们将count累计为正,另一种情况将count累计为负
********************************************/
void LF_READ_ENCODER_A()     //左前方电机A相中断
{
  if (digitalRead(LF_ENCODER_A) == HIGH)
  {
    if (digitalRead(LF_ENCODER_B) == LOW)
      LF_Count++;  //根据另外一相电平判定方向
    else
      LF_Count--;
  }
  else
  {
    if (digitalRead(LF_ENCODER_B) == LOW)
      LF_Count--; //根据另外一相电平判定方向
    else
      LF_Count++;
  }
}
void RF_READ_ENCODER_A()     //右前方电机A相中断
{
  if (digitalRead(RF_ENCODER_A) == HIGH)
  {
    if (digitalRead(RF_ENCODER_B) == LOW)
      RF_Count++;  //根据另外一相电平判定方向
    else
      RF_Count--;
  }
  else
  {
    if (digitalRead(RF_ENCODER_B) == LOW)
      RF_Count--; //根据另外一相电平判定方向
    else
      RF_Count++;
  }
}
void LR_READ_ENCODER_A()     //左后方电机A相中断
{
  if (digitalRead(LR_ENCODER_A) == HIGH)
  {
    if (digitalRead(LR_ENCODER_B) == LOW)
      LR_Count++;  //根据另外一相电平判定方向
    else
      LR_Count--;
  }
  else
  {
    if (digitalRead(LR_ENCODER_B) == LOW)
      LR_Count--; //根据另外一相电平判定方向
    else
      LR_Count++;
  }
}

void RR_READ_ENCODER_A()    //右后方电机A相中断
{
  if (digitalRead(RR_ENCODER_A) == HIGH)
  {
    if (digitalRead(RR_ENCODER_B) == LOW)
      RR_Count++;  //根据另外一相电平判定方向
    else
      RR_Count--;
  }
  else
  {
    if (digitalRead(RR_ENCODER_B) == LOW)
      RR_Count--; //根据另外一相电平判定方向
    else
      RR_Count++;
  }
}

/**********定时器中断触发函数(只需要1个定时器)*********/
HardwareTimer timer(3);//声明使用3号定时器
void control()
{ //  cli();     //关闭所有中断,此处尝试不加也行
  //把采用周期(内部定时中断周期)所累计的脉冲下降沿的个数,赋值给速度
  LF_Velocity = LF_Count;  
  RF_Velocity = RF_Count;
  LR_Velocity = LR_Count;
  RR_Velocity = RR_Count;
  
  //脉冲计数器清零
  LF_Count = 0;    
  RF_Count = 0;
  LR_Count = 0;
  RR_Count = 0;
   
//以下为4个电机同时计算PID参数
 LF_value = LF_Incremental_PI(LF_Velocity, LF_Target); //通过目标值和当前值在PID函数下算出我们需要调整用的PWM值
  RF_value = RF_Incremental_PI(RF_Velocity, RF_Target);
  LR_value = LR_Incremental_PI(LR_Velocity, LR_Target);
  RR_value = RR_Incremental_PI(RR_Velocity, RR_Target);
//以下为4个电机同时输出PWM值
  LF_Set_PWM(LF_value);    
  RF_Set_PWM(RF_value);
  LR_Set_PWM(LR_value);
  RR_Set_PWM(RR_value);

//以下为调试代码,调试完成需要删除,避免浪费CPU资源 
 Serial1.print(LF_value);//输出左前轮的PWM值
  Serial.print(",");
  Serial1.println(LF_Velocity);//输出左前轮的转速
  
  //  sei();     //打开所有中断,此处尝试不加也行
}

/***********PI控制器****************/
int LF_Incremental_PI(int LF_Encoder, float LF_Target1)
{
  static float LF_Bias, LF_MPWM = 0, LF_Last_bias = 0;     //定义全局静态浮点型变量 PWM,Bias(本次偏差),Last_bias(上次偏差)
  LF_Bias = LF_Target1 - LF_Encoder;                       //计算偏差,目标值减去当前值
  LF_MPWM += KP * (LF_Bias - LF_Last_bias) + KI * LF_Bias; //增量式PI控制计算
  if (LF_MPWM > PWM_Restrict)
    LF_MPWM = PWM_Restrict;                                   //限幅
  if (LF_MPWM < -PWM_Restrict)
    LF_MPWM = -PWM_Restrict;                                  //限幅
  LF_Last_bias = LF_Bias;                                     //保存上一次偏差
  return LF_MPWM;                                          //增量输出
}
int RF_Incremental_PI(int RF_Encoder, float RF_Target1)
{
  static float RF_Bias, RF_MPWM = 0, RF_Last_bias = 0;                //定义全局静态浮点型变量 PWM,Bias(本次偏差),Last_bias(上次偏差)
  RF_Bias = RF_Target1 - RF_Encoder;                              //计算偏差,目标值减去当前值
  RF_MPWM += KP * (RF_Bias - RF_Last_bias) + KI * RF_Bias; //增量式PI控制计算
  if (RF_MPWM > PWM_Restrict)
    RF_MPWM = PWM_Restrict;                                   //限幅
  if (RF_MPWM < -PWM_Restrict)
    RF_MPWM = -PWM_Restrict;                                  //限幅
  RF_Last_bias = RF_Bias;                                     //保存上一次偏差
   return RF_MPWM;                                          //增量输出
}
int LR_Incremental_PI(int LR_Encoder, float LR_Target1)
{
  static float LR_Bias, LR_MPWM = 0, LR_Last_bias = 0;                //定义全局静态浮点型变量 PWM,Bias(本次偏差),Last_bias(上次偏差)
  LR_Bias = LR_Target1 - LR_Encoder;                              //计算偏差,目标值减去当前值
  LR_MPWM += KP * (LR_Bias - LR_Last_bias) + KI * LR_Bias; //增量式PI控制计算
  if (LR_MPWM > PWM_Restrict)
    LR_MPWM = PWM_Restrict;                                   //限幅
  if (LR_MPWM < -PWM_Restrict)
    LR_MPWM = -PWM_Restrict;                                  //限幅
  LR_Last_bias = LR_Bias;                                     //保存上一次偏差
   return LR_MPWM;                                          //增量输出
}
int RR_Incremental_PI(int RR_Encoder, float RR_Target1)
{
  static float RR_Bias, RR_MPWM = 0, RR_Last_bias = 0;                //定义全局静态浮点型变量 PWM,Bias(本次偏差),Last_bias(上次偏差)
  RR_Bias = RR_Target1 - RR_Encoder;                              //计算偏差,目标值减去当前值
  RR_MPWM += KP * (RR_Bias - RR_Last_bias) + KI * RR_Bias; //增量式PI控制计算
  if (RR_MPWM > PWM_Restrict)
    RR_MPWM = PWM_Restrict;                                   //限幅
  if (RR_MPWM < -PWM_Restrict)
    RR_MPWM = -PWM_Restrict;                                  //限幅
  RR_Last_bias = RR_Bias;                                     //保存上一次偏差
   return RR_MPWM;                                          //增量输出
}

/**********电机驱动函数*********/
void LF_Set_PWM(int LF_motora)
{
  if (LF_motora > 0)  //如果算出的PWM为正
  {

    digitalWrite(LF_Motor_IN1, 1);
    digitalWrite(LF_Motor_IN2, 0);
    pwmWrite(LF_PWM, LF_motora + startPWM);  //让PWM在设定正转方向(我们认为的正转方向)正向输出调整,此处的PWM输出函数跟Mega2560不同
  } else if (LF_motora == 0)  //如果PWM为0停车
  {
    digitalWrite(LF_Motor_IN2, 0);
    digitalWrite(LF_Motor_IN1, 0);
  } else if (LF_motora < 0)  //如果算出的PWM为负
  {

    digitalWrite(LF_Motor_IN1, 0);
    digitalWrite(LF_Motor_IN2, 1);
    pwmWrite(LF_PWM, -LF_motora + startPWM);  //让PWM在设定反转方向反向输出调整
  }
}
void RF_Set_PWM(int RF_motora)
{
  if (RF_motora > 0)  //如果算出的PWM为正
  {

    digitalWrite(RF_Motor_IN1, 1);
    digitalWrite(RF_Motor_IN2, 0);
    pwmWrite(RF_PWM, RF_motora + startPWM);  //让PWM在设定正转方向(我们认为的正转方向)正向输出调整
  } else if (RF_motora == 0)  //如果PWM为0停车
  {
    digitalWrite(RF_Motor_IN2, 0);
    digitalWrite(RF_Motor_IN1, 0);
  } else if (RF_motora < 0)  //如果算出的PWM为负
  {

    digitalWrite(RF_Motor_IN1, 0);
    digitalWrite(RF_Motor_IN2, 1);
    pwmWrite(RF_PWM, -RF_motora + startPWM);  //让PWM在设定反转方向反向输出调整
  }
}
void LR_Set_PWM(int LR_motora)
{
  if (LR_motora > 0)  //如果算出的PWM为正
  {

    digitalWrite(LR_Motor_IN1, 1);
    digitalWrite(LR_Motor_IN2, 0);
    pwmWrite(LR_PWM, LR_motora + startPWM);  //让PWM在设定正转方向(我们认为的正转方向)正向输出调整
  } else if (LR_motora == 0)  //如果PWM为0停车
  {
    digitalWrite(LR_Motor_IN2, 0);
    digitalWrite(LR_Motor_IN1, 0);
  } else if (LR_motora < 0)  //如果算出的PWM为负
  {

    digitalWrite(LR_Motor_IN1, 0);
    digitalWrite(LR_Motor_IN2, 1);
    pwmWrite(LR_PWM, -LR_motora + startPWM);  //让PWM在设定反转方向反向输出调整
  }
}
void RR_Set_PWM(int RR_motora)
{
  if (RR_motora > 0)  //如果算出的PWM为正
  {

    digitalWrite(RR_Motor_IN1, 1);
    digitalWrite(RR_Motor_IN2, 0);
    pwmWrite(RR_PWM, RR_motora + startPWM);  //让PWM在设定正转方向(我们认为的正转方向)正向输出调整
  } else if (RR_motora == 0)  //如果PWM为0停车
  {
    digitalWrite(RR_Motor_IN2, 0);
    digitalWrite(RR_Motor_IN1, 0);
  } else if (RR_motora < 0)  //如果算出的PWM为负
  {

    digitalWrite(RR_Motor_IN1, 0);
    digitalWrite(RR_Motor_IN2, 1);
    pwmWrite(RR_PWM, -RR_motora + startPWM);  //让PWM在设定反转方向反向输出调整
  }
}

void setup()
{
  Serial1.begin(115200);            //打开串口
  Serial1.println("/*****开始驱动*****/");
  delay(1000);

  pinMode(LED, OUTPUT);            //调试用的闪烁LED,PC13
  
  pinMode(LF_ENCODER_A, INPUT);    //设置两个相线为输入模式
  pinMode(LF_ENCODER_B, INPUT);
  pinMode(LF_Motor_IN1, OUTPUT_OPEN_DRAIN);  //设置两个驱动引脚为输出模式,由于stm32的引脚接收5V作为输出时需要工作在开漏输出模式下,这与Mega2560函数定义是有差别的,Mega2560可以直接推挽输出5V,引脚直接配置为OUTPUT即可
  pinMode(LF_Motor_IN2, OUTPUT_OPEN_DRAIN);
  pinMode(LF_PWM, PWM_OPEN_DRAIN);

  pinMode(RF_ENCODER_A, INPUT);    //设置两个相线为输入模式
  pinMode(RF_ENCODER_B, INPUT);
  pinMode(RF_Motor_IN1, OUTPUT_OPEN_DRAIN);  //设置两个驱动引脚为输出模式,由于stm32的引脚接收5V作为输出时需要工作在开漏输出模式下,这与Mega2560函数定义是有差别的,Mega2560可以直接推挽输出5V,引脚直接配置为OUTPUT即可
  pinMode(RF_Motor_IN2, OUTPUT_OPEN_DRAIN);
  pinMode(RF_PWM, PWM_OPEN_DRAIN);

  pinMode(LR_ENCODER_A, INPUT);    //设置两个相线为输入模式
  pinMode(LR_ENCODER_B, INPUT);
  pinMode(LR_Motor_IN1, OUTPUT_OPEN_DRAIN);  //设置两个驱动引脚为输出模式,由于stm32的引脚接收5V作为输出时需要工作在开漏输出模式下,这与Mega2560函数定义是有差别的,Mega2560可以直接推挽输出5V,引脚直接配置为OUTPUT即可
  pinMode(LR_Motor_IN2, OUTPUT_OPEN_DRAIN);
  pinMode(LR_PWM, PWM_OPEN_DRAIN);

  pinMode(RR_ENCODER_A, INPUT);    //设置两个相线为输入模式
  pinMode(RR_ENCODER_B, INPUT);
  pinMode(RR_Motor_IN1, OUTPUT_OPEN_DRAIN);  //设置两个驱动引脚为输出模式,由于stm32的引脚接收5V作为输出时需要工作在开漏输出模式下,这与Mega2560函数定义是有差别的,Mega2560可以直接推挽输出5V,引脚直接配置为OUTPUT即可
  pinMode(RR_Motor_IN2, OUTPUT_OPEN_DRAIN);
  pinMode(RR_PWM, PWM_OPEN_DRAIN);
  //下面是外部中断的初始化
  attachInterrupt(LF_ENCODER_A, LF_READ_ENCODER_A, FALLING); //开启对应A相引脚的外部中断,触发方式为FALLING 即下降沿都触发,触发的中断函数为 LF_ENCODER_A
  attachInterrupt(RF_ENCODER_A, RF_READ_ENCODER_A, FALLING);
  attachInterrupt(LR_ENCODER_A, LR_READ_ENCODER_A, FALLING);
  attachInterrupt(RR_ENCODER_A, RR_READ_ENCODER_A, FALLING);
  //下面是定时器的初始化,Mega2560的用法与此处有差异,参考引用库函数才行
  timer.pause();// Pause the timer while we're configuring it
  timer.setPeriod(5000); // Set up period in microseconds,5000us=5ms
  timer.setChannel1Mode(TIMER_OUTPUT_COMPARE);// Set up an interrupt on channel 1
  timer.setCompare(TIMER_CH1, 1);  // Interrupt 1 count after each update
  timer.attachCompare1Interrupt(control);//定时中断函数名声明
  timer.refresh();// Refresh the timer's count, prescale, and overflow
  timer.resume();// Start the timer counting
}

void loop()
{
  while (Serial1.available() > 0)       //检测串口是否接收到了数据
  {
    Target_Value = Serial.readString(); //读取串口字符串
    i = Target_Value.toFloat();   //将字符串转换为浮点型,并将其赋给目标值
    
     if(i==1)   //底盘前进
    {
      LF_Target=100;
      LR_Target=100;
      RF_Target=100;
      RR_Target=100;
    }
    else if(i==0)//底盘停止
    { LF_Target=0;
      LR_Target=0;
      RF_Target=0;
      RR_Target=0;
    }
    else if(i==2)//底盘原地拐弯
    { LF_Target=-100;
      LR_Target=-100;
      RF_Target=100;
      RR_Target=100;
    }
    Serial.print("Target:");      //串口打印出设定的目标转速
    Serial.println(LF_Target);
  }

}

      代码中,通过串口接收的数据(0、1、2)来控制底盘的停止、前进和原地拐弯。如果需要通过串口直接控制每个电机的转速,需要确定通讯协议,通过解析通讯协议里的代码来获取每个电机目标速度值。

11、怎样通过识别机器人底盘的控制参数?

       先假设地盘的模型为二阶模型(理想的小车为一阶惯性模型,考虑到我们的底盘是充气轮胎、带有独立悬架,将其假定为二阶模型)。

      然后将电机的PWM作为输入、电机速度(编码器反馈)作为输出,程序中让4个电机的PWM一样,输入为PWM阶跃响应,底盘直线加速运动作为输出,通过无线串口获取输入输出数据。

      再然后将多次实验获得的输入输出数据(保留1、2组数据不用)导入到MATLAB中,计算出二阶模型的各参数。

      最后将刚才保留的数据输入计算出来的模型中,将实际输出与计算输出进行比较,观察模型准确性。如果实际输出与计算输出接近,证明实验模型建立还算成功。

12、系统输入(PWM)与输出(电机转速)之间的关系

如4中提及到的电机驱动器,我们能控制的只有PWM。假设驱动器电源电压不变,经过驱动器脉宽调制(PWM)后输入到电机的等效电压与PWM占空比是线性关系,则输入量PWM实际上控制的是电机输入端的电压Ea。直流有刷电机的等效模型如下:

arduino开发stm32f103c8t6,stm32,嵌入式硬件,单片机,ide

其中,Ea:电源电压,Ia:电机电流,R:电机的等效电阻,L:电机的等效电感,Ec:电机旋转反电动势。

在该等效电路中的直流关系为:Ea=Ia×R+Ec ……(1)

电机反电动势Ec与转速成正比,因此可以表达为:Ec=Ke×N ……(2)

※N:转速 [rpm],Ke:反电动势常数 [V/rpm]

电机转矩T [N.m]与电机电流成正比,因此可以表达为:T=Kt×Ia ……(3)

※Kt:转矩常数 [N.m/A]

将公式(2)、(3)代入(1)中可以得到转速N与转矩T的关系:

N=Ea/Ke-R/(Kt×Ke)×T ……(4)

当底盘匀速运动时,T只取决于底盘4个电机的阻力矩,分析公式(4)会发现N与Ea线性相关。即,通过PWM控制了Ea,就控制了底盘匀速运动时电机的转速N。

以下是来自胡寿松第7版《自动控制原理》P25中对此问题的建模过程,结论类似:

arduino开发stm32f103c8t6,stm32,嵌入式硬件,单片机,ide

arduino开发stm32f103c8t6,stm32,嵌入式硬件,单片机,ide

13、通过输入PWM直接控制底盘前进的代码

此代码的输入参数是电机的PWM,输出是电机的转速。由于4个电机是速度一致,可视为整个底盘的PWM和速度。

#define LED PC13                    //调试用的LED
//以下为左前方电机引脚定义
#define LF_Motor_IN1 PA5            //LF电机使能引脚1
#define LF_Motor_IN2 PA4            //LF电机使能引脚2
#define LF_PWM PA0                  //LF电机调速引脚
#define LF_ENCODER_A PB7            //LF编码器A相引脚
#define LF_ENCODER_B PB6            //LF编码器B相引脚
//以下为右前方电机引脚定义
#define RF_Motor_IN1 PB0            //RF电机使能引脚1
#define RF_Motor_IN2 PB1            //RF电机使能引脚2
#define RF_PWM PA2                  //RF电机调速引脚
#define RF_ENCODER_A PB12           //RF编码器A相引脚
#define RF_ENCODER_B PB13           //RF编码器B相引脚
//以下为左后方电机引脚定义
#define LR_Motor_IN1 PA7            //LR电机使能引脚1
#define LR_Motor_IN2 PA6            //LR电机使能引脚2
#define LR_PWM PA1                  //LR电机调速引脚
#define LR_ENCODER_A PB8           //LR编码器A相引脚
#define LR_ENCODER_B PB9           //LR编码器B相引脚
//以下为右后电机引脚定义
#define RR_Motor_IN1 PC14           //RR电机使能引脚1
#define RR_Motor_IN2 PC15           //RR电机使能引脚2
#define RR_PWM PA3                  //RR电机调速引脚
#define RR_ENCODER_A PB14           //RR编码器A相引脚
#define RR_ENCODER_B PB15           //RR编码器B相引脚

volatile int i = 0;                //调试用的公共变量

volatile int LF_Velocity = 0, LF_Count = 0;     //左前方电机编码器,Count计数变量 Velocity存储设定时间内A相下降沿的个数,与实际转速正相关
volatile int RF_Velocity = 0, RF_Count = 0;     //左后方电机编码器,Count计数变量 Velocity存储设定时间内A相下降沿的个数,与实际转速正相关
volatile int LR_Velocity = 0, LR_Count = 0;     //右前方电机编码器,Count计数变量 Velocity存储设定时间内A相下降沿的个数,与实际转速正相关
volatile int RR_Velocity = 0, RR_Count = 0;     //右后方电机编码器,Count计数变量 Velocity存储设定时间内A相下降沿的个数,与实际转速正相关

String Target_Value;             //串口获取的速度字符串变量
volatile int LF_value,RF_value,LR_value,RR_value;   //用于存储通过PI控制器计算得到的用于调整电机转速的PWM值,最大值65535
float KP = 200, KI = 20; //PI参数,此处调整会影响启动电流或引起震荡
volatile float LF_Target=0,RF_Target=0,LR_Target=0,RR_Target=0; //电机转速目标值,5ms定时器最大可用范围±280,2ms定时器,最大可用范围±120
///*********** 限幅************!!!!在此次的实验中不使用,所以startPWM设置为0。
//  以下两个参数让输出的PWM在一个合理区间
//  当输出的PWM小于1500时电机不转 所以要设置一个启始PWM
//  STM32单片机的PWM不能超过65535 所以 PWM_Restrict 起到限制上限的作用
//*****************************/
int startPWM = 0;               //初始PWM
int PWM_Restrict = 65535;          //startPW+PWM_Restric=65535<=65535

/**********外部中断触发计数器函数(4个电机需要独立的外部中断处理函数)************
  根据转速的方向不同我们将计数器累计为正值或者负值(计数器累计为正值为负值为计数器方向)
  只有方向累计正确了才可以实现正确的调整,否则会出现逆方向满速旋转

  ※※※※※※超级重点※※※※※※

  所谓累计在正确的方向即
  (1)计数器方向
  (2)电机输出方向(控制电机转速方向的接线是正着接还是反着接)
  (3)PI 控制器 里面的误差(Basi)运算是目标值减当前值(Target-Encoder),还是当前值减目标值(Encoder-Target)
  三个方向只有对应上才会有效果否则你接上就是使劲的朝着一个方向(一般来说是反方向)满速旋转,出现这种问题,需要将AB相的线调过来,或改下引脚定义
  我例子里是我自己对应好的,如果其他驱动单片机在自己尝试的时候出现满速旋转就是三个方向没对应上
  下列函数中由于在A相上升沿触发时,B相是低电平,和A相下降沿触发时B是高电平是一个方向,在这种触发方式下,我们将count累计为正,另一种情况将count累计为负
********************************************/
void LF_READ_ENCODER_A()     //左前方电机A相中断
{
  if (digitalRead(LF_ENCODER_A) == HIGH)
  {
    if (digitalRead(LF_ENCODER_B) == LOW)
      LF_Count++;  //根据另外一相电平判定方向
    else
      LF_Count--;
  }
  else
  {
    if (digitalRead(LF_ENCODER_B) == LOW)
      LF_Count--; //根据另外一相电平判定方向
    else
      LF_Count++;
  }
}
void RF_READ_ENCODER_A()     //右前方电机A相中断
{
  if (digitalRead(RF_ENCODER_A) == HIGH)
  {
    if (digitalRead(RF_ENCODER_B) == LOW)
      RF_Count++;  //根据另外一相电平判定方向
    else
      RF_Count--;
  }
  else
  {
    if (digitalRead(RF_ENCODER_B) == LOW)
      RF_Count--; //根据另外一相电平判定方向
    else
      RF_Count++;
  }
}
void LR_READ_ENCODER_A()     //左后方电机A相中断
{
  if (digitalRead(LR_ENCODER_A) == HIGH)
  {
    if (digitalRead(LR_ENCODER_B) == LOW)
      LR_Count++;  //根据另外一相电平判定方向
    else
      LR_Count--;
  }
  else
  {
    if (digitalRead(LR_ENCODER_B) == LOW)
      LR_Count--; //根据另外一相电平判定方向
    else
      LR_Count++;
  }
}

void RR_READ_ENCODER_A()    //右后方电机A相中断
{
  if (digitalRead(RR_ENCODER_A) == HIGH)
  {
    if (digitalRead(RR_ENCODER_B) == LOW)
      RR_Count++;  //根据另外一相电平判定方向
    else
      RR_Count--;
  }
  else
  {
    if (digitalRead(RR_ENCODER_B) == LOW)
      RR_Count--; //根据另外一相电平判定方向
    else
      RR_Count++;
  }
}

/**********定时器中断触发函数(只需要1个定时器)*********/
HardwareTimer timer(3);//声明使用3号定时器
void control()
{ //  cli();
  LF_Velocity = LF_Count;  //把采用周期(内部定时中断周期)所累计的脉冲下降沿的个数,赋值给速度
  RF_Velocity = RF_Count;
  LR_Velocity = LR_Count;
  RR_Velocity = RR_Count;
  LF_Count = 0;            //脉冲计数器清零
  RF_Count = 0;
  LR_Count = 0;
  RR_Count = 0;
   
 //此实验中速度不闭环,所以不执行PID计算

  LF_Set_PWM(LF_value);    //将串口接收到的PWM值直接输出给4个电机
  RF_Set_PWM(RF_value);
  LR_Set_PWM(LR_value);
  RR_Set_PWM(RR_value);
  Serial1.print(LF_value);  //输出左前轮电机当前的PWM值。由于4个轮子的速度基本相同,取任意一个电机的转速均可
  Serial.print(",");
  Serial1.println(LF_Velocity);//输出左前轮的转速
  
  //   sei();
}

/***********PI控制器****************/
int LF_Incremental_PI(int LF_Encoder, float LF_Target1)
{
  static float LF_Bias, LF_MPWM = 0, LF_Last_bias = 0;                //定义全局静态浮点型变量 PWM,Bias(本次偏差),Last_bias(上次偏差)
  LF_Bias = LF_Target1 - LF_Encoder;                              //计算偏差,目标值减去当前值
  LF_MPWM += KP * (LF_Bias - LF_Last_bias) + KI * LF_Bias; //增量式PI控制计算
  if (LF_MPWM > PWM_Restrict)
    LF_MPWM = PWM_Restrict;                                   //限幅
  if (LF_MPWM < -PWM_Restrict)
    LF_MPWM = -PWM_Restrict;                                  //限幅
  LF_Last_bias = LF_Bias;                                     //保存上一次偏差
 // Serial1.println(LF_MPWM);
 // Serial1.print(" ");
 // Serial1.println(LF_Encoder);
  return LF_MPWM;                                          //增量输出
}
int RF_Incremental_PI(int RF_Encoder, float RF_Target1)
{
  static float RF_Bias, RF_MPWM = 0, RF_Last_bias = 0;                //定义全局静态浮点型变量 PWM,Bias(本次偏差),Last_bias(上次偏差)
  RF_Bias = RF_Target1 - RF_Encoder;                              //计算偏差,目标值减去当前值
  RF_MPWM += KP * (RF_Bias - RF_Last_bias) + KI * RF_Bias; //增量式PI控制计算
  if (RF_MPWM > PWM_Restrict)
    RF_MPWM = PWM_Restrict;                                   //限幅
  if (RF_MPWM < -PWM_Restrict)
    RF_MPWM = -PWM_Restrict;                                  //限幅
  RF_Last_bias = RF_Bias;                                     //保存上一次偏差
  //Serial.println(RF_MPWM);
  //Serial.print(" ");
  //Serial.println(RF_Encoder);
  return RF_MPWM;                                          //增量输出
}
int LR_Incremental_PI(int LR_Encoder, float LR_Target1)
{
  static float LR_Bias, LR_MPWM = 0, LR_Last_bias = 0;                //定义全局静态浮点型变量 PWM,Bias(本次偏差),Last_bias(上次偏差)
  LR_Bias = LR_Target1 - LR_Encoder;                              //计算偏差,目标值减去当前值
  LR_MPWM += KP * (LR_Bias - LR_Last_bias) + KI * LR_Bias; //增量式PI控制计算
  if (LR_MPWM > PWM_Restrict)
    LR_MPWM = PWM_Restrict;                                   //限幅
  if (LR_MPWM < -PWM_Restrict)
    LR_MPWM = -PWM_Restrict;                                  //限幅
  LR_Last_bias = LR_Bias;                                     //保存上一次偏差
  //Serial.println(LR_MPWM);
  //Serial.print(" ");
  //Serial.println(LR_Encoder);
  return LR_MPWM;                                          //增量输出
}
int RR_Incremental_PI(int RR_Encoder, float RR_Target1)
{
  static float RR_Bias, RR_MPWM = 0, RR_Last_bias = 0;                //定义全局静态浮点型变量 PWM,Bias(本次偏差),Last_bias(上次偏差)
  RR_Bias = RR_Target1 - RR_Encoder;                              //计算偏差,目标值减去当前值
  RR_MPWM += KP * (RR_Bias - RR_Last_bias) + KI * RR_Bias; //增量式PI控制计算
  if (RR_MPWM > PWM_Restrict)
    RR_MPWM = PWM_Restrict;                                   //限幅
  if (RR_MPWM < -PWM_Restrict)
    RR_MPWM = -PWM_Restrict;                                  //限幅
  RR_Last_bias = RR_Bias;                                     //保存上一次偏差
  //Serial.println(RR_MPWM);
  //Serial.print(" ");
  //Serial.println(RR_Encoder);
  return RR_MPWM;                                          //增量输出
}

/**********电机驱动函数*********/
void LF_Set_PWM(int LF_motora)
{
  if (LF_motora > 0)  //如果算出的PWM为正
  {

    digitalWrite(LF_Motor_IN1, 1);
    digitalWrite(LF_Motor_IN2, 0);
    pwmWrite(LF_PWM, LF_motora + startPWM);  //让PWM在设定正转方向(我们认为的正转方向)正向输出调整
  } else if (LF_motora == 0)  //如果PWM为0停车
  {
    digitalWrite(LF_Motor_IN2, 0);
    digitalWrite(LF_Motor_IN1, 0);
  } else if (LF_motora < 0)  //如果算出的PWM为负
  {

    digitalWrite(LF_Motor_IN1, 0);
    digitalWrite(LF_Motor_IN2, 1);
    pwmWrite(LF_PWM, -LF_motora + startPWM);  //让PWM在设定反转方向反向输出调整
  }
}
void RF_Set_PWM(int RF_motora)
{
  if (RF_motora > 0)  //如果算出的PWM为正
  {

    digitalWrite(RF_Motor_IN1, 1);
    digitalWrite(RF_Motor_IN2, 0);
    pwmWrite(RF_PWM, RF_motora + startPWM);  //让PWM在设定正转方向(我们认为的正转方向)正向输出调整
  } else if (RF_motora == 0)  //如果PWM为0停车
  {
    digitalWrite(RF_Motor_IN2, 0);
    digitalWrite(RF_Motor_IN1, 0);
  } else if (RF_motora < 0)  //如果算出的PWM为负
  {

    digitalWrite(RF_Motor_IN1, 0);
    digitalWrite(RF_Motor_IN2, 1);
    pwmWrite(RF_PWM, -RF_motora + startPWM);  //让PWM在设定反转方向反向输出调整
  }
}
void LR_Set_PWM(int LR_motora)
{
  if (LR_motora > 0)  //如果算出的PWM为正
  {

    digitalWrite(LR_Motor_IN1, 1);
    digitalWrite(LR_Motor_IN2, 0);
    pwmWrite(LR_PWM, LR_motora + startPWM);  //让PWM在设定正转方向(我们认为的正转方向)正向输出调整
  } else if (LR_motora == 0)  //如果PWM为0停车
  {
    digitalWrite(LR_Motor_IN2, 0);
    digitalWrite(LR_Motor_IN1, 0);
  } else if (LR_motora < 0)  //如果算出的PWM为负
  {

    digitalWrite(LR_Motor_IN1, 0);
    digitalWrite(LR_Motor_IN2, 1);
    pwmWrite(LR_PWM, -LR_motora + startPWM);  //让PWM在设定反转方向反向输出调整
  }
}
void RR_Set_PWM(int RR_motora)
{
  if (RR_motora > 0)  //如果算出的PWM为正
  {

    digitalWrite(RR_Motor_IN1, 1);
    digitalWrite(RR_Motor_IN2, 0);
    pwmWrite(RR_PWM, RR_motora + startPWM);  //让PWM在设定正转方向(我们认为的正转方向)正向输出调整
  } else if (RR_motora == 0)  //如果PWM为0停车
  {
    digitalWrite(RR_Motor_IN2, 0);
    digitalWrite(RR_Motor_IN1, 0);
  } else if (RR_motora < 0)  //如果算出的PWM为负
  {

    digitalWrite(RR_Motor_IN1, 0);
    digitalWrite(RR_Motor_IN2, 1);
    pwmWrite(RR_PWM, -RR_motora + startPWM);  //让PWM在设定反转方向反向输出调整
  }
}

void setup()
{
  Serial1.begin(115200);            //打开串口
  Serial1.println("/*****开始驱动*****/");
  delay(1000);

  pinMode(LED, OUTPUT);            //调试用的闪烁LED,PC13
  
  pinMode(LF_ENCODER_A, INPUT);    //设置两个相线为输入模式
  pinMode(LF_ENCODER_B, INPUT);
  pinMode(LF_Motor_IN1, OUTPUT_OPEN_DRAIN);  //设置两个驱动引脚为输出模式
  pinMode(LF_Motor_IN2, OUTPUT_OPEN_DRAIN);
  pinMode(LF_PWM, PWM_OPEN_DRAIN);

  pinMode(RF_ENCODER_A, INPUT);    //设置两个相线为输入模式
  pinMode(RF_ENCODER_B, INPUT);
  pinMode(RF_Motor_IN1, OUTPUT_OPEN_DRAIN);  //设置两个驱动引脚为输出模式
  pinMode(RF_Motor_IN2, OUTPUT_OPEN_DRAIN);
  pinMode(RF_PWM, PWM_OPEN_DRAIN);

  pinMode(LR_ENCODER_A, INPUT);    //设置两个相线为输入模式
  pinMode(LR_ENCODER_B, INPUT);
  pinMode(LR_Motor_IN1, OUTPUT_OPEN_DRAIN);  //设置两个驱动引脚为输出模式
  pinMode(LR_Motor_IN2, OUTPUT_OPEN_DRAIN);
  pinMode(LR_PWM, PWM_OPEN_DRAIN);

  pinMode(RR_ENCODER_A, INPUT);    //设置两个相线为输入模式
  pinMode(RR_ENCODER_B, INPUT);
  pinMode(RR_Motor_IN1, OUTPUT_OPEN_DRAIN);  //设置两个驱动引脚为输出模式
  pinMode(RR_Motor_IN2, OUTPUT_OPEN_DRAIN);
  pinMode(RR_PWM, PWM_OPEN_DRAIN);
  //下面是外部中断的初始
  attachInterrupt(LF_ENCODER_A, LF_READ_ENCODER_A, FALLING); //开启对应A相引脚的外部中断,触发方式为FALLING 即下降沿都触发,触发的中断函数为 READ_ENCODER_A
  attachInterrupt(RF_ENCODER_A, RF_READ_ENCODER_A, FALLING);
  attachInterrupt(LR_ENCODER_A, LR_READ_ENCODER_A, FALLING);
  attachInterrupt(RR_ENCODER_A, RR_READ_ENCODER_A, FALLING);
  //下面是定时器的初始化
  timer.pause();// Pause the timer while we're configuring it
  timer.setPeriod(5000);  Set up period in microseconds,5000us=5ms
  timer.setChannel1Mode(TIMER_OUTPUT_COMPARE);// Set up an interrupt on channel 1
  timer.setCompare(TIMER_CH1, 1);  // Interrupt 1 count after each update
  timer.attachCompare1Interrupt(control);//定时中断函数名声明
  timer.refresh();// Refresh the timer's count, prescale, and overflow
  timer.resume();// Start the timer counting
}

void loop()
{
  while (Serial1.available() > 0)       //检测串口是否接收到了数据
  {
    Target_Value = Serial.readString(); //读取串口字符串
    i = Target_Value.toFloat();   //将字符串转换为浮点型,并将其赋给目标值
      LF_value=i;
      LR_value=i;
      RF_value=i;
      RR_value=i;
      delay(2000); //启动后延时运行2s
      i=0;
      LF_value=0;
      LR_value=0;
      RF_value=0;
      RR_value=0;
  }
}

14、Arduino中串口函数如何使用?

调试均通过串口通讯完成,有关的函数使用办法如下:

Arduino串口函数详解-CSDN博客https://blog.csdn.net/u014421313/article/details/125421394?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522169761775616800211527313%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=169761775616800211527313&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduend~default-1-125421394-null-null.142%5Ev96%5Epc_search_result_base7&utm_term=%E4%B8%B2%E5%8F%A3%E5%8D%8F%E8%AE%AE%E8%A7%A3%E6%9E%90%20Arduino&spm=1018.2226.3001.4187

15、如何接收处理串口指令?

第10节中提供的代码每次只能有效接收1个字节的指令。如果我们希望通过串口1次发送控制4个电机的速度,或通过1次发送控制底盘的线速度和偏航角,同时还希望提高串口通讯的可靠性,我们就需要使用串口指令控制底盘。简单说来,指令是按一定规则(通讯协议)编制的一系列字符串,通常由帧头、数据和帧尾组成。帧头用于识别指令类型、告诉接收端指令起始、触发解析指令的函数;数据就是指令包含的有效数据,以字节为单位,根据指令协议有规律的放置数据;帧尾用于告知接收端指令发送完成,也可能包含用于校验通讯可靠性的校验位。

简单的串口协议发送、接收办法如下,需要灵活应用串口函数:

Arduino解析串口数据超简单方法-CSDN博客

函数sscanf可以帮助直接解析接收到的字符串:

sscanf函数使用详解_faihung的博客-CSDN博客

如果需要复杂的自定义协议,可以参考下面:

Arduino自定义通信协议解析_arduino 自定义通信协议-CSDN博客

16、怎样使用无线串口连接底盘?

     由于底盘在实验时处于移动状态,为及时获取数据,需要将有线的串口UART更改为无线连接。再次感谢万能的淘宝,用于Arduino开发的蓝牙串口套件参见下面的链接:

七星虫蓝牙 无线下载器 适用于arduino 无线下载 自动复位-tmall.com天猫

arduino开发stm32f103c8t6,stm32,嵌入式硬件,单片机,ide

相关的资料见:https://pan.baidu.com/s/1ZjvOgEXduiqbXBLxzm5tnA 提取码:ye54

连接时要注意:

(1)蓝牙从机模块(上图左下角的模块)的RX接控制板的TX(PA9)引脚、蓝牙模块的TX接控制板的RX(PA10)引脚。

(2)蓝牙主机模块(上图左上角的模块)直接接入电脑USB口,装驱动后会被识别为一个串口,默认波特率应该使用115200。如果模块灯不停闪烁,就拔下重插。

(3)主机和从机都上电后二者会自行匹配连接,连接完成后两者的led灯均常亮。正常使用距离为无遮挡10m范围内。

(4)本帖使用的stm32无法使用无线模块的下载功能,由于此串口与下载程序用的串口一样,所以实际上需要先用串口模块(上图右下角的模块)烧录好程序后再将蓝牙模块重新接入引脚。其他的串口已经被控制电机的引脚占用了,所以不得不复用。Arduino官方的UNO和2560使用这款无线串口,可以实现无线下载。

17、如何使用串口上位机采集数据?

使用自带串口记录功能的上位机都能采集具体数据,这里介绍SerialPlot这个软件。

(1)首先根据蓝牙主机实际的串口编号(通过硬件管理器查询),设置波特率115200,其他部分默认即可,然后点击'open'。

arduino开发stm32f103c8t6,stm32,嵌入式硬件,单片机,ide

(2)在Data Format处选择‘ASCII’,因为控制器使用的是printf函数,发出来的是字符串。通道数选‘2’,因为需要绘制输入和输出2个数据曲线。分隔符用 “,” ,用于软件分辨2个数据。

arduino开发stm32f103c8t6,stm32,嵌入式硬件,单片机,ide

(3)Plot处需要设置数据和显示缓存的大小,设置为5000即可,Y轴Scale一定要设置为Auto。如下图。

arduino开发stm32f103c8t6,stm32,嵌入式硬件,单片机,ide

(4)Command这里设置几条测试指令,用于快速发送。如下图,可根据程序理解指令的含义,需要更多的指令可自行添加。

arduino开发stm32f103c8t6,stm32,嵌入式硬件,单片机,ide

(5)Record这里在开始试验前一定要点击Record。

arduino开发stm32f103c8t6,stm32,嵌入式硬件,单片机,ide

(6)log这里可以看到串口收发的实际数据,由于速度很快,停止串口后才能检查。

arduino开发stm32f103c8t6,stm32,嵌入式硬件,单片机,ide

18、如何处理获取的数据?

上面获取的数据,输入为PWM值(最大值65535),输出为0.002s(定时中断的时长)内电机转过的脉冲数。由12中可知PWM与直流电机的等效电压一致,等效电压由决定了电机匀速的最高转速,因此可将PWM值与单位定时中断内产生脉冲数映射,进而可将PWM经过线性变化后转换为目标脉冲数。脉冲数与电机转速是线性关系,在不讲究真实速度的情况下,可将脉冲数视为电机实际的转速。

经过以上线性变换处理,数据存储在CSV文件中,第一列为变换后的目标“速度”(电机稳定时的速度平均值即为目标转速),第二列为实际“转速”。由于后面做反馈控制,需要将输入与反馈做差值,用于喂给PID控制器,因此以上的变换是必须了。

以上数据经过MATLAB的“系统辨识工具箱”处理后,就可以获得系统传递函数,具体可参考:MATLAB 系统辨识 + PID 自动调参_matlab参数辨识代码-CSDN博客

需要提醒下,实验获得的多组数据建议保留1组不用于系统辨识,等辨识完成后用这组数据验证辨识的传递函数准确性(拟合度?)。

19、使用Simulink搭建离线仿真模型

A、搭建开环模型

为了方便下面离线仿真,先将18中的CSV文件,需要将做好的CSV文件增加1列时间戳,间隔为0.002S(定时中断的时长),文件格式如下图:

arduino开发stm32f103c8t6,stm32,嵌入式硬件,单片机,ide

将18中处理好的CSV表格在simulink读入,方法可见18中的参考链接。然后引入传递函数模块,传递函数参数即为18中辨识好的参数。最终搭建的模型如下图:

arduino开发stm32f103c8t6,stm32,嵌入式硬件,单片机,ide

arduino开发stm32f103c8t6,stm32,嵌入式硬件,单片机,ide

其中From Spreadsheet的设置如下:

arduino开发stm32f103c8t6,stm32,嵌入式硬件,单片机,ide

传递函数模块的设置如下:

arduino开发stm32f103c8t6,stm32,嵌入式硬件,单片机,ide

B、搭建闭环模型

在模型中增加1个sum模块,双击后设置正确信号的sign,通过2个增益模块反馈到sum模块输入和输出中形成闭环反馈回路,不断调试增益的大小,运行后观察示波器的信号变化。搭建的模型如下图:

arduino开发stm32f103c8t6,stm32,嵌入式硬件,单片机,ide

C、闭环模型中增加PID控制器

在上面的模型中去掉增益模块、增加PID模块,搭建模型如下:

arduino开发stm32f103c8t6,stm32,嵌入式硬件,单片机,ide

运行后观察示波器里的信号变化,修改PID参数,观察反馈的效果。注意,因为由于底盘的速度有最高值(对应PWM值65535时的最高速度),因此要限制PID输出的幅值,这里设置的是±200,需要根据底盘实际速度值进行设置。

arduino开发stm32f103c8t6,stm32,嵌入式硬件,单片机,ide

arduino开发stm32f103c8t6,stm32,嵌入式硬件,单片机,ide

arduino开发stm32f103c8t6,stm32,嵌入式硬件,单片机,ide

D、使用PID Tuner对PID参数进行调试

双击PID模块,点击“tune…”按钮,设定一定的上升时间和超调量,参考以下链接对PID参数进行调试。

simulink中的PID模块的使用_simulinkpid模块怎么用-CSDN博客

如何在 Simulink 中使用 PID Tuner 进行 PID 调参?-CSDN博客

20、在线闭环PID控制直流有刷电机

A、PID对电机速度控制的意义

通过以上的离线仿真实验可以大概认识到PID控制的好处。相比简单的反馈控制,PID可以有效减少误差,我们可以通过调整比例、积分、微分三个参数来改变系统的反应速度、稳态误差,对于容易震荡的系统,我们还可以通过调整三个参数来抑制震荡的问题。

PID主要分为位置式和增量式,对速度闭环控制而言,增量式PID比较合适;对巡线的位置闭环控制而言,位置式PID更合适。以下是2者的详细解释。

电机控制进阶——PID速度控制 - 知乎 (zhihu.com)

以下的参考链接可以简要说明直流电机PID闭环控制速度的原理:

如何使你的直流电机闭环?(PID讲解)_电机闭环控制_憨猪在度假的博客-CSDN博客s

B、有刷直流电机速度闭环控制的软硬件框架

软件的参考代码件本帖子第10节,4个电机的控制部分是独立的(其实可以将复用的函数合并,为了容易理解,也因为懒惰就不合并了)。每个电机控制部分可分为电机驱动和电机速度反馈2个部分。

       电机驱动是XX_Set_PWM函数管理,用于控制电机驱动板,输入的形参是PWM值。PWM值由PID控制函数XX_Incremental_PI(PID控制器实际上只使用了PI,微分D的贡献太小被省略)产生。电机驱动控制的部分在3号定时中断中执行。

      电机速度反馈是有4个独立的外部中断函数XX_READ_ENCODER_A获取,在1个定时中断周期内不断累加,被读取时会马上清零。注意,此处的外部中断仅使用上升沿或下降沿触发即可,毕竟电机一圈已经有500个脉冲。

       电机硬件的连接件本帖子第7节,其中,每个电机需要stm32提供3个控制引脚(1个PWM引脚,2个数字输出引脚)和2个速度反馈引脚(A\B相信号输入引脚,其中A相的输入引脚要设置为外部中断,4个电机的A相引脚要使用不同的外部中断源,详细的可以参考第6节)。接线时要注意,给A、B相供电要使用可使用5V或3.3V,给驱动板供电只能使用3.3V(因为接线图中接入驱动板的引脚只能耐受3.3V)。

C、PI参数的实际调整实验

第19节中获取的PID参数(我调试出来的D参数过小,已经被忽略)可以直接填入第10节的闭环代码中PID参数。填入后,串口输入的事4个电机的目标速度,输出的是某个电机的当前速度参数。通过实验获取数据并适当处理后,将开环控制、闭环离线仿真和闭环控制的数据进行比较,验证PID控制的效果。

如离线仿真的PID参数不甚理想,可以微调参数逐步调试到满意为止。

也可以自行按照上一周付敏跃教授讲解的PID调试方法,手动调试出合用的PID参数。

参考代码已经在第10节中提供。

21、做一个简单的线控底盘

        前面的内容完整地讲解了如何实现小车的闭环速度控制,但是参考代码只写了前进和后退的内容。为提高底盘控制的适应性,通过适当修改串口指令,通过串口输入两侧轮子的速度值,可实现底盘的停止、拐弯(含原地转向)、前进、倒车等动作。假如你准备用另外的控制器(树莓派、另外的单片机等)控制底盘的运动,可以用此串口指令控制底盘的动作。

         使用的控制串口还是1号串口(PA9、PA10),波特率115200,由6个连续发送的byte组成:

         指令中的第1个字节为帧头,始终为0xFF

         第2个字节为左边2个轮子的速度正反转符号,正转为0x10,反转为0x01

         第3个字节为左边2个轮子的速度值,范围为0x00~0xF0,对应的速度值为0~240pulse每定时中断周期(此处的中断为专门为速度闭环应用的定时中断,周期设定为0.005s,如果定时周期变短,则对应的速度最大允许值会相应变小。最大速度是当PWM最大时的转速脉冲获取值)。

         第4个字节为右边2个轮子的速度正反转符号,正转为0x10,反转为0x01

         第5个字节为右边2个轮子的速度值,范围为0x00~0xF0。

         第6个字节为帧尾,始终为0xFE

         为避免指令传输错漏,对收到数据需要帧头和帧尾的校验,确定帧头和祯尾没问题后才可以将指令解析并将两边轮子的目标速度传递给电机控制函数。

         参考的代码如下(由于调试不多,可能会存在bug,仅供参考)。

        调试时不能使用Arduino的串口助手(因为只能发送字符串,不能发送16进制字节),需要使用其他类型的串口助手,如之前的SerialPlot。文章来源地址https://www.toymoban.com/news/detail-754673.html

//PID速度闭环控制4个电机,使用串口指令控制底盘运动
 
#define LED PC13                    //调试用的LED
 
//以下为左前方电机引脚定义
#define LF_Motor_IN1 PA5            //LF电机使能引脚1
#define LF_Motor_IN2 PA4            //LF电机使能引脚2
#define LF_PWM PA0                  //LF电机调速引脚
#define LF_ENCODER_A PB7            //LF编码器A相引脚
#define LF_ENCODER_B PB6            //LF编码器B相引脚
 
//以下为右前方电机引脚定义
#define RF_Motor_IN1 PB0            //RF电机使能引脚1
#define RF_Motor_IN2 PB1            //RF电机使能引脚2
#define RF_PWM PA2                  //RF电机调速引脚
#define RF_ENCODER_A PB12           //RF编码器A相引脚
#define RF_ENCODER_B PB13           //RF编码器B相引脚
 
//以下为左后方电机引脚定义
#define LR_Motor_IN1 PA7            //LR电机使能引脚1
#define LR_Motor_IN2 PA6            //LR电机使能引脚2
#define LR_PWM PA1                  //LR电机调速引脚
#define LR_ENCODER_A PB8           //LR编码器A相引脚
#define LR_ENCODER_B PB9           //LR编码器B相引脚
 
//以下为右后电机引脚定义
#define RR_Motor_IN1 PC14           //RR电机使能引脚1
#define RR_Motor_IN2 PC15           //RR电机使能引脚2
#define RR_PWM PA3                  //RR电机调速引脚
#define RR_ENCODER_A PB14           //RR编码器A相引脚
#define RR_ENCODER_B PB15           //RR编码器B相引脚
 
volatile int i = 0;                //调试用的公共变量
volatile byte Command[6];          //串口控制指令存储,6个字节
 
volatile int LF_Velocity = 0, LF_Count = 0;     //左前方电机编码器,Count计数变量 Velocity存储设定时间内A相下降沿的个数,与实际转速正相关
volatile int RF_Velocity = 0, RF_Count = 0;     //左后方电机编码器,Count计数变量 Velocity存储设定时间内A相下降沿的个数,与实际转速正相关
volatile int LR_Velocity = 0, LR_Count = 0;     //右前方电机编码器,Count计数变量 Velocity存储设定时间内A相下降沿的个数,与实际转速正相关
volatile int RR_Velocity = 0, RR_Count = 0;     //右后方电机编码器,Count计数变量 Velocity存储设定时间内A相下降沿的个数,与实际转速正相关
 
String Target_Value;             //串口获取的速度字符串变量
volatile int LF_value,RF_value,LR_value,RR_value;   //用于存储通过PI控制器计算得到的用于调整电机转速的PWM值,最大值65535
float KP = 200, KI = 20; //PI参数,此处调整会影响启动电流,低速时可能引起震荡
volatile float LF_Target=0,RF_Target=0,LR_Target=0,RR_Target=0; //电机转速目标值,5ms定时器最大可用范围±280,2ms定时器,最大可用范围±120
 
///*********** 限幅************
//  以下两个参数让输出的PWM在一个合理区间
//  当输出的PWM小于1500时电机不转 所以要设置一个启始PWM
//  STM32单片机的PWM不能超过65535 所以 PWM_Restrict 起到限制上限的作用
//*****************************/
int startPWM = 1500;               //克服死区的启始PWM
int PWM_Restrict = 64000;          //startPW+PWM_Restric=65500<65535
 
/**********外部中断触发计数器函数(4个电机需要独立的外部中断处理函数)************
  根据转速的方向不同我们将计数器累计为正值或者负值(计数器累计为正值为负值为计数器方向)
  只有方向累计正确了才可以实现正确的调整,否则会出现逆方向满速旋转
  ※※※※※※超级重点※※※※※※
  所谓累计在正确的方向即
  (1)计数器方向
  (2)电机输出方向(控制电机转速方向的接线是正着接还是反着接)
  (3)PI 控制器 里面的误差(Basi)运算是目标值减当前值(Target-Encoder),还是当前值减目标值(Encoder-Target)
  三个方向只有对应上才会有效果否则你接上就是使劲的朝着一个方向(一般来说是反方向)满速旋转,出现这种问题,需要将AB相的线调过来,或改下引脚定义
  我例子里是我自己对应好的,如果其他驱动单片机在自己尝试的时候出现满速旋转就是三个方向没对应上
  下列函数中由于在A相上升沿触发时,B相是低电平,和A相下降沿触发时B是高电平是一个方向,在这种触发方式下,我们将count累计为正,另一种情况将count累计为负
********************************************/
void LF_READ_ENCODER_A()     //左前方电机A相中断
{
  if (digitalRead(LF_ENCODER_A) == HIGH)
  {
    if (digitalRead(LF_ENCODER_B) == LOW)
      LF_Count++;  //根据另外一相电平判定方向
    else
      LF_Count--;
  }
  else
  {
    if (digitalRead(LF_ENCODER_B) == LOW)
      LF_Count--; //根据另外一相电平判定方向
    else
      LF_Count++;
  }
}
void RF_READ_ENCODER_A()     //右前方电机A相中断
{
  if (digitalRead(RF_ENCODER_A) == HIGH)
  {
    if (digitalRead(RF_ENCODER_B) == LOW)
      RF_Count++;  //根据另外一相电平判定方向
    else
      RF_Count--;
  }
  else
  {
    if (digitalRead(RF_ENCODER_B) == LOW)
      RF_Count--; //根据另外一相电平判定方向
    else
      RF_Count++;
  }
}
void LR_READ_ENCODER_A()     //左后方电机A相中断
{
  if (digitalRead(LR_ENCODER_A) == HIGH)
  {
    if (digitalRead(LR_ENCODER_B) == LOW)
      LR_Count++;  //根据另外一相电平判定方向
    else
      LR_Count--;
  }
  else
  {
    if (digitalRead(LR_ENCODER_B) == LOW)
      LR_Count--; //根据另外一相电平判定方向
    else
      LR_Count++;
  }
}
 
void RR_READ_ENCODER_A()    //右后方电机A相中断
{
  if (digitalRead(RR_ENCODER_A) == HIGH)
  {
    if (digitalRead(RR_ENCODER_B) == LOW)
      RR_Count++;  //根据另外一相电平判定方向
    else
      RR_Count--;
  }
  else
  {
    if (digitalRead(RR_ENCODER_B) == LOW)
      RR_Count--; //根据另外一相电平判定方向
    else
      RR_Count++;
  }
}
 
/**********定时器中断触发函数(只需要1个定时器)*********/
HardwareTimer timer(3);//声明使用3号定时器
void control()
{ //  cli();     //关闭所有中断,此处尝试不加也行
  //把采用周期(内部定时中断周期)所累计的脉冲下降沿的个数,赋值给速度
  LF_Velocity = LF_Count;  
  RF_Velocity = RF_Count;
  LR_Velocity = LR_Count;
  RR_Velocity = RR_Count;
  
  //脉冲计数器清零
  LF_Count = 0;    
  RF_Count = 0;
  LR_Count = 0;
  RR_Count = 0;
   
//以下为4个电机同时计算PID参数
 LF_value = LF_Incremental_PI(LF_Velocity, LF_Target); //通过目标值和当前值在PID函数下算出我们需要调整用的PWM值
  RF_value = RF_Incremental_PI(RF_Velocity, RF_Target);
  LR_value = LR_Incremental_PI(LR_Velocity, LR_Target);
  RR_value = RR_Incremental_PI(RR_Velocity, RR_Target);
//以下为4个电机同时输出PWM值
  LF_Set_PWM(LF_value);    
  RF_Set_PWM(RF_value);
  LR_Set_PWM(LR_value);
  RR_Set_PWM(RR_value);
 
//以下为调试代码,调试完成需要删除,避免浪费CPU资源 
 Serial1.print(LF_value);//输出左前轮的PWM值
  Serial.print(",");
  Serial1.println(LF_Velocity);//输出左前轮的转速
  
  //  sei();     //打开所有中断,此处尝试不加也行
}
 
/***********PI控制器****************/
int LF_Incremental_PI(int LF_Encoder, float LF_Target1)
{
  static float LF_Bias, LF_MPWM = 0, LF_Last_bias = 0;     //定义全局静态浮点型变量 PWM,Bias(本次偏差),Last_bias(上次偏差)
  LF_Bias = LF_Target1 - LF_Encoder;                       //计算偏差,目标值减去当前值
  LF_MPWM += KP * (LF_Bias - LF_Last_bias) + KI * LF_Bias; //增量式PI控制计算
  if (LF_MPWM > PWM_Restrict)
    LF_MPWM = PWM_Restrict;                                   //限幅
  if (LF_MPWM < -PWM_Restrict)
    LF_MPWM = -PWM_Restrict;                                  //限幅
  LF_Last_bias = LF_Bias;                                     //保存上一次偏差
  return LF_MPWM;                                          //增量输出
}
int RF_Incremental_PI(int RF_Encoder, float RF_Target1)
{
  static float RF_Bias, RF_MPWM = 0, RF_Last_bias = 0;                //定义全局静态浮点型变量 PWM,Bias(本次偏差),Last_bias(上次偏差)
  RF_Bias = RF_Target1 - RF_Encoder;                              //计算偏差,目标值减去当前值
  RF_MPWM += KP * (RF_Bias - RF_Last_bias) + KI * RF_Bias; //增量式PI控制计算
  if (RF_MPWM > PWM_Restrict)
    RF_MPWM = PWM_Restrict;                                   //限幅
  if (RF_MPWM < -PWM_Restrict)
    RF_MPWM = -PWM_Restrict;                                  //限幅
  RF_Last_bias = RF_Bias;                                     //保存上一次偏差
   return RF_MPWM;                                          //增量输出
}
int LR_Incremental_PI(int LR_Encoder, float LR_Target1)
{
  static float LR_Bias, LR_MPWM = 0, LR_Last_bias = 0;                //定义全局静态浮点型变量 PWM,Bias(本次偏差),Last_bias(上次偏差)
  LR_Bias = LR_Target1 - LR_Encoder;                              //计算偏差,目标值减去当前值
  LR_MPWM += KP * (LR_Bias - LR_Last_bias) + KI * LR_Bias; //增量式PI控制计算
  if (LR_MPWM > PWM_Restrict)
    LR_MPWM = PWM_Restrict;                                   //限幅
  if (LR_MPWM < -PWM_Restrict)
    LR_MPWM = -PWM_Restrict;                                  //限幅
  LR_Last_bias = LR_Bias;                                     //保存上一次偏差
   return LR_MPWM;                                          //增量输出
}
int RR_Incremental_PI(int RR_Encoder, float RR_Target1)
{
  static float RR_Bias, RR_MPWM = 0, RR_Last_bias = 0;                //定义全局静态浮点型变量 PWM,Bias(本次偏差),Last_bias(上次偏差)
  RR_Bias = RR_Target1 - RR_Encoder;                              //计算偏差,目标值减去当前值
  RR_MPWM += KP * (RR_Bias - RR_Last_bias) + KI * RR_Bias; //增量式PI控制计算
  if (RR_MPWM > PWM_Restrict)
    RR_MPWM = PWM_Restrict;                                   //限幅
  if (RR_MPWM < -PWM_Restrict)
    RR_MPWM = -PWM_Restrict;                                  //限幅
  RR_Last_bias = RR_Bias;                                     //保存上一次偏差
   return RR_MPWM;                                          //增量输出
}
 
/**********电机驱动函数*********/
void LF_Set_PWM(int LF_motora)
{
  if (LF_motora > 0)  //如果算出的PWM为正
  {
 
    digitalWrite(LF_Motor_IN1, 1);
    digitalWrite(LF_Motor_IN2, 0);
    pwmWrite(LF_PWM, LF_motora + startPWM);  //让PWM在设定正转方向(我们认为的正转方向)正向输出调整,此处的PWM输出函数跟Mega2560不同
  } else if (LF_motora == 0)  //如果PWM为0停车
  {
    digitalWrite(LF_Motor_IN2, 0);
    digitalWrite(LF_Motor_IN1, 0);
  } else if (LF_motora < 0)  //如果算出的PWM为负
  {
 
    digitalWrite(LF_Motor_IN1, 0);
    digitalWrite(LF_Motor_IN2, 1);
    pwmWrite(LF_PWM, -LF_motora + startPWM);  //让PWM在设定反转方向反向输出调整
  }
}
void RF_Set_PWM(int RF_motora)
{
  if (RF_motora > 0)  //如果算出的PWM为正
  {
 
    digitalWrite(RF_Motor_IN1, 1);
    digitalWrite(RF_Motor_IN2, 0);
    pwmWrite(RF_PWM, RF_motora + startPWM);  //让PWM在设定正转方向(我们认为的正转方向)正向输出调整
  } else if (RF_motora == 0)  //如果PWM为0停车
  {
    digitalWrite(RF_Motor_IN2, 0);
    digitalWrite(RF_Motor_IN1, 0);
  } else if (RF_motora < 0)  //如果算出的PWM为负
  {
 
    digitalWrite(RF_Motor_IN1, 0);
    digitalWrite(RF_Motor_IN2, 1);
    pwmWrite(RF_PWM, -RF_motora + startPWM);  //让PWM在设定反转方向反向输出调整
  }
}
void LR_Set_PWM(int LR_motora)
{
  if (LR_motora > 0)  //如果算出的PWM为正
  {
 
    digitalWrite(LR_Motor_IN1, 1);
    digitalWrite(LR_Motor_IN2, 0);
    pwmWrite(LR_PWM, LR_motora + startPWM);  //让PWM在设定正转方向(我们认为的正转方向)正向输出调整
  } else if (LR_motora == 0)  //如果PWM为0停车
  {
    digitalWrite(LR_Motor_IN2, 0);
    digitalWrite(LR_Motor_IN1, 0);
  } else if (LR_motora < 0)  //如果算出的PWM为负
  {
 
    digitalWrite(LR_Motor_IN1, 0);
    digitalWrite(LR_Motor_IN2, 1);
    pwmWrite(LR_PWM, -LR_motora + startPWM);  //让PWM在设定反转方向反向输出调整
  }
}
void RR_Set_PWM(int RR_motora)
{
  if (RR_motora > 0)  //如果算出的PWM为正
  {
 
    digitalWrite(RR_Motor_IN1, 1);
    digitalWrite(RR_Motor_IN2, 0);
    pwmWrite(RR_PWM, RR_motora + startPWM);  //让PWM在设定正转方向(我们认为的正转方向)正向输出调整
  } else if (RR_motora == 0)  //如果PWM为0停车
  {
    digitalWrite(RR_Motor_IN2, 0);
    digitalWrite(RR_Motor_IN1, 0);
  } else if (RR_motora < 0)  //如果算出的PWM为负
  {
 
    digitalWrite(RR_Motor_IN1, 0);
    digitalWrite(RR_Motor_IN2, 1);
    pwmWrite(RR_PWM, -RR_motora + startPWM);  //让PWM在设定反转方向反向输出调整
  }
}
 
void setup()
{
  Serial1.begin(115200);            //打开串口
  Serial1.println("/*****开始驱动*****/");
  delay(1000);
 
  pinMode(LED, OUTPUT);            //调试用的闪烁LED,PC13
  
  pinMode(LF_ENCODER_A, INPUT);    //设置两个相线为输入模式
  pinMode(LF_ENCODER_B, INPUT);
  pinMode(LF_Motor_IN1, OUTPUT_OPEN_DRAIN);  //设置两个驱动引脚为输出模式,由于stm32的引脚接收5V作为输出时需要工作在开漏输出模式下,这与Mega2560函数定义是有差别的,Mega2560可以直接推挽输出5V,引脚直接配置为OUTPUT即可
  pinMode(LF_Motor_IN2, OUTPUT_OPEN_DRAIN);
  pinMode(LF_PWM, PWM_OPEN_DRAIN);
 
  pinMode(RF_ENCODER_A, INPUT);    //设置两个相线为输入模式
  pinMode(RF_ENCODER_B, INPUT);
  pinMode(RF_Motor_IN1, OUTPUT_OPEN_DRAIN);  //设置两个驱动引脚为输出模式,由于stm32的引脚接收5V作为输出时需要工作在开漏输出模式下,这与Mega2560函数定义是有差别的,Mega2560可以直接推挽输出5V,引脚直接配置为OUTPUT即可
  pinMode(RF_Motor_IN2, OUTPUT_OPEN_DRAIN);
  pinMode(RF_PWM, PWM_OPEN_DRAIN);
 
  pinMode(LR_ENCODER_A, INPUT);    //设置两个相线为输入模式
  pinMode(LR_ENCODER_B, INPUT);
  pinMode(LR_Motor_IN1, OUTPUT_OPEN_DRAIN);  //设置两个驱动引脚为输出模式,由于stm32的引脚接收5V作为输出时需要工作在开漏输出模式下,这与Mega2560函数定义是有差别的,Mega2560可以直接推挽输出5V,引脚直接配置为OUTPUT即可
  pinMode(LR_Motor_IN2, OUTPUT_OPEN_DRAIN);
  pinMode(LR_PWM, PWM_OPEN_DRAIN);
 
  pinMode(RR_ENCODER_A, INPUT);    //设置两个相线为输入模式
  pinMode(RR_ENCODER_B, INPUT);
  pinMode(RR_Motor_IN1, OUTPUT_OPEN_DRAIN);  //设置两个驱动引脚为输出模式,由于stm32的引脚接收5V作为输出时需要工作在开漏输出模式下,这与Mega2560函数定义是有差别的,Mega2560可以直接推挽输出5V,引脚直接配置为OUTPUT即可
  pinMode(RR_Motor_IN2, OUTPUT_OPEN_DRAIN);
  pinMode(RR_PWM, PWM_OPEN_DRAIN);
  //下面是外部中断的初始化
  attachInterrupt(LF_ENCODER_A, LF_READ_ENCODER_A, FALLING); //开启对应A相引脚的外部中断,触发方式为FALLING 即下降沿都触发,触发的中断函数为 LF_ENCODER_A
  attachInterrupt(RF_ENCODER_A, RF_READ_ENCODER_A, FALLING);
  attachInterrupt(LR_ENCODER_A, LR_READ_ENCODER_A, FALLING);
  attachInterrupt(RR_ENCODER_A, RR_READ_ENCODER_A, FALLING);
  //下面是定时器的初始化,Mega2560的用法与此处有差异,参考引用库函数才行
  timer.pause();// Pause the timer while we're configuring it
  timer.setPeriod(5000); // Set up period in microseconds,5000us=5ms
  timer.setChannel1Mode(TIMER_OUTPUT_COMPARE);// Set up an interrupt on channel 1
  timer.setCompare(TIMER_CH1, 1);  // Interrupt 1 count after each update
  timer.attachCompare1Interrupt(control);//定时中断函数名声明
  timer.refresh();// Refresh the timer's count, prescale, and overflow
  timer.resume();// Start the timer counting
}
 
void loop()
{
 
///串口指令接收处理函数
 while (Serial1.available() >= 6)       //检测串口是否接收到了不少于6个数据
  {
    Command[0] = Serial1.read();         //获取第一个数据
    while (Command[0]!=0xFF)   //等待帧头为0xFF
    {
    Command[0] = Serial1.read(); //获取串口缓存中的字节数
    i=Serial1.available();
    if(i <= 0)      //超过5个指令依旧不对后退出等待帧头的循环
    break;
    }
    i=Serial1.available();
    if (i>=5)
    {
      Command[1] = Serial1.read();
      Command[2] = Serial1.read();
      Command[3] = Serial1.read();
      Command[4] = Serial1.read();
      Command[5] = Serial1.read();
      if (Command[5] == 0xFE)            //帧尾校验
      {
        if(Command[1]==0x10)               //左轮正转
        {LF_Target=(float)Command[2];LR_Target=LF_Target;}
        if(Command[1]==0x01)               //左轮反转
        {LF_Target=(0-(float)Command[2]);LR_Target=LF_Target;}
        if(Command[3]==0x10)               //右轮正转
        {RF_Target=(float)Command[4];RR_Target=LF_Target;}
        if(Command[3]==0x01)               //右轮反转
        {RF_Target=(0-(float)Command[4]);RR_Target=LF_Target;}
      }
       while(Serial3.read() >= 0){}    //清空串口缓存
    }
    else
    {
      delay(10);  //延时,完成后面的数据接收(不超过10个字节的时间)
       i=Serial1.available(); 
       if (i>=5)
     {
      Command[1] = Serial1.read();
      Command[2] = Serial1.read();
      Command[3] = Serial1.read();
      Command[4] = Serial1.read();
      Command[5] = Serial1.read();
      if (Command[5] == 0xFE)            //帧尾校验
       {
        if(Command[1]==0x10)               //左轮正转
        {LF_Target=(float)Command[2];LR_Target=LF_Target;}
        if(Command[1]==0x01)               //左轮反转
        {LF_Target=(0-(float)Command[2]);LR_Target=LF_Target;}
        if(Command[3]==0x10)               //右轮正转
        {RF_Target=(float)Command[4];RR_Target=LF_Target;}
        if(Command[3]==0x01)               //右轮反转
        {RF_Target=(0-(float)Command[4]);RR_Target=LF_Target;}
       }
     } 
      while(Serial3.read() >= 0){}      //清空串口缓存
    }
    
  }
 
}

到了这里,关于基于STM32F103C8T6使用Arduino IDE编程闭环控制4个带编码器的有刷直流电机的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 基于STM32F103C8T6的超声波测距应用

    #一、超声波HC_SR04简介 #二、超声波工作原理 #三、超声波测距步骤 #四、硬件接线 #五、项目代码 一、超声波HC_SR04简介 超声波传感器模块上面通常有两个超声波元器件,一个用于发射,一个用于接收。电路板上有四个引脚:VCC、GND、Trig(触发)、Echo(回应) 工作电压与电流

    2024年02月03日
    浏览(43)
  • STM32f103c8t6模板的搭建-基于正点例程

           笔者认为正点编写的官方例程结构较为整洁,可以便于后期的例程开发,如果开发者对于项目开发中芯片要求较高的话,有很多人会选择正点的开发板,但是通常大多数是stm32初学者会选择用价格更为便宜的c8t6来进行学习,而正点选用的教程开发板大多都是些RC、ZE、

    2024年02月06日
    浏览(66)
  • 基于STM32F103C8T6ADC检测交流电压

    上篇文章写了硬件部分的实现思路,通过采样电阻的到小电压后经过二级放大电路得到单片机可处理的交流电压,此文介绍了如何采用单片机采集交流电压以及stm32ADC外设的使用。首先是硬件电路部分。  电路没有采用核心板,而是直接将芯片焊接到主板上,采用type-c接口供

    2024年02月12日
    浏览(53)
  • 基于STM32F103C8T6的HC-06蓝牙通信

    如果朋友们 遇到了如下问题 ,可以仔细借鉴本文章和另一篇专门讲解 蓝牙通信问题 的文章,一定能够解决你在蓝牙通信时遇到的诸多困难 1.在调试蓝牙模块AT指令时无返回值 2.身边 无USB转TTL模块 可以直接调试蓝牙模块(本人就是由于无模块花了了整整一天才调试成功)

    2024年02月03日
    浏览(60)
  • 基于stm32f103c8t6的定时器详解(持续更新)

    先声明:stm32f103c8t6中没有基本定时器、只有TIM1-TIM4:分别是高级定时器和通用定时器(对照下图请自行阅读stm32f103x的datasheet) 1、定时器功能:定时、输出比较、输入捕获、互补输出,其中基本定时器只有定时功能、通用定时器只没有互补输出功能、高级定时器具有所有功能

    2023年04月24日
    浏览(65)
  • HX711压力传感器(基于STM32F103C8T6)

    HX711模块是我们目前比较常见的压力传感器模块,主要的作用是用来做压力检测,重量监测等等。博主的这篇博文主要实现功能为,在对重量或者压力进行监测的同时,可以累加或者清零数值,在此基础上就可以对比如饮水量进行统计等等。 HX711模块是市面上比较常见的模块

    2024年02月11日
    浏览(53)
  • [STM32F103C8T6]基于stm32的循迹,跟随,避障智能小车

    目录 1.小车驱动主要是通过L9110S模块来驱动电机 motor.c 2.我们可以加入串口控制电机驱动(重写串口接收回调函数,和重定向printf) Uart.c main.c  3.点动功能 uart.c main.c 为什么使用的是HAL_Delay()要设置滴答定时器的中断优先级呢? 4.小车PWM调速,  6.跟随功能 7.避障功能 超声波测距

    2024年02月13日
    浏览(54)
  • 基于STM32F103C8T6的UAV飞控板硬件设计

    一、主控单元:         主控单元基于意法半导体公司的STM32F103C8T6单片机进行设计。STM32F103C8T6DE 内核为ARM Cortex-M3;最大主频:72MHz ;工作电压范围:2V~3.6V ;程序存储容量:64KB; 程序存储器类型:FLASH ;RAM总容量:20KB; GPIO端口数量:37 ;封装为LQFP-48;串行单线调试(

    2024年02月08日
    浏览(54)
  • 基于stm32f103c8t6及AS608-----指纹锁项目

              博主纯小白, 本文适合于初学者,大佬还请勿喷,欢迎提出意见,有纰漏之处将及时纠正。 在浅学了stmf103c8t6后,想着依据现在所拥有的知识和能力做一个小项目。 注:工程代码在文章末尾。 掌握C语言基础....这个最基础啦... 接触过类似单片机,稍微看得懂芯片

    2023年04月09日
    浏览(81)
  • 功耗测评 | STM32F103C8T6

    STM32F103C8T6 MCU越来越广泛的应用在生产生活的各个领域,外接丰富的传感器、功能模块、通信模块、显示存储等可以形成各种可样的产品项目应用。对于功耗要求比较高的产品,一般会选择STM32L系列的MCU,但是从功耗的评测角度,逻辑上是基本相似的。 在很多应用场合中都对

    2024年02月07日
    浏览(40)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包