STM32理论 —— DAC、DMA

1. DAC

1.1 内置DAC


void dac_init()		//DAC初始化
{
    GPIO_InitTypeDef GPIO_InitStructure;
    DAC_InitTypeDef DAC_InitStructure;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_AFIO,ENABLE);  // 使能引脚时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC,ENABLE);  // 使能DAC时钟

    GPIO_InitStructure.GPIO_Pin=GPIO_Pin_4 ;
    GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AIN;//模拟量输入
    GPIO_Init(GPIOA,&GPIO_InitStructure);
    GPIO_SetBits(GPIOA,GPIO_Pin_4 );//输出高


    DAC_InitStructure.DAC_Trigger=DAC_Trigger_None;//不使用触发功能
    DAC_InitStructure.DAC_WaveGeneration=DAC_WaveGeneration_None;//不使用三角波
    DAC_InitStructure.DAC_LFSRUnmask_TriangleAmplitude=DAC_LFSRUnmask_Bit0;  //屏蔽 幅值设置
    DAC_InitStructure.DAC_OutputBuffer=DAC_OutputBuffer_Disable;//关闭缓存

    DAC_Init(DAC_Channel_1,&DAC_InitStructure);//初始化DAC通道1

    DAC_Cmd(DAC_Channel_1,ENABLE);//使能DAC1_1

    DAC_SetChannel1Data(DAC_Align_12b_R,0);//12位 右对齐 写0数据,该函数定义于 stm32f10x_dac.c
    // 变量 DAC_Align_12b_R 定义于stm32f10x_dac.h
}
// 设置输出电压
void Dac1_Set_Vol(double vol)
{
    float temp=vol;
    temp/=1000;
    temp=temp*4096/3.3;
    DAC_SetChannel1Data(DAC_Align_12b_R,temp);
}

1.2 MCP4725


extern u8 IIC_Channel;
u8  MCP4725_Write_flag = 0;
u8  MCP4725_Flag = 0;

/***************************************************************************
** 函数名称   :   MCP4725_Write_DAC_EEPROM
** 功能描述   :  	向DAC芯片MCP4725的EEPROM写入数据
** 输入变量   :   
									IICChanel : IIC通道
									date      ;写入数据,范围为0~65535,对应十六进制0~0xFFFF,对应0到满量程
** 返 回 值   :  	无
** 最后修改人 :   xxx
** 最后更新日期:  20210224
** 说    明   :		无
***************************************************************************/
void MCP4725_Write_DAC_EEPROM(u8 n,u16 date)
{
	u8 temp;
	u8 DeviceAddr; // 芯片设备地址
	u16 t=0;
	// 选择芯片
	if(n==0){DeviceAddr= 0xc0;IIC_Channel=2;} 
	if(n==1){DeviceAddr= 0xc2;IIC_Channel=2;}
	//sprintfU4( " 1 \r\n"); // 调试代码
	do
	{
		IIC_Start();
		//sprintfU4( " 2 \r\n");
		IIC_WRITE_BYTE(DeviceAddr);//写从属地址

		if(IIC_Recelve_Ack()==0) // 第一次写入
		{
			IIC_WRITE_BYTE(0x60);
			if(IIC_Recelve_Ack()==0)
			{
				temp = date/256;
			//sprintfU4( "%d \r\n",temp);
				IIC_WRITE_BYTE(temp);
				//sprintfU4( "%d 1 \r\n",temp);
				if(IIC_Recelve_Ack()==0)
				{
					temp = date%256;
					IIC_WRITE_BYTE(temp);
				//	sprintfU4( "%d \r\n",temp);
					if(IIC_Recelve_Ack()==0) // 第二次写入
					{
						IIC_WRITE_BYTE(0x60);
						if(IIC_Recelve_Ack()==0)
						{
						temp = date/256;
						IIC_WRITE_BYTE(temp);
						//sprintfU4( "%d 3\r\n",temp);
						if(IIC_Recelve_Ack()==0)
						{
							temp = date%256;
							IIC_WRITE_BYTE(temp);
							if(IIC_Recelve_Ack()==0)
							{
							//sprintfU4( "%d 4\r\n",temp);
							MCP4725_Write_flag = 1; // MCP4725_Write_flag 写1,表示数据写入成功
							MCP4725_Flag=0;
							}
							else {MCP4725_Flag = 1;t++;}
						}
						else {MCP4725_Flag = 1;t++;}
					}
					else {MCP4725_Flag = 1;t++;}
				}
			 else {MCP4725_Flag = 1;t++;}
		  }
			else {MCP4725_Flag = 1;t++;}
			}
			else {MCP4725_Flag = 1;t++;}
		}
		else {MCP4725_Flag = 1;t++;}

	}		
while((MCP4725_Flag == 1)&&(t<800));	
	if(t>=800){MCP4725_Write_flag = 0;}	// MCP4725_Write_flag 写0,表示数据写入失败
	IIC_Stop();
	delay_Nus(250);
	delay_Nus(250);
	delay_Nus(250);
	delay_Nus(250);
}



1.3 可编程信号发生器 - AD9833

芯片共包含5个可编程寄存器:

  1. 1个16位控制寄存器
  2. 2个28位频率寄存器
  3. 2个12位相位寄存器

每次传输的数据以16位的方式加载到AD9833 .

1.3.1 控制寄存器

芯片唯一的16位控制寄存器(CR)描述如下:
在这里插入图片描述

  • DB15、DB14:选择频率、相位寄存器的写入模式,当DB15、DB14都为0时,表示不对频率、相位寄存器进行写操作,详见2.2 频率和相位寄存器
  • DB9、DB4、DB0:保持为0
  • DB13:对每一个频率寄存器都需要进行2次写操作。当B28=1时,每个频率寄存器都作为完整的28位使用,先写低14位,后写高14位,其中前2位说明写入的是哪个频率寄存器,01表示写入的是FREQ0寄存器,10表示写入的是FREQ1寄存器。当B28=0时,每个频率寄存器都作为2个14位寄存器,1个高14位,1个低14位,可相互独立更改,由控制寄存器的DB12(HLB)确定选择写入高14位还是低14位。
  • DB12:当DB13(B28)=1时,此位无效。当DB13(B28)=0时,若HLB=1,则允许选定寄存器的高14位进行写入,若HLB=0,则允许选定寄存器的低14位进行写入。
  • DB11:选择在相位累加器中使用FREQ0寄存器还是FREQ1寄存器,此位为0即FREQ0有效,为1即FREQ1有效。
  • DB10:选择在相位累加器中使用PHASE0 寄存器还是PHASE1 寄存器,此位为0即PHASE0 有效,为1即PHASE1 有效。
  • DB8:当Reset = 1时将内部寄存器重置为0. Reset =0表示禁用复位。在芯片输出8个 MCLK 周期后,复位完成,Reset重新被赋值为0.
  • DB7:当SLEEP1 = 1时,MCLK时钟被禁用,并且DAC输出保持在其当前值。当SLEEP1 = 0时,MCLK被启用。
  • DB6:当SLEEP12 = 1时,关闭芯片DAC。当AD9833用于输出DAC数据的MSB时,这是有用的。当SLEEP12 = 0时,开启芯片DAC .
  • DB5:这个位与位DB1(MODE)配合使用,用于控制VOUT引脚的输出。当OPBITEN = 1时,DAC的输出将不加载到VOUT 引脚,而DAC数据的MSB或MSB/2连接到VOUT引脚作为输出。这作为一个粗略的时钟源很有用。DIV2位控制输出的是MSB还是MSB/2。当OPBITEN = 0时,DAC接入VOUT. 模式位(DB1)决定输出是正弦信号还是三角波信号。
  • DB3:DIV2与DB5 (OPBITEN)配合使用。当DIV2 =1时,DAC数据的MSB直接传递到VOUT引脚作为输出。当DIV2 = 0时,DAC数据的MSB/2在VOUT引脚作为输出。
  • DB1:MODE与 DB5(OPBITEN)配合使用。当片上DAC连接到VOUT时,该位的功能是控制VOUT引脚的输出。如果控制位OPBITEN = 1,则该位应设置为0。否则,当MODE = 1时,SIN ROM被绕过,产生DAC的三角波输出。当MODE = 0时,用SIN ROM将相位信息转换为幅度信息,在输出端产生一个正弦波。

1.3.2 频率和相位寄存器

AD9833包含两个28位频率寄存器和两个12位相位寄存器。

寄存器大小描述
FREQ028 bits频率寄存器,当FSELECT位= 0时,该寄存器将输出频率定义为MCLK频率的一个分数。
FREQ128 bits当FSELECT= 1时,该寄存器将输出频率定义为MCLK频率的一个分数。
PHASE012 bits相位偏移寄存器,当PSELECT= 0时,该寄存器的内容被添加到相位累加器的输出。
PHASE112 bits当PSELECT= 1时,该寄存器的内容被添加到相位累加器的输出。

  • AD9833正弦波输出的计算公式为:
    在这里插入图片描述

其中:

  1. FREQREG为频率控制字,由频率寄存器FREQ0或FREQ1给定,其范围为0<=M<288-1
  2. f_MCLK为芯片MCK引脚上接入的晶振频率
  • AD9833正弦波输出的相位偏移计算公式为:
    在这里插入图片描述

其中:

  1. PHASEREG由相位寄存器PHASE0或PHASE1的给定

  • 写频率寄存器
    控制寄存器中的D15、D14用于选择要写入的频率寄存器,如下图,当DB15、DB14 = 01时,选择FREQ0进行写入,当DB15、DB14 = 10时,选择FREQ1进行写入.
    在这里插入图片描述
    如果用户想改变频率寄存器的全部内容,必须连续两次写入同一地址,因为频率寄存器是28位宽。第一次写包含14个lsb,第二次写包含14个msb。对于这种操作模式,B28 (D13)控制位应该设置为1 .

下表9举例了一个往FREQ0寄存器写入28 Bits数据的例子:
在这里插入图片描述

  1. 写入0010 0000 0000 00000x2000到控制寄存器,表示准备往FREQX寄存器写入数据
  2. 先写入FREQ0寄存器的低14位0100 0000 0000 00000x4000,D15、D14 = 01表示写入的频率寄存器为FREQ0.
  3. 后写入FREQ0寄存器的高14位0111 1111 1111 11110x7FFF,D15、D14 = 01表示写入的频率寄存器为FREQ0.
  4. 那么写入到FREQ0的数据就是1111 1111 1111 1100 0000 0000 00000xFFFC000
  • 独立修改频率寄存器的MSB或LSB
    有时在某些应用程序中,不需要改变频率寄存器的全部28位。在粗调时,只改变了14个msb,而细调时,只改变了14个lbs。
    通过设置D13 (B28) = 0,将28位频率寄存器作为两个14位寄存器运行,分别是14个msb和14个lsb。这意味着频率字的14个msb和14个lbs可独立修改。
    位D12(HLB)在控制寄存器中标识哪14位正在被改变。

下表10和11举例了一个往FREQ、FREQ1寄存器独立写入14 Bits数据的例子:
在这里插入图片描述
对表10:

  1. 写入0000 0000 0000 00000x0000到控制寄存器,表示准备往FREQX寄存器独立写入14位LSB数据
  2. 写入FREQ1寄存器的低14位1011 1111 1111 11110xBFFF,D15、D14 = 10表示写入的频率寄存器为FREQ1. 被写入到FREQ1的低14位数据位为0011 1111 1111 11110x3FFF.

对表11:

  1. 写入0001 0000 0000 00000x1000到控制寄存器,表示准备往FREQX寄存器独立写入14位MSB数据
  2. 写入FREQ0寄存器的低14位0100 0000 1111 11110x40FF,D15、D14 = 01表示写入的频率寄存器为FREQ0. 被写入到FREQ1的低14位数据位为0000 0000 1111 11110x00FF.

  • 写相位寄存器
    写入相位寄存器时,控制寄存器中的D15位和D14位设置为11 . D13选择要加载的相位寄存器,如下图,D13 = 0时,加载PHASE0,D13=1时,加载PHASE1 .
    在这里插入图片描述

1.3.3 输出引脚

控制寄存器中的OPBITEN (D5)和MODE(D1)位用于决定AD983的输出。下表15 描述了两个控制寄存器位搭配下的输出波形。
在这里插入图片描述

如:

  1. 将当前VOUT波形设置为三角波,只需向AD9833控制寄存器写入0000 0000 0000 00100x0002即可
  2. 将当前VOUT波形设置为正弦波(默认为正弦波),只需向AD9833控制寄存器写入0000 0000 0000 00000x0000即可
  3. 将当前VOUT波形设置为当前DAC的MSB直接输出(特定频率的方波),只需向AD9833控制寄存器写入0000 0000 0010 10000x0028即可
    将当前VOUT波形设置为当前DAC的MSB/2直接输出(特定频率除以2的方波),只需向AD9833控制寄存器写入0000 0000 0010 00000x0020即可

输出正弦波的峰峰值是固定的,约600mV,且正弦波也不是标准的正弦波,波谷不是负电压而是0V,结合 节2.2 频率和相位寄存器 中正弦波输出频率、相位偏移公式,知输出正弦波的公式为:
在这里插入图片描述

其中:

  1. K约为600mV,它与器件内部参考电压有关

1.3.4 核心代码

代码索引:
void AD9833_Init(void)
void AD9833_Write(u16 Data)
void AD9833_Out(u32 Freq_value,u16 Phase_value,u8 cyc_Mhz)
void Delay_AD9833(u32 i)
// 头文件,根据对应GPIO而定
#define AD9833_NSS(n)	 {if(n==0)GPIO_ResetBits(GPIOD, GPIO_Pin_2);else GPIO_SetBits(GPIOD, GPIO_Pin_2);} // FSYNC
#define AD9833_SCLK(n) {if(n==0)GPIO_ResetBits(GPIOD, GPIO_Pin_3);else GPIO_SetBits(GPIOD, GPIO_Pin_3);}
#define AD9833_SDTA(n) {if(n==0)GPIO_ResetBits(GPIOD, GPIO_Pin_0);else GPIO_SetBits(GPIOD, GPIO_Pin_0);}
/***************************************************************************
** 函数名称   :   AD9833_Init
** 功能描述   :  	AD9833芯片初始化
** 输入变量   :   无
** 返 回 值   :  	无
** 最后修改人 :   xxx
** 最后更新日期:  20210525
** 说    明   :		无
***************************************************************************/
void AD9833_Init(void)
{
	/* 配置IO口 代码块 开始   */
    GPIO_InitTypeDef GPIO_InitStructure;	
		
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOD, ENABLE);     //打开GPIO时钟
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;
    GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_2|GPIO_Pin_3; // PD0 - SDA, PD2 - FSYNC, PD3 - SCLK
    GPIO_Init(GPIOD, &GPIO_InitStructure);
	/* 配置IO口 代码块 结束   */
    AD9833_Write(0x2100);       //写寄存器,AD9833写1复位
    AD9833_Write(0x4000);       //对频率寄存器0 的LSB进行清零
    AD9833_Write(0x4000);       //对频率寄存器0 的MSB进行清零
    AD9833_Write(0x2900);       //写寄存器,AD9833写1复位


    AD9833_Write(0x8000);       //对频率寄存器1 的LSB进行清零
    AD9833_Write(0x8000);       //对频率寄存器1 的MSB进行清零
    AD9833_Write(0xD000);       //对相位寄存器0 进行清零,16位
    AD9833_Write(0xF000);       //对相位寄存器1 进行清零,16位

}
/***************************************************************************
** 函数名称   :   AD9833_Write
** 功能描述   :  	向AD9833写入16位数据
** 输入变量   :   Data:16位数据
** 返 回 值   :  	无
** 最后修改人 :   xxx
** 最后更新日期:  20210525
** 说    明   :		无
***************************************************************************/
void AD9833_Write(u16 Data)
{
    u8 i;
    AD9833_SCLK(1);
    AD9833_SDTA(1);
    AD9833_NSS(1);
    Delay_AD9833(2);
    AD9833_NSS(0);
    for(i=0; i<16; i++)
    {
        if(Data & 0x8000)
        {
            AD9833_SDTA(1);
        }
        else
        {
            AD9833_SDTA(0);
        }
        AD9833_SCLK(0);
        AD9833_SCLK(1);
        Data = Data<<1;
    }
    AD9833_NSS(1);
    AD9833_SCLK(0);
}
/***************************************************************************
** 函数名称   :   AD9833_Out
** 功能描述   :  	AD9833输出正弦波波形函数
** 输入变量   :   
									Freq_value:输出波形频率
									Phase_value:输出波形相位
									cyc_Mhz:MCLK引脚晶振频率
** 返 回 值   :  	无
** 最后修改人 :   xxx
** 最后更新日期:  20210525
** 说    明   :		无
***************************************************************************/
void AD9833_Out(u32 Freq_value,u16 Phase_value,u8 cyc_Mhz)
{
    u32 dds;
    u16 dds1,dds2;
    dds = Freq_value * (268.435456/cyc_Mhz); 
    dds1 = dds & 0x3fff;
    dds1 |= 0x4000;
    dds = dds >> 14;
    dds2 = dds & 0x3fff;
    dds2 |= 0x4000;
    AD9833_Write(0x2000);  // 写控制寄存器,进入写频率寄存器模式
    AD9833_Write(dds1); // 写频率寄存器,先写低14位
    AD9833_Write(dds2); // 后写高14位
    AD9833_Write(Phase_value); // 写相位寄存器,0xC000表示无相位偏移
}
/***************************************************************************
** 函数名称   :   Delay_AD9833
** 功能描述   :  	粗略的延时函数
** 输入变量   :   i:延时时长
** 返 回 值   :  	无
** 最后修改人 :   xxx
** 最后更新日期:  20210525
** 说    明   :		无
***************************************************************************/
void Delay_AD9833(u32 i)
{
    u8 d;
    while(i>0)
    {
        d=6;
        while(d--);
        i--;
    }
}

1.4 AD5693


extern u8 IIC_Channel;
u8  AD5693_Write_flag = 0;
u8  Write_Flag = 0;
void AD5693_Write_DAC_EEPROM(u8 n,u8 j,u16 date)
{
	
	u8 temp;
	u8 DeviceAddr;
	u8 Command;
	u16 t=0;
	if((n==0)&&(j==0)){DeviceAddr= 0x98;IIC_Channel=2;Command=0x00;}
	//地址编码为0,写命令,无操作
	if((n==0)&&(j==1)){DeviceAddr= 0x98;IIC_Channel=2;Command=0x10;}
	//地址编码为0,写命令,写入输入寄存器
	if((n==0)&&(j==2)){DeviceAddr= 0x98;IIC_Channel=2;Command=0x20;}
	//地址编码为0,写命令,更新	DAC寄存器
	if((n==0)&&(j==3)){DeviceAddr= 0x98;IIC_Channel=2;Command=0x30;}
	//地址编码为0,写命令,写入寄存器和更新DAC寄存器
	if((n==0)&&(j==4)){DeviceAddr= 0x98;IIC_Channel=2;Command=0x40;}
	//地址编码为0,写命令,写入控制寄存器
	if((n==1)&&(j==0)){DeviceAddr= 0x9c;IIC_Channel=2;Command=0x00;}
	//地址编码为1,写命令,无操作
	if((n==1)&&(j==1)){DeviceAddr= 0x9c;IIC_Channel=2;Command=0x10;}
	//地址编码为1,写命令,写入输入寄存器
	if((n==1)&&(j==2)){DeviceAddr= 0x9c;IIC_Channel=2;Command=0x20;}
	//地址编码为1,写命令,更新	DAC寄存器
	if((n==1)&&(j==3)){DeviceAddr= 0x9c;IIC_Channel=2;Command=0x30;}
	//地址编码为1,写命令,写入寄存器和更新DAC寄存器
	if((n==1)&&(j==4)){DeviceAddr= 0x98;IIC_Channel=2;Command=0x40;}
	//地址编码为1,写命令,写入控制寄存器
	//sprintfU4( " 1 \r\n");
	do
	{
		IIC_Start();
		IIC_WRITE_BYTE(DeviceAddr);//写从属地址
		if(IIC_Recelve_Ack()==0)
		{
				IIC_WRITE_BYTE(Command);//命令字节
				if(IIC_Recelve_Ack()==0)
				{
					temp = date/256;
					IIC_WRITE_BYTE(temp);
					if(IIC_Recelve_Ack()==0)
					{
						temp = date%256;
						IIC_WRITE_BYTE(temp);
						if(IIC_Recelve_Ack()==0)
							{
								AD5693_Write_flag = 0;
								Write_Flag = 0;
							}
							else
							{Write_Flag = 1;t++;}
						}
						else {Write_Flag = 1;t++;}
					}
					else {Write_Flag = 1;t++;}
				}
				else {Write_Flag = 1;t++;}
		}
	while((Write_Flag == 1)&&(t<800));	
	if(t>=800){AD5693_Write_flag = 1;}		
	IIC_Stop();
	delay_Nus(250);
	delay_Nus(250);
	delay_Nus(250);
	delay_Nus(250);
}

1.5 *(Debug)DAC8411

  1. 宏定义与头文件包含
#include <stm32f10x_rcc.h>

// 时钟线定义
#define DAC8411_SCLK_GPIO_CLK  RCC_APB2Periph_GPIOD
#define DAC8411_DIN_GPIO_CLK   RCC_APB2Periph_GPIOD
#define DAC8411_SYNC_GPIO_CLK  RCC_APB2Periph_GPIOB
#define DAC8411_SPI_CLK        RCC_APB2Periph_SPI1

// IO 定义
#define DAC8411_SCLK_GPIO_PORT GPIOD
#define DAC8411_DIN_GPIO_PORT  GPIOD
#define DAC8411_SYNC_GPIO_PORT GPIOB
#define DAC8411_SPI_PERIPH     SPI1

#define DAC8411_SCLK_GPIO_PIN  GPIO_Pin_5
#define DAC8411_DIN_GPIO_PIN   GPIO_Pin_6
#define DAC8411_SYNC_GPIO_PIN  GPIO_Pin_8

#define DAC8411_SPI_CS_LINE    GPIO_Pin_8
  1. 初始化接口
void DAC8411_GPIO_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    
    RCC_APB2PeriphClockCmd(DAC8411_SCLK_GPIO_CLK | DAC8411_DIN_GPIO_CLK | DAC8411_SYNC_GPIO_CLK, ENABLE);
    
    GPIO_InitStructure.GPIO_Pin = DAC8411_SCLK_GPIO_PIN | DAC8411_DIN_GPIO_PIN;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_Init(DAC8411_SCLK_GPIO_PORT, &GPIO_InitStructure);
    
    GPIO_InitStructure.GPIO_Pin = DAC8411_SYNC_GPIO_PIN;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_Init(DAC8411_SYNC_GPIO_PORT, &GPIO_InitStructure);
}
  1. 初始化SPI
void SPI1_Init(void)
{
    SPI_InitTypeDef SPI_InitStructure;
    
    RCC_APB1PeriphClockCmd(DAC8411_SPI_CLK, ENABLE);
    
    SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
    SPI_InitStructure.SPI_Mode = SPI_Mode_Master;
    SPI_InitStructure.SPI_DataSize = SPI_DataSize_16b;
    SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;
    SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;
    SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;
    SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_64;
    SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
    SPI_InitStructure.SPI_CRCPolynomial = 7;
    SPI_Init(DAC8411_SPI_PERIPH, &SPI_InitStructure);
    
    SPI_Cmd(DAC8411_SPI_PERIPH, ENABLE);
}
  1. DAC 输出
void DAC8411_write(uint16_t data)
{
	u8 i;
		
    GPIO_ResetBits(DAC8411_SYNC_GPIO_PORT, DAC8411_SYNC_GPIO_PIN); // 拉低SYNC引脚以启动写操作
	delay_us(40);
	  for(i=0;i<2;i++) // 前2位写0
		{
			GPIO_SetBits(GPIOD,	GPIO_Pin_5); // 设置SPI SCK 为高
			GPIO_ResetBits(GPIOD,		GPIO_Pin_6); // 设置SPI MOSI 为低
			delay_us(10);
			GPIO_ResetBits(GPIOD,	GPIO_Pin_5); // 设置SPI SCK 为低
			delay_us(40);
		}
    SPI_I2S_SendData(DAC8411_SPI_PERIPH, data); // 通过SPI发送16位数据
    while(SPI_I2S_GetFlagStatus(DAC8411_SPI_PERIPH, SPI_I2S_FLAG_TXE) == RESET); // 等待发送完成
    GPIO_SetBits(DAC8411_SYNC_GPIO_PORT, DAC8411_SYNC_GPIO_PIN); // 释放SYNC引脚以结束写操作
}

2. DMA

⚠下面以STM32F4xx 系列芯片为例,不同芯片DMA 资源会有所不同;


DMA(Direct Memory Access,直接存储器访问):DMA 的出现就是为了解决大量数据的传输问题。DMA 是指外部设备不通过CPU而直接与内部存储器(FLASH/SRAM)交换数据的接口技术。这样数据的传送速度就完全取决于存储器和外设的工作速度。大量节省CPU 资源;


2.1 概述、原理解析

2.1.1 功能原理示意图

如下图,数据通过CPU传输的路径为黑线所示,通过DMA传输的路径为红线所示;可见红线上的数据传输并没有经过CPU;
在这里插入图片描述

2.1.2 STM32 DMA 资源(STM32F4xx)

如下图以STM32F4 大容量MCU 为例,其内部集成2个DMA,每个DMA有8个数据流,每个数据流有8个硬件通道(又叫请求),每个通道有独立的仲裁器,每个通道可以配置外设地址以连接到外设;

仲裁器:用于管理多个DMA 请求的优先级,优先级高的先处理,反之优先级低的等待被处理;
数据流级优先级:同一个DMA 的8个数据流,可通过软件编程配置成4种优先级(非常高、高、中、低);

在这里插入图片描述

2.1.3 DMA 控制器系统框图

在这里插入图片描述

已知系统总线是由CPU 管理的,当DMA 工作时,希望CPU把总线(AHB)让出来给DMA 控制器管理,因此DMA 控制器必须有以下功能:

  1. 能向CPU 发出系统保持(HOLD)信号,提出总线接管请求;
  2. 当CPU 发出允许接管信号后,接管对总线的控制,进入DMA方式;
  3. 能对存储器寻址、能修改地址指针,实现对内存的读写;
  4. 能决定本次DMA 传送的字节数,判断DMA 传送是否借宿;
  5. 发出DMA结束信号,使CPU 恢复正常工作状态。

由上图可知,DMA 控制器提供两个 AHB 主端口:存储器端口(用于连接存储器)和 外设端口(用于连接外设)。但是,要执行存储器到存储器的传输,外设端口必须也能访问存储器;

  • DMA 支持的内部外设:定时器、ADC、SPI、IIC、USART
  • DMA 所在的时钟总线:AHB1

2.1.4 DMA 控制器系统框图 x2

在上一节中是单个DMA 控制器的框图,以下是两个DMA 控制器(一般STM32有两个DMA)与总线矩阵、外设、存储器组成的DMA 系统框图;

注:下图为STM32F405xx/07xx 和STM32F415xx/17xx 系列芯片DMA 控制器系统框图,具体芯片内部资源可能不一样,以具体芯片规格书为准;

值得注意的是,如下图红框标注地方,相比于DMA2,DMA1的SHB 外设端口并没有连接到总线矩阵,因此它并不能实现外部存储器到内部存储器的数据传输;
在这里插入图片描述

2.2 DMA的数据传输过程

2.2.1 三种数据传输方向、传输需要的条件

三种传输方向

  1. 外设到存储器
  2. 存储器到外设
  3. 存储器到存储器(内部与外部存储器互传)
    在这里插入图片描述

闪存、SRAM、外设的SRAM、APB1、APB2和AHB外设均可作为访问的源和目标;

2.2.1.1 数据传输需要的三个条件
  1. 数据的源地址(访问源)
  2. 数据传输位置的目标地址(访问目标)
  3. 数据传输量(最大65535位)

在用户配置好上述3个条件后,若有DMA 事件触发,DMA 控制器就会启动数据传输,当剩余传输数据量为0时,即认为达到传输终点,结束DMA 传输;另外,DMA 还有循环传输模式 ,即当到达传输终点时,DMA 控制器会重新启动DMA传输,一直循环;


当发生一个事件后,外设向DMA控制器发送一个请求信号。DMA控制器根据通道的优先级处理请求。当DMA控制器开始访问发出请求的外设时,DMA控制器立即发送给它一个应答信号。当从DMA控制器得到应答信号时,外设立即释放它的请求。一旦外设释放了这个请求,DMA控制器同时撤销应答信号。如果有更多的请求时,外设可以启动下一个周期。总之,每次DMA 数据传输由以下3个操作组成:

  1. 从外设数据寄存器或者从当前外设/存储器地址寄存器指示的存储器地址取数据,第一次传输时的开始地址是DMA_CPARx(存储外设基地址)或DMA_CMARx(存储内存基地址)寄存器指定的外设基地址或存储器单元。
  2. 存数据到外设数据寄存器或者当前外设/存储器地址寄存器指示的存储器地址,第一次传输时的开始地址是DMA_CPARx或DMA_CMARx寄存器指定的外设基地址或存储器单元。
  3. 执行一次DMA_CNDTRx寄存器的递减操作,该寄存器包含未完成的操作数目。

2.2.2.1 数据源、传输目标和传输模式
  • 数据源和目标在整个4GB 区域(地址在 0x0000 0000 和 0xFFFF FFFF 之间)都可以进行寻址外设和存储器;
  • 传输方向:如下表使用 DMA_SxCR 寄存器中的 DIR[1:0] 位进行配置,2.2.1节讲过,DMA 数据传输有三种传输方向:存储器到外设、外设到存储器、存储器到存储器。
    在这里插入图片描述
2.2.2.2 外设到存储器的数据传输
  1. 将 DMA_SxCR 寄存器中的位 EN 置 1时使能这种传输模式,每次产生外设请求,数据流都会启动数据源到 FIFO 的传输;
  2. 【FIFO 模式】达到 FIFO 的阈值级别时,FIFO 的内容移出并存储到目标中;
  3. 如果 DMA_SxNDTR 寄存器达到零、外设请求传输终止(在使用外设流控制器的情况下)或 DMA_SxCR 寄存器中的 EN 位由软件清零,传输即会停止;
  4. 【直接模式】在直接模式下(当 DMA_SxFCR 寄存器中的 DMDIS 值为0时),不使用 FIFO 的阈值级别控制:每完成一次从外设到 FIFO 的数据传输后,相应的数据立即就会移出并存储到目标中;
  5. 只有赢得了数据流的仲裁后,相应数据流才有权访问 AHB 源或目标端口。系统使用在 DMA_SxCR 寄存器 PL[1:0] 位中为每个数据流定义的优先级执行仲裁;

在这里插入图片描述

2.2.2.3 存储器到外设的数据传输
  1. 将 DMA_SxCR 寄存器中的 EN 位置 1时使能这种传输模式,之后数据流会立即启动传输,从源完全填充 FIFO;
  2. 每次发生外设请求,FIFO 的内容都会移出并存储到目标中。当 FIFO 的级别小于或等于预定义的阈值级别时,将使用存储器中的数据完全重载FIFO;
  3. 如果 DMA_SxNDTR 寄存器达到零、外设请求传输终止(在使用外设流控制器的情况下)或 DMA_SxCR 寄存器中的 EN 位由软件清零,传输即会停止。
  4. 在直接模式下(当 DMA_SxFCR 寄存器中的 DMDIS 值为“0”时),不使用 FIFO 的阈值级别。一旦使能了数据流,DMA 便会预装载第一个数据,将其传输到内部 FIFO。一旦外设请 求数据传输,DMA 便会将预装载的值传输到配置的目标。然后,它会使用要传输的下一个数 据再次重载内部空 FIFO。预装载的数据大小为 DMA_SxCR 寄存器中 PSIZE 位字段的值。
  5. 只有赢得了数据流的仲裁后,相应数据流才有权访问 AHB 源或目标端口。系统使用在 DMA_SxCR 寄存器 PL[1:0] 位中为每个数据流定义的优先级执行仲裁。
    在这里插入图片描述
2.2.2.4 存储器到存储器模式

DMA通道的操作可以在没有外设请求的情况下进行,这种操作就是存储器到存储器模式。当设置了DMA_CCRx寄存器中的MEM2MEM位之后,在软件设置了DMA_CCRx寄存器中的EN位启动DMA通道时,DMA传输将马上开始。当DMA_CNDTRx寄存器变为0时,DMA传输结束。存储器到存储器模式不能与循环模式同时使用

2.2.2 数据传输事件请求与DMA 控制器处理过程

在这里插入图片描述


以2.1.3 节视角看,其处理过程如下图红色箭头所示:
在这里插入图片描述


以内部寄存器看,每次DMA 传输包含三项操作:

  • 通过 DMA_SxPAR 或 DMA_SxM0AR 寄存器寻址,从外设数据寄存器或存储器单元中加载数据;(处理取数据)
  • 通过 DMA_SxPAR 或 DMA_SxM0AR 寄存器寻址,将加载的数据存储到外设数据寄存 器或存储器单元;(处理放数据)
  • DMA_SxNDTR 计数器在数据存储结束后递减,该计数器中包含仍需执行的事务数;(处理传输数据)

2.2.3 数据传输序列

数据传输序列就是由多个数据组成的序列,如下图,DMA 事件由给定数目的数据传输序列组成;数据传输序列的数目及其宽度(8 位、16 位 或 32 位)可用软件编程;
在这里插入图片描述

2.2.4 通道选择及其外设硬件映射

如2.1.2节所述,每个DMA 有8个数据流共64个通道(请求)(8*8=64),对于同一DMA,假设64个通道同时发生数据传输请求,则需要进行通道选择,该选择通过每个数据流中的DMA_SxCR 寄存器中的 CHSEL[2:0] 位控制其内8个通道的优先级;
在这里插入图片描述


下表是DMA 各数据流的8个通道对应的硬件外设连接;
在这里插入图片描述
在这里插入图片描述

如:要实现串口5接收DMA 传输,根据上表,其将通过数据流0的通道4向DMA1控制器发送数据传输请求,请求被确认后,数据将通过数据流0进行数据传输;

2.2.5 数据流_REQ_STREAMx

对于每个DMA 控制器内的8个数据流,它们都能提供源和目标之间的单向传输链路;每个数据流都可进行2种传输:

  • 常规类型事务:存储器到外设、外设到存储器或存储器到存储器的传输;
  • 双缓冲区类型事务:使用存储器的两个存储器指针的双缓冲区传输(当 DMA 正在进行自/至缓冲区的读/写操作时,应用程序可以进行至/自其它缓冲区的写/读操作);

2.3 仲裁器

如2.1.4 图所示,每个DMA 控制器中都有一个仲裁器,其作用是管理8个数据流的请求优先级;

优先权管理分2个阶段:

  1. 软件仲裁:每个通道的优先权可以在DMA_CCRx 寄存器中设置,有4个等级:

    • 最高优先级
    • 高优先级
    • 中优先级
    • 低优先级
  2. 硬件仲裁:如果2个请求有相同的软件优先级,则较低编号的通道比较高编号的通道有较高的优先权。如:通道2优先于通道4。

在这里插入图片描述

注意: 在大容量产品和互联型产品中,DMA1控制器拥有高于DMA2控制器的优先级

2.4 核心代码

下面以STM32F1 DMA应用于串口3数据接收功能为例:
在这里插入图片描述

  1. ※DMA_PeripheralBaseAddr :【源】用来设置 DMA 传输的外设基地址
  2. ※DMA_MemoryBaseAddr:【目标】内存基地址,也就是存放DMA传输数据的存储器内存地址
  3. ※DMA_DIR: 设置【数据传输方向】,决定是从外设读取数据到内存还是从内存读取数据发送到外设
  4. ※DMA_BufferSize: 设置【一次传输数据量的大小】
  5. DMA_PeripheralInc :设置传输数据的时候外设地址是不变还是递增。如果设置为递增,那么下一次传输的时候地址加 1
  6. DMA_MemoryInc:设置传输数据时候内存地址是否递增。这个参数 和DMA_PeripheralInc 意思接近,只不过针对的是内存
  7. DMA_PeripheralDataSize:用来设置外设的数据长度是为字节传输(8bits)、半字传输 (16bits) 还是字传输 (32bits)
  8. DMA_MemoryDataSize 是用来设置内存的数据长度,和第七个参数意思接近
  9. DMA_Mode:用来设置 DMA 模式是否循环采集,也就是说,比如我们要从内存中采集 64 个字节发送到串口,如果设置为重复采集,那么它会在 64 个字节采集完成之后继续从内存的第一个地址采集,如此循环。
  10. DMA_M2M:设置 DMA 通道的优先级,有低,中,高,超高三种模式,

  • 配置代码:
	  USART_ITConfig(USART3, USART_IT_IDLE, ENABLE); // 开启串口3空闲中断 		
		DMA_DeInit(DMA1_Channel3); // 复位DMA1_Channel3
		RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);  // 使能DMA1时钟
    
    // RX DMA1
    DMA_InitStruct.DMA_BufferSize = sizeof(Usart3data);      			// 传输的数据大小
    DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC;            			// 外设作为数据的来源
    DMA_InitStruct.DMA_M2M = DMA_M2M_Disable;                  			// 不使能M TO M传输
    DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)Usart3data;			// 设置DMA源地址:串口数据寄存器
    DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;		// 内存数据单元
    DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;        		// 外设地址不增
    DMA_InitStruct.DMA_Mode = DMA_Mode_Normal;                  		// DMA模式一次或者循环模式
		//DMA_InitStruct.DMA_Mode = DMA_Mode_Circular;              		// DMA模式一次或者循环模式
    DMA_InitStruct.DMA_PeripheralBaseAddr = USART3_BASE + 0x04; 		// 设置DMA源地址:串口数据寄存器地址
    DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;    // 外设数据单元
    DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;   // 外设地址不增加
    DMA_InitStruct.DMA_Priority = DMA_Priority_High;            		// 优先级为中
 
    // 初始化DMA通道     
    DMA_Init(DMA1_Channel3, &DMA_InitStruct);     
    // 清除DMA所有标志
    DMA_ClearFlag(DMA1_FLAG_TC3);
    DMA_ITConfig(DMA1_Channel3, DMA_IT_TE,ENABLE);

    USART_DMACmd(USART3, USART_DMAReq_Rx, ENABLE);// 使能串口DMA接收
    DMA_Cmd(DMA1_Channel3, ENABLE);     // 使能DMA通道	

  • 查询代码:
  1. 查询DMA当前状态:
FlagStatus DMA_GetFlagStatus(uint32_t DMAy_FLAG)

比如要查询DMA通道4传输是否完成:
DMA_GetFlagStatus(DMA2_FLAG_TC4);

  1. 获取当前剩余数据量大小:
uint16_t DMA_GetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx)

比如要获取 DMA 通道 4 还有多少个数据没有传输
DMA_GetCurrDataCounter(DMA1_Channel4);


  • STM32F4 DMA 配置参数解释:
typedef struct
{
 // DMA 通道
  uint32_t DMA_Channel;            /*!< Specifies the channel used for the specified stream. 
                                        This parameter can be a value of @ref DMA_channel */
 
 // DMA 外部接口基地址
  uint32_t DMA_PeripheralBaseAddr; /*!< Specifies the peripheral base address for DMAy Streamx. */

 // DMA 内存基地址
  uint32_t DMA_Memory0BaseAddr;    /*!< Specifies the memory 0 base address for DMAy Streamx. 
                                        This memory is the default memory used when double buffer mode is
                                        not enabled. */
  
  // 传输方向
  uint32_t DMA_DIR;                /*!< Specifies if the data will be transferred from memory to peripheral, 
                                        from memory to memory or from peripheral to memory.
                                        This parameter can be a value of @ref DMA_data_transfer_direction */
	
  // DMA 缓冲区大小
  uint32_t DMA_BufferSize;         /*!< Specifies the buffer size, in data unit, of the specified Stream. 
                                        The data unit is equal to the configuration set in DMA_PeripheralDataSize
                                        or DMA_MemoryDataSize members depending in the transfer direction. */
 
 // 指定外围地址寄存器是否应递增
  uint32_t DMA_PeripheralInc;      /*!< Specifies whether the Peripheral address register should be incremented or not.
                                        This parameter can be a value of @ref DMA_peripheral_incremented_mode */

 // 指定内存地址寄存器是否应递增
  uint32_t DMA_MemoryInc;          /*!< Specifies whether the memory address register should be incremented or not.
                                        This parameter can be a value of @ref DMA_memory_incremented_mode */

 // 外围数据的宽度
  uint32_t DMA_PeripheralDataSize; /*!< Specifies the Peripheral data width.
                                        This parameter can be a value of @ref DMA_peripheral_data_size */

 // 内存数据的宽度
  uint32_t DMA_MemoryDataSize;     /*!< Specifies the Memory data width.
                                        This parameter can be a value of @ref DMA_memory_data_size */

 // DMA 工作模式
  uint32_t DMA_Mode;               /*!< Specifies the operation mode of the DMAy Streamx.
                                        This parameter can be a value of @ref DMA_circular_normal_mode
                                        @note The circular buffer mode cannot be used if the memory-to-memory
                                              data transfer is configured on the selected Stream */

 // DMA 软件优先级
  uint32_t DMA_Priority;           /*!< Specifies the software priority for the DMAy Streamx.
                                        This parameter can be a value of @ref DMA_priority_level */

 // DMA FIFO 模式
  uint32_t DMA_FIFOMode;          /*!< Specifies if the FIFO mode or Direct mode will be used for the specified Stream.
                                        This parameter can be a value of @ref DMA_fifo_direct_mode
                                        @note The Direct mode (FIFO mode disabled) cannot be used if the 
                                               memory-to-memory data transfer is configured on the selected Stream */

  // DMA FIFO 模式的阈值水平
  uint32_t DMA_FIFOThreshold;      /*!< Specifies the FIFO threshold level.
                                        This parameter can be a value of @ref DMA_fifo_threshold_level */

  // 内存传输的突发传输配置
  uint32_t DMA_MemoryBurst;        /*!< Specifies the Burst transfer configuration for the memory transfers. 
                                        It specifies the amount of data to be transferred in a single non interruptable 
                                        transaction. This parameter can be a value of @ref DMA_memory_burst 
                                        @note The burst mode is possible only if the address Increment mode is enabled. */
  // 外围传输的突发传输配置
  uint32_t DMA_PeripheralBurst;    /*!< Specifies the Burst transfer configuration for the peripheral transfers. 
                                        It specifies the amount of data to be transferred in a single non interruptable 
                                        transaction. This parameter can be a value of @ref DMA_peripheral_burst
                                        @note The burst mode is possible only if the address Increment mode is enabled. */  
}DMA_InitTypeDef;
// 注:以上配置参数可选项,可在程序文件中搜索`@ref` 之后的的关键字获取

2.5 其他

2.5.1 指针增量

通过设置DMA_CCRx寄存器中的PINCMINC标志位,外设和存储器的指针在每次传输后可以有选择地完成自动增量。当设置为增量模式时,下一个要传输的地址将是前一个地址加上增量值,增量值取决与所选的数据宽度为1、2或4 .
在这里插入图片描述

  1. 当通道配置为非循环模式时,传输结束后(即传输计数变为0)将不再产生DMA操作。要开始新的DMA传输,需要在关闭DMA通道的情况下,在DMA_CNDTRx寄存器中重新写入传输数目。
  2. 在循环模式下,最后一次传输结束时,DMA_CNDTRx寄存器的内容会自动地被重新加载为其初始数值,内部的当前外设/存储器地址寄存器也被重新加载为DMA_CPARx/DMA_CMARx寄存器设定的初始基地址。

在这里插入图片描述

以存储器地址增量模式(MINC标志位设置为1)为例:

  1. 定义存储器数据缓冲区数据位为8位
  2. 定义外设的数据位为8位
  3. 数据从M数据缓冲区数组从低位到高位依次传输到P数据寄存器,指针根据增量值依次递增(上面设递增量为1),每传输一个数据,DMA_CNDTRx依次递减,一直等到DMA_CNDTRx为0为止
  4. DMA_CNDTRx为0后,根据是否设置为循环模式,是则在DMA_CNDTRx寄存器中重新写入传输数目,否则结束

2.5.2 循环模式

在这里插入图片描述
循环模式用于处理循环缓冲区和连续的数据传输(如ADC的扫描模式)。在DMA_CCRx寄存器中的CIRC位用于开启这一功能。当启动了循环模式,数据传输的数目变为0时,将会自动地被恢复成配置通道时设置的初值,DMA操作将会继续进行。

2.5.3 通道传输数据量

如下图,定义通道传输数据量的寄存器为DMA_CNDTRx,它是一个32位寄存器,但只使用了前16位,故,数据传输数量最大为65535
在这里插入图片描述

2.5.4 中断

每个DMA 的数据流都有5个事件标志来触发中断请求,他们分别是DMA 半传输、DMA 传输完成、DMA 传输错误、DMA FIFO 错误、直接模式错误;为应用的灵活性考虑,通过设置寄存器的不同位来打开这些中断;
在这里插入图片描述

2.5.5 CPU与外设之间的数据传送方式

CPU与外设之间的数据传送方式即与DMA对立的另一种外设数据传输方式,一般分为程序传送方式和中断传送方式。其特点是CPU通过控制系统总线与其他部件连接并进行数据传输。

它们均由CPU控制数据传输,不同的是程序传送方式由CPU来查询外设状态,CPU处于主动地位,而外设处于被动地位。这就是常说的对外设的轮询,效率低。而中断传送方式则是外设主动向CPU发生请求,等候CPU处理,在没有发出请求时,CPU和外设都可以独立进行各自的工作。 需要进行断点和现场的保护和恢复,浪费了很多CPU的时间,所以这种方式只适合少量数据的传送

2.5.6 程序传送方式

程序传送方式是指直接在程序控制下进行数据的输入/输出操作。分为无条件传送方式和查询(条件传送方式)两种:

  1. 无条件传送方式: 微机系统中的一些简单的外设(如开关、继电器、数码管、发光二极管等),在它们工作时,可以认为输入设备已随时准备好向CPU提供数据,而输出设备也随时准备好接收CPU送来的数据,这样,在CPU需要同外设交换信息时,就能直接对这些外设进行输入/输出操作。由于在这种方式下CPU对外设进行输入/输出操作时无需考虑外设的状态,故称之为无条件传送方式。
  2. 查询(有条件)传送方式:查询传送也称为条件传送,是指在执行输入指令(IN)或输出指令(OUT)前,要先查询相应设备的状态,当输入设备处于准备好状态、输出设备处于空闲状态时,CPU才执行输入/输出指令与外设交换信息。为此,接口电路中既要有数据端口,还要有状态端口。

2.5.7 中断传送方式

中断传送方式是指当外设需要与CPU进行信息交换时,由外设向CPU发出请求信号,使CPU暂停正在执行的程序,转而去执行数据输入/输出操作,待数据传送结束后,CPU再继续执行被暂停的程序。


参考:

  1. DMA之理解

  2. 【STM32】 DMA原理,步骤超细详解,一文看懂DMA

  • 3
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Truffle7电子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值