一、SPI 总线基础
(一)定义
SPI(Serial Peripheral Interface)由 Motorola 公司开发,是一种通用数据总线。
(二)通信线组成
- SCK(Serial Clock):串行时钟线,用于同步数据传输。
- MOSI(Master Output Slave Input):主设备输出从设备输入线,主设备经此向从设备发送数据。
- MISO(Master Input Slave Output):主设备输入从设备输出线,从设备经此向主设备发送数据。
- SS(Slave Select):从设备选择线,用于选定特定从设备进行通信。
(三)通信特性
- 同步:数据传输在时钟信号(SCK)同步下进行。
- 全双工:主设备和从设备可同时双向传输数据。
(四)设备连接特点
支持一主多从模式,即一个主设备可连接多个从设备,通过 SS 线选择与之通信的从设备。
二、SPI 设备连接与引脚配置
(一)设备连接方式
- 公共线连接:所有 SPI 设备的 SCK、MOSI、MISO 分别相连。其中 SCK 负责同步,MOSI 用于主机向从机发送数据,MISO 用于从机向主机发送数据。
- 从机选择连接:主机引出多条 SS 控制线,分别连接各从机的 SS 引脚,通过控制 SS 线电平高低(一般低电平选中)来选择特定从机通信。
- 注意事项
在有多个从机的情况下,会出现主机一个 MISO、从机多个 MISO 的情况。为保证从机接收到的数据的一致性,当 SS 为高位(该从机未被使用)时,从机的 MISO 需设置为高阻态(相当于断掉),以此确保正在调用的从机数据能够准确传入主机之中。这样可避免未被选中的从机干扰正在通信的从机数据传输,保障 SPI 通信系统在多从机环境下的稳定性和准确性。
(二)引脚配置
- 输出引脚:配置为推挽输出,可提供较强驱动能力,保证数据传输速度与稳定性。
- 输入引脚:配置为浮空或上拉输入,可增强抗干扰能力和信号检测准确性。
三、W25Q64 FLASH 存储芯片要点
(一)引脚对应关系
- CLK 引脚对应 SPI 总线的 SCK(串行时钟引脚)。
- DI 与 DO 引脚对应 SPI 总线的 MOSI 和 MISO,但对应关系取决于芯片是主设备还是从设备。
(二)主从设备下引脚功能
当 W25Q64 芯片作为从设备时:
- DI(数据输入)引脚:从设备的数据输入需连接主机的设备输出,即 DI 应连接到主机的 MOSI。
- DO(数据输出)引脚:从设备的数据输出应连接主机的 MISO,即 DO 连接到 MISO。
四、应用要点
- 应用 SPI 总线时,需牢记其同步、全双工、一主多从特性。
- 连接 W25Q64 芯片与主机时,要准确理解引脚功能、连接方式及配置方法,以保障通信正常。
五、移位示意图
- SPI时序
0模式是上升沿读取,下降沿切换字节,并且ss置0时同时开始传输数据
1模式时下降沿读取,上升沿切换字节,并且SCK置1时才开始传输数据
模式3与1对应,SCK波形取反,CPOL=1
模式2与0对应,SCK波形取反,CPOL=1
注意:模式0与模式3都是上升沿采入——模式1与模式2都是下降沿采入
这两张图展示了 SPI(Serial Peripheral Interface,串行外设接口)的两种工作模式。
共同点
- 都在描述交换一个字节的过程。
- 都设定了CPOL = 0,即空闲状态时,SCK(时钟信号)为低电平。
不同点
- 模式 0:CPHA = 0,SCK第一个边沿移入数据,第二个边沿移出数据。从波形图上看,在SCK的上升沿进行数据移入,下降沿进行数据移出。
- 模式 1:CPHA = 1,SCK第一个边沿移出数据,第二个边沿移入数据 。从波形图上看,在SCK的上升沿进行数据移出,下降沿进行数据移入。
简单来说,两张图都是 SPI 在不同CPHA设置下交换一个字节数据的波形图,区别在于SCK不同边沿上数据的移入和移出操作不同。
初始化阶段:
将 SS(即 a)置 0 开始运行。
SS 为片选信号,用于选择要通信的从设备,低电平有效。
此操作目的是选中对应的从设备,使其进入准备接收和发送数据的状态,开启通信流程。
数据传输同步阶段:(模式一)
发送同步时钟(SCK,即 b)至高位。
SCK 为串行时钟信号,在时钟信号的上升沿和下降沿进行数据的发送和接收操作。
其目的是在上升沿时,为主从设备数据的发送和接收提供同步时机,让双方能够在同一时刻进行数据交互。
数据发送与读取阶段:
在同步时钟 SCK 上升沿时,MOSI(即 c)和 MISO(即 d)开始读取当前自己。
MOSI 是主输出从输入线,主设备通过它向从设备发送数据;
MISO 是主输入从输出线,从设备通过它向主设备发送数据。
此时,主设备将数据通过 MOSI 线发送,从设备将数据通过 MISO 线发送,双方同时读取各自线上的数据,实现数据的双向传输。
主机读取阶段:
当同步时钟 SCK 置 0,即下降沿到来时,主机读取数据。
因为在 SCK 下降沿时,数据在 MISO 线上处于稳定状态,主机可准确读取从设备通过 MISO 线发送过来的数据。
整段数据交换完成阶段:
重复上述 SCK 上升沿发送数据、下降沿读取数据的操作,即可完成一整段数据交换。在此过程中,SS 保持低电平选中从设备,SCK 不断产生上升沿和下降沿,MOSI 和 MISO 逐位传输数据。通过多次重复这些操作,逐位完成一整段数据的双向交换。
继续通信阶段:
若主设备和从设备之间还有数据需要传输,即继续接收数据时,重复上述所有操作。也就是继续利用 SS、SCK、MOSI、MISO 协同工作,重复时钟信号的上升沿发送数据、下降沿读取数据的过程,实现后续数据的交换。
结束通信阶段:
若主设备完成数据交换,不需要继续通信,将 SS(即 a)置 1,从机停止运行,同时将 MISO(即 d)设置至高阻态。
SS 拉高表示取消对从设备的选择,使从设备停止通信操作进入空闲状态;
MISO 高阻态意味着该引脚在电气上与电路断开,不输出信号,避免对其他设备产生干扰。
- SPI时序
这里的0x06是写使能,0x03是读指令
W25Q64简介
W25Q64
1. 存储区域划分(Block Segmentation)
图的左上角展示了存储区域的划分。可以看到不同的扇区(Sector),每个扇区大小有 4KB 或 1KB ,并且有对应的十六进制地址范围。例如,Sector 15 的地址范围是 0xF000h - 0xFFFFh ,大小为 4KB。扇区是存储设备中用于数据擦除和编程的基本单元。
2. 存储块(Block)
图的右侧展示了不同的存储块(Block),每个块的大小为 64KB ,并且有对应的十六进制地址范围。例如,Block 127 的地址范围是 0x7F000h - 0x7FFFFh 。存储块是由多个扇区组成的,在闪存中,擦除操作通常是以块为单位进行的。
3. 控制逻辑部分
- Write Control Logic:写控制逻辑,负责管理数据写入操作,/WP(Write Protect)引脚用于控制写保护功能。
- Status Register:状态寄存器,用于存储芯片的状态信息,如操作是否完成、是否有错误等。
- SPI Command & Control Logic:SPI(Serial Peripheral Interface)命令和控制逻辑,用于处理通过 SPI 接口发送的命令和数据。相关引脚包括 /HOLD、CLK、/CS、DI(Data In)和 DO(Data Out)。
- High Voltage Generators:高压发生器,用于在编程和擦除操作时提供所需的高电压。
- Page Address Latch / Counter 和 Byte Address Latch / Counter:分别用于锁存页地址和字节地址,在闪存操作中,数据是以页为单位进行读写的。
4. 解码和缓冲部分
- Write Protect Logic and Row Decode:写保护逻辑和行解码,用于选择特定的存储行。
- Column Decode And 256 - Byte Page Buffer:列解码和 256 字节页缓冲,用于在读写操作时缓存数据。页缓冲器可以提高数据传输的效率,在页编程和读取操作中起到重要作用。
总体来说,这张图详细描述了存储芯片内部的地址组织结构以及实现数据读写、擦除等操作的控制逻辑和电路模块,有助于理解闪存芯片的工作原理和操作流程。
疑问:为什么FLASH操作相对于RAM操作如此繁琐?
- Flash 存储器特性:
- Flash 是一种掉电不丢失的存储器。
- 为保证掉电不丢失特性,同时实现足够大的存储容量和较低成本,在其他方面做出妥协,如操作便携性。
- Flash 与 RAM 在写入操作上的差异:
- RAM 写入特点:写入操作简单直接,想写哪里就写哪里,想写多少都可以,并且支持覆盖写入。
- Flash 写入特点:写入操作不如 RAM 简单直接,不具备像 RAM 那样想写哪里就写哪里、想写多少就写多少以及覆盖写入的特性 ,需要遵守上面tu'pia
一.SPI通信层配置
- 设置void MySPI_Init(void) --引脚初始化
端口设置PA4.5.6.7,其中a6设置GPIO_Mode_IPU,其余设置GPIO_Mode_Out_PP(why)
- 从机选择(给这几个GPIO换个名字)
void MySPI_W_SS(uint8_t BitValue)
void MySPI_W_SCK(uint8_t BitValue)
void MySPI_W_MOSI(uint8_t BitValue)
uint8_t MySPI_R_MISO(void)
- 在void MySPI_Init(void) 函数中添加引脚初始化
MySPI_W_SS(1);MySPI_W_SCK(0);
- 根据SPI基本时序单元图封装函数
void MySPI_Start(void)
void MySPI_Stop(void)
uint8_t MySPI_SwapByte(uint8_t ByteSend)
注意:MySPI_W_MOSI(ByteSend & (0x80 >> i));//取输入数据最高位(&只要有一个为0,都为0)
MySPI_W_SCK(1);//SCK置高位
if (MySPI_R_MISO() == 1){ByteReceive |= (0x80 >> i);}
//主机把从机的高位接受进来(|只要有一个为1,都为1),把第I位设置成1
//关于MySPI_R_MISO() == 1?刚刚设置ByteReceive = 0x00了嘛,所以如若接收到的这位是0,直接不用管了呀,它本身这个位置上的数就是0
MySPI_W_SCK(0);//SCK置低位
//交换数据不就是既发送也接受,如果想读取就随便发送然后接收就行,想写入,就发送写入值,不操作接收的返回值即可(ByteReceive = 0x00;已经在函数中设置过了,如果想要读取就改成ByteSend = 0x00后进行逻辑上的调整即可,只要满足主从交换就没问题)
- 补充:模式123如何改
模式1
MySPI_W_SCK(1);
MySPI_W_MOSI(ByteSend & (0x80 >> i));
MySPI_W_SCK(0);
if (MySPI_R_MISO() == 1){ByteReceive |= (0x80 >> i);}
模式3(注意,源代码中MySPI_W_SCK(0改成MySPI_W_SCK(1))
MySPI_W_SCK(0);
MySPI_W_MOSI(ByteSend & (0x80 >> i));
MySPI_W_SCK(1);
if (MySPI_R_MISO() == 1){ByteReceive |= (0x80 >> i);}
二.SPI上层配置——W25Q64
1.初始化与读ID,保证SPI底层逻辑调用成功
void W25Q64_Init(void) { MySPI_Init();} void W25Q64_ReadID(uint8_t *MID, uint16_t *DID) { // 1. 启动 SPI 通信 MySPI_Start(); // 2. 发送 JEDEC ID 读取命令 MySPI_SwapByte(W25Q64_JEDEC_ID); // 3. 读取制造商 ID *MID = MySPI_SwapByte(W25Q64_DUMMY_BYTE); // 4. 读取设备 ID 的高 8 位 *DID = MySPI_SwapByte(W25Q64_DUMMY_BYTE); // 5. 将高 8 位左移 8 位 *DID <<= 8; // 6. 读取设备 ID 的低 8 位,并与高 8 位合并 *DID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE); // 7. 停止 SPI 通信 MySPI_Stop();}
解析:
代码逐行解释
MySPI_Start();
:
- 调用
MySPI_Start
函数,该函数的作用是启动 SPI 通信。通常,这意味着将 SPI 接口的片选信号(CS)拉低,以选中 W25Q64 芯片。
MySPI_SwapByte(W25Q64_JEDEC_ID);
:
- 调用
MySPI_SwapByte
函数,发送W25Q64_JEDEC_ID
命令。W25Q64_JEDEC_ID
是一个预定义的常量,表示读取 JEDEC ID 的命令码。MySPI_SwapByte
函数会在发送一个字节的同时接收一个字节的数据。
*MID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);
:
- 发送一个虚拟字节
W25Q64_DUMMY_BYTE
,并将接收到的字节存储到MID
指针所指向的内存位置。这个接收到的字节就是 W25Q64 芯片的制造商 ID。
*DID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);
:
- 再次发送一个虚拟字节
W25Q64_DUMMY_BYTE
,并将接收到的字节存储到DID
指针所指向的内存位置。这个接收到的字节是设备 ID 的高 8 位。
*DID <<= 8;
:
- 将
DID
的值左移 8 位,为存储设备 ID 的低 8 位腾出空间。
*DID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE);
:
- 发送另一个虚拟字节
W25Q64_DUMMY_BYTE
,并将接收到的字节(设备 ID 的低 8 位)与DID
的高 8 位进行按位或运算,合并成一个完整的 16 位设备 ID。
MySPI_Stop();
:
- 调用
MySPI_Stop
函数,停止 SPI 通信。通常,这意味着将 SPI 接口的片选信号(CS)拉高,取消对 W25Q64 芯片的选中。
2.写使能与通过读状态寄存器判断是否在忙
void W25Q64_WaitBusy(void)
{
// 1. 启动 SPI 通信
MySPI_Start();
// 2. 发送读取状态寄存器 1 的命令
MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1);
// 3. 循环检查状态寄存器 1 的最低位(忙碌标志位)
while ((MySPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01)
{
// 若最低位为 1,表示芯片处于忙碌状态,继续循环等待
// 最后一位为Busy位,具体参考芯片手册
}
// 4. 停止 SPI 通信
MySPI_Stop();
}
注意:为了放置死循环意外卡死,可以设置变量Timeout
void W25Q64_WaitBusy(void)
{
uint32_t Timeout;
MySPI_Start();
MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1);
Timeout = 100000;
while ((MySPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01)
{
Timeout --;
if (Timeout == 0)
{
break;
}
}
MySPI_Stop();
}
3.页编程函数
解读:先发送指令码02,再发送三个地址(A0-A23),再发送数据
注意:只能发送256个Byte,多出会覆盖第一个
void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count)
- *DataArray 为了调用效率提升,设置一个数组提升效率,数组需要指针进行传递,数据类型定义为指针函数
- uint16_t Count 表示写多少个,注意要使用uint16,写入数据的数量范围为0-256
void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count)
{
uint16_t i;
W25Q64_WriteEnable();
MySPI_Start();
MySPI_SwapByte(W25Q64_PAGE_PROGRAM);
MySPI_SwapByte(Address >> 16);
MySPI_SwapByte(Address >> 8);
MySPI_SwapByte(Address);
for (i = 0; i < Count; i ++)
{
MySPI_SwapByte(DataArray[i]);
}
MySPI_Stop();
W25Q64_WaitBusy();
}
问:为什么Address右移传输?
在W25Q64_PageProgram
函数中,MySPI_SwapByte(Address >> 16);
、MySPI_SwapByte(Address >> 8);
和 MySPI_SwapByte(Address);
这几行代码使用右移操作是为了将 32 位的地址 Address
拆分成三个 8 位字节,以便通过 SPI 接口逐字节发送到 W25Q64 芯片。下面详细解释其原理和原因。
1. W25Q64 芯片的通信协议
W25Q64 是一款基于 SPI 接口的闪存芯片,当进行页编程(Page Program)操作时,需要向芯片发送一系列命令和数据。具体来说,首先要发送页编程命令(W25Q64_PAGE_PROGRAM
),然后发送 3 字节的地址,最后发送要写入的数据。
2. 地址拆分的必要性
SPI 接口是一种串行通信接口,每次只能传输 8 位(1 字节)的数据。而 Address
是一个 32 位的无符号整数,为了将这个 32 位的地址通过 SPI 接口发送给 W25Q64 芯片,需要将其拆分成 3 个 8 位字节。
3. 右移操作的作用
右移操作(>>
)是一种位运算,用于将一个数的二进制表示向右移动指定的位数。在这个函数中,右移操作的具体作用如下:
MySPI_SwapByte(Address >> 16);
:将Address
右移 16 位,这样就把Address
的高 8 位移动到了低 8 位,然后通过MySPI_SwapByte
函数将这 8 位发送出去。MySPI_SwapByte(Address >> 8);
:将Address
右移 8 位,把Address
的中间 8 位移动到了低 8 位,再通过MySPI_SwapByte
函数发送这 8 位。MySPI_SwapByte(Address);
:直接使用Address
的低 8 位,通过MySPI_SwapByte
函数发送出去。
假设 Address 的值为 0x123456,它的二进制表示为:
0000 0000 0001 0010 0011 0100 0101 0110
Address >> 16 的结果为 0x0012,二进制表示为:
0000 0000 0000 0000 0000 0000 0001 0010
发送的是 0x12
Address >> 8 的结果为 0x1234,二进制表示为:
0000 0000 0000 0000 0001 0010 0011 0100
发送的是 0x34。
Address 本身为 0x123456,发送的是 0x56
这样,通过三次右移操作和 MySPI_SwapByte 函数调用,就将 32 位的地址 0x123456 拆分成三个 8 位字节 0x12、0x34 和 0x56 依次发送给了 W25Q64 芯片。
综上所述,使用右移操作是为了满足 SPI 接口每次只能传输 8 位数据的要求,将 32 位的地址拆分成 3 个 8 位字节进行传输。
4.擦除选项
void W25Q64_SectorErase(uint32_t Address)//只演示扇区擦除
{
// 使能 W25Q64 的写操作,在进行擦除操作前需要先使能写操作
W25Q64_WriteEnable();
// 启动 SPI 通信,开始与 W25Q64 进行数据交互
MySPI_Start();
// 发送扇区擦除命令(4KB 扇区),通知 W25Q64 即将进行扇区擦除操作
MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB);
// 将 32 位地址的高 8 位通过 SPI 发送给 W25Q64,指定要擦除的扇区起始地址的高字节
MySPI_SwapByte(Address >> 16);
// 将 32 位地址的中间 8 位通过 SPI 发送给 W25Q64,指定要擦除的扇区起始地址的中间字节
MySPI_SwapByte(Address >> 8);
// 将 32 位地址的低 8 位通过 SPI 发送给 W25Q64,指定要擦除的扇区起始地址的低字节
MySPI_SwapByte(Address);
// 停止 SPI 通信,结束与 W25Q64 的本次数据交互
MySPI_Stop();
// 等待 W25Q64 完成扇区擦除操作,在此期间芯片处于忙碌状态,等待其操作完成
W25Q64_WaitBusy();
}
5.读取数据
- High Impedance 高阻态
- 读没有256的限制,Data Out x可以一直读,所以后面的uint16_t Count改成了uint32_t Count,扩大了数字限制
void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count)
{
uint32_t i;
// 启动 SPI 通信,准备与 W25Q64 进行数据交互
MySPI_Start();
// 发送读取数据的命令给 W25Q64,告知芯片接下来要进行读取操作
MySPI_SwapByte(W25Q64_READ_DATA);
// 将 32 位地址的高 8 位通过 SPI 发送给 W25Q64,指定要读取数据的起始地址的高字节
MySPI_SwapByte(Address >> 16);
// 将 32 位地址的中间 8 位通过 SPI 发送给 W25Q64,指定要读取数据的起始地址的中间字节
MySPI_SwapByte(Address >> 8);
// 将 32 位地址的低 8 位通过 SPI 发送给 W25Q64,指定要读取数据的起始地址的低字节
MySPI_SwapByte(Address);
// 循环读取指定数量的数据
for (i = 0; i < Count; i++)
{
// 向 W25Q64 发送一个哑字节(无实际意义的字节),同时接收从该地址读取到的数据
// 并将读取到的数据存储到 DataArray 数组中
DataArray[i] = MySPI_SwapByte(W25Q64_DUMMY_BYTE);
}
// 停止 SPI 通信,结束与 W25Q64 的本次数据交互
MySPI_Stop();
}
注意:关于 W25Q64_WaitBusy();位置相关问题
事后等待(本次代码方法)
- 优点:在函数之外等待,能确保芯片处于不忙状态,稳定性和可靠性高。
- 缺点:等待时程序处于阻塞状态,无法执行其他代码,可能导致整体运行效率降低。
事前等待
- 优点:写完后无需等待,程序可立即执行其他代码,理论上可提高效率。有可能在执行其他代码的过程中,芯片完成忙碌状态,无需额外等待时间。
- 缺点:无法完全确定等待时间,存在等待时间不足的风险。若在芯片忙碌时进行读取操作,可能导致数据读取错误。在写入操作和读取操作之前都需要等待,适用场景相对复杂,增加了代码编写的复杂性。
void ......
{
uint16_t i;
W25Q64_WaitBusy();//事前等待
W25Q64_WriteEnable();
MySPI_Start();
.....
for (i = 0; i < Count; i ++)
{
....
}
MySPI_Stop();
}
这样就配置完了,记得去头文件申明一下,并在主函数中调用啊。
6.主函数配置
#include "stm32f10x.h" // 包含STM32F10x系列微控制器的头文件,提供了该系列芯片的寄存器定义等相关信息
#include "Delay.h" // 包含延时函数的头文件,可用于实现不同时长的延时操作
#include "OLED.h" // 包含OLED显示屏驱动的头文件,用于对OLED屏幕进行初始化和显示操作
#include "W25Q64.h" // 包含W25Q64闪存芯片驱动的头文件,用于对W25Q64芯片进行读写、擦除等操作
// 定义变量用于存储W25Q64芯片的制造商ID(MID)和设备ID(DID)
uint8_t MID;
uint16_t DID;
// 定义一个用于写入W25Q64芯片的数组,包含4个字节的数据
uint8_t ArrayWrite[] = {0x01, 0x02, 0x03, 0x04};
// 定义一个用于从W25Q64芯片读取数据的数组,长度为4字节
uint8_t ArrayRead[4];
int main(void)
{
OLED_Init();
W25Q64_Init();
// 在OLED屏幕的第1行第1列开始显示字符串 "MID: DID:",用于提示接下来要显示的信息
OLED_ShowString(1, 1, "MID: DID:");
// 在OLED屏幕的第2行第1列开始显示字符串 "W:",表示接下来要显示写入的数据
OLED_ShowString(2, 1, "W:");
// 在OLED屏幕的第3行第1列开始显示字符串 "R:",表示接下来要显示读取的数据
OLED_ShowString(3, 1, "R:");
// 调用W25Q64_ReadID函数读取W25Q64芯片的制造商ID和设备ID,并将结果存储在MID和DID变量中
W25Q64_ReadID(&MID, &DID);
// 在OLED屏幕的第1行第5列开始以十六进制形式显示制造商ID,显示2位
OLED_ShowHexNum(1, 5, MID, 2);
// 在OLED屏幕的第1行第12列开始以十六进制形式显示设备ID,显示4位
OLED_ShowHexNum(1, 12, DID, 4);
// 调用W25Q64_SectorErase函数擦除W25Q64芯片地址为0x000000的扇区,为后续写入数据做准备
W25Q64_SectorErase(0x000000);
// 调用W25Q64_PageProgram函数将ArrayWrite数组中的4个字节数据写入W25Q64芯片地址为0x000000的页面
W25Q64_PageProgram(0x000000, ArrayWrite, 4);
// 调用W25Q64_ReadData函数从W25Q64芯片地址为0x000000的位置开始读取4个字节的数据,并存储到ArrayRead数组中
W25Q64_ReadData(0x000000, ArrayRead, 4);
OLED_ShowHexNum(2, 3, ArrayWrite[0], 2);
OLED_ShowHexNum(2, 6, ArrayWrite[1], 2);
OLED_ShowHexNum(2, 9, ArrayWrite[2], 2);
OLED_ShowHexNum(2, 12, ArrayWrite[3], 2);
OLED_ShowHexNum(3, 3, ArrayRead[0], 2);
OLED_ShowHexNum(3, 6, ArrayRead[1], 2);
OLED_ShowHexNum(3, 9, ArrayRead[2], 2);
OLED_ShowHexNum(3, 12, ArrayRead[3], 2);
// 进入无限循环,程序在此处保持运行,防止程序退出
while (1)
{
}
}