疑惑
esp-idf中 lcd、io、bus 像是 spi_bus 和 spi_io 不都是配套出现的吗?那为啥还要分层呢...
总结
抽象分层
核心思想:分层不是为了拆分,是为了抽象 + 可组合
ESP-IDF 的 esp_lcd 框架分为三个层级:
- BUS 层(硬件接口)
- SPI / I2C / 8080 / RGB / MIPI DSI
- 负责 怎么把数据按该总线的规则发出去。
- IO 层(屏幕指令接口)
- st7789_spi、ili9341_spi、st7789_8080 等
- 负责 怎么向某个屏幕型号发送命令/数据。
- Panel 层
- st7789、ili9341
- 负责 分辨率、初始化序列、偏移、MADCTL、坐标翻转等逻辑。
代码写的多的话会发现不同 io 所用的 SPI 总线基本一致,唯一不同的是如何对 SPI 做 IO 操作。不管如此,其他外设也会使用到 SPI 总线,如 SD 卡、Flash...
之所以分这些层,就是因为提高复用,增加可组合性。
接口
接口 = 稳定的抽象边界
之前学习 js 就有接触 class,但是权当 class 作为一种组织代码的方式,接口也从来没怎么用过,完全没 get 到接口的精髓。
现在接触了嵌入式,面对纷杂庞大的环境,第一次体会到 接口设计 的好处,接口的目的不仅仅是封装功能,而是隔离变化。
就拿屏幕来说,不同屏幕有不同驱动芯片,相同芯片也可能会使用到不同总线。只有一套配置的话,代码写在一起也还可以,这样反而更轻松。但是如果要给上面所有情况各写一套代码,那简直是 灾难😣
来看一个具体的示例:
esp_lcd_panel_io_handle_t lcd_io = NULL;
esp_lcd_new_panel_io_spi(SPI2_HOST, &io_config, &lcd_io);// 初始化 panel
esp_lcd_panel_handle_t lcd_panel = NULL;
const esp_lcd_panel_dev_config_t panel_config = {...};esp_lcd_new_panel_st7789(lcd_io, &panel_config, &lcd_panel);
新建一个 st7789_panel 只需要一个 panel_config 和 提供 io 操作的 lcd_io 句柄。
虽然 lcd_io 是一个底层由 spi 驱动的 io,但是不同总线最终都会返回一个 拥有相同操纵 io 的 esp_lcd_panel_io_handle_t 句柄。
esp_lcd_panel_io_t 的具体 io 操作:
/*** @brief LCD panel IO interface*/
struct esp_lcd_panel_io_t {/*** @brief Transmit LCD command and receive corresponding parameters*/esp_err_t (*rx_param)(esp_lcd_panel_io_t *io, int lcd_cmd, void *param, size_t param_size);/*** @brief Transmit LCD command and corresponding parameters*/esp_err_t (*tx_param)(esp_lcd_panel_io_t *io, int lcd_cmd, const void *param, size_t param_size);/*** @brief Transmit LCD RGB data*/esp_err_t (*tx_color)(esp_lcd_panel_io_t *io, int lcd_cmd, const void *color, size_t color_size);/*** @brief Destroy LCD panel IO handle (deinitialize all and free resource)*/esp_err_t (*del)(esp_lcd_panel_io_t *io);/*** @brief Register LCD panel IO callbacks*/esp_err_t (*register_event_callbacks)(esp_lcd_panel_io_t *io, const esp_lcd_panel_io_callbacks_t *cbs, void *user_ctx);
};
这样的话,即使更换底层总线驱动,也只需要更改新的 io 就行,向上暴露的方法都是一样的,也就是 隔离了变化。