前言
目前有一个项目中用到了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)
{
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_USART2);
rcu_periph_clock_enable(RCU_SPI2);
rcu_periph_clock_enable(RCU_TIMER1);
rcu_periph_clock_enable(RCU_AF);
}
二、GPIO配置
- 根据上图中手册中对SPI2引脚的描述以及实际电路原理图中的接线,相关IO配置如下:
#define SPI2_SOFT 0
#define SPI2_DMA 0
#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
#define TFT_PORT GPIOB
#define TFT_RS_PIN GPIO_PIN_7
#define TFT_RST_PIN GPIO_PIN_8
#define TFT_BG_PIN GPIO_PIN_9
void GPIO_Init(void)
{
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);
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
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);
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);
dma_channel_disable(DMA1,DMA_CH1);
}
四、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};
spi_i2s_deinit(SPI2);
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;
spi2_init.nss = SPI_NSS_SOFT;
spi2_init.prescale = SPI_PSC_2;
spi2_init.endian = SPI_ENDIAN_MSB;
spi_init(SPI2, &spi2_init);
spi_enable(SPI2);
spi_dma_enable(SPI2,SPI_DMA_TRANSMIT);
}
五、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;
while( RESET == spi_i2s_flag_get(SPI2,SPI_FLAG_TBE) )
{
if(TimeOut > 0) TimeOut--;
else break;
}
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)
{
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);
#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);
}
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);
gpio_bit_write(TFT_PORT,TFT_RS_PIN,SET);
SPIx_Transmit_DMA(SPI2,color,total);
}
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;
while( RESET == spi_i2s_flag_get(SPI2,SPI_FLAG_TBE) )
{
if(TimeOut > 0) TimeOut--;
else break;
}
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;
while( RESET == spi_i2s_flag_get(SPI2,SPI_FLAG_TBE) )
{
if(TimeOut > 0) TimeOut--;
else break;
}
spi_i2s_data_transmit(SPI2,ndata);
}
- 这里我们发现纯硬件SPI发送多字节的情况下时钟线也变成了连续的波形。 而但凡只要硬件上输出的是连续的波形,就会造成与屏通信的不正常。
八、总结与疑问
1. 总结
- 至此,我们最终用上述的这三种方式成功输出了SPI波形。其中两种方式能成功的让屏跑起来,从结果上来说也算是成功了。
2. 疑问
- 网上搜集信息,有人的文章里说SPI波形连续是正常的,而且连续波形反而比不连续的更好,说明SPI模块响应速度更快。
- 所以我想向各位问下:
1、是否有SPI连续波形通信正常的案例,若有这种案例在哪类模块中比较常见? 2、现有的SPI通信屏是在时钟翻转8次后稍有停顿就能成功驱屏,那么在GD32中如何在使用硬件SPI+DMA情况下插入短暂的停顿操作? 3、是否有使用ILI9341使用连续波形SPI四线通信成功的案例,若有能否给予相关建议? 4、对于该现象,是否是硬件兼容性的问题?是否有的SPI通信模块只支持的帧(8bit或16bit)传输而不支持这种多bit传输? - 若有相关建议的请在评论区留言即可,相互学习,共同成长。
!!!本文为欢喜6666在CSDN原创发布,复制或转载请注明出处:)!!!
|