第二章:日志数据采集——串口日志采集原理
好,咱们进入第二章。这一章我打算聊聊日志采集中最基础、也最绕不开的一环——串口日志采集。
你想想看,嵌入式设备刚上电那会儿,网络还没起来,文件系统还没挂载,唯一能跟外界“说话”的通道,往往就是那根串口线。我做了这么多年嵌入式,调试阶段90%的问题,都是靠串口日志定位的。所以,怎么把这根线上的数据稳稳当当地收上来,是咱们整个日志分析系统的地基。
2.1 串口通信的基本原理
串口通信,说白了就是一根线发、一根线收,再加上地线,就这么简单。数据是一位一位地传,不像并口那样一次传一堆。但你别小看它,稳定可靠是它的最大优点。
串口参数就那么几个:波特率、数据位、停止位、校验位。我见过不少新手,上来就栽在波特率不匹配上。两边都说自己是115200,结果收出来全是乱码。嗯,这里要注意,有些开发板的晶振不准,实际波特率会有偏差。
串口关键参数速查表:
| 参数 | 常见值 | 说明 |
|---|---|---|
| 波特率 | 9600, 115200, 921600 | 每秒传输的符号数,越高越快 |
| 数据位 | 8(最常用) | 一帧数据中有效数据的位数 |
| 停止位 | 1, 2 | 帧结束标志,1位足够 |
| 校验位 | None, Even, Odd | 奇偶校验,调试阶段一般不用 |
我个人习惯,调试阶段一律用8数据位、1停止位、无校验。省心,兼容性最好。等产品定型了,再根据需求加校验。
2.2 使用Python的pyserial库采集串口数据
Python里操作串口,pyserial是事实上的标准库。我当年第一次用它,是在一个工控项目里,设备24小时不停机,日志哗哗地往外吐。那时候我就意识到,光会打开串口读数据远远不够,还得考虑怎么读得稳、读得快。
先看一个最基础的例子:
import serial
# 打开串口
ser = serial.Serial(
port='COM3', # Windows下是COM口,Linux下是/dev/ttyUSB0
baudrate=115200,
bytesize=8,
parity='N',
stopbits=1,
timeout=1 # 读超时,单位秒
)
# 读一行数据
line = ser.readline()
print(line.decode('utf-8', errors='ignore'))
ser.close()
这段代码能跑,但别高兴太早。我在项目中遇到过一个问题:设备日志里偶尔会混进一些二进制数据,直接decode('utf-8')会抛异常。所以errors='ignore'这个参数,我建议你养成习惯加上。
小技巧: 如果你不确定串口设备用的是哪种编码,可以先用 ser.read(1024) 读一包原始字节,然后打印出来看看。我遇到过用GBK编码的国产设备,也遇到过纯ASCII的老设备。先看再解码,稳得很。
实际项目中,我们往往需要持续采集。这时候就要用循环了:
import serial
import time
ser = serial.Serial('COM3', 115200, timeout=0.1)
try:
while True:
if ser.in_waiting > 0:
data = ser.read(ser.in_waiting)
# 这里可以加你的处理逻辑
print(data.hex()) # 先以十六进制打印,方便调试
time.sleep(0.01) # 别让CPU空转
except KeyboardInterrupt:
print("采集停止")
finally:
ser.close()
为什么用time.sleep(0.01)?说白了就是给CPU喘口气。如果不加,这个循环会吃掉一个核的全部资源。你想想看,嵌入式设备本来资源就紧张,采集程序再这么搞,其他任务就别想跑了。
2.3 日志数据缓存策略
好,数据能收了。但下一个问题来了:数据收上来放哪儿?
我曾经在一个项目中,设备每秒吐200条日志,每条大概200字节。如果不做缓存,直接写文件,你会发现磁盘I/O成了瓶颈。更糟糕的是,如果这时候程序崩溃,最后几秒的日志全丢了。
缓存策略,我一般分三层:
- 应用层缓存——Python里的队列
- 文件层缓存——内存映射文件或缓冲区写入
- 持久化存储——最终落盘
先看应用层缓存。我推荐用queue.Queue,它是线程安全的,生产者和消费者模式用起来特别顺手:
import queue
import threading
import serial
# 全局缓存队列
log_queue = queue.Queue(maxsize=10000)
def collector(port):
"""采集线程:只管往队列里塞数据"""
ser = serial.Serial(port, 115200, timeout=0.1)
while True:
if ser.in_waiting > 0:
data = ser.read(ser.in_waiting)
log_queue.put(data)
threading.Event().wait(0.01)
def writer():
"""写入线程:从队列里取数据,批量写入文件"""
buffer = []
while True:
try:
data = log_queue.get(timeout=1)
buffer.append(data)
if len(buffer) >= 100: # 攒够100条再写
with open('log.bin', 'ab') as f:
for item in buffer:
f.write(item)
buffer.clear()
except queue.Empty:
# 超时了,把剩下的也写进去
if buffer:
with open('log.bin', 'ab') as f:
for item in buffer:
f.write(item)
buffer.clear()
注意: 队列的maxsize一定要设。如果不设,内存会被撑爆。我见过一个同事,没设上限,设备跑了三天,队列里攒了上千万条数据,直接把服务器内存干到99%。
文件层缓存,我建议用内存映射文件(mmap)。特别是日志量特别大的时候,mmap比普通write快一个数量级。不过这个有点进阶,咱们后面章节再细讲。
最后说说持久化策略。我的经验是:
- 按时间分片:每小时一个文件,或者每天一个文件。别把所有日志塞到一个文件里,后期查起来想死的心都有。
- 写满就换:单个文件超过100MB就自动切分。这个大小是我个人习惯,你可以根据磁盘空间调整。
- 压缩归档:超过7天的日志,自动压缩成gzip。我一般用Python的gzip模块,写的时候直接写压缩流,省事。
避坑指南: 我曾经在一个项目里,日志文件写满了磁盘,导致系统直接挂掉。从那以后,我每次都会在采集程序里加一个磁盘空间检查。低于500MB就发告警,低于100MB就自动删除最旧的日志文件。别指望运维人员会盯着磁盘,程序自己得学会保命。
嗯,这一章的内容差不多就这些。串口采集看起来简单,但要做好、做稳,还是有不少门道的。下一章咱们聊聊日志解析,那才是真正考验功力的地方。