1. 项目概述:硬币识别系统的核心价值与实现路径

在嵌入式开发领域,传感器数据的实时、准确采集是许多自动化项目的基石。今天我想分享一个我实际搭建过的项目:一个基于Arduino的硬币收集与计数系统。这个项目的核心,远不止是数清楚几个硬币那么简单,它实际上是一个关于 如何可靠地处理外部异步事件 的经典案例。无论是用在慈善捐赠箱里统计善款,还是作为自动售货机支付模块的原型,其底层逻辑——通过外部中断捕捉硬币识别器产生的脉冲信号——都是通用的关键技术。

很多朋友在入门Arduino时,第一个接触的可能是 digitalRead() 配合 loop() 循环来检测引脚状态。但在处理像硬币识别器脉冲这种对实时性要求高、信号持续时间短的事件时,轮询方式很容易丢失数据。我这个项目正是为了解决这个问题,它清晰地展示了 外部中断(External Interrupt) 在何时、以及为何是更优的选择。通过这个系统,硬币投入后,识别器会根据币值输出特定数量的脉冲,Arduino的中断服务程序会立即响应,确保每一个脉冲都被准确计数,最终累加出总金额并显示在LCD屏上。整个过程,从硬件选型、电路设计、3D打印外壳到软件编程,是一个完整的、可复现的嵌入式系统开发流程。无论你是想学习中断机制,还是需要为一个具体的支付场景搭建原型,这个项目都能提供扎实的参考。

2. 系统整体设计与核心思路拆解

2.1 需求分析与方案选型

这个项目的核心需求很明确: 准确、实时地识别投入的硬币并累计总金额 。拆解开来,它包含了几个关键子任务:硬币的物理接收与电信号转换、电信号的可靠读取、金额计算与状态显示。针对这些任务,我选择了以下方案:

  1. 核心控制器:Arduino UNO 。选择它是因为其生态成熟,IO口和中断资源对于本项目完全够用,且编程环境友好,便于快速原型开发。
  2. 传感器:电子硬币识别器(Coin Acceptor) 。这是整个系统的“眼睛”。市面上常见的型号(如常见的MG系列)工作原理类似:通过光学或电磁感应识别硬币的直径、材质,然后通过输出引脚发送对应数量的脉冲信号。例如,识别到1元硬币输出4个脉冲,5角硬币输出5个脉冲。这种数字脉冲输出方式,非常便于微控制器处理。
  3. 人机交互:20x4字符型LCD屏与四个按键 。LCD屏用于实时显示总金额、系统状态等信息。四个按键则分别实现启动/停止计数、金额清零、显示模式切换、系统设置等功能,提供了灵活的操作界面。
  4. 结构件:3D打印外壳 。为了将所有电子部件整合成一个稳固、美观的整体,我使用Autodesk Fusion 360设计了外壳,并交由3D打印服务制作。这确保了硬币滑道、电路板安装位、屏幕开孔等都能精准匹配。

为什么选择“外部中断”而不是“轮询”? 这是本项目的关键设计决策。硬币识别器输出的脉冲是异步事件,脉宽可能只有几毫秒。如果使用 loop() 循环不断去 digitalRead() 检测引脚电平,极有可能在两次读取的间隙错过脉冲。而外部中断的工作原理是:为特定引脚设置中断服务程序(ISR),当该引脚的电平发生指定变化(如从高到低)时,无论CPU正在执行什么代码,都会立即跳转到ISR执行。这保证了脉冲事件的 零丢失 ,是实现精准计数的前提。

2.2 硬件架构与信号流

整个系统的硬件架构围绕Arduino UNO展开,我为其设计了一块扩展板(Shield),使得连线更加整洁可靠。信号流如下:

  1. 电源 :系统采用12V直流电源输入。其中,硬币识别器通常需要12V工作电压,而Arduino及LCD等模块需要5V。因此,12V电源直接接入识别器,同时接入扩展板的电源输入端,通过板载的降压模块(如LM2596)为Arduino等提供稳定的5V电源。
  2. 脉冲信号采集 :硬币识别器的信号输出线(通常标记为“Pulse”或“COIN”)连接到Arduino UNO的 数字引脚2(D2) 。选择D2是因为在Arduino UNO上,它对应着 外部中断0(INT0) ,便于编程。
  3. 人机接口
    • LCD屏通过I2C接口模块与Arduino连接(SDA->A4, SCL->A5),仅需两根数据线和电源线即可通信,节省了IO口。
    • 四个按键分别连接到其他数字引脚(如D3, D4, D5, D6),并通过上拉电阻确保默认高电平,按下时变为低电平。
  4. 机械结构 :3D打印的外壳内部设计了卡槽和螺丝柱,用于固定Arduino主板、扩展板、LCD屏和硬币识别器。硬币投入口与识别器的进币口对齐,确保硬币能顺畅滑入。

注意: 不同型号的硬币识别器,其脉冲信号的电平逻辑可能不同(有的是常态高电平,投币时输出低电平脉冲;有的则相反)。在接线和编程前,务必查阅你的识别器数据手册。本项目代码基于“常态高电平,脉冲为下降沿”的型号编写。

3. 核心细节解析与实操要点

3.1 硬币识别器的工作原理与校准

硬币识别器内部通常包含一组红外对管或电磁传感器。硬币滚过时,会遮挡或改变传感器的状态。微控制器通过分析这些变化的时序和模式,与内置的硬币参数库进行比对,从而判断币值。识别成功后,它才会通过信号线输出脉冲。

实操要点:脉冲数与币值的映射 这是编程的基础。你需要通过实验来确定你的识别器对于每种硬币输出的脉冲数。方法很简单:

  1. 将识别器的信号线接到一个可以计数脉冲的仪器(或另一个Arduino的计数器引脚),或者直接使用本项目后续的代码,但先只写中断计数部分,并在串口监视器中打印 impulsCount
  2. 依次投入不同面值的硬币。
  3. 记录每种硬币投入后,串口输出的脉冲数量。例如,你可能得到“1元硬币 -> 4个脉冲”、“5角硬币 -> 5个脉冲”的对应关系。 这个映射关系必须准确,它是后续金额计算的根本。

3.2 外部中断的配置与中断服务程序(ISR)编写

Arduino UNO支持两个外部中断:INT0(引脚D2)和INT1(引脚D3)。我们使用INT0。

代码解析:

// 在setup()函数中
pinMode(2, INPUT_PULLUP); // 将D2设置为输入模式,并启用内部上拉电阻
attachInterrupt(0, incomingImpuls, FALLING); // 关联中断
  • attachInterrupt(interrupt, ISR, mode) :这是配置中断的核心函数。
    • interrupt : 中断编号,0代表INT0(D2)。
    • ISR : 中断服务函数名,当中断触发时,自动调用此函数。
    • mode : 触发模式。 FALLING 表示当引脚电平由高变低(下降沿)时触发。这符合我们识别器“高电平常态,低电平脉冲”的假设。

中断服务程序(ISR) incomingImpuls

void incomingImpuls() {
  impulsCount = impulsCount + 1;
  time = 0;
}

这个函数极其简短,只做了两件事:

  1. impulsCount++ :将脉冲计数器加1。
  2. time = 0 :将一个计时变量 time 清零。

为什么ISR要如此简短? 这是一个重要的编程原则。中断会打断主循环 loop() 的执行。如果ISR执行时间过长,可能会导致主循环中的其他任务(如刷新显示、响应按键)出现明显的延迟或卡顿。因此,ISR内应只做最必要、最快速的操作(如设置标志位、计数),将复杂的处理(如判断币值、更新显示)留给主循环。

3.3 主循环中的脉冲序列解析与防误判机制

中断负责“数脉冲”,主循环则负责“认硬币”。硬币识别器输出的是一个脉冲串,我们需要等待这个串结束后,根据总脉冲数来判断是哪种硬币。

代码逻辑解析:

void loop() {
  time = time + 1; // 每个循环周期,计时变量自增

  // 条件1:检测5角硬币(5个脉冲)
  if (time >= 30 && impulsCount == 5) {
    total_amount = total_amount + 0.5;
    updateDisplay(); // 更新LCD显示
    Serial.print("Total: ");
    Serial.println(total_amount);
    impulsCount = 0; // 重置脉冲计数,准备下一次识别
  }

  // 条件2:检测1元硬币(4个脉冲)
  if (time >= 30 && impulsCount == 4) {
    total_amount = total_amount + 1;
    updateDisplay();
    Serial.print("Total: ");
    Serial.println(total_amount);
    impulsCount = 0;
  }

  // 防误判机制:超时重置
  if(time > 15 && (impulsCount < 4 || impulsCount > 5)) {
    impulsCount = 0;
  }
  delay(15); // 主循环延迟,控制计时粒度
}

核心机制解释:

  1. 计时与等待 time 变量在每个 loop() 周期(约15ms)加1。 if (time >= 30 ...) 意味着系统在检测到第一个脉冲(触发中断, time 被清零)后,会等待大约 30 * 15ms = 450ms 。这个时间窗口是为了确保硬币识别器已经输完了该次投币的所有脉冲。
  2. 币值判断 :在等待时间结束后( time>=30 ),检查 impulsCount 的值。如果是5,则判定为5角硬币,金额加0.5;如果是4,则判定为1元硬币,金额加1。
  3. 关键的防误判机制 if(time > 15 && (impulsCount < 4 || impulsCount > 5)) 这行代码至关重要。它处理了异常情况:
    • 场景 :投入了无法识别的异物,或者硬币卡顿导致识别器输出了异常数量的脉冲(比如1个、2个或6个以上)。
    • 逻辑 :在等待了较短时间( 15 * 15ms = 225ms )后,如果脉冲数既不是4也不是5,系统就认为这是一次无效事件,直接重置 impulsCount 为0,避免干扰下一次正常投币的判断。这个“超时重置”的机制极大地提高了系统的鲁棒性。

实操心得 delay(15) 和判断条件中的 30 15 这些数值需要根据实际情况微调。它们共同决定了系统识别硬币的速度和容错性。如果 delay() 太小, time 累加过快,可能脉冲还没发完就被误判;如果 delay() 太大,系统响应会变慢。最佳值需要通过实验,结合你的硬币识别器脉冲间隔来确定。

4. 系统扩展与进阶功能实现

4.1 从捐赠箱到自动售货机的功能演进

基础版本实现了计数和显示,这已经是一个可用的捐赠箱。要升级为自动售货机,需要增加“商品选择”和“出货控制”功能。

硬件扩展:

  1. 商品选择按键 :可以复用或增加按键,让用户选择对应的商品(如A、B、C)。
  2. 出货执行机构 :根据商品不同,可以使用舵机控制的推杆、直流电机驱动的螺旋货道、或者电磁铁等。每个商品通道需要一个独立的控制引脚。
  3. 余额计算与找零逻辑(可选) :如果需要处理多商品和找零,逻辑会复杂很多。通常自动售货机支持多次投币直至金额足够。

软件逻辑升级:

  1. 状态机设计 :系统状态变得复杂,建议使用状态机(State Machine)来管理。例如:
    • STATE_IDLE :待机,显示欢迎信息。
    • STATE_SELECTING :用户按下商品键,显示商品价格和已投金额。
    • STATE_ACCEPTING_COINS :投币中,实时更新已投金额并与商品价格比较。
    • STATE_DISPENSING :金额足够,驱动对应出货电机,并显示“请取货”。
    • STATE_CHANGE (可选):如果需要找零,进入找零状态。
  2. 代码结构优化 :将投币识别、金额计算、状态判断、显示控制、电机驱动等模块化,使代码更清晰,易于维护和扩展。

4.2 物联网(IoT)集成:基于ESP32的升级方案

原项目提到了未来的ESP32升级计划,这是一个非常实用的方向。用ESP32替换Arduino UNO,可以轻松为系统添加网络功能。

实现思路:

  1. 硬件替换 :设计一块基于ESP32开发板(如ESP32 DevKitC)的PCB,集成硬币识别器接口、LCD I2C接口、按键接口和电机驱动电路。ESP32的GPIO同样支持外部中断。
  2. 数据上报 :通过ESP32的Wi-Fi模块,将每笔交易数据(时间、金额、商品编号)以HTTP POST或MQTT协议发送到云端服务器(如阿里云、腾讯云IoT平台,或自建的Node.js服务器)。
  3. 远程管理 :可以开发一个简单的Web管理页面或手机App,实时查看各个设备的销售数据、总金额,甚至远程修改商品价格、重启设备等。
  4. 无现金支付扩展 :可以进一步集成二维码扫描模块,支持微信/支付宝扫码支付。当云端服务器收到支付成功的回调后,再通过MQTT指令通知ESP32控制出货。

ESP32中断注意事项 :ESP32的中断配置函数与Arduino略有不同,使用 attachInterrupt(digitalPinToInterrupt(pin), ISR, mode) ,但核心思想完全一致。网络操作(如HTTP请求)应放在主循环中,避免在ISR内进行,以防阻塞。

5. 常见问题与排查技巧实录

在搭建和调试这套系统的过程中,我遇到了不少典型问题。下面这个排查表汇总了常见故障现象、可能原因及解决方法,希望能帮你少走弯路。

问题现象 可能原因 排查与解决方法
投入硬币无反应,金额不增加 1. 硬币识别器电源未接或电压不对。
2. 信号线未正确连接到Arduino D2。
3. 识别器未校准或硬币不被支持。
4. 中断未正确触发。
1. 用万用表检查识别器电源端电压是否为12V。
2. 检查杜邦线连接,确认信号线接在D2。
3. 查阅识别器手册,进行硬币学习/校准。投入一枚标准硬币,用示波器或逻辑分析仪探测信号线是否有脉冲输出。
4. 在 setup() 中初始化串口,在 incomingImpuls() 函数里加一句 Serial.println("INT!") ,看投币时串口是否有输出。
金额显示错误(如1元显示成0.5元) 脉冲数与币值的映射关系错误。 使用“3.1实操要点”中的方法,重新校准并确认每种硬币对应的脉冲数。修改代码中 if 判断条件里的 impulsCount 值。
偶尔漏计或多计硬币 1. 中断信号受到电磁干扰。
2. 主循环 delay() 时间或判断阈值 time 设置不当。
3. 硬币识别器机械卡顿。
1. 将信号线改用屏蔽线,或在Arduino信号输入引脚与GND之间加一个0.1uF的滤波电容。
2. 调整 delay() time 的判断阈值。可以尝试增大 delay() 或增大 time 的判定值(如从30调到40),给脉冲序列更充裕的结束时间。
3. 清洁识别器的硬币通道,确保硬币能顺畅滚落。
LCD屏幕不显示或显示乱码 1. I2C地址错误。
2. 接线错误(SDA, SCL接反)。
3. 对比度调节不当。
1. 使用I2C扫描程序(Arduino IDE示例中有)查找LCD模块的正确I2C地址(常见为0x27或0x3F),并修改代码中的地址 0x27
2. 确认SDA接A4,SCL接A5。
3. 调节LCD模块背面的电位器,改变对比度直到字符清晰。
按键操作不灵敏或无效 1. 按键引脚模式未设置为 INPUT_PULLUP
2. 按键消抖处理不足。
1. 在 setup() 中为按键引脚正确设置 pinMode(pin, INPUT_PULLUP)
2. 在主循环读取按键时,加入简单的软件消抖逻辑。例如,连续两次读取间隔20ms,如果状态一致才认为有效。
系统运行一段时间后死机 1. 中断服务程序(ISR)执行时间过长或进行了不可重入操作。
2. 电源功率不足,导致电压跌落。
1. 严格检查ISR函数,确保其极其简短,绝不使用 delay() ,谨慎使用 Serial.print() (可改为设置标志位,在主循环中打印)。
2. 检查电源适配器额定电流是否足够(需满足Arduino、LCD、识别器总和),必要时更换更大功率电源。

独家避坑技巧:

  • 中断引脚选择 :除了D2、D3,在Arduino UNO上,所有引脚都可以通过 pinChangeInterrupt 库实现类似中断的功能,但INT0/INT1是硬件中断,优先级和可靠性最高,应优先使用。
  • 共享变量处理 impulsCount time 这类在ISR和主循环中都会被修改的“共享变量”,在8位的AVR单片机(如Arduino UNO用的ATmega328P)中,对 int char 等单字节或双字节变量的简单读写操作是原子的(不可分割的),所以本例中直接使用是安全的。但如果是在32位处理器(如ESP32)上,或者操作更复杂的数据类型,就需要使用 volatile 关键字声明变量,或临时关闭中断来保护临界区代码。
  • 3D打印设计 :设计外壳时,务必为硬币识别器留出足够的空间用于拆卸和维护(如上盖可打开)。进币口的角度要反复模拟测试,确保硬币能依靠重力自然滑入识别器,不会卡住。可以在Fusion 360中使用“碰撞检测”功能来模拟硬币下落路径。

这个项目从想法到实现,最深的体会是:嵌入式开发是软硬件紧密结合的艺术。一个稳定可靠的系统,不仅需要逻辑清晰的代码,更需要考虑硬件的电气特性、机械结构的合理性以及环境干扰。当你看到投入一枚硬币,屏幕上的数字精准地跳动时,那种满足感是对所有调试工作的最好回报。如果你正在计划类似的项目,我建议先从最核心的“中断计数”功能调通,再一步步加上显示、按键和机械结构,这种分步验证的方法能有效降低调试复杂度。

Logo

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

更多推荐