GD32VW553-IoT V2 利用 Wi-Fi 在OLED屏幕上播放视频
分享作者:Mr_Fang
作者昵称:Mr_Fang
评测品牌:萤火工场
评测型号:GD32VW553-IOT-V2
发布时间:2025-10-23 09:39:42
2 1
前言
开源口碑分享内容
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.h 中 CONFIG_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_tMCU 端源码
启动软 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
