蓝桥杯单片机学习笔记(十)——PWM与串口
蓝桥杯单片机学习笔记(十)——PWM与串口
温馨提示:内容源自米醋电子工作室培训课,由本人总结
一、PWM(脉冲宽度调制)
1. 什么是PWM?
PWM(Pulse Width Modulation,脉冲宽度调制)的核心概念是占空比。
你可以把它想象成一个电灯的开关游戏:在一个固定的时间周期内,我们控制灯亮多久、灭多久。比如一个周期为100份,让LED亮70份、灭30份,那么这盏灯的亮度就被调整到了70%。人眼由于视觉暂留效应,感受到的是一个"70%亮度"的灯,而不是闪烁。
这就好比你用手快速地遮住和放开一盏灯,遮住的时间越短,你感觉灯越亮;遮住的时间越长,灯就越暗。PWM做的就是这件事,只不过速度快到肉眼无法察觉。
2. 周期划分的注意事项
值得注意的是,一个周期最好划分为 10份或20份,不要再多了。
原因很简单:分的份数越多,每一份的时间间隔就越长(在同样的定时器中断频率下),灯亮灭切换的频率就越低,人眼就越容易感受到闪烁,效果反而变差。10份或20份是一个在"调光精度"和"视觉效果"之间比较合适的平衡点。
3. PWM调光代码实现
首先,定义两个变量:
idata unsigned char pwm_period; // 周期累加计数器
idata unsigned char pwm_compare; // 比较值,即目标亮度(0~9对应0%~90%)
pwm_period:每次定时器中断都自增,用来追踪当前处于周期的哪个阶段。pwm_compare:亮度设定值,比如设为7,就代表70%的亮度。
然后在定时器中断服务函数中编写调光逻辑:
// 调光
pwm_period = (++pwm_period) % 10;
if(pwm_period < pwm_compare)
Led_Disp(ucLed); // 亮:当前计数小于比较值,点亮LED
else
Led_Off(); // 灭:当前计数大于等于比较值,熄灭LED
逻辑很直观:把一个周期(0~9)想象成一把尺子,pwm_compare就是尺子上的刻度线。刻度线左边(小于比较值)灯亮,右边(大于等于比较值)灯灭。调整pwm_compare的值,就是在移动这条刻度线。
注意:如果不需要调光功能,
Led_Disp(ucLed)应该放在主循环或其他合适的位置调用,而不是放在这里。
二、串口通信(UART)
串口通信就像两个人通过电话线传递信息,一方发送,另一方接收,双方需要提前约定好"说话的语速"(波特率),否则就会鸡同鸭讲,接收到乱码。
1. 串口初始化
打开 STC-ISP,找到"串口波特率计算器"选项卡,按如下参数配置:
| 参数 | 设置值 |
|---|---|
| 系统频率 | 12 MHz |
| 波特率 | 9600 bps |
| UART | 串口1(通用) |
| 数据位 | 8位 |
| 定时器时钟 | 12T模式 |
| 波特率发生器 | 定时器2 |
| 串口中断 | 勾选使能 |
点击生成代码,复制到项目中,并在函数末尾手动添加 EA = 1;(开启总中断),否则串口中断永远不会触发。
完整初始化代码如下:
void Uart1_Init(void) // 9600bps@12.000MHz
{
SCON = 0x50; // 8位数据,可变波特率
AUXR |= 0x01; // 串口1选择定时器2为波特率发生器
AUXR &= 0xFB; // 定时器时钟12T模式
T2L = 0xE6; // 设置定时初始值
T2H = 0xFF; // 设置定时初始值
AUXR |= 0x10; // 定时器2开始计时
ES = 1; // 使能串口1中断
EA = 1; // 开启总中断(手动添加,不可遗漏!)
}
2. 串口重定向(printf支持)
默认情况下,C语言的 printf 函数是输出到屏幕的,但单片机没有屏幕。我们需要对底层的 putchar 函数进行"重定向",告诉它:你不要往屏幕输出,你往串口发数据。
这就好比把一条水管的出口从水桶改接到水缸——水(数据)还是那些水,只是流向变了。
// 串口重定向:将printf的输出重定向到串口
extern char putchar(char ch)
{
SBUF = ch; // 将字符写入串口发送缓冲区
while(TI == 0) // 等待发送完成(TI由硬件置1)
;
TI = 0; // 手动清除发送中断标志位
return ch;
}
TI 是发送中断标志位,硬件发送完一个字节后会自动将其置1,我们检测到它变为1后,手动清零,才能发送下一个字节。
3. 完整的 Uart.c 与 Uart.h
Uart.c
#include "Uart.h"
#include <stdio.h>
void Uart1_Init(void) // 9600bps@12.000MHz
{
SCON = 0x50; // 8位数据,可变波特率
AUXR |= 0x01; // 串口1选择定时器2为波特率发生器
AUXR &= 0xFB; // 定时器时钟12T模式
T2L = 0xE6; // 设置定时初始值
T2H = 0xFF; // 设置定时初始值
AUXR |= 0x10; // 定时器2开始计时
ES = 1; // 使能串口1中断
EA = 1;
}
// 串口重定向
extern char putchar(char ch)
{
SBUF = ch;
while(TI == 0)
;
TI = 0;
return ch;
}
Uart.h
#include <STC15F2K60S2.H>
void Uart1_Init(void);
4. 超时解析机制
为什么需要超时解析?
串口接收数据时,数据是一个字节一个字节地到来的,单片机并不知道"这一帧数据什么时候算接收完了"。超时解析的思路是:如果超过一定时间(比如10ms)没有收到新数据,就认为这一帧数据接收完毕,开始解析。
这就像你在等朋友发消息,如果他超过10秒没有继续发,你就认为他说完了,可以开始回复了。
相关变量定义(在 main.c 中)
idata unsigned char Uart_Rx_Index; // 串口接收缓冲区当前写入位置(索引)
pdata unsigned char Uart_Rx_Buf[10] = {10,10,10,10,10,10,10,10,10,10}; // 串口接收缓冲区
idata unsigned char Uart_Rx_Flag; // 串口接收标志位(1=正在接收中)
idata unsigned char Uart_Rx_Tick; // 超时计时器(单位:ms)
串口中断服务函数
每当串口收到一个字节,就会触发此中断:
// 串口中断服务函数
void Uart1_Isr(void) interrupt 4
{
if (RI) // 检测串口1接收中断
{
Uart_Rx_Flag = 1; // 标记正在接收
Uart_Rx_Tick = 0; // 每收到一个字节,超时计时器清零重计
Uart_Rx_Buf[Uart_Rx_Index++] = SBUF; // 将收到的字节存入缓冲区
RI = 0; // 清除串口1接收中断请求位(必须手动清除)
if(Uart_Rx_Index > 10) // 缓冲区溢出保护
{
Uart_Rx_Index = 0;
memset(Uart_Rx_Buf, 0, 10); // 批量清空数组(需要包含 <string.h>)
}
}
}
注意:这里删去了 TI(发送中断)相关的处理,因为发送已经在
putchar里处理了,中断服务函数里只需要关心接收。
定时器中断中的超时计时
在定时器1的中断服务函数(1ms触发一次)中,加入如下语句:
if(Uart_Rx_Flag) Uart_Rx_Tick++; // 只有在接收状态下才计时
每过1ms,Uart_Rx_Tick 就加1。如果连续10ms没有新数据到来(Uart_Rx_Tick 不被清零),就触发解析。
串口处理函数
/* 串口处理函数,在主循环中调用 */
void Uart_Proc()
{
unsigned char x, y;
if(Uart_Rx_Index == 0) return; // 缓冲区为空,直接返回
if(Uart_Rx_Tick >= 10) // 超过10ms没有新数据,认为一帧接收完毕
{
Uart_Rx_Flag = 0;
Uart_Rx_Tick = 0; // 复位标志和计时器
// 尝试按 (x,y) 格式解析,%bu 是 Keil C51 中 unsigned char 的格式符
if(sscanf(Uart_Rx_Buf, "(%bu,%bu)", &x, &y) == 2)
printf("I Get x=%bu,y=%bu\r\n", x, y);
else
printf("ERROR\r\n");
memset(Uart_Rx_Buf, 0, Uart_Rx_Index); // 清空已处理的缓冲区
Uart_Rx_Index = 0;
}
}
小提示:
%bu是 Keil C51 编译器专用的格式符,用于unsigned char类型。在标准C中对应的是%hhu,注意不要混用。
5. 调试流程:先回显,再解析
第一次使用串口时,建议先做回显测试,确认收发链路是通的,再进行格式解析。
第一步:回显测试
注释掉解析代码,改为直接回显:
// 注释掉解析部分
// if(sscanf(Uart_Rx_Buf,"(%bu,%bu)",&x,&y) == 2)
// printf("I Get x=%bu,y=%bu\r\n",x,y);
// else
// printf("ERROR\r\n");
printf("%s", Uart_Rx_Buf); // 回显,确保预期接收和真实接收一致
同时,可以在 Key_Proc 里加入按键触发测试:
if(Key_Down != 0)
printf("Key_Down=%bu", Key_Down);
第二步:验证回显
烧录程序后,打开 STC-ISP 的"USB-CDC/串口助手"选项卡,选择对应串口(如 COM6),点击"打开串口"。
- 按下开发板上编号4~15的任意按键,接收缓冲区应显示
Key_Down=4等信息。 - 在发送缓冲区随意输入几个字母,点击"发送数据",接收缓冲区应原样显示这些字母。
如果以上两点都正常,说明串口收发链路完全正常。
第三步:格式解析测试
取消注释解析代码,重新编译烧录。此时:
- 发送随意字母 → 接收区显示
ERROR(格式不匹配,符合预期) - 发送
(2,2)→ 接收区显示如下内容,说明解析成功:
[15:53:02.739] 接收 ← (2,2)
I Get x=2,y=2
6. 初始化顺序提醒
串口初始化函数 Uart1_Init() 的调用位置,建议放在定时器0初始化之后、定时器1初始化之前,保持初始化顺序的整洁和逻辑清晰。
三、总结
| 模块 | 核心思路 | 关键点 |
|---|---|---|
| PWM调光 | 用占空比控制LED亮灭时间比例 | 周期建议10或20份,避免闪烁 |
| 串口初始化 | STC-ISP生成代码,手动加EA=1 | 波特率双方必须一致 |
| 串口重定向 | 重写putchar,让printf走串口 | 发完一字节后手动清TI |
| 超时解析 | 超过10ms无新数据则解析一帧 | 每收到字节需清零Tick计时器 |
| 调试建议 | 先回显确认链路,再做格式解析 | %bu是C51专用格式符 |
更多推荐



所有评论(0)