STM32-DMA数据转运


**注:**DMA对应的库函数文件讲解


**DMA_GetITStatus(uint32_t DMAy_IT) 是一个用于检查DMA(直接存储器访问)中断状态的库函数。**它通常在使用STM32系列微控制器及其标准外设库时被调用。此函数的主要作用是返回指定DMA通道的特定中断标志的状态,以帮助开发者确定是否发生了特定类型的DMA事件。

参数 uint32_t DMAy_IT 用来指定你想要检查的DMA中断类型和对应的DMA流或通道。这个参数是一个组合值,通常由两个部分组成:

  1. DMAx:指明哪个DMA控制器(例如DMA1或DMA2),因为一些STM32芯片可能有多个DMA控制器。
  2. IT(Interrupt Type):指明具体的中断类型,比如传输完成(Transfer Complete, TC)、半传输完成(Half Transfer, HT)、传输错误(Transfer Error, TE)等。

函数会返回一个位标志,表明所选中断状态是设置(即事件发生)还是清除(即事件未发生)。这对于编写中断服务程序(ISR)非常重要,因为在ISR中你需要知道是什么类型的事件触发了中断,以便可以适当地处理它。

例如,如果你正在等待DMA传输完成中断,你可以使用 DMA_GetITStatus 来检查该中断是否已经发生。如果函数返回值表示中断已被设置,那么你可以安全地假设DMA传输已完成,并且可以继续执行后续的操作,如启动新的DMA传输、处理接收到的数据等。


**DMA_ClearITPendingBit(uint32_t DMAy_IT) 是一个用于清除DMA(直接存储器访问)中断挂起位的库函数。**在STM32系列微控制器中,当DMA传输过程中发生特定事件(如传输完成、半传输完成或传输错误),DMA硬件会设置相应的中断标志位,并可能触发中断请求。

然而,一旦这些事件被软件处理后,就需要清除相应的中断标志位,以确保相同的中断不会再次被误触发。 这就是 DMA_ClearITPendingBit 函数的作用:它允许你手动清除指定DMA通道的特定中断挂起位,表明该事件已经被处理完毕。

参数 uint32_t DMAy_IT 用来指定要清除的DMA中断类型和对应的DMA流或通道。这个参数是一个组合值,通常由两个部分组成:

  1. DMAx:指明哪个DMA控制器(例如DMA1或DMA2)。
  2. IT(Interrupt Type):指明具体的中断类型,比如传输完成(Transfer Complete, TC)、半传输完成(Half Transfer, HT)、传输错误(Transfer Error, TE)等。

使用此函数是确保DMA中断系统正确工作的关键步骤之一。如果不清除这些标志位,可能会导致中断不断重复触发,或者新的相同类型的中断无法被正确识别。因此,在你的中断服务程序(ISR)中,你应该在检查并响应了某个DMA事件之后调用 DMA_ClearITPendingBit 来清除对应的中断挂起位。


定义:


DMA(Direct Memory Access) 直接存储器存取 DMA可以提供外设和存储器或者存储器和存储器之间的高速数据传输,无须CPU干预,节省了CPU的资源 12个独立可配置的通道: DMA1(7个通道), DMA2(5个通道) 每个通道都支持软件触发和特定的硬件触发 STM32F103C8T6 DMA资源:DMA1(7个通道)


SRAM可以读也可以写



DMA基本结构




1.0 数据宽度与对齐


在DMA中是如何解决数据宽度不一致的问题的,如果源端的数据大于目标端的数据,那么将读取出来的高位舍弃掉,然后只取低位。


2.0 ADC与DMA数据转运

连续扫描转运模式


3.0 手册解读



4.0 程序实现


注:本次程序主要实现的是DMA从存储器到存储器的数据转运,使用软件触发的方式

4.0.1 DMA初始化

初始化:主要包括RCC时钟初始化,GPIO初始化

函数初始化实现

cpp 复制代码
#include "stm32f10x.h" // Device header

uint16_t MyDMA_Size;

/**
 * @brief  DMA初始化,包括时钟等
 * @param  ADDRA起始地址
 * @param  ADDRB结束地址
 * @param  SIZE大小
 * @return 无返回值
 */
void MyDMA_Init(uint32_t AddrA, uint32_t AddrB, uint16_t Size)
{
	MyDMA_Size = Size;

	// RCC时钟初始化
	RCC_APB1PeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);

	// DMA 结构体初始化
	DMA_InitTypeDef DMA_InitStructure;
	// 起始地址
	DMA_InitStructure.DMA_PeripheralBaseAddr = AddrA;
	// 数据大小
	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
	// 是否自增
	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable;
	// 接收地址【目的地址】
	DMA_InitStructure.DMA_MemoryBaseAddr = AddrB;
	// 接收地址数据大小
	DMA_InitStructure.DMA_MemoryDataSize = DMA_PeripheralDataSize_Byte;
	// 地址是否自增
	DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
	// 数据的传输方向,外设传输到存储器
	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
	// 重装计数器的大小
	DMA_InitStructure.DMA_BufferSize = Size;
	// DMA模式
	DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
	// 触发方式
	DMA_InitStructure.DMA_M2M = DMA_M2M_Enable;
	// DMA优先级,通道的优先级设置为中等
	DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;
	// 初始化DMA
	DMA_Init(DMA1_Channel1, &DMA_InitStructure);

	// 使能DMA,初始化后会立即工作,等后续手动调用后再开始
	DMA_Cmd(DMA1_Channel1, DISABLE);
}

4.0.2 数据转运函数

注:该函数的主要作用是,从新设置传输计数器的值

cpp 复制代码
/**
 * @brief  DMA数据传输,重置
 * @param  MULL
 * @param  MULL
 * @param  SIZE大小
 * @return 无返回值
 */
void MyDMA_Transfer(void)
{
	// DMA失能,在写入传输计数器之前,需要DMA暂停工作
	DMA_Cmd(DMA1_Channel1, DISABLE);
	// 写入传输计数器,指定将要转运的次数
	DMA_SetCurrDataCounter(DMA1_Channel1, MyDMA_Size);
	// 使能DMA
	DMA_Cmd(DMA1_Channel1, ENABLE);
	// 等待DMA工作完成,工作完成标志位设置为1
	while (DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);
	// 清除工作完成标志位
	DMA_ClearFlag(DMA1_FLAG_TC1);
}

4.0.3 全部程序

cpp 复制代码
#include "stm32f10x.h" // Device header

uint16_t MyDMA_Size;

/**
 * @brief  DMA初始化,包括时钟等
 * @param  ADDRA起始地址
 * @param  ADDRB结束地址
 * @param  SIZE大小
 * @return 无返回值
 */
void MyDMA_Init(uint32_t AddrA, uint32_t AddrB, uint16_t Size)
{
	MyDMA_Size = Size;

	// RCC时钟初始化
	RCC_APB1PeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);

	// DMA 结构体初始化
	DMA_InitTypeDef DMA_InitStructure;
	// 起始地址
	DMA_InitStructure.DMA_PeripheralBaseAddr = AddrA;
	// 数据大小
	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
	// 是否自增
	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable;
	// 接收地址【目的地址】
	DMA_InitStructure.DMA_MemoryBaseAddr = AddrB;
	// 接收地址数据大小
	DMA_InitStructure.DMA_MemoryDataSize = DMA_PeripheralDataSize_Byte;
	// 地址是否自增
	DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
	// 数据的传输方向,外设传输到存储器
	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
	// 重装计数器的大小
	DMA_InitStructure.DMA_BufferSize = Size;
	// DMA模式
	DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
	// 触发方式
	DMA_InitStructure.DMA_M2M = DMA_M2M_Enable;
	// DMA优先级,通道的优先级设置为中等
	DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;
	// 初始化DMA
	DMA_Init(DMA1_Channel1, &DMA_InitStructure);

	// 使能DMA,初始化后会立即工作,等后续手动调用后再开始
	DMA_Cmd(DMA1_Channel1, DISABLE);
}

/**
 * @brief  DMA数据传输,重置
 * @param  MULL
 * @param  MULL
 * @param  SIZE大小
 * @return 无返回值
 */
void MyDMA_Transfer(void)
{
	// DMA失能,在写入传输计数器之前,需要DMA暂停工作
	DMA_Cmd(DMA1_Channel1, DISABLE);
	// 写入传输计数器,指定将要转运的次数
	DMA_SetCurrDataCounter(DMA1_Channel1, MyDMA_Size);
	// 使能DMA
	DMA_Cmd(DMA1_Channel1, ENABLE);
	// 等待DMA工作完成,工作完成标志位设置为1
	while (DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);
	// 清除工作完成标志位
	DMA_ClearFlag(DMA1_FLAG_TC1);
}

4.0.4 头文件


cpp 复制代码
#ifndef __MYDMA_H
#define __MYDMA_H

void MyDMA_Init(uint32_t AddrA, uint32_t AddrB, uint16_t Size);
void MyDMA_Transfer(void);

#endif

4.0.5 main函数文件


cpp 复制代码
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "MyDMA.h"

uint8_t DataA[] = {0x01, 0x02, 0x03, 0x04}; // 定义测试数组DataA,为数据源
uint8_t DataB[] = {0, 0, 0, 0};				// 定义测试数组DataB,为数据目的地

int main(void)
{
	/*模块初始化*/
	OLED_Init(); // OLED初始化

	MyDMA_Init((uint32_t)DataA, (uint32_t)DataB, 4); // DMA初始化,把源数组和目的数组的地址传入

	/*显示静态字符串*/
	OLED_ShowString(1, 1, "DataA");
	OLED_ShowString(3, 1, "DataB");

	/*显示数组的首地址*/
	OLED_ShowHexNum(1, 8, (uint32_t)DataA, 8);
	OLED_ShowHexNum(3, 8, (uint32_t)DataB, 8);

	while (1)
	{
		DataA[0]++; // 变换测试数据
		DataA[1]++;
		DataA[2]++;
		DataA[3]++;

		Delay_ms(1000); // 延时1s,观察转运前的现象

		MyDMA_Transfer();

		OLED_ShowHexNum(2, 1, DataA[0], 2); // 显示数组DataA
		OLED_ShowHexNum(2, 4, DataA[1], 2);
		OLED_ShowHexNum(2, 7, DataA[2], 2);
		OLED_ShowHexNum(2, 10, DataA[3], 2);
		OLED_ShowHexNum(4, 1, DataB[0], 2); // 显示数组DataB
		OLED_ShowHexNum(4, 4, DataB[1], 2);
		OLED_ShowHexNum(4, 7, DataB[2], 2);
		OLED_ShowHexNum(4, 10, DataB[3], 2); // 使用DMA转运数组,从DataA转运到DataB

		OLED_ShowHexNum(2, 1, DataA[0], 2); // 显示数组DataA
		OLED_ShowHexNum(2, 4, DataA[1], 2);
		OLED_ShowHexNum(2, 7, DataA[2], 2);
		OLED_ShowHexNum(2, 10, DataA[3], 2);
		OLED_ShowHexNum(4, 1, DataB[0], 2); // 显示数组DataB
		OLED_ShowHexNum(4, 4, DataB[1], 2);
		OLED_ShowHexNum(4, 7, DataB[2], 2);
		OLED_ShowHexNum(4, 10, DataB[3], 2);

		Delay_ms(1000); // 延时1s,观察转运后的现象
	}
}

......


5.0 DMA多通道


5.0.1 RCC时钟初始化

cpp 复制代码
	// RCC 开启时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);  // 开启ADC1时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 开启GPIOA时钟
	RCC_APB2PeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);	  // 开启AHB的时钟

	RCC_ADCCLKConfig(RCC_PCLK2_Div6);

5.0.2 GPIO初始化

cpp 复制代码
	// GPIO初始化
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);

5.0.3 通道初始化

cpp 复制代码
	// GPIO初始化
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);

5.0.4 ADC结构体初始化

cpp 复制代码
	ADC_InitTypeDef ADC_InitStructure;									// ADC 初始化
	ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;					// ADC 独立模式
	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;				// 数据对齐方式右对齐
	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; // 外部触发模式,使用软件触发,而不使用硬件触发
	ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;					// 连续转换,每转换完一次之后立即开始下一次的转换,使能
	ADC_InitStructure.ADC_ScanConvMode = ENABLE;						// 使能扫描模式,扫描规则组前面的4个通道
	ADC_InitStructure.ADC_NbrOfChannel = 4;								// 通道数为4个扫描规则组前面的4个通道
	ADC_Init(ADC1, &ADC_InitStructure);									// ADC结构体初始化

5.0.5 DMA结构体初始化

cpp 复制代码
	// DMA初始化
	DMA_InitTypeDef DMA_InitStructure;
	// 起始地址,转运数据寄存器里面的数值
	DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;
	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
	DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)AD_Value;
	DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
	DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
	DMA_InitStructure.DMA_BufferSize = 4;
	DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
	DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
	DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;
	DMA_Init(DMA1_Channel1, & DMA_InitStructure);

5.0.6 DMA使能与校准

cpp 复制代码
	// DMA和ADC使能
	DMA_Cmd(DMA1_Channel1, ENABLE);			// DMA1通道使能
	ADC_DMACmd(ADC1, ENABLE);				// ADC1触发DMA1的信号使能
	ADC_Cmd(ADC1, ENABLE);					// ADC1使能

	// ADC校准
	ADC_ResetCalibration(ADC1);
	// 获取ADC转换标志位
	while (ADC_GetResetCalibrationStatus(ADC1) == SET);
	// 开启ADC转换
	ADC_StartCalibration(ADC1);
	// 获取ADC转换状态
	while (ADC_GetCalibrationStatus(ADC1) == SET);

	// ADC触发
	ADC_SoftwareStartConvCmd(ADC1, ENABLE); // 软件触发ADC开始工作,由于ADC处于连续转换模式,故触发一次后ADC就可以一直连续不断地工作

对应程序文件:

AD.C文件

cpp 复制代码
#include "stm32f10x.h" // Device header

uint16_t AD_Value[4]; // 定义用于存放AD转换结果的全局数组

void AD_Init(void)
{
	// RCC 开启时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);  // 开启ADC1时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 开启GPIOA时钟
	RCC_APB2PeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);	  // 开启AHB的时钟

	RCC_ADCCLKConfig(RCC_PCLK2_Div6);

	// GPIO初始化
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);

	// 配置规则组通道
	ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); // 规则组序列1的位置
	ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5); // 规则组序列2的位置
	ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_55Cycles5); // 规则组序列3的位置
	ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_55Cycles5); // 规则组序列4的位置

	ADC_InitTypeDef ADC_InitStructure;									// ADC 初始化
	ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;					// ADC 独立模式
	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;				// 数据对齐方式右对齐
	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; // 外部触发模式,使用软件触发,而不使用硬件触发
	ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;					// 连续转换,每转换完一次之后立即开始下一次的转换,使能
	ADC_InitStructure.ADC_ScanConvMode = ENABLE;						// 使能扫描模式,扫描规则组前面的4个通道
	ADC_InitStructure.ADC_NbrOfChannel = 4;								// 通道数为4个扫描规则组前面的4个通道
	ADC_Init(ADC1, &ADC_InitStructure);									// ADC结构体初始化

	// DMA初始化
	DMA_InitTypeDef DMA_InitStructure;
	// 起始地址,转运数据寄存器里面的数值
	DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;
	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
	DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)AD_Value;
	DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
	DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
	DMA_InitStructure.DMA_BufferSize = 4;
	DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
	DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
	DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;
	DMA_Init(DMA1_Channel1, & DMA_InitStructure);

	// DMA和ADC使能
	DMA_Cmd(DMA1_Channel1, ENABLE);			// DMA1通道使能
	ADC_DMACmd(ADC1, ENABLE);				// ADC1触发DMA1的信号使能
	ADC_Cmd(ADC1, ENABLE);					// ADC1使能

	// ADC校准
	ADC_ResetCalibration(ADC1);
	// 获取ADC转换标志位
	while (ADC_GetResetCalibrationStatus(ADC1) == SET);
	// 开启ADC转换
	ADC_StartCalibration(ADC1);
	// 获取ADC转换状态
	while (ADC_GetCalibrationStatus(ADC1) == SET);

	// ADC触发
	ADC_SoftwareStartConvCmd(ADC1, ENABLE); // 软件触发ADC开始工作,由于ADC处于连续转换模式,故触发一次后ADC就可以一直连续不断地工作
}

AD.H文件

cpp 复制代码
#ifndef __AD_H
#define __AD_H

extern uint16_t AD_Value[4];

void AD_Init(void);

#endif

MAIN.C文件

cpp 复制代码
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "AD.h"

int main(void)
{
	/*模块初始化*/
	OLED_Init();				//OLED初始化
	AD_Init();					//AD初始化
	
	/*显示静态字符串*/
	OLED_ShowString(1, 1, "AD0:");
	OLED_ShowString(2, 1, "AD1:");
	OLED_ShowString(3, 1, "AD2:");
	OLED_ShowString(4, 1, "AD3:");
	
	while (1)
	{
		OLED_ShowNum(1, 5, AD_Value[0], 4);		//显示转换结果第0个数据
		OLED_ShowNum(2, 5, AD_Value[1], 4);		//显示转换结果第1个数据
		OLED_ShowNum(3, 5, AD_Value[2], 4);		//显示转换结果第2个数据
		OLED_ShowNum(4, 5, AD_Value[3], 4);		//显示转换结果第3个数据
		
		Delay_ms(100);							//延时100ms,手动增加一些转换的间隔时间
	}
}

......

相关推荐
厉昱辰1 小时前
一文读懂单片机的串口
单片机·嵌入式硬件
沐欣工作室_lvyiyi1 小时前
基于STM32的智能生态水族箱系统设计(论文+源码)
stm32·单片机·嵌入式硬件·物联网·毕业设计·水族箱
7yewh2 小时前
自制红外热像仪(二) MLX90640移植 RP2040 STM32 ESP32
驱动开发·stm32·单片机·嵌入式硬件·mcu·计算机视觉
冰糖雪莲IO3 小时前
【江协STM32】10-2/3 MPU6050简介、软件I2C读写MPU6050
stm32·单片机·嵌入式硬件
1101 11013 小时前
STM32-笔记39-SPI-W25Q128
笔记·stm32·嵌入式硬件
Leiditech__3 小时前
汽车氛围灯静电浪涌的难点
嵌入式硬件·汽车·硬件工程
云山工作室5 小时前
基于单片机的客车载客状况自动检测系统(论文+源码)
单片机·嵌入式硬件·毕业设计·毕设
2301_805962937 小时前
NRF24L01模块STM32通信-发送端
stm32·单片机·嵌入式硬件
LeoZY_9 小时前
CH348结合开源ModBus协议组成串口温度采集服务器
运维·笔记·嵌入式硬件·开源
我想学LINUX10 小时前
【STM32+QT项目】基于STM32与QT的智慧粮仓环境监测与管理系统设计(完整工程资料源码)
stm32·嵌入式硬件·qt·毕业设计·课程设计·项目开发