背景
旋钮是仪器仪表上一种常见的输入设备,它的内部是一个旋转编码器
,知乎上的这篇科普文章对其工作原理做了深入浅出的介绍。
我们公司的功率分析仪
的前面板
也用到了该类设备,最近前面板的MCU从MSP430
切换成了STM32
,因此我要将编码器的驱动移植到STM32。
查看MSP430的代码,看懂其基本思路,是将旋钮的2路输出信号接到2个GPIO管脚,并让这2个管脚作为中断源,驱动在中断里检测2路输出信号的电平组合,并做一些复杂的处理,后台循环根据中断的处理结果获悉旋钮的旋转方向,以及旋转过了几个刻度。
觉得MSP430的处理逻辑太复杂了,而且经过确认,app并不需要知道旋转过了几个刻度,决定用简单的方法来实现旋钮功能。
移植思路
了解所用旋钮的信号时序
我们用的旋钮是日本帝国通信的XRE系列,其原理图:
其时序图:
可以看到,因为增量式旋转编码器的2个信号的波形相差1/4周期,因此信号A在跳变时,信号B一定处于稳定的高电平或稳定的低电平。
找到信号波形与旋转方向之间的对应关系
根据时序图可以看出,当A相处于上升沿时,B相总是低
电平,结合时序图左下角的CLOCKWISE ROTATION注释,我们可以得出结论,只要在A相的上升沿中断里检测到B相为低电平,则说明旋钮在顺
时针旋转。
那怎么检测逆时针旋转呢?旋钮手册里并没有COUNTER-CLOCKWISE ROTATION的内容,其实我们只要从右往左看,就是逆时针的时序图:
可以看到,在逆
时针旋转场景里,当A相处于上升沿时,B相总是高
电平,因此我们只需要在A相的上升沿检测一次B相的电平高低,就能判断出旋转方向!
代码编写
STM32的EXTI中断
与MSP430的GPIO自带中断功能不同,STM32的GPIO是不具备中断能力的,要想当中断管脚用,需要搭配EXTI模块。
网上关于EXTI的样例代码较少,更多的是用STM32CubeMX生成的整套工程代码,我现有的工程就是前任员工基于STM32CubeMX生成的,很难将两个工程自动合并。
想到可以将EXTI工程里的有效代码摘出来,插入现有工程里,尝试了一下,可行。
GPIO和EXTI初始化代码
void encoder_init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
// GPIO init
GPIO_InitStruct.Pin = encoder_a2_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_IT_RISING; //A相负责触发中断,确定观测时间点
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
GPIO_InitStruct.Pin = encoder_b2_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT; //软件读取B相的电平高低,从而确定方向
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
/* EXTI interrupt init*/
HAL_NVIC_SetPriority(EXTI0_IRQn, 0, 0); //encoder_a2_Pin是连接到GPIO_PIN_0的,因此对应EXTI的0号中断
HAL_NVIC_EnableIRQ(EXTI0_IRQn);
}
中断回调代码
void EXTI0_IRQHandler(void)
{
/* USER CODE BEGIN EXTI0_IRQn 0 */
/* USER CODE END EXTI0_IRQn 0 */
HAL_GPIO_EXTI_IRQHandler(encoder_a2_Pin);
/* USER CODE BEGIN EXTI0_IRQn 1 */
/* USER CODE END EXTI0_IRQn 1 */
}
void HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin)
{
/* EXTI line interrupt detected */
if (__HAL_GPIO_EXTI_GET_IT(GPIO_Pin) != 0x00u) // 不用担心多个GPIO共用一个EXTI中断,EXTI会做区分
{
__HAL_GPIO_EXTI_CLEAR_IT(GPIO_Pin);
HAL_GPIO_EXTI_Callback(GPIO_Pin);
}
}
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
uint8_t pin_value;
switch (GPIO_Pin)
{
case encoder_a2_Pin:
{
pin_value = HAL_GPIO_ReadPin(encoder_b2_GPIO_Port, encoder_b2_Pin);
if (!pin_value) {
encoder_msg = ENCODERA_LEFT; // 逆时针旋转
} else {
encoder_msg = ENCODERA_RIGH; // 顺时针旋转
}
break;
}
default:
break;
}
}
后台循环代码
uint8_t encoder_msg = INVALID_MSG;
void encoder_process(void)
{
uint8_t out_data[6];
if (encoder_msg != INVALID_MSG)
{
data_process(ENCODER_KEY_TYPE, 0, (uint8_t *)&encoder_msg, out_data); //将旋钮消息按约定格式封装成out_data
spi_enque_tx_data(out_data); // 将out_data通过SPI总线上报主控板
encoder_msg = INVALID_MSG; // clear msg
}
}
总结
我的代码相比老工程,在app感知不到功能差异的前提下,逻辑大大简化,中断占用量还减少了一半,值得大家参考。