Driver màn hình OLED 0,96 inch (SSD1306) 4 chân giao tiếp I2C dựa trên STM32G474, hỗ trợ I2C phần cứng/I2C mềm, phiên bản HAL.
Driver này khá hoàn chỉnh, có thể hiển thị tiếng Anh, số nguyên, số thực, tiếng Trung, hình ảnh, số nhị phân, số thập lục phân; vẽ điểm, đường thẳng, hình chữ nhật, tròn, elip, tam giác… hỗ trợ nhiều font, gần như một thư viện đồ họa thu nhỏ.
Chương trình được viết lại từ mã gốc của Jiangxie Technology; bản gốc chỉ dùng I2C mềm trên STM32F103. Sau khi sửa, nó hỗ trợ I2C phần cứng và có thể chuyển sang I2C mềm bằng cách thay đổi macro.
Phần cứng thử nghiệm: kit NUCLEO-G474RE
Nguyên lý điều khiển OLED và hướng dẫn sử dụng driver xem video của Jiangxie Technology: https://url.zeruns.com/L7j6y
- STM32 đọc cảm biến nhiệt độ độ ẩm SHTC3 qua I2C phần cứng: https://blog.zeruns.com/archives/692.html
- Template STM32F407 đã port sẵn thư viện đồ họa U8g2: https://blog.zeruns.com/archives/722.html
- Driver OLED 0,96 inch cho STM32F1, hỗ trợ I2C phần cứng/mềm: https://blog.zeruns.com/archives/769.html
Nhóm thảo luận điện tử/vi điều khiển: 2169025065
Ảnh chạy thử

Giới thiệu nhanh giao thức I2C
Giao thức I2C (Inter-Integrated Circuit) do Philips phát triển; ít chân, dễ triển khai phần cứng, mở rộng tốt, không cần IC chuyển đổi mức như USART, CAN, nên được dùng phổ biến để giao tiếp giữa các IC trong hệ thống.
I2C chỉ có một đường dữ liệu SDA (Serial Data Line), truyền dữ liệu từng bit, thuộc truyền thông nối tiếp, bán song công.
Bán song công: truyền được cả hai chiều nhưng không đồng thời, luân phiên như đơn công có thể đổi chiều; mỗi thời điểm chỉ một chiều, cần duy nhất một đường dữ liệu.
Người ta chia I2C thành lớp vật lý và lớp giao thức:
- Lớp vật lý quy định tính chất cơ khí, điện (phần cứng) để đảm bảo truyền dữ liệu thô trên phương tiện.
- Lớp giao thức quy định luật giao tiếp, cách đóng/mở gói tin giữa hai bên (phần mềm).
Lớp vật lý I2C
Cách nối thường gặp giữa các thiết bị I2C
- Là bus đa chủ: nhiều thiết bị chia sẻ cùng tuyến tín hiệu. Trên một bus I2C có thể nối nhiều master và nhiều slave.
- Chỉ cần hai tuyến: SDA (Serial Data) và SCL (Serial Clock). SDA truyền dữ liệu, SCL đồng bộ bit.
- Bus được kéo lên VCC qua điện trở pull-up. Khi rảnh, thiết bị ở trạng thái high-Z; nếu tất cả đều high-Z thì điện trở pull-up kéo bus lên mức cao.
Khi giao tiếp I2C, GPIO của vi điều khiển phải đặt ở chế độ open-drain, nếu không có thể gây chập.
Chi tiết về I2C trên STM32 xem thêm: https://url.zeruns.com/JC0Ah
Video khóa học STM32 cơ bản của Jiangxie: https://www.bilibili.com/video/BV1th411z7sn?p=31
Ở đây mình không lặp lại.
Hướng dẫn sử dụng
Mặc định dùng I2C phần cứng (I2C3): SCL = PA8, SDA = PC9.
I2C phần cứng
Cấu hình STM32CubeMX: chọn chân I2C, gán chức năng SCL và SDA. Hình dưới là SCL của I2C3.
Kích hoạt ngoại vi I2C, chọn Fast Mode Plus, tốc độ 1000 kHz, còn lại để mặc định.
Cấu hình GPIO: sau bước trên, hai chân sẽ tự động thành Alternate-function open-drain. Chỉ cần đặt tốc độ IO thành Very High và đổi Label thành I2C3_SCL, I2C3_SDA (nếu dùng I2C khác thì đổi tên tương ứng và sửa lại code). Sau đó Generate Code.
Trong file OLED.c, comment dòng #define OLED_USE_SW_I2C, bỏ comment #define OLED_USE_HW_I2C. Nếu dùng chân khác thì sửa thêm I2C3_SCL và I2C3_SDA trong code.
I2C mềm
Cấu hình STM32CubeMX: chọn hai chân làm SCL/SDA, đặt Label I2C3_SCL và I2C3_SDA, chế độ GPIO output open-drain, mức cao khi reset, có pull-up, tốc độ cao nhất. Hình dưới minh họa.
Trong OLED.c, comment #define OLED_USE_HW_I2C, bỏ comment #define OLED_USE_SW_I2C. Nếu đổi tên chân thì sửa lại I2C3_SCL và I2C3_SDA cho khớp.
Linh kiện cần chuẩn bị
- Kit khởi đầu STM32: https://u.jd.com/fQS0YAe
- Kit STM32G474: https://s.click.taobao.com/8OwQ8vt
- Mô-đun OLED: https://s.click.taobao.com/EF0Evwt
- Dây nối Dupont: https://s.click.taobao.com/VMkDvwt
- Breadboard: https://s.click.taobao.com/bhg8Txt
- DAPLink (thay thế ST-Link, có VCP): https://s.click.taobao.com/QVQ8TxtBộ kit nhập môn STM32 của Jiangxie Technology: https://s.click.taobao.com/NTn9Txt
Chương trình
Địa chỉ tải toàn bộ dự án:
Baidu Netdisk: Link: https://url.zeruns.com/0CQJG Mã trích xuất: 0169
123 Netdisk (không giới hạn tốc độ): https://www.123pan.com/s/2Y9Djv-O0cvH.html Mã trích xuất: vvDt
Địa chỉ mã nguồn mở Gitee: https://gitee.com/zeruns/STM32-HAL-OLED-I2C
Địa chỉ mã nguồn mở GitHub: https://github.com/zeruns/STM32G4-OLED-SSD1306-I2C-HAL
Hãy cho một Star nhé
Dự án được tạo bằng Keil5, phát triển với Vscode + EIDE, cả hai phần mềm đều có thể mở được dự án này.
Tất cả các tệp dự án đều sử dụng mã hóa UTF-8, nếu mở ra bị lỗi font cần chỉnh sửa bộ mã hóa của trình soạn thảo thành UTF-8.
Tệp chính OLED.c:
/***************************************************************************************
* Chương trình này được tạo bởi Jiangxie Technology và chia sẻ mã nguồn mở miễn phí
* Bạn có thể xem, sử dụng và chỉnh sửa tùy ý, và áp dụng vào dự án của mình
* Bản quyền chương trình thuộc về Jiangxie Technology, bất kỳ cá nhân hoặc tổ chức nào cũng không được coi là sở hữu
*
* Tên chương trình: Màn hình OLED 0.96 inch trình điều khiển (giao tiếp I2C 4 chân)
* Thời gian tạo chương trình: 24.10.2023
* Phiên bản chương trình hiện tại: V1.1
* Thời gian phát hành phiên bản hiện tại: 08.12.2023
*
* Trang web chính thức của Jiangxie Technology: jiangxiekeji.com
* Cửa hàng Taobao chính thức của Jiangxie Technology: jiangxiekeji.taobao.com
* Giới thiệu chương trình và cập nhật động: jiangxiekeji.com/tutorial/oled.html
*
* Nếu bạn phát hiện lỗi hoặc sai sót trong chương trình, vui lòng phản hồi qua email: feedback@jiangxiekeji.com
* Trước khi gửi email, bạn có thể truy cập trang cập nhật động để xem chương trình mới nhất, nếu vấn đề này đã được sửa, thì không cần gửi email nữa
***************************************************************************************
*/
/*
* Chương trình này được chỉnh sửa lại bởi zeruns
* Nội dung chỉnh sửa: Chuyển từ thư viện chuẩn sang thư viện HAL, thêm hỗ trợ I2C phần cứng, có thể chọn có bật I2C phần cứng hay không bằng cách sửa định nghĩa macro
* Ngày chỉnh sửa: 16.3.2024
* Blog: https://blog.zeruns.com
* Trang chủ Bilibili: https://space.bilibili.com/8320520
*/
#include "main.h"
#include "OLED.h"
#include <str
```ing.h>
#include <math.h>
#include <stdio.h>
#include <stdarg.h>
// Nếu dùng tiếng Trung, cần thêm tùy chọn biên dịch --no-multibyte-chars (không cần thêm nếu dùng trình biên dịch AC6)
/*
Chọn kiểu điều khiển OLED, mặc định dùng I2C phần cứng. Nếu muốn dùng I2C phần mềm thì comment dòng định nghĩa macro của I2C phần cứng và bỏ comment dòng I2C phần mềm.
Không được bỏ comment cả hai dòng cùng lúc!
Trong STM32CubeMX khởi tạo cần đặt “user label” cho chân SCL và SDA là I2C3_SCL và I2C3_SDA.
*/
#define OLED_USE_HW_I2C // I2C phần cứng
//#define OLED_USE_SW_I2C // I2C phần mềm
/*Định nghĩa chân, có thể sửa đổi chân giao tiếp I2C tại đây*/
#define OLED_SCL 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 phần cứng vi điều khiển STM32G474: PA8 -- SCL; PC9 -- SDA*/
#ifdef OLED_USE_HW_I2C
/*Giao tiếp I2C, định nghĩa OLED dùng cổng I2C nào*/
#define OLED_I2C hi2c3
extern I2C_HandleTypeDef hi2c3; //HAL dùng để chỉ định cổng IIC phần cứng
#endif
/*Địa chỉ slave OLED*/
#define OLED_ADDRESS 0x3C << 1 // 0x3C là địa chỉ 7 bit của OLED, dịch trái 1 bit, bit cuối làm bit R/W thành 0x78
/*Thời gian timeout I2C*/
#define OLED_I2C_TIMEOUT 10
/*Thời gian trễ cho I2C phần mềm, giá trị dưới dùng cho tần số 170 MHz, nếu tần số khác thì điều chỉnh, tần số ≤100 MHz đặt thành 0*/
#define Delay_time 3
/**
* Định dạng lưu trữ dữ liệu:
* 8 điểm dọc, bit cao ở dưới, trái sang phải, trên xuống dưới
* Mỗi bit tương ứng một pixel
*
* B0 B0 B0 B0
* B1 B1 B1 B1
* B2 B2 B2 B2
* B3 B3 -------------> B3 B3 --
* B4 B4 B4 B4 |
* B5 B5 B5 B5 |
* B6 B6 B6 B6 |
* B7 B7 B7 B7 |
* |
* -----------------------------------
* |
* | B0 B0 B0 B0
* | B1 B1 B1 B1
* | B2 B2 B2 B2
* --> B3 B3 -------------> B3 B3
* B4 B4 B4 B4
* B5 B5 B5 B5
* B6 B6 B6 B6
* B7 B7 B7 B7
*
* Định nghĩa trục tọa độ:
* Góc trên bên trái là điểm (0, 0)
* Trục X: sang phải, giá trị 0~127
* Trục Y: xuống dưới, giá trị 0~63
*
* 0 trục X 127
* .------------------------------->
* 0 |
* |
* |
* |
* trục |
* Y |
* |
* |
* 63 |
* v
*
*/
/*Biến toàn cục*********************/
/**
* Mảng bộ nhớ hiển thị OLED
* Mọi hàm hiển thị chỉ đọc/ghi mảng này
* Sau đó gọi OLED_Update hoặc OLED_UpdateArea
* mới gửi dữ liệu xuống phần cứng OLED
*/
uint8_t OLED_DisplayBuf[8][128];
/*********************Biến toàn cục*/
#ifdef OLED_USE_SW_I2C
/**
* @brief Ghi mức logic cho OLED_SCL
* Theo BitValue đặt OLED_SCL ở mức cao hoặc thấp.
* @param BitValue giá trị bit, 0 hoặc 1
*/
void OLED_W_SCL(uint8_t BitValue)
{
/*Theo BitValue đặt SCL cao hoặc thấp*/
HAL_GPIO_WritePin(OLED_SCL_GPIO_Port, OLED_SCL, (GPIO_PinState)BitValue);
/*Nếu vi điều khiển quá nhanh, thêm độ trễ phù hợp để không vượt tốc độ I2C tối đa*/
for (volatile uint16_t i = 0; i < Delay_time; i++){
//for (uint16_t j = 0; j < 10; j++);
}
}
/**
* @brief Ghi mức logic cho SDA của OLED
* @param BitValue giá trị cần ghi, 0/1
* @return không
* @note Khi lớp trên cần ghi SDA, hàm này được gọi
* Người dùng cần đặt SDA theo giá trị truyền vào
* 0: SDA thấp, 1: SDA cao
*/
void OLED_W_SDA(uint8_t BitValue)
{
/*Theo BitValue đặt SDA cao hoặc thấp*/
HAL_GPIO_WritePin(OLED_SDA_GPIO_Port, OLED_SDA, (GPIO_PinState)BitValue);
/*Nếu vi điều khiển quá nhanh, thêm độ trễ phù hợp để không vượt tốc độ I2C tối đa*/
for (volatile uint16_t i = 0; i < Delay_time; i++){
//for (uint16_t j = 0; j < 10; j++);
}
}
#endif
/**
* @brief Khởi tạo chân OLED
* @param không
* @retval không
* @note Lớp trên cần khởi tạo sẽ gọi hàm này,
* người dùng cần đặt chân SCL và SDA ở chế độ open-drain và thả chân
*/
void OLED_GPIO_Init(void)
{
uint32_t i, j;
/*Trước khởi tạo, thêm độ trễ để nguồn OLED ổn định*/
for (i = 0; i < 1000; i++) {
for (j = 0; j < 1000; j++)
;
}
#ifdef OLED_USE_SW_I2C
__HAL_RCC_GPIOC_CLK_ENABLE(); // Kích hoạt clock GPIOC
__HAL_RCC_GPIOA_CLK_ENABLE(); // Kích hoạt clock GPIOA
GPIO_InitTypeDef GPIO_InitStruct = {0}; // Khai báo cấu trúc cấu hình GPIO
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD; // Chế độ open-drain
GPIO_InitStruct.Pull = GPIO_PULLUP; // Pull-up nội
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH; // Tốc độ cao
GPIO_InitStruct.Pin = I2C3_SDA_Pin; // Chân SDA
HAL_GPIO_Init(I2C3_SDA_GPIO_Port, &GPIO_InitStruct);// Khởi tạo GPIO
GPIO_InitStruct.Pin = I2C3_SCL_Pin;
HAL_GPIO_Init(I2C3_SCL_GPIO_Port, &GPIO_InitStruct);
/*Thả SCL và SDA*/
OLED_W_SCL(1);
OLED_W_SDA(1);
#endif
}
// https://blog.zeruns.com
/*Giao thức truyền thông*********************/
/**
* @brief Bắt đầu I2C
* @param không
* @return không
*/
void OLED_I2C_Start(void)
{
#ifdef OLED_USE_SW_I2C
OLED_W_SDA(1); //Thả SDA, đảm bảo SDA cao
OLED_W_SCL(1); //Thả SCL, đảm bảo SCL cao
OLED_W_SDA(0); //Trong SCL cao, kéo SDA xuống tạo tín hiệu START
OLED_W_SCL(0); //Sau START kéo SCL xuống, chiếm tổng tiếp và thuận tiện cho thời序 tiếp theo
#endif
}
/**
* @brief Kết thúc I2C
* @param không
* @return không
*/
void OLED_I2C_Stop(void)
{
#ifdef OLED_USE_SW_I2C
OLED_W_SDA(0); //Kéo SDA xuống, đảm bảo SDA thấp
OLED_W_SCL(1); //Thả SCL, SCL cao
OLED_W_SDA(1); //Trong SCL cao, thả SDA tạo tín hiệu STOP
#endif
}
/**
* @brief Gửi một byte qua I2C
* @param Byte dữ liệu byte cần gửi, 0x00~0xFF
* @return không
*/
void OLED_I2C_SendByte(uint8_t Byte)
{
#ifdef OLED_USE_SW_I2C
uint8_t i;
/*Lặp 8 lần, master gửi từng bit*/
for (i = 0; i < 8; i++)
{
/*Dùng mask lấy từng bit của Byte và ghi vào SDA*/
/*Hai dấu !! biến mọi giá trị khác 0 thành 1*/
OLED_W_SDA(!!(Byte & (0x80 >> i)));
OLED_W_SCL(1); //Thả SCL, slave đọc SDA trong SCL cao
OLED_W_SCL(0); //Kéo SCL, master gửi bit tiếp
}
OLED_W_SCL(1); //Thêm một xung, không xử lý ACK
OLED_W_SCL(0);
#endif
}
/**
* @brief Ghi lệnh cho OLED
* @param Command giá trị lệnh cần ghi, 0x00~0xFF
* @return không
*/
void OLED_WriteCommand(uint8_t Command)
{
#ifdef OLED_USE_SW_I2C
OLED_I2C_Start(); // Bắt đầu I2C
OLED_I2C_SendByte(0x78); //Gửi địa chỉ slave I2C của OLED
OLED_I2C_SendByte(0x00); //Byte điều khiển, 0x00: sắp ghi lệnh
OLED_I2C_SendByte(Command); // Ghi lệnh chỉ định
OLED_I2C_Stop(); // Kết thúc I2C
#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 Ghi dữ liệu cho OLED
* @param Data địa chỉ đầu dữ liệu cần ghi
* @param Count số byte cần ghi
* @return không
*/
void OLED_WriteData(uint8_t *Data, uint8_t Count)
{
uint8_t i;
#ifdef OLED_USE_SW_I2C
OLED_I2C_Start(); // Bắt đầu I2C
OLED_I2C_SendByte(0x78); //Gửi địa chỉ slave I2C của OLED
OLED_I2C_SendByte(0x40); // Byte điều khiển, 0x40: sắp ghi dữ liệu
/*Lặp Count lần, ghi liên tục*/
for (i = 0; i < Count; i++) {
OLED_I2C_SendByte(Data[i]); // Gửi lần lượt từng byte
}
OLED_I2C_Stop(); // Kết thúc I2C
#elif defined(OLED_USE_HW_I2C)
uint8_t TxData[Count + 1]; // Cấp phát mảng mới, kích thước Count + 1
TxData[0] = 0x40; // Byte bắt đầu
// Sao chép dữ liệu từ Data vào phần còn lại của TxData
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
}
/*********************Giao thức truyền thông*/
/*Cấu hình phần cứng*********************/
/**
* @brief Khởi tạo OLED
* @param không
* @return không
* @note Trước khi dùng cần gọi hàm khởi tạo này
*/
void OLED_Init(void)
{
OLED_GPIO_Init(); // Gọi khởi tạo chân trước
/*Gửi chuỗi lệnh khởi tạo OLED*/
OLED_WriteCommand(0xAE); // Tắt/mở hiển thị, 0xAE tắt, 0xAF mở
OLED_WriteCommand(0xD5); // Thiết lập tỉ lệ chia clock/ tần số dao động
OLED_WriteCommand(0x80); // 0x00~0xFF
OLED_WriteCommand(0xA8); // Thiết lập độ đa hợp
OLED_WriteCommand(0x3F); // 0x0E~0x3F
OLED_WriteCommand(0xD3); // Thiết lập độ lệch hiển thị
OLED_WriteCommand(0x00); // 0x00~0x7F
OLED_WriteCommand(0x40); // Thiết lập dòng bắt đầu hiển thị, 0x40~0x7F
OLED_WriteCommand(0xA1); // Thiết lập chiều trái-phải, 0xA1 bình thường, 0xA0 đảo
OLED_WriteCommand(0xC8); // Thiết lập chiều trên-dưới, 0xC8 bình thường, 0xC0 đảo
OLED_WriteCommand(0xDA); // Thiết lập cấu hình phần cứng chân COM
OLED_WriteCommand(0x12);
OLED_WriteCommand(0x81); // Thiết lập độ tương phản
OLED_WriteCommand(0xCF); // 0x00~0xFF
OLED_WriteCommand(0xD9); // Thiết lập chu kỳ nạp trước
OLED_WriteCommand(0xF1);
OLED_WriteCommand(0xDB); // Thiết lập mức VCOMH
OLED_WriteCommand(0x30);
OLED_WriteCommand(0xA4); // Mở/tắt toàn bộ hiển thị
OLED_WriteCommand(0xA6); // Thiết lập hiển thị thường/đảo, 0xA6 thường, 0xA7 đảo
OLED_WriteCommand(0x8D); // Thiết lập bơm sạc
OLED_WriteCommand(0x14);
OLED_WriteCommand(0xAF); // Mở hiển thị
OLED_Clear(); // Xóa mảng nhớ hiển thị
OLED_Update(); // Cập nhật hiển thị, xóa màn hình, tránh nhiễu sau khởi tạo
}
/**
* @brief Đặt vị trí con trỏ hiển thị OLED
* @param Page chỉ định trang, 0-7
* @param X chỉ định tọa độ trục X, 0-127
* @return không
* @note Trục Y mặc định của OLED chỉ ghi theo nhóm 8 bit, 1 trang = 8 tọa độ Y
*/
void OLED_SetCursor(uint8_t Page, uint8_t X)
{
/*Nếu dùng OLED 1.3 inch, bỏ comment dòng dưới*/
/*Vi điều khiển SH1106 có 132 cột*/
/*Màn hình bắt đầu từ cột 2, không phải 0*/
/*Cần cộng thêm 2 cho X để hiển thị đúng*/
// X += 2;
/*Đặt địa chỉ trang và cột bằng lệnh*/
OLED_WriteCommand(0xB0 | Page); // Đặt trang
OLED_WriteCommand(0x10 | ((X & 0xF0) >> 4)); // 4 bit cao của X
OLED_WriteCommand(0x00 | (X & 0x0F)); // 4 bit thấp của X
}/*********************Cấu hình phần cứng*/
/*Hàm tiện ích*********************/
/*Hàm tiện ích chỉ dùng nội bộ cho một số hàm*/
/**
* @brief Hàm lũy thừa
* @param X Cơ số
* @param Y Số mũ
* @return Bằng X mũ Y
*/
uint32_t OLED_Pow(uint32_t X, uint32_t Y)
{
uint32_t Result = 1; // Kết quả mặc định là 1
while (Y--) // Nhân lặp Y lần
{
Result *= X; // Mỗi lần nhân X vào kết quả
}
return Result;
}
/**
* @brief Kiểm tra điểm chỉ định có nằm trong đa giác chỉ định hay không
* @param nvert Số đỉnh đa giác
* @param vertx verty Mảng chứa tọa độ x và y của các đỉnh đa giác
* @param testx testy Tọa độ X và y của điểm cần kiểm tra
* @return Điểm chỉ định có nằm trong đa giác chỉ định hay không, 1: có, 0: không
*/
uint8_t OLED_pnpoly(uint8_t nvert, int16_t *vertx, int16_t *verty, int16_t testx, int16_t testy)
{
int16_t i, j, c = 0;
/*Thuật toán này do W. Randolph Franklin đề xuất*/
/*Tham khảo: https://wrfranklin.org/Research/Short_Notes/pnpoly.html*/
for (i = 0, j = nvert - 1; i < nvert; j = i++) {
if (((verty[i] > testy) != (verty[j] > testy)) &&
(testx < (vertx[j] - vertx[i]) * (testy - verty[i]) / (verty[j] - verty[i]) + vertx[i])) {
c = !c;
}
}
return c;
}
/**
* @brief Kiểm tra điểm chỉ định có nằm trong góc chỉ định hay không
* @param X Y Tọa độ điểm chỉ định
* @param StartAngle EndAngle Góc bắt đầu và góc kết thúc, phạm vi: -180-180
* Phía ngang bên phải là 0 độ, phía ngang bên trái là 180 độ hoặc -180 độ, phía dưới là số dương, phía trên là số âm, quay chiều kim đồng hồ
* @return Điểm chỉ định có nằm trong góc chỉ định hay không, 1: có, 0: không
*/
uint8_t OLED_IsInAngle(int16_t X, int16_t Y, int16_t StartAngle, int16_t EndAngle)
{
int16_t PointAngle;
PointAngle = atan2(Y, X) / 3.14 * 180; // Tính radian của điểm chỉ định và chuyển sang độ
if (StartAngle < EndAngle) // Trường hợp góc bắt đầu nhỏ hơn góc kết thúc
{
/*Nếu góc chỉ định nằm giữa góc bắt đầu và kết thúc thì xác định điểm nằm trong góc*/
if (PointAngle >= StartAngle && PointAngle <= EndAngle) {
return 1;
}
} else // Trường hợp góc bắt đầu lớn hơn góc kết thúc
{
/*Nếu góc chỉ định lớn hơn góc bắt đầu hoặc nhỏ hơn góc kết thúc thì xác định điểm nằm trong góc*/
if (PointAngle >= StartAngle || PointAngle <= EndAngle) {
return 1;
}
}
return 0; // Không thỏa mãn điều kiện trên thì xác định điểm không nằm trong góc
}
/*********************Hàm tiện ích*/
/*Hàm chức năng*********************/
/**
* @brief Cập nhật mảng bộ nhớ hiển thị OLED lên màn hình OLED
* @param Không
* @return Không
* @note Tất cả các hàm hiển thị chỉ đọc/ghi mảng bộ nhớ hiển thị OLED
* Sau đó gọi hàm OLED_Update hoặc OLED_UpdateArea
* mới gửi dữ liệu mảng nhớ đến phần cứng OLED để hiển thị
* Vì vậy sau khi gọi hàm hiển thị, muốn hiển thị thực sự cần gọi thêm hàm cập nhật
*/
void OLED_Update(void)
{
uint8_t j;
/*Duyệt từng trang*/
for (j = 0; j < 8; j++) {
/*Đặt vị trí con trỏ là cột đầu tiên của mỗi trang*/
OLED_SetCursor(j, 0);
/*Ghi liên tiếp 128 byte dữ liệu, ghi dữ liệu mảng nhớ vào phần cứng OLED*/
OLED_WriteData(OLED_DisplayBuf[j], 128);
}
}
/**
* @brief Cập nhật một phần mảng bộ nhớ hiển thị OLED lên màn hình OLED
* @param X Tọa độ X góc trên-trái vùng chỉ định, phạm vi: 0-127
* @param Y Tọa độ Y góc trên-trái vùng chỉ định, phạm vi: 0-63
* @param Width Chiều rộng vùng chỉ định, phạm vi: 0-128
* @param Height Chiều cao vùng chỉ định, phạm vi: 0-64
* @return Không
* @note Hàm này sẽ cập nhật ít nhất vùng chỉ định
* Nếu vùng cập nhật trục Y chỉ bao gồm một phần trang, phần còn lại của cùng trang sẽ được cập nhật theo
* @note Tất cả các hàm hiển thị chỉ đọc/ghi mảng bộ nhớ hiển thị OLED
* Sau đó gọi hàm OLED_Update hoặc OLED_UpdateArea
* mới gửi dữ liệu mảng nhớ đến phần cứng OLED để hiển thị
* Vì vậy sau khi gọi hàm hiển thị, muốn hiển thị thực sự cần gọi thêm hàm cập nhật
*/
void OLED_UpdateArea(uint8_t X, uint8_t Y, uint8_t Width, uint8_t Height)
{
uint8_t j;
/*Kiểm tra tham số, đảm bảo vùng chỉ định không vượt khung hình*/
if (X > 127) { return; }
if (Y > 63) { return; }
if (X + Width > 128) { Width = 128 - X; }
if (Y + Height > 64) { Height = 64 - Y; }
/*Duyệt các trang liên quan đến vùng chỉ định*/
/*(Y + Height - 1) / 8 + 1 nhằm (Y + Height) / 8 và làm tròn lên*/
for (j = Y / 8; j < (Y + Height - 1) / 8 + 1; j++) {
/*Đặt vị trí con trỏ là cột chỉ định của trang liên quan*/
OLED_SetCursor(j, X);
/*Ghi liên tiếp Width byte dữ liệu, ghi dữ liệu mảng nhớ vào phần cứng OLED*/
OLED_WriteData(&OLED_DisplayBuf[j][X], Width);
}
}
/**
* @brief Xóa toàn bộ mảng bộ nhớ hiển thị OLED
* @param Không
* @return Không
* @note Sau khi gọi hàm này, muốn hiển thị thực sự cần gọi thêm hàm cập nhật
*/
void OLED_Clear(void)
{
uint8_t i, j;
for (j = 0; j < 8; j++) // Duyệt 8 trang
{
for (i = 0; i < 128; i++) // Duyệt 128 cột
{
OLED_DisplayBuf[j][i] = 0x00; // Gán toàn bộ mảng nhớ bằng 0
}
}
}
/**
* @brief Xóa một phần mảng bộ nhớ hiển thị OLED
* @param X Tọa độ X góc trên-trái vùng chỉ định, phạm vi: 0-127
* @param Y Tọa độ Y góc trên-trái vùng chỉ định, phạm vi: 0-63
* @param Width Chiều rộng vùng chỉ định, phạm vi: 0-128
* @param Height Chiều cao vùng chỉ định, phạm vi: 0-64
* @return Không
* @note Sau khi gọi hàm này, muốn hiển thị thực sự cần gọi thêm hàm cập nhật
*/
void OLED_ClearArea(uint8_t X, uint8_t Y, uint8_t Width, uint8_t Height)
{
uint8_t i, j;
/*Kiểm tra tham số, đảm bảo vùng chỉ định không vượt khung hình*/
if (X > 127) { return; }
if (Y > 63) { return; }
if (X + Width > 128) { Width = 128 - X; }
if (Y + Height > 64) { Height = 64 - Y; }
for (j = Y; j < Y + Height; j++) // Duyệt các trang chỉ định
{
for (i = X; i < X + Width; i++) // Duyệt các cột chỉ định
{
OLED_DisplayBuf[j / 8][i] &= ~(0x01 << (j % 8)); // Xóa bit dữ liệu chỉ định trong mảng nhớ
}
}
}
/**
* @brief Đảo bit toàn bộ mảng bộ nhớ hiển thị OLED
* @param Không
* @return Không
* @note Sau khi gọi hàm này, muốn hiển thị thực sự cần gọi thêm hàm cập nhật
*/
void OLED_Reverse(void)
{
uint8_t i, j;
for (j = 0; j < 8; j++) // Duyệt 8 trang
{
for (i = 0; i < 128; i++) // Duyệt 128 cột
{
OLED_DisplayBuf[j][i] ^= 0xFF; // Đảo bit toàn bộ dữ liệu mảng nhớ
}
}
}
/**
* @brief Đảo bit một phần mảng bộ nhớ hiển thị OLED
* @param X Tọa độ X góc trên-trái vùng chỉ định, phạm vi: 0-127
* @param Y Tọa độ Y góc trên-trái vùng chỉ định, phạm vi: 0-63
* @param Width Chiều rộng vùng chỉ định, phạm vi: 0-128
* @param Height Chiều cao vùng chỉ định, phạm vi: 0-64
* @return Không
* @note Sau khi gọi hàm này, muốn hiển thị thực sự cần gọi thêm hàm cập nhật
*/
void OLED_ReverseArea(uint8_t X, uint8_t Y, uint8_t Width, uint8_t Height)
{
uint8_t i, j;
/*Kiểm tra tham số, đảm bảo vùng chỉ định không vượt khung hình*/
if (X > 127) { return; }
if (Y > 63) { return; }
if (X + Width > 128) { Width = 128 - X; }
if (Y + Height > 64) { Height = 64 - Y; }
for (j = Y; j < Y + Height; j++) // Duyệt các trang chỉ định
{
for (i = X; i < X + Width; i++) // Duyệt các cột chỉ định
{
OLED_DisplayBuf[j / 8][i] ^= 0x01 << (j % 8); // Đảo bit dữ liệu chỉ định trong mảng nhớ
}
}
}
/**
* @brief OLED hiển thị một ký tự
* @param X Tọa độ X góc trên-trái ký tự, phạm vi: 0-127
* @param Y Tọa độ Y góc trên-trái ký tự, phạm vi: 0-63
* @param Char Ký tự cần hiển thị, phạm vi: ký tự ASCII có thể nhìn thấy
* @param FontSize Cỡ chữ
* Phạm vi: OLED_8X16\t\tRộng 8 pixel, cao 16 pixel
* OLED_6X8\t\tRộng 6 pixel, cao 8 pixel
* @return Không
* @note Sau khi gọi hàm này, muốn hiển thị thực sự cần gọi thêm hàm cập nhật
*/
void OLED_ShowChar(uint8_t X, uint8_t Y, char Char, uint8_t FontSize)
{
if (FontSize == OLED_8X16) // Cỡ chữ rộng 8 pixel, cao 16 pixel
{
/*Hiển thị dữ liệu chỉ định từ bộ font ASCII OLED_F8x16 dạng ảnh 8*16*/
OLED_ShowImage(X, Y, 8, 16, OLED_F8x16[Char - ' ']);
} else if (FontSize == OLED_6X8) // Cỡ chữ rộng 6 pixel, cao 8 pixel
{
/*Hiển thị dữ liệu chỉ định từ bộ font ASCII OLED_F6x8 dạng ảnh 6*8*/
OLED_ShowImage(X, Y, 6, 8, OLED_F6x8[Char - ' ']);
}
}
/**
* @brief OLED hiển thị chuỗi
* @param X Tọa độ X góc trên-trái chuỗi, phạm vi: 0-127
* @param Y Tọa độ Y góc trên-trái chuỗi, phạm vi: 0-63
* @param String Chuỗi cần hiển thị, phạm vi: chuỗi gồm ký tự ASCII có thể nhìn thấy
* @param FontSize Cỡ chữ
* Phạm vi: OLED_8X16\t\tRộng 8 pixel, cao 16 pixel
* OLED_6X8\t\tRộng 6 pixel, cao 8 pixel
* @return Không
* @note Sau khi gọi hàm này, muốn hiển thị thực sự cần gọi thêm hàm cập nhật
*/
void OLED_ShowString(uint8_t X, uint8_t Y, char *String, uint8_t FontSize)
{
uint8_t i;
for (i = 0; String[i] != '\0'; i++) // Duyệt từng ký tự chuỗi
{
/*Gọi OLED_ShowChar, hiển thị lần lượt từng ký tự*/
OLED_ShowChar(X + i * FontSize, Y, String[i], FontSize);
}
}
/**
* @brief OLED hiển thị số (thập phân, số nguyên dương)
* @param X Tọa độ X góc trên-trái số, phạm vi: 0-127
* @param Y Tọa độ Y góc trên-trái số, phạm vi: 0-63
* @param Number Số cần hiển thị, phạm vi: 0-4294967295
* @param Length Độ dài số, phạm vi: 0-10
* @param FontSize Cỡ chữ
* Phạm vi: OLED_8X16\t\tRộng 8 pixel, cao 16 pixel
* OLED_6X8\t\tRộng 6 pixel, cao 8 pixel
* @return Không
* @note Sau khi gọi hàm này, muốn hiển thị thực sự cần gọi thêm hàm cập nhật
*/
void OLED_ShowNum(uint8_t X, uint8_t Y, uint32_t Number, uint8_t Length, uint8_t FontSize)
{
uint8_t i;
for (i = 0; i < Length; i++) // Duyệt từng chữ số
{
/*Gọi OLED_ShowChar, hiển thị lần lượt từng chữ số*/
/*Number / OLED_Pow(10, Length - i - 1) % 10 trích xuất thập phân từng chữ số*/
/*+ '0' chuyển số sang dạng ký tự*/
OLED_ShowChar(X + i * FontSize, Y, Number / OLED_Pow(10, Length - i - 1) % 10 + '0', FontSize);
}
}
/**
* @brief OLED hiển thị số có dấu (thập phân, số nguyên)
* @param X Tọa độ X góc trên-trái số, phạm vi: 0-127
* @param Y Tọa độ Y góc trên-trái số, phạm vi: 0-63
* @param Number Số cần hiển thị, phạm vi: -2147483648-2147483647
* @param Length Độ dài số, phạm vi: 0-10
* @param FontSize Cỡ chữ
* Phạm vi: OLED_8X16\t\tRộng 8 pixel, cao 16 pixel
* OLED_6X8\t\tRộng 6 pixel, cao 8 pixel
* @return Không
* @note Sau khi gọi hàm này, muốn hiển thị thực sự cần gọi thêm hàm cập nhật
*/
void OLED_ShowSignedNum(uint8_t X, uint8_t Y, int32_t Number, uint8_t Length, uint8_t FontSize)
{
uint8_t i;
uint32_t Number1;
if (Number >= 0) // Số lớn hơn hoặc bằng 0
{
OLED_ShowChar(X, Y, '+', FontSize); // Hiển thị dấu +
Number1 = Number; // Number1 bằng Number
} else // Số nhỏ hơn 0
{
OLED_ShowChar(X, Y, '-', FontSize); // Hiển thị dấu -
Number1 = -Number; // Number1 bằng Number đổi dấu
}
}for (i = 0; i < Length; i++) // duyệt từng chữ số
{
/*gọi hàm OLED_ShowChar, hiển thị lần lượt từng chữ số*/
/*Number1 / OLED_Pow(10, Length - i - 1) % 10 có thể trích từng chữ số thập phân*/
/*+ '0' chuyển số thành ký tự*/
OLED_ShowChar(X + (i + 1) * FontSize, Y, Number1 / OLED_Pow(10, Length - i - 1) % 10 + '0', FontSize);
}
}
/**
* @brief OLED hiển thị số thập lục phân (số nguyên dương)
* @param X tọa độ x góc trái trên, giới hạn: 0~127
* @param Y tọa độ y góc trái trên, giới hạn: 0~63
* @param Number số cần hiển thị, giới hạn: 0x00000000~0xFFFFFFFF
* @param Length độ dài số, giới hạn: 0~8
* @param FontSize cỡ chữ
* giới hạn: OLED_8X16 rộng 8 pixel, cao 16 pixel
* OLED_6X8 rộng 6 pixel, cao 8 pixel
* @return không
* @note sau khi gọi, muốn hiển thị thật sự cần gọi hàm cập nhật
*/
void OLED_ShowHexNum(uint8_t X, uint8_t Y, uint32_t Number, uint8_t Length, uint8_t FontSize)
{
uint8_t i, SingleNumber;
for (i = 0; i < Length; i++) // duyệt từng chữ số
{
/*trích từng chữ số thập lục phân*/
SingleNumber = Number / OLED_Pow(16, Length - i - 1) % 16;
if (SingleNumber < 10) // chữ số nhỏ hơn 10
{
/*gọi OLED_ShowChar, hiển thị chữ số*/
/*+ '0' chuyển số thành ký tự*/
OLED_ShowChar(X + i * FontSize, Y, SingleNumber + '0', FontSize);
} else // chữ số lớn hơn 10
{
/*gọi OLED_ShowChar, hiển thị chữ số*/
/*+ 'A' chuyển số thành ký tự thập lục phân từ A*/
OLED_ShowChar(X + i * FontSize, Y, SingleNumber - 10 + 'A', FontSize);
}
}
}
/**
* @brief OLED hiển thị số nhị phân (số nguyên dương)
* @param X tọa độ x góc trái trên, giới hạn: 0~127
* @param Y tọa độ y góc trái trên, giới hạn: 0~63
* @param Number số cần hiển thị, giới hạn: 0x00000000~0xFFFFFFFF
* @param Length độ dài số, giới hạn: 0~16
* @param FontSize cỡ chữ
* giới hạn: OLED_8X16 rộng 8 pixel, cao 16 pixel
* OLED_6X8 rộng 6 pixel, cao 8 pixel
* @return không
* @note sau khi gọi, muốn hiển thị thật sự cần gọi hàm cập nhật
*/
void OLED_ShowBinNum(uint8_t X, uint8_t Y, uint32_t Number, uint8_t Length, uint8_t FontSize)
{
uint8_t i;
for (i = 0; i < Length; i++) // duyệt từng chữ số
{
/*gọi OLED_ShowChar, hiển thị lần lượt từng bit*/
/*Number / OLED_Pow(2, Length - i - 1) % 2 trích từng bit nhị phân*/
/*+ '0' chuyển bit thành ký tự*/
OLED_ShowChar(X + i * FontSize, Y, Number / OLED_Pow(2, Length - i - 1) % 2 + '0', FontSize);
}
}
/**
* @brief OLED hiển thị số thực (thập phân, có dấu phẩy)
* @param X tọa độ x góc trái trên, giới hạn: 0-127
* @param Y tọa độ y góc trái trên, giới hạn: 0-63
* @param Number số cần hiển thị, giới hạn: -4294967295.0-4294967295.0
* @param IntLength độ dài phần nguyên, giới hạn: 0-10
* @param FraLength độ dài phần thập phân, giới hạn: 0-9, làm tròn
* @param FontSize cỡ chữ
* giới hạn: OLED_8X16 rộng 8 pixel, cao 16 pixel
* OLED_6X8 rộng 6 pixel, cao 8 pixel
* @return không
* @note sau khi gọi, muốn hiển thị thật sự cần gọi hàm cập nhật
*/
void OLED_ShowFloatNum(uint8_t X, uint8_t Y, double Number, uint8_t IntLength, uint8_t FraLength, uint8_t FontSize)
{
uint32_t PowNum, IntNum, FraNum;
if (Number >= 0) // số không âm
{
OLED_ShowChar(X, Y, '+', FontSize); // hiện dấu +
} else // số âm
{
OLED_ShowChar(X, Y, '-', FontSize); // hiện dấu -
Number = -Number; // lấy giá trị tuyệt đối
}
/*tách phần nguyên và phần thập phân*/
IntNum = Number; // gán vào biến nguyên
Number -= IntNum; // bỏ phần nguyên
PowNum = OLED_Pow(10, FraLength); // hệ số nhân theo số chữ số thập phân
FraNum = round(Number * PowNum); // nhân và làm tròn
IntNum += FraNum / PowNum; // nếu làm tròn có nhớ thì cộng vào nguyên
/*hiển thị phần nguyên*/
OLED_ShowNum(X + FontSize, Y, IntNum, IntLength, FontSize);
/*hiển thị dấu chấm*/
OLED_ShowChar(X + (IntLength + 1) * FontSize, Y, '.', FontSize);
/*hiển thị phần thập phân*/
OLED_ShowNum(X + (IntLength + 2) * FontSize, Y, FraNum, FraLength, FontSize);
}
/**
* @brief OLED hiển thị chuỗi chữ Hán
* @param X tọa độ x góc trái trên, giới hạn: 0-127
* @param Y tọa độ y góc trái trên, giới hạn: 0-63
* @param Chinese chuỗi chữ Hán, giới hạn: toàn bộ là chữ Hán hoặc ký tự toàn giác, không ký tự nửa giác
* chữ Hán phải được định nghĩa trong mảng OLED_CF16x16 của OLED_Data.c
* nếu không tìm thấy sẽ hiện hình mặc định (khung có dấu ?)
* @return không
* @note sau khi gọi, muốn hiển thị thật sự cần gọi hàm cập nhật
*/
void OLED_ShowChinese(uint8_t X, uint8_t Y, char *Chinese)
{
uint8_t pChinese = 0;
uint8_t pIndex;
uint8_t i;
char SingleChinese[OLED_CHN_CHAR_WIDTH + 1] = {0}; // mã UTF8 3 byte, +1 cho '\0'
for (i = 0; Chinese[i] != '\0'; i++) // duyệt chuỗi
{
SingleChinese[pChinese] = Chinese[i]; // copy từng byte
pChinese++; // tăng chỉ số
/*đủ 3 byte thì có 1 chữ Hán hoàn chỉnh*/
if (pChinese >= OLED_CHN_CHAR_WIDTH) {
SingleChinese[pChinese + 1] = '\0'; // kết thúc chuỗi
pChinese = 0; // reset
/*tìm trong bộ nhớ chữ Hán*/
/*nếu gặp chuỗi rỗng thì dừng*/
for (pIndex = 0; strcmp(OLED_CF16x16[pIndex].Index, "") != 0; pIndex++) {
/*tìm thấy chữ khớp*/
if (strcmp(OLED_CF16x16[pIndex].Index, SingleChinese) == 0) {
break; // thoát, pIndex là chỉ số chữ Hán
}
}
/*hiển thị bitmap 16×16 từ bộ nhớ*/
OLED_ShowImage(X + ((i + 1) / OLED_CHN_CHAR_WIDTH - 1) * 16, Y, 16, 16, OLED_CF16x16[pIndex].Data);
}
}
}
/**
* @brief OLED hiển thị ảnh
* @param X tọa độ x góc trái trên, giới hạn: 0-127
* @param Y tọa độ y góc trái trên, giới hạn: 0-63
* @param Width chiều rộng ảnh, giới hạn: 0-128
* @param Height chiều cao ảnh, giới hạn: 0-64
* @param Image con trỏ ảnh
* @return không
* @note sau khi gọi, muốn hiển thị thật sự cần gọi hàm cập nhật
*/
void OLED_ShowImage(uint8_t X, uint8_t Y, uint8_t Width, uint8_t Height, const uint8_t *Image)
{
uint8_t i, j;
/*kiểm tra giới hạn, tránh tràn màn hình*/
if (X > 127) { return; }
if (Y > 63) { return; }
/*xóa vùng ảnh sẽ vẽ*/
OLED_ClearArea(X, Y, Width, Height);
/*duyệt các page liên quan*/
/*(Height - 1) / 8 + 1 để lấy số page cần thiết*/
for (j = 0; j < (Height - 1) / 8 + 1; j++) {
/*duyệt từng cột*/
for (i = 0; i < Width; i++) {
/*vượt biên thì bỏ qua*/
if (X + i > 127) { break; }
if (Y / 8 + j > 7) { return; }
/*hiển thị phần ảnh ở page hiện tại*/
OLED_DisplayBuf[Y / 8 + j][X + i] |= Image[j * Width + i] << (Y % 8);
/*nếu page sau vượt biên thì continue để page trước vẫn tiếp tục*/
if (Y / 8 + j + 1 > 7) { continue; }
/*hiển thị phần ảnh ở page kế tiếp*/
OLED_DisplayBuf[Y / 8 + j + 1][X + i] |= Image[j * Width + i] >> (8 - Y % 8);
}
}
}
/**
* @brief OLED dùng printf in chuỗi định dạng
* @param X tọa độ x góc trái trên, giới hạn: 0-127
* @param Y tọa độ y góc trái trên, giới hạn: 0-63
* @param FontSize cỡ chữ
* giới hạn: OLED_8X16 rộng 8 pixel, cao 16 pixel
* OLED_6X8 rộng 6 pixel, cao 8 pixel
* @param format chuỗi định dạng, giới hạn: ký tự ASCII
* @param ... danh sách tham số
* @return không
* @note sau khi gọi, muốn hiển thị thật sự cần gọi hàm cập nhật
*/
void OLED_Printf(uint8_t X, uint8_t Y, uint8_t FontSize, char *format, ...)
{
char String[30]; // mảng ký tự
va_list arg; // danh sách tham số biến đổi
va_start(arg, format); // bắt đầu từ format
vsprintf(String, format, arg); // in vào mảng
va_end(arg); // kết thúc
OLED_ShowString(X, Y, String, FontSize); // hiển thị chuỗi
}
/**
* @brief OLED vẽ 1 điểm tại tọa độ chỉ định
* @param X tọa độ x, giới hạn: 0-127
* @param Y tọa độ y, giới hạn: 0-63
* @return không
* @note sau khi gọi, muốn hiển thị thật sự cần gọi hàm cập nhật
*/
void OLED_DrawPoint(uint8_t X, uint8_t Y)
{
/*kiểm tra giới hạn*/
if (X > 127) { return; }
if (Y > 63) { return; }
/*đặt bit tương ứng trong buffer*/
OLED_DisplayBuf[Y / 8][X] |= 0x01 << (Y % 8);
}
/**
* @brief OLED lấy trạng thái 1 điểm
* @param X tọa độ x, giới hạn: 0-127
* @param Y tọa độ y, giới hạn: 0-63
* @return 1: sáng, 0: tắt
*/
uint8_t OLED_GetPoint(uint8_t X, uint8_t Y)
{
/*kiểm tra giới hạn*/
if (X > 127) { return 0; }
if (Y > 63) { return 0; }
/*kiểm tra bit*/
if (OLED_DisplayBuf[Y / 8][X] & 0x01 << (Y % 8)) {
return 1; // bit = 1
}
return 0; // bit = 0
}
/**
* @brief OLED vẽ đường thẳng
* @param X0 tọa độ x đầu mút 1, giới hạn: 0-127
* @param Y0 tọa độ y đầu mút 1, giới hạn: 0-63
* @param X1 tọa độ x đầu mút 2, giới hạn: 0-127
* @param Y1 tọa độ y đầu mút 2, giới hạn: 0-63
* @return không
* @note sau khi gọi, muốn hiển thị thật sự cần gọi hàm cập nhật
*/
void OLED_DrawLine(uint8_t X0, uint8_t Y0, uint8_t X1, uint8_t Y1)
{
int16_t x, y, dx, dy, d, incrE, incrNE, temp;
int16_t x0 = X0, y0 = Y0, x1 = X1, y1 = Y1;
uint8_t yflag = 0, xyflag = 0;if (y0 == y1) // xử lý riêng đường ngang
{
/*nếu tọa độ X điểm 0 lớn hơn tọa độ X điểm 1 thì hoán đổi tọa độ X của hai điểm*/
if (x0 > x1) {
temp = x0;
x0 = x1;
x1 = temp;
}
/*duyệt tọa độ X*/
for (x = x0; x <= x1; x++) {
OLED_DrawPoint(x, y0); // vẽ từng điểm
}
} else if (x0 == x1) // xử lý riêng đường dọc
{
/*nếu tọa độ Y điểm 0 lớn hơn tọa độ Y điểm 1 thì hoán đổi tọa độ Y của hai điểm*/
if (y0 > y1) {
temp = y0;
y0 = y1;
y1 = temp;
}
/*duyệt tọa độ Y*/
for (y = y0; y <= y1; y++) {
OLED_DrawPoint(x0, y); // vẽ từng điểm
}
} else // đường xiên
{
/*sử dụng thuật toán Bresenham vẽ đường thẳng, tránh phép toán dấu phẩy động tốn thời gian, hiệu quả hơn*/
/*tài liệu tham khảo: https://www.cs.montana.edu/courses/spring2009/425/dslectures/Bresenham.pdf*/
/*hướng dẫn tham khảo: https://www.bilibili.com/video/BV1364y1d7Lo*/
if (x0 > x1) // tọa độ X điểm 0 lớn hơn tọa độ X điểm 1
{
/*hoán đổi tọa độ hai điểm*/
/*sau khi hoán đổi không ảnh hưởng việc vẽ đường, nhưng hướng vẽ từ góc I,II,III,IV thành góc I,IV*/
temp = x0;
x0 = x1;
x1 = temp;
temp = y0;
y0 = y1;
y1 = temp;
}
if (y0 > y1) // tọa độ Y điểm 0 lớn hơn tọa độ Y điểm 1
{
/*lấy âm tọa độ Y*/
/*sau khi lấy âm ảnh hưởng việc vẽ, nhưng hướng vẽ từ góc I,IV thành góc I*/
y0 = -y0;
y1 = -y1;
/*đặt cờ yflag, nhớ phép biến đổi hiện tại, khi vẽ thực tế sau này sẽ đổi tọa độ lại*/
yflag = 1;
}
if (y1 - y0 > x1 - x0) // độ dốc đường lớn hơn 1
{
/*hoán đổi tọa độ X và Y*/
/*sau khi hoán đổi ảnh hưởng việc vẽ, nhưng hướng vẽ từ góc I 0~90 độ thành góc I 0~45 độ*/
temp = x0;
x0 = y0;
y0 = temp;
temp = x1;
x1 = y1;
y1 = temp;
/*đặt cờ xyflag, nhớ phép biến đổi hiện tại, khi vẽ thực tế sau này sẽ đổi tọa độ lại*/
xyflag = 1;
}
/*phía dưới là thuật toán Bresenham vẽ đường thẳng*/
/*thuật toán yêu cầu hướng vẽ phải thuộc góc I 0~45 độ*/
dx = x1 - x0;
dy = y1 - y0;
incrE = 2 * dy;
incrNE = 2 * (dy - dx);
d = 2 * dy - dx;
x = x0;
y = y0;
/*vẽ điểm bắt đầu, đồng thời kiểm tra cờ, đổi tọa độ lại*/
if (yflag && xyflag) {
OLED_DrawPoint(y, -x);
} else if (yflag) {
OLED_DrawPoint(x, -y);
} else if (xyflag) {
OLED_DrawPoint(y, x);
} else {
OLED_DrawPoint(x, y);
}
while (x < x1) // duyệt từng điểm trục X
{
x++;
if (d < 0) // điểm tiếp theo ở phía đông điểm hiện tại
{
d += incrE;
} else // điểm tiếp theo ở phía đông bắc điểm hiện tại
{
y++;
d += incrNE;
}
/*vẽ từng điểm, đồng thời kiểm tra cờ, đổi tọa độ lại*/
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 hình chữ nhật OLED
* @param X tọa độ x góc trên trái, phạm vi: 0~127
* @param Y tọa độ y góc trên trái, phạm vi: 0~63
* @param Width chiều rộng, phạm vi: 0~128
* @param Height chiều cao, phạm vi: 0~64
* @param IsFilled có tô đầy hay không
* phạm vi: OLED_UNFILLED\t\tkhông tô
* OLED_FILLED\t\t\ttô đầy
* @return không
* @note sau khi gọi hàm này, để hiển thị thực sự cần gọi hàm cập nhật
*/
void OLED_DrawRectangle(uint8_t X, uint8_t Y, uint8_t Width, uint8_t Height, uint8_t IsFilled)
{
uint8_t i, j;
if (!IsFilled) // không tô đầy
{
/*duyệt tọa độ X trên dưới, vẽ hai đường trên dưới*/
for (i = X; i < X + Width; i++) {
OLED_DrawPoint(i, Y);
OLED_DrawPoint(i, Y + Height - 1);
}
/*duyệt tọa độ Y trái phải, vẽ hai đường trái phải*/
for (i = Y; i < Y + Height; i++) {
OLED_DrawPoint(X, i);
OLED_DrawPoint(X + Width - 1, i);
}
} else // tô đầy
{
/*duyệt tọa độ X*/
for (i = X; i < X + Width; i++) {
/*duyệt tọa độ Y*/
for (j = Y; j < Y + Height; j++) {
/*vẽ điểm trong vùng chỉ định, tô đầy hình chữ nhật*/
OLED_DrawPoint(i, j);
}
}
}
}
/**
* @brief tam giác OLED
* @param X0 tọa độ x điểm 1, phạm vi: 0-127
* @param Y0 tọa độ y điểm 1, phạm vi: 0-63
* @param X1 tọa độ x điểm 2, phạm vi: 0-127
* @param Y1 tọa độ y điểm 2, phạm vi: 0-63
* @param X2 tọa độ x điểm 3, phạm vi: 0-127
* @param Y2 tọa độ y điểm 3, phạm vi: 0-63
* @param IsFilled có tô đầy hay không
* phạm vi: OLED_UNFILLED\t\tkhông tô
* OLED_FILLED\t\t\ttô đầy
* @return không
* @note sau khi gọi hàm này, để hiển thị thực sự cần gọi hàm cập nhật
*/
void OLED_DrawTriangle(uint8_t X0, uint8_t Y0, uint8_t X1, uint8_t Y1, uint8_t X2, uint8_t Y2, uint8_t IsFilled)
{
uint8_t minx = X0, miny = Y0, maxx = X0, maxy = Y0;
uint8_t i, j;
int16_t vx[] = {X0, X1, X2};
int16_t vy[] = {Y0, Y1, Y2};
if (!IsFilled) // không tô đầy
{
/*gọi hàm vẽ đường, nối ba điểm bằng đường thẳng*/
OLED_DrawLine(X0, Y0, X1, Y1);
OLED_DrawLine(X0, Y0, X2, Y2);
OLED_DrawLine(X1, Y1, X2, Y2);
} else // tô đầy
{
/*tìm tọa độ X, Y nhỏ nhất của ba điểm*/
if (X1 < minx) { minx = X1; }
if (X2 < minx) { minx = X2; }
if (Y1 < miny) { miny = Y1; }
if (Y2 < miny) { miny = Y2; }
/*tìm tọa độ X, Y lớn nhất của ba điểm*/
if (X1 > maxx) { maxx = X1; }
if (X2 > maxx) { maxx = X2; }
if (Y1 > maxy) { maxy = Y1; }
if (Y2 > maxy) { maxy = Y2; }
/*hình chữ nhật giữa tọa độ min max là vùng có thể cần tô*/
/*duyệt tất cả điểm trong vùng này*/
/*duyệt tọa độ X*/
for (i = minx; i <= maxx; i++) {
/*duyệt tọa độ Y*/
for (j = miny; j <= maxy; j++) {
/*gọi OLED_pnpoly, kiểm tra điểm có trong tam giác không*/
/*nếu có thì vẽ điểm, không thì bỏ qua*/
if (OLED_pnpoly(3, vx, vy, i, j)) { OLED_DrawPoint(i, j); }
}
}
}
}
/**
* @brief vẽ hình tròn OLED
* @param X tọa độ x tâm, phạm vi: 0~127
* @param Y tọa độ y tâm, phạm vi: 0~63
* @param Radius bán kính, phạm vi: 0~255
* @param IsFilled có tô đầy hay không
* phạm vi: OLED_UNFILLED\t\tkhông tô
* OLED_FILLED\t\t\ttô đầy
* @return không
* @note sau khi gọi hàm này, để hiển thị thực sự cần gọi hàm cập nhật
*/
void OLED_DrawCircle(uint8_t X, uint8_t Y, uint8_t Radius, uint8_t IsFilled)
{
int16_t x, y, d, j;
/*dùng thuật toán Bresenham vẽ hình tròn, tránh phép toán dấu phẩy động tốn thời gian, hiệu quả hơn*/
/*tài liệu tham khảo: https://www.cs.montana.edu/courses/spring2009/425/dslectures/Bresenham.pdf*/
/*hướng dẫn tham khảo: https://www.bilibili.com/video/BV1VM4y1u7wJ*/
d = 1 - Radius;
x = 0;
y = Radius;
/*vẽ điểm bắt đầu mỗi 1/8 đường tròn*/
OLED_DrawPoint(X + x, Y + y);
OLED_DrawPoint(X - x, Y - y);
OLED_DrawPoint(X + y, Y + x);
OLED_DrawPoint(X - y, Y - x);
if (IsFilled) // tô đầy
{
/*duyệt tọa độ Y bắt đầu*/
for (j = -y; j < y; j++) {
/*vẽ điểm trong vùng chỉ định, tô một phần hình tròn*/
OLED_DrawPoint(X, Y + j);
}
}
while (x < y) // duyệt từng điểm trục X
{
x++;
if (d < 0) // điểm tiếp theo ở phía đông điểm hiện tại
{
d += 2 * x + 1;
} else // điểm tiếp theo ở phía đông nam điểm hiện tại
{
y--;
d += 2 * (x - y) + 1;
}
/*vẽ các điểm mỗi 1/8 đường tròn*/
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) // tô đầy
{
/*duyệt phần giữa*/
for (j = -y; j < y; j++) {
/*vẽ điểm trong vùng chỉ định, tô một phần hình tròn*/
OLED_DrawPoint(X + x, Y + j);
OLED_DrawPoint(X - x, Y + j);
}
/*duyệt hai bên*/
for (j = -x; j < x; j++) {
/*vẽ điểm trong vùng chỉ định, tô một phần hình tròn*/
OLED_DrawPoint(X - y, Y + j);
OLED_DrawPoint(X + y, Y + j);
}
}
}
}
/**
* @brief vẽ hình elip OLED
* @param X tọa độ x tâm, phạm vi: 0~127
* @param Y tọa độ y tâm, phạm vi: 0~63
* @param A bán trục ngang, phạm vi: 0~255
* @param B bán trục dọc, phạm vi: 0~255
* @param IsFilled có tô đầy hay không
* phạm vi: OLED_UNFILLED\t\tkhông tô
* OLED_FILLED\t\t\ttô đầy
* @return không
* @note sau khi gọi hàm này, để hiển thị thực sự cần gọi hàm cập nhật
*/
void OLED_DrawEllipse(uint8_t X, uint8_t Y, uint8_t A, uint8_t B, uint8_t IsFilled)
{
int16_t x, y, j;
int16_t a = A, b = B;
float d1, d2;
/*dùng thuật toán Bresenham vẽ hình elip, tránh một phần phép toán dấu phẩy động tốn thời gian, hiệu quả hơn*/
/*liên kết tham khảo: https://blog.csdn.net/myf_666/article/details/128167392*/
x = 0;
y = b;
d1 = b * b + a * a * (-b + 0.5);```c
if (IsFilled) // chỉ định tô màu ellipse
{
/*duyệt tọa độ Y điểm bắt đầu*/
for (j = -y; j < y; j++) {
/*vẽ điểm trong vùng chỉ định, tô phần ellipse*/
OLED_DrawPoint(X, Y + j);
OLED_DrawPoint(X, Y + j);
}
}
/*vẽ điểm bắt đầu cung ellipse*/
OLED_DrawPoint(X + x, Y + y);
OLED_DrawPoint(X - x, Y - y);
OLED_DrawPoint(X - x, Y + y);
OLED_DrawPoint(X + x, Y - y);
/*vẽ phần giữa ellipse*/
while (b * b * (x + 1) < a * a * (y - 0.5)) {
if (d1 <= 0) // điểm tiếp theo ở phía đông điểm hiện tại
{
d1 += b * b * (2 * x + 3);
} else // điểm tiếp theo ở phía đông nam điểm hiện tại
{
d1 += b * b * (2 * x + 3) + a * a * (-2 * y + 2);
y--;
}
x++;
if (IsFilled) // chỉ định tô màu ellipse
{
/*duyệt phần giữa*/
for (j = -y; j < y; j++) {
/*vẽ điểm trong vùng chỉ định, tô phần ellipse*/
OLED_DrawPoint(X + x, Y + j);
OLED_DrawPoint(X - x, Y + j);
}
}
/*vẽ cung ellipse phần giữa*/
OLED_DrawPoint(X + x, Y + y);
OLED_DrawPoint(X - x, Y - y);
OLED_DrawPoint(X - x, Y + y);
OLED_DrawPoint(X + x, Y - y);
}
/*vẽ hai bên ellipse*/
d2 = b * b * (x + 0.5) * (x + 0.5) + a * a * (y - 1) * (y - 1) - a * a * b * b;
while (y > 0) {
if (d2 <= 0) // điểm tiếp theo ở phía đông điểm hiện tại
{
d2 += b * b * (2 * x + 2) + a * a * (-2 * y + 3);
x++;
} else // điểm tiếp theo ở phía đông nam điểm hiện tại
{
d2 += a * a * (-2 * y + 3);
}
y--;
if (IsFilled) // chỉ định tô màu ellipse
{
/*duyệt hai bên*/
for (j = -y; j < y; j++) {
/*vẽ điểm trong vùng chỉ định, tô phần ellipse*/
OLED_DrawPoint(X + x, Y + j);
OLED_DrawPoint(X - x, Y + j);
}
}
/*vẽ cung ellipse hai bên*/
OLED_DrawPoint(X + x, Y + y);
OLED_DrawPoint(X - x, Y - y);
OLED_DrawPoint(X - x, Y + y);
OLED_DrawPoint(X + x, Y - y);
}
}
/**
* @brief Vẽ cung tròn OLED
* @param X tọa độ X tâm cung, phạm vi: 0~127
* @param Y tọa độ Y tâm cung, phạm vi: 0~63
* @param Radius bán kính cung, phạm vi: 0~255
* @param StartAngle góc bắt đầu cung, phạm vi: -180~180
* ngang sang phải là 0 độ, ngang sang trái là 180 hoặc -180 độ, dưới là dương, trên là âm, quay chiều kim đồng hồ
* @param EndAngle góc kết thúc cung, phạm vi: -180~180
* ngang sang phải là 0 độ, ngang sang trái là 180 hoặc -180 độ, dưới là dương, trên là âm, quay chiều kim đồng hồ
* @param IsFilled có tô màu cung không, tô thành hình quạt
* phạm vi: OLED_UNFILLED\t\tkhông tô
* OLED_FILLED\t\t\tcó tô
* @return không
* @note sau khi gọi hàm này, muốn hiển thị thực sự phải gọi hàm cập nhật
*/
void OLED_DrawArc(uint8_t X, uint8_t Y, uint8_t Radius, int16_t StartAngle, int16_t EndAngle, uint8_t IsFilled)
{
int16_t x, y, d, j;
/*hàm này mượn thuật toán Bresenham vẽ tròn*/
d = 1 - Radius;
x = 0;
y = Radius;
/*khi vẽ mỗi điểm tròn, kiểm tra điểm đó có trong góc chỉ định không, có thì vẽ, không thì bỏ qua*/
if (OLED_IsInAngle(x, y, StartAngle, EndAngle)) { OLED_DrawPoint(X + x, Y + y); }
if (OLED_IsInAngle(-x, -y, StartAngle, EndAngle)) { OLED_DrawPoint(X - x, Y - y); }
if (OLED_IsInAngle(y, x, StartAngle, EndAngle)) { OLED_DrawPoint(X + y, Y + x); }
if (OLED_IsInAngle(-y, -x, StartAngle, EndAngle)) { OLED_DrawPoint(X - y, Y - x); }
if (IsFilled) // chỉ định tô màu cung
{
/*duyệt tọa độ Y điểm bắt đầu*/
for (j = -y; j < y; j++) {
/*khi tô mỗi điểm tròn, kiểm tra điểm đó có trong góc chỉ định không, có thì vẽ, không thì bỏ qua*/
if (OLED_IsInAngle(0, j, StartAngle, EndAngle)) { OLED_DrawPoint(X, Y + j); }
}
}
while (x < y) // duyệt mỗi điểm trục X
{
x++;
if (d < 0) // điểm tiếp theo ở phía đông điểm hiện tại
{
d += 2 * x + 1;
} else // điểm tiếp theo ở phía đông nam điểm hiện tại
{
y--;
d += 2 * (x - y) + 1;
}
/*khi vẽ mỗi điểm tròn, kiểm tra điểm đó có trong góc chỉ định không, có thì vẽ, không thì bỏ qua*/
if (OLED_IsInAngle(x, y, StartAngle, EndAngle)) { OLED_DrawPoint(X + x, Y + y); }
if (OLED_IsInAngle(y, x, StartAngle, EndAngle)) { OLED_DrawPoint(X + y, Y + x); }
if (OLED_IsInAngle(-x, -y, StartAngle, EndAngle)) { OLED_DrawPoint(X - x, Y - y); }
if (OLED_IsInAngle(-y, -x, StartAngle, EndAngle)) { OLED_DrawPoint(X - y, Y - x); }
if (OLED_IsInAngle(x, -y, StartAngle, EndAngle)) { OLED_DrawPoint(X + x, Y - y); }
if (OLED_IsInAngle(y, -x, StartAngle, EndAngle)) { OLED_DrawPoint(X + y, Y - x); }
if (OLED_IsInAngle(-x, y, StartAngle, EndAngle)) { OLED_DrawPoint(X - x, Y + y); }
if (OLED_IsInAngle(-y, x, StartAngle, EndAngle)) { OLED_DrawPoint(X - y, Y + x); }
if (IsFilled) // chỉ định tô màu cung
{
/*duyệt phần giữa*/
for (j = -y; j < y; j++) {
/*khi tô mỗi điểm tròn, kiểm tra điểm đó có trong góc chỉ định không, có thì vẽ, không thì bỏ qua*/
if (OLED_IsInAngle(x, j, StartAngle, EndAngle)) { OLED_DrawPoint(X + x, Y + j); }
if (OLED_IsInAngle(-x, j, StartAngle, EndAngle)) { OLED_DrawPoint(X - x, Y + j); }
}
/*duyệt hai bên*/
for (j = -x; j < x; j++) {
/*khi tô mỗi điểm tròn, kiểm tra điểm đó có trong góc chỉ định không, có thì vẽ, không thì bỏ qua*/
if (OLED_IsInAngle(-y, j, StartAngle, EndAngle)) { OLED_DrawPoint(X - y, Y + j); }
if (OLED_IsInAngle(y, j, StartAngle, EndAngle)) { OLED_DrawPoint(X + y, Y + j); }
}
}
}
}
/*********************Hàm chức năng*/
/*****************Jiangxie Technology|Bản quyền****************/
/*****************jiangxiekeji.com*****************/
Đề xuất đọc thêm
- Giới thiệu VPS/đám mây giá rẻ, hiệu năng cao: https://blog.zeruns.com/archives/383.html
- Làm bộ thu thập điện năng 3 pha mã nguồn mở, dễ dàng giám sát điện nhà: https://blog.zeruns.com/archives/771.html
- Hướng dẫn mở máy chủ Minecraft: https://blog.zeruns.com/tag/mc/
- Hướng dẫn mở máy chủ Palworld: https://blog.zeruns.com/tag/PalWorld/
- Đánh giá nhanh nguồn DC điều khiển số Ruideng RD6012P, 60 V 12 A: https://blog.zeruns.com/archives/740.html
- Trải nghiệm mở hộp máy in 3D Bambu Lab P1SC: https://blog.zeruns.com/archives/770.html
- So sánh thực tế tụ và kháng nhiều loại, hãng (D, Q, ESR, X): https://blog.zeruns.com/archives/765.html








