本文介绍了一位视障学生在科研训练中开发的辅助视力残疾人士的导引系统,该项目最终获得全国嵌入式竞赛国家级三等奖。系统基于VL53L1X传感器实现障碍物检测、报警及环境提示功能,兼具技术探索与人文关怀。


前言

这个项目最初是我在学校科研训练中的一个选题,后经各方面的优化和方案的迭代,最终参加了全国大学生嵌入式…(忘了)竞赛芯片应用组瑞萨电子赛道,最终获得国家级三等奖,虽然奖项不大,但是一路以来的开发过程是非常有趣的,加之笔者也是视障的原因导致这个选题变得更加的有意义 (地狱) ,所以分享给大家。

项目概述

系统要求

系统的设计目的是为了给视力残疾人士提供导引,报警,以及对周围人和车辆的提示,因此功能大致分为如下几个部分:
障碍物检测:系统通过基于VL53L1X激光测距传感器的tof400c模块进行实时的距离检测和数据处理,当距离小于设定的临界值,系统发出语音提示和报警。
跌倒检测:系统通过加速度传感器MPU6050检测当前的姿态,包含加速度和角速度,通过识别人摔倒时的特征,触发紧急情况下的报警。
光线检测:系统通过自带ADC和光敏电阻检测当前光照强度,当光线低于设定值,可以设置警示灯在此情况下点亮,提示路人和车辆。警示灯也可以手动设定开启和关闭状态。
显示:在OLED 12864显示器上显示当前的距离,光照,温度信息,方便开发人员的调试。
定位和报警:系统在手动操作或自动检测紧急情况后,触发报警。报警时,系统会通过GPS模块和WLAN融合定位来确定当前的位置信息,并通过地图API来获取该经纬度对应的地点文字描述。信息将编码成电子邮件发送给紧急联系人,可以在邮件中通过超链接跳转到地图软件。同时,报警功能还会发出声音,引起注意。

上几张我们作品决赛时的图片:
这是机身:
1

2

这是接收的数据:
3

硬件和开发工具

需要的硬件

  • 主控:RA6M5(CPKIOT系列开发板)
  • 网络控制器:ESP32-C3
  • MPU6050
  • 蜂鸣器
  • LED
  • SSD1306 OLED 128x64
  • VL53L1X
  • 任意支持NMEA输出的GPS模块
  • 任意支持GBK编码解码的TTS语音模块
  • 按键和连接线若干

开发工具

  • e2 studio和适用于RA6M5的renesas FSP
  • Arduino IDE 2+(pio对ESP32C3的USB串口兼容性有些问题)
  • minicom等UART串口开发工具和JLink RTT Viewer(用于RA芯片的日志输出)

一些开发心得

先把开源仓库贴出来,里面的代码都有详细的解释->【戳我】

主控部分

瑞萨开发指南

开发瑞萨RA系列的芯片需要使用e2 studio集成开发环境,在安装程序运行时会自动安装部分驱动程序和FSP支持包,同时,如果需要使用外部/内部的SWD接口进行下载和调试,需要安装JLink支持工具,如果需要通过串口下载编译的hex文件,则需要下载瑞萨的UART烧录工具。

我的项目使用C语言,非RTOS开发,使用R7FA6M5BH3CFP芯片,在新建项目时,需要选择名称,项目类型,芯片和对应FSP版本,操作系统支持以及加密操作空间等选项,笔者选择了最简单的。
瑞萨的开发工具有着与STM32CubeMX相似的HAL管理逻辑,在工程目录下,点击comfiguration.xml进入FSP管理器,可以自由配置引脚输入输出、非GPIO的高级功能,外设堆栈等。为了开发和理解的简单,笔者已经将常见的Read/WriteUARTI2CdelayTimerprintf等操作进行了二次封装,变成了类似于Arduino代码的形式,大家可以下载我的开源代码,在mk_开头的库文件中了解外设的读写和启停操作,在FSP配置工具中,点击i字母也可以打开官方的说明文档,里面也有关于FSP函数的详细解释(虽然是英文)。
现在简单介绍一下管理器中外设的配置方式:
在管理器下方的操作栏中:

  • bsp用于定义芯片和开发板类型,修改使用的板级支持包
  • pins用于修改引脚(Pxxx)的模式、中断和初始状态,如果是GPIO,需要在此处指定,如果是外设功能引脚,需要在此处进行解绑,释放给periph模式
  • stack用于添加和删除各种外设(如串口、时钟、外部中断等),并通过属性界面进行详细的调整,包括参数设置、中断和优先级选择以及引脚分配。
  • Generate用于将代码自动部署到工程文件中

我们以初始化UART为例:

  1. 进入stack,添加一个串行接口设备(如g_uart0),属性面板中指定波特率、校验、中断函数、优先级、以及使用的硬件SCIx外设通道。
  2. 下载你使用的开发板的引脚定义图,找到串口对应的引脚。或者在已知使用串口外设编号的情况下,直接在选项卡里找到SCIx,导览到这组串口IO。
  3. IO口名称应为类似于P411的组合,4是Port,11是Pin。在引脚设置中,如果你已经注册了stack中的设备,那么只需要Enable这个引脚并取消所有的GPIO功能,功能复选框中将自动设置为periph mode。至此,一个外设初始化成功。
  4. 回到stack确定引脚不是null,按Ctrl+S保存,点击Generate生成。

其他外设的操作也是类似的,瑞萨对每个外设的操作有一套通用形式:

  • _Open(pCtrl, pCfg); 初始化
  • _Write(pCtrl, pData) 读写操作,数据和Ctrl控制对象均使用指针传递
  • _Callback(pArgs) 中断函数,传入的参数中可以解析出标志位、寄存器、缓冲区等信息

可以在hal_data.h中查看所有自己定义的外部设备句柄,包括_ctrl_cfg对象。其余的常规操作,可以使用AI工具查找和参考笔者在开源仓库的文档,此处不再赘述。

外设驱动支持

所有本项目使用的外设,其驱动文件都已进行移植并包含于开源仓库文件中,此处特别感谢:

  • OLED:江科大的STM32 OLED驱动程序
  • Serial接收:江科大的51单片机串口缓冲程序
  • VL53L1X:HFUT的智能车竞赛开源项目
  • GPSminmea project

烧录和调试

在编写完成,且指定了系统默认的调试器和编译器后,就可以进行编译和下载了,按下Ctrl+B可以进行编译,在顶栏选择Run->Debug可以使用JLink调试工具进行下载和调试,选中后,系统会自动初始化JLink工具和gdb,程序二进制文件此时被自动下载到开发板,且锁定到程序入口之前的断点。当下载完成之后,IDE会显示每个函数断点的地址,此时点击工具栏的开始按钮,若未发生内存溢出或访问外设失败的情况,程序会进入main loop,开发板上的零部件也会开始工作。
值得注意的几点是:

  • 有些板子没有自带JLink接口,而瑞萨默认的SWD调试工具是JLink,所以如果没有JLink硬件的话,建议使用串口下载。在C/C++ properties设置中,选择生成的文件类型为Intel HEX,重新执行编译,在Debug/目录下就可以找到程序的hex文件,此时,我们需要把板子的启动选择跳线连接到下载一侧,连接电脑,在下载工具中新建实例,选择对应的COM口后,快速按下Reset按键和电脑建立连接,然后加入HEX文件下载,下载后,板子断电,启动跳线连接另一侧,通电就可以运行程序啦。
  • 如果在调试时一直卡在DefaultExeption而且脱机也无法进入程序循环,那么一般是因为初始化或者访问外设资源的方式不正确,建议重新参考源码或文档。
  • 使用SEGGER RTT Viewer来读取dbg_logi()发送的日志时,我们需要在编译之后找到Debug/<projectName>.map文件,Ctrl+F搜索SEGGER_RTT,你会看到:
    1
    0x1234abcd    _SEGGER_RTT
    把这个地址复制下来,输入到监视器软件的地址框(而不是让软件自动检测),就可以打开RTT串口通道了。dbg_logi()的输出格式会带一个时间戳,格式为[timestamp]context

ESP32-C3部分

负责接收串口指令、定位、解析数据、发送邮件。下面简单讲述一下代码结构和一些设计思路。

串口数据接收

Arduino库函数简化了串口数据处理的中断注册和逐个字节接收的过程,我们只需要在主循环中查询Serial.available()参数,即可对串口缓冲区进行处理。由于需要识别特定的指令以及提取String对象进行比较,此处和RA的接收函数一样约定了指令字符串的识别格式,打包成了pollSerial()函数,具体函数如下;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 串口接收函数
void pollSerial1() {
while (Serial1.available()) {
char c = Serial1.read();

if (c == '\n' || c == '\r') { // 命令结束
cmdBuffer[cmdIndex] = '\0'; // 字符串结束符
cmdIndex = 0;

if (cmdBuffer[0] == '#') {
command = String(cmdBuffer + 1); // 去掉 #
cmdReady = true;
}
} else {
if (cmdIndex < CMD_BUFFER_SIZE - 1) {
cmdBuffer[cmdIndex++] = c;
} else {
// 超出缓冲区,重置
cmdIndex = 0;
}
}
}
}

接着,就可以与RA6M5进行握手了。

连接Wi-Fi

ESP32对于无线网络提供了丰富的系统调用,最简单的连接方法是传入代表SSID和密码的两个字符串,但出于灵活配置的需要,我没有采取硬编码,而是使用了以下逻辑:

  • 上电后:读取Flash特定地址的信息,SSID和密码各32字节,加入字符串。
  • 判断字符串,如果非空则尝试连接网络,成功则进入主循环。
  • 连接超时或字符串为空:关闭WiFi连接进程,使能一个HTTP Server,发起一个简单的网页,包含两个输入框和一个确认按钮,用于收集SSID和密码。
  • 当收到表单的提交后,将SSID和密码存储到Flash的对应地址,使用esp.restart()重启系统,重新尝试连接操作。

此处涉及的代码较多,详见开源仓库,下面展示部分代码(可以用折叠按钮隐藏):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
#include <WiFi.h>
#include <WebServer.h>
#include <EEPROM.h>

void Wifi_init_process(); //Wifi启动流程
void LoadWifiConfig(void); //加载数据
void saveWifiConfig(String newssid, String newpswd); //存储数据
bool ConnectWifi(); //网卡连接操作
void handleRoot(); //web配置界面
void handleSave(); //web服务器保存

// Variables
String ssid = "";
String pswd = "";

#define EEPROM_SIZE 64 // Define the size of EEPROM
#define SSID_ADDR 0 // Address to store SSID
#define PSWD_ADDR 32 // Address to store Password

void LoadWifiConfig(void) //加载数据
{
//初始化EEPROM
char pswd_buf[32] = {0};
char ssid_buf[32] = {0};
EEPROM.begin(EEPROM_SIZE);
EEPROM.get(SSID_ADDR, ssid_buf);
EEPROM.get(PSWD_ADDR, pswd_buf);
EEPROM.end();
ssid = String(ssid_buf);
pswd = String(pswd_buf);
}

void saveWifiConfig(String newssid, String newpswd) //存储数据
{
EEPROM.begin(EEPROM_SIZE);
int i = 0;
//Write SSID
while(newssid[i]){
EEPROM.write(SSID_ADDR+i, newssid[i]);
delay(1);
i++;
}
EEPROM.write(SSID_ADDR+i, 0);
//Write PSWD
i = 0;
while(newpswd[i]){
EEPROM.write(PSWD_ADDR+i, newpswd[i]);
delay(1);
i++;
}
EEPROM.write(PSWD_ADDR+i, 0);
delay(2);
EEPROM.end();
delay(2);
}

bool ConnectWifi()
{
WiFi.begin(ssid.c_str(), pswd.c_str());
Serial.print("Connecting");
int attempt = 0;
while (WiFi.status() != WL_CONNECTED && attempt < 10) {
delay(1000);
attempt++;
Serial.print(".");
}
Serial.println();
Serial.printf("[INFO] Wi-Fi state: %d\n", WiFi.status() == WL_CONNECTED);
return WiFi.status() == WL_CONNECTED; //返回WiFi连接状态
}

// AP 配置页面
void handleRoot() {
server.send(200, "text/html",
"<html><body>"
"<h1>ESP32 Wi-Fi Configure</h1>"
"<form action='/save' method='POST'>"
"SSID: <input type='text' name='ssid'><br>"
"Password: <input type='password' name='pass'><br>"
"<input type='submit' value='Save'>"
"</form></body></html>"
);
}

// 处理 Wi-Fi 配置提交
void handleSave() {
if (server.hasArg("ssid") && server.hasArg("pass")) {
String newSSID = server.arg("ssid");
String newPass = server.arg("pass");
saveWifiConfig(newSSID, newPass); //保存从网页获取的配置信息
server.send(200, "text/plain", "Wi-Fi config saved, rebooting...");
Serial.println("[INFO] Configuration saved, system will reboot after 2 secs...");
Serial1.println("@CONFIG_END");
delay(2000); // 等待2秒
ESP.restart(); //重启设备
} else {
server.send(400, "text/plain", "Error!");
}
}

// 网络连接程序
void Wifi_init_process()
{
// 读取存储的 Wi-Fi 信息
LoadWifiConfig();

// 连接 Wi-Fi
if (ssid.length() > 0 && ConnectWifi()) {
Serial.println("[INFO] WiFi Connected!");
Serial1.println("@WL_CONNECTED");
} else {
WiFi.softAP("ESP32_Config", "12345678");
IPAddress IP = WiFi.softAPIP();
Serial1.println("@NEED_CONFIG");
Serial.printf("[INFO] Please configure network: IP: %d.%d.%d.%d\n", IP[0], IP[1], IP[2], IP[3]);

server.on("/", handleRoot); //给定服务器回调函数
server.on("/save", handleSave);
server.begin();
while(1)
{
server.handleClient();
}
}
}

EMail的发送

邮件的发送一开始使用了ESPMail的库函数,使用QQ邮箱发送,后发现内存开销太大以至于无法运行JSON解析和打包程序,于是经过了笔者的研究,使用了一个无base64的测试邮件平台——mailtrap,在注册账号和获取实例(我使用了自己名下的域名进行绑定,这样发出的邮件是我网站的后缀,其他方法详见官网文档)后,你会获得一个账号和密码,下面有两种发送方式:

  • SMTP协议:传统的电子邮件接口协议,在ESP32中初始化一个HTTP Client,对文档中指定服务器接口依次发送指定格式的请求信息和邮件内容字符串,可以实现邮件的发送。
  • RESTful API:此平台提供的一种便捷方式,笔者使用的方法,向网站发送一个HTTP请求即可。(格式参见开源ESP32源码)

本次开发中的发送代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// Send mail
void sendMail(String s)
{
String smtp_server = SMTP_HOST; // SMTP服务器地址
int smtp_port = SMTP_PORT; // SMTP服务器端口
String smtp_user = AUTHOR_EMAIL; // SMTP用户名
String smtp_pass = AUTHOR_PASS; // SMTP密码
String sender_email = AUTHOR_EMAIL; // 发件人邮箱
String recipient_email = mail_addr; // 收件人邮箱
String author_from = AUTHOR_FROM; // 发件人名称

if (WiFi.status() == WL_CONNECTED) {
HTTPClient http;
http.begin("https://send.api.mailtrap.io/api/send");
http.addHeader("Authorization", "Bearer " + smtp_pass);
http.addHeader("Content-Type", "application/json");

// 构造 JSON 字符串
String postData = "{";
postData += "\"from\":{\"email\":\"" + author_from + "\",\"name\":\"" + String("ESP32 Dev Module") + "\"},";
postData += "\"to\":[{\"email\":\"" + mail_addr + "\"}],";
postData += "\"subject\":\"" + String("Caution") + "\",";
postData += "\"text\":\"" + s + "\"";
postData += "}";

int httpResponseCode = http.POST(postData);

if (httpResponseCode > 0) {
String response = http.getString();
Serial.print(httpResponseCode == 200 ? "[INFO] Sent" : "[Warning] Send failed: Service error");
if(httpResponseCode != 200)
{
Serial.print(", errorCode: ");
Serial.println(httpResponseCode);
}
else Serial.println();
} else {
Serial.println("[Warning] Send failed: Connect failure\n");
}
http.end();
}
}

使用网络API进行定位

由于廉价GPS模块在室内时常表现不佳,笔者引入了网络融合定位功能。在国内,这种技术十分常见,但是对于个人开发者较难获得授权,笔者使用了对嵌入式开发者友好且免费的WAYZ平台。
服务器需要收集环境WiFi的MAC地址和信号强度,我们需要通过ESP32的系统调用,获取BSSID、时间戳、信号强度、随机序列号等信息,信息通过ArduinoJSON库函数进行打包后,向服务器提交GET请求。在解码后,可以获取经纬度、区划、具体地址字符串 等定位结果信息。信息传递给上述邮件发送程序进行发送。
参见:开源项目链接 官网

定位代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
#include <time.h>
#include <ArduinoJson.h>
#include <WiFi.h>
#include <HTTPClient.h>

// Location structure
struct GpsLocation {
float latitude;
float longitude;
char valid;
String name; //非必须
} loc;

#define UUID "your_uuid"
#define KEY "your_key"

typedef struct
{
String mac;
signed char rssi;
} ap_info_typedef;

ap_info_typedef ApInfo_array[10]; // 定义AP信息结构体
int ApInfo_count = 0; // AP信息计数

void wifi_scan()
{
ApInfo_count = WiFi.scanNetworks(); // 扫描Wi-Fi网络
delay(300);
if (ApInfo_count == 0) {
return; // 没有找到网络
}
else {
for (int i = 0; i < ApInfo_count; ++i)
{
ApInfo_array[i].rssi = WiFi.RSSI(i);
ApInfo_array[i].mac = WiFi.BSSIDstr(i);
delay(10);
}
Serial.printf("[INFO] Wi-Fi scanned, count: %d\n", ApInfo_count);
return; // 扫描成功
}
}

// 定义位置信息JSON结构
DynamicJsonDocument doc(1024);
DynamicJsonDocument rep(1024);

// 获取当前时间戳
// 设置时区(中国为 UTC+8)
const long gmtOffset_sec = 8 * 3600;
const int daylightOffset_sec = 0;

void getTimeStamp()
{
// 初始化 NTP
configTime(gmtOffset_sec, daylightOffset_sec, "ntp.aliyun.com", "ntp.ntsc.ac.cn", "pool.ntp.org");

// 等待同步完成
struct tm timeinfo;
while (!getLocalTime(&timeinfo)) {
delay(500);
}
// 获取时间戳
time_t now = time(nullptr); // 获取当前秒级时间戳
Serial.printf("[INFO] Current timestamp:%d\n", now);
}

// JSON封装
String JsonSerialization()
{
String message;
unsigned long long timestamp = time(nullptr);
int64_t timestamp_ms = timestamp * 1000LL + random(1000);

doc["timestamp"] = timestamp_ms; // 获取当前时间戳
doc["id"] = "esp32-" + String(random(1000000));
doc["asset"]["id"] = UUID;
doc["asset"]["manufacturer"] = "espressif";
doc["location"]["timestamp"] = time(nullptr); // 获取当前时间戳
for(int i = 0;i < min(10, ApInfo_count);i++)
{
doc["location"]["wifis"][i]["macAddress"] = ApInfo_array[i].mac; // 获取MAC地址
doc["location"]["wifis"][i]["signalStrength"] = ApInfo_array[i].rssi; // 获取信号强度
}
serializeJson(doc, message); // 序列化JSON数据并导出字符串
Serial.printf("\nTransmit to the server:\n %s\n", message.c_str());
return message;
}

bool get_loc_online(GpsLocation *loc)
{
// 获取当前时间戳
getTimeStamp();
// 扫描Wi-Fi网络
ap_info_typedef ApInfo;
wifi_scan();
String msg_to_send = JsonSerialization();
// 发送HTTP POST请求
HTTPClient http;
http.begin("https://api.newayz.com/location/hub/v1/track_points?access_key=<your_key>");
http.addHeader("Content-Type", "application/json");
http.addHeader("Host", "api.newayz.com");
http.addHeader("Connection", "keep-alive");
int httpCode = http.POST(msg_to_send); // 发送POST请求并获取响应代码
String payload = http.getString();
// 检查HTTP响应代码
DeserializationError error = deserializeJson(rep, payload);
if (error)
{
Serial.println("[Warning] Failed to parse JSON response");
delay(1000);
return false; // 如果解析失败,返回false
}
double longitude = rep["location"]["position"]["point"]["longitude"];
double latitude = rep["location"]["position"]["point"]["latitude"];
const char * name = rep["location"]["address"]["name"];
const char * source = rep["location"]["position"]["source"];
const char * spatialReference = rep["location"]["position"]["spatialReference"];
String full_result;
serializeJson(rep, full_result);
http.end();
// 将解析后的数据存储到loc结构体中
loc->latitude = latitude;
loc->longitude = longitude;
loc->name = String(name);
loc->valid = 1; // 设置有效状态
Serial.println();
Serial.printf("[INFO] Get Location: lat:%f, lon:%f\n", latitude, longitude);
Serial.println();
Serial.printf("The full JSON Data is:\n %s", full_result.c_str());
Serial.println();
return true; // 返回true表示成功
}

烧录和下载

笔者的ESP32使用USB-CDC技术,简单来说,它的USB口不是像传统的开发板一样用一个USB-to-TTL来连接到芯片的串行接口,而是直接连接了芯片的USB外设,电脑上的串口是芯片模拟出来的,因此,我们需要一些设置来更好的使用这个串口:
启用USB CDC支持:在Arduino IDE顶栏的工具配置栏将USB CDC Support设置为Enabled
跳过CDC自检:在setup()代码中加入一条Serial.setTxTimeoutMs(0);,避免在找不到电脑时卡在死循环
然后插上板子点击IDE的Download就行了,很傻瓜。

成果展示

视频->【点我】

参赛的碎碎念和总结

虽然我是负责所有技术和开发工作的,但其实比赛负责人不是我,所以很多内幕也就不知道了,我也懒得过问这些事情,这些是纯粹的参赛心得还有需要避雷的地方。

雷点是很多老师为了冲业绩可能会对比赛内容夸大或者对赛事主办方的要求含糊其辞,要仔细阅读官方文档,仔细评估比赛的难度和作品迁移可行性!
不是随便填个选题就行!这个填了就不能大改,也就是要一直顺着这个大的主线做下去
不是拿个stm32代码改一下就行!这玩意没有想象中的那么简单,包括其他的那些赛道(比如RK、海思、RT-Thread什么的),每个平台都有自己的特性和学习曲线,请善用文档和AI工具,对自己的熟练度做好评价,别随随便便就付钱了结果大批大批的人没回本
还有这一套下来挺烧钱的,尤其是想拿一等奖的,基本上都需要完整的外壳和可以用于实际场景的可靠性,像我这样的概念模型也只能三等奖了

然后是全流程:
报名:学校会组织,去官网把信息填了就行,学校可能会预支你板子,或者你付押金自己申请
初赛:提交演示视频、正式说明文档、电路原理图、包含开发板的不同角度照片、开源仓库地址等
区域赛:前往省内的承办学校,向评委展示作品,一般在提交海选的2周之内可以看到参赛资格,时间一般在暑假(7月20日前后)
国赛:在8月中旬,全国参赛选手携带作品前往决赛场地,持续3天,包含签到、编程能力测评、答辩、领奖几个环节,现场可以看到很多优秀作品,也可以和很多科技企业的负责人近距离交流。
比赛没有纸质奖状,可以付费申请PCB奖状,邮寄送达,同时进入国赛也可以领到PCB参赛证和纪念服装。

一点碎照片,大家就当游记吧:
在机场
我们的参赛证
现场
现场
不太好看的照片

然后这应该是我最近打完的比较大的比赛了,也许是我的最后一个国家级比赛,但我希望不是hhh~