GD32F303 硬件 SPI 发送任意多字节
GD32F303 硬件 SPI 发送任意多字节

GD32F303 硬件 SPI 发送任意多字节

使用定时器主从模式,配合 DMA 和硬件 SPI,可以实现单个片选内发送任意多个字节。

使用 SPI 与 DAC 等模拟芯片交互时,有时会遇到一个数据帧包含 24bit 甚至是 32bit 数。但 GD32/STM32 往往只支持 8bit/16bit 硬件 SPI。要在一个数据帧内发送 24bit/32bit,只能软件控制 SPI_CS 信号,然后多次向 SPI_DATA 写数据,凑够 24bit/32bit 数据,最后再拉高 SPI_CS,无法做到硬件全自动发送。

本文提供了一种方案,可以实现单个 SPI 片选内传输任意多字节。全程由硬件控制 SPI_CS、SPI_SCK、SPI_MOSI 信号,实现全硬件自动传输,全程无需 CPU 干预。

原理:

1、GD32F303 的 DMA0 请求源如下所示。我们可以配置 TIMER0 产生更新事件,驱动 DMA0_CH4 传输。DMA 设置成内存到外设模式,内存地址即为数据缓冲区首地址,外设地址设置为 SPI_DATA 寄存器。如此一来 TIMER0 每输出一个更新事件,DMA0_CH4 就会自动从内存搬运一次数据到 SPI_DATA 寄存器。

2、TIMER0 做为主定时器,每输出一个更新事件,驱动 TIMER1(从定时器)的计数器加一。从定时器输出 PWM,以 4 字节发送为例,在前 1/5 周期内输出高电平,4/5 周期内输出低电平,即可模拟出 SPI_CS 片选信号。如果是 8 字节,则前 1/9 周期内输出高电平,8/9 周期内输出低电平,以此类推。

3、具体时序如下图所示。从示图可以看出,主定时器每输出一个更新事件,SPI 产生一次传输。传输第一个字节时,因为 SPI_CS 片选信号为高电平,所以是无效的,后续 4 个字节才会被目标设备正常接收。看似是传输 4 字节,实际上发生了 5 次传输。这里会多消耗一些内存,但却换来了全硬件自动传输。

注意:因为 SPI_CS 产生下降沿后立即触发了 SPI 传输,所以 SPI_SCK 信号最好是配置成空闲高电平,在第二个跳变沿采样,或者是空闲低电平,第二个跳变沿采样。为 SPI 传输第一个 bit 与 SPI_CS 下降沿之间拉开一点间隔。

调试时,建议使用逻辑分析仪,首先查看 SPI_CS 信号是否正常产生;然后观察,例如示图“字节 1”和“字节 2”之间是否有空闲间隔;以及一个 SPI_CS 低电平期间传输的字节数是否正确。如果各个字节之间没有空闲间隔,说明硬件 SPI 的预分频参数设置有误。

驱动头文件如下所示:

/*********************************************************************************************************
* 模块名称: SPISendAnyByte.h
* 摘    要: 硬件 SPI 发送驱动,支持任意字节
* 当前版本: 1.0.0
* 作    者: SZLY(COPYRIGHT 2018 - 2020 SZLY. All rights reserved.)
* 完成日期: 2025年12月09日 
* 内    容:
* 注    意:
**********************************************************************************************************
* 取代版本:
* 作    者:
* 完成日期:
* 修改内容:
* 修改文件:
*********************************************************************************************************/
#ifndef _SPI_SEND_ANY_BYTE_H_
#define _SPI_SEND_ANY_BYTE_H_

/*********************************************************************************************************
*                                              包含头文件
*********************************************************************************************************/

/*********************************************************************************************************
*                                              宏定义
*********************************************************************************************************/

//帧大小
#define SPI_SEND_ANY_BYTE_FRAME_SIZE (4)

//主定时器配置
#define SPI_SEND_ANY_BYTE_TIMER_PRESCALER (119)
#define SPI_SEND_ANY_BYTE_TIMER_PERIOD    (999)

/*********************************************************************************************************
*                                              枚举结构体定义
*********************************************************************************************************/

/*********************************************************************************************************
*                                              API函数声明
*********************************************************************************************************/
//硬件 SPI 驱动
void InitSPISendAnyByte(void);

//传输相关
void SPISendAnyByteTransmitStop(void);              //停止传输
void SPISendAnyByteTransmitClear(void);             //缓冲区清空
void SPISendAnyByteSetData(unsigned char sendByte); //设置需要发送的数据
void SPISendAnyByteTransmitStart(void);             //启动传输

#endif

驱动源文件如下所示。

/*********************************************************************************************************
* 模块名称: SPISendAnyByte.c
* 摘    要: 硬件 SPI 发送驱动,支持任意字节
* 当前版本: 1.0.0
* 作    者: SZLY(COPYRIGHT 2018 - 2020 SZLY. All rights reserved.)
* 完成日期: 2025年12月09日 
* 内    容:
* 注    意:
**********************************************************************************************************
* 取代版本:
* 作    者:
* 完成日期:
* 修改内容:
* 修改文件:
*********************************************************************************************************/

/*********************************************************************************************************
*                                              包含头文件
*********************************************************************************************************/
#include "SPISendAnyByte.h"
#include "gd32f30x_conf.h"
#include "SysTick.h"

/*********************************************************************************************************
*                                              宏定义
*********************************************************************************************************/
#define DAC_POINT_COUNT (2048)

/*********************************************************************************************************
*                                              枚举结构体定义
*********************************************************************************************************/

/*********************************************************************************************************
*                                              内部变量
*********************************************************************************************************/
// DAC 数据缓冲区
static __attribute__((aligned(4))) uint8_t s_arrSPISendBuffer[DAC_POINT_COUNT]; 

//DAC 数据量计数器
static unsigned int s_iDMABufferLen = 0;

/*********************************************************************************************************
*                                              内部函数声明
*********************************************************************************************************/

/*********************************************************************************************************
*                                              内部函数实现
*********************************************************************************************************/

/*********************************************************************************************************
*                                              API函数实现
*********************************************************************************************************/
/*********************************************************************************************************
* 函数名称: InitSPISendAnyByte
* 函数功能: 硬件 SPI 驱动
* 输入参数: void
* 输出参数: void
* 返 回 值: void
* 创建日期: 2025年12月09日 
* 注    意: 
*********************************************************************************************************/
void InitSPISendAnyByte(void)
{
  //初始化结构体
  spi_parameter_struct spi_init_struct;
  timer_parameter_struct timer_initpara;
  timer_oc_parameter_struct timer_ocinitpara;
  
  //其它变量
  unsigned long long i;

  //使能 RCU 相关时钟
  rcu_periph_clock_enable(RCU_GPIOB);  //使能 GPIOB 的时钟
  rcu_periph_clock_enable(RCU_AF);     //使能 AF 的时钟

  //装填 DMA 缓冲区
  for(i = 0; i < DAC_POINT_COUNT; i++) { s_arrSPISendBuffer[i] = i; }

/*********************************************************************************************************
*                                              配置 SPI
*********************************************************************************************************/
  //使能 SPI1 的时钟
  rcu_periph_clock_enable(RCU_SPI1);

  //SPI GPIO
  gpio_init(GPIOB, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_12); //SPI NSS(不用)
  gpio_init(GPIOB, GPIO_MODE_AF_PP, GPIO_OSPEED_MAX, GPIO_PIN_13); //SPI_SCK
  gpio_init(GPIOB, GPIO_MODE_AF_PP, GPIO_OSPEED_MAX, GPIO_PIN_14); //SPI_MISO
  gpio_init(GPIOB, GPIO_MODE_AF_PP, GPIO_OSPEED_MAX, GPIO_PIN_15); //SPI_MOSI

  //配置 SPI
  spi_i2s_deinit(SPI1);
  spi_struct_para_init(&spi_init_struct);
  spi_init_struct.device_mode          = SPI_MASTER;               //SPI 主机
  spi_init_struct.trans_mode           = SPI_TRANSMODE_FULLDUPLEX; //全双工模式
  spi_init_struct.frame_size           = SPI_FRAMESIZE_8BIT;       //8 位数据帧
  spi_init_struct.nss                  = SPI_NSS_SOFT;             //软件管理 NSS(片选)
  spi_init_struct.endian               = SPI_ENDIAN_MSB;           //高位在前,低位在后数据位顺序
  spi_init_struct.clock_polarity_phase = SPI_CK_PL_HIGH_PH_2EDGE;  //时钟信号上升沿有效且第二个时钟跳变沿为有效采样边沿
  spi_init_struct.prescale             = SPI_PSC_8;                //时钟分频系数(由 SYSCLK 分频)
  spi_init(SPI1, &spi_init_struct);                                //根据参数配置 SPI1
  spi_crc_polynomial_set(SPI1, 7);                                 //配置硬件 CRC 计算
  spi_disable(SPI1);                                               //默认禁用 SPI 设备

/*********************************************************************************************************
*                       配置主定时器,用于输出更新事件驱动 DMA 传输,以及从定时器计数器加一
*********************************************************************************************************/
  //使能 TIMER0 的时钟
  rcu_periph_clock_enable(RCU_TIMER0); 

  //复位定时器
  timer_deinit(TIMER0);
  
  //定时器基础配置
  timer_struct_para_init(&timer_initpara);
  timer_initpara.prescaler = SPI_SEND_ANY_BYTE_TIMER_PRESCALER;
  timer_initpara.alignedmode = TIMER_COUNTER_EDGE;
  timer_initpara.counterdirection = TIMER_COUNTER_UP;
  timer_initpara.period = SPI_SEND_ANY_BYTE_TIMER_PERIOD;
  timer_initpara.clockdivision = TIMER_CKDIV_DIV1;
  timer_initpara.repetitioncounter = 0;
  timer_init(TIMER0, &timer_initpara);

  //使能 TIMER0 主模式输出触发
  timer_master_output_trigger_source_select(TIMER0, TIMER_TRI_OUT_SRC_UPDATE);

  //使能 TIMER0 自动重装载预装载
  timer_auto_reload_shadow_enable(TIMER0); 
  
  //配置定时器更新事件触发 DMA
  timer_dma_enable(TIMER0, TIMER_DMA_UPD);

  //暂不使能主定时器
  timer_disable(TIMER0);

/*********************************************************************************************************
*                                  配置从定时器,用于输出片选信号
*********************************************************************************************************/
  //使能 TIMER1 的时钟
  rcu_periph_clock_enable(RCU_TIMER1); 

  //TIMER1_CH2,用作 SPI 的 NSS
  gpio_init(GPIOB, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_10);
  gpio_pin_remap_config(GPIO_TIMER1_FULL_REMAP, ENABLE);

  //复位定时器
  timer_deinit(TIMER1);
  
  //定时器基础配置
  timer_struct_para_init(&timer_initpara);
  timer_initpara.prescaler = 0;  //预分频要设置为 0
  timer_initpara.alignedmode = TIMER_COUNTER_EDGE;
  timer_initpara.counterdirection = TIMER_COUNTER_UP;
  timer_initpara.period = SPI_SEND_ANY_BYTE_FRAME_SIZE;
  timer_initpara.clockdivision = TIMER_CKDIV_DIV1;
  timer_initpara.repetitioncounter = 0;
  timer_init(TIMER1, &timer_initpara);

  //配置TIMER1为从模式,触发源为ITI0(TIMER0的更新事件)
  timer_slave_mode_select(TIMER1, TIMER_SLAVE_MODE_EXTERNAL0);
  timer_input_trigger_source_select(TIMER1, TIMER_SMCFG_TRGSEL_ITI0);
  
  //PWM配置,TIMER1_CH2,输出 SPI_NSS
  timer_channel_output_struct_para_init(&timer_ocinitpara);
  timer_ocinitpara.ocpolarity  = TIMER_OC_POLARITY_HIGH;                    //输出极性:高电平有效(但实际低电平有效模式)
  timer_ocinitpara.outputstate = TIMER_CCX_ENABLE;                          //使能通道输出
  timer_ocinitpara.ocnpolarity = TIMER_OCN_POLARITY_HIGH;                   //互补输出极性
  timer_ocinitpara.outputnstate = TIMER_CCXN_DISABLE;                       //禁用互补输出
  timer_ocinitpara.ocidlestate  = TIMER_OC_IDLE_STATE_LOW;                  //空闲状态
  timer_ocinitpara.ocnidlestate = TIMER_OCN_IDLE_STATE_LOW;                 //互补通道空闲状态
  timer_channel_output_config(TIMER1, TIMER_CH_2, &timer_ocinitpara);       //配置 PWM
  timer_channel_output_mode_config(TIMER1, TIMER_CH_2, TIMER_OC_MODE_PWM0); //PWM模式0
  timer_channel_output_pulse_value_config(TIMER1, TIMER_CH_2, 1);           //配置占空比
  
  //使能TIMER1自动重装载预装载
  timer_auto_reload_shadow_enable(TIMER1); 

  //默认定时器关闭
  timer_disable(TIMER1);
}

/*********************************************************************************************************
* 函数名称:SPISendAnyByteTransmitStop
* 函数功能:暂停传输
* 输入参数:void 
* 输出参数:void
* 返 回 值:void
* 创建日期:2025年12月09日
* 注    意:
*********************************************************************************************************/
void SPISendAnyByteTransmitStop(void)
{
  //禁止定时器工作
  timer_disable(TIMER0);
  timer_disable(TIMER1);

  //关闭 DMA 传输
  dma_channel_disable(DMA0, DMA_CH4);

  //关闭 SPI
  spi_disable(SPI1);
}

/*********************************************************************************************************
* 函数名称:SPISendAnyByteTransmitClear
* 函数功能:清空发送缓冲区
* 输入参数:void 
* 输出参数:void
* 返 回 值:void
* 创建日期:2025年12月09日
* 注    意:
*********************************************************************************************************/
void SPISendAnyByteTransmitClear(void)
{
  s_iDMABufferLen = 0;
}

/*********************************************************************************************************
* 函数名称:SPISendAnyByteSetData
* 函数功能:设置需要发送的数据
* 输入参数:void 
* 输出参数:void
* 返 回 值:void
* 创建日期:2025年12月08日
* 注    意:
*********************************************************************************************************/
void SPISendAnyByteSetData(unsigned char sendByte)
{
  //设置帧内无效字节
  if(0 == (s_iDMABufferLen % (SPI_SEND_ANY_BYTE_FRAME_SIZE + 1)))
  {
    s_arrSPISendBuffer[s_iDMABufferLen] = 0xFF;
    s_iDMABufferLen = (s_iDMABufferLen + 1) % DAC_POINT_COUNT;
  }

  //保存需要发送的数据
  s_arrSPISendBuffer[s_iDMABufferLen] = sendByte;

  //数据量计数加一
  s_iDMABufferLen = (s_iDMABufferLen + 1) % DAC_POINT_COUNT;
}

/*********************************************************************************************************
* 函数名称:SPISendAnyByteTransmitStart
* 函数功能:启动传输
* 输入参数:void 
* 输出参数:void
* 返 回 值:void
* 创建日期:2025年12月08日
* 注    意:
*          启动传输需要经历以下步骤
*          1、通过 SPISendAnyByteTransmitStop() 停止传输
*          2、通过 SPISendAnyByteTransmitClear() 清空发送缓冲区
*          3、通过 SPISendAnyByteSetData() 设置波形数据
*          4、通过 SPISendAnyByteTransmitStart() 启动传输
*********************************************************************************************************/
void SPISendAnyByteTransmitStart(void)
{
  dma_parameter_struct dma_init_struct;

  //禁止所有传输
  SPISendAnyByteTransmitStop();

  //重新配置 DMA 传输(因为没有找到清空 DMA 传输计数器的方式,就重新配置了)
  //TIMER0 更新事件触发
  rcu_periph_clock_enable(RCU_DMA0);
  dma_deinit(DMA0, DMA_CH4);
  dma_struct_para_init(&dma_init_struct);
  dma_init_struct.periph_addr = (uint32_t)&SPI_DATA(SPI1);
  dma_init_struct.memory_addr = (uint32_t)s_arrSPISendBuffer;
  dma_init_struct.direction = DMA_MEMORY_TO_PERIPHERAL;
  dma_init_struct.number = s_iDMABufferLen;
  dma_init_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE;
  dma_init_struct.memory_inc = DMA_MEMORY_INCREASE_ENABLE;
  dma_init_struct.periph_width = DMA_PERIPHERAL_WIDTH_8BIT;
  dma_init_struct.memory_width = DMA_MEMORY_WIDTH_8BIT;
  dma_init_struct.priority = DMA_PRIORITY_ULTRA_HIGH;
  dma_init(DMA0, DMA_CH4, &dma_init_struct);
  dma_circulation_enable(DMA0, DMA_CH4); //使能循环模式(可改为单次模式,一次只发送一批数据)

  //使能 SPI 外设
  spi_enable(SPI1);

  //使能 DMA 传输
  dma_channel_enable(DMA0, DMA_CH4);

  //使能定时器,开启传输
  timer_counter_value_config(TIMER1, SPI_SEND_ANY_BYTE_FRAME_SIZE); //DMA 发送数据时,偏移了一个字节,需要调回
  timer_counter_value_config(TIMER0, 0);
  timer_enable(TIMER1);
  timer_enable(TIMER0);
}

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注