1. 项目概述与核心设计思路
这个基于STM32F103C8T6的云台控制系统,通过MPU6050六轴传感器采集姿态数据,经过处理后驱动两个SG90-360°舵机实现云台随动,并在OLED上实时显示数据。整个系统采用Keil5开发环境配合STM32标准库实现,是一个典型的嵌入式传感器-执行器闭环控制系统。
核心设计思路可以分解为三个关键环节:
- 数据采集层:通过软件模拟I2C协议与MPU6050通信,获取三轴加速度和三轴陀螺仪数据
- 数据处理层:对原始传感器数据进行标定和映射,转换为舵机控制信号
- 执行控制层:根据处理后的数据生成PWM信号驱动舵机,同时通过OLED反馈系统状态
选择STM32F103C8T6作为主控是因为其72MHz主频和丰富的外设接口完全能满足实时控制需求,而软件I2C的实现则提供了引脚配置的灵活性。MPU6050作为业界常用的6DOF传感器,其±2000°/s的陀螺仪量程和±16g的加速度计量程足以应对大多数姿态检测场景。
2. 硬件准备与电路连接
2.1 所需硬件清单
- STM32F103C8T6最小系统板(蓝色药丸板) ×1
- MPU6050六轴传感器模块 ×1
- SG90-360°连续旋转舵机 ×2
- 0.96寸OLED显示屏(SSD1306驱动) ×1
- 杜邦线若干
- 5V/2A电源(为舵机单独供电)
2.2 关键电路连接
code复制MPU6050 STM32F103C8T6
VCC → 3.3V
GND → GND
SCL → PB6(可自定义)
SDA → PB7(可自定义)
SG90舵机1 STM32F103C8T6
信号线 → PA0(TIM2_CH1)
VCC → 5V外部电源
GND → 共地
SG90舵机2 STM32F103C8T6
信号线 → PA1(TIM2_CH2)
VCC → 5V外部电源
GND → 共地
OLED STM32F103C8T6
VCC → 3.3V
GND → GND
SCL → PB8(I2C1_SCL)
SDA → PB9(I2C1_SDA)
重要提示:舵机必须使用独立电源供电!STM32的3.3V稳压器无法提供舵机工作所需的大电流,直接连接可能导致MCU复位或损坏。
3. 软件I2C实现详解
3.1 软件I2C的底层驱动
在资源受限的嵌入式系统中,软件模拟I2C相比硬件I2C具有更好的引脚配置灵活性。以下是完整的软件I2C实现:
c复制// GPIO引脚定义
#define I2C_SCL_PORT GPIOB
#define I2C_SCL_PIN GPIO_Pin_6
#define I2C_SDA_PORT GPIOB
#define I2C_SDA_PIN GPIO_Pin_7
// 引脚电平操作宏
#define W_I2C_SCL(x) GPIO_WriteBit(I2C_SCL_PORT, I2C_SCL_PIN, (x)?Bit_SET:Bit_RESET)
#define W_I2C_SDA(x) GPIO_WriteBit(I2C_SDA_PORT, I2C_SDA_PIN, (x)?Bit_SET:Bit_RESET)
#define R_I2C_SDA() GPIO_ReadInputDataBit(I2C_SDA_PORT, I2C_SDA_PIN)
void Myself_I2C_Init(void) {
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitStructure.GPIO_Pin = I2C_SCL_PIN | I2C_SDA_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; // 开漏输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(I2C_SCL_PORT, &GPIO_InitStructure);
W_I2C_SCL(1);
W_I2C_SDA(1);
}
// I2C起始信号
void Myself_I2C_Start(void) {
W_I2C_SDA(1);
W_I2C_SCL(1);
delay_us(5);
W_I2C_SDA(0);
delay_us(5);
W_I2C_SCL(0);
}
// I2C停止信号
void Myself_I2C_Stop(void) {
W_I2C_SDA(0);
delay_us(5);
W_I2C_SCL(1);
delay_us(5);
W_I2C_SDA(1);
delay_us(5);
}
3.2 字节传输的实现
I2C协议的数据传输以字节为单位,每个字节后需要跟随一个应答位:
c复制// 发送一个字节
void MyI2C_SendByte(uint8_t Byte) {
uint8_t i;
for (i = 0; i < 8; i++) {
W_I2C_SDA(Byte & (0x80 >> i)); // 从高位开始发送
delay_us(2);
W_I2C_SCL(1);
delay_us(5);
W_I2C_SCL(0);
delay_us(2);
}
// 等待从机应答
W_I2C_SDA(1); // 释放SDA线
delay_us(2);
W_I2C_SCL(1);
delay_us(2);
if(R_I2C_SDA()) { // 检测ACK
// 无应答处理
}
W_I2C_SCL(0);
}
// 接收一个字节
uint8_t MyI2C_ReceiveByte(void) {
uint8_t i, Byte = 0x00;
W_I2C_SDA(1); // 释放SDA线
for (i = 0; i < 8; i++) {
W_I2C_SCL(1);
delay_us(2);
if(R_I2C_SDA()) {
Byte |= (0x80 >> i);
}
W_I2C_SCL(0);
delay_us(2);
}
return Byte;
}
调试技巧:用逻辑分析仪抓取I2C波形时,注意检查SCL频率是否在标准模式(100kHz)或快速模式(400kHz)范围内,过高的延时会导致通信失败。
4. MPU6050配置与数据采集
4.1 MPU6050寄存器配置
MPU6050需要正确初始化内部寄存器才能输出有效的传感器数据:
c复制#define MPU6050_ADDRESS 0xD0 // I2C地址
#define MPU6050_PWR_MGMT_1 0x6B
#define MPU6050_GYRO_CONFIG 0x1B
#define MPU6050_ACCEL_CONFIG 0x1C
void MPU6050_Init(void) {
Myself_I2C_Init();
// 唤醒MPU6050,选择X轴陀螺仪作为时钟源
MPU6050_Write_Byte(MPU6050_PWR_MGMT_1, 0x01);
delay_ms(50);
// 陀螺仪满量程设置 ±2000°/s
MPU6050_Write_Byte(MPU6050_GYRO_CONFIG, 0x18);
// 加速度计满量程设置 ±16g
MPU6050_Write_Byte(MPU6050_ACCEL_CONFIG, 0x18);
// 配置DLPF带宽为5Hz
MPU6050_Write_Byte(MPU6050_CONFIG, 0x06);
// 设置采样率分频为50Hz
MPU6050_Write_Byte(MPU6050_SMPLRT_DIV, 0x09);
}
4.2 传感器数据读取
MPU6050的传感器数据存储在14个寄存器中(加速度计和陀螺仪各3轴,每个轴2个寄存器):
c复制void MPU6050_GetData(int16_t *AccelX, int16_t *AccelY, int16_t *AccelZ,
int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ) {
uint8_t buf[14];
MPU6050_Read_Len(MPU6050_ACCEL_XOUT_H, 14, buf);
*AccelX = (int16_t)((buf[0] << 8) | buf[1]);
*AccelY = (int16_t)((buf[2] << 8) | buf[3]);
*AccelZ = (int16_t)((buf[4] << 8) | buf[5]);
*GyroX = (int16_t)((buf[8] << 8) | buf[9]);
*GyroY = (int16_t)((buf[10] << 8) | buf[11]);
*GyroZ = (int16_t)((buf[12] << 8) | buf[13]);
}
注意:MPU6050的原始数据是有符号的16位整数,需要根据设置的量程转换为物理量。例如±2000°/s量程下,陀螺仪灵敏度为16.4LSB/(°/s)。
5. 数据映射与舵机控制
5.1 传感器数据到舵机角度的映射
SG90-360°舵机的控制信号与普通舵机不同,它通过PWM占空比控制旋转速度和方向:
c复制// 全局参数定义
#define GYRO_FULL_SCALE 2000.0f // ±2000°/s
#define GYRO_SENSITIVITY 7000.0f // 经验值
#define SERVO_MAX_SPEED 90.0f // 0-90顺时针,90-180逆时针
#define SERVO_NEUTRAL 90 // 停止位置
#define ERROR_OFFSET 1 // 环境误差补偿
int16_t map_gyro_to_servo(int16_t gyro_value) {
float speed = (SERVO_MAX_SPEED / GYRO_SENSITIVITY) * gyro_value
+ SERVO_NEUTRAL + ERROR_OFFSET;
// 限幅处理
if(speed > 180) speed = 180;
if(speed < 0) speed = 0;
return (int16_t)speed;
}
5.2 舵机PWM信号生成
使用STM32的TIM2产生两路PWM信号分别控制两个舵机:
c复制void Servo_Init(void) {
GPIO_InitTypeDef GPIO_InitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
// 使能时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
// 配置PA0和PA1为复用输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 定时器基础配置 - 20ms周期(50Hz)
TIM_TimeBaseStructure.TIM_Period = 19999; // ARR
TIM_TimeBaseStructure.TIM_Prescaler = 71; // 72MHz/(71+1)=1MHz
TIM_TimeBaseStructure.TIM_ClockDivision = 0;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
// PWM模式配置
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = 1500; // 初始1.5ms
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
// 通道1和通道2配置
TIM_OC1Init(TIM2, &TIM_OCInitStructure);
TIM_OC2Init(TIM2, &TIM_OCInitStructure);
TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Enable);
TIM_OC2PreloadConfig(TIM2, TIM_OCPreload_Enable);
TIM_ARRPreloadConfig(TIM2, ENABLE);
TIM_Cmd(TIM2, ENABLE);
}
// 设置舵机角度(速度)
void Servo_SetAngle(uint8_t servo_num, uint8_t angle) {
uint16_t pulse = 500 + angle * 10; // 0.5ms-2.5ms对应0-180
if(servo_num == 1) {
TIM_SetCompare1(TIM2, pulse);
} else {
TIM_SetCompare2(TIM2, pulse);
}
}
6. OLED显示实现
6.1 OLED初始化
使用硬件I2C1驱动SSD1306 OLED显示屏:
c复制void OLED_Init(void) {
// I2C初始化代码省略...
// SSD1306初始化指令序列
const uint8_t init_cmds[] = {
0xAE, 0xD5, 0x80, 0xA8, 0x3F, 0xD3, 0x00,
0x40, 0x8D, 0x14, 0x20, 0x00, 0xA1, 0xC8,
0xDA, 0x12, 0x81, 0xCF, 0xD9, 0xF1, 0xDB,
0x40, 0xA4, 0xA6, 0x2E, 0xAF
};
for(uint8_t i=0; i<sizeof(init_cmds); i++) {
OLED_Write_Cmd(init_cmds[i]);
}
OLED_Clear();
}
6.2 数据显示实现
在OLED上分区域显示传感器原始数据和舵机控制量:
c复制void OLED_ShowMPU6050Data(int16_t accel[3], int16_t gyro[3], uint8_t servo_speed[2]) {
char buf[16];
// 第一行显示舵机速度
OLED_ShowString(0, 0, "SGX:");
OLED_ShowNum(24, 0, servo_speed[0], 3);
OLED_ShowString(48, 0, "SGY:");
OLED_ShowNum(72, 0, servo_speed[1], 3);
// 第二行显示加速度计数据
sprintf(buf, "AX:%5d", accel[0]);
OLED_ShowString(0, 2, buf);
sprintf(buf, "AY:%5d", accel[1]);
OLED_ShowString(64, 2, buf);
// 第三行显示陀螺仪数据
sprintf(buf, "GX:%5d", gyro[0]);
OLED_ShowString(0, 4, buf);
sprintf(buf, "GY:%5d", gyro[1]);
OLED_ShowString(64, 4, buf);
}
7. 系统集成与调试
7.1 主程序逻辑
将各模块整合到主循环中实现闭环控制:
c复制int main(void) {
int16_t accel[3], gyro[3];
uint8_t servo_speed[2];
// 硬件初始化
SystemInit();
MPU6050_Init();
Servo_Init();
OLED_Init();
while(1) {
// 1. 读取传感器数据
MPU6050_GetData(&accel[0], &accel[1], &accel[2],
&gyro[0], &gyro[1], &gyro[2]);
// 2. 数据映射到舵机控制量
servo_speed[0] = map_gyro_to_servo(gyro[0]); // GX控制舵机1
servo_speed[1] = map_gyro_to_servo(gyro[1]); // GY控制舵机2
// 3. 驱动舵机
Servo_SetAngle(1, servo_speed[0]);
Servo_SetAngle(2, servo_speed[1]);
// 4. OLED显示
OLED_ShowMPU6050Data(accel, gyro, servo_speed);
// 控制周期约20ms
delay_ms(20);
}
}
7.2 调试技巧与常见问题
问题1:舵机抖动或不响应
- 检查电源:确保舵机使用独立5V/2A以上电源
- 检查PWM信号:用示波器测量PWM周期(20ms)和脉宽(0.5ms-2.5ms)
- 检查接线:确保信号线连接正确,没有接触不良
问题2:MPU6050数据异常
- 检查I2C通信:用逻辑分析仪抓取波形,确认地址和时序正确
- 检查传感器放置:确保MPU6050水平安装,避免振动干扰
- 校准传感器:上电后保持静止2秒进行自校准
问题3:OLED显示乱码
- 检查I2C地址:SSD1306通常为0x78或0x7A
- 检查初始化序列:确保发送了完整的初始化命令
- 检查对比度:尝试调整SET_CONTRAST命令值
性能优化建议
- 增加数字滤波:对陀螺仪数据采用一阶低通滤波减少噪声
- 动态调整控制参数:根据运动状态自适应调整映射系数
- 加入死区处理:小角度变化时不响应,避免舵机微抖
8. 项目扩展方向
这个基础云台系统可以进一步扩展为更复杂的应用:
- 姿态解算升级:融合加速度计和陀螺仪数据,实现更精确的互补滤波或卡尔曼滤波
- 无线控制:增加蓝牙或2.4G模块,实现远程控制和参数调整
- 机械结构优化:设计3D打印支架,实现更稳定的机械传动
- 视觉跟踪:配合摄像头实现目标自动跟踪功能
- PID控制:引入PID算法提高云台的运动平滑性和响应速度
实际测试中发现,在快速运动时单纯依靠陀螺仪数据会导致积分漂移,后续可以考虑引入加速度计数据进行姿态融合。另外,SG90舵机的齿轮间隙会影响小角度调整的精度,选用金属齿轮舵机或步进电机可以获得更好的性能。
