显示板开发

因某产品量产需要附搭一个显示模块(可有可无,用在一些没有电脑的情况下)。因为产品主要的硬件已经定型了,所以没有集成在产品中,需要独立开发一个显示板。功能比较简单,但开发中遇到了一个关于数码管的问题,下面大体看一下:

1、显示板的功能

这次任务为:开发个显示板(我把它叫做从机)。要求实时接收显示主机所控制的系统的当前的运行状态,其中显示部分由一个五位八段数码管组成,从机与主机之间通过串口来通信,其中由从机主动向主机申请数据,主机收到申请包之后回传一个数据包。

当主机系统正常运行时,则会在最后三位数码管显示run;当伺服系统出现异常时,则会在数码管上显示AL.XXX,其中XXX为报警码,AL为报警单词Alarm的缩写。

ZGtM1P.png

任务虽然看起来不难,但开发调试过程也遇到了很多问题 。首先,先分析一下总体设计,然后得出主要的切入点为:

(1)显示部分。可以把五段八位数码管的显示封装成一个函数,该函数接收一个字符串,然后再数码管上显示这个字符串;

(2)通信部分。接收主机数据并解析,然后把有用的数据组包(组成一个字符串,如AL.XXX),最后进行显示。

首先,看一下程序流程图:

ZGaYAe.png

2、显示板的显示部分

首先,由于硬件部分不按常规设计的原因,导致软件就遇到了一个问题。(有时候硬件缺陷可以使用软件来弥补,但往往会增加了软件部分的负担)。按照常规,我们的数码管段选线引脚与MCU连接应该是按连续排列的引脚顺序连接的(如,a~dp连接着PA0~PA7,这样只要给A端口的数据寄存器写一个字节的段码数据就可以进行设置了)。

但是,硬件的同事(也是新人,哈哈)把这些引脚的顺序给打乱了,所以只能靠软件来重新排列一下,既然不能同时设定8个引脚,那可以一个引脚一个引脚的进行设置。所以,我把数码管段码数据的每个位给分离出来,保存到一个数组里,然后拿这个数组的数据去直接设置数码管段选线就可以了。具体代码实现如下:

/****************************************************************************************
** 函数: disp_one_bit_seg,显示一位数码管
**---------------------------------------------------------------------------------------
** 参数: code:数码管段码,为一个字节数据
** 返回: void
** 说明:
此函数可以达到数码管段码重组的目的,标准情况是数码管的段选线a~dp都会连续排列并且分布于1个字节之
中,这样,我们只要往GPIO数据寄存器data中写入相应的字节即可。但是,若段选线a~dp的硬件连接完全打         乱,则可运用该函数就可以达到与标准情况一样的效果。            
****************************************************************************************/
static void disp_one_bit_seg(unsigned char code)
{
    /* 把code的二进制位读出来保存到全局数组seg_dest_buf中 */
    read_bit(code);    
    set_seg_gpio_data(seg_dest_buf, SEG_BUF_LEN);
}

/****************************************************************************************
** 函数: set_seg_gpio_data,设置数码管段码GPIO数据寄存器
**---------------------------------------------------------------------------------------
** 参数: data:转换后的共阳极段码值   len:数组长度
** 返回: void
** 说明: SEG_A_GPIO~SEG_DP_GPIO的引脚是不是连续的已经无关紧要
****************************************************************************************/
static void set_seg_gpio_data(char data[], char len)
{
    SEG_A_GPIO = data[len-1];
    SEG_B_GPIO = data[len-2];
    SEG_C_GPIO = data[len-3];
    SEG_D_GPIO = data[len-4];
    SEG_E_GPIO = data[len-5];
    SEG_F_GPIO = data[len-6];
    SEG_G_GPIO = data[len-7];
    SEG_DP_GPIO = data[len-8];
}

/****************************************************************************************
** 函数: read_bit,读出一个字节数据的各个位
**---------------------------------------------------------------------------------------
** 参数: byte:单字节数据
** 返回: void
** 说明: 读取byte的各个位保存到全局数组seg_dest_buf中
****************************************************************************************/
static void read_bit(unsigned char byte)
{
    char i = 0;
    // 字节的低位保存在数组的最后一个元素中
    for (i = 7; i >= 0; i--)
    {
        seg_dest_buf[i] = 0x01 & byte;
        byte >>= 1;
    }
}

因为要拿来显示的字符数量也不多,因此我把它们都给列出来了,字符显示函数为:

/****************************************************************************************
** 函数: seg_disp_char,数码管的字符显示函数
**---------------------------------------------------------------------------------------
** 参数: ch:要显示的字符
** 返回: void
** 说明: 
****************************************************************************************/
static void seg_disp_char(char ch)
{
    switch(ch)
    {
    case '0': disp_one_bit_seg(CODE_0);     break;
    case '1': disp_one_bit_seg(CODE_1);     break;
    case '2': disp_one_bit_seg(CODE_2);     break;
    case '3': disp_one_bit_seg(CODE_3);     break;
    case '4': disp_one_bit_seg(CODE_4);     break;
    case '5': disp_one_bit_seg(CODE_5);     break;
    case '6': disp_one_bit_seg(CODE_6);     break;
    case '7': disp_one_bit_seg(CODE_7);     break;
    case '8': disp_one_bit_seg(CODE_8);     break;
    case '9': disp_one_bit_seg(CODE_9);     break;
    case 'A': disp_one_bit_seg(CODE_A);     break;
    case 'd': disp_one_bit_seg(CODE_d);     break;
    case 'n': disp_one_bit_seg(CODE_n);     break;
    case 'r': disp_one_bit_seg(CODE_r);     break;
    case 'u': disp_one_bit_seg(CODE_u);     break;
    case 'y': disp_one_bit_seg(CODE_y);     break;
    case 'L': disp_one_bit_seg(CODE_L);     break;
    case 'S': disp_one_bit_seg(CODE_5);     break;
    case ' ': disp_one_bit_seg(CODE_SPACE); break;
    default:break;
    } // end of switch
}

所以,字符串显示函数就可以通过字符显示函数来封装了,字符串显示函数如下:

/****************************************************************************************
** 函数: seg_disp_str,数码管的字符串显示函数
**---------------------------------------------------------------------------------------
** 参数: str:要显示的字符串
** 返回: void
** 说明: 
****************************************************************************************/
static void seg_disp_str(char *str)
{
    char i;

    for (i = SEG_NUM-1; i >= 0; i--)
    {
        SEG_ALL_OFF;           // 设置段选之前先熄灭所有的管
        sellect_bit_on(i);       // 打开位选
        seg_disp_char(str[i]); // 显示字符串中的各个字符
        delay(10000);           // 延时配合人眼反应时间
    }
}

3、显示板的通信部分

通信的硬件部分是232通信。首先,得知道从机发送数据的格式以及主机回传过来的数据格式是怎么样的。这些数据是由我们自己制定的数据协议,主机回传的数据示例:

0F0601FBA3B3C3B3A3A3B3C3B3A3A3B3C3B3A3A3B3C3B3A30D

其中,0F为帧头,06为功能码,01FB为报警码,0D为帧尾。当从机向主机发送申请包时,主机收到后会回发由帧头、校验码、帧尾等多字节的一帧数据,只要可以能判断接收到这一帧数据,那么,我就可以进行解析。我的处理方法很简单,就是只要判断接收到帧头、帧尾及功能码,就说明已经接收到了这一帧数据,然后进行解析并保存数据。(应该还有其他更严谨的处理方法,以后有机会再考虑)。数据解析及组包的具体实现的代码为:

/****************************************************************************************
** 函数: data_parse,帧数据解析函数
**---------------------------------------------------------------------------------------
** 参数: data:帧数据       str_pack:解析、组合后的字符串数据包
** 返回: void
** 说明:解析驱动器发送过来的状态监测帧数据,如0F0601FBA3B3C3B3A3A3B3C3B3A3A3B3C3B3A3A3B3C3B3A30D
        (0F为帧头,06为功能码,01FB为报警码,0D为帧尾)
****************************************************************************************/
static void data_parse(char *str_pack, Uint16 *data)
{
    static Uint16 alarm_code = 0;
    char alarm_code_buf[3] = {0};

    /* 接收到帧数据*/
    if ((0x0F == data[0]) && (0x06 == data[1]) && (0x0D == data[34]))
    {
        /* 报警码值不为0时为alarm状态 */
        if ((data[2] << 8) + data[3])
        {
            alarm_code = (data[2] << 8) + data[3];         // 计算报警码
            strcpy(str_pack, ALARM_STR_HEAD);             // 此时str_pack中的内容为"AL"
            sprintf(alarm_code_buf, "%.3d", alarm_code); // alarm_code_buf为"XXX"
            strcat(str_pack, alarm_code_buf);             // 此时str_pack中的内容为"ALXXX"
        }
        /* 报警码值为0时为run状态 */
        else
        {
            strcpy(str_pack, "  run");
        }
    }
}

解析判断时判断帧头、帧尾以及功能码是不是一致就可以确定这是一帧报警数据,然后就可以解析出报警码,再调用字符串操作函数进行字符串数据的组包,然后把组合好的字符串数据包通过函数形参str_pack传出即可,外部调用数码管的字符串显示函数进行显示即可。

4、最后

以上就是关于这个显示板的主要内容,主要是解决如何处理数码管段选线没有顺序连接在同一个硬件GPIO端口的问题以及通信部分数据解析的问题。


我的微信公众号:嵌入式大杂烩

VcSFJJ.md.png


 上一篇
【C语言笔记】ASCII码可见字符与不可见字符 【C语言笔记】ASCII码可见字符与不可见字符
1. 如何输出ASCII码?ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)是基于拉丁字母的一套电脑编码系统,主要用于显示现代英语和其他西欧语言。下面看
2018-11-26
下一篇 
【C语言笔记】变参函数 【C语言笔记】变参函数
提到变参函数,我们的感觉是不是既熟悉又陌生?感觉熟悉是因为我们平时都在使用着,如我们常使用的printf()函数与scanf()函数就是典型的变参函数。因为printf()函数是变参函数我们才可以根据我们的需要灵活地输出变量的值。如: pr
2018-11-22
  目录