【HUST狼牙实验室梯队学习项目】第一节 底盘搭建
【HUST狼牙实验室梯队学习项目】第一节 底盘搭建
- 主控选择
- 电机控制部分
- PWM调波
- 定时器
- A4950电机驱动
- PID闭环控制
- 串口通信
- 串口通信配置:
- 串口接收
- 串口发送
主控选择
该项目选择的是stm32f103rct6的主控
电机控制部分
本项目采用的是减速比1:30、带霍尔编码器的直流减速电机,实测中这个减速比的电机可以做到扭矩和转速的平衡,且控制简单,易于上手。霍尔编码盘编码稳定度较光电编码器高,且价格低廉,用于轮式里程计中精度可以达到预期
PWM调波
定时器
电机的PWM调波使用了TIM8定时器的四个通道CH1,CH2,CH3,CH4,对应的GPIO引脚分别是PC6,PC7,PC8,PC9
定时器初始化函数如下,其中arr为自动重装寄存器的值,psc为预分频值,72Mhz/(arr+1)/(psc+1)为分频值
void TIM8_PWM_Init(uint16_t arr,uint16_t psc) //
{GPIO_InitTypeDef GPIO_InitStructure;TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;TIM_OCInitTypeDef TIM_OCInitStructure;RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM8, ENABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);// GPIO_PinRemapConfig(GPIO_PartialRemap_TIM3, ENABLE);GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8; //TIM8_CH3GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽,GPIO输出来自于定时器GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOC, &GPIO_InitStructure);GPIO_InitStructure.GPIO_Pin=GPIO_Pin_9; //TIM8_CH4GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_Init(GPIOC, &GPIO_InitStructure);GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; //TIM8_CH1GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOC, &GPIO_InitStructure);GPIO_InitStructure.GPIO_Pin=GPIO_Pin_7; //TIM8_CH2GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_Init(GPIOC, &GPIO_InitStructure);TIM_TimeBaseStructure.TIM_Period = arr;//设置自动重装载寄存器周期的值TIM_TimeBaseStructure.TIM_Prescaler =psc;//设置用来作为TIMx时钟频率除数的预分频值TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;//时钟分频TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;//向上计数TIM_TimeBaseInit(TIM8, &TIM_TimeBaseStructure);TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; //选择定时器模式:TIM脉冲宽度调制模式1TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //比较输出使能TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; //输出极性:TIM输出比较极性高TIM_OC3Init(TIM8, &TIM_OCInitStructure); TIM_OC3PreloadConfig(TIM8, TIM_OCPreload_Enable); //在写入ccr值后才开始输出pwmTIM_CtrlPWMOutputs(TIM8,ENABLE);TIM_OC4Init(TIM8, &TIM_OCInitStructure); TIM_OC4PreloadConfig(TIM8, TIM_OCPreload_Enable); //在写入ccr值后才开始输出pwmTIM_CtrlPWMOutputs(TIM8,ENABLE);TIM_OC1Init(TIM8, &TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM8, TIM_OCPreload_Enable); //在写入ccr值后才开始输出pwmTIM_CtrlPWMOutputs(TIM8,ENABLE);TIM_OC2Init(TIM8, &TIM_OCInitStructure); TIM_OC2PreloadConfig(TIM8, TIM_OCPreload_Enable); //在写入ccr值后才开始输出pwmTIM_CtrlPWMOutputs(TIM8,ENABLE);TIM_Cmd(TIM8, ENABLE);
}
A4950电机驱动
该电机驱动PWM频率不宜超过1khz,故arr=7200-1,psc=10-1
经过测试,Forward/Reverse Slow Decay在PID闭环时阻尼较大,不容易振荡,故采用该模式进行电机驱动;为了实现电机的反转等操作,两个电机的两个通道都要配置为PWM复用推挽输出,对定时器资源消耗较大。
PWM的配置被封装成了四个函数,使用时可以直接调用
void PWM_Forward_1(uint16_t goal_speed)
{PWM_State_1=1;Goal_speed_1=goal_speed;TIM_SetCompare1(TIM8,7200-1);TIM_SetCompare2(TIM8,7160-17*goal_speed); //开环的近似线性函数,便于PID闭环的收敛pwm_space_1=7160-17*goal_speed;}void PWM_Backward_1(uint16_t goal_speed)
{PWM_State_1=-1;Goal_speed_1=goal_speed;TIM_SetCompare2(TIM8,7200-1);TIM_SetCompare1(TIM8,7160-17*goal_speed);pwm_space_1=7160-17*goal_speed;}void PWM_Forward_2(uint16_t goal_speed)
{PWM_State_2=1;Goal_speed_2=goal_speed;TIM_SetCompare3(TIM8,7200-1);TIM_SetCompare4(TIM8,7160-17*goal_speed);pwm_space_2=7160-17*goal_speed;
}void PWM_Backward_2(uint16_t goal_speed)
{PWM_State_2=-1;Goal_speed_2=goal_speed;TIM_SetCompare4(TIM8,7200-1);TIM_SetCompare3(TIM8,7160-17*goal_speed);pwm_space_2=7160-17*goal_speed;
}
PID闭环控制
导航小车需要对速度控制较为准确,便于上位机进行导航,所以使用PID闭环控制,使用TIM2和TIM4的CH1和CH2编码器接口对电机进行测速,再进行PID。
测速配置:
void Encoder_Init()
{GPIO_InitTypeDef GPIO_InitStructure;RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA , ENABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB , ENABLE);GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; //TIM2_CH1GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA, &GPIO_InitStructure);GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1; //TIM2_CH2GPIO_Init(GPIOA, &GPIO_InitStructure);GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; //TIM4_CH1GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOB, &GPIO_InitStructure);GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7; //TIM4_CH2GPIO_Init(GPIOB, &GPIO_InitStructure);TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;TIM_TimeBaseStructure.TIM_Period = 65536-1;//设置自动重装载寄存器周期的值TIM_TimeBaseStructure.TIM_Prescaler =1-1;//设置用来作为TIMx时钟频率除数的预分频值TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;//时钟分频TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;//向上计数TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);TIM_TimeBaseInit(TIM4, &TIM_TimeBaseStructure);TIM_ICInitTypeDef TIM_ICInitStructure;TIM_ICStructInit(&TIM_ICInitStructure);TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;//TIM通道1TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;//输入上升沿捕获;TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; // 每次边缘事件捕获一次;TIM_ICInitStructure.TIM_ICFilter = 0x0F; //指定输入捕获滤波器的值TIM_ICInit(TIM2,&TIM_ICInitStructure);TIM_ICInit(TIM4,&TIM_ICInitStructure);TIM_ICInitStructure.TIM_Channel = TIM_Channel_2;//TIM通道1TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;//输入上升沿捕获;TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; // 每次边缘事件捕获一次;TIM_ICInitStructure.TIM_ICFilter = 0x0F; //指定输入捕获滤波器的值TIM_ICInit(TIM2,&TIM_ICInitStructure);TIM_ICInit(TIM4,&TIM_ICInitStructure);TIM_EncoderInterfaceConfig(TIM2,TIM_EncoderMode_TI12,TIM_ICPolarity_Rising,TIM_ICPolarity_Rising);TIM_EncoderInterfaceConfig(TIM4,TIM_EncoderMode_TI12,TIM_ICPolarity_Rising,TIM_ICPolarity_Rising);TIM_Cmd(TIM2,ENABLE);TIM_Cmd(TIM4,ENABLE);
}
测速需要每隔0.05s读取一次计数器数据,读取频率太高测速精度低,读取频率过低则容易产生振荡
PID部分在使用团队开源代码的基础上略有改动
//PID.c
#define ABS(x) ((x)>0? (x):(-(x)))
#define LIMIT_MAX_MIN(x, max, min) (((x) <= (min)) ? (min):(((x) >= (max)) ? (max) : (x)))/**********************************************************************************************************
*函 数 名: PID_Calc
*功能说明: PID+各种优化
*形 参: PID_Struct *P PID参数结构体* ActualValue PID计算反馈量
*返 回 值: PID反馈计算输出值
**********************************************************************************************************/void PID_Init(Pid_Typedef* Pid_speed, float SetSpeed, float P, float I, float D)
{Pid_speed->P = P;Pid_speed->I = I;Pid_speed->D = D;Pid_speed->IMax = 20; //推荐误差的最大值*10Pid_speed->SetPoint = SetSpeed;Pid_speed->OutMax = 1000.0f; //输出最大值
}float PID_Calc(Pid_Typedef *P, float ActualValue, float goal_speed)
{P->SetPoint = goal_speed;P->ActValue=(ABS(ActualValue)> P->DeadZone)?ActualValue:0; P->PreError = P->SetPoint - P->ActValue;P->dError = P->PreError - P->LastError;P->SetPointLast = P->SetPoint;if(P->PreError > -P->ErrorMax && P->PreError < P->ErrorMax){P->SumError += P->PreError;}P->LastError = P->PreError;if(P->SumError >= P->IMax)P->SumError = P->IMax;else if(P->SumError <= -P->IMax)P->SumError = -P->IMax;P->POut = P->P * P->PreError;P->IOut = P->I * P->SumError;P->DOut = P->D * P->dError;return LIMIT_MAX_MIN(P->POut+P->IOut+P->DOut,P->OutMax,-P->OutMax);
}
//PID.h
typedef struct PID{float SetPoint; //设定目标值float SetPointLast;float ActValue; //实际值float DeadZone; //设置死区float P; //比例常数float I; //积分常数float D; //微分常数float LastError; //前次误差float PreError; //当前误差float SumError; //积分误差float dError;float ErrorMax; //偏差上限 超出偏差则不计算积分作用float IMax; //积分限制float POut; //比例输出float IOut; //积分输出float DOut; //微分输出float OutMax; //限幅
}Pid_Typedef;float PID_Calc(Pid_Typedef *P, float ActualValue, float goal_speed);
void PID_Init(Pid_Typedef* Pid_speed, float SetSpeed, float P, float I, float D);
整定PID参数时,用Jlink连接主控板,打开Jscope监测测速曲线,调到略有振荡且没有静差即可,最终电机可以在0.2s内收敛到稳定状态
串口通信
底盘需要接受上位机发送的速度控制指令,以及上报编码器测速数据,就要与上位机进行串口通信。我这里使用USB转TTL实现上位机与stm32主控的通信,一方面这样可以有效保护上位机的GPIO口,另一方面USB转TTL集成的LED指示可以便于调试
串口通信配置:
//使用空闲中断+DMA实现对定长数据的高效接收
void Serial_init()
{MyDMA_Init();RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE); //开启APB2外设时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP; //复用推挽输出GPIO_InitStructure.GPIO_Pin=GPIO_Pin_9; //A9作为发送端GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;GPIO_Init(GPIOA,&GPIO_InitStructure);GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU; //上拉输入GPIO_InitStructure.GPIO_Pin=GPIO_Pin_10; //A10作为接收端GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;GPIO_Init(GPIOA,&GPIO_InitStructure);USART_InitTypeDef USART_InitStructure; //初始化USART1USART_InitStructure.USART_BaudRate=115200; //设置波特率USART_InitStructure.USART_HardwareFlowControl=USART_HardwareFlowControl_None;USART_InitStructure.USART_Mode=USART_Mode_Tx|USART_Mode_Rx; //Tx发送,Rx接收USART_InitStructure.USART_Parity=USART_Parity_No; //检验位USART_InitStructure.USART_StopBits=USART_StopBits_1; //停止位USART_InitStructure.USART_WordLength=USART_WordLength_8b; //发送字长为8bit,无校验位USART_Init(USART1,&USART_InitStructure);USART_ITConfig(USART1,USART_IT_IDLE,ENABLE); //配置空闲中断NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //优先级分组方式:第三类分组NVIC_InitTypeDef NVIC_InitStructure; //配置NVICNVIC_InitStructure.NVIC_IRQChannel=USART1_IRQn; //配置NVIC中断channelNVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1; //抢占优先级NVIC_InitStructure.NVIC_IRQChannelSubPriority=1; //响应优先级NVIC_Init(&NVIC_InitStructure);USART_Cmd(USART1,ENABLE);USART_DMACmd(USART1,USART_DMAReq_Rx,ENABLE);
}void Serial_SendByte(uint8_t _Byte)
{USART_SendData(USART1,_Byte);while(USART_GetFlagStatus(USART1,USART_FLAG_TXE)==RESET);
}void Serial_SendString(char* _string)
{while(*_string){Serial_SendByte((uint8_t)*(_string++));}
}
extern char Serial_RxPacket[15];void MyDMA_Init()//转运数据起始地址
{RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);DMA_InitTypeDef DMA_InitStructure;DMA_InitStructure.DMA_BufferSize=14;DMA_InitStructure.DMA_DIR=DMA_DIR_PeripheralSRC; //从外设寄存器转运到SRAMDMA_InitStructure.DMA_M2M=DMA_M2M_Disable;DMA_InitStructure.DMA_MemoryBaseAddr=(uint32_t)Serial_RxPacket; //接受缓冲地址DMA_InitStructure.DMA_MemoryDataSize=DMA_MemoryDataSize_Byte;DMA_InitStructure.DMA_MemoryInc=DMA_MemoryInc_Enable;DMA_InitStructure.DMA_Mode=DMA_Mode_Circular;DMA_InitStructure.DMA_PeripheralBaseAddr=(uint32_t)&(USART1->DR); //转运数据起始地址DMA_InitStructure.DMA_PeripheralDataSize=DMA_PeripheralDataSize_Byte; //八位数据宽度DMA_InitStructure.DMA_PeripheralInc=DMA_PeripheralInc_Disable; //地址是否自增DMA_InitStructure.DMA_Priority=DMA_Priority_Medium;DMA_Init(DMA1_Channel5,&DMA_InitStructure);DMA_Cmd(DMA1_Channel5,ENABLE);
}
串口接收
void USART1_IRQHandler(void) //解析上位机指令,结构为L:(+/-)X.XXR:(+/-)X.XX
{if(USART_GetFlagStatus(USART1,USART_FLAG_IDLE)==SET){DMA_Cmd(DMA1_Channel5,DISABLE); (void)USART1->SR; (void)USART1->DR;DMA_SetCurrDataCounter(DMA1_Channel5,14);int i=0;if(Serial_RxPacket[0]=='L'){Goal_speed_1_temp=Serial_RxPacket[3]-'0';Goal_speed_1_temp=Goal_speed_1_temp+(Serial_RxPacket[5]-'0')*0.1;Goal_speed_1_temp=Goal_speed_1_temp+(Serial_RxPacket[6]-'0')*0.01;}elseGoal_speed_1_temp=0;if(Serial_RxPacket[2]=='-'){//Goal_speed_1_temp=Goal_speed_1_temp*(-1);PWM_Backward_1(360*Goal_speed_1_temp);}else if(Serial_RxPacket[2]=='+'){PWM_Forward_1(360*Goal_speed_1_temp);}//Goal_speed_1=Goal_speed_1_temp;//OLED_ShowSignedNum(1,1,(int)(100*Goal_speed_1_temp),5);if(Serial_RxPacket[7]=='R'){Goal_speed_2_temp=Serial_RxPacket[10]-'0';Goal_speed_2_temp=Goal_speed_2_temp+(Serial_RxPacket[12]-'0')*0.1;Goal_speed_2_temp=Goal_speed_2_temp+(Serial_RxPacket[13]-'0')*0.01;}if(Serial_RxPacket[9]=='-'){PWM_Backward_2(360*Goal_speed_2_temp);}else if(Serial_RxPacket[9]=='+'){PWM_Forward_2(360*Goal_speed_2_temp);}//OLED_ShowSignedNum(2,1,(int)(100*Goal_speed_2_temp),5);//Goal_speed_2=Goal_speed_2_temp;DMA_Cmd(DMA1_Channel5,ENABLE);}}
串口发送
char Tx[20]="L:+000R:+000\r\n\0";Serial_SendString(Tx);
Tx内容在编码器测速时确定,含义是单位时间(0.05s)内编码器脉冲数。主控机除PID外不进行数值运算
【HUST狼牙实验室梯队学习项目】第一节 底盘搭建相关推荐
- MySQL入门学习的第一节(SQL语句)
MySQL入门学习的第一节(SQL语句) SQL语句
- Angular学习笔记第一节 基本概念
1.Let do it! ####1.ng的基础概念 在学习ng之前,我们只需要掌握HTML.CSS.JS即可. 简称ng.名字不错,吊! 重要的特性 有了ng,我们就可以轻松的构建SPA应用,而且n ...
- 零基础学习SQL第一节
零基础学习SQL第一大节 第一节:数据库与SQL 我们身边的数据库 大家都有过下面这样的经历吧? 收到曾经为自己诊治过的牙医寄来的明信片,上面写着"距上次检查已有半年,请您再来做个牙齿健康查 ...
- CCSA学习笔记 第一节 思科安全解决方案综述
CCSA学习笔记 CCSA第二期:01思科安全解决方案综述 CCSA安全学习的目标 CCSA第二期:01思科安全解决方案综述 CCSA安全学习的目标 1.能够对思科安全的网络安全解决方案有初步认识 2 ...
- 新手零基础学习Python第一步,搭建开发环境!
如何在电脑上面搭建Python开发环境?本文会解答这个问题. Python是一门计算机编程语言,通过给计算机下达精确的指令以完成相应的任务或者事情. 人类掌握Python后,就可以利用这个工具告诉计算 ...
- 《左耳听风》学习体验第一节
就在前几天,朋友圈得技术大佬们不约而同得转发一篇公众号推文,大意就是知名程序大拿,陈浩因心梗去世.我就在想,这个陈浩是谁,搜索了一下,他的文章.发现他在某app上拥有自己的付费专栏,注册app送7天会 ...
- maven学习笔记第一节一-maven install 模块之间相互引用
为什么80%的码农都做不了架构师?>>> 我们再做项目的时候,有很多模块是可以重复使用的,maven提供了很好的解决模块之间相互引用的方法,具体流程如下: 1.建立共用模块 2 ...
- c++基础学习 (第一节课)
一.new和delete 一.介绍 new:关键字,申请内存 delete:关键字,释放内存 二.使用和特点 一.new的使用 /*new的使用*///申请内存//1事情单个内存(一个自己所需要的大小 ...
- AutoHotKey学习总结第一节:AHK基本语法
AHK需要安装AHK环境,编写AHK脚本完成快捷键设定或者自动化工作.AHK我个人觉得是一个挺方便简单的语言,写一些好用的小工具或者挂机脚本不错. 一.基本语法 重映射 OriginKey::Dest ...
最新文章
- 对硕士而言,编制和稳定究竟有多重要?
- 我热爱计算机作文500字,电脑吸引了我
- Struts 2的基石——拦截器(Interceptor)
- **设计模式中的常用原则
- centos7虚拟机开启端口后 外部不能访问的问题
- gzip算法源代码 - tankzhouqiang - 博客园
- U盘的用法用途与维护
- 养成10个优秀的习惯
- IOS工作笔记001---windows下安装通过VmWare来安装IOS系统_并连接上网_来吧超级详细
- zookeeper 3.6.0安装以及基本使用
- linux tick异常变化,linux tickGet()
- 如何查看Windows 10系统版本号?
- GPO 安装 .net 4.5和WMF4
- Java 设计模式 之 代理模式(Proxy)
- php中好看的对话框面板,有关对话框的课程推荐10篇
- 微信自动回复 html 点击文字,【微信开发】公众号自动回复文字和图文链接(示例代码)...
- ZBT的计算几何模板
- 菜鸟应用发布 全民跨入APP2.0时代
- python求圆周率马青公式_Python 实现丘德诺夫斯基(Chudnovsky)法計算高精度圓周率...
- 自律·财大自习·Java
热门文章
- (符号数)二进制乘法(从补码讲起)
- deluge webui不能添加部分torrent种子解决记录
- BFD库的使用介绍 nm工具源码分析
- pandas 转换为文本类型_Pandas对文本数据处理
- 速汇金:蚂蚁金服收购被否,却与瑞波达成合作
- ajax实现搜索提示源码,Jquery实现搜索框提示功能示例代码
- 计算机及应用自考考研学校,【王道论坛统计】2010 34所自主划线院校计算机复试线及相关详细报考信息(感谢gumuguo)...
- C++ char类型转string类型的两种方法
- 草,电脑BUG如此之多,元芳你怎么看?
- 国庆被困校园想出去想疯了_被迫抓包学校微信小程序