思路
简单说下思路,STM32自带一个两路独立输出的DAC。要输出正弦波,实质是要控制 DAC 以 v=sin(t)的正弦函数关系输出电压,其中 v 为电压输出,t 为时间。 而由于模拟信号连续而数字信号是离散的,所以使用 DAC 产生正弦波时,只能按一定时间间隔输出正弦曲线上的点,在该时间段内输出相同的电压值,若缩短时间间隔,提高单个周期内的输出点数,可以得到逼近连续正弦波的图形,见下图 37-3,若在外部电路加上适当的电容滤波,可得到更完美的图形。

由于正弦曲线是周期函数,所以只需要得到单个周期内的数据后按周期重复即可,而单个周期内取样输出的点数又是有限的,所以为了得到呈 v=sin(t)函数关系电压值的数据通常不会实时计算获取,而是预先计算好函数单个周期内的电压数据表,并且转化成以 DAC 寄存器表示的值。 如 sin 函数值的范围为[-1: +1],而 STM32 的 DAC 输出电压范围为[0~3.3]V,按 12 位 DAC 分辨率表示的方法,可写入寄存器的最大值为 212 = 4096,即范围为[0:4096]。所以,实际输出时,会进行如下处理:
- 抬升 sin 函数的输出为正值:v = sin(t)+1 ,此时,v 的输出范围为[0:2];
- 扩展输出至 DAC 的全电压范围: v = 3.3*(sin(t)+1)/2 ,此时,v 的输出范围为[0:3.3], 正是 DAC 的电压输出范围,扩展至全电压范围可以充分利用 DAC 的分辨率;
- 把电压值以 DAC 寄存器的形式表示:Reg_val = 212/3.3 * v = 211*(sin(t)+1),此时,存储到 DAC 寄存器的值范围为[0:4096];
- 实践证明,在 sin(t)的单个周期内,取 32 个点进行电压输出已经能较好地还原正弦波形,所以在 t∈[0:2π]区间内等间距根据上述 Reg_val 公式运算得到 32 个寄存器值,即可得到正弦波表;
- 控制 DAC 输出时,每隔一段相同的时间从上述正弦波表中取出一个新数据进行输出,即可输出正弦波。改变间隔时间的单位长度,可以改变正弦波曲线的周期。
所以正弦波表如下
uint16_t Sine12bit[32] = {
2048 , 2460 , 2856 , 3218 , 3532 , 3786 , 3969 , 4072 ,
4093 , 4031 , 3887 , 3668 , 3382 , 3042 ,2661 , 2255 ,
1841 , 1435 , 1054 , 714 , 428 , 209 , 65 , 3 ,
24 , 127 , 310 , 564 , 878 , 1240 , 1636 , 2048
};
CubeMX以及代码的配置
DAC

OUT1/2 Configuration:
对应着两个输出通道
External Trigger:
是否使用外部中断触发
外部中断EXTI9 触发
就是使用外部中断来触发DAC。Output Buffer:
使能DAC输出缓存。
DAC 集成了 2 个输出缓存,可以用来减少输出阻抗,无需外部运放即可直接驱动外部负载。每个 DAC 通道输出缓存可以通过设置 DAC_CR 寄存器的 BOFFx 位来使能或者关闭。如果带载能力还不行,后面就接一个电压跟随器,选择运放一定要选择电流大的型号。
使能输出缓冲后,DAC 输出的最小电压为 0.2V,最大电压为 VREF±0.2,而未使能输出缓冲则输出可达到0V。Trigger:
选择DAC的触发方式
Timer 2/4/5/6/7/8 Trigger Out event
定时器触发,利用这种方式可以输出特定的波形。在这里我们选择定时器2。Software trigger
软件触发,在本模式下,向 DAC_SWTRIGR 寄存器写入配置即可触发信号进行转换。Wave generation mode:
波形发生器的选择,有三角波,和噪声,由于这里我们要输出正弦波,所以就不选择了
DMA的配置
Priority:
当发生多个 DMA 通道请求时,就意味着有先后响应处理的顺序问题,这个就由仲裁器也管理。仲裁器管理 DMA 通道请求分为两个阶段。第一阶段属于软件阶段,可以在 DMA_CCRx 寄存器中设置,有 4 个等级:非常高、高、中和低四个优先级。第二阶段属于硬件阶段,如果两个或以上的 DMA 通道请求设置的优先级一样,则他们优先级取决于通 道编号,编号越低优先权越高,比如通道 0 高于通道 1。在大容量产品和互联型产品中,DMA1 控制器拥有高于 DMA2 控制器的优先级。
Mode:
Normal
表示单次传输,传输一次后终止传输。Circular
表示循环传输,传输完成后又重新开始继续传输,不断循环永不停止。(还是从内存的首地址)Increment Address:
Peripheral
表示外设地址自增。Memory
表示内存地址自增。Data Width:
Byte
一个字节。Half Word
半个字,等于两字节。Word
一个字,等于四字节。
TIM

- Prescaler(时钟预分频数):
则驱动计数器的时钟 CK_CNT = CK_INT(即84MHz)/(0+1) = 84MHz
即不分频 - Counter Mode(计数模式):Up(向上计数模式)
- Counter Period(自动重装载值):525-1
- auto-reload-preload(自动重装载):Disable(不使能)(PWM时使用)
- TRGO Parameters(触发输出):Update Event(更新事件)
在定时器的定时时间到达的时候输出一个信号(如:定时器更新产生TRGO信号来触发ADC的同步转换)
关于DAC输出的频率计算

参考野火STM32F103手册来计算
STM32F407时钟如图


f = 84000000/1 * 525 * 32 = 5000 Hz
代码
/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
uint16_t Sine12bit[32] = {
2048 , 2460 , 2856 , 3218 , 3532 , 3786 , 3969 , 4072 ,
4093 , 4031 , 3887 , 3668 , 3382 , 3042 ,2661 , 2255 ,
1841 , 1435 , 1054 , 714 , 428 , 209 , 65 , 3 ,
24 , 127 , 310 , 564 , 878 , 1240 , 1636 , 2048
};
/* USER CODE END PTD */
/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
/* USER CODE END PD */
/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */
/* USER CODE END PM */
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */
/* USER CODE END PV */
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */
/* USER CODE END PFP */
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
/* USER CODE END 0 */
/**
* @brief The application entry point.
* @retval int
*/
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_DMA_Init();
MX_USART1_UART_Init();
MX_USART2_UART_Init();
MX_DAC_Init();
MX_TIM2_Init();
/* USER CODE BEGIN 2 */
// uint8_t HexEnd[] = {0xff, 0xff, 0xff};
// uint8_t aTxBuffer[100] = "add 1,0,100";
HAL_TIM_Base_Start(&htim2);//开启DAC
HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, (uint32_t *)Sine12bit, 32, DAC_ALIGN_12B_R);//使能DAC——DMA方式
/* USER CODE END 2 */
结果
一点小总结
DAC的DMA模式其实就是每当触发源触发一次,DMA就像DAC的DHRX寄存器更新一次数据,DAC也将DHRX中的数据再传输到DORX寄存器中,转换为电压输出
HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, (uint32_t *)Sine12bit, 32, DAC_ALIGN_12B_R);//使能DAC——DMA方式
实际上32就是以
Sine12bit
数组的首地址为首地址,依次向后的32组数据,每次触发源触发一次DMA就传输一组数据,直到32组数据都传输完成,这时再从头开始传输数据,这样循环下去,就可以配合TIM来实现输出正弦波的功能通过这次实验,以后也可以尝试利用TIM来作为触发其他外设启动的一个触发源(ADC同理)