干货

【GD32F350学习小记】e-Paper墨水屏驱动移植及SPI调试踩坑

分类名:拆解日期:2018-09-20作者:justd0
分享到
微博
QQ
微信
LinkedIn

本帖最后由 justd0 于 2018-9-20 15:21 编辑


报名参赛的项目内容是“随身信息牌”,顾名思义就是将想要显示信息通过GD32主控控制屏幕显示出来 ,那一块屏幕必不可少,随身意味着电源受限,那低功耗也是必然要求之一了,所以我首先想到的就是e-Paper墨水屏了,正好看网上用GD32控制墨水屏的资料很少,我就先来踩踩坑。

e-Paper

我所选用的墨水屏是

http://www.waveshare.net/

出的一款2.9英寸e-Paper屏幕,该屏幕分辨率为296*128,显示颜色只支持黑白两色,可以全局刷新或者局部刷新,通过SPI进行通讯。并且它有带驱动板的版本,上面做好了升压电路和电平转换电路,这样直接连接到单片机上就可以直接显示了呢。

带驱动板的部分参数如下:

  • 工作电压:        3.3V/5V

  • 通信接口:        3-wire SPI、4-wire SPI

  • 外形尺寸:        89.5mm × 38mm

  • 显示尺寸:        66.89mm × 29.05mm

  • 点        距:        0.138 × 0.138

  • 分  辨  率:        296 × 128

  • 显示颜色:        黑、白

  • 灰度等级:        2

  • 局部刷新: 0.3s

  • 全局刷新: 2s

  • 刷新功耗: 26.4mW(typ.)

  • 待机功耗: <0.017mW

  • 可视角度:        >170°

驱动板背后的引脚说明如下:

  • VCC   :3.3V/5V

  • GND   :GND

  • DIN     :SPI通信MOSI引脚

  • CLK    :SPI通信SCK引脚

  • CS      :SPI片选引脚(低电平有效)

  • DC      :数据/命令控制引脚(高电平表示数据,低电平表示命令)

  • RST    :外部复位引脚(低电平复位)

  • BUSY :忙状态输出引脚(高电平表示忙)

第一次开发墨水屏,微雪的虽然贵了点,但是给的资料还是很全的,文档、视频、程序、数据手册、开发资料(字模提取、图片取模软件)等等。而且最重要的是他提供了STM32的

http://www.waveshare.net/w/upload/7/78/2.9inch_e-Paper_Module_code.7z

,而且库函数写的还可以,这极大的方便了我进行测试和移植。

e-Paper驱动库移植

在用STM32F1的开发板简单测试屏幕没问题之后,便开始向GD32上进行移植(基于Colibri-F350RB开发板和库函数)从e-Paper驱动板上的引脚可以看出,只要将三线制的SPI和CS、DC、RST和BUSY四个引脚进行更替即可,在程序上,由于微雪将库封装的很好,只需要把引脚定义和SPI通讯部分进行移植配置即可。

在上文提到的HAL 库版本驱动工程中,文件架构如下:

Project:epd2in9-demo

  • epd2in9-demo

    • Drivers/STM32F1xx_HAL_Drivers

    • Application/User

    • Application/MDK-ARM

    • Drivers/CMSIS

    • BSP

    • epd2in9.c                        //墨水屏功能实现库

    • epdif.c                                //墨水屏硬件接口库

    • epdpaint.c                      //绘画工具库

    • imagedata.c                   //图像数据

    • Fonts                                       //字体库

可以看到我们移植只需要复制BSP和Fonts文件到GD32的工程中来。

硬件接口调整

首先从墨水屏的硬件接口开始更改,可以看到epdif.c函数中:

#include "epdif.h"
#include "main.h"

extern SPI_HandleTypeDef hspi1;
EPD_Pin epd_cs_pin = {
  SPI_CS_GPIO_Port,
  SPI_CS_Pin,
};
EPD_Pin epd_rst_pin = {
  RST_GPIO_Port,
  RST_Pin,
};
EPD_Pin epd_dc_pin = {
  DC_GPIO_Port,
  DC_Pin,
};
EPD_Pin epd_busy_pin = {
  BUSY_GPIO_Port,
  BUSY_Pin,
};
EPD_Pin pins[4];

上面这部分代码边定义了SPI句柄、CS、RST、DC、BUSY的引脚,索引到port和pin定义出可以发现:main.h

/* Define to prevent recursive inclusion -------------------------------------*/
#ifndef __MAIN_H
#define __MAIN_H
  /* Includes ------------------------------------------------------------------*/
/* USER CODE BEGIN Includes */

/* USER CODE END Includes */
/* Private define ------------------------------------------------------------*/
#define DC_Pin GPIO_PIN_6
#define DC_GPIO_Port GPIOA
#define BUSY_Pin GPIO_PIN_1
#define BUSY_GPIO_Port GPIOA
#define RST_Pin GPIO_PIN_0
#define RST_GPIO_Port GPIOA
#define SPI_CS_Pin GPIO_PIN_4
#define SPI_CS_GPIO_Port GPIOB

STM32通过CubeMX生成的工程代码中,对IO口的定义映射会在main.h中,GD32命名规则跟STM32类似,根据需要更改后放在epdif.h中:

#ifndef EPDIF_H
#define EPDIF_H

#include "gd32f3x0.h"               //更换为GD32的头文件

#define DC_Pin                                 GPIO_PIN_8
#define DC_GPIO_Port                 GPIOA
#define BUSY_Pin                         GPIO_PIN_1
#define BUSY_GPIO_Port                 GPIOA
#define RST_Pin                         GPIO_PIN_6
#define RST_GPIO_Port                 GPIOA
#define SPI_CS_Pin                         GPIO_PIN_4
#define SPI_CS_GPIO_Port         GPIOA

//typedef struct {
//  GPIO_TypeDef* port;
//  int pin;
//} EPD_Pin;

//将GPIO_TypeDef结构体指针改为int型
typedef struct {
  int port;
  int pin;
} EPD_Pin;

(不要定义PA2,PA3引脚作为墨水屏控制引脚了,看了DataSheet和coliri_bsp_uart.c之后你会发现它们已经作为串口引脚被定义了,我开始的时候为了方便直接定义了板子左侧PA1~PA5,出问题后才发现。。第一个坑)

并且在epdif.c中实现对IO口的初始化函数:

void EpdGpioConfig(void)
{
    //使能GPIOA时钟
        rcu_periph_clock_enable(RCU_GPIOA);
        //设置CS、RST、DC为推挽输出模式
        gpio_mode_set(SPI_CS_GPIO_Port|RST_GPIO_Port|DC_GPIO_Port, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, SPI_CS_Pin|RST_Pin|DC_Pin);
    //设置CS、RST、DC上拉输出、输出速度低速即可
        gpio_output_options_set(SPI_CS_GPIO_Port|RST_GPIO_Port|DC_GPIO_Port, GPIO_OTYPE_PP, GPIO_OSPEED_2MHZ,SPI_CS_Pin|RST_Pin|DC_Pin);
    //设置BUSY端口浮空读取模式
        gpio_mode_set(BUSY_GPIO_Port,GPIO_MODE_INPUT, GPIO_PUPD_NONE, BUSY_Pin);        
}

更改实现函数命名形式

可以看到在epdif.c定义了GPIO操作和SPI传输操作函数:

void EpdDigitalWriteCallback(int pin_num, int value) {
        if (value == HIGH) {
            HAL_GPIO_WritePin((GPIO_TypeDef*)pins[pin_num].port,pins[pin_num].pin, GPIO_PIN_SET);
    } else {
            HAL_GPIO_WritePin((GPIO_TypeDef*)pins[pin_num].port, pins[pin_num].pin, GPIO_PIN_RESET);
        }
}

int EpdDigitalReadCallback(int pin_num) {
        if (HAL_GPIO_ReadPin(pins[pin_num].port, pins[pin_num].pin) == GPIO_PIN_SET) {
                return HIGH;
        } else {
                return LOW;
          }
}

void EpdDelayMsCallback(unsigned int delaytime) {
  HAL_Delay(delaytime);
}

void EpdSpiTransferCallback(unsigned char data) {
        HAL_GPIO_WritePin((GPIO_TypeDef*)pins[CS_PIN].port, pins[CS_PIN].pin, GPIO_PIN_RESET);
        HAL_SPI_Transmit(&hspi1, &data, 1, 1000);
        HAL_GPIO_WritePin((GPIO_TypeDef*)pins[CS_PIN].port, pins[CS_PIN].pin, GPIO_PIN_SET);
}

将其中所有的GPIO操作函数、SPI传输函数和延时函数都改为GD32库函数中的函数:

void EpdDigitalWriteCallback(int pin_num, int value) {
        if (value == HIGH) {
                gpio_bit_set(pins[pin_num].port, pins[pin_num].pin);
        } else {
                gpio_bit_reset(pins[pin_num].port, pins[pin_num].pin);
        }
}

int EpdDigitalReadCallback(int pin_num) {
        if (gpio_input_bit_get(pins[pin_num].port,pins[pin_num].pin) == SET) {
                return HIGH;
        } else {
                return LOW;
        }
}

void EpdDelayMsCallback(unsigned int delaytime) {
        delay_ms(delaytime);
}

void EpdSpiTransferCallback(unsigned char data) {
        gpio_bit_reset(pins[CS_PIN].port, pins[CS_PIN].pin);
        while(spi_i2s_interrupt_flag_get(SPI0,SPI_I2S_INT_FLAG_TBE) != RESET);
        spi_i2s_data_transmit(SPI0, data);                 //GDF350 spi Transmit
        gpio_bit_set(pins[CS_PIN].port, pins[CS_PIN].pin);
}

其中延时函数delay_ms()的实现方法可以参照上一篇文章中介绍的方法进行实现。这样墨水屏的硬件接口部分就更改完毕了。epd库中对墨水屏功能的实现底层都只是依靠epdif.c中封装好的函数来实现的,这样就省去了更改的麻烦。

初始化配置SPI

然后定义SPI端口和初始化,在查看GD32F3的数据手册后可以发现其SPI0所使用的端口和STM32F103C8的端口相同,这样直接按照同样的配置来配置GD32的SPI即可:

colibri_bsp_spi.h

#ifndef _TCL_COLIBRI_SPI_H
#define _TCL_COLIBRI_SPI_H

#define SPI0_SCK_Pin                GPIO_PIN_5
#define SPI0_SCK_GPIO_Port        GPIOA
#define SPI0_MOSI_Pin                GPIO_PIN_7
#define SPI0_MOSI_GPIO_Port        GPIOA        

extern void EvbSpiConfig(void);

#endif /* _TCL_COLIBRI_SPI_H*/

colibri_bsp_spi.c

void EvbSpiConfig(void)
{
    //使能GPIOA和SPI0时钟
    rcu_periph_clock_enable(RCU_GPIOA);
        rcu_periph_clock_enable(RCU_SPI0);
        //复用IO为SPI端口
        gpio_af_set(SPI0_SCK_GPIO_Port, GPIO_AF_0, SPI0_SCK_Pin);
        gpio_af_set(SPI0_MOSI_GPIO_Port, GPIO_AF_0,SPI0_MOSI_Pin);
        //设置IO复用输出模式
    gpio_mode_set(SPI0_SCK_GPIO_Port,  GPIO_MODE_AF, GPIO_PUPD_PULLUP,SPI0_SCK_Pin);
        gpio_mode_set(SPI0_MOSI_GPIO_Port, GPIO_MODE_AF, GPIO_PUPD_PULLUP,SPI0_MOSI_Pin);
        //设置IO上拉和最大输出速度
        gpio_output_options_set(SPI0_SCK_GPIO_Port, GPIO_OTYPE_PP, GPIO_OSPEED_MAX,SPI0_SCK_Pin);
        gpio_output_options_set(SPI0_MOSI_GPIO_Port,GPIO_OTYPE_PP, GPIO_OSPEED_MAX,SPI0_MOSI_Pin);
        //设置SPI工作参数,参考STM32库的参数
        spi_parameter_struct spi_para={
        .device_mode = SPI_MASTER,                                        //主机模式
        .trans_mode = SPI_TRANSMODE_BDTRANSMIT,                //单线传输
        .frame_size = SPI_FRAMESIZE_8BIT,                        //帧大小8BIT
        .nss = SPI_NSS_SOFT,                                                //软件实现片选
        .endian = SPI_ENDIAN_MSB,                                        //高八位在先传输
        .clock_polarity_phase = SPI_CK_PL_LOW_PH_1EDGE,  //时钟极性及延时
        .prescale = SPI_PSC_16                                                //16倍分频
        };
        spi_init(SPI0,&spi_para);                   //初始化SPI参数
        spi_enable(SPI0);                           //使能SPI

}

按理说到这里,移植工作就已经完成了,在主函数里初始化各模块,并且按照STM32例程中的操作对墨水屏进行操作,就可以实现对墨水屏的控制了。

但是!没有哪条路是一帆风顺的.....不想自己写库偷懒,总是要在某个部分吧这时间还回来的...

调试ing

将所有模块初始化后,声明edp、初始化epd、清屏、刷新屏幕...一通操作猛如虎,完后就看着开发板上写在while里的运行LED一直闪烁...而墨水屏高冷的一点都不骚你。。。

然而同样的操作,在STM32中运行就完全没问题,墨水屏会根据清屏刷新的操作而闪烁,可外部引脚同样的GD32咋就不行呢....

于是我便开始检测问题,首先遇到的并马上解决的就是前文所说的,将PA2和PA3给用在了DC和RST引脚上了...在配置完串口之后这俩引脚完全不受我edp库控制好不啦。。

于是换掉引脚后,我想应该没问题了吧~呵呵。。

然而!是我想简单了,,问题依旧在,屏幕依旧冷。。。我开始怀疑因为主频为108Mhz的它和72Mhz的STM32在SPI的时钟树上有何等差异?还是因为SPI配置参数在不同时钟下会有不同的输出速度?还是因为没有外部时钟的GD32在上电后的短暂时间内,时钟不稳定,然而程序已经运行到了通过SPI初始化屏幕侧操作?经过各种排查、查阅资料,,最后都否掉了。。。

那到底是什么问题啊。。。无奈的我望着逻辑分析仪出来的波形,来回切换GD32和STM32的数据图,缩放缩放。。走神的时候发现,诶我去,这来一样的协议内容,为毛缩放后数据字节间距长度不同啊!

GD32的数据截图,通道0是CLK,通道1是MOSI,0.45s的那个尖峰部分就是对墨水屏初始化SPI的数据...(本文是问题解决后才写的,能力有限啊。。有些问题已经发生了不可逆的现象,,怎么也调不出来了,配图是之前最开始发现问题的数据,可以看到有很多奇葩的):

STM32的数据截图,通道0是CLK,通道1是MOSI,字节间存在着某个操作产生的间隙。。。

诶?GD32俩字节中间的间距去哪了?对初始化部分的数据放大后发现SPI数据字节与字节之间一点间隙也没有。。。时钟连续的上升沿、数据也一个挨着一个的在MOSI线上,而STM32的数据就那么清晰的分隔开,而且用逻辑分析仪的解码功能也可以直接解析出数据来。。。头一回用SPI,不太了解之间到底应不应该有空隙,不要吓我。。。

然后我跳转到STM32的HAL库仔细看了下SPI发送的这段函数:

void EpdSpiTransferCallback(unsigned char data) {
  HAL_GPIO_WritePin((GPIO_TypeDef*)pins[CS_PIN].port, pins[CS_PIN].pin, GPIO_PIN_RESET);
  HAL_SPI_Transmit(&hspi1, &data, 1, 1000);
  HAL_GPIO_WritePin((GPIO_TypeDef*)pins[CS_PIN].port, pins[CS_PIN].pin, GPIO_PIN_SET);
}

这其中用到了STM32SPI的HAL_SPI_Transmit(&hspi1, &data, 1, 1000);函数,而这个函数实现方法中存在第四个参数” Timeout:Timeout duration“超时持续时间,最后在static HAL_StatusTypeDef SPI_WaitOnFlagUntilTimeout(SPI_HandleTypeDef *hspi, uint32_t Flag, FlagStatus Status, uint32_t Timeout)这个函数中找到了Timeout的作用:

while(__HAL_SPI_GET_FLAG(hspi, Flag) == RESET)
    {
      if(Timeout != HAL_MAX_DELAY)
      {
        if((Timeout == 0) || ((HAL_GetTick() - tickstart ) > Timeout))
        {
          /* Disable the SPI and reset the CRC: the CRC value should be cleared
             on both master and slave sides in order to resynchronize the master
             and slave for their respective CRC calculation */

          /* Disable TXE, RXNE and ERR interrupts for the interrupt process */
          __HAL_SPI_DISABLE_IT(hspi, (SPI_IT_TXE | SPI_IT_RXNE | SPI_IT_ERR));

          /* Disable SPI peripheral */
          __HAL_SPI_DISABLE(hspi);

          /* Reset CRC Calculation */
          if(hspi->Init.CRCCalculation == SPI_CRCCALCULATION_ENABLE)
          {
            SPI_RESET_CRC(hspi);
          }

          hspi->State= HAL_SPI_STATE_READY;

          /* Process Unlocked */
          __HAL_UNLOCK(hspi);

          return HAL_TIMEOUT;
        }
      }

emmm,难道就是这玩意在传输完成之后发生了一段Timeout时间的延时。。。

而恰恰是这段延时使得两字节间产生了时间间隙,而回顾GD32的SPI发送函数void spi_i2s_data_transmit(uint32_t spi_periph, uint16_t data)却没有这个参数,而且在SPI的函数库里也没找到一个发送完成后延时的方法,索性我自己在Epd发送函数中加上一个小延时:

void EpdSpiTransferCallback(unsigned char data) {
        gpio_bit_reset(pins[CS_PIN].port, pins[CS_PIN].pin);
        while(spi_i2s_interrupt_flag_get(SPI0,SPI_I2S_INT_FLAG_TBE) != RESET);
        spi_i2s_data_transmit(SPI0, data);                                        //GDF350 spi Transmit
        delay_us(4);                                                                                //传输字节间隙
        gpio_bit_set(pins[CS_PIN].port, pins[CS_PIN].pin);
}

得到了这段波形,感觉想差不多了~

接上屏幕后,接通电源,屏幕闪烁了,这说明它工作了!!那接下来的事情就好办些了!~到此移植工作也就完成了,接下来就可以用微雪提供的墨水屏的库来实现更多好玩的操作了。

当然你能细致的看完我写的这些过程,也是辛苦了,如果你手里正好有着这块开发板,且也想玩玩墨水屏,那可以直接下载我的工程附件,讲道理按照如下的接线顺序接好应该就可以玩耍啦~

附上 本人基于微雪e-Paper移植的GD32F350demo仓库地址:https://git.coding.net/justd0/e-Paper_GD32F350_Demo.git

如有问题可以在下面留言,或直接在Coding中提交你的更改建议。

关键字:
阅读原文 浏览量:289 收藏:0
此内容由EEWORLD论坛网友 justd0 原创,如需转载或用于商业用途需征 得作者同意并注明出处

上一篇: 东芝光继电器评测-直流特性测试及动态特性测试
下一篇: LAUNCHXL-CC1352P1开箱和EnergyTrace功能使用

评论

登录 | 注册 需要登陆才可发布评论    
评论加载中......
电子工程世界版权所有 京ICP证060456号 京ICP备10001474号 电信业务审批[2006]字第258号函 京公海网安备110108001534 Copyright ? 2005-2017 EEWORLD.com.cn, Inc. All rights reserved