uCOS在51单片机上的移植心得

引言

    很多人对于编写自己的操作系统很感兴趣,uCOS51是个不错的选择。它的优点是简单易懂,学习成本低,有利于向32位CPU过渡。目前,嵌入式BBS上的热点是:嵌入式实时多任务操作系统、单片机上网、32bitCPU(如ARM等)。其实通过uCOS51学习完全可以掌握这些热门技术的精髓,而且学习成本低廉。为此我会陆续将我在研发过程中的经验体会写出来与大家交流,共同进步。
    我准备讨论以下内容:uCOS51高效内核、OS人机界面SHELL的编写、51机开发板的硬件设计、RTL8019AS网卡驱动程序、51TCP/IP协议栈设计……在51上的实现技术、51OS任务划分和应用程序实例、由51软件系统向ARM的移植以及其他想到的题目。欢迎大家积极参与。

    注:提供全套开发板原理图、PCB图、GAL烧录文件、芯片手册、全部源程序,文档,可以来信索取报价(asdjf@163.com)。

    讨论1----uCOS51高效内核

    前一段时间,我参与了一个SNMP网管板的项目,我负责硬件设计和单板软件开发。该板的硬件由MCS51+RTL8019AS组成,有64K FLASH 和64K SRAM。软件部分有操作系统和TCPIP协议栈。硬件比较简单,用了一个月就搞定了,协议栈我参考了老古开发板的部分程序又上网找了SNMP源代码也很快完成了,但是测试时发现当使用较低时钟频率的CPU时(为了降低成本),由于ASN.1编解码部分过于庞大,而我的程序又是一个大循环,AGENT的响应速度受到严重影响,用户界面也反应迟钝。更坏的消息是公司为了适应市场需求,还要在上面跑PPP和HTTP。那样的话,我就得用40MHz的AT89C51RD2或者人为的把程序断成几部分然后用状态机的方法在运行时再把它们连接起来。不过,我不想增加成本,也不想把程序搞乱,迫不得已,只好使用操作系统。
    说实在的,一开始我也不是很有把握,一来我不清楚51的FLASH是否装得下这么多代码,二来我只做过OS应用开发,对于它的移植想都不敢想。不过,我在BBS上搜索了一阵儿后还是有了一些头绪。我找到了几个OS的源代码(我喜欢用现成的),按照代码大小、实时性、使用人数、众人口碑等标准,最后选定了uCOS2。我感觉它的实时性有保障,延时可预测,代码据说可小到2K,网上讨论这个话题的人也比较多,而且它的网站上有针对KEIL C51的移植实例。
    经过一番查找,我得到了5个版本。其中3个是用KEIL编译的。本来我想直接把OS代码嵌到应用程序中,但后来发现没有一个可以直接使用。有的无法用KEIL直接编译,有的需要修改DLL在软件仿真下使用。而我需要的是能在串口输入输出,不需要修改任何无关软件,能在软件仿真和硬件上运行的实时多任务操作系统。没有办法,我只好硬着头皮去改编。
    我分析了自己的劣势:1。KEIL刚开始使用,不太熟悉;2。混合编程以前从没有作过;3。时间紧迫,要在1个月内搞定。而我的优势就是有5个移植实例可供参考,可以上网查资料。一开始,我用“堆栈”、“混合编程”、“汇编”、“ucos”等关键字在C51BBS和老古论坛上检索相关信息并逐条阅读,读过之后,头脑中的思路逐渐清晰了。我了解到在KEIL的HLP目录下有A51.PDF和C51.PDF非常全面的介绍了汇编和C51,是KEIL的权威用户手册;SP初始化、内存清0等操作在STARTUP.A51文件中实现,用户可以改写它;KEIL的变量,子程序等的分配信息可以在.M51文件里查到;KEIL自己的论坛里有很多疑难问题的解答……通过阅读并经过思考,解决了堆栈起点、堆栈空间大小的设定等关键问题。论坛里的问题有些是我没有想到的,这使我发现了自己的疏漏。
    在网上获得大量信息后,我开始阅读《uCOSII》中文版,一共读了3遍。第一遍是浏览,了解到uCOSII包括任务调度、时间管理、内存管理、资源管理(信号量、邮箱、消息队列)四大部分,没有文件系统、网络接口、输入输出界面。它的移植只与4个文件相关:汇编文件(OS_CPU_A.ASM)、处理器相关C文件(OS_CPU.H、OS_CPU_C.C)和配置文件(OS_CFG.H)。有64个优先级,系统占用8个,用户可创建56个任务,不支持时间片轮转。第二遍主要是把整个工作过程在头脑里过了一下,不懂的地方有针对性地查书,重点是思考工作原理和流程。我发现其实它的思路挺简单的。就是 “近似地每时每刻总是让优先级最高的就绪任务处于运行状态” 。为了保证这一点,它在调用系统API函数、中断结束、定时中断结束时总是执行调度算法。原作者通过事先计算好数据,简化了运算量,通过精心设计就绪表结构,使得延时可预知。任务的切换是通过模拟一次中断实现的。第三遍重点看了移植部分的内容。对照实例,研究了代码的具体实现方法。
    前期准备用了20几天,真正编写代码只用了1.5天,调试用了2天。具体过程如下:
    (1)拷贝书后附赠光盘sourcecode目录下的内容到C:\YY下,删除不必要的文件和EX1L.C,只剩下p187(《uCOSII》)上列出的文件。
    (2)改写最简单的OS_CPU.H
       数据类型的设定见C51.PDF第176页。注意BOOLEAN要定义成unsigned char 类型,因为bit类型为C51特有,不能用在结构体里。
       EA=0关中断;EA=1开中断。这样定义即减少了程序行数,又避免了退出临界区后关中断造成的死机。
       MCS-51堆栈从下往上增长(1=向下,0=向上),OS_STK_GROWTH定义为0
       #define  OS_TASK_SW() OSCtxSw() 因为MCS-51没有软中断指令,所以用程序调用代替。两者的堆栈格式相同,RETI指令复位中断系统,RET则没有。实践表明,对于MCS-51,用子程序调用入栈,用中断返回指令RETI出栈是没有问题的,反之中断入栈RET出栈则不行。总之,对于入栈,子程序调用与中断调用效果是一样的,可以混用。在没有中断发生的情况下复位中断系统也不会影响系统正常运行。详见《uC/OS-II》第八章193页第12行
    (3)改写OS_CPU_C.C
       我设计的堆栈结构如下图所示:



       TCB结构体中OSTCBStkPtr总是指向用户堆栈最低地址,该地址空间内存放用户堆栈长度,其上空间存放系统堆栈映像,即:用户堆栈空间大小=系统堆栈空间大小+1。
       SP总是先加1再存数据,因此,SP初始时指向系统堆栈起始地址(OSStack)减1处(OSStkStart)。很明显系统堆栈存储空间大小=SP-OSStkStart。
       任务切换时,先保存当前任务堆栈内容。方法是:用SP-OSStkStart得出保存字节数,将其写入用户堆栈最低地址内,以用户堆栈最低地址为起址,以OSStkStart为系统堆栈起址,由系统栈向用户栈拷贝数据,循环SP-OSStkStart次,每次拷贝前先将各自栈指针增1。
       其次,恢复最高优先级任务系统堆栈。方法是:获得最高优先级任务用户堆栈最低地址,从中取出“长度”,以最高优先级任务用户堆栈最低地址为起址,以OSStkStart为系统堆栈起址,由用户栈向系统栈拷贝数据,循环“长度”数值指示的次数,每次拷贝前先将各自栈指针增1。
       用户堆栈初始化时从下向上依次保存:用户堆栈长度(15),PCL,PCH,PSW,ACC,B,DPL,DPH,R0,R1,R2,R3,R4,R5,R6,R7。不保存SP,任务切换时根据用户堆栈长度计算得出。
       OSTaskStkInit函数总是返回用户栈最低地址。
       操作系统tick时钟我使用了51单片机的T0定时器,它的初始化代码用C写在了本文件中。
       最后还有几点必须注意的事项。本来原则上我们不用修改与处理器无关的代码,但是由于KEIL编译器的特殊性,这些代码仍要多处改动。因为KEIL缺省情况下编译的代码不可重入,而多任务系统要求并发操作导致重入,所以要在每个C函数及其声明后标注reentrant关键字。另外,“pdata”、“data”在uCOS中用做一些函数的形参,但它同时又是KEIL的关键字,会导致编译错误,我通过把“pdata”改成“ppdata”,“data”改成“ddata”解决了此问题。OSTCBCur、OSTCBHighRdy、OSRunning、OSPrioCur、OSPrioHighRdy这几个变量在汇编程序中用到了,为了使用Ri访问而不用DPTR,应该用KEIL扩展关键字IDATA将它们定义在内部RAM中。
    (4)重写OS_CPU_A.ASM
       A51宏汇编的大致结构如下:
       NAME 模块名    ;与文件名无关
       ;定义重定位段   必须按照C51格式定义,汇编遵守C51规范。段名格式为:?PR?函数名?模块名
       ;声明引用全局变量和外部子程序  注意关键字为“EXTRN”没有‘E’
           全局变量名直接引用
           无参数/无寄存器参数函数 FUNC
           带寄存器参数函数      _FUNC
           重入函数            _?FUNC
       ;分配堆栈空间
           只关心大小,堆栈起点由keil决定,通过标号可以获得keil分配的SP起点。切莫自己分配堆栈起点,只要用DS通知KEIL预留堆栈空间即可。
           ?STACK段名与STARTUP.A51中的段名相同,这意味着KEIL在LINK时将把两个同名段拼在一起,我预留了40H个字节,STARTUP.A51预留了1个字节,LINK完成后堆栈段总长为41H。查看yy.m51知KEIL将堆栈起点定在21H,长度41H,处于内部RAM中。
       ;定义宏
           宏名 MACRO  实体  ENDM
       ;子程序
           OSStartHighRdy
           OSCtxSw
           OSIntCtxSw
           OSTickISR
           SerialISR
       END           ;声明汇编源文件结束

       一般指针占3字节。+0类型+1高8位数据+2低8位数据 详见C51.PDF第178页
       低位地址存高8位值,高位地址存低8位值。例如0x1234,基址+0:0x12 基址+1:0x34

    (5)移植串口驱动程序
       在此之前我写过基于中断的串口驱动程序,包括打印字节/字/长字/字符串,读串口,初始化串口/缓冲区。把它改成重入函数即可直接使用。
       系统提供的显示函数是并发的,它不是直接显示到串口,而是先输出到显存,用户不必担心IO慢速操作影响程序运行。串口输入也采用了同样的技术,他使得用户在CPU忙于处理其他任务时照样可以盲打输入命令。
    (6)编写测试程序Demo(YY.C)
       Demo程序创建了3个任务A、B、C优先级分别为2、3、4,A每秒显示一次,B每3秒显示一次,C每6秒显示一次。从显示结果看,显示3个A后显示1个B,显示6个A和2个B后显示1个C,结果显然正确。
       显示结果如下:
       AAAAAA111111 is active
       AAAAAA111111 is active
       AAAAAA111111 is active
       BBBBBB333333 is active
       AAAAAA111111 is active
       AAAAAA111111 is active
       AAAAAA111111 is active
       BBBBBB333333 is active
       CCCCCC666666 is active
       AAAAAA111111 is active
       AAAAAA111111 is active
       AAAAAA111111 is active
       BBBBBB333333 is active
       AAAAAA111111 is active
       AAAAAA111111 is active
       AAAAAA111111 is active
       BBBBBB333333 is active
       CCCCCC666666 is active
       Demo程序经Keil701编译后,代码量为7-8K,可直接在KeilC51上仿真运行。
       编译时要将OS_CPU_C.C、UCOS_II.C、OS_CPU_A.ASM、YY.C加入项目

    以上是我这次移植uCOS51的一些心得,写出来只是让准备在51上运行操作系统的同行们少走弯路并增强使用信心。我强烈推荐大家在自己的51系统中使用uCOS这个简单实用的自己的操作系统。它的大小应该不是问题,性能上的提高却是显著的。但愿此文能对朋友们有所帮助,错误在所难免,希望各位大虾指正,诸位高手们见笑了!

注:全部源码可来信索要(asdjf@163.com),以下仅为关键代码部分。     

子程序

        RSEG ?PR?OSCtxSw?OS_CPU_A
OSCtxSw:    
        PUSHALL

OSIntCtxSw_in:

        ;获得堆栈长度和起址
        MOV  A,SP
        CLR  C
        SUBB A,#OSStkStart
        MOV  R5,A     ;获得堆栈长度        

        ;OSTCBCur ===> DPTR  获得当前TCB指针,详见C51.PDF第178页
        MOV  R0,#LOW (OSTCBCur) ;获得OSTCBCur指针低地址,指针占3字节。+0类型+1高8位数据+2低8位数据
        INC  R0
        MOV  DPH,@R0    ;全局变量OSTCBCur在IDATA中
        INC  R0
        MOV  DPL,@R0

        ;OSTCBCur->OSTCBStkPtr ===> DPTR  获得用户堆栈指针
        INC  DPTR        ;指针占3字节。+0类型+1高8位数据+2低8位数据
        MOVX A,@DPTR     ;.OSTCBStkPtr是void指针
        MOV  R0,A
        INC  DPTR
        MOVX A,@DPTR
        MOV  R1,A
        MOV  DPH,R0
        MOV  DPL,R1

        ;保存堆栈长度
        MOV  A,R5
        MOVX @DPTR,A

        MOV  R0,#OSStkStart  ;获得堆栈起址
save_stack:

        INC  DPTR
        INC  R0
        MOV  A,@R0
        MOVX @DPTR,A
        DJNZ R5,save_stack

        ;调用用户程序
        LCALL _?OSTaskSwHook

        ;OSTCBCur = OSTCBHighRdy
        MOV  R0,#OSTCBCur
            MOV  R1,#OSTCBHighRdy
            MOV  A,@R1
        MOV  @R0,A
        INC  R0
            INC  R1
            MOV  A,@R1
        MOV  @R0,A
        INC  R0
            INC  R1
            MOV  A,@R1
        MOV  @R0,A

        ;OSPrioCur = OSPrioHighRdy  使用这两个变量主要目的是为了使指针比较变为字节比较,以便节省时间。
        MOV  R0,#OSPrioCur
            MOV  R1,#OSPrioHighRdy
            MOV  A,@R1
        MOV  @R0,A

        LJMP OSCtxSw_in
;-------------------------------------------------------------------------
        RSEG ?PR?OSIntCtxSw?OS_CPU_A

OSIntCtxSw:

        ;调整SP指针去掉在调用OSIntExit(),OSIntCtxSw()过程中压入堆栈的多余内容
        ;SP=SP-4

        MOV  A,SP
        CLR  C
        SUBB A,#4
        MOV  SP,A

        LJMP OSIntCtxSw_in
;-------------------------------------------------------------------------
        CSEG AT 000BH    ;OSTickISR
        LJMP OSTickISR   ;使用定时器0
        RSEG ?PR?OSTickISR?OS_CPU_A

OSTickISR:        

        USING 0        
        PUSHALL

        CLR  TR0
        MOV  TH0,#70H    ;定义Tick=50次/秒(即0.02秒/次)
        MOV  TL0,#00H    ;OS_CPU_C.C  和  OS_TICKS_PER_SEC
        SETB TR0

        LCALL _?OSIntEnter
        LCALL _?OSTimeTick
        LCALL _?OSIntExit
        POPALL        
        RETI
;-------------------------------------------------------------------------
        CSEG AT 0023H    ;串口中断
        LJMP SerialISR   ;工作于系统态,无任务切换。
        RSEG ?PR?_?serial?OS_CPU_A

SerialISR:

        USING 0        
        PUSHALL
        CLR  EA
        LCALL _?serial        
        SETB EA
        POPALL        
        RETI
;-------------------------------------------------------------------------
        END
;-------------------------------------------------------------------------

文件名 : OS_CPU_C.C

void *OSTaskStkInit (void (*task)(void *pd), void *ppdata, void *ptos, INT16U opt) reentrant
{    
    OS_STK *stk;

    ppdata = ppdata;
    opt    = opt;                           //opt没被用到,保留此语句防止告警产生    
    stk    = (OS_STK *)ptos;                //用户堆栈最低有效地址
    *stk++ = 15;                            //用户堆栈长度
    *stk++ = (INT16U)task & 0xFF;           //任务地址低8位
    *stk++ = (INT16U)task >> 8;             //任务地址高8位    
    *stk++ = 0x00;                          //PSW
    *stk++ = 0x0A;                          //ACC
    *stk++ = 0x0B;                          //B
    *stk++ = 0x00;                          //DPL
    *stk++ = 0x00;                          //DPH
    *stk++ = 0x00;                          //R0
    *stk++ = 0x01;                          //R1
    *stk++ = 0x02;                          //R2
    *stk++ = 0x03;                          //R3
    *stk++ = 0x04;                          //R4
    *stk++ = 0x05;                          //R5
    *stk++ = 0x06;                          //R6
    *stk++ = 0x07;                          //R7
                                            //不用保存SP,任务切换时根据用户堆栈长度计算得出。    
    return ((void *)ptos);
}

#if OS_CPU_HOOKS_EN
void OSTaskCreateHook (OS_TCB *ptcb) reentrant
{
    ptcb = ptcb;                       /* Prevent compiler warning                                     */
}

void OSTaskDelHook (OS_TCB *ptcb) reentrant
{
    ptcb = ptcb;                       /* Prevent compiler warning                                     */
}

void OSTimeTickHook (void) reentrant
{
}
#endif

//初始化定时器0
void InitTimer0(void) reentrant
{
    TMOD=TMOD&0xF0;
    TMOD=TMOD|0x01;    //模式1(16位定时器),仅受TR0控制
    TH0=0x70;    //定义Tick=50次/秒(即0.02秒/次)
    TL0=0x00;    //OS_CPU_A.ASM  和  OS_TICKS_PER_SEC
    ET0=1;       //允许T0中断
    TR0=1;   
}

文件名 : YY.C

#include <includes.h>

#define MAX_STK_SIZE 64

void TaskStartyya(void *yydata) reentrant;
void TaskStartyyb(void *yydata) reentrant;
void TaskStartyyc(void *yydata) reentrant;

OS_STK TaskStartStkyya[MAX_STK_SIZE+1];//注意:我在ASM文件中设置?STACK空间为40H即64,不要超出范围。
OS_STK TaskStartStkyyb[MAX_STK_SIZE+1];//用户栈多一个字节存长度
OS_STK TaskStartStkyyc[MAX_STK_SIZE+1];

void main(void)
{
    OSInit();

    InitTimer0();
    InitSerial();
    InitSerialBuffer();

    OSTaskCreate(TaskStartyya, (void *)0, &TaskStartStkyya[0],2);
    OSTaskCreate(TaskStartyyb, (void *)0, &TaskStartStkyyb[0],3);
    OSTaskCreate(TaskStartyyc, (void *)0, &TaskStartStkyyc[0],4);

    OSStart();
}


void TaskStartyya(void *yydata) reentrant
{
    yydata=yydata;
    clrscr();
    PrintStr("\n\t\t*******************************\n");
    PrintStr("\t\t*     Hello! The world.       *\n");
    PrintStr("\t\t*******************************\n\n\n");

    for(;;){
        PrintStr("\tAAAAAA111111 is active.\n");
        OSTimeDly(OS_TICKS_PER_SEC);    
    }    
}

void TaskStartyyb(void *yydata) reentrant
{
    yydata=yydata; 

    for(;;){
        PrintStr("\tBBBBBB333333 is active.\n");
        OSTimeDly(3*OS_TICKS_PER_SEC);    
    }    
}

void TaskStartyyc(void *yydata) reentrant
{
    yydata=yydata; 

    for(;;){
        PrintStr("\tCCCCCC666666 is active.\n");
        OSTimeDly(6*OS_TICKS_PER_SEC);    
    }    
}

 

人机界面积极参与应用程序网卡驱动IP协议

相关帖子

我的博客:
http://blog.sina.com.cn/u/2766726333

  回复 收藏12 分享 淘帖 

举报

   

wangch_sh

318

主题

2823

帖子

8542

积分

技术达人

专家等级:

结帖率:100%

沙发

 发表于 2014-12-9 10:05 | 只看该作者 |返回版面

顶一个

为江山踏坏了乌骓马,为社稷拉断了宝雕弓。

  回复 

举报

   

百能网PCB

586

主题

4722

帖子

1万

积分

禁止访问

专家等级:

结帖率:100%

板凳

 发表于 2014-12-9 10:45 | 只看该作者 |返回版面

提示: 作者被禁止或删除 内容自动屏蔽

www.pcbpartner.cn

  回复 

举报

   

yklstudent

     

25

主题

2425

帖子

7501

积分

高级工程师

专家等级:

结帖率:56%

打赏:0.00

受赏:15.00

地板

 发表于 2014-12-9 12:48 | 只看该作者 |返回版面

一般的51移植只能做学习用,实际上有多少人会这么用呢

本人熟悉STM32、PIC、AVR等嵌入式软件开发;联系方式:524716771.

  回复 

举报

   

ccmc

7

主题

564

帖子

1767

积分

助理工程师

专家等级:

结帖率:100%

5楼

 发表于 2014-12-10 10:17 | 只看该作者 |返回版面

66666

21ic公开课新版上线!部分课程免费试看。还有限量优惠券可领,先到先得!http://open.21ic.com

  回复 

举报

   

wsnsyy

     

5

主题

522

帖子

1582

积分

助理工程师

专家等级:

结帖率:100%

6楼

 发表于 2014-12-10 18:23 | 只看该作者 |返回版面

mark

21ic公开课新版上线!部分课程免费试看。还有限量优惠券可领,先到先得!http://open.21ic.com

  回复 

举报

   

苏山人家

2

主题

304

帖子

918

积分

高级技术员

专家等级:

结帖率:0%

7楼

 发表于 2014-12-10 19:22  | 只看该作者 |返回版面

平时学习用的51,跑os只能加简单的任务,只能凑合学习。
但是如果在arm上的话,可以硬件仿真,更有利于os的学习和使用。

年轻即出发

  回复 

举报

   

原野之狼

187

主题

8568

帖子

2万

积分

VIP会员

专家等级:

结帖率:92%

8楼

 发表于 2014-12-10 20:17 | 只看该作者 |返回版面

用来学习  是个不错的方式

  ← 我在21QA解答技术问题,欢迎您来提问~ ♥♥(o→ܫ←o)♫  ㄟ(▔,▔)ㄏ

  回复 

举报

   

armecos

60

主题

1325

帖子

4929

积分

中级工程师

专家等级:

结帖率:100%

9楼

  楼主| 发表于 2014-12-29 12:36 | 只看该作者 |返回版面

本帖最后由 armecos 于 2014-12-29 12:38 编辑

uCOS51重入问题的解决

引言

    自从发表《uCOS51移植心得》以来,我收到了很多朋友们的来信,大家对公开源码表示鼓励,谢谢大家的支持!很多人对于编写自己的操作系统很感兴趣,uCOS51是个不错的选择。它的优点是简单易懂,学习成本低,有利于向32位CPU过渡。目前,嵌入式BBS上的热点是:嵌入式实时多任务操作系统、单片机上网、32bitCPU(如ARM等)。其实通过uCOS51学习完全可以掌握这些热门技术的精髓,而且学习成本低廉。为此我会陆续将我在研发过程中的经验体会写出来与大家交流,共同进步。
    我准备讨论以下内容:uCOS51高效内核、OS人机界面SHELL的编写、51机开发板的硬件设计、RTL8019AS网卡驱动程序、51TCP/IP协议栈设计、应用协议FTP、PPP、HTTP、SMTP、SNMP……在51上的实现技术、51OS任务划分和应用程序实例、由51软件系统向ARM的移植以及其他想到的题目。欢迎大家积极参与。
    
    注:开发板原理图、PCB图、GAL烧录文件、芯片手册、全部源程序可以来信索取,在整理好后会共享在网上。
    
    讨论1----uCOS51高效内核
    
    在提供了大量uCOS51β测试版后,zxgllp网友提出了OS程序不支持函数重入问题,具体表现在任务函数中带有形参和局部变量时若使用reentrant关键字,任务函数重入时会导致致命错误,经查属实。
    具体原因是OS切换程序没有保存/恢复仿真堆栈内容。由于我刚接触KEIL对其细节不熟悉,参考的范例中有些不是KEIL编译的,没有处理仿真堆栈内容,我也理所当然地认为KEIL象TC一样自动处理了重入问题,所以导致致命错误。(最新版本中已改正!)
    我仔细研究了C51.PDF 129-131页的内容,了解到:为了函数重入,形参和局部变量必须保存在堆栈里,由于51硬件堆栈太小,KEIL将根据内存模式在相应内存空间仿真堆栈(生长方向由上向下,与硬件栈相反)。对于大模式编译,函数返回地址保存在硬件堆栈里,形参和局部变量放在仿真堆栈中,栈指针为?C_XBP,XBPSTACK=1时,起始值在startup.a51中初始化为FFFFH+1。仿真堆栈效率低下,KEIL建议尽量不用,但为了重入操作必须使用。KEIL可以混合使用3种仿真堆栈(大、中、小模式),为了提高效率,针对51我推荐统一使用大模式编译。
    为了支持重入,我重新设计了堆栈结构(如下图)。增加了保存仿真堆栈指针?C_XBP和堆栈内容的数据结构。相应改变的文件有:OS_CPU_A.ASM、OS_CPU_C.C、OS_CPU.H、YY.C。由图可知,用户栈中保存的仿真栈与硬件栈相向生长,中间为空闲间隔,显然uCOSII的堆栈检测函数失效。硬件栈的保存恢复详见《移植心得》,仿真堆栈的保存与8086移植中的一样,OS只提供堆栈空间和只操作堆栈指针,不进行内存拷贝,效率相对很高。
    我建议使用统一的固定大小的堆栈空间,尽管uCOSII原作者把不同任务使用不同空间看成是优点,但为了在51上有效实现任务重入,针对51我还是坚持不使用这个优点。
    用户堆栈空间的大小是可以精确计算出来的。用户堆栈空间=硬件堆栈空间+仿真堆栈空间。硬件栈占用内部RAM,内部RAM执行效率高,如果堆栈空间过大,会影响KEIL编译的程序性能。如果堆栈空间小,在中断嵌套和程序调用时会造成系统崩溃。综合考虑,我把硬件堆栈空间大小定成了64字节,用户根据实际情况可以自行设定。仿真堆栈大小取决于形参和局部变量的类型及数量,可以精确算出。因为所有用户栈使用相同空间大小,所以取占用空间最大的任务函数的空间大小为仿真堆栈空间大小。这样用户堆栈空间大小就唯一确定了。我将用户堆栈空间大小用宏定义在OS_CFG.H文件中,宏名为MaxStkSize。
    51的SP只有8位,无法在64K空间中自由移动,只好采用拷贝全部硬件堆栈内容的笨办法。51 本来就弱,这么一来缺点更明显了。其实,引入OS必然要付出代价,一般OS要占用CPU10%-20%的负荷能力,请权衡利弊决定。切换频率决定了CPU的耗费,频率越高耗费越大,大到一定程度就该换更强的CPU了。我选了50Hz的切换频率,不高也不低,用户可以根据需要自行定夺。在耗费无法避免的情况下,我采取了几个措施来提高效率:1。ret和reti混用减少代码;2。IE、SP不入出栈,通过另外方式解决;3。用IDATA关键字声明在汇编中用到的全局变量,变DPTR操作为Ri操作;4。设计堆栈结构,简化算法;5。让串口输入输出工作在系统态,不占用任务TCB和优先级,增加弹性缓冲区,减少等待。

任务堆栈结构:





 

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册 

x

我的博客:
http://blog.sina.com.cn/u/2766726333

  回复 

举报

   

armecos

60

主题

1325

帖子

4929

积分

中级工程师

专家等级:

结帖率:100%

10楼

  楼主| 发表于 2014-12-29 12:39 | 只看该作者 |返回版面

在51单片机上硬件仿真uCOS51的说明
    
    讨论4----uCOS51仿真
    
    zyware网友2002/11/22来信询问uCOS51在单片机上的硬件仿真问题,具体情况是“在51上用uCOS51核,以及一些构件,keilc上仿真通过,用wave接硬件仿真程序乱飞,wave仿真以前的程序没有问题,不知是何缘故”。
    由于我的OS程序已经在KEIL软件仿真和硬件上实际测试过,所以不可能是程序错。可能的原因只能是硬件仿真软件设置问题。本人用的是Medwin软件,在Insight上调试,使用uCOS51编译测试程序一样跑飞。即使添加修改后的startup.a51(详见《在51单片机上固化uCOS51的说明》)也不正常。我发现Medwin似乎没有编译startup.a51,因为它把该文件加在了other Files目录下而不是source Files目录,于是我猜测只有放在source Files目录下的文件才被编译。由观察知,以.c和.asm做后缀的文件均被放在此目录下且被编译。于是我立即将startup.a51改成startup.asm并加入项目编译,结果测试正常。不必担心startup改名造成冲突,KEIL在链接目标文件时会自动处理重名段,本目录的文件优先级高(我是这么理解的,具体原理不清楚,这只是根据实践得到的结论,希望了解此处理过程的朋友能告之,不胜感激。)。
    
    具体做法如下:
    1。按《在51单片机上固化uCOS51的说明》一文修改startup.a51,并将其更名为startup.asm。
    2。将startup.asm、yy1.c、os_cpu_c.c、ucos_ii.c、os_cpu_a.asm五个文件加入项目编译。
    3。运行

我的博客:
http://blog.sina.com.cn/u/2766726333

  回复 

举报

   

armecos

60

主题

1325

帖子

4929

积分

中级工程师

专家等级:

结帖率:100%

11楼

  楼主| 发表于 2014-12-29 12:40 | 只看该作者 |返回版面

在51单片机上固化uCOS51的说明

    讨论5---- uCOS51固化

    近来,收到多位网友来信询问uCOS51在51单片机上的固化问题,归纳其焦点就是:为什么OS在KeilC51上模拟可以正常运行,但把它烧录在CPU上却不能工作?理论上,程序在软件仿真通过测试后,将其烧录在硬件上,硬件调试应该一次成功。许多网友也有这个经验,可为什么在调试uCOS51时失效了呢?难道操作系统调试很特殊吗?
    经过约一周时间的分析思考和上BBS查资料,我找到了答案。问题出在重入函数的引入。原来KEILC51软件仿真在不修改startup.a51文件的情况下,缺剩使用64K外部RAM,它把0000H-FFFFH全部仿真为可读写的RAM,而用户的硬件系统可能没有用到那么大的RAM空间,比如只用了8K/16K/32K等,或者用户把一些地址空间映射给了别的设备,比如8019AS等。在没有调用OSTaskCreate前,定义为reentrant的函数将用FFE0H做仿真堆栈栈顶指针,而此处在用户的系统里不是RAM,造成程序跑飞。比如在我的用户板上,将FE00H-FFFFH空间的一部分分配给8019AS使用,如果把demo程序编译后直接烧到51上,将不能运行。解决办法是根据系统RAM配置,修改startup.a51文件,并将其加入项目编译,如下所示:

XBPSTACK       EQU     1       ; set to 1 if large reentrant is used.
XBPSTACKTOP   EQU     07FFFH+1; set top of stack to highest location+1. 

    按此修改后,在有32K外部RAM的系统上可以正常运行。用户可根据自己XRAM的实际配置情况修改startup.a51相关参数,并将其添加到项目里编译。不必理会KEIL/C51/LIB目录下的同名文件,此处的startup.a51优先级高,KEIL将按此处该文件的配置编译项目。
    这也解释了有些网友在BBS上问到的,“为什么加入reentrant关键字,在软件仿真时正确,烧在芯片上就死机,去掉reentrant后两者都正常”的问题。由于大多数人很少使用重入函数,往往不了解这个细节,特此提请大家注意。

    关于uCOS51不能正常工作的原因还可能是因为串口波特率和OS_TICKS_PER_SEC及TH0、TL0设置不正确引起的。demo程序默认使用22.1184MHz晶体,19200波特率,切换频率为50Hz。为此,1。在SERIAL.C中设置“TL1=0xFD;TH1=0xFD;”使波特率为19200;2。在OS_CPU_C.C和OS_CPU_A.ASM中设置“TH0=0x70;TL0=0x00;”使时钟节拍tick=50次/秒;3。在OS_CFG.H中设置OS_TICKS_PER_SEC为50Hz。用户应根据实际情况,相应地修改这些参数,否则运行不正确。
    







定时器初值设置:

定时器0用于时钟节拍发生器
//*****************************************************************************
//初值计算公式:
//        (2^16-x)*F=Fosc/12
//    其中:F=时钟节拍频率tick;Fosc=晶体或晶振频率;x=初值;
//    本例中,F=50;Fosc=21.1184MHz;所以x=0x7000。
//*****************************************************************************

定时器1用于波特率发生器
//*****************************************************************************
//初值计算公式:
//        TH1=256-(2^SMOD/32*Fosc/12*1/Bound)
//    其中:SMOD=0,1;Fosc=晶体或晶振频率;Bound=波特率
//    本例中,SMOD=0;Fosc=21.1184MHz;Bound=19200,所以TH1=0xFD。
//*****************************************************************************

    demo程序项目中增加按如上方法改写的startup.a51后,在我的用户板硬件上运行正确。

我的博客:
http://blog.sina.com.cn/u/2766726333

  回复 

举报

   

armecos

60

主题

1325

帖子

4929

积分

中级工程师

专家等级:

结帖率:100%

12楼

  楼主| 发表于 2014-12-29 12:41 | 只看该作者 |返回版面

为uCOS51增加Shell界面

    讨论2----OS人机界面SHELL的编写

    uCOSII只提供了操作系统内核,用户要自己添加文件处理、人机界面、网络接口等重要部分。其中Shell(人机界面)提供了人与机器交互的界面,是机器服务于人的体现,是系统必不可少的重要组成部分。现代的很多OS如UNIX、DOS、VxWorks都提供了友好的命令行界面。Windows更是提供了GUI。大部分人认识OS都是从这里开始的。uCOS51同样拥有Shell,它是我从以前写的前后台程序中移植过来的。

    命令行Shell的工作原理比较简单,主要思路就是单片机接收用户键盘输入的字符存入命令缓冲区,并回显到屏幕,当用户按下回车键,触发软件状态机状态变迁,从输入态转移到命令解释态,然后根据用户命令调用相关子程序执行相应操作,执行完毕后重新回到输入态。
    我感觉原理很好掌握,程序也不长,但是细节部分要反复调试多次才能稳定工作。比如:命令行左右边界的保护、退格键的处理、词表的设计等等。
    Shell程序由词表、取词子程序、状态机框架程序(输入回显和命令解释执行)、命令相关子程序组成(详见源程序清单)。
    词表结构如程序清单所示,由词数目,左括号数,右括号数,每个词的具体信息(长度,字符串)构成。左右括号数用于括号匹配检查;词数目用于程序循环;词的具体信息作为解释/执行程序的输入参数。
    取词子程序从命令行语句中提取单词并存入词表同时进行匹配检查和词法分析。默认字符为:0-9、a-z、A-Z、'.';定界符为:空格、逗号,左/右括号。建议用户补充默认字符集(? / \ -)以便实现更灵活的语法。注意:现在版本的Shell只检查左右括号数量的匹配,无优先级和语法含义。
    输入回显程序循环检查用户键盘输入。如果输入回车,程序状态转入解释执行态;如果输入退格(8)则回显退格、空格、退格,模拟删除字符,同时输入缓冲区清除相应字节,清除前先检查左边界是否越界。如越界则鸣响报警且不执行清除操作;其他字符输入直接存入输入缓冲区并回显,此前检查右边界是否溢出,如果溢出则鸣响报警且抛弃刚输入的字符。
    命令解释程序调用取词子程序分析用户命令行输入,根据词表第一个单词在散转表中的位置调用相应执行子程序处理命令,如果散转表中无此单词,则打印“Bad command!”。取词子程序返回错误指示时也打印此句。
    命令解释程序向相应的命令相关子程序传入词表指针,具体执行由用户自行决定。由命令相关子程序返回后重新回到命令输入态,完成一次输入执行全过程。此过程周而复始地循环执行。

    Shell界面的命令按功能分为以下几组:
    1。操作系统相关命令:
       查看就绪任务lt / 中止任务kill / 恢复任务执行call / CPU利用率usage / 版本查询ver / 查某个任务信息(TCB、堆栈内容)lt
       查看切换次数和时间lts
       
    2。网络相关命令:
       显示配置MAC地址macadr / 显示配置主机IP地址host / 显示配置子网掩码mask / 显示配置缺省网关gateway
       显示网络配置总情况lc / 连通测试命令ping / 用户数据报发送命令udp / telnet命令tel / 相关应用命令**
       显示ARP高速缓冲区地址对ls / 显示发送缓冲区信息lti
    
    3。屏幕显示相关命令:
       清屏clr / 帮助help / 功能键F3、F7处理 / 组合键Ctrl+C、Ctrl+B处理
    
    4。外设(闪盘X5045和I/O口)相关命令:
       读闪盘rdx / 读I/O口rdp / 写闪盘wdx
    
    5。安全相关命令:
       身份认证密码权限usr、pass
    
    6。应用相关命令:
       用户自行定义
    
    7。调试相关命令:
       单步、断点、运行、寄存器等命令,类似68K的TUTOR和ARM的驻留监控程序Angel。
    
    怎么样,像不像VxWorks的命令行Shell!
    
    用户命令大小写不敏感,程序将命令字符串统一成小写形式。程序中各种参数(如:最大词长度、词数量……)定义成宏放在一个头文件中,随时可修改配置,很方便。Shell作为一个任务工作于内核之外,占用一个任务号。

源程序:
词表
typedef struct{
    int Num;
    int LeftCurveNum,RightCurveNum;
    struct{
        int Length;
        unsigned char Str[MaxLenWord+1];        /*for '\0'*/
    } wt[MaxLenWordTable];
} WORDTABLE;

取词
bit GetWord(unsigned char *ComBuf,WORDTABLE *WordTable)
{
    int i=0;        /*ComBuf String pointer*/
    int j=0;        /*Length of Word */
    int k=-1;        /*The number of WordTable*/
    int StrFlag=0;        /*There is "0-9/a-z/A-Z" before " ,()"*/
    int SentenceEndFlag=0;        /*Sentence end*/
    char ch;

    WordTable->Num=0;
    WordTable->LeftCurveNum=0;
    WordTable->RightCurveNum=0;

    ch=ComBuf[0];
    while(!SentenceEndFlag&&i<MaxLenComBuf){
        if((ch>='0'&&ch<='9')||(ch>='a'&&ch<='z')||(ch>='A'&&ch<='Z')||(ch=='.')){
            if(StrFlag==0){
                StrFlag=1;k=k+1;j=0;
                if(k>=MaxLenWordTable) return 0;
                WordTable->wt[k].Str[j]=ch;
                WordTable->Num=k+1;
            }
            else{
                j=j+1;
                if(j>=MaxLenWord) return 0;
                WordTable->wt[k].Str[j]=ch;
            }
        }
        else if(ch==' '||ch==','||ch=='('||ch==')'||ch=='\0'){
            if(ch=='(') WordTable->LeftCurveNum++;
            if(ch==')') WordTable->RightCurveNum++;
            if(StrFlag==1){
                StrFlag=0;j=j+1;
                WordTable->wt[k].Str[j]='\0';
                WordTable->wt[k].Length=j;
            }
            if(ch=='\0') SentenceEndFlag=1;
        }
        else{
            return 0;
        }
        i=i+1;
        ch=ComBuf[i];
    }
    if(i<MaxLenComBuf||ComBuf[MaxLenComBuf]=='\0'){
        if(WordTable->LeftCurveNum==WordTable->RightCurveNum) return 1;
        else return 0;
    }
    else{
        return 0;
    }
}

输入回显和命令解释执行
void yyshell(void *yydata) reentrant
{
    yydata=yydata;
    clrscr();
    PrintStr("\t\t***********************************************\n");
    PrintStr("\t\t*         Welcom to use this program          *\n");
    PrintStr("\t\t*                   Author:YangYi 20020715    *\n");
    PrintStr("\t\t***********************************************\n\n\n");
                
    /*Login & Password*/

    PrintStr("% ");
    while(!ShellEnd){

        switch(State){
            case StatInputCom:{
                if(yygetch(&ch)){
                    if(ch==13)        /*Enter return key*/
                    {
                        PrintStr("\n");
                        ComBuf[i+1]='\0';                                        
                        if(i+1==0) PrintStr("% ");
                        else                                        
                            State=StatExeCom;
                    }
                    else{
                        i=i+1;
                        if((i>=MaxLenComBuf)&&(ch!=8)){
                            PrintChar(7);
                            i=MaxLenComBuf-1;
                        }
                        else{
                            if(ch==8){
                                i=i-2;
                                if(i<-1) {i=-1;PrintChar(7);}
                                else{
                                    PrintChar(8);
                                    PrintChar(' ');
                                    PrintChar(8);
                                }
                            }
                            else{
                                PrintChar(ch);
                                ComBuf[i]=ch;
                            }
                        }
                    }
                    break;
                }
                else{
                    //OSTimeDly(10);
                    break;
                }
            }
            case StatExeCom:{
                if(GetWord(ComBuf,&WordTable)==1&&WordTable.Num!=0){
                    yystrlwr(WordTable.wt[0].Str);
                    for(tem=0;tem<MaxComNum&&!ComMatchFlag;tem++)
                        if(yystrcmp(WordTable.wt[0].Str,ComTable[tem])==0) ComMatchFlag=1;
                    if(ComMatchFlag){
                        tem--;
                        switch(tem){
                            case 0:{DisplayTask(&WordTable);break;}
                            case 1:{Kill(&WordTable);break;}
                            case 2:{PingCommand(&WordTable);break;}
                            case 3:{UDPCommand(&WordTable);break;}
                            case 4:{CfgHost(&WordTable);break;}
                            case 5:{CfgMask(&WordTable);break;}
                            case 6:{CfgGateway(&WordTable);break;}                                                        
                            case 7:{
                                //ShellEnd=1;
                                PrintStr("\n\tThis Command is limited!\n\n");
                                break;
                            }
                            case 8:{PrintConfig(&WordTable);break;}
                            case 9:{clrscr();break;}
                            case 10:{DisplayHelpMenu(&WordTable);break;}
                        }                                                        
                    }
                    else
                        PrintStr("    Bad command!\n\n");
                }
                else{
                    if(WordTable.Num) PrintStr("    Bad command!\n\n");
                }

                ComMatchFlag=0;
                State=StatInputCom;
                if(ShellEnd) {PrintStr("\n\n");}
                else PrintStr("% ");
                i=-1;
                break;
            }
            default:{
                //ShellEnd=1;
                PrintStr("System fatal error!\n");
                PrintChar(7);PrintChar(7);PrintChar(7);
            }
        }
    }
}


命令使用说明:

一、关于93LC66的命令
1.rd66 [起址] [显示行数]
  例如:rd66 0x100 3   显示从0x100开始的数据,显示3行(每行16字节)。
  注:显示行数最大为ROMSIZE/16;省略起址和显示行数,则缺省显示0x00开始的一行,省略显示行数,则缺省显示1行。另有一rdb66指令,格式与此同,只是它是按字节显示的。

2.wd66 地址 [x] 数据0-n
  例如:wd66 all 0xad  将0xad写入93LC66全部存储单元。
        wd66 0xaa 0xff 将0xff写入0xaa存储单元。
        wd66 0xcd x aa bb cc dd 将十六进制数AA、BB、CC、DD写入0xcd单元。
        wd66 0xcd 12 34 56 78   将十进制数12、34、56、78写入0xcd单元。
  注:地址和数据不可省略;标记x用于数据进制的指定。

3.erase66 地址
  例如:erase66 all      擦除93LC66全部存储单元。
        erase66 0x20     擦除0x20存储单元。
  注:地址不可省略。全擦命令要求确认一次(按y/Y确认)。

二、关于X5045的命令
1.rxr
  例如:rxr    读X5045状态寄存器。
  注:状态寄存器格式 X  X  WD1  WD0  BL1  BL0  WEL  WIP
      WD1 WD0                  BL1 BL0  
       0   0     1.4s           0   0     None
       0   1     600ms          0   1     $180-$1FF
       1   0     200ms          1   0     $100-$1FF
       1   1     Disable        1   1     $000-$1FF
      WIP=0 空闲;WIP=1 正在写;WEL=0 不可写;WEL=1 可写。

2.wxr
  例如:wxr    写X5045状态寄存器。
  注:见rxr注。

3.rdx [起址] [显示行数]
  例如::rdx 0x100 3   显示从0x100开始的数据,显示3行(每行16字节)。
  注:显示行数最大为ROMSIZE/16;省略起址和显示行数,则缺省显示0x00开始的一行,省略显示行数,则缺省显示1行。ROMSIZE按字节为单位计算。

4.wdx 地址 [x] 数据0-n
  例如:wdx 0xaa 0xff 将0xff写入0xaa存储单元。
        wdx 0xcd x aa bb cc dd 将十六进制数AA、BB、CC、DD写入0xcd单元。
        wdx 0xcd 12 34 56 78   将十进制数12、34、56、78写入0xcd单元。
  注:地址和数据不可省略;标记x用于数据进制的指定。

三、关于端口的命令
1.rdp
  例如:rdp    读端口值。
  注:背面 JACK …BA3 BA2 BA1 BA0 POW2 POW1
      正面 ISIN LINKOK TXSPEED … JACK

2.wrbs
  例如:wrbs   写板选(0-15)。
  注:见rdp注。

四、关于shell的命令
1.clr
  例如:clr    清屏幕。

2.exit
  例如:exit   退出Shell。
  注:此命令受限制,此处不响应。

3.help
  例如:help   屏幕打印帮助菜单。

五、关于TCPIP的命令
1.lc
  例如:lc    显示TCPIP配置信息(MAC地址、主机地址、子网掩码、网关地址、SNMP主机地址)。

2.ls
  例如:ls    显示ARP缓存信息(寿命、MAC地址、IP地址)。

3.lt
  例如:lt    显示发包缓存信息(目的IP地址、重发次数)。

4.host
  例如:host  显示主机IP地址。
        host 172.18.92.86 更改IP地址为172.18.92.86。

5.mask
  例如:mask  显示子网掩码。
        mask 255.255.0.0 更改子网掩码为255.255.0.0。

6.gateway
  例如:gateway  显示网关地址。
        gateway  172.18.80.1 更改网关地址为172.18.80.1。

7.snmphost
  例如:snmphost  显示SNMP主机地址。
        snmphost  172.18.92.66 更改SNMP主机地址为172.18.92.66。

8.macadr
  例如:macadr  显示MAC地址。
        macadr  0000 1234 5678 更改MAC地址为0000 1234 5678。

9.ping
  例如:ping 172.18.92.87  检查目的主机连通情况。

10.udp
  例如:udp 172.18.92.87 Hello. 向172.18.92.87发送UDP包,包内数据为字符串。

X5045数据结构
地址类型        对应全局变量                位置                大小
MAC       my_ethernet_address    0-5          6
IP          my_ip_address         6-9          4
MASK      mask_ip_address       10-13        4
GateWay IP  gateway_ip_address     14-17        4
SNMP IP    SNMP_ip_address      18-21        4
 

我的博客:
http://blog.sina.com.cn/u/2766726333

  回复 

举报

   

armecos

60

主题

1325

帖子

4929

积分

中级工程师

专家等级:

结帖率:100%

13楼

  楼主| 发表于 2014-12-29 12:43 | 只看该作者 |返回版面

printf在uCOS51上的移植和浮点数显示

    printf函数是C语言里应用最为广泛的函数之一,我们初学C语言时实现的第一个程序《Hello the world》,就包含printf语句。它的应用十分灵活,可以打印各种类型数据,可变数量的变量,表达式,是非常理想的输出函数,广泛用于结果输出,中间变量显示,调试等。然而,编译器将其作为标准库函数,不提供源代码,其本身代码量也偏大,无法实现嵌入式系统按需裁减的要求,并且有些printf库代码不支持重入。
    解决方法是把Linux里的相关源码简化后移植到C51里。关键点在于理解变参函数、参数传递规则、浮点数存储格式。
    C编译器一般将函数参数按从右至左的顺序依次压入堆栈(C51在使用reentrant关键字后也这么处理),函数内部处理参数变量时直接在堆栈上寻址,局部变量紧跟在参数后面存放,函数返回时出栈,参数和局部变量所占用空间自动释放。例如:
    fun(char *fmt,char a,int b long c,float d) reentrant
    的堆栈结构如图1所示:
    
        ------------------
        |float d 4 bytes |
    +10 ------------------
        |long c 4 bytes  |
    +6  ------------------
        |int b 2 bytes   |
    +4  ------------------
        |char a 1 bytes  |
    +3  ------------------
        |char *fmt 3bytes|
SP+0-->------------------
        |     局部变量    |
        ------------------
        图1.fun函数参数和局部变量在堆栈里的结构
    
    C51编译器从右向左依次将float/long/int/char/char *压入仿真堆栈,各种数据类型所占空间大小如图1,例如char占1字节,float占4字节等。值得一提的是,常数压栈的格式:0-255按1字节压栈,256-32767压成2字节,32768(8000H)或以上压成4字节,带有l/L结尾的常数占4字节。
    上面的函数fun内部可以通过函数名称访问各个变量,C编译器自动把函数名转换成地址,如:访问long c转换成访问SP+6,访问char a转换成访问SP+3等。写成表达式为:
    c=0x12345678;======>(SP+6)=0x12345678
    a='y';=============>(SP+3)='y'
    总之,上面的函数通过显式地指定函数名和数据类型完成参数的传递和访问,内部细节由C编译器完成,对用户透明。
    这种方式的好处是表达清晰,结构严谨,屏蔽底层细节;坏处是不够灵活,参数必须在处理前显式确定并固定不变,这给我们用同一函数处理不同情况带来了困难,C的解决方案是引入“变参函数”(详见C语言大全),如下:
    fun(char *fmt,...) reentrant
    ...表示有0到N个可变数量参数,C编译器此时不检查参数匹配,传递参数规律与一般函数相同。如果我们用这个函数取代前一个函数,但仍按前一函数的调用方式调用,那么,参数在堆栈里的位置仍如图1所示。此时,函数形参只有“...”没有具体变量名,如何引用形参变量呢?观察图1堆栈结构可知,如果知道堆栈内第一个参数的起址和每个参数的数据类型及他们的排列顺序,就可以通过指针访问指定的变量。例如:
    知道堆栈内第一个参数的起址SP和每个参数的数据类型及排列顺序(char*/char/int/long/float),就可以通过SP,SP+3,SP+4,SP+6,SP+10访问原来必须通过参数名访问的fmt,a,b,c,d变量。写成C语言就是:
    
    fun("yy",'y',(int)2,5L,-12.5);
    
    fun(char *fmt,...) reentrant
    {
    void *p;
    
    p=&fmt;
    //此时*p指向字符串"yy"首址,**p是字符串第一个字符'y'。
    p=(char **)p+1;
    //此时*((char *)p)为字符'y'。
    p=(char *)p+1;
    //此时*((int *)p)为0x0002。
    p=(int *)p+1;
    //此时*((long *)p)为0xC1480000,即-12.5的IEEE-754标准格式。
    p=(float *)p+1;
    }
    测试代码:
void fun(char *fmt,...) reentrant
{
  void *p;

  p=&fmt;
  PrintChar(**((char **)p));
  p=((char **)p) +1;
  PrintChar(*((char *)p));
  p=((char *)p) +1;
  PrintLong(*((int *)p));
  p=((int *)p) +1;
  PrintLong(*((long *)p));
  p=((long *)p) +1;
  PrintLong(*((long *)p));
  p=((float *)p) +1;
}
    显示结果:yy0000000200000005C1480000
    由上面知,在C里不用显式使用SP等堆栈指针,而是使用void指针指向各种类型数据。变参函数的参数传递和获取就是这样运做的,知道了它的原理,就不难理解printf的实现了。
    我所移植的printf支持标准或长二进制/八进制/十进制/十六进制/无符号整数,支持字符、字符串、浮点数、百分号%显示。其中,浮点数在整个范围内被完全支持,统一采用科学记数法显示。对应的指示符如下:
    c 字符    f 浮点数    s 字符串    % 百分号显示
    d/ld 2字节/4字节有符号整数    u/lu 2字节/4字节无符号整数
    x/lx 2字节/4字节十六进制数    o/lo 2字节/4字节八进制数
    b/lb 2字节/4字节二进制数
    
    printf的功能是字符串化数据,它的第一个参数是格式化字符串fmt,用其指示第一个参数在堆栈里的起址和其后各个参数的数据类型。知道了参数堆栈起址和各个参数的类型和排放次序,就可以依次取出各个参数并字符串化。详细过程参见yyprintf源代码。同时,注意到参数是依靠起址和数据长度信息依次读出来的,那么,yyprintf的参数必须与格式化参数的指示相同,否则参数数据会乱掉。对于不能肯定的转化数据类型建议加上强制类型定义,如(int) 2。特别是常数的转换类型容易搞错。
    printf大部分代码与硬件无关,只有参数堆栈结构和打印一个字符putchar()函数是硬件相关的。移植printf时只要修改putchar()函数和堆栈结构即可。putchar()函数的功能一般是向串口输出一个字符,也可以向其他显示设备输出一个字符,取决于你的驱动程序。我已经在uCOS51里实现了PrintChar函数,直接调用就可以了。其实,在X86、POWERPC、ARM等32位CPU上移植printf更加有效和方便。
    
    测试举例:
    float r=1.9835671E-10,pi=3.1415926;
    yyprintf("R=%f  Circle area=%f\n",r,pi*r*r);
    结果:
    R=1.983567E-10  Circle area=1.236071E-19
        
    源代码:
//============================================================================================
//
//============================================================================================
void yyprintf(char *fmt,...) reentrant  //自编简单printf等效函数
{
    void *p;  //任意指针,可以指向任何类型,C语法不对其严格要求。
    char ch;
    unsigned char j;

    p=&fmt;
    p=(char **)p+1;  //此处p是指向指针的指针,fmt是字符串指针,p是指向fmt的指针

    while(1){
        while((ch=*fmt++)!='%'){
            if(ch=='\0') return;
            else if(ch=='\n'){PrintChar(10);PrintChar(13);}
            else if(ch=='\t'){
                for(j=0;j<TABNum;j++)
                    PrintChar(' ');
            }
            else PrintChar(ch);
        }
        ch=*fmt++;
        switch(ch){
            case 'c':
                PrintChar(*((char *)p));
                p=(char *)p+1;
                break;
            case 'd':
                PrintN(*((int *)p),10);
                p=(int *)p+1;
                break;
            case 'x':
                PrintN(*((int *)p),16);
                p=(int *)p+1;
                break;
            case 'o':
                PrintUN(*((int *)p),8);
                p=(int *)p+1;
                break;
            case 'b':
                PrintUN(*((int *)p),2);
                p=(int *)p+1;
                break;
            case 'l':
                ch=*fmt++;
                switch(ch){
                    case 'd':
                        PrintLN(*((long *)p),10);
                        p=(long *)p+1;
                        break;
                    case 'o':
                        PrintLUN(*((long *)p),8);
                        p=(long *)p+1;
                        break;
                    case 'u':
                        PrintLUN(*((unsigned long *)p),10);
                        p=(unsigned long *)p+1;
                        break;
                    case 'b':
                        PrintLUN(*((long *)p),2);
                        p=(long *)p+1;
                        break;
                    case 'x':
                        PrintLN(*((long *)p),16);
                        p=(long *)p+1;
                        break;
                    default:
                        return;
                }
                break;
            case 'f':
                DispF(*((float *)p));
                p=(float *)p+1;
                break;
            case 'u':
                PrintUN(*((unsigned int *)p),10);
                p=(unsigned int *)p+1;
                break;
            case 's':
                PrintStr(*((char **)p));
                p=(char **)p+1;
                break;
            case '%':
                PrintChar('%');
                p=(char *)p+1;
                break;
            default:
                return;
        }
    }
}

void PrintN(int n,int b) reentrant  //十进制显示整形数
{
    if(b==16){PrintWord(n);return;}
        if(n<0){PrintChar('-');n=-n;}
        if(n/b)
            PrintN(n/b,b);
        PrintChar(n%b+'0');
}

void PrintUN(unsigned int n,unsigned int b) reentrant  //十进制显示无符号整形数
{
    if(b==16){PrintWord(n);return;}
        if(n/b)
            PrintUN(n/b,b);
        PrintChar(n%b+'0');
}


void PrintLN(long n,long b) reentrant  //十进制显示长整形数
{
    if(b==16){PrintLong(n);return;}
        if(n<0){PrintChar('-');n=-n;}
        if(n/b)
            PrintLN(n/b,b);
        PrintChar(n%b+'0');
}

void PrintLUN(unsigned long n,unsigned long b) reentrant  //十进制显示无符号长整形数
{
    if(b==16){PrintLong(n);return;}
        if(n/b)
            PrintLUN(n/b,b);
        PrintChar(n%b+'0');
}

参考文献:

1。《ROM版本下系统调试信息的一种显示方法》合肥工业大学 彭良清 《单片机与嵌入式系统应用》p22页2002(1-6)


TO BE CONTINUED...


                                浮点数显示
                         asdjf@163.com  2003/10/20


    C51里用4字节存储一个浮点数,格式遵循IEEE-754标准(详见c51.pdf第179页说明)。一个浮点数用两个部分表示,尾数和2的幂,尾数代表浮点上的实际二进制数,2的幂代表指数,指数的保存形式是一个0到255的8位值,指数的实际值是保存值(0到255)减去127,一个范围在-127到+128之间的值,尾数是一个24位值(代表大约7个十进制数),最高位MSB通常是1,因此不保存。一个符号位表示浮点数是正或负。
浮点数保存的字节格式如下:

地址        +0          +1           +2           +3
内容    SEEE EEEE   EMMM MMMM    MMMM MMMM    MMMM MMMM

这里
S 代表符号位,1是负,0是正
E 偏移127的幂,二进制阶码=(EEEEEEEE)-127。
M 24位的尾数保存在23位中,只存储23位,最高位固定为1。此方法用最较少的位数实现了较高的有效位数,提高了精度。

零是一个特定值,幂是0 尾数也是0。

浮点数-12.5作为一个十六进制数0xC1480000保存在存储区中,这个值如下:
地址 +0     +1     +2     +3
内容0xC1   0x48   0x00   0x00

浮点数和十六进制等效保存值之间的转换相当简单。下面的例子说明上面的值-12.5如何转换。
浮点保存值不是一个直接的格式,要转换为一个浮点数,位必须按上面的浮点数保存格式表所列的那样分开,例如:

地址       +0           +1            +2            +3
格式   SEEE EEEE    EMMM MMMM     MMMM MMMM     MMMM MMMM
二进制  11000001     01001000      00000000      00000000
十六进制   C1           48            00            00

从这个例子可以得到下面的信息:
  符号位是1 表示一个负数
  幂是二进制10000010或十进制130,130减去127是3,就是实际的幂。
  尾数是后面的二进制数10010000000000000000000


在尾数的左边有一个省略的小数点和1,这个1在浮点数的保存中经常省略,加上一个1和小数点到尾数的开头,得到尾数值如下:
1.10010000000000000000000

接着,根据指数调整尾数.一个负的指数向左移动小数点.一个正的指数向右移动小数点.因为指数是3,尾数调整如下:
1100.10000000000000000000

结果是一个二进制浮点数,小数点左边的二进制数代表所处位置的2的幂,例如:1100表示(1*2^3)+(1*2^2)+(0*2^1)+(0*2^0)=12。
小数点的右边也代表所处位置的2的幂,只是幂是负的。例如:.100...表示(1*2^(-1))+(0*2^(-2))+(0*2^(-2))...=0.5。
这些值的和是12.5。因为设置的符号位表示这数是负的,因此十六进制值0xC1480000表示-12.5。

浮点数错误信息

    8051没有包含捕获浮点数错误的中断向量,因此,你的软件必须正确响应这些错误情况。
    除了正常的浮点数值,还包含二进制错误值。这些值被定义为IEEE标准的一部分并用在正常浮点数操作过程中发生错误的时候。你的代码应该在每一次浮点操作完成后检查可能出现的错误。
        名称        值       含义
        NaN     0xFFFFFFF   不是一个数
        +INF    0x7F80000   正无穷(正溢出)
        -INF    0xFF80000   负无穷(负溢出)
    你可以使用如下的联合体(union)存储浮点数。
    union f {
      float          f;  //浮点值
      unsigned long ul;  //无符号长整数
    };
    这个union包含一个float和一个unsigned long以便执行浮点数**算并响应IEEE错误状态。
    
    以上是KEIL在线帮助的中译文,下面我们讨论如何显示浮点数。
    
    尾数为24bit,最高可表达的整数值为2^24-1=16777215,也就是说,小于等于16777215的整数可以被精确显示。这决定了十进制浮点数的有效位数为7位,10^7<16777215<10^8,10的7次方以内的数小于16777215,可以精确表示。使用科学记数法时,整数部分占1位,所以小数部分最大占7-1=6位,即最大有6位十进制精度。
    长整形数和浮点数都占4字节,但表示范围差别很大。浮点数的范围为+-1.175494E-38到+-3.402823E+38,无符号长整形数范围为0到4294967295。显示浮点数要用到长整形数保存数据,可他们范围差这么多,怎么办呢?
    仔细观察十进制浮点数的显示,有一个尾数和一个阶码,由上面论证可知32位IEEE-754浮点数最大有效数字为7位十进制数,超出此范围的数字有截断误差,不必理会,因此,浮点数尾数能够放在长整形数里保存。阶码为-38到38,一个char型变量就可以保存。
    综上所述,以10^7的最大跨度为窗口(小于10^7也可以,如:10,100...10000等,但决不能大于它,那样会超出精度范围),定位浮点数的量级,然后取出7位尾数的整数值存于长整形数里,再调整阶码,就可以精确显示此浮点数。
    量级尺度如下:
      (-38)-(-35)-(-28)-(-21)-(-14)-(-7)-(0)-(7)-(14)-(21)-(28)-(35)-(38)
    请严格按照KEIL手册给出的浮点数范围显示,因为数值空间没有完全使用,有些值用于错误指示和表示正负无穷。小于1.175494E-38的数仍可以显示一些,但最好不用,以免出错。我采用直接判断的方法,剔除此种情况。
    在计算机里结合律不成立,(a*b)*c!=a*(b*c),原则是先让计算结果值动态范围小的两个数运算,请注意程序里的写法。
    注:(1E38/b)*1E6不要写成1E44/b,因为无法在32位浮点数里保存1E44,切记!
    计算机使用二进制数计算,能有效利用电子器件高速开关的特性,而人习惯于十进制数表示,二进制和十进制没有方便的转换方法,只能通过大量计算实现,浮点数的十进制科学记数法显示尤其需要大量的运算,可见,显示一个浮点数要经过若干次浮点运算,没有必要就不要显示,否则,花在显示上的时间比计算的耗时都要多得多。
    
    源程序:

我的博客:
http://blog.sina.com.cn/u/2766726333

  回复 

举报

   

armecos

60

主题

1325

帖子

4929

积分

中级工程师

专家等级:

结帖率:100%

14楼

  楼主| 发表于 2014-12-29 12:44 | 只看该作者 |返回版面

ROM和RAM测试总结

    在硬件系统出厂前要进行产品测试;在嵌入式系统工作之前,一般也要进行自检,其中ROM和RAM检测必不可少,可是有不少人对于测试目的、原因和方法存在错误理解。
    为什么要测试ROM和RAM,怎么测试呢?普遍的看法是:由于担心ROM和RAM芯片损坏,在出厂和使用前应该校验这两种芯片的好坏。测试RAM的方法是写读各个内存单元,检查是否能够正确写入;测试ROM的方法是累加各存储单元数值并与校验和比较。这种认识不能说错,但有些肤浅,照此编出的测试程序不完备。一般来说,ROM和RAM芯片本身不大会被损坏,用到次品的概率也比较小,真正出问题的,大都是其他硬件部分,因此,测试ROM和RAM往往是醉翁之意不在酒。
    
    ROM测试
    测试ROM的真正目的是保证程序完整性。
    嵌入式软件和启动代码存放在ROM里,不能保证长期稳定可靠,因为硬件注定是不可靠的。以flash ROM为例,它会由于以下两种主要原因导致程序挥发:
    1。受到辐射。本身工作在辐射环境里/运输过程中受到辐射(如过海关时被X光机检查)。
    2。长时间存放导致存储失效,某些0、1位自行翻转。
    无论如何,在硬件上存放的程序都是不可靠的。如果完全不能运行,那到也不会造成太大的损失。怕就怕程序可以运行,但某些关键数据/关键代码段被破坏,引发致命错误。为此,必须在程序正常工作前,在软件层面上保证所运行的程序100%没有被破坏,保证现在要运行的程序就是当初写入的。
    保证程序完整性的方法很多,例如对全部程序进行CRC校验(-16和-32)/累加和校验(移位累加),只要能在数学上确保出错概率极低,工程上就可以认为程序完整。
    程序完整性测试通过,捎带着也就证明了ROM没有被损坏。即测试ROM是否损坏只是测试的副产品,不是主要目的。
    
    RAM测试
    测试RAM的真正目的是保证硬件系统的可靠性。
    RAM真的是太不容易坏了,我至今还没有看见过一起因为RAM损坏导致的系统不正常现象。不过大部分问题却可以通过RAM测试反映出来。仔细想想,当硬件被生产出来/被插到背板上究竟会发生什么错误呢!是不是感到自己做的板子出问题的可能性更大!请考虑如下几点:
    1。生产工艺不过关,过孔打歪了,与临近信号线距离不满足线规甚至打在了线上。
    2。由于搭锡引起的信号线粘连。
    3。虚焊/漏焊引起的接触不良。
    4。不按规程操作,把手印儿印在了高频线上。
    5。板子脏了也不吹,覆盖了一层灰尘(内含金属微粒)。
    ......
    这些现象比较有趣,试举几例:
    1。地址线A0和A1粘连。读出XXX00、XXX01、XXX10三个字节的数据完全一样。
    2。数据线D0和D1粘连。D0和D1只要有一个为0,那么两条线都为0。
    3。接触不良。时好时坏。
    4。器件表面处理不干净,有助焊剂残留。低速访问正常,大负荷高速访问频繁死机。
    总之,我们做的板子在生产中和使用中都会有出错机会,所以出厂前必须测试,使用前必须自检。(当然如果你做的不是实际产品而是实验室样品的话,可以简化步骤。)
    如何测试RAM呢?写一个数然后读出来判断显然测不出所有问题,单个测试数据不易覆盖全部测试内容,更不用说定位错误原因了(RAM坏、地址/数据线粘连、接触不良)。好的测试应尽可能测出粘连、RAM坏、单板高频特性。
    我总结的方法是这样的:(如测试一个FFH字节的RAM)
    首先,测试地址线,
    1。'0'滑动,随机选择一个数如55、AA之类,依次写到FEH、FDH、FBH、F7H、EFH、DFH、BFH、7FH地址单元里去,把地址写成二进制数,可以看到比特0在地址总线上从低到高滑动,谓之'0'滑动。目的是测试这些地址线在依次变0时是否稳定正常。当每一根线由1变0,会产生下冲,如果下冲控制不好,在高频时会引起错误。单板上地址线不一定一样长,下冲也就不会完全一样,因此,每一根线都单独测一下下冲性能。
    2。'1'滑动,随机选择一个数如55、AA之类,依次写到1H、2H、4H、8H、10H、20H、40H、80H地址单元里去,把地址写成二进制数,可以看到比特1在地址总线上从低到高滑动,谓之'1'滑动。,目的是测试这些地址线在依次变1时是否稳定正常。当每一根线由0变1,会产生上冲,如果上冲控制不好,在高频时会引起错误。单板上地址线不一定一样长,上冲也就不会完全一样,因此,每一根线都单独测一下上冲性能。上冲和下冲是不同的指标,要分别测一下。
    3。"全0变全1",随机选择一个数如55、AA之类,写到FFH单元,再写到00H单元,然后写到FFH单元。把地址写成二进制数,可以看到地址线从全'0'变到全'1'。由信号处理理论知,在电压阶跃跳变时包含无限宽频谱,其中高频部分对外产生辐射,这些辐射信号是干扰源,对临近线路产生较大影响。地址线一般集束布线,同时跳变会引起最大干扰。地址线从全'0'变到全'1',干扰、上冲、扇出电流影响最大。
    4。"全1变全0",紧接上一步,随机选择一个数如55、AA之类,写到00H单元。把地址写成二进制数,可以看到地址线从全'1'变到全'0',产生最大下冲干扰。
    5。"粘连测试"。依次向不同地址单元写入不同数据并读出判断,如:1、2、3、4......此步骤捎带测试了RAM好坏。注意,千万别用相同数据测试,否则测不出粘连。
    6。可选"全0全1连续高速变化"。目的是模拟最恶劣情况(大扇出电流、强干扰、上/下冲)。
    然后,测试数据线,(原理与测试地址线相同,1、2两步顺带测试了数据线粘连)
    1。'0'滑动,向某一固定地址依次写入FEH、FDH、FBH、F7H、EFH、DFH、BFH、7FH并读出判断。
    2。'1'滑动,向某一固定地址依次写入1H、2H、4H、8H、10H、20H、40H、80H并读出判断。
    3。"全0变全1",所有单元置1(先清零再置1并读出判断)。
    4。"全1变全0",所有单元清零(清零并读出判断)。
    5。可选"全0全1连续高速变化"。向某一单元高速交替写入若干全'0'和全'1',最后以全'0'结束。
    至此,RAM测试完毕,同时全部存储单元清零。
    对于出厂检测程序,有较大发挥余地,如可以加入错误定位代码,自动指出错误原因和错误位置。
    每一块单板的高频特性都会因为生产工艺误差(制板、材料、焊接、组装等)和使用情况而各不相同。同一块板子的高频特性在不同情况下表现也不相同。
    综上所述,除了测试RAM好坏,大部分代码测的是单板硬件可靠性。
    如果不关心高频特性,用原来的测试方法就差不多了(如果测试数据没选好,可能测不出数据线粘连),但应该认识到,测试RAM的主要对象不是RAM本身的好坏,而是连接RAM的单板硬件和线路。
    
    以上是我实际工作经验的一些总结,写出来与大家交流,如有不对之处恳请指正!
    
源程序(伪代码)
//TEST ROM
TestROM()
{//用移位累加和校验
  sum=0;
  for(i=0;i<MAXRAMSize;i++){
    sum=sum+ram[i];
    sum=sum>>1;
  }
  if(sum==CHECKSUM) printf("ROM test OK!\n");
  else printf("ROM test ERROR!\n");
}

//TEST RAM
TestRAM()
{
  //地址线测试
  '0'滑动;
  '1'滑动;
  "全0变全1";
  "全1变全0";
  "粘连测试";
  可选"全0全1连续高速变化";
  
  //数据线测试
  '0'滑动;
  '1'滑动;
  "全0变全1";
  "全1变全0";
  可选"全0全1连续高速变化"
}

我的博客:
http://blog.sina.com.cn/u/2766726333

  回复 

举报

   

armecos

60

主题

1325

帖子

4929

积分

中级工程师

专家等级:

结帖率:100%

15楼

  楼主| 发表于 2014-12-29 12:45 | 只看该作者 |返回版面

2002/12/23

gdtyy 你好,几个问题想请教一下 

这些天我仔细看了很多ucosii的资料,但有些疑问,我想真要能将ucosii应用起来,我们需要理解的问题分为三个
ucosii工作原理
ucosii 移植
ucosii 和应用程序的接口(用户程序如何嵌入到操作系统中运行)
首先ucossii 的工作核心原理是:让最高优先级的任务处于运行状态。这需要操作系统在某个时间得运行任务调度的一个算法(操作系统提供了这个函数),她将自动完成调度运算并完成任务的切换。调度操作在下面情况中将被运行:调用api函数(用户主动调度),中断(系统、用户的)
我现在不能理解的是调度算法的api函数到底是怎么实现的(对于任务的优先级运算(查表)这个我知道),她怎么实现调度前运行的任务的数据(特别是pc地址)保存?如果任务从运行到就绪再到运行,它是否将从新开始运行任务程序,还是从调度以前的断点处运行?任务为什么需要被写成无限循环方式?还有,中断结束后,系统也要进行调度操作,那么她怎么实现的?因为按照常理,中断结束后,系统应该直接回到原先任务的断点处运行,除非她在中断函数中将调度函数用软件方式入栈了,这样reti退出后,系统运行调度函数。。。如果是这样,用户自己的中断函数是否也需要这样编写(用软件将调度函数入栈)才能实现调度运算呢?否则系统就不执行调度算法,直到系统的定时中断到了才执行调度算法(系统的中断函数是否有这样的特殊处理),当然系统可能还有其他的什么方式来实现调度算法(我是想不出来了),请赐教~~~谢谢~~~~~~~~~~~

TO:XXX
    uCOSII工作核心原理是:近似地让最高优先级的就绪任务处于运行状态。
    操作系统将在下面情况中进行任务调度:调用API函数(用户主动调用),中断(系统占用的时间片中断OsTimeTick(),用户使用的中断)。
    调度算法书上讲得很清楚,我主要讲一下整体思路。
    (1)在调用API函数时,有可能引起阻塞,如果系统API函数察觉到运行条件不满足,需要切换就调用OSSched()调度函数,这个过程是系统自动完成的,用户没有参与。OSSched()判断是否切换,如果需要切换,则此函数调用OS_TASK_SW()。这个函数模拟一次中断(在51里没有软中断,我用子程序调用模拟,效果相同),好象程序被中断打断了,其实是OS故意制造的假象,目的是为了任务切换。既然是中断,那么返回地址(即紧邻OS_TASK_SW()的下一条汇编指令的PC地址)就被自动压入堆栈,接着在中断程序里保存CPU寄存器(PUSHALL)……。堆栈结构不是任意的,而是严格按照uCOSII规范处理。OS每次切换都会保存和恢复全部现场信息(POPALL),然后用RETI回到任务断点继续执行。这个断点就是OSSched()函数里的紧邻OS_TASK_SW()的下一条汇编指令的PC地址。切换的整个过程就是,用户任务程序调用系统API函数,API调用OSSched(),OSSched()调用软中断OS_TASK_SW()即OSCtxSw,返回地址(PC值)压栈,进入OSCtxSw中断处理子程序内部。反之,切换程序调用RETI返回紧邻OS_TASK_SW()的下一条汇编指令的PC地址,进而返回OSSched()下一句,再返回API下一句,即用户程序断点。因此,如果任务从运行到就绪再到运行,它是从调度前的断点处运行。
    (2)中断会引发条件变化,在退出前必须进行任务调度。uCOSII要求中断的堆栈结构符合规范,以便正确协调中断退出和任务切换。前面已经说到任务切换实际是模拟一次中断事件,而在真正的中断里省去了模拟(本身就是中断嘛)。只要规定中断堆栈结构和uCOSII模拟的堆栈结构一样,就能保证在中断里进行正确的切换。任务切换发生在中断退出前,此时还没有返回中断断点。仔细观察中断程序和切换程序最后两句,它们是一模一样的,POPALL+RETI。即要么直接从中断程序退出,返回断点;要么先保存现场到TCB,等到恢复现场时再从切换函数返回原来的中断断点(由于中断和切换函数遵循共同的堆栈结构,所以退出操作相同,效果也相同)。用户编写的中断子程序必须按照uCOSII规范书写。任务调度发生在中断退出前,是非常及时的,不会等到下一时间片才处理。OSIntCtxSw()函数对堆栈指针做了简单调整,以保证所有挂起任务的栈结构看起来是一样的。
    (3)在uCOSII里,任务必须写成两种形式之一(《uCOSII中文版》p99页)。在有些RTOS开发环境里没有要求显式调用OSTaskDel(),这是因为开发环境自动做了处理,实际原理都是一样的。uCOSII的开发依赖于编译器,目前没有专用开发环境,所以出现这些不便之处是可以理解的。

我的博客:
http://blog.sina.com.cn/u/2766726333

  回复 

举报

   

armecos

60

主题

1325

帖子

4929

积分

中级工程师

专家等级:

结帖率:100%

16楼

  楼主| 发表于 2014-12-29 12:45 | 只看该作者 |返回版面

关于uCOS51V1.0版本不支持参数传递BUG的修正


    uCOS51V1.0版本有一个严重BUG,不支持参数传递(在uCOS51V1.1版本中已经修正)。 网友YAYACOMNET指出此问题,经过检查是由于pdata没有入栈造成的。KEIL编译器对于函数参数的传递种类繁多,有时用寄存器,有时用仿真堆栈,还有的寄存器和堆栈混用,这下处理参数传递好象变得很复杂,似乎无法实现。幸运的是uC/OS-II的任务参数只有一个void *pdata,通过这个空指针,可以传递任意的结构体变量,用户参数安排在结构体里,使用灵活。经过查C51.PDF知,此种情况下,任务的void *ppdata参数恰好是用R3、R2、R1传递,而不通过虚拟堆栈。R3、R2、R1用于传递任务参数ppdata,其中R3代表存储器类型,R2为高字节偏移,R1为低字节位移。因为我用的全是XDATA,所以存储器类型固定为1即R3=1,见C51.PDF第178页说明。修改OS_CPU_C.C的部分代码如下:

void *OSTaskStkInit (void (*task)(void *pd), void *ppdata, void *ptos, INT16U opt) reentrant
{    
    OS_STK *stk;

    ppdata = ppdata;
    opt    = opt;                               //opt没被用到,保留此语句防止告警产生    
    stk    = (OS_STK *)ptos;                    //用户堆栈最低有效地址
    *stk++ = 15;                                //用户堆栈长度
    *stk++ = (INT16U)task & 0xFF;               //任务地址低8位
    *stk++ = (INT16U)task >> 8;                 //任务地址高8位    
    *stk++ = 0x00;                              //PSW
    *stk++ = 0x0A;                              //ACC
    *stk++ = 0x0B;                              //B
    *stk++ = 0x00;                              //DPL
    *stk++ = 0x00;                              //DPH
    *stk++ = 0x00;                              //R0
    
//R3、R2、R1用于传递任务参数ppdata,其中R3代表存储器类型,R2为高字节偏移,R1为低字节位移。
//通过分析KEIL汇编,了解到任务的void *ppdata参数恰好是用R3、R2、R1传递,不是通过虚拟堆栈。
    *stk++ = (INT16U)ppdata & 0xFF;             //R1
    *stk++ = (INT16U)ppdata >> 8;               //R2
    *stk++ = 0x01;                              //R3  因为我用的全是XDATA,所以存储器类型固定为1,见C51.PDF第178页说明。

    *stk++ = 0x04;                              //R4
    *stk++ = 0x05;                              //R5
    *stk++ = 0x06;                              //R6
    *stk++ = 0x07;                              //R7
                                                //不用保存SP,任务切换时根据用户堆栈长度计算得出。    
    *stk++ = (INT16U) (ptos+MaxStkSize) >> 8;   //?C_XBP 仿真堆栈指针高8位
    *stk++ = (INT16U) (ptos+MaxStkSize) & 0xFF; //?C_XBP 仿真堆栈指针低8位
        
    return ((void *)ptos);
}

我的博客:
http://blog.sina.com.cn/u/2766726333

  回复 

举报

   

armecos

60

主题

1325

帖子

4929

积分

中级工程师

专家等级:

结帖率:100%

17楼

  楼主| 发表于 2014-12-29 12:46 | 只看该作者 |返回版面

关于keilc51入出临界区的内嵌汇编

====
背景
====
2004/02/18收到陈学章网友来信,报告ucos51的BUG,内容如下:
Wednesday, February 18, 2004 1:43 PM

> 在OS_CPU_A.ASM中的OSStartHighRdy函数中你注释
> “;上电后51自动关中断,此处不必用CLR EA指令,因为到此处还未开中断,本程序退出后,开中断.”
> 小弟感觉不妥,因为在系统调用OSInit()时会自动创建一个优先级最低的系统任务,创建过程中会调用OS_EXIT_CRITICAL()打开EA。

经查情况属实。当时移植的时候我也隐隐觉得有些不妥,但考虑到要用内嵌汇编等我不熟悉的技术时,感到困难太大(当时还有很多更重要的技术难点需要解决),就没有深入思考。后来,在调试ucos51shell程序时,发现按键不能及时显示在超级终端上,初步判断是由于在显示程序中使用了不合理的临界区保护代码,但当时没有解决。从现在分析来看,这两个问题都与临界区保护有关,实践的结果也确实如此。

===============================
1.ucos51临界区BUG的保守解决方案
===============================
ucos的作者给出两种临界区保护方案:
方法一:
    执行这两个宏的第一个也是最简单的方法是在OS_ENTER_CRITICAL()中调用处理器指令来禁止中断,以及在OS_EXIT_CRITICAL()中调用允许中断指令。
    缺点:在这个过程中还存在着小小的问题。如果用户在禁止中断的情况下调用μC/OS-Ⅱ函数,在从μC/OS-Ⅱ返回的时候,中断可能会变成是允许的了!如果用户禁止中断就表明用户想在从μC/OS-Ⅱ函数返回的时候中断还是禁止的。在这种情况下,光靠这种执行方法可能是不够的。嵌套调用OS_ENTER_CRITICAL()/OS_EXIT_CRITICAL()对时,会引发错误。
    优点:(1)速度快;(2)避免关闭中断后调用PEND阻塞类API函数引起死机。

方法二:
    执行OS_ENTER_CRITICAL()时先将中断禁止状态保存到堆栈中,然后禁止中断。而执行OS_EXIT_CRITICAL()时只是从堆栈中恢复中断状态。
    缺点:(1)总指令周期长,速度慢;
          (2)如果用户在中断禁止的时候调用μC/OS-Ⅱ服务,其实用户是在延长应用程序的中断响应时间。
          (3)用户的应用程序还可以用OS_ENTER_CRITICAL()和OS_EXIT_CRITICAL()来保护代码的临界段。但是,用户在使用这种方法的时候还得十分小心,因为如果用户在调用象OSTimeDly()之类的服务之前就禁止中断,很有可能用户的应用程序会崩溃。发生这种情况的原因是任务被挂起直到时间期满,而中断是禁止的,因而用户不可能获得节拍中断!很明显,所有的PEND调用都会涉及到这个问题,用户得十分小心。一个通用的办法是用户应该在中断允许的情况下调用μC/OS-Ⅱ的系统服务!
    优点:如果用这个方法的话,不管用户是在中断禁止还是允许的情况下调用μC/OS-Ⅱ服务,在整个调用过程中都不会改变中断状态。

结论:
    哪种方法更好一点?这就得看用户想牺牲些什么。如果用户并不关心在调用μC/OS-Ⅱ服务后用户的应用程序中中断是否是允许的,那么用户应该选择第一种方法执行。如果用户想在调用μC/OS-Ⅱ服务过程中保持中断禁止状态,那么很明显用户应该选择第二种方法。

    ucos书上给出的例子是基于PC机的,它运行的环境是Windows下的DOS仿真。由于此时CPU工作在保护模式,程序运行在V86模式,中断被Windows操作系统接管了,ucos无法实际关闭中断。而且在ucos启动前(OSStart),就有时钟中断存在。作者给出的ucos在DOS环境下的演示方案是:所有例子只提供一个时钟中断,没有其他任何IO中断存在,开关中断只影响tickISR,用户根本不用关心中断是否是允许的,用方法一或方法二入出临界区都可以。“用户必须在开始多任务调度后(即调用OSStart()后)允许时钟节拍中断。换句话说,就是用户应该在OSStart()运行后,μC/OS-Ⅱ启动运行的第一个任务中初始化节拍中断。通常所犯的错误是在调用OSInit()和OSStart()之间允许时钟节拍中断。”对此,作者给出的方案是,创建第一个任务TaskStart ,在这个任务里把ucos的tickISR挂接到硬时钟中断上,然后创建需要的各种任务,接下来死循环周期采样判断是否有按键退出。这样满足了允许时钟节拍中断的时机要求。
    对于51上的ucos,由于有多个IO中断,OS_ENTER_CRITICAL()/OS_EXIT_CRITICAL()对有时嵌套调用,所以,我保守地建议使用第二种方法入出临界区。虽然执行速度慢,但稳定可靠。只要用户遵循“在中断允许的情况下调用μC/OS-Ⅱ的系统服务!”原则,就不会死机。
    1。由于#pragma关键字不能出现在H头文件里,所以必须手工修改所有的OS_ENTER_CRITICAL()和OS_EXIT_CRITICAL()为“4.入出临界区标准代码”所示的样子。用户程序使用入出临界区保护代码时也需手工粘贴此代码,然后按“3.KEILC51内嵌汇编的方法”进行编译。虽然不如宏定义方便,但也不是很麻烦。

    2。使用第二种方法,需要在OSIntCtxSw中调整SP指针去掉在调用OSIntExit(),OS_ENTER_CRITICAL(),OSIntCtxSw()过程中压入堆栈的多余内容(SP=SP-5,2+1+2),如下所示:
    OS_CORE.C文件:51堆栈由下至上
      OSIntExit  //2字节返回地址
        OS_ENTER_CRITICAL()  //PUSH IE; 1字节
          OSIntCtxSw()  //2字节返回地址

    3。上电后51自动关中断,在什么地方打开中断最合适呢?在OSStartHighRdy退出时SETB EA开中断最合适!
    为了减少代码量,OSCtxSw、OSIntCtxSw和OSStartHighRdy合用退出代码段,会不会有问题呢?不会!
    任务切换只会发生在1、复位;2、tick中断;3、中断;4、系统API调用四个阶段。复位后打开中断,允许系统正常运行是必然的,在OSStartHighRdy退出时必须打开中断。如果是2、3情况引发的调度,中断本来就是打开的,OSIntCtxSw退出时打开中断毫无影响。按照“在中断允许的情况下调用μC/OS-Ⅱ的系统服务!”原则,用户必须在打开中断的情况下调用系统API,所以OSCtxSw退出时打开中断也毫无影响。虽然此时打开中断多此一举,浪费了代码空间和执行时间,但强制退出时开中断,可以稍微起到保护系统不死机的作用(例如:调用系统API前关中断的错误情况)。
    
    通过上面三步,入出临界区的BUG得以完整解决,我仔细审查了一下,觉得这个思路比较严密,应该是比较健壮了,希望网友们继续提出意见和建议,让我们一起把它做得更完善。

=======================================
2.ucos51shell固化后显示不正常的解决方法
=======================================
    显示函数(serial.c)在多任务环境下需要临界保护,因为共享同一个输出设备,资源是独占的,否则会出现混乱。比较好的方法是使用信号量互斥,但那样实在是太慢了,为了提高效率,一个简单的办法就是关中断(前提是关中断时间不能太长,以免影响中断响应时间)。
    开关中断的方法如“4.入出临界区标准代码”所示,在需要保护的关键段两头粘贴此段代码,然后按照“3.KEILC51内嵌汇编的方法”所示编译即可。
    详见ucos51shellv2代码。

=======================
3.KEILC51内嵌汇编的方法
=======================
有时在C51程序中需要嵌入一些汇编代码,这时当然可以用通常的作法:按照 C51 与汇编的接口写一个汇编函数,然后在 C51 程序中调用该函数。(此种方法可在论坛里搜索(www.c51bbs.com),以前有很多帖子讲到,不再重复)
下面介绍直接嵌入汇编代码的方法:
1、在 C 文件中要嵌入汇编代码片以如下方式加入汇编代码:
#pragma ASM
; Assembler Code Here
#pragma ENDASM
例如:
#pragma ASM
    PUSH IE;
    CLR EA;
#pragma ENDASM
2、在 Project 窗口中包含汇编代码的 C 文件上单击右键,选择“Options for ...”,点击右边的“Generate Assembler SRC File”和“Assemble SRC File”,使检查框由灰色变成黑色(有效)状态;
3、根据选择的编译模式,把相应的库文件(如 Small 模式时,是 Keil\C51\Lib\C51S.Lib;Large 模式时,是 Keil\C51\Lib\C51L.Lib)加入工程中,该文件必须作为工程的最后文件(切记是放在最后位置);
4、编译,即可生成目标代码。
问题解答:
问:为什么要把库文件加入工程?是BUG吗?
答:因为实际已经处于汇编连接,所以要加LIB。C连接是自动加的。 
建议:
把SRC文件也加入项目中,不然连接的OBJ只是前次生成的。

====================
4.入出临界区标准代码
====================
入临界区时首先保存EA值,然后令EA=0关中断,出临界区时恢复保存的EA值。采用这种方法,在嵌套调用OS_ENTER_CRITICAL()/OS_EXIT_CRITICAL()对时,不会发生错误(EA进入临界区前是什么值,退出后还是什么值)。

//OS_ENTER_CRITICAL()
//入临界区
#pragma ASM
    PUSH IE;
    CLR EA;
#pragma ENDASM

//OS_EXIT_CRITICAL()
//出临界区
#pragma ASM
    POP IE;
#pragma ENDASM

我的博客:
http://blog.sina.com.cn/u/2766726333

  回复 

举报

   

armecos

60

主题

1325

帖子

4929

积分

中级工程师

专家等级:

结帖率:100%

18楼

  楼主| 发表于 2014-12-29 12:47 | 只看该作者 |返回版面

关于串口发送程序的BUG修正


网友windysons在2004-3-24 14:12:02来信询问如下问题:
-------------------------------------------------------------------------------
你好~我看到你写的serial.c的文件中,有一个问题有点不明白

我把你的serial.c单独拿出来用,没有使用UCOS2,只想输出一个字符PrintChar('b');
但是怎么也出不来,但是我连续调用两次这个函数,就能输出两个b,
但是调用一次,什么也没有显示,这是为什么样?还是你这个文件中的小BUG,谢谢!
-------------------------------------------------------------------------------
2004-3-26 11:33:20网友windysons修改程序如下:
-------------------------------------------------------------------------------
问题我已经解决,稍微改动了一下你的子程序
你的原程序是:
void PrintChar(unsigned char ch) reentrant//显示字符
{
    unsigned char *t;

    //ES=0;
    if( TIflag){
        TIflag=0;
        TI=1;
    }
    t=inTxBuf;t++;
    if(t==TxBuf+LenTxBuf) t=TxBuf;
    if(t==outTxBuf) {/*ES=1;*/return;}                  //TxBuf Full
    *inTxBuf=ch;
    inTxBuf=t;
    //ES=1;
}       
我改动后好使的现程序,只是把标志位判断调到子程序最后就OK了
void PrintChar(unsigned char ch) reentrant//显示字符
{
    unsigned char *t;

    //ES=0;
    t=inTxBuf;t++;
    if(t==TxBuf+LenTxBuf) t=TxBuf;
    if(t==outTxBuf) {/*ES=1;*/return;}                  //TxBuf Full
    *inTxBuf=ch;
    inTxBuf=t;
    //ES=1;  
    if( TIflag){
        TIflag=0;
        TI=1;
    }
}
-------------------------------------------------------------------------------
2004-3-27 21:14:14我发现这是一个BUG
-------------------------------------------------------------------------------
    在执行到PrintChar函数里TI=1一句时立即引发串口中断发送过程,若此时显示缓冲区为空,则串口发送中断立即返回,导致自动连续发送过程结束,除非再次被启动。第一次发送时,还没有在显示缓冲区里准备好数据就启动发送过程,导致发送过程停止,即使随后缓冲区里写入数据,也要等下次启动时才能被发出。第二次发送时,显示缓冲区里有数据,发送中断程序被启动后将发出字符,发送完毕后再次引发TI中断,因为串口速率比CPU慢,所以在下次中断到来前CPU有时间把第二个字符写入缓冲区。只要缓冲区里有数据,一经PrintChar首次启动,发送中断就能自己维持连续发送过程,直到缓冲区空。这样,第一次发送时什么也不显示,而第二次发送时显示两个字符。
    在ucos51shell程序里也存在这个问题。在人机界面下,输入help,敲入h将不显示,继续敲入e,显示he,接着敲入l不显示,再敲入p,接着显示lp。当时百思不得其解(2002年12月),以为是任务切换导致的错误,又不知道如何写内嵌汇编,就放下了。2004/02/20我在PrintChar里加入关中断的内嵌汇编,同样解决了这个问题。这更使我误以为是任务切换不当造成的。现在看来,这是因为在TI=1前关中断保证了在缓冲区里没有存入数据前,发送中断程序不会被执行,从而避免了中断连续发送过程被打断,自持过程停止需要重新启动的问题。按你所说的方法修改serial.c,shell将正常回显键盘输入字符。
    总之,在显示缓冲区为空时不应启动发送过程,要在准备好显示数据后再启动自持发送过程。

我的博客:
http://blog.sina.com.cn/u/2766726333

  回复 

举报

   

armecos

60

主题

1325

帖子

4929

积分

中级工程师

专家等级:

结帖率:100%

19楼

  楼主| 发表于 2014-12-29 12:48 | 只看该作者 |返回版面

NE2000网卡芯片驱动程序


讨论3---- RTL8019AS网卡驱动程序

    我的SNMP网管板使用了RTL8019AS 10M ISA网卡芯片接入以太网。选它的好处是:NE2000兼容,软件移植性好;接口简单不用转换芯片如PCI-ISA桥;价格便宜2.1$/片(我的购入价为22元RMB/片);带宽充裕(针对51);较长一段时间内不会停产。8019有3种配置模式:跳线方式、即插即用P&P方式、串行Flash配置方式。为了节省成本,我去掉了9346而使用X5045作为闪盘存储MAC地址和其他可配置信息。P&P模式用在PC机中,这里用不上。只剩下跳线配置模式可用,它的电路设计参考REALTEK提供的DEMO板图纸。一天时间就可以完成,相对来说硬件设计比较简单。
    与这部分硬件相对应的软件是网卡驱动。所谓驱动程序是指一组子程序,它们屏蔽了底层硬件处理细节,同时向上层软件提供硬件无关接口。驱动程序可以写成子程序嵌入到应用程序里(如DOS下的I/O端口操作和ISR),也可以放在动态链接库里,用到的时候再动态调入以便节省内存。在WIN98中,为了使V86、WIN16、WIN32三种模式的应用程序共存,提出了虚拟机的概念,在CPU的配合下,系统工作在保护模式,OS接管了I/O、中断、内存访问,应用程序不能直接访问硬件。这样提高了系统可靠性和兼容性,也带来了软件编程复杂的问题。任何网卡驱动都要按VXD或WDM模式编写,对于硬件一侧要处理虚拟机操作、总线协议(如ISA、PCI)、即插即用、电源管理;上层软件一侧要实现NDIS规范。因此在WIN98下实现网卡驱动是一件相当复杂的事情。
    我这里说的驱动程序特指实模式下的一组硬件芯片驱动子程序。从程序员的角度看,8019工作流程非常简单,驱动程序将要发送的数据包按指定格式写入芯片并启动发送命令,8019会自动把数据包转换成物理帧格式在物理信道上传输。反之,8019收到物理信号后将其还原成数据,按指定格式存放在芯片RAM中以便主机程序取用。简言之就是8019完成数据包和电信号之间的相互转换:数据包<===>电信号。以太网协议由芯片硬件自动完成,对程序员透明。驱动程序有3种功能:芯片初始化、收包、发包。
    以太网协议不止一种,我用的是802.3。它的帧结构如图1所示。物理信道上的收发操作均使用这个帧格式。其中,前导序列、帧起始位、CRC校验由硬件自动添加/删除,与上层软件无关。值得注意的是,收到的数据包格式并不是802.3帧的真子集,而是如图2所示。明显地,8019自动添加了“接收状态、下一页指针、以太网帧长度(以字节为单位)”三个数据成员(共4字节)。这些数据成员的引入方便了驱动程序的设计,体现了软硬件互相配合协同工作的设计思路。当然,发送数据包的格式是802.3帧的真子集,如图3所示。
    

    
    有了收发包的格式,如何发送和接收数据包呢?如图4所示,先将待发送的数据包存入芯片RAM,给出发送缓冲区首地址和数据包长度(写入TPSR、TBCR0,1),启动发送命令(CR=0x3E)即可实现8019发送功能。8019会自动按以太网协议完成发送并将结果写入状态寄存器。如图5所示,接收缓冲区构成一个循环FIFO队列,PSTART、PSTOP两个寄存器限定了循环队列的开始和结束页,CURR为写入指针,受芯片控制,BNRY为读出指针,由主机程序控制。根据CURR==BNRY+1?可以判断是否收到新的数据包,新收到的数据包按图2格式存于以CURR指出的地址为首址的RAM中。当CURR==BNRY时芯片停止接收数据包。如果做过FPGA设计,用过VHDL,可以想象到硬件芯片的工作原理。此处,设计2个8bit寄存器和一个2输入比较器,当收到数据包时,接收状态机根据当前状态和比较器结果决定下一个状态,如果CURR=BNRY,进入停收状态;反之,CURR按模增1。8019数据手册没有给出硬件状态机实现方法,说明也很简略,往往要通过作实验的方法推理出工作过程。比如,ISR寄存器不只和中断有关,当接收缓冲溢出时,如果不清ISR(写入FFH),芯片将一直停止接收。在流量较大时溢出经常发生,此时不清ISR,就会导致网卡芯片死机。
    


    明白了发送和接收数据包的原理,那么数据包又是怎样被主机写入芯片RAM和从芯片RAM读出的呢?如图6所示,主机设置好远端DMA开始地址(RSAR0,1)和远端DMA数据字节数(RBCR0,1),并在CR中设置读/写,就可以从远端DMA口寄存器里读出芯片RAM里的数据/把数据写入芯片RAM。
    何谓本地/远端DMA呢?如图7所示,“远端”指CPU接口侧;“本地”指8019的硬件收发电路侧。没有更深的意思,与远近无关,仅仅为了区分主机和芯片硬件两个接口端。这里的DMA与平时所说的DMA有点不同。RTL8019AS的local DMA操作是由控制器本身完成的,而其remote DMA并不是在无主处理器的参与下,数据能自动移到主处理器的内存中。remote DMA指主机CPU给出起址和长度就可以读写芯片RAM,每操作一次RAM地址自动加1。而普通RAM操作每次要先发地址再处理数据,速度较慢。
    在一些高档通信控制器上自带有MAC控制器,工作原理与8019的差不多,比如:Motorola 68360/MPC860T内部的CPM带有以太网处理器,通过设置BD表,使软件和硬件协同工作,它的缓冲区更大且可灵活配置。这些通信控制器的设计,体现了软硬件互相融合协同工作的趋势:软件硬化(VHDL),硬件软化(DSP),希望大家关注!
    


    如图7所示,8019以太网控制器以存储器(16K双口RAM)为核心,本地和远端控制器并发操作。这种体系结构满足了数据带宽的需要。8019拥有控制、状态、数据寄存器,通过它们,51单片机可以与8019通信。由于51资源紧张,在实现TCPIP协议栈时不要进行内存块拷贝,建议(1)使用全局结构体变量,在内存中只保存一个数据包拷贝,其他没有来得及处理的包保存在8019的16K RAM里;(2)使用查询方式而不用中断;(3)客户服务器模型中服务器工作于串行方式,并发模式不适合51单片机。
    芯片内部地址空间的分配如图8所示,其中0x00-0x0B(工作于8位DMA模式)用于存放本节点MAC地址,奇偶地址内容是重复放置的。如:MAC地址0000 1234 5678存放在0x00-0x0B中为000000001212343456567878,单地址和双地址的内容是重复的.一般使用偶数地址的内容,这主要是为了同时适应8位和16位的dma。Prom内容是网卡在上电复位的时候从93C46里读出来的。如果你没有使用93C46,就不要使用Prom,那么使用了93C46后如何获得网卡的地址呢?有两种方法,一是直接读93C46,二是读Prom。网卡MAC地址既不由93C46也不由Prom决定,而是由PAR0-PAR5寄存器决定。Prom只保存上电时从9346中读出的MAC地址(如果有93C46的话),仅此而矣。
    




    网卡MAC地址不是随便定义的,它的组成结构如图9所示。以太网的地址为48位,由ieee统一分配给网卡制造商,每个网卡的地址都必须是全球唯一的。共6个字节的长度。FF:FF:FF:FF:FF:FF为广播地址,只能用在目的地址段,不能作为源地址段。目的地址为广播地址的数据包,可以被一个局域网内的所有网卡接收到。合法的以太网地址第32位组播标志必须为0。例如:
X0:XX:XX:XX:XX:XX
X2:XX:XX:XX:XX:XX
X4:XX:XX:XX:XX:XX 
X6:XX:XX:XX:XX:XX
X8:XX:XX:XX:XX:XX
XA:XX:XX:XX:XX:XX
XC:XX:XX:XX:XX:XX 
XE:XX:XX:XX:XX:XX
为合法以太网地址。上面的X代表0-F中的任一个。
地址
X1:XX:XX:XX:XX:XX
X3:XX:XX:XX:XX:XX
X5:XX:XX:XX:XX:XX 
X7:XX:XX:XX:XX:XX
X9:XX:XX:XX:XX:XX
XB:XX:XX:XX:XX:XX
XD:XX:XX:XX:XX:XX 
XF:XX:XX:XX:XX:XX 
为组播地址,只能作为目的地址,不能作为源地址。组播地址可以被支持该组播地址的一组网卡接收到。组播地址主要用在视频广播,远程唤醒(通过发一个特殊的数据包使网卡产生一个中断信号,启动电脑),游戏(多个人在局域网里联机打游戏)里等。
以下是一些具体的组播地址:
地址范围
01:00:5E:00:00:00---01:00:5E:7F:FF:FF 用于ip地址的组播,其他组播地址跟tcp/ip无关,不做介绍。
网卡可以接收以下3种地址的数据包:
第一种 目的地址跟自己的网卡地址是一样的数据包;
第二种 目的地址为FF:FF:FF:FF:FF:FF广播地址的数据包;
第三种 目的地址为跟自己的组播地址范围相同的数据包。

在以太网的应用当中,如果你希望你的数据包只发给一个网卡,目的地址用对方的网卡地址;
如果你想把数据包发给所有的网卡,目的地址用广播地址;
如果你想把数据包发给一组网卡,目的地址用组播地址。

其他用到的寄存器:
CR---命令寄存器       TSR---发送状态寄存器        ISR---中断状态寄存器
RSR---接收状态寄存器  RCR---接收配置寄存器        TCR---发送配置寄存器
DCR---数据配置寄存器  IMR---中断屏蔽寄存器        NCR---包发送期间碰撞次数
FIFO---环回检测后,查看FIFO内容
CNTR0---帧同步错总计数器
CNTR1---CRC错总计数器
CNTR2---丢包总计数器
PAR0-5---本节点MAC地址
MAR0-7---多播地址匹配

建议:将图形中寄存器名称标注上页号和地址偏移(如:BNRY 0页0x03),打印出此图,看图编程,直观且不容易出错。

备注:收缓冲区、发缓冲区、数据存储区在16K双口RAM里的安排由用户自行决定,只要不引起冲突即可,以下源程序代码实现的只是其中的一种分配方案。

部分源程序清单:
struct ethernet{
    unsigned char status;          //接收状态
    unsigned char nextpage;        //下一个页
    unsigned int  length;          //以太网长度,以字节为单位
    unsigned int  destnodeid[3];   //目的网卡地址
    unsigned int  sourcenodeid[3]; //源网卡地址
    unsigned int  protocal;        //下一层协议
    unsigned char packet[1500];    //包的内容
};

void ne2000init()//ne2000网卡初始化
{
    rtl8019as_rst();

    reg00=0x21;   //选择页0的寄存器,网卡停止运行,因为还没有初始化。
    delay_ms(10); //延时10毫秒,确保芯片进入停止模式
//使芯片处于mon和loopback模式,跟外部网络断开
    page(0);
    reg0a=0x00;
    reg0b=0x00;
    reg0c=0xE0; //monitor mode (no packet receive)
    reg0d=0xE2; //loop back mode
//使用0x40-0x4B为网卡的发送缓冲区,共12页,刚好可以存储2个最大的以太网包。
//使用0x4c-0x7f为网卡的接收缓冲区,共52页。
    reg01=0x4C; //Pstart  接收缓冲区范围
    reg02=0x80; //Pstop
    reg03=0x4C; //BNRY

    reg04=0x40; //TPSR    发送缓冲区范围

    reg07=0xFF;/*清除所有中断标志位*/
    reg0f=0x00;//IMR disable all interrupt

    reg0e=0xC8; //DCR byte dma 8位dma方式

    page(1); //选择页1的寄存器
    reg07=0x4D; //CURR  
    reg08=0x00; //MAR0
    reg09=0x41; //MAR1
    reg0a=0x00; //MAR2
    reg0b=0x80; //MAR3
    reg0c=0x00; //MAR4
    reg0d=0x00; //MAR5
    reg0e=0x00; //MAR6
    reg0f=0x00; //MAR7        

    initNIC();        //初始化MAC地址和网络相关参数

//将网卡设置成正常的模式,跟外部网络连接
    page(0);
    reg0c=0xCC; //RCR
    reg0d=0xE0; //TCR
    reg00=0x22; //这时让芯片开始工作?
    reg07=0xFF; //清除所有中断标志位
}

void send_packet(union netcard *txdnet,unsigned int length)//ne2000发包子程序
{//发送一个数据包的命令,长度最小为60字节,最大1514字节需要发送的数据包要先存放在txdnet缓冲区
    unsigned char i;
    unsigned int ii;

    page(0);
    if(length<60) length=60;
    for(i=0;i<3;i++)
        txdnet->etherframe.sourcenodeid=my_ethernet_address.words;
    txd_buffer_select=!txd_buffer_select;
    if(txd_buffer_select)
        reg09=0x40 ;          //txdwrite highaddress
    else
        reg09=0x46 ;          //txdwrite highaddress       
    reg08=0x00;                         //read page address low
    reg0b=length>>8;          //read count high
    reg0a=length&0xFF;        //read count low;
    reg00=0x12;                                //write dma, page0
   
    for(ii=4;ii<length+4;ii++)
        reg10=txdnet->bytes.bytebuf[ii]; 

    for(i=0;i<6;i++){                   //最多重发6次
        for(ii=0;ii<1000;ii++)          //检查txp为是否为低
            if((reg00&0x04)==0) break;
         
        if((reg04&0x01)!=0) break;      //表示发送成功        
        
        reg00=0x3E;
    }
        
    if(txd_buffer_select) reg04=0x40;   //txd packet start; 
    else reg04=0x46;          //txd packet start;         

    reg06=length>>8;          //high byte counter
    reg05=length&0xFF;        //low byte counter

    reg00=0x3E;               //to sendpacket;  
}

bit recv_packet(union netcard *rxdnet)//ne2000收包子程序
{
    unsigned char i;
    unsigned int ii;
    unsigned char bnry,curr;
   
    page(0);
    reg07=0xFF;
    bnry=reg03;               //bnry page have read 读页指针
    page(1);
    curr=reg07;               //curr writepoint 8019写页指针
    page(0);
    if(curr==0)
        return 0;             //读的过程出错        
    bnry=bnry++;
    if(bnry>0x7F) bnry=0x4C;
    if(bnry!=curr){           //此时表示有新的数据包在缓冲区里
        //读取一包的前18个字节:4字节的8019头部,6字节目的地址,6字节原地址,2字节协议
        //在任何操作都最好返回page0
        page(0); 
        reg09=bnry;           //read page address high
        reg08=0x00;           //read page address low
        reg0b=0x00;           //read count high
        reg0a=18;             //read count low;
        reg00=0x0A;           //read dma
        for(i=0;i<18;i++)
            rxdnet->bytes.bytebuf=reg10;
        i=rxdnet->bytes.bytebuf[3];     //将长度字段的高低字节掉转
        rxdnet->bytes.bytebuf[3]=rxdnet->bytes.bytebuf[2];
        rxdnet->bytes.bytebuf[2]=i;                         
        rxdnet->etherframe.length=rxdnet->etherframe.length-4; //去掉4个字节的CRC
        //表示读入的数据包有效
        if(((rxdnet->bytes.bytebuf[0]&0x01)==0)||(rxdnet->bytes.bytebuf[1]>0x7F)||(rxdnet->bytes.bytebuf[1]<0x4C)||(rxdnet->bytes.bytebuf[2]>0x06)){
            //接收状态错误,或者next_page_start错误或者长度错误,将丢弃所有数据包
            page(1);
            curr=reg07;       //page1
            page(0);          //切换回page0
            bnry=curr-1;
            if(bnry<0x4C) bnry=0x7F;
            reg03=bnry;       //write to bnry                   
            return 0;
        }
        else{//表示数据包是完好的.读取剩下的数据
            if((rxdnet->etherframe.protocal==0x0800)||(rxdnet->etherframe.protocal==0x0806)){
            //协议为IP或ARP才接收        
                reg09=bnry;   //read page address high
                reg08=4;      //read page address low
                reg0b=rxdnet->etherframe.length>>8;     //read count high
                reg0a=rxdnet->etherframe.length&0xFF;   //read count low;
                reg00=0x0A;   //read dma
                for(ii=4;ii<rxdnet->etherframe.length+4;ii++)
                    rxdnet->bytes.bytebuf[ii]=reg10;
            }
            bnry=rxdnet->bytes.bytebuf[1]-1;//next page start-1
            if(bnry<0x4C) bnry=0x7F;
            reg03=bnry;       //write to bnry                            
            return 1;         //have new packet
        }
    } 
    return 0;
}

参考文献:
1。老古网站(www.laogu.com)
2。《单片机与嵌入式系统应用》2001(7-12)合订本第228页《以太网控制器的嵌入式设备网络互连》湖南师范大学 万静华 丁亚军
3。RTL8019AS数据手册

我的博客:
http://blog.sina.com.cn/u/2766726333

  回复 

举报

   

armecos

60

主题

1325

帖子

4929

积分

中级工程师

专家等级:

结帖率:100%

20楼

  楼主| 发表于 2014-12-29 12:49 | 只看该作者 |返回版面

ARP协议实现原理

    ARP是Address Resolution Protocol的缩写。中文译做“地址解析协议”,本质是完成网络地址到物理地址的映射。从概念上讲就是找到一个映射方法f,使得“物理地址 = f(网络地址)”。物理地址有两种基本类型:以太网类型和proNET令牌环网类型,网络地址特指IP地址,对映射方法的要求就是高效。具体到以太网,它使用的是动态绑定转换的方法。为什么不直接使用同一种地址,而要这么麻烦呢?因为TCP/IP网络就是为将不同种类计算机互联而发明的,它的体系结构是分层的,层和层之间相互独立,改变物理层的实现不会影响到网络层。
    32位IP地址到以太网48位物理地址的映射,采用动态绑定转换的方**遇到许多细节问题,例如:减少广播,ARP包丢失,物理地址变更(更换网卡)、移动(移动设备到另一子网)、消失(关机)等。一般是设置ARP高速缓存,通过学习、老化、更新、溢出算法处理ARP映射表来解决这些问题。其中,学习指ARP收到任何指向本节点IP地址的ARP/IP包,从中提取出地址对,而ARP缓存中无对应项时,由ARP接收部分添加;老化指为每项设置寿命域,以便代谢掉陈旧的地址映射项;更新指ARP提取到新的地址对时,用其更新缓存里已有的对应项;溢出算法指当缓存满时,采取何种方法替换旧有的地址对儿。
    我找到了几个TCP/IP源代码,对比他们的实现,深感差别巨大,灵活多变。有的代码未实现ARP缓存,只用几个全局变量记录源目的IP地址和源目的MAC地址,每次通信前直接操作全局变量,这在使用51单片机,进行点对点通信时不失为一个有效的方案;而有的代码庞大复杂,细节处理精益求精。比如实现了ARP高速缓存、支持多址节点、支持网管查看/动态改变ARP相关参数、重发处理、支持IPv6等。我的看法是:ARP的本质是地址转换,只要抓住这个灵魂,设计的大方向就把握住了。具体实现过程各具特色,因人而异,没有统一要求,有些功能可以不实现,有些优点不能兼得,而唯一不变的只有思想。
    我参考了几种已有的IP协议栈并结合51单片机的特点,实现了自己的基于uCOS51的TCP/IP协议栈方案。它只是一种具体的实现范例,不同的人有不同的设计方法。我保证自己的方案可以正常使用并具有较好的完备性。
    
    ------------------------------
    |状态|寿命ttl|IP地址 |MAC地址|        学习
    ------------------------------
    |  0 |   FF  |X:X:X:X| XXXX  |  <---  老化
    ------------------------------
    |  0 |   FF  |X:X:X:X| XXXX  |        更新
    ------------------------------
           图1 ARP缓存表                  表满处理
    
    如图1所示,ARP缓存表由状态、寿命、IP地址、MAC地址4个字段组成。状态字段指示地址对是否有效(0-空闲 1-占用);寿命字段用于老化操作,初始存入最大值,以后由OS时间函数调用,每秒减1,直至为0清除;IP地址和MAC地址字段保存网络地址和物理地址的映射。此处,没有设计发送数据链表首指针和重发记数字段,我把重发操作交给上层软件统一处理,这是本程序的特色。围绕ARP缓存表,完成了4种操作:学习、老化、更新、表满处理,详见伪代码清单。使用OS的Shell命令ls可以查看ARP表的内容,但不支持修改,这个功能对测试很有用。(显示内容举例如图2所示)
    
    %ls
    
     ARP table:
     status     TTL      IP address      MAC address
     =================================================
       01        78     172.18.92.86     0050BABD4C7E
     
    %
              图2 ARP缓存表显示内容举例
              
    
             表满处理
                |
                v                               ARP请求        
            ---------             ----------- ---------->
            |       |  学习/更新  |         | <- - - - -
    老化--->| ARP表 |<------------| ARP处理 |
            |       |             |         | - - - - - >
            ---------             ----------- <----------
                ^                               ARP应答
                |学习/更新
            ---------
            |       |
            | IP_in |
            |       |
            ---------
                        图3 ARP处理过程
                        
    0                 8               16              24               31                    
    ---------------------------------------------------------------------
    |             硬件类型            |            协议类型             |
    ---------------------------------------------------------------------
    |硬件地址长度(HLEN)|协议长度(PLEN)|               操作              |
    ---------------------------------------------------------------------
    |                         发送方首部(八位组0-3)                     |
    ---------------------------------------------------------------------
    |      发送方首部(八位组4-5)      |      发送方IP地址(八位组0-1)    |
    ---------------------------------------------------------------------
    |     发送方IP地址(八位组2-3)     |        目标首部(八位组0-1)      |
    ---------------------------------------------------------------------
    |                         目标首部(八位组2-5)                       |
    ---------------------------------------------------------------------
    |                        目标IP地址(八位组0-3)                      |
    ---------------------------------------------------------------------
                                图4 ARP包结构
    
    如图3,整个ARP处理过程,我主要用5个函数实现。ARP初始化(ARP_init)、ARP请求(ARP_request)、ARP应答(ARP_answer)、ARP回应处理(ARP_process)、IP包接收预处理(IP_in)。在实现网卡驱动程序后,所有ARP处理操作就是填写ARP包(ARP包结构见图4),详见伪代码清单。
    ARP_init完成ARP表初始化,概括说就是ARP表state字段清0。
    ARP_request完成ARP请求操作。ARP协议要求程序根据子网掩码判断IP地址是否属于同一子网,如果在同一子网内,ARP请求目的MAC地址,否则请求默认网关MAC地址。
    ARP_answer比较简单,只要交换ARP请求包地址内容,填写自己的MAC地址和很少的改动后发送即可。
    ARP_process完成ARP回应回来的信息处理。主要进行ARP表的学习和更新。
    IP_in完成IP包接收预处理,用于提取地址映射信息,以便主动学习和及时更新。我的程序不会主动学习不是发给自己IP地址的MAC地址信息,因为ARP表在51中的容量有限,只有频繁用到的地址对才应该存放在里面,否则一旦出现“颠簸”,ARP表就失效了。
    有的ARP实现方案采用数据驱动方式,参数可配置,使用统一的程序,通过加载不同的配置数据,执行不同的操作。这样做使程序版本统一,不同的应用只要加载不同的配置数据即可,不用更换程序,有利于后期维护。但是考虑到51资源紧张和安全性,我的方案只能显示ARP表不允许修改其内容,用户可发挥想象力在此处增加新功能。另外,ARP程序应该记住上一次发过的请求,以避免重发,但同样考虑到资源紧张,也免了。其实无所谓,重发就重发了。表满处理采用有损性能的加速算法,快速有效。另外,本程序不能直接用于嵌入式网关产品。
    uCOS51操作系统本身提供了良好的内存管理功能,我利用它设置了大中小三种缓冲区存放不同类型的数据包。内存使用前申请,使用后释放,有效利用了资源。
    系统特点是:1.抢占式优先级;2.消息驱动;3.串行服务器模式。
    系统优点是:1.等待时不耗费CPU资源;2.有超时保护,不会死锁;3.思路清晰易懂。
    系统基于中断驱动,使用Int0做网卡中断输入口。ISR寄存器只用到4位:OVW 收溢出错/TXE 发被中断错/PTX  发送成功/PRX 接收成功。TCP/IP协议栈做成任务,脱离内核。整体框架如图5、6、7所示。主程序框架见伪代码清单(RxSem和TxSem初始化为0)
    
            ----------
            |网卡中断|
            ----------
                |
                V
            ----------  |>
            |发信号量|  |  收完/收溢出错
            |SemPost |---->-------------- RxSemPost
            ----------  |>
                |       |  发完/发被中断错
                ---------->-------------- TxSemPost
          图5 网卡中断处理程序
          
          
                进入
                 |   ------
                 V   |    |                          发
             ----------   |                       低优先级
     ------> |  等待  |<---   
     |       |TxQPend |<---------------------         -----
     |       ----------                     |          | |
     |           | TxQFIFO非空              |          | |
     |           V                          |   ---<---| |---<---
     |       ----------                     |   数据源 | |  各任务发送来的数据 
     |       | 发送包 |                     |          | |
     |       ----------                     |         -----
     |           |                          |        TxQFIFO
     |           V                          |
     |   ---------------------              |
     |   |    释放内存       |              |
     |   |(包已存入网卡RAM里)|              |
     |   ---------------------              |
     |           |    -----                 |
     |           V    |   |                 |
     |       -----------  |                 |
     |       |  等待   |<--                 | (等效发送包被抛弃)
     |       |TxSemPend|<-----------        |
     |       -----------           |        |
     |           | 发完/超时       |        |
     |           V                 |        |
     | Y  ----------------    -----------   |
     -<---| 发送成功吗? |    |重发第n次|   |
          |(无错且不超时)|    |   n<N   |   |
          ----------------    -----------   |
                 | N              /^\       |
                 V         N       |        |
           ------------------>------        |
           |已发了N次吗?|---------->--------
           ---------------       Y
              
              图6 发送流程图
              
              
                                    进入
                                     |   -----
                                     V   |   |                       收
                                -----------  |                    高优先级
             ------------------>|   等待  |<--
             |        --------->|RxSemPend|<---------------
             |        |         -----------       /|\    /|\
             |        |              | 收到包 或   |      |
             |        |              V 收错 或     |      |
             |        |              | 超时        |      |
             |        |         -----------        |  ----------
             |        |         |存并清ISR|        |  |复位网卡|
        -----------   |         -----------        |  ----------
        |RxSemPost|   |              |             |   /^\  /^\
        -----------   |              V             |    |    |
             |        |      --------------------  |    |    |
             |        |      |超时且无新包且无错| Y|    |    |
             |        |      |    (防死锁)      |->-    |    |
             |        |      --------------------       |    |
            /|\       |(不执行       | N                |    |
             |        |RxSemPost)    V                  |    |
             |        |         ------------  Y         |    |
             |        |         | 收溢出错 |--->---------    |
             |        |         | ISR之OVW |                 |
             | Y      | N       ------------                 |
        ------------------           | N                     |
        |网卡中还有包吗?|           V                       |
        |  CURR!=BNRY+1  | ------------------------  Y       |
        ------------------ |读出包头,查有无逻辑错|--->-------
               |           ------------------------
              /|\                    | N
               |                     V
               |           ------------------------
           ----------      |按包长度申请合适的大中|
           |释放内存|      |小号内存,并存入整个包|
           ----------      |,再调整BNRY          |
             /^\ /^\       ------------------------
              |   |                  |
              |   |                  V
              |   |   N  ----------------------------
              |   ---<---|是否是发给自己IP地址的包?|
              |          ----------------------------
              |                      | Y
              |                      V
              |                 ------------
              |                 |  包分发  |
              |                 ------------
              |                      |
              |                      V
              |           ----------------------------
              |           |        |        |        |
              |           V      -------------------------- IP_in过滤
              |           |        V        V        V
              |          ARP   ICMP(Ping)  UDP      TCP
              |           |        |        |        |
              |           ----------------------------
              |                      | 串行处理
              |                      | (32bitMCU可设计成并发模式)
              |---------<-------------
                 
                              图7 接收流程图
         
    我仔细检查了几遍,似乎比较完备了,各种情况下均可以正常工作。在超负荷流量下,只会抛包,不会死机。当然,由于本人接触资料有限和个人局限性,肯定有错误和疏漏之处,希望大家提出意见和建议。
    
伪代码清单:
ARP_init() //ARP缓存初始化
{
  for(i=0;i<ARPTabSize;i++)
    ARPTable.status=0;
}

ARP_request(目的IP地址) //ARP请求
{
//判断IP地址是否属于同一子网的任务交给上层软件处理
//(由它决定请求网卡IP地址还是默认网关IP地址),
//这有利于减少代码量。

  //申请小号内存
  pARP=OSMemGet();

  //填以太网帧
  以太网协议=0x0806;//ARP协议
  目的MAC地址=0xffff;//广播地址
  源MAC地址=自己的MAC地址;

  //填ARP表
  硬件类型=0x0001;
  协议类型=0x0800;
  硬件地址长度=0x06;
  协议长度=0x04;
  操作=0x0001;//请求
  发送方首部=自己的MAC地址;
  发送方IP地址=源IP地址;
  目标首部=0x0000;
  目标IP地址=目的IP地址;
  
  //填充PAD
  没有内容处填充0;

  //发送ARP包至TxQFIFO缓存
  OSQSend(QID,*pARP);
}

ARP_answer(*pARP) //ARP应答
{
  学习/更新ARP缓存表;
  
  //修改收到的ARP包,形成ARP应答
  //填以太网帧
  目的MAC地址=对方(网卡/网关)发来的源MAC地址;
  源MAC地址=自己的MAC地址;

  //填ARP表
  目标首部=发送方首部;发送方首部=自己的MAC地址;
  交换发送方IP地址和目标IP地址;
  操作=0x0002;//ARP应答

  //发送ARP包至TxQFIFO缓存
  OSQSend(QID,*pARP);
}

ARP_process(*pARP) //ARP应答处理
{
  //更新
  for(i=0;i<ARPTabSize;i++){
    if(ARPTab.status==1){
      if(ARPTab.IPAdr==收到的ARP应答包源IP地址){
        ARPTab.ttl=最大寿命;
        ARPTab.IPAdr=收到的包的源IP地址;
        ARPTab.MACAdr=收到的包的源MAC地址;
        return;
      }
    }
  }
  
  //学习
  for(i=0;i<ARPTabSize;i++){
    if(ARPTab.status==0){
      ARPTab.status=1;
      ARPTab.ttl=最大寿命;
      ARPTab.IPAdr=收到的包的源IP地址;
      ARPTab.MACAdr=收到的包的源MAC地址;
      return;     
    }
  }

  //表满处理,有损性能的快速算法
  ARPTab[index].status=1; //注:index为全局变量,保存ARP缓存表项索引。每次处理加1取模。
  ARPTab[index].ttl=最大寿命;
  index++;
  if(index>=ARPTabSize) index=0;
}

IP_in(*pIP) //IP包过滤(ARP地址学习) 注:这里处理的是IP包,伪代码与上面程序相似,但源代码差别很大。
{
  //更新
  for(i=0;i<ARPTabSize;i++){
    if(ARPTab.status==1){
      if(ARPTab.IPAdr==收到的IP包源IP地址){
        ARPTab.ttl=最大寿命;
        ARPTab.IPAdr=收到的包的源IP地址;
        ARPTab.MACAdr=收到的包的源MAC地址;
        return;
      }
    }
  }
  
  //学习
  for(i=0;i<ARPTabSize;i++){
    if(ARPTab.status==0){
      ARPTab.status=1;
      ARPTab.ttl=最大寿命;
      ARPTab.IPAdr=收到的包的源IP地址;
      ARPTab.MACAdr=收到的包的源MAC地址;
      return;     
    }
  }

  //表满处理,有损性能的快速算法
  ARPTab[index].status=1; //注:index为全局变量,保存ARP缓存表项索引。每次处理加1取模。
  ARPTab[index].ttl=最大寿命;
  index++;
  if(index>=ARPTabSize) index=0;
}

timer() //软定时器任务,用于ARP老化
{
  for(;;){
    taskDelay(1秒);
    for(i=0;i<ARPTabSize;i++){
      if(ARPTab.status==1){
        if(ARPTab.ttl==0)
          ARPTab.status=0;
        else
          ARPTab.ttl--;
    }
  }
}

主程序框架:
    initNIC    //初始化网卡
    //创建资源
    TxSem和RxSem信号量
    TxQFIFO队列
    大中小内存设立
    //创建任务
    收
    发

没有更多推荐了,返回首页