STM32项目-STM32智能小车-电子设计大赛-STM32cubemx-STM32f103c8t6STM32串口通信-

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

记录项目的详细制作过程所以笔记很长图很多、很多图不好CSDN搬运

我把笔记放网盘或者自己根据资料下载
笔记网盘下载:
链接:https://pan.baidu.com/s/1Mk2EVIha7Fpj4Xductg3Uw?pwd=VCC1
提取码:VCC1
笔记CSDN下载:

第一章-硬件

1.1-元件选型

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-R0hM1EjE-1675083353042)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/20230130191750.png)]

1.2-原理图与PCB

底板原理图

各个模块的供电电压?

模块接口引脚顺序?

如何确定使用单片机那个引脚?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iaKe5GlI-1675083353043)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221202195108446.png)]

STM32F103C8T6核心板原理图(可能使用不同核心板略有差异)

image-20220715153616072

PCB顶层截图

不同类型线粗细

布局总线方式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zLeffdEC-1675083353043)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221227210621750.png)]

1.3-焊接

PCB正面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hRLDDbEu-1675083353044)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221231144357465.png)]

PCB背面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-McMjFy3d-1675083353044)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221231144233002.png)]

然后插上元件

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LIQRGX3C-1675083353045)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221231144943159.png)]

1.4-结构与组装

这是组装好的车体照片

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dQimnPjC-1675083353045)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221231150645127.png)]

然后小车安装PCB

注意电机和红外对管不要插错

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w0zfyyzc-1675083353046)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221231155644678.png)]

1.5-测试

使用万用表 上电前测试一下

第二章-GPIO与中断

2.0-新建工程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2pvofEC5-1675083353046)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221203140147942.png)]

建议选择和我一样的版本

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-toAilRjw-1675083353046)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221203135524319.png)]

新建一个工程

image-20220715154436669

选择芯片

image-20220715154552489

选择时钟源

image-20220715154653642

配置时钟树

image-20220715155004533

选择调试

image-20220715155046118

勾选生成独立的文件

image-20220715155302764

设置保存地址

image-20220715155657378

勾选这个不添加没有使用库文件可以减小工程文件大小(也可以不勾选保持默认设置)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bgTxOyBO-1675083353048)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230112223312210.png)]

MDK打开工程调低优化等级

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RR9cj3Nb-1675083353048)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221203142727527.png)]

以上是每次新建工程要做的

以后我们不在新建工程使用之间的工程即可

2.1-点灯

这里我们点亮PC13连接的小灯

image-20220715154319943

配置PC13

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-URN61Csx-1675083353049)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/202301302021379.png)]

生成代码

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MQ6d68bD-1675083353049)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/202301302021948.png)]

生成代码后使用MDK打开工程

image-20220716160106728

先编译一下没有报错、没有问题

image-20220716160157185

在BEGIN和END添加代码

image-20220716160451492

	HAL_GPIO_TogglePin(LED_GPIO_Port,LED_Pin);
	HAL_Delay(500);

根据自己的芯片选择

image-20220716162757551

烧录程序(必看 使用其中一个方法)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QBdNfrTe-1675083353051)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221227230705273.png)]

方法一:使用DAP LINK

接线图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oG4jclwk-1675083353051)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221229210725895.png)]

DAP 在Win 10 免驱动的

然后根据自己使用的工具在MDK中设置下载工具

image-20220716160642306

设置下载算法

image-20220716163120042

然后下载程序复位小灯闪烁

image-20220716163318393

烧录后现象

小灯每0.5秒闪烁一次

方法二:使用stlink

接线图

STlink不要接3.3V

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cLWQnF0v-1675083353052)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221229211513383.png)]

使用Stlink 前先安装驱动

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xWEOxovf-1675083353052)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221227225436767.png)]

双击运行

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IJrZV7jN-1675083353053)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221227225603998.png)]

选择ST-Link

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jWA1bnyU-1675083353053)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221227224923189.png)]

选择算法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TuKw9qBS-1675083353053)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221227225802761.png)]

然后点击编译烧录

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ry4Y8bkd-1675083353053)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221227230419753.png)]

烧录后现象

小灯每0.5秒闪烁一次

补充可能遇到的失败情况

使用DAP-LINK

如果我们芯片IDCODE是0x2 开头的那么我们需要替换一下Keil 的器件包

(如果你是0x1 开头的如果能下载可以不替换)

image-20220729142306116

STM32小车相关资料V3.3.0\04使用的软件\中科芯CKS芯片支持包

image-20220729142706246

image-20220729142753531

下面这个算法就会自动切换

image-20220729142828921

使用stlink

2.2-按键

先看原理图

PB4–KEY1 单片机设置下拉输入-、上降沿触发

PA12–KEY2 单片机设置上拉输入、下降沿触发

image-20220716164000681

开始配置

image-20220716165737010

使能外部中断

image-20220727203928114

然后生成代码

重新实现中断回调函数、编写按键检测程序

image-20220716170830949

在gpio.c 中我们编写该函数

image-20220727204139553

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
	if(GPIO_Pin == KEY1_Pin){//判断一下那个引脚触发中断
	//这里编写触发中断后要执行的程序
	HAL_GPIO_TogglePin(LED_GPIO_Port,LED_Pin);//切换LED GPIO状态
	}
	if(GPIO_Pin == KEY2_Pin){//判断一下那个引脚触发中断
	//这里编写触发中断后要执行的程序
	HAL_GPIO_TogglePin(LED_GPIO_Port,LED_Pin);//切换LED GPIO状态
	}
}

把main中控制闪烁注释掉

image-20220727204442003

烧录后的现象

按下KEY1 或者KEY2可以切换LED灯开关状态

第三章-OLED使用

3.1-资料准备

我们先去下载这个OLED模块的资料

image-20220727205105383

这里我们下载:优信电子–0.96寸 OLED显示液晶屏模块 IIC液晶屏 四引脚

淘宝链接:

https://item.taobao.com/item.htm?spm=a230r.1.14.16.504611e6WA3Clv&id=562145367495&ns=1&abbucket=3#detail

OLED资料链接:

0.96寸(4管脚)资料下载链接:

https://pan.baidu.com/s/1J57Izsv-PKmbwVrA2ynDzg 提取码:vktz

找到我们要的历程–中景园电子0.96OLED显示屏_STM32F103C8_IIC_V1.0

image-20220727210815907

3.2-相关知识

这个OLED是IIC协议很多都是单片机模拟IIC和模块通信的这个也是模拟IIC控制OLED的

我们先看一下这个历程

image-20220727210946103

image-20220727211027636

image-20220727211352797

所谓我们移植的时候替换相关初始化内容和GPIO置为函数就行

3.3-解决一些错误

把OLED文件复制过去

image-20220727212108337

添加组和包含文件

image-20220727213102694 image-20220727213054063

选择添加路径

image-20220727213257813

编译一下–找不到sys.h 删掉sys.h

image-20220727213408708

编译一下–把所有的u8都替换成uint8_t u32 替换成uint32_t

image-20220727214626867

编译报错 找不到uint8_t 包含一下#include “main.h” 解决

image-20220727214254871

有警告 声明加上void

image-20220727214420870

下面是一些GPIO的错误我要解决初始化问题了

image-20220727214806261

3.4-开始初始化OLED

先看原理图 SDA-PB12 SCL-PA15

image-20220727215225137

然后我们开始初始两个GPIO为输出模式–上拉输出模式

image-20220727224538128

然后我们生成代码更改一下IIC协议的GPIO设置和初始化部分

image-20220727230824437

#define OLED_SCLK_Clr() HAL_GPIO_WritePin(OLED_SCL_GPIO_Port, OLED_SCL_Pin, GPIO_PIN_RESET)//设置SCL低电平
#define OLED_SCLK_Set() HAL_GPIO_WritePin(OLED_SCL_GPIO_Port, OLED_SCL_Pin, GPIO_PIN_SET)//设置SCL高电平

#define OLED_SDIN_Clr() HAL_GPIO_WritePin(OLED_SDA_GPIO_Port,OLED_SDA_Pin,GPIO_PIN_RESET)//设置SDA低电平
#define OLED_SDIN_Set() HAL_GPIO_WritePin(OLED_SDA_GPIO_Port,OLED_SDA_Pin,GPIO_PIN_SET)//设置SDA高电平

image-20220727231448989

下面delay函数出现报错 我们替换成HAL_Delay

image-20220727231848062

image-20220727232059531

编译没有报错了我们在主函数添加初始化和测试代码

image-20220727233213431

  OLED_Init();			//初始化OLED  
  OLED_Clear(); 
  
  		OLED_ShowCHinese(0,0,0);//中
		OLED_ShowCHinese(18,0,1);//景
		OLED_ShowCHinese(36,0,2);//园
		OLED_ShowCHinese(54,0,3);//电
		OLED_ShowCHinese(72,0,4);//子
		OLED_ShowCHinese(90,0,5);//科
		OLED_ShowCHinese(108,0,6);//技
  

烧录下载 现象OLED屏幕显示-中景园电子科技

第四章-串口实验(简单输出)

这里我们先初始化串口一、实现数据输出。

4.1-串口编写

软件初始化

image-20220727234545569

然后我们实现串口数据输出

方法一:

image-20220728210913655

	uint8_t c_Data[] = "串口输出测试:好家伙VCC\r\n";
	HAL_UART_Transmit(&huart1,c_Data,sizeof(c_Data),0xFFFF);
	HAL_Delay(1000);

方法二:实现printf函数

打开微库

image-20220728211333083

重定向fputc

image-20220728211730782

/**
* @brief 重定向printf (重定向fputc)
					使用时候记得勾选上魔法棒->Target->UseMicro LIB 
					可能需要在C文件加typedef struct __FILE FILE;
					包含这个文件#include "stdio.h"
* @param 
* @return 
*/
int fputc(int ch,FILE *stream)
{
	HAL_UART_Transmit(&huart1,( uint8_t *)&ch,1,0xFFFF);
	return ch;
}

如果有错误

image-20220728211515588

在usart.c添加这个typedef struct __FILE FILE;

image-20220728211651598

添加一下测试(记得包含"stdio.h")

image-20220728213124409

printf("printf:好家伙VCC测试\r\n");

4.2-串口实验

接线图

先烧录好再连接串口查看现象

连接串口 可以使用 USB转TTL如CH340模块 或者 用DAP的串口功能

使用USB转TTL如CH340模块

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0PsmFk7B-1675083353063)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221229214107521.png)]

使用DAP

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ib1hMh1b-1675083353064)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221229214655460.png)]

然后我们打开串口助手选择串口端口和波特率就可以看到输出

蓝牙模块使用

蓝牙模式使用在后面章节讲解

第五章-PWM控制电机

5.1-认识PWM

参数如何描述PWM

image-20220729093044088

5.2-PWM配置

根据我们小车原理图我们知道是 PA11和PA8两个引脚要设置为PWM输出

这里为什么小车原理图要这样设计那?

  1. 根据A4950的使用要求
  2. 根据STM32F103C8T6的定时器复用功能重映射
image-20220729102313833

我们这先介绍原因:

原因1:介绍电机驱动后我们会说明

原因2: 因为STM32中文参考手册介绍了TIM1_CH1和TIM1_CH4可以复用功能重映射到PA8和PA11

image-20220729102920289

我们使用软件配置 PA11和PA8这里配置

image-20220729104029766

image-20220729105524572

image-20220729105741439

然后我们生成代码

PWM输出的配置就已经完成了但是不能输出产生PWM波因为Cube在生成代码时有很多外设初始化完后默认是关闭的需要我们手动开启。

image-20220729110142345
  HAL_TIM_PWM_Start(&htim1,TIM_CHANNEL_1);//开启定时器1 通道1 PWM输出
  HAL_TIM_PWM_Start(&htim1,TIM_CHANNEL_4);//开启定时器1 通道4 PWM输出

我们软件仿真一下、查看PA11与PA8波形

image-20220729164150949

那么频率就是 1/0.002 = 500HZ

这就是我们要设置的

我们可以使用这个宏来修改占空比

 __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, 40);

image-20220729170905026

5.3-PWM测试方法

上面我们生成了PWM下面我们测试一下

KEIL软件仿真方法:

软件模拟仿真不需要任何硬件-下面是官方介绍

image-20220729145446106

选择软件仿真

image-20220729150059613

DARMSTM.DLL
-pSTM32F103C8

设置时钟频率-板子外部晶振8Mhz 这里我们选择8Mhz

(新版的keil5里没有那个设置频率的功能)

image-20220729162145332

开启仿真

image-20220729150321775

打开逻辑分析仪器

image-20220729150442778

添加要观察的引脚

image-20220729151057941

点击全速运行

image-20220729151216082

使用仿真器硬件仿真

选择仿真器仿真-检测已经识别出芯片ID

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u2b0J6K5-1675083353067)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221203144038645.png)]

一样的可以开启仿真

image-20220729162843082

但是硬件仿真好像目前还不能使用过逻辑分析仪、但是硬件仿真是在硬件上跑的可以向硬件输入数据或者由硬件输出数据、比如按键仿真的时候就可以使用硬件仿真。

使用示波器工具测量波形(非重点)

第六章-电机驱动和PWM

6.1-认识电机驱动

示波器、硬件仿真、软件仿真

项目使用电机驱动芯片为A4950、下面是电机驱动的相关介绍

image-20220728215212236 image-20220728215543547

我们按照这种使用方法

image-20220728215220067

这我们使用一个图介绍

image-20220730173302397

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-glMQEPmJ-1675083353068)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230101164812094.png)]

6.2-使用电机驱动(独立工程)

分析和编写代码

综合电机使用方法、C8T6单片机硬件资源、小车原理图我们要进行如下配置

PA11-TIM1_CH4 定时器PWM输出-PWMA 前面已经完成

PB13-GPIO输出-AIN1

PA8-TIM1_CH1 定时器PWM输出-PWMB 前面已经完成

PB3-GPIO输出-BIN1

image-20220730165128928

还有两个管脚没有初始化

image-20220730175458784

生成代码

开始添加控制电机正反转与速度的代码进行仿真和电机测试示波器测量

添加AIN1、BIN1控制代码

image-20220806092857784

	HAL_GPIO_WritePin(AIN1_GPIO_Port,AIN1_Pin,GPIO_PIN_RESET);//设置AIN1 PB13为 低电平
	HAL_GPIO_WritePin(BIN1_GPIO_Port,BIN1_Pin,GPIO_PIN_SET);  //设置BIN1 PB3为高电平
	HAL_Delay(1000);
	//两次会使得电机反向。
	HAL_GPIO_WritePin(AIN1_GPIO_Port,AIN1_Pin,GPIO_PIN_SET);//设置AIN1 PB13为 高电平
	HAL_GPIO_WritePin(BIN1_GPIO_Port,BIN1_Pin,GPIO_PIN_RESET);  //设置BIN1 PB3为低电平

仿真测试代码

使用软件仿真

检测是否软件仿真设置正确

image-20220730192214904

开启仿真-添加PB13和PB3到逻辑分析仪

image-20220730192626969

全速仿真运行

image-20220731185432931

实物测试代码

如何让电机90%电压转速 旋转

烧录代码

6.3-编写电机转速开环控制函数(另外复制工程)

新建motor文件

image-20220806094030498

包含文件并添加编译

image-20220806094635879

为了方便移植和使用我们GPIO电平控制写成宏

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VeLB1AXk-1675083353071)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230101150840270.png)]

#define AIN1_RESET  HAL_GPIO_WritePin(AIN1_GPIO_Port,AIN1_Pin,GPIO_PIN_RESET)//设置AIN1 PB13为 低电平
#define AIN1_SET    HAL_GPIO_WritePin(AIN1_GPIO_Port,AIN1_Pin,GPIO_PIN_SET)//设置AIN1 PB13为 高电平

#define BIN1_RESET 	HAL_GPIO_WritePin(BIN1_GPIO_Port,BIN1_Pin,GPIO_PIN_RESET)  //设置BIN1 PB3为低电平
#define BIN1_SET    HAL_GPIO_WritePin(BIN1_GPIO_Port,BIN1_Pin,GPIO_PIN_SET)//设置AIN1 PB13为 高电平

下面我们编写小车电机方向和速度控制

/*******************
*  @brief  设置两个电机转速和方向
*  @param  motor1:电机B设置参数、motor2:设置参数
*  @param  motor1: 输入1~100 对应控制B电机正方向速度在1%-100%、输入-1~-100 对应控制B电机反方向速度在1%-100%、motor2同理
*  @return  无
*
*******************/
void Motor_Set (int motor1,int motor2)
{
	//根据参数正负 设置选择方向
	if(motor1 < 0) BIN1_SET;
	   else      BIN1_RESET;
	if(motor2 < 0) AIN1_SET;
		else      AIN1_RESET;
	
	//motor1 设置电机B的转速
	if(motor1 < 0)
	{
		if(motor1 < -99) motor1 = -99;//超过PWM幅值
		//负的时候绝对值越小  PWM占空比越大
		//现在的motor1      -1   -99
		//给寄存器或者函数  99  1 
		 __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, (100+motor1));//修改定时器1 通道1 PA8 Pulse改变占空比
	}
	else{
		if(motor1 > 99) motor1 = 99;
		//现在是   0 1  99
		//我们赋值 0 1 99
		 __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, motor1);//修改定时器1 通道1 PA8 Pulse改变占空比
	}
	
	//motor2 设置电机A的转速
	if(motor2 < 0)
	{
		if(motor2 < -99) motor2 = -99;//超过PWM幅值
		//负的时候绝对值越小  PWM占空比越大
		//现在的motor2      -1   -99
		//给寄存器或者函数   99  1 
		__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_4, (100+motor2));//修改定时器1 通道4 PA11 Pulse改变占空比
	}
	else{
		if(motor2 > 99) motor2 = 99;
		//现在是   0 1 99
		//我们赋值 0 1 99
		 __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_4, motor2);//修改定时器1 通道4 PA11 Pulse改变占空比

	}

}  

然后我们连接电机主函数进行测试

	HAL_Delay(500);
	Motor_Set(0,0);

第七章-编码器测速

7.1-认识编码器

编码器:一般按照电机尾部、用于测量电机转速、方向、位置。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-151sF0Du-1675083353071)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230102003312991.png)]

那么编码器的输出信号具体是什么?我们如何根据输出信号测量转速 和方向?

转速: 单位时间测量到的脉冲数量(比如根据每秒测量到多少个脉冲来计算转速)

旋转方向: 两通道信号的相对电平关系

image-20220806200542121

7.2单片机定时器的编码器功能

那么我们已经知道编码器输出的波形我们如何通过单片机读取波形然后计算出速度那?

这里STM32单片机的定时器和通用定时器具有编码器接口模式、在STM32中文参考手册13章中有详细介绍

STM32中文参考手册-第200页

image-20220806225827403

STM32中文参考手册-第267页

image-20220807104608031

STM32中文参考手册-第226页

image-20220806222325272

这个是计数方向与编码器信号的关系、我们拆开来看

仅在TI1计数、电机正转、对原始数据二倍频

image-20220806222556492

仅在TI1计数、电机反转、对原始数据二倍频

image-20220806222737004

在TI1和TI2都计数

可以看到这样就对原始数据四倍频了

image-20220806222848485

计数方向

image-20220809144105548

7.3-获得单位时间计数器值变化量

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oMS4AfFe-1675083353072)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230103004459822.png)]

上一次说的方法:

这次编码器计数值 = 计数器值+计数溢出次数 * 计数最大器计数最大值

计数器两次变化值 = 这次编码器计数值 - 上次编码器计数值

然后根据这个单位变化量计算速度

还有一种方法:

计数器变化量 = 当前计数器值

每次计数值清空

然后根据这个变化量 计算速度

然后我们再看具体到哪一款电机和编码器上如何测速

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bDPUA6wi-1675083353072)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230102161121087.png)]

在STM32中文参考手册-第119页

image-20220807110602314

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3FEZjGvN-1675083353073)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230102235948243.png)]

设置TIM2

image-20220807112130804

设置ITM2滤波器

image-20220807112239092

image-20220807153021728

同理设置TIM4

image-20220807112807336

设置TIM4滤波器

image-20220807112858479

image-20220807153047276

设置引脚上拉

image-20220807154059362

生成代码

开启定时器和定时中断

image-20220807162341446

  HAL_TIM_Encoder_Start(&htim2,TIM_CHANNEL_ALL);//开启定时器2
  HAL_TIM_Encoder_Start(&htim4,TIM_CHANNEL_ALL);//开启定时器4
  HAL_TIM_Base_Start_IT(&htim2);				//开启定时器2 中断
  HAL_TIM_Base_Start_IT(&htim4);                //开启定时器4 中断

在定义两个变量保存计数器值

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4VkeuNKo-1675083353075)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230102225054673.png)]

short Encoder1Count = 0;//编码器计数器值
short Encoder2Count = 0;

每2ms读取计数器值->清零计数器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OIsKQhTE-1675083353076)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230102231545491.png)]

	Motor_Set(0,0);
	//1.保存计数器值
	Encoder1Count =(short)__HAL_TIM_GET_COUNTER(&htim4);
	Encoder2Count =(short)__HAL_TIM_GET_COUNTER(&htim2);
	//2.清零计数器值
	__HAL_TIM_SET_COUNTER(&htim4,0);
	__HAL_TIM_SET_COUNTER(&htim2,0);
	
	printf("Encoder1Count:%d\r\n",Encoder1Count);
	printf("Encoder2Count:%d\r\n",Encoder2Count);	
	
	HAL_Delay(2);

接好电池、烧录代码、串口一连接电脑

用手转动电机1或者电机2 、串口助手可以看到输出信息了

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XMBEeUKF-1675083353076)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230102232135472.png)]

7.4-主函数周期测量转速

上面我们测量出来了溢出值我们再根据当前计数器值就可以测量出计数器变化量我们通过单位时间变量就可以计算出转速

下面是电机和编码器的参数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eZuuxSac-1675083353076)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230102161121087.png)]

我们先测试的结论是否有问题?

  1. 编码器计数器会不会在计数时间内溢出?
  2. 车轮旋转一周单片机编码器计数器计数多少?9.6乘11乘4
  3. 根据计算方法计算电机转速

定义两个float变量

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JrPGY14p-1675083353076)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230103140008234.png)]

float Motor1Speed = 0.00;
float Motor2Speed = 0.00;

下面是代码(一定要把主函数没有用的删除掉)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tkxa2XaI-1675083353077)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230103140050045.png)]

	//计算速度
	Motor1Speed = (float)Encode1Count*100/9.6/11/4;
	Motor2Speed = (float)Encode2Count*100/9.6/11/4;
	
	printf("Motor1Speed:%.2f\r\n",Motor1Speed);
	printf("Motor2Speed:%.2f\r\n",Motor2Speed);

编译烧录代码就会输出结果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-btHVNFbq-1675083353077)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230103140447001.png)]

7.5-定时器中断定时测量速度

上面我们实现:在主函数周期读取计数器值然后计算速度但是如果函数加入其他内容这个周期时间就很难保证。

所以这节我们通过定时器周期读取计数器计算速度。复制一份工程开始搞!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rEMAWOUd-1675083353077)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230103153710421.png)]

我们先开启定时器、2ms进入一次定时器中断中断回调函数执行咱们的代码即可。

为什么充分利用单片机 我们使用TIM1

  1. 设置内部时钟源
  2. 使能自动重装载

image-20220809161601774

开启定义更新中断

image-20220809161647624

代码开启定时器1 中断

image-20220809162038626

  HAL_TIM_Base_Start_IT(&htim1);                //开启定时器1 中断

定时器回调函数中添加 速度计算内容

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EQNLqBD7-1675083353079)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230103151934131.png)]

/*******************
*  @brief  定时器回调函数
*  @param  
*  @return  
*
*******************/
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
	if(htim == &htim1)//htim1 500HZ  2ms 中断一次
	{
		TimerCount++;
		if(TimerCount %5 == 0)//每10ms执行一次
		{
			Encode1Count = (short)__HAL_TIM_GET_COUNTER(&htim4);
			Encode2Count = (short)__HAL_TIM_GET_COUNTER(&htim2);
			__HAL_TIM_SET_COUNTER(&htim4,0);
			__HAL_TIM_SET_COUNTER(&htim2,0);
			
			Motor1Speed = (float)Encode1Count*100/9.6/11/4;
			Motor2Speed = (float)Encode2Count*100/9.6/11/4;

			TimerCount=0;
		}
	}
}

把之前的变量定义放这里

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dguIW6Lm-1675083353079)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230103152057663.png)]

short Encode1Count = 0;
short Encode2Count = 0;
float Motor1Speed = 0.00;
float Motor2Speed = 0.00;
uint16_t TimerCount=0;

主函数就输出速度大小就可以了

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XIU2aQxi-1675083353079)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230103152344746.png)]

	printf("Motor1Speed:%.2f\r\n",Motor1Speed);
	printf("Motor2Speed:%.2f\r\n",Motor2Speed);

把变量需要声明一下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UNYaknvA-1675083353079)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230103152521123.png)]

extern float Motor1Speed ;
extern float Motor2Speed ;

然后打开串口助手

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iUlIJ8LF-1675083353080)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230103152735931.png)]

注:

根据电机和实际小车调整速度测量与占空比设置函数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ts9mcbQl-1675083353080)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230103160108176.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BfPtyrok-1675083353080)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230103160133008.png)]

第八章-PID-速度控制

8.1-速度控制探索

前面我们已经能够通过编码器测量出速度值下面我们来控制速度

我们先编写一个简单的控制方法

要求:讲转速控制再2.9-3.1转每秒

可以把中断里面不重要的输出注释掉

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tStx3Tbh-1675083353081)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230103163851923.png)]

	if(Motor1Speed>3.1) Motor1Pwm--;
	if(Motor1Speed<2.9) Motor1Pwm++;
	if(Motor2Speed>3.1) Motor2Pwm--;
	if(Motor2Speed<2.9) Motor2Pwm++;
	Motor_Set(Motor1Pwm,Motor2Pwm);
	printf("Motor1Speed:%.2f Motor1Pwm:%d\r\n",Motor1Speed,Motor1Pwm);
	printf("Motor2Speed:%.2f Motor2Pwm:%d\r\n",Motor2Speed,Motor2Pwm);
	
	HAL_Delay(100);

开始实验

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xtmxeShW-1675083353081)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230103164021564.png)]

现象就开始电机没有到达3转每秒PWM占空比逐渐增大电机逐渐达到要求转速、到达要求转速后我们增加阻力电机变慢阻力大小不边PWM占空比逐渐更大转速逐渐更大

这样我们就把转速控制到我们想要的范围但是我们并不满意、能够看出来控制的速度很慢给电机一些阻力电机至少要2-3秒能够调整过来这在一些场景是不允许的。

我们理想的控制效果是:在电机转速很慢的是时候能快速调整在电机一直转的不能达到要求时候能够更快速度调整

8.2-准备工作-匿名上位机曲线显示速度波形方便观察数据

为了方便观察电机速度数据我们通过上位机曲线显示一下。

这里我们使用的上位机是匿名上位机-大佬写的非常稳定功能也很多

我使用的版本是:匿名上位机V7.2.2.8版本推荐大家和我使用一样

匿名上位机官方下载链接:http://www.anotc.com/wiki/%E5%8C%BF%E5%90%8D%E4%BA%A7%E5%93%81%E8%B5%84%E6%96%99/%E8%B5%84%E6%96%99%E4%B8%8B%E8%BD%BD%E9%93%BE%E6%8E%A5%E6%B1%87%E6%80%BB

image-20220818223304266

我们要把STM32数据发送到匿名上位机就要满足匿名上位机的数据协议要求

在匿名上位机资料下载链接可以下载到协议介绍

  • 匿名上位机V7通信协议20210528发布:https://pan.baidu.com/s/1nGrIGWj6qr9DWOcGpKR51g 提取码:z8d1
  • CSDN 慕羽★大佬写的协议解析教程博客:https://blog.csdn.net/qq_44339029/article/details/106004997

1.先补充一下大小端模式

这是因为在计算机系统中我们是以字节为单位的每个地址单元都对应着一个字节一个字节为 8bit。但是在C语言中除了8bit的char之外还有16bit的short型32bit的long型(要看具体的编译器)另外对于位数大于8位的处理器例如16位或者32位的处理器由于寄存器宽度大于一个字节那么必然存在着一个如和将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。例如一个16bit的short型x在内存中的地址为0x0010x的值为0x1122那么0x11为高字节0x22为低字节。对于大端模式就将0x11放在低地址中即0x0010中0x22放在高地址中即0x0011中。

  • 所谓的大端模式(BE big-endian)是指数据的低位保存在内存的高地址中而数据的高位保存在内存的低地址中(低对高高对低);

  • 所谓的小端模式(LE little-endian)是指数据的低位保存在内存的低地址中而数据的高位保存在内存的高地址中(低对低高对高)。

    常见的单片机大小端模式:(1)KEIL C51中变量都是大端模式的而KEIL MDK中变量是小端模式的。(2)SDCC-C51是小端寻址AVRGCC 小端寻址.(3)PC小端大部分ARM是小端 (4)总起来说51单片机一般是大端模式32单片机一般是小端模式.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nEcYKf6g-1675083353081)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230103211719535.png)]

2.看一下上位机要求的协议

image-20220818230815447

灵活格式帧(用户自定义帧)

image-20220818231041314

前面我们好理解

0xAA:一个字节表示开始

0xFF:一个字节表示目标地址

0xF1:一个字节表示发送功能码

1-40:一个字节表示数据长度

数据内容有多个字节如何发送

因为串口每次发送一个字节但是数据可能是int16_t 16位的数据或者int32_t 32位数据每次发送16位数据,先发送数据低八位还是先发送数据高八位那?

匿名协议通信介绍给出:DATA 数据内容中的数据采用小端模式传送低字节在前高字节在后。

那么就要求比如我们在发送16位数据0x2314我们要先发送低字节0x14,然后发送高字节0x23

那么如何解析出低字节或者高字节就需要知道多字节数据在单片机里面是怎么存的因为STM32是小端存储所以低字节就在低位地址中高字节高位地址中。

如果使用32单片机 小端模式0x23高地址0x14在低地址所以我们要先发低地址再发高地址。

下面就是对16位数据或者32位数据的拆分

//需要发送16位,32位数据对数据拆分之后每次发送单个字节
//拆分过程:对变量dwTemp 去地址然后将其转化成char类型指针最后再取出指针所指向的内容
#define BYTE0(dwTemp)  (*(char *)(&dwTemp))
#define BYTE1(dwTemp)  (*((char *)(&dwTemp) + 1))
#define BYTE2(dwTemp)  (*((char *)(&dwTemp) + 2))
#define BYTE3(dwTemp)  (*((char *)(&dwTemp) + 3))

拆分后我们按照协议要求发送数据就可以了

image-20220819104451257

niming.c

#include "niming.h"
#include "main.h"
#include "usart.h"
uint8_t data_to_send[100];

//通过F1帧发送4个uint16类型的数据
void ANO_DT_Send_F1(uint16_t _a, uint16_t _b, uint16_t _c, uint16_t _d)
{
    uint8_t _cnt = 0;		//计数值
    uint8_t sumcheck = 0;  //和校验
    uint8_t addcheck = 0; //附加和校验
    uint8_t i = 0;
	data_to_send[_cnt++] = 0xAA;//帧头
    data_to_send[_cnt++] = 0xFF;//目标地址
    data_to_send[_cnt++] = 0xF1;//功能码
    data_to_send[_cnt++] = 8; //数据长度
	//单片机为小端模式-低地址存放低位数据匿名上位机要求先发低位数据所以先发低地址
	data_to_send[_cnt++] = BYTE0(_a);       
    data_to_send[_cnt++] = BYTE1(_a);
	
    data_to_send[_cnt++] = BYTE0(_b);
    data_to_send[_cnt++] = BYTE1(_b);
	
    data_to_send[_cnt++] = BYTE0(_c);
    data_to_send[_cnt++] = BYTE1(_c);
	
    data_to_send[_cnt++] = BYTE0(_d);
    data_to_send[_cnt++] = BYTE1(_d);
	 for ( i = 0; i < data_to_send[3]+4; i++)
    {
        sumcheck += data_to_send[i];//和校验
        addcheck += sumcheck;//附加校验
    }
    data_to_send[_cnt++] = sumcheck;
    data_to_send[_cnt++] = addcheck;
	HAL_UART_Transmit(&huart1,data_to_send,_cnt,0xFFFF);//这里是串口发送函数
}
//通过F2帧发送4个int16类型的数据
void ANO_DT_Send_F2(int16_t _a, int16_t _b, int16_t _c, int16_t _d)   //F2帧  4个  int16 参数
{
    uint8_t _cnt = 0;
    uint8_t sumcheck = 0; //和校验
    uint8_t addcheck = 0; //附加和校验
    uint8_t i=0;
   data_to_send[_cnt++] = 0xAA;
    data_to_send[_cnt++] = 0xFF;
    data_to_send[_cnt++] = 0xF2;
    data_to_send[_cnt++] = 8; //数据长度
	//单片机为小端模式-低地址存放低位数据匿名上位机要求先发低位数据所以先发低地址
    data_to_send[_cnt++] = BYTE0(_a);
    data_to_send[_cnt++] = BYTE1(_a);
	
    data_to_send[_cnt++] = BYTE0(_b);
    data_to_send[_cnt++] = BYTE1(_b);
	
    data_to_send[_cnt++] = BYTE0(_c);
    data_to_send[_cnt++] = BYTE1(_c);
	
    data_to_send[_cnt++] = BYTE0(_d);
    data_to_send[_cnt++] = BYTE1(_d);
	
	  for ( i = 0; i < data_to_send[3]+4; i++)
    {
        sumcheck += data_to_send[i];
        addcheck += sumcheck;
    }

    data_to_send[_cnt++] = sumcheck;
    data_to_send[_cnt++] = addcheck;
	
	HAL_UART_Transmit(&huart1,data_to_send,_cnt,0xFFFF);//这里是串口发送函数
}
//通过F3帧发送2个int16类型和1个int32类型的数据
void ANO_DT_Send_F3(int16_t _a, int16_t _b, int32_t _c )   //F3帧  2个  int16 参数   1个  int32  参数
{
    uint8_t _cnt = 0;
    uint8_t sumcheck = 0; //和校验
    uint8_t addcheck = 0; //附加和校验
    uint8_t i=0;
    data_to_send[_cnt++] = 0xAA;
    data_to_send[_cnt++] = 0xFF;
    data_to_send[_cnt++] = 0xF3;
    data_to_send[_cnt++] = 8; //数据长度
	//单片机为小端模式-低地址存放低位数据匿名上位机要求先发低位数据所以先发低地址
    data_to_send[_cnt++] = BYTE0(_a);
    data_to_send[_cnt++] = BYTE1(_a);
	
    data_to_send[_cnt++] = BYTE0(_b);
    data_to_send[_cnt++] = BYTE1(_b);
	
    data_to_send[_cnt++] = BYTE0(_c);
    data_to_send[_cnt++] = BYTE1(_c);
    data_to_send[_cnt++] = BYTE2(_c);
    data_to_send[_cnt++] = BYTE3(_c);
	
	  for ( i = 0; i < data_to_send[3]+4; i++)
    {
        sumcheck += data_to_send[i];
        addcheck += sumcheck;
    }

    data_to_send[_cnt++] = sumcheck;
    data_to_send[_cnt++] = addcheck;

	HAL_UART_Transmit(&huart1,data_to_send,_cnt,0xFFFF);//这里是串口发送函数
}

niming.h

#ifndef  NIMING_H
#define  NIMING_H
#include "main.h"
//需要发送16位,32位数据对数据拆分之后每次发送单个字节
//拆分过程:对变量dwTemp 去地址然后将其转化成char类型指针最后再取出指针所指向的内容
#define BYTE0(dwTemp)  (*(char *)(&dwTemp))
#define BYTE1(dwTemp)  (*((char *)(&dwTemp) + 1))
#define BYTE2(dwTemp)  (*((char *)(&dwTemp) + 2))
#define BYTE3(dwTemp)  (*((char *)(&dwTemp) + 3))


void ANO_DT_Send_F1(uint16_t, uint16_t _b, uint16_t _c, uint16_t _d);
void ANO_DT_Send_F2(int16_t _a, int16_t _b, int16_t _c, int16_t _d);
void ANO_DT_Send_F3(int16_t _a, int16_t _b, int32_t _c );

#endif 

添加测试代码

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GAgqw5BV-1675083353082)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230103214655364.png)]

	//电机速度等信息发送到上位机
	//注意上位机不支持浮点数所以要乘100
	ANO_DT_Send_F2(Motor1Speed*100, 3.0*100,Motor2Speed*100,3.0*100);

下面设置上位机-数据解析

image-20220819192140541

image-20220819191949193

image-20220820125740753

这个是控制效果并不理想后面我们介绍PID控制

image-20220820125338506

8.3-P I D 逐个参数理解

image-20220820173540806

加入的现在 过去 未来概念

p:现在

i:过去

d:未来

image-20220820140658286

那么我们就开始写PID

PID的结构体类型变量、里面成员都是浮点类型

先在pid.h声明一个结构体类型、声明.c中的函数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qDSiyz5r-1675083353084)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230104132732080.png)]

#ifndef __PID_H
#define __PID_H

//声明一个结构体类型
typedef struct 
{
	float target_val;//目标值
	float actual_val;//实际值
	float err;//当前偏差
	float err_last;//上次偏差
	float err_sum;//误差累计值
	float Kp,Ki,Kd;//比例积分微分系数
	
} tPid;

//声明函数
float P_realize(tPid * pid,float actual_val);
void PID_init(void);
float PI_realize(tPid * pid,float actual_val);
float PID_realize(tPid * pid,float actual_val);
#endif

然后在pid.c中定义结构体类型变量

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zsdoASI0-1675083353085)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230104132821912.png)]

#include "pid.h"

//定义一个结构体类型变量
tPid pidMotor1Speed;
//给结构体类型变量赋初值
void PID_init()
{
	pidMotor1Speed.actual_val=0.0;
	pidMotor1Speed.target_val=0.00;
	pidMotor1Speed.err=0.0;
	pidMotor1Speed.err_last=0.0;
	pidMotor1Speed.err_sum=0.0;
	pidMotor1Speed.Kp=0;
	pidMotor1Speed.Ki=0;
	pidMotor1Speed.Kd=0;
}
//比例p调节控制函数
float P_realize(tPid * pid,float actual_val)
{
	pid->actual_val = actual_val;//传递真实值
	pid->err = pid->target_val - pid->actual_val;//当前误差=目标值-真实值
	//比例控制调节   输出=Kp*当前误差
	pid->actual_val = pid->Kp*pid->err;
	return pid->actual_val;
}
//比例P 积分I 控制函数
float PI_realize(tPid * pid,float actual_val)
{
	pid->actual_val = actual_val;//传递真实值
	pid->err = pid->target_val - pid->actual_val;//当前误差=目标值-真实值
	pid->err_sum += pid->err;//误差累计值 = 当前误差累计和
	//使用PI控制 输出=Kp*当前误差+Ki*误差累计值
	pid->actual_val = pid->Kp*pid->err + pid->Ki*pid->err_sum;
	
	return pid->actual_val;
}
// PID控制函数
float PID_realize(tPid * pid,float actual_val)
{
	pid->actual_val = actual_val;//传递真实值
	pid->err = pid->target_val - pid->actual_val;当前误差=目标值-真实值
	pid->err_sum += pid->err;//误差累计值 = 当前误差累计和
	//使用PID控制 输出 = Kp*当前误差  +  Ki*误差累计值 + Kd*(当前误差-上次误差)
	pid->actual_val = pid->Kp*pid->err + pid->Ki*pid->err_sum + pid->Kd*(pid->err - pid->err_last);
	//保存上次误差: 这次误差赋值给上次误差
	pid->err_last = pid->err;
	
	return pid->actual_val;
}

然后在main中要调用PID_init();函数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mP2vbqDI-1675083353085)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230104132925536.png)]

  PID_init();

p调节函数函数只根据当前误差进行控制

//比例p调节控制函数
float P_realize(tPid * pid,float actual_val)
{
	pid->actual_val = actual_val;//传递真实值
	pid->err = pid->target_val - pid->actual_val;//目标值减去实际值等于误差值
	//比例控制调节
	pid->actual_val = pid->Kp*pid->err;
	return pid->actual_val;
}

主函数-可以估算当p=10 就有较好的响应速度

先看根据p比例控制的效果

image-20220820160006470

p调节 电机稳态后还是存在误差。

下面加入i 调节也就是加入历史误差

pi的控制函数

//比例P 积分I 控制函数
float PI_realize(tPid * pid,float actual_val)
{
	pid->actual_val = actual_val;//传递真实值
	pid->err = pid->target_val - pid->actual_val;//目标值减去实际值等于误差值
	pid->err_sum += pid->err;//误差累计求和
	//使用PI控制
	pid->actual_val = pid->Kp*pid->err + pid->Ki*pid->err_sum;
	
	return pid->actual_val;
}

因为实际值1.6的时候误差为1.4 上次偏差1.4和这次偏差1.4相加2.8 我们乘5 等于10点多就会有较好控制效果

这是pi 调节的控制效果

image-20220820161527982

下面是PID调节的

// PID控制函数
float PID_realize(tPid * pid,float actual_val)
{
	pid->actual_val = actual_val;//传递真实值
	pid->err = pid->target_val - pid->actual_val;//目标值减去实际值等于误差值
	pid->err_sum += pid->err;//误差累计求和
	//使用PID控制
	pid->actual_val = pid->Kp*pid->err + pid->Ki*pid->err_sum + pid->Kd*(pid->err - pid->err_last);
	//保存上次误差:最近一次 赋值给上次
	pid->err_last = pid->err;
	
	return pid->actual_val;
}

8.4-加入cJSON方便上位机调参

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DYKHbPkO-1675083353086)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230104152600270.png)]

调大堆栈

image-20220821142654358

软件开启中断

image-20220821141804356

开启接收中断

image-20220821142200828

 __HAL_UART_ENABLE_IT(&huart1,UART_IT_RXNE);	//开启串口1接收中断

中断回调函数

image-20220821142512762

uint8_t Usart1_ReadBuf[256];	//串口1 缓冲数组
uint8_t Usart1_ReadCount = 0;	//串口1 接收字节计数
  if(__HAL_UART_GET_FLAG(&huart1,UART_FLAG_RXNE))//判断huart1 是否读到字节
  {
		if(Usart1_ReadCount >= 255) Usart1_ReadCount = 0;
		HAL_UART_Receive(&huart1,&Usart1_ReadBuf[Usart1_ReadCount++],1,1000);
  }

编写函数用于判断串口是否发送完一帧数据

image-20220821143245954

extern uint8_t Usart1_ReadBuf[255];	//串口1 缓冲数组
extern uint8_t Usart1_ReadCount;	//串口1 接收字节计数

//判断否接收完一帧数据
uint8_t Usart_WaitReasFinish(void)
{
	static uint16_t Usart_LastReadCount = 0;//记录上次的计数值
	if(Usart1_ReadCount == 0)
	{
		Usart_LastReadCount = 0;
		return 1;//表示没有在接收数据
	}
	if(Usart1_ReadCount == Usart_LastReadCount)//如果这次计数值等于上次计数值
	{
		Usart1_ReadCount = 0;
		Usart_LastReadCount = 0;
		return 0;//已经接收完成了
	}
	Usart_LastReadCount = Usart1_ReadCount;
	return 2;//表示正在接受中
}

然后我们把cJSON库放入工程里面

下载cJSON新版

gtihub链接:https://github.com/DaveGamble/cJSON

百度网盘链接:https://pan.baidu.com/s/1AcNHtZuv5bokMQ2f6QoG7Q

提取码:a422

和添加其他文件一样加入工程然后指定路径

编写解析指令的函数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HV7KMdrV-1675083353088)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230104173819026.png)]

#include "cJSON.h"
#include <string.h>
   cJSON *cJsonData ,*cJsonVlaue;

	if(Usart_WaitReasFinish() == 0)//是否接收完毕
	{
		cJsonData  = cJSON_Parse((const char *)Usart1_ReadBuf);
		if(cJSON_GetObjectItem(cJsonData,"p") !=NULL)
		{
			cJsonVlaue = cJSON_GetObjectItem(cJsonData,"p");	
		    p = cJsonVlaue->valuedouble;
			pidMotor1Speed.Kp = p;
		}
		if(cJSON_GetObjectItem(cJsonData,"i") !=NULL)
		{
			cJsonVlaue = cJSON_GetObjectItem(cJsonData,"i");	
		    i = cJsonVlaue->valuedouble;
			pidMotor1Speed.Ki = i;
		}
		if(cJSON_GetObjectItem(cJsonData,"d") !=NULL)
		{
			cJsonVlaue = cJSON_GetObjectItem(cJsonData,"d");	
		    d = cJsonVlaue->valuedouble;
			pidMotor1Speed.Kd = d;
		}
		if(cJSON_GetObjectItem(cJsonData,"a") !=NULL)
		{
		
			cJsonVlaue = cJSON_GetObjectItem(cJsonData,"a");	
		    a = cJsonVlaue->valuedouble;
			pidMotor1Speed.target_val =a;
		}
		if(cJsonData != NULL){
		  cJSON_Delete(cJsonData);//释放空间、但是不能删除cJsonVlaue不然会 出现异常错误
		}
		memset(Usart1_ReadBuf,0,255);//清空接收buf注意这里不能使用strlen	
	}
	printf("P:%.3f  I:%.3f  D:%.3f A:%.3f\r\n",p,i,d,a);

测试发送cJSON数据就会解析收到数据

image-20220821151428650

然后我们赋值改变一个电机的PID参数和目标转速

然后我们通过串口发送命令就会改变PID的参数了

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-12qmRHqn-1675083353088)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20220821163121273.png)]

第九章-PID整定方法

9.1-调整合适的采样周期和PID调参方法

正如之前所说现在我们PID控制函数是在主函数中循环调用这样的调用方式并不能保证实时性不能保证周期得到调用

所以我们要把PID控制函数放到中断里面定时执行那么如何放到中断里面执行执行的周期是多少合适那?

image-20230104180752706

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WVBvsLou-1675083353089)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230104184558395.png)]

		if(TimerCount %10 ==0)//每20ms一次
		{
			Motor_Set(PID_realize(&pidMotor1Speed,Motor1Speed),0);
		    TimerCount=0;
		}
	}

烧录测试一下是否可以改变波形和调整参数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tWK2lNbv-1675083353089)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20220821180525104.png)]

借助上位机调节PID

  1. 调节P 把I=0、D=0先给正值或负值值测试P 正负、然后根据PID函数输入和输出估算P 大小然后I=0 D=0去测试调节一个较大值
  2. 调节I 把P等于前面的值 然后测试I给较大正值和负值 测试出I正负然后I从小值调节直到没有偏差存在
  3. 一般系统不使用D

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-suvlAlRF-1675083353089)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20220821205656468.png)]

然后当前系统特点 :I 对于系统更重要

下面我们调节I

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ySQRQZ84-1675083353090)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20220821211930247.png)]

给一个较小的i 发现 有一个大的超调我们就减少p 、减小一半p

下面是减少一半p 的效果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7OSkjxW4-1675083353090)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20220821212416979.png)]

这个效果还可以

整理双电机速度控制

首先我们的需要是控制两个电机那么这两个电机的特点不同他们的P I D 参数不同要控制不同的目标速度那么他们的目标值、实际值、偏差等都会不同所以我们的PID函数就要能够根据输入参数控制电机

我们增加tPid 类型函数的定义用于控制电机

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8tBFGnc2-1675083353090)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20220822142131051.png)]

tPid pid1_speed;//电机1的转速控制
tPid pid2_speed;//电机2的转速控制

//初始化PID参数
void PID_init()
{
	pid1_speed.actual_val=0.0;//初始化电机1转速PID 结构体
	pid1_speed.target_val=0.0;
	pid1_speed.err=0.0;
	pid1_speed.err_last=0.0;
	pid1_speed.err_sum=0.0;
	pid1_speed.Kp=0.0;
	pid1_speed.Ki=0.0;
	pid1_speed.Kd=0.0;
	
	pid2_speed.actual_val=0.0;//初始化电机2转速PID 结构体
	pid2_speed.target_val=0.0;
	pid2_speed.err=0.0;
	pid2_speed.err_last=0.0;
	pid2_speed.err_sum=0.0;
	pid2_speed.Kp=0.0;
	pid2_speed.Ki=0.0;
	pid2_speed.Kd=0.0;
}

更改一下PID函数这里我们使用结构体作为函数地址

访问因为是地址访问结构体变量要用->

float PID_realize(tPid * pid,float actual_val)
{
	pid->actual_val = actual_val;//传递真实值
	pid->err = pid->target_val - pid->actual_val;//目标值减去实际值等于误差值
	pid->err_sum += pid->err;//误差累计求和
	//使用PID控制
	pid->actual_val = pid->Kp*pid->err + pid->Ki*pid->err_sum + pid->Kd*(pid->err - pid->err_last);
	//保存上次误差:最近一次 赋值给上次
	pid->err_last = pid->err;
	
	return pid->actual_val;
}

更改主函数对PID函数的使用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q25C7HNz-1675083353090)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20220822143256375.png)]

然后可以分别调节电机1的参数和电机二的参数

把测试好的PID 参数分别写在PID_init里面

以上是入门篇

通过上面的学习与实操大家对:PWM、电机驱动、PID闭环控制、串口通信等有了一定掌握如果上面那个章节掌握不好一定要多看两遍视频多敲边代码还有疑惑可以百度查找或者留言问题。

后面的内容就是偏应用比较简单了。

下面应用篇

第10章-小车跑一跑

如何实现小车的前、后、左、右、停

控制电机速度就可以控制小车运动

如何控制电机速度?

改变小车速度PID的目标值然后定时器里面的PID控制函数就会计算输占空比然后控制小车。

代码如下:

定时器里面有电机控制我们这里还增加Motor_Set(PID_realize(&pidMotor1Speed,Motor1Speed),PID_realize(&pidMotor2Speed,Motor2Speed));

是为了提高实时性。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Xr5UxxWn-1675083353091)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230105131132508.png)]

/*******************
*  @brief  通过PID控制电机转速
*  @param  Motor1Speed:电机1 目标速度、Motor2Speed:电机2 目标速度
*  @return  无
*
*******************/
void motorPidSetSpeed(float Motor1SetSpeed,float Motor2SetSpeed)
{
	//改变电机PID参数的目标速度
	pidMotor1Speed.target_val = Motor1SetSpeed;
	pidMotor2Speed.target_val = Motor2SetSpeed;
	//根据PID计算 输出作用于电机
	Motor_Set(PID_realize(&pidMotor1Speed,Motor1Speed),PID_realize(&pidMotor2Speed,Motor2Speed));
}

很容易得到一下控制方式

//	motorPidSetSpeed(1,2);//向右转弯
//	motorPidSetSpeed(2,1);//向左转弯
//	motorPidSetSpeed(1,1);//前进
//	motorPidSetSpeed(-1,-1);//后退
//	motorPidSetSpeed(0,0);//停止

向左原地转弯、向原地转弯

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S0nU6pIT-1675083353091)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230105143208101.png)]

//	motorPidSetSpeed(-1,1);//右原地旋转
//	motorPidSetSpeed(1,-1);//左原地旋转

加速减速函数

//向前加速函数
void motorSpeedUp(void)
{
	static float MotorSetSpeedUp=0.5;//静态变量 函数结束 变量不会销毁
	if(MotorSetSpeedUp <= MAX_SPEED_UP) MotorSetSpeedUp +=0.5 ;  //如果没有超过最大值就增加0.5
	motorPidSetSpeed(MotorSetSpeedUp,MotorSetSpeedUp);//设置到电机
}
//向前减速函数
void motorSpeedCut(void)
{
	static float  MotorSetSpeedCut=3;//静态变量 函数结束 变量不会销毁
	if(MotorSetSpeedCut >=0.5) MotorSetSpeedCut-=0.5;//判断是否速度太小
	motorPidSetSpeed(MotorSetSpeedCut,MotorSetSpeedCut);//设置到电机
}

第11章-OLED速度与历程显示

这节我们显示两轮速度和里程

两轮速度很简单 之前已经计算过那么如何计算里程那?

里程:小车行驶的路程长度。

这里我们只要计算出每个单位时间小车行驶的长度然后一直相加就是这一段时间行驶的总里程长度了。

我们20ms计算一次,20ms走过了多少距离然后一直相加就是走的总距离就是里程。这里我们使用使用电机1 车轮1进行计算。你也可以电机1 和电机2相加然后除2。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CLTwJu57-1675083353091)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230105235911920.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4D1YwnAy-1675083353092)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230105221819413.png)]

		   /*里程数(cm) += 时间周期(s)*车轮转速(转/s)*车轮周长(cm)*/
		   Mileage += 0.02*Motor1Speed*22;

然后主函数我们通过OLED显示电机速度和小车里程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PUPcrMU8-1675083353092)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230105234102312.png)]

    sprintf((char *)OledString,"V1:%.2fV2:%.2f", Motor1Speed,Motor2Speed);//显示两个电机的速度
	OLED_ShowString(0,0,OledString,12);//这个是oled驱动里面的是显示位置的一个函数
	
	sprintf((char *)OledString,"Mileage:%.2f   ",Mileage);//显示里程数
	OLED_ShowString(0,1,OledString,12);//这个是oled驱动里面的是显示位置的一个函数

第12章-ADC采集电压和显示

什么是ADC

百度百科介绍:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QfXRSSGz-1675083353092)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221108210407873.png)]

我们知道万用表 电压表可以测量电池或者电路电压。那么我们是否可以通过单片机获得电压方便我 们监控电池状态

image-20221108210425866

如何测量我们的锂电池电压那?锂电池电压12V左右单片机ADC最大测量电压3.3V这里我们需要分 压电路分压。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5LPdBqk3-1675083353093)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221108211039622.png)]

然后我们通过电阻分压显而易见 ADC点的电压是VBAT_IN的 五分之一

  1. 软件初始化一下ADC 。
  2. 然后注意调长一点采样时间、这样精度才会更高一点。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7Loa44Pn-1675083353093)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221109232125714.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AXg30XbL-1675083353093)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221108213505630.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ty8ogwFT-1675083353093)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221108213614198.png)]

在adc.c文件添加ADC相关函数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WhNOC5xx-1675083353094)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230106230750593.png)]

/*******************
*  @brief  电池电压测量计算函数
*  @param  无
*  @return 小车电池电压
*
*******************/
float adcGetBatteryVoltage(void)
{
	HAL_ADC_Start(&hadc2);//启动ADC转化
	if(HAL_OK == HAL_ADC_PollForConversion(&hadc2,50))//等待转化完成、超时时间50ms
		return (float)HAL_ADC_GetValue(&hadc2)/4096*3.3*5;//计算电池电压
	return -1;
}

在main中调用显示函数显示电压

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UzNpGb7R-1675083353094)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230106230924860.png)]

	sprintf((char*)OledString, "U:%.2fV", adcGetBatteryVoltage());
	OLED_ShowString(0,2,OledString,12);//这个是oled驱动里面的是显示位置的一个函数

第13章-循迹功能

13.1-非PID循迹功能完成

先红外对管调试

我们这里学习一下如何实现循迹功能

如何才能让小车沿着黑线运动、要让小车感知到黑线的位置使用这种传感器就可以反馈黑线是否存在

image-20221110222632542

根据传感器特性我们检测红外对管DO引脚的电压就可以知道下面有没有黑线

DO 高电平->有黑线 小灯灭

DO低电平->没有黑线 小灯亮

这是好多地方对这个产品的说明

image-20221110153310630

然后我们组合上面的红外对管安装到小车上就可以知道小车是否偏离了黑线

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uQ9Zqjyb-1675083353094)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230107012917946.png)]

下面我们通过单片机读取红外对管DO口的电压就知道黑线在小车下面的位置了

STM32初始化

先看原理图需要初始化那些引脚

image-20221110224613242

OUT_1-PA5、OUT_2-PA7、OUT_3-PB0、OUT_4-PB1初始化为输入模式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w7pcGKgQ-1675083353095)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221110230859290.png)]

重新生成

然后我们在gpio.h 添加读取GPIO的宏使得程序更简洁

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-szA1ivzZ-1675083353095)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221110232405800.png)]

#define READ_HW_OUT_1   HAL_GPIO_ReadPin(HW_OUT_1_GPIO_Port,HW_OUT_1_Pin) //读取红外对管连接的GPIO电平
#define READ_HW_OUT_2   HAL_GPIO_ReadPin(HW_OUT_2_GPIO_Port,HW_OUT_2_Pin)
#define READ_HW_OUT_3   HAL_GPIO_ReadPin(HW_OUT_3_GPIO_Port,HW_OUT_3_Pin)
#define READ_HW_OUT_4   HAL_GPIO_ReadPin(HW_OUT_4_GPIO_Port,HW_OUT_4_Pin)

根据红外对管状态控制电机速度

注意:整个主函数不要加入延时这样实时性更高可以根据红外对管状态做出及时控制

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6mGg9sRU-1675083353095)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230107165249367.png)]

	if(READ_HW_OUT_1 == 0&&READ_HW_OUT_2 == 0&&READ_HW_OUT_3 == 0&&READ_HW_OUT_4 == 0 )
	{
		printf("应该前进\r\n");
		motorPidSetSpeed(1,1);//前运动
	}
	if(READ_HW_OUT_1 == 0&&READ_HW_OUT_2 == 1&&READ_HW_OUT_3 == 0&&READ_HW_OUT_4 == 0 )
	{
		printf("应该右转\r\n");
		motorPidSetSpeed(0.5,2);//右边运动
	}
	if(READ_HW_OUT_1 == 1&&READ_HW_OUT_2 == 0&&READ_HW_OUT_3 == 0&&READ_HW_OUT_4 == 0 )
	{
		printf("快速右转\r\n");
		motorPidSetSpeed(0.5,2.5);//快速右转
	}
	if(READ_HW_OUT_1 == 0&&READ_HW_OUT_2 == 0&&READ_HW_OUT_3 == 1&&READ_HW_OUT_4 == 0 )
	{
		printf("应该左转\r\n");
		motorPidSetSpeed(2,0.5);//左边运动
	}
	if(READ_HW_OUT_1 == 0&&READ_HW_OUT_2 == 0&&READ_HW_OUT_3 == 0&&READ_HW_OUT_4 == 1 )
	{
		printf("快速左转\r\n");
		motorPidSetSpeed(2.5,0.5);//快速左转
	}

然后测试

  1. 测试红外对管灵敏度放在有黑线的地上或者纸上然后把小车黑线比如放到最右边 及第一个红外对管观察红外对管小灯变化情况和串口输出情况如果小灯没有灭就调节红外对管灵敏度和室内灯光直到每个红外对管都可以感应到小灯。
  2. 然后在黑线上让小车循迹

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e7kqsA9x-1675083353095)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230107152753253.png)]

然后循迹功能完成

然后放到地上

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ToaXGSFo-1675083353098)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230107185627568.png)]

13.2-加入循迹PID

前面的代码我们对循迹是判断的几个状态然后PID控制电机不同速度但是我们可以使用红外对管状态作为PID控制的输入然后再控制电机。

PID的输入是红外对管状态我们设计 PID输入是红外对管的状态、然后输出一个速度值然后左右电机去加或者减这个值就可以完成根据红外对管输入对电机的差速控制

主函数添加的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IoPUOhho-1675083353099)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230107210726187.png)]

extern tPid pidHW_Tracking;//红外循迹的PID
uint8_t g_ucaHW_Read[4] = {0};//保存红外对管电平的数组
int8_t g_cThisState = 0;//这次状态
int8_t g_cLastState = 0; //上次状态
float g_fHW_PID_Out;//红外对管PID计算输出速度
float g_fHW_PID_Out1;//电机1的最后循迹PID控制速度
float g_fHW_PID_Out2;//电机2的最后循迹PID控制速度

然后实现PID循迹控制、注意为了更加快要减少没有必要的程序和优化判断、将没有必要的输出都注释掉

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ANO2czOA-1675083353100)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230107210854550.png)]

	g_ucaHW_Read[0] = READ_HW_OUT_1;//读取红外对管状态、这样相比于写在if里面更高效
	g_ucaHW_Read[1] = READ_HW_OUT_2;
	g_ucaHW_Read[2] = READ_HW_OUT_3;
	g_ucaHW_Read[3] = READ_HW_OUT_4;

	if(g_ucaHW_Read[0] == 0&&g_ucaHW_Read[1] == 0&&g_ucaHW_Read[2] == 0&&g_ucaHW_Read[3] == 0 )
	{
//		printf("应该前进\r\n");//注释掉更加高效减少无必要程序执行
		g_cThisState = 0;//前进
	}
	else if(g_ucaHW_Read[0] == 0&&g_ucaHW_Read[1] == 1&&g_ucaHW_Read[2] == 0&&g_ucaHW_Read[3] == 0 )//使用else if更加合理高效
	{
//		printf("应该右转\r\n");
		g_cThisState = -1;//应该右转
	}
	else if(g_ucaHW_Read[0] == 1&&g_ucaHW_Read[1] == 0&&g_ucaHW_Read[2] == 0&&g_ucaHW_Read[3] == 0 )
	{
//		printf("快速右转\r\n");
		g_cThisState = -2;//快速右转
	}
	else if(g_ucaHW_Read[0] == 1&&g_ucaHW_Read[1] == 1&&g_ucaHW_Read[2] == 0&&g_ucaHW_Read[3] == 0)
	{
//		printf("快速右转\r\n");
		g_cThisState = -3;//快速右转
	}
	else if(g_ucaHW_Read[0] == 0&&g_ucaHW_Read[1] == 0&&g_ucaHW_Read[2] == 1&&g_ucaHW_Read[3] == 0 )
	{
//		printf("应该左转\r\n");
		g_cThisState = 1;//应该左转	
	}
	else if(g_ucaHW_Read[0] == 0&&g_ucaHW_Read[1] == 0&&g_ucaHW_Read[2] == 0&&g_ucaHW_Read[3] == 1 )
	{
//		printf("快速左转\r\n");
		g_cThisState = 2;//快速左转
	}
	else if(g_ucaHW_Read[0] == 0&&g_ucaHW_Read[1] == 0&&g_ucaHW_Read[2] == 1&&g_ucaHW_Read[3] == 1)
	{
//	    printf("快速左转\r\n");
		g_cThisState = 3;//快速左转
	}
	g_fHW_PID_Out = PID_realize(&pidHW_Tracking,g_cThisState);//PID计算输出目标速度 这个速度会和基础速度加减

	g_fHW_PID_Out1 = 3 + g_fHW_PID_Out;//电机1速度=基础速度+循迹PID输出速度
	g_fHW_PID_Out2 = 3 - g_fHW_PID_Out;//电机1速度=基础速度-循迹PID输出速度
	if(g_fHW_PID_Out1 >5) g_fHW_PID_Out1 =5;//进行限幅 限幅速度在0-5之间
	if(g_fHW_PID_Out1 <0) g_fHW_PID_Out1 =0;
	if(g_fHW_PID_Out2 >5) g_fHW_PID_Out2 =5;
	if(g_fHW_PID_Out2 <0) g_fHW_PID_Out2 =0;
	if(g_cThisState != g_cLastState)//如何这次状态不等于上次状态、就进行改变目标速度和控制电机、在定时器中依旧定时控制电机
	{
		motorPidSetSpeed(g_fHW_PID_Out1,g_fHW_PID_Out2);//通过计算的速度控制电机
	}
	
	g_cLastState = g_cThisState;//保存上次红外对管状态

在pid.中

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oP5W5nBy-1675083353101)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230107211037921.png)]

tPid pidHW_Tracking;//红外循迹的PID

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8WwIWOb9-1675083353102)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230107211104780.png)]

	pidHW_Tracking.actual_val=0.0;
	pidHW_Tracking.target_val=0.00;//红外循迹PID 的目标值为0
	pidHW_Tracking.err=0.0;
	pidHW_Tracking.err_last=0.0;
	pidHW_Tracking.err_sum=0.0;
	pidHW_Tracking.Kp=-1.50;
	pidHW_Tracking.Ki=0;
	pidHW_Tracking.Kd=0.80;

然后就可以跑一下试试了。

可以改进的地方

  1. 红外对管影响差速转向也影响基础直行的速度 会有更好控制效果所以可以加入每种红外对管状态下对基础速度的影响。
  2. 红外对管的数量越多效果会越好。

第14章-手机遥控功能

我们要实现蓝牙遥控功能蓝牙遥控功能要使用:1.单片机的串口、2.蓝牙通信模块

所以我们先调试好:单片机的串口->蓝牙模块->接到一起联调

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kCzc0n6w-1675083353102)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230108094941496.png)]

14.1-电脑控制小车

完成功能:电脑连接单片机串口三 控制小车前进后退

先看原理图

通过原理图可以看出这是使用的串口3 在使用的时候注意把跳线帽跳线到蓝牙通信位置

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q01IbYjd-1675083353103)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221113145542144.png)]

打开初始化软件

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HxRxgOKa-1675083353104)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221113161440641.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aJZS5I7s-1675083353105)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221113162259965.png)]

生成代码

在main 定义全局变量

uint8_t g_ucUsart3ReceiveData;  //保存串口三接收的数据

开启串口三中断接收

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gqhU0yI3-1675083353106)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230108011852886.png)]

  HAL_UART_Receive_IT(&huart3,&g_ucUsart3ReceiveData,1);  //串口三接收数据

usart.c 重新实现串口中断回调函数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8zBEBZU4-1675083353106)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221113163755469.png)]

然后我们可以在中断回调函数里面中编写遥控命令控制逻辑了

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wyZ6dFxq-1675083353107)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230108021416560.png)]

//串口接收回调函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
	if( huart == &huart3)//判断中断源
	{
		if(g_ucUsart3ReceiveData == 'A') motorPidSetSpeed(1,1);//前运动
		if(g_ucUsart3ReceiveData == 'B') motorPidSetSpeed(-1,-1);//后运动
		if(g_ucUsart3ReceiveData == 'C') motorPidSetSpeed(0,0);//停止
		if(g_ucUsart3ReceiveData == 'D') motorPidSetSpeed(1,2);//右边运动	
		if(g_ucUsart3ReceiveData == 'E') motorPidSetSpeed(2,1);//左边运动
		if(g_ucUsart3ReceiveData == 'F') motorPidSpeedUp();//加速
		if(g_ucUsart3ReceiveData == 'G') motorPidSpeedCut();//减速
		
		HAL_UART_Receive_IT( &huart3, &g_ucUsart3ReceiveData, 1);//继续进行中断接收
	}
}

在usart.c中声明外部变量

extern uint8_t g_ucUsart3ReceiveData;  //保存串口三接收的数据

然后我们更改一下 主函数内容把PID红外循迹代码注释掉然后我们增加串口三的输出以便我们后面观察数据。

串口不定长输出

我们把转速等信息都可以显示在OLED上那么如何通过串口输出那?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eLCJBGUB-1675083353107)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230108115259681.png)]

	sprintf((char *)Usart3String,"V1:%.2fV2:%.2f\r\n",Motor1Speed,Motor2Speed);//显示两个电机转速 单位:转/秒
	HAL_UART_Transmit(&huart3,( uint8_t *)Usart3String,strlen(( const  char  *)Usart3String),50);//阻塞式发送通过串口三输出字符 strlen:计算字符串大小
	
	sprintf((char *)Usart3String,"Mileage%.2f\r\n",Mileage);//计算小车里程 单位cm
	HAL_UART_Transmit(&huart3,( uint8_t *)Usart3String,strlen(( const  char  *)Usart3String),50);//阻塞式发送通过串口三输出字符 strlen:计算字符串大小
	
	sprintf((char *)Usart3String,"U:%.2fV\r\n",adcGetBatteryVoltage());//显示电池电压
	HAL_UART_Transmit(&huart3,( uint8_t *)Usart3String,strlen(( const  char  *)Usart3String),50);//阻塞式发送通过串口三输出字符 strlen:计算字符串大小	
	HAL_Delay(5);

把之前PID初始化时候速度PID目标值改成0

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-19EO3bxr-1675083353108)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230108113408329.png)]

然后我们测试

硬件连接

我们现在使用USB-TTL连接串口三单片机串口三与电脑通信(底板不需要插入蓝牙)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LNSAxvuL-1675083353109)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230108121744588.png)]

然后打开软件

发送指令小车就会对应运动

在电脑串口软件查看输出信息、发送 指令控制小车运动

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aiSQY0FK-1675083353109)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230108115952589.png)]

14.2-手机蓝牙控制小车

功能:蓝牙遥控小车前进、后退、停止、左右转、加速、减速、手机显示数据

蓝牙模块和电脑通信

蓝牙模块-硬件介绍

使用:HC-05 主从机一体蓝牙串口透传模块

注意: 供电3.6V-6V(最好5V)

引脚顺序 VCC GND TXD RXD

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fqYKdDii-1675083353109)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221114115307410.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hrdFd1i9-1675083353110)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221114115640083.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EcxAOH5a-1675083353110)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221114115333609.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6ZUfSzNh-1675083353111)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221114115624973.png)]

先调试蓝牙模块-设置波特率

如图先把蓝牙模块通过USB-TTL模块相连接然后

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U11A05d7-1675083353112)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230108124818109.png)]

如果反复测试不能进入AT模式可能是新版蓝牙模块

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-58BZyGDj-1675083353112)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230108140057987.png)]

  1. 先连接好蓝牙模块的几根线然后按住蓝牙模块的按键

  2. 然后连接电脑然后几秒后蓝牙小灯慢闪说明进入AT模式

  3. 然后串口助手通过38400发送设置指令:AT+UART=115200,0,0

  4. 然后收到OK数据说明设置成功。

这个是设置波特率截图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o9T6gqvy-1675083353113)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221114120715660.png)]

  1. 然后重新拔插蓝牙模块(不用按按键)

  2. 在手机系统蓝牙配对HC-50 密码1234

  3. 串口助手设置波特率115200然后打开手机APP发送任意内容测试

这个是后面通信测试截图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S1iaDb9b-1675083353113)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221114125011810.png)]

  1. 设置按键-按照代码设置按下发送的数据

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HtvBWejZ-1675083353113)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230108125417623.png)]

蓝牙模块连接单片机

把蓝牙插入到底板、跳线帽选择蓝牙通信

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ISZtkML6-1675083353114)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221114131242210.png)]

按下不同按钮小车会对应控制

第A章-定位程序异常位置

参考连接:https://blog.csdn.net/supermuscleman/article/details/103929606

程序功能多 代码较多、可能会出现一些异常如何锁定程序异常位置非常重要

  1. 进入硬件调试-点击全速运行

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2OeHmOmO-1675083353114)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221116173624343.png)]

    通过LR的值确定当前堆栈使用的PSP或者MSP

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gbZLFr1X-1675083353115)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221116173713147.png)]

    然后在memory中定位到堆栈地址、然后就找到LR=08000F2D、PC=08000A02

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S3DTCPvA-1675083353115)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221116173741022.png)]

    Disassembly中,查找定位代码

    在反汇编窗口中点击右键选中show disassembly at address 。

    输入LR地址:为发生异常后调用的下一条指令的地址可看到发生异常的为

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tDLfe8s8-1675083353115)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221116173813090.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6mU1BmBR-1675083353115)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221116173845927.png)]

    输入PC地址:可以定位到发生异常的调用语句

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3ot2lmDW-1675083353116)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221116173915126.png)]

然后我们通过上面的方法就找到了异常位置

第15章-超声波避障功能

15.1-超声波测距

完成超声波测距功能、测量数据显示在OLED屏幕上

硬件介绍

使用:HC-SR04 超声波测距模块

注意: 绘制PCB注意四个引脚顺序 Vcc Trig Echo Gnd

供电3.3V-5V(最好5V)

image-20221114164857506

测距原理

image-20221114164920111

不同模式

image-20221114164940804

GPIO模式

image-20221114165014422

查看原理图

通过超声波的硬件介绍我们知道

MCU给Trig脚一个大于10us的高电平脉冲;然后读取Echo脚的高电平信号时间通过公式:距离 = T* 声速/2 就可以算出来距离。

Trig(PB5)我们配置为GPIO输出

Echo(PA6)我们配置GPIO输入功能

注:这里大家可能会问为什么不使用定时器捕获功能?

原因:

  1. 留一个定时器 方便以后扩展FreeRTOS使用
  2. 或者扩展其他舵机、电机等

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Gwjf587V-1675083353116)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221114165508600.png)]

image-20230108171955643

软件初始化

设置PB5输出模式然后起别名

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4lld2e3j-1675083353117)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221114195822786.png)]

设置PA6输入模式、

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-f06jCoz3-1675083353117)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221114195850027.png)]

然后生成代码

自己新建HC_SR04.c和HC_SR04.h 然后加入工程指定路径

防止溢出 把之前使用的数组调整大一些

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7WsOTk1i-1675083353118)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230108180914033.png)]

因为我们不适用定时器所以我们需要自己写一个us级延时函数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ggBNFXND-1675083353118)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221115164450164.png)]

/*******************
*  @brief  us级延时
*  @param  usdelay:要延时的us时间
*  @return  
*
*******************/
void HC_SR04_Delayus(uint32_t usdelay)
{
  __IO uint32_t Delay = usdelay * (SystemCoreClock / 8U / 1000U/1000);//SystemCoreClock:系统频率
  do
  {
    __NOP();
  }
  while (Delay --);
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JjmNLW9O-1675083353119)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221115165416779.png)]

/*******************
*  @brief  HC_SR04读取超声波距离
*  @param  无
*  @return 障碍物距离单位:cm (静止表面平整精度更高) 
*注意:两个HC_SR04_Read()函数调用的时间间隔要2ms及以上测量范围更大 精度更高 
*******************/
float HC_SR04_Read(void)
{
	uint32_t i = 0;
	float Distance;
	HAL_GPIO_WritePin(HC_SR04_Trig_GPIO_Port,HC_SR04_Trig_Pin,GPIO_PIN_SET);//输出15us高电平
	HC_SR04_Delayus(15);
	HAL_GPIO_WritePin(HC_SR04_Trig_GPIO_Port,HC_SR04_Trig_Pin,GPIO_PIN_RESET);//高电平输出结束设置为低电平
	
	while(HAL_GPIO_ReadPin(HC_SR04_Echo_GPIO_Port,HC_SR04_Echo_Pin) == GPIO_PIN_RESET)//等待回响高电平
	{
		i++;
		HC_SR04_Delayus(1);
		if(i>100000) return -1;//超时退出循环、防止程序卡死这里
	}
	i = 0;
	while(HAL_GPIO_ReadPin(HC_SR04_Echo_GPIO_Port,HC_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	;
}

然后就可以读距离了、连上蓝牙可以显示数据

注意:两个HC_SR04_Read()函数调用的时间间隔要2ms及以上测量范围更大 精度更高

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-82XiqPnf-1675083353119)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230108182646710.png)]

	sprintf((char *)Usart3String,"HC_SR04:%.2fcm\r\n",HC_SR04_Read());//显示超声波数据
	HAL_UART_Transmit(&huart3,( uint8_t *)Usart3String,strlen(( const  char  *)Usart3String),0xFFFF);//通过串口三输出字符 strlen:计算字符串大小	

然后把我们的手机蓝牙和小车蓝牙连接

手机显示

image-20230108182544348

15.2-避障逻辑编写

image-20221203220125504

然后我们编写循迹逻辑我们的逻辑时

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kRK3TZd3-1675083353120)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230109001446486.png)]

//**************避障功能********************//
//避障逻辑
	if(HC_SR04_Read() > 25)//前方无障碍物
	{
		motorPidSetSpeed(1,1);//前运动
		HAL_Delay(100);
	}
	else{	//前方有障碍物
		motorPidSetSpeed(-1,1);//右边运动 原地	
		HAL_Delay(500);
		if(HC_SR04_Read() > 25)//右边无障碍物
		{
			motorPidSetSpeed(1,1);//前运动
			HAL_Delay(100);
		}
		else{//右边有障碍物
			motorPidSetSpeed(1,-1);//左边运动 原地
			HAL_Delay(1000);
			if(HC_SR04_Read() >25)//左边无障碍物
			{
				 motorPidSetSpeed(1,1);//前运动
				HAL_Delay(100);
			}
			else{
				motorPidSetSpeed(-1,-1);//后运动
				HAL_Delay(1000);
				motorPidSetSpeed(-1,1);//右边运动
				HAL_Delay(50);
			}
		}
	}

第16章-超声波跟随功能

无PID跟随功能

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QyowVtOK-1675083353120)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221203220330848.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TJMTle77-1675083353120)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230109013951233.png)]

//超声波跟随
	if(HC_SR04_Read() > 25)
	{
		motorForward();//前进
		HAL_Delay(100);
	}
	if(HC_SR04_Read() < 20)
	{
		motorBackward();//后退
		HAL_Delay(100);
	}

PID跟随功能

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Pmkmm9z4-1675083353121)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230111101326946.png)]

在pid.c中定义一组PID参数

tPid pidFollow;    //定距离跟随PID
	pidFollow.actual_val=0.0;
	pidFollow.target_val=22.50;//定距离跟随 目标距离22.5cm
	pidFollow.err=0.0;
	pidFollow.err_last=0.0;
	pidFollow.err_sum=0.0;
	pidFollow.Kp=-0.5;//定距离跟随的Kp大小通过估算PID输入输出数据确定大概大小然后在调试
	pidFollow.Ki=-0.001;//Ki小一些
	pidFollow.Kd=0;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZbqVt30R-1675083353121)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230109141345691.png)]

//**********PID跟随功能***********//
    g_fHC_SR04_Read=HC_SR04_Read();//读取前方障碍物距离
	if(g_fHC_SR04_Read < 60){  //如果前60cm 有东西就启动跟随
		g_fFollow_PID_Out = PID_realize(&pidFollow,g_fHC_SR04_Read);//PID计算输出目标速度 这个速度会和基础速度加减
		if(g_fFollow_PID_Out > 6) g_fFollow_PID_Out = 6;//对输出速度限幅
		if(g_fFollow_PID_Out < -6) g_fFollow_PID_Out = -6;
		motorPidSetSpeed(g_fFollow_PID_Out,g_fFollow_PID_Out);//速度作用与电机上
	}
	else motorPidSetSpeed(0,0);//如果前面60cm 没有东西就停止
	HAL_Delay(10);//读取超声波传感器不能过快

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XkLKFUlB-1675083353121)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230109141645038.png)]

然后编译烧录测试 。

第17章-用6050走直线和转90度功能

17.1-6050姿态数据读取

STM32读取6050数据

先把我们的参考历程里面的6050文件复制过去

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qtiRtx2j-1675083353122)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230109185706496.png)]

添加文件

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6LOPciEE-1675083353122)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221116231529074.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5omNtDxH-1675083353122)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221117170521769.png)]

然后在魔术棒添加上面两个的路径不再截图了。

简单阅读代码知道 我们需要设置两个引脚这两个引脚使用模拟IIC读取6050数据

1.在mpuiic.c延时使用自己写的、引脚需要使用两个先设置推挽输出、高电平

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vQ1zNLz1-1675083353123)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221116232049948.png)]

void mpuiic_Delayus(uint32_t usdelay)
{
  __IO uint32_t Delay = usdelay * (SystemCoreClock /8U/1000U/1000);//SystemCoreClock:系统频率
  do
  {
    __NOP();//使用空指令延时、移植不同单片机注意__NOP(); 执行时间
  }
  while (Delay --);
}
  //MPU IIC 延时函数
void MPU_IIC_Delay(void)
{
	mpuiic_Delayus(2);
}

//初始化IIC
void MPU_IIC_Init(void)
{					     
//  GPIO_InitTypeDef  GPIO_InitStructure;
//	
//	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);//先使能外设IO PORTB时钟 
//		
//  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10|GPIO_Pin_11;	 // 端口配置
//  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; 		 //推挽输出
//  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;		 //IO口速度为50MHz
//  GPIO_Init(GPIOB, &GPIO_InitStructure);					 //根据设定参数初始化GPIO 
//	
//  GPIO_SetBits(GPIOB,GPIO_Pin_10|GPIO_Pin_11);						 //PB10,PB11 输出高	
 
}

使用软件初始化两个引脚

6050_SDA–PB9

6050_SCL–PB8

image-20221116232525601

PB8-输出模式-起始输出高电平

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DOkhQUMY-1675083353123)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221117172343943.png)]

PB9 输出模式 起始状态高电平

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cHOMWKGW-1675083353123)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221117172405281.png)]

生成代码

打开我们的代码是通过模拟IIC 读取6050数据的我们知道SDA是模拟IIC的数据线 所以通信过程中是再输入和输出模式中切换的但是我们的STM32CubeMX是设置的输出是在哪里更改的模式那?

是通过寄存器设置的在mpuiic.h可以看到

删除掉#include “sys.h”

把这个修改了

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BXPaKmLo-1675083353124)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221116233155491.png)]

2.在mpuiic.h更改相内容

改成下面这样的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JxCyVS1D-1675083353124)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230109224033045.png)]

//IO方向设置 设置SDA-PB9为输入或者输出
#define MPU_SDA_IN()  {GPIOB->CRH&=0XFFFFFF0F;GPIOB->CRH|=8<<4;}
#define MPU_SDA_OUT() {GPIOB->CRH&=0XFFFFFF0F;GPIOB->CRH|=3<<4;}
  1. 这是通过按位与后赋值 &=按位或后赋值 |=

  2. 设置端口配置高寄存器指定位。

先看一个例子

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fWcUxt0E-1675083353124)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230110094443036.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jIvBWA23-1675083353125)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230110000816677.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2BmM3LcL-1675083353125)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221117133006922.png)]

更改设置SDA与SCL电平的宏

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-db5CqYrS-1675083353126)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221117143405668.png)]

//IO操作函数	 
#define MPU_IIC_SCL_Hige    	HAL_GPIO_WritePin(SCL_6050_GPIO_Port,SCL_6050_Pin,GPIO_PIN_SET)//设置SCL高电平
#define MPU_IIC_SCL_Low      	HAL_GPIO_WritePin(SCL_6050_GPIO_Port,SCL_6050_Pin,GPIO_PIN_RESET)//设置SCL低电平
#define MPU_IIC_SDA_Hige        HAL_GPIO_WritePin(SDA_6050_GPIO_Port,SDA_6050_Pin,GPIO_PIN_SET)   //设置SDA高电平
#define MPU_IIC_SDA_Low         HAL_GPIO_WritePin(SDA_6050_GPIO_Port,SDA_6050_Pin,GPIO_PIN_RESET) //设置SDA低电平
  	 
#define MPU_READ_SDA            HAL_GPIO_ReadPin(SDA_6050_GPIO_Port,SDA_6050_Pin)    //读SDA电平

更改一下 mupiic.c文件

//把之前的MPU_IIC_SDA=1;	 换成 MPU_IIC_SDA_Hige;
//MPU_IIC_SDA=0;	 换成 MPU_IIC_SDA_Low;
//MPU_IIC_SCL=1;  换成 MPU_IIC_SCL_Hige;
//MPU_IIC_SCL=0;  换成 MPU_IIC_SCL_Low;
//

编译一下、删掉没有用的文件

把u8 替换为uint8_t

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7Uc7NkTz-1675083353126)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221117144658042.png)]

t替换一下u8 替换为uint8_t u32替换为uint32_t

可以一个文件一个文件的替换掉如果整个工程替换其他HAL库文件内容也可能改变了

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2Z0PwZ8C-1675083353126)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221117144918696.png)]

删除多余的库文件

注释掉

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nH4AMPJ5-1675083353127)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221117164409292.png)]

如果有其他的delay_ms 都替换为HAL_Delay

还有一个错误

		if((txd&0x80)>>7) MPU_IIC_SDA_Hige;
		else MPU_IIC_SDA_Low;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X8BObnrm-1675083353127)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221117164701076.png)]

编译一下 、没有错误和警告

然后在main.c中定义变量和添加同文件

float pitch,roll,yaw; // 俯仰角 横滚角 航向角

#include "mpu6050.h"
#include "inv_mpu.h"
#include "inv_mpu_dmp_motion_driver.h" 

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ixt0vU6o-1675083353128)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221117165511717.png)]

替换inv_mpu.h的

#include "stm32f1xx_it.h"

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nRUjQi0e-1675083353128)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221117165920050.png)]

初始化6050

  HAL_Delay(500);//延时0.5秒 6050上电稳定后初始化
  MPU_Init(); //初始化MPU6050
  while(MPU_Init()!=0);
  while(mpu_dmp_init()!=0);

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g1X36pA3-1675083353128)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230109213955121.png)]

我们通过下面的代码获得数据

   	sprintf((char *)Usart3String,"pitch:%.2f roll:%.2f yaw:%.2f\r\n",pitch,roll,yaw);//显示6050数据
	HAL_UART_Transmit(&huart3,( uint8_t *)Usart3String,strlen(( const  char  *)Usart3String),0xFFFF);//通过串口三输出字符 strlen:计算字符串大小	
   
   //mpu_dmp_get_data(&pitch,&roll,&yaw);//返回值:0,DMP成功解出欧拉角
    while(mpu_dmp_get_data(&pitch,&roll,&yaw)!=0){}  //这个可以解决经常读不出数据的问题

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Isig35C2-1675083353129)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230109214556197.png)]

然后我看一下 这个Usart3String 现在发送的大概多大的?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-f0xcPGT4-1675083353129)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230109215719786.png)]

uint8_t OledString[50];
uint8_t Usart3String[50];

编译、烧录、然后就可以连接手机蓝牙在蓝牙软件查看数据了

image-20230109225000072

17.2-利用6050直线和90度(有代码)

为什么小车还是不能走直线

为什么两个电机转速一样不能走非常正直线如何控制小车转弯90度。

当然我们可以开环控制但是控制效果可能不好受外界影响比较大。

如果我们使用闭环控制就要使用一个传感器来获得现在小车角度。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GKM43aXI-1675083353129)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221203222808445.png)]

https://www.bilibili.com/video/BV1UV4y1p7Hd/?spm_id_from=333.337.search-card.all.click

走直线(控制朝一个方向运动)

在pid.c中定义一个姿态控制使用的PID

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ADHO0dVD-1675083353130)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230110160756280.png)]

tPid pidMPU6050YawMovement;  //利用6050偏航角 进行姿态控制的PID

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0FjpH6Yk-1675083353130)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230110163152585.png)]

	pidMPU6050YawMovement.actual_val=0.0;
	pidMPU6050YawMovement.target_val=0.00;//设定姿态目标值
	pidMPU6050YawMovement.err=0.0;
	pidMPU6050YawMovement.err_last=0.0;
	pidMPU6050YawMovement.err_sum=0.0;
	pidMPU6050YawMovement.Kp=2;//定距离跟随的Kp大小通过估算PID输入输出数据确定大概大小然后在调试
	pidMPU6050YawMovement.Ki=0;
	pidMPU6050YawMovement.Kd=0;

仿照之前红外循迹代码编写姿态控制函数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tdwgSSF5-1675083353130)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230110163514918.png)]

float  g_fMPU6050YawMovePidOut = 0.00f; //姿态PID运算输出
float  g_fMPU6050YawMovePidOut1 = 0.00f; //第一个电机控制输出
float  g_fMPU6050YawMovePidOut2 = 0.00f; //第一个电机控制输出

走直线程序如下(因为上电初始化时候航向角是0、而且pidMPU6050YawMovementPID结构体的目标值target_val 也是0)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wUpkmQTI-1675083353131)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230110165825685.png)]

//*************MPU6050航向角 PID转向控制*****************//

   	sprintf((char *)Usart3String,"pitch:%.2f roll:%.2f yaw:%.2f\r\n",pitch,roll,yaw);//显示6050数据 俯仰角 横滚角 航向角
	HAL_UART_Transmit(&huart3,( uint8_t *)Usart3String,strlen(( const  char  *)Usart3String),0xFFFF);//通过串口三输出字符 strlen:计算字符串大小	
   
   //mpu_dmp_get_data(&pitch,&roll,&yaw);//返回值:0,DMP成功解出欧拉角
    while(mpu_dmp_get_data(&pitch,&roll,&yaw)!=0){}  //这个可以解决经常读不出数据的问题
	
	
	g_fMPU6050YawMovePidOut = PID_realize(&pidMPU6050YawMovement,yaw);//PID计算输出目标速度 这个速度会和基础速度加减

	g_fMPU6050YawMovePidOut1 = 1.5 + g_fMPU6050YawMovePidOut;//基础速度加减PID输出速度
	g_fMPU6050YawMovePidOut2 = 1.5 - g_fMPU6050YawMovePidOut;
	if(g_fMPU6050YawMovePidOut1 >3.5) g_fMPU6050YawMovePidOut1 =3.5;//进行限幅
	if(g_fMPU6050YawMovePidOut1 <0) g_fMPU6050YawMovePidOut1 =0;
	if(g_fMPU6050YawMovePidOut2 >3.5) g_fMPU6050YawMovePidOut2 =3.5;
	if(g_fMPU6050YawMovePidOut2 <0) g_fMPU6050YawMovePidOut2 =0;
	motorPidSetSpeed(g_fMPU6050YawMovePidOut1,g_fMPU6050YawMovePidOut2);

然后调节PID参数

顺序 先确定P 正负 然后P大小

然后D正负 然后D大小

最后调节的参数如下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DKAKCE3C-1675083353131)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230110170011105.png)]

	pidMPU6050YawMovement.Kp=0.02;//6050航向角PID运动控制 
	pidMPU6050YawMovement.Ki=0;
	pidMPU6050YawMovement.Kd=0.1;

然后我们把小车放在地上就可以完成一直朝着初始方向前进如果往侧面推也会马上矫正。

转弯90度功能(控制转弯角度)

然后我们增加一下如何旋转90度程序

在串口接收回调函数表姿态PID的目标值

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pfllo7FK-1675083353132)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230110184914912.png)]

extern tPid pidMPU6050YawMovement;  //利用6050偏航角 进行姿态控制的PID

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AN5115TH-1675083353132)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230110185214565.png)]

		if(g_ucUsart3ReceiveData == 'H')//转向90度
		{				
			if(pidMPU6050YawMovement.target_val <= 180)pidMPU6050YawMovement.target_val += 90;//目标值
		}
		if(g_ucUsart3ReceiveData == 'I')//转回90度
		{				
			if(pidMPU6050YawMovement.target_val >= -180)pidMPU6050YawMovement.target_val -= 90;//目标值
        }	

然后我们的蓝牙APP增加两个发送按钮的设置

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-t8h0SDdV-1675083353133)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221117224841058.png)]

现象 是上电小车向初始方向直行如果推小车车头方向小车能够立马矫正。

然后连接蓝牙发送转90度 小车会转90度按下 转回90度小车回转回。

第18章-综合以上功能

18-按键和app按钮切换功能

根据上面介绍我们的模式可以有:

**OLED显示模式: 速度、里程、电压、超声波数据、MPU6050俯仰角、横滚角、航向角 数据显示在OLED上和通过串口发送蓝牙APP **

PID循迹模式:红外对管PID循迹

手机遥控普通运动模式:遥控前、后、左、右加速运动

超声波避障模式

PID跟随模式:超声波PID定距离跟随

手机遥控角度闭环模式:MPU6050角度PID控制

可以设置标志位通过按键改变标志位以实现功能切换。

定义一个全局变量

uint8_t g_ucMode = 0; 
//小车运动模式标志位 0:显示功能、1:PID循迹模式、2:手机遥控普通运动模式、3.超声波避障模式、4:PID跟随模式、5:遥控角度闭环

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vBhwFRLJ-1675083353133)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230111120620970.png)]

uint8_t g_ucMode = 0; //小车运动模式标志位

在gpio.h声明一下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bnHysAI8-1675083353134)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230111120725049.png)]

extern uint8_t g_ucMode ; //小车运动模式标志位

按键中断回调函数里面补充按下按键后的处理

先不进行消抖如果后面KEY1 KEY2效果不好再消抖

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rl6JVWv4-1675083353134)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230111123626386.png)]

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
	if(GPIO_Pin == KEY1_Pin) //判断一下那个引脚触发中断
	{
		//这里编写触发中断后要执行的程序
		if(g_ucMode == 5) g_ucMode = 1;//g_ucMode模式是0 1 2 3 4 5 
		else
		{
			g_ucMode+=1;
		}
		HAL_GPIO_TogglePin(LED_GPIO_Port,LED_Pin);
	}
	if(GPIO_Pin == KEY2_Pin) //判断一下那个引脚触发中断
	{
		//这里编写触发中断后要执行的程序
		g_ucMode=0;
		HAL_GPIO_TogglePin(LED_GPIO_Port,LED_Pin);
	}
}

然后主函数显示当前处于的模式

然后判断当前模式 执行不同代码

方法:一个功能一个功能的添加代码添加好一个调试测试一下然后再添加下一个

下面这个就是我们主函数的代码。

	sprintf((char *)OledString," g_ucMode:%d",g_ucMode);//显示g_ucMode 当前模式
	OLED_ShowString(0,6,OledString,12);	//显示在OLED上
	
	sprintf((char *)Usart3String," g_ucMode:%d",g_ucMode);//蓝牙APP显示
	HAL_UART_Transmit(&huart3,( uint8_t *)Usart3String,strlen(( const  char  *)Usart3String),50);//阻塞式发送通过串口三输出字符 strlen:计算字符串大小
	
	if(g_ucMode == 0)
	{
	//0LED显示功能
		sprintf((char*)OledString, "V1:%.2fV2:%.2f", Motor1Speed,Motor2Speed);//显示速度
		OLED_ShowString(0,0,OledString,12);//这个是oled驱动里面的是显示位置的一个函数
		
		sprintf((char*)OledString, "Mileage:%.2f", Mileage);//显示里程
		OLED_ShowString(0,1,OledString,12);//这个是oled驱动里面的是显示位置的一个函数
		
		sprintf((char*)OledString, "U:%.2fV", adcGetBatteryVoltage());//显示电池电压
		OLED_ShowString(0,2,OledString,12);//这个是oled驱动里面的是显示位置的一个函数
		
		sprintf((char *)OledString,"HC_SR04:%.2fcm\r\n",HC_SR04_Read());//显示超声波数据
		OLED_ShowString(0,3,OledString,12);//这个是oled驱动里面的是显示位置的一个函数
		
		sprintf((char *)OledString,"p:%.2f r:%.2f \r\n",pitch,roll);//显示6050数据 俯仰角 横滚角
		OLED_ShowString(0,4,OledString,12);//这个是oled驱动里面的是显示位置的一个函数
		
		sprintf((char *)OledString,"y:%.2f  \r\n",yaw);//显示6050数据  航向角
		OLED_ShowString(0,5,OledString,12);//这个是oled驱动里面的是显示位置的一个函数
		
	//蓝牙APP显示
		sprintf((char*)Usart3String, "V1:%.2fV2:%.2f", Motor1Speed,Motor2Speed);//显示速度
		HAL_UART_Transmit(&huart3,( uint8_t *)Usart3String,strlen(( const  char  *)Usart3String),50);//阻塞式发送通过串口三输出字符 strlen:计算字符串大小
		//阻塞方式发送可以保证数据发送完毕中断发送不一定可以保证数据已经发送完毕才启动下一次发送
		sprintf((char*)Usart3String, "Mileage:%.2f", Mileage);//显示里程
		HAL_UART_Transmit(&huart3,( uint8_t *)Usart3String,strlen(( const  char  *)Usart3String),50);//阻塞式发送通过串口三输出字符 strlen:计算字符串大小
		
		sprintf((char*)Usart3String, "U:%.2fV", adcGetBatteryVoltage());//显示电池电压
		HAL_UART_Transmit(&huart3,( uint8_t *)Usart3String,strlen(( const  char  *)Usart3String),50);//阻塞式发送通过串口三输出字符 strlen:计算字符串大小
		
		sprintf((char *)Usart3String,"HC_SR04:%.2fcm\r\n",HC_SR04_Read());//显示超声波数据
		HAL_UART_Transmit(&huart3,( uint8_t *)Usart3String,strlen(( const  char  *)Usart3String),50);//阻塞式发送通过串口三输出字符 strlen:计算字符串大小
		
		sprintf((char *)Usart3String,"p:%.2f r:%.2f \r\n",pitch,roll);//显示6050数据 俯仰角 横滚角
		HAL_UART_Transmit(&huart3,( uint8_t *)Usart3String,strlen(( const  char  *)Usart3String),50);//阻塞式发送通过串口三输出字符 strlen:计算字符串大小
		
		sprintf((char *)Usart3String,"y:%.2f  \r\n",yaw);//显示6050数据  航向角
		HAL_UART_Transmit(&huart3,( uint8_t *)Usart3String,strlen(( const  char  *)Usart3String),50);//阻塞式发送通过串口三输出字符 strlen:计算字符串大小
	
		//获得6050数据
		while(mpu_dmp_get_data(&pitch,&roll,&yaw)!=0){}  //这个可以解决经常读不出数据的问题
		
		//显示模式电机停转
		motorPidSetSpeed(0,0);
	}
	if(g_ucMode == 1)
	{
	///****    红外PID循迹功能******************/
	g_ucaHW_Read[0] = READ_HW_OUT_1;//读取红外对管状态、这样相比于写在if里面更高效
	g_ucaHW_Read[1] = READ_HW_OUT_2;
	g_ucaHW_Read[2] = READ_HW_OUT_3;
	g_ucaHW_Read[3] = READ_HW_OUT_4;

	if(g_ucaHW_Read[0] == 0&&g_ucaHW_Read[1] == 0&&g_ucaHW_Read[2] == 0&&g_ucaHW_Read[3] == 0 )
	{
//		printf("应该前进\r\n");//注释掉更加高效减少无必要程序执行
		g_cThisState = 0;//前进
	}
	else if(g_ucaHW_Read[0] == 0&&g_ucaHW_Read[1] == 1&&g_ucaHW_Read[2] == 0&&g_ucaHW_Read[3] == 0 )//使用else if更加合理高效
	{
//		printf("应该右转\r\n");
		g_cThisState = -1;//应该右转
	}
	else if(g_ucaHW_Read[0] == 1&&g_ucaHW_Read[1] == 0&&g_ucaHW_Read[2] == 0&&g_ucaHW_Read[3] == 0 )
	{
//		printf("快速右转\r\n");
		g_cThisState = -2;//快速右转
	}
	else if(g_ucaHW_Read[0] == 1&&g_ucaHW_Read[1] == 1&&g_ucaHW_Read[2] == 0&&g_ucaHW_Read[3] == 0)
	{
//		printf("快速右转\r\n");
		g_cThisState = -3;//快速右转
	}
	else if(g_ucaHW_Read[0] == 0&&g_ucaHW_Read[1] == 0&&g_ucaHW_Read[2] == 1&&g_ucaHW_Read[3] == 0 )
	{
//		printf("应该左转\r\n");
		g_cThisState = 1;//应该左转	
	}
	else if(g_ucaHW_Read[0] == 0&&g_ucaHW_Read[1] == 0&&g_ucaHW_Read[2] == 0&&g_ucaHW_Read[3] == 1 )
	{
//		printf("快速左转\r\n");
		g_cThisState = 2;//快速左转
	}
	else if(g_ucaHW_Read[0] == 0&&g_ucaHW_Read[1] == 0&&g_ucaHW_Read[2] == 1&&g_ucaHW_Read[3] == 1)
	{
//	    printf("快速左转\r\n");
		g_cThisState = 3;//快速左转
	}
	g_fHW_PID_Out = PID_realize(&pidHW_Tracking,g_cThisState);//PID计算输出目标速度 这个速度会和基础速度加减

	g_fHW_PID_Out1 = 3 + g_fHW_PID_Out;//电机1速度=基础速度+循迹PID输出速度
	g_fHW_PID_Out2 = 3 - g_fHW_PID_Out;//电机1速度=基础速度-循迹PID输出速度
	if(g_fHW_PID_Out1 >5) g_fHW_PID_Out1 =5;//进行限幅 限幅速度在0-5之间
	if(g_fHW_PID_Out1 <0) g_fHW_PID_Out1 =0;
	if(g_fHW_PID_Out2 >5) g_fHW_PID_Out2 =5;
	if(g_fHW_PID_Out2 <0) g_fHW_PID_Out2 =0;
	if(g_cThisState != g_cLastState)//如何这次状态不等于上次状态、就进行改变目标速度和控制电机、在定时器中依旧定时控制电机
	{
		motorPidSetSpeed(g_fHW_PID_Out1,g_fHW_PID_Out2);//通过计算的速度控制电机
	}
	
	g_cLastState = g_cThisState;//保存上次红外对管状态	

	}
	if(g_ucMode == 2)
	{
		//***************遥控模式***********************//
		//遥控模式的控制在串口三的中断里面
	}
	if(g_ucMode == 3)
	{
		//******超声波避障模式*********************//
避障逻辑
		if(HC_SR04_Read() > 25)//前方无障碍物
		{
			motorPidSetSpeed(1,1);//前运动
			HAL_Delay(100);
		}
		else{	//前方有障碍物
			motorPidSetSpeed(-1,1);//右边运动 原地	
			HAL_Delay(500);
			if(HC_SR04_Read() > 25)//右边无障碍物
			{
				motorPidSetSpeed(1,1);//前运动
				HAL_Delay(100);
			}
			else{//右边有障碍物
				motorPidSetSpeed(1,-1);//左边运动 原地
				HAL_Delay(1000);
				if(HC_SR04_Read() >25)//左边无障碍物
				{
					 motorPidSetSpeed(1,1);//前运动
					HAL_Delay(100);
				}
				else{
					motorPidSetSpeed(-1,-1);//后运动
					HAL_Delay(1000);
					motorPidSetSpeed(-1,1);//右边运动
					HAL_Delay(50);
				}
			}
		}
	}
	if(g_ucMode == 4)
	{
	//**********PID跟随功能***********//
		g_fHC_SR04_Read=HC_SR04_Read();//读取前方障碍物距离
		if(g_fHC_SR04_Read < 60){  //如果前60cm 有东西就启动跟随
			g_fFollow_PID_Out = PID_realize(&pidFollow,g_fHC_SR04_Read);//PID计算输出目标速度 这个速度会和基础速度加减
			if(g_fFollow_PID_Out > 6) g_fFollow_PID_Out = 6;//对输出速度限幅
			if(g_fFollow_PID_Out < -6) g_fFollow_PID_Out = -6;
			motorPidSetSpeed(g_fFollow_PID_Out,g_fFollow_PID_Out);//速度作用与电机上
		}
		else motorPidSetSpeed(0,0);//如果前面60cm 没有东西就停止
		HAL_Delay(10);//读取超声波传感器不能过快
	}
	if(g_ucMode == 5)
	{
	//*************MPU6050航向角 PID转向控制*****************//

		sprintf((char *)Usart3String,"pitch:%.2f roll:%.2f yaw:%.2f\r\n",pitch,roll,yaw);//显示6050数据 俯仰角 横滚角 航向角
		HAL_UART_Transmit(&huart3,( uint8_t *)Usart3String,strlen(( const  char  *)Usart3String),0xFFFF);//通过串口三输出字符 strlen:计算字符串大小	
	   
	   //mpu_dmp_get_data(&pitch,&roll,&yaw);//返回值:0,DMP成功解出欧拉角
		while(mpu_dmp_get_data(&pitch,&roll,&yaw)!=0){}  //这个可以解决经常读不出数据的问题
		
		
		g_fMPU6050YawMovePidOut = PID_realize(&pidMPU6050YawMovement,yaw);//PID计算输出目标速度 这个速度会和基础速度加减

		g_fMPU6050YawMovePidOut1 = 1.5 + g_fMPU6050YawMovePidOut;//基础速度加减PID输出速度
		g_fMPU6050YawMovePidOut2 = 1.5 - g_fMPU6050YawMovePidOut;
		if(g_fMPU6050YawMovePidOut1 >3.5) g_fMPU6050YawMovePidOut1 =3.5;//进行限幅
		if(g_fMPU6050YawMovePidOut1 <0) g_fMPU6050YawMovePidOut1 =0;
		if(g_fMPU6050YawMovePidOut2 >3.5) g_fMPU6050YawMovePidOut2 =3.5;
		if(g_fMPU6050YawMovePidOut2 <0) g_fMPU6050YawMovePidOut2 =0;
		motorPidSetSpeed(g_fMPU6050YawMovePidOut1,g_fMPU6050YawMovePidOut2);
	
	}

可以测试上面的代码 然后没有问题后我们添加一个通过蓝牙APP按钮切换模式代码

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GK6PBtTL-1675083353134)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230111135256610.png)]

		if(g_ucUsart3ReceiveData == 'J') //改变模式
		{
			if(g_ucMode == 5) g_ucMode = 1;//g_ucMode模式是0 1 2 3 4 5 
			else
			{
				g_ucMode+=1;
			}
		}
		if(g_ucUsart3ReceiveData == 'K') g_ucMode=0;//设置为显示模式

然后对应APP也要添加 按钮设置

image-20230111135837135

我们

按键没有消抖效果不好我们消抖一下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CXVpEaP3-1675083353135)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230111161916819.png)]

我们增加了 HAL延时和再次判断电平

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
	if(GPIO_Pin == KEY1_Pin) //判断一下那个引脚触发中断
	{
		HAL_Delay(10);//延时消抖 主要
		if(HAL_GPIO_ReadPin(KEY1_GPIO_Port, KEY1_Pin) == GPIO_PIN_SET)//判断KEY1引脚仍为高电平
		{
			//这里编写触发中断后要执行的程序
			if(g_ucMode == 5) g_ucMode = 1;//g_ucMode模式是0 1 2 3 4 5 
			else
			{
				g_ucMode+=1;
			}
			HAL_GPIO_TogglePin(LED_GPIO_Port,LED_Pin);
		}
	}
	if(GPIO_Pin == KEY2_Pin) //判断一下那个引脚触发中断
	{
		HAL_Delay(10);//延时消抖
		if(HAL_GPIO_ReadPin(KEY2_GPIO_Port, KEY2_Pin) == GPIO_PIN_RESET)//判断KEY2引脚仍为低电平
		{
			//这里编写触发中断后要执行的程序
			g_ucMode=0;
			HAL_GPIO_TogglePin(LED_GPIO_Port,LED_Pin);
		}
	}
}

但是测试不能执行中断程序异常卡死了

原因是HAL_Delay使用的是sysTick 中断优先级在软件初始化是默认最低的比外部中断优先级低所以HAL_Delay不能在外部中断服务函数中调用。

所以我们可以通过提高sysTick 中断的优先级提高的比HAL_Delay高。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gWf0yJty-1675083353135)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230111205153163.png)]

然后我们提高至 如下图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CiPqZrwO-1675083353136)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20230111184656982.png)]

然后编译烧录测试按键是否更加稳定。

以上是应用篇

下面19是扩展

第22章-小车如何查找异常问题

前面我们已经移植 好了OLED程序下面我们就直接使用OLED,我们在定时器刷新OLED我们50ms执行一次

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8IIQcoaW-1675083353136)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221108162446208.png)]

应该这样写

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BVHWDbf2-1675083353136)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221108190207208.png)]

显示行进路程累计

但是发现计算不准确怀疑中断时间不准确如何确定中断是否按时到达那?

  1. 使用sysTick统计时间

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bl6GSFDT-1675083353137)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221108193053984.png)]

  1. 在要判断时间的地方反转GPIO通过示波器观察 是否按时中断

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HHmUvIFx-1675083353137)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221108194432504.png)]

然后示波器测量引脚PC13

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-m26i8ty8-1675083353137)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221108194541853.png)]

所以调试发现

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5p2gBauB-1675083353138)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221108194818938.png)]

所以更新显示应该放到主函数执行然后

我们的中断函数这样写

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1HO1l4Vs-1675083353138)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221108203833681.png)]

	    if(EncoderTimer_Count%10 == 0)  //每20ms 进入一次
		{
		
			//进行PID计算、然后作用于电机
			Motor_Set(PID_realize(&pid1_speed,Motor1_Speed),PID_realize(&pid2_speed,Motor2_Speed));
			//向上位机发送数据
			ANO_DT_Send_F2(Motor1_Speed*100, 3.0*100,Motor2_Speed*100,3.0*100);
			
			HAL_GPIO_TogglePin(LED_GPIO_Port,LED_Pin);//切换LED GPIO状态
			/*里程数 += 时间周期(s)*车轮转速(转/s)*车轮周长(cm)*/
			Mileage += 0.02*Motor1_Speed*22;
			
			EncoderTimer_Count = 0; //清空计数值
		}

然后主函数刷新

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c5fuRSkT-1675083353139)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221108203929170.png)]

	sprintf((char *)string,"V1:%.2fV2:%.2f ",Motor1_Speed,Motor2_Speed);//显示两个电机转速 单位:转/秒
	OLED_ShowString(0,0,string,12);
			
	sprintf((char *)string,"Mileage%.2f  ",Mileage);//显示两个电机转速 单位:转/秒
	OLED_ShowString(0,1,string,12);

一些测试中的坑

DAP识别芯片无法烧录

使用这个DAP无法下载这个淘宝核心板

点击下载出现这个报错

Not a genuine ST Device! Abort connection

image-20220809123109770

https://blog.csdn.net/chunquqiulailll/article/details/113257923

image-20220727195345745

如果这里识别为

image-20220729113616437

安装过程芯片支持包

image-20220729113514194

image-20220729113643543

image-20220729113906766

这里就会自动切换

image-20220729113953359

image-20220809124022451DAP无法识别芯片

image-20220809124049727

解决方法:

断电将板子上的BOOT0用短路帽接入3.3V高电平重新插入DAP不出意外可见程序烧录成功此时将BOOT0接回低电平后续烧录程序便不会出现SWD/JTAG Communication Failure

一个DAP可以问题记录

烧录代码(烧录时候、断掉电池供电、使用DAP给单片机供电)

stlink下载程序

下载程序出现错误

Debugger - Cortex-M ErrorCannot access target.Shutting down debug session.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qeAnDw8q-1675083353144)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221116175748401.png)]

取消这个勾选、然后编译

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gfdItLCx-1675083353144)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221116180051024.png)]

然后编译一下

然后再改回来

反复勾选、编译、取消勾选然后编译就可以下载了。

然后我勾选这就可以下载了

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Wix52Z2S-1675083353151)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221116213433686.png)]

说更新最新的支持包试试

https://www.keil.com/dd2/Pack/#!#eula-container

image-20220807152834504

其他的记录

细分更多、注意那么电机占空比控制函数也要变化

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fVss3ssR-1675083353152)(https://hjhvcc.oss-cn-nanjing.aliyuncs.com/img/image-20221210172627483.png)]

O_Port, KEY1_Pin) == GPIO_PIN_SET)//判断KEY1引脚仍为高电平
{
//这里编写触发中断后要执行的程序
if(g_ucMode == 5) g_ucMode = 1;//g_ucMode模式是0 1 2 3 4 5
else
{
g_ucMode+=1;
}
HAL_GPIO_TogglePin(LED_GPIO_Port,LED_Pin);
}
}
if(GPIO_Pin == KEY2_Pin) //判断一下那个引脚触发中断
{
HAL_Delay(10);//延时消抖
if(HAL_GPIO_ReadPin(KEY2_GPIO_Port, KEY2_Pin) == GPIO_PIN_RESET)//判断KEY2引脚仍为低电平
{
//这里编写触发中断后要执行的程序
g_ucMode=0;
HAL_GPIO_TogglePin(LED_GPIO_Port,LED_Pin);
}
}
}


但是测试不能执行中断程序异常卡死了

原因是HAL_Delay使用的是sysTick 中断优先级在软件初始化是默认最低的比外部中断优先级低所以HAL_Delay不能在外部中断服务函数中调用。

所以我们可以通过提高sysTick 中断的优先级提高的比HAL_Delay高。

[外链图片转存中...(img-gWf0yJty-1675083353135)]

然后我们提高至 如下图

[外链图片转存中...(img-CiPqZrwO-1675083353136)]

然后编译烧录测试按键是否更加稳定。

# 以上是应用篇

# 下面19是扩展

# 第22章-小车如何查找异常问题

前面我们已经移植 好了OLED程序下面我们就直接使用OLED,我们在定时器刷新OLED我们50ms执行一次

[外链图片转存中...(img-8IIQcoaW-1675083353136)]

应该这样写

[外链图片转存中...(img-BVHWDbf2-1675083353136)]

显示行进路程累计

但是发现计算不准确怀疑中断时间不准确如何确定中断是否按时到达那?

1. 使用sysTick统计时间

[外链图片转存中...(img-bl6GSFDT-1675083353137)]



2. 在要判断时间的地方反转GPIO通过示波器观察 是否按时中断

[外链图片转存中...(img-HHmUvIFx-1675083353137)]



 然后示波器测量引脚PC13

[外链图片转存中...(img-m26i8ty8-1675083353137)]

所以调试发现

[外链图片转存中...(img-5p2gBauB-1675083353138)]



 所以更新显示应该放到主函数执行然后 

我们的中断函数这样写

[外链图片转存中...(img-1HO1l4Vs-1675083353138)]

```c
	    if(EncoderTimer_Count%10 == 0)  //每20ms 进入一次
		{
		
			//进行PID计算、然后作用于电机
			Motor_Set(PID_realize(&pid1_speed,Motor1_Speed),PID_realize(&pid2_speed,Motor2_Speed));
			//向上位机发送数据
			ANO_DT_Send_F2(Motor1_Speed*100, 3.0*100,Motor2_Speed*100,3.0*100);
			
			HAL_GPIO_TogglePin(LED_GPIO_Port,LED_Pin);//切换LED GPIO状态
			/*里程数 += 时间周期(s)*车轮转速(转/s)*车轮周长(cm)*/
			Mileage += 0.02*Motor1_Speed*22;
			
			EncoderTimer_Count = 0; //清空计数值
		}

然后主函数刷新

[外链图片转存中…(img-c5fuRSkT-1675083353139)]

	sprintf((char *)string,"V1:%.2fV2:%.2f ",Motor1_Speed,Motor2_Speed);//显示两个电机转速 单位:转/秒
	OLED_ShowString(0,0,string,12);
			
	sprintf((char *)string,"Mileage%.2f  ",Mileage);//显示两个电机转速 单位:转/秒
	OLED_ShowString(0,1,string,12);

一些测试中的坑

DAP识别芯片无法烧录

使用这个DAP无法下载这个淘宝核心板

点击下载出现这个报错

Not a genuine ST Device! Abort connection

[外链图片转存中…(img-lwYBYyDV-1675083353139)]

https://blog.csdn.net/chunquqiulailll/article/details/113257923

[外链图片转存中…(img-UW9S610t-1675083353139)]

如果这里识别为

[外链图片转存中…(img-z2UZJvZn-1675083353141)]

安装过程芯片支持包

[外链图片转存中…(img-beD2JVkw-1675083353142)]

[外链图片转存中…(img-85oUCMk7-1675083353142)]

[外链图片转存中…(img-LDPqklzS-1675083353143)]

这里就会自动切换

[外链图片转存中…(img-wS7PSgsF-1675083353143)]

[外链图片转存中…(img-butu6BIy-1675083353143)]DAP无法识别芯片

[外链图片转存中…(img-KeTX9z28-1675083353144)]

解决方法:

断电将板子上的BOOT0用短路帽接入3.3V高电平重新插入DAP不出意外可见程序烧录成功此时将BOOT0接回低电平后续烧录程序便不会出现SWD/JTAG Communication Failure

一个DAP可以问题记录

烧录代码(烧录时候、断掉电池供电、使用DAP给单片机供电)

stlink下载程序

下载程序出现错误

Debugger - Cortex-M ErrorCannot access target.Shutting down debug session.

[外链图片转存中…(img-qeAnDw8q-1675083353144)]

取消这个勾选、然后编译

[外链图片转存中…(img-gfdItLCx-1675083353144)]

然后编译一下

然后再改回来

反复勾选、编译、取消勾选然后编译就可以下载了。

然后我勾选这就可以下载了

[外链图片转存中…(img-Wix52Z2S-1675083353151)]

说更新最新的支持包试试

https://www.keil.com/dd2/Pack/#!#eula-container

image-20220807152834504

其他的记录

细分更多、注意那么电机占空比控制函数也要变化

[外链图片转存中…(img-fVss3ssR-1675083353152)]

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6