Driver para pantalla OLED de 0,96" basado en STM32G4 (biblioteca HAL), compatible con I2C por hardware/software

Controlador para pantalla OLED de 0,96" (SSD1306) basado en STM32G474 (interfaz I2C de 4 pines), compatible con I2C por hardware y por software, versión HAL.

Este controlador está bastante completo: permite mostrar texto en inglés, números enteros, números de coma flotante, caracteres chinos, imágenes, números binarios y hexadecimales; también puede dibujar puntos, líneas, rectángulos, círculos, elipses y triángulos, e incluye varios tipos de letra. Se podría considerar una pequeña biblioteca gráfica.

El programa es una modificación del código de Jiangxie Technology; la versión original estaba pensada para STM32F103 y solo soportaba I2C por software. Yo lo he adaptado para que funcione con I2C por hardware; basta con cambiar un par de macros para volver al modo software.

Placa de prueba: NUCLEO-G474RE

Para entender el principio de funcionamiento del OLED y ver un tutorial sobre el controlador, consulta el vídeo de Jiangxie Technology: https://url.zeruns.com/L7j6y

Grupo de Telegram sobre electrónica y microcontroladores: 2169025065

Fotos de resultado

Breve introducción al protocolo I2C

El protocolo de comunicación I2C (Inter-Integrated Circuit) fue desarrollado por Philips. Necesita pocos pines, su implementación hardware es sencilla, es altamente escalable y no requiere chips de conversión de niveles como los protocolos USART o CAN; por eso se utiliza ampliamente para la comunicación entre varios circuitos integrados dentro de un sistema.

I2C solo tiene un bus de datos, SDA (Serial Data Line), por lo que los datos se transmiten bit a bit; es una comunicación serial y semidúplex.

Semidúplex: la comunicación es bidireccional, pero no simultánea; debe alternarse la dirección. En un momento dado solo puede transmitirse en un sentido y basta con una línea de datos.

Podemos dividir I2C en capa física y capa de protocolo. La física define las características mecánicas y eléctricas (hardware) que garantizan la transmisión de los datos brutos por el medio. La de protocolo establece la lógica de comunicación y unifica el formato de empaquetado y desempaquetado de los datos entre emisor y receptor (software).

Capa física de I2C

Conexión típica entre dispositivos I2C

  1. Es un bus multi-dispositivo. “Bus” significa que varios dispositivos comparten la misma línea de señales. En un bus I2C pueden conectarse varios maestros y varios esclavos.
  2. Solo se necesitan dos líneas: SDA (línea de datos serial) y SCL (línea de reloj serial). SDA transporta la información y SCL sincroniza la transmisión.
  3. El bus se conecta a la alimentación mediante resistencias de pull-up. Cuando un dispositivo I2C está inactivo pasa a estado de alta impedancia; si todos están inactivos, las resistencias de pull-up llevan el bus a nivel alto.

Durante la comunicación I2C los pines GPIO del microcontrolador deben configurarse como salida open-drain; de lo contrario puede producirse un cortocircuito.

Para más información sobre I2C en STM32 y su uso, consulta: https://url.zeruns.com/JC0Ah

También el curso de introducción a STM32 de Jiangxie Technology: https://www.bilibili.com/video/BV1th411z7sn?p=31

Aquí no lo explicaré en detalle.

Instrucciones de uso

Por defecto se emplea I2C por hardware (I2C3): SCL en PA8 y SDA en PC9.

I2C por hardware

En STM32CubeMX localiza los pines del periférico I2C que vayas a usar y asígnalos como SCL y SDA. En la imagen siguiente se ve el SCL de I2C3.

A continuación activa el periférico I2C, selecciona el modo “Fast Mode Plus”, fija la velocidad en 1000 y deja el resto por defecto.

Configura los GPIO: al habilitar I2C los pines se configurarán automáticamente como salida alternativa open-drain. Solo tienes que cambiar la velocidad de salida a “Very High” y asignar las etiquetas I2C3_SCL y I2C3_SDA. Si usas otro periférico I2C puedes poner otras etiquetas, pero luego modifícalas también en el código. Tras hacerlo, genera el proyecto.

En el archivo OLED.c comenta la línea #define OLED_USE_SW_I2C y descomenta #define OLED_USE_HW_I2C. Si has usado otras etiquetas para los pines, cámbialas también donde corresponda.

I2C por software

En STM32CubeMX selecciona dos pines para SCL y SDA, asigna las etiquetas I2C3_SCL e I2C3_SDA (o las que hayas elegido), configúralos como salida open-drain, nivel alto por defecto, con pull-up y la máxima velocidad. Genera el código.

En OLED.c comenta #define OLED_USE_HW_I2C y descomenta #define OLED_USE_SW_I2C. Si has cambiado los nombres de los pines, actualízalos también aquí.

Componentes necesarios

Programa

Enlace de descarga del proyecto completo:

Baidu Netdisk: enlace: https://url.zeruns.com/0CQJG código de extracción: 0169

123 Netdisk (sin límite de velocidad): https://www.123pan.com/s/2Y9Djv-O0cvH.html código de extracción: vvDt

Dirección de código abierto en Gitee: https://gitee.com/zeruns/STM32-HAL-OLED-I2C

Dirección de código abierto en GitHub: https://github.com/zeruns/STM32G4-OLED-SSD1306-I2C-HAL

Por favor, dale una estrella

El proyecto fue creado con Keil5 y desarrollado con Vscode+EIDE; ambos programas pueden abrir este proyecto.

Todos los archivos del proyecto utilizan la codificación UTF-8; si al abrirlos aparecen caracteres extraños, cambia la codificación del editor a UTF-8.

Archivo principal OLED.c:


/***************************************************************************************
 * Este programa fue creado por Jiangxie Technology y se comparte de forma gratuita y de código abierto
 * Puedes consultarlo, usarlo y modificarlo libremente, y aplicarlo a tus propios proyectos
 * Los derechos de autor del programa pertenecen a Jiangxie Technology; ninguna persona o entidad puede apropiárselo.
 *
 * Nombre del programa:		Driver para pantalla OLED de 0.96 pulgadas (interfaz I2C de 4 pines)
 * Fecha de creación del programa:	2023.10.24
 * Versión actual del programa:		V1.1
 * Fecha de publicación de la versión actual:	2023.12.8
 *
 * Sitio web oficial de Jiangxie Technology:		jiangxiekeji.com
 * Tienda oficial de Jiangxie Technology en Taobao:	jiangxiekeji.taobao.com
 * Introducción al programa y actualizaciones:	jiangxiekeji.com/tutorial/oled.html
 *
 * Si encuentras errores u omisiones en el programa, puedes informarnos por correo electrónico: [email protected]
 * Antes de enviar el correo, puedes visitar la página de actualizaciones para verificar si el problema ya ha sido corregido.
 ***************************************************************************************
 */

/*
 * Este programa fue modificado por zeruns
 * Contenido de la modificación:	Cambiado de biblioteca estándar a HAL, añadido soporte para I2C por hardware; se puede activar o desactivar mediante definiciones de macro
 * Fecha de modificación:	2024.3.16
 * Blog:				https://blog.zeruns.com
 * Página principal de 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>

// Si se utilizan caracteres chinos, el compilador debe añadir la opción --no-multibyte-chars (no es necesario con el compilador AC6)

/*
Seleccione el modo de controlador OLED; por defecto se usa I2C por hardware.  
Si desea usar I2C por software, comente la línea del macro de I2C por hardware y descomente la de I2C por software.  
¡No deben estar descomentadas ambas líneas al mismo tiempo!  
En STM32CubeMX, al inicializar, asigne a los pines SCL y SDA las etiquetas “user label” I2C3_SCL e I2C3_SDA respectivamente.
*/
#define OLED_USE_HW_I2C    // I2C por hardware
//#define OLED_USE_SW_I2C  // I2C por software

/*Definición de pines; puede modificarse aquí el par de pines de comunicación I2C*/
#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

/*I2C3 por hardware del STM32G474: PA8 -- SCL; PC9 -- SDA*/

#ifdef OLED_USE_HW_I2C
/*Interfaz I2C: define qué periférico I2C usa la pantalla OLED*/
#define OLED_I2C            hi2c3
extern  I2C_HandleTypeDef   hi2c3;  // Para HAL库, especifica la interfaz I2C por hardware
#endif

/*Dirección del esclavo OLED*/
#define OLED_ADDRESS 0x3C << 1  // 0x3C es la dirección de 7 bits del OLED; se desplaza 1 bit para formar 0x78 con el bit R/W

/*Tiempo de espera de I2C*/
#define OLED_I2C_TIMEOUT 10
/*Tiempo de retardo para I2C por software. El valor siguiente es para 170 MHz de frecuencia principal; si su frecuencia es distinta, ajústelo. Para frecuencias ≤ 100 MHz basta con poner 0*/
#define Delay_time 3

/**
 * Formato de almacenamiento de datos:
 * 8 puntos verticales, bit más alto abajo; primero de izquierda a derecha, luego de arriba abajo.
 * Cada bit representa un píxel.
 *
 *      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
 *
 * Definición de ejes:
 * Esquina superior-izquierda = (0, 0)
 * Eje X: hacia la derecha, rango: 0~127
 * Eje Y: hacia abajo,     rango: 0~63
 *
 *       0             Eje X          127
 *      .------------------------------->
 *    0 |
 *      |
 *      |
 *      |
 * Eje Y|
 *      |
 *      |
 *      |
 *   63 |
 *      v
 *
 */

/*Variables globales*********************/
/**
 * Buffer de pantalla OLED
 * Todas las funciones de visualización leen/escriben únicamente este buffer.
 * Solo tras invocar OLED_Update o OLED_UpdateArea se envían los datos al hardware.
 */
uint8_t OLED_DisplayBuf[8][128];
/*********************Variables globales*/

#ifdef OLED_USE_SW_I2C
/**
 * @brief Escribe nivel lógico en OLED_SCL
 * Según BitValue, pone SCL a alto o bajo.
 * @param BitValue valor del bit: 0 o 1
 */
void OLED_W_SCL(uint8_t BitValue)
{
    /*Según BitValue, pone SCL alto o bajo*/
    HAL_GPIO_WritePin(OLED_SCL_GPIO_Port, OLED_SCL, (GPIO_PinState)BitValue);
    /*Si el MCU es muy rápido, añada retardo para no exceder la velocidad máxima de I2C*/
    for (volatile uint16_t i = 0; i < Delay_time; i++){
        //for (uint16_t j = 0; j < 10; j++);
    }
}

/**
  * @brief Escribe nivel lógico en SDA del OLED
  * @param BitValue valor a escribir: 0/1
  * @return Nada
  * @note Se invoca cuando la capa superior necesita escribir SDA.
  *       Si BitValue es 0, SDA se pone a bajo; si es 1, a alto.
  */
void OLED_W_SDA(uint8_t BitValue)
{
    /*Según BitValue, pone SDA alto o bajo*/
    HAL_GPIO_WritePin(OLED_SDA_GPIO_Port, OLED_SDA, (GPIO_PinState)BitValue);
    /*Retardo opcional para no sobrepasar la frecuencia de I2C*/
    for (volatile uint16_t i = 0; i < Delay_time; i++){
        //for (uint16_t j = 0; j < 10; j++);
    }
}
#endif

/**
 * @brief Inicialización de pines del OLED
 * @param  Ninguno
 * @retval Ninguno
 * @note Se invoca antes de usar el OLED.
 *       El usuario debe configurar SCL y SDA como salida open-drain y liberar los pines.
 */
void OLED_GPIO_Init(void)
{
    uint32_t i, j;

    /*Retardo antes de la inicialización para estabilizar la alimentación del OLED*/
    for (i = 0; i < 1000; i++) {
        for (j = 0; j < 1000; j++)
            ;
    }
#ifdef OLED_USE_SW_I2C
    __HAL_RCC_GPIOC_CLK_ENABLE();   // Habilita reloj de GPIOC
    __HAL_RCC_GPIOA_CLK_ENABLE();   // Habilita reloj de GPIOA
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    GPIO_InitStruct.Mode  = GPIO_MODE_OUTPUT_OD;        // Modo salida open-drain
    GPIO_InitStruct.Pull  = GPIO_PULLUP;                // Pull-up interno
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;  // Alta velocidad
    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);

    /*Libera SCL y SDA*/
    OLED_W_SCL(1);
    OLED_W_SDA(1);
#endif
}

// https://blog.zeruns.com

/*Protocolo de comunicación*********************/

/**
 * @brief Condición de START en I2C
 * @param  Ninguno
 * @return Ninguno
 */
void OLED_I2C_Start(void)
{
#ifdef OLED_USE_SW_I2C
    OLED_W_SDA(1);  // Libera SDA, asegura alto
    OLED_W_SCL(1);  // Libera SCL, asegura alto
    OLED_W_SDA(0);  // Durante SCL alto, baja SDA → START
    OLED_W_SCL(0);  // Tras START, baja SCL para ocupar el bus
#endif
}

/**
 * @brief Condición de STOP en I2C
 * @param  Ninguno
 * @return Ninguno
 */
void OLED_I2C_Stop(void)
{
#ifdef OLED_USE_SW_I2C
    OLED_W_SDA(0);  // Asegura SDA bajo
    OLED_W_SCL(1);  // Libera SCL → alto
    OLED_W_SDA(1);  // Durante SCL alto, libera SDA → STOP
#endif
}

/**
 * @brief Envía un byte por I2C
 * @param Byte dato a enviar, rango: 0x00~0xFF
 * @return Ninguno
 */
void OLED_I2C_SendByte(uint8_t Byte)
{
#ifdef OLED_USE_SW_I2C
    uint8_t i;
    /*8 ciclos: el master envía cada bit*/
    for (i = 0; i < 8; i++)
    {
        /*Máscara para extraer el bit correspondiente*/
        OLED_W_SDA(!!(Byte & (0x80 >> i)));
        OLED_W_SCL(1);  // El esclavo lee SDA mientras SCL está alto
        OLED_W_SCL(0);  // Baja SCL para preparar el siguiente bit
    }
    OLED_W_SCL(1);  // Ciclo extra: no se comprueba ACK
    OLED_W_SCL(0);
#endif
}

/**
 * @brief Escribe un comando en el OLED
 * @param Command valor del comando, rango: 0x00~0xFF
 * @return Ninguno
 */
void OLED_WriteCommand(uint8_t Command)
{
#ifdef OLED_USE_SW_I2C
    OLED_I2C_Start();               // START
    OLED_I2C_SendByte(0x78);        // Dirección del OLED
    OLED_I2C_SendByte(0x00);        // Byte de control: 0x00 → comando
    OLED_I2C_SendByte(Command);     // Comando propiamente
    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 Escribe datos en el OLED
 * @param Data puntero a los datos
 * @param Count cantidad de bytes a escribir
 * @return Ninguno
 */
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);        // Dirección del OLED
    OLED_I2C_SendByte(0x40);        // Byte de control: 0x40 → datos
    for (i = 0; i < Count; i++) {
        OLED_I2C_SendByte(Data[i]); // Envía cada byte
    }
    OLED_I2C_Stop();                // STOP
#elif defined(OLED_USE_HW_I2C)
    uint8_t TxData[Count + 1];      // Array nuevo: Count + 1
    TxData[0] = 0x40;               // Byte de control
    for (i = 0; i < Count; i++) {   // Copia datos
        TxData[i + 1] = Data[i];
    }
    HAL_I2C_Master_Transmit(&OLED_I2C, OLED_ADDRESS, (uint8_t*)TxData, Count + 1, OLED_I2C_TIMEOUT);
#endif    
}

/*********************Protocolo de comunicación*/

/*Configuración de hardware*********************/

/**
 * @brief Inicialización del OLED
 * @param Ninguno
 * @return Ninguno
 * @note Invocar antes de usar el OLED
 */
void OLED_Init(void)
{
    OLED_GPIO_Init();  // Primero inicializa los pines

    /*Secuencia de comandos de inicialización*/
    OLED_WriteCommand(0xAE); // Display OFF (0xAE), ON (0xAF)

    OLED_WriteCommand(0xD5); // Set Display Clock Divide Ratio / Oscillator Frequency
    OLED_WriteCommand(0x80); // 0x00~0xFF

    OLED_WriteCommand(0xA8); // Set Multiplex Ratio
    OLED_WriteCommand(0x3F); // 0x0E~0x3F

    OLED_WriteCommand(0xD3); // Set Display Offset
    OLED_WriteCommand(0x00); // 0x00~0x7F

    OLED_WriteCommand(0x40); // Set Display Start Line, 0x40~0x7F

    OLED_WriteCommand(0xA1); // Set Segment Re-map, 0xA1 normal, 0xA0 invertido

    OLED_WriteCommand(0xC8); // Set COM Output Scan Direction, 0xC8 normal, 0xC0 invertido

    OLED_WriteCommand(0xDA); // Set COM Pins Hardware Configuration
    OLED_WriteCommand(0x12);

    OLED_WriteCommand(0x81); // Set Contrast Control
    OLED_WriteCommand(0xCF); // 0x00~0xFF

    OLED_WriteCommand(0xD9); // Set Pre-charge Period
    OLED_WriteCommand(0xF1);

    OLED_WriteCommand(0xDB); // Set VCOMH Deselect Level
    OLED_WriteCommand(0x30);

    OLED_WriteCommand(0xA4); // Set Entire Display ON (ignora RAM)

    OLED_WriteCommand(0xA6); // Set Normal/Inverse Display, 0xA6 normal, 0xA7 inverso

    OLED_WriteCommand(0x8D); // Set Charge Pump
    OLED_WriteCommand(0x14);

    OLED_WriteCommand(0xAF); // Display ON

    OLED_Clear();  // Limpia el buffer
    OLED_Update(); // Actualiza la pantalla para evitar basura tras el init
}

/**
 * @brief Establece la posición del cursor en el OLED
 * @param Page página destino, rango: 0-7
 * @param X coordenada X, rango: 0-127
 * @return Ninguno
 * @note El eje Y solo se puede escribir de 8 en 8 píxeles (1 página = 8 líneas Y)
 */
void OLED_SetCursor(uint8_t Page, uint8_t X)
{
    /*Si usa una pantalla OLED de 1.3" (controlador SH1106, 132 columnas), descomente la siguiente línea*/
    /*La pantalla comienza en la columna 2, no en la 0; por tanto se suman 2 a X*/
    //  X += 2;

    /*Envía comandos de posición de página y columna*/
    OLED_WriteCommand(0xB0 | Page);              // Set Page Address
    OLED_WriteCommand(0x10 | ((X & 0xF0) >> 4)); // Set Column Address, nibble alto
    OLED_WriteCommand(0x00 | (X & 0x0F));        // Set Column Address, nibble bajo
}/*********************Configuración de hardware*/

/*Funciones de utilidad*********************/

/*Las funciones de utilidad solo son para uso interno de algunas funciones*/

/**
 * @brief Función de potencia
 * @param X base
 * @param Y exponente
 * @return igual a X elevado a Y
 */
uint32_t OLED_Pow(uint32_t X, uint32_t Y)
{
    uint32_t Result = 1; // El resultado por defecto es 1
    while (Y--)          // Multiplicar acumulativamente Y veces
    {
        Result *= X; // Cada vez multiplicar X al resultado
    }
    return Result;
}

/**
 * @brief Determina si un punto dado está dentro de un polígono especificado
 * @param nvert número de vértices del polígono
 * @param vertx verty arreglos que contienen las coordenadas x e y de los vértices del polígono
 * @param testx testy coordenadas X e y del punto de prueba
 * @return si el punto especificado está dentro del polígono especificado, 1: dentro, 0: fuera
 */
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;

    /*Este algoritmo fue propuesto por W. Randolph Franklin*/
    /*Enlace de referencia: 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 Determina si un punto especificado está dentro de un ángulo especificado
 * @param X Y coordenadas del punto especificado
 * @param StartAngle EndAngle ángulo inicial y ángulo final, rango: -180-180
 *           Horizontal hacia la derecha es 0 grados, horizontal hacia la izquierda es 180 o -180 grados, abajo es positivo, arriba es negativo, rotación en sentido horario
 * @return si el punto especificado está dentro del ángulo especificado, 1: dentro, 0: fuera
 */
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; // Calcular la medida en radianes del punto especificado y convertir a grados
    if (StartAngle < EndAngle)             // Caso en que el ángulo inicial es menor que el ángulo final
    {
        /*Si el ángulo especificado está entre el ángulo inicial y final, se determina que el punto está dentro del ángulo*/
        if (PointAngle >= StartAngle && PointAngle <= EndAngle) {
            return 1;
        }
    } else // Caso en que el ángulo inicial es mayor que el ángulo final
    {
        /*Si el ángulo especificado es mayor que el ángulo inicial o menor que el ángulo final, se determina que el punto está dentro del ángulo*/
        if (PointAngle >= StartAngle || PointAngle <= EndAngle) {
            return 1;
        }
    }
    return 0; // Si no cumple las condiciones anteriores, se determina que el punto no está dentro del ángulo
}

/*********************Funciones de utilidad*/

/*Funciones de funcionalidad*********************/

/**
 * @brief Actualiza el búfer de memoria de visualización OLED a la pantalla OLED
 * @param ninguno
 * @return ninguno
 * @note Todas las funciones de visualización solo leen y escriben en el búfer de memoria de visualización OLED
 *           Luego llamar a la función OLED_Update o OLED_UpdateArea
 *           para enviar los datos del búfer de memoria al hardware OLED y mostrarlos
 *           Por lo tanto, después de llamar a una función de visualización, para que realmente aparezca en la pantalla, también se debe llamar a la función de actualización
 */
void OLED_Update(void)
{
    uint8_t j;
    /*Recorrer cada página*/
    for (j = 0; j < 8; j++) {
        /*Establecer la posición del cursor en la primera columna de cada página*/
        OLED_SetCursor(j, 0);
        /*Escribir 128 datos consecutivos, enviando los datos del búfer de memoria al hardware OLED*/
        OLED_WriteData(OLED_DisplayBuf[j], 128);
    }
}

/**
 * @brief Actualiza parcialmente el búfer de memoria de visualización OLED a la pantalla OLED
 * @param X coordenada X de la esquina superior izquierda del área especificada, rango: 0-127
 * @param Y coordenada Y de la esquina superior izquierda del área especificada, rango: 0-63
 * @param Width ancho del área especificada, rango: 0-128
 * @param Height altura del área especificada, rango: 0-64
 * @return ninguno
 * @note Esta función actualizará al menos el área especificada por los parámetros
 *           Si el área de actualización en el eje Y solo incluye parte de una página, el resto de la misma página también se actualizará junto
 * @note Todas las funciones de visualización solo leen y escriben en el búfer de memoria de visualización OLED
 *           Luego llamar a la función OLED_Update o OLED_UpdateArea
 *           para enviar los datos del búfer de memoria al hardware OLED y mostrarlos
 *           Por lo tanto, después de llamar a una función de visualización, para que realmente aparezca en la pantalla, también se debe llamar a la función de actualización
 */
void OLED_UpdateArea(uint8_t X, uint8_t Y, uint8_t Width, uint8_t Height)
{
    uint8_t j;

    /*Verificación de parámetros para asegurar que el área especificada no exceda el rango de la pantalla*/
    if (X > 127) { return; }
    if (Y > 63) { return; }
    if (X + Width > 128) { Width = 128 - X; }
    if (Y + Height > 64) { Height = 64 - Y; }

    /*Recorrer las páginas relevantes involucradas en el área especificada*/
    /*(Y + Height - 1) / 8 + 1 tiene como propósito (Y + Height) / 8 redondeado hacia arriba*/
    for (j = Y / 8; j < (Y + Height - 1) / 8 + 1; j++) {
        /*Establecer la posición del cursor en la columna especificada de la página relevante*/
        OLED_SetCursor(j, X);
        /*Escribir Width datos consecutivos, enviando los datos del búfer de memoria al hardware OLED*/
        OLED_WriteData(&OLED_DisplayBuf[j][X], Width);
    }
}

/**
 * @brief Borra completamente el búfer de memoria de visualización OLED
 * @param ninguno
 * @return ninguno
 * @note Después de llamar a esta función, para que realmente aparezca en la pantalla, también se debe llamar a la función de actualización
 */
void OLED_Clear(void)
{
    uint8_t i, j;
    for (j = 0; j < 8; j++) // Recorrer 8 páginas
    {
        for (i = 0; i < 128; i++) // Recorrer 128 columnas
        {
            OLED_DisplayBuf[j][i] = 0x00; // Borrar todos los datos del búfer de memoria
        }
    }
}

/**
 * @brief Borra parcialmente el búfer de memoria de visualización OLED
 * @param X coordenada X de la esquina superior izquierda del área especificada, rango: 0-127
 * @param Y coordenada Y de la esquina superior izquierda del área especificada, rango: 0-63
 * @param Width ancho del área especificada, rango: 0-128
 * @param Height altura del área especificada, rango: 0-64
 * @return ninguno
 * @note Después de llamar a esta función, para que realmente aparezca en la pantalla, también se debe llamar a la función de actualización
 */
void OLED_ClearArea(uint8_t X, uint8_t Y, uint8_t Width, uint8_t Height)
{
    uint8_t i, j;

    /*Verificación de parámetros para asegurar que el área especificada no exceda el rango de la pantalla*/
    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++) // Recorrer la página especificada
    {
        for (i = X; i < X + Width; i++) // Recorrer la columna especificada
        {
            OLED_DisplayBuf[j / 8][i] &= ~(0x01 << (j % 8)); // Borrar el dato especificado del búfer de memoria
        }
    }
}

/**
 * @brief Invierte completamente el búfer de memoria de visualización OLED
 * @param ninguno
 * @return ninguno
 * @note Después de llamar a esta función, para que realmente aparezca en la pantalla, también se debe llamar a la función de actualización
 */
void OLED_Reverse(void)
{
    uint8_t i, j;
    for (j = 0; j < 8; j++) // Recorrer 8 páginas
    {
        for (i = 0; i < 128; i++) // Recorrer 128 columnas
        {
            OLED_DisplayBuf[j][i] ^= 0xFF; // Invertir todos los datos del búfer de memoria
        }
    }
}

/**
 * @brief Invierte parcialmente el búfer de memoria de visualización OLED
 * @param X coordenada X de la esquina superior izquierda del área especificada, rango: 0-127
 * @param Y coordenada Y de la esquina superior izquierda del área especificada, rango: 0-63
 * @param Width ancho del área especificada, rango: 0-128
 * @param Height altura del área especificada, rango: 0-64
 * @return ninguno
 * @note Después de llamar a esta función, para que realmente aparezca en la pantalla, también se debe llamar a la función de actualización
 */
void OLED_ReverseArea(uint8_t X, uint8_t Y, uint8_t Width, uint8_t Height)
{
    uint8_t i, j;

    /*Verificación de parámetros para asegurar que el área especificada no exceda el rango de la pantalla*/
    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++) // Recorrer la página especificada
    {
        for (i = X; i < X + Width; i++) // Recorrer la columna especificada
        {
            OLED_DisplayBuf[j / 8][i] ^= 0x01 << (j % 8); // Invertir el dato especificado del búfer de memoria
        }
    }
}

/**
 * @brief Muestra un carácter en OLED
 * @param X coordenada X de la esquina superior izquierda del carácter, rango: 0-127
 * @param Y coordenada Y de la esquina superior izquierda del carácter, rango: 0-63
 * @param Char carácter a mostrar, rango: carácter visible ASCII
 * @param FontSize tamaño de fuente especificado
 *           rango: OLED_8X16\t\tancho 8 píxeles, alto 16 píxeles
 *                 OLED_6X8\t\tancho 6 píxeles, alto 8 píxeles
 * @return ninguno
 * @note Después de llamar a esta función, para que realmente aparezca en la pantalla, también se debe llamar a la función de actualización
 */
void OLED_ShowChar(uint8_t X, uint8_t Y, char Char, uint8_t FontSize)
{
    if (FontSize == OLED_8X16) // Fuente de ancho 8 píxeles, alto 16 píxeles
    {
        /*Mostrar los datos especificados de la biblioteca de glifos ASCII OLED_F8x16 en formato de imagen 8*16*/
        OLED_ShowImage(X, Y, 8, 16, OLED_F8x16[Char - ' ']);
    } else if (FontSize == OLED_6X8) // Fuente de ancho 6 píxeles, alto 8 píxeles
    {
        /*Mostrar los datos especificados de la biblioteca de glifos ASCII OLED_F6x8 en formato de imagen 6*8*/
        OLED_ShowImage(X, Y, 6, 8, OLED_F6x8[Char - ' ']);
    }
}

/**
 * @brief Muestra una cadena en OLED
 * @param X coordenada X de la esquina superior izquierda de la cadena, rango: 0-127
 * @param Y coordenada Y de la esquina superior izquierda de la cadena, rango: 0-63
 * @param String cadena a mostrar, rango: cadena compuesta por caracteres visibles ASCII
 * @param FontSize tamaño de fuente especificado
 *           rango: OLED_8X16\t\tancho 8 píxeles, alto 16 píxeles
 *                 OLED_6X8\t\tancho 6 píxeles, alto 8 píxeles
 * @return ninguno
 * @note Después de llamar a esta función, para que realmente aparezca en la pantalla, también se debe llamar a la función de actualización
 */
void OLED_ShowString(uint8_t X, uint8_t Y, char *String, uint8_t FontSize)
{
    uint8_t i;
    for (i = 0; String[i] != '\0'; i++) // Recorrer cada carácter de la cadena
    {
        /*Llamar a la función OLED_ShowChar para mostrar cada carácter secuencialmente*/
        OLED_ShowChar(X + i * FontSize, Y, String[i], FontSize);
    }
}

/**
 * @brief Muestra un número en OLED (decimal, entero positivo)
 * @param X coordenada X de la esquina superior izquierda del número, rango: 0-127
 * @param Y coordenada Y de la esquina superior izquierda del número, rango: 0-63
 * @param Number número a mostrar, rango: 0-4294967295
 * @param Length longitud especificada del número, rango: 0-10
 * @param FontSize tamaño de fuente especificado
 *           rango: OLED_8X16\t\tancho 8 píxeles, alto 16 píxeles
 *                 OLED_6X8\t\tancho 6 píxeles, alto 8 píxeles
 * @return ninguno
 * @note Después de llamar a esta función, para que realmente aparezca en la pantalla, también se debe llamar a la función de actualización
 */
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++) // Recorrer cada dígito del número
    {
        /*Llamar a la función OLED_ShowChar para mostrar cada dígito secuencialmente*/
        /*Number / OLED_Pow(10, Length - i - 1) % 10 puede extraer cada dígito del número en decimal*/
        /*+ '0' convierte el dígito en formato de carácter*/
        OLED_ShowChar(X + i * FontSize, Y, Number / OLED_Pow(10, Length - i - 1) % 10 + '0', FontSize);
    }
}

/**
 * @brief Muestra un número con signo en OLED (decimal, entero)
 * @param X coordenada X de la esquina superior izquierda del número, rango: 0-127
 * @param Y coordenada Y de la esquina superior izquierda del número, rango: 0-63
 * @param Number número a mostrar, rango: -2147483648-2147483647
 * @param Length longitud especificada del número, rango: 0-10
 * @param FontSize tamaño de fuente especificado
 *           rango: OLED_8X16\t\tancho 8 píxeles, alto 16 píxeles
 *                 OLED_6X8\t\tancho 6 píxeles, alto 8 píxeles
 * @return ninguno
 * @note Después de llamar a esta función, para que realmente aparezca en la pantalla, también se debe llamar a la función de actualización
 */
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) // Número mayor o igual a 0
    {
        OLED_ShowChar(X, Y, '+', FontSize); // Mostrar signo +
        Number1 = Number;                   // Number1 directamente igual a Number
    } else                                  // Número menor que 0
    {
        OLED_ShowChar(X, Y, '-', FontSize); // Mostrar signo -
        Number1 = -Number;                  // Number1 igual a Number negado
    }

}for (i = 0; i < Length; i++) // recorre cada dígito del número
    {
        /*llama a OLED_ShowChar para mostrar cada dígito secuencialmente*/
        /*Number1 / OLED_Pow(10, Length - i - 1) % 10 extrae cada dígito en base 10*/
        /*+ '0' convierte el dígito en carácter*/
        OLED_ShowChar(X + (i + 1) * FontSize, Y, Number1 / OLED_Pow(10, Length - i - 1) % 10 + '0', FontSize);
    }
}

/**
 * @brief Muestra un número hexadecimal en OLED (hexadecimal, entero positivo)
 * @param X coordenada X de la esquina superior izquierda, rango: 0~127
 * @param Y coordenada Y de la esquina superior izquierda, rango: 0~63
 * @param Number número a mostrar, rango: 0x00000000~0xFFFFFFFF
 * @param Length longitud del número, rango: 0~8
 * @param FontSize tamaño de fuente
 *           valores: OLED_8X16 8 píxeles ancho, 16 píxeles alto
 *                   OLED_6X8  6 píxeles ancho, 8 píxeles alto
 * @return ninguno
 * @note tras llamarla se requiere la función de actualización para verlo en pantalla
 */
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++) // recorre cada dígito
    {
        /*extrae cada dígito en hexadecimal*/
        SingleNumber = Number / OLED_Pow(16, Length - i - 1) % 16;

        if (SingleNumber < 10) // dígito menor que 10
        {
            /*muestra el dígito con OLED_ShowChar*/
            /*+ '0' convierte el dígito en carácter*/
            OLED_ShowChar(X + i * FontSize, Y, SingleNumber + '0', FontSize);
        }
        else // dígito mayor o igual que 10
        {
            /*muestra el dígito con OLED_ShowChar*/
            /*+ 'A' convierte el valor en carácter hex (A-F)*/
            OLED_ShowChar(X + i * FontSize, Y, SingleNumber - 10 + 'A', FontSize);
        }
    }
}

/**
 * @brief Muestra un número binario en OLED (binario, entero positivo)
 * @param X coordenada X de la esquina superior izquierda, rango: 0~127
 * @param Y coordenada Y de la esquina superior izquierda, rango: 0~63
 * @param Number número a mostrar, rango: 0x00000000~0xFFFFFFFF
 * @param Length longitud del número, rango: 0~16
 * @param FontSize tamaño de fuente
 *           valores: OLED_8X16 8 píxeles ancho, 16 píxeles alto
 *                   OLED_6X8  6 píxeles ancho, 8 píxeles alto
 * @return ninguno
 * @note tras llamarla se requiere la función de actualización para verlo en pantalla
 */
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++) // recorre cada dígito
    {
        /*llama a OLED_ShowChar para mostrar cada bit*/
        /*Number / OLED_Pow(2, Length - i - 1) % 2 extrae cada bit*/
        /*+ '0' convierte el bit en carácter*/
        OLED_ShowChar(X + i * FontSize, Y, Number / OLED_Pow(2, Length - i - 1) % 2 + '0', FontSize);
    }
}

/**
 * @brief Muestra un número flotante en OLED (decimal con fracción)
 * @param X coordenada X de la esquina superior izquierda, rango: 0-127
 * @param Y coordenada Y de la esquina superior izquierda, rango: 0-63
 * @param Number número a mostrar, rango: -4294967295.0~4294967295.0
 * @param IntLength cifras de la parte entera, rango: 0-10
 * @param FraLength cifras de la parte fraccionaria, rango: 0-9, con redondeo
 * @param FontSize tamaño de fuente
 *           valores: OLED_8X16 8 píxeles ancho, 16 píxeles alto
 *                   OLED_6X8  6 píxeles ancho, 8 píxeles alto
 * @return ninguno
 * @note tras llamarla se requiere la función de actualización para verlo en pantalla
 */
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) // número positivo o cero
    {
        OLED_ShowChar(X, Y, '+', FontSize); // muestra signo +
    }
    else                                  // número negativo
    {
        OLED_ShowChar(X, Y, '-', FontSize); // muestra signo −
        Number = -Number;                   // cambia signo
    }

    /*extrae parte entera y fraccionaria*/
    IntNum = Number;                  // asignación directa, parte entera
    Number -= IntNum;                 // quita parte entera para evitar overflow al multiplicar
    PowNum = OLED_Pow(10, FraLength); // factor según decimales deseados
    FraNum = round(Number * PowNum);  // lleva decimales a entero y redondea
    IntNum += FraNum / PowNum;        // si el redondeo genera carry, súmelo a la parte entera

    /*muestra parte entera*/
    OLED_ShowNum(X + FontSize, Y, IntNum, IntLength, FontSize);

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

    /*muestra parte fraccionaria*/
    OLED_ShowNum(X + (IntLength + 2) * FontSize, Y, FraNum, FraLength, FontSize);
}

/**
 * @brief Muestra una cadena de caracteres chinos en OLED
 * @param X coordenada X de la esquina superior izquierda, rango: 0-127
 * @param Y coordenada Y de la esquina superior izquierda, rango: 0-63
 * @param Chinese cadena de caracteres chinos; debe contener solo chinos o caracteres de ancho completo, sin ASCII
 *           los caracteres deben estar definidos en el array OLED_CF16x16 de OLED_Data.c
 *           si no se encuentra, se muestra un símbolo por defecto (cuadro con ?)
 * @return ninguno
 * @note tras llamarla se requiere la función de actualización para verlo en pantalla
 */
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 ocupa 3 bytes; +1 para \0

    for (i = 0; Chinese[i] != '\0'; i++) // recorre la cadena
    {
        SingleChinese[pChinese] = Chinese[i]; // copia carácter
        pChinese++;                           // incrementa contador

        /*cuando se han copiado OLED_CHN_CHAR_WIDTH bytes se tiene un carácter completo*/
        if (pChinese >= OLED_CHN_CHAR_WIDTH) {
            SingleChinese[pChinese + 1] = '\0'; // fin de cadena
            pChinese                    = 0;    // reinicia contador

            /*busca el carácter en la tabla*/
            /*si se llega a una entrada vacía ("") se detiene la búsqueda*/
            for (pIndex = 0; strcmp(OLED_CF16x16[pIndex].Index, "") != 0; pIndex++) {
                /*coincidencia encontrada*/
                if (strcmp(OLED_CF16x16[pIndex].Index, SingleChinese) == 0) {
                    break; // pIndex queda con el índice del carácter
                }
            }

            /*muestra el bitmap 16×16 del carácter*/
            OLED_ShowImage(X + ((i + 1) / OLED_CHN_CHAR_WIDTH - 1) * 16, Y, 16, 16, OLED_CF16x16[pIndex].Data);
        }
    }
}

/**
 * @brief Muestra una imagen en OLED
 * @param X coordenada X de la esquina superior izquierda, rango: 0-128
 * @param Y coordenada Y de la esquina superior izquierda, rango: 0-64
 * @param Width ancho de la imagen, rango: 0-128
 * @param Height alto de la imagen, rango: 0-64
 * @param Image puntero a los datos de la imagen
 * @return ninguno
 * @note tras llamarla se requiere la función de actualización para verlo en pantalla
 */
void OLED_ShowImage(uint8_t X, uint8_t Y, uint8_t Width, uint8_t Height, const uint8_t *Image)
{
    uint8_t i, j;

    /*revisa límites para que la imagen no exceda la pantalla*/
    if (X > 127) { return; }
    if (Y > 63) { return; }

    /*borra el área donde se dibujará la imagen*/
    OLED_ClearArea(X, Y, Width, Height);

    /*recorre las páginas (filas de 8 píxeles) involucradas*/
    /*(Height - 1) / 8 + 1 equivale a redondear hacia arriba Height / 8*/
    for (j = 0; j < (Height - 1) / 8 + 1; j++) {
        /*recorre cada columna de la imagen*/
        for (i = 0; i < Width; i++) {
            /*si se sale de límites, salta*/
            if (X + i > 127) { break; }
            if (Y / 8 + j > 7) { return; }

            /*muestra la parte de la imagen en la página actual*/
            OLED_DisplayBuf[Y / 8 + j][X + i] |= Image[j * Width + i] << (Y % 8);

            /*si la página siguiente está fuera, continúa sin salir del bucle*/
            if (Y / 8 + j + 1 > 7) { continue; }

            /*muestra la parte de la imagen en la página siguiente*/
            OLED_DisplayBuf[Y / 8 + j + 1][X + i] |= Image[j * Width + i] >> (8 - Y % 8);
        }
    }
}

/**
 * @brief Imprime una cadena formateada con printf en OLED
 * @param X coordenada X de la esquina superior izquierda, rango: 0-127
 * @param Y coordenada Y de la esquina superior izquierda, rango: 0-63
 * @param FontSize tamaño de fuente
 *           valores: OLED_8X16 8 píxeles ancho, 16 píxeles alto
 *                   OLED_6X8  6 píxeles ancho, 8 píxeles alto
 * @param format cadena de formato estilo printf, solo caracteres ASCII visibles
 * @param ... lista de argumentos del formato
 * @return ninguno
 * @note tras llamarla se requiere la función de actualización para verlo en pantalla
 */
void OLED_Printf(uint8_t X, uint8_t Y, uint8_t FontSize, char *format, ...)
{
    char String[30];                         // buffer de salida
    va_list arg;                             // lista de argumentos variable
    va_start(arg, format);                   // empieza a leer desde format
    vsprintf(String, format, arg);           // formatea la cadena
    va_end(arg);                             // finaliza la lista
    OLED_ShowString(X, Y, String, FontSize); // muestra el resultado
}

/**
 * @brief Dibuja un punto en OLED
 * @param X coordenada X del punto, rango: 0-127
 * @param Y coordenada Y del punto, rango: 0-63
 * @return ninguno
 * @note tras llamarla se requiere la función de actualización para verlo en pantalla
 */
void OLED_DrawPoint(uint8_t X, uint8_t Y)
{
    /*revisa límites*/
    if (X > 127) { return; }
    if (Y > 63) { return; }

    /*pone a 1 el bit correspondiente en el buffer*/
    OLED_DisplayBuf[Y / 8][X] |= 0x01 << (Y % 8);
}

/**
 * @brief Obtiene el estado de un punto en OLED
 * @param X coordenada X del punto, rango: 0-127
 * @param Y coordenada Y del punto, rango: 0-63
 * @return 1 si está encendido, 0 si apagado
 */
uint8_t OLED_GetPoint(uint8_t X, uint8_t Y)
{
    /*revisa límites*/
    if (X > 127) { return 0; }
    if (Y > 63) { return 0; }

    /*devuelve el estado del bit*/
    if (OLED_DisplayBuf[Y / 8][X] & 0x01 << (Y % 8)) {
        return 1; // encendido
    }

    return 0; // apagado
}

/**
 * @brief Dibuja una línea en OLED
 * @param X0 coordenada X del primer extremo, rango: 0-127
 * @param Y0 coordenada Y del primer extremo, rango: 0-63
 * @param X1 coordenada X del segundo extremo, rango: 0-127
 * @param Y1 coordenada Y del segundo extremo, rango: 0-63
 * @return ninguno
 * @note tras llamarla se requiere la función de actualización para verlo en pantalla
 */
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) // Procesar líneas horizontales por separado
    {
        /*Si la coordenada X del punto 0 es mayor que la del punto 1, intercambiar las coordenadas X de ambos puntos*/
        if (x0 > x1) {
            temp = x0;
            x0   = x1;
            x1   = temp;
        }

        /*Recorrer la coordenada X*/
        for (x = x0; x <= x1; x++) {
            OLED_DrawPoint(x, y0); // Dibujar puntos secuencialmente
        }
    } else if (x0 == x1) // Procesar líneas verticales por separado
    {
        /*Si la coordenada Y del punto 0 es mayor que la del punto 1, intercambiar las coordenadas Y de ambos puntos*/
        if (y0 > y1) {
            temp = y0;
            y0   = y1;
            y1   = temp;
        }

        /*Recorrer la coordenada Y*/
        for (y = y0; y <= y1; y++) {
            OLED_DrawPoint(x0, y); // Dibujar puntos secuencialmente
        }
    } else // Línea inclinada
    {
        /*Usar el algoritmo de Bresenham para dibujar líneas rectas, evitando costosas operaciones de punto flotante, más eficiente*/
        /*Documento de referencia: https://www.cs.montana.edu/courses/spring2009/425/dslectures/Bresenham.pdf*/
        /*Tutorial de referencia: https://www.bilibili.com/video/BV1364y1d7Lo*/

        if (x0 > x1) // La coordenada X del punto 0 es mayor que la del punto 1
        {
            /*Intercambiar las coordenadas de ambos puntos*/
            /*Después del intercambio no afecta el dibujo de la línea, pero la dirección de dibujo cambia de los primeros, segundos, terceros y cuartos cuadrantes a los primeros y cuartos cuadrantes*/
            temp = x0;
            x0   = x1;
            x1   = temp;
            temp = y0;
            y0   = y1;
            y1   = temp;
        }

        if (y0 > y1) // La coordenada Y del punto 0 es mayor que la del punto 1
        {
            /*Negar la coordenada Y*/
            /*Después de negarla afecta el dibujo de la línea, pero la dirección de dibujo cambia de los primeros y cuartos cuadrantes al primer cuadrante*/
            y0 = -y0;
            y1 = -y1;

            /*Establecer el indicador yflag, recordar la transformación actual, y restaurar las coordenadas posteriormente durante el dibujo real de la línea*/
            yflag = 1;
        }

        if (y1 - y0 > x1 - x0) // La pendiente de la línea es mayor que 1
        {
            /*Intercambiar las coordenadas X e Y*/
            /*Después del intercambio afecta el dibujo de la línea, pero la dirección de dibujo cambia del rango de 0~90 grados del primer cuadrante al rango de 0~45 grados del primer cuadrante*/
            temp = x0;
            x0   = y0;
            y0   = temp;
            temp = x1;
            x1   = y1;
            y1   = temp;

            /*Establecer el indicador xyflag, recordar la transformación actual, y restaurar las coordenadas posteriormente durante el dibujo real de la línea*/
            xyflag = 1;
        }

        /*A continuación, dibujar líneas rectas con el algoritmo de Bresenham*/
        /*El algoritmo requiere que la dirección de dibujo esté en el rango de 0~45 grados del primer cuadrante*/
        dx     = x1 - x0;
        dy     = y1 - y0;
        incrE  = 2 * dy;
        incrNE = 2 * (dy - dx);
        d      = 2 * dy - dx;
        x      = x0;
        y      = y0;

        /*Dibujar el punto inicial, y al mismo tiempo verificar los indicadores, restaurar las coordenadas*/
        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) // Recorrer cada punto del eje X
        {
            x++;
            if (d < 0) // El siguiente punto está al este del punto actual
            {
                d += incrE;
            } else // El siguiente punto está al noreste del punto actual
            {
                y++;
                d += incrNE;
            }

            /*Dibujar cada punto, y al mismo tiempo verificar los indicadores, restaurar las coordenadas*/
            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 Rectángulo OLED
 * @param X Coordenada X de la esquina superior izquierda del rectángulo, rango: 0~127
 * @param Y Coordenada Y de la esquina superior izquierda del rectángulo, rango: 0~63
 * @param Width Ancho del rectángulo, rango: 0~128
 * @param Height Altura del rectángulo, rango: 0~64
 * @param IsFilled Indica si el rectángulo está relleno
 *           Rango: OLED_UNFILLED\t\tSin relleno
 *                 OLED_FILLED\t\t\tRelleno
 * @return Nada
 * @note Después de llamar a esta función, para que realmente aparezca en la pantalla, también debe llamarse a la función de actualización
 */
void OLED_DrawRectangle(uint8_t X, uint8_t Y, uint8_t Width, uint8_t Height, uint8_t IsFilled)
{
    uint8_t i, j;
    if (!IsFilled) // Rectángulo sin relleno
    {
        /*Recorrer las coordenadas X superior e inferior, dibujar las dos líneas horizontal del rectángulo*/
        for (i = X; i < X + Width; i++) {
            OLED_DrawPoint(i, Y);
            OLED_DrawPoint(i, Y + Height - 1);
        }
        /*Recorrer las coordenadas Y izquierda y derecha, dibujar las dos líneas vertical del rectángulo*/
        for (i = Y; i < Y + Height; i++) {
            OLED_DrawPoint(X, i);
            OLED_DrawPoint(X + Width - 1, i);
        }
    } else // Rectángulo relleno
    {
        /*Recorrer la coordenada X*/
        for (i = X; i < X + Width; i++) {
            /*Recorrer la coordenada Y*/
            for (j = Y; j < Y + Height; j++) {
                /*Dibujar puntos en el área especificada, rellenando el rectángulo*/
                OLED_DrawPoint(i, j);
            }
        }
    }
}

/**
 * @brief Triángulo OLED
 * @param X0 Coordenada X del primer punto extremo, rango: 0-127
 * @param Y0 Coordenada Y del primer punto extremo, rango: 0-63
 * @param X1 Coordenada X del segundo punto extremo, rango: 0-127
 * @param Y1 Coordenada Y del segundo punto extremo, rango: 0-63
 * @param X2 Coordenada X del tercer punto extremo, rango: 0-127
 * @param Y2 Coordenada Y del tercer punto extremo, rango: 0-63
 * @param IsFilled Indica si el triángulo está relleno
 *           Rango: OLED_UNFILLED\t\tSin relleno
 *                 OLED_FILLED\t\t\tRelleno
 * @return Nada
 * @note Después de llamar a esta función, para que realmente aparezca en la pantalla, también debe llamarse a la función de actualización
 */
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) // Triángulo sin relleno
    {
        /*Llamar a la función de dibujo de líneas, conectar los tres puntos con líneas rectas*/
        OLED_DrawLine(X0, Y0, X1, Y1);
        OLED_DrawLine(X0, Y0, X2, Y2);
        OLED_DrawLine(X1, Y1, X2, Y2);
    } else // Triángulo relleno
    {
        /*Encontrar las coordenadas X e Y mínimas de los tres puntos*/
        if (X1 < minx) { minx = X1; }
        if (X2 < minx) { minx = X2; }
        if (Y1 < miny) { miny = Y1; }
        if (Y2 < miny) { miny = Y2; }

        /*Encontrar las coordenadas X e Y máximas de los tres puntos*/
        if (X1 > maxx) { maxx = X1; }
        if (X2 > maxx) { maxx = X2; }
        if (Y1 > maxy) { maxy = Y1; }
        if (Y2 > maxy) { maxy = Y2; }

        /*El rectángulo entre las coordenadas mínimas y máximas es el área que puede necesitar relleno*/
        /*Recorrer todos los puntos en esta área*/
        /*Recorrer la coordenada X*/
        for (i = minx; i <= maxx; i++) {
            /*Recorrer la coordenada Y*/
            for (j = miny; j <= maxy; j++) {
                /*Llamar a OLED_pnpoly para determinar si el punto especificado está dentro del triángulo*/
                /*Si está dentro, dibujar el punto; si no, no hacer nada*/
                if (OLED_pnpoly(3, vx, vy, i, j)) { OLED_DrawPoint(i, j); }
            }
        }
    }
}

/**
 * @brief Dibujar círculo OLED
 * @param X Coordenada X del centro del círculo, rango: 0~127
 * @param Y Coordenada Y del centro del círculo, rango: 0~63
 * @param Radius Radio del círculo, rango: 0~255
 * @param IsFilled Indica si el círculo está relleno
 *           Rango: OLED_UNFILLED\t\tSin relleno
 *                 OLED_FILLED\t\t\tRelleno
 * @return Nada
 * @note Después de llamar a esta función, para que realmente aparezca en la pantalla, también debe llamarse a la función de actualización
 */
void OLED_DrawCircle(uint8_t X, uint8_t Y, uint8_t Radius, uint8_t IsFilled)
{
    int16_t x, y, d, j;

    /*Usar el algoritmo de Bresenham para dibujar círculos, evitando costosas operaciones de punto flotante, más eficiente*/
    /*Documento de referencia: https://www.cs.montana.edu/courses/spring2009/425/dslectures/Bresenham.pdf*/
    /*Tutorial de referencia: https://www.bilibili.com/video/BV1VM4y1u7wJ*/

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

    /*Dibujar los puntos iniciales de cada octavo de arco*/
    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) // Círculo relleno
    {
        /*Recorrer la coordenada Y inicial*/
        for (j = -y; j < y; j++) {
            /*Dibujar puntos en el área especificada, rellenando parte del círculo*/
            OLED_DrawPoint(X, Y + j);
        }
    }

    while (x < y) // Recorrer cada punto del eje X
    {
        x++;
        if (d < 0) // El siguiente punto está al este del punto actual
        {
            d += 2 * x + 1;
        } else // El siguiente punto está al sureste del punto actual
        {
            y--;
            d += 2 * (x - y) + 1;
        }

        /*Dibujar los puntos de cada octavo de arco*/
        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) // Círculo relleno
        {
            /*Recorrer la parte central*/
            for (j = -y; j < y; j++) {
                /*Dibujar puntos en el área especificada, rellenando parte del círculo*/
                OLED_DrawPoint(X + x, Y + j);
                OLED_DrawPoint(X - x, Y + j);
            }

            /*Recorrer las partes laterales*/
            for (j = -x; j < x; j++) {
                /*Dibujar puntos en el área especificada, rellenando parte del círculo*/
                OLED_DrawPoint(X - y, Y + j);
                OLED_DrawPoint(X + y, Y + j);
            }
        }
    }
}

/**
 * @brief Dibujar elipse OLED
 * @param X Coordenada X del centro de la elipse, rango: 0~127
 * @param Y Coordenada Y del centro de la elipse, rango: 0~63
 * @param A Longitud del semieje horizontal de la elipse, rango: 0~255
 * @param B Longitud del semieje vertical de la elipse, rango: 0~255
 * @param IsFilled Indica si la elipse está rellena
 *           Rango: OLED_UNFILLED\t\tSin relleno
 *                 OLED_FILLED\t\t\tRelleno
 * @return Nada
 * @note Después de llamar a esta función, para que realmente aparezca en la pantalla, también debe llamarse a la función de actualización
 */
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;

    /*Usar el algoritmo de Bresenham para dibujar elipses, evitando parte de las costosas operaciones de punto flotante, más eficiente*/
    /*Enlace de referencia: https://blog.csdn.net/myf_666/article/details/128167392*/

    x  = 0;
    y  = b;
    d1 = b * b + a * a * (-b + 0.5);if (IsFilled) // especificar relleno de la elipse
    {
        /*recorrer coordenada Y del punto inicial*/
        for (j = -y; j < y; j++) {
            /*dibujar punto en el área especificada, rellenar parte de la elipse*/
            OLED_DrawPoint(X, Y + j);
            OLED_DrawPoint(X, Y + j);
        }
    }

    /*dibujar punto inicial del arco de la elipse*/
    OLED_DrawPoint(X + x, Y + y);
    OLED_DrawPoint(X - x, Y - y);
    OLED_DrawPoint(X - x, Y + y);
    OLED_DrawPoint(X + x, Y - y);

    /*dibujar parte media de la elipse*/
    while (b * b * (x + 1) < a * a * (y - 0.5)) {
        if (d1 <= 0) // el siguiente punto está al este del actual
        {
            d1 += b * b * (2 * x + 3);
        } else // el siguiente punto está al sureste del actual
        {
            d1 += b * b * (2 * x + 3) + a * a * (-2 * y + 2);
            y--;
        }
        x++;

        if (IsFilled) // especificar relleno de la elipse
        {
            /*recorrer parte media*/
            for (j = -y; j < y; j++) {
                /*dibujar punto en el área especificada, rellenar parte de la elipse*/
                OLED_DrawPoint(X + x, Y + j);
                OLED_DrawPoint(X - x, Y + j);
            }
        }

        /*dibujar arco de la parte media de la elipse*/
        OLED_DrawPoint(X + x, Y + y);
        OLED_DrawPoint(X - x, Y - y);
        OLED_DrawPoint(X - x, Y + y);
        OLED_DrawPoint(X + x, Y - y);
    }

    /*dibujar ambos lados de la elipse*/
    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) // el siguiente punto está al este del actual
        {
            d2 += b * b * (2 * x + 2) + a * a * (-2 * y + 3);
            x++;

        } else // el siguiente punto está al sureste del actual
        {
            d2 += a * a * (-2 * y + 3);
        }
        y--;

        if (IsFilled) // especificar relleno de la elipse
        {
            /*recorrer ambos lados*/
            for (j = -y; j < y; j++) {
                /*dibujar punto en el área especificada, rellenar parte de la elipse*/
                OLED_DrawPoint(X + x, Y + j);
                OLED_DrawPoint(X - x, Y + j);
            }
        }

        /*dibujar arcos de ambos lados de la elipse*/
        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 Dibujar arco en OLED
 * @param X coordenada X del centro del arco, rango: 0~127
 * @param Y coordenada Y del centro del arco, rango: 0~63
 * @param Radius radio del arco, rango: 0~255
 * @param StartAngle ángulo inicial del arco, rango: -180~180
 *           0 grados es horizontal hacia la derecha, 180 o -180 grados es horizontal hacia la izquierda, abajo es positivo, arriba es negativo, rotación en sentido horario
 * @param EndAngle ángulo final del arco, rango: -180~180
 *           0 grados es horizontal hacia la derecha, 180 o -180 grados es horizontal hacia la izquierda, abajo es positivo, arriba es negativo, rotación en sentido horario
 * @param IsFilled especificar si el arco está relleno, al rellenar se convierte en sector
 *           rango: OLED_UNFILLED\t\tsin relleno
 *                 OLED_FILLED\t\t\trelleno
 * @return ninguno
 * @note después de llamar a esta función, para que realmente aparezca en la pantalla, también se debe llamar a la función de actualización
 */
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;

    /*esta función utiliza el algoritmo de Bresenham para dibujar círculos*/

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

    /*al dibujar cada punto del círculo, verificar si el punto está dentro del ángulo especificado, si es así, dibujar punto, si no, no hacer nada*/
    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) // especificar relleno del arco
    {
        /*recorrer coordenada Y del punto inicial*/
        for (j = -y; j < y; j++) {
            /*al rellenar cada punto del círculo, verificar si el punto está dentro del ángulo especificado, si es así, dibujar punto, si no, no hacer nada*/
            if (OLED_IsInAngle(0, j, StartAngle, EndAngle)) { OLED_DrawPoint(X, Y + j); }
        }
    }

    while (x < y) // recorrer cada punto del eje X
    {
        x++;
        if (d < 0) // el siguiente punto está al este del actual
        {
            d += 2 * x + 1;
        } else // el siguiente punto está al sureste del actual
        {
            y--;
            d += 2 * (x - y) + 1;
        }

        /*al dibujar cada punto del círculo, verificar si el punto está dentro del ángulo especificado, si es así, dibujar punto, si no, no hacer nada*/
        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) // especificar relleno del arco
        {
            /*recorrer parte media*/
            for (j = -y; j < y; j++) {
                /*al rellenar cada punto del círculo, verificar si el punto está dentro del ángulo especificado, si es así, dibujar punto, si no, no hacer nada*/
                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); }
            }

            /*recorrer ambos lados*/
            for (j = -x; j < x; j++) {
                /*al rellenar cada punto del círculo, verificar si el punto está dentro del ángulo especificado, si es así, dibujar punto, si no, no hacer nada*/
                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); }
            }
        }
    }
}

/*********************Funciones de utilidad*/

/*****************Jiangxie Technology | Todos los derechos reservados****************/
/*****************jiangxiekeji.com*****************/

## Recomendaciones de lectura

- **Recomendación de VPS/Servidores en la nube de alta relación calidad-precio y económicos:** [https://blog.zeruns.com/archives/383.html](https://blog.zeruns.com/archives/383.html)
- He hecho un recolector de energía trifásica de código abierto, que puede monitorear fácilmente el consumo eléctrico del hogar: [https://blog.zeruns.com/archives/771.html](https://blog.zeruns.com/archives/771.html)
- Tutorial de creación de servidor de Minecraft: [https://blog.zeruns.com/tag/mc/](https://blog.zeruns.com/tag/mc/)
- Tutorial de creación de servidor de Palworld: [https://blog.zeruns.com/tag/PalWorld/](https://blog.zeruns.com/tag/PalWorld/)
- Revisión y unboxing rápido de la fuente de alimentación ajustable numérica RD6012P de Ruideng, fuente de CC numérica 60V 12A: [https://blog.zeruns.com/archives/740.html](https://blog.zeruns.com/archives/740.html)
- Experiencia de unboxing de la impresora 3D Bambu Lab P1SC: [https://blog.zeruns.com/archives/770.html](https://blog.zeruns.com/archives/770.html)
- Comparación real de capacitores e inductores de diferentes marcas y tipos (valor D, valor Q, ESR, X): [https://blog.zeruns.com/archives/765.html](https://blog.zeruns.com/archives/765.html)