STM32学习之旅⑤ SPI控制TFT,从底层到底层的设计



目录:

一、认识其本质



(一)认识SPI

  • SPI是串行外设接口(Serial Peripheral Interface)的缩写,SPI 总线是Motorola公司推出的同步串行接口技术,(关于同步还是异步,主要看通信双方的时钟线是否连在一起)。SPI由四根线完成数据传输,分别是SCK(时钟)、MOSI(主出从入)、MISO(主入从出)和SSEL(片选)。通讯时序图如下(当然很多SPI器件的数据手册都会给出响应的时序图):
    这里写图片描述

  • SPI主要特点有:

    1. 可以同时发出和接收串行数据,是一种全双工的通信;
    2. 可以当作主机或从机工作
    3. 提供频率可编程时钟
    4. 发送结束中断标志
    5. 写冲突保护
    6. 总线竞争保护等
    7. 结构简单
    8. 传输速度快,通常可以达到几兆到几十兆每秒,常用于数据量比较大的传输。
  • 主要缺点:

    1. 需要占用较多的接口线
    2. 只能有一个主机
    3. 没有应答机制

(二)常见显示器



(三)TFT优势


  • TFT(Thin Film Transistor)是薄膜晶体管的缩写。是有源矩阵液晶的一种。是最好的LCD彩色显示器之一,TFT式显示器具有高响应度、高亮度、高对比度等优点,其显示效果接近CRT式显示器

  • TFT上的每个液晶像素点都是由集成在像素点后面的薄膜晶体管来驱动,因此反应时间较快,

  • 并且可视角度大,一般可达130度左右

  • 色彩丰富,可支持65536色


回到顶部


二、所需材料

  • TFT显示屏,看着高大上的电容屏真的买不起。。。

  • 逻辑分析仪,对没错贫穷限制人的想象,不过24M的采样率测个SPI还是勉强能够应付

  • 取模工具, 密码:tjwu

三、底层建筑


(一)模拟SPI


  • STM32内部外设自带SPI接口,为什么还要用IO口去模拟呢?原因就是移植性,用IO口模拟的程序移植性是最好的,无论是51、msp430还是stm32,或者是DSP,都可以用IO口实现,我们知道经典的51单片机是没有SPI接口的,这时候就需要IO口模拟去实现;在初期写显示驱动程序的时候,IO口模拟出错的可能性比直接用SPI接口小很多,可以大大提高我们写驱动的效率;另外,用IO口模拟SPI还可以让我们对SPI的读写时序有更深刻的认识。

  • 这是TFT手册中对引脚的描述

这里写图片描述


  • 由设备最短高电平保持时间,可以粗略计算出该设备能够兼容的SPI最高速度为1/(15*2ns)=33MHz,可见SPI是一种高速协议,然而STM32中SPI2的最高速度只支持16MHz,没有能发挥TFT的最高性能
    这里写图片描述

  • 根据以下时序图可以用软件模拟出数据传输的时序,并对TFT进行控制

这里写图片描述

/****************************************************************************
* 名    称:void  SPIv_WriteData(u8 Data)
* 功    能:STM32_模拟SPI写一个字节数据底层函数
* 入口参数:Data
* 出口参数:无
* 说    明:STM32_模拟SPI读写一个字节数据底层函数
****************************************************************************/
void  SPIv_WriteData(u8 Data)
{
    unsigned char i=0;
    for ( i = 8; i > 0; i --)
    {
        LCD_SCL_CLR;
        if ( Data & 0x80)   
            LCD_SDA_SET; //输出数据
        else 
            LCD_SDA_CLR;    
        LCD_SCL_SET;
        Data <<= 1; 
    }
}
//******************************************************************
//函数名:  LCD_WR_DATA
//功能:    向液晶屏总线写入写8位数据
//输入参数:Data:待写入的数据
//返回值:  无
//修改记录:无
//******************************************************************
void LCD_WR_DATA(u8 data)
{   
    LCD_CS_CLR;          //软件控制片选信号
    LCD_RS_SET;
    SPIv_WriteData(data);
    LCD_CS_SET;          //软件控制片选信号
}
//******************************************************************
//函数名:  LCD_WR_DATA
//功能:    向液晶屏总线写入写8位数据
//输入参数:Data:待写入的数据
//返回值:  无
//修改记录:无
//******************************************************************
void LCD_WR_DATA(u8 data)
{   
    LCD_CS_CLR;          //软件控制片选信号
    LCD_RS_SET;
    SPIv_WriteData(data);
    LCD_CS_SET;          //软件控制片选信号
}
//******************************************************************
//函数名:  LCD_WR_REG
//功能:    向液晶屏总线写入写16位指令
//输入参数:Reg:待写入的指令值
//返回值:  无
//修改记录:无
//******************************************************************
void LCD_WR_REG(u16 data)
{ 
    LCD_CS_CLR;          //软件控制片选信号
    LCD_RS_CLR;
    SPIv_WriteData(data);
    LCD_CS_SET;          //软件控制片选信号
}
  • 调用底层写命令和数据的函数来写寄存器
//******************************************************************
//函数名:  LCD_WriteReg
//功能:    写寄存器数据
//输入参数:LCD_Reg:寄存器地址
//          LCD_RegValue:要写入的数据
//返回值:  无
//修改记录:无
//******************************************************************
void LCD_WriteReg(u16 LCD_Reg, u16 LCD_RegValue)
{   
    LCD_WR_REG(LCD_Reg);  
    LCD_WR_DATA(LCD_RegValue);               
}
//******************************************************************
//函数名:  LCD_WriteRAM_Prepare
//功能:    开始写GRAM
//          在给液晶屏传送RGB数据前,应该发送写GRAM指令
//输入参数:无
//返回值:  无
//修改记录:无
//******************************************************************
void LCD_WriteRAM_Prepare(void)
{
    LCD_WR_REG(lcddev.wramcmd);
}    
  • 复位函数
//******************************************************************
//函数名:  LCD_Reset
//功能:    LCD复位函数,液晶初始化前要调用此函数
//输入参数:无
//返回值:  无
//修改记录:无
//******************************************************************
void LCD_RESET(void)
{
#ifdef LCD_RST
    LCD_RST_CLR;
#endif
    LCD_WR_REG(0x01);
    delay_ms(100);
#ifdef LCD_RST
    LCD_RST_SET;
#endif
    LCD_WR_REG(0x01);
    delay_ms(50);
}
  • 初始化GPIO
//******************************************************************
//函数名:  LCD_GPIOInit
//功能:    液晶屏IO初始化,液晶初始化前要调用此函数
//输入参数:无
//返回值:  无
//修改记录:无
//******************************************************************
void LCD_GPIOInit(void)
{
    GPIO_InitTypeDef  GPIO_InitStructure;

    RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOB ,ENABLE);

#ifdef  LCD_RST
    GPIO_InitStructure.GPIO_Pin = LCD_LED| LCD_RS| LCD_CS | LCD_SCL | LCD_SDA | LCD_RST;    
#else
    GPIO_InitStructure.GPIO_Pin = LCD_LED| LCD_RS| LCD_CS | LCD_SCL | LCD_SDA;
#endif
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;

    GPIO_Init(GPIOB, &GPIO_InitStructure);      
}
  • 初始化TFT
//******************************************************************
//函数名:  LCD_Init
//功能:    LCD初始化
//输入参数:无
//返回值:  无
//修改记录:无
//******************************************************************
void LCD_Init(void)
{  
    LCD_GPIOInit();//使用模拟SPI
    LCD_RESET(); //液晶屏复位

    //************* Start Initial Sequence **********//     
    //开始初始化液晶屏
    LCD_WR_REG(0x11);//Sleep exit 
    delay_ms (120);     
    //ST7735R Frame Rate
    LCD_WR_REG(0xB1); 
    LCD_WR_DATA(0x01); 
    LCD_WR_DATA(0x2C); 
    LCD_WR_DATA(0x2D); 

    LCD_WR_REG(0xB2); 
    LCD_WR_DATA(0x01); 
    LCD_WR_DATA(0x2C); 
    LCD_WR_DATA(0x2D); 

    LCD_WR_REG(0xB3); 
    LCD_WR_DATA(0x01); 
    LCD_WR_DATA(0x2C); 
    LCD_WR_DATA(0x2D); 
    LCD_WR_DATA(0x01); 
    LCD_WR_DATA(0x2C); 
    LCD_WR_DATA(0x2D); 

    LCD_WR_REG(0xB4); //Column inversion 
    LCD_WR_DATA(0x07); 

    //ST7735R Power Sequence
    LCD_WR_REG(0xC0); 
    LCD_WR_DATA(0xA2); 
    LCD_WR_DATA(0x02); 
    LCD_WR_DATA(0x84); 
    LCD_WR_REG(0xC1); 
    LCD_WR_DATA(0xC5); 

    LCD_WR_REG(0xC2); 
    LCD_WR_DATA(0x0A); 
    LCD_WR_DATA(0x00); 

    LCD_WR_REG(0xC3); 
    LCD_WR_DATA(0x8A); 
    LCD_WR_DATA(0x2A); 
    LCD_WR_REG(0xC4); 
    LCD_WR_DATA(0x8A); 
    LCD_WR_DATA(0xEE); 

    LCD_WR_REG(0xC5); //VCOM 
    LCD_WR_DATA(0x0E); 

    LCD_WR_REG(0x36); //MX, MY, RGB mode                 
    LCD_WR_DATA(0xC8); 

    //ST7735R Gamma Sequence
    LCD_WR_REG(0xe0); 
    LCD_WR_DATA(0x0f); 
    LCD_WR_DATA(0x1a); 
    LCD_WR_DATA(0x0f); 
    LCD_WR_DATA(0x18); 
    LCD_WR_DATA(0x2f); 
    LCD_WR_DATA(0x28); 
    LCD_WR_DATA(0x20); 
    LCD_WR_DATA(0x22); 
    LCD_WR_DATA(0x1f); 
    LCD_WR_DATA(0x1b); 
    LCD_WR_DATA(0x23); 
    LCD_WR_DATA(0x37); 
    LCD_WR_DATA(0x00);  
    LCD_WR_DATA(0x07); 
    LCD_WR_DATA(0x02); 
    LCD_WR_DATA(0x10); 

    LCD_WR_REG(0xe1); 
    LCD_WR_DATA(0x0f); 
    LCD_WR_DATA(0x1b); 
    LCD_WR_DATA(0x0f); 
    LCD_WR_DATA(0x17); 
    LCD_WR_DATA(0x33); 
    LCD_WR_DATA(0x2c); 
    LCD_WR_DATA(0x29); 
    LCD_WR_DATA(0x2e); 
    LCD_WR_DATA(0x30); 
    LCD_WR_DATA(0x30); 
    LCD_WR_DATA(0x39); 
    LCD_WR_DATA(0x3f); 
    LCD_WR_DATA(0x00); 
    LCD_WR_DATA(0x07); 
    LCD_WR_DATA(0x03); 
    LCD_WR_DATA(0x10);  

    LCD_WR_REG(0x2a);
    LCD_WR_DATA(0x00);
    LCD_WR_DATA(0x00);
    LCD_WR_DATA(0x00);
    LCD_WR_DATA(0x7f);

    LCD_WR_REG(0x2b);
    LCD_WR_DATA(0x00);
    LCD_WR_DATA(0x00);
    LCD_WR_DATA(0x00);
    LCD_WR_DATA(0x9f);

    LCD_WR_REG(0xF0); //Enable test command  
    LCD_WR_DATA(0x01); 
    LCD_WR_REG(0xF6); //Disable ram power save mode 
    LCD_WR_DATA(0x00); 

    LCD_WR_REG(0x3A); //65k mode 
    LCD_WR_DATA(0x05);  
    LCD_WR_REG(0x29);//Display on   

    LCD_SetParam();//设置LCD参数     
    LCD_LED_SET;//点亮背光   
}
  • 接下来是关键一步,这一步成功与否决定底层建筑是否搭建成功,在指定位置写入一个像素点

//******************************************************************
//函数名:  LCD_DrawPoint
//功能:    在指定位置写入一个像素点数据
//输入参数:(x,y):光标坐标
//返回值:  无
//修改记录:无
//******************************************************************
void LCD_DrawPoint(u16 x,u16 y)
{
    LCD_SetCursor(x,y);//设置光标位置 
    LCD_WR_DATA_16Bit(BLACK);
}
  • 然后再主函数中调用这个函数
#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
#include "timer.h"
int main()
{
    SystemInit();                //初始化系统,系统时钟设定为72MHz       
    delay_init();               //配置systick,中断时间设置为72000/72000000 = 1us
    LCD_Init();                 //液晶屏初始化
    while(1)
    {
        LCD_DrawPoint(64, 64);
        delay_ms(10);
    }
}
  • 这一个如果成功的话,可以屏幕的正中心看到一个小小的像素点,对没错就是一个像素点,很小很小

  • 然后这是写入的时序图,用逻辑分析仪测的,可以看到和LCD_Init()中定义的是一样的

这里写图片描述

这里写图片描述

  • 为了让实验现象更明显一些,我们来填充一整块区域
/****************************************************************************
* 名    称: LCD_Clear
* 功    能: LCD全屏填充清屏函数
* 入口参数: Color:要清屏的填充色
* 出口参数: 无
* 说    明:
****************************************************************************/
void LCD_Clear(u16 Color)
{
    u16 i,j;
    LCD_SetWindows(0, 0, lcddev.width-1, lcddev.height-1);
    for (i = 0; i < lcddev.width; i ++)
    {
        for (j = 0; j < lcddev.height; j ++)
        LCD_WR_DATA_16Bit(Color);                //写入数据
    }
}
/****************************************************************************
* 名    称: LCD_SetWindows
* 功    能: 设置lcd显示窗口,在此区域写点数据自动换行
* 入口参数: xy起点和终点
* 出口参数: 无
* 说    明:
****************************************************************************/
void LCD_SetWindows(u16 xStar, u16 yStar, u16 xEnd, u16 yEnd)
{
#if USE_HORIZONTAL == 1          //使用横屏
    LCD_WR_REG(lcddev.setxcmd);
    LCD_WR_DATA(xStar >> 8);
    LCD_WR_DATA(0x00FF & xStar + 32);
    LCD_WR_DATA(xEnd >> 8);
    LCD_WR_DATA(0x00FF & xEnd + 32);

    LCD_WR_REG(lcddev.setycmd);
    LCD_WR_DATA(yStar >> 8);
    LCD_WR_DATA(0x00FF & yStar + 0);
    LCD_WR_DATA(yEnd >> 8);
    LCD_WR_DATA(0x00FF & yEnd + 0);
#else
    LCD_WR_REG(lcddev.setxcmd);
    LCD_WR_DATA(xStar>>8);
    LCD_WR_DATA(0x00FF&xStar+0);
    LCD_WR_DATA(xEnd>>8);
    LCD_WR_DATA(0x00FF&xEnd+0);

    LCD_WR_REG(lcddev.setycmd);
    LCD_WR_DATA(yStar>>8);
    LCD_WR_DATA(0x00FF&yStar+32);
    LCD_WR_DATA(yEnd>>8);
    LCD_WR_DATA(0x00FF&yEnd+32);
#endif
    LCD_WriteRAM_Prepare(); //开始写入GRAM
}
#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
#include "timer.h"
int main()
{
    SystemInit();                //初始化系统,系统时钟设定为72MHz
    //systick_init(72);
    delay_init();               //配置systick,中断时间设置为72000/72000000 = 1us
    LCD_Init();                 //液晶屏初始化
    delay_ms(10);
    LCD_Clear(RED);
    while(1)
    {

    }
}
  • 可以看到一整块屏幕都变成红色的了

这里写图片描述
这里写图片描述

  • 写入了一堆0XF800,怪不得花儿那样红

回到顶部


(二)硬件SPI


  • 通过IO口模拟,我们成功的驱动了我们的TFT 显示器,说明我们的底层建筑没问题

  • 但是我们发现,在进行屏幕更新的时候还是会有一定的延迟,通过观察我们发现,使用IO口模拟SPI的时候,时钟速度只有1.6MHz,但是TFT的SPI是支持30MHz的,这无疑降低了TFT的性能

  • 为了提高SPI的写入速度,我们可以使用STM32自带的SPI接口,根据STM32的数据手册,STM32SPI的最大速度可达18MHz,尽管没有达到TFT的最高性能,但是对于写一些文字和图片来说,已经勉强能够胜任了

  • 下面就来配置STM32的SPI吧,先配置SPI管脚SCK、SOMI对应的IO口,将其设置为复用推挽输出,然后将RST、RS、CS管脚设置为推挽输出,STM32是自带CS硬件片选接口的,这个后面再来说。然后配置SPI的工作模式,官方手册里面是由介绍的

这里写图片描述

这里写图片描述

/****************************************************************************
* 名    称:SPI2_Init(void)
* 功    能:STM32_SPI2硬件配置初始化
* 入口参数:无
* 出口参数:无
* 说    明:STM32_SPI2硬件配置初始化
****************************************************************************/
void SPI2_Init(void)
{
    /*SPI初始化*/
    SPI_InitTypeDef  SPI_InitStructure;
    GPIO_InitTypeDef GPIO_InitStructure;

    /*配置SPI2管脚对应的GPIO口*/
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO | RCC_APB2Periph_GPIOB, ENABLE);

#if USE_HARDNSS           //使用硬件NSS
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_15 | LCD_CS;
#else
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_15;
#endif
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_Init(GPIOB, &GPIO_InitStructure);

    /*IO口初始化*/
#if USE_HARDNSS           //使用硬件NSS
#ifdef  LCD_RST
    GPIO_InitStructure.GPIO_Pin = LCD_LED | LCD_RS | LCD_RST;
#else
    GPIO_InitStructure.GPIO_Pin = LCD_LED | LCD_RS;
#endif
#else
#ifdef  LCD_RST
    GPIO_InitStructure.GPIO_Pin = LCD_LED | LCD_RS | LCD_CS | LCD_RST;
#else
    GPIO_InitStructure.GPIO_Pin = LCD_LED | LCD_RS | LCD_CS;
#endif
#endif
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_Init(GPIOB, &GPIO_InitStructure);

    /*SPI2配置选项*/
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE);

    SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;//双线双向全双工
    SPI_InitStructure.SPI_Mode = SPI_Mode_Master;      //设置为主SPI
    SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;  //SPI发送接收8位帧结构
    SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;        //时钟悬空高
    SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;       //数据捕获于第二个时钟沿

#if USE_HARDNSS
    SPI_InitStructure.SPI_NSS = SPI_NSS_Hard;          //硬件控制片选信号
#else
    SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;          //软件控制片选信号
#endif

#if SPI_HIGH_SPEED_MODE
    /*波特率预分频值为32*/
    SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2;
#else
    /*波特率预分频值为2*/
    SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_32;
#endif


    SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; //数据传输从MSB位开始
    /*SPI_CRCPolynomial定义了用于CRC值计算的多项式*/
    SPI_InitStructure.SPI_CRCPolynomial = 7;
    SPI_Init(SPI2, &SPI_InitStructure);
    //SPI2->CR2 |= (1<<2);
#if USE_HARDNSS
    SPI_SSOutputCmd(SPI2, ENABLE);
#endif
    /*使能SPI2*/
    SPI_Cmd(SPI2, ENABLE);
}
  • 编写发送函数,只要发送的时候将数据送入发送BUFF,数据就能够自行移位发送出去,但是将数据送入BUFF之前要判断发送区是否空,否则会覆盖之前的数据,导致数据丢失
/****************************************************************************
* 名    称:u8 SPI_WriteByte(SPI_TypeDef* SPIx,u8 Byte)
* 功    能:STM32_硬件SPI读写一个字节数据底层函数
* 入口参数:SPIx,Byte
* 出口参数:返回总线收到的数据
* 说    明:STM32_硬件SPI读写一个字节数据底层函数
****************************************************************************/
u8 SPI_WriteByte(SPI_TypeDef* SPIx, u8 Byte)
{
    while ((SPIx->SR & SPI_I2S_FLAG_TXE) == RESET); //等待发送区空
    SPIx->DR = Byte;                           //发送一个byte
    while ((SPIx->SR & SPI_I2S_FLAG_RXNE) == RESET);//等待接收完一个byte
    return SPIx->DR;                               //返回收到的数据
}
  • 修改写入数据的底层函数,并且兼容之前的IO口模拟,当硬件SPI不好使的时候可以使用IO口模拟,设置是否选择使用高速模式
/****************************************************************************
* 名    称:LCD_WR_REG(u16 data)
* 功    能:向液晶屏总线写入写16位指令
* 入口参数:Reg:待写入的指令值
* 出口参数:无
* 说    明:
****************************************************************************/
void LCD_WR_REG(u16 data)
{
#if USE_HARDNSS          //硬件控制片选信号

#else
    LCD_CS_CLR;          //软件控制片选信号
#endif
    LCD_RS_CLR;
#if USE_HARDWARE_SPI
    SPI_WriteByte(SPI2, data);
#else
    SPIv_WriteData(data);
#endif
#if USE_HARDNSS          //硬件控制片选信号

#else
    LCD_CS_SET;          //软件控制片选信号
#endif
}

/****************************************************************************
* 名    称:LCD_WR_DATA(u8 data)
* 功    能:向液晶屏总线写入写8位数据
* 入口参数:Data:待写入的数据
* 出口参数:无
* 说    明:
****************************************************************************/
void LCD_WR_DATA(u8 data)
{
#if USE_HARDNSS          //硬件控制片选信号

#else
    LCD_CS_CLR;          //软件控制片选信号
#endif
    LCD_RS_SET;
#if USE_HARDWARE_SPI
    SPI_WriteByte(SPI2,data);
#else
    SPIv_WriteData(data);
#endif
#if USE_HARDNSS          //硬件控制片选信号

#else
    LCD_CS_SET;          //软件控制片选信号
#endif
}

/****************************************************************************
* 名    称:LCD_DrawPoint_16Bit
* 功    能:8位总线下如何写入一个16位数据
* 入口参数:(x,y):光标坐标
* 出口参数:无
* 说    明:
****************************************************************************/
void LCD_WR_DATA_16Bit(u16 data)
{
#if USE_HARDNSS          //硬件控制片选信号

#else
    LCD_CS_CLR;          //软件控制片选信号
#endif
    LCD_RS_SET;
#if USE_HARDWARE_SPI
    SPI_WriteByte(SPI2, data >> 8);
    SPI_WriteByte(SPI2, data);
#else
    SPIv_WriteData(data >> 8);
    SPIv_WriteData(data);
#endif
#if USE_HARDNSS          //硬件控制片选信号

#else
    LCD_CS_SET;          //软件控制片选信号
#endif
}
  • 设置对应的宏
/***********************************用户配置区***************************************/
#define USE_HORIZONTAL      1   //定义是否使用横屏 0:不使用 1:使用.
#define USE_HARDWARE_SPI 1      //1:Enable Hardware SPI 0:USE Soft SPI
#define USE_HARDNSS  0          //使用硬件NSS
#define SPI_HIGH_SPEED_MODE 1   //SPI高速模式  1:高速模式 0:低速模式
  • 设置好了之后可以开始测试了,把STM32的SPI时钟调到最高,发现显示确实快了很多,延迟小了很多,然后问题就来了,想看一下这时候的时钟到底是多少,就用逻辑分析仪测了一下,瞬间绝望,这什么鬼,怎么啥都没有,难道顺序错的,可是显示都是正常的啊

这里写图片描述

  • 后来仔细想了想,信号里面的奈奎斯特采样定理给了我点启发(论学以致用的重要性),由奈奎斯特采样定理可知,将模拟信号转化为数字信号的时候,采样的频率至少要是被测信号频率的2倍才不会导致失真,但是实际使用的时候一般设置采样频率为2.56~4倍,根据STM32官方数据手册知道SPI的最高时钟可以达到18MHz,可怜的我的逻辑分析仪最高采样频率才是24MHz,又去淘宝店家查了以下这个逻辑分析仪测量带宽只有5MHz。。。好了,确实是贫穷限制了人的想象,等我有钱了一定把你家淘宝店都买下来,好吧,我有钱还在赶这玩意儿,醒醒吧

这里写图片描述

  • 将SPI时钟调到8分频,这一波总算能测了,不过看上去还是有点失真,测了一下频率8分频是4MHz,那么最高2分频对应的应该是16MHz,也就是说SPI2的最高频率是16MHz,目前还不知道怎么调到18MHz,不过16MHz也够用了。

这里写图片描述

回到顶部


1、关于NSS


  • 前面提到过这个NSS,被这个坑了一整天。。。这个其实是STM32SPI自带的片选信号,NSS可以选择软件控制模式和硬件控制模式,所谓软件控制就是另外配置一个IO口来作为片选信号,和IO口模拟类似,发送数据前先将CS拉低,再把数据送入发送BUFF,发完之后再拉高;但是我担心的是这样拉低又拉高的过程占用时间,导致效率降低,然后一直在寻找能够自动完成片选的方法,之前一直以为硬件SPI的CS 能够自动拉高和拉低,但是发现我错了。以下这幅图可以清楚的告诉你为什么

这里写图片描述
这里写图片描述
这里写图片描述

  • 配置硬件NSS模式,将其设置为输出使能,只需要将SSOE为使能就行了,配置代码如下,红色圈起来的部分,没错,注释掉的那一句就是失传多年的寄存器操作(和下面那一句是等价的),这可是我查阅了数据手册找到的寄存器表,结果你告诉我固件库是有相应的函数的。。。固件库真牛逼,看来是我老了。。。

这里写图片描述

这里写图片描述

  • 好了,然后再去测时序,发现CS在初始化完之后被拉低了,但是不再被拉高,这样也是能够完成传输的,但是这和直接将CS接地有什么区别。。。正如ST官方手册所说,硬件NSS输出模式只能用于单主机通信,但是为了数据传输避免干扰,我还是选择了软件模式,慢就慢一点吧,反正也不会慢到哪去,毕竟STM3272MHz主频摆在那里

这里写图片描述

  • 到这一步说明硬件的SPI已经配置好了,可以开始我们的上层建筑了

回到顶部


四、上层建筑


  • 底层建筑搭得好不好往往决定了上层建筑的好坏。之前已经成功将一整块屏幕填充上指定的颜色,但是这仅仅是小小的实验而已,实际使用时我们通常用TFT来显示一些文字和图片,显示文字和图片就涉及到一些算法了,这一部分的好坏往往取决于设计者的软件水平,而作为嵌入式设计这也是最核心的部分,和硬件相关的部分仅仅是为上层建筑搭建根基,但是有根基还是不能够住人的,底层设计的好坏才是决定这个设计的总体价值。而一个优秀的嵌入式工程师总能够将整个系统的差异局部化和最小化,以提高系统的移植性去适应不同的底层建筑。当然市面上越来越多的嵌入式系统都将器件的差异集中到一部分,还有各种解决方案的推出为嵌入式开发者提供了更大的便利。

回到顶部


(一)写入图片

1、原理

  • 这款TFT是128*128像素的显示器,每一个像素点都直接由内部逻辑控制,每个像素点可以显示65536种颜色。要显示图片的时候就要将要显示的图片先取模,找出该图片每一个像素点对应的色彩数据,将其存入数组

这里写图片描述

  • 打开取模工具,打开要显示的图片,设置好之后点保存,就会生成128*128*2=32768个数据的数组,这里有个小tips,修改图像数据可以使用微软自带的画图工具(当然有ps大佬要用Photoshop来弄这个我也没意见),有些教程说要转成bmp格式才能够显示,试了一下发现.jpg也行

这里写图片描述

  • 新建一个.h文件,用来存放要显示的图片,将取模得到的数据存入.h文件

这里写图片描述


2、实现代码


  • 然后再程序中将数据显示出来,设置显示窗口->将数据挨个写入TFT
/****************************************************************************
* 名    称: LCD_SetWindows
* 功    能: 设置lcd显示窗口,在此区域写点数据自动换行
* 入口参数: xy起点和终点
* 出口参数: 无
* 说    明:
****************************************************************************/
void LCD_SetWindows(u16 xStar, u16 yStar, u16 xEnd, u16 yEnd)
{
#if USE_HORIZONTAL == 1          //使用横屏
    LCD_WR_REG(lcddev.setxcmd);
    LCD_WR_DATA(xStar >> 8);
    LCD_WR_DATA(0x00FF & xStar + 32);
    LCD_WR_DATA(xEnd >> 8);
    LCD_WR_DATA(0x00FF & xEnd + 32);

    LCD_WR_REG(lcddev.setycmd);
    LCD_WR_DATA(yStar >> 8);
    LCD_WR_DATA(0x00FF & yStar + 0);
    LCD_WR_DATA(yEnd >> 8);
    LCD_WR_DATA(0x00FF & yEnd + 0);
#else
    LCD_WR_REG(lcddev.setxcmd);
    LCD_WR_DATA(xStar>>8);
    LCD_WR_DATA(0x00FF&xStar+0);
    LCD_WR_DATA(xEnd>>8);
    LCD_WR_DATA(0x00FF&xEnd+0);

    LCD_WR_REG(lcddev.setycmd);
    LCD_WR_DATA(yStar>>8);
    LCD_WR_DATA(0x00FF&yStar+32);
    LCD_WR_DATA(yEnd>>8);
    LCD_WR_DATA(0x00FF&yEnd+32);
#endif
    LCD_WriteRAM_Prepare(); //开始写入GRAM
}
/****************************************************************************
* 名    称: Gui_Drawbmp16(u16 start_x,u16 start_y,const unsigned char *p)
* 功    能: 显示一副16位BMP图像
* 入口参数: start_x,start_y :起点坐标
*            length:图像的长度(从左往右)
*            width:图像的宽度(从上往下)
*            *p :图像数组起始地址
* 出口参数: 无
* 说    明:
****************************************************************************/
void Gui_Drawbmp16(u16 start_x, u16 start_y, u16 length, u16 width, const unsigned char *p)
{
    int i;
    unsigned char picH, picL;
    LCD_SetWindows(start_x, start_y, start_x +length - 1, start_y + width - 1);//窗口设置
    for (i = 0; i < length * width; i ++)
    {
        picL = *(p + i * 2);    //数据低位在前
        picH = *(p + i * 2 + 1);
        LCD_WR_DATA_16Bit(picH << 8 | picL);
    }
    LCD_SetWindows(0, 0, lcddev.width - 1,lcddev.height - 1);//恢复显示窗口为全屏
}
  • 在主函数中调用它
#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
#include "timer.h"
#include "gui.h"
#include "picture.h"
int main()
{
    SystemInit();                //初始化系统,系统时钟设定为72MHz
    delay_init();               //配置systick,中断时间设置为72000/72000000 = 1us
    LCD_Init();                 //液晶屏初始化
    delay_ms(10);
    while(1)
    {
        Gui_Drawbmp16(0, 0, 128, 128, gImage_bear_dolls);
    }
}
  • 显示效果还是挺好的,毕竟TFT嘛,手机拍照太垃圾。。。

这里写图片描述

回到顶部


(二)写入文字


  • 显示图片相对于显示文字来说用的还是比较少的,一般是装逼用的。那么我们就来搞点实用的东西吧

1、写入英文


  • 写入一个英文字符,由之前写入一个点到写入一个英文字符
/****************************************************************************
* 名    称: LCD_ShowChar(u16 x, u16 y, u16 fc, u16 bc, u8 num, u8 size, u8 mode)
* 功    能: 显示单个英文字符
* 入口参数: (x,y):字符显示位置起始坐标
*             fc:前置画笔颜色
*             bc:背景颜色
*             num:数值(0-94)
*            size:字体大小(12/16)
*            mode:模式  0,填充模式;1,叠加模式
* 出口参数: 无
* 说    明:
****************************************************************************/
void LCD_ShowChar(u16 x, u16 y, u16 fc, u16 bc, u8 num, u8 size, u8 mode)
{
    u8 temp;
    u8 pos, t;
    u16 colortemp = POINT_COLOR;
    num = num - ' ';//得到偏移后的值
    LCD_SetWindows(x, y, x + size / 2 - 1, y + size - 1);//设置单个文字显示窗口
    if (!mode) //非叠加方式
    {
        if (size == 12)
        {
            for (pos = 0; pos < size; pos ++)
            {
                temp = asc2_1206[num][pos];//调用1206字体
                for (t = 0; t < size / 2; t ++)
                {
                    if (temp & 0x01)
                    {
                        LCD_WR_DATA_16Bit(fc);
                    }
                    else
                    {
                        LCD_WR_DATA_16Bit(bc);
                    }
                    temp >>= 1;
                }
            }
        }
        else
        {
            for (pos = 0; pos < size; pos ++)
            {
                temp = asc2_1608[num][pos];//调用1608字体
                for (t = 0; t < size / 2; t ++)
                {
                    if (temp & 0x01)
                    {
                        LCD_WR_DATA_16Bit(fc);
                    }
                    else
                    {
                        LCD_WR_DATA_16Bit(bc);
                    }
                    temp >>= 1;
                }
            }
        }
    }
    else //叠加方式
    {
        if (size == 12)
        {
            for (pos = 0; pos < size; pos ++)
            {
                temp = asc2_1206[num][pos];//调用1206字体
                for (t = 0; t < size / 2; t ++)
                {
                    POINT_COLOR = fc;
                    if (temp & 0x01)
                    {
                        LCD_DrawPoint(x + t, y + pos);//画一个点
                    }
                    temp >>= 1;
                }
            }
        }
        else
        {
            for (pos = 0; pos < size; pos ++)
            {
                temp = asc2_1608[num][pos];      //调用1608字体
                for (t = 0; t < size / 2; t ++)
                {
                    POINT_COLOR = fc;
                    if (temp & 0x01)
                    {
                        LCD_DrawPoint(x + t, y + pos);//画一个点
                    }
                    temp >>= 1;
                }
            }
        }
    }
    POINT_COLOR = colortemp;
    LCD_SetWindows(0, 0, lcddev.width - 1, lcddev.height - 1);//恢复窗口为全屏
}
  • 这个没问题再写入一个字符串
/****************************************************************************
* 名    称: LCD_ShowString(u16 x,u16 y,u8 size,u8 *p,u8 mode)
* 功    能: 显示英文字符串
* 入口参数: x,y :起点坐标
*            size:字体大小
*            *p:字符串起始地址
*            mode:模式    0,填充模式;1,叠加模式
* 出口参数: 无
* 说    明:
****************************************************************************/
void LCD_ShowString(u16 x, u16 y, u8 *p, u8 size, u8 mode)
{
    while ((*p <= '~') && (*p >= ' '))//判断是不是非法字符!
    {
        if (x > (lcddev.width - 1) || y > (lcddev.height - 1))
            return;
        LCD_ShowChar(x, y, POINT_COLOR, BACK_COLOR, *p, size, mode);
        x += size / 2;
        p ++;
    }
}
  • 在主函数中调用它
#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
#include "timer.h"
#include "gui.h"
int main()
{
    SystemInit();                //初始化系统,系统时钟设定为72MHz
    //systick_init(72);
    delay_init();               //配置systick,中断时间设置为72000/72000000 = 1us
    LCD_Init();                 //液晶屏初始化
    delay_ms(10);
    LCD_Clear(WHITE);
    LCD_ShowString(18, 40, "Hello World!", 16, 1);
    while(1)
    {
    }
}
  • 好的,有一个Hello World!诞生了,骚气的红色字体,对了还可以给背景加颜色的

这里写图片描述

这里写图片描述

  • 确实骚气十足

2、写入变量


  • 写入一个整形变量
/****************************************************************************
* 名    称: LCD_ShowNum(u16 x,u16 y,u32 num,u8 len,u8 size)
* 功    能: 显示单个数字变量值
* 入口参数: x,y :起点坐标len :指定显示数字的位数
*            size:字体大小(12,16)
*            color:颜色
*            num:数值(0~4294967295)
* 出口参数: 无
* 说    明:
****************************************************************************/
void LCD_ShowNum(u16 x, u16 y, u32 num, u8 len, u8 size)
{
    u8 t, temp;
    u8 enshow = 0;
    for (t = 0; t < len; t ++)
    {
        temp = (num / mypow(10, len - t - 1)) % 10;
        if (enshow == 0 && t < (len - 1))
        {
            if (temp == 0)
            {
                LCD_ShowChar(x + (size / 2) * t, y, POINT_COLOR, BACK_COLOR, ' ', size, 1);
                continue;
            }
            else
                enshow = 1;
        }
        LCD_ShowChar(x + (size / 2) * t, y, POINT_COLOR, BACK_COLOR, temp + '0', size, 1);
    }
}

回到顶部


3、写入中文


  • 汉字的编码方式:

    1. 每个英文字符对应一个字节,这就是ASCII码,如31-‘1’,41-‘A’,’61’-‘a’.美国人定的标准
    2. 汉字采用2字节编码(现在不完全准确),国家制定。现在的标准是GB18030,早期是GB2312-80。前者含盖后者
    3. 一个字节是8位,ASCII码最高位是’0’(所以最多128个编码)。
    4. 汉字将最高位置为’1’,与ASCII码(英文符号)区隔开。
    5. 软件当读取一个字节时,先判断最高位是否为’0’。若是,则作英文符号处理;若不是,再读取下一个字节,两个字节合一处对应一个汉字。如:D1A7-‘学’,CFB0-‘习’。
  • 使用取模软件将要写入的汉字进行取模,软件设置如下,按照如下设置方式进行设置,取模出来的数据不必进行修改就能够直接使用

这里写图片描述

这里写图片描述

  • 将取模得到的数据存入字库,先定义一个结构体,结构体的成员包括汉字对应的像素数据和汉字检索的标志,”我”其实是一个16位的数据,也就是汉字在计算机中存储的内码,它的特点是最高位为1,而英文的最高为为0,这为我们区分汉字和英文提供了极大的便利
typedef struct
{
    char Msk[32];
    unsigned char Index[2];
}typFNT_GB16;
//字体取模:宋体常规小四
const typFNT_GB16 tfont16[]=
{
//     我(0) 爱(1) 学(2) 习(3)
    0x04,0x40,0x0E,0x50,0x78,0x48,0x08,0x48,0x08,0x40,0xFF,0xFE,0x08,0x40,0x08,0x44,
    0x0A,0x44,0x0C,0x48,0x18,0x30,0x68,0x22,0x08,0x52,0x08,0x8A,0x2B,0x06,0x10,0x02,"我",/*0*/
    0x00,0x08,0x01,0xFC,0x7E,0x10,0x22,0x10,0x11,0x20,0x7F,0xFE,0x42,0x02,0x82,0x04,
    0x7F,0xF8,0x04,0x00,0x07,0xF0,0x0A,0x10,0x11,0x20,0x20,0xC0,0x43,0x30,0x1C,0x0E,"爱",/*1*/
    0x22,0x08,0x11,0x08,0x11,0x10,0x00,0x20,0x7F,0xFE,0x40,0x02,0x80,0x04,0x1F,0xE0,
    0x00,0x40,0x01,0x80,0xFF,0xFE,0x01,0x00,0x01,0x00,0x01,0x00,0x05,0x00,0x02,0x00,"学",/*2*/
    0x00,0x00,0x7F,0xF8,0x00,0x08,0x00,0x08,0x08,0x08,0x04,0x08,0x02,0x08,0x02,0x08,
    0x00,0x68,0x01,0x88,0x0E,0x08,0x70,0x08,0x20,0x08,0x00,0x08,0x00,0x50,0x00,0x20,"习",/*3*/
};
  • 然后在程序中调用这个数组进行相应的显示
/****************************************************************************
* 名    称: GUI_DrawFont16(u16 x, u16 y, u16 fc, u16 bc, u8 *s,u8 mode)
* 功    能: 显示单个16X16中文字体
* 入口参数: x,y :起点坐标
*           fc:前置画笔颜色
*           bc:背景颜色
*           s:字符串地址
*           mode:模式 0,填充模式;1,叠加模式
* 出口参数: 无
* 说    明:
****************************************************************************/
void GUI_DrawFont16(u16 x, u16 y, u16 fc, u16 bc, u8 *s,u8 mode)
{
    u8 i, j;
    u16 k;
    u16 HZnum;
    u16 x0 = x;
    HZnum = sizeof(tfont16) / sizeof(typFNT_GB16);  //自动统计汉字数目
    for (k = 0; k < HZnum; k ++)
    {
        if ((tfont16[k].Index[0] == *(s)) && (tfont16[k].Index[1] == *(s+1)))
        {
            if (!mode) //非叠加方式
            {
                LCD_SetWindows(x, y, x + 16 - 1, y + 16 - 1);
                for (i = 0; i < 16 * 2; i ++)
                {
                    for (j = 0; j < 8; j ++)
                    {
                        if (tfont16[k].Msk[i] & (0x80 >> j))
                        {
                            LCD_WR_DATA_16Bit(fc);
                        }
                        else
                        {
                            LCD_WR_DATA_16Bit(bc);
                        }
                    }
                }
            }
            else//叠加方式
            {
                POINT_COLOR = fc;
                for (i = 0; i < 16 * 2; i ++)
                {
                    for (j = 0; j < 8; j ++)
                    {
                        if (tfont16[k].Msk[i] & (0x80 >> j))
                        {
                            LCD_DrawPoint(x, y);//画一个点
                        }
                        x ++;
                    }
                    if ((x - x0) == 16)
                    {
                        x = x0;
                        y ++;
                    }
                }
            }
            break;//查找到对应点阵字库立即退出,防止多个汉字重复取模带来影响
        }
        else
        {
            continue;
        }
    }
    LCD_SetWindows(0, 0, lcddev.width - 1, lcddev.height - 1);//恢复窗口为全屏
}
  • 然后再主函数中调用它就能进行相应的显示了

  • 然后有了单个中文汉字的显示还不够,因为我们要显示汉字的时候通常不是一个一个的去显示,而是一整个字符串去显示,而且会是中英混合的显示,所以我们再来编写一个字符串的显示函数

/****************************************************************************
* 名    称: Show_Str(u16 x, u16 y, u16 fc, u16 bc, u8 *str,u8 size,u8 mode)
* 功    能: 显示一个字符串,包含中英文显示
* 入口参数: x,y :起点坐标
*           fc:前置画笔颜色
*           bc:背景颜色
*           str :字符串
*           size:字体大小
*           mode:模式 0,填充模式;1,叠加模式
* 出口参数: 无
* 说    明:
****************************************************************************/
void Show_Str(u16 x, u16 y, u16 fc, u16 bc, u8 *str, u8 size, u8 mode)
{
    u16 x0 = x;
    u8 bHz = 0;        //字符或者中文
    while (*str != 0)  //数据未结束
    {
        if (!bHz)
        {
            if (x > (lcddev.width - size / 2) || y > (lcddev.height - size))
            {
                y += size;
                x = x0;
                continue;
            }
            if (*str > 0x80)  //最高为是1,为汉字,否则为英文
                bHz = 1;      //中文
            else              //字符
            {
                if (*str == '\n')//换行符号
                {
                    y += size;
                    x = x0;
                    str ++;
                    continue;
                }
                else
                {
                    if(size == 12 || size == 16)
                    {
                        LCD_ShowChar(x, y, fc, bc, *str, size, mode);
                        x += size / 2; //字符,为全字的一半
                    }
                    else//字库中没有集成16X32的英文字体,用8X16代替
                    {
                        LCD_ShowChar(x, y, fc, bc, *str, 16, mode);
                        x += 8; //字符,为全字的一半
                    }
                }
                str ++;
            }
        }
        else//中文
        {
            if (x > (lcddev.width - size) || y > (lcddev.height - size))
            {
                y += size;
                x = x0;
                continue;
            }
            bHz = 0;//有汉字库
            if (size == 32)
                GUI_DrawFont32(x, y, fc, bc, str, mode);
            else if (size == 24)
                GUI_DrawFont24(x, y, fc, bc, str, mode);
            else
                GUI_DrawFont16(x, y, fc, bc, str, mode);
            str += 2;
            x += size;//下一个汉字偏移
        }
    }
}
#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
#include "timer.h"
#include "gui.h"

int main()
{
    SystemInit();                //初始化系统,系统时钟设定为72MHz
    delay_init();               //配置systick,中断时间设置为72000/72000000 = 1us
    LCD_Init();                 //液晶屏初始化
    delay_ms(10);
    LCD_Clear(WHITE);
    Show_Str(20, 30, BLUE, YELLOW, "Hello World!\n\n我爱学习", 16, 1);
    while(1)
    {
    }
}

这里写图片描述

回到顶部


(三)自己写printf函数


1、原理

  • 尽管我们的显示函数已经可以显示中英文加变量了,但是我还并不是很满意,因为当遇到多个要显示的变量的时候,要写的可不止是一条显示语句了,我们更希望使用像printf那样的函数,能够用一句话显示多个变量,也比较人性化,所以我们再来编写一个printf函数

  • 为了编写printf函数,我查阅了可变参数的相关知识,打开了stdio.c文件进行参考

  • 为了实现printf函数,我们需要用到一个可变参数,printf(const char* string, ...),其中的...代表的就是可变参数,回想一下函数传参的过程,当函数发生调用的时候,在函数的入口处,系统会把参数依次压入堆栈,解决问题的关键就在这,这个压入堆栈的过程是遵循一定的规则的,即参数从右往左入栈,高地址先入栈,也就是说栈底占领着最高内存地址,先入栈的参数,其地理位置最高。假设调用函数是这样printf("Hello World!", a, b);那么入栈过程就是b->a->”Hello World!”,因为函数参数进栈以及参数空间地址分配都是”实现相关”的,也就是说入栈的顺序和每个数据所占的内存字节都是对应的,这样一来,一旦我们知道某函数帧的栈上的一个固定参数的位置,我们完全有可能推导出其他变长参数的位置,所以每一个printf函数都会有一个位置已知的参数,那就是字符串,根据字符串就可以推出每个参数的地址 b.addr = a.addr + x_sizeof(a);

  • 解决问题的关键在于如何将可变参数转化为确定的参数,下图很好的说明了函数调用是的传参过程,假设调用函是之这样printf("Hello World!", a, b, c, d, e);,首先是e先入栈,存放的位置是栈底,对应的地址是高地址;然后是d入栈,其次是c……最后是”Hello World!”
    这里写图片描述

  • 我们发现在printf(const char* string, ...)中,只有*string的地址是已知的,也就是”Hello World!”对应的地址,如果我们知道其他可变参数的数据类型,我们就可以根据数据存储的地址关系(函数调用时,堆栈的地址是连续的),轻而易举的找出每个可变参数的地址,根据地址就可以取出参数,这时候我们需要借助一组函数,他们在stdarg.h中:


typedef struct
{
  char *_Ap;
} _VA_LIST;

typedef __Va_list va_list;
#define va_start(ap, A) (ap._Ap = _GLB __va_start1())
#define va_end(ap)      ((void) 0)
#define va_arg(ap, T)   _VA_ARG(ap, T)
  • 如上图所示, 使用va_start(ap, string)后,ap指针首先指向栈顶元素,也就是”Hello World!”这个字符串,然后计算出这个元素所占用的内存地址,然后ap += sizeof(string);,也就是ap指向了可变参数的第一个元素,也就是a这个元素;然后使用result = va_arg(ap, int);,首先是计算出int类型占用的内存,64位系统中是4个字节,也就是ap += 4;这时ap指针指向堆栈中的下一个元素,也就是b这个int类型的变量,并且返回a的值……,这样一来我们就能把可变的参数编程确定的参数了

2、实现代码


  • 然后就是printf的实现了
/****************************************************************************
* 名    称: my_printf(char* string, ...)
* 功    能: 格式化显示字符串
* 入口参数: *string:字符串
*            ...:可变参数
* 出口参数: count:字符的总个数(一个中文占两个字符)
* 使用范例: int result;
*            int a = 666;
*            result = my_printf("Hello World!\n%d\n%s" a, "Good Luck!");
* 说    明: 注意可变参数的输入不用加&
****************************************************************************/
int my_printf(char* string, ...)
{
    u8 str_length;
    u8 x, y;
    x = START_X;
    y = START_Y;
    u32 u32_temp;
//    float num_float;
    char char_temp = NULL;       //临时变量
    char* str_temp = NULL;
    va_list ap;         //定义char* 变量 ap
    int count = 0;
    va_start(ap, string);//为arg进行初始化
    while(*string != '\0')
    {
        if (*string < 0x80)  //最高为是0,为英文字符,否则为汉字
        {
            if (*string != '%' && *string != '\n')//为英文字母,照常输出
            {
                LCD_ShowChar(x, y, PRINTF_FC, PRINTF_BC, *string, PRINTF_SIZE, PRINTF_MODE);
                x += PRINTF_SIZE / 2;
                string ++;
                count ++;
                continue;
            }
            else if (*string == '\n')
            {
                y += PRINTF_SIZE;//换行
                x = 0;
                string++;
                count ++;
                continue;
            }
            else
            {
                switch(*++string)
                {
                    case 'd':
                    u32_temp = va_arg(ap, int);
                    str_length = LCD_ShowNum(x, y, u32_temp, PRINTF_SIZE);
                    string ++;
                    x += str_length * PRINTF_SIZE / 2;
                    count += str_length;
                    break;
                    case 'c':
                    char_temp = va_arg(ap,int);
                    LCD_ShowChar(x, y, PRINTF_FC, PRINTF_BC, (u8)char_temp, PRINTF_SIZE, PRINTF_MODE);
                    x += PRINTF_SIZE / 2;
                    string ++;
                    count ++;
                    break;
                    case 's':
                    str_temp = (char*)va_arg(ap,int);//取下一个参数的地址,因为这个是字符串
                    Show_Str(x, y, PRINTF_FC, PRINTF_BC, (u8*)str_temp, PRINTF_SIZE, PRINTF_MODE);
                    str_length = strlen(str_temp);
                    x += str_length * PRINTF_SIZE / 2;
                    if (x >= lcddev.width)
                    {
                        y += (x / lcddev.width) * PRINTF_SIZE;
                        x %= lcddev.width;
                    }
                    string ++;
                    count += str_length;
                    break;
//                    case 'f':
//                    break;
                    default:   //
                    LCD_ShowChar(x, y, PRINTF_FC, PRINTF_BC, *--string, PRINTF_SIZE, PRINTF_MODE);
                    x += PRINTF_SIZE / 2;
                    LCD_ShowChar(x, y, PRINTF_FC, PRINTF_BC, *++string, PRINTF_SIZE, PRINTF_MODE);
                    x += PRINTF_SIZE / 2;
                    string ++;
                    count ++;
                    break;
                }
            }
        }
        else//中文
        {
            GUI_DrawFont16(x, y, PRINTF_FC, PRINTF_BC, (u8*)&string, PRINTF_MODE);
            string += 2;
            x += PRINTF_SIZE;//下一个汉字偏移
            continue;
        }
    }
    va_end(ap);//将arg指向空,防止野指针
    return count;
}
  • 在主函数中调用它
#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
#include "stdarg.h"
#include "timer.h"
#include "gui.h"
int main()
{
    SystemInit();                //初始化系统,系统时钟设定为72MHz
    delay_init();               //配置systick,中断时间设置为72000/72000000 = 1us
    LCD_Init();                 //液晶屏初始化
    delay_ms(10);
    LCD_Clear(WHITE);
    int a, b, c, d, e;
    a = 1;
    b = 2;
    c = 3;
    d = 4;
    e = 5;
    my_printf("Hello World!\n%d\n%d\n%d\n%d\n%d", a, b, c, d, e);
    while(1)
    {
    }
}

这里写图片描述

  • float类型的参数正在想办法。。。

回到顶部


五、缓冲区


(一)硬件隔离


  • 之所以硬件隔离,是因为外设是慢速设备,二我们的MCU(单片机)是高速设备,在写入数据的过程中会出现等待,这就造成了堵塞,解决方案是将硬件隔离开来,方法是建立一个缓冲区,上层建筑(发送程序)每次只往缓冲区写入数据,而底层驱动每次从缓冲区读取数据,然后通过中断发送出去,通过判断缓冲区是否为空可以判断数据是否发送完成,从而可以进入低功耗模式节省功耗。之所以这样做因为我们发现利用硬件SPI的时候只有每次往发送BUFF中写入数据的时刻是需要MCU进行控制,而其他时间硬件是可以自己完成的,通过建立缓冲区可以实现高速设备(MCU)和低速设备的缓冲,同时降低减少堵塞,大大提高了程序的效率。

(二)实现程序


  • 先定义缓存buff
/**********************************缓冲区管理*********************************/
#define SEND_BUFF_SIZE 9150
#define RECEIVE_BUFF_SIZE 6000
#if USE_SEND_BUFF
typedef struct
{
    _Bool first_falg;
    u32 data_length;
    u32 p_write;
    u32 p_read;
    _Bool tpye[SEND_BUFF_SIZE];
    u8 data[SEND_BUFF_SIZE];
}send_buff_typedef;
extern send_buff_typedef send_buff;
#endif
/******************************以上是缓冲区管理*******************************/
  • 将写总线的方法改为写缓存,数据不直接发送到总线上,而是写入缓存中
/****************************************************************************
* 名    称:write_buff(_Bool type, u8 data)
* 功    能:向缓冲区写入一字节数据
* 入口参数:type:该字节数据对应的类型,1:数据,0:命令
*           data:数据
* 出口参数:0:失败,1:成功
* 说    明:type的值只能为1或0
****************************************************************************/
u8 write_buff(_Bool type, u8 data)
{
    while (send_buff.data_length >= SEND_BUFF_SIZE);
    SPI_I2S_ITConfig(SPI2, SPI_I2S_IT_TXE, DISABLE);
    /*写指针等于读指针,说明buff是空的,直接将数据发送出去*/
    if (send_buff.first_falg)
    {
        send_buff.first_falg = 0;
        LCD_CS_CLR;
        if (type)
        {
            LCD_RS_SET;
        }
        else
        {
            LCD_RS_CLR;
        }
        SPI2->DR = data;
        SPI_I2S_ITConfig(SPI2, SPI_I2S_IT_TXE, ENABLE);
        return (1);
    }
    else
    {
        send_buff.data_length ++;
        if (type)
        {
            send_buff.tpye[send_buff.p_write] = 1;
        }
        else
        {
            send_buff.tpye[send_buff.p_write] = 0;
        }
        send_buff.data[send_buff.p_write] = data;
        send_buff.p_write = ++ send_buff.p_write >= SEND_BUFF_SIZE ? 0 : send_buff.p_write;
        SPI_I2S_ITConfig(SPI2, SPI_I2S_IT_TXE, ENABLE);
        return (1);
    }
}
  • 利用中断读取缓存中的数据,由于spi相对于mcu来说是一个慢速的设备,缓存可以起到缓冲的二作用,从而解决了由于mcu等待spi造成的堵塞
/****************************************************************************
* 名    称:SPI2_IRQHandler(void)
* 功    能:SPI2中断函数
* 入口参数:无
* 出口参数:无
* 说    明:
****************************************************************************/
void SPI2_IRQHandler(void)
{
    if(SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_BSY ) == RESET)
    {
        LCD_CS_SET;//发送完毕,片选拉高
        if (send_buff.data_length > 0)//缓冲区由有待发数据
        {
            send_buff.data_length --;
            LCD_CS_CLR;     //开始发送数据,先将片选拉低

            if (send_buff.tpye[send_buff.p_read])//判断是数据还是命令
            {
                LCD_RS_SET;
            }
            else
            {
                LCD_RS_CLR;
            }
            SPI2->DR = send_buff.data[send_buff.p_read];
            send_buff.p_read = ++ send_buff.p_read >= SEND_BUFF_SIZE ? 0 : send_buff.p_read;
        }
        else
        {
            send_buff.first_falg = 1; //缓冲区空,标志置位,下一个数据可以直接发送
            SPI_I2S_ITConfig(SPI2, SPI_I2S_IT_TXE, DISABLE);//将中断关闭
        }
    }
}
  • 由于中断中需要判断总线是否忙,然后再从缓存中将数据送入发送buff,所以需要开启标志位使能SPI2->CR1 |= (1 << 6); //使能BUSY_FLAG,没找到对应的控制函数,所以直接按照参考手册上的说明直接控制寄存器

(三)数据帧和数据包


  • 我们发现改变显示状态的时候总是一个画面一个画面来改变的,而需要设备发送数据只有这一个画面是连续的,那么我们可以将这一个画面对应的数据打一个包,然后等这一个包缓存完了之后再发出去,相比于一边缓存一边发送的方法,可以大大减少了一个包发送需要的时间,从而提高了一个画面刷新的速度,减少了刷面刷新过程中卡顿的现象。

  • 缺点:刷新延迟,缓存区占用的空间更大


(四)变与不变


  • 我们发现刷新显示的时候只有与上一个画面不同的部分是需要改变的,而刷新其它相同的部分无疑是在浪费资源,所以我们可以每次刷新的时候只改变与上一个画面不同的部分,方法是在写入缓存区之前进行判断,只将需要改变的部分写入,从而可以减少数据写入的量减少刷新画面需要的时间,提高刷新率,同时也减少了缓冲区占用的内存

  • 缺点:由于判断也需要时间,使得刷新的延迟更加明显了


六、程序设计


(一)程序设计一般思路


1、自上而下


  • 站在最高层,从最高点出发依次往下设计,首先规划中断和资源->状态机建模->确定接口,用模拟实现的方法将程序模块化->最后完善事件处理程序

  • 优点:思路清晰


2、自下而上


  • 从硬件底层出发,一步步完善硬件驱动->控制寄存器来达到需要的效果->将驱动程序封装,确定接口->完善模块

  • 优点,底层设计完善,易调试


(二)最后


  • 注意函数封装、可重入性、移植性、模块之间的耦合性

  • 熟能生巧,只有多练习才能有所提高,光说不练只会原地踏步

说明:本文仅作个人技术交流


Good Luck!


回到顶部

猜你喜欢

转载自blog.csdn.net/qq_39432978/article/details/81873689
今日推荐