使用定时器主从模式,配合 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);
}