嵌入式上位机开发入门(十二):Socket 封装核心步骤

张开发
2026/5/21 5:46:55 15 分钟阅读
嵌入式上位机开发入门(十二):Socket 封装核心步骤
目录一、前言二、结构体定义三、初始化流程四、连接热点五、AT 命令执行函数六、后台线程实现七、总结八、结尾一、前言大家好这里是Hello_Embed。本篇笔记分几个部分介绍实现 Socket 封装的关键步骤包括结构体定义、初始化流程、连接热点以及后台线程的具体实现。二、结构体定义AT_Device 结构体typedefstructAT_Device{char*name;/* WIFI模块的名字 */SemaphoreHandle_t at_lock;/* 发送AT命令前需要先获得这个锁 */SemaphoreHandle_t at_resp_sem;/* 发送AT命令后等待这个信号量(等待AT命令的回应) */uint8_tresp[AT_RESP_BUF_SIZE];/* 存放AT命令的回应数据 */uint32_tresp_len;/* AT命令回应数据的长度 */uint32_tresp_line_counts;/* AT命令回应的数据有多少行 */uint32_tresp_status;/* AT命令的回应是OK还是ERR */PUART_Device ptUARTDev;/* 使用这个串口设备访问WIFI模块 */AT_Socket sockets[AT_DEVICE_SOCKETS_NUM];/* socket结构体数组 */}AT_Device,*PAT_Device;支持多少个 socket 要看具体的硬件芯片手册。AT_Socket 结构体typedefstructAT_Socket{uint32_tused;/* 0-未被占用, 1-被占用 */inttype;/* TCP(SOCK_STREAM) or UDP(SOCK_DGRAM) */structsockaddrlocal;/* 用来记录本地IP/PORT */structsockaddrremote;/* 用来记录远端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 函数intat_init(char*uart_dev){returnw800_init(uart_dev);}w800_init 详细实现intw800_init(char*uart_dev){inti;PAT_Device ptDevget_netdev();/* 绑定网卡设备 *//* 创建AT设备用到的互斥锁、信号量 */ptDev-at_lockxSemaphoreCreateMutex();/* 互斥锁独占AT命令发送 */ptDev-at_resp_semxSemaphoreCreateBinary();/* 二进制信号量等待AT响应 *//* 根据名字获得UART设备 */ptDev-ptUARTDevGetUARTDevice(uart_dev);if(!ptDev-at_lock||!ptDev-at_resp_sem||!ptDev-ptUARTDev)return-1;/* 初始化socket结构体 */for(i0;iAT_DEVICE_SOCKETS_NUM;i){memset(ptDev-sockets[i],0,sizeof(AT_Socket));ptDev-sockets[i].recv_queuexQueueCreate(AT_RECV_BUF_SIZE,1);ptDev-sockets[i].at_packet_semxSemaphoreCreateBinary();if(!ptDev-sockets[i].recv_queue||!ptDev-sockets[i].at_packet_sem)return-1;}/* 创建后台线程(用来解析数据) */xTaskCreate(w800_parser,/* 函数指针, 任务函数 */w800_parser,/* 任务的名字 */AT_PARSER_TASK_STACK,/* 栈大小 */ptDev,/* 调用任务函数时传入的参数 */osPriorityNormal1,/* 优先级 */NULL);/* 任务句柄 *//* 复位AT模块 */at_exec_cmd(ptDev,(int8_t*)ATZ\r,NULL,0,NULL,AT_TIMEOUT);vTaskDelay(2000);return0;}初始化流程创建锁和信号量 → 初始化 socket 结构体数组 → 创建后台解析线程 → 复位 AT 模块。四、连接热点w800_connect_ap 实现intw800_connect_ap(char*ssid,char*passwd){interr;uint32_tresp_len;PAT_Device ptDevget_netdev();int8_tbuf[100];/* 设置工作模式为 STA */errat_exec_cmd(ptDev,(int8_t*)ATWPRT0\r,NULL,0,NULL,AT_TIMEOUT);if(err)returnerr;/* 设置需要加入的 AP 名称 */sprintf((char*)buf,(constchar*)ATSSID\%s\\r,ssid);errat_exec_cmd(ptDev,buf,NULL,0,NULL,AT_TIMEOUT);if(err)returnerr;/* 设置需要加入的 AP 的无线密钥 */sprintf((char*)buf,(constchar*)ATKEY1,0,\%s\\r,passwd);errat_exec_cmd(ptDev,(int8_t*)buf,NULL,0,NULL,AT_TIMEOUT);if(err)returnerr;/* 启用 DHCP */errat_exec_cmd(ptDev,(int8_t*)ATNIP0\r,NULL,0,NULL,AT_TIMEOUT);if(err)returnerr;/* 主动上报数据 */errat_exec_cmd(ptDev,(int8_t*)ATSKRPTM1\r,NULL,0,NULL,AT_TIMEOUT);if(err)returnerr;/* 保存参数到 spi flash */errat_exec_cmd(ptDev,(int8_t*)ATPMTF\r,NULL,0,NULL,AT_TIMEOUT);if(err)returnerr;/* 加入无线网络 */errat_exec_cmd(ptDev,(int8_t*)ATWJOIN\r,(uint8_t*)buf,sizeof(buf),resp_len,AT_TIMEOUT*10);if(err)returnerr;/* 测试发现: 执行WJOIN后要等待1秒以上,下面LKSTT才能得到IP */vTaskDelay(2000);/* 查询本端网络连接状态 */errat_exec_cmd(ptDev,(int8_t*)ATLKSTT\r,(uint8_t*)buf,sizeof(buf),resp_len,AT_TIMEOUT);if(err)returnerr;/* 没连接成功 */if(!strncmp((constchar*)buf,OK0,5))return-1;return0;}五、AT 命令执行函数at_exec_cmd是核心函数帮助 APP 发送 AT 命令接收到网络数据。执行完发送命令后阻塞等待后台线程唤醒intat_exec_cmd(PAT_Device ptDev,int8_t*cmd,uint8_t*resp,uint32_tmax_len,uint32_t*resp_len,uint32_ttimeout){interr0;PUART_Device ptUARTDevptDev-ptUARTDev;/* 互斥操作: 获得Mutex */xSemaphoreTake(ptDev-at_lock,portMAX_DELAY);/* 复位resp状态 */at_reset_status(ptDev);/* 发送AT命令 */ptUARTDev-Send(ptUARTDev,(uint8_t*)cmd,strlen((constchar*)cmd),timeout);/* 等待被唤醒 */if(pdTRUE!xSemaphoreTake(ptDev-at_resp_sem,timeout)){err-1;/* 超时 */}else{if(respresp_len){*resp_lenptDev-resp_lenmax_len?max_len:ptDev-resp_len;memcpy(resp,ptDev-resp,*resp_len);}if(ptDev-resp_statusAT_RESP_ERROR)err-1;}/* 互斥操作: 释放Mutex */xSemaphoreGive(ptDev-at_lock);returnerr;}流程获得互斥锁 → 发送 AT 命令 → 阻塞等待信号量 → 后台线程解析完成并唤醒 → 复制响应数据 → 释放互斥锁。六、后台线程实现后台线程负责读取串口数据并区分 AT 响应与网络数据staticvoidw800_parser(void*params){PAT_Device ptDevparams;PUART_Device ptUARTDevptDev-ptUARTDev;int8_tresp[AT_RESP_BUF_SIZE];uint8_tc;intlen0;while(1){/* 读取AT模块的数据 */ptUARTDev-RecvByte(ptUARTDev,c,portMAX_DELAY);if(lenAT_RESP_BUF_SIZE){resp[len]c;resp[len]\0;}/* 判断是否接收到数据包, 格式为: * SKTRPTsocket,size,remote_ip,remote_portCRLFCRLF * [data] */if(strstr((constchar*)resp,SKTRPT)){w800_recv_packet(ptDev);len0;continue;}/* AT命令的回应: 回应结束后有单独的CRLF */if(c\n){if(len2){/* 得到新行数据CRLF表示回应接收完毕 */xSemaphoreGive(ptDev-at_resp_sem);}else{/* 接收到一行回应之后,把它复制进AT设备的resp里 */if(ptDev-resp_lenlen1AT_RESP_BUF_SIZE){memcpy(ptDev-respptDev-resp_len,resp,len1);}ptDev-resp_lenlen1;ptDev-resp_line_counts;if(ptDev-resp_line_counts1){if(strstr((constchar*)resp,OK))ptDev-resp_statusAT_RESP_OK;elseptDev-resp_statusAT_RESP_ERROR;}}len0;continue;}if(lenAT_RESP_BUF_SIZE)len0;}}关键点AT 命令的回应可能有多行每行结尾必有CRLF回应结束后有单独的CRLF。也就是说当接收到空白的回车换行时表明 AT 响应接收完毕释放信号量唤醒发送者。七、总结组件作用AT_Device管理 AT 模块包含锁、信号量、socket 数组AT_Socket单个 socket含本地/远端信息、队列、信号量at_exec_cmd发送 AT 命令并等待响应后台线程读取串口区分 AT 响应与网络数据唤醒对应等待者设计要点互斥锁保证 AT 命令串行发送二进制信号量实现阻塞与唤醒队列缓存网络数据实现接收与处理的解耦八、结尾本篇完成了基于 FreeRTOS 的 Socket 封装核心实现下一篇将继续进行场景分析学习TCP 场景下代码的具体实现。Hello_Embed继续带你从原理到实践掌握嵌入式上位机开发的核心技能敬请关注

更多文章