ESP32 OLED 屏幕开发

最近更新于 2024-05-05 12:31

1 环境

1.1 硬件

ESP32-WROOM-32
128×64 OLED(IIC)

1.2 软件

Arduino IDE 2.2.1
esp32 2.0.14(开发板)
Adafruit_SSD1306 2.5.7 :https://github.com/adafruit/Adafruit_SSD1306 (依赖库:
Adafruit_BusIO 1.14.5 :https://github.com/adafruit/Adafruit_BusIO
Adafruit-GFX-Library 1.11.9 :https://github.com/adafruit/Adafruit-GFX-Library

注:如果是在 Arduino IDE 中安装,会自动提示依赖库安装,如果是从 GitHub 下载离线包,就要手动分别安装。

2 接线

2.1 默认

使用 Wire 库时,ESP32 的默认引脚对应(下面探索就使用这个默认的)

  • 21 – SDA
  • 22 – SCL

2.2 自定义

方法一

Wire.setPins(int sda, int scl);
Wire.begin();

方法二

bool begin(int sda, int scl, uint32_t frequency=0);

3 探索

3.1 确定 IIC 地址

SSD1306 驱动的 OLED 的 IIC 地址要么是 0x3D,要么就是 0x3C,其实两选一试也行,这里提供了一份代码,会进行遍历测试可用的设备的地址。

#include <Wire.h>

void setup()
{
    Serial.begin(115200);
    delay(10);

    Wire.begin();

    Serial.println("开始扫描 IIC 设备");
}

void loop()
{
    int devices = 0;

    Serial.println("正在扫描......");

    // 遍历地址,测试是否可以通信
    for (byte error, address = 1; address < 127; ++address)
    {
        Wire.beginTransmission(address); // 指定通信地址(准备通信)
        error = Wire.endTransmission(); // 结束通信
        if (0 == error)
        {
            Serial.print("发现 IIC 设备,地址为 0x");
            if (address < 16)
            {
                Serial.print(0);
            }
            Serial.println(address, HEX);
            ++devices;
        }
        else if (4 == error)
        {
            Serial.print("发生未知错误,地址为 0x");
            if (address < 16)
            {
                Serial.print(0);
            }
            Serial.println(address, HEX);
        }
    }
    if (0 == devices)
    {
        Serial.println("未发现 IIC 设备!\n");
    }
    else
    {
        Serial.println("完成!\n");
    }

    delay(5000);
}

file

3.2 显示像素点

Adafruit 这个库采用了缓存机制,内部维护了一个数组,数组每个元素映射到屏幕上的一个像素点,元素的值决定了这个点亮还是不亮,写操作其实就是修改数组元素的值,再调用 display 函数将缓存应用到屏幕上才会显示。

#include <Wire.h>
#include <Adafruit_SSD1306.h>

// 屏幕分辨率
#define OLED_WIDTH      128
#define OLED_HEIGHT     64

// IIC 地址
#define OLED_ADDRESS    0x3C

// 复位引脚
#define OLED_RESET      -1 // 不使用

Adafruit_SSD1306 display(OLED_WIDTH, OLED_HEIGHT, &Wire, OLED_RESET);

void setup()
{
    Serial.begin(115200);
    delay(10);

    if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS))
    {
        Serial.println("OLED 初始化失败!");
        while (1);
    }

    display.clearDisplay(); // 清空缓存

    for (int i = 0; i < 50; ++i) // 循环绘制像素点构成直线
    {
        display.drawPixel(i, i, SSD1306_WHITE); // 指定横纵坐标和颜色
    }

    display.display(); // 将写入缓存的内容显示到 OLED
}

void loop()
{

}

file

3.3 显示字符

字符线宽为 1 个像素点时,128×64 分辨率每行显示 21 个字符,8 行,平均每个字符像素尺寸为 6×8。

#include <Wire.h>
#include <Adafruit_SSD1306.h>

// 屏幕分辨率
#define OLED_WIDTH      128
#define OLED_HEIGHT     64

// IIC 地址
#define OLED_ADDRESS    0x3C

// 复位引脚
#define OLED_RESET      -1 // 不使用

Adafruit_SSD1306 display(OLED_WIDTH, OLED_HEIGHT, &Wire, OLED_RESET);

void oled_print(int16_t start, int16_t end)
{
    display.clearDisplay();

    display.setCursor(0, 0); // 设置开始字符的左上角坐标
    for (int16_t i = start; i < end; ++i)
    {
        if ('\n' == i)
        {
            display.write(' '); // 每次写一个字符,接着上一个字符后面
        }
        else
        {
            display.write(i);
        }
    }
    display.display();
}

void setup()
{
    Serial.begin(115200);
    delay(10);

    if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS))
    {
        Serial.println("OLED 初始化失败!");
        while (1);
    }

    display.setTextSize(1); // 设置像素大小,不设置默认为 1
    display.setTextColor(SSD1306_WHITE); // 设置亮色显示
    display.cp437(true); // Code Page 437(IBM PC ASCII),有 256 个字符
}

void loop()
{
    oled_print(0, 168); // 每行最多显示 21 个字符,共 8 行,共 168 个
    delay(1000);
    oled_print(168, 256);
    delay(1000);
}

file

file

3.4 显示字符串

#include <Wire.h>
#include <Adafruit_SSD1306.h>

// 屏幕分辨率
#define OLED_WIDTH      128
#define OLED_HEIGHT     64

// IIC 地址
#define OLED_ADDRESS    0x3C

// 复位引脚
#define OLED_RESET      -1 // 不使用

Adafruit_SSD1306 display(OLED_WIDTH, OLED_HEIGHT, &Wire, OLED_RESET);

void setup()
{
    Serial.begin(115200);
    delay(10);

    if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS))
    {
        Serial.println("OLED 初始化失败!");
        while (1);
    }

    display.setTextColor(SSD1306_WHITE);
    display.clearDisplay();

    display.setCursor(30, 30);
    // 注:Arduino IDE 中 F 函数可以让字符串常量存储在 FLASH 中,而节省 RAM
    display.println(F("hello world!")); // println 带有换行,后续显示不设置焦点会自动跳到下一行
    display.print(F("h w"));
    display.setCursor(30, 38); // 字符高度为 8 像素,纵坐标加 8 等于换行
    display.print(F("ABCD1234!@#$")); // print 没有换行,后续显示不设置焦点会接着尾部
    display.print(F(" & *"));

    display.display();
}

void loop()
{

}

file

3.5 绘制图形

#include <Wire.h>
#include <Adafruit_SSD1306.h>

// 屏幕分辨率
#define OLED_WIDTH      128
#define OLED_HEIGHT     64

// IIC 地址
#define OLED_ADDRESS    0x3C

// 复位引脚
#define OLED_RESET      -1 // 不使用

Adafruit_SSD1306 display(OLED_WIDTH, OLED_HEIGHT, &Wire, OLED_RESET);

void setup()
{
    Serial.begin(115200);
    delay(10);

    if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS))
    {
        Serial.println("OLED 初始化失败!");
        while (1);
    }

    display.clearDisplay();

    // 直线(对角线)
    display.drawLine(0, 0, // 起始点坐标
                    display.width() - 1, display.height() - 1, // 终点坐标
                    SSD1306_WHITE);

    // 矩形
    display.drawRect(0, 20, // 左上角坐标
                    20, 10, // 宽度和高度
                    SSD1306_WHITE
                    );

    // 填充矩形
    display.fillRect(0, 0,
                    20, 10,
                    SSD1306_WHITE
                    );

    // 圆角矩形
    display.drawRoundRect(0, 31, // 左上角坐标
                        20, 10, // 宽度和高度
                        3, // 圆角半径
                        SSD1306_WHITE);

    // 填充圆角矩形
    display.fillRoundRect(5, 42,
                        20, 10,
                        3,
                        SSD1306_WHITE);

    // 三角形
    display.drawTriangle(50, 0, // 指定三个点坐标
                        40, 30,
                        60, 15,
                        SSD1306_WHITE);

    // 填充三角形
    display.fillTriangle(100, 0,
                        90, 30,
                        110, 15,
                        SSD1306_WHITE);

    // 圆
    display.drawCircle(display.width() / 2, display.height() / 2, // 圆心,屏幕中心
                        10, // 半径
                        SSD1306_WHITE);

    // 填充圆
    display.fillCircle(display.width() / 2 + 20, display.height() / 2,
                        5,
                        SSD1306_WHITE);

    display.display();
}

void loop()
{

}

file

3.5 滚动

#include <Wire.h>
#include <Adafruit_SSD1306.h>

// 屏幕分辨率
#define OLED_WIDTH      128
#define OLED_HEIGHT     64

// IIC 地址
#define OLED_ADDRESS    0x3C

// 复位引脚
#define OLED_RESET      -1 // 不使用

Adafruit_SSD1306 display(OLED_WIDTH, OLED_HEIGHT, &Wire, OLED_RESET);

void setup()
{
    Serial.begin(115200);
    delay(10);

    if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS))
    {
        Serial.println(F("OLED 初始化失败!"));
        while (1);
    }

    display.clearDisplay();

    display.setTextColor(SSD1306_WHITE); // 黑底亮字
    display.setCursor(0, 0);
    display.println(F("hello world!"));
    display.setCursor(10, 10);
    display.println(F("hello world!"));

    display.setTextColor(SSD1306_BLACK, SSD1306_WHITE); // 亮底黑字
    display.println(F("1234ABCD"));

    display.setTextSize(2); // 字符线宽
    display.setTextColor(SSD1306_WHITE);
    display.setCursor(10, 26);
    display.println(F("hello!"));

    display.setTextSize(1);
    display.println(F("world"));
    display.println(F("Good"));

    display.display();
    delay(1000);
}

void loop()
{
    // 从左往右滚动
    display.startscrollright(0, 0); // 参与滚动的行范围,每行高度为一个单像素字符高度,即 8 像素。这里是第一行滚动
    delay(2000);
    display.stopscroll(); // 停止滚动
    delay(1000);

    // 从右往左滚动
    display.startscrollleft(1, 2); // 2、3 行滚动
    delay(2000);
    display.stopscroll();
    delay(1000);
}

file

file

3.6 位图

#include <Wire.h>
#include <Adafruit_SSD1306.h>

// 屏幕分辨率
#define OLED_WIDTH      128
#define OLED_HEIGHT     64

// IIC 地址
#define OLED_ADDRESS    0x3C

// 复位引脚
#define OLED_RESET      -1 // 不使用

Adafruit_SSD1306 display(OLED_WIDTH, OLED_HEIGHT, &Wire, OLED_RESET);

#define STAR_HEIGHT   16
#define STAR_WIDTH    16

// 星星图案
// PROGMEM 可以将数据存储在 flash
static const unsigned char PROGMEM star[] =
{   0b00000000, 0b11000000,
    0b00000001, 0b11000000,
    0b00000001, 0b11000000,
    0b00000011, 0b11100000,
    0b11110011, 0b11100000,
    0b11111110, 0b11111000,
    0b01111110, 0b11111111,
    0b00110011, 0b10011111,
    0b00011111, 0b11111100,
    0b00001101, 0b01110000,
    0b00011011, 0b10100000,
    0b00111111, 0b11100000,
    0b00111111, 0b11110000,
    0b01111100, 0b11110000,
    0b01110000, 0b01110000,
    0b00000000, 0b00110000 };

void setup()
{
    Serial.begin(115200);
    delay(10);

    if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS))
    {
        Serial.println(F("OLED 初始化失败!"));
        while (1);
    }
}

void draw_star(int x, int y)
{
    display.clearDisplay();
    // 绘制位图
    display.drawBitmap(x, y, // 左上角坐标
                        star, // 图像数据
                        STAR_WIDTH, STAR_HEIGHT, // 图像尺寸
                        SSD1306_WHITE);
    display.display();
}

void loop()
{
    for (int i = 0; i < 48; ++i)
    {
        draw_star(2 * i, i);
        delay(100);
    }
}

file

file

3.7 取模

取模工具下载:https://pan.baidu.com/s/1OI1mRI-rsP6qohK2DuV0dQ?pwd=r19e

3.7.1 图片取模

前面一个示例打开位图,而位图的数据就需要取模生成

首先可以选一张图片,用画图打开,准备调整大小
file

我这里 OLED 分辨率是 128×64,将 64 像素边作为高,那么这里图片的像素高度就不能超过 64,按这里的比例高度 64 时,宽度肯定超不了 128。最终图片大小就设置为 61×64,然后保存图片。(用画图调整的原因是支持按比例缩放,直接用下面的取模工具也可以设置生成图像大小,但是如果要长宽维持比例就得自己计算)
file

运行 Img2Lcd,打开刚才保存的图片
输出数据类型选C语言数组,扫描模式选水平扫描,输出灰度选单色,宽度和高度就设置为上面调整后的大小,这里就是 61×64,下面的都不勾选,然后保存
file

生成的数据就可以用于 OLED 绘制
file

#include <Wire.h>
#include <Adafruit_SSD1306.h>

// 屏幕分辨率
#define OLED_WIDTH      128
#define OLED_HEIGHT     64

// IIC 地址
#define OLED_ADDRESS    0x3C

// 复位引脚
#define OLED_RESET      -1 // 不使用

Adafruit_SSD1306 display(OLED_WIDTH, OLED_HEIGHT, &Wire, OLED_RESET);

// 图片大小
#define IMG_WIDTH    61
#define IMG_HEIGHT   64

// 图片数据
static const unsigned char PROGMEM img[] =
{   0XF9,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XEF,0XFF,0XFF,0XFF,0XFF,0XFF,
    0XFF,0XFF,0XF3,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XF0,0X7F,0XFF,0XFF,0XFF,0XFF,
    0XFF,0XFF,0XE0,0X07,0XFF,0XFF,0XFF,0XF8,0XFF,0XFF,0X00,0X01,0X7F,0XFF,0XFF,0XFB,
    0XFF,0XF8,0X07,0X00,0X1F,0XFF,0XFF,0XFF,0XFF,0XF0,0X03,0XC0,0X0F,0XFF,0XFF,0XFC,
    0XFF,0XE7,0XFD,0XF0,0X0F,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XF0,0X0F,0XFF,0XFF,0XFF,
    0XFE,0X07,0XFF,0XBE,0X07,0XFF,0XFF,0XFF,0XF0,0X07,0XFE,0XFF,0X83,0XFF,0XFF,0XFF,
    0XF0,0X0F,0XFF,0X7F,0XC3,0XFF,0XFF,0XFB,0XF8,0X1F,0XFF,0X9F,0XE1,0XFF,0XFF,0XFF,
    0XF8,0X1F,0XFF,0XEF,0XF0,0XFF,0XFF,0XFF,0XFC,0X3F,0XFF,0XF7,0XF0,0XFF,0XFF,0XFF,
    0XFC,0X7F,0XFF,0XFB,0XF8,0X7F,0XFF,0XF9,0XF8,0XFF,0XFF,0XFF,0XFC,0XFF,0XFF,0XFF,
    0XF9,0XFF,0XFF,0XFF,0XFF,0X7F,0XFF,0XFF,0XF3,0XFF,0XFF,0XFF,0XFF,0X7F,0XFF,0XFF,
    0XF3,0XFF,0XFF,0XFF,0XFE,0X3F,0XFF,0XFF,0XF7,0XFF,0XFF,0XF3,0XFF,0X1F,0XFF,0XFE,
    0XF7,0XFF,0XFF,0XFF,0XFF,0XCF,0XFF,0XFF,0XF7,0XFF,0XFF,0XFF,0XFF,0XC7,0XFF,0XFF,
    0XF3,0XFF,0XFF,0XFF,0XFF,0XC3,0XFF,0XFC,0XFB,0XF3,0XDF,0XFF,0XFF,0XC0,0XFF,0XFE,
    0XF9,0XFF,0XC3,0XFF,0XFF,0XF0,0XFF,0XFF,0XFD,0X7C,0X60,0XFF,0XFF,0XFC,0X7F,0XFF,
    0XFE,0X38,0X60,0X0F,0XFF,0XFE,0X7F,0XFF,0XFF,0X99,0XE0,0X07,0XFF,0XFE,0X7F,0XFD,
    0XFF,0XCC,0X60,0X03,0XFF,0XFF,0XFF,0XFF,0XFF,0XC6,0X40,0X01,0X7F,0XFF,0XBF,0XF8,
    0XFF,0XE2,0X40,0X00,0X3F,0XFF,0X1F,0XF8,0XFF,0XF0,0X40,0X00,0X1F,0XFE,0X8F,0XFF,
    0XFF,0XF8,0X40,0X00,0X07,0XF0,0X27,0XFF,0XFF,0XFF,0X00,0X00,0X01,0XE0,0X17,0XFF,
    0XFF,0XFF,0XE0,0X00,0X00,0XC0,0X1B,0XF9,0XFF,0XFF,0XE0,0X18,0X01,0X00,0X09,0XFF,
    0XFF,0XFF,0XE0,0X3C,0X04,0X00,0X01,0XF8,0XFF,0XFF,0XF0,0X3E,0X00,0X00,0X03,0XF8,
    0XFF,0XFF,0XF8,0XFE,0X00,0X00,0X00,0XF8,0XFF,0XFF,0XFE,0XFC,0X00,0X00,0X00,0XFF,
    0XFF,0XFF,0XFF,0X7C,0X00,0X00,0X00,0X3F,0XFF,0XFF,0XFF,0XF8,0X00,0X00,0X00,0X1F,
    0XFF,0XFF,0XFF,0XF0,0X00,0X00,0X00,0X0F,0XFF,0XFF,0XFF,0XE0,0X00,0X00,0X00,0X06,
    0XFF,0XFF,0XFF,0XE0,0X00,0X00,0X00,0X01,0XFF,0XFF,0XFF,0XE0,0X00,0X00,0X00,0X18,
    0XFF,0XFF,0XFF,0XE0,0X00,0X00,0X00,0X1C,0XFF,0XFF,0XFF,0XE0,0X00,0X00,0X00,0X08,
    0XFF,0XFF,0XFF,0XE0,0X01,0XC0,0X00,0X0F,0XFF,0XFF,0XFF,0XE0,0X03,0XC0,0X00,0X0F,
    0XFF,0XFF,0XFF,0XE0,0X07,0XE0,0X00,0X07,0XFF,0XFF,0XFF,0XC0,0X0F,0XE0,0X00,0X07,
    0XFF,0XFF,0XFF,0XD0,0X1F,0XF0,0X00,0X00,0XFF,0XFF,0XFF,0XC0,0X1F,0XF8,0X00,0X00,
    0XFF,0XFF,0XFF,0XC0,0X3F,0XF8,0X00,0X00,0XFF,0XFF,0XFF,0XC0,0X3F,0XFC,0X00,0X07,
    0XFF,0XFF,0XFF,0XC0,0X3F,0XFF,0X00,0X00,0XFF,0XFF,0XFF,0XC0,0X7F,0XFF,0X80,0X07,
    0XFF,0XFF,0XFF,0XC0,0X7F,0XFF,0X80,0X07,0XFF,0XFF,0XFF,0XC6,0X7F,0XFF,0XC1,0X07,
    0XFF,0XFF,0XFF,0XC0,0XFF,0XFF,0XFB,0XC7,0XFF,0XFF,0XFF,0XC7,0XFF,0XFF,0XFF,0X80,};

void setup()
{
    Serial.begin(115200);
    delay(10);

    if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS))
    {
        Serial.println(F("OLED 初始化失败!"));
        while (1);
    }

    display.clearDisplay();
    display.drawBitmap(0, 0, // 左上角坐标
                        img, // 图像数据
                        IMG_WIDTH, IMG_HEIGHT, // 图像尺寸
                        SSD1306_WHITE);
    display.display();
}

void loop()
{

}

file

3.7.2 字符取模(汉字)

运行 PCtoLCD2002,模式选择如图
file

然后选择字体和设置大小
file

选项设置如图
file

然后生成字模
file

然后就可以在 OLED 显示

#include <Wire.h>
#include <Adafruit_SSD1306.h>

// 屏幕分辨率
#define OLED_WIDTH      128
#define OLED_HEIGHT     64

// IIC 地址
#define OLED_ADDRESS    0x3C

// 复位引脚
#define OLED_RESET      -1 // 不使用

Adafruit_SSD1306 display(OLED_WIDTH, OLED_HEIGHT, &Wire, OLED_RESET);

// 字模大小
#define FONT_WIDTH    16
#define FONT_HEIGHT   16

// 字模
static const unsigned char PROGMEM font[][32] =
{
    0x08,0x80,0x08,0x80,0x08,0x80,0x11,0xFE,0x11,0x02,0x32,0x04,0x34,0x20,0x50,0x20,
    0x91,0x28,0x11,0x24,0x12,0x24,0x12,0x22,0x14,0x22,0x10,0x20,0x10,0xA0,0x10,0x40,/*"你",0*/

    0x10,0x00,0x10,0xFC,0x10,0x04,0x10,0x08,0xFC,0x10,0x24,0x20,0x24,0x20,0x25,0xFE,
    0x24,0x20,0x48,0x20,0x28,0x20,0x10,0x20,0x28,0x20,0x44,0x20,0x84,0xA0,0x00,0x40,/*"好",1*/

    0x02,0x20,0x12,0x20,0x12,0x20,0x12,0x20,0x12,0x20,0xFF,0xFE,0x12,0x20,0x12,0x20,
    0x12,0x20,0x12,0x20,0x13,0xE0,0x10,0x00,0x10,0x00,0x10,0x00,0x1F,0xFC,0x00,0x00,/*"世",2*/

    0x00,0x00,0x1F,0xF0,0x11,0x10,0x11,0x10,0x1F,0xF0,0x11,0x10,0x11,0x10,0x1F,0xF0,
    0x02,0x80,0x0C,0x60,0x34,0x58,0xC4,0x46,0x04,0x40,0x08,0x40,0x08,0x40,0x10,0x40,/*"界",3*/
};

void setup()
{
    Serial.begin(115200);
    delay(10);

    if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS))
    {
        Serial.println(F("OLED 初始化失败!"));
        while (1);
    }

    display.clearDisplay();
    display.drawBitmap(32, 24, font[0], FONT_WIDTH, FONT_HEIGHT, SSD1306_WHITE);
    display.drawBitmap(48, 24, font[1], FONT_WIDTH, FONT_HEIGHT, SSD1306_WHITE);
    display.drawBitmap(64, 24, font[2], FONT_WIDTH, FONT_HEIGHT, SSD1306_WHITE);
    display.drawBitmap(80, 24, font[3], FONT_WIDTH, FONT_HEIGHT, SSD1306_WHITE);
    display.display();
}

void loop()
{

}

file

3.8 局部刷新

有时候显示的东西只有局部变化,其它部分可能一直都显示相同的,那么刷新变化的内容的时候就没必要全屏重写,只需要重写变化的部分就行。但是也不能直接往变化的部分写,这样会和以前显示的内容重叠,要解决的关键问题就是如何只清除要更新的部分,然后再写。
这个就可以利用前面演示用过的填充矩形函数来实现,某部分要重写,就先用填充矩形指定范围,但是填充颜色选黑色就会抹掉这部分,再写到这个地方就可以实现局部刷新。

下面写的伪代码,大致理解逻辑

// 实例对象
Adafruit_SSD1306 oled(OLED_WIDTH, OLED_HEIGHT, &Wire, OLED_RESET);

// 初始化
oled.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS);

// 清屏
oled.clearDisplay();

// 设置打印颜色为白色
oled.setTextColor(SSD1306_WHITE);

// 固定不变的显示内容
oled.print("counter:");

// 动态变化的显示内容(需要局部刷新的)
for (int i = 0; i < 10; ++i)
{
    oled.fillRect(48, 0, 6, 8, SSD1306_BLACK); // 刷新部位局部绘制为黑色(清空)
    oled.setCursor(48, 0); // 设置要显示部位坐标
    oled.print(i); // 写入最新显示内容
    oled.display(); // 显示

    delay(1000);
}

file

ESP32 OLED 屏幕开发
Scroll to top