尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

单片机外设驱动与硬件调试:CT107D测试兼练习程序的设计与实现

单片机外设驱动与硬件调试:CT107D测试兼练习程序的设计与实现 简介蓝桥杯CT107D开发板测试兼练习程序是一份面向蓝桥杯单片机竞赛新手的基础练习资料聚焦GPIO、定时器、中断、串行通信等核心外设操作帮助学习者从零上手CT107D平台并熟悉竞赛开发流程。资源共25个文件压缩包86KB以C源码Main.c、ds1302.c、iic.c、onewire.c为主附带对应头文件、编译生成的obj/lst/hex文件及Keil工程文件uvproj/uvopt便于直接打开工程查看、编译和烧录验证。代码覆盖DS1302时钟、IIC总线、单总线等常用模块并配有驱动代码说明文本适合按实验由浅入深逐项练习。当前已有380人学习下载对准备蓝桥杯或刚接触51单片机开发的初学者而言是兼具参考价值与实操性的入门资料既能熟悉工程结构也能通过示例代码理解外设驱动与调试思路。1. 为什么 CT107D 需要一套“测试兼练习程序”备赛蓝桥杯单片机组、嵌入式组的同学拿到 CT107D 开发板的第一感觉往往是板子上的资源比教材里的最小系统多太多了。LED、数码管、矩阵键盘、1602 液晶、DS18B20、DS1302、EEPROM、ADC 全挤在一张板上上电后没有任何一个模块有动静心里根本没底。更麻烦的是历年国赛真题几乎没有单外设题目全是“键盘输入 数码管显示 温度上传 串口通信”这种组合拳。这时候如果手上没有一套能把每个模块逐一验证的测试程序后面写练习题的排错成本会高得离谱。一套合格的 CT107D 测试兼练习程序应该同时承担两个角色一是硬件体检工具上电后通过按键切换测试项把每个外设的预期行为逐条过一遍确定板子哪部分工作正常、哪部分有问题二是工程模板定时器、锁存器、延时函数、扫描机制全部提前配好之后练真题直接在这个骨架上加业务逻辑。下面就把这套程序从设计到落地完整拆开讲。2. 先把 CT107D 外设盘清楚测试目标和菜单策略2.1 板载外设清单与测试项的对应关系写测试程序之前先把板子上的外设和它们的控制方式列清楚。CT107D 的绝大多数外设都挂在 P0 总线上通过 P2 口输出的锁存器编码来决定当前 P0 的数据送给哪组外设。这个“P0 分时复用”是整个测试程序的核心认知。外设模块板卡控制信号测试动作预期结果8 位 LEDP0 数据 Y4C 锁存8 路流水灯L1~L8 依次点亮无暗灯、无拖影8 位数码管P0 Y5C 段选 / Y6C 位选全显“8.”再逐位显示 1~8无缺笔画亮度均匀无闪烁独立按键P3.0~P3.3按键按下数码管回显键号键号与物理位置一一对应矩阵键盘P3.4~P3.7 与 P4.4~P4.7行列反转扫描并回显键值16 个键号无串键、无漏键蜂鸣器/继电器P0 输出 Y7C 锁存上电短鸣 100ms继电器吸合一次有声响继电器“哒”一声1602 液晶P0 数据 RS/RW/EN 控制写入固定测试字符串无乱码、无黑影、无显示延迟DS18B20P1.4DQ读取温度并显示到 0.1℃读数与室温相差不超过 ±1℃这个表格就是测试程序的验收标准。每一个测试项跑完对照预期结果就能判断板卡对应模块是否健康。LED 有一路不亮要么是 573 锁存器对应输出坏了要么是 P0 到 LED 之间的排阻虚焊数码管某一位缺笔画通常是段选信号的问题矩阵键盘串键多半是行线或列线接反这些判断都依赖上表。2.2 测试菜单设计按键切换还是自动轮询测试项这么多第一版很容易写成“上电后按顺序把所有外设自动跑一遍”。这个方案看起来省事实际调试时会非常难受某个外设卡住了程序停在哪一步很难判断DS18B20 这类器件上电后需要时间稳定自动轮询经常读到 85℃ 的初始值然后误报故障。所以我一般把测试程序做成按键驱动的菜单式结构每个外设一个测试入口用手动方式控制切入和退出。菜单的状态集合用枚举定义这是测试程序的骨架typedef enum { TEST_LED, // LED 流水灯自检 TEST_SEG, // 数码管全显自检 TEST_KEY, // 独立按键回显自检 TEST_MATRIX, // 矩阵键盘回显自检 TEST_LCD, // 1602 液晶回显自检 TEST_TEMP, // DS18B20 温度自检 TEST_MAX } TestItem; TestItem current_item TEST_LED;主循环里只做一件事根据current_item分发到对应的测试函数。每个测试函数内部都是一个独立的小循环按到退出键时跳出回到主菜单状态。这样做的好处是某个外设异常时可以停留在该测试项反复操作配合示波器或万用表定位故障而不是被自动轮询的逻辑带走。3. 搭建 CT107D 测试程序的工程基座时钟、锁存器与毫秒时基3.1 STC15 的时钟1T 模式与 IRC 主频CT107D 使用的 MCU 是 STC15F2K60S21T 增强型 8051 内核大多数情况下用内部 IRC 时钟。需要先明确一个事实STC15 的 1T 模式不是简单地把标准 8051 的 12 分频去掉而是整个指令周期和定时器时钟都跟着主频走。定时器初值计算、串口波特率、延时函数全部依赖主频因此时钟配置必须放在工程最前面。void SystemClock_Init(void) { // CLK_DIV 的 bit0~bit2 控制主时钟分频 // 0x00 表示不分频主频 IRC 设置值 CLK_DIV 0x00; }这里有一个关键约束CLK_DIV 0x00只是软件层面的分频配置真正的频率基准在下载程序时由 STC-ISP 设置。用 STC-ISP 下载程序时IRC 频率要选 12.000MHz和代码里定时器初值的计算保持一致。如果下载时误选了 11.0592MHz代码里所有按 12MHz 计算的定时初值都会产生约 8.5% 的偏差数码管扫描还能凑合看串口波特率就直接乱码了。3.2 锁存器操作P0 口的四组“门”CT107D 上 P0 口是一根数据总线LED、数码管段选、数码管位选、蜂鸣器/继电器四组外设都挂在上面。同一时刻 P0 只能给一组外设送数据由 P2 口选择当前要打开哪组锁存器。这就是所谓“开锁存器→写 P0→关锁存器”三段式操作。// Y4C/Y5C/Y6C/Y7C 是 74HC138 译码器的使能编码 // 由开发板原理图决定不要随意改动 #define Y4C 0x80 // LED 锁存器 #define Y5C 0xA0 // 数码管段选锁存器 #define Y6C 0xC0 // 数码管位选锁存器 #define Y7C 0xE0 // 蜂鸣器/继电器锁存器 // 向 LED 锁存器写入一个字节 void Led_Write(unsigned char dat) { P2 (P2 0x1F) | Y4C; // 打开 LED 锁存器 P0 dat; // P0 总线输出数据 P2 (P2 0x1F); // 关闭锁存器数据保持 }P2 0x1F的作用是先保留 P2 低 5 位的状态只修改高 3 位来控制 138 译码器。| Y4C就是把高 3 位设置为使能 LED 锁存器的编码。关锁存器的动作非常重要如果不关后续任何一次 P0 写入都会实时反映到 LED导致模块间的数据互相污染。这个“开→写→关”模型是 CT107D 上所有外设驱动的基础。控制信号P2 操作值控制的锁存器对应外设Y4C0x80LED 锁存器L1~L8Y5C0xA0段选锁存器数码管 a~dp 段Y6C0xC0位选锁存器数码管第 1~8 位Y7C0xE0功能锁存器蜂鸣器、继电器3.3 定时器 0测试程序的毫秒时基测试程序里的按键消抖、数码管扫描、LED 闪烁全部需要时间基准。软件延时虽然简单但会阻塞主循环测试菜单的实时响应会变差。我用定时器 0 做 1ms 时基在中断里累加标志位主循环按标志位决定执行哪些操作。void Timer0_Init(void) { AUXR | 0x80; // 定时器0 工作于 1T 模式时钟不分频 TMOD 0xF0; // 清零定时器0 的模式位 TMOD | 0x01; // 定时器0 方式116位不自动重装 TL0 0x20; TH0 0xD1; // 12MHz 1T 模式下定时 1ms TR0 1; // 启动定时器0 ET0 1; // 开启定时器0 中断 EA 1; // 总中断使能 }定时初值0xD120是这样来的12MHz 主频、1T 模式定时器一个计数脉冲就是 1/12 微秒1ms 需要 12000 个脉冲。16 位定时器从 65536 向上计数到溢出所以初值 65536 - 12000 53536 0xD120写入时高字节0xD1进 TH0低字节0x20进 TL0。如果以后把主频改成 24MHz 或采用 12T 模式这个初值必须重算这是调试时最容易被忽略的隐性误差源。unsigned int sys_tick; // 通用毫秒计数 unsigned int key_scan_tick; // 按键扫描节拍 unsigned char seg_scan_pos; // 数码管当前扫描位 void Timer0_Isr(void) interrupt 1 { sys_tick; key_scan_tick; seg_scan_pos (seg_scan_pos 1) 0x07; // 0~7 循环 }三个变量分别服务不同模块sys_tick给 LED 流水灯和测试项超时用key_scan_tick驱动按键定时扫描seg_scan_pos决定数码管动态扫描当前刷到第几位。全部在中断里累加主循环只在需要的时刻读取和清零避免在中断里做复杂逻辑。3.4 时基的调度参数时基建立之后需要把各模块的时间参数统一管理起来。以下是一组在 CT107D 上实测过比较稳的参数时间参数建议值说明按键消抖20ms按下后延迟 20ms 再读电平消除机械抖动数码管扫描周期1ms/位8 位扫完一轮 8ms人眼看不到闪烁长按返回500ms测试状态下按 return 键 500ms 返回主菜单DS18B20 采样间隔500ms太快会频繁读到 85℃ 的上电默认值LED 流水灯间隔200ms视觉节奏清晰适合人工观察这些参数不要散落在各个函数里建议用宏或常量统一声明。这样测试程序后面转型为练习程序时只需要改这些宏就能调整整个工程的节奏。4. 逐个点亮LED、数码管、按键与 1602 的测试实现4.1 LED 自检把 8 个灯轮流点亮LED 测试的逻辑最简单一路一路点亮人眼确认每一路都能亮、没有暗灯。CT107D 的 LED 是低电平点亮所以写入的数据要做取反处理。void Test_LED(void) { unsigned char i; for (i 0; i 8; i) { Led_Write(0xFF); // 全灭避免上一轮残留 Led_Write(~(0x01 i)); // 第 i 路点亮其余熄灭 Delay_Ms(200); // 200ms 视觉间隔 } Led_Write(0xFF); // 测试结束全部熄灭 }~(0x01 i)就是把二进制位第 i 位置 0其余位为 1正好对应低电平点亮。200ms 的延时来自第 3 章的sys_tick提供的毫秒基础不要用空循环写死否则碰到 1T 模式的高主频延时时间很难预估。测试中如果发现某一路始终不亮先看 P0 到 LED 之间的排阻再检查 Y4C 锁存器对应的 573 芯片输出这两处是故障高发区。4.2 数码管全显段码表与动态扫描数码管测试要验证两件事每一位都能被选中每一段都能正常点亮。因此测试策略是“逐位全显 8.”这样任何一段断路或缺笔画都会立刻暴露。// 共阴数码管段码索引 0~9 对应数字10~15 对应 A~F unsigned char code seg_code[16] { 0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F, 0x77, 0x7C, 0x39, 0x5E, 0x79, 0x71 }; unsigned char seg_buf[8]; // 显存缓冲区主循环只写这里 // 定时器中断里每 1ms 调用一次做动态扫描 void Seg_DisplayScan(void) { static unsigned char pos 0; // 第一步关段选消隐 P2 (P2 0x1F) | Y5C; P0 0x00; P2 (P2 0x1F); // 第二步打开位选选中当前显示位 P2 (P2 0x1F) | Y6C; P0 0x01 pos; P2 (P2 0x1F); // 第三步打开段选写入当前位的段码 P2 (P2 0x1F) | Y5C; P0 seg_code[seg_buf[pos]]; P2 (P2 0x1F); pos (pos 1) 0x07; // 移到下一位循环 }三步操作中的顺序不能颠倒先消隐是为了防止换位瞬间出现拖影位选先行是为了在新位选通时段码已经是稳定的。seg_buf是主循环与扫描函数之间的缓冲区主循环只修改这个数组扫描函数在中断里读它双方不直接抢 P0 总线避免显示闪烁。数码管测试时把seg_buf前四位填10对应“8.”后四位依次填1~8人眼观察就能判断所有位和段的状态。4.3 矩阵键盘与独立按键的扫描实现按键测试的核心是给每个按键一个确定的键值按下时在数码管上回显。独立按键接口在 P3.0~P3.3扫描时注意和矩阵键盘的区分。// 独立按键扫描返回 0~3无键按下返回 0xFF unsigned char Key_Scan(void) { if (key_scan_tick 20) // 20ms 节拍未到 return 0xFF; key_scan_tick 0; P3 | 0x0F; // P3 低四位拉高准备读取 if ((P3 0x0F) 0x0F) return 0xFF; // 全为高无按键 if ((P3 P35) 0) return 0; // 具体引脚以原理图为准 if ((P3 P34) 0) return 1; if ((P3 P33) 0) return 2; if ((P3 P32) 0) return 3; return 0xFF; }key_scan_tick由定时器中断累加主循环读到时大于等于 20 就执行一次扫描然后把计数清零。注意拿到按键状态后不要立刻返回通常还需要等待松手否则按键会连续触发多次。实战中我习惯把“检测按下”和“等待释放”分成两个状态放进一个简单的按键状态机里避免用while死等占用主循环。矩阵键盘的扫描思路是行列反转法先检测是否有键按下再切换行列的输入输出方向两次读取确定键值。unsigned char Matrix_Scan(void) { unsigned char row 0, col 0; unsigned char temp; // 第一步行线和列线全部拉高 P3 | 0xF0; P4 | 0xF0; temp (P3 0xF0) | (P4 0xF0); if (temp 0xF0) return 0xFF; // 无键按下 Delay_Ms(10); // 二次消抖 temp (P3 0xF0) | (P4 0xF0); if (temp 0xF0) return 0xFF; // 第二步行线输出低电平读取列线 P3 0x0F; temp P4 0xF0; switch (temp) { case 0xE0: col 0; break; case 0xD0: col 1; break; case 0xB0: col 2; break; case 0x70: col 3; break; default: return 0xFF; } // 第三步列线输出低电平读取行线 P4 0x0F; temp P3 0xF0; switch (temp) { case 0xE0: row 0; break; case 0xD0: row 1; break; case 0xB0: row 2; break; case 0x70: row 3; break; default: return 0xFF; } return row * 4 col; }行列反转法的核心在于两个 switch 分支第一轮靠行线输出低电平从列线读回低电平位置得到列号第二轮反过来。键值row * 4 col是矩阵键盘最通用的编码方式只要行列扫描顺序固定键号就不会乱。CT107D 的矩阵键盘行线接 P3.4~P3.7、列线接 P4.4~P4.7行号和列号要与原理图丝印逐一核对。4.4 1602 液晶回显测试菜单1602 是测试程序里的人机交互窗口菜单提示、测试结果都要靠它显示。CT107D 上 1602 的 RS、RW、EN 分别接在 P1.0、P1.1、P1.2数据引脚复用 P0 总线。初始化时序比较固定核心操作是写入一个字节。sbit LCD_RS P1^0; // 寄存器选择0命令1数据 sbit LCD_RW P1^1; // 读写选择0写 sbit LCD_EN P1^2; // 使能脉冲 // 写入一个字节到 1602 // isData 1 时写入数据isData 0 时写入命令 void Lcd_WriteByte(unsigned char isData, unsigned char dat) { LCD_RS isData; LCD_RW 0; P0 dat; LCD_EN 1; Delay_Us(10); // 使能高电平保持 LCD_EN 0; }写入字节的过程就是“设置 RS→设置数据→拉高 EN→延时→拉低 EN”五个步骤。Delay_Us(10)保证 1602 内部的时序满足建立时间和保持时间要求。初始化时依次发 0x388 位模式、0x0C显示开、不闪烁、0x06地址自动加一、0x01清屏四条命令然后就可以用Lcd_WriteByte(1, A)循环输出字符。测试菜单在这里显示当前测试项名称切到哪个测试项LCD 第一行就显示哪个名字一目了然。4.5 主循环状态机的整合写法所有测试函数就位后主循环用一个简单的状态机把它们串起来。每个测试函数内部要有退出条件例如长按某个按键 500ms 返回菜单。void main(void) { SystemClock_Init(); Timer0_Init(); Lcd_Init(); Lcd_ShowString(0, 0, CT107D SelfTest); Lcd_ShowString(0, 1, Press Key1 Start); while (1) { switch (current_item) { case TEST_LED: Test_LED(); current_item TEST_SEG; // 自动进入下一项 break; case TEST_SEG: Test_Seg(); current_item TEST_KEY; break; case TEST_KEY: Test_Key(); current_item TEST_MATRIX; break; case TEST_MATRIX: Test_Matrix(); current_item TEST_LCD; break; case TEST_LCD: Test_Lcd(); current_item TEST_TEMP; break; case TEST_TEMP: Test_Temperature(); current_item TEST_LED; // 轮完一轮回到起点 break; } Lcd_ShowMenu(current_item); // 刷新菜单提示 Delay_Ms(100); // 等待人工观察 } }这种写法最直观每个测试函数返回后current_item指向下一项测试程序就会自动循环遍历所有外设。如果想停在某个测试项反复看只需要把对应的 case 里不更新current_item即可。主循环末尾的 100ms 延时是给人工观察和 LCD 刷新留出时间不要省掉。整个状态机没有复杂的转移表后续往里面加串口测试、ADC 测试时只需要扩展枚举和 switch 分支。5. 测试程序收尾自检判定与三个高频翻车点5.1 人工回显与自动比对怎么选到目前为止的测试项全部依赖人眼观察结果适合开发板刚到手的首轮排障。但如果测试程序要频繁用于赛前检查可以考虑把判定逻辑自动化。思路是这样每个测试函数不再只做“点亮/显示”而是把结果记录到结构体里测试结束后通过串口输出一份测试报告。typedef struct { unsigned char led_ok; // 1通过 unsigned char seg_ok; // 1通过 unsigned char key_ok; // 1通过 unsigned char matrix_ok; // 1通过 unsigned char temp_ok; // 1通过 } TestReport; TestReport report; // 示例LED 测试的自动判定 void Test_LED_Auto(void) { unsigned char i; report.led_ok 1; for (i 0; i 8; i) { Led_Write(~(0x01 i)); Delay_Ms(100); // 此处用光敏检测或回读引脚状态判断是否点亮 // 如果检测失败report.led_ok 0 } Led_Write(0xFF); }自动判定的前提是板子上有回读通路例如 LED 引脚接回 GPIO 输入或者外接光敏模块。CT107D 本身没有这个回读硬件所以实际使用中更常见的是“半自动”程序做固定的激励动作人在 LCD 上确认结果并按下“通过/不通过”按钮结果写进TestReport最后一起上报。这个方案比纯人眼记录严谨又不需要额外硬件。5.2 三个实际测试里最容易翻车的细节第一个坑是锁存器“开了没关”。CT107D 的 P0 总线被四组锁存器共享写完一次数据后如果急着操作其他模块之前打开的锁存器可能还没关P0 上的数据就会串到别的外设上。比如数码管扫描函数里位选刚写完、锁存器没关紧接着一个 LED 写入操作整个显示就乱了。解决办法是严格按照“打开锁存器→写 P0→关闭锁存器”三步走并且建议封装成类似Led_Write这样的函数不允许裸操作 P0。第二个坑是 STC-ISP 下载频率与代码主频不一致。定时器初值按 12MHz 计算下载时 IRC 频率选了 11.0592MHz会导致所有时间相关功能整体偏差。这个问题的隐蔽之处在于程序能跑、灯能亮只是节奏变慢不拿示波器根本发现不了。每次下载前检查频率设置把 12.000MHz 作为标准填进去。第三个坑是把 STC15 内部 EEPROM 和板载 AT24C02 搞混。CT107D 上两个存储器件都存在STC15 内部 EEPROM 需要通过 IAP 寄存器访问AT24C02 是 I2C 接口外设两者地址空间和操作方式完全不同。测试程序如果需要做掉电保存功能先确认题目要求的是哪一种写错访问接口轻则数据存不进去重则 I2C 总线被卡死。测试项里建议把两者分开列分别验证读写。我在写这套测试程序时养成了一个习惯先把定时器里的标志位全部独立命名key_scan_tick、seg_scan_pos、relay_time_cnt各管各的而不是所有模块共用一个tick。测试阶段看不出差别到了练真题往状态机里加功能时独立的节拍变量能省掉大量排查时间。这个命名习惯比任何一条具体驱动代码都值得先固化下来。本文还有配套的精品资源点击获取
返回列表