first commit

This commit is contained in:
seahi committed 2026-08-31 15:05:13 +08:00
commit a2a0b9785a
41 files changed
+31663

No files matched your search

+4
View File
@@ -0,0 +1,4 @@
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(xInfo)
+5
View File
@@ -0,0 +1,5 @@
# 中文字体组件(Unifont 16×16 点阵,UTF-8 渲染引擎)
# 字体数据由 tools/gen_font.py 自动生成
idf_component_register(SRCS "src/ChineseFont.cpp" "src/font16cn_data.cpp"
INCLUDE_DIRS "include"
REQUIRES epdiy_bwr)
@@ -0,0 +1,53 @@
/**
* @file ChineseFont.h
* @brief 自定义 UTF-8 中文字体系统(16×16 点阵,基于 Unifont)。
*
* 提供 Paint_DrawString_UTF8() 函数,支持中英文混合渲染:
* - ASCII 字符 → 使用原有英文字体引擎
* - 中文字符 → 查自定义 UTF-8 点阵表
*
* 字体数据由 tools/gen_font.py 从 Unifont hex 文件自动生成,
* 存放在 font16cn_data.cpp 中。如需添加新字符,
* 编辑脚本中的 CHARS 列表并重新运行即可。
*/
#ifndef CHINESE_FONT_H
#define CHINESE_FONT_H
#include <cstdint>
/// 单个 UTF-8 中文字符的点阵数据
struct UTF8_Char {
uint8_t index[3]; ///< UTF-8 编码的 3 个字节
uint8_t matrix[32]; ///< 16×16 单色位图 = 16 行 × 2 字节/行 = 32 字节
};
/// UTF-8 中文字体描述
struct UTF8_Font {
const UTF8_Char *table; ///< 字符表(按 Unicode 排序)
uint16_t size; ///< 字符表中的条目数
uint16_t asciiWidth; ///< ASCII 字符宽度(像素)
uint16_t charWidth; ///< 中文字符宽度(像素)
uint16_t charHeight; ///< 字符高度(像素)
};
/// 全局字体实例(定义在 font16cn_data.cpp 中)
extern const UTF8_Font Font16CN;
/**
* @brief 在墨水屏上绘制 UTF-8 中英文混合字符串。
*
* ASCII 部分使用英文字体(sFONT),中文部分查 UTF-8 字体表。
* 不在字体表中的中文字符会被跳过(不显示,不会乱码)。
*
* @param x 起始 X 坐标
* @param y 起始 Y 坐标
* @param str UTF-8 编码的字符串
* @param cnFont 中文字体(UTF8_Font)
* @param fg 前景色
* @param bg 背景色
*/
void Paint_DrawString_UTF8(uint16_t x, uint16_t y, const char *str,
const UTF8_Font &cnFont,
uint16_t fg, uint16_t bg);
#endif // CHINESE_FONT_H
@@ -0,0 +1,97 @@
/**
* @file ChineseFont.cpp
* @brief UTF-8 中英文混合渲染实现。
*
* 核心逻辑:遍历 UTF-8 字符串,识别 ASCII 和多字节序列,
* 分别调用英文字体引擎和自定义中文点阵渲染。
*/
#include "ChineseFont.h"
#include "GUI_Paint.h"
// --------------------------------------------------------------------------
// 在指定位置绲制单个中文字符的 16×16 点阵
// --------------------------------------------------------------------------
static void drawCNChar(uint16_t x, uint16_t y,
const uint8_t *matrix, uint16_t w, uint16_t h,
uint16_t fg, uint16_t bg) {
// matrix 按行存储,每行 ceil(w/8) 字节,MSB 在左
uint16_t bytesPerRow = (w + 7) / 8;
for (uint16_t row = 0; row < h; row++) {
for (uint16_t col = 0; col < w; col++) {
uint8_t byte = matrix[row * bytesPerRow + col / 8];
bool pixel = (byte >> (7 - col % 8)) & 1;
Paint_SetPixel(x + col, y + row, pixel ? fg : bg);
}
}
}
// --------------------------------------------------------------------------
// 在字体表中查找指定 UTF-8 编码的字符
// --------------------------------------------------------------------------
static const UTF8_Char *findChar(const UTF8_Font &font,
uint8_t b0, uint8_t b1, uint8_t b2) {
// 字体表按 Unicode 排序,可以用二分查找
int lo = 0, hi = static_cast<int>(font.size) - 1;
while (lo <= hi) {
int mid = (lo + hi) / 2;
const UTF8_Char &entry = font.table[mid];
// 按 3 字节逐个比较
if (entry.index[0] < b0) { lo = mid + 1; continue; }
if (entry.index[0] > b0) { hi = mid - 1; continue; }
if (entry.index[1] < b1) { lo = mid + 1; continue; }
if (entry.index[1] > b1) { hi = mid - 1; continue; }
if (entry.index[2] < b2) { lo = mid + 1; continue; }
if (entry.index[2] > b2) { hi = mid - 1; continue; }
return &entry; // 找到
}
return nullptr; // 未收录
}
// --------------------------------------------------------------------------
// UTF-8 中英文混合字符串渲染
// --------------------------------------------------------------------------
void Paint_DrawString_UTF8(uint16_t x, uint16_t y, const char *str,
const UTF8_Font &cnFont,
uint16_t fg, uint16_t bg) {
uint16_t curX = x;
const auto *p = reinterpret_cast<const uint8_t *>(str);
while (*p != 0) {
if (*p <= 0x7F) {
// ---- ASCII 字符:用英文字体引擎绘制 ----
// 使用 Font16(与 16px 中文等高)
Paint_DrawChar(curX, y, static_cast<char>(*p), &Font16, fg, bg);
curX += cnFont.asciiWidth;
p += 1;
} else if ((*p & 0xE0) == 0xC0) {
// 2 字节 UTF-8(拉丁扩展等,跳过)
p += 2;
} else if ((*p & 0xF0) == 0xE0) {
// ---- 3 字节 UTF-8(CJK 汉字在此范围)----
uint8_t b0 = p[0], b1 = p[1], b2 = p[2];
const UTF8_Char *ch = findChar(cnFont, b0, b1, b2);
if (ch) {
drawCNChar(curX, y, ch->matrix,
cnFont.charWidth, cnFont.charHeight, fg, bg);
}
// 未找到的字符跳过(不显示,不会乱码)
curX += cnFont.charWidth;
p += 3;
} else if ((*p & 0xF8) == 0xF0) {
// 4 字节 UTF-8(emoji 等,跳过)
p += 4;
} else {
// 非法字节,跳过
p += 1;
}
}
}
File diff suppressed because it is too large. Load diff
+4
View File
@@ -0,0 +1,4 @@
file(GLOB_RECURSE srcs "src/*.c" "src/*.cpp")
idf_component_register(SRCS ${srcs}
INCLUDE_DIRS "include"
REQUIRES esp_driver_gpio esp_driver_spi esp_timer driver)
+40
View File
@@ -0,0 +1,40 @@
#ifndef _EPD_H_
#define _EPD_H_
#define EPD_WIDTH 400
#define EPD_HEIGHT 300
#define EPD_ARRAY EPD_WIDTH*EPD_HEIGHT/8
//Full screen update display
void EPD_HW_Init(void);
void EPD_HW_Init_180(void);
void EPD_WhiteScreen_ALL(const unsigned char* datasBW,const unsigned char* datasRW);
void EPD_WhiteScreen_White(void);
void EPD_WhiteScreen_Black(void);
void EPD_DeepSleep(void);
//Partial update display
void EPD_SetRAMValue_BaseMap(const unsigned char* datasBW,const unsigned char* datasRW);
void EPD_Dis_PartAll(const unsigned char * datas);
void EPD_Dis_Part(unsigned int x_start,unsigned int y_start,const unsigned char * datas,unsigned int PART_COLUMN,unsigned int PART_LINE);
void EPD_Dis_Part_Num(unsigned int x_startA,unsigned int y_startA,const unsigned char * datasA,
unsigned int x_startB,unsigned int y_startB,const unsigned char * datasB,
unsigned int x_startC,unsigned int y_startC,const unsigned char * datasC,
unsigned int x_startD,unsigned int y_startD,const unsigned char * datasD,
unsigned int PART_COLUMN,unsigned int PART_LINE
);
//Fast full screen refresh display
void EPD_HW_Init_Fast(void);
void EPD_WhiteScreen_ALL_Fast(const unsigned char* datasBW,const unsigned char* datasRW);
void EPD_WhiteScreen_White_Fast(void);
//GUI display
void EPD_HW_Init_GUI(void);
void EPD_Display(unsigned char *datasBW,unsigned char *datasRW);
#endif
+43
View File
@@ -0,0 +1,43 @@
#ifndef _EPD_SPI_H_
#define _EPD_SPI_H_
#include "driver/gpio.h"
#include "driver/spi_master.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#ifdef __cplusplus
extern "C" {
#endif
// Pin Definitions for ESP32-C3
// SPI2 Host is used for C3
#define EPD_SCK_PIN GPIO_NUM_4
#define EPD_MOSI_PIN GPIO_NUM_6
#define EPD_CS_PIN GPIO_NUM_7
#define EPD_DC_PIN GPIO_NUM_3
#define EPD_RST_PIN GPIO_NUM_2
#define EPD_BUSY_PIN GPIO_NUM_10
#define isEPD_W21_BUSY gpio_get_level(EPD_BUSY_PIN)
#define EPD_W21_RST_0 gpio_set_level(EPD_RST_PIN, 0)
#define EPD_W21_RST_1 gpio_set_level(EPD_RST_PIN, 1)
#define EPD_W21_DC_0 gpio_set_level(EPD_DC_PIN, 0)
#define EPD_W21_DC_1 gpio_set_level(EPD_DC_PIN, 1)
#define EPD_W21_CS_0 gpio_set_level(EPD_CS_PIN, 0)
#define EPD_W21_CS_1 gpio_set_level(EPD_CS_PIN, 1)
void EPD_SPI_Init(void);
void SPI_Write(unsigned char value);
void EPD_W21_WriteDATA(unsigned char datas);
void EPD_W21_WriteCMD(unsigned char command);
unsigned char EPD_W21_ReadDATA(void);
#define delay(ms) vTaskDelay((ms) / portTICK_PERIOD_MS)
#ifdef __cplusplus
}
#endif
#endif
+168
View File
@@ -0,0 +1,168 @@
#ifndef GUI_PAINT_H
#define GUI_PAINT_H
#include "fonts.h"
#include <stdint.h>
/**
* Image attributes
**/
#define UBYTE uint8_t
#define UWORD uint16_t
#define UDOUBLE uint32_t
/**
* Image attributes
**/
typedef struct {
UBYTE *Image;
UWORD Width;
UWORD Height;
UWORD WidthMemory;
UWORD HeightMemory;
UWORD Color;
UWORD Rotate;
UWORD Mirror;
UWORD WidthByte;
UWORD HeightByte;
UWORD Scale;
} PAINT;
extern PAINT Paint;
/**
* Display rotate
**/
#define ROTATE_0 0
#define ROTATE_90 90
#define ROTATE_180 180
#define ROTATE_270 270
/**
* Display Flip
**/
typedef enum {
MIRROR_NONE = 0x00,
MIRROR_HORIZONTAL = 0x01,
MIRROR_VERTICAL = 0x02,
MIRROR_ORIGIN = 0x03,
} MIRROR_IMAGE;
#define MIRROR_IMAGE_DFT MIRROR_NONE
/**
* image color
**/
#define WHITE 0xFF
#define BLACK 0x00
#define RED 0x00
// Color
#define BLACK0 0x03
#define WHITE0 0x00
#define YELLOW0 0x01
#define RED0 0x02
/*
#define IMAGE_BACKGROUND WHITE
#define FONT_FOREGROUND BLACK
#define FONT_BACKGROUND WHITE
*/
#define TRUE 1
#define FALSE 0
//4 Gray level
#define GRAY1 0x03 //Blackest
#define GRAY2 0x02
#define GRAY3 0x01 //gray
#define GRAY4 0x00 //white
/**
* The size of the point
**/
typedef enum {
DOT_PIXEL_1X1 = 1, // 1 x 1
DOT_PIXEL_2X2 , // 2 X 2
DOT_PIXEL_3X3 , // 3 X 3
DOT_PIXEL_4X4 , // 4 X 4
DOT_PIXEL_5X5 , // 5 X 5
DOT_PIXEL_6X6 , // 6 X 6
DOT_PIXEL_7X7 , // 7 X 7
DOT_PIXEL_8X8 , // 8 X 8
} DOT_PIXEL;
#define DOT_PIXEL_DFT DOT_PIXEL_1X1 //Default dot pilex
/**
* Point size fill style
**/
typedef enum {
DOT_FILL_AROUND = 1, // dot pixel 1 x 1
DOT_FILL_RIGHTUP , // dot pixel 2 X 2
} DOT_STYLE;
#define DOT_STYLE_DFT DOT_FILL_AROUND //Default dot pilex
/**
* Line style, solid or dashed
**/
typedef enum {
LINE_STYLE_SOLID = 0,
LINE_STYLE_DOTTED,
} LINE_STYLE;
/**
* Whether the graphic is filled
**/
typedef enum {
DRAW_FILL_EMPTY = 0,
DRAW_FILL_FULL,
} DRAW_FILL;
/**
* Custom structure of a time attribute
**/
typedef struct {
UWORD Year; //0000
UBYTE Month; //1 - 12
UBYTE Day; //1 - 30
UBYTE Hour; //0 - 23
UBYTE Min; //0 - 59
UBYTE Sec; //0 - 59
} PAINT_TIME;
extern PAINT_TIME sPaint_time;
//init and Clear
void Paint_NewImage(UBYTE *image, UWORD Width, UWORD Height, UWORD Rotate, UWORD Color);
void Paint_SelectImage(UBYTE *image);
void Paint_SetRotate(UWORD Rotate);
void Paint_SetMirroring(UBYTE mirror);
void Paint_SetPixel(UWORD Xpoint, UWORD Ypoint, UWORD Color);
void Paint_SetScale(UBYTE scale);
void Paint_Clear(UWORD Color);
void Paint_ClearWindows(UWORD Xstart, UWORD Ystart, UWORD Xend, UWORD Yend, UWORD Color);
//Drawing
void Paint_DrawPoint(UWORD Xpoint, UWORD Ypoint, UWORD Color, DOT_PIXEL Dot_Pixel, DOT_STYLE Dot_FillWay);
void Paint_DrawLine(UWORD Xstart, UWORD Ystart, UWORD Xend, UWORD Yend, UWORD Color, LINE_STYLE Line_Style, DOT_PIXEL Dot_Pixel);
void Paint_DrawRectangle(UWORD Xstart, UWORD Ystart, UWORD Xend, UWORD Yend, UWORD Color, DRAW_FILL Draw_Fill, DOT_PIXEL Dot_Pixel);
void Paint_DrawCircle(UWORD X_Center, UWORD Y_Center, UWORD Radius, UWORD Color, DRAW_FILL Draw_Fill, DOT_PIXEL Dot_Pixel);
//Display string
void Paint_DrawChar(UWORD Xstart, UWORD Ystart, const char Acsii_Char, sFONT* Font, UWORD Color_Foreground, UWORD Color_Background);
void Paint_DrawString_EN(UWORD Xstart, UWORD Ystart, const char * pString, sFONT* Font, UWORD Color_Foreground, UWORD Color_Background);
void Paint_DrawString_CN(UWORD Xstart, UWORD Ystart, const char * pString, cFONT* font, UWORD Color_Foreground, UWORD Color_Background);
void Paint_DrawNum(UWORD Xpoint, UWORD Ypoint, int32_t Nummber, sFONT* Font, UWORD Color_Foreground, UWORD Color_Background);
void Paint_DrawNumDecimals(UWORD Xpoint, UWORD Ypoint, double Nummber, sFONT* Font, UWORD Digit, UWORD Color_Foreground, UWORD Color_Background); // Able to display decimals
void Paint_DrawTime(UWORD Xstart, UWORD Ystart, PAINT_TIME *pTime, sFONT* Font, UWORD Color_Foreground, UWORD Color_Background);
//pic
void Paint_DrawBitMap(const unsigned char* image_buffer);
void Paint_DrawBitMap_Paste(const unsigned char* image_buffer, UWORD Xstart, UWORD Ystart, UWORD imageWidth, UWORD imageHeight, UBYTE flipColor);
//void Paint_DrawBitMap_Half(const unsigned char* image_buffer, UBYTE Region);
//void Paint_DrawBitMap_OneQuarter(const unsigned char* image_buffer, UBYTE Region);
//void Paint_DrawBitMap_OneEighth(const unsigned char* image_buffer, UBYTE Region);
void Paint_DrawBitMap_Block(const unsigned char* image_buffer, UBYTE Region);
//Color setting
void Paint_Color_Setting(void);
#endif
File diff suppressed because it is too large. Load diff
+63
View File
@@ -0,0 +1,63 @@
#ifndef FONTS_H
#define FONTS_H
/*锟斤拷锟斤拷锟斤拷锟轿拷锟斤拷藕锟�24 (32x41) */
#define MAX_HEIGHT_FONT 41
#define MAX_WIDTH_FONT 32
#define OFFSET_BITMAP
#ifdef __cplusplus
extern "C" {
#endif
/* Includes ------------------------------------------------------------------*/
#include <stdint.h>
//ASCII
typedef struct _tFont
{
const uint8_t *table;
uint16_t Width;
uint16_t Height;
} sFONT;
//GB2312
typedef struct // 锟斤拷锟斤拷锟斤拷模锟斤拷锟捷结构
{
unsigned char index[2]; // 锟斤拷锟斤拷锟斤拷锟斤拷锟斤拷锟斤拷
const char matrix[MAX_HEIGHT_FONT*MAX_WIDTH_FONT/8]; // 锟斤拷锟斤拷锟斤拷锟斤拷锟斤拷
}CH_CN;
typedef struct
{
const CH_CN *table;
uint16_t size;
uint16_t ASCII_Width;
uint16_t Width;
uint16_t Height;
}cFONT;
extern sFONT Font24;
extern sFONT Font20;
extern sFONT Font16;
extern sFONT Font12;
extern sFONT Font8;
extern cFONT Font12CN;
extern cFONT Font24CN;
//锟街匡拷锟侥硷拷
extern const CH_CN Font12CN_Table[];
#ifdef __cplusplus
}
#endif
#endif /* __FONTS_H */
/************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE****/
+441
View File
@@ -0,0 +1,441 @@
#include "EPD.h"
#include <EPD_SPI.h>
// Delay Functions
void delay_xms(unsigned int xms) { delay(xms); }
////////////////////////////////////E-paper
///demo//////////////////////////////////////////////////////////
// Busy function
void Epaper_READBUSY(void) {
delay_xms(10); // Wait for the display to assert the busy signal
while (1) { //=1 BUSY
if (isEPD_W21_BUSY == 0)
break;
vTaskDelay(10);
}
}
// Full screen update initialization
void EPD_HW_Init(void) {
EPD_W21_RST_0; // Module reset
delay_xms(10); // At least 10ms delay
EPD_W21_RST_1;
delay_xms(10); // At least 10ms delay
Epaper_READBUSY();
EPD_W21_WriteCMD(0x12); // SWRESET
Epaper_READBUSY();
EPD_W21_WriteCMD(0x01); // Driver output control
EPD_W21_WriteDATA((EPD_HEIGHT - 1) % 256);
EPD_W21_WriteDATA((EPD_HEIGHT - 1) / 256);
EPD_W21_WriteDATA(0x00);
EPD_W21_WriteCMD(0x11); // data entry mode
EPD_W21_WriteDATA(0x01);
EPD_W21_WriteCMD(0x44); // set Ram-X address start/end position
EPD_W21_WriteDATA(0x00);
EPD_W21_WriteDATA(EPD_WIDTH / 8 - 1);
EPD_W21_WriteCMD(0x45); // set Ram-Y address start/end position
EPD_W21_WriteDATA((EPD_HEIGHT - 1) % 256);
EPD_W21_WriteDATA((EPD_HEIGHT - 1) / 256);
EPD_W21_WriteDATA(0x00);
EPD_W21_WriteDATA(0x00);
EPD_W21_WriteCMD(0x3C); // BorderWavefrom
EPD_W21_WriteDATA(0x05);
EPD_W21_WriteCMD(0x18); // Read built-in temperature sensor
EPD_W21_WriteDATA(0x80);
EPD_W21_WriteCMD(0x4E); // set RAM x address count to 0;
EPD_W21_WriteDATA(0x00);
EPD_W21_WriteCMD(0x4F); // set RAM y address count to 0X199;
EPD_W21_WriteDATA((EPD_HEIGHT - 1) % 256);
EPD_W21_WriteDATA((EPD_HEIGHT - 1) / 256);
Epaper_READBUSY();
}
// Fast full screen update initialization
void EPD_HW_Init_Fast(void) // 1.6s
{
EPD_W21_RST_0; // Module reset
delay_xms(10); // At least 10ms delay
EPD_W21_RST_1;
delay_xms(10); // At least 10ms delay
EPD_W21_WriteCMD(0x12); // SWRESET
Epaper_READBUSY();
EPD_W21_WriteCMD(0x01); // Driver output control
EPD_W21_WriteDATA((EPD_HEIGHT - 1) % 256);
EPD_W21_WriteDATA((EPD_HEIGHT - 1) / 256);
EPD_W21_WriteDATA(0x01); // mirror
EPD_W21_WriteCMD(0x11); // data entry mode
EPD_W21_WriteDATA(0x01);
EPD_W21_WriteCMD(0x44); // set Ram-X address start/end position
EPD_W21_WriteDATA(0x00);
EPD_W21_WriteDATA(EPD_WIDTH / 8 - 1);
EPD_W21_WriteCMD(0x45); // set Ram-Y address start/end position
EPD_W21_WriteDATA((EPD_HEIGHT - 1) % 256);
EPD_W21_WriteDATA((EPD_HEIGHT - 1) / 256);
EPD_W21_WriteDATA(0x00);
EPD_W21_WriteDATA(0x00);
EPD_W21_WriteCMD(0x3C); // BorderWavefrom
EPD_W21_WriteDATA(0x05);
EPD_W21_WriteCMD(0x4E); // set RAM x address count to 0;
EPD_W21_WriteDATA(0x00);
EPD_W21_WriteCMD(0x4F); // set RAM y address count to 0X199;
EPD_W21_WriteDATA((EPD_HEIGHT - 1) % 256);
EPD_W21_WriteDATA((EPD_HEIGHT - 1) / 256);
Epaper_READBUSY();
EPD_W21_WriteCMD(0x18); // Read built-in temperature sensor
EPD_W21_WriteDATA(0x80);
EPD_W21_WriteCMD(0x22); // Load temperature value
EPD_W21_WriteDATA(0xB1);
EPD_W21_WriteCMD(0x20);
Epaper_READBUSY();
EPD_W21_WriteCMD(0x1A); // Write to temperature register
EPD_W21_WriteDATA(0x5a); // 90
EPD_W21_WriteDATA(0x00);
EPD_W21_WriteCMD(0x22); // Load temperature value
EPD_W21_WriteDATA(0x91);
EPD_W21_WriteCMD(0x20);
Epaper_READBUSY();
}
//////////////////////////////Display Update
///Function///////////////////////////////////////////////////////
// Full screen update update function
void EPD_Update(void) {
EPD_W21_WriteCMD(0x22); // Display Update Control
EPD_W21_WriteDATA(0xF7);
EPD_W21_WriteCMD(0x20); // Activate Display Update Sequence
Epaper_READBUSY();
}
// Fast full screen update update function
void EPD_Update_Fast(void) {
EPD_W21_WriteCMD(0x22); // Display Update Control
EPD_W21_WriteDATA(0xC7);
EPD_W21_WriteCMD(0x20); // Activate Display Update Sequence
Epaper_READBUSY();
}
// Full screen update update function (Basemap)
void EPD_Update_BaseMap(void) {
EPD_W21_WriteCMD(0x22); // Display Update Control
EPD_W21_WriteDATA(0xF4);
EPD_W21_WriteCMD(0x20); // Activate Display Update Sequence
Epaper_READBUSY();
}
// Partial update update function
void EPD_Part_Update(void) {
EPD_W21_WriteCMD(0x22); // Display Update Control
EPD_W21_WriteDATA(0x1C);
EPD_W21_WriteCMD(0x20); // Activate Display Update Sequence
Epaper_READBUSY();
}
//////////////////////////////Display Data Transfer
///Function////////////////////////////////////////////
// Full screen update display function
void EPD_WhiteScreen_ALL(const unsigned char *datasBW,
const unsigned char *datasRW) {
unsigned int i;
// Write Data
EPD_W21_WriteCMD(0x24); // Transfer old data
for (i = 0; i < EPD_ARRAY; i++) {
EPD_W21_WriteDATA(datasBW[i]); // Transfer the actual displayed data
}
EPD_W21_WriteCMD(0x26); // Transfer new data
for (i = 0; i < EPD_ARRAY; i++) {
EPD_W21_WriteDATA(~datasRW[i]); // Transfer the actual displayed data
}
EPD_Update();
}
// Fast full screen update display function
void EPD_WhiteScreen_ALL_Fast(const unsigned char *datasBW,
const unsigned char *datasRW) {
unsigned int i;
// Write Data
EPD_W21_WriteCMD(0x24); // Transfer old data
for (i = 0; i < EPD_ARRAY; i++) {
EPD_W21_WriteDATA(datasBW[i]); // Transfer the actual displayed data
}
EPD_W21_WriteCMD(0x26); // Transfer new data
for (i = 0; i < EPD_ARRAY; i++) {
EPD_W21_WriteDATA(~datasRW[i]); // Transfer the actual displayed data
}
EPD_Update_Fast();
}
// Clear screen display
void EPD_WhiteScreen_White(void) {
unsigned int i;
// Write Data
EPD_W21_WriteCMD(0x24); // Transfer old data
for (i = 0; i < EPD_ARRAY; i++) {
EPD_W21_WriteDATA(0xFF);
}
EPD_W21_WriteCMD(0x26); // Transfer new data
for (i = 0; i < EPD_ARRAY; i++) {
EPD_W21_WriteDATA(0x00); // Transfer the actual displayed data
}
EPD_Update();
}
// Clear screen display
void EPD_WhiteScreen_White_Fast(void) {
unsigned int i;
EPD_W21_WriteCMD(0x3c); // set border
EPD_W21_WriteDATA(
0x01); // 0x00=black £» 0x01=white £» 0x80=VCOM £» 0xc0=HiZ£¨default£©
// Write Data
EPD_W21_WriteCMD(0x24); // Transfer old data
for (i = 0; i < EPD_ARRAY; i++) {
EPD_W21_WriteDATA(0xFF);
}
EPD_W21_WriteCMD(0x26); // Transfer new data
for (i = 0; i < EPD_ARRAY; i++) {
EPD_W21_WriteDATA(0x00); // Transfer the actual displayed data
}
EPD_Update_Fast();
EPD_W21_WriteCMD(0x26); // Transfer new data
for (i = 0; i < EPD_ARRAY; i++) {
EPD_W21_WriteDATA(0xFF); // Transfer the actual displayed data
}
}
// Display all black
void EPD_WhiteScreen_Black(void) {
unsigned int i;
// Write Data
EPD_W21_WriteCMD(0x24); // Transfer old data
for (i = 0; i < EPD_ARRAY; i++) {
EPD_W21_WriteDATA(0xFF);
}
EPD_W21_WriteCMD(0x26); // Transfer new data
for (i = 0; i < EPD_ARRAY; i++) {
EPD_W21_WriteDATA(0xFF); // Transfer the actual displayed data
}
EPD_Update();
}
// Partial update of background display, this function is necessary, please do
// not delete it!!!
void EPD_SetRAMValue_BaseMap(const unsigned char *datasBW,
const unsigned char *datasRW) {
unsigned int i;
// Write Data
EPD_W21_WriteCMD(0x24); // Transfer old data
for (i = 0; i < EPD_ARRAY; i++) {
EPD_W21_WriteDATA(datasBW[i]); // Transfer the actual displayed data
}
EPD_W21_WriteCMD(0x26); // Transfer new data
for (i = 0; i < EPD_ARRAY; i++) {
EPD_W21_WriteDATA(~datasRW[i]); // Transfer the actual displayed data
}
EPD_Update_BaseMap();
Epaper_READBUSY();
// Write Data (update)
EPD_W21_WriteCMD(0x24); // Transfer old data
for (i = 0; i < EPD_ARRAY; i++) {
EPD_W21_WriteDATA(0xFF);
}
EPD_W21_WriteCMD(0x26); // Transfer new data
for (i = 0; i < EPD_ARRAY; i++) {
EPD_W21_WriteDATA(0xFF); // Transfer the actual displayed data
}
//////////////////////////////////////////////
}
// Partial update display
void EPD_Dis_Part(unsigned int x_start, unsigned int y_start,
const unsigned char *datas, unsigned int PART_COLUMN,
unsigned int PART_LINE) {
unsigned int i;
unsigned int x_end, y_end;
x_start = x_start / 8; // x address start
x_end = x_start + PART_LINE / 8 - 1; // x address end
y_start = y_start; // Y address start
y_end = y_start + PART_COLUMN - 1; // Y address end
EPD_W21_WriteCMD(0x44); // set RAM x address start/end
EPD_W21_WriteDATA(x_start); // x address start
EPD_W21_WriteDATA(x_end); // y address end
EPD_W21_WriteCMD(0x45); // set RAM y address start/end
EPD_W21_WriteDATA(y_start % 256); // y address start2
EPD_W21_WriteDATA(y_start / 256); // y address start1
EPD_W21_WriteDATA(y_end % 256); // y address end2
EPD_W21_WriteDATA(y_end / 256); // y address end1
EPD_W21_WriteCMD(0x4E); // set RAM x address count to 0;
EPD_W21_WriteDATA(x_start); // x start address
EPD_W21_WriteCMD(0x4F); // set RAM y address count to 0X127;
EPD_W21_WriteDATA(y_start % 256); // y address start2
EPD_W21_WriteDATA(y_start / 256); // y address start1
EPD_W21_WriteCMD(0x24); // Write Black and White image to RAM
for (i = 0; i < PART_COLUMN * PART_LINE / 8; i++) {
EPD_W21_WriteDATA(datas[i]);
}
EPD_Part_Update();
}
// Deep sleep function
void EPD_DeepSleep(void) {
EPD_W21_WriteCMD(0x10); // Enter deep sleep
EPD_W21_WriteDATA(0x01);
delay_xms(100);
}
// Partial update write address and data
void EPD_Dis_Part_RAM(unsigned int x_start, unsigned int y_start,
const unsigned char *datas, unsigned int PART_COLUMN,
unsigned int PART_LINE) {
unsigned int i;
unsigned int x_end, y_end;
x_start = x_start / 8; // x address start
x_end = x_start + PART_LINE / 8 - 1; // x address end
y_start = y_start - 1; // Y address start
y_end = y_start + PART_COLUMN - 1; // Y address end
EPD_W21_WriteCMD(0x44); // set RAM x address start/end
EPD_W21_WriteDATA(x_start); // x address start
EPD_W21_WriteDATA(x_end); // y address end
EPD_W21_WriteCMD(0x45); // set RAM y address start/end
EPD_W21_WriteDATA(y_start % 256); // y address start2
EPD_W21_WriteDATA(y_start / 256); // y address start1
EPD_W21_WriteDATA(y_end % 256); // y address end2
EPD_W21_WriteDATA(y_end / 256); // y address end1
EPD_W21_WriteCMD(0x4E); // set RAM x address count to 0;
EPD_W21_WriteDATA(x_start); // x start address
EPD_W21_WriteCMD(0x4F); // set RAM y address count to 0X127;
EPD_W21_WriteDATA(y_start % 256); // y address start2
EPD_W21_WriteDATA(y_start / 256); // y address start1
EPD_W21_WriteCMD(0x24); // Write Black and White image to RAM
for (i = 0; i < PART_COLUMN * PART_LINE / 8; i++) {
EPD_W21_WriteDATA(datas[i]);
}
}
// Num display
void EPD_Dis_Part_Num(unsigned int x_startA, unsigned int y_startA,
const unsigned char *datasA, unsigned int x_startB,
unsigned int y_startB, const unsigned char *datasB,
unsigned int x_startC, unsigned int y_startC,
const unsigned char *datasC, unsigned int x_startD,
unsigned int y_startD, const unsigned char *datasD,
unsigned int PART_COLUMN, unsigned int PART_LINE) {
EPD_Dis_Part_RAM(x_startA, y_startA, datasA, PART_COLUMN, PART_LINE);
EPD_Dis_Part_RAM(x_startB, y_startB, datasB, PART_COLUMN, PART_LINE);
EPD_Dis_Part_RAM(x_startC, y_startC, datasC, PART_COLUMN, PART_LINE);
EPD_Dis_Part_RAM(x_startD, y_startD, datasD, PART_COLUMN, PART_LINE);
EPD_Part_Update();
}
////////////////////////////////Other newly added
///functions////////////////////////////////////////////
// Display rotation 180 degrees initialization
void EPD_HW_Init_180(void) {
EPD_W21_RST_0; // Module reset
delay_xms(10); // At least 10ms delay
EPD_W21_RST_1;
delay_xms(10); // At least 10ms delay
Epaper_READBUSY();
EPD_W21_WriteCMD(0x12); // SWRESET
Epaper_READBUSY();
EPD_W21_WriteCMD(0x11); // data entry mode
EPD_W21_WriteDATA(0x02);
EPD_W21_WriteCMD(0x44); // set Ram-X address start/end position
EPD_W21_WriteDATA(EPD_WIDTH / 8 - 1);
EPD_W21_WriteDATA(0x00);
EPD_W21_WriteCMD(0x45); // set Ram-Y address start/end position
EPD_W21_WriteDATA(0x00);
EPD_W21_WriteDATA(0x00);
EPD_W21_WriteDATA((EPD_HEIGHT - 1) % 256);
EPD_W21_WriteDATA((EPD_HEIGHT - 1) / 256);
EPD_W21_WriteCMD(0x3C); // BorderWavefrom
EPD_W21_WriteDATA(0x01);
EPD_W21_WriteCMD(0x18);
EPD_W21_WriteDATA(0X80);
EPD_W21_WriteCMD(0x4E); // set RAM x address count to 0;
EPD_W21_WriteDATA(EPD_WIDTH / 8 - 1);
EPD_W21_WriteCMD(0x4F); // set RAM y address count to 0X199;
EPD_W21_WriteDATA(0x00);
EPD_W21_WriteDATA(0x00);
Epaper_READBUSY();
}
// GUI initialization
void EPD_HW_Init_GUI(void) {
EPD_W21_RST_0; // Module reset
delay_xms(10); // At least 10ms delay
EPD_W21_RST_1;
delay_xms(10); // At least 10ms delay
Epaper_READBUSY();
EPD_W21_WriteCMD(0x12); // SWRESET
Epaper_READBUSY();
EPD_W21_WriteCMD(0x01); // Driver output control
EPD_W21_WriteDATA((EPD_HEIGHT - 1) % 256);
EPD_W21_WriteDATA((EPD_HEIGHT - 1) / 256);
EPD_W21_WriteDATA(0x01); // mirror
EPD_W21_WriteCMD(0x11); // data entry mode
EPD_W21_WriteDATA(0x01);
EPD_W21_WriteCMD(0x44); // set Ram-X address start/end position
EPD_W21_WriteDATA(0x00);
EPD_W21_WriteDATA(EPD_WIDTH / 8 - 1);
EPD_W21_WriteCMD(0x45); // set Ram-Y address start/end position
EPD_W21_WriteDATA((EPD_HEIGHT - 1) % 256);
EPD_W21_WriteDATA((EPD_HEIGHT - 1) / 256);
EPD_W21_WriteDATA(0x00);
EPD_W21_WriteDATA(0x00);
EPD_W21_WriteCMD(0x3C); // BorderWavefrom
EPD_W21_WriteDATA(0x05);
EPD_W21_WriteCMD(0x18);
EPD_W21_WriteDATA(0X80);
EPD_W21_WriteCMD(0x4E); // set RAM x address count to 0;
EPD_W21_WriteDATA(0x00);
EPD_W21_WriteCMD(0x4F); // set RAM y address count to 0X199;
EPD_W21_WriteDATA((EPD_HEIGHT - 1) % 256);
EPD_W21_WriteDATA((EPD_HEIGHT - 1) / 256);
Epaper_READBUSY();
}
// GUI display
void EPD_Display(unsigned char *datasBW, unsigned char *datasRW) {
unsigned int i;
// Write Data
EPD_W21_WriteCMD(0x24); // Transfer old data
for (i = 0; i < EPD_ARRAY; i++) {
EPD_W21_WriteDATA(datasBW[i]); // Transfer the actual displayed data
}
EPD_W21_WriteCMD(0x26); // Transfer new data
for (i = 0; i < EPD_ARRAY; i++) {
EPD_W21_WriteDATA(~datasRW[i]); // Transfer the actual displayed data
}
EPD_Update();
}
/***********************************************************
end file
***********************************************************/
+78
View File
@@ -0,0 +1,78 @@
#include "EPD_SPI.h"
static spi_device_handle_t spi_handle;
static const char *TAG = "EPD_SPI";
void EPD_SPI_Init(void) {
esp_err_t ret;
// GPIO Config for DC, RST, CS
gpio_config_t io_conf = {};
io_conf.intr_type = GPIO_INTR_DISABLE;
io_conf.mode = GPIO_MODE_OUTPUT;
io_conf.pin_bit_mask = (1ULL<<EPD_DC_PIN) | (1ULL<<EPD_RST_PIN) | (1ULL<<EPD_CS_PIN);
io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
io_conf.pull_up_en = GPIO_PULLUP_DISABLE;
gpio_config(&io_conf);
// GPIO Config for BUSY
io_conf.mode = GPIO_MODE_INPUT;
io_conf.pin_bit_mask = (1ULL<<EPD_BUSY_PIN);
io_conf.pull_up_en = GPIO_PULLUP_ENABLE;
gpio_config(&io_conf);
EPD_W21_CS_1;
// SPI Bus Config
spi_bus_config_t buscfg = {};
buscfg.miso_io_num = -1;
buscfg.mosi_io_num = EPD_MOSI_PIN;
buscfg.sclk_io_num = EPD_SCK_PIN;
buscfg.quadwp_io_num = -1;
buscfg.quadhd_io_num = -1;
buscfg.max_transfer_sz = 32;
// SPI Device Config
spi_device_interface_config_t devcfg = {};
devcfg.clock_speed_hz = 10 * 1000 * 1000; // 10MHz
devcfg.mode = 0; // SPI mode 0
devcfg.spics_io_num = -1; // Use software CS pin
devcfg.queue_size = 7;
ret = spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to initialize SPI bus");
}
ret = spi_bus_add_device(SPI2_HOST, &devcfg, &spi_handle);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to add SPI device");
}
}
void SPI_Write(unsigned char value) {
spi_transaction_t t = {};
t.length = 8;
t.tx_buffer = &value;
spi_device_polling_transmit(spi_handle, &t);
}
void EPD_W21_WriteCMD(unsigned char command) {
EPD_W21_CS_0;
EPD_W21_DC_0;
SPI_Write(command);
EPD_W21_CS_1;
}
void EPD_W21_WriteDATA(unsigned char datas) {
EPD_W21_CS_0;
EPD_W21_DC_1;
SPI_Write(datas);
EPD_W21_CS_1;
}
unsigned char EPD_W21_ReadDATA(void) {
// Basic stub, typically not used for most writes.
// Software SPI for reading is complex and omitted for standard display usage.
return 0;
}
+969
View File
@@ -0,0 +1,969 @@
#include "GUI_Paint.h"
#include <math.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h> //memset()
PAINT Paint;
unsigned char IMAGE_BACKGROUND;
unsigned char FONT_FOREGROUND;
unsigned char FONT_BACKGROUND;
/******************************************************************************
function: Create Image
parameter:
image : Pointer to the image cache
width : The width of the picture
Height : The height of the picture
Color : Whether the picture is inverted
******************************************************************************/
void Paint_NewImage(UBYTE *image, UWORD Width, UWORD Height, UWORD Rotate,
UWORD Color) {
Paint.Image = NULL;
Paint.Image = image;
Paint.WidthMemory = Width;
Paint.HeightMemory = Height;
Paint.Color = Color;
Paint.Scale = 2;
Paint.WidthByte = (Width % 8 == 0) ? (Width / 8) : (Width / 8 + 1);
Paint.HeightByte = Height;
// printf("WidthByte = %d, HeightByte = %d\r\n", Paint.WidthByte,
// Paint.HeightByte); printf(" EPD_WIDTH / 8 = %d\r\n", 122 / 8);
Paint.Rotate = Rotate;
Paint.Mirror = MIRROR_NONE;
if (Rotate == ROTATE_0 || Rotate == ROTATE_180) {
Paint.Width = Width;
Paint.Height = Height;
} else {
Paint.Width = Height;
Paint.Height = Width;
}
}
/******************************************************************************
function: Select Image
parameter:
image : Pointer to the image cache
******************************************************************************/
void Paint_SelectImage(UBYTE *image) { Paint.Image = image; }
/******************************************************************************
function: Select Image Rotate
parameter:
Rotate : 0,90,180,270
******************************************************************************/
void Paint_SetRotate(UWORD Rotate) {
if (Rotate == ROTATE_0 || Rotate == ROTATE_90 || Rotate == ROTATE_180 ||
Rotate == ROTATE_270) {
// Debug("Set image Rotate %d\r\n", Rotate);
Paint.Rotate = Rotate;
} else {
// Debug("rotate = 0, 90, 180, 270\r\n");
}
}
void Paint_SetScale(UBYTE scale) {
if (scale == 2) {
Paint.Scale = scale;
Paint.WidthByte = (Paint.WidthMemory % 8 == 0)
? (Paint.WidthMemory / 8)
: (Paint.WidthMemory / 8 + 1);
} else if (scale == 4) {
Paint.Scale = scale;
Paint.WidthByte = (Paint.WidthMemory % 4 == 0)
? (Paint.WidthMemory / 4)
: (Paint.WidthMemory / 4 + 1);
} else if (scale == 7) { // Only applicable with 5in65 e-Paper
Paint.Scale = scale;
Paint.WidthByte = (Paint.WidthMemory % 2 == 0)
? (Paint.WidthMemory / 2)
: (Paint.WidthMemory / 2 + 1);
;
} else {
// Debug("Set Scale Input parameter error\r\n");
// Debug("Scale Only support: 2 4 7\r\n");
}
}
/******************************************************************************
function: Select Image mirror
parameter:
mirror :Not mirror,Horizontal mirror,Vertical mirror,Origin mirror
******************************************************************************/
void Paint_SetMirroring(UBYTE mirror) {
if (mirror == MIRROR_NONE || mirror == MIRROR_HORIZONTAL ||
mirror == MIRROR_VERTICAL || mirror == MIRROR_ORIGIN) {
// Debug("mirror image x:%s, y:%s\r\n",(mirror & 0x01)? "mirror":"none",
// ((mirror >> 1) & 0x01)? "mirror":"none");
Paint.Mirror = mirror;
} else {
// Debug("mirror should be MIRROR_NONE, MIRROR_HORIZONTAL, MIRROR_VERTICAL
// or MIRROR_ORIGIN\r\n");
}
}
/******************************************************************************
function: Draw Pixels
parameter:
Xpoint : At point X
Ypoint : At point Y
Color : Painted colors
******************************************************************************/
void Paint_SetPixel(UWORD Xpoint, UWORD Ypoint, UWORD Color) {
UWORD X, Y;
UDOUBLE Addr;
UBYTE Rdata;
if (Xpoint > Paint.Width || Ypoint > Paint.Height) {
// Debug("Exceeding display boundaries\r\n");
return;
}
switch (Paint.Rotate) {
case 0:
X = Xpoint;
Y = Ypoint;
break;
case 90:
X = Paint.WidthMemory - Ypoint - 1;
Y = Xpoint;
break;
case 180:
X = Paint.WidthMemory - Xpoint - 1;
Y = Paint.HeightMemory - Ypoint - 1;
break;
case 270:
X = Ypoint;
Y = Paint.HeightMemory - Xpoint - 1;
break;
default:
return;
}
switch (Paint.Mirror) {
case MIRROR_NONE:
break;
case MIRROR_HORIZONTAL:
X = Paint.WidthMemory - X - 1;
break;
case MIRROR_VERTICAL:
Y = Paint.HeightMemory - Y - 1;
break;
case MIRROR_ORIGIN:
X = Paint.WidthMemory - X - 1;
Y = Paint.HeightMemory - Y - 1;
break;
default:
return;
}
if (X > Paint.WidthMemory || Y > Paint.HeightMemory) {
// Debug("Exceeding display boundaries\r\n");
return;
}
if (Paint.Scale == 2) {
Addr = X / 8 + Y * Paint.WidthByte;
Rdata = Paint.Image[Addr];
if (Color == 0x00) // BLACK
Paint.Image[Addr] = Rdata & ~(0x80 >> (X % 8));
else
Paint.Image[Addr] = Rdata | (0x80 >> (X % 8));
} else if (Paint.Scale == 4) {
Addr = X / 4 + Y * Paint.WidthByte;
Color = Color % 4; // Guaranteed color scale is 4 --- 0~3
Rdata = Paint.Image[Addr];
Rdata = Rdata & (~(0xC0 >> ((X % 4) * 2)));
Paint.Image[Addr] = Rdata | ((Color << 6) >> ((X % 4) * 2));
} else if (Paint.Scale == 7) {
Addr = X / 2 + Y * Paint.WidthByte;
Rdata = Paint.Image[Addr];
Rdata = Rdata & (~(0xF0 >> ((X % 2) * 4))); // Clear first, then set value
Paint.Image[Addr] = Rdata | ((Color << 4) >> ((X % 2) * 4));
// printf("Add = %d ,data = %d\r\n",Addr,Rdata);
}
}
/******************************************************************************
function: Clear the color of the picture
parameter:
Color : Painted colors
******************************************************************************/
void Paint_Clear(UWORD Color) {
UWORD X, Y;
UDOUBLE Addr;
if (Paint.Scale == 2) {
for (Y = 0; Y < Paint.HeightByte; Y++) {
for (X = 0; X < Paint.WidthByte; X++) { // 8 pixel = 1 byte
Addr = X + Y * Paint.WidthByte;
Paint.Image[Addr] = Color;
}
}
} else if (Paint.Scale == 4) {
for (Y = 0; Y < Paint.HeightByte; Y++) {
for (X = 0; X < Paint.WidthByte; X++) {
Addr = X + Y * Paint.WidthByte;
Paint.Image[Addr] = (Color << 6) | (Color << 4) | (Color << 2) | Color;
}
}
} else if (Paint.Scale == 7) {
for (Y = 0; Y < Paint.HeightByte; Y++) {
for (X = 0; X < Paint.WidthByte; X++) {
Addr = X + Y * Paint.WidthByte;
Paint.Image[Addr] = (Color << 4) | Color;
}
}
}
}
/******************************************************************************
function: Clear the color of a window
parameter:
Xstart : x starting point
Ystart : Y starting point
Xend : x end point
Yend : y end point
Color : Painted colors
******************************************************************************/
void Paint_ClearWindows(UWORD Xstart, UWORD Ystart, UWORD Xend, UWORD Yend,
UWORD Color) {
UWORD X, Y;
for (Y = Ystart; Y < Yend; Y++) {
for (X = Xstart; X < Xend; X++) { // 8 pixel = 1 byte
Paint_SetPixel(X, Y, Color);
}
}
}
/******************************************************************************
function: Draw Point(Xpoint, Ypoint) Fill the color
parameter:
Xpoint : The Xpoint coordinate of the point
Ypoint : The Ypoint coordinate of the point
Color : Painted color
Dot_Pixel : point size
Dot_Style : point Style
******************************************************************************/
void Paint_DrawPoint(UWORD Xpoint, UWORD Ypoint, UWORD Color,
DOT_PIXEL Dot_Pixel, DOT_STYLE Dot_Style) {
int16_t XDir_Num, YDir_Num;
if (Xpoint > Paint.Width || Ypoint > Paint.Height) {
// Debug("Paint_DrawPoint Input exceeds the normal display range\r\n");
// printf("Xpoint = %d , Paint.Width = %d \r\n ",Xpoint,Paint.Width);
// printf("Ypoint = %d , Paint.Height = %d \r\n ",Ypoint,Paint.Height);
return;
}
if (Dot_Style == DOT_FILL_AROUND) {
for (XDir_Num = 0; XDir_Num < 2 * Dot_Pixel - 1; XDir_Num++) {
for (YDir_Num = 0; YDir_Num < 2 * Dot_Pixel - 1; YDir_Num++) {
if (Xpoint + XDir_Num - Dot_Pixel < 0 ||
Ypoint + YDir_Num - Dot_Pixel < 0)
break;
// printf("x = %d, y = %d\r\n", Xpoint + XDir_Num - Dot_Pixel, Ypoint +
// YDir_Num - Dot_Pixel);
Paint_SetPixel(Xpoint + XDir_Num - Dot_Pixel,
Ypoint + YDir_Num - Dot_Pixel, Color);
}
}
} else {
for (XDir_Num = 0; XDir_Num < Dot_Pixel; XDir_Num++) {
for (YDir_Num = 0; YDir_Num < Dot_Pixel; YDir_Num++) {
Paint_SetPixel(Xpoint + XDir_Num - 1, Ypoint + YDir_Num - 1, Color);
}
}
}
}
/******************************************************************************
function: Draw a line of arbitrary slope
parameter:
Xstart :Starting Xpoint point coordinates
Ystart :Starting Xpoint point coordinates
Xend :End point Xpoint coordinate
Yend :End point Ypoint coordinate
Color :The color of the line segment
Dot_Pixel : Line width
Line_Style: Solid and dotted lines
******************************************************************************/
void Paint_DrawLine(UWORD Xstart, UWORD Ystart, UWORD Xend, UWORD Yend,
UWORD Color, LINE_STYLE Line_Style, DOT_PIXEL Dot_Pixel) {
UWORD Xpoint, Ypoint;
int dx, dy;
int XAddway, YAddway;
int Esp;
char Dotted_Len;
Paint_Color_Setting(); // Color Setting*******************/
if (Xstart > Paint.Width || Ystart > Paint.Height || Xend > Paint.Width ||
Yend > Paint.Height) {
// Debug("Paint_DrawLine Input exceeds the normal display range\r\n");
return;
}
Xpoint = Xstart;
Ypoint = Ystart;
dx = (int)Xend - (int)Xstart >= 0 ? Xend - Xstart : Xstart - Xend;
dy = (int)Yend - (int)Ystart <= 0 ? Yend - Ystart : Ystart - Yend;
// Increment direction, 1 is positive, -1 is counter;
XAddway = Xstart < Xend ? 1 : -1;
YAddway = Ystart < Yend ? 1 : -1;
// Cumulative error
Esp = dx + dy;
Dotted_Len = 0;
for (;;) {
Dotted_Len++;
// Painted dotted line, 2 point is really virtual
if (Line_Style == LINE_STYLE_DOTTED && Dotted_Len % 3 == 0) {
// Debug("LINE_DOTTED\r\n");
Paint_DrawPoint(Xpoint, Ypoint, IMAGE_BACKGROUND, Dot_Pixel,
DOT_STYLE_DFT);
Dotted_Len = 0;
} else {
Paint_DrawPoint(Xpoint, Ypoint, Color, Dot_Pixel, DOT_STYLE_DFT);
}
if (2 * Esp >= dy) {
if (Xpoint == Xend)
break;
Esp += dy;
Xpoint += XAddway;
}
if (2 * Esp <= dx) {
if (Ypoint == Yend)
break;
Esp += dx;
Ypoint += YAddway;
}
}
}
/******************************************************************************
function: Draw a rectangle
parameter:
Xstart :Rectangular Starting Xpoint point coordinates
Ystart :Rectangular Starting Xpoint point coordinates
Xend :Rectangular End point Xpoint coordinate
Yend :Rectangular End point Ypoint coordinate
Color :The color of the Rectangular segment
Dot_Pixel: Line width
Draw_Fill : Whether to fill the inside of the rectangle
******************************************************************************/
void Paint_DrawRectangle(UWORD Xstart, UWORD Ystart, UWORD Xend, UWORD Yend,
UWORD Color, DRAW_FILL Draw_Fill,
DOT_PIXEL Dot_Pixel) {
UWORD Ypoint;
if (Xstart > Paint.Width || Ystart > Paint.Height || Xend > Paint.Width ||
Yend > Paint.Height) {
// Debug("Input exceeds the normal display range\r\n");
return;
}
if (Draw_Fill) {
for (Ypoint = Ystart; Ypoint < Yend; Ypoint++) {
Paint_DrawLine(Xstart, Ypoint, Xend, Ypoint, Color, LINE_STYLE_SOLID,
Dot_Pixel);
}
} else {
Paint_DrawLine(Xstart, Ystart, Xend, Ystart, Color, LINE_STYLE_SOLID,
Dot_Pixel);
Paint_DrawLine(Xstart, Ystart, Xstart, Yend, Color, LINE_STYLE_SOLID,
Dot_Pixel);
Paint_DrawLine(Xend, Yend, Xend, Ystart, Color, LINE_STYLE_SOLID,
Dot_Pixel);
Paint_DrawLine(Xend, Yend, Xstart, Yend, Color, LINE_STYLE_SOLID,
Dot_Pixel);
}
}
/******************************************************************************
function: Use the 8-point method to draw a circle of the
specified size at the specified position->
parameter:
X_Center :Center X coordinate
Y_Center :Center Y coordinate
Radius :circle Radius
Color :The color of the :circle segment
Dot_Pixel: Line width
Draw_Fill : Whether to fill the inside of the Circle
******************************************************************************/
void Paint_DrawCircle(UWORD X_Center, UWORD Y_Center, UWORD Radius, UWORD Color,
DRAW_FILL Draw_Fill, DOT_PIXEL Dot_Pixel) {
int16_t XCurrent, YCurrent;
int16_t sCountY;
int16_t Esp;
if (X_Center > Paint.Width || Y_Center >= Paint.Height) {
// Debug("Paint_DrawCircle Input exceeds the normal display range\r\n");
return;
}
// Draw a circle from(0, R) as a starting point
XCurrent = 0;
YCurrent = Radius;
// Cumulative error,judge the next point of the logo
Esp = 3 - (Radius << 1);
if (Draw_Fill == DRAW_FILL_FULL) {
while (XCurrent <= YCurrent) { // Realistic circles
for (sCountY = XCurrent; sCountY <= YCurrent; sCountY++) {
Paint_DrawPoint(X_Center + XCurrent, Y_Center + sCountY, Color,
DOT_PIXEL_DFT, DOT_STYLE_DFT); // 1
Paint_DrawPoint(X_Center - XCurrent, Y_Center + sCountY, Color,
DOT_PIXEL_DFT, DOT_STYLE_DFT); // 2
Paint_DrawPoint(X_Center - sCountY, Y_Center + XCurrent, Color,
DOT_PIXEL_DFT, DOT_STYLE_DFT); // 3
Paint_DrawPoint(X_Center - sCountY, Y_Center - XCurrent, Color,
DOT_PIXEL_DFT, DOT_STYLE_DFT); // 4
Paint_DrawPoint(X_Center - XCurrent, Y_Center - sCountY, Color,
DOT_PIXEL_DFT, DOT_STYLE_DFT); // 5
Paint_DrawPoint(X_Center + XCurrent, Y_Center - sCountY, Color,
DOT_PIXEL_DFT, DOT_STYLE_DFT); // 6
Paint_DrawPoint(X_Center + sCountY, Y_Center - XCurrent, Color,
DOT_PIXEL_DFT, DOT_STYLE_DFT); // 7
Paint_DrawPoint(X_Center + sCountY, Y_Center + XCurrent, Color,
DOT_PIXEL_DFT, DOT_STYLE_DFT);
}
if (Esp < 0)
Esp += 4 * XCurrent + 6;
else {
Esp += 10 + 4 * (XCurrent - YCurrent);
YCurrent--;
}
XCurrent++;
}
} else { // Draw a hollow circle
while (XCurrent <= YCurrent) {
Paint_DrawPoint(X_Center + XCurrent, Y_Center + YCurrent, Color,
Dot_Pixel, DOT_STYLE_DFT); // 1
Paint_DrawPoint(X_Center - XCurrent, Y_Center + YCurrent, Color,
Dot_Pixel, DOT_STYLE_DFT); // 2
Paint_DrawPoint(X_Center - YCurrent, Y_Center + XCurrent, Color,
Dot_Pixel, DOT_STYLE_DFT); // 3
Paint_DrawPoint(X_Center - YCurrent, Y_Center - XCurrent, Color,
Dot_Pixel, DOT_STYLE_DFT); // 4
Paint_DrawPoint(X_Center - XCurrent, Y_Center - YCurrent, Color,
Dot_Pixel, DOT_STYLE_DFT); // 5
Paint_DrawPoint(X_Center + XCurrent, Y_Center - YCurrent, Color,
Dot_Pixel, DOT_STYLE_DFT); // 6
Paint_DrawPoint(X_Center + YCurrent, Y_Center - XCurrent, Color,
Dot_Pixel, DOT_STYLE_DFT); // 7
Paint_DrawPoint(X_Center + YCurrent, Y_Center + XCurrent, Color,
Dot_Pixel, DOT_STYLE_DFT); // 0
if (Esp < 0)
Esp += 4 * XCurrent + 6;
else {
Esp += 10 + 4 * (XCurrent - YCurrent);
YCurrent--;
}
XCurrent++;
}
}
}
/******************************************************************************
function: Show English characters
parameter:
Xpoint :X coordinate
Ypoint :Y coordinate
Acsii_Char :To display the English characters
Font :A structure pointer that displays a character size
Color_Foreground : Select the foreground color
Color_Background : Select the background color
******************************************************************************/
void Paint_DrawChar(UWORD Xpoint, UWORD Ypoint, const char Acsii_Char,
sFONT *Font, UWORD Color_Foreground,
UWORD Color_Background) {
UWORD Page, Column;
uint32_t Char_Offset;
const unsigned char *ptr;
Paint_Color_Setting(); // Color Setting*******************/
if (Xpoint > Paint.Width || Ypoint > Paint.Height) {
// Debug("Paint_DrawChar Input exceeds the normal display range\r\n");
return;
}
Char_Offset = (Acsii_Char - ' ') * Font->Height *
(Font->Width / 8 + (Font->Width % 8 ? 1 : 0));
ptr = &Font->table[Char_Offset];
for (Page = 0; Page < Font->Height; Page++) {
for (Column = 0; Column < Font->Width; Column++) {
// To determine whether the font background color and screen background
// color is consistent
if (FONT_BACKGROUND ==
Color_Background) { // this process is to speed up the scan
if (*ptr & (0x80 >> (Column % 8)))
Paint_SetPixel(Xpoint + Column, Ypoint + Page, Color_Foreground);
// Paint_DrawPoint(Xpoint + Column, Ypoint + Page, Color_Foreground,
// DOT_PIXEL_DFT, DOT_STYLE_DFT);
} else {
if (*ptr & (0x80 >> (Column % 8))) {
Paint_SetPixel(Xpoint + Column, Ypoint + Page, Color_Foreground);
// Paint_DrawPoint(Xpoint + Column, Ypoint + Page, Color_Foreground,
// DOT_PIXEL_DFT, DOT_STYLE_DFT);
} else {
Paint_SetPixel(Xpoint + Column, Ypoint + Page, Color_Background);
// Paint_DrawPoint(Xpoint + Column, Ypoint + Page, Color_Background,
// DOT_PIXEL_DFT, DOT_STYLE_DFT);
}
}
// One pixel is 8 bits
if (Column % 8 == 7)
ptr++;
} // Write a line
if (Font->Width % 8 != 0)
ptr++;
} // Write all
}
/******************************************************************************
function: Display the string
parameter:
Xstart :X coordinate
Ystart :Y coordinate
pString :The first address of the English string to be displayed
Font :A structure pointer that displays a character size
Color_Foreground : Select the foreground color
Color_Background : Select the background color
******************************************************************************/
void Paint_DrawString_EN(UWORD Xstart, UWORD Ystart, const char *pString,
sFONT *Font, UWORD Color_Foreground,
UWORD Color_Background) {
UWORD Xpoint = Xstart;
UWORD Ypoint = Ystart;
if (Xstart > Paint.Width || Ystart > Paint.Height) {
// Debug("Paint_DrawString_EN Input exceeds the normal display range\r\n");
return;
}
while (*pString != '\0') {
// if X direction filled , reposition to(Xstart,Ypoint),Ypoint is Y
// direction plus the Height of the character
if ((Xpoint + Font->Width) > Paint.Width) {
Xpoint = Xstart;
Ypoint += Font->Height;
}
// If the Y direction is full, reposition to(Xstart, Ystart)
if ((Ypoint + Font->Height) > Paint.Height) {
Xpoint = Xstart;
Ypoint = Ystart;
}
Paint_DrawChar(Xpoint, Ypoint, *pString, Font, Color_Foreground,
Color_Background);
// The next character of the address
pString++;
// The next word of the abscissa increases the font of the broadband
Xpoint += Font->Width;
}
}
/******************************************************************************
function: Display the string
parameter:
Xstart :X coordinate
Ystart :Y coordinate
pString :The first address of the Chinese string and English
string to be displayed
Font :A structure pointer that displays a character size
Color_Foreground : Select the foreground color
Color_Background : Select the background color
******************************************************************************/
void Paint_DrawString_CN(UWORD Xstart, UWORD Ystart, const char *pString,
cFONT *font, UWORD Color_Foreground,
UWORD Color_Background) {
const char *p_text = pString;
int x = Xstart, y = Ystart;
int i, j, Num;
Paint_Color_Setting(); // Color Setting*******************/
/* Send the string character by character on EPD */
while (*p_text != 0) {
if (*p_text <= 0x7F) { // ASCII < 126
for (Num = 0; Num < font->size; Num++) {
if (*p_text == font->table[Num].index[0]) {
const char *ptr = &font->table[Num].matrix[0];
for (j = 0; j < font->Height; j++) {
for (i = 0; i < font->Width; i++) {
if (FONT_BACKGROUND ==
Color_Background) { // this process is to speed up the scan
if (*ptr & (0x80 >> (i % 8))) {
Paint_SetPixel(x + i, y + j, Color_Foreground);
// Paint_DrawPoint(x + i, y + j, Color_Foreground,
// DOT_PIXEL_DFT, DOT_STYLE_DFT);
}
} else {
if (*ptr & (0x80 >> (i % 8))) {
Paint_SetPixel(x + i, y + j, Color_Foreground);
// Paint_DrawPoint(x + i, y + j, Color_Foreground,
// DOT_PIXEL_DFT, DOT_STYLE_DFT);
} else {
Paint_SetPixel(x + i, y + j, Color_Background);
// Paint_DrawPoint(x + i, y + j, Color_Background,
// DOT_PIXEL_DFT, DOT_STYLE_DFT);
}
}
if (i % 8 == 7) {
ptr++;
}
}
if (font->Width % 8 != 0) {
ptr++;
}
}
break;
}
}
/* Point on the next character */
p_text += 1;
/* Decrement the column position by 16 */
x += font->ASCII_Width;
} else { // Chinese
for (Num = 0; Num < font->size; Num++) {
if ((*p_text == font->table[Num].index[0]) &&
(*(p_text + 1) == font->table[Num].index[1])) {
const char *ptr = &font->table[Num].matrix[0];
for (j = 0; j < font->Height; j++) {
for (i = 0; i < font->Width; i++) {
if (FONT_BACKGROUND ==
Color_Background) { // this process is to speed up the scan
if (*ptr & (0x80 >> (i % 8))) {
Paint_SetPixel(x + i, y + j, Color_Foreground);
// Paint_DrawPoint(x + i, y + j, Color_Foreground,
// DOT_PIXEL_DFT, DOT_STYLE_DFT);
}
} else {
if (*ptr & (0x80 >> (i % 8))) {
Paint_SetPixel(x + i, y + j, Color_Foreground);
// Paint_DrawPoint(x + i, y + j, Color_Foreground,
// DOT_PIXEL_DFT, DOT_STYLE_DFT);
} else {
Paint_SetPixel(x + i, y + j, Color_Background);
// Paint_DrawPoint(x + i, y + j, Color_Background,
// DOT_PIXEL_DFT, DOT_STYLE_DFT);
}
}
if (i % 8 == 7) {
ptr++;
}
}
if (font->Width % 8 != 0) {
ptr++;
}
}
break;
}
}
/* Point on the next character */
p_text += 2;
/* Decrement the column position by 16 */
x += font->Width;
}
}
}
/******************************************************************************
function: Display nummber
parameter:
Xstart :X coordinate
Ystart : Y coordinate
Nummber : The number displayed
Font :A structure pointer that displays a character size
Color_Foreground : Select the foreground color
Color_Background : Select the background color
******************************************************************************/
#define ARRAY_LEN 255
void Paint_DrawNum(UWORD Xpoint, UWORD Ypoint, int32_t Nummber, sFONT *Font,
UWORD Color_Foreground, UWORD Color_Background) {
int16_t Num_Bit = 0, Str_Bit = 0;
uint8_t Str_Array[ARRAY_LEN] = {0}, Num_Array[ARRAY_LEN] = {0};
uint8_t *pStr = Str_Array;
if (Xpoint > Paint.Width || Ypoint > Paint.Height) {
// Debug("Paint_DisNum Input exceeds the normal display range\r\n");
return;
}
// Converts a number to a string
do {
Num_Array[Num_Bit] = Nummber % 10 + '0';
Num_Bit++;
Nummber /= 10;
} while (Nummber);
// The string is inverted
while (Num_Bit > 0) {
Str_Array[Str_Bit] = Num_Array[Num_Bit - 1];
Str_Bit++;
Num_Bit--;
}
// show
Paint_DrawString_EN(Xpoint, Ypoint, (const char *)pStr, Font,
Color_Foreground, Color_Background);
}
/******************************************************************************
function: Display nummber (Able to display decimals)
parameter:
Xstart :X coordinate
Ystart : Y coordinate
Nummber : The number displayed
Font :A structure pointer that displays a character size
Digit : Fractional width
Color_Foreground : Select the foreground color
Color_Background : Select the background color
******************************************************************************/
void Paint_DrawNumDecimals(UWORD Xpoint, UWORD Ypoint, double Nummber,
sFONT *Font, UWORD Digit, UWORD Color_Foreground,
UWORD Color_Background) {
int16_t Num_Bit = 0, Str_Bit = 0;
uint8_t Str_Array[ARRAY_LEN] = {0}, Num_Array[ARRAY_LEN] = {0};
uint8_t *pStr = Str_Array;
int temp = Nummber;
float decimals;
uint8_t i;
if (Xpoint > Paint.Width || Ypoint > Paint.Height) {
// Debug("Paint_DisNum Input exceeds the normal display range\r\n");
return;
}
if (Digit > 0) {
decimals = Nummber - temp;
for (i = Digit; i > 0; i--) {
decimals *= 10;
}
temp = decimals;
// Converts a number to a string
for (i = Digit; i > 0; i--) {
Num_Array[Num_Bit] = temp % 10 + '0';
Num_Bit++;
temp /= 10;
}
Num_Array[Num_Bit] = '.';
Num_Bit++;
}
temp = Nummber;
// Converts a number to a string
do {
Num_Array[Num_Bit] = temp % 10 + '0';
Num_Bit++;
temp /= 10;
} while (temp);
// The string is inverted
while (Num_Bit > 0) {
Str_Array[Str_Bit] = Num_Array[Num_Bit - 1];
Str_Bit++;
Num_Bit--;
}
// show
Paint_DrawString_EN(Xpoint, Ypoint, (const char *)pStr, Font,
Color_Foreground, Color_Background);
}
/******************************************************************************
function: Display time
parameter:
Xstart :X coordinate
Ystart : Y coordinate
pTime : Time-related structures
Font :A structure pointer that displays a character size
Color_Foreground : Select the foreground color
Color_Background : Select the background color
******************************************************************************/
void Paint_DrawTime(UWORD Xstart, UWORD Ystart, PAINT_TIME *pTime, sFONT *Font,
UWORD Color_Foreground, UWORD Color_Background) {
uint8_t value[10] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};
UWORD Dx = Font->Width;
// Write data into the cache
Paint_DrawChar(Xstart, Ystart, value[pTime->Hour / 10], Font,
Color_Background, Color_Foreground);
Paint_DrawChar(Xstart + Dx, Ystart, value[pTime->Hour % 10], Font,
Color_Background, Color_Foreground);
Paint_DrawChar(Xstart + Dx + Dx / 4 + Dx / 2, Ystart, ':', Font,
Color_Background, Color_Foreground);
Paint_DrawChar(Xstart + Dx * 2 + Dx / 2, Ystart, value[pTime->Min / 10], Font,
Color_Background, Color_Foreground);
Paint_DrawChar(Xstart + Dx * 3 + Dx / 2, Ystart, value[pTime->Min % 10], Font,
Color_Background, Color_Foreground);
Paint_DrawChar(Xstart + Dx * 4 + Dx / 2 - Dx / 4, Ystart, ':', Font,
Color_Background, Color_Foreground);
Paint_DrawChar(Xstart + Dx * 5, Ystart, value[pTime->Sec / 10], Font,
Color_Background, Color_Foreground);
Paint_DrawChar(Xstart + Dx * 6, Ystart, value[pTime->Sec % 10], Font,
Color_Background, Color_Foreground);
}
/******************************************************************************
function: Display monochrome bitmap
parameter:
image_buffer :A picture data converted to a bitmap
info:
Use a computer to convert the image into a corresponding array,
and then embed the array directly into Imagedata.cpp as a .c file.
******************************************************************************/
void Paint_DrawBitMap(const unsigned char *image_buffer) {
UWORD x, y;
UDOUBLE Addr = 0;
for (y = 0; y < Paint.HeightByte; y++) {
for (x = 0; x < Paint.WidthByte; x++) { // 8 pixel = 1 byte
Addr = x + y * Paint.WidthByte;
Paint.Image[Addr] = (unsigned char)image_buffer[Addr];
}
}
}
/******************************************************************************
function: paste monochrome bitmap to a frame buff
parameter:
image_buffer :A picture data converted to a bitmap
xStart: The starting x coordinate
yStart: The starting y coordinate
imageWidth: Original image width
imageHeight: Original image height
flipColor: Whether the color is reversed
info:
Use this function to paste image data into a buffer
******************************************************************************/
void Paint_DrawBitMap_Paste(const unsigned char *image_buffer, UWORD xStart,
UWORD yStart, UWORD imageWidth, UWORD imageHeight,
UBYTE flipColor) {
UBYTE color, srcImage;
UWORD x, y;
UWORD width = (imageWidth % 8 == 0 ? imageWidth / 8 : imageWidth / 8 + 1);
for (y = 0; y < imageHeight; y++) {
for (x = 0; x < imageWidth; x++) {
srcImage = image_buffer[y * width + x / 8];
if (flipColor)
color = (((srcImage << (x % 8) & 0x80) == 0) ? 1 : 0);
else
color = (((srcImage << (x % 8) & 0x80) == 0) ? 0 : 1);
Paint_SetPixel(x + xStart, y + yStart, color);
}
}
}
///******************************************************************************
// function: SDisplay half of monochrome bitmap
// parameter:
// Region : 1 Upper half
// 2 Lower half
// info:
//******************************************************************************/
// void Paint_DrawBitMap_Half(const unsigned char* image_buffer, UBYTE Region)
//{
// UWORD x, y;
// UDOUBLE Addr = 0;
//
// if(Region == 1){
// for (y = 0; y < Paint.HeightByte; y++) {
// for (x = 0; x < Paint.WidthByte; x++)
//{//8 pixel = 1 byte Addr = x + y * Paint.WidthByte; Paint.Image[Addr] =
//(unsigned char)image_buffer[Addr];
// }
// }
// }else{
// for (y = 0; y < Paint.HeightByte; y++) {
// for (x = 0; x < Paint.WidthByte; x++)
//{//8 pixel = 1 byte Addr = x + y * Paint.WidthByte ; Paint.Image[Addr] =
// (unsigned
//char)image_buffer[Addr+ (Paint.HeightByte)*Paint.WidthByte];
// }
// }
// }
// }
///******************************************************************************
// function: SDisplay half of monochrome bitmap
// parameter:
// Region : 1 Upper half
// 2 Lower half
// info:
//******************************************************************************/
// void Paint_DrawBitMap_OneQuarter(const unsigned char* image_buffer, UBYTE
// Region)
//{
// UWORD x, y;
// UDOUBLE Addr = 0;
//
// if(Region == 1){
// for (y = 0; y < Paint.HeightByte; y++) {
// for (x = 0; x < Paint.WidthByte; x++)
//{//8 pixel = 1 byte Addr = x + y * Paint.WidthByte; Paint.Image[Addr] =
//(unsigned char)image_buffer[Addr];
// }
// }
// }else if(Region == 2){
// for (y = 0; y < Paint.HeightByte; y++) {
// for (x = 0; x < Paint.WidthByte; x++)
//{//8 pixel = 1 byte Addr = x + y * Paint.WidthByte ; Paint.Image[Addr] =
// (unsigned
//char)image_buffer[Addr+ (Paint.HeightByte)*Paint.WidthByte];
// }
// }
// }else if(Region == 3){
// for (y = 0; y < Paint.HeightByte; y++) {
// for (x = 0; x < Paint.WidthByte; x++)
//{//8 pixel = 1 byte Addr = x + y * Paint.WidthByte ; Paint.Image[Addr] =
// (unsigned
//char)image_buffer[Addr+ (Paint.HeightByte)*Paint.WidthByte*2];
// }
// }
// }else if(Region == 4){
// for (y = 0; y < Paint.HeightByte; y++) {
// for (x = 0; x < Paint.WidthByte; x++)
//{//8 pixel = 1 byte Addr = x + y * Paint.WidthByte ; Paint.Image[Addr] =
// (unsigned
//char)image_buffer[Addr+ (Paint.HeightByte)*Paint.WidthByte*3];
// }
// }
// }
// }
void Paint_DrawBitMap_Block(const unsigned char *image_buffer, UBYTE Region) {
UWORD x, y;
UDOUBLE Addr = 0;
for (y = 0; y < Paint.HeightByte; y++) {
for (x = 0; x < Paint.WidthByte; x++) { // 8 pixel = 1 byte
Addr = x + y * Paint.WidthByte;
Paint.Image[Addr] = (unsigned char)
image_buffer[Addr +
(Paint.HeightByte) * Paint.WidthByte * (Region - 1)];
}
}
}
void Paint_Color_Setting(void) {
if (Paint.Scale == 2) // 2-color, 3-color
{
IMAGE_BACKGROUND = 0xFF;
FONT_FOREGROUND = 0x00;
FONT_BACKGROUND = 0xFF;
}
if (Paint.Scale == 4) // 4-color
{
IMAGE_BACKGROUND = 0x00;
FONT_FOREGROUND = 0x03;
FONT_BACKGROUND = 0x00;
}
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+966
View File
@@ -0,0 +1,966 @@
#include "fonts.h"
//
// Font data for Courier New 12pt
//
const uint8_t Font8_Table[] =
{
// @0 ' ' (5 pixels wide)
0x00, //
0x00, //
0x00, //
0x00, //
0x00, //
0x00, //
0x00, //
0x00, //
// @8 '!' (5 pixels wide)
0x20, // #
0x20, // #
0x20, // #
0x20, // #
0x00, //
0x20, // #
0x00, //
0x00, //
// @16 '"' (5 pixels wide)
0x50, // # #
0x50, // # #
0x00, //
0x00, //
0x00, //
0x00, //
0x00, //
0x00, //
// @24 '#' (5 pixels wide)
0x28, // # #
0x50, // # #
0xF8, // #####
0x50, // # #
0xF8, // #####
0x50, // # #
0xA0, // # #
0x00, //
// @32 '$' (5 pixels wide)
0x20, // #
0x30, // ##
0x60, // ##
0x30, // ##
0x10, // #
0x60, // ##
0x20, // #
0x00, //
// @40 '%' (5 pixels wide)
0x20, // #
0x20, // #
0x18, // ##
0x60, // ##
0x10, // #
0x10, // #
0x00, //
0x00, //
// @48 '&' (5 pixels wide)
0x00, //
0x38, // ###
0x20, // #
0x60, // ##
0x50, // # #
0x78, // ####
0x00, //
0x00, //
// @56 ''' (5 pixels wide)
0x20, // #
0x20, // #
0x20, // #
0x00, //
0x00, //
0x00, //
0x00, //
0x00, //
// @64 '(' (5 pixels wide)
0x10, // #
0x20, // #
0x20, // #
0x20, // #
0x20, // #
0x20, // #
0x10, // #
0x00, //
// @72 ')' (5 pixels wide)
0x40, // #
0x20, // #
0x20, // #
0x20, // #
0x20, // #
0x20, // #
0x40, // #
0x00, //
// @80 '*' (5 pixels wide)
0x20, // #
0x70, // ###
0x20, // #
0x50, // # #
0x00, //
0x00, //
0x00, //
0x00, //
// @88 '+' (5 pixels wide)
0x00, //
0x20, // #
0x20, // #
0xF8, // #####
0x20, // #
0x20, // #
0x00, //
0x00, //
// @96 ',' (5 pixels wide)
0x00, //
0x00, //
0x00, //
0x00, //
0x10, // #
0x20, // #
0x20, // #
0x00, //
// @104 '-' (5 pixels wide)
0x00, //
0x00, //
0x00, //
0x70, // ###
0x00, //
0x00, //
0x00, //
0x00, //
// @112 '.' (5 pixels wide)
0x00, //
0x00, //
0x00, //
0x00, //
0x00, //
0x20, // #
0x00, //
0x00, //
// @120 '/' (5 pixels wide)
0x10, // #
0x20, // #
0x20, // #
0x20, // #
0x40, // #
0x40, // #
0x80, // #
0x00, //
// @128 '0' (5 pixels wide)
0x20, // #
0x50, // # #
0x50, // # #
0x50, // # #
0x50, // # #
0x20, // #
0x00, //
0x00, //
// @136 '1' (5 pixels wide)
0x60, // ##
0x20, // #
0x20, // #
0x20, // #
0x20, // #
0xF8, // #####
0x00, //
0x00, //
// @144 '2' (5 pixels wide)
0x20, // #
0x50, // # #
0x20, // #
0x20, // #
0x40, // #
0x70, // ###
0x00, //
0x00, //
// @152 '3' (5 pixels wide)
0x20, // #
0x50, // # #
0x10, // #
0x20, // #
0x10, // #
0x60, // ##
0x00, //
0x00, //
// @160 '4' (5 pixels wide)
0x10, // #
0x30, // ##
0x50, // # #
0x78, // ####
0x10, // #
0x38, // ###
0x00, //
0x00, //
// @168 '5' (5 pixels wide)
0x70, // ###
0x40, // #
0x60, // ##
0x10, // #
0x50, // # #
0x20, // #
0x00, //
0x00, //
// @176 '6' (5 pixels wide)
0x30, // ##
0x40, // #
0x60, // ##
0x50, // # #
0x50, // # #
0x60, // ##
0x00, //
0x00, //
// @184 '7' (5 pixels wide)
0x70, // ###
0x50, // # #
0x10, // #
0x20, // #
0x20, // #
0x20, // #
0x00, //
0x00, //
// @192 '8' (5 pixels wide)
0x20, // #
0x50, // # #
0x20, // #
0x50, // # #
0x50, // # #
0x20, // #
0x00, //
0x00, //
// @200 '9' (5 pixels wide)
0x30, // ##
0x50, // # #
0x50, // # #
0x30, // ##
0x10, // #
0x60, // ##
0x00, //
0x00, //
// @208 ':' (5 pixels wide)
0x00, //
0x00, //
0x20, // #
0x00, //
0x00, //
0x20, // #
0x00, //
0x00, //
// @216 ';' (5 pixels wide)
0x00, //
0x00, //
0x10, // #
0x00, //
0x10, // #
0x20, // #
0x00, //
0x00, //
// @224 '<' (5 pixels wide)
0x00, //
0x10, // #
0x20, // #
0xC0, // ##
0x20, // #
0x10, // #
0x00, //
0x00, //
// @232 '=' (5 pixels wide)
0x00, //
0x70, // ###
0x00, //
0x70, // ###
0x00, //
0x00, //
0x00, //
0x00, //
// @240 '>' (5 pixels wide)
0x00, //
0x40, // #
0x20, // #
0x18, // ##
0x20, // #
0x40, // #
0x00, //
0x00, //
// @248 '?' (5 pixels wide)
0x20, // #
0x50, // # #
0x10, // #
0x20, // #
0x00, //
0x20, // #
0x00, //
0x00, //
// @256 '@' (5 pixels wide)
0x30, // ##
0x48, // # #
0x48, // # #
0x58, // # ##
0x48, // # #
0x40, // #
0x38, // ###
0x00, //
// @264 'A' (5 pixels wide)
0x60, // ##
0x20, // #
0x50, // # #
0x70, // ###
0x88, // # #
0xD8, // ## ##
0x00, //
0x00, //
// @272 'B' (5 pixels wide)
0xF0, // ####
0x48, // # #
0x70, // ###
0x48, // # #
0x48, // # #
0xF0, // ####
0x00, //
0x00, //
// @280 'C' (5 pixels wide)
0x70, // ###
0x50, // # #
0x40, // #
0x40, // #
0x40, // #
0x30, // ##
0x00, //
0x00, //
// @288 'D' (5 pixels wide)
0xF0, // ####
0x48, // # #
0x48, // # #
0x48, // # #
0x48, // # #
0xF0, // ####
0x00, //
0x00, //
// @296 'E' (5 pixels wide)
0xF8, // #####
0x48, // # #
0x60, // ##
0x40, // #
0x48, // # #
0xF8, // #####
0x00, //
0x00, //
// @304 'F' (5 pixels wide)
0xF8, // #####
0x48, // # #
0x60, // ##
0x40, // #
0x40, // #
0xE0, // ###
0x00, //
0x00, //
// @312 'G' (5 pixels wide)
0x70, // ###
0x40, // #
0x40, // #
0x58, // # ##
0x50, // # #
0x30, // ##
0x00, //
0x00, //
// @320 'H' (5 pixels wide)
0xE8, // ### #
0x48, // # #
0x78, // ####
0x48, // # #
0x48, // # #
0xE8, // ### #
0x00, //
0x00, //
// @328 'I' (5 pixels wide)
0x70, // ###
0x20, // #
0x20, // #
0x20, // #
0x20, // #
0x70, // ###
0x00, //
0x00, //
// @336 'J' (5 pixels wide)
0x38, // ###
0x10, // #
0x10, // #
0x50, // # #
0x50, // # #
0x20, // #
0x00, //
0x00, //
// @344 'K' (5 pixels wide)
0xD8, // ## ##
0x50, // # #
0x60, // ##
0x70, // ###
0x50, // # #
0xD8, // ## ##
0x00, //
0x00, //
// @352 'L' (5 pixels wide)
0xE0, // ###
0x40, // #
0x40, // #
0x40, // #
0x48, // # #
0xF8, // #####
0x00, //
0x00, //
// @360 'M' (5 pixels wide)
0xD8, // ## ##
0xD8, // ## ##
0xD8, // ## ##
0xA8, // # # #
0x88, // # #
0xD8, // ## ##
0x00, //
0x00, //
// @368 'N' (5 pixels wide)
0xD8, // ## ##
0x68, // ## #
0x68, // ## #
0x58, // # ##
0x58, // # ##
0xE8, // ### #
0x00, //
0x00, //
// @376 'O' (5 pixels wide)
0x30, // ##
0x48, // # #
0x48, // # #
0x48, // # #
0x48, // # #
0x30, // ##
0x00, //
0x00, //
// @384 'P' (5 pixels wide)
0xF0, // ####
0x48, // # #
0x48, // # #
0x70, // ###
0x40, // #
0xE0, // ###
0x00, //
0x00, //
// @392 'Q' (5 pixels wide)
0x30, // ##
0x48, // # #
0x48, // # #
0x48, // # #
0x48, // # #
0x30, // ##
0x18, // ##
0x00, //
// @400 'R' (5 pixels wide)
0xF0, // ####
0x48, // # #
0x48, // # #
0x70, // ###
0x48, // # #
0xE8, // ### #
0x00, //
0x00, //
// @408 'S' (5 pixels wide)
0x70, // ###
0x50, // # #
0x20, // #
0x10, // #
0x50, // # #
0x70, // ###
0x00, //
0x00, //
// @416 'T' (5 pixels wide)
0xF8, // #####
0xA8, // # # #
0x20, // #
0x20, // #
0x20, // #
0x70, // ###
0x00, //
0x00, //
// @424 'U' (5 pixels wide)
0xD8, // ## ##
0x48, // # #
0x48, // # #
0x48, // # #
0x48, // # #
0x30, // ##
0x00, //
0x00, //
// @432 'V' (5 pixels wide)
0xD8, // ## ##
0x88, // # #
0x48, // # #
0x50, // # #
0x50, // # #
0x30, // ##
0x00, //
0x00, //
// @440 'W' (5 pixels wide)
0xD8, // ## ##
0x88, // # #
0xA8, // # # #
0xA8, // # # #
0xA8, // # # #
0x50, // # #
0x00, //
0x00, //
// @448 'X' (5 pixels wide)
0xD8, // ## ##
0x50, // # #
0x20, // #
0x20, // #
0x50, // # #
0xD8, // ## ##
0x00, //
0x00, //
// @456 'Y' (5 pixels wide)
0xD8, // ## ##
0x88, // # #
0x50, // # #
0x20, // #
0x20, // #
0x70, // ###
0x00, //
0x00, //
// @464 'Z' (5 pixels wide)
0x78, // ####
0x48, // # #
0x10, // #
0x20, // #
0x48, // # #
0x78, // ####
0x00, //
0x00, //
// @472 '[' (5 pixels wide)
0x30, // ##
0x20, // #
0x20, // #
0x20, // #
0x20, // #
0x20, // #
0x30, // ##
0x00, //
// @480 '\' (5 pixels wide)
0x80, // #
0x40, // #
0x40, // #
0x20, // #
0x20, // #
0x20, // #
0x10, // #
0x00, //
// @488 ']' (5 pixels wide)
0x60, // ##
0x20, // #
0x20, // #
0x20, // #
0x20, // #
0x20, // #
0x60, // ##
0x00, //
// @496 '^' (5 pixels wide)
0x20, // #
0x20, // #
0x50, // # #
0x00, //
0x00, //
0x00, //
0x00, //
0x00, //
// @504 '_' (5 pixels wide)
0x00, //
0x00, //
0x00, //
0x00, //
0x00, //
0x00, //
0x00, //
0xF8, // #####
// @512 '`' (5 pixels wide)
0x20, // #
0x10, // #
0x00, //
0x00, //
0x00, //
0x00, //
0x00, //
0x00, //
// @520 'a' (5 pixels wide)
0x00, //
0x00, //
0x30, // ##
0x10, // #
0x70, // ###
0x78, // ####
0x00, //
0x00, //
// @528 'b' (5 pixels wide)
0xC0, // ##
0x40, // #
0x70, // ###
0x48, // # #
0x48, // # #
0xF0, // ####
0x00, //
0x00, //
// @536 'c' (5 pixels wide)
0x00, //
0x00, //
0x70, // ###
0x40, // #
0x40, // #
0x70, // ###
0x00, //
0x00, //
// @544 'd' (5 pixels wide)
0x18, // ##
0x08, // #
0x38, // ###
0x48, // # #
0x48, // # #
0x38, // ###
0x00, //
0x00, //
// @552 'e' (5 pixels wide)
0x00, //
0x00, //
0x70, // ###
0x70, // ###
0x40, // #
0x30, // ##
0x00, //
0x00, //
// @560 'f' (5 pixels wide)
0x10, // #
0x20, // #
0x70, // ###
0x20, // #
0x20, // #
0x70, // ###
0x00, //
0x00, //
// @568 'g' (5 pixels wide)
0x00, //
0x00, //
0x38, // ###
0x48, // # #
0x48, // # #
0x38, // ###
0x08, // #
0x30, // ##
// @576 'h' (5 pixels wide)
0xC0, // ##
0x40, // #
0x70, // ###
0x48, // # #
0x48, // # #
0xE8, // ### #
0x00, //
0x00, //
// @584 'i' (5 pixels wide)
0x20, // #
0x00, //
0x60, // ##
0x20, // #
0x20, // #
0x70, // ###
0x00, //
0x00, //
// @592 'j' (5 pixels wide)
0x20, // #
0x00, //
0x70, // ###
0x10, // #
0x10, // #
0x10, // #
0x10, // #
0x70, // ###
// @600 'k' (5 pixels wide)
0xC0, // ##
0x40, // #
0x58, // # ##
0x70, // ###
0x50, // # #
0xD8, // ## ##
0x00, //
0x00, //
// @608 'l' (5 pixels wide)
0x60, // ##
0x20, // #
0x20, // #
0x20, // #
0x20, // #
0x70, // ###
0x00, //
0x00, //
// @616 'm' (5 pixels wide)
0x00, //
0x00, //
0xD0, // ## #
0xA8, // # # #
0xA8, // # # #
0xA8, // # # #
0x00, //
0x00, //
// @624 'n' (5 pixels wide)
0x00, //
0x00, //
0xF0, // ####
0x48, // # #
0x48, // # #
0xC8, // ## #
0x00, //
0x00, //
// @632 'o' (5 pixels wide)
0x00, //
0x00, //
0x30, // ##
0x48, // # #
0x48, // # #
0x30, // ##
0x00, //
0x00, //
// @640 'p' (5 pixels wide)
0x00, //
0x00, //
0xF0, // ####
0x48, // # #
0x48, // # #
0x70, // ###
0x40, // #
0xE0, // ###
// @648 'q' (5 pixels wide)
0x00, //
0x00, //
0x38, // ###
0x48, // # #
0x48, // # #
0x38, // ###
0x08, // #
0x18, // ##
// @656 'r' (5 pixels wide)
0x00, //
0x00, //
0x78, // ####
0x20, // #
0x20, // #
0x70, // ###
0x00, //
0x00, //
// @664 's' (5 pixels wide)
0x00, //
0x00, //
0x30, // ##
0x20, // #
0x10, // #
0x60, // ##
0x00, //
0x00, //
// @672 't' (5 pixels wide)
0x00, //
0x40, // #
0xF0, // ####
0x40, // #
0x48, // # #
0x30, // ##
0x00, //
0x00, //
// @680 'u' (5 pixels wide)
0x00, //
0x00, //
0xD8, // ## ##
0x48, // # #
0x48, // # #
0x38, // ###
0x00, //
0x00, //
// @688 'v' (5 pixels wide)
0x00, //
0x00, //
0xC8, // ## #
0x48, // # #
0x30, // ##
0x30, // ##
0x00, //
0x00, //
// @696 'w' (5 pixels wide)
0x00, //
0x00, //
0xD8, // ## ##
0xA8, // # # #
0xA8, // # # #
0x50, // # #
0x00, //
0x00, //
// @704 'x' (5 pixels wide)
0x00, //
0x00, //
0x48, // # #
0x30, // ##
0x30, // ##
0x48, // # #
0x00, //
0x00, //
// @712 'y' (5 pixels wide)
0x00, //
0x00, //
0xD8, // ## ##
0x50, // # #
0x50, // # #
0x20, // #
0x20, // #
0x60, // ##
// @720 'z' (5 pixels wide)
0x00, //
0x00, //
0x78, // ####
0x50, // # #
0x28, // # #
0x78, // ####
0x00, //
0x00, //
// @728 '{' (5 pixels wide)
0x10, // #
0x20, // #
0x20, // #
0x60, // ##
0x20, // #
0x20, // #
0x10, // #
0x00, //
// @736 '|' (5 pixels wide)
0x20, // #
0x20, // #
0x20, // #
0x20, // #
0x20, // #
0x20, // #
0x20, // #
0x00, //
// @744 '}' (5 pixels wide)
0x40, // #
0x20, // #
0x20, // #
0x30, // ##
0x20, // #
0x20, // #
0x40, // #
0x00, //
// @752 '~' (5 pixels wide)
0x00, //
0x00, //
0x00, //
0x28, // # #
0x50, // # #
0x00, //
0x00, //
0x00, //
};
sFONT Font8 = {
Font8_Table,
5, /* Width */
8, /* Height */
};
/************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE****/
+6
View File
@@ -0,0 +1,6 @@
# xInfo 项目技术文档
本文档目录收录了 `xInfo` 环境监控终端的技术架构、硬件参数、接线定义以及墨水屏刷新机制说明:
- [硬件规格与引脚接线说明 (Hardware Specs & Pinout)](hardware_specs.md):记录 DEPG0420 / YMS400300 屏幕型号、400×300 分辨率、24P 排线、尺寸以及与 ESP32-C3 的完整引脚接线分配表。
- [墨水屏刷新机制与显示管理技术文档 (E-Paper Refresh Mechanism)](epaper_refresh_mechanism.md):详细介绍全量/快速刷新波形特性(三色仅全刷、黑白支持快刷)、双图层管理与自适应残影消除调度算法。
+153
View File
@@ -0,0 +1,153 @@
# xInfo 墨水屏刷新机制与显示管理技术文档
本文档详细介绍了 `xInfo` 环境监控终端中 **DEPG0420 / YMS400300(400×300)电子墨水屏** 的硬件特性、引脚分配、多级刷新机制、显存架构以及应用层智能自适应调度策略。
---
## 1. 硬件规格与物理特性
| 参数项 | 参数值 / 说明 |
| :--- | :--- |
| **屏幕型号** | **DEPG0420 / YMS400300** |
| **物理尺寸** | **91 × 77 mm**(不含排线) |
| **排线规格** | **24 Pin (24P)** |
| **屏幕分辨率** | **400 × 300 像素** |
| **色彩模式** | **黑 / 白 / 红 三色(BWR)** 或 纯黑白模式(BW) |
| **快刷特性** | **三色(BWR)模式下不支持快刷(必须全量三色刷新)**;在纯黑白(BW)模式下支持快速刷新 |
| **显示特性** | 双稳态显示(Bistable Display):断电后画面永久保持,仅在刷新瞬间消耗电量 |
| **通信接口** | 4-Wire SPI 接口(3.3V 供电) |
| **显存占用** | BWR 模式约 30KB(双 15KB FrameBuffer),BW 模式约 15KB |
### 1.1 ESP32-C3 接线定义表
| 墨水屏引脚 | ESP32-C3 引脚 | 功能描述 | 物理特性说明 |
| :--- | :--- | :--- | :--- |
| **BUSY** | **GPIO 1** | 忙碌状态输出 | 高电平表示空闲(Idle),低电平表示忙碌中(Busy) |
| **RST** | **GPIO 2** | 硬件复位引脚 | 低电平复位,高电平正常工作 |
| **DC** | **GPIO 3** | 数据 / 命令控制 | 低电平写命令(CMD),高电平写数据(DATA) |
| **CS** | **GPIO 4** | SPI 片选 (SPI CS) | 低电平有效选通 |
| **CLK (SCL)** | **GPIO 5** | SPI 时钟 (SPI SCK) | 时钟输入 |
| **DIN (SDA)** | **GPIO 6** | SPI 数据输入 (MOSI) | 数据输入 |
| **GND** | **GND** | 电源地 | 系统参考地 |
| **VCC** | **3.3V** | 供电正极 | 3.3V 电源 |
---
## 2. 刷新机制三层架构
```
┌─────────────────────────────────────────────────────────────┐
│ 3. 业务调度层 (App.cpp) │
│ - 数据变化检测 (跳过无意义刷新) │
│ - 红色图层变更指纹检测 │
│ - 连续快刷计数与周期性残影消除算法 │
└──────────────────────────────┬──────────────────────────────┘
│ 调度决策
┌──────────────────────────────▼──────────────────────────────┐
│ 2. 显示管理层 (DisplayManager.cpp) │
│ - 400×300 区域布局与组件排版 │
│ - 双图层/单图层显存分配与色彩切换 (switchColor) │
│ - 中英文矢量/点阵字库渲染 (Unifont 16×16) │
└──────────────────────────────┬──────────────────────────────┘
│ 驱动调用
┌──────────────────────────────▼──────────────────────────────┐
│ 1. 硬件驱动层 (epdiy_bwr: EPD.cpp / EPD_SPI.cpp) │
│ - 全量刷新波形 (Full Refresh Waveform, ~12-15s) │
│ - 快速刷新波形 (Fast Refresh Waveform, ~1.5s, 仅黑白) │
│ - 硬件控制器深度休眠 (Deep Sleep Command 0x10) │
└─────────────────────────────────────────────────────────────┘
```
---
## 3. 底层硬件刷新波形与接口
在驱动层 `components/epdiy_bwr/` 中,针对电子墨水微胶囊粒子的电泳物理特性,实现了两种不同驱动波形:
### 3.1 全量刷新 (Full Refresh)
* **对应 API**:`EPD_HW_Init_GUI()` + `EPD_Display(ImageBW, ImageRW)`
* **耗时**:三色模式 ~12 ~ 15 秒;纯黑白模式 ~3 ~ 4 秒
* **物理过程**:
控制器连续输出多阶段高低反转电压脉冲,驱动黑、白、红三色微胶囊粒子充分迁移。屏幕会经历数次黑白红剧烈翻转闪烁。
* **主要作用**:
1. **驱动红色粒子**:全量波形是激活红色微胶囊电泳迁移的唯一方式(DEPG0420 三色模式必须使用此波形);
2. **彻底清除残影**:中和残留在微胶囊两端的历史电荷,还原纯净白底。
### 3.2 快速刷新 (Fast Refresh)
* **对应 API**:`EPD_HW_Init_Fast()` + `EPD_WhiteScreen_ALL_Fast(ImageBW, ImageRW)`
* **耗时**:**约 1.5 秒**
* **物理过程**:
精简了电荷反转清屏步骤,仅针对黑白粒子施加单向短脉冲快速翻转。
* **主要作用**:
1. **极佳观感**:刷新极快且**完全不闪烁**,仅在**纯黑白模式(BW)**或仅更新黑白数据时适用;
2. **局限性**:**不支持三色红白刷新**,若长时间仅用快刷,屏幕背景会微弱累积灰度残影。
### 3.3 控制器深度休眠 (Deep Sleep)
* **对应 API**:`EPD_DeepSleep()`
* **执行时机**:无论是全刷还是快刷,在数据写入屏幕完成后的第一时间,**必须**调用此函数向控制器发送 `0x10` 深度休眠命令。
* **重要意义**:
1. **极致省电**:休眠状态功耗仅为微安级(µA);
2. **保护屏幕面板**:避免墨水屏长期处于工作电压导致液晶层极化损坏。
---
## 4. 显存与颜色图层管理
系统在编译期通过 `Kconfig`(`CONFIG_DISPLAY_BWR` / `CONFIG_DISPLAY_BW`)进行显存与图层模式切换:
### 4.1 黑白红三色模式 (BWR)
* **显存分配**:
* `ImageBW`:黑白位图缓冲区(400 × 300 / 8 = 15,000 字节),`0`=黑,`1`=白
* `ImageRW`:红白位图缓冲区(15,000 字节),`0`=红,`1`=白/无色
* **绘制流程**:
调用 `switchColor(true)` 切换到红色图层绘制红底顶部横幅;调用 `switchColor(false)` 返回黑白图层绘制常规文字与数值。
### 4.2 纯黑白模式 (BW)
* **显存分配**:
仅分配单块 `ImageBW`(15KB),不分配红色图层,显著降低 ESP32 内存占用,同时原生支持快速刷新。
---
## 5. 应用层自适应刷新策略
在 [`App.cpp`](file:///Users/hongli/Workspace/xInfo/main/App.cpp) 的主循环中,系统整合了一套自适应仲裁算法:
```mermaid
flowchart TD
Start([定时采集本地传感器]) --> Fetch[获取本地温湿度与空气质量数据]
Fetch --> CheckDiff{数据与上一帧相比<br/>是否有变化?}
CheckDiff -- 完全相同 --> Skip[跳过本次刷新<br/>延长墨水屏物理寿命]
CheckDiff -- 有变化 --> CheckMode{当前色彩模式?}
CheckMode -- BWR 三色模式 --> CheckCondition{是否满足全刷条件?<br/>1. 红色图层内容发生变动<br/>2. 快刷连续达阈值 约3小时}
CheckMode -- BW 纯黑白模式 --> DoFast[执行 1.5s 极速快刷<br/>无闪烁更新]
CheckCondition -- 是 --> DoFull[执行全量三色刷新<br/>消除残影 / 渲染红色图层]
CheckCondition -- 否 --> DoFast
DoFull --> Sleep[EPD_DeepSleep 硬件休眠]
DoFast --> Sleep
Skip --> Delay[等待下一个采样周期]
Sleep --> Delay
```
### 策略 1:数据无变化跳刷(Data Diff Bypass)
* **逻辑**:每次采集后,将新生成的 `DashboardData` 与上一帧 `lastDashData_` 进行结构体全字段比对。
* **优势**:如果室内环境稳定无变动,墨水屏完全不工作,极大延长电泳微胶囊的使用寿命。
### 策略 2:红色图层变更感知(Red Fingerprint Tracking)
* **逻辑**:记录上一次全刷时红色区域的文本指纹 `lastRedContent_`(例如顶部 WiFi 连接状态)。
* **优势**:当网络从 `WiFi: Home` 变为 `[Offline]` 时,由于涉及红底白字变化,系统自动由快刷升阶为全量三色刷新,确保红色像素不缺失。
### 策略 3:周期性全刷消残影(Anti-Ghosting Maintenance)
* **逻辑**:维护快速刷新计数器 `fullRefreshCounter_`。
* **计算公式**:
$$\text{maxFastRefreshes} = \frac{180\text{ 分钟}}{\text{CONFIG\_DISPLAY\_REFRESH\_INTERVAL\_MIN}}$$
* **优势**:日常采用 1.5 秒无感快刷,每累积运行约 **3 小时** 强制触发一次全量反转全刷,彻底清空物理残影,兼顾观感与画质纯净度。
---
## 6. 相关配置文件与源码索引
* **硬件引脚分配规范**:[`docs/hardware_specs.md`](hardware_specs.md)
* **应用主控状态机**:[`main/App.cpp`](file:///Users/hongli/Workspace/xInfo/main/App.cpp)、[`main/App.h`](file:///Users/hongli/Workspace/xInfo/main/App.h)
* **UI 布局与图层管理**:[`main/DisplayManager.cpp`](file:///Users/hongli/Workspace/xInfo/main/DisplayManager.cpp)、[`main/DisplayManager.h`](file:///Users/hongli/Workspace/xInfo/main/DisplayManager.h)
* **底层硬件波形驱动**:[`components/epdiy_bwr/src/EPD.cpp`](file:///Users/hongli/Workspace/xInfo/components/epdiy_bwr/src/EPD.cpp)、[`components/epdiy_bwr/include/EPD_SPI.h`](file:///Users/hongli/Workspace/xInfo/components/epdiy_bwr/include/EPD_SPI.h)
* **Kconfig 参数配置**:[`main/Kconfig.projbuild`](file:///Users/hongli/Workspace/xInfo/main/Kconfig.projbuild)
+62
View File
@@ -0,0 +1,62 @@
# xInfo 硬件规格与引脚接线说明
本文档记录 `xInfo` 环境监控终端的完整硬件参数、屏幕型号、物理规格以及 ESP32-C3 所有外设的真实引脚接线对照表。
---
## 1. 墨水屏硬件规格与接线
### 1.1 屏幕基本参数
| 参数项 | 参数值 / 说明 |
| :--- | :--- |
| **屏幕型号** | **DEPG0420 / YMS400300** |
| **分辨率** | 400 × 300 像素 |
| **显示色彩** | 黑 / 白 / 红 三色(Black / White / Red) |
| **快刷支持** | **三色(BWR)模式下不支持快刷(需全量三色刷新)**;在纯黑白(BW)模式下支持快速刷新 |
| **通信接口** | 4-Wire SPI 接口 |
| **排线规格** | 24 Pin(FPC 24P) |
| **外形尺寸** | 91 × 77 mm(不含 FPC 排线) |
| **工作电压** | 3.3V |
### 1.2 墨水屏接线引脚对照表
| 墨水屏引脚 | 接 ESP32-C3 针脚 | 功能描述 | 说明 |
| :--- | :--- | :--- | :--- |
| **BUSY** | **GPIO 1** | 忙碌状态输出 | 高电平表示空闲(Idle),低电平表示忙碌中(Busy) |
| **RST** | **GPIO 2** | 复位引脚 | 硬件复位控制 |
| **DC** | **GPIO 3** | 数据/命令控制 | 低电平写命令(CMD),高电平写数据(DATA) |
| **CS** | **GPIO 4** | 片选信号 | SPI 片选 (CS) |
| **CLK (SCL)** | **GPIO 5** | SPI 时钟 | SPI 时钟总线 (SCK) |
| **DIN (SDA)** | **GPIO 6** | SPI 数据输入 | SPI 数据输入 (MOSI) |
| **GND** | **GND** | 电源地 | 系统参考地 |
| **VCC** | **3.3V** | 3.3V 供电 | 3.3V 稳压电源输入 |
---
## 2. TVOC 三合一空气质量传感器接线
| 传感器引脚 | 接 ESP32-C3 针脚 | 开发板引脚编号 | 功能与说明 |
| :--- | :--- | :--- | :--- |
| **GND** | **GND** | 01 | 传感器电源地 |
| **5V** | **5V** | 16 | 传感器 5V 供电输入 |
| **B (TXD)** → | **GPIO00 (UART1_RX)** | 02 | 传感器数据发送端 $\rightarrow$ ESP32 串口接收(9600 8N1) |
| **A (RXD)** ← | **GPIO01 (UART1_TX)** | 03 | ESP32 串口发送端 $\rightarrow$ 传感器数据接收端 |
---
## 3. DHT11 温湿度传感器接线
| DHT11 传感器引脚 | 接 ESP32-C3 针脚名称 | 引脚编号(最优推荐) | 说明 |
| :--- | :--- | :--- | :--- |
| **VCC (电源正极)** | **3.3V** | 13 | ⚡ **请用 3.3V,不要用 5V** |
| **GND (电源负极)** | **GND** | 07 | **必须接对,接反必烧!** |
| **DO (数据输出)** | **GPIO05** | 27 | 单总线数字信号 IO 口 |
---
## 4. 驱动与源码对应索引
* **墨水屏 SPI 引脚定义**:[`components/epdiy_bwr/include/EPD_SPI.h`](file:///Users/hongli/Workspace/xInfo/components/epdiy_bwr/include/EPD_SPI.h)
* **墨水屏底层波形驱动**:[`components/epdiy_bwr/src/EPD.cpp`](file:///Users/hongli/Workspace/xInfo/components/epdiy_bwr/src/EPD.cpp)
* **UART 空气质量传感器驱动**:[`main/SensorDriver.h`](file:///Users/hongli/Workspace/xInfo/main/SensorDriver.h)、[`main/SensorDriver.cpp`](file:///Users/hongli/Workspace/xInfo/main/SensorDriver.cpp)
* **DHT11 温湿度驱动**:[`main/Dht11Driver.h`](file:///Users/hongli/Workspace/xInfo/main/Dht11Driver.h)、[`main/Dht11Driver.cpp`](file:///Users/hongli/Workspace/xInfo/main/Dht11Driver.cpp)
* **主控初始化与调用流程**:[`main/App.cpp`](file:///Users/hongli/Workspace/xInfo/main/App.cpp)
+117
View File
@@ -0,0 +1,117 @@
/**
* @file App.cpp
* @brief xInfo 环境监控终端主应用逻辑。
*/
#include "App.h"
#include "esp_log.h"
#include "esp_netif_sntp.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include <ctime>
// ============ 编译期常量(来自 Kconfig / menuconfig)============
static constexpr uint32_t kDisplayIntervalMs = CONFIG_DISPLAY_REFRESH_INTERVAL_MIN * 60 * 1000; // 墨水屏刷新间隔
static const char *TAG = "App";
App::App()
: wifi_(CONFIG_WIFI_SSID, CONFIG_WIFI_PASSWORD),
dht11Driver_(GPIO_NUM_5),
dashFetcher_(sensorDriver_, dht11Driver_) {}
App::~App() {}
void App::init() {
ESP_LOGI(TAG, "正在初始化 xInfo 环境监控终端...");
// 步骤 1:连接 WiFi
wifi_.connect();
// 步骤 2:SNTP 时间同步(获取网络时间用于显示准确时钟)
ESP_LOGI(TAG, "正在同步 NTP 时间(服务器: %s)...", CONFIG_NTP_SERVER);
setenv("TZ", "CST-8", 1);
tzset();
esp_sntp_config_t sntpConfig = ESP_NETIF_SNTP_DEFAULT_CONFIG(CONFIG_NTP_SERVER);
esp_netif_sntp_init(&sntpConfig);
constexpr int kMaxRetries = 3;
for (int i = 0; i < kMaxRetries; ++i) {
esp_err_t ret = esp_netif_sntp_sync_wait(pdMS_TO_TICKS(CONFIG_NTP_SYNC_TIMEOUT_SEC * 1000));
if (ret == ESP_OK) {
time_t now = time(nullptr);
struct tm timeinfo;
localtime_r(&now, &timeinfo);
char timeBuf[64];
strftime(timeBuf, sizeof(timeBuf), "%Y-%m-%d %H:%M:%S", &timeinfo);
ESP_LOGI(TAG, "NTP 时间同步成功: %s", timeBuf);
break;
}
ESP_LOGW(TAG, "NTP 同步第 %d 次超时...", i + 1);
}
// 步骤 3:初始化墨水屏硬件
display_.initHardware();
// 步骤 4:初始化空气质量传感器驱动
sensorDriver_.init();
// 步骤 5:初始化 DHT11 温湿度传感器驱动
dht11Driver_.init();
}
void App::run() {
ESP_LOGI(TAG, "进入主循环...");
std::string mac = wifi_.getMac();
while (true) {
ESP_LOGI(TAG, "采集本地环境数据并刷新墨水屏...");
auto dashData = dashFetcher_.fetch();
if (dashData && dashData != lastDashData_) {
display_.beginDrawing();
// 绘制环境与网络监控界面
std::string ssid = wifi_.getSsid();
display_.drawTopBanner(ssid.empty() || ssid == "N/A" ? "Offline" : ssid);
display_.drawThermoHygrometer(*dashData); // 核心温湿度看板与舒适度
display_.drawAirQuality(*dashData); // 空气质量多维度指标(CO2/TVOC/甲醛)
display_.drawBottomInfoBar(wifi_.getIp(), mac); // 底部 IP 与 MAC 网络参数
// 定期全刷消除墨水屏残影
int maxFastRefreshes = 180 / CONFIG_DISPLAY_REFRESH_INTERVAL_MIN;
if (maxFastRefreshes < 1) maxFastRefreshes = 1;
#ifdef CONFIG_DISPLAY_BWR
std::string redContent = ssid;
bool needFullRefresh = lastRedContent_.empty()
|| (redContent != lastRedContent_)
|| (fullRefreshCounter_ >= maxFastRefreshes);
#else
bool needFullRefresh = (fullRefreshCounter_ == 0)
|| (fullRefreshCounter_ >= maxFastRefreshes);
#endif
if (needFullRefresh) {
display_.updateFullAndSleep();
#ifdef CONFIG_DISPLAY_BWR
lastRedContent_ = redContent;
#endif
fullRefreshCounter_ = 0;
} else {
display_.updateFastAndSleep();
fullRefreshCounter_++;
}
lastDashData_ = dashData;
ESP_LOGI(TAG, "墨水屏刷新完成(%s)", needFullRefresh ? "全量" : "快刷");
} else {
ESP_LOGI(TAG, "数据无变化,跳过本次墨水屏刷新");
}
// 休眠等待下一个刷新周期
vTaskDelay(pdMS_TO_TICKS(kDisplayIntervalMs));
}
}
+88
View File
@@ -0,0 +1,88 @@
/**
* @file App.h
* @brief xInfo 环境监控终端主应用类。
*
* 负责协调系统初始化、WiFi 连接、NTP 网络授时、本地传感器数据采集聚合,
* 以及墨水屏 UI 定时局部/全量刷新的完整生命周期与状态机管理。
*
* 系统运行机制:
* 1. 启动与初始化(init()):
* - 建立 WiFi 连接并获取网络配置;
* - 启动 SNTP 同步网络北京时间,供本地环境数据生成时间戳;
* - 初始化 E-Paper SPI/GPIO 硬件总线;
* - 初始化 UART 空气质量传感器(TVOC/CH₂O/CO₂)与 DHT11 温湿度传感器驱动。
* 2. 主循环(run()):
* - 定时从本地硬件驱动采集传感器读数并聚合为 DashboardData;
* - 比较当前帧与历史帧数据,有变化时才驱动墨水屏刷新(延长墨水屏使用寿命);
* - 根据刷新次数与内容变化自适应选择快速黑白刷新(~1.6s)或全量刷新(消除残影);
* - 进入低功耗休眠等待下一轮刷新周期。
*/
#ifndef APP_H
#define APP_H
#include "DashboardFetcher.h"
#include "Dht11Driver.h"
#include "DisplayManager.h"
#include "SensorDriver.h"
#include "WifiManager.h"
#include <optional>
#include <string>
class App {
public:
/**
* @brief 构造函数,初始化基础组件引脚与配置。
*/
App();
/**
* @brief 析构函数。
*/
~App();
/**
* @brief 初始化系统所有子系统。
*
* 包含:
* - WiFi 连接与网络参数就绪检测;
* - SNTP 时间同步(重试机制);
* - 墨水屏底层 SPI/GPIO 接口初始化;
* - DHT11 与 UART 气体传感器硬件接口初始化。
*/
void init();
/**
* @brief 主运行循环(阻塞)。
*
* 执行环境数据周期性采集、变化检测、墨水屏快刷/全刷决策与低功耗等待。
*/
void run();
private:
WifiManager wifi_; ///< WiFi 连接管理器(负责连接建立、IP/MAC/SSID/状态维护)
DisplayManager display_; ///< 墨水屏 UI 渲染与刷新驱动管理器
SensorDriver sensorDriver_; ///< UART 空气质量传感器驱动(CO₂、TVOC、甲醛 CH₂O 读数)
Dht11Driver dht11Driver_; ///< DHT11 单总线温湿度传感器驱动(室内温度、相对湿度)
DashboardFetcher dashFetcher_; ///< 本地环境数据聚合器(聚合传感器与系统时间)
/**
* @brief 缓存上一次刷新所使用的展示数据。
* 用于数据变更比对,数据完全一致时跳过刷新以延长墨水屏寿命。
*/
std::optional<DashboardData> lastDashData_;
/**
* @brief 缓存上一次红色图层区域的内容指纹(仅在 BWR 模式下生效)。
* 当红色图层内容发生变化时,必须触发全量三色刷新以更新红色像素。
*/
std::string lastRedContent_;
/**
* @brief 连续快速刷新计数器。
* 墨水屏在多次快速黑白刷新后可能产生背景残影,
* 计数器累计达到阈值(约每 3 小时)时强制触发一次全量刷新以消除残影。
*/
int fullRefreshCounter_ = 0;
};
#endif // APP_H
+3
View File
@@ -0,0 +1,3 @@
idf_component_register(SRCS "main.cpp" "WifiManager.cpp" "DashboardFetcher.cpp" "SensorDriver.cpp" "DisplayManager.cpp" "App.cpp" "Dht11Driver.cpp"
INCLUDE_DIRS "."
REQUIRES epdiy_bwr chinese_font nvs_flash esp_wifi esp_netif esp_event esp_driver_gpio esp_driver_uart esp_timer driver)
+73
View File
@@ -0,0 +1,73 @@
/**
* @file DashboardFetcher.cpp
* @brief 本地环境数据聚合器实现(无需后端服务)。
*/
#include "DashboardFetcher.h"
#include <cmath>
#include <cstdio>
#include <ctime>
#include "esp_log.h"
static const char *TAG = "DashboardFetcher";
DashboardFetcher::DashboardFetcher(SensorDriver &sensorDriver,
Dht11Driver &dht11Driver)
: sensorDriver_(sensorDriver), dht11Driver_(dht11Driver) {}
DashboardFetcher::~DashboardFetcher() {}
std::optional<DashboardData> DashboardFetcher::fetch() {
ESP_LOGI(TAG, "开始执行本地传感器采集与数据聚合...");
DashboardData data;
char buf[32];
// 1. 读取 DHT11 温湿度
float temp = NAN;
float humi = NAN;
bool dhtOk = dht11Driver_.read(temp, humi);
if (dhtOk && !std::isnan(temp)) {
snprintf(buf, sizeof(buf), "%.1f", temp);
data.temperature = buf;
}
if (dhtOk && !std::isnan(humi)) {
snprintf(buf, sizeof(buf), "%.1f", humi);
data.humidity = buf;
}
// 2. 读取 UART 空气质量传感器(CO2, TVOC, CH2O)
SensorReading air = sensorDriver_.read();
if (!std::isnan(air.co2) && air.co2 > 0) {
snprintf(buf, sizeof(buf), "%.0f", air.co2);
data.co2 = buf;
}
if (!std::isnan(air.tvoc) && air.tvoc >= 0) {
snprintf(buf, sizeof(buf), "%.2f", air.tvoc);
data.tvoc = buf;
}
if (!std::isnan(air.ch2o) && air.ch2o >= 0) {
snprintf(buf, sizeof(buf), "%.3f", air.ch2o);
data.ch2o = buf;
}
// 3. 读取本地系统时间(通过 NTP 同步)
time_t now = time(nullptr);
struct tm timeinfo;
localtime_r(&now, &timeinfo);
if (timeinfo.tm_year >= (2020 - 1900)) {
snprintf(buf, sizeof(buf), "%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min);
data.lastReport = buf;
} else {
data.lastReport = "--:--";
}
ESP_LOGI(TAG, "本地数据聚合完成: 温度=%s, 湿度=%s, CO2=%s, TVOC=%s, CH2O=%s, 时间=%s",
data.temperature.c_str(), data.humidity.c_str(),
data.co2.c_str(), data.tvoc.c_str(), data.ch2o.c_str(),
data.lastReport.c_str());
return data;
}
+66
View File
@@ -0,0 +1,66 @@
/**
* @file DashboardFetcher.h
* @brief 本地环境数据聚合器。
*
* 无需后端服务器支持,直接从本地硬件传感器(DHT11 + UART 空气质量传感器)
* 以及 ESP32 本地系统时钟(NTP)采集环境数据并生成墨水屏展示数据。
*/
#ifndef DASHBOARD_FETCHER_H
#define DASHBOARD_FETCHER_H
#include "Dht11Driver.h"
#include "SensorDriver.h"
#include <optional>
#include <string>
/**
* @brief 墨水屏展示所需的环境数据结构体。
*/
struct DashboardData {
// 室内/本地环境传感器读数
std::string temperature; ///< 温度(℃),如 "24.5"
std::string humidity; ///< 湿度(%),如 "60.2"
std::string co2; ///< CO₂ 浓度(ppm),如 "450"
std::string tvoc; ///< TVOC 浓度(mg/m³),如 "0.08"
std::string ch2o; ///< 甲醛浓度(mg/m³),如 "0.01"
std::string lastReport; ///< 本地采集时间,如 "14:30"
/// 用于检测数据是否有变化,避免无意义的墨水屏全刷
bool operator==(const DashboardData &other) const {
return temperature == other.temperature &&
humidity == other.humidity &&
co2 == other.co2 &&
tvoc == other.tvoc &&
ch2o == other.ch2o &&
lastReport == other.lastReport;
}
bool operator!=(const DashboardData &other) const {
return !(*this == other);
}
};
/**
* @brief 负责从本地传感器与系统时钟聚合环境数据。
*/
class DashboardFetcher {
public:
/**
* @brief 构造函数。
* @param sensorDriver UART 空气质量传感器驱动引用
* @param dht11Driver DHT11 温湿度传感器驱动引用
*/
DashboardFetcher(SensorDriver &sensorDriver, Dht11Driver &dht11Driver);
~DashboardFetcher();
/**
* @brief 执行本地采样并聚合生成展示数据。
* @return 成功返回聚合好的 DashboardData
*/
std::optional<DashboardData> fetch();
private:
SensorDriver &sensorDriver_;
Dht11Driver &dht11Driver_;
};
#endif // DASHBOARD_FETCHER_H
+130
View File
@@ -0,0 +1,130 @@
#include "Dht11Driver.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_rom_sys.h"
static const char *TAG = "Dht11Driver";
Dht11Driver::Dht11Driver(gpio_num_t pin)
: pin_(pin), lastTemperature_(NAN), lastHumidity_(NAN), lastReadTimeUs_(0) {}
Dht11Driver::~Dht11Driver() {}
void Dht11Driver::init() {
ESP_LOGI(TAG, "初始化 DHT11 引脚: GPIO%d", pin_);
gpio_reset_pin(pin_);
gpio_set_pull_mode(pin_, GPIO_PULLUP_ONLY);
gpio_set_direction(pin_, GPIO_MODE_INPUT);
}
// 辅助函数:等待指定电平状态,超时返回 -1,成功返回耗时(us)
static inline int wait_for_level(gpio_num_t pin, int level, int timeout_us) {
int elapsed = 0;
while (gpio_get_level(pin) != level) {
if (elapsed >= timeout_us) return -1;
esp_rom_delay_us(1);
elapsed++;
}
return elapsed;
}
bool Dht11Driver::read(float& temperature, float& humidity) {
static portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED;
int err_phase = 0; // 0=OK, 1=PhaseA, 2=PhaseB, 3=PhaseC, 4=LowTimeout, 5=HighTimeout
int err_bit = 0;
int64_t now = esp_timer_get_time();
uint8_t data[5] = {0,0,0,0,0};
// 开机前 3 秒内,强制阻塞等待(而不是直接抛弃),确保开机第一次上报就有温湿度!
if (now < 3000000) {
int wait_ms = (3000000 - now) / 1000;
ESP_LOGI(TAG, "传感器刚上电,强制耐心等待 %d 毫秒...", wait_ms);
vTaskDelay(pdMS_TO_TICKS(wait_ms));
now = esp_timer_get_time();
}
if (now - lastReadTimeUs_ < 2200000 && lastReadTimeUs_ != 0) {
ESP_LOGI(TAG, "距离上次采集太近,直接返回缓存");
goto ret_cache;
}
lastReadTimeUs_ = now;
// 1. 发送启动信号:MCU 拉低至少 18ms
gpio_set_direction(pin_, GPIO_MODE_OUTPUT);
gpio_set_level(pin_, 0);
esp_rom_delay_us(20000); // 20ms
// 2. MCU 释放总线(拉高),等待 20-40us
gpio_set_level(pin_, 1);
esp_rom_delay_us(30);
gpio_set_direction(pin_, GPIO_MODE_INPUT);
// 3. 开始临界区,在这个 4~5 毫秒的时间内,我们将暂停 Wi-Fi 中断和操作系统任务调度
// 不然一旦 CPU 去处理网络包,几十微秒的时间窗口瞬间就会错过并导致“超时”
portENTER_CRITICAL(&mux); // 关闭中断保护时序
// 等待传感器拉低 (Phase A,传感器响应起始)
if (wait_for_level(pin_, 0, 80) < 0) { err_phase = 1; goto end_critical; }
// 等待传感器拉高 (Phase B,传感器准备发送数据)
if (wait_for_level(pin_, 1, 80) < 0) { err_phase = 2; goto end_critical; }
// 等待传感器再次拉低 (开始发送 40 bit 数据)
if (wait_for_level(pin_, 0, 80) < 0) { err_phase = 3; goto end_critical; }
// 读取 40 个 bit (5 bytes)
for (int i = 0; i < 40; i++) {
// 等待低电平结束 (50us)
if (wait_for_level(pin_, 1, 70) < 0) { err_phase = 4; err_bit = i; goto end_critical; }
// 高电平持续时间决定了这 bit 是 0 还是 1. (26-28us = 0, 70us = 1)
int high_time = wait_for_level(pin_, 0, 100);
if (high_time < 0) { err_phase = 5; err_bit = i; goto end_critical; }
data[i / 8] <<= 1;
if (high_time > 40) {
data[i / 8] |= 1;
}
}
end_critical:
portEXIT_CRITICAL(&mux); // 退出临界区,恢复系统调度和所有的硬件中断
if (err_phase != 0) {
if (err_phase == 1) ESP_LOGE(TAG, "读取失败: 传感器无响应 (Phase A timeout)");
else if (err_phase == 2) ESP_LOGE(TAG, "读取失败: 传感器握手失败 (Phase B timeout)");
else if (err_phase == 3) ESP_LOGE(TAG, "读取失败: 传感器未开始发数据 (Phase C timeout)");
else if (err_phase == 4) ESP_LOGE(TAG, "位读取失败: 低电平超时 bit %d", err_bit);
else if (err_phase == 5) ESP_LOGE(TAG, "位读取失败: 高电平超时 bit %d", err_bit);
goto ret_fail;
}
// 7. 校验和检验
if (data[4] == ((data[0] + data[1] + data[2] + data[3]) & 0xFF)) {
lastHumidity_ = data[0] + data[1] * 0.1f;
lastTemperature_ = data[2] + data[3] * 0.1f;
temperature = lastTemperature_;
humidity = lastHumidity_;
ESP_LOGI(TAG, "自写驱动读取成功: T=%.1f°C, H=%.1f%%", temperature, humidity);
return true;
} else {
ESP_LOGE(TAG, "Checksum failed: %02x %02x %02x %02x = %02x", data[0], data[1], data[2], data[3], data[4]);
goto ret_fail;
}
ret_cache:
temperature = lastTemperature_;
humidity = lastHumidity_;
return !std::isnan(lastTemperature_);
ret_fail:
lastTemperature_ = NAN;
lastHumidity_ = NAN;
temperature = NAN;
humidity = NAN;
return false;
}
+48
View File
@@ -0,0 +1,48 @@
/**
* @file Dht11Driver.h
* @brief DHT11 温湿度传感器驱动封装。
*
* 基于 esp-idf-lib 的 dht 组件,专门设计了防堵塞和失败反馈机制,
* 在读取失败或未连接传感器时安全退回 NAN。
*/
#ifndef DHT11_DRIVER_H
#define DHT11_DRIVER_H
#include "driver/gpio.h"
#include <cmath>
class Dht11Driver {
public:
/**
* @brief 构造函数
* @param pin DHT11 的数据引脚
*/
Dht11Driver(gpio_num_t pin);
~Dht11Driver();
/**
* @brief 初始化硬件和引脚状态
*/
void init();
/**
* @brief 读取环境温度和相对湿度
*
* 内部使用 esp-idf-lib 的 dht_read_float_data() 读取。
* 如果采样间隔过短(小于 2 秒),将返回上一次的缓存结果。
*
* @param [out] temperature 存放产出的温度(℃),失败为 NAN
* @param [out] humidity 存放产出的湿度(%RH),失败为 NAN
* @return true 成功读取有效数据(或返回缓存数据)
* @return false 读取失败(比如未接传感器,或时序错乱),此时 temperature 和 humidity 为 NAN
*/
bool read(float& temperature, float& humidity);
private:
gpio_num_t pin_;
float lastTemperature_;
float lastHumidity_;
int64_t lastReadTimeUs_;
};
#endif // DHT11_DRIVER_H
+275
View File
@@ -0,0 +1,275 @@
/**
* @file DisplayManager.cpp
* @brief 环境监测与网络信息墨水屏 UI 绘制实现。
*
* 屏幕:400×300,通过 CONFIG_DISPLAY_BWR / CONFIG_DISPLAY_BW 编译宏
* 区分三色(黑/白/红)和纯黑白两种模式。
* 英文字体:Font8, Font12, Font16, Font20, Font24
* 中文字体:Font16CN(Unifont 16×16)
*/
#include "DisplayManager.h"
#include "ChineseFont.h"
#include "EPD.h"
#include "EPD_SPI.h"
#include "GUI_Paint.h"
#include "esp_log.h"
#include <cstdio>
#include <cstring>
#include <cstdlib>
static const char *TAG = "DisplayManager";
// 帧缓冲区
static unsigned char ImageBW[EPD_ARRAY]; ///< 黑白像素缓冲
#ifdef CONFIG_DISPLAY_BWR
static unsigned char ImageRW[EPD_ARRAY]; ///< 红白像素缓冲(仅 BWR 模式使用)
#endif
DisplayManager::DisplayManager() {}
DisplayManager::~DisplayManager() {}
void DisplayManager::initHardware() {
EPD_SPI_Init();
}
void DisplayManager::beginDrawing() {
Paint_NewImage(ImageBW, EPD_WIDTH, EPD_HEIGHT, 0, WHITE);
Paint_SelectImage(ImageBW);
Paint_Clear(WHITE);
#ifdef CONFIG_DISPLAY_BWR
Paint_NewImage(ImageRW, EPD_WIDTH, EPD_HEIGHT, 0, WHITE);
Paint_SelectImage(ImageRW);
Paint_Clear(WHITE);
Paint_SelectImage(ImageBW);
#endif
}
void DisplayManager::updateFullAndSleep() {
#ifdef CONFIG_DISPLAY_BWR
ESP_LOGI(TAG, "全量三色刷新墨水屏...");
EPD_HW_Init_GUI();
EPD_Display(ImageBW, ImageRW);
#else
ESP_LOGI(TAG, "全量黑白刷新墨水屏...");
EPD_HW_Init_GUI();
static unsigned char emptyRW[EPD_ARRAY] = {0};
EPD_Display(ImageBW, emptyRW);
#endif
EPD_DeepSleep();
}
void DisplayManager::updateFastAndSleep() {
ESP_LOGI(TAG, "快速刷新墨水屏...");
EPD_HW_Init_Fast();
#ifdef CONFIG_DISPLAY_BWR
EPD_WhiteScreen_ALL_Fast(ImageBW, ImageRW);
#else
static unsigned char emptyRW[EPD_ARRAY] = {0};
EPD_WhiteScreen_ALL_Fast(ImageBW, emptyRW);
#endif
EPD_DeepSleep();
}
void DisplayManager::switchColor(bool toRed) {
#ifdef CONFIG_DISPLAY_BWR
Paint_SelectImage(toRed ? ImageRW : ImageBW);
#else
(void)toRed;
Paint_SelectImage(ImageBW);
#endif
}
// =========================================================================
// 顶部横幅:标题 + WiFi 状态
// Y: 0 ~ 28
// =========================================================================
void DisplayManager::drawTopBanner(const std::string &networkStatus) {
#ifdef CONFIG_DISPLAY_BWR
switchColor(true);
Paint_DrawRectangle(0, 0, EPD_WIDTH, 26, RED, DRAW_FILL_FULL, DOT_PIXEL_1X1);
Paint_DrawString_UTF8(10, 5, "xInfo \xe7\x8e\xaf\xe5\xa2\x83\xe7\x9b\x91\xe6\xb5\x8b\xe7\xbb\x88\xe7\xab\xaf", Font16CN, WHITE, RED); // xInfo 环境监测终端
char buf[32];
snprintf(buf, sizeof(buf), "[%s]", networkStatus.c_str());
int strW = static_cast<int>(strlen(buf)) * 8;
int offsetX = EPD_WIDTH - strW - 10;
if (offsetX < 220) offsetX = 220;
Paint_DrawString_EN(offsetX, 8, buf, &Font12, WHITE, RED);
switchColor(false);
#else
Paint_DrawRectangle(0, 0, EPD_WIDTH, 26, BLACK, DRAW_FILL_FULL, DOT_PIXEL_1X1);
Paint_DrawString_UTF8(10, 5, "xInfo \xe7\x8e\xaf\xe5\xa2\x83\xe7\x9b\x91\xe6\xb5\x8b\xe7\xbb\x88\xe7\xab\xaf", Font16CN, WHITE, BLACK); // xInfo 环境监测终端
char buf[32];
snprintf(buf, sizeof(buf), "[%s]", networkStatus.c_str());
int strW = static_cast<int>(strlen(buf)) * 8;
int offsetX = EPD_WIDTH - strW - 10;
if (offsetX < 220) offsetX = 220;
Paint_DrawString_EN(offsetX, 8, buf, &Font12, WHITE, BLACK);
#endif
}
// =========================================================================
// 核心温湿度看板区
// Y: 32 ~ 148
// =========================================================================
void DisplayManager::drawThermoHygrometer(const DashboardData &data) {
char buf[32];
// 更新时间(靠右显示)
if (!data.lastReport.empty()) {
char timeStr[48];
snprintf(timeStr, sizeof(timeStr), "\xe6\x9b\xb4\xe6\x96\xb0: %s", data.lastReport.c_str()); // 更新:
Paint_DrawString_UTF8(290, 34, timeStr, Font16CN, BLACK, WHITE);
}
// 左侧:室内温度看板 (X: 10 ~ 190)
Paint_DrawString_UTF8(15, 36, "\xe5\xae\xa4\xe5\x86\x85\xe6\xb8\xa9\xe5\xba\xa6", Font16CN, BLACK, WHITE); // 室内温度
if (!data.temperature.empty()) {
snprintf(buf, sizeof(buf), "%s", data.temperature.c_str());
Paint_DrawString_EN(15, 68, buf, &Font24, BLACK, WHITE);
int numW = static_cast<int>(strlen(buf)) * 17 + 12;
Paint_DrawCircle(numW + 8, 72, 3, BLACK, DRAW_FILL_EMPTY, DOT_PIXEL_1X1);
Paint_DrawString_EN(numW + 15, 70, "C", &Font20, BLACK, WHITE);
// 舒适度判断小标签
float tempVal = strtof(data.temperature.c_str(), nullptr);
if (tempVal < 18.0f) {
Paint_DrawString_UTF8(15, 115, "[\xe5\x81\x8f\xe5\x87\x89]", Font16CN, BLACK, WHITE); // [偏凉]
} else if (tempVal <= 26.0f) {
Paint_DrawString_UTF8(15, 115, "[\xe8\x88\x92\xe9\x80\x82]", Font16CN, BLACK, WHITE); // [舒适]
} else {
Paint_DrawString_UTF8(15, 115, "[\xe5\x81\x8f\xe7\x83\xad]", Font16CN, BLACK, WHITE); // [偏热]
}
} else {
Paint_DrawString_EN(15, 68, "--.-", &Font24, BLACK, WHITE);
}
// 中间竖向分隔线
Paint_DrawLine(200, 36, 200, 142, BLACK, LINE_STYLE_SOLID, DOT_PIXEL_1X1);
// 右侧:相对湿度看板 (X: 210 ~ 390)
Paint_DrawString_UTF8(215, 36, "\xe7\x9b\xb8\xe5\xaf\xb9\xe6\xb9\xbf\xe5\xba\xa6", Font16CN, BLACK, WHITE); // 相对湿度
if (!data.humidity.empty()) {
snprintf(buf, sizeof(buf), "%s%%", data.humidity.c_str());
Paint_DrawString_EN(215, 68, buf, &Font24, BLACK, WHITE);
// 湿度状态小标签
float humiVal = strtof(data.humidity.c_str(), nullptr);
if (humiVal < 40.0f) {
Paint_DrawString_UTF8(215, 115, "[\xe5\xb9\xb2\xe7\x87\xa5]", Font16CN, BLACK, WHITE); // [干燥]
} else if (humiVal <= 70.0f) {
Paint_DrawString_UTF8(215, 115, "[\xe9\x80\x82\xe5\xae\x9c]", Font16CN, BLACK, WHITE); // [适宜]
} else {
Paint_DrawString_UTF8(215, 115, "[\xe6\xbd\xae\xe6\xb9\xbf]", Font16CN, BLACK, WHITE); // [潮湿]
}
} else {
Paint_DrawString_EN(215, 68, "--.-%", &Font24, BLACK, WHITE);
}
}
// =========================================================================
// 空气质量指标多维度展示区
// Y: 152 ~ 265
// =========================================================================
void DisplayManager::drawAirQuality(const DashboardData &data) {
// 横向分隔线
Paint_DrawLine(10, 150, EPD_WIDTH - 10, 150, BLACK, LINE_STYLE_SOLID, DOT_PIXEL_1X1);
// 区域标题
Paint_DrawString_UTF8(10, 156, "\xe3\x80\x90 \xe7\xa9\xba\xe6\xb0\x94\xe8\xb2\xb4\xe9\x87\x8f\xe6\x8c\x87\xe6\xa0\x87 \xe3\x80\x91", Font16CN, BLACK, WHITE); // 【 空气质量指标 】
// 第 1 列:CO2 (X: 15 ~ 125)
Paint_DrawString_EN(15, 180, "CO", &Font16, BLACK, WHITE);
Paint_DrawString_EN(37, 184, "2", &Font12, BLACK, WHITE);
Paint_DrawString_EN(47, 182, "(ppm)", &Font12, BLACK, WHITE);
if (!data.co2.empty()) {
Paint_DrawString_EN(15, 204, data.co2.c_str(), &Font20, BLACK, WHITE);
float co2Val = strtof(data.co2.c_str(), nullptr);
if (co2Val <= 800.0f) {
Paint_DrawString_UTF8(15, 236, "\xe4\xbc\x98", Font16CN, BLACK, WHITE); // 优
} else if (co2Val <= 1200.0f) {
Paint_DrawString_UTF8(15, 236, "\xe8\x89\xaf", Font16CN, BLACK, WHITE); // 良
} else {
Paint_DrawString_UTF8(15, 236, "\xe9\x9c\x80\xe9\x80\x9a\xe9\xa3\x8e", Font16CN, BLACK, WHITE); // 需通风
}
} else {
Paint_DrawString_EN(15, 204, "--", &Font20, BLACK, WHITE);
}
// 列分割线 1
Paint_DrawLine(135, 180, 135, 255, BLACK, LINE_STYLE_DOTTED, DOT_PIXEL_1X1);
// 第 2 列:TVOC (X: 145 ~ 255)
Paint_DrawString_EN(145, 180, "TVOC", &Font16, BLACK, WHITE);
Paint_DrawString_EN(193, 182, "(mg/m", &Font12, BLACK, WHITE);
Paint_DrawString_EN(241, 179, "3", &Font8, BLACK, WHITE);
Paint_DrawString_EN(248, 182, ")", &Font12, BLACK, WHITE);
if (!data.tvoc.empty()) {
Paint_DrawString_EN(145, 204, data.tvoc.c_str(), &Font20, BLACK, WHITE);
float tvocVal = strtof(data.tvoc.c_str(), nullptr);
if (tvocVal <= 0.6f) {
Paint_DrawString_UTF8(145, 236, "\xe6\xad\xa3\xe5\xb8\xb8", Font16CN, BLACK, WHITE); // 正常
} else {
Paint_DrawString_UTF8(145, 236, "\xe8\xb6\x85\xe6\xa0\x87", Font16CN, BLACK, WHITE); // 超标
}
} else {
Paint_DrawString_EN(145, 204, "--", &Font20, BLACK, WHITE);
}
// 列分割线 2
Paint_DrawLine(265, 180, 265, 255, BLACK, LINE_STYLE_DOTTED, DOT_PIXEL_1X1);
// 第 3 列:CH2O (X: 275 ~ 385)
Paint_DrawString_EN(275, 180, "CH", &Font16, BLACK, WHITE);
Paint_DrawString_EN(297, 184, "2", &Font12, BLACK, WHITE);
Paint_DrawString_EN(307, 180, "O", &Font16, BLACK, WHITE);
Paint_DrawString_EN(320, 182, "(mg/m", &Font12, BLACK, WHITE);
Paint_DrawString_EN(368, 179, "3", &Font8, BLACK, WHITE);
Paint_DrawString_EN(375, 182, ")", &Font12, BLACK, WHITE);
if (!data.ch2o.empty()) {
Paint_DrawString_EN(275, 204, data.ch2o.c_str(), &Font20, BLACK, WHITE);
float ch2oVal = strtof(data.ch2o.c_str(), nullptr);
if (ch2oVal <= 0.08f) {
Paint_DrawString_UTF8(275, 236, "\xe5\xae\x89\xe5\x85\xa8", Font16CN, BLACK, WHITE); // 安全
} else {
Paint_DrawString_UTF8(275, 236, "\xe8\xb6\x85\xe6\xa0\x87", Font16CN, BLACK, WHITE); // 超标
}
} else {
Paint_DrawString_EN(275, 204, "--", &Font20, BLACK, WHITE);
}
}
// =========================================================================
// 底部网络及硬件信息栏
// Y: 268 ~ 300
// =========================================================================
void DisplayManager::drawBottomInfoBar(const std::string &ip,
const std::string &mac) {
Paint_DrawLine(0, EPD_HEIGHT - 25, EPD_WIDTH, EPD_HEIGHT - 25, BLACK,
LINE_STYLE_SOLID, DOT_PIXEL_1X1);
char buf[64];
snprintf(buf, sizeof(buf), "IP: %s", ip.empty() ? "0.0.0.0" : ip.c_str());
Paint_DrawString_EN(10, EPD_HEIGHT - 18, buf, &Font12, BLACK, WHITE);
snprintf(buf, sizeof(buf), "MAC: %s", mac.c_str());
int macX = EPD_WIDTH - static_cast<int>(strlen(buf)) * 7 - 10;
if (macX < 200) macX = 200;
Paint_DrawString_EN(macX, EPD_HEIGHT - 18, buf, &Font12, BLACK, WHITE);
}
// =========================================================================
// 错误页面
// =========================================================================
void DisplayManager::drawError(const std::string &errorMsg) {
Paint_DrawString_UTF8(10, 80, "\xe7\xb3\xbb\xe7\xbb\x9f\xe5\xbc\x82\xe5\xb8\xb8", Font16CN, BLACK, WHITE); // 系统异常
Paint_DrawString_EN(10, 120, errorMsg.c_str(), &Font16, BLACK, WHITE);
Paint_DrawString_UTF8(10, 150, "\xe5\xb0\x86\xe5\x9c\xa8\xe4\xb8\x8b\xe4\xb8\xaa\xe5\x91\xa8\xe6\x9c\x9f\xe9\x87\x8d\xe8\xaf\x95...", Font16CN, BLACK, WHITE); // 将在下个周期重试...
}
+64
View File
@@ -0,0 +1,64 @@
/**
* @file DisplayManager.h
* @brief 墨水屏 UI 管理器,负责环境监测大屏数据的绘制。
*
* 屏幕尺寸:400 × 300 像素,三色(黑/白/红)或纯黑白模式。
* 布局分区:
* ┌──────────────────────────────────────────────┐
* │ 顶部横幅(标题 + WiFi 连接状态) │ ~28px
* ├──────────────────────────────────────────────┤
* │ 【温湿度看板】 │
* │ 室内温度 (Font24) | 相对湿度 (Font24) │ ~125px
* ├──────────────────────────────────────────────┤
* │ 【空气质量指标】 │
* │ CO₂ ppm | TVOC mg/m³ | CH₂O mg/m³ │ ~110px
* ├──────────────────────────────────────────────┤
* │ 底部网络信息栏(IP + MAC 地址) │ ~25px
* └──────────────────────────────────────────────┘
*/
#ifndef DISPLAY_MANAGER_H
#define DISPLAY_MANAGER_H
#include "DashboardFetcher.h"
#include <string>
class DisplayManager {
public:
DisplayManager();
~DisplayManager();
/// 初始化 E-Paper SPI / GPIO 硬件
void initHardware();
/// 清空帧缓冲区,准备新一帧绘制
void beginDrawing();
/// 全量刷新并进入深度休眠
void updateFullAndSleep();
/// 快速刷新并进入深度休眠(黑白模式或仅更新黑白图层)
void updateFastAndSleep();
// ====== UI 绘制方法 ======
/// 绘制顶部横幅(应用标题 + WiFi 状态)
void drawTopBanner(const std::string &networkStatus);
/// 绘制温湿度核心看板(大字号温度、湿度与采集时间)
void drawThermoHygrometer(const DashboardData &data);
/// 绘制空气质量指标区(CO2、TVOC、甲醛三列卡片)
void drawAirQuality(const DashboardData &data);
/// 绘制底部网络及设备信息栏(IP + MAC)
void drawBottomInfoBar(const std::string &ip, const std::string &mac);
/// 绘制全屏错误提示(网络异常等)
void drawError(const std::string &errorMsg);
private:
/// 切换绘制目标到红色图层(true)或黑白图层(false)
void switchColor(bool toRed);
};
#endif // DISPLAY_MANAGER_H
+55
View File
@@ -0,0 +1,55 @@
menu " 网络配置"
config WIFI_SSID
string "WiFi 名称"
default "seaHi_2.4G"
help
WiFi 名称 (SSID)
config WIFI_PASSWORD
string "WiFi 密码"
default "mypassword"
help
WiFi 密码
config NTP_SERVER
string "NTP 服务器地址"
default "ntp1.aliyun.com"
help
用于系统时间同步的 NTP 服务器地址。
国内推荐:ntp1.aliyun.com、ntp.tencent.com
国际推荐:pool.ntp.org、time.google.com
config NTP_SYNC_TIMEOUT_SEC
int "NTP 同步超时时间 (秒)"
default 15
range 5 60
help
等待 NTP 时间同步完成的最大超时时间(秒)。
默认 15 秒,范围 5 ~ 60 秒。
endmenu
menu " 硬件与显示配置"
choice DISPLAY_COLOR_MODE
prompt "墨水屏颜色模式"
default DISPLAY_BWR
help
选择墨水屏的颜色类型。
config DISPLAY_BWR
bool "黑白红三色 (BWR)"
config DISPLAY_BW
bool "纯黑白 (BW)"
endchoice
config DISPLAY_REFRESH_INTERVAL_MIN
int "墨水屏刷新间隔 (分钟)"
default 15
range 1 1440
help
每次采集数据并刷新墨水屏的时间间隔(分钟)。
默认 15 分钟。范围 1 分钟 ~ 1440 分钟(1 天)。
endmenu
+152
View File
@@ -0,0 +1,152 @@
/**
* @file SensorDriver.cpp
* @brief UART 空气质量传感器驱动实现。
*
* 使用 ESP-IDF UART 驱动接收传感器主动推送的 9 字节数据帧,
* 解析 TVOC / CH₂O / CO₂ 三项空气质量指标。
*/
#include "SensorDriver.h"
#include <cstring>
#include "driver/uart.h"
#include "esp_log.h"
static const char *TAG = "SensorDriver";
/// 使用 UART1(UART0 被 ESP32 系统日志占用)
static constexpr uart_port_t kUartPort = UART_NUM_1;
/// UART 接收缓冲区大小(字节),足以容纳多帧数据
static constexpr int kBufSize = 256;
/// 传感器数据引脚定义(与硬件接线对应)
static constexpr int kRxPin = 0; ///< GPIO0 接传感器 B(TXD)
static constexpr int kTxPin = 1; ///< GPIO1 接传感器 A(RXD)
SensorDriver::SensorDriver() {}
SensorDriver::~SensorDriver() {}
// --------------------------------------------------------------------------
// 初始化 UART1:9600bps, 8N1
// --------------------------------------------------------------------------
void SensorDriver::init() {
ESP_LOGI(TAG, "初始化 UART1 (RX=GPIO%d, TX=GPIO%d, 9600bps)", kRxPin, kTxPin);
uart_config_t uartConfig = {};
uartConfig.baud_rate = 9600;
uartConfig.data_bits = UART_DATA_8_BITS;
uartConfig.parity = UART_PARITY_DISABLE;
uartConfig.stop_bits = UART_STOP_BITS_1;
uartConfig.flow_ctrl = UART_HW_FLOWCTRL_DISABLE;
uartConfig.source_clk = UART_SCLK_DEFAULT;
// 安装 UART 驱动并配置引脚
ESP_ERROR_CHECK(uart_driver_install(kUartPort, kBufSize * 2, 0, 0, nullptr, 0));
ESP_ERROR_CHECK(uart_param_config(kUartPort, &uartConfig));
ESP_ERROR_CHECK(uart_set_pin(kUartPort, kTxPin, kRxPin,
UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE));
ESP_LOGI(TAG, "UART1 初始化完成");
}
// --------------------------------------------------------------------------
// 高低字节换算为浓度:(high * 256 + low) * 0.001 mg/m³
// --------------------------------------------------------------------------
float SensorDriver::toConcentration(uint8_t high, uint8_t low) const {
return static_cast<float>(high * 256 + low) * 0.001f;
}
// --------------------------------------------------------------------------
// 从 UART 缓冲区读取一帧数据并解析
//
// 读取策略:
// 1. 先清空 UART 接收缓冲区中的旧数据
// 2. 等待传感器推送新的一帧数据(短轮询,避免长时间阻塞)
// 3. 读取实际可用的字节数(而非请求固定大小导致阻塞)
// 4. 从后向前搜索最新的完整帧 → 校验和 → 解析浓度
// --------------------------------------------------------------------------
SensorReading SensorDriver::read(int timeoutMs) {
// 默认返回全 NAN(表示读取失败)
SensorReading result = { NAN, NAN, NAN };
// 清空 UART 缓冲区中的旧数据,确保后续读到的是最新帧
uart_flush_input(kUartPort);
// 等待传感器新数据到达(轮询方式,每 100ms 检查一次)
// 传感器通常每 1~2 秒推送一帧,所以最多等 timeoutMs 即可
int waitedMs = 0;
size_t available = 0;
while (waitedMs < timeoutMs) {
uart_get_buffered_data_len(kUartPort, &available);
if (available >= static_cast<size_t>(kFrameSize)) {
// 缓冲区中已有至少一帧数据,可以读取
break;
}
vTaskDelay(pdMS_TO_TICKS(100)); // 短暂休眠,释放 CPU
waitedMs += 100;
}
if (available < static_cast<size_t>(kFrameSize)) {
ESP_LOGW(TAG, "UART 等待 %dms 超时,未收到足够数据(仅 %d 字节)",
timeoutMs, static_cast<int>(available));
return result;
}
// 只读取实际可用的字节数(不超过缓冲区上限),避免阻塞
int toRead = (available > static_cast<size_t>(kBufSize))
? kBufSize
: static_cast<int>(available);
uint8_t buf[kBufSize];
int len = uart_read_bytes(kUartPort, buf, toRead, pdMS_TO_TICKS(100));
if (len <= 0) {
ESP_LOGW(TAG, "UART 读取返回 %d,无有效数据", len);
return result;
}
ESP_LOGI(TAG, "UART 收到 %d 字节", len);
// 在缓冲区中从后向前搜索最后一个完整帧(取最新数据)
int frameStart = -1;
for (int i = len - kFrameSize; i >= 0; i--) {
if (buf[i] == kHeaderB1 && buf[i + 1] == kHeaderB2) {
frameStart = i;
break;
}
}
if (frameStart < 0) {
ESP_LOGW(TAG, "未找到有效帧头 (0x2C 0xE4)");
return result;
}
// 提取 9 字节帧
uint8_t *frame = &buf[frameStart];
// 校验和验证:uint8(B1 + B2 + ... + B8) == B9
uint8_t checksum = 0;
for (int i = 0; i < kFrameSize - 1; i++) {
checksum += frame[i];
}
if (checksum != frame[kFrameSize - 1]) {
ESP_LOGW(TAG, "校验和失败: 计算值=0x%02X, 接收值=0x%02X",
checksum, frame[kFrameSize - 1]);
return result;
}
// 解析各项浓度值
// TVOC 和 CH₂O 用 ×0.001 换算为 mg/m³
result.tvoc = toConcentration(frame[2], frame[3]);
result.ch2o = toConcentration(frame[4], frame[5]);
// CO₂ 直接用原始值(高×256+低)作为 ppm,不需要 ×0.001
result.co2 = static_cast<float>(frame[6] * 256 + frame[7]);
ESP_LOGI(TAG, "解析成功: TVOC=%.3f mg/m³, CH2O=%.3f mg/m³, CO2=%.0f ppm",
result.tvoc, result.ch2o, result.co2);
return result;
}
+74
View File
@@ -0,0 +1,74 @@
/**
* @file SensorDriver.h
* @brief UART 空气质量传感器驱动,解析 TVOC / CH₂O / CO₂ 数据帧。
*
* 传感器通信协议:
* 波特率 9600,8N1
* 数据帧固定 9 字节:
* B1(0x2C) B2(0xE4) B3(TVOC高) B4(TVOC低) B5(CH2O高) B6(CH2O低)
* B7(CO2高) B8(CO2低) B9(校验和)
*
* 浓度值(mg/m³) = (高字节 × 256 + 低字节) × 0.001
* 校验和:uint8(B1 + B2 + ... + B8)
*
* 硬件接线:
* 传感器 B(TXD) → ESP32-C3 GPIO0 (UART1_RX)
* 传感器 A(RXD) → ESP32-C3 GPIO1 (UART1_TX)
*/
#ifndef SENSOR_DRIVER_H
#define SENSOR_DRIVER_H
#include <cmath>
/// 传感器一次采集的结果
struct SensorReading {
float tvoc; ///< TVOC 浓度 (mg/m³),无效时为 NAN
float ch2o; ///< 甲醛浓度 (mg/m³),无效时为 NAN
float co2; ///< CO₂ 浓度 (ppm),无效时为 NAN(注意:单位与 TVOC/CH₂O 不同)
/// 检查是否所有字段都无效(读取失败时返回全 NAN)
bool isValid() const {
return !std::isnan(tvoc) || !std::isnan(ch2o) || !std::isnan(co2);
}
};
/**
* @brief UART 空气质量传感器驱动。
*
* 使用 ESP-IDF 的 UART 驱动,在 UART1 上以 9600bps 接收传感器帧数据。
* 传感器会主动推送数据,调用 read() 时从 UART 缓冲区读取并解析最新一帧。
*/
class SensorDriver {
public:
SensorDriver();
~SensorDriver();
/// 初始化 UART1 硬件(GPIO0=RX, GPIO1=TX, 9600bps, 8N1)
void init();
/**
* @brief 从 UART 缓冲区读取并解析一帧传感器数据。
*
* 读取时会在缓冲区中搜索帧头 0x2C 0xE4,然后读取完整 9 字节帧,
* 校验和通过后解析为 SensorReading。
*
* @param timeoutMs 等待数据的超时时间(毫秒),默认 3000ms
* @return SensorReading 结构体,读取失败时所有字段为 NAN
*/
SensorReading read(int timeoutMs = 3000);
private:
static constexpr int kFrameSize = 9; ///< 数据帧固定 9 字节
static constexpr uint8_t kHeaderB1 = 0x2C; ///< 帧头字节 1
static constexpr uint8_t kHeaderB2 = 0xE4; ///< 帧头字节 2
/**
* @brief 将高低字节换算为浓度值。
* @param high 高字节
* @param low 低字节
* @return 浓度值 (mg/m³)
*/
float toConcentration(uint8_t high, uint8_t low) const;
};
#endif // SENSOR_DRIVER_H
+148
View File
@@ -0,0 +1,148 @@
/**
* @file WifiManager.cpp
* @brief Implementation of the WifiManager class.
*/
#include "WifiManager.h"
#include "esp_log.h"
#include "esp_netif.h"
#include "esp_wifi.h"
#include <cstring>
static const char *TAG = "WifiManager";
// Event bits used to signal completion or failure of Wi-Fi connection
#define WIFI_CONNECTED_BIT BIT0
#define WIFI_FAIL_BIT BIT1
WifiManager::WifiManager(const std::string &ssid, const std::string &password)
: ssid_(ssid), password_(password), wifi_event_group_(nullptr) {}
WifiManager::~WifiManager() {
// Clean up registered event handlers and destroy the FreeRTOS event group.
if (wifi_event_group_) {
esp_event_handler_instance_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP,
instance_got_ip_);
esp_event_handler_instance_unregister(WIFI_EVENT, ESP_EVENT_ANY_ID,
instance_any_id_);
vEventGroupDelete(wifi_event_group_);
}
}
bool WifiManager::connect() {
// Create an event group to block main execution until connection finishes.
wifi_event_group_ = xEventGroupCreate();
// Initialize the underlying TCP/IP stack and default event loop.
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
esp_netif_create_default_wifi_sta();
// Initialize the Wi-Fi driver with standard configuration values.
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
// Register internal event handlers to process Wi-Fi and IP events.
ESP_ERROR_CHECK(esp_event_handler_instance_register(
WIFI_EVENT, ESP_EVENT_ANY_ID, &WifiManager::eventHandler, this,
&instance_any_id_));
ESP_ERROR_CHECK(esp_event_handler_instance_register(
IP_EVENT, IP_EVENT_STA_GOT_IP, &WifiManager::eventHandler, this,
&instance_got_ip_));
// Set up the Wi-Fi station configuration structures.
wifi_config_t wifi_config = {};
strncpy((char *)wifi_config.sta.ssid, ssid_.c_str(),
sizeof(wifi_config.sta.ssid) - 1);
strncpy((char *)wifi_config.sta.password, password_.c_str(),
sizeof(wifi_config.sta.password) - 1);
// Set the hardware mode to station mode (client) and commit settings.
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
ESP_ERROR_CHECK(esp_wifi_start());
ESP_LOGI(TAG, "wifi_init_sta finished.");
// Wait indefinitely until either the connected or failed bit is set.
EventBits_t bits =
xEventGroupWaitBits(wifi_event_group_, WIFI_CONNECTED_BIT | WIFI_FAIL_BIT,
pdFALSE, pdFALSE, portMAX_DELAY);
// Evaluate the outcome of the connection process.
if (bits & WIFI_CONNECTED_BIT) {
ESP_LOGI(TAG, "connected to ap SSID:%s", ssid_.c_str());
return true;
} else if (bits & WIFI_FAIL_BIT) {
ESP_LOGI(TAG, "Failed to connect to SSID:%s", ssid_.c_str());
return false;
} else {
ESP_LOGE(TAG, "UNEXPECTED EVENT");
return false;
}
}
void WifiManager::eventHandler(void *arg, esp_event_base_t event_base,
int32_t event_id, void *event_data) {
// Recover the 'this' instance from the void pointer argument.
WifiManager *instance = static_cast<WifiManager *>(arg);
if (!instance)
return;
// Dispatch the event to the appropriate handlers based on its base type.
if (event_base == WIFI_EVENT) {
instance->handleWifiEvent(event_id, event_data);
} else if (event_base == IP_EVENT) {
instance->handleIpEvent(event_id, event_data);
}
}
void WifiManager::handleWifiEvent(int32_t event_id, void *event_data) {
if (event_id == WIFI_EVENT_STA_START) {
// When the station has started, immediately kick off a connection.
esp_wifi_connect();
} else if (event_id == WIFI_EVENT_STA_DISCONNECTED) {
// Attempt continuous reconnection, without giving up.
ESP_LOGI(TAG, "retry to connect to the AP in 3 seconds...");
vTaskDelay(pdMS_TO_TICKS(3000));
esp_wifi_connect();
// Intentionally omit setting WIFI_FAIL_BIT to keep retrying indefinitely,
// ensuring the application never completely terminates due to network
// failure.
}
}
void WifiManager::handleIpEvent(int32_t event_id, void *event_data) {
if (event_id == IP_EVENT_STA_GOT_IP) {
// Received a DHCP lease from the AP.
ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data;
// Convert raw IP address into human-readable string.
char ip_buf[20];
sprintf(ip_buf, IPSTR, IP2STR(&event->ip_info.ip));
ip_addr_ = ip_buf;
// Retrieve and format the MAC address.
uint8_t mac[6];
esp_wifi_get_mac(WIFI_IF_STA, mac);
char mac_buf[20];
sprintf(mac_buf, "%02x:%02x:%02x:%02x:%02x:%02x", mac[0], mac[1], mac[2],
mac[3], mac[4], mac[5]);
mac_ = mac_buf;
// Fetch details like RSSI directly from the active AP connection record.
wifi_ap_record_t ap_info;
if (esp_wifi_sta_get_ap_info(&ap_info) == ESP_OK) {
rssi_ = ap_info.rssi;
}
// Signal the main task that the connection sequence has successfully
// completed.
xEventGroupSetBits(wifi_event_group_, WIFI_CONNECTED_BIT);
}
}
// Accessor implementations returning internal state strings.
std::string WifiManager::getIp() const { return ip_addr_; }
std::string WifiManager::getMac() const { return mac_; }
std::string WifiManager::getSsid() const { return ssid_; }
int8_t WifiManager::getRssi() const { return rssi_; }
+90
View File
@@ -0,0 +1,90 @@
/**
* @file WifiManager.h
* @brief Provides an interface for managing the ESP32 Wi-Fi connection.
*/
#ifndef WIFI_MANAGER_H
#define WIFI_MANAGER_H
#include "esp_event.h"
#include "freertos/FreeRTOS.h"
#include "freertos/event_groups.h"
#include <string>
/**
* @brief Class responsible for connecting to and monitoring Wi-Fi.
*
* This class abstracts the ESP-IDF Wi-Fi driver and event loop, providing
* synchronous connection methods and accessors for network states like IP
* and MAC addresses.
*/
class WifiManager {
public:
/**
* @brief Constructs a WifiManager instance.
*
* @param ssid The SSID of the target Wi-Fi network.
* @param password The password for the target Wi-Fi network.
*/
WifiManager(const std::string &ssid, const std::string &password);
/**
* @brief Destructor. Cleans up event handlers and FreeRTOS resources.
*/
~WifiManager();
/**
* @brief Starts the Wi-Fi connection process and blocks until connected.
*
* @return true if successfully connected and an IP address was acquired,
* false otherwise.
*/
bool connect();
/** @brief Returns the acquired IPv4 address as a string, or a fallback
* message if not connected. */
std::string getIp() const;
/** @brief Returns the station MAC address as a formatted string (e.g.,
* "AA:BB:CC:DD:EE:FF"). */
std::string getMac() const;
/** @brief Returns the SSID of the currently configured Wi-Fi network. */
std::string getSsid() const;
/** @brief Returns the Received Signal Strength Indicator (RSSI) of the
* connected AP. */
int8_t getRssi() const;
private:
/**
* @brief Static event handler dispatcher registered with the ESP event loop.
*/
static void eventHandler(void *arg, esp_event_base_t event_base,
int32_t event_id, void *event_data);
/**
* @brief Processes Wi-Fi related events (e.g., station start, disconnect).
*/
void handleWifiEvent(int32_t event_id, void *event_data);
/**
* @brief Processes IP related events (e.g., getting an IP address).
*/
void handleIpEvent(int32_t event_id, void *event_data);
std::string ssid_; ///< Stored SSID payload.
std::string password_; ///< Stored password payload.
std::string ip_addr_{"Connecting..."}; ///< Currently assigned IP.
std::string mac_{"N/A"}; ///< Hardware MAC address.
int8_t rssi_{0}; ///< Expected signal strength.
EventGroupHandle_t
wifi_event_group_; ///< FreeRTOS event group to block on connection state.
esp_event_handler_instance_t
instance_any_id_; ///< Handler instance for generic Wi-Fi events.
esp_event_handler_instance_t
instance_got_ip_; ///< Handler instance for successful IP lease event.
};
#endif // WIFI_MANAGER_H
+37
View File
@@ -0,0 +1,37 @@
/**
* @file main.cpp
* @brief Main entry point for the ESP-IDF E-paper Demo.
*
* This file handles the initial NVS flash initialization and bootstraps the
* main application execution encapsulated within the App class.
*/
#include "App.h"
#include "esp_log.h"
#include "nvs_flash.h"
static const char *TAG = "EPD_DEMO_MAIN";
/**
* @brief Application entry point called by the ESP-IDF framework.
*
* It ensures the Non-Volatile Storage (NVS) is initialized before passing
* control to the App instance.
*/
extern "C" void app_main(void) {
ESP_LOGI(TAG, "Starting NVS...");
// Initialize NVS, erasing it first if it contains an unsupported version
// or no free pages.
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES ||
ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
// Instantiate and run the main application logic
App app;
app.init();
app.run();
}
+5
View File
@@ -0,0 +1,5 @@
# 自定义分区表(适配中文字体数据,app 分区扩大到 2MB)
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x6000,
phy_init, data, phy, 0xf000, 0x1000,
factory, app, factory, 0x10000, 0x200000,
1 # 自定义分区表(适配中文字体数据,app 分区扩大到 2MB)
2 # Name, Type, SubType, Offset, Size, Flags
3 nvs, data, nvs, 0x9000, 0x6000,
4 phy_init, data, phy, 0xf000, 0x1000,
5 factory, app, factory, 0x10000, 0x200000,
+3706
View File
File diff suppressed because it is too large. Load diff
+12
View File
@@ -0,0 +1,12 @@
# This file was generated using idf.py save-defconfig. It can be edited manually.
# Espressif IoT Development Framework (ESP-IDF) 5.5.1 Project Minimal Configuration
#
CONFIG_IDF_TARGET="esp32c3"
CONFIG_WIFI_PASSWORD="seahi.me"
CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192
# 自定义分区表(app 分区扩大到 2MB,容纳中文字体数据)
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
# 墨水屏颜色模式(BWR=黑白红三色,BW=纯黑白)
CONFIG_DISPLAY_BWR=y
+149
View File
@@ -0,0 +1,149 @@
#!/usr/bin/env python3
"""
从 Unifont hex 数据中提取中文 16×16 点阵,生成 ESP32 可用的 C++ 数组。
用法:
python3 tools/gen_font.py <unifont.hex.gz>
输出:
main/font16cn_data.cpp
默认收录 GB2312 一级字(3,755 个常用字)+ 自定义扩展字符,
覆盖日常中文 99.7% 以上。如需添加额外字符,在 EXTRA_CHARS 中追加即可。
"""
import gzip
import sys
import os
# ========== GB2312 一级字 Unicode 码点列表 ==========
# GB2312 一级字包含 3,755 个常用字,按拼音排序
# 以下列表通过 GB2312 区位码 16-55 区转换得到
def _gb2312_level1_codepoints():
"""生成 GB2312 一级字(16-55 区)的 Unicode 码点集合"""
import codecs
codepoints = set()
for qu in range(16, 56): # 16~55 区
for wei in range(1, 95): # 1~94 位
b1 = qu + 0xA0
b2 = wei + 0xA0
try:
ch = bytes([b1, b2]).decode('gb2312')
codepoints.add(ord(ch))
except (UnicodeDecodeError, ValueError):
continue
return codepoints
# ========== 额外扩展字符(不在 GB2312 一级字中但项目需要的字符)==========
EXTRA_CHARS = "" # 在这里追加项目需要的特殊字符
def load_unifont_hex(hex_path):
"""加载 Unifont hex 文件,返回 {codepoint: bitmap_bytes} 字典"""
font_map = {}
opener = gzip.open if hex_path.endswith('.gz') else open
with opener(hex_path, 'rt', encoding='ascii') as f:
for line in f:
line = line.strip()
if not line or ':' not in line:
continue
cp_str, hex_data = line.split(':', 1)
cp = int(cp_str, 16)
font_map[cp] = bytes.fromhex(hex_data)
return font_map
def char_to_utf8_bytes(cp):
"""将 Unicode 码点转为 UTF-8 的 3 个字节(仅支持 U+0800~U+FFFF)"""
assert 0x0800 <= cp <= 0xFFFF
b1 = 0xE0 | (cp >> 12)
b2 = 0x80 | ((cp >> 6) & 0x3F)
b3 = 0x80 | (cp & 0x3F)
return (b1, b2, b3)
def generate_cpp(font_map, output_path):
"""生成 C++ 源文件"""
# 合并 GB2312 一级字 + 额外字符
target_cps = _gb2312_level1_codepoints()
for ch in EXTRA_CHARS:
target_cps.add(ord(ch))
entries = []
missing = 0
# 按 Unicode 排序(确保二分查找有效)
for cp in sorted(target_cps):
if cp not in font_map:
missing += 1
continue
bitmap = font_map[cp]
# Unifont 16×16 汉字 = 32 字节
if len(bitmap) != 32:
missing += 1
continue
utf8 = char_to_utf8_bytes(cp)
hex_str = ', '.join(f'0x{b:02X}' for b in bitmap)
entries.append(
f' // U+{cp:04X} "{chr(cp)}"\n'
f' {{ {{ 0x{utf8[0]:02X}, 0x{utf8[1]:02X}, 0x{utf8[2]:02X} }},\n'
f' {{ {hex_str} }} }}'
)
total_bytes = len(entries) * 35 # 每个条目约 35 字节
cpp_content = f'''\
/**
* @file font16cn_data.cpp
* @brief Unifont 16×16 中文点阵数据(自动生成,请勿手动编辑)。
*
* 由 tools/gen_font.py 从 Unifont hex 数据自动提取。
* 收录 {len(entries)} 个常用中文字符(GB2312 一级字)。
* 预计占用 Flash: ~{total_bytes // 1024} KB
*
* 如需添加字符,编辑 gen_font.py 中的 EXTRA_CHARS 并重新运行。
*/
#include "ChineseFont.h"
static const UTF8_Char kFont16CN_Table[] = {{
{(","+chr(10)).join(entries)}
}};
const UTF8_Font Font16CN = {{
kFont16CN_Table,
{len(entries)}, // 字符数
8, // ASCII 字符宽度(半角)
16, // 中文字符宽度
16 // 字符高度
}};
'''
os.makedirs(os.path.dirname(output_path), exist_ok=True)
with open(output_path, 'w', encoding='utf-8') as f:
f.write(cpp_content)
print(f"✅ 已生成 {output_path}")
print(f" 收录: {len(entries)} 个汉字")
print(f" 未找到: {missing} 个")
print(f" 预计 Flash: ~{total_bytes // 1024} KB")
def main():
if len(sys.argv) < 2:
print(f"用法: {sys.argv[0]} <unifont.hex 或 unifont.hex.gz>")
sys.exit(1)
hex_path = sys.argv[1]
script_dir = os.path.dirname(os.path.abspath(__file__))
output_path = os.path.join(script_dir, '..', 'components', 'chinese_font', 'src', 'font16cn_data.cpp')
print(f"📖 加载 Unifont 数据: {hex_path}")
font_map = load_unifont_hex(hex_path)
print(f" 共加载 {len(font_map)} 个码点")
generate_cpp(font_map, output_path)
if __name__ == '__main__':
main()