ACPI 的_PS0() 和 _PS3() 

在 ACPI 的 ASL 里,_PS0() 和 _PS3() 是设备电源状态控制方法:

  • _PS0():表示把设备切到 D0,也就是全开 / 工作状态。
  • _PS3():表示把设备切到 D3,也就是低功耗 / 关闭状态(通常是 D3hot;如果平台还配合 _PR3 / _OFF,可能进一步到 D3cold)。

更直白一点:

  • 设备要“开机能工作”时,OS/ACPI 可能调用 _PS0()
  • 设备要“省电停用”时,OS/ACPI 可能调用 _PS3()

此外,类似的还有

  • _PR0:声明设备处于 D0 时依赖哪些 power resources
  • _PR3:声明设备进入 D3 路径 时依赖哪些 power resources
  • _ON() / _OFF():真正控制 power resource 的上电/断电动作

例如,下图中设备需要从 D0 进入 D3,首先是操作系统提出要求,然后通过驱动,调用 ACPI 设备中的 _PS3()。到了这一步,设备就停止了工作。如果你的设备支持D3Cold(彻底断电),那么还会调用_PR3()获得依赖的 Power Resource。最后再调用_OFF() 进行断电。这样设备的供电就被完全切断了。

将设备从 D3唤醒的方法和上面的类似,首先调用_PS0()。如果设备支持 D3 Cold那么需要调用_PR0()获得相关 Power Resource,最后通过_On(),恢复供电或者 Clock 之类的。设备才能够正常的开始工作。

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

继续给 UEFI 下的 EasyX 增加功能,这次增加了颜色转换的几个函数

  • HSVtoRGB(H, S, V)
  • HSLtoRGB(H, S, L)
  • RGBtoGRAY(Color)
  • RGBtoHSV(Color, *H, *S, *V)
  • RGBtoHSL(Color, *H, *S, *L)

测试代码绘制彩虹条:

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

/**
  程序入口。
**/
EFI_STATUS
EFIAPI
UefiMain (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
{
  EFI_STATUS  Status;
  INT32       W;
  INT32       H;
  INT32       PenX;
  INT32       PenY;
  IMAGE       Sprite  = { 0, 0, NULL };   // 使用前必须清零
  IMAGE       Grabbed = { 0, 0, NULL };   // getimage 会读取 Buffer 判断是否需释放
  ExMessage   Msg;
  BOOLEAN     Running;
  UINTN       ColorIndex;
  CHAR8       StatusBuf[96];
  CONST COLORREF  Palette[] = {
    LIGHTRED, LIGHTGREEN, LIGHTCYAN, YELLOW, LIGHTMAGENTA, WHITE
  };

  //
  // 初始化绘图设备(跟随 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 ();

  //
  // 颜色模型演示:顶部粗大 HSV 彩虹条 + 其灰度条(RGBtoGRAY)。
  //
  {
    INT32  Px;

    setlinestyle (PS_SOLID, 1);
    for (Px = 20; Px < W - 20; Px++) {
      INT32     Hue = (Px - 20) * 360 / (W - 40);
      COLORREF  Rainbow = HSVtoRGB (Hue % 360, 100, 100);

      setlinecolor (Rainbow);
      line (Px, 8, Px, 47);          // 彩虹条:40 像素高

      setlinecolor (RGBtoGRAY (Rainbow));
      line (Px, 50, Px, 69);         // 灰度条:20 像素高
    }
  }

  //
  // 直线:不同颜色/线宽/线型(下移,为彩虹条让位)。
  //
  setlinecolor (WHITE);
  setlinestyle (PS_SOLID, 1);
  line (20, 84, W - 20, 84);

  setlinecolor (LIGHTGREEN);
  setlinestyle (PS_SOLID, 3);
  line (20, 96, W - 20, 112);

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

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

  //
  // 矩形。
  //
  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);

  //
  // 文字(P2):不同颜色/大小/背景模式。
  //
  settextcolor (WHITE);
  settextstyle (16, 8);
  outtextxy (620, 150, "EasyX for UEFI");

  settextcolor (YELLOW);
  settextstyle (24, 0);            // 高 24,宽按比例
  outtextxy (620, 180, "Hello, GOP!");

  settextcolor (LIGHTGREEN);
  settextstyle (32, 16);
  setbkmode (TRANSPARENT);
  outtextxy (620, 220, "0123456789");

  settextcolor (BLACK);
  setbkcolor (LIGHTGRAY);
  setbkmode (OPAQUE);
  settextstyle (16, 8);
  outtextxy (620, 270, " OPAQUE text mode ");

  setbkmode (TRANSPARENT);
  settextcolor (LIGHTRED);
  settextstyle (20, 10);
  outtextxy (620, 320, "The quick brown fox");
  outtextxy (620, 345, "jumps over lazy dog.");

  //
  // 图像(P3)。
  //
  // 1) 用 SetWorkingImage 把绘图重定向到内存图像,绘制一个“精灵”。
  //
  if (!EFI_ERROR (CreateImage (&Sprite, 80, 80))) {
    SetWorkingImage (&Sprite);

    setbkcolor (RGB (20, 20, 20));
    cleardevice ();
    setfillcolor (RGB (240, 200, 40));
    solidcircle (40, 40, 34);
    setfillcolor (BLACK);
    solidcircle (28, 32, 5);
    solidcircle (52, 32, 5);
    setlinecolor (BLACK);
    setlinestyle (PS_SOLID, 2);
    line (24, 54, 56, 54);

    SetWorkingImage (NULL);   // 恢复到屏幕

    // 2) 直接覆盖绘制到屏幕多个位置。
    putimage (620, 400, &Sprite);
    putimage (710, 400, &Sprite);

    // 3) 透明贴图:跳过图像四角的深灰背景色。
    putimagetrans (800, 400, &Sprite, RGB (20, 20, 20));
  }

  //
  // 4) getimage:抓取屏幕左上角一块区域,复制到别处。
  //
  if (!EFI_ERROR (getimage (&Grabbed, 40, 140, 160, 120))) {
    putimage (620, 500, &Grabbed);
    DeleteImage (&Grabbed);
  }

  DeleteImage (&Sprite);

  //
  // 高级图形(P5):底部一条演示带(先清出干净背景)。
  //
  {
    INT32  Y0 = H - 150;

    setfillcolor (RGB (16, 16, 48));
    solidrectangle (0, Y0 - 10, W - 1, Y0 + 100);

    // 圆角矩形(边框 + 填充)
    setlinestyle (PS_SOLID, 1);
    setlinecolor (WHITE);
    setfillcolor (RGB (40, 120, 200));
    fillroundrect (30, Y0, 150, Y0 + 90, 40, 40);

    // 扇形(吃豆人)
    setfillcolor (YELLOW);
    fillpie (170, Y0, 290, Y0 + 90, 35, 300);

    // 圆弧
    setlinecolor (LIGHTCYAN);
    arc (310, Y0, 430, Y0 + 90, 0, 200);

    // 多边形(三角形,边框 + 填充)
    setlinecolor (WHITE);
    setfillcolor (LIGHTGREEN);
    {
      POINT  Tri[3] = {
        { 450, Y0 + 90 },
        { 510, Y0      },
        { 570, Y0 + 90 }
      };
      fillpolygon (Tri, 3);
    }

    // 折线(锯齿)
    setlinecolor (LIGHTMAGENTA);
    setlinestyle (PS_SOLID, 2);
    {
      POINT  Zig[5] = {
        { 590, Y0 + 90 },
        { 610, Y0      },
        { 630, Y0 + 90 },
        { 650, Y0      },
        { 670, Y0 + 90 }
      };
      polyline (Zig, 5);
    }

    // 区域填充:先画空心矩形,再对内部 floodfill
    setlinestyle (PS_SOLID, 1);
    setlinecolor (WHITE);
    rectangle (700, Y0, 800, Y0 + 90);
    setfillcolor (RGB (220, 80, 160));
    floodfill (750, Y0 + 45, RGB (16, 16, 48));
  }

  //
  // 键盘操作提示。
  //
  setbkmode (TRANSPARENT);
  settextcolor (WHITE);
  settextstyle (16, 8);
  outtextxy (20, H - 24, "Arrow keys=draw, Space=color, ESC=exit");

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

  //
  // 键盘交互循环(P4):方向键移动画笔绘线。
  //
  PenX       = W / 2;
  PenY       = H / 2;
  Running    = TRUE;
  ColorIndex = 0;
  while (Running) {
    INT32  NewX;
    INT32  NewY;

    getmessage (&Msg, EX_KEY);

    if (Msg.message != WM_KEYDOWN) {
      continue;
    }

    NewX = PenX;
    NewY = PenY;

    switch (Msg.vkcode) {
      case VK_LEFT:   NewX -= 8; break;
      case VK_RIGHT:  NewX += 8; break;
      case VK_UP:     NewY -= 8; break;
      case VK_DOWN:   NewY += 8; break;
      case VK_SPACE:  ColorIndex++; break;
      case VK_ESCAPE: Running = FALSE; break;
      default:        break;
    }

    if (!Running) {
      break;
    }

    //
    // 夹取到屏幕范围内。
    //
    if (NewX < 0) {
      NewX = 0;
    }

    if (NewY < 0) {
      NewY = 0;
    }

    if (NewX > W - 1) {
      NewX = W - 1;
    }

    if (NewY > H - 1) {
      NewY = H - 1;
    }

    //
    // 从旧位置到新位置画一段线。
    //
    setlinecolor (Palette[ColorIndex % (sizeof (Palette) / sizeof (Palette[0]))]);
    setlinestyle (PS_SOLID, 2);
    line (PenX, PenY, NewX, NewY);
    PenX = NewX;
    PenY = NewY;

    //
    // 底部显示当前画笔位置与按键(先用背景色清除旧文字)。
    //
    setfillcolor (RGB (16, 16, 48));
    solidrectangle (20, H - 48, 420, H - 28);
    AsciiSPrint (
      StatusBuf,
      sizeof (StatusBuf),
      "Pen: (%d, %d)  vkcode=0x%02x",
      PenX,
      PenY,
      Msg.vkcode
      );
    setbkmode (TRANSPARENT);
    settextcolor (LIGHTCYAN);
    settextstyle (16, 8);
    outtextxy (20, H - 46, StatusBuf);
  }

  closegraph ();

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

  return EFI_SUCCESS;
}

效果如下:

完整的代码下载:

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

继续改进代码,增加了一些绘制扇形之类的函数。

示例代码:

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

  先绘制静态场景(线/矩形/圆/椭圆/文字/图像),然后进入
  键盘交互:方向键移动“画笔”绘线,空格切换颜色,ESC 退出。
  本演示不依赖鼠标。
**/

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

/**
  程序入口。
**/
EFI_STATUS
EFIAPI
UefiMain (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
{
  EFI_STATUS  Status;
  INT32       W;
  INT32       H;
  INT32       PenX;
  INT32       PenY;
  IMAGE       Sprite  = { 0, 0, NULL };   // 使用前必须清零
  IMAGE       Grabbed = { 0, 0, NULL };   // getimage 会读取 Buffer 判断是否需释放
  ExMessage   Msg;
  BOOLEAN     Running;
  UINTN       ColorIndex;
  CHAR8       StatusBuf[96];
  CONST COLORREF  Palette[] = {
    LIGHTRED, LIGHTGREEN, LIGHTCYAN, YELLOW, LIGHTMAGENTA, WHITE
  };

  //
  // 初始化绘图设备(跟随 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);

  //
  // 文字(P2):不同颜色/大小/背景模式。
  //
  settextcolor (WHITE);
  settextstyle (16, 8);
  outtextxy (620, 150, "EasyX for UEFI");

  settextcolor (YELLOW);
  settextstyle (24, 0);            // 高 24,宽按比例
  outtextxy (620, 180, "Hello, GOP!");

  settextcolor (LIGHTGREEN);
  settextstyle (32, 16);
  setbkmode (TRANSPARENT);
  outtextxy (620, 220, "0123456789");

  settextcolor (BLACK);
  setbkcolor (LIGHTGRAY);
  setbkmode (OPAQUE);
  settextstyle (16, 8);
  outtextxy (620, 270, " OPAQUE text mode ");

  setbkmode (TRANSPARENT);
  settextcolor (LIGHTRED);
  settextstyle (20, 10);
  outtextxy (620, 320, "The quick brown fox");
  outtextxy (620, 345, "jumps over lazy dog.");

  //
  // 图像(P3)。
  //
  // 1) 用 SetWorkingImage 把绘图重定向到内存图像,绘制一个“精灵”。
  //
  if (!EFI_ERROR (CreateImage (&Sprite, 80, 80))) {
    SetWorkingImage (&Sprite);

    setbkcolor (RGB (20, 20, 20));
    cleardevice ();
    setfillcolor (RGB (240, 200, 40));
    solidcircle (40, 40, 34);
    setfillcolor (BLACK);
    solidcircle (28, 32, 5);
    solidcircle (52, 32, 5);
    setlinecolor (BLACK);
    setlinestyle (PS_SOLID, 2);
    line (24, 54, 56, 54);

    SetWorkingImage (NULL);   // 恢复到屏幕

    // 2) 直接覆盖绘制到屏幕多个位置。
    putimage (620, 400, &Sprite);
    putimage (710, 400, &Sprite);

    // 3) 透明贴图:跳过图像四角的深灰背景色。
    putimagetrans (800, 400, &Sprite, RGB (20, 20, 20));
  }

  //
  // 4) getimage:抓取屏幕左上角一块区域,复制到别处。
  //
  if (!EFI_ERROR (getimage (&Grabbed, 40, 140, 160, 120))) {
    putimage (620, 500, &Grabbed);
    DeleteImage (&Grabbed);
  }

  DeleteImage (&Sprite);

  //
  // 高级图形(P5):底部一条演示带(先清出干净背景)。
  //
  {
    INT32  Y0 = H - 150;

    setfillcolor (RGB (16, 16, 48));
    solidrectangle (0, Y0 - 10, W - 1, Y0 + 100);

    // 圆角矩形(边框 + 填充)
    setlinestyle (PS_SOLID, 1);
    setlinecolor (WHITE);
    setfillcolor (RGB (40, 120, 200));
    fillroundrect (30, Y0, 150, Y0 + 90, 40, 40);

    // 扇形(吃豆人)
    setfillcolor (YELLOW);
    fillpie (170, Y0, 290, Y0 + 90, 35, 300);

    // 圆弧
    setlinecolor (LIGHTCYAN);
    arc (310, Y0, 430, Y0 + 90, 0, 200);

    // 多边形(三角形,边框 + 填充)
    setlinecolor (WHITE);
    setfillcolor (LIGHTGREEN);
    {
      POINT  Tri[3] = {
        { 450, Y0 + 90 },
        { 510, Y0      },
        { 570, Y0 + 90 }
      };
      fillpolygon (Tri, 3);
    }

    // 折线(锯齿)
    setlinecolor (LIGHTMAGENTA);
    setlinestyle (PS_SOLID, 2);
    {
      POINT  Zig[5] = {
        { 590, Y0 + 90 },
        { 610, Y0      },
        { 630, Y0 + 90 },
        { 650, Y0      },
        { 670, Y0 + 90 }
      };
      polyline (Zig, 5);
    }

    // 区域填充:先画空心矩形,再对内部 floodfill
    setlinestyle (PS_SOLID, 1);
    setlinecolor (WHITE);
    rectangle (700, Y0, 800, Y0 + 90);
    setfillcolor (RGB (220, 80, 160));
    floodfill (750, Y0 + 45, RGB (16, 16, 48));
  }

  //
  // 键盘操作提示。
  //
  setbkmode (TRANSPARENT);
  settextcolor (WHITE);
  settextstyle (16, 8);
  outtextxy (20, H - 24, "Arrow keys=draw, Space=color, ESC=exit");

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

  //
  // 键盘交互循环(P4):方向键移动画笔绘线。
  //
  PenX       = W / 2;
  PenY       = H / 2;
  Running    = TRUE;
  ColorIndex = 0;
  while (Running) {
    INT32  NewX;
    INT32  NewY;

    getmessage (&Msg, EX_KEY);

    if (Msg.message != WM_KEYDOWN) {
      continue;
    }

    NewX = PenX;
    NewY = PenY;

    switch (Msg.vkcode) {
      case VK_LEFT:   NewX -= 8; break;
      case VK_RIGHT:  NewX += 8; break;
      case VK_UP:     NewY -= 8; break;
      case VK_DOWN:   NewY += 8; break;
      case VK_SPACE:  ColorIndex++; break;
      case VK_ESCAPE: Running = FALSE; break;
      default:        break;
    }

    if (!Running) {
      break;
    }

    //
    // 夹取到屏幕范围内。
    //
    if (NewX < 0) {
      NewX = 0;
    }

    if (NewY < 0) {
      NewY = 0;
    }

    if (NewX > W - 1) {
      NewX = W - 1;
    }

    if (NewY > H - 1) {
      NewY = H - 1;
    }

    //
    // 从旧位置到新位置画一段线。
    //
    setlinecolor (Palette[ColorIndex % (sizeof (Palette) / sizeof (Palette[0]))]);
    setlinestyle (PS_SOLID, 2);
    line (PenX, PenY, NewX, NewY);
    PenX = NewX;
    PenY = NewY;

    //
    // 底部显示当前画笔位置与按键(先用背景色清除旧文字)。
    //
    setfillcolor (RGB (16, 16, 48));
    solidrectangle (20, H - 48, 420, H - 28);
    AsciiSPrint (
      StatusBuf,
      sizeof (StatusBuf),
      "Pen: (%d, %d)  vkcode=0x%02x",
      PenX,
      PenY,
      Msg.vkcode
      );
    setbkmode (TRANSPARENT);
    settextcolor (LIGHTCYAN);
    settextstyle (16, 8);
    outtextxy (20, H - 46, StatusBuf);
  }

  closegraph ();

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

  return EFI_SUCCESS;
}

代码运行结果(新增的在最下面)

完整代码和编译后的 EFI下载:

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

这个版本加入了Easxy的键盘的函数。经过研究,我放弃了在 EasyX 中的鼠标支持,因此多次测试下来感觉一般的UEFI 使用环境下并不能很好的支持键盘。

编写测试代码实现下面的功能:

按键功能
方向键 ↑↓←→画笔从当前位置向该方向移动 8 像素,并画出一段连线(像涂鸦板)
空格之后画的线换一种颜色(红→绿→青→黄→品红→白循环)
ESC退出图形界面,回到文本控制台并打印 EasyX UEFI Demo 结束…
任意移动后屏幕底部实时显示 Pen: (x, y) vkcode=0x..

完整代码:

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

/**
  程序入口。
**/
EFI_STATUS
EFIAPI
UefiMain (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
{
  EFI_STATUS  Status;
  INT32       W;
  INT32       H;
  INT32       PenX;
  INT32       PenY;
  IMAGE       Sprite  = { 0, 0, NULL };   // 使用前必须清零
  IMAGE       Grabbed = { 0, 0, NULL };   // getimage 会读取 Buffer 判断是否需释放
  ExMessage   Msg;
  BOOLEAN     Running;
  UINTN       ColorIndex;
  CHAR8       StatusBuf[96];
  CONST COLORREF  Palette[] = {
    LIGHTRED, LIGHTGREEN, LIGHTCYAN, YELLOW, LIGHTMAGENTA, WHITE
  };

  //
  // 初始化绘图设备(跟随 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);

  //
  // 文字(P2):不同颜色/大小/背景模式。
  //
  settextcolor (WHITE);
  settextstyle (16, 8);
  outtextxy (620, 150, "EasyX for UEFI");

  settextcolor (YELLOW);
  settextstyle (24, 0);            // 高 24,宽按比例
  outtextxy (620, 180, "Hello, GOP!");

  settextcolor (LIGHTGREEN);
  settextstyle (32, 16);
  setbkmode (TRANSPARENT);
  outtextxy (620, 220, "0123456789");

  settextcolor (BLACK);
  setbkcolor (LIGHTGRAY);
  setbkmode (OPAQUE);
  settextstyle (16, 8);
  outtextxy (620, 270, " OPAQUE text mode ");

  setbkmode (TRANSPARENT);
  settextcolor (LIGHTRED);
  settextstyle (20, 10);
  outtextxy (620, 320, "The quick brown fox");
  outtextxy (620, 345, "jumps over lazy dog.");

  //
  // 图像(P3)。
  //
  // 1) 用 SetWorkingImage 把绘图重定向到内存图像,绘制一个“精灵”。
  //
  if (!EFI_ERROR (CreateImage (&Sprite, 80, 80))) {
    SetWorkingImage (&Sprite);

    setbkcolor (RGB (20, 20, 20));
    cleardevice ();
    setfillcolor (RGB (240, 200, 40));
    solidcircle (40, 40, 34);
    setfillcolor (BLACK);
    solidcircle (28, 32, 5);
    solidcircle (52, 32, 5);
    setlinecolor (BLACK);
    setlinestyle (PS_SOLID, 2);
    line (24, 54, 56, 54);

    SetWorkingImage (NULL);   // 恢复到屏幕

    // 2) 直接覆盖绘制到屏幕多个位置。
    putimage (620, 400, &Sprite);
    putimage (710, 400, &Sprite);

    // 3) 透明贴图:跳过图像四角的深灰背景色。
    putimagetrans (800, 400, &Sprite, RGB (20, 20, 20));
  }

  //
  // 4) getimage:抓取屏幕左上角一块区域,复制到别处。
  //
  if (!EFI_ERROR (getimage (&Grabbed, 40, 140, 160, 120))) {
    putimage (620, 500, &Grabbed);
    DeleteImage (&Grabbed);
  }

  DeleteImage (&Sprite);

  //
  // 键盘操作提示。
  //
  setbkmode (TRANSPARENT);
  settextcolor (WHITE);
  settextstyle (16, 8);
  outtextxy (20, H - 24, "Arrow keys=draw, Space=color, ESC=exit");

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

  //
  // 键盘交互循环(P4):方向键移动画笔绘线。
  //
  PenX       = W / 2;
  PenY       = H / 2;
  Running    = TRUE;
  ColorIndex = 0;
  while (Running) {
    INT32  NewX;
    INT32  NewY;

    getmessage (&Msg, EX_KEY);

    if (Msg.message != WM_KEYDOWN) {
      continue;
    }

    NewX = PenX;
    NewY = PenY;

    switch (Msg.vkcode) {
      case VK_LEFT:   NewX -= 8; break;
      case VK_RIGHT:  NewX += 8; break;
      case VK_UP:     NewY -= 8; break;
      case VK_DOWN:   NewY += 8; break;
      case VK_SPACE:  ColorIndex++; break;
      case VK_ESCAPE: Running = FALSE; break;
      default:        break;
    }

    if (!Running) {
      break;
    }

    //
    // 夹取到屏幕范围内。
    //
    if (NewX < 0) {
      NewX = 0;
    }

    if (NewY < 0) {
      NewY = 0;
    }

    if (NewX > W - 1) {
      NewX = W - 1;
    }

    if (NewY > H - 1) {
      NewY = H - 1;
    }

    //
    // 从旧位置到新位置画一段线。
    //
    setlinecolor (Palette[ColorIndex % (sizeof (Palette) / sizeof (Palette[0]))]);
    setlinestyle (PS_SOLID, 2);
    line (PenX, PenY, NewX, NewY);
    PenX = NewX;
    PenY = NewY;

    //
    // 底部显示当前画笔位置与按键(先用背景色清除旧文字)。
    //
    setfillcolor (RGB (16, 16, 48));
    solidrectangle (20, H - 48, 420, H - 28);
    AsciiSPrint (
      StatusBuf,
      sizeof (StatusBuf),
      "Pen: (%d, %d)  vkcode=0x%02x",
      PenX,
      PenY,
      Msg.vkcode
      );
    setbkmode (TRANSPARENT);
    settextcolor (LIGHTCYAN);
    settextstyle (16, 8);
    outtextxy (20, H - 46, StatusBuf);
  }

  closegraph ();

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

  return EFI_SUCCESS;
}

运行结果(中间的线段是运行时我使用方向键绘制的)

有兴趣的朋友可以试试。

CH32V467 官方开发板的一个“坑”

如果你想使用CH32V467官方开发板测试USB Host 功能,那么一定需要特别注意:原始设计并不支持 USB 端口取电(只能对板子供电)。为了确认这一点,你可以使用万用表测试对应的USB端口电压,如果你插入一个U盘,这个端口电压不到1V。

具体设计如下:两个端口分别使用了一个CH213K 的理想二极管。

如果你想征程测试,需要去掉 CH213K, 然后短接 R45 R45。

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

继续完善 EasyX,这次增加几个了图像处理的函数,可以复制图像到指定位置。有兴趣的可以直接比较这一次的 EasyX.h 和之前的定义。

测试代码:

#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;
  IMAGE       Sprite;
  IMAGE       Grabbed;

  //
  // 初始化绘图设备(跟随 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);

  //
  // 文字(P2):不同颜色/大小/背景模式。
  //
  settextcolor (WHITE);
  settextstyle (16, 8);
  outtextxy (620, 150, "EasyX for UEFI");

  settextcolor (YELLOW);
  settextstyle (24, 0);            // 高 24,宽按比例
  outtextxy (620, 180, "Hello, GOP!");

  settextcolor (LIGHTGREEN);
  settextstyle (32, 16);
  setbkmode (TRANSPARENT);
  outtextxy (620, 220, "0123456789");

  settextcolor (BLACK);
  setbkcolor (LIGHTGRAY);
  setbkmode (OPAQUE);
  settextstyle (16, 8);
  outtextxy (620, 270, " OPAQUE text mode ");

  setbkmode (TRANSPARENT);
  settextcolor (LIGHTRED);
  settextstyle (20, 10);
  outtextxy (620, 320, "The quick brown fox");
  outtextxy (620, 345, "jumps over lazy dog.");

  //
  // 图像(P3)。
  //
  // 1) 用 SetWorkingImage 把绘图重定向到内存图像,绘制一个“精灵”。
  //
  if (!EFI_ERROR (CreateImage (&Sprite, 80, 80))) {
    SetWorkingImage (&Sprite);

    setbkcolor (RGB (20, 20, 20));
    cleardevice ();
    setfillcolor (RGB (240, 200, 40));
    solidcircle (40, 40, 34);
    setfillcolor (BLACK);
    solidcircle (28, 32, 5);
    solidcircle (52, 32, 5);
    setlinecolor (BLACK);
    setlinestyle (PS_SOLID, 2);
    line (24, 54, 56, 54);

    SetWorkingImage (NULL);   // 恢复到屏幕

    // 2) 直接覆盖绘制到屏幕多个位置。
    putimage (620, 400, &Sprite);
    putimage (710, 400, &Sprite);

    // 3) 透明贴图:跳过图像四角的深灰背景色。
    putimagetrans (800, 400, &Sprite, RGB (20, 20, 20));
  }

  //
  // 4) getimage:抓取屏幕左上角一块区域,复制到别处。
  //
  if (!EFI_ERROR (getimage (&Grabbed, 40, 140, 160, 120))) {
    putimage (620, 500, &Grabbed);
    DeleteImage (&Grabbed);
  }

  DeleteImage (&Sprite);

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

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

  closegraph ();

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

  return EFI_SUCCESS;
}

运行结果:

因为函数涉及到了内存操作,因此无法再像之前一样用EDK2 自带的模拟器进行测试。这篇以及后面的系列文章改用 QEMU 进行测试。对应的 QEMU 启动命令如下:

qemu-system-x86_64  -bios OVMF.fd -m 1024 -usb -device usb-mouse -drive format=raw,file=fat:rw:./my_efi_app/ -serial stdio -net none

测试时,请在 QEMU目录中创建一个 my_efi_app 目录,并将需要测试的EFI 放入其中。

完整的代码已经编译后的 EFI文件:

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

继续前面提到的在 UEFI下重新制作一个 EasyX 的话题,这次加入 Ascii 显示的功能。

新增加了Ascii显示功能,可以调整字体大小。

直接上结果:

上述测试代的代码:

#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);

  //
  // 文字(P2):不同颜色/大小/背景模式。
  //
  settextcolor (WHITE);
  settextstyle (16, 8);
  outtextxy (620, 150, "EasyX for UEFI");

  settextcolor (YELLOW);
  settextstyle (24, 0);            // 高 24,宽按比例
  outtextxy (620, 180, "Hello, GOP!");

  settextcolor (LIGHTGREEN);
  settextstyle (32, 16);
  setbkmode (TRANSPARENT);
  outtextxy (620, 220, "0123456789");

  settextcolor (BLACK);
  setbkcolor (LIGHTGRAY);
  setbkmode (OPAQUE);
  settextstyle (16, 8);
  outtextxy (620, 270, " OPAQUE text mode ");

  setbkmode (TRANSPARENT);
  settextcolor (LIGHTRED);
  settextstyle (20, 10);
  outtextxy (620, 320, "The quick brown fox");
  outtextxy (620, 345, "jumps over lazy dog.");

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

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

  closegraph ();

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

  return EFI_SUCCESS;
}

完整的代码和编译后的 EFI 文件下载:

CH32V407 I2C 扫描代码

这里提供一个 CH32V407 I2C 设备扫描代码,可以用于调试I2C 设备。使用默认的 PB6-SCL / PB7-SDA 两个引脚。

/*
 *@Note
 7-bit address mode, master/slave mode, transceiver routine:
 I2C1_SCL(PB6)\I2C1_SDA(PB7).
 */

#include "debug.h"

#define I2C_TIMEOUT     0xFFFF

/*********************************************************************
 * @fn      IIC_Init
 *
 * @brief   Initializes the IIC peripheral.
 *
 * @return  none
 */
void IIC_Init(u32 bound)
{
    GPIO_InitTypeDef GPIO_InitStructure = {0};
    I2C_InitTypeDef  I2C_InitTSturcture = {0};

    RCC_PB2PeriphClockCmd(RCC_PB2Periph_GPIOB | RCC_PB2Periph_AFIO, ENABLE);
    RCC_PB1PeriphClockCmd(RCC_PB1Periph_I2C1, ENABLE);

    /* 不使用重映射,I2C1走PB6/PB7 */
    GPIO_PinRemapConfig(GPIO_Remap_I2C1, DISABLE);

    GPIO_SetBits(GPIOB, GPIO_Pin_6 | GPIO_Pin_7);

    I2C_InitTSturcture.I2C_ClockSpeed          = bound;
    I2C_InitTSturcture.I2C_Mode                = I2C_Mode_I2C;
    I2C_InitTSturcture.I2C_DutyCycle           = I2C_DutyCycle_16_9;
    I2C_InitTSturcture.I2C_OwnAddress1         = 0;
    I2C_InitTSturcture.I2C_Ack                 = I2C_Ack_Enable;
    I2C_InitTSturcture.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
    I2C_Init(I2C1, &I2C_InitTSturcture);

    I2C_Cmd(I2C1, ENABLE);

    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_6;
    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_AF_OD;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_High;
    GPIO_Init(GPIOB, &GPIO_InitStructure);

    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_7;
    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_AF_OD;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_High;
    GPIO_Init(GPIOB, &GPIO_InitStructure);
}

/* 等待某个事件,带超时 */
static int I2C_WaitEvent(I2C_TypeDef *I2Cx, uint32_t event)
{
    uint32_t timeout = I2C_TIMEOUT;
    while(!I2C_CheckEvent(I2Cx, event))
    {
        if(--timeout == 0)
            return 0;
    }
    return 1;
}

/* 扫描单个地址是否存在
 * addr: 7-bit地址
 * 返回: 1=存在, 0=不存在
 */
int I2C_CheckDevice(uint8_t addr)
{
    uint32_t timeout;

    /* 等待总线空闲 */
    timeout = I2C_TIMEOUT;
    while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY))
    {
        if(--timeout == 0)
            return 0;
    }

    /* 产生START */
    I2C_GenerateSTART(I2C1, ENABLE);
    if(!I2C_WaitEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT))
        goto fail;

    /* 发送7位地址,写方向 */
    I2C_Send7bitAddress(I2C1, addr << 1, I2C_Direction_Transmitter);

    timeout = I2C_TIMEOUT;
    while(1)
    {
        /* ADDR: 收到ACK */
        if(I2C_GetFlagStatus(I2C1, I2C_FLAG_ADDR))
        {
            /* 清ADDR标志 */
            (void)I2C1->STAR1;
            (void)I2C1->STAR2;

            I2C_GenerateSTOP(I2C1, ENABLE);
            return 1;
        }

        /* AF: 无应答 */
        if(I2C_GetFlagStatus(I2C1, I2C_FLAG_AF))
        {
            I2C_ClearFlag(I2C1, I2C_FLAG_AF);
            goto fail;
        }

        if(--timeout == 0)
            goto fail;
    }

fail:
    I2C_GenerateSTOP(I2C1, ENABLE);
    return 0;
}

/* 扫描I2C设备 */
void I2C_ScanBus(void)
{
    uint8_t addr;
    uint8_t found = 0;

    printf("I2C bus scanning...\r\n");

    for(addr = 0x08; addr <= 0x77; addr++)
    {
        if(I2C_CheckDevice(addr))
        {
            printf("Found device at 0x%02X\r\n", addr);
            found++;
        }
        Delay_Ms(2);
    }

    if(found == 0)
        printf("No I2C device found.\r\n");
    else
        printf("Scan done, total %d device(s).\r\n", found);
}


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

    printf("IIC Host mode\r\n");
    IIC_Init(100000);

    while(1)
    {
        I2C_ScanBus();
        printf("Rescan after 2 seconds...\r\n\r\n");
        Delay_Ms(2000);
    }

}

运行结果

Arduino 直接驱动ssd1306 OLED

最近入手了一个 0.91寸OLED ,主控是 SSD1306。特别注意 SSD1306 支持 128×32和 128×64 两种模式,对于这个 OLED是 128×32 分辨率。

为了更方便的在项目中驱动这款 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();
}

运行效果如下:

完整的代码下载: