从入门到踩坑,DSP28335 的 McBSP 配置为 SPI 完整记录
我一直觉得 DSP28335 自带的串口资源挺富裕的,直到有次项目里 SPI-A 被 Flash 占了,SPI-B 接了一块屏幕,新到的传感器又非要走 SPI,才意识到多几个接口有多重要。手里那俩 McBSP 一直闲着,干脆把其中一个配置成 SPI 用。把 McBSP 配置为 SPI 这个操作,本质上就是利用 McBSP 灵活的信号框架去模拟 SPI 时序。这篇笔记就把我调通整个过程的配置思路、寄存器设置、代码框架、波形验证方法,还有调式时踩过的坑完整记录下来,给想在 DSP28335 上扩展 SPI 口的朋友做个参照。
1. 为什么非要跟28335的McBSP较劲
1.1 先看看28335的串口家底到底够不够
DSP28335 是 C2000 系列里非常经典的一款控制芯片,集成的串行接口包括 2 个 SPI、3 个 SCI(也就是 UART)、1 个 I2C、2 个 CAN 以及 2 个 McBSP。表面上看 SPI 有俩好像够用,但实际做产品时很容易被打脸。比如我那个项目,SPI-A 接了 W25Q128 存参数和记录,SPI-B 接了 TFT 屏显示,这时来了一个新的姿态传感器,只支持 SPI,我只能对着芯片手册发愁。
当时有几个选择:换一个接口资源更多的芯片、把传感器改成 I2C 版本、或者用 GPIO 模拟 SPI。前两种方案牵扯硬件改动,时间上来不及;GPIO 模拟 SPI 虽然也能用,但发送过程里 CPU 要一直盯着引脚电平,效率太低,中断一多时序就飘。最后我把目光落在了两个空闲的 McBSP 上——它本身就是一个支持多种帧结构的同步串口,完全有条件被配置成 SPI 模式来用。一块 28335 就这么凭空多出来一个甚至两个 SPI 接口,不改板子不换芯片,代价只是花点时间读手册。
1.2 McBSP 被当成 SPI 用,靠的是帧同步机制
McBSP 全称是 Multi-channel Buffered Serial Port,也就是多通道缓冲串口。最初它是给音频、通信这类场景设计的,支持多通道时分复用、A-law/u-law 压扩、多种帧结构和字长配置。28335 数据手册里,McBSP 工作在非音频模式时被称为 SSCI(Synchronous Serial Communications Interface)模式,说白了,这个模式提供的功能就是 SPI 兼容模式。
McBSP 的核心机制是帧同步。每开始传一个字或一帧数据,FSX/FSR 引脚上就会输出一个帧同步脉冲,数据和时钟都按照寄存器配置好的极性和顺序工作。SPI 的协议本质也是“一个时钟、一根数据进、一根数据出”,靠片选信号 CS 区分传输对象。两者在数据搬移层面非常接近,差别主要在控制信号的含义和时序细节上。理解了这一点,把 McBSP 配置成 SPI 就不是玄学了,而是一个引脚映射、极性对应、帧参数设置的问题。
2. 动手前先弄懂McBSP与SPI的映射关系
2.1 引脚怎么变成 MOSI、MISO、SCLK、CS
McBSP-A 有 6 个对外引脚:MCLKXA、MCLKRA(发送/接收时钟),MDXA、MDRA(发送/接收数据),MFSXA、MFSRA(发送/接收帧同步)。把这组引脚映射到 SPI 信号时,常规对应关系是这样的:
| McBSP-A 引脚 | 对应 GPIO | 作为 SPI 使用时 |
|---|---|---|
| MCLKXA | GPIO6 | SCLK,输出时钟 |
| MDXA | GPIO5 | MOSI,主发从收 |
| MDRA | GPIO8 | MISO,主收从发 |
| MFSXA | GPIO4 | 可作 CS,但一般不建议直接用 |
| MCLKRA | GPIO9 | 内部时钟模式下不需要外部连接 |
| MFSRA | GPIO7 | 一般空着或作通用 IO |
McBSP-B 的引脚则是 GPIO10 到 GPIO15,映射规则一样,具体可以去查 28335 数据手册的 GPIO 复用表。这里要先提醒一句,这些引脚上电复位后全部是普通 GPIO 功能,不配置 GPAMUX 寄存器,后面所有波形都出不来。我一开始没跑通,就是栽在这个地方。
实际接线时,SCLK 接从机的时钟输入,MDXA 接从机的 MOSI(主发从收),MDRA 接从机的 MISO(主收从发)。至于 CS,很多人第一反应是拿 MFSXA 当片选,我劝你别急着这么做,原因下面单独说。
2.2 帧同步信号不是片选,这里有一个大坑
SPI 的 CS 是电平信号,传输多个字节期间要求 CS 一直拉低,从机靠 CS 的低电平判断自己处于被选中状态。McBSP 的 FSX 是脉冲信号,每传一个字或一帧就跳一次,宽度由 FWID 决定,而且不会像 CS 那样在整个传输过程中保持低电平。直接拿 MFSXA 去接从机 CS,如果从机要求 CS 在整个命令序列期间持续有效,那多字节传输时 CS 就会反复跳变,从机很容易“掉线”,数据自然就错了。
所以实操里最稳妥的办法是把 McBSP 配成三线 SPI:SCLK、MOSI、MISO 全交给 McBSP,CS 单独用一颗普通 GPIO 控制。这样 CS 的拉高拉低完全由软件决定,想多低就多低,想低多久就低多久,和从机芯片手册的要求一一对应,排查问题也直观。用 GPIO 控制 CS 就是大家常说的软件片选,成本只是一根 IO,换来的是极大的灵活性。如果非要用 MFSXA 当 CS,也不是完全不行,但只适合那些对 CS 电平不敏感、只要检测到下降沿就开始传输的从机,而且一帧只能一个字节,传输多个字节时就很别扭。我的结论很简单:用软件片选。
这里多提一句,网上经常有人争论“SPI 硬件片选 vs 软件片选”。McBSP 的这种用法正好说明,硬件片选(FSX 直接当 CS)虽然少占一根 IO,但时序约束很难适配所有从机;软件片选虽然多了一行控制代码,但整个传输过程的控制权都在自己手里,调试和兼容性都更舒服。
2.3 时钟极性和相位的换算,别被手册绕晕
SPI 里有 CPOL(时钟极性)和 CPHA(时钟相位)两个概念。CPOL 决定空闲时 SCLK 是高电平还是低电平,CPHA 决定数据是在第一个边沿还是第二个边沿采样。McBSP 手册里不讲 CPOL/CPHA,只讲 CLKXP、CLKRP 这两个位。CLKXP 控制发送数据在时钟的哪个沿变化,CLKRP 控制接收数据在哪个沿采样。
我当时满脑子都是“CPOL=0 对应哪个”,后来干脆列了一张对应表,照着抄就行:
| SPI 模式 | CPOL | CPHA | CLKXP(发送变化沿) | CLKRP(接收采样沿) |
|---|---|---|---|---|
| 模式 0 | 0 | 0 | 1,下降沿变化 | 0,上升沿采样 |
| 模式 1 | 0 | 1 | 0,上升沿变化 | 1,下降沿采样 |
| 模式 2 | 1 | 0 | 0,上升沿变化 | 1,下降沿采样 |
| 模式 3 | 1 | 1 | 1,下降沿变化 | 0,上升沿采样 |
以最常用的模式 3 为例,空闲时 SCLK 为高,数据在下降沿变化、上升沿采样,对应 CLKXP=1、CLKRP=0。大多数 SPI Flash、SD 卡、显示屏驱动都支持模式 0 或模式 3,所以这个组合出现频率最高。另外还有一个 FSXP/FSRP 设置,SPI 的片选习惯上是低电平有效,McBSP 的帧同步极性一般也设成 1(低有效),这样配合软件 CS 或者外部逻辑都容易理解。这个表建议收藏,每次配置时对照一眼,能少走很多弯路。
3. 实操:把McBSP配置成SPI主模式的完整步骤
3.1 第一步:打开外设时钟并配置引脚复用
所有外设模块在上电后默认都不一定打开了时钟,McBSP 也一样。28335 里外设时钟使能主要在 PCLKCR0、PCLKCR1 这些寄存器里控制,McBSP-A 和 McBSP-B 分别对应 PCLKCR1 中的使能位。没打开时钟,后面配置寄存器写得再对也没用,读回来的寄存器可能是 0x0000,引脚上也不会有任何动作。
GPIO 复用这一步同样关键。我需要把 GPIO5、GPIO6、GPIO8、GPIO9 从普通 IO 切换成 McBSP 功能,这组引脚由 GPAMUX1 寄存器控制。代码结构大概是这样:
void McBSPA_SPI_Init(void) { // 1. 打开 McBSP-A 外设时钟 SysCtrlRegs.PCLKCR1.bit.MCBSPA = 1; // 2. GPIO 复用配置 EALLOW; GpioCtrlRegs.GPAMUX1.bit.GPIO5 = 1; // MDXA -> MOSI GpioCtrlRegs.GPAMUX1.bit.GPIO6 = 1; // MCLKXA -> SCLK GpioCtrlRegs.GPAMUX1.bit.GPIO8 = 1; // MDRA -> MISO GpioCtrlRegs.GPAMUX1.bit.GPIO9 = 1; // MCLKRA(内部时钟) GpioCtrlRegs.GPAMUX1.bit.GPIO7 = 0; // GPIO7 保持普通 IO GpioCtrlRegs.GPADIR.bit.GPIO7 = 1; // GPIO7 作为软件 CS 输出 GpioDataRegs.GPASET.bit.GPIO7 = 1; // CS 默认为高,不选中从机 EDIS; }这里我把 GPIO7 留给了软件片选,没有把它设置成 MFSRA 功能。这样一整根 CS 的控制权完全在手里,后面想怎么拉就怎么拉。GPIO4(MFSXA)我没有使用,因为在软件片选的方案里它派不上用场。如果你手里的 GPIO 资源比较紧张,也可以把软件片选安排到其他空余引脚上,规则是一样的。
3.2 第二步:先复位再配置,顺序别乱
McBSP 初始化有个习惯和很多外设不太一样,就是在配置寄存器之前,最好先把发送器、接收器、采样率发生器、帧同步发生器全部置于复位状态。这样做的好处是,复位状态下写寄存器不会引发意外动作,配置完成后再按顺序解除复位,外设就能按新配置开始工作。
推荐的操作顺序是先解除采样率发生器复位,让内部时钟先跑起来;再解除帧同步发生器复位,让帧同步逻辑就绪;最后解除发送器和接收器复位。这个顺序并不是唯一的,网上很多例程也有先使能收发再使能帧同步的,实测也能工作。我习惯用先时钟、再帧同步、最后收发的顺序,逻辑上更清楚,也不容易踩到“还没配好就开始输出波形”的坑。
我的完整初始化代码里,把 PCR(引脚控制)、SRGR(采样率与帧参数)、XCR/RCR(发送/接收配置)、SPCR(串口控制)都写清楚了。以常见的 SPI 模式 3、8 位数据、主模式为例,关键配置如下:
// 3. 配置前全部复位 McbspaRegs.SPCR1.bit.RRST = 0; McbspaRegs.SPCR2.bit.XRST = 0; McbspaRegs.SPCR2.bit.GRST = 0; McbspaRegs.SPCR2.bit.FRST = 0; // 4. PCR:主模式、极性、时钟方向 McbspaRegs.PCR.bit.CLKRP = 0; // 接收在上升沿采样 McbspaRegs.PCR.bit.CLKXP = 1; // 发送在下降沿变化 McbspaRegs.PCR.bit.FSRP = 1; // 帧同步低有效 McbspaRegs.PCR.bit.FSXP = 1; // 帧同步低有效 McbspaRegs.PCR.bit.CLKRM = 1; // 接收时钟内部产生 McbspaRegs.PCR.bit.CLKXM = 1; // 发送时钟内部产生,SCLK 输出 McbspaRegs.PCR.bit.FSRM = 1; // 接收帧同步内部产生 McbspaRegs.PCR.bit.FSXM = 1; // 发送帧同步内部产生 // 5. SRGR:采样率分频与帧参数 McbspaRegs.SRGR1.bit.CLKGDV = 14; // 见下方分频计算 McbspaRegs.SRGR1.bit.FWID = 1; // 帧同步脉冲宽度 McbspaRegs.SRGR2.bit.CLKSM = 0; // 时钟源 = LSPCLK McbspaRegs.SRGR2.bit.FSGM = 1; // 每帧都产生帧同步 McbspaRegs.SRGR2.bit.FPER = 15; // 帧周期,16 个 CLKG 周期 // 6. XCR/RCR:8 位字长、每帧 1 个字、无数据延迟 McbspaRegs.XCR1.bit.XFRLEN = 0; McbspaRegs.XCR1.bit.XWDLEN = 0; McbspaRegs.XCR1.bit.XDATDLY = 0; McbspaRegs.RCR1.bit.RFRLEN = 0; McbspaRegs.RCR1.bit.RWDLEN = 0; McbspaRegs.RCR1.bit.RDATDLY = 0; // 7. 解除复位,按顺序使能 McbspaRegs.SPCR2.bit.GRST = 1; // 采样率发生器先跑起来 McbspaRegs.SPCR2.bit.FRST = 1; // 帧同步发生器就绪 McbspaRegs.SPCR1.bit.RRST = 1; // 接收器退出复位 McbspaRegs.SPCR2.bit.XRST = 1; // 发送器退出复位MXBSP 的 PCR 寄存器里 CLKXP 和 CLKRP 这两个极性位,是配置 SPI 模式时要认真核对的。前面那张对应表在这里就能用上了。我把它们配成了模式 3 的组合,如果你接的从机要求模式 0、模式 1 或者模式 2,只需要交换或者调整这两个位,其他配置不用大动。
3.3 第三步:时钟分频到底怎么算
28335 的 McBSP 采样率发生器可以选外部时钟,也可以选内部 LSPCLK。LSPCLK 在系统主频 150MHz 时默认是 37.5MHz,是主频的四分频。CLKGDV 是一个 8 位分频器,输出时钟 CLKG 的频率按这个公式计算:
CLKG = LSPCLK / (CLKGDV + 1)
我之前在代码里把 CLKGDV 设为 14,那么 CLKG = 37.5MHz / 15 = 2.5MHz。这个时钟就是 SCLK 的输出频率。对于常见的 SPI Flash、传感器、显示屏,2.5MHz 是一个非常保守又稳定的速度。如果觉得慢,可以往下调 CLKGDV;如果从机线比较长或者没加阻抗匹配,建议先跑低速验证功能,确认没问题了再往高速走。
有个细节要注意,FPER 决定帧周期,它是以 CLKG 为单位的。8 位数据在移位时需要至少 8 个 CLKG 周期,所以 FPER 不能设得太小,不然帧同步脉冲和数据会打架。代码里 FPER=15 表示帧周期是 16 个 CLKG,比 8 位数据多出不少余量,实际操作很稳。如果把数据长度改成 16 位,FPER 要相应调大,一般来说 FPER >= 字长 + FWID + 2 就是安全的。
3.4 第四步:发送和接收的软件流程
McBSP 的发送流程是:先查 SPCR2 里的 XRDY 位,等它变成 1,说明发送寄存器可以写入;然后把数据写到 DXR1,McBSP 硬件会自动把数据搬到移位寄存器,在 SCLK 的驱动下一位一位发出去。接收流程类似:查 SPCR1 里的 RRDY 位,变成 1 时说明已经收到了一个完整的字,读 DRR1 就能拿到数据。
一个典型的单字节传输函数可以这样写:
Uint16 McBSPA_SPI_Transfer(Uint16 data) { while (McbspaRegs.SPCR2.bit.XRDY == 0) { } McbspaRegs.DXR1 = data; while (McbspaRegs.SPCR1.bit.RRDY == 0) { } return McbspaRegs.DRR1; }这个函数一次完成发送一个字节并接收一个字节,充分用上了 SPI 全双工的特性。如果只是发送不需要接收,不要忘了,发送一个字节之后仍然会有一个字节被收进 DRR1,必须及时读走,不然后面再传数据时接收标志位一直挂着,会产生干扰。最简单的方法就是像我上面那样,每次 Transfer 都把接收数据读出来,哪怕最后不用。
举个实际例子,用这个函数去读 W25Q32 这类 SPI Flash 的 JEDEC ID,命令流程是这样的:拉低 CS,发送 0x9F,然后连续读三个字节,再拉高 CS。对应代码:
Uint8 id[3]; GpioDataRegs.GPACLEAR.bit.GPIO7 = 1; // CS 拉低,选中从机 McBSPA_SPI_Transfer(0x9F); // 发送 JEDEC ID 命令 id[0] = (Uint8)McBSPA_SPI_Transfer(0x00); // 接收第 1 个字节 id[1] = (Uint8)McBSPA_SPI_Transfer(0x00); // 接收第 2 个字节 id[2] = (Uint8)McBSPA_SPI_Transfer(0x00); // 接收第 3 个字节 GpioDataRegs.GPASET.bit.GPIO7 = 1; // CS 拉高,释放从机读回来的 id[0] 就是厂商 ID,比如 0xEF 对应 Winbond;id[1] 是存储器类型,id[2] 是容量编号。如果这一串能读对,说明 McBSP 作为 SPI 主模式的配置已经基本通了。这个流程最大的价值在于,它把 CS 控制、命令发送、连续接收这几个 SPI 核心操作全部走了一遍,每一个环节的时序都能在下一章提到的调试工具里看得清清楚楚。
4. 用示波器验证波形,问题定位才能快
4.1 示波器和逻辑分析仪怎么接
代码写完不代表配置正确,波形一定要亲自看一眼。调 McBSP 当 SPI 时,我强烈建议把手头的逻辑分析仪或者示波器接上,别偷懒。一个最简单的验证方法就是把 MDXA 和 MDRA 在外面短接,也就是自发自收。配置好之后往 DXR1 写一个字节,如果 DRR1 能读回相同的数据,说明收发链路是通的。这个方法不需要接任何从机设备,CPU 自己就完成了回环测试,排查问题非常高效。
接示波器时,最少抓三个信号:SCLK(也就是 MCLKXA)、MOSI(MDXA)、CS(软件片选 GPIO7)。触发方式建议设成 CS 下降沿触发,因为每次 CS 下拉代表一次完整传输的开始。正常传输时,你期望看到的是这样的波形:CS 由高变低,随后 SCLK 开始输出连续脉冲,MOSI 在 SCLK 的下降沿改变数据、上升沿保持稳定,传输结束后 CS 拉高。如果抓到的波形里 SCLK 没有脉冲,回去查时钟使能和 GPIO 复用;如果 SCLK 有脉冲但 MOSI 一直是同一个电平,回去查发送寄存器有没有真正被写入。
4.2 常见问题排查速查表
调试过程中我整理了一张问题排查表,基本上覆盖了 McBSP 配置为 SPI 之后可能会遇到的大部分问题:
| 现象 | 可能原因 | 处理方法 |
|---|---|---|
| SCLK 引脚没有波形 | 外设时钟没使能、GPIO 复用没配置 | 检查 PCLKCR1 和 GPAMUX1 |
| 有 SCLK 但没有 MOSI 数据 | DXR 写入时机不对、发送未退出复位 | 确认 XRDY 等待逻辑,XRST=1 |
| 数据错位或乱码 | CLKXP/CLKRP 不匹配从机要求 | 按对应表调整极性 |
| 读回数据全是 FF 或全 0 | MOSI/MISO 接反、从机没供电 | 检查硬件连接和从机上电时序 |
| 多字节传输时数据中间断开 | CS 闪断,FSX 直接当 CS 导致 | 换成 GPIO 软件片选 |
| 偶发丢字节 | SCLK 过高、从机时序余量不足 | 降低 CLKG 频率,让时序更宽松 |
| 时钟频率和预期差距很大 | CLKGDV 或 LSPCLK 计算错误 | 按公式 CLKG = LSPCLK / (CLKGDV+1) 重新计算 |
有一个特别容易被忽视的问题:如果配置了中断,比如每次发送完成触发一次中断,但中断服务程序里没有及时读走 DRR1,接收标志位 RRDY 会一直保持置位,后面再想接收数据就发现状态永远“就绪”,读出来的却是上一个残留字节。这种问题用回环测试很容易暴露,读回数据和发送数据对不上时,先看看是不是有这个隐患。
5. 实际调试中让我印象最深的几个坑
5.1 翻车现场:GPIO 复用忘配了
第一次调的时候,我把所有寄存器都按手册配完了,示波器一抓,SCLK 引脚上干干净净,什么都没有。当时第一反应是时钟配置有问题,翻来覆去改了 CLKGDV、查了 PCLKCR1,还是没有波形。折腾了快一个小时才反应过来,GPIO 复用寄存器 GPAMUX1 根本没有设置,GPIO6 还处于普通 IO 模式,McBSP 的时钟信号根本到不了引脚上。这个问题现在说起来很傻,但在刚开始接触 28335 时确实很容易漏。所以我每次写初始化代码,都会先确认外设时钟、再确认 GPIO 复用、最后才去配置外设寄存器本身,顺序固定下来之后就没再犯过这种错。
5.2 翻车现场:CS 直接被 FSX 搞崩了
第二次踩坑是在接一颗 SPI 接口的传感器时,为了省一根 IO,我直接把 MFSXA 接到了从机 CS 上,想着 McBSP 每发一帧数据不是会自动产生帧同步脉冲吗,这脉冲总该拉一下 CS。结果传感器返回的数据经常不对,用逻辑分析仪一看,多字节传输时 CS 在每次字节之间都会跳回高电平,从机在发送第三个字节时已经彻底“晕”了。后来我乖乖改成 GPIO 软件片选,问题马上消失。从那以后我彻底记住了:McBSP 的帧同步是“脉冲”,SPI 的片选是“电平”,两者不能随便画等号。如果你的应用必须省那根 IO,至少要确认从机对 CS 的持续电平没有严格依赖。
5.3 一些实际建议
现在每次配置完 McBSP,我都会先抓波形确认极性没问题,再让后续的驱动逻辑跑起来。这个习惯帮我省了很多事。另外一个建议是,调试初期先把 SCLK 频率降到一个非常慢的速度,比如 100kHz 左右,等通信稳定了再慢慢提到目标频率。这样做既排除了信号完整性的干扰,也方便逻辑分析仪完整地把每个字节都抓下来分析。等代码逻辑成熟了,再把分频往上调,实测从 2.5MHz 提到 5MHz 甚至 10MHz 往往也能正常工作,但前提一定是先把低速状态下的时序看明白了。McBSP 配置为 SPI 只是它诸多玩法里的一个基础应用,把帧同步、时钟极性、数据延迟这几个概念吃透之后,再去折腾多通道模式、自定义帧结构、外接音频编解码器这些高级功能,思路会顺很多。