三天让车跑起来!stm32循迹车 —— 第一天

声明在前:本系列以程序设计为主,适用于刚学会32,想完成一个基本项目却不知道怎么上手的小伙伴。想学习硬件方面如:电路、画板等内容的朋友请不要在本系列耽误您的时间,关闭即可。

眼瞅着日子一天天地过去,学校却迟迟不传来开学的消息,不知道有多少小伙伴因疫情宅家而无缘赛场:_( 本人就是其中之一,这时候我本应该看着已经会学习的车自己在赛道上驰骋,和队友吃着火锅唱着歌,再抽时间调调参/doge。
正值五一假期,在这个充满了活力的第一天,我决定把我曾经参加过的第一个比赛拿出来与各位一起分享,分享我当初完成这个项目的全过程与心得体会,供小伙伴们参考,接下来,进入正文:

任务需求

赛道如图:让小车按照顺时针或逆时针完成赛道一周。
其中:

  • 蓝色圆点为铺在黑线旁边的磁铁
  • 红色矩形为放置在赛道上的障碍物
  • 绿色区域为停车区,也是小车出发前放置在内的区域,

要求小车在能够完成赛道一周的基础上,检测到场地内的磁铁并做出反应、顺利避开障碍物并回归赛道,检测到停止线后在指定区域内停车

在这里插入图片描述

任务分析

在明确了任务需求以后,我们来分析一下,想要用STM32做出来这个车,我们需要准备哪些材料与相关知识

材料

  1. STM32:本系列教程选用经济实惠好上手的stm32f103系列,并且是引脚非常多的zet6
  2. 红外模块:5到8个或者更多
  3. 电机×2 + 舵机 或者 电机×4 再或者单电机车模等等(没必要用这么贵的)
  4. 电机驱动,根据电机的数量选择2路×1,×2或者4路
  5. 干簧管——检测磁铁;蜂鸣器——作出相应
  6. 以及其他所有搭车、焊电路所需的材料,包括但不限于洞洞板、降压模块、电池电池盒、螺栓螺母、有能力的自己画个PCB等等等等
  • (想用摄像头寻迹的、或者编码器测速的,给个/doge自己体会,这样的水平还是不要再出来欺负其他同学了 /斜眼笑)

分析

  1. 首要任务是寻迹,所以要用到红外模块——能够区分黑白色的模块,来判断赛道情况。
  2. 在寻迹的过程中,我们要用电机使小车前进,再用舵机转向,或者4个电机利用差速转向。
  3. 避障:若是想省点经费的话,可以把朝下的红外模块掰成朝前的/doge,或者可以用光电对管,二者其实差不多。或者有余力的用超声波模块来实现避障。
  4. 磁铁检测:首选干簧管,十分便宜,但是也很容易被不小心损坏。
  5. 停车:同样用红外模块来实现。
  • 避障、磁铁检测和停车会在第三天详细说。

稍微整理下:
在这里插入图片描述

知识储备

主要用到的模块就只有:红外、驱动、电机舵机等等
在这里插入图片描述
没错,仔细分析后从总体来看,想要完成这个项目只需要会GPIO和TIM就够了,所以说想要做出来它,并不难,难的是把他做好,然鹅这是可以通过实践做到的(调车呀/斜眼笑)。所以,不需要感到迷茫,just do it !
下面对接下来三天的内容做个概述

说明

本系列的文章都将以2个方面讲述可能要用到的知识点:

  • 理论知识
  • 应用:模块应用 + 程序写法

正文

今天主要讲GPIO有关的内容,涉及到定时器的会则会在下期讲

GPIO输入/输出

理论知识

在学过几天32了以后,大家差不多也应该见过点灯之类的基本程序了,我们知道,每个模块和功能在被使用之前,都要完成其基本的配置——初始化。其实我在学32的时候点灯的操作就是初始化GPIO、设置好模式以后通过输出达到的点灯的效果。
那么想用32板子上的某些引脚,我们要做到第一步就是初始化GPIO。
按步骤来,分别是

  1. 使能端口时钟
  2. 端口配置
  3. 工作模式
  4. 端口速度
  5. 调用GPIO初始化函数
  6. (若是设置为输出的话则要在这一步选择初始高还是低)

现在以PA.1输出 1 为例,程序如下:

 void Out_Init(void){
 GPIO_InitTypeDef  GPIO_InitStructure;
 	
 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;
 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;	
 GPIO_Init(GPIOA, &GPIO_InitStructure);
 GPIO_SetBits(GPIOA,GPIO_Pin_1);
 }

别看上面这一堆这么多,想看懂它还是很简单的,接下来就依次讲述这几句程序要注意的点:

  • RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

GPIOA可以理解为选择了PA开头的引脚,ENABLE则是使能的意思,什么叫使能?可以通俗地理解为开启这个功能

  • GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;

Pin_1则是说选择了PA端口的 PA.1 引脚

  • GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;

这句话是选择工作模式,而GPIO_Mode_Out_PP则是:推挽输出的意思。我们想要查看其他的模式时,选中了GPIO_Mode_Out_PP以后鼠标右键,点第一个Go To:
在这里插入图片描述
Go To以后我们看见总共有八种工作模式(含义见注释)

typedef enum
{ GPIO_Mode_AIN = 0x0, //模拟输入
  GPIO_Mode_IN_FLOATING = 0x04, //浮空输入
  GPIO_Mode_IPD = 0x28, //下拉输入
  GPIO_Mode_IPU = 0x48, //上拉输入
  GPIO_Mode_Out_OD = 0x14, //开漏输出
  GPIO_Mode_Out_PP = 0x10, //推挽输出
  GPIO_Mode_AF_OD = 0x1C, //复用开漏输出
  GPIO_Mode_AF_PP = 0x18  //复用推挽输出
}GPIOMode_TypeDef;

其中,推挽输出、上拉及下拉输入是经常用在一些基本小任务的,就比如这个循迹小车里,上拉或下拉输入用于接收红外模块传来的信息(上下拉区别会在应用里讲),推挽输出则用于逻辑输出来控制电机方向。

  • GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;

这句话就是配置端口速度的意思,GPIO_Speed_50MHz就是50MHz,除了50MHz,还有10MHz 和 2MHz可以选择,这些端口速度对于一般不需要精细操作的任务时不需要在意,真正需要精密操作的可能也不用f103蛤/doge不过有一点要注意的就是:
像L298N驱动有一个非常大的散热片,如果使用时间过长、过热等情况下,它会散发出有点难闻的味道:)如果我们在用L298N的时候把端口速度设置为50MHz,有可能同样会发味道:(,所以这时候我们可以把端口速度改成10MHz甚至2MHz

  • GPIO_Init(GPIOA, &GPIO_InitStructure);
    这句话是库里的初始化端口函数,同样是需要选择好端口。

  • GPIO_SetBits(GPIOA,GPIO_Pin_1);(如果是输入则不用写这句)

蓝色的SetBits是说,在配置完PA.1,选择的推挽输出以后,初始的状态是“高”。如果我们想让它初始低的话,则要用ResetBits。我们同样Go To这个函数(这也是库里自带的函数)以SetBits为例:

void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
{
  /* Check the parameters */
  assert_param(IS_GPIO_ALL_PERIPH(GPIOx));
  assert_param(IS_GPIO_PIN(GPIO_Pin));
  
  GPIOx->BSRR = GPIO_Pin;
}

我们注意到最后一句:GPIOx->BSRR = GPIO_Pin,这个函数下面的函数就是Reset,相应的位置则写成GPIOx->BRR = GPIO_Pin。如果我们想直接操作寄存器的话,则要用到这两句。
初始化程序写完了,当我们想要在主函数改变引脚电平的话,直接用GPIO_SetBits(GPIOA,GPIO_Pin_1)或者ResetBits就好。;
若是想用输入模式,在主函数里想读取电平时就要用到GPIO_ReadInputDataBit函数
以上就是我们使用GPIO所需要知道的基本知识,接下来我们看看GPIO输入输出改怎么应用到具体的模块

应用

红外模块:非黑即白,非0即1

模块/寻迹介绍

红外模块大概长这样,(左图是平价款,右图则是稍微专业一点的,其实本质上没有区别,只是精度和距离等有所区别)
在这里插入图片描述
他们引出来的三个引脚,分别是 Vcc、Gnd、Out。具体位置还是要以买来的模块实物为主。V和G分别是电源与地,Out则是根据红外模块根据实际情况的返回值,
还有一种是把几个红外放到一起,如图:
在这里插入图片描述
其实不管是哪个,他们的原理都是一样的:非黑即白,非0即1

本系列用到的模块是,检测到黑色时,返回0,若为白色则为1,我们在写程序的时候,就要分配好用于输入的引脚,然后把Out与这个引脚用杜邦线连上就能用了。

比如这种情况:模块检测到的是黑长直/doge
在这里插入图片描述
前文说过:本系列用的模块检测到黑色时,返回0,若为白色则为1,那么这五个红外的返回值从左到右分别是 11011。所以,相应的,我们在程序中就应该配置五个用于输入的引脚,为了连线方便,这里建议使用挨在一起的5个引脚。
不用5个一体的红外板子也可,可以用几个单个的红外模块:(请原谅我的画图水平)
在这里插入图片描述
在介绍完了红外的用法以后,还是要回归实际的,接下来就讲讲怎么写红外寻迹相关的程序:

红外模块程序

在说程序该怎么写之前,要强调一点:每个人的编程习惯不同,为了方便理解,我用这种类型的工程文件目录:
在这里插入图片描述
其中,前四个文件夹是用于放库、主函数等文件的,“Hardware”是用来放控制外设的程序如:led、key、motor、sensor等,“Basic”是用来放基层程序的如:tim、iic、spi等等。当然可以不用这种写法,毕竟像正点原子、野火等都有各自的写法。

SEARCH.h文件

我们现在要写红外模块的控制程序,就要在Hardware文件夹里创建一个新文件夹,然后在里面分别新建.c与.h文件,(由.txt直接改后缀就行),如图:
在这里插入图片描述

然后打开keil5,把文件与文件夹添加到工程里以后,打开SEARCH.h。此文件是用于声明即将用到的变量、函数等
首先,我们define一下红外模块的输入:

#define BLACK 0
#define WHITE 1
  • 回顾前文,提到过:若是想用输入模式,在主函数里想读取电平时就要用到GPIO_ReadInputDataBit函数,在使用它的时候,我们要依次输入两个参数:1.端口、2.引脚。比如
  • GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_1)

但是这么写不容易认出来这个引脚到底是连接到哪个位置的红外模块,函数也很长,不方便写,所以我们同样要define一下引脚以及这个函数

#define LL_GPIO        GPIOA
#define LL_PIN         GPIO_Pin_1
#define LL_IN          GPIO_ReadInputDataBit(LL_GPIO, LL_PIN)

这里我把他们define为 LL 位置的引脚,若是我们用5红外的话,我按照位置从左至右分别为:LL L M R RR,所以这样3行的define我们就要复制粘贴5遍再改成各自的名字,共十五行
最后再声明我们需要用到的函数,比如初始化、赛道判断等函数

void SearchInit(void); //初始化
void Run(void);   //判断赛道

要是懒得单独写赛道判断函数,可以直接把赛道判断的部分全都堆到主函数的while(1)里,但这种习惯仅适合于基本情况。当我们需要在定时器中断卡时间的时候,总不能把这一堆全都复制粘贴到中断里吧:)那得多乱233333

声明完函数以后,我们就要在SEARCH.c里定义他们了

SEARCH.c文件

其实初始化程序只不过是在GPIO初始化的时候把引脚设置为输入模式了。
最常见的输入模式有:上拉输入、下拉输入。这两个输入方式有什么区别呢?

  • 首先,若是有高或低电平接入的话,无论是上拉还是下拉输入,他们的电平都是由输入电平决定的,输入的是高则此引脚为高,低则低。
  • 其次,上拉输入,在浮空的时候(没有电平接入)是高的,下拉反之。

这里我用的是上拉输入,即工作状态是:GPIO_Mode_IPU,( 还记得前文提到过的8个模式吗?)

void SearchInit(void)
{
	GPIO_InitTypeDef  GPIO_InitStructure;
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

	GPIO_InitStructure.GPIO_Pin = LL_PIN;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(LL_GPIO , &GPIO_InitStructure); 
		
	//此为LL位置的红外模块初始化
	//接下来是 L M R RR 的初始化,Ctrl C V即可
}

初始化函数写完以后,我们再来看看赛道判断函数:
首先我们要了解一下,当车在赛道不同位置的时候,五个红外对应的输出情况:
在这里插入图片描述
这是五种最基本的状态,我们先把这五种情况放到前面声明的 Run() 里,用最基本的if语句就能做到

void Run(void)
{
    //白白黑白白,11011
	if (LL_IN == WHITE && L_IN == WHITE  && M_IN == BLACK && R_IN ==WHITE && RR_IN ==WHITE){
	直行
	电机速度:两侧相同
	舵机角度:向前
	}
	else if( 黑白白白白 ){
	左拐大弯
	电机速度:可以差速,可以不用
	舵机角度:大角度左
	}
	else if( 白黑白白白 ){
	左拐小弯
	电机速度
	舵机角度:小角度左
	}
	在“白白白黑白”与“白白白白黑”的情况与刚才的两档左拐同样写法
}

这就是最最最基本的五种情况,然鹅如果事情真的像我们想的这样顺利就好了,在实际调车的时候,我们往往会发现,仅仅这五种情况是不够的:

  • 当车遇到任何一种除了以上五个以外的情况时,我们没有写相应的判断语句,所以往往会发生不可预知的结果

它还有非常大的进步空间(在某种意义上它身处谷底)完善判断的方法我们会在第三天里写。
那么,这个寻迹的框架但这里我们就算是写好了/doge,像“直行”、“电机速度”、“舵机角度”的控制函数,我们下次讲定时器的时候会说,接下来我们回到GPIO的下一个应用

电机驱动

好的,挖了个小坑,我们继续说GPIO。刚才说了GPIO输入的应用实例,接下来说说GPIO输出的应用:
在这里插入图片描述

电机的输出状态有两个参数

  1. 方向
  2. 转速

其中,电机的方向由输入电机驱动的逻辑输入决定,电机的转速由PWM决定,PWM明天跟定时器一起说,今天主要说的是:逻辑输入

逻辑输入就是:分别输入0或1,通过其不同组合,来达到不同的效果。
最基本最常用的电机驱动有:L298N、TB6612等,今天就以TB6612为例,讲一下该怎么使用这个逻辑输入:
我们先来看一下TB6612的背面:
在这里插入图片描述
其中,AIN1、AIN2,为一组,BIN1、BIN2为一组,两组分别控制一路电机的转向,而两个PWM分别决定了电机的转速。
以A组为例,我们来看一下不同的输入会产生什么样的结果:

IN1 IN2 转向
10 正转
01 反转
00 停止
11 自己试试 /斜眼笑

其实,如果我们只想让车往前走的话,只需要把AIN1 和 BIN1 接到板子上的Vcc,然后IN2都接地就可,不写单独的程序也行。如果真的要用这种办法的话,最好在焊电路的时候能够先单独引出来足够的Vcc 和 Gnd,然后把这些模块都接到这里,这样接完线以后的小车会很工整、很漂亮(漂亮警告)
在这里插入图片描述
如果我们想正经用电机驱动,还是要走流程的/doge
依然是在Hardware文件夹里新建TB6612的文件夹

TB6612.h

声明电机驱动初始化函数

void TB6612Init(void);

还有别的函数需要声明,说完定时器以后会写的

TB6612.c

在.h里声明了以后,我们就又回到.c里面来定义这个函数了,这里应该用两个引脚来输出高电平:以PD.1,PD.2为例

void TB6612Init(void)
{
	GPIO_InitTypeDef  GPIO_InitStructure;
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD, ENABLE);
 
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;	
    GPIO_Init(GPIOD, &GPIO_InitStructure);
    GPIO_SetBits(GPIOD,GPIO_Pin_1);
 
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;	
    GPIO_Init(GPIOD, &GPIO_InitStructure);
    GPIO_SetBits(GPIOD,GPIO_Pin_2);
 }

这两个初始化里都用了GPIO_SetBits函数,即初始电平为1,让电机正转。

主函数

include

我们现在回到主函数,最先要写的就是:include ,include “yyyyy"等等,但是由于以后可能还要再增加新的库,为了便于管理,我们可以新建一个"include.h”,我们可以把其他都放进"include.h"里,然后再主函数第一句写include “include.h” 就好

main

到目前为止,我们已经写完了两个初始化函数,1.红外初始化 2.TB6612初始化
以及一个控制函数的框架:Run()

所以现在我们可以在主函数写的程序有:

int main(void)
{   
	SearchInit();
	TB6612Init();	 
	while (1)
	{
	Run();
	}		

在这里插入图片描述
其实不光有这种把控制都放在while(1)里的写法,另一种稍微常用点的方法是,把这些控制函数放在定时器中断里,这种放在中断里的写法有一个优点就是:每次完成任务都严格按照时间进行,并且不会被其他情况给中断导致程序运行异常。


今天的内容就说这么多了,主要是简单滴介绍了在循迹小车中,与GPIO有关的理论知识与实际应用,其他像如何让电机转起来、舵机转角度等等内容,都会在下次讲定时器的时候细说,至于最后循迹小车整体项目与完善方案等等内容会在第三期细说,感兴趣的小伙伴可以关注我的频道,除了这个系列,我目前正在持续更新Python学习的系列,在Python完结后,根据时间安排会再开机器学习的系列,欢迎各位与我共同学习,一起进步!
在这里插入图片描述
点我进入下一期:三天让车跑起来!stm32寻迹车第二天!.

原创文章 23 获赞 103 访问量 2686

猜你喜欢

转载自blog.csdn.net/k_ksy/article/details/105859319
今日推荐