2006年下半年,我们在自己设计的BCNG2440开发板上移植了VXWORKS。移植的过程参考了网络上一些BSP代码,与现有的代码不同的是,我们的BSP实现了内存的重映射,中断向量的重新分配;以及cache和MMU的开启。移植的目的是用于一个数据采集系统,ARM从FPGA读取解调数据,通过100M网络发送到服务器。经过测试,使用UDP发送速率达到了43Mbps,使用TCP发送速率为20Mbps。之前,我们在类似的项目中使用了MPC8260为CPU,当时TCP最高速率也只达到了20多Mbps,因此,我们认为在某些项目中使用ARM替代MPC8260是可行的。
移植的过程分为一下几个部分:
1)异常处理
2)中断
3)MMU
4)DM9000网卡驱动
5)启动参数保存问题
6)CPU设置
7)其他问题
通常来讲,在嵌入式系统启动之初,CPU将从0地址处(或ffff0000处)开始执行代码,因此初始时将ROM映射在0(或ffff0000)地址处,当进行完必要的初始化并启动异常处理机制后,会将RAM映射到0(或ffff0000)地址处,而将ROM映射到其它地址。
这是因为发生异常时,CPU会跳转到0地址开始处执行异常向量表。由于RAM的访问速度远远高于ROM,因此将RAM映射到0地址后,可以减少异常处理的延迟时间还有其他好处,动态和灵活等。
地址重映射可通过不同的方式实现,例如MPC8260可通过重新为每个memory bank 分配地址空间来实现,对于AT91RM9200,可通过设置其独有的“REMAP”控制位来实现。
ARM的体系结构规定在异常发生时,要从0 地址开始处读取相应的处理指令,然而S3C2440A的固定地址空间管理方法在VxWorks里会遇到问题。因为从硬件上讲,S3C2440A的地址空间是不能重分配的,它也不支持所谓的REMAP功能,一旦硬件连线决定了其RAM基地址为0x30000000(nGCS6),0地址上为ROM(nGCS0),就无法再更改。因此必须采用其他办法来解决异常向量表的访问问题。VxWorks管理的RAM中异常向量表结构如下图:
对于BOOTROM来说,不会使用到MMU,访问地址0就是访问ROM,因此需要将异常向量表建立在启动ROM的开始处。
基本思想是在Flash存储器的起始地址硬编码异常入口,仿vxWorks建立异常向量表。异常发生时,经Flash存储器入口,跳转到自定义函数,再跳转到RAM中异常入口,再调用vxWorks提供的异常处理函数。中断处理流程和中断向量表如下图示意。新异常向量表和原VxWorks设计完全一样。
romInit.c(下面是处理IRQ异常的例子,其它见源代码):
_romInit:
B cold
B _romUndef
B _romSwi
B _romPrefetch
B _romDataAbort
B _romReserved
B _romIRQ
B _romFIQ
_ARM_FUNCTION(romIRQ)
_romIRQ:
sub sp, sp, #4
stmfd sp!, {r0}
ldr r0, L$_promIRQ
ldr r0, [r0]
str r0, [sp, #4]
ldmfd sp!, {r0, pc}
L$_promIRQ:
.long S3C_EXC_BASE + 20
/* exception base */
#define S3C_EXC_BASE 0x30000100
config.c中定义:
#define S3C_EXC_BASE 0x30000100
sysLib.c:
添加以下函数声明
void s3cExcVecSet(void);
IMPORT VOIDFUNCPTR excEnterUndef;
IMPORT VOIDFUNCPTR excEnterPrefetchAbort;
IMPORT VOIDFUNCPTR excEnterDataAbort;
IMPORT VOIDFUNCPTR excEnterSwi;
IMPORT VOIDFUNCPTR intEnt;
添加函数s3cExcVecSet()
void s3cExcVecSet(void)
{
int i;
i = (int)&excEnterUndef;
*((volatile int*)(S3C_EXC_BASE + 0x0)) = i;
i = (int)&excEnterSwi;
*((volatile int*)(S3C_EXC_BASE + 0x4)) = i;
i = (int)&excEnterPrefetchAbort;
*((volatile int*)(S3C_EXC_BASE + 0x8)) = i;
i = (int)&excEnterDataAbort;
*((volatile int*)(S3C_EXC_BASE + 0xc)) = i;
i = (int)&intEnt;
*((volatile int*)(S3C_EXC_BASE + 0x14)) = i;
return;
}
在sysHwInit()中调用s3cExcVecSet()
void sysHwInit(void)
{
/* install the IRQ/SVC interrupt stack splitting routine */
_func_armIntStackSplit = sysIntStackSplit;
#ifdef INCLUDE_SERIAL
/* initialise the serial devices */
sysSerialHwInit (); /* initialise serial data structure */
#endif /* INCLUDE_SERIAL */
s3cExcVecTblInstall();
s3cExcVecSet();
}
简单的描述上述代码的过程:当异常发生时,以IRQ异常为例,PC指针首先跳转到0x14处,读取指令“B _romIRQ”,然后进入到_romIRQ函数,此函数作用是将S3C_EXC_BASE + 20地址里面的值赋给PC寄存器(S3C_EXC_BASE + 20地址里面存放vxWorks的IRQ处理函数intEnt的地址,这由s3cExcVecSet建立),从而跳转到inrEnt函数处理IRQ异常。
_romIRQ函数比较复杂,其作用是将S3C_EXC_BASE + 20地址里面的值赋给PC寄存器,此段处理有一定的技巧,先将当前寄存器的内容及RAM中的中断向量的地址入栈,再从堆栈中将中断向量地址装载到PC,寄存器内容也从堆栈中恢复过来。
其实不经过堆栈直接将RAM中中断向量的地址装载到PC应该也可以,但是有人在论坛中发贴说这样做存在问题,我没有验证过,但是北理他们就是这样做的(但也通过r0转了一次),除此之外我所见到的所有s3c44b0和s3c2410的BSP都是采用前面描述的做法。
对于vxWorks映象,如果不采用MMU或者使用MMU但是进行平坦地址映射,ROM仍处于地址0,因此使用和BOOTROM同样的方式处理异常向量即可,无需对代码做其他修改。但是如果通过MMU进行非平坦地址映射(即SDRAM映射到地址0,以加快异常处理速度,ROM映射到某一空闲地址处,如0xf0000000),则需要在SDRAM起始地址处建立异常向量跳转表。
已经知道,在vxWorks内存的0x0100偏移处存在异常函数处理指针(由s3cExcVecSet()建立的),因此需要在SDRAM起始地址处填写如下代码(反汇编后形式):
ldr pc,=0xf00000fc /* 复位,此时无用,don't care */
ldr pc,=0x00000100
ldr pc,=0x00000104
ldr pc,=0x00000108
ldr pc,=0x0000010c
ldr pc,=0x00000110 /* 保留,don't care */
ldr pc,=0x00000114
ldr pc,=0x00000118 /* vxWorks不使用FIQ,don't care */
获取上面汇编代码对应的机器码0xe59ff0f4,0xe59ff0f4,0xe59ff0f4,0xe59ff0f4,0xe59ff0f4,0xe59ff0f4,0xe59ff0f4,0xe59ff0f4(至于为什么全是一样的数据,我也没搞很清除,估计应该是ldr的实现机理决定的)。
将这8个DWORD写入到SDRAM起始地址处即完成异常向量跳转表的创建;
在sysLib.c中添加函数
void s3cExcVecTblInstall()
{
int i;
long excVecTbl[] = {0xe59ff0f4,0xe59ff0f4,0xe59ff0f4,0xe59ff0f4,0xe59ff0f4,0xe59ff0f4,0xe59ff0f4,0xe59ff0f4};
for(i=0;i<8;i++)
{
*((volatile unsigned long*)(LOCAL_MEM_LOCAL_ADRS + i*4)) = excVecTbl[i];
}
return;
}
并在sysHwInit()中调用s3cExcVecSet()
void sysHwInit(void)
{
/* install the IRQ/SVC interrupt stack splitting routine */
_func_armIntStackSplit = sysIntStackSplit;
#ifdef INCLUDE_SERIAL
/* initialise the serial devices */
sysSerialHwInit (); /* initialise serial data structure */
#endif /* INCLUDE_SERIAL */
s3cExcVecTblInstall();
s3cExcVecSet();
}
其他一些说明:
对于异常向量表的建立和处理,本应该由vxWorks来完成,事实上,在bootConfig.c中的usrIni ()函数中,有intVecBaseSet()和excVecInit()函数调用,不过对于ARM体系的CPU来说,由于不存在vector base register,因此intVecBaseSet实际上是一个空操作(分析汇编可看出就是一个return);而由于调用excVecInit()函数时,处于0x100(异常函数处理指针)地址处的是ROM(此函数调用在MMU启用之前,因此即使在vxWorks映象中启用MMU并进行非平坦地址映射,0x100还是对应ROM),因此尽管excVecInit运行了,但是没起到实际作用,也就是没能在SDRAM偏移0x100处建立异常函数处理指针。
s3cExcVecSet()和rom中写入的异常向量编码(对于BOOTROM和vxworks平坦地址映射)以及s3cExcVecTblInstall(对于vxworks非平坦地址映射)实际上起到了取代excVecInit()的作用。
从bootConfig.c代码中可以看出,intVecBaseSet()和excVecInit ()之后紧接着调用了sysHwInit (),因此在sysHwInit调用s3cExcVecSet和s3cExcVecTblInstall。
(更多关于异常向量的内容可参见 “44B0x的BSP是如何调成的”以及Amine的“S3C44B0X VxWorks BSP 移植笔记”)。
ARM没有专门的中断控制器,对于中断的操作只能通过操作有限的几个寄存器实现。并且由于不同厂家生产的不同型号的ARM处理器提供的中断寄存器是不一样的,因此vxWorks没有为ARM提供中断控制的库函数,这样一来,就需要在BSP里面自己编写中断控制程序。
在我们的BSP里面,中断控制程序位于s3c2440aIntrCtl.c,程序比较简单,主要是进行中断寄存器的初始化以及实现并注册三个回调函数。但是由于中断控制实现的特殊性,有必要说明编写ISR时要注意的问题,建议编写ISR时此节必看。
1.关于中断enable/disable
对于s3c2440a,enable/disable中断本质上就是对相应中断的掩码位进行操作(INTMSK寄存器),但是在应用程序中应该调用intEnable()和intDisable()函数来实现,而不是直接操作INTMSK寄存器。
这是因为调用intEnable()和intDisable()时会通过回调函数最终调用到s3c2440aIntLvlEnable()和s3c2440aIntLvlDisable(),从源代码中可以看见,存在一个全局变量s3c2440aIntLvlEnabled,它记录了当前打开和关闭的中断的情况,enable和disable操作都是现对这个变量相应位赋值,然后将其赋给INTMSK寄存器。如果用写地址方式直接修改INTMSK寄存器,那么下一次调用intEnable()或intDisable()时会使直接修改的控制位会到以前的情况,从而使得中断控制操作无效。
这个问题在我调试中断时遇到过,当时使用直接操作INTMSK寄存器的方式,发现外部中断INT4-7对应的比特位在使能后不久就被disable了,后来使用ADS跟踪执行汇编代码,发现当执行printf语句时,由于printf向控制台打印信息会产生串口中断,在串口中断执行过程中使得INT4-7对应的比特位被disable掉,这才发现时直接操作INTMSK寄存器惹得祸。
2.带有子中断的中断操作
如串口中断等,存在发送中中断、接受中断和错误中断3个子中断。以串口中断为例,当中断发生时,会先执行s3c2440aIntLvlVecChk()函数(不是很清楚这个过程,但是跟踪汇编代码可以肯定这点),在s3c2440aIntLvlVecChk里面首先将UART中断的3个子中断全部禁止掉(通过设置INTSUBMSK对应控制位),然后清除SRCPND和INTPND寄存器中相应的比特位。接下来就进入到ISR,在ISR里面,通过读取SUBSRCPND寄存器,判断当前发生的是哪一个子中断,然后进行相应的处理并清除SUBSRCPND寄存器中相应的比特位,最后设置INTSUBMSK寄存器打开需要的子中断。
从上面的流程可以看出,中断的禁止与重新打开实际上是对子中断掩码进行操作来完成的,并没有涉及到INTMSK的操作(intEnable/intDisable)。
对于其它带有子中断的中断类型,建议也按照这个方式进行处理,具体可以参考串口驱动的相关代码。另外,由于目前不清楚其他带有子中断的中断类型的具体情况,因此s3c2440aIntLvlVecChk()函数里只有串口中断处理的相关代码,其它的情况暂时留空,如果需要编写相应的ISR,切记完善s3c2440aIntLvlVecChk()函数。
3.外部中断EINT4_7和EINT8_23
在INTMSK、INTPND和SRCPND寄存器里面,外部中断EINT4_7和EINT8_23分别只对应一个控制位,然后在EINTMASK、EINTPND再对具体是那一个外部中断进行处理。
与带有子中断的中断操作的中断操作类似,当某一外部中断产生时(以EINT6为例),先执行s3c2440aIntLvlVecChk()函数,在s3c2440aIntLvlVecChk里面首先将EINTMASK中EINT4~EINT7对应的控制位全部禁止掉,然后清除SRCPND和INTPND寄存器中相应的比特位。接下来在ISR里面进行了需要的操作之后,清除EINTPEND寄存器相应比特位,然后设置EINTMASK以打开EINT6的中断。
目前的操作是针对EINT4_7或EINT8_23中仅有一个外部中断发生的情况。以EINT4_7为例,如果存在多个外部中断源可能产生中断,则情况比较复杂,对这种情况没有做过测试,因此也无法得到切实可用的代码,但是可能的操作描述如下,仅供将来参考:在s3c2440aIntLvlVecChk()中禁止EINT4~EINT7对应的控制位(EINTMASK中)之前,先使用一个全局变量(例如eintMaskStatus)保存EINTMASK中EINT4~EINT7的当前值。对于ISR来说,需要使用一个总的ISR处理EINT4~EINT7所有的中断,在进入该ISR后,先通过读取EINTPND判断当前发生的是哪一个外部中断,接下来在完成相应操作之后清除EINTPND相应比特位,最后根据eintMaskStatus的值恢复EINTMASK中EINT4~EINT7的值(即打开相应的掩码位)。
这样做是因为EINT4~EIN T7显然处于同一优先级,当EINT4(例如)来中断时,必须禁止EINT5~EINT7。采用全局变量eintMaskStatus(在ISR所在源文件中要用extern声明)是为了确保不会误disable其它需要的外部中断。另外,在EINT4_7的ISR结束处恢复各个相应外部中断掩码时要注意不能干扰EINT8_23对应的外部中断掩码,反之亦然,以免产生误操作。
4.其它的中断(包括外部中断0、1、2、3)
其它的中断(包括外部中断0、1、2、3)在INTMSK、INTPND和SRCPND寄存器里面都有自己的控制位,因此操作相对简单。此时,若发生中断,由于s3c2440aIntLvlVecChk()中清除了SRCPND和INTPND寄存器中相应的比特位,因此在ISR中无需再做其它操作。不过在进入和退出ISR时加上intEnable和intDisable似乎也没什么问题。
5.例子
对于带有子中断的中断操作可以参照串口中断。
对于外部中断EINT4_7和EINT8_23,可以参照网络驱动(eint7)和intTest.c(eint6)。在intTest.c中,eintTestInit()、eTestInt()、eintTest()是外部中断6的测试代码。若在BOOTROM中进行测试,在syslib.c中合适位置处添加#include "intTest.c",在bootConfig.c的合适位置处添加eintTest()的声明,并在usrRoot()函数的最后一行代码(taskSpawn……)之前添加eintTest()的调用。另外必须在CPLD中将BOTTON1指定到EINT6 (EINT6 <= not BOTTON1),并焊接RP552、D551和S551。当BOOTROM进行倒数计数时按任意键进入命令行模式,此时按botton 1可以在超级中断看见打印信息“interrupt: eTestInt6,entered”。对于其它的中断,以外部中断2为例,可以参考intTest.c(eint2),其中eintTestInit2()、eTestInt2()、eintTest2()是外部中断2的测试代码。若在BOOTROM中进行测试,在syslib.c中合适位置处添加#include "intTest.c",在bootConfig.c的合适位置处添加eintTest2()的声明,并在usrRoot()函数的最后一行代码(taskSpawn……)之前添加eintTest2()的调用。另外必须在CPLD中将BOTTON2指定到EINT2 (EINT2 <= not BOTTON2),并焊接RP552、D552和S552。当BOOTROM进行倒数计数时按任意键进入命令行模式,此时按botton 2可以在超级中断看见打印信息“interrupt: eTestInt2,entered”。
MMU只在vxWorks中才使用,BOOTROM不会用到。在S3C2440A中使用MMU,需要异常向量处理的配合,详见“异常向量”相关部分的说明。
启用MMU时,要在TORNADO里面vxWorks组建配置串口包含hardwareàmemoryàMMUàMMU Modeàbasic MMU support或full MMU support。
sysLib.c中的sysPhysMemDesc可配置是使用平坦地址映射还是非平坦地址映射,使用非平坦地址映射时,由于异常向量跳转表存在SDRAM中,访问速度快于ROM,因此有利于提高系统性能。
若使用MMU时,需要正确配置sysPhysMemDesc数组。对任意内存空间的访问,必须在sysPhysMemDesc配置相应的项。若添加新的外设,该外设对应的内存空间必须在sysPhysMemDesc中配置。
网络芯片采用DAVICOM的DM9000E。驱动程序采用DAVICOM提供的v1.11版本,但是他们给的驱动是DM9000A并且基于x86的,移植自linux,并且其版本最后修正时间06年4月,存在不少问题,因此这方面的工作主要是对他们的驱动进行修改个完善,以适应我们的系统。
1.由于原来的驱动是基于x86的,因此对于DM9000_IN_ADDR、DM9000_IN_ADDR、DM9000_OUT_ADDR、DM9000_IN_BYTE、DM9000_OUT_BYTE、DM9000_IN_WORD、DM9000_OUT_WORD、DM9000_OUT_CHAR、DM9000_IN_CHAR这些宏需要重新定义。
2.关于CPU中相关寄存器的初始化。DM9000E所占用的memory bank的访问属性在BSP中的romInit中已经设置,在其驱动里面的不再做相关工作。为DM9000分配外部中断号、设置外部中断产生方式等操作在cpuForDM9000Init()函数里定义,该函数在dm9000Start()中调用。另外在dm9000Stop()中调用cpuForDm9000disable(),作用是关闭eint7对应的外部中断掩码。
3.在configNet.h中将DM9K_LOAD_STRING改为"0x4:0x4",这是CPU中EINT4-7的中断级别,另外删除dm9000end.c中dm9000Parse()函数里面关于IO Base的部分。因为arm不存在IO访问的说法。
4.原来的驱动是从SROM中获取MAC地址,我们不使用MAC地址,因此在程序里面定义全局变量DM9KdefaultMacAddr,用来记录MAC地址,并在dmfe_reset_dm9000()里面将其赋给dev->enetAddr。需要注意的是,当在以太网上使用多块板子时,要注意为每块板子修改MAC地址(重烧BOOTROM和重编译vxWorks),否则多块板子会使用同一个MAC地址,从而出现网络不同的情况。
5.32bit对齐问题。原来的驱动是基于x86的,x86不存在对齐问题,因为x86硬件可以自动将程序中没有对齐的地方对齐。但是移植到ARM上就有问题。主要是typedef struct end_device{…}END_DEVICE这个结构。它的最后一个成员txBuf没有处于32bit对齐的位置,这样在将其强制类型转化成word时(dmfe_start_xmit()函数发送数据时)会访问到非法数据。解决办法是在END_DEVICE结构体的txBuf成员以前添加
UCHAR dummyBytes[3];
从而使得txBuf处于32bit对齐的位置。DummyBytes不做其它用途。
typedef struct end_device
{
END_OBJ end; /* The class we inherit from. */
END_ERR err;
UINT IOBase;
int unit; /* unit number */
int ivec; /* interrupt vector */
int ilevel; /* interrupt level */
long flags; /* Our local flags. */
UCHAR enetAddr[6]; /* ethernet address */
CACHE_FUNCS cacheFuncs; /* cache function pointers */
CL_POOL_ID pClPoolId; /* cluster pool */
BOOL rxHandling; /* rcv task is scheduled */
UCHAR io_mode; /* 0:word, 2:byte */
char tx_pkt_cnt;
char device_wait_reset;
UWORD queue_pkt_len;
UCHAR reg0; /* registers saved */
UCHAR nic_type; /* NIC type */
UCHAR op_mode; /* PHY operation mode */
UCHAR mcastFilter[8]; /* multicast filter */
UCHAR dummyBytes[3]; /* dummyBytes, for 32-bit align */
UCHAR txBuf[ETHERMTU + EH_SIZE + 6 + 64 ];
} END_DEVICE;
6.中断处理。在ISR里面要将x86中断操作的内容删掉,然后加入ARM下处理外部中断的操作。原理可过程可参考“中断”一节的内容,另外,目前的操作是当ISR退出时先开CPU中断,再开DM9000E的片上中断,不知道反过来行不行,会不会有什么影响。
7.发送处理。通过对比发现,原来的驱动移植自linux,但是移植的却不完全,一个很大的问题体现在发送处理上面。
DM9000E有两个发送缓存,当发送一个缓存中的数据时,可以向另一个缓存写数据,以加快传送速度。而DAVICOM提供的驱动里面,只用到了一个缓存:
static int dmfe_start_xmit( PKT skb, END_DEVICE dev) /*原驱动程序发送函数*/
{
… …
… …
… …
dev->queue_pkt_len = skb->len;
dev->tx_pkt_cnt ++;
/* Set TX length to DM9000 */
DM9000_OUT_CHAR( 0xfc, skb->len & 0xff );
DM9000_OUT_CHAR( 0xfd, (skb->len >> 8) & 0xff );
/* Issue TX polling command */
DM9000_OUT_CHAR( 0x2, 0x01 ); /* Cleared after TX complete */
return 0;
}
这样以来,要是CPU向DM9000E发送数据的速度较快时,一个缓冲区会忙不过来,就会出现“tx full”,然后网卡工作不正常,无法继续进行通信。为了实现稳定告诉的通信,需要修改发送过程,参考linux下DM9000E的驱动程序,对vxWorks驱动修改如下:
static int dmfe_start_xmit( PKT *skb, END_DEVICE *dev)
{
… …
… …
… …
/* TX control: First packet immediately send, second packet queue */
if (dev->tx_pkt_cnt == 0)
{
/* First Packet */
dev->tx_pkt_cnt ++;
/* Set TX length to DM9000 */
DM9000_OUT_CHAR( 0xfc, skb->len & 0xff );
DM9000_OUT_CHAR( 0xfd, (skb->len >> 8) & 0xff );
/* Issue TX polling command */
DM9000_OUT_CHAR( 0x2, 0x01 ); /* Cleared after TX complete */
}
else /* Second packet */
{
dev->queue_pkt_len = skb->len;
dev->tx_pkt_cnt ++;
}
return 0;
}
还要修改dm9000Int()
static void dm9000Int( END_DEVICE *pDrvCtrl ) /* interrupting device */
{
… …
… …
… …
/* TODO - handle transmit interrupts */
if( isr_status & 2 )
{
DRV_LOG (DRV_DEBUG_INT, "Got an TX interrupt!", pDrvCtrl->tx_pkt_cnt, 2, 3, 4, 5, 6);
DRV_SP( DRV_PK,"x" );
DM9000_IN_CHAR(0x1, TX_comple_status);
/* if (TX_comple_status & 0x4) pDrvCtrl->tx_pkt_cnt--;
if (TX_comple_status & 0x8) pDrvCtrl->tx_pkt_cnt--; */
if (TX_comple_status & 0xc)
{
/* One packet sent complete */
pDrvCtrl->tx_pkt_cnt--;
/* Queue packet check & send */
if (pDrvCtrl->tx_pkt_cnt > 0)
{
DM9000_OUT_CHAR( 0xfc, pDrvCtrl->queue_pkt_len & 0xff );
DM9000_OUT_CHAR( 0xfd, (pDrvCtrl->queue_pkt_len >> 8) & 0xff );
DM9000_OUT_CHAR( 0x2, 0x01 );
}
}
}
… …
… …
… …
}
修改后,不会再出现网络不稳定的情况(并且可以ping大包了)。使用赵海涛的程序测试网络速度(TCP),发现使用MMU时(非平坦地址映射),可达22Mbps左右;关闭MMU但是使能cache时,略大于10Mbps;同时关闭MMU和cache,只有略大于6Mbps的速度。使用平坦地址映射MMU模式下的网络速度没有测试。
8.延时问题。linux的驱动采用uDelay进行延时,但是DAVICON提供的vxWorks驱动对这个问题处理不好,从而使得延时过长,影响运行速度。考虑到默认情况下taskDelay(1)延时0.01s,已远大于DM9000任何操作要求的最小延时,因此在需要延时的地方改为taskDelay(1)。