刚开始设想做一个上半部分可以上下180°移动,下半部分底座360°移动的激光炮台。于是便开始了实践。
所需材料清单:
序号 | 名称 | 数量 | 备注说明 |
---|---|---|---|
1 | 面包板(Breadboard) | 2 | 用于电路搭建和模块连接 |
2 | 杜邦线(公对公、公对母等) | 若干 | 建议准备 30~50 根,方便连接 |
3 | MB-102 电源模块 | 2 | 插在面包板上,提供 3.3V / 5V 电源 |
4 | 电池(适配 MB-102) | 2 | 建议 9V 方块电池或 7.4V 锂电池 |
5 | SG90 舵机(180° 限位舵机) | 1 | 控制角度在 0°~180° |
6 | SG90 舵机(360° 连续舵机) | 1 | 可连续旋转,用作角度模拟+PID控制 |
7 | STM32F103C8T6 开发板 | 2 | 最小系统板(Blue Pill) |
8 | KY-008 激光模块 | 1 | 激光发射模块(带限流电阻) |
9 | HC-05 蓝牙模块 | 2 | 一发一收,用于无线通信 |
10 | 旋转编码器(KY-040 或同类) | 2 | 用于输入角度,连接 STM32 编码器接口 |
主要过程
起初设想用简单的按钮控制,而单凭if语句只能实现按钮按一下舵机角度变化一下,无法实现舵机角度连续性变化。
//uint8_t KeyNum;
//float Angle;
//uint8_t i;
//int main(void)
//{
// OLED_Init();
// Servo_Init();
// Key_Init();
//
// OLED_ShowString(1,1,"Angle:");
//
// while (1)
// {
// KeyNum=Key_GetNum();
// if(KeyNum==1)
// {
// Angle+=30;
// if(Angle>180)
// {
// Angle=0;
// }
// }
// Servo_SetAngle(Angle);
// OLED_ShowNum(1,7,Angle,3);
// }
//}
因此,完善key.c代码,在原代码中添加按住一直返回低电平来实现。
// 新增的实时检测(按住时一直返回)
uint8_t Key_IsPressed(uint8_t keyID)
{
if (keyID == 1)
{
return (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0);
}
else if (keyID == 2)
{
return (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) == 0);
}
return 0;
}
而此时又发现新的问题,如何很好的控制转速。一开始采用的是改变延时速率来提升转速。但如此一来舵机的稳定性便出现的问题。会发生抖动。
//int main(void)
//{
// OLED_Init();
// Servo_Init();
// Key_Init();
//
// OLED_ShowString(1, 1, "Angle:");
// OLED_ShowString(2, 1, "Angle:");
// while (1)
//{
// if (Key_IsPressed(1)) // 如果按键1被按住
// {
// Angle += 1;
// if (Angle > 180) Angle = 180;
// Servo_SetAngle(Angle);
// OLED_ShowNum(1, 7, Angle, 3);
// Delay_ms(1); // 匀速控制
// }
// if (Key_IsPressed(2)) // 如果按键1被按住
// {
// Angle -= 1;
// if (Angle < 0) Angle = 0;
// Servo_SetAngle(Angle);
// OLED_ShowNum(2, 7, Angle, 3);
// Delay_ms(1); // 匀速控制
// }
//}
于是加大每一次增加的角度,而延迟秒数不变。
float Angle = 90; // 初始角度
float lastAngle = -1; // 用于减少 OLED 刷新频率
int main(void)
{
OLED_Init();
Servo_Init();
Key_Init();
OLED_ShowString(1, 1, "Angle+ :");
Servo_SetAngle(Angle); // 舵机先转到中位
while (1)
{
// 按键 1:增加角度(快速)
if (Key_IsPressed(1))
{
Angle += 5; // 每次增加 5°
if (Angle > 180) Angle = 180;
Servo_SetAngle(Angle);
Delay_ms(15); // 匀速快速控制
}
// 按键 2:减少角度(快速)
if (Key_IsPressed(2))
{
Angle -= 5; // 每次减少 5°
if (Angle < 0) Angle = 0;
Servo_SetAngle(Angle);
Delay_ms(15);
}
// 角度变化才刷新显示
if (Angle != lastAngle)
{
OLED_ShowNum(1, 9, (uint16_t)Angle, 3);
lastAngle = Angle;
}
}
}
的确这样能让舵机较好的连续变化,但是按键寿命有限,频繁操作容易损坏。于是换成更加方便顺手的旋转编码器。其优点也是比较突出的。
优点
操作更直观:想让舵机转多少,就拧多少;比按键舒服很多。
分辨率可调:可以设置每格 1° / 5° / 10°,灵活性高。
响应更快:旋钮快速转几格,舵机就能快速到位。
支持连续调节:不像按钮那样要一直按着,旋钮转动一圈就能从 0° 到 180°。
耐用性更好:旋转编码器机械寿命通常比按键长。
//main函数中
#include "Encoder.h"
float Angle = 90; // 初始角度
float lastAngle = -1; // 上一次显示的角度
int main(void)
{
OLED_Init();
PWM_Init();
Encoder_Init();
Servo_SetAngle(Angle); // 初始角度
OLED_ShowString(1,1,"Angle:");
while(1)
{
int16_t val = Encoder_Get(); // 获取旋转增量
if(val != 0)
{
Angle += val*5; // 编码器每跳一下 → 改变 5°
if(Angle < 0) Angle = 0;
if(Angle > 180) Angle = 180;
Servo_SetAngle(Angle);
}
if(Angle != lastAngle)
{
OLED_ShowNum(1, 8, (uint16_t)Angle, 3);
lastAngle = Angle;
}
}
}
//编码器函数
#include "stm32f10x.h" // Device header
/*================= 编码器初始化 =================*/
int16_t Encoder_Count;
void Encoder_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_0|GPIO_Pin_1;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStructure);
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource0);
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource1);
EXTI_InitTypeDef EXTI_InitStructure;
EXTI_InitStructure.EXTI_Line=EXTI_Line0|EXTI_Line1;
EXTI_InitStructure.EXTI_LineCmd=ENABLE;
EXTI_InitStructure.EXTI_Mode=EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger=EXTI_Trigger_Falling;
EXTI_Init(&EXTI_InitStructure);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel=EXTI0_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority=1;
NVIC_Init(&NVIC_InitStructure);
NVIC_InitStructure.NVIC_IRQChannel=EXTI1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority=2;
NVIC_Init(&NVIC_InitStructure);
}
int16_t Encoder_Get(void)
{
int16_t Temp;
Temp=Encoder_Count;
Encoder_Count=0;
return Temp;
}
/*================= 编码器中断服务函数 =================*/
void EXTI0_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line0)==SET)
{
if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_1)==0)
{
Encoder_Count--;
}
EXTI_ClearITPendingBit(EXTI_Line0);
}
}
void EXTI1_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line1)==SET)
{
if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_0)==0)
{
Encoder_Count++;
}
EXTI_ClearITPendingBit(EXTI_Line1);
}
}
但如此一来旋转编码器需要旋转720°舵机才能旋转180°,如果没有显示屏不太好控制,并且旋转两圈也比较难操作,就想着能否对应编码器旋转360°舵机旋转180。
1.确定编码器的分辨率
绝大多数常见机械旋转编码器是 20 格/圈(detents),有的高分辨率型号可能是 24、30、32 格。
如果是 20 格/圈:旋转 360° → 20 次脉冲。
中断服务函数里 Encoder_Count++ / -- 正好就是在数这些脉冲。
2. 计算换算关系
目标是:
20 格(1 圈) → 180°
那么 每格对应 = 180 ÷ 20 = 9°
如果是 24 格:
24 格(1 圈) → 180°
每格 = 180 ÷ 24 = 7.5°
于是在原有主函数上改遍(我这里的旋转编码器是20格的)
#define ENCODER_STEPS_PER_REV 20 // 编码器分辨率(根据实际修改)
#define SERVO_RANGE_DEG 180 // 舵机可动角度范围
float Angle = 90; // 初始角度
float lastAngle = -1;
while (1)
{
int16_t val = Encoder_Get(); // 获取旋转的脉冲数
if (val != 0)
{
Angle += val * (SERVO_RANGE_DEG / ENCODER_STEPS_PER_REV);
if (Angle < 0) Angle = 0;
if (Angle > 180) Angle = 180;
Servo_SetAngle(Angle);
}
if (Angle != lastAngle)
{
OLED_ShowNum(1, 8, (uint16_t)Angle, 3);
lastAngle = Angle;
}
}
3.同理,再加上一个舵机
//#define ENCODER_STEPS_PER_REV 20 // 编码器一圈脉冲数
//#define SERVO_RANGE_DEG 180 // 舵机行程角度
//#include "Encoder.h"
//#include "OLED.h"
//#include "PWM.h"
//#include "Servo.h"
//float Angle1 = 90; // 舵机1初始角度
//float Angle2 = 90; // 舵机2初始角度
//float lastAngle1 = -1;
//float lastAngle2 = -1;
//int main(void)
//{
// OLED_Init();
// PWM_Init();
// Encoder_Init();
// Servo1_SetAngle(Angle1); // 初始角度
// Servo2_SetAngle(Angle2);
// OLED_ShowString(1,1,"Servo1:");
// OL