欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

SPI + DMA

程序员文章站 2024-02-22 10:25:34
...

说一说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了吗,仔细看,看不到就算了)

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。通过配置对应外设的寄存器,每个外设的请求均可以独立的开启或者关闭。用户必须确保同一时间,在同一个通道上仅有一个外设的请求被开启。

SPI + 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

上面这个截图就是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 direction
                only 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来做下对比,对比速度可以提升多少

 

 

 

相关标签: MCU dma