0%

一、oled屏幕

分辨率: 128 * 64
尺寸:0.96英寸
驱动芯片:ssd1306
电压:3.3~5V
功耗:0.06w
寿命:16000h
显示:双色(黄、蓝)
字库:无内置字库(软件控制显示)
通信: 4线spi/i2c(默认spi,使用i2c需调整相应屏后电阻)

# 针脚
GND ------- 接地
VCC ------- 3.3 ~ 5V
D0 ------- 时钟线
D1 ------- 数据线
RES ------- 复位(低电平有效)
DC ------- spi 通信 data/command 选择;i2c通信时设置i2c地址
cs ------- 片选(spi通信,低电平有效)

SPI通信

SPI(Serial Peripheral Interface) 串行外设接口。 以主从方式进行工作,一台master主机可以连接多台slave从机,通过CS片选可以指定选中的从机。通常为4线或3线(单向传输数据)

# 4wire SPI
SCLK ------ 时钟
MOSI ------ Master Out Slave Input
MISO ------ Maset Input Slave Out
CS ------ 片选

二、连接raspberrypi

1.启用spi

sudo raspi-config

找到advanced options 下的spi,选择启用

2.连接

三、点亮

在github上查找相关ssd1306的相关js驱动,找到一个可用的驱动,为防止丢失,已Fork https://github.com/yancoding/ssd1306-spi

index.js

const rpio = require("rpio");

class SSD1306 {
constructor({
width,
height,
resetPin,
dcPin,
spiChip,
rpio
}) {
this.EXTERNAL_VCC = 0x1
this.SWITCH_CAP_VCC = 0x2

this.SET_LOW_COLUMN = 0x00
this.SET_HIGH_COLUMN = 0x10
this.SET_MEMORY_MODE = 0x20
this.SET_COL_ADDRESS = 0x21
this.SET_PAGE_ADDRESS = 0x22
this.RIGHT_HORIZ_SCROLL = 0x26
this.LEFT_HORIZ_SCROLL = 0x27
this.VERT_AND_RIGHT_HORIZ_SCROLL = 0x29
this.VERT_AND_LEFT_HORIZ_SCROLL = 0x2A
this.DEACTIVATE_SCROLL = 0x2E
this.ACTIVATE_SCROLL = 0x2F
this.SET_START_LINE = 0x40
this.SET_CONTRAST = 0x81
this.CHARGE_PUMP = 0x8D
this.SEG_REMAP = 0xA0
this.SET_VERT_SCROLL_AREA = 0xA3
this.DISPLAY_ALL_ON_RESUME = 0xA4
this.DISPLAY_ALL_ON = 0xA5
this.NORMAL_DISPLAY = 0xA6
this.INVERT_DISPLAY = 0xA7
this.DISPLAY_OFF = 0xAE
this.DISPLAY_ON = 0xAF
this.COM_SCAN_INC = 0xC0
this.COM_SCAN_DEC = 0xC8
this.SET_DISPLAY_OFFSET = 0xD3
this.SET_COM_PINS = 0xDA
this.SET_VCOM_DETECT = 0xDB
this.SET_DISPLAY_CLOCK_DIV = 0xD5
this.SET_PRECHARGE = 0xD9
this.SET_MULTIPLEX = 0xA8

this.MEMORY_MODE_HORIZ = 0x00
this.MEMORY_MODE_VERT = 0x01
this.MEMORY_MODE_PAGE = 0x02

let options = {
gpiomem: false,
mapping: "gpio",
...rpio
};

this._screenWidth = width;
this._screenHeight = height;
this._resetPin = resetPin;
this._dcPin = dcPin;
this._spiChip = spiChip;

this._rpioOptions = options;

this._screenBuffer = Buffer.alloc((width * height) / 8).fill(0x00);
}

init() {
rpio.init(this._rpioOptions);
rpio.spiBegin();
rpio.spiChipSelect(this._spiChip);
// 8MHz
rpio.spiSetClockDivider(128);

rpio.open(this._resetPin, rpio.OUTPUT, rpio.HIGH);
rpio.open(this._dcPin, rpio.OUTPUT, rpio.LOW);

// Init the OLED screen
rpio.msleep(0.01);
this.reset();
this.command(Buffer.from([this.DISPLAY_OFF]));
this.command(Buffer.from([this.SET_DISPLAY_CLOCK_DIV, 0x80]));

// Setup the right screen size
if (this._screenHeight === 64) {
this.command(Buffer.from([this.SET_MULTIPLEX, 0x3F]));
this.command(Buffer.from([this.SET_COM_PINS, 0x12]));
} else {
this.command(Buffer.from([this.SET_MULTIPLEX, 0x1F]));
this.command(Buffer.from([this.SET_COM_PINS, 0x02]));
}

this.command(Buffer.from([this.SET_DISPLAY_OFFSET, 0x00]));
this.command(Buffer.from([this.SET_START_LINE | 0x00]));
this.command(Buffer.from([this.CHARGE_PUMP, 0x14]));
this.command(Buffer.from([this.SET_MEMORY_MODE, 0x00]));
this.command(Buffer.from([this.SEG_REMAP | 0x01]));
this.command(Buffer.from([this.COM_SCAN_DEC]));
this.command(Buffer.from([this.SET_CONTRAST, 0x8f]));
this.command(Buffer.from([this.SET_PRECHARGE, 0xf1]));
this.command(Buffer.from([this.SET_VCOM_DETECT, 0x40]));
this.command(Buffer.from([this.DISPLAY_ALL_ON_RESUME]));
this.command(Buffer.from([this.NORMAL_DISPLAY]));
this.command(Buffer.from([this.DISPLAY_ON]));
}

clearDisplay() {
this._screenBuffer.fill(0);
}

invertDisplay() {
this.command(Buffer.from([this.INVERT_DISPLAY]));
}

normalDisplay() {
this.command(Buffer.from([this.NORMAL_DISPLAY]));
}

draw() {
this.command(Buffer.from([this.SET_MEMORY_MODE, this.MEMORY_MODE_HORIZ]));
this.command(Buffer.from([this.SET_PAGE_ADDRESS, 0x00, 0x07]));
this.command(Buffer.from([this.SET_COL_ADDRESS, 0x00, 0x7f]));
this.command(Buffer.from([this.DISPLAY_ON]));
this.data(this._screenBuffer);
}

drawPixel(x, y, on) {
if (x < 0 || x > this._screenWidth - 1 || y < 0 || y > this._screenHeight - 1) {
return;
}

let page = Math.floor(y / 8);
let offset = y % 8;

if (on) {
this._screenBuffer[page * this._screenWidth + x] |= (0x1 << offset);
} else {
this._screenBuffer[page * this._screenWidth + x] &= ((0x1 << offset) ^ 0xff);
}
}

reset() {
rpio.write(this._resetPin, rpio.LOW);
rpio.msleep(10);
rpio.write(this._resetPin, rpio.HIGH);
}

command(buffer) {
rpio.spiWrite(buffer, buffer.length);
}

data(buffer) {
// Set DC to high to write data
rpio.write(this._dcPin, rpio.HIGH);

rpio.spiWrite(buffer, buffer.length);

rpio.write(this._dcPin, rpio.LOW);
}

end() {
rpio.spiEnd();
rpio.close(this._resetPin, rpio.PIN_RESET);
rpio.close(this._dcPin, rpio.PIN_RESET);
}
}

module.exports = SSD1306;

使用该驱动

// 引入
const SSD1306 = require('ssd1306')
const rpio = require("rpio")

// 创建实例
const ssd1306 = new SSD1306({
width: 128,
height: 64,
resetPin: 24,
dcPin: 23,
spiChip: 0,
rpio,
})

// 初始化
ssd1306.init()

// 依次点亮每个点阵
for (let i = 1; i < 128; i++) {
for (let j = 1; j < 64; j++) {
ssd1306.drawPixel(i, j, true)
ssd1306.draw()
}
}

如果可以依次点亮屏幕上的每一点,说明oled一切正常,可以尝试做些其他的事情了

四、一个像素点是如何点亮的?

其实oled与大多数的点阵屏幕点亮原理都很类似,具体还要看下使用手册

oled 水平共有64个共阴极,那么如果在某一像素点上加上高电平,即可点亮该点

128 * 64 共有64行,128列,64行在数据层分为8页,在第一页,也就是128 * 8的区域,如果在第一列写入的数据为0xff,即二进制的 11111111,则会点亮第一页第一列的8个像素点,上为高位D0,下为低位D7。

所以屏幕的显存为128 * 8 byte,通过ssd1306的指令写入显存,即可显示我们要展示的内容

传统做法是对常用的字符生成不通大小的点阵数据,也就是所谓的字库,生成方式称为字体取模,这种方式有一定的局限性,我们可以考虑采用另一种方式来显示我们想要的任何图形

五、借助canvas绘图

oled屏幕的尺寸是固定的128 * 64尺寸,这点非常类似canvas的画布,我们因此可以将屏幕当成canvas的画布

首先创建一个屏幕大小的画布, 然后借助canvas的api可以绘制出我们想要的图案,再取出画布上的像素数据,转换为相应的数据格式,写入oled的显存,即可在屏幕上展示画布上的图案

注意这里由于屏幕每个点只有两个状态,亮或暗,因此我们在画布上绘制时,只使用黑色绘制即可

const rpio = require("rpio");
const {
createCanvas,
loadImage
} = require('canvas')

class SSD1306 {
constructor({
width,
height,
resetPin,
dcPin,
spiChip,
rpio
}) {
this.EXTERNAL_VCC = 0x1
this.SWITCH_CAP_VCC = 0x2

this.SET_LOW_COLUMN = 0x00
this.SET_HIGH_COLUMN = 0x10
this.SET_MEMORY_MODE = 0x20
this.SET_COL_ADDRESS = 0x21
this.SET_PAGE_ADDRESS = 0x22
this.RIGHT_HORIZ_SCROLL = 0x26
this.LEFT_HORIZ_SCROLL = 0x27
this.VERT_AND_RIGHT_HORIZ_SCROLL = 0x29
this.VERT_AND_LEFT_HORIZ_SCROLL = 0x2A
this.DEACTIVATE_SCROLL = 0x2E
this.ACTIVATE_SCROLL = 0x2F
this.SET_START_LINE = 0x40
this.SET_CONTRAST = 0x81
this.CHARGE_PUMP = 0x8D
this.SEG_REMAP = 0xA0
this.SET_VERT_SCROLL_AREA = 0xA3
this.DISPLAY_ALL_ON_RESUME = 0xA4
this.DISPLAY_ALL_ON = 0xA5
this.NORMAL_DISPLAY = 0xA6
this.INVERT_DISPLAY = 0xA7
this.DISPLAY_OFF = 0xAE
this.DISPLAY_ON = 0xAF
this.COM_SCAN_INC = 0xC0
this.COM_SCAN_DEC = 0xC8
this.SET_DISPLAY_OFFSET = 0xD3
this.SET_COM_PINS = 0xDA
this.SET_VCOM_DETECT = 0xDB
this.SET_DISPLAY_CLOCK_DIV = 0xD5
this.SET_PRECHARGE = 0xD9
this.SET_MULTIPLEX = 0xA8

this.MEMORY_MODE_HORIZ = 0x00
this.MEMORY_MODE_VERT = 0x01
this.MEMORY_MODE_PAGE = 0x02


let options = {
gpiomem: false,
mapping: "gpio",
...rpio
};

this._screenWidth = width;
this._screenHeight = height;
this._resetPin = resetPin;
this._dcPin = dcPin;
this._spiChip = spiChip;

this._rpioOptions = options;

this._screenBuffer = Buffer.alloc((width * height) / 8).fill(0x00);

this._ctx = createCanvas(128, 64).getContext('2d')
}

init() {
this._ctx.fillStyle = 'rgba(255, 255, 255, 1)'
this._ctx.fillRect(0, 0, 128, 64)

rpio.init(this._rpioOptions);
rpio.spiBegin();
rpio.spiChipSelect(this._spiChip);
// 8MHz
rpio.spiSetClockDivider(128);

rpio.open(this._resetPin, rpio.OUTPUT, rpio.HIGH);
rpio.open(this._dcPin, rpio.OUTPUT, rpio.LOW);

// Init the OLED screen
rpio.msleep(0.01);
this.reset();
this.command(Buffer.from([this.DISPLAY_OFF]));
this.command(Buffer.from([this.SET_DISPLAY_CLOCK_DIV, 0x80]));

// Setup the right screen size
if (this._screenHeight === 64) {
this.command(Buffer.from([this.SET_MULTIPLEX, 0x3F]));
this.command(Buffer.from([this.SET_COM_PINS, 0x12]));
} else {
this.command(Buffer.from([this.SET_MULTIPLEX, 0x1F]));
this.command(Buffer.from([this.SET_COM_PINS, 0x02]));
}

this.command(Buffer.from([this.SET_DISPLAY_OFFSET, 0x00]));
this.command(Buffer.from([this.SET_START_LINE | 0x00]));
this.command(Buffer.from([this.CHARGE_PUMP, 0x14]));
this.command(Buffer.from([this.SET_MEMORY_MODE, 0x00]));
this.command(Buffer.from([this.SEG_REMAP | 0x01]));
this.command(Buffer.from([this.COM_SCAN_DEC]));
this.command(Buffer.from([this.SET_CONTRAST, 0x8f]));
this.command(Buffer.from([this.SET_PRECHARGE, 0xf1]));
this.command(Buffer.from([this.SET_VCOM_DETECT, 0x40]));
this.command(Buffer.from([this.DISPLAY_ALL_ON_RESUME]));
this.command(Buffer.from([this.NORMAL_DISPLAY]));
this.command(Buffer.from([this.DISPLAY_ON]));
}

getContext(contextID) {
if (contextID === '2d') {
return this._ctx
} else {
throw new Error('getConText(contextID)参数错误')
}
}
clearDisplay() {
this._screenBuffer.fill(0);
}

invertDisplay() {
this.command(Buffer.from([this.INVERT_DISPLAY]));
}

normalDisplay() {
this.command(Buffer.from([this.NORMAL_DISPLAY]));
}

draw() {
this.command(Buffer.from([this.SET_MEMORY_MODE, this.MEMORY_MODE_HORIZ]));
this.command(Buffer.from([this.SET_PAGE_ADDRESS, 0x00, 0x07]));
this.command(Buffer.from([this.SET_COL_ADDRESS, 0x00, 0x7f]));
this.command(Buffer.from([this.DISPLAY_ON]));
this.data(this._screenBuffer);
}

drawPixel(x, y, on) {
if (x < 0 || x > this._screenWidth - 1 || y < 0 || y > this._screenHeight - 1) {
return;
}

let page = Math.floor(y / 8);
let offset = y % 8;

if (on) {
this._screenBuffer[page * this._screenWidth + x] |= (0x1 << offset);
} else {
this._screenBuffer[page * this._screenWidth + x] &= ((0x1 << offset) ^ 0xff);
}
}

reset() {
rpio.write(this._resetPin, rpio.LOW);
rpio.msleep(10);
rpio.write(this._resetPin, rpio.HIGH);
}

command(buffer) {
rpio.spiWrite(buffer, buffer.length);
}

data(buffer) {
// Set DC to high to write data
rpio.write(this._dcPin, rpio.HIGH);

rpio.spiWrite(buffer, buffer.length);

rpio.write(this._dcPin, rpio.LOW);
}

end() {
rpio.spiEnd();
rpio.close(this._resetPin, rpio.PIN_RESET);
rpio.close(this._dcPin, rpio.PIN_RESET);
}

scroll() {
this.command(Buffer.from([this.RIGHT_HORIZ_SCROLL]));
this.command(Buffer.from([this.ACTIVATE_SCROLL]));
this.data(this._screenBuffer);
}

drawToDisplay() {
let pixelArray = []
for (let page = 0; page < 8; page++) {
for (let col = 0; col < 128; col++) {
const imageData = this._ctx.getImageData(col, page * 8, 1, 8)
let byte = 0x00
for (let i = 0, j = 0; i < imageData.data.length; i += 4, j++) {
const r = imageData.data[i]
const g = imageData.data[i + 1]
const b = imageData.data[i + 2]
const a = imageData.data[i + 3]
const average = 0.21 * r + 0.72 * g + 0.07 * b
const bit = average < 128 ? 1 : 0
byte += bit << j
}
pixelArray.push(byte)
}
}
this._screenBuffer = Buffer.from(pixelArray)
this.draw()
}
}

module.exports = SSD1306;

尝试绘制文字

// 引入
const SSD1306 = require('ssd1306')
const rpio = require("rpio")

// 创建实例
const ssd1306 = new SSD1306({
  width: 128,
  height: 64,
  resetPin: 24,
  dcPin: 23,
  spiChip: 0,
  rpio,
})

// 初始化
ssd1306.init()

// 获取canvas 2d 上下文
const ctx = ssd1306.getContext('2d')

ctx.strokeStyle = '#000'
ctx.strokeRect(0, 0, 128, 64)

ctx.font = '18px arial'
ctx.fillStyle = 'rgba(0, 0, 0, 1)'
ctx.textAlign = 'center'
ctx.fillText(`Hello World`, 64, 32)
ssd1306.drawToDisplay()

一、屏幕

这是一块DFROBOT产的5’’ 800x480 TFT 电容屏

默认是按长边方向显示的,如果竖屏使用的话,就需要调整屏幕方向和触摸方向

二、屏幕旋转

修改屏幕旋转可以修改启动配置文件

sudo vim /boot/config.txt

添加

# 默认
display_rotate=0
# 90deg
display_rotate=1
# 180deg
display_rotate=2
# 270deg
display_rotate=3

可以根据需要添加旋转值(顺时针的)

然后重启即可生效sudo shutdown -r now

三、触摸旋转

如果只是屏幕进行了旋转的话,触摸还是保持默认方向,就无法正常操作了,需要调整与屏幕方向一致才行

编辑配置

sudo vim /usr/share/X11/xorg.conf.d/40-libinput.conf
# Match on all types of devices but joysticks
#
# If you want to configure your devices, do not copy this file.
# Instead, use a config snippet that contains something like this:
#
# Section "InputClass"
# Identifier "something or other"
# MatchDriver "libinput"
#
# MatchIsTouchpad "on"
# ... other Match directives ...
# Option "someoption" "value"
# EndSection
#
# This applies the option any libinput device also matched by the other
# directives. See the xorg.conf(5) man page for more info on
# matching devices.

Section "InputClass"
Identifier "libinput pointer catchall"
MatchIsPointer "on"
MatchDevicePath "/dev/input/event*"
Driver "libinput"
EndSection

Section "InputClass"
Identifier "libinput keyboard catchall"
MatchIsKeyboard "on"
MatchDevicePath "/dev/input/event*"
Driver "libinput"
EndSection

Section "InputClass"
Identifier "libinput touchpad catchall"
MatchIsTouchpad "on"
MatchDevicePath "/dev/input/event*"
Driver "libinput"
EndSection

Section "InputClass"
Identifier "libinput touchscreen catchall"
# 90deg
Option "CalibrationMatrix" "0 1 0 -1 0 1 0 0 1"
MatchIsTouchscreen "on"
MatchDevicePath "/dev/input/event*"
Driver "libinput"
EndSection

Section "InputClass"
Identifier "libinput tablet catchall"
MatchIsTablet "on"
MatchDevicePath "/dev/input/event*"
Driver "libinput"
EndSection

libinput touchscreen catchall部分添加一行Option "CalibrationMatrix" "0 1 0 -1 0 1 0 0 1"

注意:

# 90 deg
Option "CalibrationMatrix" "0 1 0 -1 0 1 0 0 1"

# 180 deg
Option "CalibrationMatrix" "-1 0 1 0 -1 1 0 0 1"

# 270 deg
Option "CalibrationMatrix" "0 -1 1 1 0 0 0 0 1"

重启即可

一、安装termux

termux可以在google play商店下载

安装完成后,打开termux,更新软件列表

apt update

更新软件

apt upgrade

二、ssh连接

安装ssh

apt install openssh

启动

sshd

1.密码登录

查看当前用户whoami

设置密码passwd

在电脑上通过设置的密码连接到手机的termux的ssh服务

ssh -p 8022 u0_a112@192.168.199.200

注意:这里的端口为8022, u0_a122为用户名,192.168.199.200为内网手机ip地址

输入密码即可连接

2.密钥免密登录

如果经常连接手机端的ssh,密码还是比较麻烦的,使用ssh密钥登录可以免去每次输入密码的麻烦

首先使用密码连接到手机端ssh

为手机端生成ssh密钥

ssh-keygen -t rsa

全部默认,回车生成默认密钥

执行exitlogout退出登录

下面开始将我们本地的密钥上传到手机端

如果本机还没有密钥的话,可以通过上述的方式生成密钥

然后上传到手机端

ssh-copy-id -p 8022 u0_a112@192.168.199.200

输入密码完成上传

这时再尝试

ssh -p 8022 u0_a112@192.168.199.200

连接手机的ssh服务,即可直接登录,不用输入密码

三、安装nodejs

可以直接通过apt包管理器进行安装

apt install nodejs

安装完成后,可以查看下node的版本

node --version

四、http静态服务器

既然已经可以安装nodejs,那么启动一个http静态服务器还是很简单的

通过npm安装http-server

npm install -g http-server

运行,默认当前目录作为http静态服务的根目录,端口8080

http-server