
简介面向蓝桥杯单片机方向备赛的CT107D开发板测试与练习程序适合竞赛选手及刚接触单片机的初学者。资源为zip压缩包共25个文件以c源码、h头文件为主并包含uvproj工程文件、hex烧录文件及编译中间文件整体约86KB可直接导入开发环境查看运行效果。内容覆盖GPIO控制、定时器、中断、串行通信等常见功能模块并涉及ds1302、iic、onewire等典型外设的驱动练习有助于熟悉CT107D平台的开发流程。已有380人学习下载适合用来配合蓝桥杯备赛通过上手调试逐步建立单片机编程与硬件调试的基本功。1. 把 CT107D 测试程序当成一幅外设地图“蓝桥杯CT107D开发板测试兼练习程序”经常被人当成一个纯模板工程但真正有价值的是它把 DS1302、AT24C02 与 DS18B20 三种串行外设揉进同一个工程让你在一块练手板上反复做位操作训练。代码本身并不复杂却关系到竞赛里最容易失分的地方时序是否经得起连续运行、中断服务函数是否阻塞主循环。新手可以拿它学“芯片手册和源码怎么对应”有经验的人则能拿它当驱动兼容性测试台比如新换了一块板子先烧一遍这个测试程序看各外设是否全部正常再开始写正式赛题。它适合每一位准备单片机赛项、又不确定底层驱动能不能稳定工作的备赛者。2. 从文件结构到构建流程Test_code 工程是谁在指挥谁解开 Test_code.zip 之后很多人先看 Main.c这是对的但一个 Keil 工程能跑起来靠的并不只是主函数。压缩包里还有 ds1302.c、iic.c、onewire.c 三个驱动源文件以及 STARTUP.A51、TestCode.uvproj、TestCode.hex 等工程产物。把这些文件按“作用”分类调试时才知道该去哪一层找问题。下面是我建议的关注顺序文件作用Main.c顶层状态机负责按键切换外设测试、数码管刷新ds1302.c / ds1302.h实时时钟驱动提供时间读写接口iic.c / iic.hI2C 主机驱动通常对应 AT24C02 的字节读写onewire.c / onewire.h单总线驱动用于 DS18B20 温度传感器STARTUP.A51Keil 启动代码做 XRAM 清零和堆栈初始化TestCode.uvprojKeil5 工程文件保存芯片型号、编译选项TestCode.hex编译生成的最终烧录文件TestCode.m51存储器分配映射可以查变量地址、栈大小TestCode.build_log.htm编译日志快速筛出 warning 和 error.uvopt和.uvgui这两个文件只保存窗口布局和调试器状态删掉也不影响编译建议丢进.gitignore。Objects 目录下的.obj是编译中间产物Listings 里的.lst是列表文件它们都属于可以随时再生的内容。真正要维护的是Main.c和三个驱动源文件。2.1 Keil 工程设置里最容易漏掉的开关打开 TestCode.uvproj 后第一件事不是看代码而是检查Options for Target里的三个位置。第一Device 是否选择了 STC15 系列或对应型号CT107D 常见型号是 IAP15F2K61S2 兼容 STC15F2K60S2如果选错寄存器定义和烧录地址都会出问题。第二Output页面必须勾选Create HEX File否则编译完成后只有 AXF 文件找不到可以直接烧写的 TestCode.hex。第三Debug页面如果要用 STC-Link 仿真选 STC Monitor-51 Driver如果只是下载运行选 Use Simulator 也不会影响 HEX 生成。Keil 的工程文件是 XML 文本所以可以用文本编辑器打开.uvproj检查芯片 ID。我一般会关注DeviceCpu标签里的型号和SFR宏避免在 GUI 里被无关设置干扰。一个常见误用是把Memory Model改成Large: variables in XDATA这会改变变量默认存储区导致测试程序在只有少量外部 RAM 的板子上莫名死机。只要驱动代码量不大保持 Small 模型最稳妥。2.2 用命令行编译替代反复点击鼠标备赛时每改一次驱动就要编译一次频繁点鼠标很浪费精力。Keil 自带的 UV4 支持命令行批处理在工程根目录打开 CMD执行C:\Keil_v5\UV4\UV4.exe -b TestCode.uvproj -j0 -o build.log-b表示批处理构建-j0关闭 GUI 直接等待结果-o build.log把编译信息写入日志文件。命令返回 0 表示编译成功非 0 则说明有错误。执行完打开 build.log搜索Error或Warning优先处理 ErrorWarning 可以先跳过但如果出现UNRESOLVED EXTERNAL SYMBOL这类警告说明某个驱动函数没有被正确链接程序一定跑不起来。编译通过后TestCode.hex 就是最终的烧录文件。CT107D 一般使用 STC-ISP 软件下载选择对应型号打开 HEX波特率可以设 115200点击下载后给目标板重新上电。如果下载失败优先看串口是否被占用、P3.0/P3.1 是否被外部设备拉住这是新手在竞赛现场最常遇到的“假故障”。3. DS1302、I2C 与单总线驱动寄存器、时序和那些容易错的地方CT107D 上这三个外设都用串行方式通信但性格完全不同。DS1302 是类 SPI需要 CE 拉高后按位收发光I2C 是两线开漏靠应答位判断从机状态DS18B20 的单总线对时隙宽度要求最严格差个几十微秒就读不出正确温度。蓝桥杯单片机国赛的客观题部分很喜欢考 DS1302 寄存器地址和 AT24C02 的设备地址主观题则爱让你在液晶屏上显示时间加温度所以这三个驱动必须做到“随手能写”的程度。3.1 DS1302读时间时最容易被忽略的 BCD 转换DS1302 的时间寄存器一秒一分都存的是 BCD 码不是十六进制这是第一个坑。比如秒寄存器读到0x59实际秒数是十进制 59如果直接把十六进制值发到数码管会显示成小数点的乱码。驱动里一般用一个字节读写函数// 向 DS1302 写一个字节低位在前 void ds1302_write_byte(unsigned char dat) { unsigned char i; for (i 0; i 8; i) { DS1302_SCLK 0; // 拉低时钟准备改变数据线 if (dat 0x01) DS1302_IO 1; else DS1302_IO 0; DS1302_SCLK 1; // 上升沿让 DS1302 锁存数据 dat 1; } }写字节时先拉低 SCLK 再改变 IO是为了避免数据在时钟高电平期间跳变。上升沿锁存数据所以DS1302_SCLK 1必须放在 IO 设置之后。读字节的流程类似但要注意在读数据前需要先把 IO 引脚置 1释放总线控制权// 从 DS1302 读一个字节命令地址的 bit0 置 1 unsigned char ds1302_read_byte(unsigned char addr) { unsigned char i, dat 0; DS1302_IO 1; // 释放 IO准备接收数据 DS1302_CE 1; // 拉高片选允许通信 ds1302_write_byte(addr | 0x01); // 最低位 1 表示读操作 for (i 0; i 8; i) { dat 1; DS1302_SCLK 0; // 下降沿后数据稳定 _nop_(); if (DS1302_IO) dat | 0x80; DS1302_SCLK 1; // 上升沿准备读下一位 _nop_(); } DS1302_CE 0; return dat; }注意dat 1在每次读取前执行相当于把已经接收到的位向右移动然后根据 IO 状态决定最高位是 0 还是 1这就完成了低位先收的拼装顺序。读出来的秒、分、时都是 BCD 码用(bcd 4) * 10 (bcd 0x0F)转成十进制。写入时间时则反过来用(dec / 10) 4 | (dec % 10)转换。DS1302 还有个写保护寄存器地址0x8E写入0x00才能修改时间写完时间以后建议写回0x80防止误操作把时间改乱。各寄存器地址经常考附一个速查寄存器读地址写地址注意秒0x810x80bit7 为 1 时振荡停止分0x830x82无特殊控制位时0x850x84bit7 为 1 时 12 小时制日0x870x86范围 1~31写保护0x8F0x8Ebit7 为 1 时禁止写入3.2 I2C 驱动的 ACK 等待与重复起始位I2C 驱动通常服务于 AT24C02 这类 EEPROM时序上比 DS1302 更讲究。起始条件定义为 SCL 为高电平时SDA 产生一个下降沿。标准写法是先把 SDA 和 SCL 都拉高再拉低 SDA最后拉低 SCL 准备后续传输。以下是常见的实现void IIC_Start(void) { SDA 1; SCL 1; _nop_(); _nop_(); SDA 0; // SCL 高电平期间 SDA 拉低形成起始条件 _nop_(); _nop_(); SCL 0; // 拉低 SCL释放总线准备传数据 }起始后必须让 SCL 保持低电平然后逐位发送数据。发送完第 8 位后要释放 SDA等待从机应答。我在写等待 ACK 的函数里一定会加超时计数否则从机没上电或地址写错时程序会卡死在while(SDA)里bit IIC_WaitAck(void) { unsigned char t 0; SDA 1; // 释放 SDA让从机可以拉低应答 _nop_(); _nop_(); SCL 1; while (SDA) { if (t 200) { // 超时约 200 个循环周期 SCL 0; return 1; // 返回 1 表示无应答 } } SCL 0; return 0; // SDA 被拉低从机应答成功 }超时循环的计数和时间有关。如果晶振是 12MHz、STC15 工作在 12T 模式_nop_()大约 1 微秒200 次循环约 200 微秒。如果切换到 6T 模式整个时序会快一倍这时要调整循环上限否则应答信号还没出现就误判为超时。CT107D 的板载 I2C 一般连接 AT24C02设备地址是0xA0写一个字节后需要等待约 5~10 毫秒的内部写周期连续读操作时要发重复起始位否则第二次读出来的是上次地址的缓冲数据。3.3 单总线复位与读时隙最怕中断打扰DS18B20 的单总线比 I2C 更容易受干扰尤其是“复位脉冲”。主机必须先拉低总线至少 480 微秒然后释放DS18B20 会等待一段时间把总线拉低 60~240 微秒作为存在应答。经典代码如下bit ow_reset(void) { bit presence 1; OW_DQ 0; // 主机拉低总线 Delay480us(); // 至少 480us 低电平复位脉冲 OW_DQ 1; // 释放总线等待 DS18B20 应答 Delay60us(); presence OW_DQ; // 0 表示存在应答 Delay240us(); // 完成整个复位周期 return presence 0; }注意Delay480us()不能简单靠若干_nop_()堆叠因为优化器可能把空指令循环删掉。常见做法是用定时器或者嵌入式汇编写死延时函数或者用一个 volatile 局部变量做循环计数。读一个数据位时主机要先拉低总线 1 微秒以上然后释放再在 15 微秒内采样总线电平。我一般这么写bit ow_read_bit(void) { bit dat 0; OW_DQ 0; // 拉低启动读时隙 _nop_(); _nop_(); OW_DQ 1; // 释放总线 _nop_(); _nop_(); // 等待约 5~10us dat OW_DQ; // 采样总线 Delay100us(); // 等待时隙结束 return dat; }单总线读时隙的采样窗口非常窄因此这类操作期间最好关闭总中断或者保证定时器中断服务函数足够短。竞赛现场最常见的现象是温度显示 85℃ 或 -55℃多半不是传感器坏了而是复位时序被中断拉长或者读位时采样点太靠后。如果发现这几种结果先用逻辑分析仪抓复位脉冲宽度再回头检查中断优先级。4. Main.c 如何用状态机与定时器管理外设测试竞赛代码的框架思路这个测试工程不是把三个驱动堆在一起就完事Main.c 里需要解决一个典型问题DS1302、AT24C02、DS18B20 都是耗时型串行操作而数码管刷新、按键扫描又需要周期性执行。如果主循环里单纯顺序调用三个测试函数会出现按键按下去没反应、数码管亮度不均的情况所以要把任务分层。4.1 初始化顺序先关总中断再配 IO 和定时器无论是什么板子我习惯在初始化函数开头执行EA 0等所有端口、定时器、串口都配置完以后再统一打开总中断。这样避免配置到一半时定时器溢出触发中断导致外设引脚状态不确定。CT107D 上的 STC15 可以通过P0M0、P1M0这类寄存器控制准双向口和推挽模式但测试程序没必要全部设置成推挽准双向模式已经能满足大部分用途。void System_Init(void) { EA 0; // 先关闭总中断 P0M0 0x00; P0M1 0x00; // P0 准双向口 P2M0 0x00; P2M1 0x00; // P2 准双向口 Timer0_Init(); // 配置定时器 0 Uart_Init(); // 配置串口 1调试用 EA 1; }P0M0 0x00, P0M1 0x00的意思是两组寄存器对应的位都设为 0端口处于准双向模式。STC15 的 IO 模式配置和普通 8051 不一样如果某一位需要高电平驱动能力强可以改成P0M1 0x00; P0M0 0xFF但外设测试程序里不做特殊说明时保持准双向最稳。初始化完后先别急着进主循环可以调用一个自检函数把所有外设访问一遍起点是判断 DS18B20 是否存在。4.2 主循环状态机把按键变成测试项目的切换开关主循环里我一般用一个run_state变量表示当前测试哪个外设。按键扫描放在定时器中断里只负责设置标志和读取键值主循环发现标志被置位后再调用延时消抖和状态切换。一个最简单的例子如下void main(void) { System_Init(); while (1) { if (key_scan_flag) { key_scan_flag 0; run_state GetKeyValue(); // 读取当前按键 } switch (run_state) { case 1: LoopShowTime(); break; // DS1302 显示时间 case 2: LoopEepromTest(); break; // AT24C02 读写测试 case 3: LoopDs18b20(); break; // DS18B20 温度采集 default: run_state 0; break; } } }这段代码的关键在于key_scan_flag由定时器中断置位主循环只做判断和调用。按键消抖放在中断外的GetKeyValue()里可以避免中断服务函数被延时占用。定时器 0 中断的重载值要按晶振算假设 12MHz、12T 模式10ms 定时对应的计数器初值是65536 - 10000代码可以写成void Timer0_ISR(void) interrupt 1 { TH0 (65536 - 10000) / 256; TL0 (65536 - 10000) % 256; key_scan_flag 1; }如果你的晶振是 11.0592MHz同样 10ms 的中断计数是 9216而不是 10000。这个区别是客观题喜欢出的小坑计算定时器初值时必须把晶振频率和定时器模式一起代入。定时器中断标志位由硬件自动清零这里不需要手动写TF0 0但许多人会为了保险再清一次反而画蛇添足。4.3 中断里的任务分配哪些能留哪些必须移走DS18B20 和 I2C 都是微秒级时序定时器中断如果耗时超过 50 微秒就会破坏读时隙。下表是我在 CT107D 上做任务分配时的经验任务放置位置原因按键采样定时器中断10ms 固定节拍消抖效果好数码管位选/段选刷新主循环或定时器中断刷新频率 50Hz 以上即可DS1302 读写主循环允许被低频中断打断I2C 字节读写主循环等待 ACK 时有超时保护DS18B20 读写主循环并短暂关中断时隙敏感不可被打断把耗时操作全部移到主循环之后按键扫描和数码管刷新就不会被 I2C 的while(SDA)等待卡住。如果你发现按一次按键要几秒才响应第一步不是加长消抖时间而是检查主循环里是否有阻塞函数尤其是没有加超时的 ACK 等待。测试程序的意义就是把这些阻塞源暴露出来比赛时你才知道哪些函数必须重构。5. 用逻辑分析仪校准外设时序把练习程序变成驱动稳定性测试台测试程序能跑通不等于时序达标。我见过不少代码能点亮数码管但 DS18B20 测出的温度跳来跳去原因就是复位低电平只有 300 微秒读时隙采样点偏晚。你手头不一定有示波器但几十元的逻辑分析仪就够了采样率调到 8MHz 以上通道分别接 DS1302 的 SCLK、I2C 的 SDA、单总线的 DQ。重点看三个位置复位脉冲是否大于 480 微秒DS18B20 应答脉冲是否在主机释放总线后 60 微秒内出现I2C 起始位下降沿时SCL 是否保持高电平。验证 DS18B20 稳定性时可以连续读取 10 次温度记录最大值和最小值。温度变化不超过 1℃ 说明时序基本正确如果跳动超过 2℃大概率是读时隙采样点太靠后。代码里可以加一个计数变量把每次超时都记下来unsigned int i2c_timeout_cnt 0; bit IIC_WaitAck(void) { unsigned char t 0; SDA 1; _nop_(); SCL 1; while (SDA) { if (t 200) { i2c_timeout_cnt; // 超时次数累计 SCL 0; return 1; } } SCL 0; return 0; }把i2c_timeout_cnt显示在数码管或通过串口输出就能观察总线健康度。正常情况下这个值应该长时间不变如果持续递增说明 SDA 上拉太弱或从机地址不对。最后一招把所有驱动函数的时序延时改成宏定义用条件编译区分 12MHz 和 11.0592MHz 晶振换板子时只改一个头文件不需要动驱动源码#if defined(CRYSTAL_12M) #define DS1302_PERIOD_US 12 #define OW_RESET_US 500 #elif defined(CRYSTAL_110592M) #define DS1302_PERIOD_US 11 #define OW_RESET_US 550 #endif这样测试程序就不只是一个练习包而是一套可以重复使用的驱动回归工具。每次做完比赛题回头跑一遍自检看超时计数是否增加就知道改代码时有没有把底层时序弄坏。本文还有配套的精品资源点击获取