码图T11是我最近一个项目调试记录的编号封面上就写了四个字“输入输出密码”。当时只是随手记等我把Python的print()/input()、DS18B20温度探头、PLC模拟量模块这三样东西全都串起来调试一遍之后才发现这四个字其实是整个项目最核心的浓缩不管你是纯写代码还是搞软硬件结合又或者是做自动化产线所有东西翻来覆去都在解决输入输出这一件事。这篇笔记就把我这一路拆过的输入输出“密码”完整整理出来适合正在学Python又打算接硬件的朋友也适合刚接触PLC想做模拟量采集的同行参考。1. 先想明白输入输出到底在干嘛1.1 程序的本质就是挤牙膏标准输入输出很多教程会把print()和input()当成Python入门第一课讲完就过去了。但在我看来这两个函数背后藏着的是程序的全部意义拿数据、算、交数据。拿数据是输入交数据是输出中间的“算”再厉害要是输入不可靠、输出没人接收这套程序就是废的。举个例子你在命令行里跑一个Python脚本脚本从键盘拿到的是输入往屏幕上打印的是输出。操作系统给每个进程配了三根“管子”标准输入stdin、标准输出stdout、标准错误stderr。你用的input()就是读stdinprint()就是写stdout。这个概念不是Python独有的C语言里的scanf/printf也一样。搞懂了这三根管子很多诡异问题就能想通了。我最开始接触输入输出时觉得这是最没技术含量的东西后来做数据处理脚本要把几万行日志喂给程序才意识到数据从哪来、送到哪去直接决定脚本架构。有人喜欢把stdin/out比喻成“挤牙膏”程序从外面挤一点数据进来处理完再挤出去。这个比喻虽然糙但很贴切因为挤牙膏的过程里有一个关键动作——你得知道牙膏管什么时候空、什么时候满这就是输入输出里的那点“密码”。1.2 阻塞、缓冲与握手IO的三个隐形成员输入输出之所以坑多是因为背后藏着三个看不见的家伙阻塞、缓冲、握手。阻塞最好理解。你写一行input()程序就卡在那里不走直到你回车。第一次写循环读输入的程序时我甚至怀疑是不是死循环了其实它是在等数据这叫阻塞式输入。阻塞本身不是问题问题是很多人不知道“这个输入会一直等下去”一旦上游设备没发数据程序就永远卡住。缓冲则是输出的坑。默认情况下print()往屏幕输出是“行缓冲”只要遇到换行符或者程序退出内容就刷出去。但如果你把输出重定向到文件它就变成“全缓冲”攒一大块才往文件里写一次。后果就是程序崩了你打开日志文件发现什么也没记上以为是程序没执行到其实内容还在缓冲区里没来得及落盘。握手这个词很多人只在网络协议里听过其实硬件输入输出里更常见。DS18B20测温前主机要先发一个复位脉冲器件回一个存在脉冲这就是握手两边先打招呼确认对面是谁、能不能通信再正式传数据。软件里的input()等待回车也约等于是在等用户握手。理解了握手再看后面18B20和PLC的输入输出思路会清晰很多。2. Python里的输入输出真没你想的那么简单2.1 标准输入的三种写法以及它们的脾气如果你平时只用input()读用户输入那对Python输入输出的理解还差一半。实际工作中数据更多来自文件、管道、串口这时候读stdin有几种不同姿势各有各的脾气。第一种是input()。它每次读一行自动去掉末尾换行符返回字符串遇到文件结尾会抛EOFError。很多人不知道这个异常结果写脚本批量处理文件内容时读到末尾直接崩。第二种是一次性读整个输入import sys data sys.stdin.read().strip().split() nums [float(x) for x in data]这种写法的好处是代码简单适合一次性处理完整数据的场景。坏处是如果输入流非常大一次性读进内存可能撑爆。它的脾气是保留行尾的\r\nWindows上传过来的文本经常带\rstrip()能去掉前后空白但如果中间混着\r就得用replace(\r, )。第三种是sys.stdin.buffer.read()直接读二进制bytes适合处理压缩流、序列化数据。我第一次用串口转USB接单片机时发过来的就是二进制帧用文本方式读全是乱码换成buffer就正常了。2.2 三个让我挠头的Python IO坑第一坑日志看不到。程序A调用程序BB往stdout打印了一堆调试信息但A里看不到等到B结束才一次性冒出来。原因是B的输出被重定向到管道后变成全缓冲。解药其实很简单print(..., flushTrue)强制刷新或者干脆用sys.stderr.write()标准错误默认不缓冲。第二坑编码问题。在Windows上跑Python读文本文件默认编码可能不是UTF-8中文直接报UnicodeDecodeError。别去跟编码死磕统一思路打开文件时显式指定编码open(data.txt, encodingutf-8)。二进制数据永远不要用str()去处理否则迟早出事。第三坑无法超时的input()。写一个监控程序要求5秒内没输入就继续走结果input()在那傻等十分钟。解决办法很多Linux下可以用select比如下面这样给stdin加超时import select import sys r, _, _ select.select([sys.stdin], [], [], 5) if r: line sys.stdin.readline().strip() print(got:, line) else: print(timeout, continue)这个方法在Windows上不大好用但思路值得记住输入输出不能让它无限等一定要设计超时和边界条件。3. 从print到一根数据线18B20的单总线密码3.1 先看硬件为什么18B20只有一根线还能干活看完软件输入输出再跳到硬件你会发现天差地别。我用DS18B20做了个温度采集点网上搜“18b20温控管输入输出图片”看着挺简单三只脚一根数据线接上就能读。真到自己焊的时候才发现我读回来的温度要么是85要么是乱码折腾半天才抓到元凶——少了一颗4.7kΩ的上拉电阻。DS18B20用的是单总线协议数据线DQ集成了信号传输和可能的供电。器件输出低电平时是主动拉低但输出高电平时它并不主动推高只是“放开”总线让上拉电阻把电平拉回高。所以没有上拉电阻总线就像没人管的孩子电平飘忽不定时序自然乱套。4.7kΩ电阻一头接3.3V或5V电源另一头接DQ线GND必须共地。这个细节十个新手九个栽在这。接线还有个经验线不要太长。单总线在几米内没问题但如果你图省事用了几十米网线寄生电容会把信号边沿搞缓读数据时好时坏。我自己后来做实验线长控制在1米以内稳定很多。3.2 时序里的输入输出写1、写0、读时隙软件输入输出靠函数硬件输入输出靠时序。DS18B20的“密码”就在时间参数表里。写时序的时候主机先把总线拉低表示“我要开始写了”。如果接下来要写逻辑1就在拉低后的15微秒内释放总线让上拉电阻把电平拉高保持至少60微秒如果要写逻辑0就继续把总线拉低至少60微秒。读时序类似主机拉低至少1微秒然后释放器件如果想让主机读到0就会主动把总线拉低如果读到1总线保持高。关键是主机必须在这之后15微秒内采样晚了信号就没了。用Python写这种微秒级时序其实有点悬树莓派上可以这样快速读一位import time def read_bit(): set_gpio(0) time.sleep(0.000002) set_gpio(1) time.sleep(0.000010) bit read_gpio() time.sleep(0.000050) return bit实际跑起来性能可能不太稳所以我建议你逻辑分析仪伺候。我抓过一次波形之后所有输入输出密码全部“可视化”了比看文字参数直观太多。3.3 为什么读到的温度会乱跳读DS18B20的标准流程是复位、发跳过ROM或者匹配ROM命令、发转换温度命令、等待、再复位、发读暂存器命令、连续读9个字节。第一次我只用“跳过ROM”命令总线上只有一个探头时完全没问题后来并了第二个探头温度开始互相串我才补上“搜索ROM地址、用匹配ROM命令逐个读”的步骤。多个器件共用一条总线时必须要先搞清楚谁是谁的地址这是输入输出地址分配的问题。另外一个容易踩的坑是CRC校验。DS18B20读回来的9个字节里最后一个字节是前8个字节的CRC校验。我一开始偷懒不算CRC结果偶尔读到0xFF这种异常值还当成真温度。后来老老实实加上CRC校验凡是校验不过的数据直接丢弃温度就再没出现过离谱值。4. PLC自带的模拟量输入输出和print根本不是一回事4.1 模拟量接线两线制、三线制、四线制别接错从单片机跳到PLC输入输出又多了一层模拟量。热搜词里“plc自带模拟量输入输出”说的就是PLC本体或者扩展模块上常见的AI/AO通道。数字量输入输出只有0和1模拟量则是连续数值比如4-20mA电流、0-10V电压。这一层最坑的不是编程而是接线。常见的变送器有三种接法。两线制最简单两根线既供电又传信号电源正极串进回路信号电流从传感器流出回到PLC输入端再由PLC的公共端回电源负极。三线制多一根线电源正、信号正、公共负。四线制电源和信号完全独立两组线各走各的。这三种要是接错了读数要么一直是最大值要么干脆没有甚至可能烧通道。另外千万注意共地。模拟量模块上的M端或者COM端一定要和传感器供电电源的负极连在一起。否则传感器自以为信号发出来了PLC这边却因为参考电位不一致读到的值忽大忽小你调半天程序也没用。4.2 码值换算把27648这样的原始值翻译成温度PLC的模拟量通道会把电流或电压转换成整数码值。不同品牌习惯不一样有的PLC把0-10V对应成0-4000有的对应0-27648还有的对应0-32767。我的经验是不要背这个数去看手册或者PLC硬件组态里的说明。组态里选了0-10V量程后正常输入时原始值范围就固定了。换算公式记住这一个就够了工程量 (原始值 - 原始值下限) / (原始值上限 - 原始值下限) × (工程量上限 - 工程量下限) 工程量下限比如一个温度变送器量程0-100℃组态量程0-10V对应原始值0-27648那么读取到原始值13824时温度就是50℃。用LAD或者SCL写的时候一定要注意把整数转成REAL算不然PLC的整数除法会直接把小数砍掉你算出来的温度永远是整十整五精度全丢。更要命的是断线。很多PLC模拟量模块在信号断开时原始值会跳到-32768或者满码值你要是没做限幅换算出来的温度直接是负一百多度。成熟的程序都会对原始值做上下限检查超出合理范围就报警而不是照常换算。4.3 模拟量输出的执行器细节模拟量输出也有密码。AO模块把程序里的工程量换算回电流或者电压输出比如控制变频器转速的0-10V、控制调节阀开度的4-20mA。我调试时习惯先用万用表直接量模块输出端确认信号是对的再接执行器直接接上执行器再排查分不清是模块坏了还是阀门卡了。还有一个容易忽略的点很多电压输出型仪表需要并联一个负载电阻才能正常工作纯空载测量时电压看起来对一接到实际仪表上就被拉低。另外模拟量输出千万不要用万用表电流档直接去量电压输出那不是量信号是短路。5. 一套通用的输入输出排查心法5.1 按数据流二分法定位软件和硬件输入输出问题我现在的排查思路高度统一把整条链路切开从中间找。输入输出链路一般长这样信号源、接线、传感器/模块、协议转换、传输介质、程序读取、数据处理、最终输出。出了问题先别猜先确定“数据到底走到哪一步断了”。比如Python脚本读串口没数据先看串口监视器里硬件有没有数据进来有问题在软件读取和解析没有问题在接线和设备配置。又比如PLC模拟量温度不对先看组态里的原始值对不对原始值不对查接线和变送器原始值对那就查换算和滤波。这个习惯能省掉大量瞎折腾的时间。5.2 常见问题速查表我把这段时间遇到的输入输出问题整理成了一张表遇到问题直接按表排查症状可能原因处理办法Python的print没有输出输出重定向后全缓冲加flushTrue或用stderrinput()一直卡住上游没有数据且阻塞无超时用select加超时或改读文件流读取文本乱码编码不匹配打开文件时显式指定UTF-818B20读出来全是85℃上电时序不对或没有上拉电阻检查4.7kΩ上拉和复位时序多路18B20数据串扰跳过ROM导致地址混淆先搜索ROM再匹配读取PLC模拟量显示-32768传感器断线或量程未组态检查接线和通道组态加断线判断PLC模拟量波动大接线过长、未滤波或接地不良屏蔽线单端接地程序里加滤波模拟量输出接上负载就变电压型输出负载能力不足确认负载阻抗满足要求5.3 我的通用调试流程先做最小循环最后分享一个让输入输出调试少走弯路的小流程。不管是Python还是PLC拿到一个输入输出任务我第一件事不是写完整业务而是先做一个“最小循环”程序把读到的原始数据直接原样输出先证明链路是通的。Python这边就是读一行打印一行PLC这边就是把模拟量原始值直接映射到一个HMI显示控件上。等这个最小循环跑通、能看到数据了再去写滤波、换算、报警那些业务逻辑。这么做最大的好处是把“数据链路问题”和“业务逻辑问题”彻底分开。链路通没通都不确定的时候你写再多处理逻辑最后都不知道该信谁。我见过太多人一上来就写完整采集程序最后数据不对一会儿怀疑这个传感器坏了一会儿怀疑算法错了实际查了半天发现接线松了。我后来给自己定了个规矩任何输入输出开发先花十分钟做最小循环链路通不了其他都是白搭。这个习惯帮我避开了无数莫名其妙的坑也算是我在码图T11这个项目里悟出的最重要的一条经验吧。