🚀 Simulink 端口自动生成工具:告别重复劳动的高效神器
在 Simulink 建模过程中,当面对成百上千个输入输出信号时,手动一个个添加端口(Inport/Outport)并配置参数不仅枯燥,而且极易出错。今天我们要介绍的这款 Simulink 端口自动生成工具 (v2.1),就是为了解决这一痛点而生的。
这款工具通过读取 Excel 配置表,自动完成模型构建、端口添加、参数映射和布局整理,将繁琐的体力活变成了简单的"一键操作"。
🛠️ 核心功能一览
这款工具不仅仅是一个简单的脚本,它具备完整的工程化能力,主要包含以下核心功能:
| 功能模块 | 描述 |
|---|---|
| 智能交互 | 自动检测 Excel 工作表,支持用户选择;提供详细的日志输出(Verbose模式)。 |
| 数据清洗 | 自动跳过空行、重复行检查、非法字符过滤,确保数据质量。 |
| 参数映射 | 支持从 Excel 映射数据类型、宽度、上下限、单位、初始值及存储类等丰富属性。 |
| 自动布局 | 生成端口后,自动垂直排列端口,并支持调用 Simulink 自动布线功能。 |
⚙️ 工作流程拆解
该工具的执行逻辑非常严谨,主要分为以下 5 个步骤 (对应代码中的 s2 函数):
-
配置获取 (
getConfiguration):- 弹窗让用户选择 Excel 文件。
- 自动检测工作表,若多于一个则提示用户选择,否则自动匹配。
- 设置默认参数(如模型名
myModel,子系统路径Input_Ports)。
-
数据读取 (
readExcelData):- 使用
detectImportOptions读取表格,保留原始列名。 - 强制校验 :必须包含
Name列,否则报错。
- 使用
-
验证与清理 (
validateAndCleanData):- 去空 :移除
Name列为空、缺失或空白字符的行。 - 去重:检查端口名称是否重复。
- 合规检查 :确保名称中不包含 Simulink 不允许的特殊字符(如
\,/,*等)。
- 去空 :移除
-
模型构建 (
prepareModel&addPortsToModel):- 检查模型是否存在,不存在则创建;检查子系统是否存在,不存在则递归创建嵌套子系统。
- 参数映射循环 :遍历每一行数据,将 Excel 中的
Datatype,Width,Min,Max,Unit,InitialValue,Storageclass等映射到 Simulink Block 的OutDataTypeStr,PortDimensions,SignalAttributesMin等属性。
-
整理与保存 (
finalizeModel):- 调用
arrangePortsVertically将所有端口垂直对齐排列。 - 调用 Simulink 自带的
Simulink.BlockDiagram.arrangeSystem进行自动布线。 - 自动保存模型。
- 调用
📊 支持的 Excel 映射字段
为了让你更好地准备配置表,以下是该工具支持的 Excel 列名及其对应的 Simulink 属性:
- Name (必填):端口名称。
- Datatype :映射为
OutDataTypeStr。 - Width :映射为
PortDimensions。 - Min / Max :映射为
SignalAttributesMin/Max。 - Unit :映射为
Units。 - InitialValue :映射为
InitialCondition。 - Storageclass :映射为
SignalStorageClass(用于代码生成配置)。
💡 亮点与容错机制
- 优雅的错误处理 :代码中大量使用了
try-catch,即使某一行数据出错(如果设置config.continueOnError = true),程序也会记录错误并继续执行下一行,不会导致整个任务崩溃。 - 灵活的列名匹配 :
findMatchingColumnName函数支持不区分大小写的列名匹配,甚至支持包含匹配(例如 Excel 列名为 "Port_Name" 也能匹配到代码要求的 "Name")。 - 可视化反馈 :控制台会实时输出
[✓]、[✗]、[⊘]等符号,清晰展示每一步的执行状态(成功、失败、跳过)。
📝 总结
这款工具虽然代码量不大,但结构清晰、健壮性强。它完美诠释了如何利用 MATLAB 脚本将"人工配置"转化为"自动化工程"。如果你正在处理复杂的 Simulink 模型集成,或者正在进行基于模型的设计,这款脚本无疑能为你节省大量的时间。

matlab
function s2()
clc;
clear;
close all;
fprintf('====================================\n');
fprintf('Simulink 端口自动生成工具 v2.1\n');
fprintf('====================================\n\n');
try
config = getConfiguration();
if isempty(config)
fprintf('\n====================================\n');
fprintf('程序已退出\n');
fprintf('====================================\n');
return;
end
portData = readExcelData(config);
portData = validateAndCleanData(portData, config);
modelHandle = prepareModel(config.modelName, config.subSystemPath);
addPortsToModel(portData, config);
finalizeModel(config.modelName, config.subSystemPath, config.autoSave);
fprintf('\n====================================\n');
fprintf('✓ 端口添加完成!\n');
fprintf('====================================\n');
catch ME
fprintf(2, '\n====================================\n');
fprintf(2, '✗ 错误: %s\n', ME.message);
if ~isempty(ME.stack)
fprintf(2, '位置: %s (行 %d)\n', ME.stack(1).name, ME.stack(1).line);
end
fprintf(2, '====================================\n');
rethrow(ME);
end
end
function config = getConfiguration()
config = struct();
fprintf('步骤 0: 选择 Excel 文件...\n');
[fileName, filePath] = uigetfile(...
{'*.xlsx;*.xls', 'Excel 文件 (*.xlsx, *.xls)'; ...
'*.xlsx', 'Excel 2007+ 文件 (*.xlsx)'; ...
'*.xls', 'Excel 97-2003 文件 (*.xls)'; ...
'*.*', '所有文件 (*.*)'}, ...
'选择端口定义 Excel 文件');
if isequal(fileName, 0)
fprintf(' 用户取消选择,程序退出\n');
config = struct();
return;
end
excelFullPath = fullfile(filePath, fileName);
fprintf(' ✓ 已选择文件: %s\n', excelFullPath);
try
[~, ~, ext] = fileparts(fileName);
if strcmpi(ext, '.xls')
[~, ~, sheets] = xlsfinfo(excelFullPath);
else
sheets = sheetnames(excelFullPath);
end
fprintf('\n 检测到 %d 个工作表:\n', length(sheets));
for i = 1:length(sheets)
fprintf(' %d. %s\n', i, sheets{i});
end
if length(sheets) == 1
config.sheetName = sheets{1};
fprintf('\n ✓ 自动选择工作表: %s\n', config.sheetName);
else
fprintf('\n 请输入工作表编号 (1-%d),直接回车选择第一个: ', length(sheets));
userInput = input('', 's');
if isempty(userInput)
config.sheetName = sheets{1};
else
sheetIndex = str2double(userInput);
if isnan(sheetIndex) || sheetIndex < 1 || sheetIndex > length(sheets)
fprintf(' ⚠ 输入无效,使用第一个工作表\n');
config.sheetName = sheets{1};
else
config.sheetName = sheets{sheetIndex};
end
end
fprintf(' ✓ 已选择工作表: %s\n', config.sheetName);
end
catch ME
fprintf(' ⚠ 无法读取工作表信息: %s\n', ME.message);
fprintf(' 使用默认工作表: Ports\n');
config.sheetName = 'Ports';
end
fprintf('\n');
config.excelFile = excelFullPath;
config.modelName = 'myModel';
config.subSystemPath = [config.modelName, '/Input_Ports'];
config.portType = 'Inport';
config.autoSave = true;
config.skipEmptyRows = true;
config.skipHeaderRows = 0;
config.verbose = true;
config.continueOnError = true;
fprintf('配置信息:\n');
fprintf(' ├─ Excel文件: %s\n', config.excelFile);
fprintf(' ├─ 工作表: %s\n', config.sheetName);
fprintf(' ├─ 目标模型: %s\n', config.modelName);
fprintf(' ├─ 目标子系统: %s\n', config.subSystemPath);
fprintf(' ├─ 端口类型: %s\n', config.portType);
fprintf(' ├─ 自动保存: %s\n', mat2str(config.autoSave));
fprintf(' └─ 跳过空行: %s\n\n', mat2str(config.skipEmptyRows));
end
function portData = readExcelData(config)
fprintf('步骤 1: 读取 Excel 数据...\n');
if ~exist(config.excelFile, 'file')
error('Excel文件不存在: %s\n请确保文件路径正确,或创建Excel文件。', config.excelFile);
end
try
opts = detectImportOptions(config.excelFile, 'Sheet', config.sheetName);
opts.VariableNamingRule = 'preserve';
portData = readtable(config.excelFile, opts);
fprintf(' ✓ 成功读取 %d 行数据\n', height(portData));
colNames = portData.Properties.VariableNames;
if iscell(colNames)
colNames = string(colNames);
end
if ~any(strcmpi(colNames, 'Name'))
error('Excel文件必须包含 "Name" 列\n当前列: %s', strjoin(portData.Properties.VariableNames, ', '));
end
fprintf(' ✓ 检测到的列: %s\n', strjoin(portData.Properties.VariableNames, ', '));
if config.verbose
fprintf('\n 数据预览 (前5行):\n');
fprintf(' %-5s %-20s %-15s %-10s\n', '行号', 'Name', 'Datatype', 'Width');
fprintf(' %s\n', repmat('-', 1, 55));
for i = 1:min(5, height(portData))
nameVal = getCellValue(portData, 'Name', i);
typeVal = getCellValue(portData, 'Datatype', i);
widthVal = getCellValue(portData, 'Width', i);
nameStr = formatValue(nameVal, '[空]');
typeStr = formatValue(typeVal, '-');
widthStr = formatValue(widthVal, '-');
fprintf(' %-5d %-20s %-15s %-10s\n', i, nameStr, typeStr, widthStr);
end
end
catch ME
if contains(ME.message, '无法')
error('无法读取工作表 "%s",请检查工作表名称是否正确', config.sheetName);
else
error('读取Excel文件失败: %s', ME.message);
end
end
end
function portData = validateAndCleanData(portData, config)
fprintf('\n步骤 2: 验证和清理数据...\n');
if height(portData) == 0
error('Excel文件中没有数据');
end
originalCount = height(portData);
fprintf(' 原始数据行数: %d\n', originalCount);
if config.verbose
fprintf('\n 原始数据详情 (前10行):\n');
fprintf(' %-4s %-20s %-10s\n', '行号', 'Name列值', '类型');
fprintf(' %s\n', repmat('-', 1, 40));
for i = 1:min(10, originalCount)
try
nameCol = portData.Name;
if iscell(nameCol)
val = nameCol{i};
else
val = nameCol(i);
end
if ismissing(val)
fprintf(' %-4d %-20s %-10s\n', i, '[missing]', class(val));
elseif isempty(val)
fprintf(' %-4d %-20s %-10s\n', i, '[empty]', 'empty');
elseif ischar(val)
fprintf(' %-4d "%-18s" %-10s\n', i, val, 'char');
elseif isstring(val)
fprintf(' %-4d "%-18s" %-10s\n', i, char(val), 'string');
elseif isnumeric(val)
fprintf(' %-4d %-20s %-10s\n', i, num2str(val), 'numeric');
else
fprintf(' %-4d %-20s %-10s\n', i, '[其他类型]', class(val));
end
catch ME
fprintf(' %-4d 读取错误: %s\n', i, ME.message);
end
end
end
if config.skipHeaderRows > 0
portData = portData((config.skipHeaderRows+1):end, :);
fprintf('\n ✓ 跳过 %d 行标题行,剩余 %d 行\n', config.skipHeaderRows, height(portData));
end
if config.skipEmptyRows
fprintf('\n 检测空行...\n');
emptyMask = false(height(portData), 1);
nameCol = portData.Name;
for i = 1:height(portData)
try
if iscell(nameCol)
val = nameCol{i};
else
val = nameCol(i);
end
if ismissing(val)
emptyMask(i) = true;
if config.verbose
fprintf(' 第 %d 行: [missing] -> 标记为空\n', i);
end
elseif isempty(val)
emptyMask(i) = true;
if config.verbose
fprintf(' 第 %d 行: [empty] -> 标记为空\n', i);
end
elseif (ischar(val) && isempty(strtrim(val)))
emptyMask(i) = true;
if config.verbose
fprintf(' 第 %d 行: [空白字符] -> 标记为空\n', i);
end
elseif (isstring(val) && isempty(strtrim(char(val))))
emptyMask(i) = true;
if config.verbose
fprintf(' 第 %d 行: [空白string] -> 标记为空\n', i);
end
else
emptyMask(i) = false;
if config.verbose && i <= 10
fprintf(' 第 %d 行: 有效数据\n', i);
end
end
catch ME
emptyMask(i) = true;
if config.verbose
fprintf(' 第 %d 行: 检测错误 -> 标记为空 (%s)\n', i, ME.message);
end
end
end
validCount = sum(~emptyMask);
emptyCount = sum(emptyMask);
fprintf('\n 检测结果: 有效 %d 行, 空行 %d 行\n', validCount, emptyCount);
if emptyCount > 0
portData = portData(~emptyMask, :);
fprintf(' ✓ 已移除 %d 行空数据\n', emptyCount);
end
end
if height(portData) == 0
fprintf('\n ✗ 错误: 所有数据行都被识别为空行!\n');
fprintf(' 可能原因:\n');
fprintf(' 1. Excel第一行是标题,需要设置 skipHeaderRows = 1\n');
fprintf(' 2. Name列数据格式不正确\n');
fprintf(' 3. Excel文件使用了特殊格式\n');
fprintf('\n 建议: 设置 config.verbose = true 查看详细数据\n');
error('清理后没有有效数据,请检查Excel文件');
end
portNames = portData.Name;
if iscell(portNames)
portNames = string(portNames);
end
duplicateNames = portNames(portNames ~= "");
if length(duplicateNames) ~= length(unique(duplicateNames))
duplicates = duplicateNames(duplicateNames ~= unique(duplicateNames));
error('存在重复的端口名称: %s\n请检查Excel数据', strjoin(unique(duplicates), ', '));
end
invalidChars = {'/', '\', ':', '*', '?', '"', '<', '>', '|'};
for i = 1:height(portData)
pName = getCellValue(portData, 'Name', i);
if ismissing(pName)
continue;
end
if isempty(pName)
continue;
end
pNameStr = char(pName);
for j = 1:length(invalidChars)
if contains(pNameStr, invalidChars{j})
error('端口名称 "%s" 包含非法字符 "%s"', pNameStr, invalidChars{j});
end
end
end
fprintf(' ✓ 数据验证通过\n');
fprintf(' ✓ 有效端口数: %d/%d\n', height(portData), originalCount);
end
function modelHandle = prepareModel(modelName, subSystemPath)
fprintf('\n步骤 3: 准备模型...\n');
if bdIsLoaded(modelName)
fprintf(' ✓ 模型已加载: %s\n', modelName);
else
fprintf(' + 创建新模型: %s\n', modelName);
new_system(modelName);
end
open_system(modelName);
if ~strcmp(subSystemPath, modelName)
try
get_param(subSystemPath, 'Handle');
fprintf(' ✓ 子系统已存在: %s\n', subSystemPath);
catch
fprintf(' + 创建子系统: %s\n', subSystemPath);
createNestedSubsystem(modelName, subSystemPath);
end
end
modelHandle = get_param(modelName, 'Handle');
fprintf(' ✓ 模型准备完成\n');
end
function createNestedSubsystem(modelName, subSystemPath)
parentPath = modelName;
subsystemName = strrep(subSystemPath, [modelName, '/'], '');
if contains(subsystemName, '/')
parts = strsplit(subsystemName, '/');
for i = 1:length(parts)-1
parentPath = [parentPath, '/', parts{i}];
try
get_param(parentPath, 'Handle');
catch
add_block('built-in/Subsystem', parentPath);
end
end
subsystemName = parts{end};
end
add_block('built-in/Subsystem', [parentPath, '/', subsystemName]);
end
function addPortsToModel(portData, config)
fprintf('\n步骤 4: 添加端口到模型...\n');
if strcmp(config.portType, 'Inport')
blockType = 'built-in/Inport';
portDirection = '输入';
else
blockType = 'built-in/Outport';
portDirection = '输出';
end
fprintf(' 使用模块类型: %s\n', blockType);
successCount = 0;
errorCount = 0;
skipCount = 0;
totalPorts = height(portData);
for i = 1:totalPorts
pName = getCellValue(portData, 'Name', i);
if ismissing(pName)
skipCount = skipCount + 1;
if config.verbose
fprintf(' ⊘ [%d/%d] 第 %d 行端口名称为空,跳过\n', i, totalPorts, i);
end
continue;
end
if isempty(pName)
skipCount = skipCount + 1;
if config.verbose
fprintf(' ⊘ [%d/%d] 第 %d 行端口名称为空,跳过\n', i, totalPorts, i);
end
continue;
end
pNameStr = char(pName);
blockPath = [config.subSystemPath, '/', pNameStr];
blockExists = false;
try
get_param(blockPath, 'Handle');
blockExists = true;
catch
blockExists = false;
end
if blockExists
skipCount = skipCount + 1;
if config.verbose
fprintf(' ⊘ [%d/%d] 端口 "%s" 已存在,跳过\n', i, totalPorts, pNameStr);
end
continue;
end
try
add_block(blockType, blockPath);
set_param(blockPath, 'Port', num2str(i));
setPortAttribute(portData, i, blockPath, 'Datatype', 'OutDataTypeStr');
setPortAttribute(portData, i, blockPath, 'Width', 'PortDimensions');
setPortAttribute(portData, i, blockPath, 'Min', 'SignalAttributesMin');
setPortAttribute(portData, i, blockPath, 'Max', 'SignalAttributesMax');
setPortAttribute(portData, i, blockPath, 'Unit', 'Units');
setPortAttribute(portData, i, blockPath, 'InitialValue', 'InitialCondition');
setPortAttribute(portData, i, blockPath, 'Discription', 'Description');
colNames = portData.Properties.VariableNames;
if iscell(colNames)
colNames = string(colNames);
end
if any(strcmpi(colNames, 'Storageclass'))
storageClass = getCellValue(portData, 'Storageclass', i);
if ismissing(storageClass)
% 跳过
elseif isempty(storageClass)
% 跳过
else
try
set_param(blockPath, 'SignalStorageClass', char(storageClass));
catch
if config.verbose
fprintf(' ⚠ [%d/%d] 端口 "%s" 存储类设置失败\n', i, totalPorts, pNameStr);
end
end
end
end
successCount = successCount + 1;
if config.verbose
fprintf(' ✓ [%d/%d] 已添加%s端口: %s\n', i, totalPorts, portDirection, pNameStr);
end
catch ME
errorCount = errorCount + 1;
if config.continueOnError
fprintf(' ✗ [%d/%d] 添加端口 "%s" 失败: %s\n', i, totalPorts, pNameStr, ME.message);
else
error('添加端口 "%s" 失败: %s', pNameStr, ME.message);
end
end
end
fprintf('\n 统计:\n');
fprintf(' ✓ 成功: %d\n', successCount);
if skipCount > 0
fprintf(' ⊘ 跳过: %d\n', skipCount);
end
if errorCount > 0
fprintf(' ✗ 失败: %d\n', errorCount);
end
end
function value = getCellValue(portData, colName, rowIndex)
value = missing;
actualColName = findMatchingColumnName(portData, colName);
if isempty(actualColName)
return;
end
try
colData = portData.(actualColName);
if iscell(colData)
tempValue = colData{rowIndex};
elseif isstring(colData)
tempValue = colData(rowIndex);
else
tempValue = colData(rowIndex);
if ismissing(tempValue)
% 已经是 missing
elseif ischar(tempValue)
% 已经是 char
elseif isstring(tempValue)
% 已经是 string
else
tempValue = colData{rowIndex};
end
end
if ismissing(tempValue)
return;
end
if isempty(tempValue)
return;
end
if iscell(tempValue)
if isempty(tempValue)
return;
end
if isempty(tempValue{1})
return;
end
value = tempValue{1};
else
value = tempValue;
end
catch ME
return;
end
end
function actualColName = findMatchingColumnName(portData, targetColName)
actualColName = '';
allColNames = portData.Properties.VariableNames;
for i = 1:length(allColNames)
if strcmpi(allColNames{i}, targetColName)
actualColName = allColNames{i};
return;
end
end
lowerTarget = lower(targetColName);
for i = 1:length(allColNames)
if contains(lower(allColNames{i}), lowerTarget)
actualColName = allColNames{i};
return;
end
end
end
function str = formatValue(value, defaultStr)
if ismissing(value)
str = defaultStr;
elseif isempty(value)
str = defaultStr;
elseif isnumeric(value)
str = num2str(value);
else
try
str = char(value);
catch
str = defaultStr;
end
end
end
function setPortAttribute(portData, rowIndex, blockPath, excelColName, paramName)
colNames = portData.Properties.VariableNames;
if iscell(colNames)
colNames = string(colNames);
end
if ~any(strcmpi(colNames, excelColName))
return;
end
value = getCellValue(portData, excelColName, rowIndex);
if ismissing(value)
return;
end
if isempty(value)
return;
end
try
if isnumeric(value)
set_param(blockPath, paramName, num2str(value));
else
set_param(blockPath, paramName, char(value));
end
catch
end
end
function finalizeModel(modelName, subSystemPath, autoSave)
fprintf('\n步骤 5: 整理和保存模型...\n');
arrangePortsVertically(subSystemPath);
try
Simulink.BlockDiagram.arrangeSystem(subSystemPath);
fprintf(' ✓ 已自动排列布局\n');
catch
fprintf(' ⚠ 自动排列失败,请手动调整布局\n');
end
if autoSave
try
save_system(modelName);
fprintf(' ✓ 模型已保存: %s\n', modelName);
catch ME
fprintf(' ⚠ 保存失败: %s\n', ME.message);
end
end
fprintf(' ✓ 模型准备就绪\n');
end
function arrangePortsVertically(subSystemPath)
try
blocks = find_system(subSystemPath, 'SearchDepth', 1, 'BlockType', 'Inport');
outportBlocks = find_system(subSystemPath, 'SearchDepth', 1, 'BlockType', 'Outport');
blocks = [blocks; outportBlocks];
if isempty(blocks)
return;
end
numBlocks = length(blocks);
portWidth = 30;
portHeight = 15;
startX = 20;
startY = 30;
spacingY = 40;
for i = 1:numBlocks
try
yPos = startY + (i - 1) * spacingY;
position = [startX, yPos, startX + portWidth, yPos + portHeight];
set_param(blocks{i}, 'Position', position);
catch
end
end
fprintf(' ✓ 已自动排列 %d 个端口\n', numBlocks);
catch
end
end