4、单片机模型建立:基于Simulink的MCU外设建模
好,咱们进入第四章节。前面几章我们把HIL(硬件在环)的整体框架和异常处理机制聊清楚了。这一章,咱们得动真格的了——在Simulink里把单片机的外设模型搭起来。
你可能会问:“我直接用真实单片机跑不就行了?干嘛还要建模型?”嗯,这个问题我当年也问过自己。直到有一次,我在一个电机控制项目里,ADC采样时序没调好,结果上电一瞬间电流直接爆表,板子烧了。从那以后,我深刻体会到:在仿真阶段把外设行为摸透,比在硬件上“试错”要划算得多。
4.1 为什么要在Simulink里建MCU外设模型?
说白了,HIL仿真的核心就是“骗”过你的控制算法,让它以为自己在跟真实硬件打交道。而外设模型,就是这场“骗局”的关键道具。
我个人习惯把外设模型分成三类:
| 模型类型 | 典型外设 | 建模目的 |
|---|---|---|
| 输入型 | ADC、GPIO输入、捕获 | 模拟传感器信号进入MCU |
| 输出型 | PWM、GPIO输出、DAC | 模拟MCU控制信号驱动执行器 |
| 通信型 | CAN、UART、SPI、I2C | 模拟总线上的数据交换 |
你想想看,如果连ADC的采样时序、PWM的分辨率、CAN的帧格式都没在模型里体现出来,那你的HIL仿真就只是个“花架子”。
4.2 ADC模块建模:从采样到量化
ADC建模,核心就两件事:采样时序和量化精度。
4.2.1 采样保持与触发源
真实MCU的ADC不是随时都能采的。它需要一个触发源——可能是定时器、PWM同步信号,或者软件手动触发。我在模型里一般这样处理:
// 伪代码:ADC采样触发逻辑
if (ADC_Trigger_Edge == RISING) {
sample_hold = analog_input; // 采样保持
start_conversion(); // 启动转换
}
在Simulink里,我用一个触发子系统(Triggered Subsystem)来实现这个行为。触发信号来自一个定时器模块,模拟MCU内部定时器的周期触发。
关键参数:
- 采样率:12位ADC,转换时间一般在1us左右
- 分辨率:2^12 = 4096级,对应0~3.3V
- 量化误差:±0.5 LSB
4.2.2 量化与编码
模拟信号进来,得变成数字码。这个步骤我用一个Quantizer模块加一个Gain模块搞定:
ADC_Code = floor( (Vin / Vref) * (2^N - 1) );
// N = 12, Vref = 3.3V
// 输入1.65V -> 输出2047
我曾经踩过一个坑:量化器直接用了round而不是floor。结果仿真出来的ADC码和实际芯片对不上,查了半天才发现是舍入方式不一致。嗯,这里要注意,大多数MCU的ADC是向下取整的。
4.3 PWM模块建模:分辨率与死区
PWM模型,说白了就是生成一个占空比可调的方波。但真实MCU的PWM远没那么简单——它有时基计数器、比较寄存器、死区插入、极性控制等等。
4.3.1 时基与比较逻辑
我习惯用Counter Free-Running模块模拟PWM的时基计数器。假设PWM频率为20kHz,计数器时钟为100MHz,那么周期值就是5000。
// PWM生成逻辑
counter = counter + 1;
if (counter >= PERIOD) counter = 0;
if (counter <= CMP_VALUE) {
PWM_OUT = HIGH;
} else {
PWM_OUT = LOW;
}
小技巧:在Simulink里,用Relational Operator模块配合Counter模块,就能搭出一个纯逻辑的PWM生成器。不需要写S-Function,调试起来也方便。
4.3.2 死区时间建模
驱动H桥或三相逆变器时,上下管不能同时导通。死区时间就是插入的一段“全关”时间。我在模型里用Transport Delay模块加逻辑门来实现:
// 死区插入
PWM_H = PWM_original;
PWM_L = ~PWM_original_delayed;
// 延迟时间 = 死区时间(比如2us)
我记得有一次,死区时间设得太短,仿真波形看着没问题,但实际板子一跑就炸管。后来才意识到,模型里没考虑功率管的关断延迟。所以,死区模型一定要留有余量。
4.4 CAN模块建模:帧结构与总线仲裁
CAN通信建模,是外设模型里最复杂的之一。因为它涉及协议层和物理层两层行为。
4.4.1 CAN帧结构
标准CAN帧包含:SOF、仲裁场、控制场、数据场、CRC、ACK、EOF。我在模型里用MATLAB Function模块来打包和解包:
function can_frame = pack_can_frame(id, data, dlc)
can_frame.id = id; // 11位或29位
can_frame.dlc = dlc; // 数据长度
can_frame.data = data; // 0~8字节
can_frame.crc = crc16(data); // 简单CRC
end
4.4.2 总线仲裁与错误处理
CAN总线的仲裁机制,说白了就是“谁优先级低谁退让”。我在模型里用一个Stateflow状态机来模拟:
- IDLE:总线空闲,任何节点都可以发送
- ARBITRATION:多个节点同时发送,比较ID优先级
- TRANSMIT:赢得仲裁的节点发送完整帧
- ERROR:检测到位错误或填充错误,发送错误帧
注意:很多初学者在CAN模型里只做了“数据收发”,忽略了总线错误处理。我曾经在一个项目中,仿真时CAN通信一切正常,但联调时发现真实总线上有大量错误帧,导致系统频繁复位。后来在模型里加入了错误计数和Bus-Off状态,才把问题复现出来。
4.5 整体外设模型架构
下面这张图,是我个人常用的MCU外设模型架构。它把ADC、PWM、CAN三个模块整合到一个子系统里,方便后续与HIL平台对接。
这个架构的好处是:每个外设模型都是独立的。你可以先单独测试ADC模型,再测PWM,最后联调CAN。出了问题,定位也快。
4.6 模型验证:仿真 vs 实测
模型建好了,怎么知道它准不准?我的做法是:拿真实MCU的波形来对标。
| 验证项 | 仿真结果 | 实测结果 | 误差 |
|---|---|---|---|
| ADC采样值(1.65V输入) | 2047 | 2046 | 0.05% |
| PWM频率(20kHz设定) | 20.000kHz | 19.998kHz | 0.01% |
| CAN帧发送间隔(1ms) | 1.000ms | 1.002ms | 0.2% |
核心原则:模型不需要100%精确,但关键行为必须一致。比如ADC的量化误差、PWM的死区效应、CAN的仲裁机制——这些“行为级”特征,才是HIL仿真真正需要的东西。