本项目基于RP2350开发板,配合分辨率为128×64的SSD1306 OLED显示屏,利用MicroPython语言实现了一个动态的三维立体旋转正方体视觉效果。通过I2C通信接口完成了显示模块与微控制器的连接,结合数学三维变换与投影算法,展示了一个简洁的3D旋转动画,体现了硬件驱动与三维图形渲染的融合应用。
硬件平台介绍
- 核心控制器:RP2350微控制器,支持MicroPython开发。
- 显示屏模块:SSD1306 OLED显示屏,分辨率128×64,采用I2C接口通信。
- 连接方式:通过I2C总线进行通信,SCL线连接至GPIO5,SDA线连接至GPIO4,通信速率设定为400kHz,保证数据传输效率和稳定性。

软件环境与工具
- 编程语言: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设备地址
- 若打印出的是别的,需要在程序中修改,此处“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)
评论