本学期第一次培训要求用单片机实现1到40KHz的单步进变频正弦波输出,本菜鸡一开始用了通过修改定时器预分频系数和自动重装载值改变输出波表频率、修改定时器 + DDS两种方法,但实现的效果不太理想,不仅波形不好看,如37KHz的频率也不大准。后边查阅资料、与朋友们探讨,最终决定尝试用DMA双缓冲+DDS实现波形发生器,实现效果不仅波形好看,频率也相对准确。
网上关于stm32的DMA双缓冲实现资料不多,在这里写下自己实现的两种方法,一方面是为了以后用到时候自己能够很快找到,一方面是希望可以帮助一下也想用双缓冲的网友。
( 在此先感谢一下涵哥、果子等友人的激情相助(*^▽^*) )
两种方法如下:
一、DMA半满、全满中断实现
@Yogurt
首先是配置,配置DAC的DMA。
这里我把DMA绑在了TIM4上,大家可以根据自己需要更改。
这里为了输出频率更高,把时钟频率拉到了最大。
PS:
这里说明一下,涉及中断,最好把DAC的DMA中断优先级设置得高一点,免得出现什么奇奇怪怪的问题。
接下来就是代码了,代码相对简单,主要是用到了dma中断中HAL库自带的两个回调函数:
void HAL_DAC_ConvHalfCpltCallbackCh1(DAC_HandleTypeDef *hdac); //半满中断回调
void HAL_DAC_ConvCpltCallbackCh1(DAC_HandleTypeDef *hdac); //全满中断回调
半满全满顾名思义,如用语句HAL_DAC_Start_DMA(&hdac, DAC1_CHANNEL_1, (uint32_t*)dacBuf, 4000, DAC_ALIGN_12B_R); 则在DMA传输数据到 4000/2=2000 即为dacBuf长度的一半时,就进入一次半满中断回调,当传输数据到第4000个时,即一整次传输完成,则进入全满中断回调。
两回调函数是弱函数,需自己重写,后把自己的例如DDS的代码写到其中就好了。这两个回调函数在HAL_DAC_Start_DMA函数内部,大家可以自己找一下。
这里提醒一点:一旦调用HAL_DAC_Start_DMA,就自动开启了DMA的半满、全满、错误中断,大家开启DMA且不用中断的时候,最好自己手动停止一下,就像下方把it.c文件中的这句注释一下(笔者也不知道直接这样注释会有什么问题,如果有知情人士麻烦联系一下笔者更正)。
大概代码流程如下:
1. 主函数
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_USART1_UART_Init();
MX_DMA_Init();
MX_DAC_Init();
MX_TIM3_Init();
MX_TIM4_Init();
MX_TIM6_Init();
MX_USART2_UART_Init();
/* USER CODE BEGIN 2 */
// delay_init(168);
HAL_TIM_Base_Start(&htim4);
HAL_DAC_Start_DMA(&hdac, DAC1_CHANNEL_1, (uint32_t*)dacBuf, SINN, DAC_ALIGN_12B_R); //¿ªÆôdac1£¬Êä³öÕýÏÒ²¨
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
没错,主函数中只用写HAL_TIM_Base_Start(&htim4);
HAL_DAC_Start_DMA(&hdac, DAC1_CHANNEL_1, (uint32_t*)dacBuf, SINN, DAC_ALIGN_12B_R);两句。
这里需要注意一下,DMA初始化需要放在DAC初始化之前,不然会有问题。
2.自己的半满和全满中断
#define SINN 4000
double scrb=0.5; //幅度参数
uint16_t ftw=10; //步进
uint16_t oudress; //地址
uint16_t sinI; //输出正弦波数组索引
void HAL_DAC_ConvHalfCpltCallbackCh1(DAC_HandleTypeDef *hdac) //半满中断回调
{
u16 i=0;
for(i=0; i<2000; i++) //在半满回调中更改前半个周期的数据
{
dacBuf[sinI] = (sinBuf[oudress]-1862)*scrb+2062; //循环里边的是DDS
sinI++; //大家可以自己去了解DDS原理及应用
oudress+=ftw;
if(oudress>4000 || oudress==4000) oudress%=4000;
if(sinI==4000) sinI=0;
}
}
void HAL_DAC_ConvCpltCallbackCh1(DAC_HandleTypeDef *hdac) //全满中断回调
{
u16 i=0;
for(i=0; i<2000; i++) //在全满回调中更改后半个周期的数据
{
dacBuf[sinI] = (sinBuf[oudress]-1862)*scrb+2062;
sinI++;
oudress+=ftw;
if(oudress>4000 || oudress==4000) oudress%=4000;
if(sinI==4000) sinI=0;
}
}
更改频率的话,笔者是通过一定时间后在定时器中断中修改步进ftw的值进行修变频。
以上就是实现DMA双缓冲的第一种方法,相对简单一点。
二、相对正宗双缓冲
@果子
这种方法cube的配置和第一种一样,配置好DMA和定时器就好了。
这里不讲太多原理,就讲一下实现方法,相关原理笔者会把自己觉得讲的好的大佬的文章链接放在下边,大家有需要的可以看看。
1.主函数
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_USART1_UART_Init();
MX_DMA_Init();
MX_DAC_Init();
MX_TIM2_Init();
MX_TIM3_Init();
/* USER CODE BEGIN 2 */
__HAL_DAC_ENABLE(&hdac, DAC_CHANNEL_1); //提前使能DAC外设
HAL_NVIC_SetPriority(DMA1_Stream5_IRQn,2,0);
HAL_NVIC_EnableIRQ(DMA1_Stream5_IRQn);
MY_Buffer_Init(); //初始化两个缓冲区的数据
HAL_DAC_Start_DMA(&hdac,DAC_CHANNEL_2,(uint32_t *) MY_M0_Buffer,50,DAC_ALIGN_12B_R); //开启DAC
HAL_DMAEx_MultiBufferStart_IT(&hdma_dac2, (u32)&DAC1->DHR12R1, (u32)Pm0, (u32)Pm1, 100); //开启DMA双缓冲模式中断
HAL_TIM_Base_Start(&htim2);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
LED1_T;
HAL_Delay(1000);
LED0_T;
}
/* USER CODE END 3 */
}
2.一些定义
/* USER CODE BEGIN 0 */
void MY_Buffer_Init(void); //初始化两个缓冲区的数据
volatile float MY_U=3.3; //用volatile是防止寄存器优化使程序出问题
uint16_t outadress=0; //地址
uint16_t sinBuf[4000]={2047,2050,2053,2057,2060,2063,.....}; //随便一个正弦波表,这里太长就不放了
uint16_t MY_M0_Buffer[50]; //DMA第一个缓冲区
uint16_t MY_M1_Buffer[50]; //DMA第二个缓冲区
uint16_t *Pm0=MY_M0_Buffer, *Pm1=MY_M1_Buffer;
uint16_t MY_F=40; //步进
/* USER CODE END 0 */
3.相关函数
/* USER CODE BEGIN 4 */
void dma_m0_rxcplt_callback(DMA_HandleTypeDef *hdma) //自己写的第一个回调函数,相当于半满
{
u8 i=0;
for(i=0;i<50;i++) //DDS
{
MY_M0_Buffer[i] = sinBuf[outadress];
outadress = (outadress + MY_F)%4000;
}
}
void dma_m1_rxcplt_callback(DMA_HandleTypeDef *hdma) //自己写的第二个回调函数,相当于全满
{
u8 i=0;
for(i=0;i<50;i++)
{
MY_M1_Buffer[i] = sinBuf[outadress];
outadress = (outadress + MY_F)%4000;
}
}
void MY_Buffer_Init(void) //初始化缓冲区数据
{
u8 i=0;
for(i=0;i<50;i++)
{
MY_M0_Buffer[i] = sinBuf[outadress];
outadress = (outadress + MY_F)%4000;
}
for(i=0;i<50;i++)
{
MY_M1_Buffer[i] = sinBuf[outadress];
outadress = (outadress + MY_F)%4000;
}
}
/* USER CODE END 4 */
4.重点来啦,下边的要注意一下,有些hal库内部的配置要改
(1)首先进入HAL_DMAEx_MultiBufferStart_IT的函数定义,在下方文件中可以找到
(2)加入自己的回调函数定义
(3)在合适的地方加入这两句
(4)还得先失能
总的如下
/**
* @brief Starts the multi_buffer DMA Transfer with interrupt enabled.
* @param hdma pointer to a DMA_HandleTypeDef structure that contains
* the configuration information for the specified DMA Stream.
* @param SrcAddress The source memory Buffer address
* @param DstAddress The destination memory Buffer address
* @param SecondMemAddress The second memory Buffer address in case of multi buffer Transfer
* @param DataLength The length of data to be transferred from source to destination
* @retval HAL status
*/
extern void dma_m0_rxcplt_callback(DMA_HandleTypeDef *hdma);
extern void dma_m1_rxcplt_callback(DMA_HandleTypeDef *hdma);
HAL_StatusTypeDef HAL_DMAEx_MultiBufferStart_IT(DMA_HandleTypeDef *hdma, uint32_t SrcAddress, uint32_t DstAddress, uint32_t SecondMemAddress, uint32_t DataLength)
{
HAL_StatusTypeDef status = HAL_OK;
/* Check the parameters */
assert_param(IS_DMA_BUFFER_SIZE(DataLength));
/* Memory-to-memory transfer not supported in double buffering mode */
if (hdma->Init.Direction == DMA_MEMORY_TO_MEMORY)
{
hdma->ErrorCode = HAL_DMA_ERROR_NOT_SUPPORTED;
return HAL_ERROR;
}
// /*Current memory buffer used is Memory 1 callback*/
hdma->XferCpltCallback=dma_m0_rxcplt_callback; //第一个缓冲区填满后调用
// /*Current memory buffer used is Memory 0 callback*/
hdma->XferM1CpltCallback=dma_m1_rxcplt_callback; //第二个缓冲区填满后调用
/* Check callback functions */
if ((NULL == hdma->XferCpltCallback) || (NULL == hdma->XferM1CpltCallback) || (NULL == hdma->XferErrorCallback))
{
hdma->ErrorCode = HAL_DMA_ERROR_PARAM;
return HAL_ERROR;
}
/* Process locked */
__HAL_LOCK(hdma);
/*Enable the peripheral*/
__HAL_DMA_DISABLE(hdma); //先失能DMA后边的设置才会生效
if(HAL_DMA_STATE_READY == hdma->State)
{
/* Change DMA peripheral state */
hdma->State = HAL_DMA_STATE_BUSY;
/* Initialize the error code */
hdma->ErrorCode = HAL_DMA_ERROR_NONE;
/* Enable the Double buffer mode */
hdma->Instance->CR |= (uint32_t)DMA_SxCR_DBM;
/* Configure DMA Stream destination address */
hdma->Instance->M1AR = SecondMemAddress;
/* Configure the source, destination address and the data length */
DMA_MultiBufferSetConfig(hdma, SrcAddress, DstAddress, DataLength);
/* Clear all flags */
__HAL_DMA_CLEAR_FLAG (hdma, __HAL_DMA_GET_TC_FLAG_INDEX(hdma));
__HAL_DMA_CLEAR_FLAG (hdma, __HAL_DMA_GET_HT_FLAG_INDEX(hdma));
__HAL_DMA_CLEAR_FLAG (hdma, __HAL_DMA_GET_TE_FLAG_INDEX(hdma));
__HAL_DMA_CLEAR_FLAG (hdma, __HAL_DMA_GET_DME_FLAG_INDEX(hdma));
__HAL_DMA_CLEAR_FLAG (hdma, __HAL_DMA_GET_FE_FLAG_INDEX(hdma));
/* Enable Common interrupts*/
hdma->Instance->CR |= DMA_IT_TC | DMA_IT_TE | DMA_IT_DME;
hdma->Instance->FCR |= DMA_IT_FE;
if((hdma->XferHalfCpltCallback != NULL) || (hdma->XferM1HalfCpltCallback != NULL))
{
hdma->Instance->CR |= DMA_IT_HT;
}
/* Enable the peripheral */
__HAL_DMA_ENABLE(hdma);
}
else
{
/* Process unlocked */
__HAL_UNLOCK(hdma);
/* Return error status */
status = HAL_BUSY;
}
return status;
}
三、总结
以上就是自己实践过的两种双缓冲实现方法,需要源程序的uu可以私信哈。
还有这是DDS原理博客推:
https://www.cnblogs.com/puyu9495/archive/2022/02/19/15914090.html
大家有需要自取。加油加油!