目录
项目里面要使用一个SPI接口的LCD屏幕,所以研究了一下HC32F460的SPI寄存器发送,以及如何配合DMA发送大量数据。
SPI寄存器发送
在官方的SPI单主机发送例程中,使用的函数是SPI_Trans():
cint32_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()这个函数,定义如下:
cstatic 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;
}
其中最核心的操作就是:
- 判断发送的次数:
cuint32_t u32FrameNum = READ_REG32_BIT(SPIx->CFG1, SPI_CFG1_FTHLV) + 1UL;
DDL_ASSERT(0UL == (u32Len % u32FrameNum));
- 根据帧长,往数据寄存器DR中写数据:
cif (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);
- 等待SPI总线空闲后退出函数:
cif ((SPI_MASTER == READ_REG32_BIT(SPIx->CR1, SPI_CR1_MSTR)) && (i32Ret == LL_OK)) {
i32Ret = SPI_WaitStatus(SPIx, SPI_FLAG_IDLE, 0UL, u32Timeout);
}
因为我这里只需单字节发送,所以只需要如下代码:
cCM_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()函数
cstatic 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);
}
完成中断函数:
cstatic 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;
}
完成中断函数:
取消片选信号
cvoid 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 许可协议。转载请注明出处!