• 创业之小企业的生存之道

     这几天,我在读Seth Godin的《创业者圣经:有创意,无资金,如何起家》(The Bootstrapper's Bible,上海译文出版社,2000)。 当你白手起家,开始创业的时候,市场往往已经被几家大企业牢牢占据。如何在与大企业的竞争中存活下来,就成了你面对的最大问题。 许多创业者对这个问题缺乏清醒的认识,不懂得扬长避短,结果犯下了严重的错误。他们以为,凭着自己价廉物美的产品,就可以与大企业展开正面竞争,从后者手中把客户抢过来。这种想法常常导致灾难性的后果,为小企业带来灭顶之灾。 为什么小企业不适宜与大企业正面竞争?美国著名的创业家塞思•戈丁(Seth Godin)在《创业者圣经》一书中,指出大企业有小企业不具备的五个关键优势: 1)经销渠道。大企业的产品无处不在,广告铺天盖地。 2)融资渠道。大企业能够用较低的成本,借到大笔资金。 3)名牌效应。消费者信任著名品牌。 4)客户关系。新进入市场的公司就像一个陌生人,得不到信任,客户和供应商更愿意与以前的合作方合作。 5)优秀雇员。大企业能够吸引优秀的人才。 大企业的这五点优势,都是小企业望尘莫及的。妄想在这些方面击败大企业,无异于以卵击石。但是,这是不是就意味着小企业没有任何机会呢?这倒也未必,关键是除了优秀的产品以外,还要选对竞争方法。创业者应该避开大企业的优势领域,而选择进行侧面竞争,也就是避其锋芒,攻其不备。 具体来说,是这样的: 1)经销渠道。你不要一开始就采用传统的经销渠道,比如在大商场里销售产品,而要更多地考虑新兴销售渠道,比如邮购和互联网。你还要更多地采用与用户面对面地推销。 2)融资渠道。从一开始创业,资金问题就会困扰你,贷款的利息会压得你喘不过气,所以不要选择那些需要大量资金的创业项目,也千万不要借高利贷。 3)名牌效应。你的产品没有人家名气大,那就只好选择比人家便宜了。另外,一定要突出宣传你的产品与其他产品的不同之处和优秀之处。 4)客户关系。你不太可能一下子就从大企业手中抢到一个大客户,你只能慢慢来,先做一笔小生意,或者直接把产品送给客户试用,先建立起关系再说,然后再慢慢蚕食大企业的客户。而且每次都以一个客户作为重点,开展销售工作,不分散力量。 5)优秀雇员。并非所有的雇员都追求公司的名气、稳定的工作和高报酬,事实上,很多人都喜欢带有冒险性的工作,喜欢有灵活的工作时间、关心下属的老板、认股权、方便的工作地点、没有官僚主义和复杂人际关系的工作环境,良好的发展机会等等。只要你能提供这些大企业无法提供的东西,你就能从他们手里争夺到优秀人才。 因此,只要你的产品是顾客真正需要的,然后又采取了正确的竞争策略,你就有机会在竞争中生存下来。 更有利的是,随着互联网时代的到来,小企业的生存环境进一步改善,你生存下去的机会比以前任何时候都大。互联网对小企业的帮助体现在这样几个方面: a. 小企业大多选择服务业创业,服务业的规模效应本来就不显著,大规模生产并不一定能降低多少成本。互联网加强了这种趋势,小企业和大企业之间的成本差距被拉近了。 b. 在新技术的帮助下,小企业能达到与大企业一样的高效率。互联网大大提高了小企业的管理水平,同时又大大降低了管理成本。 c. 互联网时代是一个个性化需求涌现的时代,客户不再愿意接受千篇一律的标准化产品,希望通过互联网找到最适合自己需求和个性的产品。小企业机构简单,管理灵活,更适合向顾客提供个性化的产品。 所以,总的来看,巨人并不是不可战胜的,大企业的优势其实也不是那么牢不可破,小企业灵活的体制反而更适合网络时代的特点。所以,如果你有好的项目,大可不必犹豫不决,创业成功的机会其实比看上去的要大。如果说20世纪是大公司和大集团的世纪,那么也许21世纪就是小企业和创业者的世纪。

    时间:2018-06-20 关键词: 创业

  • 顶级程序员的思维方式评《代码之美》第五条牛逼

     公司里曾在北美作为产品研发经理的资深人物某日和我闲聊,说到中美程序员的差别,北美的程序员,尤其是发明那些根本性东西比如JAVA, 比如Message机制,用得都是非常简单的办法,一方面,英语作为母语,与程序设计语言更加靠近,比中国的象形文字来的近,另一方面,这些北美的程序员似乎都深谙大道至简的道理,代码都写得很简单,当然问题的思维方式更为系统。相对的来说,国内的技术人员总是习惯把问题复杂化,不仅理解问题复杂化,解法也复杂,有时正是因为没有理解核心代码的设计,反而使得解法更加背道而驰。    《代码之美》这本书,刚好给了一些关于北美(但不限于)的顶级程序员的例子,与其说它是讲代码之美,到不如说是讲他们的思维方式,我们甚至可以窥见他们的价值观,个人的体会是,这本书里面的顶级程序员们体现了以一系列思维习惯值得国内同行参考: 第一:非常认真全面的研究问题/客户需求,比如在为霍金开发只用一个按钮的交互软件时,他们很快意识到按钮不止表达01,按扭的时长可以成为一种模拟输入,从而为长按和短按分别设计更细化的选项,由此简化了后面的设计。 第二:总是希望用一个个案抽象一种模型,并用于更大的范围,前面的程序可以服务给盲人,Google著名的MapReduce算法开始用于统计百万亿网页上的词频率,后来用于所有分布式环境 第三:永远寻求简单的方式来解决问题,不过有个技巧,就是可以把复杂的难题恰当的分割。MapReduce算法体现了一种简约之美,如同我们常讲的中间件,屏蔽一切分布式,普通的程序员都可以依靠MapReduce和GFS来写用于上千台机器的分布式应用。   在另一章开源ERP5的项目中,复杂的ERP项目的一级类仅有五个,Resouce, Node, Path, Movement, Item, 这些概念加上Order和Delivery的业务基本项构成了基础业务模型,这个设计思维有点像说宇宙是四大元素组成的。    另一个不错的案例来自Linux内核小组关于驱动的研究。   这个模型的抽象能力的缺乏,估计是中国工程师的软肋,也是思维复杂化的原因。硅谷软件人才IC(India/China)比比皆是,但能够定义软件行业的,还都是白人。或许,这个删繁入简的抽象能力,早在中国人学语文的时候被虚情假意的形容词淹没了。 第四:对于技术难题的灵感需要等待,从这本书看起来,软件天才是不存在的,几位顶级程序员都遇到一筹莫展的问题,有些几个月也没有解法,有的是某次喝咖啡的灵光一闪,或者干脆关键的程序都是在喜马拉雅山下的山村写的。 第五:他们都怀有程序改变世界的信心,在关于安全通讯的一章结尾,作者深信他们对于世界的民主化在做贡献,在他们看来:人类文明的代码,越来越需要程序员对其进行重新编程,然后重新接入社会这个操作系统中。比如,基因序列分析,商业软件,计算机建模,更不要说互联网上的多次革命,Email, Blog, SNS, VoIP。 这下理解为什么Google敢于对抗各国政府。 PS:但是对抗不了中国政府啊。

    时间:2018-06-20 关键词: 编程 程序猿

  • 对于GCC警告选项的理解

     GCC警告选项对我们及时找出代码中BUG,改善代码品质很有帮助。 听说高手们对每个警告选项都很熟悉,能灵活运用各个警告选项来分门别类地过滤代码中的BUG信息。 1 ../SRC/libpjmacl/pjmacl_jaccmd.c:1661: 警告: dereferencing type-punned pointer will break strict-aliasing rules pjmacl_free((void**)&cmn_ofs_rg_p); PjmAclComnAndOfsList_t *cmn_ofs_rg_p = NULL; 2 ../SRC/libpjmacl/pjmacl_pjm.c:332: 警告: assignment from incompatible pointer type data_p = (PjmAclPrioAddr_t *)calloc(1, sizeof(PjmAclPrioAddr_t)) uint64_t *data_p; 3 ../SRC/libpjmacl/pjmacl_pjm.c:2867: 警告: no previous declaration for ‘pjmacl_get_node_pri_U’ pjmacl_get_node_pri_U如果为外部函数的话,就应该有个声明加到类似pjsd_prt.h中 4 ../SRC/libpjmfep/pjmfep_exec.c:406: 警告: implicit declaration of function ‘pjmfep_evc_queue_init’ ../SRC/libpjmfep/pjmfep_exec.c:406: 警告: nested extern declaration of ‘pjmfep_evc_queue_init’ ‘pjmfep_evc_queue_init’函数就没有被声明过,而且如果定义文件和引用该函数文件不一样时,引用该函数的文件也没有提前用extern 声明该函数 5 ../SRC/pjmd/pjmd_conff.c:1053: 警告: passing argument 1 of ‘set_rscGrpNode_type4’ discards qualifiers from pointer target type const char *str_p, set_rscGrpNode_type4(str_p, &rscgrp_p->group_info.nodeinfo); static int set_rscGrpNode_type4(char* strwk, NodeRscInfo_t* info_p) 6警告选项中关注那些引用未初始化值的语句 7 变量定义时,就最好初始化一下,省得到GCC 警告信息里刷数据 8:警告信息过于严格,有些警告信息就不是BUG了。比如调用函数中实参是&aa,这时这个指针就失去了具体的类型 dereferencing type-punned pointer will break strict-aliasing rules call_ret = cmdlib_pjmcall(PJMCMD_CMD_PADLINE, &reqbufinf, buf_num, NULL, NULL, NULL, &exitval, &retval, (char **)&padlresp_p, &respsize, &resptype, clst_p); 中的(char **)&padlresp_p 就会产生上述警告信息 而&exitval 会产生 类似passing argument 1 of 'pjmd_pjsutil_rscunit_name2id' discards qualifiers from pointer target type的警告信息提示 dereferencing type-punned pointer will break strict-aliasing rules警告信息跟优化选项-fstrict-aliasing 有关。当开启这个优化选项时,可能优化会导致源代码中部分语句缺失,而造成系统工作不正常,所以就所有的违反strict-aliasing rules原则的地方加了条这个警告信息,提醒读者检查这个地方的代码,看看是不是如果优化后会导致部分语句工作不正常 同时这个信息提示还有另外一个目的,两个不同类型的指针指向同一个地址时,极有可能会导致代码编写出错(比如犯大小端的错误,还有地址字节对齐的错误),所以这个警告信息还有这一层的目的。

    时间:2018-06-20 关键词: gcc

  • 基于9轴惯性运动传感器的三阶卡尔曼滤波器算法

    最近在玩九轴的惯性传感器,很是有挑战性.九轴说的是三轴的加速度计、三轴的陀螺仪以及三轴的磁场传感器。但是只是单纯的测出九个轴的数据没什么用,关键是要能够融合这九轴数据得出我们想要的结果。这里就运用三阶卡尔曼滤波算法来融合这九轴运动数据为三轴的角度。运用这三个角度可以用来做自平衡车或者四轴飞行器. 一、卡尔曼算法理解 其实如果不去考虑kalman算法是怎么来的,我们只需要知道有下面几个式子就可以了,具体意思可以看上面的wikipedia链接 二 卡尔曼滤波算法的实现 这里我的算法是运行在avr单片机上的,所以采用的是c语言写的。下面的代码是要放到avr的定时器中断测试刷新的。用示波器测试了一下,这个算法在16M晶振下的运行时间需要0.35ms,而数据采集需要3ms左右,所以选定定时器时间为8ms.之前也写过一阶的kalman算法,运用在自平衡车上,这边是三阶的,主要是矩阵运算. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 //kalman.c float dtTimer   = 0.008; float xk[9] = {0,0,0,0,0,0,0,0,0}; float pk[9] = {1,0,0,0,1,0,0,0,0}; float I[9]  = {1,0,0,0,1,0,0,0,1}; float R[9]  = {0.5,0,0,0,0.5,0,0,0,0.01}; float Q[9] = {0.005,0,0,0,0.005,0,0,0,0.001};   void matrix_add(float* mata,float* matb,float* matc){     uint8_t i,j;     for (i=0; i<3; i++){        for (j=0; j<3; j++){           matc[i*3+j] = mata[i*3+j] + matb[i*3+j];        }     } }   void matrix_sub(float* mata,float* matb,float* matc){     uint8_t i,j;     for (i=0; i<3; i++){        for (j=0; j<3; j++){           matc[i*3+j] = mata[i*3+j] - matb[i*3+j];        }     } }   void matrix_multi(float* mata,float* matb,float* matc){   uint8_t i,j,m;   for (i=0; i<3; i++)   {     for (j=0; j<3; j++)    {       matc[i*3+j]=0.0;       for (m=0; m<3; m++)      {         matc[i*3+j] += mata[i*3+m] * matb[m*3+j];       }     }  } }   void KalmanFilter(float* am_angle_mat,float* gyro_angle_mat){ uint8_t i,j; float yk[9]; float pk_new[9]; float K[9]; float KxYk[9]; float I_K[9]; float S[9]; float S_invert[9]; float sdet;   //xk = xk + uk matrix_add(xk,gyro_angle_mat,xk); //pk = pk + Q matrix_add(pk,Q,pk); //yk =  xnew - xk matrix_sub(am_angle_mat,xk,yk); //S=Pk + R matrix_add(pk,R,S); //S求逆invert sdet = S[0] * S[4] * S[8]           + S[1] * S[5] * S[6]           + S[2] * S[3] * S[7]           - S[2] * S[4] * S[6]           - S[5] * S[7] * S[0]           - S[8] * S[1] * S[3];   S_invert[0] = (S[4] * S[8] - S[5] * S[7])/sdet; S_invert[1] = (S[2] * S[7] - S[1] * S[8])/sdet; S_invert[2] = (S[1] * S[7] - S[4] * S[6])/sdet;   S_invert[3] = (S[5] * S[6] - S[3] * S[8])/sdet; S_invert[4] = (S[0] * S[8] - S[2] * S[6])/sdet; S_invert[5] = (S[2] * S[3] - S[0] * S[5])/sdet;   S_invert[6] = (S[3] * S[7] - S[4] * S[6])/sdet; S_invert[7] = (S[1] * S[6] - S[0] * S[7])/sdet; S_invert[8] = (S[0] * S[4] - S[1] * S[3])/sdet; //K = Pk * S_invert matrix_multi(pk,S_invert,K); //KxYk = K * Yk matrix_multi(K,yk,KxYk); //xk = xk + K * yk matrix_add(xk,KxYk,xk); //pk = (I - K)*(pk) matrix_sub(I,K,I_K); matrix_multi(I_K,pk,pk_new); //update pk //pk = pk_new; for (i=0; i<3; i++){     for (j=0; j<3; j++){         pk[i*3+j] = pk_new[i*3+j];     }   } } 三 运用卡尔曼滤波器 这里的kalman滤波器是离散数字滤波器,需要迭代运算。这里把算法放到avr的定时器中断里面执行,进行递归运算. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 //isr.c #include "kalman.h" float mpu_9dof_values[9]={0}; float am_angle[3]; float gyro_angle[3]; float am_angle_mat[9]={0,0,0,0,0,0,0,0,0}; float gyro_angle_mat[9]={0,0,0,0,0,0,0,0,0};   //8MS ISR(TIMER0_OVF_vect){ //设置计数开始的初始值 TCNT0 = 130 ;  //8ms sei(); //采集九轴数据 //mpu_9dof_values 单位为g与度/s //加速度计和陀螺仪 mpu_getValue6(&mpu_9dof_values[0],&mpu_9dof_values[1],&mpu_9dof_values[2],&mpu_9dof_values[3],&mpu_hmc_values[4],&mpu_hmc_values[5]); //磁场传感器 compass_mgetValues(&mpu_9dof_values[6],&mpu_9dof_values[7],&mpu_9dof_values[8]);   accel_compass2angle(&mpu_9dof_values[0],&mpu_9dof_values[6],am_angle); gyro2angle(&mpu_9dof_values[3],gyro_angle);   am_angle_mat[0] = am_angle[0]; am_angle_mat[4] = am_angle[1]; am_angle_mat[8] = am_angle[2];   gyro_angle_mat[0] = gyro_angle[1]; gyro_angle_mat[4] = - gyro_angle[0]; gyro_angle_mat[8] = - gyro_angle[2];   //卡尔曼 KalmanFilter(am_angle_mat,gyro_angle_mat);   //输出三轴角度 //xk[0] xk[4] xk[8] } 实测可以准确的输出三轴的角度,为了获得更好的响应速度和跟踪精度还需调整参数.

    时间:2018-06-19 关键词: 滤波器 卡尔曼算法

  • 初步认识8051单片机芯片

     上一课我们的第一个项目完成了,可能有懂C语言的朋友会说,"这和PC机上的C语言没有多大的区别呀"。的确没有太大的区别,C语言只是一种程序语言的统称,针对不同的处理器相关的C语言都会有一些细节的改变。编写PC机的C程序时,如要对硬件编程你就必须对硬件要有一定的认识,51单片机编程就更是如此,因它的开发应用是不可与硬件脱节的,所以我们先要来初步认识一下51苾片的结构和引脚功能。MSC51架构的芯片种类很多,具体特点和功能不尽相同(在以后编写的附录中会加入常用的一些51芯片的资料列表),在此后的教程中就以Atmel公司的AT89C51和AT89C2051为中心对象来进行学习,两者是AT89系列的典型代表,在爱好者中使用相当的多,应用资料很多,价格便宜,是初学51的首选芯片。嘿嘿,口水多多有点卖广告之嫌了。:P 图2-1 AT89C51和AT89C2051引脚功能图 图2-1中是AT89C51和AT89C2051的引脚功能图。而表2-1中则是它们的主要性能表。以上可以看出它们是大体相同的,由于AT89C2051的IO线很少,导致它无法外加RAM和程序ROM,片内Flash存储器也少,但它的体积比AT89C51小很多,以后大家可根据实际需要来选用。它们各有其特点但其核心是一样的,下面就来看看AT89C51的引脚具体功能。 1.电源引脚 Vcc 40 电源端 GND 20 接地端 *工作电压为5V,另有AT89LV51工作电压则是2.7-6V, 引脚功能一样。 2.外接晶体引脚 图2-2 外接晶体引脚 XTAL1 19 XTAL2 18 XTAL1是片内振荡器的反相放大器输入端,XTAL2则是输出端,使用外部振荡器时,外部振荡信号应直接加到XTAL1,而XTAL2悬空。内部方式时,时钟发生器对振荡脉冲二分频,如晶振为12MHz,时钟频率就为6MHz。晶振的频率可以在1MHz-24MHz内选择。电容取30PF左右。 *型号同样为AT89C51的芯片,在其后面还有频率编号,有12,16,20,24MHz可选。大家在购买和选用时要注意了。如AT89C51 24PC就是最高振荡频率为24MHz,40P6封装的普通商用芯片。 3.复位 RST 9 在振荡器运行时,有两个机器周期(24个振荡周期)以上的高电平出现在此引腿时,将使单片机复位,只要这个脚保持高电平,51芯片便循环复位。复位后P0-P3口均置1引脚表现为高电平,程序计数器和特殊功能寄存器SFR全部清零。当复位脚由高电平变为低电平时,芯片为ROM的00H处开始运行程序。常用的复位电路如图2-3所示。 *复位操作不会对内部RAM有所影响。 图2-3 常用复位电路 4.输入输出引脚 (1) P0端口[P0.0-P0.7] P0是一个8位漏极开路型双向I/O端口,端口置1(对端口写1)时作高阻抗输入端。作为输出口时能驱动8个TTL。 对内部Flash程序存储器编程时,接收指令字节;校验程序时输出指令字节,要求外接上拉电阻。 在访问外部程序和外部数据存储器时,P0口是分时转换的地址(低8位)/数据总线,访问期间内部的上拉电阻起作用。 (2) P1端口[P1.0-P1.7] P1是一个带有内部上拉电阻的8位双向I/0端口。输出时可驱动4个TTL。端口置1时,内部上拉电阻将端口拉到高电平,作输入用。 对内部Flash程序存储器编程时,接收低8位地址信息。 (3) P2端口[P2.0-P2.7] P2是一个带有内部上拉电阻的8位双向I/0端口。输出时可驱动4个TTL。端口置1时,内部上拉电阻将端口拉到高电平,作输入用。 对内部Flash程序存储器编程时,接收高8位地址和控制信息。 在访问外部程序和16位外部数据存储器时,P2口送出高8位地址。而在访问8位地址的外部数据存储器时其引脚上的内容在此期间不会改变。 (4) P3端口[P3.0-P3.7] P2是一个带有内部上拉电阻的8位双向I/0端口。输出时可驱动4个TTL。端口置1时,内部上拉电阻将端口拉到高电平,作输入用。 对内部Flash程序存储器编程时,接控制信息。除此之外P3端口还用于一些专门功能,具体请看 表2-2.。 *P1-3端口在做输入使用时,因内部有上接电阻,被外部拉低的引脚会输出一定的电流。 呼!一口气说了那么多,停一下吧。嗯,什么?什么叫上拉电阻?上拉电阻简单来说就是把电平拉高,通常用4.7-10K的电阻接到Vcc电源,下拉电阻则是把电平拉低,电阻接到GND地线上。具体说明也不是这里要讨论的,接下来还是接着看其它的引脚功能吧。 5.其它的控制或复用引脚 (1) ALE/PROG 30 访问外部存储器时,ALE(地址锁存允许)的输出用于锁存地址的低位字节。即使不访问外部存储器,ALE端仍以不变的频率输出脉冲信号(此频率是振荡器频率的1/6)。在访问外部数据存储器时,出现一个ALE脉冲。对Flash存储器编程时,这个引脚用于输入编程脉冲PROG (2) PSEN 29 该引是外部程序存储器的选通信号输出端。当AT89C51由外部程序存储器取指令或常数时,每个机器周期输出2个脉冲即两次有效。但访问外部数据存储器时,将不会有脉冲输出。 (3) EA/Vpp 31 外部访问允许端。当该引脚访问外部程序存储器时,应输入低电平。要使AT89C51只访问外部程序存储器(地址为0000H-FFFFH),这时该引脚必须保持低电平。对Flash存储器编程时,用于施加Vpp编程电压。Vpp电压有两种,类似芯片最大频率值要根据附加的编号或芯片内的特征字决定。具体如表2-3所列。 表2-3 Vpp与芯片型号和片内特征字的关系 看到这您对AT89C51引脚的功能应该有了一定的了解了,引脚在编程和校验时的时序我们在这里就不做详细的探讨,通常情况下我们也没有必要去撑握它,除非你想自己开发编程器。下来的课程我们要开始以一些简单的实例来讲述C程序的语法和编写方法技巧,中间穿插相关的硬件知识如串口,中断的用法等等。

    时间:2018-06-19 关键词: 单片机 51单片机

  • 【电子制作】51单片机控制四相步进电机

     接触单片机快两年了,不过只是非常业余的兴趣,实践却不多,到现在还算是个初学者吧。这几天给自己的任务就是搞定步进电机的单片机控制。以前曾看过有关步进电机原理和控制的资料,毕竟自己没有做过,对其具体原理还不是很清楚。今天从淘宝网买了一个EPSON的UMX-1型步进电机,此步进电机为双极性四相,接线共有六根,外形如下图所示: 拿到步进电机,根据以前看书对四相步进电机的了解,我对它进行了初步的测试,就是将5伏电源的正端接上最边上两根褐色的线,然后用5伏电源的地线分别和另外四根线(红、兰、白、橙)依次接触,发现每接触一下,步进电机便转动一个角度,来回五次,电机刚好转一圈,说明此步进电机的步进角度为360/(4×5)=18度。地线与四线接触的顺序相反,电机的转向也相反。 如果用单片机来控制此步进电机,则只需分别依次给四线一定时间的脉冲电流,电机便可连续转动起来。通过改变脉冲电流的时间间隔,就可以实现对转速的控制;通过改变给四线脉冲电流的顺序,则可实现对转向的控制。所以,设计了如下电路图: C51程序代码为: 代码一 #include static unsigned int count; static unsigned int endcount; void delay(); void main(void) { count = 0; P1_0 = 0; P1_1 = 0; P1_2 = 0; P1_3 = 0; EA = 1; //允许CPU中断 TMOD = 0x11; //设定时器0和1为16位模式1 ET0 = 1; //定时器0中断允许 TH0 = 0xFC; TL0 = 0x18; //设定时每隔1ms中断一次 TR0 = 1; //开始计数 startrun: P1_3 = 0; P1_0 = 1; delay(); P1_0 = 0; P1_1 = 1; delay(); P1_1 = 0; P1_2 = 1; delay(); P1_2 = 0; P1_3 = 1; delay(); goto startrun; } //定时器0中断处理 void timeint(void) interrupt 1 { TH0=0xFC; TL0=0x18; //设定时每隔1ms中断一次 count++; } void delay() { endcount=2; count=0; do{}while(count } 将上面的程序编译,用ISP下载线下载至单片机运行,步进电机便转动起来了,初步告捷! 不过,上面的程序还只是实现了步进电机的初步控制,速度和方向的控制还不够灵活,另外,由于没有利用步进电机内线圈之间的“中间状态”,步进电机的步进角度为18度。所以,我将程序代码改进了一下,如下: 代码二 #include static unsigned int count; static int step_index; void delay(unsigned int endcount); void gorun(bit turn, unsigned int speedlevel); void main(void) { count = 0; step_index = 0; P1_0 = 0; P1_1 = 0; P1_2 = 0; P1_3 = 0; EA = 1; //允许CPU中断 TMOD = 0x11; //设定时器0和1为16位模式1 ET0 = 1; //定时器0中断允许 TH0 = 0xFE; TL0 = 0x0C; //设定时每隔0.5ms中断一次 TR0 = 1; //开始计数 do{ gorun(1,60); }while(1); } //定时器0中断处理 void timeint(void) interrupt 1 { TH0=0xFE; TL0=0x0C; //设定时每隔0.5ms中断一次 count++; } void delay(unsigned int endcount) { count=0; do{}while(count } void gorun(bit turn,unsigned int speedlevel) { switch(step_index) { case 0: P1_0 = 1; P1_1 = 0; P1_2 = 0; P1_3 = 0; break; case 1: P1_0 = 1; P1_1 = 1; P1_2 = 0; P1_3 = 0; break; case 2: P1_0 = 0; P1_1 = 1; P1_2 = 0; P1_3 = 0; break; case 3: P1_0 = 0; P1_1 = 1; P1_2 = 1; P1_3 = 0; break; case 4: P1_0 = 0; P1_1 = 0; P1_2 = 1; P1_3 = 0; break; case 5: P1_0 = 0; P1_1 = 0; P1_2 = 1; P1_3 = 1; break; case 6: P1_0 = 0; P1_1 = 0; P1_2 = 0; P1_3 = 1; break; case 7: P1_0 = 1; P1_1 = 0; P1_2 = 0; P1_3 = 1; } delay(speedlevel); if (turn==0) { step_index++; if (step_index>7) step_index=0; } else { step_index--; if (step_index<0) step_index=7; } } 改进的代码能实现速度和方向的控制,而且,通过step_index静态全局变量能“记住”步进电机的步进位置,下次调用 gorun()函数时则可直接从上次步进位置继续转动,从而实现精确步进;另外,由于利用了步进电机内线圈之间的“中间状态”,步进角度减小了一半,只为9度,低速运转也相对稳定一些了。 但是,在代码二中,步进电机的运转控制是在主函数中,如果程序还需执行其它任务,则有可能使步进电机的运转收到影响,另外还有其它方面的不便,总之不是很完美的控制。所以我又将代码再次改进: 代码三 #include static unsigned int count; //计数 static int step_index; //步进索引数,值为0-7 static bit turn; //步进电机转动方向 static bit stop_flag; //步进电机停止标志 static int speedlevel; //步进电机转速参数,数值越大速度越慢,最小值为1,速度最快 static int spcount; //步进电机转速参数计数 void delay(unsigned int endcount); //延时函数,延时为endcount*0.5毫秒 void gorun(); //步进电机控制步进函数 void main(void) { count = 0; step_index = 0; spcount = 0; stop_flag = 0; P1_0 = 0; P1_1 = 0; P1_2 = 0; P1_3 = 0; EA = 1; //允许CPU中断 TMOD = 0x11; //设定时器0和1为16位模式1 ET0 = 1; //定时器0中断允许 TH0 = 0xFE; TL0 = 0x0C; //设定时每隔0.5ms中断一次 TR0 = 1; //开始计数 turn = 0; speedlevel = 2; delay(10000); speedlevel = 1; do{ speedlevel = 2; delay(10000); speedlevel = 1; delay(10000); stop_flag=1; delay(10000); stop_flag=0; }while(1); } //定时器0中断处理 void timeint(void) interrupt 1 { TH0=0xFE; TL0=0x0C; //设定时每隔0.5ms中断一次 count++; spcount--; if(spcount<=0) { spcount = speedlevel; gorun(); } } void delay(unsigned int endcount) { count=0; do{}while(count } void gorun() { if (stop_flag==1) { P1_0 = 0; P1_1 = 0; P1_2 = 0; P1_3 = 0; return; } switch(step_index) { case 0: //0 P1_0 = 1; P1_1 = 0; P1_2 = 0; P1_3 = 0; break; case 1: //0、1 P1_0 = 1; P1_1 = 1; P1_2 = 0; P1_3 = 0; break; case 2: //1 P1_0 = 0; P1_1 = 1; P1_2 = 0; P1_3 = 0; break; case 3: //1、2 P1_0 = 0; P1_1 = 1; P1_2 = 1; P1_3 = 0; break; case 4: //2 P1_0 = 0; P1_1 = 0; P1_2 = 1; P1_3 = 0; break; case 5: //2、3 P1_0 = 0; P1_1 = 0; P1_2 = 1; P1_3 = 1; break; case 6: //3 P1_0 = 0; P1_1 = 0; P1_2 = 0; P1_3 = 1; break; case 7: //3、0 P1_0 = 1; P1_1 = 0; P1_2 = 0; P1_3 = 1; } if (turn==0) { step_index++; if (step_index>7) step_index=0; } else { step_index--; if (step_index<0) step_index=7; } } 在代码三中,我将步进电机的运转控制放在时间中断函数之中,这样主函数就能很方便的加入其它任务的执行,而对步进电机的运转不产生影响。在此代码中,不但实现了步进电机的转速和转向的控制,另外还加了一个停止的功能,呵呵,这肯定是需要的。 步进电机从静止到高速转动需要一个加速的过程,否则电机很容易被“卡住”,代码一、二实现加速不是很方便,而在代码三中,加速则很容易了。在此代码中,当转速参数speedlevel 为2时,可以算出,此时步进电机的转速为1500RPM,而当转速参数speedlevel 1时,转速为3000RPM。当步进电机停止,如果直接将speedlevel 设为1,此时步进电机将被“卡住”,而如果先把speedlevel 设为2,让电机以1500RPM的转速转起来,几秒种后,再把speedlevel 设为1,此时电机就能以3000RPM的转速高速转动,这就是“加速”的效果。 在此电路中,考虑到电流的缘故,我用的NPN三极管是S8050,它的电流最大可达1500mA,而在实际运转中,我用万用表测了一下,当转速为1500RPM时,步进电机的电流只有90mA左右,电机发热量较小,当转速为60RPM时,步进电机的电流为200mA左右,电机发热量较大,所以NPN三极管也可以选用9013,对于电机发热量大的问题,可加一个10欧到20欧的限流电阻,不过这样步进电机的功率将会变小。

    时间:2018-06-19 关键词: 51单片机 步进电机

  • 关于堆栈、静态、动态内存的理解

     预备知识—程序的内存分配 一个由C/C++编译的程序占用的内存分为以下几个部分 栈区(stack)— 由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。 堆区(heap) — 一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。 全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量、未初始化的静态变量在相邻的另一块区域。 - 程序结束后有系统释放 文字常量区 —常量字符串就是放在这里的。程序结束后由系统释放 程序代码区—存放函数体的二进制代码。 一个正常的程序在内存中通常分为程序段、数据端、堆栈三部分。程序段里放着程序的机器码、只读数据,这个段通常是只读,对它的写操作是非法的。数据段放的是程序中的静态数据。动态数据则通过堆栈来存放。 在内存中,它们的位置如下: +------------------+ 内存低端 | 程序段 | |------------------| | 数据段 | |------------------| | 堆栈 | +------------------+ 内存高端 堆栈是内存中的一个连续的块。一个叫堆栈指针的寄存器(SP)指向堆栈的栈顶。堆栈的底部是一个固定地址。堆栈有一个特点就是,后进先出。也就是说,后放入的数据第一个取出。它支持两个操作,PUSH和POP。PUSH是将数据放到栈的顶端,POP是将栈顶的数据取出。 在高级语言中,程序函数调用、函数中的临时变量都用到堆栈。为什么呢?因为在调用一个函数时,我们需要对当前的操作进行保护,也为了函数执行后,程序可以正确的找到地方继续执行,所以参数的传递和返回值也用到了堆栈。通常对局部变量的引用是通过给出它们对SP的偏移量来实现的。另外还有一个基址指针(FP,在Intel芯片中是BP),许多编译器实际上是用它来引用本地变量和参数的。通常,参数的相对FP的偏移是正的,局部变量是负的。 当程序中发生函数调用时,计算机做如下操作:首先把参数压入堆栈;然后保存指令寄存器(IP)中的内容,做为返回地址(RET);第三个放入堆栈的是基址寄存器(FP);然后把当前的栈指针(SP)拷贝到FP,做为新的基地址;最后为本地变量留出一定空间,把SP减去适当的数值。 在函数体中定义的变量通常是在栈上,用malloc, calloc, realloc等分配内存的函数分配得到的就是在堆上。在所有函数体外定义的是全局量,加了static修饰符后不管在哪里都存放在全局区(静态区),在所有函数体外定义的static变量表示在该文件中有效,不能extern到别的文件用;在函数体内定义的static表示只在该函数体内有效。另外,函数中的"adgfdf"这样的字符串存放在常量区。 对比: 1 性能 栈:栈存在于RAM中。栈是动态的,它的存储速度是第二快的。stack 堆:堆位于RAM中,是一个通用的内存池。所有的对象都存储在堆中。heap 2 申请方式 stack【栈】: 由系统自动分配。 例如,声明在函数中一个局部变量 int b; 系统自动在栈中为b开辟空间 。 heap【堆】: 需要程序员自己申请,并指明大小,在c中malloc函数 如p1 = (char *)malloc(10); 在C++中用new运算符 如p2 = (char *)malloc(10); 但是注意:p1、p2本身是在栈中的。 3 申请后系统的响应 栈【stack】:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。 堆【heap】:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序;另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。 4 申请大小的限制 栈【stack】:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。 堆【heap】:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。 5 申请效率的比较 栈【stack】:由系统自动分配,速度较快。但程序员是无法控制的。 堆【heap】:是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便. 另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,他不是在堆,也不是在栈是直接在进程的地址空间中保留一快内存,虽然用起来最不方便。但是速度快,也最灵活。 6 堆和栈中的存储内容 栈【stack】:在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。 当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。 堆【heap】:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。 7 存取效率的比较 char s1[] = "aaaaaaaaaaaaaaa"; char *s2 = "bbbbbbbbbbbbbbbbb"; aaaaaaaaaaa是在运行时刻赋值的; 而bbbbbbbbbbb是在编译时就确定的; 但是,在以后的存取中,在栈上的数组比指针所指向的字符串(例如堆)快。 比如: #include void main() { char a = 1; char c[] = "1234567890"; char *p ="1234567890"; a = c[1]; a = p[1]; return; } 对应的汇编代码 10: a = c[1]; 00401067 8A 4D F1 mov cl,byte ptr [ebp-0Fh] 0040106A 88 4D FC mov byte ptr [ebp-4],cl 11: a = p[1]; 0040106D 8B 55 EC mov edx,dword ptr [ebp-14h] 00401070 8A 42 01 mov al,byte ptr [edx+1] 00401073 88 45 FC mov byte ptr [ebp-4],al 第一种在读取时直接就把字符串中的元素读到寄存器cl中,而第二种则要先把指针值读到edx中,在根据edx读取字符,显然慢了。 小结: 堆和栈的区别可以用如下的比喻来看出: 使用栈就象我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。 使用堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。

    时间:2018-06-06 关键词: 内存 堆栈

  • 关于IAR的一些总结 -- Flash Loader原理

     先说说Flash Loader这个小程序,IAR调试器C-SPY默认是通过它来完成数据传输、Flash 擦除和烧写等任务,当然前提是得选中Options->Debugger->Download选项下的“Use Flash Loader”,如下图: 下面我们说说Flashloader的工作原理,其实Flashloader是IAR为C-SPY调试器开发的一个可执行小程序(有点类似Bootloader),IAR在调用调试器的时候需要先将Flashloader可执行文件下载到目标芯片的RAM中 图1 然后再将要下载的文件也放到RAM缓存中 图2 之后C-SPY通过指令启动Flashloader程序,它读取编程数据再将其写入到目标芯片的Flash之中去,完成目标芯片Flash的擦写和编程 图3 最后清除释放RAM区Flashloader程序和编程数据,启动新的程序运行。 图4 如上所示即为Flash loader的工作原理,其实仔细分析过来你会得到一个惊天秘密,即大多数在线下载工具在烧写目标芯片的flash时都是通过这种方式的,可能通过表象看到的是二进制文件是直接下载到flash中去了(无论是片上还是片外flash),但是真正的下载过程往往不是表面看上去那么简单。所以今天说说这个的一个原因除了发泄自己的兴奋之情外也是希望通过这么一个例子告诉大家深究某些东西原理的重要性。

    时间:2018-06-06 关键词: iar 调试

  • 你知道MCU和PC在代码加载和运行上有何区别吗?

     一、首先谈一下几种掉电不丢数据的存储设备: 1.Norflash:可擦写,贵,在Norflash上可以直接运行代码! 2.Nandflash:可擦写,便宜,只能用于存储数据; 3.磁盘:就是我们常说的硬盘,可擦写,便宜,只能用于存储数据; 二、正题 1.MCU 大多数单片机的代码都是存在Norflash里面,这就意味着程序可以直接在flash直接跑,不用加载到ram里面,而且单片机的ram本来就是比较稀缺的资源; 2.PC 先从电脑的BIOS说起,我们经常说,BIOS是一个ROM区,是一个只读的区域。其实BIOS并不绝对的“只读”,因为BIOS在现代几乎所有的电脑都是存在Norflash中,还是属于可以擦写的,至于BIOS存在ROM里是很久远之前的事了。 不同于BIOS,电脑的磁盘则只能用于存储,代码无法直接在上面跑,所以要运行代码,需要将代码从磁盘加载到ram里面,也就是我们通常说的内存条,然后在ram里面跑代码。

    时间:2018-06-06 关键词: pc MCU 程序

  • I2C上拉电阻解析

     I2C的上拉电阻可以是1.5K,2.2K,4.7K, 电阻的大小对时序有一定影响,对信号的上升时间和下降时间也有影响,一般接1.5K或2.2K I2C上拉电阻确定有一个计算公式: Rmin={Vdd(min)-o.4V}/3mA Rmax=(T/0.874) *c, T=1us 100KHz, T=0.3us 400KHz C是Bus capacitance Rp最大值由总线最大容限(Cbmax)决定,Rp最小值由Vio与上拉驱动电流(最大取3mA)决定; 于是 Rpmin=5V/3mA≈1.7K(@Vio=5V)或者2.8V/3mA≈1K(@Vio=2.8V) Rpmax的取值:参考周公的I2C总线规范中文版P33图39与P35图44 总的来说:电源电压限制了上拉电阻的最小值 ; 负载电容(总线电容)限制了上拉电阻的最大值 补充:在I2c总线可以串连300欧姆电阻RS可以用于防止SDA和SCL线的高电压毛刺 : I2c从设备的数量受总线电容,<=400pF的限制 做过I2C碰到过各种问题,多半是上拉电阻或者控制器时钟的问题。没上拉电阻或者上拉电阻过大,都会导致不稳定而出现寻址不到的问题。控制器时钟主频的话,主频667M八分频就可以

    时间:2018-06-06 关键词: i2c 上拉电阻

  • 单片机startup.a51文件内容的详解

     1.标号 IDATALEN EQU 80H ; the length of IDATA memory in bytes. 这里IDATALEN只是一个标号而已,和idata不是一回事!你要是愿意,这段程序里的IDATALEN你完全可以改成dog呀,pig呀,playboy呀这些标号(其实我的理想是过猪一样的生活,不愁吃喝,无忧无虑,可惜做不到),上面的这一句是说程序里面凡是用到IDATALEN的地方其实就是可以看成是80H这个数,你用80H去代替IDATALEN是完全对的。 之所以取IDATALEN这么个名字,只是为了好记,表明和idata有一点点关系,不至于你的程序长了,假使你本来是用了playboy作为标号的,写到后来你就会忘了playboy到底是什么含义了。idata的范围是0~FFH。如果你想改成FFH,完全可以。 2。清零 IF IDATALEN <> 0 MOV R0,#IDATALEN - 1 CLR A IDATALOOP: MOV @R0,A DJNZ R0,IDATALOOP 关于这一段,很明显是在清零,如果上面idatalen=80H,那么是对0~7FH清零;如果 你的程序是改写成: IDATALEN EQU 0100H ; 就是对0~FFH清零。 还要注意的是那条IF语句,下面再谈。 二、如何按你意愿加载这段程序 一般考虑到这个往往是你的设计中要区分上电复位和程序复位。有时候当程序复位时你不希望一些内存单元被清零了,那么你不对startup.a51作点修改,就不行了。 默认是自动加载这段startup.a51的。 所以你要这样做: 把lib目录下的原始startup.a51文件拷到你的项目所在目录下,再把你项目目录下的这个startup.a51加入到你的项目中(在keil的集成环境中,希望你对这个是知道如何做的),然后对这个startup.a51加以修改。 比如改成: IDATALEN EQU 00H ; the length of IDATA memory in bytes. 然后编译链接。这样你的程序中就不会包含对idata清零的内码了。 为什么?上面提到的IF语句的作用呀!当定义IDATALEN=0时,清零代码被跳过! //////////////////////////////////////////// Startup.a51的中文说明 ;------------------------------------------------------------------------------ ; STARTUP.A51: 用户上电初始化程序 ;------------------------------------------------------------------------------ ; ; 用户定义需上电初始化的内存空间 ; ; 使用以下EQU命令可定义在CPU复位时需用0进行初始化的内存空间 ; ;; ; IDATA 存储器的空间的绝对起始地址总是0.; IDATALEN EQU 80H ; 需用0进行初始化的IDATA存储器空间的字节数 ; XDATASTART EQU 0H ; XDATA存储器空间的绝对起始地址 XDATALEN EQU 0H ; 需用0进行初始化的XDATA存储器的空间字节数. ; PDATASTART EQU 0H ; PDATA存储器的空间的绝对起始地址 PDATALEN EQU 0H ; 需用0进行初始化的PDATA存储器的空间字节数. ; ; 注意: IDATA 存储器的空间在物理上包括了8051单片机的DATA和BIT存储器空间. ; 听 说 至少要保证与C51编译器运行库有关的存储器的空间进行0初始化 不知是否 ;------------------------------------------------------------------------------ ; ; 再入函数模拟初始化 ; ; 以下用EQU指令定义了再入函数模拟堆栈指针的初始化 ; ; 使用SMALL存储器模式时再入函数的堆栈空间 . IBPSTACK EQU 0 ; 使用SMALL存储器模式再入函数时将其设置成1. IBPSTACKTOP EQU 0FFH+1 ; 将堆栈顶设置为最高地址+1. ; ; 使用LARGE存储器模式时再入函数的堆栈空间.; 使用LARGE存储器模式时再入函数的堆栈空间. XBPSTACK EQU 0 ; 使用LARGE存储器模式再入函数时将其设置成1. XBPSTACKTOP EQU 0FFFFH+1; 将堆栈顶设置为最高地址+1. ; ; 使用COMPACT存储器模式时再入函数的堆栈空间.; 使用COMPACT存储器模式时再入函数的堆栈空间. PBPSTACK EQU 0 ; 使用COMPACT存储器模式再入函数时将其设置成1. PBPSTACKTOP EQU 0FFFFH+1; 将堆栈顶设置为最高地址+1. ; ;------------------------------------------------------------------------------ ; ; 使用COMPACT存储器模式时64K字节XDATA存储器空间的分页定义 ; ; 以下用EQU指令定义PDATA类型变量在XDATA存储器空间的页地址 ; 使用EQU指令定义PFAGE时必须与L51连接定位器PDATA指令的控制参数一致 ; PPAGEENABLE EQU 0 ; 使用PDATA类型变量时将其设置成1. PPAGE EQU 0 ; 定义页号. ; ;------------------------------------------------------------------------------ NAME ?C_STARTUP; 模块名为 ?C_STAUTUP ?C_C51STARTUP SEGMENT CODE ; 代码 ?STACK SEGMENT IDATA ; 堆栈 RSEG ?STACK ; 堆栈 DS 1 EXTRNEXTRN CODE ((?C_START)) ; 程序开始地址 PUBLIC ?C_STARTUP CSEG AT 0x8000 ; 定义用户程序的起始地址,用MON51仿真器时可能有用 ?C_STARTUP: LJMP STARTUP1 RSEG ?C_C51STARTUP STARTUP1:: ; ; 初始化串口 MOV SCON,#40H MOV TMOD,#20H MOV TH1,#0fdH SETB TR1 CLR TI ; 单片机上电IDATA内存清零 如果不需要上电清零IDATA 可以注销IF到IFEDN之间的话句 ; 或者修改IDTALEN的长度 为了具有掉电保护功能 不知IDTALEN多长为好 IF IDATALEN <> 0 MOV R0,#IDATALEN - 1 CLR A IDATALOOP: MOV @R0,A DJNZ R0,IDATALOOP ENDIF ; ; 单片机上电XDATA内存清零 如果不需要上电清零XDATA 可以注销IF到IFEDN之间的话句 ; 或者修改XDATALEN的长度 IF XDATALEN <> 0 MOV DPTR,#XDATASTART MOV R7,#LOW ((XDATALEN) IF (LOW (XDATALEN) <> 0 MOV R6,#(HIGH (XDATALEN) +1 ELSE MOV R6,,#HIGH ((XDATALEN) ENDIF CLR A XDATALOOP: MOVX @DPTR,A INC DPTR DJNZ R7,XDATALOOP DJNZ R6,XDATALOOP ENDIF ; ; 送PDATA存储器页面高位地址 IF PAGEENABLE <> 0 MOV P2,#PPAGE ENDIF ; ; 单片机上电PDATA内存清零 如果不需要上电清零XDATA 可以注销IF到IFEDN之间的话句 ; 或者修改PDATALEN的长度 IF PDATALEN <> 0 MOV R0,#PDATASTART MOV R7,#LOW (PDATALEN) CLR A PDATALOOP: MOVX @R0,A INC R0 DJNZ R7,PDATALOOP ENDIF ; ; 设置使用SMALL存储器模式时再入函数的堆栈空间. IF IBPSTACK <> 0 EXTRN DATA (?C_IBP) MOV ?C_IBP,#LOW IBPSTACKTOP ENDIF ; ; 设置使用LARGE存储器模式时再入函数的堆栈空间. IF XBPSTACK <> 0 EXTRN DATA (?C_XBP) MOV ?C_XBP,#HIGH XBPSTACKTOP MOV ?C_XBP+1,#LOW XBPSTACKTOP ENDIF ; ; 设置使用COMPACT存储器模式时再入函数的堆栈空间. IF PBPSTACK <> 0 EXTRN DATA (C_PBP) MOV ?C_PBP,#LOW PBPSTACKTOP ENDIF ; ; 设置堆栈的起始地址 MOV SP,#?STACK-1 ; 例如 MOV SP,#4FH; ; This code is required if you use L51_BANK.A51 with Banking Mode 4 ; 如果你的程序使用了Mode 4 程序分组技术 请启动下面的程序,不会吧你的程序超过64K 利害 ; EXTRN CODE (?B_SWITCH0) ; CALL ?B_SWITCH0 ; init bank mechanism to code bank 0 ;; 程序从第一组bank 0 块开始执行 ; 跳转到用户程序MAIN函数 LJMP ?C_START END ;lINSHENGFENG

    时间:2018-06-06 关键词: 单片机 startup.a51

  • 硬件工程师对于程序空间的理解

     在硬件工程师和普通用户看来,内存就是插在或固化在主板上的内存条,它们有一定的容量——比如64 MB。但在应用程序员眼中,并不过度关心插在主板上的内存容量,而是他们可以使用的内存空间——他们可以开发一个需要占用1 GB内存的程序,并让其在OS平台上运行,哪怕这台运行主机上只有128 MB的物理内存条。而对于OS开发者而言,则是介于二者之间,他们既需要知道物理内存的细节,也需要提供一套机制,为应用程序员提供另一个内存空间,这个内存空间的大小可以和实际的物理内存大小之间没有任何关系。 我们将主板上的物理内存条所提供的内存空间定义为物理内存空间;将应用程序员看到的内存空间定义为线性空间。物理内存空间大小在不同的主机上可以是不一样的,随着主板上所插的物理内存条的容量不同而不同;但为应用程序员提供的线性空间却是固定的,不会随物理内存的变化而变化,这样才能保证应用程序的可移植性。尽管物理内存的大小可以影响应用程序运行的性能,并且很多情况下对物理内存的大小有一个最低要求,但这些因素只是为了让一个OS可以正常的运行。 线性空间的大小在32-bit平台上为4 GB的固定大小,对于每个进程都是这样(一个应用可以是多进程的,在OS眼中,是以进程为单位的)。也就是说线性空间不是进程共享的,而是进程隔离的,每个进程都有相同大小的4 GB线性空间。一个进程对于某一个内存地址的访问,与其它进程对于同一内存地址的访问绝不冲突。比如,一个进程读取线性空间地址1234ABCDh可以读出整数8,而另外一个进程读取线性空间地址1234ABCDh可以读出整数20,这取决于进程自身的逻辑。 在任意一个时刻,在一个CPU上只有一个进程在运行。所以对于此CPU来讲,在这一时刻,整个系统只存在一个线性空间,这个线性空间是面向此进程的。当进程发生切换的时候,线性空间也随着切换。所以结论就是每个进程都有自己的线性空间,只有此进程运行的时候,其线性空间才被运行它的CPU所知。在其它时刻,其线性空间对于CPU来说,是不可知的。所以尽管每个进程都可以有4 GB的线性空间,但在CPU眼中,只有一个线性空间的存在。线性空间的变化,随着进程切换而变化。 尽管线性空间的大小和物理内存的大小之间没有任何关系,但使用线性空间的应用程序最终还是要运行在物理内存中。应用所给出的任何线性地址最终必须被转化为物理地址,才能够真正的访问物理内存。所以,线性内存空间必须被映射到物理内存空间中,这个映射关系需要通过使用硬件体系结构所规定的数据结构来建立。我们不妨先称其为映射表。一个映射表的内容就是某个线性内存空间和物理内存空间之间的映射关系。OS Kernel一旦告诉某个CPU一个映射表的位置,那么这个CPU需要去访问一个线性空间地址时,就根据这张映射表的内容,将这个线性空间地址转化为物理空间地址,并将此物理地址送到地址线,毕竟地址线只知道物理地址。 所以,我们很容易得出一个结论,如果我们给出不同的映射表,那么CPU将某一线性空间地址转化的物理地址也会不同。所以我们为每一个进程都建立一张映射表,将每个进程的线性空间根据自己的需要映射到物理空间上。既然某一时刻在某一CPU上只能有一个应用在运行,那么当任务发生切换的时候,将映射表也更换为响应的映射表就可以实现每个进程都有自己的线性空间而互不影响。所以,在任意时刻,对于一个CPU来说,也只需要有一张映射表,以实现当前进程的线性空间到物理空间的转化。 -------------------------------------------------------------------------------- 2. OS Kernel Space & Process Space 由于OS Kernel在任意时刻都必须存在于内存中,而进程却可以切换,所以在任意时刻,内存中都存在两部分,OS Kernel和用户进程。而在任意时刻,对于一个CPU来说只存在一个线性空间,所以这个线性空间必须被分成两部分,一部分供OS Kernel使用,另一部分供用户进程使用。既然OS Kernel在任何时候都占用线性空间中的一部分,那么对于所有进程的线性空间而言,它们为OS Kernel所留出的线性空间可以是完全相同的,也就是说,它们各自的映射表中,也分为两部分,一部分是进程私有映射部分,对于OS Kernel映射部分的内容则完全相同。 从这个意义上来说,我们可以认为,对于所有的进程而言,它们共享OS Kernel所占用的线性空间部分,而每个进程又各自有自己私有的线性空间部分。假如,我们将任意一个4 GB线性空间分割为1 GB的OS Kernel空间部分和3 GB的进程空间部分,那么所有进程的4 GB线性空间中1 GB的OS Kernel空间是共享的,而剩余的3 GB进程空间部分则是各个进程私有的。Linux就是这么做的,而Windows NT则是让OS Kernel和进程各使用2 GB线性空间。 -------------------------------------------------------------------------------- 3. Segment Mapping & Page Mapping 所有的线性空间的内容只有被放置到物理内存中才能够被真正的运行和操作。所以,尽管OS Kernel和进程都被放在线性空间中,但它们最终必须被放置到物理内存中。所以OS Kernel和所有的进程都最终共享物理内存。在现阶段,物理内存远没有线性空间那么大——线性空间是4 GB,而物理内存空间往往只有几百兆,甚至更小。另外即使物理内存有4 GB,但由于每个进程都可以有3 GB线性空间(假如进程私有线性空间是3 GB的话),如果把所有进程的线性空间内容都放在物理内存中,明显是不现实的。所以OS Kernel必须将某些进程暂时用不到的数据或代码放在物理内存之外,将有限的内存提供给当前最需要的进程。另外,由于OS Kernel在任何时候都有可能运行,所以OS Kernel最好被永远放在物理内存中。我们仅仅将进程数据进行换入换出。 从线性空间到物理空间的映射需要映射表,映射表的内容是将某段线性空间映射到相同大小的物理内存空间上。从理论上,我们可以使用两种映射方法:变长映射,和定长映射。变长映射指的是根据不同的需要,将一个一个变长段映射到物理内存上,其格式可以如下(线性空间段起始地址,物理空间段起始地址,段长度)。假如一个进程有3个段:10M的数据段,5M的代码段,和8K的堆栈段,那么就可以在映射表中建立3项内容,每一项针对一个段。这看起来没有问题。但假如现在我们的实际的内存只有32M,其中10M被内核占用,留给进程的物理空间只有22M,那么此进程在运行时,就占据了10M+5M+8K的内存空间。随后当进程发生切换时,假如另一个进程和其有相同的内存要求,那么剩余的22M-(10M+5M+8K)明显就不够用了,这时只能将原进程的某些段换出,并且必须是整段的换出。这就意味着我们必须至少换出一个10M的数据段,而换出的成本很高,因为我们必须将这10M的内容拷贝到磁盘上,磁盘I/O是很慢的。 所以,使用变长的段映射的结果就是一个段要么被全部换入,要么被全部换出。但在现实中,一个程序中并非所有的代码和数据都能够被经常访问,往往被经常访问的只占全部代码数据的一部分,甚至是一小部分。所以更有效的策略是我们最好只换出那些并不经常使用的部分,而保留那些经常被使用的部分。而不是整个段的换入换出。这样可以避免大块的慢速磁盘操作。 这就是定长映射策略,我们将内存空间分割为一个个定长块,每个定长块被称为一个页。映射表的基本格式为(物理空间页起始地址),由于页是定长的,所以不需要指出它的长度,另外,我们不需要在映射表中指定线性地址,我们可以将线性地址作为索引,到映射表中检索出相应的物理地址。当使用页时,其策略为:当换出的时候,我们只将那些不活跃的,也就是不经常使用的页换出,而保留那些活跃的页。在换入的时候,只有被请求访问的页才被换入,没有被请求访问的页将永远不会被换入到物理内存。这就是请求页(Demand Page)算法的核心思想。 这就引出一个页大小的问题:首先我们不可能以字节为单位,这样映射表的大小和线性空间大小相同——假如整个线性空间都被映射的话——我们不可能将全部线性空间用作存放这个映射表。由此,我们也可以得知,页越小,则映射表的容量越大。而我们不能让映射表占用太多的空间。但如果页太大,则面临着和不定长段映射同样的问题,每次换出一个页,都需要大量的磁盘操作。另外,由于为一个进程分配内存的最小单位是页,假如我们的页大小为4 MB,那么即使一个进程只需要使用4 KB的内存,也不得不占用整个4 MB页,这明显是一种很大的浪费。所以我们必须在两者之间进行折衷,一般平台所规定的页大小为1 KB到8 KB,IA-32所规定的页大小为4 KB。(IA-32也支持4 MB页,你可以根据你的OS的用途进行选择,一般都是使用4 KB页)。

    时间:2018-06-06 关键词: 硬件工程师 程序空间

  • 单片机STARTUP.A51详解

     Startup code:启动代码。在Keil中,启动代码在复位目标系统后立即被执行。启动代码主要实现以下功能: (1) 清除内部数据存储器 (2) 清除外部数据存储器 (3) 清除外部页存储器 (4) 初始化small模式下的可重入栈和指针 (5) 初始化large模式下的可重入栈和指针 (6) 初始化compact模式下的可重入栈和指针 (7) 初始化8051硬件栈指针 (8) 传递初始化全局变量的控制命令或者在没有初始化全局变量时给main函数传递命令。 在每一个启动文件中,提供了可供用户自己修改有来控制程序执行的汇编常量。见表1 表1 Name Description IDATALEN Specifies the number of bytes of idata to clear to 0. The default is 80h because most 8051 derivatives contain at least 128 bytes of internal data memory. Use a value of 100h for the 8052 and other derivatives that have 256 bytes of internal data memory. XDATASTART Specifies the initial xdata address to clear to 0. XDATALEN Indicates the number of bytes of xdata to clear to 0. The default is 0. PDATASTART Specifies the initial pdata address to clear to 0. PDATALEN Specifies the number of bytes of pdata to clear to 0. The default is 0. IBPSTACK Specifies whether or not the small model reentrant stack pointer (?C_IBP) should be initialized. A value of 1 causes this pointer to be initialized. A value of 0 prevents initialization of this pointer. The default is 0. IBPSTACKTOP Specifies the top of the small model reentrant stack. The default is 0xFF in idata memory. The Cx51 Compiler does not check to see if the stack area available satisfies the requirements of the application. It is your responsibility to perform such a test. XBPSTACK Specifies whether or not the large model reentrant stack pointer (?C_XBP) should be initialized. A value of 1 causes this pointer to be initialized. A value of 0 prevents initialization of this pointer. The default is 0. XBPSTACKTOP Specifies the top of the large model reentrant stack. The default is 0xFFFF in xdata memory. The Cx51 Compiler does not check to see if the stack area available satisfies the requirements of the application. It is your responsibility to perform such a test. PBPSTACK Specifies whether the compact model reentrant stack pointer (?C_PBP) should be initialized. A value of 1 causes this pointer to be initialized. A value of 0 prevents initialization of this pointer. The default is 0. PBPSTACKTOP Specifies the top of the compact model reentrant stack. The default is 0xFF in pdata memory. The Cx51 Compiler does not check to see if the stack area available satisfies the requirements of the application. It is your responsibility to perform such a test. PPAGEENABLE Enables (a value of 1) or disables (a value of 0) Port 2 initialization for pdata memory access. The default is 0. pdata addressing uses Port 2 for the upper address (or page) byte. PPAGE Specifies the value to write to Port 2 of the 8051 for pdata memory access. This value represents the xdata memory page to use for pdata. This is the upper 8 bits of the absolute address range to use for pdata. For example, if the pdata area begins at address 1000h (page 10h) in xdata memory, PPAGEENABLE should be set to 1, and PPAGE should be set to 10h. You must specify the starting pdata address to use to the BL51 Linker using the PDATA directive. For example: BL51 input modules PDATA (1050H) Neither the BL51 Linker nor the Cx51 Compiler checks to see if the PDATA directive and the PPAGE startup constant are correctly specified. You must ensure that these parameters contain suitable values. 上面这些只是标号,如果愿意,自己可以换成其他的名字。这样写意义更直观。 $NOMOD51 ;不使用预先定义的SFR,The NOMOD51 directive suppresses pre-definition of 8051 SFR ;names. This directive must be used when a customer-specific SFR definition file is included. ;------------------------------------------------------------------------------ ; This file is part of the C51 Compiler package ; Copyright (c) 1988-2005 Keil Elektronik GmbH and Keil Software, Inc. ; Version 8.01 ; ; *** <<< Use Configuration Wizard in Context Menu >>> *** ;------------------------------------------------------------------------------ ; STARTUP.A51: This code is executed after processor reset. ;代码在处理器复位后执行 ; To translate this file use A51 with the following invocation: ;通过A51用下面的命令传递该文件 ; A51 STARTUP.A51 ; ; To link the modified STARTUP.OBJ file to your application use the following ; Lx51 invocation: ;利用下面Lx51命令来链接修改的STARTUP.OBJ文件到实际的应用 ; Lx51 your object file list, STARTUP.OBJ controls ; ;------------------------------------------------------------------------------ ; ; User-defined Power-On Initialization of Memory ;用户自定义上电初始化内存 ; With the following EQU statements the initialization of memory ; at processor reset can be defined: ;在处理器复位时通过EQU来初始化内存 ; IDATALEN: IDATA memory size <0x0-0x100> ;IDATALEN:IDATA存储区的大小<0-256>,可以根据自己的选择修改 ; Note: The absolute start-address of IDATA memory is always 0 ; The IDATA space overlaps physically the DATA and BIT areas. ;IDATA绝对的起始地址总是0 ;IDATA空间涵盖物理上的DATA和BIT区 ;需用0来初始化idata区的字节数 IDATALEN EQU 80H ; ; XDATASTART: XDATA memory start address <0x0-0xFFFF> ; The absolute start address of XDATA memory ;XDATA存储区的起始地址,XDATA内存的绝对起始地址。 XDATASTART EQU 0 ;指定初始的XDATA地址清0 ; XDATALEN: XDATA memory size <0x0-0xFFFF> ; The length of XDATA memory in bytes. ;XDATA空间的长度,以字节为单位 XDATALEN EQU 0 ;说明xdata的字节数清0,该值默认为0 ; PDATASTART: PDATA memory start address <0x0-0xFFFF> ; The absolute start address of PDATA memory PDATASTART EQU 0H ; ; PDATALEN: PDATA memory size <0x0-0xFF> ; The length of PDATA memory in bytes. PDATALEN EQU 0H ; ; ;------------------------------------------------------------------------------ ; ; Reentrant Stack Initialization ;重入栈的初始化 ; The following EQU statements define the stack pointer for reentrant ; functions and initialized it: ;EQU语句定义了重入函数的栈指针并初始化 ; Stack Space for reentrant functions in the SMALL model. ;SMALL模式下的重入函数栈 ; IBPSTACK: Enable SMALL model reentrant stack ; IBPSTACK = 1使能模拟栈 ; Stack space for reentrant functions in the SMALL model. IBPSTACK EQU 0 ; set to 1 if small reentrant is used. ; IBPSTACKTOP: End address of SMALL model stack <0x0-0xFF> ; IBPSTACKTOP:栈的结束地址<0x0-0xFF> ; Set the top of the stack to the highest location. ;设置为最高地址(向下生长的) ;默认FFH+1(其实就是0,使用时进行操作(+0xFF)使其变为0xFF) IBPSTACKTOP EQU 0xFF +1 ; default 0FFH+1 ; ;下面是LARGE模式下的模拟栈,和SMALL相同不做特别说明 ; Stack Space for reentrant functions in the LARGE model. ; XBPSTACK: Enable LARGE model reentrant stack ; Stack space for reentrant functions in the LARGE model. XBPSTACK EQU 0 ; set to 1 if large reentrant is used. ; XBPSTACKTOP: End address of LARGE model stack <0x0-0xFFFF> ; Set the top of the stack to the highest location. XBPSTACKTOP EQU 0xFFFF +1 ; default 0FFFFH+1 ; ;COMPACT模式下 ; Stack Space for reentrant functions in the COMPACT model. ; PBPSTACK: Enable COMPACT model reentrant stack ; Stack space for reentrant functions in the COMPACT model. PBPSTACK EQU 0 ; set to 1 if compact reentrant is used. ; ; PBPSTACKTOP: End address of COMPACT model stack <0x0-0xFFFF> ; Set the top of the stack to the highest location. PBPSTACKTOP EQU 0xFF +1 ; default 0FFH+1 ; ; ;------------------------------------------------------------------------------ ;COMPACT模式下64K xdata的页存储器 ; Memory Page for Using the Compact Model with 64 KByte xdata RAM ; Compact Model Page Definition ;通过PDATA变量定义XDATA页 ; Define the XDATA page used for PDATA variables. ; PPAGE must conform with the PPAGE set in the linker invocation. ;PPAGE必须与链接器引用设置一致 ; Enable pdata memory page initalization PPAGEENABLE EQU 0 ; set to 1 if pdata object are used. ;设置1使能 ; PPAGE number <0x0-0xFF> ; uppermost 256-byte address of the page used for PDATA variables. ;页号,PDATA变量高256字节地址(高8位地址) PPAGE EQU 0 ; ; SFR address which supplies uppermost address byte <0x0-0xFF> ; most 8051 variants use P2 as uppermost address byte ;提供高字节地址的SFR的地址(高8位地址的存放位置),一般的8051单片机用P2口 PPAGE_SFR DATA 0A0H ; ; ;------------------------------------------------------------------------------ ; Standard SFR Symbols ;使用伪指令DATA为寄存器分配地址或者为响应的地址起一个别名 ACC DATA 0E0H B DATA 0F0H SP DATA 81H DPL DATA 82H DPH DATA 83H NAME ?C_STARTUP ;定义当前模块的目标模块名 ;NAME: ; NAME (modulename) ;NAME为输出文件定义一个模块名称(modulename),如果在文件中没有特定的 ; (modulename),默认的应用第一个输入文件的名称。 ?C_C51STARTUP SEGMENT CODE ;定义一个代码段,名称?C_C51STARTUP ?STACK SEGMENT IDATA ;堆栈 RSEG ?STACK ;RSEG选择一个先前声明的可重定位的段 DS 1 ;为堆栈预留一个低阶的存储空间 EXTRN CODE (?C_START) ;当前源文件中用的代码段存储区的符号?C_START,在其他的目标模块中定义 PUBLIC ?C_STARTUP ;声明可以用于其他目标模块的全局符号?C_STARTUP,用于和C相连接在.src文件中可以看到这个符号 CSEG AT 0 ;选择代码存储区内的一个绝对段,汇编从上面命令中的地址0开始执行这个段。 ?C_STARTUP: LJMP STARTUP1 ;芯片上电复位后,执行的第一句就是该句,该句往下是开始执行启动代码 RSEG ?C_C51STARTUP ;选择代码段?C_C51STARTUP STARTUP1: IF IDATALEN <> 0 ;如果IDATALEN不为0,则将长度-1送R0 MOV R0,#IDATALEN - 1 CLR A IDATALOOP: MOV @R0,A ;将IDATA区清0 DJNZ R0,IDATALOOP ENDIF IF XDATALEN <> 0 ;如果有外部数据存储区 MOV DPTR,#XDATASTART ;起始地址送DPTR MOV R7,#LOW (XDATALEN) ;长度低8为送R7 IF (LOW (XDATALEN)) <> 0 ;低8位不为0 MOV R6,#(HIGH (XDATALEN)) +1 ;高8位+1送R6,下面清0用 ELSE MOV R6,#HIGH (XDATALEN) ENDIF CLR A ;清0 XDATALOOP: MOVX @DPTR,A INC DPTR DJNZ R7,XDATALOOP DJNZ R6,XDATALOOP ENDIF IF PPAGEENABLE <> 0 ;使能外部页编址 MOV PPAGE_SFR,#PPAGE ;页号送SFR ENDIF IF PDATALEN <> 0 ;0-FF之间 MOV R0,#LOW (PDATASTART) MOV R7,#LOW (PDATALEN) CLR A PDATALOOP: MOVX @R0,A ;清0 INC R0 DJNZ R7,PDATALOOP ENDIF IF IBPSTACK <> 0 ;使能SMALL模式下的模拟栈 EXTRN DATA (?C_IBP) ;使用其他目标模块中定义的?C_IBP(模拟栈指针) ;(.M51文件中) MOV ?C_IBP,#LOW IBPSTACKTOP ;模拟栈指针指向栈顶 ENDIF IF XBPSTACK <> 0 ;Large模式下使能模拟栈 EXTRN DATA (?C_XBP) ;栈指针指向栈顶 MOV ?C_XBP,#HIGH XBPSTACKTOP MOV ?C_XBP+1,#LOW XBPSTACKTOP ENDIF IF PBPSTACK <> 0 ;COMPACT模式下的模拟栈 EXTRN DATA (?C_PBP) MOV ?C_PBP,#LOW PBPSTACKTOP ;指向栈顶 ENDIF MOV SP,#?STACK-1 ;硬件栈SP赋值 ;硬件栈与模拟栈相区别,硬件栈是向上的,模拟栈是向下的 ;在M51中可以看到具体的?STACK的值 ;模拟栈的指针指向所在模式下,存储区最大地址+1(0xFF+1或0xFFFF+1) ;硬件栈初始化指向栈底-1; ;因为C51中栈操作是先操作栈指针(加或减),然后在写数 ; This code is required if you use L51_BANK.A51 with Banking Mode 4 ; Code Banking ; Select Bank 0 for L51_BANK.A51 Mode 4 #if 0 ; Initialize bank mechanism to code bank 0 when using L51_BANK.A51 with Banking Mode 4. EXTRN CODE (?B_SWITCH0) CALL ?B_SWITCH0 ; init bank mechanism to code bank 0 #endif ; LJMP ?C_START ;执行main函数 END ;该文件中的一些符号的值的大小,可以在M51中看到,此外,可以通过系统上电复位后反 ;汇编程序看到详细的执行过程 ;文件中的一些命令可以查阅KeilC帮助文档

    时间:2018-06-06 关键词: 单片机

  • 程序的思考(从单片机到PC)

     关于程序的执行,以前想的不多,没有意识到一个程序在运行时,从哪里读指令,数据又写在哪里。 最近在看CSAPP时这个念头经常在脑袋中晃荡。 从单片机上知道,在上电的那一刻,MCU的程序指针PC会被初始化为上电复位时的地址,从哪个地址处读取将要执行的指令,由此程序在MCU上开始执行(当然在调用程序的 main之前,还有一系列其他的的初始化要做,如堆栈的初始化,不过这些我们很少回去修改)。PC在上电时,和MCU差不多,不过读取的是BIOS,有它完成了很多初始化操作,最后,调用系统的初始化函数,将控制权交给了操作系统,于是我们看到了Windows,Linux系统启动了。如果将操作系统看作是在处理器上跑的一个很大的裸机程序(就是直接在硬件上跑的程序,因为操作系统就是直接跑在CPU上的,这样看待是可以的,不过这个裸机程序功能很多,很强大),那么操作系统的启动很像MCU程序的启动。前者有一个很大的初始化程序完成很复杂的初始化,后者有一段不长的汇编代码完成一些简单的初始化。这一点看,它们在流程上是很相似的。 如果是系统上的程序启动呢?它们是由系统来决定的。Linux上在shell下输入./p后,首先检查是否是一个内建的shell命令;如果不是,则shell假设他是一个可执行文件(Linux上一般是elf格式),然后调用一些相关的函数,将在硬盘上的p文件的内容拷贝到内存(DDR RAM)中,并建立一个它的运行环境(当然这里边还有内存映射,虚拟内存,连接与加载,等一些其他东西),准备执行。 由以上可知,单片机上的程序和平时在系统上运行的程序,在启动时差异是很大的(如果将程序调用main以前的动作,都抽象为初始化的话,程序的启动可以简化为:建立运行环境+调用main函数,这样程序的执行差异是不大的)。因为单片机上跑的程序(裸机程序),是和操作系统一样跑在硬件上的,它们属于一个层次的。过去之所以没有区分出单片机上的程序和PC机上的程序的一些差异,就是没有弄明白这一点。 由此,以前的一些疑惑也就解开了。为什么在单片机上的程序不怎么使用malloc,而PC上经常使用?因为单片机上没有已经写好的内存管理算法的代码,而在PC上操作系统里运行的程序,libc已经把这些都做了,只需要调用就可以了。如果在单片机上想用动态内存,也可以,但是这些代码要自己去实现,并定义一个相应的malloc,有时候一些公司会给提供一些库函数可能会实现malloc,但是因为单片机上RAM内存十分有限,如果不知道它的运行方式,估计会很危险。同样,因为在PC的系统上运行的程序与逻机程序的不同,裸机程序不会有动态链接,有的只是静态链接。 关于程序在执行时,从哪里读取指令,哪里读取数据,也曾因为没有弄清楚系统上的程序和裸机程序之间的区别,而疑惑了很久。虽然在《微型计算机原理》课上知道程序运行时,从内存中读取指令和数据进行执行和回写。但是单片机上只有几K的RAM,而flash一般有几十K甚至1M,这个时候指令和数据都在内存中吗(这里指的内存仅指RAM,因为PC上我们常说的内存就是DDR RAM memory,先入为主以至于认为单片机上也是这样,还没有明白其实RAM和Flash都是内存)?这不可能,因为课上老师只说内存,但是PC上内存一般就是DDR RAM,不会是硬盘,硬盘是保存数据的地方;由此类比时,自己把自己弄晕菜了,单片机的RAM对应于DDR RAM,那Flash是不是就对应于硬盘了呢?在CSAPP上明白了,PC上之所以都在DDR RAM上,是速度的因素。硬盘的速度太慢,即使是即将到来的SSD比起DDRRAM,还是差着几个数量级,所以拷贝到DDRRAM中。这时,一个程序的代码和数据是连续存放的,其中代码段是只读区域,数据段是可读写区域(这是由操作系统的内存管理机制决定的)。运行时,再将它们拷贝到速度更快的SRAM中,以得到更快的执行速度。而对于,单片机而言工作频率也就几M,几十M,从Flash中与从RAM中读的差异可能并不明显,不会成为程序执行的瓶颈(而对于PC而言,Flash的速度太慢,DDRRAM的速度也是很慢,即使是SRAM也是慢了不少,于是再提高工作频率也提高不了程序的执行速度,所以现在CPU工作频率最快是在2003左右。一个瓶颈出现了。为了提高CPU的使用率,换个角度想一下,既然不能减少一段程序的执行时间,就在同样的时间执行更多的程序,一个核执行一段程序,两个核就可以执行两段程序,于是多核CPU成为了现在的主流)。所以裸机程序指令就在Flash(Flash memory)中存放,而数据就放在了RAM中(flash的写入次数有限制,同时它的速度和RAM还是差很多)。更广泛说,在单片机上RAM存放data段,bss段,堆栈段;ROM(EPROM,EEPROM,Flash等非易失性存储设备)存放代码,只读数据段。本质上说,这和PC上程序都在RAM中存放是一样的,PC 上是操作系统规定了可读与可写,而单片机上是依靠不同的存储设备区分了可读与可写(当然现在的Flash是可读写的,如果Flash没有写入次数限制,速度又可以和RAM相差不多,单片机上是不是只要Flash就可以了呢(直接相当于PC上的DDRRAM)?这样成本也会比一个RAM,一个Flash低,更节省成本,对于生产商更划算)。 对于单片机的程序执行时指令和数据的存放与读取,理解如下: 对单片机编程后,程序的代码段,data段,bss段,rodata段等都存放在Flash中。当单片机上电后,初始化汇编代码将data段,bss段,复制到RAM中,并建立好堆栈,开始调用程序的main函数。以后,便有了程序存储器,和数据存储器之分,运行时从Flash(即指令存储器,代码存储器)中读取指令 ,从RAM中读取与写入数据。RAM存在的意义就在于速度更快。 无论是单片机也好,PC也罢,存在的存储器金字塔都是一致的,速度的因素,成本的限制导致了一级级更快的存储器的更快速度与更高的成本。应该说,对于程序执行的理解,就是存储器金字塔的理解。 注: 那么,什么是RAM,ROM和Flash呢?尽管他们都是计算机内存的一种形式,但是RAM,ROM,FLASH它们三个都以各自的方式和他们存储的数据进行交互。下面对每种内存有一个简短的说明。 RAM:表示随机访问内存(random access memory):微处理器可以读写访问的内存。当我们创建一些东西时,它是在内存中完成的。RAM是内存,反之亦然。 ROM:表示只读内存:微处理器可以读ROM,但是不能写入或修改。ROM是永久性的。ROM芯片经常保存一些重要且永不改变的特殊计算机指令。无论何时,微处理器都可以访问到存储在ROM上的信息。因为这些指令不可被擦出,所以他们保存在ROM中。 Flash Memory:是一种兼具RAM和ROM二者性质的特殊内存。我们可以像操作RAM一样,向Flash 内存写入数据;但是它又像ROM一样,数据在掉电时不丢失。悲剧的是,Flash 内存没有RAM那么快,所以任何时候都不要指望它能取代标准的计算机内存。

    时间:2018-06-06 关键词: 单片机 程序

  • I2C总线的仲裁问题

     【问】有网友问关于I2C总线的仲裁问题: The I2C-bus specification的第13页有这样的话: In other words, arbitration isn’t allowed between: * A repeated START condition and a data bit * A STOP condition and a data bit * A repeated START condition and a STOP condition. Slaves are not involved in the arbitration procedure. 我现在遇到了一个问题,假设说有两台主机A和B同时发送数据给C,但是A发送一个字节的数据,B发送两个字节的数据。并且他们第一个字节的数据都相同。即A 发送的是" start+C地址+字节1+stop",B发送的数据是"start+C地址+字节1+字节2+stop".由于第一个字节相同所以在第一个字节传送完后仲裁过程没有结束。这就遇到了规范上所讲到的一个停止位和一个数据位的情况,那么是不应该发生仲裁的。如果说B发送的第二个字节的第一位是1,那么“线与”后SCL和SDA线实际上就表示了stop信号,如果不仲裁则B继续发数据,而此前C已经收到了一个停止信号。C停止了,那么B所发送的第二个字节的数据根本就没有成功。有那位大虾可以帮我解释一下,应该怎么办。多谢了。另外如果两个主机,一个发送停止条件,一个发送重复起始条件,如果不仲裁,总的效果就是产生一个低电平的数据,那么从机就会把这个数据接收过去,但事实上这并不是两个主机期望发送的数据,I2C总线的时序就乱了。 【答】I2C规范的这段话的含义可以译为: 也就是说,仲裁不能在下面情况之间进行: * 重复起始条件和数据位。 * 停止条件和数据位。 * 重复起始条件和停止条件。 从机不被卷入仲裁过程。 翻阅《I2C总线规范》,对于I2C总线的仲裁有比较详细地论述。个人认为需要明白其的先决条件: 1、I2C总线的控制只由地址或主机码以及竞争主机发送的数据决定,没有中央主机,总线也没有任何定制的优先权。 2、主机只能在总线空闲的时侯启动传送。两个或多个主机可能在起始条件的最小持续时间tHD;STA内产生一个起始条件,结果在总线上产生一个规定的起始条件。 3、当SCL线是高电平时,仲裁在SDA 线发生;这样,在其他主机发送低电平时,发送高电平的主机将断开它的数据输出级,因为总线上的电平与它自己的电平不相同。 然后,进一步获得其的判定条件: 1、仲裁可以持续多位。首先是比较地址位。如果每个主机都试图寻址同一的器件,仲裁会继续比较数据位(假设主机是发送器),或者比较响应位(假设主机是接收器)。 2、I2C总线的地址和数据信息由赢得仲裁的主机决定,在仲裁过程中不会丢失信息。丢失仲裁的主机可以产生时钟脉冲直到丢失仲裁的该字节末尾。 3、在串行传输过程中时,一旦有重复的起始条件或停止条件发送到I2C总线的时侯,仲裁过程仍在进行。如果可能产生这样的情况,有关的主机必须在帧格式相同位置发送这个重复起始条件或停止条件,也就是开始那段话的含义。 4、此外,如果主机也结合了从机功能,而且在寻址阶段丢失仲裁,它很可能就是赢得仲裁的主机在寻址的器件。那么,丢失仲裁的主机必须立即切换到它的从机模式

    时间:2018-06-06 关键词: i2c 总线

发布文章