回到首页 返回首页
回到顶部 回到顶部
返回上一页 返回上一页

Beetle 树莓派RP2350 驱动OLED显示三维立体旋转正方体 简单

头像 卓尔 2025.05.17 13 0

本项目基于RP2350开发板,配合分辨率为128×64的SSD1306 OLED显示屏,利用MicroPython语言实现了一个动态的三维立体旋转正方体视觉效果。通过I2C通信接口完成了显示模块与微控制器的连接,结合数学三维变换与投影算法,展示了一个简洁的3D旋转动画,体现了硬件驱动与三维图形渲染的融合应用。

硬件平台介绍

  • 核心控制器:RP2350微控制器,支持MicroPython开发。
  • 显示屏模块:SSD1306 OLED显示屏,分辨率128×64,采用I2C接口通信。
  • 连接方式:通过I2C总线进行通信,SCL线连接至GPIO5,SDA线连接至GPIO4,通信速率设定为400kHz,保证数据传输效率和稳定性。
666c9f5385aed965ecd18a33d31b462.jpg

软件环境与工具

  • 编程语言:MicroPython,适合快速开发和硬件控制。
  • 使用库
    • machine模块:负责I2C等硬件接口初始化与管理。
    • ssd1306库:实现对SSD1306 OLED屏幕的驱动,支持像素绘制、线条绘制及屏幕刷新。
    • math模块:提供三角函数,支持三维旋转计算。
  • 开发与调试工具:采用Thonny IDE,方便代码编写、上传及串口调试,提升开发效率。

功能实现及软件设计说明

I2C接口初始化
通过machine.I2C类实例化I2C对象,指定SCL和SDA引脚以及通信频率。调用i2c.scan()函数检测并确认OLED的I2C设备地址,确保设备连接正常。

OLED屏幕驱动
使用SSD1306_I2C类创建OLED控制实例,传入屏幕分辨率、I2C对象及设备地址,实现屏幕的初始化和控制。

三维立方体顶点和边的数据结构
采用列表存储正方体8个顶点的三维坐标(以原点为中心,边长24),以及12条边对应顶点索引对,用于后续绘制连线。

三维旋转函数
rotate_point(x, y, z, ax, ay, az)函数实现绕X、Y、Z三个轴的旋转变换,使用标准三维旋转矩阵和math.cos、math.sin计算旋转后的坐标。

三维投影函数
project_point(x, y, z, viewer_distance=50)函数实现简单的透视投影,将三维空间中的点映射到二维屏幕坐标,投影距离参数调整视角深度和透视效果。

绘制函数
draw_cube(ax, ay, az)函数完成以下步骤:

  • 清空屏幕缓存(oled.fill(0))。
  • 对所有顶点进行旋转和投影处理,得到二维屏幕坐标。
  • 遍历边数组,使用oled.line()函数绘制连接对应顶点的线段,形成立体框架。
  • 调用oled.show()刷新显示,实现动画效果。

主循环控制
程序在无限循环中不断更新旋转角度rotx、roty、rotz,通过不断调用draw_cube()函数实现立方体的连续旋转动画,time.sleep(0.05)控制刷新频率,确保动画流畅。

  • 初始化I2C接口时,自动扫描并确认OLED设备地址
  • image.png
  • 若打印出的是别的,需要在程序中修改,此处“60”对应的16进制是“0x3C”

完整代码如下

代码
import machine
import time
import math
from ssd1306 import SSD1306_I2C

WIDTH = 128
HEIGHT = 64

i2c = machine.I2C(0, scl=machine.Pin(5), sda=machine.Pin(4), freq=400000)
oled_addr = 0x3C
oled = SSD1306_I2C(WIDTH, HEIGHT, i2c, addr=oled_addr)

cube_vertices = [
    [-12, -12, -12],
    [12, -12, -12],
    [12, 12, -12],
    [-12, 12, -12],
    [-12, -12, 12],
    [12, -12, 12],
    [12, 12, 12],
    [-12, 12, 12]
]

cube_edges = [
    (0, 1), (1, 2), (2, 3), (3, 0),
    (4, 5), (5, 6), (6, 7), (7, 4),
    (0, 4), (1, 5), (2, 6), (3, 7)
]

rotx = 0.0
roty = 0.0
rotz = 0.0

def project_point(x, y, z, viewer_distance=50):
    factor = viewer_distance / (viewer_distance + z)
    sx = int(x * factor + WIDTH / 2)
    sy = int(y * factor + HEIGHT / 2)
    return sx, sy

def rotate_point(x, y, z, ax, ay, az):
    cosx = math.cos(ax)
    sinx = math.sin(ax)
    y2 = y * cosx - z * sinx
    z2 = y * sinx + z * cosx

    cosy = math.cos(ay)
    siny = math.sin(ay)
    x2 = x * cosy + z2 * siny
    z3 = -x * siny + z2 * cosy

    cosz = math.cos(az)
    sinz = math.sin(az)
    x3 = x2 * cosz - y2 * sinz
    y3 = x2 * sinz + y2 * cosz

    return x3, y3, z3

def draw_cube(ax, ay, az):
    oled.fill(0)
    projected_points = []
    for vertex in cube_vertices:
        x, y, z = vertex
        xr, yr, zr = rotate_point(x, y, z, ax, ay, az)
        px, py = project_point(xr, yr, zr)
        projected_points.append((px, py))

    for edge in cube_edges:
        p1 = projected_points[edge[0]]
        p2 = projected_points[edge[1]]
        oled.line(p1[0], p1[1], p2[0], p2[1], 1)

    oled.show()

while True:
    draw_cube(rotx, roty, rotz)
    rotx += 0.03
    roty += 0.02
    rotz += 0.01
    time.sleep(0.05)

实际效果:

 

其它代码:SSD1306.py:

 

代码
	# MicroPython SSD1306 OLED driver, I2C and SPI interfaces
 
from micropython import const
import framebuf
 
 
# register definitions
SET_CONTRAST = const(0x81)
SET_ENTIRE_ON = const(0xA4)
SET_NORM_INV = const(0xA6)
SET_DISP = const(0xAE)
SET_MEM_ADDR = const(0x20)
SET_COL_ADDR = const(0x21)
SET_PAGE_ADDR = const(0x22)
SET_DISP_START_LINE = const(0x40)
SET_SEG_REMAP = const(0xA0)
SET_MUX_RATIO = const(0xA8)
SET_COM_OUT_DIR = const(0xC0)
SET_DISP_OFFSET = const(0xD3)
SET_COM_PIN_CFG = const(0xDA)
SET_DISP_CLK_DIV = const(0xD5)
SET_PRECHARGE = const(0xD9)
SET_VCOM_DESEL = const(0xDB)
SET_CHARGE_PUMP = const(0x8D)
 
# Subclassing FrameBuffer provides support for graphics primitives
# http://docs.micropython.org/en/latest/pyboard/library/framebuf.html
class SSD1306(framebuf.FrameBuffer):
    def __init__(self, width, height, external_vcc):
        self.width = width
        self.height = height
        self.external_vcc = external_vcc
        self.pages = self.height // 8
        self.buffer = bytearray(self.pages * self.width)
        super().__init__(self.buffer, self.width, self.height, framebuf.MONO_VLSB)
        self.init_display()
 
    def init_display(self):
        for cmd in (
            SET_DISP | 0x00,  # off
            # address setting
            SET_MEM_ADDR,
            0x00,  # horizontal
            # resolution and layout
            SET_DISP_START_LINE | 0x00,
            SET_SEG_REMAP | 0x01,  # column addr 127 mapped to SEG0
            SET_MUX_RATIO,
            self.height - 1,
            SET_COM_OUT_DIR | 0x08,  # scan from COM[N] to COM0
            SET_DISP_OFFSET,
            0x00,
            SET_COM_PIN_CFG,
            0x02 if self.width > 2 * self.height else 0x12,
            # timing and driving scheme
            SET_DISP_CLK_DIV,
            0x80,
            SET_PRECHARGE,
            0x22 if self.external_vcc else 0xF1,
            SET_VCOM_DESEL,
            0x30,  # 0.83*Vcc
            # display
            SET_CONTRAST,
            0xFF,  # maximum
            SET_ENTIRE_ON,  # output follows RAM contents
            SET_NORM_INV,  # not inverted
            # charge pump
            SET_CHARGE_PUMP,
            0x10 if self.external_vcc else 0x14,
            SET_DISP | 0x01,
        ):  # on
            self.write_cmd(cmd)
        self.fill(0)
        self.show()
 
    def poweroff(self):
        self.write_cmd(SET_DISP | 0x00)
 
    def poweron(self):
        self.write_cmd(SET_DISP | 0x01)
 
    def contrast(self, contrast):
        self.write_cmd(SET_CONTRAST)
        self.write_cmd(contrast)
 
    def invert(self, invert):
        self.write_cmd(SET_NORM_INV | (invert & 1))
 
    def show(self):
        x0 = 0
        x1 = self.width - 1
        if self.width == 64:
            # displays with width of 64 pixels are shifted by 32
            x0 += 32
            x1 += 32
        self.write_cmd(SET_COL_ADDR)
        self.write_cmd(x0)
        self.write_cmd(x1)
        self.write_cmd(SET_PAGE_ADDR)
        self.write_cmd(0)
        self.write_cmd(self.pages - 1)
        self.write_data(self.buffer)
 
 
class SSD1306_I2C(SSD1306):
    def __init__(self, width, height, i2c, addr=0x3C, external_vcc=False):
        self.i2c = i2c
        self.addr = addr
        self.temp = bytearray(2)
        self.write_list = [b"\x40", None]  # Co=0, D/C#=1
        super().__init__(width, height, external_vcc)
 
    def write_cmd(self, cmd):
        self.temp[0] = 0x80  # Co=1, D/C#=0
        self.temp[1] = cmd
        self.i2c.writeto(self.addr, self.temp)
 
    def write_data(self, buf):
        self.write_list[1] = buf
        self.i2c.writevto(self.addr, self.write_list)
 
 
class SSD1306_SPI(SSD1306):
    def __init__(self, width, height, spi, dc, res, cs, external_vcc=False):
        self.rate = 10 * 1024 * 1024
        dc.init(dc.OUT, value=0)
        res.init(res.OUT, value=0)
        cs.init(cs.OUT, value=1)
        self.spi = spi
        self.dc = dc
        self.res = res
        self.cs = cs
        import time
 
        self.res(1)
        time.sleep_ms(1)
        self.res(0)
        time.sleep_ms(10)
        self.res(1)
        super().__init__(width, height, external_vcc)
 
    def write_cmd(self, cmd):
        self.spi.init(baudrate=self.rate, polarity=0, phase=0)
        self.cs(1)
        self.dc(0)
        self.cs(0)
        self.spi.write(bytearray([cmd]))
        self.cs(1)
 
    def write_data(self, buf):
        self.spi.init(baudrate=self.rate, polarity=0, phase=0)
        self.cs(1)
        self.dc(1)
        self.cs(0)
        self.spi.write(buf)
        self.cs(1)

评论

user-avatar