十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Arduino IO扩展实战:用74HC595驱动双数码管计数器

Arduino IO扩展实战:用74HC595驱动双数码管计数器 1. 项目缘起为什么需要两个数码管计数器最近在做一个自动化小项目需要同时监控两个独立传感器的脉冲计数比如一个记录传送带上的产品数量另一个记录合格品数量。最直接的想法当然是上两块Arduino但成本和控制复杂度都上去了。另一个方案是用Arduino的多个数字引脚直接驱动两个独立的数码管但稍微一算就知道不现实一个7段数码管带小数点就是8段需要8个IO口两个就需要16个Arduino Uno总共才14个数字IO这还没算上可能需要的按钮、传感器等其他外设。所以这个“Arduino控制的双7段LED计数器”项目核心要解决的就是在有限的IO口资源下实现两个独立数字的稳定、清晰显示。这不仅仅是点亮两个数码管那么简单它涉及到IO扩展、动态扫描、代码效率以及抗干扰等一系列实际问题。我最终选择了“移位寄存器动态扫描”这个经典组合拳成本不到十块钱只用Arduino的3个引脚就搞定了两个数码管代码逻辑也清晰可控。下面我就把从方案选型、电路搭建到代码调试的完整过程以及中间踩过的几个坑详细拆解一遍。2. 核心方案选型为什么是74HC595移位寄存器面对IO口不足的问题通常有几种扩展思路使用IO扩展芯片如PCF8574、专用的LED驱动芯片如TM1637、MAX7219或者使用移位寄存器。我选择了最基础、也最具有学习价值的74HC595串入并出移位寄存器。2.1 几种方案的横向对比先简单说说为什么没选其他方案。PCF8574这类I2C扩展芯片固然好但它本质上还是扩展出并行IO驱动多个数码管依然需要较多的连接线且动态扫描逻辑仍需主控完成。TM1637或MAX7219是专用的LED驱动芯片集成度高甚至自带译码器使用起来最简单但“黑盒”程度也高不利于理解底层驱动原理且成本稍高。74HC595则是一个完美的教学和实践结合点。它价格低廉单价几毛钱工作原理直观串行输入并行输出并且可以轻松地级联多个理论上用3个引脚数据、时钟、锁存就能控制几乎无限多的输出位。通过它来驱动数码管你能亲手实现从串行数据到并行显示的全过程对理解数字电路和微控制器通信大有裨益。2.2 74HC595驱动单个数码管的原理一个74HC595有8个并行输出口Q0-Q7正好对应一个7段数码管的a-g段加上小数点dp。驱动一个数码管的基本流程是“串行移入并行锁存输出”准备数据Arduino将一个8位的数据一个字节按位从最高位或最低位通过DATA引脚送出。时钟同步每送出一个位Arduino在CLOCK引脚产生一个上升沿脉冲告诉74HC595“请读取当前DATA引脚上的值并移入内部的移位寄存器。”更新显示当8位数据全部移入后Arduino在LATCH引脚产生一个上升沿脉冲。这个脉冲告诉74HC595“现在请把移位寄存器里的8位数据一次性复制到输出锁存器并驱动Q0-Q7引脚输出。”此时数码管的各段才会根据数据亮灭。这个过程就像是一个仓库74HC595有8个货位输出口。Arduino通过一条狭窄的传送带串行数据线依次送来8件货物数据位每来一件就敲一下钟时钟脉冲通知仓库接收。等8件货都在临时堆放区移位寄存器放好后再拉一下闸锁存脉冲所有货物瞬间被摆上对应的8个正式货架输出锁存器。2.3 级联两个74HC595驱动两个数码管要驱动两个数码管就需要两个74HC595级联。级联的关键在于将第一个595的串行输出引脚Q7‘或叫SER_OUT连接到第二个595的串行数据输入引脚SER_IN或DS。这时数据传输过程变成了“16位串行移入并行锁存输出”Arduino先准备一个16位的数据。其中高8位对应第二个更远的数码管低8位对应第一个离Arduino更近的数码管。Arduino通过DATA线先发送第二个数码管的8位数据高8位再发送第一个数码管的8位数据低8位。每发送一位都伴随一个CLOCK脉冲。数据在第一个595的移位寄存器里暂存后会通过Q7‘引脚“挤”到第二个595的移位寄存器里。发送完16个脉冲后两个595的移位寄存器里分别存放了各自的数据。最后一个LATCH脉冲同时作用于两个595它们的LATCH引脚并联两个595同时将各自移位寄存器的数据更新到输出锁存器两个数码管同时显示新数字。这里有个非常重要的细节虽然数据更新是同时的一个锁存信号但显示内容本身是独立的。我们通过发送不同的16位数据就能让两个数码管显示不同的数字。这正是实现“双计数器”的基础。3. 硬件连接与电路搭建详解理论清楚了动手接线是关键。接线错误是导致项目失败的最常见原因。下图清晰地展示了Arduino Uno、两个74HC595和两个共阴极数码管之间的连接关系此处为文字描述实际项目应配接线图。3.1 元件清单与引脚定义主控Arduino Uno x1核心芯片74HC595移位寄存器 x2显示器件0.56英寸共阴极7段LED数码管 x2 务必确认是共阴极限流电阻220Ω 或 330Ω 电阻 x8 每个段一个或每个595的输出引脚串联一个到Vcc动态扫描时计算方法不同面包板与杜邦线若干74HC595关键引脚说明DS(Pin 14): 串行数据输入 (Serial Data Input)SH_CP(Pin 11): 移位寄存器时钟输入 (Shift Register Clock Input)ST_CP(Pin 12): 存储寄存器时钟输入 (Storage Register Clock Input / Latch)OE(Pin 13): 输出使能低电平有效 (Output Enable, active LOW)。通常直接接地使其一直有效。MR(Pin 10): 主复位低电平有效 (Master Reset, active LOW)。通常直接接Vcc防止意外复位。Q0-Q7(Pin 15, 1-7): 并行数据输出。Q7(Pin 9): 串行数据输出用于级联。3.2 分步接线指南注意在接通任何电源前请再三检查接线特别是Vcc和GND不要接反。建议先接好电源和地线再连接信号线。第一步搭建第一个74HC595IC1的最小系统将IC1的Vcc(Pin 16) 和MR(Pin 10) 连接到面包板的5V电源轨。将IC1的GND(Pin 8) 和OE(Pin 13) 连接到面包板的GND电源轨。将Arduino的5V和GND分别连接到面包板的电源轨为整个电路供电。第二步连接Arduino控制线将Arduino的数字引脚D2连接到IC1的DS(Pin 14)。这是数据线。将Arduino的D3连接到IC1的SH_CP(Pin 11)。这是移位时钟线。将Arduino的D4连接到IC1的ST_CP(Pin 12)。这是锁存时钟线。 这三根线之后会并联到第二个595上。第三步连接第一个数码管DISPLAY1确认数码管是共阴极。共阴极数码管的所有段LED的阴极负极是连接在一起的通常对应中间的两个引脚。你需要查阅数码管的数据手册或用万用表测试确认。将IC1的8个输出引脚Q0-Q7分别对应Pin 15, 1-7通过220Ω的限流电阻依次连接到数码管的8个段选引脚a, b, c, d, e, f, g, dp。电阻必不可少直接连接会因电流过大烧毁LED或芯片。例如IC1-Q0 (Pin15) - 电阻 - 数码管-a段。段码顺序需要与代码中的数组定义严格对应接错了显示会乱。建议先按一个标准顺序接如果显示不对再调整代码中的数组顺序这比改硬件接线方便。将数码管的共阴极引脚通常是3和8连接到GND。第四步级联第二个74HC595IC2像第一步一样为IC2接好电源Vcc, MR和地GND, OE。级联关键步骤将IC1的Q7(Pin 9) 连接到IC2的DS(Pin 14)。这样数据就能从第一个芯片“流”到第二个芯片。将IC2的SH_CP(Pin 11) 和ST_CP(Pin 12)分别并联到IC1的对应引脚上。也就是说Arduino的D3和D4两根线现在同时连接着两个芯片的时钟引脚。这样时钟信号能同时控制两个芯片移位和锁存。第五步连接第二个数码管DISPLAY2将IC2的Q0-Q7输出引脚同样通过220Ω限流电阻连接到第二个数码管的各个段选引脚。将第二个数码管的共阴极连接到GND。至此硬件连接完成。整个系统只占用了Arduino的3个数字引脚D2, D3, D4就控制了两个8位输出端口共16个控制点。4. 软件驱动从数字到点亮的逻辑硬件是躯体软件是灵魂。驱动代码不仅要负责发送数据还要解决“如何让Arduino知道哪个数字对应哪段LED亮”以及“如何实现两个独立计数”的问题。4.1 段码表数字的“翻译官”7段数码管显示数字本质上就是控制a-g这7个段的亮灭组合。对于共阴极数码管要点亮某一段就需要在对应的引脚输出高电平1。我们可以预先定义一个数组把0-9这十个数字各自对应的a-g段亮灭状态一个字节存起来这个数组就叫段码表。// 共阴极数码管段码表 (a, b, c, d, e, f, g, dp) // 顺序必须与你的硬件接线顺序一致 byte digitSegments[10] { B00111111, // 0 - a,b,c,d,e,f亮 B00000110, // 1 - b,c亮 B01011011, // 2 - a,b,d,e,g亮 B01001111, // 3 - a,b,c,d,g亮 B01100110, // 4 - b,c,f,g亮 B01101101, // 5 - a,c,d,f,g亮 B01111101, // 6 - a,c,d,e,f,g亮 B00000111, // 7 - a,b,c亮 B01111111, // 8 - 全亮 B01101111 // 9 - a,b,c,d,f,g亮 }; // 注意这里忽略了小数点dp如果需要可以修改字节的最高位(MSB)定义好这个表后要显示数字5我们只需要查找digitSegments[5]得到B01101101然后把这个字节发送给74HC595即可。4.2 核心驱动函数shiftOut与手动锁存Arduino提供了shiftOut()函数来简化串行数据发送过程。它的原型是shiftOut(dataPin, clockPin, bitOrder, value)其中bitOrder可以是MSBFIRST先发送最高位或LSBFIRST先发送最低位。这需要与你定义的段码表位顺序以及硬件接线匹配。通常使用MSBFIRST。我们的驱动函数需要处理两个字节16位的数据并发送给级联的595// 引脚定义 const int dataPin 2; // DS const int clockPin 3; // SH_CP const int latchPin 4; // ST_CP void writeToDisplays(byte digit1, byte digit2) { // 先拉低锁存引脚准备接收数据 digitalWrite(latchPin, LOW); // 先发送第二个数码管的数据高8位 shiftOut(dataPin, clockPin, MSBFIRST, digit2); // 再发送第一个数码管的数据低8位 shiftOut(dataPin, clockPin, MSBFIRST, digit1); // 所有数据移位完成后拉高锁存引脚更新输出 digitalWrite(latchPin, HIGH); }这个函数是核心。digit2对应第二个595显示十位或第二个计数器digit1对应第一个595。调用writeToDisplays(segCodeForNum1, segCodeForNum2)两个数码管就会同时更新显示。4.3 实现双计数器逻辑与动态扫描有了驱动函数实现计数器就简单了。我们可以在内存中维护两个整数变量counter1和counter2。但这里有一个关键点如何显示多位数一个数码管只能显示一位数字0-9。对于0-99的计数器我们需要用两个数码管来显示一个计数器。这意味着我们需要4个数码管来显示两个独立的0-99计数器。我们的硬件正好是两级级联的595驱动4个数码管需要4个595或者更常见的做法是利用动态扫描技术用两个595驱动4个数码管。动态扫描的原理是利用人眼的视觉暂留。让4个数码管轮流点亮每个只点亮很短时间比如1-5毫秒只要轮换速度足够快50Hz人眼就会觉得它们是同时亮的。这样做可以极大节省硬件资源从4个595减少到2个但代码会稍复杂。我们需要修改硬件和代码硬件两个数码管的段选线a-g, dp分别并联在一起连接到第一个595的Q0-Q7。两个数码管的位选线共阴极端不再直接接地而是分别连接到第二个595的两个输出引脚例如Q0和Q1并通过一个晶体管如2N2222或直接如果电流很小由595控制接地。代码需要维护一个显示缓冲区数组存放4个要显示的数字的段码。在loop()函数中快速循环扫描4个数码管先通过第一个595发送第1个数码管的段码再通过第二个595的对应位选通第1个数码管的位选线使其共阴极为低电平其他位为高电平断开延时1-2ms然后发送第2个数码管的段码选通第2个数码管……如此循环。由于篇幅和初始项目目标我们先实现用两个595驱动两个独立的一位计数器0-9。更复杂的4位数动态扫描显示可以作为本项目的进阶扩展。4.4 基础双计数器示例代码下面是一个完整的、简单的双计数器代码示例。两个计数器分别由两个按钮控制递增复位到0。// 引脚定义 const int dataPin 2; const int clockPin 3; const int latchPin 4; const int buttonPin1 5; // 计数器1按钮 const int buttonPin2 6; // 计数器2按钮 const int resetPin 7; // 复位按钮同时复位两个 // 共阴极段码表 (a,b,c,d,e,f,g,dp)dp为最高位此处为0 byte segCodes[10] { B00111111, //0 B00000110, //1 B01011011, //2 B01001111, //3 B01100110, //4 B01101101, //5 B01111101, //6 B00000111, //7 B01111111, //8 B01101111 //9 }; int counter1 0; int counter2 0; int lastButtonState1 HIGH; int lastButtonState2 HIGH; void setup() { pinMode(dataPin, OUTPUT); pinMode(clockPin, OUTPUT); pinMode(latchPin, OUTPUT); pinMode(buttonPin1, INPUT_PULLUP); pinMode(buttonPin2, INPUT_PULLUP); pinMode(resetPin, INPUT_PULLUP); updateDisplay(); // 初始显示0,0 } void loop() { // 读取按钮状态内部上拉按下为LOW int buttonState1 digitalRead(buttonPin1); int buttonState2 digitalRead(buttonPin2); int resetState digitalRead(resetPin); // 检测计数器1按钮下降沿按下 if (buttonState1 LOW lastButtonState1 HIGH) { delay(50); // 简单消抖 if (digitalRead(buttonPin1) LOW) { counter1 (counter1 1) % 10; // 0-9循环 updateDisplay(); } } lastButtonState1 buttonState1; // 检测计数器2按钮下降沿 if (buttonState2 LOW lastButtonState2 HIGH) { delay(50); if (digitalRead(buttonPin2) LOW) { counter2 (counter2 1) % 10; updateDisplay(); } } lastButtonState2 buttonState2; // 检测复位按钮 if (resetState LOW) { delay(50); if (digitalRead(resetPin) LOW) { counter1 0; counter2 0; updateDisplay(); while(digitalRead(resetPin) LOW); // 等待松开 } } } // 更新显示函数 void updateDisplay() { byte digit1Code segCodes[counter1]; byte digit2Code segCodes[counter2]; digitalWrite(latchPin, LOW); shiftOut(dataPin, clockPin, MSBFIRST, digit2Code); // 显示在第二个数码管 shiftOut(dataPin, clockPin, MSBFIRST, digit1Code); // 显示在第一个数码管 digitalWrite(latchPin, HIGH); }5. 调试与实战中的坑点汇总项目做完了但一次成功的情况很少。下面是我在调试过程中遇到的一些典型问题及解决方案希望能帮你避开这些坑。5.1 显示乱码或部分段不亮这是最常见的问题根源通常是段码顺序不匹配。症状按下按钮显示的数字不是预期的比如按一下从0跳到8或者某个段该亮不亮。排查检查硬件接线确认74HC595的Q0-Q7引脚是否严格按照a,b,c,d,e,f,g,dp的顺序连接到数码管。一个引脚接错整个编码就乱了。检查段码表段码表中每个字节的每一位必须与硬件接线的段顺序严格对应。例如如果你的接线顺序是a-Q0, b-Q1, c-Q2... dp-Q7那么段码表的最低有效位(LSB)就应对应a段。我上面的示例代码假设了这种顺序。如果你的顺序不同就需要重排segCodes数组里每个字节的位。使用测试代码写一个简单的测试程序依次点亮数码管的每一段例如依次发送B00000001,B00000010,B00000100...观察亮起的段是否符合预期。这能最直观地确定段序。5.2 显示暗淡或亮度不均症状所有段都亮但很暗或者某些段特别暗。原因与解决限流电阻过大我推荐使用220Ω电阻。如果使用1kΩ或更大电流会太小导致LED亮度不足。可以尝试减小电阻值不低于100Ω以防电流过大。动态扫描的占空比问题如果你采用了动态扫描驱动4个数码管每个管点亮的时间太短平均电流小就会变暗。可以适当增加每个数码管的点亮时间但需保持总扫描频率高于50Hz或者减少限流电阻值。注意在动态扫描下因为每个LED只有1/4的时间在通电为了达到相同的视觉亮度其瞬间电流可以是静态驱动的4倍。所以电阻可以比静态驱动时小。例如静态用330Ω动态扫描可以用100Ω。电源功率不足如果使用USB供电且驱动多个LED总电流可能超过USB端口的限值通常500mA。尝试使用外部电源如9V电池适配器为Arduino供电。5.3 按钮抖动与误触发症状按一次按钮计数器跳了好几个数字。解决机械按钮在按下和释放的瞬间会产生快速的通断抖动持续约10-50毫秒。上面的示例代码使用了简单的delay(50)进行消抖这是一种“阻塞式”消抖在延时期间单片机无法做其他事。对于更复杂的项目建议使用“非阻塞式”消抖通过记录时间戳来判断。unsigned long lastDebounceTime1 0; const unsigned long debounceDelay 50; int reading1 digitalRead(buttonPin1); if (reading1 ! lastButtonState1) { lastDebounceTime1 millis(); // 重置消抖计时器 } if ((millis() - lastDebounceTime1) debounceDelay) { // 此时reading1才是稳定状态 if (reading1 ! buttonState1) { buttonState1 reading1; if (buttonState1 LOW) { // 执行动作 } } } lastButtonState1 reading1;5.4 级联时第二个数码管无显示或显示错误症状只有第一个数码管工作正常第二个始终不亮或显示固定内容。排查级联线连接确保第一个595的Q7(Pin 9) 正确连接到第二个595的DS(Pin 14)。这是数据流过去的关键。数据发送顺序在writeToDisplays函数中务必先发送第二个数码管的数据高8位再发送第一个的低8位。因为先发送的数据会被时钟脉冲推到级联链的更深处。锁存信号并联确认两个595的ST_CP(锁存)引脚是并联在一起接到Arduino的latchPin上的。如果第二个595的锁存引脚没接它永远不会更新输出。电源与地检查第二个595和第二个数码管的Vcc和GND是否都连接牢固。5.5 从一位显示扩展到多位动态扫描如果你想将本项目升级为显示00-99的双计数器采用动态扫描是必由之路。这里有几个额外的坑点位选驱动能力74HC595的输出引脚驱动电流有限通常每个引脚约20-35mA。如果直接用它来驱动数码管的共阴极当多个段同时点亮时总电流可能超标。务必使用晶体管如NPN型2N2222或专用的位选驱动芯片如ULN2003来放大电流控制数码管的共阴极。将595的输出引脚接到晶体管的基极数码管的共阴极接到晶体管的集电极发射极接地。扫描频率loop()中扫描4个数码管的总时间必须小于20ms即频率50Hz否则会出现闪烁。每个管的点亮时间建议在1-5ms之间。使用delay()会严重影响扫描频率和程序其他部分的响应。应该用millis()进行非阻塞的时间管理。亮度平衡显示数字“1”时只有2段亮显示数字“8”时7段全亮两者的总电流相差很大。如果电源内阻较大可能会导致在显示“8”时电压被拉低亮度下降甚至单片机复位。确保电源特别是给数码管部分供电的线路足够“硬”电容滤波、粗导线。6. 项目进阶与扩展思路这个双计数器项目是一个非常好的起点掌握了它你可以轻松扩展出更多有趣的应用。6.1 输入源的扩展现在的计数器由按钮触发。你可以将其替换为红外对射传感器用于统计通过的人数或物体数。旋转编码器用于测量转速或位置实现可增可减的计数器。声音传感器配合特定滤波算法实现拍手计数。其他Arduino数字/模拟引脚接收来自其他传感器或模块的信号。6.2 显示内容的扩展显示字母或符号7段数码管可以显示部分英文字母如A, b, C, d, E, F, H, L, P, U等。你只需要在段码表中增加对应的编码即可实现简单的状态指示如“Err”, “On”, “OFF”。添加小数点利用段码字节的最高位dp段可以显示小数点用于显示电压、温度等带一位小数的数值。连接更多数码管通过级联更多的74HC595可以驱动4位、8位甚至更多的数码管制作大型计数器或计时器。6.3 系统功能的扩展加入EEPROM存储使用Arduino的EEPROM库将计数器的值在断电前保存起来上电后自动恢复实现非易失计数。增加无线模块接入蓝牙模块如HC-05或Wi-Fi模块如ESP-01S通过手机App或网页远程查看、控制或重置计数器。与上位机通信通过串口将计数器的值实时发送到电脑上的Processing或Python程序进行数据记录和可视化分析。制作成独立设备使用Arduino Pro Mini或ATtiny85等更小的核心板配合电池和充电模块将整个系统集成到一个小盒子里做成一个便携式的双路计数器/计时器工具。这个项目的魅力在于它像一块积木掌握了基本原理后你可以根据自己的需求把它嵌入到各种更大的系统中去。从简单的按钮计数到复杂的自动化监测其核心的“IO扩展”与“动态显示”思想是相通的。动手做一遍调试一遍这些知识就真正变成你自己的了。
返回列表