一、 综述:
a) 项目名称:小车自动走迷宫;
b) 小组成员:
i. 周杰:负责总体规划,及ARM编程;
ii. 宋大成:负责车轮驱动;
iii. 陈潇:负责红外驱动;
c) 小车图片:
MicroMouse615:
MicroMouse120:
二、 项目介绍:
电脑鼠走迷宫竞赛的目的是制作一个微型机器人,它能在最短的时间内穿越迷宫到达终点。参赛的机机器人称为“电脑鼠”,将电脑鼠放入迷宫并启动操作的人称为“操作员”。
电脑鼠的基本功能是从起点开始走到终点,这个过程称为一次“运行”,所花费的时间称为“运行时间”。从终点回到起点所花费的时间不计算在运行时间内。从电脑鼠的第一次激活到每次运行开始,这段期间所花费的时间称为“迷宫时间”。如果电脑鼠在比赛时需要手动辅助,这个动作称为“碰触”。竞赛使用这三个参数,从速度﹑求解迷宫的效率和电脑鼠的可靠性三个方面来进行评分。器人称为“电脑鼠”,将电脑鼠放入迷宫并启动操作的人称为“操作员”。
在G02小组的工作中,基本完成了电脑鼠的驱动部分和算法部分:电脑鼠可以进行直线行走、90度左转、90度右转和180度后转;能准确监测左前右三个方向的挡板,能准确判断左右间距是否恰当;能进行调距运动,以保持与左右挡板的距离;能简单判断通路,并计算记忆合适通路;并按照通路完成迷宫行走。
但是,由于某些原因,小车的没有取得完美的效果:由于电机转速控制不当,小车直线行走效果不佳(参看第条);左转右转不够准确,只能依靠调整函数补偿;算法不够优秀,正在探究更好的算法。这些问题都在努力克服中,我们不会因为检查完毕就放弃小车的调试。
三、 项目整体结构:
MicroMouse102 电脑老鼠,采用美国LuminaryMicro 公司生产的32 位ARM CortexM3处理器LM3S102,控制和检测红外传感器;主CPU 根据检测到的传感信号,控制电机驱动电路调整行走路径,直到到达终点。
四、 硬件部分介绍:
LED 电路
电脑鼠有5 个独立的LED,通过LM3S 系统单片机的GPIO 口直接控制,如图 1.6 所示。电路采用了I/O 口灌电流的驱动方式来驱动LED,LM3S 系统单片机的灌电流为2~8mA(可配置),所以不需要驱动就可以点亮LED。GPIO 引脚输出高电平时LED 熄灭,低电平时LED 点亮。
电机驱动电路
电机采用直流减速电机,最高输出转速为800 转/分钟,工作电压为DC3V。电机驱动
电路采用专用的单相直流电动机桥式驱动芯片。
车速检测电路
车速检测用于检测并记录车体运行的路径,通过车速检测记录车体做迷宫的坐标,同
时也起到控制车速和保持左右双轮的速度一致。
检测原理:在左轮和右轮的内则都贴有的光电码盘,码盘由两种颜色组成白色和黑色。
红外发射管安装在车轮光电检测码盘的检测区域,当红外发射与接收管正对着黑色边时,
红外线没有被反射,接收管的电阻很大;当红外发射与接收管正对着白色边时,红外线被
反射,接收管的电阻很小。
红外检测电路
红外检测电路是用于迷宫挡板的检测,分为左侧、右侧、前方三个方向,三个方向的
检测原理相同,某一个方向的检测电路。
CPU 及晶振电路
电脑鼠的单片机、晶体振荡器和LDO输出原理如图所示。该单片机选用LM3S102
微处理器。
五、 软件部分介绍:
一体化红外接收头工作原理
一体式红外线接收传感器IRM8601S,它内部集成自动增益控制电路、带通滤波电路、
解码电路及输出驱动电路。当连续收到38KHz 的红外线信号时,将产生脉宽10ms 左右的
低电平。如果没有收到信号,便立即输出高电平。Send 为发射控制端,高
电平时发射38KHz 的红外信号。Out 为接收输出端,低电平表示收到信号。
检测障碍物的软件设计
根据接收头是否检测到经过反射的红外线信号,就可以判断是否存在障碍物。由于接
收头检测到信号时只产生一个负脉冲,所以只需要在检测时使能红外线发射,一次检测结
束后使能无效,程序设计参考流程图如图2.5 所示。
接收头有一定的
响应时间
开始发送38KHz
的红外线
迷宫挡板检测
调制信号产生
本设计中采用定时器1 产生38KHz 的调制信号,由PB5 输出,该端口连接到图2.3 中
的Pulse 端口。在中断中翻转PB5 输出信号,所以要产生频率为f 的脉冲,定时器的频率
要为2f。在本设计中要产生38KHz 的频率,定时器中断频率为76KHz。
程序清单 3.1 为定时器1 的初始化函数,程序清单 3.2 为中断服务函数,在这里翻转
PB5 口输出状态。
程序清单 3.1 定时器1 初始化
void PULSEIni(void)
{
GPIODirModeSet(GPIO_PORTB_BASE, SEND | PULSE, GPIO_DIR_MODE_OUT); // 设置为输
出
GPIOPinWrite( GPIO_PORTB_BASE,SEND | PULSE,0); // 红外线初始时停止发射
SysCtlPeripheralEnable( SYSCTL_PERIPH_TIMER1 ); // 使能定时器1 外设
TimerConfigure(TIMER1_BASE, TIMER_CFG_32_BIT_PER); // 设置定时器1 为周期触发
TimerLoadSet(TIMER1_BASE, TIMER_A, SysCtlClockGet()/76000); // 设置定时器装载值
TimerIntEnable(TIMER1_BASE, TIMER_TIMA_TIMEOUT);
TimerEnable(TIMER1_BASE, TIMER_A);
IntEnable(INT_TIMER1A);
}
程序清单 3.2 定时器1 服务函数
void Timer1A_ISR(void)
{
TimerIntClear(TIMER1_BASE, TIMER_TIMA_TIMEOUT); // 清除定时器1 中断
GPIOPinWrite(GPIO_PORTB_BASE, PULSE,GPIOPinRead(GPIO_PORTB_BASE, PULSE) ^ PULSE);
// 翻转GPIO B5 端口
}
抗干扰处理
红外线在空气中传播和反射受外界的干扰,如果测量距离刚好处在能够检测到信号的
临界状态,保持距离不变,传感器输出信号也可能不确定。这样就需要在软件中进行抗干
扰处理。参考程序如程序清单3.3 所示。
程序清单3.3 抗干扰处理程序
GPIOPinWrite( GPIO_PORTB_BASE,SEND , SEND); // 发送脉冲
Delay(150); // 延时
for(i=0,j=0;i
{
if(GPIOPinRead(GPIO_PORTA_BASE, OUT_L)==0)
j++;
}
GPIOPinWrite( GPIO_PORTB_BASE,SEND , ~SEND); // 停止发送
if(j>5) // 左边存在挡板
{
}
else // 左边存在支路
{
}
图3.1 为抗干扰程序在Micromouse 中运行后用逻辑分析仪抓到的波形图,Pulse 为
38KHz 的输出信号,Send 高电平有效,有效时发送红外线脉冲,OUT 为一体化接收头输
…… ……
出端,该图所示为接收头探测到障碍物,软件在Send 信号无效(下降沿)前完成检测OUT
输出信号,从图中可以看出,此时正处于OUT 有效信号的中间,所以软件里延时参数能
保证正确检测到信号。
图3.1 传感器检测波形图
软件设计参考
为用一组红外实现两组参数(是否存在挡板和是否太接近挡板)的检测流程图。在Micromouse 中,用到了三组(左、前、右)反射式红外检测传感器,左边和右边的传感器各自都需要检测两组参数,而前方的传感器只需要探测有无挡板,存在挡板就必须根据策略转换行进方向,若不存在就可以继续前进。如图3.3 所示为Micromouse 红外检测的程序设计流程图。红外检测参考程序见程序清单 3.4 所示,该程序中使用了五个LED 用来指示传感器检测的状态,由于这几个LED 硬件上连接到JTAG,关于如何切换GPIO和JTAG功能参见6 使用JTAG 引脚作GPIO。
此频率仅作为参考,要根据实际检测距离来确定。结合可调电阻R1 变可以实现挡板和防碰撞的检测。
程序清单 3.4 Micromouse 红外检测函数(见附录)
电机的调速
电机的调速
直流电机的转速控制在本设计中通过PWM来控制,LM3S102 单片机则刚有两路PWM
输出,非常适合用于控制两个电机的转速。
两路PWM 是LM3S102 通用定时器0(Timer0)的三种工作模式之一,16 位PWM 模
式。该模式是将一个32 位的定时器,折分成两个16 位的定时器TimerA 和TimerB。这些
定时器为计数寄存器(GPTMTnR)递减计数,递减到0 时自动加载预装载值(GPTMTnILR)。
当然预装载值也是由用户设定,该直也就决定了定时周期,也即PWM 的输出周期。
当计数器的值与预装载值相等时,输出PWM 信号有效,当计数器的值与匹配寄存器
(GPTMnMATCHR)的值相等时,输出PWM 信号失效。通过软件可以设定PWM 输的信
号有效和信号无效的电平状态。当GPTMCTL 寄存器的TnPWML 位值为0 时,信号有效
为高电平,信号无效为低电平;TnPWML 位值为1 时,则反之。如图 4.1 所示。
输出信号
计数
0x411A
0xC350
TnPWML=0
TnPWML=1
TnEN置位
GPTMTnR=GPTMnMR GPTMTnR=GPTMnMR
时间
图 4.1 16 位PWM 模式输出
占空比的约定:占空比为在一个周期内,输出有信号有效电平占整个周期时间的比率。
在这里为以统一软件控制的约定,用户API 函数输入的占空比值越大,电机转速越快,正
向运行和反向运行都一样。
为了简化占空比输出的计算,将计数寄存器与匹配寄存器值相等时,输出的电平信号
为驱动电机的有效信号。例如将PWM 周期时间设定为60000 个时钟节拍,需要输出驱动
电机的占空比为75%,则设置匹配寄存器值为75*6000。
由于电机的转向不一样,所以电机驱动的有效电平也需要调整,通过控制TnPWML
实现。
2 程序设计
Timer0 的两路16 定时器TimerA 和TimerB 的PWM 输出引脚分别为PB0 和PB6,PB0
和PB6 分别控制左轮和右轮驱动器TA7291S 的IN1 引脚,而它们的IN2 引脚分别由GPIO
输出的PA4 和PA5 控制。
左轮的控制函数如程序清单 4.1 所示。
该函数的第1 个参数sel 为选择轮子的控制方式:0 为停止,1 为轮子向前,2 为轮子
向后;percen 参数为占空比,其最大值为99,最小值为1,对于轮子的停止控制该参数无
效。
程序清单 4.1 左轮控制函数
void LeftWheelRun(int sel,unsigned char percen)
{
switch(sel)
{
/*轮子停止转动*/
case 0:
TimerDisable(TIMER0_BASE,TIMER_A); // 禁止定时器
GPIOPinWrite(GPIO_PORTA_BASE,LWC2,0xff); // 控制引脚输出高电平
GPIODirModeSet(GPIO_PORTB_BASE, LWC1, GPIO_DIR_MODE_OUT); // GPIO 输出
GPIOPinWrite(GPIO_PORTB_BASE,LWC1, 0xff); // GPIO 输出高电平
break;
/*左轮向前*/
case 1:
GPIOPinWrite(GPIO_PORTA_BASE,LWC2, 0xff); // PA4 输出高电平
TimerControlLevel(TIMER0_BASE,TIMER_A,true); // PWM 有效电平方向
GPIODirModeSet(GPIO_PORTB_BASE, LWC1, GPIO_DIR_MODE_HW); // PWM 输出
TimerMatchSet(TIMER0_BASE,TIMER_A,percen*600); // 设置占空比
TimerEnable(TIMER0_BASE,TIMER_A); // 使能定时器
break;
/*左轮向后*/
case 2:
GPIOPinWrite(GPIO_PORTA_BASE,LWC2, 0); // PA4 输出低电平
TimerControlLevel(TIMER0_BASE,TIMER_A,false); // PWM 有效电平方向
GPIODirModeSet(GPIO_PORTB_BASE, LWC1, GPIO_DIR_MODE_HW); // PWM 输出
TimerMatchSet(TIMER0_BASE,TIMER_A,percen*600); // 设置占空比
TimerEnable(TIMER0_BASE,TIMER_A); // 使能定时器
break;
}
}
右轮的控制函数如程序清单 4.2 所示。
该函数的第1 个参数sel 为选择轮子的控制方式:0 为停止,1 为轮子向前,2 为轮子
向后;percen 参数为占空比,其最大值为99,最小值为1,对于轮子的停止控制该参数无
效。
程序清单 4.2 右轮控制函数
void RightWheelRun(int sel,unsigned char percen)
{
switch(sel)
{
/*轮子停止转动*/
case 0:
TimerDisable(TIMER0_BASE,TIMER_B); // 禁止定时器
GPIOPinWrite(GPIO_PORTA_BASE,RWC2,0xff); // 控制引脚输出高电平
GPIODirModeSet(GPIO_PORTB_BASE, RWC1, GPIO_DIR_MODE_OUT); // GPIO 输出
GPIOPinWrite(GPIO_PORTB_BASE,RWC1,0xff); // GPIO 输出高电平
break;
/*右轮向后*/
case 2:
GPIOPinWrite(GPIO_PORTA_BASE,RWC2, 0xff); // PA4 输出高电平
TimerControlLevel(TIMER0_BASE,TIMER_B,true); // PWM 有效电平方向
GPIODirModeSet(GPIO_PORTB_BASE, RWC1, GPIO_DIR_MODE_HW); // PWM 输出
TimerMatchSet(TIMER0_BASE,TIMER_B,percen*600); // 设置占空比
TimerEnable(TIMER0_BASE,TIMER_B); // 使能定时器
break;
/*右轮向前*/
case 1:
GPIOPinWrite(GPIO_PORTA_BASE,RWC2, 0); // PA4 输出低电平
TimerControlLevel(TIMER0_BASE,TIMER_B,false); // PWM 有效电平方向
GPIODirModeSet(GPIO_PORTB_BASE, RWC1, GPIO_DIR_MODE_HW); // PWM 输出
TimerMatchSet(TIMER0_BASE,TIMER_B,percen*600); // 设置占空比
TimerEnable(TIMER0_BASE,TIMER_B); // 使能定时器
break;
}
}
需要注意的是,当PWM 信号禁止后,其输出引脚的电平状态是保持静止时的状态(可
能为低电平也可能为高电平),导致电机可能不能停止,所以在制停电机时,需要将PWM
引脚改为GPIO 输出,并且出高电平,使电机刹车停止。
定时器PWM 初始化函数如程序清单 4.3 所示。
程序清单 4.3 定时器PWM 初始化
void PWMTimer0AIni(void)
{
SysCtlPeripheralEnable(SYSCTL_PERIPH_TIMER0); // 使能定时器0 模
块
GPIODirModeSet(GPIO_PORTB_BASE, LWC1|RWC1 , GPIO_DIR_MODE_OUT);
/* 控制引脚输出*/
GPIOPinWrite(GPIO_PORTB_BASE, LWC1|RWC1 , 0xff); // GPIO 输出高电
平
GPIODirModeSet(GPIO_PORTA_BASE, LWC2|RWC2 , GPIO_DIR_MODE_OUT); // 控制引脚输出
GPIOPinWrite(GPIO_PORTA_BASE, LWC2|RWC2 , 0xff); // GPIO 输出高电
平
/* 定时器配置*/
TimerConfigure(TIMER0_BASE,TIMER_CFG_16_BIT_PAIR
|TIMER_CFG_A_PWM|TIMER_CFG_B_PWM); // 16 位PWM 输出
TimerControlLevel(TIMER0_BASE,TIMER_A,false); //有效信号为低电
平
TimerControlLevel(TIMER0_BASE,TIMER_B,false);
TimerLoadSet(TIMER0_BASE,TIMER_A,60000); // 设定PWM 频率
TimerLoadSet(TIMER0_BASE,TIMER_B,60000);
}
Micromouse 车速检测
车速检测程序设计
本设计中选用了LM3S102 的PA0 和PB1 分别检测左轮和右轮的下降沿的脉冲个数,
为了快速向应检测信号,使用了下降沿触发中断。LM3S102 单片机的特点,任何一个GPIO
引脚都可以配置为中断输入,并且可以作任意设定为高电平触发、低电平触发、下降沿触
发、上升沿触发和上升或下降沿触发5 种模式。本应用中使用下降沿触发,其初始化如程
序清单 5.1 所示。
程序清单 5.1 轮子脉冲检测初始化
void WheelPulseIni(void)
{
// 配置引脚为输入
GPIODirModeSet(GPIO_PORTA_BASE, PULSE_R, GPIO_DIR_MODE_IN);
GPIODirModeSet(GPIO_PORTB_BASE, PULSE_L, GPIO_DIR_MODE_IN);
// 配置引脚下降沿触发中断
GPIOIntTypeSet(GPIO_PORTA_BASE,PULSE_R,GPIO_FALLING_EDGE);
GPIOIntTypeSet(GPIO_PORTB_BASE,PULSE_L,GPIO_FALLING_EDGE);
// 使能引脚输入中断
GPIOPinIntEnable(GPIO_PORTA_BASE,PULSE_R);
GPIOPinIntEnable(GPIO_PORTB_BASE,PULSE_L);
// 使能GPIO PA 口和GPIO PB 口中断
IntEnable(INT_GPIOA);
IntEnable(INT_GPIOB);
}
左右轮检测脉冲中断处函数如程序清单 5.2 所示。
程序清单 5.2 左右轮检测脉冲中断处函数
//------------------------------------------------------------------------------------
// 函数名称: GPIO_Port_A_ISR
// 函数功能: 右轮检测脉冲中断处函数
//------------------------------------------------------------------------------------
void GPIO_Port_A_ISR (void)
{
unsigned char IntStatus;
IntStatus = GPIOPinIntStatus(GPIO_PORTA_BASE,true); // 读PA 口中断状态
if(IntStatus&PULSE_R) // 是否为左轮脉冲中断
{
PulCount_R++;
if(PulCount_R >= RightPulse)
{
RightWheelRun(0, 1);
WheelStop_R= 1;
}
GPIOPinIntClear(GPIO_PORTA_BASE,PULSE_R); // 清中断
}
}
//------------------------------------------------------------------------------------
// 函数名称: GPIO_Port_B_ISR
// 函数功能: 左轮检测脉冲中断处函数
//------------------------------------------------------------------------------------
void GPIO_Port_B_ISR (void)
{
unsigned char IntStatus;
IntStatus = GPIOPinIntStatus(GPIO_PORTB_BASE,true); // 读PA 口中断状态
if(IntStatus&PULSE_L) // 是否为右轮脉冲中断
{
PulCount_L++;
if(PulCount_L>= LeftPulse)
{
WheelStop_L= 1;
LeftWheelRun(0, 1);
}
GPIOPinIntClear(GPIO_PORTB_BASE,PULSE_L); // 清中断
}
}
六、 系统DV:参看附件:
a) 直线行走3格+右转:
该运动中,小车首先直线行走3格(50CM),然后右转,并在碰到障碍物后右转,直线行走一格,再右转一次;
b) 围绕小桌运动:
该运动中,小车围绕方形小桌绕圈,判断安全距离、检测是否存在通路,并自动修正方向。
七、 测试情况:
经检测,小车能较好的完成给定的运行任务,较为准确地直走、左右转,及180度转。小车完全可以完成探究迷宫、自动寻路等任务,并以较短的时间完成迷宫行走。
但是,小车依旧存在些问题:直走中有摆动现象,左右转也做不到90度(大概85度到95度之间),180度更不好;算法不够优秀,时间依旧较长。虽然这些问题更为琐碎更为难处理,但我们有信心,暑假间会完全克服。
八、 曾遇到的问题:
1.车轮行进速度不一:
同一函数中,小车的左右转速相差比较大,我们尝试采取了以下几种方式解决
1)。调占空比:无效,原因不明。在与别的小组交流之后,我们尝试改写了原先的驱动函数,使速度函数与正反转函数区分开:
(原函数 & 改进后的函数 请见附录)
还不太明白是为什么,把两个函数分开写了之后,发现有一定效果。
2)。脉冲补偿:
这也是演示程序的方法,在执行完一次运行任务之后,把相差的脉冲补偿到下一次任务的设定中:
PWMTimer0AIni(); // PWM初始化
PULSEIni(); // 调制信号初始化
WheelPulseIni(); // 测速初始化
while(1)
{
LeftPulse = 10; // 设定电机运行任务
RightPulse = 10;
WheelStop_L = 0; // 清零电机停止标志位
WheelStop_R = 0;
LeftWheelRun(1, 99); // 启动左右电机
RightWheelRun(1, 99);
while(!(WheelStop_L && WheelStop_R))// 等待运行结束,状态在中断中改变
Check_Infrared(0); // 等待过程中进行红外检测
PulCount_L -= LeftPulse; // 误差补偿到下一次运动中
PulCount_R -= RightPulse;
}
想法很好,实际行不通。
以最低误差来算,假设左轮10个脉冲,右轮9个,小车明显走出一条弧线;而当下次补偿到下次之中时,小车还是先走同样的路程,然后左轮停转,右轮转2个脉冲(上次误差+ 这次误差),表现出了明显的“一瘸一拐”的情况;而且,在我们的调试过程中,出现了不稳定的左右摆动,这是由于“半个脉冲”的问题,小车不能有效识别比较小的距离差,在最好的状态下,小车也存在计数的问题,比如,左轮刚转就开始向下的触发,右轮快转一圈才有第一次的向下触发。而且,这种误差是随机的,与小车车轮的起始位置有关,不好避免。
3).从硬件下手:
在左右电机同加固定电压时,清楚地看到转速不一样,因此,最好的办法还是从硬件上下手,通过串联电阻来解决问题。这是我们的一个想法,还没开始实施,准备在暑假时再想想软件解决办法,不行就改电路。(电机内阻,运行时电流)
2。检测信号的波形:
受灯光的影响,可以看到,红外接受得到的波形不时很好。检测初速的还好,因为幅度比较大,所以没有误判的问题;检测挡板的波形振动比较大。按照电路图,尝试着变换的电阻,取得不错的效果,以下函数足够完成判断:
( 源程序详见附录)
3。编译器的问题:
大量铁的事实证明,crosswork不好用,推荐使用Keil for ARM。前期使用的是crosswork,很多不明就里的问题,换到Keil for ARM就解决了,说明crosswork对小车的支持不够好。很多函数,在crosswork上,完全起不到效果,跑出来都知道是怎么回事。我们不是在责怪crosswork不好,但至少说明,不易于上手。
总结了一些发现的问题:
1)。不能出现汉字:还好,无非就是存盘载入时麻烦点。
2)。只能装C盘:刚开始装在D盘,整天蓝屏。
3)。头文件载入:很多头文件需要手动一一载入,还是查百度知道的,说明文件上没有。
4)。编译显示错误,不显示为什么错:开始时,发现错误就在Keil上找错。。。。。。
5)。脱机运行问题:想脱机运行?需要改很多。。。。。。
因此,不推荐用crosswork。
八、 心得体会:
a) 辛苦:在刚着手时,一点头绪没有。于是从单片机、电路图、红外与电机一点点看,为了一个小车重新学了很多东西;后来接到小车,开始忙着调试写程序,面临着很多的问题。由于没有足够的资料,三人齐心协力一点点地摸索着走。经历过一连几天夜夜2、3点,也有过一整天三份外卖一起叫的时间。由于我们对自己的要求特别高,所以在每一个小的细节上都力求完美,花费了很多时间精力。
b) 兴趣:与别的课程不一样的地方,电脑鼠非常的好玩,有点像小时候的四驱车,但多了的是科技含量。看着小车在手中逐渐的成长,探路行进等功能一一实现,心中的好奇与兴奋是很难说的清的。因为兴趣,所以很多的辛苦都可以忽略不计,很多的焦头烂额也都能谈笑而过。
c) 荣耀:最喜欢做的一件事,是把小车打开,让他在寝室里自己逛着玩。没养过小孩,但相信看着自己的孩子一点点学会走路的感觉,和我们看小车的感觉一样。也许正是因为我们比别的组走过更艰难的路,所以在取得小小成绩后比别人更为的骄傲。现在的小车已经能完成基本任务了,我们期待着他能早点的大学毕业~~~~~~~~~
最后,我们在此要感谢每一位队友的努力与付出,感谢兄弟间的团结与友爱;我们也要感谢其余小组给与的帮助,彼此的交流帮助我们更好的进步;感谢助教的指导,在ARM的编程中给我们指出了最重要的方向;我们当然更要感谢张老师的教导,感谢张老师对我们错误的包容指教,和提供的广阔的学习空间。在暑假的日子里,我们会再接再厉,一定会在竞赛中取得成绩!
G02小组全体成员
2008/7/7
一、 综述:
a) 项目名称:小车自动走迷宫;
b) 小组成员:
i. 周杰:负责总体规划,及ARM编程;
ii. 宋大成:负责车轮驱动;
iii. 陈潇:负责红外驱动;
c) 小车图片:
MicroMouse615:
MicroMouse120:
二、 项目介绍:
电脑鼠走迷宫竞赛的目的是制作一个微型机器人,它能在最短的时间内穿越迷宫到达终点。参赛的机机器人称为“电脑鼠”,将电脑鼠放入迷宫并启动操作的人称为“操作员”。
电脑鼠的基本功能是从起点开始走到终点,这个过程称为一次“运行”,所花费的时间称为“运行时间”。从终点回到起点所花费的时间不计算在运行时间内。从电脑鼠的第一次激活到每次运行开始,这段期间所花费的时间称为“迷宫时间”。如果电脑鼠在比赛时需要手动辅助,这个动作称为“碰触”。竞赛使用这三个参数,从速度﹑求解迷宫的效率和电脑鼠的可靠性三个方面来进行评分。器人称为“电脑鼠”,将电脑鼠放入迷宫并启动操作的人称为“操作员”。
在G02小组的工作中,基本完成了电脑鼠的驱动部分和算法部分:电脑鼠可以进行直线行走、90度左转、90度右转和180度后转;能准确监测左前右三个方向的挡板,能准确判断左右间距是否恰当;能进行调距运动,以保持与左右挡板的距离;能简单判断通路,并计算记忆合适通路;并按照通路完成迷宫行走。
但是,由于某些原因,小车的没有取得完美的效果:由于电机转速控制不当,小车直线行走效果不佳(参看第条);左转右转不够准确,只能依靠调整函数补偿;算法不够优秀,正在探究更好的算法。这些问题都在努力克服中,我们不会因为检查完毕就放弃小车的调试。
三、 项目整体结构:
MicroMouse102 电脑老鼠,采用美国LuminaryMicro 公司生产的32 位ARM CortexM3处理器LM3S102,控制和检测红外传感器;主CPU 根据检测到的传感信号,控制电机驱动电路调整行走路径,直到到达终点。
四、 硬件部分介绍:
LED 电路
电脑鼠有5 个独立的LED,通过LM3S 系统单片机的GPIO 口直接控制,如图 1.6 所示。电路采用了I/O 口灌电流的驱动方式来驱动LED,LM3S 系统单片机的灌电流为2~8mA(可配置),所以不需要驱动就可以点亮LED。GPIO 引脚输出高电平时LED 熄灭,低电平时LED 点亮。
电机驱动电路
电机采用直流减速电机,最高输出转速为800 转/分钟,工作电压为DC3V。电机驱动
电路采用专用的单相直流电动机桥式驱动芯片。
车速检测电路
车速检测用于检测并记录车体运行的路径,通过车速检测记录车体做迷宫的坐标,同
时也起到控制车速和保持左右双轮的速度一致。
检测原理:在左轮和右轮的内则都贴有的光电码盘,码盘由两种颜色组成白色和黑色。
红外发射管安装在车轮光电检测码盘的检测区域,当红外发射与接收管正对着黑色边时,
红外线没有被反射,接收管的电阻很大;当红外发射与接收管正对着白色边时,红外线被
反射,接收管的电阻很小。
红外检测电路
红外检测电路是用于迷宫挡板的检测,分为左侧、右侧、前方三个方向,三个方向的
检测原理相同,某一个方向的检测电路。
CPU 及晶振电路
电脑鼠的单片机、晶体振荡器和LDO输出原理如图所示。该单片机选用LM3S102
微处理器。
五、 软件部分介绍:
一体化红外接收头工作原理
一体式红外线接收传感器IRM8601S,它内部集成自动增益控制电路、带通滤波电路、
解码电路及输出驱动电路。当连续收到38KHz 的红外线信号时,将产生脉宽10ms 左右的
低电平。如果没有收到信号,便立即输出高电平。Send 为发射控制端,高
电平时发射38KHz 的红外信号。Out 为接收输出端,低电平表示收到信号。
检测障碍物的软件设计
根据接收头是否检测到经过反射的红外线信号,就可以判断是否存在障碍物。由于接
收头检测到信号时只产生一个负脉冲,所以只需要在检测时使能红外线发射,一次检测结
束后使能无效,程序设计参考流程图如图2.5 所示。
接收头有一定的
响应时间
开始发送38KHz
的红外线
迷宫挡板检测
调制信号产生
本设计中采用定时器1 产生38KHz 的调制信号,由PB5 输出,该端口连接到图2.3 中
的Pulse 端口。在中断中翻转PB5 输出信号,所以要产生频率为f 的脉冲,定时器的频率
要为2f。在本设计中要产生38KHz 的频率,定时器中断频率为76KHz。
程序清单 3.1 为定时器1 的初始化函数,程序清单 3.2 为中断服务函数,在这里翻转
PB5 口输出状态。
程序清单 3.1 定时器1 初始化
void PULSEIni(void)
{
GPIODirModeSet(GPIO_PORTB_BASE, SEND | PULSE, GPIO_DIR_MODE_OUT); // 设置为输
出
GPIOPinWrite( GPIO_PORTB_BASE,SEND | PULSE,0); // 红外线初始时停止发射
SysCtlPeripheralEnable( SYSCTL_PERIPH_TIMER1 ); // 使能定时器1 外设
TimerConfigure(TIMER1_BASE, TIMER_CFG_32_BIT_PER); // 设置定时器1 为周期触发
TimerLoadSet(TIMER1_BASE, TIMER_A, SysCtlClockGet()/76000); // 设置定时器装载值
TimerIntEnable(TIMER1_BASE, TIMER_TIMA_TIMEOUT);
TimerEnable(TIMER1_BASE, TIMER_A);
IntEnable(INT_TIMER1A);
}
程序清单 3.2 定时器1 服务函数
void Timer1A_ISR(void)
{
TimerIntClear(TIMER1_BASE, TIMER_TIMA_TIMEOUT); // 清除定时器1 中断
GPIOPinWrite(GPIO_PORTB_BASE, PULSE,GPIOPinRead(GPIO_PORTB_BASE, PULSE) ^ PULSE);
// 翻转GPIO B5 端口
}
抗干扰处理
红外线在空气中传播和反射受外界的干扰,如果测量距离刚好处在能够检测到信号的
临界状态,保持距离不变,传感器输出信号也可能不确定。这样就需要在软件中进行抗干
扰处理。参考程序如程序清单3.3 所示。
程序清单3.3 抗干扰处理程序
GPIOPinWrite( GPIO_PORTB_BASE,SEND , SEND); // 发送脉冲
Delay(150); // 延时
for(i=0,j=0;i
{
if(GPIOPinRead(GPIO_PORTA_BASE, OUT_L)==0)
j++;
}
GPIOPinWrite( GPIO_PORTB_BASE,SEND , ~SEND); // 停止发送
if(j>5) // 左边存在挡板
{
}
else // 左边存在支路
{
}
图3.1 为抗干扰程序在Micromouse 中运行后用逻辑分析仪抓到的波形图,Pulse 为
38KHz 的输出信号,Send 高电平有效,有效时发送红外线脉冲,OUT 为一体化接收头输
…… ……
出端,该图所示为接收头探测到障碍物,软件在Send 信号无效(下降沿)前完成检测OUT
输出信号,从图中可以看出,此时正处于OUT 有效信号的中间,所以软件里延时参数能
保证正确检测到信号。
图3.1 传感器检测波形图
软件设计参考
为用一组红外实现两组参数(是否存在挡板和是否太接近挡板)的检测流程图。在Micromouse 中,用到了三组(左、前、右)反射式红外检测传感器,左边和右边的传感器各自都需要检测两组参数,而前方的传感器只需要探测有无挡板,存在挡板就必须根据策略转换行进方向,若不存在就可以继续前进。如图3.3 所示为Micromouse 红外检测的程序设计流程图。红外检测参考程序见程序清单 3.4 所示,该程序中使用了五个LED 用来指示传感器检测的状态,由于这几个LED 硬件上连接到JTAG,关于如何切换GPIO和JTAG功能参见6 使用JTAG 引脚作GPIO。
此频率仅作为参考,要根据实际检测距离来确定。结合可调电阻R1 变可以实现挡板和防碰撞的检测。
程序清单 3.4 Micromouse 红外检测函数(见附录)
电机的调速
电机的调速
直流电机的转速控制在本设计中通过PWM来控制,LM3S102 单片机则刚有两路PWM
输出,非常适合用于控制两个电机的转速。
两路PWM 是LM3S102 通用定时器0(Timer0)的三种工作模式之一,16 位PWM 模
式。该模式是将一个32 位的定时器,折分成两个16 位的定时器TimerA 和TimerB。这些
定时器为计数寄存器(GPTMTnR)递减计数,递减到0 时自动加载预装载值(GPTMTnILR)。
当然预装载值也是由用户设定,该直也就决定了定时周期,也即PWM 的输出周期。
当计数器的值与预装载值相等时,输出PWM 信号有效,当计数器的值与匹配寄存器
(GPTMnMATCHR)的值相等时,输出PWM 信号失效。通过软件可以设定PWM 输的信
号有效和信号无效的电平状态。当GPTMCTL 寄存器的TnPWML 位值为0 时,信号有效
为高电平,信号无效为低电平;TnPWML 位值为1 时,则反之。如图 4.1 所示。
输出信号
计数
0x411A
0xC350
TnPWML=0
TnPWML=1
TnEN置位
GPTMTnR=GPTMnMR GPTMTnR=GPTMnMR
时间
图 4.1 16 位PWM 模式输出
占空比的约定:占空比为在一个周期内,输出有信号有效电平占整个周期时间的比率。
在这里为以统一软件控制的约定,用户API 函数输入的占空比值越大,电机转速越快,正
向运行和反向运行都一样。
为了简化占空比输出的计算,将计数寄存器与匹配寄存器值相等时,输出的电平信号
为驱动电机的有效信号。例如将PWM 周期时间设定为60000 个时钟节拍,需要输出驱动
电机的占空比为75%,则设置匹配寄存器值为75*6000。
由于电机的转向不一样,所以电机驱动的有效电平也需要调整,通过控制TnPWML
实现。
2 程序设计
Timer0 的两路16 定时器TimerA 和TimerB 的PWM 输出引脚分别为PB0 和PB6,PB0
和PB6 分别控制左轮和右轮驱动器TA7291S 的IN1 引脚,而它们的IN2 引脚分别由GPIO
输出的PA4 和PA5 控制。
左轮的控制函数如程序清单 4.1 所示。
该函数的第1 个参数sel 为选择轮子的控制方式:0 为停止,1 为轮子向前,2 为轮子
向后;percen 参数为占空比,其最大值为99,最小值为1,对于轮子的停止控制该参数无
效。
程序清单 4.1 左轮控制函数
void LeftWheelRun(int sel,unsigned char percen)
{
switch(sel)
{
/*轮子停止转动*/
case 0:
TimerDisable(TIMER0_BASE,TIMER_A); // 禁止定时器
GPIOPinWrite(GPIO_PORTA_BASE,LWC2,0xff); // 控制引脚输出高电平
GPIODirModeSet(GPIO_PORTB_BASE, LWC1, GPIO_DIR_MODE_OUT); // GPIO 输出
GPIOPinWrite(GPIO_PORTB_BASE,LWC1, 0xff); // GPIO 输出高电平
break;
/*左轮向前*/
case 1:
GPIOPinWrite(GPIO_PORTA_BASE,LWC2, 0xff); // PA4 输出高电平
TimerControlLevel(TIMER0_BASE,TIMER_A,true); // PWM 有效电平方向
GPIODirModeSet(GPIO_PORTB_BASE, LWC1, GPIO_DIR_MODE_HW); // PWM 输出
TimerMatchSet(TIMER0_BASE,TIMER_A,percen*600); // 设置占空比
TimerEnable(TIMER0_BASE,TIMER_A); // 使能定时器
break;
/*左轮向后*/
case 2:
GPIOPinWrite(GPIO_PORTA_BASE,LWC2, 0); // PA4 输出低电平
TimerControlLevel(TIMER0_BASE,TIMER_A,false); // PWM 有效电平方向
GPIODirModeSet(GPIO_PORTB_BASE, LWC1, GPIO_DIR_MODE_HW); // PWM 输出
TimerMatchSet(TIMER0_BASE,TIMER_A,percen*600); // 设置占空比
TimerEnable(TIMER0_BASE,TIMER_A); // 使能定时器
break;
}
}
右轮的控制函数如程序清单 4.2 所示。
该函数的第1 个参数sel 为选择轮子的控制方式:0 为停止,1 为轮子向前,2 为轮子
向后;percen 参数为占空比,其最大值为99,最小值为1,对于轮子的停止控制该参数无
效。
程序清单 4.2 右轮控制函数
void RightWheelRun(int sel,unsigned char percen)
{
switch(sel)
{
/*轮子停止转动*/
case 0:
TimerDisable(TIMER0_BASE,TIMER_B); // 禁止定时器
GPIOPinWrite(GPIO_PORTA_BASE,RWC2,0xff); // 控制引脚输出高电平
GPIODirModeSet(GPIO_PORTB_BASE, RWC1, GPIO_DIR_MODE_OUT); // GPIO 输出
GPIOPinWrite(GPIO_PORTB_BASE,RWC1,0xff); // GPIO 输出高电平
break;
/*右轮向后*/
case 2:
GPIOPinWrite(GPIO_PORTA_BASE,RWC2, 0xff); // PA4 输出高电平
TimerControlLevel(TIMER0_BASE,TIMER_B,true); // PWM 有效电平方向
GPIODirModeSet(GPIO_PORTB_BASE, RWC1, GPIO_DIR_MODE_HW); // PWM 输出
TimerMatchSet(TIMER0_BASE,TIMER_B,percen*600); // 设置占空比
TimerEnable(TIMER0_BASE,TIMER_B); // 使能定时器
break;
/*右轮向前*/
case 1:
GPIOPinWrite(GPIO_PORTA_BASE,RWC2, 0); // PA4 输出低电平
TimerControlLevel(TIMER0_BASE,TIMER_B,false); // PWM 有效电平方向
GPIODirModeSet(GPIO_PORTB_BASE, RWC1, GPIO_DIR_MODE_HW); // PWM 输出
TimerMatchSet(TIMER0_BASE,TIMER_B,percen*600); // 设置占空比
TimerEnable(TIMER0_BASE,TIMER_B); // 使能定时器
break;
}
}
需要注意的是,当PWM 信号禁止后,其输出引脚的电平状态是保持静止时的状态(可
能为低电平也可能为高电平),导致电机可能不能停止,所以在制停电机时,需要将PWM
引脚改为GPIO 输出,并且出高电平,使电机刹车停止。
定时器PWM 初始化函数如程序清单 4.3 所示。
程序清单 4.3 定时器PWM 初始化
void PWMTimer0AIni(void)
{
SysCtlPeripheralEnable(SYSCTL_PERIPH_TIMER0); // 使能定时器0 模
块
GPIODirModeSet(GPIO_PORTB_BASE, LWC1|RWC1 , GPIO_DIR_MODE_OUT);
/* 控制引脚输出*/
GPIOPinWrite(GPIO_PORTB_BASE, LWC1|RWC1 , 0xff); // GPIO 输出高电
平
GPIODirModeSet(GPIO_PORTA_BASE, LWC2|RWC2 , GPIO_DIR_MODE_OUT); // 控制引脚输出
GPIOPinWrite(GPIO_PORTA_BASE, LWC2|RWC2 , 0xff); // GPIO 输出高电
平
/* 定时器配置*/
TimerConfigure(TIMER0_BASE,TIMER_CFG_16_BIT_PAIR
|TIMER_CFG_A_PWM|TIMER_CFG_B_PWM); // 16 位PWM 输出
TimerControlLevel(TIMER0_BASE,TIMER_A,false); //有效信号为低电
平
TimerControlLevel(TIMER0_BASE,TIMER_B,false);
TimerLoadSet(TIMER0_BASE,TIMER_A,60000); // 设定PWM 频率
TimerLoadSet(TIMER0_BASE,TIMER_B,60000);
}
Micromouse 车速检测
车速检测程序设计
本设计中选用了LM3S102 的PA0 和PB1 分别检测左轮和右轮的下降沿的脉冲个数,
为了快速向应检测信号,使用了下降沿触发中断。LM3S102 单片机的特点,任何一个GPIO
引脚都可以配置为中断输入,并且可以作任意设定为高电平触发、低电平触发、下降沿触
发、上升沿触发和上升或下降沿触发5 种模式。本应用中使用下降沿触发,其初始化如程
序清单 5.1 所示。
程序清单 5.1 轮子脉冲检测初始化
void WheelPulseIni(void)
{
// 配置引脚为输入
GPIODirModeSet(GPIO_PORTA_BASE, PULSE_R, GPIO_DIR_MODE_IN);
GPIODirModeSet(GPIO_PORTB_BASE, PULSE_L, GPIO_DIR_MODE_IN);
// 配置引脚下降沿触发中断
GPIOIntTypeSet(GPIO_PORTA_BASE,PULSE_R,GPIO_FALLING_EDGE);
GPIOIntTypeSet(GPIO_PORTB_BASE,PULSE_L,GPIO_FALLING_EDGE);
// 使能引脚输入中断
GPIOPinIntEnable(GPIO_PORTA_BASE,PULSE_R);
GPIOPinIntEnable(GPIO_PORTB_BASE,PULSE_L);
// 使能GPIO PA 口和GPIO PB 口中断
IntEnable(INT_GPIOA);
IntEnable(INT_GPIOB);
}
左右轮检测脉冲中断处函数如程序清单 5.2 所示。
程序清单 5.2 左右轮检测脉冲中断处函数
//------------------------------------------------------------------------------------
// 函数名称: GPIO_Port_A_ISR
// 函数功能: 右轮检测脉冲中断处函数
//------------------------------------------------------------------------------------
void GPIO_Port_A_ISR (void)
{
unsigned char IntStatus;
IntStatus = GPIOPinIntStatus(GPIO_PORTA_BASE,true); // 读PA 口中断状态
if(IntStatus&PULSE_R) // 是否为左轮脉冲中断
{
PulCount_R++;
if(PulCount_R >= RightPulse)
{
RightWheelRun(0, 1);
WheelStop_R= 1;
}
GPIOPinIntClear(GPIO_PORTA_BASE,PULSE_R); // 清中断
}
}
//------------------------------------------------------------------------------------
// 函数名称: GPIO_Port_B_ISR
// 函数功能: 左轮检测脉冲中断处函数
//------------------------------------------------------------------------------------
void GPIO_Port_B_ISR (void)
{
unsigned char IntStatus;
IntStatus = GPIOPinIntStatus(GPIO_PORTB_BASE,true); // 读PA 口中断状态
if(IntStatus&PULSE_L) // 是否为右轮脉冲中断
{
PulCount_L++;
if(PulCount_L>= LeftPulse)
{
WheelStop_L= 1;
LeftWheelRun(0, 1);
}
GPIOPinIntClear(GPIO_PORTB_BASE,PULSE_L); // 清中断
}
}
六、 系统DV:参看附件:
a) 直线行走3格+右转:
该运动中,小车首先直线行走3格(50CM),然后右转,并在碰到障碍物后右转,直线行走一格,再右转一次;
b) 围绕小桌运动:
该运动中,小车围绕方形小桌绕圈,判断安全距离、检测是否存在通路,并自动修正方向。
七、 测试情况:
经检测,小车能较好的完成给定的运行任务,较为准确地直走、左右转,及180度转。小车完全可以完成探究迷宫、自动寻路等任务,并以较短的时间完成迷宫行走。
但是,小车依旧存在些问题:直走中有摆动现象,左右转也做不到90度(大概85度到95度之间),180度更不好;算法不够优秀,时间依旧较长。虽然这些问题更为琐碎更为难处理,但我们有信心,暑假间会完全克服。
八、 曾遇到的问题:
1.车轮行进速度不一:
同一函数中,小车的左右转速相差比较大,我们尝试采取了以下几种方式解决
1)。调占空比:无效,原因不明。在与别的小组交流之后,我们尝试改写了原先的驱动函数,使速度函数与正反转函数区分开:
(原函数 & 改进后的函数 请见附录)
还不太明白是为什么,把两个函数分开写了之后,发现有一定效果。
2)。脉冲补偿:
这也是演示程序的方法,在执行完一次运行任务之后,把相差的脉冲补偿到下一次任务的设定中:
PWMTimer0AIni(); // PWM初始化
PULSEIni(); // 调制信号初始化
WheelPulseIni(); // 测速初始化
while(1)
{
LeftPulse = 10; // 设定电机运行任务
RightPulse = 10;
WheelStop_L = 0; // 清零电机停止标志位
WheelStop_R = 0;
LeftWheelRun(1, 99); // 启动左右电机
RightWheelRun(1, 99);
while(!(WheelStop_L && WheelStop_R))// 等待运行结束,状态在中断中改变
Check_Infrared(0); // 等待过程中进行红外检测
PulCount_L -= LeftPulse; // 误差补偿到下一次运动中
PulCount_R -= RightPulse;
}
想法很好,实际行不通。
以最低误差来算,假设左轮10个脉冲,右轮9个,小车明显走出一条弧线;而当下次补偿到下次之中时,小车还是先走同样的路程,然后左轮停转,右轮转2个脉冲(上次误差+ 这次误差),表现出了明显的“一瘸一拐”的情况;而且,在我们的调试过程中,出现了不稳定的左右摆动,这是由于“半个脉冲”的问题,小车不能有效识别比较小的距离差,在最好的状态下,小车也存在计数的问题,比如,左轮刚转就开始向下的触发,右轮快转一圈才有第一次的向下触发。而且,这种误差是随机的,与小车车轮的起始位置有关,不好避免。
3).从硬件下手:
在左右电机同加固定电压时,清楚地看到转速不一样,因此,最好的办法还是从硬件上下手,通过串联电阻来解决问题。这是我们的一个想法,还没开始实施,准备在暑假时再想想软件解决办法,不行就改电路。(电机内阻,运行时电流)
2。检测信号的波形:
受灯光的影响,可以看到,红外接受得到的波形不时很好。检测初速的还好,因为幅度比较大,所以没有误判的问题;检测挡板的波形振动比较大。按照电路图,尝试着变换的电阻,取得不错的效果,以下函数足够完成判断:
( 源程序详见附录)
3。编译器的问题:
大量铁的事实证明,crosswork不好用,推荐使用Keil for ARM。前期使用的是crosswork,很多不明就里的问题,换到Keil for ARM就解决了,说明crosswork对小车的支持不够好。很多函数,在crosswork上,完全起不到效果,跑出来都知道是怎么回事。我们不是在责怪crosswork不好,但至少说明,不易于上手。
总结了一些发现的问题:
1)。不能出现汉字:还好,无非就是存盘载入时麻烦点。
2)。只能装C盘:刚开始装在D盘,整天蓝屏。
3)。头文件载入:很多头文件需要手动一一载入,还是查百度知道的,说明文件上没有。
4)。编译显示错误,不显示为什么错:开始时,发现错误就在Keil上找错。。。。。。
5)。脱机运行问题:想脱机运行?需要改很多。。。。。。
因此,不推荐用crosswork。
八、 心得体会:
a) 辛苦:在刚着手时,一点头绪没有。于是从单片机、电路图、红外与电机一点点看,为了一个小车重新学了很多东西;后来接到小车,开始忙着调试写程序,面临着很多的问题。由于没有足够的资料,三人齐心协力一点点地摸索着走。经历过一连几天夜夜2、3点,也有过一整天三份外卖一起叫的时间。由于我们对自己的要求特别高,所以在每一个小的细节上都力求完美,花费了很多时间精力。
b) 兴趣:与别的课程不一样的地方,电脑鼠非常的好玩,有点像小时候的四驱车,但多了的是科技含量。看着小车在手中逐渐的成长,探路行进等功能一一实现,心中的好奇与兴奋是很难说的清的。因为兴趣,所以很多的辛苦都可以忽略不计,很多的焦头烂额也都能谈笑而过。
c) 荣耀:最喜欢做的一件事,是把小车打开,让他在寝室里自己逛着玩。没养过小孩,但相信看着自己的孩子一点点学会走路的感觉,和我们看小车的感觉一样。也许正是因为我们比别的组走过更艰难的路,所以在取得小小成绩后比别人更为的骄傲。现在的小车已经能完成基本任务了,我们期待着他能早点的大学毕业~~~~~~~~~
最后,我们在此要感谢每一位队友的努力与付出,感谢兄弟间的团结与友爱;我们也要感谢其余小组给与的帮助,彼此的交流帮助我们更好的进步;感谢助教的指导,在ARM的编程中给我们指出了最重要的方向;我们当然更要感谢张老师的教导,感谢张老师对我们错误的包容指教,和提供的广阔的学习空间。在暑假的日子里,我们会再接再厉,一定会在竞赛中取得成绩!
G02小组全体成员
2008/7/7