大佬tsl0092的nrf52811墨水屏4.2寸EPD_service.c文件学习-ELink墨水屏电子纸社区-FPGA CPLD-ChipDebug

大佬tsl0092的nrf52811墨水屏4.2寸EPD_service.c文件学习

 

EPD_service.c文件代码如下 :

/* Copyright (c) 2012 Nordic Semiconductor. All Rights Reserved.
 *
 * The information contained herein is property of Nordic Semiconductor ASA.
 * Terms and conditions of usage are described in detail in NORDIC
 * SEMICONDUCTOR STANDARD SOFTWARE LICENSE AGREEMENT.
 *
 * Licensees are granted free, non-transferable use of the information. NO
 * WARRANTY of ANY KIND is provided. This heading must NOT be removed from
 * the file.
 *
 */
 
#include <string.h>
#include "sdk_macros.h"
#include "ble_srv_common.h"
#include "nrf_delay.h"
#include "nrf_gpio.h"
#include "nrf_pwr_mgmt.h"
#include "app_scheduler.h"
#include "EPD_service.h"
#include "nrf_log.h"
 
#if defined(S112)
//#define EPD_CFG_DEFAULT {0x14, 0x13, 0x06, 0x05, 0x04, 0x03, 0x02, 0x03, 0xFF, 0x12, 0x07} // 52811
#define EPD_CFG_DEFAULT {0x14, 0x13, 0x12, 0x11, 0x10, 0x0F, 0x0E, 0x03, 0xFF, 0x0D, 0x02} // 52810
#else
//#define EPD_CFG_DEFAULT {0x05, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x01, 0x07}
#endif
 
#ifndef EPD_CFG_DEFAULT
#define EPD_CFG_DEFAULT {0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x03, 0x09, 0x03}
#endif
 
// defined in main.c
extern uint32_t timestamp(void);
extern void set_timestamp(uint32_t timestamp);
extern void sleep_mode_enter(void);
 
static void epd_gui_update(void * p_event_data, uint16_t event_size)
{
    epd_gui_update_event_t *event = (epd_gui_update_event_t *)p_event_data;
    ble_epd_t *p_epd = event->p_epd;
 
    EPD_GPIO_Init();
    epd_model_t *epd = epd_init((epd_model_id_t)p_epd->config.model_id);
    gui_data_t data = {
        .bwr             = epd->bwr,
        .width           = epd->width,
        .height          = epd->height,
        .timestamp       = event->timestamp,
        .temperature     = epd->drv->read_temp(),
        .voltage         = EPD_ReadVoltage(),
    };
    DrawGUI(&data, epd->drv->write_image, p_epd->display_mode);
    epd->drv->refresh();
    EPD_GPIO_Uninit();
}
 
/**@brief Function for handling the @ref BLE_GAP_EVT_CONNECTED event from the S110 SoftDevice.
 *
 * @param[in] p_epd     EPD Service structure.
 * @param[in] p_ble_evt Pointer to the event received from BLE stack.
 */
static void on_connect(ble_epd_t * p_epd, ble_evt_t * p_ble_evt)
{
    p_epd->conn_handle = p_ble_evt->evt.gap_evt.conn_handle;
    EPD_GPIO_Init();
}
 
/**@brief Function for handling the @ref BLE_GAP_EVT_DISCONNECTED event from the S110 SoftDevice.
 *
 * @param[in] p_epd     EPD Service structure.
 * @param[in] p_ble_evt Pointer to the event received from BLE stack.
 */
static void on_disconnect(ble_epd_t * p_epd, ble_evt_t * p_ble_evt)
{
    UNUSED_PARAMETER(p_ble_evt);
    p_epd->conn_handle = BLE_CONN_HANDLE_INVALID;
    EPD_GPIO_Uninit();
}
 
static void epd_service_on_write(ble_epd_t * p_epd, uint8_t * p_data, uint16_t length)
{
    NRF_LOG_DEBUG("[EPD]: on_write LEN=%d\n", length);
    NRF_LOG_HEXDUMP_DEBUG(p_data, length);
    if (p_data == NULL || length <= 0) return;
 
    switch (p_data[0])
    {
      case EPD_CMD_SET_PINS:
          if (length < 8) return;
 
          p_epd->config.mosi_pin = p_data[1];
          p_epd->config.sclk_pin = p_data[2];
          p_epd->config.cs_pin = p_data[3];
          p_epd->config.dc_pin = p_data[4];
          p_epd->config.rst_pin = p_data[5];
          p_epd->config.busy_pin = p_data[6];
          p_epd->config.bs_pin = p_data[7];
          if (length > 8)
              p_epd->config.en_pin = p_data[8];
          epd_config_write(&p_epd->config);
 
          EPD_GPIO_Uninit();
          EPD_GPIO_Load(&p_epd->config);
          EPD_GPIO_Init();
          break;
 
      case EPD_CMD_INIT: {
          uint8_t id = length > 1 ? p_data[1] : p_epd->config.model_id;
          if (id != p_epd->config.model_id) {
              p_epd->config.model_id = id;
              epd_config_write(&p_epd->config);
          }
          p_epd->epd = epd_init((epd_model_id_t)id);
        } break;
 
      case EPD_CMD_CLEAR:
          p_epd->display_mode = MODE_NONE;
          p_epd->epd->drv->clear();
          break;
 
      case EPD_CMD_SEND_COMMAND:
          if (length < 2) return;
          EPD_WriteCommand(p_data[1]);
          break;
 
      case EPD_CMD_SEND_DATA:
          EPD_WriteData(&p_data[1], length - 1);
          break;
 
      case EPD_CMD_REFRESH:
          p_epd->display_mode = MODE_NONE;
          p_epd->epd->drv->refresh();
          break;
 
      case EPD_CMD_SLEEP:
          p_epd->epd->drv->sleep();
          break;
 
      case EPD_CMD_SET_TIME: {
          if (length < 5) return;
 
          NRF_LOG_DEBUG("time: %02x %02x %02x %02x\n", p_data[1], p_data[2], p_data[3], p_data[4]);
          if (length > 5) NRF_LOG_DEBUG("timezone: %d\n", (int8_t)p_data[5]);
 
          uint32_t timestamp = (p_data[1] << 24) | (p_data[2] << 16) | (p_data[3] << 8) | p_data[4];
          timestamp += (length > 5 ? (int8_t)p_data[5] : 8) * 60 * 60; // timezone
          set_timestamp(timestamp);
          p_epd->display_mode = length > 6 ? (display_mode_t)p_data[6] : MODE_CALENDAR;
          ble_epd_on_timer(p_epd, timestamp, true);
      } break;
 
      case EPD_CMD_WRITE_IMAGE: // MSB=0000: ram begin, LSB=1111: black
          if (length < 3) return;
          if ((p_data[1] >> 4) == 0x00) {
              bool black = (p_data[1] & 0x0F) == 0x0F;
              EPD_WriteCommand(black ? p_epd->epd->drv->cmd_write_ram1 : p_epd->epd->drv->cmd_write_ram2);
          }
          EPD_WriteData(&p_data[2], length - 2);
          break;
 
      case EPD_CMD_SET_CONFIG:
          if (length < 2) return;
          memcpy(&p_epd->config, &p_data[1], (length - 1 > EPD_CONFIG_SIZE) ? EPD_CONFIG_SIZE : length - 1);
          epd_config_write(&p_epd->config);
          break;
 
      case EPD_CMD_SYS_SLEEP:
          sleep_mode_enter();
          break;
 
        case EPD_CMD_SYS_RESET:
#if defined(S112)
            nrf_pwr_mgmt_shutdown(NRF_PWR_MGMT_SHUTDOWN_RESET);
#else
            NVIC_SystemReset();
#endif
        break;
 
      case EPD_CMD_CFG_ERASE:
          epd_config_clear(&p_epd->config);
          nrf_delay_ms(100); // required
          NVIC_SystemReset();
          break;
 
      default:
        break;
    }
}
 
/**@brief Function for handling the @ref BLE_GATTS_EVT_WRITE event from the S110 SoftDevice.
 *
 * @param[in] p_epd     EPD Service structure.
 * @param[in] p_ble_evt Pointer to the event received from BLE stack.
 */
static void on_write(ble_epd_t * p_epd, ble_evt_t * p_ble_evt)
{
    ble_gatts_evt_write_t * p_evt_write = &p_ble_evt->evt.gatts_evt.params.write;
 
    if (
        (p_evt_write->handle == p_epd->char_handles.cccd_handle)
        &&
        (p_evt_write->len == 2)
       )
    {
        if (ble_srv_is_notification_enabled(p_evt_write->data))
        {
            NRF_LOG_DEBUG("notification enabled\n");
            p_epd->is_notification_enabled = true;
            static uint16_t length = sizeof(epd_config_t);
            NRF_LOG_DEBUG("send epd config\n");
            uint32_t err_code = ble_epd_string_send(p_epd, (uint8_t *)&p_epd->config, length);
            if (err_code != NRF_ERROR_INVALID_STATE)
                APP_ERROR_CHECK(err_code);
        }
        else
        {
            p_epd->is_notification_enabled = false;
        }
    }
    else if (p_evt_write->handle == p_epd->char_handles.value_handle)
    {
        epd_service_on_write(p_epd, p_evt_write->data, p_evt_write->len);
    }
    else
    {
        // Do Nothing. This event is not relevant for this service.
    }
}
 
#if defined(S112)
void ble_epd_evt_handler(ble_evt_t const * p_ble_evt, void * p_context)
{
    if (p_context == NULL || p_ble_evt == NULL) return;
    
    ble_epd_t *p_epd = (ble_epd_t *)p_context;
    ble_epd_on_ble_evt(p_epd, (ble_evt_t *)p_ble_evt);
}
#endif
 
void ble_epd_on_ble_evt(ble_epd_t * p_epd, ble_evt_t * p_ble_evt)
{
    if ((p_epd == NULL) || (p_ble_evt == NULL))
    {
        return;
    }
 
    switch (p_ble_evt->header.evt_id)
    {
        case BLE_GAP_EVT_CONNECTED:
            on_connect(p_epd, p_ble_evt);
            break;
 
        case BLE_GAP_EVT_DISCONNECTED:
            on_disconnect(p_epd, p_ble_evt);
            break;
 
        case BLE_GATTS_EVT_WRITE:
            on_write(p_epd, p_ble_evt);
            break;
 
        default:
            // No implementation needed.
            break;
    }
}
 
 
static uint32_t epd_service_init(ble_epd_t * p_epd)
{
    ble_uuid_t            ble_uuid = {0};
    ble_uuid128_t         base_uuid = BLE_UUID_EPD_SVC_BASE;
    ble_add_char_params_t add_char_params;
    uint8_t               app_version = APP_VERSION;
 
    VERIFY_SUCCESS(sd_ble_uuid_vs_add(&base_uuid, &ble_uuid.type));
 
    ble_uuid.type = ble_uuid.type;
    ble_uuid.uuid = BLE_UUID_EPD_SVC;
    VERIFY_SUCCESS(sd_ble_gatts_service_add(BLE_GATTS_SRVC_TYPE_PRIMARY,
                                            &ble_uuid,
                                            &p_epd->service_handle));
 
    memset(&add_char_params, 0, sizeof(add_char_params));
    add_char_params.uuid                     = BLE_UUID_EPD_CHAR;
    add_char_params.uuid_type                = ble_uuid.type;
    add_char_params.max_len                  = BLE_EPD_MAX_DATA_LEN;
    add_char_params.init_len                 = sizeof(uint8_t);
    add_char_params.is_var_len               = true;
    add_char_params.char_props.notify        = 1;
    add_char_params.char_props.write         = 1;
    add_char_params.char_props.write_wo_resp = 1;
    add_char_params.read_access              = SEC_OPEN;
    add_char_params.write_access             = SEC_OPEN;
    add_char_params.cccd_write_access        = SEC_OPEN;
 
    VERIFY_SUCCESS(characteristic_add(p_epd->service_handle, &add_char_params, &p_epd->char_handles));
 
    memset(&add_char_params, 0, sizeof(add_char_params));
    add_char_params.uuid                     = BLE_UUID_APP_VER;
    add_char_params.uuid_type                = ble_uuid.type;
    add_char_params.max_len                  = sizeof(uint8_t);
    add_char_params.init_len                 = sizeof(uint8_t);
    add_char_params.p_init_value             = &app_version;
    add_char_params.char_props.read          = 1;
    add_char_params.read_access              = SEC_OPEN;
 
    return characteristic_add(p_epd->service_handle, &add_char_params, &p_epd->app_ver_handles);
}
 
void ble_epd_sleep_prepare(ble_epd_t * p_epd)
{
    // Turn off led
    EPD_LED_OFF();
    // Prepare wakeup pin
    if (p_epd->config.wakeup_pin != 0xFF)
    {
        nrf_gpio_cfg_sense_input(p_epd->config.wakeup_pin, NRF_GPIO_PIN_NOPULL, NRF_GPIO_PIN_SENSE_HIGH);
    }
}
 
uint32_t ble_epd_init(ble_epd_t * p_epd)
{
    if (p_epd == NULL) return NRF_ERROR_NULL;
 
    // Initialize the service structure.
    p_epd->max_data_len = BLE_EPD_MAX_DATA_LEN;
    p_epd->conn_handle             = BLE_CONN_HANDLE_INVALID;
    p_epd->is_notification_enabled = false;
 
    epd_config_init(&p_epd->config);
    epd_config_read(&p_epd->config);
    
    // write default config
    if (epd_config_empty(&p_epd->config))
    {
        uint8_t cfg[] = EPD_CFG_DEFAULT;
        memcpy(&p_epd->config, cfg, sizeof(cfg));
        epd_config_write(&p_epd->config);
    }
 
    // load config
    EPD_GPIO_Load(&p_epd->config);
 
    // blink LED on start
    EPD_LED_BLINK();
 
    // Add the service.
    return epd_service_init(p_epd);
}
 
uint32_t ble_epd_string_send(ble_epd_t * p_epd, uint8_t * p_string, uint16_t length)
{
    if ((p_epd->conn_handle == BLE_CONN_HANDLE_INVALID) || (!p_epd->is_notification_enabled))
        return NRF_ERROR_INVALID_STATE;
    if (length > p_epd->max_data_len)
        return NRF_ERROR_INVALID_PARAM;
 
    ble_gatts_hvx_params_t hvx_params;
 
    memset(&hvx_params, 0, sizeof(hvx_params));
 
    hvx_params.handle = p_epd->char_handles.value_handle;
    hvx_params.p_data = p_string;
    hvx_params.p_len  = &length;
    hvx_params.type   = BLE_GATT_HVX_NOTIFICATION;
 
    return sd_ble_gatts_hvx(p_epd->conn_handle, &hvx_params);
}
 
void ble_epd_on_timer(ble_epd_t * p_epd, uint32_t timestamp, bool force_update)
{
    // Update calendar on 00:00:00, clock on every minute
    if (force_update || 
        (p_epd->display_mode == MODE_CALENDAR && timestamp % 86400 == 0) ||
        (p_epd->display_mode == MODE_CLOCK && timestamp % 60 == 0)) {
        epd_gui_update_event_t event = { p_epd, timestamp };
        app_sched_event_put(&event, sizeof(epd_gui_update_event_t), epd_gui_update);
    }
}

 

EPD_service.h文件代码如下:

/* Copyright (c) 2012 Nordic Semiconductor. All Rights Reserved.
 *
 * The information contained herein is property of Nordic Semiconductor ASA.
 * Terms and conditions of usage are described in detail in NORDIC
 * SEMICONDUCTOR STANDARD SOFTWARE LICENSE AGREEMENT.
 *
 * Licensees are granted free, non-transferable use of the information. NO
 * WARRANTY of ANY KIND is provided. This heading must NOT be removed from
 * the file.
 *
 */
 
#ifndef __EPD_SERVICE_H
#define __EPD_SERVICE_H
 
#include <stdint.h>
#include <stdbool.h>
#include "ble.h"
#include "ble_srv_common.h"
#if defined(S112)
#include "nrf_sdh_ble.h"
#endif
#include "sdk_config.h"
#include "EPD_driver.h"
#include "EPD_config.h"
#include "GUI.h"
 
/**@brief   Macro for defining a ble_hts instance.
 *
 * @param   _name   Name of the instance.
 * @hideinitializer
 */
#if defined(S112)
void ble_epd_evt_handler(ble_evt_t const * p_ble_evt, void * p_context);
 
#define BLE_EPD_BLE_OBSERVER_PRIO 2
#define BLE_EPD_DEF(_name)                                                                              \
    static ble_epd_t _name;                                                                             \
    NRF_SDH_BLE_OBSERVER(_name ## _obs,                                                                 \
                         BLE_EPD_BLE_OBSERVER_PRIO,                                                     \
                         ble_epd_evt_handler, &_name)
#else
#define BLE_EPD_DEF(_name) static ble_epd_t _name;
#endif
 
#define APP_VERSION 0x16
 
#define BLE_UUID_EPD_SVC_BASE              {{0XEC, 0X5A, 0X67, 0X1C, 0XC1, 0XB6, 0X46, 0XFB, \
                                             0X8D, 0X91, 0X28, 0XD8, 0X22, 0X36, 0X75, 0X62}}
#define BLE_UUID_EPD_SVC                   0x0001
#define BLE_UUID_EPD_CHAR                  0x0002
#define BLE_UUID_APP_VER                   0x0003
 
#define EPD_SVC_UUID_TYPE BLE_UUID_TYPE_VENDOR_BEGIN
 
#if defined(S112)
#define BLE_EPD_MAX_DATA_LEN (NRF_SDH_BLE_GATT_MAX_MTU_SIZE - 3)
#else
#define BLE_EPD_MAX_DATA_LEN  (GATT_MTU_SIZE_DEFAULT - 3) /**< Maximum length of data (in bytes) that can be transmitted to the peer. */
#endif
 
/**< EPD Service command IDs. */
enum EPD_CMDS
{
    EPD_CMD_SET_PINS     = 0x00,                        /**< set EPD pin mapping. */
    EPD_CMD_INIT         = 0x01,                        /**< init EPD display driver */
    EPD_CMD_CLEAR        = 0x02,                        /**< clear EPD screen */
    EPD_CMD_SEND_COMMAND = 0x03,                        /**< send command to EPD */
    EPD_CMD_SEND_DATA    = 0x04,                        /**< send data to EPD */
    EPD_CMD_REFRESH      = 0x05,                        /**< diaplay EPD ram on screen */
    EPD_CMD_SLEEP        = 0x06,                        /**< EPD enter sleep mode */
 
	EPD_CMD_SET_TIME     = 0x20,                        /** < set time with unix timestamp */
 
    EPD_CMD_WRITE_IMAGE  = 0x30,                        /** < write image data to EPD ram */
 
    EPD_CMD_SET_CONFIG   = 0x90,                        /**< set full EPD config */
    EPD_CMD_SYS_RESET    = 0x91,                        /**< MCU reset */
    EPD_CMD_SYS_SLEEP    = 0x92,                        /**< MCU enter sleep mode */
    EPD_CMD_CFG_ERASE    = 0x99,                        /**< Erase config and reset */
};
 
/**@brief EPD Service structure.
 *
 * @details This structure contains status information related to the service.
 */
typedef struct
{
    uint16_t                 service_handle;          /**< Handle of EPD Service (as provided by the S110 SoftDevice). */
    ble_gatts_char_handles_t char_handles;            /**< Handles related to the EPD characteristic (as provided by the SoftDevice). */
    ble_gatts_char_handles_t app_ver_handles;         /**< Handles related to the APP version characteristic (as provided by the SoftDevice). */
    uint16_t                 conn_handle;             /**< Handle of the current connection (as provided by the SoftDevice). BLE_CONN_HANDLE_INVALID if not in a connection. */
    uint16_t                 max_data_len;            /**< Maximum length of data (in bytes) that can be transmitted to the peer */
    bool                     is_notification_enabled; /**< Variable to indicate if the peer has enabled notification of the RX characteristic.*/
    epd_model_t              *epd;                    /**< current EPD model */
    epd_config_t             config;                  /**< EPD config */
    display_mode_t           display_mode;            /**< GUI display mode */
} ble_epd_t;
 
typedef struct
{
    ble_epd_t *p_epd;
    uint32_t timestamp;
} epd_gui_update_event_t;
 
#define EPD_GUI_SCHD_EVENT_DATA_SIZE sizeof(epd_gui_update_event_t)
 
/**@brief Function for preparing sleep mode.
 *
 * @param[in] p_epd       EPD Service structure.
 */
void ble_epd_sleep_prepare(ble_epd_t * p_epd);
 
/**@brief Function for initializing the EPD Service.
 *
 * @param[out] p_epd      EPD Service structure. This structure must be supplied
 *                        by the application. It is initialized by this function and will
 *                        later be used to identify this particular service instance.
 *
 * @retval NRF_SUCCESS If the service was successfully initialized. Otherwise, an error code is returned.
 * @retval NRF_ERROR_NULL If either of the pointers p_epd or p_epd_init is NULL.
 */
uint32_t ble_epd_init(ble_epd_t * p_epd);
 
/**@brief Function for handling the EPD Service's BLE events.
 *
 * @details The EPD Service expects the application to call this function each time an
 * event is received from the S110 SoftDevice. This function processes the event if it
 * is relevant and calls the EPD Service event handler of the
 * application if necessary.
 *
 * @param[in] p_epd       EPD Service structure.
 * @param[in] p_ble_evt   Event received from the S110 SoftDevice.
 */
void ble_epd_on_ble_evt(ble_epd_t * p_epd, ble_evt_t * p_ble_evt);
 
/**@brief Function for sending a string to the peer.
 *
 * @details This function sends the input string as an RX characteristic notification to the
 *          peer.
 *
 * @param[in] p_epd       Pointer to the EPD Service structure.
 * @param[in] p_string    String to be sent.
 * @param[in] length      Length of the string.
 *
 * @retval NRF_SUCCESS If the string was sent successfully. Otherwise, an error code is returned.
 */
uint32_t ble_epd_string_send(ble_epd_t * p_epd, uint8_t * p_string, uint16_t length);
 
void ble_epd_on_timer(ble_epd_t * p_epd, uint32_t timestamp, bool force_update);
 
#endif // EPD_BLE_H__
 
/** @} */

 

第一部分:EPD_service.c文件学习

#include <string.h>
#include "sdk_macros.h"
#include "ble_srv_common.h"
#include "nrf_delay.h"
#include "nrf_gpio.h"
#include "nrf_pwr_mgmt.h"
#include "app_scheduler.h"
#include "EPD_service.h"
#include "nrf_log.h"
 
#if defined(S112)
//#define EPD_CFG_DEFAULT {0x14, 0x13, 0x06, 0x05, 0x04, 0x03, 0x02, 0x03, 0xFF, 0x12, 0x07} // 52811
#define EPD_CFG_DEFAULT {0x14, 0x13, 0x12, 0x11, 0x10, 0x0F, 0x0E, 0x03, 0xFF, 0x0D, 0x02} // 52810
#else
//#define EPD_CFG_DEFAULT {0x05, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x01, 0x07}
#endif
 
#ifndef EPD_CFG_DEFAULT
#define EPD_CFG_DEFAULT {0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x03, 0x09, 0x03}
#endif
 
// defined in main.c
extern uint32_t timestamp(void);
extern void set_timestamp(uint32_t timestamp);
extern void sleep_mode_enter(void);

 

以下是对这段代码的逐行解释和关键功能说明:

一、头文件包含

#include <string.h>          // C标准字符串处理库(如memcpy、memset)
#include "sdk_macros.h"      // Nordic SDK宏定义(如VERIFY_SUCCESS、UNUSED_PARAMETER)
#include "ble_srv_common.h"  // BLE服务通用头文件(特征值添加、安全权限等)
#include "nrf_delay.h"       // 延时函数(nrf_delay_ms等)
#include "nrf_gpio.h"        // GPIO控制接口(引脚配置、电平操作)
#include "nrf_pwr_mgmt.h"    // 电源管理(低功耗模式、复位等)
#include "app_scheduler.h"   // 应用调度器(事件队列管理,异步执行任务)
#include "EPD_service.h"     // 自定义电子纸服务头文件(核心数据结构、函数声明)
#include "nrf_log.h"         // 日志模块(调试输出,如NRF_LOG_DEBUG)

 

作用

  • 引入 Nordic SDK 底层硬件操作接口(GPIO、电源、延时)。
  • 包含 BLE 协议栈相关工具(服务创建、特征值管理)。
  • 依赖自定义的电子纸服务头文件,实现业务逻辑。

二、条件编译与默认配置

1. 针对 S112 SoftDevice 的配置
#if defined(S112)
    //#define EPD_CFG_DEFAULT {0x14, 0x13, 0x06, 0x05, 0x04, 0x03, 0x02, 0x03, 0xFF, 0x12, 0x07} // 注释的52811配置
    #define EPD_CFG_DEFAULT {0x14, 0x13, 0x12, 0x11, 0x10, 0x0F, 0x0E, 0x03, 0xFF, 0x0D, 0x02} // nRF52810默认配置
#else
    //#define EPD_CFG_DEFAULT {0x05, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x01, 0x07} // 非S112的默认配置(注释状态)
#endif

 

逻辑
  • 当编译时定义了 S112(通常对应 Nordic S112 SoftDevice 版本),使用针对 nRF52810 的默认配置(11 字节数组)。
  • 注释的配置可能用于其他芯片(如 nRF52811),当前未启用。
  • #else 分支预留了非 S112 的配置,但代码中被注释,实际使用默认兜底配置。
2. 兜底默认配置
#ifndef EPD_CFG_DEFAULT
    #define EPD_CFG_DEFAULT {0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x03, 0x09, 0x03} // 10字节数组
#endif

 

作用
  • 若上述条件编译未定义 EPD_CFG_DEFAULT(例如未启用 S112 且默认配置被注释),则使用此兜底配置。
  • 数组内容通常对应电子纸的硬件参数(如 GPIO 引脚编号、寄存器默认值等),不同芯片 / 型号需匹配不同参数。

三、外部函数声明

// 在main.c中定义
extern uint32_t timestamp(void);       // 获取系统时间戳(单位:秒)
extern void set_timestamp(uint32_t);  // 设置系统时间戳
extern void sleep_mode_enter(void);   // 进入低功耗睡眠模式

说明

  • extern 关键字声明这些函数在其他文件(如 main.c)中实现,当前文件可直接调用。
  • timestamp:用于电子纸显示时间、日历等场景,提供时间基准。
  • sleep_mode_enter:配合电源管理,降低设备待机功耗,是低功耗设计的核心接口。

四、关键概念与应用场景

1. 条件编译的意义
  • 硬件适配:不同 Nordic 芯片(如 nRF52810、nRF52832)或 SoftDevice 版本(S110、S112)可能需要不同的寄存器配置或引脚定义,通过 #if defined(S112) 等条件编译区分。
  • 代码复用:同一套逻辑支持多种硬件平台,减少重复开发。
2. 默认配置数组的作用

EPD_CFG_DEFAULT 通常包含以下信息(示例):

  • GPIO 引脚编号:如 MOSI、SCLK、CS、DC、RST 等控制电子纸的引脚。
  • 电子纸型号 ID:用于驱动层识别屏幕型号(如 2.13 寸、2.7 寸等)。
  • 电源管理参数:如默认电压、复位延迟等。

示例场景
当设备首次启动或配置数据丢失时,ble_epd_init 会将此默认配置写入存储,作为电子纸的初始工作参数。

3. 低功耗与时间管理
  • sleep_mode_enter:通过关闭非必要外设、进入 CPU 休眠模式降低功耗,常用于蓝牙断开后的待机状态。
  • timestamp:结合电子纸的日历 / 时钟显示功能,定时触发界面更新(如每分钟刷新时钟,每天刷新日历)。

五、潜在扩展与调试

  1. 修改默认配置
    • 若更换电子纸型号或硬件平台,需根据实际引脚和寄存器参数调整 EPD_CFG_DEFAULT 数组。
    • 例如,若使用 nRF52811 芯片,需取消注释对应配置并调整数组长度和数值。
  2. 调试与日志
    • 启用 nrf_log.h 中的调试宏(如 NRF_LOG_DEBUG),输出配置加载过程、时间戳更新等信息,验证默认配置是否正确生效。
  3. 硬件兼容性
    • 确保 EPD_CFG_DEFAULT 中的引脚编号与实际硬件电路一致(如 nrf_gpio.h 中的引脚定义),避免因引脚错误导致电子纸无法通信。

总结

这段代码是电子纸服务的基础配置部分,通过条件编译适配不同硬件平台,利用默认配置数组初始化电子纸硬件参数,并依赖外部时间和低功耗接口实现核心功能。理解其配置逻辑是后续开发(如驱动适配、功能扩展)的关键。


函数epd_gui_update

static void epd_gui_update(void * p_event_data, uint16_t event_size)
{
    epd_gui_update_event_t *event = (epd_gui_update_event_t *)p_event_data;
    ble_epd_t *p_epd = event->p_epd;
 
    EPD_GPIO_Init();
    epd_model_t *epd = epd_init((epd_model_id_t)p_epd->config.model_id);
    gui_data_t data = {
        .bwr             = epd->bwr,
        .width           = epd->width,
        .height          = epd->height,
        .timestamp       = event->timestamp,
        .temperature     = epd->drv->read_temp(),
        .voltage         = EPD_ReadVoltage(),
    };
    DrawGUI(&data, epd->drv->write_image, p_epd->display_mode);
    epd->drv->refresh();
    EPD_GPIO_Uninit();
}

 

一、函数声明与参数

static void epd_gui_update(void * p_event_data, uint16_t event_size)

 

作用:更新电子纸(EPD)屏幕的显示内容,属于界面渲染的核心函数。

  • 参数
    • p_event_data:指向事件数据的指针,强制转换为 epd_gui_update_event_t 结构体(包含 ble_epd_t 指针和时间戳)。
    • event_size:事件数据的大小(实际使用中可能未严格校验,但需确保与结构体大小一致)。
  • 静态函数:仅在当前文件内可见,属于内部实现细节。
  • epd_gui_update_event_t结构体变量定义如下:

typedef struct
{
ble_epd_t *p_epd;
uint32_t timestamp;
} epd_gui_update_event_t;

其中:ble_epd_t定义如下:

typedef struct{
    uint16_t                 service_handle;          /**< EPD服务句柄(由S110 SoftDevice提供) */
 ble_gatts_char_handles_t char_handles;            /**< 与EPD主特征相关的句柄(由SoftDevice提供) */
ble_gatts_char_handles_t app_ver_handles;         /**< 与APP版本特征相关的句柄(由SoftDevice提供) */
    uint16_t                 conn_handle;             /**< 当前连接句柄(由SoftDevice提供)
                                                         * 若未连接则为BLE_CONN_HANDLE_INVALID 
                                                         */
    uint16_t                 max_data_len;            /**< 可传输至对端的最大数据长度(字节) */
    bool                     is_notification_enabled; /**< 指示对端是否已启用RX特征的通知功能 */
    epd_model_t              *epd;                    /**< 当前使用的EPD型号指针 */
    epd_config_t             config;                  /**< EPD配置参数 */
    display_mode_t           display_mode;            /**< GUI显示模式 */
} ble_epd_t;

 

其中ble_gatts_char_handles_t结构体定义如下:

typedef struct
{
  uint16_t          value_handle;       /**< 特征值句柄 - 指向实际存储特征值的属性 */
  uint16_t          user_desc_handle;   /**< 用户描述符句柄 - 指向特征的文本描述信息
                                           * 若不支持则为BLE_GATT_HANDLE_INVALID */
  uint16_t          cccd_handle;        /**< 客户端特征配置描述符句柄 - 用于启用/禁用通知或指示
                                           * 若不支持则为BLE_GATT_HANDLE_INVALID */
  uint16_t          sccd_handle;        /**< 服务器特征配置描述符句柄 - 用于服务器端配置
                                           * 若不支持则为BLE_GATT_HANDLE_INVALID */
} ble_gatts_char_handles_t;

 

其中EPD_GPIO_Init函数定义如下:

void EPD_GPIO_Init(void)
{
    // 引用计数保护:避免重复初始化
    if (m_driver_refs++ > 0) return;
 
    // 配置基本控制引脚的方向
    pinMode(EPD_DC_PIN, OUTPUT);     // 数据/命令控制引脚
    pinMode(EPD_RST_PIN, OUTPUT);    // 复位控制引脚
    pinMode(EPD_BUSY_PIN, INPUT);    // 忙状态指示引脚
 
    // 配置SPI接口参数
    nrf_drv_spi_config_t spi_config = NRF_DRV_SPI_DEFAULT_CONFIG;
    spi_config.sck_pin = EPD_SCLK_PIN;    // 时钟引脚
    spi_config.mosi_pin = EPD_MOSI_PIN;   // 主输出从输入引脚
    spi_config.ss_pin = EPD_CS_PIN;       // 片选引脚
    
    // 根据Nordic SDK版本选择适当的初始化函数
#if defined(S112)
    APP_ERROR_CHECK(nrf_drv_spi_init(&spi, &spi_config, NULL, NULL));
#else
    APP_ERROR_CHECK(nrf_drv_spi_init(&spi, &spi_config, NULL));
#endif
 
    // 可选引脚配置:根据引脚定义是否有效决定是否配置
    if (EPD_BS_PIN != 0xFF) {
        pinMode(EPD_BS_PIN, OUTPUT);
        digitalWrite(EPD_BS_PIN, LOW);    // 选择工作模式
    }
    if (EPD_EN_PIN != 0xFF) {
        pinMode(EPD_EN_PIN, OUTPUT);
        digitalWrite(EPD_EN_PIN, HIGH);   // 使能显示模块
    }
 
    // 设置默认控制信号状态
    digitalWrite(EPD_DC_PIN, LOW);        // 初始化为命令模式
    digitalWrite(EPD_RST_PIN, HIGH);      // 取消复位状态
 
    // 可选LED指示引脚配置
    if (EPD_LED_PIN != 0xFF)
        pinMode(EPD_LED_PIN, OUTPUT);
 
    // 打开LED指示
    EPD_LED_ON();
}

 

关键点说明

1.  引用计数保护: m_driver_refs 是一个全局变量,用于跟踪初始化次数。 第一次调用时,m_driver_refs 从 0 变为 1,执行初始化;后续调用时直接返回,避免重复配置硬件。

2.  SPI 接口配置: 使用 Nordic SDK 的 SPI 驱动,配置了时钟、数据和片选引脚。 ◦ 条件编译部分处理不同 SDK 版本的 API 差异,确保兼容性。

3.  可选引脚处理: ◦ 代码使用 0xFF 作为无效引脚的标志值。 ◦ 仅当引脚定义有效时才进行配置,增加了代码的灵活性和可移植性。

4.  状态控制: ◦ 初始化完成后,将 DC 引脚设为低电平(命令模式),RST 引脚设为高电平(正常工作状态)。 ◦ 如果定义了 LED 引脚,则将其配置为输出并点亮,作为初始化成功的指示。    这个初始化函数为电子纸显示模块提供了必要的硬件接口配置,确保后续的显示操作能够正常进行。

二、核心流程解析

1. 数据解析与硬件初始化
epd_gui_update_event_t *event = (epd_gui_update_event_t *)p_event_data;
ble_epd_t *p_epd = event->p_epd; // 获取BLE服务状态结构体
 
EPD_GPIO_Init(); // 初始化电子纸所需的GPIO引脚(如CS、DC、RST等)

 

步骤
    1. 从事件数据中提取 ble_epd_t 指针(p_epd),该结构体包含电子纸的配置和状态(如型号、显示模式、连接句柄等)。
    2. 调用 EPD_GPIO_Init() 初始化电子纸的控制引脚,确保硬件通信接口就绪(如设置引脚为输出模式、初始化 SPI/I2C 等)。
2. 电子纸驱动初始化
epd_model_t *epd = epd_init((epd_model_id_t)p_epd->config.model_id); 
  • 作用
    • 根据 p_epd->config.model_id(电子纸型号 ID,如 2.13 寸、2.7 寸等)调用 epd_init 函数,获取对应型号的驱动实例 epd_model_t
    • epd_model_t 结构体封装了该型号电子纸的硬件参数(如宽度、高度、黑白反转标志 bwr)和驱动函数指针(如 write_imagerefreshread_temp)。
  • 关键点
    • 型号 ID 由配置文件或 BLE 命令动态设置,实现多型号适配。
    • epd_init 通常会读取配置中的引脚参数(如 MOSI、SCLK 等),并完成驱动层的初始化(如发送初始化命令序列)。

epd_init函数代码如下 :

epd_model_t *epd_init(epd_model_id_t id)
{
    // 遍历预定义的EPD模型数组,查找匹配ID的模型
    for (uint8_t i = 0; i < ARRAY_SIZE(epd_models); i++) {
        if (epd_models[i]->id == id) {
            EPD = epd_models[i];
        }
    }
    
    // 如果未找到匹配的模型,默认使用数组中的第一个模型
    if (EPD == NULL) EPD = epd_models[0];
    
    // 调用找到的模型对应的驱动初始化函数
    EPD->drv->init();
    
    // 返回初始化后的EPD模型指针
    return EPD;
}

 

关键点说明

1.  驱动查找机制: ◦ 函数遍历预定义的 epd_models 数组,该数组包含了系统支持的所有 EPD 型号 ◦ 通过比较每个模型的 id 字段与传入的 id 参数,找到匹配的驱动

2.  默认驱动处理: ◦ 如果未找到匹配的型号 ID,函数会默认使用 epd_models 数组中的第一个驱动 ◦ 这确保了即使传入无效的型号 ID,系统仍能正常工作

3.  驱动初始化: ◦ 每个 EPD 模型包含一个驱动结构指针 drv ◦ 通过 EPD->drv->init() 调用特定型号的初始化函数,完成硬件配置

4.  返回值: ◦ 函数返回初始化后的 EPD 模型指针,供后续显示操作使用    这种设计实现了 EPD 驱动的模块化和可扩展性,允许系统支持多种不同型号的电子纸显示屏,同时保持统一的初始化接口。

3. 构建显示数据结构体
gui_data_t data = {
    .bwr             = epd->bwr,        // 黑白反转标志(用于反色显示)
    .width           = epd->width,      // 屏幕宽度(像素)
    .height          = epd->height,     // 屏幕高度(像素)
    .timestamp       = event->timestamp,// 时间戳(用于显示时间/日历)
    .temperature     = epd->drv->read_temp(), // 读取温度传感器数据
    .voltage         = EPD_ReadVoltage(),       // 读取电源电压
};
gui_data_t 作用:存储绘制界面所需的所有数据,传递给 DrawGUI 函数。
  • 字段解析
    • bwr:控制像素颜色反转(如黑色背景白色字体或反之),由电子纸型号决定默认值。
    • width/height:屏幕分辨率,决定绘制区域大小。
    • timestamp:来自事件数据,用于更新时间显示(如时钟、日历)。
    • temperature/voltage:硬件状态数据,用于显示设备状态或环境信息。
  • 数据来源
    • epd->drv->read_temp():调用驱动层函数读取温度传感器(可能集成在电子纸模块中)。
    • EPD_ReadVoltage():通过 ADC 或硬件电路获取当前供电电压。
4. 绘制界面
DrawGUI(&data, epd->drv->write_image, p_epd->display_mode); // 绘制界面  
  • DrawGUI 函数
    • 作用:根据 gui_data_t 中的参数和显示模式(p_epd->display_mode),生成待显示的图像数据,并通过 epd->drv->write_image 函数写入电子纸的显存。
    • 参数
      • &data:显示数据结构体(包含分辨率、时间、状态等)。
      • epd->drv->write_image:驱动层提供的写图像函数指针,用于将像素数据传输到电子纸。
      • p_epd->display_mode:显示模式(如 MODE_CLOCKMODE_CALENDARMODE_CUSTOM_IMAGE 等),决定绘制内容(时钟界面、日历界面或自定义图像)。
  • epd->drv->refresh()
    • 调用驱动层的刷新函数,将显存中的数据实际渲染到电子纸屏幕上(可能触发全刷新或局部刷新,取决于型号和驱动实现)。

DrawGUI函数代码如下:

void DrawGUI(gui_data_t *data, buffer_callback draw, display_mode_t mode)
{
    // 初始化时间结构和农历日期结构
    tm_t tm = {0};
    struct Lunar_Date Lunar;
 
    // 将时间戳转换为可读的时间格式
    transformTime(data->timestamp, &tm);
 
    // 创建图形上下文对象
    Adafruit_GFX gfx;
 
    // 根据是否支持黑白红三色显示,选择不同的初始化方式
    if (data->bwr)
      GFX_begin_3c(&gfx, data->width, data->height, PAGE_HEIGHT);
    else
      GFX_begin(&gfx, data->width, data->height, PAGE_HEIGHT);
 
    // 开始绘制过程(分页渲染)
    GFX_firstPage(&gfx);
    do {
        // 清屏为白色
        GFX_fillScreen(&gfx, GFX_WHITE);
 
        // 计算当前日期对应的农历日期
        LUNAR_SolarToLunar(&Lunar, tm.tm_year + YEAR0, tm.tm_mon + 1, tm.tm_mday);
 
        // 根据显示模式选择不同的绘制函数
        switch (mode) {
            case MODE_CALENDAR:
                DrawCalendar(&gfx, &tm, &Lunar, data);
                break;
            case MODE_CLOCK:
                DrawClock(&gfx, &tm, &Lunar, data);
                break;
            default:
                break;
        }
    } while(GFX_nextPage(&gfx, draw));
 
    // 结束绘制过程,释放资源
    GFX_end(&gfx);
}

 

关键点说明

1.  时间处理: ◦ 使用 transformTime 函数将时间戳转换为 tm_t 结构体 ◦ 通过 LUNAR_SolarToLunar 计算对应的农历日期,支持阴阳历同时显示

2.  图形上下文初始化: ◦ 根据 data->bwr 判断是否支持黑白红三色显示 ◦ 调用不同的初始化函数,为不同类型的显示屏准备绘图环境

3.  分页渲染机制: ◦ 电子纸显示屏通常内存有限,采用分页渲染方式 ◦ GFX_firstPage 开始绘制,GFX_nextPage 循环处理每一页 ◦ draw 回调函数用于将绘制的内容刷新到实际显示屏

4.  模式切换: ◦ MODE_CALENDAR:绘制日历视图,可能包含月历、日程等信息 ◦ MODE_CLOCK:绘制时钟视图,可能包含时、分、秒显示

5.  资源管理: ◦ 使用 GFX_end 函数结束绘制过程,释放图形上下文资源    这种设计模式使代码能够灵活支持不同类型的电子纸显示屏和多种显示模式,同时通过分页渲染解决了内存限制问题。


GFX_begin函数代码如下:


void GFX_begin(Adafruit_GFX *gfx, int16_t w, int16_t h, int16_t buffer_height) {
  // 清空图形上下文内存
  memset(gfx, 0, sizeof(Adafruit_GFX));
  memset(&gfx->u8g2, 0, sizeof(gfx->u8g2));
  
  // 设置显示屏物理尺寸
  gfx->WIDTH = gfx->_width = w;
  gfx->HEIGHT = gfx->_height = h;
  
  // 配置u8g2库的绘图回调函数
  gfx->u8g2.draw_hv_line = GFX_u8g2_draw_hv_line;
  
  // 分配帧缓冲区内存
  // 每个像素用1位表示(黑白),每行宽度按字节对齐
  gfx->buffer = malloc(((gfx->WIDTH + 7) / 8) * buffer_height);
  
  // 设置分页渲染参数
  gfx->page_height = buffer_height;
  gfx->total_pages = (gfx->HEIGHT / gfx->page_height) + (gfx->HEIGHT % gfx->page_height > 0);
}

 

关键点说明

1.  内存初始化: ◦ 使用 memset 函数将图形上下文和 u8g2 子结构清零 ◦ 确保所有字段初始化为已知状态,避免未定义行为

2.  尺寸设置: ◦ WIDTH 和 HEIGHT:存储显示屏的物理像素尺寸 ◦ _width 和 _height:可能用于内部计算的工作尺寸

3.  绘图回调配置: ◦ gfx->u8g2.draw_hv_line = GFX_u8g2_draw_hv_line: ◦ 设置水平 / 垂直线绘制的回调函数 ◦ 这是 u8g2 图形库的扩展机制,允许自定义绘图行为

4.  帧缓冲区分配: ◦ ((gfx->WIDTH + 7) / 8):计算每行需要的字节数,向上取整到字节边界 ◦ 黑白显示模式下,每个像素只需 1 位,因此每 8 个像素共用 1 个字节 ◦ buffer_height 决定了每次渲染的垂直高度,影响内存使用和渲染效率

5.  分页渲染参数: ◦ page_height:每一页的高度,通常远小于显示屏总高度 ◦ total_pages:计算总页数,使用整除加余数判断确保覆盖整个屏幕

这种设计是电子纸显示系统的典型优化:

• 通过分页渲染减少内存需求

• 每一页独立渲染,最终合并显示

• 适合资源受限的嵌入式系统

分页渲染的优势在于:

• 即使显示屏很大 (如 2000×3000 像素),也只需分配一小部分内存

• 可以逐页更新显示内容,减少刷新时间

• 降低系统资源压力,提高响应速度


GFX_begin_3c函数代码:


void GFX_begin_3c(Adafruit_GFX *gfx, int16_t w, int16_t h, int16_t buffer_height) {
  // 调用标准初始化函数,设置基本参数
  GFX_begin(gfx, w, h, buffer_height);
  
  // 减半页高度,为每个物理页面分配两个逻辑页面(黑白和彩色)
  gfx->page_height = buffer_height / 2;
  
  // 设置彩色缓冲区的起始位置
  // 在黑白缓冲区之后分配相同大小的空间用于存储彩色信息
  gfx->color = gfx->buffer + ((gfx->WIDTH + 7) / 8) * gfx->page_height;
  
  // 重新计算总页数,考虑新的页面高度
  gfx->total_pages = (gfx->HEIGHT / gfx->page_height) + (gfx->HEIGHT % gfx->page_height > 0);
}

 

关键点说明

1.  继承与扩展:

◦ 复用 GFX_begin 的基本初始化逻辑,避免代码重复

◦ 在此基础上添加专门针对三色屏的配置

2.  内存布局优化:

◦ gfx->page_height = buffer_height / 2:

◦ 将每页物理高度减半,为每个物理页面分配两个逻辑页面

◦ 一个页面用于存储黑白像素数据

◦ 另一个页面用于存储彩色像素数据

3.  双缓冲区设计:

◦ gfx->color = …:

◦ 在黑白缓冲区之后分配连续内存用于彩色数据

◦ 两个缓冲区并行存储相同区域的不同颜色信息

◦ 每个像素在黑白缓冲区和彩色缓冲区中都有对应位置

4.  页数调整:

◦ 由于页面高度减半,总页数变为原来的两倍

◦ 这意味着渲染相同高度的内容需要处理更多页面

◦ 但每页数据量减少,整体内存占用不变

5.  性能与显示效果权衡:

◦ 三色屏需要更多渲染时间,因为每个区域需要处理两次

◦ 内存占用与黑白屏相同,但分页更细

◦ 这种设计允许同时控制黑白和彩色像素,实现混合颜色显示

三色屏驱动中的内存优化策略解析

这句话描述了三色电子纸显示系统中一种巧妙的内存管理策略,让我来详细解释:

基本概念

  1. 物理区域 vs 逻辑页面
    • 物理区域:显示屏上的实际物理空间
    • 逻辑页面:内存中用于存储显示数据的虚拟页面
  2. 为什么需要两个逻辑页面?
    • 三色电子纸需要同时控制两种不同的显示元素:
      • 黑白像素层:控制黑色和白色显示
      • 彩色像素层:控制彩色显示(通常是红色或黄色)

内存分配优化

在黑白屏驱动中:

  • 一个物理区域对应一个逻辑页面
  • 内存布局:[黑白数据 1][黑白数据 2]…

在三色屏驱动中:

  • 一个物理区域对应两个逻辑页面
  • 内存布局:[黑白数据 1][彩色数据 1][黑白数据 2][彩色数据 2]…

内存布局示意图

黑白屏内存布局:
┌─────────────────────────────┐
│ 物理区域1 → 黑白逻辑页面1   │
├─────────────────────────────┤
│ 物理区域2 → 黑白逻辑页面2   │
├─────────────────────────────┤
│ 物理区域3 → 黑白逻辑页面3   │
└─────────────────────────────┘
 
三色屏内存布局:
┌───────────────────────────────────────────┐
│ 物理区域1 → 黑白逻辑页面1 + 彩色逻辑页面1 │
├───────────────────────────────────────────┤
│ 物理区域2 → 黑白逻辑页面2 + 彩色逻辑页面2 │
├───────────────────────────────────────────┤
│ 物理区域3 → 黑白逻辑页面3 + 彩色逻辑页面3 │
└───────────────────────────────────────────┘

 

实际效果

通过将 buffer_height 减半:

  1. 每个物理区域的内存被平均分为两部分
  2. 前半部分存储黑白像素数据
  3. 后半部分存储彩色像素数据
  4. 总内存占用不变,但数据组织方式改变

技术优势

  1. 资源利用率
    • 在不增加内存的情况下支持彩色显示
    • 充分利用有限的嵌入式系统资源
  2. 显示灵活性
    • 可以独立控制黑白层和彩色层
    • 实现混合颜色效果(如彩色文本在黑白背景上)
  3. 驱动兼容性
    • 保留了与黑白屏驱动相同的总内存需求
    • 简化了驱动程序的上层接口设计

这种设计是嵌入式系统中典型的 “空间换时间” 优化策略,通过更精细的内存管理,在不增加硬件成本的情况下实现了功能增强。


GFX_firstPage函数代码:


void GFX_firstPage(Adafruit_GFX *gfx) {
  // 将整个显示缓冲区填充为白色(清空屏幕)
  GFX_fillScreen(gfx, GFX_WHITE);
  
  // 重置当前页码为0,表示从第一页开始渲染
  gfx->current_page = 0;
}

 

关键点说明
  1. 清屏操作
    • GFX_fillScreen(gfx, GFX_WHITE)
      • 将整个显示缓冲区填充为白色
      • 这是一个必要的初始化步骤,确保新内容不会与旧内容混合
      • 在黑白电子纸中,白色通常表示 “无变化” 状态
  2. 页码重置
    • gfx->current_page = 0
      • 页码从 0 开始计数,0 表示第一页
      • 这是分页渲染机制的关键,后续将逐页处理显示内容
  3. 与分页渲染的关系
    • 电子纸显示系统通常内存有限,无法一次渲染整个屏幕
    • 通过分页渲染,可以将大画面分解为多个小页面
    • 每个页面单独渲染后合并,最终显示完整内容
  4. 与 GFX_nextPage 的协作
    • GFX_firstPage 启动渲染过程
    • GFX_nextPage 处理后续页面
    • 它们共同构成了分页渲染的循环机制

GFX_nextpage函数代码


bool GFX_nextPage(Adafruit_GFX *gfx, buffer_callback callback) {
  // 计算当前页面在整个显示屏中的Y坐标
  int16_t page_y = gfx->current_page * gfx->page_height;
  
  // 计算当前页面的实际高度(处理最后一页可能不足page_height的情况)
  int16_t height = MIN(gfx->page_height, gfx->HEIGHT - page_y);
  
  // 如果提供了回调函数,则调用它将当前页面内容发送到显示屏
  if (callback)
    callback(gfx->buffer, gfx->color, 0, page_y, gfx->WIDTH, height);
  
  // 递增页码,准备处理下一页
  gfx->current_page++;
  
  // 清空缓冲区,为下一页渲染做准备
  GFX_fillScreen(gfx, GFX_WHITE);
  
  // 返回布尔值,表示是否还有更多页面需要处理
  return gfx->current_page < gfx->total_pages;
}

 

关键点说明
  1. 页面位置计算
    • page_y = gfx->current_page * gfx->page_height
      • 计算当前页面在整个显示屏中的垂直起始位置
      • 例如,第 0 页从 Y=0 开始,第 1 页从 Y=page_height 开始
  2. 高度处理
    • height = MIN(gfx->page_height, gfx->HEIGHT - page_y)
      • 确保最后一页不会超出显示屏边界
      • 当剩余高度不足一个完整页面时,使用实际剩余高度
  3. 回调函数机制
    • callback(gfx->buffer, gfx->color, 0, page_y, gfx->WIDTH, height)
      • 将当前页面的缓冲区数据传递给回调函数
      • 回调函数负责将数据发送到实际的显示硬件
      • 对于三色屏,同时传递黑白缓冲区和彩色缓冲区
  4. 状态更新
    • gfx->current_page++:递增页码,准备处理下一页
    • GFX_fillScreen(gfx, GFX_WHITE):清空缓冲区,为下一页渲染做准备
  5. 循环控制
    • return gfx->current_page < gfx->total_pages
      • 返回布尔值,指示是否还有更多页面需要处理
      • 这允许调用者使用循环结构处理所有页面

分页渲染流程示例

// 分页渲染的典型使用方式
GFX_firstPage(&gfx);
do {
    // 在当前页面上绘制内容
    GFX_drawText(&gfx, "Hello", 10, 10);
    // 其他绘图操作...
} while(GFX_nextPage(&gfx, display_callback));

 

技术背景

分页渲染是电子纸显示系统的核心优化技术:

  • 内存优化
    • 避免一次性分配整个显示屏所需的内存
    • 对于大尺寸显示屏(如 10.3 英寸,分辨率 2200×1650),完整帧缓冲区需要约 450KB 内存
    • 通过分页,只需分配单页缓冲区(如 10KB)即可
  • 性能优化
    • 减少单次刷新的数据量,加快显示更新速度
    • 可以实现部分更新,只刷新变化的页面
  • 兼容性
    • 支持不同尺寸的显示屏,只需调整总页数
    • 统一处理黑白屏和三色屏,简化驱动程序设计

这种设计模式使电子纸显示系统能够在资源受限的嵌入式环境中高效工作,同时提供良好的用户体验。

5. 刷新屏幕与释放硬件资源
epd->drv->refresh();
EPD_GPIO_Uninit(); // 关闭电子纸的GPIO引脚,释放资源
  • 刷新显示,将缓存内的数据显示到屏,调用驱动层的刷新函数,将显存中的数据实际渲染到电子纸屏幕上(可能触发全刷新或局部刷新,取决于型号和驱动实现)。
  • 作用:关闭 SPI/I2C 通信接口、复位引脚等,避免待机时的不必要功耗,为进入低功耗模式做准备
  • 这里以ssd1916驱动中的refresh来说明:
    static void SSD1619_Refresh(void)
    {
        // 获取当前EPD模型指针
        epd_model_t *EPD = epd_get();
     
        // 发送显示控制命令
        EPD_WriteCommand(CMD_DISP_CTRL1);
        EPD_WriteByte(0x80); // 反转红色RAM(适用于三色屏)
        EPD_WriteByte(0x00); // 单芯片应用模式
     
        // 记录刷新开始日志
        NRF_LOG_DEBUG("[EPD]: refresh begin\n");
        
        // 读取并记录当前温度(温度会影响刷新效果)
        NRF_LOG_DEBUG("[EPD]: temperature: %d\n", SSD1619_Read_Temp());
        
        // 触发全屏刷新,参数0xF7表示"强制刷新"
        SSD1619_Update(0xF7);
        
        // 等待刷新完成,最多等待30秒
        SSD1619_WaitBusy(30000);
        
        // 记录刷新结束日志
        NRF_LOG_DEBUG("[EPD]: refresh end\n");
     
        // 设置后续部分更新的区域为整个屏幕
        // 这一步非常重要,确保后续部分更新操作正常
        _setPartialRamArea(0, 0, EPD->width, EPD->height);
        
        // 发送电源关闭命令,参数0x83表示"部分更新模式下关闭电源"
        SSD1619_Update(0x83);
    }

    关键点说明

  1. 显示控制配置
    • CMD_DISP_CTRL1:显示控制命令 1
    • 0x80:设置红色 RAM 反转,用于三色屏的色彩校正
    • 0x00:配置为单芯片应用模式
  2. 温度监测
    • 电子纸刷新效果受温度影响较大
    • 驱动程序可能会根据温度自动调整刷新参数
    • 记录温度用于调试和性能分析
  3. 刷新模式
    • SSD1619_Update(0xF7)
      • 发送 “强制刷新” 命令
      • 会更新整个屏幕,包括黑白层和彩色层
      • 刷新过程较慢,但显示效果更稳定
  4. 忙状态等待
    • SSD1619_WaitBusy(30000)
      • 等待显示屏完成刷新操作
      • 超时时间 30 秒,防止程序卡死
      • 电子纸刷新通常需要几百毫秒到几秒不等
  5. 部分更新区域设置
    • _setPartialRamArea(0, 0, EPD->width, EPD->height)
      • 将部分更新区域设置为整个屏幕
      • 确保后续调用部分更新命令时能正常工作
      • 这是一个重要的初始化步骤,不能省略
  6. 电源管理
    • SSD1619_Update(0x83)
      • 发送 “部分更新模式下关闭电源” 命令
      • 电子纸在刷新完成后不需要持续供电
      • 降低功耗,延长电池寿命

技术背景

电子纸显示技术的特点:

  1. 双稳态特性
    • 刷新完成后无需持续供电即可保持显示内容
    • 非常适合低功耗应用(如电子书阅读器)
  2. 刷新类型
    • 全屏刷新:速度慢但效果好,用于显示大幅变化
    • 部分刷新:速度快但可能有残影,用于小区域更新
  3. 温度敏感性
    • 低温环境下刷新速度会显著变慢
    • 某些驱动芯片内置温度传感器自动调整参数
  4. 显示质量优化
    • 通常需要多次刷新循环以提高显示质量
    • 刷新参数(如波形)需要根据温度和内容类型调整

这个函数实现了完整的电子纸刷新周期,确保显示内容清晰、稳定,同时优化了功耗和响应时间。

 

三、关键设计逻辑

1. 驱动与业务逻辑分离
  • epd_model_t 与 epd_init:通过型号 ID 解耦不同电子纸的驱动实现,上层代码(如 epd_gui_update)只需调用统一接口(write_imagerefresh),无需关心具体型号的寄存器细节。
  • 优势:方便扩展支持新的电子纸型号,只需新增 epd_model_id_t 枚举值和对应的驱动实现即可。
2. 数据驱动的界面渲染
  • gui_data_t 结构体:将界面所需的所有数据(分辨率、时间、状态)封装为统一结构,DrawGUI 函数根据这些数据生成图像,实现数据与渲染逻辑的解耦。
  • 灵活性:支持动态切换显示模式(如从时钟切换到日历),只需修改 p_epd->display_mode 和 timestamp 即可,无需修改渲染逻辑。
3. 功耗优化
  • GPIO 初始化与释放:仅在更新界面时初始化 GPIO,更新完成后立即释放,减少硬件外设的启用时间。
  • 配合低功耗模式:函数执行完毕后,设备可通过 sleep_mode_enter 进入睡眠模式,进一步降低功耗(见外部函数 sleep_mode_enter)。

四、潜在扩展与调试

1. 功能扩展方向
  • 新增显示模式:在 display_mode_t 枚举中添加新值(如 MODE_WEATHER),并在 DrawGUI 中实现天气图标绘制逻辑。
  • 温度补偿:根据 temperature 数据调整电子纸刷新频率或电压(某些型号对温度敏感)。
  • 电压监控报警:当 voltage 低于阈值时,通过 BLE 通知客户端或显示警告界面。
2. 调试关键点
  • GPIO 初始化失败:若 EPD_GPIO_Init() 未正确配置引脚(如引脚编号错误),会导致电子纸无响应,需通过日志或示波器检查引脚电平。
  • 驱动初始化错误epd_init 返回 NULL 或无效指针时,需确认 model_id 是否正确,以及驱动层是否支持该型号。
  • 图像显示异常:若画面花屏或颜色错误,可能是 bwr 标志错误、显存写入顺序错误(如 MSB/LSB 问题)或刷新模式不正确(全刷 / 局部刷)。

五、总结

epd_gui_update 函数是电子纸显示的核心流程,负责协调硬件初始化、数据采集、界面渲染和资源释放。其设计通过驱动抽象和数据封装实现了灵活性与可扩展性,同时通过功耗优化适配低功耗设备场景。理解此函数有助于进一步开发自定义显示内容、优化刷新性能或适配新硬件型号。

epd_gui_update 函数简略脉落如下:

图片[1]-大佬tsl0092的nrf52811墨水屏4.2寸EPD_service.c文件学习-ELink墨水屏电子纸社区-FPGA CPLD-ChipDebug

接下一篇文章。

请登录后发表评论

    没有回复内容