0xAA55 发表于 2018-12-5 00:20:16

【STM32】踩了个SPI与DMA的坑

我想写一个能被多个文件共享的异步SPI库,能同时驱动多个SPI设备进行通讯的玩意儿。
并且这些设备有的能工作在很高的波特率下,而有的则很十分缓慢——这个差距不是一点半点,而是36 MHz到100 KHz(SD卡初始化需求)之间的差距(360倍)。在这种情况下使用阻塞式模型去等待传输完成再干别的,就会显得非常没有效率,尤其是你需要和一个低波特率的设备进行通讯的时候,干等的这段时间足以完成一些简单的小计算,比如与其它的UART设备通讯,或者进行ADC采样等(用于制作示波器等)。

我给我的SPI库同时封装了PIO和DMA两种方式,并且两者可以混合使用。但由于我脑补了SPI与DMA外设的协作方式,走了一些弯路,主要症状是:DMA并没有按照我预想的去吧数据传递给SPI的DR寄存器,而是“磨洋工”。为了调试这个问题,我“愤怒地”用ST-Util直接看STM32F103的外设地址寄存器(用看内存的方式来看),发现DMA1_Channel3的CNDTR寄存器值一直为零,如下图:



如图所示,选中的那一格就是DMA1_Channel3的CNDTR寄存器的值,保持为0根本不动。这个的意思是:“我(DMA1通道3)没有数据可传输。”

但我使用软件方式把数据写入到SPI1的DR寄存器的时候,它是进行了传输的——我用PIO方式让STM32F103在ILI9341屏幕上随机绘制像素点的时候,我可以看到屏幕上随机出现了各种颜色的像素点。
而我对于DMA的测试方式是:我让它完成清屏,也就是用一种特定颜色写屏。我让它每在屏幕上画一个像素点就清一次屏,这样一来,如果屏幕上堆满了花花绿绿的像素,就意味着清屏的功能不行——DMA没有正确传输。

为了解决这个问题,我调整了我进行DMA方式传输的代码。原先的代码是这样的:



以上代码是配置DMA通道的部分,而对于DMA传输开启的代码我是怎么写的呢?请看下图:



我是这样想的:

我的SPI和DMA都是开启的状态,因为我调用了下面的两条函数:SPI_Cmd(ILI9341_SPIx, ENABLE);
DMA_Cmd(ILI9341_DMAx_ChannelTX, ENABLE);但我知道它并没有立即就开始传输数据,因为我没有让SPI外设请求DMA通道去获取需要传输的数据。

当我调用以下语句后,SPI1就会在TXE(传输缓冲区空)的时候请求DMA提供数据。这样它就能开启传输了:SPI_I2S_DMACmd(ILI9341_SPIx, SPI_I2S_DMAReq_Tx, ENABLE);当DMA传输进行的时候,SPI外设会因为传输的过程而进入“Busy”(繁忙)的状态。只要等到它不繁忙了,就可以确保数据已经完成传输了。




如图所示,DMA与SPI的协作方式就是:
TXE(传输缓冲区空)bit为1的时候,DMA写一个数据到SPI->DR,开启传输(SPI复制数据到它的发送缓冲区,根据高位和低位谁在前的顺序进行一边移位一边),这个过程同时也清零了TXE的值;

因为SPI的移位寄存器(发送缓冲区)在发送东西,所以传输缓冲区不为空,TXE为0,DMA的传输通道(对于SPI1,是DMA3;对于SPI2,是DMA5)等待下一个TXE信号,而SPI外设则每时钟每次往MOSI传输一个BIT时,也从MISO接收一个BIT,这叫“全双工”;

SPI传输完8个bit(此处暂不考虑16bit模式的情况,但原理相同)后,也同时收到了8个bit,于是完成一次传输的同时也完成了一次接收,接收缓冲区满,RXNE为1;

于此同时,要发送的数据也发送出去了,移位寄存器空了,于是TXE为1,此时往SPI->DR写入数据,它就能准备好下一轮的发送,那么DMA的传输通道将会继续写入数据;

因为RXNE的值是1,所以DMA的接收通道(对于SPI1,是DMA2;对于SPI2,是DMA4)从SPI->DR读取一次,将其写入到内存,这个过程读取了数据,所以RXNE为0;

因为DMA的传输通道写入了数据到SPI->DR,它就会开始进行新一轮的传输;
或者DMA传输通道不传输数据,那么SPI就处于待机状态。

我认为只需要通过SPI_I2S_DMACmd(ILI9341_SPIx, SPI_I2S_DMAReq_Tx, ENABLE);这句来让SPI允许其钦定的DMA的传输通道通过判断SPI的TXE状态来填写数据到SPI->DR从而完成发送,就可以启动DMA方式SPI发送数据的过程;
并且当SPI在发送数据的时候,只需要不断poll判断SPI的BSY状态(忙碌状态,也就是正在发送数据的状态)就可以判断DMA是否传输完成了。

但当我实际测试的时候,我发现这种方式并不能正确传输数据——实际的症状是,我成功进行了第一次DMA方式使用SPI传输数据;但当我想启动第二次传输的时候,我的单片机似乎卡在了等待传输的地方,也就是进入了“死等”的状态。换句话说,传输并没有按照我预期的开始。这种方式不可行。

为了找出问题,我重新读了一遍PDF(的对应章节)。然而也许是我读得太“潦草”,我只读出了“在SPI_CR2寄存器的使能位是‘使能’的时候,一个DMA的访问是请求了的(渣翻勿喷)”(A DMA access is requested when the enable bit in the SPI_CR2 register is enabled. 引用自en.CD00171190.pdf 第719页 25.3.9)

也就是说,我的理解没错,但它确实没有进行DMA方式的SPI传输。我曾怀疑过是中断没有触发而导致的(事实上,我犯过一个错误,我写过这样的代码:NVIC_InitStructure.NVIC_IRQChannel = DMA1_Channel2_IRQn | DMA1_Channel3_IRQn; 但当我看了stm32f10x.h后我发现,这些中断请求,其实都是连续的数值,而非位域,使用OR操作并不能一次性就开启多个IRQ请求。只能一个个单独开启。



我通过在中断处理过程里面插入asm ("bkpt");来创建断点,然后在编译、下载程序、运行后用ST-Util查看单片机的MCU Core可以看到PC停到了断点上。这可以证明中断其实已经被触发了。

中途我尝试过混合使用DMA方式与PIO方式来实现写屏,测试结果发现PIO方式工作正常——写SPI外设DR寄存器后,该传输的它也传输了。屏幕上对应坐标位置处的像素也变成了对应的颜色。

我上下检查了一下源码,确保自己只使用STM32的外设驱动提供的API,去掉了所有对bitband、外设寄存器的直接写入,依然不能解决问题——反而说:按照PDF写的去操作外设寄存器似乎并没有发现什么问题,而且看了外设驱动的源码,它就是直接读写外设驱动,写入的数值也是我想写入的,理论上编译出来的代码应该没有差异才对——实测了一下,编译出来的两份代码,一份是自己写外设,一份是用STM32外设驱动写外设,编译生成的指令几乎相同。

我查看了STM32的示例源码,其中有SPI与DMA协作的部分。我照抄了它的代码,创建了我自己的工程,然后我把SPI1和SPI2的MOSI与MISO都接好,CS也接好,调试的结果与示例里面提到的效果相同——我的工程配置没有问题,并且示例看起来也没问题——关闭优化,可以看到“奴隶”(从)的接收缓冲区里接收到了来自“老大”(主)发送的正确的数据。

但有一点不同:STM32示例源码、SPI大类里DMA的例子,它是这样写的:/* Enable SPI_SLAVE Rx request */
SPI_I2S_DMACmd(SPI_SLAVE, SPI_I2S_DMAReq_Rx, ENABLE);

/* Enable SPI_SLAVE */
SPI_Cmd(SPI_SLAVE, ENABLE);
/* Enable SPI_MASTER */
SPI_Cmd(SPI_MASTER, ENABLE);

/* Enable DMA1 Channel4 */
DMA_Cmd(SPI_SLAVE_Rx_DMA_Channel, ENABLE);它先开启SPI对于DMA方式读取数据的一个开关,然后开启SPI,再开启DMA。是不是这个顺序很敏感呢?我也调整了我的代码的处理部分:我先用SPI_I2S_DMACmd开启我的SPI_I2S_DMAReq_Rx | SPI_I2S_DMAReq_Tx,然后我用SPI_Cmd启动SPI,再用DMA_Cmd启动DMA。调试结果发现,运行正常——我的SPI外设终于发送了正确的数据,在ILI9341上,我用PIO方式和DMA方式传输的图像它都正常显示了。

仔细想了一下,我觉得这个问题在于,STM32对于DMA传输是否开启的过程,由DMA_Cmd(对应写DMA外设寄存器)的过程产生的边沿触发来决定,而不是由SPI_I2S_DMACmd设置的SPI外设的“DMA使能”进行电平触发来决定。
虽然感觉问题其实挺简单,但我这个调试也是够走弯路的。坑爹的PDF

总而言之,一步步照着示例去改,改一下调试一下,准没错。
页: [1]
查看完整版本: 【STM32】踩了个SPI与DMA的坑