一、临界区

临界区(Critical Section) 是多任务/实时操作系统(如 FreeRTOS)里非常核心的一个概念,一旦进入这个区域,其他任务就不能抢占、中断也不能进来捣乱,直到你自己跑完并主动“退出”为止。

应用场景:

1、保护共享变量/数据 ;比如你有一个全局变量 int count = 0; 任务A 要做 count++(实际是读→加1→写回三个步骤) 如果这时候被更高优先级的任务B 抢占,B 也改 count,就会出现经典的“数据竞争/赛况”(race condition),最终 count 值错乱。

2、严格的硬件时序要求;DHT11 通信靠非常精确的高低电平时间(几十微秒级)来区分 0 和 1。 如果中途被任务切换或被定时器中断打断,哪怕只耽误 10μs,整个 bit 就读错了,数据直接废掉。 所以必须把“测高电平多久→判断是0还是1”这段代码锁死,不允许任何人插队。

3、调用不可重入的函数/操作外设;比如操作 I2C、SPI、LCD 驱动,如果中途被打断,下次进来状态就乱了。

实现代码:

​
portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED;   // 自旋锁(ESP32 多核专用)

taskENTER_CRITICAL(&mux);     // 进入临界区
// 这里写你绝对不能被打断的代码
// 比如读 DHT11 的 40 个 bit
taskEXIT_CRITICAL(&mux);      // 退出临界区

​

内部实现过程:

1、关中断

2、在 ESP32(双核)上还会获取一个自旋锁(portMUX),防止另一个核同时进来

3、禁止任务调度(更高优先级任务也抢不到 CPU)

释放过程相反:释放锁、开中断、恢复调度

TIPS:临界区虽然强大,但越短越好!

spi_master源码阅读

/**
 * @brief SPI common used frequency (in Hz)
 * @note SPI peripheral only has an integer divider, and the default clock source can be different on other targets,
 *       so the actual frequency may be slightly different from the desired frequency.
 */
#define SPI_MASTER_FREQ_8M      (80 * 1000 * 1000 / 10)   ///< 8MHz
#define SPI_MASTER_FREQ_9M      (80 * 1000 * 1000 / 9)    ///< 8.89MHz
#define SPI_MASTER_FREQ_10M     (80 * 1000 * 1000 / 8)    ///< 10MHz
#define SPI_MASTER_FREQ_11M     (80 * 1000 * 1000 / 7)    ///< 11.43MHz
#define SPI_MASTER_FREQ_13M     (80 * 1000 * 1000 / 6)    ///< 13.33MHz
#define SPI_MASTER_FREQ_16M     (80 * 1000 * 1000 / 5)    ///< 16MHz
#define SPI_MASTER_FREQ_20M     (80 * 1000 * 1000 / 4)    ///< 20MHz
#define SPI_MASTER_FREQ_26M     (80 * 1000 * 1000 / 3)    ///< 26.67MHz
#define SPI_MASTER_FREQ_40M     (80 * 1000 * 1000 / 2)    ///< 40MHz
#define SPI_MASTER_FREQ_80M     (80 * 1000 * 1000 / 1)    ///< 80MHz

这些是推荐的 SPI 时钟频率(基于 APB 时钟 80MHz 的整数分频)

为什么不是精确 10MHz 而是 80M/8 = 10M?因为 ESP32 SPI 只能整数分频

设置设备标志位

#define SPI_DEVICE_TXBIT_LSBFIRST          (1<<0)  // 发送 LSB first
#define SPI_DEVICE_RXBIT_LSBFIRST          (1<<1)
#define SPI_DEVICE_3WIRE                   (1<<2)  // 单线半双工 (MOSI 复用收发)
#define SPI_DEVICE_POSITIVE_CS             (1<<3)  // CS 高有效(默认低有效)
#define SPI_DEVICE_HALFDUPLEX              (1<<4)  // 先发后收(默认全双工同时收发)
#define SPI_DEVICE_NO_DUMMY                (1<<6)  // 禁用自动 dummy bit 插入(高频读有风险)

3. 核心结构体:spi_device_interface_config_t(设备配置)

这是挂载一个从设备时最关键的结构体。

关键字段解释(按重要性排序):

字段 类型 典型值 含义 & 使用场景
command_bits uint8_t 0 / 8 / 16 命令阶段位数(很多芯片有 1 字节命令,如 0x03 读、0x02 写)
address_bits uint8_t 0 / 8 / 24 地址阶段位数(闪存常用 24bit 地址)
dummy_bits uint8_t 0–16 地址 → 数据之间的空周期(高频读补偿 slave 延迟)
mode uint8_t 0–3 SPI 模式(CPOL + CPHA):0 最常见,3 也很多
clock_speed_hz int 1000000–40000000 时钟频率(用上面宏定义最安全)
spics_io_num int GPIO 号 / -1 CS 引脚(-1 = 不使用硬件 CS,自己手动控制)
queue_size int 1–10 队列深度(中断模式下可同时排队几个事务,建议 ≥1)
flags uint32_t 0 或组合标志 上面那些 SPI_DEVICE_xxx 标志
pre_cb / post_cb 函数指针 NULL 或回调 事务前/后回调(在中断里执行,常用于 GPIO 控制 DC/RS 线、CS 手动翻转)
input_delay_ns int 0 或 几十 ns slave 输出延迟(高频 >8M 时建议设置,补偿 MISO 时序)

小结:大多数简单 SPI 器件,只需要设置 mode、clock_speed_hz、spics_io_num、flags 就够了。复杂器件(如 NOR Flash、LCD)才会用到 command/address/dummy。

4. 事务描述结构体:spi_transaction_t(最常操作的)

每次发一次数据,就是填一个这个结构体。

关键字段:

  • flags:SPI_TRANS_xxx(如 USE_TXDATA、USE_RXDATA、VARIABLE_CMD 等)
  • cmd:命令值(长度由 device config 决定)
  • addr:地址值
  • length:总发送位数(bits!不是 bytes)
  • rxlength:期望接收位数(全双工时可 < length)
  • tx_buffer / tx_data[4]:发送数据(用 tx_data 时最多 32bit 直接内嵌)
  • rx_buffer / rx_data[4]:接收缓冲
  • user:用户自定义指针(常用来传上下文)

小技巧

  • 小数据(≤32bit) → 用 tx_data[] / rx_data[] + 对应 flag,省 DMA
  • 大数据 → 用 buffer + DMA

二、缓冲区Buffer

缓冲区就是一块临时存放数据的内存区域,用来“缓冲”(缓和、暂存)数据在生产者和消费者之间速度不匹配的问题。

在LVGL中,缓冲区让 LVGL 可以“先把画面画好存起来,再一次性高效发给 LCD”,避免闪烁、撕裂、卡顿,同时支持部分刷新,极大提高 UI 流畅度和性能。

  • LVGL 用 CPU(或 GPU)慢慢画 UI(按钮、文字、动画)
  • LCD 通过 QSPI/DMA 很快就能把一整块像素数据显示出来
  • 如果没有缓冲区:CPU 画一点推一点 → 画面会闪烁、撕裂(tearing)
  • 有缓冲区:CPU 先把一整块画完存到 buf1 → 一次性 DMA 推给 LCD → 画面完整

实现双缓冲(double buffering)避免闪烁

  • LVGL 同时用两块 buf:
    • 当前显示用 buf1(LCD 正在从 buf1 读)
    • CPU 在 buf2 上画下一帧
  • 画完后交换:buf2 变成显示缓冲,buf1 给 CPU 画下一帧
  • 这样用户永远看到完整的画面,不会看到“半成品”
    static lv_color_t *buf1 = NULL;
    static lv_color_t *buf2 = NULL;
    
    lv_disp_draw_buf_init(&draw_buf, buf1, buf2, DISP_BUF_SIZE);

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐