type
status
date
slug
summary
tags
category
icon
password
摘要
本报告主要探讨了四轴飞行器的设计与实现,课题设计的意义在于,四轴飞行器作为一种新型飞行器,凭借其出色的稳定性和灵活性,以及广泛的应用领域,成为了当前研究的热点。深入研究和理解四轴飞行器的设计和实现,不仅有助于提升学生的实践能力,更有助于培养大家的科研创新能力。
为了完成这个课题,我们首先进行了详细的需求分析和建模,以确保设计的合理性和有效性。我们搭建了Keil5开发环境,为后续的开发工作奠定了坚实的基础。在此环境下,我们实现了多个关键功能,包括读取姿态传感器GY86的数据、传输遥控器信号并接收、解析接收机信号、驱动电机,以及实现蓝牙上位机和下位机之间的数据直传。
在硬件连接方面,我们精心设计了上下两层PCB转接板,以便于固定外设和串接开发板。这一设计既保证了硬件连接的稳定性和可靠性,又提高了整体结构的紧凑性和美观度。
关键词:四轴飞行器、嵌入式开发、PCB电路板绘制
第1章 需求建模与复杂工程问题归纳
软件工程实践是软件计划和开发时需要考虑的方方面面,包括原理、概念、方法和工具等。指导实践的原则称为软件工程实施的基础。本章将介绍我们组在立项之初,对四轴项目做出的开发规划、开发过程模型、需求分析与复杂问题归纳的相关问题。
  1. 软件过程
在开发产品或构建系统时,遵循一系列可预测的步骤(即路线图)是非常重要的,它有助于及时交付高质量的产品。软件开发中所遵循的路线图就被称为“软件过程”。
notion image
软件类似于其他复杂的系统,会随着时间的推移而演化,在长达一年半的课程中,四轴飞行器所需要实现的功能将不断扩展,为了提升开发效率,我们选用了螺旋模型作为项目开发的过程模型。随着的过程开始,从圆心开始顺时针方向,执行螺旋上的一圈所表示的活动;在每次演进的时候,都要考虑开发风险——自己的想法是否能在紧张的学习生活中实现。每个演进过程还要标记里程碑——每当四轴的开发进入到一个完整的新阶段时,我们组就会使用Git将Develop分支Push至main分支。
图1-1 螺旋模型
notion image
根据螺旋模型的普遍定义,螺旋的第一圈一般开发出产品的规格说明——廖老开学第一节课的PPT以及Github仓库上编写的README.md文档;接下来开发产品的原型系统,并在每次迭代中逐步完善。
图1-2 版本控制示意图
1.2 需求分析与建模
本项目将完成基于STM32F401RE的嵌入式四轴飞行器的部分内容,包括裸板初始化、姿态传感器数据采集、蓝牙通信、遥控器信号解析、电机控制以及利用AD绘制转接板以将各个模块与主板正确连接,最终将各功能模块的程序集成。
1.2.1功能需求:
1. 裸板启动:供电、烧录,启动芯片并配置时钟树,实现裸板的初始化和驱动。
2. 姿态传感器驱动:实现底层驱动函数的实现、GY-86的封装并获取相应的姿态数据。
3. 调试数据传输:驱动蓝牙模块,实现与上位机的数据通信,确保可接收和发送数据。
4. 遥控信号:解析接收机信号,将遥控器发送的信号转化为可操作的一系列数据。
5. 电机驱动:实现TIM定时器的初始化与驱动函数的实现,能够根据接收到的数据实时改变转速。
6. PCB转接板设计:设计PCB转接板,用于挂载外设并方便与电调、电机等硬件连接,如有缺陷需多次修正迭代。
7. 集成测试:将各个模块集成,进行整体测试,确保系统功能正常。
1.2.2非功能需求:
1. 实时性:对于飞行器控制系统,需要具备较高的实时性,确保系统能够快速响应和适应外界变化。
2. 稳定性:系统需要在不同工作条件下保持稳定,避免异常情况的发生。
3. 可维护性:代码需要具备良好的可读性和可维护性,以便未来的迭代和修改。
notion image
4. 安全性:对于飞行器系统,安全性是至关重要的,确保系统在异常情况下有安全措施。
图1-3 需求分析用例图
1.3复杂工程问题归纳
探索未知领域的过程会面对相当多的挑战。正如廖老开学时曾说,并非制作一个高大上的复杂软件才能算是复杂工程问题,对于接触嵌入式开发不久的我们,仅仅是使用GPIO口点亮一颗LED就是一个典型的复杂工程问题。
在这一学期的开发流程中,我们归纳出了以下复杂工程问题:
  • Keil如何编写C程序和ARM汇编程序
  • STM32如何能开始工作
  • 如何搭建基于STM32+Keil的嵌入式交叉开发环境
  • 如何为系统提供时钟源?如何确定系统工作的心跳
  • 用 Keil5 点亮STM32的灯并使之闪烁(汇编实现)
  • 裸机下驱动姿态传感器 GY-86
  • 裸机下驱动蓝牙模块
  • 裸机下驱动电机
  • 使用 Altium Designer 完成四轴飞行器转接板原理图及PCB图设计与制作
  • 硬件子系统接受并解析遥控器命令
  • 启动硬件子系统,在自己设计的硬件系统上实现裸机软件集成
对于初学者而言,初次接触STM32,了解裸板的启动过程,包括复位向量、启动文件、启动顺序等,确保在启动时能够正确执行初始化代码,需要有一段适应时间。同时还要完成时钟树的配置,理解芯片内部的配置和初始化,包括主时钟源、分频器、PLL(相位锁定环)等设置,错误的时钟配置将影响整个系统的正常运行。
姿态传感器驱动方面,通信协议配置具有复杂性,配置GY-86模块与STM32F401RE主控板之间的通信协议(如IIC)会涉及复杂的寄存器设置和时序要求
生成PWM波的底层配置通常涉及对定时器模块的寄存器进行设置,这需要对芯片的技术文档和参考手册有深入的了解,包括各个寄存器的功能、位域的含义、寄存器的设置顺序等。此外,对于飞行器电机控制系统,时序的误差可能导致飞行器不稳定或性能下降,这包括PWM波的频率和占空比的准确控制颇具挑战。调整预分频值、自动重载值和输出比较值需要根据具体要求和硬件特性来进行。
PCB设计要学习使用专业的PCB设计软件Altium Designer,修改制作开发板,PCB设计需要考虑信号完整性、电源分配、布线美观等问题,由于开发板的版型限制,各模块合理安装布局在一片开发板上较为困难,可能会设计转接板,用于各模块的连接和线路的封装,同时因为设计考虑不周全,开发版可能需要进行多次迭代和优化。
还有许多除去工程、技术之外的难题:如何平衡修炼技术与课内考试的时间、如何查找资料、如何管理一个工程等等。总的来说这一学期遇到了相当多的挑战,最终还是攻克了其中的绝大多数问题。
第2章 可行性研究与硬件子系统的设计
经过需求分析后,我们能大致确定制作所需要用到的元件与技术。有了初步构思之后,需要对其进行可行性探究,以规避软件开发过程中失败的风险;确定可行后,将进行硬件子系统的设计环节。
2.1 总体概要设计
notion image
图2-1 总体结构设计图
本学期的课程需要完成以下七个任务:
  • 裸板的初始化和驱动
  • 姿态传感器驱动,并获取相应的数据
  • 蓝牙模块的驱动,上位机能接收到下位机发送的数据
  • 接收机信号的解析,接收到遥控器的信号,并最终将其转化为驱动电机的数字
  • 电机的驱动,可根据数据实时改变转速
  • PCB转接板的设计
以STM32F401RET6开发板为中心,实现所有数据的整合和相应功能的实现,总体结构设计图如图3-1,接收机和姿态传感器用来实时捕获外界数据,以及与外界进行交互。接收机精确接收并解析来自遥控器的信号,将其转化为可操作的一系列数据。这些数据随后被用于调节电调,进而精确控制电机的转速,实现了实时的人为控制。为了实现姿态传感器数据的可视化,我们将其读出,通过蓝牙通信模块发送到相应上位机,实现机体姿态的监控。为了固定硬件外设、减少数据线的使用并简化开发板的硬件连接结构,我们要设计PCB转接板,用来挂载外设,且更加方便地与电机、电调等硬件连接。
2.2 可行性研究与模块详细设计
2.2.1 开发环境选择
2.2.1.1 软件开发环境
我们使用keil5来作为STM32的开发环境,因为它提供了完整的集成开发环境(IDE),包括代码编辑器、编译器、链接器和调试器等工具。并且支持多种微控制器架构,包括ARM Cortex-M系列、8051系列等,支持多种编程语言,如C和C++等。Keil还拥有强大的调试功能,能够帮助使用者快速调试程序并发现错误。使用Keil开发STM32应用程序可以大大提高开发效率,并且可以方便地进行代码调试和优化。
2.2.1.2 硬件设计环境
在本次PCB板的开发过程中,我们使用的是Altium Designer开发软件。AD是一款全面的电子设计自动化软件,广泛用于PCB设计、原理图设计和仿真等领域。它提供了强大的工具集,适用于中小型到大型项目。它也是多数人使用的PCB设计主流软件,不仅功能完备,而且经过多年的发展学习资料十分齐全。但是该软件的元件库和封装库均比较少,使用AD进行电路设计时,需要自己花费大量的时间去绘制元件库和封装库。即便是到网上搜索别人绘制过的封装库,也不一定能找到完全适合的封装。
表3-4 EDA软件选择
比较
优点
缺点
AD
操作界面功能强大,快捷方式多 资料齐全,使用时出现问题能及时搜索答案 设计自动化,具有强大的自动布线系统
需要自己绘制原理图元件和PCB封装 入门相较于立创EDA较难
嘉立创
入门简单,上手容易 具有完备的元件库和封装库,无需自己手动绘制 正在逐渐成为国内PCB绘制的主流
操作过程中BUG较多 网上解决问题的资料较少
2.2.3 姿态传感设计
为了精准测算飞行姿态数据,我们需要实时获取加速度、角加速度、磁场环境等数据。在传感器选择方面,GY-86模块集成了多个传感器,包括加速度计、陀螺仪、磁力计、气压计等,减少了硬件设计的复杂性;它通过一条或几条接口线连到主控板,减少了传感器之间的连接问题;若采取独立传感器单独测量单独的数据,优点是可以根据项目的具体需求选择性能更高的独立传感器;维护和升级更为方便,可以独立更换某个故障的传感器,而不影响整体系统;但是会导致系统集成度降低,增大编码与PCB转接板的绘制难度,且价格较于GY-86没有优势。
综合需求后,我们组选择了GY-86模块。该模块包含了三个传感器:
  • 3轴加速度&角加速度计-MPU6050
  • 3轴磁力计-HMC5883L
  • 大气压计-MS5611
通过以上数据的综合,可以获得四轴飞行器的飞行姿态、航向与高度信息。
notion image
2-2 飞行器姿态测算的关键角度
经过系列配置后,该模块将按照一定通信协议(IIC)与F401主控板进行通信,传输姿态数据。这些数据将被F401主控板解算,同时通过蓝牙模块发送到上位机,用于控制电机转速与PID参数调试。
notion image
图2-3 姿态传感器工作流程图
2.2.4 姿态数据传输设计
在飞行器正式试飞之前,需要事先在地面做好电机控制的调试工作,而调试环节最重要的参考依据便是飞行器各轴的加速度和角加速度。鉴于飞行器的运动特性,使用无线方式传输姿态数据的方案比较恰当。
Wi-Fi通信可以提供更高的传输速率和更长的传输距离,适用于需要在较大范围内进行通信的场景;其兼容性强,可以连接到不同类型的设备和网络。但Wi-Fi通信成本相对较高,要更复杂的硬件设计和天线配置;而且会消耗较多电力。
蓝牙模块通常较小巧,集成度高,可以简化硬件设计;它是一种通用的无线通信标准,兼容性强,可以方便地与各种设备连接,而且相对成本较低,使得整体系统的成本相对可控。蓝牙通信的缺点也较为明显,它适用于短距离传输,超过一定距离后信号可能变得不稳定;
经以上对比分析,其实Wi-Fi和蓝牙其实各自有着独到的优势和特性。尽管Wi-Fi在传输的速率上、传输的距离上要高于蓝牙,但是蓝牙在数据的稳定性、安全性、组网的等方面要高于Wi-Fi。因此,我们选择了蓝牙模块。
在蓝牙模块硬件设计上,我们采取了如下方案:
notion image
F401主控板与蓝牙模块1(发送数据)通过一块PCB转接板相连,蓝牙模块2无线远程接收蓝牙模块1发来的数据,通过USB-TTL模块与电脑有线连接。
图2-4 姿态数据传输工作流程
2.2.5 遥控信号设计
飞行器的飞行需要遥控器的操作控制,而遥控器信号的接收和转化直接依赖于接收机,在接收机与遥控器对频(匹配)成功后,便可无线接收遥控器的操作信号,并将其转化为其他信号格式输出,如PWM、PPM或SBUS信号。
notion image
基于以上原理,我们将接收机的输出端口与STM32F401RE主控板的GPIO接口连接,便可将遥控器信号转换后的电信号送入F401进行处理。
图 2-5 PPM与PWM信号示意图
考虑到接收机处理后信号的特点,如上图,我们组选择采用PPM信号作为接收机的输出信号格式。原因如下:
PPM信号将遥控器八个通道的信息集合在一个数据帧内,并以一个通道输出,采用该信号格式能够大大地节省stm32硬件外设资源,减少中断处理的次数,大大提高cpu工作效率;
此外,由图可以看出,八个通道的信息是依次传送的,相互之间并无交叉和重叠,换句话说,一个时间只会传来一个通道的数据而非多个,这也是我们选择PPM信号而非pwm信号的原因,选择pwm会占用多个通道,而且同一时间只会有一个通道在接收数据,其他通道都是处于闲置状态的,为了提高资源的利用率,我们最终选定用PPM信号作为接收机输出信号。
PPM信号格式由一段起始信号和八个通道信号组成,其中每个通道低电平持续时间固定为0.5ms,高电平持续时间为0.5ms-1.5ms,相邻高电平的时间即为一个pwm通道高电平的持续时间,根据计算可以得到起始信号的变化范围为3.5ms-11.5ms。
我们使用TIM定时器的输入捕获模块来捕获接收机的输出信号。对于如何记录各个通道的数据,我们采用中断的方式,当检测到跳变沿(我们选择的是上升沿),触发中断,读取寄存器CCR的值,该值代表通道相邻高电平的时间间隔,我们首先根据它的值判断是否在起始信号范围内,如果在,则开始(重新)依次接收各通道数据,即之后每当遇到上升沿时,记录现有CCR值并清零CNT计数器,直到遇到下一个起始信号(各通道数据接收完毕)。
notion image
这样,我们就收集到了遥控器各个通道的数据信息,之后进行相应的数据转换便可以用来驱动电机,作为改变电机转速的依据。
图2-6 接收遥控信号工作流程
2.2.6 电机驱动设计
notion image
三相无刷电机是基于通电导体在磁场中受力的现象而研制出的电机。我们组采取的无刷直流电机属于外转子无刷直流电机,具有三根输入线,其中任意两根线通电,电机就将转动到一定角度;如果按照一定规律和频率的选择两个线通入电流,电机就将持续性的转动起来。这样一来,三根线切换供电的频率就决定了电机转动的速率,而通入电流的大小决定了电机扭矩的大小。
图2-7 三相无刷电机工作原理示意图
notion image
为了实现“按照一定频率切换输入相”,有两个方案。其一是使用板载高级定时器TIM1的死区生成等功能来直接驱动三相无刷电机。但由于高级定时器外设硬件资源紧缺,且该电机工作电流巨大(可达数十安培),该F401-Nucleo64开发板完全无法承受四个电机同时满载工作的功率,故采取了“开发板——电调——三项无刷电机”的设计方案:开发板输出PWM信号给电调,电调解析该信号并转换为能够驱动三相无刷电机信号。
图2-8 电机工作流程图
2.2.7 采购元件清单
经过上述设计流程,仔细综合考虑了功能需求、预算限制以及性能需求等多个因素,于在线购物平台上进行了仔细筛选和比较,最终选购了符合要求的机架、主控板、蓝牙模块等关键部件。这些部件经过精心挑选,能够满足项目的技术要求和性能期望,同时也符合预期的价格范围,详见表2-1。
表2-1 元件清单
器件
型号
机架
F450 黑色
主控板
ST Nucleo64——STM32F401RE开发板
加速度计&陀螺仪
MPU6050(集成于GY-86)
磁力计
MHC5883L(集成于GY-86)
蓝牙
正点原子ATK-BLE01
遥控器
RadioLink T8S(BT)
接收器
RadioLink R8FM
电机
新西达 2212 100KV 黑色
电调
SkyWalker 30A
电池
格式 3S 2200mAh 30C
转接板
使用 Altium Designer 自行制作
分电板
Matek PDB-XT60 5V-12V分电板
LED
1206贴片发光二极管;12V黄色夜航灯
2.3 硬件工业设计
2.3.1 机体结构设计
外形结构主要由机架决定。飞机顶端主要为STM32控制板,中层放置电池,底层背面安装分电板,焊接供电线缆。四个悬臂上固定电调、电池与夜航灯,并使用扎带固定线缆
内部布局方面,我们组做出了一定考量。在外观上,我们组尽可能的选择了纯黑色的硬件设施,以在视觉上给人简洁、优雅的第一印象;其次,除去电池供电线缆,没有任何线缆暴露在外,这样设计的目的是为了提高安全性,曾经观摩学长调试PID参数、试飞,出现过螺旋桨击打线缆的事故;我们将PWM信号线、接收机天线和电源线缆尽可能的隐藏,并固定;
2.3.2 PCB转接板设计
notion image
由于开发板的版型限制,实现四轴功能所需要的各种模块不能很好的安装在开发版上,所以我们需要制作转接板,来用于各模块的转接、布局以及连接线路的封装,这能使硬件子系统的组装更加便捷,也有利于飞行器外观上的整洁美观并且方便我们后期的调整及调试。
图2-9 转接板设计草图
考虑到如果只有一块转接板来容纳所有的模块会显得杂乱而拥挤,我们将转接板分成了两个部分——上层板及下层板:
上层板主要用于GY-86和一块OLED显示屏的转接,外形则为了贴切开发板的形状而设计为方形,并且考虑到转接板对开发板重启按键的遮挡,还在板子中上方开了方形的槽。
下层板主要用于电机、接收器、蓝牙、电源接口、电源开关、OLED显示屏等部件的转接,外形设计大概为圆角矩形且多有弧度,既贴近机架的形状而又美观大方。板上设计了两处圆孔槽以便于将下层板固定在机架上。同时为了方便与四个电机连接,下层板中央还追加了两个大开槽,能让电机的线路从其中穿过,隐蔽线路而美观便捷。最后,为了整个飞行器的美观,又在下层板底部设计了八个小灯,均匀分布在板子的四角,且其电源线路与电机模块相连,致使其亮度可随电机转速变化而变化,增添了飞行器的可观性及趣味性。
notion image
总的来说,转接板与开发板安装完成后,从下至上的层次为:下层板--开发板--上层板,各模块布局合理,安装亦整洁而便捷,层次感分明,美观大气。
图2-10 PCB转接板设计图
第3章 硬件子系统的实现
按照螺旋的软件开发过程模型,在若干周的制定计划、需求分析、风险分析和产品设计后,我们组的四轴飞行器项目进入到第一轮的编码、实现过程。
本章节将详细介绍该四轴飞行器的硬件子系统实现过程,包含开发过程、标准规范、流程图解、技术细节、代码注解、焊接组装等内容;整个硬件子系统的实现分为两部分:软件驱动代码与整机硬件组装。
notion image
在整机硬件组装的实现部分中,我们严格按照先前阶段的设计进行,并根据实现情况动态调整硬件工业设计。经过数次改装后,现已形成一架体系结构完整的四轴无人机。
图3-1 成品效果示意图
在软件过程模型中,螺旋模型具有鲜明的特点:支持高效扩充、修改的迭代。进阶式挑战性综合项目课程长达三个学期,飞行器的进化贯穿近两年,代码结构精心设计后,在将来新的若干轮螺旋开发中,诸如部署操作系统、对电机加入PID反馈控制算法、添加摄像头等需求都能方便、精准、高效的添加到已有基础之上。
3.1 代码层级设计
在软件驱动部分,我们组引入了面向对象的开发思想,每个模块的代码均以三个层次编写、封装。通过虽然本项目目前基于C语言与汇编语言实现,但仍可通过变量和函数的作用域限定来实现一定程度的封装:
  • 底层驱动层
我们将与硬件直接沟通的代码放置其中(如电机模块的寄存器操作、GPIO口配置),并提供简洁的接口,方便中间层构建进一步的基本操作;
  • 中间层(可选)
它的目的是提供一些高级的功能和抽象,以更方便的使用底层驱动。如IIC通信中,将底层“发送/接受一个字节”实现为“读/写寄存器”;
  • 对外接口层
上述两个层级均是模块内的交互,而在这一层级,我们将每一个模块封装成具有清晰、易用的接口函数,以供模块之间控制流与数据流的交互(如接收机调度电机的转速);
对外接口层应该隐藏底层驱动和中间层的实现细节,提供高级别的抽象,以避免诸如变量名污染等问题,进一步降低代码的耦合性。
3.2 版本控制
版本控制是软件开发过程中不可或缺的环节,它能够有效管理和追踪项目的变化,提供了备份和恢复的能力。在项目建立之初,我么组选择使用Git作为版本控制系统,并将项目仓库托管在GitHub上,这是一个常见且方便的做法。
使用Git和GitHub有以下几个主要优势:
历史记录和版本管理:Git可以记录每个提交的详细信息,包括作者、时间戳和更改内容。这使得组员能能够跟踪项目的演进,并轻松地查看和比较不同版本之间的变化。
分支管理和并行开发:Git支持创建分支,使得团队成员可以并行开发不同的功能或修复不同的问题。每个分支都可以独立进行开发和测试,而不会影响其他分支。一旦完成,可以将分支合并到主分支中,确保代码的整合和稳定性。
团队协作和远程仓库:通过将项目仓库托管在GitHub上,组员可以方便地共享和协作开发代码。GitHub提供了许多协作功能,如问题跟踪、Pull Request和代码审查,促进团队成员之间的交流和合作。
notion image
回滚和恢复:当开发过程中引入了错误或不可预料的问题时,Git的回滚功能非常有用。通过回滚到之前的提交,可以快速恢复到稳定的代码状态,避免对整个项目造成影响。
图3-2 Github在线仓库
在线仓库的建立既方便了组员之间的沟通,也可以方便的与其他组进行学习交流。我们组该仓库的Github链接是:
  • https://github.com/Crystal-PuNK/Quadcopter_Group3.git
3.3 裸板启动实现
为启动这块STM32F401主控板,我们需要对该板的供电、烧录、芯片启动文件和时钟树做一定了解,进行一定的配置。
3.3.1供电与烧录
notion image
根据User Manual-Nucleo F401RE 手册以及该开发板的原理图,共有三种供电方式:USB供电、外部供电(5V)以及外部供电(7V~12V)。我们选择了外部供电(5V)方案:将跳线帽JP5移动至左侧两针脚;将E5V引脚直接连接到下层转接板的5V供电层。
图3-3 原理图(供电部分)
该开发板的默认烧录方式十分简单。仅需一根 USB-A to microUSB 的线缆即可将程序下载到该开发板的闪存内。但有线连接的方式不太方便,我们组的未来规划是在寒假阶段实现无线烧录功能。
3.3.2 芯片启动以及时钟树配置
notion image
为了启动这块开发板,需要初始化系统、配置好时钟树。本工程是基于标准库函数,在Keil5中完成开发,下面是实现过程。
图3-4 Nucleo64-F4开发板俯视图
3.3.2.1新建工程
如图
notion image
3-5,输入F401RE;选上CMSIS-CORE和DEVICE-Startup两个选项
图3-5 新建工程
3.3.2.2 配置标准库函数
首先前往ST公司官网,下载适用于STM32F4XX系列的标准库函数。随后在工程文文件夹根目录上新建三个文件夹(Library、Startup、User)文件夹(如图3-6),进行如下操作:
notion image
图3-6 工程文件夹架构
\STM32F4xx_DSP_StdPeriph_Lib_V1.9.0\Libraries\CMSIS\Device\ST\STM32F4xx\Include中的 stm32f4xx.hsystem_stm32f4xx.h 复制到 Startup 中
notion image
\STM32F4xx_DSP_StdPeriph_Lib_V1.9.0\Libraries\STM32F4xx_StdPeriph_Driver 中 inc 和 src文件夹中的所有文件都复制到 Library 中 STM32F4xx_DSP_StdPeriph_Lib_V1.9.0\Project\STM32F4xx_StdPeriph_Templates 中的
stm32f4xx_conf.h
stm32f4xx_it.c
stm32f4xx_it.h
复制到 User 中
图3-7 STM32 标准外设固件库文件关系图
3.3.1.3 配置Keil5开发环境
1、Add Group:
在keil5工程中添加以上三个同名文件夹,并Add Existing Files。在User中新建main.c和main.h(直接复制粘贴官方样例工程中的也行)
2、配置include路径:
点击魔术棒-c/c++-include path,添加三个文件夹:User、Library和StartUp
3、配置宏定义
点击魔术棒-c/c++-include path,加入USE_STDPERIPH_DRIVER,STM32F401xx
  1. 配置编译器
点击魔术棒-Target,选择 “Use default compiler version 5”
  1. stm32f4xx_fmc.cstm32f4xx_fsmc.c 移除编译列表。
notion image
从官网下载的固件库函数是为整个F4系列设计,而该开发板上不存在fmc和fsmc外设,故应将相应文件移除编译列表。
图3-8 待移除编译列表的文件
3.3.3.2配置下载器
如图
notion image
3-9,选择ST-Link Debugger:
图3-9 烧录器选择界面
3.3.3.3配置时钟树
时钟源是电子设备中的一个关键组成部分,用于生成稳定的时间基准信号。这些时间基准信号通常用于同步各种电子元件和系统中的操作。时钟是一切设备的核心。所以在配置这条线路上的所有硬件前,我们需要将时钟使能,给他一个频率,这个是可以自己设置的。在本次课程中,我们使用的是默认的时钟频率。
notion image
经过系列研究,该F401主控板的时钟来源为其随板烧录器上F103使用的8MHz高速晶振。经过PLL倍频与各路分频器,得到的SysCLK与HCLK为16MHz。事先确定时钟树的工作频率是一件相当重要的事,在后续延时函数、USART串口通信波特率计算、TIM定时器计算中起到了不可替代的作用。
图3-10 本项目时钟树配置示意图
3.4姿态传感器的实现
姿态传感器GY-86与STM32F4主控板之间使用IIC协议进行通信。经过对IIC协议的学习,我们采取了软件IIC的解决方案。该模块的实现大致分为三个部分:底层驱动——相关外设初始化、Delay()系列延时函数和IIC通信协议封装;中间层——初始化GY-86模块、读取并处理GY-86原始数据;GY-86模块对外封装层——初始化模块与读取加速度等系列数据。
3.4.1 底层驱动函数的实现
IIC通信的本质即是在约定好的线缆上有规律的改变电平。
所以,底层的驱动涉及两部分:如何改变电平?——GPIO针脚的初始化;如何支持“有规律”?——时间控制Delay系列函数。
3.4.1.1 IIC通信引脚初始化
经过斟酌,我们选择PB8来模拟SCL、PB9来模拟SDA。查阅手册得知,GPIOB挂载在AHB1总线上。
代码3-1 IIC针脚初始化
void MyIIC_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure; // GPIO初始化结构
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE); // 开启GPIOB总线的时钟
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9; // 设置GPIOB引脚8和9
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz; // 设置GPIO速度为100MHz
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT; // 设置GPIO为输出模式
GPIO_InitStructure.GPIO_OType = GPIO_OType_OD; // 设置GPIO为开漏输出模式
GPIO_Init(GPIOB, &GPIO_InitStructure); // 根据上述配置初始化GPIOB引脚
GPIO_SetBits(GPIOB, GPIO_Pin_8 | GPIO_Pin_9); // 将GPIOB引脚8和9设置为高电平(默认拉高电平)
}

3.4.1.2读/写 SCL与SDA线的电平信号
简单的调用GPIO中读写引脚的库函数,并封装为IIC读写电平的函数。
代码3-2 读写SCL与SDA的电平信号
uint8_t MyIIC_R_SCL(void)
{
uint8_t BitValue; // 用于存储读取的引脚值
BitValue = GPIO_ReadInputDataBit(SCL_PORT, SCL_PIN); // 读取SCL引脚的值
Delay_us(10); // 延迟10微秒(根据具体需求调整延迟时间)
return BitValue; // 返回读取到的引脚值
}
uint8_t MyIIC_R_SDA(void)
{
uint8_t BitValue; // 用于存储读取的引脚值
BitValue = GPIO_ReadInputDataBit(SDA_PORT, SDA_PIN); // 读取SDA引脚的值
Delay_us(10); // 延迟10微秒(根据具体需求调整延迟时间)
return BitValue; // 返回读取到的引脚值
}

3.4.1.3 IIC数据帧中的基本操作
代码3-3 起始/终止位
void MyIIC_Start(void)
{
// 注意,此处需要先释放SDA。考虑到重复起始条件Sr,如果先释放SCL会引起歧义(因为SCL起始是低电平,可能被理解为终止条件)
MyIIC_W_SDA(1); // 将SDA引脚设置为高电平
MyIIC_W_SCL(1); // 将SCL引脚设置为高电平
MyIIC_W_SDA(0); // 将SDA引脚设置为低电平,发送起始条件信号
MyIIC_W_SCL(0); // 将SCL引脚设置为低电平,保持起始条件
}
void MyIIC_Stop(void)
{
// 先拉低SDA保证为低电平
MyIIC_W_SDA(0); // 将SDA引脚设置为低电平,发送终止条件信号
MyIIC_W_SCL(1); // 将SCL引脚设置为高电平,发送终止条件信号
MyIIC_W_SDA(1); // 将SDA引脚设置为高电平,保持终止条件
}

代码 4-4发送/接收字节
void MyIIC_SendByte(uint8_t Byte)
{
// 从高到低发送Byte到SDA线缆上
// 取出最高位(从左到右数第一个数字)
for(uint8_t i = 0; i < 8; i++)
{
MyIIC_W_SDA(Byte & (0x80 >> i)); // 将Byte的每一位通过与运算发送到SDA线上
MyIIC_W_SCL(1); // 将SCL引脚设置为高电平,发送数据
MyIIC_W_SCL(0); // 将SCL引脚设置为低电平,保持数据
}
}
uint8_t MyIIC_ReceiveByte(void)
{
uint8_t Byte = 0x00; // 用于存储接收到的字节数据
MyIIC_W_SDA(1); // 主机释放SDA,交由从机控制
// 释放SDA后可以加一个延时,防止SCL释放后从机还没发出数据
for(uint8_t i = 0; i < 8; i++)
{
MyIIC_W_SCL(1); // 拉高SCL,在SCL高电平期间读取从机发送的数据
if(MyIIC_R_SDA() == 1) { Byte |= (0x80 >> i); } // 通过与运算读取SDA线上的数据,并将其存储在Byte变量中
MyIIC_W_SCL(0); // 将SCL引脚设置为低电平,保持数据
}
return Byte; // 返回接收到的字节数据
}

代码3-5 IIC应答位
 
void MyIIC_SendACK(uint8_t AckBit)
{
MyIIC_W_SDA(AckBit); // 将AckBit的值发送到SDA线上,用于发送应答位
MyIIC_W_SCL(1); // 将SCL引脚设置为高电平,发送应答位
MyIIC_W_SCL(0); // 将SCL引脚设置为低电平,保持应答位
}
uint8_t MyIIC_ReceiveACK(void)
{
uint8_t AckBit; // 用于存储接收到的应答位数据
MyIIC_W_SDA(1); // 主机释放SDA,交由从机控制
MyIIC_W_SCL(1); // 拉高SCL,在SCL高电平期间读取从机发送的应答位数据
AckBit = MyIIC_R_SDA(); // 通过调用函数MyIIC_R_SDA()读取SDA线上的应答位数据
MyIIC_W_SCL(0); // 将SCL引脚设置为低电平,保持应答位
return AckBit; // 返回接收到的应答位数据
}

3.4.3.4软件IIC通信协议封装
至此,IIC内部的代码部分已经完成,现在需要考虑的是如何为IIC这一底层的通信协议构建简洁易用的接口,从而增强代码的可复用性。在这里,我们选择将一些读/写电平信号的涉及硬件的函数与变量隐藏,仅对外提供IIC数据帧的函数。为了在C和汇编语言的编程环境下实现该目标,我们编写如下头文件:
代码3-6 GY86.h
#ifndef __MYIIC_H
#define __MYIIC_H
/* Includes --------------------------------------------------------------*/
#include "stm32f4xx.h"
#include "Delay.h"
/*Public Variables -------------------------------------------------------*/
/*Public Functions -------------------------------------------------------*/
void MyIIC_Init(void); //IIC通信初始化
void MyIIC_Start(void); //数据帧起始位
void MyIIC_Stop(void); //数据帧终止位
void MyIIC_SendByte(uint8_t Byte); //发送一字节
uint8_t MyIIC_ReceiveByte(void); //接受一字节
void MyIIC_SendACK(uint8_t AckBit); //发送应答位
uint8_t MyIIC_ReceiveACK(void); //接受应答位
#endif

3.4.2 GY-86 初始化中间层函数的实现
GY-86模块内包含了MPU6050、HMC5883L和MS5611模块。这三个模块挂载在GY-86内部的同一条IIC总线上。想要对他们做出正确的配置,就需要读写相应的寄存器。为了实现这个目标,我们大致需要三个步骤:利用IIC底层接口书写读取GY-86模块寄存器的函数;在正确的寄存器中读/写正确的内容;封装好模块内部初始化函数与原始数据处理函数。
3.4.2.1 GY-86模块通过IIC通信读/写寄存器
虽然IIC读写操作的数据帧格式相对固定,但我们最终决定将读写寄存器的函数在GY-86中间层实现,而非在IIC通信底层。这么做的原因是不同的模块寄存器规则差异较大,封装在中间层能带来更多针对IIC数据帧的操作空间。
GY-86模块会用到两种数据帧:
notion image
1、对于指定设备,在指定的地址下写入指定的数据
图3-11 “指定地址写”波形图
代码3-7 写入GY-86寄存器
void GY86_WriteRegister(uint8_t GY86_DeviceAddress, uint8_t RegAddress, uint8_t Data)
{
MyIIC_Start(); // 发送起始信号
MyIIC_SendByte(GY86_DeviceAddress | 0x00); // 发送设备地址,并设置为写模式
MyIIC_ReceiveACK(); // 接收应答位
MyIIC_SendByte(RegAddress); // 发送寄存器地址
MyIIC_ReceiveACK(); // 接收应答位
MyIIC_SendByte(Data); // 发送数据
MyIIC_ReceiveACK(); // 接收应答位
MyIIC_Stop(); // 发送停止信号
}

notion image
2、对于指定设备,在指定地址读取寄存器
图3-12 “指定地址读”波形图
代码3-8 读取GY-86寄存器
uint8_t GY86_ReadRegister(uint8_t GY86_DeviceAddress,uint8_t RegAddress)
{
uint8_t Data;
MyIIC_Start();
MyIIC_SendByte(GY86_DeviceAddress | 0x00);
MyIIC_ReceiveACK();
MyIIC_SendByte(RegAddress);
MyIIC_ReceiveACK();
MyIIC_Start();
MyIIC_SendByte(GY86_DeviceAddress | 0x01);
MyIIC_ReceiveACK();
Data = MyIIC_ReceiveByte();
MyIIC_SendACK(1);
MyIIC_Stop();
return Data;
}

3.4.2.2 MPU6050模块的初始化
通过IIC通信,读写MPU6050的相关寄存器,实现读取加速度计、陀螺仪并控制HMC5883L和MS5611。
  1. 在RM-MPU-6000A.pdf与MPU-6000A.pdf中的Register Map查找需要用到的寄存器以及地址:
代码3-9 系列寄存器地址宏定义
//分频器配置
#define MPU6050_SMPLRT_DIV0x19
//CONFIG系列
#define MPU6050_CONFIG 0x1A
#define MPU6050_GYRO_CONFIG0x1B
#define MPU6050_ACCEL_CONFIG0x1C
//加速度计、温度计、磁力计输出的数据
#define MPU6050_ACCEL_XOUT_H0x3B
#define MPU6050_ACCEL_XOUT_L0x3C
#define MPU6050_ACCEL_YOUT_H0x3D
#define MPU6050_ACCEL_YOUT_L0x3E
#define MPU6050_ACCEL_ZOUT_H0x3F
#define MPU6050_ACCEL_ZOUT_L0x40
#define MPU6050_TEMP_OUT_H0x41
#define MPU6050_TEMP_OUT_L0x42
#define MPU6050_GYRO_XOUT_H0x43
#define MPU6050_GYRO_XOUT_L0x44
#define MPU6050_GYRO_YOUT_H0x45
#define MPU6050_GYRO_YOUT_L0x46
#define MPU6050_GYRO_ZOUT_H0x47
#define MPU6050_GYRO_ZOUT_L0x48
//电源配置
#define MPU6050_PWR_MGMT_10x6B
#define MPU6050_PWR_MGMT_20x6C
//ID
#define MPU6050_WHO_AM_I 0x75

2、寄存器配置
notion image
    供电
    图3-13 Register 107 - Power Management 1
    解除睡眠:MPU6050 芯片上电默认睡眠模式,无法操作寄存器,只能读取 WHO_AM_I 寄存器获得其IIC地址号。
    选择时钟源:MPU6050有三个时钟源:内置8MHz、内置陀螺仪时钟、外部时钟。手册强烈推荐选择内置陀螺仪时钟。
    其余位不需要变更。参考手册说明,得出该寄存器应该填入 0000 0001
    • 基础配置
      • notion image
    图3-14 Register 25 - Sample Rate Divider
    notion image
    配置分频系数:配置的分频系数越小,采样率越高,数据刷新越快。具体值根据需求决定,这里设置为 0000 1001
    图3-15 Register 26 – Configuration
    notion image
    配置低通滤波 DLPF_CFG:随着低通滤波变高,数据变化的将更加平滑。具体值根据需求决定,这里设置为 0000 0110
    图3-16 Register 27 - Gyroscope Configuration
    陀螺仪自测 X/Y/ZG_ST设置为0,不用使能。自测是MPU6050模拟一个外部力场,读出一个数据,与手册上的参考值相比较,在允许的误差范围内即代表该模块并未损坏。
    陀螺仪满量程选择 FS_SEL根据项目实际需求选择,量程越小精度越高,此处选择满量程。故此寄存器设置为 00011000
    表3-1 角加速度量程选择
    FS_SEL
    满量程
    0
    ± 250 度/秒
    1
    ± 500 度/秒
    10
    ± 1000 度/秒
    11
    ± 2000 度/秒
    notion image
    3-17 Register 28 - Accelerometer Configuration
    加速度计自测:设置为0,不用使能。自测是MPU6050模拟一个外部力场,读出一个数据,与手册上的参考值相比较,在允许的误差范围内即代表该模块并未损坏。
    加速度计满量程选择 AFS_SEL:根据项目实际需求选择,量程越小精度越高,此处选择满量程。
    表3-2 加速度量程选择
    AFS_SEL
    满量程
    0
    ± 2g
    1
    ± 4g
    10
    ± 8g
    11
    ± 16g
    加速度计高通滤波器:MPU6050通常允许你配置高通滤波器的截止频率,以满足特定应用的需求。较高的截止频率会滤除更多低频分量,但可能会导致数据损失,因此需要权衡。高通滤波器是一个重要的工具,用于提高IMU(惯性测量单元)的性能,特别是在需要高精度姿态估计和动态运动检测的应用中。根据具体项目确定,此处先默认设置为关闭(000)。
    故此寄存器设置为 00011000
    3、IIC总线配置
    notion image
    在 GY-521 模块上,仅有一个MPU6050芯片。这个模块有四个关于IIC通信的引脚:SDA、SCL、XDA、XCL。其中前两个引脚单片机(STM32F4)直接与MPU6050相连,后两个引脚可供其他模块(比如磁力计、气压计等)与MPU6050相连。而在GY-86中,XDA与XCL被整合到PCB板中,与磁力计和气压计相连。
    图3-18 GY-86内部IIC总线示意图
    notion image
    想要STM32F4直接读取磁力计和气压计的数据,我们需要先设置MPU6050的IIC-Master模式。需要配置以下两个寄存器
    图3-19 Register 55 - INT Pin / Bypass Enable Configuration
    notion image
    旁路选择器 I2C_BYPASS_EN:将 I2C_BYPASS_EN 位设定为 1,使能此处的 Master IIC Serial Interface ,使得 AUX_CL 和 AUX_DA 能够连接到MPU6050。
    图3-20 Register 106 - User Control
    IIC-Master模式:当 I2C_MST_EN 被设置为 1 时,IIC-Master模式将被打开,单片机作为MPU6050的主机与之通信,MPU6050作为其他外挂模块的主机与之通信。所以我们需要将此位设置为 0 。
    至此,即可书写 MPU6050 的初始化函数了:
    代码3-10 MPU-6050初始化
    void MPU6050_init(void)
    {
    //解除睡眠,选择时钟(陀螺仪时钟)
    GY86_WriteRegister(MPU6050_ADDRESS,MPU6050_PWR_MGMT_1 ,0x01);
    GY86_WriteRegister(MPU6050_ADDRESS,MPU6050_PWR_MGMT_2 ,0x00);
    //选择分频,越小刷新越快
    GY86_WriteRegister(MPU6050_ADDRESS,MPU6050_SMPLRT_DIV ,0x09);
    //外部同步以及低通滤波
    GY86_WriteRegister(MPU6050_ADDRESS,MPU6050_CONFIG ,0x06);
    //陀螺仪 自测不使能,最大量程选择
    GY86_WriteRegister(MPU6050_ADDRESS,MPU6050_GYRO_CONFIG ,0x18);
    //加速度 自测不使能,最大量程选择,高通滤波器
    GY86_WriteRegister(MPU6050_ADDRESS,MPU6050_ACCEL_CONFIG,0x18);
    //使能AUXIIC-BYPASS
    GY86_WriteRegister(MPU6050_ADDRESS,MPU6050_INT_PIN_CFG ,0x02);
    //关闭IICMASTERmode
    GY86_WriteRegister(MPU6050_ADDRESS,MPU6050_USER_CTRL ,0x00);
    }

    3.4.2.3 HMC5883L模块的初始化
    notion image
    1、CFR_A寄存器配置
    • CRA-7:该为默认位1,只有被设置为0后该芯片才能开始工作
    • 平均采样数 MA1&MA0:00 = 1、01 = 2、10 = 4、11 = 8 (default)
    • 输出频率 DO2~DO0:在连续输出模式下,最高能达到75Hz;在单次输出模式下,通过监控DRDY中断引脚可以实现160Hz的输出频率。
    综上所述,该寄存器可以写入 0111 0000
    notion image
    2、CFR_B寄存器配置
    图 3-22 Configuration Register B
    • 增益默认选择001档位
    • CRB4~CRB0:这五位必须被设置为0以保证芯片正常运行。
    综上所述,该寄存器应该被设置为 0010 0000
    notion image
    3、MR寄存器配置
    图 3-23 Mode Register B
    • MR7~MR2:这七位必须被设置为0才能进行测量。在单次测量模式下,每进行一次测量后,MR7就会自动被置1。
    • 配置模式:我们需要设置为连续测量模式
    因此该寄存器应该被设置为 0000 0000
    综上所述,该模块的初始化函数可以被书写为:
    代码3-11 HMC5883L初始化函数
    void HMC5883L_init(void)
    {
    //配置寄存器A为 0111 0000
    GY86_WriteRegister(HMC5883L_ADDRESS,HMC5883L_CR_A, 0x70);
    //配置寄存器B为 0010 0000
    GY86_WriteRegister(HMC5883L_ADDRESS,HMC5883L_CR_B, 0x20);
    //模式寄存器 为 0000 0000
    GY86_WriteRegister(HMC5883L_ADDRESS,HMC5883L_MODE, 0x00);
    }

    3.4.2.4 MS5611模块的初始化
    notion image
    MS5611在GY86上,针脚PS为高电平,默认为IIC通信。这块芯片也比较特殊,我们是通过发送指令的方式来操控这块芯片,涉及到寄存器的操作仅有“读
    图3-24 MS5611手册截图(命令表)
    取”。初始化很简单,仅需要RESET,并读取几个初始校准数据即可。不同于上面模块通过寄存器地址来访问对应的寄存器,MS5611采取命令代码来访问对应的寄存器。
    对应的初始化函数:
    代码3-12 MS5611初始化函数
    void MS5611_init(void)
    {
    MS5611_SendCommand(MS5611_RESET); // 发送复位命令给MS5611芯片
    SENS_T1 = MS5611_ReadC_x(MS5611_PROM_COEFFICIENT_1) << 15; // 读取并存储MS5611的压力灵敏度值
    OFF_T1 = MS5611_ReadC_x(MS5611_PROM_COEFFICIENT_2) << 16; // 读取并存储MS5611的压力偏移值
    TCS = MS5611_ReadC_x(MS5611_PROM_COEFFICIENT_3) / 256.0; // 读取并存储MS5611的温度系数 TCS
    TCO = MS5611_ReadC_x(MS5611_PROM_COEFFICIENT_4) / 128.0 ; // 读取并存储MS5611的温度偏移系数 TCO
    T_REF = MS5611_ReadC_x(MS5611_PROM_COEFFICIENT_5) << 8 ; // 读取并存储MS5611的温度补偿值 T_REF
    TEMPSENS= MS5611_ReadC_x(MS5611_PROM_COEFFICIENT_6) / 8388608.0; // 读取并存储MS5611的温度灵敏度值 TEMPSENS
    }

    这段代码的执行流程是:发送复位命令 -> 读取并存储压力灵敏度值 -> 读取并存储压力偏移值 -> 读取并存储温度系数 TCS -> 读取并存储温度偏移系数 TCO -> 读取并存储温度补偿值 T_REF -> 读取并存储温度灵敏度值。void MS5611_init(void): 定义了一个函数MS5611_init,用于初始化MS5611传感器。
    MS5611_SendCommand(MS5611_RESET): 发送复位命令给MS5611芯片,将其复位为默认状态。
    SENS_T1 = MS5611_ReadC_x(MS5611_PROM_COEFFICIENT_1) << 15: 通过调用函数MS5611_ReadC_x,读取MS5611的压力灵敏度值,并将其左移15位后保存在SENS_T1变量中。
    OFF_T1 = MS5611_ReadC_x(MS5611_PROM_COEFFICIENT_2) << 16: 通过调用函数MS5611_ReadC_x,读取MS5611的压力偏移值,并将其左移16位后保存在OFF_T1变量中。
    TCS = MS5611_ReadC_x(MS5611_PROM_COEFFICIENT_3) / 256.0: 通过调用函数MS5611_ReadC_x,读取MS5611的温度系数 TCS,并将其除以256.0后保存在TCS变量中。
    3.4.2.5读取原始姿态数据并做初步处理
    notion image
    MS5611模块基于环境温度计算气压,经过查询手册,发现两张流程图,我们将其截图于此:
    图3-25 MS5611手册截图(温度计算流程图)
    notion image
    代码3-13 MS5611 温度计算
    void MS5611_GetTemperature(uint8_t MS5611_D2_OSR_xxxx)
    {
    MS5611_SendCommand(MS5611_D2_OSR_xxxx); // 发送读取温度的命令
    Delay_ms(10); // 延迟10毫秒,等待传感器完成温度转换
    D2_Temperature = MS5611_ReadADC(); // 读取转换后的温度值
    dT = D2_Temperature - T_REF; // 计算温度差值
    TEMP_100times = 2000 + dT * TEMPSENS; // 计算实际温度值并存储
    }

    图3-26 MS5611手册截图(气压计算流程图)
    代码3-14 M5611气压决定
    void MS5611_GetPressure(uint8_t MS5611_D1_OSR_xxxx)
    {
    MS5611_GetTemperature(MS5611_D1_OSR_4096); // 获取温度值,使用默认的过采样率MS5611_D1_OSR_4096
    double T2;
    double OFF2;
    double SENS2;
    MS5611_SendCommand(MS5611_D1_OSR_xxxx); // 发送读取气压的命令,使用指定的过采样率MS5611_D1_OSR_xxxx
    Delay_ms(10); // 延迟10毫秒,等待传感器完成气压转换
    D1_Pressur = MS5611_ReadADC(); // 读取转换后的气压值
    OFF = OFF_T1 + TCO * dT; // 根据温度差值计算气压偏移值
    SENS = SENS_T1 + TCS * dT; // 根据温度差值计算气压灵敏度值
    if(TEMP_100times < 2000)
    {
    T2 = (dT*dT)/2147483648.0;
    OFF2 = 5*(TEMP_100times-2000)*(TEMP_100times-2000)/2;
    SENS2 = 5*(TEMP_100times-2000)*(TEMP_100times-2000)/4;
    if(TEMP_100times < -1500)
    {
    OFF2 = OFF2 + 7 *(TEMP_100times+1500)*(TEMP_100times+1500);
    SENS2 = SENS2 + 11*(TEMP_100times+1500)*(TEMP_100times+1500)/2;
    }
    }
    TEMP_100times = TEMP_100times - T2; // 根据温度修正值修正温度值
    OFF = OFF - OFF2; // 根据修正值修正气压偏移值
    SENS = SENS - SENS2; // 根据修正值修正气压灵敏度值
    P_100times = (D1_Pressur * SENS / 2097152.0 - OFF)/32768.0; // 计算实际气压值并存储
    }

    3.4.3 GY-86 函数的封装
    经过上述底层和中间层的构建,到此处,我们只需要对模块外建立如下接口:一个结构体(姿态数据)、两个函数(GY-86初始化、获取姿态数据)
    3.4.3.1 GY-86 初始化
    该函数将中间层的IIC与三个模块的初始化函数结合在一起调用即可,并将该函数声明在头文件GY86.h中,使其对引用了该头文件的外部环境可见:
    代码3-15 GY-86函数的封装
    void GY86_init(void)
    {
    MyIIC_Init();
    MPU6050_init();
    HMC5883L_init();
    MS5611_init();
    }

    3.4.3.2获取GY-86测得的姿态数据
    该函数将各个小模块的读取寄存器函数结合在一起调用,并对数据进行移位等预处理即可。将该函数声明在头文件GY86.h中,使其对引用了该头文件的外部文件可见。该函数将把姿态数据存储在一个结构体中,该结构也对引用了GY86.h的外部环文件可见。
    代码3-16 读取GY-86数据
    void GY86_GetData(void)
    {
    int16_t DataH,DataL;
    DataH = GY86_ReadRegister(MPU6050_ADDRESS,MPU6050_ACCEL_XOUT_H);
    DataL = GY86_ReadRegister(MPU6050_ADDRESS,MPU6050_ACCEL_XOUT_L);
    Original_Data_List->AX = ((DataH << 8) | DataL)/16; //除以量程16g
    DataH = GY86_ReadRegister(MPU6050_ADDRESS,MPU6050_ACCEL_YOUT_H);
    DataL = GY86_ReadRegister(MPU6050_ADDRESS,MPU6050_ACCEL_YOUT_L);
    Original_Data_List->AY = ((DataH << 8) | DataL)/16; //除以量程16g
    DataH = GY86_ReadRegister(MPU6050_ADDRESS,MPU6050_ACCEL_ZOUT_H);
    DataL = GY86_ReadRegister(MPU6050_ADDRESS,MPU6050_ACCEL_ZOUT_L);
    Original_Data_List->AZ = ((DataH << 8) | DataL)/16; //除以量程16g
    DataH = GY86_ReadRegister(MPU6050_ADDRESS,MPU6050_TEMP_OUT_H);
    DataL = GY86_ReadRegister(MPU6050_ADDRESS,MPU6050_TEMP_OUT_L);
    Original_Data_List->CORE_Temperature = (DataH << 8) | DataL;
    DataH = GY86_ReadRegister(MPU6050_ADDRESS,MPU6050_GYRO_XOUT_H);
    DataL = GY86_ReadRegister(MPU6050_ADDRESS,MPU6050_GYRO_XOUT_L);
    Original_Data_List->GX = ((DataH << 8) | DataL)/2000;//除以量程2000度/秒
    DataH = GY86_ReadRegister(MPU6050_ADDRESS,MPU6050_GYRO_YOUT_H);
    DataL = GY86_ReadRegister(MPU6050_ADDRESS,MPU6050_GYRO_YOUT_L);
    Original_Data_List->GY = ((DataH << 8) | DataL)/2000;//除以量程2000度/秒
    DataH = GY86_ReadRegister(MPU6050_ADDRESS,MPU6050_GYRO_ZOUT_H);
    DataL = GY86_ReadRegister(MPU6050_ADDRESS,MPU6050_GYRO_ZOUT_L);
    Original_Data_List->GZ = ((DataH << 8) | DataL)/2000;//除以量程2000度/秒
    DataH = GY86_ReadRegister(HMC5883L_ADDRESS,HMC5883L_GA_XOUT_H);
    DataL = GY86_ReadRegister(HMC5883L_ADDRESS,HMC5883L_GA_XOUT_L);
    Original_Data_List->GaX = (DataH << 8) | DataL;
    DataH = GY86_ReadRegister(HMC5883L_ADDRESS,HMC5883L_GA_YOUT_H);
    DataL = GY86_ReadRegister(HMC5883L_ADDRESS,HMC5883L_GA_YOUT_L);
    Original_Data_List->GaY = (DataH << 8) | DataL;
    DataH = GY86_ReadRegister(HMC5883L_ADDRESS,HMC5883L_GA_ZOUT_H);
    DataL = GY86_ReadRegister(HMC5883L_ADDRESS,HMC5883L_GA_ZOUT_L);
    Original_Data_List->GaZ = (DataH << 8) | DataL;
    MS5611_GetPressure(MS5611_D2_OSR_4096);
    Original_Data_List->Height = P_100times;
    }

    3.4.3.3 书写头文件GY86.h
    通过头文件来实现“类”的继承关系。在这里,声明了一个extern结构体变量和两个public函数:
    代码3-17 GY86.h
    #ifndef __GY86_H
    #define __GY86_H
    /* Includes ------------------------------------------------------------------*/
    #include "stm32f4xx.h"
    #include "Delay.h"
    #include "MyIIC.h"
    /* Public Variables -------------------------------------------------------------------*/
    extern struct GY86_Data GY86DataList;
    /* Public Functions -------------------------------------------------------------------*/
    void GY86_init(void);
    void GY86_GetData(void);
    #endif

    #ifndef __GY86_H: 条件编译指令,用于判断是否已经包含了该头文件。如果没有包含,则继续编译下面的内容,否则跳过整个文件。
    #define __GY86_H: 定义了一个宏,用于防止头文件的重复包含。
    最后,#endif表示条件编译结束的标志,与#ifndef对应。整个头文件的作用是声明了GY86模块的初始化函数和数据获取函数,并包含了一些必要的头文件和变量声明,以便在其他源文件中使用GY86模块。
    3.5 蓝牙模块的实现
    蓝牙模块的实现大致由两部分:蓝牙模块分别与下位机、上位机通信。蓝牙模块使用USART串口通信协议与STM32主控板进行交流。经过对USART通信协议的学习,我们采取标准外设库函数来实现下位机蓝牙模块;经过斟酌,我们选择使用手机与电脑作为上位机接收端。
    3.5.1 ATK-BLE01 初始化与底层驱动函数的实现
    3.5.1.1 USART串口通信初始化
    通过查找 DataSheet-f401re 中的引脚定义表,我们选择了板载通信外设USART1作为通信接口,其对应USART1_TX引脚是PA_9。
    notion image
    根据目前的需求分析,只需要使用TX引脚即可。在选择USART串口时有一个需要特别注意的细节:ST公司的Nucleo-64开发板已经预先使用USART2的TX(PA2)与RX(PA3)串口作为烧录、调试功能。若将这两个GPIO口另做他用,将导致后续无法正常烧录!
    图3-27 DataSheet-引脚定义表截图
    因此,初始化GPIO口的步骤大致为:打开对应时钟、选择引脚、配置输出模式、AFIO引脚复用至USART2。随后进入到USART2外设的初始化。
    代码3-18 USART串口初始化
    void BLE_Init(void)
    {
    // 打开时钟
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOC, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
    // 初始化GPIO口
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; // GPIOA的第9个引脚
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; // 复用功能
    GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; // 推挽输出
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 输出速度50MHz
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; // 上拉电阻
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    GPIO_PinAFConfig(GPIOA, GPIO_PinSource9, GPIO_AF_USART1); // 配置引脚为USART1的复用功能
    // 初始化USART串口
    USART_InitTypeDef USART_InitStructure;
    USART_InitStructure.USART_BaudRate = 115200; // 波特率115200
    USART_InitStructure.USART_WordLength = USART_WordLength_8b; // 数据位长度8位
    USART_InitStructure.USART_StopBits = USART_StopBits_1; // 停止位1位
    USART_InitStructure.USART_Parity = USART_Parity_No; // 不使用奇偶校验
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; // 不使用硬件流控制
    USART_InitStructure.USART_Mode = USART_Mode_Tx; // 仅发送模式
    USART_Init(USART1, &USART_InitStructure);
    USART_Cmd(USART1, ENABLE); // 使能USART1
    }

    3.5.1.2 USART发送字节函数封装
    我们组决定将标准库函数中的USART_SendData()封装为USART1_SendByte()函数,以为后续中间层提供一个简单清晰的接口。并根据USART通信原理。
    notion image
    我们在封装中添加了对TDR寄存器中发送标志位的判断 ,以进一步提升系统的稳健性。下图是F4系列芯片的USART外设寄存器结构图,在PWDATA数据总线上的数据会先以8bit为单位被置入TDR寄存器,随后TDR将把其中的数据以此送至Transmit Shift Register寄存器,以1bit为单位送出。TDR和RDR在硬件上是同一个寄存器,他们以影子寄存器的方式为USART串口通信提供缓冲功能。
    图3-28 USART外设结构示意图
    代码3-19 USART串口发送一个字节
    void USART1_SendByte(uint8_t Byte)
    {
    USART_SendData(USART1, Byte); // 发送一个字节的数据到USART1
    while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
    // 等待TDR中的数据完全移出移位寄存器,即等待发送完成
    }

    3.5.2 蓝牙模块应用层函数的封装
    基于目前的需求分析与模块设计,该模块暂时不需要中间层提供更多扩展功能。因此在这里可以直接调用底层对外的发送字节接口来书写多样化的蓝牙发送数组、蓝牙发送字符串和基于蓝牙的printf函数,并将这些函数声明在头文件ATKBLE01.h头文件中,以对外提供简洁易用的蓝牙发送接口。
    3.5.2.1蓝牙发送数组
    简单的循环调用串口发送字节函数,这里的的设计是发送类型为uint8_t的数组。
    代码3-20 蓝牙发送数组
    void BLE_SendArray(uint8_t *Array, uint16_t Length)
    {
    uint16_t i;
    for (i = 0; i < Length; i++)
    {
    USART1_SendByte(Array[i]); // 逐个字节发送数组中的数据
    }
    }

    3.5.2.2 蓝牙发送字符串
    在C语言中,字符串的本质是字符数组。简单的若干循环调用串口发送字节函数即可。
    代码3-21 蓝牙发送字符串
    void BLE_SendString(char *String)
    {
    uint8_t i;
    for (i = 0; String[i] != '\0'; i++)
    {
    USART1_SendByte(String[i]); // 逐个字节发送字符串中的字符
    }
    }

    3.5.2.3 蓝牙printf函数的实现
    为了降低用户在使用蓝牙发送数据前的学习成本,我们按照printf函数的形式封装了蓝牙输出函数BLE_Printf,这样一来,使用蓝牙模块的语法就与使用printf函数一样简单。
    代码3-22 蓝牙printf
    void BLE_Printf(char *format, ...)
    {
    char String[100]; // 创建一个用于存储格式化后字符串的字符数组,长度为100
    va_list arg; // 定义一个参数列表变量
    va_start(arg, format); // 初始化参数列表,以format参数为起点
    vsprintf(String, format, arg); // 将格式化的字符串存储到String数组中,使用传入的format和arg参数
    va_end(arg); // 结束参数列表
    BLE_SendString(String); // 调用BLE_SendString函数,将格式化后的字符串发送
    }

    3.5.2.4书写头文件AKTBLE01.h
    蓝牙模块对外提供的接口仅四个输出函数,我们只需要将这四个函数声明在该头文件里,就可以被引用了该头文件的模块调用了。
    代码3-23 ATKBLE01.h
    #ifndef __ATKBLE01_H
    #define __ATKBLE01_H
    /* Includes ------------------------------------------------------------------*/
    #include "stm32f4xx.h"
    #include <stdio.h>
    #include <stdarg.h>
    /* Publi Functions ------------------------------------------------------------------*/
    void BLE_Init(void);
    void BLE_SendArray(uint8_t *Array, uint16_t Length);
    void BLE_SendString(char *String);
    void BLE_SendNumber(uint32_t Number, uint8_t Length);
    void BLE_Printf(char *format, ...);
    #endif

    3.6 接收机的实现
    接收机是遥控器和stm32f401开发板之间连接的桥梁,通过接收机将遥控器发送来的信号进行解析,转换为开发板能处理的信息格式,开发板利用处理后的信息便可以进行控制电机转速等操作。实现接收机主要利用的是TIM定时器的输入捕获功能和中断方式。
    3.6.1 底层初始化函数
    在接收机的设计中提到,PPM信号相邻上升沿之间的时间间隔为一个通道pwm波的高电平的持续时间。因此,如果上一个上升沿后CNT值清零,那么,当引脚检测到下一个上升沿时,此时CNT中的值就代表了一个通道高电平的持续时间,我们便可依据这个数值来调节该通道对应电机的转速。
    notion image
    我们就沿着这个思路,采用TIM定时器的输入捕获功能,CNT一直处于向上计数的状态,当边沿检测器检测到上升沿后,立即将CNT的值锁存到CCR2寄存器中,并且触发从模式的复位功能,将CNT清零,同时提出中断请求,进入中断服务程序,将CCR2中的值读出来,存放到数组中,用于主程序的读取。这样我们就实现了一个通道数据的采集,按照这个流程,我们就能采集到八个通道包括起始信号的数据信息,完成遥控器数据的接收过程。
    图3-29 接收机底层初始化流程图
    代码编写主要分为两个部分,分别是TIM定时器的初始化和中断服务程序的编写,下面是整个接收机的详细设计和实现流程。
    3.6.1.1 GPIO初始化配置
    首先打开TIM2和GPIOA的时钟,将与TIM2 CH2对应的GPIO口初始化为复用输入模式,用于接收引脚的PPM信号,接口速度选择high speed;同时配置AFR寄存器,进行引脚重映射,将引脚定义映射到TIM2定时器的通道二。
    打开GPIOA的时钟,对应RCC的APB1ENR寄存器末位赋值为1,同理操作AHB1ENR寄存器打开TIM2的时钟。
    notion image
    图3-30 GPIO_AHB1ENR寄存器
    notion image
    接下来配置GPIOA PA1,通过对2-3位赋10,将PA1设置为复用模式,用于接收PPM信号。
    图3-31 GPIO_MODER寄存器
    配置OSPEEDR寄存器到high speed模式,实现对端口输出速度的配置。
    由于,stm32f401开发板的外设引脚使用时存在引脚重映射,我们还需要配置AFR寄存器,TIM2 CH2是PA1复用AF1,所以我们对AFRL低位寄存器的相应位赋值,实现复用功能。
    代码3-24 GPIO初始化
    RCC->APB1ENR |= 1; //打开TIM2时钟
    RCC->AHB1ENR |= 1; //打开GPIOA时钟
    GPIOA->MODER |= 1<<3; //复用模式
    GPIOA->OSPEEDR |= 3<<2; //high speed
    GPIOA->AFR[0] &= ~(0xF<<4);
    GPIOA->AFR[0] |= 1<<4; //复用AF1

    3.6.1.2TIM定时器配置
    我们选择的是TIM2的通道二,选择上升沿触发捕获,使能了CCR2的中断捕获寄存器,用于发出中断请求。此外,配置主从触发模式,选择从模式清零,这样每进行一次捕获后,会自动清零CNT便于进行下一次捕获。
    配置流程如下:
    首先是时基单元的初始化。配置自动重装寄存器ARR,将其值设置为65536-1,表示CNT理论最大计数值,超过这个值将会溢出,触发更新事件,CNT重新从0开始计数;配置预分频寄存器,值设为16-1,表示对时钟频率的分频;将CNT设置为向上计数,这样在时钟频率为16MHz的情况下,实际频率为1MHz,CNT每1微秒进行加1操作,对于PPM信号来说,相邻两个上升沿的时间间隔为3500微秒到11500微秒,CNT最大计数值只会达到11500,并不会溢出。
    其中TIM的CR1寄存器的第四位0表示向上计数,1表示向下计数,因为它的初始值为0,所以我们不需要对它进行配置;其他时基单元的配置直接对相应寄存器写入对应二进制值即可。
    notion image
    图3-32 TIM_CR1寄存器
    notion image
    然后是输入捕获通道的初始化。配置CCMR1寄存器的8-9位为01,将输入捕获通道二配置为输入模式,输入引脚映射到 TI2 上;
    图3-33 TIM_CCMR1寄存器
    notion image
    配置SMCR寄存器4-6位为110,选择滤波后TL2FP2信号源;配置SMCR寄存器0-2位为100,表示选择从模式的复位模式,这样每次边沿检测器检测到上升沿后,就会触发从模式,清零CNT,便于进行下一次捕获。
    图3-34 TIM_SMCR寄存器
    notion image
    由于我们还需要中断服务程序的处理,还需要使能有关中断。配置DIER寄存器第2位为1,使能CCR2的捕获中断,在它捕获到数据时,便可以提出中断请求到NVIC进行处理。
    图3-35 TIM_DIER寄存器
    最后配置CCER寄存器第四位为1,使能比较捕获寄存器二,让该寄存器能够进行工作
    这样,在CNT不断的向上计数过程中,一段PPM信号传来,边沿检测器一检测到上升沿,就会立即将CNT的值锁存到CCR2比较捕获寄存器二,同时触发主从触发模式,清零CNT,进行下一次上升沿检测;然后,CCR2提出中断请求,NVIC处理后转入相应的中断服务程序的执行。代码如下:
    代码3-25 TIM定时器初始化
    TIM2->ARR |= 65536-1;
    TIM2->PSC |= 16-1;
    TIM2->SMCR |= 6<<4; //TL2FP2
    TIM2->SMCR |= 1<<2; //复位模式 内部时钟源
    TIM2->CCMR1 |= 1<<8; //IC2映射到TL2上
    TIM2->CCMR1 |= 16<<12; //0xF滤波
    TIM2->CCER |= 1<<4; //使能捕获寄存器
    TIM2->DIER |= 1<<2; //捕获中断使能

    3.6.1.3NVIC中断控制器配置
    划分中断优先组,选择二分组,将TIM2中断源的抢占优先级配置为2,响应优先级配置为1,中断源选择TIM2_IRQn,配置完成后使能NVIC和TIM2。
    notion image
    对于优先级分组,我们配置SCB的AIRCR寄存器,将其设为二分组;然而AIRCR寄存器存在写保护,我们首先需要在31-16位赋值0x05FA才能对该寄存器进行写操作。
    图3-36 AIRCR寄存器
    配置优先级分组时,参考下方的表格,对10-8位赋值为101表示二分组。抢占和响应优先级都有4种等级。
    在NVIC的IPR寄存器中,配置TIM2_IRQn28号中断源对应的位置来设置其优先级。我们将IPR[28]的7-6位赋值为10,表示抢占优先级为2;5-4位赋值为01,表示响应优先级为1。
    最后,配置ISER[0]寄存器的第28位为1,来使能NVIC处理28号TIM2_IRQn中断源,并开启中断。这样在收到中断请求后,通过NVIC的处理,便可执行相应的中断服务程序。初始化的最后,使能TIM2,完成整个初始化流程。
    代码如下:
    代码3-26 NVIC中断配置
    SCB->AIRCR = 0x05FA0000 | 0x500; //划分优先组 NVIC_PriorityGroup_2
    NVIC->IP[28] = 0x9 << 4; //抢占优先级2,响应优先级1
    NVIC->ISER[0] = 1 << 28; //开启NVIC中断
    TIM2->CR1 |= 1; //TIM2使能

    3.6.2 输入捕获中间层函数
    这一部分的中间层函数与之前的函数很不同。收取接收机信号需要利用到“外部中断”的方法,而编写中断服务程序时,其函数名、可见性与接口均是由ST公司的启动文件决定。
    在中断服务程序中,首先判断标志位,是否是CCR2发生输入捕获产生的中断标志。
    接着,读取CCR2的值,并进行判断是否在起始信号的大小范围,如果在,则开始或重新从遥控器通道1开始计数;如果不在,则代表PPM的一个数据帧还未接收完,继续依次接收各个通道值,将它们存放在数组CH2[]中。
    代码3-27 中断服务程序
    void TIM2_IRQHandler(void)
    {
    if ((TIM2->SR & (1<<2))!=0)
    {
    uint16_t CCR2 = TIM2->CCR2;
    //判断是否为起始信号
    if((CCR2 >= 3500) && (CCR2<= 11500)){
    okay2 = 1;
    i2=0;
    }
    //依次存放各个通道数据
    if(okay2==1){
    CH2[i2] = CCR2;
    i2++;
    if(i2>=9){
    okay2 = 0;
    }
    }
    TIM2->SR &= (uint16_t)~0x4;
    }
    }

    这样,我们就可以实现对遥控器通道信号的数据进行捕获,将接收机输出端与GPIO口相连接,每当上升沿来时,触发中断,捕获CNT寄存器里的值到CCR2,并存放到相应的数组位上,其值代表相应通道的高电平持续时间。同时,触发从模式reset将CNT清零,开始下一个通道数据捕获,如此循环往复。最终,我们便可以根据数组中的值来驱动电机,调节电机转速。
    notion image
    以下图是stm32处理接收机送出的PPM信号的流程图,是对上述文字的图形化总结:
    图3-37 stm32处理PPM信号流程图
    3.6.3 接收机模块对外接口封装
    该模块中间层封装的中断服务程序性质特殊,是由硬件自动触发,并不能被封装为对外调用的接口。因此,我们选择了通过一个数组CH2和一个标志变量okay2来实现接收机模块与其他模块的信息交互;此外,对外提供了初始化接收机的接口REC_Init2(void)。
    代码3-28 头文件Receiver.h
    #ifndef __RECEIVER_H
    #define __RECEIVER_H
    /* Includes -------------------------------------------------------------*/
    #include "stm32f4xx.h"
    /* Pubilic Variables ----------------------------------------------------*/
    extern uint16_t CH2[9];
    extern uint16_t okay2;
    /* Public Functions -----------------------------------------------------*/
    void REC_Init2(void);
    #endif

    3.7 驱动电机的实现
    notion image
    根据第一轮螺旋开发的需求分析,我们需要实现“遥控器控制电机转速”的功能,就需要我们实现“输出某一占空比的PWM波”函数,再进行封装,对电机模块外提供“调节电机转动”这一简单、易用的接口即可。
    图3-38 电机驱动硬件示意图
    控制各电机转速是四轴飞行器最关键的核心问题。在可预见的未来,该模块会进行相当多次迭代,因此各层代码的开发规范就显得尤为重要,以便于未来遥控器和实时姿态测算对电机的共同自动化控制。
    3.7.1 TIM定时器 初始化与底层驱动函数的实现
    在STM32主控板上输出PWM波的关键是TIM定时器中的“输出比较”功能。经过斟酌,我们选择了TIM3作为生成PWM波的定时器。
    3.7.1.1GPIO口初始化
    查阅手册得知其四个输出通道的引脚为:PA6、PA7、PB0、PB1。根据需求,配置为复用推挽输出。
    代码3-29 电机驱动的GPIO口初始化
    //开启对应时钟
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);
    //初始化PA6PA7
    GPIO_InitTypeDef GPIO_InitStructure;//定义了一个GPIO_InitTypeDef类型的结构体变量,用于存储GPIO的配置信息。
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;//将GPIO_InitStructure结构体中的GPIO_Pin成员设置为PA6和PA7,表示要配置这两个引脚。
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;//将GPIO_InitStructure结构体中的GPIO_Mode成员设置为GPIO_Mode_AF,表示将引脚配置为复用功能。
    GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;//将GPIO_InitStructure结构体中的GPIO_OType成员设置为GPIO_OType_PP,表示将引脚配置为推挽输出。
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;//将GPIO_InitStructure结构体中的GPIO_Speed成员设置为GPIO_Speed_100MHz,表示引脚输出速度为100MHz。
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;//将GPIO_InitStructure结构体中的GPIO_PuPd成员设置为GPIO_PuPd_UP,表示启用上拉电阻。
    GPIO_Init(GPIOA,&GPIO_InitStructure);//根据GPIO_InitStructure的配置信息初始化GPIOA的引脚。
    GPIO_PinAFConfig(GPIOA,GPIO_PinSource6,GPIO_AF_TIM3);//配置GPIOA的引脚PA6的复用功能为TIM3。
    GPIO_PinAFConfig(GPIOA,GPIO_PinSource7,GPIO_AF_TIM3);//配置GPIOA的引脚PA7的复用功能为TIM3。
    //初始化PB0PB1
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;//同上
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
    GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
    GPIO_Init(GPIOB,&GPIO_InitStructure);
    GPIO_PinAFConfig(GPIOB,GPIO_PinSource0,GPIO_AF_TIM3);
    GPIO_PinAFConfig(GPIOB,GPIO_PinSource1,GPIO_AF_TIM3);

    3.7.1.2 TIM3通用计时器初始化
    notion image
    下图是TIM定时器的基本结构。输出PWM波的关键是图3-39中标号1、2、3所指示的ARR自动重装寄存器、CNT自动计数器和CCR比较寄存器。
    图3-39 捕获/比较通道示意图
    CNT寄存器会以时钟频率为基础自动从0开始自增;ARR寄存器中储存了自动计数的上限值,一旦CNT自动计数到ARR上限值后,就会将CNT清零;CCR寄存器中存储了一个0~ARR的值,它会与CNT的值实时进行比较,当比较结果为“小于”、“等于”或者“大于”时,会触发一系列动作。
    配置“CCR比较寄存器”的触发动作是实现输出PWM波的关键。TIM通用定时器的有几个设定好的触发动作,如下表所示。
    表3-3 输出比较模式
    模式
    描述
    冻结
    CNT=CCR时,REF保持原状态
    匹配时置有效电平
    CNT=CCR时,REF置有效电平
    匹配时置无效电平
    CNT=CCR时,REF置无效电平
    匹配时电平翻转
    CNT=CCR时,REF电平翻转
    强制为无效电平
    CNT与CCR无效,REF强制为无效电平
    强制为有效电平
    CNT与CCR无效,REF强制为有效电平
    PWM模式1
    向上计数:CNT<CCR时,REF置有效电平,CNT≥CCR时,REF置无效电平 向下计数:CNT>CCR时,REF置无效电平,CNT≤CCR时,REF置有效电平
    PWM模式2
    向上计数:CNT<CCR时,REF置无效电平,CNT≥CCR时,REF置有效电平 向下计数:CNT>CCR时,REF置有效电平,CNT≤CCR时,REF置无效电平
    为实现目标,我们选择“PWM模式1”。这样依赖,只需要通过设定TIM3时钟频率和CCR、ARR寄存器的值,以取得期望的CNT的计数频率和CCR与ARR的比值即可输出确定周期、高电平时长的一定占空比的PWM波。
    查阅电调说明书,可知:PWM波周期需要为20ms;高电平时长2ms代表满油门驱动电机;高电平时长1ms代表油门为0,电机不转动。在我们的工程中,时钟树为TIM3所在的APB1总线提供的基准频率是16MHz,我们配置为1/160分频和ARR计数上限2000(CNT每过10us自增一次),CCR的值供后续接口调用时配置。
    代码3-30 TIM定时器初始化
    //初始化TIM3的时基单元,PWM周期为20ms,即50Hz频率.每0.02秒计数2000下
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
    TIM_TimeBaseStructure.TIM_Prescaler = 160 -1; //配置预分频系数
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //向上计数
    TIM_TimeBaseStructure.TIM_Period = 2000 -1; //设定ARR的值
    TIM_TimeBaseStructure.TIM_ClockDivision = 0;
    TIM_TimeBaseInit(TIM3,&TIM_TimeBaseStructure);
    //初始化TIM3CH1~4的PWM输出模式
    TIM_OCInitTypeDef TIM_OCInitStructure;
    TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; //选择CCR的触发动作
    TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
    TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
    TIM_OC1Init(TIM3,&TIM_OCInitStructure);
    TIM_OC2Iit(TIM3,&TIM_OCInitStructure);
    TIM_OC3Init(TIM3,&TIM_OCInitStructure);
    TIM_OC4Init(TIM3,&TIM_OCInitStructure);
    //使能预装载器
    TIM_OC1PreloadConfig(TIM3,TIM_OCPreload_Enable);
    TIM_OC2PreloadConfig(TIM3,TIM_OCPreload_Enable);
    TIM_OC3PreloadConfig(TIM3,TIM_OCPreload_Enable);
    TIM_OC4PreloadConfig(TIM3,TIM_OCPreload_Enable);
    TIM_ARRPreloadConfig(TIM3,ENABLE);
    TIM_Cmd(TIM3,ENABLE);

    以上两段代码综合起来,构成函数PWM_Init
    3.7.1.3 CCR值的设定
    经过上述基准设置后,CCR的值成为了确定高电平时长以及占空比的唯一因素。根据需求分析,目前需要将CCR的值配置为模块内接口,以供电机模块更高层用例函数方便的调用。
    使用STM官方标准外设库函数即可方便的对TIM3定时器四个通道的CCR进行配置。目前仅有一次性配置全部电机的需求,带后续学期再添加单独调度每个通道的函数。
    代码3-31 设置CCR寄存器的值
    void PWM_SetCompareAll(uint16_t Compare)
    {
    TIM_SetCompare1(TIM3, Compare);
    TIM_SetCompare2(TIM3, Compare);
    TIM_SetCompare3(TIM3, Compare);
    TIM_SetCompare4(TIM3, Compare);
    }

    3.7.2 电机驱动 对外接口的封装
    根据本学期的需求分析,暂无中间层函数提供更多丰富的功能,后续学期会在中间层添加如加速度控制、位置控制、电流控制等函数,以便与PID反馈控制功能的开发。现在仅对模块外提供电机初始化与设定所有电机的油门两个接口。
    3.7.3.1电机初始化
    每次为电机接通电源时,都需要解锁电机:持续输出高电平时长为1ms、周期为20ms的PWM波,直到听到电机发出三次音调渐高的“滴”声
    代码3-32 电机初始化
    void Motor_Init()
    {
    PWM_Init(); //初始化PWM波产生模块
    PWM_SetCompareAll(100);//设定CCR的值为100,代表高电平时长10ms
    Delay_ms(5000);//延时5s
    }

    3.7.3.2设定所有电机的油门
    需求的PWM波的高电平时长为1ms~2ms;CCR寄存器的值为100~200;按照用户习惯与行业规范,油门范围应是0~100。所以设计变量speed,合法的取值范围是0~100;该变量加上100后再设置为CCR寄存器的值。
    代码3-33 设置电机油门
    void Motor_SetSpeed_All(uint8_t speed)
    {
    speed = speed < 100 ? (speed > 0 ? speed : 0) : 100; //检查输入是否合法!若超出边界值则设置为边界值。
    PWM_SetCompareAll(speed+100);
    }

    3.7.3.3书写头文件Motor.h
    该文件提供两个接口:电机初始化与设定所有电机的油门。
    代码3-34 头文件Motor.h
    #ifndef __MOTOR_H
    #define __MOTOR_H
    /* Includes ------------------------------------------------------------------*/
    #include "stm32f4xx.h"
    #include <stdio.h>
    #include <Delay.h>
    /* Defines -------------------------------------------------------------------*/
    /* Public Functions ------------------------------------------------------------------*/
    void Motor_Init();
    void Motor_SetSpeed_All(uint8_t speed);
    #endif

    #ifndef __MOTOR_H: 条件编译指令,如果__MOTOR_H未定义,则编译以下代码,否则跳过。
    __MOTOR_H: 自定义的宏定义,用于避免重复包含头文件。
    #define __MOTOR_H: 定义__MOTOR_H宏,用于标记该头文件已被包含。
    /* Includes ------------------------------------------------------------------*/: 注释,指示以下是包含的头文件部分。
    #include "stm32f4xx.h": 包含stm32f4xx.h头文件,该头文件包含了与STM32F4系列微控制器相关的定义和函数声明。
    #include <stdio.h>: 包含stdio.h头文件,该头文件包含了输入输出相关的函数声明。
    #include <Delay.h>: 包含Delay.h头文件,该头文件包含了延时函数的声明。
    注释:在这个部分可以添加你自己的宏定义,用于定义常量、标志位等。
    /* Public Functions ------------------------------------------------------------------*/: 注释,指示以下是公共函数声明部分。
    void Motor_Init();: 声明了一个名为Motor_Init的函数,无返回值和参数。
    void Motor_SetSpeed_All(uint8_t speed);: 声明了一个名为Motor_SetSpeed_All的函数,无返回值,参数为一个uint8_t类型的速度值。
    #endif: 条件编译指令结束标志,表示结束条件编译块。
    3.8 PCB转接板的实现
    PCB转接板的实现是将前期硬件设计整合为一个系统的关键步骤。设计合理的转接板可以使机架、电池、F401主控板、外设模块等能通过螺丝、排母、针脚等优雅、牢固、安全的方式相互连接。对于转接板,我们可以使用Altium Designer软件自行绘制,其中需要查出主控板的长宽、及两处外设排针的间距等数据,并了解各个引脚对应的功能进而进行设计。绘制转接板的整个流程可以分为绘制转接板原理图和绘制转接板pcb图两大部分。下面是转接板的设计细节与实现过程:
    3.8.1 原理图的绘制
    3.8.1.1导入或绘制原理图库
    首先我们需要获取基本元件的原理图,这一步可以从厂商或开源库中直接选择合适的部件来导入元件符号,在库中筛选到所需的元件后,下载并载入到AD中后即可使用;然而当所需的元件过于特殊而无法搜寻到时,或者因为开源库过于宏大而使搜查无异于大海捞针时,也建议通过自行手工绘制来,寥寥几笔即可勾勒出符合需求的元件原理图,既可排解搜寻无果的郁闷之气,亦可节省无意义的时间开销、提高设计效率。
    notion image
    绘制时,首先需要用一块矩形高度抽象地概括元件的主体,再添加与实际元件引脚数目相匹配的针脚,同时为针脚标注名称及序号,以方便常态化连接的便携。而有效的库管理和符号标注,总体上也有利于高效创建符合标准的原理图,提高整体设计效率。
    图3-40 元件库示意图
    3.8.1.2分配引脚
    原理图设计的第二个步骤即是需要明确各模块组件与主控板的具体连接方式。先根据stm32参考手册及蓝牙等模块的数据手册,深入了解各模块的各个引脚的相应功能,再根据硬件系统上各组件的实际连接需求,将功能需求映射到芯片引脚上,使外设模块的功能实现所需引脚与主控板外设排座上的引脚进行一一对应的匹配。
    匹配过程中也会出现引脚匹配出现冲突的问题,即两个模块的引脚需要连接到主控板排座的同一位置,而若进行此类操作的话,则可能会有电源分配等种种情况,可能会威胁到整块转接板功能实现的稳定性,所以最好的措施还是排针的一个位置仅仅匹配外设模块的一个针脚。例如设计之初,OLED需连接+3V3针脚,但GY-86亦需要连接该针脚,两者的连接产生了冲突,而经过测试,发现OLED也可以通过5V来供电,因此就将OLED的VCC针脚调换到与E5V针脚相连。
    表3-4 引脚分配情况
    模块
    引脚分配
    功能
    GY-86
    PB8
    H_SCL
    PB9
    H_SDA
    GND
    接地
    +3V3
    供电3.3V
    OLED1
    PC6
    SCL
    PC5
    SDA
    +5V
    供电5V
    GND
    接地
    OLED2
    PC0
    SCL
    PC1
    SDA
    E5V
    VCC
    GND
    接地
    接收机
    PA1
    PPM
    E5V
    供电5V
    GND
    接地
    蓝牙
    PA9
    RXD
    PA10
    TXD
    GND
    接地
    3V3
    供电3V3
    电机x4
    PA6
    1号电机pwm
    PA7
    2号电机pwm
    PB0
    3号电机pwm
    PB1
    4号电机pwm
    GND
    接地
    XT30PW
    P
    连接开关
    GND
    接地,作为负极
    开关
    P
    接电源正极
    E5V
    给E5V引脚供电
    GND
    接地
    3.8.1.3电气连接
    notion image
    按照经过验证了的引脚分配方案,再将各个模块的引脚精确连接到所匹配的相应的管脚上。在绘制时,有两种引脚连接方式,其一是直接放置引线,将两处连接点精准相连,如转接板-下中的电机M的pwm引脚与R1、R2两电阻直接相连;其二则是放置网络标签,在两处连接点放置同样名称的网络标签,及可隐性地连接两引脚,同时也保证了原理图整体上的整洁性,并且提高了辨识度,使得连接关系一目了然。而通过遵循预定的引脚分配计划,确保每个模块的引脚正确
    图3-41原理图示意图
    连接后,即实现了电气连接的准确性,确保了整个系统在设计规范内正常运行,达到预期的功能和性能。
    3.8.2 PCB图的绘制
    3.8.2.1导入或绘制PCB库
    该步骤的大体操作与“导入或绘制原理图库”相似,故不再赘述。
    而若需要进行元件封装的绘制,首先描绘主体框架以及封装的边界线,再在边界内按照需求及软件绘制标准来放置相应的焊盘,在进行上述的操作时,也应时刻注意元件实际的物理模型的各种数据,以免制作的封装与原组件不匹配而无法连接。而尽量细致入微的设计和考虑,有助于确保电路板的可靠性、准确性和良好的性能。
    notion image
    图3-42 导入PCB示意图
    3.8.2.2 原理图导入为PCB
    在原理图设计完成后,紧接着的步骤是将原理图文件无缝导入到PCB设计工具中。这一关键步骤旨在保障电路连接的准确性和一致性。在进行这个过程时,不仅要确保原理图与PCB之间的信息一致,还需要将原理图中的各个连接关系精准映射到PCB图中。这涵盖了元件的位置、引脚的对应关系以及电路的逻辑结构等方面。这一映射过程不仅提供了设计的一致性,也为后续的PCB布局阶段提供了坚实的基础。
    3.8.2.3测量尺寸与转接板的形状绘制
    在PCB设计中,明确定义转接板的物理形状和边界非常重要。通过准确勾勒转接板的轮廓,可以在物理空间内按照目的需求规划模块的布局,为最佳性能和可维护性提供基础,也为后续的模块布局和布线提供了清晰的设计框架。
    图3-43 转接板尺寸设计图
    尺寸的测量十分重要。底层板依靠两颗螺丝与机架相连,一旦尺寸有误,将直接导致巨大的安装问题。因此,我们在设计好底层板后,我们讲设计图纸导出为pdf格式,并使用打印机 1:1 打印在A4纸上进行测量。
    图3-43 尺寸测试
    3.8.2.4模块的布局
    notion image
    将各个模块合理地放置到PCB上并进行规划布局。首先要安排的是两个外设排座的位置,在经过精确的计算及测试后,将两者分别对称地放置整个板子的两侧,并且要为转接板上下两方的模块放置留足空间。其中很关键的是要通过软件内的测量距离的功能测量两者的垂直距离,进而来判断两排座是否处于同一水平线上。其余的模块则根据开槽位置、外形设计、布线的便捷性等需考虑的因素来进行布位。通过合理安排模块的位置,可以最大程度地减少各部件之间的互相干扰,优化信号传输路径,提高整体系统的可靠性。
    图3-44 下层转接板设计图
    3.8.2.5连线、滴泪和覆铜
    连线阶段是将各个模块的引脚精确连接起来,以确保电路的正确连接和功能正常,需要考虑信号的传输路径,避免交叉干扰和阻抗不匹配。这个过程中,引脚的物理位置和走线的走向至关重要,直接影响信号传输的效果。同时也要考虑外貌的美观程度,确保排线的规律不混乱。
    滴泪操作则在连接处添加了适量的焊膏,以加强焊点的牢固性和可靠性。这对于提高电气连接的稳定性至关重要,尤其是在高频信号和复杂电路布局的情况下。
    覆铜阶段在PCB正反两侧的表面添加了一层铜覆盖,可以提供电气连接的均匀性和增强PCB的物理强度。并且也有助于减少电气信号的传输损耗,提高整个电路板的抗干扰能力,并且通过将需要接地的焊盘与覆铜十字相连的方式,使得接地的功能正常实现。同时,覆铜还有助于散热,确保电子元件在操作中保持适当的温度。
    3.8.2.6设计规则检查
    notion image
    在设计规则检查阶段,会审查电气连接、物理布局等方面的规则,以确保设计符合行业标准以及商家制板的要求。在这一阶段,任何潜在的错误都会被发现并及时修复,以保证设计的质量和可靠性。当检查报告登出后,即可根据特殊字符标记来找到需要修改的不规矩之处,进而进行修改。此时的错误一般都是焊盘与焊盘、焊盘与线路相离过近,进行微调即可。将所有错误修改完成后,再次进行设计规则检查,当报告中的Warnings与Rule Violations的总数为零时,即可停止修改,这也说明该pcb板在理论上设计成功了。
    图3-45 电气检查界面
    3.8.2.7下单打板
    在设计规则检查通过并确保没有设计错误后,可以导出原理图及PCB图,并联系嘉立创厂商沟通该pcb板的功能性及可实现性,并且还要检查该pcb板是否符合嘉立创制板的标准,排查出不足及漏洞后再次修改,确保无误后就可进行下单打板制作。
    打板完成后,完美符合预期效果和初期设计稿:
    notion image
    图3-46 设计草图与实物对比
    3.8.3 转接板历代版本
    3.8.3.1第一版设计
    第一次绘制转接板时,自然而然地,我们的想法是直接拟合原主控板的形状特点,将其设计为矩形,再将各组件模块较为有序整洁地排放在两处外设排座之间,打板及焊上各排座后,再将此板倒扣在主控板正面。
    由于绘制第一版转接板时,处于pcb设计相关技术的初学阶段,一是无累积的电路板pcb制作经验,一切的设计细节都在摸索探索,二是对AD软件的熟练度偏低,操作都较为生硬。因而第一代转接板设计得还是较为粗糙,有许多的漏洞及缺陷。
    具体缺陷体现在以下三点:
    1.引脚分配错误:由于当时的硬件系统开发还处在持续地研究进程中,部分模块的引脚连接方式还是不太能知晓清晰,因而引脚分配出现错误,导致转接板无法正常发挥其功能。
    2.遮挡复位按钮:设计之初并未充分考虑到主控板的外形特点,因而完全地把主控板中央的复位按钮遮挡住了,较为阻碍正常的功能使用。
    3.模块装载拥挤:转接板制作完成后,才发现各模块放置过于紧密,首先可能会妨碍模块与转接板的正常连接,进而使各模块功能无法实现,其次也降低了整体外观的美观性。
    4.缺失电源模块:此次设计仅考虑到了对几处所需的功能模块的安置,而未考虑到对整个系统的供电分配。
    图3-47 第一版PCB
    3.8.3.2第二版设计
    此版本在改正了上一版中的引脚分配错误等问题的基础上,进行了较大幅度的更改。经过充分探讨及精心设计后,第二版我们决定采取双层板设计策略,即将第一版转接板分成两部分来设计——上层板及下层板。上层板主要用于GY-86和一块OLED显示屏的转接,下层板主要用于电机、接收器、蓝牙、电源接口、电源开关、OLED显示屏等部件的转接。整个硬件系统的组装也因此更层次分明:先将主控板的背面与下层板相接,也就是将其扣在下层板之上,蓝牙、接收器等模块则连接在下层板正面的对应位置;再把上层板倒扣在主控板的正面,然后将显示屏和GY-86接在上层板上,也就是整个硬件装置的最上层。
    此次版本迭代变化较大,主要有以下一些进步点:
    1.避免遮挡:考虑到转接板对开发板重启按键的遮挡,我们在上层板的中上方开了方形的槽。
    2.底层板大开槽:为了方便与四个电机连接,下层板中央追加了两个大开槽,能让电机的线路从其中穿过,隐蔽线路而美观便捷。
    3.电源设计:吸取上一版设计的教训,第二版的下层板中添加上了电源分配接口,以实现对硬件系统中多个外设模块的供电,并且还配置了一个开关模块来控制整块转接板的电源接通或断流。
    4.双层设计:有效地解决了各外设模块过于拥挤的问题,使得转接板层面上的空间分配及模块放置更为合理,并且也使外设的连接更为便捷高效,也提升了整体外貌的美观性。
    图3-48 第二版PCB(上)
    notion image
    图3-49 第二版PCB(下)
    然而在收到厂家制作的下层板后,我们发现了一个巨大的功能缺陷:中央处的两大凹槽并未打孔打通。此状况与设计之初的设想并不相同,因此我们又进行了第三版的设计,力求功能的再优化。
    3.8.3.3第三版设计
    本次版本设计是基于第二版中下层板的设计来进行改进的。
    在仔细勘察以及与嘉立创客服的积极沟通后,我们甄别出了开槽未开孔的具体原因:嘉立创打板时的标准是先查看哪一层有完整外形就优先根据哪一层来进行外形制作,而一般都是优先按照机械层,其次再是禁止布线层。因此解决开槽未开孔的方案显而易见——将外形设计到同一层。但又因为事先不知晓该制板默认“潜规则”,第二版转接板的外形设计较为混乱,可能是处于不同的层次上而难以进行修改及重新整合,再多次纠正无果后,我们决定暂时放弃这个解决方法,牢记这个教训,再另寻其他的出路。
    于是在经过长达数个小时的网上搜寻及多次咨询学长后,我们找到了另外的方法:删除开槽处的boardcut,用焊盘的孔洞来代替开槽。这个方法初看之时好似有些不可思议,而细想之后,实则富有创意及可行性,焊盘的本质就是一个开孔,只要将其拉扯放大,改变形状,做成方孔,即可完美地实现中央大开槽这一目的。
    图3-50 第三板PCB
    但是在兴高采烈地提交新方案后,嘉立创客服又再次联系,提出了转接板制作上的技术性困难:锣刀锣不出方形槽,且过大的开孔在喷锡工艺下会爆孔,即开孔变形甚至致使功能失效。于是再经过又一轮的探讨后,我们又得到了解决方案:将方形槽改成圆角矩形或者是椭圆形,在把开孔改为无铜孔,这样就不会爆孔,完美地解决了所有问题。
    最后,为了整个飞行器的美观,我们又在下层板底部添加了八个小灯,其每一角都装置有两个小灯,并且它们的电源线路与电机模块的供电引脚相连接。这样设计使得电机模块的亮度能够随着电机转速的变化而相应地改变。
    至此,项目I阶段的pcb转接板设计就已大功告成!在打板制作之后,我们立马进行了对转接板的元件焊接等操作,将排座、排针、LED小灯及电阻等部件通过锡焊的方法进行焊接,或是贴片焊接,或是插件焊接,来将其固定在转接板上,并且保证元件与焊盘的正常、准确对接。焊接完成之后,我们将主控板及各外设模块与转接板正确连接,并一一测试了各需求的功能,发现均能正常运行,这也说明,第三版转接板的pcb设计是正确无误而十分精准有效的。
    3.8.4 PCB电路板的焊接
    notion image
    收到厂家发来的PCB板是相当激动的,但此时需要静下心来仔细焊接,并检查电气特性。此环节的安全性问题也十分需要重视。焊接时全神贯注,焊枪不乱放,人走即断电。
    图3-51 常见的焊接工具
    3.8.4.1焊接排针、排座与开关
    notion image
    在设计阶段,排针、排座与开关的封装是一致的——一个小圆孔,焊接的方法也如出一辙,它们都会在PCB板的背面露出一段针脚。在工作室取来一定数量的直排针、弯排针、直排座、弯排座后,首先将它们直接插在板子上,随后使用一些方法固定好待焊接的元件和PCB板(相当重要),将焊枪加热至350摄氏度后,先让焊枪与PCB板直接接触数秒,起到预热板材的作用。预热是为了防止融化的锡刚接触板材就凝固,加大焊接难度。随后将焊枪放置在两个针脚一旁,使用锡丝从两个针脚的中间接触焊枪,不到一秒,锡丝就会融化,此时需要迅速移走锡丝,并用焊枪不断在针脚之间涂抹。
    图3-52 排针的焊接
    3.8.4.2贴片LED与电阻焊接
    notion image
    在最终版的转接板上,设计有贴片LED,用来指示电机的工作状态。贴片焊接的难度较大,需要集中注意力,并多多练习。贴片焊接的方法是:将锡膏涂抹在PCB板的焊接区域;屏住呼吸,并使用镊子将元件放置在目标焊接区域;打开热风枪,设置风温为400摄氏度,对准目标区域风干一到两分钟,直到灰色的锡膏变为银色的焊锡;冷却。
    图3-53 贴片元件的焊接
    3.9 底层函数汇编化
    3.9.1 C语言与汇编语言的转换
    由于在ARM架构上,GCC编译器可以支持混合汇编的功能,所以我们可以将c语言代码快捷。
    GCC编译器通过使用内嵌汇编的方式支持混合汇编。内嵌汇编允许在C或C++代码中嵌入汇编指令,以实现对底层硬件的直接访问。
    在GCC中,内嵌汇编使用asm关键字来标识。在嵌入的汇编代码中,可以使用特定的语法来指定寄存器、操作数、指令和约束等。
    在配置好arm-gcc编译环境以后,打开cmd使用指令3-1即可生成对应的s文件,需要注意的是,如果c文件中使用了其他头文件,则需将其链接在指令后方。
    指令3-1 生成汇编文件
    arm-none-eabi-gcc -S -o GY86.s GY86.c

    GCC编译器在编译过程中会将嵌入的汇编代码嵌入到生成的目标文件中。这些目标文件可以通过连接器和其他对象文件一起生成可执行文件。
    在运行时,生成的可执行文件会被加载到ARM架构的目标设备上执行。ARM处理器会按照指令的顺序逐条执行其中的汇编指令。嵌入的汇编代码可以直接操作寄存器、内存和其他硬件资源。
    需要注意的是,混合汇编通常是与C或C++代码混合使用的,用于对性能关键的代码进行优化,或者处理底层硬件相关的操作。在编写混合汇编代码时,需要对底层的ARM架构和相关指令集有一定的了解,以确保正确性和性能优化。
    语言规则转化
    分析C语言代码:仔细阅读和理解C语言代码,包括变量的声明、函数调用、控制结构等。了解代码的逻辑和功能。
    选择要转换的部分:根据代码的目的和需求,选择需要转换为汇编的部分。通常,密集计算、关键循环、对硬件直接访问的部分是最常选择转换的区域。
    了解ARM体系结构:熟悉ARM体系结构和指令集。了解寄存器的用途、指令的操作方式以及编码规则。这将帮助你在汇编代码中正确使用寄存器和指令。
    编写汇编代码:根据C语言代码的逻辑和功能,在相应的位置编写等效的汇编代码。使用正确的寄存器和指令来实现所需的功能。在需要使用C变量的地方,可以使用内联汇编将其加载到寄存器中。
    代码3-35 //一些常见的汇编指令与对应关系
    LDR R1,=R2 ;将R2中的地址加载到R1中(正好与STR相反),R2一般是一个立即数
    STR R3,[R4] ;将R3寄存器中的值存在R4的地址中
    ORR R2,R0,R1 ;将R0与R1进行或运算,运算结果储存在R2中
    MOV R1,R2 ;将R2的值移动到R1中
    ADD、SUB、MUL、DIV:执行加法、减法、乘法和除法运算
    AND、OR、XOR:执行位逻辑与、或和异或运算
    CMP:比较两个值,并设置相应的标志位
    PUSH:将数据压入栈顶
    POP:从栈顶弹出数据
    LDM、STM:用于加载和存储多个寄存器的值
    BL 调用函数,进入别的分支

    与C代码交互:在汇编代码中,可以使用标签来表示跳转目标或数据地址。通过在C代码中使用内联汇编或使用外部汇编文件,将汇编代码与C代码进行交互,比如将寄存器中的结果存储到C变量中,或者调用C函数。
    函数参数传递:在c语言中,我们可以用关键字定义变量来存储参数,但是在汇编语言中,我们需要使用寄存器来存储参数,这引出了一个核心问题--“寄存器分配”,我如何知道哪个参数存在哪个寄存器里面?又如何使用呢?在学习编译技术之后,帮助我们理解参数传递和寄存器分配的知识,实质上我们通过线性回归算法,给活跃变量分配寄存器即可。在arm的架构体系中,R0-R3是用来存储参数的寄存器,R4-R15则用来存储变量和其他参数。对于一个需要外部输入参数的函数而言,如void MyIIC_SendACK(uint8_t AckBit),这里的AckBit由R0寄存器存储,为了后续使用时其值不会被改变,我们需要将R0入栈保存,同理,我们函数中在调用另外一个含参函数的时候,也需要采用这样的方法保护参数。此时我们还需要考虑一种情况,当我们的参数过多,寄存器不够用的时候,应该如何处理呢?这个时候我们想到了编译技术中函数的栈帧,我们同样可以在汇编代码中实现将溢出的参数存储在栈中的操作,在这个过程中使用到了sp寄存器。
    返回参数:在c语言中,我们使用return语句来返回我们所需要的值,汇编语言中,我们可以使用指令BX LR来实现以上操作。BX LR 指令的含意是将程序的控制权转移回调用函数的地方,即函数返回。它使用寄存器 LR(链接寄存器)中保存的地址作为跳转目标。LR 寄存器在函数调用时被用作返回地址的保存位置。
    具体执行过程如下:
    将 LR 寄存器中的地址加载到程序计数器(PC)中。PC 是存储下一条将要执行指令地址的寄存器,它决定了程序的控制流向。
    执行跳转到 LR 寄存器中保存的地址,即返回到调用函数的地方。这样,程序的执行流程将回到调用函数的下一条指令,继续执行。BX LR 指令通常用于函数的正常返回或异常处理时。在函数执行完毕后,通过执行 BX LR 指令返回到调用函数的位置,实现函数调用的返回。
    3.9.2 汇编语言实现用例
    实现中遇到的问题与解决方案:
    混合汇编:在最初配置好环境,准备尝试将c文件转换成s文件时,发现编译过程中出现了诸多奇奇怪怪的报错,经过一步步排查,发现是没有链接完头文件,以及头文件里面的一些声明与gcc-arm不兼容,但是由于我们开发时涉及到的库函数过多过复杂,最终还是没能混合编译通过,希望在下一学期的学习中找到这种方法的正确道路
    如何调用含参函数:由于不能在指令中直接写上我们要调用的函数将要使用哪些参数,在遇到含参函数时,汇编化成了一个难题。在搜查一些资料,询问ai后,我们得到了很多版本的答案,较为常见的说法是调用函数之前放入寄存器的参数,就会作为函数的参数去使用,当我们疑惑为什么的时候,编译技术的知识又派上了用场,因为函数使用的参数是此时活跃的变量,为了保护我们将要使用的参数,在使用他们之前,要进行入栈操作,使用完成以后出栈,这是从三地址码的转换处得来的灵感,成功应用了别的学科的知识
    如何给变量赋值:因为不能像c语言一样可以随意定义变量,所以我们使用寄存器来保存参数的值,这里要注意变量的活跃性,合理分配寄存器,用尽量少的寄存器完成更多的操作
    for循环:在汇编语言中,一个循环出现的标志是loop,但是在for循环中,有三个条件,应该如何在汇编中表达出来它的意思呢?首先我们需要一个寄存器来保存计数器i的值,然后在循环的结尾使用比较判断指令CMP,判定是否满足条件,再用分支指令跳转即可
    全局变量:c语言中可以定义一些全局变量,在不同的函数中都可以使用这些变量的值,但是在汇编语言里面,我们没有找到标准的全局变量使用方式,资料中的一些表示方法,如global关键字等,都会造成编译器报错,这是我们目前还没能解决的,希望在下学期的arm课程中能够解惑
    1. 参数传递
    代码3-36 基于Arm汇编指令实现的参数传递
    MyIIC_SendByte
    PUSH {LR} ; 保存返回地址
    MOV R2, #0 ; 初始化计数器 i = 0
    MOV R3, #0x80 ; 设置掩码,初始为 0x80
    Loop1
    LSR R1, R3, R2 ; 右移掩码 R3,次数为计数器 R2 的值,并将结果存储在寄存器 R1 中
    AND R1, R1, R0 ; 逻辑与操作,将 R1 和参数 Byte 进行按位与,并将结果存储在 R1 中
    BL MyIIC_W_SDA ; 调用 MyIIC_W_SDA 函数,将 R1 传递给函数
    MOV R1, #1
    BL MyIIC_W_SCL ; 调用 MyIIC_W_SCL 函数,将参数 1 传递给函数
    MOV R1, #0
    BL MyIIC_W_SCL ; 调用 MyIIC_W_SCL 函数,将参数 0 传递给函数
    ADD R2, R2, #1 ; 计数器 i 加 1
    CMP R2, #8 ; 比较计数器 R2 和立即数 8
    BNE Loop1 ; 如果不相等,则跳转到 Loop 标签处继续循环
    POP {PC} ; 恢复返回地址并返回

    1. 多参数调用入栈
    代码3-37 基于ARM汇编指令实现的GY86读取参数函数
    GY86_GetData
    PUSH {LR} ; 保存返回地址
    ; 读取加速度计X轴数据
    PUSH {R0, R1} ; 保存需要使用的寄存器
    ; 调用GY86_ReadRegister函数读取寄存器值
    LDR R0, =MPU6050_ADDRESS ; 将MPU6050地址加载到R0
    LDR R1, =MPU6050_ACCEL_XOUT_H ; 将加速度计X轴高位寄存器地址加载到R1
    BL GY86_ReadRegister ; 调用GY86_ReadRegister函数
    ; 将返回值存储到DataH变量中
    STRH R0, [SP, #4] ; 将R0的低16位存储到栈中的偏移4的位置
    ; 读取加速度计X轴数据的低位
    LDR R0, =MPU6050_ADDRESS ; 将MPU6050地址加载到R0
    LDR R1, =MPU6050_ACCEL_XOUT_L ; 将加速度计X轴低位寄存器地址加载到R1
    BL GY86_ReadRegister ; 调用GY86_ReadRegister函数
    ; 将返回值存储到DataL变量中
    STRH R0, [SP, #6] ; 将R0的低16位存储到栈中的偏移6的位置
    ; 将DataH和DataL合并为16位的AX值
    LDRH R0, [SP, #4] ; 从栈中的偏移4的位置加载DataH的低16位到R0
    LSL R0, R0, #8 ; 将R0的值左移8位
    LDRH R1, [SP, #6] ; 从栈中的偏移6的位置加载DataL的低16位到R1
    ORR R0, R0, R1 ; 将R0和R1进行按位或操作,并将结果存储到Original_Data_List->A
    POP {R0, R1} ; 恢复寄存器

    3.10 系统整合
    各个模块的代码编写完毕,经过系列层级的封装后,形成了数个可灵活调用的组件。在main 函数中即可根据事先设计好的用例,编写代码链接各个模块。本学期的任务是使用轮询来完成系统整合,下学期即将部署操作系统。
    在主程序中,先进行必要的初始化工作,包括姿态传感器、蓝牙模块、接收机以及电机对应模块的GPIO口和外设。然后,通过轮询机制,调用GY_86_GetData函数获取姿态传感器的实时数据,并利用蓝牙发送函数将这些数据实时传输至上位机。与此同时,将遥控器八个通道的数据值发送至上位机,并基于这些数据值调用电机转速设置函数,以精确调整或维持电机的转速。
    代码3-38 main函数
    /* Includes ------------------------------------------------------------------*/
    #include "main.h"
    static __IO uint32_t uwTimingDelay;
    RCC_ClocksTypeDef RCC_Clocks;
    int main(void)
    {
    /* System -------------------------------------------------------------------*/
    /*!< At this stage the microcontroller clock setting is already configured,
    this is done through SystemInit() function which is called from startup
    files before to branch to application main.
    To reconfigure the default setting of SystemInit() function,
    refer to system_stm32f4xx.c file */
    /* SysTick end of count event each 10ms */
    RCC_GetClocksFreq(&RCC_Clocks);
    SysTick_Config(RCC_Clocks.HCLK_Frequency / 100);
    /* Add your application code here */
    /* Insert 50 ms delay */
    Delay(5);
    /* Initials -------------------------------------------------------------------*/
    LD2_init();
    GY86_init();
    OLED_Init();
    OLED2_Init();
    BLE_Init();
    REC_Init2();
    Motor_Init();
    LD2_ON();
    while(1)
    {
    OLED_ShowSignedNum(1,1,GY86DataList.AX,5);
    OLED_ShowSignedNum(2,1,GY86DataList.AY,5);
    OLED_ShowSignedNum(3,1,GY86DataList.AZ,5);
    OLED_ShowSignedNum(1,8,GY86DataList.GX,5);
    OLED_ShowSignedNum(2,8,GY86DataList.GY,5);
    OLED_ShowSignedNum(3,8,GY86DataList.GZ,5);
    OLED_ShowSignedNum(4,1,GY86DataList.GaX,5);
    OLED_ShowSignedNum(4,1,GY86DataList.GaY,5);
    OLED2_ShowString(1,1,"Group3 LCCZD");
    OLED2_ShowNum(2,1,CH2[1],5);
    OLED2_ShowNum(2,8,CH2[2],5);
    OLED2_ShowNum(3,1,CH2[3],5);
    OLED2_ShowNum(3,8,CH2[4],5);
    OLED2_ShowNum(4,1,CH2[5],5);
    OLED2_ShowNum(4,8,CH2[6],5);
    BLE_Printf("Acc:%d-%d-%d\n",GY86DataList.AX,GY86DataList.AY,GY86DataList.AZ);
    BLE_Printf("G:%d-%d-%d\n",GY86DataList.GX,GY86DataList.GY,GY86DataList.GZ);
    BLE_Printf("CH[1]:%d CH[2]:%d",CH2[1],CH2[2]);
    BLE_Printf("CH[3]:%d CH[4]:%d",CH2[3],CH2[4]);
    BLE_Printf("CH[5]:%d CH[6]:%d",CH2[5],CH2[6]);
    BLE_Printf("CH[7]:%d CH[8]:%d",CH2[7],CH2[8]);
    Motor_SetSpeed_All((int)(CH2[3]/10)-100);
    }
    // LD2_OFF();
    // PWM_SetCompare1(140-1);
    }
    /**
    • @brief Inserts a delay time.
    • @param nTime: specifies the delay time length, in milliseconds.
    • @retval None
    • /
    void Delay(__IO uint32_t nTime)
    {
    uwTimingDelay = nTime;
    while(uwTimingDelay != 0);
    }
    /**
    • @brief Decrements the TimingDelay variable.
    • @param None
    • @retval None
    • /
    void TimingDelay_Decrement(void)
    {
    if (uwTimingDelay != 0x00)
    {
    uwTimingDelay--;
    }
    }
    #ifdef USE_FULL_ASSERT
    /**
    • @brief Reports the name of the source file and the source line number
    • where the assert_param error has occurred.
    • @param file: pointer to the source file name
    • @param line: assert_param error line source number
    • @retval None
    • /
    void assert_failed(uint8_t* file, uint32_t line)
    {
    /* User can add his own implementation to report the file name and line number,
    ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
    /* Infinite loop */
    while (1)
    {
    }
    }
    #endif

    3.11 整机组装
    notion image
    组装过程十分令人激动,几个月来的努力逐渐结出了果实。在机架的组装中,有几个部分:安装分电板、焊接线缆、安装悬臂、安装电机、安装夜航灯、整理固定线缆、安装电池、安装减震板、安装STM32主控板、安装低压报警器。在安装过程中,需要特别注意安全问题,不少操作涉及到高温焊接,线缆与主控板也需要做好绝缘保护。
    图3-54 组装过程图
    电缆的焊接十分值得注意。四轴飞行器的工作电流较大,焊接时需要做到一丝不苟,杜绝虚焊,仔细检查是否存在短路、断路的情况。在后期,若是固定下来了供电的方案,将会使用热熔胶固定线缆以及焊接点,增强飞机的稳健性。
    第4章 硬件子系统的测试
    软件开发完毕后,进入到第一轮螺旋的测试环节。软件需求是软件质量测量的基础,本章节将介绍项目开发结束后的测试工作,以对软件质量做出保证,系统监测和评估工程的各个方面,最大限度提高质量最低标准。
    4.1 模块测试
    针对软件设计的最小单位 ─ 程序模块,进行正确性检验 的测试工作。其目的在于发现各模块内部可能存在的各种差错。单元测试需要从程序的内部结构出发设计测试用例,多个模块可以平行地独立进行单元测试。
    4.1.1 姿态传感器测试
    测试目的
    接通电源后,手动调整飞行器的姿态,实现不同位置方向的倾斜,将姿态数据显示在OLED显示器上,沿着xyz轴改变机体倾角,并观察对应数值变化,且与理论变化相同。
    测试设备与工具:
    GY-86模块、电源、OLED显示器、数据记录工具
    测试步骤:
    连接飞行器至电源,确保电源供应正常。
    手动调整飞行器的姿态,包括不同位置和方向的倾斜,例如沿着xyz轴改变机体倾角。
    将姿态数据显示在OLED显示器上。
    读出并观察显示的姿态数据变化。
    比较观察到的数据变化与理论变化是否相同。
    预期结果:
    观察到的姿态数据变化与手动调整飞行器姿态的变化相符。
    观察到的姿态数据与理论变化相一致
    测试结果:
    在进行飞行器姿态调整与姿态数据显示的测试过程中,我们观察到以下结果:
    沿着xyz轴改变机体倾角时,姿态数据在OLED显示器上显示了相应的变化。
    所观察到的数据变化与手动调整飞行器姿态的变化一致。
    观察到的姿态数据变化与理论变化相符。
    结论:
    notion image
    经过测试,飞行器姿态调整与姿态数据显示功能正常。姿态数据的变化与手动调整的姿态一致,并且与理论变化相符。测试结果表明,该功能能够准确地计算飞行器的姿态数据。
    图4-1 GY-86模块测试
    4.1.2 蓝牙模块的测试
    测试目的
    验证模块的功能是否正常,包括USART串口通信和蓝牙模块连接功能。通过手机作为上位机,与下位机进行连接,并成功传输姿态传感器的数值。
    测试设备与工具
    下位机(包含USART串口和蓝牙模块)
    上位机(手机作为上位机)
    测试步骤
    使用手机作为上位机,通过蓝牙模块与下位机进行连接。
    确保连接成功后,下位机蓝牙模块灯光常亮,表示连接正常。
    下位机将姿态传感器的数值通过USART串口传送给上位机。
    在上位机中观察到相应的数值。
    预期结果
    通过蓝牙模块成功连接手机作为上位机和下位机。
    下位机蓝牙模块灯光常亮,表示连接正常。
    上位机能够接收到下位机通过USART串口传送的姿态传感器的数值。
    测试结果
    在进行整体测试的过程中,我们观察到以下结果:
    蓝牙模块成功连接上位机和下位机。
    下位机蓝牙模块灯光常亮,表示连接正常。
    上位机成功接收到下位机通过USART串口传送的姿态传感器的数值。
    结论
    notion image
    经过整体测试,模块的USART串口通信和蓝牙模块连接功能均正常。下位机通过蓝牙模块成功与上位机(手机)连接,并且下位机蓝牙模块灯光常亮,表示连接正常。上位机能够正确接收到下位机通过USART串口传送的姿态传感器的数值。
    图4-2 蓝牙模块测试
    4.1.3接收机的测试
    测试目的:
    通过遥控器与接收机的对频(匹配)过程,验证通信的可靠性,并观察遥控器拨动阀门时,接收到的数据是否准确,并且在理论值范围内。
    测试设备与工具:
    遥控器、接收机、OLED显示器、示波器
    测试步骤:
    打开遥控器开关,使其处于工作状态。
    进行遥控器与接收机的对频(匹配)过程,确保通信连接正常。
    拨动遥控器上的阀门,观察遥控器灯是否闪烁,以验证信号传输是否正常。
    遥控器将信号传送到接收机,接收机解析后将数据传入TIM定时器的捕获通道。
    在主程序中读取八个通道的数据,并通过OLED屏显示出来。
    观察在拨动不同的阀门时,OLED屏上对应的数字是否改变。
    检查观察到的数据变化是否在理论值范围内。
    预期结果:
    遥控器与接收机成功进行对频(匹配),通信连接正常。
    拨动遥控器上的阀门时,遥控器灯不闪烁,表明信号传输正常。
    在OLED屏上能够观察到拨动不同阀门时,对应数字的改变。
    观察到的数据变化与理论值范围内的变化相符。
    测试结果:
    在进行遥控器与接收机通信及数据显示的测试过程中,我们观察到以下结果:
    • 遥控器与接收机成功进行对频(匹配),通信连接正常。
    • 拨动遥控器上的阀门时,遥控器灯不闪烁,表明信号传输正常。
    • 在OLED屏上观察到拨动不同阀门时,对应数字发生了改变。
    • 观察到的数据变化在理论值范围内。
    结论:
    经过测试,遥控器与接收机通信及数据显示功能正常。遥控器拨动阀门时,能够正确传送信号到接收机,接收机通过TIM定时器的捕获通道解析数据,并通过OLED屏显示出来。观察到的数据变化与理论值范围内的变化相符。
    notion image
    图4-3 接收机模块测试
    4.1.4 电机模块的测试
    测试目的:
    验证使用STM32主控板发送不同占空比的PWM波对电机转速的影响。通过调节PWM波的占空比,观察电机转速的变化。
    测试设备与工具:
    STM32主控板
    电机
    电池电源
    示波器(用于测量PWM波形)
    测试步骤:
    接通电池电源,将STM32主控板与电机连接。
    在STM32主控板上设置不同的PWM波占空比,可以逐步增加或减小,或选择多个不同的占空比。
    将设置好的PWM波发送到电机驱动引脚上。
    使用示波器或其他合适的工具,测量PWM波形和电机转速。
    预期结果:
    随着PWM波占空比的增加,预期观察到电机转速的增加。
    随着PWM波占空比的减小,预期观察到电机转速的减小。
    不同的PWM波占空比应导致电机转速的明显差异。
    测试结果:
    在设置不同的PWM波占空比时,观察到电机转速的变化。
    随着PWM波占空比的增加,电机转速逐渐增加。
    随着PWM波占空比的减小,电机转速逐渐减小。
    不同的PWM波占空比导致了电机转速的明显差异。
    结论:
    经过整体测试,使用STM32主控板发送不同占空比的PWM波对电机转速有明显的影响。随着PWM波占空比的增加,电机转速增加;随着PWM波占空比的减小,电机转速减小。这表明PWM波的占空比可以作为控制电机转速的有效手段。
    notion image
    图4-3 电机模块测试
    4.2 集成测试
    集成测试是将各个模块组合起来进行整体性能和功能验证的测试阶段
    接通电池电源,电机完成初始化,以两声哔为标志。上位机与下位机成功连接,下位机灯常亮。接收机和遥控器完成匹配,遥控器灯不闪烁。系统初始配置完成。
    拨动遥控器方向杆,遥控器信号传送到接收机,接收机解析、TIM定时器输入捕获后将数据传给电调,电调做出对应改变,实现电机转速的改变。测试中,遥控器油门舵向上拨动,转速增大;向下拨动,转速减小。
    同时,姿态传感器实时捕捉机体姿态,并将其以及电机转速数据通过蓝牙模块传送到上位机,上位机显示此时机体姿态数据。完成集成测试。
    notion image
    图4-4 系统测试现场
    第5章 执行情况总结
    5.1 总结与感悟
    本学期综合设计的预期规划实现程度很高,有一些达到自己预期的地方,在接下来的一年里需要继续坚持:
    • 时间规划成功。
      • 基本任务于二零二三年十一月底全部达到验收水准。
    • 代码编写水平有较大进步。
      • 自行撰写的代码达1200余行,代码规范与层次划分均有较高水准,更深层次的理解到了“面相对象”的编程思想。
    • “To learn by doing."
      • 对于此类项目式学习,并非是如同高中一般学好了所有必备知识,再如同期末考试一般集中火力把项目制作出来。先花费数周时间刷完所有网课,然后再最后几周冲刺做项目的做法虽能达到预期效果,但是无法调动自身的学习积极性。
      • 相反,“To learn by doing."是一种自顶向下的分析模式:拿到一个项目,整理出它有什么功能;为了实现这些功能,需要些什么知识;为了学会这些知识,我需要些什么学习资料?我需要到互联网上查找什么东西?做中学,学中做,在项目导向式的学习中逐步构建自己的技能树,形成自己对整个计算机体系结构的见解。
    • 学科交叉。
      • 将软件工程学习到的理论知识即刻实践,即加深了对工程管理的理论知识理解,又让本学期的项目管理效率;
      • 嵌入式开发中涉及到了很多寄存器、总线、中断等知识,很好的辅助了计算机组成原理的学习;
      • 计算机组成原理与编译原理的课程又帮助到我们跟深入的理解STM32F4这块芯片的运转方式。
    除此之外,还有很多值得改进、纠正的地方:
    • 虽然项目开发的进度完美达到时间规划节点的要求,但是时间节点的规划还有更值得优化的空间
      • 比如我们组半期之前为半期考试复习预留了过多的时间,较早的停下了项目。这也需要自己进一步思考如何平衡制作项目磨练技术与课内考试的时间平衡关系。
    组员间代码开发的风格与规范还需要进一步统一。
    • 合并代码后虽能正常运行,但代码参差划分、注释风格、变量命名方式均有一定差别。造成此现象的根本原因是组员间沟通效率不够高。在开发前期,使用QQ进行项目交流,但是其实时性较低,且聊天记录零散,不易于整理;后期应考虑转移至飞书“知识空间”功能。
    • PCB转接板开发周期过长。
      • 由于各种差错,导致绘制了三个版本的PCB,每个版本设计、绘制花费两天,厂家制作、邮寄花费5天,焊接、电气测试又花费一天,一块板子就要花一周。后面需要优化我们的软件开发过程模型,以更好的适应这种实现周期短、测试周期极长的开发流程。
    • 扩展功能实现效果不佳。
      • 在完成基础任务后,我们制定了四个扩展、优化目标:构造双层PCB转接板;使用Python将收到的蓝牙数据实时绘制为曲线图;使用汇编语言对各个模块的底层部分进行改写;部署FreeRTOS来替代轮询的系统整合方式。但仅仅有第一个扩展点的实现效果达到了预期。
        • notion image
    图5-1 实现效果
    5.2 未来展望
    notion image
    许多师兄师姐的四轴作品相当优秀,有很多可以参考,学习的地方。在刘珮泽师兄的建议下,计划于寒假制作好一个一体板,并阅读PX4这一业界领先的开源固件代码,尝试部署于一操作系统,为下学期的课程做好充足的准备。
    图5-2 师兄师姐的优秀作品
    参考文献
    [1] HMC5883L中文规格书[J].Honeywell
    [2] 廖勇等.嵌入式操作系统[M].北京:高等教育出版社.2017.
    [3] STM32F4xx中文参考手册[J].意法半导体
    [4] STM32 F407最小系统板开发指南-寄存器版本[J].意法半导体
    [5] STM32F4开发指南-寄存器版本[J].正点原子
    [6] ATK-BLE01蓝牙串口模块用户手册[J].正点原子
    [7] User manual f401re [J].意法半导体
    [8] Datasheet f401re [J].意法半导体
    [9] Reference manual f401re [J].意法半导体
    [10 ]MS5611-01BA01 [J].measurement
    [11] PS-MPU-6000A [J].TDKinensense
    [12] Roger S.Pressman等.SoftwareEngineering[M].北京:机械工业出版社.2021.
    致谢
    本项目的工作是在我们的指导教师廖勇老师的悉心指导下完成的,廖老师平易近人的教学和的独特的探究性课堂,让我们学习到了很多:“To learn by doing" 的学习模式、嵌入式开发的基础知识、如何更好将自己学到知识讲解出来、黑板板书的书写、项目报告的撰写等等,让我们对计算机系统架构有了更深层次的理解;陈家煜、袁佳琪、刘芮利等师兄师姐也在需求分析、可行性研究、PCB设计、焊接、报告撰写环节提供了很多帮助,给出了很多建设性意见,提示了许多制作细节。还有第一组、第五组等小组也为我们提供了很多援助;在此,借报告完成之际,表示由衷的感谢和敬意!
    感谢各位老师的对本报告的阅读!
    PX4·开发环境搭建四轴 II——RTOS
    Loading...