25

守勤 · 2022年11月30日 · 重庆市江北区

【GD32F427开发板试用】开发一款网络音乐播放器

资源介绍

非常荣幸能够参与到这次GD32F427开发板试用的活动中来,开发板的设计非常简洁,板载了一颗GD32F103C8T6和一颗GD32F407VKT6,其中GD32F103C8T6是为了给GD32F407VKT6的程序调试仿真用的,GD32F407VKT6的绝大多数gpio都通过两侧的排针引出(需要自己焊接),板子上还外置了25MHz和32.768KHz的晶振,故系统的时钟频率达到200MHz(最高可达到240MHz),另外,板子上还设计了USB全速(USBFS)和USB高速接口(USBHS)接口,USBHS使用了一颗USB PHY芯片USB3300_EZK,所以用USB做一些应用上的设计应该是比较方便的。
board.jpg

硬件设计

板子上的大致资源就是这些,所以要设计一些复杂点的应用就需要额外的一些模块,本次设计就用到了另外的两个模块:VS1053B和W5500模块,这两个模块在某宝上可以很容易买到。其中VS1053B内置了音频解码硬件单元,包含16 KiB 指令RAM和0.5KiB 多的数据RAM,而W5500集成全硬件 TCP/IP 协议栈的嵌入式以太网控制器,同时也是一颗工业级以太网控制芯片。

GD32F407VKT6与这两个模块的通信方式都是通过spi总线,所以这里有必要简单介绍一下SPI总线。

SPI是一种高速、高效率的串行接口技术。通常由一个主模块和一个或多个从模块组成,主模块选择一个从模块进行同步通信,从而完成数据的交换。SPI是一个环形结构,通信时需要至少4根线(事实上在单向传输时3根线也可以)。

SPI的通信原理很简单,它以主从方式工作,这种模式通常有一个主设备和一个或多个从设备,需要至少4根线,事实上3根也可以(单向传输时)。也是所有基于SPI的设备共有的,它们是MISO(主设备数据输入)、MOSI(主设备数据输出)、SCLK(时钟)、CS(片选)。

(1)MISO– Master Input Slave Output,主设备数据输入,从设备数据输出;
(2)MOSI– Master Output Slave Input,主设备数据输出,从设备数据输入;
(3)SCLK – Serial Clock,时钟信号,由主设备产生;
(4)CS – Chip Select,从设备使能信号,由主设备控制。

其中,CS是从芯片是否被主芯片选中的控制信号,也就是说只有片选信号为预先规定的使能信号时(高电位或低电位),主芯片对此从芯片的操作才有效。这就使在同一条总线上连接多个SPI设备成为可能。

接下来就负责通讯的3根线了。通讯是通过数据交换完成的,这里先要知道SPI是串行通讯协议,也就是说数据是一位一位的传输的。这就是SCLK时钟线存在的原因,由SCLK提供时钟脉冲,SDI,SDO则基于此脉冲完成数据传输。数据输出通过 SDO线,数据在时钟上升沿或下降沿时改变,在紧接着的下降沿或上升沿被读取。完成一位数据传输,输入也使用同样原理。因此,至少需要8次时钟信号的改变(上沿和下沿为一次),才能完成8位数据的传输。

时钟信号线SCLK只能由主设备控制,从设备不能控制。同样,在一个基于SPI的设备中,至少有一个主设备。这样的传输方式有一个优点,在数据位的传输过程中可以暂停,也就是时钟的周期可以为不等宽,因为时钟线由主设备控制,当没有时钟跳变时,从设备不采集或传送数据。SPI还是一个数据交换协议:因为SPI的数据输入和输出线独立,所以允许同时完成数据的输入和输出。芯片集成的SPI串行同步时钟极性和相位可以通过寄存器配置,IO模拟的SPI串行同步时钟需要根据从设备支持的时钟极性和相位来通讯。

最后,SPI接口的一个缺点:没有指定的流控制,没有应答机制确认是否接收到数据。

这里贴出GD32与VS1053B和W5500模块的引脚的初始化代码,也需要对应连线。

void gpio_init(void)
{ 
    rcu_periph_clock_enable(RCU_GPIOA);
    rcu_periph_clock_enable(RCU_GPIOB);
    rcu_periph_clock_enable(RCU_GPIOC);
    rcu_periph_clock_enable(RCU_GPIOD);

    /* init uart0 pin */
    gpio_mode_set(GPIOB, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_6);
    gpio_output_options_set(GPIOB, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_6);
    
    gpio_mode_set(GPIOB, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_7);

    gpio_af_set(GPIOB, GPIO_AF_7, GPIO_PIN_6 | GPIO_PIN_7);
    
    /*init w5500 pin */
    gpio_mode_set(W5500_RST_PORT, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, W5500_RST_PIN);
    gpio_output_options_set(W5500_RST_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, W5500_RST_PIN);
    gpio_bit_set(W5500_RST_PORT, W5500_RST_PIN);
    
    gpio_mode_set(W5500_INT_PORT, GPIO_MODE_INPUT, GPIO_PUPD_NONE, W5500_INT_PIN);
    
    gpio_mode_set(W5500_CS_PORT, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, W5500_CS_PIN);
    gpio_output_options_set(W5500_CS_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, W5500_CS_PIN);
    gpio_bit_set(W5500_CS_PORT, W5500_CS_PIN);

    gpio_mode_set(GPIOB, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_3| GPIO_PIN_4| GPIO_PIN_5);
    gpio_output_options_set(GPIOB, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_3| GPIO_PIN_4| GPIO_PIN_5);
    gpio_af_set(GPIOB, GPIO_AF_6, GPIO_PIN_3| GPIO_PIN_4| GPIO_PIN_5);
   
    /* init vs1053B pin */
    gpio_mode_set(VS1053B_XCS_PORT, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, VS1053B_XCS_PIN);
    gpio_output_options_set(VS1053B_XCS_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, VS1053B_XCS_PIN);
    gpio_bit_set(VS1053B_XCS_PORT, VS1053B_XCS_PIN);

    gpio_mode_set(VS1053B_RST_PORT, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, VS1053B_RST_PIN);
    gpio_output_options_set(VS1053B_RST_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, VS1053B_RST_PIN);
    gpio_bit_set(VS1053B_RST_PORT, VS1053B_RST_PIN);

    gpio_mode_set(VS1053B_DREQ_PORT, GPIO_MODE_INPUT, GPIO_PUPD_NONE, VS1053B_DREQ_PIN);

    gpio_mode_set(VS1053B_XDCS_PORT, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, VS1053B_XDCS_PIN);
    gpio_output_options_set(VS1053B_XDCS_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, VS1053B_XDCS_PIN);
    gpio_bit_set(VS1053B_XDCS_PORT, VS1053B_XDCS_PIN);

    gpio_mode_set(GPIOA, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7);
    gpio_output_options_set(GPIOA, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7);
    gpio_af_set(GPIOA, GPIO_AF_5,  GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7);
}

void gd_spi_init(uint32_t SPI)
{
    spi_parameter_struct   spi_parameter_structure;

    rcu_periph_clock_enable(RCU_SPI0);
    rcu_periph_clock_enable(RCU_SPI2);

    spi_parameter_structure.trans_mode = SPI_TRANSMODE_FULLDUPLEX;
    spi_parameter_structure.device_mode = SPI_MASTER;
    spi_parameter_structure.frame_size = SPI_FRAMESIZE_8BIT;
    if (SPI == VS1053B_SPI) {
        spi_parameter_structure.clock_polarity_phase = SPI_CK_PL_HIGH_PH_2EDGE;
        spi_parameter_structure.prescale = SPI_PSC_64;
    } else if (SPI == W5500_SPI) {
        spi_parameter_structure.clock_polarity_phase = SPI_CK_PL_LOW_PH_1EDGE;
        spi_parameter_structure.prescale = SPI_PSC_2;
    }
    spi_parameter_structure.nss = SPI_NSS_SOFT;
    spi_parameter_structure.endian = SPI_ENDIAN_MSB;

    spi_crc_polynomial_set(SPI, 7);
    spi_crc_on(SPI);
    spi_init(SPI, &spi_parameter_structure);
    spi_enable(SPI);
    #endif
}

软件设计

网络协议的选择

W5500提供了UDP与TCP两种协议供我们进行选择,那么,这个网络播放器播放器是采用UDP还是TCP进行通讯就是一个问题了.

首先UDP是一个相当简单的协议,它有着更加简短的包头,这意味着你可以省下更多的带宽来做又用的事情,它是无连接的,你也不需要担心你的W5500是否有足够多的socket来维持多连接,另外它拥有非常好的可控性.你也不用花太多心思去处理一堆开放性连接带来的问题。

同时,TCP带来的好处是显而易见的,它有可靠的丢包重发机制,能够保证时序性,连线断线一目了然,我们也不需要去关心那些烦人的心跳包,如果说到文件传输,那么几乎很多答案都是:别考虑了,用TCP吧,你看谁谁谁用TCP不一样做的很好.是的,TCP有如此之多的优势,看上去确实十分的诱人,但是如果你真正思考一下我们所处的环境,并且真正理解TCP在何时才算真正的”有效”,你也许会发现,TCP并不适合在当前的系统中来做。

为什么我们使用UDP而非TCP?实际上,TCP之前所述的优势,在我们当前片上系统并不好使:

1、我们的片上系统缓存有限(256KB),如果没有做额外的修改,而W5500仅仅能提供2k的收缓存,如果我们设计的每个包的大小在1.3K左右,那意味着TCP所谓保证的时序性将不再起作用.更加糟糕的是,我们将无法在带宽处理上做出优化,假设在一个网络状况极其糟糕的环境中,你必须等待服务器回应数据包后才能请求下一个,假如延迟在百毫秒乃甚于一些高码率的音频在几十毫秒以上,这种延迟都将是毁灭性的.这将直接导致音频无法正常播放.
2、TCP也许能帮我们来实现重发,但这个功能我们在UDP上一样可以非常简单的实现,但TCP的重发机制在第一点所述(window根本不足以容纳更多的包,这种重发还可能导致毁灭性的延迟)已经成为了一种累赘,同时,TCP在接近底层的编码上也显得不那么的”友好”,你必须开始花心思来处理W5500给你的一堆中断问题(比如连接,断线….)为此你得开个状态机来重新处理这堆问题(比如断线重连)增加自己的工作量,而UDP没有这些本没有必要去耗费精力的问题.
3、UDP在带宽与延迟优化上,实现起来简单多了,具体的步骤我们将在下一个章节进行讨论.

程序结构的设计

线程设计

整个系统的数据流的路径是由客户端向服务端主动请求音频数据,然后服务端只管发送请求的对应数据类型。
data flow.PNG
考虑程序的可设计性和易读性,这里用了rtthred nano的嵌入式系统,创建了两个线程,一个是用于udp的数据处理线程,一个用于VS1053B播放音频的线程,两个线程的接口交互是通过函数指针的方式。

void player_entry(void *parameter)
{
    uint8_t **data = rt_malloc(sizeof(uint8_t *));
    uint8_t *rdata, cnt, pkt_cnt, left ,bytes;
    uint16_t len1 = 0;
    (void)parameter;
    vs10xx_init();
    
    while(1) {
        if(player_contxt.data_cbk((uint8_t **)data, &len1)) {
            rdata = (uint8_t *)*data;
            left = len1 % WRITE_BYTES;
            pkt_cnt = len1/WRITE_BYTES-1;
            for (cnt = 0; cnt <= pkt_cnt; cnt++) {
                bytes = (cnt <= pkt_cnt) ? WRITE_BYTES : left;
                if(vs10xx_write(rdata, bytes) == 1) {
                    printf("vs10xx no write:%d\r\n", cnt);
                    cnt--;
                    Delay_ms(1);
                } else {
                    if (cnt <= pkt_cnt) {
                        rdata = rdata + WRITE_BYTES;
                    }
                }
            }
            player_contxt.node_cbk((uint8_t *)*data);
        }
    }
}

int player_init()
{
    uint8_t ret = ERR_OK, i;

    memset(&player_contxt, 0, sizeof(player_contxt));


    player_contxt.stack_size = PLAYER_THREAD_STACK_SIZE;
    player_contxt.name = PLAYER_THREAD_NAME;
    player_contxt.task_prio = PLAYER_THREAD_PRIORITY;
    player_contxt.tick = PLAYER_THREAD_TIMESLICE;
    player_contxt.exe_func = player_entry;
    player_contxt.task_handle = rt_thread_create(player_contxt.name, player_contxt.exe_func,
        NULL, player_contxt.stack_size, player_contxt.task_prio, player_contxt.tick);
    if (player_contxt.task_handle == NULL) {
        ret = ERR_FAIL;
        goto error_0;
    }
    rt_thread_startup(player_contxt.task_handle);

    printf("creat player thread successful!\r\n");
    goto success;

error_0:
    printf("creat player thread failed!\r\n");


success:
    return ret;
}

void udptxrx_entry(void *parameter)
{
    uint8_t status;

    (void)parameter;
    while(1) {
        if (rt_mb_recv(udptxrx_contxt.mb, (rt_uint32_t *)&status, RT_WAITING_FOREVER) == RT_EOK) {
            switch(status) {
            case QUERY_MUSIC_INDX_STATUS:
            {
                udptxrx_music_idx_handle();
                break;
            }
            case WAIT_MUSIC_INDX_STATUS:
            {
                udptxrx_wait_music_idx_handle();
                break;
            }
            case QUERY_MUSIC_DATA_STATUS:
            {
                udptxrx_music_data_handle();
                break;
            }
            case WAIT_MUSIC_MUSIC_DATA_STATUS:
            {
                udptxrx_wait_music_data_handle();
                break;
            }
            case IDLE_STATUS:
            {
                udptxrx_idle_handle();
                break;
            }
            default:
                break;
            }
        }
    }
}

int udptxrx_init()
{
    uint8_t ret = ERR_OK, i;
    uint8_t destip[4] = DEST_IP_ADDR;
    udps_addr_info_t local = {
        .ip = DEFAULT_IP_ADDR,
        .sn = LOCAL_SOCKET_NUM,
        .port = LOCAL_SOCKET_PORT,
    };

    memset(&udptxrx_contxt, 0, sizeof(udptxrx_contxt));

    memset(&data_pkt, 0x0, sizeof(data_pkt));

    w5500_init(&local);   
    udptxrx_contxt.dest.sn = DEST_SOCKET_NUM;
    udptxrx_contxt.dest.port = DEST_SOCKET_PORT;
    memcpy(udptxrx_contxt.dest.ip, destip, sizeof(destip));

    udptxrx_contxt.stack_size = UDPTXRX_THREAD_STACK_SIZE;
    udptxrx_contxt.name = UDPTXRX_THREAD_NAME;
    udptxrx_contxt.task_prio = UDPTXRX_THREAD_PRIORITY;
    udptxrx_contxt.tick = UDPTXRX_THREAD_TIMESLICE;
    udptxrx_contxt.exe_func = udptxrx_entry;
    udptxrx_contxt.task_handle = rt_thread_create(udptxrx_contxt.name, udptxrx_contxt.exe_func,
        NULL, udptxrx_contxt.stack_size, udptxrx_contxt.task_prio, udptxrx_contxt.tick);
    if (udptxrx_contxt.task_handle == NULL) {
        ret = ERR_FAIL;
        goto error_0;
    }
    rt_thread_startup(udptxrx_contxt.task_handle);


    udptxrx_contxt.mb = rt_mb_create("udptxrx mb", 4, RT_IPC_FLAG_FIFO);

    /* init write and read queue and put all will write pkt in queue */
    udptxrx_contxt.wq = rt_malloc(sizeof(pktq_t));
    if (udptxrx_contxt.wq == NULL) {
        ret = ERR_NOMEM;
        goto error_1;
    }

    udptxrx_contxt.rq = rt_malloc(sizeof(pktq_t));
    if (udptxrx_contxt.rq == NULL) {
         ret = ERR_NOMEM;
         goto error_2;
    }
    queue_init(udptxrx_contxt.wq);
    queue_init(udptxrx_contxt.rq);

    udptxrx_contxt.wq->lock = rt_mutex_create("wq_mutex",RT_IPC_FLAG_PRIO);
    if (udptxrx_contxt.wq->lock == NULL) {
        ret = ERR_NOMEM;
        goto error_3;
    }

    udptxrx_contxt.rq->lock = rt_mutex_create("rq_mutex",RT_IPC_FLAG_PRIO);
    if (udptxrx_contxt.rq->lock == NULL) {
        ret = ERR_NOMEM;
        goto error_4;
    }

    for (i = 0; i < PKT_CNT; i++) {
        pkt_enqueue(udptxrx_contxt.wq, &data_pkt[i].node);
        printf("&data_pkt[%d].node=%d\r\n", i, (int)&data_pkt[i].node);
    }

    udptxrx_contxt.query_idx_timer = rt_timer_create("udp txrx query music index timer",
         udptxrx_query_idx_timer_data_path_func, RT_NULL, TIMER_PERIOD,
         RT_TIMER_FLAG_PERIODIC );
    if (udptxrx_contxt.query_idx_timer == RT_NULL) {
         ret = ERR_NOMEM;
         goto error_5;
    }
    rt_timer_start(udptxrx_contxt.query_idx_timer);

    udptxrx_contxt.inter_timer = rt_timer_create("udp txrx interval timer",
        udptxrx_interval_timer_data_path_func, RT_NULL, TIMER_PERIOD,
        RT_TIMER_FLAG_PERIODIC );
    if (udptxrx_contxt.inter_timer == RT_NULL) {
        ret = ERR_NOMEM;
        goto error_6;
    }

    udptxrx_contxt.retry_timer = rt_timer_create("udp txrx retry timer",
        udptxrx_retry_timer_data_path_func, RT_NULL, TIMER_PERIOD,
        RT_TIMER_FLAG_ONE_SHOT );
    if (udptxrx_contxt.retry_timer == RT_NULL) {
        ret = ERR_NOMEM;
        goto error_7;
    }

    printf("creat udptxrx thread successful!\r\n");
    goto success;

error_7:
    rt_timer_delete(udptxrx_contxt.inter_timer);
    printf("creat retry timer failed!\r\n");
error_6:
    rt_timer_delete(udptxrx_contxt.query_idx_timer);
    printf("creat query music index timer failed!\r\n");
error_5:
    rt_mutex_delete(udptxrx_contxt.rq->lock);
    printf("creat interval timer failed!\r\n");
error_4:
    rt_mutex_delete(udptxrx_contxt.wq->lock);
    printf("creat rq lock failed!\r\n");
error_3:
    rt_free(udptxrx_contxt.rq);
    printf("creat wq lock failed!\r\n");
error_2:
    rt_free(udptxrx_contxt.wq);
    printf("malloc rq failed!\r\n");
error_1:
    rt_thread_delete(udptxrx_contxt.task_handle);
    printf("malloc wq failed!\r\n");
error_0:
    printf("creat udptxrx thread failed!\r\n");


success:
    return ret;
}

int main()
{
    udptxrx_init();
    player_init();
    iot_udptxrx_player_register(udptxrx_player_data_send_cbk, udptxrx_player_node_send_cbk);
    
    return 0;
}

环形缓存

因为片上系统资源有限,所以绝大多数的时候,我们无法将整个多媒体资源下载下来后再进行播放.以此来看,环形缓存是个不错的选择.
在流媒体的环形缓存中,一般存在两个队列,下面笔者使用几张图来演示环形缓存是如何工作的.
首先我们先定义两个队列,一个是可用于缓存音频数据的队列wq;一个是可用于播放音频数据的队列rq。wq未空,表示可以取出pkt用于音频数据的缓存,rq未空,表示pkt可以用于音频数据的播放。队列的进和出只是利用了pkt的头,其数据域实现零拷贝,在一定程度上不会因为大量数据的搬移影响性能。

void queue_init(pktq_t *queue)
{
    queue->list_head = NULL;
    queue->list_tail = NULL;
    queue->depth = 0;
}

/* push in tail/backend */
void pkt_enqueue(pktq_t *queue, list_node_t *p_node)
{
    rt_mutex_take(queue->lock,RT_WAITING_FOREVER);

    if (queue->list_tail == NULL) {
        queue->list_head = queue->list_tail = p_node;
        p_node->next = NULL;
    } else {
        queue->list_tail->next = p_node;
        p_node->next = NULL;
        queue->list_tail = p_node;
    }

    queue->depth++;
    rt_mutex_release(queue->lock);
}

/* pop from head/front */
list_node_t *pkt_dequeue(pktq_t *queue)
{
    list_node_t *entry = NULL;

    rt_mutex_take(queue->lock,RT_WAITING_FOREVER);
    if (queue->list_head) {
        queue->depth--;
        entry = queue->list_head;
        if (queue->list_head == queue->list_tail) {
            queue->list_head = NULL;
            queue->list_tail = NULL;
        } else {
            queue->list_head = queue->list_head->next;
        }
        entry->next = NULL;
    }
    rt_mutex_release(queue->lock);

    return entry;
}

uint8_t queue_empty_check(pktq_t *queue) {

    uint32_t depth;

    rt_mutex_take(queue->lock,RT_WAITING_FOREVER);
    depth = queue->depth;
    rt_mutex_release(queue->lock);

    return (depth > 0) ? ERR_OK : ERR_FAIL;
}

/* init write and read queue and put all will write pkt in queue */
    udptxrx_contxt.wq = rt_malloc(sizeof(pktq_t));
    if (udptxrx_contxt.wq == NULL) {
        ret = ERR_NOMEM;
        goto error_1;
    }

    udptxrx_contxt.rq = rt_malloc(sizeof(pktq_t));
    if (udptxrx_contxt.rq == NULL) {
         ret = ERR_NOMEM;
         goto error_2;
    }
    queue_init(udptxrx_contxt.wq);
    queue_init(udptxrx_contxt.rq);

    udptxrx_contxt.wq->lock = rt_mutex_create("wq_mutex",RT_IPC_FLAG_PRIO);
    if (udptxrx_contxt.wq->lock == NULL) {
        ret = ERR_NOMEM;
        goto error_3;
    }

    udptxrx_contxt.rq->lock = rt_mutex_create("rq_mutex",RT_IPC_FLAG_PRIO);
    if (udptxrx_contxt.rq->lock == NULL) {
        ret = ERR_NOMEM;
        goto error_4;
    }

    for (i = 0; i < PKT_CNT; i++) {
        pkt_enqueue(udptxrx_contxt.wq, &data_pkt[i].node);
        printf("&data_pkt[%d].node=%d\r\n", i, (int)&data_pkt[i].node);
    }

数据请求

我们首先先来实现比较简单的服务端,为了尽量保持精简设计,服务端只处理两种类型的数据包,一个是客户端发送上来的音频文件请求包,一种是音频数据请求包,其中音频数据请求包是根据最后一次请求的音频文件而定的。

其逻辑实现大致如下:
1、初始化网络,监听UDP端口;
2、当收到音频文件请求包时,打开这个音频文件,并且返回这个音频文件的文件大小,如果这个文件不存在,返回的大小为0;
3、当收到音频数据请求包时,返回对应音频数据;
4、重复2,3过程。

对应的客户端数据请求也分为两种,一种是请求整首音乐文件的大小,另一种是请求音乐包序列号的数据。

void udptxrx_music_idx_handle(void)
{
    query_pkt_t send_pkt;

    send_pkt.type = QUERY_MUSIC_INDX;
    send_pkt.music.idx = udptxrx_contxt.music_idx;  //query music index
    if (w5500_udps_send(&udptxrx_contxt.dest, (uint8_t *)&send_pkt, sizeof(query_pkt_t))) {
        printf("query music index:%d\r\n", send_pkt.music.idx);
    }
}

void udptxrx_music_data_handle(void)
{
    query_pkt_t send_pkt;

    send_pkt.type = QUERY_MUSIC_DATA;
    if ( udptxrx_contxt.retry_flag == 1) {    //必须要此次要求重传的标志清零,否则正常的seq仍然为重传seq
        udptxrx_contxt.retry_flag = 0;
        udptxrx_contxt.pkt_timer_flag.pkt_timer |= 0x01;
        send_pkt.music.seq = udptxrx_contxt.retry_seq;
    } else {
        udptxrx_contxt.pkt_seq++;
        send_pkt.music.seq = udptxrx_contxt.pkt_seq;
        if (send_pkt.music.seq == (udptxrx_contxt.win_min + TX_SLID_WINDOWS)) {
            udptxrx_contxt.pkt_timer_flag.pkt_timer |= 1<<(TX_SLID_WINDOWS - 1);
        } else {
            udptxrx_contxt.pkt_timer_flag.pkt_timer |= 1<<((udptxrx_contxt.pkt_seq -
                udptxrx_contxt.win_cnt) % TX_SLID_WINDOWS);
        }
    }
    
    if (w5500_udps_send(&udptxrx_contxt.dest, (uint8_t *)&send_pkt, sizeof(query_pkt_t))) {
        printf("send QUERY_MUSIC_DATA,seq = %d\r\n", send_pkt.music.seq);
    }
}

重发机制

因为采用了UDP协议,就不免会有丢包的问题。这里就必须在应用层方面设计一套机制实现丢失的数据包重发。这里介绍一下选择重传协议的原理。
SR1.png

SR发送方要做的事
从上层收到数据后, SR发送方检查一下可用于该帧的信号, 如果序号位于发送窗口内, 则发送数据帧,否则就会像GBN一样, 要么将数据缓存, 要么返回给上层之后再传输。如果收到ACK, 加入该帧序号在窗口内,则SR发送方将那个确认的帧标记为已接收;如果该帧序号是窗口的下界, 则窗口向前移动到具有最小序号的未确认帧处;如果窗口移动了并且有序号在窗口内的未发送帧, 则发送这些帧。

SR接收方要做的事
SR接收方将确认一个正确接收的帧而不管其是否乱序, 失序的帧将被缓存, 并返回给发送方一个该帧的确认帧, 直到所有帧皆被收到为止, 这时才可以将这一批帧按序交付给上层, 然后向前移动滑动窗口。

SR2.png
我们这里简单应用了一下滑动窗口的原理,只有在客户端设计了滑动窗口。首先会用inter_timer定时器回调函数去正常请求序列号的数据包,当接收到的数据包超过了滑动窗口的数量并且需要接收数据包的最小序列号未收到就会通过retry_timer的回调函数请求重传。

另外,程序设计了日志打印,方便查看丢包和重传信息。

void udptxrx_wait_music_data_handle(void)
{
    recv_pkt_t *pkt = NULL;
    uint8_t idx , j = 0;
    static int16_t last_retry_seq = -1;

    if (queue_empty_check(udptxrx_contxt.wq)) {
        printf("no wq\r\n");
        return;
    }

    pkt = (recv_pkt_t*)pkt_dequeue(udptxrx_contxt.wq);
    printf("wq dequeue node addr: %d\r\n", (int)pkt);
    if (pkt == NULL) {
        return;
    }

    pkt->len = 0;
    pkt->len = w5500_udps_recv(&udptxrx_contxt.dest, (uint8_t *)&pkt->recv_data,
        sizeof(pkt->recv_data));
    if ((pkt->len > sizeof(recv_data_pkt_t)) || (pkt->len == 0) ||
        (RESP_MUSIC_MUSIC_DATA != pkt->recv_data.type) ||
        ((RESP_MUSIC_MUSIC_DATA == pkt->recv_data.type) &&
        (pkt->recv_data.seq <= udptxrx_contxt.win_min) &&
        (udptxrx_contxt.win_min != -1))) {
        printf("recv len=%d,type=%d\r\n",pkt->len,pkt->recv_data.type);
        pkt_enqueue(udptxrx_contxt.wq, &pkt->node);
        return;
    }

    //便于打印调试信息
    if ((pkt->recv_data.seq == udptxrx_contxt.retry_seq) &&
        (udptxrx_contxt.retry_seq != -1) && (last_retry_seq != -1)) {
        printf("recv seq=%d,last_retry_seq=%d\r\n", pkt->recv_data.seq, last_retry_seq);
    }

    //seq is from 0 ~ n
    if (pkt->recv_data.seq == udptxrx_contxt.retry_seq) {
        idx = 0;
        last_retry_seq = pkt->recv_data.seq;
    } else if (pkt->recv_data.seq == (udptxrx_contxt.win_min + TX_SLID_WINDOWS)) {
        idx = TX_SLID_WINDOWS - 1;
    } else {
        idx = (pkt->recv_data.seq  - udptxrx_contxt.win_cnt) % TX_SLID_WINDOWS;
    }

    buf_node[idx] = (uint32_t)&pkt->node;
    udptxrx_contxt.pkt_timer_flag.pkt_timer &= ~(1<<idx);
    printf("recv music seq %d:%d B,idx:%d,pkt_timer_flag:%d\r\n", pkt->recv_data.seq,
        pkt->len,idx,udptxrx_contxt.pkt_timer_flag.pkt_timer);
 
    //等待最小序列号数据包收到。就将其加入rq
    while ((udptxrx_contxt.pkt_timer_flag.pkt_timer & 0x01) == 0 && (buf_node[0] != 0) &&
        (j < TX_SLID_WINDOWS) && (udptxrx_contxt.win_min < udptxrx_contxt.pkt_cnt)) {
        udptxrx_contxt.win_cnt = (udptxrx_contxt.win_cnt + 1) % TX_SLID_WINDOWS;
        udptxrx_contxt.win_min = pkt->recv_data.seq + j;
        pkt_enqueue(udptxrx_contxt.rq, (list_node_t *)buf_node[0]);
        printf("rq enqueue node,win_min=%d,buf_node[%d]=%d\r\n", udptxrx_contxt.win_min,
            j, buf_node[0]);
        udptxrx_contxt.pkt_timer_flag.pkt_timer >>= 1;
        move_node(buf_node, 7);
        buf_node[7] = 0;
        j++;
    }
}

void udptxrx_interval_timer_data_path_func(void *parameter)
{
    uint8_t sta;
    (void)parameter;

    udptxrx_contxt.tm_cnt++;
    if (udptxrx_contxt.tm_cnt % QUERY_DATA_INVERVAL == 0) {
        if (udptxrx_contxt.wq->depth == 0) {
            printf("wq->depth %d,rq->depth %d\r\n", udptxrx_contxt.wq->depth,
            udptxrx_contxt.rq->depth);
            return;
        }

        //若最小序列的窗口未收到序号且请求的窗口满了,不能继续请求,只能等重传
        if ((udptxrx_contxt.pkt_timer_flag.pkt_timer & 0x01) == 1 &&
            (udptxrx_contxt.pkt_seq == (udptxrx_contxt.win_min + TX_SLID_WINDOWS))) {
            rt_timer_control(udptxrx_contxt.retry_timer, RT_TIMER_CTRL_GET_STATE, &sta);
            if (RT_TIMER_FLAG_DEACTIVATED == sta) {
                rt_timer_start(udptxrx_contxt.retry_timer);
            }
            udptxrx_contxt.status = WAIT_MUSIC_MUSIC_DATA_STATUS;
            rt_mb_send(udptxrx_contxt.mb, (rt_uint32_t)udptxrx_contxt.status);
            return;
        }

        if ((udptxrx_contxt.pkt_seq + 1) < udptxrx_contxt.pkt_cnt) {
            udptxrx_contxt.status = QUERY_MUSIC_DATA_STATUS;
            printf("wq->depth %d,rq->depth %d\r\n", udptxrx_contxt.wq->depth,
                udptxrx_contxt.rq->depth);
            rt_mb_send(udptxrx_contxt.mb, (rt_uint32_t)udptxrx_contxt.status);
        } else {
            //避免最后几个序列丢了
            if (udptxrx_contxt.pkt_timer_flag.pkt_timer != 0) {
                rt_timer_control(udptxrx_contxt.retry_timer, RT_TIMER_CTRL_GET_STATE, &sta);
                if (RT_TIMER_FLAG_DEACTIVATED == sta) {
                    rt_timer_start(udptxrx_contxt.retry_timer);
                }
                udptxrx_contxt.status = WAIT_MUSIC_MUSIC_DATA_STATUS;
                rt_mb_send(udptxrx_contxt.mb, (rt_uint32_t)udptxrx_contxt.status);
            } else {
                rt_timer_stop(udptxrx_contxt.inter_timer);
                udptxrx_contxt.status = IDLE_STATUS;
                rt_mb_send(udptxrx_contxt.mb, (rt_uint32_t)udptxrx_contxt.status);
            }
        }
    } else if (udptxrx_contxt.tm_cnt % WAIT_DATA_INVERVAL == 0) {
        udptxrx_contxt.status = WAIT_MUSIC_MUSIC_DATA_STATUS;
        printf("WAIT_MUSIC_MUSIC_DATA_STATUS\r\n");
        rt_mb_send(udptxrx_contxt.mb, (rt_uint32_t)udptxrx_contxt.status);
    }
}

void udptxrx_retry_timer_data_path_func(void *parameter)
{
    uint8_t seq_idx;

    (void)parameter;

    for (seq_idx = 0; seq_idx < TX_SLID_WINDOWS; seq_idx++) {
        if(udptxrx_contxt.pkt_timer_flag.pkt_timer<<(TX_SLID_WINDOWS - seq_idx - 1))
            break;
    }
    if (seq_idx < TX_SLID_WINDOWS) {
        udptxrx_contxt.retry_seq = udptxrx_contxt.win_min + seq_idx + 1;
        udptxrx_contxt.retry_flag = 1;
        printf("retry data query seq %d\r\n", udptxrx_contxt.retry_seq);
        udptxrx_contxt.status = QUERY_MUSIC_DATA_STATUS;
        rt_mb_send(udptxrx_contxt.mb, (rt_uint32_t)udptxrx_contxt.status);
    } else {
        rt_timer_stop(udptxrx_contxt.retry_timer);
        udptxrx_contxt.status = IDLE_STATUS;
    }
}

最后,查看下日志的记录,虽然是局域网传输,但是还是有丢包的情况,通过重传机制,完美地解决了丢包。
log.PNG

效果展示

https://www.bilibili.com/vide...

写在最后

整个程序的驱动和应用是松耦合的,所以可以很容易移植到其他MCU芯片上,而不需要更改应用层一点儿内容,只需要更改相应的外设配置即可。
另外,关于GD32F427的库函数,在gpio复用spi功能的时候,使用gpio_af_set函数,其中alt_func_num的参数填GPIO_AF_5,则SPI2的数据不对,后来改成的GPIO_AF_6才可以,但是库函数的注释又写了GPIO_AF_5支持SPI2,翻看使用手册并没有详细介绍AF寄存器。这个问题不知道是我使用问题还是官方库的注释问题。

/*!
    \brief      set GPIO alternate function
    \param[in]  gpio_periph: GPIO port
                only one parameter can be selected which is shown as below:
      \arg        GPIOx(x = A,B,C,D,E,F,G,H,I)
    \param[in]  alt_func_num: GPIO pin af function
      \arg        GPIO_AF_0: SYSTEM
      \arg        GPIO_AF_1: TIMER0, TIMER1
      \arg        GPIO_AF_2: TIMER2, TIMER3, TIMER4
      \arg        GPIO_AF_3: TIMER7, TIMER8, TIMER9, TIMER10
      \arg        GPIO_AF_4: I2C0, I2C1, I2C2
      \arg        GPIO_AF_5: SPI0, SPI1, SPI2, SPI3, SPI4, SPI5
      \arg        GPIO_AF_6: SPI2, SPI3, SPI4
      \arg        GPIO_AF_7: USART0, USART1, USART2, SPI1, SPI2
      \arg        GPIO_AF_8: UART3, UART4, USART5, UART6, UART7
      \arg        GPIO_AF_9: CAN0, CAN1, TLI, TIMER11, TIMER12, TIMER13, I2C1, I2C2, CTC
      \arg        GPIO_AF_10: USB_FS, USB_HS
      \arg        GPIO_AF_11: ENET
      \arg        GPIO_AF_12: EXMC, SDIO, USB_HS
      \arg        GPIO_AF_13: DCI
      \arg        GPIO_AF_14: TLI
      \arg        GPIO_AF_15: EVENTOUT
    \param[in]  pin: GPIO pin
                one or more parameters can be selected which are shown as below:
      \arg        GPIO_PIN_x(x=0..15), GPIO_PIN_ALL
    \param[out] none
    \retval     none
*/
void gpio_af_set(uint32_t gpio_periph, uint32_t alt_func_num, uint32_t pin)
{
    uint16_t i;
    uint32_t afrl, afrh;

    afrl = GPIO_AFSEL0(gpio_periph);
    afrh = GPIO_AFSEL1(gpio_periph);

    for(i = 0U; i < 8U; i++) {
        if((1U << i) & pin) {
            /* clear the specified pin alternate function bits */
            afrl &= ~GPIO_AFR_MASK(i);
            afrl |= GPIO_AFR_SET(i, alt_func_num);
        }
    }

    for(i = 8U; i < 16U; i++) {
        if((1U << i) & pin) {
            /* clear the specified pin alternate function bits */
            afrh &= ~GPIO_AFR_MASK(i - 8U);
            afrh |= GPIO_AFR_SET(i - 8U, alt_func_num);
        }
    }

    GPIO_AFSEL0(gpio_periph) = afrl;
    GPIO_AFSEL1(gpio_periph) = afrh;
}
推荐阅读
关注数
1
文章数
4
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息