用STM32F103的DAC和ADC做个简易信号发生器:从PA4输出,PA1读取并串口显示
STM32F103 DAC与ADC闭环系统实战:构建可交互信号发生器
在嵌入式开发中,数字模拟转换(DAC)和模拟数字转换(ADC)是两个基础但极其重要的外设功能。许多开发者虽然能独立使用它们,却很少尝试将二者结合形成完整的闭环系统。本文将带您用STM32F103的DAC和ADC构建一个实时可调的信号发生器,通过PA4输出模拟信号,同时用PA1读取反馈值并通过串口显示,实现"设置-输出-测量-验证"的全流程。
1. 系统架构设计与核心组件
1.1 硬件资源规划
STM32F103ZET6芯片内置了两个12位精度的DAC模块和一个12位ADC模块,这是我们项目的硬件基础。系统需要合理分配以下资源:
- DAC通道1:使用PA4引脚作为模拟输出
- ADC通道1:使用PA1引脚作为模拟输入
- USART1:用于PC通信和数据显示
- GPIO按键:用于交互控制输出电压
关键参数对比表:
| 模块 | 分辨率 | 参考电压 | 转换时间 | 输出范围 |
|---|---|---|---|---|
| DAC | 12位 | 3.3V | 无 | 0-3.3V |
| ADC | 12位 | 3.3V | 1μs | 0-3.3V |
1.2 软件架构设计
整个系统采用分层设计思想,各功能模块通过清晰的接口交互:
主控制循环 ├─ 用户输入处理(按键扫描) ├─ DAC输出控制 ├─ ADC采样处理 └─ 数据显示输出(串口)这种架构保证了系统的可扩展性,未来可以方便地添加新功能如波形生成、数据存储等。
2. 硬件初始化与配置
2.1 时钟与GPIO配置
所有外设使用前必须使能相应时钟,这是STM32编程的基本要求:
// 使能各外设时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE);PA4(DAC输出)和PA1(ADC输入)都需要配置为模拟模式:
GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_4; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; // 模拟输入模式 GPIO_Init(GPIOA, &GPIO_InitStructure);注意:即使DAC是输出功能,其引脚也必须配置为模拟输入模式,这是STM32的特殊设计要求。
2.2 DAC模块深度配置
DAC的配置需要考虑输出缓冲、触发方式等关键参数:
DAC_InitTypeDef DAC_InitStructure; DAC_InitStructure.DAC_Trigger = DAC_Trigger_None; // 不使用硬件触发 DAC_InitStructure.DAC_WaveGeneration = DAC_WaveGeneration_None; // 无波形生成 DAC_InitStructure.DAC_OutputBuffer = DAC_OutputBuffer_Disable; // 关闭输出缓冲 DAC_Init(DAC_Channel_1, &DAC_InitStructure); DAC_Cmd(DAC_Channel_1, ENABLE); // 使能DAC通道输出缓冲的取舍:
- 启用缓冲:提高驱动能力,但会引入约20mV的偏移
- 禁用缓冲:输出阻抗较高,但精度更好
对于需要高精度的测量系统,建议禁用输出缓冲。
3. 核心功能实现
3.1 电压输出控制函数
设计一个可直接设置输出电压的函数,隐藏底层寄存器操作:
// 设置DAC输出电压 // vol: 目标电压值,单位mV,范围0-3300 void DAC_SetVoltage(uint16_t vol) { // 电压值转换为DAC数字量 uint16_t dacValue = (uint32_t)vol * 4095 / 3300; DAC_SetChannel1Data(DAC_Align_12b_R, dacValue); }这个封装函数使得电压设置更加直观,开发者只需关心需要的电压值,而不必了解底层DAC寄存器的细节。
3.2 ADC采样与数据处理
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; ADC_Init(ADC1, &ADC_InitStructure); // ADC时钟配置 RCC_ADCCLKConfig(RCC_PCLK2_Div6); // 72MHz/6=12MHz ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_239Cycles5);为提高测量精度,实现了一个带平均滤波的ADC读取函数:
// 获取ADC采样平均值 uint16_t ADC_ReadAverage(uint8_t ch, uint8_t times) { uint32_t sum = 0; for(uint8_t i=0; i<times; i++) { sum += ADC_ReadSingle(ch); delay_ms(1); // 适当延时保证采样间隔 } return sum / times; }4. 系统集成与性能优化
4.1 主控制逻辑实现
主循环中整合了按键扫描、DAC控制、ADC采样和数据显示功能:
while(1) { key = KEY_Scan(0); // 扫描按键 // 按键处理 if(key == KEY_UP) { voltage += 100; // 电压增加0.1V if(voltage > 3300) voltage = 3300; DAC_SetVoltage(voltage); } else if(key == KEY_DOWN) { voltage -= 100; // 电压减少0.1V if(voltage < 0) voltage = 0; DAC_SetVoltage(voltage); } // 定期采样和显示 if(timer_count >= 100) { // 约1秒间隔 uint16_t adcValue = ADC_ReadAverage(ADC_Channel_1, 10); float measuredVoltage = (float)adcValue * 3.3f / 4095; printf("设定电压: %.2fV, 测量电压: %.2fV\r\n", voltage/1000.0f, measuredVoltage); timer_count = 0; } delay_ms(10); timer_count++; }4.2 精度优化技巧
通过实践发现几个提高系统精度的关键点:
- 电源稳定性:为AVDD和VDDA提供干净稳定的3.3V电源
- 参考电压:确保VREF+引脚有良好的去耦电容(100nF+10μF)
- PCB布局:
- 模拟信号走线远离数字信号
- 使用地平面隔离敏感信号
- 软件校准:
- 在代码中实现零点校准和增益校准
- 存储校准参数到Flash或EEPROM
典型误差来源分析:
| 误差类型 | 影响程度 | 解决方法 |
|---|---|---|
| 量化误差 | ±0.8mV | 软件平均滤波 |
| 参考电压波动 | ±10mV | 使用精密基准源 |
| PCB噪声 | ±5mV | 优化布局布线 |
5. 进阶功能扩展
5.1 波形生成功能
基于现有框架,可以扩展DAC的波形生成能力:
// 生成正弦波 void DAC_GenerateSineWave(uint16_t freq) { static const uint16_t sineTable[] = {2048,2447,2831,3185,...}; // 预计算正弦表 static uint8_t index = 0; DAC_SetChannel1Data(DAC_Align_12b_R, sineTable[index]); index = (index + 1) % sizeof(sineTable); // 使用定时器控制频率 TIM_SetAutoreload(TIM2, SystemCoreClock / (freq * sizeof(sineTable))); }5.2 上位机交互设计
通过串口协议实现更丰富的控制功能:
协议帧格式: [头字节][命令][数据长度][数据][校验和] 示例命令: 0x01 - 设置输出电压 0x02 - 读取当前电压 0x03 - 开始波形生成在STM32端实现简单的协议解析:
void USART_ProcessCommand(uint8_t* buf) { switch(buf[1]) { // 命令字节 case 0x01: DAC_SetVoltage((buf[3]<<8)|buf[4]); break; case 0x02: USART_SendVoltage(); break; // 其他命令处理... } }6. 调试技巧与常见问题
6.1 典型问题排查指南
问题1:DAC无输出或输出不正确
- 检查PA4模式是否为GPIO_Mode_AIN
- 确认DAC已使能(DAC_Cmd)
- 测量VREF+电压是否正常
问题2:ADC读数不稳定
- 增加采样次数使用平均滤波
- 检查模拟电源是否干净
- 适当增加采样时间(ADC_SampleTime)
问题3:串口数据显示异常
- 确认波特率设置匹配
- 检查串口初始化代码
- 确保USART时钟已使能
6.2 性能测试方法
建立系统化的测试流程:
静态精度测试:
- 设置DAC输出从0到3.3V,步进0.1V
- 记录ADC测量值和万用表实测值
- 计算误差并绘制曲线
动态响应测试:
- 快速改变DAC输出
- 测量ADC跟踪速度
- 评估系统响应时间
长期稳定性测试:
- 固定输出某电压值
- 连续监测8小时以上
- 观察漂移情况
通过这个项目,您不仅掌握了DAC和ADC的基本用法,更重要的是理解了如何将多个外设有机结合,构建完整的嵌入式系统。在实际产品开发中,这种闭环控制思想随处可见,比如智能家居中的温控系统、工业设备中的伺服控制等。
