HC32F460 SPI寄存器发送+DMA发送
2025-10-06
软件学习笔记
00

目录

SPI寄存器发送
DMA发送
官方配置
我的配置

项目里面要使用一个SPI接口的LCD屏幕,所以研究了一下HC32F460的SPI寄存器发送,以及如何配合DMA发送大量数据。

SPI寄存器发送

在官方的SPI单主机发送例程中,使用的函数是SPI_Trans():

c
展开代码
int32_t SPI_Trans(CM_SPI_TypeDef *SPIx, const void *pvTxBuf, uint32_t u32TxLen, uint32_t u32Timeout) { uint32_t u32Flags; int32_t i32Ret = LL_ERR_INVD_PARAM; if ((pvTxBuf != NULL) && (u32TxLen != 0U)) { u32Flags = READ_REG32_BIT(SPIx->CR1, SPI_CR1_TXMDS); if (u32Flags == SPI_SEND_ONLY) { /* Transmit data in send only mode. */ i32Ret = SPI_Tx(SPIx, pvTxBuf, u32TxLen, u32Timeout); } else { /* Transmit data in full duplex mode. */ i32Ret = SPI_TxRx(SPIx, pvTxBuf, NULL, u32TxLen, u32Timeout); } } return i32Ret; }

实际发送调用的是SPI_Tx()这个函数,定义如下:

c
展开代码
static int32_t SPI_Tx(CM_SPI_TypeDef *SPIx, const void *pvTxBuf, uint32_t u32Len, uint32_t u32Timeout) { __IO uint32_t u32TxCnt = 0U; uint32_t u32BitSize; int32_t i32Ret = LL_OK; __IO uint32_t u32FrameCnt; uint32_t u32FrameNum = READ_REG32_BIT(SPIx->CFG1, SPI_CFG1_FTHLV) + 1UL; DDL_ASSERT(0UL == (u32Len % u32FrameNum)); /* Get data bit size, SPI_DATA_SIZE_4BIT ~ SPI_DATA_SIZE_32BIT */ u32BitSize = READ_REG32_BIT(SPIx->CFG2, SPI_CFG2_DSIZE); while (u32TxCnt < u32Len) { u32FrameCnt = 0UL; while (u32FrameCnt < u32FrameNum) { if (u32BitSize <= SPI_DATA_SIZE_8BIT) { /* SPI_DATA_SIZE_4BIT ~ SPI_DATA_SIZE_8BIT */ WRITE_REG32(SPIx->DR, ((const uint8_t *)pvTxBuf)[u32TxCnt]); } else if (u32BitSize <= SPI_DATA_SIZE_16BIT) { /* SPI_DATA_SIZE_9BIT ~ SPI_DATA_SIZE_16BIT */ WRITE_REG32(SPIx->DR, ((const uint16_t *)pvTxBuf)[u32TxCnt]); } else { /* SPI_DATA_SIZE_20BIT ~ SPI_DATA_SIZE_32BIT */ WRITE_REG32(SPIx->DR, ((const uint32_t *)pvTxBuf)[u32TxCnt]); } u32FrameCnt++; u32TxCnt++; } /* Wait TX buffer empty. */ i32Ret = SPI_WaitStatus(SPIx, SPI_FLAG_TX_BUF_EMPTY, SPI_FLAG_TX_BUF_EMPTY, u32Timeout); if (i32Ret != LL_OK) { break; } } if ((SPI_MASTER == READ_REG32_BIT(SPIx->CR1, SPI_CR1_MSTR)) && (i32Ret == LL_OK)) { i32Ret = SPI_WaitStatus(SPIx, SPI_FLAG_IDLE, 0UL, u32Timeout); } return i32Ret; }

其中最核心的操作就是:

  1. 判断发送的次数:
c
展开代码
uint32_t u32FrameNum = READ_REG32_BIT(SPIx->CFG1, SPI_CFG1_FTHLV) + 1UL; DDL_ASSERT(0UL == (u32Len % u32FrameNum));
  1. 根据帧长,往数据寄存器DR中写数据:
c
展开代码
if (u32BitSize <= SPI_DATA_SIZE_8BIT) { /* SPI_DATA_SIZE_4BIT ~ SPI_DATA_SIZE_8BIT */ WRITE_REG32(SPIx->DR, ((const uint8_t *)pvTxBuf)[u32TxCnt]); } else if (u32BitSize <= SPI_DATA_SIZE_16BIT) { /* SPI_DATA_SIZE_9BIT ~ SPI_DATA_SIZE_16BIT */ WRITE_REG32(SPIx->DR, ((const uint16_t *)pvTxBuf)[u32TxCnt]); } else { /* SPI_DATA_SIZE_20BIT ~ SPI_DATA_SIZE_32BIT */ WRITE_REG32(SPIx->DR, ((const uint32_t *)pvTxBuf)[u32TxCnt]); }
  1. 等待发送缓冲区为空后继续发送:
c
展开代码
/* Wait TX buffer empty. */ i32Ret = SPI_WaitStatus(SPIx, SPI_FLAG_TX_BUF_EMPTY, SPI_FLAG_TX_BUF_EMPTY, u32Timeout);
  1. 等待SPI总线空闲后退出函数:
c
展开代码
if ((SPI_MASTER == READ_REG32_BIT(SPIx->CR1, SPI_CR1_MSTR)) && (i32Ret == LL_OK)) { i32Ret = SPI_WaitStatus(SPIx, SPI_FLAG_IDLE, 0UL, u32Timeout); }

因为我这里只需单字节发送,所以只需要如下代码:

c
展开代码
CM_SPIx->DR = Data; while ((CM_SPIx->SR & SPI_SR_TDEF) == 0); while ((CM_SPIx->SR & SPI_SR_IDLNF) == 1);

发数据看看效果: 比起库函数发送来说快了不少:

如果说在发送完数据之后有片选(CS),或者数据(DC)之类引脚的拉高拉低,最好在GPIO操作之前加入几个__NOP();(无操作指令),避免GPIO提前翻转。如下图的黄色波形(DC引脚),在0x2B和0x2C指令未完全发送之前就已经翻转: 加入__NOP();即可解决:

c
展开代码
// 向显示屏发送命令的程序 __inline static void GC9D01_SendCmd(uint8_t Cmd){ // pin DC LOW LCD_DC_LOW; CM_SPI3->DR = Cmd; while ((CM_SPI3->SR & SPI_SR_TDEF) == 0); while ((CM_SPI3->SR & SPI_SR_IDLNF) == 1); // 寄存器操作太快,使用空操作调整时序 __NOP(); __NOP(); __NOP(); __NOP(); __NOP(); __NOP(); // pin DC HIGH LCD_DC_HIGH; }

DMA发送

官方配置

同样翻看官方例程spi_dma,关键代码如下:
配置DMA源地址,目的地址,完成中断并初始化

c
展开代码
/* DMA configuration */ FCG_Fcg0PeriphClockCmd(DMA_CLK, ENABLE); (void)DMA_StructInit(&stcDmaInit); stcDmaInit.u32BlockSize = 1UL; stcDmaInit.u32TransCount = EXAMPLE_SPI_BUF_LEN; stcDmaInit.u32DataWidth = DMA_DATAWIDTH_8BIT; /* Configure TX */ stcDmaInit.u32SrcAddrInc = DMA_SRC_ADDR_INC; stcDmaInit.u32DestAddrInc = DMA_DEST_ADDR_FIX; stcDmaInit.u32SrcAddr = (uint32_t)(&u8TxBuf[0]); stcDmaInit.u32DestAddr = (uint32_t)(&SPI_UNIT->DR); if (LL_OK != DMA_Init(DMA_UNIT, DMA_TX_CH, &stcDmaInit)) { for (;;) { } } AOS_SetTriggerEventSrc(DMA_TX_TRIG_CH, SPI_TX_EVT_SRC); /* Configure RX */ stcDmaInit.u32IntEn = DMA_INT_ENABLE; stcDmaInit.u32SrcAddrInc = DMA_SRC_ADDR_FIX; stcDmaInit.u32DestAddrInc = DMA_DEST_ADDR_INC; stcDmaInit.u32SrcAddr = (uint32_t)(&SPI_UNIT->DR); stcDmaInit.u32DestAddr = (uint32_t)(&u8RxBuf[0]); if (LL_OK != DMA_Init(DMA_UNIT, DMA_RX_CH, &stcDmaInit)) { for (;;) { } } AOS_SetTriggerEventSrc(DMA_RX_TRIG_CH, SPI_RX_EVT_SRC); /* DMA receive NVIC configure */ stcIrqSignConfig.enIntSrc = DMA_RX_INT_SRC; stcIrqSignConfig.enIRQn = DMA_RX_IRQ_NUM; stcIrqSignConfig.pfnCallback = &DMA_TransCompleteCallback; (void)INTC_IrqSignIn(&stcIrqSignConfig); NVIC_ClearPendingIRQ(stcIrqSignConfig.enIRQn); NVIC_SetPriority(stcIrqSignConfig.enIRQn, DDL_IRQ_PRIO_DEFAULT); NVIC_EnableIRQ(stcIrqSignConfig.enIRQn); /* Enable DMA and channel */ DMA_Cmd(DMA_UNIT, ENABLE); DMA_ChCmd(DMA_UNIT, DMA_TX_CH, ENABLE); DMA_ChCmd(DMA_UNIT, DMA_RX_CH, ENABLE); } /** * @brief SPI configure. * @param None * @retval None */

循环中调用:

c
展开代码
DMA_ReloadConfig(); /* Enable SPI */ SPI_Cmd(SPI_UNIT, ENABLE); /* Waiting for completion of reception */ while (RESET == enRxCompleteFlag) { } /* Disable SPI */ SPI_Cmd(SPI_UNIT, DISABLE);

DMA_ReloadConfig()函数

c
展开代码
static void DMA_ReloadConfig(void) { DMA_SetSrcAddr(DMA_UNIT, DMA_TX_CH, (uint32_t)(&u8TxBuf[0])); DMA_SetTransCount(DMA_UNIT, DMA_TX_CH, EXAMPLE_SPI_BUF_LEN); DMA_SetDestAddr(DMA_UNIT, DMA_RX_CH, (uint32_t)(&u8RxBuf[0])); DMA_SetTransCount(DMA_UNIT, DMA_RX_CH, EXAMPLE_SPI_BUF_LEN); /* Enable DMA channel */ DMA_ChCmd(DMA_UNIT, DMA_TX_CH, ENABLE); DMA_ChCmd(DMA_UNIT, DMA_RX_CH, ENABLE); }

完成中断函数:

c
展开代码
static void DMA_TransCompleteCallback(void) { enRxCompleteFlag = SET; DMA_ClearTransCompleteStatus(DMA_UNIT, DMA_RX_INT_CH); }

我的配置

DMA以及中断初始化:
源地址为自增,目的地址固定,数据位宽8bit,开启发送完成中断,DMA触发条件为SPI3发送缓冲区空。

c
展开代码
//DMA Config static void App_DMAxCfg(void) { stc_dma_init_t stcDmaInit; stc_irq_signin_config_t stcIrqSignConfig; /* DMA configuration */ FCG_Fcg0PeriphClockCmd(FCG0_PERIPH_DMA1 | FCG0_PERIPH_AOS, ENABLE); (void)DMA_StructInit(&stcDmaInit); stcDmaInit.u32IntEn = DMA_INT_ENABLE; stcDmaInit.u32BlockSize = 1UL; stcDmaInit.u32DataWidth = DMA_DATAWIDTH_8BIT; /* Configure SPI TX */ stcDmaInit.u32SrcAddrInc = DMA_SRC_ADDR_INC; stcDmaInit.u32DestAddrInc = DMA_DEST_ADDR_FIX; // stcDmaInit.u32SrcAddr = (uint32_t)(&u8TxBuf[0]); stcDmaInit.u32DestAddr = (uint32_t)(&CM_SPI3->DR); if (LL_OK != DMA_Init(CM_DMA1, DMA_CH0, &stcDmaInit)) { for (;;) { } } // 配置触发源 AOS_SetTriggerEventSrc(AOS_DMA1_0, EVT_SRC_SPI3_SPTI); /* DMA receive NVIC configure */ stcIrqSignConfig.enIntSrc = INT_SRC_DMA1_TC0; stcIrqSignConfig.enIRQn = INT006_IRQn; stcIrqSignConfig.pfnCallback = &DMA_TransCompleteCallback; (void)INTC_IrqSignIn(&stcIrqSignConfig); NVIC_ClearPendingIRQ(stcIrqSignConfig.enIRQn); NVIC_SetPriority(stcIrqSignConfig.enIRQn, DDL_IRQ_PRIO_14); NVIC_EnableIRQ(stcIrqSignConfig.enIRQn); /* Enable DMA */ DMA_Cmd(CM_DMA1, ENABLE); // DMA_ChCmd(CM_DMA1, DMA_CH0, ENABLE); }

调用DMA发送:
设置源地址,目的地址,启动DMA

c
展开代码
// 向显示屏发送数据(参数)的程序 批量 __inline static void GC9D01_SendDataMASS(uint8_t* buff, size_t buff_size){ DEBUG_IN; DMA_SetSrcAddr(CM_DMA1, DMA_CH0, (uint32_t)(&buff[1])); DMA_SetTransCount(CM_DMA1, DMA_CH0, (uint16_t)(buff_size - 1)); DMA_ChCmd(CM_DMA1, DMA_CH0, ENABLE); CM_SPI3->DR = buff[0]; // SPI_Trans(CM_SPI3, buff, 1, 0xfffff); DEBUG_OUT; }

完成中断函数:
取消片选信号

c
展开代码
void DMA_TransCompleteCallback(void){ DEBUG_IN; GC9D01_Unselect(); // DMA_Cmd(CM_DMA1, DISABLE); // DMA_ChCmd(CM_DMA1, DMA_CH0, DISABLE); DMA_ClearTransCompleteStatus(CM_DMA1, DMA_INT_TC_CH0); DEBUG_OUT; }

下面解释一下为什么要这样配置。

  • 在DMA的初始化中,只需要配置源地址为自增,目的地址固定,数据位宽8bit,DMA触发条件为SPI发送缓冲区空。源地址,目的地址和数据长度都是在调用DMA发送时配置,所以初始化的时候不用管(如果要初始化的话要把存数据的数组extern出来,我只是懒得搞)。DMA中断可开可不开,我要在数据发送完成的时候取消片选,所以才开了完成中断,如果没有类似需求可以不开。// 初始化完成之后开启DMA外设,但是不要开启对应的通道
  • DMA的触发条件:
    这里有一点坑,手册中关于DMA应用举例的那部分是这样举例的: 也就是说DMA的触发必须选择发送缓冲区为空,不能手动软件触发。这就会带来一个问题:如果我配置了DMA,但是由于开始搬运的时间我不能手动干预,就会导致在某次正常发送数据之后,缓冲区空的信号触发DMA搬运数据到SPI发送缓冲区,破坏掉数据的顺序。因此在初始化DMA之后,只开启DMA模块,不要开启对应的通道,DMA就不会响应来自SPI的触发信号。只在调用DMA之前开启通道,然后在完成中断中关闭通道。
    所以一开始我的调用DMA发送的函数是这样写的:
c
展开代码
__inline static void GC9D01_SendDataMASS(uint8_t* buff, size_t buff_size){ DEBUG_IN; DMA_SetSrcAddr(CM_DMA1, DMA_CH0, (uint32_t)(&buff[0])); DMA_SetTransCount(CM_DMA1, DMA_CH0, (uint16_t)buff_size); DMA_ChCmd(CM_DMA1, DMA_CH0, ENABLE); // SPI_Trans(CM_SPI3, buff, 1, 0xfffff); DEBUG_OUT; }

但是在调用完函数之后数据并没有发送出来,反倒是在下一个正常不使用DMA发送的数据后面发出来了(((゚Д゚;)))
思来想去也不知道怎么办,又看了下例程,在例程里面,SPI默认是关闭的,只在DMA发送时才开启。官方的例程很明显是只考虑了DMA发送数据的场景,没有考虑到像我这样既有正常发送,又有DMA发送的情况(我这才是正常应用场景好不好( º﹃º ))。如果要像官方那样,又要改一堆代码。我是懒狗,不可能改的
又翻了下手册的SPI章节,发现DMA的触发信号来自SPTI: SPTI在缓冲区空和满的时候都是低电平,只有满状态切换到空状态的时候才会出现一个脉冲,难道说DMA是靠这个脉冲信号触发的吗。改了下代码,手动发送一个数据去触发SPTI脉冲:

c
展开代码
__inline static void GC9D01_SendDataMASS(uint8_t* buff, size_t buff_size){ DEBUG_IN; DMA_SetSrcAddr(CM_DMA1, DMA_CH0, (uint32_t)(&buff[0])); DMA_SetTransCount(CM_DMA1, DMA_CH0, (uint16_t)buff_size); DMA_ChCmd(CM_DMA1, DMA_CH0, ENABLE); CM_SPI3->DR = 0x00; // SPI_Trans(CM_SPI3, buff, 1, 0xfffff); DEBUG_OUT; }

改完之后DMA果然能正常发送数据了,但是这样写有个问题,会导致多发一个数据,导致后面的数据错位。

那么可以这样改:手动发送的数据改为DMA数据中的第一个数据,DMA起始地址从第二个数据开始,发送的数据长度也减一

c
展开代码
// 向显示屏发送数据(参数)的程序 批量 __inline static void GC9D01_SendDataMASS(uint8_t* buff, size_t buff_size){ DEBUG_IN; DMA_SetSrcAddr(CM_DMA1, DMA_CH0, (uint32_t)(&buff[1])); DMA_SetTransCount(CM_DMA1, DMA_CH0, (uint16_t)(buff_size - 1)); DMA_ChCmd(CM_DMA1, DMA_CH0, ENABLE); CM_SPI3->DR = buff[0]; // SPI_Trans(CM_SPI3, buff, 1, 0xfffff); DEBUG_OUT; }

这样数据就不会错位了 而且末尾的CS信号(黄色信号)也能在完成中断中被正确拉高: 哦对,还有一件事:
DMA的通道会在一次完整的DMA传输之后被自动关闭,同时源地址指针也到了数据的末尾。所以DMA传输完成中断中不需要手动关闭DMA通道,只需要在DMA发送前再次把源地址指针指向数据开头,并重新开启DMA通道即可。

本文作者:zxcli

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!