From 1bd5009b9d2de267a28a0e5d6fe8dec28fbf01d8 Mon Sep 17 00:00:00 2001 From: Razvalyaev Date: Sat, 14 Jun 2025 19:51:05 +0300 Subject: [PATCH] release 1.0 --- McuLib/install_my_library.m | 47 ++ McuLib/lib/MCU.c | 209 ++++++ McuLib/lib/McuLib.slx | Bin 0 -> 22959 bytes McuLib/m/asynchManage.m | 104 +++ McuLib/m/customtable.m | 157 +++++ McuLib/m/editCode.m | 172 +++++ McuLib/m/init.m | 120 ++++ McuLib/m/mcuMask.m | 641 ++++++++++++++++++ McuLib/m/mcuPorts.m | 284 ++++++++ McuLib/m/mexing.m | 377 ++++++++++ McuLib/m/periphConfig.m | 414 +++++++++++ McuLib/mcuwrapper.prj | 123 ++++ McuLib/sl_customization.m | 5 + McuLib/slblocks.m | 5 + McuLib/startup.m | 2 + McuLib/templates/MCU_Wrapper/mcu_wrapper.c | 250 +++++++ .../templates/MCU_Wrapper/mcu_wrapper_conf.h | 219 ++++++ McuLib/templates/MCU_Wrapper/run_mex.bat | 115 ++++ McuLib/templates/app_wrapper/app_configs.h | 10 + McuLib/templates/app_wrapper/app_includes.h | 15 + McuLib/templates/app_wrapper/app_init.c | 33 + McuLib/templates/app_wrapper/app_io.c | 50 ++ McuLib/templates/app_wrapper/app_wrapper.c | 22 + McuLib/templates/app_wrapper/app_wrapper.h | 12 + mcuwrapper.mltbx | Bin 0 -> 73515 bytes 25 files changed, 3386 insertions(+) create mode 100644 McuLib/install_my_library.m create mode 100644 McuLib/lib/MCU.c create mode 100644 McuLib/lib/McuLib.slx create mode 100644 McuLib/m/asynchManage.m create mode 100644 McuLib/m/customtable.m create mode 100644 McuLib/m/editCode.m create mode 100644 McuLib/m/init.m create mode 100644 McuLib/m/mcuMask.m create mode 100644 McuLib/m/mcuPorts.m create mode 100644 McuLib/m/mexing.m create mode 100644 McuLib/m/periphConfig.m create mode 100644 McuLib/mcuwrapper.prj create mode 100644 McuLib/sl_customization.m create mode 100644 McuLib/slblocks.m create mode 100644 McuLib/startup.m create mode 100644 McuLib/templates/MCU_Wrapper/mcu_wrapper.c create mode 100644 McuLib/templates/MCU_Wrapper/mcu_wrapper_conf.h create mode 100644 McuLib/templates/MCU_Wrapper/run_mex.bat create mode 100644 McuLib/templates/app_wrapper/app_configs.h create mode 100644 McuLib/templates/app_wrapper/app_includes.h create mode 100644 McuLib/templates/app_wrapper/app_init.c create mode 100644 McuLib/templates/app_wrapper/app_io.c create mode 100644 McuLib/templates/app_wrapper/app_wrapper.c create mode 100644 McuLib/templates/app_wrapper/app_wrapper.h create mode 100644 mcuwrapper.mltbx diff --git a/McuLib/install_my_library.m b/McuLib/install_my_library.m new file mode 100644 index 0000000..2c4a542 --- /dev/null +++ b/McuLib/install_my_library.m @@ -0,0 +1,47 @@ +% install_my_library.m +function install_my_library() + libDir = fileparts(mfilename('fullpath')); + + % 1. Добавляем библиотеку и m-файлы в путь + addpath(fullfile(libDir, 'lib')); + addpath(fullfile(libDir, 'm')); + savepath; + + % 2. Диалог выбора папки для копирования шаблонов + defaultTargetDir = pwd; + answer = questdlg(['Выберите папку для копирования шаблонов кода. ', ... + 'По умолчанию текущая папка: ' defaultTargetDir], ... + 'Выбор папки', ... + 'Текущая папка', 'Выбрать другую', 'Текущая папка'); + + switch answer + case 'Выбрать другую' + targetDir = uigetdir(defaultTargetDir, 'Выберите папку для шаблонов'); + if isequal(targetDir,0) + disp('Копирование шаблонов отменено пользователем.'); + targetDir = ''; + end + case 'Текущая папка' + targetDir = defaultTargetDir; + otherwise + targetDir = defaultTargetDir; + end + + if ~isempty(targetDir) + templatesDir = fullfile(libDir, 'templates'); + templateFiles = dir(fullfile(templatesDir, '*.*')); + for k = 1:numel(templateFiles) + if ~templateFiles(k).isdir + copyfile(fullfile(templatesDir, templateFiles(k).name), ... + fullfile(targetDir, templateFiles(k).name)); + end + end + fprintf('Шаблоны кода скопированы в папку:\n%s\n', targetDir); + end + + % 3. Обновляем Library Browser + rehash; + sl_refresh_customizations; + + disp('Библиотека успешно установлена и добавлена в Library Browser.'); +end diff --git a/McuLib/lib/MCU.c b/McuLib/lib/MCU.c new file mode 100644 index 0000000..7eb0c5b --- /dev/null +++ b/McuLib/lib/MCU.c @@ -0,0 +1,209 @@ +/** +************************************************************************** +* @file MCU.c +* @brief Исходный код S-Function. +************************************************************************** +@details +Данный файл содержит функции S-Function, который вызывает MATLAB. +************************************************************************** +@note +Описание функций по большей части сгенерировано MATLAB'ом, поэтому на английском +**************************************************************************/ + +/** + * @addtogroup WRAPPER_SFUNC S-Function funtions + * @ingroup MCU_WRAPPER + * @brief Дефайны и функции блока S-Function + * @details Здесь собраны функции, с которыми непосредственно работает S-Function + * @note Описание функций по большей части сгенерировано MATLAB'ом, поэтому на английском + * @{ + */ + +#define S_FUNCTION_NAME MCU +#define S_FUNCTION_LEVEL 2 + +#include "mcu_wrapper_conf.h" + +#define MDL_UPDATE ///< для подключения mdlUpdate() + /** + * @brief Update S-Function at every step of simulation + * @param S - pointer to S-Function (library struct from "simstruc.h") + * @details Abstract: + * This function is called once for every major integration time step. + * Discrete states are typically updated here, but this function is useful + * for performing any tasks that should only take place once per + * integration step. + */ +static void mdlUpdate(SimStruct* S, int_T tid) +{ + // get time of simulation + time_T TIME = ssGetT(S); + + //---------------SIMULATE MCU--------------- + MCU_Step_Simulation(S, TIME); // SIMULATE MCU + //------------------------------------------ +}//end mdlUpdate + +/** + * @brief Writting outputs of S-Function + * @param S - pointer to S-Function (library struct from "simstruc.h") + * @details Abstract: + * In this function, you compute the outputs of your S-function + * block. Generally outputs are placed in the output vector(s), + * ssGetOutputPortSignal. + */ +static void mdlOutputs(SimStruct* S, int_T tid) +{ + SIM_writeOutputs(S); +}//end mdlOutputs + +#define MDL_CHECK_PARAMETERS /* Change to #undef to remove function */ +#if defined(MDL_CHECK_PARAMETERS) && defined(MATLAB_MEX_FILE) +static void mdlCheckParameters(SimStruct* S) +{ + int i; + + // Проверяем и принимаем параметры и разрешаем или запрещаем их менять + // в процессе моделирования + for (i = 0; i < 1; i++) + { + // Input parameter must be scalar or vector of type double + if (!mxIsDouble(ssGetSFcnParam(S, i)) || mxIsComplex(ssGetSFcnParam(S, i)) || + mxIsEmpty(ssGetSFcnParam(S, i))) + { + ssSetErrorStatus(S, "Input parameter must be of type double"); + return; + } + // Параметр м.б. только скаляром, вектором или матрицей + if (mxGetNumberOfDimensions(ssGetSFcnParam(S, i)) > 2) + { + ssSetErrorStatus(S, "Параметр м.б. только скаляром, вектором или матрицей"); + return; + } + // sim_dt = mxGetPr(ssGetSFcnParam(S,0))[0]; + // Parameter not tunable + // ssSetSFcnParamTunable(S, i, SS_PRM_NOT_TUNABLE); + // Parameter tunable (we must create a corresponding run-time parameter) + ssSetSFcnParamTunable(S, i, SS_PRM_TUNABLE); + // Parameter tunable only during simulation +// ssSetSFcnParamTunable(S, i, SS_PRM_SIM_ONLY_TUNABLE); + + }//for (i=0; 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 literal 0 HcmV?d00001 diff --git a/McuLib/m/asynchManage.m b/McuLib/m/asynchManage.m new file mode 100644 index 0000000..423c5bd --- /dev/null +++ b/McuLib/m/asynchManage.m @@ -0,0 +1,104 @@ +classdef asynchManage < handle + properties (Access = private) + modelName % Имя модели + maskBlockPath % Полный путь к блоку с маской + timerSave + timerUpdate + timerConfigUpdate + end + + methods + function obj = asynchManage(modelName, maskBlockPath) + % Конструктор принимает имя модели и путь к блоку с маской + obj.modelName = modelName; + if nargin < 2 + obj.maskBlockPath = ''; % если не передали, оставляем пустым + else + obj.maskBlockPath = maskBlockPath; + end + end + + function saveAndUpdateModel(obj) + obj.timerSave = timer(... + 'StartDelay', 0.01, ... + 'ExecutionMode', 'singleShot', ... + 'TimerFcn', @(~,~) obj.saveCallback()); + start(obj.timerSave); + end + + + function updateGUIfromConfig(obj) + obj.timerConfigUpdate = timer(... + 'StartDelay', 0.01, ... + 'ExecutionMode', 'singleShot', ... + 'TimerFcn', @(~,~) obj.GUIconfigCallback()); + start(obj.timerConfigUpdate); + end + end + + methods (Access = private) + function saveCallback(obj) + try + mcuMask.saveAndClose(obj.maskBlockPath); + save_system(obj.modelName); + catch ME + warning('progr:Nneg', 'Ошибка при сохранении модели: %s', ME.message); + end + stop(obj.timerSave); + delete(obj.timerSave); + obj.timerSave = []; + + obj.timerUpdate = timer(... + 'StartDelay', 0.05, ... + 'ExecutionMode', 'singleShot', ... + 'TimerFcn', @(~,~) obj.updateCallback()); + start(obj.timerUpdate); + end + + function updateCallback(obj) + try + set_param(obj.modelName, 'SimulationCommand', 'update'); + save_system(obj.modelName); + catch ME + warning('progr:Nneg', 'Ошибка при обновлении модели: %s', ME.message); + end + + % Открываем маску, если задан путь к блоку + if ~isempty(obj.maskBlockPath) + try + mcuMask.open(obj.maskBlockPath, 1); + fprintf('Mask opened for block %s\n', obj.maskBlockPath); + catch ME + warning('progr:Nneg', 'Не удалось открыть маску: %s', ME.message); + end + end + + stop(obj.timerUpdate); + delete(obj.timerUpdate); + obj.timerUpdate = []; + end + function GUIconfigCallback(obj) + + try + mcuMask.saveAndClose(obj.maskBlockPath); + mexing(0); + catch ME + warning('progr:Nneg', 'Ошибка при обновлении модели: %s', ME.message); + end + + % Открываем маску, если задан путь к блоку + if ~isempty(obj.maskBlockPath) + try + mcuMask.open(obj.maskBlockPath, 1); + catch ME + warning('progr:Nneg', 'Не удалось открыть маску: %s', ME.message); + end + end + + stop(obj.timerConfigUpdate); + delete(obj.timerConfigUpdate); + obj.timerConfigUpdate = []; + end + + end +end diff --git a/McuLib/m/customtable.m b/McuLib/m/customtable.m new file mode 100644 index 0000000..94b011f --- /dev/null +++ b/McuLib/m/customtable.m @@ -0,0 +1,157 @@ +classdef customtable + + methods(Static) + % формирование таблицы на всю ширину + function format(table_name) + block = gcb; + mask = Simulink.Mask.get(block); + tableControl = mask.getDialogControl(table_name); + tableParameter = mask.getParameter(table_name); + nCols = tableControl.getNumberOfColumns; + if nCols > 0 + for i = 1:nCols + tableControl.removeColumn(1); + end + end + column = tableControl.addColumn(Name='Title', Type='edit'); + tableControl.Sortable = 'on'; + column.Name = tableParameter.Alias; + end + + + function update(tableName) + block = gcb; + mask = Simulink.Mask.get(block); + Table = mask.getParameter(tableName); + + cellArray = customtable.parse(tableName); + cleaned = customtable.removeEmptyRows(cellArray); + + if numel(cleaned) ~= numel(cellArray) + quoted = cellfun(@(s) ['''' s ''''], cleaned, 'UniformOutput', false); + newStr = ['{' strjoin(quoted, ';') '}']; + Table.Value = newStr; + end + end + + function column_titles = save_all_tables(table_names) + % Очищает столбцы в каждой таблице из массива имен table_names + % Возвращает cell-массив с названиями первых столбцов каждой таблицы + block = gcb; + + % Получить объект маски блока + maskObj = Simulink.Mask.get(block); + + % Инициализировать cell-массив для хранения названий столбцов + column_titles = cell(size(table_names)); + + for k = 1:numel(table_names) + table_name = table_names{k}; + + % Получить объект управления таблицей + tableControl = maskObj.getDialogControl(table_name); + + % Получить количество столбцов + nCols = tableControl.getNumberOfColumns; + + if nCols > 0 + % Получить первый столбец (который будем удалять) + column = tableControl.getColumn(1); + column_titles{k} = column.Name; + + % Удаляем все столбцы + % Важно: при удалении столбцов индексы меняются, + % поэтому удаляем всегда первый столбец nCols раз + for i = 1:nCols + tableControl.removeColumn(1); + end + else + % Если столбцов нет, возвращаем пустую строку + column_titles{k} = ''; + end + end + end + + function restore_all_tables(table_names, column_titles) + % Восстанавливает первый столбец в каждой таблице из массива имен + % Использует массив column_titles для установки имени столбца + block = gcb; + + % Получить объект маски блока + maskObj = Simulink.Mask.get(block); + + for k = 1:numel(table_names) + table_name = table_names{k}; + title = column_titles{k}; + + % Получить объект управления таблицей + tableControl = maskObj.getDialogControl(table_name); + + % Добавить новый столбец + column = tableControl.addColumn(Name='title', Type='edit'); + column.Name = title; + end + end + + + function out = parse(tableName) + block = gcb; + TableStr = get_param(block, tableName); + out = customtable.parse__(TableStr); + end + + function collect(tableName, cellArray) + block = gcb; + newTableStr = customtable.collect__(cellArray); + % Записываем обратно в параметр маски + set_param(block, tableName, newTableStr); + end + end + + + + + + methods(Static, Access=private) + + function out = parse__(tableStr) + str = strtrim(tableStr); + if startsWith(str, '{') && endsWith(str, '}') + str = str(2:end-1); + end + + parts = split(str, ';'); + out = cell(numel(parts), 1); + for i = 1:numel(parts) + el = strtrim(parts{i}); + if startsWith(el, '''') && endsWith(el, '''') + el = el(2:end-1); + end + out{i} = el; + end + + if isempty(out) || (numel(out) == 1 && isempty(out{1})) + out = {}; + end + end + + + function tableStr = collect__(cellArray) + quoted = cellfun(@(s) ['''' s ''''], cellArray, 'UniformOutput', false); + tableStr = ['{' strjoin(quoted, ';') '}']; + end + + + function cleaned = removeEmptyRows(cellArray) + if isempty(cellArray) + cleaned = {}; + else + % Проверяем каждую строку, есть ли в ней содержимое (не пустая строка) + isEmptyRow = cellfun(@(s) isempty(strtrim(s)), cellArray); + cleaned = cellArray(~isEmptyRow); + end + end + + end + +end \ No newline at end of file diff --git a/McuLib/m/editCode.m b/McuLib/m/editCode.m new file mode 100644 index 0000000..5780303 --- /dev/null +++ b/McuLib/m/editCode.m @@ -0,0 +1,172 @@ +classdef editCode + + methods(Static) + + function newCode = insertSection(code, sectionName, newText) + % insertSection – вставка или замена содержимого секции или тела функции + % Аргументы: + % code – исходный текст (строка) + % sectionName – имя секции (например, 'MY_SECTION') или заголовок функции (например, 'void myFunc(...)') + % newText – новый текст, который будет вставлен внутрь секции или функции + % + % Возвращает: + % newCode – обновлённый текст с подставленным содержимым + % + % Особенности: + % - Если sectionName начинается с 'void ', считается что это функция, и вставка происходит внутрь её тела. + % - В остальных случаях ищется секция между маркерами " START" и " END", и она заменяется на newText. + + newCode = code; + + % Если это функция + if startsWith(strtrim(sectionName), 'void ') + tokens = regexp(sectionName, 'void\s+(\w+)\s*\(', 'tokens'); + if isempty(tokens) + return; + end + funcName = tokens{1}{1}; + expr = sprintf('void\\s+%s\\s*\\(.*?\\)\\s*\\{', funcName); + startIdx = regexp(code, expr, 'start'); + if isempty(startIdx) + return; + end + + % Найдём тело функции с учётом вложенных скобок + from = startIdx(1); + braceCount = 0; + i = from; + while i <= length(code) + if code(i) == '{' + braceCount = braceCount + 1; + if braceCount == 1 + bodyStart = i + 1; + end + elseif code(i) == '}' + braceCount = braceCount - 1; + if braceCount == 0 + bodyEnd = i - 1; + break; + end + end + i = i + 1; + end + + if braceCount ~= 0 + return; + end + + newCode = [ ... + code(1:bodyStart-1), ... + newline, newText, newline, ... + code(bodyEnd+1:end) ... + ]; + return; + end + + % Иначе это обычная секция + % Формируем шаблон с группами для поиска нужного блока + pattern = sprintf('(%s START\\s*\\n)(.*?)(\\s*%s END)', sectionName, sectionName); + + % Проверяем, есть ли совпадение + startIdx = regexp(code, pattern, 'start', 'once'); + if isempty(startIdx) + error('Секция "%s" не найдена в тексте.', sectionName); + end + + % Формируем новую секцию с нужным текстом + replacement = sprintf('%s START\n%s\n%s END', sectionName, newText, sectionName); + + % Заменяем всю найденную секцию на новую + newCode = regexprep(code, pattern, replacement, 'dotall'); + end + + + + function result = extractSection(code, sectionName) + % extractSection – извлечение содержимого секции или тела функции + % Аргументы: + % code – исходный текст (строка) + % sectionName – имя секции (например, 'MY_SECTION') или заголовок функции (например, 'void myFunc(...)') + % + % Возвращает: + % result – извлечённый текст из секции или тела функции + % + % Особенности: + % - Если sectionName начинается с 'void ', считается что это функция, и извлекается содержимое её тела. + % - В остальных случаях ищется секция между маркерами " START" и " END". + + result = ''; + % Если это функция (начинается с 'void ') + if startsWith(strtrim(sectionName), 'void ') + % Получаем имя функции из заголовка + tokens = regexp(sectionName, 'void\s+(\w+)\s*\(', 'tokens'); + if isempty(tokens) + return; + end + funcName = tokens{1}{1}; + + % Строим шаблон начала функции + expr = sprintf('void\\s+%s\\s*\\(.*?\\)\\s*\\{', funcName); + startIdx = regexp(code, expr, 'start'); + if isempty(startIdx) + return; + end + + % Поиск тела функции с учётом вложенных скобок + from = startIdx(1); + braceCount = 0; + i = from; + while i <= length(code) + if code(i) == '{' + braceCount = braceCount + 1; + if braceCount == 1 + % Найдём первую новую строку после { + braceOpenIdx = i; + end + elseif code(i) == '}' + braceCount = braceCount - 1; + if braceCount == 0 + braceCloseIdx = i; + break; + end + end + i = i + 1; + end + + if braceCount ~= 0 + return; + end + + % Найдём \n после { + newlineAfterOpen = regexp(code(braceOpenIdx:end), '\n', 'once'); + if isempty(newlineAfterOpen) + return; + end + bodyStart = braceOpenIdx + newlineAfterOpen; + + % Найдём \n до } + newlinesBeforeClose = regexp(code(1:braceCloseIdx), '\n'); + if isempty(newlinesBeforeClose) + return; + end + bodyEnd = newlinesBeforeClose(end) - 1; + + % Извлекаем блок как есть, включая отступы + result = code(bodyStart:bodyEnd); + return; + end + + % Иначе считаем, что это секция вида // NAME START ... // NAME END + pattern = sprintf('%s START\\s*\\n(.*?)\n%s END', sectionName, sectionName); + + match = regexp(code, pattern, 'tokens', 'dotall'); + if ~isempty(match) + result = match{1}{1}; + else + mcuMask.disp(0, 'Ошибка: cекция "%s" не найдена в тексте.', sectionName); + end + end + + + end +end \ No newline at end of file diff --git a/McuLib/m/init.m b/McuLib/m/init.m new file mode 100644 index 0000000..8106cc0 --- /dev/null +++ b/McuLib/m/init.m @@ -0,0 +1,120 @@ +% C + +clear;% + +%% + +addpath('MCU_Wrapper'); +addpath('motor'); +Ts = 10e-6;% +Decim = 1;% +DisableScope = { + "Idc"; + "Udc"; + }; + +GED = "23550"; +% GED = "22220"; + +% , NmNom +w0 = 0;%0.5;%-0.75;% +% , .. +Mst = 0.6;%0.6; + +% / / +changingLoadEnable = 0;%1 +% / +noiseEnable = 0;%1;% +% ... +NP = 0.08; + + +%% +% ... , +Pnom = 6300e3; +% ... , (rms) +Unom = 3300; +% ... , / +NmNom = 180; +% ... +Pp = 6; +% ... +CosFi = 0.87; +% ... +Eff = 0.968; +% ... , *^2 +J = 87e3*0.1; + + +%% +% + +modelName = [bdroot '/Measurements']; +blocks = find_system(modelName, ... + 'IncludeCommented', 'on', ... + 'FollowLinks', 'on', ... + 'LookUnderMasks', 'all', ... + 'BlockType', 'Scope'); +for i = 1:length(blocks) + set_param(blocks{i}, 'Commented', 'off'); +end +% +for i = 1:length(DisableScope) + set_param([modelName '/'] + DisableScope{i}, 'Commented', 'on'); +end + +% +SQRT2 = sqrt(2); +SQRT3 = sqrt(3); +PI2 = pi*2; + +% ... , +Snom = Pnom/CosFi/Eff; +% ... , / +WmNom = NmNom/60*PI2; +% ... , * +Mnom = Pnom/WmNom; +% ... . , / +WeNom = WmNom*Pp; +% ... . , +FeNom = WeNom/PI2; +% ... , +PsiNom = Unom*SQRT2/(WeNom*SQRT3); +% ... , B +UdcNom = Unom*SQRT2; +% ... , (ampl) +Inom = Snom/(Unom*SQRT3)*SQRT2*0.5;%0.5 - .. + +% +if GED == "22220" + GED + Rs = 11.8e-3;% + Xls = 72.7e-3;%72.7e-3;% + Rr = 11.1e-3*2.0;%*0.8;% + Xlr = 85.5e-3;% + Xm = 2.9322;%2.87;% + Fe = 18;% + Lls = Xls/(Fe*PI2);% + Llr = Xlr/(Fe*PI2);% + Lm = Xm/(Fe*PI2);% +elseif GED == "23550" + GED + Rs = 0.0282;% + Xls = 0.4016;% + Rr = 0.139;% + Xlr = 0.2006;% + Xm = 5.2796;% + Fe = 18.2;% + Lls = Xls/(Fe*PI2);% + Llr = Xlr/(Fe*PI2);% + Lm = Xm/(Fe*PI2);% +end + +% INU, +Cdc = 50e-3; +% INU +Csn = Pnom/(1000*WeNom*Unom^2)/10;% (0.5 - .. ) +Rsn = 2*Ts/Csn*10;% + +% , c +Tiac = 30e-6; diff --git a/McuLib/m/mcuMask.m b/McuLib/m/mcuMask.m new file mode 100644 index 0000000..7755161 --- /dev/null +++ b/McuLib/m/mcuMask.m @@ -0,0 +1,641 @@ +classdef mcuMask + + methods(Static) + % Following properties of 'maskInitContext' are avalaible to use: + % - BlockHandle + % - MaskObject + % - MaskWorkspace - Use get/set APIs to work with mask workspace. + function MaskInitialization(maskInitContext) + % Получаем хэндл текущего блока + blk = gcbh; + % Получаем объект маски текущего блока + mask = Simulink.Mask.get(gcb); + % mcuMask.disp(1,''); + try + % Проверка наличия findjobj + findjobjAvailable = exist('findjobj', 'file') == 2; + catch + findjobjAvailable = false; + end + % Получаем объект маски текущего блока + mask = Simulink.Mask.get(gcb); + % Имя checkbox-параметра (укажите точное имя из маски) + checkboxParamName = 'extConsol'; % пример + findjobjLinkName = 'findjobj_link'; % пример + % Получаем параметр по имени + checkboxParam = mask.getParameter(checkboxParamName); + findjobjLink = mask.getDialogControl(findjobjLinkName); + if isempty(findjobjLink) + error('Параметр %s не найден в маске.', findjobjLinkName); + end + if isempty(checkboxParam) + error('Параметр %s не найден в маске.', checkboxParamName); + end + % Блокируем чекбокс, если findjobj не найден + if ~findjobjAvailable + checkboxParam.Enabled = 'off'; + checkboxParam.Value = 'off'; % и на всякий случай снимаем галочку + checkboxParam.Prompt = 'External Console (requires findjobj)'; + findjobjLink.Visible = 'on'; + else + checkboxParam.Enabled = 'on'; + checkboxParam.Prompt = 'External Console'; + findjobjLink.Visible = 'off'; + end + % формирование таблицы на всю ширину + table_names = {'srcTable', 'incTable'}; + for k = 1:numel(table_names) + table_name = table_names{k}; + % customtable.format(table_name); + end + % запись описания блока + textDesc = ['Блок для настройки параметров симуляции микроконтроллера. ' newline ... + 'Позволяет задавать параметры оболочки, приложения МК и периферии']; + + % Получаем объект описания + toolTextArea = mask.getDialogControl('BlockDesc'); + toolTextArea.Prompt = textDesc; + end + + %% WRAPPER PARAMS + function enableThreading(callbackContext) + block = gcb; + maskNames = get_param(block, 'MaskNames'); + maskValues = get_param(block, 'MaskValues'); + maskEnables = get_param(block, 'MaskEnables'); + idxEnable = find(strcmp(maskNames, 'enableThreading')); + idxEdit = find(strcmp(maskNames, 'threadCycles')); + if isempty(idxEnable) || isempty(idxEdit) + error('Параметры enableThreading или threadCycles не найдены в маске'); + end + val = maskValues{idxEnable}; + if strcmp(val, 'on') + maskEnables{idxEdit} = 'on'; + else + maskEnables{idxEdit} = 'off'; + end + set_param(block, 'MaskEnables', maskEnables); + end + + function enableDeinit(callbackContext) + block = gcb; + maskNames = get_param(block, 'MaskNames'); + maskValues = get_param(block, 'MaskValues'); + maskEnables = get_param(block, 'MaskEnables'); + idxEnable = find(strcmp(maskNames, 'enableThreading')); + idxEdit = find(strcmp(maskNames, 'threadCycles')); + if isempty(idxEnable) || isempty(idxEdit) + error('Параметры enableThreading или threadCycles не найдены в маске'); + end + val = maskValues{idxEnable}; + if strcmp(val, 'on') + maskEnables{idxEdit} = 'on'; + else + maskEnables{idxEdit} = 'off'; + end + set_param(block, 'MaskEnables', maskEnables); + end + + function extConsol(callbackContext) + block = gcb; + mask = Simulink.Mask.get(block); + fullOut = mask.getParameter('fullOutput'); + extCons = mask.getParameter('extConsol'); + if isempty(extCons) || isempty(fullOut) + error('Параметры fullOutput или extConsol не найдены в маске'); + end + + if(strcmp(extCons.Enabled, 'on')) + if strcmp(extCons.Value, 'on') + fullOut.Enabled = 'off'; + fullOut.Value = 'on'; + else + fullOut.Enabled = 'on'; + end + else + fullOut.Enabled = 'on'; + end + + end + + function wrapperPath_add(callbackContext) + block = gcb; + mask = Simulink.Mask.get(block); + % Открываем окно выбора папки + folderPath = uigetdir('', 'Выберите папку'); + % Проверка на отмену + if isequal(folderPath, 0) + return; + end + % Установка значения параметра маски + rel = mcuMask.absoluteToRelativePath(folderPath); + param = mask.getParameter('wrapperPath'); + param.Value = rel; + + end + %% USER WRAPPER CODE + + function wrapperFunc(callbackContext) + block = gcb; + % Получаем имя функции и путь к файлам + [filename, section, tool, example]= mcuMask.getWrapperUserFile(block); + mcuMask.tool(tool, example); + + % Загружаем содержимое файла + set_param(block, 'wrapperCode', ''); + code = fileread(filename); + code = regexprep(code, '\r\n?', '\n'); % нормализуем окончания строк + + includesText = editCode.extractSection(code, section); + set_param(block, 'wrapperCode', includesText); + % % Поиск тела обычной функции + % expr = sprintf('void %s()', sel); + % funcBody = editCode.extractSection(code, expr); + % set_param(block, 'wrapperCode', funcBody); + end + + function saveWrapperCode(callbackContext) + block = gcb; + + % Получаем имя функции и путь к файлам + [filename, section] = mcuMask.getWrapperUserFile(block); + if ~isfile(filename) + errordlg(['Файл не найден: ', filename]); + return; + end + + sel = get_param(block, 'wrapperFunc'); + basePath = get_param(block, 'wrapperPath'); + if isempty(basePath) + errordlg('Не указан путь к файлам обёртки (wrapperPath).'); + return; + end + newBody = get_param(block, 'wrapperCode'); + code = fileread(filename); + code = regexprep(code, '\r\n?', '\n'); + code = editCode.insertSection(code, section, newBody); + % else + % % Обновляем тело функции + % expr = sprintf('void %s()', sel); + % code = editCode.insertSection(code, expr, newBody); + % end + fid = fopen(filename, 'w', 'n', 'UTF-8'); + if fid == -1 + errordlg('Не удалось открыть файл для записи'); + return; + end + fwrite(fid, code); + fclose(fid); + mcuMask.disp(1, ['Обновлено: ' sel]); + end + + function openWrapperCode(callbackContext) + block = gcb; + + % Получаем имя функции и путь к файлам + filename = mcuMask.getAbsolutePath(mcuMask.getWrapperUserFile(block)); + if exist(filename, 'file') == 2 + % Формируем команду без кавычек + cmd = sprintf('rundll32.exe shell32.dll,OpenAs_RunDLL %s', filename); + status = system(cmd); + if status ~= 0 + errordlg('Не удалось открыть окно выбора приложения.'); + end + else + errordlg('Файл не найден'); + end + end + + %% USER CODE + function srcTable(callbackContext) + customtable.update('srcTable'); + end + + function incTable(callbackContext) + customtable.update('incTable'); + end + + function btnAddSrc(callbackContext) + blockHandle = gcb; + % Открываем проводник для выбора файлов + [files, pathstr] = uigetfile({ ... + '*.c;*.cpp', 'Исходные файлы (*.c, *.cpp)'; ... + '*.obj;*.lib', 'Библиотеки (*.obj, *.lib)'; ... + '*.*', 'Все файлы (*.*)'}, ... + 'Выберите файлы', ... + 'MultiSelect', 'on'); + + if isequal(files, 0) + return; % Отмена выбора + end + if ischar(files) + files = {files}; % Один файл — в cell + end + % Парсим строку в cell-массив + oldTable = customtable.parse('srcTable'); + + % Добавляем новые пути, проверяя уникальность + for i = 1:numel(files) + fullpath = fullfile(pathstr, files{i}); + rel = mcuMask.absoluteToRelativePath(fullpath); + if ~any(strcmp(rel, oldTable)) + oldTable{end+1, 1} = rel; + end + end + + % Парсим строку в cell-массив + customtable.collect('srcTable', oldTable); + + end + + function btnAddInc(callbackContext) + blockHandle = gcb; + % Открываем проводник для выбора папок + pathstr = uigetdir(pwd, 'Выберите папку с заголовочными файлами'); + if isequal(pathstr, 0) + return; % Отмена выбора + end + % Парсим таблицу + oldTable = customtable.parse('incTable'); + + rel = mcuMask.absoluteToRelativePath(pathstr); + + % Проверяем наличие пути + if ~any(strcmp(rel, oldTable)) + oldTable{end+1, 1} = rel; + end + + % Собираем таблицу + customtable.collect('incTable', oldTable); + end + + %% PERIPH CONFIG + + function periphPath_add(callbackContext) + block = gcbh; + mask = Simulink.Mask.get(block); + [file, path] = uigetfile({'*.*','Все файлы (*.*)'}, 'Выберите файл'); + if isequal(file, 0) || isequal(path, 0) + % Отмена выбора — ничего не делаем + return; + end + fullFilePath = fullfile(path, file); + rel = mcuMask.absoluteToRelativePath(fullFilePath); + param = mask.getParameter('periphPath'); + param.Value = rel; + end + + function compile(callbackContext) + addpath('MCU_Wrapper'); + mexing(1); + end + + + function updateModel(callbackContext) + addpath('MCU_Wrapper'); + res = mexing(1); + if res ~= 0 + return; + end + + modelName = bdroot(gcb); % получить имя верхнего уровня модели + blockName = gcb; + mgr = asynchManage(modelName, blockName); % создать объект класса + mgr.saveAndUpdateModel(); % запустить сохранение и обновление + end + + + function findjobj_link(callbackContext) + web('https://www.mathworks.com/matlabcentral/fileexchange/14317-findjobj-find-java-handles-of-matlab-graphic-objects'); + end + + function set_name() + block = gcb; + % Получаем параметр имени S-Function из маски блока + newName = get_param(block, 'sfuncName'); + + % Путь к файлу, в котором надо заменить строку + cFilePath = fullfile(pwd, './MCU_Wrapper/MCU.c'); % <-- укажи правильный путь + + % Считаем файл в память + fileText = fileread(cFilePath); + + % Регулярное выражение для поиска строки с define + % Заменим строку вида: #define S_FUNCTION_NAME old_name + pattern = '#define\s+S_FUNCTION_NAME\s+\w+'; + + % Новая строка + newLine = ['#define S_FUNCTION_NAME ', newName]; + + % Замена + updatedText = regexprep(fileText, pattern, newLine); + + % Записываем обратно в файл + fid = fopen(cFilePath, 'w', 'n', 'UTF-8'); + if fid == -1 + error('Не удалось открыть файл для записи.'); + end + fwrite(fid, updatedText); + fclose(fid); + + end + + end + + +%% SPECIFIC TOOLS + methods(Static, Access = private) + + function [filename, section, tool, example] = getWrapperUserFile(block) + sel = get_param(block, 'wrapperFunc'); + basePath = get_param(block, 'wrapperPath'); + if isempty(basePath) + errordlg('Не указан путь к файлам обёртки (wrapperPath).'); + return; + end + % Формируем путь к файлу в зависимости от типа запроса + if strcmp(sel, 'Includes') + filename = fullfile(basePath, 'app_includes.h'); + section = '// INCLUDES'; + tool = 'Инклюды для доступа к коду МК в коде оболочке'; + example = '#include "main.h"'; + elseif strcmp(sel, 'Dummy') + filename = fullfile(basePath, 'app_wrapper.c'); + section = '// DUMMY'; + tool = 'Заглушки для различных функций и переменных'; + example = ['CAN_HandleTypeDef hcan = {0};' newline... + 'void hardware_func(handle *huart) {}' newline... + 'int wait_for_hardware_flag(int *flag) {' newline... + ' return 1;' newline... + '}' newline... + '']; + elseif strcmp(sel, 'App Init') + filename = fullfile(basePath, 'app_init.c'); + section = '// USER APP INIT'; + tool = ['Код для инициализации приложения МК.' newline newline... + 'Вызов функций инициализации, если не используется отдельный поток для main().']; + example = 'init_func();'; + elseif strcmp(sel, 'App Step') + filename = fullfile(basePath, 'app_wrapper.c'); + section = '// USER APP STEP'; + tool = ['Код приложения МК для вызова в шаге симуляции.' newline newline ... + 'Вызов функций программы МК, если не используется отдельный поток для main().']; + example = 'step_func();'; + elseif strcmp(sel, 'App Inputs') + filename = fullfile(basePath, 'app_io.c'); + section = '// USER APP INPUT'; + tool = ['Работа с буффером для портов S-Function' newline newline ... + 'Буфер в начале хранит входные порты S-Function, далее идут выходные порты:' newline ... + 'Buffer[0:15] - входной порт, Buffer[16:31] - входной 1 порт, ' newline ... + 'Buffer[32:47] - выходной 1 порт, Buffer[48:63] - выходной 2 порт']; + example = ['// чтение 1-го элемента 0-го входного массива' newline... + 'app_variable_2 = ReadInputArray(0, 1);' newline newline... + '// запись в буфер выходов' newline ... + 'app_variable_2 = Buffer[10];']; + elseif strcmp(sel, 'App Outputs') + filename = fullfile(basePath, 'app_io.c'); + section = '// USER APP OUTPUT'; + tool = ['Работа с буффером для портов S-Function' newline newline ... + 'Буфер в начале хранит входные порты S-Function, далее идут выходные порты:' newline ... + 'Buffer[0:15] - входной порт, Buffer[16:31] - входной 1 порт, ' newline ... + 'Buffer[32:47] - выходной 1 порт, Buffer[48:63] - выходной 2 порт']; + example = ['// запись в 1-й элемент 0-го выходного массива' newline... + 'WriteOutputArray(app_variable, 0, 1);' newline newline ... + '// запись в буфер выходов' newline ... + 'Buffer[XD_OUTPUT_START + 10] = app_variable_2;']; + elseif strcmp(sel, 'App Deinit') + filename = fullfile(basePath, 'app_init.c'); + section = '// USER APP DEINIT'; + tool = ['Код для деинициализации приложения МК.' newline newline ... + 'Можно деинициализировать приложение МК, для повторного запуска.']; + example = 'memset(&htim1, sizeof(htim1), 0;'; + else + tool = ''; + mcuMask.disp(0, '\nОшибка выбора типа секции кода: неизвестное значение'); + end + + end + + end + +%% GENERAL TOOLS + methods(Static, Access = public) + + function saveAndClose(blockPath) + try + % Считываем текущее имя модели + modelName = bdroot(blockPath); + % Включаем возможность изменения маски + set_param(blockPath, 'MaskSelfModifiable', 'on'); + + % Считываем текущие значения параметров маски + currentMaskValues = get_param(blockPath, 'MaskValues'); + + % Применяем текущие значения заново, чтобы "применить" маску + set_param(blockPath, 'MaskValues', currentMaskValues); + save_system(modelName); + catch ME + warning('progr:Nneg', 'Ошибка при сохранении маски: %s', ME.message); + end + close_system(blockPath, 0); + end + + function open(blockPath, clear_flag) + open_system(blockPath, 'mask'); + mcuMask.disp(clear_flag, ''); + end + + + function absPath = getAbsolutePath(relPath) + % relativeToAbsolutePath — преобразует относительный путь в абсолютный. + % + % Если путь уже абсолютный — возвращается он же, приведённый к канонической форме. + % Если путь относительный — преобразуется относительно текущей директории. + + % Проверка: абсолютный ли путь + if ispc + isAbsolute = ~isempty(regexp(relPath, '^[a-zA-Z]:[\\/]', 'once')) || startsWith(relPath, '\\'); + else + isAbsolute = startsWith(relPath, '/'); + end + + if isAbsolute + % Канонизируем абсолютный путь (убираем ./, ../ и т.п.) + absPath = char(java.io.File(relPath).getCanonicalPath()); + else + % Строим абсолютный путь от текущей директории + cwd = pwd; + combined = fullfile(cwd, relPath); + absPath = char(java.io.File(combined).getCanonicalPath()); + end + end + + + + function rel = absoluteToRelativePath(pathstr) + % absoluteToRelativePath — преобразует абсолютный путь в относительный от текущей директории. + % + % Если путь находится в текущей директории или вложенной в неё — добавляется префикс './' + % Если выше — формируются переходы '..' + % Если путь совпадает с текущей директорией — возвращается '.' + + % Получаем текущую рабочую директорию + cwd = pwd; + + % Преобразуем пути в канонические абсолютные пути + fullpath = char(java.io.File(pathstr).getCanonicalPath()); + cwd = char(java.io.File(cwd).getCanonicalPath()); + + % Разбиваем пути на части + targetParts = strsplit(fullpath, filesep); + baseParts = strsplit(cwd, filesep); + + % Находим длину общего префикса + j = 1; + while j <= min(length(targetParts), length(baseParts)) && strcmpi(targetParts{j}, baseParts{j}) + j = j + 1; + end + + % Формируем количество подъемов ".." из cwd + numUps = length(baseParts) - (j - 1); + ups = repmat({'..'}, 1, numUps); + + % Оставшаяся часть пути после общего префикса + rest = targetParts(j:end); + + % Объединяем для получения относительного пути + relParts = [ups, rest]; + rel = fullfile(relParts{:}); + + % Если путь пустой — это текущая директория + if isempty(rel) + rel = '.'; + end + + % Если путь не содержит ".." и начинается внутри текущей директории — добавим './' + if ~isempty(rest) && isempty(ups) + rel = fullfile('.', rel); + end + end + + + + function checkbox_state = read_checkbox(checkboxName) + maskValues = get_param(gcbh, 'MaskValues'); + paramNames = get_param(gcbh, 'MaskNames'); + + inxCheckBox = find(strcmp(paramNames, checkboxName)); + + checkbox_state_str = maskValues{inxCheckBox}; + if strcmpi(checkbox_state_str, 'on') + checkbox_state = 1; + else + checkbox_state = 0; + end + end + + + function children = get_children(ctrl) + if isprop(ctrl, 'DialogControls') + children = ctrl.DialogControls; + elseif isprop(ctrl, 'Controls') + children = ctrl.Controls; + elseif isprop(ctrl, 'Children') + children = ctrl.Children; + else + children = []; + end + end + + function params = collect_all_parameters(container) + params = {}; + children = container.DialogControls; + for i = 1:numel(children) + ctrl = children(i); + if isa(ctrl, 'Simulink.dialog.Tab') + % Если вкладка — рекурсивно собираем параметры внутри неё + params = [params, mcuMask.collect_all_parameters(ctrl)]; + else + % Иначе это параметр — добавляем имя + params{end+1} = ctrl.Name; %#ok + end + end + end + + function delete_all_tabs(mask, container) + children = container.DialogControls; + % Идём в обратном порядке, чтобы безопасно удалять + for i = numel(children):-1:1 + ctrl = children(i); + if isa(ctrl, 'Simulink.dialog.Tab') + % Сначала рекурсивно удаляем вкладки внутри текущей вкладки + mcuMask.delete_all_tabs(mask, ctrl); + try + container.removeDialogControl(ctrl.Name); + catch ME + warning('Не удалось удалить вкладку %s: %s', ctrl.Name, ME.message); + end + end + end + end + + function res = ternary(cond, valTrue, valFalse) + if cond + res = valTrue; + else + res = valFalse; + end + end + + + function tool(text, example) + % Устанавливает заданный текст в параметр Text Area 'toolText' через объект маски + + % Получаем ссылку на текущий блок + block = gcb; + + % Получаем объект маски + mask = Simulink.Mask.get(block); + + toolTextArea = mask.getDialogControl('toolText'); + exampleTextArea = mask.getDialogControl('exampleText'); + toolTextArea.Prompt = text; + exampleTextArea.Prompt = example; + end + + function disp(clcFlag, varargin) + if clcFlag + set_param(gcb, 'consoleOutput', ''); + end + + if length(varargin) == 1 && ischar(varargin{1}) + % Если передан один аргумент — просто строка, передаем напрямую + out = varargin{1}; + else + % Иначе считаем, что первый аргумент — формат, остальные — параметры + out = sprintf(varargin{:}); + end + + out_now = get_param(gcb, 'consoleOutput'); + set_param(gcb, 'consoleOutput', [out_now out]); + end + + + function updateModelAsync() + mdl = bdroot(gcb); + + try + % Применить изменения, если есть + set_param(mdl, 'ApplyChanges', 'on'); + catch + beep + % Игнорировать, если не удалось + end + + t = timer('StartDelay', 0.01, 'TimerFcn', @(~,~) set_param(mdl, 'SimulationCommand', 'update')); + start(t); + end + + + end +end \ No newline at end of file diff --git a/McuLib/m/mcuPorts.m b/McuLib/m/mcuPorts.m new file mode 100644 index 0000000..315871e --- /dev/null +++ b/McuLib/m/mcuPorts.m @@ -0,0 +1,284 @@ +classdef mcuPorts + + methods(Static) + + function write() + block = gcb; + mask = Simulink.Mask.get(block); + hPath = fullfile('.\MCU_Wrapper', 'mcu_wrapper_conf.h'); + cPath = fullfile('.\MCU_Wrapper', 'mcu_wrapper.c'); + mcuPorts.defaultUnused(); + %% CREATE + prefixNumb = 'IN'; + [widths, portPrefixes] = mcuPorts.getMaskNames('in'); + + headerText = mcuPorts.addPortHeaderDefines('', widths, prefixNumb, portPrefixes); + cText = mcuPorts.addPortCDefines('', widths, prefixNumb, portPrefixes); + + prefixNumb = 'OUT'; + [widths, portPrefixes] = mcuPorts.getMaskNames('out'); + + headerText = mcuPorts.addPortHeaderDefines(headerText, widths, prefixNumb, portPrefixes); + cText = mcuPorts.addPortCDefines(cText, widths, prefixNumb, portPrefixes); + + %% WRITE + + hCode = fileread(hPath); + hCode = regexprep(hCode, '\r\n?', '\n'); + cCode = fileread(cPath); + cCode = regexprep(cCode, '\r\n?', '\n'); + + code = editCode.insertSection(hCode, '// INPUT/OUTPUTS PARAMS', headerText.PARAMS); + code = editCode.insertSection(code, '// INPUT/OUTPUTS AUTO-PARAMS', headerText.AUTO_PARAMS); + + fid = fopen(hPath, 'w', 'n', 'UTF-8'); + if fid == -1 + error('Не удалось открыть файл для записи'); + end + fwrite(fid, code); + fclose(fid); + + code = editCode.insertSection(cCode, '// INPUT/OUTPUTS AUTO-PARAMS', cText); + fid = fopen(cPath, 'w', 'n', 'UTF-8'); + if fid == -1 + error('Не удалось открыть файл для записи'); + end + fwrite(fid, code); + fclose(fid); + end + + + function [portwidth, defnames] = getMaskNames(port_prefix) + block = gcb; + % Получаем значение из спиннера + mask = Simulink.Mask.get(block); + paramName = sprintf('%sNumb', port_prefix); + param = mask.getParameter(paramName); + numb = str2double(param.Value); + + + % Инициализируем массив для значений + defnames = strings(1, numb); + portwidth = []; + + % Чтение значений edit-параметров + for i = 1:numb + paramName = sprintf('%s_port_%d_name', port_prefix, i); + param = mask.getParameter(paramName); + defnames(i) = param.Value; + paramName = sprintf('%s_port_%d_width', port_prefix, i); + param = mask.getParameter(paramName); + portwidth(i) = str2double(param.Value); + end + end + + + function updateMask() + mcuPorts.updateMask_prefix('in'); + mcuPorts.updateMask_prefix('out'); + end + + + function defaultUnused() + mcuPorts.defaultUnused_prefix('in'); + mcuPorts.defaultUnused_prefix('out'); + end + end + + methods(Static, Access=private) + + + + function updateMask_prefix(port_prefix) + block = gcb; + % Получаем значение из спиннера + mask = Simulink.Mask.get(block); + paramName = sprintf('%sNumb', port_prefix); + param = mask.getParameter(paramName); + n = str2double(param.Value); + + % Максимальное количество портов + maxPorts = param.Range(2); + + % Проходим по всем edit-полям + for i = 1:maxPorts + % Формируем имя параметра + paramDefName = sprintf('%s_port_%d_name', port_prefix, i); + paramWidthName = sprintf('%s_port_%d_width', port_prefix, i); + paramDef = mask.getParameter(paramDefName); + paramWidth = mask.getParameter(paramWidthName); + + if i <= n + % Показываем параметр + paramDef.Visible = 'on'; + paramWidth.Visible = 'on'; + + % Если значение пустое — задаём дефолтное + if isempty(strtrim(paramDef.Value)) + paramDef.Value = upper(port_prefix); + end + % Если значение пустое — задаём дефолтное + if isempty(strtrim(paramWidth.Value)) || strcmp(paramWidth.Value, '0') + paramWidth.Value = '16'; + end + else + % Скрываем параметр + paramDef.Visible = 'off'; + paramWidth.Visible = 'off'; + end + end + end + + + function defaultUnused_prefix(port_prefix) + block = gcb; + % Получаем значение из спиннера + mask = Simulink.Mask.get(block); + paramName = sprintf('%sNumb', port_prefix); + param = mask.getParameter(paramName); + numb = str2double(param.Value); + + % Максимальное количество портов + maxPorts = param.Range(2); + % Чтение значений edit-параметров + for i = numb+1:maxPorts + paramName = sprintf('%s_port_%d_name', port_prefix, i); + param = mask.getParameter(paramName); + param.Value = upper(port_prefix); + paramName = sprintf('%s_port_%d_width', port_prefix, i); + param = mask.getParameter(paramName); + param.Value = '16'; + end + end + + + function headerText = addPortHeaderDefines(existingText, widths, prefixNumb, portPrefixes) + % existingText — структура с полями PARAMS, AUTO_PARAMS + % widths — вектор ширин портов + % prefixNumb — префикс общего счётчика (например, 'OUT') + % portPrefixes — {'OUT', 'OUT', ...} + + n = numel(widths); + upperPrefix = upper(prefixNumb); + + % === PARAMS === + lines = { + sprintf('#define %s_PORT_NUMB %d', upperPrefix, n) + }; + for i = 1:n + lines{end+1} = sprintf('#define %s_PORT_%d_WIDTH %d', ... + upper(portPrefixes{i}), i, widths(i)); + end + newParams = strjoin(lines, newline); + newParams = [newParams, newline]; + + % === AUTO-PARAMS === + lines = {}; + + % Формируем выражение суммы ширин с добавлением PORT + sumExprParts = cell(1,n); + for i = 1:n + sumExprParts{i} = sprintf('%s_PORT_%d_WIDTH', upper(portPrefixes{i}), i); + end + sumExpr = strjoin(sumExprParts, ' + '); + + lines{end+1} = sprintf('/// === Полный размер буфера ==='); + lines{end+1} = sprintf('#define TOTAL_%s_SIZE (%s)', upperPrefix, sumExpr); + + lines{end+1} = ''; + lines{end+1} = sprintf('/// === Смещения массивов (внутри общего буфера) ==='); + + for i = 1:n + if i == 1 + lines{end+1} = '#define OFFSET_ARRAY_1 0'; + else + lines{end+1} = sprintf('#define OFFSET_ARRAY_%d (OFFSET_ARRAY_%d + %s_PORT_%d_WIDTH)', ... + i, i - 1, upper(portPrefixes{i - 1}), i - 1); + end + end + newAuto = strjoin(lines, newline); + + % === Добавление к существующему === + if isfield(existingText, 'PARAMS') + headerText.PARAMS = [existingText.PARAMS, newline, newParams]; + else + headerText.PARAMS = newParams; + end + + if isfield(existingText, 'AUTO_PARAMS') + headerText.AUTO_PARAMS = [existingText.AUTO_PARAMS, newline, newAuto, newline]; + else + headerText.AUTO_PARAMS = [newAuto, newline]; + end + end + + + function cText = addPortCDefines(existingText, widths, prefixNumb, portPrefixes) + % existingText — существующий текст .c + % widths — вектор ширин портов + % prefixNumb — общий префикс ('OUT') + % portPrefixes — {'OUT', 'LOG', ...} + + n = numel(widths); + upperPrefix = upper(prefixNumb); + lowerPrefix = lower(prefixNumb); + + lines = {}; + + % === Таблица длин === + lines{end+1} = '/**'; + lines{end+1} = sprintf(' * @brief Таблица длин массивов %s', upperPrefix); + lines{end+1} = ' */'; + % Здесь используем общий префикс для количества портов + lines{end+1} = sprintf('const int %sLengths[%s_PORT_NUMB] = {', lowerPrefix, upperPrefix); + for i = 1:n + comma = ','; + if i == n + comma = ''; + end + % Используем макросы с портовыми префиксами из portPrefixes + lines{end+1} = sprintf(' %s_PORT_%d_WIDTH%s', upper(portPrefixes{i}), i, comma); + end + lines{end+1} = '};'; + + % === Таблица смещений === + lines{end+1} = '/**'; + lines{end+1} = sprintf(' * @brief Таблица смещений в выходном массиве %s', upperPrefix); + lines{end+1} = ' */'; + lines{end+1} = sprintf('const int %sOffsets[%s_PORT_NUMB] = {', lowerPrefix, upperPrefix); + for i = 1:n + comma = ','; + if i == n + comma = ''; + end + lines{end+1} = sprintf(' OFFSET_ARRAY_%d%s', i, comma); + end + lines{end+1} = '};'; + lines{end+1} = ''; + + newText = strjoin(lines, newline); + + if nargin < 1 || isempty(existingText) + cText = newText; + else + cText = [existingText, newline, newText]; + end + end + + + + + + function val = iff(cond, a, b) + % Условное выражение inline + if cond + val = a; + else + val = b; + end + end + + + end + +end \ No newline at end of file diff --git a/McuLib/m/mexing.m b/McuLib/m/mexing.m new file mode 100644 index 0000000..e91e644 --- /dev/null +++ b/McuLib/m/mexing.m @@ -0,0 +1,377 @@ +% Компилирует S-function +function res = mexing(compile_mode) + global Ts + Ts = 0.00001; + + if compile_mode == 1 + delete("*.mexw64") + delete("*.mexw64.pdb") + delete(".\MCU_Wrapper\Outputs\*.*"); + set_param(gcb, 'consoleOutput', ''); + % Порты S-Function + mcuPorts.write(); + % Дефайны + definesUserArg = parseDefinesMaskText(); + definesWrapperConfigArg = buildWrapperDefinesString(); + definesPeriphConfigArg = buildConfigDefinesString(); + definesConfigArg = [definesWrapperConfigArg + " " + definesPeriphConfigArg]; + + %режимы компиляции + if mcuMask.read_checkbox('enableDebug') + modeArg = "debug"; + else + modeArg = "release"; + end + if mcuMask.read_checkbox('fullOutput') || mcuMask.read_checkbox('extConsol') + echoArg = 'echo_enable'; + else + echoArg = 'echo_disable'; + end + + [includesArg, codeArg] = make_mex_arguments('incTable', 'srcTable'); + + % Вызов батника с двумя параметрами: includes и code + cmd = sprintf('.\\MCU_Wrapper\\run_mex.bat "%s" "%s" "%s" "%s" %s %s', includesArg, codeArg, definesUserArg, definesConfigArg, modeArg, echoArg); + + if mcuMask.read_checkbox('extConsol') + cmdout = runBatAndShowOutput(cmd); + else + [status, cmdout]= system(cmd); + end + + % Сохраним вывод в параметр маски с именем 'consoleOutput' + mcuMask.disp(1, cmdout); + + block = gcb; + + newName = get_param(block, 'sfuncName'); + oldName = get_param(block, 'FunctionName'); + if ~strcmp(newName, oldName) + set_param(block, 'FunctionName', newName); + end + + newParam = get_param(block, 'sfuncParam'); + oldParam = get_param(block, 'Parameters'); + if ~strcmp(newParam, oldParam) + set_param(block, 'Parameters', newParam); + end + + if status == 0 + res = 0; + else + res = 1; + end + beep + else + blockPath = gcb; + config = periphConfig.read_config(blockPath); + config = periphConfig.update_config(blockPath, config); + periphConfig.write_config(config); + periphConfig.update(blockPath, config); + % Порты S-Function + mcuPorts.write(); + % set_param(gcb, 'consoleOutput', 'Peripheral configuration file loaded. Re-open Block Parameters'); + end +end + +%% COMPILE PARAMS + + +function [includesArg, codeArg] = make_mex_arguments(incTableName, srcTableame) +%MAKE_MEX_ARGUMENTS Формирует строки аргументов для вызова mex-компиляции через батник +% +% [includesArg, codeArg] = make_mex_arguments(includesCell, codeCell) +% +% Вход: +% includesCell — ячейковый массив путей к директориям include +% codeCell — ячейковый массив исходных файлов +% +% Выход: +% includesArg — строка для передачи в батник, например: "-I"inc1" -I"inc2"" +% codeArg — строка с исходниками, например: ""src1.c" "src2.cpp"" + + + % Здесь пример получения из маски текущего блока (замени по своему) + includesCell = customtable.parse(incTableName); + codeCell = customtable.parse(srcTableame); + + % Оборачиваем пути в кавычки и добавляем -I + includesStr = strjoin(cellfun(@(f) ['-I"' f '"'], includesCell, 'UniformOutput', false), ' '); + + % Оборачиваем имена файлов в кавычки + codeStr = strjoin(cellfun(@(f) ['"' f '"'], codeCell, 'UniformOutput', false), ' '); + + % Удаляем символ переноса строки и пробел в конце, если вдруг попал + codeStr = strtrim(codeStr); + includesStr = strtrim(includesStr); + + % Оборачиваем всю строку в кавычки, чтобы батник корректно понял + % includesArg = ['"' includesStr '"']; + % codeArg = ['"' codeStr '"']; + includesArg = includesStr; + codeArg = codeStr; + +end + + +function definesWrapperArg = buildWrapperDefinesString() + + definesWrapperArg = ''; + definesWrapperArg = addDefineByParam(definesWrapperArg, 'enableThreading', 0); + definesWrapperArg = addDefineByParam(definesWrapperArg, 'enableDeinit', 0); + definesWrapperArg = addDefineByParam(definesWrapperArg, 'threadCycles', 1); + definesWrapperArg = addDefineByParam(definesWrapperArg, 'mcuClk', 1); +end + + +function definesUserArg = parseDefinesMaskText() + blockHandle = gcbh; + % Получаем MaskValues и MaskNames + maskValues = get_param(blockHandle, 'MaskValues'); + paramNames = get_param(blockHandle, 'MaskNames'); + + % Индекс параметра userDefs + idxUserDefs = find(strcmp(paramNames, 'userDefs')); + definesText = maskValues{idxUserDefs}; % Текст с пользовательскими определениями + + % Убираем буквальные символы \n и \r + definesText = strrep(definesText, '\n', ' '); + definesText = strrep(definesText, '\r', ' '); + + % Разбиваем по переносам строк + lines = split(definesText, {'\n', '\r\n', '\r'}); + + parts = strings(1,0); % пустой массив строк + + for k = 1:numel(lines) + line = strtrim(lines{k}); + if isempty(line) + continue; + end + + % Разбиваем по пробелам, чтобы получить отдельные определения в строке + tokens = split(line); + + for t = 1:numel(tokens) + token = strtrim(tokens{t}); + if isempty(token) + continue; + end + + eqIdx = strfind(token, '='); + if isempty(eqIdx) + % Просто ключ без значения + parts(end+1) = sprintf('-D"%s"', token); + else + key = strtrim(token(1:eqIdx(1)-1)); + val = strtrim(token(eqIdx(1)+1:end)); + parts(end+1) = sprintf('-D"%s__EQ__%s"', key, val); + end + end + end + + definesUserArg = strjoin(parts, ' '); +end + + + +function definesWrapperArg = buildConfigDefinesString() + blockHandle = gcbh; + mask = Simulink.Mask.get(blockHandle); + + tabName = 'configTabAll'; % Имя вкладки (Prompt) + + tabCtrl = mask.getDialogControl(tabName); + + if isempty(tabCtrl) + error('Вкладка с названием "%s" не найдена в маске', tabName); + end + + + params = mcuMask.collect_all_parameters(tabCtrl); + definesWrapperArg = ''; + for i = 1:numel(params) + % Получаем имя параметра из контрола + paramName = string(params(i)); + try + % Получаем объект параметра по имени + param = mask.getParameter(paramName); + + % Определяем тип параметра + switch lower(param.Type) + case 'checkbox' + definesWrapperArg = addDefineByParam(definesWrapperArg, paramName, 0); + case 'edit' + definesWrapperArg = addDefineByParam(definesWrapperArg, paramName, 1); + otherwise + % Необрабатываемые типы + end + catch ME + % warning('Не удалось получить параметр "%s": %s', paramName, ME.message); + end + end +end + + +%% PARSE FUNCTIONS + +function definesWrapperArg = addDefineByParam(definesWrapperArg, paramName, val_define) + blockHandle = gcbh; + mask = Simulink.Mask.get(blockHandle); + + % Получаем MaskValues, MaskNames + maskValues = get_param(blockHandle, 'MaskValues'); + paramNames = get_param(blockHandle, 'MaskNames'); + param = mask.getParameter(paramName); % для alias + + % Найдём индекс нужного параметра + idxParam = find(strcmp(paramNames, paramName), 1); + if isempty(idxParam) + error('Parameter "%s" not found in block mask parameters.', paramName); + end + + % Берём alias из маски + alias = param.Alias; + + if val_define ~= 0 + % Значение параметра + val = maskValues{idxParam}; + if strcmp(param.Evaluate, 'on') + val = evalin('base', val); % Вычисляем выражение + val = num2str(val); % Преобразуем результат в строку + end + % Формируем define с кавычками и значением + newDefine = ['-D"' alias '__EQ__' val '"']; + else + if mcuMask.read_checkbox(paramName) + % Формируем define с кавычками без значения + newDefine = ['-D"' alias '"']; + else + newDefine = ''; + end + end + + + + % Добавляем новый define к существующему (string) + if isempty(definesWrapperArg) || strlength(strtrim(definesWrapperArg)) == 0 + definesWrapperArg = newDefine; + else + definesWrapperArg = definesWrapperArg + " " + newDefine; + end +end + + +%% CONSOLE FUNCTIONS +function cmdret = runBatAndShowOutput(cmd) + import java.io.*; + import java.lang.*; + cmdEnglish = ['chcp 437 > nul && ' cmd]; + pb = java.lang.ProcessBuilder({'cmd.exe', '/c', cmdEnglish}); + pb.redirectErrorStream(true); + process = pb.start(); + + reader = BufferedReader(InputStreamReader(process.getInputStream())); + + cmdret = ""; % Здесь будем накапливать весь вывод + + while true + if reader.ready() + line = char(reader.readLine()); + if isempty(line) + break; + end + cmdret = cmdret + string(line) + newline; % сохраняем вывод + % Здесь выводим только новую строку + safeLine = strrep(line, '''', ''''''); % Экранируем апострофы + logWindow_append(safeLine); + drawnow; % обновляем GUI + else + if ~process.isAlive() + % дочитываем оставшиеся строки + while reader.ready() + line = char(reader.readLine()); + if isempty(line) + break; + end + cmdret = cmdret + string(line) + newline; % сохраняем вывод + safeLine = strrep(line, '''', ''''''); + logWindow_append(safeLine); + drawnow; + end + break; + end + pause(0.2); + end + end + process.waitFor(); +end + + +function logWindow_append(line) + persistent fig hEdit jScrollPane jTextArea + + if isempty(fig) || ~isvalid(fig) + fig = figure('Name', 'Log Window', 'Position', [100 100 600 400]); + hEdit = uicontrol('Style', 'edit', ... + 'Max', 2, 'Min', 0, ... + 'Enable', 'on', ... + 'FontName', 'Courier New', ... + 'Position', [10 10 580 380], ... + 'HorizontalAlignment', 'left', ... + 'BackgroundColor', 'white', ... + 'Tag', 'LogWindowFigure'); + + jScrollPane = findjobj(hEdit); % JScrollPane + jTextArea = jScrollPane.getViewport.getView; % JTextArea внутри JScrollPane + end + + oldText = get(hEdit, 'String'); + if ischar(oldText) + oldText = {oldText}; + end + + set(hEdit, 'String', [oldText; {line}]); + drawnow; + % Автоскролл вниз: + jTextArea.setCaretPosition(jTextArea.getDocument.getLength); + drawnow; +end + + +%% READ CONFIGS + +function isOpen = isMaskDialogOpen(blockPath) + isOpen = false; + + try + % Получаем имя блока + blockName = get_param(blockPath, 'Name'); + + % Получаем список окон MATLAB GUI + jWindows = java.awt.Window.getWindows(); + + for i = 1:numel(jWindows) + win = jWindows(i); + + % Проверка, что окно видимое и активно + if win.isShowing() + try + title = char(win.getTitle()); + % Проверка по ключевому слову, соответствующему заголовку маски + if contains(title, ['Mask Editor: ' blockName]) || ... + contains(title, ['Mask: ' blockName]) || ... + contains(title, blockName) + isOpen = true; + return; + end + catch + % Окно не имеет заголовка — пропускаем + end + end + end + catch + isOpen = false; + end +end + diff --git a/McuLib/m/periphConfig.m b/McuLib/m/periphConfig.m new file mode 100644 index 0000000..01e3061 --- /dev/null +++ b/McuLib/m/periphConfig.m @@ -0,0 +1,414 @@ +classdef periphConfig + + methods(Static) + function update(blockPath, config) + % blockPath = [blockPath '/MCU']; + + % Проверяем, была ли маска открыта +% wasOpen = isMaskDialogOpen(blockPath); + mask = Simulink.Mask.get(blockPath); + periphPath = get_param(blockPath, 'periphPath'); + [periphPath, ~, ~] = fileparts(periphPath); + + tableNames = {'incTable', 'srcTable'}; + columns_backup = customtable.save_all_tables(tableNames); + + containerName = 'configTabAll'; + periphConfig.clear_all_from_container(mask, containerName); + + % Ищем контейнер, в который будем добавлять вкладки + container = mask.getDialogControl(containerName); + if isempty(container) + error('Контейнер "%s" не найден в маске.', containerName); + end + + if ~isempty(config) + + if isfield(config, 'Code') + res = periphConfig.addCodeConfig(config.Code, periphPath); + if res == 0 + error('Ошибка: неудачное добавление кода периферии. Проверьте корректность файлов и путей в конфигурационном файле') + end + else + error('Ошибка: в конфигурационном файле не задан исходный код для симуляции периферии') + end + + if isfield(config, 'UserCode') + res = periphConfig.addUserCodeConfig(config.UserCode); + if res == 0 + error('Ошибка: неудачное добавление функций для симуляции. Проверьте корректность названий функций в конфигурационном файле') + end + else + error('Ошибка: в конфигурационном файле не заданы функции для симуляции периферии') + end + + % Проходим по каждому модулю (ADC, TIM...) + periphs = fieldnames(config); + for i = 1:numel(periphs) + periph = periphs{i}; + + % Пропускаем Code и UserCode, они уже обработаны + if strcmp(periph, 'Code') || strcmp(periph, 'UserCode') + continue; + end + + defines = config.(periph).Defines; + defNames = fieldnames(defines); + + % Создаём вкладку для модуля + tabCtrl = container.addDialogControl('tab', periph); + tabCtrl.Prompt = [periph ' Config']; + + for j = 1:numel(defNames) + defPrompt = defNames{j}; + def = defines.(defPrompt); + + % Вызов функции добавления одного параметра + periphConfig.addDefineConfig(mask, containerName, periph, defPrompt, def); + end + end + end + % Восстанавливаем таблицы + customtable.restore_all_tables(tableNames, columns_backup); + + % % Повторно открываем маску, если она была открыта + % if wasOpen + % open_system(blockPath, 'mask'); + % end + end + + function config = update_config(blockPath, config) + if isempty(config) + return; + end + + mask = Simulink.Mask.get(blockPath); + maskParams = mask.Parameters; + paramNames = arrayfun(@(p) p.Name, maskParams, 'UniformOutput', false); + + % Обработка остальных секций (с дефайнами) + periphs = fieldnames(config); + for i = 1:numel(periphs) + periph = periphs{i}; + + % Пропускаем Code и UserCode, они уже обработаны + if strcmp(periph, 'Code') || strcmp(periph, 'UserCode') + continue; + end + + % Проверяем есть ли Defines + if ~isfield(config.(periph), 'Defines') + continue; + end + + defines = config.(periph).Defines; + defNames = fieldnames(defines); + + for j = 1:numel(defNames) + defPrompt = defNames{j}; + paramName = matlab.lang.makeValidName(defPrompt); + + % Проверка, существует ли параметр с таким именем + if ismember(paramName, paramNames) + param = mask.getParameter(paramName); + valStr = param.Value; + + % Проверяем, существует ли элемент defPrompt в структуре defines + if isfield(defines, defPrompt) + % Преобразуем строку в соответствующий тип + if strcmpi(defines.(defPrompt).Type, 'checkbox') + config.(periph).Defines.(defPrompt).Default = strcmpi(valStr, 'on'); + elseif strcmpi(defines.(defPrompt).Type, 'edit') + valNum = str2double(valStr); + if isnan(valNum) + config.(periph).Defines.(defPrompt).Default = valStr; + else + config.(periph).Defines.(defPrompt).Default = valNum; + end + end + end + end + end + end + end + + function config = read_config(blockPath) + mask = Simulink.Mask.get(blockPath); + + pathparam = mask.getParameter('periphPath'); + config_path = pathparam.Value; + + if ~isempty(config_path) + jsonText = fileread(config_path); + config = jsondecode(jsonText); + else + config = []; + end + end + + function write_config(config) + if isempty(config) + return + end + + blockHandle = gcbh; + mask = Simulink.Mask.get(blockHandle); + + pathparam = mask.getParameter('periphPath'); + config_path = pathparam.Value; + + jsonText = jsonencode(config, 'PrettyPrint', true); + fid = fopen(config_path, 'w', 'n', 'UTF-8'); + if fid == -1 + error('Не удалось открыть файл periph_config.json для записи.'); + end + fwrite(fid, jsonText, 'char'); + fclose(fid); + end + + function clear_all_from_container(mask, containerName) + % allControls = mask.getDialogControls(); + container = mask.getDialogControl(containerName); + if isempty(container) + warning('Контейнер "%s" не найден.', containerName); + return; + end + + % Рекурсивно собрать все параметры (не вкладки) + paramsToDelete = mcuMask.collect_all_parameters(container); + + % Удаляем все параметры + for i = 1:numel(paramsToDelete) + try + mask.removeParameter(paramsToDelete{i}); + catch + warning('Не удалось удалить параметр %s', paramsToDelete{i}); + end + end + + % Рекурсивно удалить все вкладки внутри контейнера + mcuMask.delete_all_tabs(mask, container); + end + + end + + methods(Static, Access=private) + + function res = addCodeConfig(codeConfig, periphPath) + % Возвращает 0 при успехе, 1 при ошибке + try + % Источники + srcList = {}; + if isfield(codeConfig, 'Sources') && isfield(codeConfig.Sources, 'Options') + srcFiles = codeConfig.Sources.Options; + for i = 1:numel(srcFiles) + fullPath = fullfile(periphPath, srcFiles{i}); + srcList{end+1} = [strrep(fullPath, '\', '\\')]; + end + end + + % Формируем srcText с переносами строк и ^ + srcText = ''; + for i = 1:numel(srcList) + if i < numel(srcList) + srcText = [srcText srcList{i} '^' newline ' ']; + else + srcText = [srcText srcList{i}]; + end + end + + % Инклуды + incList = {}; + if isfield(codeConfig, 'Includes') && isfield(codeConfig.Includes, 'Options') + incPaths = codeConfig.Includes.Options; + for i = 1:numel(incPaths) + fullPath = fullfile(periphPath, incPaths{i}); + incList{end+1} = ['-I"' strrep(fullPath, '\', '\\') '"']; + end + end + + % Формируем incText с переносами строк и ^ + incText = ''; + for i = 1:numel(incList) + if i == 1 && numel(incList) ~= 1 + incText = [incText incList{i} '^' newline]; + elseif i < numel(incList) + incText = [incText ' ' incList{i} '^' newline]; + else + incText = [incText ' ' incList{i}]; + end + end + + % Добавляем префиксы + srcText = ['set code_PERIPH' '=' srcText]; + incText = ['set includes_PERIPH' '=' incText]; + + % Записываем результат + res = periphConfig.updateRunMexBat(srcText, incText); % Всё прошло успешно + catch + % В случае ошибки просто возвращаем 1 + res = 1; + end + end + + + function res = addUserCodeConfig(userCodeConfig) + % userCodeConfig — структура config.UserCode + + initFuncsText = ''; + simFuncsText = ''; + deinitFuncsText = ''; + + if isfield(userCodeConfig, 'Functions') + funcs = userCodeConfig.Functions; + + if isfield(funcs, 'PeriphInit') && isfield(funcs.PeriphInit, 'Options') + initFuncs = funcs.PeriphInit.Options; + initFuncsText = strjoin(strcat('\t', initFuncs, ';'), '\n'); + end + + if isfield(funcs, 'PeriphSimulation') && isfield(funcs.PeriphSimulation, 'Options') + simFuncs = funcs.PeriphSimulation.Options; + simFuncsText = strjoin(strcat('\t', simFuncs, ';'), '\n'); + end + + if isfield(funcs, 'PeriphDeinit') && isfield(funcs.PeriphDeinit, 'Options') + deinitFuncs = funcs.PeriphDeinit.Options; + deinitFuncsText = strjoin(strcat('\t', deinitFuncs, ';'), '\n'); + end + + res = periphConfig.updateWrapperCode(initFuncsText, simFuncsText, deinitFuncsText); + end + end + + + function res = updateWrapperCode(initFuncsText, simFuncsText, deinitFuncsText) + % Входные параметры: + % srcText - текст для записи set code_... + % incText - текст для записи set includes_... + % + % Возвращает: + % res - 0 при успехе, 1 при ошибке + wrapPath = fullfile('.\MCU_Wrapper', 'mcu_wrapper.c'); + res = 1; + try + code = fileread(wrapPath); + code = regexprep(code, '\r\n?', '\n'); + + % Записываем строки initFuncsText и simFuncsText + code = editCode.insertSection(code, '// PERIPH INIT', initFuncsText); + code = editCode.insertSection(code, '// PERIPH SIM', simFuncsText); + code = editCode.insertSection(code, '// PERIPH DEINIT', deinitFuncsText); + + fid = fopen(wrapPath, 'w', 'n', 'UTF-8'); + if fid == -1 + error('Не удалось открыть файл для записи'); + end + fwrite(fid, code); + fclose(fid); + res = 1; + catch ME + error('Ошибка: неудачная запись в файл при записи файла: %s', ME.message); + end + end + + + + function res = updateRunMexBat(srcText, incText) + % Входные параметры: + % srcText - текст для записи set code_... + % incText - текст для записи set includes_... + % + % Возвращает: + % res - 0 при успехе, 1 при ошибке + periphBat = [srcText '\n\n' incText]; + batPath = fullfile('.\MCU_Wrapper', 'run_mex.bat'); + res = 1; + try + code = fileread(batPath); + code = regexprep(code, '\r\n?', '\n'); + + % Записываем строки srcText и incText с переносами строк + code = editCode.insertSection(code, ':: PERIPH BAT', periphBat); + + fid = fopen(batPath, 'w', 'n', 'UTF-8'); + if fid == -1 + error('Не удалось открыть файл для записи'); + end + fwrite(fid, code); + fclose(fid); + res = 1; + catch ME + mcuMask.disp(0, '\nОшибка: неудачная запись в файл при записи файла: %s', ME.message); + end + end + + + + function addDefineConfig(mask, containerName, periphName, defPrompt, def) + % mask — объект маски Simulink.Mask.get(blockPath) + % containerName — имя контейнера, в который добавляем параметр (например, 'configTabAll') + % periphName — имя вкладки / контейнера для текущего периферийного блока (например, 'ADC') + % defPrompt — имя параметра в Defines (например, 'shift_enable') + % def — структура с описанием параметра (Prompt, Def, Type, Default, NewRow и т.п.) + + % Найдем контейнер с таким именем + container = mask.getDialogControl(containerName); + if isempty(container) + error('Контейнер "%s" не найден в маске.', containerName); + end + + % Проверим, есть ли вкладка с именем periphName, если нет — создадим + tabCtrl = mask.getDialogControl(periphName); + if isempty(tabCtrl) + tabCtrl = container.addDialogControl('tab', periphName); + tabCtrl.Prompt = [periphName ' Config']; + end + + % Определяем тип параметра (checkbox или edit) + switch lower(def.Type) + case 'checkbox' + paramType = 'checkbox'; + case 'edit' + paramType = 'edit'; + otherwise + % Игнорируем остальные типы + return; + end + + paramName = matlab.lang.makeValidName(defPrompt); + + % Преобразуем значение Default в строку для Value + val = def.Default; + if islogical(val) + valStr = mcuMask.ternary(val, 'on', 'off'); + elseif isnumeric(val) + valStr = num2str(val); + elseif ischar(val) + valStr = val; + else + error('Unsupported default value type for %s.%s', periphName, defPrompt); + end + + % Добавляем параметр в маску + param = mask.addParameter( ... + 'Type', paramType, ... + 'Prompt', def.Prompt, ... + 'Name', paramName, ... + 'Value', valStr, ... + 'Container', periphName ... + ); + + param.Alias = def.Def; + param.Evaluate = 'off'; + + if def.NewRow + param.DialogControl.Row = 'new'; + else + param.DialogControl.Row = 'current'; + end + end + + + end +end diff --git a/McuLib/mcuwrapper.prj b/McuLib/mcuwrapper.prj new file mode 100644 index 0000000..e886622 --- /dev/null +++ b/McuLib/mcuwrapper.prj @@ -0,0 +1,123 @@ + + + mcuwrapper + Razvalyaev + wot890089@mail.ru + NIO-12 + Library for run MCU program in Simulink + + + 1.0 + ${PROJECT_ROOT}\mcuwrapper.mltbx + + + + + e7dd2564-e462-4878-b445-45763482263f + + true + + + + + + + + + false + + + findjobj - find java handles of Matlab graphic objects + + + + + + + false + true + true + true + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ${PROJECT_ROOT} + + + ${PROJECT_ROOT}\install_my_library.m + ${PROJECT_ROOT}\lib + ${PROJECT_ROOT}\m + ${PROJECT_ROOT}\sl_customization.m + ${PROJECT_ROOT}\slblocks.m + ${PROJECT_ROOT}\startup.m + ${PROJECT_ROOT}\templates + + + + + + E:\.WORK\MATLAB\matlab_23550\McuLib\mcuwrapper.mltbx + + + + C:\Program Files\MyProgs\MATLAB\R2023a + + + + false + false + true + false + false + false + false + false + 10.0 + false + true + win64 + true + + + \ No newline at end of file diff --git a/McuLib/sl_customization.m b/McuLib/sl_customization.m new file mode 100644 index 0000000..028950e --- /dev/null +++ b/McuLib/sl_customization.m @@ -0,0 +1,5 @@ +function sl_customization(cm) + % Добавим путь к коллбекам + addpath(fullfile(fileparts(mfilename('fullpath')), 'm')); + addpath(fullfile(fileparts(mfilename('fullpath')), 'lib')); +end \ No newline at end of file diff --git a/McuLib/slblocks.m b/McuLib/slblocks.m new file mode 100644 index 0000000..a3c0eb9 --- /dev/null +++ b/McuLib/slblocks.m @@ -0,0 +1,5 @@ +function blkStruct = slblocks + blkStruct.Browser.Library = 'McuLib'; + blkStruct.Browser.Name = 'MCU Wrapper'; + blkStruct.Browser.IsFlat = false; +end \ No newline at end of file diff --git a/McuLib/startup.m b/McuLib/startup.m new file mode 100644 index 0000000..640df74 --- /dev/null +++ b/McuLib/startup.m @@ -0,0 +1,2 @@ +% startup.m +install_my_library; diff --git a/McuLib/templates/MCU_Wrapper/mcu_wrapper.c b/McuLib/templates/MCU_Wrapper/mcu_wrapper.c new file mode 100644 index 0000000..16014bc --- /dev/null +++ b/McuLib/templates/MCU_Wrapper/mcu_wrapper.c @@ -0,0 +1,250 @@ +/** +************************************************************************** +* @file mcu_wrapper.c +* @brief Исходный код оболочки МК. +************************************************************************** +@details +Данный файл содержит функции для симуляции МК в Simulink (S-Function). +**************************************************************************/ +#include "mcu_wrapper_conf.h" +#include "app_wrapper.h" + +/** + * @addtogroup WRAPPER_CONF + * @{ + */ + +SIM__MCUHandleTypeDef hmcu; ///< Хендл для управления потоком программы МК + +// INPUT/OUTPUTS AUTO-PARAMS START + +// INPUT/OUTPUTS AUTO-PARAMS END + +/** MCU_WRAPPER + * @} + */ + //-------------------------------------------------------------// + //-----------------CONTROLLER SIMULATE FUNCTIONS---------------// + /* THREAD FOR MCU APP */ +#ifdef RUN_APP_MAIN_FUNC_THREAD +/** + * @brief Главная функция приложения МК. + * @details Функция с которой начинается выполнение кода МК. Выход из данной функции происходит только в конце симуляции @ref mdlTerminate + */ +extern int main(void); // extern while from main.c +/** + * @brief Поток приложения МК. + * @details Поток, который запускает и выполняет код МК (@ref main). + */ +unsigned __stdcall MCU_App_Thread(void) { + main(); // run MCU code + return 0; // end thread + // note: this return will reached only at the end of simulation, when all whiles will be skipped due to @ref sim_while +} +#endif //RUN_APP_MAIN_FUNC_THREAD +/* SIMULATE MCU FOR ONE SIMULATION STEP */ +/** + * @brief Симуляция МК на один такт симуляции. + * @param S - указатель на структуру S-Function из "simstruc.h" + * @param time - текущее время симуляции. + * @details Запускает поток, который выполняет код МК и управляет ходом потока: + * Если прошел таймаут, поток прерывается, симулируется периферия + * и на следующем шаге поток возобнавляется. + * + * Вызывается из mdlUpdate() + */ +void MCU_Step_Simulation(SimStruct* S, time_T time) +{ + hmcu.SystemClockDouble += hmcu.sSystemClock_step; // emulate core clock + hmcu.SystemClock = hmcu.SystemClockDouble; + hmcu.SimTime = time; + + MCU_readInputs(S); // считывание портов + + MCU_Periph_Simulation(S); // simulate peripheral + +#ifdef RUN_APP_MAIN_FUNC_THREAD + ResumeThread(hmcu.hMCUThread); + for (int i = DEKSTOP_CYCLES_FOR_MCU_APP; i > 0; i--) + { + } + SuspendThread(hmcu.hMCUThread); +#else + app_step(); +#endif //RUN_APP_MAIN_FUNC_THREAD + + MCU_writeOutputs(S); // запись портов (по факту запись в буфер. запись в порты в mdlOutputs) +} + +/* SIMULATE MCU PERIPHERAL */ +/** + * @brief Симуляция периферии МК + * @details Пользовательский код, который симулирует работу периферии МК. + */ +void MCU_Periph_Simulation(SimStruct* S) +{ +// PERIPH SIM START + +// PERIPH SIM END +} + +/* READ INPUTS S-FUNCTION TO MCU REGS */ +/** + * @brief Считывание входов S-Function в порты ввода-вывода. + * @param S - указатель на структуру S-Function из "simstruc.h" + * @details Пользовательский код, который записывает входы МК из входов S-Function. + */ +void MCU_readInputs(SimStruct* S) +{ + SIM_readInputs(S); + /* Get S-Function descrete array (IO buffer) */ + real_T* In_Buff = ssGetDiscStates(S); + app_readInputs(In_Buff); +} + +/* WRITE OUTPUTS BUFFER S-FUNCTION FROM MCU REGS*/ +/** + * @brief Запись портов ввода-вывода в буфер выхода S-Function + * @param S - указатель на структуру S-Function из "simstruc.h" + * @details Пользовательский код, который записывает буфер выходов S-Function из портов ввода-вывода. + */ +void MCU_writeOutputs(SimStruct* S) +{ + /* Get S-Function descrete array (IO buffer) */ + real_T* Out_Buff = ssGetDiscStates(S); + + app_writeOutputBuffer(Out_Buff); +} +//-----------------CONTROLLER SIMULATE FUNCTIONS---------------// +//-------------------------------------------------------------// + + + +//-------------------------------------------------------------// +//----------------------SIMULINK FUNCTIONS---------------------// +/* MCU WRAPPER DEINITIALIZATION */ +/** + * @brief Инициализация симуляции МК. + * @details Пользовательский код, который создает поток для приложения МК + и настраивает симулятор МК для симуляции. + */ +void SIM_Initialize_Simulation(SimStruct* S) +{ +#ifdef RUN_APP_MAIN_FUNC_THREAD + // инициализация потока, который будет выполнять код МК + hmcu.hMCUThread = (HANDLE)CreateThread(NULL, 0, MCU_App_Thread, 0, CREATE_SUSPENDED, &hmcu.idMCUThread); +#endif //RUN_APP_MAIN_FUNC_THREAD + + /* user initialization */ + app_init(); +// PERIPH INIT START + +// PERIPH INIT END + + /* clock step initialization */ + hmcu.sSystemClock_step = MCU_CORE_CLOCK * hmcu.sSimSampleTime; // set system clock step + hmcu.fInitDone = 1; +} +/* MCU WRAPPER DEINITIALIZATION */ +/** + * @brief Деинициализация симуляции МК. + * @details Пользовательский код, который будет очищать все структуры после окончания симуляции. + */ +void SIM_deInitialize_Simulation(SimStruct* S) +{ +#ifdef DEINITIALIZE_AFTER_SIM + // deinitialize app + app_deinit(); +// PERIPH DEINIT START + +// PERIPH DEINIT END +#endif// DEINITIALIZE_AFTER_SIM +} +/* WORK WITH IN/OUT BUFFER OF S-BLOCK */ + +/** + * @brief Функция для записи переменной в буфер выходов в определенный массив + * @param xD - указатель на буфер состояний + * @param value - значение для записи + * @param array_index - индекс выходного массива + * @param value_index - индекс внутри массива + */ +void __WriteOutputArray(real_T* xD, float value, int array_index, int value_index) +{ + if (array_index >= OUT_PORT_NUMB) + return; + + if (value_index >= outLengths[array_index]) + return; + + int global_index = XD_OUTPUT_START + outOffsets[array_index] + value_index; + xD[global_index] = value; +} + +/** + * @brief Функция для чтения значения из буфера входов из определенного массива + * @param xD - указатель на буфер состояний + * @param array_index - индекс входного массива + * @param value_index - индекс внутри массива + * @return - считанное значение или 0.0 при выходе за границы + */ +float __ReadInputArray(const real_T* xD, int array_index, int value_index) +{ + if (array_index >= IN_PORT_NUMB) + return 0.0f; + + if (value_index >= inLengths[array_index]) + return 0.0f; + + int global_index = XD_INPUT_START + inOffsets[array_index] + value_index; + return xD[global_index]; +} + +/** + * @brief Формирование выходов S-Function. + * @param S - указатель на структуру S-Function из "simstruc.h" + * @details Пользовательский код, который записывает выходы S-Function из буфера дискретных состояний. + */ +void SIM_writeOutputs(SimStruct* S) +{ + real_T* Output = ssGetOutputPortRealSignal(S,0); + real_T* Out_Buff = ssGetDiscStates(S); + int global_index; + + //-------------WRITTING OUTPUT-------------- + for (int j = 0; j < OUT_PORT_NUMB; j++) + { + Output = ssGetOutputPortRealSignal(S, j); + for (int i = 0; i < outLengths[i]; i++) + { + global_index = XD_OUTPUT_START + outOffsets[j] + i; + Output[i] = Out_Buff[global_index]; + Out_Buff[global_index] = 0; + } + } + //------------------------------------------ +} +/** + * @brief Формирование входов S-Function. + * @param S - указатель на структуру S-Function из "simstruc.h" + * @details Пользовательский код, который считывает входы S-Function в буфер дискретных состояний. + */ +void SIM_readInputs(SimStruct* S) +{ + real_T* Input = ssGetInputPortRealSignal(S, 0); + real_T* In_Buff = ssGetDiscStates(S); + int global_index; + + //-------------READING INPUTS--------------- + for (int j = 0; j < IN_PORT_NUMB; j++) + { + Input = ssGetInputPortRealSignal(S, j); + for (int i = 0; i < inLengths[j]; i++) + { + global_index = XD_INPUT_START + inOffsets[j] + i; + In_Buff[global_index] = Input[i]; + } + } + //------------------------------------------ +} +//-------------------------------------------------------------// diff --git a/McuLib/templates/MCU_Wrapper/mcu_wrapper_conf.h b/McuLib/templates/MCU_Wrapper/mcu_wrapper_conf.h new file mode 100644 index 0000000..a40d8fb --- /dev/null +++ b/McuLib/templates/MCU_Wrapper/mcu_wrapper_conf.h @@ -0,0 +1,219 @@ +/** +************************************************************************** +* @dir ../MCU_Wrapper +* @brief Папка с исходным кодом оболочки МК. +* @details +В этой папке содержаться оболочка(англ. wrapper) для запуска и контроля +эмуляции микроконтроллеров в MATLAB (любого МК, не только STM). +Оболочка представляет собой S-Function - блок в Simulink, который работает +по скомпилированому коду. Компиляция происходит с помощью MSVC-компилятора. +**************************************************************************/ + +/** +************************************************************************** +* @file mcu_wrapper_conf.h +* @brief Заголовочный файл для оболочки МК. +************************************************************************** +@details +Главный заголовочный файл для матлаба. Включает дейфайны для S-Function, +объявляет базовые функции для симуляции МК и подключает базовые библиотеки: +- для симуляции "stm32fxxx_matlab_conf.h" +- для S-Function "simstruc.h" +- для потоков +**************************************************************************/ +#ifndef _WRAPPER_CONF_H_ +#define _WRAPPER_CONF_H_ + +// Includes +#include "simstruc.h" // For S-Function variables +#include // For threads + +#include "app_includes.h" + + +/** + * @defgroup MCU_WRAPPER MCU Wrapper + * @brief Всякое для оболочки МК + */ + +/** + * @addtogroup WRAPPER_CONF Wrapper Configuration + * @ingroup MCU_WRAPPER + * @brief Параметры конфигурации для оболочки МК + * @details Здесь дефайнами задается параметры оболочки, которые определяют как она будет работать + * @{ + */ + +// Parametrs of MCU simulator +//#define RUN_APP_MAIN_FUNC_THREAD ///< Enable using thread for MCU main() func +//#define DEKSTOP_CYCLES_FOR_MCU_APP 0xFFFF ///< number of for() cycles after which MCU thread would be suspended +//#define MCU_CORE_CLOCK 150000000 ///< MCU clock rate for simulation + +// Parameters of S_Function +// INPUT/OUTPUTS PARAMS START + +// INPUT/OUTPUTS PARAMS END +/** WRAPPER_CONF + * @} + */ + + + +/** + * @addtogroup MCU_WRAPPER + * @{ + */ + +/** @brief Записывает значение в выходной массив блока S-Function + * @param _var_ Значение, которое необходимо записать (будет преобразовано в float) + * @param _arr_ind_ Индекс выходного порта + * @param _val_ind_ Индекс элемента в выходном массиве + */ +#define WriteOutputArray(_var_, _arr_ind_, _val_ind_) __WriteOutputArray(Buffer, (float)_var_, _arr_ind_, _val_ind_) + +/** @brief Считывает значение из входного массива блока S-Function + * @param _var_ Значение, которое необходимо записать (будет преобразовано в float) + * @param _arr_ind_ Индекс входного порта + * @param _val_ind_ Индекс элемента во входном массиве + */ +#define ReadInputArray(_arr_ind_, _val_ind_) __ReadInputArray(Buffer, _arr_ind_, _val_ind_) + + + +// INPUT/OUTPUTS AUTO-PARAMS START + +// INPUT/OUTPUTS AUTO-PARAMS END + +extern const int outLengths[OUT_PORT_NUMB]; +extern const int outOffsets[OUT_PORT_NUMB]; +extern const int inLengths[IN_PORT_NUMB]; +extern const int inOffsets[IN_PORT_NUMB]; +#define TOTAL_XD_SIZE (TOTAL_IN_SIZE + TOTAL_OUT_SIZE) +#define XD_INPUT_START 0 +#define XD_OUTPUT_START (XD_INPUT_START + TOTAL_IN_SIZE) + + + + + +// Fixed parameters(?) of S_Function +#define NPARAMS 1 ///< number of input parametrs (only Ts) +#define DISC_STATES_WIDTH TOTAL_XD_SIZE ///< width of discrete states array (outbup buffer) + + + +/** + * @brief Define for creating thread in suspended state. + * @details Define from WinBase.h. We dont wanna include "Windows.h" or smth like this, because of HAL there are a lot of redefine errors. + */ +#define CREATE_SUSPENDED 0x00000004 +typedef void* HANDLE; ///< MCU handle typedef + +/** + * @brief MCU handle Structure definition. + * @note Prefixes: h - handle, s - settings, f - flag + */ +typedef struct { + // MCU Thread + HANDLE hMCUThread; ///< Хендл для потока МК + int idMCUThread; ///< id потока МК (unused) + // Flags + unsigned fMCU_Stop : 1; ///< флаг для выхода из потока программы МК + unsigned fInitDone : 1; ///< флаг для выхода из потока программы МК + + double SimTime; ///< Текущее время симуляции + long SystemClock; ///< Счетчик тактов для симуляции системных тиков (в целочисленном формате) + + double SystemClockDouble; ///< Счетчик в формате double для точной симуляции системных тиков С промежуточными значений + double sSystemClock_step; ///< Шаг тиков для их симуляции, в формате double + double sSimSampleTime; ///< Период дискретизации симуляции +}SIM__MCUHandleTypeDef; +extern SIM__MCUHandleTypeDef hmcu; // extern для видимости переменной во всех файлах + +//-------------------------------------------------------------// +//------------------ SIMULINK WHILE DEFINES -----------------// +#ifdef RUN_APP_MAIN_FUNC_THREAD +/* DEFINE TO WHILE WITH SIMULINK WHILE */ +/** + * @brief Redefine C while statement with sim_while() macro. + * @param _expression_ - expression for while. + * @details Это while который будет использоваться в симулинке @ref sim_while для подробностей. + */ +#define while(_expression_) sim_while(_expression_) +#endif + +/* SIMULINK WHILE */ +/** + * @brief While statement for emulate MCU code in Simulink. + * @param _expression_ - expression for while. + * @details Данный while необходим, чтобы в конце симуляции, завершить поток МК: + * При выставлении флага окончания симуляции, все while будут пропускаться + * и поток сможет дойти до конца функции main и завершить себя. + */ +#define sim_while(_expression_) while((_expression_)&&(hmcu.fMCU_Stop == 0)) + +/* DEFAULT WHILE */ +/** + * @brief Default/Native C while statement. + * @param _expression_ - expression for while. + * @details Данный while - аналог обычного while, без дополнительного функционала. + */ +#define native_while(_expression_) for(; (_expression_); ) + /***************************************************************/ + +//------------------ SIMULINK WHILE DEFINES -----------------// +//-------------------------------------------------------------// + + + +//-------------------------------------------------------------// +//---------------- SIMULATE FUNCTIONS PROTOTYPES -------------// +/* Step simulation */ +void MCU_Step_Simulation(SimStruct *S, time_T time); + +/* MCU peripheral simulation */ +void MCU_Periph_Simulation(SimStruct* S); + +/* Initialize MCU simulation */ +void SIM_Initialize_Simulation(SimStruct* S); + +/* Deinitialize MCU simulation */ +void SIM_deInitialize_Simulation(SimStruct* S); + +/* Read inputs S-function */ +void MCU_readInputs(SimStruct* S); + +/* Write pre-outputs S-function (out_buff states) */ +void MCU_writeOutputs(SimStruct* S); + +/* Write outputs of block of S-Function*/ +void SIM_writeOutputs(SimStruct* S); + +/* Write inputs of block of S-Function*/ +void SIM_readInputs(SimStruct* S); + +/* Set output of block of S-Function*/ +void __WriteOutputArray(real_T* xD, float value, int array_index, int value_index); + +/* Get input of block of S-Function*/ +float __ReadInputArray(const real_T* xD, int array_index, int value_index); +//---------------- SIMULATE FUNCTIONS PROTOTYPES -------------// +//-------------------------------------------------------------// + +/** MCU_WRAPPER + * @} + */ +#endif // _WRAPPER_CONF_H_ + + +//-------------------------------------------------------------// +//---------------------BAT FILE DESCRIBTION--------------------// +/** + * @file run_mex.bat + * @brief Батник для компиляции оболочки МК. + * @details + * Вызывается в матлабе из allmex.m. + * + * Исходный код батника: + * @include run_mex.bat + */ \ No newline at end of file diff --git a/McuLib/templates/MCU_Wrapper/run_mex.bat b/McuLib/templates/MCU_Wrapper/run_mex.bat new file mode 100644 index 0000000..7c6316c --- /dev/null +++ b/McuLib/templates/MCU_Wrapper/run_mex.bat @@ -0,0 +1,115 @@ +@echo off +:: Получаем аргументы из командной строки +:: %1 - includes_USER +:: %2 - code_USER +:: %3 - режим (например, debug) + +:: Аргументы: +:: %1 — includes строка (в кавычках) +:: %2 — sources строка +:: %3 — defines строка +:: %4 — режим компиляции (debug/release) + +:: Сохраняем как переменные +set includes_USER=%~1 +set code_USER=%~2 +set defines_USER=%~3 +set defines_CONFIG=%~4 +set compil_mode=%~5 + +:: Заменяем __EQ__ на = +set defines_USER=%defines_USER:__EQ__==% +set defines_CONFIG=%defines_CONFIG:__EQ__==% + + +set defines_WRAPPER=-D"MATLAB"^ -D"__sizeof_ptr=8" +:: -------------------------USERS PATHS AND CODE--------------------------- +::------------------------------------------------------------------------- + + +:: -------------------------WRAPPER PATHS AND CODE--------------------------- +:: оболочка, которая будет моделировать работу МК в симулинке +set includes_WRAPPER=-I"."^ + -I".\MCU_Wrapper"^ + -I".\app_wrapper" + +set code_WRAPPER= .\MCU_Wrapper\MCU.c^ + .\MCU_Wrapper\mcu_wrapper.c^ + .\app_wrapper\app_init.c^ + .\app_wrapper\app_io.c^ + .\app_wrapper\app_wrapper.c + +:: PERIPH BAT START + +:: PERIPH BAT END +::------------------------------------------------------------------------- + + +:: ---------------------SET PARAMS FOR MEX COMPILING----------------------- +:: -------------ALL------------ +set includes= %includes_WRAPPER% %includes_PERIPH% %includes_USER% +set codes= %code_WRAPPER% %code_PERIPH% %code_USER% +set defines= %defines_WRAPPER% %defines_CONFIG% %defines_USER% +:: -------OUTPUT FOLDER-------- +set output= -outdir "." + +:: если нужен дебаг, до запускаем run_mex с припиской debug +IF [%1]==[debug] (set debug= -g) +::------------------------------------------------------------------------- + + +::------START COMPILING------- +if "%6"=="echo_enable" ( + echo Compiling... + + echo =========================== + echo =========INCLUDES========== + echo USER: + for %%f in (%includes_USER%) do ( + echo %%f + ) + echo INTERNAL: + for %%f in (%includes_WRAPPER%) do ( + echo %%f + ) + echo PERIPH: + for %%f in (%includes_PERIPH%) do ( + echo %%f + ) + + echo =========================== + echo ==========SOURCES========== + echo USER: + for %%f in (%code_USER%) do ( + echo %%f + ) + echo INTERNAL: + for %%f in (%code_WRAPPER%) do ( + echo %%f + ) + echo PERIPH: + for %%f in (%code_PERIPH%) do ( + echo %%f + ) + + echo =========================== + echo ==========DEFINES========== + echo USER: + for %%d in (%defines_USER%) do ( + echo %%d + ) + echo CONFIG: + for %%f in (%defines_CONFIG%) do ( + echo %%f + ) + echo INTERNAL: + for %%f in (%defines_WRAPPER%) do ( + echo %%f + ) +) +echo =========================== +echo MODE: %compil_mode% +echo =========================== +mex %output% %defines% %includes% %codes% %debug% +echo %DATE% %TIME% +exit /b %ERRORLEVEL% \ No newline at end of file diff --git a/McuLib/templates/app_wrapper/app_configs.h b/McuLib/templates/app_wrapper/app_configs.h new file mode 100644 index 0000000..5665abe --- /dev/null +++ b/McuLib/templates/app_wrapper/app_configs.h @@ -0,0 +1,10 @@ +/** +************************************************************************** +* @file app_config.h +* @brief . +**************************************************************************/ +#ifndef _APP_CONFIG +#define _APP_CONFIG + + +#endif //_APP_CONFIG diff --git a/McuLib/templates/app_wrapper/app_includes.h b/McuLib/templates/app_wrapper/app_includes.h new file mode 100644 index 0000000..1d729f1 --- /dev/null +++ b/McuLib/templates/app_wrapper/app_includes.h @@ -0,0 +1,15 @@ +/** +************************************************************************** +* @file app_includes.h +* @brief Заголовочный файл для подключаения заголовочных файлов программы МК. +**************************************************************************/ +#ifndef _APP_INCLUDES_H_ +#define _APP_INCLUDES_H_ + +#include "app_configs.h" + +// INCLUDES START +// Инклюды для доступа к коду МК в коде оболочке +// INCLUDES END + +#endif //_APP_INCLUDES_H_ \ No newline at end of file diff --git a/McuLib/templates/app_wrapper/app_init.c b/McuLib/templates/app_wrapper/app_init.c new file mode 100644 index 0000000..2509cf7 --- /dev/null +++ b/McuLib/templates/app_wrapper/app_init.c @@ -0,0 +1,33 @@ +/** +************************************************************************** +* @file app_init.h +* @brief Файл с функцией инициализации программы МК @ref app_init. +**************************************************************************/ +#include "mcu_wrapper_conf.h" +#include "app_wrapper.h" + +/** + * @brief Функция для инициализации приложения МК + * @details Используется в случае симуляции без отдельного потока для main(). + */ +void app_init(void) { +// USER APP INIT START +// Код для инициализации приложения МК +// +// Вызов разных функций в случае, +// если не используется отдельный поток для main(). +// USER APP INIT END +} + + +/** + * @brief Функция для деинициализации приложения МК + */ +void app_deinit(void) { +// USER APP DEINIT START +// Код для деинициализации приложения МК +// +// Структуры, переменные и так далее, которые надо очистить, +// для повторного запуска симуляции. +// USER APP DEINIT END +} \ No newline at end of file diff --git a/McuLib/templates/app_wrapper/app_io.c b/McuLib/templates/app_wrapper/app_io.c new file mode 100644 index 0000000..40c7a6f --- /dev/null +++ b/McuLib/templates/app_wrapper/app_io.c @@ -0,0 +1,50 @@ +/** +************************************************************************** +* @file app_init.h +* @brief Файл с функциями записи входов/выходов программы МК @ref app_init. +**************************************************************************/ +#include "mcu_wrapper_conf.h" +#include "app_wrapper.h" + +/** + * @brief Функция для записи входов в приложение МК + * @param u - массив входных значений + */ +void app_readInputs(const real_T* Buffer) { +// USER APP INPUT START +// Код для записи считывания входов из IO буфера +// Буфер в начале хранит входные порты S-Function, далее идут выходные порты: +// Buffer[0:15] - входной 1 порт, Buffer[16:31] - входной 2 порт, +// Buffer[32:47] - выходной 1 порт, Buffer[48:63] - выходной 2 порт +// +// Note: используте для чтения: +// val = ReadInputArray(arr_ind, val_ind) +// Пример: +// // запись в второй элемент первого массива +// app_variable = ReadInputArray(0, 1); +// // чтение из IO буфера напрямую +// app_variable_2 = Buffer[10]; +// USER APP INPUT END +} + +/** + * @brief Функция для записи выходов приложения МК + * @param xD - массив буффера выходов(дискретных выходов) + * @details Используте WriteOutputArray(val, arr_ind, val_ind) для записи + */ +void app_writeOutputBuffer(real_T* Buffer) { +// USER APP OUTPUT START +// Код для записи выходов в IO буфер +// Буфер в начале хранит входные порты S-Function, далее идут выходные порты: +// Buffer[0:15] - входной 1 порт, Buffer[16:31] - входной 2 порт, +// Buffer[32:47] - выходной 1 порт, Buffer[48:63] - выходной 2 порт +// +// Note: используте для записи: +// WriteOutputArray(val, arr_ind, val_ind) +// Пример: +// // запись в второй элемент первого массива +// WriteOutputArray(app_variable, 0, 1); +// // запись в IO буфер напрямую +// Buffer[XD_OUTPUT_START + 10] = app_variable_2; +// USER APP OUTPUT END +} \ No newline at end of file diff --git a/McuLib/templates/app_wrapper/app_wrapper.c b/McuLib/templates/app_wrapper/app_wrapper.c new file mode 100644 index 0000000..add68e4 --- /dev/null +++ b/McuLib/templates/app_wrapper/app_wrapper.c @@ -0,0 +1,22 @@ +#include "mcu_wrapper_conf.h" +#include "app_wrapper.h" + + +/** + * @brief Функция для симуляции шага приложения МК + * @details Используется в случае симуляции без отдельного потока для main(). + */ +void app_step(void) +{ +// USER APP STEP START +// Код приложения МК для вызова в шаге симуляции +// +// Вызов разных функций на шаге симуляции в случае, +// если не используется отдельный поток для main(). +// USER APP STEP END +} + + +// DUMMY START +// Заглушки для различных функций и переменных +// DUMMY END \ No newline at end of file diff --git a/McuLib/templates/app_wrapper/app_wrapper.h b/McuLib/templates/app_wrapper/app_wrapper.h new file mode 100644 index 0000000..7a8022a --- /dev/null +++ b/McuLib/templates/app_wrapper/app_wrapper.h @@ -0,0 +1,12 @@ +#ifndef _APP_WRAPPER_H_ +#define _APP_WRAPPER_H_ + +#include "app_includes.h" + +void app_step(void); +void app_init(void); +void app_deinit(void); +void app_readInputs(const real_T* u); +void app_writeOutputBuffer(real_T* xD); + +#endif //_APP_WRAPPER_H_ diff --git a/mcuwrapper.mltbx b/mcuwrapper.mltbx new file mode 100644 index 0000000000000000000000000000000000000000..584d9450a7cf77a0a0b04c696dfaac3b52979222 GIT binary patch literal 73515 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�&YeV+`ZeYnC{ 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

d&mHN%9#H*YW^ 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 z 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