蓝桥杯单片机学习笔记(十)——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专用格式符
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐