Arduino 直接驱动ssd1306 OLED

最近入手了一个 0.91寸OLED ,主控是 SSD1306。

为了更方便的在项目中驱动这款 OLED 研究了一下直接使用代码驱动,不使用第三方库来进行显示。

#include <Wire.h>
#include "Font/ssd1306_font5x8.h"

/*===============================C header===============================*/
#define SSD1306_ADDR 0x3C
#define SSD1306_WIDTH 128
#define SSD1306_PAGE_HEIGHT 8
#define SSD1306_MAX_SCALE 8

typedef struct {
  const uint8_t width;
  const uint8_t height;
  const uint8_t space;
  const uint8_t first_char;
  const uint8_t last_char;
  const uint8_t *data;
} font_t;

typedef enum {
  SSD1306_HEIGHT_16 = 16,
  SSD1306_HEIGHT_32 = 32,
  SSD1306_HEIGHT_64 = 64
} ssd1306_height_t;

typedef enum {
  SSD1306_MEMORY_ADDRESSING_MODE_HORIZONTAL = 0x00,
  SSD1306_MEMORY_ADDRESSING_MODE_VERTICAL = 0x01,
  SSD1306_MEMORY_ADDRESSING_MODE_PAGE = 0x02
} ssd1306_mam_t;

typedef enum {
  SSD1306_MULTIPLEX_RATIO_16 = 0x0F,
  SSD1306_MULTIPLEX_RATIO_32 = 0x1F,
  SSD1306_MULTIPLEX_RATIO_64 = 0x3F
} ssd1306_mux_t;

typedef enum {
  SSD1306_COM_PINS_SEQUENTIAL_NO_REMAP = 0x02,
  SSD1306_COM_PINS_ALTERNATIVE_NO_REMAP = 0x12,
  SSD1306_COM_PINS_SEQUENTIAL_REMAP = 0x22,
  SSD1306_COM_PINS_ALTERNATIVE_REMAP = 0x32
} ssd1306_com_pins_t;

typedef enum {
  SSD1306_VCOMH_065 = 0x00,
  SSD1306_VCOMH_077 = 0x20,
  SSD1306_VCOMH_083 = 0x30
} ssd1306_vcomh_t;

static uint8_t height = SSD1306_HEIGHT_64;
static uint8_t pages = SSD1306_HEIGHT_64 / SSD1306_PAGE_HEIGHT;

const font_t Font5x8 = { 5, 8, 1, 32, 126, (const uint8_t *)font5x8 };

static uint8_t cur_scale = 1;
static const font_t *cur_font = &Font5x8;

static uint16_t cmd_buf_index = 0;
static uint8_t cmd_buf[31];
static uint16_t data_buf_index = 0;
static uint16_t update_data_buf_index = 0;
static uint8_t *data_buf;

static uint8_t data_buf_16[SSD1306_WIDTH * SSD1306_HEIGHT_16 / SSD1306_PAGE_HEIGHT];
static uint8_t data_buf_32[SSD1306_WIDTH * SSD1306_HEIGHT_32 / SSD1306_PAGE_HEIGHT];
static uint8_t data_buf_64[SSD1306_WIDTH * SSD1306_HEIGHT_64 / SSD1306_PAGE_HEIGHT];

/*===============================C code===============================*/
void ssd1306_setup(ssd1306_height_t ssd1306_height) {
  height = ssd1306_height;
  pages = height / SSD1306_PAGE_HEIGHT;

  switch (ssd1306_height) {
    case 16:
      data_buf = data_buf_16;
      break;
    case 32:
      data_buf = data_buf_32;
      break;
    default:
      data_buf = data_buf_64;
      break;
  }
}
static void add_command(uint8_t cmd) {
  cmd_buf[cmd_buf_index] = cmd;
  cmd_buf_index++;
}

static void set_contrast_control(uint8_t percent) {
  if (percent > 100) {
    percent = 100;
  }
  add_command(0x81);
  add_command(percent * 255 / 100);
}

static void set_entire_display_on(bool on) {
  add_command(on ? 0xA5 : 0xA4);
}

static void set_normal_inverse_display(bool inverse) {
  add_command(inverse ? 0xA7 : 0xA6);
}

static void set_display_on_off(bool on) {
  add_command(on ? 0xAF : 0xAE);
}

static void set_memory_addressing_mode(ssd1306_mam_t mode) {
  add_command(0x20);
  add_command(mode);
}

static void set_display_start_line(uint8_t line) {
  if (line > height - 1) {
    line = height - 1;
  }
  add_command(0x40 | line);
}

// (RESET = false)
static void set_segment_remap(bool remap) {
  add_command(remap ? 0xA1 : 0xA0);
}

// (RESET = SSD1306_MULTIPLEX_RATIO_64)
static void set_multiplex_ratio(ssd1306_mux_t ratio) {
  add_command(0xA8);
  add_command(ratio);
}

static void set_com_scan_direction(bool remapped) {
  add_command(remapped ? 0xC8 : 0xC0);
}

static void set_display_offset(uint8_t offset) {
  if (offset > height - 1) {
    offset = height - 1;
  }
  add_command(0xD3);
  add_command(offset);
}

static void set_display_clock(uint8_t clock, uint8_t osc_freq) {
  if (clock > 15) {
    clock = 15;
  }
  if (osc_freq > 15) {
    osc_freq = 15;
  }
  add_command(0xD5);
  add_command((osc_freq << 4) | clock);
}

static void set_com_pins(ssd1306_com_pins_t com_pins) {
  add_command(0xDA);
  add_command(com_pins);
}

static void set_precharge_period(uint8_t period) {
  add_command(0xD9);
  add_command(period);
}

static void set_vcomh_deselect_level(ssd1306_vcomh_t level) {
  add_command(0xDB);
  add_command(level);
}

static void set_charge_pump_setting(bool enable) {
  add_command(0x8D);
  add_command(enable ? 0x14 : 0x10);
}

void ssd1306_i2c_write(uint8_t control, const uint8_t *data, uint16_t len) {
  uint16_t i = 0;
  while (i < len) {
    uint8_t chunkSize = min(len - i, 31);

    Wire.beginTransmission(SSD1306_ADDR);
    Wire.write(control);
    Wire.write(&data[i], chunkSize);
    uint8_t result = Wire.endTransmission();

    if (result != 0) {
      Serial.print(F("ssd1306_i2c_write error: "));
      Serial.println(result);
      return;
    }

    i += chunkSize;
  }
}
void ssd1306_init(void) {
  cmd_buf_index = 0;

  set_contrast_control(50);
  set_entire_display_on(false);
  set_normal_inverse_display(false);
  set_display_on_off(false);

  // set_column_start_address(0);
  set_memory_addressing_mode(SSD1306_MEMORY_ADDRESSING_MODE_HORIZONTAL);
  // set_column_address(0, SSD1306_WIDTH - 1);
  // set_page_address(0, pages - 1);
  // set_page_start_address(0);

  set_display_start_line(0);
  set_segment_remap(false);
  ssd1306_mux_t mux;
  ssd1306_com_pins_t com_pins;
  if (height == 16) {
    mux = SSD1306_MULTIPLEX_RATIO_16;
    com_pins = SSD1306_COM_PINS_SEQUENTIAL_NO_REMAP;
  } else if (height == 32) {
    mux = SSD1306_MULTIPLEX_RATIO_32;
    com_pins = SSD1306_COM_PINS_SEQUENTIAL_NO_REMAP;
  } else {
    mux = SSD1306_MULTIPLEX_RATIO_64;
    com_pins = SSD1306_COM_PINS_ALTERNATIVE_NO_REMAP;
  }
  set_multiplex_ratio(mux);
  set_com_scan_direction(false);
  set_display_offset(0);
  set_com_pins(com_pins);

  set_display_clock(0, 8);
  set_precharge_period(0x22);
  set_vcomh_deselect_level(SSD1306_VCOMH_077);

  set_charge_pump_setting(true);

  set_display_on_off(true);

  ssd1306_i2c_write(0x00, cmd_buf, cmd_buf_index + 1);
}

void ssd1306_clear(void) {
  memset(&data_buf[0], 0, SSD1306_WIDTH * pages);

  update_data_buf_index = SSD1306_WIDTH * pages - 1;
}

void ssd1306_set_caret(uint8_t col, uint8_t row) {
  if (col > SSD1306_WIDTH - 1) {
    col = SSD1306_WIDTH - 1;
  }
  if (row > height - 1) {
    row = height - 1;
  }

  data_buf_index = row * SSD1306_WIDTH + col;
}

static void scale_column(uint8_t col, uint8_t *out) {
  memset(out, 0, cur_scale);

  for (uint8_t i = 0; i < 8; i++) {
    uint8_t bit = (col >> i) & 0x01;
    if (!bit) {
      continue;
    }

    for (uint8_t j = 0; j < cur_scale; j++) {
      uint8_t dst_row = i * cur_scale + j;
      uint8_t byte_index = dst_row / 8;
      uint8_t bit_index = dst_row % 8;

      out[byte_index] |= (0x01 << bit_index);
    }
  }
}

void set_column_data(uint8_t col, uint8_t page, uint8_t page_offset, uint8_t col_data) {
  uint8_t scaled_bytes[cur_scale];
  if (cur_scale == 1) {
    scaled_bytes[0] = col_data;
  } else {
    scale_column(col_data, scaled_bytes);
  }
  for (uint8_t scale_h = 0; scale_h < cur_scale; scale_h++) {
    for (uint8_t scale_w = 0; scale_w < cur_scale; scale_w++) {
      uint8_t upper = scaled_bytes[scale_w] << page_offset;
      uint8_t lower = scaled_bytes[scale_w] >> (SSD1306_PAGE_HEIGHT - page_offset);

      uint16_t buf_index = (page + scale_w) * SSD1306_WIDTH + (col + scale_h);
      if (buf_index > SSD1306_WIDTH * pages - 1) {
        continue;
      }
      data_buf[buf_index] |= upper;
      if (lower) {
        buf_index = buf_index + SSD1306_WIDTH;
        data_buf[buf_index] |= lower;
      }

      if (buf_index > update_data_buf_index) {
        update_data_buf_index = buf_index;
      }
    }
  }
}

void ssd1306_print_char(char c) {
  if ((uint8_t)c < cur_font->first_char || (uint8_t)c > cur_font->last_char) {
    return;
  }

  uint8_t row = data_buf_index / SSD1306_WIDTH;
  uint8_t col = data_buf_index % SSD1306_WIDTH;
  if (col + cur_font->width * cur_scale > SSD1306_WIDTH) {
    col = 0;
    row += SSD1306_PAGE_HEIGHT * cur_scale;
    ssd1306_set_caret(col, row);
  }
  if (cur_font->height * cur_scale + row > height) {
    return;
  }

  uint16_t char_index = (uint8_t)c - cur_font->first_char;
  uint16_t char_offset = char_index * cur_font->width;
  const uint8_t *bitmap = &cur_font->data[char_offset];
  uint8_t page = row / SSD1306_PAGE_HEIGHT;
  uint8_t page_offset = row % SSD1306_PAGE_HEIGHT;

  for (uint8_t font_col = 0; font_col < cur_font->width; font_col++) {
    uint8_t col_data = pgm_read_byte(bitmap + font_col);
    set_column_data(col + font_col * cur_scale, page, page_offset, col_data);
  }

  col = col + cur_font->width * cur_scale + cur_font->space;

  ssd1306_set_caret(col, row);
}

void ssd1306_print(const char* str) {
  while (*str) {
    ssd1306_print_char(*str++);
  }
}

void ssd1306_set_scale(uint8_t scale) {
  if (scale == 0) {
    scale = 1;
  }
  if (scale > 8) {
    scale = 8;
  }
  cur_scale = scale;
}

static void set_column_address(uint8_t start, uint8_t end) {
  if (end > SSD1306_WIDTH - 1) {
    end = SSD1306_WIDTH - 1;
  }
  if (start > end) {
    start = end;
  }
  add_command(0x21);
  add_command(start);
  add_command(end);
}

static void set_page_address(uint8_t start, uint8_t end) {
  if (end > pages - 1) {
    end = pages - 1;
  }
  if (start > end) {
    start = end;
  }
  add_command(0x22);
  add_command(start);
  add_command(end);
}

void ssd1306_update(void) {
  cmd_buf_index = 0;
  set_column_address(0, SSD1306_WIDTH - 1);
  set_page_address(0, pages - 1);
  ssd1306_i2c_write(0x00, cmd_buf, cmd_buf_index + 1);

  ssd1306_i2c_write(0x40, data_buf, update_data_buf_index + 1);
  update_data_buf_index = 0;
}


void setup() {
  // put your setup code here, to run once:
  Serial.begin(115200);
  Wire.begin();
  ssd1306_setup(SSD1306_HEIGHT_64);
  ssd1306_init();
}

void loop() {
  ssd1306_clear();
  ssd1306_print_char('!');
  ssd1306_print("\"#$%&'()*+,-./");
  ssd1306_set_caret(10, 10);
  ssd1306_set_scale(2);
  ssd1306_print("0123456789");
  ssd1306_set_caret(40, 28);
  ssd1306_set_scale(1);
  ssd1306_print(":;<=>?@");
  ssd1306_set_caret(0, 40);
  ssd1306_print("ABCDEFGHIJKLMNOPQRSTUVWXYZ");
  ssd1306_print("[\\]^_`");
  ssd1306_print("abcdefghijklmnopqrstuvwxyz");
  ssd1306_print("{|}~");
  ssd1306_update();
}

运行效果如下:

完整的代码下载:

BIOS 工程师的工作

一般人很难理解BIOS工程师的工作,甚至我工作二十多年,母亲还会疑惑:电脑这东西生产出来就是好的,为什么需要调试?

去年碰到了一个诡异的问题,以此为例可以看看BIOS工程师的工作。

去年的冬天比往年要冷一些。这个季节的业务量相比其他季节也会少很多,因为圣诞节是传统的销售旺季,在此之前货物必须达到量产阶段,生产出来的大量产品已经达到商铺等待终端客户的选择。

这时收到一封客户的邮件,他们碰到了问题需要支持:最近生产的一批板子,在启动过程中 SoC 会卡在内存初始化中(MRC)。因此,怀疑我们给他们提供的样品有批次问题。基本情况是:之前客户打过同样的板子,前一个批次完全没有问题,但是这一次发现大量的不良品,屏幕亮了然后就卡住了。从串口看到的信息是内存初始化失败。客户使用了三种内存:Samsung 的差不多1/4有问题;LongSys 的1/10 有问题。这个问题影响非常严重,需要我们马上解决。

厂商生产的是类似下面的主板,最终会装进笔记本电脑外壳中,图片中的红色方框就是 SoC (CPU + IO 芯片 + 显卡 等等集成在一起,通常叫  SoC。随着技术的进步,越来越多的功能集成在同一颗芯片中)

电脑主板,来源参考【1】

出于体积和性能的考量,内存颗粒会直接焊接在主板上,上面的图片中就是焊接在主板上的内存颗粒来源【参考2】

对于内存问题,串口Log 是关键,拿到Log 后我请 Memory 专家检查,发现错误集中固定在第一个内存控制器的第一个通道的Byte1上。对于同一块主板发生的错误是固定的,但是不同主板会发生在 RefPi / LockUI / DQS Training过程中。根据现象怀疑是下面问题导致的:

1. BIOS / MRC问题

2. Memory兼容性(D-die EOL)

3. Board设计(PI / layout)

接下来一个个确认。首先怀疑 BIOS/MRC 问题,升级 MRC 版本 (这里强烈推荐使用 Intel  FSP 构建 BIOS,可以方便的验证问题),经过一些列实验,想象没有变化。虽然在这个过程中发现了客户设计上的一些瑕疵,比如,内存单路供电和双路供电设置的问题,但是这些对问题都没有影响。

继续检查发现客户选择的 Memory 颗粒是厂家年初计划准备 EOL 的,理论上应该不再使用,但是估计是产能/成本问题,现在仍在使用。客户快递给我们内存颗粒,在Intel RVP (参考板)上验证,工作正常。通常情况下内存出现问题,通过降频可以解决大部分。但是这招在这里不好用。

接下来,硬件工程师上场,仔细 review 电路,PCB 设计,然后进行仿真,结论是信号质量在正常范围内,排除板子设计问题。

又请客户更换板子上的内存颗粒,问题依旧。

最终客户将 SOC 焊接下来,放在我们的 RVP 上实验,结果 RVP 会出现同样的现象。继续实验,客户的将有问题的旧主板更换一个新的 SoC ,问题会消失。这就是俗话说的问题跟着 SoC 走了。客户更加怀疑我们给他们一批有问题的SoC。

图片来源【参考3】 RVP  是  Intel 制作的参考开发板,上面的SoC 无需焊接即可更换的,同时有些RVP 内存也是可以更换的。这种开发板因为出货量少,所以价格奇高,通常一片需要3000美元。

接着又拆下新的 SOC 更换为旧的 SoC,同样的问题再次出现。最终结论: SoC 确实坏了。

每年在研发阶段, Intel 都会提供价值2到3亿美元,大量的样品给厂商进行测试。这正是因为这样的大量的“撒币”,使得 Intel 处理器整体稳定性高于竞争对手。并且作为终端用户或者开发者,不太容易碰到独一无二奇怪的问题。这些样品无论是服务器,还是台式机笔记本,我从来没听说过有批量损坏的问题。但是目前确认 SoC 坏了,不是一块,而是至少有十片以上。

最终只能联系 CPU的生产工厂,对于他们来说,批量性的样品损坏也是前所未闻的事情。在怀疑声中,有问题的 SOC 快递给了 CPU 工厂,请他们帮助分析。经过一周的等待,最终的结论是:DDR PHY区域存在硅裂纹(silicon crack)。

前面的解决问题的过程中,客户快递给我一台有问题的整机,根据CPU工厂的提示,我用显微镜查看:在板子 SoC 的角落同样有一个破碎的痕迹(例如,下图中指示位置)。这个痕迹估计只有苍蝇屎大小。如果真是出厂时 SoC 就已经损坏,只能是有人用小锤子在显微镜的帮助下,在每一个 SoC 上角认真仔细的锤过。

图片并非有问题的SoC, 只是用于说明问题来源 【参考4】,中间凸起部分会和散热器紧密接触。

我们把这边拍到的照片发给客户,客户用手上的显微镜去检查,果然,在其他有问题板子的 SoC上也发现了同样的损坏。经过一天的沉默,最终客户确认手上的散热模组有问题:两家供应商提供的散热模组,一家提供的存在尺寸问题,当按照特定顺序安装的时候会压坏 SoC,这就是硅裂纹的来源。

真相让一切奇怪的现象都能得到完美解释:之前生产的那一批没问题是因为用的没有问题的那一家散热模组;后面换成有问题的散热模组,操作工在安装的时候顺序手法不同,有些人操作之后会损坏,有些人安装之后不会。随机损坏的结果让人怀疑是SOC 本体的问题。

前后花了将近2个月,终于找到了问题答案。回到最初的问题作为BIOS工程师,整个过程中最重要的作用可能是:拿着板子去看了一下显微镜吧?

参考:

  1. https://tao.hooos.com/goods_WJj82G6Fot2kGKbedyiarKHQta-yz622os6Kp36WmOSQ.html
  2. http://www.douyin.com/note/7452258197479378213
  3. http://www.acfun.cn/a/ac4786301
  4. https://www.techpowerup.com/316728/intel-accelerates-ai-everywhere-with-launch-of-powerful-next-gen-products

做一个 UEFI 下的 EasyX 图形库(1)

之前我使用过一个 Windows 下的图形库:EasyX(官方网站https://easyx.cn/),它可以帮助用户非常方便的在Windows下实现简单的绘图功能,

美中不足的是这个库并非开源项目,因此无法直接进行迁移。

这次我尝试编写一个 UEFI 下的 EasyX 库,尽量做到完全兼容。代码主要是 AI 生成。

本来打算做成一个库,但是后来考虑到通俗易懂直接方便,索性直接和 Application 写在了一起。这样做更加直截了当,并且方便用户自行剪裁。

测试编写的UEFI Application假设所有内容处于\AppPkg\Applications\EasyXDemo1 目录下,然后在AppPkg\AppPkg.dsc中加入下面这一行:

              AppPkg/Applications/EasyXDemo1/EasyXDemo.inf

代码如下:

/** @file
  EasyX UEFI 图形库演示程序(阶段 P0 + P1)。

  演示:初始化设备、清屏、绘制直线、矩形、圆、椭圆及其填充变体,
  然后等待任意按键后退出。
**/

#include <Uefi.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/UefiLib.h>
#include "easyx.h"

/**
  等待用户按下任意键。
**/
STATIC
VOID
WaitAnyKey (
  VOID
  )
{
  EFI_INPUT_KEY  Key;
  UINTN          Index;

  gBS->WaitForEvent (1, &gST->ConIn->WaitForKey, &Index);
  gST->ConIn->ReadKeyStroke (gST->ConIn, &Key);
}

/**
  程序入口。
**/
EFI_STATUS
EFIAPI
UefiMain (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
{
  EFI_STATUS  Status;
  INT32       W;
  INT32       H;

  //
  // 初始化绘图设备(跟随 GOP 最大分辨率模式)。
  //
  Status = initgraph (0, 0);
  if (EFI_ERROR (Status)) {
    Print (L"initgraph 失败: %r\n", Status);
    return Status;
  }

  W = getwidth ();
  H = getheight ();

  //
  // 批量绘图,减少刷新次数。
  //
  BeginBatchDraw ();

  //
  // 背景填充深蓝。
  //
  setbkcolor (RGB (16, 16, 48));
  cleardevice ();

  //
  // 直线:不同颜色/线宽/线型。
  //
  setlinecolor (WHITE);
  setlinestyle (PS_SOLID, 1);
  line (20, 20, W - 20, 20);

  setlinecolor (LIGHTGREEN);
  setlinestyle (PS_SOLID, 3);
  line (20, 40, W - 20, 60);

  setlinecolor (YELLOW);
  setlinestyle (PS_DASH, 1);
  line (20, 80, W - 20, 80);

  setlinecolor (LIGHTCYAN);
  setlinestyle (PS_DOT, 1);
  line (20, 100, W - 20, 100);

  //
  // 矩形。
  //
  setlinestyle (PS_SOLID, 1);
  setlinecolor (LIGHTRED);
  rectangle (40, 140, 200, 260);

  setfillcolor (RGB (40, 120, 200));
  setlinecolor (WHITE);
  fillrectangle (240, 140, 400, 260);

  setfillcolor (LIGHTMAGENTA);
  solidrectangle (440, 140, 600, 260);

  //
  // 圆。
  //
  setlinecolor (WHITE);
  circle (110, 360, 60);

  setfillcolor (RGB (220, 160, 40));
  setlinecolor (WHITE);
  fillcircle (320, 360, 60);

  setfillcolor (LIGHTGREEN);
  solidcircle (520, 360, 60);

  //
  // 椭圆。
  //
  setlinecolor (LIGHTCYAN);
  ellipse (40, 440, 260, 540);

  setfillcolor (RGB (200, 60, 120));
  setlinecolor (WHITE);
  fillellipse (300, 440, 560, 540);

  //
  // 一次性刷新并结束批量绘图。
  //
  EndBatchDraw ();

  //
  // 等待按键后清理退出。
  //
  WaitAnyKey ();

  closegraph ();

  //
  // 恢复文本控制台。
  //
  gST->ConOut->ClearScreen (gST->ConOut);
  Print (L"EasyX UEFI Demo 结束。分辨率: %d x %d\n", W, H);

  return EFI_SUCCESS;
}

在EDK2 自带的模拟器中测试结果如下,可以说完全达到了预期:

完整代码和编译后的 EFI 可以在这里下载:

Ch32V307 I2C 扫描程序

Ch32V307 使用 I2C2 进行扫描的代码,使用 I2C2 ,对应引脚 PB10 为 SCL , PB11 为 SDA。串口输出波特率 6000 000bps。

#include “ch32v30x.h”
#include “debug.h”

/* I2C 引脚定义 (可根据实际硬件修改) */
#define I2C_SCL_PIN GPIO_Pin_10
#define I2C_SDA_PIN GPIO_Pin_11
#define I2C_PORT GPIOB
#define I2C_CLK RCC_APB2Periph_GPIOB

/* I2C 实例定义 */
#define I2C_INSTANCE I2C2
#define I2C_CLK_SRC RCC_APB1Periph_I2C2

/* I2C GPIO 初始化 */
void I2C_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;

RCC_APB2PeriphClockCmd(I2C_CLK, ENABLE);

GPIO_InitStructure.GPIO_Pin = I2C_SCL_PIN | I2C_SDA_PIN;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; // 开漏复用输出
GPIO_Init(I2C_PORT, &GPIO_InitStructure);
}

/* I2C 模块初始化 */
void I2C_Master_Init(void)
{
I2C_InitTypeDef I2C_InitStructure;

RCC_APB1PeriphClockCmd(I2C_CLK_SRC, ENABLE);

I2C_DeInit(I2C_INSTANCE);

I2C_InitStructure.I2C_ClockSpeed = 100000; // 100kHz 标准模式
I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
I2C_InitStructure.I2C_OwnAddress1 = 0x00; // 主机模式不需要自身地址
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;

I2C_Init(I2C_INSTANCE, &I2C_InitStructure);
I2C_Cmd(I2C_INSTANCE, ENABLE);
}

/* 检测单个 I2C 地址是否存在设备 */
// 返回 0: 设备存在 (ACK), 1: 无设备 (NACK) 或错误
uint8_t I2C_Check_Device(uint8_t address)
{
I2C_GenerateSTART(I2C_INSTANCE, ENABLE);

// 等待起始条件发送完成
while(!I2C_CheckEvent(I2C_INSTANCE, I2C_EVENT_MASTER_MODE_SELECT));

// 发送地址 (写方向, 最后一位为0)
I2C_Send7bitAddress(I2C_INSTANCE, address << 1, I2C_Direction_Transmitter);

// 等待地址发送完成或接收到 NACK
// 注意:CH32V307 库函数可能需要根据具体状态标志判断
// 这里使用简单的超时机制和状态检查

uint32_t timeout = 10000;
while(timeout–)
{
if(I2C_GetFlagStatus(I2C_INSTANCE, I2C_FLAG_ADDR))
{
// 地址发送成功,收到 ACK
I2C_ClearFlag(I2C_INSTANCE, I2C_FLAG_ADDR);
I2C_GenerateSTOP(I2C_INSTANCE, ENABLE);
return 0; // 设备存在
}

if(I2C_GetFlagStatus(I2C_INSTANCE, I2C_FLAG_AF))
{
// 收到 NACK
I2C_ClearFlag(I2C_INSTANCE, I2C_FLAG_AF);
I2C_GenerateSTOP(I2C_INSTANCE, ENABLE);
return 1; // 无设备
}
}

// 超时错误
I2C_GenerateSTOP(I2C_INSTANCE, ENABLE);
return 1;
}

/* 扫描总线上的所有设备 */
void I2C_Scan_Bus(void)
{
uint8_t address;
uint8_t found_count = 0;

printf(“\r\n========================================\r\n”);
printf(” CH32V307 I2C Scanner Start\r\n”);
printf(“========================================\r\n”);
printf(“Scanning I2C bus…\r\n”);

for(address = 1; address < 127; address++)
{
// 跳过保留地址 (通常 0x00 是通用呼叫,0x78-0x7F 是保留)
// 但为了全面扫描,我们扫描 1-127,具体保留地址视情况而定

if(I2C_Check_Device(address) == 0)
{
printf(“Found device at address: 0x%02X (7-bit)\r\n”, address);
printf(” : 0x%02X (8-bit Write)\r\n”, address << 1);
printf(” : 0x%02X (8-bit Read)\r\n”, (address << 1) | 1);
found_count++;
}
// 短暂延时,避免总线过于繁忙
Delay_Ms(5);
}

if(found_count == 0)
{
printf(“No I2C devices found.\r\n”);
}
else
{
printf(“Total devices found: %d\r\n”, found_count);
}
printf(“========================================\r\n\r\n”);
}

/*********************************************************************
* @fn main
*
* @brief Main program.
*
* @return none
*/
int main(void) {
SystemCoreClockUpdate();
Delay_Init();
USART_Printf_Init(6000000);
printf(“SystemClk:%d\r\n”, SystemCoreClock);
printf(“ChipID:%08x\r\n”, DBGMCU_GetCHIPID());

I2C_GPIO_Init();
I2C_Master_Init();

while(1)
{
I2C_Scan_Bus();
Delay_Ms(5000); // 每5秒扫描一次
}

}
运行结果:

UEFI EDK2 INF 文件编码问题

最近偶然发现,如果EDK2 INF使用 Unicode 编码,可能会导致“: error 0004: File read failure”错误。

例如:

Build environment: Windows-10-10.0.26100-SP0
Build start time: 10:58:15, Jul.16 2026

WORKSPACE = c:\buildbs\edk2202302\edk2
EDK_TOOLS_PATH = c:\buildbs\edk2202302\edk2\basetools
EDK_TOOLS_BIN = c:\buildbs\edk2202302\edk2\basetools\bin\win32
CONF_PATH = c:\buildbs\edk2202302\edk2\conf
PYTHON_COMMAND = py -3

Processing meta-data .Architecture(s) = X64
Build target = DEBUG
Toolchain = VS2019

Active Platform = c:\buildbs\edk2202302\edk2\AppPkg\AppPkg.dsc

build.py…
: error 0004: File read failure

解决方法上来说,可以使用 Notepad++ 或者类似工具,更改一下编码,然后保存即可:

USB 3.0 U盘速度慢的一种可能

如果你的3.0U盘支持的是 BOT mode,那么很可能传输速度较慢。

判断的方法是查看描述符中的 Interface Protocol,如果是50h,那么就是。比如,下面来自我手上的一个U盘:

[Port13]  :  USB Mass Storage Device


Is Port User Connectable:         yes
Is Port Debug Capable:            yes
Companion Port Number:            3
Companion Hub Symbolic Link Name: USB#ROOT_HUB30#4&amp;16cc2fc5&amp;0&amp;0#{f18a0e88-c30c-11d0-8815-00a0c906bed8}
Protocols Supported:
 USB 1.1:                         no
 USB 2.0:                         no
 USB 3.0:                         yes

Device Power State:               PowerDeviceD0

       ---===>Device Information&lt;===---
English product name: "Disk 20"

ConnectionStatus:                  
Current Config Value:              0x01  -> Device Bus Speed: SuperSpeed
Device Address:                    0x17
Open Pipes:                           2

          ===>Device Descriptor&lt;===
bLength:                           0x12
bDescriptorType:                   0x01
bcdUSB:                          0x0320
bDeviceClass:                      0x00  -> This is an Interface Class Defined Device
bDeviceSubClass:                   0x00
bDeviceProtocol:                   0x00
bMaxPacketSize0:                   0x09 = (9) Bytes
idVendor:                        0x346D = Vendor ID not listed with USB.org
idProduct:                       0x5678
bcdDevice:                       0x0200
iManufacturer:                     0x01
     English (United States)  "USB"
iProduct:                          0x02
     English (United States)  "Disk 20"
iSerialNumber:                     0x03
     English (United States)  "FC097CCF38A2A"
bNumConfigurations:                0x01

          ---===>Open Pipes&lt;===---

          ===>Endpoint Descriptor&lt;===
bLength:                           0x07
bDescriptorType:                   0x05
bEndpointAddress:                  0x01  -> Direction: OUT - EndpointID: 1
bmAttributes:                      0x02  -> Bulk Transfer Type
wMaxPacketSize:                  0x0400
bInterval:                         0x00

          ===>Endpoint Descriptor&lt;===
bLength:                           0x07
bDescriptorType:                   0x05
bEndpointAddress:                  0x81  -> Direction: IN - EndpointID: 1
bmAttributes:                      0x02  -> Bulk Transfer Type
wMaxPacketSize:                  0x0400
bInterval:                         0x00

       ---===>Full Configuration Descriptor&lt;===---

          ===>Configuration Descriptor&lt;===
bLength:                           0x09
bDescriptorType:                   0x02
wTotalLength:                    0x002C  -> Validated
bNumInterfaces:                    0x01
bConfigurationValue:               0x01
iConfiguration:                    0x00
bmAttributes:                      0x80  -> Bus Powered
MaxPower:                          0x12 = 144 mA

          ===>Interface Descriptor&lt;===
bLength:                           0x09
bDescriptorType:                   0x04
bInterfaceNumber:                  0x00
bAlternateSetting:                 0x00
bNumEndpoints:                     0x02
bInterfaceClass:                   0x08  -> This is a Mass Storage USB Device Interface Class
bInterfaceSubClass:                0x06
bInterfaceProtocol:                0x50
iInterface:                        0x00

如果使用抓包工具,会看到下面这种模式的传输

31B OUT
大块 IN
13B IN

与之相反,还有一种UASP,对应的Interface Protocol 是 62h,速度更快。

吹蟑螂游戏机

这是一个使用表压传感器NSPGD1M006DT04设计一个双人对战的“吹蟑螂游戏机”。

在神秘的南亚次大陆,存在着一个神奇的国度,那里的人们经过几千年的洗礼,能够做到和各种小动物和平共存。在当地有着一个古老的游戏:吹蟑螂比赛。双方选手将一直蟑螂放置在玻璃管中,然后同时吹气,能够将蟑螂吹到另外一方的选手就获得了胜利。

很多人第一次听到这种游戏的时候,都会由衷感叹:印吹死挺!

项目描述

项目使用 WS2812 LED 作为指示灯,它是一款集控制电路与发光电路于一体的智能外控LED光源,其外型与5050LED灯珠相同,每个元件即为一个像素点‌。它采用单线归零码通信方式,通过一根信号线即可实现数据的接收与解码,支持级联控制,理论上可无限扩展‌。

游戏方法非常简单:

1.      双方比赛开始后,对着自己面前带有传感器的盒子吹气

2.      中间有24个LED,其中2个LED会亮起红色,当一方吹气后,LED会向着对方方向移动

3.      红色LED触及一方最后一个 LED后游戏结束

4.      按下按钮会重新开始新的对战

芯片选型/简短的硬件介绍

项目实际上有三块 PCB

1.      NSPGD1M006DT04 的转接板,方便我们实现这个传感器和 Arduino 的连接。NSPGD1M006DT04是表压传感器。表压传感器(Gauge Pressure Sensor)是一种测量相对压力的传感器,即相对于大气压力的压力值。相对的还有绝压传感器(Absolute Pressure Sensor)是一种测量相对于真空或绝对零点的压力的传感器。

2.      带有 Arduino 的控制板,位于游戏机的最左侧和最右侧,理论上需要设计一个左侧和右侧板,但是这里为了成本考量,将二者放在同一块板子上,根据需要焊接左侧或者右侧;

3.      WS2812 灯板,这个位于中间

出于成本考虑,板子长度都是小于10cm的,中间使用 U形状的 2.54mm排针进行连接

方案框图 + 设计思路

原理图、PCB设计

1.NSPGD1M006DT04 的转接板电路板和PCB

2.控制板电路和PCB设计,控制板上也带有 WS2812 LED,这样可以最大化利用空间

具体使用的时候,需要额外导线将右侧Arduino 的 TX 连接左侧的 RX

3.WS2812 电路板和 PCB,输入VCC ,控制信号(DIN)和 GND 即可,多余的引脚主要是为了保证连接的稳固

软件流程图 + 调试软件说明 + 关键代码说明

代码分为2部分:左侧和右侧 Arduino 代码不同(具体左右区分方法在下图中)。右侧的只负责读取传感器数据,然后通过串口传输给左侧。然后左侧同样获得传感器数据,比较之后对 WS2812 发送控制信号,操作LED

右侧代码:

#include <Wire.h>

#include "NSPGD1M006DT04.h"

 

NSPGD1M006DT04 sensor(0xFF);

 

void setup() {

    Serial.begin(115200);

    Serial1.begin(2400);

    if (!sensor.begin()) {

       Serial.println("Device not found");

       while(1);

    }

}

 

void loop() {

    char s[20];

    float pr=sensor.readPressure();

    //float pr=3.14159269;

    uint8_t *p;

    dtostrf(pr,1,6,s);

    Serial.println(s);

    p=(uint8_t*)&pr;

    Serial1.write(0x55);

    Serial1.write(0xAA);

    Serial1.write(*p);

    Serial1.write(*(p+1));

    Serial1.write(*(p+2));

    Serial1.write(*(p+3));

   

    delay(50);

}

左侧代码:

#include <FastLED.h>

#include "NSPGD1M006DT04.h"

 

// 灯带

#define LED_PIN     5

#define COLOR_ORDER GRB

#define CHIPSET     WS2812B

#define NUM_LEDS    24

 

#define BLOWTIME    300

 

#define RSTARTPIN   10

 

NSPGD1M006DT04 sensor(0xFF);

 

CRGB leds[NUM_LEDS];

 

void(* resetFunc) (void) = 0;

 

void setup() {

  //Serial.begin(115200);

  Serial1.begin(2400);

  if (!sensor.begin()) {

    Serial.println("Device not found");

    while (1);

  }

 

  pinMode(RSTARTPIN, INPUT_PULLUP);

 

  FastLED.addLeds<CHIPSET, LED_PIN, COLOR_ORDER>(leds, NUM_LEDS).setCorrection( TypicalLEDStrip );

  FastLED.setBrightness( 1 );

 

  for (uint8_t i = 0; i < NUM_LEDS / 2; i++) {

    memset(leds, 0, sizeof(leds));

    leds[NUM_LEDS / 2 - i] = CRGB::Red;

    leds[NUM_LEDS / 2 + i] = CRGB::Red;

    FastLED.show(); // display this frame

    FastLED.delay(300);

  }

 

  memset(leds, 0, sizeof(leds));

  leds[NUM_LEDS / 2 ] = CRGB::Red;

  leds[NUM_LEDS / 2 + 1] = CRGB::Red;

  FastLED.show(); // display this frame

  FastLED.delay(300);

}

 

long int Elsp = 0;

uint8_t  CockroachPosition = NUM_LEDS / 2;

float LeftSum = 0, RightSum = 0;

uint8_t Index = 0;

void loop() {

  float prRight;

  uint8_t *p;

 

 

 

  while (Serial1.available()) {

    uint8_t c = Serial1.read();

 

    if ((Index == 0) && (c == 0x55)) {

      Index++;

      p = (uint8_t*)&prRight;

    } else if ((Index == 1) && (c == 0xAA)) {

      Index++;

    } else if (Index != 0) {

      *p = c;

      p++;

      Index++;

      if (Index == 6) {

        RightSum = RightSum + prRight;

        float Left = sensor.readPressure();

        Index = 0;

        if ((prRight < 0) || (Left < 0)) {

          break;

        }

        LeftSum = LeftSum + Left;

        // Serial.print("Rcv"); Serial.print(" "); Serial.print(Left); Serial.print(" "); Serial.println(prRight);

      }

    }

  }

 

  if ((millis() - Elsp > BLOWTIME) && (Index == 0)) {

    if (LeftSum > RightSum + 0.1) {

      CockroachPosition++;

    }

    if (LeftSum + 0.1 < RightSum) {

      CockroachPosition--;

    }

    memset(leds, 0, sizeof(leds));

    leds[CockroachPosition] = CRGB::Red;

    leds[CockroachPosition + 1] = CRGB::Red;

 

    FastLED.show(); // display this frame

 

 

    // Serial.print("Set LED"); Serial.print(CockroachPosition); Serial.print(" ");

    // Serial.print(LeftSum); Serial.print("   "); Serial.print(RightSum); Serial.print("   "); Serial.println(Index);

 

    LeftSum = 0; RightSum = 0;

    if ((CockroachPosition == 0) || (CockroachPosition == NUM_LEDS - 2)) {

      Serial.println("Win");

      while (1) {

        if (digitalRead(RSTARTPIN) == LOW) {

          resetFunc();

        }

      }

    }

    Elsp = millis();

  }

}

实物演示以及说明

前面提到了,这次设计了三块PCB,实物如下

1.NSPGD1M006DT04 的转接板实物

2.控制板电路板实物

3.WS2812 电路板和

上述PCB 焊接之后通过U型排针连接在一起,按照下图放置

游戏的时候双方对着传感器吹气即可:

遇到的难点及解决方法

1.      NSPGD1M006DT04传感器无法设定设备地址,因此 I2C 上只能放置一个。如果有需要,那么可以通过 GPIO 来进行模拟,或者通过添加元件来解决。最终选择的方案是使用2个Arduino 这样可以化简电路,方便代码编写;

2.      两个 Arduino 使用串口通讯,Arduino Leonardo 中 float 占用4字节。为了简单起见,采用直接将float 拆开然后加上识别头的方法来进行传输

工作的视频

本文提到的 PCB 和代码下载:

====================================================

本文是2025年参加电子森林活动的项目,获得优秀奖

https://www.eetree.cn/project/4720

在Buffer 中显示 Ascii

编写了一个代码,在一个 160*120 Buffer 中显示 Ascii。

这个代码可以看作实现了一个简单的字库。

VS2019 代码如下:

#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <windows.h>

// 显示缓冲区尺寸
#define BUFFER_WIDTH  160
#define BUFFER_HEIGHT 120

// 字符尺寸 (8x8像素)
#define CHAR_WIDTH  8
#define CHAR_HEIGHT 8

// 计算可显示的字符数量
#define CHARS_PER_ROW (BUFFER_WIDTH / CHAR_WIDTH)   // 20个字符
#define CHARS_PER_COL (BUFFER_HEIGHT / CHAR_HEIGHT) // 15行

// 显示缓冲区
uint8_t display_buffer[BUFFER_HEIGHT][BUFFER_WIDTH];

// 修正后的ASCII字符字体数据 (8x8像素,可见字符32-126)
const uint8_t ascii_font[95][8] = {
    // 空格 (32)
    {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
    // ! (33)
    {0x18, 0x18, 0x18, 0x18, 0x18, 0x00, 0x18, 0x00},
    // " (34)
    {0x6C, 0x6C, 0x6C, 0x00, 0x00, 0x00, 0x00, 0x00},
    // # (35)
    {0x6C, 0x6C, 0xFE, 0x6C, 0xFE, 0x6C, 0x6C, 0x00},
    // $ (36)
    {0x30, 0x7C, 0xC0, 0x78, 0x0C, 0xF8, 0x30, 0x00},
    // % (37)
    {0x00, 0xC6, 0xCC, 0x18, 0x30, 0x66, 0xC6, 0x00},
    // & (38)
    {0x38, 0x6C, 0x38, 0x76, 0xDC, 0xCC, 0x76, 0x00},
    // ' (39)
    {0x60, 0x60, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00},
    // ( (40)
    {0x18, 0x30, 0x60, 0x60, 0x60, 0x30, 0x18, 0x00},
    // ) (41)
    {0x60, 0x30, 0x18, 0x18, 0x18, 0x30, 0x60, 0x00},
    // * (42)
    {0x00, 0x66, 0x3C, 0xFF, 0x3C, 0x66, 0x00, 0x00},
    // + (43)
    {0x00, 0x30, 0x30, 0xFC, 0x30, 0x30, 0x00, 0x00},
    // , (44)
    {0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x60, 0x00},
    // - (45)
    {0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x00},
    // . (46)
    {0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x30, 0x00},
    // / (47)
    {0x06, 0x0C, 0x18, 0x30, 0x60, 0xC0, 0x80, 0x00},
    // 0 (48)
    {0x7C, 0xC6, 0xCE, 0xDE, 0xF6, 0xE6, 0x7C, 0x00},
    // 1 (49)
    {0x30, 0x70, 0x30, 0x30, 0x30, 0x30, 0xFC, 0x00},
    // 2 (50)
    {0x78, 0xCC, 0x0C, 0x38, 0x60, 0xCC, 0xFC, 0x00},
    // 3 (51)
    {0x78, 0xCC, 0x0C, 0x38, 0x0C, 0xCC, 0x78, 0x00},
    // 4 (52)
    {0x1C, 0x3C, 0x6C, 0xCC, 0xFE, 0x0C, 0x1E, 0x00},
    // 5 (53)
    {0xFC, 0xC0, 0xF8, 0x0C, 0x0C, 0xCC, 0x78, 0x00},
    // 6 (54)
    {0x38, 0x60, 0xC0, 0xF8, 0xCC, 0xCC, 0x78, 0x00},
    // 7 (55)
    {0xFC, 0xCC, 0x0C, 0x18, 0x30, 0x30, 0x30, 0x00},
    // 8 (56)
    {0x78, 0xCC, 0xCC, 0x78, 0xCC, 0xCC, 0x78, 0x00},
    // 9 (57)
    {0x78, 0xCC, 0xCC, 0x7C, 0x0C, 0x18, 0x70, 0x00},
    // : (58)
    {0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00},
    // ; (59)
    {0x00, 0x30, 0x30, 0x00, 0x00, 0x30, 0x60, 0x00},
    // < (60)
    {0x18, 0x30, 0x60, 0xC0, 0x60, 0x30, 0x18, 0x00},
    // = (61)
    {0x00, 0x00, 0xFC, 0x00, 0x00, 0xFC, 0x00, 0x00},
    // > (62)
    {0x60, 0x30, 0x18, 0x0C, 0x18, 0x30, 0x60, 0x00},
    // ? (63)
    {0x78, 0xCC, 0x0C, 0x18, 0x30, 0x00, 0x30, 0x00},
    // @ (64)
    {0x7C, 0xC6, 0xDE, 0xDE, 0xDE, 0xC0, 0x78, 0x00},
    // A (65)
    {0x30, 0x78, 0xCC, 0xCC, 0xFC, 0xCC, 0xCC, 0x00},
    // B (66)
    {0xFC, 0x66, 0x66, 0x7C, 0x66, 0x66, 0xFC, 0x00},
    // C (67)
    {0x3C, 0x66, 0xC0, 0xC0, 0xC0, 0x66, 0x3C, 0x00},
    // D (68)
    {0xF8, 0x6C, 0x66, 0x66, 0x66, 0x6C, 0xF8, 0x00},
    // E (69)
    {0xFE, 0x62, 0x68, 0x78, 0x68, 0x62, 0xFE, 0x00},
    // F (70)
    {0xFE, 0x62, 0x68, 0x78, 0x68, 0x60, 0xF0, 0x00},
    // G (71)
    {0x3C, 0x66, 0xC0, 0xC0, 0xCE, 0x66, 0x3E, 0x00},
    // H (72)
    {0xCC, 0xCC, 0xCC, 0xFC, 0xCC, 0xCC, 0xCC, 0x00},
    // I (73)
    {0x78, 0x30, 0x30, 0x30, 0x30, 0x30, 0x78, 0x00},
    // J (74)
    {0x1E, 0x0C, 0x0C, 0x0C, 0xCC, 0xCC, 0x78, 0x00},
    // K (75)
    {0xE6, 0x66, 0x6C, 0x78, 0x6C, 0x66, 0xE6, 0x00},
    // L (76)
    {0xF0, 0x60, 0x60, 0x60, 0x62, 0x66, 0xFE, 0x00},
    // M (77)
    {0xC6, 0xEE, 0xFE, 0xFE, 0xD6, 0xC6, 0xC6, 0x00},
    // N (78)
    {0xC6, 0xE6, 0xF6, 0xDE, 0xCE, 0xC6, 0xC6, 0x00},
    // O (79)
    {0x38, 0x6C, 0xC6, 0xC6, 0xC6, 0x6C, 0x38, 0x00},
    // P (80)
    {0xFC, 0x66, 0x66, 0x7C, 0x60, 0x60, 0xF0, 0x00},
    // Q (81)
    {0x78, 0xCC, 0xCC, 0xCC, 0xDC, 0x78, 0x1C, 0x00},
    // R (82)
    {0xFC, 0x66, 0x66, 0x7C, 0x6C, 0x66, 0xE6, 0x00},
    // S (83)
    {0x78, 0xCC, 0xE0, 0x70, 0x1C, 0xCC, 0x78, 0x00},
    // T (84)
    {0xFC, 0xB4, 0x30, 0x30, 0x30, 0x30, 0x78, 0x00},
    // U (85)
    {0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xFC, 0x00},
    // V (86)
    {0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0x78, 0x30, 0x00},
    // W (87)
    {0xC6, 0xC6, 0xC6, 0xD6, 0xFE, 0xEE, 0xC6, 0x00},
    // X (88)
    {0xC6, 0xC6, 0x6C, 0x38, 0x38, 0x6C, 0xC6, 0x00},
    // Y (89)
    {0xCC, 0xCC, 0xCC, 0x78, 0x30, 0x30, 0x78, 0x00},
    // Z (90)
    {0xFE, 0xC6, 0x8C, 0x18, 0x32, 0x66, 0xFE, 0x00},
    // [ (91)
    {0x78, 0x60, 0x60, 0x60, 0x60, 0x60, 0x78, 0x00},
    // \ (92)
    {0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x02, 0x00},
    // ] (93)
    {0x78, 0x18, 0x18, 0x18, 0x18, 0x18, 0x78, 0x00},
    // ^ (94)
    {0x10, 0x38, 0x6C, 0xC6, 0x00, 0x00, 0x00, 0x00},
    // _ (95)
    {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF},
    // ` (96)
    {0x30, 0x30, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00},
    // a (97)
    {0x00, 0x00, 0x78, 0x0C, 0x7C, 0xCC, 0x76, 0x00},
    // b (98)
    {0xE0, 0x60, 0x60, 0x7C, 0x66, 0x66, 0xDC, 0x00},
    // c (99)
    {0x00, 0x00, 0x78, 0xCC, 0xC0, 0xCC, 0x78, 0x00},
    // d (100)
    {0x1C, 0x0C, 0x0C, 0x7C, 0xCC, 0xCC, 0x76, 0x00},
    // e (101)
    {0x00, 0x00, 0x78, 0xCC, 0xFC, 0xC0, 0x78, 0x00},
    // f (102)
    {0x38, 0x6C, 0x60, 0xF0, 0x60, 0x60, 0xF0, 0x00},
    // g (103)
    {0x00, 0x00, 0x76, 0xCC, 0xCC, 0x7C, 0x0C, 0xF8},
    // h (104)
    {0xE0, 0x60, 0x6C, 0x76, 0x66, 0x66, 0xE6, 0x00},
    // i (105)
    {0x30, 0x00, 0x70, 0x30, 0x30, 0x30, 0x78, 0x00},
    // j (106)
    {0x0C, 0x00, 0x0C, 0x0C, 0x0C, 0xCC, 0xCC, 0x78},
    // k (107)
    {0xE0, 0x60, 0x66, 0x6C, 0x78, 0x6C, 0xE6, 0x00},
    // l (108)
    {0x70, 0x30, 0x30, 0x30, 0x30, 0x30, 0x78, 0x00},
    // m (109)
    {0x00, 0x00, 0xCC, 0xFE, 0xFE, 0xD6, 0xC6, 0x00},
    // n (110)
    {0x00, 0x00, 0xF8, 0xCC, 0xCC, 0xCC, 0xCC, 0x00},
    // o (111)
    {0x00, 0x00, 0x78, 0xCC, 0xCC, 0xCC, 0x78, 0x00},
    // p (112)
    {0x00, 0x00, 0xDC, 0x66, 0x66, 0x7C, 0x60, 0xF0},
    // q (113)
    {0x00, 0x00, 0x76, 0xCC, 0xCC, 0x7C, 0x0C, 0x1E},
    // r (114)
    {0x00, 0x00, 0xDC, 0x76, 0x66, 0x60, 0xF0, 0x00},
    // s (115)
    {0x00, 0x00, 0x7C, 0xC0, 0x78, 0x0C, 0xF8, 0x00},
    // t (116)
    {0x10, 0x30, 0x7C, 0x30, 0x30, 0x34, 0x18, 0x00},
    // u (117)
    {0x00, 0x00, 0xCC, 0xCC, 0xCC, 0xCC, 0x76, 0x00},
    // v (118)
    {0x00, 0x00, 0xCC, 0xCC, 0xCC, 0x78, 0x30, 0x00},
    // w (119)
    {0x00, 0x00, 0xC6, 0xD6, 0xFE, 0xFE, 0x6C, 0x00},
    // x (120)
    {0x00, 0x00, 0xC6, 0x6C, 0x38, 0x6C, 0xC6, 0x00},
    // y (121)
    {0x00, 0x00, 0xCC, 0xCC, 0xCC, 0x7C, 0x0C, 0xF8},
    // z (122)
    {0x00, 0x00, 0xFC, 0x98, 0x30, 0x64, 0xFC, 0x00},
    // { (123)
    {0x1C, 0x30, 0x30, 0xE0, 0x30, 0x30, 0x1C, 0x00},
    // | (124)
    {0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x18, 0x00},
    // } (125)
    {0xE0, 0x30, 0x30, 0x1C, 0x30, 0x30, 0xE0, 0x00},
    // ~ (126)
    {0x76, 0xDC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
};


// 清空显示缓冲区
void clear_buffer() {
    memset(display_buffer, 0, sizeof(display_buffer));
}

// 在指定位置绘制单个字符
void draw_char(int x, int y, char c) {
    if (c < 32 || c > 126) return; // 只支持可见ASCII字符

    int char_index = c - 32;
    const uint8_t* font_data = ascii_font[char_index];

    for (int row = 0; row < CHAR_HEIGHT; row++) {
        if (y + row >= BUFFER_HEIGHT) break;

        uint8_t font_row = font_data[row];
        for (int col = 0; col < CHAR_WIDTH; col++) {
            if (x + col >= BUFFER_WIDTH) break;

            if (font_row & (0x80 >> col)) {
                display_buffer[y + row][x + col] = 1;
            }
        }
    }
}

// 在指定字符位置绘制字符(自动计算像素位置)
void draw_char_at_pos(int char_x, int char_y, char c) {
    if (char_x >= CHARS_PER_ROW || char_y >= CHARS_PER_COL) return;

    int pixel_x = char_x * CHAR_WIDTH;
    int pixel_y = char_y * CHAR_HEIGHT;
    draw_char(pixel_x, pixel_y, c);
}

// 绘制字符串
void draw_string(int char_x, int char_y, const char* str) {
    int x = char_x;
    int y = char_y;

    while (*str && y < CHARS_PER_COL) {
        if (*str == '\n') {
            x = char_x;
            y++;
        }
        else {
            if (x < CHARS_PER_ROW) {
                draw_char_at_pos(x, y, *str);
                x++;
            }
            else {
                // 自动换行
                x = char_x;
                y++;
                if (y < CHARS_PER_COL) {
                    draw_char_at_pos(x, y, *str);
                    x++;
                }
            }
        }
        str++;
    }
}

// 打印缓冲区内容(用于调试)
void print_buffer() {
    for (int y = 0; y < BUFFER_HEIGHT; y++) {
        for (int x = 0; x < BUFFER_WIDTH; x++) {
            printf("%c", display_buffer[y][x] ? '#' : '.');
        }
        printf("\n");
    }
}

void SetConsoleSize(int width, int height) {
    HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);

    // 设置缓冲区大小
    COORD bufferSize = { (SHORT)width, (SHORT)height };
    SetConsoleScreenBufferSize(hConsole, bufferSize);

    // 设置窗口大小
    SMALL_RECT windowSize = { 0, 0, (SHORT)(width - 1), (SHORT)(height - 1) };
    SetConsoleWindowInfo(hConsole, TRUE, &windowSize);
}

// 示例使用
int main() {
    // 设置控制台为160列x120行
    SetConsoleSize(160, 120);

    // 清空缓冲区
    clear_buffer();

    // 绘制一些文本
    draw_string(0, 1, "ASCII Display Test");
    draw_string(0, 2, "0123456789");
    draw_string(0, 3, "ABCDEFGHIJKLMNOPQRST");
    draw_string(0, 4, "abcdefghijklmnopqrst");
    draw_string(0, 5, "!@#$%^&*()_+-=[]{}|;");
    draw_string(0, 6, "':\",./<>?`~");

    print_buffer();

    printf("Display buffer size: %dx%d pixels\n", BUFFER_WIDTH, BUFFER_HEIGHT);
    printf("Character grid: %dx%d characters\n", CHARS_PER_ROW, CHARS_PER_COL);
    printf("Output saved to display_output.pbm\n");

    return 0;
}

运行结果: