stm32f103 w5500 tcp server

mult TCP clients supported TCP server
ORIGINAL POST
By donglicaiju76152
details

QQ截图20211005151836.png

背景

項目中需要使用網絡,開始使用的stm32f4+lwip的方案,但是硬件成本有些高,更主要的是lwip不好用,老是斷,可能是自己沒有研究透它吧。經過長時間的調研論證,最終選擇了w5500這款芯片。它把TCP/IP網絡協議棧固化在了硬件芯片中,爲用戶留出應用層接口,簡單穩定。

移植過程

首先,從https://w5500.com/上下載芯片手冊和參考代碼瞭解芯片的原理及基本用法,內容不是很複雜。項目中需要W5500作爲TCP Server,會有三個客戶端連接。參考相關代碼,移植到項目中,具體代碼如下。

配置

static void net_set_config(void)
{
    unsigned char mac[6] = {0x00, 0x08, 0xdc, 0x11, 0x11, 0x11};
    unsigned char ip[4]  = {192, 168, 1, 100};
    unsigned char sub[4] = {255, 255, 255, 0};
    unsigned char gw[4]  = {192, 168, 1, 1};
    unsigned char tx_size[8] = {2,2,2,2,2,2,2,2};
    unsigned char rx_size[8] = {2,2,2,2,2,2,2,2};

    /* 設置Mac地址 */
    set_mac_addr(mac);
    /* 設置IP */
    set_source_ip_addr(ip);
    /* 設置子網掩碼 */
    set_subnet_mask(sub);
    /* 設置網關 */
    set_gateway(gw);

    /* 初始化8個socket */
    sys_init(tx_size, rx_size);
    /* 設置超時時間 */
    set_retrans_time(2000);
    /* 設置最大重新發送次數 */
    set_retrans_num(3);
	
    /* 打開心跳功能 */
    set_keepalive(SOCKET_ID_0);
	set_keepalive(SOCKET_ID_1);
	set_keepalive(SOCKET_ID_2);	
}

狀態機

int net_process_socket(SOCKET sock, unsigned short port)
{
    int ret = -1;
    unsigned char state = SOCK_CLOSED;
    unsigned short len = 0;
    unsigned char data[MAX_MSG_LEN];

    state = get_sock_status(sock);
    switch (state) {
        case SOCK_INIT:
            listen(sock);
            break;
        case SOCK_ESTABLISHED:
            if (get_sock_interrupt_status(sock) & Sn_IR_CON) {
                set_sock_interrupt_status(sock, Sn_IR_CON);
            }
            len = get_sock_rx_free_buff_size(sock);
            if (len > 0) {
                recv(sock, data, MAX_MSG_LEN);
				/* process the recv data */
            }
            break;
        case SOCK_CLOSE_WAIT:
            disconnect(sock);
            break;
        case SOCK_CLOSED:
            ret = socket(sock, Sn_MR_TCP, port, Sn_MR_ND);
            break;
        default:
            break;
    }

    return 0;
}

運行

void net_task(void *arg)
{    
    /* 配置網絡信息 */
    net_set_config();

    while (1) { 
        net_process_socket(SOCKET_ID_0, 12345);
        net_process_socket(SOCKET_ID_1, 12345);
        net_process_socket(SOCKET_ID_2, 12345);
		
		/* 延時20ms */
        vTaskDelay(100);
    }
}

w5500最多支持8個socket連接,由於項目中需要三個客戶端,所以,配置了三個socket。然後,在單獨的線程中依次運行每個socket對應的狀態機。具體的基礎知識不去細說,官網和博客中有非常多的描述。

問題

按說,按照官網的設置後,應該可以正常運行。確實可以運行了,三個客戶端也都可以連接上,但是,設備長時間測試中發現,客戶端和服務器之間的連接經常斷開。一旦斷了,有的客戶端可以連上,有的客戶端就再也連接不上了。解決的方式只能是重啓設備,這樣肯定是不行的啊。

客戶端是有重連機制的,會週期地發送心跳包給服務器,超時後就重新連接服務器,整個機制都沒有問題,問題就是始終連接不上服務器了。採用的有線連接,按說本身就不應該斷開的。這個問題折騰了一個月多都沒有頭緒,設備出廠的日期也只能往後推了。

解決方案

首先,通過網絡調試助手模擬客戶端,長時間運行看看會不會斷,沒有斷過。通過wireshark抓包,發現沒有keepalive心跳包,不知道爲什麼服務器沒有發出來。參考了同事和浩然電子杜工的建議,進行了如下修改,問題得到解決,目前運行十幾天,設備沒有斷過一次。

  • 增加備用socket
    有三個客戶端,那麼就開六個客戶端,每個客戶端有一個正常使用的,一個備用的,一旦連接斷掉,另一個立馬啓用,無縫切換。
  • 心跳處理
    自動發送心跳改成手動發送。
  • 增加延時
    官網上的資料包均是基於裸機的演示程序,而我們的項目是多線程的,所以,需要在死等的地方修改成延時。

配置

static void net_set_config(void)
{
    unsigned char mac[6] = {0x00, 0x08, 0xdc, 0x11, 0x11, 0x11};
    unsigned char ip[4]  = {192, 168, 1, 100};
    unsigned char sub[4] = {255, 255, 255, 0};
    unsigned char gw[4]  = {192, 168, 1, 1};
    unsigned char tx_size[8] = {2,2,2,2,2,2,2,2};
    unsigned char rx_size[8] = {2,2,2,2,2,2,2,2};

    /* 設置Mac地址 */
    set_mac_addr(mac);
    /* 設置IP */
    set_source_ip_addr(ip);
    /* 設置子網掩碼 */
    set_subnet_mask(sub);
    /* 設置網關 */
    set_gateway(gw);

    /* 初始化8個socket */
    sys_init(tx_size, rx_size);
    /* 設置超時時間 */
    set_retrans_time(2000);
    /* 設置最大重新發送次數 */
    set_retrans_num(3);	
}

不去開啓心跳自動發送機制。

狀態機

int net_process_socket(SOCKET sock, unsigned short port)
{
    int ret = -1;
    unsigned char state = SOCK_CLOSED;
    unsigned short len = 0;
    unsigned char data[MAX_MSG_LEN];
    static unsigned char sock_est_flag[8] = {1, 1, 1, 1, 1, 1, 1, 1};
    SOCKET another_sock;

    state = get_sock_status(sock);
    switch (state) {
        case SOCK_INIT:
            listen(sock);
            sock_est_flag[sock] = 1;
            break;
        case SOCK_ESTABLISHED:
            if (sock_est_flag[sock] == 1) {
                sock_est_flag[sock] = 0;
                if ((sock % 2) == 0) {
                    another_sock = sock + 1;
                } else {
                    another_sock = sock - 1;
                }
                if (get_sock_status(another_sock) == SOCK_ESTABLISHED) {
                    close(another_sock);
                }
            }
            
            if (get_sock_interrupt_status(sock) & Sn_IR_CON) {
                set_sock_interrupt_status(sock, Sn_IR_CON);
            }
            len = get_sock_rx_free_buff_size(sock);
            if (len > 0) {
                recv(sock, data, MAX_MSG_LEN);
                /* process the recv data */
            }
            break;
        case SOCK_CLOSE_WAIT:
            disconnect(sock);
            break;
        case SOCK_CLOSED:
            ret = socket(sock, Sn_MR_TCP, port, Sn_MR_ND);
            break;
        default:
            break;
    }

    return 0;
}

增加備用socket。備用socket時刻處於listen狀態。

運行

void net_task(void *arg)
{    
	int i = 0;
	int counter = 0;

    /* 配置網絡信息 */
    net_set_config();

    while (1) { 
        net_process_socket(SOCKET_ID_0, 12345);
        net_process_socket(SOCKET_ID_1, 12345);
        net_process_socket(SOCKET_ID_2, 12346);
        net_process_socket(SOCKET_ID_3, 12346);
        net_process_socket(SOCKET_ID_4, 12347);
        net_process_socket(SOCKET_ID_5, 12347);

		if ((counter++ % 100) == 0) {
			for (i = 0 ; i < 6; i++) {
				iinchip_write_data(Sn_CR(i),Sn_CR_SEND_KEEP);
			}
		}
		
		/* 延時20ms */
        vTaskDelay(100);
    }
}

手動發送keepalive心跳包,每10s發送一次。

延時

另外, 非常重要的一點是,在socket的發送和接收函數中死等的地方增加操作系統的延時。

總結

遇到問題不要着急,總會有解決的方案,現在沒有隻是時候未到。說明目前掌握的線索還不足以破案,需要繼續分析研究。黔驢技窮後多和同事請教討論,很多時候會思維定式,別人的某句話可能就能給你提供有用的線索。

再次感謝浩然電子的杜工耐心的幫助。
再此記錄下來艱辛歷程,希望可以幫到其他人。

QQ截图20211005151836.png

背景

項目中需要使用網絡,開始使用的stm32f4+lwip的方案,但是硬件成本有些高,更主要的是lwip不好用,老是斷,可能是自己沒有研究透它吧。經過長時間的調研論證,最終選擇了w5500這款芯片。它把TCP/IP網絡協議棧固化在了硬件芯片中,爲用戶留出應用層接口,簡單穩定。

移植過程

首先,從https://w5500.com/上下載芯片手冊和參考代碼瞭解芯片的原理及基本用法,內容不是很複雜。項目中需要W5500作爲TCP Server,會有三個客戶端連接。參考相關代碼,移植到項目中,具體代碼如下。

配置

static void net_set_config(void)
{
    unsigned char mac[6] = {0x00, 0x08, 0xdc, 0x11, 0x11, 0x11};
    unsigned char ip[4]  = {192, 168, 1, 100};
    unsigned char sub[4] = {255, 255, 255, 0};
    unsigned char gw[4]  = {192, 168, 1, 1};
    unsigned char tx_size[8] = {2,2,2,2,2,2,2,2};
    unsigned char rx_size[8] = {2,2,2,2,2,2,2,2};

    /* 設置Mac地址 */
    set_mac_addr(mac);
    /* 設置IP */
    set_source_ip_addr(ip);
    /* 設置子網掩碼 */
    set_subnet_mask(sub);
    /* 設置網關 */
    set_gateway(gw);

    /* 初始化8個socket */
    sys_init(tx_size, rx_size);
    /* 設置超時時間 */
    set_retrans_time(2000);
    /* 設置最大重新發送次數 */
    set_retrans_num(3);
	
    /* 打開心跳功能 */
    set_keepalive(SOCKET_ID_0);
	set_keepalive(SOCKET_ID_1);
	set_keepalive(SOCKET_ID_2);	
}

狀態機

int net_process_socket(SOCKET sock, unsigned short port)
{
    int ret = -1;
    unsigned char state = SOCK_CLOSED;
    unsigned short len = 0;
    unsigned char data[MAX_MSG_LEN];

    state = get_sock_status(sock);
    switch (state) {
        case SOCK_INIT:
            listen(sock);
            break;
        case SOCK_ESTABLISHED:
            if (get_sock_interrupt_status(sock) & Sn_IR_CON) {
                set_sock_interrupt_status(sock, Sn_IR_CON);
            }
            len = get_sock_rx_free_buff_size(sock);
            if (len > 0) {
                recv(sock, data, MAX_MSG_LEN);
				/* process the recv data */
            }
            break;
        case SOCK_CLOSE_WAIT:
            disconnect(sock);
            break;
        case SOCK_CLOSED:
            ret = socket(sock, Sn_MR_TCP, port, Sn_MR_ND);
            break;
        default:
            break;
    }

    return 0;
}

運行

void net_task(void *arg)
{    
    /* 配置網絡信息 */
    net_set_config();

    while (1) { 
        net_process_socket(SOCKET_ID_0, 12345);
        net_process_socket(SOCKET_ID_1, 12345);
        net_process_socket(SOCKET_ID_2, 12345);
		
		/* 延時20ms */
        vTaskDelay(100);
    }
}

w5500最多支持8個socket連接,由於項目中需要三個客戶端,所以,配置了三個socket。然後,在單獨的線程中依次運行每個socket對應的狀態機。具體的基礎知識不去細說,官網和博客中有非常多的描述。

問題

按說,按照官網的設置後,應該可以正常運行。確實可以運行了,三個客戶端也都可以連接上,但是,設備長時間測試中發現,客戶端和服務器之間的連接經常斷開。一旦斷了,有的客戶端可以連上,有的客戶端就再也連接不上了。解決的方式只能是重啓設備,這樣肯定是不行的啊。

客戶端是有重連機制的,會週期地發送心跳包給服務器,超時後就重新連接服務器,整個機制都沒有問題,問題就是始終連接不上服務器了。採用的有線連接,按說本身就不應該斷開的。這個問題折騰了一個月多都沒有頭緒,設備出廠的日期也只能往後推了。

解決方案

首先,通過網絡調試助手模擬客戶端,長時間運行看看會不會斷,沒有斷過。通過wireshark抓包,發現沒有keepalive心跳包,不知道爲什麼服務器沒有發出來。參考了同事和浩然電子杜工的建議,進行了如下修改,問題得到解決,目前運行十幾天,設備沒有斷過一次。

  • 增加備用socket
    有三個客戶端,那麼就開六個客戶端,每個客戶端有一個正常使用的,一個備用的,一旦連接斷掉,另一個立馬啓用,無縫切換。
  • 心跳處理
    自動發送心跳改成手動發送。
  • 增加延時
    官網上的資料包均是基於裸機的演示程序,而我們的項目是多線程的,所以,需要在死等的地方修改成延時。

配置

static void net_set_config(void)
{
    unsigned char mac[6] = {0x00, 0x08, 0xdc, 0x11, 0x11, 0x11};
    unsigned char ip[4]  = {192, 168, 1, 100};
    unsigned char sub[4] = {255, 255, 255, 0};
    unsigned char gw[4]  = {192, 168, 1, 1};
    unsigned char tx_size[8] = {2,2,2,2,2,2,2,2};
    unsigned char rx_size[8] = {2,2,2,2,2,2,2,2};

    /* 設置Mac地址 */
    set_mac_addr(mac);
    /* 設置IP */
    set_source_ip_addr(ip);
    /* 設置子網掩碼 */
    set_subnet_mask(sub);
    /* 設置網關 */
    set_gateway(gw);

    /* 初始化8個socket */
    sys_init(tx_size, rx_size);
    /* 設置超時時間 */
    set_retrans_time(2000);
    /* 設置最大重新發送次數 */
    set_retrans_num(3);	
}

不去開啓心跳自動發送機制。

狀態機

int net_process_socket(SOCKET sock, unsigned short port)
{
    int ret = -1;
    unsigned char state = SOCK_CLOSED;
    unsigned short len = 0;
    unsigned char data[MAX_MSG_LEN];
    static unsigned char sock_est_flag[8] = {1, 1, 1, 1, 1, 1, 1, 1};
    SOCKET another_sock;

    state = get_sock_status(sock);
    switch (state) {
        case SOCK_INIT:
            listen(sock);
            sock_est_flag[sock] = 1;
            break;
        case SOCK_ESTABLISHED:
            if (sock_est_flag[sock] == 1) {
                sock_est_flag[sock] = 0;
                if ((sock % 2) == 0) {
                    another_sock = sock + 1;
                } else {
                    another_sock = sock - 1;
                }
                if (get_sock_status(another_sock) == SOCK_ESTABLISHED) {
                    close(another_sock);
                }
            }
            
            if (get_sock_interrupt_status(sock) & Sn_IR_CON) {
                set_sock_interrupt_status(sock, Sn_IR_CON);
            }
            len = get_sock_rx_free_buff_size(sock);
            if (len > 0) {
                recv(sock, data, MAX_MSG_LEN);
                /* process the recv data */
            }
            break;
        case SOCK_CLOSE_WAIT:
            disconnect(sock);
            break;
        case SOCK_CLOSED:
            ret = socket(sock, Sn_MR_TCP, port, Sn_MR_ND);
            break;
        default:
            break;
    }

    return 0;
}

增加備用socket。備用socket時刻處於listen狀態。

運行

void net_task(void *arg)
{    
	int i = 0;
	int counter = 0;

    /* 配置網絡信息 */
    net_set_config();

    while (1) { 
        net_process_socket(SOCKET_ID_0, 12345);
        net_process_socket(SOCKET_ID_1, 12345);
        net_process_socket(SOCKET_ID_2, 12346);
        net_process_socket(SOCKET_ID_3, 12346);
        net_process_socket(SOCKET_ID_4, 12347);
        net_process_socket(SOCKET_ID_5, 12347);

		if ((counter++ % 100) == 0) {
			for (i = 0 ; i < 6; i++) {
				iinchip_write_data(Sn_CR(i),Sn_CR_SEND_KEEP);
			}
		}
		
		/* 延時20ms */
        vTaskDelay(100);
    }
}

手動發送keepalive心跳包,每10s發送一次。

延時

另外, 非常重要的一點是,在socket的發送和接收函數中死等的地方增加操作系統的延時。

總結

遇到問題不要着急,總會有解決的方案,現在沒有隻是時候未到。說明目前掌握的線索還不足以破案,需要繼續分析研究。黔驢技窮後多和同事請教討論,很多時候會思維定式,別人的某句話可能就能給你提供有用的線索。

再次感謝浩然電子的杜工耐心的幫助。
再此記錄下來艱辛歷程,希望可以幫到其他人。

COMMENTS

Please Login to comment
  Subscribe  
Notify of
POSTED BY
TAGS
Reusable S/W