Funpack4-3 - 基于MAX32655实现LVGL移植和LCD屏幕显示ADC数值的设计
该项目使用了MAX32655,实现了LVGL移植和LCD屏幕显示ADC数值的设计,它的主要功能为:MAX32655检测ADC输入,并将其转化为电压值,在LCD屏幕上显示。LCD屏幕通过移植LVGL设计了一个UI界面。
标签
ADC
ADI
ST7789
MAX32655
Funpack4-3
LVGL移植
波波l
更新2026-02-03
23

一、项目介绍

基于MAX32655FTHR开发板,实现了LCD屏幕的LVGL移植(屏幕驱动IC为ST7789和屏幕显示ADC数值的设计。它的主要功能为:MAX32655检测ADC输入,并将其转化为电压值,在LCD屏幕上显示ADC数值和转化后的电压值

二、硬件介绍

image.png

MAX32655FTHR 是一款快速开发平台,可帮助工程师使用MAX32655 Arm© Cortex®-M4F和Bluetooth® 5.2低功耗(LE)快速实施超低功耗无线解决方案。该电路板还包括MAX20303 PMIC以实现电池和电源管理。0.9 x 2.6英寸小尺寸双排接头与Adafruit Feather Wing外设扩展板兼容。该电路板包括各种外设,如数字麦克风、低功耗立体声音频编解码器、128MB QSPI闪存、micro SD卡连接器、RGB指示器LED和按钮。


2.1 MAX32655FTHR板卡

  • MAX32655微控制器
    • ARM Cortex-M4F,100MHz
    • 32位RISC-V协处理器,可减轻时序关键型蓝牙处理负荷
    • 512KB闪存
    • 128KB SRAM
    • 16KB缓存
    • 蓝牙5.2 LE无线电
    • 带电量计的MAX20303可穿戴PMIC
    • 通过USB充电
    • 用于Arm Cortex-M4F的板载DAPLink调试和编程接口
    • 试验板兼容接头
    • Micro USB连接器
    • Micro SD卡连接器
  • 集成外设
    • RGB指示灯LED
    • 用户按钮
    • 低功耗立体声音频编解码器
    • 数字麦克风
    • SWD调试器
    • 虚拟UART控制台


板卡引脚图:

max32655引脚图.png


MAX32655FTHR 组件:


板卡框图:


2.2 MAX32655微控制器

MAX32655微控制器(MCU)是一款先进的片上系统(SoC),采用Arm® Cortex®-M4F CPU,可高效执行复杂的函数和算法计算,额定温度范围为-40°C至+105°C。该SoC将功率调节和管理功能与单电感多输出(SIMO)降压稳压器系统集成于一体。板载新一代蓝牙® 5.2低功耗(LE)无线电,支持远程(编码)和高吞吐量模式以及医疗身体区域网络(MBAN)。该器件提供具有512KB闪存和128KB SRAM的大型板载存储器,并在一个32KB SRAM存储区上提供可选的纠错编码(ECC)功能。该32KB存储区也可保留在BACKUP模式中。提供8KB用户OTP区域。

MAX32655采用81 CTBGA封装(8mm x 8mm,0.8mm间距),支持多种高速外设,例如I2C、50MHz SPI和UART,以及一个用于连接音频编解码器的I2S端口。八路输入、10位ADC可用于监测来自外部模拟源的模拟输入。此外,低功耗UART (LPUART)支持在超低功耗的睡眠模式下工作,有助于在不发生任何数据丢失的情况下进行唤醒。总共提供了六个具有I/O功能的定时器,包括两个低功耗定时器,即使在超低功耗的睡眠模式下也能实现脉冲计数、捕获/比较和脉宽调制(PWM)生成。


特性:

  • 超低功耗无线微控制器
    • 内置100MHz振荡器
    • 具有7.3728MHz系统时钟选项的灵活低功耗模式
    • 512KB闪存和128KB SRAM
      • 一个32KB SRAM存储区上提供可选ECC
    • 16KB指令缓存
  • 蓝牙5.2 LE无线电
    • 专用的超低功耗32位RISC-V协处理器,可减轻时序关键型蓝牙处理负荷
    • 提供完全开源的蓝牙5.2协议栈
    • 支持医疗身体区域网络(MBAN)和Mesh
    • 高吞吐量(2Mbps)模式
    • 远程(125kbps和500kbps)模式
    • Rx灵敏度:-97dBm;Tx功率:+5.5dBm
    • 单端天线连接(50Ω)
  • 提供电源管理充分延长电池寿命
    • 电源电压范围:2.0V至3.6V
    • 集成SIMO功率调节器
    • 3.0V时的有源电流为12.9μA/MHz
    • 对于32KB,3.0V时的保持电流为1.53μA
    • 低功耗模式下可选择SRAM数据保留功能+RTC
  • 多个外设,用于实施系统控制
    • 多达两个高速SPI控制器/目标
    • 多达三个I2C控制器/目标
    • 多达四个UART
    • 一个I2S控制器/目标
    • 多达8路输入、10位Σ-Δ ADC 7.8ksps
    • 多达四个微功耗比较器
    • 定时器:四个32位、两个低功耗、一个看门狗、一个低功耗看门狗
    • 1-Wire®控制器
    • 多达四个脉冲序列(PWM)引擎
    • 带唤醒定时器的RTC
    • 多达52个GPIO
  • 安全性和完整性
    • 可选安全引导
    • TRNG Seed发生器
    • AES 128/192/256硬件加速引擎


三、各功能主要代码


3.1系统框图

image.png


3.2主要代码


LCD屏幕初始化

drv_time.h:

LCD屏幕驱动IC为ST7789,这里使用软件SPI(方便移植),在头文件中定义好对应的引脚即可。由于是GPIO模拟spi,所以采取直接修改寄存器的方式控制GPIO的输出,从而提高模拟spi速率。

#ifndef __LCD_INIT_H
#define __LCD_INIT_H
/*
移植注意事项:
1.修改lcd_init.h文件下“LCD端口定义”宏定义
2.修改delay.h文件的延时函数宏定义
3.实现lcd_gpio_init.c文件软件spi引脚初始化函数 void LCD_GPIO_Init(void)
*/
#include "delay.h"
#include "mxc_device.h"
#include <stdint.h>
typedef uint32_t  u32;
typedef uint16_t u16;
typedef uint8_t  u8;

//设置屏幕分辨率和横屏或者竖屏显示 0或1为竖屏 2或3为横屏
#define USE_HORIZONTAL 2  
#if USE_HORIZONTAL==0||USE_HORIZONTAL==1
#define LCD_W 135
#define LCD_H 240
#else
#define LCD_W 240
#define LCD_H 135
#endif

#define USE_DIRECT_GPIO 1 // 1: 使用直接寄存器写入(最快);0: 使用 MXC_GPIO_OutSet/OutClr(兼容但较慢)
//-----------------LCD端口定义(原始方案,最稳定)----------------
#define SCLK_PORT       MXC_GPIO0
#define SCLK_PIN        MXC_GPIO_PIN_24

#define MOSI_PORT       MXC_GPIO0
#define MOSI_PIN        MXC_GPIO_PIN_20

#define RES_PORT        MXC_GPIO1
#define RES_PIN         MXC_GPIO_PIN_8

#define DC_PORT         MXC_GPIO1
#define DC_PIN          MXC_GPIO_PIN_9

#define CS_PORT         MXC_GPIO0
#define CS_PIN          MXC_GPIO_PIN_30

#define BLK_PORT        MXC_GPIO0
#define BLK_PIN         MXC_GPIO_PIN_31
//--------------------------------------------------------------
#if USE_DIRECT_GPIO
// 使用寄存器(快速)
#define LCD_SCLK_Clr() (SCLK_PORT->out_clr = SCLK_PIN) // SCL 清零
#define LCD_SCLK_Set() (SCLK_PORT->out_set = SCLK_PIN) // SCL 置位

#define LCD_MOSI_Clr() (MOSI_PORT->out_clr = MOSI_PIN) // MOSI 清零
#define LCD_MOSI_Set() (MOSI_PORT->out_set = MOSI_PIN) // MOSI 置位

#define LCD_RES_Clr()  (RES_PORT->out_clr = RES_PIN)    // RES 清零
#define LCD_RES_Set()  (RES_PORT->out_set = RES_PIN)    // RES 置位

#define LCD_DC_Clr()   (DC_PORT->out_clr = DC_PIN)      // DC 清零
#define LCD_DC_Set()   (DC_PORT->out_set = DC_PIN)      // DC 置位

#define LCD_CS_Clr()   (CS_PORT->out_clr = CS_PIN)      // CS 清零
#define LCD_CS_Set()   (CS_PORT->out_set = CS_PIN)      // CS 置位

#define LCD_BLK_Clr()  (BLK_PORT->out_clr = BLK_PIN)    // BLK 清零
#define LCD_BLK_Set()  (BLK_PORT->out_set = BLK_PIN)    // BLK 置位

#else
// 使用 SDK API(兼容但较慢)
#define LCD_SCLK_Clr() MXC_GPIO_OutClr(SCLK_PORT,SCLK_PIN)//SCL=SCLK
#define LCD_SCLK_Set() MXC_GPIO_OutSet(SCLK_PORT,SCLK_PIN)

#define LCD_MOSI_Clr() MXC_GPIO_OutClr(MOSI_PORT,MOSI_PIN)//SDA=MOSI
#define LCD_MOSI_Set() MXC_GPIO_OutSet(MOSI_PORT,MOSI_PIN)

#define LCD_RES_Clr()  MXC_GPIO_OutClr(RES_PORT,RES_PIN)//RES
#define LCD_RES_Set()  MXC_GPIO_OutSet(RES_PORT,RES_PIN)

#define LCD_DC_Clr()   MXC_GPIO_OutClr(DC_PORT,DC_PIN)//DC
#define LCD_DC_Set()   MXC_GPIO_OutSet(DC_PORT,DC_PIN)

#define LCD_CS_Clr()   MXC_GPIO_OutClr(CS_PORT,CS_PIN)//CS
#define LCD_CS_Set()   MXC_GPIO_OutSet(CS_PORT,CS_PIN)

#define LCD_BLK_Clr()  MXC_GPIO_OutClr(BLK_PORT,BLK_PIN)//BLK
#define LCD_BLK_Set()  MXC_GPIO_OutSet(BLK_PORT,BLK_PIN)
#endif

void LCD_GPIO_Init(void);//初始化GPIO
void LCD_Writ_Bus(u8 dat);//模拟SPI时序
void LCD_WR_DATA8(u8 dat);//写入一个字节
void LCD_WR_DATA(u16 dat);//写入两个字节
void LCD_WR_REG(u8 dat);//写入一个指令
void LCD_Address_Set(u16 x1,u16 y1,u16 x2,u16 y2);//设置坐标函数
void LCD_Init(void);//LCD初始化
#endif


lcd_init.c

LCD_Writ_Bus()函数为软件SPI的核心,原理就是通过要发送的数据每一位的数值来翻转GPIO电平。若要修改为硬件SPI则修改LCD_Writ_Bus()函数的内部实现,改为硬件SPI发送数据即可。

#include "lcd_init.h"

/******************************************************************************
      函数说明:LCD串行数据写入函数
      入口数据:dat  要写入的串行数据
      返回值:  无
******************************************************************************/
void LCD_Writ_Bus(u8 dat)
{  
    u8 i;
    LCD_CS_Clr();      // CS = 0,片选
    for(i=0;i<8;i++)
    {            
        LCD_SCLK_Clr();   // SCLK = 0
        if(dat&0x80)
        {
           LCD_MOSI_Set();
        }
        else
        {
           LCD_MOSI_Clr();
        }
        LCD_SCLK_Set();   // SCLK = 1,上升沿采样数据
        dat<<=1;
    }  
//  LCD_SCLK_Clr();   // SCLK = 0,回到空闲状态
    LCD_CS_Set();     // CS = 1,释放片选
}

/******************************************************************************
      函数说明:LCD写入数据
      入口数据:dat 写入的数据
      返回值:  无
******************************************************************************/
void LCD_WR_DATA8(u8 dat)
{
    LCD_Writ_Bus(dat);
}

/******************************************************************************
      函数说明:LCD写入数据
      入口数据:dat 写入的数据
      返回值:  无
******************************************************************************/
void LCD_WR_DATA(u16 dat)
{
    LCD_Writ_Bus(dat>>8);
    LCD_Writ_Bus(dat);
}

/******************************************************************************
      函数说明:LCD写入命令
      入口数据:dat 写入的命令
      返回值:  无
******************************************************************************/
void LCD_WR_REG(u8 dat)
{
    LCD_DC_Clr();//写命令
    LCD_Writ_Bus(dat);
    LCD_DC_Set();//写数据
}

/******************************************************************************
      函数说明:设置起始和结束地址
      入口数据:x1,x2 设置列的起始和结束地址
                y1,y2 设置行的起始和结束地址
      返回值:  无
******************************************************************************/
void LCD_Address_Set(u16 x1,u16 y1,u16 x2,u16 y2)
{
    if(USE_HORIZONTAL==0)
    {
        LCD_WR_REG(0x2a);//列地址设置
        LCD_WR_DATA(x1+52);
        LCD_WR_DATA(x2+52);
        LCD_WR_REG(0x2b);//行地址设置
        LCD_WR_DATA(y1+40);
        LCD_WR_DATA(y2+40);
        LCD_WR_REG(0x2c);//储存器写
    }
    else if(USE_HORIZONTAL==1)
    {
        LCD_WR_REG(0x2a);//列地址设置
        LCD_WR_DATA(x1+53);
        LCD_WR_DATA(x2+53);
        LCD_WR_REG(0x2b);//行地址设置
        LCD_WR_DATA(y1+40);
        LCD_WR_DATA(y2+40);
        LCD_WR_REG(0x2c);//储存器写
    }
    else if(USE_HORIZONTAL==2)
    {
        LCD_WR_REG(0x2a);//列地址设置
        LCD_WR_DATA(x1+40);
        LCD_WR_DATA(x2+40);
        LCD_WR_REG(0x2b);//行地址设置
        LCD_WR_DATA(y1+53);
        LCD_WR_DATA(y2+53);
        LCD_WR_REG(0x2c);//储存器写
    }
    else
    {
        LCD_WR_REG(0x2a);//列地址设置
        LCD_WR_DATA(x1+40);
        LCD_WR_DATA(x2+40);
        LCD_WR_REG(0x2b);//行地址设置
        LCD_WR_DATA(y1+52);
        LCD_WR_DATA(y2+52);
        LCD_WR_REG(0x2c);//储存器写
    }
}

//LCD屏幕初始化函数
void LCD_Init(void)
{
    LCD_GPIO_Init();//初始化GPIO
   
    LCD_RES_Clr();//复位
    delay_ms(100);
    LCD_RES_Set();
    delay_ms(100);
   
    LCD_BLK_Set();//打开背光
    delay_ms(100);
   
    LCD_WR_REG(0x11);
    delay_ms(120);
    LCD_WR_REG(0x36);
    if(USE_HORIZONTAL==0)LCD_WR_DATA8(0x00);
    else if(USE_HORIZONTAL==1)LCD_WR_DATA8(0xC0);
    else if(USE_HORIZONTAL==2)LCD_WR_DATA8(0x70);
    else LCD_WR_DATA8(0xA0);

    LCD_WR_REG(0x3A);
    LCD_WR_DATA8(0x05);

    LCD_WR_REG(0xB2);
    LCD_WR_DATA8(0x0C);
    LCD_WR_DATA8(0x0C);
    LCD_WR_DATA8(0x00);
    LCD_WR_DATA8(0x33);
    LCD_WR_DATA8(0x33);

    LCD_WR_REG(0xB7);
    LCD_WR_DATA8(0x35);  

    LCD_WR_REG(0xBB);
    LCD_WR_DATA8(0x19);

    LCD_WR_REG(0xC0);
    LCD_WR_DATA8(0x2C);

    LCD_WR_REG(0xC2);
    LCD_WR_DATA8(0x01);

    LCD_WR_REG(0xC3);
    LCD_WR_DATA8(0x12);  

    LCD_WR_REG(0xC4);
    LCD_WR_DATA8(0x20);  

    LCD_WR_REG(0xC6);
    LCD_WR_DATA8(0x0F);    

    LCD_WR_REG(0xD0);
    LCD_WR_DATA8(0xA4);
    LCD_WR_DATA8(0xA1);

    LCD_WR_REG(0xE0);
    LCD_WR_DATA8(0xD0);
    LCD_WR_DATA8(0x04);
    LCD_WR_DATA8(0x0D);
    LCD_WR_DATA8(0x11);
    LCD_WR_DATA8(0x13);
    LCD_WR_DATA8(0x2B);
    LCD_WR_DATA8(0x3F);
    LCD_WR_DATA8(0x54);
    LCD_WR_DATA8(0x4C);
    LCD_WR_DATA8(0x18);
    LCD_WR_DATA8(0x0D);
    LCD_WR_DATA8(0x0B);
    LCD_WR_DATA8(0x1F);
    LCD_WR_DATA8(0x23);

    LCD_WR_REG(0xE1);
    LCD_WR_DATA8(0xD0);
    LCD_WR_DATA8(0x04);
    LCD_WR_DATA8(0x0C);
    LCD_WR_DATA8(0x11);
    LCD_WR_DATA8(0x13);
    LCD_WR_DATA8(0x2C);
    LCD_WR_DATA8(0x3F);
    LCD_WR_DATA8(0x44);
    LCD_WR_DATA8(0x51);
    LCD_WR_DATA8(0x2F);
    LCD_WR_DATA8(0x1F);
    LCD_WR_DATA8(0x1F);
    LCD_WR_DATA8(0x20);
    LCD_WR_DATA8(0x23);

    LCD_WR_REG(0x21);

    LCD_WR_REG(0x29);
}

LCD的其他功能接口函数在这里不在展示,放在文章末尾的附件处。


定时器初始化

drv_time.c:

自己封装了一个定时器初始化函数,每1ms溢出一次,为LVGL提供心跳。

/**
 * @file    drv_time.c
 * @brief   1ms Timer interrupt example
 * @details Configures a timer to generate interrupts every 1ms
 */

/***** Includes *****/
#include "nvic_table.h"
#include "gpio.h"
#include "drv_time.h"
/***** Definitions *****/
// Parameters for 1ms timer
#define TIMER_1MS_CLOCK_SOURCE MXC_TMR_IBRO_CLK  // \ref mxc_tmr_clock_t
#define TIMER_1MS_FREQ 1000  // (Hz) - 1ms period = 1000Hz frequency
#define TIMER_1MS MXC_TMR0   // Can be MXC_TMR0 through MXC_TMR5

/***** Global Variables *****/
volatile uint32_t timer_count = 0;  // Interrupt counter

/***** Functions *****/
/**
 * @brief   Timer 1ms interrupt handler
 * @details Called every 1ms when timer overflows
 */
__weak void DrvTimeHandler(void)
{
    // Clear interrupt flag
    MXC_TMR_ClearFlags(TIMER_1MS);
   
    // // Increment counter
    // timer_1ms_count++;
    MXC_GPIO_OutToggle(MXC_GPIO0, MXC_GPIO_PIN_9);
    // Add your 1ms interval code here
    // For example: toggle LED, read sensor, update state machine, etc.
}

/**
 * @brief   Initialize 1ms timer with interrupt
 * @details Configures the timer for continuous mode with 1ms period
 * @return  E_NO_ERROR if successful, error code otherwise
 */
int DrvTimeInit(void)
{
    // Declare variables
    mxc_tmr_cfg_t tmr;
    uint32_t periodTicks = MXC_TMR_GetPeriod(TIMER_1MS, TIMER_1MS_CLOCK_SOURCE, 1, TIMER_1MS_FREQ);
   
    /*
    Steps for configuring a timer for continuous mode:
    1. Disable the timer
    2. Set the prescale value
    3. Configure the timer for continuous mode
    4. Set timer parameters
    5. Enable interrupts
    6. Enable Timer
    */
   
    // Step 1: Disable the timer
    MXC_TMR_Shutdown(TIMER_1MS);
   
    // Step 2: Configure timer structure
    tmr.pres = TMR_PRES_1;           // Prescale: 1
    tmr.mode = TMR_MODE_CONTINUOUS;  // Continuous mode
    tmr.bitMode = TMR_BIT_MODE_32;   // 32-bit mode
    tmr.clock = TIMER_1MS_CLOCK_SOURCE;
    tmr.cmp_cnt = periodTicks;       // Compare count for 1ms period
    tmr.pol = 0;                     // Polarity: normal
   
    // Step 3: Initialize timer
    if (MXC_TMR_Init(TIMER_1MS, &tmr, true) != E_NO_ERROR) {
        printf("Failed to initialize 1ms timer.\n");
        return E_FAIL;
    }
   
    // Step 4: Set up interrupt handler
    MXC_NVIC_SetVector(TMR0_IRQn, DrvTimeHandler);
    NVIC_EnableIRQ(TMR0_IRQn);
   
    // Step 5: Enable timer interrupts
    MXC_TMR_EnableInt(TIMER_1MS);
   
    // Step 6: Start the timer
    MXC_TMR_Start(TIMER_1MS);
   
    printf("1ms timer initialized and started.\n");
    return E_NO_ERROR;
}

/**
 * @brief   Get current timer interrupt count
 * @return  Number of 1ms intervals elapsed
 */
uint32_t DrvTimeGetCount(void)
{
    return timer_count;
}

/**
 * @brief   Reset timer interrupt count
 */
void DrvTimeResetCount(void)
{
    timer_count = 0;
}

/**
 * @brief   Stop 1ms timer
 */
void DrvTimeStop(void)
{
    MXC_TMR_Stop(TIMER_1MS);
    MXC_TMR_Shutdown(TIMER_1MS);
    NVIC_DisableIRQ(TMR0_IRQn);
    printf("1ms timer stopped.\n");
}


main函数

main.c:

main函数逻辑为初始化需要的外设和模块,然后轮询读取ADC的值和执行LVGL事务处理函数。定时器的中断回调函数在main.c中进行了重定义,添加了LVGL的心跳函数。ADC的初始化没有另外封装,直接在main.c中实现和使用。ADC配置为缩放三倍输入,也就是1.22 * 3 = 3.66V 可检测范围为 0~3.66V

/**
 * @file        main.c
 * @details
 */
/***** Includes *****/
#include <stdio.h>
#include <string.h>
#include "mxc_device.h"
#include "mxc_delay.h"
#include "adc.h"
#include "lcd.h"
#include "lcd_init.h"
#include "drv_gpio.h"
#include "drv_time.h"

#include "lvgl.h"
#include "lv_port_disp_template.h"
#include "lv_port_indev_template.h"
#include "gui_guider.h"
#include "events_init.h"

/***** Functions *****/
//ADC外设初始化
int Drv_ADC_Init(void);
//按指定通道读取ADC数据
int Drv_ADC_GetDataForChannel(mxc_adc_chsel_t channel, uint16_t *outdata);

/***** Definitions *****/
int main(void)
{
    LCD_Init();//LCD初始化
    LCD_Fill(0,0,LCD_W,LCD_H,BLACK);
    LCD_ShowString(0,0,"Holle World !\n",WHITE,BLACK,24,0);
    printf("Holle World !\n");

    DrvTimeInit();
    //DrvPWMInit(50);

    lv_init();
    lv_port_disp_init();  // lvgl显示接口初始化,放在lv_init()的后面
    lv_port_indev_init(); // lvgl输入接口初始化,放在lv_init()的后面
    lv_ui guider_ui;
    setup_ui(&guider_ui);
    events_init(&guider_ui);

    if(Drv_ADC_Init() != 0)  return -1;
    mxc_adc_chsel_t channel_2 = MXC_ADC_CH_3;
    mxc_adc_chsel_t channel_1 = MXC_ADC_CH_5;
    static uint16_t adc_val_2;
    static uint16_t adc_val_1;
    MXC_ADC_SetExtScale(MXC_ADC_SCALE_3);

    while (1) {
        lv_task_handler(); // lvgl的事务处理

        if (Drv_ADC_GetDataForChannel(channel_2, &adc_val_2))
        {
            printf("ADC channel %d Value: %d\n",channel_2, adc_val_2);
            LCD_ShowIntNum(95,20,adc_val_2,4,BLACK,WHITE,24);
            LCD_ShowFloatNum1(160,20,adc_val_2 / (float)280,4,BLACK,WHITE,24);
            LCD_ShowChar(220,20,'V',BLACK,WHITE,24,1);
        }

        if (Drv_ADC_GetDataForChannel(channel_1, &adc_val_1))
        {
            printf("ADC channel %d Value: %d\n",channel_1, adc_val_1);
            LCD_ShowIntNum(95,78,adc_val_1,4,BLACK,WHITE,24);
            LCD_ShowFloatNum1(160,78,adc_val_1 / (float)280,4,BLACK,WHITE,24);
            LCD_ShowChar(220,78,'V',BLACK,WHITE,24,1);
        }
       
        delay_ms(100);
    }
    return 0;
}

//中断回调函数
void DrvTimeHandler(void)
{
    // Clear interrupt flag
    MXC_TMR_ClearFlags(MXC_TMR0);
    // Toggle LED
    // MXC_GPIO_OutToggle(MXC_GPIO2, MXC_GPIO_PIN_2);
    lv_tick_inc(1);//lvgl的1ms中断
}

int Drv_ADC_Init(void)
{
    /* Initialize ADC */
    if (MXC_ADC_Init() != E_NO_ERROR) {
        printf("Error Bad Parameter\n");
        return -1;
    }
    return 0;
}

//按指定通道读取数据
int Drv_ADC_GetDataForChannel(mxc_adc_chsel_t channel, uint16_t *outdata)
{
    MXC_ADC_StartConversion(channel);
    return (MXC_ADC_GetData(outdata) == E_OVERFLOW ? 0 : 1);
}

LVGL相关文件较多不在列举,放在文章末尾的附件处。


四、功能展示图片及说明

实物图:

P20251130-153632.jpg

LCD屏幕接线:

SCLK-------------P0_24

MOSI------------P0_20

RES--------------P1_8

DC---------------P1_9

CS---------------P0_30

BLK--------------P0_31

电位器接线:

电位器1----------P2_3(AIN3)

电位器2----------P2_5(AIN5)


电位器一端接Vcc一端接ADC输入脚,通过旋转电位器改变电阻,从而产生不同的模拟电压,MAX32655读取ADC数值并将其转化为电压值,在LCD屏幕上显示。LCD屏幕通过移植的LVGL预先显示一个制作好的UI界面,然后在对应位置打印ADC数值和转换后的电压值。


五、项目中遇到的难题和解决方法

1.GPIO的不同配置选项

// 通用GPIO配置函数,消除代码重复
static void LCD_ConfigGPIO(mxc_gpio_regs_t *port, uint32_t pin)
{
    mxc_gpio_cfg_t gpio_out = {
        .port = port,
        .mask = pin,
        .pad = MXC_GPIO_PAD_NONE,
        .func = MXC_GPIO_FUNC_OUT,
        .vssel = MXC_GPIO_VSSEL_VDDIOH, //3.3V高电压
        .drvstr = MXC_GPIO_DRVSTR_3 //大电流输出
    };
    MXC_GPIO_Config(&gpio_out);
}

起初我没有在意GPIO的配置,只是用官方提供的GPIO初始化的方式去跑,先做了个流水灯,发现流水灯的个别几个灯比较暗淡,不过也没有在意。后来移植LCD屏幕驱动的时候总是无法点亮屏幕,用示波器抓了波形,没有发现问题该发送的数据正常发送,用逻辑分析仪抓数据也是想要的数据。后来分别抓了其他引脚的数据,发现有的引脚高电平只有Vcc的一半,后来研究GPIO配置选项得知GPIO配置结构体mxc_gpio_cfg_t中成员vssel控制GPIO电压输出的大小,vssel为MXC_GPIO_VSSEL_VDDIO时GPIO高电平大约为1.65V,vssel为MXC_GPIO_VSSEL_VDDIOH时GPIO高电平大约为3.3V。GPIO配置结构体mxc_gpio_cfg_t中成员drvstr控制GPIO输出电流的大小,由于我使用的是GPIO模拟的SPI则GPIO输出大电流有利于我提高软件SPI速率。

/**
 * @brief   Enumeration type for the voltage level on a given pin.
 */
typedef enum {
    MXC_GPIO_VSSEL_VDDIO, /**< Set pin to VIDDIO voltage */
    MXC_GPIO_VSSEL_VDDIOH, /**< Set pin to VIDDIOH voltage */
} mxc_gpio_vssel_t;

/**
 * @brief   Enumeration type for drive strength on a given pin.
 *          This represents what the two GPIO_DS[2] (Drive Strength)
 *          registers are set to for a given GPIO pin; NOT the
 *          drive strength level.
 *
 *          For example:
 *              MXC_GPIO_DRVSTR_0: GPIO_DS1[pin] = 0; GPIO_DS0[pin] = 0
 *              MXC_GPIO_DRVSTR_1: GPIO_DS1[pin] = 0; GPIO_DS0[pin] = 1
 *              MXC_GPIO_DRVSTR_2: GPIO_DS1[pin] = 1; GPIO_DS0[pin] = 0
 *              MXC_GPIO_DRVSTR_3: GPIO_DS1[pin] = 1; GPIO_DS0[pin] = 1
 *
 *          Refer to the user guide and datasheet to select the
 *          appropriate drive strength. Note: the drive strength values
 *          are not linear, and can vary from pin-to-pin and the state
 *          of the GPIO pin (alternate function and voltage level).
 */
typedef enum {
    MXC_GPIO_DRVSTR_0, /**< Drive Strength GPIO_DS[2][pin]=0b00 */
    MXC_GPIO_DRVSTR_1, /**< Drive Strength GPIO_DS[2][pin]=0b01 */
    MXC_GPIO_DRVSTR_2, /**< Drive Strength GPIO_DS[2][pin]=0b10 */
    MXC_GPIO_DRVSTR_3, /**< Drive Strength GPIO_DS[2][pin]=0b11 */
} mxc_gpio_drvstr_t;


2.LVGL的移植

之前完全没有接触过LVGL的移植,首次接触疑惑还是比较多的,看大佬们的教程视频并跟着一步一步做,逐渐理解了LVGL的运行机制。
首先就是源文件和头文件的包含,包含文件较多且makefile文件架构接触的也不多,根据AI的提示逐步完成了文件的包含问题。VPATH += ... (文件夹地址)即为包含该文件夹下源文件,IPATH += ... (文件夹地址)即为包含该文件夹下头文件。对于需要包含众多子文件夹内的文件,则可以SRCS += $(shell find ... (文件夹地址 -name '*.c' -print)这样的方式一键包含,不过你的构建环境必须支持shell 'find'功能。

# Source search paths for this project
VPATH += ST7789         # ST7789 display driver  
VPATH += Drive/TIM
VPATH += Drive/GPIO
VPATH += Drive/ADC
VPATH += Drive/PWM  
VPATH += LVGL           # keep top-level if you want
VPATH += LVGL/examples/porting
VPATH += LVGL/src
VPATH += LVGL/custom
 
# Header search paths for this project
IPATH += ST7789         # ST7789 display driver headers
IPATH += Drive/TIM
IPATH += Drive/GPIO
IPATH += Drive/ADC
IPATH += Drive/PWM
IPATH += LVGL           # LVGL headers
IPATH += LVGL/examples/porting
IPATH += LVGL/src
IPATH += LVGL/src/font
IPATH += LVGL/custom
IPATH += LVGL/generated
IPATH += LVGL/generated/guider_customer_fonts
IPATH += LVGL/generated/guider_fonts

# If your build environment supports a shell 'find' you can alternatively
# append all LVGL .c files recursively with something like:
SRCS += $(shell find LVGL/src -name '*.c' -print)
SRCS += $(shell find LVGL/generated -name '*.c' -print)


其次是LVGL的移植的细节,LVGL的移植主要就是提供画点函数,但其实这个画点函数更像是绘制图片函数,要是真提供一个每次只画一个点的函数,则运行效率会及其的慢。这里我提供的就是一个绘制图片函数。

/*Flush the content of the internal buffer the specific area on the display
 *You can use DMA or any hardware acceleration to do this operation in the background but
 *'lv_disp_flush_ready()' has to be called when finished.*/
static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
    /*The most simple case (but also the slowest) to put all pixels to the screen one-by-one*/
    // 1. 计算 LVGL 当前刷新区域的宽度和高度
    u16 w = area->x2 - area->x1 + 1; // 区域宽度 (对应 LCD_ShowPicture 的 length)
    u16 h = area->y2 - area->y1 + 1; // 区域高度 (对应 LCD_ShowPicture 的 width)
    // 2. 调用你的显示图片函数
    // 注意:
    // a. 参数顺序是 x, y, 宽度(length), 高度(width)
    // b. 需要将 color_p (lv_color_t 类型) 强制转换为 (u8 *) 以匹配函数定义
    LCD_ShowPicture(area->x1, area->y1, w, h, (u8 *)color_p);
    /*IMPORTANT!!!
     *Inform the graphics library that you are ready with the flushing*/
    lv_disp_flush_ready(disp_drv);
}

对于触摸的移植我暂时还没有实现,因为我的屏幕不是一个触摸屏。还有移植需要注意色彩的问题,因为一般用到的都是565格式的16位颜色数据,牵扯到先发高位还是低位,如果颜色不对可以去lv_conf.h中更改。长和宽还有屏幕的显示方向也要和设计的LVGL UI界面对应好。

#define MY_DISP_VER_RES   LCD_H
#define MY_DISP_HOR_RES   LCD_W
// #define MY_DISP_VER_RES   LCD_W
// #define MY_DISP_HOR_RES   LCD_H

/*Color depth: 1 (1 byte per pixel), 8 (RGB332), 16 (RGB565), 32 (ARGB8888)*/
#define LV_COLOR_DEPTH 16

/*Swap the 2 bytes of RGB565 color. Useful if the display has an 8-bit interface (e.g. SPI)*/
#define LV_COLOR_16_SWAP 1


六、对本活动的心得体会

本项目虽然功能看似简单,但涵盖了底层寄存器操作、外设驱动调试、构建系统配置以及第三方 GUI 库移植的全过程。

特别是通过示波器解决 GPIO 电压域问题的经历,让我从单纯的“写代码”进阶到了“软硬结合排查问题”的层面。目前系统已能稳定运行,UI 交互流畅。后续计划在当前基础上尝试触摸屏驱动的移植,并利用 MAX32655 的低功耗特性优化电源管理,向企业级项目的标准靠拢。
最后非常感谢硬禾学堂联合DigiKey发起的Funpack“玩成功就全额退”活动。希望今后能开展更多独特且有趣的活动!


附件下载
MAX32655数据手册.pdf
MAX32655用户手册.pdf
MAX32655FTHR.pdf
板卡原理图
lcd_st7789_32655.7z
项目源码
团队介绍
电子开发爱好者
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号