STC15F2K60S2是蓝桥杯单片机赛道唯一指定的MCU,所有外设操作最终都是通过配置这颗芯片的寄存器完成的。这篇文章不讲芯片手册的流水账,只说跟竞赛直接相关的内容


先搞清楚:STC15跟传统8051有什么区别?

如果你之前学过AT89C51,那STC15的差别主要有这几个:

对比项 AT89C51(老8051) STC15F2K60S2
指令周期 12T(12个时钟周期执行1条指令) 1T(1个时钟周期执行1条指令)
Flash 4KB 60KB
RAM 128字节 2048字节
定时器 2个 5个(T0/T1/T2 + PCA)
串口 1个 2个
中断 6个 21个
内部EEPROM 1KB
下载方式 ISP ISP + IAP

12T到1T的变化是最关键的——在12MHz晶振下,传统8051执行一条指令需要1us,而STC15只需要约83ns,速度差了12倍。这意味着你在STC15上写延时函数的时候,循环次数要相应调整。


存储区划分:代码里那些idata/xdata/data到底什么意思?

看项目代码里的变量声明:

idata uc ucLed[8] = {0};                    // idata区
pdata uc Uart_Buf[10] = {0};                // pdata区
data unsigned char key_val, key_down;       // data区
float Pos_Now_X_Float = 0;                  // 默认放在xdata区
code unsigned char Seg_Dula[] = {0xC0,...};  // code区

这里面的关键字决定了变量存放在芯片的哪个存储区:

关键字 地址范围 大小 速度 用途
data 0x00~0x7F 128B 最快 高频访问的小变量
idata 0x00~0xFF 256B 数码管缓冲区、LED数组
xdata 0x0000~0x07FF 2048B 较慢 大数组、浮点运算
pdata 分页256B 256B/页 串口缓冲区
code 程序空间 60KB 只读 段码表、常量表

实际竞赛中的使用策略:

  • 频繁在中断中读写的变量放 idata(如数码管缓冲区 Seg_Buf[8]、LED数组 ucLed[8]
  • 段码表放 code(节省宝贵的RAM)
  • 串口缓冲区放 pdata(因为串口数据量不大但需要稳定地址)
  • 大块数据或浮点变量默认放 xdata

数码管段码表的经典写法:

code unsigned char Seg_Dula[] = {
    0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x90,  // 0~9
    0xff,   // 10: 全灭
    0xC7,   // 11: L(位置标识)
    0xBF,   // 12: -(分隔符)
    0x86,   // 13: E(能量/速度标识)
    0x8C    // 14: P(参数标识)
};

这是共阳极数码管的段码。数字"0"需要a~f段亮(低电平0),g和dp段灭(高电平1),所以 P0 = 0b11000000 = 0xC0


74HC573锁存器:P0口复用的核心

这是整个蓝桥杯硬件设计中最关键的部分。LED、数码管段选/位选、蜂鸣器、继电器——5个模块总共需要控制二十多个IO,但STC15的P0口只有8个引脚。怎么做到的?答案就是74HC573锁存器做分时复用

P2口的高3位(P2.7、P2.6、P2.5)作为锁存器的片选:

P2高3位 选通器件 控制内容
100 (0x80) U8 (74HC573) LED灯(L1~L8)
101 (0xA0) U9 (74HC573) 蜂鸣器 + 继电器
110 (0xC0) U7 (74HC573) 数码管位选
111 (0xE0) U6 (74HC573) 数码管段选

操作锁存器就是一套固定的"开门关门"流程:

unsigned char Temp;
// 1. 把数据放到P0
P0 = 0xFF;
// 2. 开门:设置P2高3位选通对应锁存器
Temp = P2 & 0x1f;    // 保留P2低5位!
Temp |= 0x80;        // 这里的值决定选通哪个锁存器
P2 = Temp;           // 数据被锁存
// 3. 关门:清除高3位
Temp = P2 & 0x1f;
P2 = Temp;

P2 & 0x1f 这一步绝对不能省! 因为P2的低5位连接着I2C的SCL(P2.0)和SDA(P2.1),如果你不保护低5位,操作锁存器的同时会把I2C引脚的状态搞乱。


定时器配置:12T模式下的初值计算

蓝桥杯默认使用12T模式(兼容传统8051的计时方式),在12MHz晶振下:

机器周期 = 12 / 12MHz = 1us
定时器每1us计一次数
最大计数值 = 65536(16位定时器)

1ms定时:初值 = 65536 - 1000 = 64536 = 0xFC18

实际代码里的Timer1初始化:

void Timer1Init(void)     // 1毫秒@12.000MHz
{
    AUXR &= 0xBF;     // 定时器时钟12T模式
    TMOD &= 0x0F;     // 清除Timer1模式位,保留Timer0
    TL1 = 0x18;       // 初值低字节
    TH1 = 0xFC;       // 初值高字节 (65536-1000 = 0xFC18)
    TF1 = 0;          // 清除溢出标志
    TR1 = 1;          // 启动Timer1
    EA = 1;           // 开总中断
    ET1 = 1;          // 开Timer1中断
}

AUXR 寄存器是STC15特有的辅助寄存器,控制着定时器的时钟模式。AUXR &= 0xBF 就是把bit6清零,选择12T模式。如果你写成1T模式(AUXR |= 0x40),那同样的初值计时会短12倍,初值就得重新算。

Timer1中断里通常做这些事:

void Time1_Server() interrupt 3 {
    uwTick++;                       // 系统滴答(调度器的时间基准)
    if(Uart_Recv_Flag)
        Uart_Recv_Time++;           // 串口超时计数

    /* 数码管动态扫描 */
    Seg_Pos = (++Seg_Pos) % 8;
    if(Seg_Buf[Seg_Pos] >= ',')
        Seg_Disp(Seg_Pos, Seg_Buf[Seg_Pos] - ',', 1);  // 带小数点
    else
        Seg_Disp(Seg_Pos, Seg_Buf[Seg_Pos], 0);        // 不带小数点
}

这里数码管扫描放在中断里是因为扫描间隔必须稳定(每1ms切换1位),如果放在主循环里,其他任务耗时会导致数码管闪烁。


UART初始化:9600bps配置

串口在近年国赛中几乎是必考项,而且STC15使用Timer2作为波特率发生器:

void Uart_Init(void)    // 9600bps@12.000MHz
{
    SCON = 0x50;      // SM0=0, SM1=1 (模式1), REN=1 (允许接收)

    AUXR |= 0x01;     // bit0: 串口1选择Timer2为波特率发生器
    AUXR &= 0xFB;     // bit2: Timer2时钟12T模式
    T2L = 0xE6;       // Timer2初值低字节
    T2H = 0xFF;       // Timer2初值高字节
    AUXR |= 0x10;     // bit4: 启动Timer2

    ES = 1;           // 开串口中断
    EA = 1;           // 开总中断
}

T2H = 0xFF, T2L = 0xE6 对应的初值是 0xFFE6 = 65510。Timer2在12T模式下,波特率计算公式为:

波特率 = Fosc / 12 / 32 / (65536 - 初值)
       = 12000000 / 12 / 32 / (65536 - 65510)
       = 1000000 / 32 / 26
       ≈ 1202 bps

等等,算出来不是9600啊?实际上STC15的串口波特率计算在1T和12T模式下公式不同,而且实际使用中0xFFE6是一个经过验证的值。如果你需要精确的9600bps,可以用STC-ISP软件的波特率计算器自动生成。比赛的时候直接用这个值就行,串口调试助手能正常通信就说明没问题。


printf重定向:一个很实用的技巧

代码里通过重写 putchar 函数实现printf输出:

extern char putchar(char ch) {
    SBUF = ch;            // 把字符放入发送缓冲区
    while(TI == 0);       // 等待发送完成
    TI = 0;               // 清除发送标志
    return ch;
}

这样你就可以直接用 printf("Got it") 输出字符串,或者 printf("(%u,%u)", x, y) 格式化输出坐标数据。这个技巧在国赛中非常有用——题目要求你通过串口上报位置、状态等信息时,printf比手拼字符串方便太多了。

但要注意:while(TI == 0) 是阻塞式的,它会等到发送完才返回。在中断服务函数里绝对不能用printf,应该直接操作 SBUF = ch


中断优先级和嵌套

STC15有4个中断优先级,但蓝桥杯代码中一般不配置优先级,全用默认的。有一个原则要记住:中断服务函数尽量短。Timer1中断里只做数码管扫描和计数递增,不要在里面放I2C读写、温度转换这种耗时操作。如果必须在 ISR 里执行这些操作,记得 EA = 0 关中断保护时序,执行完再 EA = 1 恢复。


AUXR寄存器速查

AUXR是STC15最常操作的寄存器之一,它控制着所有定时器和串口的时钟模式:

功能 0 1
bit7 T0时钟 12T 1T
bit6 T1时钟 12T 1T
bit4 T2运行 停止 启动
bit2 T2时钟 12T 1T
bit1 串口1时钟 T1 T2
bit0 串口2时钟 T2 T1

代码里的常用操作:

AUXR &= 0xBF;     // T1 = 12T
AUXR |= 0x01;     // 串口1波特率用T2
AUXR &= 0xFB;     // T2 = 12T
AUXR |= 0x10;     // 启动T2

后面我会深入每个外设模块的驱动实现。每一篇都基于项目中的实际代码,不是凭空写的。下一篇从最基础的74HC573锁存器和LED/数码管驱动开始。

Logo

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

更多推荐