Hướng dẫn Điều Khiển Hiển Thị Màn Hình OLED (SSD1306 + STM32) Trong Keil MDK

Mục tiêu của hướng dẫn này là giải thích cách sử dụng Keil MDK để điều khiển màn hình OLED thông qua giao diện I2C và chip điều khiển SSD1306. Các bước chính bao gồm việc thiết lập môi trường dự án, viết các hàm điều khiển I2C, khởi tạo SSD1306, và thực hiện các chức năng hiển thị như ký tự, số và chuỗi.

1. Chuẩn bị Trước Khi Bắt Đầu

1.1 Yêu cầu Phần cứng và Phần mềm

  • Phần cứng: STM32F103C8T6, màn hình OLED 0.96 inch với giao diện I2C (sử dụng chip SSD1306), dây nối DuPont.
  • Phần mềm: Keil MDK-ARM phiên bản 5.x (cần cài đặt gói Device Pack phù hợp với STM32).
  • Dự án cơ bản: Dự án Keil đã được thiết lập với STM32F103 bao gồm các hàm khởi tạo hệ thống và độ trễ.

1.2 Kết nối Phần cứng (Giao diện I2C)

Bảng dưới đây mô tả cách kết nối 4 chân chính của màn hình OLED với STM32:
Chân OLED Chân STM32 Mô tả Chức Năng
VCC 3.3V Nguồn điện (không kết nối 5V)
GND GND Công chung đất
SCL PB6 Đường clock I2C
SDA PB7 Đường dữ liệu I2C

2. Thêm Mã Điều Khiển OLED vào Dự Án Keil

2.1 Tạo Tệp Động Cơ

Trong dự án Keil, tạo hai tệp tin mới: oled.h (tệp tiêu đề) và oled.c (tệp nguồn). Thêm chúng vào thư mục dự án và đưa vào quá trình biên dịch.

2.2 Viết Mã Điều Khiển I2C (Phần mềm mô phỏng, thân thiện cho người mới bắt đầu)

Phần mềm I2C không phụ thuộc vào ngoại vi I2C cứng của STM32 mà chỉ sử dụng GPIO để mô phỏng chu kỳ tín hiệu, do đó có tính tương thích cao hơn. Thực hiện trong oled.c như sau:
#include "oled.h"
#include "delay.h"  // Cần triển khai hàm độ trễ us/ms trước đó

// Định nghĩa chân GPIO (có thể tùy chỉnh theo yêu cầu)
#define OLED_CLK_PIN     GPIO_Pin_6
#define OLED_CLK_PORT    GPIOB
#define OLED_DAT_PIN     GPIO_Pin_7
#define OLED_DAT_PORT    GPIOB

// Macro điều khiển mức điện áp chân
#define OLED_CLK_H()  GPIO_SetBits(OLED_CLK_PORT, OLED_CLK_PIN)
#define OLED_CLK_L()  GPIO_ResetBits(OLED_CLK_PORT, OLED_CLK_PIN)
#define OLED_DAT_H()  GPIO_SetBits(OLED_DAT_PORT, OLED_DAT_PIN)
#define OLED_DAT_L()  GPIO_ResetBits(OLED_DAT_PORT, OLED_DAT_PIN)

/**
 * Khởi tạo chân I2C cho OLED
 */
void OLED_Init_I2C(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;

    // Kích hoạt clock GPIOB
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);

    // Cấu hình CLK/DAT là đầu ra đẩy
    GPIO_InitStructure.GPIO_Pin = OLED_CLK_PIN | OLED_DAT_PIN;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(OLED_CLK_PORT, &GPIO_InitStructure);

    // Đặt mức cao ban đầu
    OLED_CLK_H();
    OLED_DAT_H();
}

/**
 * Tạo tín hiệu bắt đầu I2C (nhận dạng chính của chu kỳ)
 */
void OLED_Start_I2C(void)
{
    OLED_DAT_H();
    OLED_CLK_H();
    delay_us(2);  // Độ trễ đảm bảo chu kỳ ổn định
    OLED_DAT_L(); // DAT xuống thấp khi CLK ở mức cao tạo ra tín hiệu bắt đầu
    delay_us(2);
    OLED_CLK_L();
}

/**
 * Tạo tín hiệu dừng I2C
 */
void OLED_Stop_I2C(void)
{
    OLED_DAT_L();
    OLED_CLK_H();
    delay_us(2);
    OLED_DAT_H(); // DAT lên cao khi CLK ở mức cao tạo ra tín hiệu dừng
    delay_us(2);
    OLED_CLK_L();
}

/**
 * Gửi một byte qua I2C
 * @param dat: Dữ liệu 8 bit cần gửi
 */
void OLED_Send_Byte_I2C(uint8_t dat)
{
    uint8_t i;
    for(i=0; i<8; i++)
    {
        OLED_CLK_L();
        delay_us(2);
        if(dat & 0x80) OLED_DAT_H();
        else OLED_DAT_L();
        dat <<= 1;
        delay_us(2);
        OLED_CLK_H();
        delay_us(2);
    }
    OLED_CLK_L();
    delay_us(2);
    OLED_DAT_H();
    delay_us(2);
    OLED_CLK_H();
    delay_us(2);
    OLED_CLK_L();
}

/**
 * Gửi lệnh đến OLED
 * @param cmd: Byte lệnh
 */
void OLED_Send_Command(uint8_t cmd)
{
    OLED_Start_I2C();
    OLED_Send_Byte_I2C(0x78);  // Địa chỉ thiết bị SSD1306 (0x3C<<1, một số màn hình có thể là 0x38)
    OLED_Send_Byte_I2C(0x00);  // 0x00 cho biết dữ liệu tiếp theo là lệnh
    OLED_Send_Byte_I2C(cmd);
    OLED_Stop_I2C();
}

/**
 * Gửi dữ liệu hiển thị đến OLED
 * @param dat: Byte dữ liệu pixel
 */
void OLED_Send_Data(uint8_t dat)
{
    OLED_Start_I2C();
    OLED_Send_Byte_I2C(0x78);  // Địa chỉ thiết bị
    OLED_Send_Byte_I2C(0x40);  // 0x40 cho biết dữ liệu tiếp theo là dữ liệu
    OLED_Send_Byte_I2C(dat);
    OLED_Stop_I2C();
}

2.3 Viết Hàm Khởi Tạo OLED và Thiết Lập Tọa Độ

Tiếp tục thêm hàm khởi tạo SSD1306 (đặt các tham số hiển thị) và hàm thiết lập tọa độ trong oled.c:

/**
 * Khởi tạo màn hình OLED (cấu hình chính của SSD1306)
 */
void OLED_Init_Display(void)
{
    delay_ms(100);  // Độ trễ sau khi cấp điện, đảm bảo chip ổn định

    OLED_Init_I2C();  // Khởi tạo chân I2C

    // Tập lệnh khởi tạo SSD1306
    OLED_Send_Command(0xAE);  // Tắt hiển thị
    OLED_Send_Command(0x00);  // Đặt địa chỉ cột bắt đầu phần thấp
    OLED_Send_Command(0x10);  // Đặt địa chỉ cột bắt đầu phần cao
    OLED_Send_Command(0x40);  // Đặt dòng hiển thị bắt đầu
    OLED_Send_Command(0xB0);  // Đặt địa chỉ trang (trục Y, mỗi trang 8 hàng)
    OLED_Send_Command(0x81);  // Bật điều chỉnh độ tương phản
    OLED_Send_Command(0xFF);  // Độ tương phản tối đa (0~255)
    OLED_Send_Command(0xA1);  // Ánh xạ đoạn (hiển thị bình thường, 0xA0 để lật trái phải)
    OLED_Send_Command(0xA6);  // Hiển thị bình thường (0xA7 để lật màu sắc)
    OLED_Send_Command(0xA8);  // Đặt tỷ lệ đa kênh
    OLED_Send_Command(0x3F);  // Hiển thị 64 hàng
    OLED_Send_Command(0xC8);  // Hướng quét (hiển thị bình thường, 0xC0 để lật trên dưới)
    OLED_Send_Command(0xD3);  // Đặt độ lệch hiển thị
    OLED_Send_Command(0x00);  // Độ lệch 0
    OLED_Send_Command(0xD5);  // Đặt tần số chia
    OLED_Send_Command(0x80);  // Giá trị mặc định
    OLED_Send_Command(0xD9);  // Đặt chu kỳ sạc
    OLED_Send_Command(0xF1);
    OLED_Send_Command(0xDA);  // Đặt cấu hình chân COM
    OLED_Send_Command(0x12);
    OLED_Send_Command(0xDB);  // Đặt điện áp VCOMH
    OLED_Send_Command(0x40);
    OLED_Send_Command(0x8D);  // Bật bộ pomp điện tích (bắt buộc mở, nếu không sẽ không hiển thị)
    OLED_Send_Command(0x14);
    OLED_Send_Command(0xAF);  // Bật hiển thị
}

/**
 * Thiết lập vị trí hiển thị trên OLED
 * @param x: Tọa độ cột (0~127)
 * @param y: Tọa độ trang (0~7, mỗi trang tương ứng 8 hàng)
 */
void OLED_Set_Position(uint8_t x, uint8_t y)
{
    OLED_Send_Command(0xB0 + y);                // Đặt địa chỉ trang
    OLED_Send_Command(((x & 0xF0) >> 4) | 0x10); // Phần cao của địa chỉ cột
    OLED_Send_Command(x & 0x0F);                // Phần thấp của địa chỉ cột
}

/**
 * Xóa màn hình OLED
 */
void OLED_Clear_Display(void)
{
    uint8_t x, y;
    for(y=0; y<8; y++)  // Lặp qua 8 trang
    {
        OLED_Set_Position(0, y);
        for(x=0; x<128; x++)  // Lặp qua 128 cột
        {
            OLED_Send_Data(0x00);  // Ghi 0, tắt tất cả pixel
        }
    }
}

2.4 Viết Hàm Hiển Thị Ký Tự/Chuỗi

Thêm bảng điểm mảng ASCII 8x16 (lấy phần số/ chữ cái) và các hàm hiển thị, bổ sung vào oled.c:

// Bảng điểm mảng ASCII 8x16 (số từ 0-9, có thể mở rộng chữ cái/ký hiệu)
const unsigned char OLED_Font_8x16[] = {
    0x00,0x00,0x7C,0x12,0x11,0x12,0x7C,0x00,0x00,0x00,0x7C,0x12,0x11,0x12,0x7C,0x00, // 0
    0x00,0x00,0x00,0x70,0x08,0x08,0x08,0x70,0x00,0x00,0x00,0x70,0x08,0x08,0x08,0x70, // 1
    0x00,0x00,0x7C,0x02,0x01,0x02,0x7C,0x00,0x00,0x00,0x7C,0x02,0x01,0x02,0x7C,0x00, // 2
    // Các điểm mảng số/chữ cái khác có thể tự bổ sung
};

/**
 * Hiển thị một ký tự đơn (điểm mảng 8x16)
 * @param x: Cột bắt đầu (0~127)
 * @param y: Trang bắt đầu (0~7, mỗi ký tự chiếm 2 trang)
 * @param c: Ký tự cần hiển thị (mã ASCII)
 */
void OLED_Display_Char(uint8_t x, uint8_t y, uint8_t c)
{
    uint8_t i;
    c -= '0';  // Đặt điểm xuất phát từ số 0 (để hiển thị chữ cái cần điều chỉnh điểm xuất phát)
    // Hiển thị nửa trên (trang 1)
    OLED_Set_Position(x, y);
    for(i=0; i<8; i++)
    {
        OLED_Send_Data(OLED_Font_8x16[c*16 + i]);
    }
    // Hiển thị nửa dưới (trang 2)
    OLED_Set_Position(x, y+1);
    for(i=8; i<16; i++)
    {
        OLED_Send_Data(OLED_Font_8x16[c*16 + i]);
    }
}

/**
 * Hiển thị chuỗi ký tự
 * @param x: Cột bắt đầu
 * @param y: Trang bắt đầu
 * @param str: Con trỏ đến chuỗi
 */
void OLED_Display_String(uint8_t x, uint8_t y, uint8_t *str)
{
    while(*str != '\0')
    {
        OLED_Display_Char(x, y, *str);
        x += 8;  // Mỗi ký tự chiếm 8 cột
        if(x > 120)  // Nếu vượt quá chiều rộng màn hình thì xuống dòng
        {
            x = 0;
            y += 2;
        }
        str++;
    }
}

2.5 Khai Báo Trong Tệp Tiêu Đề oled.h

Trong oled.h, thêm các khai báo hàm và các thư viện cần thiết, giúp hàm chính gọi dễ dàng:

#ifndef __OLED_H
#define __OLED_H

#include "stm32f10x.h"
#include "stdint.h"

// Khai báo hàm
void OLED_Init_I2C(void);
void OLED_Send_Command(uint8_t cmd);
void OLED_Send_Data(uint8_t dat);
void OLED_Init_Display(void);
void OLED_Set_Position(uint8_t x, uint8_t y);
void OLED_Clear_Display(void);
void OLED_Display_Char(uint8_t x, uint8_t y, uint8_t c);
void OLED_Display_String(uint8_t x, uint8_t y, uint8_t *str);

#endif

3. Viết Hàm Chính trong Keil Để Kiểm Tra

Trong main.c, gọi các hàm điều khiển OLED, thực hiện kiểm tra hiển thị:

#include "stm32f10x.h"
#include "oled.h"
#include "delay.h"  // Đảm bảo đã triển khai delay_init, delay_ms, delay_us

int main(void)
{
    // Khởi tạo hệ thống
    SystemInit();          // Cấu hình tần số CPU (72MHz)
    delay_init();          // Khởi tạo hàm độ trễ
    OLED_Init_Display();   // Khởi tạo màn hình OLED
    OLED_Clear_Display();  // Xóa màn hình

    while(1)
    {
        // Hiển thị chuỗi cố định
        OLED_Display_String(0, 0, "Keil OLED Test");
        // Hiển thị số (ví dụ: đếm tăng dần)
        static uint32_t count = 0;
        OLED_Display_String(0, 2, "Count:");
        // Chuyển đổi số thành chuỗi (hoặc mở rộng hàm OLED_Display_Number)
        uint8_t num_str[6];
        sprintf((char*)num_str, "%05d", count);
        OLED_Display_String(40, 2, num_str);

        count++;
        if(count > 99999) count = 0;
        delay_ms(100);  // Cập nhật mỗi 100ms
    }
}

4. Biên Dịch và Tải Dự Án Keil

  1. Kiểm tra dự án: Đảm bảo oled.c đã được thêm vào nhóm nguồn của Keil, đường dẫn thư mục tiêu đề chính xác;
  2. Biên dịch dự án: Nhấp vào Build hoặc Rebuild trên thanh công cụ Keil, giải quyết lỗi cú pháp (ví dụ: thiếu hàm độ trễ, định nghĩa GPIO sai);
  3. Tải chương trình: Kết nối bộ gỡ lỗi (J-Link/ST-Link), nhấn Download để tải chương trình vào STM32;
  4. Kiểm tra kết quả: Sau khi cấp điện, màn hình OLED hiển thị "Keil OLED Test" và số đếm tăng dần, cho thấy điều khiển thành công.

5. Giải Quyết Các Vấn Đề Thường Gặp (Trong Môi Trường Keil)

  1. Lỗi biên dịch "undefined reference to delay_us": - Nguyên nhân: Chưa triển khai hàm độ trễ hoặc chưa thêm vào dự án; - Giải pháp: Thêm delay.c/delay.h, triển khai delay_usdelay_ms dựa trên SysTick.
  2. Màn hình OLED không hiển thị sau khi tải: - Kiểm tra cấu hình gỡ lỗi Keil: Xác nhận thiết lập Debug tab về bộ gỡ lỗi, tốc độ Core Clock đúng; - Kiểm tra địa chỉ I2C: Thay đổi OLED_Send_Byte_I2C(0x78) thành 0x38 thử nghiệm (một số màn hình có địa chỉ là 0x1C<<1); - Kiểm tra mã tải: Trong Keil, nhấn View->Watch & Call Stack Window, xác nhận OLED_Init_Display đã hoàn thành.
  3. Hiển thị lộn chữ: - Nguyên nhân: Điểm mảng ký tự không khớp với hàm hiển thị, hoặc mức tối ưu hóa của Keil cao; - Giải pháp: Đặt mức tối ưu hóa của Keil thành -O0 (Options for Target->Optimization), kiểm tra lại định dạng điểm mảng ký tự.

Thẻ: STM32 SSD1306 OLED I2C Keil

Đăng vào ngày 8 tháng 6 lúc 18:31