草庐IT

【GD32】从0开始学GD32单片机(9)—— SPI外设详解+主机从机发送和接收例程

马浩同学 2023-09-05 原文

目录

简介

SPI是串行外设接口(Serial Peripheral Interface)的缩写,是一种高速的,全双工,同步的通信总线。
SPI总共需要4根线来实现通信,NSS:片选线,用于选择需要通信的从机;CLK:同步时钟线,用于提供同步时钟信号;MISO:主机读从机写线;MOSI:主机写从机读线
GD32F103系列的SPI最高速度为18MHz

片选线

SPI的片选逻辑要比I2C的简单得多,通常一个SPI外设会有多条片选线,如下图。


所以我们想要与哪个从机进行通信,那么只需要拉低对应从机的片选线即可,无需像I2C那样要通过传送从机地址来片选对应的从机。

不过GD32的SPI外设好像只有一条片选线,目前我还没有见过有多条片选线的SPI外设,可能高级一点的型号会有吧

时序

因为SPI为同步通信协议,因此通信要配合同步时钟信号,时序如下图。


值得注意的是时钟线有4种不同的时序,通过CKPH和CKPL两个配置位进行配置,CKPH配置的是第一或第二个时钟边沿为有效采样边沿,CKPL配置的是空闲状态时时钟线的电平

运行模式

SPI外设有好几种运行模式,分别有——全双工主机模式、单向线连接主机发送模式、单向线连接主机接收模式、双向线连接主机发送模式、双向线连接主机接收模式、全双工从机模式、单向线连接从机发送模式、单向线连接从机接收模式、双向线连接从机发送模式、双向线连接从机接收模式
其实简单点说就是,虽然SPI是一个全双工的通信协议,但通过配置可以使它工作在全双工、半双工和单工的状态,配置十分灵活。
全双工的接线示意图

半双工接线示意图

单工接线示意图

基本发送和技术流程

主机发送

简要流程:

  1. 配置SPI外设的时序,使能SPI外设
  2. 拉低对应从机NSS线电平
  3. 软件写一个数据到发送缓冲区
  4. 数据帧从数据缓冲区加载到移位寄存器中,开始发送加载的数据
  5. 数据帧的第一位发送之后,TBE位置1
  6. 如果有更多数据则重复第3-5步
  7. 等待TRANS置0,关闭时钟信号输出,释放片选线,数据传输结束

说明:
TBE全称Transmit Buffer Empty,“发送缓冲区空”标志位。
TRANS,“通信进行中”标志位。

主机接收

简要流程:

  1. 配置SPI外设的时序,使能SPI外设
  2. 拉低对应从机NSS线电平
  3. 输出SCK时钟信号
  4. 接收到的数据将从移位寄存器存入到接收缓冲区,且RBNE位置1
  5. 软件读取接收缓冲区,RBNE位置0
  6. 如果有更多数据则重复第4-5步
  7. 等待TRANS置0,关闭时钟信号输出,释放片选线,数据传输结束

说明:
RBNE全称Read Buffer Not Empty,"接收缓冲区非空"标志位。

从机发送

简要流程:

  1. 配置SPI外设的时序,使能SPI外设
  2. 软件写第一个数据到发送缓冲区
  3. 等待NSS线电平为低,SCK线开始翻转
  4. 数据帧从数据缓冲区加载到移位寄存器中,开始发送加载的数据
  5. 数据帧的第一位发送之后,TBE位置1
  6. 如果有更多数据,在每一次TBE置1时都可将数据写入发送缓冲区
  7. 等待TRANS置0,数据传输结束

从机接收

简要流程:

  1. 配置SPI外设的时序,使能SPI外设
  2. 等待NSS线电平为低,SCK线开始翻转
  3. 接收到的数据将从移位寄存器存入到接收缓冲区,且RBNE位置1
  4. 软件读取接收缓冲区,RBNE位置0
  5. 如果有更多数据则重复第4-5步
  6. 等待TRANS置0,数据传输结束

例程

STM32F103C8T6有两个SPI外设,对应管脚映射如下:

外设NSSSCKMISOMOSI
SPI0PA4PA5PA6PA7
SPI1PB12PB13PB14PB15

主机和从机全双工通信

现象:主机从机每2秒同时发送一组数据,同时接收一组数据

spi.c文件

#include "spi.h"

void SPI_MasterInit(void)
{
    /* 初始化GPIO */
    rcu_periph_clock_enable(RCU_GPIOA);
    /* SPI0 GPIO: NSS/PA4, SCK/PA5, MISO/PA6, MOSI/PA7 */
    gpio_init(GPIOA, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_7);
    gpio_init(GPIOA, GPIO_MODE_IN_FLOATING, GPIO_OSPEED_50MHZ, GPIO_PIN_6);

    /* 初始化SPI */
    rcu_periph_clock_enable(RCU_SPI0);
    
    spi_parameter_struct spi_init_struct = {0};
    spi_struct_para_init(&spi_init_struct);
    
    spi_init_struct.trans_mode           = SPI_TRANSMODE_FULLDUPLEX;  // 全双工模式
    spi_init_struct.device_mode          = SPI_MASTER;  // 主机模式
    spi_init_struct.frame_size           = SPI_FRAMESIZE_8BIT;  // 8位数据
    spi_init_struct.clock_polarity_phase = SPI_CK_PL_LOW_PH_1EDGE;  // 空闲电平为低,第一个边沿采样
    spi_init_struct.nss                  = SPI_NSS_HARD;  // 硬件片选
    spi_init_struct.prescale             = SPI_PSC_8;  // 时钟8分频,108MHz / 8 = 13.5MHz
    spi_init_struct.endian               = SPI_ENDIAN_MSB;  // 大端模式
    spi_init(SPI0, &spi_init_struct);
    
    spi_nss_output_enable(SPI0);
    spi_enable(SPI0);
}

void SPI_SlaveInit(void)
{
    /* 初始化GPIO */
    rcu_periph_clock_enable(RCU_GPIOB);
    /* SPI0 GPIO: NSS/PB12, SCK/PB13, MISO/PB14, MOSI/PB15 */
    gpio_init(GPIOB, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_14);
    gpio_init(GPIOB, GPIO_MODE_IN_FLOATING, GPIO_OSPEED_50MHZ, GPIO_PIN_12 | GPIO_PIN_13 | GPIO_PIN_15);

    /* 初始化SPI */
    rcu_periph_clock_enable(RCU_SPI1);
    
    spi_parameter_struct spi_init_struct = {0};
    spi_struct_para_init(&spi_init_struct);
    
    spi_init_struct.trans_mode           = SPI_TRANSMODE_FULLDUPLEX;  // 全双工模式
    spi_init_struct.device_mode          = SPI_SLAVE;  // 从机模式
    spi_init_struct.frame_size           = SPI_FRAMESIZE_8BIT;  // 8位数据
    spi_init_struct.clock_polarity_phase = SPI_CK_PL_LOW_PH_1EDGE;  // 空闲电平为低,第一个边沿采样
    spi_init_struct.nss                  = SPI_NSS_HARD;  // 硬件片选
    spi_init_struct.prescale             = SPI_PSC_8;  // 时钟8分频,108MHz / 8 = 13.5MHz
    spi_init_struct.endian               = SPI_ENDIAN_MSB;  // 大端模式
    spi_init(SPI1, &spi_init_struct);
    
    spi_enable(SPI1);
}

void SPI_MasterSlaveFullduplex(void)
{
    uint8_t master_tx_buf[16] = {0};
    uint8_t master_rx_buf[16] = {0};
    uint8_t slave_tx_buf[16] = {0};
    uint8_t slave_rx_buf[16] = {0};
    
    memset(master_tx_buf, 0x00, sizeof(master_tx_buf));
    memset(master_rx_buf, 0x00, sizeof(master_rx_buf));
    memset(slave_tx_buf, 0x00, sizeof(slave_tx_buf));
    memset(slave_rx_buf, 0x00, sizeof(slave_rx_buf));
    
    for(uint8_t i = 0; i < 16; ++i)
    {
        master_tx_buf[i] = 0x80 + i;
        slave_tx_buf[i] = 0x40 + i;
    }
    
    printf("Master send: ");
    for(uint8_t i = 0; i < 16; ++i)
    {
        printf("%x ", master_tx_buf[i]);
    }
    printf("\n");
    
    printf("Slave send: ");
    for(uint8_t i = 0; i < 16; ++i)
    {
        printf("%x ", slave_tx_buf[i]);
    }
    printf("\n");

    for(uint8_t i = 0; i < 16; ++i)
    {
        while(RESET == spi_i2s_flag_get(SPI1, SPI_FLAG_TBE));
        spi_i2s_data_transmit(SPI2, slave_tx_buf[i]);
        while(RESET == spi_i2s_flag_get(SPI0, SPI_FLAG_TBE));
        spi_i2s_data_transmit(SPI0, master_tx_buf[i]);
        while(RESET == spi_i2s_flag_get(SPI1, SPI_FLAG_RBNE));
        slave_rx_buf[i] = spi_i2s_data_receive(SPI2);
        while(RESET == spi_i2s_flag_get(SPI0, SPI_FLAG_RBNE));
        master_rx_buf[i] = spi_i2s_data_receive(SPI0);
    }
    
    printf("Master receive: ");
    for(uint8_t i = 0; i < 16; ++i)
    {
        printf("%x ", master_rx_buf[i]);
    }
    printf("\n");
    
    printf("Slave receive: ");
    for(uint8_t i = 0; i < 16; ++i)
    {
        printf("%x ", slave_rx_buf[i]);
    }
    printf("\n");
}

main.c文件

#include "gd32f10x.h"
#include "main.h"
#include "systick.h"
#include "usart.h"
#include "spi.h"
#include <stdio.h>
#include <string.h>

int main(void)
{	
    systick_config();
    USART_Config();
    SPI_MasterInit();
    SPI_SlaveInit();
        
    while(1)
    {
        SPI_MasterSlaveFullduplex();
        delay_ms(2000);
    }
}

有关【GD32】从0开始学GD32单片机(9)—— SPI外设详解+主机从机发送和接收例程的更多相关文章

  1. ruby - 即使失败也继续进行多主机测试 - 2

    我已经构建了一些serverspec代码来在多个主机上运行一组测试。问题是当任何测试失败时,测试会在当前主机停止。即使测试失败,我也希望它继续在所有主机上运行。Rakefile:namespace:specdotask:all=>hosts.map{|h|'spec:'+h.split('.')[0]}hosts.eachdo|host|begindesc"Runserverspecto#{host}"RSpec::Core::RakeTask.new(host)do|t|ENV['TARGET_HOST']=hostt.pattern="spec/cfengine3/*_spec.r

  2. ruby - 从 Ruby 中的主机名获取 IP 地址 - 2

    我有一个存储主机名的Ruby数组server_names。如果我打印出来,它看起来像这样:["hostname.abc.com","hostname2.abc.com","hostname3.abc.com"]相当标准。我想要做的是获取这些服务器的IP(可能将它们存储在另一个变量中)。看起来IPSocket类可以做到这一点,但我不确定如何使用IPSocket类遍历它。如果它只是尝试像这样打印出IP:server_names.eachdo|name|IPSocket::getaddress(name)pnameend它提示我没有提供服务器名称。这是语法问题还是我没有正确使用类?输出:ge

  3. Observability:从零开始创建 Java 微服务并监控它 (二) - 2

    这篇文章是继上一篇文章“Observability:从零开始创建Java微服务并监控它(一)”的续篇。在上一篇文章中,我们讲述了如何创建一个Javaweb应用,并使用Filebeat来收集应用所生成的日志。在今天的文章中,我来详述如何收集应用的指标,使用APM来监控应用并监督web服务的在线情况。源码可以在地址 https://github.com/liu-xiao-guo/java_observability 进行下载。摄入指标指标被视为可以随时更改的时间点值。当前请求的数量可以改变任何毫秒。你可能有1000个请求的峰值,然后一切都回到一个请求。这也意味着这些指标可能不准确,你还想提取最小/

  4. STM32读取串口传感器数据(颗粒物传感器,主动上传) - 2

    文章目录1.开发板选择*用到的资源2.串口通信(个人理解)3.代码分析(注释比较详细)1.主函数2.串口1配置3.串口2配置以及中断函数4.注意问题5.源码链接1.开发板选择我用的是STM32F103RCT6的板子,不过代码大概在F103系列的板子上都可以运行,我试过在野火103的霸道板上也可以,主要看一下串口对应的引脚一不一样就行了,不一样的就更改一下。*用到的资源keil5软件这里用到了两个串口资源,采集数据一个,串口通信一个,板子对应引脚如下:串口1,TX:PA9,RX:PA10串口2,TX:PA2,RX:PA32.串口通信(个人理解)我就从串口采集传感器数据这个过程说一下我自己的理解,

  5. C51单片机——实现用独立按键控制LED亮灭(调用函数篇) - 2

    说在前面这部分我本来是合为一篇来写的,因为目的是一样的,都是通过独立按键来控制LED闪灭本质上是起到开关的作用,即调用函数和中断函数。但是写一篇太累了,我还是决定分为两篇写,这篇是调用函数篇。在本篇中你主要看到这些东西!!!1.调用函数的方法(主要讲语法和格式)2.独立按键如何控制LED亮灭3.程序中的一些细节(软件消抖等)1.调用函数的方法思路还是比较清晰地,就是通过按下按键来控制LED闪灭,即每按下一次,LED取反一次。重要的是,把按键与LED联系在一起。我打算用K1来作为开关,看了一下开发板原理图,K1连接的是单片机的P31口,当按下K1时,P31是与GND相连的,也就是说,当我按下去时

  6. SPI接收数据异常问题总结 - 2

    SPI接收数据左移一位问题目录SPI接收数据左移一位问题一、问题描述二、问题分析三、探究原理四、经验总结最近在工作在学习调试SPI的过程中遇到一个问题——接收数据整体向左移了一位(1bit)。SPI数据收发是数据交换,因此接收数据时从第二个字节开始才是有效数据,也就是数据整体向右移一个字节(1byte)。请教前辈之后也没有得到解决,通过在网上查阅前人经验终于解决问题,所以写一个避坑经验总结。实际背景:MCU与一款芯片使用spi通信,MCU作为主机,芯片作为从机。这款芯片采用的是它规定的六线SPI,多了两根线:RDY和INT,这样从机就可以主动请求主机给主机发送数据了。一、问题描述根据从机芯片手

  7. ruby - 允许主机名包含下划线的 URI.parse 的替代方法 - 2

    我正在使用DMOZ的listofurltopics,其中包含一些具有包含下划线的主机名的url。例如:608609TheOuterHeaven610InformationandimagegalleryofMcFarlane'sactionfiguresforTrigun,Akira,TenchiMuyoandotherJapaneseSci-Fianimations.611Top/Arts/Animation/Anime/Collectibles/Models_and_Figures/Action_Figures612虽然此url可以在网络浏览器中使用(或者至少在我的浏览器中可以使用:

  8. ruby-on-rails - Ruby/Rails 中的夏令时开始和结束日期 - 2

    我正在开发一个Rails应用程序,我需要在其中找到给定特定偏移量或时区的夏令时开始和结束日期。我基本上在我的数据库中保存了从用户浏览器接收到的时区偏移量(“+3”,“-5”),我想在它出现时修改它由于夏令时的变化。我知道Time实例变量有dst?和isdst方法,如果存储在它们中的日期在夏令时与否。>Time.new.isdst=>true但是使用它来查找夏令时的开始和结束日期会占用太多资源,而且我还必须为我拥有的每个时区偏移量执行此操作。我想知道更好的方法。 最佳答案 好的,基于你所说的和@dhouty'sanswer:您希望能够

  9. ruby-on-rails - phusion passenger 和 ruby​​ 1.9.1 已经开始工作了吗? - 2

    我有一台生产机器和一台开发机器,都运行ubuntu8.10并且都运行最新的phusionpassenger。当我在osx上的本地开发机器上使用ruby​​1.9.1时,我想知道外面的人是否已经在使用带有ruby​​1.9.1甚至1.9.2的phusionpassenger?如果是这样,请告诉我们您的设置!此外,有没有办法在apache上使用phusionpassenger同时运行ruby​​1.8.7(ree)和1.9.1?感谢您的指点,我在任何地方都找不到任何提示... 最佳答案 是的,从某些2.2.x版本开始就正式支持它,我不记

  10. Ruby 服务器在本地主机(teambox)之外非常慢 - 2

    我刚刚在我的Ubuntu9.10服务器上安装了TeamBox。我使用提供的服务器脚本在端口3000上启动并运行它。它的运行速度非常慢,从另一台计算机连接时每个HTTP请求最多需要30秒。我使用链接从shell加载TeamBox,一点也不花时间。然后我设置了一个SSH隧道,它再次运行得非常快。我通过此服务器上的apache以及SAMBA等运行了大约30个虚拟主机,没有任何问题。我该如何解决这个问题? 最佳答案 我的redmine(ruby,webrick)太慢了。现在我解决了这个问题:apt-getinstallmongrelruby

随机推荐