项目里面要使用一个SPI接口的LCD屏幕,所以研究了一下HC32F460的SPI寄存器发送,以及如何配合DMA发送大量数据。
在官方的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;
}
其中最核心的操作就是:
c展开代码uint32_t u32FrameNum = READ_REG32_BIT(SPIx->CFG1, SPI_CFG1_FTHLV) + 1UL;
DDL_ASSERT(0UL == (u32Len % u32FrameNum));
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]);
}
c展开代码/* Wait TX buffer empty. */
i32Ret = SPI_WaitStatus(SPIx, SPI_FLAG_TX_BUF_EMPTY, SPI_FLAG_TX_BUF_EMPTY, u32Timeout);
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;
}

同样翻看官方例程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的触发必须选择发送缓冲区为空,不能手动软件触发。这就会带来一个问题:如果我配置了DMA,但是由于开始搬运的时间我不能手动干预,就会导致在某次正常发送数据之后,缓冲区空的信号触发DMA搬运数据到SPI发送缓冲区,破坏掉数据的顺序。因此在初始化DMA之后,只开启DMA模块,不要开启对应的通道,DMA就不会响应来自SPI的触发信号。只在调用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 许可协议。转载请注明出处!