06-HAL库硬件SPIDMA驱动LCD并移植LVGL8.3

原创 小飞哥玩嵌入式 2024-06-12 08:00

关注、星标公众号,直达精彩内容

1、本节内容介绍

  • 1.1、HAL库硬件SPI DMA在cubemx中的配置及注意事项;
  • 1.2、HAL库SPI DMA详解与结构介绍;
  • 1.3、使用SPI DMA驱动LCD显示屏并移植LVGL V8.3

源码地址:https://gitee.com/MR_Wyf/hal-cubemx-rt-thread/tree/hal_rttNano_st7789_menu/

或者关注公众号,后台回复“SPI DMA”,获取本章节源码

2、HAL库SPI DMA在CUBEMX中的配置

2.1、配置界面

配置非常简单,只需要选择SPI1的TX配置为DMA模式即可,选择正常模式即可,不需要循环模式,否则LVGL可能会显示异常

2.2、SPI DMA代码详解

先来看上面配置生成的代码,主要就是DMA的模式配置参数

    /* SPI1 DMA Init */
    /* SPI1_TX Init */
    hdma_spi1_tx.Instance = DMA1_Channel3;
    hdma_spi1_tx.Init.Request = DMA_REQUEST_1;
    hdma_spi1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
    hdma_spi1_tx.Init.PeriphInc = DMA_PINC_DISABLE;
    hdma_spi1_tx.Init.MemInc = DMA_MINC_ENABLE;
    hdma_spi1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
    hdma_spi1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
    hdma_spi1_tx.Init.Mode = DMA_NORMAL;
    hdma_spi1_tx.Init.Priority = DMA_PRIORITY_LOW;
    if (HAL_DMA_Init(&hdma_spi1_tx) != HAL_OK)
    {
      Error_Handler();
    }

    __HAL_LINKDMA(spiHandle,hdmatx,hdma_spi1_tx);

继续来看下SPI DMA的接口:

HAL_StatusTypeDef HAL_SPI_Transmit_DMA(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_SPI_Receive_DMA(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_SPI_TransmitReceive_DMA(SPI_HandleTypeDef *hspi, uint8_t *pTxData, uint8_t *pRxData,
                                              uint16_t Size)
;
HAL_StatusTypeDef HAL_SPI_DMAPause(SPI_HandleTypeDef *hspi);
HAL_StatusTypeDef HAL_SPI_DMAResume(SPI_HandleTypeDef *hspi);
HAL_StatusTypeDef HAL_SPI_DMAStop(SPI_HandleTypeDef *hspi);

主要就是发送、接收,以及接收暂停、接受恢复、接收停止功能函数,本次驱动LCD主要用到的是DMA发送函数,主要有3个参数

/**
  * @brief  Transmit an amount of data in non-blocking mode with DMA.
  * @param  hspi pointer to a SPI_HandleTypeDef structure that contains
  *               the configuration information for SPI module.
  * @param  pData pointer to data buffer
  * @param  Size amount of data to be sent
  * @retval HAL status
  */

HAL_StatusTypeDef HAL_SPI_Transmit_DMA(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size);

2.3 LCD驱动函数改造

上一章节中我们使用的是SPI驱动的LCD,并没有加入DMA,本章节咱们对驱动函数进行改造,加入DMA驱动,只需要把我们的SPI发送函数改为DMA发送函数即可,以下几个函数同理改动

// ST7789写函数
static HAL_StatusTypeDef lcd_st7789_write(int is_cmd, uint8_t data)
{
 uint8_t pData[2] = { 0 };
 assert_param(NULL != hspi_lcd);
 pData[0] = data;
 if (is_cmd)
  HAL_GPIO_WritePin(LCD_DC_GPIO_Port, LCD_DC_Pin, GPIO_PIN_RESET);
 else
  HAL_GPIO_WritePin(LCD_DC_GPIO_Port, LCD_DC_Pin, GPIO_PIN_SET);

// return HAL_SPI_Transmit(hspi_lcd, pData, 1, HAL_MAX_DELAY);
  return HAL_SPI_Transmit_DMA(hspi_lcd, pData, 1);

}
/********************************************************************
 *
 *       LcdWriteReg
 *
 * Function description:
 *   Sets display register
 */

void lcd_st7789_write_reg(uint8_t Data)
{
 HAL_GPIO_WritePin(LCD_DC_GPIO_Port, LCD_DC_Pin, GPIO_PIN_RESET);
// HAL_SPI_Transmit(&hspi1, &Data, 1, 10);
 HAL_SPI_Transmit_DMA(&hspi1, &Data, 1);
}
/********************************************************************
 *
 *       LcdWriteData
 *
 * Function description:
 *   Writes a value to a display register
 */

void lcd_st7789_write_data(uint8_t Data)
{
 HAL_GPIO_WritePin(LCD_DC_GPIO_Port, LCD_DC_Pin, GPIO_PIN_SET);
 HAL_SPI_Transmit_DMA(&hspi1, &Data, 1);

 //HAL_SPI_Transmit(&hspi1, &Data, 1, 10);
}

/********************************************************************
 *
 *       lcd_st7789_write_data_multiple
 *
 * Function description:
 *   Writes multiple values to a display register.
 */

extern uint8_t g_spi_dma_tc;

void lcd_st7789_write_data_multiple(uint8_t *pData, int NumItems)
{
 if (g_spi_dma_tc) {
  g_spi_dma_tc = 0;
  HAL_GPIO_WritePin(LCD_DC_GPIO_Port, LCD_DC_Pin, GPIO_PIN_SET);
  //HAL_SPI_Transmit(&hspi1, pData, NumItems, 10);
  HAL_SPI_Transmit_DMA(&hspi1, pData, NumItems);
 }
}

3、移植LVGL V8.3

本次移植的是lvgl V8.3,源码在lvgl官方github上就可以下载到,想下载的兄弟小手动一动,不想下载的也没关系,小飞哥会把源码开源,直接拿过去就行了

至于LVGL的移植,就不再赘述了,相信网上有成堆的教程,小飞哥也不再废话浪费大家伙时间了,直接下载源码即可

主要强调几个移植的点:

  • 1、周期调用lvgl tick接口,提供lvgl“心跳”

在定时器3回调函数中调用lv_tick_inc(1),为LVGL提供心跳,周期10ms足够

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{

  /**timer for lvgl,period 1ms*/
  if (htim->Instance == TIM3)
  {
    lv_tick_inc(1);
  }
  if (htim->Instance == TIM15)
  {
    // if(RT_EOK==rt_sem_take(sem_uart_rec,RT_WAITING_NO))
    // {
    if (embedded_get_uart_rec_flag())
    {
      /*100ms*/
      if (embedded_get_uart_timeout_cnt() > 9)
      {
        //      lv_tick_inc(1);

        embedded_set_uart_rec_flag(RT_FALSE);

        rt_sem_release(sem_uart_timeout);
      }
    }

    // }
  }
}
  • 2、lvgl初始化配置,使用“双缓存”
void lv_port_disp_init(void)
{
    /*-------------------------
     * Initialize your display
     * -----------------------*/

    disp_init();

    /*-----------------------------
     * Create a buffer for drawing
     *----------------------------*/


    /**
     * LVGL requires a buffer where it internally draws the widgets.
     * Later this buffer will passed to your display driver's `flush_cb` to copy its content to your display.
     * The buffer has to be greater than 1 display row
     *
     * There are 3 buffering configurations:
     * 1. Create ONE buffer:
     *      LVGL will draw the display's content here and writes it to your display
     *
     * 2. Create TWO buffer:
     *      LVGL will draw the display's content to a buffer and writes it your display.
     *      You should use DMA to write the buffer's content to the display.
     *      It will enable LVGL to draw the next part of the screen to the other buffer while
     *      the data is being sent form the first buffer. It makes rendering and flushing parallel.
     *
     * 3. Double buffering
     *      Set 2 screens sized buffers and set disp_drv.full_refresh = 1.
     *      This way LVGL will always provide the whole rendered screen in `flush_cb`
     *      and you only need to change the frame buffer's address.
     */


    /* Example for 1) */
    static lv_disp_draw_buf_t draw_buf_dsc_1;
    static lv_color_t buf_1[MY_DISP_HOR_RES * 10];                             /*A buffer for 10 rows*/
    lv_disp_draw_buf_init(&draw_buf_dsc_1, buf_1, NULL, MY_DISP_HOR_RES * 10); /*Initialize the display buffer*/

    /* Example for 2) */
    static lv_disp_draw_buf_t draw_buf_dsc_2;
    static lv_color_t buf_2_1[MY_DISP_HOR_RES * 10];                                /*A buffer for 10 rows*/
    static lv_color_t buf_2_2[MY_DISP_HOR_RES * 10];                                /*An other buffer for 10 rows*/
    lv_disp_draw_buf_init(&draw_buf_dsc_2, buf_2_1, buf_2_2, MY_DISP_HOR_RES * 10); /*Initialize the display buffer*/

    /* Example for 3) also set disp_drv.full_refresh = 1 below*/
    static lv_disp_draw_buf_t draw_buf_dsc_3;
    static lv_color_t buf_3_1[MY_DISP_HOR_RES * MY_DISP_VER_RES]; /*A screen sized buffer*/
    static lv_color_t buf_3_2[MY_DISP_HOR_RES * MY_DISP_VER_RES]; /*Another screen sized buffer*/
    lv_disp_draw_buf_init(&draw_buf_dsc_3, buf_3_1, buf_3_2,
                          MY_DISP_VER_RES * MY_DISP_VER_RES); /*Initialize the display buffer*/

    /*-----------------------------------
     * Register the display in LVGL
     *----------------------------------*/


    static lv_disp_drv_t disp_drv; /*Descriptor of a display driver*/
    lv_disp_drv_init(&disp_drv);   /*Basic initialization*/

    /*Set up the functions to access to your display*/

    /*Set the resolution of the display*/
    disp_drv.hor_res = MY_DISP_HOR_RES;
    disp_drv.ver_res = MY_DISP_VER_RES;

    /*Used to copy the buffer's content to the display*/
    disp_drv.flush_cb = disp_flush;

    /*Set a display buffer*/
    disp_drv.draw_buf = &draw_buf_dsc_2;
    disp_drv_p = &disp_drv;
    /*Required for Example 3)*/
    // disp_drv.full_refresh = 1;

    /* Fill a memory array with a color if you have GPU.
     * Note that, in lv_conf.h you can enable GPUs that has built-in support in LVGL.
     * But if you have a different GPU you can use with this callback.*/

    // disp_drv.gpu_fill_cb = gpu_fill;

    /*Finally register the driver*/
    lv_disp_drv_register(&disp_drv);
}

刷新函数:

/*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)
{
    if (disp_flush_enabled)
    {
        // /*The most simple case (but also the slowest) to put all pixels to the screen one-by-one*/
        //        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)*/
        //                lcd_st7789_write_pixel(x, y, color_p->full);
        //                color_p++;
        //            }
        //        }

        // int32_t y;

        // lcd_st7789_set_addr_win(area->x1, area->y1, area->x2, area->y2); // 指定填充区域
        //                                                                  // 一行一行 DMA
        // for (y = area->y1; y <= area->y2; y++)
        // {
        //     lcd_st7789_write_data_multiple((uint8_t *)color_p, (uint16_t)(area->x2 - area->x1 + 1) * 2);
        //     color_p += (area->x2 - area->x1 + 1);
        // }

        unsigned int size = (area->x2 - area->x1 + 1) * (area->y2 - area->y1 + 1) * 2;
        lcd_st7789_set_addr_win(area->x1, area->y1, area->x2, area->y2); // 指定填充区域
        lcd_st7789_write_data_multiple((uint8_t *)color_p, size);
    }

    /*IMPORTANT!!!
     *Inform the graphics library that you are ready with the flushing*/

    //    lv_disp_flush_ready(disp_drv);
}

  • 3、移植后的目录如下

4、代码展示

我们还是在LCD的任务中,替换掉我们上一章节展示的电子表功能,代码如下:

/**
 * @function lcd menu thread
 * @author:小飞哥玩嵌入式-小飞哥
 * @TODO: LED控制线程
 * @param:
 * @return: NULL
 */

static void rt_lcd_menu_entry(void *parameter)
{
 uint8_t s_cnt = 0;
 key_para_t key_para = { 0 };
 clock_time_t clock_time = { 0 };
 lcd_st7789_init();
 lcd_st7789_clear(LCD_DISP_WHITE);
 rt_thread_mdelay(2);
 lcd_st7789_fill_area(10104040, LCD_DISP_BLUE);

 embedded_tim_start_init();

 // clock_time.hour = 11;
 // clock_time.minute = 50;

 // menu_main_window();
 // lcd_menu_key_init();

 lv_init(); // lvgl 系统初始化
 lv_port_disp_init(); // lvgl 显示接口初始化,放在 lv_init()的后面
 lv_port_indev_init(); // lvgl 输入接口初始化,放在 lv_init()的后面
 // lv_example_btn_1();
 // lv_example_led_1();
 // lv_example_calendar_1();
 lv_demo_stress();
 for (;;) {
  //    s_cnt++;
  //    {
  //      if (s_cnt > 2)
  //        s_cnt = 0;
  //      clock_time.second++;
  //    }

  //    if (clock_time.second > 59)
  //    {
  //      clock_time.second = 0;
  //      clock_time.minute++;
  //      if (clock_time.minute > 59)
  //      {
  //        clock_time.minute = 0;
  //        clock_time.hour++;
  //        if (clock_time.hour > 12)
  //        {
  //          clock_time.hour = 1;
  //        }
  //      }
  //    }

  //    lcd_menu_keyvalue_get(&key_para, 20);
  //    lcd_menu_handler(&key_para);
  //    menu_clock_run(&clock_time);

  lv_task_handler();
  rt_thread_mdelay(5);
 }
}

实现效果就不展示了,兄弟们拿起手里的板子,开撸吧~~~

5、经验交流

欢迎关注公众号“小飞哥玩嵌入式”,一起解锁更多的嵌入式开发技能包。

小飞哥玩嵌入式 分享嵌入式开发相关知识,喜欢DIY分享
评论
  • 这篇内容主要讨论三个基本问题,硅电容是什么,为什么要使用硅电容,如何正确使用硅电容?1.  硅电容是什么首先我们需要了解电容是什么?物理学上电容的概念指的是给定电位差下自由电荷的储藏量,记为C,单位是F,指的是容纳电荷的能力,C=εS/d=ε0εrS/4πkd(真空)=Q/U。百度百科上电容器的概念指的是两个相互靠近的导体,中间夹一层不导电的绝缘介质。通过观察电容本身的定义公式中可以看到,在各个变量中比较能够改变的就是εr,S和d,也就是介质的介电常数,金属板有效相对面积以及距离。当前
    知白 2025-01-06 12:04 111浏览
  • 本文介绍Linux系统更换开机logo方法教程,通用RK3566、RK3568、RK3588、RK3576等开发板,触觉智能RK3562开发板演示,搭载4核A53处理器,主频高达2.0GHz;内置独立1Tops算力NPU,可应用于物联网网关、平板电脑、智能家居、教育电子、工业显示与控制等行业。制作图片开机logo图片制作注意事项(1)图片必须为bmp格式;(2)图片大小不能大于4MB;(3)BMP位深最大是32,建议设置为8;(4)图片名称为logo.bmp和logo_kernel.bmp;开机
    Industio_触觉智能 2025-01-06 10:43 72浏览
  • 彼得·德鲁克被誉为“现代管理学之父”,他的管理思想影响了无数企业和管理者。然而,关于他的书籍分类,一种流行的说法令人感到困惑:德鲁克一生写了39本书,其中15本是关于管理的,而其中“专门写工商企业或为企业管理者写的”只有两本——《为成果而管理》和《创新与企业家精神》。这样的表述广为流传,但深入探讨后却发现并不完全准确。让我们一起重新审视这一说法,解析其中的矛盾与根源,进而重新认识德鲁克的管理思想及其著作的真正价值。从《创新与企业家精神》看德鲁克的视角《创新与企业家精神》通常被认为是一本专为企业管
    优思学院 2025-01-06 12:03 76浏览
  •     为控制片内设备并且查询其工作状态,MCU内部总是有一组特殊功能寄存器(SFR,Special Function Register)。    使用Eclipse环境调试MCU程序时,可以利用 Peripheral Registers Viewer来查看SFR。这个小工具是怎样知道某个型号的MCU有怎样的寄存器定义呢?它使用一种描述性的文本文件——SVD文件。这个文件存储在下面红色字体的路径下。    例:南京沁恒  &n
    电子知识打边炉 2025-01-04 20:04 84浏览
  • 每日可见的315MHz和433MHz遥控模块,你能分清楚吗?众所周知,一套遥控设备主要由发射部分和接收部分组成,发射器可以将控制者的控制按键经过编码,调制到射频信号上面,然后经天线发射出无线信号。而接收器是将天线接收到的无线信号进行解码,从而得到与控制按键相对应的信号,然后再去控制相应的设备工作。当前,常见的遥控设备主要分为红外遥控与无线电遥控两大类,其主要区别为所采用的载波频率及其应用场景不一致。红外遥控设备所采用的射频信号频率一般为38kHz,通常应用在电视、投影仪等设备中;而无线电遥控设备
    华普微HOPERF 2025-01-06 15:29 93浏览
  • 光耦合器,也称为光隔离器,是一种利用光在两个隔离电路之间传输电信号的组件。在医疗领域,确保患者安全和设备可靠性至关重要。在众多有助于医疗设备安全性和效率的组件中,光耦合器起着至关重要的作用。这些紧凑型设备经常被忽视,但对于隔离高压和防止敏感医疗设备中的电气危害却是必不可少的。本文深入探讨了光耦合器的功能、其在医疗应用中的重要性以及其实际使用示例。什么是光耦合器?它通常由以下部分组成:LED(发光二极管):将电信号转换为光。光电探测器(例如光电晶体管):检测光并将其转换回电信号。这种布置确保输入和
    腾恩科技-彭工 2025-01-03 16:27 171浏览
  • PLC组态方式主要有三种,每种都有其独特的特点和适用场景。下面来简单说说: 1. 硬件组态   定义:硬件组态指的是选择适合的PLC型号、I/O模块、通信模块等硬件组件,并按照实际需求进行连接和配置。    灵活性:这种方式允许用户根据项目需求自由搭配硬件组件,具有较高的灵活性。    成本:可能需要额外的硬件购买成本,适用于对系统性能和扩展性有较高要求的场合。 2. 软件组态   定义:软件组态主要是通过PLC
    丙丁先生 2025-01-06 09:23 71浏览
  • 在智能家居领域中,Wi-Fi、蓝牙、Zigbee、Thread与Z-Wave等无线通信协议是构建短距物联局域网的关键手段,它们常在实际应用中交叉运用,以满足智能家居生态系统多样化的功能需求。然而,这些协议之间并未遵循统一的互通标准,缺乏直接的互操作性,在进行组网时需要引入额外的网关作为“翻译桥梁”,极大地增加了系统的复杂性。 同时,Apple HomeKit、SamSung SmartThings、Amazon Alexa、Google Home等主流智能家居平台为了提升市占率与消费者
    华普微HOPERF 2025-01-06 17:23 97浏览
  • 自动化已成为现代制造业的基石,而驱动隔离器作为关键组件,在提升效率、精度和可靠性方面起到了不可或缺的作用。随着工业技术不断革新,驱动隔离器正助力自动化生产设备适应新兴趋势,并推动行业未来的发展。本文将探讨自动化的核心趋势及驱动隔离器在其中的重要角色。自动化领域的新兴趋势智能工厂的崛起智能工厂已成为自动化生产的新标杆。通过结合物联网(IoT)、人工智能(AI)和机器学习(ML),智能工厂实现了实时监控和动态决策。驱动隔离器在其中至关重要,它确保了传感器、执行器和控制单元之间的信号完整性,同时提供高
    腾恩科技-彭工 2025-01-03 16:28 166浏览
  • 随着市场需求不断的变化,各行各业对CPU的要求越来越高,特别是近几年流行的 AIOT,为了有更好的用户体验,CPU的算力就要求更高了。今天为大家推荐由米尔基于瑞芯微RK3576处理器推出的MYC-LR3576核心板及开发板。关于RK3576处理器国产CPU,是这些年的骄傲,华为手机全国产化,国人一片呼声,再也不用卡脖子了。RK3576处理器,就是一款由国产是厂商瑞芯微,今年第二季推出的全新通用型的高性能SOC芯片,这款CPU到底有多么的高性能,下面看看它的几个特性:8核心6 TOPS超强算力双千
    米尔电子嵌入式 2025-01-03 17:04 48浏览
  • 根据Global Info Research项目团队最新调研,预计2030年全球封闭式电机产值达到1425百万美元,2024-2030年期间年复合增长率CAGR为3.4%。 封闭式电机是一种电动机,其外壳设计为密闭结构,通常用于要求较高的防护等级的应用场合。封闭式电机可以有效防止外部灰尘、水分和其他污染物进入内部,从而保护电机的内部组件,延长其使用寿命。 环洋市场咨询机构出版的调研分析报告【全球封闭式电机行业总体规模、主要厂商及IPO上市调研报告,2025-2031】研究全球封闭式电机总体规
    GIRtina 2025-01-06 11:10 89浏览
我要评论
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦