3.2 STM32单片机程序模块化设计
图3.13定义了机器人的前后左右四个方向。在第2章实际上你应该已经发现,如果按照图3.13前进方向的定义,机器人向前走时,从机器人的左边看,它向前走时轮子是逆时针旋转的;从右边看另一个轮子则是顺时针旋转的。
图3.13 机器人及其前进方向的定义
任务二 基本巡航动作
发给单片机控制引脚的高电平持续时间决定了伺服舵机旋转的速度和方向。for循环的参数控制了发送给电机的脉冲数量。由于每个脉冲的时间是相同的,因而for循环的参数也控制了伺服电机运行的时间。下面是使机器人向前走三秒钟的程序实例。
例程:RobotForwardThreeSeconds.c
● 确保控制器和伺服电机都已接通电源;
● 输入、保存、编译、下载并运行程序RobotForwardThreeSeconds.c。
#include "stm32f10x_heads.h"
#include "HelloRobot.h"
int main(void)
{
int counter;
BSP_Init();
USART_Configuration();
printf("Program Running!\n");
for(counter=0;counter<130;counter++)//运行3秒
{
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);
}
while(1);
}
RobotForwardThreeSeconds.c是如何工作的?
理解该例程的运行你应该没什么问题:for循环体中前三行语句使左侧电机逆时针旋转,接着的三行语句使右侧电机顺时针旋转。因此两个轮子转向机器人的前端,使机器人向前运动。整个for循环执行130次大约需要3秒钟,从而机器人也向前运动3秒钟。
关于例程的一点说明
例程中使用printf函数是为了起提示作用。若你觉得串口线影响了机器人的运动,可以不用此函数;还有一个进行调试的方法:让机器人的前端悬空,让伺服电机空转。这样调试起来就方便了,机器人不会到处乱跑。后面的例程调试也是这样。
该你了——调节距离和速度
delay_nus函数的参数n为1700和1300都使电机接近它们的最大速度旋转。把每个delay_nus函数的参数n设定得更接近让电机保持停止的值——1500,可以使机器人减速。
向后走,原地转弯和绕轴旋转
将delay_nus函数的参数n以不同的值组合就可以使机器人以其他的方式运行,你可以在一个程序中实现机器人向前走、左转、右转及向后走。
例程:ForwardLeftRightBackward.c
● 输入、保存并运行程序ForwardLeftRightBackward.c。
#include "stm32f10x_heads.h" #include "HelloRobot.h" int main(void) { int counter; BSP_Init(); USART_Configuration(); printf("Program Running!\n"); for(counter=1;counter<=65;counter++)//向前 { 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); } for(counter=1;counter<=26;counter++)//向左转 { 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); } for(counter=1;counter<=26;counter++)//向右转 { 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); } for(counter=1;counter<=65;counter++)//向后 { 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); } while(1); }
该你了——以一个轮子为支点旋转
你可以使机器人绕一个轮子旋转。诀窍是使一个轮子不动而另一个旋转。例如,保持左轮不动而右轮从前面顺时针旋转,机器人将以左轮为轴旋转。
GPIO_SetBits(GPIOD, GPIO_Pin_10); delay_nus(1500); GPIO_ResetBits(GPIOD,GPIO_Pin_10); GPIO_SetBits(GPIOD, GPIO_Pin_9); delay_nus(1300); GPIO_ResetBits(GPIOD,GPIO_Pin_9); delay_nms(20);
如果你想使它从前面向右旋转,很简单,停止右轮,左轮从前面逆时针旋转。
GPIO_SetBits(GPIOD, GPIO_Pin_10); delay_nus(1700); GPIO_ResetBits(GPIOD,GPIO_Pin_10); GPIO_SetBits(GPIOD, GPIO_Pin_9); delay_nus(1500); GPIO_ResetBits(GPIOD,GPIO_Pin_9); delay_nms(20);
用刚讨论过的代码片段替代前进、左转、右转和后退相应的代码片段,通过更改每个for循环的循环次数来调整每个动作的运行时间,更改注释来反应每个新的旋转动作。
运行更改后的程序,验证上述旋转运动是否不同。
任务三 匀加速/减速运动
在前面机器人运动过程中,你是否发现机器人在每次启动和停止的时候,是不是有些太快,从而导致机器人几乎要倾倒。为什么会这样呢?回忆一下你学过的物理知识,还记得牛顿第二定律和运动学知识吗?前面的程序总是直接就给机器人伺服电机输出最大速度控制命令。根据运动学知识,一个物体要从零加速到最大运动速度时,时间越短,所需加速度就越大。而根据牛顿定律,加速度越大,物体所受的惯性力就越大。因此,前面的程序因为没有给机器人足够的加速时间,所以受到的惯性力就比较大,从而导致机器人在启动和停止有一个较大的前倾力或者后坐力。要消除这种情况,就必须让机器人速度渐渐增加或渐渐减小。采用均匀加速/减速是一种比较好的速度控制策略,这样不仅可以让机器人运动得更加平稳,还可以增加机器人电机的使用寿命。
编写匀加速运动程序
匀加速运动程序片段示例:
for(pulseCount=10;pulseCount<=200;pulseCount=pulseCount+10) { GPIO_SetBits(GPIOD, GPIO_Pin_10); delay_nus(1500+pulseCount); GPIO_ResetBits(GPIOD,GPIO_Pin_10); GPIO_SetBits(GPIOD, GPIO_Pin_9); delay_nus(1500-pulseCount); GPIO_ResetBits(GPIOD,GPIO_Pin_9); delay_nms(20); }
上述for循环语句能使机器人的速度由停止到全速。循环每重复执行一次,变量pulseCount就增加10:第一次循环时,变量pulseCount的值是10,此时发给PD10、PD9的脉冲的宽度分别为1.51ms、1.49ms;第二次循环时,变量pulseCount的值是20,此时发给PD10、PD9的脉冲的宽度分别为1.52ms、1.48ms。随着变量pulseCount值的增加,电机的速度也在逐渐增加。到执行第20次循环时,变量pulseCount的值是200,此时发给PD10、PD9的脉冲的宽度分别为1.7ms、1.3ms,电机全速运转。
回顾第2章任务三,for循环也可以由高向低计数。你可以通过使用for(pulseCount=200;pulseCount>=0; pulseCount=pulseCount-10)来实现速度的逐渐减小。下面是一个使用for循环来实现电机速度逐渐增加到全速然后逐步减小的例子。
例程:StartAndStopWithRamping.c
#include "stm32f10x_heads.h" #include "HelloRobot.h" int main(void) { int pulseCount; BSP_Init(); USART_Configuration(); printf("Program Running!\n"); for(pulseCount=10;pulseCount<=200;pulseCount=pulseCount+10) { GPIO_SetBits(GPIOD,GPIO_Pin_10); delay_nus(1500+pulseCount); GPIO_ResetBits(GPIOD,GPIO_Pin_10); GPIO_SetBits(GPIOD,GPIO_Pin_9); delay_nus(1500-pulseCount); GPIO_ResetBits(GPIOD,GPIO_Pin_9); delay_nms(20); } for(pulseCount=1;pulseCount<=75;pulseCount++) { 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); } for(pulseCount=200;pulseCount>=0;pulseCount=pulseCount-10) { GPIO_SetBits(GPIOD,GPIO_Pin_10); delay_nus(1500+pulseCount); GPIO_ResetBits(GPIOD,GPIO_Pin_10); GPIO_SetBits(GPIOD,GPIO_Pin_9); delay_nus(1500-pulseCount); GPIO_ResetBits(GPIOD,GPIO_Pin_9); delay_nms(20); } while(1); }
● 输入、保存并运行程序StartAndStopWithRamping.c;
● 验证机器人是否逐渐加速到全速,保持一段时间,然后逐渐减速到停止。
任务四 用函数调用简化运动程序
在第4章,你的机器人将需要执行各种运动来避开障碍物和完成其他动作。不过,无论机器人要执行何种动作,都离不开前面讨论的各种基本动作。为了各种应用程序方便使用这些基本动作程序,你可以将这些基本动作放在函数中,供其他函数调用来简化程序。
C语言提供了强大的函数定义功能。一个C程序就是由一个主函数和若干个其他函数构成,由主函数调用其他函数,其他函数也可以相互调用。同一个函数可以被一个或多个函数调用任意多次。实际上,为了实现复杂的程序设计,在所有的计算机高级语言中都有子程序或者子过程的概念。在C语言程序中,子程序的作用就是由函数来完成的。从函数定义的角度来看,函数有两种:
(1)标准函数,即库函数。由开发系统提供。用户不必自己定义而直接使用,只需在程序前包含有该函数原型的头文件即可在程序中直接调用,如前面已经用到的串口标准输出函数(printf)。应该说明,不同的语言编译系统提供的库函数的数量和功能会有一些不同,但许多基本函数是共同的。
(2)用户定义函数,以解决你的专门需要。不仅要在程序中定义函数本身,而且在主调函数模块中还必须对该被调函数进行类型说明,然后才能使用。
main函数的返回值
前面说过,main函数是不能被其他函数调用的,那它的返回值类型int是怎么回事呢?
其实不难理解,main函数执行完之后,它的返回值是给操作系统的。虽然在main函数体内并没有什么语句来指出返回值的大小,但系统默认的处理方式是:当main函数成功执行,它的返回值为1;否则为0。
现在看看下面的函数定义:
void Forward(void)
{
int i;
for(i=1;i<=65;i++)
{
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);
}
}
Forward函数可以使机器人向前运动约1.5s,该函数没有形参,也没有返回值。在主程序中,你可以调用它来让你的机器人向前运动约1.5s。但是这个函数并没有太大的使用价值,如果你想让你的机器人向前运动2s,该怎么办呢?是重新写一个函数来实现这个运动吗?当然不是!通过修改上面的函数,给它增加两个形式参数,一个是脉冲数量,另一个是速度参数,这样主程序调用时就可以按照你的要求灵活设置这些参数,从而使函数真正成为一个有用的模块。重新定义向前运动函数如下:
void Forward(int PulseCount,int Velocity) //Velocity should be between 0 and 200 { int i; for(i=1;i<=PulseCount;i++) { GPIO_SetBits(GPIOD, GPIO_Pin_10); delay_nus(1500+Velocity); GPIO_ResetBits(GPIOD,GPIO_Pin_10); GPIO_SetBits(GPIOD, GPIO_Pin_9); delay_nus(1500-Velocity); GPIO_ResetBits(GPIOD,GPIO_Pin_9); delay_nms(20); } }
函数定义旁有一行注释,提醒你在调用该函数时,速度参量的值必须在0~200之间。
注释符
除“//”外,C语言还提供了另一种语句注释符——“/*”和“ */”。
“/*”和“*/”必须成对使用,在它们之间的内容将被注释掉。它的作用范围比“//”大:“//”仅仅对它所在的一行起注释作用;但“/*…*/”可以对多行注释。
注释是你在学习程序设计时要养成的良好习惯。
下面是一个完整的使用向前、左转、右转和向后四个函数的例程。
例程:MovementsWithFunctions.c
● 输入、保存、编译、下载并运行程序MovementsWithFunctions.c。
#include "stm32f10x_heads.h" #include "HelloRobot.h" void Forward(int PulseCount,int Velocity) // Velocity should be between 0 and 200 { int i; for(i=1;i<= PulseCount;i++) { GPIO_SetBits(GPIOD, GPIO_Pin_10); delay_nus(1500+ Velocity); GPIO_ResetBits(GPIOD,GPIO_Pin_10); GPIO_SetBits(GPIOD, GPIO_Pin_9); delay_nus(1500- Velocity); GPIO_ResetBits(GPIOD,GPIO_Pin_9); delay_nms(20); } } void Left(int PulseCount,int Velocity) { int i; for(i=1;i<= PulseCount;i++) { GPIO_SetBits(GPIOD, GPIO_Pin_10); delay_nus(1500-Velocity); GPIO_ResetBits(GPIOD,GPIO_Pin_10); GPIO_SetBits(GPIOD, GPIO_Pin_9); delay_nus(1500-Velocity); GPIO_ResetBits(GPIOD,GPIO_Pin_9); delay_nms(20); } } void Right(int PulseCount,int Velocity) { int i; for(i=1;i<= PulseCount;i++) { GPIO_SetBits(GPIOD, GPIO_Pin_10); delay_nus(1500+Velocity); GPIO_ResetBits(GPIOD,GPIO_Pin_10); GPIO_SetBits(GPIOD, GPIO_Pin_9); delay_nus(1500+Velocity); GPIO_ResetBits(GPIOD,GPIO_Pin_9); delay_nms(20); } } void Backward(int PulseCount,int Velocity) { int i; for(i=1;i<= PulseCount;i++) { GPIO_SetBits(GPIOD, GPIO_Pin_10); delay_nus(1500-Velocity); GPIO_ResetBits(GPIOD,GPIO_Pin_10); GPIO_SetBits(GPIOD, GPIO_Pin_9); delay_nus(1500+ Velocity); GPIO_ResetBits(GPIOD,GPIO_Pin_9); delay_nms(20); } } int main(void) { BSP_Init(); USART_Configuration(); printf("Program Running!\n"); Forward(65,200); Left(26,200); Right(26,200); Backward(65,200); while(1); }
这个程序的运行结果与程序ForwardLeftRightBackward.c产生的效果是相同的。你有没有发现四个函数的具体实现是不是有些啰嗦。四个函数的具体实现部分几乎完全一样,有没有可能将这些函数进一步归纳,用一个函数来实现所有这些功能呢?当然有,前面的四个函数都用了两个形式参数,一个是控制时间的脉冲个数,另一个是控制运动速度的参数,而四个函数实际上代表了四个不同的运动方向。如果能够通过参数控制运动方向,显然这四个函数就完全可以简化成为一个更为通用的函数,它不仅可以涵盖以上四个基本运动,同时还可以使机器人朝你希望的方向运动。
由于机器人由两个轮子驱动,实际上两个轮子的不同速度组合控制着机器人的运动速度和方向,因此可以直接用两个车轮的速度作为形式参数,就可以将所有的机器人运动用一个函数来实现。
例程:MovementsWithOneFuntion.c
这个例子使你的机器人做同样动作,但是它只用了一个子函数来实现。
#include "stm32f10x_heads.h" #include "HelloRobot.h" void Move(int counter,int PC1_pulseWide,int PC0_pulseWide) { int i; for(i=1;i<=counter;i++) { GPIO_SetBits(GPIOD, GPIO_Pin_10); delay_nus(PC1_pulseWide); GPIO_ResetBits(GPIOD,GPIO_Pin_10); GPIO_SetBits(GPIOD, GPIO_Pin_9); delay_nus(PC0_pulseWide); GPIO_ResetBits(GPIOD,GPIO_Pin_9); delay_nms(20); } } int main(void) { BSP_Init(); USART_Configuration(); printf("Program Running!\n"); Move(65,1700,1300); Move(26,1300,1300); Move(26,1700,1700); Move(65,1300,1700); while(1); }
● 输入、保存并运行程序MovementsWithOneFuntion.c;
● 你的机器人是否执行了你熟悉的前、左、右、后运动呢?
任务五 高级主题——用数组建立复杂运动
到目前为止你已经试过三种不同的编程方法来使机器人向前走,左转,右转和向后走。每种方法都有它的优点,但是如果你要让机器人执行一个更长,更复杂的动作时用这些方法都很麻烦。下面要介绍的两个例子将用子函数来实现每个简单的动作,将复杂的运动存储在数组中,然后在程序执行过程中读出并解码。避免了重复调用一长串子函数。这里,你要用到C语言的数据类型——数组。
在程序设计中,为了处理方便,可以把具有相同类型的若干变量按有序的形式组织起来。这些按序排列的同类数据元素的集合称为数组。一个数组可以分解为多个数组元素,根据数组元素数据类型的不同,数组可以分为多种不同类型。数组又分为一维数组、二维数组甚至三维数组。本节只会用到一维数组。例如,下面的语句定义了一个字符型数组,该数组有10个元素,并对这10个元素进行了初始化。
char Navigation[10]={'F','L','F','F','R','B','L','B','B','Q'};
字符串和字符串结束标志
在C语言中没有专门的字符串变量,通常用一个字符数组或字符指针来存放一个字符串。字符串常量在存储时,系统自动在字符串的末尾加一个“串结束标志”,即ASCII码值为0的字符NULL,常用“\0”表示。因此在程序中,长度为n字符的字符串常量在内存中占有n+1个字节的存储空间。C语言允许用字符串的方式对数组作初始化赋值,如Navigation[10]的初始化赋值可写为:
char Navigation[10]={“FLFFRBLBBQ”};
或者去掉“{}”,写为:char Navigation[10]=“FLFFRBLBBQ”;
要特别注意字符与字符串的区别,除了表示形式不同外,其存储性质也不相同,字符“A”只占1字节,而字符串“A”占2字节。
下面的例程采用字符数组定义一系列复杂的运动。
例程:NavigationWithSwitch.c
● 输入、保存、编译、下载并运行程序NavigationWithSwitch.c。
#include "stm32f10x_heads.h" #include "HelloRobot.h" void Forward(void) { int i; for(i=1;i<=65;i++) { 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) { char Navigation[10]={'F','L','F','F','R','B','L','B','B','Q'}; int address=0; BSP_Init(); USART_Configuration(); printf(" Program Running!\n"); while(Navigation[address]!='Q') { switch(Navigation[address]) { case 'F':Forward();break; case 'L':Left_Turn();break; case 'R':Right_Turn();break; case 'B':Backward();break; } address++; } while(1); }
你的机器人是否走了一个矩形?如果它走得更像一个梯形,你可能需要调节转动程序中for循环的循环次数,使其旋转精确的90度。
在程序主函数中定义了一个字符数组,这个数组中存储的是一些命令:F表示向前运动,L表示向左转,R表示向右转,B表示向后退,Q表示程序结束。之后,定义了一个int型变量address,用来作为访问数组的索引。接着是一个while循环,注意到这个循环的条件表达式与前面的不同:只有当前访问的数组值不为Q时,才执行循环体内的语句。在循环内,每次执行switch语句后,都要更新address,以使下次循环时执行新的运动。
当Navigation[address]为“F”时,执行向前运动的函数Forward();当Navigation[address]为“L”时,执行向左转的函数Left_Turn();当Navigation[address]为“R”时,执行向右转的函数Right_Turn();当Navigation[address]为“B”时,执行向后运动的函数Backward()。你可以增加或删除数组中的字符来获取新的运动路线。记住:数组中的最后字符应该是“Q”。
例程:NavigationWithValues.c
在本例程中,将不使用子函数,而是使用三个整型数组来存储控制机器人运动的三个变量,即循环的次数和控制左右电机运动的两个参数。具体定义如下:
int Pulses_Count[5]={65,26,26,65,0};
int Pulses_Left[4]={1700,1300,1700,1300};
int Pulses_Right[4]={1300,1300,1700,1700};
int型变量address作为访问数组的索引值,每次用address提取一组数据:Pulses_Count[address],Pulses_Left[address],Pulses_Right[address],这些变量值被放在下面的代码块中,作为机器人运动一次的参数。
for(int counter=1;counter<=Pulses_Count[address];counter++) { GPIO_SetBits(GPIOD, GPIO_Pin_10); delay_nus(Pulses_Left[address]); GPIO_ResetBits(GPIOD,GPIO_Pin_10); GPIO_SetBits(GPIOD, GPIO_Pin_9); delay_nus(Pulses_Right[address]); GPIO_ResetBits(GPIOD,GPIO_Pin_9); delay_nms(20); }
address加1,再提取一组数据,作为机器人下次运动的参数。以此继续直至Pulses_Count[address]=0时,机器人停止运动。具体程序如下:
#include "stm32f10x_heads.h" #include "HelloRobot.h" int main(void) { int Pulses_Count[5]={65,26,26,65,0}; int Pulses_Left[4]={1700,1300,1700,1300}; int Pulses_Right[4]={1300,1300,1700,1700}; int address=0; int counter; BSP_Init(); USART_Configuration(); printf("Program Running!\n"); while(Pulses_Count[address]!=0) { for(counter=1;counter<=Pulses_Count[address];counter++) { GPIO_SetBits(GPIOD, GPIO_Pin_10); delay_nus(Pulses_Left[address]); GPIO_ResetBits(GPIOD,GPIO_Pin_10); GPIO_SetBits(GPIOD, GPIO_Pin_9); delay_nus(Pulses_Right[address]); GPIO_ResetBits(GPIOD,GPIO_Pin_9); delay_nms(20); } address++; } while(1); }