前言

参考

智能平衡车:(1)硬件选型和原理图设计

天问51编译器下载

天问ASRPRO资料汇总

天问语音视频教程

天问文档

HC-05蓝牙模块调试笔记以及使用正点原子例程无法检测到蓝牙模块原因分析

无需另配定时器在STM32 HAL下实现微秒级延时(兼容FreeRTOS)

【TIM】编码器接口

鲁乎乎CSDN博客-平衡车

基于ESP32CAM的图传勘探小车代码(含APP Inventor制作的APP上位机)

直立车各环的调试_平衡小车调试指南(直立环 速度环)

基于PID算法的自平衡小车-github

【平衡小车制作】串级PID调参及平衡成果展示

MPU6050问题解决办法(本人遇到的)

STM32 HAL 库串口同时收发,接收卡死?

电机选型

电机的转速对平衡小车是非常重要的,电压的话选择6V,因为锂电池充电后可达8V多,最低到5.6V左右,所以可以进行降压给MCU供电,然后给电机供电,5.6V电机还是可以转动的

电机转速过大的话会导致回正过大,然后导致一会左偏一会右偏,一直不能回到0度

问题:为什么床比地面好调试,因为床不是一个平面,车轮接触面积变大,轮子在前进时这些都变成阻力,相当于这个阻力削弱了超调的效果

PCB设计

此原理图非我平衡车使用的,只供参考

PID算法

初步了解

r(t):你期望的实际值

e(t):实际值与你设定值的差值,误差

上面这个PID公式是连续的,但是在程序里一般做不到连续的,因为肯定会有时间间隔,程序一般是隔多少ms去计算一次,因此把上面公式变成离散型,简化后的公式如下(位置式):

Uk=Kp×ek+Kij=0kej+Kd(ekek1)Uk=K_p×e_k+Ki∑_{j=0}^{k}e_j+K_d(e_k−e_{k−1})

只需要改Kp,Ki,Kd即可

什么是离散型,就是一个个点

增量式PID,原则上也是位置式,只不过它考虑了前一次的误差值,而且这个公式结果是变化量

通俗易懂了解什么是位置式(为了简约计算,暂时把积分项和微分项设置为0,只看比例项)

先假设设定kp为10

第一次计算:期待值是100,目标值当前为0,那误差就是100,那PWM输出就是 kp*ek 就是 100*10=1000,假设1000PWM相当于1m\s

第二次计算:期待着是100,目标值现在运动了1s,那误差就变成99,那PWM输出就是 99*10=990,占空比小了,速度减慢

假设当前小车来到101m,超出1m,误差是-1,那PWM输出就是 -1*10=-10

增量式的话就不是把这个误差结果 -10 直接作用到PWM上,而是 PWM+∆uk,即 PWM+(-10),即它速度刚超的时候不会马上往回倒,而是一直减,直到减到0然后再减就变成负数,这时候才往回倒,而位置式PID刚超就马上PWM变成负数马上倒回去

那什么情况用增量式,想要一直保持在某个值时,它就算超了也不会倒转,因为它不是直接把结果给PWM,而且是上一次PWM+这一次误差,如果使用位置式则它一旦超出预值则变成负数PWM,轮子会瞬间倒转

串级PID:多个反馈,外环计算的结果传入内环,内环计算的结果才传到最终目标位置

比例P

Kp×ekKp×ek

以水池注水举例说明

  1. K_p越大.系统响应越快,越快达到目标值。
  2. K_p过大会使系统产生较大的超调和振荡,导致系统的稳定性变差。
  3. 仅有比例环节无法消除静态误差。

假设期待值为100cm,当前为0cm

第一次 — e误差等于100,因为还没开始注水,kp=0.5,计算结果为 50cm(假设结果就是注水高度),此时水池注水高度为50cm

第二次 — e误差等于50,kp=0.5,计算结果为25cm,此时水池注水高度为75cm

第三次 — e误差等于25,kp=0.5,计算结果为12.5cm,此时水池注水高度为87.5cm

第四次 — e误差等于12.5,kp=0.5,计算结果为6.25cm,此时水池注水高度为93.75cm

第五次 — e误差等于6.25,kp=0.5,计算结果为3.125cm,此时水池注水高度为96.875cm

当kp设置大点0.8时,结果又是

第一次 — e误差等于100,因为还没开始注水,kp=0.8,计算结果为 80cm(假设结果就是注水高度),此时水池注水高度为80cm

第二次 — e误差等于20,kp=0.8,计算结果为16cm,此时水池注水高度为96cm

第三次 — e误差等于4,kp=0.8,计算结果为3.2cm,此时水池注水高度为99.2cm

第四次 — e误差等于0.8,kp=0.8,计算结果为0.64cm,此时水池注水高度为99.84cm

第五次 — e误差等于0.16,kp=0.8,计算结果为0.128cm,此时水池注水高度为99.968cm

所以验证了1

但是kp太大会导致震荡,然后慢慢再去接近期待值,比如一开始直接给kp=1.5,则现象是:

验证了2

假设kp=0.8,当到达96cm时,此时误差是4cm,而且恰巧水池开始漏水,每小时漏4cm,而我们PID调节也是每小时一次,这样就会造成PID刚想调节时水就漏了4cm,然后PID调节升到96,一直循环,导致水位一直不能到达100cm,但是虽然它没有达到期望值,但是也进入了稳态,一直稳态在96cm,此时误差值也不再变化,所以就产生了静态误差

积分I

Kij=0kejKi∑_{j=0}^{k}e_j

积分是把误差一直累加,所以一般设置很小,太大会导致震荡超调,只要误差存在,那这个值会一直累加

积分项的作用以水池注水举例说明

  1. Ki越大系统响应越快.越快达到目标值。积分项的引入可以消除稳态误差
  2. Ki过大会使系统产生较大的超调和振荡.导致系统的稳定性变差。
  3. 随着时间的增加.积分项数值会越来越大,导致系统响应变慢。

【积分限幅】

为了防止积分部分太大,导致系统响应变慢。需要对积分进行幅度限制。当积分值超过或者小于某值后不再变化。

比如现在加入Ki,达到注水高度后需要下降到某处,此时由于积分项累加了很大,所以在往下调整的时候还要抵消掉累加和

【积分分离】

积分的主要目的就是消除稳态误差,稳态误差一般出现在接近目标值附近才会产生。为了防止积分累加次数过多而饱和,我们在系统调节 前期不进行积分累加当误差值较大时只进行比例调节当误差值较小时才引入积分调节。程序上增加对误差的判断,误差值小于设定值时积分项不累加。

微分D

Kd(ekek1)K_d(e_k−e_{k−1})

以日常生活中往水杯倒水为例

  1. Kd表示变化趋势越大,微分环节作用越强,对超调和振荡的抑制越强。
  2. Kd过大会引起系统的不稳定,容易引入高频噪声。
  3. PID控制不一定三项都要参与
    1. 例如小车平衡.为了提高系统的响应速度我们只是用了PD控制
    2. 例如轮子速度控制,为了提高轮子速度变化的连续性以及避免震荡的产生,只采用PI控制

比如倒水,一开始因为误差大所以倒的速度很快,当后面越来越接近杯口时,速度应该是越来越慢这样才会到达杯口而不导致溢出,可以看到微分项后面计算是负数,那这样uk就减小,相当于 你越接近期望值Kd项就越小,误差越大这个kd项越大

物理模型

直立环

使用 PD控制

小车直立需要一个快速的响应,并且在平衡车模型中小车受到地球重力影响几乎不可能产生稳态误差,因为小车只要没有达到机械

中值就会倾倒,所以这里直接省略掉了积分控制。下图是一个PD直立环的控制回路:

入口参数是平衡小车倾角和Y轴陀螺仪(这个取决MPU6050的安装)

机械中值

因为受到小车组装等移速影响,小车的真是平衡位置可能不是0度,我们需要自行测试小车平衡时的倾角。此角度才是我们期望的角度。因为只有小车在机械中值的时候才能保持平衡,实现直立

测试机械中值方法1

首先把小车放地上左倾一点,然后用手去往右慢慢一点点推它,当推到某一点时小车它自己往右边倒了,此时记住角度A,然后把小车右倾一点,用手慢慢往左一点点推它,当推到某一点时小车它自己往左边倒了,此时记住角度B,最终 机械中值就等于(A+B)/2

测试机械中值方法2

利用速度环,一开始机械中值就给0度,然后调PD,当调到它既不倒时它一直往一个方向去移动,如果它往右移动,那 机械中值一定是在0度的左侧,然后慢慢加,给-0.5度,-0.8度,慢慢加,当加到某点时它速度消失了在某点平衡了那这个角度就是机械中值,相反如果它往左移动,那 机械中值一定是在0度的右侧,然后慢慢加,给0.5度,0.8度,慢慢加,当加到某点时它速度消失了在某点平衡了那这个角度就是机械中值

标准的PID公式是:

Uk=Kp×ek+Kij=0kej+Kd(ekek1)Uk=K_p×e_k+Ki∑_{j=0}^{k}e_j+K_d(e_k−e_{k−1})

我们只取比例和微分控制,所以公式简化为:

Uk=Kp×ek+Kd(ekek1)Uk=K_p×e_k+K_d(e_k−e_{k−1})

误差 = 机械中值 - 陀螺仪返回实际角度值

ek=Zeroθe_k = Zero - θ

ω表示陀螺仪返回实际角速度

ekek1=ωe_k−e_{k−1} = ω

为什么ω可以用角速度表示,因为角速度的定义是单位时间的角度变化,机械中值恒定不变。

而我们的 PID 控制的算法是离散的,每间隔固定的时间就计算一次,所以正好也是单位时间。虽然这里计算出的并不是实际角速度,

因为计算间隔不是1秒。但是我们的Kd系数最后是要去调整的,和实际角速度是成比例的。我们为了减少程序的运算量。这里就直接

采用陀螺仪输出的角速度即可。所以没必要去管它单位是ms还是s

经过上面的整理,最终的PD公式为:

PWM=Kp(Zeroθ)+KdωPWM = K_p*(Zero-θ)+K_d*ω

公式中的 θω 由陀螺仪采集返回给单片机, Zero 由我们自己手动测得

速度环

使用 PI控制

我们想要直立的同时可以进行运动,所以需要加速度环,下面是串级PID,速度环没用到D是因为我们需要的是一个缓慢的速度,如果加了D当达到我们设定速度后它可能需要去消除震荡就可能会导致轮子抖动,这个是不利的效果

速度反馈可以直接使用编码器的数值,因为速度的定义是单位时间内物体的位移,我们不使用物理世界中的m/s 的单位,因为是呈比

例关系我们为了计算简便直接采用编码器数值在单位时间的变化量表示速度。又因为我们的PI计算公式是间隔固定周期计算,且编码

器在固定时间内读取后直接清零,因此 直接读取编码器的数值就可以表示速度

标准的PID公式是:

Uk=Kpek+Kij=0kej+Kd(ekek1)Uk=K_p*e_k+Ki∑_{j=0}^{k}e_j+K_d(e_k−e_{k−1})

我们只取比例和积分控制,所以公式简化为:

Uk=Kpek+Kij=0kejUk=K_p*e_k+Ki∑_{j=0}^{k}e_j

误差值 = 期望速度 - 实际速度

ek=Encoder_SetEncodere_k = Encoder\_Set - Encoder

速度环整理得出:

output=Kp(Encoder_SetEncoder)+Kij=0kejoutput=K_p*(Encoder\_Set - Encoder)+Ki∑_{j=0}^{k}e_j

串级PID整理:

公式①:PWM=Kp(Zeroθ)+KdωPWM = Kp*(Zero-θ)+Kd*ω

公式②:output=Kp(Encoder_SetEncoder)+Kij=0kejoutput=K_p*(Encoder\_Set - Encoder)+Ki∑_{j=0}^{k}e_j

那将 output+Zero 作为直立环的输入代入①(由于速度环输出的期望角度是基于机械中值的基础,因此需要加上Zreo之后代入,因为小车运动是在直立的基础上倾斜才能运动)得出:

公式③:PWM=Kp(output+Zeroθ)+KdωPWM = Kp*(output+Zero-θ)+Kd*ω

将公式②代入③得出:

公式④:PWM=Kp(Kp(Encoder_SetEncoder)+Kij=0kej+Zeroθ)+KdωPWM=K_p*({K_p}^{'}*(Encoder\_Set - Encoder)+Ki∑_{j=0}^{k}e_j + Zero-θ) + Kd*ω

将公式④展开得出:

公式⑤:PWM=KpKp(Encoder_SetEncoder)+KpKij=0kej+KpZeroKpθ+KdωPWM=\textcolor{green}{K_p*{K_p}^{'}*(Encoder\_Set - Encoder)+K_p*Ki∑_{j=0}^{k}e_j} +\color{red}{K_p*Zero-K_p*θ + Kd*ω}

上面的公式绿色正好是速度环,红色正好是直立环。因此这个双环PID 的控制代码相当于把两个环的输出结果求和后传入电机驱动器。有一点需要注意的是它速度环前面都乘上了直立环的一个KpK_p,到时候程序调试的时候速度环的 kp{k_p}^{'}要取小点因为它乘以了直立环的一个系数KpK_p

  • 【物理含义】

它肯定是在这个附近进行抖动的,然后呢,肯定是不会抖动的太大,它才能维持在这个机械中值,那你这时候抖动呢,这个角度呢,是在速度为零的时候进行一个抖动。那我如果想让它有一个速度,我们怎麼办?我把这个速度进行平移呀,沿着这个Y轴进行平移,比如说我的速度变成100了,相当于你是在100的这个基础上振动的。维持你平衡靠的是什么?靠的是这块儿的振动,这块儿振动来维持它的平衡,那参考系是速度,并不会影响我这个振动,我这个波形的一个形状,它比如就是个锯齿波,那么我把这个锯齿波往上移还是往下平移,它只要是平移了,不会改变它的形状,那我就可以保持小车平衡。所以说作用到最后的一个现象就是直立环直接加上速度环,所以这就是它一个物理的含义,那么到后面呢,我们讲这个转向法,我们还想实现小车转向的时候,一个小车不是两个轮子吗?那么一个轮快,一个轮子慢,它就可以实现转向。到时候的时候呢,我们直接可以把转向环输出的这个值直接叠加到这个轮子上就可以,因为它叠加呢,并不会影响你这个振动这个波的形状,所以呢,不会影响它一个直立的状态这就是说为什么我们最后作用出来这个串级PID就直接求和就可以。那是不是代表别的物理模型也是串级PID直接求和,那不一定,正好是因为是小车这个模型,所以说我们推导出来它是可以以求和的状态来去实现的,但是你要换个物理模型的话,要具体情况具体分析

转向环

使用 PD控制

小车两个车轮的转速不同即可实现转向。因此主要是在直立环和速度环的基础上叠加转向环的PWM即可实现。

  • 转向环的两种期望
  1. 抑制转向

因为两个电机即使输入的pwm是完全相同的也会产生速度差,所以小车即使没有加入转向环也会自己发生偏转,这时候我们期望加入转向环抑制偏转,实现小车直线行走。因此我们的参数选用负极性抑制这种状态

  1. 期望转向

我们直接取消抑制作用,直接对PWM进行加减操作

  • 公式推导

转向控制不要求特别精确,因此我们在期望转向时只是用Р调节,抑制转向时只是用D调节。D调节我们直接使用陀螺仪的Z轴角速度

抑制转向:
output=KdGyro_zoutput=K_d*Gyro\_z

期望转向:

output=Kp期望转角output=K_p*\text{期望转角}

(此转角为模糊控制,并非精确转向角度,可以通过KpK_p调试,增加精度)

原理图绘制

  • 主板

组成:STM32F103VET6,0.96OLED,MPU6050,TB6612FNG,HC-05蓝牙模块,按键,LED,降压电路,蜂鸣器,超声波,7路灰度,ASR语音模块

注意:

  1. VBAT 是外部电池专用引脚,如果没用到则需要接VDD
  2. 看数据手册知道 VDDAVREF+ 都是接VDD,VREF-VSSA 接地,NC 悬空(即芯片公司预留的一个未连接引脚)
  3. BOOT0BOOT1(PB2) 接地即可
  4. TPS54302DDCR 降压电路直接参考IC数据手册即可,注意布局选用户自定义那,那里找一模一样的官方的不一样不方便画线,只要封装没错就行
  5. 电机固定孔的话用通孔即可,给直径
  • 底板

  • 注意尺寸

元器件在 讯华微电子 买,大多数都有

  • 主板
名称 数量 位号 封装 备注
22uF电容 1 C1 0805
0.1uF电容 8 C2,C3,C4,C5,C10,C11,C20,C23 0805
20pF电容 2 C6,C7 0805
100nF电容 9 C8,C15,C16,C19,C26,C27,C28,C29,C30 0805
10uF电容 3 C9,C12,C13 0805
1uF电容 2 C14,C17 0805
22pF电容 1 C18 0805
22uF钽电容 1 C21 A型
220uF铝电解电容 1 C22 贴片 额定电压25V
DC电源插座 1 DC1 内径:2mm 外径:6.3mm 铜材质,额定电流3A,额定电压30V
TPS54302DDCR降压转换器 1 U2 SOT-23-6封装
ME6119C33M5G线性稳压器(LDO) 1 U3 SOT-23-5封装,输出极性:正 最大输入电压:18V 输出电压:3.3V 输出电流:400mA
TB6612FNG电机驱动芯片 1 U4 输出电流:1A ,电源电压:2.7V~5.5V SSOP-24封装
LED 2 0805 红色
LED 1 0805 绿色
有源蜂鸣器 1 BUZZER1 5V
4P排母 1 H1 2.54mm
6P排母 1 H2 2.54mm
2x4P排母 1 H3 2.54mm
2x4P排针 2 J3,J4
按键 4 K1,K2,K3,RESET
10uH±20%电感 1 L1 额定电压25V
MPU6050 1 MPU1
SS8050-NPN 1 Q2 SOT23
10K电阻 6 R1,R8,R10,R11,R12,R14 0805
110k 1%电阻 2 R2,R15 0805
15k 1%电阻 1 R3 0805
20k电阻 3 R4,R5,R6 0805
1K电阻 3 R7,R9,R13 0805
510K电阻 1 R16 0805
STM32F103VET6 1 U1
8MHz晶振 1 X1
4P插件XH 2 XH1,XH3 2.54mm
9P插件XH 1 XH2 2.54mm
6P插件ZH 2 ZH1,ZH2 1.5mm
  • 底板
名称 数量 位号 封装 备注
2P插件PH 1 CN1 2.0mm
9P排母 2 H1,H2 2.54mm
4P排母 2 H3,H4 2.54mm
9P排针 2 J1,J2 2.54mm
2x4P排针 2 J3,J4 2.54mm
4P插件XH 3 XH1,XH2,XH4 2.54mm
2P插件XH 1 XH3 2.54mm

PCB

焊接

注意引脚分布,看数据手册

电源不能用铝电解电容,要陶瓷,这样阻抗小

我的模块

其他

  • MX配置

用于系统计数,任务调度

用于编码器,MPU等执行

STM32主控

  • 硬件

ASRPRO语音核心板

语音模块使用天问51的ASRPRO,需要去官网下载编译器

喇叭的红色是+,黑色是-,分别连接核心板的 SPK+SPK-(端子是2.0)

麦克风的红色是+,黑色是-,分别连接核心板的 MIC+MIC-(端子是2.54)

UART0串口不推荐用来MCU通信,一般用来固件烧写

  • 硬件

  • MX配置

需要跟STM32通信

STM32引脚号 模式
PB10 USART3_TX
PB11 USART3_RX

  • 程序
Voice.h
cpp
#ifndef __VOICE_H
#define __VOICE_H

// 协议是4个字节
#define Voice_Protocol_Data_LEN	(uint8_t)7

typedef struct
{
    void (*Voice_Init)(void);   // 语音识别初始化
    void (*Voice_Protocol_Analyze)(void);   // 语音识别协议解析
}Voice_t;

extern Voice_t Voice;

#endif
Voice.c
cpp
/***************************************************************************
 * File: Voice.c
 * Author: Luckys.
 * Date: 2023/08/3
 * description: ASR语音开发板通信
 -----------------------------------
协议:
    语音识别开发板发送给STM32协议格式:【0xAA 0xXX 0xXX 0xBB】
    STM32发送给语音识别开发板是字符串形式...
 -----------------------------------
****************************************************************************/
#include "AllHead.h"

/*====================================static function declaration area BEGIN====================================*/
static void Voice_Init(void);
static void Voice_Protocol_Analyze(void);
/*====================================static function declaration area   END====================================*/

Voice_t Voice = 
{
    .Voice_Init = &Voice_Init,
    .Voice_Protocol_Analyze = &Voice_Protocol_Analyze
};

/*
* @function: Voice_Init
* @param: None
* @retval: None
* @brief: 描述
*/
static void Voice_Init(void)
{
    __HAL_UART_ENABLE_IT(&huart3, UART_IT_IDLE);
    HAL_UART_Receive_DMA(&huart3, UART3.pucRec_Buffer, (uint16_t)UART3_Rec_MAX_LENGTH);
}

/*
* @function: Voice_Protocol_Analyze
* @param: None
* @retval: None
* @brief: 描述
*/
static void Voice_Protocol_Analyze(void)
{
    uint8_t Temp_Array[4] = {0x00};
    uint8_t i = 0, Index = 0;

    HAL_UART_DMAStop(&huart3); // 串口停止DMA接收

    for (i = 0; i < UART3_Rec_MAX_LENGTH; i++)
    {
        if (0 == Index) // 检测键值起始数据0xAA
        {
            if (*(UART3.pucRec_Buffer + i) != 0xAA)
            {
                continue;
            }
        }
        Temp_Array[Index] = *(UART3.pucRec_Buffer + i);
        if (Voice_Protocol_Data_LEN == Index)
        {
            break;
        }
        Index++;
    }
    HAL_UART_Receive_DMA(&huart3, UART3.pucRec_Buffer, (uint16_t)UART3_Rec_MAX_LENGTH);
    // 处理数据
    if (Voice_Protocol_Data_LEN == Index)
    {
        if ((0xAA == Temp_Array[0]) && (0xBB == Temp_Array[3]))
        {
            if (0x01 == Temp_Array[1])
            {
                switch(Temp_Array[2])
                {
                    case 0x01:
                    {
                        break;
                    }
                    case 0x02:
                    {
                        break;
                    }
                    default:break;
                }
            }
        }
        else
        {
            ;
        }
    }
}

ASRPRO基础

基本的东西都在编译器里的编程手册中

使用专业版,扩展库的初始化一般放系统应用初始化中(不是强制性只是个人习惯问题)

上电初始化和系统应用初始化效果一样的就是上电执行一次然后就不会执行

功能

控制WS2812

  1. 在专业版模式下点击扩展,找到官方的WS2812库点击【加载】即可添加进来(版本选择最新)

与STM32通信

Voice.h
cpp
#ifndef __VOICE_H
#define __VOICE_H

// 协议是4个字节
#define Voice_Protocol_Data_LEN	(uint8_t)4

typedef struct
{
	uint8_t ASR_Rec_Temp_Arr[Voice_Protocol_Data_LEN];	// ASR接收缓存用于OLED显示
    void (*Voice_Init)(void);   // 语音识别初始化
    void (*Voice_Protocol_Analyze)(void);   // 语音识别协议解析
}Voice_t;

extern Voice_t Voice;

#endif
Voice.c
cpp
/***************************************************************************
 * File: Voice.c
 * Author: Luckys.
 * Date: 2023/08/3
 * description: ASR语音开发板通信
 -----------------------------------
协议:
    语音识别开发板发送给STM32协议格式:【0xAA 0xXX 0xXX 0xBB】
    STM32发送给语音识别开发板是字符串形式...
 -----------------------------------
****************************************************************************/
#include "AllHead.h"

/*====================================static function declaration area BEGIN====================================*/
static void Voice_Init(void);
static void Voice_Protocol_Analyze(void);
/*====================================static function declaration area   END====================================*/

Voice_t Voice = 
{
	.ASR_Rec_Temp_Arr = {0,0,0,0},
    .Voice_Init = &Voice_Init,
    .Voice_Protocol_Analyze = &Voice_Protocol_Analyze
};

/*
* @function: Voice_Init
* @param: None
* @retval: None
* @brief: 描述
*/
static void Voice_Init(void)
{
    __HAL_UART_ENABLE_IT(&huart3, UART_IT_IDLE);
    HAL_UART_Receive_DMA(&huart3, UART3.pucRec_Buffer, (uint16_t)UART3_Rec_MAX_LENGTH);
}

/*
* @function: Voice_Protocol_Analyze
* @param: None
* @retval: None
* @brief: 描述
*/
static void Voice_Protocol_Analyze(void)
{
    uint8_t Temp_Array[4] = {0x00};
    uint8_t i = 0, Index = 0;

    HAL_UART_DMAStop(&huart3); // 串口停止DMA接收

    for (i = 0; i < UART3_Rec_MAX_LENGTH; i++)
    {
        if (0 == Index) // 检测键值起始数据0xAA
        {
            if (*(UART3.pucRec_Buffer + i) != 0xAA)
            {
                continue;
            }
        }
        Temp_Array[Index] = *(UART3.pucRec_Buffer + i);
        if (Voice_Protocol_Data_LEN == Index)
        {
            break;
        }
        Index++;
    }
    HAL_UART_Receive_DMA(&huart3, UART3.pucRec_Buffer, (uint16_t)UART3_Rec_MAX_LENGTH);
    // 处理数据
    if (Voice_Protocol_Data_LEN == Index)
    {
        if ((0xAA == Temp_Array[0]) && (0xBB == Temp_Array[3]))
        {
			Public.Memory_Copy((char*)Voice.ASR_Rec_Temp_Arr,(char*)Temp_Array,sizeof(Temp_Array));
			Menu.ASR_Data_Refresh_Flag = TRUE;
			
            if (0x01 == Temp_Array[1])
            {
                switch(Temp_Array[2])
                {
                    case 0x01:
                    {
						Buzzer.Buzzer_ON();
                        break;
                    }
                    case 0x02:
                    {
						Buzzer.Buzzer_OFF();
                        break;
                    }
                    default:break;
                }
            }
            if (0x02 == Temp_Array[1])
            {
                switch(Temp_Array[2])
                {
                    case 0x00:
                    {
						PID.BT_Left_Right_Speed = 0;
						PID.BT_Forward_Later_Speed = 300;
                        break;
                    }
                    case 0x01:
                    {
						PID.BT_Left_Right_Speed = 0;
						PID.BT_Forward_Later_Speed = -110;
                        break;
                    }
                    case 0x02:
                    {
						PID.BT_Forward_Later_Speed = 0;
						PID.BT_Left_Right_Speed = -100;
                        break;
                    }
                    case 0x03:
                    {
						PID.BT_Forward_Later_Speed = 0;
						PID.BT_Left_Right_Speed = 100;
                        break;
                    }					
                    default:break;
                }
            }
            if (0x03 == Temp_Array[1])
            {
                switch(Temp_Array[2])
                {
                    case 0x00:
                    {
						u8g2_ClearBuffer(&myU8g2); 
						Page_Status = (Page_Status - 1 + 1) % (PAGE_MAX - 1 + 1) + 1;						
                        break;
                    }
                    default:break;
                }
            }			
        }
        else
        {
            ;
        }
    }
}

LED

  • 硬件

  • MX配置

低电平亮,高电平灭

  • 程序
led.h
cpp
#ifndef __LED_H
#define __LED_H

typedef enum
{
    LED1 = (uint8_t)0,
    LED2 = (uint8_t)1,
    LED3 = (uint8_t)2,
    LED4 = (uint8_t)3,
} Led_Type_et;

typedef struct
{
    void (*Led_Init)(void); // LED初始化
    void (*Led_ON)(Led_Type_et);   // 打开
    void (*Led_OFF)(Led_Type_et);  // 关闭
    void (*Led_Flip)(Led_Type_et); // 翻转
} Led_t;

extern Led_t Led;

#endif
led.c
cpp
/***************************************************************************
 * File: Led.c
 * Author: Luckys.
 * Date: 2023/06/23
 * description: LED
****************************************************************************/
#include "AllHead.h"

/*====================================static function declaration area BEGIN====================================*/

static void Led_Init(void);
static void Led_ON(Led_Type_et);
static void Led_OFF(Led_Type_et);
static void Led_Flip(Led_Type_et);

/*====================================static function declaration area   END====================================*/

Led_t Led = 
{
    .Led_Init = &Led_Init,
    .Led_ON = &Led_ON,
    .Led_OFF = &Led_OFF,
    .Led_Flip = &Led_Flip,
};

/*
* @function: Led_Init
* @param: None
* @retval: None
* @brief: 
*/
static void Led_Init(void)
{
    Led_OFF(LED1);
    Led_OFF(LED2);
    Led_OFF(LED3);
    Led_OFF(LED4);
}

/*
* @function: Led_ON
* @param: None
* @retval: None
* @brief: 
*/
static void Led_ON(Led_Type_et LEDx)
{
    switch (LEDx)
    {
        case LED1:
        {
            HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_RESET);
            break;
        }
        case LED2:
        {
            HAL_GPIO_WritePin(LED2_GPIO_Port, LED2_Pin, GPIO_PIN_RESET);
            break;
        }
        case LED3:
        {
            HAL_GPIO_WritePin(LED3_GPIO_Port, LED3_Pin, GPIO_PIN_RESET);
            break;
        }
        case LED4:
        {
            HAL_GPIO_WritePin(LED4_GPIO_Port, LED4_Pin, GPIO_PIN_RESET);
            break;
        }
        default:
            break;
    }
}

/*
* @function: Led_OFF
* @param: None
* @retval: None
* @brief: 
*/
static void Led_OFF(Led_Type_et LEDx)
{
    switch (LEDx)
    {
        case LED1:
        {
            HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_SET);
            break;
        }
        case LED2:
        {
            HAL_GPIO_WritePin(LED2_GPIO_Port, LED2_Pin, GPIO_PIN_SET);
            break;
        }
        case LED3:
        {
            HAL_GPIO_WritePin(LED3_GPIO_Port, LED3_Pin, GPIO_PIN_SET);
            break;
        }
        case LED4:
        {
            HAL_GPIO_WritePin(LED4_GPIO_Port, LED4_Pin, GPIO_PIN_SET);
            break;
        }
        default:
            break;
    }
}

/*
* @function: Led_Flip
* @param: None
* @retval: None
* @brief: 
*/
static void Led_Flip(Led_Type_et LEDx)
{
    switch (LEDx)
    {
        case LED1:
        {
            HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
            break;
        }
        case LED2:
        {
            HAL_GPIO_TogglePin(LED2_GPIO_Port, LED2_Pin);
            break;
        }
        case LED3:
        {
            HAL_GPIO_TogglePin(LED3_GPIO_Port, LED3_Pin);
            break;
        }
        case LED4:
        {
            HAL_GPIO_TogglePin(LED4_GPIO_Port, LED4_Pin);
            break;
        }
        default:
            break;
    }    
}

按键

  • 硬件

  • MX配置
STM32引脚号 模式
PE4 上拉输入
PE5 上拉输入
PE6 上拉输入

  • 程序
Key.h
cpp
#ifndef __KEY_H
#define __KEY_H

// 读取按键电平
#define READ_KEY1    HAL_GPIO_ReadPin(K1_GPIO_Port,K1_Pin)
#define READ_KEY2    HAL_GPIO_ReadPin(K2_GPIO_Port,K2_Pin)
#define READ_KEY3    HAL_GPIO_ReadPin(K3_GPIO_Port,K3_Pin)

typedef enum
{
    KEY_NULL  = (uint8_t)0x00,  // 无按键按下键值
    KEY1_DOWN = (uint8_t)0x01,  // 按键1按下键值  
    KEY2_DOWN = (uint8_t)0x02,  // 按键2按下键值
    KEY3_DOWN = (uint8_t)0x03,  // 按键3按下键值
}Key_Status_t;

typedef struct
{
    uint16_t volatile vusKey_Timer_Count; // 长按计数
    uint8_t volatile vucKey_Flag_Arr[6];  // 按键标志位(短长按)
    void (*Key_Scan)(void);   // 按键三行消抖---按键扫描
    void (*Key_Handler)(void);    // 按键处理
}Key_t;

extern Key_t Key;


#endif
Key.c
cpp
/***************************************************************************
 * File: Key.c
 * Author: Luckys.
 * Date: 2023/06/23
 * description: 

****************************************************************************/
#include "AllHead.h"

/*====================================variable definition declaration area BEGIN===================================*/
static uint8_t ucKey_Value,ucKey_Up,ucKey_Down;
/*====================================variable definition declaration area   END===================================*/

/*====================================static function declaration area BEGIN====================================*/

static uint8_t Key_Return_Value(void); 
static void Key_Scan(void);
static void Key_Handler(void);
/*====================================static function declaration area   END====================================*/

Key_t Key = 
{
    .vusKey_Timer_Count = 0,
    .vucKey_Flag_Arr = {FALSE},
    .Key_Scan = &Key_Scan,
    .Key_Handler = &Key_Handler
};


/*
* @function: Key_Return_Value
* @param: None
* @retval: None
* @brief: 
*/
static uint8_t Key_Return_Value(void)
{
    if ((!READ_KEY1) || (!READ_KEY2) || (!READ_KEY3))
    {
        if (!READ_KEY1)
        {
            return KEY1_DOWN;
        }
        else if (!READ_KEY2)
        {
            return KEY2_DOWN;
        }
        else if (!READ_KEY3)
        {
            return KEY3_DOWN;
        }            
    }

    return KEY_NULL;
}

/*
* @function: Key_Scan
* @param: None
* @retval: None
* @brief:
*/
static void Key_Scan(void)
{
    static uint8_t uckey_old;
    static uint8_t long_press_triggered = 0;
    
    ucKey_Value = Key_Return_Value();                     
    ucKey_Up = ~ucKey_Value & (uckey_old ^ ucKey_Value);
    ucKey_Down = ucKey_Value & (uckey_old ^ ucKey_Value);
    uckey_old = ucKey_Value;                         

    if (ucKey_Down)
    {
        Key.vusKey_Timer_Count = 0;
        long_press_triggered = 0; 
    }

    if (Key.vusKey_Timer_Count < 10)
    {
        switch (ucKey_Up)
        {
        case KEY1_DOWN:
            Key.vucKey_Flag_Arr[0] = TRUE;
            break;
        case KEY2_DOWN:
            Key.vucKey_Flag_Arr[1] = TRUE;
            break;
        case KEY3_DOWN:
            Key.vucKey_Flag_Arr[2] = TRUE;
            break;
            break;
        default:
            break;
        }
    }
    else
    {
        if (!long_press_triggered) 
        {
            switch (ucKey_Value) 
            {
            case KEY1_DOWN:
                Key.vucKey_Flag_Arr[3] = TRUE;
                break;
            case KEY2_DOWN:
                Key.vucKey_Flag_Arr[4] = TRUE;
                break;
            case KEY3_DOWN:
                Key.vucKey_Flag_Arr[5] = TRUE;
                break;
            default:
                break;
            }
            long_press_triggered = 1;
        }
    }
}

/*
* @function: Key_Handler
* @param: None
* @retval: None
* @brief:
*/
static void Key_Handler(void)
{
    if (Key.vucKey_Flag_Arr[0])
    {
        Key.vucKey_Flag_Arr[0] = FALSE;
        u8g2_ClearBuffer(&myU8g2); 
        Page_Status = (Page_Status - 1 + 1) % (PAGE_MAX - 1 + 1) + 1;		
    }

    else if (Key.vucKey_Flag_Arr[1])
    {
        Key.vucKey_Flag_Arr[1] = FALSE;
    }

    else if (Key.vucKey_Flag_Arr[2])
    {
        Key.vucKey_Flag_Arr[2] = FALSE;
        Motor.Motor_Set_PWM(Motor_LEFT,0);
        Motor.Motor_Set_PWM(Motor_RIGHT,0);        
    }

    else if (Key.vucKey_Flag_Arr[3])
    {
        Key.vucKey_Flag_Arr[3] = FALSE;
    }

    else if (Key.vucKey_Flag_Arr[4])
    {
        Key.vucKey_Flag_Arr[4] = FALSE;
    }  

    else if (Key.vucKey_Flag_Arr[5])
    {
        Key.vucKey_Flag_Arr[5] = FALSE;
    }              
}

OLED

  • 硬件

  • MX配置
STM32引脚号 模式
PC8(SCL) 推挽输出
PC9(SDA) 推挽输出

  • 程序

使用U8g2库

UI.h
cpp
#ifndef __UI_H
#define __UI_H
#include "U8g2.h"

#define NOT_Compile 0

// 引脚定义
#define OLED_SCL_High() HAL_GPIO_WritePin(OLED_SCL_GPIO_Port, OLED_SCL_Pin, GPIO_PIN_SET)
#define OLED_SCL_Low() HAL_GPIO_WritePin(OLED_SCL_GPIO_Port, OLED_SCL_Pin, GPIO_PIN_RESET)
#define OLED_SDA_High() HAL_GPIO_WritePin(OLED_SDA_GPIO_Port, OLED_SDA_Pin, GPIO_PIN_SET)
#define OLED_SDA_Low() HAL_GPIO_WritePin(OLED_SDA_GPIO_Port, OLED_SDA_Pin, GPIO_PIN_RESET)

typedef struct
{
    void (*UI_U8g2_Init)(void); // U8g2初始化
    void (*UI_Update_Battery)(u8g2_uint_t, u8g2_uint_t);    // 实时更新电池电量
    void (*UI_Update_BT_Connected)(void);   // 蓝牙连接状态UI
    void (*UI_Update_BT_Unconnected)(void); // 蓝牙连接断开状态UI
	void (*UI_Update_Message)(void);	// 串口有数据时状态
} UI_st;

extern UI_st UI;
extern u8g2_t myU8g2;
extern uint8_t ui_logo_battery[];
extern uint8_t ui_logo_signal[];
extern uint8_t ui_logo_bluetooth[];

#endif
UI.c
cpp
/***************************************************************************
 * File: xxx.c
 * Author: Luckys.
 * Date: 2023/08/13
 * description: 描述
****************************************************************************/
#include "AllHead.h"
#include "U8g2.h"

/*====================================variable definition declaration area BEGIN===================================*/
u8g2_t myU8g2; // 定义结构体

// 电池icon
uint8_t ui_logo_battery[] = {
    0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x7E, 0x00, 0xC0, 0xFF, 0x03, 0xE0, 0xFF, 0x07, 0x60, 0x00, 0x06, 0x60, 0x30, 0x06, 0x60, 0x38, 0x06, 0x60, 0x38, 0x06, 0x60, 0x1C, 0x06, 0x60, 0x0C,
    0x06, 0x60, 0x0E, 0x06, 0x60, 0x7E, 0x06, 0x60, 0x7E, 0x06, 0x60, 0x70, 0x06, 0x60, 0x70, 0x06, 0x60, 0x38, 0x06, 0x60, 0x18, 0x06, 0x60, 0x1C, 0x06, 0x60, 0x08, 0x06, 0x60, 0x00, 0x06, 0xE0,
    0xFF, 0x07, 0xC0, 0xFF, 0x03, 0x00, 0x00, 0x00,/* E:\Code\ST\F103C8Tx\Resources\icon\Battery.bmp */0
};
// 连接断开icon
uint8_t ui_logo_connect_failed[] = {
    0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0xE0, 0xF1, 0x07, 0xC0, 0xFF, 0x1F, 0x80, 0xFF, 0x3F, 0x80, 0x1F, 0x3C, 0x00, 0x0F, 0x78, 0x60, 0x1E, 0x70, 0xF0, 0xDC, 0x71, 0xF8, 0xF8, 0x71, 0x7C, 0xF8,
    0x71, 0x3C, 0xF0, 0x70, 0x1E, 0xE6, 0x79, 0x0E, 0xCF, 0x3F, 0x8E, 0x8F, 0x3F, 0x8E, 0x87, 0x1F, 0x0E, 0x01, 0x0F, 0x0E, 0x60, 0x1E, 0x1E, 0xF0, 0x3C, 0x7C, 0xFC, 0x38, 0xF8, 0x3F, 0x78, 0xF0,
    0x1F, 0x70, 0xC0, 0x07, 0x00, 0x00, 0x00, 0x00,/* E:\Code\ST\F103C8Tx\Resources\icon\connect_failed.bmp */0
};
// 连接成功icon
uint8_t ui_logo_signal[] = {
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0x1C, 0x00, 0xE0,
    0x1C, 0x00, 0xE0, 0x1C, 0x00, 0xE0, 0x1C, 0x00, 0xE7, 0x1C, 0x00, 0xE7, 0x1C, 0x38, 0xE7, 0x1C, 0x38, 0xE7, 0x1C, 0x38, 0xE7, 0x1C, 0x38, 0xE7, 0x1C, 0x10, 0x42, 0x08, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,/* E:\Code\ST\F103C8Tx\Resources\icon\signal.bmp */0
};
// 蓝牙icon
uint8_t ui_logo_bluetooth[] = {
    0x00, 0x38, 0x00, 0x00, 0xF8, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x98, 0x07, 0x00, 0x18, 0x0F, 0x18, 0x18, 0x1C, 0x78, 0x18, 0x1E, 0xF0, 0x18, 0x0F, 0xC0, 0x99, 0x03, 0x80, 0xFF, 0x01, 0x00, 0xFF,
    0x00, 0x00, 0x7E, 0x00, 0x00, 0x7E, 0x00, 0x00, 0xFF, 0x00, 0x80, 0xFF, 0x01, 0xC0, 0x99, 0x07, 0xF0, 0x18, 0x0F, 0x78, 0x18, 0x1E, 0x18, 0x18, 0x1C, 0x00, 0x18, 0x0F, 0x00, 0x98, 0x07, 0x00,
    0xF8, 0x01, 0x00, 0xF8, 0x00, 0x00, 0x38, 0x00,/* E:\Code\ST\F103C8Tx\Resources\icon\bluetooth.bmp */0
};

// 信息
uint8_t ui_logo_Message[] = {0x00,0x00,0xE0,0x07,0x10,0x08,0x08,0x10,0x84,0x21,0x82,0x41,0x02,0x40,0x82,0x41,0x82,0x41,0x82,0x41,0x82,0x41,0x84,0x21,0x88,0x11,0x10,0x08,0xE0,0x07,0x00,0x00};

/*====================================variable definition declaration area   END===================================*/

/*====================================static function declaration area BEGIN====================================*/
static uint8_t u8x8_stm32_gpio_and_delay(U8X8_UNUSED u8x8_t *, U8X8_UNUSED uint8_t, U8X8_UNUSED uint8_t, U8X8_UNUSED void *);
static void UI_U8g2_Init(void);
static void UI_Draw_ProgressBar(u8g2_t *);
static void UI_Update_Battery(u8g2_uint_t, u8g2_uint_t);
static void UI_Update_BT_Connected(void);
static void UI_Update_BT_Unconnected(void);
static void UI_Update_Message(void);	

#if NOT_Compile
static void draw(u8g2_t *);
#endif
/*====================================static function declaration area   END====================================*/
UI_st UI = 
{
    .UI_U8g2_Init = &UI_U8g2_Init,
    .UI_Update_Battery = &UI_Update_Battery,
    .UI_Update_BT_Connected = &UI_Update_BT_Connected,
    .UI_Update_BT_Unconnected = &UI_Update_BT_Unconnected,
	.UI_Update_Message = &UI_Update_Message
};

/*
* @function: u8x8_stm32_gpio_and_delay
* @param: u8x8--u8x8_t结构体指针 msg--表示消息类型的参数,用于指示执行何种操作 arg_int--传递整型参数的参数,用法取决于msg arg_ptr--传递指针参数的参数,用法取决于msg
* @retval: TRUE--成功,FALSE--失败
* @brief: 回调函数
*/
static uint8_t u8x8_stm32_gpio_and_delay(U8X8_UNUSED u8x8_t *u8x8, U8X8_UNUSED uint8_t msg, U8X8_UNUSED uint8_t arg_int, U8X8_UNUSED void *arg_ptr)
{
    switch (msg)
    {
    case U8X8_MSG_DELAY_MILLI: // Function which implements a delay, arg_int contains the amount of ms
    {
        Public.Public_Delay_ms(arg_int);
        break;
    }
    case U8X8_MSG_DELAY_10MICRO: // Function which delays 10us
    {
        Public.Public_Delay_us(10);
        break;
    }
    case U8X8_MSG_DELAY_100NANO: // Function which delays 100ns
    {
        __NOP();
        break;
    }
    case U8X8_MSG_GPIO_I2C_CLOCK:   // 更改SCL IO 方向
    {
        if (arg_int)
        {
            OLED_SCL_High();
        }
        else
        {
            OLED_SCL_Low();
        }
        break;
    }
    case U8X8_MSG_GPIO_I2C_DATA:   // 更改SDA IO 方向
    {
        if (arg_int)
        {
            OLED_SDA_High();
        }
        else
        {
            OLED_SDA_Low();
        }        
        break;
    }    
    default: // A message was received which is not implemented, return 0 to indicate an error
    {
        return FALSE;
    }
    }
    return TRUE;
}

/*
* @function: UI_U8g2_Init
* @param: None
* @retval: None
* @brief: U8g2初始化
*/
static void UI_U8g2_Init(void)
{
    // 参数1--u8g2_t结构体 参数2--屏幕方向(R0顺时针0度 R1顺时针90度 R2顺时针180度 R3顺时针270度 U8G2_MIRROR是R0的镜像) 参数3--驱动方式 参数4--回调函数(自己写)
    u8g2_Setup_ssd1306_i2c_128x64_noname_f(&myU8g2, U8G2_R0, u8x8_byte_sw_i2c, u8x8_stm32_gpio_and_delay);
    u8g2_InitDisplay(&myU8g2); // 根据所选的芯片进行初始化工作,初始化完成后,显示器处于关闭状态
    u8g2_SetPowerSave(&myU8g2, 0);
    u8g2_ClearBuffer(&myU8g2);
    UI_Draw_ProgressBar(&myU8g2); // 进度条
    u8g2_ClearBuffer(&myU8g2);
}

/*
* @function: UI_Draw_ProgressBar
* @param: None
* @retval: None
* @brief: 绘制进度条
*/
static void UI_Draw_ProgressBar(u8g2_t *u8g2)
{
    for (int i = 10; i <= 80; i = i + 2)
    {
        u8g2_ClearBuffer(u8g2);

        char buff[20];
        sprintf(buff, "%d%%", (int)(i / 80.0 * 100));

        u8g2_SetFont(u8g2, u8g2_font_ncenB12_tf);
        u8g2_DrawStr(u8g2, 0, 32, "Hardware Init"); // 字符显示(x横坐标y纵坐标)

        u8g2_SetFont(u8g2, u8g2_font_ncenB08_tf);
        u8g2_DrawStr(u8g2, 100, 49, buff); // 当前进度显示

        u8g2_DrawRBox(u8g2, 16, 40, i, 10, 4);    // 圆角填充框矩形框
        u8g2_DrawRFrame(u8g2, 16, 40, 80, 10, 4); // 圆角矩形

        u8g2_SendBuffer(u8g2);
    }
}

#if NOT_Compile

/*
* @function: draw
* @param: None
* @retval: None
* @brief: 官方提供的Logo绘制demo
*/
static void draw(u8g2_t *u8g2)
{
    u8g2_SetFontMode(u8g2, 1);              // 字体模式选择
    u8g2_SetFontDirection(u8g2, 0);         // 字体方向选择
    u8g2_SetFont(u8g2, u8g2_font_inb24_mf); // 字库选择
    u8g2_DrawStr(u8g2, 0, 20, "U");

    u8g2_SetFontDirection(u8g2, 1);
    u8g2_SetFont(u8g2, u8g2_font_inb30_mn);
    u8g2_DrawStr(u8g2, 21, 8, "8");

    u8g2_SetFontDirection(u8g2, 0);
    u8g2_SetFont(u8g2, u8g2_font_inb24_mf);
    u8g2_DrawStr(u8g2, 51, 30, "g");
    u8g2_DrawStr(u8g2, 67, 30, "\xb2");

    u8g2_DrawHLine(u8g2, 2, 35, 47);
    u8g2_DrawHLine(u8g2, 3, 36, 47);
    u8g2_DrawVLine(u8g2, 45, 32, 12);
    u8g2_DrawVLine(u8g2, 46, 33, 12);
}

#endif



/*
* @function: UI_Update_Battery
* @param: u8g2--结构体 x--要打印的左下角x轴起始坐标 y--要打印的左下角y轴起始坐标
* @retval: None
* @brief: 实时更新电池电量
*/
static void UI_Update_Battery(u8g2_uint_t x, u8g2_uint_t y)
{
    u8g2_SetDrawColor(&myU8g2, 0);        // 开启反色
    u8g2_DrawBox(&myU8g2, 60, 0, 30, 24); // 局部刷新
    u8g2_SetDrawColor(&myU8g2, 1);

    char buf[8] = {0};
    sprintf(buf, "%3d%%", (uint8_t)myADC.ADC_Get_Calculate());
    u8g2_SetFont(&myU8g2, u8g2_font_inb16_mr);
    u8g2_DrawStr(&myU8g2, x, y, buf);
    u8g2_SendBuffer(&myU8g2);
}

/*
* @function: UI_Update_BT_Connected
* @param: None
* @retval: None
* @brief: 蓝牙连接状态UI
*/
static void UI_Update_BT_Connected(void)
{
    u8g2_SetDrawColor(&myU8g2, 0);    // 开启反色
    u8g2_DrawBox(&myU8g2, 24, 1, 24, 24);  // 局部刷新
    u8g2_SetDrawColor(&myU8g2, 1); 
    u8g2_DrawXBMP(&myU8g2, 24, 1, 24, 24, ui_logo_signal);
    u8g2_SendBuffer(&myU8g2);
}

/*
* @function: UI_Update_BT_Unconnected
* @param: None
* @retval: None
* @brief: 蓝牙连接断开状态UI
*/
static void UI_Update_BT_Unconnected(void)
{
    u8g2_SetDrawColor(&myU8g2, 0);        // 开启反色
    u8g2_DrawBox(&myU8g2, 24, 1, 24, 24);  // 局部刷新
    u8g2_SetDrawColor(&myU8g2, 1); 
    u8g2_DrawXBMP(&myU8g2, 24, 1, 24, 24, ui_logo_connect_failed);
    u8g2_SendBuffer(&myU8g2);
}

/*
* @function: UI_Update_Message
* @param: None
* @retval: None
* @brief: 信息显示16x16
*/
static void UI_Update_Message(void)
{
    u8g2_SetDrawColor(&myU8g2, 0);        // 开启反色
    u8g2_DrawBox(&myU8g2, 45, 47, 16, 16);  // 局部刷新
    u8g2_SetDrawColor(&myU8g2, 1); 
    u8g2_DrawXBMP(&myU8g2, 45, 47, 16, 16, ui_logo_Message);
    u8g2_SendBuffer(&myU8g2);
}
Menu.h
cpp
#ifndef __MENU_H
#define __MENU_H

// 最大页面数量
#define PAGE_MAX 12

typedef enum
{
    // 默认页面
    PAGE_Default = (uint8_t)1,
    // 欧拉角参数
    PAGE_6050_EulerAngle = (uint8_t)2,
    // 角速度参数
    PAGE_6050_AngleSpeed = (uint8_t)3,  
    // 系统参数(电池电压,MPU6050芯片温度)
    PAGE_SYS_Parameter = (uint8_t)4,
    // 直立环可调参数
    PAGE_Upright_Ring = (uint8_t)5,
	// 速度环可调参数
	PAGE_Speed_Ring = (uint8_t)6,
	// 转向环可调参数
	PAGE_TurnTo_Ring = (uint8_t)7,
	// 3个环单独的PWM输出
	PAGE_Ring_PWM = (uint8_t)8,
	// 两个电机的最终PWM输出和占空比
	PAGE_Motor_PWM_Duty = (uint8_t)9,
	// 两个电机的编码器值
	PAGE_Encoder = (uint8_t)10,
	// 蓝牙数据帧
	PAGE_BT_Data = (uint8_t)11,
	// ASR语音数据帧
	PAGE_ASR_Data = (uint8_t)12
} Page_Status_et;

typedef enum
{
    CAR_MODE_Balance = (uint8_t)0,
    CAR_MODE_Follow = (uint8_t)1,
    CAR_MODE_Track = (uint8_t)2
} Car_Mode_et;


typedef struct
{
	uint8_t ASR_Data_Refresh_Flag;	// ASR接收数据页面刷新标志位
	uint8_t BT_Data_Refresh_Flag;	// 蓝牙接收数据页面刷新标志位
    uint8_t BT_Connect_Flag;    // 蓝牙连接标志位
    void (*Menu_Init)(void);
    void (*Menu_Switch)(void);
} Menu_st;

extern Menu_st Menu;
extern Page_Status_et Page_Status;
extern Car_Mode_et Car_Mode;
#endif
Menu.c
cpp
/***************************************************************************
 * File: xxx.c
 * Author: Luckys.
 * Date: 2023/08/13
 * description: 描述
****************************************************************************/
#include "AllHead.h"
#include "U8g2.h"

/*====================================variable definition declaration area BEGIN===================================*/
Page_Status_et Page_Status;
Car_Mode_et Car_Mode;
/*====================================variable definition declaration area   END===================================*/

/*====================================static function declaration area BEGIN====================================*/
static void Menu_Init(void);
static void Menu_Switch(void);
static void Menu_Display_1(void);
static void Menu_Display_2(void);
static void Menu_Display_3(void);
static void Menu_Display_4(void);
static void Menu_Display_5(void);
static void Menu_Display_6(void);
static void Menu_Display_7(void);
static void Menu_Display_8(void);
static void Menu_Display_9(void);
static void Menu_Display_10(void);
static void Menu_Display_11(const uint8_t *);
static void Menu_Display_12(const uint8_t *);
/*====================================static function declaration area   END====================================*/

Menu_st Menu = 
{
	.ASR_Data_Refresh_Flag = FALSE,
	.BT_Data_Refresh_Flag = FALSE,
    .BT_Connect_Flag = FALSE,
    .Menu_Init = &Menu_Init,
    .Menu_Switch = &Menu_Switch
};

/*
* @function: Menu_Init
* @param: None
* @retval: None
* @brief: 描述
*/
static void Menu_Init(void)
{
    Page_Status = PAGE_Default;
    Car_Mode = CAR_MODE_Balance;
}

/*
* @function: Menu_Switch
* @param: None
* @retval: None
* @brief: 描述
*/
static void Menu_Switch(void)
{
    switch (Page_Status)
    {
        case PAGE_Default:
        {
            Menu_Display_1();
            break;
        }
        case PAGE_6050_EulerAngle:
        {
            Menu_Display_2();
            break;
        }
        case PAGE_6050_AngleSpeed:
        {
            Menu_Display_3();
            break;
        }
        case PAGE_SYS_Parameter:
        {
            Menu_Display_4();
            break;
        }
        case PAGE_Upright_Ring:
        {
            Menu_Display_5();
            break;
        }
		case PAGE_Speed_Ring:
		{
			Menu_Display_6();
			break;
		}
		case PAGE_TurnTo_Ring:
		{
			Menu_Display_7();
			break;
		}
		case PAGE_Ring_PWM:
		{
			Menu_Display_8();
			break;
		}
		case PAGE_Motor_PWM_Duty:
		{
			Menu_Display_9();
			break;
		}	
		case PAGE_Encoder:
		{
			Menu_Display_10();
			break;
		}	
		case PAGE_BT_Data:
		{
			Menu_Display_11(HC05.BT_Rec_Temp_Arr);
			break;
		}
		case PAGE_ASR_Data:
		{
			Menu_Display_12(Voice.ASR_Rec_Temp_Arr);
			break;
		}
        default:
        {
            Menu_Display_1();
            break;
        }
    }
}

/*
* @function: Menu_Display_1
* @param: None
* @retval: None
* @brief: 描述
*/
static void Menu_Display_1(void)
{
    char buff[20];
        
    // 默认显示
    u8g2_DrawXBMP(&myU8g2, 106, 0, 24, 24, ui_logo_battery);
    u8g2_DrawXBMP(&myU8g2, 0, 0, 24, 24, ui_logo_bluetooth);
    if (TRUE == Menu.BT_Connect_Flag)
    {
        UI.UI_Update_BT_Connected();
    }
    else
    {
        UI.UI_Update_BT_Unconnected();
    }
    u8g2_SendBuffer(&myU8g2);
    UI.UI_Update_Battery(50, 20);

    u8g2_SetDrawColor(&myU8g2, 0);         // 开启反色
    u8g2_DrawBox(&myU8g2, 40, 40, 88, 24); // 局部刷新
    u8g2_SetDrawColor(&myU8g2, 1);

    u8g2_SetFont(&myU8g2,u8g2_font_helvR14_tr);
    if (CAR_MODE_Balance == Car_Mode)
    {
        u8g2_DrawStr(&myU8g2,0,60,"Mode:Balance");
    }
    else if (CAR_MODE_Follow == Car_Mode)
    {
        u8g2_DrawStr(&myU8g2,0,60,"Mode:Follow");
    }
    else if (CAR_MODE_Track == Car_Mode)
    {
        u8g2_DrawStr(&myU8g2,0,60,"Mode:Track");
    }
    else
    {
        ;
    }
	u8g2_SendBuffer(&myU8g2); 
}

/*
* @function: Menu_Display_2
* @param: None
* @retval: None
* @brief: 描述
*/
static void Menu_Display_2(void)
{
    char buf[20];

    u8g2_SetFont(&myU8g2,u8g2_font_helvR14_tr);
    u8g2_SetDrawColor(&myU8g2, 0);         // 开启反色
    u8g2_DrawBox(&myU8g2, 20, 0, 128, 64); // 局部刷新
    u8g2_SetDrawColor(&myU8g2, 1);

    sprintf(buf, "Pitch: %.2f", myMPU6050.mpu_pitch);
    u8g2_DrawStr(&myU8g2, 0, 20, buf);

    sprintf(buf, "Roll: %.2f", myMPU6050.mpu_roll);
    u8g2_DrawStr(&myU8g2, 0, 40, buf);

    sprintf(buf, "Yaw: %.2f", myMPU6050.mpu_yaw);
    u8g2_DrawStr(&myU8g2, 0, 60, buf);
    u8g2_SendBuffer(&myU8g2);         
}

/*
* @function: Menu_Display_3
* @param: None
* @retval: None
* @brief: 描述
*/
static void Menu_Display_3(void)
{
    char buf[20];

    u8g2_SetFont(&myU8g2,u8g2_font_helvR14_tr);
    u8g2_SetDrawColor(&myU8g2, 0);         // 开启反色
    u8g2_DrawBox(&myU8g2, 20, 0, 128, 64); // 局部刷新
    u8g2_SetDrawColor(&myU8g2, 1);

    sprintf(buf, "gyrox: %d", myMPU6050.gyro_x);
    u8g2_DrawStr(&myU8g2, 0, 20, buf);

    sprintf(buf, "gyroy: %d", myMPU6050.gyro_y);
    u8g2_DrawStr(&myU8g2, 0, 40, buf);

    sprintf(buf, "gyroz: %d", myMPU6050.gyro_z);
    u8g2_DrawStr(&myU8g2, 0, 60, buf);
    u8g2_SendBuffer(&myU8g2);         
}

/*
* @function: Menu_Display_4
* @param: None
* @retval: None
* @brief: 描述
*/
static void Menu_Display_4(void)
{
    char buf[20];

    myADC.ADC_Get_Calculate();
    myMPU6050.MPU6050_Temperature = (float)MPU_Get_Temperature() / 100;
    
    u8g2_SetFont(&myU8g2,u8g2_font_helvR14_tr);
    u8g2_SetDrawColor(&myU8g2, 0);         // 开启反色
    u8g2_DrawBox(&myU8g2, 20, 0, 128, 64); // 局部刷新
    u8g2_SetDrawColor(&myU8g2, 1);

    sprintf(buf, "BAT: %.2fV", myADC.Power_V);
    u8g2_DrawStr(&myU8g2, 0, 20, buf);

    sprintf(buf, "Temp: %.2f",myMPU6050.MPU6050_Temperature);
    u8g2_DrawStr(&myU8g2, 0, 60, buf);
    u8g2_SendBuffer(&myU8g2);            
}

/*
* @function: Menu_Display_5
* @param: None
* @retval: None
* @brief: 描述
*/
static void Menu_Display_5(void)
{
    char buf[30];

    u8g2_SetDrawColor(&myU8g2, 0);         // 开启反色
    u8g2_DrawBox(&myU8g2, 0, 0, 128, 64); // 局部刷新
    u8g2_SetDrawColor(&myU8g2, 1);

    u8g2_SetFont(&myU8g2,u8g2_font_helvR12_tr);
    u8g2_DrawStr(&myU8g2, 18, 15, (const char*)"Upright Ring");

    u8g2_SetFont(&myU8g2,u8g2_font_ncenB08_tf);
    sprintf(buf, "zero: %.2f", PID.Car_Zero);
    u8g2_DrawStr(&myU8g2, 0, 30, buf);

    sprintf(buf, "Kp: %.3f", Upright.Kp);
    u8g2_DrawStr(&myU8g2, 0, 45, buf);

    sprintf(buf, "Kd: %.3f",Upright.Kd);
    u8g2_DrawStr(&myU8g2, 0, 60, buf);
    u8g2_SendBuffer(&myU8g2);        
}

/*
* @function: Menu_Display_6
* @param: None
* @retval: None
* @brief: 描述
*/
static void Menu_Display_6(void)
{
    char buf[30];

    u8g2_SetDrawColor(&myU8g2, 0);         // 开启反色
    u8g2_DrawBox(&myU8g2, 0, 0, 128, 64); // 局部刷新
    u8g2_SetDrawColor(&myU8g2, 1);

    u8g2_SetFont(&myU8g2,u8g2_font_helvR12_tr);
    u8g2_DrawStr(&myU8g2, 15, 13, (const char*)"Speed Ring");

    u8g2_SetFont(&myU8g2,u8g2_font_ncenB08_tf);
    sprintf(buf, "T_Speed: %.2f", PID.BT_Forward_Later_Speed);
    u8g2_DrawStr(&myU8g2, 0, 30, buf);

    sprintf(buf, "Kp: %.5f", Speed.Kp);
    u8g2_DrawStr(&myU8g2, 0, 45, buf);

    sprintf(buf, "Ki: %.5f",Speed.Ki);
    u8g2_DrawStr(&myU8g2, 0, 60, buf);
    u8g2_SendBuffer(&myU8g2);        
}

/*
* @function: Menu_Display_7
* @param: None
* @retval: None
* @brief: 描述
*/
static void Menu_Display_7(void)
{
    char buf[30];

    u8g2_SetDrawColor(&myU8g2, 0);         // 开启反色
    u8g2_DrawBox(&myU8g2, 0, 0, 128, 64); // 局部刷新
    u8g2_SetDrawColor(&myU8g2, 1);

    u8g2_SetFont(&myU8g2,u8g2_font_helvR12_tr);
    u8g2_DrawStr(&myU8g2, 16, 13, (const char*)"TurnTo Ring");

    u8g2_SetFont(&myU8g2,u8g2_font_ncenB08_tf);
    sprintf(buf, "T_Yaw: %.2f", PID.BT_Left_Right_Speed);
    u8g2_DrawStr(&myU8g2, 0, 30, buf);

    sprintf(buf, "Kp: %.3f", Turn_To.Kp);
    u8g2_DrawStr(&myU8g2, 0, 45, buf);

    sprintf(buf, "Kd: %.3f",Turn_To.Kd);
    u8g2_DrawStr(&myU8g2, 0, 60, buf);
    u8g2_SendBuffer(&myU8g2);        
}

/*
* @function: Menu_Display_8
* @param: None
* @retval: None
* @brief: 描述
*/
static void Menu_Display_8(void)
{
    char buf[30];

    u8g2_SetDrawColor(&myU8g2, 0);         // 开启反色
    u8g2_DrawBox(&myU8g2, 0, 0, 128, 64); // 局部刷新
    u8g2_SetDrawColor(&myU8g2, 1);

    u8g2_SetFont(&myU8g2,u8g2_font_helvR12_tr);
    u8g2_DrawStr(&myU8g2, 16, 13, (const char*)"Ring PWM");

    u8g2_SetFont(&myU8g2,u8g2_font_ncenB08_tf);
    sprintf(buf, "Upright PWM: %d",PID.Upright_Pwm_Out);
    u8g2_DrawStr(&myU8g2, 0, 30, buf);

    sprintf(buf, "Speed PWM: %d", PID.Speed_Pwm_Out);
    u8g2_DrawStr(&myU8g2, 0, 45, buf);

    sprintf(buf, "TurnTo PWM: %d",PID.TurnTo_Pwm_Out);
    u8g2_DrawStr(&myU8g2, 0, 60, buf);
    u8g2_SendBuffer(&myU8g2);        
}

/*
* @function: Menu_Display_9
* @param: None
* @retval: None
* @brief: 描述
*/
static void Menu_Display_9(void)
{
    char buf[30];

    u8g2_SetDrawColor(&myU8g2, 0);         // 开启反色
    u8g2_DrawBox(&myU8g2, 0, 0, 128, 64); // 局部刷新
    u8g2_SetDrawColor(&myU8g2, 1);

    u8g2_SetFont(&myU8g2,u8g2_font_helvR12_tr);
    u8g2_DrawStr(&myU8g2, 16, 13, (const char*)"Motor PWM");

    u8g2_SetFont(&myU8g2,u8g2_font_ncenB08_tf);
    sprintf(buf, "Left: %d -- %.2f%%\r\n", PID.Left_Pwm_Out,((float)PID.Left_Pwm_Out / TIM3->ARR) * 100);
    u8g2_DrawStr(&myU8g2, 0, 30, buf);

    sprintf(buf, "Right: %d -- %.2f%%", PID.Right_Pwm_Out,((float)PID.Right_Pwm_Out / TIM3->ARR) * 100);
    u8g2_DrawStr(&myU8g2, 0, 45, buf);

    u8g2_SendBuffer(&myU8g2);        
}

/*
* @function: Menu_Display_10
* @param: None
* @retval: None
* @brief: 描述
*/
static void Menu_Display_10(void)
{
    char buf[30];

    u8g2_SetDrawColor(&myU8g2, 0);         // 开启反色
    u8g2_DrawBox(&myU8g2, 0, 0, 128, 64); // 局部刷新
    u8g2_SetDrawColor(&myU8g2, 1);

    u8g2_SetFont(&myU8g2,u8g2_font_helvR12_tr);
    u8g2_DrawStr(&myU8g2, 12, 13, (const char*)"Encoder Speed");

    u8g2_SetFont(&myU8g2,u8g2_font_ncenB08_tf);
    sprintf(buf, "Left: %d",Encoder.Left_Speed);
    u8g2_DrawStr(&myU8g2, 0, 30, buf);

    sprintf(buf, "Right: %d", Encoder.Right_Speed);
    u8g2_DrawStr(&myU8g2, 0, 45, buf);

    u8g2_SendBuffer(&myU8g2);        
}

/*
* @function: Menu_Display_11
* @param: None
* @retval: None
* @brief: 描述
*/
static void Menu_Display_11(const uint8_t * p_Arr)
{
	static uint8_t Display_Cnt;	// 用于限制ico显示停留的时间
	
    u8g2_SetFont(&myU8g2, u8g2_font_helvR12_tr);
    u8g2_DrawStr(&myU8g2, 25, 13, (const char *)"BT Data");
	
	if (10 == Display_Cnt)	// 清除ico
	{
		u8g2_SetDrawColor(&myU8g2, 0);        // 开启反色
		u8g2_DrawBox(&myU8g2, 45, 47, 16, 16);  // 局部刷新
		u8g2_SetDrawColor(&myU8g2, 1); 		
	}
	
    if (TRUE == Menu.BT_Data_Refresh_Flag)
    {
        char buf[30];

        Menu.BT_Data_Refresh_Flag = FALSE;
		Display_Cnt = 0;
		
		u8g2_SetDrawColor(&myU8g2, 0);        // 开启反色
		u8g2_DrawBox(&myU8g2, 0, 20, 128, 64); // 局部刷新
		u8g2_SetDrawColor(&myU8g2, 1);
		
        u8g2_SetFont(&myU8g2, u8g2_font_ncenB10_tf);
        sprintf(buf, "%02X - %02X - %02X - %02X", p_Arr[0], p_Arr[1], p_Arr[2], p_Arr[3]);
        u8g2_DrawStr(&myU8g2, 0, 40, buf);
        UI.UI_Update_Message();	
    }
	Display_Cnt++;
    u8g2_SendBuffer(&myU8g2);
}

/*
* @function: Menu_Display_12
* @param: None
* @retval: None
* @brief: 描述
*/
static void Menu_Display_12(const uint8_t * p_Arr)
{
	static uint8_t Display_Cnt;	// 用于限制ico显示停留的时间
	
    u8g2_SetFont(&myU8g2, u8g2_font_helvR12_tr);
    u8g2_DrawStr(&myU8g2, 20, 13, (const char *)"ASR Data");
	
	if (10 == Display_Cnt)	// 清除ico
	{
		u8g2_SetDrawColor(&myU8g2, 0);        // 开启反色
		u8g2_DrawBox(&myU8g2, 45, 47, 16, 16);  // 局部刷新
		u8g2_SetDrawColor(&myU8g2, 1); 		
	}
	
    if (TRUE == Menu.ASR_Data_Refresh_Flag)
    {
        char buf[30];

        Menu.ASR_Data_Refresh_Flag = FALSE;
		Display_Cnt = 0;
		
		u8g2_SetDrawColor(&myU8g2, 0);        // 开启反色
		u8g2_DrawBox(&myU8g2, 0, 20, 128, 64); // 局部刷新
		u8g2_SetDrawColor(&myU8g2, 1);
		
        u8g2_SetFont(&myU8g2, u8g2_font_ncenB10_tf);
        sprintf(buf, "%02X - %02X - %02X - %02X", p_Arr[0], p_Arr[1], p_Arr[2], p_Arr[3]);
        u8g2_DrawStr(&myU8g2, 0, 40, buf);
        UI.UI_Update_Message();	
    }
	Display_Cnt++;
    u8g2_SendBuffer(&myU8g2);
}

HC-05蓝牙

使用daplink连接蓝牙,然后按住按键再插电,看到灯在慢闪表示进入了AT模式,然后可以进行修改波特率等等

单片机通信一般是设置为:

cpp
AT+UART=115200,0,0
  • 硬件

  • MX配置
STM32引脚号 模式
PA2 USART2_TX
PA3 USART2_RX

  • 程序
HC05.h
cpp
#ifndef __HC05_H
#define __HC05_H

// 协议是4个字节--0x55 0xXX 0xXX 0xXX(最后一个字节是校验和是前面3个字节相加取低8位)
#define HC05_Protocol_Data_LEN	(uint8_t)4
// 功能码数量--对应标志位
#define HC05_Function_Code_MAX_Num 6
// 调试PID宏
#define BT_PID_Debug

typedef enum
{
    CAR_NONE = (uint8_t)0x00,   // 备用无作用
    CAR_STOP = (uint8_t)0x01,   // 停车
    CAR_FRONT = (uint8_t)0x02,  // 前进
    CAR_BACK = (uint8_t)0x03,   // 后退
    CAR_LEFT = (uint8_t)0x04,   // 左转
    CAR_RIGHT = (uint8_t)0x05,  // 右转
} HC05_Function_Code_t;

typedef struct
{
	uint8_t BT_Rec_Temp_Arr[HC05_Protocol_Data_LEN];	// 接收缓存用于OLED显示
    uint8_t HC05_Rx_Over_Flag;  // 接收完成标志位
    uint8_t HC05_Car_Flag_Arr[HC05_Function_Code_MAX_Num];  // 蓝牙的标志位
    uint8_t HC05_Data1; // 存储接收的数据[1]
    uint8_t HC05_Data2; // 存储接收的数据[2]
    void (*HC05_Init)(void);    // 蓝牙初始化
    void (*HC05_Protocol_Analyze)(void);    // 蓝牙协议解析
    void (*HC05_Handler)(void); // 蓝牙控制小车动作函数
} HC05_t;


extern HC05_t HC05;

#endif
HC05.c
cpp
/***************************************************************************
 * File: xxx.c
 * Author: Luckys.
 * Date: 2023/08/3
 * description: 描述
****************************************************************************/
#include "AllHead.h"

/*====================================static function declaration area BEGIN====================================*/

static void HC05_Init(void);
static void HC05_Protocol_Analyze(void);
static uint8_t HC05_CRC(uint8_t, uint8_t, uint8_t);
static void HC05_Handler(void); 
/*====================================static function declaration area   END====================================*/

HC05_t HC05 = 
{
	.BT_Rec_Temp_Arr = {0,0,0,0},
    .HC05_Rx_Over_Flag = FALSE,
    .HC05_Car_Flag_Arr = {FALSE},
    .HC05_Data1 = 0,
    .HC05_Data2 = 0,
    .HC05_Init = &HC05_Init,
    .HC05_Protocol_Analyze = &HC05_Protocol_Analyze,
    .HC05_Handler = &HC05_Handler
};

/*
* @function: HC05_Init
* @param: None
* @retval: None
* @brief: 描述
*/
static void HC05_Init(void)
{
    __HAL_UART_ENABLE_IT(&huart2, UART_IT_IDLE);
    HAL_UART_Receive_DMA(&huart2, UART2.pucRec_Buffer, (uint16_t)UART2_Rec_MAX_LENGTH);
}

/*
* @function: Voice_Protocol_Analyze
* @param: None
* @retval: None
* @brief: 协议解析
*/
static void HC05_Protocol_Analyze(void)
{
    uint8_t Temp_Array[4] = {0x00};
    uint8_t i = 0, Index = 0;
	uint8_t rx_length;	// 接收的长度
	
    HAL_UART_DMAStop(&huart2); // 串口停止DMA接收
	rx_length = UART2_Rec_MAX_LENGTH - __HAL_DMA_GET_COUNTER(&hdma_usart2_rx);
	
    for (i = 0; i < rx_length; i++)
    {
        if (0 == Index) // 检测键值起始数据0x55
        {
            if (*(UART2.pucRec_Buffer + i) != 0x55)
            {
                continue;
            }
        }
        Temp_Array[Index] = *(UART2.pucRec_Buffer + i);
        if (HC05_Protocol_Data_LEN == Index)
        {
            break;
        }
        Index++;
    }
#ifdef BT_PID_Debug
	// 修改机械中值
	if (strncmp((char*)UART2.pucRec_Buffer, "zero=", 5) == 0)
	{
		PID.PID_Parameter_Clear();
		Public.Str_To_Float((char*)UART2.pucRec_Buffer,&Upright.target_val);
		PID.Car_Zero = Upright.target_val;
	}
	// 直立环-Kp
	if (strncmp((char*)UART2.pucRec_Buffer, "Upright_Kp=", 11) == 0)
	{
		PID.PID_Parameter_Clear();
		Public.Str_To_Float((char*)UART2.pucRec_Buffer,&Upright.Kp);
	}
	// 直立环-Kd
	if (strncmp((char*)UART2.pucRec_Buffer, "Upright_Kd=", 11) == 0)
	{
		PID.PID_Parameter_Clear();
		Public.Str_To_Float((char*)UART2.pucRec_Buffer,&Upright.Kd);
	}	
	// 速度环-Kp,Ki
	if (strncmp((char*)UART2.pucRec_Buffer, "Speed_Kp=", 9) == 0)
	{
		PID.PID_Parameter_Clear();
		Public.Str_To_Float((char*)UART2.pucRec_Buffer,&Speed.Kp);
		Speed.Ki = Speed.Kp / 200;
	}
	// 速度环-速度
	if (strncmp((char*)UART2.pucRec_Buffer, "Speed_speed=", 12) == 0)
	{
		PID.PID_Parameter_Clear();
		Public.Str_To_Float((char*)UART2.pucRec_Buffer,&PID.BT_Forward_Later_Speed);
	}	
	// 转向环-Kp
	if (strncmp((char*)UART2.pucRec_Buffer, "Turn_To_Kp=", 11) == 0)
	{
		PID.PID_Parameter_Clear();
		Public.Str_To_Float((char*)UART2.pucRec_Buffer,&Turn_To.Kp);
	}
	// 转向环-Kd
	if (strncmp((char*)UART2.pucRec_Buffer, "Turn_To_Kd=", 11) == 0)
	{
		PID.PID_Parameter_Clear();
		Public.Str_To_Float((char*)UART2.pucRec_Buffer,&Turn_To.Kd);
	}
	// 转向环-转的速度
	if (strncmp((char*)UART2.pucRec_Buffer, "Turn_To_speed=", 14) == 0)
	{
		PID.PID_Parameter_Clear();
		Public.Str_To_Float((char*)UART2.pucRec_Buffer,&PID.BT_Left_Right_Speed);
	}	
#endif	
    HAL_UART_Receive_DMA(&huart2, UART2.pucRec_Buffer, (uint16_t)UART2_Rec_MAX_LENGTH);
    // 处理数据
    if (HC05_Protocol_Data_LEN == Index)
    {
        if ((0x55 == Temp_Array[0]) && (HC05_CRC(Temp_Array[0], Temp_Array[1], Temp_Array[2]) == Temp_Array[3]))    // 判断帧头和校验和
        {
            HC05.HC05_Data1 = Temp_Array[1];    // 功能码
            HC05.HC05_Data2 = Temp_Array[2];    // 数据
			Public.Memory_Copy((char*)HC05.BT_Rec_Temp_Arr,(char*)Temp_Array,sizeof(Temp_Array));
            HC05.HC05_Rx_Over_Flag = TRUE;
			Menu.BT_Data_Refresh_Flag = TRUE;
        }
        else
        {
            ;
        }
    }
	
}

/*
* @function: HC05_CRC
* @param: None
* @retval: 校验和
* @brief: CRC校验,三个字节相加取低8位
*/
static uint8_t HC05_CRC(uint8_t Byte_1, uint8_t Byte_2, uint8_t Byte_3)
{
    return (Byte_1 + Byte_2 + Byte_3) & 0x00FF;
}

/*
* @function: HC05_Handler
* @param: None
* @retval: None
* @brief: 蓝牙控制车
*/
static void HC05_Handler(void)
{
	int speed;
	
	
    if (FALSE == HC05.HC05_Rx_Over_Flag)
    {
        return;
    }
    switch (HC05.HC05_Data1)
    {
    case CAR_STOP:	// 停止
    {
        HC05.HC05_Car_Flag_Arr[CAR_STOP] = TRUE;
        break;
    }
    case CAR_FRONT:	// 前进
    {
        HC05.HC05_Car_Flag_Arr[CAR_FRONT] = TRUE;
        break;
    }
    case CAR_BACK:	// 后退
    {
        HC05.HC05_Car_Flag_Arr[CAR_BACK] = TRUE;
        break;
    }
    case CAR_LEFT:	// 左转
    {
        HC05.HC05_Car_Flag_Arr[CAR_LEFT] = TRUE;
        break;
    }
    case CAR_RIGHT:	// 右转
    {
        HC05.HC05_Car_Flag_Arr[CAR_RIGHT] = TRUE;
        break;
    }
    default:
        break;
    }
	
	speed = HC05.HC05_Data2;
    HC05.HC05_Data1 = 0;
	HC05.HC05_Data2 = 0;
	
    if (TRUE == HC05.HC05_Car_Flag_Arr[CAR_STOP]) 
    {
        HC05.HC05_Car_Flag_Arr[CAR_STOP] = FALSE;
        PID.BT_Forward_Later_Speed = 0;
        PID.BT_Left_Right_Speed = 0;
        return;
    }
    if (TRUE == HC05.HC05_Car_Flag_Arr[CAR_FRONT])	// 前进
    {
        HC05.HC05_Car_Flag_Arr[CAR_FRONT] = FALSE;
        PID.BT_Forward_Later_Speed = speed * 10;
        return;
    }
    if (TRUE == HC05.HC05_Car_Flag_Arr[CAR_BACK])	// 后退
    {
        HC05.HC05_Car_Flag_Arr[CAR_BACK] = FALSE;
        PID.BT_Forward_Later_Speed = (-speed) * 10;
        return;
    } 
    if (TRUE == HC05.HC05_Car_Flag_Arr[CAR_LEFT])	// 左转
    {
        HC05.HC05_Car_Flag_Arr[CAR_LEFT] = FALSE;
        PID.BT_Left_Right_Speed = (-speed) * 10;
        return;
    }
    if (TRUE == HC05.HC05_Car_Flag_Arr[CAR_RIGHT])	// 右转
    {
        HC05.HC05_Car_Flag_Arr[CAR_RIGHT] = FALSE;
        PID.BT_Left_Right_Speed = speed * 10;
        return;
    }  
	HC05.HC05_Rx_Over_Flag = FALSE;
}

HC-SR04超声波

  • 硬件

  • MX配置
STM32引脚号 模式
PB14(Trig) 输出
PB15(Echo) 输入

  • 程序
HC_SR04.h
cpp
#ifndef __HC_SR04_H
#define __HC_SR04_H

typedef struct
{
    float HC_SR04_Distance; //测距值
    float (*HC_SR04_Ranging)(void); // 测距
}HC_SR04_t;

extern HC_SR04_t HC_SR04;

#endif
HC_SR04.c
cpp
/***************************************************************************
 * File: xxx.c
 * Author: Luckys.
 * Date: 2023/08/3
 * description: 描述
****************************************************************************/
#include "AllHead.h"

/*====================================static function declaration area BEGIN====================================*/
static float HC_SR04_Ranging(void);
static void HC_SR04_Delayus(uint32_t usdelay);

/*====================================static function declaration area   END====================================*/

HC_SR04_t HC_SR04 = 
{
    .HC_SR04_Distance = 0,
    .HC_SR04_Ranging = &HC_SR04_Ranging
};

// 延时us
static void HC_SR04_Delayus(uint32_t usdelay)
{
  __IO uint32_t Delay = usdelay * (SystemCoreClock / 8U / 1000U/1000);//SystemCoreClock:系统频率
  do
  {
    __NOP();
  }
  while (Delay --);
}

/*
* @function: xxx
* @param: None
* @retval: None
* @brief: 描述
*/
static float HC_SR04_Ranging(void)
{
    uint32_t i = 0;
    float Distance;

    HAL_GPIO_WritePin(SR04_TIRG_GPIO_Port, SR04_TIRG_Pin, GPIO_PIN_SET); // 输出15us高电平
    HC_SR04_Delayus(15);
    HAL_GPIO_WritePin(SR04_TIRG_GPIO_Port, SR04_TIRG_Pin, GPIO_PIN_RESET); // 高电平输出结束,设置为低电平

    while (HAL_GPIO_ReadPin(SR04_ECHO_GPIO_Port, SR04_ECHO_Pin) == GPIO_PIN_RESET) // 等待回响高电平
    {
        i++;
        HC_SR04_Delayus(1);
        if (i > 100000)
            return -1; // 超时退出循环、防止程序卡死这里
    }
    i = 0;
    while (HAL_GPIO_ReadPin(SR04_ECHO_GPIO_Port, SR04_ECHO_Pin) == GPIO_PIN_SET) // 下面的循环是2us
    {
        i = i + 1;
        HC_SR04_Delayus(1); // 1us 延时,但是整个循环大概2us左右
        if (i > 100000)
            return -2; // 超时退出循环
    }
    Distance = i * 2 * 0.033 / 2; // 这里乘2的原因是上面是2微妙
    
    return Distance;
}

7路灰度寻迹

  • 硬件

  • MX配置
STM32引脚号 模式
PD1(L3) 上拉输入
PD2(L2) 上拉输入
PD3(L1) 上拉输入
PD4(M) 上拉输入
PD5(R1) 上拉输入
PD6(R2) 上拉输入
PD7(R3) 上拉输入

  • 程序
Track.h
cpp
#ifndef __TRACK_H
#define __TRACK_H

typedef enum
{
    R1_Status = (uint8_t)0,
    R2_Status = (uint8_t)1,
    R3_Status = (uint8_t)2,
    M_Status = (uint8_t)3,
    L1_Status = (uint8_t)4,
    L2_Status = (uint8_t)5,
    L3_Status = (uint8_t)6,
}Track_Status_t;

typedef struct
{
    uint8_t ucTrack_Status_Buff[7]; // 存储7路状态

    void (*Track_Read_Status)(void);    // 读取状态
}Track_t;

extern Track_t Track;

#endif
Track.c
cpp
/***************************************************************************
 * File: xxx.c
 * Author: Luckys.
 * Date: 2023/08/3
 * description: 描述
 -----------------------------------
None
 -----------------------------------
****************************************************************************/
#include "AllHead.h"

/*====================================static function declaration area BEGIN====================================*/
static void Track_Read_Status(void);
/*====================================static function declaration area   END====================================*/

Track_t Track = 
{
    .ucTrack_Status_Buff = {0},
    .Track_Read_Status = &Track_Read_Status
};

/*
* @function: Track_Read_Status
* @param: None
* @retval: None
* @brief: 读取寻迹灯状态
*/
static void Track_Read_Status(void)
{
    // 从左到右排序 传感器返回的数字信号依次存入
    Track.ucTrack_Status_Buff[L3_Status] = HAL_GPIO_ReadPin(L3_GPIO_Port, L3_Pin) ? 0 : 1;
    Track.ucTrack_Status_Buff[L2_Status] = HAL_GPIO_ReadPin(L2_GPIO_Port, L2_Pin) ? 0 : 1;
    Track.ucTrack_Status_Buff[L1_Status] = HAL_GPIO_ReadPin(L1_GPIO_Port, L1_Pin) ? 0 : 1;
    Track.ucTrack_Status_Buff[M_Status] = HAL_GPIO_ReadPin(M_GPIO_Port, M_Pin) ? 0 : 1;
    Track.ucTrack_Status_Buff[R1_Status] = HAL_GPIO_ReadPin(R1_GPIO_Port, R1_Pin) ? 0 : 1;
    Track.ucTrack_Status_Buff[R2_Status] = HAL_GPIO_ReadPin(R2_GPIO_Port, R2_Pin) ? 0 : 1;
    Track.ucTrack_Status_Buff[R3_Status] = HAL_GPIO_ReadPin(R3_GPIO_Port, R3_Pin) ? 0 : 1;
#if LOG_DEBUG
    printf("R1-%d R2-%d R3-%d\r\nM-%d\r\nL1-%d L2-%d L3-%d\r\n\r\n", Track.ucTrack_Status_Buff[R1_Status],Track.ucTrack_Status_Buff[R2_Status],Track.ucTrack_Status_Buff[R3_Status],Track.ucTrack_Status_Buff[M_Status],Track.ucTrack_Status_Buff[L1_Status],Track.ucTrack_Status_Buff[L2_Status],Track.ucTrack_Status_Buff[L3_Status]);
#endif    
}

蜂鸣器

  • 硬件

  • MX配置

NPN高电平导通

STM32引脚号 模式
PE14 推挽输出,默认低电平

  • 程序
Buzzer.h
cpp
#ifndef __BUZZER_H
#define __BUZZER_H

typedef enum
{
    Buzzer_Status_ON = (uint8_t)0x01,  
    Buzzer_Status_OFF = (uint8_t)0x00,  
}Buzzer_Status_t;

typedef struct
{
    Buzzer_Status_t Buzzer_Status;    
    void (*Buzzer_Init)(void); // Buzzer 初始化  
    void (*Buzzer_ON)(void);    // 打开Buzzer
    void (*Buzzer_OFF)(void);   // 关闭Buzzer
    void (*Buzzer_Flip)(void);  // 翻转Buzzer
}Buzzer_t;

extern Buzzer_t Buzzer;

#endif
Buzzer.c
cpp
/***************************************************************************
 * File: Buzzer.c
 * Author: Luckys.
 * Date: 2023/06/24
 * description: 蜂鸣器
****************************************************************************/
#include "AllHead.h"

/*====================================static function declaration area BEGIN====================================*/

static void Buzzer_Init(void);
static void Buzzer_ON(void);
static void Buzzer_OFF(void);
static void Buzzer_Flip(void);

/*====================================static function declaration area   END====================================*/
Buzzer_t Buzzer = 
{
    .Buzzer_Status = Buzzer_Status_OFF,
    .Buzzer_Init = &Buzzer_Init,
    .Buzzer_ON = &Buzzer_ON,
    .Buzzer_OFF = &Buzzer_OFF,
    .Buzzer_Flip = &Buzzer_Flip
};

/*
* @function: Buzzer_Init
* @param: None
* @retval: None
* @brief: 蜂鸣器初始化
*/
static void Buzzer_Init(void)
{
    Buzzer.Buzzer_OFF();
}

/*
* @function: Buzzer_ON
* @param: None
* @retval: None
* @brief: 蜂鸣器打开
*/
static void Buzzer_ON(void)
{
    HAL_GPIO_WritePin(Buzzer_GPIO_Port,Buzzer_Pin,GPIO_PIN_SET);
    Buzzer.Buzzer_Status = Buzzer_Status_ON;
}

/*
* @function: Buzzer_OFF
* @param: None
* @retval: None
* @brief: 蜂鸣器关闭
*/
static void Buzzer_OFF(void)
{
    HAL_GPIO_WritePin(Buzzer_GPIO_Port,Buzzer_Pin,GPIO_PIN_RESET);
    Buzzer.Buzzer_Status = Buzzer_Status_OFF;
}

/*
* @function: Buzzer_Filp
* @param: None
* @retval: None
* @brief: 蜂鸣器翻转
*/
static void Buzzer_Flip(void)
{
    if (Buzzer_Status_ON == Buzzer.Buzzer_Status)
    {
        HAL_GPIO_WritePin(Buzzer_GPIO_Port,Buzzer_Pin,GPIO_PIN_RESET);
        Buzzer.Buzzer_Status = Buzzer_Status_OFF;
    }
    else
    {
        HAL_GPIO_WritePin(Buzzer_GPIO_Port,Buzzer_Pin,GPIO_PIN_SET);
        Buzzer.Buzzer_Status = Buzzer_Status_ON;
    }
}

ADC

ADC点的电压是VBAT_IN的 五分之一

如果两个串联的电阻阻值相等,则分压结果是输入电压的一半

总电压12V,总电阻50K。总电流0.00024A,PC4电压(该点的对地阻值)2.4V

锂电池满电电压为12.6V,截止电压为9V,所以

电量百分比应为: (ADC_val / 4096 * 3.3 * 5) / (12.6 - 9) * 100%

为了提高测量精度,采样时间设置了满量程,那么根据Datasheet提供的公式则可知转换一次所需时间为:

Tconv = (239.5 + 12.5) / 12MHz = 21us

  • 硬件

  • MX配置
STM32引脚号 模式
PC4 ADC1_IN14
  • 程序
myADC.h
cpp
#ifndef __MYADC_H
#define __MYADC_H

typedef struct
{
    float Power_V;  // 最终计算电压
    float Power_Percent;    // 电池电量百分比
    uint16_t usADC_Value;
    void (*ADC_Calibration_Start_DMA)(void);
    float (*ADC_Get_Calculate)(void);
}myADC_t;

extern myADC_t myADC;

#endif
myADC.c
cpp
/***************************************************************************
 * File: myADC.c
 * Author: Luckys.
 * Date: 2023/06/24
 * description: ADC
****************************************************************************/
#include "AllHead.h"

/*====================================static function declaration area BEGIN====================================*/

static void ADC_Calibration_Start_DMA(void);
static float ADC_Get_Calculate(void);

/*====================================static function declaration area   END====================================*/

myADC_t myADC = 
{
    .Power_V = 0.0,
    .Power_Percent = 0.0,
    .usADC_Value = 0,
    .ADC_Calibration_Start_DMA = &ADC_Calibration_Start_DMA,
    .ADC_Get_Calculate = &ADC_Get_Calculate
};

/*
* @function: ADC_Calibration_Start_DMA
* @param: None
* @retval: None
* @brief: ADC校准+启动+DMA
*/
static void ADC_Calibration_Start_DMA(void)
{
    HAL_ADCEx_Calibration_Start(&hadc1);    // ADC校准
    HAL_ADC_Start_DMA(&hadc1, (uint32_t*)&myADC.usADC_Value, 1);   // 开始ADC DMA转换
}

/*
* @function: ADC_Get_Calculate
* @param: None
* @retval: None
* @brief: ADC计算
*/
static float ADC_Get_Calculate(void)
{
    uint16_t Temp;

    Temp = myADC.usADC_Value;
    myADC.Power_V = (float)Temp / 4096 * 3.3f * 5;
    myADC.Power_Percent = ((float)Temp / 4096 * 3.3f * 5 - 9) / (12.2 - 9) * 100;

    return myADC.Power_Percent;
}

MPU6050

模块基础

  • 欧拉角定义

pitch是围绕X轴旋转,也叫做俯仰角。

yaw是围绕z轴旋转,也叫偏航角。

roll是围绕y轴旋转,也叫翻滚角。

  • MPU6050偏航角(yaw)零飘

MPU6050的偏航角是不正常的,即使静止不动也会跳动。这个纯粹是硬件问题,就算算法怎么牛逼也解决不了。只能外加磁力计解决,也可以直接使用MPU9250,这个是自带磁力计的。使用时注意不要和电机靠太近,磁场很容易受到干扰。

陀螺仪主要短时间有效,不能长时间测量——零点漂移

  • MPU6050万向节锁

默认Z-Y-X顺序下,不知道大家有没有发现这个问题。当MPU6050只转动俯仰角,俯仰角接近90度或者等于90度时,偏航角的角度会发生很大的偏差。那么这个就是万向节锁,此时如果比作一架飞机,当飞机垂直90度向上时,按照Z-Y-X顺序无论怎么改变方向飞机都只能向上飞。所以不要让俯仰角垂直,这样可以减少bug。

陀螺仪的us延时如果过大或者过小都会导致初始化失败

采样率表示1s中陀螺仪输出多少次数值

还有一个重要的是当你使用官方的自检函数时你的陀螺仪必须是水平正放或者反放!而且初始化大概需要8s,需要你用手扶住,所以我们不想使用自检函数

去掉标准库头文件不然报错

IIC使用自己

也使用了卡尔曼滤波,但是yaw不能使用,所以还是使用dmp库

  • 硬件

  • MX配置

中断引脚暂时没用到

STM32引脚号 模式
PB6(SCL) 开漏输出
PB7(SDA) 开漏输出

  • 程序

  • 修改记录

修改了 mpu_dmp_get_data 函数

修改了 gyro_orientation数组

电机

  • 硬件

  • MX配置

电机频率10KHz即可

STM32引脚号 用作
TIM3_CH1 左电机
TIM3_CH2 右电机
TIM4 左编码器
TIM5 右编码器
  • 程序
Motor.h
cpp
#ifndef __MOTOR_H
#define __MOTOR_H

// 死区
#define motor_dead_zone 720

typedef enum
{
    // 左
    Motor_LEFT = (uint8_t)0,  
    // 右
    Motor_RIGHT = (uint8_t)1
}Motor_Mark_t;

typedef struct
{
    float Mileage;  // 里程数
	int16_t usMotor_Fre;	// 电机频率
    int16_t usLeft_Duty; // 左电机占空比
    int16_t usRight_Duty; // 右电机占空比
    void (*Motor_Init)(void);  
    void (*Motor_Set_PWM)(Motor_Mark_t, int); // 设置电机PWM(速度)
    void (*Motor_Clamp)(int *, int *);   // 限幅 
    void (*Motor_Set_Forward)(Motor_Mark_t);    // 设置为正转
    void (*Motor_Set_Reverse)(Motor_Mark_t);    // 设置为反转
	void (*Motor_Fre_And_Duty_Compute)(void);   // 频率占空比计算(用于显示OLED)
}Motor_t;


extern Motor_t Motor;

#endif
Motor.c
cpp
/***************************************************************************
 * File: xxx.c
 * Author: Luckys.
 * Date: 2023/08/13
 * description: 电机
****************************************************************************/
#include "AllHead.h"

/*====================================static function declaration area BEGIN====================================*/
static void Motor_Init(void);
static void Motor_Set_Forward(Motor_Mark_t);
static void Motor_Set_Reverse(Motor_Mark_t);
static inline void Motor_Set_PWM(Motor_Mark_t, int);
static void Motor_Fre_And_Duty_Compute(void);
static void Motor_Clamp(int *, int *);
/*====================================static function declaration area   END====================================*/
Motor_t Motor = 
{
    .Mileage = 0.0,
	.usMotor_Fre = 0,
    .usLeft_Duty = 0,
    .usRight_Duty = 0,
    .Motor_Init = &Motor_Init,
    .Motor_Set_PWM = &Motor_Set_PWM,
    .Motor_Clamp = &Motor_Clamp,
    .Motor_Set_Forward = &Motor_Set_Forward,
    .Motor_Set_Reverse = &Motor_Set_Reverse,
	.Motor_Fre_And_Duty_Compute = &Motor_Fre_And_Duty_Compute
};

/*
* @function: Motor_Init
* @param: None
* @retval: None
* @brief: 电机初始化
*/
static void Motor_Init(void)
{
    HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);   // 左
    HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_2);   // 右

    // 初始化轮子正转
    Motor_Set_Forward(Motor_LEFT);
    Motor_Set_Forward(Motor_RIGHT);

    Motor_Set_PWM(Motor_LEFT,0);
    Motor_Set_PWM(Motor_RIGHT,0);

}

/*
* @function: Motor_Set_Forward
* @param: motor -> 哪个电机
* @retval: None
* @brief: 设置轮子正转
*/
static void Motor_Set_Forward(Motor_Mark_t motor)
{
    switch(motor)
    {
        case Motor_LEFT:
        {
            HAL_GPIO_WritePin(Left_AIN1_GPIO_Port, Left_AIN1_Pin, GPIO_PIN_RESET);
            HAL_GPIO_WritePin(Left_AIN2_GPIO_Port, Left_AIN2_Pin, GPIO_PIN_SET);            
            break;
        }
        case Motor_RIGHT:
        {
            HAL_GPIO_WritePin(Right_BIN1_GPIO_Port, Right_BIN1_Pin, GPIO_PIN_RESET);    // 看调试是否需要调转
            HAL_GPIO_WritePin(Right_BIN2_GPIO_Port, Right_BIN2_Pin, GPIO_PIN_SET);             
            break;
        }
        default:break;
    }
}

/*
* @function: Motor_Set_Reverse
* @param: motor -> 哪个电机
* @retval: None
* @brief: 设置轮子反转
*/
static void Motor_Set_Reverse(Motor_Mark_t motor)
{
    switch(motor)
    {
        case Motor_LEFT:
        {
            HAL_GPIO_WritePin(Left_AIN1_GPIO_Port, Left_AIN1_Pin, GPIO_PIN_SET);
            HAL_GPIO_WritePin(Left_AIN2_GPIO_Port, Left_AIN2_Pin, GPIO_PIN_RESET);            
            break;
        }
        case Motor_RIGHT:
        {
            HAL_GPIO_WritePin(Right_BIN1_GPIO_Port, Right_BIN1_Pin, GPIO_PIN_SET);  // 看调试调转
            HAL_GPIO_WritePin(Right_BIN2_GPIO_Port, Right_BIN2_Pin, GPIO_PIN_RESET);             
            break;
        }
        default:break;
    }
}

/*
* @function: Motor_Set_PWM
* @param: motor -> 哪个电机 PWM -> 占空比设置(范围0~7201)
* @retval: None
* @brief: 设置电机占空比
*/
static inline void Motor_Set_PWM(Motor_Mark_t motor, int PWM)
{
	int pwm_temp = 0;
	
	pwm_temp = Public.Number_ABS(PWM);
	
    switch(motor)
    {
        case Motor_LEFT:
        {
			TIM3->CCR1 = pwm_temp;
            if (PWM < 0)
            {
                Motor_Set_Reverse(Motor_LEFT);
            }
            else
            {
                Motor_Set_Forward(Motor_LEFT);
            }
            
            break;
        }
        case Motor_RIGHT:
        {
			TIM3->CCR2 = pwm_temp;
            if (PWM < 0)
            {
                Motor_Set_Reverse(Motor_RIGHT);
            }
            else
            {
                Motor_Set_Forward(Motor_RIGHT);
            }   
            
            break;
        }
        default:break;                        
    }
#if LOG_DEBUG
    printf("TIM3_CH1:%d  TIM3_CH2:%d\r\n", TIM3->CCR1,TIM3->CCR2);
#endif
}

/*
* @function: Motor_Fre_And_Duty_Compute
* @param: None
* @retval: None
* @brief: 实际电机频率占空比计算
*/
static void Motor_Fre_And_Duty_Compute(void)
{
    uint16_t Timer3_fre = 0;
    float T3_CH1_Duty = 0, T3_CH2_Duty = 0;

    Timer3_fre = 72000000 / (TIM3->PSC + 1) / (TIM3->ARR + 1);  // 计算频率
	Motor.usMotor_Fre = Timer3_fre;
    T3_CH1_Duty = ((float)TIM3->CCR1 / TIM3->ARR) * 100;    // 计算占空比
    T3_CH2_Duty = ((float)TIM3->CCR2 / TIM3->ARR) * 100;
	Public.UsartPrintf(huart_debug,"FRE:%d CH1:%.0f  CH2:%.0f\r\n", Timer3_fre, T3_CH1_Duty, T3_CH2_Duty);
//#if LOG_DEBUG
//    printf("CH1:%.0f  CH2:%.0f\r\n", T3_CH1_Duty, T3_CH2_Duty);
//#endif
	return;
}

/*
* @function: Motor_Clamp
* @param: None
* @retval: None
* @brief: 限幅函数
*/
static void Motor_Clamp(int *pwm_left, int *pwm_right)
{
    int pwm_left_max = 4000;	// TIM3->ARR - motor_dead_zone, 3528
    int pwm_right_max = 4000;	// TIM3->ARR - motor_dead_zone;
	
    if (*pwm_left < -pwm_left_max) {
        *pwm_left = -pwm_left_max;
    }
    if (*pwm_left > pwm_left_max) {
        *pwm_left = pwm_left_max;
    }

    if (*pwm_right < -pwm_right_max) {
        *pwm_right = -pwm_right_max;
    }
    if (*pwm_right > pwm_right_max) {
        *pwm_right = pwm_right_max;
    }
}
Encoder.h
cpp
#ifndef __ENCODER_H
#define __ENCODER_H

typedef struct
{
    int Left_Speed; // 左电机速度
    int Right_Speed;    // 右电机速度
    void (*Encoder_Timer_Init)(void);   // 编码器初始化   
    int (*Encoder_Value_Read)(TIM_TypeDef *); // 编码器数据读取 
    void (*Encoder_Count_Clear)(TIM_TypeDef *);    // 编码器清0
    void (*Encoder_Speed_Read)(Motor_Mark_t);  // 编码器速度读取
}Encoder_t;

extern Encoder_t Encoder;

#endif
Encoder.c
cpp
/***************************************************************************
 * File: xxx.c
 * Author: Luckys.
 * Date: 2023/08/13
 * description: 描述
****************************************************************************/
#include "AllHead.h"

/*====================================static function declaration area BEGIN====================================*/
static void Encoder_Timer_Init(void);
static int Encoder_Value_Read(TIM_TypeDef *);
static void Encoder_Count_Clear(TIM_TypeDef *);
static void Encoder_Speed_Read(Motor_Mark_t);
/*====================================static function declaration area   END====================================*/
Encoder_t Encoder = 
{
    .Left_Speed = 0,
    .Right_Speed = 0,
    .Encoder_Timer_Init = &Encoder_Timer_Init,
    .Encoder_Value_Read = &Encoder_Value_Read,
    .Encoder_Count_Clear = &Encoder_Count_Clear,
    .Encoder_Speed_Read = &Encoder_Speed_Read
};


/*
* @function: Encoder_Timer_Init
* @param: None
* @retval: None
* @brief: 
*/
static void Encoder_Timer_Init(void)
{
    HAL_TIM_Encoder_Start(&htim4,TIM_CHANNEL_ALL);//开启定时器(左)
    HAL_TIM_Encoder_Start(&htim5,TIM_CHANNEL_ALL);//开启定时器(右)

    HAL_TIM_Base_Start_IT(&htim4);				//开启定时器4 中断
    HAL_TIM_Base_Start_IT(&htim5);                //开启定时器5 中断   
}

/*
* @function: Encoder_Value_Read
* @param: None
* @retval: None
* @brief: 编码器的数据读取
*/
static int Encoder_Value_Read(TIM_TypeDef *TIMx)
{
    int channal_val = 0;
    
    channal_val = TIMx->CNT;
    if (channal_val >> 15)  // 负数最高位是1 正数是0
    {
        channal_val = (channal_val & 0x7FFF) - 32767;
    }

    return channal_val;
}

/*
* @function: Encoder_Count_Clear
* @param: None
* @retval: None
* @brief: 编码器清0
*/
static void Encoder_Count_Clear(TIM_TypeDef *TIMx)
{
    TIMx->CNT = 0;
}

/*
* @function: Encoder_Speed_Read
* @param: None
* @retval: None
* @brief: 编码器速度读取
*/
static void Encoder_Speed_Read(Motor_Mark_t motor)
{
    if (motor == Motor_LEFT)
    {
        Encoder.Left_Speed = Encoder_Value_Read(TIM4);
        Encoder.Left_Speed = -Encoder.Left_Speed * 10;  // 看打印--正常是正转是正反转是负如果不是就取反即可
        Encoder_Count_Clear(TIM4);
    }
    if (motor == Motor_RIGHT)
    {
        Encoder.Right_Speed = Encoder_Value_Read(TIM5);
        Encoder.Right_Speed = Encoder.Right_Speed * 10;
        Encoder_Count_Clear(TIM5);
    }
}

  • 问题

这里 >>15 是为了判断最高位是否是负数,1则是负数,0则是正数,看编码器模式那个表,正转的状态都向上计数,反转的状态都向下计数

cpp
static int Encoder_Value_Read(TIM_TypeDef *TIMx)
{
    int channal_val = 0;
    
    channal_val = TIMx->CNT;
    if (channal_val >> 15)  // 负数最高位是1 正数是0
    {
        channal_val = (channal_val & 0x7FFF) - 32767;
    }

    return channal_val;
}

这里乘以10是因为编码器速度是5ms读取一次的,所以有可能读出来的数据很小,所以需要比例放大10倍这样调参也会方便

cpp
static void Encoder_Speed_Read(Motor_Mark_t motor)
{
    if (motor == Motor_LEFT)
    {
        Encoder.Left_Speed = Encoder_Value_Read(TIM4);
        Encoder.Left_Speed = Encoder.Left_Speed * 10;
        Encoder_Count_Clear(TIM4);
    }
    if (motor == Motor_RIGHT)
    {
        Encoder.Right_Speed = Encoder_Value_Read(TIM5);
        Encoder.Right_Speed = Encoder.Right_Speed * 10;
        Encoder_Count_Clear(TIM5);
    }
}

串口

串口1 — 上位机调试

串口2 — 蓝牙

串口3 — ASRPRO语音板

UART.h
cpp
#ifndef __UART_H
#define __UART_H

typedef struct
{
    uint8_t *pucSend_Buffer; // 发送缓存指针
    uint8_t *pucRec_Buffer;  // 接收缓存指针

    void (*Send_Array)(uint8_t *, uint16_t); // 串口发送数组
    void (*Send_String)(uint8_t *);          // 串口发送字符串
} UART_t;


#endif
UART3.h
cpp
#ifndef __UART3_H
#define __UART3_H

// 串口3接收/发送数据最大长度
#define UART3_Rec_MAX_LENGTH 50
#define UART3_Send_MAX_LENGTH 50


extern UART_t UART3;

#endif
UART3.c
cpp
/***************************************************************************
 * File: xxx.c
 * Author: Luckys.
 * Date: 2023/08/3
 * description: 描述
 -----------------------------------
None
 -----------------------------------
****************************************************************************/
#include "AllHead.h"

/*====================================static function declaration area BEGIN====================================*/
static uint8_t ucUart3_Send_Buffer[UART3_Send_MAX_LENGTH] = {0x00};
static uint8_t ucUart3_Rec_Buffer[UART3_Rec_MAX_LENGTH] = {0x00};

static void UART3_Send_Array(uint8_t *, uint16_t);
static void UART3_Send_String(uint8_t *);
/*====================================static function declaration area   END====================================*/

UART_t UART3 = 
{
    .pucSend_Buffer = ucUart3_Send_Buffer,
    .pucRec_Buffer = ucUart3_Rec_Buffer,
    .Send_Array = &UART3_Send_Array,
    .Send_String = &UART3_Send_String
};

/*
* @function: UART3_Send_Array
* @param: None
* @retval: None
* @brief: 描述
*/
static void UART3_Send_Array(uint8_t *p_Arr, uint16_t LEN)
{
    HAL_UART_Transmit(&huart3, p_Arr, LEN, 1000);
}

/*
* @function: UART3_Send_String
* @param: None
* @retval: None
* @brief: 描述
*/
static void UART3_Send_String(uint8_t *p_Str)
{
    HAL_UART_Transmit(&huart3, p_Str, strlen((const char*)p_Str), 1000);
}
UART2.h
cpp
#ifndef __UART2_H
#define __UART2_H

// 串口2接收/发送数据最大长度
#define UART2_Rec_MAX_LENGTH 50
#define UART2_Send_MAX_LENGTH 50


extern UART_t UART2;

#endif
UART2.c
cpp
/***************************************************************************
 * File: xxx.c
 * Author: Luckys.
 * Date: 2023/08/3
 * description: 描述
 -----------------------------------
None
 -----------------------------------
****************************************************************************/
#include "AllHead.h"

/*====================================static function declaration area BEGIN====================================*/
static uint8_t ucUart2_Send_Buffer[UART2_Send_MAX_LENGTH] = {0x00};
static uint8_t ucUart2_Rec_Buffer[UART2_Rec_MAX_LENGTH] = {0x00};

static void UART2_Send_Array(uint8_t *, uint16_t);
static void UART2_Send_String(uint8_t *);
/*====================================static function declaration area   END====================================*/

UART_t UART2 = 
{
    .pucSend_Buffer = ucUart2_Send_Buffer,
    .pucRec_Buffer = ucUart2_Rec_Buffer,
    .Send_Array = &UART2_Send_Array,
    .Send_String = &UART2_Send_String
};

/*
* @function: UART2_Send_Array
* @param: None
* @retval: None
* @brief: 描述
*/
static void UART2_Send_Array(uint8_t *p_Arr, uint16_t LEN)
{
    HAL_UART_Transmit(&huart2, p_Arr, LEN, 1000);
}

/*
* @function: UART2_Send_String
* @param: None
* @retval: None
* @brief: 描述
*/
static void UART2_Send_String(uint8_t *p_Str)
{
    HAL_UART_Transmit(&huart2, p_Str, strlen((const char*)p_Str), 1000);
}

PID

PID.h
cpp
#ifndef __PID_H
#define __PID_H


typedef struct
{
    float Car_Zero; // 机械中值
    float BT_Left_Right_Speed; // 接收蓝牙发送过来的左右速度(+顺时针 -逆时针)
    float BT_Forward_Later_Speed; // 接收蓝牙发送过来的前进后退速度(+前 -后)
    int Right_Pwm_Out;  // 最终右电机输出
    int Left_Pwm_Out;   // 最终左电机输出
    int Upright_Pwm_Out;    // 直立环输出
    int Speed_Pwm_Out;  // 速度环输出
    int TurnTo_Pwm_Out; // 转向环输出

    void (*PID_Parameter_Init)(void);   // PID参数初始化
    void (*PID_Handler)(void);  // PID作用到电机运行函数
	void (*PID_Parameter_Clear)(void);	// PID参数,输出清除(用于上位机改变参数时)
} PID_t;

typedef struct
{
    float target_val; // 目标值
    float actual_val; // 实际值
    float err;        // 当前偏差
    float err_last;   // 上次偏差
    float err_sum;    // 误差累计值
    float Kp, Ki, Kd; // 比例,积分,微分系数
} PID_Parameter_t;

extern PID_t PID;
extern PID_Parameter_t Upright; 
extern PID_Parameter_t Speed;   
extern PID_Parameter_t Turn_To;
#endif
PID.c
cpp
/***************************************************************************
 * File: xxx.c
 * Author: Luckys.
 * Date: 2023/08/3
 * description: 描述
****************************************************************************/
#include "AllHead.h"

/*====================================static function declaration area BEGIN====================================*/
static void PID_Parameter_Init(void);
static int PID_Upright_Ring(float, float);
static int PID_Speed_Ring(int,int);
static int PID_TurnTo_Ring(float, float);
static void PID_Handler(void);
static void PID_Parameter_Clear(void);
/*====================================static function declaration area   END====================================*/

/*====================================variable definition declaration area BEGIN===================================*/
PID_Parameter_t Upright;  // 直立环
PID_Parameter_t Speed;    // 速度环
PID_Parameter_t Turn_To;  // 转向环

PID_t PID = 
{
    .Car_Zero = 1.91,
    .BT_Left_Right_Speed = 0,
    .BT_Forward_Later_Speed = 0,
    .Left_Pwm_Out = 0,
    .Upright_Pwm_Out = 0,
    .Speed_Pwm_Out = 0,
    .TurnTo_Pwm_Out = 0,

    .PID_Parameter_Init = &PID_Parameter_Init,
    .PID_Handler = &PID_Handler,
	.PID_Parameter_Clear = &PID_Parameter_Clear
};
/*====================================variable definition declaration area   END===================================*/

/*
* @function: PID_Parameter_Init
* @param: None
* @retval: None
* @brief: 描述
*/
static void PID_Parameter_Init(void)
{
    // ************* 直立环PD *************
    Upright.target_val = PID.Car_Zero;
    Upright.actual_val = 0.0;
    Upright.err = 0.0;
    Upright.err_last = 0.0;
    Upright.err_sum = 0;
    Upright.Kp = 900;   // 【1】极性-正数1500
    Upright.Ki = 0.0;
    Upright.Kd = -1.62;   // 【2】极性-正数--- -2.7

    // ************* 速度环PI *************
    Speed.target_val = 0.0;   // 目标速度
    Speed.actual_val = 0.0;
    Speed.err = 0.0;
    Speed.err_last = 0.0;
    Speed.err_sum = 0;
    Speed.Kp = 12;   // 【1】正数 
    Speed.Ki = 0.06;   // 【2】正数 Kp/200
    Speed.Kd = 0;  

    // ************* 转向环PD *************
    Turn_To.target_val = 0.0;
    Turn_To.actual_val = 0.0;
    Turn_To.err = 0.0;
    Turn_To.err_last = 0.0; 	
    Turn_To.err_sum = 0;
    Turn_To.Kp = 20.0;   // 【1】正数
    Turn_To.Ki = 0.0;
    Turn_To.Kd = 0.65;   // 【2】 正数     
}

/*
* @function: PID_Upright_Ring
* @param: Angle--采集到的实际角度值 Gyro--采集到的实际角速度值
* @retval: None
* @brief: 直立环控制【PWM = Kp*(Zero-θ)+Kd*ω】
*/
static int PID_Upright_Ring(float Angle, float Gyro)
{
    float pwm_out;

    Upright.actual_val = Angle;
    Upright.err = Upright.target_val - Upright.actual_val;  // 误差 = 期望值 - 实际值
    pwm_out = Upright.Kp * Upright.err + Gyro * Upright.Kd;
	
    return (int)pwm_out;
}

/*
* @function: PID_Speed_Ring
* @param: Encoder_left--左轮编码器值 Encoder_right--右轮编码器值
* @retval: None
* @brief: 速度环控制【output=Kp*(Encoder_Set - Encoder)+Ki∑】
*/
static int PID_Speed_Ring(int Encoder_left,int Encoder_right)
{
    static int pwm_out, Encoder_Least;
	
    Encoder_Least = (Encoder_left + Encoder_right) - PID.BT_Forward_Later_Speed;  // 获取最新速度偏差 = 测量速度(左右编码器之和) - 目标速度(蓝牙控制的)
    // 一阶低通滤波器
    Speed.err *= 0.8;
    Speed.err += Encoder_Least * 0.2;
    Speed.err_sum += Speed.err; // 累加误差 积分出位移 积分时间:5ms
	if (Speed.err_sum < -8000)// 积分限幅(严重注意这里不要写成>-8000!!!不然永远看不到效果)
	{
		Speed.err_sum = -8000;
	}
	if (Speed.err_sum > 8000)
	{
		Speed.err_sum = 8000;
	}  
    pwm_out = Speed.Kp * Speed.err + Speed.Ki * Speed.err_sum;
    if ((myMPU6050.mpu_pitch >= 27) || (myMPU6050.mpu_pitch <= -23))    // 小车跌倒清0
    {
        Speed.err_sum = 0;
    }
    
    return pwm_out;
}

/*
* @function: PID_TurnTo_Ring
* @param: Set_turn--目标旋转角速度(偏航角差值的期望值) Gyro_z--陀螺仪Z轴的角速度
* @retval: None
* @brief: 转向环控制(非严格PD控制器,为小车的叠加控制)若Set_turn == 0,则表示要抑制转向走直线;若Set_turn != 0,则表示要基于当前的偏航角进行偏移
*/
static int PID_TurnTo_Ring(float Set_turn, float Gyro_z)
{
    int pwm_out;

    if (0 == Set_turn)
    {
        pwm_out = Turn_To.Kd * Gyro_z;  // 没有转向需求,Kd约束小车转向
    }
    else
    {
        pwm_out = Turn_To.Kp * Set_turn;    // 有转向需求,Kp为期望小车转向
    }

    return pwm_out;
}

/*
* @function: PID_Handler
* @param: 
* @retval: None
* @brief: PID作用到电机运行函数
*/
static void PID_Handler(void)
{
    PID.Upright_Pwm_Out = PID_Upright_Ring(myMPU6050.mpu_pitch, myMPU6050.gyro_x);  // 这里gyro_y还是gyro_x取决于6050安装
	PID.Speed_Pwm_Out = PID_Speed_Ring(Encoder.Left_Speed, Encoder.Right_Speed);
    PID.TurnTo_Pwm_Out = PID_TurnTo_Ring(PID.BT_Left_Right_Speed, myMPU6050.gyro_z);
    PID.Left_Pwm_Out = PID.Upright_Pwm_Out + PID.Speed_Pwm_Out + PID.TurnTo_Pwm_Out;    // 最终输出
    PID.Right_Pwm_Out = PID.Upright_Pwm_Out + PID.Speed_Pwm_Out - PID.TurnTo_Pwm_Out;
    Motor.Motor_Clamp(&PID.Left_Pwm_Out, &PID.Right_Pwm_Out);   // 输出限幅
    if ((myMPU6050.mpu_pitch >= 27) || (myMPU6050.mpu_pitch <= -23))    // 小车跌倒清0
    {
        PID.Left_Pwm_Out = 0;
		PID.Right_Pwm_Out = 0;
    }
    Motor.Motor_Set_PWM(Motor_LEFT, PID.Left_Pwm_Out);  // 作用到电机
    Motor.Motor_Set_PWM(Motor_RIGHT, PID.Right_Pwm_Out);
}

/*
* @function: PID_Parameter_Clear
* @param: 
* @retval: None
* @brief: PID参数,输出清除(用于上位机改变参数时)
*/
static void PID_Parameter_Clear(void)
{
	Speed.err = 0.0;
	Speed.err_sum = 0.0;
	
	PID.Upright_Pwm_Out = 0;
	PID.Speed_Pwm_Out = 0;
	PID.TurnTo_Pwm_Out = 0;
	
    Motor.Motor_Set_PWM(Motor_LEFT, 0);
    Motor.Motor_Set_PWM(Motor_RIGHT, 0);
}

其他

中断

CallBack.c
cpp
/***************************************************************************
 * File: CallBack.c
 * Author: Luckys.
 * Date: 2023/06/19
 * description: 存放中断函数
****************************************************************************/
#include "AllHead.h"


/*
* @function: HAL_TIM_PeriodElapsedCallback
* @param: htim -> 处理定时器的结构体指针
* @retval: None
* @brief: 定时器回调函数
*/
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == htim7.Instance)
    {
		static uint8_t Key_Cnt;
		static uint8_t Tim7_5ms;
		
		Key_Cnt++;
		Tim7_5ms++;
		
		System.Task_Marks_Handler();    // 任务标记
		
		if (6 == Tim7_5ms)
		{
			Tim7_5ms = 0;
			Key.Key_Scan();
		}
		if (Key_Cnt >= 100)	// 按键长按计数
		{
			Key_Cnt = 0;
			Key.vusKey_Timer_Count++;
		}
    }   
	if (htim->Instance == htim8.Instance)
	{
		static uint8_t Timer8_5ms = 0;

		Timer8_5ms++;

		myMPU6050.MPU6050_Data_Read();	// 陀螺仪读取
		if (Timer8_5ms >= 5)
		{
			Timer8_5ms = 0;
			// 读取电机速度
			Encoder.Encoder_Speed_Read(Motor_LEFT);
			Encoder.Encoder_Speed_Read(Motor_RIGHT);
		}
		PID.PID_Handler();
	}
}

/*
* @function: USART2_IRQHandler
* @param: None
* @retval: None
* @brief: 串口2中断函数
*/
void USART2_IRQHandler(void)
{
	if(__HAL_UART_GET_FLAG(&huart2,UART_FLAG_IDLE) != 0x00u)
	{
		
        HC05.HC05_Protocol_Analyze();
		__HAL_UART_CLEAR_IDLEFLAG(&huart2);	// 清除标志位一定要放在后面
	}    
    HAL_UART_IRQHandler(&huart2);
}

/*
* @function: USART3_IRQHandler
* @param: None
* @retval: None
* @brief: 串口3中断函数
*/
void USART3_IRQHandler(void)
{ 
	if(__HAL_UART_GET_FLAG(&huart3,UART_FLAG_IDLE) != 0x00u)
	{
		Voice.Voice_Protocol_Analyze();
		__HAL_UART_CLEAR_IDLEFLAG(&huart3);
	}    

    HAL_UART_IRQHandler(&huart3);
}

公共

Public.h
cpp
#ifndef __PUBLIC_H
#define __PUBLIC_H

/***********************global macro precompilation BEGIN***********************/
// Debug串口
#define	huart_debug		huart1
// 打印
#define LOG_DEBUG 0
/***********************global macro precompilation   END***********************/

// BIT
typedef enum
{
    BIT0 = (uint8_t)(0x01 << 0),    // 0x01 -- 0000 0001
    BIT1 = (uint8_t)(0x01 << 1),    // 0x02 -- 0000 0010
    BIT2 = (uint8_t)(0x01 << 2),    // 0x04 -- 0000 0100    
    BIT3 = (uint8_t)(0x01 << 3),    // 0x08 -- 0000 1000
    BIT4 = (uint8_t)(0x01 << 4),    // 0x10 -- 0001 0000
    BIT5 = (uint8_t)(0x01 << 5),    // 0x20 -- 0010 0000
    BIT6 = (uint8_t)(0x01 << 6),    // 0x40 -- 0100 0000
    BIT7 = (uint8_t)(0x01 << 7),    // 0x80 -- 1000 0000
}BIT_t;

// TRUE/FALSE
typedef enum
{
    FALSE = 0U,
    TRUE = !FALSE 
}FLagStatus_t;

typedef struct
{
    void (*Memory_Clear)(uint8_t*, uint16_t);   // 内存清除函数
    void (*Public_Delay_ms)(uint16_t);  // 系统ms延时
    void (*Public_Delay_us)(uint16_t);  // 系统us延时
	void (*UsartPrintf)(UART_HandleTypeDef, char *,...);	// 打印
    void (*Memory_Copy)(char*, const char*, uint16_t);// 复制固定长度数组/字符串
	int (*Number_ABS)(int n);	// 绝对值转换
	void (*Str_To_Float)(const char *, float *);	// 提取字符串里面的=后面的浮点数
}Public_t;

extern Public_t Public;

#endif
Public.c
cpp
/***************************************************************************
 * File: Public.c
 * Author: Luckys.
 * Date: 2023/06/23
 * description: 存放通用
****************************************************************************/
#include "AllHead.h"
#include <stdarg.h>
#include <string.h>
#include <stdio.h>
/*====================================static function declaration area BEGIN====================================*/

static inline void Memory_Clear(uint8_t*, uint16_t);   
static void Public_Delay_ms(uint16_t);  
static void Public_Delay_us(uint16_t);
static void UsartPrintf(UART_HandleTypeDef, char *,...);
static inline void Memory_Copy(char*, const char*, uint16_t);
static inline int Number_ABS(int);
static inline void Str_To_Float(const char *, float *);

/*====================================static function declaration area   END====================================*/

Public_t Public = 
{
    .Memory_Clear = &Memory_Clear,
    .Public_Delay_ms = &Public_Delay_ms,
    .Public_Delay_us = &Public_Delay_us,
	.UsartPrintf = &UsartPrintf,
    .Memory_Copy = &Memory_Copy,
	.Number_ABS = &Number_ABS,
	.Str_To_Float = &Str_To_Float
};


/*
* @function: Memory_Clear
* @param: pucBuffer -> 内存首地址 LEN -> 内存长度
* @retval: None
* @brief: 描述
*/
static inline void Memory_Clear(uint8_t* pucBuffer, uint16_t LEN)
{
    uint16_t i;

    for (i = 0; i < LEN; i++)
    {
        *(pucBuffer + i) = (uint8_t)0;
    }
}

/*
* @function: Public_Delay_ms
* @param: ms -> 需要延时的时间(ms)
* @retval: None
* @brief: 系统ms延时
*/
static void Public_Delay_ms(uint16_t ms)
{
    HAL_Delay(ms);
}

/*
* @function: Public_Delay_us
* @param: us -> 需要延时的时间(us)
* @retval: None
* @brief: 系统us延时
*/
static void Public_Delay_us(uint16_t us)
{
    uint8_t i;
    // 通过示波器测量进行校准
    while(us--)
    {
        for (i = 0; i < 7; i++);
    }
}

/*
* @function: UsartPrintf
* @param: None
* @retval: None
* @brief: 打印函数
*/
static void UsartPrintf(UART_HandleTypeDef USARTx, char *fmt, ...)
{

    uint8_t UsartPrintfBuf[296];
    va_list ap;
    uint8_t *pStr = UsartPrintfBuf;

    va_start(ap, fmt);
    vsnprintf((char *)UsartPrintfBuf, sizeof(UsartPrintfBuf), fmt, ap); // 格式化
    va_end(ap);

    while (*pStr != NULL)
    {
        HAL_UART_Transmit(&USARTx, (uint8_t *)pStr++, 1, HAL_MAX_DELAY);
    }
}

/*
* @function: Memory_Copy
* @param: dest -> 要复制到的指针地址 src -> 被复制的指针地址 count -> 大小(数组用sizeof 字符串用strlen)
* @retval: None
* @brief: 复制固定长度数组/字符串
*/
static inline void Memory_Copy(char* dest, const char* src, uint16_t count)
{
    uint16_t i;

    for (i = 0; i < count; i++) 
    {
        dest[i] = src[i];
    }    
}

/*
* @function: Number_ABS
* @param: n -> 要转换的数值
* @retval: 转换完的数值
* @brief: 转换为绝对值返回
*/
static inline int Number_ABS(int n)
{
  if(n<0)
	{
	  n=(-n);
	}
	if(n>=0)
	{
	 n=n;
	}
	return n;
}

/*
* @function: Str_To_Float
* @param: p_Str -> 待提取的字符串 save_var -> 提取结果存储地址
* @retval: None
* @brief: 提取字符串里面=后面的浮点数
*/
static inline void Str_To_Float(const char *p_Str, float *save_var)
{
	const char* start = strchr(p_Str, '='); // 查找等号的位置
	
    if (start == NULL)
    {
        return;
    }
    sscanf(start + 1, "%f", save_var); // 使用 sscanf 函数提取浮点数值
}

/*
* @function: fputc
* @param: None
* @retval: None
* @brief: 重定向printf函数(&必须要写)
*/
int fputc(int ch, FILE *f)
{
	HAL_UART_Transmit(&huart_debug,(uint8_t*)&ch,1,0xFFFF);
    return ch;
}

初始化

System_Init.c
cpp
/***************************************************************************
 * File: System_Init.c
 * Author: Luckys.
 * Date: 2023/06/23
 * description: 存放系统初始化
****************************************************************************/
#include "AllHead.h"

/*====================================static function declaration area BEGIN====================================*/

static void Hardware_Init(void);

/*====================================static function declaration area   END====================================*/


System_Init_t System_Init = 
{
    Hardware_Init
};

/*
* @function: Hardware_Init
* @param: None
* @retval: None
* @brief: 硬件初始化
*/
static void Hardware_Init(void)
{
    Voice.Voice_Init(); // 语音识别初始化
	HC05.HC05_Init();	// 蓝牙初始化
    Led.Led_Init(); // LED初始化
    Buzzer.Buzzer_Init();   // 蜂鸣器初始化
	myADC.ADC_Calibration_Start_DMA();
    myMPU6050.mpu_dmp_flag = MPU_Init(); // 初始化MPU6050
	myMPU6050.mpu_dmp_flag = mpu_dmp_init();
	Menu.Menu_Init();	// OLED初始化
	UI.UI_U8g2_Init();
	Motor.Motor_Init();	// 电机初始化
    Encoder.Encoder_Timer_Init();   // 编码器初始化
	PID.PID_Parameter_Init();   // PID参数初始化
    HAL_TIM_Base_Start_IT(&htim7);  // 系统计数定时器
    HAL_TIM_Base_Start_IT(&htim8);  // 编码器,6050等代码执行
	Public.UsartPrintf(huart_debug,"Init Success\r\n");
}

任务

Task.h
cpp
#ifndef __TASK_H
#define __TASK_H


typedef struct
{
    uint8_t Run_Status; // 任务状态:TRUE/FALSE
    uint16_t Task_Cnt;  // 任务定时计数器(ms)
    uint16_t Task_Timer;    // 重载计数器(任务分配的时间ms)
    void (*Task_Hook)(void);    // 任务函数
}Task_t;

extern Task_t Task[];
extern uint8_t ucTasks_Max;

#endif
Task.c
cpp
/***************************************************************************
 * File: Task.c
 * Author: Luckys.
 * Date: 2023/06/23
 * description: 
****************************************************************************/
#include "AllHead.h"

/*====================================static function declaration area BEGIN====================================*/
static void TasksHandle_2MS(void);
static void TasksHandle_10MS(void);
static void TasksHandle_50MS(void);
static void TasksHandle_100MS(void);
static void TasksHandle_1S(void);
double extractFloatValue(const char* str);
/*====================================static function declaration area   END====================================*/

Task_t Task[] =
{
    {FALSE, 2, 2, TasksHandle_2MS},    // task Period: 2ms
    {FALSE, 10, 10, TasksHandle_10MS},    // task Period: 10ms
	{FALSE, 50, 50, TasksHandle_50MS},    // task Period: 50ms
    {FALSE, 100, 100, TasksHandle_100MS},    // task Period: 500ms
	{FALSE, 1000, 1000, TasksHandle_1S},    // task Period: 1s
};

/*====================================variable definition declaration area BEGIN===================================*/

uint8_t ucTasks_Max = sizeof(Task) / sizeof(Task[0]);

/*====================================variable definition declaration area   END===================================*/

static void TasksHandle_2MS(void)
{
    HC05.HC05_Handler();
}

static void TasksHandle_10MS(void)
{
    Key.Key_Handler(); 
}

static void TasksHandle_50MS(void)
{
#if 0	// 测距	
	float a = 0;
	a = HC_SR04.HC_SR04_Ranging();
	Public.UsartPrintf(huart_debug, "JL:%.2f\r\n",a);
#endif	
}

static void TasksHandle_1S(void)
{
	static uint8_t led_switch_flag = FALSE;
	
	if (FALSE == led_switch_flag)
    {
        Led.Led_ON(LED1);
        Led.Led_ON(LED2);
        Led.Led_OFF(LED3);
        Led.Led_OFF(LED4);        
    }
    else
    {
        Led.Led_ON(LED3);
        Led.Led_ON(LED4);
        Led.Led_OFF(LED1);
        Led.Led_OFF(LED2);
    }
    led_switch_flag = !led_switch_flag;    
}

static void TasksHandle_100MS(void)
{
	Menu.Menu_Switch();
}

系统

System.h
cpp
#ifndef __SYSTEM_H
#define __SYSTEM_H


typedef struct
{
    void (*Run)(void);  // 系统运行
    void (*SysTem_Func_Fail_Handler)(void); // 函数错误处理
    void (*System_Assert_Fail_Handler)(void);   // 参数错误处理
    void (*Task_Marks_Handler)(void);   // 任务标记函数
}System_t;

extern System_t System;

#endif
System.c
cpp
/***************************************************************************
 * File: System.c
 * Author: Luckys.
 * Date: 2023/06/23
 * description: 存放系统相关
****************************************************************************/
#include "AllHead.h"


/*====================================static function declaration area BEGIN====================================*/

static void Run(void);
static void SysTem_Func_Fail_Handler(void); 
static void System_Assert_Fail_Handler(void);   
static void Task_Marks_Handler(void);
static void Task_Pro_Handler(void);

/*====================================static function declaration area   END====================================*/

System_t System = 
{
    Run,
    SysTem_Func_Fail_Handler,
    System_Assert_Fail_Handler,
    Task_Marks_Handler
};

/*
* @function: Run
* @param: None
* @retval: None
* @brief: 系统运行
*/
static void Run(void)
{
    Task_Pro_Handler(); // 任务调度
}

/*
* @function: SysTem_Func_Fail_Handler
* @param: None
* @retval: None
* @brief: 系统函数错误处理
*/
static void SysTem_Func_Fail_Handler(void)
{
    
}

/*
* @function: System_Assert_Fail_Handler
* @param: None
* @retval: None
* @brief: 系统参数错误处理
*/
static void System_Assert_Fail_Handler(void)
{
    
}

/*
* @function: Task_Marks_Handler
* @param: None
* @retval: None
* @brief: 任务标记函数
*/
static void Task_Marks_Handler(void)
{
    uint8_t i;

    for (i = 0; i < ucTasks_Max; i++)
    {
        if (Task[i].Task_Cnt)   // 判断计数是否为0
        {
            Task[i].Task_Cnt--; // 递减
            if (0 == Task[i].Task_Cnt)  // 计数到0
            {
                Task[i].Task_Cnt = Task[i].Task_Timer;  // 重装载计数
                Task[i].Run_Status = TRUE;  // 任务执行状态标志置1
            }
        }
    }
}

/*
* @function: Task_Pro_Handler
* @param: None
* @retval: None
* @brief: 任务处理函数
*/
static void Task_Pro_Handler(void)
{
    uint8_t i;

    for (i = 0; i < ucTasks_Max; i++)
    {
        if (Task[i].Run_Status) // 判断执行状态:TRUE--执行 FALSE--不执行
        {
            Task[i].Run_Status = FALSE;
            Task[i].Task_Hook();    // 执行函数
        }
    }
}

问题

这里PCB注释写错了以焊盘为准(PCB里已经修改实物板未修改)

焊接电源后发现输入12V正常测到,输出5V测不到,相当于输出部分电压全部0V,经过看数据手册知道EN引脚需要接高电平,不能悬空,但是没有飞线铜丝所以把杜邦线拆了拿条铜丝进行飞线,把EN飞到VIN那,然后再测量输出5V左右正常了,这个教训告诉我要看数据手册的参考电路,不能只看引脚说明!!!(原理图已修改接到VIN然后顺便修改了IC的5V线宽20~25mil),不能直接飞会烧IC(刚刚直接燃起来了…),需要按照手册串联电阻

重写HAL_Delay

不太明白官方源码为啥这么写,会多延时1ms,注释掉后更准

cpp
__weak void HAL_Delay(uint32_t Delay)
{
  uint32_t tickstart = HAL_GetTick();
  uint32_t wait = Delay;

  /* Add a freq to guarantee minimum wait */
//  if (wait < HAL_MAX_DELAY)
//  {
//    wait += (uint32_t)(uwTickFreq);
//  }

  while ((HAL_GetTick() - tickstart) < wait)
  {
  }
}

MPU读不出数据,后面把 mpu6050.c 里的IIC操作里把等待应答的全部屏蔽

MPU初始化卡死,原因是打印问题,把log语句全部屏蔽

MPU读取函数在中断里卡死,查找原因是因为读取函数用到了系统延时HAL_Delay,导致中断里卡死,解决方法是把系统Systick中断优先级提高,比MPU读取函数所在的中断高就行了

ADC打开DMA中断的话导致卡死,可能是因为上面修改了优先级问题,所以我把DMA中断关闭了

蓝牙串口2问题,如果发送数据到蓝牙太快导致程序卡死,找到问题是标志位的清除问题,把串口中断标志位清除放在协议解析完后面就行了

蓝牙调试PID时发送要有结束符 \r,否则接收会有问题【我第一次没加\r导致发送新值如果少于旧值的位数,则结果变成新值加旧值】

调参

  • 直立环调整

极性确定:注释速度环和转向环,分别把 KpK_pKdK_d 置0,现象一样

现象:极性正确:倾斜小车,车轮转动方向和倾斜方向一致;极性错误:倾斜小车,车轮转动方向和倾斜方向相反

参数确定:注释速度环和转向环

KpK_p:先将 KdK_d 置0,将 KpK_p 参数从小到大,当小车基本稳定后继续加大 KpK_p 直到出现低频抖动(需要预估,比如ARR最大是7200,那倾斜10度我们给一半,3600,除以10就是360,一开始给360看看)

KdK_d:逐渐加大 KdK_d,当小车可以很好的直立后继续加大 KdK_d 直到出现高频抖动(出现后要马上断开电源因为长时间高频抖动会损坏电机)

  • 速度环调整

直立环保留且在其参数基础上乘以 0.6(为什么呢?这个是经验值),然后发现没有加速度环时车很难保持直立

平衡小车调速使用的是正反馈,比如我们小车正在以一定速度运动,此时需要小车停下,正常逻辑是减小轮子速度。这是负反馈。但是放到平衡小车上,如果此时减小轮子速度,小车会因为惯性向前倾倒。反而需要加速轮子追上车身,这时倾角减小,直立环作用小车反而会停下。所以需要使用 正反馈

极性确定:注释直立环和转向环

现象:极性正确:轻微转一下其中一个轮,轮子不断加速直到最大转速;极性错误:旋转其中一个轮子,另一个轮子反向转动

比如一个球放在上面,手推它,它肯定往下滚,速度越来越大,这个就是正反馈

参数确定:打开直立环,注释转向环

KpK_p:逐渐增大KpK_p,使小车很好的保持直立,且原地不动(预估值,因为公式里它还要乘以一个直立环的KpK_p所以这里的KPK_P尽量小点)

KiK_i:为KpK_p1/200

  • 转向环

极性确定:注释直立环,注释速度环

调节KpK_p的时候需要在转向环参数传入期望角速度,可以设定一个 10

Kp极性正确:用手转动小车,小车帮助转动;Kp极性错误:用手转动小车,小车抑制转动

调节KdK_d的时候需要在转向环参数传入期望角速度,必须设定为 0,因为不期望旋转

Kd极性正确:用手转动小车,小车抑制转动;Kd极性错误:用手转动小车,小车帮助转动

参数确定:注释直立环和速度环

KpK_p:逐渐增加KpK_p,使小车可以很好的保持直立,且可以按期望旋转

KdK_d:逐渐增加KdK_d,使小车可以很好的保持直立,且可以按期稳定在原地

调好后把三个环打开

实操

测机械中值即(Roll),往后倒是-1.38,往前倒是-1.6,取中间大概是-1.4

小车跌倒的话我的是Roll角,一边是26,另一边是-27【因为安装方式不一样所以我选择修改哪个矩阵遵循一般的安装方式】

函数 PID_Upright_Ring 入口参数1是pitch角,参数2角速度看安装方向,我的是Gyrox变化【正常是前倾是正数后倾是负数但是我的相反所以我把它加个负号前面】

极性1:负数,给200左右大点小的话看不到效果

极性2:正数,不要给200这么大不然转太快然后没反应了,给小点,1~2之间,然后把车拿起来前后倾看看轮子是不是转的一致

Kp和Ki给正数看效果,手动转一边轮子发现慢慢加到最大速度,如果都给负数发现转一边另一边反转,错误

然后慢慢的给,一开始发现不管怎么给它还是往一个方向去慢慢的,后面我把机械中值直接改成后点就可以了,所以机械中值很重要

实验现象

调得不太行,还是傻傻的,废了废了,然后12V转5V电路也有问题所以只能通过DapLink连着供电5V