2.4 STM32单片机I/O端口的应用
任务五 机器人伺服电机控制信号
控制伺服电机转动速度的脉冲信号如图2.10、图2.11和图2.12所示。
图2.10 电机转速为零的控制信号时序图
图2.11 1.3ms的控制脉冲序列使电机顺时针全速旋转
图2.12 1.7ms的连续脉冲序列使电机逆时针全速旋转
图2.10所示是高电平持续1.5ms低电平持续20ms,然后不断重复地控制脉冲序列。该脉冲序列发给经过零点标定后的伺服电机,伺服电机不会旋转。如果此时你的电机旋转,表明电机需要标定。此时,你可调节伺服电机的可调电阻使电机停止旋转。控制电机运动转速的是高电平持续的时间,当高电平持续时间为1.3ms时,电机顺时针全速旋转,当高电平持续时间1.7ms时,电机逆时针速旋转。下面你将看到如何给STM32单片机微控制器编程使PD端口的第10脚(PD10)来发出伺服电机的控制信号。
在进行下面的实验之前,你必须首先确认一下机器人两个伺服电机的控制线是否已经正确地连接到了STM32单片机教学开发板的两个专用电机控制接口上。按照图2.13所示的电机连接原理图和实际接线图进行检查。“黑线”表示地线,“红线”表示电源线,“白线”表示信号线。PD9用来控制左边的伺服电机,而PD10引脚则用来控制右边的伺服电机。
图2.13 伺服电机与教学开发板的连接原理图(左)和实际接线图(右)
显然这里对微控制器编程发给伺服电机的高、低电平信号必须具备更精确的时间。因为单片机只有整数,没有小数,所以要生成伺服电机的控制信号,要求具有比delay_nms()函数的时间更精确的函数,这就需要用另一个延时函数delay_nus()。前面已经介绍过,这个函数可以实现更小的延时,它的延时单位是微秒,即千分之一毫秒,参数n为延时微秒数。
看看下面的代码片断:
while (1) { GPIO_SetBits(GPIOD,GPIO_Pin_10); //PD10输出高电平 delay_nus(1500); //延时1500μs GPIO_ResetBits(GPIOD,GPIO_Pin_10); //PD10输出低电平 delay_nus(20000); //延时20ms }
如果用这个代码段代替例程Led_Blink.c中相应程序片断,它是不是就会输出图2.6所示的脉冲信号?肯定是!如果你手边有个示波器,可以用示波器观察PD10脚输出的波形是不是如图2.6所示。此时,连接到该脚的机器人轮子是不是静止不动。如果它在慢慢转动,就说明你的机器人伺服电机可能没有经过调整。
同样,用下面的程序片断代替例程Led_Blink.c中相应程序片断,编译、连接下载执行代码,观察连接到PD10脚的机器人轮子是不是顺时针全速旋转。
while(1) { GPIO_SetBits(GPIOD,GPIO_Pin_10); //PD10输出高电平 delay_nus(1300); //延时1500μs GPIO_ResetBits(GPIOD,GPIO_Pin_10); //PD10输出低电平 delay_nus(20000); //延时20ms }
用下面的程序片断代替例程Led_Blink.c中相应程序片断,编译、连接下载执行代码,观察连接到PD10脚的机器人轮子是不是逆时针全速旋转。
while (1) { GPIO_SetBits(GPIOD,GPIO_Pin_10); //PD10输出高电平 delay_nus(1700); //延时1500μs GPIO_ResetBits(GPIOD,GPIO_Pin_10); //PD10输出低电平 delay_nus(20000); //延时20ms }
该你了——让机器人的两个轮子全速旋转
刚才是让连接到PD10脚的伺服电机轮子全速旋转,下面你自己可以修改程序让连接到PD9机器人轮子全速旋转。
当然,最后你需要修改程序,让机器人的两个轮子都能够旋转。让机器人两个轮子都顺时钟全速旋转的程序参考下面的程序。
例程:BothServo.c
● 接通板上的电源,输入、保存、下载并运行程序(整个过程请参考第1章);
● 观察机器人小车的运动行为。
#include "stm32f10x_heads.h" #include "HelloRobot.h" int main(void) { BSP_Init(); USART_Configuration(); printf("Program Running!\n"); while (1) { GPIO_SetBits(GPIOD, GPIO_Pin_10); GPIO_SetBits(GPIOD, GPIO_Pin_9); delay_nus(1300); GPIO_ResetBits(GPIOD,GPIO_Pin_10); GPIO_ResetBits(GPIOD,GPIO_Pin_9); delay_nms(20); } }
注意:上述程序用到了两个不同的延时函数,效果与前面例子一样。运行上述程序时,你是不是对机器人的运动行为感到惊讶!
任务六 计数并控制循环次数
任务五中已经通过对STM32单片机编程实现了对机器人伺服电机的控制,为了让微控制器不断发出控制指令,你用到了以while(1)开头的死循环(即永不结束的循环)。不过在实际的机器人控制过程中,你会经常要求机器人运动一段给定的距离或者一段固定的时间。这时就需要你能控制代码执行的次数。
最方便的控制一段代码执行次数的方法是利用for循环,语法如下:
for(表达式1;表达式2;表达式3) 语句
例如,下面是一个用整型变量myCounter来计数的for循环程序片断。每执行一次循环,它会显示myCounter的值。
for(myCounter=1; myCounter<=10; myCounter++) { printf(“%d”,myCounter); delay_nms(500); }
该你了——不同的初始值和终值及计数步长
你可以修改表达式3来使myCounter以不同步长计数,而不是按9,10,11,…来计,你可以让它每次增加2(9,11,13,…)或增加5(10,15,20,…)或任何你想要的步进,递增或递减都可以。下面的例子是每次减3。
for(myCounter=21; myCounter>=9; myCounter=myCounter-3) { printf(“%d\n”,myCounter); delay_nms(500); }
for循环控制电机的运行时间
到目前为止,你已经理解了利用脉冲宽度调制(Pulse Width Modulation,PWM)来控制电机旋转速度和方向的原理。控制电机速度和方向的方法是非常简单的。控制电机运行的时间也非常简单,那就是用for循环。下面是for循环的例子,它会使电机运行几秒钟。
for(Counter=1;Counter<=100;i++) { GPIO_SetBits(GPIOD, GPIO_Pin_10); delay_nus(1700); GPIO_ResetBits(GPIOD,GPIO_Pin_10); delay_nms(20); }
让我们来计算一下这个代码能使电机转动的确切的时间长度。每通过循环一次,delay_nus(1700)持续1.7 ms,delay_nms(20)持续20ms,其他语句的执行时间很少,可忽略。那么for循环整体执行一次的时间是:1.7ms+20ms=21.7ms,本循环执行100次,即就是21.7ms乘以100,时间=100×21.7ms=100×0.0217s=2.17s。
例程:ControlServoRunTimes.c
● 输入、保存并运行程序;
● 验证是否与PD10连接的电机逆时针旋转2.17s,然后与PD9连接的电机旋转4.34s。
int main(void) { int Counter; BSP_Init(); USART_Configuration(); printf("Program Running!\n"); for(Counter=1;Counter<=100;Counter++) { GPIO_SetBits(GPIOD, GPIO_Pin_10); delay_nus(1700); GPIO_ResetBits(GPIOD,GPIO_Pin_10); delay_nms(20); } for(Counter=1;Counter<=200;Counter++) { GPIO_SetBits(GPIOD, GPIO_Pin_9); delay_nus(1700); GPIO_ResetBits(GPIOD,GPIO_Pin_9); delay_nms(20); } while(1); }
假如你想让两个电机同时运行,给与PD10连接的电机发出1.7ms的脉宽,给与PD9连接的电机发出1.3ms的脉宽,现在每通过循环一次要用的时间是:
1.7ms——与PD10连接的电机
1.3ms——与PD8连接的电机
20 ms——中断持续时间
---------- ------------------------------
一共是23 ms
假如你想让电机运行3s,计算如下:脉冲数量=3 / 0.023= 130
现在,你可以将for循环中作如下修改,程序如下:
for(counter=1;counter<=130;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); }
例程:BothServosThreeSeconds.c
下面是一个使电机向一个方向旋转3s,然后反向旋转的例子。
● 输入、保存并运行程序。
int main(void) { int counter; BSP_Init(); USART_Configuration(); printf("Program Running!\n"); for(counter=1;counter<=130;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<=130;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); }
验证每个机器人是否沿一个方向运行3s然后反方向运行3s。你是否注意到当电机同时反向的时候,它们总是保持同步运行?这将有什么作用呢?
任务七 用你的计算机来控制机器人的运动
在工业自动化、测控等领域,经常需要你的单片机与计算机通信。一方面,单片机需要读取周边传感器的信息,并把数据传给计算机;另一方面,计算机需要解释和分析传感器数据,然后把分析结果或者决策发给单片机以执行某种操作。
在第1章中你已经知道STM32单片机可以通过串口向计算机发送信息,本章将使用串口和串口调试终端软件,从计算机向单片机发送数据来控制机器人的运动。
在本任务中,你需要编程让STM32单片机从调试窗口接收两个数据:发给伺服舵机的脉冲个数和脉冲宽度(以微秒为单位)。
例程:ControlServoWithComputer.c
● 输入、保存、下载并运行程序ControlServoWithComputer.c;
● 验证是否机器人各个轮子的转动是不是同你期望的运动一样。
#include "stm32f10x_heads.h" #include "HelloRobot.h" unsigned int USART_Scanf() { u16 index = 0; u16 recdata; u16 tmp[5]={0,0,0,0,0}; while(1) { /* Wait until RXNE = 1 */ while(USART_GetFlagStatus(USART1,USART_FLAG_RXNE)==RESET); tmp[index] = (USART_ReceiveData(USART1)); if(tmp[index] == '#') { if(index!=0) break; else continue; } index++; } /* Calculate the Corresponding value */ if(index==1) recdata = (tmp[0] -0x30); if(index==2) recdata = (tmp[1] -0x30) + ((tmp[0] -0x30) * 10); if(index==3) recdata = (tmp[2] -0x30) + ((tmp[1] -0x30) * 10)+ ((tmp[0] -0x30) * 100); if(index==4) recdata = (tmp[3] -0x30) + ((tmp[2] -0x30) * 10)+ ((tmp[1] -0x30) * 100)+ ((tmp[0] -0x30) * 1000); return recdata; } int main(void) { int Counter; unsigned int PulseNumber,PulseDuration; BSP_Init(); USART_Configuration(); printf("Program Running!\r\n"); printf("Please input pulse number:\r\n"); PulseNumber=USART_Scanf(); printf("Input pulse number is %d\r\n",PulseNumber); printf("Please input pulse duration:\r\n"); PulseDuration=USART_Scanf(); printf("Input pulse duration is %d\r\n",PulseDuration); for(Counter=1;Counter<=PulseNumber;Counter++) { GPIO_SetBits(GPIOD, GPIO_Pin_10); delay_nus(PulseDuration); GPIO_ResetBits(GPIOD,GPIO_Pin_10); delay_nms(20); } for(Counter=1;Counter<=PulseNumber;Counter++) { GPIO_SetBits(GPIOD, GPIO_Pin_9); delay_nus(PulseDuration); GPIO_ResetBits(GPIOD,GPIO_Pin_9); delay_nms(20); } while(1); }
ControlServoWithComputer.c是如何工作的?
在这个程序中,单片机不仅向串口传送信息给你看,而且还通过串口从计算机读取输入的数据。串口接收数据函数USART_Scanf的工作机制将在后面的章节讲解,其作用是从计算机接收数据,计算机向单片机发出的数据以#作为结束标记,就像使用充值卡给电话(手机)充值时,输入完账号和密码按#表示结束,如图2.14所示。
图2.14 例程运行过程
注意break和continue的区别:break是结束整个循环体,而continue是结束本次循环。
(1)首先输出“Program Running!”和“Please input pulse number:”;
(2)程序处于等待状态,等待你从串口调试软件输入数据,数据以“#”作为结束标记;
(3)将输入的数据给变量PulseNumber,再把这个数据回传给计算机显示;
(4)输出“Please input pulse duration:”;
(5)又处于等待状态,等待你从串口调试软件再次输入数据,数据以“#”作为结束标记;
(6)将输入的数据给变量PulseDuration,再把这个数据回传给计算机显示;
(7)电机运转。