本文主要是介绍基于STM32F103C8T6使用Arduino IDE编程闭环控制4个带编码器的有刷直流电机,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
题记:标题有点长了,纯粹为了方便被检索到~~~本贴主要用于支持南方科技大学SDIM学院工业设计专业大三综合项目移动底盘学习,也是我自己按照费曼学习方法的一次尝试,用从底层搭建一个机器人底盘来学习自动控制原理。
由于工业设计专业没有开设嵌入式课程,多数同学不具备使用Keil或STM32CubeIDE的基础。鉴于Arduino开发的友好性(主要是参考资料多),特使用支持Arduino环境的STM32F103C8T6作为底盘控制核心。已经会使用stm32单片机的同学推荐直接使用官方推荐的编程方式,Arduino的性能和资源丰富性确实不如CubeIDE。
言归正转,以下是一些项目设计解读:
1、底盘长什么样子?
有人问模型能给吗?当然可以,上图的模型必须给到位:
链接:https://pan.baidu.com/s/1U_TS00JWqtHS8pN8qT-5gA?pwd=l52p
提取码:l52p
————有关的电子资料会及时更新在这个位置。
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电机的一些性能参数和选型参考:
需要额外指出的是,本帖使用的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
5、底盘如何设计?
加上几根型材、拧几个螺丝即可。: )SW的模型可以提供,如选用36的电机可以自行根据商家图纸简易绘制。
6、STM32F103控制板长什么样子?
图上的板子是常见的样子,价格含运费不高于15元,比UNO还要便宜!!本项目中建议买不焊接排针的C8T6版本,参考链接如下:
STM32F103C8T6最小系统板C6T6STM32单片机开发板核心板板江协科技-tmall.com天猫https://detail.tmall.com/item.htm?abbucket=6&id=739127102803&ns=1&skuId=5097382116547&spm=a21n57.1.0.0.5583523cqTJBZI板子接口图如下,要是能找到更清楚的再更新:
图中可以看到引脚存在复用情况,要参考以下链接根据物理引脚的实际需求选用合适的引脚,比如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放一点参考链接的截图:
7、底盘怎么接线?
所用电机驱动板是兼容3.3V驱动的,随便接合适的引脚即可。用于电机转角反馈的GMR编码器理论上也是兼容的5V/3.3V。不过考虑信号线挺长的,还是给编码器用了5V电源,因此A/B相的引脚就只能选择5V兼容引脚。此外还要预留出串口通讯和USB下载引脚,PC13用于LED指示也需要预留。
借用上面的接口图修改的图如下,其中LF指左前方电机、LR指左后方电机、RF指有前方电机、RR指右后方电机。
驱动板和电机之间接线参考下图:
8、电源怎么设计?
由于电机工作在24V下,建议采用DJI的 TB48S/TB47S航模电池(主要是有现成的)。
9、如何使用Arduino IDE给STM32F103开发板下载代码?
有3种方式可以给开发板下载代码,分别是通过USB接口(PA11、PA12)、UART1接口(PA9、PA10)和默认的SWD接口。
USB接口可参考以下链接配置,优点是不用管BOOT跳线位置,使用体验约等于UNO,也不需要额外的硬件(其实下载bin文件还是需要USB转串口模块)。此种方法只能使用Arduino1.8的版本,新版本会报错。另外一般最高频率只能到72MHz,在本项目中足够使用。
STM32F103C8T6在Arduino IDE里编程_stm32f103c8t6 烧录米思齐bin文件-CSDN博客https://blog.csdn.net/bobo184/article/details/84349184 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 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,接线图参考上面。
以下的代码使用串口控制电机的速度,速度为闭环控制。此代码不能用于系统参数辨识,辨识的代码会在后面提供。另外需要注意:如果迁移到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 PB11 //LR编码器A相引脚
#define LR_ENCODER_B PB10 //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++; //根据另外一相电平判定方向elseLF_Count--;}else{if (digitalRead(LF_ENCODER_B) == LOW)LF_Count--; //根据另外一相电平判定方向elseLF_Count++;}
}
void RF_READ_ENCODER_A() //右前方电机A相中断
{if (digitalRead(RF_ENCODER_A) == HIGH){if (digitalRead(RF_ENCODER_B) == LOW)RF_Count++; //根据另外一相电平判定方向elseRF_Count--;}else{if (digitalRead(RF_ENCODER_B) == LOW)RF_Count--; //根据另外一相电平判定方向elseRF_Count++;}
}
void LR_READ_ENCODER_A() //左后方电机A相中断
{if (digitalRead(LR_ENCODER_A) == HIGH){if (digitalRead(LR_ENCODER_B) == LOW)LR_Count++; //根据另外一相电平判定方向elseLR_Count--;}else{if (digitalRead(LR_ENCODER_B) == LOW)LR_Count--; //根据另外一相电平判定方向elseLR_Count++;}
}void RR_READ_ENCODER_A() //右后方电机A相中断
{if (digitalRead(RR_ENCODER_A) == HIGH){if (digitalRead(RR_ENCODER_B) == LOW)RR_Count++; //根据另外一相电平判定方向elseRR_Count--;}else{if (digitalRead(RR_ENCODER_B) == LOW)RR_Count--; //根据另外一相电平判定方向elseRR_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,PC13pinMode(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_AattachInterrupt(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 ittimer.setPeriod(5000); Set up period in microseconds,5000us=5mstimer.setChannel1Mode(TIMER_OUTPUT_COMPARE);// Set up an interrupt on channel 1timer.setCompare(TIMER_CH1, 1); // Interrupt 1 count after each updatetimer.attachCompare1Interrupt(control);//定时中断函数名声明timer.refresh();// Refresh the timer's count, prescale, and overflowtimer.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。直流有刷电机的等效模型如下:
其中,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中对此问题的建模过程,结论类似:
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 PB11 //LR编码器A相引脚
#define LR_ENCODER_B PB10 //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++; //根据另外一相电平判定方向elseLF_Count--;}else{if (digitalRead(LF_ENCODER_B) == LOW)LF_Count--; //根据另外一相电平判定方向elseLF_Count++;}
}
void RF_READ_ENCODER_A() //右前方电机A相中断
{if (digitalRead(RF_ENCODER_A) == HIGH){if (digitalRead(RF_ENCODER_B) == LOW)RF_Count++; //根据另外一相电平判定方向elseRF_Count--;}else{if (digitalRead(RF_ENCODER_B) == LOW)RF_Count--; //根据另外一相电平判定方向elseRF_Count++;}
}
void LR_READ_ENCODER_A() //左后方电机A相中断
{if (digitalRead(LR_ENCODER_A) == HIGH){if (digitalRead(LR_ENCODER_B) == LOW)LR_Count++; //根据另外一相电平判定方向elseLR_Count--;}else{if (digitalRead(LR_ENCODER_B) == LOW)LR_Count--; //根据另外一相电平判定方向elseLR_Count++;}
}void RR_READ_ENCODER_A() //右后方电机A相中断
{if (digitalRead(RR_ENCODER_A) == HIGH){if (digitalRead(RR_ENCODER_B) == LOW)RR_Count++; //根据另外一相电平判定方向elseRR_Count--;}else{if (digitalRead(RR_ENCODER_B) == LOW)RR_Count--; //根据另外一相电平判定方向elseRR_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,PC13pinMode(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_AattachInterrupt(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 ittimer.setPeriod(5000); Set up period in microseconds,5000us=5mstimer.setChannel1Mode(TIMER_OUTPUT_COMPARE);// Set up an interrupt on channel 1timer.setCompare(TIMER_CH1, 1); // Interrupt 1 count after each updatetimer.attachCompare1Interrupt(control);//定时中断函数名声明timer.refresh();// Refresh the timer's count, prescale, and overflowtimer.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); //启动后延时运行2si=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天猫
相关的资料见: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'。
(2)在Data Format处选择‘ASCII’,因为控制器使用的是printf函数,发出来的是字符串。通道数选‘2’,因为需要绘制输入和输出2个数据曲线。分隔符用 “,” ,用于软件分辨2个数据。
(3)Plot处需要设置数据和显示缓存的大小,设置为5000即可,Y轴Scale一定要设置为Auto。如下图。
(4)Command这里设置几条测试指令,用于快速发送。如下图,可根据程序理解指令的含义,需要更多的指令可自行添加。
(5)Record这里在开始试验前一定要点击Record。
(6)log这里可以看到串口收发的实际数据,由于速度很快,停止串口后才能检查。
这篇关于基于STM32F103C8T6使用Arduino IDE编程闭环控制4个带编码器的有刷直流电机的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!