故事发生在一个月黑风高的夜晚,男主正在焦急的用外部中断模式读取电机编码器输出的脉冲个数并进行处理,企图用这种方式求得电机的转速和转过的角位置。终于他用这种最直接的方式赢得了女主的芳心,他们在一起了!故事本该是两人从此过上了幸福的生活(撒花~~),然而五天后另一个男人出现了,他并不健谈长得也不是很帅,但是他包装的很好,男主做了三天才成功解决的问题,他只用一句话就解决了!我们暂且称他为编码男,编码男果然名不虚传,很快男主就变成了绿灯侠,自此他又开始孤单一个人苦逼敲代码了…(真实故事改编,如有雷同,纯属偶然)。
啊啊啊啊啊不扯淡了,今天俺来给大家稍微介绍一下STM32的编码器模式,考虑到对应不同版型开发版的程序会有细微差异,这里着重强调一下博主本人是在STM32F407ZGT6核心板上做的开发;故事的真实背景是这样的,在备赛中国机器人大赛的过程中,我们的机器人遇到了一个棘手的问题,那就是实时了解自己在一个固定地图中的位置。由于缺乏机器人定位导航的的经验和基础知识,大家开始自行摸索,后来给出了激光雷达SLAM的导航方案和惯性导航模块直接导航两种方案。考虑到SLAM多用于动态地图环境构建且其开发很大程度上需要依赖于开发板上携带的操作系统以便进行任务调度,我们放弃了这一成本较高的方案(后来发现赛场上很多队伍都用的激光雷达做SLAM但是效果都很一般的时候,大家内心会心一笑)。惯性导航是一种比较成熟的导航方式,主要用于固定地图情境下的机器人导航,现成的惯导模块简易好用,但是成本较高,考虑到我们当时的机械结构已经不太能安装,我们放弃了直接购买模块的方案,于是一场闹剧开始上演了…基于电机编码器和车载陀螺仪的自制惯性导航模块登场了。下面我们就来讲讲编码器模式究竟是何方神圣以及如何去使用。
暑假期间参加中国机器人大赛,我们队参与的是医疗机器人送药巡诊项目,项目的大致内容如下
要求机器人从出发区出发,按照预先抽签决定好的顺序分别到bed1和bed3处,扫描床头柜上固定区域粘贴的二维码并显示条码信息,之后打开自身自带的封闭药箱(此次我们使用舵机控制两个药盒的开闭)并进行语音播报提醒病人取药,之后自行回到出发区,每一步各自得分。显然由于场景中没有巡线的条件(该项目2018年的比赛中,出发区到病人床头的路是有白线指引的,只需灰度进行巡线即可,但是在其他方面要求更高些),主办方的说法是为了尽可能的模仿医院场景,机器人需具备自主导航能力。基于此,为了保证机器人出发后能够清楚自己所在的位置,导航功能已是不可或缺的了。由于经费有限,我们没能买现成的惯导模块,于是就考虑自己用陀螺仪和编码器来进行自制惯导;一方面,编码器用来记录轮子走过的直线距离,通过一定的关系将其转化为小车实际位移,另一方面陀螺仪的存在保证车子在行进过程中始终保持自己的姿态尽量不发生偏移。
每转过单位的角度就发出一个脉冲信号,通常为A相、B相(某些包括Z相)输出。A相、B相为相互延迟1/4周期的脉冲输出(即正交信号),根据延迟关系可以区别正反转,而且通过取A相、B相的上升和下降沿可以进行2或4倍频。Z相为单圈脉冲,即每圈发出一个脉冲,常用于校正累计误差。
对应一圈,每个基准的角度发出一个唯一与该角度对应二进制的数值,通过外部记圈器件可以进行多个位置的记录和测量。
参考:百度文库
上图表征出了编码器在工作时的脉冲输出特性以及其表达的含义,当我们将编码器的A B两相接在MCU上时,对应的TI1和TI2分别响应输出脉冲,进而得到我们所需要的信息。
信号特性 | 包含信息 |
---|---|
脉冲相位关系 | 电机转向 |
脉冲个数 | 电机角位移 |
到了这里大家就不难发现,所谓编码器模式并不止是代替掉了我们之前的外部中断模式,它在统计脉冲数的基础上,还能得到轮子转向的相关信息,因此在使用时确实相当方便。
下面给出在STM32F407ZGT6板上成功运行的编码器模式初始化程序,由于当时所用为四轮驱动,我们对四个定时器均进行了初始化,但是对每个定时器来说区别不大,由于每个定时器独立工作,因此彼此之间互不干扰。这里给出其中一个定时器的初始化过程:
/**
* TIM1定时器编码器模式初始化
* 入口参数:
* @arr: 自动重装载值,此处为16位自动重装载寄存器
* @psc: 预分频数值,不分频
*/
void Encoder_Init_TIM1(uint16_t arr, uint16_t psc) //16位自动重载计数器
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
GPIO_InitTypeDef GPIO_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE); //使能定时器2的时钟
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE); //使能PA端口时钟
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOE, ENABLE); //使能PA端口时钟
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8; //端口配置
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; //浮空输入
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
GPIO_Init(GPIOA, &GPIO_InitStructure); //根据设定参数初始化GPIOA
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11; //端口配置
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; //浮空输入
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
GPIO_Init(GPIOE, &GPIO_InitStructure); //根据设定参数初始化GPIOA
GPIO_PinAFConfig(GPIOA, GPIO_PinSource8, GPIO_AF_TIM1);
GPIO_PinAFConfig(GPIOE, GPIO_PinSource11, GPIO_AF_TIM1);
TIM_TimeBaseStructInit(&TIM_TimeBaseStructure);
TIM_TimeBaseStructure.TIM_Prescaler = psc; // 预分频器,不分频
TIM_TimeBaseStructure.TIM_Period = arr; //设定计数器自动重装值,是为编码器转整圈时候的脉冲个数102000(0x)
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; //选择时钟分频:不分频
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; ////TIM向上计数
TIM_TimeBaseStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure);
TIM_EncoderInterfaceConfig(TIM1, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising); //使用编码器模式3
TIM1->SR &= 0 << 0;
TIM1->SR &= ~(1 << 0);
TIM1->SR &= ~(1 << 3);
TIM1->SR &= ~(1 << 4); //清零处理SR寄存器的上述几位,否则在未发生编码器脉冲时也会进入中断
TIM_ITConfig(TIM1, TIM_IT_Update, ENABLE); //允许TIM1溢出中断
NVIC_InitStructure.NVIC_IRQChannel = TIM1_UP_TIM10_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x01;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x01;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
//Reset counter
TIM_SetCounter(TIM1, 0);
TIM_Cmd(TIM1, DISABLE);
}
上述初始化的过程中核心的语句为
TIM_EncoderInterfaceConfig(TIM1, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising); //使用编码器模式3
关于该语句的湘西永发大家可以参照库文件中的注释,这里我主要解释一下几个入口参数的作用,TIM_EncoderMode_TI12是指定时器需要响应编码器A相B相两相的脉冲并进行计数,后面两个参数是指响应上升沿,即每有一个上升沿到来均会进行中断操作,这里只需写出中断服务函数即可完成计数功能,下面给出代码实现:
void TIM1_UP_TIM10_IRQHandler(void)
{
if (TIM1->SR & 0x0001)
{
MF.OEncoder++; //编码器脉冲计数
}
TIM1->SR &= ~(1 << 0);
TIM1->SR &= ~(1 << 3);
TIM1->SR &= ~(1 << 4);
}
每当定时器发生溢出时均会触发该中断进行一次计数,因此我们可以考虑找到轮子转动一周对应的编码器脉冲数作为计数的溢出值,每当计数到达该溢出值时即为轮子转动一整圈,由于我们本次比赛所用的编码器线数较多,轮子转一圈对应的脉冲数超过了定时器所能允许的最大重装值(16位定时器,最大允许重装值为2^16=65536),因此我们按照理论值计算出了轮子每转过10cm所对应的脉冲数,并以此为基准进行里程测量,经测试最终误差在mm级,完全能够保证测量精度。此外另一个问题就是计数方向,由于编码器模式下由硬件进行脉冲计数,因此AB相的相位差也被考虑在内,我们知道电机的正反转编码器脉冲输出的相位是不同的,在STM32的编码器模式下寄存器SR内就存储了增/减计数所对应的状态位,只需要按位进行查询即可
关于编码器模式的使用方法就给大家介绍到这里,有任何问题的话可以跟我留言哦,欢迎交流!