1 Star 0 Fork 4

armku / FN1895E-MCU101

forked from 建伟F4nniu / FN1895E-MCU101 
Create your Gitee Account
Explore and code with more than 12 million developers,Free private repositories !:)
Sign up
This repository doesn't specify license. Please pay attention to the specific project description and its upstream code dependency when using it.
Clone or Download
MCU044.md 42.54 KB
Copy Edit Raw Blame History
建伟F4nniu authored 2016-12-23 00:30 . Track 2 files into repository.

#第四十四节:从机的串口收发综合程序框架

开场白:

根据上一节的预告,本来这一节内容打算讲“利用AT24C02进行掉电后的数据保存”的,但是由于网友“261854681”强烈建议我讲一个完整的串口收发程序实例,因此我决定再花两节篇幅讲讲这方面的内容。

实际上在大部分的项目中,串口都需要“一收一应答”的握手协议,上位机作为主机,单片机作为从机,主机先发一串数据,从机收到数据后进行校验判断,如果校验正确则返回正确应答指令,如果校验错误则返回错误应答指令,主机收到应答指令后,如果发现是正确应答指令则继续发送其它的新数据,如果发现是错误应答指令,或者超时没有接收到任何应答指令,则继续重发,如果连续重发三次都是错误应答或者无应答,主机就进行报错处理。

这节先讲从机的收发端程序实例。要教会大家三个知识点:

  • 第一个:为了保证串口中断接收的数据不丢失,在初始化时必须设置IP = 0x10,相当于把串口中断设置为最高优先级,这个时候,串口中断可以打断任何其他的中断服务函数,实现中断嵌套。
  • 第二个:从机端的收发端程序框架。
  • 第三个:从机的状态指示程序框架。可以指示待机,通讯中,超时出错三种状态。

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

  • (1)硬件平台:
  • 基于朱兆祺51单片机学习板。
  • (2)实现功能:
  • 显示和独立按键部分根据第29节的程序来改编,用朱兆祺51单片机学习板中的S1,S5,S9,S13作为独立按键。
  • 一共有4个窗口。每个窗口显示一个参数。有两种更改参数的方式:
  • 第一种:按键更改参数:
  • 第8,7,6,5位数码管显示当前窗口,P-1代表第1个窗口,P-2代表第2个窗口,P-3代表第3个窗口,P-4代表第1个窗口。
  • 第4,3,2,1位数码管显示当前窗口被设置的参数。范围是从0到9999。S1是加按键,按下此按键会依次增加当前窗口的参数。S5是减按键,按下此按键会依次减少当前窗口的参数。S9是切换窗口按键,按下此按键会依次循环切换不同的窗口。S13是复位按键,当通讯超时蜂鸣器报警时,可以按下此键清除报警。
  • 第二种:通过串口来更改参数:
  • 波特率是:9600.
  • 通讯协议:EB 00 55 GG 00 02 XX XX CY
  • 其中第1,2,3位EB 00 55就是数据头
  • 其中第4位GG就是数据类型。01代表更改参数1,02代表更改参数2,03代表更改参数3,04代表更改参数4,
  • 其中第5,6位00 02就是有效数据长度。高位在左,低位在右。
  • 其中从第7,8位XX XX是被更改的参数。高位在左,低位在右。
  • 第9位CY是累加和,前面所有字节的累加。
  • 一个完整的通讯必须接收完4串数据,每串数据之间的间隔时间不能超过10秒钟,否则认为通讯超时出错引发蜂鸣器报警。如果接收到得数据校验正确,
  • 则返回校验正确应答:eb 00 55 f5 00 00 35,
  • 否则返回校验出错应答::eb 00 55 fa 00 00 3a。
  • 系统处于待机状态时,LED灯一直亮,
  • 系统处于非待机状态时,LED灯闪烁,
  • 系统处于通讯超时出错状态时,LED灯闪烁,并且蜂鸣器间歇鸣叫报警。
  • 通过电脑的串口助手,依次发送以下测试数据,将会分别更改参数1,参数2,参数3,参数4。注意,每串数据之间的时间最大不能超过10秒,否则系统认为通讯超时报警。
  • 把参数1更改为十进制的1: eb 00 55 01 00 02 00 01 44
  • 把参数2更改为十进制的12: eb 00 55 02 00 02 00 0c 50
  • 把参数3更改为十进制的123: eb 00 55 03 00 02 00 7b c0
  • 把参数4更改为十进制的1234:eb 00 55 04 00 02 04 d2 1c
  • (3)源代码讲解如下:
#include "REG52.H"

#define const_voice_short  40   //蜂鸣器短叫的持续时间
#define const_key_time1  20    //按键去抖动延时的时间
#define const_key_time2  20    //按键去抖动延时的时间
#define const_key_time3  20    //按键去抖动延时的时间
#define const_key_time4  20    //按键去抖动延时的时间

#define const_led_0_5s  200   //大概0.5秒的时间
#define const_led_1s    400   //大概1秒的时间

#define const_send_time_out   4000  //通讯超时出错的时间 大概10秒

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

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

void initial_myself(void);
void initial_peripheral(void);
void delay_short(unsigned int uiDelayShort);
void delay_long(unsigned int uiDelaylong);
//驱动数码管的74HC595
void dig_hc595_drive(unsigned char ucDigStatusTemp16_09, unsigned char ucDigStatusTemp08_01);
void display_drive(void); //显示数码管字模的驱动函数
void display_service(void); //显示的窗口菜单服务程序
//驱动LED的74HC595
void hc595_drive(unsigned char ucLedStatusTemp16_09, unsigned char ucLedStatusTemp08_01);

void T0_time(void);  //定时中断函数
void usart_receive(void); //串口接收中断函数
void usart_service(void);  //串口服务程序,在main函数里
void eusart_send(unsigned char ucSendData); //发送一个字节,内部自带每个字节之间的delay延时

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

void status_service(void);  //状态显示的应用程序


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,因此必须一直输出低电平
sbit beep_dr = P2 ^ 7; //蜂鸣器的驱动IO口
sbit led_dr = P3 ^ 5; //作为状态指示灯 亮的时候表示待机状态.闪烁表示非待机状态,处于正在发送数据或者出错的状态

sbit dig_hc595_sh_dr = P2 ^ 0; //数码管的74HC595程序
sbit dig_hc595_st_dr = P2 ^ 1;
sbit dig_hc595_ds_dr = P2 ^ 2;
sbit hc595_sh_dr = P2 ^ 3; //LED灯的74HC595程序
sbit hc595_st_dr = P2 ^ 4;
sbit hc595_ds_dr = P2 ^ 5;

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

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

unsigned char  ucSendCntLock = 0; //串口计时器的原子锁
unsigned char ucRcType = 0; //数据类型
unsigned int  uiRcSize = 0; //数据长度
unsigned char ucRcCy = 0; //校验累加和

unsigned int  uiLedCnt = 0; //控制Led闪烁的延时计时器
unsigned int  uiSendTimeOutCnt = 0; //用来识别接收数据超时的计时器
unsigned char ucSendTimeOutLock = 0; //原子锁


unsigned char ucStatus = 0; //当前状态变量 0代表待机 1代表正在通讯过程 2代表发送出错

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 int  uiVoiceCnt = 0; //蜂鸣器鸣叫的持续时间计数器
unsigned char  ucVoiceLock = 0; //蜂鸣器鸣叫的原子锁

unsigned char ucDigShow8;  //第8位数码管要显示的内容
unsigned char ucDigShow7;  //第7位数码管要显示的内容
unsigned char ucDigShow6;  //第6位数码管要显示的内容
unsigned char ucDigShow5;  //第5位数码管要显示的内容
unsigned char ucDigShow4;  //第4位数码管要显示的内容
unsigned char ucDigShow3;  //第3位数码管要显示的内容
unsigned char ucDigShow2;  //第2位数码管要显示的内容
unsigned char ucDigShow1;  //第1位数码管要显示的内容

unsigned char ucDigDot8;  //数码管8的小数点是否显示的标志
unsigned char ucDigDot7;  //数码管7的小数点是否显示的标志
unsigned char ucDigDot6;  //数码管6的小数点是否显示的标志
unsigned char ucDigDot5;  //数码管5的小数点是否显示的标志
unsigned char ucDigDot4;  //数码管4的小数点是否显示的标志
unsigned char ucDigDot3;  //数码管3的小数点是否显示的标志
unsigned char ucDigDot2;  //数码管2的小数点是否显示的标志
unsigned char ucDigDot1;  //数码管1的小数点是否显示的标志
unsigned char ucDigShowTemp = 0; //临时中间变量
unsigned char ucDisplayDriveStep = 1; //动态扫描数码管的步骤变量

unsigned char ucWd1Update = 1; //窗口1更新显示标志
unsigned char ucWd2Update = 0; //窗口2更新显示标志
unsigned char ucWd3Update = 0; //窗口3更新显示标志
unsigned char ucWd4Update = 0; //窗口4更新显示标志
unsigned char ucWd = 1; //本程序的核心变量,窗口显示变量。类似于一级菜单的变量。代表显示不同的窗口。
unsigned int  uiSetData1 = 0; //本程序中需要被设置的参数1
unsigned int  uiSetData2 = 0; //本程序中需要被设置的参数2
unsigned int  uiSetData3 = 0; //本程序中需要被设置的参数3
unsigned int  uiSetData4 = 0; //本程序中需要被设置的参数4

unsigned char ucTemp1 = 0; //中间过渡变量
unsigned char ucTemp2 = 0; //中间过渡变量
unsigned char ucTemp3 = 0; //中间过渡变量
unsigned char ucTemp4 = 0; //中间过渡变量

//根据原理图得出的共阴数码管字模表
code unsigned char dig_table[] =
{
    0x3f,  //0       序号0
    0x06,  //1       序号1
    0x5b,  //2       序号2
    0x4f,  //3       序号3
    0x66,  //4       序号4
    0x6d,  //5       序号5
    0x7d,  //6       序号6
    0x07,  //7       序号7
    0x7f,  //8       序号8
    0x6f,  //9       序号9
    0x00,  //无      序号10
    0x40,  //-       序号11
    0x73,  //P       序号12
};
void main()
{
    initial_myself();
    delay_long(100);
    initial_peripheral();
    while(1)
    {
        key_service(); //按键服务的应用程序
        usart_service();  //串口服务程序
        display_service(); //显示的窗口菜单服务程序
        status_service();  //状态显示的应用程序
    }
}

void status_service(void)  //状态显示的应用程序
{
    if(ucStatus != 0) //处于非待机的状态,Led闪烁
    {
        if(uiLedCnt < const_led_0_5s) //大概0.5秒
        {
            led_dr = 1; //前半秒亮

            if(ucStatus == 2) //处于发送数据出错的状态,则蜂鸣器间歇鸣叫报警
            {
                ucVoiceLock = 1; //原子锁加锁,保护主函数与中断函数的共享变量uiVoiceCnt
                uiVoiceCnt = const_voice_short; //按键声音触发,滴一声就停。
                ucVoiceLock = 0; //原子锁解锁,保护主函数与中断函数的共享变量uiVoiceCnt
            }
        }
        else if(uiLedCnt < const_led_1s) //大概1秒
        {
            led_dr = 0; //前半秒灭
        }
        else
        {
            uiLedCnt = 0; //延时计时器清零,让Led灯处于闪烁的反复循环中
        }

    }
    else  //处于待机状态,Led一直亮
    {
        led_dr = 1;

    }



}



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

    unsigned int i;

    if(uiSendCnt >= const_receive_time && ucSendLock == 1) //说明超过了一定的时间内,再也没有新数据从串口来
    {

        ucSendLock = 0;  //处理一次就锁起来,不用每次都进来,除非有新接收的数据
        //下面的代码进入数据协议解析和数据处理的阶段

        uiRcMoveIndex = 0; //由于是判断数据头,所以下标移动变量从数组的0开始向最尾端移动

        while(uiRcregTotal >= 5 && uiRcMoveIndex <= (uiRcregTotal - 5))
        {

            if(ucRcregBuf[uiRcMoveIndex + 0] == 0xeb && ucRcregBuf[uiRcMoveIndex + 1] == 0x00 && ucRcregBuf[uiRcMoveIndex + 2] == 0x55) //数据头eb 00 55的判断
            {

                ucRcType = ucRcregBuf[uiRcMoveIndex + 3]; //数据类型  一个字节
                uiRcSize = ucRcregBuf[uiRcMoveIndex + 4]; //数据长度  两个字节
                uiRcSize = uiRcSize << 8;
                uiRcSize = uiRcSize + ucRcregBuf[uiRcMoveIndex + 5];

                ucRcCy = ucRcregBuf[uiRcMoveIndex + 6 + uiRcSize]; //记录最后一个字节的校验
                ucRcregBuf[uiRcMoveIndex + 6 + uiRcSize] = 0; //清零最后一个字节的累加和变量

                for(i = 0; i < (3 + 1 + 2 + uiRcSize); i++) //计算校验累加和
                {
                    ucRcregBuf[uiRcMoveIndex + 6 + uiRcSize] = ucRcregBuf[uiRcMoveIndex + 6 + uiRcSize] + ucRcregBuf[i];
                }


                if(ucRcCy == ucRcregBuf[uiRcMoveIndex + 6 + uiRcSize]) //如果校验正确,则进入以下数据处理
                {
                    switch(ucRcType)   //根据不同的数据类型来做不同的数据处理
                    {
                    case 0x01:   //设置参数1

                        ucStatus = 1; //从设置参数1开始,表示当前处于正在发送数据的状态

                        uiSetData1 = ucRcregBuf[uiRcMoveIndex + 6]; //把两个字节合并成一个int类型的数据
                        uiSetData1 = uiSetData1 << 8;
                        uiSetData1 = uiSetData1 + ucRcregBuf[uiRcMoveIndex + 7];
                        ucWd1Update = 1; //窗口1更新显示
                        break;

                    case 0x02:   //设置参数2

                        uiSetData2 = ucRcregBuf[uiRcMoveIndex + 6]; //把两个字节合并成一个int类型的数据
                        uiSetData2 = uiSetData2 << 8;
                        uiSetData2 = uiSetData2 + ucRcregBuf[uiRcMoveIndex + 7];
                        ucWd2Update = 1; //窗口2更新显示
                        break;

                    case 0x03:   //设置参数3

                        uiSetData3 = ucRcregBuf[uiRcMoveIndex + 6]; //把两个字节合并成一个int类型的数据
                        uiSetData3 = uiSetData3 << 8;
                        uiSetData3 = uiSetData3 + ucRcregBuf[uiRcMoveIndex + 7];
                        ucWd3Update = 1; //窗口3更新显示
                        break;

                    case 0x04:   //设置参数4

                        ucStatus = 0; //从设置参数4结束发送数据的状态,表示发送数据的过程成功,切换回待机状态

                        uiSetData4 = ucRcregBuf[uiRcMoveIndex + 6]; //把两个字节合并成一个int类型的数据
                        uiSetData4 = uiSetData4 << 8;
                        uiSetData4 = uiSetData4 + ucRcregBuf[uiRcMoveIndex + 7];
                        ucWd4Update = 1; //窗口4更新显示
                        break;


                    }


                    ucSendregBuf[0] = 0xeb;  //把准备发送的数据放入发送缓冲区
                    ucSendregBuf[1] = 0x00;
                    ucSendregBuf[2] = 0x55;
                    ucSendregBuf[3] = 0xf5; //代表校验正确
                    ucSendregBuf[4] = 0x00;
                    ucSendregBuf[5] = 0x00;
                    ucSendregBuf[6] = 0x35;

                    for(i = 0; i < 7; i++) //返回校验正确的应答指令
                    {
                        eusart_send(ucSendregBuf[i]);  //发送一串数据给上位机
                    }

                }
                else
                {
                    ucSendTimeOutLock = 1; //原子锁加锁
                    uiSendTimeOutCnt = 0; //超时计时器计时清零
                    ucSendTimeOutLock = 0; //原子锁解锁

                    ucSendregBuf[0] = 0xeb;  //把准备发送的数据放入发送缓冲区
                    ucSendregBuf[1] = 0x00;
                    ucSendregBuf[2] = 0x55;
                    ucSendregBuf[3] = 0xfa; //代表校验错误
                    ucSendregBuf[4] = 0x00;
                    ucSendregBuf[5] = 0x00;
                    ucSendregBuf[6] = 0x3a;

                    for(i = 0; i < 7; i++) //返回校验错误的应答指令
                    {
                        eusart_send(ucSendregBuf[i]);  //发送一串数据给上位机
                    }

                }

                ucSendTimeOutLock = 1; //原子锁加锁
                uiSendTimeOutCnt = 0; //超时计时器计时清零
                ucSendTimeOutLock = 0; //原子锁解锁

                break;   //退出循环
            }
            uiRcMoveIndex++; //因为是判断数据头,游标向着数组最尾端的方向移动
        }

        uiRcregTotal = 0; //清空缓冲的下标,方便下次重新从0下标开始接受新数据

    }

}


void eusart_send(unsigned char ucSendData) //发送一个字节,内部自带每个字节之间的delay延时
{

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

    delay_short(400);  //每个字节之间的延时,这里非常关键,也是最容易出错的地方。延时的大小请根据实际项目来调整

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

}


void display_service(void) //显示的窗口菜单服务程序
{

    switch(ucWd)  //本程序的核心变量,窗口显示变量。类似于一级菜单的变量。代表显示不同的窗口。
    {
    case 1:   //显示P--1窗口的数据
        if(ucWd1Update == 1) //窗口1要全部更新显示
        {
            ucWd1Update = 0; //及时清零标志,避免一直进来扫描
            ucDigShow8 = 12; //第8位数码管显示P
            ucDigShow7 = 11; //第7位数码管显示-
            ucDigShow6 = 1; //第6位数码管显示1
            ucDigShow5 = 10; //第5位数码管显示无

            //先分解数据
            ucTemp4 = uiSetData1 / 1000;
            ucTemp3 = uiSetData1 % 1000 / 100;
            ucTemp2 = uiSetData1 % 100 / 10;
            ucTemp1 = uiSetData1 % 10;

            //再过渡需要显示的数据到缓冲变量里,让过渡的时间越短越好

            if(uiSetData1 < 1000)
            {
                ucDigShow4 = 10; //如果小于1000,千位显示无
            }
            else
            {
                ucDigShow4 = ucTemp4; //第4位数码管要显示的内容
            }
            if(uiSetData1 < 100)
            {
                ucDigShow3 = 10; //如果小于100,百位显示无
            }
            else
            {
                ucDigShow3 = ucTemp3; //第3位数码管要显示的内容
            }
            if(uiSetData1 < 10)
            {
                ucDigShow2 = 10; //如果小于10,十位显示无
            }
            else
            {
                ucDigShow2 = ucTemp2; //第2位数码管要显示的内容
            }
            ucDigShow1 = ucTemp1; //第1位数码管要显示的内容
        }
        break;
    case 2:  //显示P--2窗口的数据
        if(ucWd2Update == 1) //窗口2要全部更新显示
        {
            ucWd2Update = 0; //及时清零标志,避免一直进来扫描
            ucDigShow8 = 12; //第8位数码管显示P
            ucDigShow7 = 11; //第7位数码管显示-
            ucDigShow6 = 2; //第6位数码管显示2
            ucDigShow5 = 10; //第5位数码管显示无
            ucTemp4 = uiSetData2 / 1000; //分解数据
            ucTemp3 = uiSetData2 % 1000 / 100;
            ucTemp2 = uiSetData2 % 100 / 10;
            ucTemp1 = uiSetData2 % 10;

            if(uiSetData2 < 1000)
            {
                ucDigShow4 = 10; //如果小于1000,千位显示无
            }
            else
            {
                ucDigShow4 = ucTemp4; //第4位数码管要显示的内容
            }
            if(uiSetData2 < 100)
            {
                ucDigShow3 = 10; //如果小于100,百位显示无
            }
            else
            {
                ucDigShow3 = ucTemp3; //第3位数码管要显示的内容
            }
            if(uiSetData2 < 10)
            {
                ucDigShow2 = 10; //如果小于10,十位显示无
            }
            else
            {
                ucDigShow2 = ucTemp2; //第2位数码管要显示的内容
            }
            ucDigShow1 = ucTemp1; //第1位数码管要显示的内容
        }
        break;
    case 3:  //显示P--3窗口的数据
        if(ucWd3Update == 1) //窗口3要全部更新显示
        {
            ucWd3Update = 0; //及时清零标志,避免一直进来扫描
            ucDigShow8 = 12; //第8位数码管显示P
            ucDigShow7 = 11; //第7位数码管显示-
            ucDigShow6 = 3; //第6位数码管显示3
            ucDigShow5 = 10; //第5位数码管显示无
            ucTemp4 = uiSetData3 / 1000; //分解数据
            ucTemp3 = uiSetData3 % 1000 / 100;
            ucTemp2 = uiSetData3 % 100 / 10;
            ucTemp1 = uiSetData3 % 10;
            if(uiSetData3 < 1000)
            {
                ucDigShow4 = 10; //如果小于1000,千位显示无
            }
            else
            {
                ucDigShow4 = ucTemp4; //第4位数码管要显示的内容
            }
            if(uiSetData3 < 100)
            {
                ucDigShow3 = 10; //如果小于100,百位显示无
            }
            else
            {
                ucDigShow3 = ucTemp3; //第3位数码管要显示的内容
            }
            if(uiSetData3 < 10)
            {
                ucDigShow2 = 10; //如果小于10,十位显示无
            }
            else
            {
                ucDigShow2 = ucTemp2; //第2位数码管要显示的内容
            }
            ucDigShow1 = ucTemp1; //第1位数码管要显示的内容
        }
        break;
    case 4:  //显示P--4窗口的数据
        if(ucWd4Update == 1) //窗口4要全部更新显示
        {
            ucWd4Update = 0; //及时清零标志,避免一直进来扫描
            ucDigShow8 = 12; //第8位数码管显示P
            ucDigShow7 = 11; //第7位数码管显示-
            ucDigShow6 = 4; //第6位数码管显示4
            ucDigShow5 = 10; //第5位数码管显示无
            ucTemp4 = uiSetData4 / 1000; //分解数据
            ucTemp3 = uiSetData4 % 1000 / 100;
            ucTemp2 = uiSetData4 % 100 / 10;
            ucTemp1 = uiSetData4 % 10;

            if(uiSetData4 < 1000)
            {
                ucDigShow4 = 10; //如果小于1000,千位显示无
            }
            else
            {
                ucDigShow4 = ucTemp4; //第4位数码管要显示的内容
            }
            if(uiSetData4 < 100)
            {
                ucDigShow3 = 10; //如果小于100,百位显示无
            }
            else
            {
                ucDigShow3 = ucTemp3; //第3位数码管要显示的内容
            }
            if(uiSetData4 < 10)
            {
                ucDigShow2 = 10; //如果小于10,十位显示无
            }
            else
            {
                ucDigShow2 = ucTemp2; //第2位数码管要显示的内容
            }
            ucDigShow1 = ucTemp1; //第1位数码管要显示的内容
        }
        break;
    }


}

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:// 加按键 对应朱兆祺学习板的S1键
        switch(ucWd)  //在不同的窗口下,设置不同的参数
        {
        case 1:
            uiSetData1++;
            if(uiSetData1 > 9999) //最大值是9999
            {
                uiSetData1 = 9999;
            }
            ucWd1Update = 1; //窗口1更新显示
            break;
        case 2:
            uiSetData2++;
            if(uiSetData2 > 9999) //最大值是9999
            {
                uiSetData2 = 9999;
            }
            ucWd2Update = 1; //窗口2更新显示
            break;
        case 3:
            uiSetData3++;
            if(uiSetData3 > 9999) //最大值是9999
            {
                uiSetData3 = 9999;
            }
            ucWd3Update = 1; //窗口3更新显示
            break;
        case 4:
            uiSetData4++;
            if(uiSetData4 > 9999) //最大值是9999
            {
                uiSetData4 = 9999;
            }
            ucWd4Update = 1; //窗口4更新显示
            break;
        }

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

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

    case 2:// 减按键 对应朱兆祺学习板的S5键
        switch(ucWd)  //在不同的窗口下,设置不同的参数
        {
        case 1:
            uiSetData1--;

            if(uiSetData1 > 9999)
            {
                uiSetData1 = 0; //最小值是0
            }
            ucWd1Update = 1; //窗口1更新显示
            break;
        case 2:
            uiSetData2--;
            if(uiSetData2 > 9999)
            {
                uiSetData2 = 0; //最小值是0
            }
            ucWd2Update = 1; //窗口2更新显示
            break;
        case 3:
            uiSetData3--;
            if(uiSetData3 > 9999)
            {
                uiSetData3 = 0; //最小值是0
            }
            ucWd3Update = 1; //窗口3更新显示
            break;
        case 4:
            uiSetData4--;
            if(uiSetData4 > 9999)
            {
                uiSetData4 = 0; //最小值是0
            }
            ucWd4Update = 1; //窗口4更新显示
            break;
        }

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

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

    case 3:// 切换窗口按键 对应朱兆祺学习板的S9键
        ucWd++;  //切换窗口
        if(ucWd > 4)
        {
            ucWd = 1;
        }
        switch(ucWd)  //在不同的窗口下,在不同的窗口下,更新显示不同的窗口
        {
        case 1:
            ucWd1Update = 1; //窗口1更新显示
            break;
        case 2:
            ucWd2Update = 1; //窗口2更新显示
            break;
        case 3:
            ucWd3Update = 1; //窗口3更新显示
            break;
        case 4:
            ucWd4Update = 1; //窗口4更新显示
            break;
        }
        ucVoiceLock = 1; //原子锁加锁,保护主函数与中断函数的共享变量uiVoiceCnt
        uiVoiceCnt = const_voice_short; //按键声音触发,滴一声就停。
        ucVoiceLock = 0; //原子锁解锁,保护主函数与中断函数的共享变量uiVoiceCnt

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

    case 4:// 复位按键 对应朱兆祺学习板的S13键
        switch(ucStatus)  //在不同的状态下,进行不同的操作
        {
        case 0:  //处于待机状态
            break;

        case 1:  //处于正在通讯的过程
            break;

        case 2: //发送数据出错,比如中间超时没有接收到数据
            ucStatus = 0; //切换回待机的状态
            break;
        }
        ucVoiceLock = 1; //原子锁加锁,保护主函数与中断函数的共享变量uiVoiceCnt
        uiVoiceCnt = const_voice_short; //按键声音触发,滴一声就停。
        ucVoiceLock = 0; //原子锁解锁,保护主函数与中断函数的共享变量uiVoiceCnt

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

        break;

    }
}

void display_drive(void)
{
    //以下程序,如果加一些数组和移位的元素,还可以压缩容量。但是鸿哥追求的不是容量,而是清晰的讲解思路
    switch(ucDisplayDriveStep)
    {
    case 1:  //显示第1位
        ucDigShowTemp = dig_table[ucDigShow1];
        if(ucDigDot1 == 1)
        {
            ucDigShowTemp = ucDigShowTemp | 0x80; //显示小数点
        }
        dig_hc595_drive(ucDigShowTemp, 0xfe);
        break;
    case 2:  //显示第2位
        ucDigShowTemp = dig_table[ucDigShow2];
        if(ucDigDot2 == 1)
        {
            ucDigShowTemp = ucDigShowTemp | 0x80; //显示小数点
        }
        dig_hc595_drive(ucDigShowTemp, 0xfd);
        break;
    case 3:  //显示第3位
        ucDigShowTemp = dig_table[ucDigShow3];
        if(ucDigDot3 == 1)
        {
            ucDigShowTemp = ucDigShowTemp | 0x80; //显示小数点
        }
        dig_hc595_drive(ucDigShowTemp, 0xfb);
        break;
    case 4:  //显示第4位
        ucDigShowTemp = dig_table[ucDigShow4];
        if(ucDigDot4 == 1)
        {
            ucDigShowTemp = ucDigShowTemp | 0x80; //显示小数点
        }
        dig_hc595_drive(ucDigShowTemp, 0xf7);
        break;
    case 5:  //显示第5位
        ucDigShowTemp = dig_table[ucDigShow5];
        if(ucDigDot5 == 1)
        {
            ucDigShowTemp = ucDigShowTemp | 0x80; //显示小数点
        }
        dig_hc595_drive(ucDigShowTemp, 0xef);
        break;
    case 6:  //显示第6位
        ucDigShowTemp = dig_table[ucDigShow6];
        if(ucDigDot6 == 1)
        {
            ucDigShowTemp = ucDigShowTemp | 0x80; //显示小数点
        }
        dig_hc595_drive(ucDigShowTemp, 0xdf);
        break;
    case 7:  //显示第7位
        ucDigShowTemp = dig_table[ucDigShow7];
        if(ucDigDot7 == 1)
        {
            ucDigShowTemp = ucDigShowTemp | 0x80; //显示小数点
        }
        dig_hc595_drive(ucDigShowTemp, 0xbf);
        break;
    case 8:  //显示第8位
        ucDigShowTemp = dig_table[ucDigShow8];
        if(ucDigDot8 == 1)
        {
            ucDigShowTemp = ucDigShowTemp | 0x80; //显示小数点
        }
        dig_hc595_drive(ucDigShowTemp, 0x7f);
        break;
    }
    ucDisplayDriveStep++;
    if(ucDisplayDriveStep > 8) //扫描完8个数码管后,重新从第一个开始扫描
    {
        ucDisplayDriveStep = 1;
    }

}

//数码管的74HC595驱动函数
void dig_hc595_drive(unsigned char ucDigStatusTemp16_09, unsigned char ucDigStatusTemp08_01)
{
    unsigned char i;
    unsigned char ucTempData;
    dig_hc595_sh_dr = 0;
    dig_hc595_st_dr = 0;
    ucTempData = ucDigStatusTemp16_09; //先送高8位
    for(i = 0; i < 8; i++)
    {
        if(ucTempData >= 0x80)dig_hc595_ds_dr = 1;
        else dig_hc595_ds_dr = 0;
        dig_hc595_sh_dr = 0;   //SH引脚的上升沿把数据送入寄存器
        delay_short(1);
        dig_hc595_sh_dr = 1;
        delay_short(1);
        ucTempData = ucTempData << 1;
    }
    ucTempData = ucDigStatusTemp08_01; //再先送低8位
    for(i = 0; i < 8; i++)
    {
        if(ucTempData >= 0x80)dig_hc595_ds_dr = 1;
        else dig_hc595_ds_dr = 0;
        dig_hc595_sh_dr = 0;   //SH引脚的上升沿把数据送入寄存器
        delay_short(1);
        dig_hc595_sh_dr = 1;
        delay_short(1);
        ucTempData = ucTempData << 1;
    }
    dig_hc595_st_dr = 0; //ST引脚把两个寄存器的数据更新输出到74HC595的输出引脚上并且锁存起来
    delay_short(1);
    dig_hc595_st_dr = 1;
    delay_short(1);
    dig_hc595_sh_dr = 0;  //拉低,抗干扰就增强
    dig_hc595_st_dr = 0;
    dig_hc595_ds_dr = 0;
}

//LED灯的74HC595驱动函数
void hc595_drive(unsigned char ucLedStatusTemp16_09, unsigned char ucLedStatusTemp08_01)
{
    unsigned char i;
    unsigned char ucTempData;
    hc595_sh_dr = 0;
    hc595_st_dr = 0;
    ucTempData = ucLedStatusTemp16_09; //先送高8位
    for(i = 0; i < 8; i++)
    {
        if(ucTempData >= 0x80)hc595_ds_dr = 1;
        else hc595_ds_dr = 0;
        hc595_sh_dr = 0;   //SH引脚的上升沿把数据送入寄存器
        delay_short(1);
        hc595_sh_dr = 1;
        delay_short(1);
        ucTempData = ucTempData << 1;
    }
    ucTempData = ucLedStatusTemp08_01; //再先送低8位
    for(i = 0; i < 8; i++)
    {
        if(ucTempData >= 0x80)hc595_ds_dr = 1;
        else hc595_ds_dr = 0;
        hc595_sh_dr = 0;   //SH引脚的上升沿把数据送入寄存器
        delay_short(1);
        hc595_sh_dr = 1;
        delay_short(1);
        ucTempData = ucTempData << 1;
    }
    hc595_st_dr = 0; //ST引脚把两个寄存器的数据更新输出到74HC595的输出引脚上并且锁存起来
    delay_short(1);
    hc595_st_dr = 1;
    delay_short(1);
    hc595_sh_dr = 0;  //拉低,抗干扰就增强
    hc595_st_dr = 0;
    hc595_ds_dr = 0;
}


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; //将串口接收到的数据缓存到接收缓冲区里

        if(ucSendCntLock == 0) //原子锁判断
        {
            ucSendCntLock = 1; //加锁
            uiSendCnt = 0; //及时喂狗,虽然在定时中断那边此变量会不断累加,但是只要串口的数据还没发送完毕,那么它永远也长不大,因为每个串口接收中断它都被清零。
            ucSendCntLock = 0; //解锁
        }

    }
    else  //我在其它单片机上都不用else这段代码的,可能在51单片机上多增加" TI = 0;"稳定性会更好吧。
    {
        TI = 0;  //如果不是串口接收中断,那么必然是串口发送中断,及时清除发送中断的标志,否则一直发送中断
    }

}

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


    /* 注释一:
      * 此处多增加一个原子锁,作为中断与主函数共享数据的保护,实际上是借鉴了"红金龙吸味"关于原子锁的建议.
      */
    if(ucSendCntLock == 0) //原子锁判断
    {
        ucSendCntLock = 1; //加锁
        if(uiSendCnt < const_receive_time) //如果超过这个时间没有串口数据过来,就认为一串数据已经全部接收完
        {
            uiSendCnt++;    //表面上这个数据不断累加,但是在串口中断里,每接收一个字节它都会被清零,除非这个中间没有串口数据过来
            ucSendLock = 1;   //开自锁标志
        }
        ucSendCntLock = 0; //解锁
    }

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

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

        }
        else
        {

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

        }
    }

    if(ucStatus != 0) //处于非待机的状态,Led闪烁
    {
        uiLedCnt++; //Led闪烁计时器不断累加
    }

    if(ucStatus == 1) //处于正在通讯的状态,
    {
        if(ucSendTimeOutLock == 0) //原子锁判断
        {
            uiSendTimeOutCnt++;   //超时计时器累加
            if(uiSendTimeOutCnt > const_send_time_out) //超时出错
            {
                uiSendTimeOutCnt = 0;
                ucStatus = 2; //切换到出错报警状态
            }
        }
    }



    key_scan(); //按键扫描函数
    display_drive();  //数码管字模的驱动函数

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

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就是本程序中用到的一个独立按键。
    */
    key_gnd_dr = 0; //模拟独立按键的地GND,因此必须一直输出低电平
    led_dr = 1; //点亮独立LED灯
    beep_dr = 1; //用PNP三极管控制蜂鸣器,输出高电平时不叫。
    hc595_drive(0x00, 0x00); //关闭所有经过另外两个74HC595驱动的LED灯
    TMOD = 0x01; //设置定时器0为工作方式1
    TH0 = 0xfe; //重装初始值(65535-500)=65035=0xfe0b
    TL0 = 0x0b;

    //配置串口
    SCON = 0x50;
    TMOD = 0X21;

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

    TH1 = TL1 = -(11059200L / 12 / 32 / 9600); //串口波特率为9600。
    TR1 = 1;
}
void initial_peripheral(void) //第二区 初始化外围
{
    ucDigDot8 = 0; //小数点全部不显示
    ucDigDot7 = 0;
    ucDigDot6 = 0;
    ucDigDot5 = 0;
    ucDigDot4 = 0;
    ucDigDot3 = 0;
    ucDigDot2 = 0;
    ucDigDot1 = 0;

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

总结陈词:

这节详细讲了从机收发端的程序框架,而主机端的程序则用电脑的串口助手来模拟。实际上,主机端的程序也有很多内容,它包括依次发送每一串数据,根据返回的应答来决定是否需要重发数据,重发三次如果没反应则进行报错,以及超时没接收到数据等等内容。主机收发端的程序框架是什么样的?欲知详情,请听下回分解-----主机的串口收发综合程序框架

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

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

Search