tag 标签: esp32

相关帖子
相关博文
  • 热度 3
    2025-6-29 20:40
    1453 次阅读|
    1 个评论
    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无法进行下一步刷新
  • 热度 9
    2025-6-17 16:39
    15587 次阅读|
    2 个评论
    概述 相关API函数 举例:定时发送一个事件 总结 概述 ESP32有一组外设--定时器组。它可以选择不同的时钟源和分配系数。该定时器应用灵活,超时报警可以自动更新计数值。 相关API函数 1.定时器配置结构体 typedefstruct { gptimer_clock_source_tclk_src; /* 定时器时钟源,在clk_tree_defs.h中有个枚举soc_periph_gptimer_clk_src_t */ gptimer_count_direction_tdirection; /*计数方向 ,定义在timer_typers.h的枚举gptimer_count_direction_t中*/ uint32_tresolution_hz; /*频率分辨率,单位HZ,每个滴答步长是1/ resolution_hz秒*/ intintr_priority; /*定时器中断优先级,如果设置0,将分配优先级较低的中断*/ struct { uint32_tintr_shared: 1 ; /*设置是否与其他外设共享此定时器终端号*/ } flags; } gptimer_config_t ; 2.创建定时器句柄 esp_err_tgptimer_new_timer ( constgptimer_config_t *config, //填充完成的gptimer_config_t变量 gptimer_handle_t *ret_timer //返回的定时器句柄,所以在调用之前先要定义一个gptimer_handle_t变量,用于返回定时器句柄 ) 3.设置报警并触发一个事件 配置警报动作 esp_err_tgptimer_set_alarm_action ( gptimer_handle_ttimer, //上一步返回的定时器句柄 constgptimer_alarm_config_t *config //警报的相关配置 ) typedefstruct { uint64_talarm_count; /*警报的目标计数值 */ uint64_treload_count; /*只有当auto_reload_on_alarm设置为ture,才会有影响,重载计数值 */ struct { uint32_tauto_reload_on_alarm: 1 ; /*当警报发生时,重载按硬件开始 */ } flags; } gptimer_alarm_config_t ; 注册事件回调函数 esp_err_tgptimer_register_event_callbacks ( gptimer_handle_ttimer, //返回的定时器句柄 constgptimer_event_callbacks_t *cbs, //事件回调函数 void *user_data //传递给回调函数的参数 ) //回调函数的函数原型,在gptimer_event_callbacks_t结构体里定义 typedefbool (* gptimer_alarm_cb_t )(gptimer_handle_ttimer, //定时器句柄 constgptimer_alarm_event_data_t *edata, //传递报警事件的相关数据 void *user_ctx //用户自定义参数 ) 4.使能定时器 esp_err_tgptimer_enable(gptimer_handle_ttimer) 5.启动定时器 esp_err_tgptimer_start(gptimer_handle_ttimer) 举例:定时发送一个事件 /** * Copyright (C) 2024-2034 HalfMoon2. * All rights reserved. * * @file Filename without the absolute path * @brief Brief description * @author HalfMoon2 * @date 2025-06-13 * @version v0.1 * * @revision history: * 2025-06-13 - Initial version. */ # include stdio.h # include driver/gptimer.h # include freertos/FreeRTOS.h # include freertos/task.h # include esp_err.h # include esp_log.h ​ # define event_bit0 (10) EventGroupHandle_teventGroup= NULL ; ​ boolgptimer_alarm_cb (gptimer_handle_ttimer, constgptimer_alarm_event_data_t *edata, void *user_ctx) { BaseType_tpxHigherPriorityTaskWoken; xEventGroupSetBitsFromISR (eventGroup,event_bit0,pxHigherPriorityTaskWoken); returnfalse; } ​ voidevent_task ( void *pvParam) { while ( 1 ){ if ( xEventGroupWaitBits (eventGroup,event_bit0,pdTRUE,pdFALSE,portMAX_DELAY)== 1 ){ ESP_LOGI ( "gptimer" , "alarm success!!!" ); } } } ​ staticvoidgptimer_init () { gptimer_handle_tgptimer= NULL ; gptimer_config_ttimer_config={ .clk_src=GPTIMER_CLK_SRC_DEFAULT, .direction=GPTIMER_COUNT_UP, .intr_priority= 0 , .resolution_hz= 1 * 1000 * 1000 , //分辨率为1M,一次滴答1us .flags.intr_shared= false }; //创建定时器句柄 ESP_ERROR_CHECK ( gptimer_new_timer (timer_config, gptimer)); ​ //设置警报动作,目标计数值,是否自动重载等 gptimer_alarm_config_talarm_config={ .alarm_count= 1000000 , //一次滴答1us,这里刚好1s .reload_count= 0 , .flags.auto_reload_on_alarm= true }; ESP_ERROR_CHECK ( gptimer_set_alarm_action (gptimer, alarm_config)); ​ //注册警报事件 gptimer_event_callbacks_tcbs={ .on_alarm=gptimer_alarm_cb }; ESP_ERROR_CHECK ( gptimer_register_event_callbacks (gptimer,cbs, NULL )); ​ //使能定时器 ESP_ERROR_CHECK ( gptimer_enable (gptimer)); ​ //开启定时器 ESP_ERROR_CHECK ( gptimer_start (gptimer)); } ​ voidapp_main ( void ) { //创建一个事件组 eventGroup= xEventGroupCreate (); gptimer_init (); xTaskCreatePinnedToCore (event_task, "event_task" , 2048 , NULL , 3 , NULL , 1 ); } ​ 从打印可以看出,刚好1秒触发一次警报。 gptimer还支持动态更新计数值,可在回调函数中增加如下代码 gptimer_alarm_config_talarm_config= { .alarm_count=edata-alarm_value +1000000 , // 下一次警报在当前警报的基础上加 1s }; // 更新警报值 gptimer_set_alarm_action (timer, alarm_config); 总结 一定要注意,在回调函数中不能使用ESP_LOGI或printf等打印函数,否则系统会崩溃。
  • 热度 2
    2025-6-9 22:37
    5095 次阅读|
    2 个评论
    ESP32开发之WS2812B控制
    WS2812B数据手册重点摘录 硬件电路 Remote Control Transceive(RMT)概念 RMT的相关API函数 一段简单的控制WS2812B的应用举例 总结 WS2812B数据手册重点摘录 WS2812的数据手册还是很好找的,可以直接去立创商城输入型号就可以找到。 重点一:引脚结构 VDD:供电输入;DI:数据输入;DO:数据输出(用于级联);GND:供电地 重点二:供电 如图供电5V足够了 重点三:数据结构 每个颜色数据为8bit 重点四:数据传输波形 波形图还是很明了易懂的,类似于NEC编码方式。根据数据传输定义,可以知道咱们需要给它的控制信号是1MHz的。 重点五:厂商给的设计建议 一定要看,厂商一般都会做大量的测试,他们的建议是最好的参考标准。这个器件的手册还是很详细的,如图的1、2条说明仔细看。 电路图 根据器件的数据手册,可以很轻松的绘制电路图 Remote Control Transceive(RMT)概念 背景 :还记得在上次的呼吸灯么?通过ESP32自带的LEDC控制器实现的呼吸灯还是很轻松的。通过对WS2812B的数据手册阅读,可以发现对它的驱动其实也是PWM,是否也可用LEDC呢?答案是不行的,在使用LEDC产生PWM的时候可以发现,PWM的占空比是不可控的(无法控制其之发送一段PWM)。由WS2812B的传输波形可知,其需要的波形是一段的且每个周期是可控的。 引入 :根据WS2812B的传输波形特征,发现ESP32中有一个Remote Control Transceive的外设控制器,结合安可信官网的知识解读,这个RMT是控制WS2812B的不二之选。且在RMT的案例库中有一个控制WS2812这类LED的例程,路径如下: 例程内容较为复杂,初学者一时半会很难明白其中的逻辑。在此,我做一个简单的实现例程, RMT的概念 :RMT是一个红外发射和接收控制器,数据格式灵活,可以发送或接收多种类型的信号。它有自己的数据模式,叫做RMT符号。每个符号由两对两个值组成,每对中的第一个值是一个 15 位的值,表示信号持续时间,以 RMT 滴答计。每对中的第二个值是一个 1 位的值,表示信号的逻辑电平,即高电平或低电平。这个格式非常适合控制WS2812B . 其他具体信息可以查看官方手册 https://docs.espressif.com/projects/esp-idf/zh_CN/v5.4.1/esp32/api-reference/peripherals/rmt.html RMT的相关API函数 目前只用到了RMT的发送,所以只说明发送的API函数 RMT发送通道配置结构体 typedef struct { gpio_num_t gpio_num; /*所要映射的GPIO口,不使用时设置为-1*/ rmt_clock_source_t clk_src; /*RMT通道所使用的时钟源 */ uint32_t resolution_hz; /*此通道的时钟分辨率 */ size_t mem_block_symbols; /*内存块大小,如果启用了DMA,则该字段可以控制内部DMA缓冲区大小,建议设置1024;如果未启用DMA,则至少设置为64。不是所有通道都支持DMA,具体要查看芯片的TRM文档*/ size_t trans_queue_depth; /*设置后台等待处理的事务数量*/ int intr_priority; /*设置中断优先级 */ struct { uint32_t invert_out: 1 ; /*设置是否反转输出信号*/ uint32_t with_dma: 1 ; /*是否开启DMA */ uint32_t io_loop_back: 1 ; /*是否返回给输入,作为换回测试 */ uint32_t io_od_mode: 1 ; /*设置GPIO是否作为开漏模式 */ } flags; /*! TX channel config flags */ } rmt_tx_channel_config_t ; 分配和初始化TX通道 esp_err_t rmt_new_tx_channel ( const rmt_tx_channel_config_t *config, rmt_channel_handle_t *ret_chan) /*第一个参数是 rmt_tx_channel_config_t定义并填充的结构体变量,第二个参数是此函数返回的RMT通道句柄,需要使用rmt_channel_handle_t先声明一个变量*/ 使能通道和禁用通道 //使能 esp_err_t rmt_enable ( rmt_channel_handle_t channel) //参数为rmt_new_tx_channel返回的RMT通道句柄 //禁用 esp_err_t rmt_disable ( rmt_channel_handle_t channel) //参数为rmt_new_tx_channel返回的RMT通道句柄 设置RMT符号格式,即输出什么样的信号 typedef struct { rmt_symbol_word_t bit0; /*RMT符号的bit0字节格式 */ rmt_symbol_word_t bit1; /*RMT符号的bit1字节格式*/ struct { uint32_t msb_first: 1 ; /*最先编最高有效位*/ } flags; /*编码配置状态 */ } rmt_bytes_encoder_config_t ; typedef union { struct { uint16_t duration0 : 15 ; /* level0 持续时间*/ uint16_t level0 : 1 ; /*Level0的电平值*/ uint16_t duration1 : 15 ; /*level1 持续时间*/ uint16_t level1 : 1 ; /*Level1的电平值 */ }; uint32_t val; /* 与RMT 符号等价的无符号值 */ } rmt_symbol_word_t ; 创建字节编码器 esp_err_t rmt_new_bytes_encoder ( const rmt_bytes_encoder_config_t *config, //创建好的 rmt_bytes_encoder_config_t的结构体变量 rmt_encoder_handle_t *ret_encoder //返回的编码器句柄,需要在调用前创建 rmt_encoder_handle_t变量 ) 发送配置结构体 typedef struct { int loop_count; /*循环次数 ,设置为-1则无限循环*/ struct { uint32_t eot_level : 1 ; /*发送结束后输出的电平 */ uint32_t queue_nonblocking : 1 ; /*设置当传输队列满的时候该函数是否需要等待 */ } flags; /*! Transmit specific config flags */ } rmt_transmit_config_t ; 启动发送 esp_err_t rmt_transmit ( rmt_channel_handle_t tx_channel, //发送通道句柄 rmt_encoder_handle_t encoder, //编码器句柄 const void *payload, //指向需要发送的值 size_t payload_bytes, //发送的值的大小 const rmt_transmit_config_t *config //发送的配置结构体 ) 一段简单的控制WS2812B的应用举例 /** * Copyright (C) 2024-2034 HalfMoon2. * All rights reserved. * * @file Filename without the absolute path * @brief Brief description * @author HalfMoon2 * @date 2025-06-05 * @version v0.1 * * @revision history: * 2025-06-05 - Initial version. */ # include stdio.h # include "driver/rmt_common.h" # include "driver/rmt_tx.h" # include "driver/rmt_encoder.h" # include freertos/FreeRTOS.h # include freertos/task.h void app_main ( void ) { uint8_t value ={ 0x0f , 0xb0 , 0x01 }; rmt_channel_handle_t tx_chan = NULL ; rmt_tx_channel_config_t tx_chan_config = { .clk_src = RMT_CLK_SRC_DEFAULT, // 选择时钟源 .gpio_num = 48 , // GPIO 编号 .mem_block_symbols = 64 , // 内存块大小,即 64 * 4 = 256 字节 .resolution_hz = 10 * 1000 * 1000 , // 10 MHz 滴答分辨率,即 1 滴答 = 100ns .trans_queue_depth = 4 , // 设置后台等待处理的事务数量 .flags.invert_out = false , // 不反转输出信号 .flags.with_dma = false , // 不需要 DMA 后端 }; ESP_ERROR_CHECK ( rmt_new_tx_channel (tx_chan_config, tx_chan)); rmt_encoder_handle_t rmt_encoder= NULL ; rmt_bytes_encoder_config_t rmt_bytes_encoder={ .bit0={ .level0= 1 , .duration0= 0.3 * 10000000 / 1000000 , //T0H .level1= 0 , .duration1= 0.6 * 10000000 / 1000000 , //T0L }, .bit1={ .level0= 1 , .duration0= 0.6 * 10000000 / 1000000 , //T1H .level1= 0 , .duration1= 0.3 * 10000000 / 1000000 , //T1L }, .flags={ .msb_first= 1 , } }; rmt_new_bytes_encoder (rmt_bytes_encoder,rmt_encoder); rmt_transmit_config_t rmt_transmit_config={ .loop_count= 0 , }; ESP_ERROR_CHECK ( rmt_enable (tx_chan)); ESP_ERROR_CHECK ( rmt_transmit (tx_chan,rmt_encoder,value, sizeof (value),rmt_transmit_config)); } 从波形到颜色,说明这个简单的操作是可以很轻松实现WS2812B的控制的。 总结 整篇文章结构可以看出,如果要控制一个外设要做到以下步骤: 仔细阅读数据手册, 知道硬件电路的搭建, 看懂控制外设的协议(就是时序图), 匹配主控芯片可用的外设控制器, 阅读官方指导文档,查看当前需要的API函数 一步步填充API函数及相关结构体 这样一个简单的控制程序就完成了。当然最后的结果就跟我粘贴的代码一样,都放在了main函数里了,业界称为屎山。这个时候就需要通过面向对象的编程思想以及设计模式等知识,将代码规范化、增强其可移植性等。 要记住的是,只有理解了其基本的原理,才能万变不离其中。
  • 热度 1
    2025-5-18 20:54
    352 次阅读|
    0 个评论
    7. ESP32开发之freeRTOS的互斥量
    什么是互斥量 互斥量的应用场合 互斥量的API函数 基本代码结构 互斥量使用举例 递归锁 递归锁举例 总结 什么是互斥量 在freeRTOS中,多个任务访问一块共享资源,会产生竞争现象。 比如马路上只有一个很早以前的电话亭,A、B都想要打电话,然后他们就开始打架了。但是如果A先进去了然后把门锁了,B想进去打电话的话只能在外面等,必须等到A把门锁打开。 互斥量的应用场合 像freeRTOS的多任务系统,任务A正在使用某个资源,还没用完的时候,任务B也来使用,就可能会导致问题。 就比如串口,任务A正在用串口发送数据,此时任务B也来用这个串口发送数据,这样就会导致数据混乱。 简而言之,多任务使用共享资源的情况下,就需要使用互斥量了。 这里有个特别强调的注意点 :按照正常的情况来说,只要任务A获取了互斥量,其他任务都无法释放互斥量才对。但是freeRTOS中并没有实现,其他任务也可释放互斥量。所以在freeRTOS中,大家默认谁获取互斥量,就该谁释放。 互斥量的API函数 创建互斥量 /* 返回值:正确返回SemaphoreHandle_t 变量,错误返回NULL */ SemaphoreHandle_t xSemaphoreCreateMutex( void ); 获取 /* 参数: xSemaphore : 创建的互斥量 xBlockTime : 获取锁等待时间,超时获取失败 返回值: 获取到,返回pdTRUE,反之,pdFALSE */ SemaphoreHandle_t xSemaphoreTake( xSemaphore, xBlockTime ) 释放 /* 参数: xSemaphore : 创建的互斥量 返回值: 释放成功,返回pdTRUE,反之,pdFALSE */ SemaphoreHandle_t xSemaphoreGive( xSemaphore, xBlockTime ) 基本代码结构 if(xSemaphoreTake(xSemaphore,pdMS_TO_TICKS(1000))==pdTRUE){ /*资源处理代码,这里可以称之为临界区*/ xSemaphoreGive(xSemaphore); } 互斥量使用举例 首先举一个错误例子,这样会有个对比 void taskA(void* pvPram) { while(1){ for(int i=0;i5;i++){ num++; ESP_LOGI("taskA","num is %d",num); } vTaskDelay(pdMS_TO_TICKS(1000)); } } void taskB(void* pvPram) { while(1){ num++; ESP_LOGI("taskB","num is %d",num); vTaskDelay(pdMS_TO_TICKS(1000)); } } void app_main(void) { xSemaphore=xSemaphoreCreateMutex(); xTaskCreatePinnedToCore(taskA,"taskA",2048,NULL,4,NULL,1); xTaskCreatePinnedToCore(taskB,"taskB",2048,NULL,4,NULL,1); } 打印结果如下:代码目的是taskA执行过程中,num值是连续增加的,然而打印中会发现,taskA打印并不连续,会被taskB抢占。 使用mutex函数 /** * Copyright (C) 2024-2034 HalfMoon2. * All rights reserved. * * @file Filename without the absolute path * @brief Brief description * @author HalfMoon2 * @date 2025-05-07 * @version v0.1 * * @revision history: * 2025-05-07 - Initial version. */ #include stdio.h #include freertos/FreeRTOS.h #include freertos/task.h #include freertos/semphr.h #include esp_log.h SemaphoreHandle_t xSemaphore; BaseType_t num=0; void taskA(void* pvPram) { while(1){ /*如果在1S获取到互斥量,那么执行num++五次并打印 */ if(xSemaphoreTake(xSemaphore,pdMS_TO_TICKS(1000))==pdTRUE){ for(int i=0;i5;i++){ num++; ESP_LOGI("taskA","num is %d",num); } xSemaphoreGive(xSemaphore); } vTaskDelay(pdMS_TO_TICKS(1000)); } } void taskB(void* pvPram) { while(1){ if(xSemaphoreTake(xSemaphore,pdMS_TO_TICKS(1000))==pdTRUE){ num++; ESP_LOGI("taskB","num is %d",num); xSemaphoreGive(xSemaphore); } vTaskDelay(pdMS_TO_TICKS(1000)); } } void app_main(void) { xSemaphore=xSemaphoreCreateMutex(); /*为了产生竞争,两个任务优先级设置一样 */ xTaskCreatePinnedToCore(taskA,"taskA",2048,NULL,4,NULL,1); xTaskCreatePinnedToCore(taskB,"taskB",2048,NULL,4,NULL,1); } 打印结果: 分析: 从打印看,当任务A执行完五次num++后,才会轮到任务B执行一次。完全符合执行逻辑。 递归锁 递归锁的引出 假设有两个互斥量M1、M2,taskA获取了M1,taskB获取了M2。然而taskA需要再获取M2才能执行下一步,且taskB也需要再获取M1才能执行下一步。这样就导致了taskA、taskB都无法释放互斥量。导致了死锁。 再假设有一个互斥量M1,任务A获取了M1后,执行一个函数,然而此函数中也获取了M1,此时任务A还没释放M1,这将导致任务A自己把自己锁了。 然而递归锁,可以多次获取,可以解决以上问题 递归锁的函数 /*创建递归锁*/ SemaphoreHandle_t xSemaphoreCreateRecursiveMutex( void ); /*获得 */ BaseType_t xSemaphoreTakeRecursive( SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait); /*释放 */ BaseType_t xSemaphoreGiveRecursive( SemaphoreHandle_t xSemaphore ); 递归锁举例 死锁举例 SemaphoreHandle_t xSemaphore; BaseType_t num=0; void func(BaseType_t *a) { //获取互斥量等待时间设置为portMAX_DELAY,因为taskA为释放互斥量,导致此处一直等待 if(xSemaphoreTakeRecursive(xSemaphore,portMAX_DELAY)==pdTRUE){ *a=*a+1; xSemaphoreGiveRecursive(xSemaphore); } } void taskA(void* pvPram) { while(1){ /*如果在1S获取到互斥量,那么执行num++五次并打印 */ if(xSemaphoreTake(xSemaphore,pdMS_TO_TICKS(1000))==pdTRUE){ func(num); ESP_LOGI("taskA","num is %d",num); xSemaphoreGive(xSemaphore); } vTaskDelay(pdMS_TO_TICKS(1000)); } } void app_main(void) { xSemaphore=xSemaphoreCreateMutex(); /*为了产生竞争,两个任务优先级设置一样 */ xTaskCreatePinnedToCore(taskA,"taskA",2048,NULL,4,NULL,1); } 打印如下:程序将一直锁着,无法执行 使用递归锁 SemaphoreHandle_t xSemaphore; BaseType_t num=0; void func(BaseType_t *a) { //获取互斥量等待时间设置为portMAX_DELAY,因为taskA为释放互斥量,导致此处一直等待 if(xSemaphoreTakeRecursive(xSemaphore,portMAX_DELAY)==pdTRUE){ *a=*a+1; xSemaphoreGiveRecursive(xSemaphore); } } void taskA(void* pvPram) { while(1){ /*如果在1S获取到互斥量,那么执行num++五次并打印 */ if(xSemaphoreTakeRecursive(xSemaphore,pdMS_TO_TICKS(1000))==pdTRUE){ func(num); ESP_LOGI("taskA","num is %d",num); xSemaphoreGiveRecursive(xSemaphore); } vTaskDelay(pdMS_TO_TICKS(1000)); } } void app_main(void) { xSemaphore=xSemaphoreCreateRecursiveMutex(); /*为了产生竞争,两个任务优先级设置一样 */ xTaskCreatePinnedToCore(taskA,"taskA",2048,NULL,4,NULL,1); } 打印如下:发现完全可以正常打印了 总结 为了避免死锁,在中断中不要使用互斥量 如果有嵌套使用互斥量,请使用递归锁,递归的层数由queueQUEUE_TYPE_RECURSIVE_MUTEX决定 虽然freeRTOS没有实现谁锁谁开,但是开发过程中还是默认这么操作了
  • 2025-5-18 20:48
    302 次阅读|
    0 个评论
    6. ESP32开发之freeRTOS的信号量
    什么是信号量 信号量能干啥 信号量的函数 实例举例 总结 什么是信号量 简而言之,就是发出通知,接收通知的任务获得通知后去干啥啥。通知有多有少。自定义通知数量的,叫计数型信号量;只有有无(即“0”,“1”)通知的,叫二进制信号量。 信号量能干啥 资源管理:控制多个任务对共享资源(如外设、内存块)的访问权限,避免竞争条件 任务同步 :实现任务间的时序协调(如等待某个事件完成) 中断与任务通信:在中断服务程序(ISR)中快速通知任务处理事件(需使用 xxxFromISR 版本的函数) 信号量的函数 创建二进制信号量函数原型 SemaphoreHandle_t xSemaphoreCreateBinary()//返回值为SemaphoreHandle_t结构体指针,即创建的信号量,不成功返回NULL 释放信号量,使信号量计数加1 xSemaphoreGive(xSemaphore) 获取信号量,使信号量计数减1 xSemaphoreTake(xSemaphore, //创建的信号量 xBlockTime //等待时间,超时则返回pdFulse ); 创建计数型信号量 SemaphoreHandle_t xSemaphoreCreateCounting( UBaseType_t uxMaxCount, //最大计数值 UBaseType_t uxInitialCount//初始计数值 );//返回值为SemaphoreHandle_t结构体指针,即创建的信号量,不成功返回NULL 删除信号量 void vSemaphoreDelete( SemaphoreHandle_t xSemaphore ); 实例 二进制信号量的使用 /** * Copyright (C) 2024-2034 HalfMoon2. * All rights reserved. * * @file Filename without the absolute path * @brief Brief description * @author HalfMoon2 * @date 2025-04-29 * @version v0.1 * * @revision history: * 2025-04-29 - Initial version. */ #include stdio.h #include freertos/FreeRTOS.h #include freertos/task.h #include freertos/semphr.h #include esp_log.h SemaphoreHandle_t semap=NULL; void taskA(void *pvParam) { while(1){ xSemaphoreGive(semap); vTaskDelay(pdMS_TO_TICKS(1000)); } } void taskB(void *pvParam) { BaseType_t num=0; while(1){ if(xSemaphoreTake(semap,pdMS_TO_TICKS(500))==pdPASS){ num++; ESP_LOGI("taskB","num is %d",num); }else{ ESP_LOGI("taskB","Take failed"); } } } void app_main(void) { semap=xSemaphoreCreateBinary(); xTaskCreatePinnedToCore(taskA,"taskA",2048,NULL,4,NULL,1); xTaskCreatePinnedToCore(taskB,"taskB",2048,NULL,3,NULL,1); } 结果解析:taskB任务每500ms获取一次信号量,当获取成功则num变量加一,然而taskA任务每1000ms发出一个信号量,所以taskB每间隔500ms会失败一次。 计数型信号量使用 /** * Copyright (C) 2024-2034 HalfMoon2. * All rights reserved. * * @file Filename without the absolute path * @brief Brief description * @author HalfMoon2 * @date 2025-04-30 * @version v0.1 * * @revision history: * 2025-04-30 - Initial version. */ #include stdio.h #include stdio.h #include freertos/FreeRTOS.h #include freertos/task.h #include freertos/semphr.h #include esp_log.h SemaphoreHandle_t semap=NULL; void taskA(void *pvParam) { while(1){ xSemaphoreGive(semap); vTaskDelay(pdMS_TO_TICKS(1000)); } } void taskB(void *pvParam) { BaseType_t num=0; while(1){ if(xSemaphoreTake(semap,pdMS_TO_TICKS(400))==pdPASS){ num++; ESP_LOGI("taskB","num is %d",num); }else{ ESP_LOGI("taskB","Take failed"); } } } void app_main(void) { semap=xSemaphoreCreateCounting(3,2); xTaskCreatePinnedToCore(taskA,"taskA",2048,NULL,4,NULL,1); xTaskCreatePinnedToCore(taskB,"taskB",2048,NULL,3,NULL,1); } 结果解析:创建计数型信号量时,初始值为2,taskA优先级高些先执行,信号量计数值加1,信号量计数值为3,taskB每400ms获取信号量前三次时成功的,第四次时,由于taskA阻塞时间为1000ms,这个时候其还没发出信号量,信号量计数值为0,所以taskB获取失败。 总结 二进制信号量可以说是计数型信号量的特殊用法。 信号量相对于消息队列要简单很多,区分好应用场景。信号量只传通知,消息队列是要传整个消息结构体的。如果看源码的话,信号量的API背后用的也是队列的API。 二进制信号量对后面学习互斥量给予了启发,一个任务如果take了此信号量不give,另一个任务想take会是什么结果呢?
相关资源