news 2026/10/5 1:35:55

Halcon C++工业相机实时采集与SDK配置实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Halcon C++工业相机实时采集与SDK配置实战

很多时候我们接手一个视觉项目,难点往往不是算法本身,而是第一步就卡住了:相机连不上、SDK配置不出来、图像一直不刷新。这篇东西我就拿最近一个C++版本的项目来复盘,从Halcon安装、Visual Studio工程配置,到工业相机接入、实时图像采集、在线调参,再到线程稳定性和常见坑位,完整走一遍流程。适合刚接触工业视觉的C++开发,也适合已经用HDevelop写过脚本、想搬到C++工程里的朋友。只要跟着配置一遍,就能搞出一套可用的实时采集代码,而不是停留在Demo阶段。


1. 整体方案设计:先想清楚两条采集路线

1.1 这个项目到底要解决什么问题

工业视觉项目里,相机采集是所有视觉算法的前置条件。一个完整的实时检测系统通常包含三层:相机信号采集层、图像处理算法层、结果展示与交互层。很多人在C++里调Halcon时,只盯着算法部分,结果项目一启动就发现图像拿不进来,或者好不容易拿到一帧,显示又卡成幻灯片。

我这次的项目需求其实很典型:用一台GigE接口的工业相机,在Windows下通过C++调用Halcon完成图像采集和实时显示,并且后续要在同一套框架里做缺陷检测。所以整个项目从一开始就要考虑三个问题:相机驱动怎么接、图像数据怎么组织、显示和采集线程怎么配合。

在这个前提下,最关键的选型就是“用Halcon直连相机”还是“相机厂商SDK取流之后转成Halcon图像对象”。这两条路线我这次都实际踩了一遍,后面第2章会详细对比。

1.2 为什么会选Halcon C++而不只写HDevelop脚本

有个很常见的误区:先在HDevelop里写好了采集和图像处理脚本,又跑通了,就直接认为C++工程也能轻松搞定。但实际上HDevelop底层做了大量封装,比如窗口管理、图像缓存、异常处理都帮你做好了;到了C++里这些全都要自己处理。这个项目的目标就是从HDevelop原型过渡到可编译、可部署的C++程序。

选择Halcon C++接口而不是纯C接口,主要原因有三个:HObject和HImage这些类封装了引用计数,内存管理比C接口的Hobject句柄省心;运算符重载让代码比较接近HDevelop写法;新版本对C++11以上标准兼容性更好,配合STL容器写工程代码也更顺手。

我需要特别提醒一点,这个项目里我用的是x64平台。Halcon的库有x86和x64之分,工程平台必须和Halcon库位数一致,否则编译链接阶段会冒出一堆莫名其妙的LNK2019。这个问题我在最初搭建时踩过,后面会反复强调。

1.3 版本与环境选型的几个注意点

这次我用的组合是Halcon 22.05 + Visual Studio 2019 + Windows 10 专业版。Halcon从20.11之后的版本在C++接口上改动不大,配置方式基本通用,但不同小版本之间头文件目录可能有细微差别。Visual Studio这边,我建议至少用VS2017以上,否则C++标准支持不够,有些Halcon头文件里的模板代码会编译不过。

第二个注意点是Halcon的许可证。开发调试阶段可以使用HDevelop开发版的License,但部署到现场机器时需要对应版本的Runtime License。License文件可以放在两个位置:一是%HALCONROOT%\license目录下,二是通过环境变量HALCON_LICENSE_FILE指定。我实际测试下来,后者更灵活,因为可以在一台机器上切换不同版本的License而不用反复拷贝文件。

第三个注意点是系统环境变量HALCONROOT。安装Halcon时一般会自动写入,但如果安装时选了“只给当前用户”,某些VS工程里用$(HALCONROOT)宏就会取不到,导致包含目录配置失败。稳妥的做法是在工程属性里直接用绝对路径,或者手动把HALCONROOT加到系统环境变量并重启VS。


2. SDK配置详解:从安装到能跑一个最小采集Demo

2.1 Halcon安装与License配置

Halcon的安装包是MVTec官方下载的,安装过程没有特殊注意事项,唯一要留意的就是安装目录不要带中文和空格,C:\Program Files\MVTec\Halcon-22.05这种默认路径是可以的,但如果你放到D:\视觉项目\Halcon这种自定义目录,后面VS配置里写路径时容易因为转义和空格出问题。

装完之后第一件事不是写代码,而是确定License能否识别。打开环境变量,确认HALCONROOT指向的是当前安装的Halcon目录。然后在命令行跑一下:

echo %HALCONROOT%

如果输出为空,就手动新建系统环境变量HALCONROOT,值设为Halcon安装路径。接着检查%HALCONROOT%\license目录下有没有有效的license.dat。我这次用的是试用License,功能有部分限制,比如不支持某些深度学习算子,但常规采集和图像处理完全够用。

还有一个小技巧:如果机器上装了多个版本的Halcon,务必保证PATH里只有一个bin路径排在前面,否则运行程序时可能会加载到旧版本的DLL。排查这种问题的方法是用where halconcpp.dll命令查看实际加载路径,这个在后面的常见问题部分会再提到。

2.2 Visual Studio工程里配置Halcon头文件与库文件

新建一个C++控制台工程之后,工程的“属性页”按下图思路配置。

  • 平台选择x64,然后配置管理器里确认当前平台也是x64。
  • C/C++ → 常规 → 附加包含目录,添加两个路径:
    • $(HALCONROOT)\include
    • $(HALCONROOT)\include\halconcpp
  • 链接器 → 常规 → 附加库目录,添加:
    • $(HALCONROOT)\lib\x64-win64
  • 链接器 → 输入 → 附加依赖项,添加:
    • halconcpp.lib

不同Halcon版本的库目录名会稍有不同,有的版本是x64sse2-win64,有的是x64-win64,我建议直接打开Halcon安装目录看一眼lib文件夹再填,避免照抄网络教程写错路径。

还有一个容易忽略的点:Debug和Release配置都要重复设置一遍,不要把时间浪费在“为什么Debug能跑Release不能”这种问题上。我通常还会顺手把_CRT_SECURE_NO_WARNINGS加到预处理定义里,省得一些老接口的警告信息刷屏。

2.3 编写一个最小验证程序

配置完成后,先不急着接相机,跑一个最小的Halcon窗口显示程序,确认环境和链接都没问题。代码如下:

#include "halconcpp/HalconCpp.h" using namespace HalconCpp; int main() { try { HWindow w(0, 0, 640, 480, 0, "visible", ""); HImage img; img.ReadImage("D:/test.png"); w.DispObj(img); std::cout << "Press Enter to exit..."; std::cin.get(); } catch (HException& e) { std::cerr << e.ErrorMessage().Text() << std::endl; return -1; } return 0; }

我特别强调一下,整个项目里所有Halcon调用都要包在try-catch里。Halcon的异常机制和C++标准异常不太一样,它抛的是HException,如果不在入口处捕获,程序会直接崩溃,而且报错信息在Release模式里还特别难定位。

编译运行后,如果能看到图片显示出来,说明Halcon头文件、库文件、运行DLL都正常了。接下来就可以进入相机采集环节了。


3. 工业相机接入:两种方案怎么选

3.1 方案对比:Halcon直连相机 vs 厂商SDK取流

我这次一开始用的是Halcon直连GigE相机的方式。在HDevelop里这只需要一个open_framegrabber算子,然后在C++里调对应的OpenFramegrabber就能连上相机。Halcon内部通过GenICam接口解析相机参数,像曝光、增益、触发模式都能直接设置,非常方便。

但Halcon直连方式有一个明显短板:如果你用的相机是Basler、海康、大华这些品牌,厂商SDK里额外提供了一些私有特性,比如特殊的帧率控制、诊断信息、多相机同步配置等,Halcon的通用接口不一定能完全暴露出来。而且遇到图像丢帧或者网络异常时,Halcon给出的错误信息很有限,排查起来远不如厂商SDK直观。

所以这次项目的实际做法是:先判断相机是否兼容Halcon的GenICam接口,兼容就用直连;不兼容或需要深度调试时,切到厂商SDK取流,再转换成Halcon图像对象。两条路线我都实现了,代码结构上做了接口隔离,切换时不用动上层图像处理代码。

3.2 Halcon直连方式:open_framegrabber参数详解

Halcon里连接工业相机的核心算子是OpenFramegrabber,它的参数非常多,让人一看就头大。这里给一个GigE相机的典型调用:

HTuple acqHandle; OpenFramegrabber( "GigEVision2", // 相机接口类型 0, 0, 0, 0, 0, 0, // 预留参数,GigE模式下填0 "default", // 相机类型逻辑 -1, // 默认数据位宽 "default", // 颜色格式 -1, // 默认字段 "false", // 外部触发关闭,先自由运行 "default", // 相机接口默认参数 "camera1", // 指定相机名称或IP -1, // 超时时间默认-1表示无限等待 0, // 句柄类型 &acqHandle);

前面的参数对GigE相机基本是固定写法,需要理解的是几个关键值:第一个参数GigEVision2决定了Halcon使用哪种传输协议来和相机通信;camera1对应相机的IP或设备名,可以用InfoFramegrabber查询当前网络上的相机列表;最后那个外部触发参数设为"false",表示先让相机自由运行出图,等采集稳定后再切到硬件触发。

连接成功之后,可以用GetFramegrabberParam把相机参数逐个读出来。比如查看当前分辨率:

HTuple width, height; GetFramegrabberParam(acqHandle, "Width", &width); GetFramegrabberParam(acqHandle, "Height", &height);

在实际项目中我还喜欢加一步info_framegrabber查询,确认Halcon是否能看到这张网卡和相机列表。如果在OpenFramegrabber时报“没有找到设备”,大概率是网络设置没配对,GigE相机和电脑必须在同一网段,相机的IP要静态设置,关闭防火墙,网卡巨型帧要打开。这些细节在HDevelop里表现不明显,但到了C++工程里一个都不能少。

3.3 厂商SDK方式:从相机Buffer转换到HObject

第二种方案是用厂商SDK自己完成取流,比如Basler的Pylon、海康的MVS、大华的DahuaSDK。厂商SDK通常采用回调函数或者主动拉流两种模式,回调方式的实时性更好,但在回调函数里不能直接做耗时操作。

拿到图像原始数据后,需要把它转成Halcon的HObject。这一步的关键是像素格式要匹配。对8位灰度图:

unsigned char* pBuf = nullptr; // 相机SDK返回的图像buffer int width = 2448; int height = 2048; HObject ho_Image; GenImage1(&ho_Image, "byte", width, height, (Hlong)pBuf);

如果相机输出的是彩色图像,且内存排列是RGB三通道交错,可以用GenImageInterleaved,指定像素格式为"rgb"或"bgr"。很多相机默认输出是BGR顺序,如果不注意,转出来的图像颜色会错乱,缺陷检测时色差会出大问题。我一般习惯在相机SDK里直接把像素格式设置成Mono8或RGB8,转换逻辑就不用在代码里反复判断。

还遇到过一种情况:相机SDK返回的是Bayer格式,Halcon里有专门的算子是CvtImageFromBayer,配合相机的CFA排列方式使用。如果无法确定Bayer排列,可以在Halcon里试几种排列方式,看哪一张纹理最清晰。

这里带出一个经验:在回调里直接调用GenImage1并不是最优解,因为GenImage1默认会做一次数据拷贝,性能压力大。想追求极致性能可以用GenImage1Extern实现零拷贝,但这个算子不管理内存,意味着相机buffer被Halcon引用期间,你不能释放它。我用的方案是维护一个循环缓冲区,回调里把数据memcpy到缓冲槽,再把槽内地址交给GenImage1Extern。这种方式说起来简单,但实际要考虑槽位生命周期和信号量同步,后面第5章会详细展开。


4. 实时采集循环:从“能出图”到“高效出图”

4.1 同步采集与异步采集的核心区别

工业相机实时采集最常见的坑是:只会用同步GrabImage,结果帧率一高就卡顿。Halcon的GrabImage是阻塞式等待,相机来一帧取一帧,显示和处理都在一个循环里串行完成,处理耗时多少,帧间隔就是多少,实时性非常差。

更合理的方式是异步抓图,核心是GrabImageStart和GrabImageAsync组合。GrabImageStart通知采集设备开始连续采集,图像数据进入内部缓冲区队列;GrabImageAsync则从缓冲区队列里取出最新的一帧。两者搭配使用时,抓图和算法处理可以有一定程度的重叠,相当于流水线式工作。

GrabImageStart(acqHandle, -1); while (running) { HObject ho_Image; GrabImageAsync(&ho_Image, acqHandle, -1); // 在这里写图像处理逻辑 DispObj(ho_Image, windowHandle); }

这个循环里,GrabImageAsync的第三个参数是超时时间,单位毫秒。设为-1表示一直等到有图才返回,设为300表示最多等300毫秒,超时后返回空对象。在UI程序里我建议不要用-1,因为如果相机掉线,你的采集线程会永远卡在里面无法退出,用超时机制可以检测异常并做重连处理。

4.2 实时显示与帧率限制

实时显示也是一门学问。直接把DispObj写在采集循环里,如果处理和显示的耗时超过相机出图的间隔,队列会越堆越高,图像延迟越来越大,最后一退出循环,内存占用惊人。

我这次用了一个简单实用的方法:处理线程只管拿图,拿到的图用CopyObj复制一份放入图像队列;显示线程从队列里取图,每帧间隔通过std::this_thread::sleep_for控制到约30ms。这样即使算法耗时很长,显示线程也不会拖累采集。当然如果你对延迟极度敏感,就得用环形缓冲和信号量,我这里不过度展开了。

显示图像时还有一个小技巧:DispObj是按图像原始大小绘制的,如果图像是500万像素而窗口只有1000x800,直接显示会做缩放,CPU占用不低。更合理的做法是先调SetPart设置窗口显示区域,让窗口显示的图像范围匹配窗口大小,这样绘制效率会高不少。

4.3 在线调整曝光、增益与触发模式

实时采集系统里,曝光和增益往往需要在运行中动态调整,不能每次都重连接相机。Halcon的参数设置算子SetFramegrabberParam支持在采集过程中随时修改相机的Camera Link或GigE参数。

SetFramegrabberParam(acqHandle, "ExposureTime", 5000.0); // 曝光时间,单位微秒 SetFramegrabberParam(acqHandle, "Gain", 6.5); // 增益,单位dB SetFramegrabberParam(acqHandle, "TriggerMode", "On"); // 开启硬件触发 SetFramegrabberParam(acqHandle, "TriggerSource", "Line1"); // 触发源设为Line1 SetFramegrabberParam(acqHandle, "TriggerActivation", "RisingEdge");

这里有一个高频问题:曝光时间的单位。不同厂商相机返回的单位不一样,大多数GigE相机是微秒,但有些工业相机内部用“行数”表示曝光。因此代码里最好有一个封装层,把上层传过来的物理时间统一换算成对应相机SDK定义的参数值。我用过一个土办法,就是先在HDevelop里手动调曝光,观察参数值和实际亮度变化,确定单位后再写进C++代码。

硬件触发模式下还有几个隐藏参数要注意。GigE相机通常需要先把TriggerMode设为On,再把TriggerSource设为对应的物理输入线;有些相机的输入线编号从1开始,有些则要区分Line0还是Line1。如果相机一直收不到外部信号,先用软件触发测试相机本身是否正常,如果软件触发能出图、硬件触发不行,那问题几乎都在触发源配置、接线或者极性设置上。


5. 项目的稳定性与工程化:线程、缓冲区和窗口集成

5.1 为什么采集线程里不能做重活

做过实时视觉项目的人都清楚,丢帧往往不是相机问题,而是你的采集线程被阻塞住了。Halcon异步采集虽然有内部队列兜底,但队列深度有限,一旦图像出图速度大于消费速度,新帧就会被丢弃。

我的线程模型是这样的:一个采集线程专职调用GrabImageAsync拿图,拿到后马上放入无锁环形队列;一个算法线程从队列里取图做处理;一个UI线程负责显示和交互。三个线程之间通过原子变量和互斥锁传递数据,保证每个线程的耗时不互相影响。这种结构虽然写起来比单线程复杂,但稳定性完全不是一个级别。

关键点在于:HObject本身不是线程安全的。如果你在采集线程里创建了一个HObject,把它直接传给算法线程使用,必须用CopyObj做一次深拷贝,或者用GenImage1重新构造一份。直接赋值跨线程使用,轻则图像花屏,重则程序崩溃。

5.2 零拷贝图像传递:性能与生命周期的权衡

如果图像数据量大、帧率高,频繁拷贝buffer会成为性能瓶颈。这时可以使用GenImage1Extern,它让Halcon图像对象直接指向你提供的内存,不做拷贝。

unsigned char* pBuf = ringBuffer.GetCurrentSlot(); HObject ho_Image; GenImage1Extern(&ho_Image, "byte", width, height, (Hlong)pBuf, 0);

但这里有个大坑:Halcon内部在用这个图像对象时,会默认该内存一直是有效的。如果你的环形缓冲槽在算法还没有处理完时就被下一帧覆盖,那图像数据就全乱了。我踩过几次坑之后总结出来的经验是:尽量让槽位的生命周期覆盖“取帧后到处理完成前”的整段时间,推荐的做法是槽位上放引用计数或状态标志,处理完才允许覆盖。

5.3 把Halcon窗口嵌进Qt界面

如果是纯控制台程序,HWindow默认创建一个独立窗口显示图像,这在项目交付时往往不够体面。更常见的需求是把图像显示区域嵌入到Qt的QLabel或QWidget里。

Halcon的OpenWindow算子最后两个参数可以传窗口句柄。你只要在Qt里拿到控件的winId(),把它转成Hlong传给Halcon即可:

HWND hwnd = (HWND)ui->labelDisplay->winId(); HWindow win(0, 0, ui->labelDisplay->width(), ui->labelDisplay->height(), (Hlong)hwnd, "visible", ""); m_windowHandle = win;

这里有个细节:如果用winId()获取句柄的方式,控件在窗口整体重绘时,Halcon绘制的图像可能会被Qt的背景色盖掉。稳妥的解决方式是在Qt中用QWidget的paintEvent里定期调用Halcon绘制,或者把Halcon窗口直接嵌套在QWinWidget等封装控件中,网上相关的Halcon Qt集成方案很多。我这次是直接用了winId,在Windows下实测下来配合repaint能稳定显示,但如果你的Qt版本升级或换了窗口系统,建议先做一个最小Demo确认显示路径没问题。


6. 常见问题排查实录

6.1 大华/Basler相机丢帧的排查思路

丢帧是最容易让人崩溃的问题。排查时先搞清楚丢帧发生在相机内部、网络传输还是应用层。大华和Basler都有自己的网络诊断工具,能看到网络丢包率、接收缓冲占用等参数。

如果是GigE相机,网卡设置影响很大。一定要在网卡属性里开启巨型帧(Jumbo Frame),建议9000字节;接收缓冲区调到最大;关闭“大量发送卸载”等高级选项。如果还丢帧,就把相机的包间隔适当调大,或者降低分辨率,先排除带宽瓶颈。

应用层的丢帧往往是因为处理耗时过长。我那段时间不停调算法参数,后来把显示、处理和采集彻底分离后,丢帧就消失了。排查时可以给每一帧打上序号,在显示界面叠加当前帧号和总帧数,就能直观知道丢帧的节奏。

6.2 海康相机“未收到触发信号”怎么处理

海康的相机在Halcon里有时会提示没有触发信号。我通常按这个顺序排查:第一步,在相机SDK自带的客户端软件里关掉Halcon连接,单独测试相机的硬件触发能不能工作;第二步,看触发源是否选择正确,相机型号不同,物理输入线对应的逻辑名称也不同;第三步,检查极性,有些项目接线是高电平有效,但相机默认是下降沿触发。

一个容易被忽略的地方是:相机如果之前用别的软件配置过“触发延时”或“触发过滤宽度”,可能会把很短的触发脉冲过滤掉。普通的光电传感器输出脉冲很短,相机识别不到,可以适当降低触发滤波时间。

6.3 License与运行环境类错误的快速定位

编译链接都正常、运行时却报License不可用,这是部署阶段最典型的错误。先确认运行目录下是否存在license.dat,再看环境变量HALCON_LICENSE_FILE是否被错误地指向了别的路径。还有一种情况是程序启动时加载了旧版本的Halcon DLL,导致License类型不匹配,可以用工具查看进程加载的DLL路径,确认不是从系统PATH里加载了旧版本。

VS工程里还有一类“半天查不出原因”的问题是:Debug编译正常,Release编译报链接错误,或者反过来。这种大概率是平台位数选错了,或者Debug和Release链接了不同版本的库。我习惯把目标平台固定为x64,Debug和Release都链接同一个halconcpp.lib,避免引入不必要的变量。

6.4 几个容易忽略的细节清单

我大致整理了一些平时容易翻车的小点,做成表格方便对照:

现象可能原因处理办法
编译时报无法打开HalconCpp.h附加包含目录配错,或$(HALCONROOT)未生效检查环境变量,或改为绝对路径
运行时报halconcpp.dll找不到%HALCONROOT%\bin\x64-win64未加入PATH添加环境变量后重启
图像颜色不对相机输出BGR,Halcon按RGB解析使用GenImageInterleaved并指定bgr
画面卡顿采集和处理在同一个线程分离线程,异步抓图
相机连不上IP不在同一网段、防火墙拦截设置静态IP,开放相机端口
图像越用越亮或越暗自动曝光和手动设置冲突关闭Auto功能后再手动设置

这些坑不一定每个项目都会遇到,但一旦遇到,按表格排查能省很多时间。尤其中间那几条,都对应真实项目里常见的现场问题。


从SDK配置到实时图像采集这一套流程,真正跑通一次之后你会发现,后续开发视觉算法反而轻松很多。我个人在实际操作中的体会是,工程架构比单纯调通相机重要得多——图像对象的生命周期管理、线程之间的数据传递方式、光一个采集模块就能决定整个项目稳定性的上限。建议你把自己的采集部分封装成一个独立的类,对外只暴露图像回调或队列接口,这样无论换相机还是换SDK,都能尽量减少上层算法的改动。

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

工业嵌入式存储方案:MKV46F256VLH16与MR25H40CDF的SPI通信与掉电保护

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

作者头像 李华
网站建设 2026/10/5 1:35:11

ZynqMP多核异构:Linux+裸机共享内存与Cache一致性实战

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

作者头像 李华
网站建设 2026/10/5 1:34:39

MRAM掉电保护实战:MR25H40CDF与MK20DN128VFM5的SPI驱动与避坑指南

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

作者头像 李华
网站建设 2026/10/5 1:34:37

MIPI信号实测与调试:从波形特征到常见问题排查

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

作者头像 李华
网站建设 2026/10/5 1:34:11

双脑架构:Linux与STM32在嵌入式安全系统中的分工本质

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

作者头像 李华
网站建设 2026/10/5 1:33:13

STM32 DSP库配置全解:解决arm_math.h报错与Undefined symbol

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

作者头像 李华