368 lines
12 KiB
C
368 lines
12 KiB
C
/* oled.c - 128x64 OLED(SSD1306, I2C) + 晶联讯 GB2312 字库 IC(SPI) 驱动
|
||
* 屏幕: I2C 接口(原理图网络 OLED_SCL/OLED_SDA), 与 EEPROM 共用软件 I2C 总线
|
||
* (SCL=PB5 SDA=PB4, 见 iic.h), 无 RST 脚; 地址 OLED_I2C_ADDR=0x78;
|
||
* 与 24C02 的跨任务互斥用 storage_lock(每个完整显示事务内)
|
||
* 字库: SPI, 与 W25Q64 共用三线(ROM_CLK=PB6 ROM_DI=PB7 ROM_DO=PB9), ROM_CS=PC12 独立片选;
|
||
* ROM_DO(PB9) 固定为输入(W25Q64_Init 已配置), 字库读取需持 storage_lock 与 Flash 互斥 */
|
||
|
||
#include "oled.h"
|
||
#include "qrcodegen.h"
|
||
#include "iic.h"
|
||
#include "app_loop.h" /* storage_lock/unlock */
|
||
#include <string.h>
|
||
|
||
#define ROM_CLK_PORT GPIOB
|
||
#define ROM_CLK_PIN GPIO_PIN_6 /* 字库 CLK, 与 W25Q64 CLK 共线 */
|
||
#define ROM_DI_PORT GPIOB
|
||
#define ROM_DI_PIN GPIO_PIN_7 /* 字库 DI, 与 W25Q64 DI 共线 */
|
||
#define ROM_DO_PORT GPIOB
|
||
#define ROM_DO_PIN GPIO_PIN_9 /* 字库 DO(FSO), 与 W25Q64 DO 共线, 恒为输入 */
|
||
#define ROM_CS_PORT GPIOC
|
||
#define ROM_CS_PIN GPIO_PIN_12
|
||
|
||
#define rom_di(a) HAL_GPIO_WritePin(ROM_DI_PORT, ROM_DI_PIN, (GPIO_PinState)(a))
|
||
#define rom_clk(a) HAL_GPIO_WritePin(ROM_CLK_PORT, ROM_CLK_PIN, (GPIO_PinState)(a))
|
||
#define rom_cs(a) HAL_GPIO_WritePin(ROM_CS_PORT, ROM_CS_PIN, (GPIO_PinState)(a))
|
||
#define ROM_DO_READ HAL_GPIO_ReadPin(ROM_DO_PORT, ROM_DO_PIN)
|
||
|
||
static uint8_t s_row = 0; /* 当前页地址(16px 行 × 2) */
|
||
static uint8_t s_col = 0; /* 当前列(像素) */
|
||
|
||
/* 注: 字库 DO(FSO) 与 W25Q64 DO 共线(PB9), 由 W25Q64_Init 固定为输入, 不再做方向切换 */
|
||
/*==== GPIO 初始化 ====*/
|
||
static void oled_gpio_init(void)
|
||
{
|
||
GPIO_InitTypeDef g = {0};
|
||
|
||
__HAL_RCC_GPIOA_CLK_ENABLE();
|
||
__HAL_RCC_GPIOB_CLK_ENABLE();
|
||
__HAL_RCC_GPIOC_CLK_ENABLE();
|
||
|
||
/* 屏幕 I2C 不占 GPIO; 字库 CLK(PB6)/DI(PB7) 与 W25Q64 共线(其 Init 已配为输出),
|
||
* DO(PB9) 恒为输入(其 Init 已配), 这里只配字库独立片选 CS2(PC12), 默认拉高不选中 */
|
||
HAL_GPIO_WritePin(ROM_CS_PORT, ROM_CS_PIN, GPIO_PIN_SET);
|
||
g.Mode = GPIO_MODE_OUTPUT_PP;
|
||
g.Pull = GPIO_PULLUP;
|
||
g.Speed = GPIO_SPEED_FREQ_LOW;
|
||
g.Pin = ROM_CS_PIN; HAL_GPIO_Init(ROM_CS_PORT, &g);
|
||
}
|
||
|
||
/*==== SSD1306 I2C 传输(与 EEPROM 共用软件 I2C 总线) ====*/
|
||
#define OLED_I2C_ADDR 0x78 /* 8 位写地址(7 位 0x3C); 模块 SA0 拉高时为 0x7A */
|
||
|
||
/* 写字节到屏幕(dc=0 指令 / dc=1 数据): START+地址+控制字节+数据+STOP */
|
||
static void oled_write(uint8_t dc, uint8_t dat)
|
||
{
|
||
iicStart();
|
||
iicSendByteAck(OLED_I2C_ADDR);
|
||
iicSendByteAck(dc ? 0x40 : 0x00);
|
||
iicSendByteAck(dat);
|
||
iicStop();
|
||
}
|
||
|
||
/* 数据流: 一次 START 连续写多字节(清屏/画图用, 比逐字节事务快约 3 倍) */
|
||
static void oled_data_begin(void)
|
||
{
|
||
iicStart();
|
||
iicSendByteAck(OLED_I2C_ADDR);
|
||
iicSendByteAck(0x40);
|
||
}
|
||
static void oled_data_byte(uint8_t d) { iicSendByteAck(d); }
|
||
static void oled_data_end(void) { iicStop(); }
|
||
|
||
/*==== 设置页/列地址 ====*/
|
||
static void lcd_address(uint8_t page, uint8_t column)
|
||
{
|
||
oled_write(0, 0xb0 + column); /* 页地址 */
|
||
oled_write(0, ((page & 0xf0) >> 4) | 0x10); /* 列地址高 4 位 */
|
||
oled_write(0, (page & 0x0f)); /* 列地址低 4 位 */
|
||
}
|
||
|
||
/*==== 开机二维码页: 内容=text(设备 ClientID), 左侧 QR 白底暗码, 右侧 ID 文本 ====
|
||
* QR 用 Nayuki qrcodegen 生成(V1~V3 自动, ECC-L, 2px/模块放大), 白底亮块+2 模块静区;
|
||
* 右侧 x=64 起分行打印 ID(每行 8 字符, 最多 3 行) */
|
||
void oled_show_qrcode(const char *text)
|
||
{
|
||
static uint8_t qr[qrcodegen_BUFFER_LEN_FOR_VERSION(3)];
|
||
static uint8_t tmp[qrcodegen_BUFFER_LEN_FOR_VERSION(3)];
|
||
static uint8_t pagebuf[8][64];
|
||
|
||
char id[21];
|
||
strncpy(id, text, 20);
|
||
id[20] = 0;
|
||
|
||
if (!qrcodegen_encodeText(id, tmp, qr, qrcodegen_Ecc_LOW, 1, 3,
|
||
qrcodegen_Mask_AUTO, true)) {
|
||
return; /* 编码失败(ID 为空/超长)则不显示 */
|
||
}
|
||
int qs = qrcodegen_getSize(qr); /* 21/25/29 */
|
||
int px = qs * 2; /* 2px/模块 -> 42/50/58 */
|
||
int x0 = 2;
|
||
int y0 = (64 - px) / 2; /* 垂直居中 */
|
||
|
||
memset(pagebuf, 0, sizeof(pagebuf));
|
||
/* 白底(含 2 模块=4px 静区): 点亮矩形区域 */
|
||
int lx = x0 - 4, rx = x0 + px + 4;
|
||
int ty = y0 - 4, by = y0 + px + 4;
|
||
if (lx < 0) lx = 0; if (rx > 64) rx = 64;
|
||
if (ty < 0) ty = 0; if (by > 64) by = 64;
|
||
for (int y = ty; y < by; y++)
|
||
for (int x = lx; x < rx; x++)
|
||
pagebuf[y >> 3][x] |= (uint8_t)(1u << (y & 7));
|
||
|
||
/* QR 暗模块: 对应像素清位 */
|
||
for (int r = 0; r < qs; r++) {
|
||
for (int c = 0; c < qs; c++) {
|
||
if (qrcodegen_getModule(qr, c, r)) {
|
||
for (int dy = 0; dy < 2; dy++) {
|
||
for (int dx = 0; dx < 2; dx++) {
|
||
int y = y0 + r * 2 + dy;
|
||
int x = x0 + c * 2 + dx;
|
||
pagebuf[y >> 3][x] &= (uint8_t)~(1u << (y & 7));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 写左半屏 64 列(整页覆写, 不影响右侧文本区) */
|
||
storage_lock();
|
||
for (uint8_t pg = 0; pg < 8; pg++) {
|
||
oled_write(0, 0xb0 + pg);
|
||
oled_write(0, 0x00);
|
||
oled_write(0, 0x10);
|
||
oled_data_begin();
|
||
for (uint8_t x = 0; x < 64; x++) oled_data_byte(pagebuf[pg][x]);
|
||
oled_data_end();
|
||
}
|
||
storage_unlock();
|
||
|
||
/* 右侧 ID 文本: x=64 起每行 8 个 ASCII, 最多 3 行 */
|
||
int len = (int)strlen(id);
|
||
oled_locate(0, 8);
|
||
oled_print("ID:");
|
||
for (int r = 0; r < 3 && r * 8 < len; r++) {
|
||
char seg[9];
|
||
memcpy(seg, id + r * 8, 8); /* id 已 NUL 填充, 尾部短串会被 NUL 截断 */
|
||
seg[8] = 0;
|
||
oled_locate(1 + r, 8);
|
||
oled_print(seg);
|
||
}
|
||
}
|
||
|
||
/*==== 关显示(黑屏,断电预警时调用)====*/
|
||
void oled_off(void)
|
||
{
|
||
storage_lock();
|
||
oled_write(0, 0xAE); /* display off */
|
||
storage_unlock();
|
||
}
|
||
|
||
/*==== 全屏清屏 ====*/
|
||
void oled_clear(void)
|
||
{
|
||
storage_lock();
|
||
for (uint8_t i = 0; i < 8; i++) {
|
||
oled_write(0, 0xb0 + i);
|
||
oled_write(0, 0x00);
|
||
oled_write(0, 0x10);
|
||
oled_data_begin();
|
||
for (uint8_t j = 0; j < 128; j++) oled_data_byte(0x00);
|
||
oled_data_end();
|
||
}
|
||
storage_unlock();
|
||
}
|
||
|
||
/*==== 屏幕初始化(SSD1306 兼容序列)====*/
|
||
void oled_init(void)
|
||
{
|
||
HAL_Delay(400); /* 等屏幕/字库上电稳定(I2C 屏无 RST 脚, 靠上电延时) */
|
||
oled_gpio_init();
|
||
rom_cs(1);
|
||
oled_write(0, 0xAE); /* display off */
|
||
oled_write(0, 0x20); /* 寻址模式 */
|
||
oled_write(0, 0x10); /* 水平寻址 */
|
||
oled_write(0, 0xb0);
|
||
oled_write(0, 0xc8); /* COM 扫描方向 */
|
||
oled_write(0, 0x00);
|
||
oled_write(0, 0x10);
|
||
oled_write(0, 0x40); /* 起始行 */
|
||
oled_write(0, 0x81); /* 对比度 */
|
||
oled_write(0, 0xFF);
|
||
oled_write(0, 0xa1); /* 段重映射 */
|
||
oled_write(0, 0xa6); /* 正常显示 */
|
||
oled_write(0, 0xa8); /* 复用率 */
|
||
oled_write(0, 0x3F);
|
||
oled_write(0, 0xa4);
|
||
oled_write(0, 0xd3); /* 显示偏移 */
|
||
oled_write(0, 0x00);
|
||
oled_write(0, 0xd5); /* 时钟分频 */
|
||
oled_write(0, 0xf0);
|
||
oled_write(0, 0xd9); /* 预充电 */
|
||
oled_write(0, 0x22);
|
||
oled_write(0, 0xda); /* COM 硬件配置 */
|
||
oled_write(0, 0x12);
|
||
oled_write(0, 0xdb); /* VCOMH */
|
||
oled_write(0, 0x20);
|
||
oled_write(0, 0x8d); /* DC-DC */
|
||
oled_write(0, 0x14);
|
||
oled_write(0, 0xaf); /* display on */
|
||
oled_clear();
|
||
}
|
||
|
||
/*==== 显示 16x16 点阵(汉字)====*/
|
||
static void display_graphic_16x16(uint8_t page, uint8_t column, const uint8_t *dp)
|
||
{
|
||
storage_lock();
|
||
for (uint8_t j = 2; j > 0; j--) {
|
||
lcd_address(column, page);
|
||
oled_data_begin();
|
||
for (uint8_t i = 0; i < 16; i++) oled_data_byte(*dp++);
|
||
oled_data_end();
|
||
page++;
|
||
}
|
||
storage_unlock();
|
||
}
|
||
|
||
/*==== 显示 8x16 点阵(ASCII)====*/
|
||
static void display_graphic_8x16(uint8_t page, uint8_t column, const uint8_t *dp)
|
||
{
|
||
storage_lock();
|
||
for (uint8_t j = 2; j > 0; j--) {
|
||
lcd_address(column, page);
|
||
oled_data_begin();
|
||
for (uint8_t i = 0; i < 8; i++) oled_data_byte(*dp++);
|
||
oled_data_end();
|
||
page++;
|
||
}
|
||
storage_unlock();
|
||
}
|
||
|
||
/*==== 字库 IC 写字节(指令/地址)====*/
|
||
static void rom_write_byte(uint8_t dat)
|
||
{
|
||
for (int i = 0; i < 8; i++) {
|
||
rom_di(dat & 0x80);
|
||
dat <<= 1;
|
||
rom_clk(0);
|
||
rom_clk(1);
|
||
}
|
||
}
|
||
|
||
/*==== 字库 IC 读一字节 ====*/
|
||
static uint8_t rom_read_byte(void)
|
||
{
|
||
uint8_t ret = 0;
|
||
rom_clk(1);
|
||
for (int i = 0; i < 8; i++) {
|
||
rom_clk(0);
|
||
ret <<= 1;
|
||
if (ROM_DO_READ) ret |= 1;
|
||
rom_clk(1);
|
||
}
|
||
return ret;
|
||
}
|
||
|
||
/*==== 从字库 IC 连续读 DataLen 字节 ====*/
|
||
static void rom_read_n(uint8_t ah, uint8_t am, uint8_t al, uint8_t *buf, uint8_t len)
|
||
{
|
||
storage_lock(); /* 与 W25Q64 共用 SPI 三线, 与 Flash 读写互斥 */
|
||
rom_cs(0);
|
||
rom_clk(0);
|
||
rom_write_byte(0x03); /* 读指令 */
|
||
rom_write_byte(ah);
|
||
rom_write_byte(am);
|
||
rom_write_byte(al);
|
||
for (uint8_t i = 0; i < len; i++) buf[i] = rom_read_byte();
|
||
rom_cs(1);
|
||
storage_unlock();
|
||
}
|
||
|
||
/*==== 定位:row 0~3(16px 行),col8 为 8 像素单位 ====*/
|
||
void oled_locate(uint8_t row, uint8_t col8)
|
||
{
|
||
s_row = row * 2;
|
||
s_col = col8 * 8;
|
||
}
|
||
|
||
/*==== 显示 GBK 字符串(16x16 汉字 / 8x16 ASCII,编码换算见晶联讯字库手册)====*/
|
||
void oled_print(const char *s)
|
||
{
|
||
uint8_t fontbuf[32];
|
||
uint8_t y = s_row;
|
||
uint8_t x = s_col;
|
||
const uint8_t *text = (const uint8_t *)s;
|
||
uint32_t fontaddr;
|
||
|
||
s_col += strlen(s) * 8; /* 位置自加(按 8px/字符计) */
|
||
|
||
uint8_t i = 0;
|
||
while (text[i] > 0x00) {
|
||
if ((text[i] >= 0xb0 && text[i] <= 0xf7) && text[i+1] >= 0xa1) {
|
||
/* GB2312 汉字:Address = ((MSB-0xB0)*94 + (LSB-0xA1) + 846) * 32 */
|
||
fontaddr = (text[i] - 0xb0) * 94;
|
||
fontaddr += (text[i+1] - 0xa1) + 846;
|
||
fontaddr *= 32;
|
||
rom_read_n((uint8_t)(fontaddr >> 16), (uint8_t)(fontaddr >> 8),
|
||
(uint8_t)fontaddr, fontbuf, 32);
|
||
display_graphic_16x16(y, x, fontbuf);
|
||
i += 2;
|
||
x += 16;
|
||
}
|
||
else if ((text[i] >= 0xa1 && text[i] <= 0xa3) && text[i+1] >= 0xa1) {
|
||
/* GB2312 全角符号:Address = ((MSB-0xA1)*94 + (LSB-0xA1)) * 32 */
|
||
fontaddr = (text[i] - 0xa1) * 94;
|
||
fontaddr += (text[i+1] - 0xa1);
|
||
fontaddr *= 32;
|
||
rom_read_n((uint8_t)(fontaddr >> 16), (uint8_t)(fontaddr >> 8),
|
||
(uint8_t)fontaddr, fontbuf, 32);
|
||
display_graphic_16x16(y, x, fontbuf);
|
||
i += 2;
|
||
x += 16;
|
||
}
|
||
else if (text[i] >= 0x20 && text[i] <= 0x7e) {
|
||
/* ASCII 8x16:Address = (char-0x20)*16 + 0x3CF80 */
|
||
fontaddr = (text[i] - 0x20) * 16 + 0x3cf80;
|
||
rom_read_n((uint8_t)(fontaddr >> 16), (uint8_t)(fontaddr >> 8),
|
||
(uint8_t)fontaddr, fontbuf, 16);
|
||
display_graphic_8x16(y, x, fontbuf);
|
||
i += 1;
|
||
x += 8;
|
||
}
|
||
else {
|
||
i++;
|
||
}
|
||
}
|
||
}
|
||
|
||
/*==== 当前列右移 px 像素 ====*/
|
||
void oled_xoff(uint8_t px)
|
||
{
|
||
s_col += px;
|
||
}
|
||
|
||
/*==== 文本像素宽度:汉字/全角 16px,ASCII 8px ====*/
|
||
uint8_t oled_text_w(const char *s)
|
||
{
|
||
const uint8_t *p = (const uint8_t *)s;
|
||
uint32_t w = 0;
|
||
while (*p) {
|
||
if (p[0] >= 0xa1 && p[1] >= 0xa1) { w += 16; p += 2; }
|
||
else { w += 8; p += 1; }
|
||
}
|
||
return (uint8_t)(w > 128 ? 128 : w);
|
||
}
|
||
|
||
/*==== 整行显示:计算像素宽度,不足 128px 用空格补齐(覆盖上次内容)====*/
|
||
void oled_print_line(const char *s)
|
||
{
|
||
const uint8_t *p = (const uint8_t *)s;
|
||
uint32_t w = 0;
|
||
while (*p) {
|
||
if (p[0] >= 0xa1 && p[1] >= 0xa1) { w += 16; p += 2; } /* 汉字/全角 */
|
||
else { w += 8; p += 1; } /* ASCII */
|
||
}
|
||
oled_print(s);
|
||
while (w < 128) { oled_print(" "); w += 8; }
|
||
}
|