tag 标签: spi

相关帖子
相关博文
  • 2025-6-29 20:40
    81 次阅读|
    0 个评论
    14. ESP32开发之ST7789显示
    SPI概述 ESP IDF中SPI LCD的相关API 简单使用LVGL 完整代码 总结 SPI概述 当进入嵌入式行业开始,SPI总线是最需要且基础的知识了,它是高速全双工串行总线,可做到同时收发数据。时序和控制根据各家的芯片或者屏幕等设备的数据手册进行阅读和进行编程,比如总线模式就有四种,通过寄存器CPOL和CPHA配置 模式 CPOL CPHA 数据采样时刻 适用场景 0 0 0 SCK下降沿,第1个跳变沿采样 多数传感器(如BME280) 1 0 1 SCK下降沿,第2个跳变沿采样 部分ADC芯片 2 1 0 SCK上升沿,第1个跳变沿采样 特殊存储器 3 1 1 SCK上升沿,第2个跳变沿采样 某些RF模块 更多的SPI相关基础,可以在网上查询 ESP IDF中SPI LCD相关API 在ESP32开发库ESP IDF中有完整的SPI LCD实现的代码,我们只需要合适的调用相关API函数就可以了,在此案例中用到如下函数和结构体 1. 配置SPI总线 SPI总线结构体 typedef struct { union { int mosi_io_num; //spi的mosi接口,不用赋值-1 int data0_io_num; /// GPIO pin for spi data0 signal in quad/octal mode, or -1 if not used. }; union { int miso_io_num; /// GPIO pin for Master In Slave Out (=spi_q) signal, or -1 if not used. int data1_io_num; /// GPIO pin for spi data1 signal in quad/octal mode, or -1 if not used. }; int sclk_io_num; /// GPIO pin for SPI Clock signal, or -1 if not used. union { int quadwp_io_num; /// GPIO pin for WP (Write Protect) signal, or -1 if not used. int data2_io_num; /// GPIO pin for spi data2 signal in quad/octal mode, or -1 if not used. }; union { int quadhd_io_num; /// GPIO pin for HD (Hold) signal, or -1 if not used. int data3_io_num; /// GPIO pin for spi data3 signal in quad/octal mode, or -1 if not used. }; int data4_io_num; /// GPIO pin for spi data4 signal in octal mode, or -1 if not used. int data5_io_num; /// GPIO pin for spi data5 signal in octal mode, or -1 if not used. int data6_io_num; /// GPIO pin for spi data6 signal in octal mode, or -1 if not used. int data7_io_num; /// GPIO pin for spi data7 signal in octal mode, or -1 if not used. int max_transfer_sz; /// Maximum transfer size, in bytes. Defaults to 4092 if 0 when DMA enabled, or to `SOC_SPI_MAXIMUM_BUFFER_SIZE` if DMA is disabled. uint32_t flags; /// Abilities of bus to be checked by the driver. Or-ed value of ``SPICOMMON_BUSFLAG_*`` flags. esp_intr_cpu_affinity_t isr_cpu_id; /// Select cpu core to register SPI ISR. int intr_flags; /** Interrupt flag for the bus to set the priority, and IRAM attribute, see * ``esp_intr_alloc.h``. Note that the EDGE, INTRDISABLED attribute are ignored * by the driver. Note that if ESP_INTR_FLAG_IRAM is set, ALL the callbacks of * the driver, and their callee functions, should be put in the IRAM. */ } spi_bus_config_t ; 初始化SPI总线函数 esp_err_t spi_bus_initialize ( spi_host_device_t host_id, //使用哪个主机SPI,有SPI0_HOST、SPI1_HOST、SP2_HOST const spi_bus_config_t *bus_config, //使用spi_bus_config_t创建的结构体 spi_dma_chan_t dma_chan //dma通道,设置SPI_DMA_DISABLED则不开启 ) 2. 创建LCD SPI句柄,并与SPI总线相连 LCD SPI面板相关io结构体 typedef struct { int cs_gpio_num; /*! GPIO used for CS line */ int dc_gpio_num; /*! GPIO used to select the D/C line, set this to -1 if the D/C line is not used */ int spi_mode; /*! Traditional SPI mode (0~3) */ unsigned int pclk_hz; /*! Frequency of pixel clock */ size_t trans_queue_depth; /*! Size of internal transaction queue */ esp_lcd_panel_io_color_trans_done_cb_t on_color_trans_done; /*! Callback invoked when color data transfer has finished */ void *user_ctx; /*! User private data, passed directly to on_color_trans_done's user_ctx */ int lcd_cmd_bits; /*! Bit-width of LCD command */ int lcd_param_bits; /*! Bit-width of LCD parameter */ struct { unsigned int dc_low_on_data: 1 ; /*! If this flag is enabled, DC line = 0 means transfer data, DC line = 1 means transfer command; vice versa */ unsigned int octal_mode: 1 ; /*! transmit with octal mode (8 data lines), this mode is used to simulate Intel 8080 timing */ unsigned int quad_mode: 1 ; /*! transmit with quad mode (4 data lines), this mode is useful when transmitting LCD parameters (Only use one line for command) */ unsigned int sio_mode: 1 ; /*! Read and write through a single data line (MOSI) */ unsigned int lsb_first: 1 ; /*! transmit LSB bit first */ unsigned int cs_high_active: 1 ; /*! CS line is high active */ } flags; /*! Extra flags to fine-tune the SPI device */ } esp_lcd_panel_io_spi_config_t ; 创建LCD SPI面板相关io的句柄 esp_err_t esp_lcd_new_panel_io_spi ( esp_lcd_spi_bus_handle_t bus, //SPI总线 const esp_lcd_panel_io_spi_config_t *io_config, //填充完成的esp_lcd_panel_io_spi_config_t结构体变量 esp_lcd_panel_io_handle_t *ret_io //返回的LCD SPI面板句柄 ) //在使用这个函数之前需先创建个esp_lcd_panel_io_spi_config_t结构体变量,比如esp_lcd_panel_io_handle_t io_handle = NULL; LCD SPI面板驱动配置结构体 typedef struct { int reset_gpio_num; /*! GPIO used to reset the LCD panel, set to -1 if it's not used */ union { lcd_rgb_element_order_t color_space; /*! @deprecated Set RGB color space, please use rgb_ele_order instead */ lcd_rgb_element_order_t rgb_endian; /*! @deprecated Set RGB data endian, please use rgb_ele_order instead */ lcd_rgb_element_order_t rgb_ele_order; /*! Set RGB element order, RGB or BGR */ }; lcd_rgb_data_endian_t data_endian; /*! Set the data endian for color data larger than 1 byte */ unsigned int bits_per_pixel; /*! Color depth, in bpp */ struct { unsigned int reset_active_high: 1 ; /*! Setting this if the panel reset is high level active */ } flags; /*! LCD panel config flags */ void *vendor_config; /*! vendor specific configuration, optional, left as NULL if not used */ } esp_lcd_panel_dev_config_t ; 创建 LCD 面板句柄,并指定 SPI IO 设备句柄 以下函数是ESP IDF套件中已经实现的st7789相关函数,其内部还有很多适配的LCD,可以在如下路径找到 esp-idf/components/esp_lcd/src //其实就是将实现的st7789相关函数注册到LCD面板这个大的虚拟对象中,这是C语言的面向对象编程 esp_err_t esp_lcd_new_panel_st7789 ( const esp_lcd_panel_io_handle_t io, //创建过的lcd面板io句柄 const esp_lcd_panel_dev_config_t *panel_dev_config, //创建过的LCD面板驱动句柄 esp_lcd_panel_handle_t *ret_panel //返回一个LCD面板句柄 ) 3. 通过以上一步步完善结构体,和创建的LCD面板句柄,我们将可以调用关于LCD 面板的所有API函数,比如: //控制LCD复位 esp_err_t esp_lcd_panel_reset ( esp_lcd_panel_handle_t panel) //初始化LCD esp_err_t esp_lcd_panel_init ( esp_lcd_panel_handle_t panel) //开关屏幕 esp_err_t esp_lcd_panel_disp_on_off ( esp_lcd_panel_handle_t panel, bool on_off) //控制屏幕是否反色,白天模式暗黑模式的控制 esp_err_t esp_lcd_panel_invert_color ( esp_lcd_panel_handle_t panel, bool invert_color_data) 使用LVGL 1. 将LVGL的库加入到项目中 ESP IDF开发套件有完善的组件管理方式,在UBUNTU中我们可以如下操作 在项目根目录下使用如下命令,将会在main文件夹下生成一个idf_component.yml的文件 idf.py create-manifest 增加参数: --path:显示的指定在什么目录下创建组件清单文件 --component:在componets目录下,为相关组件创建(此参数所赋值)清单,比如--component=my_components 使用如下命令重新配置项目 idf.py reconfigure 这样就后面每次在构建项目时,则会跟踪上一步生成的idf_component.yml文件 编辑idf_component.yml ## IDF Component Manager Manifest File dependencies: ## Required IDF version idf: version: "=4.1.0" lvgl/lvgl: "~9.2.0" //这是我需要添加的LVGL库,版本时9.2.0 更多操作可以参考如下链接; https://docs.espressif.com/projects/esp-idf/zh_CN/release-v5.2/esp32s3/api-guides/tools/idf-component-manager.html 2. 使用LVGL在屏幕上显示"hello world" 初始化lvgl lv_init(); 创建lvgl显示器 lv_display_t * lv_display_create ( int32_t hor_res, int32_t ver_res) //参数为实际屏幕的分辨率 申请绘制使用的buffer void lv_display_set_buffers ( lv_display_t * disp, //上一步创建的显示器 void * buf1, //缓存1 void * buf2, //缓存2 uint32_t buf_size, //缓存大小 lv_display_render_mode_t render_mode //绘制图像的模式 ) 将LVGL与上面创建的LCD面板进行连接 void lv_display_set_user_data ( lv_display_t * disp, void * user_data) 设置显示的颜色格式 void lv_display_set_color_format ( lv_display_t * disp, lv_color_format_t color_format) 设置显示的方向 void lv_display_set_rotation ( lv_display_t * disp, lv_display_rotation_t rotation) 设置渲染回调函数 void lv_display_set_flush_cb ( lv_display_t * disp, lv_display_flush_cb_t flush_cb) //回调函数如果设置为NULL,则使用默认函数 为LVGL创建定时器 static void example_increase_lvgl_tick ( void *arg) { /* Tell LVGL how many milliseconds has elapsed */ lv_tick_inc ( 2 ); //这里是2ms,建议跟定时器定时时间同步 } const esp_timer_create_args_t lvgl_tick_timer_args = { .callback = example_increase_lvgl_tick, //定时事件,定时通知lvgl已经多久了 .name = "lvgl_tick" }; esp_timer_handle_t lvgl_tick_timer = NULL ; ESP_ERROR_CHECK ( esp_timer_create (lvgl_tick_timer_args, lvgl_tick_timer)); ESP_ERROR_CHECK ( esp_timer_start_periodic (lvgl_tick_timer, 2 * 1000 )); //2ms触发一次事件 注册一事件回调函数,当刷新就绪 const esp_lcd_panel_io_callbacks_t cbs = { .on_color_trans_done = example_notify_lvgl_flush_ready, }; /* Register done callback */ ESP_ERROR_CHECK ( esp_lcd_panel_io_register_event_callbacks (io_handle, cbs, display)); //回调函数中调用lvgl的刷新就绪函数,通知LVGL就绪状态,让lvgl进行下一步刷新 LV_ATTRIBUTE_FLUSH_READYvoid lv_display_flush_ready(lv_display_t * disp) 为lvgl创建任务 //通过lv_timer_handler(void)持续更新lvgl,这是LVGL的心脏 static void example_lvgl_port_task ( void *arg) { ESP_LOGI (TAG, "Starting LVGL task" ); uint32_t time_till_next_ms = 0 ; while ( 1 ) { _lock_acquire(lvgl_api_lock); time_till_next_ms = lv_timer_handler (); _lock_release(lvgl_api_lock); // in case of triggering a task watch dog time out time_till_next_ms = MAX (time_till_next_ms, EXAMPLE_LVGL_TASK_MIN_DELAY_MS); // in case of lvgl display not ready yet time_till_next_ms = MIN (time_till_next_ms, EXAMPLE_LVGL_TASK_MAX_DELAY_MS); usleep ( 1000 * time_till_next_ms); } } xTaskCreatePinnedToCore (example_lvgl_port_task, "LVGL" , EXAMPLE_LVGL_TASK_STACK_SIZE, NULL , EXAMPLE_LVGL_TASK_PRIORITY, NULL , 1 ); 绘制显示内容,如下绘制“hello world" lv_obj_t *scr = lv_display_get_screen_active (disp); lv_obj_t *label1= lv_label_create (scr); lv_label_set_text (label1, "Hello\nworld" ); lv_obj_align (label1, LV_ALIGN_CENTER, 0 , 0 ); 完整代码 /** * Copyright (C) 2024-2034 HalfMoon2. * All rights reserved. * * @file Filename without the absolute path * @brief Brief description * @author HalfMoon2 * @date 2025-06-23 * @version v0.1 * * @revision history: * 2025-06-23 - Initial version. */ # include stdio.h # include unistd.h # include sys/lock.h # include sys/param.h # include freertos/FreeRTOS.h # include freertos/task.h # include esp_log.h # include driver/gpio.h # include driver/spi_master.h # include "driver/spi_common.h" # include "esp_timer.h" # include "esp_lcd_panel_io.h" # include "esp_lcd_panel_vendor.h" # include "esp_lcd_panel_ops.h" # include "esp_lcd_panel_commands.h" # include "esp_dma_utils.h" # include "lvgl.h" # include "esp_task_wdt.h" static const char *TAG = "ST7789" ; //LCD使用的SPI接口 # define LCD_SPI SPI2_HOST //SPI相关引脚定义 # define SPI_LCD_MOSI GPIO_NUM_12 # define SPI_LCD_MISO GPIO_NUM_18 # define SPI_LCD_SCLK GPIO_NUM_11 # define SPI_LCD_CS GPIO_NUM_15 # define SPI_LCD_DC GPIO_NUM_17 # define SPI_LCD_RST GPIO_NUM_21 # define SPI_LCD_BL GPIO_NUM_26 //LCD显示屏的大小 # define LCD_SIZE_WIDTH 240 # define LCD_SIZE_HIGHT 240 # define EXAMPLE_LVGL_TASK_MIN_DELAY_MS 1000/CONFIG_FREERTOS_HZ # define EXAMPLE_LVGL_TASK_MAX_DELAY_MS 500 # define EXAMPLE_LVGL_TASK_STACK_SIZE (4 * 1024) # define EXAMPLE_LVGL_TASK_PRIORITY 2 // LVGL library is not thread-safe, this example will call LVGL APIs from different tasks, so use a mutex to protect it static _lock_t lvgl_api_lock; static lv_obj_t * btn; static esp_lcd_panel_io_handle_t io_handle = NULL ; static void set_angle ( void * obj, int32_t v) { ESP_LOGI (TAG, "Starting set_angle" ); lv_arc_set_value (obj, v); } static void anim_x_cb ( void *var, int32_t v) { lv_obj_set_x (var, v); } void example_lvgl_demo_ui ( lv_display_t *disp) { lv_obj_t * scr= lv_display_get_screen_active (disp); lv_obj_t *label1= lv_label_create (scr); lv_label_set_text (label1, "Hello\nworld" ); lv_obj_align (label1, LV_ALIGN_CENTER, 0 , 0 ); # if 1 btn = lv_button_create (scr); lv_obj_t * lbl = lv_label_create (btn); lv_label_set_text_static (lbl, LV_SYMBOL_REFRESH " ROTATE" ); lv_obj_align (btn, LV_ALIGN_BOTTOM_MID, 0 , 0 ); /*Button event*/ // lv_obj_add_event_cb(btn, btn_cb, LV_EVENT_CLICKED, disp); # endif /*Create an Arc*/ lv_obj_t * arc = lv_arc_create (scr); lv_arc_set_rotation (arc, 270 ); lv_arc_set_bg_angles (arc, 0 , 360 ); lv_obj_remove_style (arc, NULL , LV_PART_KNOB); /*Be sure the knob is not displayed*/ lv_obj_remove_flag (arc, LV_OBJ_FLAG_CLICKABLE); /*To not allow adjusting by click*/ lv_obj_center (arc); # if 1 lv_anim_t a; lv_anim_init (a); lv_anim_set_var (a, arc); lv_anim_set_exec_cb (a, set_angle); lv_anim_set_duration (a, 1000 ); lv_anim_set_repeat_count (a, LV_ANIM_REPEAT_INFINITE); /*Just for the demo*/ lv_anim_set_repeat_delay (a, 500 ); lv_anim_set_values (a, 0 , 100 ); lv_anim_start (a); # endif } static bool example_notify_lvgl_flush_ready ( esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_io_event_data_t *edata, void *user_ctx) { lv_display_t *disp = ( lv_display_t *)user_ctx; lv_display_flush_ready (disp); return false ; } static void example_lvgl_port_task ( void *arg) { ESP_LOGI (TAG, "Starting LVGL task" ); uint32_t time_till_next_ms = 0 ; while ( 1 ) { _lock_acquire(lvgl_api_lock); time_till_next_ms = lv_timer_handler (); _lock_release(lvgl_api_lock); // in case of triggering a task watch dog time out time_till_next_ms = MAX (time_till_next_ms, EXAMPLE_LVGL_TASK_MIN_DELAY_MS); // in case of lvgl display not ready yet time_till_next_ms = MIN (time_till_next_ms, EXAMPLE_LVGL_TASK_MAX_DELAY_MS); usleep ( 1000 * time_till_next_ms); } } /* Rotate display and touch, when rotated screen in LVGL. Called when driver parameters are updated. */ static void example_lvgl_port_update_callback ( lv_display_t *disp) { esp_lcd_panel_handle_t panel_handle = lv_display_get_user_data (disp); lv_display_rotation_t rotation = lv_display_get_rotation (disp); switch (rotation) { case LV_DISPLAY_ROTATION_0: // Rotate LCD display esp_lcd_panel_swap_xy (panel_handle, false ); esp_lcd_panel_mirror (panel_handle, false , false ); break ; case LV_DISPLAY_ROTATION_90: // Rotate LCD display esp_lcd_panel_swap_xy (panel_handle, true ); esp_lcd_panel_mirror (panel_handle, true , true ); break ; case LV_DISPLAY_ROTATION_180: // Rotate LCD display esp_lcd_panel_swap_xy (panel_handle, false ); esp_lcd_panel_mirror (panel_handle, true , true ); break ; case LV_DISPLAY_ROTATION_270: // Rotate LCD display esp_lcd_panel_swap_xy (panel_handle, true ); esp_lcd_panel_mirror (panel_handle, true , false ); break ; } } static void example_increase_lvgl_tick ( void *arg) { /* Tell LVGL how many milliseconds has elapsed */ lv_tick_inc ( 2 ); } static void example_lvgl_flush_cb ( lv_display_t *disp, const lv_area_t *area, uint8_t *px_map) { example_lvgl_port_update_callback (disp); esp_lcd_panel_handle_t panel_handle = lv_display_get_user_data (disp); int offsetx1 = area-x1; int offsetx2 = area-x2; int offsety1 = area-y1; int offsety2 = area-y2; // because SPI LCD is big-endian, we need to swap the RGB bytes order lv_draw_sw_rgb565_swap (px_map, (offsetx2 + 1 - offsetx1) * (offsety2 + 1 - offsety1)); // copy a buffer's content to a specific area of the display esp_lcd_panel_draw_bitmap (panel_handle, offsetx1, offsety1, offsetx2 + 1 , offsety2 + 1 , px_map); } static esp_lcd_panel_handle_t st7789_init ( void ) { //spi bus的相关配置 spi_bus_config_t busconfig={ // .flags=SPICOMMON_BUSFLAG_MASTER, //设置SPI为主机模式 .sclk_io_num=SPI_LCD_SCLK, .miso_io_num=SPI_LCD_MISO, .mosi_io_num=SPI_LCD_MOSI, .quadwp_io_num = -1 , .quadhd_io_num = -1 , .max_transfer_sz=LCD_SIZE_HIGHT* 20 * sizeof ( uint16_t ) //单次最多可传输80行像素 }; ESP_ERROR_CHECK ( spi_bus_initialize (LCD_SPI, busconfig, SPI_DMA_CH_AUTO)); // 启用 DMA //创建SPI LCD句柄 esp_lcd_panel_io_spi_config_t io_config = { .dc_gpio_num = SPI_LCD_DC, .cs_gpio_num = SPI_LCD_CS, .pclk_hz = 40 * 1000 * 1000 , .lcd_cmd_bits = 8 , .lcd_param_bits = 8 , .spi_mode = 0 , .trans_queue_depth = 10 }; // 将 LCD 连接到 SPI 总线 ESP_ERROR_CHECK ( esp_lcd_new_panel_io_spi (( esp_lcd_spi_bus_handle_t )LCD_SPI, io_config, io_handle)); esp_lcd_panel_handle_t panel_handle = NULL ; esp_lcd_panel_dev_config_t panel_config = { .reset_gpio_num = SPI_LCD_RST, .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_BGR, .bits_per_pixel = 16 , }; // 为 ST7789 创建 LCD 面板句柄,并指定 SPI IO 设备句柄 ESP_ERROR_CHECK ( esp_lcd_new_panel_st7789 (io_handle, panel_config, panel_handle)); ESP_ERROR_CHECK ( esp_lcd_panel_reset (panel_handle)); ESP_ERROR_CHECK ( esp_lcd_panel_init (panel_handle)); ESP_ERROR_CHECK ( esp_lcd_panel_disp_on_off (panel_handle, true )); esp_lcd_panel_invert_color (panel_handle, true ); ESP_LOGI (TAG, "Initialize LVGL library" ); return panel_handle; } void app_main ( void ) { esp_lcd_panel_handle_t panel_handle= st7789_init (); lv_init (); // create a lvgl display lv_display_t *display = lv_display_create ( 240 , 240 ); // alloc draw buffers used by LVGL // it's recommended to choose the size of the draw buffer(s) to be at least 1/10 screen sized size_t draw_buffer_sz = LCD_SIZE_WIDTH * 24 * sizeof ( lv_color16_t ); void *buf1 = heap_caps_malloc (draw_buffer_sz, MALLOC_CAP_DMA); assert (buf1); void *buf2 = heap_caps_malloc (draw_buffer_sz, MALLOC_CAP_DMA); assert (buf2); // initialize LVGL draw buffers lv_display_set_buffers (display, buf1, buf2, draw_buffer_sz, LV_DISPLAY_RENDER_MODE_PARTIAL); // associate the mipi panel handle to the display lv_display_set_user_data (display, panel_handle); // set color depth lv_display_set_color_format (display, LV_COLOR_FORMAT_RGB565); // set the callback which can copy the rendered image to an area of the display lv_display_set_rotation (display,LV_DISPLAY_ROTATION_0); lv_display_set_flush_cb (display, example_lvgl_flush_cb); ESP_LOGI (TAG, "Install LVGL tick timer" ); // Tick interface for LVGL (using esp_timer to generate 2ms periodic event) const esp_timer_create_args_t lvgl_tick_timer_args = { .callback = example_increase_lvgl_tick, .name = "lvgl_tick" }; esp_timer_handle_t lvgl_tick_timer = NULL ; ESP_ERROR_CHECK ( esp_timer_create (lvgl_tick_timer_args, lvgl_tick_timer)); ESP_ERROR_CHECK ( esp_timer_start_periodic (lvgl_tick_timer, 2 * 1000 )); ESP_LOGI (TAG, "Register io panel event callback for LVGL flush ready notification" ); const esp_lcd_panel_io_callbacks_t cbs = { .on_color_trans_done = example_notify_lvgl_flush_ready, }; /* Register done callback */ ESP_ERROR_CHECK ( esp_lcd_panel_io_register_event_callbacks (io_handle, cbs, display)); ESP_LOGI (TAG, "Create LVGL task" ); xTaskCreatePinnedToCore (example_lvgl_port_task, "LVGL" , EXAMPLE_LVGL_TASK_STACK_SIZE, NULL , EXAMPLE_LVGL_TASK_PRIORITY, NULL , 1 ); ESP_LOGI (TAG, "Display LVGL Meter Widget" ); // Lock the mutex due to the LVGL APIs are not thread-safe _lock_acquire(lvgl_api_lock); example_lvgl_demo_ui (display); _lock_release(lvgl_api_lock); } 显示结果: 总结 一定要注意以下几点: 缓存区大小的设定,如果设置不正确将会出现如下情况,偏移也会如此,所以碰到这个情况可以想想会不会是缓存区的问题 显示镜像,根据不同的实现方式,要仔细调试 //这里只是设置了标志 lv_display_set_rotation (display,LV_DISPLAY_ROTATION_0); //要通过如下函数进行具体实现 static void example_lvgl_port_update_callback ( lv_display_t *disp) { esp_lcd_panel_handle_t panel_handle = lv_display_get_user_data (disp); lv_display_rotation_t rotation = lv_display_get_rotation (disp); switch (rotation) { case LV_DISPLAY_ROTATION_0: // Rotate LCD display esp_lcd_panel_swap_xy (panel_handle, false ); esp_lcd_panel_mirror (panel_handle, false , false ); break ; case LV_DISPLAY_ROTATION_90: // Rotate LCD display esp_lcd_panel_swap_xy (panel_handle, true ); esp_lcd_panel_mirror (panel_handle, true , true ); break ; case LV_DISPLAY_ROTATION_180: // Rotate LCD display esp_lcd_panel_swap_xy (panel_handle, false ); esp_lcd_panel_mirror (panel_handle, true , true ); break ; case LV_DISPLAY_ROTATION_270: // Rotate LCD display esp_lcd_panel_swap_xy (panel_handle, true ); esp_lcd_panel_mirror (panel_handle, true , false ); break ; } } static void example_lvgl_flush_cb ( lv_display_t *disp, const lv_area_t *area, uint8_t *px_map) { example_lvgl_port_update_callback (disp); esp_lcd_panel_handle_t panel_handle = lv_display_get_user_data (disp); int offsetx1 = area-x1; int offsetx2 = area-x2; int offsety1 = area-y1; int offsety2 = area-y2; // because SPI LCD is big-endian, we need to swap the RGB bytes order lv_draw_sw_rgb565_swap (px_map, (offsetx2 + 1 - offsetx1) * (offsety2 + 1 - offsety1)); // copy a buffer's content to a specific area of the display esp_lcd_panel_draw_bitmap (panel_handle, offsetx1, offsety1, offsetx2 + 1 , offsety2 + 1 , px_map); } 如下情况,就需要改example_lvgl_port_update_callback函数里的具体实现 esp idf开发套件里有大量的案例,完全可以参考相关代码进行设计,当然从零搭建也是可以的,如果时间充裕的话 lvgl的相关配置,可以使用idf.py menuconfig进行更改 一定要在刷新就绪后,使用lv_display_flush_ready(lv_display_t * disp)函数通知lvgl,否则lvgl无法进行下一步刷新
  • 热度 6
    2024-9-15 07:13
    691 次阅读|
    0 个评论
    SPI、I2C和I3C是三种常见的串行通信协议,广泛应用于嵌入式系统和微控制器之间的数据交换。每种协议都有其独特的特点和适用场景,以下是对这三种协议的性能比较: 1. SPI (Serial Peripheral Interface) 速度:SPI是一种高速的同步串行通信协议,通常比I2C更快,因为它没有起始位和停止位的概念,数据传输是全双工的(即可以同时发送和接收数据)。 引脚数:SPI需要至少4个引脚(MOSI, MISO, SCLK, SS/CS),这可能在某些应用中是一个限制因素。 灵活性:由于SPI是全双工通信,它可以在主机和从机之间实现真正的并行数据传输,这对于需要高吞吐量的应用非常有用。 配置:SPI支持多种模式,包括时钟极性和相位,这提供了额外的灵活性。 2. I2C (Inter-Integrated Circuit) 速度:I2C的速度通常低于SPI,但它足以满足许多低至中等速度的应用需求。 引脚数:I2C只需要两根线(SDA和SCL),这使得它非常适合于空间受限的设计。 可扩展性:I2C支持多主设备和多从设备,允许在同一总线上连接多个设备,这是其最大的优势之一。 地址分配:每个I2C设备都有一个唯一的地址,这使得总线上的设备可以轻松地被识别和寻址。 3. I3C (Improved Inter-Integrated Circuit) 速度:I3C是I2C的改进版,旨在提供更高的速度和更低的功耗。它通过引入新的功能来提高性能,如动态地址分配和更高效的电源管理。 兼容性:I3C保持了与I2C的向后兼容性,这意味着现有的I2C设备可以直接与I3C总线交互,无需修改。 引脚数:I3C通常使用与I2C相同的两根线,但在需要时可以通过增加额外的信号线来实现更多的功能。 新特性:I3C引入了一些新特性,如快速模式切换和增强的错误检测机制,这些特性使其在某些应用中比传统的I2C更具吸引力。 总结来说,SPI适合需要高速数据传输且不介意多引脚的应用;I2C适用于需要多设备通信且空间受限的设计;而I3C则结合了两者的优点,提供了更高的速度和新的功能,同时保持了与I2C的兼容性。选择哪种协议取决于具体的应用需求和设计约束。
  • 热度 9
    2024-3-15 11:22
    974 次阅读|
    0 个评论
    SPI(Serial Peripheral Interface)通信总线以其高速、全双工、同步的特性而被广泛应用,它只需要四根线就能实现数据传输,有效地节约了芯片管脚的数量,同时为PCB布局带来了空间上的优化和便捷。正因为它简单易用的特点,现在越来越多的芯片选择集成SPI通信协议。 作为TI Sitara™产品线新一代MPU产品,TI AM62x处理器特别配备了多达4路的SPI接口以及1路OSPI接口(有时也称为QSPI),丰富的SPI接口配置使得该处理器能够同时与多个设备进行通信连接,大大提高了系统的扩展性和灵活性。 在飞凌嵌入式推出的搭载了AM62x处理器的OK6254-C开发板上,NOR Flash存储器正是通过连接到处理器的OSPI总线上进行工作的。这样的设计充分利用了SPI通信协议的高速传输和全双工特性,保证了开发板在数据处理和存储方面的效率。本文就通过OK6254-C开发板为大家介绍一下AM62x的SPI。 1、SPI的工作流程和时序 我们首先需要了解一下SPI是如何工作的——通常SPI通过4个引脚与外部器件相连: (1) MISO:主设备输入/从设备输出引脚 该引脚在从模式下发送数据,在主模式下接收数据; (2) MOSI:主设备输出/从设备输入引脚 该引脚在主模式下发送数据,在从模式下接收数据; (3) CLK:串口时钟 作为主设备的输出,从设备的输入; (4) NSS:从设备选择 这是一个可选的引脚,用来选择从设备。 SPI的工作流程是这样的: (1) 主机先将NSS信号拉低,这样保证开始接收数据; (2) 当接收端检测到时钟的边沿信号时,将立即读取数据线上的信号,这样就得到了一位数据; (3) 由于时钟是随数据一起发送的,因此指定数据的传输速度并不重要,尽管设备将具有可以运行的最高速度; (4) 主机发送到从机时,主机产生相应的时钟信号,然后数据一位一位地将从MOSI信号线上进行发送到从机; (5) 主机接收从机数据时,如果从机需要将数据发送回主机,则主机将继续生成预定数量的时钟信号,并且从机会将数据通过MISO信号线发送。 SPI工作时序图如下: 2、AM62x处理器中SPI总线的特点 在AM62x这款芯片中,TI将SPI的MISO与MOSI设计为d0和d1,具体哪一个作为输入,哪一个作为输出,是由设备树中的 ti,pindir-d0-out-d1-in= 来设置的。 默认属性值为0,即d0是输入,d1是输出; 当属性值为1时,d0为输出,d1为输入。 3、AM62x的SPI应用 (1)menuconfig配置: 将该项选中,SPI驱动将编译进内核中。 makemenuconfi Usermode SPI device driver support (注:在飞凌嵌入式OK6254-C开发板中,SPI驱动编译已进去。) (2)设备树配置: 1) 选择需要使用的spi,这里我们用spi0,节点为&main_spi0; 2) 将该节点所用的引脚复用为相应的功能。 spi0_pins_default: spi0-pins-default { pinctrl-single,pins = < AM62X_IOPAD(0x1B4, PIN_OUTPUT, 0) /* (A13) SPI0_CS0 */ AM62X_IOPAD(0x1B8, PIN_OUTPUT, 0) /* (C13) SPI0_CS1 */ AM62X_IOPAD(0x1C0, PIN_INPUT, 0) /* (B13) SPI0_D0 */ AM62X_IOPAD(0x1BC, PIN_OUTPUT, 0) /* (A14) SPI0_CLK */ AM62X_IOPAD(0x1C4, PIN_INPUT, 0) /* (B14) SPI0_D1 */ ; }; 3) 描述节点的属性,具体配置项的功能见注释。 &main_spi0 { status = "okay"; pinctrl-names = "default"; pinctrl-0 = ; /* 描述引脚复用节点 */ ti,spi-num-cs = ; /* 描述片选的数量 */ ti,pindir-d0-out-d1-in = ; /* 描述输入输出分别是哪个 */ /* 描述子节点spidev0设备 */ spidev@0 { spi-max-frequency = ; /* 描述spidev0设备的最大频率 */ reg = ; /* 描述spidev0设备所用的片选,这里是第0个 */ spi-cs-high; /* 描述spidev0设备高有效 */ compatible = "rohm,dh2228fv"; /* 描述spidev0设备所用驱动 */ }; /* 描述子节点spidev1设备 */ spidev@1 { spi-max-frequency = ; reg = ; compatible = "rohm,dh2228fv"; }; }; (3)编译烧录: 在源码路径下输入以下命令: . build.sh sudo ./build.sh kernel 没有报错即为编译成功。 将源码路径下的image中的OK6254-C.dtb文件放到开发板的/boot目录中,重启开发板即可。 (4)SPI测试: 将spi0_D0和spi0_D1短接 重启开发板后,在/dev目录下看到多出两个spidev设备。 使用我们的测试程序 fltest_spidev_test -D /dev/spidev3.0 -s 42000 有如下打印信息即为成功: spimode: 0 bitsper word: 8 maxspeed: 42000 Hz (42 KHz) FFFF FF FF FF FF 4000 00 00 00 95 FFFF FF FF FF FF FFFF FF FF FF FF FFFF FF FF FF FF DEAD BE EF BA AD F00D 4、总结 TI AM62x处理器有着丰富的SPI资源,而SPI又可以作为许多设备的总线,这使得AM62x能够接入许多SPI接口的设备,因此在那些对SPI有着比较多需求的应用场景下,TI AM62x无疑是一个非常好的主控选择。
  • 热度 8
    2024-1-19 15:25
    847 次阅读|
    0 个评论
    对于做快速存储采集数据类产品的用户来说,在处理突发掉电情况时需要保存现有数据并避免数据丢失,这种情况下有很多种解决方案,铁电存储器(FRAM) 就是个很好的选择。FRAM是一种具有快速写入速度的非易失性存储器,既可以进行非易失性数据存储,又可以像RAM一样操作。 本文将借助飞凌嵌入式OK3568-C开发板来为大家介绍一种采用FRAM的方案—— 使用SPI0挂载PB85RS2MC (FRAM) 芯片 。本文所描述的驱动文件和应用文件,可联系飞凌嵌入式的技术支持获取。 修改思路—— 我们要添加一个SPI设备,需要进行如下操作:在设备树中添加描述→设备树描述中对应设备驱动→设备驱动添加到内核。 修改结果—— 修改 OK3568-linux-source/kernel/arch/arm64/boot/dts/rockchip/OK3568-C-common.dtsi 修改如下: 接下来笔者为大家介绍一下适配的过程。 1 驱动程序 我们在menuconfig中搜索fm25、pb85等比较常用的FRAM字眼,发现并没有类似的驱动程序,这时就需要手写或者移植一个驱动程序。在搜遍各大网站之后找到了一个W25Q64的驱动,对比了一下PB85RS2MC的各种操作码以后,发现两者的操作码大差不差,因此就决定将W25Q64的驱动移植过来。根据PB85RS2MC芯片手册中叙述,各种操作码为: 因此,在驱动程序中宏定义以下操作码,以便在接下来的驱动程序中使用: 首先要在驱动程序中进行初始化函数和退出函数,也就是spidev_init和spidev_exit,init函数里就是进行字符设备的初始化,注册等的操作,exit函数就是要把我们注册的东西在退出时都释放掉,再者就是驱动程序和设备树匹配,匹配是通过compatible属性值匹配的,这里我们一定要和设备树中的compatible属性值一致,否则会匹配不成功。驱动程序中.compatible值如下图所示。 驱动和设备树匹配成功以后就要执行probe函数,这里probe函数执行了一些初始化和注册主次设备号的操作。我们可以通过是否打印 spi_probe success! 来判断驱动程序,是否和设备树匹配成功。 从PB85RS2MC芯片手册中看读的条件,读取FRAM存储单元的数据,需要READ的操作码,任意24位地址输入到SI。第一个spi_transfer 结构体用于发送命令cmd 到SPI设备,在读取数据之前准备设备,第二个是发送地址到SPI设备,第三个是接收从设备读取的数据。 这段代码实现了从 SPI设备中同步读取数据,并将数据复制到用户空间的功能。 从PB85RS2MC芯片手册中看写的条件,WREN命令用于设置写使能锁存器。需要在写操作(WRITE命令) 之前使用WREN命令设置写使能锁存器,WRITE命令将数据写入 FRAM存储单元阵列。WRITE操作码、任意24位地址和8位写入数据输入到 SI。 下面这段代码实现了向SPI设备发送写入使能命令的功能。 这段代码实现了向SPI设备同步写入数据的功能。它先发送写入使能命令,然后发送地址信息和数据。 下面这段代码向 SPI设备中同步写入数据的功能,将用户空间的数据复制到设备的发送缓冲区,并调用spidev_sync_write 函数将数据写入SPI 设备。 2 应用程序 向SPI设备写数据,调用lseek来改变写入数据的位置,这样就可以在整个SPI设备里写数据,如./writeframAPP /dev/pb85rs 0 forlinx ( 0 是要写入的地址, forlinx 是要写入的内容)。 从SPI设备读数据,调用lseek来改变读取数据的位置,这样就可以在整个SPI设备里读数据,如./readframAPP /dev/pb85rs 0 ( 0 是要读取数据的地址) 3 实际测试 (1)首先将fram.ko, readframAPP, writefram APP拷贝到OK3568-C开发板的任意文件夹中。 (2)用insmod加载fram.ko模块,出现 spi_probe success! 说明驱动和设备树匹配成功。 (3)在/dev/下看有没有 pb85rs 设备。 (4)使用./writeframAPP /dev/pb85rs 1500 forlinx 向设备写数据, 1500 是地址, forlinx 是要写入的内容。 (5)使用./readframAPP /dev/pb85rs 1500 ( 1500 是要读取数据的地址) 4 断电测试 断电一天后重新读取PB85RS2MC的数据,发现数据仍然存在。验证了PB85RS2MC铁电存储芯片的断电数据保留特性。 至此,我们就完成了在OK3568-C开发板上添加一个新的SPI铁电存储芯片的操作!
  • 热度 4
    2023-9-22 14:31
    1343 次阅读|
    0 个评论
    对于做快速存储采集数据类产品的用户来说,在处理突发掉电情况时需要保存现有数据并避免数据丢失,这种情况下有很多种解决方案,铁电存储器(FRAM) 就是个很好的选择。FRAM是一种具有快速写入速度的非易失性存储器,既可以进行非易失性数据存储,又可以像RAM一样操作。 本文将借助飞凌嵌入式OK3568-C开发板来为大家介绍一种采用FRAM的方案—— 使用SPI0挂载PB85RS2MC (FRAM) 芯片 。本文所描述的驱动文件和应用文件,可联系飞凌嵌入式的技术支持获取。 修改思路—— 我们要添加一个SPI设备,需要进行如下操作:在设备树中添加描述→设备树描述中对应设备驱动→设备驱动添加到内核。 修改结果—— 修改 OK3568-linux-source/kernel/arch/arm64/boot/dts/rockchip/OK3568-C-common.dtsi 修改如下: 接下来笔者为大家介绍一下适配的过程。 1 驱动程序 我们在menuconfig中搜索fm25、pb85等比较常用的FRAM字眼,发现并没有类似的驱动程序,这时就需要手写或者移植一个驱动程序。在搜遍各大网站之后找到了一个W25Q64的驱动,对比了一下PB85RS2MC的各种操作码以后,发现两者的操作码大差不差,因此就决定将W25Q64的驱动移植过来。根据PB85RS2MC芯片手册中叙述,各种操作码为: 因此,在驱动程序中宏定义以下操作码,以便在接下来的驱动程序中使用: 首先要在驱动程序中进行初始化函数和退出函数,也就是spidev_init和spidev_exit,init函数里就是进行字符设备的初始化,注册等的操作,exit函数就是要把我们注册的东西在退出时都释放掉,再者就是驱动程序和设备树匹配,匹配是通过compatible属性值匹配的,这里我们一定要和设备树中的compatible属性值一致,否则会匹配不成功。驱动程序中.compatible值如下图所示。 驱动和设备树匹配成功以后就要执行probe函数,这里probe函数执行了一些初始化和注册主次设备号的操作。我们可以通过是否打印 spi_probe success! 来判断驱动程序,是否和设备树匹配成功。 从PB85RS2MC芯片手册中看读的条件,读取FRAM存储单元的数据,需要READ的操作码,任意24位地址输入到SI。第一个spi_transfer 结构体用于发送命令cmd 到SPI设备,在读取数据之前准备设备,第二个是发送地址到SPI设备,第三个是接收从设备读取的数据。 这段代码实现了从 SPI设备中同步读取数据,并将数据复制到用户空间的功能。 从PB85RS2MC芯片手册中看写的条件,WREN命令用于设置写使能锁存器。需要在写操作(WRITE命令) 之前使用WREN命令设置写使能锁存器,WRITE命令将数据写入 FRAM存储单元阵列。WRITE操作码、任意24位地址和8位写入数据输入到 SI。 下面这段代码实现了向SPI设备发送写入使能命令的功能。 这段代码实现了向SPI设备同步写入数据的功能。它先发送写入使能命令,然后发送地址信息和数据。 下面这段代码向 SPI设备中同步写入数据的功能,将用户空间的数据复制到设备的发送缓冲区,并调用spidev_sync_write 函数将数据写入SPI 设备。 2 应用程序 向SPI设备写数据,调用lseek来改变写入数据的位置,这样就可以在整个SPI设备里写数据,如./writeframAPP /dev/pb85rs 0 forlinx ( 0 是要写入的地址, forlinx 是要写入的内容)。 从SPI设备读数据,调用lseek来改变读取数据的位置,这样就可以在整个SPI设备里读数据,如./readframAPP /dev/pb85rs 0 ( 0 是要读取数据的地址) 3 实际测试 (1)首先将fram.ko, readframAPP, writefram APP拷贝到OK3568-C开发板的任意文件夹中。 (2)用insmod加载fram.ko模块,出现 spi_probe success! 说明驱动和设备树匹配成功。 (3)在/dev/下看有没有 pb85rs 设备。 (4)使用./writeframAPP /dev/pb85rs 1500 forlinx 向设备写数据, 1500 是地址, forlinx 是要写入的内容。 (5)使用./readframAPP /dev/pb85rs 1500 ( 1500 是要读取数据的地址) 4 断电测试 断电一天后重新读取PB85RS2MC的数据,发现数据仍然存在。验证了PB85RS2MC铁电存储芯片的断电数据保留特性。 至此,我们就完成了在OK3568-C开发板上添加一个新的SPI铁电存储芯片的操作!
相关资源