剛開始設想做一個上半部分可以上下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