# PIKA - WIRELESS 开发板 我们骄傲地推出PIKA 派 - WIRELESS开发板,这是PikaPython官方首次推出的全流程、手把手教程的开发板。 在这个信息爆炸的新时代,嵌入式编程与Python结合,创造出了前所未有的应用可能性。而PikaPython,作为嵌入式Python的领军力量,成为了许多创新项目的核心动力。 为了培养与帮助下一代的开发者掌握这一创新技术,我们荣誉推出专为技术爱好者与新世代研发者设计的“PIKA 派 - WIRELESS”开发学习板。 “PIKA 派 - WIRELESS”的核心采用了业界知名、学习友好的ESP32S3。该芯片性能卓越、功能多样,绝对能满足您对嵌入式Python的所有探索与实验需求。这块开发板集成了大量的硬件资源(8M的Flash、WIFI与4G通信、众多传感器),并且提供了许多扩展接口,为您在各种项目中展翅翱翔创造了条件。配合此开发学习板,您将得以深度体验PikaPython的魅力,锻炼自己的编程能力,并为未来的技术挑战做好万全的准备。 ![img](assets/O1C-1712804613733-3.png) **🛠 技术参数一览:** - 主控:ESP32S3 - 存储:FLASH 8M, PSRAM 8M, nand FLASH 128MB - 通信:WIFI、4G、RS485 接口 - 显示:LCD 屏幕 - 传感器:板载温湿度传感器、光敏电阻 - 其他:蜂鸣器、继电器、12V供电、WS2812 此款开发板完整覆盖了内核移植、驱动适配、模块开发、事件驱动、应用开发到最佳实践的全流程。它旨在帮助开发者快速并深入地学习PikaPython在项目实战中的各种技术,并迅速掌握实战开发能力。 📚 **课程亮点:** 1. PikaPython基础移植与脚本启动模式深入讲解 2. ESP32实战系列,包括驱动适配、事件驱动等核心技术 3. 深入嵌入式驱动框架设计与应用 4. 嵌入式编程思想与最佳实践 ![image-20230909174529904](assets/image-20230909174529904-1712804613733-4.png) 我们精心准备的课程不仅确保您能在此开发板上掌握PikaPython的核心技能,还设计了许多案例和实战,确保您可以举一反三,轻松拓展到任何新的平台上。 探索、学习,然后再创新。这不仅是我们的信念,更是我们希望与大家共同实现的目标。期待与您共同探索PikaPython的无限可能!💡 购买链接:https://m.tb.cn/h.5eu8O7B?tk=AgpWdB6P4Op 下面是程住气大佬结合开发板和课程的内容编写的课程笔记,非常值得一读! ## CMAKE 内核编译和应用 教程中的 makefile 和 CMake 工程可以在 Win 上运行,这样可以快速进行测试和进行仿真运行,后面可以移植到你的实际硬件平台上(例如 STM32、ESP32等)。 ### 官网 https://pikapython.com/ ### 先下载包管理器:用于拉取pikapython的源码 ``` http://pikapython.com/doc//%E5%8C%85%E7%AE%A1%E7%90%86%E5%99%A8%E4%B8%8E%E6%A8%A1%E5%9D%97%E7%AE%A1%E7%90%86.html ``` ![image](assets/image-1712804613733-16.png) ### 拉取内核源码 新建记事本requestment.txt,输入下面内容。 (不过即使没有这步,包管理器也会自动新建requestment.txt) ```pikascript-core==latest pikascript-core PikaStdLib ``` 其他可用版本查看:https://gitee.com/Lyon1998/pikascript/blob/master/packages.toml 然后执行包管理器,进行拉取代码。 运行预编译rust-msc-latest-win10.exe,它会将python文件预编译到pikascript-api中的C文件中去 ![image2](assets/image2-1712804613733-5.png) ### 编写应用主程序 新建main.c文件,代码如下: ``` c #include #include"pikascript.h" int main(int argc,char **argv) { printf("hello world\r\n"); //固件启动,需要编译 PikaObj* pikaMain = pikaScriptInit(); } ``` ### 内核编译 官方视频是用 visual studio 来做的,这个软件太大了,家里网速慢,同时电脑有现成的 vscode,所以将分别采用 WIN 环境下 makefile 和 CMAKE 来编译。(其实是后续的 ESP32 视频,编译用的是 CMAKE,看不懂所以回头学了 CMAKE + makefile) 下载和安装MinGW32 下载地址: https://www.onlinedown.net/soft/10056269.htm 解压后,添加环境变量后: ![95b7f3d6abb74a7212c67620e8db5aa](assets/95b7f3d6abb74a7212c67620e8db5aa-1712804613733-6.png) ![img](assets/1689835725547-6176253f-831c-4cdc-9e94-3f6e976bb324-1712804613733-7.png) ![image-20231202162719952](assets/image-20231202162719952-1712804613733-8.png) makefile: 1.新建makefile文件,代码如下 2.执行make ``` TAR = pikapython # 添加头文件搜索路径 INC = -I ./ -I ./pikascript-core -I ./pikascript-api -I ./pikascript-lib/PikaStdLib # 寻找源文件.c SOURCES = $(wildcard *.c ./pikascript-api/*.c ./pikascript-core/*.c ./pikascript-lib/PikaStdLib/*.c) OBJ = $(patsubst %.c,%.o,$(SOURCES)) # test: # @echo $(SOURCES) $(TAR):$(OBJ) gcc $^ -o $@ $(INC) %.o:%.c gcc -c $^ -o $@ $(INC) clean: rm -rf *.o ./**/*.o ./**/**/*.o $(TAR) ``` ![](assets/image-20231202154030572-1712804613733-9.png) CMAKE: CMAKE环境搭建:下载地址:https://cmake.org/download/ ![img](assets/1701420793546-6017e949-7941-4fff-b097-a2531429714f-1712804613733-10.png) ![img](assets/1701421800201-df27a972-9520-4a6a-80f7-af0c1d4bdeee-1712804613733-11.png) ![img](assets/1701422428690-142dcc8a-fe85-4a65-ac59-9ac565d36c4f-1712804613733-12.png) ![img](assets/1701424914576-2826ade5-f09a-4eda-954b-3b6fd2fa4d4a-1712804613733-13.png) ![img](assets/1701426224068-c8fa8c2d-1671-4497-9241-2f2de0a2ec9a-1712804613733-14.png) CMAKE开始: 1.新建CMakeLists.txt文件,代码如下. 2.新建build文件夹,然后进入build文件, 3.cmake .. 4.make (2.3的步骤是为了简洁工程,cmake会生成一堆中间文件,这样中间文件全部在build文件夹里了) ``` cmake # 指定使用的 cmake 的最低版本,可选,非必须,如果不加可能会有警告 cmake_minimum_required(VERSION 3.10) # 生成工程名字 project(pikapython) # GLOB查找.c文件 file(GLOB API_SRC ${CMAKE_CURRENT_SOURCE_DIR}/pikascript-api/*.c) file(GLOB CORE_SRC ${CMAKE_CURRENT_SOURCE_DIR}/pikascript-core/*.c) file(GLOB LIB_SRC ${CMAKE_CURRENT_SOURCE_DIR}/pikascript-lib/PikaStdLib/*.c) # 将所有的源文件组合在一起 set(SOURCE_FILES ${API_SRC} ${CORE_SRC} ${LIB_SRC} main.c) # message("所有的.c文件:",${SOURCE_FILES}) # 添加头文件搜索路径 include_directories(${CMAKE_SOURCE_DIR}/pikascript-core) include_directories(${CMAKE_SOURCE_DIR}/pikascript-api) include_directories(${CMAKE_SOURCE_DIR}/pikascript-lib/PikaStdLib) # 定义工程会生成一个可执行程序 add_executable(pikapython ${SOURCE_FILES}) ``` ![image-20231202155152260](assets/image-20231202155031717-1712804613733-15.png) ![image-20231202155056347](assets/image-20231202155056347-1712804613733-18.png) ### 应用入门 修改main.py文件,加一句print('hello world!') 运行预编译rust-msc-latest-win10.exe 编译内核makefile/cmake 执行 ![](assets/image-20231202160754059-1712804613733-17.png) ## ESP32 入门体验 ### 安装 #### 官方入门指南:[入门指南](https://docs.espressif.com/projects/esp-idf/zh_CN/v5.1/esp32s3/index.html) #### 下载地址:[ESP-IDF下载地址](https://dl.espressif.cn/dl/esp-idf/?idf=5.0) ![image-20240215152317535](assets/image-20240215152317535-1712804613733-19.png) 这里选择 v5.1 版本,**【一定一定一定】** 要选 **v5.1**,v5.1.1不行!,v5.1.2也不行,后面会导致奇怪的问题!!! (后面的截图中可能出现了 v5.1.2,这是当初使用 v5.1.2 的时候的截图,v5.1.2 已经掉坑里了!换回 v5.1 了! **一切以 v5.1 为准**!) 下载完,一路next就行 ![2](assets/2-1707981481855-3-1712804613733-21.png) ### 新建工程、编译、下载 ``` bash # 打开esp-idf,切目录(这个目录后续用于插件,创建使用模板) cd esample/get-start # 新建工程 idf.py create-project hello # 设置芯片 idf.py set-target esp32s3 # 编写代码hello.c #include void app_main(void) { printf("hello world,god bless you"); } # 编译 idf.py build # 下载代码并开启监听(串口的查看,参看下图) idf.py flash -p COM8 monitor # 退出监听,键盘快捷键 ctrl+] ``` ![3](assets/3-1707981504224-5-1712804613733-20.png) ![4](assets/4-1707981508873-7-1712804613733-22.png) ### vscode插件 之前的操作都是使用命令行,使用vscode+esp32插件,快乐加倍 ### vscode插件复制 ![5](assets/5-1707981515028-9-1712804613733-23.png) 打开命令面板:ctrl+shift+p ![6](assets/6-1707981519105-11-1712804613733-24.png) ![7](assets/7-1707981523596-13-1712804613733-25.png) ![8](assets/8-1707981527000-15-1712804613733-26.png) ![9](assets/9-1707981530028-17-1712804613733-27.png) ### 点灯 插件新建工程 ![10](assets/10-1707981537772-19-1712804613733-28.png) ![11](assets/11-1707981541472-21-1712804613733-29.png) ![12](assets/12-1707981545717-23-1712804613733-30.png) 原理图 ![13](assets/13-1707981549346-25-1712804613733-31.png) 插件按钮 ![14](assets/14-1707981552298-27-1712804613733-32.png) ``` c #include #include "driver/gpio.h" #include "FreeRTOS/freertos.h" #include "FreeRTOS/task.h" #define LED2_PIN 4 #define LED1_PIN 7 void led_init(int index) { gpio_config_t led_pin_config; led_pin_config.pin_bit_mask = 1< #include "pikascript.h" void app_main(void) { printf("aaaa"); PikaObj* pikaMain = pikaScriptInit(); } ``` ![2](assets/2-1707981702939-35-1712804613733-36.png) ### 心得 移植这个还是挺简单的,跟pikapython内核编译差不多。 主要需要熟悉gcc、CMake、freertos、esp32开发的知识,不然真的无法下手。 如果对上面不是很了解,建议先熟悉上面的知识点,磨刀不误砍柴工。 ## 串口通信 ### 通过串口进行交互,先熟悉下ESP32的串口代码。下面esp32s3的串口通信代码,实验结果为uart回传。 ![](assets/1-1712804613733-37.png) ``` 串口通信代码 #include #include "sdkconfig.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "esp_system.h" #include "esp_spi_flash.h" #include "string.h" #include "driver/usb_serial_jtag.h" void app_main(void) { printf("uart test/r/n"); usb_serial_jtag_driver_config_t usb_cdc = { .rx_buffer_size = 65, //环形buf应大于64 .tx_buffer_size = 65 //环形buf应大于64 }; ESP_ERROR_CHECK(usb_serial_jtag_driver_install(&usb_cdc)); //安装usb_serial驱动 // usb_serial_jtag_driver_uninstall(); //卸载usb_serial驱动 while(1) { char buf[1] = {0}; if(usb_serial_jtag_read_bytes(buf, 1, portMAX_DELAY) >0) //阻塞当前任务,直到接收数据 { usb_serial_jtag_write_bytes(buf, 1, portMAX_DELAY); } vTaskDelay(10/portTICK_PERIOD_MS); } } ``` ## 交互运行 ### 交互式运行环境 - 为了实现在目标平台上的交互式运行,我们需要提供getchar、putchar接口来重载。 getchar从标准输入字符,putchar从标准输出字符,查看pika平台的代码。 - PIKA_WEAK 就是弱函数,弱函数可以应用到几个模块之间的交互接口,在项目中并行开发多个存在交互模块时,本模块代码需要使用到外部模块接口,但其他模块并未开发完成,此处可以使用弱函数解决,编译链接报错问题。 ![](assets/2-1712804613733-38.png) ![](assets/3-1712804613733-39.png) ![](assets/4-1712804613733-40.png) ``` //添加重载函数 char pika_platform_getchar() { while (1) { char buff[1] = {0}; if (usb_serial_jtag_read_bytes(buff, 1,portMAX_DELAY) > 0) { return buff[0]; } return buff[1]; } } int pika_platform_putchar(char ch) { usb_serial_jtag_write_bytes(&ch, 1, portMAX_DELAY); return 0; } ``` ``` //在主函数中,增加shell代码 //必须使用宏PRIu32,否则putchar会卡死,系统不工作。目前还没找到原因,后续有空再来查查 uint32_t a =5; printf("Minimum free heap size: %" PRIu32 " bytes\n",a); PikaObj* pikaMain = pikaScriptInit(); pikaScriptShell(pikaMain); ``` ![](assets/5-1712804613733-41.png) ### 移植好的shell代码 [增加shell的代码gitee地址](https://gitee.com/codercmd/esp32_pikapython_code/tree/shell) ## 文件系统 ### 文件系统适配 文件系统的适配使 PikaPython 能够从文件加载脚本,进行存储、检索和执行操作。 这可能包括对FLASH存储或SD卡等外部存储介质的支持,以及实现或利用现有文件系统 API。 常见文件系统:选择一个常用文件系统,来提供文件系统的适配,如 fatfs、LittleFS等。 ### 原理图 NAND FLASH ![](assets/imagasde-1712804613733-42.png) ### 新建sd_fatfs.c sd_fatfs.h文件、修改CMAKE ```c //sd_fatfs.c #include #include #include #include #include #include #include "driver/gpio.h" #include "driver/sdspi_host.h" #include "driver/uart.h" #include "esp_event.h" #include "esp_log.h" #include "esp_system.h" #include "esp_vfs_fat.h" #include "freertos/FreeRTOS.h" #include "freertos/queue.h" #include "freertos/task.h" #include "hal/spi_types.h" #include "sdmmc_cmd.h" #include "esp_http_client.h" #include "esp_https_ota.h" #include "esp_ota_ops.h" #include "esp_wifi.h" #include "nvs.h" #include "nvs_flash.h" #include "pikaScript.h" #include "sd_fatfs.h" static sdmmc_card_t* card; static bool is_sd_mounted = false; int sd_fatfs_init(void) { esp_err_t ret; const char mount_point[] = MOUNT_POINT; esp_vfs_fat_sdmmc_mount_config_t mount_config = { .format_if_mount_failed = true, .max_files = 5, .allocation_unit_size = 16 * 1024, }; // Use settings defined above to initialize SD card and mount FAT // filesystem. Note: esp_vfs_fat_sdmmc/sdspi_mount is all-in-one convenience // functions. Please check its source code and implement error recovery when // developing production applications. sdmmc_host_t host = SDSPI_HOST_DEFAULT(); host.slot = SPI3_HOST; spi_bus_config_t bus_cfg = { .mosi_io_num = SDSPI_MOSI, .miso_io_num = SDSPI_MISO, .sclk_io_num = SDSPI_CLK, .quadwp_io_num = -1, .quadhd_io_num = -1, .max_transfer_sz = 4000, }; ret = spi_bus_initialize(host.slot, &bus_cfg, SDSPI_DEFAULT_DMA); if (ret != ESP_OK && ret != ESP_ERR_INVALID_STATE) { printf("Failed to initialize SPI bus.\r\n"); return -1; } // This initializes the slot without card detect (CD) and write protect (WP) // signals. Modify slot_config.gpio_cd and slot_config.gpio_wp if your board // has these signals. sdspi_device_config_t slot_config = SDSPI_DEVICE_CONFIG_DEFAULT(); slot_config.gpio_cs = SDSPI_CS; slot_config.host_id = host.slot; printf("Mounting filesystem...\r\n"); ret = esp_vfs_fat_sdspi_mount(mount_point, &host, &slot_config, &mount_config, &card); if (ret != ESP_OK) { if (ret == ESP_FAIL) { printf( "Failed to mount filesystem. " "If you want the card to be formatted, set the " "CONFIG_EXAMPLE_FORMAT_IF_MOUNT_FAILED menuconfig option.\r\n"); } else { printf( "Failed to initialize the card (%s). " "Make sure SD card lines have pull-up resistors in place.\r\n", esp_err_to_name(ret)); } return -1; } else { printf("Filesystem mounted\r\n"); is_sd_mounted = true; sdmmc_card_print_info(stdout, card); } return 0; } ``` ```c //sd_fatfs.h #ifndef SD_FATFS_H #define SD_FATFS_H #define MOUNT_POINT "/sd" #define SDSPI_CS (GPIO_NUM_47) #define SDSPI_CLK (GPIO_NUM_21) #define SDSPI_MISO (GPIO_NUM_14) #define SDSPI_MOSI (GPIO_NUM_13) int sd_fatfs_init(void); #endif ``` ### 在main.c主函数中,初始化文件和添加操作文件的重载函数 ```c //main.c文件 主函数添加 sd_fatfs_init(); ``` ```c //main.c文件,添加文件重载函数 FILE *pika_platform_fopen(const char *filename, const char *modes) { return fopen(filename, modes); } int pika_platform_fclose(FILE *fp) { return fclose(fp); } int pika_platform_fseek(FILE *fp, long offset, int whence) { return fseek(fp, offset, whence); } long pika_platform_ftell(FILE *fp) { return ftell(fp); } size_t pika_platform_fread(void *ptr, size_t size, size_t count, FILE *fp) { return fread(ptr, size, count, fp); } size_t pika_platform_fwrite(const void *ptr, size_t size, size_t count, FILE *fp) { return fwrite(ptr, size, count, fp); } ``` ### 在cmake上需要添加编译 SRCS "sd_fatfs.c" ```c file(GLOB_RECURSE SOURCES *.c) idf_component_register(SRCS "hello.c" SRCS "sd_fatfs.c" INCLUDE_DIRS ".") idf_build_set_property( COMPILE_DEFINITIONS "-DPIKA_CONFIG_ENABLE" APPEND) ``` ![](assets/image-1-1712804613733-45.png) ## os模块引入 os 模块提供了非常丰富的方法用来处理文件和目录,举个栗子: ```c import os # 路径的拼接 folder_path = os.path.join("file","user") print(folder_path) # file\user # 查看sd文件夹里的文件 os.listdir('/sd') ``` ### 增加os模块 在requestment.txt增加os,增加完还需要运行pikaPackage.exe包来进行下载,然后执行rust-msc-latest-win10.exe,打包进去 ![](assets/image-10-1712804613733-43.png) ![](assets/image-2-1712804613733-44.png) - 这个特别注意下,参考下图,视频os当时是v0.1.1来做的,现在os版本是v0.1.4了(2024.2.27日),编译报错。其实在报错的时候,有错误提示在最后一个报错提示的,需要v1.12.7以上才可以,而我习惯看错误从一个错误开始看起,以为漏了那个宏定义的步骤。各种找问题,后面看到需要升级版本到v1.12.7,编译通过了,在执行os.listdir('/sd')的时候,由于内核PikaPlatform.c改了,需要重载pika_platform_listdir才行,我也是哭死了,为什么这么折磨我,后面对比固件代码,跟固件代码版本一致。成功了,从晚上十点折磨到凌晨4点,一口老血喷出。所以以后务必指明版本,我也没想到版本差别这么大。 ![](assets/image-12-1712804613733-46.png) ### 修改pikapython中的CMAKE 增加os模块编译 ![](assets/image-3-1712804613733-47.png) 因为os模块很多代码使用的是linux环境下,所以我们需要把pikapython也设置为linux环境下,在main文件夹下修改CMAKE ![](assets/image-4-1712804613733-48.png) ### 在main文件夹下添加pika_config.h文件 ```c //pika_config.h #define PIKA_LINUX_COMPATIBLE 1 ``` ### 在py文件引入os ```python // main.py 更改后,记得执行rust-msc-latest-win10.exe import os print('hello pikapython!') ``` ### 修改esp32配置 支持文件系统的长名字和大小写区分 命令:idf.py menuconfig ![](assets/image-5-1712804613733-50.png) ![](assets/image-6-1712804613733-49.png) ![](assets/image-7-1712804613733-51.png) ![](assets/image-8-1712804613733-52.png) - S为保存,保存后,idf.py fullclean,重新编译下 ![](assets/image-14-1712804613734-53.png) ### 代码 [文件系统gitee代码](https://gitee.com/codercmd/esp32_pikapython_code/tree/fatfs) ## 串口烧录脚本 回顾上一章的内容,实现了交互和文件系统,这次从串口发送脚本文件保存,并将这个脚本文件启动,实现串口烧录 ### 串口下载python脚本 - 串口下载 Python 脚本和交互式运行非常类似,仍然是使用 obj_run 内核 API 进行脚本的运行。和交互式运行不同的是,下载 Python 脚本还需要对python脚本进行 存储。 - obj_run 支持运行字符串形式的 Python 脚本,因此无论以哪种方式存储,只要最后给 obj_run 传入 Python 脚本的字符串即可。所以可以的存储方式有:flash 直接存储、文件系统、外部存储器等。 ### 存储python脚本 - 存储 Python 源码很简单,将串口接收到的 Python 脚本字符串完整写入 Flash 即可。启动时不使用 pikaScriptInit() 函数,而是手动创建 pikaMain 根对象,再使用 obj_run(pikaMain, code) 运行脚本,code 代表的是存储好的 python 源码。 ### 运行脚本 - 首先用pikastudio软件通过串口发送的.a文件,然后保存会保存到app.pika ![20240325113029](assets/20240325113029.png) ![20240325112553](assets/20240325112553.png) - 读取脚本 ```c PikaObj *root= NULL; //判断是否文件 FILE *fp = fopen(PIKA_SHELL_SAVE_APP_PATH, "rb"); if(fp!=NULL){ fclose(fp); printf("[Info] load app from sd card\n"); root = newRootObj("pikaMain", New_PikaMain); obj_linkLibraryFile(root,PIKA_SHELL_SAVE_APP_PATH); obj_runModule(root,"main"); } else { printf("[Info] load app from firmware\n"); root = pikaPythonInit(); } pikaScriptShell(root); ``` - 重启脚本,需要重载函数 ```c void pika_platform_reboot(void){ // esp_restart(); abort(); } ``` ### 擦除脚本 - 当写脚本的时候,不小心写了死循环,那么将无法通过串口再次烧录了,这时候需要先擦除,再来烧录 - 擦除脚本 ```c remove(PIKA_SHELL_SAVE_APP_PATH); esp_restart();//擦除之后,需要重启 ``` - 新建一个任务,用来判断是否擦除脚本 ```c // 创建动态任务 xTaskCreate(eriase_task, "Dynamic Task", 2048, (void *)5, 5, &task_handle); vTaskDelay(3000/portTICK_PERIOD_MS); /*IO 操作*/ #define BOOT_GPIO_PIN GPIO_NUM_0 #define BOOT gpio_get_level(BOOT_GPIO_PIN) void key_init(void) { gpio_config_t gpio_init_struct; gpio_init_struct.intr_type = GPIO_INTR_DISABLE; /* 失能引脚中断 */ gpio_init_struct.mode = GPIO_MODE_INPUT; /* 输入模式 */ gpio_init_struct.pull_up_en = GPIO_PULLUP_ENABLE; /* 使能上拉 */ gpio_init_struct.pull_down_en = GPIO_PULLDOWN_DISABLE; /* 失能下拉 */ gpio_init_struct.pin_bit_mask = 1ull << BOOT_GPIO_PIN; /* BOOT 按键引脚 */ gpio_config(&gpio_init_struct); /* 配置使能 */ } void eriase_task(void *pvParameters) { while(1){ if(BOOT == 0) { vTaskDelay(10/portTICK_PERIOD_MS); /* 去抖动 */ if(BOOT == 0) { //擦除 printf("remove"); remove(PIKA_SHELL_SAVE_APP_PATH); esp_restart(); } } vTaskDelay(10/portTICK_PERIOD_MS); } } ``` ### 源码地址 [串口烧录源码](https://gitee.com/codercmd/esp32_pikapython_code/tree/%E4%B8%B2%E5%8F%A3%E7%83%A7%E5%BD%95)