news 2026/4/16 15:58:04

FreeRtos之按键检测

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
FreeRtos之按键检测

一、输入子系统结构体设计

在这个项目中,有使用一个按键,仿照高手代码进行编程,抽象对应的结构体如下

input_system.h

#ifndef __INPUT_SYSTEM_H #define __INPUT_SYSTEM_H #ifndef NULL #define NULL (void *)0 #endif #define TIME_T int #define INPUT_BUF_LEN 20 /* 事件类型 */ typedef enum { INPUT_EVENT_TYPE_KEY, INPUT_EVENT_TYPE_TOUCH, INPUT_EVENT_TYPE_NET, INPUT_EVENT_TYPE_STDIO } INPUT_EVENT_TYPE; /* 按键状态*/ typedef enum { KEY_STATE_PRESSED, /* 按下 */ KEY_STATE_RELEASED, /* 弹起 */ KEY_STATE_LONG_PRESS, /* 长按 */ KEY_STATE_LONG_RELEASED, /* 长按弹起 */ KEY_STATE_REPEAT, /* 长按连发 */ KEY_STATE_DOUBLE_CLICK, /* 双击 */ KEY_STATE_MULTI_CLICK /* 多击 */ } KEY_STATE; /* 输入事件结构体扩展 */ typedef struct InputEvent { TIME_T time; /* 事件时间戳 */ INPUT_EVENT_TYPE eType; /* 事件类型 */ /* 通用事件数据 */ union { /* 按键事件数据 */ struct { int iKey; /* 按键代码 */ KEY_STATE eState; /* 按键状态 */ int iDuration; /* 持续时间(ms),用于长按判断 */ int iClickCount; /* 点击次数,用于多击判断 */ } key; /* 触摸事件数据 */ struct { int iX; int iY; int iPressure; } touch; /* 网络事件数据 */ struct { int iEventCode; char strData[INPUT_BUF_LEN]; } net; /* 标准输入事件数据 */ struct { char strInput[INPUT_BUF_LEN]; } stdio; } data; } InputEvent, *PInputEvent; typedef struct InputDevice { char *name; int (*GetInputEvent)(PInputEvent ptInputEvent); int (*DeviceInit)(void); int (*DeviceExit)(void); struct InputDevice *pNext; } InputDevice, *PInputDevice; /********************************************************************** * 函数名称: AddInputDevices * 功能描述: 注册多个输入设备 * 输入参数: 无 * 输出参数: 无 * 返 回 值: 无 ***********************************************************************/ void AddInputDevices(void); /********************************************************************** * 函数名称: InitInputDevices * 功能描述: 初始化所有的输入设备 * 输入参数: 无 * 输出参数: 无 * 返 回 值: 无 ***********************************************************************/ void InitInputDevices(void); /********************************************************************** * 函数名称: InputDeviceRegister * 功能描述: 注册一个输入设备 * 输入参数: ptInputDevice-输入设备 * 输出参数: 无 * 返 回 值: 无 ***********************************************************************/ void InputDeviceRegister(PInputDevice ptInputDevice); #endif /* __INPUT_SYSTEM_H */

使用共用体设计

在上述的代码片段中,使用了union共用体进行设计,主要原因是节省内存空间,

输入事件在某个时刻只能是一种类型: 不可能同时是按键事件和触摸事件

不可能同时是网络事件和标准输入事件 这种互斥性非常适合使用共用体,因为同一时间只需要存储一种事件的数据。

二、输入子系统按键驱动测试

1、硬件原理图

设计思路:找到自己积累的按键驱动代码,进行移植即可,移植过程略。设计一些c函数,代码片段如下:

2、扫描任务

/* ********************************************************************************************************* * 函 数 名: key_scan_task * 功能说明: 按键扫描任务,每10ms调用一次bsp_KeyScan10ms * 形 参: pvParameters - 任务参数 * 返 回 值: 无 ********************************************************************************************************* */ static void key_scan_task(void *pvParameters) { while (1) { /* 每10ms调用一次按键扫描函数 */ bsp_KeyScan10ms(); /* 处理按键事件 */ key_event_process(); /* 延时10ms */ vTaskDelay(10); } }

3、按键处理部分

/* ********************************************************************************************************* * 函 数 名: key_event_process * 功能说明: 处理按键事件,转换为InputEvent并发送到队列 * 形 参: 无 * 返 回 值: 无 ********************************************************************************************************* */ static void key_event_process(void) { uint8_t key_code; InputEvent event; /* 获取按键事件 */ key_code = bsp_GetKey(); if (key_code != KEY_NONE) { /* 填充InputEvent结构 */ event.time = xTaskGetTickCount(); event.eType = INPUT_EVENT_TYPE_KEY; /* 根据按键代码解析按键信息 */ switch (key_code) { case KEY_1_DOWN: event.data.key.iKey = 1; event.data.key.eState = KEY_STATE_PRESSED; event.data.key.iTime = xTaskGetTickCount(); event.data.key.iClickCount = 0; break; case KEY_1_UP: event.data.key.iKey = 1; event.data.key.eState = KEY_STATE_RELEASED; event.data.key.iTime = xTaskGetTickCount(); event.data.key.iClickCount = 1; break; case KEY_1_LONG_DOWN: event.data.key.iKey = 1; event.data.key.eState = KEY_STATE_LONG_PRESS; event.data.key.iTime = xTaskGetTickCount(); event.data.key.iClickCount = 0; break; case KEY_1_LONG_UP: event.data.key.iKey = 1; event.data.key.eState = KEY_STATE_LONG_RELEASED; event.data.key.iTime = xTaskGetTickCount(); event.data.key.iClickCount = 0; break; case KEY_1_AUTO_UP: event.data.key.iKey = 1; event.data.key.eState = KEY_STATE_REPEAT; event.data.key.iTime = xTaskGetTickCount(); event.data.key.iClickCount = 0; break; case KEY_1_DB_UP: event.data.key.iKey = 1; event.data.key.eState = KEY_STATE_DOUBLE_CLICK; event.data.key.iTime = xTaskGetTickCount(); event.data.key.iClickCount = 2; break; default: return; } /* 发送事件到队列 */ xQueueSend(xKeyQueue, &event, 0); } }

4、接收队列

/* ********************************************************************************************************* * 函 数 名: input_test_task * 功能说明: 按键测试任务 * 形 参: 无 * 返 回 值: 无 ********************************************************************************************************* */ static void input_test_task(void *pvParameters) { InputEvent event; while (1) { /* 从队列中获取按键事件 */ if (xQueueReceive(xKeyQueue, &event, portMAX_DELAY) == pdTRUE) { /* 打印按键事件信息 */ DBG_log("[INFO] Key event: type=%d, key=%d, state=%d, iTime=%d, click_count=%d\n", event.eType, event.data.key.iKey, event.data.key.eState, event.data.key.iTime, event.data.key.iClickCount); } } }

5、函数入口

/* ********************************************************************************************************* * 函 数 名: input_test_start * 功能说明: 启动按键扫描任务 * 形 参: 无 * 返 回 值: 无 ********************************************************************************************************* */ void input_test_start(void) { BaseType_t xReturn = pdPASS; /* 初始化按键测试 */ input_test_init(); /* 创建按键扫描任务 */ xReturn = xTaskCreate( (TaskFunction_t)key_scan_task, (const char *)"key_scan", (uint16_t)128, (void *)NULL, (UBaseType_t)1, (TaskHandle_t *)&xKeyScanTask ); if (xReturn != pdPASS) { DBG_log("[ERROR] Create key scan task failed\n"); return; } /* 创建按键测试任务 */ xReturn = xTaskCreate( (TaskFunction_t)input_test_task, (const char *)"input_test", (uint16_t)128, (void *)NULL, (UBaseType_t)2, (TaskHandle_t *)&xInputTestTask ); if (xReturn != pdPASS) { DBG_log("[ERROR] Create input test task failed\n"); return; } DBG_log("[INFO] Input test started\n"); }

实验结果

可以看到,按键驱动可以正常驱动起来,日志比较正常。

总结

本文设计了一个按键扫描任务,将按键的键值放进队列中,在接收队列中拿出来,进行处理,算是一个freertos的队列实战。

三、按键输入事件管理系统设计与实现

设计思路如下,从输入事件得到数据,将数据放入对应的buffer进行管理,方便供上层代码进行调用管理。

1、系统架构

按键输入事件管理系统采用分层设计,主要包括以下几个层次:

  1. 底层驱动层:负责硬件按键的扫描和状态检测

  2. 设备抽象层:将硬件按键抽象为输入设备

  3. 输入系统层:管理所有输入设备,提供统一的事件获取接口

  4. 应用层:从输入系统获取事件并进行处理

2、核心组件

2.1 输入事件结构体

input_system.h中定义了InputEvent结构体,用于表示各种输入事件:

typedef struct InputEvent { TIME_T time; /* 事件时间戳 */ INPUT_EVENT_TYPE eType; /* 事件类型 */ /* 通用事件数据 */ union { /* 按键事件数据 */ struct { int iKey; /* 按键代码 */ KEY_STATE eState; /* 按键状态 */ int iTime; /* 时间ms */ int iClickCount; /* 点击次数,用于多击判断 */ } key; /* 触摸事件数据 */ struct { int iX; int iY; int iPressure; } touch; /* 网络事件数据 */ struct { int iEventCode; char strData[INPUT_BUF_LEN]; } net; /* 标准输入事件数据 */ struct { char strInput[INPUT_BUF_LEN]; } stdio; } data; } InputEvent, *PInputEvent;

2.2 输入设备结构体

input_system.h中定义了InputDevice结构体,用于表示输入设备:

typedef struct InputDevice { char *name; /* 设备名称 */ int (*GetInputEvent)(PInputEvent ptInputEvent); /* 获取输入事件的函数 */ int (*DeviceInit)(void); /* 设备初始化函数 */ int (*DeviceExit)(void); /* 设备退出函数 */ struct InputDevice *pNext; /* 指向下一个设备的指针 */ } InputDevice, *PInputDevice;

2.3 输入缓冲区

input_system.c中实现了一个环形缓冲区,用于存储输入事件:

#define INPUT_BUFFER_SIZE 10 static InputEvent g_tInputBuffer[INPUT_BUFFER_SIZE]; static volatile int g_iInputBufferRead = 0; static volatile int g_iInputBufferWrite = 0;

3. 调用流程

3.1 系统初始化

  1. 调用AddInputDevices()注册所有输入设备

  2. 调用InitInputDevices()初始化所有输入设备

  3. 创建key_scan_task任务,每10ms进行一次按键扫描

3.2 按键扫描与事件处理

  1. key_scan_task每10ms调用一次bsp_KeyScan10ms()进行按键扫描

  2. bsp_KeyScan10ms()检测按键状态并将按键事件放入按键FIFO

  3. 调用GetInputEvent()从所有输入设备中获取输入事件

  4. 调用InputBufferPut()将输入事件放入输入缓冲区

3.3 事件获取与处理

  1. 应用层调用InputBufferGet()从输入缓冲区中获取输入事件

  2. 根据事件类型和事件数据进行相应的处理

4. 代码实现

4.1 输入系统层实现

4.1.1 设备注册
/********************************************************************** * 函数名称: InputDeviceRegister * 功能描述: 注册一个输入设备(头插法) ***********************************************************************/ void InputDeviceRegister(PInputDevice ptInputDevice) { if (ptInputDevice == NULL) return; ptInputDevice->pNext = g_ptInputDevices; g_ptInputDevices = ptInputDevice; } /********************************************************************** * 函数名称: AddInputDevices * 功能描述: 注册所有需要的输入设备 ***********************************************************************/ void AddInputDevices(void) { /* 注册按键设备 */ extern void AddInputDeviceKey(void); AddInputDeviceKey(); /* 未来扩展:触摸、网络等设备 */ // extern void AddInputDeviceTouch(void); // AddInputDeviceTouch(); }
4.1.2 设备初始化
/********************************************************************** * 函数名称: InitInputDevices * 功能描述: 初始化所有已注册的输入设备 ***********************************************************************/ void InitInputDevices(void) { PInputDevice pDev = g_ptInputDevices; while (pDev) { if (pDev->DeviceInit != NULL) { pDev->DeviceInit(); } pDev = pDev->pNext; } }
4.1.3 事件获取
/********************************************************************** * 函数名称: GetInputEvent * 功能描述: 从所有输入设备中获取输入事件 * 输入参数: ptInputEvent - 输入事件指针 * 输出参数: 无 * 返 回 值: 0 - 成功,非0 - 失败 ***********************************************************************/ int GetInputEvent(PInputEvent ptInputEvent) { PInputDevice pDev = g_ptInputDevices; while (pDev) { if (pDev->GetInputEvent != NULL) { if (pDev->GetInputEvent(ptInputEvent) == 0) { return 0; } } pDev = pDev->pNext; } return -1; }
4.1.4 输入缓冲区管理
/********************************************************************** * 函数名称: InputBufferPut * 功能描述: 向输入缓冲区中放入一个输入事件 * 输入参数: ptInputEvent - 输入事件指针 * 输出参数: 无 * 返 回 值: 0 - 成功,非0 - 失败 ***********************************************************************/ int InputBufferPut(PInputEvent ptInputEvent) { int iNextWrite = (g_iInputBufferWrite + 1) % INPUT_BUFFER_SIZE; if (iNextWrite == g_iInputBufferRead) { /* 缓冲区已满 */ return -1; } g_tInputBuffer[g_iInputBufferWrite] = *ptInputEvent; g_iInputBufferWrite = iNextWrite; return 0; } /********************************************************************** * 函数名称: InputBufferGet * 功能描述: 从输入缓冲区中获取一个输入事件 * 输入参数: ptInputEvent - 输入事件指针 * 输出参数: 无 * 返 回 值: 0 - 成功,非0 - 失败 ***********************************************************************/ int InputBufferGet(PInputEvent ptInputEvent) { if (g_iInputBufferRead == g_iInputBufferWrite) { /* 缓冲区为空 */ return -1; } *ptInputEvent = g_tInputBuffer[g_iInputBufferRead]; g_iInputBufferRead = (g_iInputBufferRead + 1) % INPUT_BUFFER_SIZE; return 0; } /********************************************************************** * 函数名称: InputBufferClear * 功能描述: 清空输入缓冲区 * 输入参数: 无 * 输出参数: 无 * 返 回 值: 无 ***********************************************************************/ void InputBufferClear(void) { g_iInputBufferRead = 0; g_iInputBufferWrite = 0; }

4.2 设备抽象层实现

4.2.1 按键设备初始化
static int GPIOKeyInit(void) { KAL_GPIOKkeyInit(); /* 创建按键事件队列 */ xKeyQueue = xQueueCreate(10, sizeof(InputEvent)); if (xKeyQueue == NULL) { return -1; } /* 初始化按键驱动 */ bsp_InitKey(); return 0; }
4.2.2 按键事件获取
static int GPIOKeyGetInputEvent(PInputEvent ptInputEvent) { uint8_t key_code; InputEvent event; /* 获取按键事件 */ key_code = bsp_GetKey(); if (key_code != KEY_NONE) { /* 填充InputEvent结构 */ event.time = xTaskGetTickCount(); event.eType = INPUT_EVENT_TYPE_KEY; /* 根据按键代码解析按键信息 */ switch (key_code) { case KEY_1_DOWN: event.data.key.iKey = 1; event.data.key.eState = KEY_STATE_PRESSED; event.data.key.iTime = event.time; event.data.key.iClickCount = 0; break; case KEY_1_UP: event.data.key.iKey = 1; event.data.key.eState = KEY_STATE_RELEASED; event.data.key.iTime = event.time; event.data.key.iClickCount = 1; break; case KEY_1_LONG_DOWN: event.data.key.iKey = 1; event.data.key.eState = KEY_STATE_LONG_PRESS; event.data.key.iTime = event.time; event.data.key.iClickCount = 0; break; case KEY_1_LONG_UP: event.data.key.iKey = 1; event.data.key.eState = KEY_STATE_LONG_RELEASED; event.data.key.iTime = event.time; event.data.key.iClickCount = 0; break; case KEY_1_AUTO_UP: event.data.key.iKey = 1; event.data.key.eState = KEY_STATE_REPEAT; event.data.key.iTime = event.time; event.data.key.iClickCount = 0; break; case KEY_1_DB_UP: event.data.key.iKey = 1; event.data.key.eState = KEY_STATE_DOUBLE_CLICK; event.data.key.iTime = event.time; event.data.key.iClickCount = 2; break; default: return -1; } /* 复制事件到输出参数 */ *ptInputEvent = event; return 0; } return -1; }
4.2.3 按键设备注册
static InputDevice g_tKeyDevice = { "gpio_key", GPIOKeyGetInputEvent, GPIOKeyInit, NULL, }; void AddInputDeviceKey(void) { InputDeviceRegister(&g_tKeyDevice); }

4.3 应用层实现

4.3.1 按键扫描任务
/********************************************************************** * 函数名称: key_scan_task * 功能描述: 按键扫描任务,每10ms调用一次bsp_KeyScan10ms * 输入参数: pvParameters - 任务参数 * 输出参数: 无 * 返 回 值: 无 ***********************************************************************/ void key_scan_task(void *pvParameters) { InputEvent event; while (1) { /* 每10ms调用一次按键扫描函数 */ bsp_KeyScan10ms(); /* 获取按键事件并放入输入缓冲区 */ if (GetInputEvent(&event) == 0) { InputBufferPut(&event); } /* 延时10ms */ vTaskDelay(10); } }
4.3.2 输入测试任务
static void input_test_task(void *pvParameters) { InputEvent event; while (1) { /* 从队列中获取按键事件 */ if (xQueueReceive(xKeyQueue, &event, portMAX_DELAY) == pdTRUE) { /* 打印按键事件信息 */ DBG_log("[INFO] Key event: type=%d, key=%d, state=%d, iTime=%d, click_count=%d\n", event.eType, event.data.key.iKey, event.data.key.eState, event.data.key.iTime, event.data.key.iClickCount); } } }
4.3.3 输入系统单元测试
/********************************************************************** * 函数名称: input_test * 功能描述: 输入系统单元测试函数 * 输入参数: 无 * 输出参数: 无 * 返 回 值: 无 ***********************************************************************/ void input_test(void) { InputEvent event; AddInputDevices(); InitInputDevices(); DBG_log("Input system unit test start...\n"); while (1) { if (GetInputEvent(&event) == 0) { /* 打印按键事件信息 */ DBG_log("[INFO] Key event: type=%d, key=%d, state=%d, iTime=%d, click_count=%d\n", event.eType, event.data.key.iKey, event.data.key.eState, event.data.key.iTime, event.data.key.iClickCount); /* 将事件放入输入缓冲区 */ if (InputBufferPut(&event) == 0) { DBG_log("[INFO] Event put into buffer success\n"); } else { DBG_log("[ERROR] Event put into buffer failed\n"); } } /* 尝试从输入缓冲区获取事件 */ if (InputBufferGet(&event) == 0) { DBG_log("[INFO] Event get from buffer: type=%d, key=%d, state=%d\n", event.eType, event.data.key.iKey, event.data.key.eState); } /* 延时10ms */ vTaskDelay(10); } }

5. 使用方法

5.1 初始化输入系统

/* 注册并初始化输入设备 */ AddInputDevices(); InitInputDevices(); /* 创建按键扫描任务 */ xTaskCreate( key_scan_task, "key_scan", 128, NULL, 1, &xKeyScanTask );

5.2 获取并处理输入事件

/* 从输入缓冲区中获取输入事件 */ InputEvent event; if (InputBufferGet(&event) == 0) { /* 根据事件类型进行处理 */ switch (event.eType) { case INPUT_EVENT_TYPE_KEY: /* 处理按键事件 */ printf("Key event: key=%d, state=%d\n", event.data.key.iKey, event.data.key.eState); break; /* 处理其他类型的事件... */ } }

6. 扩展建议

  1. 添加更多输入设备:可以添加触摸屏幕、网络输入等其他类型的输入设备

  2. 优化输入缓冲区:可以根据实际需求调整输入缓冲区的大小

  3. 添加事件过滤:可以添加事件过滤机制,过滤掉不需要的事件

  4. 添加事件回调:可以添加事件回调机制,当有输入事件时自动调用回调函数

  5. 添加事件优先级:可以为不同类型的事件添加优先级,优先处理重要的事件

7. 总结

按键输入事件管理系统采用分层设计,通过输入设备抽象和输入缓冲区管理,提供了一个统一、高效的输入事件处理接口。系统支持多种按键状态的检测,包括按下、弹起、长按、双击等,可以满足各种应用场景的需求。

四、任务优先级优化划分

前言:在本次开发空气检测仪项目中,划分了一些任务,有些中任务优先级没处理好,故本篇文章进行记录与修复下。

1. 现象分析

uart_test任务未能运行,怀疑是优先级设置不当导致。 原系统所有任务(KeyScan, InputTest, UartTest, LogPrint)优先级均为2,且LedTest1。 虽然理论上开启时间片轮转(Time Slicing)后同优先级任务应轮流执行,但若配置缺失或某些任务占用过多(尽管检查发现都有 Delay),可能导致响应迟缓或饿死风险。

2. 优先级重新规划

为了保证系统响应性和实时性,采用Rate Monotonic Scheduling (RMS)的思想,按照任务的重要性和实时性要求重新分配优先级。

FreeRTOS 优先级范围: 0 (Low) - 4 (High) [configMAX_PRIORITIES = 5]

任务名称

原优先级

新优先级

说明

KeyScan

2

4

(最高)

硬件扫描任务,需要严格的 10ms 周期,抖动会影响按键体验。

uart_test

2

3

(高)

通信任务,需要及时响应接收中断和处理数据,避免缓冲区溢出。

input_test

2

2

(中)

业务逻辑/事件处理,对实时性要求稍低,只要能处理完 buffer 即可。

LogPrint

2

1

(低)

后台日志打印,不应抢占业务资源。

LedTest

1

1

(低)

后台状态指示,低优先级。

3. 修改记录

3.1 FreeRTOSConfig.h

  • 显式定义#define configUSE_TIME_SLICING 1,确保同优先级任务(如 LogPrint 和 LedTest)能公平共享 CPU。

3.2 User/main.c

  • vTaskLogPrint优先级从 2 降为1

3.3 uart_test.c

  • uart_test_task优先级从 2 升为3

3.4 gpio_key.c

  • vTaskKeyScan优先级从 2 升为4

3.5 input_test.c

  • 保持input_test_task优先级为2(或明确注释为 Medium)。

4. 预期效果

  • KeyScan将获得最高优先权,确保按键扫描不丢失。

  • uart_test其次,保证串口通信流畅。

  • input_test在空闲时处理事件。

  • LogPrintLedTest在系统空闲时运行,不会影响关键业务。

这种梯队设计能有效避免关键任务被饿死,同时保证系统的实时响应能力。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/16 10:15:30

交换拓扑:企业如何设计满足能力设定、配置统一与资源公平分配的拓扑

设定交换能力基准、统一配置管理流程、落实互联网资源分配原则 摘要 针对企业IT部门、信息化负责人及运维团队,通过标准化交换拓扑设计与配套管理机制,支撑系统规划、标准化交付与平台化运维,实现高确定性的ICT基础设施管理,降低…

作者头像 李华
网站建设 2026/4/16 10:20:30

利用5-FAM Maleimide,787632-00-2进行生物分子标记与成像分析

基本信息 英文名称:5-FAM Maleimide;5-FAM Mal;5-Carboxyfluorescein-MAL 中文名称:5-FAM马来酰亚胺;5-羧基荧光素-马来酰亚胺 CAS号:787632-00-2 分子式:C27H18N2O8 分子量:49…

作者头像 李华
网站建设 2026/4/16 10:17:29

Android Jetpack Compose 开发问题:无法使用 HorizontalUncontainedCarousel

在 Android 开发中,使用 Jetpack Compose 时,无法使用 HorizontalUncontainedCarousel,即 HorizontalUncontainedCarousel 不存在 问题原因 HorizontalUncontainedCarousel 是较新的版本的 material3 中的 API 处理策略 将 material3 升级到较…

作者头像 李华
网站建设 2026/4/16 10:14:43

开发改 bug 改到秃太苦了!转网安月薪翻倍不用加班,悔哭了

开发改bug改到秃?这行转网安,月薪翻倍不用加班,我后悔没早转! 前言 作为一个从开发转岗到网安的老兵,我经常被以前的同事问:“天天跟代码较劲,写那些没人用的功能,有意思吗&#x…

作者头像 李华
网站建设 2026/4/15 12:34:32

第八届传智杯场景环境艺术设计挑战赛练习题库(一)

1.[单选] 在AIGC辅助场景环境艺术设计中,以下哪种数据格式常用于存储设计方案? ( 1分 ) 得分:0分 .jpg .docx .pdf .3ds未选 正确答案.3ds 答案解析.3ds是三维模型数据格式,常用于存储场景环境设计方案…

作者头像 李华