说一说DMA是什么东西,DMA本身的意思是Direct Memory Access,直接存取访问,可以看到这只是一种存取方式,或者说读写方式,或是直白点来说,就是直接读取,说的太直白了,感觉这个DMA这个词在脑子里感觉有点SB了,就这么一个破烂玩意儿起这个这个类似遇到DNA一样的玩意儿。

直接存取或者直接读取写入什么呢?当然是数据了,从哪里读,或者往哪里写呢?

这个问题好,后面会说到读取的位置和写入的位置。

今天说的这个DMA不是解释这个读写方式的,其实也没什么读写方式可言,就和普通的SPI,I2C一样,我也可以叫他们直接读写

有人肯定说你这个解释错了,好吧,是错了!因为没有体现出Direct "直接"这个意思,直接的意思是不需要通过CPU,可以把数据从特定地址读出来。

哇靠!认知有点感觉弱爆,从小到大都是被告诉CPU是数据运算的中枢神经!你一个不需要中枢神经,不需要大脑的植物人做法是怎么实现的?!!!

话先说回来,DMA这个东西只是中存取方式,在我们的MCU中,使用DMA控制器才可以实现直接存取访问,也就是说使用DMA控制器才可以实现DMA操作

DMA控制器提供了一种“硬件的方式”在外设和存储器之间或者存储器和存储器之间的传输数据,而不需要CPU介入,从而释放带宽,(其实是释放CPU,可以让CPU去做其他活)

如果你做个其他传输总线设备的驱动比如说I2C设备,SPI设备,你就知道,有I2C 控制器,SPI控制器,那么这里是一个简单的DMA控制器

DMA控制器的工作,特别是需要在软件中实现的内容,比前面i2C,SPI ,你一定要觉得他更简单,难的是是理解(仔细品味这句话,你就觉得这是废话)

先贴一张我也不知道为什么这个时候要贴出来的图片:(看到DMA了吗,仔细看,看不到就算了)

可以看到DMA位于AHB总线矩阵中,AHB你就记得它是一个很重要的高速总线得了,对应有APB外设总线,它的频率可能比AHB低一点,比较是外设嘛

从图片上就可以看出,我这里是用的一款GD32E10X的国产MCU为例子介绍的,它的主要特征呢,我也懒得打字了,贴个图:

可以看出,最大传输数据长度是65536也就是是2的十六次方,二的十次方是1K,那么2的十六次就是16K了,sorry 64k

64k对于一款嵌入式的MCU来说,我感觉足够了,不要抬杠,[旺财]

通道什么的不管,不过要说一点,不同的通道对应不同的外设地址,这句看不懂直接往下看。

然后是说的源端和目的端。其实就是读取和写入地址。

后面说的是传输模式啊,中断这些,懒的说了,自己体会下,如果体会不出来,留个言吧。(其实留言我也不一定回复)

下面一句话我看了之后感觉很经典这是SPEC上说的:

DMA传输分两步:从源地址读取数据,之后将读取的数据存储到目的地址。。。。这我想起了把大象放入冰箱分几步了

这SPEC的撰写人之前一定是说相声的。

DMA控制器基于DMA_CHxPADDR,DMA_CHxMADDR,DMA_CHxCTL寄存器的值计算下一次操作的源/目的地址。

DMA_CHxCNT寄存器用于控制传输的次数。

DMA_CHxCTL寄存器的PWIDTH和MWIDTH位域决定每次发送和接收的字节数(字节、半字,字)

这些对关键寄存器的介绍很不错,如果你仔细去体会它要表达的意思,你会有很多疑问,会带领你去思考。

DMA_CHxCNT寄存器的CNT位域必须在CHEN位置位前被配置,其控制传输的次数。在传输过程中,CNT位域的值表示还有多少次数据传输将被执行。

这句话可以品味一下,一旦你设置了读取的地址,然后设置CNT(也就是读取的次数),那么它就会读取你设置的次数。

但是它必须在CHEN为被置位前设置。如果将DMA_CHxCTL寄存器的CHEN位清零,可以停止DMA传输。

地址生成

存储器和外设都独立的支持两种地址生成算法:固定模式和增量模式。寄存器DMA_CHxCTL的PNAGA和MNAGA位用来设置存储器和外设的地址生成算法。

在固定模式中,地址一直固定为初始化的基地址(DAM_CHxPADDR,DMA_CHxMADDR).

在增量模式中,下一次传输数据的地址是当前地址加1(或者2,4),这个值取决于数据传输宽度。

循环模式

循环模式用来处理连续的外设请求(如ADC扫描模式)。将DMA_CHxCTL寄存器的CMEN位置位可以使能循环模式。

在循环模式中,当每次DMA传输完成后,CNT值会被重新载入,且传输完成标志位会被置1.DMA会一直响应外设的请求,知道通道使能位(DMA_CHxCTL寄存器的CHEN位)被清0.

存储器到存储器模式

将DMA_CHxCTL寄存器的M2M位置位可以使能存储器到存储器模式。在此模式下,DMA通道传输数据时不依赖外设的请求信号。一旦DMA_CHxCTL寄存器的CHEN位被置1,DMA通道就离家开始传输数据,直到DMA_CHXCNT寄存器达到0,DMA通道才会停止。

通道配置

要启动一次新的DMA数据传输,建议遵循以下步骤进行操作:

1:读取CHEN位,判断通道是否使能。如果为1(通道已经使能),清零改位。当CHEN为0时,请按照下列步骤配置DMA,启动新的传输。

2:配置DMA_CHxCTL寄存器的M2M以及DIR位,选择传输模式

3:配置DMA_CHxCTL寄存器的CMEN位,注意这里是CMEN不是CHEN,选择是否使能循环模式。

4:配置DMA_CHxCTL寄存器的PRIO位,选择该通道的软件优先级。

5:通过DMA_CHxCTL寄存器配置存储器和外设的传输宽度以及存储器和外设地址生成算法。这里的传输宽度是否对速度有大的影响?可以测下一个字的宽度和一个字节的宽度的速度差异。生成算法,读取肯定是固定算法了,接收是增量算法,因为做SPI读取的时候只能通过SPI的SPI_DATA寄存器读取数据,写入是写入内存中的连续区域,要按照读取宽度写入对应的地址,注意这里的地址偏移是对应的传输宽度。

6:通过DMA_CHxCTL寄存器配置传输完成中断,半传输完成中断,传输错误中断的使能位,中断都可以配置起来看看,看下传输完成的中断是否有被调用到,半传输完成中断是否是传输了一半给出的中断,传输错误中断是什么样子的

7:通过DMA_CHxPADDR寄存器配置外设基地址。SPI Flash的话外设的基地址就是SPI_DATA这个数据寄存器,这个寄存器是32位的,这个可以考虑最大的位数传输

8:通过DMA_CHxMADDR寄存器配置存储器基地址。

9:通过DMA_CHxCNT寄存器配置设计及传输总量。

10:将DMA_CHxCTL寄存器的CHEN位置1,使能DMA通道。

中断:

每个DMA通道都有一个专用的中断。中断事件有三种类型:传输完成,半传输完成和传输错误。每一个中断事件在DMA_INTF寄存器中有专用的标志位,在DMA_INTC寄存器中有专用的清除位,在DMA_CHxCTL寄存器中有专用的使能位。

中断其实还是比较容易理解的。毕竟我们在嵌入式开发中中断很常见。

这里如果你要使用中断,

nvic_irq_enable(DMA0_Channel3_IRQn,0,0);
dma_interrupt_enable(DMA0, DMA_CH3, DMA_INT_FTF);

然后在gd32e10x_it.c中实现中断handler就可以了,

void DMA0_Channel3_IRQHandler(void)
{if(dma_interrupt_flag_get(DMA0, DMA_CH3, DMA_INT_FLAG_FTF)){     dma_interrupt_flag_clear(DMA0, DMA_CH3, DMA_INT_FLAG_G);}
}

注意一点你使用哪个channel就实现哪个channle的中断。或者你要有在中断代码写完之后重新检查的习惯。

DMA 请求映射

多个外设请求被映射到同一个DMA通道。这些请求信号在经过逻辑或后进入DMA。通过配置对应外设的寄存器,每个外设的请求均可以独立的开启或者关闭。用户必须确保同一时间,在同一个通道上仅有一个外设的请求被开启。

这里截图是为了说明,你要用哪个外设的DMA功能,或者说DMA控制器要去写入或者读取哪个外设,你要找的可以用在这个外设的DMA以及channel.

比如我现在要用DMA去读取SPI1的数据,你就需要用DMA0 CH3这个通道,因为可以看到到只有DMA0 通道3支持SPI1 R。

下面有个例子,是SPI + DMA的操作,之前是纯SPI操作,现在是通过SPI+DMA的方式去读取spi接口的nor flash的操作

用的还是GD flash

之前用纯SPI读取flash数据的接口是:

/*!\brief      read a block of data from the flash\param[in]  pbuffer: pointer to the buffer that receives the data read from the flash\param[in]  read_addr: flash's internal address to read from\param[in]  num_byte_to_read: number of bytes to read from the flash\param[out] none\retval     none
*/
spiflash_ret spiflash_buffer_read(uint8_t* pbuffer, uint32_t read_addr, uint16_t num_byte_to_read)
{spiflash_ret ret = spiflash_ret_success;/* select the flash: chip slect low */SPI_FLASH_CS_LOW();/* send "read from memory " instruction */spi_flash_send_byte(READ);/* send read_addr high nibble address byte to read from */spi_flash_send_byte((read_addr & 0xFF0000) >> 16);/* send read_addr medium nibble address byte to read from */spi_flash_send_byte((read_addr& 0xFF00) >> 8);/* send read_addr low nibble address byte to read from */spi_flash_send_byte(read_addr & 0xFF);/* while there is data to be read */while(num_byte_to_read--){/* read a byte from the flash */*pbuffer = spi_flash_send_byte(DUMMY_BYTE);/* point to the next location where the byte read will be saved */pbuffer++;}/* deselect the flash: chip select high */SPI_FLASH_CS_HIGH();return ret;
}

可以看到在读取命令和地址发送之后,有一个while循环,就是这个while循环来读取数据的,可以看出这样一个while循环似乎花费了太多的时间,

但是做技术可以不是只是看的,你可以用这种方式读取比如说1M的数据量看看,看看耗时是多少,再用后面的SPI + DMA 的方式去读取做过对比:

下面这个函数是利用这个上面的函数做的一个修改,当然主要修改的是while循环部分,我们前面还是利用纯SPI去发送读取命令和发送地址,

在接收数据的时候我们来看下。

如果我们要用SPI+DMA的方式来读取,我们不妨看下SEPC,看下SPI介绍中对于DMA有没有介绍。

上面这个截图就是SPI接口的描述中对于DMA的介绍

这主要是说如果你要用DMA来传输SPI的TX 和 RX数据,要做一下使能SPI模式的DMA动作,下面在代码中有说明:

static void dma0_ch3_init(void)
{/* enable DMA clock */rcu_periph_clock_enable(RCU_DMA0);nvic_irq_enable(DMA0_Channel3_IRQn,0,0);dma0_ch3_test_config();
}

这里在SPI的初始化同时会调用DMA CH3的初始化,时钟,和DMA0 CH3 IRQ,

void dma0_ch3_test_config(void)
{memset(g_destbuf,0x00,sizeof(g_destbuf));dma_parameter_struct dma_init_struct;/* initialize DMA channel 3 */dma_deinit(DMA0, DMA_CH3);//先要deinit一下dma_struct_para_init(&dma_init_struct);//将这个结构体中的数据全部初始化为0,dma_init_struct.direction = DMA_PERIPHERAL_TO_MEMORY;//这里我们只是做个测试,从外设(SPI)读取数据到内存dma_init_struct.memory_addr = (uint32_t)g_destbuf;//这个是个数组,也就是前面说的内存dma_init_struct.memory_inc = DMA_MEMORY_INCREASE_ENABLE;//在内存中需要采用自动增长的方式dma_init_struct.memory_width = DMA_MEMORY_WIDTH_8BIT;//每次读取8bit,当然SPI 的DATA寄存器是32位的寄存器你可以读取32位dma_init_struct.number = TRANSFER_NUM;//这个就是你这次DAM传输需要传输的字节数dma_init_struct.periph_addr = (uint32_t)&SPI_DATA(SPI1);//这里就是外设的地址,DMA读取数据都是从外设读取的,这里相对于DMA来说,SPI就是外设,是ARM内核的外设dma_init_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE;//SPI的Data寄存器是数据的唯一读入地址,SPI的数据在传输的时候都会不断的写入到这个寄存器,所以我们读取数据都只读一个寄存器,地址并不会增加改变dma_init_struct.periph_width = DMA_PERIPHERAL_WIDTH_8BIT;//外设的读取位数也是8位dma_init_struct.priority = DMA_PRIORITY_ULTRA_HIGH;//优先级高,这个在多个外设都要用到DMA的时候会用到dma_init(DMA0, DMA_CH3, &dma_init_struct);//把我们的这些设置都写入到DMA的对应寄存器中/* DMA channel 0 mode configuration */dma_circulation_disable(DMA0, DMA_CH3);//不采用循环模式,读完就结束dma_memory_to_memory_disable(DMA0, DMA_CH3);//不是从memory到memory的读取,所以disable/* DMA channel 0 interrupt configuration */dma_interrupt_enable(DMA0, DMA_CH3, DMA_INT_FTF);//enable 中断,如果你需要用到中断的话/* enable DMA transfer *///dma_channel_enable(DMA0, DMA_CH3);//以为你这里只是初始化,先不要enable DMA,只有在真正用的时候单独调用DMA,就可以传输数据了。
}

好了,初始化结束了,看下我们的读取操作,这里是采用DMA读取数据后在后面直接把数据打印出来

/*!\brief      read a block of data from the flash\param[in]  pbuffer: pointer to the buffer that receives the data read from the flash\param[in]  read_addr: flash's internal address to read from\param[in]  num_byte_to_read: number of bytes to read from the flash\param[out] none\retval     none
*/
spiflash_ret spiflash_dma_read(uint8_t* pbuffer, uint32_t read_addr, uint16_t num_byte_to_read)
{spiflash_ret ret = spiflash_ret_success;/* select the flash: chip slect low */SPI_FLASH_CS_LOW();/* send "read from memory " instruction */spi_flash_send_byte(READ);/* send read_addr high nibble address byte to read from */spi_flash_send_byte((read_addr & 0xFF0000) >> 16);/* send read_addr medium nibble address byte to read from */spi_flash_send_byte((read_addr& 0xFF00) >> 8);/* send read_addr low nibble address byte to read from */spi_flash_send_byte(read_addr & 0xFF);spi_parameter_struct spi_init_struct;/* SPI1 parameter config */spi_init_struct.trans_mode           = SPI_TRANSMODE_RECEIVEONLY;spi_init_struct.device_mode          = SPI_MASTER;spi_init_struct.frame_size           = SPI_FRAMESIZE_8BIT;spi_init_struct.clock_polarity_phase = SPI_CK_PL_LOW_PH_1EDGE;spi_init_struct.nss                  = SPI_NSS_SOFT;spi_init_struct.prescale             = SPI_PSC_8;spi_init_struct.endian               = SPI_ENDIAN_MSB;spi_init(SPI1, &spi_init_struct);dma_channel_enable(DMA0, DMA_CH3);while(g_dmacomplete_flag == 0);/* deselect the flash: chip select high */SPI_FLASH_CS_HIGH();uint8_t i = 0;for(i = 0; i < 8; i++)printf("[%d] = %d\r\n",i,g_destbuf[i]);printf("g_destbuf = %s strlen(g_destbuf) = %d\r\n",g_destbuf,strlen(g_destbuf));return ret;
}

主要是下面这一部分:

spi_parameter_struct spi_init_struct;/* SPI1 parameter config */spi_init_struct.trans_mode           = SPI_TRANSMODE_RECEIVEONLY;spi_init_struct.device_mode          = SPI_MASTER;spi_init_struct.frame_size           = SPI_FRAMESIZE_8BIT;spi_init_struct.clock_polarity_phase = SPI_CK_PL_LOW_PH_1EDGE;spi_init_struct.nss                  = SPI_NSS_SOFT;spi_init_struct.prescale             = SPI_PSC_8;spi_init_struct.endian               = SPI_ENDIAN_MSB;spi_init(SPI1, &spi_init_struct);dma_channel_enable(DMA0, DMA_CH3);while(g_dmacomplete_flag == 0);

我们看到之前的函数在读取的时候都会发送0xFF,然后才能获取数据,这里我看了为网友的介绍

在这里可以把SPI trans mode设置为RECEIVE ONLY模式,这样就不用发送0xff,可以直接用DMA来读取了。

真的感谢这位网友,他的博客地址https://blog.csdn.net/chenwei2002/article/details/49722373

因为他用的STM32的,在设置SPI trans mode的时候我设置错了,是用了

把这个函数当成了设置receive only的设置了

/*!\brief      configure SPI bidirectional transfer direction\param[in]  spi_periph: SPIx(x=0,1,2)\param[in]  transfer_direction: SPI transfer directiononly one parameter can be selected which is shown as below:\arg        SPI_BIDIRECTIONAL_TRANSMIT: SPI work in transmit-only mode\arg        SPI_BIDIRECTIONAL_RECEIVE: SPI work in receive-only mode\param[out] none\retval     none
*/
void spi_bidirectional_transfer_config(uint32_t spi_periph, uint32_t transfer_direction)
{if(SPI_BIDIRECTIONAL_TRANSMIT == transfer_direction){/* set the transmit-only mode */SPI_CTL0(spi_periph) |= (uint32_t)SPI_BIDIRECTIONAL_TRANSMIT;}else{/* set the receive-only mode */SPI_CTL0(spi_periph) &= SPI_BIDIRECTIONAL_RECEIVE;}
}

其实不是他,设置为Receive only是

spi_init_struct.trans_mode           = SPI_TRANSMODE_RECEIVEONLY;

,然后写入对应的SPI寄存器

至于while(g_dmacomplete_flag == 0);这句话,我是在中断中对这个变量设置为了1

所以这里是一直等待DMA操作完成才去将片选拉高,否则会出现读取错误的问题

总的来说,介绍的例子比之前讲的理论浅薄很多,其实例子在实现过程中,或者说我自己在摸索过程中遇到了比想象更多的问题。

好在现在可以通过DMA来实现数据传输了。

后面我会利用SPI+DMA 和纯SPI来做下对比,对比速度可以提升多少

SPI + DMA相关推荐

  1. STM32G0 硬件SPI+DMA+LL库,最高通讯速率32MBit/s

    硬件SPI+DMA+LL库代码连接STM32G0硬件SPI+DMA+LL库,最高通讯速率32MBit/s-嵌入式文档类资源-CSDN下载 近期在写0.96寸OLED显示屏驱动的程序,之前试过用软件模拟 ...

  2. [SPI+DMA] 驱动WS2812B显示时钟

    [SPI+DMA] 驱动WS2812B显示时钟 实现原理 本人一个大学萌新,偶然间发现RGB时钟这神奇的玩意,就想试试.目前初步实现时钟显示,第一次写博客,只是为了记录自己的学习成果方便以后查看,大家 ...

  3. STM32 SPI+DMA 驱动 SRAM LY68L6400SLIT 应用笔记

    关键词:库函数,STM32F407,SPI+DMA ,SPI-DMA,SRAM , LY68L6400SLIT,STM32CubeMX 编 辑:大黄蜂 说明:本笔记记录 基于 STM32F407 + ...

  4. 两块STM32之间 SPI DMA通信

    @[两块STM32之间 SPI DMA通信] 这里讲的是两块STM32F407板子的互相通讯,折腾了3天,终于比较清楚了,特此记录. 两块板子互相通讯,除了SPI的接线,肯定还有其他信号线,必须注意这 ...

  5. 0.96寸OLED(二)SPI+DMA刷新OLED

    SPI+DMA刷新OLED 文章目录 SPI+DMA刷新OLED 原理简介 一.修改缓存区大小 二.修改SSD1306的指令 三.替换原来的刷新函数 验证结果 原理简介 OLED屏幕有三种刷新方式分别 ...

  6. W5100S SPI+DMA 中的片选信号处理

    最近在玩W5100S-L这个芯片. 使用SPI+DMA时,CPU在执行完DMA函数调用后会继续执行后续代码,而DMA的TC中断也并不是数据完全传完的时刻(而是最后一个数据放入缓存区后就TC中断),同时 ...

  7. STM32F413 SPI+DMA接收错误(SCK时钟接收后一直存在)【后参考网上论坛及F4官方例程:收发同时进行】

    1:平台 STM32F413 + GD5F4(spi nand flash)(没开启DMA中断,读写操作NAND 都是单片机发起,所以知道什么时候读,什么时候写) 2: 问题(STM32 SPI+DM ...

  8. stm32H7 SPI和SPI DMA时间差异对比

    偶然发现spi dma如果在单个字节速度会比spi模式慢很多, 因此做一个对比 SPI速度设置为16M 1 单个字节收发 SPI模式 可以看到每个字节的数据之间有不是连续的, 中间会有一段时间没有数据 ...

  9. GD32 SPI DMA收发

    由于需求用到GD32 SPI, 故做相关实验记录分享,本实验为 SPI DMA 发送与接收 16bit 数据, GD32 相关配置如下 GD32 F30x系列 DMA配置如下 SPI DMA 发:使用 ...

  10. STM32F4与STM32L4,SPI DMA HAL 关闭片选 时机探讨

    STM32F4与STM32L4,SPI DMA HAL 关闭片选 时机探讨 我使用STM32F407,标准库 + SPI + DMA 通信,发送接收数据. 当我们配置好SPI,DMA发送模式后,首先开 ...

最新文章

  1. 利用tuning-primer脚本优化MySQL数据库
  2. 暑假想打比赛,小白怎么从0入门?
  3. 两步改动CentOS主机名称
  4. 学习LINUX的几点注意事项
  5. Spring 2.5 基于注解驱动的 Spring MVC
  6. Class Activation Mapping (CNN可视化) Python示例
  7. Geospark-SQL加载SHP数据
  8. 关于Promise.all()的理解
  9. java netty socket_Java-彻底弄懂netty-原来netty是这样启动的-知识铺
  10. NB-IoT在智慧烟感解决方案中的应用
  11. CVPR 2021 | 微软提出解构式关键点回归, 刷新COCO自底向上多人姿态检测记录!...
  12. 简单理解L0、L1与L2范数
  13. 硬件开发者之路之——保护电路系列之输入电源端口的防护设计
  14. linux消息队列优缺点,linux消息队列概念
  15. 三峡大坝水位落差达40层楼高,船舶要如何翻越?
  16. 上传应用到苹果一直卡住验证中(authentication with the app store)问题解决
  17. 在unity中创建python机器学习环境
  18. SecureCRT自动保存日志设置
  19. 电脑连接了网络却打不开网页
  20. “Papi酱”之后,走向平庸的短视频,还有什么机会?

热门文章

  1. 微信小程序 - Setting data field “xxx“ to undefined is invalid.报错原因及解决方案
  2. simulink中固定大小矩阵和可变大小矩阵创建的几个陷阱
  3. 照妖镜纳米“相机”:让反应历程无处遁形
  4. ubuntu系统下更改python路径
  5. 【CSS】CSS之旋转动画
  6. DOM深入学习 --- DOM 树介绍,如何遍历 DOM 树节点(一)
  7. #UVM# 搞清验证平台中的 uvm_top、uvm_test_top及module top的层次关系
  8. 项目怎么改服务器版本,服务器上的项目怎样设置jdk版本
  9. 用python写抢红包程序的具体代码
  10. MongoDB 客户端 MongoVue