Đã port thư viện đồ họa U8g2 cho mẫu dự án thư viện chuẩn STM32F407, sử dụng màn hình OLED 0,96 inch (SSD1306), điều khiển bằng phần cứng IIC.
Mất cả một tối để port. Board phát triển sử dụng MCU chính là STM32F407VET6, giao tiếp I2C dùng I2C1, SCL nối PB6, SDA nối PB7.
Bài viết liên quan đến nhúng: https://blog.zeruns.com/category/IOT/
Bài viết liên quan đến điện tử/mạch: https://blog.zeruns.com/category/electrical/
Nhóm kỹ thuật điện tử/vi điều khiển: 2169025065
Giới thiệu U8g2
Thư viện đồ họa U8g2 là thư viện đồ họa đơn sắc dành cho thiết bị nhúng, hỗ trợ nhiều bộ điều khiển màn hình OLED và LCD đơn sắc như SSD1306, ST7920, v.v. Thư viện U8g2 có thể cài quản trị thư viện Arduino IDE, cũng có thể port sang các nền tảng như STM32. U8g2 hỗ trợ ba chế độ vẽ: toàn màn hình bộ nhớ đệm, bộ nhớ đệm trang và chế độ ký tự U8x8. Khi dùng U8g2 cần chọn constructor phù hợp, khởi tạo màn hình, đặt số chân, viết hàm callback và lệnh vẽ.
Ưu điểm của U8g2: nhiều font chữ, hỗ trợ tiếng Trung, cung cấp nhiều hình vẽ như đường, khung, tròn, bitmap, v.v. Nhược điểm: chiếm không gian bộ nhớ, tốc độ chậm, không hỗ trợ màn hình không có bộ điều khiển. Ứng dụng: hiển thị dữ liệu cảm biến, đồng hồ, menu, hoạt hình, v.v. U8g2 là thư viện đồ họa đơn sắc mạnh, tương thích tốt, dễ dùng.
Hình ảnh demo
Video demo: https://www.bilibili.com/video/BV17W4y197WW/
Về vấn đề thạch anh
Mình dùng thạch anh 8 MHz, đã sửa tham số PLL trong cấu hình cây clock để MCU chạy 168 MHz. Nếu đổi thạch anh khác cần chỉnh lại tham số, cách sửa tự Google nhé.
Chỗ cần sửa như sau:
stm32f4xx.h dòng 137.
#define HSE_VALUE ((uint32_t)8000000) /*!< Value of the External oscillator in Hz */
// Thay 8000000 thành tần số thạch anh của bạn, đơn vị Hz
system_stm32f4xx.c dòng 364 và 394.
#if defined(STM32F40_41xxx) || defined(STM32F427_437xx) || defined(STM32F429_439xx) || defined(STM32F401xx) || defined(STM32F469_479xx)
/* PLL_VCO = (HSE_VALUE or HSI_VALUE / PLL_M) * PLL_N */
#define PLL_M 4
// 4 này tương ứng /M = /4 trong ảnh cây clock dưới
#if defined (STM32F40_41xxx)
#define PLL_N 168
// 168 này tương ứng *N = x168 trong ảnh cây clock dưới
```
## Địa chỉ mua linh kiện
- Màn hình OLED 0.96 inch: [https://u.jd.com/1zjpDNC](https://union-click.jd.com/jdc?e=618%7Cpc%7C&p=JF8BARAJK1olXwQCXVheC0IUBV8IGloTXw4AU1hVDkweBV9MRANLAjZbERscSkAJHTdNTwcKBlMdBgABFksWAmkKE1kSWw4EU1dbFxJSXzI4WzB2K1NjPSw9VzFBQBpJTg5iPBhWElJROEonA24JGloSWgAAXG5tCEwnQgEIGV4SXwcFVW5cOEsQCmgNHV0UWg4EUVttD0seM20IGFgQWgQLUUJUDEoUAGY4K2sWbQECXUpbegpFF2l6K2sVbQUyVF9dAEgXA2cOGFMJXQMGXF9cFEsQCmgNHV0UXAEFVlttCkoWB2Y4K2tVGQViISotXEpUR2xIeSNWKGJREh4DXxF5AS1dSD0SFQNXVQoHfghLSm8LKw)
- Board phát triển STM32F407: [https://s.click.taobao.com/7AtnHFu](https://s.click.taobao.com/t?e=m%3D2%26s%3DczQqv026Zjtw4vFB6t2Z2ueEDrYVVa64LKpWJ%2Bin0XLjf2vlNIV67lRwohxEizvIxa9spvDO8Cl%2FmzaS0s2vXCsmIF0UHtujBkxkgN7jKg%2BiJaQjFxYBevWfPAv4x3d%2FOlGkMkr%2BSUUYmUGuVZn0uRDBbf95h6seTxpmwkkouzqi1jMNxDhLMnotgd7NXRy%2F3OppJwt5ethtXyEYK9MgsI1cD97yFV3TmSpANgNSVXSPM7MeivsNdkAXdMlWzMNyHk3i9xXmKdKMJi1gYBJvNFSsXCNd9EoxE59iYTGkDbWFi4V5KBpmMoBZLSyIfgF7gR2l5BVRVVbGDmntuH4VtA%3D%3D&union_lens=lensId%3APUB%401686477136%40210562f5_0a55_188a9dee240_346f%4001%40eyJmbG9vcklkIjo2MTM1NCwiic3BtQiiI6Il9wb3J0YWxfdjJfcGFnZXNfcHJvbW9fZ29vZHNfaW5kZXhfaHRtIn0ie)
- Dây Dupont: [https://u.jd.com/1zjFP0Z](https://union-click.jd.com/jdc?e=618%7Cpc%7C&p=JF8BAQcJK1olVQAKVlZZCUoXM2oJElkcXwUHXVtYOA9IWzFXKwJQGEdAX0BDUA5DX3BTTkRHA1ocUV9UCkIVAGoBHl4KBENeCW4NDA9CAGcLXjlHWnBqEycFACNOCxZoF1clXDYCVV9cCUwQBW0AK2sVWjZDOllYAE0XAF8JK1sSVAEHUldZCU4fBWo4HFscbQQCV11YD0keBnMBH1oWXg8yZG5eOEwXCnsOaRpHSQBwZG5dOEgnA24IE1gQXwYHV1lBCEkWAW8IB1sSVAEHUlhdAUweCmY4GVoUWQ8yZG4YXDFkd2tXEjhHCXxLB1wAXzsVAmxfRAN7X19ZVgg8VhZMYB94fChoFEVmZA)
## Mã nguồn
Tải toàn bộ project: [https://url.zeruns.com/JUoKJ](https://url.zeruns.com/JUoKJ) Mã giải nén: t6wt
Một phần mã:
**main.c**
```C
#include "stm32f4xx.h"
// #include "Timer.h"
#include "Delay.h"
#include "u8g2.h"
#include "OLED.h"
#include "IWDG.h"
uint8_t u8x8_gpio_and_delay(U8X8_UNUSED u8x8_t *u8x8, U8X8_UNUSED uint8_t msg, U8X8_UNUSED uint8_t arg_int, U8X8_UNUSED void *arg_ptr);
uint8_t u8x8_byte_hw_i2c(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr);
void u8g2_Init(u8g2_t *u8g2);
void draw(u8g2_t *u8g2);
int main(void)
{
uint8_t t = 0;
IWDG_Configuration(); // Khởi tạo Watchdog
OLED_I2C_Init(); // Khởi tạo OLED
u8g2_t u8g2;
u8g2_Init(&u8g2); // Khởi tạo U8g2
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE); // Bật clock ngoại vi GPIOA
GPIO_InitTypeDef GPIO_InitStructure; // Khai báo cấu trúc
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT; // Đặt chế độ GPIO là ngõ ra
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; // Chân GPIO 6 và 7
GPIO_InitStructure.GPIO_Speed = GPIO_High_Speed; // Tốc độ 100 MHz
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; // Ngõ ra Push-Pull
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; // Không kéo lên/xuống
GPIO_Init(GPIOA, &GPIO_InitStructure); // Khởi tạo GPIO
Delay_ms(100);
u8g2_DrawLine(&u8g2, 0, 0, 127, 63); // Vẽ đường thẳng từ (0,0) đến (127,63)
u8g2_SendBuffer(&u8g2); // Gửi dữ liệu bộ đệm
u8g2_DrawLine(&u8g2, 127, 0, 0, 63);
u8g2_SendBuffer(&u8g2);
Delay_ms(300);
u8g2_ClearBuffer(&u8g2); // Xóa bộ đệm
draw(&u8g2);
u8g2_SendBuffer(&u8g2);
Delay_ms(1000);
u8g2_ClearBuffer(&u8g2);
IWDG_FeedDog(); // “Cho ăn” Watchdog, tránh reset CPU
u8g2_SetFont(&u8g2, u8g2_font_ncenB14_tr); // Chọn font
u8g2_DrawStr(&u8g2, 0, 15, "Hello World!");
u8g2_SetFont(&u8g2, u8g2_font_wqy16_t_chinese2);
u8g2_DrawUTF8(&u8g2, 0, 30, "H你好世界");
u8g2_SetFont(&u8g2, u8g2_font_wqy12_t_chinese2);
u8g2_DrawUTF8(&u8g2, 0, 43, "H你好世界");
u8g2_SetFont(&u8g2, u8g2_font_fur11_tr);
u8g2_DrawUTF8(&u8g2, 0, 59, "blog.zeruns.com");
u8g2_SendBuffer(&u8g2);
Delay_ms(1300);
while (1)
{
Delay_ms(100);
u8g2_ClearBuffer(&u8g2);// Xóa bộ đệm
if (++t >= 32)
t = 1;
u8g2_DrawCircle(&u8g2, 64, 32, t, U8G2_DRAW_ALL); // Vẽ hình tròn
u8g2_DrawCircle(&u8g2, 32, 32, t, U8G2_DRAW_ALL);
u8g2_DrawCircle(&u8g2, 96, 32, t, U8G2_DRAW_ALL);
u8g2_SendBuffer(&u8g2); // Gửi bộ đệm
GPIO_ToggleBits(GPIOA, GPIO_Pin_6);
IWDG_FeedDog(); // “Cho ăn” Watchdog
}
}
// https://blog.zeruns.com
uint8_t u8x8_byte_hw_i2c(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr)
{
static uint8_t buffer[32]; /* u8g2/u8x8 không bao giờ gửi quá 32 byte giữa START_TRANSFER và END_TRANSFER */
static uint8_t buf_idx;
uint8_t *data;
switch (msg)
{
case U8X8_MSG_BYTE_SEND:
data = (uint8_t *)arg_ptr;
while (arg_int > 0)
{
buffer[buf_idx++] = *data;
data++;
arg_int--;
}
break;
case U8X8_MSG_BYTE_INIT:
/* thêm mã tùy chỉnh để khởi tạo hệ thống I2C */
break;
case U8X8_MSG_BYTE_START_TRANSFER:
buf_idx = 0;
break;
case U8X8_MSG_BYTE_END_TRANSFER:
HW_I2cWrite(buffer, buf_idx); // Ghi byte qua I2C phần cứng
break;
default:
return 0;
}
return 1;
}
// https://blog.zeruns.com
uint8_t u8g2_gpio_and_delay(U8X8_UNUSED u8x8_t *u8x8, U8X8_UNUSED uint8_t msg, U8X8_UNUSED uint8_t arg_int, U8X8_UNUSED void *arg_ptr)
{
switch (msg)
{
case U8X8_MSG_GPIO_AND_DELAY_INIT:
OLED_I2C_Init(); // Khởi tạo
break;
case U8X8_MSG_DELAY_MILLI:
Delay_ms(arg_int); // Trì hoãn
break;
case U8X8_MSG_GPIO_I2C_CLOCK:
break;
case U8X8_MSG_GPIO_I2C_DATA:
break;
default:
return 0;
}
return 1; // lệnh xử lý thành công
}
void u8g2_Init(u8g2_t *u8g2)
{
// u8g2_Setup_ssd1306_i2c_128x64_noname_f(u8g2, U8G2_R0, u8x8_byte_sw_i2c, u8x8_gpio_and_delay); // khởi tạo u8g2, I2C mềm
u8g2_Setup_ssd1306_i2c_128x64_noname_f(u8g2, U8G2_R0, u8x8_byte_hw_i2c, u8g2_gpio_and_delay); // khởi tạo u8g2, I2C phần cứng
u8g2_InitDisplay(u8g2); // khởi tạo theo chip đã chọn, sau đó màn hình ở trạng thái tắt
u8g2_SetPowerSave(u8g2, 0); // bật màn hình
u8g2_SetContrast(u8g2, 88); // độ sáng
u8g2_ClearBuffer(u8g2); // xóa bộ đệm
}
// https://blog.vpszj.cn
void draw(u8g2_t *u8g2)
{
u8g2_SetFontMode(u8g2, 1); /*chọn chế độ font*/
u8g2_SetFontDirection(u8g2, 0); /*chọn hướng font*/
u8g2_SetFont(u8g2, u8g2_font_inb24_mf); /*chọn font*/
u8g2_DrawStr(u8g2, 0, 20, "U");
u8g2_SetFontDirection(u8g2, 1);
u8g2_SetFont(u8g2, u8g2_font_inb30_mn);
u8g2_DrawStr(u8g2, 21, 8, "8");
u8g2_SetFontDirection(u8g2, 0);
u8g2_SetFont(u8g2, u8g2_font_inb24_mf);
u8g2_DrawStr(u8g2, 51, 30, "g");
u8g2_DrawStr(u8g2, 67, 30, "\xb2");
u8g2_DrawHLine(u8g2, 2, 35, 47);
u8g2_DrawHLine(u8g2, 3, 36, 47);
u8g2_DrawVLine(u8g2, 45, 32, 12);
u8g2_DrawVLine(u8g2, 46, 33, 12);
u8g2_SetFont(u8g2, u8g2_font_4x6_tr);
u8g2_DrawStr(u8g2, 1, 54, "github.com/olikraus/u8g2");
}
```OLED.c
```C
#include "stm32f4xx.h"
#include "OLED_Font.h"
#include "Delay.h"
/*Địa chỉ màn hình OLED*/
#define OLED_ADDRESS 0x78
//Thời gian chờ timeout I2C
#define I2C_TIMEOUT 5000
/*Khởi tạo chân*/
void OLED_I2C_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1,ENABLE); //Bật clock I2C1
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB,ENABLE);//Bật clock GPIOB
GPIO_PinAFConfig(GPIOB,GPIO_PinSource6,GPIO_AF_I2C1); //Bật chức năng alternate PB6 nối tới I2C1
GPIO_PinAFConfig(GPIOB,GPIO_PinSource7,GPIO_AF_I2C1);
/*Phần cứng I2C của STM32F407: PB6 -- SCL; PB7 -- SDA */
GPIO_InitTypeDef GPIO_InitStructure; //Khai báo cấu trúc cấu hình GPIO
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; //Đặt chế độ GPIO thành chế độ AF
GPIO_InitStructure.GPIO_Speed = GPIO_High_Speed; //Tốc độ GPIO 100 MHz
GPIO_InitStructure.GPIO_OType = GPIO_OType_OD; //Đặt kiểu ra open-drain
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; //Kéo lên
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; //Chọn chân
GPIO_Init(GPIOB, &GPIO_InitStructure);
I2C_DeInit(I2C1); //Reset thanh ghi I2C1 về mặc định
I2C_InitTypeDef I2C_InitStructure;
I2C_InitStructure.I2C_Mode = I2C_Mode_I2C; //Chế độ làm việc
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2; //Tỷ lệ duty-cycle, Tlow/Thigh = 2
I2C_InitStructure.I2C_OwnAddress1 = 0x30; //Địa chỉ I2C của master, không dùng thì điền tùy ý
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable; //Bật bit ACK
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;//Độ dài địa chỉ 7 bit
I2C_InitStructure.I2C_ClockSpeed = 400000; //Tốc độ I2C 400 kHz, xem datasheet chip để biết tốc độ hỗ trợ
I2C_Init(I2C1, &I2C_InitStructure);
I2C_Cmd(I2C1, ENABLE);
}
// https://blog.zeruns.com
void HW_I2cWrite(uint8_t *buf,uint8_t len)
{
if(len<=0)
return ;
uint32_t wait_time=0;
/* chờ cờ busy bị xóa */
while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY))
{
wait_time++;
if(wait_time>=I2C_TIMEOUT){
wait_time=0;
break;
}
}
I2C_GenerateSTART(I2C1, ENABLE);//Bắt đầu I2C1
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT))/*EV5, chế độ master*/
{
wait_time++;
if(wait_time>=I2C_TIMEOUT){
wait_time=0;
break;
}
}
I2C_Send7bitAddress(I2C1, OLED_ADDRESS, I2C_Direction_Transmitter); //Địa chỉ thiết bị -- mặc định 0x78
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))
{
wait_time++;
if(wait_time>=I2C_TIMEOUT){
wait_time=0;
break;
}
}
for(uint8_t i=0;i<len;i++)
{
I2C_SendData(I2C1, buf[i]);//Gửi dữ liệu
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
{
wait_time++;
if(wait_time>=I2C_TIMEOUT){
wait_time=0;
break;
}
}
}
I2C_GenerateSTOP(I2C1, ENABLE);//Dừng bus I2C1
}
Dự án mã nguồn mở đề xuất
- Vẽ xong mạch tối thiểu MSP430F149, đã open-source: https://blog.zeruns.com/archives/713.html
- Mạch tối thiểu STM32F030C8T6 và đèn chạy (sơ đồ nguyên lý & PCB): https://blog.zeruns.com/archives/715.html
- Module nguồn bước xuống đồng bộ SY8205 có thể điều chỉnh (sơ đồ & PCB): https://blog.zeruns.com/archives/717.html
- Đề thi Điện toàn quốc 2011 – Hệ thống cấp nguồn mô-đun song song: https://blog.zeruns.com/archives/718.html
- Đề nguồn 2007: Module nâng áp 30-36 V điều chỉnh được (UC3843): https://oshwhub.com/zeruns/36v-sheng-ya-dcdc-mo-kuai-uc3842
Đọc thêm
- Giới thiệu VPS/cloud server giá rẻ, hiệu năng cao: https://blog.vpszj.cn/archives/41.html
- Cách dựng blog cá nhân: https://blog.zeruns.com/archives/218.html
- Hướng dẫn cài máy chủ Minecraft: https://blog.zeruns.com/tag/mc/
- STM32 đọc cảm biến nhiệt độ/độ ẩm SHT3x: https://blog.zeruns.com/archives/700.html
- Dùng VSCode thay Keil lập trình STM32 và 8051: https://blog.zeruns.com/archives/690.html
- Đánh giá hiệu năng cloud Hong Kong BGP野草云, 1C2G 100 M, chỉ 138 ¥/năm: https://blog.vpszj.cn/archives/1474.html
- Đánh giá cloud 5900X宿迁雨云, 2C4G 5 M, 150 G chống DDoS, chỉ 50 ¥/tháng: https://blog.vpszj.cn/archives/1125.html

