1. CT107D开发板与蓝桥杯省赛的硬核组合

第一次拿到CT107D开发板的时候,我盯着那块印着IAP15F2K61S2的芯片看了半天。这块看似普通的开发板,却是蓝桥杯单片机省赛的指定平台,每年都有成千上万的电子爱好者通过它来证明自己的实力。

这块板子的特别之处在于它的"五脏俱全"——LED、数码管、独立按键、矩阵键盘、EEPROM、ADC/DAC转换、温度传感器等外设一应俱全。我记得第一次调试数码管显示时,因为没处理好段选和位选的时序,显示出来的数字全是乱的,折腾了整整一个下午才发现问题所在。这种"踩坑"经历在蓝桥杯备赛中太常见了,但正是这些实战经验让我对硬件编程有了更深的理解。

蓝桥杯省赛的题目设计非常考验选手的综合能力。就拿第十二届省赛真题来说,它要求选手在有限的时间内完成多任务处理:实时温度采集与显示、按键参数设置、DA电压输出模式切换、LED状态指示等。这些功能看似独立,实则环环相扣,一个处理不好就会导致整个系统崩溃。

2. 核心代码架构设计之道

2.1 定时器中断:系统的心跳

在单片机编程中,定时器就像系统的心跳。我习惯把定时器中断配置为1ms触发一次,这个节奏不快不慢刚刚好。在中断服务函数里,我会做三件事:

  1. 维护一个全局的毫秒计数器ms_Tick,这个变量特别重要,相当于系统的时间基准
  2. 更新各个任务的减速计数器,控制任务执行频率
  3. 处理数码管动态扫描和LED刷新这些实时性要求高的任务
void tm1_isr() interrupt 3 
{
    ms_Tick++; // 系统心跳,能记录长达29天的时间
    if(++Key_Slow_Down == 10) Key_Slow_Down = 0; // 按键扫描减速
    if(++Seg_Slow_Down == 500) Seg_Slow_Down = 0; // 数码管刷新减速
    if(++Led_Slow_Down == 100) Led_Slow_Down = 0; // LED状态更新减速
    if(++Volatge_Output_Slow_Down == 500) Volatge_Output_Slow_Down = 0; // DA输出减速
    
    Seg_Disp(seg_buf, pos); // 数码管动态扫描
    if(++pos == 8) pos = 0;
    Led_Disp(ucLed); // LED状态刷新
}

这种设计最大的好处是各任务之间互不干扰。比如按键扫描每10ms执行一次,数码管显示每500ms更新一次,DA输出也是500ms刷新一次。通过调整这些减速计数器的阈值,可以灵活控制每个任务的执行频率。

2.2 状态机编程:让复杂逻辑变简单

省赛题目中经常需要处理多种界面切换和模式选择,这时候状态机就派上大用场了。我习惯用Disp_Interface这个变量来记录当前显示界面:

  • 0:温度显示界面
  • 1:参数设置界面
  • 2:DA输出界面

在按键处理函数中,通过检测按键事件来切换状态:

switch(Key_Down) {
    case 4: // 显示界面切换按键
        if(++Disp_Interface == 3) Disp_Interface = 0;
        if(Disp_Interface == 2) 
            Temp_Compare_Ctrl = Temp_Compare_Disp; // 使能设定的比较值
        break;
    // 其他按键处理...
}

这种状态机的写法让代码逻辑特别清晰。在数码管显示函数中,根据当前状态决定显示内容:

switch(Disp_Interface) {
    case 0: // 温度显示
        sprintf(seg_string,"C %5.2f",Temp);
        break;
    case 1: // 参数设置
        sprintf(seg_string,"P %02d",(unsigned int)Temp_Compare_Disp);
        break;
    case 2: // DA输出界面
        sprintf(seg_string,"A %4.2f",DA_Inter_Num/51.0);
        break;
}

3. 模块化编程实战技巧

3.1 外设驱动封装的艺术

好的代码应该像搭积木一样方便。我把所有外设操作都封装成了独立的模块:

  • bsp_key.c:按键扫描驱动
  • bsp_seg.c:数码管显示驱动
  • bsp_led.c:LED控制驱动
  • bsp_onewire.c:单总线温度传感器驱动
  • bsp_iic.c:I2C接口驱动(用于PCF8591)

每个模块都有对应的.h头文件,声明外部可调用的函数和变量。比如数码管模块就提供了Seg_Tran()和Seg_Disp()两个主要函数:

// 在bsp_seg.h中声明
extern void Seg_Tran(unsigned char *str, unsigned char *buf);
extern void Seg_Disp(unsigned char *buf, unsigned char pos);

// 在main.c中使用
Seg_Tran(seg_string, seg_buf); // 将字符串转换为数码管段码

这种模块化设计最大的好处是代码复用性高。备赛时,我会提前准备好这些驱动模块,比赛时直接调用,省去了重复编写底层驱动的时间。

3.2 全局变量的合理使用

单片机编程中,全局变量是把双刃剑。用得好能提高效率,用不好会导致各种奇怪的问题。我的经验是:

  1. 把相关变量集中声明,并加上详细注释
  2. 对外部模块需要访问的变量,在.h文件中用extern声明
  3. 尽量减少全局变量的数量,能用局部变量解决的就不用全局变量

比如下面这些变量声明就很有代表性:

//--定时器减速变量
unsigned int Key_Slow_Down;   // 按键扫描减速
unsigned int Seg_Slow_Down;   // 数码管刷新减速
unsigned int Led_Slow_Down;   // LED状态更新减速
unsigned int Volatge_Output_Slow_Down; // DA输出减速

//--按键专用变量
unsigned char Key_Value;     // 当前按键值
unsigned char Key_Old;       // 上次按键值
unsigned char Key_Down;      // 按键下降沿

//--数码管专用变量
unsigned char seg_string[10]; // 显示内容字符串缓存
unsigned char seg_buf[8];     // 数码管段码缓存

4. 省赛真题深度解析

4.1 温度采集与显示的实现

DS18B20温度传感器通过单总线协议通信,这里有个坑要注意:读取温度前必须先调用转换命令,然后等待转换完成。我封装了一个rd_temperature()函数来处理这些细节:

float Temp = rd_temperature()/16.0; // 读取温度并转换为浮点数

温度显示部分使用了sprintf来做格式化输出,这是个小技巧:

sprintf(seg_string,"C %5.2f",Temp); // 格式化为"℃ XX.XX"的格式

这里要注意浮点数运算会占用较多资源,在资源紧张的单片机上要谨慎使用。

4.2 DA电压输出模式设计

省赛题目要求实现两种DA输出模式:

  • 模式1:温度低于设定值时输出0V,高于时输出5V
  • 模式2:分段线性输出,20℃以下输出1V,20-40℃线性变化,40℃以上输出4V

实现代码如下:

void Voltage_Output_Proc(void) {
    if(DA_Output_Mode == 0) { // 模式1
        if(Temp < Temp_Compare_Ctrl) 
            DA_Inter_Num = 0;
        else 
            DA_Inter_Num = 255;
    } else { // 模式2
        if(Temp < 20)
            DA_Inter_Num = 51;  // 对应1V
        else if(Temp > 40)
            DA_Inter_Num = 204; // 对应4V
        else
            DA_Inter_Num = 7.65*Temp - 102; // 线性变换
    }
    Pcf8591_Dac(DA_Inter_Num); // 通过PCF8591输出
}

4.3 LED状态指示设计

LED用来指示当前的工作模式和界面状态,这个设计虽然简单但很实用:

void Led_Proc(void) {
    if(DA_Output_Mode == 0) // 模式1,L1点亮
        ucLed |= 0x01;
    else                    // 模式2,L1熄灭
        ucLed &= (~0x01);
    
    switch(Disp_Interface) { // 根据界面点亮不同LED
        case 0: ucLed = (ucLed & 0xF1) | 0x02; break; // L2亮
        case 1: ucLed = (ucLed & 0xF1) | 0x04; break; // L3亮
        case 2: ucLed = (ucLed & 0xF1) | 0x08; break; // L4亮
    }
}

5. 备赛经验与实战建议

准备蓝桥杯比赛,光看理论是远远不够的。我总结了几条血泪经验:

首先,一定要亲手调试每一个外设。记得我第一次用PCF8591做DA转换时,输出电压总是不对,后来发现是I2C的时序有问题。这些经验只有在实战中才能获得。

其次,要建立自己的代码库。把常用的驱动模块(数码管、按键、I2C等)都封装好,并做好详细注释。比赛时可以直接调用,省时省力。

第三,多练习往届真题。省赛和国赛的题目套路其实很相似,把近五年的真题都做一遍,基本上就能掌握出题规律了。

最后,比赛时的时间管理很重要。建议按照这个顺序来:

  1. 先搭建程序框架(定时器初始化、外设初始化等)
  2. 实现基础功能(温度采集、数码管显示等)
  3. 完成高级功能(参数设置、模式切换等)
  4. 最后处理细节(LED指示、异常处理等)
Logo

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

更多推荐