文章目录
本文从内存的显示,缓存的读写来进行研究
python
class SSD1306(object):
def __init__(self, pinout, height=32, external_vcc=True, i2c_devid=DEVID):
self.external_vcc = external_vcc
self.height = 32 if height == 32 else 64
self.pages = int(self.height / 8)
self.columns = 128
self.i2c = pyb.I2C(1)
self.i2c.init(pyb.I2C.MASTER, baudrate=400000) # 400kHz
self.devid = i2c_devid
self.offset = 1
self.cbuffer = bytearray(2)
self.cbuffer[0] = CTL_CMD
def clear(self):
self.buffer = bytearray(self.offset + self.pages * self.columns)
if self.offset == 1:
self.buffer[0] = CTL_DAT
def display(self):
self.write_command(COLUMNADDR)
self.write_command(0)
self.write_command(self.columns - 1)
self.write_command(PAGEADDR)
self.write_command(0)
self.write_command(self.pages - 1)
if self.offset == 1:
self.i2c.send(self.buffer, addr=self.devid, timeout=5000)
else:
self.dc.high()
self.spi.send(self.buffer)
def set_pixel(self, x, y, state):
index = x + (int(y / 8) * self.columns)
if state:
self.buffer[self.offset + index] |= (1 << (y & 7))
else:
self.buffer[self.offset + index] &= ~(1 << (y & 7))
对于这段代码,我们可以知道,OLED设置了两个缓冲区,一个是命令缓冲区cbuffer ,一个是buffer数据缓冲区。
在读写数据前,我们要使用clear函数,来创建缓冲区,
self.buffer = bytearray(self.offset + self.pages * self.columns)
我们来粗略的计算一下这个buffer。
8*128+1 =1025
绘制字体的代码,需要
python
# 计算真实的x坐标,怎么计算
#初始位置x,第几个字符*放缩尺寸*字符的列数,space * char_number间隔
char_offset = x + char_number * size * font.cols + space * char_number
#像素的偏移,除了char_offset,还有字符内部是第几列(这个是x的偏移乘以放缩尺寸),Point_row是尺寸增加的额外补全点
pixel_offset = char_offset + char_column * size + point_row
#计算出最后的坐标,使用128减之后,代表
return 128 - pixel_offset
python
def draw_text(self, x, y, string, size=1, space=1):
def pixel_x(char_number, char_column, point_row):
char_offset = x + char_number * size * font.cols + space * char_number
pixel_offset = char_offset + char_column * size + point_row
return self.columns - pixel_offset
def pixel_y(char_row, point_column):
char_offset = y + char_row * size
return char_offset + point_column
def pixel_mask(char, char_column, char_row):
char_index_offset = ord(char) * font.cols
#取到对应的font.py数组
return font.bytes[char_index_offset + char_column] >> char_row & 0x1
# 0x1 是二进制的 00000001。& 运算符是位与运算符,它的作用是从字节的最低位提取出那一位的值(0 或 1)。
# & 0x1 操作确保我们只关注字节的最低位,判断这一位是 1(点亮)还是 0(不点亮)。
pixels = (
(pixel_x(char_number, char_column, point_row),#(第几个字母,字母列定位,生成多个(尺寸的点))放大尺寸
pixel_y(char_row, point_column),#
pixel_mask(char, char_column, char_row))
for char_number, char in enumerate(string)
for char_column in range(font.cols)#cols=5
for char_row in range(font.rows)#rows=8
for point_column in range(size)
for point_row in range(1, size + 1))
for pixel in pixels:
self.set_pixel(*pixel)
这个 draw_text
函数的作用是在 OLED 显示屏上显示文本。它通过将文本字符串中的每个字符逐个绘制到显示屏上,模拟了字符的像素显示效果。以下是对该函数的详细分析:
函数作用
draw_text
负责将字符串中的每个字符逐个绘制到 OLED 显示屏上。它根据字符的大小、间距以及具体的像素位置,计算出每个字符应该显示的像素位置,并调用 set_pixel
来设置相应的像素点。
参数解释
x
:文本的起始 x 坐标,表示文本在屏幕的水平位置。y
:文本的起始 y 坐标,表示文本在屏幕的垂直位置。string
:要显示的字符串。size
:字符的大小缩放比例,默认为 1,表示原始大小。space
:字符之间的间隔,默认为 1。
嵌套函数分析
-
pixel_x
:- 计算当前字符的像素 x 坐标。
- 参数:
char_number
: 当前字符在字符串中的位置。char_column
: 当前字符的列位置(表示字符的某一列像素)。point_row
: 字符点阵的某一行的像素。
- 公式解释:
char_offset
: 基于x
坐标和字符的编号、大小、间距,计算当前字符的偏移量。pixel_offset
: 计算字符在列方向上的像素偏移量。self.columns
用来确保不超出显示屏的宽度。
-
pixel_y
:- 计算当前字符的像素 y 坐标。
- 参数:
char_row
: 当前字符在点阵中的行数。point_column
: 点阵某一列的像素点。
- 公式解释:
char_offset
: 基于y
坐标和字符的行数、大小,计算字符在垂直方向的偏移量。- 最终返回的是这个字符的像素点在显示屏中的 y 坐标。
-
pixel_mask
:- 获取当前字符在某一行某一列的像素点是亮(1)还是灭(0)。
- 参数:
char
: 当前字符。char_column
: 当前字符的列位置。char_row
: 当前字符的行位置。
- 通过
ord(char)
获取字符的 ASCII 编码,然后从字体数组中找到该字符的位图信息,具体到每一个像素点。
主代码分析
-
pixels = (...)
:- 这是一个生成器表达式,它遍历字符串中的每个字符,逐行逐列地处理字符的每个像素点。每个字符由
font.cols
列和font.rows
行组成。 - 它遍历每个字符的所有像素点,并调用上面定义的
pixel_x
,pixel_y
,pixel_mask
来确定每个像素点的 x 和 y 坐标,以及它是否点亮(通过pixel_mask
函数)。 - 该生成器最终生成一个包含了所有像素信息的三元组
(pixel_x, pixel_y, pixel_mask)
,并将其保存为pixels
。
- 这是一个生成器表达式,它遍历字符串中的每个字符,逐行逐列地处理字符的每个像素点。每个字符由
-
for pixel in pixels:
:- 遍历生成的所有像素点,并调用
self.set_pixel()
来设置 OLED 显示屏上相应位置的像素点。 self.set_pixel(*pixel)
:将pixel
作为位置参数解包,传入set_pixel()
,从而在 OLED 上设置对应的像素。
- 遍历生成的所有像素点,并调用
逻辑流程
- 遍历字符串 :函数首先遍历传入的字符串
string
,并对每个字符进行处理。 - 逐列处理字符像素 :每个字符由多个列组成,每列包含多个像素点。通过
char_column
和char_row
逐个遍历字符的点阵。 - 获取像素信息 :通过
pixel_x
、pixel_y
和pixel_mask
获取字符中每个像素点的位置和状态(亮或灭)。 - 绘制到显示屏 :通过
self.set_pixel()
将每个像素点绘制到 OLED 显示屏上。
总结
draw_text
函数的主要目的是在 OLED 显示屏上绘制文本字符串。它通过遍历每个字符的像素点阵信息,将字符的像素映射到显示屏的相应位置,并通过设置像素来显示文本内容。
如果需要在显示屏上显示不同大小、不同间隔的文本,或者需要精确控制字符在屏幕上的位置,可以通过修改 size
和 space
参数来实现。
难点的解析:
生成器的代码:
这个生成器表达式 pixels = (...)
是一个复杂的多重循环,它遍历了字符串中的每个字符、字符的每个列、每列的每一行、行中的每一个像素点,然后计算出每个像素点的 x 坐标、y 坐标,以及该像素点是否点亮(通过 pixel_mask
函数决定)。
生成器的主要逻辑分解:
这个生成器由多个嵌套的 for
循环构成。它的执行过程可以分为以下几个步骤:
for char_number, char in enumerate(string)
:
- 这是最外层的循环,遍历字符串
string
中的每个字符。 char_number
:字符在字符串中的索引,用于计算字符在屏幕上的横向位置。char
:当前字符。
for char_column in range(font.cols)
:
- 这是第二层循环,遍历当前字符在字体点阵中的列。
font.cols
是字符的列数(即字符的宽度,以像素为单位),通常一个字符在点阵字体中是 5 列。 char_column
:当前列的索引,用于计算字符点阵中的列像素。
for char_row in range(font.rows)
:
- 这是第三层循环,遍历当前列中字符的行。
font.rows
是字符的行数(即字符的高度,以像素为单位),通常一个字符在点阵字体中有 8 行。 char_row
:当前行的索引,用于计算字符点阵中的行像素。
-
for point_column in range(size)
:- 这是第四层循环,用于处理字符放大(缩放)时的列像素扩展。
size
是字符的缩放大小,range(size)
表示在 x 轴上重复size
次,以模拟字符的放大效果。 point_column
:缩放列的索引,用于实现字符的宽度放大。
- 这是第四层循环,用于处理字符放大(缩放)时的列像素扩展。
-
for point_row in range(1, size + 1)
:
- 这是第五层循环,类似于
point_column
,用于处理字符放大时的行像素扩展。在 y 轴上重复size
次,以实现字符的高度放大。 point_row
:缩放行的索引,用于实现字符的高度放大。
每次生成的元组 (pixel_x, pixel_y, pixel_mask)
:
这三个值是通过每层循环计算出来的,它们表示当前像素点的 x 坐标、y 坐标和该像素点是否点亮的状态。每次生成一个这样的三元组,传递给 set_pixel()
方法,来在屏幕上绘制这个像素点。
-
pixel_x(char_number, char_column, point_row)
:通过字符的索引
char_number
和当前列索引char_column
,结合字符缩放后的列索引point_row
计算出这个像素点在屏幕上的 x 坐标。 -
pixel_y(char_row, point_column)
:通过字符的行索引
char_row
和缩放后的行索引point_column
计算出这个像素点在屏幕上的 y 坐标。 -
pixel_mask(char, char_column, char_row)
:通过字符的 ASCII 值
char
以及字符点阵中的行列位置来确定这个像素点是否需要点亮(返回 1)或不点亮(返回 0)。
生成器的整体流程
假设我们有一个字符串 "A"
,字体点阵为 5 列 8 行,size = 2
,那么生成器的执行流程大致如下:
-
for char_number, char in enumerate("A")
:- 遍历字符串
"A"
中的字符,当前字符是"A"
,char_number = 0
。
- 遍历字符串
-
for char_column in range(font.cols)
:- 遍历
"A"
的点阵列,假设"A"
是 5 列字符,这个循环会从0
遍历到4
。
- 遍历
-
for char_row in range(font.rows)
:- 遍历
"A"
每列的点阵行,假设"A"
是 8 行字符,这个循环会从0
遍历到7
。
- 遍历
-
for point_column in range(size)
:- 假设
size = 2
,表示字符需要放大 2 倍,这里会重复每列两次。
- 假设
-
for point_row in range(1, size + 1)
:- 同样地,字符的每一行也会放大 2 倍,行像素重复 2 次。
举例
假设 "A"
的点阵如下(5x8 字符):
01110
10001
10001
11111
10001
10001
10001
10001
生成器会依次生成 "A"
的每个像素点的坐标和亮度信息。通过缩放,每个原始像素点都会被放大 2 倍,从而使 "A"
的像素图显示更大。
总结
这个生成器的作用是通过多重嵌套循环遍历字符串中的每个字符,并处理字符的点阵像素信息。在考虑字符的大小缩放和位置偏移的基础上,生成对应的屏幕坐标和像素状态,最后通过 set_pixel()
绘制到 OLED 显示屏上。
一个例子:
python
cols = 5
rows = 8
bytes = [
0x00, 0x00, 0x00, 0x00, 0x00,
0x3E, 0x5B, 0x4F, 0x5B, 0x3E,
0x是一个八位的数,就是一列。每一行,就是一个字符的表示。
0x3E -> 00111110
0x5B -> 01011011
0x4F -> 01001111
0x5B -> 01011011
0x3E -> 00111110
00111110 -> ******
01011011 -> * ** **
01001111 -> * ****
01011011 -> * ** **
00111110 -> ******
如上图表示。所以每个字符都是这样保存和取用的。
反转后的文本绘制
修改过上下翻转的字符绘制:
python
#可以修改一下函数名
# draw_text_normal()
def draw_text(self, x, y, string, size=1, space=1):
def pixel_x(char_number, char_column, point_row):
char_offset = x + char_number * size * font.cols + space * char_number
pixel_offset = char_offset + char_column * size + point_row
# return self.columns - pixel_offset
return pixel_offset *************************************************************
def pixel_y(char_row, point_column):
#char_offset = y + char_row * size
#另一种布局,修改一下坐标
#原来的布局
# char_offset=y+char_row*size
char_offset = y + (font.rows - 1 - char_row) * size***************************
#y垂直方向不设置间隔
# return char_offset + point_column
return char_offset + point_column
def pixel_mask(char, char_column, char_row):
char_index_offset = ord(char) * font.cols
return font.bytes[char_index_offset + char_column] >> char_row & 0x1
pixels = (
(pixel_x(char_number, char_column, point_row),
pixel_y(char_row, point_column),
pixel_mask(char, char_column, char_row))
for char_number, char in enumerate(string)
for char_column in range(font.cols)
for char_row in range(font.rows)
for point_column in range(size)
for point_row in range(1, size + 1))
for pixel in pixels:
self.set_pixel(*pixel)
修改了星标这两个地方。
竖直布局
为了实现竖直布局的书写,我们的屏幕从(128,64)->(64,128)
我们不需要修改mask的部分,只需要把,求y和x的逻辑进行交换就可以。
python
def draw_ylayout(self, x, y, string, size=1, space=1):
def pixel_y(char_number,char_column, point_row):
char_offset = y + char_number * size * font.cols+space * char_number
pixel_offset =char_offset+char_column * size + point_row
#y垂直方向不设置间隔
# return char_offset + point_column
return pixel_offset
def pixel_x( char_row , point_column):
#行的距离域(128->64)偏移从(5->8)s
char_offset = x + char_row * size
# return self.columns - pixel_offset
return char_offset + point_column
def pixel_mask(char, char_column, char_row):
char_index_offset = ord(char) * font.cols
return font.bytes[char_index_offset + char_column] >> char_row & 0x1
pixels = ((pixel_x(char_row, point_column),
pixel_y(char_number, char_column, point_row),
pixel_mask(char, char_column, char_row))
for char_number, char in enumerate(string)
for char_column in range(font.cols)
for char_row in range(font.rows)
for point_column in range(1, size + 1)
for point_row in range(size))
for pixel in pixels:
self.set_pixel(*pixel)
本来本文尝试实现90度旋转,但是因为最终需要的模式不同,非方阵,矩阵的旋转很容易出现溢出。尝试了几个生成的代码,没有比较满意的。
下面这个应该有错,但现在不想探索了。
有问题的旋转
python
def rotate_buffer(self):
# 旋转后的缓冲区
rotated_buffer = bytearray(1025) # 保留控制字节
rotated_buffer[0] = self.buffer[0] # 保留控制字节
for x in range(128):
for y in range(64):
# 计算原缓冲区中的位置
original_index = 1 + (x * 8) + (y // 8)
original_bit = (self.buffer[original_index] >> (y % 8)) & 0x01
# 计算旋转后的位置
new_x = y # 90度旋转,新列
new_y = 127 - x # 反转行
# 计算新缓冲区中的位置
new_index = 1 + (new_x * 8) + (new_y // 8)
if original_bit: # 如果当前像素为1
rotated_buffer[new_index] |= (1 << (new_y % 8))
# 用旋转后的缓冲区替换原缓冲区
self.buffer = rotated_buffer
180度旋转
旋转180度的反转代码是好用的。
python
def rotate_180(self):
# 创建一个新的缓冲区来存储反转后的内容
new_buffer = bytearray(self.offset + self.pages * self.columns)
# 复制控制字节
new_buffer[0] = self.buffer[0]
# 反转每个字节和行的顺序
for page in range(self.pages): # 这里 pages 是 8,因为屏幕高度为 64 像素
for column in range(self.columns): # 这里 columns 是 128
# 计算反转后的索引
original_index = 1 + page * self.columns + column # 原始索引
new_index = 1 + (self.pages - 1 - page) * self.columns + (self.columns - 1 - column) # 新索引
# 将字节反转并赋值到新的缓冲区
new_buffer[new_index] = self.buffer[original_index] ^ 0xFF # 反转字节
# 更新原始缓冲区
self.buffer = new_buffer
代码并不难,但是旋转感觉自己绕来绕去,以后要多写文档,理清楚思路。180旋转直接就可以替代反转后的文本绘制
额外的绘制函数坐标轴。
坐标轴
绘制了正方向,设置了一下角度,按照原来的。
可调参数不多,需要进一步调整。
python
def draw_axes(self, origin_x=None, origin_y= None, length_x=None, length_y=None):
# 绘制 X 轴
if origin_x is None:
origin_x = self.columns-38
if origin_y is None:
origin_y = self.height-10
# 默认 X 轴长度为屏幕宽度
if length_x is None:
length_x = self.columns
# 默认 Y 轴长度为屏幕高度
if length_y is None:
length_y = self.height
for x in range(1, origin_x):
self.set_pixel(x, origin_y, 1)
# 绘制 Y 轴
for y in range(origin_y, origin_y - length_y, -1):
self.set_pixel(origin_x, y, 1)
#绘制单位
self.draw_text(110,56,"X/s")
self.draw_text(1, 1, "Y/d")
#绘制横的三角
self.draw_triangle(0,54,5,1)
self.draw_triangle(90, 0, 5, 0)
#绘制竖的三角
#是线,不是填充的。
def draw_triangle(self, x, y, size=2,direction=1):
#认为是横的三角
if 1 == direction:
for i in range(1, size + 1):
self.set_pixel(x + i, y, 1)
self.set_pixel(x + i, y - i, 1)
self.set_pixel(x + i, y + i, 1)
else:
#认为是竖的三角
for i in range(1, size + 1):
self.set_pixel(x, y + i, 1)
self.set_pixel(x - i, y + i, 1)
self.set_pixel(x + i, y + i, 1)
绘制矩形
python
def draw_rectangle(self, x, y, width, height, filled=True):
"""
绘制一个方块或矩形
:param x: 方块左上角的 X 坐标
:param y: 方块左上角的 Y 坐标
:param width: 方块的宽度
:param height: 方块的高度
:param filled: 是否填充方块(True 为填充,False 为仅绘制边框)
"""
# 绘制填充的方块
if filled:
for i in range(x, x + width):
for j in range(y, y + height):
self.set_pixel(i, j, 1)
else:
# 绘制空心方块(仅边框)
# 上边框
for i in range(x, x + width):
self.set_pixel(i, y, 1)
# 下边框
for i in range(x, x + width):
self.set_pixel(i, y + height - 1, 1)
# 左边框
for j in range(y, y + height):
self.set_pixel(x, j, 1)
# 右边框
for j in range(y, y + height):
self.set_pixel(x + width - 1, j, 1)
# 显示绘制的方块
self.display()
可以利用这个来绘制直方图。
绘制曲线(正弦波)
python
import math#记得添加这个库
def draw_curve(self, start_x=98, start_y=32, step=-1):
#可绘制的区域x[98,0],y[54,0]数越大,绘制的是越小,或者说越早的值
# """
# 从 x = 98 处开始动态绘制曲线,x 递减到 0 时重新更新显示器并从新开始绘制。
# :param start_x: 起始 x 坐标,默认为 98
# :param start_y: 起始 y 坐标,默认为屏幕中间
# :param step: 每次 x 变化的步长,默认为 -1(向左绘制)
# """
x = start_x
y = start_y
while x >= 0:
# 根据某个逻辑更新 y 值(这里示例使用正弦函数进行动态曲线绘制)
y = int(32 + 10 * math.sin(x / 10))
# 绘制当前像素
self.set_pixel(x, y, 1)
# 更新 x 值
x += step
# 如果 x 达到 0,刷新显示器并清空屏幕
if x < 0:
# 刷新显示内容
self.display()
# 清空显示屏以重新绘制曲线
self.clear()
#绘制坐标轴
self.draw_axes()
# 重新设置 x 为 98,重新开始绘制曲线
x = 98
# 显示绘制的点
self.display()
控制命令
python
def poweron(self):
if self.offset == 1:
pyb.delay(10)
else:
self.res.high()
pyb.delay(1)
self.res.low()
pyb.delay(10)
self.res.high()
pyb.delay(10)
def poweroff(self):
self.write_command(DISPLAYOFF)
def contrast(self, contrast):
self.write_command(SETCONTRAST)
self.write_command(contrast)
屏幕的滚动
python
def write_command(self, command_byte):
self.cbuffer[1] = command_byte
self.i2c.send(self.cbuffer, addr=self.devid, timeout=5000)
python
def setup_scroll(self):
# Step 1: 停止任何正在进行的滚动
self.write_command(DEACTIVATE_SCROLL)
# Step 2: 设置水平滚动,从右向左滚动
self.write_command(LEFT_HORIZONTAL_SCROLL) # 左水平滚动
self.write_command(0x00) # 虚拟字节
self.write_command(0x00) # 起始页地址 (0表示第1页)
self.write_command(0x00) # 设置滚动速度,0x00 是较快滚动
self.write_command(0x07) # 结束页地址 (7表示第8页)
self.write_command(0x00) # 无垂直偏移
self.write_command(0xFF) # 无垂直偏移
就先记录到这里。