GD32VW553-IoT V2 利用 Wi-Fi 在OLED屏幕上播放视频
分享作者:Mr_Fang
作者昵称:Mr_Fang
评测品牌:萤火工场
评测型号:GD32VW553-IOT-V2
发布时间:2025-10-23 09:39:42
1
前言
GD32VW553-IoT V2 利用 Wi-Fi 在OLED屏幕上播放视频
开源口碑分享内容

GD32VW553-IoT V2 利用 Wi-Fi 在OLED屏幕上播放视频

V2 相较 V1 的改动

最大的一处优化就是 V2 优化了程序下载的方式:板子上新增了 CH340 芯片,只需要拨动拨码开关即可进入 BOOT 模式下载程序,搭配 GD32 All In One Programmer 软件的 Jump to run the App program 功能可以更方便的进行程序调试 —— 只需要保持拨码开关不动,按下复位按钮即可进入下载模式,下载程序后将自动运行程序

另外,V1 饱受用户诟病的屏蔽壳焊盘也被去掉了,焊接排针时不用担心连锡短路,同时也在板子背面增加了引脚丝印,使用更加方便

修改 MSDK 的 platform_def.h

虽然 V2 板载了 CH340 芯片,可以直接进行 USB 转串口调试,但是串口使用了 PB15、PA8,所以需要修改 /config/platform_def.hCONFIG_BOARD 常量为 PLATFORM_BOARD_32VW55X_EVAL,修改后即可直接使用 USB 串口调试

移植 OLED 驱动

这里所使用的是来自中景园电子的 1.3 寸 7 针 SPI OLED 屏幕,可以直接使用 STM32F103C8T6 SPI 例程进行移植

首先修改初始化函数 OLED_Init

- GPIO_InitTypeDef  GPIO_InitStructure;
- RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_AFIO, ENABLE);
- GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);
- GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3|GPIO_Pin_4|GPIO_Pin_15;
- GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
- GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
- GPIO_Init(GPIOA, &GPIO_InitStructure);
- GPIO_SetBits(GPIOA,GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3|GPIO_Pin_4|GPIO_Pin_15);

+ rcu_periph_clock_enable(RCU_GPIOA);
+ rcu_periph_clock_enable(RCU_GPIOB);
+ (GPIOA, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, GPIO_PIN_10|GPIO_PIN_9|GPIO_PIN_12);
+ (GPIOB, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, GPIO_PIN_3|GPIO_PIN_4);

OLED_RES_Clr();
- delay_ms(200);
+ sys_us_delay(200000);
OLED_RES_Set();

然后修改 oled.h

+ #include "gd32vw55x.h"
#include "sys.h"
#include "stdlib.h"    

//-----------------OLED端口定义---------------- 

- #define OLED_SCL_Clr() GPIO_ResetBits(GPIOA,GPIO_Pin_0)//SCL
- #define OLED_SCL_Set() GPIO_SetBits(GPIOA,GPIO_Pin_0)
-
- #define OLED_SDA_Clr() GPIO_ResetBits(GPIOA,GPIO_Pin_1)//SDA
- #define OLED_SDA_Set() GPIO_SetBits(GPIOA,GPIO_Pin_1)
-
- #define OLED_RES_Clr() GPIO_ResetBits(GPIOA,GPIO_Pin_2)//RES
- #define OLED_RES_Set() GPIO_SetBits(GPIOA,GPIO_Pin_2)
-
- #define OLED_DC_Clr()  GPIO_ResetBits(GPIOA,GPIO_Pin_3)//DC
- #define OLED_DC_Set()  GPIO_SetBits(GPIOA,GPIO_Pin_3)
-
- #define OLED_CS_Clr()  GPIO_ResetBits(GPIOA,GPIO_Pin_4)//CS
- #define OLED_CS_Set()  GPIO_SetBits(GPIOA,GPIO_Pin_4)

+ #define OLED_SCL_Clr() gpio_bit_reset(GPIOA,GPIO_PIN_10)  //SCL
+ #define OLED_SCL_Set() gpio_bit_set(GPIOA,GPIO_PIN_10)
+
+ #define OLED_SDA_Clr() gpio_bit_reset(GPIOA,GPIO_PIN_9)  //SDA
+ #define OLED_SDA_Set() gpio_bit_set(GPIOA,GPIO_PIN_9)
+
+ #define OLED_RES_Clr() gpio_bit_reset(GPIOA,GPIO_PIN_12)   //RES
+ #define OLED_RES_Set() gpio_bit_set(GPIOA,GPIO_PIN_12)
+
+ #define OLED_DC_Clr()  gpio_bit_reset(GPIOB,GPIO_PIN_3)   //DC
+ #define OLED_DC_Set()  gpio_bit_set(GPIOB,GPIO_PIN_3)
+
+ #define OLED_CS_Clr()  gpio_bit_reset(GPIOB,GPIO_PIN_4)   //CS
+ #define OLED_CS_Set()  gpio_bit_set(GPIOB,GPIO_PIN_4)
+
+ #define u8  uint8_t
+ #define u16 uint16_t
+ #define u32 uint32_t

MCU 端源码

启动软 AP 并启动 http server 接收 POST 请求,将 POST 收到的数据显示在屏幕上 —— 程序运行后将启动一个软 AP(可以在屏幕上看到 SSID 和密码),可使用其他设备连接

这里规定传输协议为 协议头+二进制图片,协议头为四字节的图片大小和显示坐标 uint8_t[4] = {img_w, img_h, offset_x, offset_y}

#ifdef IFNAMSIZ
#undef IFNAMSIZ
#endif
#define IFNAMSIZ NETIF_NAMESIZE

#include <stdint.h>
#include <stdio.h>
#include "app_cfg.h"
#include "gd32vw55x_platform.h"
#include "wifi_management.h"
#include "wifi_init.h"

#include "lwip/sockets.h"
#include "lwip/sys.h"
#include "lwip/netdb.h"

#include "oled.h"
#include "bmp.h"

// Soft AP 相关
#define AP_SSID        "GD32_AP"
#define AP_PASSWORD    "GD32666666"
#define HTTP_PORT    80

// 解析收到的JSON,-1为错误
int get_json_num(char *json, char *key) {
    // 构造查找的键字符串,例如 "cmd":"
    char search_key[256];
    snprintf(search_key, sizeof(search_key), "\"%s\":", key);

    // 在 JSON 字符串中查找键
    char *key_start = strstr(json, search_key);
    if (key_start == NULL) {
        printf("JSON data does not contain '%s' field.\n", key);
        return -1; // 返回错误码
    }

    // 跳过键名和冒号,例如跳过 "cmd":"
    key_start += strlen(search_key);

    // 跳过可能存在的空格
    while (*key_start == ' ') {
        key_start++;
    }

    // 找到键值的结束位置(假设键值是数字,且后面紧跟逗号或右大括号)
    char *key_end = strchr(key_start, ',');
    if (key_end == NULL) {
        key_end = strchr(key_start, '}');
        if (key_end == NULL) {
            printf("Invalid JSON format.\n");
            return -1; // 返回错误码
        }
    }

    // 提取键值
    int value = 0;
    if (sscanf(key_start, "%d", &value) != 1) {
        printf("Failed to parse '%s' value.\n", key);
        return -1; // 返回错误码
    }

    return value; // 返回解析的整数值
}

int get_param(const char* input, const char* key, char* value) {
    char search_key[64] = {0};
    snprintf(search_key, sizeof(search_key), "%s=", key);

    const char* start = strstr(input, search_key);
    if (start != NULL) {
        start += strlen(search_key);  // 跳过 "key="

        const char* end = strchr(start, '&');
        if (end != NULL) {
            // 如果找到 & 分隔符,复制到分隔符位置
            size_t len = end - start;
            memcpy(value, start, len);
            value[len] = '\0';
        } else {
            // 如果没找到 &,说明是最后一个参数,直接复制到末尾
            strcpy(value, start);
        }
        return true;
    }
    return false;
}

static void http_server_netconn_serve(int conn_fd) {
    char buffer[1024];
    int len = read(conn_fd, buffer, sizeof(buffer) - 1);

    if (len > 0) {
        buffer[len] = '\0';
        // printf("Received request:\n%s\n", buffer);

        // 判断请求方法
        if (strncmp(buffer, "GET", 3) == 0) {
            // GET 请求
            const char *response =
                "HTTP/1.1 200 OK\r\n"
                "Content-Type: text/html\r\n"
                "Connection: close\r\n"
                "\r\n"
                "<html><head>"
                "<title>GD32Vw55x-IoT</title><meta name='viewport' content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no' />"
                "<style>body{margin:0;padding:20px;background-color:#f0f0f0;}h1{text-align:center;}"
                "form{margin:0 auto;background-color:#fff;padding: 20px;border-radius:5px;}"
                "div{margin-bottom:15px;}p{color:gray;font-size:10px;text-align:center;}"
                "input[type='text']{width:100%;padding:8px;border:1px solid #ddd;}"
                "input[type='submit']{width:100%;padding:10px;background-color:#d3d3d3;color:#fff;border:none;}"
                "input[type='submit']:hover{background-color: #808080;}</style></head>"
                "<body><h1>GD32Vw55x-IoT</h1>"
                "<p>Hello, world!</p>"
                "</body></html>";

            write(conn_fd, response, strlen(response));
            printf("HTTP response sent.\n");
        } else if (strncmp(buffer, "POST", 4) == 0) {
            // POST 请求
            char *content_type = strstr(buffer, "Content-Type: ");
            if (content_type != NULL) {
                content_type += 13; // 跳过 "Content-Type: "
                if (strstr(content_type, "application/octet-stream") != NULL) {
                    char *body = strstr(buffer, "\r\n\r\n");
                    if (body != NULL) {
                        body += 4;
                        uint8_t img[1024];
                        uint8_t w = body[0];
                        uint8_t h = body[1];
                        uint8_t x = body[2];
                        uint8_t y = body[3];
                        body += 4;

                        memcpy(img, body, 1016);

                        OLED_Clear();
                        OLED_ShowPicture(x, y, w, h, img, 1);
                        OLED_Refresh();

                        const char *response =
                            "HTTP/1.1 200 OK\r\n"
                            "Content-Type: application/json\r\n"
                            "Connection: close\r\n"
                            "\r\n"
                            "{\"err\": \"0\"}";

                        write(conn_fd, response, strlen(response));
                    }
                }
            }
        } else {
            // 其他请求方法,返回 405 Method Not Allowed
            const char *response =
                "HTTP/1.1 405 Method Not Allowed\r\n"
                "Content-Type: text/html\r\n"
                "Connection: close\r\n"
                "\r\n"
                "<html><head><title>405 Method Not Allowed</title></head>"
                "<body><h1>405 Method Not Allowed</h1></body></html>";

            write(conn_fd, response, strlen(response));
            printf("HTTP response sent.\n");
        }
    } else {
        printf("No data received.\n");
    }

    close(conn_fd);
    printf("Client connection closed.\n");
}

static void http_server_task(void *arg) {
    int listen_fd, conn_fd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_addr_len = sizeof(client_addr);

    // 创建 socket
    listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd < 0) {
        printf("Failed to create socket.\n");
        return;
    }

    printf("Socket created successfully.\n");

    // 配置 socket 地址
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(HTTP_PORT);
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);

    // 绑定 socket
    if (bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        printf("Failed to bind socket.\n");
        close(listen_fd);
        return;
    }

    printf("Socket bound to port %d.\n", HTTP_PORT);

    // 监听连接
    if (listen(listen_fd, 5) < 0) {
        printf("Failed to listen on socket.\n");
        close(listen_fd);
        return;
    }

    printf("HTTP server is listening on port %d\n", HTTP_PORT);

    while (1) {
        // 接受连接
        conn_fd = accept(listen_fd, (struct sockaddr *)&client_addr, &client_addr_len);
        if (conn_fd < 0) {
            printf("Failed to accept connection.\n");
            continue;
        }

        printf("New client connected. IP: %s\n", inet_ntoa(client_addr.sin_addr));

        // 处理 HTTP 请求
        http_server_netconn_serve(conn_fd);
    }
}

static void wifi_ap_task(void *param) {
    // 启动软 AP
    printf("Starting soft AP...\n");
    if (wifi_management_ap_start((char *)AP_SSID, (char *)AP_PASSWORD, 1, AUTH_MODE_WPA, 0) != 0) {
        printf("Failed to start soft AP.\n");
        goto exit;
    }

    printf("Soft AP started. SSID: %s, Password: %s\n", AP_SSID, AP_PASSWORD);

    // 启动 HTTP 服务器
    printf("Starting HTTP server...\n");
    http_server_task(NULL);

exit:
    printf("The ap task has ended.\n");
    sys_task_delete(NULL);
}

int main(void)
{
    platform_init();

    // 初始化 OLED
    OLED_Init();
    OLED_ColorTurn(0);
    OLED_DisplayTurn(0);

    OLED_ShowString(6, 6, (uint8_t *)"SSID:", 8, 1);
    OLED_ShowString(45, 6, (uint8_t *)AP_SSID, 8, 1);
    OLED_ShowString(6, 26, (uint8_t *)"PASS:", 8, 1);
    OLED_ShowString(45, 26, (uint8_t *)AP_PASSWORD, 8, 1);
    // OLED_ShowPicture(0, 0, 128, 64, (int8_t *)BMPt, 1);
    OLED_Refresh();


    if (wifi_init()) {
        printf("wifi init failed.\r\n");
    }

    sys_task_create_dynamic((const uint8_t *)"WiFi AP", 4096, OS_TASK_PRIORITY(0), wifi_ap_task, NULL);

    sys_os_start();
    for ( ; ; );
}

Python 上位机

Python 上位机负责对图片进行处理,处理后 POST 发送

import socket, time, sys, argparse, platform, subprocess
from PIL import Image
import numpy as np
from skimage import filters, morphology

HOST = "192.168.237.1"
PORT = 80
GIF_FILE = "7f7f601fd877889b.gif"
POST_URL = "/"

# 屏幕上限
MAX_W = 127
MAX_H = 64


def scale_keep_ratio(im: Image.Image) -> Image.Image:
    ow, oh = im.size
    ratio = min(MAX_W / ow, MAX_H / oh)
    return im.resize((int(ow * ratio), int(oh * ratio)), Image.LANCZOS)


def clean_binarize(im: Image.Image) -> Image.Image:
    im = im.convert('RGB')
    bg_samples = [
        np.array(im.crop((0, 0, 20, 20))),
        np.array(im.crop((im.width - 20, 0, im.width, 20))),
        np.array(im.crop((0, im.height - 20, 20, im.height))),
        np.array(im.crop((im.width - 20, im.height - 20, im.width, im.height)))
    ]
    bg_color = np.mean(bg_samples, axis=(0, 1, 2))  # RGB 背景均值

    img_arr = np.array(im)
    diff = np.linalg.norm(img_arr - bg_color, axis=2)
    fg_mask = diff > 15  # 阈值可微调

    fg_gray = np.array(im.convert('L')) * fg_mask
    otsu = filters.threshold_otsu(fg_gray[fg_mask > 0])
    binary = (fg_gray > otsu) & fg_mask

    binary = morphology.remove_small_objects(binary, min_size=64)

    return Image.fromarray(binary.astype(np.uint8) * 255).convert('1')


def pack_real_size(bmp: Image.Image) -> tuple[bytes, int, int]:
    w, h = bmp.size
    cols = w
    rows = (h + 7) // 8  # 每8行一组,向上取整
    buf = bytearray(cols * rows)  # 总字节数
    for c in range(cols):  # 逐列
        for r8 in range(rows):  # 每8行一组
            byte = 0
            for dy in range(8):
                y = r8 * 8 + dy
                px = bmp.getpixel((c, y)) if y < h else 0
                byte = (byte >> 1) | ((1 if px else 0) << 7)
            buf[r8 * cols + c] = byte
    return bytes(buf), w, h


def process_one_frame(im: Image.Image) -> tuple[bytes, int, int, int, int]:
    im = im.convert('RGB')
    scaled = scale_keep_ratio(im)
    sc_w, sc_h = scaled.size
    # bmp = scaled.convert('L').point(lambda p: 1 if p < 128 else 0, mode='1')
    bmp = clean_binarize(scaled)
    packed, _, _ = pack_real_size(bmp)
    off_x = (MAX_W - sc_w) // 2
    off_y = (MAX_H - sc_h) // 2
    header = bytes([sc_w, sc_h, off_x, off_y])
    return header + packed, sc_w, sc_h, off_x, off_y


def gif_frames_to_bytes(path: str):
    with Image.open(path) as im:
        for idx in range(im.n_frames):
            im.seek(idx)
            im.load()
            yield process_one_frame(im)[0]


def send_frame(pkt: bytes) -> bytes:
    req = (f"POST {POST_URL} HTTP/1.1\r\n"
           f"Host: {HOST}\r\n"
           "Content-Type: application/octet-stream\r\n"
           f"Content-Length: {len(pkt)}\r\n"
           "Connection: close\r\n\r\n").encode() + pkt
    with socket.create_connection((HOST, PORT), timeout=5) as sock:
        sock.sendall(req)
        return sock.recv(4096)


def main():
    ap = argparse.ArgumentParser(description="任意图→等比缩放→打包→4字节头+数据→POST")
    ap.add_argument("--dump", action="store_true", help="显示第一帧信息并保存预览图")
    args = ap.parse_args()

    with Image.open(GIF_FILE) as im:
        packed, sc_w, sc_h, off_x, off_y = process_one_frame(im)

    if args.dump:
        print("协议头(宽,高,x,y):", [sc_w, sc_h, off_x, off_y])
        print("数据长度:", len(packed) - 4)
        png = "dump_real_size.png"
        Image.open(GIF_FILE).convert('RGB').resize((sc_w, sc_h), Image.LANCZOS).save(png)
        print(f"预览图已保存:{png}")
        if platform.system() == "Windows":
            subprocess.run(["start", "", png], shell=True, check=False)
        elif platform.system() == "Darwin":
            subprocess.run(["open", png], check=False)
        else:
            subprocess.run(["xdg-open", png], check=False)
        return

    frames = list(gif_frames_to_bytes(GIF_FILE))
    print(f"共 {len(frames)} 帧,开始 POST …")
    for i, pkt in enumerate(frames, 1):
        print(f"[{i}/{len(frames)}] 发送 {len(pkt)} 字节", end="")
        try:
            resp = send_frame(pkt)
            print(f"  返回 {len(resp)} 字节")
        except Exception as e:
            print(f"  失败:{e}")
        if i != len(frames):
            time.sleep(0.1)
    print("完成!")


if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        sys.exit("用户中断")

效果图

全部评论
暂无评论
0/144