织女星开发板通过SPI协议驱动ARDUINO LCD模块

Home 论坛 开发和更新问题 织女星开发板通过SPI协议驱动ARDUINO LCD模块

正在查看 5 帖子:1-5 (共 6 个帖子)
  • 作者
    帖子
  • #1831
    123123
    参与者

    前言

    难得的机会,手边刚好有VEGA的开发板和Arduino的LCD模块,尝试了一下点亮LCD屏幕,后来又接着实现了一些功能,想和大家分享一下。
    一开始只是想着通过SPI协议初始化LCD屏幕,然后发个字符串就好了,后来又尝试着显示图片,优化了一下读取速度,修修改改的也都成功了,开心!把这些过程写下来,欢迎讨论,如有不足之处也希望大家不吝指教。

    准备工作

    1. 焊接织女星开发板J1,J2,J3,J4的双排母座,以便与LCD屏通信。前段时间免费申请的织女星开发板出厂是没有焊接这些模块的,所以要自己焊一下。
    2. 参照《织女星开发板快速入门指南》把“hello world”示例在Eclipse IDE中跑一遍。拿到一个新的东西总是要先把最基本的摸索尝试一下嘛。
    3. 网上查看LCD模块的相关资料, 网址贴在这里了:http://www.waveshare.net/wiki/2.8inch_TFT_Touch_Shield .

    代码分析和修改添加

    SPI基本概念

    一般情况下支持SPI的设备都会有四根通信线,分别是SIN(数据输入),SOUT(数据输出),SCK(时 钟),CS(片选)。其中CS是控制芯片是否被选中的,也就是说只有片选信号为预先规定的使能信号时(高电位或低电位),对此芯片的操作才有效。
    通讯是通过数据交换完成的, SPI是串行通讯协议,数据是一位一位的传输的。由SCK提供时钟脉冲,SIN,SOUT则基于此脉冲完成数据传输。数据输出通过SOUT线,数据在时钟上沿或下沿时改变,在紧接着的下沿或上沿被读取,完成一位数据传输,输入也使用同样原理。这样,在至少8次时钟信号的改变(上沿和下沿为一次),就可以完成8位数据的传输。
    查看LCD模块的原理图得知CN2的3,4,5,6引脚分别对应着CS(片选信号),MOSI(主设备输出,从设备输入),MISO(主设备输入,从设备输出),CKL(时钟),如图1 所示。在本示例中,主设备是织女星开发板,从设备是LCD屏,所以MOSI对应着SOUT,MISO对应着SIN。暂时不考虑MISO的情形,因为目前要实现的功能只是用织女星开发板驱动LCD屏并打印,并不需要MISO的功能。除此之外,还有BL(backlight)和DC(command)信号线分别对应着CN2的2号和CN3的8号引脚。
    图 1 LCD模块引脚

    由物理连接可知LCD的CN2的2到6号引脚对应着织女星开发板J2的4,6,8,10,12号引脚,LCD的CN3的8号引脚对应着织女星开发板的J1的16号引脚。查看织女星开发板原理图《VEGA_Lite_SCH》文件,可知J2的4,6,8,10,12号引脚分别连接到PTB3, PTB6, PTB5, PTB7, PTB4,J1的16号引脚连接到PTB1。如图2所示。

    图 2 织女星开发板J1, J2端子

    表1总结了织女星开发板和LCD模块的部分连接关系。

    表1 PIN脚和端子对应表

    信号名称 端子号 信号值
    时钟CKL 12(J2) PTB4
    片选CS 6(J2) PTB6
    数据输出SOUT 8(J2) PTB5
    数据输入SIN 10(J2) PTB7
    背光BL 4(J2) PTB3
    数据命令DC 16(J1) PTB1

    初始化相关代码

    在BOARD_InitPins()函数中根据以上分析,初始化相应的pin脚,具体实现如下:
    void BOARD_InitPins(void) {
    /* Clock Gate Control: 0x01u */
    CLOCK_EnableClock(kCLOCK_PortB);
    /* PORTB4 is configured as LPSPI0_SCK */
    PORT_SetPinMux(PORTB, PIN4_IDX, kPORT_MuxAlt2);
    /* PORTB5 is configured as LPSPI0_SOUT */
    PORT_SetPinMux(PORTB, PIN5_IDX, kPORT_MuxAlt2);
    /* PORTB6 is configured as LPSPI0_PCS2 */
    PORT_SetPinMux(PORTB, PIN6_IDX, kPORT_MuxAlt2);
    /* PORTB7 is configured as LPSPI0_SIN */
    // PORT_SetPinMux(PORTB, PIN7_IDX, kPORT_MuxAlt2);
    /* PORTB1 is configured as GPIO */
    PORT_SetPinMux(PORTB, PIN1_IDX, kPORT_MuxAsGpio);
    /* PORTB3 is configured as GPIO */
    PORT_SetPinMux(PORTB, PIN3_IDX, kPORT_MuxAsGpio);
    }

    由于只需要输出,暂时不考虑输入的问题,所以代码中把设置输入信号的一行注释掉了。Pin1和pin3设置为GPIO,需要初始化设置一下,在main函数中的语句如下:
    gpio_pin_config_t spi_config = {
    kGPIO_DigitalOutput, 0,
    };
    GPIO_PinInit(GPIOB, 1u, &spi_config);
    GPIO_PinInit(GPIOB, 3u, &spi_config);

    配置LCD模块

    以上,关于PIN脚的配置就完成了,接下来是关于LCD和SPI 的设置。直接调用SDK中的CLOCK_SetIpSrc函数为SPI设置时钟源和获取主时钟源。
    /*Set clock source for LPSPI and get master clock source*/
    CLOCK_SetIpSrc(kCLOCK_Lpspi0, kCLOCK_IpSrcFircAsync);

    在LCD初始化函数中主要做了两件事,分别是硬件初始化和写寄存器。
    硬件初始化就是配置SPI相关参数,具体实现如下:

    static uint8_t lcd_hardware_init(void)
    {
    lpspi_master_config_t masterConfig;
    uint32_t srcClock_Hz;
    /* SPI init */
    LPSPI_MasterGetDefaultConfig(&masterConfig);

    /*********Master config*********/
    masterConfig.baudRate = 24000000;
    /* 这里LPSPI_TRANSFER_SIZE的值在宏定义中设置为1 */
    masterConfig.bitsPerFrame = 8*LPSPI_TRANSFER_SIZE;
    masterConfig.cpol = kLPSPI_ClockPolarityActiveHigh;
    masterConfig.cpha = kLPSPI_ClockPhaseFirstEdge;
    masterConfig.direction = kLPSPI_MsbFirst;

    masterConfig.pcsToSckDelayInNanoSec = 1000000000 / masterConfig.baudRate;
    masterConfig.lastSckToPcsDelayInNanoSec = 1000000000 / masterConfig.baudRate;
    masterConfig.betweenTransferDelayInNanoSec = 1000000000 / masterConfig.baudRate;

    /* 设置pcs2为片选信号 */
    masterConfig.whichPcs = kLPSPI_Pcs2;
    masterConfig.pcsActiveHighOrLow = kLPSPI_PcsActiveLow;

    masterConfig.pinCfg = kLPSPI_SINInSOUTOut;
    masterConfig.dataOutConfig = kLpspiDataOutRetained;

    srcClock_Hz = CLOCK_GetIpFreq(kCLOCK_Lpspi0);
    LPSPI_MasterInit(LCD_SPI, &masterConfig, srcClock_Hz);

    return true;
    }

    通过写寄存器来实现LCD模块中的各类电信号,代码如下:

    void lcd_write_reg(uint8_t reg, uint8_t val)
    {
    lcd_write_byte(reg, LCD_CMD);
    lcd_write_byte(val, LCD_DATA);
    }

    传入两个参数分别是lcd的目标寄存器reg和要写入的数据内容val,一次写入一个字长的数据,分别由两个字节写入函数来实现。在宏定义中将LCD_CMD定义为0,将LCD_DATA定义为1,分别代表了命令(command)和数据(data)。当传输数据的时候将DC信号置1,当确定寄存器的时候将DC信号清零。
    通过SPI协议将8个比特的数据写入LCD的代码如下:

    static void LCD_BYTE_WRITE(uint8_t data){
    lpspi_transfer_t masterXfer;
    masterXfer.txData = &data;
    masterXfer.rxData = NULL;
    masterXfer.dataSize = 1;
    masterXfer.configFlags = BOARD_LPSPI_PCS_FOR_TRANSFER | kLPSPI_MasterPcsContinuous ;
    LPSPI_MasterTransferBlocking(LCD_SPI, &masterXfer);
    while (LPSPI_GetStatusFlags(LCD_SPI) & kLPSPI_ModuleBusyFlag) {}
    }

    当发送数据的内容,大小等信息配置完成后,直接调用了SDK中的LPSPI_MasterTransferBlocking(LPSPI_Type *base, lpspi_transfer_t *transfer)函数来实现数据块的输出。这个函数大概150行的样子,就不贴在这里了,在SDK中的fsl_lpspi.c文件中可以查看。此函数使用Polling的方法传输数据,所以会一直等待所有的数据传送完毕。传入的两个参数分别为基本SPI外设地址和传输数据指针,返回值为传输状态。
    初始化完成之后就可以在LCD屏幕上显示想要的东西了。lcd_clear_screen(uint16_t color)函数将整个屏幕刷新为指定的颜色,lcd_display_string(uint16_t xpos, uint16_t ypos, const uint8_t *string, uint8_t size, uint16_t color)函数实现在屏幕上的制定位置显示字符串。其中参数color 表示颜色,xpos和ypos分别为字符长起始位置的横坐标和纵坐标,string为字符串指针,size为字体格式,在这个例子中只能选择12或者16这两种,如果把size参数设置为其他数值,将无法实现打印。
    图 3 打印出字符串

    #1832
    123123
    参与者

    添加显示图片代码

    前面提到的那个Arduino LCD模块的网页里有图片提取软件,先利用这个工具把图片转换成16位的彩色数据,生成一个大数组的C文件,把这个数组复制到 lcd_fonts文件中,再在主函数中调用就可以了。
    根据屏幕刷新的部分改写了个显示图片的代码:
    void lcd_showimage(const unsigned char *p)
    {
    uint32_t i, cnt = 0;
    uint16_t data;
    uint8_t d1,d2;
    cnt = LCD_WIDTH * LCD_HEIGHT ;
    lcd_set_cursor(0, 0);
    lcd_write_byte(0x22, LCD_CMD);
    LCD_DC_SET();
    for (i = 0; i < cnt; i ++)
    {
    d1 = *p++;
    d2 = *p++;
    data = (uint16_t)d1+((uint16_t)d2<<8);
    LCD_WORD_WRITE(data);
    }
    }

    展示一下最终的成果:

    lcd显示图片

    #1833
    123123
    参与者

    代码的补充和优化

    优化

    以单个像素点来读取数据可能会有点慢,一次读取多个像素点能大大提高读取速度,但也有一些情况就是需要一次只读取一个像素点,所以我定义了一个宏HIGH_LEVEL_OPTIMIZE,来控制选择是否一次读取多个像素点:
    #if HIGH_LEVEL_OPTIMIZE
    static uint16_t LCD_MULTI_WORD_WRITE(uint16_t* data, uint16_t len)
    {
    uint8_t *transfer = (uint8_t*)malloc(len);
    lpspi_transfer_t masterXfer;
    if(transfer){
    uint8_t exist = len % 8; //copy 8 Byte one time
    uint32_t i = 0;
    for(;i<(len - exist);){ transfer[i++] = (*(data)) & 0xff; transfer[i++] = ((*(data++)) & 0xff00) >> 8;
    transfer[i++] = (*(data)) & 0xff;
    transfer[i++] = ((*(data++)) & 0xff00) >> 8;
    transfer[i++] = (*(data)) & 0xff;
    transfer[i++] = ((*(data++)) & 0xff00) >> 8;
    transfer[i++] = (*(data)) & 0xff;
    transfer[i++] = ((*(data++)) & 0xff00) >> 8;
    }
    for(int j=0;j<exist/2;j++){ transfer[i++] = (*(data)) & 0xff; transfer[i++] = ((*(data++)) & 0xff00) >> 8;
    }
    masterXfer.txData = transfer;
    masterXfer.rxData = NULL;
    masterXfer.dataSize = len;
    masterXfer.configFlags = BOARD_LPSPI_PCS_FOR_TRANSFER | kLPSPI_MasterPcsContinuous;
    LPSPI_MasterTransferBlocking(LCD_SPI, &masterXfer);
    free(transfer);
    transfer = NULL;
    }
    else{
    free(transfer);
    transfer = NULL;
    return -1;
    }
    return *data;
    }
    #endif

    修改lcd_clear_screen(uint16_t color)函数,代码如下:
    void lcd_clear_screen(uint16_t color)
    {
    uint32_t i, cnt = 0;
    cnt = LCD_WIDTH * LCD_HEIGHT;
    lcd_set_cursor(0, 0);
    lcd_write_byte(0x22, LCD_CMD);
    LCD_DC_SET();
    #if HIGH_LEVEL_OPTIMIZE
    const uint32_t nPixelSet = 32; //send n pixels one time
    uint16_t temp[nPixelSet];
    uint16_t color2send = INVERSE_MSB(color); //MSB first, need to convert the data
    for(int i=0;i<nPixelSet;){
    temp[i++] = color2send;
    temp[i++] = color2send;
    temp[i++] = color2send;
    temp[i++] = color2send;
    }
    /*** Send 64 Bytes one time, but the lcd will accept one 16-bit as a pixel,
    so the len will be the len of the (uint16), need to sizeof(temp)/sizeof(uint16_t);***/
    for (i = 0; i < cnt; i += nPixelSet)
    {
    LCD_MULTI_WORD_WRITE(temp, sizeof(temp));
    }
    #else
    /**** send 2 bytes one time***/
    for (i = 0; i < cnt; i ++)
    {
    LCD_WORD_WRITE(color);
    }
    #endif
    }

    显示图片部分的代码也是通过类似的方法来优化。

    补充

    一楼显示的配置SPI的代码看起来有点乱,我想修改的时候已经不能编辑了,在这里重新写一遍:
    static uint8_t lcd_hardware_init(void)
    {
    lpspi_master_config_t masterConfig;
    uint32_t srcClock_Hz;
    /* SPI init */
    LPSPI_MasterGetDefaultConfig(&masterConfig);
    /*********Master config*********/
    masterConfig.baudRate = 24000000;
    /* 这里LPSPI_TRANSFER_SIZE的值在宏定义中设置为1 */
    masterConfig.bitsPerFrame = 8*LPSPI_TRANSFER_SIZE;
    masterConfig.cpol = kLPSPI_ClockPolarityActiveHigh;
    masterConfig.cpha = kLPSPI_ClockPhaseFirstEdge;
    masterConfig.direction = kLPSPI_MsbFirst;
    masterConfig.pcsToSckDelayInNanoSec = 1000000000 / masterConfig.baudRate;
    masterConfig.lastSckToPcsDelayInNanoSec = 1000000000 / masterConfig.baudRate;
    masterConfig.betweenTransferDelayInNanoSec = 1000000000 / masterConfig.baudRate;
    /* 设置pcs2为片选信号 */
    masterConfig.whichPcs = kLPSPI_Pcs2;
    masterConfig.pcsActiveHighOrLow = kLPSPI_PcsActiveLow;
    masterConfig.pinCfg = kLPSPI_SdiInSdoOut;
    masterConfig.dataOutConfig = kLpspiDataOutRetained;
    srcClock_Hz = CLOCK_GetIpFreq(kCLOCK_Lpspi0);
    LPSPI_MasterInit(LCD_SPI, &masterConfig, srcClock_Hz);
    return true;
    }

     

    #1836
    HowardHoward
    参与者

    @123 加油继续!

    #1841
    west_windwest_wind
    参与者

    学习了。

正在查看 5 帖子:1-5 (共 6 个帖子)
  • 抱歉,回复话题必需登录。