一、SPI协议简介
SPI 接口提供两个主要功能,支持 SPI 协议或 I 2 S 音频协议。默认情况下,选择的是 SPI 功能。可通过软件将接口从 SPI 切换到 I 2 S。
(一)物理层
SPI 协议是由摩托罗拉公司提出的通讯协议(Serial Peripheral Interface),即串行外围设备接口,是一种高速全双工的通信总线。它被广泛地使用在 ADC、LCD 等设备与 MCU 间,要求通讯速率较高的场合。
串行外设接口 (SPI) 可与外部器件进行半双工 / 全双工的同步串行通信。该接口可配置为主模式,在这种情况下,它可为外部从器件提供通信时钟 (SCK)。该接口还能够在多主模式配置下工作。它可用于多种用途,包括基于双线的单工同步传输,其中一条可作为双向数据线,或使用CRC 校验实现可靠通信。
I2S 也是同步串行通信接口。它可满足四种不同音频标准的要求,包括 I2SPhilips 标准、MSB 和 LSB 对齐标准,以及 PCM 标准。它可在全双工模式(使用 4 引脚)或半双工模式(使用 3 个引脚)下作为从器件或主器件工作。当 I 2 S 配置为通信主模式时,该接口可以向外部从器件提供主时钟。
SPI 通讯设备之间的常用连接方式
常见的 SPI 通讯系统
SPI 通讯使用 3 条总线及片选线,3 条总线分别为 SCK、MOSI、MISO,片选线为S S ,它们的作用介绍如下:
1、SS ( Slave Select):
从设备选择信号线,常称为片选信号线,也称为 NSS、CS,以下用 NSS 表示。
当有多个 SPI 从设备与 SPI 主机相连时,设备的其它信号线 SCK、MOSI 及 MISO 同时并联到相同的 SPI 总线上,即无论有多少个从设备,都共同只使用这 3 条总线;而每个从设备都有独立的这一条 NSS 信号线,本信号线独占主机的一个引脚,即有多少个从设备,就有多少条片选信号线。I2C 协议中通过设备地址来寻址、选中总线上的某个设备并与其进行通讯;而 SPI 协议中没有设备地址,它使用 NSS 信号线来寻址,当主机要选择从设备时,把该从设备的 NSS 信号线设置为低电平,该从设备即被选中,即片选有效,接着主机开始与被选中的从设备进行 SPI 通讯。所以SPI 通讯以 NSS 线置低电平为开始信号,以 NSS 线被拉高作为结束信号。
2、SCK (Serial Clock):
时钟信号线,用于通讯数据同步。
它由通讯主机产生,决定了通讯的速率,不同的设备支持的最高时钟频率不一样,如 STM32 的 SPI 时钟频率最大为f pclk /2,两个设备之间通讯时,通讯速率受限于低速设备。
3、MOSI (Master Output,Slave Input):
主设备输出/从设备输入引脚。
主机的数据从这条信号线输出,从机由这条信号线读入主机发送的数据,即这条线上数据的方向为主机到从机。
4、 MISO(Master Input,Slave Output):
主设备输入/从设备输出引脚。
主机从这条信号线读入数据,从机的数据由这条信号线输出到主机,即在这条线上数据的方向为从机到主机。
特点
使用线数 | 4 |
---|---|
最大速度 | 最大1 0 M bps |
同步或异步 | 同步 |
串行或并行 | 串行 |
最大主机数 | 1 |
最大从机数 | 理论上无上限 |
(二)协议层
SPI协议层定义了通信的起始信号和停止信号、数据有效性、时钟同步等环节
1、SPI基本通信过程
SPI 通讯时序
这是一个主机的通讯时序。NSS、SCK、MOSI 信号都由主机控制产生,而 MISO 的信号由从机产生,主机通过该信号线读取从机的数据。MOSI 与 MISO 的信号只在 NSS 为低电平的时候才有效,在 SCK 的每个时钟周期 MOSI 和 MISO 传输一位数据。
以上通讯流程中包含的各个信号分解如下
2、通信的起始与停止信号
在图 (SPI通信时序)中的标号1处,NSS 信号线由高变低,是 SPI 通讯的起始信号。NSS 是每个从机各自独占的信号线,当从机检在自己的 NSS 线检测到起始信号后,就知道自己被主机选中了,开始准备与主机通讯。在图中的标号6处,NSS 信号由低变高,是 SPI 通讯的停止信号,表示本次通讯结束,从机的选中状态被取消。
从器件选择 (NSS) 引脚管理(Slave select (NSS)pin management)
可以使用 SPI_CR1 寄存器中的 SSM 位设置硬件或软件管理从器件选择。
- 软件管理 NSS (SSM = 1)
从器件选择信息在内部由 SPI_CR1 寄存器中的 SSI 位的值驱动。外部 NSS 引脚空闲,可供其它应用使用。
- 硬件管理 NSS (SSM = 0)
根据 NSS 输出配置(SPI_CR1 寄存器中的 SSOE 位),硬件管理 NSS 有两种模式。
- NSS 输出使能(SSM = 0,SSOE = 1)
仅当器件在主模式下工作时才使用此配置。当主器件开始通信时,NSS 信号驱动为低电平,并保持到 SPI 被关闭为止。
- NSS 输出禁止(SSM = 0,SSOE = 0)
对于在主模式下工作的器件,此配置允许多主模式功能。对于设置为从模式的器件,NSS 引脚用作传统 NSS 输入:在 NSS 为低电平时片选该从器件,在 NSS 为高电平时取消对它的片选。
3、数据的有效性
SPI 使用 MOSI 及 MISO 信号线来传输数据,使用 SCK 信号线进行数据同步。MOSI及 MISO 数据线在 SCK 的每个时钟周期传输一位数据,且数据输入输出是同时进行的。数据传输时,MSB 先行或 LSB 先行并没有作硬性规定,但要保证两个 SPI 通讯设备之间使用同样的协定,一般都会采用图 24-2 中的 MSB 先行模式。
观察图中的2、3、4、5标号处,MOSI 及 MISO 的数据在 SCK 的上升沿期间变化输出,在 SCK 的下降沿时被采样。即在 SCK 的下降沿时刻,MOSI 及 MISO 的数据有效,高电平时表示数据“1”,为低电平时表示数据“0”。在其它时刻,数据无效,MOSI 及 MISO为下一次表示数据做准备。
SPI 每次数据传输可以 8 位或 16 位为单位,每次传输的单位数不受限制。
4、时钟极性(CPOL)与时钟相位(CPHA)及通信模式
- 时钟极性(clock polarity)
时钟极性 CPOL 是指 SPI 通讯设备处于空闲状态时,SCK 信号线的电平信号(即 SPI 通讯开始前、 NSS 线为高电平时 SCK 的状态)。CPOL=0 时, SCK 在空闲状态时为低电平,CPOL=1 时,则相反。
- 时钟相位(Clock phase )
时钟相位 CPHA 是指数据的采样的时刻,当 CPHA=0 时,MOSI 或 MISO 数据线上的信号将会在 SCK 时钟线的第一个跳变开始采样(奇数边)。当 CPHA=1 时,数据线在 SCK 的第二个跳变开始采样(偶数辺)。
CPHA=0 时的 SPI 通讯模式
分析这个 CPHA=0 的时序图。首先,根据 SCK 在空闲状态时的电平,分为两种情况。SCK 信号线在空闲状态为低电平时,CPOL=0;空闲状态为高电平时,CPOL=1。
无论 CPOL=0 还是=1,因为我们配置的时钟相位 CPHA=0,在图中可以看到,采样时刻都是在 SCK 的奇数边沿。注意当 CPOL=0 的时候,时钟的奇数边沿是上升沿,而CPOL=1 的时候,时钟的奇数边沿是下降沿。
所以 SPI 的采样时刻不是由上升/下降沿决定的。MOSI 和 MISO 数据线的有效信号在 SCK 的奇数边沿保持不变,数据信号将在 SCK 奇数边沿时被采样,在非采样时刻,MOSI 和 MISO 的有效信号才发生切换。
类似地,当 CPHA=1 时,不受 CPOL 的影响,数据信号在 SCK 的偶数边沿被采样。如下图
CPHA=1 时的 SPI 通讯模式
通过 SPI_CR1 寄存器中的 CPOL 和 CPHA 位,可以用软件选择四种可能的时序关系。CPOL 及 CPHA 的不同状态,SPI 分成了四种模式,主机与从机需要工作在相同的模式下才可以正常通讯,实际中采用较多的是“模0”与“模式 3”。
SPI的四种模式
SPI模式 | CPLO | CPHA | 空闲时SCK时钟 | 采样时刻 |
---|---|---|---|---|
0 | 0 | 0 | 低电平 | 奇数边 |
1 | 0 | 1 | 低电平 | 偶数边 |
2 | 1 | 0 | 高电平 | 奇数边 |
3 | 1 | 1 | 高电平 | 偶数边 |
注意:
- 在切换 CPOL/CPHA 位之前,必须通过复位 SPE 位来关闭 SPI 。
- 必须以同一时序模式对主器件和从器件进行编程。
- SCK 的空闲状态必须与 SPI_CR1 寄存器中选择的极性相对应(如果 CPOL=1 ,则上拉 SCK ;如果 CPOL=0 ,则下拉 SCK )。
通过 SPI_CR1 寄存器中的 DFF 位选择数据帧格式( 8 或 16 位),该格式决定了发送 / 接收过程中的数据长度。
二、STM32的SPI与I2S主要特性及架构
(一)SPI特性与I2S特性
1、SPI特性
基于三条线的全双工同步传输
- 基于双线的单工同步传输,其中一条可作为双向数据线
- 8 位或 16 位传输帧格式选择
- 主模式或从模式操作
- 多主模式功能
- 8 个主模式波特率预分频器(最大值为 f PCLK/2)
- 从模式频率(最大值为 f PCLK /2)
- 对于主模式和从模式都可实现更快的通信
- 对于主模式和从模式都可通过硬件或软件进行 NSS 管理:动态切换主 / 从操作
- 可编程的时钟极性和相位
- 可编程的数据顺序,最先移位 MSB 或 LSB
- 可触发中断的专用发送和接收标志
- SPI 总线忙状态标志
- SPI TI 模式
- 用于确保可靠通信的硬件 CRC 功能:
- 在发送模式下可将 CRC 值作为最后一个字节发送
- — 根据收到的最后一个字节自动进行 CRC 错误校验
- 可触发中断的主模式故障、上溢和 CRC 错误标志
具有 DMA 功能的 1 字节发送和接收缓冲器:发送和接收请求
2、I2S特性
全双工通信
- 半双工通信(仅作为发送器或接收器)
- 主模式或从模式操作
- 8 位可编程线性预分频器,可实现精确的音频采样频率(从 8 kHz 到 192 kHz)
- 数据格式可以是 16 位、24 位或 32 位
- 数据包帧由音频通道固定为 16 位(可容纳 16 位数据帧)或 32 位(可容纳 16 位、
- 24 位、32 位数据帧)
- 可编程的时钟极性(就绪时的电平状态)
- 从发送模式下的下溢标志、接收模式下的上溢标志(主模式和从模式),以及接收和发
- 送模式下的帧错误标志(仅从模式)
- 发送和接收使用同一个 16 位数据寄存器
- 向始终为 MSB 在前
- 用于发送和接收的 DMA 功能(16 位宽)
- 可输出主时钟以驱动外部音频元件。比率固定为 256 × F S (其中 F S 为音频采样频率)
- 两个 I 2 S(I2S2 和 I2S3)均有专用的 PLL (PLLI2S),可生成更为精确的时钟。
I 2 S(I2S2 和 I2S3)时钟可由 I2S_CKIN 引脚上的外部时钟提供。
(二)STM32的SPI 架构
1、通信引脚
通常,SPI 通过 4 个引脚与外部器件连接:
MISO:主输入/从输出数据。此引脚可用于在从模式下发送数据和在主模式下接收数据。
- MOSI:主输出/从输入数据。此引脚可用于在主模式下发送数据和在从模式下接收数据。
- SCK:用于 SPI 主器件的串行时钟输出以及 SPI 从器件的串行时钟输入。
- NSS:从器件选择。这是用于选择从器件的可选引脚。此引脚用作“片选”,可让 SPI
主器件与从器件进行单独通信,从而并避免数据线上的竞争。从器件的 NSS 输入可由主器件上的标准 IO 端口驱动。NSS 引脚在使能(SSOE 位)时还可用作输出,并可在SPI 处于主模式配置时驱动为低电平。通过这种方式,只要器件配置成 NSS 硬件管理模式,所有连接到该主器件 NSS 引脚的其它器件 NSS 引脚都将呈现低电平,并因此而作为从器件。当配置为主模式,且 NSS 配置为输入(MSTR=1 且 SSOE=0)时,如果NSS 拉至低电平,SPI 将进入主模式故障状态:MSTR 位自动清零,并且器件配置为从模式。
下图是单个主器件和单个从器件之间的互连的基本示例
单个主器件和单个从器件之间的互连
2、时钟控制逻辑
SCK 线的时钟信号,由波特率发生器根据“控制寄存器 CR1”中的 BR[0:2]位控制,该位是对 f pclk 时钟的分频因子,对 f pclk 的分频结果就是 SCK 引脚的输出时钟频率,计算方法见下表 。
其中的 f pclk 频率是指 SPI 所在的 APB 总线频率,APB1 为 f pclk1 ,APB2 为 f pckl2 。
通过配置“控制寄存器 CR”的“CPOL 位”及“CPHA”位可以把 SPI 设置成前面分析的 4 种 SPI 模式。
3、数据控制逻辑
SPI 的 MOSI 及 MISO 都连接到数据移位寄存器上,数据移位寄存器的内容来源于接收缓冲区及发送缓冲区以及 MISO、MOSI 线。
当向外发送数据的时候,数据移位寄存器以“发送缓冲区”为数据源,把数据一位一位地通过数据线发送出去;当从外部接收数据的时候,数据移位寄存器把数据线采样到的数据一位一位地存储到“接收缓区”中。通过写 SPI 的“数据寄存器 DR”把数据填充到发送缓冲区中,通过 “数据寄存器 DR”,可以获取接收缓冲区中的内容。
其中数据帧长度可以通过“控制寄存器 CR1”的“DFF 位”配置成 8 位及 16 位模式;配置“LSBFIRST 位”可选择 MSB 先行还是 LSB 先行。
4、整体控制逻辑
整体控制逻辑负责协调整个 SPI 外设,控制逻辑的工作模式根据我们配置的“控制寄存器(CR1/CR2)”的参数而改变,基本的控制参数包括前面提到的 SPI 模式、波特率、LSB先行、主从模式、单双向模式等等。
在外设工作时,控制逻辑会根据外设的工作状态修改“状态寄存器(SR)”,我们只要读取状态寄存器相关的寄存器位,就可以了解 SPI 的工作状态了。除此之外,控制逻辑还根据要求,负责控制产生 SPI 中断信号、DMA 请求及控制NSS 信号线。
实际应用中,我们一般不使用 STM32 SPI 外设的标准 NSS 信号线,而是更简单地使用普通的 GPIO,软件控制它的电平输出,从而产生通讯起始和停止信号。
(三)数据发送和接收过程
接收缓冲区和发送缓冲区
在接收过程中,数据收到后,先存储到内部接收缓冲区中;而在发送过程中,先将数据存储到内部发送缓冲区中,然后发送数据。
对 SPI_DR 寄存器的读访问将返回接收缓冲值,而对 SPI_DR 寄存器的写访问会将写入的数据存储到发送缓冲区中。
1、在主模式下启动通信序列
- 在全双工模式下(BIDIMODE=0 且 RXONLY=0)
- 将数据写入到 SPI_DR 寄存器(发送缓冲区)时,通信序列启动。
- 随后在第一个位的发送期间,将数据从发送缓冲区并行加载到 8 位移位寄存器中,然后以串行方式将其移出到 MOSI 引脚。
- 同时,将 MISO 引脚上接收的数据以串行方式移入 8 位移位寄存器,然后并行加载到 SPI_DR 寄存器(接收缓冲区)中。
- 在单向只接收模式下(BIDIMODE=0 且 RXONLY=1)
- 只要 SPE = 1,通信序列就立即开始。
- 只有接收器激活,并且在 MISO 引脚上接收的数据以串行方式移入 8 位移位寄存器,然后并行加载到 SPI_DR 寄存器(接收缓冲区)中。
- 在双向模式下,进行发送时(BIDIMODE=1 且 BIDIOE=1)
- 将数据写入到 SPI_DR 寄存器(发送缓冲区)时,通信序列启动。
- 随后在第一个位的发送期间,将数据从发送缓冲区并行加载到 8 位移位寄存器中,然后以串行方式将其移出到 MOSI 引脚。
- 不接收任何数据。
- 在双向模式下,进行接收时(BIDIMODE=1 且 BIDIOE=0)
- 只要 SPE=1 且 BIDIOE=0,通信序列就立即开始。
- 在 MOSI 引脚上接收的数据以串行方式移入 8 位移位寄存器,然后并行加载到 SPI_DR寄存器 (接收缓冲区)中。
- 发送器没有激活,因此不会有数据以串行方式移出 MOSI 引脚。
下面主要介绍全双工模式下(BIDIMODE=0 且 RXONLY=0)的情况,其他模式可以查询参考手册
2、处理数据发送与接收
将数据从发送缓冲区传输到移位寄存器时,TXE 标志(发送缓冲区为空)置 1。该标志表示内部发送缓冲区已准备好加载接下来的数据。如果 SPI_CR2 寄存器中的 TXEIE 位置 1,可产生中断。通过对 SPI_DR 寄存器执行写操作将 TXE 位清零。
将数据从移位寄存器传输到接收缓冲区时,RXNE 标志(接收缓冲区非空)会在最后一个采样时钟边沿置 1。它表示已准备好从 SPI_DR 寄存器中读取数据。如果 SPI_CR2 寄存器中的 RXNEIE 位置 1,可产生中断。通过读取 SPI_DR 寄存器将 RXNE 位清零。对于某些配置,可以在最后一次数据传输期间使用 BSY 标志来等待传输完成。
注意:
软件必须确保在尝试写入发送缓冲区之前 TXE 标志已置 1 。否则,将覆盖之前写入发送缓冲区的数据。
(1)主模式或从模式下的全双工发送和接收过程(连续传输)
(BIDIMODE=0 且 RXONLY=0)
软件必须遵循以下步骤来发送和接收数据(参见下图):
- 通过将 SPE 位置 1 来使能 SPI。
- 将第一个要发送的数据项写入 SPI_DR 寄存器(此操作会将 TXE 标志清零)。
- 等待至 TXE=1,然后写入要发送的第二个数据项。然后等待至 RXNE=1,读取 SPI_DR以获取收到的第一个数据项(此操作会将 RXNE 位清零)。对每个要发送/ 接收的数据项重复此操作,直到第 n-1 个接收的数据为止。
- 等待至 RXNE=1,然后读取最后接收的数据。
- 等待至 TXE=1,然后等待至 BSY=0,再关闭 SPI。
此外,还可以使用在 RXNE 或 TXE 标志所产生的中断对应的各个中断子程序来实现该过程。
主模式
从模式
(2)连续传输和间断传输
在主模式下发送数据时,如果软件速度快到能在下一个数据传输完成之前响应该数据从数据寄存器传输到移位寄存器所触发的 TXE 中断,并能立即完成再下一个数据的写 SPI_DR 操作,则这种通信称为连续通信。在这种情况下,各数据项之间 SPI 时钟的生成不会间断,并且各数据传输之间不会清零 BSY 位。
相反,如果软件速度不够快,则可能导致通信中断。在这种情况下,各数据传输之间会清零BSY 位(参见下图)。
发送时(BIDIRMODE=0 且 RXONLY=0)的 TXE/BSY 行为(在间断传输的情况下)
在主设备仅接收模式 (RXONLY=1) 下,通信始终是连续的,且 BSY 标志始终读为 1。在从模式下,通信的连续性由 SPI 主器件决定。任何情况下(即使通信是连续的),在各个数据传输之间 BSY 标志都会短暂变为低电平,持续时间为一个 SPI 时钟周期。
三、SPI通信实现
四、SPI通信中注意的问题
(一)“错位”问题
1、现象
在Linux上SPI接收到的第一帧数据(8或16bit)为0,导致后面取出的数据错位。
假设现在需要循环传输1,2,3,4,5,6,7,8,9这9个数,包含CRC校验
理想情况下传输的一组数据格式
0x01 | 0x02 | 0x03 | 0x04 | 0x05 | 0x06 | 0x07 | 0x08 | 0x09 | CRC |
---|---|---|---|---|---|---|---|---|---|
接收端接收的数据格式为
第一组数据:
0x00 | 0x01 | 0x02 | 0x03 | 0x04 | 0x05 | 0x06 | 0x07 | 0x08 | 0x09 |
---|---|---|---|---|---|---|---|---|---|
第二组数据:
RCR | 0x01 | 0x02 | 0x03 | 0x04 | 0x05 | 0x06 | 0x07 | 0x08 | 0x09 |
---|---|---|---|---|---|---|---|---|---|
第三组数据:
RCR | 0x01 | 0x02 | 0x03 | 0x04 | 0x05 | 0x06 | 0x07 | 0x08 | 0x09 |
---|---|---|---|---|---|---|---|---|---|
2、原因
是主从端写入和读取顺序不一致,现在作为从端的STM32,是先察看接收的缓冲区可读性,再检查发送缓冲区的可写性,然后向发送缓冲区写数据。(2020年11月14日),根本原因是STM32上的程序首先使能的是可读中断,在可读之前,既没有向发送缓冲区写也没有从接收缓冲区中读,再加上SPI通信的接收缓冲区可读,要比时序晚一帧的时间,导致第一个时间周期,发送端没有向缓冲区内写数据,导致接收端多读了一个字节0x00数据。如下图
3、解决方案
将STM32的中断修改为先使能可写中断,保证能够发出对方能够接收到第一帧数据,二不会导致错序。
实际上在本项目中,STM32端作为数据的生产者,当发送缓冲区可写时(说明发送缓冲区中的数据已经被发送)应该向发送缓冲区中“补充”数据。
因为STM32端对读取数据的要求不那么高,所以可以在接收到可写中断时,顺带检查接收缓冲区的可读性,取出缓冲区中的数据,而不需要单独设置接收缓冲区的可读中断。能够这么做的另一个原因是,除了第一帧数据,如果时序正确且传输连续,能够可写,一般是可读的,因为SPI收发是移位寄存器一个bit一个bit数据交换的。可写中断触发后,向缓冲区补充数据,随后检查接收缓冲区的可读标志即可。
(二)SPI传输过程丢失或增加一个或多个字节
1、现象
导致数据错位,以后每组数据不是CRC校验位结尾,导致数据校验出错
……
正常数据组
0x01 | 0x02 | 0x03 | 0x04 | 0x05 | 0x06 | 0x07 | 0x08 | 0x09 | CRC |
---|---|---|---|---|---|---|---|---|---|
……
异常数据组(干扰丢了一个字节0x06)
0x01 | 0x02 | 0x03 | 0x04 | 0x05 | 0x07 | 0x08 | 0x09 | CRC | 0x01 |
---|---|---|---|---|---|---|---|---|---|
0x02 | 0x03 | 0x04 | 0x05 | 0x06 | 0x07 | 0x08 | 0x09 | CRC | 0x01 |
---|---|---|---|---|---|---|---|---|---|
2、解决方案
在多次出现CRC校验出错后,应该怀疑是数据发生了偏移,每组最后一个字节(一阵数据)可能不是CRC校验值了,但是我们不知道是因为丢了一个还是多个,是增加了一个还是多个。
现假定我们一组数据长度为n字节,认为丢失一个字节的概率更大(或增n-1个字节),所以在多次CRC校验错误后,先从缓冲区中读取n-1个字节(取出不完整的一组数据)丢弃,然后在读取n个字节,若校验正确,说明数据偏移矫正成功,如果校验失败,说明矫正失败。继续矫正,认为原始错误丢失2个字节(或增加n-2个字节),再次从缓冲区中读取n-1(不是n-2,因为第一次矫正已经取出了一个字节)个字节丢弃,然后再取出n个字节,若校验正确,说明矫正成功,否则失败,继续以此方式矫正,直到i=n-1 (i为连续矫正次数),即极端强况下需要矫正i=n-1次。现假定我们一组数据长度为n字节,认为丢失一个字节的概率更大(或增n-1个字节),所以在多次CRC校验错误后,先从缓冲区中读取n-1个字节(取出不完整的一组数据)丢弃,然后在读取n个字节,若校验正确,说明数据偏移矫正成功,如果校验失败,说明矫正失败。继续矫正,认为丢失2个字节(或增加n-2个字节),先从缓冲区中读取n-2个字节丢弃,再取出n个字节,若校验正确,说明矫正成功,否则失败,继续以此方式矫正,直到n-i=1 (i为连续矫正次数),即极端强况下需要矫正i=n-1次。(有问题)
改进版的算法是如果不是减少一个字节,那就认为是增加了一个字节。(算法绕人)所以第一次矫正取出n-1,如果第一次矫正失败,认为原始错误是增加了一个字节,第二次矫正应该取出2个字节;如果矫正失败,第三次认为是原始错误减少了两个字节,所以第三次应该取n-4,如果矫正失败,第四次认为是原始错误是增加了两个字节,