目录
一、引言
二、硬件准备
三、软件准备
四、LWIP 协议栈的配置与初始化
五、创建 TCP 服务器
1.创建任务以及全局变量
2.创建 TCP 控制块
3.绑定端口
4. 进入监听状态
5.设置接收回调函数
六、处理多个客户端连接
七、总结
一、引言
在嵌入式系统开发中,常常需要实现设备之间的网络通信。STM32 作为一款广泛应用的微控制器,结合网络通信功能可以实现与多个设备的交互。本文将介绍如何在 STM32 上实现 TCP 服务器端,以便与多个设备进行通讯。
二、硬件准备
- STM32 开发板:选择一款带有以太网接口的 STM32 开发板,如 STM32F429 系列等。
- 以太网模块(可选):如果开发板没有内置以太网接口,可以选择一个外接的以太网模块,如 W5500 等。
- 网络连接:将开发板连接到同一局域网内,确保其他设备可以通过网络访问到 STM32 开发板。
三、软件准备
- 开发环境:如 Keil MDK、IAR Embedded Workbench 等。
- LWIP(Lightweight IP)协议栈:LWIP 是一个轻量级的 TCP/IP 协议栈,适用于嵌入式系统。可以从 LWIP 官方网站下载并集成到开发环境中。
- STM32 库:使用 STM32 的官方库或者其他第三方库来进行硬件驱动和开发。
四、LWIP 协议栈的配置与初始化
- 将 LWIP 协议栈的源文件添加到 STM32 项目中,并设置正确的编译选项。
- 在项目的初始化代码中,调用 LWIP 的初始化函数
lwip_init()
,完成协议栈的初始化。 - 根据实际需求,配置 LWIP 的参数,如 IP 地址、子网掩码、默认网关等。可以通过修改
lwipopts.h
文件来实现。
五、创建 TCP 服务器
1.创建任务以及全局变量
我用的是ucosIII的实时操作系统,用freertos也差不多,把调度的代码替换一下就行了,第五个大标题2 3 4 5的代码都是在 tcp_server_task 这个任务函数里面的,然后第六个标题是每个客户端的回调函数,整理在一起就是完整的代码。
#include "sys.h"
#include "lwip_comm.h"
#include "includes.h"
#include "lwip/api.h"
#include "lwip/err.h"
#include "lwip/tcp.h"
#include <stdio.h>
#define TCP_SERVER_PORT 8088 // 定义TCP服务器监听的端口号,可按需修改
#define MAX_CONNECTIONS 2 // 最大允许同时连接的客户端数量
#define RX_BUFFER_SIZE 1024 // 接收缓冲区大小
// 用于存储客户端连接的结构体数组
struct tcp_pcb *tcp_server_pcbs[MAX_CONNECTIONS];
// 用于保护对tcp_server_pcbs数组操作的互斥信号量
OS_SEM tcp_server_pcbs_sem;
// 处理客户端连接的函数
err_t client_connection_handler(void *arg, struct tcp_pcb *newpcb, err_t err);
// 处理客户端接收数据的函数
err_t client_recv_handler(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err);
// 处理客户端发送数据的函数(示例中简单回显数据给客户端)
err_t client_send_handler(void *arg, struct tcp_pcb *tpcb, u16_t len);
// 处理客户端断开连接的函数
void client_err_handler(void *arg, err_t err);
// TCP服务器任务函数
void tcp_server_task(void *p_arg);
//TCP客户端任务
#define TCP_PRIO 7
//任务堆栈大小
#define TCP_STK_SIZE 300
//任务控制块
OS_TCB TcpTaskTCB;
//任务堆栈
CPU_STK TCP_TASK_STK[TCP_STK_SIZE];
//创建TCP线程
//返回值:0 TCP创建成功
// 其他 TCP创建失败
u8 tcp_demo_init(void)
{
OS_ERR err;
CPU_SR_ALLOC();
OS_CRITICAL_ENTER();//进入临界区
//创建TCP任务
OSTaskCreate((OS_TCB * )&TcpTaskTCB,
(CPU_CHAR * )"tcp task",
(OS_TASK_PTR )tcp_server_task,
(void * )0,
(OS_PRIO )TCP_PRIO,
(CPU_STK * )&TCP_TASK_STK[0],
(CPU_STK_SIZE)TCP_STK_SIZE/10,
(CPU_STK_SIZE)TCP_STK_SIZE,
(OS_MSG_QTY )0,
(OS_TICK )0,
(void * )0,
(OS_OPT )OS_OPT_TASK_STK_CHK|OS_OPT_TASK_STK_CLR,
(OS_ERR * )&err);
OS_CRITICAL_EXIT(); //退出临界区
return err;
}
2.创建 TCP 控制块
使用 LWIP 的 API 函数tcp_new()
创建一个新的 TCP 控制块。
struct tcp_pcb *server_pcb;
err_t err;
OS_ERR os_err;
// 创建一个TCP协议控制块(PCB)用于服务器端
server_pcb = tcp_new();
if (server_pcb == NULL)
{
printf("Error creating TCP PCB\n");
return;
}
3.绑定端口
使用tcp_bind()
函数将 TCP 控制块绑定到一个特定的端口号。通常选择一个未被其他应用程序占用的端口。
// 绑定服务器的IP地址和端口号
err = tcp_bind(server_pcb, IP_ADDR_ANY, TCP_SERVER_PORT);
if (err!= ERR_OK)
{
printf("Error binding TCP socket: %d\n", err);
tcp_close(server_pcb);
return;
}
这里的SERVER_PORT
是服务器监听的端口号。
4. 进入监听状态
使用tcp_listen()
函数使 TCP 控制块进入监听状态,等待客户端的连接请求。
// 将服务器PCB设置为监听状态,等待客户端连接
server_pcb = tcp_listen(server_pcb);
if (server_pcb == NULL)
{
printf("Error listening for connections\n");
tcp_close(server_pcb);
return;
}
5.设置接收回调函数
使用tcp_accept()
函数设置一个接收回调函数,当有客户端连接请求时,该回调函数将被调用。
// 设置接受客户端连接的回调函数
tcp_accept(server_pcb, client_connection_handler);
while (1)
{
// 任务可以在这里进行适当的阻塞等待,避免过度占用CPU资源
OSTimeDlyHMSM(0,0,0,100,OS_OPT_TIME_HMSM_STRICT,&os_err); //延时100ms
}
client_connection_handler
是自定义的回调函数,用于处理客户端的连接请求。
六、处理多个客户端连接
1.在接收回调函数中,接受客户端的连接请求,并为每个连接创建一个新的 TCP 控制块来处理与该客户端的通信,由于单片机资源有限和需求,我设置的最多被两个客户端连接,再来了新的客户端连接会把之前最旧的关闭,这也是为了网线拔掉或者不主动关闭的无效连接占用资源。
err_t client_connection_handler(void *arg, struct tcp_pcb *newpcb, err_t err)
{
OS_ERR os_err;
int client_index;
if (err!= ERR_OK)
{
return err;
}
// 获取互斥信号量,保护对tcp_server_pcbs数组的访问
OSSemPend(&tcp_server_pcbs_sem,0,OS_OPT_PEND_BLOCKING,0,&os_err); //请求信号量
if(tcp_server_pcbs[close_tcp]!=NULL){
tcp_close(tcp_server_pcbs[close_tcp]);
}
tcp_server_pcbs[close_tcp]=newpcb;
if(close_tcp==0){
close_tcp=1;
}else{
close_tcp=0;
}
// 设置接收、发送和错误处理的回调函数
tcp_recv(newpcb, client_recv_handler);
tcp_sent(newpcb, client_send_handler);
tcp_err(newpcb, client_err_handler);
OSSemPost(&tcp_server_pcbs_sem, OS_OPT_POST_1, &os_err);
return ERR_OK;
}
2.为每个连接设置接收回调函数,以便在有数据到达时进行处理,如果tcp客户端主动断开连接会走 else if (err == ERR_OK && p == NULL) 这里。
err_t client_recv_handler(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err)
{
OS_ERR os_err;
if (err == ERR_OK && p!= NULL)
{
// 将接收到的pbuf数据复制到本地缓冲区(这里简单示例,可优化)
char rx_buffer[RX_BUFFER_SIZE];
int copied_bytes = 0;
struct pbuf *q = p;
while (q!= NULL)
{
int bytes_to_copy = (q->len < (RX_BUFFER_SIZE - copied_bytes))? q->len : (RX_BUFFER_SIZE - copied_bytes);
memcpy(&rx_buffer[copied_bytes], q->payload, bytes_to_copy);
copied_bytes += bytes_to_copy;
q = q->next;
}
// 这里可以对接收到的数据进行处理,比如根据协议解析等,现在简单回显数据给客户端
tcp_write(tpcb, rx_buffer, copied_bytes, TCP_WRITE_FLAG_COPY);
tcp_output(tpcb);
// 释放接收到的pbuf内存
pbuf_free(p);
}
else if (err == ERR_OK && p == NULL)
{
OSSemPend(&tcp_server_pcbs_sem,0,OS_OPT_PEND_BLOCKING,0,&os_err); //请求信号量
// 客户端关闭了连接,正常处理
for (int i = 0; i < MAX_CONNECTIONS; i++)
{
if (tcp_server_pcbs[i] == tpcb)
{
tcp_server_pcbs[i] = NULL;
break;
}
}
OSSemPost(&tcp_server_pcbs_sem, OS_OPT_POST_1, &os_err);
tcp_close(tpcb);
}
else
{
// 出现错误情况,关闭连接
tcp_close(tpcb);
}
return ERR_OK;
}
3. 发送函数,这个我没用到,我只是被动返回消息。
err_t client_send_handler(void *arg, struct tcp_pcb *tpcb, u16_t len)
{
// 这里可以根据实际发送情况做一些后续处理,当前示例只是简单回显,无需额外操作
return ERR_OK;
}
4.错误处理,如果有超时的那种tcp连接,应该是会走这里释放TCP连接。
void client_err_handler(void *arg, err_t err)
{
struct tcp_pcb *tpcb = (struct tcp_pcb *)arg;
OS_ERR os_err;
// 获取互斥信号量,保护对tcp_server_pcbs数组的访问
OSSemPend(&tcp_server_pcbs_sem,0,OS_OPT_PEND_BLOCKING,0,&os_err); //请求信号量
for (int i = 0; i < MAX_CONNECTIONS; i++)
{
if (tcp_server_pcbs[i] == tpcb)
{
tcp_server_pcbs[i] = NULL;
break;
}
}
OSSemPost(&tcp_server_pcbs_sem, OS_OPT_POST_1, &os_err);
tcp_close(tpcb);
}
七、总结
通过以上步骤,我们可以在 STM32 上实现一个 TCP 服务器,与多个设备进行通信。在实际应用中,可以根据具体的需求进行进一步的优化和扩展,例如添加安全认证、数据加密、流量控制等功能。同时,还需要注意网络稳定性和可靠性,以确保通信的正常进行。
希望本文对大家在 STM32 上实现 TCP 服务器与多个设备通信有所帮助。如果有任何问题或建议,欢迎在评论区留言交流。