当前位置:   article > 正文

STM32项目-STM32智能小车-电子设计大赛-STM32cubemx-STM32f103c8t6STM32串口通信-_基于stm32开发板的智能车代码adc控制部分

基于stm32开发板的智能车代码adc控制部分

记录项目的详细制作过程,所以笔记很长,图很多、很多图不好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);
  • 1
  • 2

根据自己的芯片选择

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状态
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

把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高电平
  • 1
  • 2
  • 3
  • 4
  • 5

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);//技
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

烧录下载 现象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);
  • 1
  • 2
  • 3

方法二:实现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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

如果有错误

image-20220728211515588

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

image-20220728211651598

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

image-20220728213124409

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

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输出
  • 1
  • 2

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

image-20220729164150949

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

这就是我们要设置的

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

 __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, 40);
  • 1

image-20220729170905026

5.3-PWM测试方法

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

KEIL软件仿真方法:

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

image-20220729145446106

选择软件仿真

image-20220729150059613

DARMSTM.DLL
-pSTM32F103C8
  • 1
  • 2

设置时钟频率-板子外部晶振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为低电平
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

仿真测试代码

使用软件仿真

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

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为 高电平

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

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

/*******************
*  @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改变占空比

	}

}  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49

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

	HAL_Delay(500);
	Motor_Set(0,0);
  • 1
  • 2

第七章-编码器测速

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 中断
  • 1
  • 2
  • 3
  • 4

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

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

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

每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
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

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

用手转动电机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;
  • 1
  • 2

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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 中断
  • 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;
		}
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

把之前的变量定义放这里

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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;
  • 1
  • 2
  • 3
  • 4
  • 5

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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);
  • 1
  • 2

把变量需要声明一下

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

extern float Motor1Speed ;
extern float Motor2Speed ;
  • 1
  • 2

然后打开串口助手

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

开始实验

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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,在内存中的地址为0x0010,x的值为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))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

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

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);//这里是串口发送函数
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107

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 

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

添加测试代码

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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);
  • 1
  • 2
  • 3

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

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

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

然后在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;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50

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

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

  PID_init();
  • 1

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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

主函数-可以估算当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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

因为实际值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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

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接收中断
  • 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);
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

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

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;//表示正在接受中
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

然后我们把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);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

测试发送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;
		}
	}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

更改一下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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

更改主函数,对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));
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

很容易得到一下控制方式

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

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

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

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

加速减速函数

//向前加速函数
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);//设置到电机
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

第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;
  • 1
  • 2

然后主函数我们通过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驱动里面的,是显示位置的一个函数
  • 1
  • 2
  • 3
  • 4
  • 5

第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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

在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驱动里面的,是显示位置的一个函数,
  • 1
  • 2

第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)
  • 1
  • 2
  • 3
  • 4

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

然后测试

  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控制速度
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

然后实现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;//保存上次红外对管状态
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54

在pid.中

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

tPid pidHW_Tracking;//红外循迹的PID
  • 1

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

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

可以改进的地方

  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;  //保存串口三接收的数据
  • 1

开启串口三中断接收

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

  HAL_UART_Receive_IT(&huart3,&g_ucUsart3ReceiveData,1);  //串口三接收数据
  • 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);//继续进行中断接收
	}
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

在usart.c中声明外部变量

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

然后我们更改一下 主函数内容,把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);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

把之前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 --);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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	;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

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

注意:两个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:计算字符串大小	

  • 1
  • 2
  • 3

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

手机显示

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);
			}
		}
	}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

第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);
	}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

PID跟随功能

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

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

tPid pidFollow;    //定距离跟随PID
  • 1
	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;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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);//读取超声波传感器不能过快
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 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 输出高	
 
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

使用软件初始化两个引脚

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
  • 3
  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电平
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

更改一下 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;
//
  • 1
  • 2
  • 3
  • 4
  • 5

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

把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;
  • 1
  • 2

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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" 
  • 1
  • 2
  • 3
  • 4
  • 5

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

替换inv_mpu.h的

#include "stm32f1xx_it.h"
  • 1

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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);
  • 1
  • 2
  • 3
  • 4

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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){}  //这个可以解决经常读不出数据的问题
  • 1
  • 2
  • 3
  • 4
  • 5

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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];
  • 1
  • 2

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

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
  • 1

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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; //第一个电机控制输出
  • 1
  • 2
  • 3

走直线程序如下(因为上电初始化时候航向角是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);

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

然后调节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;
  • 1
  • 2
  • 3

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

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

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

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

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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;//目标值
        }	
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

然后我们的蓝牙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:遥控角度闭环
  • 1
  • 2

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

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

在gpio.h声明一下

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

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

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

先不进行消抖,如果后面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);
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

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

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

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

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

	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);
	
	}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185

可以测试上面的代码 然后没有问题后,我们添加一个通过蓝牙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;//设置为显示模式
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

然后对应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);
		}
	}
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

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

原因是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; //清空计数值
		}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

然后主函数刷新

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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);
  • 1
  • 2
  • 3
  • 4
  • 5

一些测试中的坑

DAP识别芯片无法烧录

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

点击下载出现这个报错

Not a genuine ST Device! Abort connection
  • 1

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; //清空计数值
		}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76

然后主函数刷新

[外链图片转存中…(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);
  • 1
  • 2
  • 3
  • 4
  • 5

一些测试中的坑

DAP识别芯片无法烧录

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

点击下载出现这个报错

Not a genuine ST Device! Abort connection
  • 1

[外链图片转存中…(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)]

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小小林熬夜学编程/article/detail/684915
推荐阅读
相关标签
  

闽ICP备14008679号