实际应用中,在采用单片机进行项目设计时,常常会出现这样一种场景。场景中包含主控和其它多个从设备,且主控与从设备之间需要定时进行数据交互,如:恒温箱中,单片机定时读取从设备温度传感器的数据,根据获取的温度值,控制加热模块的加热方式。

简单应用中,可根据外设的技术手册,以及相应的接口协议(如:IIC,SPI等)直接获取从设备数据;若从设备逻辑复杂、实时性等有一定的技术要求,此时,就不得不考虑将从设备和其它外设单独作为一个”小系统“,通过特定的接口和主控进行通讯。

主控需要通过协议(自定义或通用协议)和“小系统”进行通讯。相应地,主控和“小系统”都需要实现协议的封包和解包函数。

基于以上内容,本文将介绍一种协议的设计方法,此方法源于乐鑫提供的ESP8266 RTOS源码中的AT指令部分。事实上,此设计方法非常通用,在芯片设计行业几乎是“标准”。

1. 源码下载

笔者已将AT相关的例程放在此网站,可直接下载(点击下载附件)。

2. 架构逻辑

(1)入口函数

./esp8266_at/user/user_main.c  -> at_init();

(2)函数处理逻辑

3. 指令介绍

(1)at.h

系统整个全局变量定义为枚举类型,增加了代码的可读性。

通过结构体+函数指针来实现所有指令的统一管理,后期维护将会很方便。

typedef enum
{
    at_statIdle,
    at_statRecving,
    at_statProcess,
    at_statIpSending,
    at_statIpSended,
    at_statIpTraning
} at_stateType;

......

typedef struct
{
    char *at_cmdName;
    int8_t at_cmdLen;
    void (*at_testCmd)(uint8_t id);
    void (*at_queryCmd)(uint8_t id);
    void (*at_setupCmd)(uint8_t id, char *pPara);
    void (*at_exeCmd)(uint8_t id);
} at_funcationType;

(2)at_cmd.h

定义了相关指令的具体名称、长度、所需执行的函数等。采用此种方法定义指令,可在其它文件中实现具体函数功能,方便扩展。

#define at_cmdNum   32

at_funcationType at_fun[at_cmdNum]=
{
    {NULL, 0, NULL, NULL, NULL, at_exeCmdNull},
    {"E", 1, NULL, NULL, at_setupCmdE, NULL},
    {"+RST", 4, NULL, NULL, NULL, at_exeCmdRst},
    {"+GMR", 4, NULL, NULL, NULL, at_exeCmdGmr},
    {"+GSLP", 5, NULL, NULL, at_setupCmdGslp, NULL},
    {"+IPR", 4, NULL, NULL, at_setupCmdIpr, NULL},
#ifdef ali
    {"+UPDATE", 7, NULL, NULL, NULL, at_exeCmdUpdate},
#endif
    {"+CWMODE", 7, at_testCmdCwmode, at_queryCmdCwmode, at_setupCmdCwmode, NULL},
    {"+CWJAP", 6, NULL, at_queryCmdCwjap, at_setupCmdCwjap, NULL},
    {"+CWLAP", 6, NULL, NULL, at_setupCmdCwlap, at_exeCmdCwlap},
    {"+CWQAP", 6, at_testCmdCwqap, NULL, NULL, at_exeCmdCwqap},
    {"+CWSAP", 6, NULL, at_queryCmdCwsap, at_setupCmdCwsap, NULL},
    {"+CWLIF", 6, NULL, NULL, NULL, at_exeCmdCwlif},
    {"+CWDHCP", 7, NULL, at_queryCmdCwdhcp, at_setupCmdCwdhcp, NULL},
    {"+CIFSR", 6, at_testCmdCifsr, NULL, at_setupCmdCifsr, at_exeCmdCifsr},
    {"+CIPSTAMAC", 10, NULL, at_queryCmdCipstamac, at_setupCmdCipstamac, NULL},
    {"+CIPAPMAC", 9, NULL, at_queryCmdCipapmac, at_setupCmdCipapmac, NULL},
    {"+CIPSTA", 7, NULL, at_queryCmdCipsta, at_setupCmdCipsta, NULL},
    {"+CIPAP", 6, NULL, at_queryCmdCipap, at_setupCmdCipap, NULL},
    {"+CIPSTATUS", 10, at_testCmdCipstatus, NULL, NULL, at_exeCmdCipstatus},
    {"+CIPSTART", 9, at_testCmdCipstart, NULL, at_setupCmdCipstart, NULL},
    {"+CIPCLOSE", 9, at_testCmdCipclose, NULL, at_setupCmdCipclose, at_exeCmdCipclose},
    {"+CIPSEND", 8, at_testCmdCipsend, NULL, at_setupCmdCipsend, at_exeCmdCipsend},
    {"+CIPMUX", 7, NULL, at_queryCmdCipmux, at_setupCmdCipmux, NULL},
    {"+CIPSERVER", 10, NULL, NULL,at_setupCmdCipserver, NULL},
    {"+CIPMODE", 8, NULL, at_queryCmdCipmode, at_setupCmdCipmode, NULL},
    {"+CIPSTO", 7, NULL, at_queryCmdCipsto, at_setupCmdCipsto, NULL},
    {"+CIUPDATE", 9, NULL, NULL, NULL, at_exeCmdCiupdate},
    {"+CIPING", 7, NULL, NULL, NULL, at_exeCmdCiping},
    {"+CIPAPPUP", 9, NULL, NULL, NULL, at_exeCmdCipappup},
#ifdef ali
    {"+MPINFO", 7, NULL, NULL, at_setupCmdMpinfo, NULL}
#endif
};

4. 解析和处理

(1)at_recvTask

接收串口数据,并对相应的AT指令进行解析,然后交给at_procTask函数进行处理。

/**
  * @brief  Uart receive task.
  * @param  events: contain the uart receive data
  * @retval None
  */
static void ICACHE_FLASH_ATTR
at_recvTask(os_event_t *events)
{
    static uint8_t atHead[2];
    static uint8_t *pCmdLine;
    uint8_t temp;
    //add transparent determine
    while(READ_PERI_REG(UART_STATUS(UART0)) & (UART_RXFIFO_CNT << UART_RXFIFO_CNT_S))
    {
        WRITE_PERI_REG(0X60000914, 0x73); //WTD
        if(at_state != at_statIpTraning)
        {
            temp = READ_PERI_REG(UART_FIFO(UART0)) & 0xFF;
            if((temp != '\n') && (echoFlag))
            {
                uart_tx_one_char(temp); //display back
            }
        }
        switch(at_state)
        {
        case at_statIdle: //serch "AT" head
            atHead[0] = atHead[1];
            atHead[1] = temp;
            if((os_memcmp(atHead, "AT", 2) == 0) || (os_memcmp(atHead, "at", 2) == 0))
            {
                at_state = at_statRecving;
                pCmdLine = at_cmdLine;
                atHead[1] = 0x00;
            }
            else if(temp == '\n') //only get enter
            {
                uart0_sendStr("\r\nERROR\r\n");
            }
            break;
        case at_statRecving: //push receive data to cmd line
            *pCmdLine = temp;
            if(temp == '\n')
            {
                system_os_post(at_procTaskPrio, 0, 0);
                pCmdLine++;
                *pCmdLine = '\0';
                at_state = at_statProcess;
                if(echoFlag)
                {
                    uart0_sendStr("\r\n");
                }
            }
            else if(pCmdLine >= &at_cmdLine[at_cmdLenMax - 1])
            {
                at_state = at_statIdle;
            }
            pCmdLine++;
            break;
        case at_statProcess: //process data
            if(temp == '\n')
            {
                uart0_sendStr("\r\nbusy p...\r\n");
            }
            break;
        case at_statIpSending:
            *pDataLine = temp;
            if((pDataLine >= &at_dataLine[at_sendLen - 1]) || (pDataLine >= &at_dataLine[at_dataLenMax - 1]))
            {
                system_os_post(at_procTaskPrio, 0, 0);
                at_state = at_statIpSended;
            }
            else
            {
                pDataLine++;
            }
            break;
        case at_statIpSended: //send data
            if(temp == '\n')
            {
                uart0_sendStr("busy s...\r\n");
            }
            break;
        case at_statIpTraning:
            os_timer_disarm(&at_delayCheck);
            if(pDataLine > &at_dataLine[at_dataLenMax - 1])
            {
                os_timer_arm(&at_delayCheck, 0, 0);
                os_printf("exceed\r\n");
                return;
            }
            else if(pDataLine == &at_dataLine[at_dataLenMax - 1])
            {
                temp = READ_PERI_REG(UART_FIFO(UART0)) & 0xFF;
                *pDataLine = temp;
                pDataLine++;
                at_tranLen++;
                os_timer_arm(&at_delayCheck, 0, 0);
                return;
            }
            else
            {
                temp = READ_PERI_REG(UART_FIFO(UART0)) & 0xFF;
                *pDataLine = temp;
                pDataLine++;
                at_tranLen++;
                os_timer_arm(&at_delayCheck, 20, 0);
            }
            break;

        default:
            if(temp == '\n')
            {
            }
            break;
        }
    }
    if(UART_RXFIFO_FULL_INT_ST == (READ_PERI_REG(UART_INT_ST(UART0)) & UART_RXFIFO_FULL_INT_ST))
    {
        WRITE_PERI_REG(UART_INT_CLR(UART0), UART_RXFIFO_FULL_INT_CLR);
    }
    else if(UART_RXFIFO_TOUT_INT_ST == (READ_PERI_REG(UART_INT_ST(UART0)) & UART_RXFIFO_TOUT_INT_ST))
    {
        WRITE_PERI_REG(UART_INT_CLR(UART0), UART_RXFIFO_TOUT_INT_CLR);
    }
    ETS_UART_INTR_ENABLE();
}

(2)at_procTask

对指令进行处理,具体函数在at_baseCmd.c、at_wifiCmd.c、at_ipCmd.c三个文件中实现,处理完成后并返回相应的数据。

/**
  * @brief  Task of process command or txdata.
  * @param  events: no used
  * @retval None
  */
static void ICACHE_FLASH_ATTR
at_procTask(os_event_t *events)
{
    if(at_state == at_statProcess)
    {
        at_cmdProcess(at_cmdLine);
        if(specialAtState)
        {
            at_state = at_statIdle;
        }
    }
    else if(at_state == at_statIpSended)
    {
        at_ipDataSending(at_dataLine);
        if(specialAtState)
        {
            at_state = at_statIdle;
        }
    }
    else if(at_state == at_statIpTraning)
    {
        at_ipDataSendNow();
    }
}

5. 指令分类

ESP8266 AT指令集(点击下载附件)可分为三大类:基础AT指令 、wifi功能AT指令、TCP/IP相关指令。

基础AT指令,在文件at_baseCmd.c中实现;wifi功能AT指令,在文件at_wifiCmd.c中实现;TCP/IP相关指令,在文件at_ipCmd.c中实现。

笔者说:

在设计协议框架时,考虑此例程的设计思路,将大大提高协议的灵活性。另外,建议开发者在进行嵌入式项目开发时,尽量在不同项目中复用之前已验证的协议框架,这样既可以缩短开发时间,又能保证框架的稳定性。

注意:本站所有文章除特别说明外,均为原创,转载请务必以超链接方式并注明作者出处。