[APM32F4]mbedos TCP 客户端程序设计

    最近了解了ETH的TCP相关知识,准备在mbedos上开发一个TCP Client Demo,这篇文章从TCP的定义以及后面具体的代码实现,记录了我在开发时遇到的一些问题以及经验。

##1、TCP应用基础知识介绍
    TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。它在网络通信中扮演着至关重要的角色,常用于实现可靠的数据传输。

    1. **面向连接**:TCP是一种面向连接的协议,通信双方在传输数据之前必须先建立连接。连接的建立是通过三次握手来实现的,确保发送方和接收方都能够互相通信。

    2. **可靠性**:TCP提供可靠的数据传输服务,通过序列号、确认应答和重传机制等手段来确保数据的可靠性。如果数据包丢失、损坏或者乱序,TCP会进行重传,直到接收方正确接收到数据。

    3. **流量控制**:TCP使用滑动窗口机制进行流量控制,以防止发送方发送过多的数据导致接收方无法及时处理。接收方通过发送窗口大小来告知发送方可以接收的数据量,发送方根据窗口大小调整发送速率。

    4. **拥塞控制**:TCP具有拥塞控制机制,用于避免网络拥塞的发生。通过动态调整发送速率、检测丢包并进行重传、以及快速恢复等技术,TCP可以在网络拥塞时减少数据丢失和延迟,保持网络的稳定性。

    5. **面向字节流**:TCP是一种面向字节流的协议,数据在连接上以字节流的形式进行传输。TCP并不关心应用层的消息边界,而是将数据划分为字节流进行传输,因此应用层需要自行处理消息的边界。

    6. **全双工通信**:TCP连接是全双工的,即双方都可以同时发送和接收数据,实现了双向的通信。

    总之,TCP是一种可靠的、面向连接的通信协议,适用于构建各种类型的网络应用程序,如Web浏览器、电子邮件、文件传输等。

##2、mbedos TCP介绍

    mbedOS 是一款针对物联网设备的嵌入式操作系统,它提供了许多功能丰富的组件和库,其中包括 TCP/IP 协议栈,用于构建连接到网络的设备。在 mbedOS 中,TCP/IP 协议栈提供了 TCP 和 IP 层的实现,使得设备可以通过 TCP 协议进行可靠的数据通信。

    在 mbedOS 中使用 TCP 协议进行网络通信时,可以利用其提供的套接字 API,通过简单的函数调用来实现建立连接、发送和接收数据等操作。以下是在 mbedOS 中使用 TCP 协议的基本步骤:

    1. **创建套接字**:使用 `socket` 函数创建一个 TCP 套接字,该函数返回一个套接字描述符,用于后续的操作。

    2. **绑定套接字**:如果需要,可以使用 `bind` 函数将套接字绑定到特定的 IP 地址和端口上。

    3. **建立连接**:对于作为客户端的设备,使用 `connect` 函数连接到远程服务器;对于作为服务器的设备,使用 `listen` 函数监听连接请求,并使用 `accept` 函数接受连接。

    4. **发送和接收数据**:使用 `send` 和 `recv` 函数来发送和接收数据。发送数据时,将数据缓冲区和数据长度传递给 `send` 函数;接收数据时,指定接收缓冲区和最大接收长度给 `recv` 函数。

    5. **关闭连接**:通信完成后,使用 `close` 函数关闭套接字,释放资源。

    在使用 TCP 协议进行网络通信时,还需要注意处理各种可能出现的错误情况,如连接超时、数据发送失败等,以确保通信的稳定性和可靠性。同时,也可以利用 mbedOS 提供的其他功能,如事件驱动的网络堆栈、定时器等,来优化和增强 TCP 通信的性能和可靠性。

    开发使用相关API说明:https://os.mbed.com/docs/mbed-os/v6.16/apis/socket.html

##3、mbedos TCP客户端程序设计
    TCP 客户端程序的特点是随时可以根据应用的需要发起或结束连接,服务端实现无需知道客户端的存在,而一旦与服务端建立连接后,双方可以任意收发数据,所以非常适合那些需要远程监控的场合。

1、准备
    ① mbed studio开发环境
    ② 一根usb数据线
    ③ 一根网口线
    ④ APM32F407IG-Tiny板
    ⑤ 网络调试助手

2、mbed studio的开发环境的搭建,这里就不介绍了,可以参考论坛其他大佬关于这部分的内容,我们直接上源码。
    我在写这部分代码的时候,经过了多次修改,也是发现了一些问题,并总结出了一些经验。

第一次代码编写:

复制
#include "mbed.h"

#include "EthernetInterface.h"



//声明以太网接口

EthernetInterface eth;

//声明开发板ip地址

SocketAddress ip_address;

//声明开发板子网掩码

SocketAddress netmask_address;

//声明开发板网关

SocketAddress gateway_address;



//声明TCP连接

TCPSocket client;



//声明TCP服务器ip地址及端口

SocketAddress server_address;



DigitalOut led2(LED2);

InterruptIn button(KEY1);



void button_handler()

{

    char buf[100] = "Button was pressed!";

    client.send(buf, sizeof buf);

}



int main()

{

    //设置开发板ip信息

    ip_address.set_ip_address("192.168.10.2");

    netmask_address.set_ip_address("255.255.255.0");

    gateway_address.set_ip_address("192.168.74.254");



    //绑定到ETH接口

    eth.set_network(ip_address, netmask_address, gateway_address);



    //若eth成功获取到静态ip,打印ip信息

    if(eth.connect() < 0)

    {

        printf("setting static ip error!\r\n");

    }

    else

    {

        SocketAddress ip;

        eth.get_ip_address(&ip);

        printf("ip address: %s\r\n",ip.get_ip_address());

    }



    //绑定ip地址到tcp连接

    client.open(ð);



    //设置tcl服务器ip信息

    server_address.set_ip_address("192.168.10.100");

    server_address.set_port(8080);



    //开发板连接TCP服务器,并打印连接的信息

    if (client.connect(server_address) < 0)

    {

        printf("Connect server failed!\r\n");

    }

    else

    {

        printf("Connect server successed!\r\n");

    }



    button.fall(&button_handler);



    while(true)

    {

        led2 = !led2;

        ThisThread::sleep_for(1s);

    }



    return 0;

}

代码实现功能点:

    这段代码实现了一个基于mbedOS的嵌入式系统的简单TCP客户端程序。

    1. 引入必要的头文件和库:代码中使用了 `#include "mbed.h"` 和 `#include "EthernetInterface.h"` 来引入mbedOS和EthernetInterface的相关库。

    2. 定义变量
       - `EthernetInterface eth;`:声明了一个以太网接口对象 `eth`,用于管理以太网连接。
       - `SocketAddress ip_address;`、`SocketAddress netmask_address;`、`SocketAddress gateway_address;`:声明了三个套接字地址对象,分别用于存储开发板的IP地址、子网掩码和网关地址。
       - `TCPSocket client;`:声明了一个TCP套接字对象 `client`,用于与服务器建立连接并进行通信。
       - `SocketAddress server_address;`:声明了一个套接字地址对象 `server_address`,用于存储服务器的IP地址和端口号。
       - `DigitalOut led2(LED2);`、`InterruptIn button(KEY1);`:声明了一个LED控制对象 `led2` 和一个按键中断对象 `button`,用于控制LED和响应按键事件。

    3. 定义按键中断处理函数:`void button_handler()`,当按键被按下时,向服务器发送一个包含文本 "Button was pressed!" 的数据包。

    4. 主函数 `int main()`
       - 设置开发板的IP地址、子网掩码和网关地址,并绑定到以太网接口。
       - 如果以太网接口成功连接到网络,则打印开发板的IP地址。
       - 打开TCP连接,并设置服务器的IP地址和端口号。
       - 尝试连接到服务器,如果连接失败则打印连接失败信息,否则打印连接成功信息。
       - 注册按键中断处理函数。
       - 在主循环中,通过轮询方式交替改变LED的状态,并使用 `ThisThread::sleep_for()` 函数使程序休眠1秒钟。

    总体而言,这段代码的功能是初始化以太网接口,连接到网络,然后建立一个TCP连接到指定的服务器,并在按键被按下时向服务器发送数据,同时在循环中交替改变LED的状态。

    下载代码到开发板,用以太网线连接开发板和PC端,打开控制面板,设置以太网接口的静态的ip地址为TCP Server的IP(192.168.10.100)。

 



    打开网络调试助手,进行如下的配置,这样我们的网络调试助手就成功配置成一个TCP服务器了,我们就可以进行后续的测试工作了。

 



    点击网络调试助手连接,复位开发板,按下KEY1后,发现IDE竟然报错了!!!

 



发现的问题点:
    最终定位在中断服务函数中,原来是中断服务函数执行在执行client.send()函数时凉了。

问题原因:
    在嵌入式系统中,中断服务函数(ISR)应该尽量保持简短和高效,因为它们会在中断发生时立即执行,而且在执行期间,其他中断可能会被屏蔽,因此长时间运行的操作可能会引起问题。

    在这个特定的情况下,`client.send()` 函数是一个涉及网络通信的操作,可能需要一段时间来完成,尤其是在网络速度较慢或者网络状况不佳的情况下。如果在中断服务函数中调用`client.send()`函数,那么当按钮按下时,就会触发网络通信操作,这可能会导致以下问题:

    1. **延迟和响应性问题:** 在网络通信过程中,中断服务函数会被阻塞,导致延迟。如果延迟太长,用户可能会感觉到按钮响应速度慢或者不一致。

    2. **资源竞争和冲突:** 在中断服务函数中进行网络通信操作可能会导致资源竞争和冲突,因为网络通信可能需要访问共享资源,比如缓冲区或者网络接口。如果其他部分的代码也在访问这些资源,可能会导致不可预测的行为。

    3. **栈溢出:** 中断服务函数通常在一个单独的栈上执行,如果在中断服务函数中进行了过多的操作或者递归调用,可能会导致栈溢出问题,从而使系统崩溃。

    原来是中断服务函数中最好只执行必要且尽可能快速的操作,为了解决这个问题,我在中断服务函数中设置了一个标志位,然后给发送的代码创建了一个线程,在线程中执行client.send()函数。


代码调整:

复制
#include "mbed.h"

#include "EthernetInterface.h"

#include <cstdint>



//声明以太网接口

EthernetInterface eth;

//声明开发板ip地址

SocketAddress ip_address;

//声明开发板子网掩码

SocketAddress netmask_address;

//声明开发板网关

SocketAddress gateway_address;



//声明TCP连接

TCPSocket client;



//声明TCP服务器ip地址及端口

SocketAddress server_address;



DigitalOut led2(LED2);

DigitalOut led3(LED3);

InterruptIn button(KEY1);



uint32_t press_index = 0;

Queue<uint32_t,255> queue;

Thread thread;



void button_handler()

{

    // char buf[100] = "Button was pressed!";

    // client.send(buf, sizeof buf);

    press_index++;

    queue.put(&press_index);

}



void send_thread()

{

    while(true)

    {

        osEvent evt = queue.get();

        if(evt.status == osEventMessage)

        {

            char buf[100] = "Button is pressed!\r\n";

            led3 = !led3;

            client.send(buf, sizeof buf);

        }

    }

}



int main()

{

    //设置开发板ip信息

    ip_address.set_ip_address("192.168.10.2");

    netmask_address.set_ip_address("255.255.255.0");

    gateway_address.set_ip_address("192.168.74.254");



    //绑定到ETH接口

    eth.set_network(ip_address, netmask_address, gateway_address);



    //若eth成功获取到静态ip,打印ip信息

    if(eth.connect() < 0)

    {

        printf("setting static ip error!\r\n");

    }

    else

    {

        SocketAddress ip;

        eth.get_ip_address(&ip);

        printf("ip address: %s\r\n",ip.get_ip_address());

    }



    //绑定ip地址到tcp连接

    client.open(ð);



    //设置tcl服务器ip信息

    server_address.set_ip_address("192.168.10.100");

    server_address.set_port(8080);



    //开发板连接TCP服务器,并打印连接的信息

    if (client.connect(server_address) < 0)

    {

        printf("Connect server failed!\r\n");

    }

    else

    {

        printf("Connect server successed!\r\n");

    }



    button.fall(&button_handler);

    thread.start(&send_thread);



    while(true)

    {

        led2 = !led2;

        ThisThread::sleep_for(1s);

    }



    return 0;

}




在上一次代码的基础上,主要做了以下修改:

    1. 引入了 `Queue` 和 `Thread` 类,用于在中断服务函数中将按键事件发送到一个线程进行处理。

    2. 修改了 `button_handler()` 函数,将按键事件发送的次数存储到 `press_index` 变量中,并将其放入队列中。

    3. 定义了一个新的线程 `send_thread()`,该线程不断从队列中获取按键事件,并向服务器发送数据。

    4. 在主函数中启动了 `send_thread()` 线程。


    这些修改的目的是将网络通信操作从中断服务函数中移出,避免在中断服务函数中进行长时间的操作,以提高系统的响应性和稳定性。通过将按键事件发送到一个线程中处理,可以保证网络通信操作在主线程中进行,从而避免了在中断服务函数中进行网络通信操作可能带来的问题。

    之后我再次下载代码,复位开发板,按下KEY1,发现TCP服务器端正常响应了客户端发送的信息。
 




    当然,以上代码又存在一个问题,就是数据的单向发送问题,如何才能让客户端也能收到服务端返回的数据呢,答案就是让客户端代码不断地监测是否有数据到达。为了实现这个功能,我在main函数中添加了这部分的数据解析。
 

复制
int main()

{

    //设置开发板ip信息

    ip_address.set_ip_address("192.168.10.2");

    netmask_address.set_ip_address("255.255.255.0");

    gateway_address.set_ip_address("192.168.74.254");



    //绑定到ETH接口

    eth.set_network(ip_address, netmask_address, gateway_address);



    //若eth成功获取到静态ip,打印ip信息

    if(eth.connect() < 0)

    {

        printf("setting static ip error!\r\n");

    }

    else

    {

        SocketAddress ip;

        eth.get_ip_address(&ip);

        printf("ip address: %s\r\n",ip.get_ip_address());

    }



    //绑定ip地址到tcp连接

    client.open(ð);



    //设置tcl服务器ip信息

    server_address.set_ip_address("192.168.10.100");

    server_address.set_port(8080);



    //开发板连接TCP服务器,并打印连接的信息

    if (client.connect(server_address) < 0)

    {

        printf("Connect server failed!\r\n");

    }

    else

    {

        printf("Connect server successed!\r\n");

    }



    button.fall(&button_handler);

    thread.start(&send_thread);



    while(true)

    {

        // led2 = !led2;

        // ThisThread::sleep_for(1s);

        char buf[1024];

        for (int i = 0; i < 1024; i++)

        {

            buf[i] = '\0';

        }

        int count = client.recv(buf, sizeof(buf));

        if (count > 0)

        {

            led2 = !led2;

            printf("%s",buf);

        }

        ThisThread::yield();

    }

    client.close();



    return 0;

}



在这个修改后的 `main()` 函数中,主要做了以下几个修改:

    1. 注释掉了之前的 LED 控制代码:`led2 = !led2;` 和 `ThisThread::sleep_for(1s);`。

    2. 添加了一个循环用于接收来自服务器的数据。在循环中使用 `client.recv()` 函数接收数据,并将其存储在 `buf` 缓冲区中。接收到的数据长度存储在 `count` 变量中。

    3. 如果接收到的数据长度大于 0,则切换 LED2 状态并打印接收到的数据。这个操作是为了在接收到数据时提供一种视觉指示。

    4. 在循环的末尾调用了 `ThisThread::yield()` 函数。这个函数的作用是让出当前线程的执行权,以便其他线程有机会执行。这样做可以确保主线程不会独占 CPU 资源,以便其他任务(比如网络通信)能够及时处理。

    5. 在循环结束后调用了 `client.close()` 函数,关闭了与服务器的连接。这个操作是为了确保在程序退出之前关闭网络连接,以释放资源并避免网络连接泄漏。


经过多次修改后,最终代码如下:

复制
#include "mbed.h"

#include "EthernetInterface.h"

#include <cstdint>



//声明以太网接口

EthernetInterface eth;

//声明开发板ip地址

SocketAddress ip_address;

//声明开发板子网掩码

SocketAddress netmask_address;

//声明开发板网关

SocketAddress gateway_address;



//声明TCP连接

TCPSocket client;



//声明TCP服务器ip地址及端口

SocketAddress server_address;



DigitalOut led2(LED2);

DigitalOut led3(LED3);

InterruptIn button(KEY1);



uint32_t press_index = 0;

Queue<uint32_t,255> queue;

Thread thread;



void button_handler()

{

    // char buf[100] = "Button was pressed!";

    // client.send(buf, sizeof buf);

    press_index++;

    queue.put(&press_index);

}



void send_thread()

{

    while(true)

    {

        osEvent evt = queue.get();

        if(evt.status == osEventMessage)

        {

            char buf[100] = "Button is pressed!\r\n";

            led3 = !led3;

            client.send(buf, sizeof buf);

        }

    }

}



int main()

{

    //设置开发板ip信息

    ip_address.set_ip_address("192.168.10.2");

    netmask_address.set_ip_address("255.255.255.0");

    gateway_address.set_ip_address("192.168.74.254");



    //绑定到ETH接口

    eth.set_network(ip_address, netmask_address, gateway_address);



    //若eth成功获取到静态ip,打印ip信息

    if(eth.connect() < 0)

    {

        printf("setting static ip error!\r\n");

    }

    else

    {

        SocketAddress ip;

        eth.get_ip_address(&ip);

        printf("ip address: %s\r\n",ip.get_ip_address());

    }



    //绑定ip地址到tcp连接

    client.open(ð);



    //设置tcl服务器ip信息

    server_address.set_ip_address("192.168.10.100");

    server_address.set_port(8080);



    //开发板连接TCP服务器,并打印连接的信息

    if (client.connect(server_address) < 0)

    {

        printf("Connect server failed!\r\n");

    }

    else

    {

        printf("Connect server successed!\r\n");

    }



    button.fall(&button_handler);

    thread.start(&send_thread);



    while(true)

    {

        // led2 = !led2;

        // ThisThread::sleep_for(1s);

        char buf[1024];

        for (int i = 0; i < 1024; i++)

        {

            buf[i] = '\0';

        }

        int count = client.recv(buf, sizeof(buf));

        if (count > 0)

        {

            led2 = !led2;

            printf("%s",buf);

        }

        ThisThread::yield();

    }

    client.close();



    return 0;

}


    下载代码到开发板,再次复位,在TCP 服务端发送消息,可以看到客户端也正常接收到了服务端的信息了。

 



##4、问题总结

    在编写中断服务函数时,应避免使用或调用一些阻塞或超长延时函数,例如我们如果在中断服务函数中使用printf()函数,就可能出现下列的一系列情况。

    1. **资源竞争:** printf函数通常需要使用系统的I/O资源,如串口或文件,但在中断服务函数中,这些资源可能已经被其他部分的代码占用,如果在中断服务函数中调用printf,可能会造成资源竞争,甚至死锁。

    2. **执行时间不确定性:** printf函数可能会花费较长的时间来执行,因为它涉及到格式化字符串、系统调用等操作。在中断服务函数中执行时间不确定会导致中断响应时间过长,影响系统的实时性能。

    3. **堆栈溢出风险:** printf函数通常会使用堆栈来存储临时变量和函数调用信息,而在中断服务函数中,堆栈空间可能比较有限,如果printf函数在中断服务函数中使用过多的堆栈空间,可能会导致堆栈溢出。

    因此,为了保证中断服务函数的可靠性、实时性和效率,一般建议在中断服务函数中尽量避免使用这类函数,可以使用轻量级的日志记录方式或者设置标志位,在主程序中再进行相应的输出或日志记录。


    以上就是本次使用Mbed studio开发TCP Client的全部内容,附件TCP Client 的Main.cpp 文件,如有问题,欢迎讨论。

附件:

 

 main源码.zip (1.15 KB)

 

 网络调试助手.zip (483.81 KB)。
---------------------
作者:DKENNY
链接:https://bbs.21ic.com/icview-3374698-1-1.html
来源:21ic.com
此文章已获得原创/原创奖标签,著作权归21ic所有,任何人未经允许禁止转载。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: APM32F030是一款32位微控制器芯片,程序跳转是指在程序执行过程中无条件或有条件地改变执行地址,使程序跳转到其他指定的地址处继续执行。 在APM32F030中,程序跳转可以通过修改程序计数器(Program Counter,简称PC)来实现。PC是一个特殊的寄存器,用于指向下一条将要执行的指令地址。通过改变PC的值,就可以改变程序的执行流程。 在APM32F030中,程序跳转使用跳转指令来实现。通过跳转指令,可以无条件地将程序跳转到指定的地址,或根据特定条件来判断是否跳转。 比如,可以使用无条件跳转指令“JMP”来实现无条件跳转。通过设置跳转指令的操作数为目标地址,执行该指令时就会将PC的值设置为目标地址,使程序跳转到目标地址处继续执行。 另外,还可以使用有条件跳转指令,如“JE”(等于跳转)、“JNE”(不等于跳转)、“JZ”(零跳转)等,根据特定条件来判断是否跳转。例如,可以通过判断某个标志位的状态,来确定是否执行跳转指令。 总之,在APM32F030中,程序跳转是通过修改PC的值来实现的。通过使用跳转指令,可以无条件或有条件地改变PC的值,从而实现程序的跳转,改变程序的执行流程。这样,可以根据实际需要,实现复杂的程序逻辑和功能。 ### 回答2: APM32F030是一款微控制器芯片,程序跳转是指在程序执行过程中,从当前位置跳转到其他位置继续执行程序的操作。在APM32F030中,程序跳转可以通过使用函数指针或者汇编语言来实现。 首先,函数指针是一种指向函数的指针变量,通过函数指针,我们可以将某个函数的地址赋值给函数指针,然后可以通过调用函数指针来执行这个函数。如果想要在程序中实现跳转到其他位置执行的功能,可以定义一个函数指针变量,并将需要跳转的位置的函数地址赋值给函数指针,然后通过调用函数指针来执行跳转到该位置的程序代码。 其次,汇编语言是一种低级的编程语言,可以直接访问计算机的硬件资源。在汇编语言中,通过使用跳转指令可以实现程序的跳转。APM32F030支持汇编语言编程,通过使用汇编语言编写一段程序代码,可以实现程序的跳转操作。 总结起来,APM32F030的程序跳转可以通过使用函数指针或者汇编语言来实现。函数指针适用于高级语言编程,而汇编语言适用于底层编程,根据具体的应用场景和开发需求,选择合适的方式来实现程序跳转。 ### 回答3: APM32F030是一款基于ARM Cortex-M0内核的微控制器,其具有支持程序跳转的功能。程序跳转是指在程序的执行过程中,通过特定的指令或条件,将控制权从当前位置转移到另一个指定的位置。 在APM32F030中,程序跳转可以通过使用汇编语言或C语言中的跳转指令来实现。例如,使用汇编语言的跳转指令可以是B(无条件跳转)、BL(带链接的跳转)、BEQ(等于零时跳转)等。这些指令可以在程序中的特定位置使用,以实现在需要的时候改变程序的控制流。 具体来说,要进行程序跳转的操作,可以按照以下步骤进行: 1. 首先,确定需要跳转的目标位置。这可以是代码的另一个函数、循环体、条件语句等。 2. 在需要执行跳转的位置,通过使用适当的跳转指令将控制权转移到目标位置。这可以是使用汇编语言编写的指令,也可以是通过C语言中的特殊语法来实现。 3. 跳转到目标位置后,程序会从该位置继续执行。这样可以实现在运行时改变程序流程,实现不同的功能。 需要注意的是,在进行程序跳转时,需要确保目标位置的代码是可执行的,并且满足程序的逻辑要求。否则,可能会导致程序运行错误或异常。 总的来说,APM32F030支持灵活的程序跳转功能,通过适当的跳转指令,可以实现在程序的不同位置之间切换控制权,以满足不同的需求。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值