STC89C52单片机AD/DA实战:从XPT2046配置到PWM模拟DA输出 1. STC89C52与XPT2046的AD采集实战第一次用STC89C52做AD采集时我对着XPT2046芯片发呆了半小时——这玩意儿不是触摸屏驱动芯片吗后来才发现它其实是个隐藏的ADC高手。下面我就把踩过的坑和总结的经验分享给大家。1.1 硬件连接要点XPT2046虽然只有8个引脚但接错线的情况我见过不下十种。正确的接法应该是VCC接5V注意不是3.3VGND接地DIN接P3.4单片机输出DOUT接P3.7单片机输入CS接P3.5片选DCLK接P3.6时钟有个容易忽略的细节在VCC和GND之间要加个0.1μF的退耦电容否则采集值时会有跳变。我当初没加这个电容采集的温度值能上下波动5℃加了之后稳定在±0.3℃以内。1.2 寄存器配置玄机XPT2046的配置命令字是个8位二进制数每位都有讲究#define XPT2046_XP 0x9C // 测X通道(AIN0) #define XPT2046_YP 0xDC // 测Y通道(AIN1) #define XPT2046_VBAT 0xAC // 测电池电压(AIN2)这个命令字的构成是这样的第7位(MSB)必须为1表示开始A2/A1/A0三位选择通道001对应AIN0011对应AIN1MODE位选择精度18位012位SER/DFR选择单端/差分模式建议设1用单端实测发现用12位模式时噪声反而比8位大这是因为STC89C52的IO口抗干扰能力有限。如果环境干扰大建议用8位模式加软件滤波。1.3 代码实现细节完整的读取函数应该包含时序控制和数据处理unsigned int XPT2046_ReadAD(unsigned char Command) { unsigned char i; unsigned int Data0; XPY2046_DCLK0; XPY2046_CS0; // 发送控制字 for(i0;i8;i) { XPY2046_DINCommand(0x80i); XPY2046_DCLK1; XPY2046_DCLK0; } // 读取16位数据实际有效12位 for(i0;i16;i) { XPY2046_DCLK1; XPY2046_DCLK0; if(XPY2046_DOUT) Data|(0x8000i); } XPY2046_CS1; return Data4; // 对齐有效位 }这里有个坑芯片手册要求DCLK高电平至少保持1μs而STC89C52在12MHz时钟下一个_nop_()只有1μs所以每次翻转DCLK后要加_nop_()。2. 多通道数据采集技巧2.1 通道切换策略XPT2046有三个模拟输入通道AIN0-AIN2典型应用是AIN0接电位器电压调节AIN1接热敏电阻温度检测AIN2接光敏电阻光照传感切换通道时要注意每次切换后延迟1ms再采集让信号稳定建议采集顺序电位器→温度→光照因为温度变化慢对温度信号要做10次平均滤波2.2 热敏电阻处理NTC热敏电阻的接法很讲究VCC ---[10K电阻]------[NTC]---GND | AIN1计算温度的公式为float Get_Temperature(unsigned int adc) { float Rt 10000.0 * adc / (4095 - adc); // 10K上拉 float temp 1/(1/(273.1525) log(Rt/10000)/3950.0) - 273.15; return temp; }这个公式里的3950是热敏指数B值不同型号要改。我在实验室测过用这个公式误差在±0.5℃内。2.3 光敏电阻应用光敏电阻建议用GL5528它的亮电阻10Lux约8KΩ暗电阻约1MΩ。接法VCC ---[GL5528]------[10K电阻]---GND | AIN2光照强度公式#define LUX_SCALE 100.0 // 校准系数 float Get_Light(unsigned int adc) { if(adc 10) return 0; // 防除零 float RL 10000.0 * (4095 - adc) / adc; return LUX_SCALE * 1000 / RL; // 简易线性换算 }实际使用时要用照度计校准LUX_SCALE值。我在项目中发现加个绿色LED做背光补偿能显著改善低照度下的检测线性度。3. PWM模拟DA输出详解3.1 硬件设计要点STC89C52没有真正的DAC但用PWM加RC滤波就能实现不错的DA输出P2.1(PWM) ---[1K电阻]------[10uF电容]---GND | 输出端这个二阶滤波电路的截止频率f_c 1/(2π√(R1×R2×C1×C2)) ≈ 16Hz (当R21K, C20.1uF)实测纹波小于20mV足够驱动LED调光。如果要驱动电机建议再加个电压跟随器。3.2 定时器配置用定时器0产生PWM的初始化代码void Timer0_Init() { TMOD 0xF0; // 设置定时器模式 TMOD | 0x01; // 定时器0工作模式1 TH0 0xFF; // 初始值决定频率 TL0 0x00; ET0 1; // 使能定时器中断 EA 1; // 开总中断 TR0 1; // 启动定时器 } void Timer0_ISR() interrupt 1 { static unsigned char pwm_cnt 0; TH0 0xFF; // 重装初值约100Hz TL0 0x00; if(pwm_cnt 100) pwm_cnt 0; DA (pwm_cnt duty) ? 1 : 0; // duty是占空比0-100 }PWM频率计算当系统时钟12MHz时定时器每256个周期中断一次21.3μs100次中断约2.13ms470Hz。如果想更精确可以用自动重装模式。3.3 闭环控制实例将AD采集和PWM输出结合实现自动调光void main() { unsigned int light_adc; Timer0_Init(); while(1) { light_adc XPT2046_ReadAD(XPT2046_VBAT); // 目标亮度200-300超出范围自动调节 if(light_adc 200) duty; else if(light_adc 300) duty--; if(duty 100) duty 100; if(duty 0) duty 0; Delay(10); } }这个例子用光敏反馈控制LED亮度实测响应时间约0.5秒。如果需要更快响应可以增大PWM频率但要注意滤波电容要相应减小。4. 系统优化与调试技巧4.1 软件滤波算法对于AD采集的噪声问题我有三个实用方法滑动平均滤波保留最近10次采样值取平均#define FILTER_LEN 10 unsigned int filter_buf[FILTER_LEN]; unsigned int Moving_Average(unsigned int new_val) { static unsigned char index 0; unsigned long sum 0; filter_buf[index] new_val; if(index FILTER_LEN) index 0; for(unsigned char i0; iFILTER_LEN; i) { sum filter_buf[i]; } return sum / FILTER_LEN; }中值滤波连续采样3次取中间值限幅滤波相邻两次采样差值过大时舍弃4.2 电源管理AD采集时电源噪声会影响精度建议模拟部分单独用LC滤波如100μH电感100μF电容在VREF引脚加1μF钽电容如果使用电池供电记得在代码里检测电压float Get_Battery_Voltage() { unsigned int adc XPT2046_ReadAD(XPT2046_VBAT); return adc * 5.0 / 4095 * 2; // 电阻分压比1:1 }4.3 性能测试数据在我的测试环境下室温25℃项目8位模式12位模式单次转换时间320μs480μs电压分辨率19.5mV1.22mV实测噪声峰峰值±2LSB±5LSB功耗增加0.2mA0.3mA从数据看12位模式的实际有效位数约10位。如果项目对速度敏感用8位模式反而更合适。