在STM32中,循环DMA外设到内存在传输结束时将如何表现



我想问,在以下情况下,STM32中的DMA SPI rx将如何运行。我有一个指定的(例如)96字节数组,名为a,用于存储从SPI接收的数据。我打开我的循环SPI DMA,它在每个字节上运行,配置为96字节。在循环DMA开始写入A(并销毁保存在B中的数据)之前,当DMA将填充我的96字节数组时,传输完成中断是否会关闭,以快速将96字节数组复制到另一个-B?我想(每次我从B中的A获得新数据时)通过USB将B的数据快速传输到PC。

我只是在想如何通过USB将STM32的连续数据流SPI传输到PC,因为我认为每特定时间通过USB传输一次96字节的数据块比通过STM32将实时SPI传输到USB更容易?我甚至不知道

要做到这一点,您必须能够保证在接收到下一个SPI字节并将其传输到缓冲区的开头之前复制所有数据。这是否可能取决于处理器的时钟速度和SPI的速度,并且能够保证不会发生可能延迟传输的更高优先级中断。为了安全起见,它需要非常慢的SPI速度,在这种情况下可能根本不需要使用DMA。

总而言之,这是个坏主意,完全没有必要。DMA控制器有一个"半传输"中断正是为了这个目的。当传输前48个字节时,您将获得HT中断,当您复制下半部分缓冲区时,DMA将继续传输剩余的48个字节。当你完成转移时,你转移上半部分。这将数据传输时间从单个字节的接收时间延长到48字节的接收速度。

如果每次传输实际需要96个字节,那么只需将缓冲区设置为192个字节长(2x96)。

在伪代码中:

#define BUFFER_LENGTH 96
char DMA_Buffer[2][BUFFER_LENGTH] ;
void DMA_IRQHandler()
{
if( DMA_IT_Flag(DMA_HT) == SET )
{
memcpy( B, DMA_Buffer[0], BUFFER_LENGTH ) ;
Clear_IT_Flag(DMA_HT) ;
}
else if( DMA_IT_Flag(DMA_TC) == SET )
{
memcpy( B, DMA_Buffer[1], BUFFER_LENGTH ) ;
Clear_IT_Flag(DMA_TC) ;
}
}

关于通过USB将数据传输到PC,首先你需要确保你的USB传输速率至少与SPI传输速率一样快或更快。USB传输可能不太确定(因为它由PC主机控制,也就是说,只有当主机明确要求时,你才能在USB上输出数据),所以即使平均传输速率足够,也可能存在需要进一步缓冲的延迟,所以与其简单地从DMA缓冲区A复制到USB缓冲区B,您可能需要一个循环缓冲区或FIFO队列来馈送USB。另一方面,如果您已经有了缓冲区DMA_Buffer[0]DMA_Buffer[1]B,那么您实际上已经有了一个96字节的三个块的FIFO,这可能就足够了

在我的一个项目中,我遇到了类似的问题。任务是通过全速USB将来自外部ADC芯片(与SPI连接)的数据传输到PC。数据是(8信道x 16位),我被要求实现尽可能快的采样频率。

我最终得到了一个三重缓冲解决方案。缓冲区有4种可能的状态:

  1. 就绪:缓冲区已满数据,可通过USB发送
  2. 发送:缓冲区已发送且过期
  3. IN_USE:DMA(SPI请求)当前正在填充此缓冲区
  4. 下一步:此缓冲区被视为空,将在IN_USE已满时使用

由于USB请求的时间无法与SPI进程同步,我认为双缓冲区解决方案不起作用。如果您没有NEXT缓冲区,那么当您决定发送READY缓冲器时,DMA可能会完成IN_USE的填充,并开始损坏READY缓冲区。但在三缓冲区解决方案中,READY缓冲区可以安全地通过USB发送,因为即使当前in_USE缓冲器已满,它也不会被填满。

因此,随着时间的推移,缓冲区状态看起来是这样的:

Buf0     Buf1      Buf2
====     ====      ====
READY    IN_USE    NEXT
SENT     IN_USE    NEXT
NEXT     READY     IN_USE
NEXT     SENT      IN_USE
IN_USE   NEXT      READY

当然,如果电脑启动USB请求的速度不够快,一旦READY缓冲区变成NEXT(在变成发送之前),您可能仍然会释放它。PC异步发送USB IN请求,没有关于当前缓冲区状态的信息。如果没有READY缓冲区(它处于SENT状态),STM32会用ZLP(零长度包)进行响应,PC会在延迟1ms后重试。

对于在STM32上的实现,我使用了双缓冲模式,并修改了M0AR&M1AR在DMA传输完成ISR中注册到地址3缓冲区。

顺便说一句,我使用了(3 x 4000)字节的缓冲区,并在最后实现了32 kHz的采样频率。USB被配置为供应商特定的类,它使用批量传输。

通常,使用循环DMA只有在半满/半空触发时才有效,否则您没有足够的时间从缓冲区复制信息。

我建议不要在中断期间将数据从缓冲区中复制出来。而是直接使用缓冲区中的数据,而不需要额外的复制步骤。

如果在中断中进行复制,则在复制过程中会阻止其他优先级较低的中断。在STM32上,48字节的简单原始字节拷贝可能需要额外的48*6~300个时钟周期。

如果独立跟踪缓冲区的读写位置,则只需要更新一个指针,并向缓冲区的使用者发出延迟的通知调用。

如果您想要更长的周期,那么不要使用循环DMA,而是在48字节块中使用普通DMA,并将循环字节缓冲区作为数据结构来实现。

我这样做是为了一个460k波特的USART,它接收异步可变长度的数据包。如果您确保生产者只更新写指针,消费者只更新读指针,则可以避免大部分指针中的数据竞争。请注意,对齐<皮层上的32位变量m3/m4是原子的。

所包含的代码是我使用的具有DMA支持的循环缓冲区的简化版本。它被限制为2^n的缓冲区大小,并使用Templates和C++11功能,因此根据您的开发/平台限制,它可能不合适。

要使用缓冲区,请调用getDmaReadBlock()或getDMAwriteBlock()并获取DMA内存地址和块长度。DMA完成后,使用skipRead()/skipWrite()将读取或写入指针增加实际传输量。

/**
* Creates a circular buffer. There is a read pointer and a write pointer
* The buffer is full when the write pointer is = read pointer -1
*/
template<uint16_t SIZE=256>
class CircularByteBuffer {
public:
struct MemBlock {
uint8_t  *blockStart;
uint16_t blockLength;
};
private:
uint8_t *_data;
uint16_t _readIndex;
uint16_t _writeIndex;
static constexpr uint16_t _mask = SIZE - 1;
// is the circular buffer a power of 2
static_assert((SIZE & (SIZE - 1)) == 0);
public:
CircularByteBuffer &operator=(const CircularByteBuffer &) = default;
CircularByteBuffer(uint8_t (&data)[SIZE]);
CircularByteBuffer(const CircularByteBuffer &) = default;
~CircularByteBuffer() = default;
private:
static uint16_t wrapIndex(int32_t index);
public:
/*
* The number of byte available to be read. Writing bytes to the buffer can only increase this amount.
*/
uint16_t readBytesAvail() const;
/**
* Return the number of bytes that can still be written. Reading bytes can only increase this amount.
*/
uint16_t writeBytesAvail() const;
/**
* Read a byte from the buffer and increment the read pointer
*/
uint8_t readByte();
/**
* Write a byte to the buffer and increment the write pointer. Throws away the byte if there is no space left.
* @param byte
*/
void writeByte(uint8_t byte);
/**
* Provide read only access to the buffer without incrementing the pointer. Whilst memory accesses outside the
* allocated memeory can be performed. Garbage data can still be read if that byte does not contain valid data
* @param pos the offset from teh current read pointer
* @return the byte at the given offset in the buffer.
*/
uint8_t operator[](uint32_t pos) const;
/**
* INcrement the read pointer by a given amount
*/
void skipRead(uint16_t amount);
/**
* Increment the read pointer by a given amount
*/
void skipWrite(uint16_t amount);

/**
* Get the start and lenght of the memeory block used for DMA writes into the queue.
* @return
*/
MemBlock getDmaWriteBlock();
/**
* Get the start and lenght of the memeory block used for DMA reads from the queue.
* @return
*/
MemBlock getDmaReadBlock();
};
// CircularByteBuffer
// ------------------
template<uint16_t SIZE>
inline CircularByteBuffer<SIZE>::CircularByteBuffer(uint8_t (&data)[SIZE]):
_data(data),
_readIndex(0),
_writeIndex(0) {
}
template<uint16_t SIZE>
inline uint16_t CircularByteBuffer<SIZE>::wrapIndex(int32_t index){
return static_cast<uint16_t>(index & _mask);
}
template<uint16_t SIZE>
inline uint16_t CircularByteBuffer<SIZE>::readBytesAvail() const {
return wrapIndex(_writeIndex - _readIndex);
}
template<uint16_t SIZE>
inline uint16_t CircularByteBuffer<SIZE>::writeBytesAvail() const {
return wrapIndex(_readIndex - _writeIndex - 1);
}
template<uint16_t SIZE>
inline uint8_t CircularByteBuffer<SIZE>::readByte() {
if (readBytesAvail()) {
uint8_t result = _data[_readIndex];
_readIndex = wrapIndex(_readIndex+1);
return result;
} else {
return 0;
}
}
template<uint16_t SIZE>
inline void CircularByteBuffer<SIZE>::writeByte(uint8_t byte) {
if (writeBytesAvail()) {
_data[_writeIndex] = byte;
_writeIndex = wrapIndex(_writeIndex+1);
}
}
template<uint16_t SIZE>
inline uint8_t CircularByteBuffer<SIZE>::operator[](uint32_t pos) const {
return _data[wrapIndex(_readIndex + pos)];
}
template<uint16_t SIZE>
inline void CircularByteBuffer<SIZE>::skipRead(uint16_t amount) {
_readIndex = wrapIndex(_readIndex+ amount);
}
template<uint16_t SIZE>
inline void CircularByteBuffer<SIZE>::skipWrite(uint16_t amount) {
_writeIndex = wrapIndex(_writeIndex+ amount);
}
template <uint16_t SIZE>
inline typename CircularByteBuffer<SIZE>::MemBlock  CircularByteBuffer<SIZE>::getDmaWriteBlock(){
uint16_t len = static_cast<uint16_t>(SIZE - _writeIndex);
// full is  (write == (read -1)) so on wrap around we need to ensure that we stop 1 off from the read pointer.
if( _readIndex == 0){
len = static_cast<uint16_t>(len - 1);
}
if( _readIndex > _writeIndex){
len = static_cast<uint16_t>(_readIndex - _writeIndex - 1);
}
return {&_data[_writeIndex], len};
}
template <uint16_t SIZE>
inline typename CircularByteBuffer<SIZE>::MemBlock  CircularByteBuffer<SIZE>::getDmaReadBlock(){
if( _readIndex > _writeIndex){
return {&_data[_readIndex], static_cast<uint16_t>(SIZE- _readIndex)};
} else {
return {&_data[_readIndex], static_cast<uint16_t>(_writeIndex - _readIndex)};
}
}
`

最新更新