1.前言
你是否因为大一迷惘而焦虑??
这个实验教程献给所有大一想努力的新手
2.欣赏成果
成果
3.安装对应软件
首先先安装对应软件Arduino
网址arduino.cc/en/software
4.学习软件的使用
安装结束,我们进入首页选择我们对应的开发板Arduino UNO

选择之后就会将UNO开发板作为默认**(UNO开发板适合初学者简单易上手)**

并将开发板连接到电脑**(注意:后面一定带有后缀,我前面买的盗版无法识别就没有后缀)**
如果没有出现开发板(com)请检查链接器是否连接好或者开发板是否接触不良
准备道具
– Arduino Uno控制器
– 1个四位共阴数码管(注意分清阴阳)
– 8个220 欧姆电阻
– 1个按键
– 1个面包版
–条线若
如何区别共阴和共阳极数码管
共阳极:标注 Common Anode(简称 CA);
共阴极:标注 Common Cathode(简称 CC);
共阳数码管
是指将所有发光二极管的阳极接到一起形成公共阳极 (COM) 的数码管,共阳数码管在应用时应将公共极 COM 接到 + 5V,当某一字段发光二极管的阴极为低电平时,相应字段就点亮,当某一字段的阴极为高电平时,相应字段就不亮。
共阴数码管
是指将所有发光二极管的阴极接到一起形成公共阴极 COM) 的数码管,共阴数码管在应用时应将公共极 COM 接到地线 GND 上,当某一字段发光二极管的阳极为高电平时,相应字段就点亮,当某一字段的阳极为低电平时,相应字段就不亮
我们本次实验使用共阳极管


注意加上按钮
6.代码
这个代码存在bug(但是也是跑)
/*
* 项目名称:Arduino四位数码管计时系统 (初代共阴极版)
* 硬件配置:
* - 段选 A-H: D2-D9
* - 位选 COM1-4: D10-D13
* - 按键: D1
* 状态:存在阻塞Bug
* 说明:按住按键不放时,数码管会熄灭
*/
// 按键定义
#define KEY_PIN 1
// 数码管段选引脚 (a, b, c, d, e, f, g, dp) -> (2, 3, 4, 5, 6, 7, 8, 9)
int ledPins[] = {2, 3, 4, 5, 6, 7, 8, 9};
// 数码管位选引脚 (千, 百, 十, 个) -> (COM1, COM2, COM3, COM4) -> (10, 11, 12, 13)
int segPins[] = {10, 11, 12, 13};
// 共阴极段码表 (0-9)
// 1(HIGH) 亮
const unsigned char DuanMa[10] = {
0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07, 0x7f, 0x6f
};
unsigned char displayTemp[4];
bool countEnable = false;
int currentNumber = 0;
// 按键状态记录
bool lastKeyState = LOW;
bool currentKeyState = LOW;
void setup() {
// 初始化段选
for (int i = 0; i < 8; i++) pinMode(ledPins[i], OUTPUT);
// 初始化位选
for (int i = 0; i < 4; i++) {
pinMode(segPins[i], OUTPUT);
digitalWrite(segPins[i], HIGH); // 共阴初始关闭
}
pinMode(KEY_PIN, INPUT);
updateDisplayBuffer(0);
}
// ❌ 存在问题的按键处理函数
void handleKeyPress() {
currentKeyState = digitalRead(KEY_PIN);
// 检测按下瞬间
if (currentKeyState == HIGH && lastKeyState == LOW) {
delay(20); // 阻塞1:延时消抖
// 再次确认
if (digitalRead(KEY_PIN) == HIGH) {
countEnable = !countEnable;
// ☠️ 阻塞2:死循环等待按键松开
// 只要你不松手,程序就永远卡在这里,回不到 loop()
while(digitalRead(KEY_PIN) == HIGH);
}
}
lastKeyState = currentKeyState;
}
void loop() {
// 1. 处理按键
handleKeyPress();
// 2. 计时逻辑
static unsigned long lastTimerTime = 0;
if(countEnable && millis() - lastTimerTime >= 1000) {
lastTimerTime = millis();
currentNumber = (currentNumber + 1) % 10000;
updateDisplayBuffer(currentNumber);
}
// 3. 动态扫描
// 如果 handleKeyPress 卡住了,这里就不会执行 -> 数码管熄灭
refreshDisplay();
}
void updateDisplayBuffer(int num) {
displayTemp[0] = DuanMa[num / 1000];
displayTemp[1] = DuanMa[(num % 1000) / 100];
displayTemp[2] = DuanMa[(num % 100) / 10];
displayTemp[3] = DuanMa[num % 10];
}
void refreshDisplay() {
static unsigned long lastScanTime = 0;
static int currentDigit = 0;
if(millis() - lastScanTime >= 3) {
lastScanTime = millis();
// 消影
for(int i=0; i<8; i++) digitalWrite(ledPins[i], LOW);
digitalWrite(segPins[currentDigit], HIGH); // 关位选
currentDigit = (currentDigit + 1) % 4;
digitalWrite(segPins[currentDigit], LOW); // 开新位选
for(int i=0; i<8; i++) digitalWrite(ledPins[i], bitRead(displayTemp[currentDigit], i));
}
}
这个代码有bug,虽然可以使用但是容易造成堵塞原代码的核心 Bug(按键阻塞导致数码管熄灭)
阻塞点 1:delay(20)
问题:delay()会暂停整个程序 20ms,期间无法执行动态扫描refreshDisplay(),数码管会短暂闪烁 / 变暗;
阻塞点 2:while(digitalRead(KEY_PIN) == HIGH)
问题:这是 “死循环等待”—— 只要按键不松开,程序就永远卡在这个循环里,完全无法回到loop()执行refreshDisplay(),数码管直接熄灭。
修改后完整代码如下
/*
* 修复版:Arduino四位数码管计时系统(共阴极,非阻塞)
* 硬件配置:
* - 段选 A-H: D2-D9
* - 位选 COM1-4: D10-D13
* - 按键: D1
* 修复点:
* 1. 移除delay/while阻塞,改用millis()防抖
* 2. 按键松开才触发状态切换,避免卡死
*/
// 按键定义
#define KEY_PIN 1
// 数码管段选引脚 (a, b, c, d, e, f, g, dp) -> (2, 3, 4, 5, 6, 7, 8, 9)
int ledPins[] = {2, 3, 4, 5, 6, 7, 8, 9};
// 数码管位选引脚 (千, 百, 十, 个) -> (COM1, COM2, COM3, COM4) -> (10, 11, 12, 13)
int segPins[] = {10, 11, 12, 13};
// 共阴极段码表 (0-9):1(HIGH)亮,0(LOW)灭
const unsigned char DuanMa[10] = {
0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07, 0x7f, 0x6f
};
unsigned char displayTemp[4]; // 显示缓冲区(存4位段码)
bool countEnable = false; // 计时使能标志
int currentNumber = 0; // 当前显示数字
// 按键防抖相关
unsigned long keyDebounceTime = 0; // 防抖时间戳
const unsigned long debounceDelay = 20; // 防抖延时20ms
bool lastKeyState = LOW; // 上一次按键状态
void setup() {
// 初始化段选引脚(输出)
for (int i = 0; i < 8; i++) {
pinMode(ledPins[i], OUTPUT);
digitalWrite(ledPins[i], LOW);
}
// 初始化位选引脚(输出,共阴初始HIGH关闭)
for (int i = 0; i < 4; i++) {
pinMode(segPins[i], OUTPUT);
digitalWrite(segPins[i], HIGH);
}
// 初始化按键引脚(INPUT默认高阻,建议改INPUT_PULLUP更稳定)
pinMode(KEY_PIN, INPUT);
updateDisplayBuffer(0); // 初始显示0
}
// ✅ 修复后:非阻塞按键处理(无delay/while)
void handleKeyPress() {
bool currentKeyState = digitalRead(KEY_PIN);
unsigned long currentTime = millis();
// 1. 防抖:只有状态变化且超过防抖时间才处理
if (currentKeyState != lastKeyState && currentTime - keyDebounceTime > debounceDelay) {
keyDebounceTime = currentTime; // 更新防抖时间戳
// 2. 检测「按键松开」事件(避免按住时重复触发)
if (currentKeyState == LOW && lastKeyState == HIGH) {
countEnable = !countEnable; // 切换计时状态(开始/暂停)
}
}
lastKeyState = currentKeyState; // 更新上一次按键状态
}
void loop() {
// 1. 非阻塞按键处理(永不卡死)
handleKeyPress();
// 2. 非阻塞计时逻辑(1秒递增)
static unsigned long lastTimerTime = 0;
if (countEnable && millis() - lastTimerTime >= 1000) {
lastTimerTime = millis();
currentNumber = (currentNumber + 1) % 10000; // 0~9999循环
updateDisplayBuffer(currentNumber); // 更新显示缓冲区
}
// 3. 动态扫描显示(高频执行,永不阻塞)
refreshDisplay();
}
// ✅ 拆分数字+填充显示缓冲区(核心逻辑)
void updateDisplayBuffer(int num) {
// 提取千位:num ÷ 1000(整数除法)
displayTemp[0] = DuanMa[num / 1000];
// 提取百位:先取余1000去掉千位,再÷100
displayTemp[1] = DuanMa[(num % 1000) / 100];
// 提取十位:先取余100去掉千/百位,再÷10
displayTemp[2] = DuanMa[(num % 100) / 10];
// 提取个位:取余10
displayTemp[3] = DuanMa[num % 10];
}
// ✅ 非阻塞动态扫描(避免闪烁)
void refreshDisplay() {
static unsigned long lastScanTime = 0;
static int currentDigit = 0; // 当前扫描的位(0=千,1=百,2=十,3=个)
unsigned long currentTime = millis();
// 3ms扫描一次(人眼无闪烁)
if (currentTime - lastScanTime >= 3) {
lastScanTime = currentTime;
// 1. 消影:先关闭所有位选+段选
digitalWrite(segPins[currentDigit], HIGH); // 关闭当前位
for (int i = 0; i < 8; i++) {
digitalWrite(ledPins[i], LOW); // 段选清零
}
// 2. 切换到下一位(循环0-3)
currentDigit = (currentDigit + 1) % 4;
// 3. 输出当前位的段码
digitalWrite(segPins[currentDigit], LOW); // 打开当前位(共阴低电平有效)
for (int i = 0; i < 8; i++) {
// bitRead:提取段码的第i位(0/1),控制对应段脚亮灭
digitalWrite(ledPins[i], bitRead(displayTemp[currentDigit], i));
}
}
}
如果是阳极管代码则是
/*
* 项目名称:Arduino四位数码管计时系统 (共阳极版)
* 硬件配置:
* - 段选 A-H: D2-D9
* - 位选 COM1-4: D10-D13
* - 按键: D1
* 功能:修复阻塞Bug + 适配共阳极数码管
*/
// 按键定义
#define KEY_PIN 1
// 数码管段选引脚 (a, b, c, d, e, f, g, dp) -> (2, 3, 4, 5, 6, 7, 8, 9)
int ledPins[] = {2, 3, 4, 5, 6, 7, 8, 9};
// 数码管位选引脚 (千, 百, 十, 个) -> (COM1, COM2, COM3, COM4) -> (10, 11, 12, 13)
int segPins[] = {10, 11, 12, 13};
// 共阳极段码表 (0-9) 低电平点亮,段码取反
// 0(0x3F)->0xC0, 1(0x06)->0xF9, 2(0x5B)->0xA4, 3(0x4F)->0xB0, 4(0x66)->0x99
// 5(0x6D)->0x92, 6(0x7D)->0x82, 7(0x07)->0xF8, 8(0x7F)->0x80, 9(0x6F)->0x90
const unsigned char DuanMa[10] = {
0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x90
};
unsigned char displayTemp[4];
bool countEnable = false;
int currentNumber = 0;
// 按键状态记录
bool lastKeyState = LOW;
bool currentKeyState = LOW;
unsigned long lastDebounceTime = 0; // 防抖时间戳
void setup() {
// 初始化段选
for (int i = 0; i < 8; i++) {
pinMode(ledPins[i], OUTPUT);
digitalWrite(ledPins[i], HIGH); // 共阳极初始熄灭(高电平)
}
// 初始化位选(共阳极位选高电平有效,初始关闭所有位)
for (int i = 0; i < 4; i++) {
pinMode(segPins[i], OUTPUT);
digitalWrite(segPins[i], LOW); // 共阳极初始关闭(低电平)
}
pinMode(KEY_PIN, INPUT_PULLUP); // 改为内部上拉(更稳定)
updateDisplayBuffer(0);
}
// 优化版按键处理(非阻塞)
void handleKeyPress() {
unsigned long currentTime = millis();
// 防抖间隔20ms
if (currentTime - lastDebounceTime < 20) return;
currentKeyState = digitalRead(KEY_PIN);
// 检测按下瞬间(INPUT_PULLUP 按下为LOW)
if (currentKeyState == LOW && lastKeyState == HIGH) {
countEnable = !countEnable;
lastDebounceTime = currentTime; // 更新防抖时间
}
lastKeyState = currentKeyState;
}
void loop() {
// 1. 处理按键(非阻塞)
handleKeyPress();
// 2. 计时逻辑
static unsigned long lastTimerTime = 0;
if(countEnable && millis() - lastTimerTime >= 1000) {
lastTimerTime = millis();
currentNumber = (currentNumber + 1) % 10000;
updateDisplayBuffer(currentNumber);
}
// 3. 动态扫描(核心显示逻辑)
refreshDisplay();
}
// 更新显示缓冲区
void updateDisplayBuffer(int num) {
displayTemp[0] = DuanMa[num / 1000]; // 千位
displayTemp[1] = DuanMa[(num % 1000) / 100]; // 百位
displayTemp[2] = DuanMa[(num % 100) / 10]; // 十位
displayTemp[3] = DuanMa[num % 10]; // 个位
}
// 共阳极动态扫描刷新(适配阳极电平)
void refreshDisplay() {
static unsigned long lastScanTime = 0;
static int currentDigit = 0;
if(millis() - lastScanTime >= 3) {
lastScanTime = millis();
// 消影:先关闭所有段选和当前位选
for(int i=0; i<8; i++) digitalWrite(ledPins[i], HIGH); // 段选熄灭
digitalWrite(segPins[currentDigit], LOW); // 关闭当前位选(共阳极低电平关闭)
// 切换到下一位
currentDigit = (currentDigit + 1) % 4;
// 点亮新位
digitalWrite(segPins[currentDigit], HIGH); // 开启当前位(共阳极高电平有效)
for(int i=0; i<8; i++) {
// 共阳极:段码bit为0时点亮(输出LOW),bit为1时熄灭(输出HIGH)
digitalWrite(ledPins[i], bitRead(displayTemp[currentDigit], i) ? HIGH : LOW);
}
}
}
并且将GND和5v的位置调换
修改后代码的好处
按键非阻塞:
移除delay(20)和while死循环,改用millis()记录时间戳,防抖同时不暂停程序;
只在 “按键松开” 时触发状态切换,避免按住按键时重复触发。
扫描永不中断:
refreshDisplay()在loop()中高频执行,即使按键按住,数码管也能正常显示;
稳定性提升:
增加 “消影” 逻辑,避免多位数码管之间的串影 / 重影;
按键状态仅在 “状态变化 + 防抖超时” 时处理,误触发率大幅降低。
接下验证 ,左上角的勾是验证符号

查看是否验证成功,然后上传

上传成功就可以欣赏到开头的结果了
7.心得
我认为这是个新的开始我也遇到了很多问题(见下面),但是当我成功做出来了后感到非常自豪,所以我认为在这个时代人类要不断走出自己的舒适圈,才能前进,如果觉得有帮助就请点一下赞,我火速更新下一章,我是FOX,励志做一个博学的萌新。
8.花絮
在看到代码的时候,我就已经在想代码有什么可以修改的了,于是这些问题就诞生了
为什么会用millis()记录时间戳
用millis()替代delay:通过时间戳记录按键状态变化的时间,超过防抖阈值才处理,不阻塞程序;
检测 “松开边缘”:仅在「按键从按下(HIGH)变为松开(LOW)」时触发逻辑,避免按住时重复触发;
无while死循环:彻底移除等待按键松开的死循环,保证loop()持续运行,数码管扫描不中断。

溢出问题:millis() 约 49.7 天后会从 4294967295 重置为 0,此时 currentTime - lastTime 仍能正确计算(无符号数减法特性),无需额外处理。
// 按键防抖+非阻塞配置
unsigned long keyDebounceTime = 0; // 按键防抖时间戳
const unsigned long debounceDelay = 20; // 防抖阈值(20ms)
bool lastKeyState = HIGH; // 上一次按键状态(初始高电平,适配INPUT_PULLUP)
bool keyTriggered = false; // 避免重复触发的标志
// ✅ 非阻塞按键处理:仅松开触发,无delay/while
void handleKeyPress() {
// 1. 读取当前按键状态(建议按键接INPUT_PULLUP,按下为LOW,松开为HIGH)
bool currentKeyState = digitalRead(KEY_PIN);
unsigned long currentTime = millis();
// 2. 防抖判断:状态变化且超过防抖时间,才处理
if (currentKeyState != lastKeyState && currentTime - keyDebounceTime > debounceDelay) {
keyDebounceTime = currentTime; // 更新防抖时间戳
// 3. 检测「按键松开」边缘(从按下→松开)
// 若用INPUT(非上拉),则判断条件改为:currentKeyState == HIGH && lastKeyState == LOW
if (currentKeyState == HIGH && lastKeyState == LOW) {
countEnable = !countEnable; // 切换计时状态(仅松开时触发一次)
keyTriggered = true; // 标记已触发,避免重复
}
}
// 4. 更新上一次按键状态
lastKeyState = currentKeyState;
}
经典知识点回顾
for()
for()是我们常见的循环,这这个代码中甚至可以看到很多for循环
for (int i = 0; i < 8; i++) {
pinMode(ledPins[i], OUTPUT);
digitalWrite(ledPins[i], LOW);
}
然而我们可以通过for循环变成其他循环比如while()和do…while()
分别是while()
// 替换原for循环:用while初始化段选引脚
int i = 0; // 初始化循环变量
while (i < 8) { // 条件:i小于8时执行
pinMode(ledPins[i], OUTPUT);
digitalWrite(ledPins[i], LOW);
i++; // 循环变量自增,避免死循环
}
先定义循环变量 i 并赋值为 0;
判断 i < 8,满足则执行循环体(配置引脚 + 拉低电平);
每次循环结束后 i++,直到 i=8 时条件不满足,退出循环
do…while()
// 替换原for循环:用do...while初始化段选引脚
int i = 0; // 初始化循环变量
do {
pinMode(ledPins[i], OUTPUT);
digitalWrite(ledPins[i], LOW);
i++; // 循环变量自增
} while (i < 8); // 条件:i小于8时继续循环
先执行一次循环体(即使条件不满足,也会先执行);
执行完后判断 i < 8,满足则继续循环,不满足则退出;
此处 i 初始为 0,最终执行逻辑和 for/while 完全一致(执行 8 次)
两者的区别则是do…while 至少执行一次循环体,适合 “必须执行一次” 的场景,while则不用。

1843

被折叠的 条评论
为什么被折叠?



