STC15 ADC转软件PWM:从寄存器配置到工程实现
简介面向STC单片机开发者的AD转PWM功能实现源码包覆盖从模拟量采集到脉宽调制输出的完整链路适合电机调速、LED亮度调节等控制场景。压缩包内共22个文件包含C语言源程序、头文件、Keil工程配置以及编译生成的HEX和列表文件整体仅37KB小巧便于移植。已有245人学习说明该示例具有较高参考价值。源码中详细展示了ADC参考电压与采样通道配置、定时器初始化、基于中断的软件PWM占空比调整等关键环节并配有工程备份文件便于开发者对照修改。通过阅读与运行可快速掌握STC单片机模拟量转换与PWM输出的协作机制为实际项目提供直接可用的代码基础。1. 从模拟电压到脉冲宽度拆解一个STC15的AD转PWM工程一块STC15F2K60S2、一路电位器分压、一个普通GPIO输出外加一片TM1637段码屏就可以构成一条完整的AD转PWM数据链ADC把旋钮电压读成10位数字量主循环把它映射成占空比定时器0中断按这个占空比翻转引脚数码管同时显示当前读数。拿这个工程去驱动LED无级调光、小型直流风扇调速或者加热器功率控制都不需要额外接PWM专用芯片一套逻辑走到底。这套方案适合两类人。一类是想搞懂“ADC结果到底怎么变成PWM波形”的初学者另一类是手头正好有STC15开发板、又不想被片内硬件PWM引脚和周期寄存器绑住手脚的工程师。下面按A_ADC.C、main.c、TM1637.C这几个工程文件逐层拆重点讲寄存器配置、软件PWM的中断实现以及实际调参时容易翻车的几个细节。2. 先读准电压STC15F2K60S2的AD采样与数据准备STC15F2K60S2片内集成的ADC是逐次逼近型参考电压默认接VCC转换结果由ADC_RES和ADC_RESL两个寄存器拼接而成。控制核心寄存器是ADC_CONTRbit7是ADC_POWER电源开关bit6是ADC_START启动位bit5和bit4组合成转换速度bit3到bit0选择采样通道。P1口的P1.0到P1.7复用为ADC输入对应通道号0到7。实际项目里容易在第一步就翻车P1口默认是准双向IO直接把电位器分压输出接到引脚上数字输入电路会分流导致旋钮拧到一半电压被拉低采样值非线性严重。无论用哪个通道都要先把对应引脚配置成高阻输入模式再开启ADC电源。2.1 引脚模式与转换速度的寄存器配置STC15的P1M0和P1M1两个寄存器控制引脚工作模式。P1M1对应位置1、P1M0对应位置0时该引脚为高阻输入这是ADC采样前必须完成的步骤。转换速度通过ADC_CONTR的SPEED位选择不同档位对应不同的时钟周期数在11.0592MHz系统时钟下实际耗时如下表ADC_SPEED位转换时钟周期11.0592MHz下约需适用场景0090个时钟约10us常规电位器、光敏电阻采样01180个时钟约20us信号源内阻较大时10360个时钟约36us高精度求稳场景11540个时钟约54us极低频率缓变信号选90个时钟周期足够应对大多数旋钮类输入转换结果更稳定。初始化函数如下// A_ADC.C 初始化ADC通道ch0~7对应P1.0~P1.7 void adc_init(u8 ch) { P1M0 ~(0x01 ch); // P1.x 输出模式控制位清零 P1M1 | (0x01 ch); // P1.x 置1配合P1M00构成高阻输入 ADC_CONTR 0x80 | 0x00 | (ch 0x0F); // 0x80ADC_POWER1打开ADC电源 // 0x00ADC_SPEED00约90个时钟周期完成一次转换 // ch 0x0F通道号写入bit3~bit0 }代码里先动P1M0再动P1M1避免中间状态把引脚配置成强推挽输出。ADC_CONTR的bit5和bit4是速度位如果改成0x20则对应180个时钟周期。注意ADC_CONTR其他位不能随意覆盖初始化之后每次启动转换都要基于这个基础值操作。2.2 启动转换、等待标志与10位结果拼接STC15的ADC结果是10位ADC_RES存放高8位ADC_RESL只用到低2位。启动一次转换的完整流程是清ADC_FLAG标志、置ADC_START启动位、轮询ADC_FLAG等待转换完成、读出两个寄存器拼接。下面的read_adc函数就是A_ADC.C里的核心采样逻辑// 读取一次ADC结果返回0~1023 u16 read_adc(u8 ch) { u16 res 0; ADC_CONTR ~0x10; // bit4 ADC_FLAG清零 ADC_CONTR | 0x40; // bit6 ADC_START置1启动一次转换 while (!(ADC_CONTR 0x10)); // 等待硬件置位ADC_FLAG res ADC_RES; // 取高8位 res (res 2) | (ADC_RESL 0x03); // 左移2位拼上低2位得到10位结果 return res; }注意ADC_FLAG必须由软件写0清除。如果不清标志直接启动下一次转换循环判断会立即通过读到的还是旧数据。ADC_RESL寄存器只有bit1和bit0有意义所以用 0x03做掩码。轮询方式虽然占用CPU但胜在时序确定AD转PWM这种低速应用完全够用。如果换成中断方式需要额外处理中断优先级与PWM定时器中断的竞争复杂度上去了收益却不大。2.3 均值滤波与占空比映射公式单次采样值抖动在正负2到3个LSB之间直接映射成PWM会导致亮度或转速肉眼可见地跳动。常见的做法是连续采样若干次求平均A_ADC.C里的实现可以写成这样// 连续采样times次取平均times一般取4~16 u16 average_adc(u8 ch, u8 times) { u32 sum 0; u8 i; for (i 0; i times; i) { sum read_adc(ch); } return (u16)(sum / times); }这里必须用u32类型保存累加值8次1023累加最大为818416次为16368u8根本放不下。采样次数翻倍带来的噪声抑制收益递减我一般取8次既平滑又不会让主循环卡顿。拿到10位ADC结果后下一步就是转PWM。开发板上用的映射公式是pwm_duty (u8)((u32)adc_val * PWM_MAX / 1023);乘法和除法用u32运算避免中间量溢出PWM_MAX取250而不是255是为了配合后面软件PWM的周期计数器让占空比达到100%时计数器刚好满量程。这个映射关系就是“AD转PWM”的数值核心电压越高占空比越大输出给负载的平均功率越高。3. 不依赖硬件PWM定时器0中断实现脉宽输出STC15F2K60S2内部有PWM模块但它的输出引脚和周期配置都受硬件约束改动频率要重新算寄存器换输出引脚还要查复用关系。软件PWM只需要一个普通GPIO核心逻辑是定时器以固定周期产生中断每次中断把计数值和占空比比较决定引脚输出高还是低。这样一来PWM频率、占空比、极性全部变成主循环里的变量调试时直接改一个宏定义就能重新编译配合万用表和逻辑分析仪调参数非常方便。软件PWM还有一个硬件PWM不好替代的场景频率同步。比如摄像头采集需要PWM信号与帧同步对齐软件方案可以在中断里直接读取外部同步标志作为相位基准这在寄存器固定的硬件PWM上实现起来要绕很多弯路。3.1 周期计数与占空比比较的中断逻辑main.c中定时器0按50us产生一次中断250次中断构成一个完整的PWM周期所以PWM频率是80Hz占空比可调步进为1/250约0.4个百分点。定时器初始化与中断服务函数如下// main.c PWM周期与占空比控制 #define PWM_PERIOD 250 // 250 x 50us 12.5msPWM频率80Hz #define PWM_PIN P30 // 假设P3.0输出PWM volatile u8 pwm_duty 0; // 占空比寄存器 0~250 volatile u8 pwm_count 0; // 周期计数器 void timer0_init(void) { AUXR | 0x80; // 定时器0工作在1T模式 TMOD 0xF0; // 只修改T0部分 TMOD | 0x01; // T0为16位定时器 TH0 0xFF; TL0 0xD7; // 11.0592MHz下重装值约50us ET0 1; // 使能定时器0中断 TR0 1; // 启动定时器 } void timer0_isr(void) interrupt 1 { TH0 0xFF; // 重装初值 TL0 0xD7; if (pwm_count PWM_PERIOD) { pwm_count 0; // 一个周期结束计数器归零 } PWM_PIN (pwm_count pwm_duty) ? 1 : 0; }初值0xFFD7的计算逻辑STC15是1T单片机1个定时器计数脉冲等于1个系统时钟。11.0592MHz下每计数一次约0.0904us50us需要约553个计数65536减553等于64983十六进制就是0xFFD7。代码里中断一进来立即重装初值减少重装延时的抖动。PWM引脚的电平判定本质是计数比较pwm_count从0增长到249时前pwm_duty个计数输出高电平剩余输出低电平。pwm_duty等于125时高电平持续约12.5ms乘以50%正好是50%占空比。这样得到的PWM信号不需要翻转延时边沿位置完全由计数器决定波形抖动比软件延时翻转方案小得多。3.2 PWM频率、分辨率与中断开销的参数关系软件PWM的性能由三个参数互相制约这一层关系理清了后面调参才不会盲目参数默认值调高影响调低影响定时器中断间隔50us中断更频繁CPU占用上升PWM边沿位置量化变粗PWM_PERIOD250频率变低占空比分辨率提高频率变高分辨率下降pwm_duty更新主循环写响应变快响应变慢中断间隔决定PWM的时间量化粒度。50us对应20kHz的采样频率意味着占空比更新最快能到50us响应一次。PWM_PERIOD决定周期长度和分辨率250个计数把0到100%切成250份对调光和调速来说足够细腻如果把PWM_PERIOD改成500分辨率提高到1/500但80Hz会掉到40HzLED的频闪就会明显。实际负载对PWM频率的要求也写在同一个表里。LED调光建议200Hz以上否则手机摄像头能拍到滚动条纹直流电机调速80到200Hz比较合适频率太高电机电感阻抗增大电流反而上不去加热器这类惯性负载用10到50Hz就够还能减少MOS管开关损耗。3.3 中断服务函数里的操作边界定时器0中断服务函数里只允许做两件事重装初值、更新PWM引脚。AD转换代码如果放进中断一次采样要等10us以上中断被拉长后pwm_count的计数节奏完全被打乱输出波形会出现肉眼可见的毛刺。TM1637的IO模拟时序也要远离中断它一次字节传输需要拉低CLK几十微秒和定时器中断抢占会产生显示错乱和PWM抖动。如果追求更平滑的占空比变化可以在主循环里对pwm_duty做增量逼近每次只朝目标值移动1到2个计数让输出电压缓慢爬升。电机启动时会平滑很多不会出现猛冲。4. 闭环看得见TM1637显示与主循环调度AD转PWM系统如果没有显示反馈调参时只能靠万用表量输出电压效率很低。TM1637.c这个文件就是干这个事的。TM1637是两线制LED驱动芯片CLK和DIO两根线最多驱动4位数码管它能锁存显示数据写入一次后由内部硬件维持输出主循环不需要反复刷新对CPU占用极小。TM1637的通信协议不是标准I2C时序更接近自定义的两线串行起始条件、8位数据、应答位、停止条件。写命令帧0x44表示地址固定写入0x40表示地址自动递增显示地址从0xC0开始0x8F是显示开且亮度最大0x88是最暗。段码是共阴数码管编码0到9的段码表如下。4.1 段码映射与单字节写入函数TM1637.C里通常把CLK和DIO的拉高拉低封装成底层函数再向上提供写字节和显示数字的接口。数字0到9的共阴段码存放在const数组里节省RAM// TM1637.C 共阴段码表 const u8 seg_table[10] { 0x3F, // 0: a b c d e f 0x06, // 1: b c 0x5B, // 2: a b d e g 0x4F, // 3: a b c d g 0x66, // 4: b c f g 0x6D, // 5: a c d f g 0x7D, // 6: a c d e f g 0x07, // 7: a b c 0x7F, // 8: 全部点亮 0x6F // 9: a b c d f g };显示一个数字要发三个帧先发0x44选择固定地址写模式再发0xC0加地址写入段码最后发0x8F开显示。实际写入函数如下void display_number(u8 pos, u8 num) { tm1637_start(); tm1637_write_byte(0x44); // 固定地址写模式 tm1637_stop(); tm1637_start(); tm1637_write_byte(0xC0 | pos); // 起始地址0xC0pos取0~3 tm1637_write_byte(seg_table[num]); tm1637_stop(); }0x44帧的作用是告诉TM1637接下来用固定地址方式写入这样每发一个字节只需要指定一次地址。0xC0加pos是目标地址比如pos0对应最左侧数码管。段码表如果还要显示小数点和负号可以额外定义0x80为小数点标志位取段码后与0x80做或运算。4.2 主循环里的整数节拍调度main函数的核心不是简单的“采样一次、更新一次显示”而是把任务按节拍错开。AD采样每64次循环做一次显示刷新每16次循环做一次其余时间主循环空转等待定时器中断。这样的非阻塞调度让PWM中断始终能抢占CPU不会被采样循环卡住。// main.c 主循环 void main(void) { u16 adc_val 0; u8 display_buf[4] {0}; u16 tick 0; adc_init(1); // 初始化P1.1为ADC输入 timer0_init(); // 启动软件PWM tm1637_init(); // 初始化TM1637引脚和显示状态 while (1) { tick; if ((tick 0x3F) 0) { // 每64次循环采样一次 adc_val average_adc(1, 8); // 8次采样取平均 pwm_duty (u8)((u32)adc_val * PWM_PERIOD / 1023); } if ((tick 0x0F) 0) { // 每16次循环刷新显示 snprintf_buf(adc_val, display_buf); tm1637_display(display_buf); } } }tick变量是u16溢出后2的幂与运算依然成立所以用 0x3F和 0x0F判断节拍不需要担心回绕问题。AD采样周期与显示刷新周期是两个独立节拍前者12.8ms量级后者3.2ms量级。采样和显示的频度都不需要太快ADC转换本身要耗时近百微秒显示刷新要写4个字节两件事错开后PWM中断的响应时间更稳定。4.3 显示闪烁与采样干扰的处理TM1637驱动数码管出现闪烁首要检查不是刷新频率而是显示地址写错。常见错误是0xC0和0x44两帧顺序颠倒或者写入段码后没有发0x8F亮度控制帧导致内部显示关闭。数码管本身由TM1637硬件驱动刷新主循环只在数值变化时发送数据因此不存在传统动态扫描的闪烁问题。还有一个容易被忽略的坑TM1637的CLK和DIO是IO模拟时序写数据期间会频繁拉高拉低必须避开ADC启动转换的时刻。如果两个操作撞在一起CLK的跳变会耦合进模拟采样通道ADC结果末尾几位明显跳动。我一般会把显示刷新的节拍安排在采样节拍之后让它们在时间上错开至少几个毫秒。实际调试时可以用万用表监测PWM引脚如果发现占空比输出随显示更新出现周期性波动基本就是采样被显示时序干扰了。5. 调稳一套参数再谈换平台移植AD转PWM这套链路真正的技术含量在参数匹配。AD转换速度、PWM频率、占空比分辨率、显示刷新节拍四个参数互相牵制。把A_ADC.C里的通道号、main.c里的PWM_PERIOD和定时器初值、TM1637.C里的段码表梳理清楚整个工程就变成了一个可以随意改参数的基础框架。以11.0592MHz系统时钟为基准实测可用的参数配比如下应用PWM频率PWM_PERIOD定时器初值效果LED调光200Hz1000xFFD7无明显频闪分辨率1%直流电机调速80Hz2500xFFD7转速稳定无啸叫加热器功率控制20Hz10000xFFD7温度波动小开关损耗低舵机控制50Hz40000xFFD7脉宽范围0.5ms到2.5ms需要做占空比偏移每个应用只改PWM_PERIOD和占空比映射方式即可。舵机控制额外需要在0到250的占空比上加偏移量把pwm_duty换算成0.5到2.5ms的高电平时间这就不是简单的ADC乘除法了。占空比计算公式在软件PWM里始终是高电平时间除以PWM周期乘以100%对应到代码就是pwm_duty除以PWM_PERIOD再乘以100%。ADC位数变化时把公式里的1023换成对应的满量程值就行10位换12位就改成40958位ADC改成255这是移植到其他单片机必须动的一行。移植到STM32这类平台时检查四个点。第一ADC引脚配置改成模拟输入模式对应STC15的高阻输入。第二定时器初值计算要按T 定时器时钟周期乘以重装值重新算STC15的1T模式和标准51的12T模式初值差别很大。第三硬件PWM替代软件PWM后把中断里的引脚比较操作换成调用底层占空比更新函数比如__HAL_TIM_SET_COMPARE。第四STM32高级定时器支持中心对齐模式和采样点偏移这是STC15没有的能力移植时可以在定时器更新事件触发ADC采样让采样时刻固定在高电平中点比STC15靠软件错开节拍更准确但也别把STC15的软调度代码硬塞进去。最后给一个调参技巧先用固定占空比跑PWM输出用逻辑分析仪确认频率和占空比正确再接入ADC和显示采样值用串口或数码管观察确认电压对应关系后最后才连负载。三步验证下来出问题时能立刻定位是AD采样、PWM输出还是显示环节的故障。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →