Simulink 端口自动生成工具 (v2.1)(EXCEL+m语言)

在 Simulink 建模过程中,当面对成百上千个输入输出信号时,手动一个个添加端口(Inport/Outport)并配置参数不仅枯燥,而且极易出错。今天我们要介绍的这款 Simulink 端口自动生成工具 (v2.1),就是为了解决这一痛点而生的。

这款工具通过读取 Excel 配置表,自动完成模型构建、端口添加、参数映射和布局整理,将繁琐的体力活变成了简单的"一键操作"。

🛠️ 核心功能一览

这款工具不仅仅是一个简单的脚本,它具备完整的工程化能力,主要包含以下核心功能:

功能模块 描述
智能交互 自动检测 Excel 工作表,支持用户选择;提供详细的日志输出(Verbose模式)。
数据清洗 自动跳过空行、重复行检查、非法字符过滤,确保数据质量。
参数映射 支持从 Excel 映射数据类型、宽度、上下限、单位、初始值及存储类等丰富属性。
自动布局 生成端口后,自动垂直排列端口,并支持调用 Simulink 自动布线功能。
⚙️ 工作流程拆解

该工具的执行逻辑非常严谨,主要分为以下 5 个步骤 (对应代码中的 s2 函数):

  1. 配置获取 (getConfiguration)

    • 弹窗让用户选择 Excel 文件。
    • 自动检测工作表,若多于一个则提示用户选择,否则自动匹配。
    • 设置默认参数(如模型名 myModel,子系统路径 Input_Ports)。
  2. 数据读取 (readExcelData)

    • 使用 detectImportOptions 读取表格,保留原始列名。
    • 强制校验 :必须包含 Name 列,否则报错。
  3. 验证与清理 (validateAndCleanData)

    • 去空 :移除 Name 列为空、缺失或空白字符的行。
    • 去重:检查端口名称是否重复。
    • 合规检查 :确保名称中不包含 Simulink 不允许的特殊字符(如 \, /, * 等)。
  4. 模型构建 (prepareModel & addPortsToModel)

    • 检查模型是否存在,不存在则创建;检查子系统是否存在,不存在则递归创建嵌套子系统。
    • 参数映射循环 :遍历每一行数据,将 Excel 中的 Datatype, Width, Min, Max, Unit, InitialValue, Storageclass 等映射到 Simulink Block 的 OutDataTypeStr, PortDimensions, SignalAttributesMin 等属性。
  5. 整理与保存 (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
相关推荐
不会写DN2 小时前
为什么TCP是三次握手?
服务器·网络·网络协议·tcp/ip
angushine2 小时前
gitlab跨服务器备份
服务器·gitlab·github
爱学习的小囧2 小时前
ESXi CPU 使用率高怎么排查?esxtop 一键定位占用高的虚拟机与进程
java·linux·运维·服务器·网络·虚拟化
Fanfanaas2 小时前
Linux 进程篇 (四)
linux·运维·服务器·开发语言·c++·学习
复园电子2 小时前
电子签章系统选型方法论:SaaS、私有部署、API接口版怎么选
服务器·网络·lims系统
发发就是发2 小时前
触摸屏驱动调试手记:从I2C鬼点到坐标漂移的实战录
linux·服务器·驱动开发·单片机·嵌入式硬件
Jacob程序员2 小时前
Linux 下启动达梦数据库 Manager 图形化客户端
linux·运维·服务器
IMPYLH2 小时前
Linux 的 pwd 命令
linux·运维·服务器·bash