恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
GD32F30x SPI单线双向模式详解:原理、代码与踩坑记录
首页
资讯中心
/
GD32F30x SPI单线双向模式详解:原理、代码与踩坑记录
GD32F30x SPI单线双向模式详解:原理、代码与踩坑记录
发布时间:2026/9/7 7:44:09
简介GD32F30x SPI 单线双向模式例程包面向嵌入式开发者和 GD32 单片机学习者聚焦如何在一根数据线上完成双向通信解决引脚资源受限场景下的硬件设计难题。压缩包仅 303KB却包含 132 个文件覆盖 61 个头文件、50 个 C 源文件以及启动文件、hex/bin 固件、uvproj 工程等其中 .c/.h 为驱动源码hex/bin 为可直接烧录的固件uvproj/sct 为 Keil 工程与链接脚本便于分层查阅下载后可直接编译验证 SPI 单线双向收发流程。例程内容围绕 SPI 时钟分频、模式寄存器配置、数据线切换时序、片选管理和中断/DMA 传输展开并配有 LCD、LED 等外设联动示例便于从综合工程角度理解整个系统的协作方式尤其展示了中断与 DMA 两种高效收发路径可帮助评估不同实现方式对 CPU 占用与实时性的影响。已有 1308 人浏览/学习适合需要深入掌握 GD32F30x 特殊 SPI 工作模式、或将其移植到自研项目中的开发者参考初学者也可按引脚连接和代码注释逐步上手。 前阵子别人传了我一份名为“GD32F30x SPI 单线双向模式”的例程包解压前我还在想SPI不是最少四根线嘛怎么还能玩出单线来解压跑了一遍才意识到这里说的“单线”不是把SCK省了而是把MOSI和MISO合并成一根双向数据线也就是常说的三线SPI或BIDI模式。这个模式在MCU引脚紧张的时候特别好用尤其是做传感器采集、短距离命令响应这类半双工通信的场景。如果你手头正好在用GD32F30x又想把SPI省出一根引脚来这篇文章应该能帮你少走不少弯路。我先把结论放在前面GD32F30x的SPI外设是支持单线双向模式的核心就两个控制位一个是使能位BDOEN一个是方向位BDOE完全靠软件切方向。听起来简单但实际操作里方向切换的时机、引脚配置、从机协议配合个个都是坑。下面我把原理、代码、实测和踩坑记录一起整理出来尽量讲透。1. 单线双向模式到底是怎么回事1.1 从四线变三线MOSI和MISO合并标准的SPI通信是四线SCK提供时钟CS选从机MOSI主机输出从机输入MISO从机输出主机输入。四线制的好处是数据上行下行互不干扰全双工跑的飞起。但代价是占引脚尤其是小封装芯片一个SPI外设吃掉四根脚再加上电源、晶振、调试口留给其他功能的空间就不多了。单线双向模式做的就是一件事把MOSI和MISO合并成一根线。这根线在同一时刻只能有一个方向的数据流动要么主机发从机收要么从机发主机收所以它只能工作在半双工。这样原本四线的SPI变成了三线SCK、CS、DATA。省下来的那根引脚可以直接释放为普通GPIO或者干脆不接。这种模式跟I2C有点类似都是半双工、数据线和时钟分开。但SPI单线双向模式跟I2C最大的区别是它的时钟仍然由主机硬件产生不需要像I2C那样靠协议里的ACK机制去同步时序要简单直接得多。1.2 适用的场景和必须接受的限制什么样的场景适合用单线双向模式我实际用下来觉得主要有三类第一类是引脚极度紧张的小封装项目。比如LQFP48或者更小的封装一个SPI外设占四根脚确实心疼省一根是一根。第二类是只支持单线协议的从机设备。有些传感器芯片、单线存储器本身就只引出一根数据脚那主机配备单线双向模式是必须的否则你就只能IO口软件模拟。第三类是通信数据量不大、但需要SPI速率的外设。比如给蓝牙模组发AT指令、读传感器寄存器、配置逻辑芯片这类应用通常一条命令加几个字节的回应就完了半双工完全够用。但有一个限制必须提前说清楚单线双向模式不能支持全双工传输。如果从机需要一边收一边发比如音频编解码数据流或者高速率大吞吐的数据传输这个模式就不合适老老实实用标准四线。另外这个模式下主机和从机之间的方向切换完全靠软件互相配合。主机切方向很简单改一个寄存器位就行但从机那边什么时候把数据放上总线什么时候把总线让出来需要双方协议约定清楚。所以这个模式更适合自己掌握从机协议的情况如果要连一个不是为单线模式设计的通用SPI设备基本没戏。2. GD32F30x SPI外设的关键位与硬件设计2.1 找对两个控制位BDOEN和BDOEGD32F30x的SPI外设控制寄存器和STM32F10x系列非常像核心寄存器叫SPI_CTL0对应ST的SPI_CR1。单线双向模式的控制位就在这个寄存器的高位第14位BDOEN双向模式使能位。写1进入单线双向模式写0恢复标准四线模式。第15位BDOE双向模式下数据线方向位。写1时数据线作为输出写0时作为输入。当BDOEN为1时硬件会直接把MISO引脚从SPI逻辑中断开原本的MOSI引脚变成唯一的双向数据脚。所以在这个模式下不需要再给MISO做任何SPI配置这根引脚可以释放出来做其他事。方向位BDOE非常关键它必须由软件在合适的时候切换。主机准备发数据时把BDOE置1发完要听从机回话时把BDOE清0。这个切换必须在SCK的间隙完成否则就会把数据线上的电平状态搞乱。对应的代码操作也很直接翻GD32F30x标准外设库时你可能会遇到库函数封装也可以用寄存器直接操作我个人更推荐寄存器操作因为逻辑非常清楚/* 发送方向 */ SPI0-CTL0 | SPI_CTL0_BDOEN; /* 使能单线双向模式 */ SPI0-CTL0 | SPI_CTL0_BDOE; /* 数据线方向输出 */ /* 接收方向 */ SPI0-CTL0 | SPI_CTL0_BDOEN; /* 保持单线双向模式 */ SPI0-CTL0 ~SPI_CTL0_BDOE; /* 数据线方向输入 */如果你的固件库版本比较老可能没有SPI_CTL0_BDOEN这种宏没关系直接按位操作也可以Bit14和Bit15的位置是固定的。2.2 引脚分配、上下拉与板级设计GD32F30x的SPI0默认引脚是PA5的SCK、PA6的MISO、PA7的MOSI。进入单线双向模式后PA7变成双向数据线PA6被释放。这个时候硬件设计上有两个细节要注意。第一PA6被释放以后它默认是浮空输入状态。如果板上已经把它连到了其他外设记得在初始化里重新配置成对应的复用功能如果没用到就让它浮空或者配成输入上拉避免悬空引入噪声和额外功耗。很多人在这个点上吃过亏以为是SPI没配置好实际上是PA6异常电平干扰了系统。第二双向数据线PA7建议配置成复用开漏并且外部加上拉电阻。为什么不用复用推挽因为单线模式下这根线既要输出又要输入用推挽配置在方向切换的瞬间可能出现主从两边同时驱动总线的冲突。开漏加外部上拉的话任意一方释放总线时电平都被上拉电阻拉高不会出现硬碰硬的驱动冲突。如果板子上实在没法加上拉电阻至少用内部上拉顶一下虽然驱动能力弱一些但在低速下也能工作。SCK引脚可以保持复用推挽配置这个没有方向切换问题驱动能力越强越好。如果用的是高速SPI建议SCK和数据线上都不要串过大阻值的电阻保持信号边沿干净。3. 例程代码拆解初始化、发送和接收3.1 初始化函数怎么写初始化部分和标准SPI差不多先开时钟再配GPIO然后配置SPI外设参数。区别在于GPIO配置和最后的模式切换。我用GD32F30x标准外设库的接口风格写了一个参考具体API名称以你手上的库版本为准void spi_bidi_init(void) { /* 使能时钟 */ rcu_periph_clock_enable(RCU_GPIOA); rcu_periph_clock_enable(RCU_SPI0); /* PA5-SCK推挽复用PA7-DATA开漏复用 */ gpio_init(GPIOA, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_5); gpio_init(GPIOA, GPIO_MODE_AF_OD, GPIO_OSPEED_50MHZ, GPIO_PIN_7); /* SPI0主机模式8bit软件NSS16分频MSB先行 */ spi_init(SPI0, SPI_MODE_MASTER, SPI_TRANSMODE_FULLDUPLEX, SPI_FRAMESIZE_8BIT, SPI_NSS_SOFT, SPI_PSC_16, SPI_FIRSTBIT_MSB); /* 进入单线双向模式先设为发送方向 */ SPI0-CTL0 | SPI_CTL0_BDOEN | SPI_CTL0_BDOE; /* 使能SPI0 */ spi_enable(SPI0); }这里有一个特别注意的地方spi_init那个函数我传的是SPI_TRANSMODE_FULLDUPLEX也就是标准全双工模式因为初始化时SPI外设本身还没进入BIDI模式。等基础参数配置完再用寄存器操作把BDOEN打开。如果你用的固件库里spi_init支持直接传BIDI模式参数那就更省事但寄存器手动打开这步逻辑是最通用的建议理解透彻。3.2 方向切换与收发函数实现接下来这组代码是核心。发送和接收都要先切方向再操作数据寄存器。GD32F30x的SPI数据寄存器只有16bit低8位是有效数据位8bit帧格式下。/* 发送一个字节 */ void spi_bidi_send_byte(uint8_t data) { /* 切到发送方向 */ SPI0-CTL0 | SPI_CTL0_BDOE; /* 等待发送缓冲区空 */ while (RESET spi_i2s_flag_get(SPI0, SPI_FLAG_TBE)); /* 写数据 */ SPI0-DATA data; /* 等待发送完成 */ while (SET spi_i2s_flag_get(SPI0, SPI_FLAG_BSY)); }接收一个字节就没那么简单了因为SPI本身是主设备提供时钟从机没办法主动发送数据。就算切到了接收方向如果主机不输出时钟从机也发不出数据。所以主机接收时要先写一个哑字节来产生SCK时钟。/* 接收一个字节需要主机先产生时钟 */ uint8_t spi_bidi_recv_byte(void) { /* 切到接收方向 */ SPI0-CTL0 ~SPI_CTL0_BDOE; /* 等待发送缓冲区空 */ while (RESET spi_i2s_flag_get(SPI0, SPI_FLAG_TBE)); /* 写哑字节产生时钟数据线处于输入状态实际不输出数据 */ SPI0-DATA 0x00; /* 等待接收缓冲区非空 */ while (RESET spi_i2s_flag_get(SPI0, SPI_FLAG_RBNE)); /* 读数据 */ return (uint8_t)SPI0-DATA; }这里有一个很多人一开始搞不懂的点为什么切到接收方向后还要写DATA寄存器因为在SPI主机模式下写DATA寄存器是产生SCK时钟的前提即使输出被禁止时钟照常产生。从机就是靠着这些SCK时钟把数据一位一位送到数据线上的。如果你不写哑字节整个通信就会卡住从机干等着时钟主机干等着数据。3.3 典型交互流程发命令再收数据实际项目里最常见的流程是主机先发一个命令字节然后切方向再读回数据。这里以读一个假设的从机ID为例uint8_t spi_bidi_read_id(void) { uint8_t id 0; /* 发命令 */ spi_bidi_send_byte(0x90); /* 建议使用从机协议里的读ID命令 */ /* 切换方向读取一个字节 */ SPI0-CTL0 ~SPI_CTL0_BDOE; SPI0-DATA 0x00; while (RESET spi_i2s_flag_get(SPI0, SPI_FLAG_RBNE)); id (uint8_t)SPI0-DATA; /* 用完之后切回发送方向方便下一次通信 */ SPI0-CTL0 | SPI_CTL0_BDOE; return id; }如果从机一次返回多个字节比如读一个64字节的缓冲区可以保持接收方向不变连续写哑字节连续读不需要反复切方向。只有从主机发送阶段转到接收阶段时才需要那一次方向切换。3.4 速度、延时和DMA的取舍单线双向模式的速率上限和标准SPI是一样的GD32F30x的SPI0挂在APB2上SPI1和SPI2挂在APB1上分频后实际能跑多快建议查官方手册确认我一般不会在BIDI模式下跑满速原因是方向切换需要时间如果SCK太快方向切换的瞬间很可能把总线状态搞乱出现数据错位。延时是另一个要注意的点。方向切换之后给从机一点准备时间通常没有坏处尤其是在命令阶段和回复阶段交界处加个几百纳秒到几微秒的延时能大幅降低通信失败的几率。逻辑分析仪上看得最清楚如果切方向后立刻来时钟有些反应慢的从机根本跟不住。DMA在这个模式下也能用但方向切换时序得自己控制好。DMA负责纯粹的数据搬运方向切换还是得靠CPU在DMA启动前和结束后操作BDOE位。如果DMA忙于处理大块数据CPU在中间插一脚切方向时序会非常紧张所以我个人建议数据量大还是用标准四线SPI加DMA单线双向模式更适合少量交互式通信。4. 实测验证与调试记录4.1 用逻辑分析仪验证时序对不对代码写完后第一步别急着接从机先用逻辑分析仪抓主机这边的SCK和DATA。接法是SCK接PA5DATA接PA7然后跑一个简单的读写命令。我用的采样率是24MHz对几百kHz的SPI时钟来说足够看清细节。抓下来的波形会有三个明显的阶段第一个阶段主机发命令数据线上有稳定数据SCK正常输出第二个阶段SCK继续有空闲跳变但数据线变成高阻输入状态如果板上没有设备驱动它它会被上拉电阻拉高第三个阶段从机开始响应数据线上出现从机输出电平SCK同步输出。判断方向切换是否成功就看重叠阶段数据线上有没有出现波形畸变。如果数据线上出现一段又拉高又拉低的混乱电平大概率是BDOE切换时机不对或者主从双方在切换瞬间同时在驱动总线。还有一个常见现象是命令发出去了但后面收回来全0xFF这通常是收方向下没有写哑字节产生SCK或者从机压根没收到命令。4.2 实测中得到的数据和结论我拿一个支持单线模式的传感器做了测试SCK配置在2MHz因为这种小传感器本身支持不了太高的速率。用上面的代码跑读ID寄存器数据完全是稳定复现的。把速率提到8MHz第一个字节开始偶尔出错分析下来不是SPI外设的问题而是传感器本身响应时间跟不上方向切换的间隔。这再次验证了一个观点单线双向模式的瓶颈往往在从机的响应速度上而不是主机SPI外设本身。换成全双工四线模式同样这个传感器在8MHz下没有任何问题因为MOSI和MISO物理隔离从机不需要在“输入命令”和“输出响应”之间做引脚方向切换。所以在选型阶段最好先确认一下从机是否原生支持单线通信如果从机只是普通SPI设备你却想用单线双向模式那大概率要自己在从机端用外部电路或者IO口电平转换处理复杂度远高于直接多加一根线。5. 常见问题排查速查表我在这个模式上踩过的坑、以及周边朋友问过的问题整理在这张表里希望能帮你省点时间。问题现象可能的根因解决办法主机发出的命令从机收不到BDOEN没打开SPI仍处于标准四线模式数据只在MOSI线上发出确认SPI_CTL0的BDOEN位为1从机有响应但主机收到全是0xFF接收方向下没有写哑字节SCK没有产生切换BDOE为0后往DATA写任意值产生时钟收数据总差一个字节或者错位方向切换后没有留间隙从机来不及输出方向切换后加延时或者降低SCK频率数据线上波形畸变、电平冲突主从端同时在驱动数据线检查从机是否按协议让出总线检查BDOE切换时序推荐开漏加上拉连上从机后整个SPI通信都卡死PA6/MISO没有被正确释放或PA7配置成推挽导致驱动冲突PA6重新配置为GPIO或让其浮空PA7改开漏加上拉SPI使能后SCK没波形软件NSS下SSI配置不正确确保NSS配置为软件模式且SSI位置1还有一个独家心得排查这类问题时最好先把从机断开只用逻辑分析仪看主机波形。这样能快速判断是主机侧配置问题还是从机侧响应问题。很多同学一上来就把主从连在一起出了问题根本分不清是哪边的毛病。最后分享一个小技巧。如果你手头没有支持单线双向模式的从机但偏偏想验证一下主机这个模式是否工作正常可以用一根杜邦线把主机自身的MOSIPA7和MISOPA6短接配合一个简单的回环测试进入BIDI模式后MISO被SPI逻辑释放回环主要验证的是初始化代码和收发时序不能完全模拟真实从机响应。真正要验证方向切换还是得接一个单线设备或者用两块GD32开发板互相连一块配主机BIDI模式一块配从机BIDI模式主从对打这才是最接近实战的测试方法。本文还有配套的精品资源点击获取