STM32DAC输出可调电压、三角波、正弦波
本期内容我们将学习stm32DAC的原理和使用方法
DAC简介
DAC,全称:Digital-to-Analog Converter,指数字/模拟转换器。可以将数字量转换为模拟量进行输出,原理与ADC相反。由于stm32F411RCT6上面未搭载DAC模块,所以我们本期内容以f103RCT6做演示。
stm32F103RCT6上只有一个DAC,但是有两个输出通道(分别对应PA4、PA5),可以同时工作并输出,并带有输出缓存功能,可用来降低输出阻抗并在不增加外部运算放大器的情况下直接驱动外部负载。
不知道什么是DAC的可以看看 蓝桥杯单片机学习11——PCF8591A/D&D/A转换芯片
下面是DAC的一些重要知识
1.主要特性
- 两个 DAC 转换器:各对应一个输出通道
- 12 位模式下数据采用左对齐或右对齐
- 同步更新功能
- 生成噪声波
- 生成三角波
- DAC 双通道单独或同时转换
- 每个通道都具有 DMA 功能
- DMA 下溢错误检测
- 通过外部触发信号进行转换
- 输入参考电压 VREF+
2.DAC时钟频率
- F1系列为36MHz
- F4系列为42MHz(挂在APB1)或45MHz(挂在APB2上)
- F7系列为54MHz
- H7系列为120MHz
3.建立时间
- 建立时间表示将一个数字量转换为稳定模拟信号所需的时间
- F1、4、7系列都是3us
- H7系列建立时间为1.7us
4.输出电压计算
- DAC输出电压
- DAC输出电压 = ( DORX / 2^ n) * (Vref+ - Vref-)
其中:DORX为DAC的数字量 ,n为DAC的分辨率,Vref为参考电压
输出可调电压
1.打开CubeMX,进入DAC选项,进行如下配置
其他部分配置,这里不做介绍
在main函数中添加以下代码
int main(void) { /* USER CODE BEGIN 1 */ uint8_t i=0; //计数变量 uint16_t DAC_Data=0; //DAC数字量 float DAC_Val=0; //DAC输出电压 /* 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_DAC_Init(); MX_USART1_UART_Init(); /* USER CODE BEGIN 2 */ HAL_DAC_Start(&hdac,DAC_CHANNEL_1); /* USER CODE END 2 */ /* Infinite loop */ /* USER CODE BEGIN WHILE */ while (1) { for(i=0;i<10;i++) { HAL_DAC_SetValue(&hdac,DAC_CHANNEL_1,DAC_ALIGN_12B_R,i*4096/10); //设置DAC输出电压 = 0.33*i DAC_Data = HAL_DAC_GetValue(&hdac,DAC_CHANNEL_1); //读取DAC的数字量 printf("DAC_Data = %d ",DAC_Data); //打印数字量 DAC_Val = (float)DAC_Data/4096*3.3; //计算应该输出的电压值 printf("DAC_Val = %f ",DAC_Val); //打印电压值 printf("\r\n"); //换行 HAL_Delay(1000); //延时1s } /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ } /* USER CODE END 3 */ }
得到输出结果:
输出正弦波
stm32的DAC是没有直接输出正弦波的功能,所以我们只能模拟一个正弦波,并且进行输出,具体思路如下:
正弦函数是一个连续变化的函数,想要输出一个正弦函数的波形,我们通常会将一个周期以时间间隔t进行很多次的等分,并计算出每个时间节点对应的输出电压值,这样我们只需要在每一个时间节点,输出对应的电压(写入对应的数字量),就可以实现正弦函数波形输出,当然,如果时间间隔t越小,即等分的次数越多,输出的波形越接近正弦函数,随之而来的,单片机的CPU占用率就越大。
当然,我们也可以提前对输出的波形进行等分,计算出对应的数字量,再通过数组定义,可以减轻CPU的负担。
或者在单片机上电就开始计算对应的电压值,并存放到数组中。
下面我们来看具体应用:
- CubeMX配置
其他部分配置,这里不做介绍
//正弦波数字量数组,这里进行512等分,适用于12为分辨率的情况 uint16_t Sin_512_bit[512] ={ 2073, 2098, 2123, 2148, 2173, 2198, 2223, 2248, 2273, 2298, 2323, 2348, 2373, 2398, 2422, 2447, 2472, 2496, 2521, 2545, 2569, 2594, 2618, 2642, 2666, 2690, 2714, 2737, 2761, 2785, 2808, 2831, 2854, 2877, 2900, 2923, 2946, 2968, 2991, 3013, 3035, 3057, 3079, 3100, 3122, 3143, 3164, 3185, 3206, 3227, 3247, 3267, 3288, 3307, 3327, 3347, 3366, 3385, 3404, 3423, 3441, 3460, 3478, 3496, 3513, 3531, 3548, 3565, 3582, 3598, 3615, 3631, 3646, 3662, 3677, 3692, 3707, 3722, 3736, 3750, 3764, 3778, 3791, 3804, 3817, 3829, 3842, 3854, 3865, 3877, 3888, 3899, 3909, 3920, 3930, 3940, 3949, 3958, 3967, 3976, 3984, 3992, 4000, 4007, 4014, 4021, 4028, 4034, 4040, 4046, 4051, 4056, 4061, 4065, 4069, 4073, 4077, 4080, 4083, 4086, 4088, 4090, 4092, 4093, 4094, 4095, 4095, 4095, 4095, 4095, 4094, 4093, 4092, 4090, 4088, 4086, 4083, 4080, 4077, 4073, 4069, 4065, 4061, 4056, 4051, 4046, 4040, 4034, 4028, 4021, 4014, 4007, 4000, 3992, 3984, 3976, 3967, 3958, 3949, 3940, 3930, 3920, 3909, 3899, 3888, 3877, 3865, 3854, 3842, 3829, 3817, 3804, 3791, 3778, 3764, 3750, 3736, 3722, 3707, 3692, 3677, 3662, 3646, 3631, 3615, 3598, 3582, 3565, 3548, 3531, 3513, 3496, 3478, 3460, 3441, 3423, 3404, 3385, 3366, 3347, 3327, 3307, 3288, 3267, 3247, 3227, 3206, 3185, 3164, 3143, 3122, 3100, 3079, 3057, 3035, 3013, 2991, 2968, 2946, 2923, 2900, 2877, 2854, 2831, 2808, 2785, 2761, 2737, 2714, 2690, 2666, 2642, 2618, 2594, 2569, 2545, 2521, 2496, 2472, 2447, 2422, 2398, 2373, 2348, 2323, 2298, 2273, 2248, 2223, 2198, 2173, 2148, 2123, 2098, 2073, 2047, 2022, 1997, 1972, 1947, 1922, 1897, 1872, 1847, 1822, 1797, 1772, 1747, 1722, 1697, 1673, 1648, 1623, 1599, 1574, 1550, 1526, 1501, 1477, 1453, 1429, 1405, 1381, 1358, 1334, 1310, 1287, 1264, 1241, 1218, 1195, 1172, 1149, 1127, 1104, 1082, 1060, 1038, 1016, 995, 973, 952, 931, 910, 889, 868, 848, 828, 807, 788, 768, 748, 729, 710, 691, 672, 654, 635, 617, 599, 582, 564, 547, 530, 513, 497, 480, 464, 449, 433, 418, 403, 388, 373, 359, 345, 331, 317, 304, 291, 278, 266, 253, 241, 230, 218, 207, 196, 186, 175, 165, 155, 146, 137, 128, 119, 111, 103, 95 , 88 , 81 , 74 , 67 , 61 , 55 , 49 , 44 , 39 , 34 , 30 , 26 , 22 , 18 , 15 , 12 , 9 , 7 , 5 , 3 , 2 , 1 , 0 , 0 , 0 , 0 , 0 , 1 , 2 , 3 , 5 , 7 , 9 , 12 , 15 , 18 , 22 , 26 , 30 , 34 , 39 , 44 , 49 , 55 , 61 , 67 , 74 , 81 , 88 , 95 , 103, 111, 119, 128, 137, 146, 155, 165, 175, 186, 196, 207, 218, 230, 241, 253, 266, 278, 291, 304, 317, 331, 345, 359, 373, 388, 403, 418, 433, 449, 464, 480, 497, 513, 530, 547, 564, 582, 599, 617, 635, 654, 672, 691, 710, 729, 748, 768, 788, 807, 828, 848, 868, 889, 910, 931, 952, 973, 995, 1016, 1038, 1060, 1082, 1104, 1127, 1149, 1172, 1195, 1218, 1241, 1264, 1287, 1310, 1334, 1358, 1381, 1405, 1429, 1453, 1477, 1501, 1526, 1550, 1574, 1599, 1623, 1648, 1673, 1697, 1722, 1747, 1772, 1797, 1822, 1847, 1872, 1897, 1922, 1947, 1972, 1997, 2022, 2047, }; 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_DAC_Init(); MX_TIM2_Init(); /* USER CODE BEGIN 2 */ HAL_TIM_Base_Start(&htim2); //开启定时器2 HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, (uint32_t *)Sin_512_bit, 512, DAC_ALIGN_12B_R); //通过DMA开启DAC转换 /* USER CODE END 2 */ //顶上去2每溢出一次,就会从Sin_512_bit搬运一次数据到DAC进行转换输出 /* Infinite loop */ /* USER CODE BEGIN WHILE */ while (1) { /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ } /* USER CODE END 3 */ }
这里我们采样的是提前计算出DAC转换数字量的方式,因为要输出的波形越好看,等分数量会越多,单片机计算量也会随之增加,为了减轻单片机负担和代码运行效率,我们选择提前计算。
演示效果如下:
这里讲一下生成波形的频率计算:
我们配置定时器2时,选择时钟不分频,计数值最大值为19,也就是说定时器每次产生更新事件频率为:
72MHz/(0+1)* (19+1)= 3.6MHz
一个波形被平均分为512等分,也就是所一个周期内包含512个更新事件:
那么产生波形的频率 = 3.6MHz / 512 = 7031.25Hz,与图中7.04KHz相近
//,这里进行512等分,适用于12为分辨率的情况
正弦波数字量数组可以借助工具Excel生成,我就是这么干的,网上有教程,也可以问问万能的GPT,或者在系统上电前计算出。代码如下:
- g_dac_sin_buf是存放DAC正弦波数字量的数组,长度可自己定义
- 代码来自正点原子官方提供代码,亲测好用
/** * @brief 产生正弦波序列函数 * @note 需保证: maxval > samples/2 * @param maxval : 最大值(0 < maxval < 2048) * @param samples: 采样点的个数 * @retval 无 */ void dac_creat_sin_buf(uint16_t maxval, uint16_t samples) { uint8_t i; float outdata = 0; /* 存放计算后的数字量 */ float inc = (2 * 3.1415962) / samples; /* 计算相邻两个点的x轴间隔 */ if(maxval <= (samples / 2))return ; /* 数据不合法 */ for (i = 0; i < samples; i++) { /* * 正弦波函数解析式:y = Asin(ωx + φ)+ b * 计算每个点的y值,将峰值放大maxval倍,并将曲线向上偏移maxval到正数区域 * 注意:DAC无法输出负电压,所以需要将曲线向上偏移一个峰值的量,让整个曲线都落在正数区域 */ outdata = maxval * sin(inc * i) + maxval; if (outdata > 4095) outdata = 4095; /* 上限限定 */ //printf("%f\r\n",outdata); g_dac_sin_buf[i] = outdata; } }
具体原理如下:、
讲完正弦波,我们来讲讲三角波。
输出三角波
三角波理论上可以通过正弦波的方式产生,这里就不做解释了,给大家提供一个DAC产生三角波的数组量数组吧
适用于12位分辨率情况下输出,等分次数为256
/*三角波数组,256个元素*/ uint16_t SanJiao_256_bit[256] = { 2048, 2080, 2112, 2144, 2176, 2208, 2240, 2272, 2304, 2336, 2368, 2400, 2432, 2464, 2496, 2528, 2560, 2592, 2624, 2656, 2688, 2720, 2752, 2784, 2816, 2848, 2880, 2912, 2944, 2976, 3008, 3040, 3072, 3104, 3136, 3168, 3200, 3232, 3264, 3296, 3328, 3360, 3392, 3424, 3456, 3488, 3520, 3552, 3584, 3616, 3648, 3680, 3712, 3744, 3776, 3808, 3840, 3872, 3904, 3936, 3968, 4000, 4032, 4064, 4095, 4063, 4031, 3999, 3967, 3935, 3903, 3871, 3839, 3807, 3775, 3743, 3711, 3679, 3647, 3615, 3583, 3551, 3519, 3487, 3455, 3423, 3391, 3359, 3327, 3295, 3263, 3231, 3199, 3167, 3135, 3103, 3071, 3039, 3007, 2975, 2943, 2911, 2879, 2847, 2815, 2783, 2751, 2719, 2687, 2655, 2623, 2591, 2559, 2527, 2495, 2463, 2431, 2399, 2367, 2335, 2303, 2271, 2239, 2207, 2175, 2143, 2111, 2079, 2047, 2015, 1983, 1951, 1919, 1887, 1855, 1823, 1791, 1759, 1727, 1695, 1663, 1631, 1599, 1567, 1535, 1503, 1471, 1439, 1407, 1375, 1343, 1311, 1279, 1247, 1215, 1183, 1151, 1119, 1087, 1055, 1023, 991, 959, 927, 895, 863, 831, 799, 767, 735, 703, 671, 639, 607, 575, 543, 511, 479, 447, 415, 383, 351, 319, 287, 255, 223, 191, 159, 127, 95, 63, 31, 0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, 480, 512, 544, 576, 608, 640, 672, 704, 736, 768, 800, 832, 864, 896, 928, 960, 992, 1024, 1056, 1088, 1120, 1152, 1184, 1216, 1248, 1280, 1312, 1344, 1376, 1408, 1440, 1472, 1504, 1536, 1568, 1600, 1632, 1664, 1696, 1728, 1760, 1792, 1824, 1856, 1888, 1920, 1952, 1984, 2016, };
前面提到DAC有输出三角波功能 接下来我们讲解DAC的三角波输出功能
- CubeMX配置
其他部分配置,这里不做介绍
代码如下;
int main(void) { /* USER CODE BEGIN 1 */ uint16_t i=0; /* 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_DAC_Init(); MX_TIM2_Init(); /* USER CODE BEGIN 2 */ HAL_TIM_Base_Start(&htim2); //开启定时器2 HAL_DAC_Start(&hdac,DAC_CHANNEL_1); //开启DAC转换 HAL_DACEx_TriangleWaveGenerate(&hdac,DAC_CHANNEL_1,DAC_TRIANGLEAMPLITUDE_2047); //设置幅值为1.65V,进行2047*2等分 /* USER CODE END 2 */ /* Infinite loop */ /* USER CODE BEGIN WHILE */ while (1) { /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ } /* USER CODE END 3 */ }
运行结果如下:
如图,输出三角波频率为481Hz ,幅值1.65V
计算推导如下:
三角波频率 = 72Mhz/36/(2*2047) = 488Hz,与结果相近。
当然你也可以通过以下代码产生三角波:
- 代码同样来源与正点原子
/** * @brief 设置DAC_OUT1输出三角波 * @note 输出频率 ≈ 1000 / (dt * samples) Khz, 不过在dt较小的时候,比如小于5us时, 由于delay_us * 本身就不准了(调用函数,计算等都需要时间,延时很小的时候,这些时间会影响到延时), 频率会偏小. * * @param maxval : 最大值(0 < maxval < 4096), (maxval + 1)必须大于等于samples/2 * @param dt : 每个采样点的延时时间(单位: us) * @param samples: 采样点的个数, samples必须小于等于(maxval + 1) * 2 , 且maxval不能等于0 * @param n : 输出波形个数,0~65535 * * @retval 无 */ void dac_triangular_wave(uint16_t maxval, uint16_t dt, uint16_t samples, uint16_t n) { uint16_t i, j; float incval; /* 递增量 */ float Curval; /* 当前值 */ if(samples > ((maxval + 1) * 2))return ; /* 数据不合法 */ incval = (maxval + 1) / (samples / 2); /* 计算递增量 */ for(j = 0; j < n; j++) { Curval = 0; HAL_DAC_SetValue(&g_dac_handle, DAC_CHANNEL_1, DAC_ALIGN_12B_R, Curval); /* 先输出0 */ for(i = 0; i < (samples / 2); i++) /* 输出上升沿 */ { Curval += incval; /* 新的输出值 */ HAL_DAC_SetValue(&g_dac_handle, DAC_CHANNEL_1, DAC_ALIGN_12B_R, Curval); delay_us(dt); } for(i = 0; i < (samples / 2); i++) /* 输出下降沿 */ { Curval -= incval; /* 新的输出值 */ HAL_DAC_SetValue(&g_dac_handle, DAC_CHANNEL_1, DAC_ALIGN_12B_R, Curval); delay_us(dt); } } }
不过,这个函数需要一直执行才能输出三角波,CPU占用率高达100%,不是很建议使用。具体原理如下: