先搞清楚:蓝桥杯单片机到底考什么?

很多新手上来就开始啃芯片手册,其实方向不对。蓝桥杯单片机赛道的核心是CT107D开发板,主控芯片是STC15F2K60S2。这个板子上集成了以下外设:

外设 接口 重要程度
8个LED灯 P0口+74HC573锁存器 必考
8位数码管 P0口+74HC573锁存器 必考
4x4矩阵键盘 P3/P4口 必考
蜂鸣器/继电器 P0口+74HC573锁存器 高频
DS18B20温度传感器 P1.4(单总线) 常考
DS1302实时时钟 P1.3/P1.7/P2.3 常考
PCF8591(ADC/DAC) P2.0/P2.1(I2C) 常考
AT24C02(EEPROM) P2.0/P2.1(I2C) 常考
超声波测距 P1.0/P1.1(PCA) 近年必考
NE555频率发生器 P3.4(T0计数) 国赛常见
UART串口通信 P3.0/P3.1 国赛必考

仔细看这个表你会发现,基础外设(LED、数码管、按键、I2C)每年必考,传感器(超声波、温度、RTC)是核心考点,而串口通信和综合应用才是拉开分差的关键。从第15届开始,国赛的难度明显上了一个台阶,串口成了必考项。


代码架构:为什么"西风模板"这么好用?

我分析了手里的满分代码,发现不管是省赛还是国赛,几乎都基于同一个架构——"西风模板"。这个模板的核心思想其实很朴素:

前台(定时器中断):数码管扫描 + 系统计时 + 必要的精确定时
后台(主循环):通过调度器按周期调用各任务函数

看一下样板代码里的调度器定义:

/* 定义调度器结构体 */
typedef struct {
    void (*task_func)(void);       // 任务函数指针
    unsigned long int rate_ms;     // 运行周期(毫秒)
    unsigned long int last_run;    // 上一次运行的时间戳
} task_c;

/* 配置调度器 —— 这就是你的"任务清单" */
idata task_c Scheduler_Task[] = {
    {Led_Proc,    1,    0},   // LED刷新 1ms
    {Key_Proc,    10,   0},   // 按键扫描 10ms
    {Seg_Proc,    100,  0},   // 数码管刷新 100ms
    {Uart_Proc,   10,   0},   // 串口处理 10ms
    {AD_DA,       160,  0},   // ADC读取 160ms
    {Read_Tem,    100,  0},   // 温度读取 100ms
    {Read_Time,   500,  0},   // 时间读取 500ms
};

主循环就一句话:

while(1) {
    Scheduler_Run();
}

调度器内部会检查每个任务的 rate_ms 和 last_run,时间到了就执行对应函数。这样你完全不需要在主循环里写一堆 if 判断,加减任务只需要改一行配置。比赛的时候,你只需要根据题目要求增删任务行、修改业务逻辑,框架本身完全不用动。

我在对比第15届国赛和第16届省赛的代码时发现,它们都直接复用了这个调度器,只是任务数量和内容不同:

/* 第15届国赛 —— 8个任务 */
{Led_Proc,    1,    0},
{Key_Proc,    10,   0},
{Seg_Proc,    100,  0},
{Uart_Proc,   10,   0},
{AD_DA,       160,  0},
{Read_Freq,   1000, 0},    // 新增:频率读取(NE555)
{Get_Distance, 300,  0},    // 新增:超声波测距
{Run_Calc,    500,  0}      // 新增:运动轨迹计算

你看,基础任务完全没变,新增的三个任务直接追加就行。这就是模板的价值——你在比赛前就把基础框架调通,比赛时只写业务代码。


按键处理的那个公式,理解了就很简单

西风模板里有一个看起来很巧妙的按键处理方式,一开始我看了好几遍才明白:

Key_Down = Key_Val & (Key_Val ^ Key_Old);   // 按下检测
Key_Up   = ~Key_Val & (Key_Val ^ Key_Old);  // 释放检测
Key_Old  = Key_Val;                          // 状态更新

拿一个具体例子走一遍就清楚了。假设按键S4按下(键值=4):

时间点1: Key_Val=0, Key_Old=0(无按键)
  Key_Down = 0 & (0^0) = 0    → 没有按下
  Key_Up   = ~0 & (0^0) = 0   → 没有释放

时间点2: Key_Val=4, Key_Old=0(S4刚按下!)
  Key_Down = 4 & (4^0) = 4 & 4 = 4    → 检测到S4按下 ✓
  Key_Up   = ~4 & (4^0) = ~4 & 4 = 0  → 无释放

时间点3: Key_Val=4, Key_Old=4(S4保持按下)
  Key_Down = 4 & (4^4) = 4 & 0 = 0    → 不会重复触发 ✓
  Key_Up   = ~4 & (4^4) = ~4 & 0 = 0  → 无释放

时间点4: Key_Val=0, Key_Old=4(S4刚释放)
  Key_Down = 0 & (0^4) = 0 & 4 = 0    → 无按下
  Key_Up   = ~0 & (0^4) = 0xFF & 4 = 4 → 检测到S4释放 ✓

核心逻辑就是:Key_Val ^ Key_Old 提取出"变化的部分",然后通过与运算判断是按下(当前值非零)还是释放(当前值为零)。这个公式配合10ms的调度周期,天然就实现了按键消抖——因为调度器保证每10ms才扫描一次。


main函数的标准骨架

这里直接给一个经过验证的main函数模板:

void main()
{
    System_Init();       // 第一步:初始化硬件(LED熄灭、蜂鸣器关闭)
    Uart_Init();         // 第二步:串口初始化(如果题目需要)
    Scheduler_Init();    // 第三步:计算任务数量(必须在Timer1之前!)
    Timer1Init();        // 第四步:启动系统时基(1ms中断)
    // Set_Rtc(ucRtc);   // 可选:设置RTC时间

    while(1) {
        Scheduler_Run(); // 主循环:交给调度器
    }
}

有一个细节很多人不注意:Scheduler_Init 必须放在 Timer1Init 之前。因为一旦启动了定时器中断,uwTick 就开始递增了,而调度器的 last_run 初始值为0,如果此时还没初始化任务数量,可能出问题。虽然大多数情况下不会报错,但养成好的初始化顺序习惯很重要。


项目目录结构

一个规范的蓝桥杯工程应该长这样:

Project/
├── Driver/          # 驱动层(比赛前写好,比赛中不改动)
│   ├── Init.c/h     # 系统初始化
│   ├── Led.c/h      # LED/继电器/蜂鸣器
│   ├── Seg.c/h      # 数码管
│   ├── Key.c/h      # 按键扫描
│   ├── iic.c/h      # I2C(PCF8591 + AT24C02)
│   ├── onewire.c/h  # 单总线(DS18B20)
│   ├── ds1302.c/h   # RTC时钟
│   ├── Uart.c/h     # 串口通信
│   ├── U_Wave.c/h   # 超声波测距
│   └── filtering.c/h# 滤波算法
└── User/
    └── main.c       # 业务逻辑(比赛时主要改这个文件)

这个分层的好处是,驱动层代码在备赛期间反复验证,比赛中完全不用动。你只需要在 main.c 里写业务逻辑,出了问题也只需要排查 main.c。

开发环境说明

  • Keil C251:官方推荐IDE,编译STC15代码
  • STC-ISP:下载程序到芯片
  • CH340驱动:USB转串口
  • 芯片选 STC15F2K60S2,晶振 12.000MHz

后面我会按模块逐个拆解驱动代码,每一篇都会配上实际的完整代码(从项目源码中直接提取),而不是伪代码。建议按照文章顺序阅读,先理解架构,再深入每个外设的实现细节。

Logo

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

更多推荐