编程技巧(基于STM32)第四章 数据收发策略

参考教程:https://www.bilibili.com/video/BV1gow6zWEJj/?spm_id_from=333.1387.favlist.content.click&vd_source=8f8a7bd7765d52551c498d7eaed8acd5

一、单片机处理数据的几种方案

1、CPU直接完成数据转运

(1)在单片机开发中,经常需要处理流式的数据,在保证处理效率的同时,还要保证数据不会丢失或处理错误。

(2)CPU可以直接处理流式的数据,它具备这个能力,但CPU同时还需要承担其它复杂的运算任务,如果CPU还要经常轮询是否需要搬运数据,然后消耗算力去搬运数据,这就相当于抢占了其它运算任务能够被分配到的算力。为了节省CPU的算力,通常不会把搬运数据的任务全部交给CPU执行。

2、DMA+单缓冲区完成数据转运

(1)DMA可以提供外设和存储器或者存储器和存储器之间的高速数据传输,而且传输可以是双向的,数据传输过程无须CPU干预,CPU只需设置传输方向、起点和终点的地址等参数。

(2)DMA进行的数据传输需要基于通道,每个通道都支持软件触发和特定的硬件触发(被触发即开始数据传输)。

①软件触发不是CPU调用一次函数,传输才进行一次,CPU配置好参数并使能DMA通道后,DMA会依托总线全速连续搬运数据,直至传输计数器减为0自动停止,全程无需CPU反复干预。

②硬件触发由硬件外设向DMA发出数据传输请求,每发出一次DMA请求,DMA进行一次数据传输。

(3)单缓冲区数据结构:

①对于接收到的连续数据流,CPU没办法做到马上处理它们(往往有更高优先级的任务排在数据搬运的前面),对此CPU需要划分一片连续的内存作为队列缓冲,用于暂存未处理的数据流,当CPU能够“腾出时间”处理它们时,可以直接访问队列缓冲。

②对于需要发送的连续数据流,CPU没办法做到逐字节发送,对此CPU也需要划分一片连续的内存作为队列缓冲,用于暂存未发送的数据流,CPU只需将待发送的数据写入队列缓冲,然后配置DMA,DMA就能帮助CPU将待发送数据转运到外设。

(4)单缓冲区的缺陷:

①数据覆盖风险:CPU作为消费者,如果在读取缓冲区的数据时,生产者突然生产大量新数据,如果CPU没来得及处理旧数据,这些新数据将直接把就数据覆盖,CPU再处理这些问题数据时,可能引发不可预知的问题。但这并不是说明单缓冲完全不可行,只要确定数据生产速率、数据处理速率、单次生产数据量等关键性能参数,那么数据覆盖风险是可控的。

②强制“死等”:如果想完全根除数据覆盖风险,那么主程序一旦进入临界区读取数据,就要禁止生产者往缓冲区写入新数据,当处理数据比较慢时(比如使用FFT对采样信号进行频谱分析),容易导致外设一直处于等待状态。

③拷贝开销:为了避免强制“死等”的情况发生,耗时长的数据处理算法不会放在临界区中,临界区只做拷贝数据副本的工作,数据处理算法直接使用数据副本作为输入,但这就把DMA的优势削弱了,CPU仍然要执行数据搬运的任务。

3、DMA+双缓冲区完成数据转运

(1)单缓冲区的缺陷,主要是CPU作为消费者时可能产生的读写冲突和数据覆盖,先前介绍过应对方案,但是可能会阻塞外设,或者增加CPU的开销。既然存在读写冲突的情况,那么可以设计两个缓冲区,消费者读一个缓冲区,生产者写另一个缓冲区,双方交替使用两个缓冲区,以此将读写操作隔离,能够有效减少读写冲突和数据覆盖的情况。

(2)虽然双缓冲区的数据结构是两个缓冲区,但并不意味着在开发中要定义两个独立数组。

①实际上,DMA提供了三个中断源——传输完成中断、传输半满中断、传输错误中断,那么在开发中可以只定义一个Buffer数组,但逻辑上要将其等分为A、B两份,DMA的传输计数器大小配置为Buffer数组大小,传输模式配置为循环模式。

②DMA启动后,生产者(硬件外设)往上半部分Buffer写入数据,消费者(CPU)从下半部分Buffer读取数据(如有),当生产者写满上半部分Buffer时,DMA触发传输半满中断,如果CPU已经处理了下半部分Buffer的数据,则交换缓冲区。

③交换缓冲区后,生产者(硬件外设)往下半部分Buffer写入数据,消费者(CPU)从上半部分Buffer读取数据,当生产者写满下半部分Buffer时,DMA触发传输全满中断,如果CPU已经处理了上半部分Buffer的数据,则交换缓冲区,开启下一轮的数据转运。

④如果CPU处理数据的速度没能赶上硬件外设生产数据的速度,有两种策略:

[1]如果允许少量丢帧,则放弃未处理的旧数据,但需要在半满中断服务函数中置位溢出标志位,主程序可以知悉发生了数据丢失的情况。

[2]如果不允许旧数据丢失,则在中断中关闭DMA,并置位待处理标志,然后立刻退出中断,主循环处理完旧数据后,再清除待处理标志,然后重新开启DMA。(这种方案仍然存在丢帧的可能)

4、在RTOS中应用DMA+缓冲区完成数据转运

(1)任务同步机制:

①在裸机中,通常在DMA中断中置位一个全局标志,然后在“while(1)”大循环中轮询这个标志,如果主循环此时正在处理其它耗时的任务,数据就会滞后。

②在RTOS中,通常在DMA中断中调用xSemaphoreGiveFromISR函数(释放信号量)或xQueueSendFromISR函数(发送消息),负责处理数据的任务会因等待这个信号量或消息而进入阻塞态(Blocked),不占用CPU资源。中断服务函数末尾必须调用portYIELD_FROM_ISR(),检查是否有更高优先级的任务因此信号量而就绪,如果有则立即触发任务切换,保证数据得到最及时的响应。

(2)多任务读写的潜在风险:

①在裸机中,或者即使在RTOS中,但只有单个任务读缓冲区、DMA中断写缓冲区,则不需要使用互斥锁,但读写共享缓冲区时必须进入临界区保护,同时保证缓冲区索引为原子变量,防止中断打断,避免读到残缺数据。

②在RTOS中,如果存在多个任务读写同一个DMA缓冲区的情况,则必须使用互斥锁,避免多个任务同时读写一个缓冲区。互斥锁API禁止在中断服务函数内调用。

二、不同外设的数据处理实验

1、ADC数据收发处理

(1)实验目标:DMA将ADC生产的数据搬运到内存的双缓冲区中,CPU对ADC的数据做处理(本实验直接做复制处理),另一个DMA将CPU处理后的数据搬运到DAC并输出。

(2)实验准备:

①将OLED显示屏的项目文件夹复制一份作为模板使用。

②在项目中添加“Core”组,用于存放核心业务代码,而数据处理就是核心业务代码之一。

(3)在工程中添加如下代码文件。

①在Hardware组中添加adc_dma.h文件。

#ifndef __ADC_DMA_H
#define __ADC_DMA_H

#include "stm32f10x.h"

#define ADC_BUFFER_SIZE     256     // 总缓冲区大小(双缓冲各占一半)
#define HALF_BUFFER_SIZE    (ADC_BUFFER_SIZE / 2)

extern volatile uint16_t g_adc_raw_buffer[ADC_BUFFER_SIZE];
extern volatile uint8_t  g_adc_half_done_flag;
extern volatile uint8_t  g_adc_full_done_flag;

void ADC_DMA_Init(void);
void ADC_DMA_Start(void);

#endif

②在Hardware组中添加adc_dma.c文件。

#include "adc_dma.h"

// 双缓冲区:逻辑上等分为 A(前一半) 和 B(后一半)
volatile uint16_t g_adc_raw_buffer[ADC_BUFFER_SIZE];
volatile uint8_t  g_adc_half_done_flag = 0;
volatile uint8_t  g_adc_full_done_flag = 0;

/**
 * @brief ADC + DMA 初始化 (单通道, 连续转换, 循环模式)
 */
void ADC_DMA_Init(void)
{
	GPIO_InitTypeDef GPIO_InitStruct;
	ADC_InitTypeDef ADC_InitStruct;
	DMA_InitTypeDef DMA_InitStruct;

	// 1. 开启时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE);
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);

	// 2. 配置 ADC 输入引脚 (PA0 - ADC1 Channel 0)
	GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AIN;
	GPIO_Init(GPIOA, &GPIO_InitStruct);

	// 3. 配置 DMA
	DMA_DeInit(DMA1_Channel1);
	DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;
	DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)g_adc_raw_buffer;
	DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC;          // 外设 → 内存
	DMA_InitStruct.DMA_BufferSize = ADC_BUFFER_SIZE;
	DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
	DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;
	DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
	DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
	DMA_InitStruct.DMA_Mode = DMA_Mode_Circular;             // 循环模式
	DMA_InitStruct.DMA_Priority = DMA_Priority_High;
	DMA_InitStruct.DMA_M2M = DMA_M2M_Disable;
	DMA_Init(DMA1_Channel1, &DMA_InitStruct);

	// 4. 使能 DMA 半满中断和全满中断
	DMA_ITConfig(DMA1_Channel1, DMA_IT_HT | DMA_IT_TC, ENABLE);

	// 5. 配置 ADC
	ADC_InitStruct.ADC_Mode = ADC_Mode_Independent;
	ADC_InitStruct.ADC_ScanConvMode = DISABLE;               // 单通道
	ADC_InitStruct.ADC_ContinuousConvMode = ENABLE;          // 连续转换
	ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
	ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right;
	ADC_InitStruct.ADC_NbrOfChannel = 1;
	ADC_Init(ADC1, &ADC_InitStruct);

	// 6. 配置 ADC 通道 (采样周期 55.5 cycles)
	ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);

	// 7. 使能 ADC DMA 请求
	ADC_DMACmd(ADC1, ENABLE);

	// 8. 使能 ADC
	ADC_Cmd(ADC1, ENABLE);

	// 9. 校准 ADC
	ADC_ResetCalibration(ADC1);
	while (ADC_GetResetCalibrationStatus(ADC1));
	ADC_StartCalibration(ADC1);
	while (ADC_GetCalibrationStatus(ADC1));

	// 10. 配置 NVIC 中断
	NVIC_InitTypeDef NVIC_InitStruct;
	NVIC_InitStruct.NVIC_IRQChannel = DMA1_Channel1_IRQn;
	NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0;
	NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
	NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
	NVIC_Init(&NVIC_InitStruct);
}

/**
 * @brief 启动 ADC + DMA 采集
 */
void ADC_DMA_Start(void)
{
	g_adc_half_done_flag = 0;
	g_adc_full_done_flag = 0;
	DMA_Cmd(DMA1_Channel1, ENABLE);
	ADC_SoftwareStartConvCmd(ADC1, ENABLE);
}

/**
 * @brief DMA1 Channel1 中断服务函数 (ADC采集)
 *        半满中断 → 前半缓冲区(A)已填满
 *        全满中断 → 后半缓冲区(B)已填满
 */
void DMA1_Channel1_IRQHandler(void)
{
	// 半满中断: DMA 写满了前一半缓冲区 (A区)
	if (DMA_GetITStatus(DMA1_IT_HT1)) {
		DMA_ClearITPendingBit(DMA1_IT_HT1);
		g_adc_half_done_flag = 1;      // 通知主循环: A区数据就绪
	}

	// 全满中断: DMA 写满了后一半缓冲区 (B区)
	if (DMA_GetITStatus(DMA1_IT_TC1)) {
		DMA_ClearITPendingBit(DMA1_IT_TC1);
		g_adc_full_done_flag = 1;      // 通知主循环: B区数据就绪
	}
}

③在Hardware组中添加dac_dma.h文件。

#ifndef __DAC_DMA_H
#define __DAC_DMA_H

#include "stm32f10x.h"

#define DAC_BUFFER_SIZE     ADC_BUFFER_SIZE     // 与ADC缓冲区大小一致

extern uint16_t g_dac_output_buffer[DAC_BUFFER_SIZE];

void DAC_DMA_Init(void);
void DAC_DMA_Start(void);

#endif

④在Hardware组中添加dac_dma.c文件。

#include "dac_dma.h"

uint16_t g_dac_output_buffer[DAC_BUFFER_SIZE];

/**
 * @brief DAC + DMA 初始化 (TIM2触发)
 */
void DAC_DMA_Init(void)
{
	GPIO_InitTypeDef GPIO_InitStruct;
	DAC_InitTypeDef DAC_InitStruct;
	DMA_InitTypeDef DMA_InitStruct;
	TIM_TimeBaseInitTypeDef TIM_InitStruct;

	// 1. 开启时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_DAC, ENABLE);
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);

	// 2. 配置 DAC 输出引脚 (PA4)
	GPIO_InitStruct.GPIO_Pin = GPIO_Pin_4;
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AOut;
	GPIO_Init(GPIOA, &GPIO_InitStruct);

	// 3. 配置 DAC
	DAC_InitStruct.DAC_Trigger = DAC_Trigger_T2_TRGO;       // TIM2触发
	DAC_InitStruct.DAC_WaveGeneration = DAC_WaveGeneration_None;
	DAC_InitStruct.DAC_LFSRUnmask_TriangleAmplitude = DAC_LFSRUnmask_Bit0;
	DAC_InitStruct.DAC_OutputBuffer = DAC_OutputBuffer_Enable;
	DAC_Init(DAC_Channel_1, &DAC_InitStruct);
	DAC_Cmd(DAC_Channel_1, ENABLE);

	// 4. 配置 DMA (通道3, DAC专用)
	DMA_DeInit(DMA1_Channel3);
	DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&DAC->DHR12R1;
	DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)g_dac_output_buffer;
	DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralDST;         // 内存 → 外设
	DMA_InitStruct.DMA_BufferSize = DAC_BUFFER_SIZE;
	DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
	DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;
	DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
	DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
	DMA_InitStruct.DMA_Mode = DMA_Mode_Circular;            // 循环输出
	DMA_InitStruct.DMA_Priority = DMA_Priority_Medium;
	DMA_InitStruct.DMA_M2M = DMA_M2M_Disable;
	DMA_Init(DMA1_Channel3, &DMA_InitStruct);

	// 5. 配置定时器 TIM2 作为触发源
	TIM_InitStruct.TIM_Period = 1000 - 1;      // 调整输出频率
	TIM_InitStruct.TIM_Prescaler = 72 - 1;     // 1MHz 定时器时钟
	TIM_InitStruct.TIM_ClockDivision = 0;
	TIM_InitStruct.TIM_CounterMode = TIM_CounterMode_Up;
	TIM_TimeBaseInit(TIM2, &TIM_InitStruct);

	TIM_SelectOutputTrigger(TIM2, TIM_TRGOSource_Update);
	TIM_Cmd(TIM2, ENABLE);

	// 6. 使能 DAC DMA 请求
	DAC_DMACmd(DAC_Channel_1, ENABLE);
}

/**
 * @brief 启动 DAC + DMA 输出
 */
void DAC_DMA_Start(void)
{
	DMA_Cmd(DMA1_Channel3, ENABLE);
}

⑤在Core组中添加process.h文件。

#ifndef __PROCESS_H
#define __PROCESS_H

#include "adc_dma.h"

void Process_ADC_Data(uint16_t *src, uint16_t *dst, uint32_t len);

#endif

⑥在Core组中添加process.c文件。

#include "process.h"
#include "dac_dma.h"

/**
 * @brief 处理ADC数据 (直接复制到DAC输出缓冲区)
 * @param src  源数据指针 (ADC缓冲区)
 * @param dst  目标数据指针 (DAC缓冲区)
 * @param len  数据长度
 */
void Process_ADC_Data(uint16_t *src, uint16_t *dst, uint32_t len)
{
	uint32_t i;
	for (i = 0; i < len; i++) {
		dst[i] = src[i];
	}
}

(4)更新main.c文件,在进入主循环前完成外设和时钟的初始化,在主循环内轮询DMA中断事件,有则进入数据处理逻辑,无则跳过。

#include "stm32f10x.h"
#include "adc_dma.h"
#include "dac_dma.h"
#include "process.h"

// 指向当前待处理数据块的指针
static uint16_t *p_adc_block = NULL;
static uint16_t *p_dac_block = NULL;
static uint32_t  block_len = HALF_BUFFER_SIZE;

/**
 * @brief 系统时钟初始化 (72MHz)
 */
void SystemClock_Init(void)
{
	ErrorStatus HSEStartUpStatus;
	RCC_DeInit();
	RCC_HSEConfig(RCC_HSE_ON);
	HSEStartUpStatus = RCC_WaitForHSEStartUp();
	if (HSEStartUpStatus == SUCCESS) {
		RCC_HCLKConfig(RCC_SYSCLK_Div1);
		RCC_PCLK2Config(RCC_HCLK_Div1);
		RCC_PCLK1Config(RCC_HCLK_Div2);
		RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_9);
		RCC_PLLCmd(ENABLE);
		while (RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET);
		RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK);
		while (RCC_GetSYSCLKSource() != 0x08);
	}
}

int main(void)
{
	// 1. 系统初始化
	SystemClock_Init();

	// 2. 初始化外设
	ADC_DMA_Init();
	DAC_DMA_Init();

	// 3. 启动采集和输出
	ADC_DMA_Start();
	DAC_DMA_Start();

	while (1) {
		// 4. 检查半满中断 → A区数据就绪 (索引 0 ~ HALF_BUFFER_SIZE-1)
		if (g_adc_half_done_flag) {
			g_adc_half_done_flag = 0;

			p_adc_block = (uint16_t*)g_adc_raw_buffer;
			p_dac_block = g_dac_output_buffer;

			// CPU处理: 将ADC数据复制到DAC输出缓冲区
			Process_ADC_Data(p_adc_block, p_dac_block, block_len);
		}

		// 5. 检查全满中断 → B区数据就绪 (索引 HALF_BUFFER_SIZE ~ ADC_BUFFER_SIZE-1)
		if (g_adc_full_done_flag) {
			g_adc_full_done_flag = 0;

			p_adc_block = (uint16_t*)g_adc_raw_buffer + HALF_BUFFER_SIZE;
			p_dac_block = g_dac_output_buffer + HALF_BUFFER_SIZE;

			// CPU处理: 将ADC数据复制到DAC输出缓冲区
			Process_ADC_Data(p_adc_block, p_dac_block, block_len);
		}
	}
}

2、USART数据收发处理

(1)实验目标:DMA将串口RX收到的数据搬运到内存的双缓冲区中,CPU对串口收到的数据做处理(本实验直接做复制处理),另一个DMA将CPU处理后的数据搬运到串口TX并输出。

(2)实验准备:

①将OLED显示屏的项目文件夹复制一份作为模板使用。

②在项目中添加“Core”组,用于存放核心业务代码,而数据处理就是核心业务代码之一。

(3)在工程中添加如下代码文件。

①在Hardware组中添加uart_dma_rx.h文件。

#ifndef __UART_DMA_RX_H
#define __UART_DMA_RX_H

#include "stm32f10x.h"

#define RX_BUFFER_SIZE      256         // 总接收缓冲区大小(双缓冲各占一半)
#define RX_HALF_SIZE        (RX_BUFFER_SIZE / 2)

extern volatile uint8_t g_rx_buffer[RX_BUFFER_SIZE];
extern volatile uint8_t g_rx_half_done;   // 半满标志(前半部就绪)
extern volatile uint8_t g_rx_full_done;   // 全满标志(后半部就绪)

void USART1_DMA_RX_Init(uint32_t baudrate);
void USART1_DMA_RX_Start(void);

#endif

②在Hardware组中添加uart_dma_rx.c文件。

#include "uart_dma_rx.h"

volatile uint8_t g_rx_buffer[RX_BUFFER_SIZE];
volatile uint8_t g_rx_half_done = 0;
volatile uint8_t g_rx_full_done = 0;

/**
 * @brief 串口1 + DMA接收初始化(双缓冲,半满/全满中断)
 * @param baudrate 波特率(如 115200)
 */
void USART1_DMA_RX_Init(uint32_t baudrate)
{
	GPIO_InitTypeDef GPIO_InitStruct;
	USART_InitTypeDef USART_InitStruct;
	DMA_InitTypeDef DMA_InitStruct;
	NVIC_InitTypeDef NVIC_InitStruct;

	// 1. 开启时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);

	// 2. 配置 USART1 TX (PA9) 和 RX (PA10)
	//    TX 为复用推挽输出,RX 为浮空输入
	GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9;
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStruct);

	GPIO_InitStruct.GPIO_Pin = GPIO_Pin_10;
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
	GPIO_Init(GPIOA, &GPIO_InitStruct);

	// 3. 配置 USART1
	USART_InitStruct.USART_BaudRate = baudrate;
	USART_InitStruct.USART_WordLength = USART_WordLength_8b;
	USART_InitStruct.USART_StopBits = USART_StopBits_1;
	USART_InitStruct.USART_Parity = USART_Parity_No;
	USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
	USART_InitStruct.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
	USART_Init(USART1, &USART_InitStruct);

	// 4. 配置 DMA1 Channel5 (USART1_RX) – 外设到内存,循环模式
	DMA_DeInit(DMA1_Channel5);
	DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR;
	DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)g_rx_buffer;
	DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC;
	DMA_InitStruct.DMA_BufferSize = RX_BUFFER_SIZE;
	DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
	DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;
	DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
	DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
	DMA_InitStruct.DMA_Mode = DMA_Mode_Circular;          // 循环模式,实现自动回绕
	DMA_InitStruct.DMA_Priority = DMA_Priority_High;
	DMA_InitStruct.DMA_M2M = DMA_M2M_Disable;
	DMA_Init(DMA1_Channel5, &DMA_InitStruct);

	// 5. 使能 DMA 半满(HT)和全满(TC)中断
	DMA_ITConfig(DMA1_Channel5, DMA_IT_HT | DMA_IT_TC, ENABLE);

	// 6. 使能 USART1 的 DMA 接收请求
	USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE);

	// 7. 使能 USART1
	USART_Cmd(USART1, ENABLE);

	// 8. 配置 NVIC 中断 (DMA1 Channel5 中断)
	NVIC_InitStruct.NVIC_IRQChannel = DMA1_Channel5_IRQn;
	NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0;
	NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
	NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
	NVIC_Init(&NVIC_InitStruct);
}

/**
 * @brief 启动 DMA 接收
 */
void USART1_DMA_RX_Start(void)
{
	g_rx_half_done = 0;
	g_rx_full_done = 0;
	DMA_Cmd(DMA1_Channel5, ENABLE);      // 使能DMA,开始接收
}

/**
 * @brief DMA1 Channel5 中断服务函数 (USART1_RX)
 *        半满中断 → 接收缓冲区前一半已满
 *        全满中断 → 接收缓冲区后一半已满
 */
void DMA1_Channel5_IRQHandler(void)
{
	// 半满中断
	if (DMA_GetITStatus(DMA1_IT_HT5)) {
		DMA_ClearITPendingBit(DMA1_IT_HT5);
		g_rx_half_done = 1;       // 通知主循环处理前半部
	}

	// 全满中断
	if (DMA_GetITStatus(DMA1_IT_TC5)) {
		DMA_ClearITPendingBit(DMA1_IT_TC5);
		g_rx_full_done = 1;       // 通知主循环处理后半部
	}
}

③在Hardware组中添加uart_dma_tx.h文件。

#ifndef __UART_DMA_TX_H
#define __UART_DMA_TX_H

#include "stm32f10x.h"

#define TX_BUFFER_SIZE      RX_BUFFER_SIZE    	// 与接收缓冲区大小一致

extern uint8_t g_tx_buffer[TX_BUFFER_SIZE];   	// 发送缓冲区
extern volatile uint8_t g_tx_busy;             	// 发送忙标志

void USART1_DMA_TX_Init(void);
void USART1_DMA_TX_Send(uint8_t *data, uint16_t len);  // 发送指定长度数据

#endif

④在Hardware组中添加uart_dma_tx.c文件。

#include "uart_dma_tx.h"

uint8_t g_tx_buffer[TX_BUFFER_SIZE];
volatile uint8_t g_tx_busy = 0;

/**
 * @brief 串口1 + DMA发送初始化
 */
void USART1_DMA_TX_Init(void)
{
	DMA_InitTypeDef DMA_InitStruct;

	// 1. 时钟已在接收初始化中开启,此处不再重复
	// 2. 配置 DMA1 Channel4 (USART1_TX) – 内存到外设,非循环模式
	DMA_DeInit(DMA1_Channel4);
	DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR;
	DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)g_tx_buffer;
	DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralDST;
	DMA_InitStruct.DMA_BufferSize = 0;                 // 初始为0,发送前再设置
	DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
	DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;
	DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
	DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
	DMA_InitStruct.DMA_Mode = DMA_Mode_Normal;         // 非循环,发送完一次即停止
	DMA_InitStruct.DMA_Priority = DMA_Priority_Medium;
	DMA_InitStruct.DMA_M2M = DMA_M2M_Disable;
	DMA_Init(DMA1_Channel4, &DMA_InitStruct);

	// 3. 使能 USART1 的 DMA 发送请求
	USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE);

	// 注意:发送DMA中断(传输完成)可选,这里不开启,以简化
}

/**
 * @brief 启动一次 DMA 发送
 * @param data 数据缓冲区指针
 * @param len  数据长度(字节)
 */
void USART1_DMA_TX_Send(uint8_t *data, uint16_t len)
{
	if (len == 0 || g_tx_busy) return;   // 空闲才能发送

	// 等待上一次发送完成(如果DMA还在忙,则等待,但这里用标志保护)
	while (DMA_GetFlagStatus(DMA1_FLAG_TC4) == RESET) {
		// 如果DMA正在传输,等待
	}
	DMA_ClearFlag(DMA1_FLAG_TC4);

	// 设置发送缓冲区地址和长度
	DMA_Cmd(DMA1_Channel4, DISABLE);
	DMA1_Channel4->CMAR = (uint32_t)data;
	DMA1_Channel4->CNDTR = len;
	DMA_Cmd(DMA1_Channel4, ENABLE);

	g_tx_busy = 1;   // 标记忙
}

⑤在Core组中添加process.h文件。

#ifndef __PROCESS_H
#define __PROCESS_H

#include <stdint.h>

void Process_CopyData(uint8_t *src, uint8_t *dst, uint32_t len);

#endif

⑥在Core组中添加process.c文件。

#include "process.h"

void Process_CopyData(uint8_t *src, uint8_t *dst, uint32_t len)
{
	uint32_t i;
	for (i = 0; i < len; i++) {
		dst[i] = src[i];
	}
}

(4)更新main.c文件,在进入主循环前完成外设和时钟的初始化,在主循环内轮询DMA中断事件,有则进入数据处理逻辑,无则跳过。

#include "stm32f10x.h"
#include "uart_dma_rx.h"
#include "uart_dma_tx.h"
#include "process.h"

/**
 * @brief 系统时钟配置为 72MHz (使用外部晶振 8MHz)
 */
void SystemClock_Config(void)
{
	ErrorStatus HSEStartUpStatus;
	RCC_DeInit();
	RCC_HSEConfig(RCC_HSE_ON);
	HSEStartUpStatus = RCC_WaitForHSEStartUp();
	if (HSEStartUpStatus == SUCCESS) {
		RCC_HCLKConfig(RCC_SYSCLK_Div1);
		RCC_PCLK2Config(RCC_HCLK_Div1);
		RCC_PCLK1Config(RCC_HCLK_Div2);
		RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_9);
		RCC_PLLCmd(ENABLE);
		while (RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET);
		RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK);
		while (RCC_GetSYSCLKSource() != 0x08);
	}
}

int main(void)
{
	// 1. 系统时钟初始化
	SystemClock_Config();

	// 2. 初始化串口及DMA
	USART1_DMA_RX_Init(115200);
	USART1_DMA_TX_Init();

	// 3. 启动接收DMA
	USART1_DMA_RX_Start();

	while (1) {
		// 4. 检测半满标志 → 处理接收缓冲区前一半 (索引 0 ~ RX_HALF_SIZE-1)
		if (g_rx_half_done) {
			g_rx_half_done = 0;

			// 将接收缓冲区前半部分复制到发送缓冲区前半部分
			Process_CopyData((uint8_t*)g_rx_buffer,
				g_tx_buffer,
				RX_HALF_SIZE);

			// 启动DMA发送前一半数据(发送缓冲区起始地址,长度 RX_HALF_SIZE)
			USART1_DMA_TX_Send(g_tx_buffer, RX_HALF_SIZE);
		}

		// 5. 检测全满标志 → 处理接收缓冲区后一半 (索引 RX_HALF_SIZE ~ RX_BUFFER_SIZE-1)
		if (g_rx_full_done) {
			g_rx_full_done = 0;

			// 将接收缓冲区后半部分复制到发送缓冲区后半部分
			Process_CopyData((uint8_t*)(g_rx_buffer + RX_HALF_SIZE),
				(g_tx_buffer + RX_HALF_SIZE),
				RX_HALF_SIZE);

			// 启动DMA发送后一半数据(发送缓冲区偏移位置,长度 RX_HALF_SIZE)
			USART1_DMA_TX_Send(g_tx_buffer + RX_HALF_SIZE, RX_HALF_SIZE);
		}
	}
}

3、I2C数据收发处理

(1)实验目标:DMA将I2C硬件外设收到的数据搬运到内存的单缓冲区中,CPU对I2C硬件外设收到的数据做处理(本实验直接做复制处理),然后DMA再将CPU处理后的数据搬运到I2C硬件外设并输出给同一个I2C从机。

(2)实验准备:

①将OLED显示屏的项目文件夹复制一份作为模板使用。

②在项目中添加“Core”组,用于存放核心业务代码,而数据处理就是核心业务代码之一。

(3)在工程中添加如下代码文件。

①在Hardware组中添加i2c_dma.h文件。

#ifndef __I2C_DMA_H
#define __I2C_DMA_H

#include "stm32f10x.h"

#define I2C_SLAVE_ADDR      0x50        // 从机地址(7位,根据实际修改)
#define I2C_DEV_REG         0x00        // 从机寄存器地址(根据实际修改)
#define BUFFER_SIZE         32          // 单缓冲区大小(字节)

// 状态标志(供主循环查询)
extern volatile uint8_t g_i2c_rx_done;  // DMA接收完成标志
extern volatile uint8_t g_i2c_tx_done;  // DMA发送完成标志

// 数据缓冲区
extern uint8_t g_i2c_buffer[BUFFER_SIZE];

void I2C_DMA_Init(void);
void I2C_DMA_StartRead(void);
void I2C_DMA_StartWrite(uint8_t *data, uint16_t len);

#endif

②在Hardware组中添加i2c_dma.c文件。

#include "i2c_dma.h"

volatile uint8_t g_i2c_rx_done = 0;
volatile uint8_t g_i2c_tx_done = 0;
uint8_t g_i2c_buffer[BUFFER_SIZE];

/**
 * @brief I2C2 + DMA1 初始化(主机模式,100kHz)
 */
void I2C_DMA_Init(void)
{
	GPIO_InitTypeDef GPIO_InitStruct;
	I2C_InitTypeDef I2C_InitStruct;
	DMA_InitTypeDef DMA_InitStruct;
	NVIC_InitTypeDef NVIC_InitStruct;

	// 1. 开启时钟
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);

	// 2. 配置 I2C2 引脚 (PB10: SCL, PB11: SDA) – 复用开漏输出
	GPIO_InitStruct.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_OD;
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStruct);

	// 3. 配置 I2C2
	I2C_DeInit(I2C2);
	I2C_InitStruct.I2C_Mode = I2C_Mode_I2C;
	I2C_InitStruct.I2C_DutyCycle = I2C_DutyCycle_2;
	I2C_InitStruct.I2C_OwnAddress1 = 0x00;          // 主机模式,自身地址无用
	I2C_InitStruct.I2C_Ack = I2C_Ack_Enable;
	I2C_InitStruct.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
	I2C_InitStruct.I2C_ClockSpeed = 100000;         // 100kHz
	I2C_Init(I2C2, &I2C_InitStruct);

	// 4. 配置 DMA1 Channel6 (I2C2_RX) – 外设→内存,单次模式[reference:2][reference:3]
	DMA_DeInit(DMA1_Channel6);
	DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&I2C2->DR;
	DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)g_i2c_buffer;
	DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC;
	DMA_InitStruct.DMA_BufferSize = 0;               // 初始为0,启动前再设置
	DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
	DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;
	DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
	DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
	DMA_InitStruct.DMA_Mode = DMA_Mode_Normal;       // 单次模式(非循环)
	DMA_InitStruct.DMA_Priority = DMA_Priority_High;
	DMA_InitStruct.DMA_M2M = DMA_M2M_Disable;
	DMA_Init(DMA1_Channel6, &DMA_InitStruct);

	// 5. 配置 DMA1 Channel7 (I2C2_TX) – 内存→外设,单次模式[reference:4]
	DMA_DeInit(DMA1_Channel7);
	DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&I2C2->DR;
	DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)g_i2c_buffer;
	DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralDST;
	DMA_InitStruct.DMA_BufferSize = 0;
	DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
	DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;
	DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
	DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
	DMA_InitStruct.DMA_Mode = DMA_Mode_Normal;
	DMA_InitStruct.DMA_Priority = DMA_Priority_Medium;
	DMA_InitStruct.DMA_M2M = DMA_M2M_Disable;
	DMA_Init(DMA1_Channel7, &DMA_InitStruct);

	// 6. 使能 DMA 传输完成中断(RX和TX)
	DMA_ITConfig(DMA1_Channel6, DMA_IT_TC, ENABLE);
	DMA_ITConfig(DMA1_Channel7, DMA_IT_TC, ENABLE);

	// 7. 使能 I2C2 DMA 请求
	I2C_DMACmd(I2C2, ENABLE);

	// 8. 使能 I2C2
	I2C_Cmd(I2C2, ENABLE);

	// 9. 配置 NVIC 中断(DMA1 Channel6 和 Channel7)
	NVIC_InitStruct.NVIC_IRQChannel = DMA1_Channel6_IRQn;
	NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0;
	NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
	NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
	NVIC_Init(&NVIC_InitStruct);

	NVIC_InitStruct.NVIC_IRQChannel = DMA1_Channel7_IRQn;
	NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0;
	NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1;  // 略低于RX,避免竞争[reference:5]
	NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
	NVIC_Init(&NVIC_InitStruct);
}

/**
 * @brief 启动一次 DMA 接收(从I2C从机读取数据到缓冲区)
 */
void I2C_DMA_StartRead(void)
{
	g_i2c_rx_done = 0;

	// 设置DMA接收长度
	DMA_Cmd(DMA1_Channel6, DISABLE);
	DMA1_Channel6->CNDTR = BUFFER_SIZE;
	DMA_Cmd(DMA1_Channel6, ENABLE);

	// 发送起始条件 + 从机地址(读方向)
	I2C_GenerateSTART(I2C2, ENABLE);
	while (!I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT));  // EV5

	I2C_Send7bitAddress(I2C2, I2C_SLAVE_ADDR << 1, I2C_Direction_Receiver);
	while (!I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED)); // EV6

	// 使能DMA接收(I2C硬件自动将DR数据交给DMA)
	// 注意:STM32F103 I2C DMA接收需要先使能DMA,再使能I2C的ACK
	I2C_DMALastTransferCmd(I2C2, ENABLE);  // 最后1字节自动发送NACK
}

/**
 * @brief 启动一次 DMA 发送(将缓冲区数据写入I2C从机)
 * @param data 数据缓冲区指针
 * @param len  数据长度(字节)
 */
void I2C_DMA_StartWrite(uint8_t *data, uint16_t len)
{
	if (len == 0) return;

	g_i2c_tx_done = 0;

	// 设置DMA发送源地址和长度
	DMA_Cmd(DMA1_Channel7, DISABLE);
	DMA1_Channel7->CMAR = (uint32_t)data;
	DMA1_Channel7->CNDTR = len;
	DMA_Cmd(DMA1_Channel7, ENABLE);

	// 发送起始条件 + 从机地址(写方向)
	I2C_GenerateSTART(I2C2, ENABLE);
	while (!I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT));  // EV5

	I2C_Send7bitAddress(I2C2, I2C_SLAVE_ADDR << 1, I2C_Direction_Transmitter);
	while (!I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); // EV6

	// 使能DMA发送(I2C硬件自动从DR发送数据)
	// 注意:需要先写入第一个字节触发DMA请求
	// 对于STM32F103,使能DMA后,TXE事件会自动触发DMA搬运
}

/**
 * @brief DMA1 Channel6 中断(I2C2_RX 接收完成)
 */
void DMA1_Channel6_IRQHandler(void)
{
	if (DMA_GetITStatus(DMA1_IT_TC6)) {
		DMA_ClearITPendingBit(DMA1_IT_TC6);

		// 等待I2C总线停止(传输完成)
		while (I2C_GetFlagStatus(I2C2, I2C_FLAG_BUSY));

		// 发送停止条件
		I2C_GenerateSTOP(I2C2, ENABLE);

		// 置位接收完成标志(通知主循环处理数据)
		g_i2c_rx_done = 1;
	}
}

/**
 * @brief DMA1 Channel7 中断(I2C2_TX 发送完成)
 */
void DMA1_Channel7_IRQHandler(void)
{
	if (DMA_GetITStatus(DMA1_IT_TC7)) {
		DMA_ClearITPendingBit(DMA1_IT_TC7);

		// 等待I2C总线停止
		while (I2C_GetFlagStatus(I2C2, I2C_FLAG_BUSY));

		// 发送停止条件
		I2C_GenerateSTOP(I2C2, ENABLE);

		// 置位发送完成标志
		g_i2c_tx_done = 1;
	}
}

③在Core组中添加process.h文件。

#ifndef __PROCESS_H
#define __PROCESS_H

#include <stdint.h>

void Process_CopyData(uint8_t *src, uint8_t *dst, uint32_t len);

#endif

④在Core组中添加process.c文件。

#include "process.h"

void Process_CopyData(uint8_t *src, uint8_t *dst, uint32_t len)
{
	uint32_t i;
	for (i = 0; i < len; i++) {
		dst[i] = src[i];
	}
}

(4)更新main.c文件,在进入主循环前完成外设和时钟的初始化,在主循环内轮询DMA中断事件,有则进入数据处理逻辑,无则跳过。

#include "stm32f10x.h"
#include "i2c_dma.h"
#include "process.h"

// 临时发送缓冲区(用于存放处理后的数据)
static uint8_t g_tx_buffer[BUFFER_SIZE];

typedef enum {
	STATE_IDLE,              	// 空闲(初始状态)
	STATE_WAITING_RX,       	// 等待DMA接收完成
	STATE_PROCESSING,        	// CPU处理数据中
	STATE_WAITING_TX       	// 等待DMA发送完成
} AppState_t;

static AppState_t app_state = STATE_IDLE;

// 超时计数器(需根据实际时钟频率调整)
#define TIMEOUT_THRESHOLD    10000   // 约100ms(假设主循环约10µs/次)
static uint32_t timeout_counter = 0;

static void HandleTimeout(void)
{
	// 超时后执行恢复操作:停止I2C,重新触发接收
	I2C_GenerateSTOP(I2C2, ENABLE);

	// 清除DMA状态(禁用并重新使能)
	DMA_Cmd(DMA1_Channel6, DISABLE);
	DMA_ClearFlag(DMA1_FLAG_TC6);
	DMA_Cmd(DMA1_Channel6, ENABLE);

	// 清除标志位
	g_i2c_rx_done = 0;
	g_i2c_tx_done = 0;
	timeout_counter = 0;

	// 重新开始接收
	I2C_DMA_StartRead();
	app_state = STATE_WAITING_RX;
}

void SystemClock_Config(void)
{
	ErrorStatus HSEStartUpStatus;
	RCC_DeInit();
	RCC_HSEConfig(RCC_HSE_ON);
	HSEStartUpStatus = RCC_WaitForHSEStartUp();
	if (HSEStartUpStatus == SUCCESS) {
		RCC_HCLKConfig(RCC_SYSCLK_Div1);
		RCC_PCLK2Config(RCC_HCLK_Div1);
		RCC_PCLK1Config(RCC_HCLK_Div2);
		RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_9);
		RCC_PLLCmd(ENABLE);
		while (RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET);
		RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK);
		while (RCC_GetSYSCLKSource() != 0x08);
	}
}

int main(void)
{
	// 1. 系统时钟初始化
	SystemClock_Config();

	// 2. 初始化 I2C 和 DMA
	I2C_DMA_Init();

	// 3. 触发第一次 DMA 接收,进入等待状态
	I2C_DMA_StartRead();
	app_state = STATE_WAITING_RX;
	timeout_counter = 0;

	while (1) {
		switch (app_state) {
		case STATE_WAITING_RX:
			// 检查接收完成标志(由DMA中断置位)
			if (g_i2c_rx_done) {
				g_i2c_rx_done = 0;          // 清除标志
				timeout_counter = 0;         // 重置超时计数
				app_state = STATE_PROCESSING;
			}
			else {
				// 超时检测
				timeout_counter++;
				if (timeout_counter > TIMEOUT_THRESHOLD) {
					HandleTimeout();        // 超时处理:复位并重试
				}
			}
			break;

		case STATE_PROCESSING:
			// CPU处理数据(复制操作极快,直接执行)
			Process_CopyData((uint8_t*)g_i2c_buffer, g_tx_buffer, BUFFER_SIZE);

			// 立即触发DMA发送
			I2C_DMA_StartWrite(g_tx_buffer, BUFFER_SIZE);
			timeout_counter = 0;
			app_state = STATE_WAITING_TX;
			break;

		case STATE_WAITING_TX:
			// 检查发送完成标志(由DMA中断置位)
			if (g_i2c_tx_done) {
				g_i2c_tx_done = 0;          // 清除标志
				timeout_counter = 0;

				// ===== 一轮完成,开启下一轮 =====
				I2C_DMA_StartRead();        // 重新触发接收
				app_state = STATE_WAITING_RX;
			}
			else {
				// 超时检测
				timeout_counter++;
				if (timeout_counter > TIMEOUT_THRESHOLD) {
					HandleTimeout();        // 超时处理:复位并重试
				}
			}
			break;

		case STATE_IDLE:
		default:
			// 空闲状态(可在此加入其它低优先级任务)
			break;
		}

		// ============================================================
		// 在这里可以插入其它非阻塞任务(如LED闪烁、串口打印等)
		// 这些任务不会因为等待DMA而被阻塞
		// ============================================================
	}
}

4、SPI数据收发处理

(1)实验目标:DMA将SPI硬件外设收到的数据搬运到内存的单缓冲区中,CPU对SPI硬件外设收到的数据做处理(本实验直接做复制处理),然后DMA再将CPU处理后的数据搬运到SPI硬件外设并输出给同一个SPI从机。

(2)实验准备:

①将OLED显示屏的项目文件夹复制一份作为模板使用。

②在项目中添加“Core”组,用于存放核心业务代码,而数据处理就是核心业务代码之一。

(3)在工程中添加如下代码文件。

①在Hardware组中添加spi_dma.h文件。

#ifndef __SPI_DMA_H
#define __SPI_DMA_H

#include "stm32f10x.h"

#define BUFFER_SIZE         32          // 单缓冲区大小(字节)

// 状态标志(供主循环查询)
extern volatile uint8_t g_spi_rx_done;  // DMA接收完成标志
extern volatile uint8_t g_spi_tx_done;  // DMA发送完成标志

// 数据缓冲区
extern uint8_t g_spi_buffer[BUFFER_SIZE];

void SPI_DMA_Init(void);
void SPI_DMA_StartRead(void);
void SPI_DMA_StartWrite(uint8_t *data, uint16_t len);

#endif

②在Hardware组中添加spi_dma.c文件。

#include "spi_dma.h"

volatile uint8_t g_spi_rx_done = 0;
volatile uint8_t g_spi_tx_done = 0;
uint8_t g_spi_buffer[BUFFER_SIZE];

/**
 * @brief SPI1 + DMA1 初始化(主机模式,全双工)
 */
void SPI_DMA_Init(void)
{
	GPIO_InitTypeDef GPIO_InitStruct;
	SPI_InitTypeDef SPI_InitStruct;
	DMA_InitTypeDef DMA_InitStruct;
	NVIC_InitTypeDef NVIC_InitStruct;

	// 1. 开启时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE);
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);

	// 2. 配置 SPI1 引脚 (PA5: SCK, PA6: MISO, PA7: MOSI)
	//    SCK 和 MOSI 为复用推挽输出,MISO 为浮空输入
	GPIO_InitStruct.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStruct);

	GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6;
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
	GPIO_Init(GPIOA, &GPIO_InitStruct);

	// 3. 配置 SPI1
	SPI_InitStruct.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
	SPI_InitStruct.SPI_Mode = SPI_Mode_Master;          // 主机模式
	SPI_InitStruct.SPI_DataSize = SPI_DataSize_8b;
	SPI_InitStruct.SPI_CPOL = SPI_CPOL_Low;             // 时钟空闲低电平
	SPI_InitStruct.SPI_CPHA = SPI_CPHA_1Edge;           // 第一个边沿采样
	SPI_InitStruct.SPI_NSS = SPI_NSS_Soft;              // 软件控制片选
	SPI_InitStruct.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4; // 18MHz (72MHz/4)
	SPI_InitStruct.SPI_FirstBit = SPI_FirstBit_MSB;
	SPI_InitStruct.SPI_CRCPolynomial = 7;
	SPI_Init(SPI1, &SPI_InitStruct);

	// 4. 配置 DMA1 Channel2 (SPI1_RX) – 外设→内存,单次模式
	DMA_DeInit(DMA1_Channel2);
	DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&SPI1->DR;
	DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)g_spi_buffer;
	DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC;
	DMA_InitStruct.DMA_BufferSize = 0;                  // 初始为0,启动前再设置
	DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
	DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;
	DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
	DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
	DMA_InitStruct.DMA_Mode = DMA_Mode_Normal;          // 单次模式
	DMA_InitStruct.DMA_Priority = DMA_Priority_High;
	DMA_InitStruct.DMA_M2M = DMA_M2M_Disable;
	DMA_Init(DMA1_Channel2, &DMA_InitStruct);

	// 5. 配置 DMA1 Channel3 (SPI1_TX) – 内存→外设,单次模式
	DMA_DeInit(DMA1_Channel3);
	DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&SPI1->DR;
	DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)g_spi_buffer;
	DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralDST;
	DMA_InitStruct.DMA_BufferSize = 0;
	DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Enable;
	DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;
	DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
	DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
	DMA_InitStruct.DMA_Mode = DMA_Mode_Normal;
	DMA_InitStruct.DMA_Priority = DMA_Priority_Medium;
	DMA_InitStruct.DMA_M2M = DMA_M2M_Disable;
	DMA_Init(DMA1_Channel3, &DMA_InitStruct);

	// 6. 使能 DMA 传输完成中断(RX和TX)
	DMA_ITConfig(DMA1_Channel2, DMA_IT_TC, ENABLE);
	DMA_ITConfig(DMA1_Channel3, DMA_IT_TC, ENABLE);

	// 7. 使能 SPI1 DMA 请求(接收和发送)
	SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Rx, ENABLE);
	SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Tx, ENABLE);

	// 8. 使能 SPI1
	SPI_Cmd(SPI1, ENABLE);

	// 9. 配置 NVIC 中断(DMA1 Channel2 和 Channel3)
	NVIC_InitStruct.NVIC_IRQChannel = DMA1_Channel2_IRQn;
	NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0;
	NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
	NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
	NVIC_Init(&NVIC_InitStruct);

	NVIC_InitStruct.NVIC_IRQChannel = DMA1_Channel3_IRQn;
	NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0;
	NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1;
	NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
	NVIC_Init(&NVIC_InitStruct);
}

/**
 * @brief 启动一次 DMA 接收(从SPI从机读取数据到缓冲区)
 * @note  SPI是主从同步通信,主机要产生时钟才能读取从机数据。
 *        因此需要同时发送哑元数据(0xFF)来产生时钟。
 */
void SPI_DMA_StartRead(void)
{
	g_spi_rx_done = 0;

	// 设置DMA接收长度
	DMA_Cmd(DMA1_Channel2, DISABLE);
	DMA1_Channel2->CNDTR = BUFFER_SIZE;
	DMA_Cmd(DMA1_Channel2, ENABLE);

	// 启动SPI的DMA发送(发送哑元数据产生时钟)
	// 注意:发送和接收DMA需同时启动
	DMA_Cmd(DMA1_Channel3, DISABLE);
	DMA1_Channel3->CNDTR = BUFFER_SIZE;
	DMA1_Channel3->CMAR = (uint32_t)g_spi_buffer; // 复用缓冲区作为发送源
	DMA_Cmd(DMA1_Channel3, ENABLE);
}

/**
 * @brief 启动一次 DMA 发送(将缓冲区数据写入SPI从机)
 * @param data 数据缓冲区指针
 * @param len  数据长度(字节)
 */
void SPI_DMA_StartWrite(uint8_t *data, uint16_t len)
{
	if (len == 0) return;

	g_spi_tx_done = 0;

	// 设置DMA发送源地址和长度
	DMA_Cmd(DMA1_Channel3, DISABLE);
	DMA1_Channel3->CMAR = (uint32_t)data;
	DMA1_Channel3->CNDTR = len;
	DMA_Cmd(DMA1_Channel3, ENABLE);

	// 注意:发送DMA启动后,SPI硬件会自动产生时钟并发送数据
	// 接收DMA可以不用开启(如果不需要读取从机返回的数据)
}

/**
 * @brief DMA1 Channel2 中断(SPI1_RX 接收完成)
 */
void DMA1_Channel2_IRQHandler(void)
{
	if (DMA_GetITStatus(DMA1_IT_TC2)) {
		DMA_ClearITPendingBit(DMA1_IT_TC2);
		// 置位接收完成标志(通知主循环处理数据)
		g_spi_rx_done = 1;
	}
}

/**
 * @brief DMA1 Channel3 中断(SPI1_TX 发送完成)
 */
void DMA1_Channel3_IRQHandler(void)
{
	if (DMA_GetITStatus(DMA1_IT_TC3)) {
		DMA_ClearITPendingBit(DMA1_IT_TC3);
		// 置位发送完成标志
		g_spi_tx_done = 1;
	}
}

③在Core组中添加process.h文件。

#ifndef __PROCESS_H
#define __PROCESS_H

#include <stdint.h>

void Process_CopyData(uint8_t *src, uint8_t *dst, uint32_t len);

#endif

④在Core组中添加process.c文件。

#include "process.h"

void Process_CopyData(uint8_t *src, uint8_t *dst, uint32_t len)
{
	uint32_t i;
	for (i = 0; i < len; i++) {
		dst[i] = src[i];
	}
}

(4)更新main.c文件,在进入主循环前完成外设和时钟的初始化,在主循环内轮询DMA中断事件,有则进入数据处理逻辑,无则跳过。

#include "stm32f10x.h"
#include "spi_dma.h"
#include "process.h"

// 临时发送缓冲区(用于存放处理后的数据)
static uint8_t g_tx_buffer[BUFFER_SIZE];

typedef enum {
	STATE_IDLE,              	// 空闲
	STATE_WAITING_RX,        	// 等待DMA接收完成
	STATE_PROCESSING,        	// CPU处理数据中
	STATE_WAITING_TX        	// 等待DMA发送完成
} AppState_t;

static AppState_t app_state = STATE_IDLE;

// 超时计数器
#define TIMEOUT_THRESHOLD    10000   // 约100ms
static uint32_t timeout_counter = 0;

static void HandleTimeout(void)
{
	// 停止SPI传输
	SPI_Cmd(SPI1, DISABLE);

	// 清除DMA状态
	DMA_Cmd(DMA1_Channel2, DISABLE);
	DMA_Cmd(DMA1_Channel3, DISABLE);
	DMA_ClearFlag(DMA1_FLAG_TC2 | DMA1_FLAG_TC3);

	// 清除标志位
	g_spi_rx_done = 0;
	g_spi_tx_done = 0;
	timeout_counter = 0;

	// 重新使能SPI
	SPI_Cmd(SPI1, ENABLE);

	// 重新开始接收
	SPI_DMA_StartRead();
	app_state = STATE_WAITING_RX;
}

void SystemClock_Config(void)
{
	ErrorStatus HSEStartUpStatus;
	RCC_DeInit();
	RCC_HSEConfig(RCC_HSE_ON);
	HSEStartUpStatus = RCC_WaitForHSEStartUp();
	if (HSEStartUpStatus == SUCCESS) {
		RCC_HCLKConfig(RCC_SYSCLK_Div1);
		RCC_PCLK2Config(RCC_HCLK_Div1);
		RCC_PCLK1Config(RCC_HCLK_Div2);
		RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_9);
		RCC_PLLCmd(ENABLE);
		while (RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET);
		RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK);
		while (RCC_GetSYSCLKSource() != 0x08);
	}
}

int main(void)
{
	// 1. 系统时钟初始化
	SystemClock_Config();

	// 2. 初始化 SPI 和 DMA
	SPI_DMA_Init();

	// 3. 触发第一次 DMA 接收
	SPI_DMA_StartRead();
	app_state = STATE_WAITING_RX;
	timeout_counter = 0;

	while (1) {
		switch (app_state) {
		case STATE_WAITING_RX:
			if (g_spi_rx_done) {
				g_spi_rx_done = 0;
				timeout_counter = 0;
				app_state = STATE_PROCESSING;
			}
			else {
				timeout_counter++;
				if (timeout_counter > TIMEOUT_THRESHOLD) {
					HandleTimeout();
				}
			}
			break;

		case STATE_PROCESSING:
			// CPU处理数据:复制到发送缓冲区
			Process_CopyData((uint8_t*)g_spi_buffer, g_tx_buffer, BUFFER_SIZE);

			// 触发DMA发送
			SPI_DMA_StartWrite(g_tx_buffer, BUFFER_SIZE);
			timeout_counter = 0;
			app_state = STATE_WAITING_TX;
			break;

		case STATE_WAITING_TX:
			if (g_spi_tx_done) {
				g_spi_tx_done = 0;
				timeout_counter = 0;

				// 一轮完成,开启下一轮接收
				SPI_DMA_StartRead();
				app_state = STATE_WAITING_RX;
			}
			else {
				timeout_counter++;
				if (timeout_counter > TIMEOUT_THRESHOLD) {
					HandleTimeout();
				}
			}
			break;

		case STATE_IDLE:
		default:
			break;
		}

		// ============================================================
		// 在这里可以插入其它非阻塞任务
		// ============================================================
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Zevalin爱灰灰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值