4.2 STM32单片机输入端口的应用
在第2章,你就已经知道了STM32系列单片机有5个16位的并行I/O口:PA、PB、PC、PD和PE。这5个端口,既可以作为输入,也可以作为输出,既可按16位处理,也可按位方式使用。实际上,在单片机复位期间和刚复位后,复用功能未开启,I/O端口被配置成浮空输入模式。所有端口都有外部中断能力。为了使用外部中断线,端口必须配置成输入模式。
作为输入,如果I/O脚上的电压为高电平(5V或3.3V),则其相对应的I/O口寄存器中的相应位存储1;如果电压为低电平(0V),则存储0。
布置恰当的电路,你可以让胡须达到上述效果:当胡须没有被碰到时,使I/O脚上的电压为高电平(5V或3.3V);当胡须被碰到时,则使I/O脚上电压为低电平(0V)。然后,单片机就可以读入相应数据,进行分析、处理,控制机器人的运动。安装好触须的机器人小车全貌如图4.2所示。
图4.2 安装好触须的机器人小车全貌
任务二 安装并测试机器人的触觉——胡须
让机器人通过触觉胡须进行导航,首先必须安装并测试胡须。图4.3所示是所需的硬件元件清单,包括:
图4.3 胡须硬件
(1)金属丝2根;
(2)平头M3×22盘头螺钉2个;
(3)13mm圆形立柱2个;
(4)M3尼龙垫圈2个;
(5)3-pin公-公接头2个;
(6)2个220Ω电阻(色环:红—红—黑—黑);
(7)2个10kΩ电阻(色环:棕—黑—黑—红)。
参考图4.4,安装胡须
图4.4 安装机器人胡须
螺钉依次穿过M3尼龙垫圈、13mm圆形立柱;
螺钉穿过主板上的圆孔之后,拧进主板下面的支架中,但不要拧紧;
把须状金属丝的其中一个钩在尼龙垫圈之上,另一个钩在尼龙垫圈之下,调整它们的位置使它们横向交叉但又不接触;拧紧螺钉到支架上;
参考接线图4.5,搭建胡须电路。注意:右边胡须状态信息输入是通过PE口的第1脚完成的,而左边胡须状态信息输入是通过PE口的第0脚完成的;
图4.5 胡须电路示意图
确定两条胡须比较靠近,但又不接触面包板上的3-pin头,推荐保持3mm的距离;
图4.6所示是实际的参考接线图。安装好触觉胡须的教学开发板如图4.7所示。
图4.6 胡须接线图
图4.7 安装好触须的教学开发板
测试胡须
观察一下图4.5所示的胡须电路示意图,显然每条胡须都是一个机械式的、接地常开的开关(类似按键)。胡须接地(GND)是因为教学板外围的镀金孔都连接到GND。金属支架和螺丝钉提供电气连接给胡须。
通过编程让单片机探测什么时候胡须被触动。由图4.5可知,连接到每个胡须电路的I/O引脚监视着10kΩ上拉电阻上的电压变化。当胡须没有被触动,连接胡须的I/O引脚的电压是高电平;当胡须被触动时,I/O短接到地,所以I/O引脚的电压是低电平。
上拉电阻
上拉电阻就是与电源相连,并起到拉高电平作用的电阻。此电阻还起到限流的作用,如图4.5中的10kΩ电阻即为上拉电阻。与之对应的还有“下拉电阻”,它与“地(GND)”相连,可把电平拉至低位。
例程:TestWhiskers.c
#include "stm32f10x_heads.h" #include "HelloRobot.h" int PE2state(void)//获取PE2的状态 { return GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_2); } int PE3state(void)//获取PE3的状态 { return GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_3); } int main(void) { BSP_Init(); USART_Configuration(); printf("Program Running!\r\n"); while(1) { printf("右边胡须的状态:%d ", PE2state()); printf("左边胡须的状态:%d\r\n",PE3state()); delay_nms(150); } }
上面的例程是用来测试胡须的功能是否正常。首先,定义了两个无参数有返回值子函数int PE2state(void)和int PE3state(void)来获取左右两个胡须的状态。STM32单片机的5个端口PA、PB、PC、PD和PE是可以按位来操作的,从低到高依次为第0口、第1口、……、第15口。
在搞清楚整个程序的执行原理后,按照下面的步骤实际执行程序,对触觉胡须进行测试。
● 连接好串口电缆,接通教学板和伺服电机的电源。
● 输入、保存并运行程序TestWhiskers.c。
● 检查图4.5,弄清楚哪条胡须是左胡须,哪条是右胡须。
● 此时调试终端显示:“右边胡须的状态:1左边胡须的状态:1”,如图4.8所示。
图4.8 左右胡须均未碰到
● 把右胡须按到3-pin转接头上,注意显示为:“右边胡须的状态:0左边胡须的状态:1”,如图4.9所示。
图4.9 右胡须碰到
● 把左胡须按到3-pin转接头上,注意显示为:“右边胡须的状态:1左边胡须的状态:0”,如图4.10所示。
图4.10 左胡须碰到
● 同时把两个胡须按到各自的3-pin转接头上,显示为:“右边胡须的状态:0左边胡须的状态:0”,如图4.11所示。
图4.11 左右胡须均碰到
● 如果两个胡须都通过测试,你可以继续下面的内容;否则检查程序或电路中存在的错误。
任务三 基于胡须的机器人触觉导航
任务二中,你已经通过编程检测胡须是否被触动。在本任务中将利用这些信息对机器人进行运动导航。在机器人行走过程中,如果有胡须被触动,那就意味着碰到了什么。导航程序需要接受这些输入信息,判断它的意义,调用一系列使机器人倒退、旋转朝不同方向行走的动作子函数以避开障碍物。
下面的程序让机器人向前走直到碰到障碍物。在这种情况下,机器人用它的一根或者两根胡须探测障碍物。一旦胡须探测到障碍物,调用第3 章中的导航程序和子程序使小车倒退或者旋转,然后再重新向前行走,直到遇到另一个障碍物。
赋值运算符“=”与关系运算符“==”
注意赋值运算符“=”与关系运算符“==”的区别:赋值运算符“=”用来给变量赋值;关系运算符“==”判断两个值是否是相等的关系。
逻辑与“&&”运算符的运算规则:
A&&B 若A、B都为真,则A&&B为真。 注意区分位操作符“&”和逻辑运算符“&&”。
例程:RoamingWithWhiskers.c
● 打开主板和伺服电机的电源;
● 输入、保存并运行程序RoamingWithWhiskers.c;
● 尝试让机器人运动,当在其路线上遇到障碍物时,它将后退、旋转并向另一个方向。
#include "stm32f10x_heads.h"
#include "HelloRobot.h"
int PE2state(void)//获取PE2的状态
{
return GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_2);
}
int PE3state(void)//获取PE3的状态
{
return GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_3);
}
void Forward(void)
{
GPIO_SetBits(GPIOD, GPIO_Pin_10);
delay_nus(1700);
GPIO_ResetBits(GPIOD,GPIO_Pin_10);
GPIO_SetBits(GPIOD, GPIO_Pin_9);
delay_nus(1300);
GPIO_ResetBits(GPIOD,GPIO_Pin_9);
delay_nms(20);
}
void Left_Turn(void)
{
int i;
for(i=1;i<=26;i++)
{
GPIO_SetBits(GPIOD, GPIO_Pin_10);
delay_nus(1300);
GPIO_ResetBits(GPIOD,GPIO_Pin_10);
GPIO_SetBits(GPIOD, GPIO_Pin_9);
delay_nus(1300);
GPIO_ResetBits(GPIOD,GPIO_Pin_9);
delay_nms(20);
}
}
void Right_Turn(void)
{
int i;
for(i=1;i<=26;i++)
{
GPIO_SetBits(GPIOD, GPIO_Pin_10);
delay_nus(1700);
GPIO_ResetBits(GPIOD,GPIO_Pin_10);
GPIO_SetBits(GPIOD, GPIO_Pin_9);
delay_nus(1700);
GPIO_ResetBits(GPIOD,GPIO_Pin_9);
delay_nms(20);
}
}
void Backward(void)
{
int i;
for(i=1;i<=65;i++)
{
GPIO_SetBits(GPIOD, GPIO_Pin_10);
delay_nus(1300);
GPIO_ResetBits(GPIOD,GPIO_Pin_10);
GPIO_SetBits(GPIOD, GPIO_Pin_9);
delay_nus(1700);
GPIO_ResetBits(GPIOD,GPIO_Pin_9);
delay_nms(20);
}
}
int main(void)
{
BSP_Init();
USART_Configuration();
printf("Program Running!\n");
while(1)
{
if((PE2state()==0)&&(PE3state()==0)) //两胡须同时碰到
{
Backward(); //向后
Left_Turn(); //向左
Left_Turn(); //向左
}
else if(PE2state()==0) //右胡须碰到
{
Backward(); //向后
Left_Turn(); //向左
}
else if(PE3state()==0) //左胡须碰到
{
Backward(); //向后
Right_Turn(); //向右
}
else //胡须没有碰到
Forward(); //向前
}
}
注意:函数Forward()有一个变动。它只发送一个脉冲,然后返回。这点相当重要,因为机器人可以在向前运动中的每两个脉冲之间检查胡须的状态。意味着,机器人在向前行走的过程中,每秒检查触须状态大概43次(1000ms/23ms≈=43)。
因为每个全速前进的脉冲都使得机器人前进大约半厘米。只发送一个脉冲,然后回去检查胡须的状态是一个好主意。每次程序从Forward()返回后,程序再次从while循环的开始处执行,此时if…else语句会再次检查胡须的状态。
任务四 机器人进入死区后的人工智能决策
你或许已注意到机器人卡在墙角里的情况。当机器人进入墙角时,左胡须触墙,于是它右转,向前行走,右胡须触墙,于是左转前进,又碰到左墙,再次碰到右墙……如果不是你把它从墙角拿出来,它就会一直困在墙角里而出不来。
编程逃离墙角死区
你可以修改RoamingWithWhiskers.c来让机器人碰到上述问题时逃离死区。技巧是记下胡须交替触动的总次数。技巧的关键是程序必须记住每个胡须的前一次触动状态,并和当前触动状态对比。如果状态相反,就在交替总数上加1。如果这个交替总数超过了程序中预先给定的阀值,表示这是个墙角(死区),那么就该做一个“U”型转弯,并且把胡须交替计数器复位。
这个技巧的编程实现依赖于if…else嵌套语句。换句话说,程序检查一种条件,如果该条件成立(条件为真),则再检查包含于这个条件之内的另一个条件。下面是用伪代码说明嵌套语句的用法。
IF (condition1) { commands for condition1 IF(condition2) { commands for both condition2 and condition1 } ELSE { commands for condition1 but not condition2 } } ELSE { commands for not condition1 }
伪代码通常用来描述不依赖于计算机语言的算法。实际上在前面几章的任务和小结中,已经多次提醒和暗示你,无论是哪种计算机语言,都必须能够描述人类知识的逻辑结构。而人类知识的逻辑结构是统一的,如条件判断就是人类知识最核心的逻辑之一。因此,各种计算机语言都有语法和关键词来实现条件判别。因此,在写条件判断算法时,经常用一种用于描述人类知识结构逻辑的伪代码来描述在计算机中如何实现这些逻辑算法,以使算法具有通用性。有了伪代码,用具体的语言来实现算法就很简单了。
下面的例程,用于探测连续的、交替出现的胡须触动过程。这个程序使机器人在第四次或第五次交替探测到墙角后,完成一个“U”型的拐弯,次数依赖于哪一个胡须先被触动。
● 输入、保存并运行程序EscapingCorners.c;
● 在机器人行走时,轮流触动它的胡须,测试该程序。
例程:EscapingCorners.c
#include "stm32f10x_heads.h" #include "HelloRobot.h" int PE2state(void)//获取PE2的状态 { return GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_2); } int PE3state(void)//获取PE3的状态 { return GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_3); } void Forward(void) { … //略,同前 } void Left_Turn(void) { … //略,同前 } void Right_Turn(void) { … //略,同前 } void Backward(void) { … //略,同前 } int main(void) { int counter=1; //胡须碰撞总次数 int old2=1; //右胡须旧状态 int old3=0; //左胡须旧状态 BSP_Init(); USART_Configuration(); printf("Program Running!\n"); while(1) { if(PE3state()!=PE2state()) { if((old2!=PE2state())&&(old3!=PE3state())) { counter=counter+1; old2=PE2state(); old3=PE3state(); if(counter>4) { counter=1; Backward();//向后 Left_Turn();//向左 Left_Turn();//向左 } } else counter=1; } if((PE3state()==0)&&(PE2state()==0)) { Backward(); //向后 Left_Turn(); //向左 Left_Turn(); //向左 } else if(PE2state()==0) { Backward(); //向后 Left_Turn(); //向左 } else if(PE3state()==0) { Backward(); //向后 Right_Turn();//向右 } else Forward(); //向前 } }
EscapingCorners.c是如何工作的?
由于该程序是经RoamingWithWhiskers.c修改而来,下面只讨论与探测和逃离墙角相关的新特征。
int counter=1; int old2=1; int old3=0;
这三个变量用于探测墙角。int型变量counter用来存储交替探测的次数。例程中,设定的交替探测的最大值为4。int型变量old2、old3存储胡须旧的状态值。
程序赋counter初值为1,当机器人卡在墙角此值累计到4时,counter复位为1。old2和old3必须赋值以至于看起来好像两根胡须的其中一根在程序开始之前被触动了。这些工作之所以必须做,是因为探测墙角的程序总是对比交替触动的部分,或者PE2state()==0,或者PE3state()==0。与之对应,old2和old3的值也相互不同。
现在看探测连续而交替触动墙角的部分。
首先要检查的是,是否有且只有一个胡须被触动。简单的方法就是询问“是否PE2state()不等于PE3state()”。其具体判断语句如下:
if(PE3state()!=PE3state())
假如真有胡须被触动,接下来要做的事情就是检查当前状态是否确实与上次不同。换句话说,是old2不等于PE2state()和old3不等于PE3state()吗?如果是,就在胡须触动计数器上加1,同时记下当前的状态,设置old2等于当前的PE2state(),old3等于当前的PE3state()。
if((PE2state()==0)&&(PE3state()==0)) { Backward();//向后 Left_Turn();//向左 Left_Turn();//向左 }
如果发现胡须连续四次被触动,那么计数值置1,并且进行“U”型拐弯。
if(counter>4) { counter=1; Backward(); Left_Turn(); Left_Turn(); }
紧接的else语句是机器人没有陷入墙角情况,故需要将计数器值置1。之后的程序和RoamingWithWhiskers.c中的一样。
该你了
● 尝试增加变量counter的数值为5和6,注意结果;
● 尝试减小变量counter的数值,观察小车在正常行走过程中是否有任何不同。