1. DAC与ADC闭环验证系统概述在嵌入式系统开发中模拟信号的处理一直是工程师们需要面对的重要课题。STM32F103ZET6作为一款经典的ARM Cortex-M3内核微控制器其内置的12位DAC数模转换器和ADC模数转换器为模拟信号处理提供了硬件基础。这次我们要实现的DAC与ADC闭环验证系统简单来说就是让芯片自己产生一个电压信号然后再自己测量这个信号形成一个完整的自检回路。这种闭环验证在实际项目中非常实用。比如在工业控制领域我们需要确保输出的控制信号准确无误在医疗设备中精确的模拟信号输出更是关乎生命安全。通过这个实验我们不仅能验证硬件功能是否正常还能评估系统的整体精度。我做过不少类似项目发现这种自检机制往往能在早期就发现硬件设计或PCB布线的问题避免后期更大的损失。STM32F103ZET6有两个独立的12位DAC通道分别对应PA4和PA5引脚。DAC的参考电压通常直接使用芯片的Vref3.3V这意味着输出范围是0-3.3V。而ADC部分则有16个输入通道最高采样速率可达1MHz。在本次实验中我们将使用DAC通道1PA4输出模拟电压然后用ADC通道1PA1进行回读形成一个完整的闭环验证系统。2. 硬件配置与初始化2.1 时钟与GPIO配置任何STM32外设的使用都始于时钟使能。对于这个实验我们需要同时开启三个外设的时钟GPIOA因为DAC和ADC都用到了PA端口、DAC和ADC1。这里有个细节需要注意DAC挂在APB1总线上而ADC挂在APB2总线它们的时钟使能函数是不同的。// 时钟使能配置 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 使能GPIOA时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE); // 使能DAC时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); // 使能ADC1时钟GPIO配置上有个容易踩坑的地方虽然DAC输出和ADC输入都需要将GPIO设置为模拟输入模式但它们的原理完全不同。对于DAC引脚设置为模拟输入是为了关闭施密特触发器减少干扰而对于ADC引脚则是真正作为输入使用。我在早期项目中曾经混淆过这个概念导致信号质量很差。// GPIO初始化结构体配置 GPIO_InitTypeDef GPIO_InitStructure; // PA1 (ADC通道1) 配置 GPIO_InitStructure.GPIO_Pin GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; // 模拟输入 GPIO_Init(GPIOA, GPIO_InitStructure); // PA4 (DAC通道1) 配置 GPIO_InitStructure.GPIO_Pin GPIO_Pin_4; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; // 模拟输入 GPIO_Init(GPIOA, GPIO_InitStructure);2.2 DAC模块初始化DAC的初始化相对简单但有几个关键参数需要特别注意。首先是触发方式我们选择不使用硬件触发DAC_Trigger_None这样可以直接通过软件设置输出电压。其次是输出缓冲关闭缓冲DAC_OutputBuffer_Disable可以获得更高的输出阻抗但驱动能力会下降。根据我的经验在需要驱动低阻抗负载时应该开启缓冲但在精密测量场合最好关闭。DAC_InitTypeDef DAC_InitType; DAC_InitType.DAC_Trigger DAC_Trigger_None; // 不使用硬件触发 DAC_InitType.DAC_WaveGeneration DAC_WaveGeneration_None; // 不生成波形 DAC_InitType.DAC_LFSRUnmask_TriangleAmplitude DAC_LFSRUnmask_Bit0; // 不使用噪声或三角波 DAC_InitType.DAC_OutputBuffer DAC_OutputBuffer_Disable; // 关闭输出缓冲 DAC_Init(DAC_Channel_1, DAC_InitType); // 初始化DAC通道1 DAC_Cmd(DAC_Channel_1, ENABLE); // 使能DAC通道1为了方便电压设置我通常会封装一个设置电压的函数。这里有个实用技巧使用浮点数计算可以提高设置精度但要注意STM32F1没有硬件浮点单元频繁的浮点运算会影响性能。在实际项目中如果对性能要求高可以考虑使用定点数运算。// 设置DAC输出电压函数 // 参数vol范围0~3300对应0~3.3V void Dac1_Set_Vol(u16 vol) { float temp vol; temp / 1000; // 转换为伏特 temp temp * 4096 / 3.3; // 转换为12位数字量 DAC_SetChannel1Data(DAC_Align_12b_R, temp); // 12位右对齐 }3. ADC模块配置与校准3.1 ADC初始化流程ADC的配置比DAC复杂得多主要是因为ADC的性能受很多因素影响。首先是时钟配置STM32F103的ADC时钟不能超过14MHz我们通常将APB2时钟72MHz6分频得到12MHz。独立模式ADC_Mode_Independent适合单一ADC工作如果是双ADC应用则需要选择其他模式。ADC_InitTypeDef ADC_InitStructure; ADC_InitStructure.ADC_Mode ADC_Mode_Independent; // 独立模式 ADC_InitStructure.ADC_ScanConvMode DISABLE; // 单通道模式 ADC_InitStructure.ADC_ContinuousConvMode DISABLE;// 单次转换模式 ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_None; // 软件触发 ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right; // 数据右对齐 ADC_InitStructure.ADC_NbrOfChannel 1; // 转换通道数为1 ADC_Init(ADC1, ADC_InitStructure); // 时钟配置 RCC_ADCCLKConfig(RCC_PCLK2_Div6); // 6分频72MHz/612MHz3.2 ADC校准的重要性ADC校准是很多新手容易忽略的步骤但它对测量精度影响很大。校准过程包括复位校准和启动校准两个阶段每个阶段都需要等待完成。我在一个温度测量项目中曾经因为忘记等待校准完成导致测量值始终偏差3%左右排查了很久才发现问题所在。ADC_Cmd(ADC1, ENABLE); // 先使能ADC // 复位校准 ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); // 等待复位校准完成 // 开始校准 ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1)); // 等待校准完成3.3 ADC采样函数实现为了获得稳定的采样值我通常会实现一个多次采样取平均的函数。这里有个经验值采样时间设置为239.5个周期可以在精度和速度之间取得较好的平衡。对于变化缓慢的信号比如温度、电压等适当增加采样次数和采样时间能显著提高信噪比。// 单次ADC采样函数 u16 Get_Adc(u8 ch) { ADC_RegularChannelConfig(ADC1, ch, 1, ADC_SampleTime_239Cycles5); ADC_SoftwareStartConvCmd(ADC1, ENABLE); // 启动转换 while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)); // 等待转换完成 return ADC_GetConversionValue(ADC1); // 返回转换结果 } // 多次采样取平均函数 u16 Get_Adc_Average(u8 ch, u8 times) { u32 temp_val 0; for(u8 t 0; t times; t) { temp_val Get_Adc(ch); delay_ms(5); // 适当延时 } return temp_val / times; }4. 闭环验证与系统调试4.1 主程序逻辑设计在主程序中我们需要实现完整的闭环验证流程设置DAC输出 - 读取ADC值 - 比较两者差异。为了便于调试我添加了通过按键调整DAC输出的功能这样可以在不重新烧录程序的情况下测试不同电压点。int main(void) { // 初始化各种外设 delay_init(); uart_init(115200); // 串口用于输出调试信息 KEY_Init(); // 按键初始化 Adc_Init(); // ADC初始化 Dac1_Init(); // DAC初始化 u16 dacval 0; // DAC设置值 while(1) { u8 key KEY_Scan(0); // 按键控制DAC输出增减 if(key WKUP_PRES) { if(dacval 4000) dacval 200; Dac1_Set_Vol(dacval); } else if(key KEY1_PRES) { if(dacval 200) dacval - 200; else dacval 0; Dac1_Set_Vol(dacval); } // 定期读取并打印DAC和ADC值 static u8 t 0; if(t 10 || key) { float dac_voltage (float)DAC_GetDataOutputValue(DAC_Channel_1) * (3.3 / 4096); u16 adc_val Get_Adc_Average(ADC_Channel_1, 10); float adc_voltage (float)adc_val * (3.3 / 4096); printf(DAC设置电压: %.4fV, ADC测量电压: %.4fV, 差值: %.4fV\r\n, dac_voltage, adc_voltage, dac_voltage - adc_voltage); t 0; } delay_ms(100); } }4.2 精度分析与误差处理在实际测试中DAC输出和ADC测量值之间往往存在微小差异。这些差异主要来自以下几个方面DAC和ADC的固有误差包括增益误差、偏移误差等参考电压的精度和稳定性PCB布局和走线引入的噪声电源纹波的影响根据我的测试数据在3.3V参考电压下STM32F103的DAC和ADC组合通常能达到±0.5%左右的精度。如果需要更高精度可以考虑以下改进措施使用外部精密参考电压源增加硬件滤波电路在软件中实现校准算法如两点校准优化PCB布局减少数字信号对模拟信号的干扰4.3 串口调试技巧串口输出是调试模拟系统的重要工具。除了直接打印电压值外我还经常使用以下调试技巧输出原始数字量0-4095便于分析线性度在电压变化时输出时间戳分析响应速度实现简单的数据记录功能便于后续分析添加自检模式自动扫描全量程并记录误差这里分享一个实用的printf技巧使用%.4f格式可以显示足够的精度但又不会像%.8f那样产生过多的无效数字。在资源受限的嵌入式系统中合理控制输出信息量可以提高调试效率。
STM32F103ZET6【标准库函数开发】------07 DAC与ADC闭环验证:从电压设定到回读校准
1. DAC与ADC闭环验证系统概述在嵌入式系统开发中模拟信号的处理一直是工程师们需要面对的重要课题。STM32F103ZET6作为一款经典的ARM Cortex-M3内核微控制器其内置的12位DAC数模转换器和ADC模数转换器为模拟信号处理提供了硬件基础。这次我们要实现的DAC与ADC闭环验证系统简单来说就是让芯片自己产生一个电压信号然后再自己测量这个信号形成一个完整的自检回路。这种闭环验证在实际项目中非常实用。比如在工业控制领域我们需要确保输出的控制信号准确无误在医疗设备中精确的模拟信号输出更是关乎生命安全。通过这个实验我们不仅能验证硬件功能是否正常还能评估系统的整体精度。我做过不少类似项目发现这种自检机制往往能在早期就发现硬件设计或PCB布线的问题避免后期更大的损失。STM32F103ZET6有两个独立的12位DAC通道分别对应PA4和PA5引脚。DAC的参考电压通常直接使用芯片的Vref3.3V这意味着输出范围是0-3.3V。而ADC部分则有16个输入通道最高采样速率可达1MHz。在本次实验中我们将使用DAC通道1PA4输出模拟电压然后用ADC通道1PA1进行回读形成一个完整的闭环验证系统。2. 硬件配置与初始化2.1 时钟与GPIO配置任何STM32外设的使用都始于时钟使能。对于这个实验我们需要同时开启三个外设的时钟GPIOA因为DAC和ADC都用到了PA端口、DAC和ADC1。这里有个细节需要注意DAC挂在APB1总线上而ADC挂在APB2总线它们的时钟使能函数是不同的。// 时钟使能配置 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 使能GPIOA时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE); // 使能DAC时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); // 使能ADC1时钟GPIO配置上有个容易踩坑的地方虽然DAC输出和ADC输入都需要将GPIO设置为模拟输入模式但它们的原理完全不同。对于DAC引脚设置为模拟输入是为了关闭施密特触发器减少干扰而对于ADC引脚则是真正作为输入使用。我在早期项目中曾经混淆过这个概念导致信号质量很差。// GPIO初始化结构体配置 GPIO_InitTypeDef GPIO_InitStructure; // PA1 (ADC通道1) 配置 GPIO_InitStructure.GPIO_Pin GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; // 模拟输入 GPIO_Init(GPIOA, GPIO_InitStructure); // PA4 (DAC通道1) 配置 GPIO_InitStructure.GPIO_Pin GPIO_Pin_4; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; // 模拟输入 GPIO_Init(GPIOA, GPIO_InitStructure);2.2 DAC模块初始化DAC的初始化相对简单但有几个关键参数需要特别注意。首先是触发方式我们选择不使用硬件触发DAC_Trigger_None这样可以直接通过软件设置输出电压。其次是输出缓冲关闭缓冲DAC_OutputBuffer_Disable可以获得更高的输出阻抗但驱动能力会下降。根据我的经验在需要驱动低阻抗负载时应该开启缓冲但在精密测量场合最好关闭。DAC_InitTypeDef DAC_InitType; DAC_InitType.DAC_Trigger DAC_Trigger_None; // 不使用硬件触发 DAC_InitType.DAC_WaveGeneration DAC_WaveGeneration_None; // 不生成波形 DAC_InitType.DAC_LFSRUnmask_TriangleAmplitude DAC_LFSRUnmask_Bit0; // 不使用噪声或三角波 DAC_InitType.DAC_OutputBuffer DAC_OutputBuffer_Disable; // 关闭输出缓冲 DAC_Init(DAC_Channel_1, DAC_InitType); // 初始化DAC通道1 DAC_Cmd(DAC_Channel_1, ENABLE); // 使能DAC通道1为了方便电压设置我通常会封装一个设置电压的函数。这里有个实用技巧使用浮点数计算可以提高设置精度但要注意STM32F1没有硬件浮点单元频繁的浮点运算会影响性能。在实际项目中如果对性能要求高可以考虑使用定点数运算。// 设置DAC输出电压函数 // 参数vol范围0~3300对应0~3.3V void Dac1_Set_Vol(u16 vol) { float temp vol; temp / 1000; // 转换为伏特 temp temp * 4096 / 3.3; // 转换为12位数字量 DAC_SetChannel1Data(DAC_Align_12b_R, temp); // 12位右对齐 }3. ADC模块配置与校准3.1 ADC初始化流程ADC的配置比DAC复杂得多主要是因为ADC的性能受很多因素影响。首先是时钟配置STM32F103的ADC时钟不能超过14MHz我们通常将APB2时钟72MHz6分频得到12MHz。独立模式ADC_Mode_Independent适合单一ADC工作如果是双ADC应用则需要选择其他模式。ADC_InitTypeDef ADC_InitStructure; ADC_InitStructure.ADC_Mode ADC_Mode_Independent; // 独立模式 ADC_InitStructure.ADC_ScanConvMode DISABLE; // 单通道模式 ADC_InitStructure.ADC_ContinuousConvMode DISABLE;// 单次转换模式 ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_None; // 软件触发 ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right; // 数据右对齐 ADC_InitStructure.ADC_NbrOfChannel 1; // 转换通道数为1 ADC_Init(ADC1, ADC_InitStructure); // 时钟配置 RCC_ADCCLKConfig(RCC_PCLK2_Div6); // 6分频72MHz/612MHz3.2 ADC校准的重要性ADC校准是很多新手容易忽略的步骤但它对测量精度影响很大。校准过程包括复位校准和启动校准两个阶段每个阶段都需要等待完成。我在一个温度测量项目中曾经因为忘记等待校准完成导致测量值始终偏差3%左右排查了很久才发现问题所在。ADC_Cmd(ADC1, ENABLE); // 先使能ADC // 复位校准 ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); // 等待复位校准完成 // 开始校准 ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1)); // 等待校准完成3.3 ADC采样函数实现为了获得稳定的采样值我通常会实现一个多次采样取平均的函数。这里有个经验值采样时间设置为239.5个周期可以在精度和速度之间取得较好的平衡。对于变化缓慢的信号比如温度、电压等适当增加采样次数和采样时间能显著提高信噪比。// 单次ADC采样函数 u16 Get_Adc(u8 ch) { ADC_RegularChannelConfig(ADC1, ch, 1, ADC_SampleTime_239Cycles5); ADC_SoftwareStartConvCmd(ADC1, ENABLE); // 启动转换 while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)); // 等待转换完成 return ADC_GetConversionValue(ADC1); // 返回转换结果 } // 多次采样取平均函数 u16 Get_Adc_Average(u8 ch, u8 times) { u32 temp_val 0; for(u8 t 0; t times; t) { temp_val Get_Adc(ch); delay_ms(5); // 适当延时 } return temp_val / times; }4. 闭环验证与系统调试4.1 主程序逻辑设计在主程序中我们需要实现完整的闭环验证流程设置DAC输出 - 读取ADC值 - 比较两者差异。为了便于调试我添加了通过按键调整DAC输出的功能这样可以在不重新烧录程序的情况下测试不同电压点。int main(void) { // 初始化各种外设 delay_init(); uart_init(115200); // 串口用于输出调试信息 KEY_Init(); // 按键初始化 Adc_Init(); // ADC初始化 Dac1_Init(); // DAC初始化 u16 dacval 0; // DAC设置值 while(1) { u8 key KEY_Scan(0); // 按键控制DAC输出增减 if(key WKUP_PRES) { if(dacval 4000) dacval 200; Dac1_Set_Vol(dacval); } else if(key KEY1_PRES) { if(dacval 200) dacval - 200; else dacval 0; Dac1_Set_Vol(dacval); } // 定期读取并打印DAC和ADC值 static u8 t 0; if(t 10 || key) { float dac_voltage (float)DAC_GetDataOutputValue(DAC_Channel_1) * (3.3 / 4096); u16 adc_val Get_Adc_Average(ADC_Channel_1, 10); float adc_voltage (float)adc_val * (3.3 / 4096); printf(DAC设置电压: %.4fV, ADC测量电压: %.4fV, 差值: %.4fV\r\n, dac_voltage, adc_voltage, dac_voltage - adc_voltage); t 0; } delay_ms(100); } }4.2 精度分析与误差处理在实际测试中DAC输出和ADC测量值之间往往存在微小差异。这些差异主要来自以下几个方面DAC和ADC的固有误差包括增益误差、偏移误差等参考电压的精度和稳定性PCB布局和走线引入的噪声电源纹波的影响根据我的测试数据在3.3V参考电压下STM32F103的DAC和ADC组合通常能达到±0.5%左右的精度。如果需要更高精度可以考虑以下改进措施使用外部精密参考电压源增加硬件滤波电路在软件中实现校准算法如两点校准优化PCB布局减少数字信号对模拟信号的干扰4.3 串口调试技巧串口输出是调试模拟系统的重要工具。除了直接打印电压值外我还经常使用以下调试技巧输出原始数字量0-4095便于分析线性度在电压变化时输出时间戳分析响应速度实现简单的数据记录功能便于后续分析添加自检模式自动扫描全量程并记录误差这里分享一个实用的printf技巧使用%.4f格式可以显示足够的精度但又不会像%.8f那样产生过多的无效数字。在资源受限的嵌入式系统中合理控制输出信息量可以提高调试效率。