Skip to content

工程实现

Jason_xy edited this page Feb 11, 2021 · 1 revision

2.1 工程实现任务

硬件部分任务:

  • 完成机架搭建

  • PCB制作与焊接调试

软件部分任务:

基础外设驱动代码编写:

  • 时钟配置

  • I2C驱动

  • TIM驱动

  • USART驱动

功能模块驱动代码编写:

  • OLED驱动

  • GY-86驱动

  • ESP8266驱动

  • 电机驱动

  • 接收机驱动

  • DMP姿态解算驱动

应用层软件程序编写与测试:

  • 上位机数据传送程序

  • 测试程序

2.2 硬件部分实现

我们在第一章将硬件部分的设计制作进行了详细的描述,因此本部分将对详细设计进行实现,完成四轴飞行器的硬件搭建。

1、硬件设备采购焊接

首先是采购STM32核心系统、传感器元器件等并引脚焊接,焊接完成后实物如下页图:

D:\Huawei Share\HwMirror\IMG_20210119_231451.jpg

D:\Huawei Share\HwMirror\IMG_20210119_231411.jpg

图2-1 STM32最小核心系统实物图

核心板引脚采用反焊,便于连接面包板进行调试,也适合接入转接板进行集成。

D:\Huawei Share\HwMirror\IMG_20210119_231626.jpg

图2-2 ESP8266 GY-86 OLED实物图

ESP8266出厂自带排针,不用焊接。GY-86与OLED屏幕引脚焊接时需注意与水平板面呈90度直角焊接,便于安装和校准。

然后是将电机、电调与接收机固定到机臂上:

D:\Huawei Share\HwMirror\IMG_20210119_231932.jpg

D:\Huawei Share\HwMirror\IMG_20210119_231821.jpg

图2-3 电机 电调 接收机实物完成固定

2.机架与组装与搭建实现

收到机架散件后,完成分电板的焊接,再利用螺丝刀将上下盖与机臂固定,然后连接电调后进行电调供电的焊接。最后结合转接板设计方案,将所有数据传输线从中间穿孔引出。

D:\Huawei Share\HwMirror\IMG_20210119_231940.jpg

D:\Huawei Share\HwMirror\IMG_20210119_232011.jpg

图2-5 电源焊接处于机架走线实物

3.PCB转接板设计实现

PCB转接板片上资源分配情况如下:

  • ESP8266:USART1_RX、USART1_TX

  • OLED:I2C1_SDA、I2C1_SCL

  • GY-86:I2C2_SDA、I2C2_SCL

  • Motor Output:TIM3_CH1、TIM3_CH2、TIM3_CH3、TIM3_CH4

    (分别为电机一至电机四)

  • Controller Input:TIM2_CH1、TIM2_CH2、TIM2_CH3、TIM2_CH4

    (接收机通道1~4)

    TIM4_CH3、TIM4_CH4(接收机通道五、六)

  • 调试接口两个,时钟接口4个,供电接口10个左右

图2-6 片上资源分配情况

按照设计要求进行原理图绘制,成果如下

图2-7 V1原理图绘制成果

完成PCB绘制后,2D平面图与3D渲染图结构如下:

2D平面图:

图2-8 V1 PCB绘制成果(2D)

3D渲染图:

图2-9 V1渲染图成果

我们将最终设计好的PCB工程文件进行电路编译,编译通过后即可将工程文件发送给生产厂商,商家会将制作好的PCB板实物发给你。收到后我们按照3D渲染图进行焊接。最终V1转接板实物效果如图:

D:\Huawei Share\HwMirror\IMG_20210119_231720.jpg

D:\Huawei Share\HwMirror\IMG_20210119_231709.jpg

图2-10 转接板焊接实物图

将传感器和核心板与转接板连接,上电测试,测试正常,证明设计合理无误。

图2-11 转接板调试图

最后我们将所有硬件进行组装,得到真机实物图如下:

图2-12 四轴飞行器硬件实现

4.一体化PCB设计制作

V2版本主要完成了MCU最小系统的自主设计,可以极大程度上地缩小模块体积,同时也让模块的整体布局更加自由。

V2原理图绘制:

图2-13 V2原理图

V2版本 2D平面图与3D渲染图:

图2-14 V2 2D平面图与3D渲染图

PCB_V3—最终集成版本

该版本为最终目标版本,在自主设计的最小系统基础上完全重新设计外围硬件模块电路,根据各个功能模块的原理图重新设计PCB布局,实现了硬件模块的完全自主设计。除了第一期所载外设功能模块,该版本还添加了光流传感器、摄像头图传模块、GPS定位模块、SRAM存储器、SD卡等部件。在保持体积的基础上,使模块的功能性尽可能地完善。但是由于设计较为复杂,投产成本较高,目前还在仿真验证阶段,验证完成后就进行初步投产。

原理图绘制:

绘制V3版本2D平面图与3D渲染图:

图2-16 V3 2D平面图与3D渲染图

V3版本打样完成实物图如下,至此硬件实现结束,下节为四轴软件部分实现。

图2-17 V3打样实物图

2.3软件部分实现

我们在第一章将软件部分的设计制作进行了详细的描述,因此本部分将对详细设计进行实现,完成四轴飞行器的软件系统。

软件部分整体的系统逻辑如下图:

INT

图2-18 前后台系统逻辑图

  1. 基础外设驱动编码实现

在了解时钟、I2C、USART、TIM的工作原理与配置思路后,我们分别对以上基础外设进行配置实现。

注:所有基础配置代码都有CubeMX预先生成,此处只针对核心代码与业务层代码进行说明。

时钟代码实现:

首先利用宏定义设置PLL多路复用器的分频系数:

代码2-1 PLL分频系数

#define PLL_M 12
#define PLL_N 96
#define PLL_P 2
#define PLL_Q 4

经过配置后我们的主PLL时钟为:PLL=25/12*96/2=100MHz

代码2-2 系统时钟配置函数

void SystemClock_Config(void)//设置系统时钟为100MHz
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}

  __HAL_RCC_PWR_CLK_ENABLE();//开启时钟
  __HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1); 
//电压缩放
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;//使用HSE
  RCC_OscInitStruct.HSEState = RCC_HSE_ON;//开启HSE
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;//开启PLL
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;//选择HSE做为PLL时钟源
  RCC_OscInitStruct.PLL.PLLM = 12;
  RCC_OscInitStruct.PLL.PLLN = 96;
  RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2;
  RCC_OscInitStruct.PLL.PLLQ = 4;//设置倍频与分频系数
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)//使用HAL函数初始化时钟
  {
    Error_Handler();
  }
  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
  |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;//选择要配置的时钟
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;//将PLL做为系统时钟源
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;//设置AHB总线分频系数
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;//设置APB1分频系数
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;//设置APB2分频系数

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_3) != HAL_OK)
  {
    Error_Handler();
  }
}

上面一段代码首先新建了两个时钟初始化结构体,然后利用HAL库开启时钟并完成电压的缩放。接下来配置结构体相关参数,使用HSE外部高速,开启HSE与PLL,选择HSE作为PLL是中原,然后设置对应的分频系数。最后将初始化结构体导入初始化配置中,完成配置。然后再配置另一个结构体,以同样的方式完成系统时钟的初始化。

IIC驱动代码实现

IIC协议层配置:

代码2-3 I2C协议层实现

void MX_I2C1_Init(void)
{  hi2c1.Instance = I2C1;
  hi2c1.Init.ClockSpeed = 400000;//设置时钟为FAST MODE
  hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;//设置时序占空比为50%
  hi2c1.Init.OwnAddress1 = 0;
  hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;//采用7位地址模式
  hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
  hi2c1.Init.OwnAddress2 = 0;
  hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
  hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;
  if (HAL_I2C_Init(&hi2c1) != HAL_OK)
  {Error_Handler(); }
}

该协议层配置利用CubeMX自带的配置系统,选定I2C号后,设置时钟为快速模式,然后设置时序占空比,四轴I2C采用7地址模式,自有地址1和2都设置为0,不使能双倍地址和通用呼叫模式,但是使能I2C中断。

下面进行I2C物理层配置:

代码2-4 I2C物理层实现

void HAL_I2C_MspInit(I2C_HandleTypeDef* i2cHandle)
{
  	GPIO_InitTypeDef GPIO_InitStruct = {0};//定义初始化结构体
    __HAL_RCC_GPIOB_CLK_ENABLE();//使能GPIO时钟
    //PB6     ------> I2C1_SCL
    //PB7     ------> I2C1_SDA
    GPIO_InitStruct.Pin = GPIO_PIN_6|GPIO_PIN_7;//选择引脚
    GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;//复用开漏输出
    GPIO_InitStruct.Pull = GPIO_PULLUP;//上拉
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;//高速模式
    GPIO_InitStruct.Alternate = GPIO_AF4_I2C1;//引脚复用
    HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);//初始化GPIO
    __HAL_RCC_I2C1_CLK_ENABLE();//使能IIC时钟
}

物理层需要设置I2C使用的GPIO引脚,定义SDL和SCK并且初始化引脚时钟。因为STM32有固定复用给I2C的引脚,所以我们只需要查询手册进行对应的配置即可。

TIM驱动代码实现

TIM1协议层配置(SoftTick计数中断):

代码2-5 TIM1协议层配置

htim1.Instance = TIM1;
htim1.Init.Prescaler = 99;//PSC设置为99
htim1.Init.CounterMode = TIM_COUNTERMODE_UP;//向上计数
htim1.Init.Period = 999;//设置ARR为999
htim1.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;//不分频
htim1.Init.RepetitionCounter = 0;
htim1.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;

代码2-6 TIM1中断配置

HAL_NVIC_SetPriority(TIM1_UP_TIM10_IRQn, 1, 0);
HAL_NVIC_EnableIRQ(TIM1_UP_TIM10_IRQn);

首先是计数中断的配置。我们直到TIM是挂载到APB2总线上的,在时钟配置中我们以及将APB2的时钟配置为100MHz。为达到1us的节拍,经过公式计算,我们将TIM1的PSC配置为99,将ARR配置为999,设置向上计数模式,不分频。这样就能够每隔1us产生一个中断了。然后我们将这个中断配置到用户程序的最高优先级,即不能被其他的外设中断打断。

TIM2/4协议层配置(输入捕获中断):

代码2-7 TIM2协议层配置

//TIM2
htim2.Instance = TIM2;
htim2.Init.Prescaler = 99;//将节拍调整到1us
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;//向上计数
htim2.Init.Period = 4294967295;//32位寄存器默认最大值
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;//不分频
htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;

代码2-8 TIM4协议层配置

//TIM4
htim4.Instance = TIM4;
htim4.Init.Prescaler = 99;//将节拍调整到1us
htim4.Init.CounterMode = TIM_COUNTERMODE_UP;//向上计数
htim4.Init.Period = 65535;//16位寄存器默认最大值
htim4.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;//不分频
htim4.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;

配置TIM2与TIM4的PWM输入捕获。在物理层中,设置PA0~PA3四个引脚对应TIM2的四个通道,PB8与PB9对饮TIM4的一二通道。对相关时钟进行使能,然后对GPIO引脚模式进行配置,最后初始化GPIO,配置并使能中断。在协议层中,我们也将节拍调整到1us,然后将ARR寄存器配置为最大值,用于准备捕获中断。

TIM2/4物理层配置:

代码2-9 TIM2/4物理层配置

GPIO_InitTypeDef GPIO_InitStruct = {0};
if(tim_icHandle->Instance==TIM2)
{
  __HAL_RCC_TIM2_CLK_ENABLE();//使能TIM2时钟
  __HAL_RCC_GPIOA_CLK_ENABLE();//使能GPIOA时钟
  //PA0-WKUP     ------> TIM2_CH1
  //PA1     ------> TIM2_CH2
  //PA2     ------> TIM2_CH3
  //PA3     ------> TIM2_CH4
  GPIO_InitStruct.Pin = GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_2|GPIO_PIN_3;//选择引脚(对应接收机1、2、3、4通道)
  GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;//复用推挽输出
  GPIO_InitStruct.Pull = GPIO_PULLDOWN;//下拉
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;//低速模式
  GPIO_InitStruct.Alternate = GPIO_AF1_TIM2;//引脚复用
  HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);//GPIO初始化
  //TIM2中断配置
  HAL_NVIC_SetPriority(TIM2_IRQn, 3, 0);//中断优先级3
  HAL_NVIC_EnableIRQ(TIM2_IRQn);//使能中断
}
else if(tim_icHandle->Instance==TIM4)
{
  __HAL_RCC_TIM4_CLK_ENABLE();//使能TIM4时钟
  __HAL_RCC_GPIOB_CLK_ENABLE();//使能GPIOB时钟
  //PB8     ------> TIM4_CH3
  //PB9     ------> TIM4_CH4
  GPIO_InitStruct.Pin = GPIO_PIN_8|GPIO_PIN_9;//选择引脚(对应接收机5、6通道)
  GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;//复用推挽输出
  GPIO_InitStruct.Pull = GPIO_PULLDOWN;//下拉
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;//低速模式
  GPIO_InitStruct.Alternate = GPIO_AF2_TIM4;//引脚复用分组
  HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);//GPIO初始化
  //TIM4中断配置
  HAL_NVIC_SetPriority(TIM4_IRQn, 2, 0);//中断优先级2
  HAL_NVIC_EnableIRQ(TIM4_IRQn);//使能中断
}

TIM3物理层配置:

代码2-10 TIM3物理层配置

__HAL_RCC_GPIOA_CLK_ENABLE();//GPIOA时钟使能
__HAL_RCC_GPIOB_CLK_ENABLE();//GPIOB时钟使能
//PA6     ------> TIM3_CH1
//PA7     ------> TIM3_CH2
//PB0     ------> TIM3_CH3
//PB1     ------> TIM3_CH4
GPIO_InitStruct.Pin = GPIO_PIN_6|GPIO_PIN_7;//引脚选择
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;//复用推挽输出
GPIO_InitStruct.Pull = GPIO_PULLDOWN;//下拉
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;//低速模式
GPIO_InitStruct.Alternate = GPIO_AF2_TIM3;//引脚复用分组
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);//GPIOA初始化

GPIO_InitStruct.Pin = GPIO_PIN_0|GPIO_PIN_1;//引脚选择
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;//复用推挽输出
GPIO_InitStruct.Pull = GPIO_PULLDOWN;//下拉
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;//低速模式
GPIO_InitStruct.Alternate = GPIO_AF2_TIM3;//引脚复用分组
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);//GPIOB初始化

最后是配置TIM3的PWM输出。物理层配置与之前类似,四个电机分别对应PA6、PA7、PB0、PB1.协议层中,查询了电机和电调的说明书,配置频率为50Hz的PWM,用于电机转速的控制。

代码2-11 TIM3协议层配置

//输出50HzPWM波用于电机的转速控制。
  htim3.Instance = TIM3;
  htim3.Init.Prescaler = 199;//PSC设置199
  htim3.Init.CounterMode = TIM_COUNTERMODE_UP;//向上计数
  htim3.Init.Period = 9999;//ARR设置9999
  htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;//不分频
  htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;

USART驱动代码实现

USART物理层配置:

代码2-12 USART物理层配置

__HAL_RCC_USART1_CLK_ENABLE();//USART1时钟使能
__HAL_RCC_GPIOA_CLK_ENABLE();//GPIOA时钟使能
//PA9     ------> USART1_TX
//PA10     ------> USART1_RX
GPIO_InitStruct.Pin = GPIO_PIN_9|GPIO_PIN_10;//引脚选择
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;//复用推挽输出
GPIO_InitStruct.Pull = GPIO_NOPULL;//浮空
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;//高速模式
GPIO_InitStruct.Alternate = GPIO_AF7_USART1;//引脚复用分组
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);//GPIOA初始化

//USART1中断配置
HAL_NVIC_SetPriority(USART1_IRQn, 4, 0);//中断优先级4
HAL_NVIC_EnableIRQ(USART1_IRQn);//使能中断

USART协议层配置:

代码2-13 USART协议层配置

huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;//波特率
huart1.Init.WordLength = UART_WORDLENGTH_8B;//8位字长
huart1.Init.StopBits = UART_STOPBITS_1;//1停止位
huart1.Init.Parity = UART_PARITY_NONE;//无校验位
huart1.Init.Mode = UART_MODE_TX_RX;//全双工模式
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_16;//16倍过采样

Printf函数重定向

代码2-14 输出函数重定向

//重定向c库函数printf到USART1
int fputc(int ch, FILE *f)
{
  HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF);
  return ch;
}

2.功能模块驱动编码实现

在本节并没有将所有外设功能模块的驱动代码予以详细分析,这样太过于啰嗦,因此只对核心配置流程进行阐述,主要涉及GY-86、ESP8266、接收机与电机。

GY-86驱动

首先利用I2C通信协议封装GY-86的读写函数,能够实现读写功能。

代码2-15 MPU写函数

//IIC写一个字节 reg:寄存器地址data:数据
//返回值:0,正常
//      其他,错误代码
uint8_t MPU_Write_Byte(uint8_t addr,uint8_t reg,uint8_t data) 				 
{ 
  extern I2C_HandleTypeDef MPU_I2C;
  unsigned char W_Data=0;
  W_Data = data;
  HAL_I2C_Mem_Write(&MPU_I2C, (addr<<1), reg, I2C_MEMADD_SIZE_8BIT, &W_Data, 1, 0xfff);
  return 0;
}

代码2-16 MPU读函数

//IIC读一个字节 reg:寄存器地址 返回值:读到的数据
uint8_t MPU_Read_Byte(uint8_t addr,uint8_t reg,uint8_t *data)
{	extern I2C_HandleTypeDef MPU_I2C;
 	 HAL_I2C_Mem_Read(&MPU_I2C, (addr<<1), reg, I2C_MEMADD_SIZE_8BIT, 
data, 1, 0xfff);
  return 0;
}

配置完成基础读写操作后,则可以进行对照手册进行MPU的初始化。

代码2-17 MPU6050初始化

//初始化MPU6050
//返回值:0,成功
//    其他,错误代码
uint8_t MPU6050_Init(void)
{ 
  uint8_t res;
  extern I2C_HandleTypeDef MPU_I2C;
  MPU_Write_Byte(MPU_ADDR,MPU6050_RA_PWR_MGMT_1,0X80);//复位MPU6050
  MPU_Write_Byte(MPU_ADDR,MPU6050_RA_PWR_MGMT_1,0X00);//唤醒MPU6050 
  MPU_Set_Gyro_Fsr(3);//陀螺仪传感器,±2000dps
  MPU_Set_Accel_Fsr(0);//加速度传感器,±2g
  MPU_Set_Rate(50);//设置采样率50Hz
  MPU_Write_Byte(MPU_ADDR,MPU6050_RA_INT_ENABLE,0X00);//关闭所有中断
  MPU_Write_Byte(MPU_ADDR,MPU6050_RA_USER_CTRL,0X00);//I2C主模式关闭
  MPU_Write_Byte(MPU_ADDR,MPU6050_RA_FIFO_EN,0X00);//关闭FIFO
  MPU_Write_Byte(MPU_ADDR,MPU6050_RA_INT_PIN_CFG,0X80);//INT引脚低电平有效
  MPU_Read_Byte(MPU_ADDR,MPU6050_RA_WHO_AM_I,&res);
  if(res==MPU_ADDR)//器件ID正确
  {
    MPU_Write_Byte(MPU_ADDR,MPU6050_RA_PWR_MGMT_1,0X01);//设置CLKSEL,PLL X轴为参考
    MPU_Write_Byte(MPU_ADDR,MPU6050_RA_PWR_MGMT_2,0X00);//加速度与陀螺仪都工作
    MPU_Set_Rate(50);//设置采样率为50Hz
  }else
  {return 1;}
  return 0;
}

完成MPU6050初始化后,紧接着进行HMC的初始化,原理相同。

代码2-18 HMC5885L初始化

//HMC初始化配置
void HMC_Init(void)
{
	HMC_Write_Byte(HMC_CONFIGA, 0x58);   //01011000/采样平均数4,输出速率75Hz,正常测量配置
	HMC_Write_Byte(HMC_CONFIGB, 0xE0);   //11100000/将增益调至最小
	HMC_Write_Byte(HMC_MODE, 0x00);      //00000000/设置为连续模式
}

最后再次对以上两个函数进行封装,完成GY-86模块的初始化。

代码2-19 GY-86初始化

//GY-86初始化配置
void GY86_Init(void)
{
	MPU6050_Init();
	MPU_Write_Byte(MPU_ADDR,MPU_CFG, 0x02);//将MPU的CFG寄存器的第二位设置为1,其他位在使用MPU时配置
	MPU_Write_Byte(MPU_ADDR,MPU_CTRL, 0x00);//将MPU的CTRL寄存器的第六位设置为0,与上面一步共同开启bypass模式
	HAL_Delay(200);
	HMC_Init();//HMC初始化
	GY86_SelfTest();
}

配置完成以上代码后,则可以交给业务层进行相关业务的处理,如采集GY-86姿态数据,或者利用DMP进行硬件解算等。硬件解算功能模块代码后面会详讲。

ESP8266驱动

ESP8266主要基于USART通讯协议,在正常WIFI传输之前要使用一系列的AT指令进行配置,比如说配置TCP/IP协议,配置从机模式,配置透传模式等。相关驱动代码如下:

在完成配置之后只需要调用串口输出即可输出到上位机显示。

代码2-20 ESP8366驱动代码

//8266的TCP_Client透传模式配置
void esp8266_ap_cipsend_init(void)
{	
	esp8266_cmd("AT+RST\r\n");  //复位模块
    HAL_Delay(500);
    esp8266_cmd("AT+CWMODE=1\r\n"); //设置为AP模式
    HAL_Delay(500);
    esp8266_cmd("AT+CWJAP=\"WuhuTakeOff\",\"uestc404\"\r\n");   //WiFi基本设置
    HAL_Delay(15000);//此处可以通过判定返回信息来替换
    esp8266_cmd("AT+CIPMUX=0\r\n");   //单链路设置
    HAL_Delay(500);
    esp8266_cmd("AT+CIPSTART=\"TCP\",\"192.168.4.1\",8080\r\n"); //TCP服务器
    HAL_Delay(500);
    esp8266_cmd("AT+CIPMODE=1\r\n"); //透传
    HAL_Delay(500);
	esp8266_cmd("AT+CIPSEND\r\n"); //透传开始
    HAL_Delay(500);
}

接收机PWM捕获

接收机驱动主要是完成对接收机信号的捕获和解析,其重点是PWM的输入捕获。在使用之前需要先初始化捕获中断,将时钟频率置为100MHz,这样CNT每经过1us计数一次。

在之前接收机驱动设计的时候已经详细绘制过接收机中断捕获的流程图,下面对该流程图进行代码实现。

代码2-21 接收机中断捕获

//输入捕获中断回调函数
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
    if(htim->Instance == TIM2)
    {
      for(i = 0; i < 4; i++)    
      {
        if(htim->Channel == ActiveChannel[i] && Duty[5] > 0.07)
        {
           cap = 1;    //标志是否进行了一次捕获
           switch(Flag[i])//捕获状态
           {
              case 0://第一次捕获到上升沿
              __HAL_TIM_SET_COUNTER(htim,0);  //计数器置0
              TIM_RESET_CAPTUREPOLARITY (htim, Channel[i]);  //重置触发条件
              TIM_SET_CAPTUREPOLARITY(htim,Channel[i],
              TIM_ICPOLARITY_FALLING);  //设置为下降沿捕获
              Flag[i]++;  //修改捕获状态
              break;

              case 1://第一次捕获到下降沿
              CapVal[i] = HAL_TIM_ReadCapturedValue(htim,Channel[i]); //读取计数器值
              if(CapVal[i] <= 2500)   //过滤超时情况
              Duty[i] = CapVal[i] / Cycle;    //计算占空比,Cycle = 20000us
              Motor_Set(Duty[i], Channel[i]); //更改单机转速
              //准备进行下一次捕获
              TIM_RESET_CAPTUREPOLARITY(htim, Channel[i]);   //重置触发条件
              TIM_SET_CAPTUREPOLARITY(htim,Channel[i],
TIM_ICPOLARITY_RISING);   //设置为下降沿捕获
              Flag[i] = 0;    //重置捕获状态
              break;
            }
          }
          if(cap)
          {
              cap = 0;
              break;
          }
        }
    }

按照中断捕获的原理,用标志位判断是否进行一次捕获,以及是否捕获到上升沿或者下降沿中断。

如果捕获到了上升沿中断,则表示中断首次开始,将修改标志位,然后重置触发条件,设置为下降沿捕获,期间一直在计时。

如果第一次捕获到了下降沿中断,则表示一个PWM捕获完成了,则可以计算占空比了。计算完占空比立即更新电机的转速,然后重置捕获,准备进行下一次捕获。

如果在过程中遇到了上升沿开始后计数器数满了但是还是没捕获到下降沿,则应该采用周期性计数,记录下当前的循环状态,然后清零计数器,重新进行下降沿捕获,直到捕获到了为止。结合周期进行占空比的计算与更新。

else if(htim->Instance == TIM4)
    {
        for(i = 4; i < 6; i++)
        {
            if(htim->Channel == ActiveChannel[i-2])
            {
                cap = 1;
                switch(Flag[i])//捕获状态
                {
                    case 0:
                    __HAL_TIM_SET_COUNTER(htim,0);
                    TIM_RESET_CAPTUREPOLARITY (htim, Channel[i-2]);
                    TIM_SET_CAPTUREPOLARITY(htim, Channel[i-2],
TIM_ICPOLARITY_FALLING);
                    Flag[i]++;
                    break;

                    case 1:
                    CapVal[i] = HAL_TIM_ReadCapturedValue(htim,Channel[i-2]);
                    if(CapVal[i] <= 2500)   //过滤超时情况
                    Duty[i] = CapVal[i] / Cycle;//Cycle = 20000us
                    if(Duty[5] <= 0.055)
                    Motor_Lock();   //电机锁定
                    TIM_RESET_CAPTUREPOLARITY(htim, Channel[i-2]);
                    TIM_SET_CAPTUREPOLARITY(htim, Channel[i-2],
TIM_ICPOLARITY_RISING);
                    Flag[i] = 0;
                    break;
                }
            }
            if(cap)
            {
                cap = 0;
                break;
            }
        }
    }
}

DMP硬件姿态解算驱动

如果选择了使用DMP姿态解算,那么前面的GY-86初始化实际上可以直接调用DMP固件库函数完成。

可以用以下代码完成基本接口的对接

代码2-22 DMP接口对接

#define i2c_write   MPU_Write_Len
#define i2c_read    MPU_Read_Len
#define delay_ms    HAL_Delay
#define get_ms      mget_ms

下面利用DMP固件库完成模块的初始化:

代码2-23 DMP初始化

u8 mpu_dmp_init(void) //mpu6050,dmp初始化  /返回值:0,正常   其他,失败
{	u8 res=0;
	if(mpu_init()==0)	//初始化MPU6050
	{	 res=mpu_set_sensors(INV_XYZ_GYRO|INV_XYZ_ACCEL);//设置所需要的传感器
		if(res)return 1; 
		res=mpu_configure_fifo(INV_XYZ_GYRO | INV_XYZ_ACCEL);//设置FIFO
		if(res)return 2; 
		res=mpu_set_sample_rate(DEFAULT_MPU_HZ);	//设置采样率
		if(res)return 3; 
		res=dmp_load_motion_driver_firmware();		//加载dmp固件
		if(res)return 4; 
	res=dmp_set_orientation(inv_orientation_matrix_to_scalar(gyro_orientation));//设置陀螺仪方向
		if(res)return 5; 
	res=dmp_enable_feature(DMP_FEATURE_6X_LP_QUAT|DMP_FEATURE_TAP|DMP_FEATURE_ANDROID_ORIENT|DMP_FEATURE_SEND_RAW_ACCEL|DMP_FEATURE_SEND_CAL_GYRO|DMP_FEATURE_GYRO_CAL);//设置DMP功能
		if(res)return 6; 
		res=dmp_set_fifo_rate(DEFAULT_MPU_HZ);	//设置DMP输出速率		if(res)return 7;   
		res=run_self_test();		//自检
		if(res)return 8;    
		res=mpu_set_dmp_state(1);	//使能DMP
		if(res)return 9;     
	}return 0;
}

先设置所需要的传感器,在设置FIFO,然后规定采样率,接着加载DMP固件。完成以上步骤后我们将设置陀螺仪方向,然后设置DMP功能。这是功能的函数有点长,有许多参数,需要重点掌握。然后是设置DMP的速率,最大不能超过200MHz。最后进行DMA的自建和使能。在以上过程中,每一步都是哟res进行返回值的接收,如果返回值为0则代表初始化失败,需要重新初始化,则执行return函数退出。

至此所有外设功能模块驱动编写完成。

3.应用层软件编码实现与系统测试

完成所有外设功能模块的驱动编写后,我们下一步将进行系统联调,使所有模块按照前后台程序进行运行,然后上位机中能收到四轴飞行器发来的数据,并且能够解算出实时的姿态。最后通过测试程序完成软件部分的测试。

上位机数据传送程序

上位机程序采用“匿名上位机”的方案,调用串口收发进行源数据的传输。但是数据的结构需要按照匿名上位机的数据帧格式进行安排,以Status功能为例:

帧名称 帧头 1byte 发送设备 1byte 目标设备 1byte 功能字 1byte 数据长度 1byte 数据 N byte 和校验 1byte 注释
VER 0xAA S_ADDR D_ADDR 00 LEN uint8 HardwareType uint16 HardwareVER*100 uint16 SoftwareVER*100 uint16BootloaderVER*100 SUM 版本信息
STATUS 0xAA S_ADDR D_ADDR 01 LEN int16 ROL*100 int16 PIT*100 int16 YAW*100 int32 ALT_USE uint8 FLY_MODEL uint8 ARMED SUM 姿态等基本信息 ARMED:0锁定 1解锁

表格 2-1 匿名上位机数据帧

对应代码如下页

代码2-24 传送数据帧

//数据拆分宏定义,在发送大于1字节的数据类型时,比如int16、float等,需要把数据拆分成单独字节进行发送
#define BYTE0(dwTemp)       ( *( (char *)(&dwTemp)    ) )
#define BYTE1(dwTemp)       ( *( (char *)(&dwTemp) + 1) )
#define BYTE2(dwTemp)       ( *( (char *)(&dwTemp) + 2) )
#define BYTE3(dwTemp)       ( *( (char *)(&dwTemp) + 3) )

void ANO_DT_Send_Status(float angle_rol, float angle_pit, float angle_yaw, s32 alt, u8 fly_model, u8 armed)
{
    u8 _cnt=0;vs16 _temp;vs32 _temp2 = alt;
    data_to_send[_cnt++]=0xAA;//数据
    data_to_send[_cnt++]=0x05;//发送设备编
	data_to_send[_cnt++]=0xAF;//接收设备编
    data_to_send[_cnt++]=0x01;//功能
    data_to_send[_cnt++]=0x0C;//数据长度
    _temp = (int)(angle_rol*100)
    data_to_send[_cnt++]=BYTE1(_temp);  data_to_send[_cnt++]=BYTE0(_temp)
    _temp = (int)(angle_pit*100)
    data_to_send[_cnt++]=BYTE1(_temp);  data_to_send[_cnt++]=BYTE0(_temp)
    _temp = (int)(angle_yaw*100)
    data_to_send[_cnt++]=BYTE1(_temp);  data_to_send[_cnt++]=BYTE0(_temp);
    data_to_send[_cnt++]=BYTE3(_temp2);  data_to_send[_cnt++]=BYTE2(_temp2);
    data_to_send[_cnt++]=BYTE1(_temp2);  data_to_send[_cnt++]=BYTE0(_temp2);
    data_to_send[_cnt++] = fly_model;  data_to_send[_cnt++] = armed;
    u8 sum = 0
    for(u8 i=0;i<_cnt;i++)
        sum += data_to_send[i]
    data_to_send[_cnt++]=sum;
    ANO_DT_Send_Data(data_to_send, _cnt)
}

在完成姿态解算后,我们将用前后台逻辑使得能够利用遥控器控制四个电机的旋转。

软件联调测试

在实施方案一章我们描述过整个软件系统的运行逻辑,在此处我们再次用一张图表的形式展示以下整个软件的前后台关系:

图2-19 前后台逻辑示意图

最后的测试代码如下:

代码2-25 前后台程序测试

while (1)//后台程序
{	if(SoftTick%50==2)//OLED屏幕刷新
	{	OLED_Show_3num(Gx, Gy, Gz, 0);
		OLED_Show_3num(Ax, Ay, Az, 1);
		OLED_Show_3num(Mag_x, Mag_y, Mag_z, 2);
		OLED_Show_3num(Cap[0], Cap[1], Cap[2], 3);
		OLED_Show_3num(Cap[3], Cap[4], Cap[5], 4);
		OLED_ShowNum(24, 7, MPU_Get_Temperature(),2,12);
	}	if(SoftTick%2==1)//上位机数据传输
	{	for(int i = 0; i < 6; i++)
			Cap[i] =  (Duty[i] * 100 - 5) / 0.05;
		ANO_DT_Send_Status(roll,pitch,yaw,2333,122,(Cap[5]>50));
		ANO_DT_Send_Senser(Ax,Ay,Az,Gx,Gy,Gz,2333,2333,2333);
		ANO_DT_Send_RCData(0,0,0,0,Cap[0],Cap[1],Cap[2],Cap[3],Cap[4],Cap[5]);
	}
	read_Gyroscope_DPS();
	read_Accelerometer_MPS();
}
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)//溢出中断,用于1ms计时。
{	
	if(SoftTick>1000)SoftTick=0;
	SoftTick++;
	mpu_dmp_get_data(&pitch,&roll,&yaw);//读取姿态数据  
}