打印
[51单片机]

从业将近十年!手把手教你单片机程序框架(连载)

[复制链接]
楼主: jianhong_wu
手机看帖
扫描二维码
随时随地手机跟帖
161
jianhong_wu|  楼主 | 2014-3-30 09:14 | 只看该作者 |只看大图 回帖奖励 |倒序浏览
woairgzn 发表于 2014-3-28 22:07
我就喜欢这样的实干家,现实生活也没有那么多理论,也不需要精确的理论分析,知道大概,多实践把问题解决了 ...

我很赞成你的观点。谢谢你的关注。

使用特权

评论回复
162
544539018| | 2014-3-30 14:26 | 只看该作者
高手啊

使用特权

评论回复
163
lf19880813| | 2014-3-30 14:43 | 只看该作者
真的非常感谢鸿哥的无私分享!让我完善程序的框架,我会一直关注鸿哥!希望自己能力也不断上升,传递鸿哥的正能量!

使用特权

评论回复
164
十月风城| | 2014-3-30 18:01 | 只看该作者
jianhong_wu 发表于 2014-3-30 09:11
我觉得你误会了我想表达的意思。我想很多人不会这样解答我第一节的内容。
(1)汇编语言就没有指针的概念 ...

以下纯属个人理解:
         1、 C语言说白了,是对输入数据的处理与加工,并给出输出。在进行数据处理过程中,会有:输入的数据,处理的数据和输出的数据。当数据数量比较少的时候,我们还可以给他们一一起个名字,但是当需要处理更多的数据时怎么办,这时就需要指针了。现在产品越来越复杂,团队合作开发一个产品是一个趋势。当队友需要帮助时,你只能因为不懂指针,爱莫能助啊!换句话说,现在单片机厂商为了提高竞争力,会自动产生底层驱动,来减少软件的开发时程,他们的代码里面也会有指针。所以说不要让指针成为自己的短板。
         2、我现在是做通讯类的产品,也用的是单片机,当接收和发送数据时,对时间都有很高的要求;还有一个硬线信号的输入,需要防反跳的一个时间;单片机上电,多长时间开始工作,需要根据电源部分什么时候电源稳定(电容的充放电)来决定时间,等等;那么这是功能对时间的要求。另一方面,有的客户很关心产品的反应时间,那就需要工程师根据软件的运行,来对反应时间做一个评估。那这都是前期设计需要产出的东西。
大白话:
         说实话,我也没有多少工作经验,只是发泄一下:老板,你要求真。。严格。不多说了,都是痛!

使用特权

评论回复
165
cjseng| | 2014-3-30 21:24 | 只看该作者
jianhong_wu 发表于 2014-3-30 09:11
我觉得你误会了我想表达的意思。我想很多人不会这样解答我第一节的内容。
(1)汇编语言就没有指针的概念 ...

1.51的汇编里就有指针的概念,不过它不叫指针,叫做间接寻址,用@来表示,高128字节的RAM必须用间接寻址来访问,直接访问就成了访问SFR;还有外部RAM也是用指针来访问的。
2.时间精度重要么?大多数运用确实对时间精度没要求,但是必须要有一个概念:资源有限!既然开了定时中断,就必须尽量保证定时时间的准确性,因为这决定了程序运行的“节奏”。“节奏”没必要太快,就是定时时间不要过短,以免经常进入中断,导致其它中断(如串口中断不能及时响应,会丢失数据)得不到正确的执行;定时时间也不能太长,以免产生按键扫描不灵敏、数码管显示有闪烁等等情况出现。总之,定时器周期需要根据系统的响应周期、程序的运行时间来确定,而不是随便设个数字就搞定一切的。
所以,我认为,应该经常性地测试每一个功能模块的运行时间、统计各模块一个循环需要的最长时间,看这个最长的时间是否满足系统的实时性要求,如果能满足,就以这个时间为基准,确定定时器的定时周期,当然要留有一定余量,用来响应其它中断。
至于定时时间的精确计算,可以用公式计算,也可以用软件仿真,直接在Keil里仿真调试一下就可以确定;或者在某个引脚输出一个反转的电平信号,用示波器观察,也可以用万用表量这个引脚的平均电平,从而测算出CPU的使用率,尽可能地不要让CPU使用率太高。
总之,一定要记住:资源有限!

使用特权

评论回复
166
liuhaihai00| | 2014-3-30 21:52 | 只看该作者
这些程序似曾相识

使用特权

评论回复
167
jianhong_wu|  楼主 | 2014-3-31 09:46 | 只看该作者
cjseng 发表于 2014-3-30 21:24
1.51的汇编里就有指针的概念,不过它不叫指针,叫做间接寻址,用@来表示,高128字节的RAM必须用间接寻址 ...

你的观点我很认同。

使用特权

评论回复
168
善解人意| | 2014-3-31 09:54 | 只看该作者
好东西,楼主辛苦了

使用特权

评论回复
169
soho789| | 2014-3-31 17:21 | 只看该作者
最近一直在学习 寄存器,发现寄存器太多了,根本就记不住啊,然后还有使能,开启中断,各种移位,然后头就大了,
看了楼主说不用记这些,又给我增加了很多信心,但是还是担心以后会不会要走弯路, 你说的这些 while,for 我都知道,难道我一直停留在这个阶段 ? 怎么才能更深入一步?

使用特权

评论回复
170
叶丷| | 2014-3-31 17:55 | 只看该作者
要是做单片机销售,你会怎么去做好呢,以前我做PCB行业的。

使用特权

评论回复
171
jianhong_wu|  楼主 | 2014-3-31 18:35 | 只看该作者
soho789 发表于 2014-3-31 17:21
最近一直在学习 寄存器,发现寄存器太多了,根本就记不住啊,然后还有使能,开启中断,各种移位,然后头就 ...

      会了开桑塔纳,就等于会了开奥迪,宝马等所有小汽车。你如果问我怎么样才能进一步提高驾驶技术,那就是在不同的路况,在不同的气候下多开车。
      会了51单片机的编程框架和套路,就等于你会了PIC,STM32等所有裸机跑的单片机。你如果问我怎么样才能更深一步,那就是多做一些项目。
    所以,你最缺的是做项目经验。做项目你会遇到各种问题,你把这些问题克服解决了,你就自然进一步了。

使用特权

评论回复
评分
参与人数 2威望 +8 收起 理由
hkl_fs + 3 点石为金
xyz549040622 + 5
172
jianhong_wu|  楼主 | 2014-3-31 18:51 | 只看该作者
叶丷 发表于 2014-3-31 17:55
要是做单片机销售,你会怎么去做好呢,以前我做PCB行业的。

我有一个做单片机销售很厉害的朋友,他是某著名芯片公司深圳地区的负责人,他跟我分享的单片机销售经验是:
(1)在你打算改行做销售时,首先你要敢于清零自己,从头再来。他当时是写程序代码的,工资有一万多,做了三年后,改行跑业务,工资才1500元。现在已经很厉害了。
(2)想做好芯片的销售,首先要尽可能选择加入一家有实力的芯片公司。
(3)选客户时,要找到各行业前三名的公司,只有这种公司出货量才会大。
(4)选客户时,要找到负责人。
(5)选择销售,就是选择一种生活方式,要有泡的心理,不能急,要能喝酒。在优哉游哉的生活中,慢慢积累客户,慢慢跟同行销售交朋友,交换客户信息,形成庞大的人脉网络。那时候想找某某公司的负责人,只需要让同行的朋友打个招呼就轻而易举联系上,到了那个层次和阶段,哪里还要陌生拜访,哪里还要电话营销。

使用特权

评论回复
173
JessLi| | 2014-3-31 19:55 | 只看该作者
专业的**,支持!!!

使用特权

评论回复
174
叶丷| | 2014-4-1 12:03 | 只看该作者
jianhong_wu 发表于 2014-3-31 18:51
我有一个做单片机销售很厉害的朋友,他是某著名芯片公司深圳地区的负责人,他跟我分享的单片机销售经验是 ...

嗯  是的  不知道我否成功  谢谢你的意见

使用特权

评论回复
175
jianhong_wu|  楼主 | 2014-4-3 01:19 | 只看该作者
第三十七节:数码管作为仪表盘显示跑马灯的方向,速度和运行状态。

开场白:
    我在第24节中讲过按键控制跑马灯的方向,速度和运行状态的项目程序,只可惜那个程序不能直观地显示运行中的三种状态,这节我决定在24节的基础上,增加一个数码管显示作为类似汽车仪表盘的界面,实时显示跑马灯的方向,速度,和运行状态。
这一节要教会大家一个知识点:继续加深理解运动,按键与数码管三者之间的关联程序框架。

具体内容,请看源代码讲解。

(1)硬件平台:
基于朱兆祺51单片机学习板。用S1键作为控制跑马灯的方向按键,S5键作为控制跑马灯方向的加速度按键,S9键作为控制跑马灯方向的减速度按键,S13键作为控制跑马灯方向的启动或者暂停按键。记得把输出线P0.4一直输出低电平,模拟独立按键的触发地GND。

(2)实现功能:
跑马灯运行:第1个至第8个LED灯一直不亮。在第9个至第16个LED灯,依次逐个亮灯并且每次只能亮一个灯。每按一次独立按键S13键,原来运行的跑马灯会暂停,原来暂停的跑马灯会运行。用S1来改变方向。用S5和S9来改变速度,每按一次按键的递增或者递减以10为单位。
数码管显示:本程序只有1个窗口,这个窗口分成3个局部显示。8,7,6位数码管显示运行状态,启动时显示“on”,停止时显示“oFF”。5位数码管显示数码管方向,正向显示“n”,反向显示“U”。4,3,2,1位数码管显示速度。数值越大速度越慢,最慢的速度是550,最快的速度是50。

(3)源代码讲解如下:
第三十七节源代码讲解.rar (4.78 KB)

总结陈词:
    前面花了大量的章节在讲数码管显示,按键,运动的关联程序框架,从下一节开始,我将会用八节内容来讲我常用的串口程序框架,内容非常精彩和震撼,思路非常简单而又实用。欲知详情,请听下回分解-----判断数据尾来接收一串数据的串口通用程序框架。

(未完待续,下节更精彩,不要走开哦)

使用特权

评论回复
176
tabu| | 2014-4-4 14:47 | 只看该作者
马克一下,顶

使用特权

评论回复
177
jianhong_wu|  楼主 | 2014-4-5 11:03 | 只看该作者
本帖最后由 jianhong_wu 于 2014-4-6 22:22 编辑

第三十八节:判断数据尾来接收一串数据的串口通用程序框架。

开场白:
    在实际项目中,串口通讯不可能一次通讯只发送或接收一个字节,大部分的项目都是一次发送或者接受一串的数据。我们还要在这一串数据里解析数据协议,提取有用的数据。
这一节要教会大家三个知识点:
第一个:如何识别一串数据已经发送接收完毕。
第二个:如何在已经接收到的一串数据中解析数据尾协议并且提取有效数据。
第三个:接收一串数据的通用程序框架涉及到main循环里的串口服务程序,定时器的计时程序,串口接收中断程序的密切配合。大家要理解它们三者之间是如何关联起来的。

具体内容,请看源代码讲解。

(1)硬件平台:
基于朱兆祺51单片机学习板。

(2)实现功能:

波特率是:9600 。
通讯协议:XX YY  EB 00 55
          其中后三位 EB 00 55就是我所说的数据尾,它的有效数据XX YY在数据尾的前面。
        任意时刻,单片机从电脑“串口调试助手”上位机收到的一串数据中,只要此数据中包含关键字EB00 55 ,并且此关键字前面两个字节的数据XX YY 分别为01 02,那么蜂鸣器鸣叫一声表示接收的数据尾和有效数据都是正确的。

(3)源代码讲解如下:

#include "REG52.H"


#define const_voice_short  40   //蜂鸣器短叫的持续时间
#define const_rc_size  10  //接收串口中断数据的缓冲区数组大小

#define const_receive_time  5  //如果超过这个时间没有串口数据过来,就认为一串数据已经全部接收完,这个时间根据实际情况来调整大小

void initial_myself(void);   
void initial_peripheral(void);
void delay_long(unsigned int uiDelaylong);



void T0_time(void);  //定时中断函数
void usart_receive(void); //串口接收中断函数
void usart_service(void);  //串口服务程序,在main函数里

sbit beep_dr=P2^7; //蜂鸣器的驱动IO口

unsigned int  uiSendCnt=0;     //用来识别串口是否接收完一串数据的计时器
unsigned char ucSendLock=1;    //串口服务程序的自锁变量,每次接收完一串数据只处理一次
unsigned int  uiRcregTotal=0;  //代表当前缓冲区已经接收了多少个数据
unsigned char ucRcregBuf[const_rc_size]; //接收串口中断数据的缓冲区数组
unsigned int  uiRcMoveIndex=0;  //用来解析数据协议的中间变量


unsigned int  uiVoiceCnt=0;  //蜂鸣器鸣叫的持续时间计数器



void main()
  {
   initial_myself();  
   delay_long(100);   
   initial_peripheral();
   while(1)  
   {
       usart_service();  //串口服务程序
   }

}


void usart_service(void)  //串口服务程序,在main函数里
{

        
/* 注释一:
* 识别一串数据是否已经全部接收完了的原理:
* 在规定的时间里,如果没有接收到任何一个字节数据,那么就认为一串数据被接收完了,然后就进入数据协议
* 解析和处理的阶段。这个功能的实现要配合定时中断,串口中断的程序一起阅读,要理解他们之间的关系。
*/
     if(uiSendCnt>=const_receive_time&&ucSendLock==1) //说明超过了一定的时间内,再也没有新数据从串口来
     {

            ucSendLock=0;    //处理一次就锁起来,不用每次都进来,除非有新接收的数据

                    //下面的代码进入数据协议解析和数据处理的阶段

                    uiRcMoveIndex=uiRcregTotal; //由于是判断数据尾,所以下标移动变量从数组的最尾端开始向0移动
            while(uiRcMoveIndex>=5)   //如果处理的数据量大于等于5(2个有效数据,3个数据头)说明还没有把缓冲区的数据处理完
            {
               if(ucRcregBuf[uiRcMoveIndex-3]==0xeb&&ucRcregBuf[uiRcMoveIndex-2]==0x00&&ucRcregBuf[uiRcMoveIndex-1]==0x55)  //数据尾eb 00 55的判断
               {
                              if(ucRcregBuf[uiRcMoveIndex-5]==0x01&&ucRcregBuf[uiRcMoveIndex-4]==0x02)  //有效数据01 02的判断
                                  {
                                      uiVoiceCnt=const_voice_short; //蜂鸣器发出声音,说明数据尾和有效数据都接收正确
                                  }
                  break;   //退出循环
               }
               uiRcMoveIndex--; //因为是判断数据尾,下标向着0的方向移动
           }
                                         
           uiRcregTotal=0;  //清空缓冲的下标,方便下次重新从0下标开始接受新数据
  
     }
                        
}


void T0_time(void) interrupt 1    //定时中断
{
  TF0=0;  //清除中断标志
  TR0=0; //关中断


  if(uiSendCnt<const_receive_time)   //如果超过这个时间没有串口数据过来,就认为一串数据已经全部接收完
  {
          uiSendCnt++;    //表面上这个数据不断累加,但是在串口中断里,每接收一个字节它都会被清零,除非这个中间没有串口数据过来
      ucSendLock=1;     //开自锁标志
  }

  if(uiVoiceCnt!=0)
  {
     uiVoiceCnt--; //每次进入定时中断都自减1,直到等于零为止。才停止鸣叫
     beep_dr=0;  //蜂鸣器是PNP三极管控制,低电平就开始鸣叫。

  }
  else
  {
     ; //此处多加一个空指令,想维持跟if括号语句的数量对称,都是两条指令。不加也可以。
     beep_dr=1;  //蜂鸣器是PNP三极管控制,高电平就停止鸣叫。
  }


  TH0=0xfe;   //重装初始值(65535-500)=65035=0xfe0b
  TL0=0x0b;
  TR0=1;  //开中断
}


void usart_receive(void) interrupt 4                 //串口接收数据中断        
{        

   if(RI==1)  
   {
        RI = 0;

            ++uiRcregTotal;
        if(uiRcregTotal>const_rc_size)  //超过缓冲区
        {
           uiRcregTotal=const_rc_size;
        }
        ucRcregBuf[uiRcregTotal-1]=SBUF;   //将串口接收到的数据缓存到接收缓冲区里
        uiSendCnt=0;  //及时喂狗,虽然main函数那边不断在累加,但是只要串口的数据还没发送完毕,那么它永远也长不大,因为每个中断都被清零。
   
   }
   else  //我在其它单片机上都不用else这段代码的,可能在51单片机上多增加" TI = 0;"稳定性会更好吧。
   {
        TI = 0;
   }
                                                         
}                                


void delay_long(unsigned int uiDelayLong)
{
   unsigned int i;
   unsigned int j;
   for(i=0;i<uiDelayLong;i++)
   {
      for(j=0;j<500;j++)  //内嵌循环的空指令数量
          {
             ; //一个分号相当于执行一条空语句
          }
   }
}


void initial_myself(void)  //第一区 初始化单片机
{

  beep_dr=1; //用PNP三极管控制蜂鸣器,输出高电平时不叫。

  //配置定时器
  TMOD=0x01;  //设置定时器0为工作方式1
  TH0=0xfe;   //重装初始值(65535-500)=65035=0xfe0b
  TL0=0x0b;


  //配置串口
  SCON=0x50;
  TMOD=0X21;
  TH1=TL1=-(11059200L/12/32/9600);  //这段配置代码具体是什么意思,我也不太清楚,反正是跟串口波特率有关。
  TR1=1;

}

void initial_peripheral(void) //第二区 初始化外围
{

   EA=1;     //开总中断
   ES=1;     //允许串口中断
   ET0=1;    //允许定时中断
   TR0=1;    //启动定时中断

}

总结陈词:
     这一节讲了判断数据尾的程序框架,但是在大部分的项目中,都是通过判断数据头来接收数据的,这样的程序该怎么写?欲知详情,请听下回分解-----判断数据头来接收一串数据的串口通用程序框架。

(未完待续,下节更精彩,不要走开哦)

使用特权

评论回复
178
mwxpk| | 2014-4-5 15:29 | 只看该作者
哄不会的啊,全是没用教程

使用特权

评论回复
评论
a7020907110bc 2018-4-7 12:55 回复TA
nixingnishang 
cjseng 2014-4-5 21:48 回复TA
呵呵! 
179
jianhong_wu 发表于 2014-3-5 21:53
第一节:吴坚鸿谈初学单片机的误区。

(1)很难记住繁杂的寄存器?寄存器不用死记硬背,鸿哥我行走江湖多 ...

很给力

使用特权

评论回复
180
cjseng| | 2014-4-5 21:53 | 只看该作者
鸿哥,你知道你单片机不能做精确定时的原因了吗?
你每次用到定时中断,都是先把定时器停掉,然后执行一端程序后,再重装初值,再启动定时器,这里边中间执行的那段程序会因为各种条件,导致每次执行的时间不一样,最终导致你的定时器每次中断的周期不一样,所以不能精确定时。

所以,你可以这样用,但是不要把错误的方法传授给别人。
单片机是可以精确定时的!

使用特权

评论回复
评分
参与人数 1威望 +3 收起 理由
yanjibao + 3 同意,确实可以精确定时
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则