songguoda 发表于 2011-4-10 15:37:21

从单片机初学者迈向单片机工程师(一份正真的让你成为工程师的贴子)整理 补充

从学校毕业到工作,我已经学习单片机有六七年了,可是一直还处于入门阶段吧,没能深入进去,从51单片机到AVR单片机学的都是一些基础的东西,真正到应用到一个项目就感觉无头绪,不知如何下手,于是就想学习一下多任务操作系统UCOSII,但是毕竟是在8位机上运行,增加了代码量不说,学习起来也不是那么容易,任务的划分,单元功能的编写也很不好理,自从看了《从单片机初学者迈向单片机工程师(一份正真的让你成为工程师的贴子)》这份贴后,瞬间让我感觉舒畅,让我重新点燃了对单片机项目开发的兴趣,感谢这些无私帮助别人的工程师们,今天我把这个贴子的资料重新整理了一份,并且再加上一份在网上找的仿真实例传上来,希望能帮助像我一样的学了一段时间单片机还感到困惑,感到无助的朋友们
    另外我还在希望工程师高手们能再讲得深入些,如何在Easy51RTOS下编写各种芯片的驱动,为谢!!!


http://cache.amobbs.com/bbs_upload782111/files_38/ourdev_629763U3DRFQ.GIF
(原文件名:未命名.GIF)

http://cache.amobbs.com/bbs_upload782111/files_38/ourdev_629766KL2HXC.GIF
(原文件名:未命名1.GIF)

从单片机初学者迈向单片机工程师 经过仔细的审核编排,修改了中间的错误原码,看不清的流程图也经过了处理,错别字也改了不少!
从单片机初学者迈向单片机工程师ourdev_629744IQEIMW.rar(文件大小:1.90M) (原文件名:1.rar)


第二章例程ourdev_629746M0CEMT.rar(文件大小:17K) (原文件名:第二章例程.rar)

第三章例程ourdev_629748SUPXQU.rar(文件大小:23K) (原文件名:第三章例程.rar)

第五章例程ourdev_629749FY3046.rar(文件大小:57K) (原文件名:第五章例程.rar)


http://cache.amobbs.com/bbs_upload782111/files_38/ourdev_629761W6CCJB.GIF
(原文件名:1.GIF)

原来的基于Easy51RTOS的Mini51板万年历仿真版
原码重新编译后不能运行,我去掉了原来仿真电路中的CPLD元件,部份代码已修改完成,经编译完全可以运行

基于Easy51RTOS的Mini51板万年历仿真版ourdev_629750U8VCS2.rar(文件大小:126K) (原文件名:基于Easy51RTOS的Mini51板万年历仿真版.rar)



C51编程多任务程序设计的结构ourdev_629751A8HA6V.doc(文件大小:20K) (原文件名:C51编程多任务程序设计的结构.doc)



一种裸奔多任务模型

一个网友的总结:stateMachine + timerTick + queue。

在RTOS环境下的多任务模型:
任务通常阻塞在一个OS调用上(比如从消息队列取数据)。
外部如果想让该任务运转,就要向消息队列发送消息。
任务收到消息时,根据当前状态,决定如何处理消息。这就是状态机。
任务将消息队列中的消息处理完毕后,重新进入阻塞状态。
任务在处理中,有时要延时一段时间,然后才继续工作:
    为了充分使用CPU,可以通过OS调用让其它任务去工作。
    OS通常会提供一个taskDelay调用。
    当任务调用taskDelay时,即进入阻塞状态,直到超时,才重新进入可工作状态(就绪状态)。

下面说说裸奔环境下的多任务模型:
裸奔也可以多任务,但调度是由用户自主控制。
在RTOS环境下,一般提供抢占式调度。在裸奔时,一般是任务在处理告一段落后,主动结束处理。
RTOS环境下的任务,一般处于一个while(1)循环中。
    while(1){
      从消息队列接收消息。如果没有,将阻塞。
      处理消息。
      }
裸奔下的任务,一般采用查询方式:

    查询是否有待处理的事件。
    如果没有,返回。
    如果有,根据任务的当前状态,进行处理。处理完毕后,可能返回,也可能将待处理事件全部处理完毕后再返回。

裸奔任务其实也处于一个while(1)循环中,只不过这个循环在任务外部。
main()
{
    A_taskInit();      //任务的初始化
    B_taskInit();
    ...
    while(1){
      A_taskProc();    //任务的处理
      B_taskProc();
      }


状态机既适用于OS环境,也适用于裸奔环境。
但在裸奔环境下,状态可能被切分得更细。例如后面讲的如何在裸奔环境实现taskDelay()。

消息队列既适用于OS环境,也适用于裸奔环境。
在OS环境下,消息队列机制由OS提供。
在裸奔环境下,消息队列要自己来实现。如果对队列的概念不清楚,可参考《数据结构》教材。
这个队列机制,可做成通用模块,在不同的程序中复用。
消息队列用于缓冲事件。事件不知道什么时候会到来,也不能保证来了就能迅速得到处理。
    使用消息队列,可以保证每个事件都被处理到,以及处理顺序。
一般在两种情况下会用到消息队列:
    存储外部事件:外部事件由中断收集,然后存储到队列。
      串口接收程序中的接收循环缓冲区,可理解为消息队列。
    任务间通讯:一个任务给其它任务发送消息。

timerTick,就是系统中的时钟基准。OS中总是有一个这样的基准。
在裸奔时,我们要用一个定时器(或RTC或watchdog)来建立这个时间基准。
一个tick间隔可以设置为10ms(典型RTOS的缺省设置)。让定时器10ms中断一次,中断发生时给tickNum++。
    以前,我在定时器中断中设置1S标志、200ms标志等等。时间相关的任务根据这些标志判断是否要执行。
    近来,一般让任务直接去察看tickNum。两次相减来判断定时是否到达。
    也可以在系统中建立一个通用定时器任务,管理与不同任务相关的多个定时器;在定时到达时,由定时器任务去调用相应的callback。
系统时钟基准是所谓“零耗时裸奔”的基础。
timerTick的分辨率,决定了只适于于较大的时间延时。
    在做时序时的小延时,用传统方法好了。

OS中的taskDelay()在裸奔环境下的一种实现:
OS环境:
void xxxTask(void)
{

    while(1){
      //waitEvent

      //do step_1

      taskDelay(TIME_OUT_TICK_NUM);

      //do step_2
    }

裸奔环境:
void xxxTask(void)
{
    static unsigned int taskStat = STAT_GENERAL;    //任务状态变量
    static timer_t startTick;
    timer_t currTick;
   
    if (taskStat == STAT_GENERAL)
    {
      //check event

      //if no event
            return;

      //do step_1

      startTick = sysGetTick();    //sysGetTick()就是察看系统时间
      taskStat = STAT_WAIT;
      return;
    }
    else if (taskStat == STAT_WAIT)
    {
      currTick = sysGetTick();    //sysGetTick()就是察看系统时间
      if ((currTick - startTick) >= TIME_OUT_TICK_NUM)
      {
            //do step_2   

            taskStat = STAT_GENERAL;
            return;
      }
      else
            return;
    }
}


老生常谈---一种裸奔多任务模型ourdev_629752P0O6JH.txt(文件大小:4K) (原文件名:老生常谈---一种裸奔多任务模型.txt)


C51多任务编程思想ourdev_629753EWA0LM.pdf(文件大小:143K) (原文件名:C51多任务编程思想.pdf)

基于51单片机的C语言多任务操作 完美版ourdev_629754PETS4B.rar(文件大小:3K) (原文件名:基于51单片机的C语言多任务操作 完美版.rar)


Easy51RTOS的原理

//Easy51RTOS操作系统头文件

#include "os_cfg.h"   

#include "functns.h" //常用一些功能函数


unsigned char TempBuffer;            //显示温度字符串

unsigned char str2={' ',' ',' ',0,0,0,0,0,0,0xdf,0x43,0};                           


//任务0:测温度送显

void task0(void)

{   

         temp=ReadTemperature();

      IntToStr(temp,TempBuffer);


            str2=TempBuffer;

      str2=TempBuffer;

      str2=TempBuffer;

            str2=TempBuffer;

            str2=TempBuffer;

            str2=TempBuffer;


            GotoXY(0,1);

            Print(str2);

         delay_nms(300);

}


//任务1:键盘扫描,LCD显示

void task1(void)

{

   if(CHANGE==0)                         //判断change温度键是否按下

             {

               set_temp=key_set();         //设定需要更改的温度值

      

                     if(set_temp<temp)

               {

                  fengshan();                        //设定的温度<实际温度,则打开电机风扇

         }

               else if(set_temp>temp)

               {

                     dianlu();                                    //若大于,则打开电炉(这里用LED模拟一下)

               }

         }

}


//任务2

void task2()

{


}


//任务3

void task3()

{


}


//任务4

void task4(void)

{   


}


//任务5

void task5(void)

{


}


//任务6

void task6()

{


}


//任务7

void task7()

{

}


//main主函数

void main(void)

{            

OS_InitTimer0();

EA=1;

LCD_Init();

LCD_w_data(1,1,Temp_Str);

LCD_w_data(2,1,Key_Str);

while(1)

{

if (OS_Delay==0){task0();OS_Delay=100;} //温度测量,每秒1次

if (OS_Delay==0){task1();OS_Delay=10;}//键盘扫描,键值存储

if (OS_Delay==0){task2();OS_Delay=100;} //读出存储的键值,LCD显示

if (OS_Delay==0){task3();OS_Delay=50;}

if (OS_Delay==0){task4();OS_Delay=100;}

if (OS_Delay==0){task5();OS_Delay=60;}

if (OS_Delay==0){task6();OS_Delay=70;}

if (OS_Delay==0){task7();OS_Delay=80;}

Delay(50);

//Taskturn;

}

}

//定时中断服务

void OS_Timer0(void) interrupt 1 using 2

{

uchar i;

//CRY_OSC,TIME_PER_SEC在easycfg.h中配置

TH0 = 255-CRY_OSC/TIME_PER_SEC/12/256;

TL0 = 255-CRY_OSC/TIME_PER_SEC/12%256;

//每节拍对任务延时变量减1 ,减至 0 后,任务就绪。

for(i=0;i<MAX_TASK;i++)

{

if(OS_Delay!=0) OS_Delay--;

}

//Runing(On);

}

//和传统的前后感觉基本上是一样的…

//唯一的优点呢,是感觉OS_Delay数组起到了分配各



Easy51RTOS的原理ourdev_629755MEIQGP.txt(文件大小:3K) (原文件名:Easy51RTOS的原理.txt)


基于51单片机的C语言多任务操作 完美版
/*
1.本程序不使用任何汇编指令
2.由定时器T0产生中断,切换进程
3.由于中断或调用子程序,要把PC堆栈,故可以以SP为基址的地方找到PC
4.中断或子程序返回,要把SP出栈给PC,故可以操作SP改变程序入口
5.本程序经调试运行电路图已上传
6.程序编译是会有一个警告提示,为正常现象,因为保存R0-R7时,重新定义地址,
       出现地址覆盖的警告提示。
7.用户以此模板写程序只需写用户的进程子程序和用户初始化子程序,并把各进程参数
       放在规定地方,各程序放在规定地方就可以;所有的任务调度已处理好。
*/
//头文件
#include<reg52.h>
//#include<absacc.h>
//#include<intrins.h>
//宏定义
#define        uchar unsigned char
#define uint unsigned int          
#define TN65436


//进程1,2,3执行时间之比为T1:T2:T3(时间单位us)
#define TN155536          //1个进程循环周期内进程1执行的时间T1usTN1=(65536-T1)
#define TN255536          //1个进程循环周期内进程2执行的时间T2usTN2=(65536-T1)
#define TN355536          //1个进程循环周期内进程3执行的时间T3usTN3=(65536-T1)
//

#define N14                  // 进程1的延时参数
#define N24                  // 进程2的延时参数
#define N34                  // 进程3的延时参数


idata uchar temp _at_ 0x00;       //R0--R7
uchar tempbf1;                  //用于保存R0--R7 进程1
uchar tempbf2;                  //用于保存R0--R7 进程2
uchar tempbf3;                  //用于保存R0--R7 进程3
//定义全局变量
uint address1,address2,address3;
uchar test1_1=0,test2_1=0,test3_1=0,PID=1;
//各进程的标志位,是否为第一次执行,0第一次,非0非第一次;PID进程号;
uint ac1,ac2,ac3; //, PC_Next; 各进程的初始地址寄存器.

       
//test1                  的参数由于进程切换时 没有保存普通变量,
//所以各进程的普通参数需要定义成全局变量.
uint m1,i1,j1,k1;
uchar table1;
//在此加入 用户进程1参数

//test2                  的参数   
int m2,i2,j2,k2;
uchar table2;
//在此加入 用户进程2参数

//test3                  的参数       
int m3,i3,j3,k3;          
uchar table3;
//在此加入 用户进程1参数


//声明
//unsigned int Get_Next_PC(void);//调用子程序,获取PC
void chushihua(void);          //初始化函数
void yonghuchushihua(void);          //用户初始化函数
void test1(void);                  //进程一
void test2(void);
void test3(void);

//main函数
void main(void)
{
// PC_Next=Get_Next_PC();       
chushihua();
ac1=(unsigned int)(test1);       //获取进程1的入口地址
ac2=(unsigned int)(test2);        //获取进程2的入口地址
ac3=(unsigned int)(test3);       //获取进程3的入口地址
yonghuchushihua();
TR0=1;
while(1);
}

//初始化时钟
void chushihua(void)
{       
    TMOD=0x01;                //
        EA=1;                       
        ET0=1;                       
        TH0=TN/256;
        TL0=TN%256;
}

//中断处理,进程调度
void time0() interrupt 1 using 1
{   uchar ib;
        TR0=0;
//进程顺序分配       
        PID++;
        if(PID==4)
           {PID=1;}
//进程调度
        switch(PID)
        {
          case 1:       
       {
                if(test3_1!=0)                //第一次否?
          { //保存现场,还回地址
                   address3=*((unsigned char *)(SP-4));                //PC的高字节
         address3 <<= 8;
         address3+=*((unsigned char *)(SP-5));   //PC的低字节
                   table3=*((unsigned char *)(SP));               //现场保护
                   table3=*((unsigned char *)(SP-1));       //现场保护
                   table3=*((unsigned char *)(SP-2));       //现场保护
                   table3=*((unsigned char *)(SP-3));       //现场保护
                   for(ib=0;ib<=7;ib++)                               //保护R0--R7
                  {
                          tempbf3=temp;
                        }
                }

          if(test1_1==0)       //第一次执行
                { //执行新进程,恢复现场
                test1_1=1;
          *((unsigned char *)(SP-4))=ac1>>8;                       //PC的高字节
      *((unsigned char *)(SP-5))=ac1 & 0x00ff;       //PC的低字节
                }
                else        //非第一次执行
                { //执行新进程,恢复现场
                *((unsigned char *)(SP-4))=address1>>8;                   //PC的高字节
      *((unsigned char *)(SP-5))=address1 & 0x00ff;//PC的低字节
                *((unsigned char *)(SP))=table1;                   //现场恢复
                *((unsigned char *)(SP-1))=table1;           //现场恢复
                *((unsigned char *)(SP-2))=table1;           //现场恢复
                *((unsigned char *)(SP-3))=table1;           //现场恢复
                for(ib=0;ib<=7;ib++)       //恢复R0--R7
                  {
                       temp=tempbf1;
                        }
                }
           TH0=TN1/256;
           TL0=TN1%256;
       TR0=1;
      }break;

          case 2:
          {
          if(test1_1!=0)           //第一次否?
          {//保存现场,还回地址        ,否
                   address1=*((unsigned char *)(SP-4));       //PC的高字节
         address1 <<= 8;
         address1+=*((unsigned char *)(SP-5));//PC的低字节
                   table1=*((unsigned char *)(SP));           //现场保护
                   table1=*((unsigned char *)(SP-1));        //现场保护
                   table1=*((unsigned char *)(SP-2));        //现场保护
                   table1=*((unsigned char *)(SP-3));       //现场保护
                   for(ib=0;ib<=7;ib++)                          //保护R0--R7
                  {
                          tempbf1=temp;
                        }
                }
                if(test2_1==0)                //第一次
                { //执行进程2,恢复现场
                test2_1=1;                                                                       
          *((unsigned char *)(SP-4))=ac2>>8;                       //PC的高字节
      *((unsigned char *)(SP-5))=ac2 & 0x00ff;       //PC的低字节
                }
                else                   //非第一次
                { //执行进程2,恢复现场
                *((unsigned char *)(SP-4))=address2>>8;                //PC的高字节
      *((unsigned char *)(SP-5))=address2 & 0x00ff; //PC的低字节
                *((unsigned char *)(SP))=table2;               //现场恢复
                *((unsigned char *)(SP-1))=table2;       //现场恢复
                *((unsigned char *)(SP-2))=table2;       //现场恢复
                *((unsigned char *)(SP-3))=table2;          //现场恢复
                for(ib=0;ib<=7;ib++)                //恢复R0--R7
                  {
                          temp=tempbf2;
                        }
                }                       
               TH0=TN2/256;
          TL0=TN2%256;
      TR0=1;
          }break;
       
           case 3:
           {
          if(test2_1!=0)
          {//保存现场,还回地址
                   address2=*((unsigned char *)(SP-4));          //PC的高字节
         address2 <<= 8;
         address2+=*((unsigned char *)(SP-5));   //PC的低字节
                   table2=*((unsigned char *)(SP));           //现场保护
                   table2=*((unsigned char *)(SP-1));   //现场保护
                   table2=*((unsigned char *)(SP-2));   //现场保护
                   table2=*((unsigned char *)(SP-3));       //现场保护
             for(ib=0;ib<=7;ib++)                           //保护R0--R7
                  {
                          tempbf2=temp;
                        }
               }
                if(test3_1==0)
                { //执行进程3
                test3_1=1;
          *((unsigned char *)(SP-4))=ac3>>8;                                //PC的高字节
      *((unsigned char *)(SP-5))=ac3 & 0x00ff;           //PC的低字节
                }
                else
                { //执行进程3,恢复现场
                *((unsigned char *)(SP-4))=address3>>8;                  //PC的高字节
      *((unsigned char *)(SP-5))=address3 & 0x00ff;//PC的低字节
                *((unsigned char *)(SP))=table3;                          //现场恢复
                *((unsigned char *)(SP-1))=table3;                  //现场恢复
                *((unsigned char *)(SP-2))=table3;                  //现场恢复
                *((unsigned char *)(SP-3))=table3;               //现场恢复
                for(ib=0;ib<=7;ib++)                                  //恢复R0--R7
                  {
                          temp=tempbf3;
                        }
                }
               TH0=TN3/256;
          TL0=TN3%256;
      TR0=1;
          }break;

          default:
                  TH0=TN/256;
          TL0=TN%256;
      TR0=1;
          break;
   }
}


//以下部分是需要按用户要求添加的部分


void yonghuchushihua(void)          //用户初始化函数
{
//加入用户初始化函数
}



//进程一P1演示二进制加法 死循环       
void test1(void)
{
while(1)   //用户可以删除while(1)循环中的全部内容(演示用) 安排用户进程
{
   
   for(i1=0;i1<256;i1++)
   {
   for(k1=0;k1<=N1;k1++)
   {for(j1=0;j1<=20;j1++)
   for(m1=0;m1<=113;m1++);}           // 约1ms*T1/T1+T2+T3
   P1=i1;
   //P2=0x0;
   }
}
}

//进程二        P2演示二进制加法 死循环       
void test2(void)
{
while(1)   //用户可以删除while(1)循环中的全部内容(演示用) 安排用户进程
{
   
   for(i2=0;i2<256;i2++)
   {
   for(k2=0;k2<=N2;k2++)
   {for(j2=0;j2<=20;j2++)
   for(m2=0;m2<=113;m2++);}
   P2=i2;
   }
}
}

//进程三          P0口演示二进制加法 死循环           进程3的延时参数
void test3(void)
{
while(1)   //用户可以删除while(1)循环中的全部内容(演示用) 安排用户进程
{
   
   for(i3=0;i3<256;i3++)
   {
   for(k3=0;k3<=N3;k3++)
   {for(j3=0;j3<=20;j3++)
   for(m3=0;m3<=113;m3++);}
   P0=i3;
   }
}
}

/* //获取下一条语句地址:PC
unsigned int Get_Next_PC(void)
{
unsigned int address;
address=*((unsigned char *)SP);
address <<= 8;
address+=*((unsigned char *)(SP-1));
return address+4;
}
*/

http://cache.amobbs.com/bbs_upload782111/files_38/ourdev_629777JH31EJ.GIF
(原文件名:未命名22.GIF)

Hello_LeeHom 发表于 2011-4-10 16:31:21

另外我还在希望工程师高手们能再讲得深入些,如何在Easy51RTOS下编写各种芯片的驱动,为谢!!!

我也希望。。
那帖子讲到这里后面就TJ了。。

Hello_LeeHom 发表于 2011-4-10 16:32:31

不过我下的那一份资料没有你这后面的那些代码。。。好好研究一下。。

songguoda 发表于 2011-4-10 16:39:32

回复【2楼】Hello_LeeHom郑飞
-----------------------------------------------------------------------

那些代码是我在原贴中下到的http://www.eehome.cn/
另外原来的基于Easy51RTOS的Mini51板万年历仿真版
在网上下了之后,原码重新编译后不能运行,我去掉了原来仿真电路中的CPLD元件,部份代码已修改完成,经编译完全可以运行

Hello_LeeHom 发表于 2011-4-10 16:55:15

我也去那网站找了原帖,悲剧的是没找到。。

Hello_LeeHom 发表于 2011-4-10 16:57:02

你对于基于OS环境下的编程还有另外的资料没有?共享一下,3Q了。。如在ucos或陈计明的small rtos51下的应用开发。

songguoda 发表于 2011-4-10 16:59:38

回复【5楼】Hello_LeeHom郑飞

-----------------------------------------------------------------------

http://www.ourdev.cn/bbs/bbs_content.jsp?bbs_sn=4428744&bbs_page_no=1&bbs_id=3004

songguoda 发表于 2011-4-10 17:03:58

回复【4楼】Hello_LeeHom郑飞
-----------------------------------------------------------------------
原贴地址
http://www.eehome.cn/search.php?#submit

Hello_LeeHom 发表于 2011-4-10 17:10:16

你复制的链接登不进去,因为那网站要登录了才能进去。。。我登录了再进去,又链接到另外的地方去了。。这是我在那论坛搜索的结果,那里是原创怎么内容还没有另外网站找的多。

http://cache.amobbs.com/bbs_upload782111/files_38/ourdev_629774HTP7JK.png
(原文件名:搜狗截图_2011-04-10_17-09-17.png)

Hello_LeeHom 发表于 2011-4-10 17:11:29

那网站就只搜到这点点内容。。

songguoda 发表于 2011-4-10 17:14:27

回复8楼


这里就是原贴的内容,只是都没有更新了

另外 Hello_LeeHom郑飞
你有什么好的单片机学习资料也上传给大家学习共享一下嘛

wangyj173 发表于 2011-4-10 17:16:30

谢谢LZ

Hello_LeeHom 发表于 2011-4-10 17:22:08

我也只有你给我的那链接的东东,就是任哲的ucos和周慈航的那本基于ucos的程序设计 那两本书。。周慈航那书讲太多原理了,看的我想睡瞌睡。而且也是基于他们公司的开发板讲的。所以,暂时还没兴趣看。。正在找看有另外的资料没有。

ltway 发表于 2011-4-10 18:14:40

真的很不错 谢谢

tangwei039 发表于 2011-4-10 18:25:51

楼主想看看您的补充。。。。。

Chuda 发表于 2011-4-10 18:32:41

先mark后看

Hello_LeeHom 发表于 2011-4-10 18:33:22

楼上的tangwei039大虾 就是那篇帖子的原帖主吧。。我记的我下的那一份资料里面 就有tangwei039@126.com这个邮箱。。
tangwei039老大,就等你更新啊。望穿秋水。万众期待。。。或者给我们点这方面应用的代码研究一下也是对于我们初学者天大的好处和帮助。。。

songguoda 发表于 2011-4-10 18:41:06

回复【14楼】tangwei039
-----------------------------------------------------------------------

补充不在后面吗?有仿真实例,有文档

tangwei039 发表于 2011-4-10 18:50:37

回复【16楼】Hello_LeeHom 郑飞
楼上的tangwei039大虾 就是那篇帖子的原帖主吧。。我记的我下的那一份资料里面 就有tangwei039@126.com这个邮箱。。
tangwei039老大,就等你更新啊。望穿秋水。万众期待。。。或者给我们点这方面应用的代码研究一下也是对于我们初学者天大的好处和帮助。。。

-----------------------------------------------------------------------

这个贴子是很常时间没有更新了,其实正真做项目时,硬件才最重要,多看看别人设计产说品,对自己提高,不要指望有什么技巧,那都是传说!!!从单片机初学者迈向单片机工程师(一份正真的让你成为工程师的贴子)这个贴子说的太夸张了,但我觉的初学者应该认真仔细看看这个贴子,咱们论坛里有好几个开源的项目,比如春风大哥的电源,ourdev雕刻机,示波器、充电器。我都认真看过,有的地方还运用到实际产品。其实我也是个菜鸟

crazyrat 发表于 2011-4-10 18:59:05

大师们多多提些建议给我们初学者~

songguoda 发表于 2011-4-10 19:08:56

回复【18楼】tangwei039
-----------------------------------------------------------------------

谢谢!

tcp1985 发表于 2011-4-10 19:49:49

mark

afei8856 发表于 2011-4-10 20:40:33

mark

lsy5110 发表于 2011-4-10 21:56:16

mark

eworker 发表于 2011-4-10 22:07:44

回复【18楼】tangwei039
-----------------------------------------------------------------------

平均来看,至少从收入上来看,硬件工程师比不过软件工程师。

csg0203 发表于 2011-4-10 22:20:37

谢谢

songguoda 发表于 2011-4-10 23:21:57

本想将匠人手记传上来,可就是网速太慢传不上,有30多兆呀

songguoda 发表于 2011-4-11 20:45:44

顶吧,兄弟们

szy19811114 发表于 2011-4-12 11:24:00

DING

songguoda 发表于 2011-4-22 15:22:48

czcxxh 发表于 2011-5-4 19:33:03

怎么不能下载呢?

eryueniao 发表于 2011-5-4 20:23:38

回复【3楼】songguoda
回复【2楼】hello_leehom郑飞
-----------------------------------------------------------------------
那些代码是我在原贴中下到的http://www.eehome.cn/
另外原来的基于easy51rtos的mini51板万年历仿真版
在网上下了之后,原码重新编译后不能运行,我去掉了原来仿真电路中的cpld元件,部份代码已修改完成,经编译完全可以运行
-----------------------------------------------------------------------

hehe   cool

fuwu 发表于 2011-5-4 20:42:37

mark

stely 发表于 2011-5-10 00:08:11

MARK

songguoda 发表于 2011-5-15 15:36:02

好久没有来更新了,最近一直很忙,来些状态机的资料吧,加上前面的资料相信初学者看了后会有很大的收获的!

一、
基于状态机控制的面向对象的前后台协从多任务系统设计
一、任务分析
    根据题目要求,划分任务如下:
    1、键盘扫描线程
    2、灯显示线程
    3、LED1-LED4四个独立线程
    4、后台监视线程
    5、串口收发中断
    共计7个线程1个中断。

二、软件整体结构设计

               后台                         前台                     串口中断

---------|             ---------------               --------------
|      V             | int10ms中断 |               | serial中断 |
|-------------       ---------------               --------------
||监视monitor|             |                               |
|-------------      -----------------                --------------
|      |            |键盘扫描keyscan|                | 收 RI 检查 |
---------|            -----------------                --------------
                               |                               |
                        -----------------                --------------
                        | 灯显示displed |                | 发 TI 检查 |
                        -----------------                --------------
                               |                               |
                  --------------------------         --------------
                  | LED1-LED4四个线程lednp |         |    RETI    |
                  --------------------------         --------------
                               |
                        -----------------
                        |   END       |
                        -----------------

                     图1 软件整体结构设计图

    由图1可知,本软件是基于状态机控制的前后台协从多任务系统,其基本原理是通过均衡地分割CPU时间片达到并发多任务的目的。前台任务要求极精确定时,包括键盘扫描、灯显示、LED1-LED4控制,使用10ms定时中断作为步调,可保证时间误差不超过11ms。后台运行人机监控界面,实时性要求不高,只要满足人的生理要求(响应延迟不超过0.5秒)即可,因此将其放在后台,使用剩余时间片。串口收发属于随机事件,发生频率不高,但实时性要求严格,所以用中断实现最为妥帖。

三、功能描述
    1、人机监控界面monitor
      (1)help显示在线帮助,说明各种命令的使用方法;
      (2)mb、mw、md以字节、字、长字格式向内存数据区写入数据/读出数据;
      如:mX 地址 个数 数据
            mb 10 2 ----- 从地址10开始读出2个字节数据并显示
            mb 20 3 0xaa ----- 从地址20开始连续写入3个字节,值均为0xaa
      (3)ls显示各线程状态号;
      如:
            ls monitor ----- 显示监控状态机的状态变迁
            ls led1p ----- 显示LED1控制状态机的状态变迁
            注:此命令只显示变化的状态号,不变化不显示,由此可以动态观察运行情况。例如:显示(0)->(1)->(4)...按CTRL+B键退出监视并显示提示符“%”;或者按CTRL+G切换到另一控制台继续输入命令/显示。
      (4)bye挂起后台监控,以便节省能源,同时按下key1+key4+key8再次激活(只能在某些支持电源管理的单片机上实现);
      (5)其他命令:退格、CTRL+C(重启)、CTRL+G(切换控制台,支持2个显存);

    2、键盘扫描keyscan
      因为不需要软件去抖动,所以很简单。令key=P1即可。假定按下键盘为0。变量key保存采样的键值。
      加入软件去抖动也很方便,只要把去抖动状态机层串接在keyscan前即可。这种模块化设计可以平滑增加新功能,不影响其他部分,而且可以实现相当复杂的处理算法。
      keyscan每隔10ms采样一次键盘,相对于人手几百毫秒的机械运动就是实时的。

    3、灯显示displed
      因为是直接驱动,所以直接令 P3=led 即可。假定0为亮、1为灭。变量led保存处理后的灯显示值。

    4、串口收发中断
      关于串口收发中断和显示API函数及缓冲区队列处理不再叙述,详见相关源代码。

    5、四个LED控制
      详见LED控制状态机一节的说明。

四、具体设计思路和采用技术
    分析题目的功能需求,核心对象是LED,动作有四种:快闪灭、慢闪灭、快短亮长灭、慢长亮短灭,这样我们似乎要设计4种状态机进行控制,再进一步分析,其实本质只有闪灭和亮灭的区别,其他只不过是时间参数不同,这样我们只要设计两种状态机对应就可以了。再深入想一下,如果以后增加新的动作怎么办?或者这几个灯的动作需要互换位置如何才能不改动程序而灵活实现呢?用户的需求千奇百怪,怎么才能在不改程序的前提下满足未来用户不断增长和变化的需求呢?“数据驱动”技术可以解决这个问题。
    ============
    * 数据驱动 *
    ============
    数据是灵活的,程序是僵死的,用数据驱动程序流向,既灵活又稳定。(前提是严密的数据合法性检查)程序和数据在计算机里是分家的,程序位于ROM或只读RAM里,不可写,数据位于RAM里,可读写。如果把程序的流程用数据控制,那么不同的数据组合将产生千变万化的程序行为。我们可以把程序的所有功能写在同一个程序里,然后用数据配置定义个性化的程序行为。单一版本的软件为维护和管理带来了方便。
    针对本题目,我们可以给每个灯设置一个配置项smsel,如下:
    if(smsel==1) 闪灭状态机处理;
    else if(smsel==2) 亮灭状态机处理;
    else //可以新增其他动作的状态机处理;
    由上可知,所有灯的处理程序都是由以上同一个程序段处理,差别只在于各个灯的配置数据不同。LED1和LED2的私有smsel均为1,LED3和LED4的私有smsel均为0,如果有一天客户提出改变LED1为亮灭动作,只要改它的配置数据即可,根本不用动程序。如果要增加LED5、LED6等新的处理,仍然只需调用这段程序控制动作,不必增加代码,加几个smsel私有配置数据就可以了。总之,增加新灯和改变动作,只动数据,不改程序。
   
    --------------------         -------------------
    |状态号state| ^          | LED1数据结构 | ^
    ----------------- |          ---------------- |
    |状态机选择smsel| |          | LED2数据结构 | |
    ----------------- |          ---------------- |
    |   亮时间on    | |          | LED3数据结构 | 数据区(属性)
    ----------------- |          ---------------- |
    |   灭时间off   | 私有数据   | LED4数据结构 | |
    ----------------- |          ---------------- |
    |闪亮时间fon| |          |。。。LEDn| V
    ----------------- |          -------------------
    |闪灭时间foff | |          |闪灭状态机| |
    ----------------- |          |亮闪状态机| 行为动作(方法)
    |当前时间计数cnt| V          |其他状态机| |
    --------------------         -------------------
    |   键值keyval| 公有数据   图3 LED对象在内存里的映像
    --------------------
   图2 LED数据结构

    如图2、3所示,LED可以用面向对象(OOP)的方法分析。每个灯有自己的属性和方法,映射到内存中就是类的实例化。比如:每个灯有自己的私有数据,当前状态号、状态机选择、亮时间、灭时间、闪亮灭时间,计数,这些数据唯一确定了此灯与众不同的个性,是每个灯特有的。一个灯受A、B两个键的控制,这个键值对外可见,是公有数据。
    如图3,所有灯对象使用同一个类方法,每个对象有自己独立的数据区,即:对象只有属性不同,调用的方法程序是同一个,这也可以说是一种数据驱动吧。其实,C++程序在内存里的映像就是图3。
    class LED{
      private:
      unsigned char state;
      unsigned char smsel;
      unsigned int on,off,fon,foff,cnt;
      public:
      unsigned char keyval;
      
      void smstate();//闪灭状态机
      void lmstate();//亮灭状态机
    }
    LED led1,led2,led3,led4;
    以上是对应图3的C++伪代码。

songguoda 发表于 2011-5-15 15:37:40

五、LED控制状态机
                         11
                     --->---
                     |   |
                      ---------
    ----------------->| 0空闲 |    10
    |             <--------------------->
    |      00/01|                     |
    |       -----------   00/01   ----------------
    |       | 2闭态灭 |<----------| 1延时fon秒亮 |<----
    |       -----------<-------   ----------------    |
    |             |         |         |             |10/11
    |      ----------------   |   -----------------   |
    |      | 4延时off秒灭 |   ----| 3延时foff秒灭 |----
    |      ----------------00/01-----------------
    |             |
    ---------------
       无条件
               图4 闪灭状态机变迁图

                           11
                         --->---
                         |   |
         无条件       ---------   无条件
      --------------->| 0空闲 |<--------------
      |         <--------------->          |
      |    00/01|               |10      |
      |-------------------------------   |
      ---| 2延时off秒灭 || 1延时on秒亮 |----
         -------------------------------
               图5 亮灭状态机变迁图

    图4、5是LED控制状态机的变迁图,第一个数字是状态号,紧接着是状态描述,条件是AB键状态。如下:
    B键A键描述
    0    0    A、B键同时按下,B键优先级高,忽略A键
    0    1    B键按下,关闭灯
    1    0    A键按下,闪/亮灯,优先级低于关闭,随时可被B键中断(10ms采样)
    1    1    没有键按下,维持原状态
    图4的状态2和4不判断按键情况,其他状态每一步都先检测按键输入。
    图5的状态1和2不判断按键情况,0状态每一步都先检测按键输入。
   
    现在根据题目要求配置数据,就是初始化每个灯的属性。
    LED1-----fon=0.5;foff=0.5;off=5;smsel=1
    LED2-----fon=0.3;foff=0.3;off=1;smsel=1
    LED3-----on=3;off=4;smsel=2
    LED4-----on=10;off=6;smsel=2
   
    -----------------------------------
    |state=0smsel=1on=0off=5|<-------LED1数据区
    |fon=0.5foff=0.5cnt=?       |
    -----------------------------------
    |state=0smsel=1on=0off=1|<-------LED2数据区
    |fon=0.3foff=0.3cnt=?       |
    -----------------------------------
    |state=0smsel=2on=3off=4|<-------LED3数据区
    |fon=0foff=0cnt=?         |
    -----------------------------------
    |state=0smsel=2on=10off=6 |<-------LED4数据区
    |fon=0.5foff=0.5cnt=?       |
    -----------------------------------
    | 闪灭状态机(参见图4状态变迁图)   |<-------公用的行为方法
    | 亮闪状态机(参见图5状态变迁图)   |
    -----------------------------------
      图6 LED对象实例化内存映像

    所有未用成员变量均缺省初始化为0,状态号初始一律为0,cnt是临时变量,保存当前时间,不必初始化。
    由图6可知,所有灯的控制都是同一段程序,内存中只有一个副本,每个灯的特质体现在其独立拥有的数据区(属性)。尽管每个灯的行为各异,时间参数不同,但它们都属于灯类。
    如果想改变LED1的行为,非常简单,把smsel改成2即可。想改变闪烁频率和亮灭比,只要改fon和foff。软件上如果需要引入更多的灯控,只要改一下数据区结构体数组的下标,根本不用动程序,面向对象和数据驱动的设计方法保证了后期维护升级的便利和可靠。
    下面结合实例说明一下工作过程:
    ======================
    ======================
    * 状态实例分析 *重点 *
    ======================
    ======================
    1、先看看LED1,其状态变迁如图4所示。
      初始状态号state=0,位于空闲状态,此时若无按键(11),则继续检查按键事件(10ms采样),下一状态仍为自己0。
      若按下A键10,状态变迁到S1(亮灯并延时fon秒),LED1的fon=0.5,此态点亮灯LED1并延时0.5秒,同时随时检查B键是否按下,如果B键按下01或者A、B键同时按下00,则立即转到2状态关灯。否则,延时0.5秒后转3态。在3态关闭灯并延时foff秒(foff=0.5秒),同时随时检查B键是否按下,如果00/01则立即转2态。否则延时满0.5秒后回到1态,周而复始灯就按指定亮灭参数闪烁起来。
      一旦进入2状态,立即关闭灯,下一步进入4态延时off秒(off=5秒),因为题目要求“off 状态>=5秒”,所以此状态内不进行按键检测,以保证至少关灯5秒。延时满5秒后回到0空闲态检测按键情况,如无按键或又按下B键则继续关灯(0态缺省保持关灯状态,所以此时保证>5秒闭灯)。如果按下A键则进入闪烁状态(此时保证了=5秒闭灯)。
    2、再看看LED3,其状态变迁如图5所示。
      初始状态号state=0,位于空闲状态,此时若无按键(11),则继续检查按键事件(10ms采样),下一状态仍为自己0。
      在0态,若按下A键10,状态变迁到S1,亮灯并延时on秒(on=3秒),因为题目要求“on 状态>=3秒”,所以此状态内不进行按键检测,以保证至少亮灯3秒。延时满3秒后回到0空闲态检测按键情况,如无按键或又按下A键则继续亮灯(0态缺省保持亮灯状态,所以此时保证>3秒亮灯)。如果按下B键则进入闭灯状态(此时保证了=3秒亮灯)。
      在0态,若按下B键01或同时按下A、B键00,状态变迁到S2,亮灯并延时off秒(off=4秒),因为题目要求“off 状态>=4秒”,所以此状态内不进行按键检测,以保证至少灭灯4秒。延时满4秒后回到0空闲态检测按键情况,如无按键或又按下B键/AB键则继续灭灯(0态缺省保持灭灯状态,所以此时保证>4秒灭灯)。如果按下A键则进入亮灯状态(此时保证了=4秒灭灯)。
    可以保存每次采样到的键值,新值覆盖旧值,这样程序就能记住最后一次的按键值,以便状态变迁后处理。
    ========
    如何调试
    ========
    连接PC机和监控串口,输入以下命令:
    %ls led1p<回车>
    (0)->
    此时monitor后台交互界面显示LED1在0状态。按下A键,显示变成
    (0)->(1)->
    表明LED1进入1状态,此时灯亮,过了0.5秒后显示
    (0)->(1)->(3)->
    表明LED1进入3状态,此时灯灭,再过0.5秒后显示
    (0)->(1)->(3)->(1)->
    灯又亮了,就这样每隔0.5秒在控制台上显示一个状态并改变一次灯的状态。过段时间,控制台可能显示
    (0)->(1)->(3)->(1)->(3)->(1)->(3)->(1)->
    此时按下B键/同时按下AB键,控制台立即显示
    (0)->(1)->(3)->(1)->(3)->(1)->(3)->(1)->(2)->(4)->
    表明LED1顺序进入2状态和4状态,灯马上灭了,延时5秒后,控制台显示
    (0)->(1)->(3)->(1)->(3)->(1)->(3)->(1)->(2)->(4)->(0)->
    此时进入0状态,灯依然是灭的,继续等待按键。
    在PC键盘按CTRL+B退出状态显示,出现提示符
    %
    或者按CTRL+G切换到控制台2继续输入命令。
    在提示符处输入
    %mb 0 16<回车>
    00000000 00 01 00 00 01 F4 00 32 --- 00 32 00 10 00 00 01 00 ................
    %
    其中显示的内容为LED数据区,00状态号、01状态机选择、00 00亮时间、01 F4灭时间(5秒=500个10ms=0x1F4)、00 32闪亮时间(0.5秒=50个10ms=0x32)、00 32闪灭时间(0.5秒)、00 10当前时间计数(此时为16个10ms,即0.16秒)、00键值(没有键按下)。
    后面的数据是LED2的,00状态号、01状态机选择、00亮时间高8位。
    可见运行情况可以通过查看内存数据区或者打印状态号获得。

六、总结
    基于状态机的协从多任务就是把一个大任务分成若干小片,每一步(此处为10ms)顺序执行所有任务的一个状态(节约时间,增加实时性),这样CPU资源被各个任务瓜分,从微观上看是顺序执行,从宏观上看每个任务都好象独占一个CPU,任务是并发的。其实CPU本身就是数字系统,不连续而是离散运行的,完全可以认为分配了时间片的任务单独拥有一个慢些的CPU,用此观点看这个程序更容易理解其工作原理。
    这中结构的程序可以看作多任务,虽然没有OS,没有任务调度,但状态机把任务调度过程固化在结构里了。此时没有切换消耗,所以调度过程极为迅速,只是设计者比较累。有网友说,“在程序员心中,每个程序员都是一个OS”,大概就是这个意思。
    状态机在汇编和C中均可实现。在汇编里用散转方法,注意参数合法性检查;在C里用switch-case方法。
    由于中断比较关键,可以单独设计一个软定时进程,在中断里仅处理时间,其他任务挪到主循环里完成,以避免中断响应延迟。
    本结构的程序还可以灵活增加新功能,比如键盘去抖动模块、同时按下双键的特殊处理模块等。本结构的延时delay程序不会浪费CPU资源。

songguoda 发表于 2011-5-15 15:38:20

二、
一种裸奔多任务模型
stateMachine + timerTick + queue。

在RTOS环境下的多任务模型:
任务通常阻塞在一个OS调用上(比如从消息队列取数据)。
外部如果想让该任务运转,就要向消息队列发送消息。
任务收到消息时,根据当前状态,决定如何处理消息。这就是状态机。
任务将消息队列中的消息处理完毕后,重新进入阻塞状态。
任务在处理中,有时要延时一段时间,然后才继续工作:
    为了充分使用CPU,可以通过OS调用让其它任务去工作。
    OS通常会提供一个taskDelay调用。
    当任务调用taskDelay时,即进入阻塞状态,直到超时,才重新进入可工作状态(就绪状态)。

下面说说裸奔环境下的多任务模型:
裸奔也可以多任务,但调度是由用户自主控制。
在RTOS环境下,一般提供抢占式调度。在裸奔时,一般是任务在处理告一段落后,主动结束处理。
RTOS环境下的任务,一般处于一个while(1)循环中。
    while(1){
      从消息队列接收消息。如果没有,将阻塞。
      处理消息。
      }
裸奔下的任务,一般采用查询方式:

    查询是否有待处理的事件。
    如果没有,返回。
    如果有,根据任务的当前状态,进行处理。处理完毕后,可能返回,也可能将待处理事件全部处理完毕后再返回。

裸奔任务其实也处于一个while(1)循环中,只不过这个循环在任务外部。
main()
{
    A_taskInit();      //任务的初始化
    B_taskInit();
    ...
    while(1){
      A_taskProc();    //任务的处理
      B_taskProc();
      }


状态机既适用于OS环境,也适用于裸奔环境。
但在裸奔环境下,状态可能被切分得更细。例如后面讲的如何在裸奔环境实现taskDelay()。

消息队列既适用于OS环境,也适用于裸奔环境。
在OS环境下,消息队列机制由OS提供。
在裸奔环境下,消息队列要自己来实现。如果对队列的概念不清楚,可参考《数据结构》教材。
这个队列机制,可做成通用模块,在不同的程序中复用。
消息队列用于缓冲事件。事件不知道什么时候会到来,也不能保证来了就能迅速得到处理。
    使用消息队列,可以保证每个事件都被处理到,以及处理顺序。
一般在两种情况下会用到消息队列:
    存储外部事件:外部事件由中断收集,然后存储到队列。
      串口接收程序中的接收循环缓冲区,可理解为消息队列。
    任务间通讯:一个任务给其它任务发送消息。

timerTick,就是系统中的时钟基准。OS中总是有一个这样的基准。
在裸奔时,我们要用一个定时器(或RTC或watchdog)来建立这个时间基准。
一个tick间隔可以设置为10ms(典型RTOS的缺省设置)。让定时器10ms中断一次,中断发生时给tickNum++。
    以前,我在定时器中断中设置1S标志、200ms标志等等。时间相关的任务根据这些标志判断是否要执行。
    近来,一般让任务直接去察看tickNum。两次相减来判断定时是否到达。
    也可以在系统中建立一个通用定时器任务,管理与不同任务相关的多个定时器;在定时到达时,由定时器任务去调用相应的callback。
系统时钟基准是所谓“零耗时裸奔”的基础。
timerTick的分辨率,决定了只适于于较大的时间延时。
    在做时序时的小延时,用传统方法好了。

OS中的taskDelay()在裸奔环境下的一种实现:
OS环境:
void xxxTask(void)
{

    while(1){
      //waitEvent

      //do step_1

      taskDelay(TIME_OUT_TICK_NUM);

      //do step_2
    }

裸奔环境:
void xxxTask(void)
{
    static unsigned int taskStat = STAT_GENERAL;    //任务状态变量
    static timer_t startTick;
    timer_t currTick;
   
    if (taskStat == STAT_GENERAL)
    {
      //check event

      //if no event
            return;

      //do step_1

      startTick = sysGetTick();    //sysGetTick()就是察看系统时间
      taskStat = STAT_WAIT;
      return;
    }
    else if (taskStat == STAT_WAIT)
    {
      currTick = sysGetTick();    //sysGetTick()就是察看系统时间
      if ((currTick - startTick) >= TIME_OUT_TICK_NUM)
      {
            //do step_2   

            taskStat = STAT_GENERAL;
            return;
      }
      else
            return;
    }

songguoda 发表于 2011-5-15 15:39:24

三、
状态机的两种写法
有限状态机FSM思想广泛应用于硬件控制电路设计,也是软件上常用的一种处理方法(软件上称为FMM--有限消息机)。它把复杂的控制逻辑分解成有限个稳定状态,在每个状态上判断事件,变连续处理为离散数字处理,符合计算机的工作特点。同时,因为有限状态机具有有限个状态,所以可以在实际的工程上实现。但这并不意味着其只能进行有限次的处理,相反,有限状态机是闭环系统,有限无穷,可以用有限的状态,处理无穷的事务。
    有限状态机的工作原理如图1所示,发生事件(event)后,根据当前状态(cur_state),决定执行的动作(action),并设置下一个状态号(nxt_state)。

                         -------------
                         |         |-------->执行动作action
   发生事件event ----->| cur_state |
                         |         |-------->设置下一状态号nxt_state
                         -------------
                            当前状态
                      图1 有限状态机工作原理


                               e0/a0
                              --->--
                              |    |
                   -------->----------
             e0/a0 |      |   S0   |-----
                   |    -<------------    | e1/a1
                   |    | e2/a2         V
               ----------         ----------
               |   S2   |-----<-----|   S1   |
               ----------   e2/a2   ----------
                     图2 一个有限状态机实例

            --------------------------------------------
            当前状态   s0      s1      s2   | 事件
            --------------------------------------------
                     a0/s0      --       a0/s0   |e0
            --------------------------------------------
                     a1/s1      --      --   |e1
            --------------------------------------------
                     a2/s2   a2/s2      --   |e2
            --------------------------------------------

               表1 图2状态机实例的二维表格表示(动作/下一状态)

    图2为一个状态机实例的状态转移图,它的含义是:
      在s0状态,如果发生e0事件,那么就执行a0动作,并保持状态不变;
               如果发生e1事件,那么就执行a1动作,并将状态转移到s1态;
               如果发生e2事件,那么就执行a2动作,并将状态转移到s2态;
      在s1状态,如果发生e2事件,那么就执行a2动作,并将状态转移到s2态;
      在s2状态,如果发生e0事件,那么就执行a0动作,并将状态转移到s0态;
    有限状态机不仅能够用状态转移图表示,还可以用二维的表格代表。一般将当前状态号写在横行上,将事件写在纵列上,如表1所示。其中“--”表示空(不执行动作,也不进行状态转移),“an/sn”表示执行动作an,同时将下一状态设置为sn。表1和图2表示的含义是完全相同的。
    观察表1可知,状态机可以用两种方法实现:竖着写(在状态中判断事件)和横着写(在事件中判断状态)。这两种实现在本质上是完全等效的,但在实际操作中,效果却截然不同。

==================================
竖着写(在状态中判断事件)C代码片段
==================================
    cur_state = nxt_state;
    switch(cur_state){                  //在当前状态中判断事件
      case s0:                        //在s0状态
            if(e0_event){               //如果发生e0事件,那么就执行a0动作,并保持状态不变;
                执行a0动作;
                //nxt_state = s0;       //因为状态号是自身,所以可以删除此句,以提高运行速度。
            }
            else if(e1_event){          //如果发生e1事件,那么就执行a1动作,并将状态转移到s1态;
                执行a1动作;
                nxt_state = s1;
            }
            else if(e2_event){          //如果发生e2事件,那么就执行a2动作,并将状态转移到s2态;
                执行a2动作;
                nxt_state = s2;
            }
            break;
      case s1:                        //在s1状态
            if(e2_event){               //如果发生e2事件,那么就执行a2动作,并将状态转移到s2态;
                执行a2动作;
                nxt_state = s2;
            }
            break;
      case s2:                        //在s2状态
            if(e0_event){               //如果发生e0事件,那么就执行a0动作,并将状态转移到s0态;
                执行a0动作;
                nxt_state = s0;
            }
    }

==================================
横着写(在事件中判断状态)C代码片段
==================================
//e0事件发生时,执行的函数
void e0_event_function(int * nxt_state)
{
    int cur_state;
   
    cur_state = *nxt_state;
    switch(cur_state){
      case s0:                        //观察表1,在e0事件发生时,s1处为空
      case s2:
            执行a0动作;
            *nxt_state = s0;
    }
}

//e1事件发生时,执行的函数
void e1_event_function(int * nxt_state)
{
    int cur_state;
   
    cur_state = *nxt_state;
    switch(cur_state){
      case s0:                        //观察表1,在e1事件发生时,s1和s2处为空
            执行a1动作;
            *nxt_state = s1;
    }
}

//e2事件发生时,执行的函数
void e2_event_function(int * nxt_state)
{
    int cur_state;
   
    cur_state = *nxt_state;
    switch(cur_state){
      case s0:                        //观察表1,在e2事件发生时,s2处为空
      case s1:
            执行a2动作;
            *nxt_state = s2;
    }
}

    上面横竖两种写法的代码片段,实现的功能完全相同,但是,横着写的效果明显好于竖着写的效果。理由如下:
    1、竖着写隐含了优先级排序(其实各个事件是同优先级的),排在前面的事件判断将毫无疑问地优先于排在后面的事件判断。这种if/else if写法上的限制将破坏事件间原有的关系。而横着写不存在此问题。
    2、由于处在每个状态时的事件数目不一致,而且事件发生的时间是随机的,无法预先确定,导致竖着写沦落为顺序查询方式,结构上的缺陷使得大量时间被浪费。对于横着写,在某个时间点,状态是唯一确定的,在事件里查找状态只要使用switch语句,就能一步定位到相应的状态,延迟时间可以预先准确估算。而且在事件发生时,调用事件函数,在函数里查找唯一确定的状态,并根据其执行动作和状态转移的思路清晰简洁,效率高,富有美感。
    总之,我个人认为,在软件里写状态机,使用横着写的方法比较妥帖。
   
    竖着写的方法也不是完全不能使用,在一些小项目里,逻辑不太复杂,功能精简,同时为了节约内存耗费,竖着写的方法也不失为一种合适的选择。
    在FPGA类硬件设计中,以状态为中心实现控制电路状态机(竖着写)似乎是唯一的选择,因为硬件不太可能靠事件驱动(横着写)。不过,在FPGA里有一个全局时钟,在每次上升沿时进行状态切换,使得竖着写的效率并不低。虽然在硬件里竖着写也要使用IF/ELSIF这类查询语句(用VHDL开发),但他们映射到硬件上是组合逻辑,查询只会引起门级延迟(ns量级),而且硬件是真正并行工作的,这样竖着写在硬件里就没有负面影响。因此,在硬件设计里,使用竖着写的方式成为必然的选择。这也是为什么很多搞硬件的工程师在设计软件状态机时下意识地只使用竖着写方式的原因,盖思维定势使然也。

    TCP和PPP框架协议里都使用了有限状态机,这类软件状态机最好使用横着写的方式实现。以某TCP协议为例,见图3,有三种类型的事件:上层下达的命令事件;下层到达的标志和数据的收包事件;超时定时器超时事件。
   
                  上层命令(open,close)事件
            -----------------------------------
                  --------------------
                  |       TCP      |<----------超时事件timeout
                  --------------------
            -----------------------------------
               RST/SYN/FIN/ACK/DATA等收包事件
                  
            &nb

songguoda 发表于 2011-5-15 15:42:27

基于状态机思想的程序设计(汇总) word版
基于状态机思想的程序设计(汇总)ourdev_639969LPMJAT.doc(文件大小:128K) (原文件名:基于状态机思想的程序设计(汇总).doc)

wpnx 发表于 2011-5-15 15:52:37

mark

zzyzbtx2009 发表于 2011-5-15 17:07:50

几个记号,呵呵谢谢

ansai0089 发表于 2011-5-15 17:09:35

不错谢谢

sunxm 发表于 2011-5-15 17:19:22

顶。好贴

davidd 发表于 2011-5-15 18:01:35

mark

yanzhang12 发表于 2011-5-15 22:09:03

mark

dzq112358 发表于 2011-5-15 23:31:39

这个收藏了

441826060 发表于 2011-5-16 00:35:35

这个收藏了

gongjian2100 发表于 2011-5-16 17:44:12

好贴 收藏

songguoda 发表于 2011-5-20 00:02:34

周立功写给学单片机的年轻人
作为过来人思前想后,我感到完全有责任将发自心底的感受传递给年轻一代,“一个企业家心灵深处渴望优秀人才的卓越追求和深层次的叹息、痛苦和感受”。您们千万不要等到毕业求职时才觉得自己能力太差,世界上从来就没有后悔药。当然,如果您现在看了我写的这篇文章可能还不算晚,因为您还有机会在以后的岁月里奋起直追——“亡羊补牢,尤未为晚”。对于现在刚进入大学的学生,您应该更加珍惜这美好的求学机会,因为眨眼之间几年就过去了,您很快就会感到来自全社会生存竞争的压力,您面临的对手再也不仅仅是您身边的同学,今天您在班上的成绩的确是前几名,但一走到社会上去才感到是多么地脆弱而又多么地不堪一击。
  在面试大多数本科生时,我仅仅是询问了一些有关MCS-51 系列单片机的基本原理,但却几乎很少有人能够完全答对,简直是五花八门。很多作为一个即将毕业的自动化专业本科生,至今还不知道单片机是这个专业的核心基础,难道不可悲吗?您的水平不高我完全心中有底,其实我只要求这些学生能够掌握单片机应用开发的基本技能,用汇编和C51 写过一些基本的程序,真正动手做过一些简单的项目,然后将自己做过的项目写成比较规范的文档。我想,这种形式的“自我介绍”肯定要比让别人看您那写的千遍一律的“八股文”简历不知要强多少倍,古人言:“一叶知秋”其实讲的就是这个道理。平心而论只有具备这样基础的学生才配得上企业花钱对您进行二次“开发” 事实上,很多学生根本就不管老师平时是多么地劝导都听不进去,我认为您只要平时善于做一个有心人,主动一些多找老师请教,然后从大三开始帮老师打打下手干一些活。还有一个途径就是自己花钱购买一些学习开发实验板,加强动手能力的训练。但也有很多学生说没有钱,可事实上并非如此,现在的学生购买手机成风,我不知道手机对您们现在来说到底有多大的用途?事实上,现在的学生家庭条件普遍都不太好,可这些孩子们的攀比心里却十分地严重和可怕。到今天即将毕业之际才感到找工作太难。父母一个子儿地攒下一些辛苦钱给您购买电脑容易吗?“望子成龙――可怜天下父母心”!他们是希望您能够学到一些真本事,而您可能很多时间都在玩游戏,上网聊天,实际上对于您来说仅仅是举手之劳,只要花几百元买一块实验板,辛苦一个暑假的时间强化实践,可能您就会与众不同。与此同时,可能您的信心大增,前途一片光明。俗话说得好:仓库有粮,心中不慌!如果您平时没有准备,那么临时匆匆忙忙地上阵面试肯定是要败下来的。从2003 年开始是全国第一次扩招之后毕业人数最多的第一年,以后的毕业生还会更多,同时还有更多的研究生与本科生抢饭碗,面对就业的困境压力不能说不大,痛苦在其中。还有就是我每天都要面临那些看不完的自我介绍简历,很可惜几乎都是清一色的“八股文”令人讨厌。其实只要您的成绩不是太差,分数不是应聘成功的关键,重要的是做人要踏实,不要有水分,实际上,只要将您平时做过的小制作写成一篇心得,再带上您的作品,“事实胜于雄辩”这样更能打动人。还有很多经历了四年本科又三年研究生阶段学习行将毕业的硕士生,不仅不知道嵌入式操作系统是什么东西,而且连C++都不能熟练掌握,驱动程序开发的能力就更不容提了,仅仅做了一个单片机的应用设计就拿到硕士文凭了,术业缺乏专攻泛泛而谈,说句实在话,怎么样也教人无法接受啊!对于我来说,如果您没有特别的才能,我宁愿用三年时间培养一个好的本科生给更高的待遇,他肯定不比一般的硕士生差。所以,考上了研究生之后,您要时刻明白加强动手能力的培养和前沿科学技术的学习至关重要的,这是您将来面试的“杀手涧”,因为您毕业之后要求的工资待遇起码是一般本科生的两倍,但是这个钱不是那么好拿的。顶多3-5 年的时间,大家肯定能够看到一个现实,那就是如果您还不能熟练地掌握嵌入式操作系应用开发技术的话,您只能拿2000 元的月薪。即便您是研究生或是博士,那又有什么稀奇的呢?邓伯伯有句名言,不管白猫还是黑猫,抓住老鼠就是好猫!我们知道企业家是要赚钱的,否则这个企业就留不住人才,无情的市场竞争机制将会毫不犹豫地将这个老板淘汰出局。一个可持续发展的企业,如果离开了富有聪明才智的优秀人才,那是不可想象的。光有几个还不行,而是要有一个卓越的军团。
  其实就业的问题,对于一个努力的人来说是何等地简单,我面试了300 多本科生,我只对其中几个成绩中等的本科生感兴趣,他们主要是参加过2001 年全国电子大赛,有的是平时业余时间,暑假及其寒假都在跟老师干活的学生,与他们聊天真是一种莫大的享受和欣慰,对于我来说真的是如获至宝。于是,我就立即劝他们,您的成绩中等确实难以考上好的学校和导师还不如不考研究生以免浪费时间,同时也充满自信告诉他们,“我就是伯乐!我愿意给您机会和花钱培养您”。事实上,即便您考上研究生,如果没有遇上具有超前眼光和经费充足的导师,您读了也白读,更何况现在的研究生扩招的这么多?一个导师带那么多研究生,有那么多课题吗?即便有的话,他应付得过来吗?可能残酷的现实将会让您看到,毕业之后回头一看还远远不如当年同您一道毕业的同班同学。确实也是,人家的机会比您好得多,关键是人家对待机会比您把握得好,可以说:恰到火候。难道非要读研究生理论水平才高吗?这是何等地荒谬啊!我们知道电子与计算机技术的发展日新月异。大学的更新速度一定就要快吗?回答是否定的,比如说推广PHILIPS 的最新单片机和USB 技术吧!他们的更新速度肯定比一个可持续发展的企业要慢得多,难道说我们的人才水平不够高吗?我们公司就有一批这样在各个领域里出类拔萃的“年轻专家”从 2003 年开始,我们将会有一系列的专著在北京航空航天大学出版社出版,大多数的著作都来自于我们长期的基础研究和应用开发,可以毫不夸张地说这些都是我们这个年轻团队合作的结晶。他们是优秀和卓越的人才,他们没有就业的压力,他们不怕炒鱿鱼,相反我倒时刻要想尽办法加强公司的管理,从工资,奖金,福利,生活和事业成就感等各个方面绞尽脑际善待人才和留住人才,这么努力的人才根本就不需要过多地为工作和生活发愁,对于这个问题当然是仁者见仁智者见智,也就是下面我要讲到的问题,一个人如何根据自己的特点、条件和机遇对自己如何定位的问题,也是至关重要的。
  比如说,如果您对单片机的理解仅仅是入门水平的话,那么您首先就应该低调一些,因为您主要的任务是为了寻找一个可以学习的机会,说句实在话,此刻此刻您一定要清醒地意识到:寻找伯乐比眼前的利益更加重要!因为您现在确实还没有可以骄傲的资本,您一定要想尽办法取得第一次正式踏入社会的入场卷,这对您来说是何等地重要啊!俗话说得好,“万丈高楼从地起”您一定要寻找机会通过业绩表现出您的能力,您确实与众不同之处。这样您就可能在一个公司里树立您的“信用”,有了信用之后随之而来的机会将会越来越多,到那时您根本就不用不愁自己的待遇和地位问题了。所以对于一个刚刚步入社会的年轻人来说,您能够做到以“诚信” 打天下,您将来的前途肯定是无限光明。千万不要感到自己在班上的成绩是前几名或者相对来说自己的动手能力相比之下比同学强多少、多少,或者自己在读书期间贷了多少款,或者家里是多么地缺钱,因为当您加入整个社会的竞争之后,这些都不是您要求高薪的理由,这个时候您的心态将可能会决定您的命运,因此对自己要有充分的认识和正确的估价,经常是很多学生用这些理由向我开出高价,我无话可说只好“摇头和叹息”。
  如果您的水平还不够,我认为也不要自卑,要知道机会是时刻属于有准备的年轻人,但也不可掉以轻心,此时此刻,您应该振作起来,立即拿起书本努力学习和加强实战的训练,待到羽翼丰满之时大胆地走出去闯荡,让社会来对您做一个恰当的评价,然后再根据现实的情况不断地调整自己,继续学习和实践,由于电子和计算机科学的高速发展呈现日新月异的变化局势,您一定要明白:您距离失败永远只有6 个月!

songguoda 发表于 2011-5-20 00:05:52

高效可移植程序的一些习惯

对于一些状态的集合,尽可能使用enum而不是#define 来定义。

如:

enum {OFF = 0, ON = 1};

远比
#define OFF 0

#define ON 1

好。

两种写法编译的效果是一样的,但是写程序及看程序的人员来说感觉就不一样了。用enum定义时,可以觉得OFF与ON是一个集合,有相关性(且可减少程序中#define的定义)。但用#define定义时,完全可认为OFF与ON不相干。特别是在程序中#define定义过多时,查找并理解它的意思都是一个痛苦。(如:uc/os,看见它的一大堆#define都头大,要完全明白那一堆定义都得花不少时间)

第二,程序中少用

#if

#ifdef

等条件编译。不符合条件的程序段是不被编译的,都不检查语法错误,当在某些情况下打开了条件开关时,也许一堆错误就出现了。而且程序并不利于阅读,当读程序时突然得去查找某东东是否#define真是痛苦。

好了,下面是我的习惯

1、I/O口输出电平定义一下,以后万一I/O电平驱动取反,一改就好。

//I/O Port Stutes

enum {OFF = 0, ON = 1};
//Led Stutes
enum {LED_ON = 0, LED_OFF = 1};
eg. LED = LED _ON; //低电平驱动LED亮。

2、I/O口与任何的其它IC接口,I/O口都要定义,不能直接使用I/O口。(方便以后用于其它的应用中)

如93C46:

#define CS93C46 P1_0 //片选信号

#define SK93C46 P1_1 //时钟信号

#define DI93C46 P1_2 //数据输入,相对93C46而言 (即93C46的数据输入)

#define DO93C46 P1_3 //数据输出

如字符型LCD:

#define LCDPORT P0 //lcd数据口

#define RS P1_7 //数据指令选择。低--ins ; 高--data

#define RW P1_6 //读写选择。 低--w ; 高--r

#define EN P1_5 //使能。 高到低--使能。

3、键盘口要定义输入口及屏蔽字

如:
#define KEYMASK 0x0f

#define KEYPORT P2


4、串口用中断驱动,要带Buf。

5、取按键的程序及串口的程序编2个,一个为非阻塞式,一个为阻塞式(不按键或不收到数据不返回)。

6、晶振频率要定义
#define FOSC 11059200ul //频率

7、波特律定义

#define BAUD 4800 //波特率4800

#define SMODX 0 //是否倍频 0-NO or 1-YES

#define T1H_VAL (256 - (UINT8)(FOSC * (1<<SMODX) * 1.0 / 384 / BAUD))

#define T1L_VAL T1H_VAL

8、延时程序赋值时写成FOSC函数,编译器会预先算,不占用MCU资源
以后移植方便

9、液晶程序一定要定义长宽

如点阵型:
#define ROW 64
#define CLUM 128 /*128*64*/

字符型:

#define ROW 4
#define CLUM 20 /*128*64*/

#define ROW1_ADDR (0x80 + 0) //第一行地址

#define ROW2_ADDR (ROW1_ADDR + 0x40) //第二行地址

#define ROW3_ADDR (ROW1_ADDR + CLUM) //第三行地址

#define ROW4_ADDR (ROW2_ADDR + CLUM) //第四行地址
然后所有的和液晶地址有关的计算全部引用这2个量。
换用任何屏直接一改,高层不变

10、驱动层与应用接口层分在不同的文件,移植方便。

如:键盘、LCD、串口等。

songguoda 发表于 2011-5-20 00:08:34

浅谈单片机程序设计中的“分层思想”
    分层的思想,并不是什么神秘的东西,事实上很多做项目的工程师本身自己也会在用。看了不少帖子都发现没有提及这个东西,然而分层结构确是很有用的东西,参透后会有一种恍然大悟的感觉。如果说我不懂LCD怎么驱动,那好办,看一下datasheet,参考一下阿别人的程序,很快就可以做出来。但是如果不懂程序设计的思想的话,会给你做项目的过程中带来很多很多的困惑。

参考了市面上各种各样的嵌入式书籍,MCS-51,AVR ,ARM 等都有看过,但是没有发现有哪本是介绍设计思想的,就算有也是凤毛麟角。写程序不难,但是程序怎么样才能写的好,写的快,那是需要点经验积累的。结构化模块化的程序设计的思想,使最基本的要求。然而这么将这个抽象的概念运用到工程实践当中恩?那需要在做项目的过程中经历磨难,将一些东西总结出来,抽象升华为理论,对经验的积累和技术的传播都大有裨益。所以在下出来献丑一下,总结一些东西。



就我个人的经验而谈,有两个设计思想是非常重要的。

一个就是“时间片轮的设计思想”,这个对实际中解决多任务问题非常有用,通常可以用这个东西来判断一个人是单片机学习者,还是一个单片机工程师。这个必须掌握。由于网上介绍这个的帖子也不少,所以这里就不多说了。



第二个就是我今天想说的主题“分层屏蔽的设计思想”。下面用扫描键盘程序例子作为引子,引出今天说的东西。



问题的提出

单片机学习板一般为了简单起见,将按键分配的很好,例如整个 4*4 的键盘矩阵分配到 P1 口上面,8条控制线,刚好。这样的话程序也非常好写。只需要简单的

KEY_DAT = P1;

端口的数据就读进来了。



诚然,现实中没有这么好的事情。在实际的项目应用当中,单片机引脚的复用相当厉害,这跟那些所谓的单片机学习板就有很大的差别了。

另外一个原因,一般设计来说,是“软件配合硬件”的设计流程,简单点说就是,先确定好硬件原理图,硬件布线,最后才是软件的开发,因为硬件修改起来比较麻烦,相对来说软件修改的时候比较好改。这个就是中国传统的阴阳平衡哲学原理。硬件设计和软件设计本来就是鱼和熊掌的关系,两者不可兼得。方便了硬件设计,很可能给写软件带来很大的麻烦。反过来说,方便了软件设计,硬件设计也会相当的麻烦。如果硬件设计和软件设计同时方便了,那只有两种可能,一是这个设计方案非常简单,二是设计师已经达到了一个非常高的境界。我们不考虑那么多情况,单纯从常用的实际应用的角度来看问题。



硬件为了布线的方便,很多时候会可能将IO口分配到不同的端口上面,例如上面说的4*4键盘,8根线分别分配到 P0 P1 P2 P3 上面去了。那么,开发板的那些扫描键盘程序可以去见鬼了。怎么扫按键?我想起了我刚开始学习的时候,分成3段非常相似的程序,一个一个按键的扫描的经历......



或许有人不甘心,“那些东西我花了很长时间学习的,也用的好好的,怎么能说一句不用就不用?”虽然有点残忍,但是我还是想说“兄弟,接受现实吧,现实是残酷的......”



不过,人区别于低等动物的差别,是人会创造,在碰到困难的时候会想办法解决,于是我们开始了沉思......



最后我们引入初中数学学的“映射”的概念来解决问题。基本思想就是,将不同端口的按键映射到相同端口上面。



这样按键扫描程序就分成3个层次了。

1)最底层的是硬件层,完成端口扫描,20ms延时消抖,将端口的数据映射到一个KEY_DAT寄存器上面,KEY_DAT作为对上层驱动层的一个接口。



2)中间的一层是驱动层,驱动层只对 KEY_DAT 寄存器的数值进行操作。简单点说,我们无论底层的硬件是怎么接线的,在驱动层都不需要关心,只需要关心 KEY_DAT 这个寄存器的数值是什么就可以了。这样出来的间接效果就是“屏蔽了底层硬件的差异”,所以驱动层写的程序就可以通用了。

驱动层的另外一个功能是为了上层提供消息接口。我们用了类似window程序的消息的概念。这里可以提供一些按键消息,例如:按下消息,松开消息,长按键消息,长按键的时候的步进消息,等等。



3)应用层。这里就是根据项目的不同分别写按键功能程序,属于最上层的程序。它使用的是驱动层提供的消息接口。在应用层写程序的思想就是,我不管下层是怎么工作的,我只关心按键消息。有按键消息来的时候我就执行功能,没有消息来的时候,我就什么也不做。



下面用一个简单的常用的例子,说明我们这个设计思想的用法。



秒表调整时间的时候,要求按着某个按键不放,时间能连续的向上增加。这个东西很实用,实际的家电中用途很广泛。

在看下面的东西之前,大家可以想一下,这东西难吗?相信大家都会很响亮的回答,“不难!!”,然而我再问:“这东西麻烦吗?”我相信很多人肯定会说“很麻烦!!” 这不禁让我想起开始学单片机的时候写这种按键的那程序,乱七八糟的结构。如果不相信的话,可以自己用51写一下哦,那样就更加能体会本文说的分层结构的优越性。



项目要求:

两个按键,分别分配在P10 和P20,分别是“加”“减”按键,要求长按键的时候实现连续加和连续减的功能。



实战:



假设:

按键上拉,没有按键的时候高电平,有按键的时候低电平,另外,为了突出问题,这里没有将延时消抖的程序写上去,在实际项目中应该加上。C语言函数参数的传递多种多样,这里作为例子,用了最简单的全局变量来传递参数,当然你也可以用 unsigned char ReadPort(void) 返回一个读键结果,甚至还可以 void ReadPort(unsigned char *pt) 用一个指针变量传递地址而达到直接修改变量的目的。方法是多种多样的,这个决定于每个人的程序风格。





1)开始写硬件层程序,完成映射

#define KYE_MIN 0X01

#define KEY_PLUS 0X01



unsigned char KeyDat;



void ReadPort(void)

{

      if (P1 & KEY_PLUS == 0 ){

          KeyDat |= 0x01 ;

      }

      if (P2 & KEY_MIN == 0 ){

          KeyDat |= 0x02 ;

      }               

}



C语言应该很容易看懂吧?如果 KEY_PLUS 按下,P10口读到低电平,则 P1 & KEY_PLUS 的结果为 0 ,满足if 的条件,进入KeyDat |= 0x01 是将 KeyDat 的bit0 置一,也就是说,将 KEY_PLUS 映射到 KeyDat 的 bit0

KEY_MIN 是同样的道理映射到 KeyDat 的 bit1

如果 KeyDat 的 bit0 为 1 ,则说明 KEY_PLUS 按下,反则亦然。

不需要想的很神秘,映射就是这么一回事。如果还有其他按键的话,用同样办法,将他们全部映射到 KeyDat 上面。



2)驱动层程序编写

如果将 KeyDat想象成 P1 口,那么这个跟学习板那标准的扫描程序不就是一样了吗?对的,这个就是底层映射的目的了。



3)应用层程序编写

根据消息



硬件层是必须分离出来,然而驱动层和应用层的要求就不那么严格了,事实上一些简单的项目没有必要将这两层分离开来,根据实际应用灵活应对就可以了。其实这样写程序是很方便移植的,根据板子的不同而适当的修改一下硬件层那个 ReadPort 函数就完成了,驱动层和应用层很多代码可以不经过修改直接用,很能提高开发效率的。当然这个按键程序会存在一定的问题,特别是遇到常闭按键和点触按键的混合使用的场合。这个留给大家自己去想了,反正问题总是能找到解决办法的,尽管方法有好有坏。





结束语

以按键为媒介,介绍了程序设计当中的“分层屏蔽”的思想的原理和应用,按键只是一个例子,其实分层的思想普遍存在着程序设计当中。细心留意一下的话发现其实window,linux,网络的tcp/ip 结构全部都是分层的。这东西不是绣花枕头,而是实际用在工程上面的,只是平时不多见帖子介绍,或者没有人特意这样来总结,又或者是有经验的工程师作为藏在心中的法宝吧,这个就不得而知。不过好东西应该共享,菜鸟应该共勉,一起来学飞吧。

jssz_hf 发表于 2011-5-25 19:11:01

mark楼主辛苦了

cheche017 发表于 2011-5-26 21:52:56

mark!

oaixuw 发表于 2011-5-27 14:50:11

mark

weilan2200 发表于 2011-5-27 20:55:56

谢谢Lz分享!

QQ373466062 发表于 2011-5-27 22:37:25

看一看学习

shunda 发表于 2011-5-28 00:43:58

学习了

szw890526 发表于 2011-5-30 10:07:38

标记下

coyool 发表于 2011-5-31 21:18:56

mark

mcudesign 发表于 2011-6-1 08:32:02

楼主,继续更。。。

happyw2004 发表于 2011-6-1 09:01:15

好东西呀……。

cong2010 发表于 2011-6-1 11:23:21

很好

sailen 发表于 2011-6-1 13:26:13

好东西,必须mark

power_check 发表于 2011-6-1 16:41:26

mark

chenshu2834 发表于 2011-6-1 19:24:02

mark

tyqhaha 发表于 2011-6-1 23:01:50

一定找个时间看看,关QQ,断网

flame0510 发表于 2011-6-2 21:30:10

mark

sync765 发表于 2011-6-2 22:42:02

讲的很好哦
支持楼主了

yeyo 发表于 2011-6-2 23:28:03

我也标记一下

CODE2 发表于 2011-6-2 23:42:54

mark~~~~~~~~好东西

songguoda 发表于 2011-6-4 21:11:26

好久没有来更新了,最近一直在找关于状态机方面的资料,有的朋友讲用UCOS操作系统得了,何必那么麻烦呀,其实我也想过,但是必竟是8位单片机用得多一些,再说这种单片机资源有限,还是觉得用状态机来得实用些,只是说起来容易做起来难呀,真想找到一本关于单片机状态机编程入门的好书籍、资料,希望各位同仁们推荐一下,谢谢啦!

chen_5295 发表于 2011-6-4 21:17:55

MARK~~~~

xiaowu191 发表于 2011-6-4 22:16:08

强贴留名

Elderfox 发表于 2011-6-4 22:54:22

好!鼓掌

taishandadi 发表于 2011-6-4 23:07:29

可以参考傻孩子的关于状态机的帖子。

2bao2 发表于 2011-6-5 09:52:44

顶起,好资料就要分享

ouyj_0210 发表于 2011-6-8 11:49:15

手记好,呵呵~~~

shennaijun0433 发表于 2011-6-8 16:56:21

mark

essar 发表于 2011-6-10 16:59:39

收藏之。 下班啦。

songguoda 发表于 2011-6-10 21:53:50

终于找到一本关于编程思想讲解得很详细的书《时间触发嵌入式系统设计模式 8051系列微控制器开发可靠应用》,咱们论坛上有,我就不上传了,给阿莫省些空间
http://www.ourdev.cn/bbs/bbs_content.jsp?bbs_sn=749967&bbs_page_no=1&search_mode=1&search_text=%E6%97%B6%E9%97%B4%E8%A7%A6%E5%8F%91%E5%B5%8C%E5%85%A5%E5%BC%8F%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F&bbs_id=9999
9楼书带的光盘相关代码
29楼 时间触发合作式调度器在avr中的移植(cvavr编译器)

llluo 发表于 2011-6-13 00:23:39

Easy51RTOS的原理

hyj9988 发表于 2011-6-13 09:05:30

mark

li8303 发表于 2011-6-17 22:34:39

mark

lccc 发表于 2011-6-19 18:29:47

MARK

cctv02 发表于 2011-6-19 21:42:28

谢谢 顶了

javine 发表于 2011-6-21 17:42:52

突然发现,以前根本不算逛论坛...好帖啊!!

czw90130 发表于 2011-6-21 18:11:20

mark

chushichongyu 发表于 2011-6-22 13:26:11

受益匪浅

qqand 发表于 2011-7-1 08:35:27

好东西,谢谢分享!!!

oaixuw 发表于 2011-7-1 09:11:30

mark

liwboy 发表于 2011-7-1 09:39:20

受益匪浅
mark

bulinsheng 发表于 2011-7-1 10:00:54

谢谢楼主!

liwboy 发表于 2011-7-1 11:35:46

怎么提供的源程序编译时还有错误
看来还得自己修改!

1533952857 发表于 2011-7-1 14:05:28

mark

qinhya 发表于 2011-7-1 17:40:48

mark

guxingganyue 发表于 2011-7-1 18:43:48

楼主辛苦了

Super_C 发表于 2011-7-1 19:27:30

MARK.

lwcumt 发表于 2011-7-1 20:10:30

有空看看

brian2321 发表于 2011-7-2 14:11:22

mark

ecitlc 发表于 2011-7-3 14:21:38

mark
页: [1] 2 3
查看完整版本: 从单片机初学者迈向单片机工程师(一份正真的让你成为工程师的贴子)整理 补充