任务需要移植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调试器的使用,最后也成功完成了任务。