最近项目需要,要用wch32v003驱动ST7735S,用硬件spi+DMA方式可以提高屏幕刷新率,但是使用过程遇到一下问题,分享出来,有清楚的大佬可以指点指点。
这篇文章并不是给着急移植程序使用的人看的,因为在赶进度的时候都是希望越快实现越好,不会细细琢磨,如果你有时间可以耐心看完,希望对你有所帮助。
下面是spi+dma代码部分,屏幕部分的代码就先不放了,可以去我另外一篇文章里面移植。让你学会写ST7735s驱动LCD程序(SPI)-CSDN博客
硬件SPI+DMA驱动0.96寸ST7735S(含代码)_st7735s 指令0x2a-CSDN博客
/********************************** (C) COPYRIGHT *******************************
* File Name : main.c
* Author : WCH
* Version : V1.0.0
* Date : 2022/08/08
* Description : Main program body.
*********************************************************************************
* Copyright (c) 2021 Nanjing Qinheng Microelectronics Co., Ltd.
* Attention: This software (modified or not) and binary are used for
* microcontroller manufactured by Nanjing Qinheng Microelectronics.
*******************************************************************************/
/*
*@Note
*SPI DMA, master/slave mode transceiver routine:
*Master:PI1_SCK(PC5)\PI1_MISO(PC7)\PI1_MOSI(PC6).
*Slave:PI1_SCK(PC5)\PI1_MISO(PC7)\PI1_MOSI(PC6).
*
*This example demonstrates simultaneous full-duplex transmission and reception
*between Master and Slave.
*Note: The two boards download the Master and Slave programs respectively,
*and power on at the same time.
* Hardware connection:
* PC5 -- PC5
* PC6 -- PC6
* PC7 -- PC7
*
*/
#include "debug.h"
#include "string.h"
#include "lcd_init.h"
/* SPI Mode Definition */
#define HOST_MODE 0
#define SLAVE_MODE 1
/* SPI Communication Mode Selection */
#define SPI_MODE HOST_MODE
//#define SPI_MODE SLAVE_MODE
/* Global define */
#define Size 18
/* Global Variable */
u16 TxData[Size] = {0x1234, 0x5678, 0x9abc, 0xe123, 0x4567, 0x89ab,
0xcde0, 0x1234, 0x5678, 0x9abc, 0xe123, 0x4567,
0x89ab, 0xcde0, 0x1234, 0x5678, 0x9abc,0xe123};
u16 RxData[Size];
/*********************************************************************
* @fn SPI_FullDuplex_Init
*
* @brief Configuring the SPI for full-duplex communication.
*
* @return none
*/
void SPI_FullDuplex_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure = {0};
SPI_InitTypeDef SPI_InitStructure = {0};
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC | RCC_APB2Periph_SPI1, ENABLE);
#if(SPI_MODE == HOST_MODE)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOC, &GPIO_InitStructure);
// GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7;
// GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
// GPIO_Init(GPIOC, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOC, &GPIO_InitStructure);
#elif(SPI_MODE == SLAVE_MODE)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOC, &GPIO_InitStructure);
// GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7;
// GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
// GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
// GPIO_Init(GPIOC, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOC, &GPIO_InitStructure);
#endif
SPI_InitStructure.SPI_Direction = SPI_Direction_1Line_Tx;
#if(SPI_MODE == HOST_MODE)
SPI_InitStructure.SPI_Mode = SPI_Mode_Master;
#elif(SPI_MODE == SLAVE_MODE)
SPI_InitStructure.SPI_Mode = SPI_Mode_Slave;
#endif
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;
SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;
SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_64;
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
SPI_InitStructure.SPI_CRCPolynomial = 0;
SPI_Init(SPI1, &SPI_InitStructure);
SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Tx, ENABLE);
// SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Rx, ENABLE);
SPI_Cmd(SPI1, ENABLE);
}
/*********************************************************************
* @fn DMA_Tx_Init
*
* @brief Initializes the DMAy Channelx configuration.
*
* @param DMA_CHx - x can be 1 to 7.
* ppadr - Peripheral base address.
* memadr - Memory base address.
* bufsize - DMA channel buffer size.
*
* @return none
*/
void DMA_Tx_Init(DMA_Channel_TypeDef *DMA_CHx, u32 ppadr, u32 memadr, u16 bufsize)
{
DMA_InitTypeDef DMA_InitStructure = {0};
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
DMA_DeInit(DMA_CHx);
DMA_InitStructure.DMA_PeripheralBaseAddr = ppadr;
DMA_InitStructure.DMA_MemoryBaseAddr = memadr;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;
DMA_InitStructure.DMA_BufferSize = bufsize;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh;
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
DMA_Init(DMA_CHx, &DMA_InitStructure);
}
/*********************************************************************
* @fn DMA_Rx_Init
*
* @brief Initializes the SPI1 DMA Channelx configuration.
*
* @param DMA_CHx - x can be 1 to 7.
* ppadr - Peripheral base address.
* memadr - Memory base address.
* bufsize - DMA channel buffer size.
*
* @return none
*/
void DMA_Rx_Init(DMA_Channel_TypeDef *DMA_CHx, u32 ppadr, u32 memadr, u16 bufsize)
{
DMA_InitTypeDef DMA_InitStructure = {0};
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
DMA_DeInit(DMA_CHx);
DMA_InitStructure.DMA_PeripheralBaseAddr = ppadr;
DMA_InitStructure.DMA_MemoryBaseAddr = memadr;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
DMA_InitStructure.DMA_BufferSize = bufsize;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
DMA_Init(DMA_CHx, &DMA_InitStructure);
}
/*********************************************************************
* @fn main
*
* @brief Main program.
*
* @return none
*/
int main(void)
{
SystemCoreClockUpdate();
Delay_Init();
USART_Printf_Init(115200);
printf("SystemClk:%d\r\n", SystemCoreClock);
printf( "ChipID:%08x\r\n", DBGMCU_GetCHIPID() );
SPI_FullDuplex_Init();
#if(SPI_MODE == SLAVE_MODE)
printf("Slave Mode\r\n");
Delay_Ms(1000);
#endif
#if(SPI_MODE == HOST_MODE)
printf("Host Mode\r\n");
Delay_Ms(100);
#endif
LCD_GPIO_Init();
LCD_RST_CLR;
DMA_Tx_Init(DMA1_Channel3, (u32)&SPI1->DATAR, (u32)TxData, 15);
// DMA_Rx_Init(DMA1_Channel2, (u32)&SPI1->DATAR, (u32)RxData, Size);
DMA_Cmd(DMA1_Channel3, ENABLE);
// DMA_Cmd(DMA1_Channel2, ENABLE);
while(!DMA_GetFlagStatus(DMA1_FLAG_TC3));
LCD_RST_SET;
while(1)
{
}
}
之所以没有放屏幕点亮部分是因为屏幕没有点亮,但是我用逻辑分析仪抓取spi的波形发现我数据传输都是正常的。
但是一直驱动不了屏幕,我最近用了很多spi,说实话软件硬件spi都已经比较熟悉了,既然发送通信没有问题,我想问题可能出现在RST或者DC数据/指令这两个线上了。经过我的测试发现,问题出现在DC数据/指令这个线上,我代码里有一个测试的地方
就是让一个引脚拉低,然后启用dma发送,然后等待发送完成,最后再将引脚拉高,最后看看波形。
最下面的就是这个翻转的引脚,可以看到在数据并没有传输完成的时候,引脚已经被拉高了,所以在驱动屏幕的时候,会出现DC线操作和数据、指令发送不匹配的问题,当然无法驱动屏幕了。
然后我又把目光放在了等待传输完成这个地方,会不会是这个用错了
while(!DMA_GetFlagStatus(DMA1_FLAG_TC3));
点进去看发现这个函数就是读寄存器是否置1,我猜测是判断dma传输完成中断标志位,于是我查阅了一下寄存器使用手册。
可以看到DMA_INTFR的TCIFx就是完成时候的标志位,由硬件置位,再看看具体的是第几位。
DMA_INTFR的地址是0x40020000
找到上面说的TCIF位,因为我代码里发送的使用dma的通道3,所以我们要找的是TCIF3,也就是第9位,换成十六进制就是0x00002000
再看代码里的
可以看到跟我的推断完全一样,这说明函数没有用错。
但是在我总结的时候,再看一眼文字描述的时候发现了一个之前没有注意到的地方
再看看这句话,当dma的传输字节数目减至0将会产生DMA传输完成标志
看到这里我大概猜测是因为dma启动传输一个字节后就会减一,但是其实最后那里只是启动了传输,但是并没有真正意义上的传输完成,所以这里有一个时间差。我们可以验证一下这个猜想。(顺便提一嘴,我现在一直找问题的习惯都是提出猜想,想办法去验证它,如果不对那就继续重复,直到找到为止)
我们先计算一下spi+dma的时钟频率,芯片的时钟频率是48M
SPI我用的是64分频,所以就是48M/64=0.75M,也就是750kHz,一个脉冲周期就是1/75K=0.000001333s,也就是1.33us,按50%的占空比算,scl的一个高电平或者低电平持续时间大概是1.33/2=0.665us,差不多是600ns,用逻辑分析仪验证一下。(这里我提一嘴,要是有逻辑分析仪真的很有用,尤其是这种高速通信中的问题,很难找到问题所在,用传统示波器也不好分析,有条件大家可以整一个,牌子就不说了,tb上搜有挺多的,免得说我打广告)
实际的是650ns左右,有误差很正常,周期1.3us,跟我算的一样。
芯片时钟是48Mhz,一个时钟周期是1/48M=0.020833us,一个机器周期包含12个时钟周期,所以一个机器周期是12*0.020833=0.25us,简单指令运行是一个机器周期时间,大约250nm
这里可以看出确实是在DMA开始传输后大约一个指令周期的时间引脚就被拉高了,可以验证上面的猜想应该是正确的。
既然找到的问题,验证了猜想,下一步就是解决问题,我的想法是用延时函数,简单粗暴,如果大家有什么好的方法也可以提出,大家一起讨论进步。
算了一下最后是剩下16位(这里还有点疑问就是为什么描述是1个字节为单位,而这里剩下2字节,这个问题还有待研究,有知道的也欢迎提出),一个周期大概是1.3us,16个就是20.8us,保险起见我们延时25us。如果库函数自带延时us函数可以直接调用,如果没有可以参考我下面的函数。(我的库里是有延迟us函数的,所以我直接调用)
/* 简易的us级别的延迟函数 我的芯片时钟是48Mhz 一个时钟周期是1/48M=0.020833us,一个机器周期包含12个时钟周期,所以一个机器周期是12*0.020833=0.25us 所以这里i++运行一次是0.25us,1us大概是运行4次 */ void delay_us(uint16_t num) { for(uint16_t j=0;j<num;j++) { for(uint16_t i=0;i<4;i++){}//根据不同的芯片周期修改i的数值 } }
编译下载后再看波形,普天同庆,跟我的猜想一模一样,最后的引脚变化已经是完全在传输结束之后了,问题成功解决。
最后总结一下,我写csdn的初衷只有两个,总结自己,帮助别人。在嵌入式领域我也是一个新手,刚接触没几年,一路上也是看其他csdn文章一步步学过来的,不能说有什么成就,但是还是有一些小小的心得,希望可以帮助到刚踏上这条路的你。
首先我想说的是不要害怕底层!不要害怕底层!不要害怕底层!重要的事情说三遍!!
刚开始接触的时候特别讨厌底层、封装、寄存器那些,觉得特别难懂,所以一直喜欢逃避,就喜欢用别人的封装库,最好是一顿复制粘贴就可以使用的那种,但是后面我发现这样并不是学习,只是在搬运、缝合,我反问自己,这真的能学到什么呢?后面我想明白了,我要走出我自己的“舒适圈”,我下定决心要弄懂原理,慢慢接触底层知识。人最大的恐惧是源于无知,当你从未接触之前,你觉得什么都难,但是你沉下心来学习时,其实并没有想象中这么难,你只要告诉自己,别人能写,别人能看,为什么自己就不能呢?真正难的,是你踏出自己舒适圈的第一步。尝试去接触底层原理,去看寄存器手册,芯片手册。可以看看库函数封装的内容,本质其实还是操作寄存器。
第二,不仅要踏出舒适圈,还要不断的踏出舒适圈。之前总有一种想法,就是我学这么多差不多了,够用了已经,但是后面我慢慢醒悟过来,时代在发展,技术在进步,没有人可以说自己学得已经够多了,千万不要安于现状!!!一定要不断学习,不断前进,不断提升自我,嵌入式这条道路上是没有所谓的尽头的,大家都在不断向前,如逆水行舟,不进则退。
第三,不但要学,还要应用。应用才能体现出你的理解和存在的问题,可以查漏补缺,可以加深理解,所以多去找项目做,多去用。
第四,学会总结,我觉得总结才是最重要的。以前的我对总结是嗤之以鼻,现在的我是逐字分析。总结也是学习的过程,像是上面提到了那句话也是我在总结的过程中发现的,不但可以加深自己的理解,整合自己的想法。学会总结,才算真正的学习。
希望可以对大家有所帮助、有所启发。说的不对的地方也欢迎大家批评指正。