[研电赛技术支持] GD32F103 硬件SPI通信

[复制链接]
 楼主| tpgf 发表于 2025-2-22 09:58 | 显示全部楼层 |阅读模式
1. SPI的通信原理
SPI既可以做主机也可以做从机。

当做主机时。MOSI,SCK,CS都是作为输出。 而作为从机时。MOSI,SCK,CS都是作为输入。

2745767b92f4eb6aca.jpg

所以SPI的硬件电路应该实现这样的功能。

2. GD32/STM32的SPI框图
1. GD32框图
如下图做主机的数据流向:

666967b92f47be7ae.png

如下图做从机的数据流向:

6341467b92f4190217.png

2. STM32框图
通过一些寄存器的配置来控制电路。跟GD32的差不多。

波特率配置越高,采样越快。SPI的速率越快。

5133567b92f3b0c496.jpg

3. SPI的寄存器介绍
1. 控制寄存器0(SPI_CTL0)

9638767b92f34ae9a6.jpg

3409867b92f2ab4d6f.jpg

2. 控制寄存器1(SPI_CTL1)

8579467b92f239ce01.jpg

3. 状态寄存器(SPI_STAT)  

5800067b92f1d630c0.jpg


3768367b92f0f98c23.png

4. 数据寄存器(SPI_DATA)  

132267b92f0b3f9e7.jpg

4. SPI主模式配置

5355267b92f0449b8e.jpg

1. 发送数据
先判断发送主机发送缓冲器是否为空。

4155567b92efdeb57e.jpg

2. 接收数据
接收数据缓冲器是否为空。如果为空就等待,否则就接收。

5854967b92ef82c8da.jpg

5. dome (硬件SPI访问w25Q32)
NSS\SCK\MISO\MOSI  对应的 PA4\PA5\PA6\PA7引脚。

1. 具体的SPI配置步骤。
1. SPI时钟使能,SPI对应的GPIO时钟使能。复用时钟使能。

2. SPI的GOIP配置。

3. SPI的初始化配置

4. SPI使能。

2. 代码实现
spi.h

#ifndef _SPI_H
#define _SPI_H

#include "gd32f10x.h"


void w25qxx_rcu_init(void);
void w25qxx_io_init(void);
void w25qxx_spi_init(void);

#endif
spi.c

#include "spi.h"

// 使能外设时钟
void w25qxx_rcu_init(void)
{
        rcu_periph_clock_enable(RCU_GPIOA); //使能GPIOA时钟
        rcu_periph_clock_enable(RCU_AF);    //使能AF时钟
        rcu_periph_clock_enable(RCU_SPI0);  //使能SPI0时钟
}
       
// IO口进行配置,使之复用为SPI0, PA4\PA5\PA6\PA7,NSS\SCK\MISO\MOSI
void w25qxx_io_init(void)
{
        gpio_init(GPIOA, GPIO_MODE_IN_FLOATING, GPIO_OSPEED_50MHZ, GPIO_PIN_6); // MISO 浮空输入
       
        gpio_init(GPIOA, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_5 | GPIO_PIN_7); // SCK\MOSI 复用推挽
       
        gpio_init(GPIOA, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_4);// NSS片选口 普通的推挽输出
}
       
// SPI0初始化
void w25qxx_spi_init(void)
{
        spi_parameter_struct spi_struct;
        spi_struct.device_mode = SPI_MASTER;                    /*!< SPI master  做主机*/
  spi_struct.trans_mode = SPI_TRANSMODE_FULLDUPLEX;         /*!< SPI transfer type 全双工 */
  spi_struct.frame_size =  SPI_FRAMESIZE_8BIT;              /*!< SPI frame size  一次8字节 */
  spi_struct.nss = SPI_NSS_SOFT;                            /*!< SPI NSS control by software 软件CS */
  spi_struct.endian = SPI_ENDIAN_MSB;                       /*!< SPI big endian or little endian  传输高字节在前*/
  spi_struct.clock_polarity_phase = SPI_CK_PL_LOW_PH_1EDGE; /*!< SPI clock phase and polarity 空闲低电平 第一个边沿进行采样*/
  spi_struct.prescale = SPI_PSC_8;                          /*!< SPI prescaler factor 8分频*/
        spi_init(SPI0, &spi_struct);
}


w25qxx.h

#ifndef _W25QXX_SPI_H
#define _W25QXX_SPI_H

#include "gd32f10x.h"
#include "w25qxx_ins.h"
#include "gd32f10x_spi.h"

#define W25QXX_ID_1           1

#define W25QXX_SR_ID_1        1
#define W25QXX_SR_ID_2        2
#define W25QXX_SR_ID_3        3
void w25qxx_init(void);
void w25qxx_wait_busy(void);
uint8_t w25qxx_read_sr(uint8_t sregister_id);  // 读状态寄存器
       
void w25qxx_read(uint8_t *p_buffer, uint32_t read_addr, uint16_t num_read_bytes);

void w25qxx_write(uint8_t *p_buffer, uint32_t write_addr, uint16_t num_write_bytes);
void w25qxx_write_nocheck(uint8_t *p_buffer, uint32_t write_addr, uint16_t num_write_bytes); //
void w25qxx_write_page(uint8_t *p_buffer, uint32_t write_addr, uint16_t num_write_bytes);   // page program

void w25qxx_erase_sector(uint32_t sector_addr);
void w25qxx_erase_chip(void);

void w25qxx_write_enable(void);
void w25qxx_write_disable(void);

void w25qxx_power_down(void);
void w25qxx_wake_up(void);

void w25qxx_cs_enable(uint8_t cs_id);
void w25qxx_cs_disable(uint8_t cs_id);
uint8_t w25qxx_swap(uint8_t byte_to_send);


#endif

w25qxx.c

#include "w25qxx.h"
#include "spi.h"

void w25qxx_init(void){
        // 使能外设时钟
        w25qxx_rcu_init();
       
        // IO口进行配置,使之复用为SPI0, PA4\PA5\PA6\PA7,NSS\SCK\MISO\MOSI
        w25qxx_io_init();
       
        // SPI0初始化
        w25qxx_spi_init();
        // SPI使能
        spi_enable(SPI0);
}


// 如果SR-1的BUSY位为1的话,一直等待,直到BUSY位为0,结束等待
void w25qxx_wait_busy(void){
        while((w25qxx_read_sr(W25QXX_SR_ID_1) & 0x01) == 0x01){
                ;
        }
}

// 读状态寄存器
uint8_t w25qxx_read_sr(uint8_t sregister_id){
        uint8_t command, result;
        switch(sregister_id){
                case W25QXX_SR_ID_1:
                        command = W25QXX_READ_STATUS_REGISTER_1;
                break;
                case W25QXX_SR_ID_2:
                        command = W25QXX_READ_STATUS_REGISTER_2;
                break;
                case W25QXX_SR_ID_3:
                        command = W25QXX_READ_STATUS_REGISTER_3;
                break;
                default:
                        command = W25QXX_READ_STATUS_REGISTER_1;
                break;
        }
       
        w25qxx_cs_enable(W25QXX_ID_1);
        w25qxx_swap(command);
        result = w25qxx_swap(0xFF);
        w25qxx_cs_disable(W25QXX_ID_1);
       
        return result;
}

// 读flash的数据
// *p_buffer 读回的数据的存放位置
void w25qxx_read(uint8_t *p_buffer, uint32_t read_addr, uint16_t num_read_bytes){
        uint16_t i;
       
        w25qxx_cs_enable(W25QXX_ID_1);
       
        w25qxx_swap(W25QXX_READ_DATA); //发送读数据的指令
        w25qxx_swap(read_addr >> 16);  //发送24bit地址
        w25qxx_swap(read_addr >> 8);
        w25qxx_swap(read_addr);
       
        for(i=0; i < num_read_bytes; i++){
                p_buffer = w25qxx_swap(0xFF);
        }
       
        w25qxx_cs_disable(W25QXX_ID_1);
}

//
uint8_t W25QXX_Buffer[4096];  //用来存放从sector读出的bytes
void w25qxx_write(uint8_t *p_buffer, uint32_t write_addr, uint16_t num_write_bytes){
        uint32_t sec_num;
        uint16_t sec_remain;
        uint16_t sec_off;
        uint16_t i;
       
        sec_num        = write_addr / 4096;              //要写入的位置处在第sec_num个扇区上
        sec_off = write_addr % 4096;
       
        sec_remain = 4096 - sec_off;
       
        if(num_write_bytes <= sec_remain){
                w25qxx_read(W25QXX_Buffer, sec_num * 4096, 4096);  //扇区的数据读出来
               
                for(i = 0; i < sec_remain; i++){
                        if(W25QXX_Buffer[i + sec_off] != 0xFF)  //说明这个扇区的第i+sec_off位没有擦除
                                break;
                }
               
                if(i < sec_remain){ // 扇区没有擦除
                        w25qxx_erase_sector(sec_num * 4096);
                        for(i = 0; i < sec_remain; i++){
                                W25QXX_Buffer[i + sec_off] = p_buffer;
                        }
                        w25qxx_write_nocheck(W25QXX_Buffer, sec_num * 4096, 4096);
                }else{              // 扇区sec_remain部分是擦除过的
                        w25qxx_write_nocheck(p_buffer, write_addr, num_write_bytes);
                }
        }else{
                w25qxx_read(W25QXX_Buffer, sec_num * 4096, 4096);  //扇区的数据读出来
               
                for(i = 0; i < sec_remain; i++){
                        if(W25QXX_Buffer[i + sec_off] != 0xFF)  //说明这个扇区的第i+sec_off位没有擦除
                                break;
                }
               
                if(i < sec_remain){ // 扇区没有擦除
                        w25qxx_erase_sector(sec_num * 4096);
                        for(i = 0; i < sec_remain; i++){
                                W25QXX_Buffer[i + sec_off] = p_buffer;
                        }
                        w25qxx_write_nocheck(W25QXX_Buffer, sec_num * 4096, 4096);
                }else{              // 扇区sec_remain部分是擦除过的
                        w25qxx_write_nocheck(p_buffer, write_addr, sec_remain);
                }
               
                write_addr += sec_remain;
                p_buffer += sec_remain;
                num_write_bytes -= sec_remain;
                w25qxx_write(p_buffer, write_addr, num_write_bytes);
        }
               
        //判断读出来的数据是否都为0xFF
        ;//扇区是否删除
         //判断是否跨页
}

// 调用之前先确保扇区删除
void w25qxx_write_nocheck(uint8_t *p_buffer, uint32_t write_addr, uint16_t num_write_bytes){
        uint16_t page_remain = 256 - write_addr % 256;
       
        if(num_write_bytes <= page_remain){
                w25qxx_write_page(p_buffer, write_addr, num_write_bytes);
        }else{
                w25qxx_write_page(p_buffer, write_addr, page_remain);
                p_buffer += page_remain;
                write_addr += page_remain;
                num_write_bytes -= page_remain;
                w25qxx_write_nocheck(p_buffer, write_addr, num_write_bytes);
        }
}

// page program
// 保证没有跨页写的前提下调用此函数往某个页上写内容
void w25qxx_write_page(uint8_t *p_buffer, uint32_t write_addr, uint16_t num_write_bytes){
        uint16_t i;
       
        w25qxx_write_enable();
       
        w25qxx_cs_enable(W25QXX_ID_1);
        w25qxx_swap(W25QXX_PAGE_PROGRAM);
        w25qxx_swap(write_addr >> 16);  //发送24bit地址
        w25qxx_swap(write_addr >> 8);
        w25qxx_swap(write_addr);
       
        for(i = 0; i < num_write_bytes; i++){
                w25qxx_swap(p_buffer);
        }
        w25qxx_cs_disable(W25QXX_ID_1);
       
        w25qxx_wait_busy();
}

void w25qxx_erase_sector(uint32_t sector_addr){
        w25qxx_write_enable();
       
        w25qxx_cs_enable(W25QXX_ID_1);
        w25qxx_swap(W25QXX_SECTOR_ERASE_4KB);
        w25qxx_swap(sector_addr >> 16);
        w25qxx_swap(sector_addr >> 8);
        w25qxx_swap(sector_addr);
        w25qxx_cs_disable(W25QXX_ID_1);
       
        w25qxx_wait_busy();
}

void w25qxx_erase_chip(void){
        w25qxx_write_enable();
       
        w25qxx_cs_enable(W25QXX_ID_1);
        w25qxx_swap(W25QXX_CHIP_ERASE);
        w25qxx_cs_disable(W25QXX_ID_1);
       
        w25qxx_wait_busy();
}

void w25qxx_write_enable(void){
        w25qxx_cs_enable(W25QXX_ID_1);
        w25qxx_swap(W25QXX_WRITE_ENABLE);
        w25qxx_cs_disable(W25QXX_ID_1);
}

void w25qxx_write_disable(void){
        w25qxx_cs_enable(W25QXX_ID_1);
        w25qxx_swap(W25QXX_WRITE_DISABLE);
        w25qxx_cs_disable(W25QXX_ID_1);
}

// 低电量休眠
void w25qxx_power_down(void){
        w25qxx_cs_enable(W25QXX_ID_1);
        w25qxx_swap(W25QXX_POWER_DOWN);
        w25qxx_cs_disable(W25QXX_ID_1);
}

// 唤醒
void w25qxx_wake_up(void){
        w25qxx_cs_enable(W25QXX_ID_1);
        w25qxx_swap(W25QXX_RELEASE_POWER_DOWN_HPM_DEVICE_ID);
        w25qxx_cs_disable(W25QXX_ID_1);
}

/*
brief:使能片选引脚cs
cs_id: cs引脚的序号,即第几个w25qxx flash
*/
void w25qxx_cs_enable(uint8_t cs_id){
        switch(cs_id){
                case W25QXX_ID_1:
                        gpio_bit_reset(GPIOA, GPIO_PIN_4);
                break;
                default:
                        break;
        }
}

void w25qxx_cs_disable(uint8_t cs_id){
        switch(cs_id){
                case W25QXX_ID_1:
                        gpio_bit_set(GPIOA, GPIO_PIN_4);
                break;
                default:
                        break;
        }
}

/*
主从数据交换
*/
uint8_t w25qxx_swap(uint8_t byte_to_send){
        while(spi_i2s_flag_get(SPI0, SPI_FLAG_TBE) == RESET){ // 等待SPI发送缓冲器为空
                ;
        }
        spi_i2s_data_transmit(SPI0, byte_to_send);            // 把数据放到发生缓冲器
        while(spi_i2s_flag_get(SPI0, SPI_FLAG_TRANS) == SET){ // 等待通信结束
                ;
        }
       
        while(spi_i2s_flag_get(SPI0, SPI_FLAG_RBNE) == RESET){ // 等待SPI接收缓冲器非空
                ;
        }       
        return spi_i2s_data_receive(SPI0); /* 把接收到的数据返回(从接收缓冲器里拿出) */
}




————————————————

                            版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/qq_41328470/article/details/133690947

更多更合适ii 发表于 2025-2-28 16:47 | 显示全部楼层
SPI 是一种同步的串行通信协议,广泛用于短距离、高速度的设备之间的数据交换。
biechedan 发表于 2025-3-10 16:14 | 显示全部楼层
使能SPI外设时钟。
使能SPI对应的GPIO时钟。
使能复用时钟。
loutin 发表于 2025-3-12 16:35 | 显示全部楼层
如果 SPI 通信的双方存在较大的电压差或可能受到干扰,建议使用光耦或磁耦等隔离器件进行信号隔离,以提高通信的稳定性和可靠性。
bestwell 发表于 2025-3-12 17:41 | 显示全部楼层
SPI支持8位或16位的数据帧格式,可以通过寄存器配置。
averyleigh 发表于 2025-3-12 22:29 | 显示全部楼层
如果使用DMA进行数据传输,确保DMA通道配置正确。
配置DMA的存储器宽度、传输方向、内存宽度等参数。
pentruman 发表于 2025-3-14 11:51 | 显示全部楼层
SPI支持全双工通信,即同时进行数据的发送和接收。
gygp 发表于 2025-3-14 15:10 | 显示全部楼层
SPI 通信对电源的稳定性要求较高,不稳定的电源可能会导致通信错误。
janewood 发表于 2025-3-14 17:14 | 显示全部楼层
正确配置SPI时钟信号的极性和相位,以匹配从设备的要求。
bestwell 发表于 2025-3-14 22:05 | 显示全部楼层
SPI 的时钟极性(CPOL)和相位(CPHA)需要根据通信双方的要求进行正确配置。
maqianqu 发表于 2025-3-15 01:08 | 显示全部楼层
主设备需要在通信开始时拉低片选信号选中从设备,通信结束后再拉高片选信号释放从设备。
adolphcocker 发表于 2025-3-15 04:21 | 显示全部楼层
GD32F103是一款基于ARM Cortex-M3内核的微控制器,具有丰富的外设接口
mickit 发表于 2025-3-15 08:54 | 显示全部楼层
要根据通信距离、设备性能等因素合理设置 SPI 的波特率。波特率过高可能会导致信号失真,通信不稳定;波特率过低则会影响通信效率。
sheflynn 发表于 2025-3-15 12:22 | 显示全部楼层
注意数据的字节顺序和位顺序,确保发送和接收的数据与外部设备的期望格式一致。在进行多字节数据传输时,要注意数据帧的大小和边界对齐。
pentruman 发表于 2025-3-15 14:16 | 显示全部楼层
SPI 是同步通信协议,主从设备之间需要保持时钟同步。在进行连续数据传输时,要注意数据的发送和接收时机,避免数据丢失或错误。
gygp 发表于 2025-3-15 18:56 | 显示全部楼层
根据从设备的最大通信速率和系统时钟频率,合理设置SPI时钟分频值,避免通信速率过高导致数据丢失。
jonas222 发表于 2025-3-15 22:19 | 显示全部楼层
硬件SPI可以用于与各种外部设备进行高速通信
bartonalfred 发表于 2025-3-16 12:22 | 显示全部楼层
如果使用DMA或中断,确保正确配置DMA请求和中断向量,并在中断服务例程中正确处理数据。
sanfuzi 发表于 2025-3-18 22:48 | 显示全部楼层
SPI可以工作在主模式或从模式。在主模式下,SPI控制器产生时钟信号;在从模式下,SPI控制器接收外部时钟信号。
chenci2013 发表于 2025-3-19 11:29 | 显示全部楼层
在数据传输前,通过控制NSS引脚来选中外部SPI设备。若配置为软件控制NSS,则需要手动将NSS引脚拉低以选中设备;若配置为硬件自动控制NSS,则无需此操作。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

2350

主题

17466

帖子

21

粉丝
快速回复 在线客服 返回列表 返回顶部

2350

主题

17466

帖子

21

粉丝
快速回复 在线客服 返回列表 返回顶部