目录

一、前言

大家好,这里是 Hello_Embed。本篇笔记分几个部分介绍实现 Socket 封装的关键步骤,包括结构体定义、初始化流程、连接热点以及后台线程的具体实现。


二、结构体定义

AT_Device 结构体

typedef struct AT_Device {
    char *name;                              /* WIFI模块的名字 */
    SemaphoreHandle_t at_lock;               /* 发送AT命令前需要先获得这个锁 */
    SemaphoreHandle_t at_resp_sem;           /* 发送AT命令后等待这个信号量(等待AT命令的回应) */
    uint8_t resp[AT_RESP_BUF_SIZE];         /* 存放AT命令的回应数据 */
    uint32_t resp_len;                       /* AT命令回应数据的长度 */
    uint32_t resp_line_counts;               /* AT命令回应的数据有多少行 */
    uint32_t resp_status;                    /* AT命令的回应是OK还是ERR */
    PUART_Device ptUARTDev;                  /* 使用这个串口设备访问WIFI模块 */
    AT_Socket sockets[AT_DEVICE_SOCKETS_NUM];/* socket结构体数组 */
} AT_Device, *PAT_Device;

支持多少个 socket 要看具体的硬件芯片手册。

AT_Socket 结构体

typedef struct AT_Socket {
    uint32_t used;                           /* 0-未被占用, 1-被占用 */
    int type;                                /* TCP(SOCK_STREAM) or UDP(SOCK_DGRAM) */
    struct sockaddr local;                    /* 用来记录本地IP/PORT */
    struct sockaddr remote;                   /* 用来记录远端IP/PORT */
    void *user_data;                         /* AT模块自己的数据,
                                               * 对于W800就是硬件socket值,
                                               * 对于ESP8266就是link id */
    SemaphoreHandle_t at_packet_sem;         /* 读取网络数据时,等待这个信号量 */
    QueueHandle_t recv_queue;                 /* 队列,用来存放接收到的网络数据 */
} AT_Socket, *PAT_Socket;

使用 TCP 连接时远端信息保存在 remote,连接之后会得到硬件 socket 保存在 user_data,以后就可以读取队列 recv_queue 获得数据。


三、初始化流程

使用 FreeRTOS 实现 Socket 封装,不论作为 TCP-Server、TCP-Client 还是 UDP-Client,都需要先调用 at_init 初始化,再调用 at_connect_ap 连接热点。

at_init 函数

int at_init(char *uart_dev)
{
    return w800_init(uart_dev);
}

w800_init 详细实现

int w800_init(char *uart_dev)
{
    int i;
    PAT_Device ptDev = get_netdev();  /* 绑定网卡设备 */

    /* 创建AT设备用到的互斥锁、信号量 */
    ptDev->at_lock     = xSemaphoreCreateMutex();      /* 互斥锁:独占AT命令发送 */
    ptDev->at_resp_sem = xSemaphoreCreateBinary();     /* 二进制信号量:等待AT响应 */

    /* 根据名字获得UART设备 */
    ptDev->ptUARTDev   = GetUARTDevice(uart_dev);

    if (!ptDev->at_lock || !ptDev->at_resp_sem || !ptDev->ptUARTDev)
        return -1;

    /* 初始化socket结构体 */
    for (i = 0; i < AT_DEVICE_SOCKETS_NUM; i++)
    {
        memset(&ptDev->sockets[i], 0, sizeof(AT_Socket));
        ptDev->sockets[i].recv_queue = xQueueCreate(AT_RECV_BUF_SIZE, 1);
        ptDev->sockets[i].at_packet_sem = xSemaphoreCreateBinary();
        if (!ptDev->sockets[i].recv_queue || !ptDev->sockets[i].at_packet_sem)
            return -1;
    }

    /* 创建后台线程(用来解析数据) */
    xTaskCreate(
        w800_parser,                       /* 函数指针, 任务函数 */
        "w800_parser",                      /* 任务的名字 */
        AT_PARSER_TASK_STACK,               /* 栈大小 */
        ptDev,                              /* 调用任务函数时传入的参数 */
        osPriorityNormal+1,                 /* 优先级 */
        NULL);                              /* 任务句柄 */

    /* 复位AT模块 */
    at_exec_cmd(ptDev, (int8_t *)"AT+Z\r", NULL, 0, NULL, AT_TIMEOUT);
    vTaskDelay(2000);

    return 0;
}

初始化流程:创建锁和信号量 → 初始化 socket 结构体数组 → 创建后台解析线程 → 复位 AT 模块。


四、连接热点

w800_connect_ap 实现

int w800_connect_ap(char *ssid, char *passwd)
{
    int err;
    uint32_t resp_len;
    PAT_Device ptDev = get_netdev();
    int8_t buf[100];

    /* 设置工作模式为 STA */
    err = at_exec_cmd(ptDev, (int8_t *)"AT+WPRT=0\r", NULL, 0, NULL, AT_TIMEOUT);
    if (err) return err;

    /* 设置需要加入的 AP 名称 */
    sprintf((char *)buf, (const char *)"AT+SSID=\"%s\"\r", ssid);
    err = at_exec_cmd(ptDev, buf, NULL, 0, NULL, AT_TIMEOUT);
    if (err) return err;

    /* 设置需要加入的 AP 的无线密钥 */
    sprintf((char *)buf, (const char *)"AT+KEY=1,0,\"%s\"\r", passwd);
    err = at_exec_cmd(ptDev, (int8_t *)buf, NULL, 0, NULL, AT_TIMEOUT);
    if (err) return err;

    /* 启用 DHCP */
    err = at_exec_cmd(ptDev, (int8_t *)"AT+NIP=0\r", NULL, 0, NULL, AT_TIMEOUT);
    if (err) return err;

    /* 主动上报数据 */
    err = at_exec_cmd(ptDev, (int8_t *)"AT+SKRPTM=1\r", NULL, 0, NULL, AT_TIMEOUT);
    if (err) return err;

    /* 保存参数到 spi flash */
    err = at_exec_cmd(ptDev, (int8_t *)"AT+PMTF\r", NULL, 0, NULL, AT_TIMEOUT);
    if (err) return err;

    /* 加入无线网络 */
    err = at_exec_cmd(ptDev, (int8_t *)"AT+WJOIN\r", (uint8_t *)buf, sizeof(buf), &resp_len, AT_TIMEOUT*10);
    if (err) return err;

    /* 测试发现: 执行WJOIN后要等待1秒以上,下面LKSTT才能得到IP */
    vTaskDelay(2000);

    /* 查询本端网络连接状态 */
    err = at_exec_cmd(ptDev, (int8_t *)"AT+LKSTT\r", (uint8_t *)buf, sizeof(buf), &resp_len, AT_TIMEOUT);
    if (err) return err;

    /* 没连接成功 */
    if (!strncmp((const char *)buf, "+OK=0", 5))
        return -1;

    return 0;
}

五、AT 命令执行函数

at_exec_cmd 是核心函数,帮助 APP 发送 AT 命令,接收到网络数据。执行完发送命令后阻塞等待后台线程唤醒:

int at_exec_cmd(PAT_Device ptDev, int8_t *cmd, uint8_t *resp, uint32_t max_len, uint32_t *resp_len, uint32_t timeout)
{
    int err = 0;
    PUART_Device ptUARTDev = ptDev->ptUARTDev;

    /* 互斥操作: 获得Mutex */
    xSemaphoreTake(ptDev->at_lock, portMAX_DELAY);

    /* 复位resp状态 */
    at_reset_status(ptDev);

    /* 发送AT命令 */
    ptUARTDev->Send(ptUARTDev, (uint8_t *)cmd, strlen((const char *)cmd), timeout);

    /* 等待被唤醒 */
    if (pdTRUE != xSemaphoreTake(ptDev->at_resp_sem, timeout))
    {
        err = -1;  /* 超时 */
    }
    else
    {
        if (resp && resp_len)
        {
            *resp_len = ptDev->resp_len > max_len ? max_len : ptDev->resp_len;
            memcpy(resp, ptDev->resp, *resp_len);
        }
        if (ptDev->resp_status == AT_RESP_ERROR)
            err = -1;
    }

    /* 互斥操作: 释放Mutex */
    xSemaphoreGive(ptDev->at_lock);

    return err;
}

流程:获得互斥锁 → 发送 AT 命令 → 阻塞等待信号量 → 后台线程解析完成并唤醒 → 复制响应数据 → 释放互斥锁。


六、后台线程实现

后台线程负责读取串口数据并区分 AT 响应与网络数据:

static void w800_parser(void *params)
{
    PAT_Device ptDev = params;
    PUART_Device ptUARTDev = ptDev->ptUARTDev;
    int8_t resp[AT_RESP_BUF_SIZE];
    uint8_t c;
    int len = 0;

    while (1)
    {
        /* 读取AT模块的数据 */
        ptUARTDev->RecvByte(ptUARTDev, &c, portMAX_DELAY);

        if (len < AT_RESP_BUF_SIZE)
        {
            resp[len++] = c;
            resp[len] = '\0';
        }

        /* 判断是否接收到数据包, 格式为:
         * +SKTRPT=<socket>,<size>,<remote_ip>,<remote_port><CR><LF><CR><LF>
         * [data]
         */
        if (strstr((const char *)resp, "+SKTRPT="))
        {
            w800_recv_packet(ptDev);
            len = 0;
            continue;
        }

        /* AT命令的回应: 回应结束后有单独的"<CR><LF>" */
        if (c == '\n')
        {
            if (len == 2)
            {
                /* 得到新行数据"<CR><LF>"表示回应接收完毕 */
                xSemaphoreGive(ptDev->at_resp_sem);
            }
            else
            {
                /* 接收到一行回应之后,把它复制进AT设备的resp里 */
                if (ptDev->resp_len + len + 1 < AT_RESP_BUF_SIZE)
                {
                    memcpy(ptDev->resp + ptDev->resp_len, resp, len+1);
                }
                ptDev->resp_len += len + 1;
                ptDev->resp_line_counts++;
                if (ptDev->resp_line_counts == 1)
                {
                    if (strstr((const char *)resp, "+OK"))
                        ptDev->resp_status = AT_RESP_OK;
                    else
                        ptDev->resp_status = AT_RESP_ERROR;
                }
            }
            len = 0;
            continue;
        }

        if (len >= AT_RESP_BUF_SIZE)
            len = 0;
    }
}

关键点:AT 命令的回应可能有多行,每行结尾必有 <CR><LF>,回应结束后有单独的 <CR><LF>。也就是说当接收到空白的回车换行时,表明 AT 响应接收完毕,释放信号量唤醒发送者。


七、总结

组件 作用
AT_Device 管理 AT 模块,包含锁、信号量、socket 数组
AT_Socket 单个 socket,含本地/远端信息、队列、信号量
at_exec_cmd 发送 AT 命令并等待响应
后台线程 读取串口,区分 AT 响应与网络数据,唤醒对应等待者

设计要点

  • 互斥锁保证 AT 命令串行发送
  • 二进制信号量实现阻塞与唤醒
  • 队列缓存网络数据,实现接收与处理的解耦

八、结尾

本篇完成了基于 FreeRTOS 的 Socket 封装核心实现,下一篇将继续进行场景分析,学习 TCP 场景下代码的具体实现

Hello_Embed 继续带你从原理到实践,掌握嵌入式上位机开发的核心技能,敬请关注~

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐