STC15 ADC转软件PWM:从寄存器配置到工程实现
2026/9/14 15:12:14 网站建设 项目流程

简介:面向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_CONTR,bit7是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通道ch,0~7对应P1.0~P1.7 void adc_init(u8 ch) { P1M0 &= ~(0x01 << ch); // P1.x 输出模式控制位清零 P1M1 |= (0x01 << ch); // P1.x 置1,配合P1M0=0构成高阻输入 ADC_CONTR = 0x80 | 0x00 | (ch & 0x0F); // 0x80:ADC_POWER=1,打开ADC电源 // 0x00:ADC_SPEED=00,约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累加最大为8184,16次为16368,u8根本放不下。采样次数翻倍带来的噪声抑制收益递减,我一般取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.5ms,PWM频率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.0904us,50us需要约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会掉到40Hz,LED的频闪就会明显。

实际负载对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); // 起始地址0xC0,pos取0~3 tm1637_write_byte(seg_table[num]); tm1637_stop(); }

0x44帧的作用是告诉TM1637接下来用固定地址方式写入,这样每发一个字节只需要指定一次地址。0xC0加pos是目标地址,比如pos=0对应最左侧数码管。段码表如果还要显示小数点和负号,可以额外定义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位就改成4095,8位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输出还是显示环节的故障。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询