基于STM32F103单片机的小四轴飞行器开发指南

序言

        本文采用STM32F103C8T6做主控芯片,整体控制思路分为以下四步:

        1、获取飞行器六轴数据:MPU6050采集飞行器原始六轴数据(三轴加速度、三轴角速度),通过卡尔曼滤波算法对加速度进行滤波、角速度采用一阶低通滤波。

        2、进行姿态解算:对滤波后的数据采用四元数姿态解算,得到飞行器姿态:欧拉角(翻滚角、俯仰角和偏航角)。

        3、获取手柄控制数据(期望值):通过NRF24L01无线模块,获取遥控手柄数据,包括:油门、翻滚角、俯仰角和偏航角。

        4、通过PID调节控制电机转速:对获取到的期望值与实际值,采用PID算法,控制定时器输出,调节电机转速。

硬件准备:

1)3.3V稳压:MIC5219

        为IC元器件提供3.3V驱动电压

2)主控芯片:STM32F103C8T6

        进行数据处理及运算,最高频率72Mhz.

3)姿态检测:MPU6050

        采用IIC总线通讯方式进行数据交互,读取六轴数据(三轴加速度、三轴角速度),根据四元数姿态解算,获取飞行器实际位置(翻滚角、俯仰角和偏航角实际值)。

4)无线传输:NRF24L01

        采用SPI通讯方式进行数据交互,接收遥控手柄数据(油门、翻转角、俯仰角和偏航角期望值)。

5)空心杯电机驱动:SI2302

        使用定时器输出PWM波实现电机调速。

一、获取飞行器六轴数据:

1)模拟IIC——MPU6050读写数据底层驱动

#include "iic.h"

void iic_gpio_config(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    //GPIO时钟使能
    IIC_GPIO_CLK_ENABLE();
    //IIC_SCL输入输出模式配置
    GPIO_InitStructure.Pin=IIC_SCL_BIN;
    GPIO_InitStructure.Mode=GPIO_MODE_OUTPUT_OD;
    GPIO_InitStructure.Speed=GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(IIC_SCL_GPIO_PORT,&GPIO_InitStructure);
    //IIC_SDA输入输出模式配置
    GPIO_InitStructure.Pin=IIC_SDA_PIN;
    HAL_GPIO_Init(IIC_SDA_GPIO_PORT,&GPIO_InitStructure);
    iic_stop();
}

static void iic_delay (void)
{
    uint8_t i;
    for(i=10;i<=0;i--);
}
//iic启动信号
void iic_start(void)
{
    IIC_SDA_H;
    IIC_SCL_H;
    iic_delay();
    IIC_SDA_L;
    iic_delay();
    IIC_SCL_L;
    iic_delay();
}
//iic停止信号
void iic_stop(void)
{
    IIC_SDA_L;
    IIC_SCL_H;
    iic_delay();
    IIC_SDA_H;
    iic_delay();
}
//iic发送数据
void iic_SendByte(uint8_t Sendbyte)
{
    uint8_t i;
    for(i=0;i<8;i++)
    {
        if(Sendbyte & 0x80)
        {
            IIC_SDA_H;
        }
        else
        {
            IIC_SDA_L;
        }
        iic_delay();
        IIC_SCL_H;
        iic_delay();
        IIC_SCL_L;
        if(i==7)
        {
            IIC_SDA_H;   //发送结束后释放数据总线,输出高阻态
        }
        Sendbyte<<=1;
        iic_delay();
    }
}
//iic接收数据
uint8_t iic_ReceiveByte(void)
{
    uint8_t Result=0;
    uint8_t i;
    for(i=0;i<8;i++)
    {
        Result<<=1;
        IIC_SCL_H;
        iic_delay();
        if(IIC_SDA_READ())
        {
            Result++;
        }
        IIC_SCL_L;
        iic_delay();
    }
    return Result;
}
//等待响应
//接收响应成功返回1,失败返回0
uint8_t iic_WaitAck(void)
{
    uint8_t value;
    IIC_SDA_H;            
    iic_delay();
    IIC_SCL_H;
    iic_delay();
    if(IIC_SDA_READ())
    {
        value = 0;
    }
    else
    {
        value = 1;
    }
    IIC_SCL_L;
    iic_delay();
    return value;
}
//发送Ack信号
void iic_Ack(void)
{
    IIC_SDA_L;
    iic_delay();
    IIC_SCL_H;
    iic_delay();
    IIC_SCL_L;
    iic_delay();
    IIC_SDA_H;      //释放SDA总线
    iic_delay();
}
//发送NAck信号
void iic_NAck(void)
{
    IIC_SDA_H;
    iic_delay();
    IIC_SCL_H;
    iic_delay();
    IIC_SCL_L;
    iic_delay();
}

2)MPU6050数据读写:

#include "mpu6050.h"

//写单字节数据
static void iic_mpu6050_write_one_byte(uint8_t slave_adr,
                                       uint8_t reg_adr,
                                       uint8_t data)
{
    //发送起始信号
    iic_start();
    //发送读写设备地址+读写信号
    iic_SendByte(slave_adr);
    //等待从站相应
    while(iic_WaitAck()==0);
    //发送读写寄存器地址
    iic_SendByte(reg_adr);
    //等待从站相应
    while(iic_WaitAck()==0);
    //发送数据
    iic_SendByte(data);
    //等待从站相应
    while(iic_WaitAck()==0);
    //发送停止信号
    iic_stop();
}

//写多字节数据
static void iic_mpu6050_write_bytes(uint8_t slave_adr,
                                    uint8_t reg_adr,
                                    uint8_t* buffer,
                                    uint8_t len)
{
    uint8_t i;
    //发送起始信号
    iic_start();
    //发送读写设备地址+读写信号
    iic_SendByte(slave_adr);
    //等待从站相应
    while(iic_WaitAck()==0);
    //发送读写寄存器地址
    iic_SendByte(reg_adr);
    //等待从站相应
    while(iic_WaitAck()==0);
    //循环发送字节
    for(i=0;i<len;i++)
    {
        iic_SendByte(buffer[i]);
        while(iic_WaitAck()==0);
    }
    iic_stop();
}

//读单字节数据
static uint8_t iic_mpu6050_read_one_byte(uint8_t slave_adr,
                                         uint8_t reg_adr)
{
    uint8_t result;
    //发送起始信号
    iic_start();
    //发送读写设备地址+读写信号
    iic_SendByte(slave_adr);
    //等待从站相应
    while(iic_WaitAck()==0);
    //发送读写寄存器地址
    iic_SendByte(reg_adr);
    //等待从站相应
    while(iic_WaitAck()==0);
    //发送起始信号
    iic_start();
    //发送读写设备地址+读写信号
    iic_SendByte(slave_adr+1);
    //等待从站相应
    while(iic_WaitAck()==0);
    //读取从站数据
    result=iic_ReceiveByte();
    //发送非相应信号
    iic_NAck();
    //发送停止信号
    iic_stop();
    return result;
}

//读多字节数据
static void iic_mpu6050_read_bytes(uint8_t slave_adr,
                                   uint8_t reg_adr,
                                   uint8_t* buffer,
                                   uint8_t len)
{
    uint8_t i;
    //发送起始信号
    iic_start();
    //发送读写设备地址+读写信号
    iic_SendByte(slave_adr);
    //等待从站相应
    while(iic_WaitAck()==0);
    //发送读写寄存器地址
    iic_SendByte(reg_adr);
    //等待从站相应
    while(iic_WaitAck()==0);
    //发送起始信号
    iic_start();
    //发送读写设备地址+读写信号
    iic_SendByte(slave_adr+1);
    //等待从站相应
    while(iic_WaitAck()==0);
    //连续读出数据
    for(i=0;i<len;i++)
    {
        buffer[i]=iic_ReceiveByte();
        //判断是否是最后一个字节
        (i==len-1) ? iic_NAck() : iic_Ack();
    }
    iic_stop();
}

3)获取六轴数据并进行滤波:

#define Acc_read()        iic_mpu6050_read_bytes(MPU6050_ADR,MPU6050_ACC_ADR,buf,6)
#define Gyro_read()       iic_mpu6050_read_bytes(MPU6050_ADR,MPU6050_ACC_ADR,&buf[6],6)

//获取MPU6050数据并滤波
void mpu6050_getdate(uint16_t* mpudate)
{
    uint8_t i;
    uint8_t buf[12];
    Acc_read();
    Gyro_read();
    for(i=0;i<6;i++)
    {
        mpudate[i]=(((uint16_t)buf[i<<1]<<8) + buf[(i<<1)+1]);
        //加速度进行一阶卡尔曼滤波
        if(i<3)
        {
            struct _1_ekf_filter ekf[3]={{0.02,0,8192,0,0.001,0.543},{0.02,0,8192,0,0.001,0.543},{0.02,0,8192,0,0.001,0.543}};
            kalman_1(&ekf[i],(float)mpudate[i]);
            mpudate[i]=(uint16_t)ekf[i].out;
        }
        //角速度进行一阶低通滤波
        else
        {
            uint8_t k=i-3;
            const float factor =0.25f;
            static float tbuf[3];
            mpudate[i]=tbuf[k]=(1-factor)*tbuf[k]+factor*mpudate[i];
        }
    }
}
//一维卡尔曼滤波算法
void kalman_1(struct _1_ekf_filter *ekf,float input)  
{
	ekf->Now_P = ekf->LastP + ekf->Q;
	ekf->Kg = ekf->Now_P / (ekf->Now_P + ekf->R);
	ekf->out = ekf->out + ekf->Kg * (input - ekf->out);
	ekf->LastP = (1-ekf->Kg) * ekf->Now_P ;
}

二、进行姿态解算:

采用四元数姿态解获取欧拉角

void GetAngle(const st_mpu *pMpu,st_ange *pAngE, float dt) 
{		
	volatile struct V Gravity,Acc,Gyro,AccGravity;

	static  float KpDef = 0.5f ;
	static  float KiDef = 0.0001f;
//	static Quaternion NumQ = {1, 0, 0, 0};
	float q0_t,q1_t,q2_t,q3_t;
  //float NormAcc;	
	float NormQuat; 
	float HalfTime = dt * 0.5f;

	

	// 提取等效旋转矩阵中的重力分量 
	Gravity.x = 2*(NumQ.q1 * NumQ.q3 - NumQ.q0 * NumQ.q2);								
	Gravity.y = 2*(NumQ.q0 * NumQ.q1 + NumQ.q2 * NumQ.q3);						  
	Gravity.z = 1-2*(NumQ.q1 * NumQ.q1 + NumQ.q2 * NumQ.q2);	
	// 加速度归一化
    NormAcc = 1/sqrt(squa(mpu6050_data.acc_x)+ squa(mpu6050_data.acc_y) +squa(mpu6050_data.acc_z));
	
    Acc.x = pMpu->acc_x * NormAcc;
    Acc.y = pMpu->acc_y * NormAcc;
    Acc.z = pMpu->acc_z * NormAcc;	
 	//向量差乘得出的值
	AccGravity.x = (Acc.y * Gravity.z - Acc.z * Gravity.y);
	AccGravity.y = (Acc.z * Gravity.x - Acc.x * Gravity.z);
	AccGravity.z = (Acc.x * Gravity.y - Acc.y * Gravity.x);
	//再做加速度积分补偿角速度的补偿值
    GyroIntegError.x += AccGravity.x * KiDef;
    GyroIntegError.y += AccGravity.y * KiDef;
    GyroIntegError.z += AccGravity.z * KiDef;
	//角速度融合加速度积分补偿值
    Gyro.x = pMpu->gyro_x * Gyro_Gr + KpDef * AccGravity.x  +  GyroIntegError.x;//弧度制
    Gyro.y = pMpu->gyro_y * Gyro_Gr + KpDef * AccGravity.y  +  GyroIntegError.y;
    Gyro.z = pMpu->gyro_z * Gyro_Gr + KpDef * AccGravity.z  +  GyroIntegError.z;		
	// 一阶龙格库塔法, 更新四元数

	q0_t = (-NumQ.q1*Gyro.x - NumQ.q2*Gyro.y - NumQ.q3*Gyro.z) * HalfTime;
	q1_t = ( NumQ.q0*Gyro.x - NumQ.q3*Gyro.y + NumQ.q2*Gyro.z) * HalfTime;
	
	
	q2_t = ( NumQ.q3*Gyro.x + NumQ.q0*Gyro.y - NumQ.q1*Gyro.z) * HalfTime;
	q3_t = (-NumQ.q2*Gyro.x + NumQ.q1*Gyro.y + NumQ.q0*Gyro.z) * HalfTime;
	
	NumQ.q0 += q0_t;
	NumQ.q1 += q1_t;
	NumQ.q2 += q2_t;
	NumQ.q3 += q3_t;
	// 四元数归一化
	NormQuat = 1/sqrt(squa(NumQ.q0) + squa(NumQ.q1) + squa(NumQ.q2) + squa(NumQ.q3));
	NumQ.q0 *= NormQuat;
	NumQ.q1 *= NormQuat;
	NumQ.q2 *= NormQuat;
	NumQ.q3 *= NormQuat;	
	

		// 四元数转欧拉角
	{
		 
			#ifdef	YAW_GYRO
			*(float *)pAngE = atan2f(2 * NumQ.q1 *NumQ.q2 + 2 * NumQ.q0 * NumQ.q3, 1 - 2 * NumQ.q2 *NumQ.q2 - 2 * NumQ.q3 * NumQ.q3) * RtA;  //yaw
			#else
				float yaw_G = pMpu->gyro_z * Gyro_G;
				if((yaw_G > 1.2f) || (yaw_G < -1.2f)) 
				{
					pAngE->yaw  += yaw_G * dt;			
				}
			#endif
			pAngE->pitch  =  asin(2 * NumQ.q0 *NumQ.q2 - 2 * NumQ.q1 * NumQ.q3) * RtA;						
		
			pAngE->roll	= atan2(2 * NumQ.q2 *NumQ.q3 + 2 * NumQ.q0 * NumQ.q1, 1 - 2 * NumQ.q1 *NumQ.q1 - 2 * NumQ.q2 * NumQ.q2) * RtA;	//PITCH 			
	}
}

三、获取手柄控制数据:

1)模拟SPI——NRF24L01读写数据底层驱动

#include "spi.h"


//SPI初始化GPIO配置
void spi_gpio_congig(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    //GPIO时钟使能
    SPI_GPIO_CLK_ENABLE();
    //输入输出模式配置
    GPIO_InitStructure.Pin = SPI_MOSI_PIN | SPI_SCK_PIN | SPI_CSN_PIN ;
    GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_PP;
    GPIO_InitStructure.Speed = GPIO_SPEED_HIGH;
    HAL_GPIO_Init(SPI_GPIO_PORT,&GPIO_InitStructure);
    GPIO_InitStructure.Pin = SPI_MISO_PIN ;
    GPIO_InitStructure.Mode = GPIO_MODE_INPUT;
    GPIO_InitStructure.Pull = GPIO_NOPULL;
    HAL_GPIO_Init(SPI_GPIO_PORT,&GPIO_InitStructure);

    SPI_SCK_0;

}

//SPI发送、接收1byte
uint8_t spi_wr(uint8_t t_data)
{
    SPI_SCK_0;
    uint8_t result;
    for(uint8_t i=0;i<8;i++)
    {
        if(t_data&0x80)
        {
            SPI_MOSI_1;
        }
        else
        {
            SPI_MOSI_0;
        }
        t_data<<=1;
        result<<=1;
        SPI_SCK_1;
        if(SPI_MISO_READ())
        {
            result++;
        }
        SPI_SCK_0;
    }
    return result;
}

//spi写寄存器
void spi_w_reg(uint8_t reg,uint8_t value)
{
    SPI_CSN_0;
    spi_wr(reg);
    spi_wr(value);
    SPI_CSN_1;
}
//spi读寄存器
uint8_t spi_r_reg(uint8_t reg)
{
    uint8_t result;
    SPI_CSN_0;
    spi_wr(reg);
    result=spi_wr(0XFF);
    SPI_CSN_1;
    return result;
}
//spi写缓存区
void spi_w_buffer(uint8_t reg,uint8_t* buffer_adr,uint8_t len)
{
    uint8_t i;
    SPI_CSN_0;
    spi_wr(reg);
    for(i=0;i<len;i++)
    spi_wr(buffer_adr[i]);
    SPI_CSN_1;
}
//spi读缓存区
void spi_r_buffer(uint8_t reg,uint8_t* buffer_adr,uint8_t len)
{
    uint8_t i;
    SPI_CSN_0;
    spi_wr(reg);
    for(i=0;i<len;i++)
    buffer_adr[i]=spi_wr(0xFF);
    SPI_CSN_1;
}

2)获取遥控数据

#include "remote.h"

uint8_t rc_temp[32];
void RC_Angle(void)
{
    static uint16_t cnt_15s=0;
    if(nrf_rx_packet(rc_temp)==1)
    {
        cnt_15s=0;
        uint8_t i;
        uint8_t checksum;
        for(i=0;i<31;i++)
        {
            checksum += rc_temp[i];
        }
        if(checksum==rc_temp[31] && rc_temp[0]==0xAA && rc_temp[1]==0xAF)
        {
            Remote.thr=(uint16_t)rc_temp[4]<<8 | rc_temp[5];
            Remote.roll=(uint16_t)rc_temp[6]<<8 | rc_temp[7];
            Remote.pitch=(uint16_t)rc_temp[8]<<8 | rc_temp[9];
            Remote.yaw=(uint16_t)rc_temp[10]<<8 | rc_temp[11];
            {
                //处理遥控数据
                float roll_pitch_ratio = 0.04f;
                pidRoll.desired=-(Remote.roll-1500)*roll_pitch_ratio;
                pidPitch.desired=-(Remote.pitch-1500)*roll_pitch_ratio;
                if(Remote.yaw>1820)
                {
                    pidYaw.desired -= 0.45f;
                }
                else if(Remote.yaw<1180)
                {
                    pidYaw.desired += 0.45f;
                }
                //限制油门不要太大
                Remote.thr=LIMIT(Remote.thr,1000,2000);
            }
        }
    }
}

//NRF接收数据包
uint8_t nrf_rx_packet(uint8_t* rxbuffer)
{
    uint8_t sta;
    NRF_CE_1;
    //获取STATUS状态寄存器的值
    sta = spi_r_reg(STATUS);
    //写状态寄存器,清除中断标志
    spi_w_reg(NRF_WRITE_REG+STATUS,sta);
    if(sta&RX_OK)
    {
        //从接收缓冲区读取数据
        spi_r_buffer(RD_RX_PLOAD,rxbuffer,0x32);
        //清空数据缓冲区
        spi_w_reg(FLUSH_RX,0xff);
        return 1;
    }
    return 0;
}

四、通过PID调节控制电机:

1)定时器输出PWM波

#include "timer.h"

TIM_HandleTypeDef htimer2;

//定时器2初始化,输出PWM波控制电机,周期500ms
void timer2_config(void)
{
    TIM_OC_InitTypeDef TIM_OC_InitStructure;
    //定时器时基配置
    htimer2.Instance = TIM2;
    htimer2.Init.Prescaler = 7199;
    htimer2.Init.CounterMode = TIM_COUNTERMODE_UP;
    htimer2.Init.Period = 499;
    htimer2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
    HAL_TIM_Base_Init(&htimer2);
    //定时器输出比较模式配置
    HAL_TIM_PWM_Init(&htimer2);
    TIM_OC_InitStructure.OCMode = TIM_OCMODE_PWM1;
    TIM_OC_InitStructure.OCPolarity = TIM_OCPOLARITY_HIGH;
    TIM_OC_InitStructure.OCFastMode = TIM_OCFAST_DISABLE;
    TIM_OC_InitStructure.Pulse = 250;
    
    HAL_TIM_PWM_ConfigChannel(&htimer2,&TIM_OC_InitStructure,TIM_CHANNEL_2);
    //定时器启动
    HAL_TIM_Base_Start(&htimer2);
    HAL_TIM_PWM_Start(&htimer2,TIM_CHANNEL_2);
}

void HAL_TIM_Base_MspInit(TIM_HandleTypeDef* htim)
{
    if(htim == &htimer2)
    {
        //定时器时钟使能
        TIM2_CLK_ENABLE();
        //GPIO外设时钟使能
        TIM2_GPIO_CLK_ENABLE();

        GPIO_InitTypeDef GPIO_InitStructure;
        //GPIO输出模式配置
        GPIO_InitStructure.Mode = GPIO_MODE_AF_PP;
        GPIO_InitStructure.Speed = GPIO_SPEED_LOW;
        GPIO_InitStructure.Pin = (TIM2_CH1_PIN | TIM2_CH2_PIN | TIM2_CH3_PIN | TIM2_CH4_PIN);
        HAL_GPIO_Init(TIM2_GPIO_PORT,&GPIO_InitStructure);
    }
}

2)对电机转速进行PID调节

#include "motor.h"

PidObject* (pPidObject[]) = {&pidRateX,&pidRateY,&pidRateZ,&pidRoll,&pidPitch,&pidYaw};

void FlyPidControl(float dt)
{
    volatile static uint8_t status=WAITING_1;
    switch(status)
    {
        case WAITING_1 :
            if(ALL_flag.unlock)
            {
                status=READY_11;
            }
            break;
        case READY_11 :
            //PID参数复位
            pidreset(pPidObject,6);
            status=PROCESS_31;
            break;
        case PROCESS_31:
            //三轴角速度实测值
            pidRateX.measured=mpu6050_data.gyro_x*Gyro_G;
            pidRateY.measured=mpu6050_data.gyro_y*Gyro_G;
            pidRateZ.measured=mpu6050_data.gyro_z*Gyro_G;
            //欧拉角转换值(实际值)
            pidRoll.measured=angle.roll;
            pidPitch.measured=angle.pitch;
            pidYaw.measured=angle.yaw;

            //翻滚角PID调节
            Cascadepid(&pidRoll,&pidRateX,dt);
            //俯仰角PID调节
            Cascadepid(&pidPitch,&pidRateY,dt);
            //偏航角PID调节
            Cascadepid(&pidYaw,&pidRateZ,dt);
            break;
        case EXIT_255 :
            pidreset(pPidObject,6);
            status=WAITING_1;
            break;
        default:
            status=EXIT_255;
            break;
    }
}

uint16_t motor[4];
uint16_t low_thr_cnt;
void MotorControl(void)
{
    volatile static uint8_t status=WAITING_1;
    switch (status)
    {
    case WAITING_1:
        if(ALL_flag.unlock)
        {
            motor[0]=motor[1]=motor[2]=motor[3]=0;
            status=WAITING_2;
        }
        break;
    case WAITING_2:
        if(Remote.thr>1100)
        {
            pidreset(pPidObject,6);
            low_thr_cnt=0;
            status=PROCESS_31;
        }
        break;
    case PROCESS_31:
        uint16_t temp;
        temp=Remote.thr-1000;
        if(temp<10)
        {
            low_thr_cnt++;
            if(low_thr_cnt>500)
            {
                motor[0]=motor[1]=motor[2]=motor[3]=0;
                pidreset(pPidObject,6);
                status=WAITING_2;
            }
        }
        low_thr_cnt=0;
        motor[0]=motor[1]=motor[2]=motor[3]=LIMIT(temp,0,800);
        motor[0]+= + pidRateX.out + pidRateY.out + pidRateZ.out;//; 姿态输出分配给各个电机的控制量
		motor[1]+= - pidRateX.out + pidRateY.out - pidRateZ.out ;//;
		motor[2]+= + pidRateX.out - pidRateY.out - pidRateZ.out;
		motor[3]+= - pidRateX.out - pidRateY.out + pidRateZ.out;//;
        break;
    case EXIT_255:
        motor[0]=motor[1]=motor[2]=motor[3]=LIMIT(temp,0,800);
        status=WAITING_1;
        break;
    default:
        break;
    }
}

五、结尾

        整体的小四轴控制思路大概就是以上几步,调控阶段可以增加USB库,使用USB通讯与匿名上位机链接,实时监测小四轴位置数据,调试参数可以增加EEPROM通过IIC通讯存储。

作者:FrankFeng01

物联沃分享整理
物联沃-IOTWORD物联网 » 基于STM32F103单片机的小四轴飞行器开发指南

发表回复