蓝桥杯单片机竞赛 · 完整模板指南

从零到精通
单片机模板全解析

基于STC15F2K60S2的完整驱动模板构建教程,包含12个核心模块:锁存器、LED、键盘、数码管、时钟、温度、IIC、串口、超声波、NE555测频等

开始学习 →

基础概念

理解锁存器、三八译码器和延时函数的工作原理

🔒

锁存器控制原理

通过P2的高三位(P25/P26/P27)控制Y4C-Y7C四个锁存器,实现对不同外设的独立控制

  • Y4C → LED控制 (0x80)
  • Y5C → 蜂鸣器/继电器/MOTOR (0xA0)
  • Y6C → 数码管位选 (0xC0)
  • Y7C → 数码管段选 (0xE0)
⏱️

NOP延时函数

使用intrins.h中的_nop_()函数实现微秒级精确延时,用于各种通信时序控制

  • 12MHz晶振:1 NOP ≈ 1μs
  • 用于DS1302、IIC等时序控制
  • 配合循环实现ms级延时

核心操作公式

锁存器操作的标准写法,确保高三位正确控制且不影响其他位

P2 = P2 & 0x1f | 0x80; // 打开Y4C P2 &= 0x1f; // 关闭锁存器

LED、蜂鸣器、继电器、MOTOR

掌握基础输出设备的控制方法与状态保持技巧

LED控制
蜂鸣器
继电器
MOTOR
Led.c - 带状态保持的LED控制
/// @brief LED扫描控制(带状态保持)
/// @param addr LED地址 0-7 对应 L1-L8
/// @param enable 0-熄灭 1-点亮
void Led_Disp(unsigned char addr, unsigned char enable) {
    static unsigned char temp = 0x00;      // 当前LED状态
    static unsigned char temp_old = 0xff;  // 上一次状态
    
    if (enable)
        temp |= 0x01 << addr;            // 置位对应位
    else
        temp &= ~(0x01 << addr);          // 清零对应位
    
    if (temp != temp_old) {               // 状态变化才更新(避免闪烁)
        P0 = ~temp;                        // 取反输出(共阳接法,1灭0亮)
        P2 = P2 & 0x1f | 0x80;            // Y4C锁存器打开
        P2 &= 0x1f;                       // 关闭锁存器
        temp_old = temp;
    }
}
💡

关键技巧

使用static变量保存状态,只在状态变化时更新锁存器,有效避免影响数码管显示,节省CPU资源

unsigned char temp_0 = 0x00;
unsigned char temp_old_0 = 0xff;

void Beep(bit enable) {
    if (enable)
        temp_0 |= 0x40;        // BUZZ对应位 0100_0000 = 0x40
    else
        temp_0 &= ~(0x40);
    
    if (temp_0 != temp_old_0) {
        P0 = temp_0;
        P2 = P2 & 0x1f | 0xa0; // Y5C锁存器
        P2 &= 0x1f;
        temp_old_0 = temp_0;
    }
}
void Relay(bit enable) {
    if (enable)
        temp_0 |= 0x10;        // RELAY对应位 0001_0000 = 0x10
    else
        temp_0 &= ~(0x10);
    
    if (temp_0 != temp_old_0) {
        P0 = temp_0;
        P2 = P2 & 0x1f | 0xa0;
        P2 &= 0x1f;
        temp_old_0 = temp_0;
    }
}
void MOTOR(bit enable) {
    if (enable)
        temp_0 |= 0x20;        // MOTOR对应位 0010_0000 = 0x20
    else
        temp_0 &= ~(0x20);
    
    if (temp_0 != temp_old_0) {
        P0 = temp_0;
        P2 = P2 & 0x1f | 0xa0;
        P2 &= 0x1f;
        temp_old_0 = temp_0;
    }
}
设备 对应引脚 控制值 锁存器
LED (L1-L8)Q0-Q70x80Y4C
RELAY (L10)Q40x10Y5C (0xA0)
MOTORQ50x20
BUZZ (蜂鸣器)Q60x40

键盘扫描与消抖

4×3矩阵键盘扫描 + 精妙的三行代码消抖原理

Key.c - 4×3矩阵键盘
unsigned char Key_Read() {
    unsigned char temp = 0;
    
    // 关闭定时器1中断,避免与串口冲突
    ET1 = 0;
    
    // 第一列 (P44=0)
    P44 = 0; P42 = 1; P35 = 1;
    if (P33 == 0) temp = 4;
    if (P32 == 0) temp = 5;
    if (P31 == 0) temp = 6;
    if (P30 == 0) temp = 7;
    
    // 第二列 (P42=0)
    P44 = 1; P42 = 0; P35 = 1;
    if (P33 == 0) temp = 8;
    if (P32 == 0) temp = 9;
    if (P31 == 0) temp = 10;
    if (P30 == 0) temp = 11;
    
    // 第三列 (P35=0) - 已删除P34避免NE555冲突
    P44 = 1; P42 = 1; P35 = 0;
    if (P33 == 0) temp = 12;
    if (P32 == 0) temp = 13;
    if (P31 == 0) temp = 14;
    if (P30 == 0) temp = 15;
    
    // 恢复P3口并开启中断
    P3 = 0xff;
    ET1 = 1;
    
    return temp;
}
Key_Proc - 三行代码消抖
void Key_Proc() {
    static unsigned char Key_Val, Key_Down, Key_Up, Key_Old;
    
    if (time_all_1s % 10) return;    // 10ms执行一次
    
    Key_Val = Key_Read();               // 读取当前值
    Key_Down = Key_Val & (Key_Old ^ Key_Val);   // 按下检测
    Key_Up = ~Key_Val & (Key_Old ^ Key_Val);    // 抬起检测
    Key_Old = Key_Val;                  // 保存旧值
    
    // 使用示例
    if (Key_Down == 4) {
        // S4按下执行的代码
    }
}

🔍 消抖原理详解(位运算分析)

键盘状态 Key_Old Key_Val Key_Old^Key_Val Key_Down Key_Up
未按下00000000000000000000
按下过程中00000100010001000000
按下稳定01000100000000000000
抬起过程中01000000010000000100

关键:Key_Down只在按下瞬间为按键值,其余时刻为0;Key_Up只在抬起瞬间为按键值

⚠️

2024年重要更新

省赛和国赛都考了NE555测量,使用P34引脚,与键盘第四列冲突。建议直接使用4×3键盘布局(删除P34相关代码)。

数码管显示模板

共阳极数码管的位选、段选与消隐处理

🔢

段码表推导

共阳极数码管:0亮1灭,按照 dp-g-f-e-d-c-b-a 顺序编码

  • 数字"0": 1100_0000 = 0xC0
  • 数字"2": 1010_0100 = 0xA4
  • 数字"8": 1000_0000 = 0x80
  • 熄灭: 1111_1111 = 0xFF
👁️

消隐处理

位选切换前先将段码置0xFF,避免显示残影和鬼影现象

  • 先消隐:P0 = 0xFF
  • 再位选:打开Y6C锁存器
  • 后段选:打开Y7C锁存器

小数点控制

小数点对应DP段(最高位),通过 &0x7F 实现点亮

if(point) P0 &= 0x7F; // 0x7F = 0111_1111
Seg.c - 带消隐的数码管显示
// 段码表:0-9 + 灭(10) + A(11)
code unsigned char seg_dula[] = {
    0xc0, 0xf9, 0xa4, 0xb0, 0x99,   // 0-4
    0x92, 0x82, 0xf8, 0x80, 0x90,   // 5-9
    0xff, 0x88                            // 灭, A
};

/// @brief 数码管显示函数
/// @param wela 位选 0-7(对应COM1-8)
/// @param dula 段选 0-9(对应显示数字)
/// @param point 小数点 0-无 1-有
void Seg_Disp(unsigned char wela, unsigned char dula, unsigned char point) {
    // 1. 消隐(关键!避免鬼影)
    P0 = 0xff;
    P2 = P2 & 0x1f | 0xe0;   // Y7C - 段选锁存器
    P2 &= 0x1f;
    
    // 2. 位选
    P0 = 0x01 << wela;       // 选择第wela位
    P2 = P2 & 0x1f | 0xc0;   // Y6C - 位选锁存器
    P2 &= 0x1f;
    
    // 3. 段选
    P0 = seg_dula[dula];
    if (point) P0 &= 0x7f;   // 显示小数点(DP置0)
    P2 = P2 & 0x1f | 0xe0;   // Y7C
    P2 &= 0x1f;
}

DS1302时钟模板

实时时钟芯片的读写与BCD码转换

📌 读写地址说明

操作小时分钟秒钟写保护
读取0x850x830x81-
写入0x840x820x800x8E

写入前需将0x8E的WP位置0解除写保护,写入后置1开启保护

🔄 BCD码转换原理

十进制 → BCD: 23 → 2,3 → 0010,0011 → 0x23

temp = (ucRtc[i] / 10) << 4 | (ucRtc[i] % 10);

BCD → 十进制: 0x23 → 0x2,0x3 → 2*10+3 → 23

ucRtc[i] = (temp >> 4) * 10 + (temp & 0x0f);
Ds1302.c - 时钟读写与BCD转换
// 引脚定义
sbit SDA = P2 ^ 3;
sbit RST = P1 ^ 3;
sbit SCK = P1 ^ 7;

/// @brief 写入时间(十进制输入,自动转BCD)
/// @param ucRtc 时间数组 [时,分,秒]
void Set_Rtc(unsigned char *ucRtc) {
    unsigned char i, temp;
    
    Write_Ds1302_Byte(0x8e, 0x00);      // 解除写保护
    
    for (i = 0; i < 3; i++) {
        // 十进制转BCD: 23 -> 0x23
        temp = (ucRtc[i] / 10) << 4 | (ucRtc[i] % 10);
        Write_Ds1302_Byte(0x84 - 2 * i, temp);
    }
    
    Write_Ds1302_Byte(0x8e, 0x80);      // 开启写保护
}

/// @brief 读取时间(BCD转十进制)
void Read_Rtc(unsigned char *ucRtc) {
    unsigned char i, temp;
    
    for (i = 0; i < 3; i++) {
        temp = Read_Ds1302_Byte(0x85 - 2 * i);
        // BCD转十进制: 0x23 -> 23
        ucRtc[i] = (temp >> 4) * 10 + (temp & 0x0f);
    }
}
📝

使用示例

unsigned char time[3] = {11, 12, 13}; // 时:分:秒
Set_Rtc(time); // 设置时间
Read_Rtc(time); // 读取时间

Onewire温度传感器

DS18B20单总线温度采集与精度配置

🌡️

操作流程

  • 初始化总线(init_ds18b20)
  • 跳过ROM检查(0xCC)
  • 启动温度转换(0x44)
  • 延时等待转换完成
  • 读取温度值(0xBE)
📊

精度配置

  • 9位精度:0.5°C 分辨率
  • 10位精度:0.25°C 分辨率
  • 11位精度:0.125°C 分辨率
  • 12位精度:0.0625°C(默认)
🔌

计算公式

L = 340m/s × T / 2 = 0.017 × time(us)

return (0.017 * time + 3);
Onewire.c - 温度读取
sbit DQ = P1 ^ 4;

/// @brief 读取DS18B20温度值
/// @return 温度值(浮点数,单位:°C)
float rd_temperature() {
    unsigned char low, high;
    
    // 1. 启动温度转换
    init_ds18b20();
    Write_DS18B20(0xcc);      // 跳过ROM
    Write_DS18B20(0x44);      // 启动转换
    Delay_OneWire(200);       // 等待转换完成(必须延时)
    
    // 2. 读取温度值
    init_ds18b20();
    Write_DS18B20(0xcc);      // 跳过ROM
    Write_DS18B20(0xbe);      // 读取暂存器
    
    low = Read_DS18B20();        // 低位字节
    high = Read_DS18B20();       // 高位字节
    
    // 合成温度值并乘以精度(12位默认0.0625)
    return (float)(high << 8 | low) * 0.0625;
}

IIC通信模板

PCF8591(AD/DA)与AT24C02(EEPROM)的IIC操作

IIC起始
I2CStart()
发送设备地址
0x90/0xA0
等待应答
I2CWaitAck()
发送数据
I2CSendByte()
IIC停止
I2CStop()
AD采样
DA输出
EEPROM读取
EEPROM写入
/// @brief PCF8591 DA输出
/// @param dat 数字值 0-255 对应 0-5V
void Da_Write(unsigned char dat) {
    I2CStart();
    I2CSendByte(0x90);          // PCF8591写地址
    I2CWaitAck();
    I2CSendByte(0x41);          // 开启DA输出 + AIN1通道
    I2CWaitAck();
    I2CSendByte(dat);           // 发送数字值
    I2CWaitAck();
    I2CStop();
}

// 电压换算:数字值 = 电压(V) × 51
// 例如:输出2V -> Da_Write(2 * 51)
// 例如:输出2.5V -> Da_Write(2.5 * 51)
/// @brief AT24C02 读取数据
/// @param str 数据存储数组
/// @param addr EEPROM内部地址
/// @param num 读取字节数
void EEPROM_Read(unsigned char *str, unsigned char addr, unsigned char num) {
    I2CStart();
    I2CSendByte(0xa0);          // AT24C02写地址
    I2CWaitAck();
    I2CSendByte(addr);          // 设置读取地址
    I2CWaitAck();
    
    I2CStart();
    I2CSendByte(0xa1);          // AT24C02读地址
    I2CWaitAck();
    
    while (num--) {
        *str++ = I2CReceiveByte();
        if (num)
            I2CSendAck(0);       // 继续读取
        else
            I2CSendAck(1);       // 读取完成
    }
    I2CStop();
}
/// @brief AT24C02 写入数据
/// @param str 数据数组
/// @param addr EEPROM内部地址
/// @param num 写入字节数
void EEPROM_Write(unsigned char *str, unsigned char addr, unsigned char num) {
    I2CStart();
    I2CSendByte(0xa0);          // AT24C02写地址
    I2CWaitAck();
    I2CSendByte(addr);          // 设置写入地址
    I2CWaitAck();
    
    while (num--) {
        I2CSendByte(*str++);
        I2CWaitAck();
        I2C_Delay(200);         // 写入延时
    }
    
    I2CStop();
    // 必须延时等待写入完成(重要!)
    I2C_Delay(255);
    I2C_Delay(255);
    I2C_Delay(255);
    I2C_Delay(255);
}

UART串口通信

使用定时器2的串口初始化与printf重定向

⚙️

ISP配置参数

  • 波特率:9600bps
  • 时钟:12MHz
  • 定时器:定时器2
  • 数据位:8位
  • 定时器时钟:12T
📝

printf重定向原理

通过重载putchar函数,将printf输出重定向到串口

  • printf调用putchar
  • SBUF发送数据
  • TI标志判断完成
Uart.c - 串口初始化与重定向
/// @brief 串口1初始化 9600bps@12.000MHz
void Uart1_Init(void) {
    SCON = 0x50;           // 8位数据,可变波特率
    AUXR |= 0x01;          // 串口1选择定时器2
    AUXR |= 0x04;          // 定时器时钟1T模式
    T2L = 0xC7;            // 定时器初值低8位
    T2H = 0xFE;            // 定时器初值高8位
    AUXR |= 0x10;          // 定时器2开始计时
    ES = 1;                // 使能串口1中断
    EA = 1;                // 开启总中断
}

/// @brief putchar重定向,支持printf
char putchar(char ch) {
    SBUF = ch;              // 发送数据
    while (!TI);            // 等待发送完成
    TI = 0;                 // 清除标志
    return ch;
}
⚠️

硬件冲突注意

串口使用P30(RXD)和P31(TXD)引脚,与键盘扫描存在硬件冲突。在Key_Read()函数中需要关闭定时器1中断(ET1=0)并在结束后恢复(ET1=1)。

超声波测距模块

PCA定时器实现超声波发送与接收测距

📡

测距原理

发送8个40kHz方波,测量回波时间,计算距离

  • 声速:340m/s
  • 距离公式:L = 0.017 × time(us)
  • +3修正提高远距离精度
⏱️

PCA定时器

使用PCA模块计时,节省定时器资源

  • TLx→CL, THx→CH
  • TMOD→CMOD
  • TRx→CR
  • TFx→CF
Ul.c - 超声波测距
sbit Tx = P1 ^ 0;    // 发送引脚
sbit Rx = P1 ^ 1;    // 接收引脚

// 12us延时(ISP生成,可调整为38)
void Delay12us(void) {
    unsigned char i;
    _nop_(); _nop_();
    i = 33;    // 可修改为38
    while (--i);
}

// 发送8个40kHz方波
void Ut_Wave_Init() {
    unsigned char i;
    for (i = 0; i < 8; i++) {
        Tx = 1;
        Delay12us();
        Tx = 0;
        Delay12us();
    }
}

/// @brief 超声波测距
/// @return 距离值(cm),0表示测量错误
unsigned char Ut_Wave_Data() {
    unsigned int time;
    
    CH = CL = 0;           // 清零计数器
    CMOD = 0x00;           // 16位不重载
    
    EA = 0;
    Ut_Wave_Init();        // 发送超声波
    EA = 1;
    
    CR = 1;                // 开始计时
    while (Rx && !CF);     // 等待接收或溢出
    CR = 0;                // 停止计时
    
    if (!CF) {             // 正常接收
        time = CH << 8 | CL;
        return (0.017 * time + 3);  // 距离计算
    } else {               // 溢出错误
        CF = 0;
        return 0;
    }
}

NE555频率测量

定时器0计数 + 定时器1定时 = 精准测频

📊

测频原理

定时器0作为计数器,定时器1每秒读取一次计数值

  • TMOD |= 0x05 设置T0为计数模式
  • TL0 = TH0 = 0 清零计数
  • 1秒后读取TH0<<8 | TL0
⚠️

硬件连接

P34引脚与SIGNAL跳线帽连接

  • 注意:P34与键盘第四列冲突
  • 使用4×3键盘布局
  • 删除Key_Read中的P34代码
NE555测频 - 定时器配置
/// @brief 定时器0初始化 - 用于NE555计数
void Timer0_Init(void) {
    AUXR &= 0x7F;        // 定时器时钟12T模式
    TMOD &= 0xF0;        // 清除T0配置
    TMOD |= 0x05;        // T0设置为16位计数模式
    TL0 = 0;             // 计数初值低8位
    TH0 = 0;             // 计数初值高8位
    TF0 = 0;             // 清除TF0标志
    TR0 = 1;             // 定时器0开始计数
    EA = 1;
}

/// @brief 定时器1中断 - 1秒读取频率
void Timer1_Isr(void) interrupt 3 {
    if (++time_all_1s == 1000) {    // 1秒到达
        time_all_1s = 0;
        Freq = (TH0 << 8) | TL0;     // 读取计数值
        TH0 = TL0 = 0;              // 清零计数器
    }
    // ... 其他中断处理
}

主函数框架

模块化编程与定时器中断调度完整示例

📋 完整main.c代码

#include "main.h"

/* ==================== 全局变量定义 ==================== */
unsigned char ucLed[8] = {0};
unsigned char Seg_Pos;
unsigned char Seg_Buf[8] = {10,10,10,10,10,10,10,10};
unsigned char Seg_Point[8] = {0};
unsigned char Uart_Buf[10];
unsigned char Uart_Rx_Index;
bit Uart_flag;
unsigned char Sys_Tick;
unsigned char ucRtc[3] = {11,11,11};
unsigned int time_all_1s;
unsigned int Freq;

/* ==================== 数据读取模块 ==================== */
void Data_Proc() {
    if (time_all_1s % 50 == 0) {
        Read_Rtc(ucRtc);           // 50ms读取时间
    }
    if (time_all_1s % 100 == 0) {
        // 100ms读取AD
    }
    if (time_all_1s % 500 == 0) {
        // 500ms读取温度
    }
}

/* ==================== 键盘处理 ==================== */
void Key_Proc() {
    static unsigned char Key_Val, Key_Down, Key_Up, Key_Old;
    if (time_all_1s % 10) return;
    
    Key_Val = Key_Read();
    Key_Down = Key_Val & (Key_Old ^ Key_Val);
    Key_Up = ~Key_Val & (Key_Old ^ Key_Val);
    Key_Old = Key_Val;
    
    if (Key_Down == 4) {
        // S4按下处理
    }
}

/* ==================== 数码管处理 ==================== */
void Seg_Proc() {
    if (time_all_1s % 20) return;
    // 界面切换与显示更新
}

/* ==================== 定时器1中断 ==================== */
void Timer1_Isr(void) interrupt 3 {
    unsigned char i;
    
    if (++time_all_1s == 1000) {
        time_all_1s = 0;
        Freq = (TH0 << 8) | TL0;   // 读取NE555频率
        TH0 = TL0 = 0;
    }
    
    if (Uart_flag) Sys_Tick++;      // 串口超时计数
    
    Seg_Pos = (++Seg_Pos) % 8;     // 数码管位选
    Seg_Disp(Seg_Pos, Seg_Buf[Seg_Pos], Seg_Point[Seg_Pos]);
    
    for (i = 0; i < 8; i++)
        Led_Disp(i, ucLed[i]);      // LED扫描
}

/* ==================== 串口中断 ==================== */
void Uart1_Isr(void) interrupt 4 {
    if (RI) {
        Uart_flag = 1;
        Sys_Tick = 0;
        Uart_Buf[Uart_Rx_Index++] = SBUF;
        RI = 0;
    }
    if (Uart_Rx_Index > 10) Uart_Rx_Index = 0;
}

/* ==================== 主函数 ==================== */
void main() {
    System_Init();           // 系统初始化
    Timer0_Init();           // NE555计数器
    Timer1_Init();           // 轮询定时器
    Set_Rtc(ucRtc);
    rd_temperature();
    Delay750ms();
    
    while (1) {
        Data_Proc();
        Key_Proc();
        Seg_Proc();
        Led_Proc();
    }
}
💡

编程思想

采用轮询方式,通过定时器产生不同时间间隔的标志位,在main循环中调度各个功能模块。数码管和LED在中断中扫描,保证显示稳定;业务逻辑在主循环中处理,避免中断中执行耗时操作。

0%