03.立创梁山派GD32移植TFT-LCD屏幕,软硬件SPI的实现

  • 此系列文章源于SYSU 2024电信学院通信工程专业工训课的训练题目,同时也是2024电子设计校内赛的题目。题目的要求描述如下:
    无线手持二维码识别器是一个通过图像识别技术,对二维码图片进行识别,并解析出二维码的内容数据,并通过无线传输给手机或电脑的一种设备,通过这个设备,可以快速对物品进行扫描并在电脑端进行归档。要求能够对二维码图片进行扫描,二维码可自行生成,源信息包含字母和数字,能够支持将扫描数据上传到电脑,并在电脑端设计上位机进行数据显示,能够支持识别特殊二维码时,进行报警鸣叫处理,同时要使用3D建模软件对识别器进行建模并制作,大小符合手持。
  • 项目使用的主控为立创梁山派GD32F470ZGT6,项目源码存放地址为https://github.com/liangbm3/GD32_QR_Decoder
  • 此系列文章更多的是对开发过程的记录和思路的呈现,文章分则可以作为教程实现单个功能,合则可以作为复现整个项目的教程。
  • 文章的撰写部分参考了立创官方文档
  • 文章未经作者许可,不得转载。


前言

根据项目需求,需要实现显示二维码识别后的结果,显示菜单等功能,因此需要移植屏幕,我选用的是1.8寸的LCD屏幕,采用SPI通信协议,共8个引脚,参考商家给出的中景园stm32例程和手册,将屏幕移植到GD32上。


一、LCD的引脚定义

LCD屏幕的外观如图
在这里插入图片描述
引脚定义如图
在这里插入图片描述

二、SPI通信协议介绍

SPI(Serial Peripheral Interface)是一种同步串行通信协议,用于在微控制器和外部设备之间进行数据传输。它由一个主设备(通常是微控制器MCU)和一个或多个从设备组成,即一主多从模式。它通常用于短距离、高速、全双工的通信,它在许多嵌入式系统和电子设备中被广泛应用,如存储器芯片、传感器、显示器驱动器、无线模块等。
在SPI协议中,主设备是通信的发起方和控制方,而从设备则是被动接收和响应主设备的命令和数据。主设备通过时钟信号来同步数据传输,同时使用多个双向数据线来实现数据的传输和接收。
SPI协议是一种全双工通信方式,意味着主设备和从设备可以同时发送和接收数据。它还使用一种选择信号(通常称为片选或使能信号),用于选择与主设备进行通信的特定从设备。

  1. 主设备通过MOSI线向从设备发送数据。在每个时钟周期中,主设备将一个位发送到MOSI线上,从设备在下一个时钟周期中读取该位。
  2. 从设备通过MISO线向主设备发送数据。在每个时钟周期中,从设备将一个位发送到MISO线上,主设备在下一个时钟周期中读取该位。
  3. 数据传输可以是全双工的,即主设备和从设备可以同时发送和接收数据。
  4. 数据传输的长度可以是可变的,通常以字节为单位。
  5. 数据传输可以是单向的,即主设备只发送数据或只接收数据。
  6. 数据传输可以是多主设备的,即多个主设备可以与多个从设备进行通信。

SPI的四种逻辑线:
MISO:Master input slave output ,也叫SDO,主机输入,从机输出(数据来自从机);
MOSI:Master output slave input ,也叫SDI,主机输出,从机输入(数据来自主机);
SCLK :Serial Clock 串行时钟信号,也叫SCK,由主机产生发送给从机;
SS:Slave Select 片选信号,也叫CS,由主机发送,以控制与哪个从机通信,通常是低电平有效信号。

SPI的接线示意图:
在这里插入图片描述

三、移植过程

1.引脚连线表

屏幕引脚开发板引脚
GNDGND
VCC3V3
SCLPB13
SDAPB15
RESPD0
DCPG6
CSPB12
BLPG7

2.软件SPI

(1)将文件添加到工程

1.按照工程模板建立工程,本次移植基于上篇文章的usart工程进行添加。

2.在商家给出的配套资料中找到stm32f103c8t6的例程,找到LCD文件夹,复制其中的文件,然后复制到工程文件夹的/Hardware/LCD/目录,如图
在这里插入图片描述
在这里插入图片描述
3.将文件添加到工程,并添加include路径,如图
在这里插入图片描述
在这里插入图片描述

(2)修改头文件、引脚宏定义

1.将#include "delay.h"删去,并把#include "sys.h"改为#include "systick.h",因为需要用到延时操作,而在systick.h中已经预先定义好了延时函数,然后我们在lcd_init.h下添加#define delay_ms delay_1ms即可使用GD32中的延时函数

2.在原来的代码中,使用了u8u16u32等数据类型,这些是Keil C51编译器定义的数据类型,在GD32中无法被识别,因此我们需要在lcd.h和lcd_init.h中添加以下宏

#ifndef u8
#define u8 uint8_t
#endif

#ifndef u16
#define u16 uint16_t
#endif

#ifndef u32
#define u32 uint32_t
#endif

3.添加GD32头文件#include "gd32f4xx.h"

4.根据引脚连线进行引脚宏定义,方便后期修改

#define RCU_LCD_SCL     RCU_GPIOB//SCK
#define PORT_LCD_SCL    GPIOB
#define GPIO_LCD_SCL    GPIO_PIN_13

#define RCU_LCD_SDA     RCU_GPIOB//MOSI
#define PORT_LCD_SDA    GPIOB
#define GPIO_LCD_SDA    GPIO_PIN_15

#define RCU_LCD_CS      RCU_GPIOB//NSS
#define PORT_LCD_CS     GPIOB
#define GPIO_LCD_CS     GPIO_PIN_12

#define RCU_LCD_DC      RCU_GPIOG //DC
#define PORT_LCD_DC     GPIOG
#define GPIO_LCD_DC     GPIO_PIN_6

#define RCU_LCD_RES     RCU_GPIOD//RES
#define PORT_LCD_RES    GPIOD
#define GPIO_LCD_RES    GPIO_PIN_0

#define RCU_LCD_BLK     RCU_GPIOG//BLK
#define PORT_LCD_BLK    GPIOG
#define GPIO_LCD_BLK    GPIO_PIN_7

(3)将stm32相关函数用GD32相关函数重写

1.重写初始化函数

void LCD_GPIO_Init(void)
{
	/* 使能时钟 */
	rcu_periph_clock_enable(RCU_LCD_SCL);
	rcu_periph_clock_enable(RCU_LCD_SDA);
	rcu_periph_clock_enable(RCU_LCD_CS);
	rcu_periph_clock_enable(RCU_LCD_DC);
	rcu_periph_clock_enable(RCU_LCD_RES);
	rcu_periph_clock_enable(RCU_LCD_BLK);

	/* 配置SCL */
	gpio_mode_set(PORT_LCD_SCL, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, GPIO_LCD_SCL);
	gpio_output_options_set(PORT_LCD_SCL, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_LCD_SCL);
	gpio_bit_write(PORT_LCD_SCL, GPIO_LCD_SCL, SET);

	/* 配置SDA */
	gpio_mode_set(PORT_LCD_SDA, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, GPIO_LCD_SDA);
	gpio_output_options_set(PORT_LCD_SDA, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_LCD_SDA);
	gpio_bit_write(PORT_LCD_SDA, GPIO_LCD_SDA, SET);

	/* 配置DC */
	gpio_mode_set(PORT_LCD_DC, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, GPIO_LCD_DC);
	gpio_output_options_set(PORT_LCD_DC, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_LCD_DC);
	gpio_bit_write(PORT_LCD_DC, GPIO_LCD_DC, SET);

	/* 配置CS */
	gpio_mode_set(PORT_LCD_CS, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, GPIO_LCD_CS);
	gpio_output_options_set(PORT_LCD_CS, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_LCD_CS);
	gpio_bit_write(PORT_LCD_CS, GPIO_LCD_CS, SET);

	/* 配置RES */
	gpio_mode_set(PORT_LCD_RES, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, GPIO_LCD_RES);
	gpio_output_options_set(PORT_LCD_RES, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_LCD_RES);
	gpio_bit_write(PORT_LCD_RES, GPIO_LCD_RES, SET);

	/* 配置BLK */
	gpio_mode_set(PORT_LCD_BLK, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, GPIO_LCD_BLK);
	gpio_output_options_set(PORT_LCD_BLK, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_LCD_BLK);
	gpio_bit_write(PORT_LCD_BLK, GPIO_LCD_BLK, SET);
}

2.将端口的操作函数进行更改,如图
在这里插入图片描述
更改如下

#define LCD_SCLK_Clr() gpio_bit_write(PORT_LCD_SCL, GPIO_LCD_SCL, RESET)//SCL=SCLK
#define LCD_SCLK_Set() gpio_bit_write(PORT_LCD_SCL, GPIO_LCD_SCL, SET)

#define LCD_MOSI_Clr() gpio_bit_write(PORT_LCD_SDA, GPIO_LCD_SDA, RESET)//SDA=MOSI
#define LCD_MOSI_Set() gpio_bit_write(PORT_LCD_SDA, GPIO_LCD_SDA, SET)

#define LCD_RES_Clr()  gpio_bit_write(PORT_LCD_RES, GPIO_LCD_RES, RESET)//RES
#define LCD_RES_Set()  gpio_bit_write(PORT_LCD_RES, GPIO_LCD_RES, SET)

#define LCD_DC_Clr()   gpio_bit_write(PORT_LCD_DC, GPIO_LCD_DC, RESET)//DC
#define LCD_DC_Set()   gpio_bit_write(PORT_LCD_DC, GPIO_LCD_DC, SET)
                       
#define LCD_CS_Clr()   gpio_bit_write(PORT_LCD_CS, GPIO_LCD_CS, RESET)//CS
#define LCD_CS_Set()   gpio_bit_write(PORT_LCD_CS, GPIO_LCD_CS, SET)

#define LCD_BLK_Clr()  gpio_bit_write(PORT_LCD_BLK, GPIO_LCD_BLK, RESET)//BLK
#define LCD_BLK_Set()  gpio_bit_write(PORT_LCD_BLK, GPIO_LCD_BLK, SET)

(4)进行测试

1.编写测试函数如下

int main()
{
	float t = 0;
	usart_gpio_config(115200);
	dma_config();
	systick_config();
	LCD_Init();
	LCD_Fill(0, 0, LCD_W, LCD_H, WHITE);
	while (1)
	{
		delay_1ms(100);
		LCD_ShowString(0, 0, "QQ", RED, WHITE, 24, 0);
		LCD_ShowString(24, 30, "LCD_W:", RED, WHITE, 16, 0);
		LCD_ShowIntNum(72, 30, LCD_W, 3, RED, WHITE, 16);
		LCD_ShowString(24, 50, "LCD_H:", RED, WHITE, 16, 0);
		LCD_ShowIntNum(72, 50, LCD_H, 3, RED, WHITE, 16);
		LCD_ShowFloatNum1(20, 80, t, 4, RED, WHITE, 16);
		t += 0.11;
		LCD_ShowPicture(65, 80, 40, 40, gImage_1);
		if (g_recv_complete_flag)
		{
			g_recv_complete_flag = 0;
			printf("字节长度:%d ", g_recv_length);
			printf("内容:%s\r\n", g_recv_buff);
			memset(g_recv_buff, 0, g_recv_length);
			g_recv_length = 0;
		}
	}
}

2.测试结果如下
在这里插入图片描述

3.硬件SPI

为了减少CPU资源的占用,充分利用梁山派开发板上的硬件资源,考虑将软件SPI改为硬件SPI。硬件SPI与软件SPI相比,硬件SPI是靠硬件上面的SPI控制器,所有的时钟边缘采样,时钟发生,还有时序控制,都是由硬件完成的。它降低了CPU的使用率,提高了运行速度。软件SPI就是用代码控制IO输出高低电平,模拟SPI的时序,这种方法通信速度较慢,且不可靠。

(1)选择SPI外设

通过查看手册,如图
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/997ad81d5bbb45699950cdfa4caca8a3.pn
在这里插入图片描述
PB13对应SPI1的SCK,复用功能为AF5;PB14对应SPI1的MISO,复用功能为AF5;PB15对应SPI1的MOSI,复用功能为AF5。由于屏幕只有数据输入,没有数据输出,使用GD32只需发送数据即可。因此选用PB13,PB15,片选由软件进行控制。

(2)进行代码添加

1.添加硬件SPI相关宏定义

#define RCU_SPI_HARDWARE  		RCU_SPI1
#define PORT_SPI 							SPI1
#define LINE_AF_SPI 					GPIO_AF_5

2.在初始化函数中添加SPI的初始化
添加开启SPI的时钟

rcu_periph_clock_enable(RCU_SPI_HARDWARE); // 开启SPI时钟

更改SCK、MOSI对应端口的配置,更改为

/*配置SPI的SCK GPIO*/
gpio_af_set(PORT_LCD_SCL, LINE_AF_SPI, GPIO_LCD_SCL);//设置端口复用
gpio_mode_set(PORT_LCD_SCL, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_LCD_SCL);
gpio_output_options_set(PORT_LCD_SCL, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_LCD_SCL);
gpio_bit_set(PORT_LCD_SCL, GPIO_LCD_SCL);

/*配置SPI的MOSI GPIO*/
gpio_af_set(PORT_LCD_SDA, LINE_AF_SPI, GPIO_LCD_SDA);//设置端口复用
gpio_mode_set(PORT_LCD_SDA, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_LCD_SDA);
gpio_output_options_set(PORT_LCD_SDA, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_LCD_SDA);
gpio_bit_set(PORT_LCD_SDA, GPIO_LCD_SDA);

配置SPI的参数

/*配置SPI参数*/
spi_parameter_struct spi_init_struct;
spi_init_struct.trans_mode           = SPI_TRANSMODE_FULLDUPLEX;  //配置为传输模式全通工
spi_init_struct.device_mode          = SPI_MASTER;   //配置为主机
spi_init_struct.frame_size           = SPI_FRAMESIZE_8BIT; // 8位数据
spi_init_struct.clock_polarity_phase = SPI_CK_PL_HIGH_PH_2EDGE;
spi_init_struct.nss                  = SPI_NSS_SOFT;  // 软件CS
spi_init_struct.prescale             = SPI_PSC_2;//2分频
spi_init_struct.endian               = SPI_ENDIAN_MSB;
spi_init(PORT_SPI, &spi_init_struct);

最后使能SPI

spi_enable(PORT_SPI);

3.更改发送函数
根据官方手册,时钟空闲位高电平,数据在时钟信号的第二个时钟上升沿进行采样和稳定。
据此和和参考stm32的函数进行配置
在这里插入图片描述

原来的发送函数是通过模拟SPI时序进行发送的,如图
在这里插入图片描述
这段代码先是拉低了CS的电平,使能从机输入,然后拉低时钟线,主机逐位去输出8位数据,最后拉高时钟线,完成数据传输,最后失能从机输入。
读懂这段代码后,我们调用GD32的库函数,用硬件SPI进行实现,更改如下

LCD_CS_Clr();
while (RESET == spi_i2s_flag_get(PORT_SPI, SPI_FLAG_TBE))
	;
spi_i2s_data_transmit(PORT_SPI, dat);
while (RESET == spi_i2s_flag_get(PORT_SPI, SPI_FLAG_RBNE))
	;
spi_i2s_data_receive(PORT_SPI);
LCD_CS_Set();

这里通过查询发送缓冲区和状态缓冲区的状态寄存器,确保其为空时,再进行数据发送

(3)进行测试

采用上述提到的测试代码,测试得到相同的结果,说明是成功的。


本移植代码在我的二维码识别项目的SPI_LCD分支下,源码发布地址:https://github.com/liangbm3/GD32_QR_Decoder/releases/tag/SPI_LCDV1.0

如果觉得文章有帮助,请点赞收藏关注

  • 28
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值