STM32G4-based 0.96-inch OLED display driver (HAL library), supports hardware/software I2C

STM32G474-based 0.96-inch OLED (SSD1306) Display Driver (4-pin I2C interface), supporting both hardware and software I2C, HAL-library version.

This driver is quite complete: it can display English text, integers, floats, Chinese characters, images, binary and hexadecimal numbers, and provides drawing primitives (dots, lines, rectangles, circles, ellipses, triangles). Multiple fonts are supported—essentially a mini graphics library.

The code is a secondary modification of Jiangxie Technology’s original, which targeted STM32F103 and supported only software I2C. I added hardware-I2C support while keeping the option to switch back to software I2C via a macro.

Test hardware: NUCLEO-G474RE board.

For OLED driving principles and a usage tutorial, see Jiangxie Technology’s video: https://url.zeruns.com/L7j6y

Electronics / MCU QQ group: 2169025065

Effect pictures

I2C Protocol Brief

I2C (Inter-Integrated Circuit) was developed by Philips. With few pins, simple hardware, and high expandability—no external transceivers like USART or CAN—it is now widely used for on-board IC communication.

I2C has only one data line: SDA (Serial Data Line). Data is sent bit by bit (serial) and communication is half-duplex.

Half-duplex: bidirectional but not simultaneous; direction must alternate. Only one data line is needed.

We divide I2C into physical layer (hardware) and protocol layer (software rules).

I2C Physical Layer

Typical I2C bus connection

  1. Multi-device bus: several devices share the same signal lines. Multiple masters and slaves are supported.
  2. Two wires only: SDA (data) and SCL (clock).
  3. Pull-up resistors connect the bus to VCC. When idle, devices output high-impedance, so the resistors pull the lines high.

MCU GPIOs must be configured as open-drain outputs; otherwise short-circuits may occur.

More STM32 I2C details: https://url.zeruns.com/JC0Ah
Jiangxie STM32 intro (video): https://www.bilibili.com/video/BV1th411z7sn?p=31

Usage Notes

Hardware I2C is default: I2C3, SCL = PA8, SDA = PC9.

Hardware I2C

STM32CubeMX: assign the desired I2C pins to SCL/SDA (example below for I2C3 SCL).

Enable the I2C peripheral, set speed mode to Fast Mode Plus, frequency to 1000 kHz, leave the rest default.

GPIO tab: the two pins will auto-configure as alternate-function open-drain. Set speed to Very High and User Labels to I2C3_SCL and I2C3_SDA (change in code if you use other names). Generate code.

In OLED.c, comment out #define OLED_USE_SW_I2C and uncomment #define OLED_USE_HW_I2C. Adjust I2C3_SCL / I2C3_SDA if you used different labels.

Software I2C

STM32CubeMX: choose two GPIOs, set User Labels to I2C3_SCL and I2C3_SDA, configure as open-drain, default high, pull-up, maximum speed. Generate code.

In OLED.c, comment out #define OLED_USE_HW_I2C and uncomment #define OLED_USE_SW_I2C. Adjust pin labels if necessary.

Required Parts

Program

Full project download links:

Baidu Netdisk: Link: https://url.zeruns.com/0CQJG Extraction code: 0169

123 Netdisk (no speed limit): https://www.123pan.com/s/2Y9Djv-O0cvH.html Extraction code: vvDt

Gitee open-source repo: https://gitee.com/zeruns/STM32-HAL-OLED-I2C

GitHub open-source repo: https://github.com/zeruns/STM32G4-OLED-SSD1306-I2C-HAL

Please give it a Star

The project was created with Keil5 and developed with VSCode + EIDE; both IDEs can open it.

All project files are UTF-8 encoded; if garbled text appears, set your editor’s encoding to UTF-8.

Main file OLED.c:


/***************************************************************************************
 * This program was created by Jiangxie Technology and is freely open-sourced
 * You may view, use, and modify it at will and apply it to your own projects
 * Copyright of the program belongs to Jiangxie Technology; no individual or organization may claim ownership
 *
 * Program name:            0.96-inch OLED display driver (4-pin I2C interface)
 * Creation date:           2023.10.24
 * Current version:         V1.1
 * Current release date:    2023.12.8
 *
 * Jiangxie Technology official site:      jiangxiekeji.com
 * Jiangxie Technology official Taobao:    jiangxiekeji.taobao.com
 * Program description & updates:          jiangxiekeji.com/tutorial/oled.html
 *
 * If you find bugs or typos, email us: feedback@jiangxiekeji.com
 * Before mailing, check the updates page for the latest version; if already fixed, no need to mail
 ***************************************************************************************
 */

/*
 * Modified by zeruns
 * Changes:        Ported from StdPeriph to HAL library, added hardware I2C support, toggle via macro
 * Date:           2024.3.16
 * Blog:           https://blog.zeruns.com
 * Bilibili:       https://space.bilibili.com/8320520
*/

#include "main.h"
#include "OLED.h"
#include <str
```#include <stdio.h>
#include <math.h>th.h>
#include <stdio.h>
#include <stdarg.h

// If Chinese characters are used, the compiler must be given the option --no-multibyte-chars (not needed for AC6)

/*
Choose OLED driver mode; hardware I2C is used by default.  
To use software I2C, comment out the hardware-I2C macro and uncomment the software-I2C line.  
Never leave both uncommented at the same time!  
In STM32CubeMX, set the “user label” of the SCL and SDA pins to I2C3_SCL and I2C3_SDA respectively.
*/
#define OLED_USE_HW_I2C    // hardware I2C
//#define OLED_USE_SW_I2C    // software I2C

/*Pin definitions—change the I2C pins here if necessary*/
#define OLED_SCL            I2C3_SCL_Pin // SCL
#define OLED_SDA            I2C3_SDA_Pin // SDA
#define OLED_SCL_GPIO_Port  I2C3_SCL_GPIO_Port
#define OLED_SDA_GPIO_Port  I2C3_SDA_GPIO_Port

/*STM32G474 on-chip hardware I2C3: PA8 -- SCL; PC9 -- SDA*/

#ifdef OLED_USE_HW_I2C
/*I2C handle—specifies which I2C peripheral the OLED uses*/
#define OLED_I2C            hi2c3
extern  I2C_HandleTypeDef   hi2c3;    // HAL library: designate the hardware I2C interface
#endif

/*OLED slave address*/
#define OLED_ADDRESS 0x3C << 1    // 0x3C is the 7-bit address; left-shifted once the R/W bit becomes 0x78

/*I2C timeout*/
#define OLED_I2C_TIMEOUT 10
/*Delay for software I2C; the value below is for 170 MHz core clock.
If your frequency differs, adjust it; for ≤100 MHz simply set to 0.*/
#define Delay_time 3

/**
 * Data layout:
 * 8 vertical pixels, MSB at bottom, left-to-right first, then top-to-bottom.
 * Each bit represents one 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
 *
 * Coordinate system:
 * Top-left corner is (0, 0).
 * X axis points right, range 0–127.
 * Y axis points down, range 0–63.
 *
 *       0             X-axis          127
 *      .------------------------------->
 *    0 |
 *      |
 *      |
 *      |
 * Y-axis
 *      |
 *      |
 *      |
 *   63 |
 *      v
 *
 */

/*Global variables*********************/
/**
 * OLED frame-buffer array.
 * All drawing functions only read/write this buffer.
 * Call OLED_Update() or OLED_UpdateArea() afterwards
 * to send the buffer to the OLED hardware.
 */
uint8_t OLED_DisplayBuf[8][128];
/*********************Global variables*/

#ifdef OLED_USE_SW_I2C
/**
 * @brief  Write a level to OLED_SCL
 * According to BitValue, set SCL high or low.
 * @param  BitValue  0 or 1
 */
void OLED_W_SCL(uint8_t BitValue)
{
    /*Set SCL high or low based on BitValue*/
    HAL_GPIO_WritePin(OLED_SCL_GPIO_Port, OLED_SCL, (GPIO_PinState)BitValue);
    /*If the MCU is too fast, add a small delay to stay within I²C max speed*/
    for (volatile uint16_t i = 0; i < Delay_time; i++){
        //for (uint16_t j = 0; j < 10; j++);
    }
}

/**
  * @brief  Write a level to OLED_SDA
  * @param  BitValue  0/1
  * @return None
  * @note   Called by upper layer when SDA needs to be driven.
  *         BitValue 0 → SDA low, 1 → SDA high
  */
void OLED_W_SDA(uint8_t BitValue)
{
    /*Set SDA high or low based on BitValue*/
    HAL_GPIO_WritePin(OLED_SDA_GPIO_Port, OLED_SDA, (GPIO_PinState)BitValue);
    /*Small delay to respect I²C timing*/
    for (volatile uint16_t i = 0; i < Delay_time; i++){
        //for (uint16_t j = 0; j < 10; j++);
    }
}
#endif

/**
 * @brief  OLED pin initialization
 * @param  None
 * @retval None
 * @note   Must be called before using the OLED.
 *         SCL and SDA must be configured as open-drain and released.
 */
void OLED_GPIO_Init(void)
{
    uint32_t i, j;

    /*Wait a bit for OLED power to stabilize*/
    for (i = 0; i < 1000; i++) {
        for (j = 0; j < 1000; j++)
            ;
    }
#ifdef OLED_USE_SW_I2C
    __HAL_RCC_GPIOC_CLK_ENABLE();      // Enable GPIOC clock
    __HAL_RCC_GPIOA_CLK_ENABLE();      // Enable GPIOA clock
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    GPIO_InitStruct.Mode  = GPIO_MODE_OUTPUT_OD;        // Open-drain output
    GPIO_InitStruct.Pull  = GPIO_PULLUP;                // Internal pull-up
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;  // High speed
    GPIO_InitStruct.Pin   = I2C3_SDA_Pin;
    HAL_GPIO_Init(I2C3_SDA_GPIO_Port, &GPIO_InitStruct);

    GPIO_InitStruct.Pin   = I2C3_SCL_Pin;
    HAL_GPIO_Init(I2C3_SCL_GPIO_Port, &GPIO_InitStruct);

    /*Release SCL and SDA*/
    OLED_W_SCL(1);
    OLED_W_SDA(1);
#endif
}

// https://blog.zeruns.com

/*Communication protocol*********************/

/**
 * @brief  I²C start condition
 * @param  None
 * @return None
 */
void OLED_I2C_Start(void)
{
#ifdef OLED_USE_SW_I2C
    OLED_W_SDA(1);    // Release SDA to ensure high
    OLED_W_SCL(1);    // Release SCL to ensure high
    OLED_W_SDA(0);    // Pull SDA low while SCL is high → START
    OLED_W_SCL(0);    // Pull SCL low to claim bus and prepare next edge
#endif
}

/**
 * @brief  I²C stop condition
 * @param  None
 * @return None
 */
void OLED_I2C_Stop(void)
{
#ifdef OLED_USE_SW_I2C
    OLED_W_SDA(0);    // Ensure SDA is low
    OLED_W_SCL(1);    // Release SCL (high)
    OLED_W_SDA(1);    // Release SDA while SCL is high → STOP
#endif
}

/**
 * @brief  Send one byte via I²C
 * @param  Byte  data to send, 0x00–0xFF
 * @return None
 */
void OLED_I2C_SendByte(uint8_t Byte)
{
#ifdef OLED_USE_SW_I2C
    uint8_t i;
    /*Shift out 8 bits, MSB first*/
    for (i = 0; i < 8; i++)
    {
        OLED_W_SDA(!!(Byte & (0x80 >> i)));
        OLED_W_SCL(1);  // Clock high, slave samples
        OLED_W_SCL(0);  // Clock low, prepare next bit
    }
    OLED_W_SCL(1);      // Extra clock pulse, ignore ACK
    OLED_W_SCL(0);
#endif
}

/**
 * @brief  Write an OLED command
 * @param  Command  command byte, 0x00–0xFF
 * @return None
 */
void OLED_WriteCommand(uint8_t Command)
{
#ifdef OLED_USE_SW_I2C
    OLED_I2C_Start();               // START
    OLED_I2C_SendByte(0x78);        // OLED I²C slave address
    OLED_I2C_SendByte(0x00);        // Control byte: 0x00 = command next
    OLED_I2C_SendByte(Command);     // Send command
    OLED_I2C_Stop();                // STOP
#elif defined(OLED_USE_HW_I2C)
    uint8_t TxData[2] = {0x00, Command};
    HAL_I2C_Master_Transmit(&OLED_I2C, OLED_ADDRESS, (uint8_t*)TxData, 2, OLED_I2C_TIMEOUT);
#endif
}

/**
 * @brief  Write OLED data
 * @param  Data   pointer to data buffer
 * @param  Count  number of bytes to send
 * @return None
 */
void OLED_WriteData(uint8_t *Data, uint8_t Count)
{
    uint8_t i;
#ifdef OLED_USE_SW_I2C
    OLED_I2C_Start();               // START
    OLED_I2C_SendByte(0x78);        // OLED I²C slave address
    OLED_I2C_SendByte(0x40);        // Control byte: 0x40 = data stream next
    for (i = 0; i < Count; i++) {
        OLED_I2C_SendByte(Data[i]); // Send data bytes
    }
    OLED_I2C_Stop();                // STOP
#elif defined(OLED_USE_HW_I2C)
    uint8_t TxData[Count + 1];
    TxData[0] = 0x40;               // Co = 0, D/C# = 1
    for (i = 0; i < Count; i++) {
        TxData[i + 1] = Data[i];
    }
    HAL_I2C_Master_Transmit(&OLED_I2C, OLED_ADDRESS, (uint8_t*)TxData, Count + 1, OLED_I2C_TIMEOUT);
#endif
}

/*********************Communication protocol*/

/*Hardware configuration*********************/

/**
 * @brief  OLED initialization
 * @param  None
 * @return None
 * @note   Must be called before using the OLED.
 */
void OLED_Init(void)
{
    OLED_GPIO_Init(); // Low-level pin init first

    /*Send initialization sequence*/
    OLED_WriteCommand(0xAE); // Display OFF

    OLED_WriteCommand(0xD5); // Set display clock divide ratio / oscillator frequency
    OLED_WriteCommand(0x80);

    OLED_WriteCommand(0xA8); // Set multiplex ratio
    OLED_WriteCommand(0x3F);

    OLED_WriteCommand(0xD3); // Set display offset
    OLED_WriteCommand(0x00);

    OLED_WriteCommand(0x40); // Set display start line

    OLED_WriteCommand(0xA1); // Segment remap: normal

    OLED_WriteCommand(0xC8); // COM output scan direction: normal

    OLED_WriteCommand(0xDA); // Set COM pins hardware configuration
    OLED_WriteCommand(0x12);

    OLED_WriteCommand(0x81); // Set contrast control
    OLED_WriteCommand(0xCF);

    OLED_WriteCommand(0xD9); // Set pre-charge period
    OLED_WriteCommand(0xF1);

    OLED_WriteCommand(0xDB); // Set VCOMH deselect level
    OLED_WriteCommand(0x30);

    OLED_WriteCommand(0xA4); // Entire display ON (follows RAM)

    OLED_WriteCommand(0xA6); // Normal (not inverted) display

    OLED_WriteCommand(0x8D); // Enable charge pump regulator
    OLED_WriteCommand(0x14);

    OLED_WriteCommand(0xAF); // Display ON

    OLED_Clear();  // Clear frame buffer
    OLED_Update(); // Push to screen to avoid random pixels
}

/**
 * @brief  Set OLED cursor position
 * @param  Page  page address, 0–7
 * @param  X     column address, 0–127
 * @return None
 * @note   OLED Y address is grouped in 8-pixel pages.
 */
void OLED_SetCursor(uint8_t Page, uint8_t X)
{
    /*Uncomment the line below when using 1.3" OLED (SH1106) which has 132 columns
    and the screen starts at column 2*/
    // X += 2;

    /*Set page and column addresses*/
    OLED_WriteCommand(0xB0 | Page);                    // Page address
    OLED_WriteCommand(0x10 | ((X & 0xF0) >> 4));       // Column high nibble
    OLED_WriteCommand(0x00 | (X & 0x0F));              // Column low nibble
}/*********************Hardware Configuration*/

/*Utility Functions*********************/

/*Utility functions are for internal use only*/

/**
 * @brief Power function
 * @param X Base
 * @param Y Exponent
 * @return Equals X to the power of Y
 */
uint32_t OLED_Pow(uint32_t X, uint32_t Y)
{
    uint32_t Result = 1; // Default result is 1
    while (Y--)          // Multiply Y times
    {
        Result *= X; // Multiply X to result each time
    }
    return Result;
}

/**
 * @brief Determine if a specified point is inside a specified polygon
 * @param nvert Number of polygon vertices
 * @param vertx verty Arrays containing x and y coordinates of polygon vertices
 * @param testx testy X and y coordinates of the test point
 * @return Whether the specified point is inside the specified polygon, 1: inside, 0: outside
 */
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;

    /*This algorithm was proposed by W. Randolph Franklin*/
    /*Reference link: 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;
}

/**
 * @brief Determine if a specified point is within a specified angle
 * @param X Y Coordinates of the specified point
 * @param StartAngle EndAngle Start and end angles, range: -180-180
 *           0 degrees is horizontal right, 180 or -180 is horizontal left, positive below, negative above, clockwise rotation
 * @return Whether the specified point is within the specified angle, 1: inside, 0: outside
 */
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; // Calculate radians of specified point and convert to degrees
    if (StartAngle < EndAngle)             // Case where start angle is less than end angle
    {
        /*If specified angle is between start and end angles, determine point is within angle*/
        if (PointAngle >= StartAngle && PointAngle <= EndAngle) {
            return 1;
        }
    } else // Case where start angle is greater than end angle
    {
        /*If specified angle is greater than start angle or less than end angle, determine point is within angle*/
        if (PointAngle >= StartAngle || PointAngle <= EndAngle) {
            return 1;
        }
    }
    return 0; // If none of above conditions are met, determine point is not within angle
}

/*********************Utility Functions*/

/*Function Functions*********************/

/**
 * @brief Update OLED display buffer array to OLED screen
 * @param None
 * @return None
 * @note All display functions only read/write to OLED display buffer array
 *           Then call OLED_Update function or OLED_UpdateArea function
 *           to send buffer array data to OLED hardware for display
 *           So after calling display functions, must call update function to actually display on screen
 */
void OLED_Update(void)
{
    uint8_t j;
    /*Iterate through each page*/
    for (j = 0; j < 8; j++) {
        /*Set cursor position to first column of each page*/
        OLED_SetCursor(j, 0);
        /*Continuously write 128 data points, write buffer array data to OLED hardware*/
        OLED_WriteData(OLED_DisplayBuf[j], 128);
    }
}

/**
 * @brief Partially update OLED display buffer array to OLED screen
 * @param X X-coordinate of top-left corner of specified area, range: 0-127
 * @param Y Y-coordinate of top-left corner of specified area, range: 0-63
 * @param Width Width of specified area, range: 0-128
 * @param Height Height of specified area, range: 0-64
 * @return None
 * @note This function updates at least the area specified by parameters
 *           If update area only partially includes pages on Y-axis, remaining parts of same page will be updated together
 * @note All display functions only read/write to OLED display buffer array
 *           Then call OLED_Update function or OLED_UpdateArea function
 *           to send buffer array data to OLED hardware for display
 *           So after calling display functions, must call update function to actually display on screen
 */
void OLED_UpdateArea(uint8_t X, uint8_t Y, uint8_t Width, uint8_t Height)
{
    uint8_t j;

    /*Parameter check to ensure specified area doesn't exceed screen bounds*/
    if (X > 127) { return; }
    if (Y > 63) { return; }
    if (X + Width > 128) { Width = 128 - X; }
    if (Y + Height > 64) { Height = 64 - Y; }

    /*Iterate through relevant pages involved in specified area*/
    /*(Y + Height - 1) / 8 + 1 aims to (Y + Height) / 8 and round up*/
    for (j = Y / 8; j < (Y + Height - 1) / 8 + 1; j++) {
        /*Set cursor position to specified column of relevant page*/
        OLED_SetCursor(j, X);
        /*Continuously write Width data points, write buffer array data to OLED hardware*/
        OLED_WriteData(&OLED_DisplayBuf[j][X], Width);
    }
}

/**
 * @brief Clear entire OLED display buffer array
 * @param None
 * @return None
 * @note After calling this function, must call update function to actually display on screen
 */
void OLED_Clear(void)
{
    uint8_t i, j;
    for (j = 0; j < 8; j++) // Iterate through 8 pages
    {
        for (i = 0; i < 128; i++) // Iterate through 128 columns
        {
            OLED_DisplayBuf[j][i] = 0x00; // Clear all buffer array data to zero
        }
    }
}

/**
 * @brief Partially clear OLED display buffer array
 * @param X X-coordinate of top-left corner of specified area, range: 0-127
 * @param Y Y-coordinate of top-left corner of specified area, range: 0-63
 * @param Width Width of specified area, range: 0-128
 * @param Height Height of specified area, range: 0-64
 * @return None
 * @note After calling this function, must call update function to actually display on screen
 */
void OLED_ClearArea(uint8_t X, uint8_t Y, uint8_t Width, uint8_t Height)
{
    uint8_t i, j;

    /*Parameter check to ensure specified area doesn't exceed screen bounds*/
    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++) // Iterate through specified pages
    {
        for (i = X; i < X + Width; i++) // Iterate through specified columns
        {
            OLED_DisplayBuf[j / 8][i] &= ~(0x01 << (j % 8)); // Clear specified buffer array data to zero
        }
    }
}

/**
 * @brief Invert entire OLED display buffer array
 * @param None
 * @return None
 * @note After calling this function, must call update function to actually display on screen
 */
void OLED_Reverse(void)
{
    uint8_t i, j;
    for (j = 0; j < 8; j++) // Iterate through 8 pages
    {
        for (i = 0; i < 128; i++) // Iterate through 128 columns
        {
            OLED_DisplayBuf[j][i] ^= 0xFF; // Invert all buffer array data
        }
    }
}

/**
 * @brief Partially invert OLED display buffer array
 * @param X X-coordinate of top-left corner of specified area, range: 0-127
 * @param Y Y-coordinate of top-left corner of specified area, range: 0-63
 * @param Width Width of specified area, range: 0-128
 * @param Height Height of specified area, range: 0-64
 * @return None
 * @note After calling this function, must call update function to actually display on screen
 */
void OLED_ReverseArea(uint8_t X, uint8_t Y, uint8_t Width, uint8_t Height)
{
    uint8_t i, j;

    /*Parameter check to ensure specified area doesn't exceed screen bounds*/
    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++) // Iterate through specified pages
    {
        for (i = X; i < X + Width; i++) // Iterate through specified columns
        {
            OLED_DisplayBuf[j / 8][i] ^= 0x01 << (j % 8); // Invert specified buffer array data
        }
    }
}

/**
 * @brief OLED display a character
 * @param X X-coordinate of top-left corner of character, range: 0-127
 * @param Y Y-coordinate of top-left corner of character, range: 0-63
 * @param Char Specified character to display, range: ASCII visible characters
 * @param FontSize Specified font size
 *           Range: OLED_8X16        8 pixels wide, 16 pixels high
 *                 OLED_6X8         6 pixels wide, 8 pixels high
 * @return None
 * @note After calling this function, must call update function to actually display on screen
 */
void OLED_ShowChar(uint8_t X, uint8_t Y, char Char, uint8_t FontSize)
{
    if (FontSize == OLED_8X16) // Font is 8 pixels wide, 16 pixels high
    {
        /*Display specified data from ASCII font library OLED_F8x16 in 8*16 image format*/
        OLED_ShowImage(X, Y, 8, 16, OLED_F8x16[Char - ' ']);
    } else if (FontSize == OLED_6X8) // Font is 6 pixels wide, 8 pixels high
    {
        /*Display specified data from ASCII font library OLED_F6x8 in 6*8 image format*/
        OLED_ShowImage(X, Y, 6, 8, OLED_F6x8[Char - ' ']);
    }
}

/**
 * @brief OLED display string
 * @param X X-coordinate of top-left corner of string, range: 0-127
 * @param Y Y-coordinate of top-left corner of string, range: 0-63
 * @param String Specified string to display, range: String composed of ASCII visible characters
 * @param FontSize Specified font size
 *           Range: OLED_8X16        8 pixels wide, 16 pixels high
 *                 OLED_6X8         6 pixels wide, 8 pixels high
 * @return None
 * @note After calling this function, must call update function to actually display on screen
 */
void OLED_ShowString(uint8_t X, uint8_t Y, char *String, uint8_t FontSize)
{
    uint8_t i;
    for (i = 0; String[i] != '\0'; i++) // Iterate through each character of string
    {
        /*Call OLED_ShowChar function to display each character in sequence*/
        OLED_ShowChar(X + i * FontSize, Y, String[i], FontSize);
    }
}

/**
 * @brief OLED display number (decimal, positive integer)
 * @param X X-coordinate of top-left corner of number, range: 0-127
 * @param Y Y-coordinate of top-left corner of number, range: 0-63
 * @param Number Specified number to display, range: 0-4294967295
 * @param Length Specified length of number, range: 0-10
 * @param FontSize Specified font size
 *           Range: OLED_8X16        8 pixels wide, 16 pixels high
 *                 OLED_6X8         6 pixels wide, 8 pixels high
 * @return None
 * @note After calling this function, must call update function to actually display on screen
 */
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++) // Iterate through each digit of number
    {
        /*Call OLED_ShowChar function to display each digit in sequence*/
        /*Number / OLED_Pow(10, Length - i - 1) % 10 can extract each decimal digit*/
        /*+ '0' converts digit to character format*/
        OLED_ShowChar(X + i * FontSize, Y, Number / OLED_Pow(10, Length - i - 1) % 10 + '0', FontSize);
    }
}

/**
 * @brief OLED display signed number (decimal, integer)
 * @param X X-coordinate of top-left corner of number, range: 0-127
 * @param Y Y-coordinate of top-left corner of number, range: 0-63
 * @param Number Specified number to display, range: -2147483648-2147483647
 * @param Length Specified length of number, range: 0-10
 * @param FontSize Specified font size
 *           Range: OLED_8X16        8 pixels wide, 16 pixels high
 *                 OLED_6X8         6 pixels wide, 8 pixels high
 * @return None
 * @note After calling this function, must call update function to actually display on screen
 */
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) // Number is greater than or equal to 0
    {
        OLED_ShowChar(X, Y, '+', FontSize); // Display + sign
        Number1 = Number;                   // Number1 directly equals Number
    } else                                  // Number is less than 0
    {
        OLED_ShowChar(X, Y, '-', FontSize); // Display - sign
        Number1 = -Number;                  // Number1 equals negative of Number
    }

for (i = 0; i < Length; i++) // Iterate through each digit of the number
    {
        /* Call OLED_ShowChar to display each digit in turn */
        /* Number1 / OLED_Pow(10, Length - i - 1) % 10 extracts each decimal digit */
        /* + '0' converts the digit to character format */
        OLED_ShowChar(X + (i + 1) * FontSize, Y, Number1 / OLED_Pow(10, Length - i - 1) % 10 + '0', FontSize);
    }
}

/**
 * @brief OLED display hexadecimal number (hexadecimal, positive integer)
 * @param X Specifies the x-coordinate of the top-left corner of the number, range: 0~127
 * @param Y Specifies the y-coordinate of the top-left corner of the number, range: 0~63
 * @param Number Specifies the number to display, range: 0x00000000~0xFFFFFFFF
 * @param Length Specifies the length of the number, range: 0~8
 * @param FontSize Specifies the font size
 *           Range: OLED_8X16        8 pixels wide, 16 pixels high
 *                 OLED_6X8        6 pixels wide, 8 pixels high
 * @return None
 * @note After calling this function, you must call the update function to actually display it on the screen
 */
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++) // Iterate through each digit of the number
    {
        /* Extract each hexadecimal digit */
        SingleNumber = Number / OLED_Pow(16, Length - i - 1) % 16;

        if (SingleNumber < 10) // Single digit less than 10
        {
            /* Call OLED_ShowChar to display this digit */
            /* + '0' converts the digit to character format */
            OLED_ShowChar(X + i * FontSize, Y, SingleNumber + '0', FontSize);
        }
        else // Single digit greater than or equal to 10
        {
            /* Call OLED_ShowChar to display this digit */
            /* + 'A' converts the digit to a hexadecimal character starting from A */
            OLED_ShowChar(X + i * FontSize, Y, SingleNumber - 10 + 'A', FontSize);
        }
    }
}

/**
 * @brief OLED display binary number (binary, positive integer)
 * @param X Specifies the x-coordinate of the top-left corner of the number, range: 0~127
 * @param Y Specifies the y-coordinate of the top-left corner of the number, range: 0~63
 * @param Number Specifies the number to display, range: 0x00000000~0xFFFFFFFF
 * @param Length Specifies the length of the number, range: 0~16
 * @param FontSize Specifies the font size
 *           Range: OLED_8X16        8 pixels wide, 16 pixels high
 *                 OLED_6X8        6 pixels wide, 8 pixels high
 * @return None
 * @note After calling this function, you must call the update function to actually display it on the screen
 */
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++) // Iterate through each digit of the number
    {
        /* Call OLED_ShowChar to display each digit in turn */
        /* Number / OLED_Pow(2, Length - i - 1) % 2 extracts each binary digit */
        /* + '0' converts the digit to character format */
        OLED_ShowChar(X + i * FontSize, Y, Number / OLED_Pow(2, Length - i - 1) % 2 + '0', FontSize);
    }
}

/**
 * @brief OLED display floating-point number (decimal, fractional)
 * @param X Specifies the x-coordinate of the top-left corner of the number, range: 0-127
 * @param Y Specifies the y-coordinate of the top-left corner of the number, range: 0-63
 * @param Number Specifies the number to display, range: -4294967295.0-4294967295.0
 * @param IntLength Specifies the length of the integer part, range: 0-10
 * @param FraLength Specifies the length of the fractional part, range: 0-9, rounded for display
 * @param FontSize Specifies the font size
 *           Range: OLED_8X16        8 pixels wide, 16 pixels high
 *                  OLED_6X8        6 pixels wide, 8 pixels high
 * @return None
 * @note After calling this function, you must call the update function to actually display it on the screen
 */
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) // Number is greater than or equal to 0
    {
        OLED_ShowChar(X, Y, '+', FontSize); // Display + sign
    }
    else                                  // Number is less than 0
    {
        OLED_ShowChar(X, Y, '-', FontSize); // Display - sign
        Number = -Number;                   // Negate Number
    }

    /* Extract integer and fractional parts */
    IntNum = Number;                  // Directly assign to integer variable to extract integer
    Number -= IntNum;                 // Subtract the integer part from Number to prevent errors when multiplying the fraction to integer later
    PowNum = OLED_Pow(10, FraLength); // Determine multiplier based on specified fractional digits
    FraNum = round(Number * PowNum);  // Multiply fraction to integer and round to avoid display errors
    IntNum += FraNum / PowNum;        // If rounding caused carry-over, add it to the integer

    /* Display integer part */
    OLED_ShowNum(X + FontSize, Y, IntNum, IntLength, FontSize);

    /* Display decimal point */
    OLED_ShowChar(X + (IntLength + 1) * FontSize, Y, '.', FontSize);

    /* Display fractional part */
    OLED_ShowNum(X + (IntLength + 2) * FontSize, Y, FraNum, FraLength, FontSize);
}

/**
 * @brief OLED display Chinese character string
 * @param X Specifies the x-coordinate of the top-left corner of the string, range: 0-127
 * @param Y Specifies the y-coordinate of the top-left corner of the string, range: 0-63
 * @param Chinese Specifies the Chinese character string to display, range: must be all Chinese or full-width characters, do not include any half-width characters
 *           Displayed characters must be defined in the OLED_CF16x16 array in OLED_Data.c
 *           If the specified character is not found, a default graphic (a square with a question mark inside) will be displayed
 * @return None
 * @note After calling this function, you must call the update function to actually display it on the screen
 */
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}; // UTF8 encoding is 3 bytes, +1 for \0 terminator

    for (i = 0; Chinese[i] != '\0'; i++) // Iterate through the Chinese string
    {
        SingleChinese[pChinese] = Chinese[i]; // Extract Chinese string data to single character array
        pChinese++;                           // Increment counter

        /* When extraction count reaches OLED_CHN_CHAR_WIDTH, a complete Chinese character is extracted */
        if (pChinese >= OLED_CHN_CHAR_WIDTH) {
            SingleChinese[pChinese + 1] = '\0'; // Append null terminator after the character
            pChinese                    = 0;    // Reset counter

            /* Traverse the entire Chinese character font library to find a match */
            /* If the last character (defined as empty string) is reached, it means the character is not defined in the font library, stop searching */
            for (pIndex = 0; strcmp(OLED_CF16x16[pIndex].Index, "") != 0; pIndex++) {
                /* Found matching character */
                if (strcmp(OLED_CF16x16[pIndex].Index, SingleChinese) == 0) {
                    break; // Exit loop, pIndex now holds the index of the specified character
                }
            }

            /* Display the specified data from the OLED_CF16x16 font library in 16*16 image format */
            OLED_ShowImage(X + ((i + 1) / OLED_CHN_CHAR_WIDTH - 1) * 16, Y, 16, 16, OLED_CF16x16[pIndex].Data);
        }
    }
}

/**
 * @brief OLED display image
 * @param X Specifies the x-coordinate of the top-left corner of the image, range: 0-127
 * @param Y Specifies the y-coordinate of the top-left corner of the image, range: 0-63
 * @param Width Specifies the width of the image, range: 0-128
 * @param Height Specifies the height of the image, range: 0-64
 * @param Image Specifies the image to display
 * @return None
 * @note After calling this function, you must call the update function to actually display it on the screen
 */
void OLED_ShowImage(uint8_t X, uint8_t Y, uint8_t Width, uint8_t Height, const uint8_t *Image)
{
    uint8_t i, j;

    /* Parameter check to ensure the specified image does not exceed screen bounds */
    if (X > 127) { return; }
    if (Y > 63) { return; }

    /* Clear the area where the image will be displayed */
    OLED_ClearArea(X, Y, Width, Height);

    /* Iterate through relevant pages involved in the specified image */
    /* (Height - 1) / 8 + 1 is equivalent to Height / 8 rounded up */
    for (j = 0; j < (Height - 1) / 8 + 1; j++) {
        /* Iterate through relevant columns involved in the specified image */
        for (i = 0; i < Width; i++) {
            /* Skip display if out of bounds */
            if (X + i > 127) { break; }
            if (Y / 8 + j > 7) { return; }

            /* Display image content on current page */
            OLED_DisplayBuf[Y / 8 + j][X + i] |= Image[j * Width + i] << (Y % 8);

            /* Skip display if out of bounds */
            /* Use continue so that remaining content on previous page continues to display if next page is out of bounds */
            if (Y / 8 + j + 1 > 7) { continue; }

            /* Display image content on next page */
            OLED_DisplayBuf[Y / 8 + j + 1][X + i] |= Image[j * Width + i] >> (8 - Y % 8);
        }
    }
}

/**
 * @brief OLED print formatted string using printf
 * @param X Specifies the x-coordinate of the top-left corner of the string, range: 0-127
 * @param Y Specifies the y-coordinate of the top-left corner of the string, range: 0-63
 * @param FontSize Specifies the font size
 *           Range: OLED_8X16        8 pixels wide, 16 pixels high
 *                 OLED_6X8        6 pixels wide, 8 pixels high
 * @param format Specifies the formatted string to display, range: string composed of ASCII visible characters
 * @param ... Format string argument list
 * @return None
 * @note After calling this function, you must call the update function to actually display it on the screen
 */
void OLED_Printf(uint8_t X, uint8_t Y, uint8_t FontSize, char *format, ...)
{
    char String[30];                         // Define character array
    va_list arg;                             // Define variable argument list variable arg
    va_start(arg, format);                   // Receive argument list starting from format into arg
    vsprintf(String, format, arg);           // Use vsprintf to print formatted string and argument list into character array
    va_end(arg);                             // End variable arg
    OLED_ShowString(X, Y, String, FontSize); // OLED display character array (string)
}

/**
 * @brief OLED draw a point at specified position
 * @param X Specifies the x-coordinate of the point, range: 0-127
 * @param Y Specifies the y-coordinate of the point, range: 0-63
 * @return None
 * @note After calling this function, you must call the update function to actually display it on the screen
 */
void OLED_DrawPoint(uint8_t X, uint8_t Y)
{
    /* Parameter check to ensure the specified position does not exceed screen bounds */
    if (X > 127) { return; }
    if (Y > 63) { return; }

    /* Set one bit in the display buffer at the specified position to 1 */
    OLED_DisplayBuf[Y / 8][X] |= 0x01 << (Y % 8);
}

/**
 * @brief OLED get the value of a point at specified position
 * @param X Specifies the x-coordinate of the point, range: 0-127
 * @param Y Specifies the y-coordinate of the point, range: 0-63
 * @return Whether the point at the specified position is lit, 1: lit, 0: off
 */
uint8_t OLED_GetPoint(uint8_t X, uint8_t Y)
{
    /* Parameter check to ensure the specified position does not exceed screen bounds */
    if (X > 127) { return 0; }
    if (Y > 63) { return 0; }

    /* Check data at specified position */
    if (OLED_DisplayBuf[Y / 8][X] & 0x01 << (Y % 8)) {
        return 1; // If 1, return 1
    }

    return 0; // Otherwise, return 0
}

/**
 * @brief OLED draw line
 * @param X0 Specifies the x-coordinate of one endpoint, range: 0-127
 * @param Y0 Specifies the y-coordinate of one endpoint, range: 0-63
 * @param X1 Specifies the x-coordinate of the other endpoint, range: 0-127
 * @param Y1 Specifies the y-coordinate of the other endpoint, range: 0-63
 * @return None
 * @note After calling this function, you must call the update function to actually display it on the screen
 */
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) // Handle horizontal line separately
    {
        /*If point 0's X coordinate is greater than point 1's X coordinate, swap the two points' X coordinates*/
        if (x0 > x1) {
            temp = x0;
            x0   = x1;
            x1   = temp;
        }

        /*Traverse X coordinates*/
        for (x = x0; x <= x1; x++) {
            OLED_DrawPoint(x, y0); // Draw points in sequence
        }
    } else if (x0 == x1) // Handle vertical line separately
    {
        /*If point 0's Y coordinate is greater than point 1's Y coordinate, swap the two points' Y coordinates*/
        if (y0 > y1) {
            temp = y0;
            y0   = y1;
            y1   = temp;
        }

        /*Traverse Y coordinates*/
        for (y = y0; y <= y1; y++) {
            OLED_DrawPoint(x0, y); // Draw points in sequence
        }
    } else // Slanted line
    {
        /*Use Bresenham's algorithm to draw lines, avoiding time-consuming floating-point operations for higher efficiency*/
        /*Reference: https://www.cs.montana.edu/courses/spring2009/425/dslectures/Bresenham.pdf*/
        /*Tutorial: https://www.bilibili.com/video/BV1364y1d7Lo*/

        if (x0 > x1) // Point 0's X coordinate is greater than point 1's X coordinate
        {
            /*Swap the two points' coordinates*/
            /*After swapping, it doesn't affect line drawing, but the drawing direction changes from the first, second, third, and fourth quadrants to the first and fourth quadrants*/
            temp = x0;
            x0   = x1;
            x1   = temp;
            temp = y0;
            y0   = y1;
            y1   = temp;
        }

        if (y0 > y1) // Point 0's Y coordinate is greater than point 1's Y coordinate
        {
            /*Negate the Y coordinates*/
            /*After negation, it affects line drawing, but the drawing direction changes from the first and fourth quadrants to the first quadrant*/
            y0 = -y0;
            y1 = -y1;

            /*Set flag yflag to remember this transformation; restore coordinates during actual drawing*/
            yflag = 1;
        }

        if (y1 - y0 > x1 - x0) // Line slope greater than 1
        {
            /*Swap X and Y coordinates*/
            /*After swapping, it affects line drawing, but the drawing direction changes from the 0–90 degree range in the first quadrant to the 0–45 degree range in the first quadrant*/
            temp = x0;
            x0   = y0;
            y0   = temp;
            temp = x1;
            x1   = y1;
            y1   = temp;

            /*Set flag xyflag to remember this transformation; restore coordinates during actual drawing*/
            xyflag = 1;
        }

        /*Bresenham's algorithm to draw lines below*/
        /*Algorithm requires line drawing direction to be in the 0–45 degree range of the first quadrant*/
        dx     = x1 - x0;
        dy     = y1 - y0;
        incrE  = 2 * dy;
        incrNE = 2 * (dy - dx);
        d      = 2 * dy - dx;
        x      = x0;
        y      = y0;

        /*Draw starting point, and check flags to restore coordinates*/
        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) // Traverse every point on the X axis
        {
            x++;
            if (d < 0) // Next point is to the east of the current point
            {
                d += incrE;
            } else // Next point is to the northeast of the current point
            {
                y++;
                d += incrNE;
            }

            /*Draw each point, and check flags to restore coordinates*/
            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);
            }
        }
    }
}

/**
 * @brief OLED rectangle
 * @param X Specifies the top-left corner's X coordinate, range: 0~127
 * @param Y Specifies the top-left corner's Y coordinate, range: 0~63
 * @param Width Specifies the rectangle's width, range: 0~128
 * @param Height Specifies the rectangle's height, range: 0~64
 * @param IsFilled Specifies whether the rectangle is filled
 *           Range: OLED_UNFILLED        not filled
 *                  OLED_FILLED          filled
 * @return None
 * @note After calling this function, to actually display on the screen, you must call the update function
 */
void OLED_DrawRectangle(uint8_t X, uint8_t Y, uint8_t Width, uint8_t Height, uint8_t IsFilled)
{
    uint8_t i, j;
    if (!IsFilled) // Rectangle not filled
    {
        /*Traverse top and bottom X coordinates, draw the top and bottom lines of the rectangle*/
        for (i = X; i < X + Width; i++) {
            OLED_DrawPoint(i, Y);
            OLED_DrawPoint(i, Y + Height - 1);
        }
        /*Traverse left and right Y coordinates, draw the left and right lines of the rectangle*/
        for (i = Y; i < Y + Height; i++) {
            OLED_DrawPoint(X, i);
            OLED_DrawPoint(X + Width - 1, i);
        }
    } else // Rectangle filled
    {
        /*Traverse X coordinates*/
        for (i = X; i < X + Width; i++) {
            /*Traverse Y coordinates*/
            for (j = Y; j < Y + Height; j++) {
                /*Draw points in the specified area to fill the rectangle*/
                OLED_DrawPoint(i, j);
            }
        }
    }
}

/**
 * @brief OLED triangle
 * @param X0 Specifies the first vertex's X coordinate, range: 0-127
 * @param Y0 Specifies the first vertex's Y coordinate, range: 0-63
 * @param X1 Specifies the second vertex's X coordinate, range: 0-127
 * @param Y1 Specifies the second vertex's Y coordinate, range: 0-63
 * @param X2 Specifies the third vertex's X coordinate, range: 0-127
 * @param Y2 Specifies the third vertex's Y coordinate, range: 0-63
 * @param IsFilled Specifies whether the triangle is filled
 *           Range: OLED_UNFILLED        not filled
 *                  OLED_FILLED          filled
 * @return None
 * @note After calling this function, to actually display on the screen, you must call the update function
 */
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) // Triangle not filled
    {
        /*Call line drawing function to connect the three points with straight lines*/
        OLED_DrawLine(X0, Y0, X1, Y1);
        OLED_DrawLine(X0, Y0, X2, Y2);
        OLED_DrawLine(X1, Y1, X2, Y2);
    } else // Triangle filled
    {
        /*Find the minimum X and Y coordinates among the three points*/
        if (X1 < minx) { minx = X1; }
        if (X2 < minx) { minx = X2; }
        if (Y1 < miny) { miny = Y1; }
        if (Y2 < miny) { miny = Y2; }

        /*Find the maximum X and Y coordinates among the three points*/
        if (X1 > maxx) { maxx = X1; }
        if (X2 > maxx) { maxx = X2; }
        if (Y1 > maxy) { maxy = Y1; }
        if (Y2 > maxy) { maxy = Y2; }

        /*The rectangle between the min and max coordinates is the area that may need to be filled*/
        /*Traverse all points in this area*/
        /*Traverse X coordinates*/
        for (i = minx; i <= maxx; i++) {
            /*Traverse Y coordinates*/
            for (j = miny; j <= maxy; j++) {
                /*Call OLED_pnpoly to determine whether the specified point is inside the specified triangle*/
                /*If inside, draw the point; if not, do nothing*/
                if (OLED_pnpoly(3, vx, vy, i, j)) { OLED_DrawPoint(i, j); }
            }
        }
    }
}

/**
 * @brief OLED draw circle
 * @param X Specifies the circle's center X coordinate, range: 0~127
 * @param Y Specifies the circle's center Y coordinate, range: 0~63
 * @param Radius Specifies the circle's radius, range: 0~255
 * @param IsFilled Specifies whether the circle is filled
 *           Range: OLED_UNFILLED        not filled
 *                  OLED_FILLED          filled
 * @return None
 * @note After calling this function, to actually display on the screen, you must call the update function
 */
void OLED_DrawCircle(uint8_t X, uint8_t Y, uint8_t Radius, uint8_t IsFilled)
{
    int16_t x, y, d, j;

    /*Use Bresenham's algorithm to draw circles, avoiding time-consuming floating-point operations for higher efficiency*/
    /*Reference: https://www.cs.montana.edu/courses/spring2009/425/dslectures/Bresenham.pdf*/
    /*Tutorial: https://www.bilibili.com/video/BV1VM4y1u7wJ*/

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

    /*Draw the starting points of each eighth of the circle*/
    OLED_DrawPoint(X + x, Y + y);
    OLED_DrawPoint(X - x, Y - y);
    OLED_DrawPoint(X + y, Y + x);
    OLED_DrawPoint(X - y, Y - x);

    if (IsFilled) // Circle filled
    {
        /*Traverse the starting point's Y coordinate*/
        for (j = -y; j < y; j++) {
            /*Draw points in the specified area to fill part of the circle*/
            OLED_DrawPoint(X, Y + j);
        }
    }

    while (x < y) // Traverse every point on the X axis
    {
        x++;
        if (d < 0) // Next point is to the east of the current point
        {
            d += 2 * x + 1;
        } else // Next point is to the southeast of the current point
        {
            y--;
            d += 2 * (x - y) + 1;
        }

        /*Draw points for each eighth of the circle*/
        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) // Circle filled
        {
            /*Traverse the middle part*/
            for (j = -y; j < y; j++) {
                /*Draw points in the specified area to fill part of the circle*/
                OLED_DrawPoint(X + x, Y + j);
                OLED_DrawPoint(X - x, Y + j);
            }

            /*Traverse the side parts*/
            for (j = -x; j < x; j++) {
                /*Draw points in the specified area to fill part of the circle*/
                OLED_DrawPoint(X - y, Y + j);
                OLED_DrawPoint(X + y, Y + j);
            }
        }
    }
}

/**
 * @brief OLED draw ellipse
 * @param X Specifies the ellipse's center X coordinate, range: 0~127
 * @param Y Specifies the ellipse's center Y coordinate, range: 0~63
 * @param A Specifies the ellipse's horizontal semi-axis length, range: 0~255
 * @param B Specifies the ellipse's vertical semi-axis length, range: 0~255
 * @param IsFilled Specifies whether the ellipse is filled
 *           Range: OLED_UNFILLED        not filled
 *                  OLED_FILLED          filled
 * @return None
 * @note After calling this function, to actually display on the screen, you must call the update function
 */
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;

    /*Use Bresenham's algorithm to draw ellipses, avoiding some time-consuming floating-point operations for higher efficiency*/
    /*Reference: https://blog.csdn.net/myf_666/article/details/128167392*/

    x  = 0;
    y  = b;
    d1 = b * b + a * a * (-b + 0.5);if (IsFilled) // Specify ellipse fill
    {
        /*Traverse starting Y coordinate*/
        for (j = -y; j < y; j++) {
            /*Draw points in specified area, filling partial ellipse*/
            OLED_DrawPoint(X, Y + j);
            OLED_DrawPoint(X, Y + j);
        }
    }

    /*Draw starting points of ellipse arc*/
    OLED_DrawPoint(X + x, Y + y);
    OLED_DrawPoint(X - x, Y - y);
    OLED_DrawPoint(X - x, Y + y);
    OLED_DrawPoint(X + x, Y - y);

    /*Draw middle part of ellipse*/
    while (b * b * (x + 1) < a * a * (y - 0.5)) {
        if (d1 <= 0) // Next point is to the east
        {
            d1 += b * b * (2 * x + 3);
        } else // Next point is to the southeast
        {
            d1 += b * b * (2 * x + 3) + a * a * (-2 * y + 2);
            y--;
        }
        x++;

        if (IsFilled) // Specify ellipse fill
        {
            /*Traverse middle part*/
            for (j = -y; j < y; j++) {
                /*Draw points in specified area, filling partial ellipse*/
                OLED_DrawPoint(X + x, Y + j);
                OLED_DrawPoint(X - x, Y + j);
            }
        }

        /*Draw middle arc of 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);
    }

    /*Draw sides of 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) // Next point is to the east
        {
            d2 += b * b * (2 * x + 2) + a * a * (-2 * y + 3);
            x++;

        } else // Next point is to the southeast
        {
            d2 += a * a * (-2 * y + 3);
        }
        y--;

        if (IsFilled) // Specify ellipse fill
        {
            /*Traverse sides*/
            for (j = -y; j < y; j++) {
                /*Draw points in specified area, filling partial ellipse*/
                OLED_DrawPoint(X + x, Y + j);
                OLED_DrawPoint(X - x, Y + j);
            }
        }

        /*Draw side arcs of 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);
    }
}

/**
 * @brief Draw OLED arc
 * @param X Specifies arc center X coordinate, range: 0~127
 * @param Y Specifies arc center Y coordinate, range: 0~63
 * @param Radius Specifies arc radius, range: 0~255
 * @param StartAngle Specifies arc start angle, range: -180~180
 *           Horizontal right is 0°, horizontal left is 180° or -180°, bottom is positive, top is negative, clockwise rotation
 * @param EndAngle Specifies arc end angle, range: -180~180
 *           Horizontal right is 0°, horizontal left is 180° or -180°, bottom is positive, top is negative, clockwise rotation
 * @param IsFilled Specifies whether arc is filled, filled becomes sector
 *           Range: OLED_UNFILLED        not filled
 *                 OLED_FILLED           filled
 * @return None
 * @note After calling this function, to actually display on screen, update function must be called
 */
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;

    /*This function borrows Bresenham's circle drawing method*/

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

    /*When drawing each point of circle, check if specified point is within specified angle, if yes draw point, if no skip*/
    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) // Specify arc fill
    {
        /*Traverse starting Y coordinate*/
        for (j = -y; j < y; j++) {
            /*When filling each point of circle, check if specified point is within specified angle, if yes draw point, if no skip*/
            if (OLED_IsInAngle(0, j, StartAngle, EndAngle)) { OLED_DrawPoint(X, Y + j); }
        }
    }

    while (x < y) // Traverse each point on X axis
    {
        x++;
        if (d < 0) // Next point is to the east
        {
            d += 2 * x + 1;
        } else // Next point is to the southeast
        {
            y--;
            d += 2 * (x - y) + 1;
        }

        /*When drawing each point of circle, check if specified point is within specified angle, if yes draw point, if no skip*/
        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) // Specify arc fill
        {
            /*Traverse middle part*/
            for (j = -y; j < y; j++) {
                /*When filling each point of circle, check if specified point is within specified angle, if yes draw point, if no skip*/
                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); }
            }

            /*Traverse sides*/
            for (j = -x; j < x; j++) {
                /*When filling each point of circle, check if specified point is within specified angle, if yes draw point, if no skip*/
                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); }
            }
        }
    }
}

/*********************Function Functions*/

/*****************Jiangxie Technology|All Rights Reserved****************/
/*****************jiangxiekeji.com*****************/

Recommended Reading