资讯动态

基于Simulink与STM32的硬件在环测试系统搭建指南

发布时间:2026/8/6 13:07:34 来源:尧图企业网站定制
1. 项目概述当Simulink模型遇见真实的STM32芯片搞嵌入式开发或者控制算法的朋友对“硬件在环”这个词应该不陌生。简单来说HIL就是让你的算法模型跑在电脑上但控制的对象不是虚拟的而是连接在电脑旁的一块真实的电路板。这就像给一个游戏角色你的算法配上了一副真实的身体硬件让它去真实世界“打怪升级”测试其反应和性能。今天要聊的就是如何用我们最熟悉的MATLAB Simulink和STM32单片机搭建一套属于你自己的、低成本但功能强大的HIL测试环境。为什么这事儿值得花时间折腾我自己的体会是它能极大缩短从算法仿真到产品落地的周期。以前我们在Simulink里把电机控制、电源管理或者自动驾驶的模型调得飞起仿真波形完美无缺。但一把代码下载到STM32里电机要么不转要么啸叫各种稀奇古怪的问题全来了。问题出在哪是模型参数不对还是硬件驱动有bugHIL测试能把这两者剥离开。在HIL环境下Simulink模型作为“控制器”发出PWM、ADC指令等控制信号而真实的STM32板子则作为“被控对象”执行这些指令并反馈真实的电压、电流信号。这样你就能在安全、可控的桌面环境中反复“折磨”你的控制算法验证其在真实硬件接口下的鲁棒性而不用担心烧坏昂贵的负载设备。这个例程测试的核心就是打通Simulink与STM32之间的实时数据桥梁。它适合所有正在从纯仿真迈向硬件实现的工程师、学生和爱好者。无论你是做电机驱动、电源转换、机器人控制还是物联网设备的复杂逻辑验证这套方法都能提供一个可靠的测试基准。接下来我会拆解整个流程的设计思路、需要准备的工具链、具体的实操步骤以及我踩过的一些坑和对应的排查技巧。2. 整体方案设计与核心工具链选型搭建一个可用的HIL系统核心在于实现“实时性”和“可靠性”的数据交换。Simulink模型以固定的步长运行它需要在每个步长内完成从STM32读取数据、执行算法计算、向STM32发送指令这一完整闭环。任何一步的延迟或数据错误都会导致仿真失真甚至崩溃。2.1 主流实现路径对比与选择通常实现Simulink与外部硬件通信有几种主流方式基于MATLAB Instrument Control Toolbox的串口/UDP通信这种方式最直接利用Serial或UDP模块进行数据打包发送。优点是简单无需额外软件授权。缺点是实时性较差通信速率和稳定性受操作系统调度影响大很难做到毫秒级以下的稳定步长适合对实时性要求不高的场景。使用Simulink Real-Time和Speedgoat等专业实时目标机这是工业级的方案通过专用的实时操作系统和硬件IO板卡能实现微秒级的高精度硬实时。缺点是成本极高一套系统动辄数十万不适合个人或小团队。通过STM32 Mat/Target Support Package生成代码并在线调试这是MathWorks官方提供的方式利用STM32硬件支持包直接从Simulink模型生成C代码并下载到STM32中运行。但这更像是“软件在环”或“处理器在环”STM32运行的是完整的控制算法我们难以将其灵活地配置为单纯的“被控对象”执行器。利用Simulink的外部模式与自定义设备驱动这是我们本次采用的核心方法。Simulink的外部模式允许模型在PC上运行同时通过一个通信通道如USB、以太网与外部硬件交换数据。我们需要在STM32端编写一个固件程序专门负责与Simulink进行实时数据收发和硬件IO操作。我们选择第4种方案因为它在成本、灵活性和实时性之间取得了最佳平衡。STM32负责最底层的、确定性的IO操作如产生精确的PWM、读取ADC而复杂的算法和逻辑仍在PC端强大的Simulink中运行和调整。通信层我们选择USB虚拟串口原因如下首先STM32的USB CDC类实现成熟在主流操作系统上即插即用无需安装额外驱动其次USB全速模式的理论带宽达到12Mbps足以应对多数控制系统的数据量如几路PWM、ADC最后其可靠性远高于普通UART串口且连接方便。注意如果对实时性有更高要求步长1ms可以考虑使用STM32的以太网接口配合UDP协议并可能需要搭配一个实时性更好的PC端调度器但这会显著增加复杂性。对于大多数百赫兹到千赫兹级别的控制系统USB虚拟串口方案经过优化后完全可以满足。2.2 核心工具与软件准备清单工欲善其事必先利其器。以下是需要提前准备好的软件环境我会说明每个工具的作用和版本选择的考量。工具/软件推荐版本/型号核心作用与选择理由MATLAB SimulinkR2020a 或更新算法建模与仿真环境。建议使用较新版本其对USB通信的支持和硬件支持包更完善。STM32CubeMX最新版ST官方初始化代码生成工具。用于快速配置STM32的时钟、USB CDC、GPIO、定时器、ADC等外设生成工程框架事半功倍。IDE/编译器Keil MDK-ARM 或 STM32CubeIDE用于编写、编译和下载STM32端的固件程序。两者皆可Keil生态更成熟CubeIDE免费且与CubeMX集成度更高。STM32开发板主频≥72MHz带USB FS接口如STM32F103C8T6蓝色小板、STM32F407、STM32G4系列等。F103是最经济的入门选择性能足够本例程。USB数据线屏蔽良好的Micro-USB线用于供电和通信。劣质线缆可能导致通信不稳定务必选用可靠的线材。版本兼容性心得我曾遇到过MATLAB R2018b与新版STM32CubeMX生成的HAL库代码存在兼容性问题Simulink的S-Function Builder编译会报错。因此建议保持工具链的相对同步。如果遇到奇怪编译错误首先检查MATLAB的MEX编译器是否已正确配置在命令行输入mex -setup查看并尝试使用CubeMX生成一个兼容旧版HAL库的工程。3. STM32端固件数据交换与硬件执行中枢STM32在这套系统中扮演着“翻译官”和“执行者”的双重角色。它一方面要通过USB接收来自Simulink的指令数据包并解析成具体的硬件操作如设置PWM占空比另一方面要定时采集硬件状态如ADC值打包后发送回Simulink。3.1 外设配置与工程搭建以STM32F103C8T6为例首先使用STM32CubeMX进行图形化配置。时钟树配置将系统时钟设置为最高频率对于F103通常使用8MHz外部晶振通过PLL倍频到72MHz。更高的主频意味着更快的USB数据处理能力和更精确的定时器控制。USB外设配置在“Connectivity”下启用“USB (FS)”。在“Middleware”中选择“USB_DEVICE” Class For FS IP选择“Communication Device Class (Virtual Port Com)”。这会将STM32配置为一个USB转串口设备。关键一步在“Project Manager - Code Generator”中勾选“Generate peripheral initialization as a pair of ‘.c/.h’ files per peripheral”。这会将USB的代码生成独立的文件方便我们修改和移植。关键功能外设配置PWM输出例如我们使用TIM1的通道1PA8输出PWM。在“Timers - TIM1”中选择“Clock Source”为内部时钟通道1选择“PWM Generation CH1”。调整“Prescaler”和“Counter Period”以设置PWM频率。例如72MHz主频预分频72-1计数周期1000-1可得到1kHz的PWM频率分辨率为1/1000。ADC采样例如使用ADC1的通道0PA0采样。在“Analog - ADC1”中启用通道0。设置“Scan Conversion Mode”为Disable单通道“Continuous Conversion Mode”为Enable连续转换“DMA Continuous Requests”可启用以提高效率。设置采样时间。一个高精度定时器用于固定周期地触发数据收发。例如使用TIM2配置为1kHz中断1ms周期。在中断服务函数中我们将执行“读取ADC值 - 打包数据 - 通过USB发送”以及“从USB接收缓冲区解析指令 - 更新PWM”的流程。生成代码指定工程名称、路径、IDEMDK-ARM然后生成代码。3.2 数据协议设计与通信驱动实现通信的稳定可靠依赖于一套简单、健壮的应用层协议。我们不能简单地把ADC的整数直接扔进USB发送缓冲区因为数据流可能会错位。我设计的帧协议如下仅供参考可调整每帧数据共10字节。[帧头0xAA][帧头0x55][命令字][数据1高字节][数据1低字节][数据2高字节][数据2低字节][...][校验和][帧尾0x0D][帧尾0x0A]帧头用于在数据流中识别一帧的开始。命令字区分是Simulink下发的控制指令如0x01代表设置PWM还是STM32上传的传感器数据如0x02代表上传ADC。数据具体内容。例如控制PWM时数据为一个16位整数0-1000代表占空比。上传ADC时数据为ADC的12位原始值0-4095。校验和从命令字到最后一个数据字节的累加和取低8位。用于验证数据在传输过程中是否出错。帧尾标识帧结束。在STM32的usbd_cdc_if.c文件中我们需要修改两个核心函数// 接收来自PCSimulink的数据 static int8_t CDC_Receive_FS(uint8_t* Buf, uint32_t *Len) { // 将接收到的数据存入自定义的环形缓冲区 ring_buffer_write(rx_buf, Buf, *Len); // 设置数据接收标志 usb_rx_flag 1; return (USBD_OK); } // 向PCSimulink发送数据 uint8_t CDC_Transmit_FS(uint8_t* Buf, uint16_t Len) { uint8_t result USBD_OK; if (hUsbDeviceFS.dev_state USBD_STATE_CONFIGURED) { // 调用HAL库的发送函数注意此函数非阻塞需等待上次发送完成 if(CDC_Transmit_State 0) // 判断是否空闲 { CDC_Transmit_State 1; result USBD_CDC_SetTxBuffer(hUsbDeviceFS, Buf, Len); result USBD_CDC_TransmitPacket(hUsbDeviceFS); } } return result; }在TIM2的1ms中断服务函数中我们实现主逻辑void TIM2_IRQHandler(void) { if (__HAL_TIM_GET_FLAG(htim2, TIM_FLAG_UPDATE) ! RESET) { __HAL_TIM_CLEAR_FLAG(htim2, TIM_FLAG_UPDATE); // 1. 读取ADC值 uint16_t adc_value hadc1.Instance-DR; // 假设使用轮询或DMA已更新 // 2. 打包ADC数据帧 uint8_t tx_buffer[10]; tx_buffer[0] 0xAA; tx_buffer[1] 0x55; tx_buffer[2] 0x02; // 命令字上传数据 tx_buffer[3] (adc_value 8) 0xFF; // 数据高字节 tx_buffer[4] adc_value 0xFF; // 数据低字节 // ... 可以填充更多数据 uint8_t checksum 0; for(int i2; i4; i) checksum tx_buffer[i]; // 计算校验和 tx_buffer[5] checksum; tx_buffer[6] 0x0D; tx_buffer[7] 0x0A; // 3. 通过USB发送数据帧 CDC_Transmit_FS(tx_buffer, 8); // 4. 处理接收解析来自Simulink的指令 if(usb_rx_flag) { usb_rx_flag 0; // 从环形缓冲区rx_buf中解析数据 if(parse_usb_command(rx_buf)) { // 解析成功例如更新PWM占空比 __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, pwm_duty); } } } }关键点与避坑指南USB发送非阻塞CDC_Transmit_FS是非阻塞的如果上一次发送未完成就再次调用会导致数据丢失。必须通过状态标志如CDC_Transmit_State或回调函数CDC_TransmitCplt_FS来管理发送流程。环形缓冲区是必须的USB接收回调CDC_Receive_FS可能在任意时刻被调用且数据可能是分包的。必须使用环形缓冲区暂存数据在主循环或定时中断中解析避免在中断服务函数中做复杂的解析操作。定时器中断优先级确保TIM2的中断优先级高于USB中断优先级。否则当USB通信繁忙时可能会阻塞定时器中断破坏1ms周期的稳定性。电源稳定性使用USB供电时确保电脑USB口供电充足。对于驱动电机等有较大电流波动的负载强烈建议为STM32板子使用独立、稳定的电源并将地与电脑地共接仅用USB线通信以避免电源噪声导致单片机复位或通信错误。4. Simulink模型搭建算法、通信与可视化STM32端准备就绪后我们在Simulink中构建测试模型。这个模型需要完成三件事通过USB与STM32通信、执行控制算法、可视化数据。4.1 配置串口通信模块与协议解析Simulink与USB虚拟串口的交互主要通过“Instrument Control Toolbox”中的Serial Receive和Serial Send模块实现。如果没有这个工具箱也可以使用MATLAB Function模块调用底层的fread/fwrite函数但实时性更难保证。建立串口连接在MATLAB命令行中使用serialportlist命令找到STM32虚拟出的串口号如COM3或/dev/ttyACM0。在Simulink库浏览器中找到Serial Receive和Serial Send模块拖入模型。双击Serial Receive模块设置端口为COM3波特率Baud rate实际上对USB CDC类设备无效但必须设置一个值通常保持默认即可。最关键的是设置“Data Size”和“Sample Time”。“Data Size”应等于我们一帧数据的长度例如8字节。“Sample Time”必须与模型的基础采样时间例如0.001秒即1kHz严格一致或者设为-1以继承模型采样时间。解析与封装数据帧 从Serial Receive模块输出的是uint8类型的原始字节流。我们需要用MATLAB Function模块或Embedded MATLAB Function模块来解析它。% 示例解析STM32上传的ADC数据帧 function adc_value parse_adc_frame(data) % data: 输入的一维uint8数组长度为8 persistent buffer; % 使用持久变量处理可能的粘包 if isempty(buffer) buffer []; end buffer [buffer; data]; % 将新数据加入缓冲区 adc_value single(0); % 初始化为0 idx 1; while length(buffer) 8 % 至少有一帧数据 if buffer(idx) 0xAA buffer(idx1) 0x55 cmd buffer(idx2); if cmd 0x02 % 确认是ADC数据命令 % 提取数据并计算校验和 data_high buffer(idx3); data_low buffer(idx4); rx_checksum buffer(idx5); calc_checksum mod(sum(buffer(idx2:idx4)), 256); if rx_checksum calc_checksum % 校验成功转换数据 adc_value single(bitshift(data_high, 8) data_low); % 移除已处理帧 buffer(1:idx7) []; break; % 找到一帧就退出避免复杂处理 end end end idx idx 1; if idx length(buffer) - 7 buffer(1:idx-1) []; % 清除无效头部 break; end end end同样我们需要一个封装函数将Simulink计算出的PWM指令如一个0-1之间的浮点数打包成协议帧送给Serial Send模块。实操心得Serial Receive模块的“Data Size”设置非常关键。如果设置过小会收不到完整帧设置过大会引入不必要的延迟。最佳实践是将其设置为单帧数据的精确长度。同时在模型属性Modeling - Model Settings - Solver中将求解器类型Solver type设置为固定步长Fixed-step并选择离散求解器如discrete。步长Fixed-step size设置为与通信周期一致如0.001。这是保证Simulink模型实时运行的基础。4.2 构建控制算法与闭环测试模型现在我们搭建一个简单的闭环测试系统。例如一个直流电机的速度控制。被控对象模拟STM32硬件部分代表真实的电机和驱动器。我们假设STM32的PWM驱动一个电机电机的转速通过一个模拟的编码器或通过电机反电动势由ADC采样得到电压信号并上传给Simulink。Simulink控制器在Simulink中我们设计一个PID控制器。输入来自parse_adc_frame函数转换后的ADC值经过一个标度变换模块Gain将其转换为速度反馈值例如0-4095对应0-1000 RPM。设定点使用一个Constant或Signal Builder模块提供目标速度。PID计算使用Simulink自带的PID Controller模块。需要仔细整定P、I、D参数。这里有个技巧可以先在纯Simulink仿真中用一个近似的传递函数模型如一阶惯性环节代替真实电机初步整定PID参数。然后在HIL测试中以此为基础进行微调。输出限幅PID输出经过一个Saturation模块限制在0-1之间然后乘以PWM周期值如1000得到整数占空比指令。指令发送将占空比指令送入pack_pwm_frame函数打包输出给Serial Send模块。数据记录与可视化使用Scope模块同时观察目标速度、实际速度、控制输出PWM占空比和误差。为了后期分析强烈建议使用To Workspace模块将关键信号记录到MATLAB工作区。设置其保存格式为Timeseries或Array并勾选“Log data to workspace”。4.3 模型参数配置与实时性优化要让模型尽可能“实时”地运行需要进行一些关键配置求解器设置如前所述使用固定步长、离散求解器。步长是关键它决定了模型与硬件交互的频率。这个步长必须大于等于模型完成一次计算和通信所需的最长时间。可以从一个保守值开始如0.01秒逐步缩小直到出现通信超时或丢帧。硬件执行时间估计在Simulink的“Diagnostics - Sample Time”中将颜色设置为“All”。运行模型后不同颜色的区块代表了不同的采样率。确保整个信号链从串口接收到串口发送是单一的、最快的采样率颜色。如果出现多速率可能会引入额外的延迟和同步问题。对于HIL通常整个模型使用同一个最快的采样时间。禁用非必要模块在HIL测试时关闭所有用于纯仿真的显示模块如Spectrum Analyzer或者将其更新速率调至很低。图形渲染会消耗大量CPU时间破坏实时性。提升PC端优先级高级操作在Windows系统中可以通过任务管理器将MATLAB进程的优先级设置为“高于正常”或“高”。在模型初始化脚本中甚至可以尝试调用系统命令来提升实时性此操作有风险需谨慎。5. 联合调试、问题排查与实战技巧将编译好的STM32固件下载到板子连接USB线打开Simulink模型点击运行——理想情况下你应该能看到Scope中的曲线开始跟随设定值变化。但现实往往是骨感的下面是我总结的常见问题排查清单。5.1 通信链路建立失败症状Simulink报错提示无法打开串口或超时。排查步骤确认串口号拔插USB线在设备管理器中确认STM32虚拟出的COM口编号。注意这个编号可能会变。检查独占访问确保没有其他软件如串口助手、旧的MATLAB命令行占用了该COM口。关闭所有可能冲突的程序。测试STM32端使用一个简单的串口助手工具如Putty、AccessPort以正确的波特率任意如115200打开COM口。如果STM32程序正确你应该能看到它定期发送的十六进制数据帧。如果能收到数据说明STM32端正常。测试Simulink端可以创建一个最简单的模型仅包含Serial Receive模块和一个Display模块尝试接收数据。如果这里能收到说明Simulink配置基本正确。5.2 数据错乱或解析失败症状Scope中数据跳动剧烈、出现不可能的值如NaN或极大值或者控制完全失灵。排查步骤协议同步检查在串口助手中以十六进制模式查看收发数据。对比Simulink发送的帧和STM32发送的帧是否符合协议格式帧头、帧尾、长度。最常见的问题是粘包和拆包。确保Simulink的发送周期和STM32的接收解析周期匹配且解析函数有足够的容错能力如前面代码中的环形缓冲区和对齐逻辑。校验和验证在解析函数中加入校验和错误计数。如果错误率很高可能是通信受到干扰或者PC端CPU负载过高导致数据丢失。尝试加长Simulink的固定步长。数据类型转换仔细检查Simulink中MATLAB Function模块的输入输出数据类型以及数据标度变换。例如STM32发送的ADC是uint16Simulink中可能需要先转换为double或single再乘以一个系数转换为物理量。信号接地与滤波如果ADC采样的是真实的模拟信号如电机电流信号线上的噪声会被引入控制系统。在硬件上确保模拟地AGND和数字地DGND单点连接在信号输入端加入RC低通滤波。在软件Simulink中可以对ADC反馈信号加入一阶低通滤波模块。5.3 系统运行不稳定或延迟大症状控制响应缓慢或者运行一段时间后Simulink报错“模型执行时间超过步长”。排查步骤测量实际步长时间在Simulink模型中添加一个Clock模块和To Workspace模块记录仿真时间。运行结束后在MATLAB中计算相邻时间戳的差值。这个差值应该非常接近你设定的固定步长。如果波动很大或明显偏大说明模型计算超时。简化模型关闭所有Scope的显示右键Scope取消勾选“Open at simulation start”将To Workspace的采样间隔调大。使用Profile工具在MATLAB命令行输入sim(模型名, SimulationMode, normal); profile viewer;分析模型中各模块的计算耗时优化或移除耗时大户。检查PC性能关闭不必要的后台程序。确保MATLAB有足够的CPU和内存资源。调整通信频率如果控制对象动态响应较慢如温控系统没必要追求1kHz的通信频率。将Simulink步长和STM32定时器中断周期同步降低到100Hz或50Hz可以大幅提高系统稳定性和鲁棒性。5.4 进阶技巧使用Simulink Coder实现更高效的通信对于性能要求更高的场景可以放弃Instrument Control Toolbox转而使用Simulink Coder生成一个独立的可执行程序。这个程序通过调用操作系统底层的API与USB通信实时性远高于运行在MATLAB环境下的模型。基本思路是创建一个S-Function内部用C语言调用系统串口APIWindows的CreateFile/ReadFileLinux的open/read或libusb库。在Simulink模型中使用这个自定义S-Function模块进行通信。使用Simulink Coder将整个模型包括这个S-Function生成C代码并编译成一个独立的桌面应用程序。 这个程序可以以更高的优先级运行几乎不受MATLAB图形界面和其他任务的影响能实现接近硬实时的性能。当然这需要更多的C语言和系统编程知识。6. 从例程到实际项目扩展与应用场景掌握了基本的HIL测试框架后你可以将其扩展到更复杂的系统中。多通道与复杂被控对象STM32可以同时读取多路ADC电流、电压、温度、编码器接口输出多路PWM、DAC、控制继电器等。只需在协议中扩展命令字和数据字段在Simulink端增加对应的解析和封装逻辑即可。状态机与逻辑测试除了模拟量控制HIL也非常适合测试复杂的逻辑状态机。例如测试一个电池管理系统的充放电逻辑Simulink模拟电池模型和负载需求STM32根据指令控制真实的MOSFET开关和继电器并反馈状态。可以验证各种正常和故障工况下的逻辑是否正确。与更高级仿真软件联合Simulink可以作为中间件。例如在Carsim车辆动力学软件中模拟整车Carsim输出车辆状态速度、横摆角给SimulinkSimulink中的控制器算法计算出转向或制动指令再通过HIL发送给真实的EPS电动助力转向或ESP车身稳定系统控制器进行测试。这就构成了一个更复杂的、跨软件的HIL系统。自动化测试利用Simulink的Test Manager或编写MATLAB脚本可以自动运行一系列测试用例如阶跃响应、正弦跟踪、故障注入并自动收集数据、生成测试报告极大提升验证效率。我个人最深的体会是HIL测试的价值不仅在于“测试”更在于“开发”。它提供了一个算法与硬件并行开发的沙盒。硬件工程师可以专注于驱动和电路的稳定性而算法工程师可以基于一个稳定的硬件接口尽情迭代和优化控制策略。两者之间的交互通过这套HIL系统清晰定义任何问题都能快速定位是在模型端还是硬件端。当最终将算法从Simulink生成代码并集成到STM32时你会发现大部分令人头疼的兼容性问题早已在HIL阶段被解决项目进度会变得异常顺畅。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价