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

[复制链接]
155|1
本文转载自iCEasy商城口碑评测作者:Mr_Fang
欢迎大家戳链接GD32VW553-IoT V2 利用 Wi-Fi 在OLED屏幕上播放视频_口碑测评-iCEasy商城与原贴作者互动交流哦~
视频链接
https://www.bilibili.com/opus/1126464544781107235
前言
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.h 中 CONFIG_BOARD 常量为 PLATFORM_BOARD_32VW55X_EVAL,修改后即可直接使用 USB 串口调试
移植 OLED 驱动
这里所使用的是来自中景园电子的 1.3 寸 7 针 SPI OLED 屏幕,可以直接使用 STM32F103C8T6 SPI 例程进行移植
首先修改初始化函数 OLED_Init:
  1. - GPIO_InitTypeDef GPIO_InitStructure;
  2. - RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_AFIO, ENABLE);
  3. - GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);
  4. - GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3|GPIO_Pin_4|GPIO_Pin_15;
  5. - GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
  6. - GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  7. - GPIO_Init(GPIOA, &GPIO_InitStructure);
  8. - GPIO_SetBits(GPIOA,GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3|GPIO_Pin_4|GPIO_Pin_15);

  9. + rcu_periph_clock_enable(RCU_GPIOA);
  10. + rcu_periph_clock_enable(RCU_GPIOB);
  11. + (GPIOA, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, GPIO_PIN_10|GPIO_PIN_9|GPIO_PIN_12);
  12. + (GPIOB, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, GPIO_PIN_3|GPIO_PIN_4);

  13. OLED_RES_Clr();
  14. - delay_ms(200);
  15. + sys_us_delay(200000);
  16. OLED_RES_Set();
然后修改 oled.h:
  1. + #include "gd32vw55x.h"
  2. #include "sys.h"
  3. #include "stdlib.h"

  4. //-----------------OLED端口定义----------------

  5. - #define OLED_SCL_Clr() GPIO_ResetBits(GPIOA,GPIO_Pin_0)//SCL
  6. - #define OLED_SCL_Set() GPIO_SetBits(GPIOA,GPIO_Pin_0)
  7. -
  8. - #define OLED_SDA_Clr() GPIO_ResetBits(GPIOA,GPIO_Pin_1)//SDA
  9. - #define OLED_SDA_Set() GPIO_SetBits(GPIOA,GPIO_Pin_1)
  10. -
  11. - #define OLED_RES_Clr() GPIO_ResetBits(GPIOA,GPIO_Pin_2)//RES
  12. - #define OLED_RES_Set() GPIO_SetBits(GPIOA,GPIO_Pin_2)
  13. -
  14. - #define OLED_DC_Clr() GPIO_ResetBits(GPIOA,GPIO_Pin_3)//DC
  15. - #define OLED_DC_Set() GPIO_SetBits(GPIOA,GPIO_Pin_3)
  16. -
  17. - #define OLED_CS_Clr() GPIO_ResetBits(GPIOA,GPIO_Pin_4)//CS
  18. - #define OLED_CS_Set() GPIO_SetBits(GPIOA,GPIO_Pin_4)

  19. + #define OLED_SCL_Clr() gpio_bit_reset(GPIOA,GPIO_PIN_10) //SCL
  20. + #define OLED_SCL_Set() gpio_bit_set(GPIOA,GPIO_PIN_10)
  21. +
  22. + #define OLED_SDA_Clr() gpio_bit_reset(GPIOA,GPIO_PIN_9) //SDA
  23. + #define OLED_SDA_Set() gpio_bit_set(GPIOA,GPIO_PIN_9)
  24. +
  25. + #define OLED_RES_Clr() gpio_bit_reset(GPIOA,GPIO_PIN_12) //RES
  26. + #define OLED_RES_Set() gpio_bit_set(GPIOA,GPIO_PIN_12)
  27. +
  28. + #define OLED_DC_Clr() gpio_bit_reset(GPIOB,GPIO_PIN_3) //DC
  29. + #define OLED_DC_Set() gpio_bit_set(GPIOB,GPIO_PIN_3)
  30. +
  31. + #define OLED_CS_Clr() gpio_bit_reset(GPIOB,GPIO_PIN_4) //CS
  32. + #define OLED_CS_Set() gpio_bit_set(GPIOB,GPIO_PIN_4)
  33. +
  34. + #define u8 uint8_t
  35. + #define u16 uint16_t
  36. + #define u32 uint32_t
MCU 端源码
启动软 AP 并启动 http server 接收 POST 请求,将 POST 收到的数据显示在屏幕上 —— 程序运行后将启动一个软 AP(可以在屏幕上看到 SSID 和密码),可使用其他设备连接
这里规定传输协议为 协议头+二进制图片,协议头为四字节的图片大小和显示坐标 uint8_t[4] = {img_w, img_h, offset_x, offset_y}
  1. #ifdef IFNAMSIZ
  2. #undef IFNAMSIZ
  3. #endif
  4. #define IFNAMSIZ NETIF_NAMESIZE

  5. #include <stdint.h>
  6. #include <stdio.h>
  7. #include "app_cfg.h"
  8. #include "gd32vw55x_platform.h"
  9. #include "wifi_management.h"
  10. #include "wifi_init.h"

  11. #include "lwip/sockets.h"
  12. #include "lwip/sys.h"
  13. #include "lwip/netdb.h"

  14. #include "oled.h"
  15. #include "bmp.h"

  16. // Soft AP 相关
  17. #define AP_SSID "GD32_AP"
  18. #define AP_PASSWORD "GD32666666"
  19. #define HTTP_PORT 80

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

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

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

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

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

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

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

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

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

  60. const char* end = strchr(start, '&');
  61. if (end != NULL) {
  62. // 如果找到 & 分隔符,复制到分隔符位置
  63. size_t len = end - start;
  64. memcpy(value, start, len);
  65. value[len] = '\0';
  66. } else {
  67. // 如果没找到 &,说明是最后一个参数,直接复制到末尾
  68. strcpy(value, start);
  69. }
  70. return true;
  71. }
  72. return false;
  73. }

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

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

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

  99. write(conn_fd, response, strlen(response));
  100. printf("HTTP response sent.\n");
  101. } else if (strncmp(buffer, "POST", 4) == 0) {
  102. // POST 请求
  103. char *content_type = strstr(buffer, "Content-Type: ");
  104. if (content_type != NULL) {
  105. content_type += 13; // 跳过 "Content-Type: "
  106. if (strstr(content_type, "application/octet-stream") != NULL) {
  107. char *body = strstr(buffer, "\r\n\r\n");
  108. if (body != NULL) {
  109. body += 4;
  110. uint8_t img[1024];
  111. uint8_t w = body[0];
  112. uint8_t h = body[1];
  113. uint8_t x = body[2];
  114. uint8_t y = body[3];
  115. body += 4;

  116. memcpy(img, body, 1016);

  117. OLED_Clear();
  118. OLED_ShowPicture(x, y, w, h, img, 1);
  119. OLED_Refresh();

  120. const char *response =
  121. "HTTP/1.1 200 OK\r\n"
  122. "Content-Type: application/json\r\n"
  123. "Connection: close\r\n"
  124. "\r\n"
  125. "{"err": "0"}";

  126. write(conn_fd, response, strlen(response));
  127. }
  128. }
  129. }
  130. } else {
  131. // 其他请求方法,返回 405 Method Not Allowed
  132. const char *response =
  133. "HTTP/1.1 405 Method Not Allowed\r\n"
  134. "Content-Type: text/html\r\n"
  135. "Connection: close\r\n"
  136. "\r\n"
  137. "<html><head><title>405 Method Not Allowed</title></head>"
  138. "<body><h1>405 Method Not Allowed</h1></body></html>";

  139. write(conn_fd, response, strlen(response));
  140. printf("HTTP response sent.\n");
  141. }
  142. } else {
  143. printf("No data received.\n");
  144. }

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

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

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

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

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

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

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

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

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

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

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

  185. // 处理 HTTP 请求
  186. http_server_netconn_serve(conn_fd);
  187. }
  188. }

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

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

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

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

  204. int main(void)
  205. {
  206. platform_init();

  207. // 初始化 OLED
  208. OLED_Init();
  209. OLED_ColorTurn(0);
  210. OLED_DisplayTurn(0);

  211. OLED_ShowString(6, 6, (uint8_t *)"SSID:", 8, 1);
  212. OLED_ShowString(45, 6, (uint8_t *)AP_SSID, 8, 1);
  213. OLED_ShowString(6, 26, (uint8_t *)"PASS:", 8, 1);
  214. OLED_ShowString(45, 26, (uint8_t *)AP_PASSWORD, 8, 1);
  215. // OLED_ShowPicture(0, 0, 128, 64, (int8_t *)BMPt, 1);
  216. OLED_Refresh();


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

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

  221. sys_os_start();
  222. for ( ; ; );
  223. }
Python 上位机
Python 上位机负责对图片进行处理,处理后 POST 发送
  1. import socket, time, sys, argparse, platform, subprocess
  2. from PIL import Image
  3. import numpy as np
  4. from skimage import filters, morphology

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

  9. # 屏幕上限
  10. MAX_W = 127
  11. MAX_H = 64


  12. def scale_keep_ratio(im: Image.Image) -&gt; Image.Image:
  13. ow, oh = im.size
  14. ratio = min(MAX_W / ow, MAX_H / oh)
  15. return im.resize((int(ow * ratio), int(oh * ratio)), Image.LANCZOS)


  16. def clean_binarize(im: Image.Image) -&gt; Image.Image:
  17. im = im.convert('RGB')
  18. bg_samples = [
  19. np.array(im.crop((0, 0, 20, 20))),
  20. np.array(im.crop((im.width - 20, 0, im.width, 20))),
  21. np.array(im.crop((0, im.height - 20, 20, im.height))),
  22. np.array(im.crop((im.width - 20, im.height - 20, im.width, im.height)))
  23. ]
  24. bg_color = np.mean(bg_samples, axis=(0, 1, 2)) # RGB 背景均值

  25. img_arr = np.array(im)
  26. diff = np.linalg.norm(img_arr - bg_color, axis=2)
  27. fg_mask = diff &gt; 15 # 阈值可微调

  28. fg_gray = np.array(im.convert('L')) * fg_mask
  29. otsu = filters.threshold_otsu(fg_gray[fg_mask &gt; 0])
  30. binary = (fg_gray &gt; otsu) &amp; fg_mask

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

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


  33. def pack_real_size(bmp: Image.Image) -&gt; tuple[bytes, int, int]:
  34. w, h = bmp.size
  35. cols = w
  36. rows = (h + 7) // 8 # 每8行一组,向上取整
  37. buf = bytearray(cols * rows) # 总字节数
  38. for c in range(cols): # 逐列
  39. for r8 in range(rows): # 每8行一组
  40. byte = 0
  41. for dy in range(8):
  42. y = r8 * 8 + dy
  43. px = bmp.getpixel((c, y)) if y &lt; h else 0
  44. byte = (byte &gt;&gt; 1) | ((1 if px else 0) &lt;&lt; 7)
  45. buf[r8 * cols + c] = byte
  46. return bytes(buf), w, h


  47. def process_one_frame(im: Image.Image) -&gt; tuple[bytes, int, int, int, int]:
  48. im = im.convert('RGB')
  49. scaled = scale_keep_ratio(im)
  50. sc_w, sc_h = scaled.size
  51. # bmp = scaled.convert('L').point(lambda p: 1 if p &lt; 128 else 0, mode='1')
  52. bmp = clean_binarize(scaled)
  53. packed, _, _ = pack_real_size(bmp)
  54. off_x = (MAX_W - sc_w) // 2
  55. off_y = (MAX_H - sc_h) // 2
  56. header = bytes([sc_w, sc_h, off_x, off_y])
  57. return header + packed, sc_w, sc_h, off_x, off_y


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


  64. def send_frame(pkt: bytes) -&gt; bytes:
  65. req = (f"POST {POST_URL} HTTP/1.1\r\n"
  66. f"Host: {HOST}\r\n"
  67. "Content-Type: application/octet-stream\r\n"
  68. f"Content-Length: {len(pkt)}\r\n"
  69. "Connection: close\r\n\r\n").encode() + pkt
  70. with socket.create_connection((HOST, PORT), timeout=5) as sock:
  71. sock.sendall(req)
  72. return sock.recv(4096)


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

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

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

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


  104. if __name__ == "__main__":
  105. try:
  106. main()
  107. except KeyboardInterrupt:
  108. sys.exit("用户中断")






cr315 发表于 2025-10-30 12:48 | 显示全部楼层
是否支持硬件解码?
您需要登录后才可以回帖 登录 | 注册

本版积分规则

13

主题

16

帖子

0

粉丝
快速回复 在线客服 返回列表 返回顶部