当前位置: 首页 > article >正文

STM32 DMA直接存储器存取

目录

  • 概念
  • 存储器映像
  • DMA基本结构
  • DMA请求
  • 数据宽度与对齐
  • DAM转运
  • 硬件触发
  • 相关库函数
  • 内存到内存DMA转运代码
  • ADC+DMA
    • ADC单次扫描+DMA单次转运
    • ADC多次扫描+DMA多次转运

概念

DMA是协助CPU完成数据转运工作的,DMA可以提供外设和存储器或者存储器和存储器之间的高速数据传输,无须CPU干预,节省了CPU的资源。
STM32有12个独立可配置的通道: DMA1(7个通道), DMA2(5个通道)

从一个地方移动到另一个地方,需要占用一个通道,DMA多通道进行运转时候,之间可以各转各的互不干扰

每个通道都支持软件触发和特定的硬件触发

  • 若是存储器到存储器的数据转运,比如把flash里的一批数据转运到SRAM中去,就需要软件触发,触发后,DMA就会一股脑的转运,以最快的速度全部转运好。
  • 若是外设到存储器的数据转运,不能一股脑的转运,因为外设的数据是有一定时机的,需要硬件触发。比如转运ADC的数据,就得ADC每个通道AD转换完成后,硬件触发一次DMA,之后DMA再转运,触发一次转运一次,才能保证数据正确。
  • 特定的硬件触发:每个DMA的通道,它的硬件出发源是不一样的,当使用某个外设的硬件出发源,就得使用它连接的那个通道,而不能任意选择通道。

STM32F103C8T6 拥有的DMA资源:DMA1(7个通道)

存储器映像

DMA是在存储器之间进行数据转运的,STM32的存储器和他们的地址如下
在这里插入图片描述
STM32的CPU是32位的,有4G的寻址能力,但是芯片的存储器都是KB级别的,所以就有很多空间没有使用到,图中灰色部分[Reserved]都是没有使用到的地址。
可以发现外设的地址都是0x4000开头,然后后面的地址再分是哪一个寄存器,这样每一个外设寄存器包括其存储内容都有对应地址了,其他的存储器也都是一样的。
BOOT引脚的选择控制启动区域

整理出如下
在这里插入图片描述

系统存储器:BootLoader程序是芯片出厂自动写入的,一般不允许我们修改
选项字节:用于存储一些独立于程序代码的配置参数,位置是在ROM区的最后面,下载程序可以不刷新选项字节的内容,这样选项字节的配置就可以保持不变,选项字节里,存的主要是flash的读保护、写保护,还有看门狗等等的配置。
内核外设寄存器:内核外设就是NVIC和SysTick,内核外设和其他外设不是同个厂家设计的,所以地址被分开。

在这里插入图片描述

图中的仲裁器,由于DMA总线只有一条,所以所有的分支都只能分时复用这一条总线,如果产生冲突就由仲裁器决定先后使用顺序,红色的总线矩阵里面也有仲裁器,如果DMA总线和系统总线都要访问同一个目标,那么DMA就会让CPU停止访问,但是仲裁器仍会将总线一半的带宽给CPU,使CPU正常工作。
DAM模块中还有AHB从设备,AHB从设备也就是DMA的寄存器,DMA作为一个外设也有自己的配置寄存器,这个寄存器连接在了AHB总线上,用粉色标记。
DAM既是总线矩阵的主动单元,读写其他寄存器,也是AHB总线的被动单元,被读写。

芯片整体结构如上,Flash是只读不可写的,只有配置Flash接口才能写入。但是这只是在正常运行模式下,并不意味着我们不能下载程序进去,在编程模式一样可以下载程序。

DMA基本结构

DMA基本结构如下:
在这里插入图片描述

  • 上面一个是外设站点,一个是存储器站点,站点是存储要交换的数据的地方,究竟是外设到存储器还是存储器到外设,这个方向我们也有参数可以控制。我们也可以存储器到存储器,但是由于Flash是只读存储器,所以只能Flash到SRAM或者SRAM到SRAM。
  • 站点都有三个参数:起始地址,数据宽度,地址是否自增。起始地址是前面讲的每个存储器特有的地址,用来确认数据从哪里来,到哪里去;数据宽度就是被交换数据的大小,这里宽度有三种,分别是字节Byte,半字Half Word,字Word,大小分别为8位,16位,32位,例如我们的AD值是12位的,这里就要选择半字的宽度;地址是否自增是指每次转运完成之后,起始地址是否自增,比如说我们的ADC_DR(ADC数据寄存器),这个转运后肯定不能自增,改了地址可能下次就去转运其他寄存器了,但是存储器肯定要自增,不然这次存在存储器这个地址,下次还是存在这个地址,上一次存进来的值就会被覆盖了,数据寄存器本来就不用担心被覆盖,它的值已经转运到存储器部分被记录下来了,直接读取存储器就能知道上一次存的值是什么。
  • 这个外设站点的起始地址也不一定非要是外设寄存器的,写存储器的也行,只要改变一下控制方向的参数就行了,而且进行存储器之间的转运的话,两个站点就都是存储器了。上面的名字只是为了表述方便好区分,并不是必须是固定的哪些外设或者内存。
  • 传输寄存器是用来控制转运次数的,我们可以给该寄存器一个值,这个值会自减,减到0之后DMA就不会再转运了,并且减到0之后之前站点自增的地址又会回到增长之前。
  • 自动重装器是用来使传输寄存器的起始值清零后复原的,如果不使用的话,就只能够进行一次转运,传输寄存器清零后就不能够再次转运了,也就是单次模式,如果使用,就能够一直转运,也就是循环模式。
  • M2M(Memory to Memory):即存储器到存储器。当给M2M置1时就是软件触发,置0就是硬件触发,这个软件触发的逻辑是以最快的速度连续不断触发DMA使传输寄存器自减为0,完成这一轮的转换,所以这个模式不能和循环模式一起使用,不然DMA转运会停不下来。存储器到存储器的转运一般使用软件触发,因为存储器之间的转运不需要时机,而且越快越好。硬件触发则需要看时机,一般用于存储器和外设之间的转运。硬件触发源可以选择ADC、串口收到数据、定时时间到等等,所以需要硬件触发。

DMA转运的条件:

  1. 开关控制,DMA_Cmd必须使能
  2. 传输计数器必须大于0
  3. 触发源必须有触发信号,触发一次转运一次,传输计数器自减一次。当传输计数器等于0,且没有自动重装时,这时无论是否触发,DMA都不会再进行转运了。 即,软件触发时,因为软件触发不能和自动重装一起使用,所以此时就需要再次设置。
    此时就需要DMA_Cmd给DISABLE,关闭DMA,再为传输计数器写一个大于0的数,在DMA_Cmd给Elable开启DMA,DMA才能继续工作,写传输计数器时必须要先关闭DMA再进行,不能在DMA开启时写传输计数器,这是手册里的规定。

DMA请求

在这里插入图片描述
看图可知ADC1的DMA请求通道就是通道一,每一个外设硬件触发的请求通道都是固定对应的通道。但是软件触发的话就不需要选择通道,因为每个通道都可以软件触发。

如果使用ADC请求通道,那么使能这个DMA通道就要用ADC_DMAcmd函数使能,使用定时器也有TIM_DMAcmd使能,使用什么外设就要用其对应的DMA使能函数使能DMA,如果三个通道都开启了,我们可以看到这个或门,理论上三个通道都是有效的,但是一般只开启一个。

七个DMA通道只有一个总线,这里也有一个仲裁器来控制优先级,默认的是通道号越小优先级越高,也可以在程序中配置优先级。

数据宽度与对齐

在这里插入图片描述
源端宽度(转运起点),目标宽度(转运终点),传输数目(要转运几个数据)
B0[7:0]表示B0这个数据是8位的,B0[15:0]表示B0这个数据是16位的。
0x1,0x2是源端和目标的地址。
这个传输数目都为4,所以每一行都是,源端0x0处的B0数据传输到目标0x0处,源端0x1处的B1数据传输到目标0x1处,传输4个数据也就是一直到B4。

当源端宽度=目标宽度时,数据高低位一一对应,eg.都是8位大小,0x0B0->0x0B0
当源端宽度>目标宽度时,舍弃高位,低位对齐,eg.源端16位大小,目标8位大小,0x0B1B0->0x0B0
当源端宽度<目标宽度时,高位补0,低位对齐,eg.源端8位大小,目标16位大小,0x0B0->0x000B0

DAM转运

在这里插入图片描述
上图就是定义两个SRAM数组,并把数组地址放进站点,两个站点地址都自增,转运7次,软件触发,单次模式(不用自动重装器),数据宽度示情况而定,优先级配置也示情况而定。

硬件触发

在这里插入图片描述
DMA配置(以ADC为例):把ADC_DR的地址塞进外设站点,定义的数组塞进存储器站点,如果ADC是连续转换则设置为循环模式,非连续则设置为单次模式,通道有几个就转运几次,并且每一次转运完成都硬件触发一次DMA请求进行DMA转运(对应外设要对应器通道),ADC_DR地址不自增,存储器地址自增,数据宽度都为半字,优先级配置示情况而定。

相关库函数

void DMA_DeInit(DMA_Channel_TypeDef* DMAy_Channelx);
void DMA_Init(DMA_Channel_TypeDef* DMAy_Channelx, DMA_InitTypeDef* DMA_InitStruct);
void DMA_StructInit(DMA_InitTypeDef* DMA_InitStruct);
void DMA_Cmd(DMA_Channel_TypeDef* DMAy_Channelx, FunctionalState NewState);
void DMA_ITConfig(DMA_Channel_TypeDef* DMAy_Channelx, uint32_t DMA_IT, FunctionalState NewState);
void DMA_SetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx, uint16_t DataNumber); 
uint16_t DMA_GetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx);
FlagStatus DMA_GetFlagStatus(uint32_t DMAy_FLAG);
void DMA_ClearFlag(uint32_t DMAy_FLAG);
ITStatus DMA_GetITStatus(uint32_t DMAy_IT);
void DMA_ClearITPendingBit(uint32_t DMAy_IT);

有些函数都使用了很多次了,看见参数应该就知道功能了。
DMA_StructInit初始化结构体的,上面DMA结构中各种参数都是使用结构体进行配置的。
DMA_ITConfig是中断输出使能,需要DMA中断的话就需要这个函数。
DMA_SetCurrDataCounter设置当前数据寄存器,就是给传输计数器写数据的。
DMA_GetCurrDataCounterDMA获取当前数据寄存器,就是返回当前传输计数器的值。
剩下四个是获取标志位状态,清楚标志位,获取中断状态,清楚中断挂起位。

内存到内存DMA转运代码

MyDMA.c:

#include "Config.h"
uint16_t MyDMA_Size;
void MyDMA_Init(uint32_t MemAddr, uint32_t PerAddr, uint32_t Size)
{
	MyDMA_Size = Size;
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);//DMA是AHB总线上的设备
	DMA_InitTypeDef DMA_InitStructure;
	DMA_InitStructure.DMA_BufferSize = Size;//计数器大小
	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;//数据传输方向,这个参数指定外设站点是源端还是目的地
	DMA_InitStructure.DMA_M2M = DMA_M2M_Enable;// 软件触发
	DMA_InitStructure.DMA_MemoryBaseAddr = MemAddr;
	DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
	DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;//地址是否递增
	DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;//是否循环
	DMA_InitStructure.DMA_PeripheralBaseAddr = PerAddr;
	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable;
	DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;//优先级
	DMA_Init(DMA1_Channel1, &DMA_InitStructure);
	
	DMA_Cmd(DMA1_Channel1, DISABLE);
}

void MyDMA_Transfer(void)
{
	DMA_Cmd(DMA1_Channel1, DISABLE);
	DMA_SetCurrDataCounter(DMA1_Channel1, MyDMA_Size);//设置计数器的值
	DMA_Cmd(DMA1_Channel1, ENABLE);
	//因为这个标志位需要手动清零,所以需要一直监测,然后清零
	while(DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);//转运完成退出循环
	DMA_ClearFlag(DMA1_FLAG_TC1);
}

main.c:

#include "Config.h"

char arr1[] = {11, 12, 13, 43};
char arr2[4] = {0};
int main(void)
{
	OLED_Init();
	MyDMA_Init((uint32_t)arr2, (uint32_t)arr1, 4);

	while(1)
	{
		for(int i = 0; i < 4; i++)
		{
			arr1[i]++;
		}
		OLED_ShowNum(1, 1, arr1[0], 2);
		OLED_ShowNum(1, 4, arr1[1], 2);
		OLED_ShowNum(1, 7, arr1[2], 2);
		OLED_ShowNum(1, 10, arr1[3], 2);
		OLED_ShowNum(2, 1, arr2[0], 2);
		OLED_ShowNum(2, 4, arr2[1], 2);
		OLED_ShowNum(2, 7, arr2[2], 2);
		OLED_ShowNum(2, 10, arr2[3], 2);
		Delay_s(1);
		
		MyDMA_Transfer();
		OLED_ShowNum(4, 1, arr2[0], 2);
		OLED_ShowNum(4, 4, arr2[1], 2);
		OLED_ShowNum(4, 7, arr2[2], 2);
		OLED_ShowNum(4, 10, arr2[3], 2);
		Delay_s(1);
	}
}

ADC+DMA

ADC单次扫描+DMA单次转运

此时ADC的配置需要配置四个通道,DMA也要配置为硬件触发。
AD.c:

#include "stm32f10x.h"                  // Device header

uint16_t AD_Value[4];

void AD_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
	
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);
	
	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);
	ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5);
	ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_55Cycles5);
	ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_55Cycles5);
		
	ADC_InitTypeDef ADC_InitStructure;
	ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
	ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;
	ADC_InitStructure.ADC_ScanConvMode = ENABLE;
	ADC_InitStructure.ADC_NbrOfChannel = 4;
	ADC_Init(ADC1, &ADC_InitStructure);
	
	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_Cmd(DMA1_Channel1, ENABLE);
	ADC_DMACmd(ADC1, ENABLE);
	ADC_Cmd(ADC1, ENABLE);
	
	ADC_ResetCalibration(ADC1);
	while (ADC_GetResetCalibrationStatus(ADC1) == SET);
	ADC_StartCalibration(ADC1);
	while (ADC_GetCalibrationStatus(ADC1) == SET);
	
	ADC_SoftwareStartConvCmd(ADC1, ENABLE);
}

main.c:

#include "Config.h"

int main(void)
{
	OLED_Init();
	AD_Init();
	OLED_ShowString(1, 1, "AD1:");
	OLED_ShowString(2, 1, "AD2:");
	OLED_ShowString(3, 1, "AD3:");
	OLED_ShowString(4, 1, "AD4:");
	
	while(1)
	{
		AD_GetValue();

		OLED_ShowNum(1, 5, AD_value[0], 4);
		OLED_ShowNum(2, 5, AD_value[1], 4);
		OLED_ShowNum(3, 5, AD_value[2], 4);
		OLED_ShowNum(4, 5, AD_value[3], 4);
		Delay_ms(1000);
	}
}

ADC多次扫描+DMA多次转运

此时就不需要AD_GetValue了,因为ADC触发后是一直工作的,DMA也是一直工作的。
AD.c:

#include "Config.h"
uint16_t AD_value[4];
void AD_Init()
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
	RCC_APB1PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);//DMA是AHB总线上的设备
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AIN;//模拟输入
	GPIO_InitStructure.GPIO_Pin=GPIO_Pin_0;
	GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStructure);
	
	ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_13Cycles5);//ADC规则组通道配置,给序列的每个位置填写指定的通道
	ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_13Cycles5);//ADC规则组通道配置,给序列的每个位置填写指定的通道
	ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_13Cycles5);//ADC规则组通道配置,给序列的每个位置填写指定的通道
	ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_13Cycles5);//ADC规则组通道配置,给序列的每个位置填写指定的通道

	
	ADC_InitTypeDef ADC_InitStructure;
	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;//扫描转换模式,选择扫描模式还是非扫描模式
	ADC_InitStructure.ADC_NbrOfChannel = 4;//通道数目,在扫描模式下用到几个通道
	ADC_Init(ADC1, &ADC_InitStructure);

	DMA_InitTypeDef DMA_InitStructure;
	DMA_InitStructure.DMA_BufferSize = 4;//计数器大小
	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;//数据传输方向,这个参数指定外设站点是源端还是目的地
	DMA_InitStructure.DMA_M2M = DMA_M2M_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_Mode = DMA_Mode_Circular;//是否循环,即是否重装
	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_Priority = DMA_Priority_Medium;//优先级
	DMA_Init(DMA1_Channel1, &DMA_InitStructure);//必须使用DMA1的通道1,因为ADC1在这个通道
	
	DMA_Cmd(DMA1_Channel1, ENABLE);
	
	ADC_DMACmd(ADC1, ENABLE);
	ADC_Cmd(ADC1, ENABLE);
	
	ADC_ResetCalibration(ADC1);//复位校准
	while(ADC_GetResetCalibrationStatus(ADC1) == SET);//等待复位校准完成
	ADC_StartCalibration(ADC1);//开始校准
	while(ADC_GetCalibrationStatus(ADC1) == SET);//等待校准完成
	
	ADC_SoftwareStartConvCmd(ADC1, ENABLE);
}

main.c:

#include "Config.h"

int main(void)
{
	OLED_Init();
	AD_Init();
	OLED_ShowString(1, 1, "AD1:");
	OLED_ShowString(2, 1, "AD2:");
	OLED_ShowString(3, 1, "AD3:");
	OLED_ShowString(4, 1, "AD4:");
	
	while(1)
	{

		OLED_ShowNum(1, 5, AD_value[0], 4);
		OLED_ShowNum(2, 5, AD_value[1], 4);
		OLED_ShowNum(3, 5, AD_value[2], 4);
		OLED_ShowNum(4, 5, AD_value[3], 4);
		Delay_ms(100);
	}
}

还可以和定时器进行结合,定时器触发ADC,ADC转换后就会自动触发DMA,此时就完全自动了,且不过多消耗CPU资源。


http://www.kler.cn/a/613840.html

相关文章:

  • 机器学习knnlearn4
  • 如何同步fork的更新
  • 利用python调接口获取物流标签,并转成PDF保存在指定的文件夹。
  • Scala课后总结(2)
  • 内核编程十一:进程的数据结构
  • springboot 实现base64格式wav转码并保存
  • 第 4 章 | Solidity安全 权限控制漏洞全解析
  • GelSight视触觉3D显微系统在透明材料检测中的应用
  • Go红队开发—CLI框架(二)
  • 网络华为HCIA+HCIP 防火墙
  • SpringBoot第一节
  • 当编程语言有了人格
  • HCIP(TCP)(2)
  • 「HTML5+Canvas实战」星际空战游戏开发 - 纯前端实现 源码即开即用【附演示视频】
  • 2025 年中国家电零售与创新趋势解析:以旧换新国补激活需求,AI 技术渗透至研发、供应链、营销
  • 优秀的 React 入门开源项目推荐
  • 蓝桥杯第 12 天 109 国赛第一题 分考场(干了一个小时的题)
  • CSS3学习教程,从入门到精通,CSS3 定位布局页面知识点及案例代码(18)
  • C++类与对象的第一个简单的实战练习-3.24笔记
  • 20250328易灵思FPGA的烧录器FT4232_DL的驱动安装