P89LPC93x单片机SPI接口配置与驱动开发实战指南

1. 项目概述与SPI核心价值

如果你正在捣鼓一块基于NXP P89LPC933/934/935/936系列的老牌51内核单片机,想要驱动一个SPI Flash、TFT屏幕或者数字传感器,却发现官方用户手册(UM10116)里那几十页关于SPI的章节读起来像天书,寄存器位描述看得人头大,那么这篇笔记可能就是为你准备的。我最近刚好在用一个P89LPC935做个小玩意,需要和一颗SPI接口的温湿度传感器通信,把手册翻来覆去啃了好几遍,又实际调通了代码,这里就把SPI接口从硬件原理到软件配置,再到实际调试中踩过的坑,系统地梳理一遍。

SPI,全称Serial Peripheral Interface,中文叫串行外设接口。它不像UART那样需要事先约定好波特率,也不像I2C那样需要复杂的起始、停止和应答信号。SPI的核心就是一个“同步时钟”加几根数据线,主设备产生时钟,从设备跟着时钟节拍收发数据,简单粗暴,效率极高。在P89LPC93x这类资源有限的8位MCU上,SPI是连接外部世界的高速通道,官方标称最高支持3 Mbit/s的速率,对于大多数传感器、存储器和显示模块来说,这个速度已经绰绰有余。它的技术价值就在于用最少的引脚(通常4根线)和最简单的协议,实现了全双工的高速数据流,极大简化了PCB布线和软件驱动开发的复杂度。

2. SPI硬件架构与引脚功能解析

要玩转P89LPC93x的SPI,首先得把它当成一个独立的硬件模块来理解,而不是一堆零散的寄存器位。这个模块内部有一个8位的移位寄存器、一个时钟生成逻辑、以及配套的控制和状态逻辑。对我们程序员来说,最关键的就是那四根物理引脚和三个特殊功能寄存器(SFR)。

2.1 四线制引脚定义与角色

这四根线是SPI通信的物理基础,每一根都有明确的角色,接错了通信肯定失败。

  • SPICLK (P2.5): 串行时钟线,由主设备产生,从设备接收。它是一切数据收发的节拍器。时钟的极性(CPOL)和相位(CPHA)决定了数据在时钟的哪个边沿被采样和更新,这是SPI模式配置的核心,后面会细说。
  • MOSI (P2.2): 主设备输出,从设备输入。顾名思义,主设备通过这根线发送数据给从设备。
  • MISO (P2.3): 主设备输入,从设备输出。从设备通过这根线将数据发回给主设备。这里有个关键点:当一个SPI从设备未被选中时(SS为高),它的MISO引脚必须处于高阻态(Hi-Z),否则会与总线上其他设备的数据线冲突,导致通信紊乱。P89LPC93x的硬件会自动处理这一点。
  • SS (P2.4): 从设备选择线,低电平有效。这是SPI主从架构的关键。在一个多从设备的系统中,主设备通过控制不同的GPIO引脚拉低对应从设备的SS引脚,来选中与之通信的从机。对于从设备而言,只有当自己的SS引脚被拉低时,它才会响应SPI时钟和数据。

注意:当SPI功能被禁用(SPEN=0)时,这四个引脚(P2.2, P2.3, P2.4, P2.5)会恢复为普通的I/O口功能。在初始化SPI前,务必确保已经通过SPCTL寄存器开启了SPI功能,否则你操作的可能只是一个普通的GPIO。

2.2 核心寄存器拆解:SPCTL, SPSTAT, SPDAT

手册里表格很多,但抓住这三个寄存器就抓住了SPI的命脉。

SPI控制寄存器 (SPCTL - 地址 E2h)这是SPI模块的“大脑”,所有的模式、速率、时序配置都在这里。

符号描述复位值实操解读
7SSIGSS引脚忽略控制00:SS引脚功能有效,用于模式选择(主/从)。1:忽略SS引脚,由MSTR位单独决定主从模式。通常在多主机或固定主从系统中设置为1以简化设计。
6SPENSPI使能01:开启SPI功能,相关引脚被SPI模块占用。0:关闭SPI,引脚作普通I/O。上电后第一步就是将此位置1。
5DORD数据位顺序00:高位(MSB)先发送。1:低位(LSB)先发送。必须与外设器件的数据格式严格匹配,否则读到的数据全是乱的。大部分SPI器件默认是MSB在先。
4MSTR主/从模式选择01:主机模式。0:从机模式。当SSIG=0时,此位可能被硬件自动清零(见模式切换部分)。
3CPOL时钟极性00:时钟空闲时为低电平。1:时钟空闲时为高电平。决定了SPICLK引脚在无数据传输时的稳态电平。
2CPHA时钟相位10/1:与CPOL组合,定义数据采样的时钟边沿。共形成4种SPI模式(Mode 0-3)。这是SPI通信中最容易配错的地方。
1SPR1时钟速率选择0SPR1和SPR0共同决定SPI时钟相对于系统时钟(CCLK)的分频系数,从而设置通信波特率。
0SPR0时钟速率选择000: CCLK/4; 01: CCLK/16; 10: CCLK/64; 11: CCLK/128。

SPI状态寄存器 (SPSTAT - 地址 E1h)这是SPI模块的“眼睛”,用来判断一次传输是否完成,或者是否发生了错误。

符号描述复位值实操解读
7SPIF传输完成标志0硬件置1:当一次完整的8位数据传输完成时,该位由硬件自动置1。软件清0:必须通过向该位写1来清除。这是判断一次SPI收发是否结束的唯一标志。
6WCOL写冲突标志0硬件置1:当SPI数据寄存器(SPDAT)在数据传输过程中被写入时,该位置1。软件清0:通过向该位写1清除。表明你试图在SPI忙的时候写数据,这次写操作无效。
5:0-保留x读为未定义值,应写0。

SPI数据寄存器 (SPDAT - 地址 E3h)这是SPI模块的“嘴巴”和“耳朵”,8位宽,读写都通过它。

  • 写入SPDAT:在主机模式下,向此寄存器写入一个字节,会立即启动一次SPI发送(同时也会接收一个字节)。在从机模式下,写入的数据会被放入发送缓冲区,等待主机时钟来移出。
  • 读取SPDAT:读取此寄存器,得到的是接收缓冲区里的数据,即最近一次SPI交换(主机发送/从机接收,或从机发送/主机接收)所获取到的字节。

这里有一个非常重要的硬件机制:SPI的发送是单缓冲的,而接收是双缓冲的。这意味着,在上一字节还没发送完之前,你不能写入下一个字节(否则会触发WCOL)。但是,在接收时,硬件会将移位寄存器里的数据自动转移到独立的接收缓冲区,这样移位寄存器可以立即准备接收下一个字节,只要你在下一个字节接收完成前,把当前接收缓冲区的数据读走就行。

3. SPI主从模式配置与实战代码

理解了寄存器,我们来动手配置。根据你的系统架构,配置方法截然不同。

3.1 经典单主单从配置

这是最常见的场景,比如MCU作为主机,连接一个SPI Flash作为从机。

主机配置步骤:

  1. 初始化引脚:虽然SPEN使能后引脚功能会自动切换,但良好的习惯是先确保P2.2 (MOSI), P2.5 (SPICLK) 初始化为推挽输出模式,P2.3 (MISO) 初始化为输入模式。SS引脚(P2.4)在此模式下可以配置为普通GPIO输出,用于控制从机的片选。
  2. 配置SPCTL寄存器
    • SSIG = 1:我们忽略SS引脚功能,用GPIO控制从机片选。
    • SPEN = 1:使能SPI模块。
    • DORD = 0:假设外设是MSB在先。
    • MSTR = 1:设置为主机模式。
    • CPOLCPHA:根据外设手册选择。例如,对于大多数SPI Flash,常用Mode 0 (CPOL=0, CPHA=0) 或 Mode 3 (CPOL=1, CPHA=1)。这里以Mode 0为例,设置CPOL=0,CPHA=0
    • SPR1, SPR0:设置SPI时钟分频。假设系统CCLK为12MHz,想要1.5Mbit/s的速率,则分频系数应为8(12M/8=1.5M)。查表可知,CCLK/8这个分频不存在,最接近的是CCLK/4 (3M) 或 CCLK/16 (750k)。我们选择CCLK/16,即SPR1=0, SPR0=1
  3. 编写收发函数:主机发起通信的流程是:拉低从机SS -> 写入SPDAT启动传输 -> 等待SPIF置位 -> 读取SPDAT获取从机返回数据 -> 拉高从机SS。
// 假设SS控制引脚连接在P0.0 sbit SPI_SS = P0^0; void SPI_MasterInit(void) { // 1. 配置SPI相关引脚模式 (以准双向口为例,实际可根据驱动能力调整) P2M1 &= ~((1<<2) | (1<<5)); // P2.2(MOSI), P2.5(SCLK) 推挽输出 P2M2 |= ((1<<2) | (1<<5)); P2M1 &= ~(1<<3); // P2.3(MISO) 仅输入 P2M2 &= ~(1<<3); // 2. 配置SPI控制寄存器: Mode 0, Master, CCLK/16, SS忽略 SPCTL = (1<<7) | // SSIG = 1 (1<<6) | // SPEN = 1 (0<<5) | // DORD = 0 (MSB first) (1<<4) | // MSTR = 1 (Master) (0<<3) | // CPOL = 0 (0<<2) | // CPHA = 0 (0<<1) | // SPR1 = 0 (1<<0); // SPR0 = 1 (CCLK/16) SPI_SS = 1; // 默认不选中从机 } unsigned char SPI_MasterTransfer(unsigned char dat) { unsigned char recv_data; SPSTAT = 0xC0; // 清除SPIF和WCOL标志位(写1清零) SPDAT = dat; // 写入数据,启动传输 while (!(SPSTAT & 0x80)); // 等待传输完成标志SPIF置位 recv_data = SPDAT; // 读取接收到的数据 return recv_data; } // 使用示例:向从机发送一个命令字节并读取一个字节的响应 unsigned char SPI_SendCommand(unsigned char cmd) { unsigned char response; SPI_SS = 0; // 选中从设备 response = SPI_MasterTransfer(cmd); SPI_SS = 1; // 释放从设备 return response; }

从机配置步骤:

  1. 初始化引脚:确保P2.3 (MISO) 初始化为输出模式,P2.2 (MOSI), P2.5 (SPICLK), P2.4 (SS) 初始化为输入模式。
  2. 配置SPCTL寄存器
    • SSIG = 0:使用SS引脚功能。
    • SPEN = 1:使能SPI。
    • MSTR = 0:设置为从机模式。
    • CPOLCPHA必须与主机严格一致
  3. 中断或轮询处理:从机通信由主机时钟驱动。从机需要预先将要发送的数据写入SPDAT。当主机发起传输时,从机会自动接收数据并置位SPIF。从机应在SPIF置位后,读取收到的数据,并准备下一个要发送的数据。
void SPI_SlaveInit(void) { // 配置SPI相关引脚模式 P2M1 &= ~(1<<3); // P2.3(MISO) 推挽输出 P2M2 |= (1<<3); P2M1 |= ((1<<2) | (1<<4) | (1<<5)); // P2.2(MOSI), P2.4(SS), P2.5(SCLK) 高阻输入 P2M2 &= ~((1<<2) | (1<<4) | (1<<5)); // 配置SPI控制寄存器: Mode 0, Slave, 使用SS引脚 SPCTL = (0<<7) | // SSIG = 0 (1<<6) | // SPEN = 1 (0<<5) | // DORD = 0 (0<<4) | // MSTR = 0 (Slave) (0<<3) | // CPOL = 0 (0<<2) | // CPHA = 0 (0<<1) | // SPR1 = 0 (从机忽略时钟分频) (0<<0); // SPR0 = 0 // 使能SPI中断(可选) IEN1 |= 0x08; // 设置ESPI (IEN1.3) = 1 EA = 1; // 全局中断使能 // 预先写入一个初始数据(例如设备ID) SPDAT = 0xA5; } // SPI中断服务函数 void SPI_ISR(void) interrupt 9 { // SPI中断向量号需查手册确定 unsigned char recv_cmd, send_data; if (SPSTAT & 0x80) { // 检查SPIF标志 SPSTAT = 0x80; // 清除SPIF标志(写1清零) recv_cmd = SPDAT; // 读取主机发来的命令 // 根据命令准备要返回的数据 send_data = ProcessCommand(recv_cmd); // 将待发送数据写入SPDAT,准备下一次传输 SPDAT = send_data; } // 注意:从机模式下,WCOL也可能发生,如果SPDAT在传输中被写 if (SPSTAT & 0x40) { SPSTAT = 0x40; // 清除WCOL标志 // 处理写冲突错误 } }

3.2 多从机系统与SS片选管理

当单个主机需要控制多个SPI从设备时,硬件连接上所有从机的SPICLK、MOSI、MISO分别并联到主机的对应引脚。关键在于SS片选线的管理。每个从机需要独立的SS线。主机通过控制不同的GPIO引脚,在同一时刻只拉低一个从机的SS,确保总线不会冲突。

配置要点:

  • 主机:配置与单主单从相同,SSIG=1MSTR=1。使用多个普通GPIO口(如P0.0, P0.1, P0.2)分别连接到各从机的SS引脚。
  • 从机:每个从机的配置也与前述相同,SSIG=0MSTR=0。每个从机只响应自己SS引脚的低电平信号。
  • 软件流程:主机在与某个从机通信前,先将其对应的SS线拉低,通信结束后再拉高。在切换从机时,必须确保前一个从机的SS已拉高,再拉低下一个从机的SS,中间最好有短暂延时,避免信号毛刺导致误触发。

3.3 主从动态切换与“模式改变”机制

P89LPC93x的SPI支持一个有趣的功能:通过SS引脚动态改变主从模式。这在两个对等设备需要互为主从的场合(例如通过SPI互连的两块MCU)非常有用。

机制原理: 当设备配置为主机模式(SPEN=1,SSIG=0,MSTR=1),且其SS引脚(P2.4)被配置为输入或准双向模式时,如果另一个主机将该SS引脚拉低,本设备会被强制切换为从机模式(硬件自动将MSTR位清零)。同时,SPIF标志会被置位,如果中断使能,还会产生SPI中断。

应用场景与配置: 假设Device A和Device B通过SPI直连(MOSI-MOSI, MISO-MISO, SPICLK-SPICLK, SS-SS)。两者初始都配置为“潜在主机”:

  • SPEN = 1
  • SSIG = 0// 使用SS引脚功能
  • MSTR = 1// 初始设为主机
  • P2.4 (SS)配置为准双向口P2M2.4, P2M1.4 = 01

当Device A想要发送数据时,它执行以下操作:

  1. 将自己的SS引脚(P2.4)配置为输出模式P2M2.4, P2M1.4 = 00? 注意:准双向口本身可输出低电平,这里更准确的是直接操作端口寄存器拉低)。
  2. 驱动自己的SS引脚输出低电平。
  3. 这个低电平信号会作用于Device B的SS引脚,导致Device B的MSTR位被硬件清零,变为从机。
  4. 此时,Device A是唯一的主机,可以开始SPI通信。

软件注意事项

  • 在作为“潜在主机”的设备中,软件必须定期检查MSTR位。如果发现MSTR被意外清零(即自己被选为了从机),应进入从机接收状态。
  • 通信结束后,发起方(Device A)应释放SS线(拉高),并可能将SS引脚配置回输入/准双向模式,等待下一次通信。
  • 这个机制对时序要求严格,软件需要妥善处理竞争和状态切换,否则容易导致总线冲突或死锁。

4. SPI时钟与数据模式深度解析

SPI通信的时序由CPOL和CPHA两位精确控制,形成了四种标准模式。理解这四种模式的波形差异,是调试SPI通信的基石。

4.1 CPOL与CPHA:定义四种SPI模式

  • CPOL (Clock Polarity): 时钟极性。决定了SCLK线在空闲时的状态。
    • CPOL=0: 时钟空闲时为低电平。
    • CPOL=1: 时钟空闲时为高电平。
  • CPHA (Clock Phase): 时钟相位。决定了数据是在时钟的第一个边沿还是第二个边沿被采样(捕获)。
    • CPHA=0: 数据在第一个时钟边沿被采样。对于CPOL=0,第一个边沿是上升沿;对于CPOL=1,第一个边沿是下降沿。
    • CPHA=1: 数据在第二个时钟边沿被采样。

组合起来就是以下四种模式,主从设备的模式必须完全一致

模式CPOLCPHA时钟空闲电平数据采样边沿数据变化边沿
Mode 000低电平第一个上升沿下降沿
Mode 101低电平第二个下降沿上升沿
Mode 210高电平第一个下降沿上升沿
Mode 311高电平第二个上升沿下降沿

关键记忆点:对于P89LPC93x,当CPHA=0时,数据在时钟的前沿(Leading Edge)被采样,在后沿(Trailing Edge)更新。当CPHA=1时,则相反。这个“前沿”和“后沿”的具体是上升沿还是下降沿,则由CPOL决定。

4.2 模式0 (CPOL=0, CPHA=0) 详解与波形

这是最常用的模式。我们结合手册中的图45(从机格式)和图47(主机格式)来分析。

时序特点

  1. 空闲时,SCLK为低电平。
  2. 当主机启动传输(写入SPDAT),SCLK在短暂延时后产生第一个脉冲。
  3. 数据采样时刻:在SCLK的第一个上升沿,主从双方同时采样对方的数据线(主机采样MISO,从机采样MOSI)。这意味着,在上升沿到来之前,数据必须已经稳定在数据线上。
  4. 数据更新时刻:在SCLK的下降沿,双方将下一个要发送的数据位放到数据线上。

对软件的影响

  • 主机:在写入SPDAT启动传输后,数据会在约半个到一个SPI位时间后出现在MOSI线上,确保在第一个上升沿前稳定。
  • 从机:必须在自己的SS引脚变低且第一个SCLK上升沿到来之前,将第一个要发送的数据位准备好。这就要求从机的响应速度要足够快,通常需要在SS下降沿触发的中断中,或通过预先写入SPDAT来准备数据。

4.3 模式3 (CPOL=1, CPHA=1) 及其他模式

Mode 3是另一种常见模式,尤其在一些存储器中用到。

时序特点

  1. 空闲时,SCLK为高电平。
  2. 数据采样时刻:在SCLK的第二个上升沿(即第一个下降沿之后的那个上升沿)。
  3. 数据更新时刻:在SCLK的下降沿

Mode 1和Mode 2相对较少使用,但原理相通。选择哪种模式,完全取决于你的外设器件的数据手册要求,绝对不能自己想当然。

4.4 时钟预分频器(SPR1, SPR0)配置与速率计算

SPI的通信速率由系统主时钟(CCLK)和SPCTL寄存器中的SPR1、SPR0分频位共同决定。P89LPC93x提供4种分频选择。

速率计算公式SPI_BaudRate = CCLK / (4 * (2^(2*SPR1 + SPR0)) )更直观地看下表:

SPR1SPR0分频系数SPI时钟频率 (当CCLK=12MHz)
0043.0 MHz
0116750 kHz
1064187.5 kHz
1112893.75 kHz

选择策略

  1. 不超过外设极限:首先查看外设器件手册的最大支持SCLK频率,确保配置的速率低于此值。
  2. 考虑信号完整性:在长导线或干扰较大的环境中,过高频率会导致信号畸变,应适当降低速率。
  3. 匹配从机速度:如果从机是低速设备(如某些传感器),主机速率过高可能导致从机来不及响应。
  4. 系统负载:更高的SPI速率意味着更频繁的中断或DMA请求,会占用更多CPU时间或总线带宽。

一个常见的做法是,在初始化时设置一个较低的、可靠的速率(如CCLK/64),在确认通信正常后,如果需要高性能,再尝试提高速率。

5. 高级主题与疑难杂症排查

在实际项目中,仅仅完成基础通信往往不够,还会遇到一些棘手的边界情况和错误。

5.1 写冲突(Write Collision)的产生与处理

写冲突标志WCOL是SPI状态寄存器里一个非常重要的错误指示位。它的触发条件是:在SPI数据传输正在进行的过程中(即SPIF=0期间),软件向SPDAT数据寄存器执行了写操作

为什么会发生?

  • 主机端:相对少见,因为主机通常能控制发送节奏。但如果程序bug导致在未检查SPIF标志时就连续写入SPDAT,就会触发。
  • 从机端:非常常见!从机无法预知主机何时发起传输。如果从机在主机时钟正在移出数据的中间时刻,尝试更新SPDAT中的数据,就会发生写冲突。

硬件行为:当写冲突发生时,WCOL位被硬件置1。重要的是,这次冲突的写入操作是无效的,SPDAT中准备发送的数据不会被加载到移位寄存器,当前正在进行的传输不受影响,继续完成。

软件处理流程

  1. 在每次写入SPDAT之前,尤其是从机在中断服务程序中准备下一个发送数据时,必须检查SPIF标志,确保上一次传输已完成。
  2. 在SPI中断服务程序或轮询处理中,读取SPSTAT后,应检查WCOL位。
  3. 如果WCOL为1,说明发生了冲突。软件需要向WCOL位写1来清除该标志。然后,根据应用逻辑决定是重发丢失的数据,还是忽略这次错误。
// 安全的从机数据更新函数(在中断中使用) void SPI_SlaveUpdateTxData(unsigned char new_data) { if (SPSTAT & 0x40) { // 先检查是否有未处理的写冲突 SPSTAT = 0x40; // 清除WCOL标志 } // 确保当前没有正在进行的传输(SPIF为0),或者等待其完成 // 对于从机,在SPI中断中,当SPIF置位表示一次传输结束,此时是更新数据的安全窗口 SPDAT = new_data; }

5.2 从机模式下的SS引脚行为与CPHA=0的陷阱

手册第13.2节特别强调了一个重要限制:当CPHA = 0时,SSIG必须为0(即必须使用SS引脚功能),并且SS引脚必须在连续的字节传输之间被取消选中(拉高)再重新选中(拉低)

原因分析: 在CPHA=0模式下,数据采样的时刻(第一个时钟边沿)与SS引脚的激活密切相关。如果SS在两个字节之间保持低电平,从机硬件可能无法正确区分字节边界,导致数据错位。因此,主机在发送多字节数据时,如果从机配置为CPHA=0,必须对每个字节都执行“拉低SS -> 发送字节 -> 拉高SS”的操作,或者至少在每个字节发送间隙短暂拉高SS。这对于一些需要连续传输的命令-数据序列的外设(如Flash的读操作)来说,会降低效率。

解决方案

  1. 改用CPHA=1模式:如果外设支持,优先使用CPHA=1。在此模式下,SS引脚可以在整个多字节传输期间保持低电平,效率更高。
  2. 严格遵守字节间SS切换:如果外设只支持CPHA=0,则主机软件必须严格管理SS信号,确保字节间有足够的SS高电平时间。

5.3 SPI中断的应用与优化

使能SPI中断(设置ESPI (IEN1.3)EA)可以让CPU在数据收发完成时被及时通知,避免轮询等待,提高系统效率。

中断服务程序(ISR)要点

  1. 进入ISR后,首先读取SPSTAT。这个操作本身不会清除标志,但可以获取状态。
  2. 判断中断源:检查SPIFWCOL位。
  3. 清除标志:通过向SPIF和WCOL位写1来清除它们。这是必须的步骤,否则会连续触发中断。
  4. 处理数据:读取SPDAT获取接收到的数据,写入下一个要发送的数据(如果需要全双工通信)。
  5. 注意从机的写冲突:在从机ISR中,如果需要在SPDAT中放置下一个响应数据,务必确保在SPIF置位后的安全窗口内操作,或做好冲突检测和处理。

一个典型的主机中断驱动发送流程

volatile unsigned char spi_tx_buffer[10]; volatile unsigned char spi_rx_buffer[10]; volatile unsigned char spi_index = 0; volatile bit spi_busy = 0; void SPI_MasterSendArray(unsigned char *data, unsigned char len) { // 等待上一次传输完成 while(spi_busy); // 复制数据到发送缓冲区 for (spi_index=0; spi_index<len; spi_index++) { spi_tx_buffer[spi_index] = data[spi_index]; } spi_index = 0; spi_busy = 1; SPI_SS = 0; // 选中从机 SPSTAT = 0xC0; // 清除标志 SPDAT = spi_tx_buffer[spi_index++]; // 启动第一次传输 } void SPI_ISR(void) interrupt 9 { if (SPSTAT & 0x80) { // SPIF SPSTAT = 0x80; // 清除SPIF spi_rx_buffer[spi_index-1] = SPDAT; // 保存刚接收到的数据 if (spi_index < sizeof(spi_tx_buffer)) { // 发送下一个字节 SPDAT = spi_tx_buffer[spi_index++]; } else { // 所有字节发送完毕 SPI_SS = 1; // 释放从机 spi_busy = 0; // 标记传输结束 } } if (SPSTAT & 0x40) { // WCOL SPSTAT = 0x40; // 清除WCOL,这里可以添加错误处理 } }

5.4 低功耗模式下的SPI行为

当P89LPC93x进入空闲(Idle)模式时,CPU停止执行指令,但外设(包括SPI)的时钟通常还在运行(取决于具体配置)。这意味着,如果SPI被配置为从机,它仍然可以响应主机发起的通信,并在数据接收完成后产生SPI中断,从而将CPU从空闲模式唤醒。这是一个非常有用的特性,可以用于构建低功耗的传感器节点。

然而,在掉电(Power-down)模式下,主振荡器停止,SPI模块由于失去时钟源而完全停止工作,无法进行通信或产生中断。如果系统需要在深度睡眠下仍能通过SPI被唤醒,则需要考虑使用外部中断或其他有独立时钟源的外设(如看门狗定时器或比较器)作为唤醒源。

6. 实战案例:驱动SPI Flash存储器

我们以一个具体的例子——驱动W25Q16(2MB SPI Flash)——来串联前面所有的知识点。这款Flash支持标准SPI模式0和模式3。

硬件连接

  • MCU MOSI (P2.2) -> Flash DI (数据输入)
  • MCU MISO (P2.3) <- Flash DO (数据输出)
  • MCU SPICLK (P2.5) -> Flash CLK
  • MCU P0.0 (GPIO) -> Flash CS (片选,低有效)
  • Flash WP# 和 HOLD# 接高电平。

软件驱动关键点

  1. 初始化:配置SPI为模式0,主机,速率设为CCLK/16(保证稳定性)。将CS引脚(P0.0)配置为推挽输出并拉高。
  2. 实现基础收发函数:就是前面SPI_MasterTransfer函数,它封装了单字节交换。
  3. 实现Flash专用命令函数:每个Flash操作都以一个命令字节开始。
    #define CMD_READ_ID 0x9F #define CMD_READ_DATA 0x03 #define CMD_WRITE_ENABLE 0x06 unsigned long SPI_FlashReadID(void) { unsigned long id = 0; FLASH_CS = 0; // 选中Flash SPI_MasterTransfer(CMD_READ_ID); id |= (unsigned long)SPI_MasterTransfer(0xFF) << 16; id |= (unsigned long)SPI_MasterTransfer(0xFF) << 8; id |= SPI_MasterTransfer(0xFF); FLASH_CS = 1; // 释放Flash return id; } void SPI_FlashReadData(unsigned long addr, unsigned char *buffer, unsigned int len) { unsigned int i; FLASH_CS = 0; SPI_MasterTransfer(CMD_READ_DATA); SPI_MasterTransfer((addr >> 16) & 0xFF); // 发送24位地址 SPI_MasterTransfer((addr >> 8) & 0xFF); SPI_MasterTransfer(addr & 0xFF); for (i=0; i<len; i++) { buffer[i] = SPI_MasterTransfer(0xFF); // 读数据时,主机发送哑元数据 } FLASH_CS = 1; }
  4. 处理Flash的“忙”状态:Flash在写或擦除操作期间,会置位状态寄存器中的“忙”位。在写入前需要发送WRITE_ENABLE命令,在写入后需要轮询状态寄存器直到“忙”位清除。
    void SPI_FlashWaitBusy(void) { FLASH_CS = 0; SPI_MasterTransfer(CMD_READ_STATUS_REG1); while (SPI_MasterTransfer(0xFF) & 0x01); // 检查BUSY位 FLASH_CS = 1; }

调试心得

  • 第一步永远是读ID:上电后先尝试读取器件ID(0x9F命令),这是验证SPI硬件连接和基本时序是否正确的最快方法。如果读出的ID不对,检查模式(CPOL/CPHA)、时钟极性、片选信号和电源。
  • 注意字节序:SPI Flash通常是MSB在先,与P89LPC93x的默认设置一致。
  • 片选时序:Flash对CS的下降沿和上升沿非常敏感。确保在发送命令字节之前拉低CS,在整个命令序列(包括地址、数据)完成之后再拉高CS。两个命令之间,CS必须有一个短暂的高电平时间(具体看Flash数据手册的tSHSL参数)。
  • 上拉电阻:如果导线较长,在SPI总线上(特别是MISO)加上4.7k-10k的上拉电阻,可以增强抗干扰能力。

通过这个完整的案例,你应该能够将P89LPC93x的SPI模块从寄存器配置到实际应用融会贯通。记住,数据手册是你的第一参考资料,当通信出现问题时,第一件事就是用逻辑分析仪或示波器抓取SPI四根线的实际波形,与数据手册的时序图逐一对齐,这是定位硬件和软件问题最直接有效的方法。