ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

ZYNQ GPIO模拟SPI驱动OLED屏幕(SSD1306)完整教程

ZYNQ GPIO模拟SPI驱动OLED屏幕(SSD1306)完整教程 做ZYNQ开发这几年被问得最多的一个问题是片上的SPI控制器不是现成的吗为什么要用GPIO去模拟SPI尤其是碰上OLED这种对时序不算太苛刻的外设很多人觉得直接用硬核SPI不就行了。但真正在项目里踩过坑的人都明白情况远没那么简单。芯片的SPI引脚可能被其他功能占用硬件布线绕不过去或者你同时挂了多个SPI设备但片选不够用更别提有些IP在Linux驱动层还一堆配置问题。这时候GPIO模拟SPI简直是救场神器。这篇文章我手把手带你把整个流程走一遍从Vivado工程搭建、PS端GPIO配置到裸机代码模拟SPI时序驱动SSD1306控制的OLED屏幕全部干货照着做就能亮屏。我用的硬件是ZYNQ-7020平台开发环境是Vivado 2020.2老版本也没问题操作流程几乎一样。软件模拟SPI的思路有一个巨大的好处——引脚随意映射几乎不受硬件约束在调试初期可以极大降低排线难度。文章最后我会把整个工程目录结构和使用说明一并列出包括调试中遇到的奇葩问题和对应的排查方法这些东西在官方文档里基本找不到。1. 项目背景与方案选型1.1 为什么不用ZYNQ的硬件SPI控制器ZYNQ的PS端Processing System确实集成了两个SPI控制器名字叫SPI0和SPI1理论上你可以直接通过MIO引脚把它们引出来用。但实际开发中你会发现几个非常现实的问题:第一是引脚冲突。MIO的管教功能是复用的同一个引脚经常要在UART、SPI、I2C、SDIO之间做选择。比如你的板子已经用掉了一路UART做调试打印又用了一路SDIO挂SD卡那剩下的MIO引脚很可能不够凑齐一组完整的SPI接口更别说OLED屏幕加上DC数据/命令选择线和RST复位线之后足足需要6根信号线。第二是硬件布线。就算引脚够用板子的物理走线也可能让你很头疼。MIO引脚的位置是固定的如果你的OLED屏幕在板子的另一侧飞线会绕得很远这对SPI的时钟信号来说是个隐患特别是频率稍高一点的时候信号反射和串扰能让你查到怀疑人生。第三是驱动轮子的成本。如果你后面打算跑Linux硬件SPI在设备树里要配置中断要处理DMA要调试一套组合拳下来可能大半天就过去了。而GPIO模拟SPI在裸机下就是个延时函数的事几十行代码搞定在Linux下用gpio sysfs接口或者新版的gpiod库也能快速实现几乎没有学习成本。所以我一直强调一个观点如果外设速率要求不高OLED的SPI时钟通常几MHz就够GPIO模拟SPI不是妥协而恰恰是工程上的最优解。它把通信时序的控制权完全握在自己手里出了问题可以用示波器一根线一根线地看逻辑清晰得多。1.2 方案选型GPIO模拟SPI的适用范围和边界当然我也得说清楚GPIO模拟SPI不是万能的它有自己的适用范围和性能边界。适合用GPIO模拟的场景包括器件通信速率不高10MHz以下引脚数目紧张需要灵活映射或者处于快速原型验证阶段需要最小化工程量。OLED屏幕、温湿度传感器、ADC采样芯片、Flash存储器低速模式下都属于这一类。我甚至用GPIO模拟SPI驱动过TFT液晶屏虽然刷新率不算高但做简单的状态显示完全够用。不适合的场景就一句话高速、大批量数据传输。用GPIO模拟SPI每发送一个bit都要CPU亲自翻转电平以常见的CPU主频来算模拟SPI的实际吞吐量撑死也就几Mbps而且期间CPU被完全占住什么别的事都干不了。如果你要驱动的是SD卡、高速ADC或者需要连续刷屏的视频输出老老实实用硬件SPI、QSPI甚至并行接口别在这上面跟物理规律较劲。另外还有一个容易忽略的问题GPIO模拟SPI没有硬件FIFO做缓冲也没有硬件片选管理所有时序完全依赖软件延时因此它不适合对时序抖动极其敏感的外设。但OLED的SSD1306控制芯片是个例外它对时序的要求非常宽松只要满足基本的建立时间和保持时间慢一点完全没问题甚至可以说越慢越稳。所以OLED用GPIO模拟SPI驱动几乎是最完美的搭配。2. 硬件准备与Vivado工程搭建2.1 硬件清单与接线方案你需要准备的东西不复杂一块ZYNQ开发板7020、7010都可以一块IIC/SPI接口的OLED屏幕我用的是一块0.96寸、128x64分辨率的白色屏非常常见某宝上十几块钱以及若干杜邦线。这里有个关键点选屏幕的时候看清楚是SPI接口还是I2C接口。市面上很多小的OLED模块是I2C接口只有4个引脚做SPI实验的话你需要买那种7脚的版本。7脚OLED的引脚定义一般是GND、VCC、D0SCLK、D1MOSI、RES、DC、CS。有的屏幕还会把BS0、BS1两个配置引脚引出来通过它们的上下拉组合选择接口模式SPI模式一般要求BS0接地、BS1接高具体以你买的模块的丝印和说明书为准。再看ZYNQ端。我们这次不做复杂的PL逻辑走的是纯PS端GPIO操作但我建议把OLED的信号通过EMIO从PS端引到PL端再从PL端的引脚绑定到板子的物理引脚上。原因有两个第一PS端的MIO引脚很多被板卡功能占用不一定方便引出第二EMIO方式下GPIO控制代码和MIO几乎一样但引脚选择灵活得多可以随意绑定到PL端任何一个引脚后续就算换板子也只需要改约束文件。我这次的引脚分配表如下OLED信号ZYNQ引脚Bank 500Vivado中引脚名说明D0 (SCLK)R18GPIO_0[0]SPI时钟D1 (MOSI)N16GPIO_0[1]SPI数据DCP15GPIO_0[2]命令/数据选择高电平为数据RESP16GPIO_0[3]复位低电平有效CSR14GPIO_0[4]片选低电平有效不同板卡的引脚编号差异很大这张表的物理位置对你的板子不一定适用但原理完全一样。你打开板卡的原理图手册找到一组空闲的PL端引脚把USB转UART的收发接上用于SDK里的串口打印剩下就是把OLED的VCC接3.3VGND接GND千万别接5V。2.2 Vivado工程创建与PS侧配置打开Vivado创建一个新的RTL工程。在创建过程中选择你的开发板对应的型号如果是7020一般选xc7z020clg400-1。工程创建好之后第一步是点击Create Block Design然后添加一个ZYNQ7 Processing System IP核。双击这个IP核进行配置。其中我建议重点配置三块在PS-PL Configuration页面里把UART1打开用来打印调试信息配置为MIO 48/49不同板子串口对应的MIO不一样去原理图查一下。GPIO那栏的EMIO GPIO宽度填6因为我们正好要用6个EMIO引脚分别是GPIO_0[0]到GPIO_0[4]加上UART用掉一个。实际上填2的倍数比较规范填8也行多余的不用就行。在DDR Configuration页面选择你板子上实际使用的DDR颗粒型号。选错的话轻则内存不稳定重则启动不了这块务必对照板卡手册确认。在其他配置页面里保持默认即可。M_AXI_GP0这些接口我们用不到可以不打开。配置完成后点击工具条上的Run Block Automation如果你勾选了自动连接或者手动连线把PS端的FCLK_CLK0连接到整个设计然后右键生成输出产物包括约束文件。这里要特别提醒一个初学者常犯的错误很多人会忘记连接FCLK_RESET0_N引脚。连接时建议用Processor System Reset IP核连接到外部否则后面综合时会出现一堆诡异的时序报错。我的习惯是无论电路是否用到PL逻辑都要加上这个复位逻辑有备无患。2.3 EMIO引脚约束与比特流生成Block Design内部的连接做完后我们需要把GPIO信号引到芯片的物理引脚上。这一步在Vivado里叫做接口约束XDC文件。做法是右键Block Design里的GPIO_0端口选择Make External这样就会在设计顶层暴露出一组名为GPIO_0_tri_io的引脚。接下来创建一个XDC约束文件把GPIO_0_tri_io[0]到[4]这五个信号分别绑定到你在原理图上选好的引脚并设置IOSTANDARD为LVCMOS33。举个例子set_property PACKAGE_PIN R18 [get_ports {GPIO_0_tri_io[0]}] set_property IOSTANDARD LVCMOS33 [get_ports {GPIO_0_tri_io[0]}] set_property PACKAGE_PIN N16 [get_ports {GPIO_0_tri_io[1]}] set_property IOSTANDARD LVCMOS33 [get_ports {GPIO_0_tri_io[1]}] # 其余引脚类似注意检查Bank的电压。大多数开发板的PL端IO都接的是3.3V所以IOSTANDARD选LVCMOS33没问题。如果你的板子某个Bank是2.5V或者1.8V供电电平标准要相应地改否则IO口会工作不正常OLED无法识别信号。约束写好后点击Generate Bitstream。生成成功后菜单栏File - Export Hardware勾选Include bitstream导出一个.xsa文件。这个文件包含了硬件信息接下来SDK/Vitis要用它来创建软件工程。如果你在生成比特流时遇到routed design中有未连接的引脚之类的错误多半是XDC里的引脚绑错了或者漏绑了回头检查一下约束文件有没有覆盖所有Make External出来的引脚。我见过不少人卡在这一步以为自己写代码有问题其实只是XDC没写完整。3. OLED屏幕与SPI协议核心原理3.1 SSD1306驱动芯片与SPI工作模式市面上绝大多数0.96寸OLED屏幕内部用的都是Solomon Systech公司的SSD1306驱动芯片。这是一颗单芯片的OLED驱动控制器内部带128x64 bit的GRAM显存你往显存里写1对应的像素就点亮写0就熄灭。因此驱动这颗屏幕的核心工作可以概括为两件事正确初始化SSD1306以及通过SPI接口往它的GRAM里搬运数据。SSD1306支持6800/8080并行、SPI和I2C四种接口实际模块上引出的引脚就决定了它工作在哪种模式。我们这里用SPI模式。SPI模式下SSD1306是一个从设备需要MCU主动发起通信。值得注意的是SSD1306 SPI模式只支持Mode 0和Mode 3两者的区别只是时钟极性和相位对于软件模拟来说我们只要保证时序逻辑一致即可硬件兼容性其实很好。这颗芯片的命令集不算复杂最常用的无非是设置显示开关0xAE/0xAF、设置显示时钟分频0xD5、设置多路复用比0xA8、设置显示偏移0xD3、设置起始行0x40、设置内存寻址模式0x20、设置列地址0x21、设置页地址0x22、设置对比度0x81、设置充电泵0x8D等十几条。由于SPI是串行协议每一次通信只能传输8位。SSD1306在SPI模式下通过DC引脚区分当前字节是命令还是数据DC为低时写入的是命令DC为高时写入的是显存数据。这是整个驱动中最容易出错的地方——命令和数据顺序搞反轻则屏幕不亮重则显示异常花屏。3.2 SPI协议与GPIO模拟的核心思想如果对SPI还不熟悉我用最简单的话帮你把本质抓住SPI是一种主从式、同步、全双工的串行通信协议总共有四根线——SCLK时钟、MOSI主出从入、MISO主入从出、CS片选。通信时主机产生时钟信号数据在时钟边沿进行采样。OLED这种纯显示设备不需要向主机回传数据所以MISO这根线可以不接只需要SCLK、MOSI、CS三根加上控制用的DC和RES共五根线。GPIO模拟SPI的原理说白了就是软件按照SPI协议规定的时间顺序去翻转GPIO电平。以SPI Mode 0为例CPOL0CPHA0时钟空闲为低电平数据在上升沿采样在下降沿变化发送一个字节的数据伪代码逻辑是拉低CS表示开始通信循环8次每次从最高位开始取一位数据先把SCLK拉低然后把这一位数据放到MOSI上延时一小段时间保证建立时间再把SCLK拉高此时从设备会采样MOSI线上的数据再延时一小段时间循环结束后拉高CS通信结束。这段逻辑是不是很简单但正是这简单的时序欺骗了很多人以为模拟SPI的程序随便写写就行。实际上真正的坑都藏在延时和电平转换的边界处。比如数据信号要提前于时钟上升沿建立好否则从设备采到的是上一次的电平数据就会错位。又比如CS的拉低要早于第一个时钟沿否则从设备不认为通信开始了。3.3 ZYNQ GPIO的寄存器操作方式在ZYNQ上操作GPIO和STM32的HAL库不一样更接近寄存器级操作。PS端GPIO由XGpioPs驱动提供核心用法就几个函数XGpioPs_LookupConfig查找设备配置、XGpioPs_CfgInitialize初始化驱动、XGpioPs_SetDirectionDirection设置引脚方向、XGpioPs_WritePin写引脚电平、XGpioPs_ReadPin读引脚电平。对于EMIO引脚编号是固定的。EMIO GPIO从引脚号54开始MIO占0~53我用的五个引脚分别对应GPIO_0[0]到GPIO_0[4]在驱动里的实际引脚号就是54、55、56、57、58。如果你开的是MIO引脚则直接用对应的MIO编号即可。有个细节值得注意XGpioPs_SetDirection第二个参数是按Bank设置的掩码不是按单个引脚。你说把第54脚设为输出会写成XGpioPs_SetDirection(Gpio, GPIO_BANK(54), 1)其中GPIO_BANK(54)是54号引脚所属的Bank掩码1表示该Bank的第0位设为输出。如果一次要设置多个引脚为输出就得把掩码按位或起来例如0x1F就是把低5位都设为输出。这块不搞清楚容易出现只有一个引脚能输出其余怎么都不动的诡异现象。4. 裸机驱动代码实现4.1 整体代码框架与模块划分在Vitis2020.2以后叫Vitis之前叫SDK里新建一个Application Project平台选择你导出的.xsa文件模板随便选Empty Application就行因为我们要完全自己写代码。我的习惯是把代码划分成三个层次最底层是spi_gpio.c负责GPIO初始化和最原始的翻转电平操作中间层是ssd1306.c实现SPI发送字节、发送命令、发送数据、初始化序列、清屏、画点、显示字符串等功能最上层就是main.c调用驱动接口展示显示效果。这样分层的结构以后想移植到STM32或者ESP32上只需要改最底层的几个函数OLED相关的代码几乎可以原封不动搬过去。在main.c里要做的事情包括初始化UART用于串口打印、初始化GPIO、初始化OLED、然后进入一个循环每隔一定时间刷新或者变换屏幕上显示的内容。4.2 底层GPIO初始化与SPI时序模拟实现底层的GPIO初始化代码如下这段代码放到任何一个ZYNQ裸机工程中都能直接运行#include xgpiops.h XGpioPs Gpio; // OLED引脚编号EMIO从54开始 #define OLED_SCLK_PIN 54 // GPIO_0[0] #define OLED_MOSI_PIN 55 // GPIO_0[1] #define OLED_DC_PIN 56 // GPIO_0[2] #define OLED_RES_PIN 57 // GPIO_0[3] #define OLED_CS_PIN 58 // GPIO_0[4] void GPIO_Init(void) { XGpioPs_Config *ConfigPtr; u32 Bank; ConfigPtr XGpioPs_LookupConfig(XPAR_XGPIOPS_0_DEVICE_ID); XGpioPs_CfgInitialize(Gpio, ConfigPtr, ConfigPtr-BaseAddr); // 设置所有用到的EMIO引脚为输出掩码按位或 Bank GPIO_BANK(OLED_SCLK_PIN); XGpioPs_SetDirection(Gpio, Bank, 0x1F); // 低5位输出 XGpioPs_SetOutputEnable(Gpio, Bank, 0x1F); // 初始电平时钟低片选高复位高DC低 XGpioPs_WritePin(Gpio, OLED_SCLK_PIN, 0); XGpioPs_WritePin(Gpio, OLED_MOSI_PIN, 0); XGpioPs_WritePin(Gpio, OLED_CS_PIN, 1); XGpioPs_WritePin(Gpio, OLED_DC_PIN, 0); XGpioPs_WritePin(Gpio, OLED_RES_PIN, 1); }这里有一个很重要但容易被忽略的细节XGpioPs_WritePin的置位操作内部实际上是先读再写不是直接写寄存器。在模拟SPI这种频繁翻转电平的场景下每一次写操作都会多出一次读寄存器的开销虽然影响不大但如果你对性能有极致要求可以直接操作寄存器用Xil_In32和Xil_Out32去改写DATA寄存器速度能快不少。接下来是SPI模拟的核心时序函数void SPI_WriteByte(u8 data) { int i; for (i 0; i 8; i) { XGpioPs_WritePin(Gpio, OLED_SCLK_PIN, 0); if (data 0x80) XGpioPs_WritePin(Gpio, OLED_MOSI_PIN, 1); else XGpioPs_WritePin(Gpio, OLED_MOSI_PIN, 0); // 数据建立时间 Delay_us(1); XGpioPs_WritePin(Gpio, OLED_SCLK_PIN, 1); // 数据保持时间 Delay_us(1); data 1; } XGpioPs_WritePin(Gpio, OLED_SCLK_PIN, 0); }这里数据从高位开始发送。为什么从高位这是SSD1306的约定它的SPI传输顺序是MSB first如果你从低位发送显示出来就是镜像的。关于延时我的经验是1us够了SSD1306的SPI最小时钟周期是100ns级别1us属于留了充足余量。如果OLED用的是超长飞线或者屏幕老化严重可以加大到5us对稳定性有利但屏幕刷新会肉眼可见地变慢。命令和数据的发送函数其实只有一行代码的区别void OLED_WriteCmd(u8 cmd) { XGpioPs_WritePin(Gpio, OLED_CS_PIN, 0); XGpioPs_WritePin(Gpio, OLED_DC_PIN, 0); // DC低电平表示命令 SPI_WriteByte(cmd); XGpioPs_WritePin(Gpio, OLED_CS_PIN, 1); } void OLED_WriteData(u8 dat) { XGpioPs_WritePin(Gpio, OLED_CS_PIN, 0); XGpioPs_WritePin(Gpio, OLED_DC_PIN, 1); // DC高电平表示数据 SPI_WriteByte(dat); XGpioPs_WritePin(Gpio, OLED_CS_PIN, 1); }这里再唠叨一句片选的问题。OLED模块只有一个CS引脚所以在每次传输之前拉低、传输结束拉高是标准的操作习惯。但如果你手头有多个SPI设备共享MOSI和SCLK那么片选管理就要格外小心切换设备时一定要给一小段时间的缓冲避免总线冲突。软件拉片选和硬件片选的主要区别就在这里硬件片选在硬件控制器层面管理时间精度高软件片选依赖代码调用顺序稍不留神就会出现毛刺。但在OLED这种点对点场景中软件片选完全够用。4.3 SSD1306初始化序列与显示缓冲管理SSD1306上电后不能直接开始显示必须按照官方规定的序列初始化。初始化序列看似是一堆魔术数字但每一条都有明确意义。我把常用的初始化序列贴出来并做简单注释void OLED_Init(void) { Delay_ms(100); // 等待屏幕内部复位完成 // 硬件复位 XGpioPs_WritePin(Gpio, OLED_RES_PIN, 0); Delay_ms(100); XGpioPs_WritePin(Gpio, OLED_RES_PIN, 1); Delay_ms(100); OLED_WriteCmd(0xAE); // 关闭显示 OLED_WriteCmd(0xD5); OLED_WriteCmd(0x80); // 设置显示时钟分频/振荡器频率 OLED_WriteCmd(0xA8); OLED_WriteCmd(0x3F); // 设置多路复用比128x64屏这里是63 OLED_WriteCmd(0xD3); OLED_WriteCmd(0x00); // 设置显示偏移为0 OLED_WriteCmd(0x40); // 设置显示起始行为0 OLED_WriteCmd(0x8D); OLED_WriteCmd(0x14); // 开启充电泵内部升压 OLED_WriteCmd(0x20); OLED_WriteCmd(0x00); // 水平寻址模式 OLED_WriteCmd(0xA1); // 段重映射左右方向修正 OLED_WriteCmd(0xC8); // 扫描方向从上到下 OLED_WriteCmd(0xDA); OLED_WriteCmd(0x12); // COM引脚硬件配置 OLED_WriteCmd(0x81); OLED_WriteCmd(0xCF); // 对比度设置 OLED_WriteCmd(0xD9); OLED_WriteCmd(0xF1); // 预充电周期 OLED_WriteCmd(0xDB); OLED_WriteCmd(0x40); // VCOMH电压选择 OLED_WriteCmd(0xA4); // 恢复RAM内容显示 OLED_WriteCmd(0xA6); // 正常显示非反色 OLED_WriteCmd(0xAF); // 打开显示 }这段代码里的0x8D 0x14开启充电泵是很多人栽跟头的地方。SSD1306如果要正常工作需要较高的驱动电压这颗芯片内部集成了升压电路需要软件主动开启。如果你忘了配置或者设置成0x10关闭充电泵屏幕大概率就是一片黑。显示缓冲管理方面由于SSD1306的GRAM是按页组织的128x64像素被分成8页page0~page7每页代表8行像素每个字节代表8个垂直排列的像素。这就带来一个惯性思维陷阱你在内存里连续写入的每个字节在屏幕上对应的是竖直方向的8个点而不是水平方向。所以很多从LCD公版驱动移植过来的代码直接往显存里填数据显示出来的文字就是竖着的。正确做法是建立一块128x8的显存缓冲在缓冲里操作好后再整块刷新到OLED或者在采样字模时使用列行式取模方式。4.4 完整驱动代码与显示效果测试现在我们写一个具体的显示函数在屏幕上显示一行文字。字符显示的原理是查表每个字符对应一组字节数组每一个字节代表字符的一部分像素。我用的字模是6x8大小的ASCII字符集取模方式是先列后行、从低字节开始。const unsigned char F6x8[][6] { {0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // sp {0x00, 0x00, 0x00, 0x2f, 0x00, 0x00}, // ! {0x00, 0x00, 0x07, 0x00, 0x07, 0x00}, // // 其他字符略 }; void OLED_ShowChar(u8 x, u8 y, char c) { u8 i; u8 y0 y; u8 page y / 8; // 计算字符所在页 u8 offset y % 8; u8 row; // 设置页地址和列地址 OLED_WriteCmd(0xB0 page); OLED_WriteCmd(0x00 (x 0x0F)); OLED_WriteCmd(0x10 ((x 4) 0x0F)); // 写6字节显存数据 for (i 0; i 6; i) { // 这里需要根据offset做移位处理简化起见假设offset为0 OLED_WriteData(F6x8[c - ][i]); } }如果你发现文字显示位置不对或者花屏多半是页地址和列地址的计算出错。SSD1306的列地址可以直接指定范围是0~127但要注意它的列地址设置分两次低4位和高4位分别写入逻辑或计算时别写错。另外写数据到GRAM时地址会自动递增只有当你需要跨页或者跳列显示时才需要重新设置地址。main函数写完整个流程后烧录到板子上。正常情况下屏幕会显示你指定的字符和数字。如果遇到屏幕完全没有反应先别急着检查代码先用万用表量一下OLED的VCC和GND之间有没有3.3V再量一下RES引脚在初始化时有没有产生负脉冲。显示问题八成出在硬件连接或初始化时序上代码的问题反而好排查。5. 常见问题与排查技巧实录5.1 屏幕完全不亮或只有背光这个问题排名第一。我总结的原因无非几种一是电压问题。OLED模块的VCC接错或者电源纹波过大导致驱动芯片保护。换一根质量好一点的杜邦线或者外接独立3.3V供电能解决掉一半的屏幕不亮问题。尤其是那种用了很长的母对母杜邦线的场景接触电阻和线间电容可能让供电瞬间掉压。二是复位时序。SSD1306的RES引脚要求低电平复位而且复位脉冲的最低宽度有要求。如果你的代码里复位脉冲一下子就过去了芯片可能没有完成内部初始化。我习惯的做法是复位低电平保持至少100ms然后拉高再等100ms确保芯片完全苏醒。三是充电泵没开。前面提到过OLED驱动需要内部升压0x8D和0x14这两个命令必须成对出现。如果你在初始化序列里把它们注释掉了屏幕自然不亮。四是DC引脚状态不对。如果DC引脚在初始化时序中保持高电平芯片会把所有命令字节当作显存数据来处理界面自然一片混乱。用示波器量一下DC引脚的电平变化很快就能定位问题。5.2 屏幕能亮但显示花屏或镜像花屏的情况比较典型初始化正常、背光亮了但显示内容乱码或者文字方向不对或者显示区域偏移。花屏的大多数原因是显存地址错乱。SSD1306有几种寻址模式水平寻址、垂直寻址和页寻址各有各的地址递增规则。我默认用水平寻址模式0x20, 0x00这种模式下写完一行数据列地址自动加1到127之后自动跳转到下一行起始扫描顺序符合大多数人的直觉。如果你之前用的是页寻址模式那么每写一页数据必须重新设置页地址否则数据会全部堆在同一页里。镜像问题则和段重映射以及COM扫描方向相关。A1命令段重映射控制水平方向的映射C8命令扫描方向控制垂直方向的递增顺序。如果你的显示内容左右颠倒把A1改成A0上下颠倒把C8改成C0。这块没有绝对的正确答案取决于你的OLED模组的内部电路连接方式同样一块屏幕不同厂家出货的模组引脚和内部走线可能有差异。显示区域偏移有时是因为多路复用比设置不对。128x64的屏幕写0x3F是对的如果你错误地设成0x1F屏幕只工作一半下半部分要么是黑的要么是残影。这个问题在1.3寸的OLED上更常见因为1.3寸屏用的往往是SH1106而不是SSD1306初始化的参数会有一点差异。所以买屏幕之前先确认驱动芯片不要看到OLED就套代码。5.3 时序问题与性能优化最后一个常见问题屏幕能工作但是刷新很慢或者偶发闪烁。对于软件模拟SPI来说瓶颈几乎都在延时函数上。我上文中的代码每次翻转时钟都延时1us发送一个字节大概16us刷新一帧128x64的显存需要发送1024字节算下来大概16毫秒。也就是说理论最高帧率60帧左右实际因为还有地址设置和逻辑处理的开销做到30帧就已经很流畅了。如果你觉得刷新速度不够可以做的优化有几个方向一是去掉延时或者把延时缩小到100ns级别配合GPIO直接寄存器操作速度可以提升好几倍二是只刷新变化区域而不是每次把整块显存都推给屏幕三是把显存缓冲改成128字节一行的结构避免逐字节设置地址的开销。但这里要奉劝一句OLED不适合高刷新率应用。如果连续大量刷新耗电量会很可观而且OLED长时间高亮度显示会有烧屏风险。做产品的时候能静态显示就静态显示能局部刷新就局部刷新这不仅是对硬件的保护也是设计成熟度的体现。另外我实测中发现一个很有意思的现象ZYNQ的EMIO引脚翻转速度并不是完全一致的不同的引脚因为内部走线不同翻转时间会有几十纳秒的差异。这个差异在SPI时序上几乎没有影响但在某些对时序要求特别严格的总线上可能会埋雷。如果之后你想把同一套代码用来驱动更敏感的SPI设备建议在关键翻转点之间预留足够大的时间余量。5.4 移植到其他平台的小技巧这套软件模拟SPI驱动OLED的思路不止适用于ZYNQ。我后来把它原封不动地移植到了STM32的HAL库工程上又移植到了ESP32-IDF环境里移植过程总共不到半小时。核心思路是把引脚操作抠出来一个抽象层上层全部用宏或者函数指针去调用。在STM32上最底层操作就是HAL_GPIO_WritePin把XGpioPs_WritePin替换掉延时函数从自定义Delay替换成HAL_Delay或者DWT等精密延时其余代码几乎不用动。在ESP32上用gpio_set_level就能搞定。甚至你在Linux用户态用sysfs的gpio接口也能用着一模一样的SSD1306驱动代码只是底层的电平翻转换来换去。这里给大家一个建议把这一套代码保存成你自己的工具库放到GitHub或者私有仓库里以后凡是遇到SPI接口的屏幕、传感器都能很快地跑起来。我在实际项目中已经因此节省了大把的时间尤其是原型验证阶段很多需求就是先看看这块屏能不能用这时候有现成的底层驱动半天就能给出结论非常有效率。最后再分享一个小技巧。调试SPI时序出问题时不要急着看逻辑分析仪更不用上昂贵的示波器用最简单的办法在SPI_WriteByte函数里发送完一个字节后把一个空闲的GPIO翻转一次然后用逻辑分析仪或者示波器去测这个GPIO和SCLK、MOSI的波形关系。你很快就能看出SCLK是否有了预期的脉冲、MOSI的电平对不对、数据的建立时间和保持时间是否合理。这种软件打点的习惯能让你在嵌入式调试中少走很多弯路。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表