一、单片机处理数据的几种方案
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;
}
// ============================================================
// 在这里可以插入其它非阻塞任务
// ============================================================
}
}
第四章 数据收发策略&spm=1001.2101.3001.5002&articleId=163421852&d=1&t=3&u=744317d4ad1a4228b4176cb21d3a8762)
1589

被折叠的 条评论
为什么被折叠?



