2026年2月更新,Step to UEFI 文章索引:
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工程师,整个过程中最重要的作用可能是:拿着板子去看了一下显微镜吧?
参考:
PDF 转 Markdown 网站
这个网站可以帮助你把 pdf 转为 MarkDown格式,比较特别的是除了文字,它还能将PDF中的图片以 Base64 格式重新编码后插入在 MD 文件中:
对于MD文件,普通的记事本就可以打开,但是如果想看带有图片的MD, 可以使用下面这这个网站 :
做一个 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&16cc2fc5&0&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<===---
English product name: "Disk 20"
ConnectionStatus:
Current Config Value: 0x01 -> Device Bus Speed: SuperSpeed
Device Address: 0x17
Open Pipes: 2
===>Device Descriptor<===
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<===---
===>Endpoint Descriptor<===
bLength: 0x07
bDescriptorType: 0x05
bEndpointAddress: 0x01 -> Direction: OUT - EndpointID: 1
bmAttributes: 0x02 -> Bulk Transfer Type
wMaxPacketSize: 0x0400
bInterval: 0x00
===>Endpoint Descriptor<===
bLength: 0x07
bDescriptorType: 0x05
bEndpointAddress: 0x81 -> Direction: IN - EndpointID: 1
bmAttributes: 0x02 -> Bulk Transfer Type
wMaxPacketSize: 0x0400
bInterval: 0x00
---===>Full Configuration Descriptor<===---
===>Configuration Descriptor<===
bLength: 0x09
bDescriptorType: 0x02
wTotalLength: 0x002C -> Validated
bNumInterfaces: 0x01
bConfigurationValue: 0x01
iConfiguration: 0x00
bmAttributes: 0x80 -> Bus Powered
MaxPower: 0x12 = 144 mA
===>Interface Descriptor<===
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,速度更快。
WAVE 音频文件查看器
吹蟑螂游戏机
这是一个使用表压传感器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*)≺
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年参加电子森林活动的项目,获得优秀奖
在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;
}
运行结果:

