一.选题
本次我选择的题目是任务一:“使用板卡上的触摸按键,实现点按和左右滑动,实现传感器选择和切换,并将数据发送到上位机,功能选择的可视化也在上位机完成如:能够选择加速度传感器,开启X轴数据发送,然后关闭加速度显示,选择温度”
二.项目功能介绍
本项目分为2各部分:上位机、下位机。上位机与下位机之间通过串口通讯。
功能1:上位机可以接收下位机发来的传感器数据,并显示出来
功能2:上位机可以选择当前工作的传感器或关闭,并通过串口指令告诉下位机,下位机根据指令决定当前应该读取那个传感器的值并上报给上位机或关闭传感器
功能3:下位机可以通过2个触摸电极实现点击和滑动的识别。点击:通过串口发送切换上一个/下一个传感器的指令给上位机,上位机收到后改变传感器选择框内的值。滑动:下位机切换当前工作传感器为下一个/上一个。并且通过串口发送切换后的工作传感器的序号给上位机,上位机显示切换后的传感器名称,同时数据显示界面也变成新的传感器
三.软硬件平台
3.1系统框图
3.2下位机
下位机硬件平台由X-NUCLEO-IKS4A1开发板和STM32U5 NUCLEO开发板组成
X-NUCLEO-IKS4A1开发板的跳帽配置如下图所示
3.3上位机
上位机软件由Python编写,使用了Tkinter绘制GUI
四.软件流程图及关键代码
4.1串口协议
在开始代码之前,先设计一个串口协议,来实现上位机和下位机之间的通讯。协议如下
指令格式
AT+[CMD]=[参数1],[参数2]......
| 名称 | CMD | 发送方向 |
|---|---|---|
| 数据上报 | DataReport | MCU->上位机 |
| 选择当前工作传感器 | SelectWorkSensor | 上位机->MCU |
| 切换选择的传感器 | ChangeSelectSensor | MCU->上位机 |
| 切换工作的传感器 | ChangeWorkSensor | MCU->上位机 |
传感器序号及数据汇总
| 传感器名称 | 序号 | 数据 |
|---|---|---|
| LSM6DSO16IS 加速度计、陀螺仪 | 1 | ACC、GYR |
| LIS2MDL 磁力计 | 2 | MAG |
| LIS2DUXS12 加速度计 | 3 | ACC |
| LPS22DF 压力传感器 | 4 | Temp、Press |
| SHT40AD1B 温湿度传感器 | 5 | Temp、Hum |
| STTS22H 温度传感器 | 6 | Temp |
| LSM6DSV16X 加速度计、陀螺仪 | 7 | ACC、GYR |
数据上报
说明
当MCU当前有工作的传感器,MCU就要定时发送该指令给上位机,让上位机定时刷新这个传感器的数据
指令格式
AT+DataReport=[传感器序号],[数据1],[数据2]......
传感器序号详见“传感器序号及数据汇总”,这个传感器有几个数据就填几个,文本,填什么上位机就显示什么,如果没有数据或者数据读取失败等情况就填空
选择当前工作传感器
说明
上位机选择好传感器后,点击确定,上位机就把这个指令发给MCU,MCU收到后就知道现在应该定时上传那个传感器的数据
指令格式
AT+SelectWorkSensor=[传感器序号]
切换选择的传感器
说明
开发板上有2个触摸按钮,当用户短按2个触摸按键中的任意一个,MCU就要发送这个指令给上位机,告诉上位机选择传感器的下拉框应该显示什么(他的作用等同于用户去下拉框中选择一个传感器)
指令格式
AT+ChangeSelectSensor=[data]
data:+1:下一个;-1:上一个
切换工作的传感器
说明
开发板上有2个触摸按钮,当用户左右滑动后,MCU就要发送这个指令给上位机,告诉上位机MCU现在要切换工作的传感器(等同于用户选择传感器再点确定)
指令格式
AT+ChangeWorkSensor=[传感器序号]
4.2 下位机软件
下位机为裸机开发
4.2.1 main函数
main函数的代码如下
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* Configure the System Power */
SystemPower_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_ICACHE_Init();
MX_TIM1_Init();
MX_MEMS_Init();
/* USER CODE BEGIN 2 */
HAL_UART_Receive_IT(&huart1, &uartRecvDataByteTemp, 1);
HAL_TIM_Base_Start_IT(&htim1);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
// MX_MEMS_Process();
/* USER CODE BEGIN 3 */
UART_TASK();
MEMS_TASK();
QVAR_TASK();
}
/* USER CODE END 3 */
}
流程图如下
在while1中只有3个TASK,分别是串口TASK、MEMS TASK、QVAR TASK,这三个任务的运行由他们自己的标志位控制,只有标志位为1才会被执行,反之就return。
串口TASK的标志位在串口中断接收到一帧数据后才会被置位
MEMS TASK、QVAR TASK的标志位由定时器定时置位,我设置的运行周期分别为200ms和100ms。这样就可以实现每200ms读取一次传感器数据并上报给上位机,每100ms读取一次QVAR的数据,然后进行单机、滑动的解析。
4.2.2 串口中断回调函数
代码如下
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance == USART1)
{
/* save recv data to buffer */
buffer_uartRecv[index_uartDataRecv] = uartRecvDataByteTemp;
index_uartDataRecv++;
if(uartRecvDataByteTemp == '\n')
{
buffer_uartRecv[index_uartDataRecv] = '\0';
index_uartDataRecv++;
index_uartDataRecv = 0;
taskRunFlag_uart = 1;
}
else
{
if(index_uartDataRecv >= sizeof(buffer_uartRecv) - 1)
{
index_uartDataRecv = 0;
}
HAL_UART_Receive_IT(&huart1, &uartRecvDataByteTemp, 1);
}
}
}
串口每收到一个字节的数据,就会进入一次中断,在中断中会把收到的数据从接收缓存中放到buffer数组中。再判断是否为'\n',也就是一帧串口指令的尾巴,如果是的,那就是收到一帧数据了,要把标志位置1,这样while1中就可以运行串口TASK一次,接收index清零。反之判断一下index是否到最大值,是的话就要重置index,最后再次开启串口中断
4.2.3 UART_TASK
代码如下
void UART_TASK(void)
{
if (taskRunFlag_uart == 1)
{
taskRunFlag_uart = 0;
#if 1
ParseATCommandAndSetWorkSensor((char*)buffer_uartRecv);
#else
if(ParseATCommandAndSetWorkSensor((char*)buffer_uartRecv))
{
printf("Parse AT Command success\r\n");
}
else
{
printf("Parse AT Command fail\r\n");
}
#endif
HAL_UART_Receive_IT(&huart1, &uartRecvDataByteTemp, 1);
}
}
当标志位为1时,表示串口收到一帧数据,需要解析,进来后先把标志位恢复成0,然后调用解析函数,解析后再次开启串口中断
4.2.4 ParseATCommandAndSetWorkSensor
bool ParseATCommandAndSetWorkSensor(char* buffer)
{
char temp;
uint8_t SelectWorkSensorId = 0;
if(strncmp(buffer, "AT+SelectWorkSensor=", 20) != 0) return false;
char* dataPtr = buffer + 20;
temp = *dataPtr;
SelectWorkSensorId = temp - '0';
// printf("SelectWorkSensorId = %d\r\n", SelectWorkSensorId);
set_now_work_sensor(SelectWorkSensorId);
return true;
}
这是AT指令的解析函数,进来后会验证指令格式是否正确,正确再解析对应的数据,然后设置当前工作的传感器。set_now_work_sensor中会给一个变量赋值,该变量就是用于指示当前那个传感器工作的
void set_now_work_sensor(uint8_t sensorId)
{
nowWorkSensor = sensorId;
}
4.2.5 定时器超时回调
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM1)
{
runTimeCnt_sensor++;
runTimeCnt_QVAR++;
if(runTimeCnt_sensor == 20)
{
runTimeCnt_sensor = 0;
taskRunFlag_sensor = 1;
}
if(runTimeCnt_QVAR == 10)
{
runTimeCnt_QVAR = 0;
taskRunFlag_QVAR = 1;
}
}
}
我在CUBEMX中配置定时器10ms触发一次,并自动重装载。进入超时回调后,我有runTimeCnt_sensor 和runTimeCnt_QVAR 两个变量进行计数,通过这两个变量的计数,我就可以实现每200ms、100ms触发一次MEMS TASK和QVAR TASK
4.2.6 MEMS_TASK
MEMS_TASK代码如下
void MEMS_TASK(void)
{
if (taskRunFlag_sensor == 1)
{
taskRunFlag_sensor = 0;
mems_process();
}
}
mems_process中会根据之前说的当前工作传感器的变量的值,决定去读取那个传感器的数据,然后发送串口指令
void mems_process(void)
{
if (nowWorkSensor == 0)
{
return;
}
switch (nowWorkSensor)
{
case 1:
get_sensor_1_LSM6DSO16IS_data();
break;
case 2:
get_sensor_2_LIS2MDL_data();
break;
case 3:
get_sensor_3_LIS2DUXS12_data();
break;
case 4:
get_sensor_4_LPS22DF_data();
break;
case 5:
get_sensor_5_SHT40AD1B_data();
break;
case 6:
get_sensor_6_STTS22H_data();
break;
case 7:
get_sensor_7_LSM6DSV16X_data();
break;
default:
break;
}
}
一共有7个传感器,我就随便找一个展示一下,其他传感器代码也是类似的,大同小异
void get_sensor_1_LSM6DSO16IS_data(void)
{
IKS4A1_MOTION_SENSOR_Axes_t ACC;
IKS4A1_MOTION_SENSOR_Axes_t GYR;
if (IKS4A1_MOTION_SENSOR_GetAxes(IKS4A1_LSM6DSO16IS_0, MOTION_ACCELERO, &ACC))
{
// printf("sensor_1_LSM6DSO16IS get ACC fail\r\n");
return;
}
if (IKS4A1_MOTION_SENSOR_GetAxes(IKS4A1_LSM6DSO16IS_0, MOTION_GYRO, &GYR))
{
// printf("sensor_1_LSM6DSO16IS get ACC fail\r\n");
return;
}
printf("AT+DataReport=1,%d,%d,%d,%d,%d,%d\r\n", ACC.x, ACC.y, ACC.z, GYR.x, GYR.y, GYR.z);
}
4.2.7 QVAR_TASK
void QVAR_TASK(void)
{
qvar_action_e qvarAction = E_QVAR_ACTION_NONE;
if (taskRunFlag_QVAR == 1)
{
taskRunFlag_QVAR = 0;
if (1 == getAndStoreQVARData())
{
qvarAction = singleTouchOrSlideJudge();
if (qvarAction != E_QVAR_ACTION_NONE)
{
qvarAction_Process(qvarAction);
}
}
}
}
在QVAR_TASK中也有一个同串口、MEMS的标志位,这个标志位由定时器控制。QVAR TASK运行一次,其中会进行QVAR数据的读取,然后采集,等采集完成后,进行点击、滑动的判定,然后把识别到的结果进行使用。这三个函数代码如下
static uint8_t getAndStoreQVARData(void)
{
int16_t qvarValue = 0;
static uint8_t state = 0;
get_QVAR_data(&qvarValue);
qvarValue = qvarValue / 78;
// printf("QVAR = %d\r\n", qvarValue);
/* 未开始采样 */
if (state == 0)
{
/* 是否检测到按钮按下 */
if ((qvarValue > 200) || (qvarValue < -400))
{
// printf("***Start***\r\n");
state = 1;
index_QVAR = 0;
buffer_QVAR[index_QVAR] = qvarValue;
index_QVAR++;
return 0;
}
else
{
return 0;
}
}
/* 采样中 */
else
{
buffer_QVAR[index_QVAR] = qvarValue;
index_QVAR++;
/* 判断是都到采样结束 */
if (index_QVAR >= 10)
{
index_QVAR = 0;
state = 0;
// printf("***End***\r\n");
return 1;
}
}
return 0;
}
static qvar_action_e singleTouchOrSlideJudge(void)
{
uint8_t i;
// printf("buffer %d %d %d %d %d %d %d %d %d %d",
// buffer_QVAR[0], buffer_QVAR[1], buffer_QVAR[2], buffer_QVAR[3],
// buffer_QVAR[4], buffer_QVAR[5], buffer_QVAR[6], buffer_QVAR[7],
// buffer_QVAR[8], buffer_QVAR[9]);
/* 左边被按下 */
if (buffer_QVAR[0] > 200)
{
for (i = 1; i < 5; i++)
{
if (buffer_QVAR[i] < -400)
{
return E_QVAR_ACTION_LEFT_TO_RIGHT_SLIDE;
}
}
return E_QVAR_ACTION_LEFT_TOUCH;
}
/* 右边按下 */
else if (buffer_QVAR[0] < -400)
{
for (i = 1; i < 5; i++)
{
if (buffer_QVAR[i] > 10)
{
return E_QVAR_ACTION_RIGHT_TO_LEFT_SLIDE;
}
}
return E_QVAR_ACTION_RIGHT_TOUCH;
}
else
{
return E_QVAR_ACTION_NONE;
}
}
static void qvarAction_Process(qvar_action_e action)
{
uint8_t sensorId = get_now_work_sensor();
switch(action)
{
case E_QVAR_ACTION_LEFT_TOUCH:
{
/* last sensor */
printf("AT+ChangeSelectSensor=-1\r\n");
break;
}
case E_QVAR_ACTION_RIGHT_TOUCH:
{
/* next sensor */
printf("AT+ChangeSelectSensor=+1\r\n");
break;
}
case E_QVAR_ACTION_LEFT_TO_RIGHT_SLIDE:
{
if (sensorId == 0)
{
return;
}
sensorId++;
if (sensorId == 8)
{
sensorId = 1;
}
set_now_work_sensor(sensorId);
printf("AT+ChangeWorkSensor=%d\r\n", sensorId);
break;
}
case E_QVAR_ACTION_RIGHT_TO_LEFT_SLIDE:
{
if (sensorId == 0)
{
return;
}
if (sensorId == 1)
{
sensorId = 7;
}
else
{
sensorId--;
}
set_now_work_sensor(sensorId);
printf("AT+ChangeWorkSensor=%d\r\n", sensorId);
break;
}
default:
{
return;
break;
}
}
}
简述一下QVAR的代码工作逻辑,当手指触摸电极时,会得到一个+MAX或-MAX的值,MAX大约是32000多(这个值/78就是对应mV,此时MAX=420,左边按下是420,右边按下是-420)。如果没有触摸就会在0左右浮动。理论上可以把这个过程看做-1、0、1,单机和滑动可以用状态机的思想来判断,当出现+MAX或-MAX时开始采样,采样一段时间。采样结束后进行分析,如果只有+MAX或-MAX,那就是单机,两个都有,就是滑动。
但是我在实测时发现正值有时无法到达最大幅值,只能到20000多,并且有时松手后还不会立刻下降,而是缓慢下降(很奇怪,右边百试百灵,但是左边偶尔好,大部分情况下都不好)。因此我额外做了一下处理。左边的判定条件我改成200,以应对无法到达最大幅值的问题,然后采样一开始我是采样5次,也就是500ms就结束,然后分析时把这5次都用上,现在我采样10次,也就是1S,分析时还是用前5次。后5次相当于就是一个delay的作用,过滤掉值缓慢下降的问题。
4.3上位机软件
上位机由python编写,使用了tkinter库绘制GUI。上位机一共有4个py文件,分别为main、gui、mySerial、myProtocol
上位机界面如下
4.3.1 main
main作为整个程序的入口,代码如下
import gui
import mySerial
def main():
#初始化串口
mySerial.initSerial()
#绘制GUI界面
gui.createGuiMain()
if __name__ == '__main__':
main()
4.3.2 gui
gui中放置界面绘制、传感器数据显示、各个按键的回调函数等的相关代码。下面展示一下部分函数
#开关串口按钮被按下的处理函数
def onOffSerialButtonPressedProcess():
selected_value = selected_port.get()
serial_State = mySerial.getSerialState()
if 0 == serial_State:
# print("serial_State = 0")
if True == mySerial.openSerialPort(selected_value):
print("open serial port", selected_value, "success")
on_off_serial_button.config(text = "关闭串口")
update_serial_button.config(state=tk.DISABLED) #串口打开,不允许按刷新按钮
com_port_select_combobox.state(['disabled']) #串口打开,禁用Combobox
sensor_select_confirm_button.config(state=tk.NORMAL) #串口打开,允许按确定键
elif 1 == serial_State:
# print("serial_State = 1")
if True == mySerial.closeSerialPort():
print("close serial port success")
on_off_serial_button.config(text = "打开串口")
update_serial_button.config(state=tk.NORMAL) #串口关闭,不允许按刷新按钮
com_port_select_combobox.state(['!disabled']) #串口关闭,禁用Combobox
sensor_select_confirm_button.config(state=tk.DISABLED) #串口关闭,不允许按确定键
else:
print("ERROR unknow serial_State!!!")
clearAllSensorData() #不管是开/关串口,都把传感器显示数据清空
#传感器选择确认按钮被按下的处理函数
def SensorSelectConfirmButtonPressedProcess():
sensor = selected_sensor.get()
data = myProtocol.buildSelectWorkSensorAtMsg(sensor)
mySerial.sendData(data)
#传感器1数据设置
def set_sensor_1_data(sensor_1_acc_x, sensor_1_acc_y, sensor_1_acc_z, sensor_1_gry_x, sensor_1_gry_y, sensor_1_gry_z):
sensor_1_ACC_X_entry.delete(0, tk.END)
sensor_1_ACC_Y_entry.delete(0, tk.END)
sensor_1_ACC_Z_entry.delete(0, tk.END)
sensor_1_GRY_X_entry.delete(0, tk.END)
sensor_1_GRY_Y_entry.delete(0, tk.END)
sensor_1_GRY_Z_entry.delete(0, tk.END)
if sensor_1_acc_x != "":
sensor_1_ACC_X_entry.insert(0, sensor_1_acc_x)
if sensor_1_acc_y != "":
sensor_1_ACC_Y_entry.insert(0, sensor_1_acc_y)
if sensor_1_acc_z != "":
sensor_1_ACC_Z_entry.insert(0, sensor_1_acc_z)
if sensor_1_gry_x != "":
sensor_1_GRY_X_entry.insert(0, sensor_1_gry_x)
if sensor_1_gry_y != "":
sensor_1_GRY_Y_entry.insert(0, sensor_1_gry_y)
if sensor_1_gry_z != "":
sensor_1_GRY_Z_entry.insert(0, sensor_1_gry_z)
4.3.3 mySerial
mySerial中放置串口相关代码,串口初始化、开关串口、收发串口数据等,下面为其中的全部函数
#获取串口开关的状态
def getSerialState():
global serial_State
# print("getSerialState = ",serial_State)
return serial_State
#初始化串口
def initSerial():
global ser
ser = serial.Serial()
#更新串口号
def updateSerialPorts():
# 声明ports为全局变量
global ports
# 清空列表以确保每次调用时都是最新的端口列表
ports.clear()
#遍历系统中的串行端口,并将每个端口的设备名添加到全局列表ports里
ports.extend([p.device for p in serial.tools.list_ports.comports()])
#打开串口
def openSerialPort(port):
global serial_State
try:
ser.port = port
ser.baudrate = 921600
ser.open()
#创建一个线程,专门用于接收数据
receive_thread = threading.Thread(target = receiveData)
receive_thread.daemon = True #设置为守护线程,主程序退出时自动结束
receive_thread.start()
#设置是否开启串口的变量
serial_State = 1
return True
except Exception as e:
print("ERROR open serial port", port ,"fail, error code = ", e)
serial_State = 0
return False
#关闭串口
def closeSerialPort():
global serial_State
try:
serial_receive_stop_event.set() # 请求线程停止
ser.close()
serial_State = 0
return True
except Exception as e:
print("ERROR close serial port fail, error code = ", e)
#关闭有错误直接终止程序
exit(1)
return False
#发送数据
def sendData(data):
#用串口库自己的isOpen接口做二重保护
if ser.isOpen():
ser.write(data.encode())
print(f'Send: {data}')
else:
print('ERROR cannot send. Serial port is closed.')
#接收数据
def receiveData():
while not serial_receive_stop_event.is_set():
try:
data = ser.readline()
if data:
print(f"receive data: {data}")
#串口收到的数据是byte类型的字节串,不是字符串(string),需要转一下
data_str = data.decode('utf-8')
myProtocol.parseReceivedAtMsg(data_str)
except Exception as e:
print(f"ERROR receiving data fail, error code: {e}")
time.sleep(0.1) # 防止CPU占用过高
4.3.4 myProtocol
myProtocol中放置串口协议先关代码,组装发送的串口数据、解析接收到的串口数据
解析函数
#解析接收到的AT指令
def parseReceivedAtMsg(receiveData):
# 检查指令的完整性
at_prefix = "AT+"
crlf_suffix = "\r\n"
if not receiveData.startswith(at_prefix) or not receiveData.endswith(crlf_suffix):
print("错误:指令不完整或格式错误")
return
# 分割指令和数据
cmd_end_index = receiveData.find('=')
if cmd_end_index == -1: # 确保找到了等号
print("错误:未找到等号分隔符")
return
cmd = receiveData[3:cmd_end_index]
# 提取data部分,先移除结尾的换行符,然后提取等号后面的内容
data_start_index = cmd_end_index + 1 # 等号后第一个字符的索引
data = receiveData[data_start_index:-2] # 移除结尾的'\r\n'
# 根据不同的指令调用对应的处理函数
if cmd == "DataReport":
parseDataReport(data)
elif cmd == "ChangeSelectSensor":
parseChangeSelectSensor(data)
elif cmd == "ChangeWorkSensor":
parseChangeWorkSensor(data)
else:
print(f"未知的AT指令,cmd = {cmd}")
三条指令的解析函数
#处理AT+DataReport指令的解析函数
def parseDataReport(data):
# print(f"parse_data_report,data = {data}")
#提取传感器序号
sensorID = data[0]
#提取传感器数据部分
sensorData = data[2:]
if sensorID == "1":
parseDataReport_Sensor1(sensorData)
elif sensorID == "2":
parseDataReport_Sensor2(sensorData)
elif sensorID == "3":
parseDataReport_Sensor3(sensorData)
elif sensorID == "4":
parseDataReport_Sensor4(sensorData)
elif sensorID == "5":
parseDataReport_Sensor5(sensorData)
elif sensorID == "6":
parseDataReport_Sensor6(sensorData)
elif sensorID == "7":
parseDataReport_Sensor7(sensorData)
else:
print(f"AT+DataReport 传感器序号部分解析错误,解析到数据为{sensorID}")
#处理AT+ChangeSelectSensor指令的解析函数
def parseChangeSelectSensor(data):
# print(f"parse_change_select_sensor,data = {data}")
now_select_sensor = gui.sensor_select_combobox.get()
sensorID = getSensorIdfromSensorListName(now_select_sensor)
if data == "+1":
if sensorID == 7:
sensorID = 0
else:
sensorID = sensorID + 1
elif data == "-1":
if sensorID == 0:
sensorID = 7
else:
sensorID = sensorID - 1
gui.changeSensorSelectComboboxShowData(sensorID)
#处理AT+ChangeWorkSensor指令的解析函数
def parseChangeWorkSensor(data):
try:
sensorId = int(data)
except ValueError as e:
print("错误:", e)
print("AT+ChangeWorkSensor 传感器序号部分解析错误,不是数字")
return
if 0 <= sensorId <= 7:
gui.changeSensorSelectComboboxShowData(sensorId)
else:
print(f"AT+ChangeWorkSensor 传感器序号部分不在0-7之间,解析到数据为{sensorId}")
发出的指令的组装函数
def buildSelectWorkSensorAtMsg(sensor):
sensorID = getSensorIdfromSensorListName(sensor)
command = f"AT+SelectWorkSensor={sensorID}\r\n"
return command
五.功能展示
详见视频
六.心得
很高兴可以顺利完成本次项目,ST的开发板我玩过很多,他的软件生态做的很棒,文档也很详细,虽然我是老手,但是一些新手也是可以快速上手的。本次项目对我来说最大的挑战是上位机的制作,以前学习过QT,但是QT在商业上的限制,导致我没有继续学习,之后一直想用python配合GUI库,例如GTK、Tkinter等,学习一下写个上位机,这样既学习了python又学习了这些GUI库,但是一直没有动力去学习。看到本次活动的任务中有上位机的制作,正好又是我熟悉的ST,我果断报名了。在这期间我动力满满,学习python和Tkinter,经过本次活动,很高兴我又掌握了一门新的技术。最后感谢硬禾学堂与得捷电子举办的funpack活动,给大家提供了这样一次免费学习的机会。