打印
[51单片机]

从单片机基础到程序框架(连载)

[复制链接]
楼主: jianhong_wu
手机看帖
扫描二维码
随时随地手机跟帖
441
jianhong_wu|  楼主 | 2018-10-30 11:40 | 只看该作者 |只看大图 回帖奖励 |倒序浏览
第一百三十二节:“转发、透传、多种协议并存”的双缓存串口程序框架。
第一百三十二节.pdf (126.08 KB)
【132.1   字节间隔时间、双缓存切换、指针切换关联。】

       在一些通讯模块的项目中,常常涉及数据的转发,透传,提取关键字的处理,单片机接收到的数据不许随意丢失,必须全部暂存,然后提取关键字,再把整包数据原封不动地转发或者透传给“下家”。这类项目的特点是,通讯协议不是固定唯一的,因此,前面章节那种接头暗号(数据头)的程序框架不适合这里,本节跟大家分享另外一种程序框架。
       第一个要突破的技术难点是,既然通讯协议不是固定唯一的,那么,如何识别一串数据已经接收完毕?答案是靠接收每个字节之间的间隔时间来识别。当一串数据正在接收时,每个字节之间的间隔时间是“短暂的相对均匀的”。当一串数据已经接收完毕时,每个字节之间的间隔时间是“突然变长的”。代码的具体实现,是靠一个软件定时器,模拟单片机“看门狗”的“喂狗”原理。
       第二个要突破的技术难点是,既然通讯协议不是固定唯一的,数据内容带有随机性,甚至字节之间的间隔时间的长短也带有随机性和不确定性,那么,如何预防正在处理数据时突然“接收中断”又接收到的新数据覆盖了尚未来得及处理的旧数据,或者,如何预防正在处理旧数据时丢失了突然又新过来的本应该接收的新数据?答案是用双缓存轮流切换的机制。双缓存,一个用在处理刚刚接收到的旧数据,另一个用在时刻准备着接收新数据,轮流切换,两不误。
       第三个要突破的技术难点是,既然是用双缓存轮流切换的机制,那么,在主程序里如何统一便捷地处理两个缓存的数组?这里的“统一”是关键,要把两个数组“统一”成(看成是)一个数组,方法是,只需用“指针切换关联”的技术就可以了。

【132.2   程序例程。】


      
       上图132.2.1  有源蜂鸣器电路





       上图132.2.2  232串口电路

        程序功能如下:单片机接收任意长度(最大一次不超过30字节)的一串数据。如果发现连续有三个字节是0x02 0x03 0x04,蜂鸣器则“短叫”100ms提示;如果发现连续有四个字节是0x06 0x07 0x08 0x09,蜂鸣器则“长叫”2000ms提示。
       比如测试“短叫”100ms,发送十六进制的数据串:05 02 00 00 02 03 04 09
       比如测试“长叫”2000ms,发送十六进制的数据串:02 02 06 07 08 09 01 08 03 00 05
       代码如下:

#include "REG52.H"

#define DOG_TIME_OUT  20  //理论上,9600波特率的字节间隔时间大概0.8ms左右,因此取20ms足够
#define RECE_BUFFER_SIZE  30   //接收缓存的数组大小

void usart(void);  //串口接收的中断函数
void T0_time();    //定时器的中断函数

void UsartTask(void);    //串口接收的任务函数,放在主函数内

void SystemInitial(void) ;
void Delay(unsigned long u32DelayTime) ;
void PeripheralInitial(void) ;

void BeepOpen(void);   
void BeepClose(void);
void VoiceScan(void);

sbit P3_4=P3^4;  

unsigned char Gu8CurrentReceBuffer_Sec=0; //当前接收缓存的选择标志。0代表缓存A,1代表缓存B

unsigned char Gu8ReceBuffer_A[RECE_BUFFER_SIZE]; //双缓存其中之一的缓存A
unsigned long Gu32ReceCnt_A=0;    //缓存A的数组下标与计数器,必须初始化为0,做好接收准备

unsigned char Gu8ReceBuffer_B[RECE_BUFFER_SIZE]; //双缓存其中之一的缓存B
unsigned long Gu32ReceCnt_B=0;    //缓存B的数组下标与计数器,必须初始化为0,做好接收准备

unsigned char Gu8ReceFeedDog=1; //“喂狗”的操作变量。
unsigned char Gu8FinishFlag=0; //接收完成标志。0代表还没有完成,1代表已经完成了一次接收

volatile unsigned char vGu8ReceTimeOutFlag=0;//通信过程中字节之间的超时定时器的开关
volatile unsigned int vGu16ReceTimeOutCnt=0; //通信过程中字节之间的超时定时器,“喂狗”的对象

volatile unsigned char vGu8BeepTimerFlag=0;  
volatile unsigned int vGu16BeepTimerCnt=0;  

void main()
{
SystemInitial();            
Delay(10000);               
PeripheralInitial();      
    while(1)  
{  
   UsartTask();   //串口接收的任务函数
    }
}

void usart(void) interrupt 4           
{        
   if(1==RI)  
   {
        RI = 0;

Gu8FinishFlag=0; //此处也清零,意味深长,当主函数正在处理数据时,可以兼容多次接收完成
            Gu8ReceFeedDog=1; //看门狗的“喂狗”操作,给软件定时器继续“输血”
if(0==Gu8CurrentReceBuffer_Sec)   //0代表选择缓存A
{
      if(Gu32ReceCnt_A<RECE_BUFFER_SIZE)
{
Gu8ReceBuffer_A[Gu32ReceCnt_A]=SBUF;
Gu32ReceCnt_A++; //记录当前缓存A的接收字节数
}
}
else     //1代表选择缓存B
{
      if(Gu32ReceCnt_B<RECE_BUFFER_SIZE)
{
Gu8ReceBuffer_B[Gu32ReceCnt_B]=SBUF;
Gu32ReceCnt_B++;  //记录当前缓存B的接收字节数
}

}
   }
   else  //发送数据引起的中断
   {
        TI = 0;  //及时清除发送中断的标志,避免一直无缘无故的进入中断。
        //以下可以添加一个全局变量的标志位的相关代码,通知主函数已经发送完一个字节的数据了。
   }                                                      
}  

void UsartTask(void)    //串口接收的任务函数,放在主函数内
{
static unsigned char *pSu8ReceBuffer;  //“指针切换关联”中的指针,切换内存
static unsigned char Su8Lock=0;  //用来避免一直更新的临时变量
static unsigned long i;  //用在数据处理中的循环变量
static unsigned long Su32ReceSize=0; //接收到的数据大小的临时变量


    if(1==Gu8ReceFeedDog) //每被“喂一次狗”,就及时更新一次“超时检测的定时器”的初值
        {
                Gu8ReceFeedDog=0;

Su8Lock=0; //解锁。用来避免一直更新的临时变量
               
        //以下三行代码是看门狗中的“喂狗”操作。继续给软件定时器“输血”               
                vGu8ReceTimeOutFlag=0;
        vGu16ReceTimeOutCnt=DOG_TIME_OUT;//正在通信时,两个字节间隔的最大时间,本节选用20ms
                vGu8ReceTimeOutFlag=1;
        }
        else if(0==Su8Lock&&0==vGu16ReceTimeOutCnt) //超时,代表一串数据已经接收完成
        {
            Su8Lock=1;  //避免一直进来更新
        Gu8FinishFlag=1; //两个字节之间的时间超时,因此代表了一串数据已经接收完成
    }

        
        if(1==Gu8FinishFlag)  //1代表已经接收完毕一串新的数据,需要马上去处理
        {
if(0==Gu8CurrentReceBuffer_Sec)  
{
Gu8CurrentReceBuffer_Sec=1; //以最快的速度先切换接收内存,避免丢失新发过来的数据
//Gu32ReceCnt_B=0;//这里不能清零缓存B的计数器,意味深长,避免此处临界点发生中断
            Gu8FinishFlag=0;  //尽可能以最快的速度清零本次完成的标志,为下一次新数据做准备
pSu8ReceBuffer=(unsigned char *)&Gu8ReceBuffer_A[0]; //关联刚刚接收的数据缓存
Su32ReceSize=Gu32ReceCnt_A; //记录当前缓存的有效字节数
Gu32ReceCnt_A=0; //及时把当前缓存计数清零,为一次切换接收缓存做准备。意味深长。
}
else
{
Gu8CurrentReceBuffer_Sec=0; //以最快的速度先切换接收内存,避免丢失新发过来的数据
//Gu32ReceCnt_A=0;//这里不能清零缓存A的计数器,意味深长,避免此处临界点发生中断
            Gu8FinishFlag=0;  //尽可能以最快的速度清零本次完成的标志,为下一次新数据做准备
pSu8ReceBuffer=(unsigned char *)&Gu8ReceBuffer_B[0]; //关联刚刚接收的数据缓存
Su32ReceSize=Gu32ReceCnt_B; //记录当前缓存的有效字节数
Gu32ReceCnt_B=0; //及时把当前缓存计数清零,为一次切换接收缓存做准备。意味深长。
}

        //Gu8FinishFlag=0; //之所以不选择在这里清零,是因为在上面清零更及时快速。意味深长。

        //开始处理刚刚接收到的一串新数据,直接“统一”处理pSu8ReceBuffer指针为代表的数据即可
        for(i=0;i<Su32ReceSize;i++)
{
             if(0x02==pSu8ReceBuffer[i]&&
0x03==pSu8ReceBuffer[i+1]&&
0x04==pSu8ReceBuffer[i+2]) //连续三个数是0x02 0x03 0x04
{
vGu8BeepTimerFlag=0;  
vGu16BeepTimerCnt=100;   //让蜂鸣器“短叫”100ms
vGu8BeepTimerFlag=1;  
    return; //直接退出当前函数
}

             if(0x06==pSu8ReceBuffer[i]&&
0x07==pSu8ReceBuffer[i+1]&&
0x08==pSu8ReceBuffer[i+2]&&
0x09==pSu8ReceBuffer[i+3]) //连续四个数是0x06 0x07 0x08 0x09
{
vGu8BeepTimerFlag=0;  
vGu16BeepTimerCnt=2000;   //让蜂鸣器“长叫”2000ms
vGu8BeepTimerFlag=1;  
    return; //直接退出当前函数
}

}

    }
}

void T0_time() interrupt 1     
{
VoiceScan();  

if(1==vGu8ReceTimeOutFlag&&vGu16ReceTimeOutCnt>0) //通信过程中字节之间的超时定时器
        {
                 vGu16ReceTimeOutCnt--;        
}  

TH0=0xfc;   
TL0=0x66;   
}


void SystemInitial(void)
{
unsigned char u8_TMOD_Temp=0;

//以下是定时器0的中断的配置
TMOD=0x01;  
TH0=0xfc;   
TL0=0x66;   
EA=1;      
ET0=1;      
TR0=1;   

//以下是串口接收中断的配置
//串口的波特率与内置的定时器1直接相关,因此配置此定时器1就等效于配置波特率。
u8_TMOD_Temp=0x20; //即将把定时器1设置为:工作方式2,初值自动重装的8位定时器。
TMOD=TMOD&0x0f; //此寄存器低4位是跟定时器0相关,高4位是跟定时器1相关。先清零定时器1。
TMOD=TMOD|u8_TMOD_Temp;  //把高4位的定时器1填入0x2,低4位的定时器0保持不变。
TH1=256-(11059200L/12/32/9600);  //波特率为9600。11059200代表晶振11.0592MHz,
TL1=256-(11059200L/12/32/9600);  //L代表long的长类型数据。根据芯片手册提供的计算公式。
TR1=1;  //开启定时器1

SM0=0;  
SM1=1;  //SM0与SM1的设置:选择10位异步通信,波特率根据定时器1可变  
REN=1;  //允许串口接收数据

//为了保证串口中断接收的数据不丢失,必须设置IP = 0x10,相当于把串口中断设置为最高优先级,
//这个时候,串口中断可以打断任何其他的中断服务函数实现嵌套,
IP =0x10;  //把串口中断设置为最高优先级,必须的。

ES=1;         //允许串口中断  
EA=1;         //允许总中断
}

void Delay(unsigned long u32DelayTime)
{
    for(;u32DelayTime>0;u32DelayTime--);
}

void PeripheralInitial(void)
{

}

void BeepOpen(void)
{
P3_4=0;  
}

void BeepClose(void)
{
P3_4=1;  
}

void VoiceScan(void)
{

          static unsigned char Su8Lock=0;  

if(1==vGu8BeepTimerFlag&&vGu16BeepTimerCnt>0)
          {
                  if(0==Su8Lock)
                  {
                   Su8Lock=1;  
BeepOpen();
     }
    else  
{     

                       vGu16BeepTimerCnt--;         

                   if(0==vGu16BeepTimerCnt)
                   {
                           Su8Lock=0;     
BeepClose();  
                   }

}
          }         
}


使用特权

评论回复
评分
参与人数 1威望 +1 收起 理由
arima + 1 很给力!
442
ztzp| | 2018-11-3 23:18 | 只看该作者
下载了,谢谢。

使用特权

评论回复
443
Mouselt| | 2018-11-14 08:48 | 只看该作者
点赞

使用特权

评论回复
444
why130110| | 2018-11-14 11:38 | 只看该作者
单片机小白可以在某宝上找 51单片机开发 远程服务   

使用特权

评论回复
445
jianhong_wu|  楼主 | 2018-11-14 12:24 | 只看该作者
第一百三十三节:常用的三种串口发送函数。
第一百三十三节.pdf (116.99 KB)
【133.1   发送单字节的底层驱动函数。】

       单片机内置的“独立硬件串口模块”能直接实现“发送一个字节数据”的基础功能,因此,发送单字节的函数是应用层与硬件层的最小单位的接口函数,也称为底层驱动函数。应用层再复杂的发送函数都基于此最小单位的接口函数来实现。单片机应用层与“独立硬件串口模块”之间的接口通信是靠寄存器SBUF作为中间载体的,要实现发送单字节的最小接口函数,有如下三个关键点。
       第一个,单片机应用层如何知道“硬件模块”已经发送完了一个字节,靠什么来识别?答:在初始化函数里,可以把“硬件模块”配置成,每发送完一个字节后都产生一次发送中断,在发送中断函数里让一个全局变量从0变成1,依此全局变量作为识别是否已经发送完一个字节的标志。
       第二个,发送一个字节数据的时候,如果“硬件模块”通讯异常,没有按预期产生发送中断,单片机就会一直处于死循环等待“完成标志”的状态,怎么办?答:在等待“完成标志”的时候,加入超时处理的机制。
       第三个,在连续发送一堆数据时,如果接收方(或者上位机)发现有丢失数据的时候,如何调节此发送函数?答:可以根据实际调试的结果,如果接收方发现丢失数据,可以尝试在每发送一个字节之后插入一个Delay延时,延时的时间长度根据实际调试为准。我个人的经验中,感觉stm32这类M3核或者M4核的单片机在发送一个字节的时候只需判断是否发送完成的标志位即可,不需要插入Delay延时。但是在其它某些个别厂家单片机的串口发送数据中,是需要插入Delay延时作为调节,否则在连续发送一堆数据时会丢失数据,这个,应该以实际调试项目为准。
      片段的讲解代码如下:

unsigned char Gu8ReceData;
unsigned char Gu8SendByteFinish=0; //发送一个字节完成的标志
void usart(void) interrupt 4     //串口的中断函数      
{        
        if(1==RI)  
        {
            RI = 0;
Gu8ReceData=SBUF;
}
        else  //发送数据引起的中断
        {
           TI = 0;  //及时清除发送中断的标志,避免一直无缘无故的进入中断。
       Gu8SendByteFinish=1; //从0变成1通知主函数已经发送完一个字节的数据了。
        }                                                      
}  

void UsartSendByteData(unsigned char u8SendData) //发送一个字节的底层驱动函数
{
    static unsigned int Su16TimeOutDelay;  //超时处理的延时计时器

    Gu8SendByteFinish=0;  //在发送一个字节之前,必须先把此全局变量的标志清零。
SBUF =u8SendData; //依靠寄存器SBUF作为载体发送一个字节的数据
Su16TimeOutDelay=0xffff;  //超时处理的延时计时器装载一个相对合理的计时初始值
while(Su16TimeOutDelay>0)  //超时处理
{
    if(1==Gu8SendByteFinish)  
{
    break;  //如果Gu8SendByteFinish为1,则发送一个字节完成,退出当前循环等待。
}
Su16TimeOutDelay--;  //超时计时器不断递减
}

//Delay();//在实际应用中,当连续发送一堆数据时如果发现丢失数据,可以尝试在此增加延时
}

【133.2   发送任意起始位置任意长度的函数。】

       要连续发送一堆数据,必须先把这堆数据封装成一个数组,然后编写一个发送数组的函数。该函数内部是基于“发送单字节的最小接口函数”来实现的。该函数对外通常需要两个接口,一个是数组的任意起始位置,一个发送的数据长度。数组的任意起始位置只需靠指针即可实现。片段的讲解代码如下:

//任意数组
unsigned char Gu8SendBuffer[11]={0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0A};

//发送任意起始位置任意长度的函数
void UsartSendBuffer(const unsigned char *pCu8SendBuffer,unsigned long u32SendSize)
{
    static unsigned long i;
    for(i=0;i<u32SendSize;i++) //u32SendSize为发送的数据长度
{
       UsartSendByteData(pCu8SendBuffer[i]);  //基于“发送单字节的最小接口函数”来实现的
    }
}

void main()
{
    UsartSendBuffer((const unsigned char *)&Gu8SendBuffer[0],5);//从第0位置发送5个数据
    UsartSendBuffer((const unsigned char *)&Gu8SendBuffer[6],5);//从第6位置发送5个数据
    while(1)  
{  

    }
}

【133.3   发送带协议的函数。】

       前面章节中,我们讲过接收“带固定协议”的程序框架,这类“带固定协议”的数据串里本身就自带了“数据的长度”,因此,要编程一个发送带协议的函数,关键在于,在函数内部根据协议先提取整串数据的有效长度。该函数对外通常也需要两个接口,一个是数组的起始位置,一个发送数据的最大限制长度。最大限制长度的作用是用来防止数组越界,增强程序的安全性。片段的讲解代码如下:

//“固定协议”十六进制的数据格式:EB 01 00 00 00 0B 03 E8 00 01 0B 。其中:
    // EB是数据头。
// 01是代表数据类型。
// 00 00 00 0B代表数据长度是11个(十进制)。
// 03 E8 00 01 0B代表其它数据

//“带固定协议”的数组
unsigned char Gu8SendMessage[11]={0xEB,0x01,0x00,0x00,0x00,0x0B,0x03,0xE8,0x00,0x01,0x0B};

//发送带协议的函数
void UsartSendMessage(const unsigned char *pCu8SendMessage,unsigned long u32SendMaxSize)
{
    static unsigned long i;
    static unsigned long *pSu32;
    static unsigned long u32SendSize;

    pSu32=(const unsigned long *)&pCu8SendMessage[2];
u32SendSize=*pSu32;  //从带协议的数组中提取整包数组的有效发送长度

if(u32SendSize>u32SendMaxSize) //如果“有效发送长度”大于“最大限制的长度”,数据异常
{
   return;  //数据异常,直接退出当前函数,预防数组越界
}

    for(i=0;i<u32SendSize;i++) //u32SendSize为发送的数据长度
{
       UsartSendByteData(pCu8SendMessage[i]); //基于“发送单字节的最小接口函数”来实现的
    }
}

void main()
{
    UsartSendMessage((const unsigned char *)&Gu8SendMessage[0],100); //必须从第0位置发送
    while(1)  
{  

    }
}



【133.4   程序例程。】


     上图133.4.1  232串口电路

       程序功能如下:
       单片机上电瞬间,直接发送三串数据。
       第一串是十六进制的任意数据:00 01 02 03 04
       第二串是十六进制的任意数据:06 07 08 09 0A
       第三串是十六进制的“带协议”数据:EB 01 00 00 00 0B 03 E8 00 01 0B
       波特率9600,校验位NONE(无),数据位8,停止位1。在电脑的串口助手软件里,设置接收显示的为“十六进制”(HEX模式),即可观察到发送的三串数据。
      代码如下:


#include "REG52.H"

void UsartSendByteData(unsigned char u8SendData); //发送一个字节的底层驱动函数

//发送任意起始位置任意长度的函数
void UsartSendBuffer(const unsigned char *pCu8SendBuffer,unsigned long u32SendSize);

//发送带协议的函数
void UsartSendMessage(const unsigned char *pCu8SendMessage,unsigned long u32SendMaxSize);

void usart(void);  //串口接收的中断函数
void SystemInitial(void);
void Delay(unsigned long u32DelayTime);
void PeripheralInitial(void);

unsigned char Gu8ReceData;
unsigned char Gu8SendByteFinish=0; //发送一个字节完成的标志

//任意数组
unsigned char Gu8SendBuffer[11]={0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0A};

//“固定协议”十六进制的数据格式:EB 01 00 00 00 0B 03 E8 00 01 0B 。其中:
// EB是数据头。
// 01是代表数据类型。
// 00 00 00 0B代表数据长度是11个(十进制)。
// 03 E8 00 01 0B代表其它数据
//“带固定协议”的数组
unsigned char Gu8SendMessage[11]={0xEB,0x01,0x00,0x00,0x00,0x0B,0x03,0xE8,0x00,0x01,0x0B};

void main()
{
SystemInitial();            
Delay(10000);               
PeripheralInitial();    //在此函数内部调用了发送的三串数据
    while(1)  
{  
    }
}

void usart(void) interrupt 4     //串口的中断函数      
{        
        if(1==RI)  
        {
            RI = 0;
Gu8ReceData=SBUF;
}
        else  //发送数据引起的中断
        {
           TI = 0;  //及时清除发送中断的标志,避免一直无缘无故的进入中断。
       Gu8SendByteFinish=1; //从0变成1通知主函数已经发送完一个字节的数据了。
        }                                                      
}  

void UsartSendByteData(unsigned char u8SendData) //发送一个字节的底层驱动函数
{
    static unsigned int Su16TimeOutDelay;  //超时处理的延时计时器

    Gu8SendByteFinish=0;  //在发送以字节之前,必须先把此全局变量的标志清零。
SBUF =u8SendData; //依靠寄存器SBUF作为载体发送一个字节的数据
Su16TimeOutDelay=0xffff;  //超时处理的延时计时器装载一个相对合理的计时初始值
while(Su16TimeOutDelay>0)  //超时处理
{
    if(1==Gu8SendByteFinish)  
{
    break;  //如果Gu8SendByteFinish为1,则发送一个字节完成,退出当前循环等待。
}
Su16TimeOutDelay--;  //超时计时器不断递减
}

//Delay();//在实际应用中,当连续发送一堆数据时如果发现丢失数据,可以尝试在此增加延时
}

//发送任意起始位置任意长度的函数
void UsartSendBuffer(const unsigned char *pCu8SendBuffer,unsigned long u32SendSize)
{
    static unsigned long i;
    for(i=0;i<u32SendSize;i++) //u32SendSize为发送的数据长度
{
       UsartSendByteData(pCu8SendBuffer[i]);  //基于“发送单字节的最小接口函数”来实现的
    }
}

//发送带协议的函数
void UsartSendMessage(const unsigned char *pCu8SendMessage,unsigned long u32SendMaxSize)
{
    static unsigned long i;
    static unsigned long *pSu32;
    static unsigned long u32SendSize;

    pSu32=(const unsigned long *)&pCu8SendMessage[2];
u32SendSize=*pSu32;  //从带协议的数组中提取整包数组的有效发送长度

if(u32SendSize>u32SendMaxSize) //如果“有效发送长度”大于“最大限制的长度”,数据异常
{
   return;  //数据异常,直接退出当前函数,预防数组越界
}

    for(i=0;i<u32SendSize;i++) //u32SendSize为发送的数据长度
{
       UsartSendByteData(pCu8SendMessage[i]); //基于“发送单字节的最小接口函数”来实现的
    }
}

void SystemInitial(void)
{
unsigned char u8_TMOD_Temp=0;

//以下是定时器0的中断的配置
TMOD=0x01;  
TH0=0xfc;   
TL0=0x66;   
EA=1;      
ET0=1;      
TR0=1;   

//以下是串口接收中断的配置
//串口的波特率与内置的定时器1直接相关,因此配置此定时器1就等效于配置波特率。
u8_TMOD_Temp=0x20; //即将把定时器1设置为:工作方式2,初值自动重装的8位定时器。
TMOD=TMOD&0x0f; //此寄存器低4位是跟定时器0相关,高4位是跟定时器1相关。先清零定时器1。
TMOD=TMOD|u8_TMOD_Temp;  //把高4位的定时器1填入0x2,低4位的定时器0保持不变。
TH1=256-(11059200L/12/32/9600);  //波特率为9600。11059200代表晶振11.0592MHz,
TL1=256-(11059200L/12/32/9600);  //L代表long的长类型数据。根据芯片手册提供的计算公式。
TR1=1;  //开启定时器1

SM0=0;  
SM1=1;  //SM0与SM1的设置:选择10位异步通信,波特率根据定时器1可变  
REN=1;  //允许串口接收数据

//为了保证串口中断接收的数据不丢失,必须设置IP = 0x10,相当于把串口中断设置为最高优先级,
//这个时候,串口中断可以打断任何其他的中断服务函数实现嵌套,
IP =0x10;  //把串口中断设置为最高优先级,必须的。

ES=1;         //允许串口中断  
EA=1;         //允许总中断
}

void Delay(unsigned long u32DelayTime)
{
    for(;u32DelayTime>0;u32DelayTime--);
}

void PeripheralInitial(void)
{
    //发送任意数组
    UsartSendBuffer((const unsigned char *)&Gu8SendBuffer[0],5);//从第0位置发送5个数据
UsartSendBuffer((const unsigned char *)&Gu8SendBuffer[6],5);//从第6位置发送5个数据

//发送带协议的数组
    UsartSendMessage((const unsigned char *)&Gu8SendMessage[0],100); //必须从第0位置发送
}




使用特权

评论回复
446
panqh| | 2018-11-16 13:41 | 只看该作者
为楼主伟大的善举点赞

使用特权

评论回复
447
mingxie| | 2018-11-16 22:00 | 只看该作者
学习了,顶顶顶!!!

使用特权

评论回复
448
avensun| | 2018-11-26 20:58 | 只看该作者
感谢楼主,**了这么长时间……

使用特权

评论回复
449
arima| | 2018-11-27 19:43 | 只看该作者
感谢楼主,**学习!!!!

使用特权

评论回复
450
少女姐姐| | 2018-11-28 13:25 | 只看该作者
这个必须顶啊

使用特权

评论回复
451
xiaokia| | 2018-12-4 11:22 | 只看该作者
太感谢,为鸿哥无私奉献的精神点赞!

使用特权

评论回复
452
callhgd| | 2018-12-5 12:34 | 只看该作者
为你的认真执着点个赞吧。

使用特权

评论回复
453
xiaokia| | 2018-12-7 10:52 | 只看该作者
朝闻道夕死可矣,跪求更新

使用特权

评论回复
454
arima| | 2018-12-22 22:31 | 只看该作者
最近好几周没更新了,楼主继续加油!!!!

使用特权

评论回复
455
wengyj| | 2018-12-25 22:47 | 只看该作者
感谢楼主无私奉献,帮助我们得以重新认识了单片机。

使用特权

评论回复
456
liendong1977| | 2019-1-12 16:54 | 只看该作者
关注一下

使用特权

评论回复
457
ztzp| | 2019-1-15 19:58 | 只看该作者
楼主可能最近比较忙,别催他,耐心等待。

使用特权

评论回复
458
☆black| | 2019-1-21 09:28 | 只看该作者

想请问一下大家:
定时器中断1us产生一次,如何知道放入的代码量多少合适(比如定时中断控制12位并行DA转换器)呢?
还是只要不放入大循环或者阻塞延时一般都满足条件?

使用特权

评论回复
评论
☆black 2019-1-21 11:55 回复TA
@xiaokia :谢谢!刚刚看了一下其他帖子,说到STM32中断响应速度;中断时间太短,CPU干不了其他事;中断函数还是应该考虑一下执行指令时间 
xiaokia 2019-1-21 09:52 回复TA
1us时间太短,1MHZ机器周期都1us了,几条指令可能就超出了,会严重影响效率吧 
459
jianhong_wu|  楼主 | 2019-2-2 12:53 | 只看该作者
第一百三十四节:“应用层半双工”双机串口通讯的程序框架。
第一百三十四节.pdf (258.38 KB)
【134.1   应用层的“半双工”和“全双工”。】

       应用层的“半双工”。主机与从机在程序应用层采用“一问一答”的查询模式,主机是主动方,从机是被动方,主机问一句从机答一句,“聊天对话“的氛围很无趣很呆板。从机没有发言权,当从机想主动给主机发送一些数据时就“憋得慌”。半双工适用于大多数单向通讯的场合。
       应用层的“全双工”。主机与从机在程序应用层可以实现任意双向的通讯,这时从机也可变为主机,主机也可变为从机,就像两个人平时聊天,无所谓谁是从机谁是主机,也无所谓非要对方对我每句话都要应答附和(只要对方能听得清我讲什么就可以),“聊天对话”的氛围很生动很活泼。全双工适用于通讯更复杂的场合。
       本节从“半双工“开始讲,让初学者先熟悉双机通讯的基本程序框架,下一节再讲“全双工“。

【134.2   双机通讯的三类核心函数。】

       双机通讯在程序框架层面有三类核心的涵数,它们分别是:通讯过程的控制涵数,发送的队列驱动涵数,接收数据后的处理涵数。
       “通讯过程的控制涵数”的数量可以不止1个,每一个通讯事件都对应一个独立的“通讯过程的控制涵数”,根据通讯事件的数量,一个系统往往有N个“通讯过程的控制涵数”。顾名思义,它负责过程的控制,无论什么项目,凡是过程控制我都首选switch语句。此函数是属于上层应用的函数,它的基础底层是“发送的队列驱动涵数”和“接收数据后的处理涵数”这两个函数。
       “发送的队列驱动涵数”在系统中只有1个“发送的队列驱动涵数”,负责“通讯管道的占用”的分配,负责数据的具体发送。当同时存在很多“待发送”的请求指令时,此函数会根据“if ,else if...”的优先级,像队列一样安排各指令发送的先后顺序,确保各指令不会发生冲突。此函数属于底层的驱动函数。
       “接收数据后的处理涵数”在系统中只有1个,负责处理当前接收到的数据,它既属于“底层函数”也属于“应用层函数”,二者成分皆有。
       我们一旦深刻地领悟了这三类函数各自的分工与关联方式,将来应付再复杂的通讯系统都会脉络清析,游刃有余。

【134.3   例程的功能需求。】

       上位机与下位机都有一个一模一样的57个字节的大数组。在上位机端按下独立按键K1后,上位机开始与下位机建立通讯,上位机的目的是读取下位机的那57个字节的大数组,分批读取,每批读取10个字节,最后一批读取的是余下的7个字节。读取完毕后,上位机把读取到的大数组与自己的大数组进行对比:如果相等,表示通讯正确,蜂鸣器“长鸣”一声;如果不相等,表示通讯错误,蜂鸣器“短鸣”一声。在通讯过程中,如果出现通信异常(比如因为接收超时或者接收某批次数据错误而导致重发的次数超过最大限制的次数)也表示通讯错误,蜂鸣器也会发出“短鸣”一声的提示。

【134.4   例程的电路图。】

        两个单片机进行232串口通讯,一共需要3根线:1根作为共地线,其它2根是交叉的收发数据线(上位机的“接收线”连接下位机的“发送线”,上位机的“发送线”连接下位机的“接收线”),如下图所示:

       上图134.4.1  双机通讯的232串口接线图



       上图134.4.2  上位机的独立按键



       上图134.4.3 上位机的有源蜂鸣器

【134.5   例程的通讯协议。】

(一)通讯参数。波特率9600,校验位NONE(无),数据位8,停止位1。

(二)上位机读取下位机的数组容量的大小的指令。
        (1)上位机发送十六进制的数据:EB 01 00 00 00 07 ED。
         EB是数据头。
         01是指令类型,01代表请求下位机返回大数组的容量大小。
         00 00 00 07代表整个指令的数据长度。
         ED是前面所有字节数据的异或结果,用来作为校验数据。

       (2)下位机返回十六进制的数据:EB 01 00 00 00 0C XX XX XX XX ZZ。
         EB是数据头。
         01是指令类型,01代表返回大数组的容量大小。
         00 00 00 0B代表整个指令的数据长度
         XX XX XX XX代表大数组的容量大小
         ZZ是前面所有字节数据的异或结果,用来作为校验数据。

(三)上位机读取下位机的大数组的分段数据的指令。
       (1)上位机发送十六进制的数据:EB 02 00 00 00 0F RR RR RR RR YY YY YY YY ZZ
         EB是数据头
         02是指令类型,02代表请求下位机返回当前分段的数据。
         00 00 00 0F代表整个指令的数据长度
         RR RR RR RR代表请求下位机返回的数据的“请求起始地址”
         YY YY YY YY代表请求下位机从“请求起始地址”一次返回的数据长度
         ZZ是前面所有字节数据的异或结果,用来作为校验数据。

      (2)下位机返回十六进制的数据:EB 02 TT TT TT TT RR RR RR RR YY YY YY YY HH ...HH ZZ
        EB是数据头
        02是指令类型,02代表返回大数组当前分段的数据
        TT TT TT TT 代表整个指令的数据长度
        RR RR RR RR代表下位机返回数据时的“请求起始地址”
        YY YY YY YY代表下位机从“请求起始地址”一次返回的数据长度
        HH ...HH代表中间有效的数据内容
        ZZ是前面所有字节数据的异或结果,用来作为校验数据。

【134.6   解决本节例程编译不过去的方法。】

        因为本节用到的全局变量比较多,如果有初学者在编译的时候出现“error C249: 'DATA': SEGMENT TOO LARGE”的提示,请按下图的窗口提示来设置一下编译的环境。



       上图134.5.1 设置编译的环境

【134.7   例程的上位机程序。】
上位机的C语言程序.rar (7.05 KB)
【134.8   例程的下位机程序。】

      下位机作为从机应答上位机的指令,程序相对简化了很多。不需要“通讯过程的控制涵数”,直接在“接收数据后的处理涵数”里启动“发送的队列驱动涵数”来发送应答的数据即可。发送应答数据后,也不用等待上位机的应答数据。
下位机的C语言程序.rar (4.85 KB)

使用特权

评论回复
评论
xiaokia 2019-2-2 20:12 回复TA
终于盼到坚哥的更新,好开心 
460
ztzp| | 2019-2-2 23:26 | 只看该作者
盼了两个多月终于更新了,谢谢。

使用特权

评论回复
发新帖 本帖赏金 42.00元(功能说明)我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则