前言:本实验用CW32L012作为主控,本实验用CW32L012作为主控,两个舵机组成上下层云台实现左右上下摆的控制。
一、舵机云台介绍:
本实验使用两个SG90舵机,上下两侧各一个组成一个两个自由度的云台。SG90是最经典、最便宜的9g 微型模拟舵机,广泛用于单片机、机器人、航模、云台等项目。上下两个电机分别用两个按键控制实现动作。舵机一共有三根线,黄色是信号线(接引脚),红色接正,灰色接地。下附SG90舵机和云台图片。
舵机云台 |
SG90舵机 |
二、舵机驱动原理:定时器+pwm
1.舵机靠什么控制角度?
答:靠高电平的持续时间,SG90规定PWM频率固定50Hz,一个周期固定20ms。角度只看高电平宽度:1.0ms 高电平 → 转到 0°;1.5ms 高电平 → 转到 90°(中间);2.0ms 高电平 → 转到 180°
2.什么是PWM?
答:PWM=脉冲宽度调制。也就是固定周期里,高电平占多久、低电平占多久。在舵机上面就是20ms的周期内,改变高电平的宽度等于改变了舵机转的角度。
3.为什么一定要用定时器?
答:不用定时器行不行,答案可以但不建议,因为用普通 IO + 延时函数软件模拟 PWM有个致命的缺点那就是:延时卡死 CPU,程序干不了别的;延时不准,波形歪 → 舵机抖动、角度跑偏;主程序一跑别的任务,PWM 就乱了。用定时器的好处:单片机定时器外设自己独立运行;自动生成 20ms 周期;自动控制高电平 1ms、1.5ms、2ms;完全不占用 CPU;你主程序该干嘛干嘛;波形极其标准 → 舵机不抖、定位准。
4.定时器是怎么生成PWM的?
答:定时器就像一个精准秒表。先设定好计时频率(分频),设定好一个完整周期一共多少个数(自动重装载值)。再设置一个比较值,计数小于比较值就输出高电平;计数超过比较值就输出低电平。改比较值就是改高电平时间也就是直接改舵机角度。这就是定时器 PWM 输出原理。
下面具体展示出在Keil工程的代码:
三、软件讲解:
Gimbal.c:#include "Gimbal.h"void Gimbal_Init(void){__SYSCTRL_GPIOA_CLK_ENABLE();__SYSCTRL_GTIM2_CLK_ENABLE();// 1. 配置复用功能PA00_AFx_GTIM2CH1(); // 摇头 CH1PA01_AFx_GTIM2CH2(); // 点头 CH2//2. 配置 GPIO 为推挽输出GPIO_InitTypeDef GPIO_InitStruct;GPIO_InitStruct.IT = GPIO_IT_NONE;GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;GPIO_InitStruct.Pins = GPIO_PIN_0 | GPIO_PIN_1;GPIO_Init(CW_GPIOA, &GPIO_InitStruct);// 3. 定时器基础配置(完全对齐能动的代码)GTIM_InitTypeDef GTIM_InitStruct = {0};GTIM_InitStruct.AlignMode = GTIM_ALIGN_MODE_EDGE;GTIM_InitStruct.ARRBuffState = GTIM_ARR_BUFF_EN;GTIM_InitStruct.Direction = GTIM_DIRECTION_UP;GTIM_InitStruct.EventOption = GTIM_EVENT_NORMAL;GTIM_InitStruct.Prescaler = 95;GTIM_InitStruct.PulseMode = GTIM_PULSE_MODE_DIS;GTIM_InitStruct.ReloadValue = 20000 - 1;GTIM_InitStruct.UpdateOption = GTIM_UPDATE_DIS;GTIM_TimeBaseInit(CW_GTIM2, >IM_InitStruct);// 4. PWM 输出模式配置(完全对齐能动的代码)GTIM_OCModeCfgTypeDef GTIM_OCModeCfgStruct = {DISABLE, DISABLE, 0};GTIM_OCModeCfgStruct.FastMode = DISABLE;GTIM_OCModeCfgStruct.OCMode = GTIM_OC_MODE_PWM1;GTIM_OCModeCfgStruct.OCPolarity = GTIM_OC_POLAR_NONINVERT;GTIM_OCModeCfgStruct.PreloadState = DISABLE;GTIM_OC1ModeCfg(CW_GTIM2, >IM_OCModeCfgStruct);GTIM_OC2ModeCfg(CW_GTIM2, >IM_OCModeCfgStruct);// 5. 初始比较值设 0GTIM_SetCompare1(CW_GTIM2, 0);GTIM_SetCompare2(CW_GTIM2, 0);// 6. 使能通道输出GTIM_OC1Cmd(CW_GTIM2, ENABLE);GTIM_OC2Cmd(CW_GTIM2, ENABLE);// 7. 启动定时器GTIM_Cmd(CW_GTIM2, ENABLE);}void Set_Yaw(float Angle){if(Angle < 0) Angle = 0;if(Angle > 180) Angle = 180;CW_GTIM2->CCR1 = (uint32_t)(Angle * 2000.0f / 180.0f + 500);}void Set_Pitch(float Angle){if(Angle < 0) Angle = 0;if(Angle > 180) Angle = 180;CW_GTIM2->CCR2 = (uint32_t)(Angle * 2000.0f / 180.0f + 500);}Gimbal.h:#ifndef __GIMBAL_H#define __GIMBAL_H#include "cw32l012_gpio.h"#include "cw32l012_gtim.h"#include "cw32l012_sysctrl.h"void Gimbal_Init(void);void Set_Yaw(float Angle);void Set_Pitch(float Angle);#endifKEY.c:#include "key.h"#include "cw32l012_gpio.h"#include "cw32l012_sysctrl.h"static uint8_t key_val = 0;static uint8_t key_down = 0;static uint8_t key_old = 0;// 简单延时消抖(粗略,约10ms @96MHz)static void Key_Delay(uint32_t ms){uint32_t i;while(ms--){i = 12000;while(i--);}}void key_Init(void){__SYSCTRL_GPIOB_CLK_ENABLE();GPIO_InitTypeDef GPIO_Initstructure;GPIO_Initstructure.IT = GPIO_IT_NONE;GPIO_Initstructure.Mode = GPIO_MODE_INPUT_PULLUP; // 上拉输入GPIO_Initstructure.Pins = GPIO_PIN_11 | GPIO_PIN_13;GPIO_Init(CW_GPIOB, &GPIO_Initstructure);}// 读取当前按键状态(带消抖)static uint8_t readpin(void){uint8_t temp = 0;if(GPIO_ReadPin(CW_GPIOB, GPIO_PIN_11) == 0){Key_Delay(10); // 消抖等待if(GPIO_ReadPin(CW_GPIOB, GPIO_PIN_11) == 0)temp |= 0x01; // bit0 表示 PB11 按下}if(GPIO_ReadPin(CW_GPIOB, GPIO_PIN_13) == 0){Key_Delay(10);if(GPIO_ReadPin(CW_GPIOB, GPIO_PIN_13) == 0)temp |= 0x02; // bit1 表示 PB13 按下}return temp;}// 按键扫描:检测下降沿(按下瞬间)void key_scan(void){key_val = readpin();uint8_t changed = key_old ^ key_val; // 哪些位发生了变化if((changed & 0x01) && (key_val & 0x01)) // PB11 刚按下key_down = 1;if((changed & 0x02) && (key_val & 0x02)) // PB13 刚按下key_down = 2;key_old = key_val;}// 获取按键值(读一次后清零,防止重复触发)uint8_t get_keyval(void){uint8_t temp = key_down;key_down = 0;return temp;}KEY.h:#ifndef __KEY_H#define __KEY_H#include <stdint.h>void key_Init(void);void key_scan(void);uint8_t get_keyval(void);#endifmain:#include "cw32l012.h"#include "main.h"#include "Gimbal.h"#include "key.h"#include "oled.h"// 粗略延时(约1ms @96MHz)void Delay_ms(uint32_t ms){uint32_t i;while(ms--){i = 12000;while(i--);}}// 云台当前角度float current_yaw = 90.0f; // 摇头初始90度(居中)float current_pitch = 90.0f; // 点头初始90度(居中)int main(void){// 换掉 SystemCoreClockUpdate(),改成这个SYSCTRL_HSI_Enable(SYSCTRL_HSIOSC_DIV1);SYSCTRL_HCLKPRS_Config(SYSCTRL_HCLK_DIV1);SYSCTRL_PCLKPRS_Config(SYSCTRL_PCLK_DIV1);SYSCTRL_SystemCoreClockUpdate(96000000);key_Init();OLED_Init();OLED_Clear();OLED_Printf(0, 0, OLED_6X8, "Yaw : %.1f", current_yaw);OLED_Printf(0, 10, OLED_6X8, "Pitch: %.1f", current_pitch);OLED_Update();Gimbal_Init();Set_Yaw(current_yaw);Set_Pitch(current_pitch);Delay_ms(500);while(1){key_scan();uint8_t key = get_keyval();if(key == 1){current_yaw += 10.0f;if(current_yaw > 180.0f) current_yaw = 0.0f;Set_Yaw(current_yaw);// 刷新显示OLED_Printf(0, 0, OLED_6X8, "Yaw : %.1f ", current_yaw);OLED_Update();}else if(key == 2){current_pitch += 10.0f;if(current_pitch > 150.0f) current_pitch = 30.0f;Set_Pitch(current_pitch);//刷新显示OLED_Printf(0, 10, OLED_6X8, "Pitch: %.1f ", current_pitch);OLED_Update();}Delay_ms(20);}}void assert_failed(uint8_t *file, uint32_t line) { while(1); }
四、最后总结注意事项:
1.必须做按键消抖硬件加下拉 / 上拉电阻,软件加简单延时消抖或状态机消抖,避免误触发。
2.优先用定时器硬件 PWM别用普通 IO 延时模拟 PWM,容易抖、占用 CPU、按键响应变慢,硬件 PWM 最稳。
3.舵机单独供电不要靠单片机开发板 3.3V/5V 小电流给舵机供电,容易电压跌落,导致抖动、卡死,最好外部 5V 独立供电、共地。
4.角度不要一次性跳太大程序里设置逐级缓转,不要从 0° 直接跳到 180°,保护齿轮不易扫齿,运行更顺滑。
5.程序逻辑写简单分层分开写:按键扫描函数、舵机角度设置函数,不要全部揉在主循环里,方便后期改功能、加模式。
6.预留角度限位程序里限定最小 0°、最大 180°,防止赋值超范围导致舵机卡死烧机。
7.区分 180° 和 360° 舵机实验用180° 角度舵机;买到 360° 连续旋转款是不能定点停角度的,怎么按都只会转圈,注意别买错。
扫码加入QQ群3群| 610403240
106