1 Star 0 Fork 4

armku / FN1895E-MCU101

forked from 建伟F4nniu / FN1895E-MCU101 
加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
该仓库未声明开源许可证文件(LICENSE),使用请关注具体项目描述及其代码上游依赖。
克隆/下载
MCU043.md 21.40 KB
一键复制 编辑 原始数据 按行查看 历史
建伟F4nniu 提交于 2016-12-23 00:30 . Track 2 files into repository.

#第四十三节:通过串口用计数延时方式发送一串数据。

开场白:

上一节讲了通过串口用delay延时方式发送一串数据,这种方式要求发送一串数据的时候一气呵成,期间不能执行其它任务,由于delay(400)这个时间还不算很长,所以可以应用在很多简单任务的系统中。但是在某些任务量很多的系统中,实时运行的主任务不允许被长时间和经常性地中断,这个时候就需要用计数延时来替代delay延时。

本节要教会大家两个知识点:

  • 第一个:用计数延时方式发送一串数据的程序框架。
  • 第二个:环形消息队列的程序框架。

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

  • (1)硬件平台:
  • 基于朱兆祺51单片机学习板。
  • (2)实现功能:
  • 波特率是:9600.
  • 用朱兆祺51单片机学习板中的S1,S5,S9,S13作为独立按键。
  • 按一次按键S1,发送EB 00 55 01 00 00 00 00 41
  • 按一次按键S5,发送EB 00 55 02 00 00 00 00 42
  • 按一次按键S9,发送EB 00 55 03 00 00 00 00 43
  • 按一次按键S13,发送EB 00 55 04 00 00 00 00 44
  • (3)源代码讲解如下:
#include "REG52.H"


#define const_send_time  100  //累计主循环次数的计数延时 请根据项目实际情况来调整此数据大小

#define const_send_size  10  //串口发送数据的缓冲区数组大小

#define const_Message_size  10  //环形消息队列的缓冲区数组大小

#define const_key_time1  20    //按键去抖动延时的时间
#define const_key_time2  20    //按键去抖动延时的时间
#define const_key_time3  20    //按键去抖动延时的时间
#define const_key_time4  20    //按键去抖动延时的时间

#define const_voice_short  40   //蜂鸣器短叫的持续时间

void initial_myself(void);
void initial_peripheral(void);
//void delay_short(unsigned int uiDelayshort);
void delay_long(unsigned int uiDelaylong);

void eusart_send(unsigned char ucSendData);  //发送一个字节,内部没有每个字节之间的延时
void send_service(void);  //利用累计主循环次数的计数延时方式来发送一串数据

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

void key_service(void); //按键服务的应用程序
void key_scan(void); //按键扫描函数 放在定时中断里


void insert_message(unsigned char ucMessageTemp);  //插入新的消息到环形消息队列里
unsigned char get_message(void);  //从环形消息队列里提取消息



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

sbit key_sr1 = P0 ^ 0; //对应朱兆祺学习板的S1键
sbit key_sr2 = P0 ^ 1; //对应朱兆祺学习板的S5键
sbit key_sr3 = P0 ^ 2; //对应朱兆祺学习板的S9键
sbit key_sr4 = P0 ^ 3; //对应朱兆祺学习板的S13键

sbit key_gnd_dr = P0 ^ 4; //模拟独立按键的地GND,因此必须一直输出低电平



unsigned char ucSendregBuf[const_send_size]; //串口发送数据的缓冲区数组

unsigned char ucMessageBuf[const_Message_size]; //环形消息队列的缓冲区数据
unsigned int  uiMessageCurrent = 0; //环形消息队列的取数据当前位置
unsigned int  uiMessageInsert = 0; //环形消息队列的插入新消息时候的位置
unsigned int  uiMessageCnt = 0; //统计环形消息队列的消息数量  等于0时表示消息队列里没有消息

unsigned char ucMessage = 0; //当前获取到的消息

unsigned int  uiVoiceCnt = 0; //蜂鸣器鸣叫的持续时间计数器
unsigned char  ucVoiceLock = 0; //蜂鸣器鸣叫的原子锁

unsigned char ucKeySec = 0; //被触发的按键编号

unsigned int  uiKeyTimeCnt1 = 0; //按键去抖动延时计数器
unsigned char ucKeyLock1 = 0; //按键触发后自锁的变量标志

unsigned int  uiKeyTimeCnt2 = 0; //按键去抖动延时计数器
unsigned char ucKeyLock2 = 0; //按键触发后自锁的变量标志

unsigned int  uiKeyTimeCnt3 = 0; //按键去抖动延时计数器
unsigned char ucKeyLock3 = 0; //按键触发后自锁的变量标志

unsigned int  uiKeyTimeCnt4 = 0; //按键去抖动延时计数器
unsigned char ucKeyLock4 = 0; //按键触发后自锁的变量标志


unsigned char ucSendStep = 0; //发送一串数据的运行步骤
unsigned int  uiSendTimeCnt = 0; //累计主循环次数的计数延时器

unsigned int uiSendCnt = 0; //发送数据时的中间变量

void main()
{
    initial_myself();
    delay_long(100);
    initial_peripheral();
    while(1)
    {
        key_service(); //按键服务的应用程序
        send_service();  //利用累计主循环次数的计数延时方式来发送一串数据
    }

}

/* 注释一:
  * 通过判断数组下标是否超范围的条件,把一个数组的首尾连接起来,就像一个环形,
  * 因此命名为环形消息队列。环形消息队列有插入消息,获取消息两个核心函数,以及一个
  * 统计消息总数的uiMessageCnt核心变量,通过此变量,我们可以知道消息队列里面是否有消息需要处理.
  * 我在做项目中很少用消息队列的,印象中我只在两个项目中用过消息队列这种方法。大部分的单片机
  * 项目其实直接用一两个中间变量就可以起到传递消息的作用,就能满足系统的要求。以下是各变量的含义:
  * #define const_Message_size  10  //环形消息队列的缓冲区数组大小
  * unsigned char ucMessageBuf[const_Message_size]; //环形消息队列的缓冲区数据
  * unsigned int  uiMessageCurrent=0;  //环形消息队列的取数据当前位置
  * unsigned int  uiMessageInsert=0;  //环形消息队列的插入新消息时候的位置
  * unsigned int  uiMessageCnt=0;  //统计环形消息队列的消息数量  等于0时表示消息队列里没有消息
  */

void insert_message(unsigned char ucMessageTemp)  //插入新的消息到环形消息队列里
{
    if(uiMessageCnt < const_Message_size) //消息总数小于环形消息队列的缓冲区才允许插入新消息
    {
        ucMessageBuf[uiMessageInsert] = ucMessageTemp;

        uiMessageInsert++;  //插入新消息时候的位置
        if(uiMessageInsert >= const_Message_size) //到了缓冲区末尾,则从缓冲区的开头重新开始。数组的首尾连接,看起来就像环形
        {
            uiMessageInsert = 0;
        }
        uiMessageCnt++; //消息数量累加  等于0时表示消息队列里没有消息
    }
}

unsigned char get_message(void)  //从环形消息队列里提取消息
{
    unsigned char ucMessageTemp = 0; //返回的消息中间变量,默认为0

    if(uiMessageCnt > 0) //只有消息数量大于0时才可以提取消息
    {
        ucMessageTemp = ucMessageBuf[uiMessageCurrent];
        uiMessageCurrent++;  //环形消息队列的取数据当前位置
        if(uiMessageCurrent >= const_Message_size) //到了缓冲区末尾,则从缓冲区的开头重新开始。数组的首尾连接,看起来就像环形
        {
            uiMessageCurrent = 0;
        }
        uiMessageCnt--; //每提取一次,消息数量就减一  等于0时表示消息队列里没有消息
    }

    return ucMessageTemp;
}


void send_service(void)  //利用累计主循环次数的计数延时方式来发送一串数据
{
    switch(ucSendStep)  //发送一串数据的运行步骤
    {
    case 0:   //从环形消息队列里提取消息
        if(uiMessageCnt > 0) //说明有消息需要处理
        {
            ucMessage = get_message();
            switch(ucMessage)   //消息处理
            {
            case 1:
                ucSendregBuf[0] = 0xeb;  //把准备发送的数据放入发送缓冲区
                ucSendregBuf[1] = 0x00;
                ucSendregBuf[2] = 0x55;
                ucSendregBuf[3] = 0x01;  //01代表1号键
                ucSendregBuf[4] = 0x00;
                ucSendregBuf[5] = 0x00;
                ucSendregBuf[6] = 0x00;
                ucSendregBuf[7] = 0x00;
                ucSendregBuf[8] = 0x41;

                uiSendCnt = 0; //发送数据的中间变量清零
                uiSendTimeCnt = 0; //累计主循环次数的计数延时器清零
                ucSendStep = 1; //切换到下一步发送一串数据
                break;
            case 2:
                ucSendregBuf[0] = 0xeb;  //把准备发送的数据放入发送缓冲区
                ucSendregBuf[1] = 0x00;
                ucSendregBuf[2] = 0x55;
                ucSendregBuf[3] = 0x02;  //02代表2号键
                ucSendregBuf[4] = 0x00;
                ucSendregBuf[5] = 0x00;
                ucSendregBuf[6] = 0x00;
                ucSendregBuf[7] = 0x00;
                ucSendregBuf[8] = 0x42;

                uiSendCnt = 0; //发送数据的中间变量清零
                uiSendTimeCnt = 0; //累计主循环次数的计数延时器清零
                ucSendStep = 1; //切换到下一步发送一串数据
                break;
            case 3:
                ucSendregBuf[0] = 0xeb;  //把准备发送的数据放入发送缓冲区
                ucSendregBuf[1] = 0x00;
                ucSendregBuf[2] = 0x55;
                ucSendregBuf[3] = 0x03;  //03代表3号键
                ucSendregBuf[4] = 0x00;
                ucSendregBuf[5] = 0x00;
                ucSendregBuf[6] = 0x00;
                ucSendregBuf[7] = 0x00;
                ucSendregBuf[8] = 0x43;

                uiSendCnt = 0; //发送数据的中间变量清零
                uiSendTimeCnt = 0; //累计主循环次数的计数延时器清零
                ucSendStep = 1; //切换到下一步发送一串数据
                break;
            case 4:
                ucSendregBuf[0] = 0xeb;  //把准备发送的数据放入发送缓冲区
                ucSendregBuf[1] = 0x00;
                ucSendregBuf[2] = 0x55;
                ucSendregBuf[3] = 0x04;  //04代表4号键
                ucSendregBuf[4] = 0x00;
                ucSendregBuf[5] = 0x00;
                ucSendregBuf[6] = 0x00;
                ucSendregBuf[7] = 0x00;
                ucSendregBuf[8] = 0x44;

                uiSendCnt = 0; //发送数据的中间变量清零
                uiSendTimeCnt = 0; //累计主循环次数的计数延时器清零
                ucSendStep = 1; //切换到下一步发送一串数据
                break;

            default:  //如果没有符合要求的消息,则不处理

                ucSendStep = 0; //维持现状,不切换
                break;
            }
        }
        break;

    case 1:  //利用累加主循环次数的计数延时方式来发送一串数据

        /* 注释二:
          * 这里的计数延时为什么不用累计定时中断次数的延时,而用累计主循环次数的计数延时?
          * 因为本程序定时器中断一次需要500个指令时间,时间分辨率太低,不方便微调时间。因此我
          * 就用累计主循环次数的计数延时方式,在做项目的时候,各位读者应该根据系统的实际情况
          * 来调整const_send_time的大小。
          */
        uiSendTimeCnt++;  //累计主循环次数的计数延时,为每个字节之间增加延时,
        if(uiSendTimeCnt > const_send_time) //请根据实际系统的情况,调整const_send_time的大小
        {
            uiSendTimeCnt = 0;

            eusart_send(ucSendregBuf[uiSendCnt]);  //发送一串数据给上位机
            uiSendCnt++;
            if(uiSendCnt >= 9) //说明数据已经发送完毕
            {
                uiSendCnt = 0;
                ucSendStep = 0; //返回到上一步,处理其它未处理的消息
            }
        }

        break;
    }

}


void eusart_send(unsigned char ucSendData)
{

    ES = 0; //关串口中断
    TI = 0; //清零串口发送完成中断请求标志
    SBUF = ucSendData; //发送一个字节

    /* 注释三:
      * 根据我个人的经验,在发送一串数据中,每个字节之间必须添加一个延时,用来等待串口发送完成。
      * 当然,也有一些朋友可能不增加延时,直接靠单片机自带的发送完成标志位来判断,但是我以前
      * 在做项目中,感觉单单靠发送完成标志位来判断还是容易出错(当然也有可能是我自身程序的问题),
      * 所以后来在大部分的项目中我就干脆靠延时来等待它发送完成。我在51,PIC单片机中都是这么做的。
      * 但是,凭我的经验,在stm32单片机中,可以不增加延时,直接靠单片机自带的标志位来判断就很可靠。
      */

    //  delay_short(400);  //因为外部在每个发送字节之间用了累计主循环次数的计数延时,因此不要此行的delay延时

    TI = 0; //清零串口发送完成中断请求标志
    ES = 1; //允许串口中断

}


void key_scan(void)//按键扫描函数 放在定时中断里
{


    if(key_sr1 == 1) //IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
    {
        ucKeyLock1 = 0; //按键自锁标志清零
        uiKeyTimeCnt1 = 0; //按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。
    }
    else if(ucKeyLock1 == 0) //有按键按下,且是第一次被按下
    {
        uiKeyTimeCnt1++; //累加定时中断次数
        if(uiKeyTimeCnt1 > const_key_time1)
        {
            uiKeyTimeCnt1 = 0;
            ucKeyLock1 = 1; //自锁按键置位,避免一直触发
            ucKeySec = 1;  //触发1号键
        }
    }

    if(key_sr2 == 1) //IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
    {
        ucKeyLock2 = 0; //按键自锁标志清零
        uiKeyTimeCnt2 = 0; //按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。
    }
    else if(ucKeyLock2 == 0) //有按键按下,且是第一次被按下
    {
        uiKeyTimeCnt2++; //累加定时中断次数
        if(uiKeyTimeCnt2 > const_key_time2)
        {
            uiKeyTimeCnt2 = 0;
            ucKeyLock2 = 1; //自锁按键置位,避免一直触发
            ucKeySec = 2;  //触发2号键
        }
    }

    if(key_sr3 == 1) //IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
    {
        ucKeyLock3 = 0; //按键自锁标志清零
        uiKeyTimeCnt3 = 0; //按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。
    }
    else if(ucKeyLock3 == 0) //有按键按下,且是第一次被按下
    {
        uiKeyTimeCnt3++; //累加定时中断次数
        if(uiKeyTimeCnt3 > const_key_time3)
        {
            uiKeyTimeCnt3 = 0;
            ucKeyLock3 = 1; //自锁按键置位,避免一直触发
            ucKeySec = 3;  //触发3号键
        }
    }

    if(key_sr4 == 1) //IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
    {
        ucKeyLock4 = 0; //按键自锁标志清零
        uiKeyTimeCnt4 = 0; //按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。
    }
    else if(ucKeyLock4 == 0) //有按键按下,且是第一次被按下
    {
        uiKeyTimeCnt4++; //累加定时中断次数
        if(uiKeyTimeCnt4 > const_key_time4)
        {
            uiKeyTimeCnt4 = 0;
            ucKeyLock4 = 1; //自锁按键置位,避免一直触发
            ucKeySec = 4;  //触发4号键
        }
    }


}


void key_service(void) //第三区 按键服务的应用程序
{


    switch(ucKeySec) //按键服务状态切换
    {
    case 1:// 1号键 对应朱兆祺学习板的S1键

        insert_message(0x01);  //把新消息插入到环形消息队列里等待处理

        ucVoiceLock = 1; //原子锁加锁,保护中断与主函数的共享数据
        uiVoiceCnt = const_voice_short; //按键声音触发,滴一声就停。
        ucVoiceLock = 0; //原子锁解锁

        ucKeySec = 0; //响应按键服务处理程序后,按键编号清零,避免一致触发
        break;
    case 2:// 2号键 对应朱兆祺学习板的S5键

        insert_message(0x02);  //把新消息插入到环形消息队列里等待处理

        ucVoiceLock = 1; //原子锁加锁,保护中断与主函数的共享数据
        uiVoiceCnt = const_voice_short; //按键声音触发,滴一声就停。
        ucVoiceLock = 0; //原子锁解锁

        ucKeySec = 0; //响应按键服务处理程序后,按键编号清零,避免一致触发
        break;
    case 3:// 3号键 对应朱兆祺学习板的S9键

        insert_message(0x03);  //把新消息插入到环形消息队列里等待处理

        ucVoiceLock = 1; //原子锁加锁,保护中断与主函数的共享数据
        uiVoiceCnt = const_voice_short; //按键声音触发,滴一声就停。
        ucVoiceLock = 0; //原子锁解锁

        ucKeySec = 0; //响应按键服务处理程序后,按键编号清零,避免一致触发
        break;
    case 4:// 4号键 对应朱兆祺学习板的S13键

        insert_message(0x04);  //把新消息插入到环形消息队列里等待处理

        ucVoiceLock = 1; //原子锁加锁,保护中断与主函数的共享数据
        uiVoiceCnt = const_voice_short; //按键声音触发,滴一声就停。
        ucVoiceLock = 0; //原子锁解锁

        ucKeySec = 0; //响应按键服务处理程序后,按键编号清零,避免一致触发
        break;

    }
}



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

    /* 注释四:
      * 此处多增加一个原子锁,作为中断与主函数共享数据的保护,实际上是借鉴了"红金龙吸味"关于原子锁的建议.
      */

    if(ucVoiceLock == 0) //原子锁判断
    {
        if(uiVoiceCnt != 0)
        {

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

        }
        else
        {

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

        }
    }

    key_scan();//按键扫描函数


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


void usart_receive(void) interrupt 4                 //串口中断
{

    if(RI == 1)
    {
        RI = 0;   //接收中断,及时把接收中断标志位清零



    }
    else
    {
        TI = 0;    //发送中断,及时把发送中断标志位清零
    }

}



//void delay_short(unsigned int uiDelayShort)
//{
//   unsigned int i;
//   for(i=0;i<uiDelayShort;i++)
//   {
//     ;   //一个分号相当于执行一条空语句
//   }
//}


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)  //第一区 初始化单片机
{
    /* 注释五:
    * 矩阵键盘也可以做独立按键,前提是把某一根公共输出线输出低电平,
    * 模拟独立按键的触发地,本程序中,把key_gnd_dr输出低电平。
    * 朱兆祺51学习板的S1和S5两个按键就是本程序中用到的两个独立按键。
    */
    key_gnd_dr = 0; //模拟独立按键的地GND,因此必须一直输出低电平

    led_dr = 0; //关Led灯
    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); //串口波特率9600。
    TR1 = 1;

}

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

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

}

总结陈词:

前面几个章节中,每个章节要么独立地讲解串口收数据,要么独立地讲解发数据,实际上在大部分的项目中,串口都需要“一收一应答”的握手协议,上位机作为主机,单片机作为从机,主机先发一串数据,从机收到数据后进行校验判断,如果校验正确则返回正确应答指令,如果校验错误则返回错误应答指令,主机收到应答指令后,如果发现是正确应答指令则继续发送其它的新数据,如果发现是错误应答指令,或者超时没有接收到任何应答指令,则继续重发,如果连续重发三次都是错误应答或者无应答,主机就进行报错处理。读者只要把我的串口收发程序结合起来,就很容易实现这样的功能,我就不再详细讲解了。从下一节开始我讲解单片机掉电后数据保存的内容,欲知详情,请听下回分解-----利用AT24C02进行掉电后的数据保存。

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

C
1
https://gitee.com/armku/FN1895E-MCU101.git
git@gitee.com:armku/FN1895E-MCU101.git
armku
FN1895E-MCU101
FN1895E-MCU101
master

搜索帮助