Funpack4-3 - 用MAX32655实现移植LVGL显示两个ADC通道数值
使用MAX32655FTHR板卡搭配彩色LCD屏幕,移植LVGL,设计一个显示两个ADC通道数值的app
标签
Funpack活动
嵌入式
ADI
小凡
更新2026-02-25
21


任务需要移植LVGL然后显示两路ADC值。单看任务不是很难,在STM32上都是基操。但是用ADI的SDK实现,如果对底层接口没有一些理解实现起来还是有点难度的。

主要就三个任务:

  • 调通SPI接口点亮屏幕
  • 移植LVGL
  • 读取ADC值

SPI显示屏接口移植

首先点亮屏幕。

序号

引脚标号

说明

1

VCC

5V/3.3V电源输入

2

GND

接地

3

CS

液晶屏片选信号,低电平使能

4

RESET

液晶屏复位信号,低电平复位

5

DC/RS

液晶屏寄存器/数据选择信号,低电平:寄存器,高电平:数据

6

SDI(MOSI)

SPI总线写数据信号

7

SCK

SPI总线时钟信号

8

LED

背光控制,高电平点亮,如无需控制则接3.3V常亮

9

SDO(MISO)

SPI总线读数据信号,如无需读取功能则可不接

(以下为触摸屏信号线接线,如无需触摸或者模块本身不带触摸功能,可不连接)

10

T_CLK

触摸SPI总线时钟信号

11

T_CS

触摸屏片选信号,低电平使能

12

T_DIN

触摸SPI总线输入

13

T_DO

触摸SPI总线输出

14

T_IRQ

触摸屏中断信号,检测到触摸时为低电平

羽毛板上是SPI1.根据SPI1的引脚定义和TFT屏幕上SPI接口连接。其中LED引脚直接接3.3V就好。片选信号和RESET信号需要额外接控制IO,可以接SDIO2和SDIO3引脚。引脚连接正常后,就开始配置软件。SDK包中有提供多款屏幕驱动的代码,可以直接调用。这款屏幕的驱动是ILI9341.

包含相应的头文件,并在头文件中定义SPI相关引脚


在头文件中增加定义

#define TFT_SPI1_PINS MXC_GPIO_PIN_20 | MXC_GPIO_PIN_21 | MXC_GPIO_PIN_22 | MXC_GPIO_PIN_23

#include "tft_ili9341.h"

直接调用初始化函数,对片选信号和RESET信号引脚初始化。
参考MAX78000 TFT_DEMO例程,须要更改目标板定义和make文件。


mxc_gpio_cfg_t tft_reset_pin = {MXC_GPIO0, MXC_GPIO_PIN_25, MXC_GPIO_FUNC_OUT, MXC_GPIO_PAD_NONE, MXC_GPIO_VSSEL_VDDIOH};
mxc_gpio_cfg_t tft_blen_pin = {MXC_GPIO0, MXC_GPIO_PIN_9, MXC_GPIO_FUNC_OUT, MXC_GPIO_PAD_NONE, MXC_GPIO_VSSEL_VDDIOH};
MXC_TFT_Init(MXC_SPI1, 0, &tft_reset_pin, &tft_blen_pin);

最终运行结果如下图:

LVGL移植

显示接口刷点函数移植

显示驱动调通后,就需要移植LVGL库。主要需要修改两个地方,实现LVGL刷点函数,实现LVGL心跳函数,可以参考其他平台LVGL移植步骤,这里不再赘述。
对于刷点函数"tft_ili9341.h"中有相应的实现,并且需要把SPI发送函数改为SPI外设发送。

static void pixel(int x, int y, int color)
{
    write_command(0x2A);
    write_data(x >> 8);
    write_data(x);
    write_command(0x2B);
    write_data(y >> 8);
    write_data(y);
    write_command(0x2C); // send pixel
    write_data(color >> 8);
    write_data(color & 0xff);
}
static void spi_transmit(void* datain, unsigned int count)
{
    mxc_spi_req_t request = {
        spi,    // spi
        ssel,       // ssIdx
        1,      // ssDeassert
        (uint8_t*) datain, // txData
        NULL,       // rxData
        count,      // txCnt
        0       // rxCnt
    };
    MXC_SPI_MasterTransaction(&request);
}


这是内部函数,可以修改下定义使其成为外部函数,然后在“lv_port_disp.c”中利用上述函数实现刷点函数。


static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
    if(disp_flush_enabled) {
        int32_t x;
        int32_t y;
        for(y = area->y1; y <= area->y2; y++) {
            for(x = area->x1; x <= area->x2; x++) {
                /Put a pixel to the display. For example:/
                /put_px(x, y, *color_p)/
                pixel(x,y,color_p->full);
                color_p++;
            }
        }
    }
    /*IMPORTANT!!!
     Inform the graphics library that you are ready with the flushing/
    lv_disp_flush_ready(disp_drv);
}

初始化函数增加ili9341初始化函数,并且调转下方向


static void disp_init(void)
{
    /You code here/
    mxc_gpio_cfg_t tft_reset_pin = {MXC_GPIO0, MXC_GPIO_PIN_25, MXC_GPIO_FUNC_OUT, MXC_GPIO_PAD_NONE, MXC_GPIO_VSSEL_VDDIOH};
    mxc_gpio_cfg_t tft_blen_pin = {MXC_GPIO0, MXC_GPIO_PIN_9, MXC_GPIO_FUNC_OUT, MXC_GPIO_PAD_NONE, MXC_GPIO_VSSEL_VDDIOH};
    MXC_TFT_Init(MXC_SPI1, 0, &tft_reset_pin, &tft_blen_pin);
    MXC_TFT_SetRotation(3);
}

还需要初始化一个定时器产生1KHz的中断产生心跳

void ContinuousTimerHandler(void)
{
    // Clear interrupt
    MXC_TMR_ClearFlags(CONT_TIMER);
    lv_tick_inc(1);
}
void ContinuousTimer(void)
{
    // Declare variables
    mxc_tmr_cfg_t tmr;
    uint32_t periodTicks = MXC_TMR_GetPeriod(CONT_TIMER, CONT_CLOCK_SOURCE, 128, CONT_FREQ);
    /*
    Steps for configuring a timer for PWM mode:
Disable the timer
Set the prescale value
    3  Configure the timer for continuous mode
Set polarity, timer parameters
Enable Timer
    */
    MXC_TMR_Shutdown(CONT_TIMER);
    tmr.pres = TMR_PRES_64;
    tmr.mode = TMR_MODE_CONTINUOUS;
    tmr.bitMode = TMR_BIT_MODE_16B;
    tmr.clock = CONT_CLOCK_SOURCE;
    tmr.cmp_cnt = periodTicks; //SystemCoreClock*(1/interval_time);
    tmr.pol = 0;
    if (MXC_TMR_Init(CONT_TIMER, &tmr, true) != E_NO_ERROR) {
        printf("Failed Continuous timer Initialization.\n");
        return;
    }
    MXC_TMR_EnableInt(CONT_TIMER);
    MXC_TMR_Start(CONT_TIMER);
    printf("Continuous timer started.\n\n");
}


LVGL其他配置步骤和其他平台基本一样,这里就不多说了,我的这块屏幕正好是320*240,所以不需要更改屏幕参数配置。
主函数实现如下,使用benchmark这个DEMO进行测试:


int main(void)
{
    /* Enable cache */
    MXC_ICC_Enable(MXC_ICC0);
    /* Set system clock to 100 MHz */
    MXC_SYS_Clock_Select(MXC_SYS_CLOCK_IPO);
    SystemCoreClockUpdate();
    MXC_NVIC_SetVector(TMR1_IRQn, ContinuousTimerHandler);
    NVIC_EnableIRQ(TMR1_IRQn);
    ContinuousTimer();
    lv_init();
    lv_port_disp_init();
    lv_demo_benchmark();
    while (1) {
        lv_timer_handler(); /* Let the GUI do its work */
        MXC_Delay(5);
    }  
}



实现效果如下图:

但是实现效果很差,基本实现不了动画效果得分如下:

虽然一帧都实现不了,但已经移植成功LVGL了。

刷点函数优化

接下来就是对刷点函数进行优化,使其显示效果更加流畅。改动方向有两点,一是使用DMA发送数据,二优化刷点程序从点更新变为块更新。
ili9341库函数以及有相应的参考程序,我们只需要稍加更改。
首先将SPI发送函数修改为DMA发送,并且发送完成后再使能LVGL刷新标志。



static void spi_transmit(void *datain, unsigned int count)
{
    unsigned int offset;
    unsigned int fifo;


    volatile uint16_t *u16ptrin = (volatile uint16_t *)datain;
    unsigned int start = 0;
    // HW requires disabling/renabling SPI block at end of each transaction (when SS is inactive).
    spi->ctrl0 &= ~(MXC_F_SPI_CTRL0_EN);
    // Setup the slave select
    MXC_SETFIELD(spi->ctrl0, MXC_F_SPI_CTRL0_SS_ACTIVE,
                 ((1 << ssel) << MXC_F_SPI_CTRL0_SS_ACTIVE_POS));
    // number of RX Char is 0xffff
    spi->ctrl1 &= ~(MXC_F_SPI_CTRL1_RX_NUM_CHAR);
    //DMA RX FIFO disabled
    spi->dma &= ~(MXC_F_SPI_DMA_RX_FIFO_EN);
    // set number of char to be transmit
    MXC_SETFIELD(spi->ctrl1, MXC_F_SPI_CTRL1_TX_NUM_CHAR, count << MXC_F_SPI_CTRL1_TX_NUM_CHAR_POS);
    // DMA TX fifo enable
    spi->dma |= MXC_F_SPI_DMA_TX_FIFO_EN;
    /* Clear TX and RX FIFO in DMA
        TX: Set this bit to clear the TX FIFO and all TX FIFO flags in the QSPIn_INT_FL register.
            Note: The TX FIFO should be disabled (QSPIn_DMA.tx_fifo_en = 0) prior to setting this field.
            Note: Setting this field to 0 has no effect.
        RX: Clear the RX FIFO and any pending RX FIFO flags in QSPIn_INTFL.
            This should be done when the RX FIFO is inactive.
    */
    spi->dma |= (MXC_F_SPI_DMA_TX_FLUSH | MXC_F_SPI_DMA_RX_FLUSH);
    // QSPIn port is enabled
    spi->ctrl0 |= (MXC_F_SPI_CTRL0_EN);
    // Clear master done flag
    spi->intfl = MXC_F_SPI_INTFL_MST_DONE;
    /* Loop until all data is transmitted */
    offset = 0;
    do {
        fifo = (count > 8) ? 8 : count;
        count -= fifo;
        while (fifo > 0) {
            /* Send data */
            spi->fifo16[0] = u16ptrin[offset];
            offset++;
            fifo--;
        }
        /*
            Master Start Data Transmission
                Set this field to 1 to start a SPI master mode transaction.
                0: No master mode transaction active.
                1: Master initiates a data transmission. Ensure that all pending transactions are
                complete before setting this field to 1.
                Note: This field is only used when the QSPIn is configured for Master Mode
                (QSPIn_CTRL0.master = 1).
        */
        if (start == 0) {
            spi->ctrl0 |= MXC_F_SPI_CTRL0_START;
            start = 1;
        }
        /* Wait for data transmitting complete and then Deasserts nSS I/O */
        // Deassert slave select at the end of the transaction
        spi->ctrl0 &= ~MXC_F_SPI_CTRL0_SS_CTRL;
    } while (count);
    while (!(spi->intfl & MXC_F_SPI_INTFL_MST_DONE)) {
    }
    lv_disp_flush_ready(&disp_drv);
    return;
}


然后修改刷点函数,更改为刷新一片区域,不再挨个刷点。


static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
    if(disp_flush_enabled) {
        /The most simple case (but also the slowest) to put all pixels to the screen one-by-one/
        int h = area->y2 - area->y1 + 1;
        int w = area->x2 - area->x1 + 1;
        int pixel = h * w;
        __disable_irq();
        window(area->x1, area->y1, w, h);
        write_command(0x2C); // send pixel
        for (int p = 0; p < pixel; p++) {
            write_data(color_p->full >> 8);
            write_data(color_p->full & 0xff);
            color_p++;
        }
        WindowMax();
        __enable_irq();
    }
    /*IMPORTANT!!!
     Inform the graphics library that you are ready with the flushing/
   // lv_disp_flush_ready(disp_drv);
}


最终实现效果:

最后得分相比没优化前有了很大的提升:

ADC 读取实现

ADC读取很简单,参考例程:
初始化然后读取。

if (MXC_ADC_Init() != E_NO_ERROR) {
        printf("Error Bad Parameter\n");
        while (1) {}
    }
adc_val = MXC_ADC_StartConversion(MXC_ADC_CH_0);
adc_val1 = MXC_ADC_StartConversion(MXC_ADC_CH_1);


这款单片机ADC的参考电压只有两路都很低,所以最后读出的电压值范围很小。

默认1.22V为参考电压。

LVGL显示ADC值

最后将ADC读取代码和LVGL代码结合下最终main函数如下:


int main(void)
{
    /* Enable cache */
    MXC_ICC_Enable(MXC_ICC0);
    /* Set system clock to 100 MHz */
    MXC_SYS_Clock_Select(MXC_SYS_CLOCK_IPO);
    SystemCoreClockUpdate();
    MXC_NVIC_SetVector(TMR1_IRQn, ContinuousTimerHandler);
    NVIC_EnableIRQ(TMR1_IRQn);
    ContinuousTimer();
     /* Initialize ADC */
    if (MXC_ADC_Init() != E_NO_ERROR) {
        printf("Error Bad Parameter\n");
        while (1) {}
    }


    lv_init();
    lv_port_disp_init();
   // lv_demo_benchmark();
    lv_obj_t * label0 = lv_label_create(lv_scr_act());
    lv_obj_align(label0, LV_ALIGN_TOP_LEFT, 70, 70);
    lv_obj_set_style_text_font(label0, &lv_font_montserrat_32, 0);
    lv_obj_t * label1 = lv_label_create(lv_scr_act());
    lv_obj_align(label1, LV_ALIGN_TOP_LEFT, 70, 120);
    lv_obj_set_style_text_font(label1, &lv_font_montserrat_32, 0);
    while (1) {
        adc_val = MXC_ADC_StartConversion(MXC_ADC_CH_0);
        adc_val1 = MXC_ADC_StartConversion(MXC_ADC_CH_1);
        lv_label_set_text_fmt(label0, "ADC0: %d mV", (adc_val * 1220) / 1024);
        lv_label_set_text_fmt(label1, "ADC1: %d mV", (adc_val1 * 1220) / 1024);
        lv_timer_handler(); /* Let the GUI do its work */
        MXC_Delay(5);
    }  
}



最终显示效果:


总结

整个项目不是很难,都是很常规的任务,主要难点在于在一个新的环境中进行开发。本次我选择在VSCODE 中进行开发,主要的卡点在于对Makefile文件以及规则的不熟悉,导致任务卡了很久,最后学习了下Makefile文件的一些基本规范还有GCC编译器的使用还有GDB调试器的使用,最后也成功完成了任务。




附件下载
TFT_Demo.zip
团队介绍
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号