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

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控制台
板卡引脚图:

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系统框图

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相关文件较多不在列举,放在文章末尾的附件处。
四、功能展示图片及说明
实物图:

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“玩成功就全额退”活动。希望今后能开展更多独特且有趣的活动!