嵌入式上位机开发入门(十二):Socket 封装核心步骤
本文介绍了基于FreeRTOS的Socket封装实现方案,主要包括以下内容: 结构体定义: AT_Device结构体封装了WIFI模块的基本属性和操作接口 AT_Socket结构体管理socket连接状态和数据传输 初始化流程: 创建必要的互斥锁和信号量 初始化socket结构体数组 创建后台数据解析线程 复位AT模块 热点连接: 设置STA工作模式 配置AP名称和密码 启用DHCP 保存参数并加
目录
一、前言
大家好,这里是 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 继续带你从原理到实践,掌握嵌入式上位机开发的核心技能,敬请关注~
更多推荐
所有评论(0)