ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

自动售货机MDB调试实战:从VMC轮询到纸币器通信的完整指南

自动售货机MDB调试实战:从VMC轮询到纸币器通信的完整指南 简介自动售货机内部各模块通常通过MDB总线协同工作纸币器正是其中最关键的交互设备之一。压缩包提供了一套完整的VMC与纸币器通讯代码骨架面向嵌入式开发、物联网及支付设备学习者帮助他们理解自动售货机在投币验证、面额识别和异常处理环节的底层通讯逻辑。包内共2个文件含1个.h头文件和1个.c源文件整体仅4KB代码量精简却涵盖MDB总线初始化、命令构造、响应读取等核心接口便于快速研读。头文件中的接口声明与源文件实现一一对应读者既能学习MDB帧格式与状态机设计也能参考多币种识别、故障检测和总线时序控制的具体写法。压缩包目前已有133人学习下载对想要快速掌握工业总线通讯原理的开发者、嵌入式爱好者或自动售货机相关从业者来说是一份高效、轻量的入门参考方案。1. VMC.zip 拆开之后MDB 才是自动售货机调试的真正门槛拿到一个名为 VMC.zip 的压缩包里面的目录多半是 VMC_mdb_vending 之类的命名暗示这是一个自动售货机控制器Vending Machine Controller简称 VMC的 MDB 通信工程或调试记录。真正接触过售货机行业的工程师都清楚VMC 主板本身并不神秘难点在 MDBMulti-Drop Bus总线上——如何让 VMC 和纸币器、硬币器、找零器等外设说上话并在几百毫秒内完成一次完整的交易应答。这篇内容从 MDB 协议讲起直接落地到用串口工具抓总线报文、用 Python 模拟 VMC 轮询、以及排查纸币器不收款这类高频故障。适合刚接手售货机项目、被 MDB 时序折磨过的嵌入式工程师和售后调试人员也适合想理解 VMC 外设通信逻辑的后端开发在介入物联网设备时快速建立真实感。2. MDB 协议原理从 VMC 角度理解总线时序和帧格式2.1 为什么 VMC 必须主动轮询而不是纸币器主动上报MDB 总线是主从结构VMC 作为唯一主机纸币器、硬币器、读卡器等外设都是从机。总线上所有通信都由 VMC 发起外设不能主动说话。这个设计和 RS-485 类似但 MDB 的时序要求严格得多VMC 发出一个命令字节后必须在严格的超时窗口内收到从机响应否则本次通信直接判定失败。从机地址由硬件拨码开关设定常见纸币器的地址是 0x30二元纸币器地址段硬币器是 0x08找零器是 0x60。VMC 在启动时会按地址逐个发送轮询命令Poll命令字 0x33纸币器收到后必须应答。2.2 帧格式和字节级时序的具体拆解MDB 的串口参数是 9600bps、9 位数据位、1 位停止位、无校验。注意是 9 位数据位不是常见 PC 串口的 8 位。第 9 位是模式位置 1 表示该字节是地址或命令字节置 0 表示数据字节。这个设计和某些银行终端或工业仪表类似但 MDB 的帧还要加上长度字段和校验字段。标准 MDB 帧格式如下字段长度说明地址/命令1 字节模式位置 1如 0x30 指向纸币器0x33 是轮询命令数据长度1 字节后续数据字节数不含校验字段数据N 字节模式位均为 0具体内容依命令而定校验和1 字节从地址字节到最后一个数据字节的累加和只取低 8 位VMC 发送时地址/命令字节必须把第 9 位置 1。外设只有在收到模式位为 1 的字节时才会确认这是给自己的命令。这个细节非常关键很多自制的 MDB 调试板就是在这里翻车因为普通 USB 转串口芯片根本不支持 9 位模式而 MDB 调试器必须能手动控制第 9 位。2.3 MDB 电气层不是普通 TTL别直接接单片机引脚MDB 总线的电气标准是电流环模式VMC 侧必须用专门的 MDB 从站接口芯片或分立电路转换。常见做法是用一个类似 MDB-01 的专用接口板或者用光耦加三极管实现电流环转 UART。如果你只是做调试抓包更省事的方案是直接在总线上并接一个逻辑分析仪采电平波形再用软件解码而不是让调试器真的参与通信。提示纸币器的 MDB 输入口是 20mA 电流环和 RS-232、TTL 完全不兼容直接把 VMC 的 TX/RX 接到纸币器上大概率烧接口或完全无响应。3. 用 Python 实现 MDB 抓包与命令发送的完整流程3.1 软硬件准备便宜可靠的 MDB 调试器方案要抓 MDB 总线上的数据硬件层面有两种选择。第一种是市售成品 MDB 调试器像 USA Technologies 或 Crane 的调试工具价格高但对协议支持完整。第二种是自己做一个串口监听器用一个 MDB 电平转换小板把总线信号转成 TTL再接到 USB 转串口模块上用 Python 脚本读串口数据然后把原始字节流按 MDB 帧格式解析。后者成本不到五十块钱关键是电平转换小板必须支持双向或至少监听方向的数据透传。实际接法是用光耦隔离并联在 MDB 总线的 MASTER 发送线上这样不会挂载到总线上影响通信只负责把电平变化同步输出到串口。串口设置成 9600, 8 位数据位、1 位停止位、无校验模式位会丢但模式位的规律可事后推断凡是紧跟帧头出现的字节大概率是命令/地址因为模式位为 1 的字节通常出现在帧的起始位置。3.2 直接用 pyserial 写一个最小监听脚本以下是监听 MDB 总线的基础代码import serial import time from collections import deque SERIAL_PORT /dev/ttyUSB0 BAUDRATE 9600 BYTESIZE serial.EIGHTBITS PARITY serial.PARITY_NONE STOPBITS serial.STOPBITS_ONE def decode_mdb_frame(raw: bytes) - dict: if len(raw) 3: return {raw: raw.hex(), error: too short} addr raw[0] length raw[1] if len(raw) 2 length 1: return {raw: raw.hex(), error: incomplete frame} data raw[2 : 2 length] checksum raw[2 length] calc (addr length sum(data)) 0xFF return { addr: hex(addr), data: data.hex(), checksum: hex(checksum), match: calc checksum, } ser serial.Serial(SERIAL_PORT, BAUDRATE, parityPARITY, bytesizeBYTESIZE, stopbitsSTOPBITS, timeout0.5) buffer deque(maxlen64) try: while True: byte ser.read(1) if byte: buffer.append(byte[0]) # 假设帧起始位是地址字节尝试在滑动窗口中解析 if buffer[0] in (0x30, 0x08, 0x60, 0x33, 0x34, 0x35, 0x36): result decode_mdb_frame(bytes(buffer)) if error not in result and result[match]: print(time.strftime(%H:%M:%S), result) buffer.clear() else: # 窗口不匹配就滑动丢弃头字节 buffer.popleft() else: time.sleep(0.001) finally: ser.close()这段代码的核心逻辑是维持一个滑动窗口模拟 MDB 帧边界。当窗口头部字节命中常见命令值0x30 纸币器地址、0x08 硬币器地址、0x33 轮询命令时按长度字段向后解析并校验累加和。如果校验失败就丢掉第一个字节继续滑动。注意这里串口用的是 8 位模式和 0.5 秒超时这是为了兼容 PC 串口芯片的硬件限制牺牲模式位换取可操作性。如果你手里的串口芯片支持 9 位字符长度把 BYTESIZE 换成 serial.NINEBITS 并做对应处理即可。参数说明buffer用deque(maxlen64)做窗口避免无限增长timeout0.5防止读不到数据时死等decode_mdb_frame里length字段对应 MDB 帧的数据长度字节解析为字典后方便二次处理。实际运行时你会看到不间断的0x33轮询和对应的0x00/0xFF应答这就是最小可用的总线健康观测。3.3 从原始字节到交易事件的解析思路抓回来的数据如果只是十六进制字节序列人肉看不过来。实际调试时我通常把解析逻辑分成两层底层把字节流切帧上层把帧序列归类为VMC 发起的事件和外设的应答事件。整理成 CSV 或 JSON 再丢给数据处理工具分析。event_log [] def record_event(raw_frame: dict, direction: str): event_log.append({ time: time.time(), direction: direction, frame: raw_frame, })这里direction参数标明该帧是 VMC 发出还是外设返回依据是时间先后和帧头命令类型。VMC 发出轮询后紧接着收到从机应答这两帧之间存在固定的时间窗可以按recv与send逻辑自动标注。日志连续跑 5 分钟后统计各地址设备的响应率基本就能判断某外设是否离线。4. 纸币器接入 VMC 的实战轮询、Bills 状态机与错误码4.1 纸币器命令集速查表纸币器是 MDB 总线里最常用的外设之一其命令集相对固定。以下是我常用的几张关键表命令字节方向含义0x33VMC → 纸币器轮询纸币器有空闲、纸币已接受、返回中、故障等状态位返回0x34VMC → 纸币器BILLS返回纸币器在线状态和支持的纸币面额信息0x35VMC → 纸币器允许/禁止接收纸币后跟控制字节0x36VMC → 纸币器外部返回/找零命令纸币器把暂存纸币返还给顾客0x37VMC → 纸币器找零发配命令纸币器预留某面额的找零0x38VMC → 纸币器从纸币器读取累计交易计数0x39VMC → 纸币器扩展命令段0x40VMC → 纸币器状态查询0x41VMC → 纸币器同步/复位指令0x42VMC → 纸币器通信测试返回设备ID4.2 轮询应答的位含义解析纸币器对轮询命令0x33的应答通常是一个状态字逐位拆分可得以下信息位含义D0纸币器是否正忙例如正在接收纸币D1卡钞错误D2纸币器内部故障D3纸币已放入闸口但存在争议正在等待 VMC 决策D4纸币器处于禁止接收状态D5传感器故障D6纸币器正尝试识别纸币D7纸币器正在复位或自检应答数据会紧跟在一个状态字节后有的实现会带长度和校验字段。务必对照纸币器型号的数据手册确认位数含义不同厂家的位映射有可能不一样尤其是国产四通、晓星和进口 MEI 设备之间差异不小。轮询应答里 D3 位置 1 时需要 VMC 立即做出接受或退回的决策否则纸币器会超时把纸币退到退币口这个流程在调试中一旦失误顾客会以为机器吞钱。4.3 用 Python 模拟 VMC 的纸币器接入逻辑这里给一个最小可运行的状态机实现用来跑通纸币器等待纸币 → 识别 → 暂存 → VMC 确认 → 入钞箱的完整流程import serial import time class BillAcceptor: STATE_IDLE 0x00 STATE_ESCROW 0x03 STATE_ACCEPTING 0x04 def __init__(self, port: str): self.ser serial.Serial(port, 9600, timeout0.2) self.state self.STATE_IDLE self.bill_value 0 def poll(self) - bytes: self.ser.write(bytes([0x30, 0x00, 0x00, 0x30])) time.sleep(0.05) response self.ser.read(16) return response def enable(self, enable: bool) - None: if enable: self.ser.write(bytes([0x35, 0x01, 0x01, 0x37])) else: self.ser.write(bytes([0x35, 0x00, 0x00, 0x35])) def accept_bill(self) - None: self.ser.write(bytes([0x36, 0x00, 0x00, 0x36])) def return_bill(self) - None: self.ser.write(bytes([0x37, 0x00, 0x00, 0x37])) def loop(self): self.enable(True) while True: resp self.poll() if len(resp) 6: status resp[2] if status 0x08: self.state self.STATE_ESCROW self.accept_bill() elif len(resp) 0: print(no response from bill acceptor) time.sleep(0.1)代码里ser.write发送的是固定长度帧地址0x30、长度0x00、数据段空、校验值0x30。实际纸币器接收的帧格式和具体校验计算方式要查阅对应协议手册我这里的bytes([0x35, 0x01, 0x01, 0x37])中0x37是累加和即0x35 0x01 0x01的结果。poll()里也做了同样的累加和预计算。注意 16 字节的读取上限问题如果实际应答超过 16 字节这里会截断建议根据已知协议把上限设到 32 字节。状态机的核心在处理status 0x08的暂存位。一旦纸币器确认纸币在闸口并等待决策VMC 必须立即调accept_bill()或return_bill()二选一。如果省略这一步纸币器会尝试自行处理纸币导致用户体验崩溃或钱箱计数与实际不符。循环结束条件通常用一个退出标志位或者捕获 KeyboardInterrupt这是最简单的可运行版本。4.4 常见错误码与排查表轮询应答错误码含义排查方向0x00无应答检查 20mA 电流环电源、地址拨码、总线接线0x01初始化失败上电时序VMC 必须在纸币器上电 200ms 后再发命令0x02传感器异常清理纸币通道灰尘检查传感器排线0x04电机故障拆开看传动皮带是否脱落0x08暂存位异常纸币卡在闸口等待 VMC 决策时超时以上错误码中有些位与传输错误相关有些与机械结构相关不要一上来就换主板。先抓总线上 VMC 发给纸币器的使能命令和轮询命令是否正常到达再用万用表测 MDB 总线空闲时是否有 3.6V 左右的稳定的电压电平。多数不收款案例最后都查出来是使能命令里的控制字节写错比如把enable和disable写反。5. 验证 MDB 通信是否健康的三个快速手段与高级调试技巧5.1 监听 VMC 是否在正确的时间窗口内收到应答MDB 协议规定从机必须在 5ms 到 100ms 内完成对主机命令的应答。超出 100ms 的应答在多数 VMC 实现里会被当作超时丢弃但不影响总线上其他通信。这个延迟阈值可以用逻辑分析仪直接测量总线电平和字节间间隔精度远高于串口抓包。测量时重点看0x33命令发出到首个应答字节起始位的间隔。一个更省事的方法是跑一轮旧的整机测试固件用脚本记录 VMC 发送轮询到收到应答的间隔python3 mdb_ttl_measure.py --port /dev/ttyUSB0 --command 0x33 --expect-addr 0x30 --interval-ms这个脚本会输出每一轮轮询的应答耗时如果均值超过 60ms 就要怀疑总线负载电容过大、从机响应慢或者 VMC 自身的调度出了问题。5.2 用 CRC 错误计数判断是软件问题还是硬件干扰在抓包脚本里加一个简单的 CRC 错误计数器用它区分硬件干扰和协议逻辑错误。如果某一帧的校验和不对先看同一秒内错误帧是否扎堆。扎堆出现多半是 VMC 或纸币器电源纹波太大导致出错的位集中在供电瞬间零星错误则要考虑波特率偏差比如 VMC 实际晶振偏差超过 2% 就可能导致边缘误码。5.3 一个容易被忽略的启动顺序VMC 与纸币器的上电时序很多 MDB 总线问题出在 VMC 和纸币器共用一个电源但纸币器的电流环电路上电慢于 VMC 的串口初始化。针对这个最直接的手段是给 VMC 的 MDB 发送逻辑加 500ms 的上电延迟确保纸币器完成自检后再发0x35使能命令。如果你手里的整机系统支持延时启动直接在系统启动脚本里写一条sleep 0.6 echo 0x35 enable | /usr/bin/mdb_cli不同 VMC 主板的命令行工具可能有差异这里只演示延迟 600ms 后发使能命令的思路。假如纸币器仍然无响应先别急着怀疑协议拿示波器看它的电源引脚是否在上电瞬间跌破 4.5V这会触发纸币器内部看门狗反复复位从 MDB 侧看就是永久离线。这类问题在 12V/24V 转换电源余量不足的老机器上特别常见处理方式是把 VMC 和纸币器的电源分路或者加大输入电容。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进