GD32F303调试小记(二)之SPI(软件SPI、硬件SPI、硬件SPI+DMA)

前言

目前有一个项目中用到了TFT-LCD,其驱动芯片为ILI9341。为更好的达到显示效果,在最终的代码中我们会使用单片机自带的硬件SPI+DMA模块(由于调试过程中SPI+DMA输出的波形没能驱屏成功,最终只使用GD32的硬件SPI,这里也请在阅读完本文后对这方面有调试经验的大哥给个建议,我会详细的说明其中的现象),以尽可能的减轻CPU的负荷。不过在前期调试过程中,我们会使用到软件模拟SPI以及单独的硬件SPI。由于我们只是使用SPI与屏显的驱动芯片通信,所以只使用SPI的发送,在接下来的代码演示中,不包含SPI接收。以后如果有机会用到SPI通信的FLASH存储芯片或其他,会再出一个调试文章。(关于驱屏显示和LVGL图形界面库的移植我会另出一篇文章详细介绍,有兴趣的朋友可以耐心等待。)

SPI

一种串行通信总线,包含一根时钟线一根片选线以及一根两根数据线

  • 若使用一根数据线代表SPI的TX与RX共用一根线,也就是3线SPI,发送数据与接收数据不能同步进行。
  • 若使用两根数据线代表SPI的TX与RX各自单独用一根线,也就是4线SPI,发送数据与接收数据能同步进行。
  • 片选线为从设备的SPI通信使能,这样在主机(如单片机)控制的SPI通信总线上可挂载多个可以SPI通信的从设备(这也意味着在多个从设备下片选线也是多个的)。
  • 时钟线为SPI通信的速率,就如单片机的晶振,人的心跳一样。数据线上的发送与接收肯定是随着时钟线上电平的跳变而进行。

这里不例举出SPI通信的典型时序图,因为凡是支持的SPI通信的从设备的芯片手册上都会有对应的通信时序,而且在下面的章节中会有实际示波器抓到的波形。

DMA

一个能帮助CPU大大减轻处理压力的助手,这里不多作介绍。

各模块程序编写

在配置前,请确保你已经有一个GD32F303包含其对应标准库的keil工程,工程可使用官方的例程或可按照GD32F303调试小记(零)之工程创建与编译创建。此外,强烈建议身边有个示波器逻辑分析仪,用于查看我们端口输出的通信波形。

一、时钟配置

  • 开启GPIO端口时钟、GPIO引脚复用时钟、DMA时钟和SPI2模块的时钟。
void SystemClock_Reconfig(void)
{
		/* Enable all peripherals clocks you need*/
		rcu_periph_clock_enable(RCU_GPIOA);
		rcu_periph_clock_enable(RCU_GPIOB);
		rcu_periph_clock_enable(RCU_GPIOC);
		rcu_periph_clock_enable(RCU_GPIOD);
		
		rcu_periph_clock_enable(RCU_DMA0);
		rcu_periph_clock_enable(RCU_DMA1);
//		rcu_periph_clock_enable(RCU_I2C1);
//		rcu_periph_clock_enable(RCU_ADC0);
//		rcu_periph_clock_enable(RCU_ADC2);
//		rcu_periph_clock_enable(RCU_USART1);
		rcu_periph_clock_enable(RCU_USART2);
		rcu_periph_clock_enable(RCU_SPI2);
		/* Timer1,2,3,4,5,6,11,12,13 are hanged on APB1,
		 * Timer0,7,8,9,10 			 are hanged on APB2
		 */
		rcu_periph_clock_enable(RCU_TIMER1);	

		rcu_periph_clock_enable(RCU_AF);
}

二、GPIO配置

请添加图片描述请添加图片描述
请添加图片描述

  • 根据上图中手册中对SPI2引脚的描述以及实际电路原理图中的接线,相关IO配置如下:
/* SPIx By Soft Or Hardware */
#define SPI2_SOFT			0		// 1:使用软件SPI				0:使用硬件SPI 这里使用软件SPI初始化屏,硬件SPI刷屏 
#define SPI2_DMA			0		//1:使用硬件SPI+DMA		0:仅使用硬件SPI(使用软件SPI时该宏无意义)

// SPI port and pins definition
#define SPI2_PORT						GPIOB
#define SPI2_SCK_PIN					GPIO_PIN_3
#define SPI2_MISO_PIN					GPIO_PIN_4
#define SPI2_MOSI_PIN					GPIO_PIN_5
#define SPI2_CS_PIN						GPIO_PIN_6 //NSS

// TFT port and pins definition
#define TFT_PORT						GPIOB
#define TFT_RS_PIN						GPIO_PIN_7		//1:发数据 0:发命令
#define TFT_RST_PIN						GPIO_PIN_8		//1:不复位 0:复位	 
#define TFT_BG_PIN						GPIO_PIN_9		//1:亮 		 0:不亮	

void GPIO_Init(void)
{
	/* 使用SW下载,不使用JTAG下载,管脚用作其它功能 */
	gpio_pin_remap_config(GPIO_SWJ_SWDPENABLE_REMAP, ENABLE);
	
#if SPI2_SOFT
	gpio_init(SPI2_PORT, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ, SPI2_CS_PIN | SPI2_MOSI_PIN | SPI2_SCK_PIN);	
	gpio_init(SPI2_PORT, GPIO_MODE_IN_FLOATING, GPIO_OSPEED_50MHZ, SPI2_MISO_PIN);
#else
	gpio_pin_remap_config(GPIO_SPI2_REMAP,DISABLE);
	gpio_init(SPI2_PORT, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, SPI2_MOSI_PIN | SPI2_SCK_PIN); // | SPI2_MISO_PIN
	gpio_init(SPI2_PORT, GPIO_MODE_IN_FLOATING, GPIO_OSPEED_50MHZ, SPI2_MISO_PIN);
	gpio_init(SPI2_PORT, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ, SPI2_CS_PIN);	

#endif
	/* demo board TFT_LCD I/O */
	gpio_init(TFT_PORT, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ, TFT_RST_PIN | TFT_BG_PIN);	
	gpio_init(TFT_PORT, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ, TFT_RS_PIN);

}

三、DMA配置

请添加图片描述

  • 由上图可知,SPI2的TX对应DMA1的CH1,SPI2的RX对应DMA1的CH0。
  • 由于是驱动屏的,代码里只需要写发送就可以了。
#define SPI2_TX_SIZE	8
uint8_t SPI2TX_Buffer[SPI2_TX_SIZE] = {0};

void DMA_Init(void)
{
	dma_parameter_struct dma_init_SPI2_TX={0};

	dma_deinit(DMA1, DMA_CH1);									//SPI2_TX

	/* initialize DMA1 channel1(SPI2_TX) */						
	dma_init_SPI2_TX.direction = DMA_MEMORY_TO_PERIPHERAL;
	dma_init_SPI2_TX.memory_addr = (uint32_t)SPI2TX_Buffer;
	dma_init_SPI2_TX.memory_inc = DMA_MEMORY_INCREASE_ENABLE;
	dma_init_SPI2_TX.memory_width = DMA_MEMORY_WIDTH_32BIT;
	dma_init_SPI2_TX.number = SPI2_TX_SIZE;
	dma_init_SPI2_TX.periph_addr = (uint32_t)(&SPI_DATA(SPI2));
	dma_init_SPI2_TX.periph_inc = DMA_PERIPH_INCREASE_DISABLE;
	dma_init_SPI2_TX.periph_width = DMA_PERIPHERAL_WIDTH_32BIT;
	dma_init_SPI2_TX.priority = DMA_PRIORITY_LOW;
	dma_init(DMA1, DMA_CH1, &dma_init_SPI2_TX);	
	dma_circulation_disable(DMA1, DMA_CH1);
	
	dma_memory_to_memory_disable(DMA1,DMA_CH1);					//SPI2_TX
	/* enable all DMA channels you need */
	dma_channel_disable(DMA1,DMA_CH1);							//SPI2_TX
}

四、SPI配置

  • 主机全双工模式、数据位8位、时钟线空闲高电平第2个边沿检测、不适用硬件NSS、SPI波特率为27MHz、数据先发送高位。
  • 其中spi2_init.clock_polarity_phase可以换成其它、spi2_init.prescale在最初调试时设置的慢一点如几百kHz。
void SPIx_Init(void)
{
	spi_parameter_struct spi2_init = {0};
  /* deinitilize SPI and the parameters */
  spi_i2s_deinit(SPI2);

	/* SPI2 parameter config */
	spi2_init.device_mode			= SPI_MASTER;
	spi2_init.trans_mode			= SPI_TRANSMODE_FULLDUPLEX;
	spi2_init.frame_size			= SPI_FRAMESIZE_8BIT;
	spi2_init.clock_polarity_phase	= SPI_CK_PL_HIGH_PH_2EDGE;		//SPI_CK_PL_HIGH_PH_1EDGE		SPI_CK_PL_LOW_PH_1EDGE
	spi2_init.nss					= SPI_NSS_SOFT;
	spi2_init.prescale				= SPI_PSC_2;					// Fspi1=54MHz/32=0.84375MHz
	spi2_init.endian				= SPI_ENDIAN_MSB;
	spi_init(SPI2, &spi2_init);
	
	/* enable SPI2 */
	spi_enable(SPI2);
	/* enable SPI2 DMA channel */
	spi_dma_enable(SPI2,SPI_DMA_TRANSMIT);
//	spi_dma_enable(SPI2,SPI_DMA_RECEIVE);
}

五、SPI写函数

  • 使用的芯片不一样或者实现的方法不一样,意味着编写底层代码时也会略有区别。在这一章节中,就是SPI写这个函数的区别。
  • 如我文章标题所示,我会列出软件SPI、硬件SPI、硬件SPI+DMA三种方式的底层写函数。

1. 软件SPI写函数

  • 软件SPI,说白了就是让CPU去操作IO来模拟SPI波形。
  • 我们写一个字节,SCK时钟线翻转8次。数据从高位开始发送,那么就是与上0x80,数据线根据与上0x80后的结果判断当前位是高还是低输出高低电平。这么一帧的操作,我们认为是写一个字节的操作。
void SPI_WriteByte(uint8_t ndata)
{
	uint8_t i;
	for(i=0;i<8;i++)
	{
		gpio_bit_write(SPI2_PORT, SPI2_SCK_PIN, RESET);
		if(ndata&0x80)
			gpio_bit_write(SPI2_PORT, SPI2_MOSI_PIN, SET);
		else
			gpio_bit_write(SPI2_PORT, SPI2_MOSI_PIN, RESET);
		gpio_bit_write(SPI2_PORT, SPI2_SCK_PIN, SET);
		ndata <<= 1;
	}
}

2.硬件SPI写函数

  • 硬件SPI,则是让CPU去控制单片机里能输出SPI波形的硬件模块。
  • 还记得上面我们初始化配置的SPI2吗,在那里我们已经设置好了SPI2的工作模式,这里我们只需发送读写命令即可。
  • 为了保证严格遵循通信的时序,读/写前需查看相关缓存寄存器是否置位。
  • 为防止实际情况下意外干扰导致MCU卡死或跑飞,除了必要的看门狗外,任何非必要的while循环里都得有超时跳出机制。
  • 这里我虽然进行了读操作,但并没有去处理读的内容,若想有此功能,将读到的值作为函数的返回值即可。这里这么写还有别的用处。
void SPIx_Master_Write_Byte_Hard(uint8_t ndata,uint32_t TimeOut)
{
	uint32_t timeout_t=0;

	timeout_t = TimeOut;

	/* loop while spi tx data register in not emplty */
	while( RESET == spi_i2s_flag_get(SPI2,SPI_FLAG_TBE) )
	{
		if(TimeOut > 0) TimeOut--; 
		else 				break;			
	}
	/* send byte through the SPI2 peripheral */
	spi_i2s_data_transmit(SPI2,ndata);

	while( RESET == spi_i2s_flag_get(SPI2,SPI_FLAG_RBNE))
	{
		if(timeout_t > 0) timeout_t--; 
		else 					break;		
	}		
	spi_i2s_data_receive(SPI2);
}

3.硬件SPI+DMA写函数

  • 硬件SPI+DMA,道理与单纯操作硬件SPI一样,不过是变成了DMA去操作硬件SPI模块,而CPU去控制DMA罢了。
  • 根据上面我们对DMA1_CH1的配置,当我们把该通道使能就意味着一次数据传输的开始。
void SPIx_Transmit_DMA(uint32_t spi_periph,uint16_t* ndata,uint16_t Size)
{
	if(SPI2 == spi_periph)
	{
		/* TX */
		dma_channel_disable(DMA1,DMA_CH1);	
		
		dma_memory_address_config(DMA1,DMA_CH1,(uint32_t)ndata);
		dma_transfer_number_config(DMA1,DMA_CH1,Size);

		dma_channel_enable(DMA1,DMA_CH1);		
	}
}

六、主函数部分

1. 显示部分

  • 这里我们再封装一层,用于切软件SPI和硬件SPI底层函数。
void lcd_wr_byte(uint8_t ndata)   
{		
	gpio_bit_write(TFT_PORT,TFT_RS_PIN,SET);	
	gpio_bit_reset(SPI2_PORT,SPI2_CS_PIN);	//CS=0
#if SPI2_SOFT
	SPIx_Master_Write_Byte(ndata,0xFFFFFFFFU);
#else
	SPIx_Master_Write_Byte_Hard(ndata,0xFFFFFFFFU);
#endif
	gpio_bit_set(SPI2_PORT,SPI2_CS_PIN);	//CS=1
}
  • 这里是TFT-LCD刷彩屏部分,大概理解即可。
void LCD_Fillx(uint16_t sx,uint16_t sy,uint16_t ex,uint16_t ey,uint16_t* color)
{          
	uint16_t i;
	uint8_t MSB_8=0,LSB_8=0;
	
	if(sx>ex)
	{
		i	= sx;
		sx = ex;
		ex = i;
	}
	if(sy > ey)
	{
		i	= sy;
		sy = ey;
		ey = i;			
	}

	uint32_t total = (ex - sx + 1)*(ey - sy + 1);
	uint32_t j = 0;		
	LCD_SetCursor(sx,sy,ex,ey);
	lcd_wr_gram();

	for(j = 0;j < total;j++)
	{									   
		lcd_wr_byte((*color)>>8);
		lcd_wr_byte((*color)&0x00FF);
	}
} 

void LCD_Fill_DMA(uint16_t sx,uint16_t sy,uint16_t ex,uint16_t ey,uint16_t* color)
{          
	uint16_t i;
	
	if(sx>ex)
	{
		i	= sx;
		sx = ex;
		ex = i;
	}
	if(sy > ey)
	{
		i	= sy;
		sy = ey;
		ey = i;			
	}

	uint32_t total = (ex - sx + 1)*(ey - sy + 1)*2;
	
	LCD_SetCursor(sx,sy,ex,ey);
	lcd_wr_gram();
	gpio_bit_reset(SPI2_PORT,SPI2_CS_PIN);	//CS=0
	gpio_bit_write(TFT_PORT,TFT_RS_PIN,SET);	
	SPIx_Transmit_DMA(SPI2,color,total);		//(uint16_t*)0xF800		color
}  

2. 任务函数

void TASK_TFT_REFRESH(void)
{
	static uint32_t refresh_count=0;
	static uint16_t color_temp=0;
	
	if(refresh_count%3==0)
		color_temp = 0xF800;
	else if(refresh_count%3==1)
		color_temp = 0x001F;
	else if(refresh_count%3==2)
		color_temp = 0xFFE0;		
#if (SPI2_SOFT==0) && (SPI2_DMA==1)
	LCD_Fill_DMA(0,0,319,239,&color_temp);
#else
	LCD_Fillx(0,0,319,239,&color_temp);
#endif
	refresh_count++;
}

3. 主函数

  • 主函数程序逻辑如下:
  • TMT是个时间片框架,源码见GITEE,这里我们是让TASK_TFT_REFRESH();这个函数每1.5秒执行一次。主函数理解到此处即可。
int main(void)
{	
	SystemTick_Init();
	
	SystemClock_Reconfig();
	
	GPIO_Init();
	Timer1_Init();
	DMA_Init();
	USARTx_Init();
#if !SPI2_SOFT
	SPIx_Init();
#endif	
	NVIC_Init();
	TMT_Init();
	LCD_Init();
	TMT.Create(TASK_TFT_REFRESH,1500);
	delay_ms(2000);
	while(1)
	{
		TMT.Run();
	}
}

七、结果演示

1. 实际效果

  • 最终效果如下,软硬件SPI的区别也就在刷屏速度上,就不单独再加视频了。

2. 驱动波形

  • 调试过程中,其实这里才是重点,判断我们代码是否配置成功,逻辑是否有误,均需要查看相应IO的输入/输出波形才能明白。

- 软件SPI波形
请添加图片描述

  • 上图中,黄色的波形是SPI的时钟线(SCK),蓝色是SPI的数据输出线(MOSI)。
  • 我们可以清楚的看到SPI波形是以一个字节(8位)为一帧,时钟线翻转8次,发送的数据为0x55(0b01010101),速率在2.3MHz左右。

- 硬件SPI波形

请添加图片描述

  • 上图,在硬件SPI模拟下,我们可以清楚的看到SPI波形还是以一个字节(8位)为一帧,时钟线翻转8次,发送的数据依然为0x55(0b01010101),速率在3.5MHz左右。不过硬件的IO响应比CPU软件模拟快,通信时SCK和MOSI的翻转几乎是同时的。
    请添加图片描述

  • 这个时候我们可以调整硬件SPI的配置,将spi2_init.prescale = SPI_PSC_2; 由于我的单片机是工作在108M的频率下,该配置下SPI2模块频率为27MHz。

  • 上图为示波器抓到的波形,可以看到频率为25.64MHz,与配置的差不多。然而时钟线(SCK)的波形已经发生了很明显的畸变。个人认为是由PCB的走线长短示波器抓取的参考地以及芯片本身该模块内部电路导致的,不过仍能刷屏成功,我也就不在此纠结了。
    请添加图片描述

  • 多字节的SPI发送时钟线波形如上:

- 硬件SPI+DMA波形
请添加图片描述

  • 上图是使用硬件SPI+DMA的时钟线波形图。
  • 我们发现这种情况下的波形是连续的,从某种程度上说,我们的代码配置并没有问题。但是,该连续波形无法驱动我的彩屏,后经反复验证发现,无论是否使用DMA,只要输出的SPI波形为连续的,都无法与TFT彩屏通信上。
  • 还记得上面我们怎么写纯硬件SPI的写字节函数吗?
void SPIx_Master_Write_Byte_Hard(uint8_t ndata,uint32_t TimeOut)
{
	uint32_t timeout_t=0;

	timeout_t = TimeOut;

	/* loop while spi tx data register in not emplty */
	while( RESET == spi_i2s_flag_get(SPI2,SPI_FLAG_TBE) )
	{
		if(TimeOut > 0) TimeOut--; 
		else 				break;			
	}
	/* send byte through the SPI2 peripheral */
	spi_i2s_data_transmit(SPI2,ndata);

	while( RESET == spi_i2s_flag_get(SPI2,SPI_FLAG_RBNE))
	{
		if(timeout_t > 0) timeout_t--; 
		else 					break;		
	}		
	spi_i2s_data_receive(SPI2);
}
  • 这里我们稍作修改,变成这样:
  • 在4线全双工模式下,如果我只用到了发送,不去管接收,实际上也没接收处理什么。
void SPIx_Master_Write_Byte_Hard(uint8_t ndata,uint32_t TimeOut)
{
	uint32_t timeout_t=0;

	timeout_t = TimeOut;

	/* loop while spi tx data register in not emplty */
	while( RESET == spi_i2s_flag_get(SPI2,SPI_FLAG_TBE) )
	{
		if(TimeOut > 0) TimeOut--; 
		else 				break;			
	}
	/* send byte through the SPI2 peripheral */
	spi_i2s_data_transmit(SPI2,ndata);

//	while( RESET == spi_i2s_flag_get(SPI2,SPI_FLAG_RBNE))
//	{
//		if(timeout_t > 0) timeout_t--; 
//		else 					break;		
//	}		
//	spi_i2s_data_receive(SPI2);
}
  • 这里我们发现纯硬件SPI发送多字节的情况下时钟线也变成了连续的波形。 而但凡只要硬件上输出的是连续的波形,就会造成与屏通信的不正常。

八、总结与疑问

1. 总结

  • 至此,我们最终用上述的这三种方式成功输出了SPI波形。其中两种方式能成功的让屏跑起来,从结果上来说也算是成功了。

2. 疑问

  • 网上搜集信息,有人的文章里说SPI波形连续是正常的,而且连续波形反而比不连续的更好,说明SPI模块响应速度更快。
  • 所以我想向各位问下:
    1、是否有SPI连续波形通信正常的案例,若有这种案例在哪类模块中比较常见?
    2、现有的SPI通信屏是在时钟翻转8次后稍有停顿就能成功驱屏,那么在GD32中如何在使用硬件SPI+DMA情况下插入短暂的停顿操作?
    3、是否有使用ILI9341使用连续波形SPI四线通信成功的案例,若有能否给予相关建议?
    4、对于该现象,是否是硬件兼容性的问题?是否有的SPI通信模块只支持的帧(8bit或16bit)传输而不支持这种多bit传输?
  • 若有相关建议的请在评论区留言即可,相互学习,共同成长。

!!!本文为欢喜6666在CSDN原创发布,复制或转载请注明出处:)!!!

版权声明:本文为CSDN博主「欢喜6666」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_37554315/article/details/120591975

生成海报
点赞 0

欢喜6666

我还没有学会写个人说明!

暂无评论

发表评论

相关推荐

GD32三种低功耗例程

GD32F303ZET6三种低功耗例程 睡眠模式例程:MCU的UART3接收到数据 ,进入UART3接收中断  即唤醒睡眠模式。 int main(void) { /******** 本实验测试单片机睡眠模

GD32的Keil环境搭建简述

以GD32F30x为例 从GD官网下载开发文件: GD32F30x_AddOn_V2.1.0.rar包含Keil所需的gd32相关芯片的pack: GD32F30x_Demo_Suites_V2.3.0.rar包