Chương trình điều khiển màn hình OLED 0.96 inch dựa trên STM32F1, hỗ trợ I2C phần cứng/phần mềm

Driver cho màn hình OLED 0.96 inch 4 chân giao tiếp I2C dựa trên STM32F103, hỗ trợ IIC cứng/IIC mềm.

Driver này khá hoàn chỉnh, có thể hiển thị tiếng Anh, số nguyên, số thực, tiếng Trung, hình ảnh, số nhị phân, số thập lục phân… có thể vẽ điểm, đường thẳng, hình chữ nhật, tròn, elip, tam giác… hỗ trợ nhiều font chữ, gần như một thư viện đồ họa thu nhỏ.

Chương trình được viết lại từ mã gốc của Jiangxie Technology; bản gốc chỉ hỗ trợ I2C mềm, mình sửa lại để hỗ trợ I2C cứng, đồng thời có thể đổi macro để quay lại dùng I2C mềm.

Code dùng thư viện chuẩn ST.

Test trên STM32F103C8T6 và AIR32F103CBT6, cả hai đều chạy tốt; khi dùng I2C cứng, STM32 đạt tốc độ tối đa 1,3 Mbit/s, AIR32 chỉ lên được 600 kbit/s.

Nguyên lý điều khiển OLED và hướng dẫn sử dụng driver xem video của Jiangxie Technology: https://url.zeruns.com/L7j6y

STM32 đọc cảm biến nhiệt độ độ ẩm SHTC3 qua I2C cứng: https://blog.zeruns.com/archives/692.html

Template project STM32F407 đã port sẵn thư viện đồ họa U8g2: https://blog.zeruns.com/archives/722.html

Nhóm thảo luận điện tử/vi điều khiển: 2169025065

Ảnh demo

Giới thiệu nhanh về I2C

Giao thức I2C (Inter-Integrated Circuit) do Philips phát triển; ít chân, dễ triển khai phần cứng, mở rộng tốt, không cần chip chuyển đổi mức như USART, CAN… nên được dùng phổ biến để giao tiếp giữa các IC trong hệ thống.

I2C chỉ có một tuyến dữ liệu SDA (Serial Data Line), truyền từng bit, thuộc truyền thông nối tiếp, bán song công.

Bán song công: truyền được cả hai chiều nhưng không đồng thời, phải luân phiên; có thể hiểu là “single-duplex” có thể đổi chiều, mỗi thời điểm chỉ một chiều, cần duy nhất một tuyến dữ liệu.

Người ta chia giao thức I2C thành tầng vật lý và tầng protocol: tầng vật lý quy định đặc tính cơ-điện (phần cứng) để đảm bảo truyền dữ liệu thô trên phương tiện vật lý; tầng protocol quy định logic giao tiếp, thống nhất cách đóng/mở gói tin giữa hai bên (phần mềm).

Tầng vật lý I2C

Cách kết nối thường gặp giữa các thiết bị I2C

(1) Là bus đa chủ: nhiều thiết bị chia sẻ cùng tuyến tín hiệu. Trên một bus I2C có thể treo nhiều master và nhiều slave.

(2) Chỉ cần hai tuyến: SDA (Serial Data Line) hai chiều và SCL (Serial Clock Line) xung nhịp.

(3) Bus được kéo lên VCC qua điện trở pull-up. Khi thiết bị I2C rảnh sẽ ở trạng thái high-Z; nếu tất cả đều rảnh thì điện trở pull-up sẽ kéo bus lên mức cao.

Khi giao tiếp I2C, GPIO của vi điều khiển phải đặt ở chế độ open-drain, nếu không có thể gây chập.

Chi tiết thêm về I2C trên STM32 và cách dùng xem bài: https://url.zeruns.com/JC0Ah

Seri học STM32 của Jiangxie Technology: https://www.bilibili.com/video/BV1th411z7sn?p=31

Mình không lặp lại ở đây.

Linh kiện cần chuẩn bị

  • Bộ kit khởi đầu STM32: https://u.jd.com/fQS0YAe
  • Board最小系统 STM32F103: https://s.click.taobao.com/TsFFvwt
  • Module OLED: https://s.click.taobao.com/EF0Evwt
  • Dây Dupont: https://s.click.taobao.com/VMkDvwt
  • Breadboard: https://s.click.taobao.com/bhg8Txt
  • DAPLink (thay thế ST-Link, có cổng ảo): https://s.click.taobao.com/QVQ8Txt

Bộ kit khởi đầu STM32 của Jiangxie Technology: https://s.click.taobao.com/NTn9Txt

Code

Link tải toàn bộ project:

Baidu Netdisk: https://url.zeruns.com/kSxoe Mã giải nén: wgc3

123 Pan (không giới hạn tốc độ): https://www.123pan.com/s/2Y9Djv-HGcvH.html Mã giải nén: m7sp

Project tạo bằng Keil5, phát triển thêm bằng Vscode+EIDE, cả hai đều mở được.

Toàn bộ file project dùng mã hóa UTF-8; nếu mở bị lỗi font vui lòng chuyển editor sang UTF-8.

File main.c:

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"

/**
 * @brief Đảo trạng thái mức logic của một chân GPIO.
 * 
 * @param GPIOx Con trỏ port GPIO, ví dụ GPIOA, GPIOB…
 * @param GPIO_Pin Chân cần đảo, có thể là một chân (GPIO_Pin_0) hoặc nhiều chân (GPIO_Pin_0 | GPIO_Pin_1).
 * 
 * Hàm đọc mức ra hiện tại của chân; nếu đang thấp (Bit_RESET) thì set cao (Bit_SET), ngược lại thì reset thấp.
 */
void GPIO_TogglePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
{
    // Đọc trạng thái chân
    if (GPIO_ReadOutputDataBit(GPIOx, GPIO_Pin) == Bit_RESET)
    {
        // Nếu đang thấp → lên cao
        GPIO_SetBits(GPIOx, GPIO_Pin);
    }
    else
    {
        // Nếu đang cao → xuống thấp
        GPIO_ResetBits(GPIOx, GPIO_Pin);
    }
}
```int main(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);//Bật clock GPIOC
	// Khởi tạo chân GPIO
    GPIO_InitTypeDef GPIO_InitStructure;                // Khai báo cấu trúc cấu hình GPIO
    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_Out_PP;   // Đặt chế độ GPIO là open-drain, cần pull-up
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;   // Tốc độ GPIO 50 MHz
    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_13; 		// Chân OLED_SCL và OLED_SDA
    GPIO_Init(GPIOC, &GPIO_InitStructure);          	// Khởi tạo GPIO

	GPIO_TogglePin(GPIOC, GPIO_Pin_13);	// Nháy LED

	/*Khởi tạo OLED*/
	OLED_Init();
	
	/*Hiển thị ký tự 'A' tại (0, 0), cỡ font 8×16*/
	//OLED_ShowChar(0, 0, 'A', OLED_8X16);
	
	/*Hiển thị chuỗi "blog.zeruns.com" tại (16, 0), cỡ font 8×16*/
	OLED_ShowString(0, 0, "blog.zeruns.com", OLED_8X16);
	
	/*Hiển thị ký tự 'A' tại (0, 18), cỡ font 6×8*/
	OLED_ShowChar(0, 18, 'A', OLED_6X8);
	
	/*Hiển thị chuỗi "Hello World!" tại (16, 18), cỡ font 6×8*/
	OLED_ShowString(16, 18, "Hello World!", OLED_6X8);
	
	/*Hiển thị số 12345 tại (0, 28), độ dài 5, cỡ font 6×8*/
	OLED_ShowNum(0, 28, 12345, 5, OLED_6X8);
	
	/*Hiển thị số có dấu -66 tại (40, 28), độ dài 2, cỡ font 6×8*/
	OLED_ShowSignedNum(40, 28, -66, 2, OLED_6X8);
	
	/*Hiển thị số thập lục phân 0xA5A5 tại (70, 28), độ dài 4, cỡ font 6×8*/
	OLED_ShowHexNum(70, 28, 0xA5A5, 4, OLED_6X8);
	
	/*Hiển thị số nhị phân 0xA5 tại (0, 38), độ dài 8, cỡ font 6×8*/
	OLED_ShowBinNum(0, 38, 0xA5, 8, OLED_6X8);
	
	/*Hiển thị số thực 123.45 tại (60, 38), phần nguyên 3 chữ số, phần thập phân 2 chữ số, cỡ font 6×8*/
	OLED_ShowFloatNum(60, 38, 123.45, 3, 2, OLED_6X8);
	
	/*Hiển thị chuỗi tiếng Trung "你好,世界。" tại (0, 48), cỡ font cố định 16×16*/
	OLED_ShowChinese(0, 48, "你好,世界。");
	
	/*Hiển thị ảnh tại (96, 48), rộng 16 pixel, cao 16 pixel, dữ liệu ảnh mảng Diode*/
	OLED_ShowImage(96, 48, 16, 16, Diode);
	
	/*In chuỗi định dạng tại (96, 18), cỡ font 6×8, chuỗi "[%02d]"*/
	OLED_Printf(96, 18, OLED_6X8, "[%02d]", 6);
	
	/*Gọi OLED_Update để cập nhật bộ nhớ đệm lên OLED*/
	OLED_Update();
	
	/*Trì hoãn 3000 ms để quan sát*/
	Delay_ms(3000);

	GPIO_TogglePin(GPIOC, GPIO_Pin_13);	// Nháy LED
	
	/*Xóa bộ nhớ đệm OLED*/
	OLED_Clear();
	
	/*Vẽ điểm tại (5, 8)*/
	OLED_DrawPoint(5, 8);
	
	/*Lấy trạng thái điểm tại (5, 8)*/
	if (OLED_GetPoint(5, 8))
	{
		/*Nếu điểm sáng, hiển thị "YES" tại (10, 4), cỡ font 6×8*/
		OLED_ShowString(10, 4, "YES", OLED_6X8);
	}
	else
	{
		/*Nếu điểm tắt, hiển thị "NO " tại (10, 4), cỡ font 6×8*/
		OLED_ShowString(10, 4, "NO ", OLED_6X8);
	}
	
	/*Vẽ đường thẳng từ (40, 0) đến (127, 15)*/
	OLED_DrawLine(40, 0, 127, 15);
	
	/*Vẽ đường thẳng từ (40, 15) đến (127, 0)*/
	OLED_DrawLine(40, 15, 127, 0);
	
	/*Vẽ hình chữ nhật tại (0, 20), rộng 12 pixel, cao 15 pixel, không tô*/
	OLED_DrawRectangle(0, 20, 12, 15, OLED_UNFILLED);
	
	/*Vẽ hình chữ nhật tại (0, 40), rộng 12 pixel, cao 15 pixel, có tô*/
	OLED_DrawRectangle(0, 40, 12, 15, OLED_FILLED);
	
	/*Vẽ tam giác qua 3 điểm (20, 20), (40, 25), (30, 35), không tô*/
	OLED_DrawTriangle(20, 20, 40, 25, 30, 35, OLED_UNFILLED);
	
	/*Vẽ tam giác qua 3 điểm (20, 40), (40, 45), (30, 55), có tô*/
	OLED_DrawTriangle(20, 40, 40, 45, 30, 55, OLED_FILLED);
	
	/*Vẽ hình tròn tại (55, 27), bán kính 8 pixel, không tô*/
	OLED_DrawCircle(55, 27, 8, OLED_UNFILLED);
	
	/*Vẽ hình tròn tại (55, 47), bán kính 8 pixel, có tô*/
	OLED_DrawCircle(55, 47, 8, OLED_FILLED);
	
	/*Vẽ hình elip tại (82, 27), bán trục ngang 12 pixel, bán trục dọc 8 pixel, không tô*/
	OLED_DrawEllipse(82, 27, 12, 8, OLED_UNFILLED);
	// https://blog.zeruns.com
	/*Vẽ hình elip tại (82, 47), bán trục ngang 12 pixel, bán trục dọc 8 pixel, có tô*/
	OLED_DrawEllipse(82, 47, 12, 8, OLED_FILLED);
	
	/*Vẽ cung tròn tại (110, 18), bán kính 15 pixel, góc bắt đầu 25°, góc kết thúc 125°, không tô*/
	OLED_DrawArc(110, 18, 15, 25, 125, OLED_UNFILLED);
	
	/*Vẽ cung tròn tại (110, 38), bán kính 15 pixel, góc bắt đầu 25°, góc kết thúc 125°, có tô*/
	OLED_DrawArc(110, 38, 15, 25, 125, OLED_FILLED);
	
	/*Gọi OLED_Update để cập nhật bộ nhớ đệm lên OLED*/
	OLED_Update();
	
	/*Trì hoãn 1500 ms để quan sát*/
	Delay_ms(1500);

	GPIO_TogglePin(GPIOC, GPIO_Pin_13);	// Nháy LED
	
	while (1)
	{
		for (uint8_t i = 0; i < 4; i ++)
		{
			/*Đảo bit vùng từ (0, i * 16), rộng 128 pixel, cao 16 pixel*/
			OLED_ReverseArea(0, i * 16, 128, 16);
			
			/*Gọi OLED_Update để cập nhật bộ nhớ đệm lên OLED*/
			OLED_Update();
			
			/*Trì hoãn 1000 ms để quan sát*/
			Delay_ms(1000);
			
			/*Đảo bit lại như cũ*/
			OLED_ReverseArea(0, i * 16, 128, 16);

			GPIO_TogglePin(GPIOC, GPIO_Pin_13);	// Nháy LED
		}
		// https://blog.zeruns.com
		/*Đảo toàn bộ bộ nhớ đệm OLED*/
		OLED_Reverse();
		
		/*Gọi OLED_Update để cập nhật bộ nhớ đệm lên OLED*/
		OLED_Update();
		
		/*Trì hoãn 1000 ms để quan sát*/
		Delay_ms(1000);

		GPIO_TogglePin(GPIOC, GPIO_Pin_13);	// Nháy LED
	}
}

Tệp OLED.c:


/***************************************************************************************
 * Chương trình này được Jiangxie Technology tạo ra và chia sẻ miễn phí mã nguồn mở
 * Bạn có thể xem, sử dụng và chỉnh sửa tùy ý, áp dụng vào dự án của mình
 * Bản quyền chương trình thuộc về Jiangxie Technology, không ai được độc quyền
 *
 * Tên chương trình:		Driver OLED 0.96 inch (giao tiếp I2C 4 chân)
 * Thời gian tạo:		24.10.2023
 * Phiên bản hiện tại:		V1.1
 * Thời gian phát hành:		08.12.2023
 *
 * Website chính thức:		jiangxiekeji.com
 * Cửa hàng Taobao:		jiangxiekeji.taobao.com
 * Giới thiệu & cập nhật:	jiangxiekeji.com/tutorial/oled.html
 *
 * Nếu phát hiện lỗi hoặc sai sót, vui lòng gửi email: [email protected]
 * Trước khi gửi, hãy kiểm tra trang cập nhật để xem lỗi đã được sửa chưa
 ***************************************************************************************
 */

/*
 * Chương trình được zeruns chỉnh sửa lần hai
 * Nội dung sửa:	Thêm hỗ trợ I2C phần cứng, chọn qua định nghĩa macro
 * Ngày sửa:	25.02.2024
 * Blog:		https://blog.zeruns.com
 * Trang chủ Bilibili:	https://space.bilibili.com/8320520
*/

#include "stm32f10x.h"
#include "OLED.h"
#include <string.h>
#include <math.h>
#include <stdio.h>
#include <stdarg.h>

// Nếu dùng tiếng Trung, cần thêm tùy chọn biên dịch --no-multibyte-chars

/*
Chọn kiểu driver OLED, mặc định dùng I2C phần cứng. Nếu muốn dùng I2C phần mềm thì comment dòng định nghĩa macro I2C phần cứng và bỏ comment dòng I2C phần mềm.
Không được bỏ comment cả hai dòng cùng lúc!
*/
#define OLED_USE_HW_I2C		// I2C phần cứng
//#define OLED_USE_SW_I2C	// I2C phần mềm

/*Định nghĩa chân, có thể sửa đổi chân giao tiếp I2C tại đây*/
#define OLED_SCL  GPIO_Pin_6 // SCL
#define OLED_SDA  GPIO_Pin_7 // SDA
#define OLED_GPIO GPIOB
#define OLED_RCC  RCC_APB2Periph_GPIOB
/*STM32F103 chip I2C1 phần cứng: PB6 -- SCL; PB7 -- SDA */

/*Giao diện I2C, định nghĩa OLED dùng giao diện I2C nào*/
#define OLED_I2C     I2C1
#define OLED_I2C_RCC RCC_APB1Periph_I2C1

/*Địa chỉ slave OLED*/
#define OLED_ADDRESS 0x3C << 1	// 0x3C là địa chỉ 7 bit của OLED, dịch trái 1 bit bit cuối làm bit R/W thành 0x78

/*Thời gian chờ I2C*/
#define OLED_I2C_TIMEOUT 1000

/**
 * Định dạng lưu trữ dữ liệu:
 * 8 điểm dọc, bit cao ở dưới, trái sang phải, trên xuống dưới
 * Mỗi bit tương ứng một pixel
 *
 *      B0 B0                  B0 B0
 *      B1 B1                  B1 B1
 *      B2 B2                  B2 B2
 *      B3 B3  ------------->  B3 B3 --
 *      B4 B4                  B4 B4  |
 *      B5 B5                  B5 B5  |
 *      B6 B6                  B6 B6  |
 *      B7 B7                  B7 B7  |
 *                                    |
 *  -----------------------------------
 *  |
 *  |   B0 B0                  B0 B0
 *  |   B1 B1                  B1 B1
 *  |   B2 B2                  B2 B2
 *  --> B3 B3  ------------->  B3 B3
 *      B4 B4                  B4 B4
 *      B5 B5                  B5 B5
 *      B6 B6                  B6 B6
 *      B7 B7                  B7 B7
 *
 * Định nghĩa trục tọa độ:
 * Góc trên-trái là điểm (0, 0)
 * Trục X sang phải, giá trị: 0~127
 * Trục Y xuống dưới, giá trị: 0~63
 *
 *       0             trục X           127
 *      .------------------------------->
 *    0 |
 *      |
 *      |
 *      |
 * trục Y |
 *      |
 *      |
 *      |
 *   63 |
 *      v
 *
 */

/*Biến toàn cục*********************/
/**
 * Mảng bộ nhớ hiển thị OLED
 * Tất cả hàm hiển thị chỉ đọc/ghi mảng này
 * Sau đó gọi OLED_Update hoặc OLED_UpdateArea
 * mới gửi dữ liệu sang OLED hiển thị
 */
uint8_t OLED_DisplayBuf[8][128];
/*********************Biến toàn cục*/

#ifdef OLED_USE_SW_I2C
/**
  * Hàm: OLED viết mức SCL
  * Tham số: giá trị logic cần viết SCL, 0/1
  * Trả về: không
  * Mô tả: khi lớp trên cần viết SCL thì gọi hàm này
  *        người dùng cần đặt SCL theo tham số
  *        0: SCL thấp, 1: SCL cao
  */
void OLED_W_SCL(uint8_t BitValue)
{
	/*Đặt SCL theo BitValue*/
	GPIO_WriteBit(OLED_GPIO, OLED_SCL, (BitAction)BitValue);
	
	/*Nếu MCU quá nhanh, thêm delay nhỏ tránh vượt tốc độ I2C tối đa*/
	//...
}

/**
  * Hàm: OLED viết mức SDA
  * Tham số: giá trị logic cần viết SDA, 0/1
  * Trả về: không
  * Mô tả: khi lớp trên cần viết SDA thì gọi hàm này
  *        người dùng cần đặt SDA theo tham số
  *        0: SDA thấp, 1: SDA cao
  */
void OLED_W_SDA(uint8_t BitValue)
{
	/*Đặt SDA theo BitValue*/
	GPIO_WriteBit(OLED_GPIO, OLED_SDA, (BitAction)BitValue);
	
	/*Nếu MCU quá nhanh, thêm delay nhỏ tránh vượt tốc độ I2C tối đa*/
	//...
}
#endif

/**
 * Hàm: Khởi tạo chân OLED
 * Tham số: không
 * Trả về: không
 * Mô tả: khi lớp trên cần khởi tạo thì gọi hàm này
 *        người dùng cần đặt SCL và SDA ở chế độ open-drain và nhả chân
 */
void OLED_GPIO_Init(void)
{
    uint32_t i, j;

    /*Trước khởi tạo, thêm delay để nguồn OLED ổn định*/
    for (i = 0; i < 1000; i++) {
        for (j = 0; j < 1000; j++)
            ;
    }
#ifdef OLED_USE_HW_I2C
    RCC_APB1PeriphClockCmd(OLED_I2C_RCC, ENABLE); 	// Enable I2C1 clock
#endif
    RCC_APB2PeriphClockCmd(OLED_RCC, ENABLE);		// Enable GPIO clock

	GPIO_InitTypeDef GPIO_InitStructure;                 // Khai báo cấu trúc cấu hình GPIO
#ifdef OLED_USE_HW_I2C
    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_AF_OD;     // Chế độ AF open-drain, cần pull-up ngoài
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;    // Tốc độ 50 MHz
    GPIO_InitStructure.GPIO_Pin   = OLED_SCL | OLED_SDA; // Chân SCL và SDA
    GPIO_Init(OLED_GPIO, &GPIO_InitStructure);           // Khởi tạo GPIO
#elif defined(OLED_USE_SW_I2C)
 	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;	// Chế độ open-drain output, cần pull-up ngoài
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_InitStructure.GPIO_Pin   = OLED_SCL | OLED_SDA;// Chân SCL và SDA
 	GPIO_Init(OLED_GPIO, &GPIO_InitStructure);
	/*Nhả SCL và SDA*/
	OLED_W_SCL(1);
	OLED_W_SDA(1);
#endif

#ifdef OLED_USE_HW_I2C
    I2C_DeInit(OLED_I2C);                                                     // Reset I2C về mặc định
    I2C_InitTypeDef I2C_InitStructure;                                        // Khai báo cấu trúc cấu hình I2C
    I2C_InitStructure.I2C_Mode                = I2C_Mode_I2C;                 // Chế độ I2C
    I2C_InitStructure.I2C_DutyCycle           = I2C_DutyCycle_2;              // Duty cycle, Tlow/Thigh = 2
    I2C_InitStructure.I2C_OwnAddress1         = 0x30;                         // Địa chỉ I2C master, không dùng thì đặt tùy
    I2C_InitStructure.I2C_Ack                 = I2C_Ack_Enable;               // Enable ACK
    I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; // Địa chỉ 7 bit
    I2C_InitStructure.I2C_ClockSpeed          = 600000;// Tần số I2C 600 kHz, nên đặt 400 kHz (ổn định), trên STM32F103 tối đa ~1.3 MHz, AIR32F103 chỉ 600 kHz
    I2C_Init(OLED_I2C, &I2C_InitStructure);                                   // Khởi tạo I2C

    I2C_Cmd(OLED_I2C, ENABLE); // Enable I2C
#endif
}

// https://blog.zeruns.com

/*Giao thức truyền thông*********************/

/**
 * Hàm: I2C Start
 * Tham số: không
 * Trả về: không
 */
void OLED_I2C_Start(void)
{
#ifdef OLED_USE_HW_I2C
    for(uint16_t i = 0; I2C_GetFlagStatus(OLED_I2C, I2C_FLAG_BUSY) && i < OLED_I2C_TIMEOUT; i++);	// Kiểm tra I2C busy
    I2C_GenerateSTART(OLED_I2C, ENABLE); 						// Gửi start
    // Chờ event EV5 (master mode) đến khi xảy ra
    for(uint16_t i = 0; !I2C_CheckEvent(OLED_I2C, I2C_EVENT_MASTER_MODE_SELECT) && i < OLED_I2C_TIMEOUT; i++);
    I2C_Send7bitAddress(OLED_I2C, OLED_ADDRESS, I2C_Direction_Transmitter); // Gửi địa chỉ 7 bit, chuyển sang truyền
    for(uint16_t i = 0; !I2C_CheckEvent(OLED_I2C, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) && i < OLED_I2C_TIMEOUT; i++); // Chờ EV6
#elif defined(OLED_USE_SW_I2C)
	OLED_W_SDA(1);		//Nhả SDA, đảm bảo SDA cao
	OLED_W_SCL(1);		//Nhả SCL, đảm bảo SCL cao
	OLED_W_SDA(0);		//Kéo SDA thấp trong SCL cao, tạo start
	OLED_W_SCL(0);		//Sau start kéo SCL thấp, chiếm bus và tiện cho timing
#endif
}

/**
 * Hàm: I2C Stop
 * Tham số: không
 * Trả về: không
 */
void OLED_I2C_Stop(void)
{
#ifdef OLED_USE_HW_I2C
    I2C_GenerateSTOP(OLED_I2C, ENABLE); // Dừng I2C
#elif defined(OLED_USE_SW_I2C)
	OLED_W_SDA(0);		//Kéo SDA thấp, đảm bảo SDA thấp
	OLED_W_SCL(1);		//Nhả SCL, SCL cao
	OLED_W_SDA(1);		//Trong SCL cao, nhả SDA, tạo stop
#endif
}

/**
 * Hàm: I2C gửi 1 byte
 * Tham số: Byte dữ liệu cần gửi, 0x00~0xFF
 * Trả về: không
 */
void OLED_I2C_SendByte(uint8_t Byte)
{
#ifdef OLED_USE_HW_I2C
    I2C_SendData(OLED_I2C, Byte);	// Gửi 1 byte
	// Chờ EV8_2, kiểm tra truyền xong
    for(uint16_t i = 0; I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED) != 1 && i < OLED_I2C_TIMEOUT; i++);
#elif defined(OLED_USE_SW_I2C)
	uint8_t i;
	/*Lặp 8 lần, master gửi từng bit*/
	for (i = 0; i < 8; i++)
	{
		/*Dùng mask lấy từng bit của Byte và viết lên SDA*/
		/*Hai dấu !! biến mọi giá trị khác 0 thành 1*/
		OLED_W_SDA(!!(Byte & (0x80 >> i)));
		OLED_W_SCL(1);	//Nhả SCL, slave đọc SDA trong SCL cao
		OLED_W_SCL(0);	//Kéo SCL thấp, master chuẩn bị bit tiếp
	}
	OLED_W_SCL(1);		//Thêm một xung, không xử lý ACK
	OLED_W_SCL(0);
#endif
}

/**
 * Hàm: OLED viết lệnh
 * Tham số: Command giá trị lệnh, 0x00~0xFF
 * Trả về: không
 */
void OLED_WriteCommand(uint8_t Command)
{
    OLED_I2C_Start();           // I2C start
#ifdef OLED_USE_SW_I2C
	OLED_I2C_SendByte(0x78);		//Gửi địa chỉ slave OLED
#endif
	OLED_I2C_SendByte(0x00);	//Byte điều khiển, 0x00: sắp viết lệnh
    OLED_I2C_SendByte(Command); // Viết lệnh
    OLED_I2C_Stop();            // I2C stop
}

/**
 * Hàm: OLED viết dữ liệu
 * Tham số: Data con trỏ dữ liệu cần viết
 * Tham số: Count số byte cần viết
 * Trả về: không
 */
void OLED_WriteData(uint8_t *Data, uint8_t Count)
{
    uint8_t i;

    OLED_I2C_Start();        // I2C start
#ifdef OLED_USE_SW_I2C
	OLED_I2C_SendByte(0x78);		//Gửi địa chỉ slave OLED
#endif
    OLED_I2C_SendByte(0x40); // Byte điều khiển, 0x40: sắp viết dữ liệu
    /*Lặp Count lần, viết liên tiếp*/
    for (i = 0; i < Count; i++) {
        OLED_I2C_SendByte(Data[i]); // Gửi từng byte
    }
    OLED_I2C_Stop(); // I2C stop
}

/*********************Giao thức truyền thông*/

/*Cấu hình phần cứng*********************/

/**
 * Hàm: Khởi tạo OLED
 * Tham số: không
 * Trả về: không
 * Mô tả: gọi trước khi dùng
 */
void OLED_Init(void)
{
    OLED_GPIO_Init(); // Gọi khởi tạo chân trước

    /*Viết chuỗi lệnh khởi tạo OLED*/
    OLED_WriteCommand(0xAE); // Tắt/mở hiển thị, 0xAE tắt, 0xAF mở

    OLED_WriteCommand(0xD5); // Thiết lập tỉ lệ chia clock/osc
    OLED_WriteCommand(0x80); // 0x00~0xFF

    OLED_WriteCommand(0xA8); // Thiết lập multiplex ratio
    OLED_WriteCommand(0x3F); // 0x0E~0x3F

    OLED_WriteCommand(0xD3); // Thiết lập offset hiển thị
    OLED_WriteCommand(0x00); // 0x00~0x7F

    OLED_WriteCommand(0x40); // Thiết lập dòng bắt đầu hiển thị, 0x40~0x7F

OLED_WriteCommand(0xA1); // Thiết lập hướng trái-phải, 0xA1 bình thường, 0xA0 đảo trái-phải

    OLED_WriteCommand(0xC8); // Thiết lập hướng trên-dưới, 0xC8 bình thường, 0xC0 đảo trên-dưới

    OLED_WriteCommand(0xDA); // Thiết lập cấu hình phần cứng chân COM
    OLED_WriteCommand(0x12);

    OLED_WriteCommand(0x81); // Thiết lập độ tương phản
    OLED_WriteCommand(0xCF); // 0x00~0xFF

    OLED_WriteCommand(0xD9); // Thiết lập chu kỳ nạp trước
    OLED_WriteCommand(0xF1);

    OLED_WriteCommand(0xDB); // Thiết lập mức VCOMH bỏ chọn
    OLED_WriteCommand(0x30);

    OLED_WriteCommand(0xA4); // Thiết lập bật/tắt toàn bộ màn hình

    OLED_WriteCommand(0xA6); // Thiết lập hiển thị bình thường/đảo màu, 0xA6 bình thường, 0xA7 đảo màu

    OLED_WriteCommand(0x8D); // Thiết lập bơm nạp
    OLED_WriteCommand(0x14);

    OLED_WriteCommand(0xAF); // Bật hiển thị

    OLED_Clear();  // Xóa mảng bộ nhớ hiển thị
    OLED_Update(); // Cập nhật hiển thị, xóa màn hình, tránh nhiễu sau khi khởi tạo
}

/**
 * Hàm: Thiết lập vị trí con trỏ hiển thị OLED
 * Tham số: Page chỉ trang hiện tại của con trỏ, phạm vi: 0~7
 * Tham số: X chỉ tọa độ trục X của con trỏ, phạm vi: 0~127
 * Giá trị trả về: Không
 * Mô tả: Theo mặc định của OLED, trục Y chỉ có thể ghi theo nhóm 8 bit, tức 1 trang bằng 8 tọa độ Y
 */
void OLED_SetCursor(uint8_t Page, uint8_t X)
{
    /*Nếu dùng chương trình này điều khiển màn hình OLED 1.3 inch, cần bỏ chú thích dòng dưới*/
    /*Vì driver OLED 1.3 inch (SH1106) có 132 cột*/
    /*Cột bắt đầu của màn hình nối ở cột 2, không phải 0*/
    /*Nên cần cộng thêm 2 cho X để hiển thị đúng*/
    //	X += 2;

    /*Thiết lập địa chỉ trang và cột qua lệnh*/
    OLED_WriteCommand(0xB0 | Page);              // Thiết lập vị trí trang
    OLED_WriteCommand(0x10 | ((X & 0xF0) >> 4)); // Thiết lập 4 bit cao của X
    OLED_WriteCommand(0x00 | (X & 0x0F));        // Thiết lập 4 bit thấp của X
}

/*********************Cấu hình phần cứng*/

/*Hàm tiện ích*********************/

/*Các hàm tiện ích chỉ dùng nội bộ*/

/**
 * Hàm: Hàm lũy thừa
 * Tham số: X cơ số
 * Tham số: Y số mũ
 * Giá trị trả về: Bằng X mũ Y
 */
uint32_t OLED_Pow(uint32_t X, uint32_t Y)
{
    uint32_t Result = 1; // Kết quả mặc định là 1
    while (Y--)          // Nhân lặp Y lần
    {
        Result *= X; // Mỗi lần nhân X vào kết quả
    }
    return Result;
}

/**
 * Hàm: Kiểm tra điểm có nằm trong đa giác cho trước
 * Tham số: nvert số đỉnh đa giác
 * Tham số: vertx verty mảng chứa tọa độ x và y các đỉnh đa giác
 * Tham số: testx testy tọa độ X và y của điểm cần kiểm tra
 * Giá trị trả về: Điểm có nằm trong đa giác không, 1: có, 0: không
 */
uint8_t OLED_pnpoly(uint8_t nvert, int16_t *vertx, int16_t *verty, int16_t testx, int16_t testy)
{
    int16_t i, j, c = 0;

    /*Thuật toán do W. Randolph Franklin đề xuất*/
    /*Tham khảo: https://wrfranklin.org/Research/Short_Notes/pnpoly.html*/
    for (i = 0, j = nvert - 1; i < nvert; j = i++) {
        if (((verty[i] > testy) != (verty[j] > testy)) &&
            (testx < (vertx[j] - vertx[i]) * (testy - verty[i]) / (verty[j] - verty[i]) + vertx[i])) {
            c = !c;
        }
    }
    return c;
}

/**
 * Hàm: Kiểm tra điểm có nằm trong góc cho trước
 * Tham số: X Y tọa độ điểm cần kiểm tra
 * Tham số: StartAngle EndAngle góc bắt đầu và kết thúc, phạm vi: -180~180
 *           Phía ngang bên phải là 0 độ, ngang trái là 180 hoặc -180 độ, dưới là số dương, trên là số âm, quay chiều kim đồng hồ
 * Giá trị trả về: Điểm có nằm trong góc không, 1: có, 0: không
 */
uint8_t OLED_IsInAngle(int16_t X, int16_t Y, int16_t StartAngle, int16_t EndAngle)
{
    int16_t PointAngle;
    PointAngle = atan2(Y, X) / 3.14 * 180; // Tính radian của điểm và chuyển sang độ
    if (StartAngle < EndAngle)             // Trường hợp góc bắt đầu nhỏ hơn góc kết thúc
    {
        /*Nếu góc nằm giữa góc bắt đầu và kết thúc thì điểm nằm trong góc*/
        if (PointAngle >= StartAngle && PointAngle <= EndAngle) {
            return 1;
        }
    } else // Trường hợp góc bắt đầu lớn hơn góc kết thúc
    {
        /*Nếu góc lớn hơn góc bắt đầu hoặc nhỏ hơn góc kết thúc thì điểm nằm trong góc*/
        if (PointAngle >= StartAngle || PointAngle <= EndAngle) {
            return 1;
        }
    }
    return 0; // Không thỏa mãn thì điểm nằm ngoài góc
}

/*********************Hàm tiện ích*/

/*Hàm chức năng*********************/

/**
 * Hàm: Cập nhật mảng bộ nhớ hiển thị OLED lên màn hình
 * Tham số: Không
 * Giá trị trả về: Không
 * Mô tả: Tất cả các hàm hiển thị chỉ đọc/ghi mảng bộ nhớ OLED
 *           Sau đó gọi OLED_Update hoặc OLED_UpdateArea
 *           mới gửi dữ liệu mảng lên phần cứng OLED để hiển thị
 *           Nên sau khi gọi hàm hiển thị, muốn thấy trên màn hình phải gọi hàm cập nhật
 */
void OLED_Update(void)
{
    uint8_t j;
    /*Duyệt từng trang*/
    for (j = 0; j < 8; j++) {
        /*Đặt con trỏ về cột đầu của mỗi trang*/
        OLED_SetCursor(j, 0);
        /*Ghi liên tiếp 128 byte, đưa dữ liệu từ mảng bộ nhớ lên phần cứng OLED*/
        OLED_WriteData(OLED_DisplayBuf[j], 128);
    }
}

/**
 * Hàm: Cập nhật một phần mảng bộ nhớ hiển thị OLED lên màn hình
 * Tham số: X hoành độ góc trên-trái vùng cần cập nhật, phạm vi: 0~127
 * Tham số: Y tung độ góc trên-trái vùng cần cập nhật, phạm vi: 0~63
 * Tham số: Width chiều rộng vùng, phạm vi: 0~128
 * Tham số: Height chiều cao vùng, phạm vi: 0~64
 * Giá trị trả về: Không
 * Mô tả: Hàm sẽ cập nhật ít nhất vùng được chỉ định
 *           Nếu vùng cập nhật chỉ bao phủ một phần trang, phần còn lại của trang đó cũng được cập nhật theo
 * Mô tả: Tất cả các hàm hiển thị chỉ đọc/ghi mảng bộ nhớ OLED
 *           Sau đó gọi OLED_Update hoặc OLED_UpdateArea
 *           mới gửi dữ liệu mảng lên phần cứng OLED để hiển thị
 *           Nên sau khi gọi hàm hiển thị, muốn thấy trên màn hình phải gọi hàm cập nhật
 */
void OLED_UpdateArea(uint8_t X, uint8_t Y, uint8_t Width, uint8_t Height)
{
    uint8_t j;

    /*Kiểm tra tham số, đảm bảo vùng không vượt ngoài màn hình*/
    if (X > 127) { return; }
    if (Y > 63) { return; }
    if (X + Width > 128) { Width = 128 - X; }
    if (Y + Height > 64) { Height = 64 - Y; }

    /*Duyệt các trang liên quan đến vùng chỉ định*/
    /*(Y + Height - 1) / 8 + 1 nhằm làm tròn lên của (Y + Height) / 8*/
    for (j = Y / 8; j < (Y + Height - 1) / 8 + 1; j++) {
        /*Đặt con trỏ về cột chỉ định của trang liên quan*/
        OLED_SetCursor(j, X);
        /*Ghi liên tiếp Width byte, đưa dữ liệu từ mảng bộ nhớ lên phần cứng OLED*/
        OLED_WriteData(&OLED_DisplayBuf[j][X], Width);
    }
}

/**
 * Hàm: Xóa toàn bộ mảng bộ nhớ hiển thị OLED
 * Tham số: Không
 * Giá trị trả về: Không
 * Mô tả: Sau khi gọi hàm này, muốn thấy trên màn hình phải gọi hàm cập nhật
 */
void OLED_Clear(void)
{
    uint8_t i, j;
    for (j = 0; j < 8; j++) // Duyệt 8 trang
    {
        for (i = 0; i < 128; i++) // Duyệt 128 cột
        {
            OLED_DisplayBuf[j][i] = 0x00; // Gán 0 cho toàn bộ mảng bộ nhớ
        }
    }
}

/**
 * Hàm: Xóa một phần mảng bộ nhớ hiển thị OLED
 * Tham số: X hoành độ góc trên-trái vùng cần xóa, phạm vi: 0~127
 * Tham số: Y tung độ góc trên-trái vùng cần xóa, phạm vi: 0~63
 * Tham số: Width chiều rộng vùng, phạm vi: 0~128
 * Tham số: Height chiều cao vùng, phạm vi: 0~64
 * Giá trị trả về: Không
 * Mô tả: Sau khi gọi hàm này, muốn thấy trên màn hình phải gọi hàm cập nhật
 */
void OLED_ClearArea(uint8_t X, uint8_t Y, uint8_t Width, uint8_t Height)
{
    uint8_t i, j;

    /*Kiểm tra tham số, đảm bảo vùng không vượt ngoài màn hình*/
    if (X > 127) { return; }
    if (Y > 63) { return; }
    if (X + Width > 128) { Width = 128 - X; }
    if (Y + Height > 64) { Height = 64 - Y; }

    for (j = Y; j < Y + Height; j++) // Duyệt các trang chỉ định
    {
        for (i = X; i < X + Width; i++) // Duyệt các cột chỉ định
        {
            OLED_DisplayBuf[j / 8][i] &= ~(0x01 << (j % 8)); // Xóa bit tương ứng trong mảng bộ nhớ
        }
    }
}

/**
 * Hàm: Đảo toàn bộ mảng bộ nhớ hiển thị OLED
 * Tham số: Không
 * Giá trị trả về: Không
 * Mô tả: Sau khi gọi hàm này, muốn thấy trên màn hình phải gọi hàm cập nhật
 */
void OLED_Reverse(void)
{
    uint8_t i, j;
    for (j = 0; j < 8; j++) // Duyệt 8 trang
    {
        for (i = 0; i < 128; i++) // Duyệt 128 cột
        {
            OLED_DisplayBuf[j][i] ^= 0xFF; // Đảo bit toàn bộ mảng bộ nhớ
        }
    }
}

/**
 * Hàm: Đảo một phần mảng bộ nhớ hiển thị OLED
 * Tham số: X hoành độ góc trên-trái vùng cần đảo, phạm vi: 0~127
 * Tham số: Y tung độ góc trên-trái vùng cần đảo, phạm vi: 0~63
 * Tham số: Width chiều rộng vùng, phạm vi: 0~128
 * Tham số: Height chiều cao vùng, phạm vi: 0~64
 * Giá trị trả về: Không
 * Mô tả: Sau khi gọi hàm này, muốn thấy trên màn hình phải gọi hàm cập nhật
 */
void OLED_ReverseArea(uint8_t X, uint8_t Y, uint8_t Width, uint8_t Height)
{
    uint8_t i, j;

    /*Kiểm tra tham số, đảm bảo vùng không vượt ngoài màn hình*/
    if (X > 127) { return; }
    if (Y > 63) { return; }
    if (X + Width > 128) { Width = 128 - X; }
    if (Y + Height > 64) { Height = 64 - Y; }

    for (j = Y; j < Y + Height; j++) // Duyệt các trang chỉ định
    {
        for (i = X; i < X + Width; i++) // Duyệt các cột chỉ định
        {
            OLED_DisplayBuf[j / 8][i] ^= 0x01 << (j % 8); // Đảo bit tương ứng trong mảng bộ nhớ
        }
    }
}

/**
 * Hàm: OLED hiển thị một ký tự
 * Tham số: X hoành độ góc trên-trái ký tự, phạm vi: 0~127
 * Tham số: Y tung độ góc trên-trái ký tự, phạm vi: 0~63
 * Tham số: Char ký tự cần hiển thị, phạm vi: ký tự ASCII có thể hiển thị
 * Tham số: FontSize cỡ chữ
 *           Phạm vi: OLED_8X16		rộng 8 pixel, cao 16 pixel
 *                 OLED_6X8		rộng 6 pixel, cao 8 pixel
 * Giá trị trả về: Không
 * Mô tả: Sau khi gọi hàm này, muốn thấy trên màn hình phải gọi hàm cập nhật
 */
void OLED_ShowChar(uint8_t X, uint8_t Y, char Char, uint8_t FontSize)
{
    if (FontSize == OLED_8X16) // Cỡ chữ rộng 8 pixel, cao 16 pixel
    {
        /*Hiển thị dữ liệu từ bộ font OLED_F8x16 dạng ảnh 8*16*/
        OLED_ShowImage(X, Y, 8, 16, OLED_F8x16[Char - ' ']);
    } else if (FontSize == OLED_6X8) // Cỡ chữ rộng 6 pixel, cao 8 pixel
    {
        /*Hiển thị dữ liệu từ bộ font OLED_F6x8 dạng ảnh 6*8*/
        OLED_ShowImage(X, Y, 6, 8, OLED_F6x8[Char - ' ']);
    }
}

/**
 * Hàm: OLED hiển thị chuỗi
 * Tham số: X hoành độ góc trên-trái chuỗi, phạm vi: 0~127
 * Tham số: Y tung độ góc trên-trái chuỗi, phạm vi: 0~63
 * Tham số: String chuỗi cần hiển thị, phạm vi: chuỗi gồm ký tự ASCII có thể hiển thị
 * Tham số: FontSize cỡ chữ
 *           Phạm vi: OLED_8X16		rộng 8 pixel, cao 16 pixel
 *                 OLED_6X8		rộng 6 pixel, cao 8 pixel
 * Giá trị trả về: Không
 * Mô tả: Sau khi gọi hàm này, muốn thấy trên màn hình phải gọi hàm cập nhật
 */
void OLED_ShowString(uint8_t X, uint8_t Y, char *String, uint8_t FontSize)
{
    uint8_t i;
    for (i = 0; String[i] != '\0'; i++) // Duyệt từng ký tự của chuỗi
    {
        /*Gọi OLED_ShowChar để hiển thị lần lượt từng ký tự*/
        OLED_ShowChar(X + i * FontSize, Y, String[i], FontSize);
    }
}/**
 * Hàm: OLED hiển thị số (thập phân, số nguyên dương)
 * Tham số: X tọa độ ngang góc trên-trái của chữ số, giới hạn: 0~127
 * Tham số: Y tọa độ dọc góc trên-trái của chữ số, giới hạn: 0~63
 * Tham số: Number số cần hiển thị, giới hạn: 0~4294967295
 * Tham số: Length độ dài chữ số, giới hạn: 0~10
 * Tham số: FontSize kích thước font
 *           Giới hạn: OLED_8X16 rộng 8 pixel, cao 16 pixel
 *                      OLED_6X8 rộng 6 pixel, cao 8 pixel
 * Giá trị trả về: không
 * Ghi chú: sau khi gọi hàm này, để thực sự hiển thị lên màn hình cần gọi hàm cập nhật
 */
void OLED_ShowNum(uint8_t X, uint8_t Y, uint32_t Number, uint8_t Length, uint8_t FontSize)
{
    uint8_t i;
    for (i = 0; i < Length; i++) // Duyệt từng chữ số
    {
        /*Gọi hàm OLED_ShowChar, hiển thị lần lượt từng chữ số*/
        /*Number / OLED_Pow(10, Length - i - 1) % 10 trích từng chữ số thập phân*/
        /*+ '0' chuyển số thành ký tự*/
        OLED_ShowChar(X + i * FontSize, Y, Number / OLED_Pow(10, Length - i - 1) % 10 + '0', FontSize);
    }
}

/**
 * Hàm: OLED hiển thị số có dấu (thập phân, số nguyên)
 * Tham số: X tọa độ ngang góc trên-trái của chữ số, giới hạn: 0~127
 * Tham số: Y tọa độ dọc góc trên-trái của chữ số, giới hạn: 0~63
 * Tham số: Number số cần hiển thị, giới hạn: -2147483648~2147483647
 * Tham số: Length độ dài chữ số, giới hạn: 0~10
 * Tham số: FontSize kích thước font
 *           Giới hạn: OLED_8X16 rộng 8 pixel, cao 16 pixel
 *                      OLED_6X8 rộng 6 pixel, cao 8 pixel
 * Giá trị trả về: không
 * Ghi chú: sau khi gọi hàm này, để thực sự hiển thị lên màn hình cần gọi hàm cập nhật
 */
void OLED_ShowSignedNum(uint8_t X, uint8_t Y, int32_t Number, uint8_t Length, uint8_t FontSize)
{
    uint8_t i;
    uint32_t Number1;

    if (Number >= 0) // Số lớn hơn hoặc bằng 0
    {
        OLED_ShowChar(X, Y, '+', FontSize); // Hiển thị dấu +
        Number1 = Number;                   // Number1 bằng Number
    } else                                  // Số nhỏ hơn 0
    {
        OLED_ShowChar(X, Y, '-', FontSize); // Hiển thị dấu -
        Number1 = -Number;                  // Number1 bằng Number đổi dấu
    }

    for (i = 0; i < Length; i++) // Duyệt từng chữ số
    {
        /*Gọi hàm OLED_ShowChar, hiển thị lần lượt từng chữ số*/
        /*Number1 / OLED_Pow(10, Length - i - 1) % 10 trích từng chữ số thập phân*/
        /*+ '0' chuyển số thành ký tự*/
        OLED_ShowChar(X + (i + 1) * FontSize, Y, Number1 / OLED_Pow(10, Length - i - 1) % 10 + '0', FontSize);
    }
}

/**
 * Hàm: OLED hiển thị số thập lục phân (hex, số nguyên dương)
 * Tham số: X tọa độ ngang góc trên-trái của chữ số, giới hạn: 0~127
 * Tham số: Y tọa độ dọc góc trên-trái của chữ số, giới hạn: 0~63
 * Tham số: Number số cần hiển thị, giới hạn: 0x00000000~0xFFFFFFFF
 * Tham số: Length độ dài chữ số, giới hạn: 0~8
 * Tham số: FontSize kích thước font
 *           Giới hạn: OLED_8X16 rộng 8 pixel, cao 16 pixel
 *                      OLED_6X8 rộng 6 pixel, cao 8 pixel
 * Giá trị trả về: không
 * Ghi chú: sau khi gọi hàm này, để thực sự hiển thị lên màn hình cần gọi hàm cập nhật
 */
void OLED_ShowHexNum(uint8_t X, uint8_t Y, uint32_t Number, uint8_t Length, uint8_t FontSize)
{
    uint8_t i, SingleNumber;
    for (i = 0; i < Length; i++) // Duyệt từng chữ số
    {
        /*Trích từng chữ số thập lục phân*/
        SingleNumber = Number / OLED_Pow(16, Length - i - 1) % 16;

        if (SingleNumber < 10) // Chữ số nhỏ hơn 10
        {
            /*Gọi hàm OLED_ShowChar, hiển thị chữ số*/
            /*+ '0' chuyển số thành ký tự*/
            OLED_ShowChar(X + i * FontSize, Y, SingleNumber + '0', FontSize);
        } else // Chữ số lớn hơn 10
        {
            /*Gọi hàm OLED_ShowChar, hiển thị chữ số*/
            /*+ 'A' chuyển số thành ký tự hex từ A*/
            OLED_ShowChar(X + i * FontSize, Y, SingleNumber - 10 + 'A', FontSize);
        }
    }
}

/**
 * Hàm: OLED hiển thị số nhị phân (binary, số nguyên dương)
 * Tham số: X tọa độ ngang góc trên-trái của chữ số, giới hạn: 0~127
 * Tham số: Y tọa độ dọc góc trên-trái của chữ số, giới hạn: 0~63
 * Tham số: Number số cần hiển thị, giới hạn: 0x00000000~0xFFFFFFFF
 * Tham số: Length độ dài chữ số, giới hạn: 0~16
 * Tham số: FontSize kích thước font
 *           Giới hạn: OLED_8X16 rộng 8 pixel, cao 16 pixel
 *                      OLED_6X8 rộng 6 pixel, cao 8 pixel
 * Giá trị trả về: không
 * Ghi chú: sau khi gọi hàm này, để thực sự hiển thị lên màn hình cần gọi hàm cập nhật
 */
void OLED_ShowBinNum(uint8_t X, uint8_t Y, uint32_t Number, uint8_t Length, uint8_t FontSize)
{
    uint8_t i;
    for (i = 0; i < Length; i++) // Duyệt từng chữ số
    {
        /*Gọi hàm OLED_ShowChar, hiển thị lần lượt từng chữ số*/
        /*Number / OLED_Pow(2, Length - i - 1) % 2 trích từng chữ số nhị phân*/
        /*+ '0' chuyển số thành ký tự*/
        OLED_ShowChar(X + i * FontSize, Y, Number / OLED_Pow(2, Length - i - 1) % 2 + '0', FontSize);
    }
}

/**
 * Hàm: OLED hiển thị số thực (thập phân, số thập phân)
 * Tham số: X tọa độ ngang góc trên-trái của chữ số, giới hạn: 0~127
 * Tham số: Y tọa độ dọc góc trên-trái của chữ số, giới hạn: 0~63
 * Tham số: Number số cần hiển thị, giới hạn: -4294967295.0~4294967295.0
 * Tham số: IntLength độ dài phần nguyên, giới hạn: 0~10
 * Tham số: FraLength độ dài phần thập phân, giới hạn: 0~9, làm tròn
 * Tham số: FontSize kích thước font
 *           Giới hạn: OLED_8X16 rộng 8 pixel, cao 16 pixel
 *                      OLED_6X8 rộng 6 pixel, cao 8 pixel
 * Giá trị trả về: không
 * Ghi chú: sau khi gọi hàm này, để thực sự hiển thị lên màn hình cần gọi hàm cập nhật
 */
void OLED_ShowFloatNum(uint8_t X, uint8_t Y, double Number, uint8_t IntLength, uint8_t FraLength, uint8_t FontSize)
{
    uint32_t PowNum, IntNum, FraNum;

    if (Number >= 0) // Số lớn hơn hoặc bằng 0
    {
        OLED_ShowChar(X, Y, '+', FontSize); // Hiển thị dấu +
    } else                                  // Số nhỏ hơn 0
    {
        OLED_ShowChar(X, Y, '-', FontSize); // Hiển thị dấu -
        Number = -Number;                   // Number đổi dấu
    }

    /*Trích phần nguyên và phần thập phân*/
    IntNum = Number;                  // Gán trực tiếp, lấy phần nguyên
    Number -= IntNum;                 // Trừ phần nguyên khỏi Number
    PowNum = OLED_Pow(10, FraLength); // Xác định hệ số nhân theo số chữ số thập phân
    FraNum = round(Number * PowNum);  // Nhân phần thập phân thành nguyên và làm tròn
    IntNum += FraNum / PowNum;        // Nếu làm tròn gây nhớ, cộng vào phần nguyên

    /*Hiển thị phần nguyên*/
    OLED_ShowNum(X + FontSize, Y, IntNum, IntLength, FontSize);

    /*Hiển thị dấu chấm thập phân*/
    OLED_ShowChar(X + (IntLength + 1) * FontSize, Y, '.', FontSize);

    /*Hiển thị phần thập phân*/
    OLED_ShowNum(X + (IntLength + 2) * FontSize, Y, FraNum, FraLength, FontSize);
}

/**
 * Hàm: OLED hiển thị chuỗi chữ Hán
 * Tham số: X tọa độ ngang góc trên-trái của chuỗi, giới hạn: 0~127
 * Tham số: Y tọa độ dọc góc trên-trái của chuỗi, giới hạn: 0~63
 * Tham số: Chinese chuỗi chữ Hán cần hiển thị, giới hạn: toàn bộ phải là chữ Hán hoặc ký tự toàn giác, không chứa ký tự nửa giác
 *           Chữ Hán hiển thị phải được định nghĩa trong mảng OLED_CF16x16 của OLED_Data.c
 *           Nếu không tìm thấy sẽ hiển thị hình mặc định (hình vuông có dấu ? bên trong)
 * Giá trị trả về: không
 * Ghi chú: sau khi gọi hàm này, để thực sự hiển thị lên màn hình cần gọi hàm cập nhật
 */
void OLED_ShowChinese(uint8_t X, uint8_t Y, char *Chinese)
{
    uint8_t pChinese = 0;
    uint8_t pIndex;
    uint8_t i;
    char SingleChinese[OLED_CHN_CHAR_WIDTH + 1] = {0}; // Mã UTF8 3 byte, +1 cho ký tự kết thúc \0

    for (i = 0; Chinese[i] != '\0'; i++) // Duyệt chuỗi chữ Hán
    {
        SingleChinese[pChinese] = Chinese[i]; // Sao chép dữ liệu vào mảng chữ Hán đơn
        pChinese++;                           // Tăng bộ đếm

        /*Khi đủ OLED_CHN_CHAR_WIDTH byte tức đã có một chữ Hán hoàn chỉnh*/
        if (pChinese >= OLED_CHN_CHAR_WIDTH) {
            SingleChinese[pChinese + 1] = '\0'; // Thêm chuỗi kết thúc sau chữ Hán
            pChinese                    = 0;    // Reset bộ đếm

            /*Duyệt toàn bộ thư viện bitmap chữ Hán để tìm khớp*/
            /*Nếu gặp chữ rỗng cuối cùng tức không tìm thấy, dừng tìm*/
            for (pIndex = 0; strcmp(OLED_CF16x16[pIndex].Index, "") != 0; pIndex++) {
                /*Tìm thấy chữ Hán khớp*/
                if (strcmp(OLED_CF16x16[pIndex].Index, SingleChinese) == 0) {
                    break; // Thoát vòng lặp, pIndex là chỉ số chữ Hán cần
                }
            }

            /*Hiển thị bitmap 16×16 của chữ Hán từ thư viện OLED_CF16x16*/
            OLED_ShowImage(X + ((i + 1) / OLED_CHN_CHAR_WIDTH - 1) * 16, Y, 16, 16, OLED_CF16x16[pIndex].Data);
        }
    }
}

/**
 * Hàm: OLED hiển thị hình ảnh
 * Tham số: X tọa độ ngang góc trên-trái của hình, giới hạn: 0~127
 * Tham số: Y tọa độ dọc góc trên-trái của hình, giới hạn: 0~63
 * Tham số: Width chiều rộng hình, giới hạn: 0~128
 * Tham số: Height chiều cao hình, giới hạn: 0~64
 * Tham số: Image con trỏ dữ liệu hình cần hiển thị
 * Giá trị trả về: không
 * Ghi chú: sau khi gọi hàm này, để thực sự hiển thị lên màn hình cần gọi hàm cập nhật
 */
void OLED_ShowImage(uint8_t X, uint8_t Y, uint8_t Width, uint8_t Height, const uint8_t *Image)
{
    uint8_t i, j;

    /*Kiểm tra tham số, đảm bảo hình không vượt khung màn hình*/
    if (X > 127) { return; }
    if (Y > 63) { return; }

    /*Xóa vùng chứa hình*/
    OLED_ClearArea(X, Y, Width, Height);

    /*Duyệt các trang liên quan đến hình*/
    /*(Height - 1) / 8 + 1 để chia Height cho 8 và làm tròn lên*/
    for (j = 0; j < (Height - 1) / 8 + 1; j++) {
        /*Duyệt các cột liên quan đến hình*/
        for (i = 0; i < Width; i++) {
            /*Vượt biên thì bỏ qua*/
            if (X + i > 127) { break; }
            if (Y / 8 + j > 7) { return; }

            /*Hiển thị phần hình ở trang hiện tại*/
            OLED_DisplayBuf[Y / 8 + j][X + i] |= Image[j * Width + i] << (Y % 8);

            /*Vượt biên thì bỏ qua*/
            /*Dùng continue để nếu trang sau vượt biên thì trang trước vẫn tiếp tục*/
            if (Y / 8 + j + 1 > 7) { continue; }

            /*Hiển thị phần hình ở trang kế tiếp*/
            OLED_DisplayBuf[Y / 8 + j + 1][X + i] |= Image[j * Width + i] >> (8 - Y % 8);
        }
    }
}/**
 * Hàm: OLED sử dụng hàm printf để in chuỗi định dạng
 * Tham số: X tọa độ ngang góc trên bên trái của chuỗi định dạng, phạm vi: 0~127
 * Tham số: Y tọa độ dọc góc trên bên trái của chuỗi định dạng, phạm vi: 0~63
 * Tham số: FontSize kích thước font chữ
 *           Phạm vi: OLED_8X16 rộng 8 pixel, cao 16 pixel
 *                 OLED_6X8 rộng 6 pixel, cao 8 pixel
 * Tham số: format chuỗi định dạng cần hiển thị, phạm vi: chuỗi ký tự ASCII có thể nhìn thấy
 * Tham số: ... danh sách tham số của chuỗi định dạng
 * Giá trị trả về: không có
 * Ghi chú: Sau khi gọi hàm này, để thực sự hiển thị trên màn hình, cần gọi hàm cập nhật
 */
void OLED_Printf(uint8_t X, uint8_t Y, uint8_t FontSize, char *format, ...)
{
    char String[30];                         // Khai báo mảng ký tự
    va_list arg;                             // Khai báo biến arg kiểu danh sách tham số biến đổi
    va_start(arg, format);                   // Bắt đầu từ format, nhận danh sách tham số vào biến arg
    vsprintf(String, format, arg);           // Sử dụng vsprintf in chuỗi định dạng và danh sách tham số vào mảng ký tự
    va_end(arg);                             // Kết thúc biến arg
    OLED_ShowString(X, Y, String, FontSize); // OLED hiển thị mảng ký tự (chuỗi)
}

/**
 * Hàm: OLED vẽ một điểm tại vị trí chỉ định
 * Tham số: X tọa độ ngang của điểm, phạm vi: 0~127
 * Tham số: Y tọa độ dọc của điểm, phạm vi: 0~63
 * Giá trị trả về: không có
 * Ghi chú: Sau khi gọi hàm này, để thực sự hiển thị trên màn hình, cần gọi hàm cập nhật
 */
void OLED_DrawPoint(uint8_t X, uint8_t Y)
{
    /*Kiểm tra tham số, đảm bảo vị trí chỉ định không vượt ra ngoài màn hình*/
    if (X > 127) { return; }
    if (Y > 63) { return; }

    /*Đặt 1 bit dữ liệu tại vị trí chỉ định của mảng bộ nhớ hiển thị thành 1*/
    OLED_DisplayBuf[Y / 8][X] |= 0x01 << (Y % 8);
}

/**
 * Hàm: OLED lấy giá trị điểm tại vị trí chỉ định
 * Tham số: X tọa độ ngang của điểm, phạm vi: 0~127
 * Tham số: Y tọa độ dọc của điểm, phạm vi: 0~63
 * Giá trị trả về: Trạng thái điểm tại vị trí chỉ định có đang sáng hay không, 1: sáng, 0: tắt
 */
uint8_t OLED_GetPoint(uint8_t X, uint8_t Y)
{
    /*Kiểm tra tham số, đảm bảo vị trí chỉ định không vượt ra ngoài màn hình*/
    if (X > 127) { return 0; }
    if (Y > 63) { return 0; }

    /*Kiểm tra dữ liệu tại vị trí chỉ định*/
    if (OLED_DisplayBuf[Y / 8][X] & 0x01 << (Y % 8)) {
        return 1; // Là 1, trả về 1
    }

    return 0; // Ngược lại, trả về 0
}

/**
 * Hàm: OLED vẽ đường thẳng
 * Tham số: X0 tọa độ ngang của một điểm đầu, phạm vi: 0~127
 * Tham số: Y0 tọa độ dọc của một điểm đầu, phạm vi: 0~63
 * Tham số: X1 tọa độ ngang của điểm đầu kia, phạm vi: 0~127
 * Tham số: Y1 tọa độ dọc của điểm đầu kia, phạm vi: 0~63
 * Giá trị trả về: không có
 * Ghi chú: Sau khi gọi hàm này, để thực sự hiển thị trên màn hình, cần gọi hàm cập nhật
 */
void OLED_DrawLine(uint8_t X0, uint8_t Y0, uint8_t X1, uint8_t Y1)
{
    int16_t x, y, dx, dy, d, incrE, incrNE, temp;
    int16_t x0 = X0, y0 = Y0, x1 = X1, y1 = Y1;
    uint8_t yflag = 0, xyflag = 0;

    if (y0 == y1) // Xử lý riêng đường ngang
    {
        /*Nếu tọa độ X của điểm 0 lớn hơn điểm 1 thì hoán đổi tọa độ X của hai điểm*/
        if (x0 > x1) {
            temp = x0;
            x0   = x1;
            x1   = temp;
        }

        /*Duyệt tọa độ X*/
        for (x = x0; x <= x1; x++) {
            OLED_DrawPoint(x, y0); // Lần lượt vẽ điểm
        }
    } else if (x0 == x1) // Xử lý riêng đường dọc
    {
        /*Nếu tọa độ Y của điểm 0 lớn hơn điểm 1 thì hoán đổi tọa độ Y của hai điểm*/
        if (y0 > y1) {
            temp = y0;
            y0   = y1;
            y1   = temp;
        }

        /*Duyệt tọa độ Y*/
        for (y = y0; y <= y1; y++) {
            OLED_DrawPoint(x0, y); // Lần lượt vẽ điểm
        }
    } else // Đường xiên
    {
        /*Sử dụng thuật toán Bresenham vẽ đường thẳng, có thể tránh phép toán dấu phẩy động tốn thời gian, hiệu quả hơn*/
        /*Tài liệu tham khảo: https://www.cs.montana.edu/courses/spring2009/425/dslectures/Bresenham.pdf*/
        /*Hướng dẫn tham khảo: https://www.bilibili.com/video/BV1364y1d7Lo*/

        if (x0 > x1) // Tọa độ X của điểm 0 lớn hơn điểm 1
        {
            /*Hoán đổi tọa độ hai điểm*/
            /*Sau khi hoán đổi không ảnh hưởng đến việc vẽ đường, nhưng hướng vẽ từ góc phần tư thứ nhất, hai, ba, bốn thành góc phần tư thứ nhất, bốn*/
            temp = x0;
            x0   = x1;
            x1   = temp;
            temp = y0;
            y0   = y1;
            y1   = temp;
        }

        if (y0 > y1) // Tọa độ Y của điểm 0 lớn hơn điểm 1
        {
            /*Lấy âm tọa độ Y*/
            /*Sau khi lấy âm ảnh hưởng đến việc vẽ đường, nhưng hướng vẽ từ góc phần tư thứ nhất, bốn thành góc phần tư thứ nhất*/
            y0 = -y0;
            y1 = -y1;

            /*Đặt cờ yflag, nhớ phép biến đổi hiện tại, khi vẽ đường thực tế sau này sẽ đổi lại tọa độ*/
            yflag = 1;
        }

        if (y1 - y0 > x1 - x0) // Độ dốc đường lớn hơn 1
        {
            /*Hoán đổi tọa độ X và Y*/
            /*Sau khi hoán đổi ảnh hưởng đến việc vẽ đường, nhưng hướng vẽ từ phạm vi 0~90 độ góc phần tư thứ nhất thành phạm vi 0~45 độ*/
            temp = x0;
            x0   = y0;
            y0   = temp;
            temp = x1;
            x1   = y1;
            y1   = temp;

            /*Đặt cờ xyflag, nhớ phép biến đổi hiện tại, khi vẽ đường thực tế sau này sẽ đổi lại tọa độ*/
            xyflag = 1;
        }

        /*Dưới đây là thuật toán Bresenham vẽ đường thẳng*/
        /*Thuật toán yêu cầu hướng vẽ đường phải trong phạm vi 0~45 độ góc phần tư thứ nhất*/
        dx     = x1 - x0;
        dy     = y1 - y0;
        incrE  = 2 * dy;
        incrNE = 2 * (dy - dx);
        d      = 2 * dy - dx;
        x      = x0;
        y      = y0;

        /*Vẽ điểm bắt đầu, đồng thời kiểm tra cờ, đổi lại tọa độ*/
        if (yflag && xyflag) {
            OLED_DrawPoint(y, -x);
        } else if (yflag) {
            OLED_DrawPoint(x, -y);
        } else if (xyflag) {
            OLED_DrawPoint(y, x);
        } else {
            OLED_DrawPoint(x, y);
        }

        while (x < x1) // Duyệt từng điểm trên trục X
        {
            x++;
            if (d < 0) // Điểm tiếp theo ở phía đông của điểm hiện tại
            {
                d += incrE;
            } else // Điểm tiếp theo ở phía đông bắc của điểm hiện tại
            {
                y++;
                d += incrNE;
            }

            /*Vẽ từng điểm, đồng thời kiểm tra cờ, đổi lại tọa độ*/
            if (yflag && xyflag) {
                OLED_DrawPoint(y, -x);
            } else if (yflag) {
                OLED_DrawPoint(x, -y);
            } else if (xyflag) {
                OLED_DrawPoint(y, x);
            } else {
                OLED_DrawPoint(x, y);
            }
        }
    }
}

/**
 * Hàm: OLED hình chữ nhật
 * Tham số: X tọa độ ngang góc trên bên trái hình chữ nhật, phạm vi: 0~127
 * Tham số: Y tọa độ dọc góc trên bên trái hình chữ nhật, phạm vi: 0~63
 * Tham số: Width chiều rộng hình chữ nhật, phạm vi: 0~128
 * Tham số: Height chiều cao hình chữ nhật, phạm vi: 0~64
 * Tham số: IsFilled xác định có tô đầy hình chữ nhật hay không
 *           Phạm vi: OLED_UNFILLED không tô
 *                 OLED_FILLED tô đầy
 * Giá trị trả về: không có
 * Ghi chú: Sau khi gọi hàm này, để thực sự hiển thị trên màn hình, cần gọi hàm cập nhật
 */
void OLED_DrawRectangle(uint8_t X, uint8_t Y, uint8_t Width, uint8_t Height, uint8_t IsFilled)
{
    uint8_t i, j;
    if (!IsFilled) // Xác định không tô đầy hình chữ nhật
    {
        /*Duyệt tọa độ X trên dưới, vẽ hai đường trên dưới của hình chữ nhật*/
        for (i = X; i < X + Width; i++) {
            OLED_DrawPoint(i, Y);
            OLED_DrawPoint(i, Y + Height - 1);
        }
        /*Duyệt tọa độ Y trái phải, vẽ hai đường trái phải của hình chữ nhật*/
        for (i = Y; i < Y + Height; i++) {
            OLED_DrawPoint(X, i);
            OLED_DrawPoint(X + Width - 1, i);
        }
    } else // Xác định tô đầy hình chữ nhật
    {
        /*Duyệt tọa độ X*/
        for (i = X; i < X + Width; i++) {
            /*Duyệt tọa độ Y*/
            for (j = Y; j < Y + Height; j++) {
                /*Vẽ điểm tại vùng chỉ định, tô đầy hình chữ nhật*/
                OLED_DrawPoint(i, j);
            }
        }
    }
}

/**
 * Hàm: OLED hình tam giác
 * Tham số: X0 tọa độ ngang điểm đầu thứ nhất, phạm vi: 0~127
 * Tham số: Y0 tọa độ dọc điểm đầu thứ nhất, phạm vi: 0~63
 * Tham số: X1 tọa độ ngang điểm đầu thứ hai, phạm vi: 0~127
 * Tham số: Y1 tọa độ dọc điểm đầu thứ hai, phạm vi: 0~63
 * Tham số: X2 tọa độ ngang điểm đầu thứ ba, phạm vi: 0~127
 * Tham số: Y2 tọa độ dọc điểm đầu thứ ba, phạm vi: 0~63
 * Tham số: IsFilled xác định có tô đầy tam giác hay không
 *           Phạm vi: OLED_UNFILLED không tô
 *                 OLED_FILLED tô đầy
 * Giá trị trả về: không có
 * Ghi chú: Sau khi gọi hàm này, để thực sự hiển thị trên màn hình, cần gọi hàm cập nhật
 */
void OLED_DrawTriangle(uint8_t X0, uint8_t Y0, uint8_t X1, uint8_t Y1, uint8_t X2, uint8_t Y2, uint8_t IsFilled)
{
    uint8_t minx = X0, miny = Y0, maxx = X0, maxy = Y0;
    uint8_t i, j;
    int16_t vx[] = {X0, X1, X2};
    int16_t vy[] = {Y0, Y1, Y2};

    if (!IsFilled) // Xác định không tô đầy tam giác
    {
        /*Gọi hàm vẽ đường, nối ba điểm bằng đường thẳng*/
        OLED_DrawLine(X0, Y0, X1, Y1);
        OLED_DrawLine(X0, Y0, X2, Y2);
        OLED_DrawLine(X1, Y1, X2, Y2);
    } else // Xác định tô đầy tam giác
    {
        /*Tìm tọa độ X, Y nhỏ nhất của ba điểm*/
        if (X1 < minx) { minx = X1; }
        if (X2 < minx) { minx = X2; }
        if (Y1 < miny) { miny = Y1; }
        if (Y2 < miny) { miny = Y2; }

        /*Tìm tọa độ X, Y lớn nhất của ba điểm*/
        if (X1 > maxx) { maxx = X1; }
        if (X2 > maxx) { maxx = X2; }
        if (Y1 > maxy) { maxy = Y1; }
        if (Y2 > maxy) { maxy = Y2; }

        /*Hình chữ nhật giữa tọa độ nhỏ nhất và lớn nhất là vùng có thể cần tô*/
        /*Duyệt tất cả các điểm trong vùng này*/
        /*Duyệt tọa độ X*/
        for (i = minx; i <= maxx; i++) {
            /*Duyệt tọa độ Y*/
            for (j = miny; j <= maxy; j++) {
                /*Gọi OLED_pnpoly, xác định điểm chỉ định có nằm trong tam giác chỉ định hay không*/
                /*Nếu có, thì vẽ điểm, nếu không, thì không xử lý*/
                if (OLED_pnpoly(3, vx, vy, i, j)) { OLED_DrawPoint(i, j); }
            }
        }
    }
}

/**
 * Hàm: OLED vẽ hình tròn
 * Tham số: X tọa độ ngang tâm hình tròn, phạm vi: 0~127
 * Tham số: Y tọa độ dọc tâm hình tròn, phạm vi: 0~63
 * Tham số: Radius bán kính hình tròn, phạm vi: 0~255
 * Tham số: IsFilled xác định có tô đầy hình tròn hay không
 *           Phạm vi: OLED_UNFILLED không tô
 *                 OLED_FILLED tô đầy
 * Giá trị trả về: không có
 * Ghi chú: Sau khi gọi hàm này, để thực sự hiển thị trên màn hình, cần gọi hàm cập nhật
 */
void OLED_DrawCircle(uint8_t X, uint8_t Y, uint8_t Radius, uint8_t IsFilled)
{
    int16_t x, y, d, j;

    /*Sử dụng thuật toán Bresenham vẽ hình tròn, có thể tránh phép toán dấu phẩy động tốn thời gian, hiệu quả hơn*/
    /*Tài liệu tham khảo: https://www.cs.montana.edu/courses/spring2009/425/dslectures/Bresenham.pdf*/
    /*Hướng dẫn tham khảo: https://www.bilibili.com/video/BV1VM4y1u7wJ*/

    d = 1 - Radius;
    x = 0;
    y = Radius;

    /*Vẽ điểm bắt đầu của mỗi cung tròn tám phần*/
    OLED_DrawPoint(X + x, Y + y);
    OLED_DrawPoint(X - x, Y - y);
    OLED_DrawPoint(X + y, Y + x);
    OLED_DrawPoint(X - y, Y - x);```c
if (IsFilled) // chỉ định tô màu hình tròn
    {
        /*duyệt tọa độ Y điểm bắt đầu*/
        for (j = -y; j < y; j++) {
            /*vẽ điểm trong vùng chỉ định, tô màu phần hình tròn*/
            OLED_DrawPoint(X, Y + j);
        }
    }

    while (x < y) // duyệt từng điểm trên trục X
    {
        x++;
        if (d < 0) // điểm tiếp theo ở phía đông điểm hiện tại
        {
            d += 2 * x + 1;
        } else // điểm tiếp theo ở phía đông nam điểm hiện tại
        {
            y--;
            d += 2 * (x - y) + 1;
        }

        /*vẽ điểm của mỗi cung tròn một phần tám*/
        OLED_DrawPoint(X + x, Y + y);
        OLED_DrawPoint(X + y, Y + x);
        OLED_DrawPoint(X - x, Y - y);
        OLED_DrawPoint(X - y, Y - x);
        OLED_DrawPoint(X + x, Y - y);
        OLED_DrawPoint(X + y, Y - x);
        OLED_DrawPoint(X - x, Y + y);
        OLED_DrawPoint(X - y, Y + x);

        if (IsFilled) // chỉ định tô màu hình tròn
        {
            /*duyệt phần giữa*/
            for (j = -y; j < y; j++) {
                /*vẽ điểm trong vùng chỉ định, tô màu phần hình tròn*/
                OLED_DrawPoint(X + x, Y + j);
                OLED_DrawPoint(X - x, Y + j);
            }

            /*duyệt phần hai bên*/
            for (j = -x; j < x; j++) {
                /*vẽ điểm trong vùng chỉ định, tô màu phần hình tròn*/
                OLED_DrawPoint(X - y, Y + j);
                OLED_DrawPoint(X + y, Y + j);
            }
        }
    }
}

/**
 * 函    数:OLED画椭圆
 * 参    数:X 指定椭圆的圆心横坐标,范围:0~127
 * 参    数:Y 指定椭圆的圆心纵坐标,范围:0~63
 * 参    数:A 指定椭圆的横向半轴长度,范围:0~255
 * 参    数:B 指定椭圆的纵向半轴长度,范围:0~255
 * 参    数:IsFilled 指定椭圆是否填充
 *           范围:OLED_UNFILLED\t\t不填充
 *                 OLED_FILLED\t\t\t填充
 * 返 回 值:无
 * 说    明:调用此函数后,要想真正地呈现在屏幕上,还需调用更新函数
 */
void OLED_DrawEllipse(uint8_t X, uint8_t Y, uint8_t A, uint8_t B, uint8_t IsFilled)
{
    int16_t x, y, j;
    int16_t a = A, b = B;
    float d1, d2;

    /*sử dụng thuật toán Bresenham vẽ ellipse, tránh một số phép toán dấu phẩy động tốn thời gian, hiệu quả hơn*/
    /*tham khảo liên kết:https://blog.csdn.net/myf_666/article/details/128167392*/

    x  = 0;
    y  = b;
    d1 = b * b + a * a * (-b + 0.5);

    if (IsFilled) // chỉ định tô màu ellipse
    {
        /*duyệt tọa độ Y điểm bắt đầu*/
        for (j = -y; j < y; j++) {
            /*vẽ điểm trong vùng chỉ định, tô màu phần ellipse*/
            OLED_DrawPoint(X, Y + j);
            OLED_DrawPoint(X, Y + j);
        }
    }

    /*vẽ điểm bắt đầu của ellipse*/
    OLED_DrawPoint(X + x, Y + y);
    OLED_DrawPoint(X - x, Y - y);
    OLED_DrawPoint(X - x, Y + y);
    OLED_DrawPoint(X + x, Y - y);

    /*vẽ phần giữa ellipse*/
    while (b * b * (x + 1) < a * a * (y - 0.5)) {
        if (d1 <= 0) // điểm tiếp theo ở phía đông điểm hiện tại
        {
            d1 += b * b * (2 * x + 3);
        } else // điểm tiếp theo ở phía đông nam điểm hiện tại
        {
            d1 += b * b * (2 * x + 3) + a * a * (-2 * y + 2);
            y--;
        }
        x++;

        if (IsFilled) // chỉ định tô màu ellipse
        {
            /*duyệt phần giữa*/
            for (j = -y; j < y; j++) {
                /*vẽ điểm trong vùng chỉ định, tô màu phần ellipse*/
                OLED_DrawPoint(X + x, Y + j);
                OLED_DrawPoint(X - x, Y + j);
            }
        }

        /*vẽ cung ellipse phần giữa*/
        OLED_DrawPoint(X + x, Y + y);
        OLED_DrawPoint(X - x, Y - y);
        OLED_DrawPoint(X - x, Y + y);
        OLED_DrawPoint(X + x, Y - y);
    }

    /*vẽ phần hai bên ellipse*/
    d2 = b * b * (x + 0.5) * (x + 0.5) + a * a * (y - 1) * (y - 1) - a * a * b * b;

    while (y > 0) {
        if (d2 <= 0) // điểm tiếp theo ở phía đông điểm hiện tại
        {
            d2 += b * b * (2 * x + 2) + a * a * (-2 * y + 3);
            x++;

        } else // điểm tiếp theo ở phía đông nam điểm hiện tại
        {
            d2 += a * a * (-2 * y + 3);
        }
        y--;

        if (IsFilled) // chỉ định tô màu ellipse
        {
            /*duyệt phần hai bên*/
            for (j = -y; j < y; j++) {
                /*vẽ điểm trong vùng chỉ định, tô màu phần ellipse*/
                OLED_DrawPoint(X + x, Y + j);
                OLED_DrawPoint(X - x, Y + j);
            }
        }

        /*vẽ cung ellipse phần hai bên*/
        OLED_DrawPoint(X + x, Y + y);
        OLED_DrawPoint(X - x, Y - y);
        OLED_DrawPoint(X - x, Y + y);
        OLED_DrawPoint(X + x, Y - y);
    }
}

/**
 * 函    数:OLED画圆弧
 * 参    数:X 指定圆弧的圆心横坐标,范围:0~127
 * 参    数:Y 指定圆弧的圆心纵坐标,范围:0~63
 * 参    数:Radius 指定圆弧的半径,范围:0~255
 * 参    数:StartAngle 指定圆弧的起始角度,范围:-180~180
 *           水平向右为0度,水平向左为180度或-180度,下方为正数,上方为负数,顺时针旋转
 * 参    数:EndAngle 指定圆弧的终止角度,范围:-180~180
 *           水平向右为0度,水平向左为180度或-180度,下方为正数,上方为负数,顺时针旋转
 * 参    数:IsFilled 指定圆弧是否填充,填充后为扇形
 *           范围:OLED_UNFILLED\t\t不填充
 *                 OLED_FILLED\t\t\t填充
 * 返 回 值:无
 * 说    明:调用此函数后,要想真正地呈现在屏幕上,还需调用更新函数
 */
void OLED_DrawArc(uint8_t X, uint8_t Y, uint8_t Radius, int16_t StartAngle, int16_t EndAngle, uint8_t IsFilled)
{
    int16_t x, y, d, j;

    /*hàm này mượn thuật toán Bresenham vẽ hình tròn*/

    d = 1 - Radius;
    x = 0;
    y = Radius;

    /*khi vẽ mỗi điểm tròn, kiểm tra điểm đó có nằm trong góc chỉ định, nếu có thì vẽ, không thì bỏ qua*/
    if (OLED_IsInAngle(x, y, StartAngle, EndAngle)) { OLED_DrawPoint(X + x, Y + y); }
    if (OLED_IsInAngle(-x, -y, StartAngle, EndAngle)) { OLED_DrawPoint(X - x, Y - y); }
    if (OLED_IsInAngle(y, x, StartAngle, EndAngle)) { OLED_DrawPoint(X + y, Y + x); }
    if (OLED_IsInAngle(-y, -x, StartAngle, EndAngle)) { OLED_DrawPoint(X - y, Y - x); }

    if (IsFilled) // chỉ định tô màu cung tròn
    {
        /*duyệt tọa độ Y điểm bắt đầu*/
        for (j = -y; j < y; j++) {
            /*khi tô màu mỗi điểm tròn, kiểm tra điểm đó có nằm trong góc chỉ định, nếu có thì vẽ, không thì bỏ qua*/
            if (OLED_IsInAngle(0, j, StartAngle, EndAngle)) { OLED_DrawPoint(X, Y + j); }
        }
    }

    while (x < y) // duyệt từng điểm trên trục X
    {
        x++;
        if (d < 0) // điểm tiếp theo ở phía đông điểm hiện tại
        {
            d += 2 * x + 1;
        } else // điểm tiếp theo ở phía đông nam điểm hiện tại
        {
            y--;
            d += 2 * (x - y) + 1;
        }

        /*khi vẽ mỗi điểm tròn, kiểm tra điểm đó có nằm trong góc chỉ định, nếu có thì vẽ, không thì bỏ qua*/
        if (OLED_IsInAngle(x, y, StartAngle, EndAngle)) { OLED_DrawPoint(X + x, Y + y); }
        if (OLED_IsInAngle(y, x, StartAngle, EndAngle)) { OLED_DrawPoint(X + y, Y + x); }
        if (OLED_IsInAngle(-x, -y, StartAngle, EndAngle)) { OLED_DrawPoint(X - x, Y - y); }
        if (OLED_IsInAngle(-y, -x, StartAngle, EndAngle)) { OLED_DrawPoint(X - y, Y - x); }
        if (OLED_IsInAngle(x, -y, StartAngle, EndAngle)) { OLED_DrawPoint(X + x, Y - y); }
        if (OLED_IsInAngle(y, -x, StartAngle, EndAngle)) { OLED_DrawPoint(X + y, Y - x); }
        if (OLED_IsInAngle(-x, y, StartAngle, EndAngle)) { OLED_DrawPoint(X - x, Y + y); }
        if (OLED_IsInAngle(-y, x, StartAngle, EndAngle)) { OLED_DrawPoint(X - y, Y + x); }

        if (IsFilled) // chỉ định tô màu cung tròn
        {
            /*duyệt phần giữa*/
            for (j = -y; j < y; j++) {
                /*khi tô màu mỗi điểm tròn, kiểm tra điểm đó có nằm trong góc chỉ định, nếu có thì vẽ, không thì bỏ qua*/
                if (OLED_IsInAngle(x, j, StartAngle, EndAngle)) { OLED_DrawPoint(X + x, Y + j); }
                if (OLED_IsInAngle(-x, j, StartAngle, EndAngle)) { OLED_DrawPoint(X - x, Y + j); }
            }

            /*duyệt phần hai bên*/
            for (j = -x; j < x; j++) {
                /*khi tô màu mỗi điểm tròn, kiểm tra điểm đó có nằm trong góc chỉ định, nếu có thì vẽ, không thì bỏ qua*/
                if (OLED_IsInAngle(-y, j, StartAngle, EndAngle)) { OLED_DrawPoint(X - y, Y + j); }
                if (OLED_IsInAngle(y, j, StartAngle, EndAngle)) { OLED_DrawPoint(X + y, Y + j); }
            }
        }
    }
}

/*********************Hàm chức năng*/

/*****************Jiangxie Technology|Bản quyền****************/
/*****************jiangxiekeji.com*****************/

Tập tin OLED.h:

#ifndef __OLED_H
#define __OLED_H

#include <stdint.h>
#include "OLED_Data.h"

/*Định nghĩa macro tham số*********************/\n\n/*Tham số FontSize*/\n/*Giá trị này không chỉ dùng để phán đoán mà còn để tính độ lệch ký tự theo chiều ngang, mặc định bằng chiều rộng pixel của font*/\n#define OLED_8X16\t\t\t\t8\n#define OLED_6X8\t\t\t\t6\n\n/*Giá trị tham số IsFilled*/\n#define OLED_UNFILLED\t\t\t0\n#define OLED_FILLED\t\t\t\t1\n\n/*********************Định nghĩa macro tham số*/\n\n\n/*Khai báo hàm*********************/\n\n/*Hàm khởi tạo*/\nvoid OLED_Init(void);\n\n/*Hàm cập nhật*/\nvoid OLED_Update(void);\nvoid OLED_UpdateArea(uint8_t X, uint8_t Y, uint8_t Width, uint8_t Height);\n\n/*Hàm điều khiển bộ nhớ hiển thị*/\nvoid OLED_Clear(void);\nvoid OLED_ClearArea(uint8_t X, uint8_t Y, uint8_t Width, uint8_t Height);\nvoid OLED_Reverse(void);\nvoid OLED_ReverseArea(uint8_t X, uint8_t Y, uint8_t Width, uint8_t Height);\n\n/*Hàm hiển thị*/\nvoid OLED_ShowChar(uint8_t X, uint8_t Y, char Char, uint8_t FontSize);\nvoid OLED_ShowString(uint8_t X, uint8_t Y, char *String, uint8_t FontSize);\nvoid OLED_ShowNum(uint8_t X, uint8_t Y, uint32_t Number, uint8_t Length, uint8_t FontSize);\nvoid OLED_ShowSignedNum(uint8_t X, uint8_t Y, int32_t Number, uint8_t Length, uint8_t FontSize);\nvoid OLED_ShowHexNum(uint8_t X, uint8_t Y, uint32_t Number, uint8_t Length, uint8_t FontSize);\nvoid OLED_ShowBinNum(uint8_t X, uint8_t Y, uint32_t Number, uint8_t Length, uint8_t FontSize);\nvoid OLED_ShowFloatNum(uint8_t X, uint8_t Y, double Number, uint8_t IntLength, uint8_t FraLength, uint8_t FontSize);\nvoid OLED_ShowChinese(uint8_t X, uint8_t Y, char *Chinese);\nvoid OLED_ShowImage(uint8_t X, uint8_t Y, uint8_t Width, uint8_t Height, const uint8_t *Image);\nvoid OLED_Printf(uint8_t X, uint8_t Y, uint8_t FontSize, char *format, ...);\n\n/*Hàm vẽ*/\nvoid OLED_DrawPoint(uint8_t X, uint8_t Y);\nuint8_t OLED_GetPoint(uint8_t X, uint8_t Y);\nvoid OLED_DrawLine(uint8_t X0, uint8_t Y0, uint8_t X1, uint8_t Y1);\nvoid OLED_DrawRectangle(uint8_t X, uint8_t Y, uint8_t Width, uint8_t Height, uint8_t IsFilled);\nvoid OLED_DrawTriangle(uint8_t X0, uint8_t Y0, uint8_t X1, uint8_t Y1, uint8_t X2, uint8_t Y2, uint8_t IsFilled);\nvoid OLED_DrawCircle(uint8_t X, uint8_t Y, uint8_t Radius, uint8_t IsFilled);\nvoid OLED_DrawEllipse(uint8_t X, uint8_t Y, uint8_t A, uint8_t B, uint8_t IsFilled);\nvoid OLED_DrawArc(uint8_t X, uint8_t Y, uint8_t Radius, int16_t StartAngle, int16_t EndAngle, uint8_t IsFilled);\n\n/*********************Khai báo hàm*/\n\n#endif\n\n\n/*****************Jiangxie Technology | Bản quyền thuộc về****************/\n/*****************jiangxiekeji.com*****************/\n\n```\n\n**Tập tin OLED_Data.c:**\n\n```c
#include "OLED_Data.h"

/**
  * Định dạng lưu trữ dữ liệu:
  * 8 điểm theo chiều dọc, bit cao ở dưới, trái sang phải trước, trên xuống dưới sau
  * Mỗi bit tương ứng một pixel
  * 
  *      B0 B0                  B0 B0
  *      B1 B1                  B1 B1
  *      B2 B2                  B2 B2
  *      B3 B3  ------------->  B3 B3 --
  *      B4 B4                  B4 B4  |
  *      B5 B5                  B5 B5  |
  *      B6 B6                  B6 B6  |
  *      B7 B7                  B7 B7  |
  *                                    |
  *  -----------------------------------
  *  |   
  *  |   B0 B0                  B0 B0
  *  |   B1 B1                  B1 B1
  *  |   B2 B2                  B2 B2
  *  --> B3 B3  ------------->  B3 B3
  *      B4 B4                  B4 B4
  *      B5 B5                  B5 B5
  *      B6 B6                  B6 B6
  *      B7 B7                  B7 B7
  * 
  */

/*Dữ liệu mẫu ký tự ASCII*********************//*Rộng 8 pixel, cao 16 pixel*/
const uint8_t OLED_F8x16[][16] =
{
	0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
	0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,//   0
	0x00,0x00,0x00,0xF8,0x00,0x00,0x00,0x00,
	0x00,0x00,0x00,0x33,0x30,0x00,0x00,0x00,// ! 1
	0x00,0x16,0x0E,0x00,0x16,0x0E,0x00,0x00,
	0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,// " 2
	0x40,0xC0,0x78,0x40,0xC0,0x78,0x40,0x00,
	0x04,0x3F,0x04,0x04,0x3F,0x04,0x04,0x00,// # 3
	0x00,0x70,0x88,0xFC,0x08,0x30,0x00,0x00,
	0x00,0x18,0x20,0xFF,0x21,0x1E,0x00,0x00,// $ 4
	0xF0,0x08,0xF0,0x00,0xE0,0x18,0x00,0x00,
	0x00,0x21,0x1C,0x03,0x1E,0x21,0x1E,0x00,// % 5
	0x00,0xF0,0x08,0x88,0x70,0x00,0x00,0x00,
	0x1E,0x21,0x23,0x24,0x19,0x27,0x21,0x10,// & 6
	0x00,0x00,0x00,0x16,0x0E,0x00,0x00,0x00,
	0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,// ' 7
	0x00,0x00,0x00,0xE0,0x18,0x04,0x02,0x00,
	0x00,0x00,0x00,0x07,0x18,0x20,0x40,0x00,// ( 8
	0x00,0x02,0x04,0x18,0xE0,0x00,0x00,0x00,
	0x00,0x40,0x20,0x18,0x07,0x00,0x00,0x00,// ) 9
	0x40,0x40,0x80,0xF0,0x80,0x40,0x40,0x00,
	0x02,0x02,0x01,0x0F,0x01,0x02,0x02,0x00,// * 10
	0x00,0x00,0x00,0xF0,0x00,0x00,0x00,0x00,
	0x01,0x01,0x01,0x1F,0x01,0x01,0x01,0x00,// + 11
	0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
	0x00,0xB0,0x70,0x00,0x00,0x00,0x00,0x00,// , 12
	0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
	0x00,0x01,0x01,0x01,0x01,0x01,0x01,0x01,// - 13
	0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
	0x00,0x30,0x30,0x00,0x00,0x00,0x00,0x00,// . 14
	0x00,0x00,0x00,0x00,0x80,0x60,0x18,0x04,
	0x00,0x60,0x18,0x06,0x01,0x00,0x00,0x00,// / 15
	0x00,0xE0,0x10,0x08,0x08,0x10,0xE0,0x00,
	0x00,0x0F,0x10,0x20,0x20,0x10,0x0F,0x00,// 0 16
	0x00,0x10,0x10,0xF8,0x00,0x00,0x00,0x00,
	0x00,0x20,0x20,0x3F,0x20,0x20,0x00,0x00,// 1 17
	0x00,0x70,0x08,0x08,0x08,0x88,0x70,0x00,
	0x00,0x30,0x28,0x24,0x22,0x21,0x30,0x00,// 2 18
	0x00,0x30,0x08,0x88,0x88,0x48,0x30,0x00,
	0x00,0x18,0x20,0x20,0x20,0x11,0x0E,0x00,// 3 19
	0x00,0x00,0xC0,0x20,0x10,0xF8,0x00,0x00,
	0x00,0x07,0x04,0x24,0x24,0x3F,0x24,0x00,// 4 20
	0x00,0xF8,0x08,0x88,0x88,0x08,0x08,0x00,
	0x00,0x19,0x21,0x20,0x20,0x11,0x0E,0x00,// 5 21
	0x00,0xE0,0x10,0x88,0x88,0x18,0x00,0x00,
	0x00,0x0F,0x11,0x20,0x20,0x11,0x0E,0x00,// 6 22
	0x00,0x38,0x08,0x08,0xC8,0x38,0x08,0x00,
	0x00,0x00,0x00,0x3F,0x00,0x00,0x00,0x00,// 7 23
	0x00,0x70,0x88,0x08,0x08,0x88,0x70,0x00,
	0x00,0x1C,0x22,0x21,0x21,0x22,0x1C,0x00,// 8 24
	0x00,0xE0,0x10,0x08,0x08,0x10,0xE0,0x00,
	0x00,0x00,0x31,0x22,0x22,0x11,0x0F,0x00,// 9 25
	0x00,0x00,0x00,0xC0,0xC0,0x00,0x00,0x00,
	0x00,0x00,0x00,0x30,0x30,0x00,0x00,0x00,// : 26
	0x00,0x00,0x00,0xC0,0xC0,0x00,0x00,0x00,
	0x00,0x00,0x80,0xB0,0x70,0x00,0x00,0x00,// ; 27
	0x00,0x00,0x80,0x40,0x20,0x10,0x08,0x00,
	0x00,0x01,0x02,0x04,0x08,0x10,0x20,0x00,// < 28
	0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x00,
	0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x00,// = 29
	0x00,0x08,0x10,0x20,0x40,0x80,0x00,0x00,
	0x00,0x20,0x10,0x08,0x04,0x02,0x01,0x00,// > 30
	0x00,0x70,0x48,0x08,0x08,0x08,0xF0,0x00,
	0x00,0x00,0x00,0x30,0x36,0x01,0x00,0x00,// ? 31
}? 31
	0xC0,0x30,0xC8,0x28,0xE8,0x10,0xE0,0x00,
	0x07,0x18,0x27,0x24,0x23,0x14,0x0B,0x00,// @ 32
	0x00,0x00,0xC0,0x38,0xE0,0x00,0x00,0x00,
	0x20,0x3C,0x23,0x02,0x02,0x27,0x38,0x20,// A 33
	0x08,0xF8,0x88,0x88,0x88,0x70,0x00,0x00,
	0x20,0x3F,0x20,0x20,0x20,0x11,0x0E,0x00,// B 34
	0xC0,0x30,0x08,0x08,0x08,0x08,0x38,0x00,
	0x07,0x18,0x20,0x20,0x20,0x10,0x08,0x00,// C 35
	0x08,0xF8,0x08,0x08,0x08,0x10,0xE0,0x00,
	0x20,0x3F,0x20,0x20,0x20,0x10,0x0F,0x00,// D 36
	0x08,0xF8,0x88,0x88,0xE8,0x08,0x10,0x00,
	0x20,0x3F,0x20,0x20,0x23,0x20,0x18,0x00,// E 37
	0x08,0xF8,0x88,0x88,0xE8,0x08,0x10,0x00,
	0x20,0x3F,0x20,0x00,0x03,0x00,0x00,0x00,// F 38
	0xC0,0x30,0x08,0x08,0x08,0x38,0x00,0x00,
	0x07,0x18,0x20,0x20,0x22,0x1E,0x02,0x00,// G 39
	0x08,0xF8,0x08,0x00,0x00,0x08,0xF8,0x08,
	0x20,0x3F,0x21,0x01,0x01,0x21,0x3F,0x20,// H 40
	0x00,0x08,0x08,0xF8,0x08,0x08,0x00,0x00,
	0x00,0x20,0x20,0x3F,0x20,0x20,0x00,0x00,// I 41
	0x00,0x00,0x08,0x08,0xF8,0x08,0x08,0x00,
	0xC0,0x80,0x80,0x80,0x7F,0x00,0x00,0x00,// J 42
	0x08,0xF8,0x88,0xC0,0x28,0x18,0x08,0x00,
	0x20,0x3F,0x20,0x01,0x26,0x38,0x20,0x00,// K 43
	0x08,0xF8,0x08,0x00,0x00,0x00,0x00,0x00,
	0x20,0x3F,0x20,0x20,0x20,0x20,0x30,0x00,// L 44
	0x08,0xF8,0xF8,0x00,0xF8,0xF8,0x08,0x00,
	0x20,0x3F,0x00,0x3F,0x00,0x3F,0x20,0x00,// M 45
	0x08,0xF8,0x30,0xC0,0x00,0x08,0xF8,0x08,
	0x20,0x3F,0x20,0x00,0x07,0x18,0x3F,0x00,// N 46
	0xE0,0x10,0x08,0x08,0x08,0x10,0xE0,0x00,
	0x0F,0x10,0x20,0x20,0x20,0x10,0x0F,0x00,// O 47
	0x08,0xF8,0x08,0x08,0x08,0x08,0xF0,0x00,
	0x20,0x3F,0x21,0x01,0x01,0x01,0x00,0x00,// P 48
	0xE0,0x10,0x08,0x08,0x08,0x10,0xE0,0x00,
	0x0F,0x18,0x24,0x24,0x38,0x50,0x4F,0x00,// Q 49
	0x08,0xF8,0x88,0x88,0x88,0x88,0x70,0x00,
	0x20,0x3F,0x20,0x00,0x03,0x0C,0x30,0x20,// R 50
	0x00,0x70,0x88,0x08,0x08,0x08,0x38,0x00,
	0x00,0x38,0x20,0x21,0x21,0x22,0x1C,0x00,// S 51
	0x18,0x08,0x08,0xF8,0x08,0x08,0x18,0x00,
	0x00,0x00,0x20,0x3F,0x20,0x00,0x00,0x00,// T 52
	0x08,0xF8,0x08,0x00,0x00,0x08,0xF8,0x08,
	0x00,0x1F,0x20,0x20,0x20,0x20,0x1F,0x00,// U 53
	0x08,0x78,0x88,0x00,0x00,0xC8,0x38,0x08,
	0x00,0x00,0x07,0x38,0x0E,0x01,0x00,0x00,// V 54
	0xF8,0x08,0x00,0xF8,0x00,0x08,0xF8,0x00,
	0x03,0x3C,0x07,0x00,0x07,0x3C,0x03,0x00,// W 55
	0x08,0x18,0x68,0x80,0x80,0x68,0x18,0x08,
	0x20,0x30,0x2C,0x03,0x03,0x2C,0x30,0x20,// X 56
	0x08,0x38,0xC8,0x00,0xC8,0x38,0x08,0x00,
	0x00,0x00,0x20,0x3F,0x20,0x00,0x00,0x00,// Y 57
	0x10,0x08,0x08,0x08,0xC8,0x38,0x08,0x00,
	0x20,0x38,0x26,0x21,0x20,0x20,0x18,0x00,// Z 58
	0x00,0x00,0x00,0xFE,0x02,0x02,0x02,0x00,
	0x00,0x00,0x00,0x7F,0x40,0x40,0x40,0x00,// [ 59
	0x00,0x0C,0x30,0xC0,0x00,0x00,0x00,0x00,
	0x00,0x00,0x00,0x01,0x06,0x38,0xC0,0x00,// \ 60
	0x00,0x02,0x02,0x02,0xFE,0x00,0x00,0x00,
	0x00,0x40,0x40,0x40,0x7F,0x00,0x00,0x00,// ] 61
	0x00,0x20,0x10,0x08,0x04,0x08,0x10,0x20,
	0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,// ^ 62
	0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
	0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,// _ 63
	0x00,0x02,0x04,0x08,0x00,0x00,0x00,0x00,
	0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,// ` 64
	0x00,0x00,0x80,0x80,0x80,0x80,0x00,0x00,
	0x00,0x19,0x24,0x22,0x22,0x22,0x3F,0x20,// a 65
	0x08,0xF8,0x00,0x80,0x80,0x00,0x00,0x00,
	0x00,0x3F,0x11,0x20,0x20,0x11,0x0E,0x00,// b 66
	0x00,0x00,0x00,0x80,0x80,0x80,0x00,0x00,
	0x00,0x0E,0x11,0x20,0x20,0x20,0x11,0x00,// c 67
	0x00,0x00,0x00,0x80,0x80,0x88,0xF8,0x00,
	0x00,0x0E,0x11,0x20,0x20,0x10,0x3F,0x20,// d 68
	0x00,0x00,0x80,0x80,0x80,0x80,0x00,0x00,
	0x00,0x1F,0x22,0x22,0x22,0x22,0x13,0x00,// e 69
	0x00,0x80,0x80,0xF0,0x88,0x88,0x88,0x18,
	0x00,0x20,0x20,0x3F,0x20,0x20,0x00,0x00,// f 70
	0x00,0x00,0x80,0x80,0x80,0x80,0x80,0x00,
	0x00,0x6B,0x94,0x94,0x94,0x93,0x60,0x00,// g 71
	0x08,0xF8,0x00,0x80,0x80,0x80,0x00,0x00,
	0x20,0x3F,0x21,0x00,0x00,0x20,0x3F,0x20,// h 72
	0x00,0x80,0x98,0x98,0x00,0x00,0x00,0x00,
	0x00,0x20,0x20,0x3F,0x20,0x20,0x00,0x00,// i 73
	0x00,0x00,0x00,0x80,0x98,0x98,0x00,0x00,
	0x00,0xC0,0x80,0x80,0x80,0x7F,0x00,0x00,// j 74
	0x08,0xF8,0x00,0x00,0x80,0x80,0x80,0x00,
	0x20,0x3F,0x24,0x02,0x2D,0x30,0x20,0x00,// k 75
	0x00,0x08,0x08,0xF8,0x00,0x00,0x00,0x00,
	0x00,0x20,0x20,0x3F,0x20,0x20,0x00,0x00,// l 76
	0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x00,
	0x20,0x3F,0x20,0x00,0x3F,0x20,0x00,0x3F,// m 77
	0x00,0x80,0x80,0x00,0x80,0x80,0x00,0x00,
	0x00,0x20,0x3F,0x21,0x00,0x20,0x3F,0x20,// n 78
	0x00,0x00,0x80,0x80,0x80,0x80,0x00,0x00,
	0x00,0x1F,0x20,0x20,0x20,0x20,0x1F,0x00,// o 79
	0x80,0x80,0x00,0x80,0x80,0x00,0x00,0x00,
	0x80,0xFF,0xA1,0x20,0x20,0x11,0x0E,0x00,// p 80
	0x00,0x00,0x00,0x80,0x80,0x80,0x80,0x00,
	0x00,0x0E,0x11,0x20,0x20,0xA0,0xFF,0x80,// q 81
	0x80,0x80,0x80,0x00,0x80,0x80,0x80,0x00,
	0x20,0x20,0x3F,0x21,0x20,0x00,0x01,0x00,// r 82
	0x00,0x00,0x80,0x80,0x80,0x80,0x80,0x00,
	0x00,0x33,0x24,0x24,0x24,0x24,0x19,0x00,// s 83
	0x00,0x80,0x80,0xE0,0x80,0x80,0x00,0x00,
	0x00,0x00,0x00,0x1F,0x20,0x20,0x00,0x00,// t 84
	0x80,0x80,0x00,0x00,0x00,0x80,0x80,0x00,
	0x00,0x1F,0x20,0x20,0x20,0x10,0x3F,0x20,// u 85
	0x80,0x80,0x80,0x00,0x00,0x80,0x80,0x80,
	0x00,0x01,0x0E,0x30,0x08,0x06,0x01,0x00,// v 86
	0x80,0x80,0x00,0x80,0x00,0x80,0x80,0x80,
	0x0F,0x30,0x0C,0x03,0x0C,0x30,0x0F,0x00,// w 87
	0x00,0x80,0x80,0x00,0x80,0x80,0x80,0x00,
	0x00,0x20,0x31,0x2E,0x0E,0x31,0x20,0x00,// x 88
	0x80,0x80,0x80,0x00,0x00,0x80,0x80,0x80,
	0x80,0x81,0x8E,0x70,0x18,0x06,0x01,0x00,// y 89
	0x00,0x80,0x80,0x80,0x80,0x80,0x80,0x00,
	0x00,0x21,0x30,0x2C,0x22,0x21,0x30,0x00,// z 90
	0x00,0x00,0x00,0x00,0x80,0x7C,0x02,0x02,
	0x00,0x00,0x00,0x00,0x00,0x3F,0x40,0x40,// { 91
	0x00,0x00,0x00,0x00,0xFF,0x00,0x00,0x00,
	0x00,0x00,0x00,0x00,0xFF,0x00,0x00,0x00,// | 92
	0x00,0x02,0x02,0x7C,0x80,0x00,0x00,0x00,
	0x00,0x40,0x40,0x3F,0x00,0x00,0x00,0x00,// } 93
	0x00,0x80,0x40,0x40,0x80,0x00,0x00,0x80,
	0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x00,// ~ 94
};/*Rộng 6 pixel, cao 8 pixel*/
const uint8_t OLED_F6x8[][6] = 
{
	0x00,0x00,0x00,0x00,0x00,0x00,//   0
	0x00,0x00,0x00,0x2F,0x00,0x00,// ! 1
	0x00,0x00,0x07,0x00,0x07,0x00,// " 2
	0x00,0x14,0x7F,0x14,0x7F,0x14,// # 3
	0x00,0x24,0x2A,0x7F,0x2A,0x12,// $ 4
	0x00,0x23,0x13,0x08,0x64,0x62,// % 5
	0x00,0x36,0x49,0x55,0x22,0x50,// & 6
	0x00,0x00,0x00,0x07,0x00,0x00,// ' 7
	0x00,0x00,0x1C,0x22,0x41,0x00,// ( 8
	0x00,0x00,0x41,0x22,0x1C,0x00,// ) 9
	0x00,0x14,0x08,0x3E,0x08,0x14,// * 10
	0x00,0x08,0x08,0x3E,0x08,0x08,// + 11
	0x00,0x00,0x00,0xA0,0x60,0x00,// , 12
	0x00,0x08,0x08,0x08,0x08,0x08,// - 13
	0x00,0x00,0x60,0x60,0x00,0x00,// . 14
	0x00,0x20,0x10,0x08,0x04,0x02,// / 15
	0x00,0x3E,0x51,0x49,0x45,0x3E,// 0 16
	0x00,0x00,0x42,0x7F,0x40,0x00,// 1 17
	0x00,0x42,0x61,0x51,0x49,0x46,// 2 18
	0x00,0x21,0x41,0x45,0x4B,0x31,// 3 19
	0x00,0x18,0x14,0x12,0x7F,0x10,// 4 20
	0x00,0x27,0x45,0x45,0x45,0x39,// 5 21
	0x00,0x3C,0x4A,0x49,0x49,0x30,// 6 22
	0x00,0x01,0x71,0x09,0x05,0x03,// 7 23
	0x00,0x36,0x49,0x49,0x49,0x36,// 8 24
	0x00,0x06,0x49,0x49,0x29,0x1E,// 9 25
	0x00,0x00,0x36,0x36,0x00,0x00,// : 26
	0x00,0x00,0x56,0x36,0x00,0x00,// ; 27
	0x00,0x08,0x14,0x22,0x41,0x00,// < 28
	0x00,0x14,0x14,0x14,0x14,0x14,// = 29
	0x00,0x00,0x41,0x22,0x14,0x08,// > 30
	0x00,0x02,0x01,0x51,0x09,0x06,// ? 31
	0x00,0x3E,0x49,0x55,0x59,0x2E,// @ 32
	0x00,0x7C,0x12,0x11,0x12,0x7C,// A 33
	0x00,0x7F,0x49,0x49,0x49,0x36,// B 34
	0x00,0x3E,0x41,0x41,0x41,0x22,// C 35
	0x00,0x7F,0x41,0x41,0x22,0x1C,// D 36
	0x00,0x7F,0x49,0x49,0x49,0x41,// E 37
	0x00,0x7F,0x09,0x09,0x09,0x01,// F 38
	0x00,0x3E,0x41,0x49,0x49,0x7A,// G 39
	0x00,0x7F,0x08,0x08,0x08,0x7F,// H 40
	0x00,0x00,0x41,0x7F,0x41,0x00,// I 41
	0x00,0x20,0x40,0x41,0x3F,0x01,// J 42
	0x00,0x7F,0x08,0x14,0x22,0x41,// K 43
	0x00,0x7F,0x40,0x40,0x40,0x40,// L 44
	0x00,0x7F,0x02,0x0C,0x02,0x7F,// M 45
	0x00,0x7F,0x04,0x08,0x10,0x7F,// N 46
	0x00,0x3E,0x41,0x41,0x41,0x3E,// O 47
	0x00,0x7F,0x09,0x09,0x09,0x06,// P 48
	0x00,0x3E,0x41,0x51,0x21,0x5E,// Q 49
	0x00,0x7F,0x09,0x19,0x29,0x46,// R 50
	0x00,0x46,0x49,0x49,0x49,0x31,// S 51
	0x00,0x01,0x01,0x7F,0x01,0x01,// T 52
	0x00,0x3F,0x40,0x40,0x40,0x3F,// U 53
	0x00,0x1F,0x20,0x40,0x20,0x1F,// V 54
	0x00,0x3F,0x40,0x38,0x40,0x3F,// W 55
	0x00,0x63,0x14,0x08,0x14,0x63,// X 56
	0x00,0x07,0x08,0x70,0x08,0x07,// Y 57
	0x00,0x61,0x51,0x49,0x45,0x43,// Z 58
	0x00,0x00,0x7F,0x41,0x41,0x00,// [ 59
	0x00,0x02,0x04,0x08,0x10,0x20,// \ 60
	0x00,0x00,0x41,0x41,0x7F,0x00,// ] 61
	0x00,0x04,0x02,0x01,0x02,0x04,// ^ 62
	0x00,0x40,0x40,0x40,0x40,0x40,// _ 63
	0x00,0x00,0x01,0x02,0x04,0x00,// ` 64
	0x00,0x20,0x54,0x54,0x54,0x78,// a 65
	0x00,0x7F,0x48,0x44,0x44,0x38,// b 66
	0x00,0x38,0x44,0x44,0x44,0x20,// c 67
	0x00,0x38,0x44,0x44,0x48,0x7F,// d 68
	0x00,0x38,0x54,0x54,0x54,0x18,// e 69
	0x00,0x08,0x7E,0x09,0x01,0x02,// f 70
	0x00,0x18,0xA4,0xA4,0xA4,0x7C,// g 71
	0x00,0x7F,0x08,0x04,0x04,0x78,// h 72
	0x00,0x00,0x44,0x7D,0x40,0x00,// i 73
	0x00,0x40,0x80,0x84,0x7D,0x00,// j 74
	0x00,0x7F,0x10,0x28,0x44,0x00,// k 75
	0x00,0x00,0x41,0x7F,0x40,0x00,// l 76
	0x00,0x7C,0x04,0x18,0x04,0x78,// m 77
	0x00,0x7C,0x08,0x04,0x04,0x78,// n 78
	0x00,0x38,0x44,0x44,0x44,0x38,// o 79
	0x00,0xFC,0x24,0x24,0x24,0x18,// p 80
	0x00,0x18,0x24,0x24,0x18,0xFC,// q 81
	0x00,0x7C,0x08,0x04,0x04,0x08,// r 82
	0x00,0x48,0x54,0x54,0x54,0x20,// s 83
	0x00,0x04,0x3F,0x44,0x40,0x20,// t 84
	0x00,0x3C,0x40,0x40,0x20,0x7C,// u 85
	0x00,0x1C,0x20,0x40,0x20,0x1C,// v 86
	0x00,0x3C,0x40,0x30,0x40,0x3C,// w 87
	0x00,0x44,0x28,0x10,0x28,0x44,// x 88
	0x00,0x1C,0xA0,0xA0,0xA0,0x7C,// y 89
	0x00,0x44,0x64,0x54,0x4C,0x44,// z 90
	0x00,0x00,0x08,0x7F,0x41,0x00,// { 91
	0x00,0x00,0x00,0x7F,0x00,0x00,// | 92
	0x00,0x00,0x41,0x7F,0x08,0x00,// } 93
	0x00,0x08,0x04,0x08,0x10,0x08,// ~ 94
};
/*********************Dữ liệu mẫu ASCII*/

/*Dữ liệu mẫu chữ Hán*********************/

/*Cùng một chữ Hán chỉ cần định nghĩa một lần, thứ tự chữ Hán không quan trọng*/
/*Phải toàn bộ là chữ Hán hoặc ký tự toàn phân, không được thêm ký tự nửa phân nào*/

/*Rộng 16 pixel, cao 16 pixel*/
const ChineseCell_t OLED_CF16x16[] = {
	",",
	0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
	0x00,0x00,0x58,0x38,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,

	"。",
	0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
	0x00,0x00,0x18,0x24,0x24,0x18,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,

	"你",
	0x00,0x80,0x60,0xF8,0x07,0x40,0x20,0x18,0x0F,0x08,0xC8,0x08,0x08,0x28,0x18,0x00,
	0x01,0x00,0x00,0xFF,0x00,0x10,0x0C,0x03,0x40,0x80,0x7F,0x00,0x01,0x06,0x18,0x00,

	"好",
	0x10,0x10,0xF0,0x1F,0x10,0xF0,0x00,0x80,0x82,0x82,0xE2,0x92,0x8A,0x86,0x80,0x00,
	0x40,0x22,0x15,0x08,0x16,0x61,0x00,0x00,0x40,0x80,0x7F,0x00,0x00,0x00,0x00,0x00,

	"世",
	0x20,0x20,0x20,0xFE,0x20,0x20,0xFF,0x20,0x20,0x20,0xFF,0x20,0x20,0x20,0x20,0x00,
	0x00,0x00,0x00,0x7F,0x40,0x40,0x47,0x44,0x44,0x44,0x47,0x40,0x40,0x40,0x00,0x00,

	"界",
	0x00,0x00,0x00,0xFE,0x92,0x92,0x92,0xFE,0x92,0x92,0x92,0xFE,0x00,0x00,0x00,0x00,
	0x08,0x08,0x04,0x84,0x62,0x1E,0x01,0x00,0x01,0xFE,0x02,0x04,0x04,0x08,0x08,0x00,
	/*Thêm dữ liệu chữ Hán mới theo định dạng trên tại vị trí này*/
	//...

	/*Hình mặc định hiển thị khi không tìm thấy chữ Hán chỉ định (một khung vuông, bên trong có dấu hỏi), hãy đảm bảo nó nằm cuối cùng của mảng*/
	"",		
	0xFF,0x01,0x01,0x01,0x31,0x09,0x09,0x09,0x09,0x89,0x71,0x01,0x01,0x01,0x01,0xFF,
	0xFF,0x80,0x80,0x80,0x80,0x80,0x80,0x96,0x81,0x80,0x80,0x80,0x80,0x80,0x80,0xFF,
};

/*********************Dữ liệu mẫu chữ Hán*/


/*Dữ liệu hình ảnh*********************/

/*Hình ảnh thử nghiệm (một khung vuông, bên trong có biểu tượng diode), rộng 16 pixel, cao 16 pixel*/
const uint8_t Diode[] = {
	0xFF,0x01,0x81,0x81,0x81,0xFD,0x89,0x91,0xA1,0xC1,0xFD,0x81,0x81,0x81,0x01,0xFF,
	0xFF,0x80,0x80,0x80,0x80,0x9F,0x88,0x84,0x82,0x81,0x9F,0x80,0x80,0x80,0x80,0xFF,
};

/*Thêm dữ liệu hình ảnh mới theo định dạng trên tại vị trí này*/
//...

/*********************Dữ liệu hình ảnh*/


/*****************Jiangxie Technology | Bản quyền thuộc về****************/
/*****************jiangxiekeji.com*****************/


**Tệp OLED_Data.h:**

```c
#ifndef __OLED_DATA_H
#define __OLED_DATA_H

#include <stdint.h>

/*Chiều rộng byte ký tự chữ Hán*/
#define OLED_CHN_CHAR_WIDTH			3		//UTF-8 định dạng cho 3, GB2312 định dạng cho 2

/*Ô cơ bản của mẫu chữ*/
typedef struct 
{
	char Index[OLED_CHN_CHAR_WIDTH + 1];	//Chỉ mục chữ Hán
	uint8_t Data[32];						//Dữ liệu mẫu chữ
} ChineseCell_t;

/*Khai báo dữ liệu mẫu ASCII*/
extern const uint8_t OLED_F8x16[][16];
extern const uint8_t OLED_F6x8[][6];

/*Khai báo dữ liệu mẫu chữ Hán*/
extern const ChineseCell_t OLED_CF16x16[];

/*Khai báo dữ liệu hình ảnh*/
extern const uint8_t Diode[];
/*Thêm khai báo dữ liệu hình ảnh mới theo định dạng trên tại vị trí này*/
//...

#endif


/*****************Jiangxie Technology | Bản quyền thuộc về****************/
/*****************jiangxiekeji.com*****************/

Đề xuất đọc