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平台对接。

MCU外设模型架构图 模拟信号输入 PWM控制指令 CAN消息输入 ADC模型 采样保持+量化 PWM模型 时基+比较+死区 CAN模型 帧打包+仲裁+错误 寄存器映射层(ADC_DR / PWM_CCR / CAN_TX) 将模型输出映射为MCU内存地址空间 HIL平台接口(I/O映射) 说明 每个外设模型 独立封装 可单独测试

这个架构的好处是:每个外设模型都是独立的。你可以先单独测试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仿真真正需要的东西。