1. 项目概述
在嵌入式开发领域,ADC和DAC的协同控制是一个经典而实用的课题。这次我使用CW32L012这款国产MCU,搭建了一个完整的电压闭环控制系统。这个系统通过DAC输出目标电压,经过RC滤波电路后,再用ADC采集实际电压值,最后通过PID算法实现精准的电压控制。
这个项目看似简单,但涉及到了嵌入式开发的多个核心环节:外设配置、模拟电路设计、控制算法实现以及人机交互设计。我在实际开发过程中遇到了不少有意思的问题,比如如何选择合适的PID算法形式、如何处理ADC采集的噪声、如何优化控制响应速度等。
2. 硬件设计与选型
2.1 主控芯片选择
CW32L012是武汉芯源半导体推出的一款32位ARM Cortex-M0+内核MCU,选择它主要基于以下几个考虑:
- 集成度高:内置12位ADC和12位DAC,省去了外接转换芯片的麻烦
- 性能足够:96MHz主频完全能满足实时控制的需求
- 成本优势:相比国外大厂的同类产品,价格更具竞争力
- 开发便利:配套的库函数和文档比较完善
实际使用中发现,这款芯片的模拟外设性能相当不错,ADC的线性度和DAC的输出稳定性都达到了项目要求。
2.2 外围电路设计
核心电路就是一个简单的RC低通滤波器:
- 电阻:10kΩ
- 电容:47μF
这个参数的选择考虑了以下几个因素:
- 截止频率:f=1/(2πRC)≈0.34Hz,足够滤除高频噪声
- 响应速度:时间常数τ=RC=0.47s,与我们的控制周期匹配
- 元件常见度:这些都是非常常见的元件值,容易采购
电路连接也很简单:
- DAC输出 → RC电路输入
- RC电路输出 → ADC输入
- 共地连接确保参考电平一致
3. 软件架构设计
3.1 系统流程图
整个系统的软件流程可以分为以下几个部分:
-
初始化:
- 时钟配置
- GPIO初始化
- ADC/DAC校准
- 串口配置
- LCD初始化
- 按键扫描初始化
-
主循环:
- 读取按键输入
- 采集ADC值
- 计算PID输出
- 更新DAC值
- 刷新显示
- 串口数据上传
3.2 关键外设配置
3.2.1 DAC配置
c复制void DAC_Init(void)
{
__SYSCTRL_GPIOB_CLK_ENABLE();
GPIO_Init(CW_GPIOB, GPIO_PIN_1, GPIO_MODE_ANALOG);
__SYSCTRL_DAC_CLK_ENABLE();
DAC_InitTypeDef DAC_InitStruct;
DAC_InitStruct.DAC_Trigger = DAC_TRIGGER_SOFTWARE;
DAC_InitStruct.DAC_WaveType = DAC_WAVE_NONE;
DAC_InitStruct.DAC_OutputBuffer = DAC_OUTPUTBUFFER_ENABLE;
DAC_Init(CW_DAC, &DAC_InitStruct);
DAC_Enable(CW_DAC);
}
3.2.2 ADC配置
c复制void ADC_Init(void)
{
__SYSCTRL_GPIOA_CLK_ENABLE();
GPIO_Init(CW_GPIOA, GPIO_PIN_4, GPIO_MODE_ANALOG);
__SYSCTRL_ADC_CLK_ENABLE();
ADC_InitTypeDef ADC_InitStruct;
ADC_InitStruct.ADC_Resolution = ADC_RESOLUTION_12B;
ADC_InitStruct.ADC_DataAlign = ADC_DATAALIGN_RIGHT;
ADC_InitStruct.ADC_ScanMode = ADC_SCANMODE_SINGLE;
ADC_InitStruct.ADC_ContinuousMode = ADC_CONTINUOUSMODE_SINGLE;
ADC_Init(CW_ADC, &ADC_InitStruct);
ADC_ChannelConfig(CW_ADC, ADC_CHANNEL_4, ADC_SAMPLETIME_15CYCLES);
ADC_Enable(CW_ADC);
}
4. PID算法实现
4.1 增量式PID选择
为什么选择增量式而不是位置式PID?主要基于以下几点考虑:
- 抗积分饱和:增量式PID天然避免了积分项累积过大的问题
- 平滑输出:每次只计算输出变化量,DAC输出更平稳
- 手动自动切换无扰动:在需要手动干预时切换更平滑
4.2 PID核心代码
c复制typedef struct {
float target; // 目标值
float actual; // 实际值
float err; // 当前误差
float err_last; // 上一次误差
float err_last_last; // 上上次误差
float Kp, Ki, Kd; // PID参数
float out; // 输出值
} PID;
#define Error_Scale 15
uint16_t pid_control(void)
{
static float out_last=0;
temper_pid.err=Error_Scale*(temper_pid.target-temper_pid.actual);
temper_pid.out=temper_pid.Kp*(temper_pid.err-temper_pid.err_last)+
temper_pid.Ki*temper_pid.err +
temper_pid.Kd*(temper_pid.err-2*temper_pid.err_last+temper_pid.err_last_last);
// 更新误差历史
temper_pid.err_last_last = temper_pid.err_last;
temper_pid.err_last = temper_pid.err;
float temp=out_last+temper_pid.out;
temp=(temp>DAC_MAX)? DAC_MAX:temp; // 输出限幅
temp=(temp<0)? 0:temp;
out_last=temp;
return temp;
}
4.3 参数整定经验
通过实际调试,我总结出以下参数整定技巧:
- 先调P:将I和D设为0,逐渐增大P直到系统开始振荡,然后取这个值的50%-70%
- 再调I:在P的基础上加入I,从小值开始,观察静差消除速度
- 最后调D:大多数电压控制系统中D可以设为0,除非有特殊需求
- 观察指标:
- 超调量不超过10%
- 调节时间在合理范围内
- 稳态误差小于1%
5. 人机交互设计
5.1 按键功能
设计了三个按键实现参数调整:
- 按键1:参数增加
- 按键2:参数减少
- 按键3:切换调整项(目标值、Kp、Ki、Kd)
5.2 LCD显示
LCD实时显示以下信息:
- 目标电压值
- 实际电压值
- 当前PID参数
- DAC输出值
- 调整状态指示
5.3 串口通信
通过串口将数据上传到上位机(VOFA+)显示波形,方便观察系统响应:
- 目标值(放大10倍)
- 实际值(放大10倍)
- PID参数
6. 实际调试中的问题与解决
6.1 ADC采样噪声问题
现象:ADC采样值有较大波动
解决:
- 增加软件滤波:采用滑动平均滤波,窗口大小取8
- 优化PCB布局:模拟和数字地分开,最后单点连接
- 增加去耦电容:在ADC输入引脚加0.1μF电容
6.2 响应速度慢
现象:电压变化跟不上设定值变化
解决:
- 优化PID参数:适当增大P值
- 减小RC时间常数:将电容改为10μF
- 提高控制频率:将控制周期从100ms改为50ms
6.3 DAC输出不稳定
现象:DAC输出有微小跳变
解决:
- 使用内部电压跟随器模式
- 在DAC输出端加小电容(100nF)
- 确保电源稳定,纹波小于10mV
7. 性能测试结果
经过优化后,系统达到了以下性能指标:
- 稳态误差:<0.5%
- 超调量:<5%
- 调节时间:<1s(对于1V阶跃变化)
- 温度漂移:<0.1%/℃
8. 项目扩展思路
这个基础框架可以扩展很多有趣的应用:
- 温度控制系统:将电压控制改为温度控制,接入PT100传感器
- 电机转速控制:通过PWM输出控制电机转速,编码器反馈
- 智能照明调光:根据环境光强自动调节LED亮度
- 电源管理:实现可编程电压源
9. 关键经验总结
通过这个项目,我总结了以下几点重要经验:
- 模拟电路要简单可靠:有时候最简单的RC电路反而最可靠
- PID参数需要耐心调试:没有通用的最佳参数,必须根据实际系统调整
- 抗干扰设计很重要:嵌入式系统中噪声无处不在,必须重视
- 可视化调试很关键:波形显示能极大提高调试效率
- 文档记录不可少:记录每次修改和测试结果,便于回溯
这个项目虽然不大,但涵盖了嵌入式开发的多个核心技能点,对初学者来说是个很好的综合练习。在实际开发中,我建议先分模块验证每个功能,再逐步集成,这样可以快速定位问题。
