news 2026/9/26 3:31:05

Ubuntu低配CPU部署YOLOv8:C++与onnxruntime推理实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Ubuntu低配CPU部署YOLOv8:C++与onnxruntime推理实践

简介:在Ubuntu系统下需用C++完成YOLOv8模型部署的开发者,可借助这套包含完整源码与说明文档的资源,实现基于onnxruntime和OpenCV的模型加载、推理与输出解析,尤其适合低配置机器上的深度学习应用体验。压缩包共363个文件,以hpp/h头文件、cpp源文件、jpg/bmp测试图片、txt配置文件、names类别文件、onnx/pt模型文件及动态链接库为主,整体约120.57MB;目录按bin、src、include、lib、models等划分,层级清晰。目前已有161人学习下载。资源内附带onnxruntime相关库、yolov8n模型和密封圈缺陷识别示例,涵盖配置文件中标准尺寸、类别数、缺陷阈值与重叠阈值的调整方法,可帮助开发者掌握模型转换后的部署全流程,减少环境配置和推理解析阶段的踩坑成本。

1. 低配置 Ubuntu 机器上跑 YOLOv8:为什么我最终选了 C++ + onnxruntime

手里的办公机只有 8GB 内存,CPU 是四年前的 i5,没有独立显卡,装的是 Ubuntu 20.04。就这样的配置,把 YOLOv8 训练好的 onnx 模型用 Python 端 onnxruntime 跑,单帧推理稳定在 1.2 秒左右;换成 C++ 工具链,把预处理、onnxruntime 会话、后处理全部串起来之后,同一台机器直接降到 90 毫秒上下。这个差距不是玄学,Python 每帧都在处理 ndarray 拷贝、GIL 调度和动态库调用开销,C++ 把这些都省掉了。

这里拆的这套资源,就是一套在 Ubuntu 系统下用 C++ 和 onnxruntime、OpenCV 完成 YOLOv8 onnx 模型推理、解析和部署的完整源码加说明文档。它解决的核心问题是:低配置机器想体验深度学习推理,不想被 Python 推理速度劝退。适合两类人,一类是刚在 Ubuntu 20.04 上搭好 YOLOv8 CPU 环境、准备往 C++ 落地的同学;另一类是用 Python 已经调通,但卡在输出解析、坐标映射和库链接上的从业者。

整套源码的难点不在“调用模型”本身,而在三件事:onnx 原始输出怎么解析、letterbox 坐标怎么映射回原图、onnxruntime 在 Ubuntu 下怎么正确链接。下面按实际工程顺序来拆,每一步都可以直接对着抄。

2. 把 best.pt 转成可部署的 onnx:CPU 推理前必须确认的三个细节

下载这套源码之前,先要把模型准备好。绝大多数人的训练流程是跑 Ultralytics 的 yolo 命令,得到 runs/detect/train/weights/best.pt,然后导出成 onnx。导出这一步如果参数随意,后面 C++ 端会反复出现 shape 对不上、输出解析错乱的问题。

2.1 导出命令:固定输入尺寸、关闭动态轴、选对 opset

资源里的说明文档推荐的是下面这条命令,我实际用过很多次,参数可以直接照抄:

yolo export model=runs/detect/train/weights/best.pt format=onnx \ imgsz=640 opset=12 dynamic=False simplify=True

这里几个参数不要只复制,要理解为什么这么选。imgsz=640把模型输入固定成 640x640,后续 C++ 端预处理也必须 resize 到 640x640。如果你训练时用的是 1280,导出的 imgsz 要同步改成 1280,不能随意。dynamic=False会让输入和输出 shape 完全固定,C++ 端就不用处理动态 batch 或动态宽高,省掉大量边界判断。opset=12在 onnxruntime 1.x 上兼容性最好,CPU 部署不需要追求高 opset,onnxruntime 版本老一点也能跑。simplify=True会调用 onnx-simplifier 清理计算图中的冗余节点,C++ 端解析图结构更稳。如果 simplify 之后输出的检测结果不对劲,再把它关掉重新导出。

这里有一个非常容易翻车的点:如果你训练的是自定义数据集,比如检测 5 类零件而不是 COCO 80 类,导出后的输出通道数就不是 84,而是 4+5=9。后面所有 C++ 后处理代码里的类别数都要跟着改。不要拿着 COCO 预训练模型导出的 84 通道去解析自己的模型,结果全是错的。

2.2 导出核对脚本:先用 Python 看清楚 shape 再写 C++

导出完成后先不要急着写 C++,在 Ubuntu 上跑一段 Python 脚本,把模型的输入输出 shape 和数值范围打印出来。这个脚本保留好,后面 C++ 端出问题时拿它做对拍。

import onnxruntime as ort import numpy as np sess = ort.InferenceSession("best.onnx", providers=["CPUExecutionProvider"]) for inp in sess.get_inputs(): print("input:", inp.name, inp.shape, inp.type) for out in sess.get_outputs(): print("output:", out.name, out.shape, out.type) # 用一个全 0.5 的假输入确认输出不是 NaN x = np.full((1, 3, 640, 640), 0.5, dtype=np.float32) y = sess.run(None, {sess.get_inputs()[0].name: x})[0] print("output shape:", y.shape) print("min/max:", float(y.min()), float(y.max()))

正常输出是output0,shape 是1x84x8400。84 对应 4 个框坐标加 80 个 COCO 类别分数,8400 是三个检测尺度的 anchor 总数:640/8 的平方 6400,加上 640/16 的平方 1600,加上 640/32 的平方 400,合计 8400。如果模型的输出是1x8400x84,说明顺序是通道在后,后面 C++ 解析代码要换一种读取方式。

如果你用的是不同版本的 ultralytics 导出,输出名可能不叫 output0,而是onnx::Concat_419这种名字。以实际打印为准,不要照网上任意一份代码硬抄。这个核对脚本建议一直留着,后面 C++ 输出不对时,用它和 C++ 结果做对拍,很快就能定位是模型坏了还是解析写错了。

2.3 只有 onnx 没有 best.pt:如何反推模型输入输出

有些场景下拿到的只有一份别人导出的 onnx,没有训练权重。这时候不要干瞪眼,用 onnx 库直接读计算图的 meta 信息:

import onnx m = onnx.load("best.onnx") for inp in m.graph.input: dims = [d.dim_value for d in inp.type.tensor_type.shape.dim] print("input:", inp.name, dims) for out in m.graph.output: dims = [d.dim_value for d in out.type.tensor_type.shape.dim] print("output:", out.name, dims)

打印出来的输入名和 shape 是后续 C++ 端动态获取输入参数的依据。我的习惯是 C++ 代码里不写死images这个字符串,而是直接用session.GetInputName(0)取第一个输入名,这样换模型不用改代码。如果打印出来的输入是1x3xNxN这种动态 shape,说明导出时的 dynamic 没有关,最省事的做法还是让有训练权重的同事重新导一份固定 shape 的 onnx。

3. Ubuntu 下安装库与 CMake:onnxruntime 与 OpenCV 版本匹配的实操记录

模型的 onnx 准备好后,就该在 Ubuntu 上搭 C++ 编译环境了。这一章踩坑率非常高,很多人的 C++ 代码写得没问题,最后全挂在库没配对、链接参数不对、运行时找不到 so 文件上。

3.1 apt 装 OpenCV,解压 onnxruntime,别从源码编译

在这套资源里,OpenCV 负责读图、缩放、画框这些图像操作,onnxruntime 只负责模型计算,两者各自独立。Ubuntu 上安装 OpenCV 最省钱的是用 apt:

sudo apt update sudo apt install -y libopencv-dev pkg-config --modversion opencv4

Ubuntu 20.04 默认装的是 OpenCV 4.2,22.04 会更新一些,对于 YOLOv8 推理来说完全够用。低配置机器没必要从源码编译 OpenCV,除非你要用 OpenCV 的 CUDA 模块——但那样的机器也不会是低配。onnxruntime 同样不建议源码编译,直接下载官方发布的 Linux x64 CPU 版本压缩包,解压后使用。我用的版本是 1.16.3,Ubuntu 20.04 的 gcc 9 兼容性没问题。

# 假设你已经下载了 onnxruntime-linux-x64-1.16.3.tgz cd ~/downloads tar -xzf onnxruntime-linux-x64-1.16.3.tgz sudo mkdir -p /opt/onnxruntime sudo cp -r onnxruntime-linux-x64-1.16.3/include /opt/onnxruntime/ sudo cp -r onnxruntime-linux-x64-1.16.3/lib /opt/onnxruntime/

源码编译 onnxruntime 在低配机器上可能要等半小时以上,中途还容易因为内存不足被 kill。除非你要改算子或者集成自定义 OP,否则不要碰这条路线。另外注意,如果你在 Ubuntu 上用 Python 装过 onnxruntime,那只是 Python 扩展,C++ 编译链接完全用不到。C++ 端必须能看到 onnxruntime 的头文件和 libonnxruntime.so,这一步也是很多人 Python 跑通后 C++ 编译失败的根本原因。

提示:onnxruntime 的 CPU 推理不需要 CUDA,低配置 ubuntu 机器上装 GPU 版反而会引入一堆驱动依赖,部署体验直线下降。

3.2 CMakeLists 接好两个库:常见链接坑

这套资源里的 CMakeLists 核心逻辑很简洁,我拆开看过后可以精简成下面这段:

cmake_minimum_required(VERSION 3.16) project(yolov8_cpp) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE Release) endif() # OpenCV find_package(OpenCV REQUIRED) # onnxruntime set(ORT_ROOT "/opt/onnxruntime") include_directories(${ORT_ROOT}/include) link_directories(${ORT_ROOT}/lib) add_executable(yolov8_cpp src/main.cpp) target_link_libraries(yolov8_cpp ${OpenCV_LIBS} onnxruntime pthread)

find_package(OpenCV REQUIRED)在 apt 安装 libopencv-dev 后基本不会失败。onnxruntime 的链接名是onnxruntime,因为解压出来的库文件叫 libonnxruntime.so。链接器需要pthread,否则 Ubuntu 上运行时会报线程相关符号找不到。

有一个 CMake 细节要提醒:link_directories在较新的 CMake 版本下不总是生效。更稳妥的写法是直接把动态库路径写进 target:

add_library(onnxruntime SHARED IMPORTED) set_target_properties(onnxruntime PROPERTIES IMPORTED_LOCATION "${ORT_ROOT}/lib/libonnxruntime.so" INTERFACE_INCLUDE_DIRECTORIES "${ORT_ROOT}/include") target_link_libraries(yolov8_cpp ${OpenCV_LIBS} onnxruntime)

编译时用 Release 模式,这样编译器会开 O2 优化。命令一般是:

cd build cmake .. -DCMAKE_BUILD_TYPE=Release make -j$(nproc)

如果 nproc 在低配机器上输出 8,但内存只有 4GB,建议先make -j2,避免编译时内存打满导致系统卡死。

3.3 运行时找不到 libonnxruntime.so:一个命令判断问题

编译通过只是第一步。运行./yolov8_cpp时,最常见的报错是:

error while loading shared libraries: libonnxruntime.so.1.16.3: cannot open shared object file

现象很明显,程序编译时能找到库,运行时却找不到。原因是 onnxruntime 被手动解压到了 /opt/onnxruntime,不在系统默认的共享库搜索路径里。解决方式有两种,第一种是每次运行前设置环境变量:

export LD_LIBRARY_PATH=/opt/onnxruntime/lib:$LD_LIBRARY_PATH ./yolov8_cpp best.onnx test.jpg

第二种是一劳永逸,把路径写进 ld.so 配置:

echo "/opt/onnxruntime/lib" | sudo tee /etc/ld.so.conf.d/onnxruntime.conf sudo ldconfig

平时调试我习惯用第一种,因为可以随时切换 onnxruntime 版本。OpenCV 的 so 通过 apt 安装后已经在系统路径里了,不需要设置。如果程序中还用了其他动态库,用ldd ./yolov8_cpp一次性查看所有 so 的依赖情况,缺哪个一目了然。

4. C++ 推理主流程:YOLOv8 的 onnx 输出解析与坐标还原

环境搭好、模型就位之后,进入主流程。这套源码的 main.cpp 逻辑一共三步:读图预处理、session run、输出解析。下面把最容易错的关节位置拆开讲。

4.1 letterbox 预处理:等比例缩放和灰边填充

YOLOv8 训练时的输入是 640x640,训练管线里做了 letterbox,也就是等比例缩放加灰色填充。C++ 推理如果不做同样的 letterbox,直接把图拉伸到 640x640,物体比例会被改变,检测精度明显下降,尤其是小目标。

cv::Mat letterbox(const cv::Mat& src, cv::Size dst_size, float& scale, int& pad_w, int& pad_h) { int h = src.rows, w = src.cols; scale = std::min((float)dst_size.height / h, (float)dst_size.width / w); int new_w = std::round(w * scale); int new_h = std::round(h * scale); cv::Mat resized; cv::resize(src, resized, cv::Size(new_w, new_h)); pad_w = (dst_size.width - new_w) / 2; pad_h = (dst_size.height - new_h) / 2; cv::Mat canvas(dst_size.height, dst_size.width, CV_8UC3, cv::Scalar(114, 114, 114)); resized.copyTo(canvas(cv::Rect(pad_w, pad_h, new_w, new_h))); return canvas; }

这个函数返回的是 BGR 格式的 640x640 画布,同时通过引用带出了 scale、pad_w、pad_h。这三个值必须保存,推理完成后把检测框映射回原图坐标全靠它们。如果你在别处看到有人返回后不保存 pad,那检测框大概率会偏。

接下来把这张画布转成 onnxruntime 需要的 tensor:

cv::Mat canvas = letterbox(img, cv::Size(640, 640), scale, pad_w, pad_h); // YOLOv8 训练时用的是 RGB 顺序,OpenCV 读进来是 BGR,这里要转 cv::Mat rgb; cv::cvtColor(canvas, rgb, cv::COLOR_BGR2RGB); // 转成 float 并归一化到 [0,1] rgb.convertTo(rgb, CV_32FC3, 1.0 / 255.0); // HWC 转 NCHW,得到 1x3x640x640 cv::Mat blob = cv::dnn::blobFromImage(rgb);

blobFromImage(rgb)默认会把 HWC 转成 NCHW,不需要再手动写三层循环。注意别重复归一化:前面已经做了1.0 / 255.0,后面如果再给 blobFromImage 传 1/255,像素会被缩小成原来的 1/255,检测结果极有可能全空。

4.2 用 Ort::Session 跑模型:动态获取输入名

onnxruntime 的 C++ API 在 1.16 版本下非常稳定。会话创建和推理代码可以这样写:

#include <onnxruntime_cxx_api.h> Ort::Env env(ORT_LOGGING_LEVEL_WARNING, "yolov8"); Ort::SessionOptions opts; opts.SetIntraOpNumThreads(4); opts.SetGraphOptimizationLevel(ORT_ENABLE_ALL); Ort::Session session(env, "best.onnx", opts); // 动态获取输入名,不要写死 "images" auto input_name = session.GetInputName(0); std::vector<int64_t> input_dims{1, 3, 640, 640}; Ort::MemoryInfo memory_info = Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault); Ort::Value input_tensor = Ort::Value::CreateTensor<float>( memory_info, (float*)blob.data, 1 * 3 * 640 * 640, input_dims.data(), input_dims.size()); auto output_tensors = session.Run( Ort::RunOptions{nullptr}, &input_name, &input_tensor, 1, session.GetOutputName(0), 1); float* output = output_tensors.front().GetTensorMutableData<float>();

GetInputName(0)返回的是 session 内部管理的字符串,不需要手动释放。CreateTensor直接包住 blob 的数据,不会发生深拷贝,输入数据在 run 执行期间必须保持有效。GetTensorMutableData<float>()拿到的指针指向 onnxruntime 内部的输出缓冲,后续解析就从这个指针开始。

4.3 解析 1x84x8400:没有 objectness 的输出怎么处理

YOLOv8 的输出 tensor 布局是 1x84x8400,也就是通道在前、anchor 在后。8400 个 anchor 不是排成连续的一行,而是每个维度在同一通道里排完,再到下一个通道。写成代码最容易理解的姿势是把这段输出包装成一个 8400 行、84 列的 cv::Mat:

int rows = 8400; int cols = 84; // 4 + 80, 自定义数据集要改成 4 + num_classes cv::Mat output_mat(rows, cols, CV_32F, output); std::vector<cv::Rect> boxes; std::vector<float> confidences; std::vector<int> class_ids; const float conf_thres = 0.25f; for (int i = 0; i < rows; ++i) { const float* p = output_mat.ptr<float>(i); float cx = p[0], cy = p[1], w = p[2], h = p[3]; float best_score = 0.f; int best_cls = -1; for (int c = 4; c < cols; ++c) { if (p[c] > best_score) { best_score = p[c]; best_cls = c - 4; } } if (best_score < conf_thres) continue; float x1 = cx - w * 0.5f; float y1 = cy - h * 0.5f; boxes.push_back(cv::Rect((int)x1, (int)y1, (int)w, (int)h)); confidences.push_back(best_score); class_ids.push_back(best_cls); } std::vector<int> keep; cv::dnn::NMSBoxes(boxes, confidences, conf_thres, 0.45f, keep);

重点说一下为什么没有 objectness。YOLOv8 的检测头直接输出类别条件概率,不再像 YOLOv5 那样额外输出一个前景置信度。所以这里直接从第 4 列开始找最大类别分数作为置信度,不需要再乘一个 objectness。如果硬套 YOLOv5 的解析逻辑,把第 4 列当成 objectness,然后从第 5 列开始取类别分数,结果要么全为 0,要么框位置错位。

4.4 坐标回原图:先减 pad 再除以 scale

NMS 之后拿到的 keep 索引对应的 boxes,坐标还是在 640x640 输入图上的。映射回原图的公式不复杂,但顺序不能反:

for (auto idx : keep) { cv::Rect box = boxes[idx]; float x1 = box.x; float y1 = box.y; float x2 = box.x + box.width; float y2 = box.y + box.height; // letterbox 逆向:先去掉灰边 pad,再除以缩放比例 int orig_x1 = (int)((x1 - pad_w) / scale); int orig_y1 = (int)((y1 - pad_h) / scale); int orig_x2 = (int)((x2 - pad_w) / scale); int orig_y2 = (int)((y2 - pad_h) / scale); orig_x1 = std::max(0, orig_x1); orig_y1 = std::max(0, orig_y1); orig_x2 = std::min(img.cols - 1, orig_x2); orig_y2 = std::min(img.rows - 1, orig_y2); cv::rectangle(img, cv::Point(orig_x1, orig_y1), cv::Point(orig_x2, orig_y2), cv::Scalar(0, 0, 255), 2); }

这里必须强调:先减 pad 再除以 scale。如果你先除以 scale 再减 pad,框会偏到完全错误的位置。如果只用 scale 不处理 pad,框整体会向右下偏移,越靠近图像边缘越明显。这是 YOLOv8 C++ 部署里出现频率最高的坐标 bug,没有之一。

5. 避坑:Ubuntu + onnxruntime + OpenCV 部署 YOLOv8 的 5 个高频问题

这套资源我在不同机器上跑过,踩过的坑基本都集中在编译链接、坐标映射、输出维度三个方向。下面按“现象 → 原因 → 解决”记下来,每条都是血泪经验。

5.1 运行时提示 libonnxruntime.so: cannot open shared object file

现象:程序编译成功,但执行时立刻弹error while loading shared libraries,找不到 libonnxruntime.so。

原因:onnxruntime 手动静默装在 /opt/onnxruntime,不在系统默认动态库搜索路径里。CMake 链接时能找到,运行时却不会自动带路径。

解决:运行前执行export LD_LIBRARY_PATH=/opt/onnxruntime/lib:$LD_LIBRARY_PATH,或者把路径加入/etc/ld.so.conf.d/onnxruntime.conf后执行sudo ldconfig。建议用 export,方便切换 onnxruntime 版本。

5.2 检测框整体向右下偏移

现象:目标能被检测到,置信度也正常,但框的位置总是往右下偏,越靠近边缘偏移越严重。

原因:letterbox 预处理时记录了 scale 和 pad,但后处理映射时没用 pad,或者把 pad 当成了 0。另一种可能是copyTo(canvas(cv::Rect(pad_w, pad_h, new_w, new_h)))写错了坐标,导致画面在 640x640 画布上整体偏移。

解决:预处理函数把 scale、pad_w、pad_h 用引用返回,后处理严格按(坐标 - pad) / scale映射。如果还不对,输出几个坐标值人工对比,基本一眼能看出 pad 是否生效。

5.3 用 YOLOv5 的解析逻辑解析 YOLOv8 输出

现象:代码是从 YOLOv5 项目里拷的,解析 25200 行、85 列数据,但模型输出是 1x84x8400。运行后要么数组越界,要么一个框也检不到。

原因:YOLOv8 的检测头结构和 YOLOv5 不一样。YOLOv8 没有 objectness 分支,anchor 数量也不是 25200。把 8400 误当成 25200 来遍历,每一行读取的数据完全错位,类别分数当然全是垃圾值。

解决:解析前先打印 onnx 输出的 shape。代码里固定用1x84x8400,类别数根据实际模型改成 4+num_classes,从第 4 列开始取最大类别分数。

5.4 C++ 测出来反而比 Python onnxruntime 慢

现象:低配置机器上,同样的模型 Python 跑 200ms,C++ 跑出来 400ms,完全违背直觉。

原因:Python onnxruntime 默认会按机器核数自动设置线程,C++ 端如果不设置 SessionOptions,intra-op 线程数可能只有 1 或 2。另外编译时如果用了 Debug 模式,优化被关闭,速度会差好几倍。

解决:在创建 Session 前设置opts.SetIntraOpNumThreads(std::thread::hardware_concurrency()),编译用 Release 模式。检查 CPU 占用率时如果发现只有一两个核在忙,就是线程数没设置到位。

5.5 阈值调到 0.01 还是没有任何检测框

现象:图里目标非常明显,但程序输出 0 个框,把 conf_thres 一路降到 0.01 还是空白。

原因:预处理数据不对。最常见的是没有把 BGR 转成 RGB,或者归一化范围不对,或者 blob 的数据类型不是 float32。另一种可能是 onnx 模型是 FP16 导出的,用 CPU 推理时输入格式不匹配,导致输出全是 NaN。

解决:写一个简单的 dump 函数,把送进 session 的 blob 前几个像素值打出来,跟 Python 端对比。Python 里输入是 RGB、范围 0~1,C++ 端也必须完全一致。模型建议统一用 FP32 导出,低配 CPU 上 FP16 不会带来加速。

6. 验证与提速:让低配置 Ubuntu 机器上的推理稳定跑到几十毫秒

第一版程序跑通只是开始。我建议在接摄像头之前,先做一次输出对拍,再用三个小技巧把延迟压下来。

6.1 用 Python 端输出对拍,验证 C++ 后处理逻辑

固定选一张测试图,分别在 Python 和 C++ 里跑,把每个检测框的 x、y、宽度、高度、置信度、类别写成文本文件。然后对比两份结果。坐标误差允许 1~2 像素,置信度误差在 0.01 以内。如果 C++ 端框位置完全不对,优先检查 letterbox 的 pad 和 scale;如果框对但置信度不同,优先检查预处理归一化和通道顺序。

6.2 三个立即可用的提速习惯:预热、线程数、编译优化

第一,预热。模型第一次推理会触发算子初始化,速度慢不代表真实性能。正式测试前先跑 5 次空推理:

for (int i = 0; i < 5; ++i) { auto warmup = session.Run(Ort::RunOptions{nullptr}, &input_name, &input_tensor, 1, session.GetOutputName(0), 1); }

第二,线程数按物理核设。低配 Ubuntu 机器上逻辑线程设太高反而因为争抢变慢,4~6 个线程通常是最优区间。第三,编译时用 Release 并加 O2 优化。对 FP32 模型不要盲目转 FP16,很多 CPU 上 FP16 需要通过 FP32 模拟,速度不升反降。

从那以后,我每次在 Ubuntu 上换一台机器部署,都强制走一遍这个流程:先导出固定 shape 的 onnx,再用 Python 核对输出维度,写 C++ 后处理前先画一个假框验证坐标映射,最后调线程数。这套源码加说明文档直接对着跑一遍,能省掉你至少一周的排查时间,希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 3:29:09

JLink VCOM虚拟串口:无需USB转TTL实现printf打印调试

1. 为什么我放弃了USB转TTL&#xff0c;转投JLink VCOM搞嵌入式开发的朋友大概率都经历过这样的场景&#xff1a;板子已经连了JLink做下载和调试&#xff0c;程序里想加几行printf打印看看变量状态&#xff0c;结果发现手头没有USB转TTL模块&#xff0c;或者串口线被别的设备占…

作者头像 李华
网站建设 2026/9/26 3:26:56

基于Java的IEC 62056-21 C模式主站协议库:统一读取电水气热表

简介&#xff1a;面向能源管理、智能家居与市政计量领域的Java开发者&#xff0c;这份资源实现了IEC 62056-21 C模式主站协议&#xff0c;支持通过串口或网络连接燃气表、水表、热量表、电表等计量设备&#xff0c;直接读取标准化数据。协议库基于国际电工委员会标准设计&#…

作者头像 李华
网站建设 2026/9/26 3:25:43

全栈开发者桌面状态仪表盘:ESP32-S3 + JSON + BLE 实战设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华